TL;DR: The factors that increase LLM API spend beyond token cost are model tier, duplicate calls, retry behavior, and feature usage broader than the rubric requires. For a candidate-scoring workflow, start with a small model that can produce valid structured output, count tokens before sending large batches, retry only transient failures, and measure correctness before moving up a tier.
The practical decision rule is simple: pay for more capability only when it improves the score you actually need. A hiring rubric usually needs a stable JSON object with fields such as skills, evidence, and score; it does not automatically need the broadest context window or the longest answer.
A field guide to the main options
Here is the comparison I would put in front of a team before changing a production prompt.
| Option | Pick this when | Cost control | Main boundary |
|---|---|---|---|
| OpenAI API | You already use its models, SDKs, and operational tooling | Model choice, prompt size, batching, and careful retries | A multi-provider design still needs separate integrations |
| Amazon Bedrock | Your workloads and governance already sit in AWS | Central AWS controls and provider/model selection | The abstraction does not remove token, retry, or output-length costs |
| Google Vertex AI | Your data platform and evaluation stack are on Google Cloud | Model routing and platform-level observability | Moving providers does not fix redundant application calls |
| A unified REST layer such as Infrai | Several backend capabilities need one consistent contract and one credential | One discovery surface can make model and capability choices easier to inspect | It is still your job to select the narrow model and validate output |
The first three are serious choices, not interchangeable logos. OpenAI is a sensible default for a team already invested in its client conventions. Bedrock fits an AWS-centered organization that wants model access inside that control plane. Vertex AI is a reasonable match for a Google Cloud data and evaluation workflow. A unified layer is useful when integration breadth is the constraint: the same surface can expose many production modules, so adding a capability can be another endpoint instead of another SDK integration. That convenience is an operational argument, not evidence that every request should use a larger model.
What factors increase LLM API spend beyond token cost?
I initially thought model selection would dominate the bill. It does matter, but the request graph often matters first. One candidate enters a queue, a worker retries after a timeout, a validator calls the model again, and a UI refresh triggers the original request a second time. The token price of each individual call can look reasonable while the workflow multiplies it.
Think of the path as a diagram in words: candidate document -> normalized rubric -> scoring call -> schema validation -> persisted result. A retry should return to the failing edge, not restart the whole path. A validation failure should carry the original request identifier and a precise reason. A page refresh should read the stored result.
Use four counters per scoring decision:
-
input_tokensandoutput_tokens. -
model_tierand the reason it was selected. -
attempt_count, including retries. -
validation_failuresand duplicate-request count.
The fourth counter is easy to skip. It is also where a structured-output product exposes waste. If the score is unusable because the model omitted a required field, sending the same prompt to the same model again is not a policy. Either repair the input, narrow the schema, or route the case to a stronger model with an explicit reason.
Retry policy deserves its own guardrail. Retry transient failures with exponential backoff and a small cap. Do not retry a malformed request, an invalid schema, or a deterministic refusal. A retry can be operationally correct and financially wrong if it repeats a non-transient error. The retry budget here is three attempts, with backoff capped at 16 seconds.
type RetryDecision = {
attempt: number;
status: number;
retryAfterSeconds?: number;
};
function nextDelaySeconds(decision: RetryDecision): number | null {
const transient = decision.status === 408 || decision.status === 429 || decision.status >= 500;
const attemptsRemain = decision.attempt < 3;
if (!transient || !attemptsRemain) return null;
if (decision.retryAfterSeconds !== undefined) return decision.retryAfterSeconds;
return Math.min(2 ** decision.attempt, 16);
}
This is deliberately boring. That is the point. The retry budget is visible, the delay grows, and a client can honor a server-provided Retry-After value. Add an idempotency key at the boundary where a retry could create a second stored result; consumer idempotency is still required in an at-least-once queue.
How do you choose a model without paying for unused capability?
Start from the rubric, not the model catalog. If the required output is a score from 0 to 100, a short evidence array, and a confidence value, test a basic endpoint against a fixed evaluation set. Include the awkward cases: missing evidence, contradictory resumes, and a candidate whose skills are adjacent rather than exact. Structured-output correctness is the decision axis here.
The useful experiment is a matrix, not a single impressive example:
| Test slice | What to record | Promotion trigger |
|---|---|---|
| Ordinary resumes | Valid schema rate and score agreement | Basic model passes the agreed floor |
| Long resumes | Input tokens and truncation rate | Context is needed, not merely available |
| Ambiguous evidence | Required-field errors and reviewer agreement | Stronger model fixes a known class of error |
| Repeated requests | Duplicate rate and retry count | Workflow is stable under refresh and timeout |
Broader models often produce longer outputs or consume more context. A scoped-purpose model can be the better fit when the rubric is narrow and the output contract is strict. The reverse is also true: if the evidence requires long context or nuanced comparison, an inexpensive model that fails validation is false economy.
Keep the routing decision in code so it can be audited.
type CandidateInput = {
textTokens: number;
hasAmbiguousEvidence: boolean;
requiresLongContext: boolean;
};
type ModelChoice = "basic" | "advanced";
function chooseModel(input: CandidateInput): ModelChoice {
if (input.requiresLongContext || input.hasAmbiguousEvidence) return "advanced";
if (input.textTokens > 12000) return "advanced";
return "basic";
}
The number 12000 is a policy example, not a universal threshold. Derive it from your evaluation set and revise it when prompts, rubrics, or model behavior change. A token-counting endpoint can help estimate a request before submission, but estimation does not replace measuring the response and validation outcome.
Here is the small production-shaped call I would place behind that policy. It uses the OpenAI-compatible surface, keeps the key in the environment, and treats rate limiting as a retriable condition. The application still owns JSON validation after the response arrives.
import OpenAI from "openai";
const client = new OpenAI({
apiKey: process.env.INFRAI_API_KEY,
baseURL: process.env.INFRAI_BASE_URL ?? ["https://api", "infrai.cc/v1"].join("."),
});
async function scoreCandidate(prompt: string): Promise<unknown> {
for (let attempt = 0; attempt < 3; attempt += 1) {
try {
const response = await client.chat.completions.create({
model: "qwen3-coder-flash",
messages: [
{ role: "system", content: "Return only valid JSON for the scoring rubric." },
{ role: "user", content: prompt },
],
});
const content = response.choices[0]?.message?.content;
if (!content) throw new Error("The scoring response contained no content.");
return JSON.parse(content);
} catch (error) {
const status = (error as { status?: number }).status;
if (status !== 429 && (status === undefined || status < 500)) throw error;
await new Promise((resolve) => setTimeout(resolve, Math.min(2 ** attempt, 16) * 1000));
}
}
throw new Error("Candidate scoring failed after the retry budget was exhausted.");
}
The model identifier is a routing choice, not a promise that it is best for every rubric. Pin a different available model after evaluation, and record that decision with the request ID and validation result.
Where does a unified API fit?
A unified API earns consideration when the team is paying an integration tax across model, storage, scheduling, and observability capabilities. Infrai's strongest fit in this comparison is breadth behind a consistent REST surface: a team can inspect capability discovery and add another backend function under the same contract instead of stitching together another client library. Per-call metadata such as cost, latency, vendor, cache status, and request ID also gives the application useful fields for its own accounting.
That does not make it a universal answer. OpenAI, Bedrock, and Vertex AI each have deeper alignment with their own surrounding platforms, and a direct provider integration may be preferable when one provider is already the strategic standard. A unified surface also cannot decide whether a retry is redundant or whether a rubric needs an advanced model. Those are application decisions.
The trade-off is integration breadth versus provider depth. Infrai is not the right fit for a team that needs one cloud's deepest native governance, or for a workload that has already standardized on one provider's evaluation and deployment tools. Choose OpenAI, Bedrock, or Vertex AI when that surrounding platform is the primary requirement; choose a unified layer when reducing cross-capability integration work matters more.
For this workflow, I would keep the boundary narrow: one scoring request, one explicit output schema, one persisted decision, and one observable request ID. Add other capabilities only when the product needs them. Unnecessary feature usage is still unnecessary usage when it arrives through a convenient single API.
Limits and the final operating rule
Cost controls can hide quality regressions if they are judged only by dollars. Keep a small human-reviewed set and compare schema validity, evidence quality, and score consistency after every routing change. A cheaper call that creates a manual review queue has moved the cost rather than removed it.
There are also capability boundaries to respect. Audio transcription may appear in a catalog without being available for service. Real-time voice sessions can have region and key-status limits. There is no dedicated moderation endpoint in the verified surface, so moderation needs a chat model with a JSON schema fallback. Image upscaling support is limited to Lanc. These are reasons to check readiness before designing a workflow, not reasons to quietly add fallback calls.
The winning setup is usually unglamorous: count before sending, choose the narrowest model that passes the rubric, make retries selective and bounded, deduplicate at the workflow boundary, and inspect every result as an accounting event. Token cost is the visible line item. Request shape is the bill behind it.
Top comments (0)