Skip to content

fix(session): omit tool definitions for models that cannot call tools - #41463

Closed
IngTian wants to merge 1 commit into
anomalyco:devfrom
IngTian:fix/omit-tools-for-non-toolcall-models
Closed

IngTian wants to merge 1 commit into
anomalyco:devfrom
IngTian:fix/omit-tools-for-non-toolcall-models

Conversation

@IngTian

@IngTian IngTian commented Aug 10, 2026 •

Copy link
Copy Markdown

Issue for this PR

Closes #41464

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

capabilities.toolcall is set from provider config but never read when building a request, so resolveTools declares every tool even to models that can't call any. Providers that reject function calling then fail every send — Vertex Gemini image models answer Unable 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. model is 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 resolveTools covers every send path at once; the guard sits before the provider-specific tweaks, so the github-copilot _noop path 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 build agent, no config overrides:

Model tools field declarations
gemini-2.5-flash-image (toolcall: false) absent 0 (was 12)
gemini-3.6-flash (toolcall: true), same session present 12

Two tests added next to the existing LLMRequestPrep.prepare coverage in test/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 typecheck clean.

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

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

`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]>
@github-actions

Copy link
Copy Markdown
Contributor

Thanks for your contribution!

This PR doesn't have a linked issue. All PRs must reference an existing issue.

Please:

  1. Open an issue describing the bug/feature (if one doesn't exist)
  2. Add Fixes #<number> or Closes #<number> to this PR description

See CONTRIBUTING.md for details.

@IngTian

IngTian commented Aug 10, 2026

Copy link
Copy Markdown
Author

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 tools map doesn't work as a client-side substitute:

  • {"<one-tool>": false} only removes one declaration; the remaining built-ins still trip the provider.
  • {"*": false} does suppress the set, but it persists as the session ruleset, so later sends on a tool-calling model in that session also get nothing.
  • {"*": true} to recover is a privilege escalation, not a no-op — Permission.evaluate returns early on allow, so it converts an agent's edit: deny to allow and bash: ask to allow, suppressing the approval prompt.
  • session.update({permission}) merges rather than replaces, so it can't clear a deny either.

Hence the one-line guard rather than anything downstream.

@IngTian

IngTian commented Aug 10, 2026

Copy link
Copy Markdown
Author

Heads-up that v2 has the same gap, if you want it covered there too.

capabilities.toolcall is never read on that branch either — git grep toolcall across packages/ai and packages/core returns nothing, and packages/ai/src/protocols/gemini.ts builds functionDeclarations from request.tools.length > 0 with no capability check.

It isn't a cherry-pick: tool assembly there lives in packages/core/src/session/model-request.ts around the session/context hook and tools.definitions, which is structurally different from resolveTools. Happy to port it if you'd like — I held off until this one gets a verdict so I build it the way you prefer rather than guessing.

@IngTian

IngTian commented Aug 10, 2026

Copy link
Copy Markdown
Author

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 --enable-auto-tool-choice, and NVIDIA NIM. The guard sits in resolveTools before any provider-specific serialization, so all three are covered by the same line.

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.

@github-actions

Copy link
Copy Markdown
Contributor

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:

  • The PR was created more than 1 month ago
  • The PR had fewer than 2 positive reactions
  • Positive reactions are counted as thumbs-up, heart, celebration, or rocket reactions on the PR

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Tool definitions sent to models that cannot call tools (Vertex Gemini image models reject every request)

1 participant