Build Observable AI Model Fallbacks with Telnyx AI Gateway Telnyx published a Python sample, gateway-fallbacks, that moves AI model retry and fallback logic into its Telnyx AI Gateway rather than client-side SDK loops. The sample provisions a primary model (Kimi-K3) and a fallback (GLM-5.3-Flash) through a token group, forces a one-second timeout on the primary to trigger failover, and exposes the served model and attempt count via response headers such as x-ltg-served-model and x-ltg-attempts. It also disables the OpenAI client's own retries so gateway attempts, ledger records, and billing stay aligned around a single retry policy. An AI model fallback is only useful if your application can answer two questions afterward: A client-side retry loop can hide both answers. The application eventually receives a successful response, but the path to that response is difficult to reconstruct. SDK retries can also stack on top of gateway retries, creating extra requests and misleading attempt counts. The gateway-fallbacks https://github.com/team-telnyx/telnyx-code-examples/tree/main/gateway-fallbacks Python sample moves that resilience policy into Telnyx AI Gateway. It provisions a primary and fallback model, forces a timeout on the primary, and proves the resulting failover twice: The example separates management, inference, and observation: Management plane Inference plane api.telnyx.com/v2/llm token gateway llm.telnyx.com/v1 Telnyx account API key scoped token key POST /token groups GET /models POST /token keys POST /chat/completions GET /spend/events | GET /usage/summary v | gateway retries and fallbacks +----------------------+----------------+ | v Observation: x-ltg-served-model, x-ltg-attempts, requested model, model, attempts, cache hit The management plane owns token groups, token keys, budgets, and usage reporting. It uses the Telnyx account API key. The inference plane is OpenAI-compatible. It uses only the token key issued for the group, so application code does not need the account credential. The sample provisions two allowed model aliases and one fallback relationship: allowed models = "Kimi-K3", "GLM-5.3-Flash" fallbacks = { "Kimi-K3": "GLM-5.3-Flash" , } retries = { "max attempts": 2, "retry delay ms": 250, "backoff": "exponential", } group = client.llm token gateway.token groups.create name="gateway-fallbacks-demo", allowed models=allowed models, fallbacks=fallbacks, retries=retries, max budget=10, budget duration="1d", idempotency key=f"gateway-fallbacks-group-{uuid.uuid4 }", .data The group constrains which models its key can call and defines how failures move through the chain. The sample validates the same policy limits locally before making the API request: max attempts must be between one and five allowed models max attempts × 1 + longest chain cannot exceed eight The gateway also enforces these rules. policy.py deliberately sends an invalid max attempts=9 update and expects an HTTP 400 without mutating the group. The gateway owns retries and model fallbacks, so the OpenAI client disables its own retry behavior: llm = OpenAI base url="https://llm.telnyx.com/v1", api key=token key, max retries=0, This matters because an SDK retry is a new request layered on top of the gateway's attempts. Disabling it keeps the response headers, ledger records, and billing aligned around one retry policy. The sample distinguishes failures where no model work occurred from failures where processing may already have started. Definite no-work failures such as pre-dispatch errors, 429 , and 529 can retry the same model. Uncertain failures such as timeouts, 408 , 500 , 502 , 503 , and 504 move to the next fallback because the original request may already have reached the provider. A context-length rejection is terminal because sending the same oversized context to another attempt does not fix the input. Waiting for a provider incident is not a useful test plan. The sample creates a controlled race instead. inference.py sends a streaming request to Kimi-K3 with a one-second first-event deadline: result = chat completion token key, "Kimi-K3", build forced failure prompt , extra headers={ "x-ltg-request-timeout": "1000", "x-ltg-max-attempts": "1", }, stream=True, max tokens=64, The generated input is intentionally heavy, approximately 22,000 tokens, and includes a unique nonce so a repeated run cannot rely on provider KV caching to shorten prefill. The goal is for the primary's first event to miss the one-second deadline while GLM-5.3-Flash completes as the fallback. Provider latency changes over time, so this is not presented as an absolute guarantee. The script tries up to three times and reports honestly if the primary answers within the deadline or if both attempts time out. The client reads two headers from the raw response: served model = raw.headers.get "x-ltg-served-model" attempts = raw.headers.get "x-ltg-attempts" A successful failover should show: requested model: Kimi-K3 served model: GLM-5.3-Flash x-ltg-attempts: 2 or more The SDK response model is useful, but the gateway headers make the routing decision explicit on the live path. The next step reads the gateway spend events for the issued token key: events = client.llm token gateway.spend.retrieve events start date=start date, end date=end date, token key id=token key id, page number=1, page size=50, for event in events.data: print event.requested model, "- ", event.model, "attempts:", event.attempts, "status:", event.status, "cache hit:", event.cache hit, When requested model and model differ, the ledger provides the durable counterpart to the response header. The sample uses a half-open UTC date range and snapshot pagination so the report remains reproducible as the event set changes. GET /usage/summary adds totals and per-model rollups when the account has the llm token gateway.usage.read permission. A 403 from that endpoint is treated as a nonfatal warning because the spend-events ledger still contains the request-level proof. The gateway supports request-level overlays for attempts, delay, backoff, and timeout: extra headers = { "x-ltg-max-attempts": "1", "x-ltg-retry-delay": "1000", "x-ltg-backoff": "exponential", } These controls adjust one request without rewriting the token-group policy. The sample also updates cache ttl to 60 seconds and sends the same prompt twice. Gateway caching is best effort, so the script does not invent a cache hit: it reports x-ltg-attempts: 0 when the repeat is served from cache and otherwise directs you to the ledger's cache hit field. Every update and delete follows the mutation-safety pattern: If-Match Idempotency-Key A stale ETag returns 412 , preventing one process from overwriting another process's newer configuration. Clone the repository and install the pinned dependencies: git clone https://github.com/team-telnyx/telnyx-code-examples.git cd telnyx-code-examples/gateway-fallbacks python -m venv .venv source .venv/bin/activate pip install -r requirements.txt cp .env.example .env Add your Telnyx account API key to .env : TELNYX API KEY=your telnyx api key here PRIMARY MODEL=Kimi-K3 FALLBACK MODEL=GLM-5.3-Flash Run the local smoke tests before making network requests: python -m pytest smoke test.py -v Then run the five-step demonstration: Creates the group and key. The key secret is printed once. python provision.py export GATEWAY TOKEN KEY="