Skip to content

Troubleshooting

Common shunt errors and how to fix them.

Updated View as Markdown
Symptom Cause / Fix
Sentry: upstream SSE stream was cut before a terminal event A streaming response ended without a terminal event. Read the event’s cut_kind tag: transport_error means the body read failed (upstream_error has the message), eof means the upstream closed cleanly mid-message, marker means shunt detected the cut and synthesized a completion for the client. The sse_events, bytes_forwarded, last_event_type, elapsed_ms, and ttft_ms extras say how far the response got and how long it took.
ChatGPT auth not found; run codex login shunt can’t read ~/.codex/auth.json. Run codex login.
authentication_error on a mapped model Expired/absent provider credential — re-run codex login, or export OPENAI_API_KEY. shunt surfaces the backend’s real detail message.
Gemini fails instead of completing when Code Assist closes the stream Gemini success responses require both an explicit provider finish and clean transport closure. EOF or [DONE] without that finish, invalid UTF-8, malformed JSON or unsupported fields, oversized data, multiple candidates, data after terminal, or truncated responses fail explicitly. Streaming and unary modes apply the same terminal and provider-error semantics, never converting embedded provider errors into synthetic success.
Gemini tool history rejected before dispatch Client-visible tool_use IDs preserve authentic Code Assist call pairings and thoughtSignature on Gemini 3. Return the matching ID in tool_result. Malformed, duplicate, or orphaned metadata is rejected without repair. Tool calls imported from another provider (for example after a failover) carry no Gemini signature; on Gemini 3 the first such call in a turn is sent with Google’s documented placeholder signature instead of being rejected. Valid results become functionResponse in the next request. shunt does not persist or write back this metadata.
Gemini transient status not retried to the same upstream Gemini generations are non-idempotent. Only transient connection or timeout failures proven to have occurred before response headers are retried with the same selected token, project, and payload. Returned statuses and all body-time failures surface immediately without retrying the same upstream. The exception is a single-credential auth = "antigravity_oauth" provider, which still retries returned 429/502/503/504/529 statuses under the provider’s retry policy; pooled Antigravity accounts never retry.
400 … model is not supported when using Codex with a ChatGPT account You used a -codex slug (or one your account isn’t entitled to). Use an entitled slug from models.json (e.g. gpt-5.6-sol, gpt-5.5) or set upstream_model.
Gateway log: upstream refused the account; skipping it until the window ends (or a 400/402 reading You have reached your specified API usage limits or Your credit balance is too low) An Anthropic-protocol upstream (not a kimi_oauth pool, which relays these refusals unchanged) refused the whole account: an API usage limit, an empty credit balance, or a 402 billing_error. shunt advances to the next upstream in the chain, and the refusal field names which case it was. The client sees the 400/402 only when no other upstream served the request. A non-passthrough upstream is then skipped for new requests until the time in You will regain access on … UTC, or for 1 hour, and only a restart clears the skip; a config reload does not. To clear the cause, raise the limit for a usage limit, top up the balance for a credit balance, or fix the billing and payment details in the Anthropic Console for a billing_error. That does not end the current window early: the skip holds until its end, and when every element of a chain is skipped the chain is tried as configured. Plan-included Max API credit has been reported to refuse Claude Code traffic, so a shared upstream backed by it can be skipped for other clients for that hour. In a Claude OAuth pool, the refusing account is cooled for that window and the request rotates to the next account; the pool upstream itself is skipped only when its last answer is a refusal, until its earliest account can serve again.
/model doesn’t list your model For gpt-* ids use ANTHROPIC_CUSTOM_MODEL_OPTION; discovery only surfaces claude/anthropic-prefixed ids.
opus selects Opus 4.7 / sonnet selects Sonnet 4.6 Claude Code’s built-in alias table pins those tiers for gateway sessions. Pin the tier client-side with ANTHROPIC_DEFAULT_OPUS_MODEL=claude-opus-5, or remap the id in shunt — see Model Aliases.
Context window shows 200K on Opus/Fable Claude Code only trusts a model’s native 1M window when the base URL is api.anthropic.com. Select opus[1m] / fable[1m] — see Model Aliases.
Fable missing from /model, or claude-fable-5 falls back to Opus Fable is filtered out behind a plain ANTHROPIC_BASE_URL. Set ANTHROPIC_DEFAULT_FABLE_MODEL=claude-fable-5, or use gateway login — see Model Aliases.
Discovery never fires It’s gated on a gateway credential (ANTHROPIC_AUTH_TOKEN, API key, or apiKeyHelper) plus CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY=1. Debug with claude --debug → [gatewayDiscovery] lines.
config check failed Run shunt check for the exact reason (bind address, unknown provider in a route, wrong adapter/auth).
Prompt caching uses the 5-minute TTL Check which setup you’re in first. Behind a plain ANTHROPIC_BASE_URL, “subscription users within included usage receive 1-hour TTL automatically” and only those “drawing on usage credits” need ENABLE_PROMPT_CACHING_1H=1 (1-hour cache writes bill “at a higher rate”; FORCE_PROMPT_CACHING_5M=1 forces 5 minutes back). On a gateway login session the client omits the extended-cache-ttl beta outright, so the 5-minute TTL is expected there. See Client Environment Variables.
Remote Control or voice dictation unavailable Expected behind a gateway credential, not a shunt bug. Both are unavailable while ANTHROPIC_AUTH_TOKEN, ANTHROPIC_API_KEY, or an apiKeyHelper is set, and as of Claude Code v2.1.196 Remote Control is also disabled while ANTHROPIC_BASE_URL points at a non-Anthropic host. Run claude doctor to see which variable to unset — see Features a gateway credential turns off.
Fast mode reports a connectivity error while inference works The fast-mode availability check calls api.anthropic.com directly instead of following ANTHROPIC_BASE_URL, so it never reaches shunt. On a network that blocks direct egress to api.anthropic.com it fails while gateway inference keeps working. The WebFetch domain safety check bypasses the base URL the same way — see What a base URL does not redirect.
Claude Code asks you to log in Set an Anthropic credential (ANTHROPIC_AUTH_TOKEN / login) that shunt can forward for unmapped models. A base URL alone is not a credential.
Effort stuck at medium on a mapped model Check the model id first: ids on Claude Code’s legacy effort deny list (claude-sonnet-4-5, claude-haiku-4-5) never send output_config.effort, and no environment variable overrides that — remap to a different id. Otherwise confirm an effort level is actually set (/effort or CLAUDE_CODE_EFFORT_LEVEL). CLAUDE_CODE_ALWAYS_ENABLE_EFFORT=1 does not help here; verified against v2.1.224, a custom gateway id already sends effort without it. See Effort & Context.
400 Invalid schema for function '…': '…' is not a 'regex' on a mapped model The OpenAI backend compiles every tool-schema pattern with Python’s re, which rejects JavaScript-only regexes (\p{Cc}, (?<name>…), \u{…}); Claude Code’s Artifact tool ships one. shunt drops such patterns from the forwarded schema — outside strict mode a pattern is advisory, so only a hint is lost, and the description does not always restate it. Update if you still see this.
Tool calls on a mapped model arrive with every optional parameter filled The backend normalizes a function tool without an explicit strict toward strict mode, where a closed parameter object (additionalProperties:false) behaves as if every property were required, so the model emits values for properties the caller never set. shunt pins strict:false on forwarded function tools and on the specs replayed inside tool_search_output, keeping optionals optional; xAI/Grok routes withhold the field (unverified there), and native tool_search omits it (its schema has no optional properties). Update if you still see this.
Tool search inactive on a mapped model (every tool’s schema sent each turn) Set ENABLE_TOOL_SEARCH=true. Claude Code auto-disables optimistic tool search behind a non-Anthropic base URL; shunt forwards tool_reference blocks and reveals deferred schemas on demand — see ChatGPT / Codex → Tool search.
400 Deferred custom tools are only supported on Anthropic models… Received stealth/ox-alpha (or another non-Claude slug) Claude Code sent defer_loading / a tool_search_tool_* on a non-Anthropic Messages model. shunt strips those fields for non-claude* / non-anthropic/* upstream ids before forwarding; Anthropic slugs on OpenRouter keep the protocol.
Tool search works but reclaims no context (still paying the text shim’s cache-invalidation cost) Native tool_search defaults (“auto”) to known-good hosts only — the ChatGPT/Codex backend and api.openai.com — plus the flavor/model gates (stock OpenAI/ChatGPT-Codex flavor, gpt-5.4+ model). A custom OpenAI-compatible endpoint (LiteLLM, vLLM, OpenRouter, self-hosted) is not auto-enabled; set tool_search = true on it once you’ve confirmed it implements tool_search items. If you expected native on a known host and it’s still shimming, confirm you haven’t set tool_search = false — see ChatGPT / Codex → Tool search → Native protocol.
Session stuck after a context-length error on a mapped model shunt rewrites upstream overflow errors to prompt is too long … so Claude Code auto-compacts and retries — see Context overflow recovery. If it recurs every few turns, lower CLAUDE_CODE_MAX_CONTEXT_TOKENS to the model’s real window.
Stream dies behind Cloudflare (524) Keep sse_keepalive_seconds at its default (30) instead of 0.
401 on mapped models on a shared gateway Missing/invalid client token — set ANTHROPIC_AUTH_TOKEN=<token> (accepted as Authorization: Bearer, pool-only gateways) or ANTHROPIC_CUSTOM_HEADERS="x-shunt-token: <token>" (recommended when passthrough models are mixed in, so each caller’s real credential keeps a slot of its own — a client token that does land in Authorization / x-api-key is cleared from that slot rather than forwarded); see Sharing a Gateway.
429 on Anthropic-adapter models Check rate_limit_kind in the gateway log. quota (has retry-after / anthropic-ratelimit-* headers) is a real rate limit — back off or reduce parallel load. client-shape-rejection (OAuth request, neither header, body just "Error") means api.anthropic.com rejected a subscription-OAuth request that doesn’t look like Claude Code — non-Claude-Code clients must use an API key instead of an OAuth token. Some Claude Code requests reach upstream without the identity block it looks for — the auto-mode permission classifier, and side queries such as mods’ $.model.complete with CLAUDE_CODE_ATTRIBUTION_HEADER=0. shunt restores the identity on the auto-mode classifier and on any Claude Code request (identified by x-claude-code-session-id, a claude-cli/ user agent, or x-app: cli) that carries no accepted marker. The repair applies only when the outbound bearer is a subscription OAuth token, never to count_tokens (which upstream does not gate), and an empty x-claude-code-session-id does not count. It is what keeps auto mode from failing closed with “model temporarily unavailable” and side queries from being rejected with this bare 429; a quota 429 still reaches the client. Other clients, and every other request, keep byte-for-byte passthrough, apart from the thinking-signature strip described on the Anthropic provider page. To keep those checks off an expensive model, pin them with classifier_model. no-ratelimit-headers (non-OAuth credential) is a provider 429 without rate-limit metadata — treat it as quota.
Claude Code reports that the session isn’t eligible for auto mode’s no-charge classifier requests Auto mode asks the API to classify each tool use server-side, and only api.anthropic.com answers it. shunt relays that request and its answer untouched on Anthropic routes. A route served by another upstream (Codex/Responses, Cursor, Gemini, Antigravity) has no server verdict to relay, so shunt answers “could not evaluate” per action rather than returning nothing: Claude Code classifies that one action locally and asks the server again on the next turn, instead of retiring the server classifier — and billing its own — for the rest of the session. Anthropic-protocol third parties (Kimi, OpenRouter, DeepSeek, …) additionally have the safeguards field and its dangerous-tool-use-* beta token stripped before the request goes out: none of them is known to accept the field, and a 400 naming it would make Claude Code deny every auto-mode tool use until /clear, whereas a stripped field only costs that turn’s server verdict. To turn the feature off entirely and keep classification on the client, set CLAUDE_CODE_AUTO_MODE_SERVER=0.
503 overloaded_error on a shared gateway The gateway is at its inbound concurrency cap and shed the request instead of queueing it (body message too many requests are already in flight, with Retry-After: 1). This is shunt’s own admission control, not an upstream 503 — an upstream one is relayed with the provider’s own message. Retry after the delay, reduce parallel load, or raise max_concurrent_requests and restart (the limit is fixed at startup).
shunt run exits at startup with a store … error The optional [store] failed to open. Connection and open errors name the store target (never the password); the snapshot-import error below names the subsystem, snapshot path, and reason instead. is held by another shunt instance (<host>:<pid>): another shunt process is using the store; stop it, or after a crash wait up to hold_stale_seconds (default 30) for its hold to go stale. store statement failed: … lock timeout at startup (PostgreSQL): another session kept the instance-hold row locked for 5 s, usually a backend whose client is gone; end it (find it in pg_stat_activity, then pg_terminate_backend) or wait for the server to reap it, then restart. is at schema version N, newer than the M this binary supports: the database was migrated by a newer shunt and is left untouched; upgrade shunt. importing the <subsystem> state snapshot <path> into the store failed: nothing was imported for that subsystem, unless the reason is a failed commit: that import may have landed, and the next boot then reports it as already imported. Read the reason after the colon: a malformed or unsupported-version JSON snapshot means fix or remove the named file; an unreadable file means fix its permissions or path; a store transaction failure means check the store itself. Then restart. is unreachable: the database cannot be reached or created; shunt check prints the same cause without starting. requires a build with the store-postgres feature: use a release binary or build with --features store-postgres.
Gateway /oauth/token returns 503 temporarily_unavailable With a [store], the token could not be written, so it was not issued and the change was rolled back. For a refresh-token rotation, the client’s retry succeeds once the store is writable. For a device-flow login, the approved grant was already used, so the retried poll gets expired_token and the login must be restarted. In both cases, check the shunt.store.write_failures{subsystem="gateway"} counter and the store’s disk or connection. A warning whose reason reads postgres … did not complete within 15s means a PostgreSQL round trip got no answer within 15 s, because the connection stopped answering or the server was too slow. shunt gives up on each database round trip after 15 s, so token requests queued behind a stalled write wait a bounded time instead of indefinitely. When the step that gave up was the COMMIT, shunt reads the store back: if the write landed, the request succeeds and only the warning remains.

For the full gateway troubleshooting table, see Connect Claude Code to an LLM gateway.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close