Repository navigation
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) <[email protected]>
|
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. |
|
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 |
|
The duplicate bot on #41464 pointed me at prior reports of this exact root cause, which I'd missed:
So this is the fourth report and, as far as I can tell, the first patch. Three providers affected: Vertex Gemini image models, morphllm/vLLM without Happy to retarget this PR to close #35432 instead of #41464 (or both) — whichever keeps your tracker tidy. I've left a note on #35432 as well. |
|
Automated PR Cleanup Thank you for contributing to opencode. Due to the high volume of PRs from users and AI agents, we periodically close older PRs using automated criteria so maintainers can focus review time on the most active and community-supported contributions. This PR was closed because it matched the following cleanup criteria:
PRs created within the last month are not affected by this cleanup. If you believe this PR was closed incorrectly, or if you are still actively working on it, please leave a comment explaining why it should be reopened. A maintainer can review and reopen it if appropriate. Thanks again for taking the time to contribute. |
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