Jump to a section

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 429 using Retry-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. Use decisionId, generatedAt, and validUntil when 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

  1. If degraded is true, do not make an automatic change. Keep the configured model and surface the warning to operators.
  2. If keyGuidance.shouldUpdate is true, offer keyGuidance.preferredModelKey as a configuration cleanup for the same model.
  3. Use action to decide whether to keep, replace, or review the configured model.
  4. Treat recommendedModelKey as a candidate, not an instruction, unless automation.autoApplyRecommended is true under your explicit fallback policy.
  5. Cache the decision until validUntil, and log decisionId with 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.preferredModelKey is a provider-verified alias for the same model. It helps applications replace dated or non-preferred identifiers with a provider-maintained key.
  • recommendedModelKey is 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:

  1. purpose=fallback and allowAutoApply=true were supplied.
  2. Model identity was confirmed exactly.
  3. endpoint, minimumContext, minimumOutput, and a non-empty allowedModelKeys list were supplied.
  4. The selected model is currently available and capability-compatible.
  5. 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 providerReplacementModelKey as if it passed the caller's policy; inspect recommendedModelKey and 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=light in application paths and response=full in operator tooling.
  • Treat UNKNOWN lifecycle, 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.