Skip to content

fix(models): route Gemini to the Google API on every vendor - #260

Merged
ltmoerdani merged 1 commit into
ltmoerdani:mainfrom
NeoMetric:agents/fix-gemini-routing-opencodego-models
Oct 20, 2026
Merged

ltmoerdani merged 1 commit into
ltmoerdani:mainfrom
NeoMetric:agents/fix-gemini-routing-opencodego-models

Conversation

@NeoMetric

@NeoMetric NeoMetric commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Gemini models served by OpenCode Go (gemini-4-argon-contributor) failed every request with 400 "Model does not support this protocol." — the gemini registry row was restricted to opencodezen, so Go fell through to the chat-completions catch-all while the gateway serves the model Google-native. The row now applies to every vendor; since Go's /zen/go/v1 base does not serve the :streamGenerateContent route at all, providers carry an optional googleModelsUrl (Go default https://opencode.ai/inference/go/v1/models per OpenCode's own endpoint table; Zen and custom apiBaseUrl gateways keep deriving from modelsUrl, issue #247 contract intact).

Also in this change:

  • feat(thinking): GeminiThinking strategy — Thinking Effort picker mapped to generationConfig.thinkingConfig (thinkingLevel + includeThoughts), opencodego.thinking.gemini workspace default. Verified live: the level is accepted and thinking is billed (thoughtsTokenCount). Thought summaries are not relayed upstream (opaque thoughtSignature only) — documented in thinking/gemini.ts.
  • fix(usage): updateRequestUsageSummary understands Google usageMetadata and candidates[].finishReason (Gemini usage logged n/a and metered 0).
  • fix(usage): [diag-empty-response] no longer requires completion tokens to fire — that precondition made the raw-SSE dump unreachable exactly when Google parsing produced nothing.
  • fix(models): stop sending temperature to Google models that declare temperature: false.

Verified live on gemini-4-argon-contributor (200 OK, thinkingLevel accepted, real usage); deepseek-v4.1-flash / mimo-v2.6-flash regression checked. npm run lint 7/7; npm test 540/540.

📝 What does this change?

🧪 How did you test it?

✅ Checklist

  • npm run compile passes
  • npm run lint passes
  • npm test passes
  • npm run package produces a VSIX
  • I tested it works
  • I updated docs/CHANGELOG if needed

Closes #259

