Skip to content

fix(google): map tool_choice onto functionCallingConfig - #968

Closed
DevMello wants to merge 4 commits into
lidge-jun:devfrom
DevMello:fix/google-tool-choice
Closed

fix(google): map tool_choice onto functionCallingConfig#968
DevMello wants to merge 4 commits into
lidge-jun:devfrom
DevMello:fix/google-tool-choice

Conversation

@DevMello

@DevMello DevMello commented Aug 3, 2026

Copy link
Copy Markdown

Summary

The google adapter dropped a client's tool_choice: "none", "required", and a forced tool all produced a wire body identical to auto, with only a system-prompt nudge stating the contract in prose. The wire compiler already validates and compiles toolConfig.functionCallingConfig; the adapter just never built it. buildRequest now maps the parsed choice for all three google modes: "none" to NONE, "required" to ANY, a forced tool to ANY with its resolved wire name in allowedFunctionNames, and the allowedTools variant's "required" half to ANY. "auto" and absent choices stay byte-identical, the config is only sent when declarations exist, and Claude on Antigravity keeps its protocol-required VALIDATED mode over a client choice.

Verification

  • Six new tests in tests/google-adapter.test.ts: every mapping, dotted-alias resolution to the namespaced wire name, byte-stable auto, the no-declaration guard, and the Antigravity VALIDATED interplay.
  • Probe harness: Anthropic tool_choice and chat-completions forced functions now produce the enforced wire config end to end.
  • bun run test, typecheck, lint:gui, privacy:scan.

Checklist

  • Scope stays focused and avoids unrelated cleanup.
  • Docs or release notes were updated when needed.
  • Security-sensitive changes were reviewed for secrets, auth, and unsafe defaults.

Summary by CodeRabbit

  • New Features

    • Added Google Gemini tool-selection support for disabled, automatic, required, and named tool choices.
    • Added support for restricting tool use to specific allowed functions.
  • Bug Fixes

    • Tool settings are no longer sent when no tools are available.
    • Corrected Claude on Antigravity behavior for disabled tool use while preserving validated and allowed-function selection.
    • Improved handling of tool-choice settings across supported request configurations.

@github-actions github-actions Bot added the bug Something isn't working label Aug 3, 2026
@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The Google adapter serializes tool choices into Gemini functionCallingConfig. It maps none, required, and named choices, omits configuration when appropriate, and preserves VALIDATED handling for Claude-on-Antigravity.

Changes

Gemini tool-choice serialization

Layer / File(s) Summary
Tool-choice mapping and request wiring
src/adapters/google.ts, tests/google-adapter.test.ts
The adapter maps tool choices to Gemini modes and resolved function names. It attaches toolConfig only when tools are declared. Tests cover mappings, filtering, name resolution, and omission rules.
Claude-on-Antigravity tool handling
src/adapters/google.ts, tests/google-adapter.test.ts
Claude requests retain VALIDATED precedence. A none choice removes tool declarations and configuration. Tests verify Claude and Gemini behavior separately.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant buildRequest
  participant toolChoiceToGeminiToolConfig
  participant resolveToolChoiceWireName
  participant GeminiAPI
  buildRequest->>toolChoiceToGeminiToolConfig: pass declared tools and toolChoice
  toolChoiceToGeminiToolConfig->>resolveToolChoiceWireName: resolve named tool choice
  resolveToolChoiceWireName-->>toolChoiceToGeminiToolConfig: return wire function name
  toolChoiceToGeminiToolConfig-->>buildRequest: return Gemini toolConfig
  buildRequest->>GeminiAPI: send request with toolConfig when tools exist
Loading

Possibly related PRs

Suggested reviewers: ingwannu, wibias, lidge-jun

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: mapping Google tool_choice values to functionCallingConfig.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 46756f5e50

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/adapters/google.ts
Comment on lines +334 to +335
const toolConfig = tools ? toolChoiceToGeminiToolConfig(parsed) : undefined;
if (toolConfig) body.toolConfig = toolConfig;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Honor tool_choice none for Claude Antigravity

When googleMode === "cloud-code-assist" routes to a Claude model, a request with tool_choice: "none" and a non-empty tool catalog gets mode: "NONE" here, but the later Claude Antigravity block overwrites that mode to VALIDATED while leaving body.tools intact. In that scenario the upstream still receives callable declarations and may emit tool calls even though the client explicitly disabled tools; preserve the no-tools contract by suppressing declarations for none or otherwise avoiding the VALIDATED override for that case.

