The Actual Model in an API Response Without Guesswork
Actual model in an API response: a metadata schema for aliases, providers, versions, fallback, and auditing LLM request execution.

A model alias is convenient for the client, but it does not prove who handled the request. In production, this distinction quickly stops being academic: a team notices a cost spike, an unexpected difference in response quality, or a complaint about data residency, then discovers that the log contains only model: "smart-route".
You need a separate result-metadata contract. It should connect the client's intent with actual execution: the requested alias, the resolved canonical model, the provider, each attempt, its status, and the conditions under which the router changed course. Then the model response can be checked, explained, and matched to costs without guesswork.
An alias describes intent, not execution
An alias is useful for management, but it is not proof of the actual model. A team may send support-assistant, while a routing rule expands it into one of several models based on task class, availability, cost limits, or regional requirements. Even when the canonical model stays the same, different providers may accept the request.
The problem starts when one field is expected to answer two different questions:
- What did the client ask for?
- What actually performed the inference?
The first question belongs to your application contract. The second belongs to execution observability. When you merge them, you lose the ability to explain a result after a fallback, retry, or policy change.
OpenRouter documentation clearly separates routing between providers from the model name: by default, requests may be distributed among available providers, and fallback may move a call to another route after an error or limitation. Its normalized response includes a model field, but that field alone is not enough when you need the attempt history and the actual provider.
Do not call an alias «the model that responded» in an audit interface. Call it requested_alias or requested_model. This small naming discipline prevents many false conclusions during an incident review.
Four identifiers must not be collapsed into one string
Tracing requires at least four different values because each answers a different question.
requested_alias describes the client's selection. It might be legal-review, fast-chat, or a name used by the product team. The alias changes with your policy and is usually unsuitable for comparing results across months.
resolved_model describes the canonical identifier resolved by the router. It needs a format that does not depend on a display name. OpenRouter catalog documentation distinguishes between id, the display name, and canonical_slug, which is described as a permanent model identifier. This separation is useful: people read the attractive name, while logs and rules need an immutable identifier.
provider identifies the organization or computing environment that handled the attempt. It matters especially when the same model is available from several providers with different regions, queues, prices, and parameter support.
execution_model identifies the value sent to or confirmed by the actual endpoint. Sometimes it matches resolved_model. Sometimes the provider exposes a more precise deployment name. If it is unavailable, do not put the alias there or copy a value from the catalog. Set it to null and record that the source did not provide it.
There is also a fifth object that many teams forget: route_policy. It is neither a model name nor a provider name. It is the version of the rule that made the decision. Without it, you cannot explain why the same alias chose one route on Monday and another on Tuesday.
The result contract must separate facts from assumptions
A good schema does not try to fill every field at any cost. It stores the source of each value and allows uncertainty. This matters for the model version: a public family name, a catalog date, and the actual revision of a specific deployment are not the same thing.
I usually add source next to fields that can come from different places. A value confirmed by the provider's response carries more weight than an assumption made from the routing configuration. A field calculated by the gateway is still useful, but it must not be presented as provider confirmation.
Here is an example of a useful fragment of a final response. It is not a universal standard, but a contract that fits naturally beside an ordinary OpenAI-compatible response object.
{
"id": "req_01J9X7KQ5Y",
"object": "chat.completion",
"model": "support-assistant",
"routing": {
"requested_alias": "support-assistant",
"resolved_model": {
"value": "vendor-x/chat-pro",
"source": "router_catalog"
},
"selected_attempt_id": "att_02",
"policy": {
"id": "support-default",
"revision": "2026-07-23.3",
"hash": "sha256:6c33c8..."
},
"attempts": [
{
"id": "att_01",
"provider": "provider-a",
"execution_model": null,
"execution_model_source": "not_disclosed",
"status": "failed",
"failure_class": "timeout",
"started_at": "2026-07-23T09:14:01Z",
"finished_at": "2026-07-23T09:14:16Z"
},
{
"id": "att_02",
"provider": "provider-b",
"execution_model": "chat-pro-2026-06",
"execution_model_source": "provider_response",
"model_revision": null,
"model_revision_source": "not_disclosed",
"status": "selected",
"started_at": "2026-07-23T09:14:16Z",
"finished_at": "2026-07-23T09:14:18Z"
}
]
}
}
This object has two useful properties. First, it does not pretend to know the version: null means that the exact revision is unknown. Second, it shows not only the winning attempt but also the path that led to it.
Keep the top-level model field for compatibility with older client code. But do not make it the only source of truth. In the contract above, it remains a convenient result label, while the details live in the separate routing namespace.
Record fallback as a sequence of attempts
The final status «request succeeded» hides half the story. If the first attempt received a 429, the second did not support response_format, and the third returned the text, that chain explains the latency, cost, and behavioral differences.
Each attempt should have its own identifier, start and end times, status, provider, and failure class. Do not record only the raw error text. The raw text is useful in a protected log, but analytics needs stable classes: timeout, rate_limited, upstream_5xx, unsupported_parameter, policy_rejected, and cancelled.
Store selection_reason separately for the successful attempt. Possible values include:
primary_routefor the first selected path;fallback_after_failureafter a technical failure;fallback_after_policy_rejectionafter a data or regional restriction;retry_same_providerwhen retrying with the same provider;manual_overridewhen an operator or client rule fixed the route.
Do not turn this list into free text. Free text is useful for explaining an event to an engineer, but it cannot be aggregated reliably. In a month, you may need to know what share of requests left the primary route because of a limit, and you will not want to parse thousands of log lines with regular expressions.
There is an unpleasant case that teams often miss. The router sends a request to a provider, the provider starts generating, and then the connection between them breaks. You may not know whether the full cost was incurred or whether the response reached the client. Do not mark the attempt simply as failed. Use unknown_outcome and do not promise exact billing until it has been reconciled with the provider's data.
A streaming response gets its final status only at the end
With streaming, the first chunk cannot be treated as confirmation that the entire request completed with the selected provider. Early events may contain a role, text, or service fields, and the stream may then stop. If you record the route as final when the first byte arrives, the log will show successful executions that never actually completed.
Create the attempt record before sending the request upstream. On the first event, record first_byte_at. On a normal finish, add finished_at, the final finish_reason, usage, and the selected status. If the stream breaks, preserve what is known and use one of stream_interrupted, client_disconnected, or unknown_outcome.
The event flow might look like this:
request accepted
-> attempt created
-> upstream connected
-> first token received
-> final usage received
-> attempt selected
-> client stream closed
Not all providers send usage at the very end of a stream in the same way. Therefore, separate usage_reported_by_provider and usage_estimated_by_gateway. The first can be used to reconcile the bill. The second is suitable for operational analytics, but must be marked as an estimate.
OpenRouter documentation for streaming mode also warns about service SSE comments that clients should ignore. For auditing, this leads to a simple rule: do not create a new attempt or change the status for every incoming event. Classify the event type first.
A model version is useful only when its origin is known
The model_version field is often added to a schema for appearances, then filled with a model's marketing name. Do not do that. Chat Pro, chat-pro, chat-pro-latest, and chat-pro-2026-06 may describe a family, an alias, an update channel, and a specific build. They are not interchangeable.
Separate at least three fields:
model_familyfor the stable family, if known;model_releasefor the release or snapshot published by the provider;deployment_revisionfor the exact version of the running deployment.
Providers often disclose only the first. Sometimes they disclose the second. The third is rarely available, especially for closed models. Your audit should handle this limitation honestly: null plus not_disclosed is better than a string assembled from assumptions.
The situation is different for your own open-weight deployments. There, you can and should record the weights hash, tokenizer version, chat template, container revision, and GPU pool identifier. Otherwise, the statement «we ran the same model» proves nothing. A change in the chat template or quantization can noticeably change the response without changing the model name.
Do not force one schema to describe a closed external endpoint and your own cluster in exactly the same level of detail. Keep common fields, and move provider-specific details into provider_metadata. At the same time, prohibit unverifiable fields in the main part of the contract.
The route policy must be reproducible
The actual executor does not explain the decision by itself. If you know that provider-b produced the response but do not know which rules were active at that moment, you cannot tell whether the choice was expected.
Store an immutable policy revision from the moment the request is accepted. A Git revision, the identifier of the published rule, and a cryptographic hash of the normalized configuration will work. Do not store only the name default-policy, because its contents change over time.
For example, the logic might allow fallback only between routes that meet regional and data-processing requirements:
{
"policy_id": "claims-assistant",
"revision": "2026-07-23.3",
"allowed_regions": ["KZ"],
"fallback": "same_data_boundary_only",
"providers": ["local-gpu", "provider-kz"],
"max_attempts": 2
}
The important point here is not the field names, but the verifiable consequence: if an attempt with an external provider appears in the audit, you can compare the record with the policy and immediately determine whether the gateway broke the rule or an operator changed the configuration.
In AI Router, this metadata is especially useful for teams that need to combine a single OpenAI-compatible endpoint with auditing, PII masking, and data-storage requirements in Kazakhstan. But the schema remains useful when working directly with one provider: today you have one route, and tomorrow you may add a backup environment and separate rules for sensitive tasks.
Identifiers connect the response to the log, but do not replace the log
Every call should have at least two identifiers. The gateway creates request_id and uses it in the response, trace, and logs. client_request_id comes from the application and connects the call to a user action, queue job, or CRM operation.
Add trace_id if your service already uses distributed tracing. One trace can then connect the HTTP entry point, policy check, data masking, model request, tools, result recording, and retry. But do not use it as a replacement for the route log: a trace shows the sequence of service operations, while routing.attempts stores the meaning of the decision.
The response to the client usually needs only a short summary:
{
"request_id": "req_01J9X7KQ5Y",
"requested_model": "support-assistant",
"resolved_model": "vendor-x/chat-pro",
"routing_status": "completed"
}
The full attempt array is better exposed to a trusted server client, administrator, or audit system. The provider name, endpoint identifier, and switching reasons may reveal internal topology and contract terms. Keep the external diagnostic contract separate from the internal operational record.
Do not put the original prompt, model response, authorization header, or personal data into metadata for the sake of convenient searching. Store a hash of the normalized request, input size, sensitivity classification, and a reference to separate protected storage only where the full text is genuinely needed.
Test the contract with controlled failures
Routing is not working merely because the first request returned 200. It is working when the team can deliberately trigger a known failure and receive a predictable attempt history.
Run five checks in a test environment:
- Send a request through an alias and confirm that the response contains both the alias and the resolved canonical model.
- Force a timeout on the first route and check that
att_01appears withfailed, whileatt_02receivesselected. - Pass a parameter that the second route does not support and verify the
unsupported_parameterclassification instead of a silent request change. - Interrupt the stream after several tokens and verify that the record does not receive a successful-completion status.
- Change the policy, repeat the call, and confirm that the old response still points to the old revision.
Do not compare the exact text generated by a probabilistic model in CI. Compare the schema, statuses, attempt order, required identifiers, and policy invariants. For example, a test for region-restricted tasks should fail if a prohibited provider appears in attempts, even when the text response looks perfect.
When the log contains only an alias and an HTTP status, an investigation becomes a reconstruction based on indirect clues. Add the actual route to the result contract before you have to explain a discrepancy in quality, billing, or data residency after the fact.
Frequently asked questions
Why is the model field in a request not enough to audit an LLM call?
No. An alias tells you what the client asked the router for, not which executor accepted the request. If the router can switch providers, models, or locations after an error, the model field in the request is not enough for an investigation.
What metadata should be stored for every LLM request?
At a minimum, store the call ID, alias, canonical model, provider, actual execution identifier, status, and timestamps. For production, add the attempt list, the reason for switching, the routing policy version, and the configuration hash.
Can you always include the model version in an API response?
Only when the provider actually supplies an immutable identifier for the version, build, or deployment. If it does not, store null and indicate that the version was not disclosed. An invented version is worse than no version because it creates false confidence during an incident review.
How should fallback between providers be recorded?
Store every attempt separately, including unsuccessful ones. The final object should clearly show which attempt produced the result and which ones ended in a timeout, parameter compatibility error, or provider rejection.
How do you preserve the actual model for a streaming response?
Intermediate events cannot be treated as final proof because the stream may stop after part of the text has been sent. Create the record when the request starts, update it as processing continues, and mark it complete only after the final event or server confirmation.
Should users see the provider and request route?
The provider, region, internal endpoint IDs, and fallback reason may be operational details. The user-facing response should usually contain only a safe summary, while the full log stays in protected audit storage with access controls.
How is request_id different from client_request_id?
The system-generated request_id connects the HTTP request, gateway logs, queue record, and application trace. The client identifier serves a separate purpose: your application supplies it to find all calls belonging to one user action or business process.
Can routing metadata be stored without the prompt text?
Yes. Logs should not contain original prompts, responses, access tokens, or personal data without a clear reason. Hashes, sizes, data classifications, identifiers, and a link to protected storage are usually enough when the full text is needed for an investigation.
How should model routing be tested in CI?
Compare response contracts, not random model wording. Define the expected alias, allowed canonical models, metadata schema, attempt count, and the rule that assigns selected to the final attempt.
Does every LLM request need an actual-model log?
Do not make it mandatory for every user scenario. It is needed when reproducibility, cost calculation, quality analysis, data residency requirements, or result disputes matter. Internal experiments can use a shortened record, but it should not be confused with an audit.