Skip to content

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

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

fix(session): omit tool definitions for models that cannot call tools#41463
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) <noreply@anthropic.com>
@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 added a commit to IngTian/openchamber that referenced this pull request Aug 10, 2026
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>
@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.

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.

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

1 participant