AGENTS.md reference: src/AGENTS.md:L19-L19

Useful? React with 👍 / 👎.

@Wibias
Wibias marked this pull request as draft August 3, 2026 21:00
@Wibias

Wibias commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

Please put your Pull-Request on Ready for Review, once you are finished.

@DevMello
DevMello marked this pull request as ready for review August 3, 2026 21:33

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tests/google-adapter.test.ts (1)

192-240: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Test the complete byte-stability contract.

The test title claims that allowedTools + auto is byte-identical. That choice filters body.tools, so it is not identical to an unrestricted request. The current assertions only check toolConfig; regressions in contents, systemInstruction, or declarations would pass. (raw.githubusercontent.com)

Rename the test. Compare serialized bodies for "auto" and absent choices. Assert the filtered declaration names separately for allowedTools + auto.

Proposed test adjustment
-  test('"auto", absent, and allowedTools+auto stay byte-identical (no toolConfig)', async () => {
-    expect((await geminiBody(parsedWithChoice("auto"))).toolConfig).toBeUndefined();
-    expect((await geminiBody(parsedWithChoice(undefined))).toolConfig).toBeUndefined();
-    expect((await geminiBody(parsedWithChoice({ allowedTools: ["get_weather"], mode: "auto" }))).toolConfig).toBeUndefined();
+  test('"auto" and absent stay byte-identical; allowedTools+auto omits toolConfig', async () => {
+    const autoBody = await geminiBody(parsedWithChoice("auto"));
+    const absentBody = await geminiBody(parsedWithChoice(undefined));
+    expect(JSON.stringify(autoBody)).toBe(JSON.stringify(absentBody));
+    expect(autoBody.toolConfig).toBeUndefined();
+
+    const allowedAutoBody = await geminiBody(
+      parsedWithChoice({ allowedTools: ["get_weather"], mode: "auto" }),
+    );
+    expect(allowedAutoBody.toolConfig).toBeUndefined();
+    const declared = (allowedAutoBody.tools as { functionDeclarations: { name: string }[] }[])
+      [0].functionDeclarations.map(d => d.name);
+    expect(declared).toEqual(["get_weather"]);
   });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/google-adapter.test.ts` around lines 192 - 240, Update the test named
`"auto", absent, and allowedTools+auto stay byte-identical (no toolConfig)` to
compare serialized bodies from `"auto"` and an absent choice, covering contents,
systemInstruction, tools, and all other fields. Rename the test to reflect that
only auto and absent choices are byte-identical, then separately assert that the
allowedTools-plus-auto request contains only the expected filtered declaration
names while retaining the existing no-toolConfig assertion.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/google-adapter.test.ts`:
- Around line 256-260: Update the Gemini request assertions near geminiRequest
to verify tools equals the exact declaration catalog ["get_weather",
"mcp__chrome__shot"] instead of only checking that tools is defined; retain the
existing toolConfig assertion.

---

Outside diff comments:
In `@tests/google-adapter.test.ts`:
- Around line 192-240: Update the test named `"auto", absent, and
allowedTools+auto stay byte-identical (no toolConfig)` to compare serialized
bodies from `"auto"` and an absent choice, covering contents, systemInstruction,
tools, and all other fields. Rename the test to reflect that only auto and
absent choices are byte-identical, then separately assert that the
allowedTools-plus-auto request contains only the expected filtered declaration
names while retaining the existing no-toolConfig assertion.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 7a020c23-e82a-4f00-9b3b-de838de6999f

📥 Commits

Reviewing files that changed from the base of the PR and between 46756f5 and a366934.

📒 Files selected for processing (2)
  • src/adapters/google.ts
  • tests/google-adapter.test.ts

Comment thread tests/google-adapter.test.ts
@Wibias
Wibias marked this pull request as draft August 3, 2026 22:35
@Wibias

Wibias commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

You are putting it on Ready for Review and changing it afterwards. Please only change it to ready for review once you are completely finished.

@DevMello

DevMello commented Aug 3, 2026

Copy link
Copy Markdown
Author

Sorry! That was just me addressing a CodeRabbit review comment that came in after I marked it ready. Nothing else changed.

@DevMello
DevMello marked this pull request as ready for review August 3, 2026 22:40
@Wibias
Wibias marked this pull request as draft August 4, 2026 00:30
@Wibias

Wibias commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

[GD] Verdict: changes-requested

TLDR

  • PR: fix(google): map tool_choice onto functionCallingConfig #968fix(google): map tool_choice onto functionCallingConfig
  • Head: 9ba66a18 on dev (mergeStateStatus: CLEAN, not draft, fork PR by @DevMello — reviewed as a foreign PR)
  • Decision: useful and well-tested, but not merge-ready yet — owner actions remain (resolve the open Codex thread, update from latest dev, get CI to actually run, fix one overclaiming test name).
  • Usefulness: real bug fix — the Google adapter ignored tool_choice, so none/required/forced tools silently degraded to auto with only a prose nudge. This PR enforces the choice on the wire. Claimed behavior verified.
  • Bugs: none blocking. One Low edge (undeclared forced tool name reaches allowedFunctionNames without a matching declaration).
  • Security: Pass — no confirmed findings on the targeted surfaces.
  • Spec / standards: clean. PR body matches the code; no repo-standard violations; no docs gap.
  • Reviews: CodeRabbit inline thread addressed (9ba66a18); CodeRabbit outside-diff finding (test-name overclaim) open and real; Codex bot P2 thread unresolved (code already fixes it, thread needs owner reply+resolve).
  • Base / CI: head is ~142 commits behind dev. No branch protection configured; the repo's Cross-platform CI has never run on this head — every run is action_required at the fork-approval gate. Local gates: focused 16/16, typecheck, privacy scan green; full suite has zero PR-attributable failures (identical 15/15 Windows-environment failure set on the merge-base).
  • Gate: blocked — ship-gate.mjs blocker: reviewThreads:unresolved_review_threads.
  • Owner actions (foreign PR): (1) reply to and resolve the Codex thread on src/adapters/google.ts:335; (2) update from latest dev; (3) get CI approved/run for the fork; (4) rename/fix the overclaiming test; (5) optional: apply the 2 simplification candidates below.
  • Bottom line: the fix is correct, focused, and the tests are good. Ship after the owner actions above — none of them require code rework beyond a test rename and optionally two tiny cleanups.
Full verdict

Semantic propagation

Concept 1 — client tool-choice → Gemini wire toolConfig.functionCallingConfig

  • Authoritative source: src/types.ts OcxToolChoice (lines 205–213); src/adapters/google-wire-compiler.ts compileToolConfig (mode enum + name codec).
  • Producers: src/chat/inbound.ts, src/claude/inbound.ts, src/responses/parser.ts (mapToolChoice), src/images/loop.ts, src/server/responses/core.ts (image-gen rewrite).
  • Consumers: new toolChoiceToGeminiToolConfig in src/adapters/google.ts; siblings src/adapters/anthropic.ts (identical resolveToolChoiceWireName semantics), src/adapters/openai-chat.ts (toolChoiceToChatFormat), src/adapters/kiro.ts + kiro-tools.ts; src/adapters/tool-catalog-nudge.ts prose contract.
  • Public/derived representations: Gemini/Vertex/AI-Studio request.toolConfig.functionCallingConfig; CCA envelope request.toolConfig; codec-mapped allowedFunctionNames vs declarations.
  • Material variants: auto/absent/none/required; forced {name} (plain, dotted alias, namespaced, undeclared); {allowedTools, auto}; {allowedTools, required}; empty allowedTools; empty catalog; Claude vs Gemini on CCA; Vertex/AI-Studio paths.
  • Required equality: allowedFunctionNames ≡ declaration wire names post-codec (verified in google-wire-compiler.ts — both flow through the same toolNameCodec); auto/absent byte-identical (probe: true); noneNONE only when tools exist; no toolConfig when no tools.
  • Intentional differences: Claude-on-CCA overrides mode to VALIDATED (protocol); none on Claude drops declarations.
  • Positive tests: all 6 new tests; exact-catalog assertion for Gemini-on-CCA none.
  • Negative tests: no-tools ⇒ no toolConfig even with a choice; Gemini keeps the exact catalog under none. Gaps: (a) allowedTools+auto filtered-catalog is asserted only as "no toolConfig" — probe shows the body is not byte-identical (decls: ["get_weather"] vs ["get_weather","mcp__chrome__shot"]), so the test title overclaims (matches CodeRabbit); (b) undeclared forced name is untested — probe shows allowedFunctionNames: ["not_declared"] with no matching declaration (likely upstream 400 vs the old silent auto).
  • Result: pass (two coverage gaps recorded, both carried into the verdict).

Concept 2 — Claude-on-Antigravity tool availability for tool_choice: "none"

  • Authoritative source: CCA protocol claim in the code comment ("the real client always sets VALIDATED") + probe harness evidence in the PR body.
  • Producers: buildRequest CCA branch.
  • Consumers: compileGoogleWireBody (tools absent ⇒ no declarations), applyAntigravityReplay / sanitizeAntigravityClaudeSignatures (contents only).
  • Variants: Claude + none + tools; Claude + none + no-tools (pre-existing VALIDATED-only shape, unchanged); Gemini on CCA + none; Claude + forced/required (VALIDATED + allowed names).
  • Required equality: none suppresses declarations; config stays VALIDATED.
  • Tests: new test asserts tools undefined + VALIDATED config; Gemini-on-CCA exact catalog preserved.
  • Residual: VALIDATED with zero declarations is claimed to be "the wire shape of a tool-less Claude turn" — not independently verifiable from this repo; it was the pre-existing shape for tool-less Claude turns, so risk is low.
  • Result: matched.

Semantic propagation verdict

  • Concepts audited: tool-choice wire enforcement; Claude-Antigravity none handling.
  • Unmapped surfaces: none — all producers/consumers/siblings inspected repo-wide (rg toolChoice in src/ + docs).
  • Unproven equivalence assumptions: none.
  • Representation mismatches: none.
  • Variant coverage gaps: two (allowedTools+auto exact catalog; undeclared forced name).
  • Verdict: pass (gaps non-blocking for a changes-requested verdict, but recorded).

Usefulness

Real, user-visible bug: a Google-routed request with tool_choice: "none", "required", or a forced tool produced a wire body identical to auto — the model could ignore the client's explicit contract. The wire compiler already validated functionCallingConfig; the adapter just never built it. This PR fixes that for all three Google modes, keeps auto/absent byte-identical, and preserves the Antigravity VALIDATED requirement. Label bug is appropriate. No linked issue; spec source is the PR body.

Bugs / correctness

  • Method: bug-scope.mjs (deep, lenses boundary_conditions, parsing_serialization, resource_lifecycle, api_compatibility, filesystem_atomicity, network_cancellation, state_consistency + baseline silent_failures/resource_leaks/edge_cases). Bugbot: n/a-unavailable (Cursor-only harness; this host is Codex) — complementary pass only.
  • Findings:
    • Low / edge (confirmed by probe): forced tool name not in the catalog → allowedFunctionNames: ["not_declared"] with no matching declaration. Behavior change vs the old silent-auto degradation (likely upstream 400). Consistent with the Anthropic sibling's identical passthrough; suggest a guard or documented behavior, not blocking.
    • Residual: Claude-on-CCA noneVALIDATED with zero declarations (upstream acceptance unverifiable in-repo; pre-existing shape).
    • No silent-failure, leak, or state-consistency issues in the diff; the added code is pure request-object construction. Must-probe (locks/OAuth/finally/error-mapping): n/a — none of those surfaces are touched.

Security

  • Scope: security-scope.mjs targeted — required iac_docker, ai_agent_mcp; baseline authn/authz/secrets_config/injection; requireAiAgentSecurity: true; no deps/lockfile changes (requireDepsAudit: false); no removed-control leads.
  • Decision: Pass — no confirmed findings.
  • Coverage: iac_docker n/a (no IaC/Docker files changed; matched only on import-line churn); ai_agent_mcp done — this PR moves tool policy from prompt prose to wire-level enforcement; no tool-poisoning or injection surface added; authn/authz/secrets_config n/a (no auth/token/secret code touched; fixture key is "key"); injection done — client-supplied tool names flow only through JSON.stringify and the compiler's name codec (mode enum-validated), no shell/query/HTML sink. Secrets scan n/a (no new secret-bearing files). Adversarial pass: not run (not requested).
  • Residual: undeclared forced-name passthrough (Low, from the bug axis).

Spec / standards

  • Spec source: PR body (no linked issue). Claims verified against code + probe: auto/absent byte-identical (true), noneNONE, requiredANY, forced → ANY + resolved wire name (dotted alias confirmed), allowedTools required half → ANY, no-declaration guard, Claude-on-CCA none drops declarations, VALIDATED preserved. No unrequested scope (2 files, +122/−1).
  • Standards sources checked: root AGENTS.md, src/AGENTS.md, structure/04_transports-and-sidecars.md, tsconfig strict, privacy scan. No violations: helper placement matches the file's conventions, tests sit beside the subsystem, no event-contract/streaming/cancellation/error-mapping changes, no logging of request bodies/keys. Docs (proxy-formats.md already documents tool_choice; adapters.md google section doesn't claim otherwise) — no docs gap.

Reviews

  • Humans (owners first): @Wibias draft-readiness etiquette comments — addressed by the author (PR is ready, not draft); not review-blocking.
  • CodeRabbit: inline thread (exact-catalog assert) resolved — addressed in 9ba66a18. Outside-diff finding (test '"auto", absent, and allowedTools+auto stay byte-identical' overclaims; assert the filtered catalog separately) — open, confirmed real by probe: allowedAuto body ≠ auto body. Owner: rename the test and assert declared === ["get_weather"] (or decline with rationale on-thread).
  • Codex (chatgpt-codex-connector): P2 thread on src/adapters/google.ts:335unresolved. The code addresses it: a366934d8 deletes tools/toolConfig for none on Claude-Antigravity (test pins tools undefined + VALIDATED config). Owner: reply in-thread citing a366934d8 and resolve; until then ship-gate.mjs stays blocked.

Base / CI

  • Behind/conflicts: head is ~142 commits behind dev (merge-base 6a7351b4, dev tip a088e4b1+), no conflicts, rebaseable. Owner action: update from latest dev.
  • Required checks: no branch protection/rulesets configured on dev; GitHub reports CLEAN. The repo's Cross-platform CI (ci.yml, covers src/** + tests/**) has never run on this head — all runs for fix/google-tool-choice are action_required at the fork-approval gate (first-time contributor). CI evidence is missing, not green; a maintainer must approve the run(s).
  • Local tip verification (head 9ba66a18, clean worktree): bun test tests/google-adapter.test.ts → 16/16 pass; bun run typecheck → pass; bun run privacy:scan → pass. Full suite (4 CI-style shards): only pre-existing Windows-environment failures — the identical 15-failure set reproduces on the merge-base (631 pass / 15 fail both, exact set match: symlink EPERM, icacls, restore-ambiguity, config-surface parity); the extra failures under 4-way parallel shard load were machine contention (they pass single-process). Zero failures attributable to this PR.

Simplification (for the PR owner)

Foreign PR — nothing was edited or pushed. Two bounded, behavior-preserving candidates:

  1. C1 — dedupe the ccaProvider literal in teststests/google-adapter.test.ts (~lines 214, 255, and the existing test ~154): identical provider object appears 3×. Hoist a shared const. Invariants: same config; risk: very low; validation: bun test tests/google-adapter.test.ts.
  2. C2 — collapse duplicate ANY returnssrc/adapters/google.ts toolChoiceToGeminiToolConfig: choice === "required" and isAllowedToolChoice(choice) && choice.mode === "required" return the identical { functionCallingConfig: { mode: "ANY" } }. Combine into one guard. Invariants: identical output for both inputs; risk: very low; validation: focused test + typecheck.

Skipped with rationale: the Claude-none delete-then-rebuild sequence looks redundant but is load-bearing for the two distinct wire shapes (declarations dropped, VALIDATED kept) and is test-pinned — leave it. The test-name fix is a review finding, not a simplification.

Gate

ship-gate.mjs (read-only): blocked — single blocker reviewThreads:unresolved_review_threads. Required checks/base health/review policy/CODEOWNERS components: ready. No draft/WIP gate.

Bottom line

Useful, correct, well-scoped fix with good regression coverage — the Google adapter now actually enforces tool_choice. To reach merge-ready, the owner needs to: resolve the Codex thread on google.ts:335 (fix already landed in a366934d8), update from latest dev, get the fork's CI approved/run, and fix the overclaiming test name (+ assert the filtered catalog). The two simplification candidates are optional and low-risk.

@lidge-jun

Copy link
Copy Markdown
Owner

Carried into #973 (stack 6/6), with authorship preserved (cherry-pick -x, all four commits patch-id verified identical).

This is a good catch: the wire compiler already validated toolConfig.functionCallingConfig, so none, required, and a forced tool were all producing a body identical to auto with only a prose nudge in the system prompt. A contract stated only in prose is not a contract.

Checked specifically for interaction with your #943, which is already carried on this stack and touches the same file — #943 changes terminal truncation handling, #968 changes request-side compilation, separate hunks, git merge-tree clean. Both compose.

Verified on the stack: bun x tsc --noEmit exit 0, tests/google-adapter.test.ts + tests/google-vertex-stream.test.ts 27 pass / 0 fail, full suite 7740 pass / 8 skip / 0 fail. Closing since it now lives in #973.

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

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants