fix(session): omit tool definitions for models that cannot call tools - #41463
fix(session): omit tool definitions for models that cannot call tools#41463IngTian wants to merge 1 commit into
Conversation
`capabilities.toolcall` is populated from provider config and models.dev but is
never read when building a request. `resolveTools` filters only by permission
rules and the per-message `tools` map, so every registered tool is declared to
the provider even when the selected model cannot use any of them.
Most providers ignore the unusable definitions. Some reject the request outright:
Vertex Gemini image models (`gemini-2.5-flash-image`, "Nano Banana") answer
Unable to submit request because the model does not support function calling
so every send fails. The error names the model, which points users away from the
cause, and the request is billed. Setting `tool_call: false` on the model looks
like the fix but changes nothing, because the flag is never consulted here. The
only working workaround today is an agent with `permission: {"*": deny}`, and a
user cannot discover it from the error.
`resolveTools` now returns no tools when the model cannot call them. This runs
before the provider-specific adjustments, so the existing github-copilot `_noop`
path still engages when prior tool calls are replayed — the same behavior an
agent-level wildcard deny already produces today.
Verified by capturing the outgoing provider request from a source build against a
recording endpoint, with the stock `build` agent and no config overrides:
`gemini-2.5-flash-image` now sends no `tools` field at all (previously 12
declarations), while `gemini-3.6-flash` still sends all 12 in the same session.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Thanks for your contribution! This PR doesn't have a linked issue. All PRs must reference an existing issue. Please:
See CONTRIBUTING.md for details. |
Points at anomalyco/opencode#41463 and issue #41464 so a future maintainer can tell whether the constraint still applies, and notes that the section becomes history once a release carrying the fix ships. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Context on how I hit this: I first tried to work around it downstream in OpenChamber (openchamber#2787) and couldn't do it safely, which is what convinced me the guard belongs here. For anyone else who lands on that approach — the per-send
Hence the one-line guard rather than anything downstream. |
|
Heads-up that
It isn't a cherry-pick: tool assembly there lives in |
Issue for this PR
Closes #41464
Type of change
What does this PR do?
capabilities.toolcallis set from provider config but never read when building a request, soresolveToolsdeclares every tool even to models that can't call any. Providers that reject function calling then fail every send — Vertex Gemini image models answerUnable to submit request because the model does not support function calling.The fix is one guard in
resolveTools: return no tools when!input.model.capabilities.toolcall.modelis already on the same input, so nothing new is threaded through.Suppressing the whole set (rather than filtering individual tools) is what's needed, because these providers reject on the presence of any declaration. Doing it in
resolveToolscovers every send path at once; the guard sits before the provider-specific tweaks, so thegithub-copilot_nooppath still engages on an empty set exactly as it does today when an agent wildcard-deny empties it.How did you verify your code works?
Captured the outgoing provider request from a source build against a recording endpoint, stock
buildagent, no config overrides:toolsfieldgemini-2.5-flash-image(toolcall: false)gemini-3.6-flash(toolcall: true), same sessionTwo tests added next to the existing
LLMRequestPrep.preparecoverage intest/provider/transform.test.ts. I checked the negative one fails without the guard (exactly 1 failure), so it's real coverage.bun test test/provider/536 pass ·bun test test/session/396 pass, 0 fail ·bun run typecheckclean.Not verified: live Vertex traffic — the endpoint was a local recording proxy, so the 400 is reproduced by request shape, not by Google's response.
Screenshots / recordings
Not a UI change.
Checklist