{"slug": "five-ways-my-gemini-for-claude-code-setup-broke-and-the-fixes", "title": "Five ways my Gemini-for-Claude-Code setup broke, and the fixes", "summary": "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.", "body_md": "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.\n\nThe snippets are trimmed from the broker I ended up writing (Python, `httpx`, Gemini REST `generateContent`).\n\n**Symptom:** a model ID that worked in every tutorial returned HTTP 404, saying the model is no longer available to new users.\n\n**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.\n\n**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\".\n\n``` php\ndef _classify(status: int, message: str) -> str:\n    m = message.lower()\n    if status == 429 and (\"quota\" in m or \"billing\" in m):\n        return \"quota\"\n    if status in RETRYABLE:          # {429, 500, 502, 503, 504}\n        return \"overloaded\"\n    if status == 404:\n        return \"not_found\"           # retired or misspelt model: don't retry\n    return \"bad_request\"\n```\n\n**Symptom:** 503s on two Flash models at the same time, with the agent sitting there for minutes.\n\n**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.\n\n``` php\ndef _delay(self, attempt: int, r: httpx.Response | None) -> float:\n    delay = min(self.cfg.max_delay_s, self.cfg.base_delay_s * 2 ** (attempt - 1))\n    ra = r.headers.get(\"retry-after\") if r is not None else None\n    if ra and ra.isdigit():\n        delay = min(self.cfg.max_delay_s, float(ra))\n    return delay + random.uniform(0, delay * 0.25)\n```\n\n**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.\n\n**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.\n\n**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.\n\n```\nclass QuotaBreaker:\n    \"\"\"Once the project's quota is exhausted, stop calling the API for a while.\"\"\"\n    def __init__(self, cooldown_s: float):\n        self.cooldown_s, self._tripped_at = cooldown_s, None\n        self._lock = threading.Lock()\n\n    def trip(self) -> None:\n        with self._lock:\n            self._tripped_at = time.monotonic()\n\n    def is_open(self) -> bool:\n        with self._lock:\n            return self._tripped_at is not None and time.monotonic() - self._tripped_at < self.cooldown_s\n```\n\n**Symptom:** HTTP 200, no error, and `text == \"\"`.\n\n**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`.\n\n**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:\n\n```\nparts = (c.get(\"content\") or {}).get(\"parts\") or []\nres.text = \"\".join(p.get(\"text\", \"\") for p in parts if not p.get(\"thought\"))\nres.ok = bool(res.text)\nif not res.ok:\n    hint = \" - raise max_output_tokens (thinking used the budget)\" if c.get(\"finishReason\") == \"MAX_TOKENS\" else \"\"\n    res.error = f\"empty response (finishReason={c.get('finishReason')}){hint}\"\n```\n\n**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.\n\n**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`).\n\nOne 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`:\n\n```\nevent = json.loads(sys.stdin.buffer.read().decode(\"utf-8-sig\"))\n```\n\nIt'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.\n\nClaude 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.\n\nThe 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.\n\nWhat 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.\n\n*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.*", "url": "https://wpnews.pro/news/five-ways-my-gemini-for-claude-code-setup-broke-and-the-fixes", "canonical_source": "https://dev.to/fablefusestudios/five-ways-my-gemini-for-claude-code-setup-broke-and-the-fixes-h7n", "published_at": "2026-10-12 00:21:50+00:00", "updated_at": "2026-10-12 00:35:27.705892+00:00", "lang": "en", "topics": ["ai-agents", "ai-tools", "large-language-models", "developer-tools", "ai-infrastructure"], "entities": ["Gemini", "Claude Code", "Anthropic", "Google", "httpx", "Python"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/five-ways-my-gemini-for-claude-code-setup-broke-and-the-fixes", "markdown": "https://wpnews.pro/news/five-ways-my-gemini-for-claude-code-setup-broke-and-the-fixes.md", "text": "https://wpnews.pro/news/five-ways-my-gemini-for-claude-code-setup-broke-and-the-fixes.txt", "jsonld": "https://wpnews.pro/news/five-ways-my-gemini-for-claude-code-setup-broke-and-the-fixes.jsonld"}}