# Five ways my Gemini-for-Claude-Code setup broke, and the fixes

> Source: <https://dev.to/fablefusestudios/five-ways-my-gemini-for-claude-code-setup-broke-and-the-fixes-h7n>
> Published: 2026-10-12 00:21:50+00:00

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.*
