Realtime Model Alternative API
Use the public Model Alternatives API to keep model keys in application configuration current. It resolves a provider model key, reports lifecycle state, identifies a provider-verified preferred alias when one is known, and evaluates replacement candidates under an explicit policy.
- Base URL:
https://api.usagetap.com - Method:
GET - Authentication: none
- Hosted price: free to call; no UsageTap subscription is required
- Shared service limit: 50 requests/second with a burst of 100; retry HTTP
429usingRetry-After - Response type:
application/json - Interactive explorer: usagetap.com/model-lifecycle
- OpenAPI 3.1: Download the machine-readable contract
The API never changes an application's configuration. Treat its response as a decision input, and require your own allowlist and evaluation before changing or retrying a model.
Stable contract: this page documents schema version
1. All model recommendations, prices, lifecycle states, and alias mappings are live data and can change independently of the schema. UsedecisionId,generatedAt, andvalidUntilwhen recording a decision.
Lifecycle results include the provider source and the date UsageTap last checked that source. Live inventory and recommendation data is cached for up to five minutes. Provider lifecycle notices are reviewed before publication, so this is not an instantaneous provider webhook. Re-run scheduled checks even when your code has not changed.
Five-minute quickstart
For most applications, start with purpose=retirement&response=light. It answers the operational questions without downloading candidate and pricing detail.
curl --fail-with-body \
'https://api.usagetap.com/v1/model-alternatives/openai/gpt-5.6-terra?purpose=retirement&response=light'
Every code block on this page has a Copy code button. The endpoint is public and needs no UsageTap account, API key, SDK, or request body.
Handle the result in this order
- If
degradedistrue, do not make an automatic change. Keep the configured model and surface the warning to operators. - If
keyGuidance.shouldUpdateistrue, offerkeyGuidance.preferredModelKeyas a configuration cleanup for the same model. - Use
actionto decide whether to keep, replace, or review the configured model. - Treat
recommendedModelKeyas a candidate, not an instruction, unlessautomation.autoApplyRecommendedistrueunder your explicit fallback policy. - Cache the decision until
validUntil, and logdecisionIdwith any action taken.
type LightDecision = {
action: "KEEP" | "REPLACE" | "REVIEW";
recommendedModelKey?: string;
keyGuidance?: { shouldUpdate: boolean; preferredModelKey: string };
decisionId?: string;
validUntil?: string;
degraded: boolean;
automation: { autoApplyRecommended: boolean };
};
async function checkConfiguredModel(modelKey: string): Promise<LightDecision> {
const path = modelKey.split("/").map(encodeURIComponent).join("/");
const response = await fetch(
`https://api.usagetap.com/v1/model-alternatives/${path}?purpose=retirement&response=light`,
{ signal: AbortSignal.timeout(2_000) },
);
if (!response.ok) throw new Error(`Model lookup failed: ${response.status}`);
return response.json() as Promise<LightDecision>;
}
const configured = process.env.AI_MODEL ?? "openai/gpt-5.6-terra";
const decision = await checkConfiguredModel(configured);
if (decision.degraded) {
console.warn("Model catalog unavailable; configuration was not changed");
} else if (decision.keyGuidance?.shouldUpdate) {
console.info("Preferred provider key:", decision.keyGuidance.preferredModelKey);
}
if (decision.action === "REPLACE") {
console.warn("Model requires migration:", decision.recommendedModelKey);
}
Choose an integration
| Goal | Recommended request | When to run it | Safe behavior |
|---|---|---|---|
| Keep configuration current | purpose=retirement&response=light |
CI, deploy, or application startup | Warn on REVIEW; open a migration task on REPLACE. |
| Normalize to a preferred provider key | purpose=retirement&response=light |
Configuration editing or startup | Suggest keyGuidance.preferredModelKey; never infer an alias when the field is absent. |
| Recover from model-not-found | purpose=fallback&response=light plus explicit constraints |
Only after your provider adapter classifies the original error as model unavailable | Retry once only when automation.autoApplyRecommended is true. |
| Compare migration candidates | purpose=upgrade&response=full |
Operator tooling, reviews, or scheduled audits | Evaluate candidates against representative workloads before adoption. |
Recommended architecture: call this API from CI, a backend, or a provider adapter rather than on every end-user request. Cache through validUntil; use the compact response only in a failure path where a current decision is required.
Endpoint
GET /v1/model-alternatives/{provider}/{model}
The model key is carried in the path. Encode each path segment separately if you build the URL dynamically.
curl --fail-with-body \
'https://api.usagetap.com/v1/model-alternatives/openai/gpt-5.6-terra?purpose=upgrade&response=light'
Calling GET /v1/model-alternatives without a model key returns machine-readable discovery information, including supported purposes, parameter names, defaults, and lifecycle transitions.
Request parameters
Path parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
provider |
string | Yes | Provider namespace. Supported public catalog providers are openai, anthropic, and google. |
model |
string | Yes | Provider model ID or alias. The complete {provider}/{model} key may be no longer than 300 characters. |
Query parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
response |
full | light |
full |
Selects the complete inspection payload or the compact real-time decision payload. |
purpose |
upgrade | fallback | retirement |
upgrade |
Changes candidate selection. upgrade favors newer eligible models, fallback favors an available compatible model, and retirement favors provider replacement guidance. |
inputTokens |
number, 0–10,000,000 | 8000 |
Expected uncached plus cached input tokens per request for price comparison. |
outputTokens |
number, 0–10,000,000 | 2000 |
Expected output tokens per request for price comparison. Input and output cannot both be zero. |
cachedInputPercent |
number, 0–100 | 30 |
Percentage of input tokens expected to receive cached-input pricing. |
maxPriceIncreasePercent |
number, 0–1000 | 20 |
Maximum permitted estimated request-price increase for a candidate. |
requiredParameters |
comma-separated strings | Empty | Required API capabilities, such as tools,structured_outputs. |
minimumContext |
number, 0–10,000,000 | Not set | Minimum acceptable context window in tokens. |
minimumOutput |
number, 0–10,000,000 | Not set | Minimum acceptable maximum output in tokens. |
endpoint |
string | Not set | Required provider endpoint or operation, for example chat/completions. Letters, numbers, _, ., /, and - are accepted. |
allowedProviders |
comma-separated strings | Empty | Restricts candidates to provider namespaces. |
allowedModelKeys |
comma-separated strings | Empty | Restricts candidates to model keys your application has evaluated. Required for automatic application. |
allowPreview |
boolean | false |
When true, preview models may be considered. |
allowAutoApply |
boolean | false |
Requests automatic-application eligibility. This does not change a model or perform a retry. |
Lists accept at most 100 unique, comma-separated values. Each list value may be no longer than 300 characters. Boolean values must be exactly true or false.
Preferred model-key guidance
keyGuidance answers a different question from recommendedModelKey:
keyGuidance.preferredModelKeyis a provider-verified alias for the same model. It helps applications replace dated or non-preferred identifiers with a provider-maintained key.recommendedModelKeyis a candidate chosen for an upgrade, fallback, or retirement decision. It may identify a different model.
UsageTap does not create an alias by stripping dates or guessing from a vendor's naming convention. The field is returned only when current provider inventory records an alias and its resolved model relationship. Providers do not all expose alias relationships in the same way, so absence of keyGuidance means “not verified,” not “no alias exists.”
For example, a provider-verified Anthropic alias can produce:
{
"keyGuidance": {
"requestedModelKey": "anthropic/claude-haiku-4-5-20251001",
"preferredModelKey": "anthropic/claude-haiku-4-5",
"resolvedModelKey": "anthropic/claude-haiku-4-5-20251001",
"relationship": "provider_alias",
"confidence": "provider_verified",
"shouldUpdate": true,
"note": "The provider verifies this shorter alias for the same model. Use it in application configuration when you want the provider-maintained key."
}
}
Whether an alias moves to future snapshots is controlled by the provider. Review that provider's stability contract before adopting it.
Response parameters
Every successful model lookup returns HTTP 200 and either a full or light JSON object. Fields marked optional are omitted when the catalog cannot support them.
Fields in both response modes
| Field | Type | Description |
|---|---|---|
schemaVersion |
number | Response schema version. Currently 1. |
catalogVersion |
string | Version of the source-backed lifecycle catalog. |
requested.modelKey |
string | Normalized requested model key. |
lifecycle.status |
ACTIVE | DEPRECATED | RETIRED | UNKNOWN |
Current lifecycle classification. UNKNOWN is not evidence that a model is active. |
lifecycle.announcedAt |
ISO date, optional | Published deprecation announcement date. |
lifecycle.shutdownAt |
ISO date, optional | Published shutdown date when known. |
lifecycle.source |
object, optional | Provider source label, URL, and date UsageTap checked the lifecycle facts. |
action |
KEEP | REPLACE | REVIEW |
High-level action derived from lifecycle state. |
keyGuidance |
object, optional | Provider-verified guidance for using a preferred alias for the same model. See the field table below. |
recommendedModelKey |
string, optional | Selected candidate under the supplied policy. |
providerReplacementModelKey |
string, optional | Provider-designated lifecycle replacement, even when it does not satisfy the supplied policy. |
recommendationSource |
provider | admin_review | computed | none, optional |
Provenance of recommendedModelKey. |
decisionId |
string, optional | Stable identifier for the decision inputs and current catalog state. Log this with application decisions. |
generatedAt |
ISO datetime, optional | Time the live catalog snapshot was loaded. |
validUntil |
ISO datetime, optional | End of the current decision's validity window. Re-resolve after this time. |
degraded |
boolean | true when live inventory enrichment is unavailable. Never auto-apply a degraded response. |
automation.autoApplyRecommended |
boolean | Whether the selected recommendation passed all automatic-application gates. It does not perform the change. |
keyGuidance fields
| Field | Type | Description |
|---|---|---|
requestedModelKey |
string | Key supplied to the resolver after normalization. |
preferredModelKey |
string | Provider-verified alias suggested for application configuration. |
resolvedModelKey |
string | Model key to which the alias resolved when the inventory was checked. |
relationship |
provider_alias |
Relationship between the preferred key and resolved model. |
confidence |
provider_verified |
Evidence level. UsageTap does not return inferred aliases. |
shouldUpdate |
boolean | true when the requested key differs from the preferred key. |
note |
string | Human-readable use and safety guidance. |
Additional full-response fields
The default response=full payload adds inspection and evaluation detail.
| Field | Type | Description |
|---|---|---|
requested.provider |
string, optional | Recognized provider namespace. |
requested.model |
string | Model portion of the normalized key. |
match |
object, optional | Catalog model key, match strategy, and optional score used to establish identity. |
pricing |
object, optional | Price snapshot ID and input, output, and blended USD rates per million tokens. |
equivalents |
array | Same-provider candidates and family variants. |
variants |
array, optional | Family variants also represented in equivalents. |
alternatives |
array | Cross-provider candidates. |
excludedCandidates |
array, optional | Candidate keys and reasons they failed the policy. |
policy |
object, optional | Validated request policy actually used to compute the decision. |
scoringVersion |
number, optional | Recommendation scoring implementation version. |
warnings |
string array, optional | Safety, identity, availability, or evaluation limitations. |
automation.canResolveWithoutUsageTapAccount |
true |
Confirms that the public resolver requires no UsageTap account. |
automation.note |
string | Explanation of automatic-application eligibility or required review. |
Candidate objects in equivalents, variants, and alternatives can contain:
| Field | Type | Description |
|---|---|---|
modelKey |
string | Candidate's public model key. |
provider |
string | Candidate provider. |
relation |
string | official_replacement, same_provider_similar, or similar_price_and_strength. |
confidence |
string | authoritative, reviewed, or candidate. |
rationale |
string | Concise explanation of the candidate. |
requiresEvaluation |
boolean | Whether workload evaluation is still required. |
score |
number, optional | Overall candidate score. |
category |
string, optional | Recommendation category from the scoring engine. |
compatibility |
object, optional | Capability compatibility result and gaps. |
availability |
string, optional | Current inventory availability classification. |
estimatedRequestUsd |
number, optional | Estimated price for the supplied token mix. |
priceChangePercent |
number, optional | Estimated price change from the requested model. |
priceDistancePercent |
number, optional | Absolute estimated price distance. |
inputUsdPerMillion |
number, optional | Input price used for comparison. |
outputUsdPerMillion |
number, optional | Output price used for comparison. |
reasons |
string array, optional | Scoring explanations. |
Light responses for real-time checks
Add response=light when an application only needs the current decision. The light response omits pricing, candidate arrays, policy detail, warnings, and source provenance. It retains keyGuidance, lifecycle, the recommendation, validity, degradation, and the auto-apply flag.
async function currentModelDecision(model) {
const path = model.split("/").map(encodeURIComponent).join("/");
const response = await fetch(
`https://api.usagetap.com/v1/model-alternatives/${path}?purpose=retirement&response=light`,
{ signal: AbortSignal.timeout(2000) },
);
if (!response.ok) throw new Error(`Model lookup failed: ${response.status}`);
return response.json();
}
const decision = await currentModelDecision("openai/gpt-5.6-terra");
if (decision.keyGuidance?.shouldUpdate) {
console.info("Preferred provider key:", decision.keyGuidance.preferredModelKey);
}
Implementation recipes
Reusable TypeScript client
This client URL-encodes the provider and model separately, applies a timeout, preserves structured API errors, and defaults to the light response.
export type ResolvePurpose = "upgrade" | "fallback" | "retirement";
export type ResolveOptions = {
purpose?: ResolvePurpose;
inputTokens?: number;
outputTokens?: number;
cachedInputPercent?: number;
maxPriceIncreasePercent?: number;
requiredParameters?: string[];
minimumContext?: number;
minimumOutput?: number;
endpoint?: string;
allowedProviders?: string[];
allowedModelKeys?: string[];
allowPreview?: boolean;
allowAutoApply?: boolean;
};
export class ModelAlternativeApiError extends Error {
constructor(
readonly status: number,
readonly code: string,
message: string,
) {
super(message);
}
}
export async function resolveModel(
modelKey: string,
options: ResolveOptions = {},
): Promise<LightModelAlternativeResolution> {
const parts = modelKey.split("/");
if (parts.length < 2 || parts.some((part) => !part)) {
throw new TypeError("modelKey must use provider/model form");
}
const path = parts.map(encodeURIComponent).join("/");
const query = new URLSearchParams({
response: "light",
purpose: options.purpose ?? "retirement",
});
for (const [name, value] of Object.entries(options)) {
if (value === undefined || name === "purpose") continue;
query.set(name, Array.isArray(value) ? value.join(",") : String(value));
}
const response = await fetch(
`https://api.usagetap.com/v1/model-alternatives/${path}?${query}`,
{
headers: { Accept: "application/json" },
signal: AbortSignal.timeout(2_000),
},
);
if (!response.ok) {
const body = (await response.json().catch(() => ({}))) as {
error?: string;
message?: string;
};
throw new ModelAlternativeApiError(
response.status,
body.error ?? "MODEL_ALTERNATIVE_REQUEST_FAILED",
body.message ?? `Model lookup failed with HTTP ${response.status}`,
);
}
return response.json() as Promise<LightModelAlternativeResolution>;
}
CI or startup configuration check
This check reports a preferred alias separately from a required migration. It exits non-zero only for a confirmed replacement action; adjust that policy to fit your release process.
import { resolveModel } from "./model-alternatives.js";
const modelKey = process.env.AI_MODEL;
if (!modelKey) throw new Error("AI_MODEL is required");
const decision = await resolveModel(modelKey, { purpose: "retirement" });
console.info({
modelKey,
action: decision.action,
decisionId: decision.decisionId,
validUntil: decision.validUntil,
});
if (decision.degraded) {
console.warn("UsageTap returned a degraded result; leaving AI_MODEL unchanged");
} else if (decision.keyGuidance?.shouldUpdate) {
console.warn(
`Prefer ${decision.keyGuidance.preferredModelKey} for the same provider model`,
);
}
if (decision.action === "REPLACE") {
console.error(`Migration required. Candidate: ${decision.recommendedModelKey ?? "none"}`);
process.exitCode = 1;
}
Safe model-not-found recovery
Automatic eligibility requires a fallback purpose, an evaluated allowlist, endpoint and capacity constraints, exact identity, current availability, and allowAutoApply=true. The API still does not execute the retry.
type ProviderAdapter<TRequest, TResponse> = {
invoke(modelKey: string, request: TRequest): Promise<TResponse>;
isModelUnavailable(error: unknown): boolean;
};
export async function invokeWithOneFallback<TRequest, TResponse>(
adapter: ProviderAdapter<TRequest, TResponse>,
configuredModel: string,
request: TRequest,
): Promise<TResponse> {
try {
return await adapter.invoke(configuredModel, request);
} catch (originalError) {
if (!adapter.isModelUnavailable(originalError)) throw originalError;
const decision = await resolveModel(configuredModel, {
purpose: "fallback",
endpoint: "chat/completions",
minimumContext: 32_000,
minimumOutput: 4_000,
requiredParameters: ["tools", "structured_outputs"],
allowedModelKeys: [
"openai/gpt-5.6-terra",
"anthropic/claude-sonnet-4-6",
],
allowAutoApply: true,
});
const replacement = decision.recommendedModelKey;
if (
decision.degraded ||
!decision.automation.autoApplyRecommended ||
!replacement ||
replacement === configuredModel
) {
throw originalError;
}
try {
return await adapter.invoke(replacement, request);
} catch (retryError) {
throw new AggregateError(
[originalError, retryError],
`Configured model and approved fallback both failed (${decision.decisionId ?? "no decision ID"})`,
);
}
}
}
Python client
from __future__ import annotations
import json
from typing import Any
from urllib.error import HTTPError
from urllib.parse import quote, urlencode
from urllib.request import Request, urlopen
API_BASE = "https://api.usagetap.com"
def resolve_model(
model_key: str,
*,
purpose: str = "retirement",
response_mode: str = "light",
**policy: Any,
) -> dict[str, Any]:
parts = model_key.split("/")
if len(parts) < 2 or any(not part for part in parts):
raise ValueError("model_key must use provider/model form")
path = "/".join(quote(part, safe="") for part in parts)
query: dict[str, Any] = {
"purpose": purpose,
"response": response_mode,
**policy,
}
query = {
key: ",".join(value) if isinstance(value, list) else str(value).lower()
if isinstance(value, bool)
else value
for key, value in query.items()
if value is not None
}
request = Request(
f"{API_BASE}/v1/model-alternatives/{path}?{urlencode(query)}",
headers={"Accept": "application/json"},
)
try:
with urlopen(request, timeout=2) as response:
return json.load(response)
except HTTPError as error:
body = json.loads(error.read() or b"{}")
raise RuntimeError(
f"{body.get('error', 'MODEL_ALTERNATIVE_REQUEST_FAILED')}: "
f"{body.get('message', error.reason)}"
) from error
decision = resolve_model("openai/gpt-5.6-terra")
if decision.get("degraded"):
print("Catalog degraded; no automatic change")
elif decision.get("keyGuidance", {}).get("shouldUpdate"):
print("Preferred key:", decision["keyGuidance"]["preferredModelKey"])
TypeScript contract
Use these interfaces at the network boundary. Optional fields really can be absent, so keep strict null checking enabled and do not replace optional properties with required ones.
export type LifecycleStatus = "ACTIVE" | "DEPRECATED" | "RETIRED" | "UNKNOWN";
export type ResolutionAction = "KEEP" | "REPLACE" | "REVIEW";
export type ResolvePurpose = "upgrade" | "fallback" | "retirement";
export type ModelKeyGuidance = {
requestedModelKey: string;
preferredModelKey: string;
resolvedModelKey: string;
relationship: "provider_alias";
confidence: "provider_verified";
shouldUpdate: boolean;
note: string;
};
export type LightModelAlternativeResolution = {
schemaVersion: 1;
catalogVersion: string;
response: "light";
requested: { modelKey: string };
lifecycle: {
status: LifecycleStatus;
announcedAt?: string;
shutdownAt?: string;
source?: { label: string; url: string; checkedAt: string };
};
action: ResolutionAction;
keyGuidance?: ModelKeyGuidance;
recommendedModelKey?: string;
providerReplacementModelKey?: string;
recommendationSource?: "provider" | "admin_review" | "computed" | "none";
decisionId?: string;
generatedAt?: string;
validUntil?: string;
degraded: boolean;
automation: { autoApplyRecommended: boolean };
};
export type ModelAlternativeCandidate = {
modelKey: string;
provider: "openai" | "anthropic" | "google";
relation:
| "official_replacement"
| "same_provider_similar"
| "similar_price_and_strength";
confidence: "authoritative" | "reviewed" | "candidate";
rationale: string;
requiresEvaluation: boolean;
score?: number;
priceDistancePercent?: number;
inputUsdPerMillion?: number;
outputUsdPerMillion?: number;
category?: "upgrade" | "variant" | "alternative";
compatibility?: {
status: "compatible" | "unknown" | "incompatible";
gaps: string[];
unknowns: string[];
};
availability?: "available" | "unknown";
estimatedRequestUsd?: number;
priceChangePercent?: number;
reasons?: string[];
};
export type RecommendationPolicy = {
purpose: "upgrade" | "fallback" | "retirement";
inputTokens: number;
outputTokens: number;
cachedInputPercent: number;
maxPriceIncreasePercent: number;
requiredParameters: string[];
allowedProviders: string[];
allowedModelKeys: string[];
minimumContext?: number;
minimumOutput?: number;
endpoint?: string;
allowPreview: boolean;
allowAutoApply: boolean;
};
export type FullModelAlternativeResolution =
Omit<LightModelAlternativeResolution, "response" | "requested" | "lifecycle" | "automation"> & {
response?: never;
requested: {
modelKey: string;
provider?: "openai" | "anthropic" | "google";
model: string;
};
lifecycle: {
status: LifecycleStatus;
announcedAt?: string;
shutdownAt?: string;
source?: { label: string; url: string; checkedAt: string };
};
match?: { matchedModelKey: string; strategy: string; score?: number };
pricing?: {
snapshotId: string;
inputUsdPerMillion?: number;
outputUsdPerMillion?: number;
blendedUsdPerMillion?: number;
};
equivalents: ModelAlternativeCandidate[];
alternatives: ModelAlternativeCandidate[];
variants?: ModelAlternativeCandidate[];
excludedCandidates?: { modelKey: string; reasons: string[] }[];
policy?: RecommendationPolicy;
scoringVersion?: number;
warnings?: string[];
automation: {
canResolveWithoutUsageTapAccount: true;
autoApplyRecommended: boolean;
note: string;
};
};
Automatic fallback guardrails
automation.autoApplyRecommended can be true only when all of the following are true:
purpose=fallbackandallowAutoApply=truewere supplied.- Model identity was confirmed exactly.
endpoint,minimumContext,minimumOutput, and a non-emptyallowedModelKeyslist were supplied.- The selected model is currently available and capability-compatible.
- The response is not degraded.
Even then, the application owns the retry. Retry once through an evaluated adapter, preserve the original error, and log decisionId. A preferred alias is not by itself authorization to retry or migrate.
Caching and conditional requests
Non-degraded responses use Cache-Control: public, max-age=60, s-maxage=60 and may include an ETag. Send the ETag back in If-None-Match; an unchanged decision returns HTTP 304 with an empty body. Full and light responses have distinct ETags. Degraded responses use Cache-Control: no-store.
Errors
| Status | Error | Meaning |
|---|---|---|
200 |
— | Successful lookup, including degraded lifecycle-only results. Inspect degraded. |
304 |
— | Conditional request matched the current ETag. |
400 |
INVALID_MODEL_KEY |
Model key exceeds 300 characters. |
400 |
INVALID_MODEL_REQUEST |
Invalid response mode, URL encoding, or policy parameter. |
405 |
METHOD_NOT_ALLOWED |
Method other than GET. |
Error bodies contain error and message strings.
{
"error": "INVALID_MODEL_REQUEST",
"message": "Check the URL-encoded model key and recommendation policy parameters."
}
Coding-agent integration brief
This section is deliberately concise and normative so it can be linked from an AGENTS.md, CLAUDE.md, implementation ticket, or coding-agent prompt. The detailed tables on this page remain the source of truth.
Copy this brief into an implementation task
Integrate the UsageTap Realtime Model Alternative API.
Reference: https://usagetap.com/docs/MODEL_ALTERNATIVES_API
OpenAPI: https://usagetap.com/openapi/model-alternatives.yaml
Endpoint: GET https://api.usagetap.com/v1/model-alternatives/{provider}/{model}
Authentication: none
Requirements:
1. Accept model keys only in provider/model form and URL-encode every path segment.
2. Use purpose=retirement&response=light for CI/startup configuration checks.
3. Set a two-second timeout and handle non-2xx responses as lookup failures.
4. Never treat UNKNOWN lifecycle, a missing field, or degraded=true as approval.
5. Keep keyGuidance.preferredModelKey separate from recommendedModelKey:
- keyGuidance is a verified alias for the same model;
- recommendedModelKey is an upgrade/fallback/migration candidate.
6. Never invent an alias by stripping a date or parsing a vendor model name.
7. Do not change application configuration automatically from a normal lookup.
8. For model-not-found recovery, use purpose=fallback plus endpoint,
minimumContext, minimumOutput, allowedModelKeys, requiredParameters, and
allowAutoApply=true. Retry once only when autoApplyRecommended is true.
9. Preserve the original provider error if fallback is ineligible or fails.
10. Cache until validUntil, support ETag/If-None-Match where practical, and log
decisionId with any warning, migration, alias update, or fallback.
11. Add tests for KEEP, REPLACE, REVIEW, UNKNOWN, degraded, preferred alias,
missing recommendation, timeout, API error, and eligible/ineligible fallback.
12. Use current model keys in sample and fixture data.
Acceptance tests
| Scenario | Expected integration behavior |
|---|---|
action=KEEP, no key guidance |
Keep the configured key. |
keyGuidance.shouldUpdate=true |
Report or propose the preferred alias; do not confuse it with a model migration. |
action=REPLACE, candidate present |
Open or surface a migration action; do not silently update normal configuration. |
action=REVIEW or lifecycle UNKNOWN |
Require human/operator review. |
degraded=true |
Keep the configured model and disable automatic action. |
HTTP 400 or 405 |
Surface the structured error and message; fix the request rather than retrying it. |
Timeout, network failure, or HTTP 5xx |
Preserve current configuration; apply bounded service-level retry policy outside the request path. |
| Fallback is eligible | Retry exactly once with recommendedModelKey and log decisionId. |
| Fallback is ineligible or retry fails | Re-throw the original error, or include it as the primary cause. |
| Field is absent | Treat it as unavailable information, never as false, 0, or approval. |
What not to implement
- Do not poll the full response on every inference request.
- Do not switch providers merely because a cross-provider candidate has a high score.
- Do not auto-apply a response when identity was inferred, inventory is stale, or compatibility is unknown.
- Do not use
providerReplacementModelKeyas if it passed the caller's policy; inspectrecommendedModelKeyand automation eligibility separately. - Do not persist a recommendation forever. Re-resolve after
validUntil.
Operational recommendations
- Resolve configured model keys during CI, deployment, or startup, then refresh after
validUntil. - Use
response=lightin application paths andresponse=fullin operator tooling. - Treat
UNKNOWNlifecycle, missing alias guidance, missing pricing, and degraded inventory as uncertainty rather than approval. - Keep an application-owned model allowlist and capability tests.
- Log the requested key,
decisionId,generatedAt, chosen action, and any applied key. - Benchmark cross-provider candidates on representative workloads before production routing.