Gemini models served by OpenCode Go (gemini-4-argon-contributor) failed
every request with 400 "Model does not support this protocol." — the
gemini registry row was restricted to opencodezen, so Go fell through to
the chat-completions catch-all while the gateway serves the model
Google-native. The row now applies to every vendor; since Go's /zen/go/v1
base does not serve the :streamGenerateContent route at all, providers
carry an optional googleModelsUrl (Go default
https://opencode.ai/inference/go/v1/models per OpenCode's own endpoint
table; Zen and custom apiBaseUrl gateways keep deriving from modelsUrl,
issue ltmoerdani#247 contract intact).

Also in this change:
- feat(thinking): GeminiThinking strategy — Thinking Effort picker mapped
  to generationConfig.thinkingConfig (thinkingLevel + includeThoughts),
  opencodego.thinking.gemini workspace default. Verified live: the level
  is accepted and thinking is billed (thoughtsTokenCount). Thought
  summaries are not relayed upstream (opaque thoughtSignature only) —
  documented in thinking/gemini.ts.
- fix(usage): updateRequestUsageSummary understands Google usageMetadata
  and candidates[].finishReason (Gemini usage logged n/a and metered 0).
- fix(usage): [diag-empty-response] no longer requires completion tokens
  to fire — that precondition made the raw-SSE dump unreachable exactly
  when Google parsing produced nothing.
- fix(models): stop sending temperature to Google models that declare
  temperature: false.

Verified live on gemini-4-argon-contributor (200 OK, thinkingLevel
accepted, real usage); deepseek-v4.1-flash / mimo-v2.6-flash regression
checked. npm run lint 7/7; npm test 540/540.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@ltmoerdani

Copy link
Copy Markdown
Owner

@NeoMetric nice one on splitting the Go Google base out of modelsUrl, that was the part I was most nervous about because it has to keep the apiBaseUrl contract from #247 intact, and it does.

Ran it against current main (2986b1f): lint gate 7/7, 540/540 tests, tsc clean, merge state CLEAN. Base is already the tip of main, no rebase needed.

I spent most of my time on the endpoint, since that's the part that could bite us. I probed both candidate bases and the error envelope tells you which adapter you hit: /inference/go/v1/models/{id}:streamGenerateContent comes back in Google's google.rpc.ErrorInfo shape, same as the /inference/go/google/v1beta/... path from OpenCode's own Go endpoint table, while the chat-completions path comes back in OpenCode's own error shape. So your base lands on the Google adapter even though it differs from the documented one. My local Go key is expired so I could not get a 200 out of it, the envelope difference was enough to tell the two apart.

Also good: reusing normalizeGoogleUsage for the request summary instead of writing a second mapping, and reading candidates[].finishReason in the usage extractor directly.

One blocker though, this one needs a fix before it goes in.

thinkingLevel: "minimal" gets silently dropped. The picker schema comes from models.dev reasoning_options, and for gemini-3.5-flash, gemini-3-flash, gemini-3.6-flash and gemini-3.5-flash-lite that list is ["minimal","low","medium","high"], so the picker does show Minimal. But GeminiThinking.applyOverride hands the hardcoded GEMINI_EFFORTS list to applyEffort, and "minimal" is not in it, so the pick gets rejected and settings.gemini stays at off.

Confirmed against your branch:

### gemini-3.5-flash
  picker enum : ["off","minimal","low","medium","high"]
  pick=minimal -> applied=false settings.gemini=off  payload={}
  pick=low     -> applied=true  settings.gemini=low  payload={"thinkingConfig":{"includeThoughts":true,"thinkingLevel":"low"}}

So the user picks Minimal, the request goes out with no thinkingConfig at all, and the model thinks at its own default with no feedback. That is the "control that silently does nothing" case the fallback strategy exists to avoid, and it lands on exactly the models where the level matters most.

What I would do: build the allowed list from metadata.reasoningOptions instead of the constant, so the picker and the override can't disagree. applyOverride doesn't receive metadata today, but FallbackThinking already takes it in the constructor, so GeminiThinking can do the same. Adding "minimal" to the constant also closes it, it just leaves the next models.dev value to break it again.

Two smaller notes, neither blocking on its own:

  1. MODELS_WITHOUT_TEMPERATURE in src/models/modelTables.ts only holds the two Kimi models. The bundled offline snapshot is built from MODEL_LIMITS_BY_PROVIDER plus fallbackModelMetadata, and temperature: false only propagates from that table, so gemini-4-argon and gemini-4-argon-contributor are not covered. If the live models.dev fetch fails, metadata.temperature ends up undefined and the builder sends temperature again, which is the 400 this PR is fixing. The comment on that table says it exists so we still omit temperature when the live fetch fails, so adding those two ids would keep the fix holding offline.

  2. No docs/issues/ entry. The routing change itself is covered by the update to docs/features/17-..., so this is not a missing-docs complaint. The root cause and the minimal-level gotcha would be worth an issue doc the way [BUG] Models are shown twice (once with agent-host) #41 did for the gpt-5.6-luna routing fix, since both are the kind of thing someone hits again in six months.

Small aside: src/thinking.ts also exports MuseThinking now, which this PR does not use. Harmless, just unrelated to the diff.

On the registry drift you flagged for minimax-m3 and the qwen3.8/3.7 rows, deferring each until it gets a live check is the right call. Worth keeping in mind that those models will keep 400ing on Go until then, so it may be worth opening issues for them so they do not get lost.

On the merge itself, I want to keep the commit history intact, so this goes in as a regular merge commit, not a squash. I will re-run the gate once the minimal fix lands.

@ltmoerdani
ltmoerdani merged commit 61a813f into ltmoerdani:main Oct 20, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] "Model does not support this protocol" for gemini-4-argon-contributor

2 participants