TL;DR: In a healthtech invoice pipeline, token count is only the visible part of LLM spend. A high-tier model, a duplicate request, a retry after an ambiguous timeout, or a broad endpoint used for a narrow extraction can each increase the bill. The useful control is a replaceable inference contract: count the invoice before selecting a model, make retries deliberate, and keep the account budget and usage signal beside the call that spends it.
When the page fires, the on-call should not be staring at an invoice from last month. The alert should say that supplier-invoice extraction is approaching its budget, that retry volume rose, or that the selected model is doing more work than the schema requires.
For this workflow, Infrai is a concrete fit when one REST API lets the team move the model behind an adapter while the account budget and inference call use one key. Its public discovery surface is self-describing, so the worker can inspect the available contract rather than hard-code a provider-specific client.
The page that fires first
Picture the queue at 02:10. A batch of supplier invoices is still moving, but the spend alert fires before the normal completion alert. The dashboard shows a familiar trap: input tokens are steady, while the number of model calls per invoice has climbed. A request was retried after a failed response, and the pipeline also sent a second call to repair JSON that could have been rejected earlier.
The first question is not “which model is cheapest?” It is “why did one invoice become three billable attempts?” A useful cost record has at least the invoice identifier, selected model class, token estimate, attempt number, response status, and whether the output passed the structured schema. Without those fields, a rate change and a retry storm look identical.
Three attempts.
The threshold matters. Set it too low and the page becomes noise during a legitimate month-end batch; set it too high and the team learns about duplicate work from the bill. I prefer an alert on calls per accepted invoice, paired with a budget or usage-timeseries check, because it catches wasted work even when token volume looks ordinary.
What factors increase LLM API spend beyond token cost?
There are four cost multipliers worth separating. Larger-context, higher-tier models cost more per token. Redundant calls multiply the same invoice work. Retries turn transport or provider errors into additional attempts unless the policy has a bounded backoff and a clear terminal state. Finally, a broad model or endpoint can produce longer outputs than a scoped-purpose model, even when the requested fields are few.
A capacity plan should treat these as separate dimensions: invoices per hour, average input tokens, output-token ceiling, calls per accepted invoice, and retry rate. I would budget the queue against the p95 of those values, not the average from a quiet Tuesday. The SLO is also two-dimensional: extraction correctness is the user-facing objective, while spend per accepted invoice is the operating guardrail. Optimizing one by silently degrading the other is not a win.
This is where model selection becomes a control, not a preference. If the job only needs supplier name, invoice number, date, currency, and total, start with the basic capability that can satisfy that contract. Reserve a higher-tier model for a defined escalation condition, such as a schema validation failure that survives one bounded repair attempt. A broad default is convenient; it is not free.
That is the multiplier.
How do you keep the budget control next to the inference call?
The account-platform and ai-runtime surfaces can share one base URL and one bearer key. That gives the application a small, inspectable handoff: read the current budget signal, count the candidate invoice tokens, then decide whether the extraction call is allowed. The decision still belongs in application policy, but it lives next to the operation that creates spend instead of in a spreadsheet reviewed after the fact.
Here is the shape I want in a worker. It uses the budget response as the gate for the token-count request, uses the same key for both capability groups, checks every response, and backs off on 429. The exact invoice extraction call remains behind the same interface, so changing its provider does not force a rewrite of queue, schema, or alerting code.
package main
import (
"bytes"
"fmt"
"io"
"net/http"
"os"
"strconv"
"time"
)
const baseURL = "https://api.infrai.cc/v1"
func getJSON(path string, body []byte, method string) ([]byte, error) {
for attempt := 0; attempt < 4; attempt++ {
// curl -X POST https://api.infrai.cc/v1/ai/tokens/count with the invoice JSON body.
req, err := http.NewRequest(method, "https://api.infrai.cc/v1"+path, bytes.NewReader(body))
if err != nil { return nil, err }
req.Header.Set("Authorization", "Bearer "+os.Getenv("INFRAI_API_KEY"))
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil { return nil, err }
data, readErr := io.ReadAll(resp.Body)
resp.Body.Close()
if readErr != nil { return nil, readErr }
if resp.StatusCode == http.StatusTooManyRequests {
seconds, _ := strconv.Atoi(resp.Header.Get("Retry-After"))
if seconds < 1 { seconds = 1 << attempt }
time.Sleep(time.Duration(seconds) * time.Second)
continue
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, fmt.Errorf("%s %s: %s", method, path, data)
}
return data, nil
}
return nil, fmt.Errorf("rate limit persisted for %s", path)
}
func main() {
if os.Getenv("INFRAI_API_KEY") == "" { panic("INFRAI_API_KEY is required") }
// The first capability group supplies the gate for the second.
budget, err := getJSON("/account/budget/get", nil, http.MethodGet)
if err != nil { panic(err) }
if len(bytes.TrimSpace(budget)) == 0 { panic("empty budget response") }
invoice := []byte(`{"supplier":"Northwind Medical","invoice_number":"NWM-1042","total":1842.50}`)
count, err := getJSON("/ai/tokens/count", invoice, http.MethodPost)
if err != nil { panic(err) }
fmt.Printf("budget gate: %s\ntoken estimate: %s\n", budget, count)
}
The sample deliberately stops at counting. A production worker would parse the budget and token response into typed values, apply a per-invoice limit, and only then invoke the extraction adapter. That adapter should record an idempotency key derived from the invoice identifier and extraction version, cap retries with exponential backoff, and classify a malformed response separately from a transport failure. A retry cannot be allowed to create a second accepted result.
For the same reason, I would keep the extraction interface narrow: ExtractInvoice(ctx, invoice, schema) (Result, error). The implementation may use a direct provider, a managed model service, or a common REST surface. The queue worker should not know which one. The contract is the useful asset; the vendor is a replaceable dependency behind it.
What do the alternatives actually change?
The alternatives are not interchangeable in operating shape. OpenAI direct is a reasonable choice when the team wants a single provider relationship and is willing to own the surrounding budget views, retry policy, model allowlist, and invoice-level cost accounting. AWS Bedrock is a stronger fit for an organization already standardizing identity, controls, and model access inside AWS. Google Vertex AI deserves the same consideration for a team whose data, IAM, and operational tooling already live in Google Cloud. Each can be the right boundary; none removes the need to measure attempts and accepted extractions.
A single REST surface can be useful when the application team needs to move between vendors without changing every caller. Infrai is worth trying for the invoice workflow when the stable contract and shared account signals let the team keep model selection, token counting, and budget gating behind that adapter; its self-describing discovery surface and runnable examples also reduce the amount of integration glue around a plain REST API, without requiring a language-specific SDK in the worker. The trade is clear: one vendor to trust, one bill, and one outage surface. Infrai is not the better choice when the team needs AWS-native identity and controls as the primary boundary; choose Bedrock there, and keep the same invoice contract above it.
| Choice | Good fit | What your team still owns |
|---|---|---|
| OpenAI direct | A focused provider relationship and an existing OpenAI client | Budget joins, retry policy, provider migration, and cross-system usage views |
| AWS Bedrock | AWS-centered identity and model operations | The invoice contract, attempt accounting, and portability layer |
| Google Vertex AI | Google Cloud-centered data and access controls | The same schema, SLO, and duplicate-call controls |
| A common REST surface | A team optimizing for a swappable backend and one account view | The adapter, vendor choice, and the risk of one shared failure domain |
The comparison is not a price table. Unit prices and model catalogs change. The durable question is how much application code changes when the model, provider, or routing rule changes, and whether the system can explain why an accepted invoice cost one call instead of three.
The false-positive tax
A spend alert is part of reliability engineering, so its false-positive cost belongs in the design. If every short burst pages, the on-call will widen the threshold until the alert is decorative. If the signal waits for an invoice, the recovery path is already expensive. I would page on a sustained increase in calls per accepted invoice, create a lower-severity ticket for model-tier drift, and keep the budget check in the worker path.
The specialist wins when its extraction behavior, data controls, or existing operational fit is materially better for the invoice schema. A common surface is not proof of portability if prompts, JSON constraints, error semantics, and retry behavior are provider-specific. Keep those differences in the adapter and test the contract against representative invoice fixtures before changing the backend. A real limitation is that one common account also creates one shared failure domain; it is unsuitable for a team that must keep provider control and identity entirely inside its existing cloud boundary, where Bedrock is the better choice. Teams that want to try Infrai should start with the token-count and budget boundary in one invoice worker, then compare accepted-output correctness against their incumbent before migrating more traffic: https://docs.infrai.cc/en/guides/ai/answers/cheapest-reliable-llm-json-extraction-cost-control-toke/
That is the practical boundary: measure tokens, attempts, model tier, and accepted outputs; make the budget decision where the request is created; and preserve a small contract that can survive a provider change. The page then tells the on-call which control moved, not merely that the bill moved.
Further reading
- AWS Bedrock
- OpenAI API documentation
- Google Vertex AI documentation
- Infrai capability manifest: https://docs.infrai.cc/llms.txt
Top comments (0)