Five ways my Gemini-for-Claude-Code setup broke, and the fixes A developer documented five failure modes encountered while wiring Google's Gemini into Anthropic's Claude Code as a secondary research and review model, including retired model IDs returning 404, 503 overloads, quota-exhausting 429s, empty replies from thinking models, and an agent that tried to print the API key. The fixes center on a Python broker using httpx that classifies errors by message rather than status code, applies jittered exponential backoff with Retry-After support, trips a process-wide quota circuit breaker, and reserves output-token headroom for thinking models. The developer measured roughly 20 requests per day per model on the free tier and noted that a one-word reply consumed 2 answer tokens against 68 thinking tokens. I set up Claude Code to hand research and review jobs to Gemini. Over two days of building I hit a retired model, overloaded endpoints, an exhausted quota, an empty reply from a thinking model, and an agent that wanted to print my API key. None of these is exotic. Anyone wiring a second model into a coding agent will meet them, so here's each failure, how to recognise it from the API response, and what fixed it. The snippets are trimmed from the broker I ended up writing Python, httpx , Gemini REST generateContent . Symptom: a model ID that worked in every tutorial returned HTTP 404, saying the model is no longer available to new users. Why: model IDs get retired, and "retired for new users" means a fresh API key can't call a model that older keys still can. A hard-coded ID from a blog post or from your own memory is a time bomb. Fix: ask the API which models your key can actually call, and treat 404 as "fail fast with the API's own message", never "retry". php def classify status: int, message: str - str: m = message.lower if status == 429 and "quota" in m or "billing" in m : return "quota" if status in RETRYABLE: {429, 500, 502, 503, 504} return "overloaded" if status == 404: return "not found" retired or misspelt model: don't retry return "bad request" Symptom: 503s on two Flash models at the same time, with the agent sitting there for minutes. Fix: jittered exponential backoff that honours Retry-After , capped, then a fallback chain of models, with a small retry budget per model so a saturated chain fails in seconds rather than minutes. php def delay self, attempt: int, r: httpx.Response | None - float: delay = min self.cfg.max delay s, self.cfg.base delay s 2 attempt - 1 ra = r.headers.get "retry-after" if r is not None else None if ra and ra.isdigit : delay = min self.cfg.max delay s, float ra return delay + random.uniform 0, delay 0.25 Symptom: 429 RESOURCE EXHAUSTED . My first retry loop lumped it in with 503s, so it kept retrying, and every retry spent more of a daily allowance that was already gone. Why: a 429 for "too many requests right now" and a 429 for "you've used today's quota" look the same at the status-code level. The message tells them apart. On the free tier I measured roughly 20 requests per day per model, so this happens fast. Fix: classify quota separately see classify above and trip a process-wide circuit breaker. Every later call fails immediately, without touching the API, until a cooldown passes. class QuotaBreaker: """Once the project's quota is exhausted, stop calling the API for a while.""" def init self, cooldown s: float : self.cooldown s, self. tripped at = cooldown s, None self. lock = threading.Lock def trip self - None: with self. lock: self. tripped at = time.monotonic def is open self - bool: with self. lock: return self. tripped at is not None and time.monotonic - self. tripped at < self.cooldown s Symptom: HTTP 200, no error, and text == "" . Why: thinking tokens come out of the same output budget as the answer. In my logs, a one-word reply cost 2 answer tokens and 68 thinking tokens. Set a small maxOutputTokens and the thinking can use all of it, leaving nothing for the answer. The response tells you, but only in finishReason: MAX TOKENS . Fix: give thinking models real output headroom, drop the thought parts, and turn an empty answer into an error that names the cause instead of returning a blank string: parts = c.get "content" or {} .get "parts" or res.text = "".join p.get "text", "" for p in parts if not p.get "thought" res.ok = bool res.text if not res.ok: hint = " - raise max output tokens thinking used the budget " if c.get "finishReason" == "MAX TOKENS" else "" res.error = f"empty response finishReason={c.get 'finishReason' } {hint}" Symptom: to confirm the key was set, Claude Code proposed echo $GEMINI API KEY . That puts the key into the transcript, the logs and anything you later share. Fix: a PreToolUse hook that blocks the common leak paths echoing or printing KEY / TOKEN variables, printenv / env dumps, PowerShell $env: , reading .env files, writing live-looking keys into code and suggests a presence check instead -n "$VAR" && echo set . One gotcha: on Windows some shells pipe the hook's JSON input with a UTF-8 BOM. A plain json.loads then raises, and a hook that fails open on bad input lets everything through. Decode with utf-8-sig : event = json.loads sys.stdin.buffer.read .decode "utf-8-sig" It's pattern-based, so it's a seatbelt, not a sandbox. It catches the common ways an agent leaks a key, not every possible one. Claude stays the primary agent. It delegates research and second-opinion reviews through the broker, then checks what comes back before acting on it. The broker owns retries, fallback, the quota breaker and usage logging; the hook sits between the agent and your environment variables. The hook is free and MIT-licensed on its own: claude-secret-guard https://github.com/fable-and-fuse/claude-secret-guard . The full kit broker, a Claude Code subagent, /research and /second-opinion commands, the hook and 57 offline tests is Switchboard https://fable-and-fuse.github.io/brainrot-colour-chaos-site/switchboard/ , a one-time US$29 download. You bring your own Gemini API key, so Google's quotas and pricing apply. What failure modes have you hit when wiring two models together? I'd especially like to hear about ones that look like success, like the empty-string case. This post was drafted with help from an AI assistant Claude . The errors, numbers and code come from Switchboard's own source and build logs.