Skip to content

feat: emulate OpenAI tool calling for gemini-web via the webTools prompt shim (#7286)#7727

Merged
diegosouzapw merged 3 commits into
release/v3.8.49from
feat/7286-geminiweb-tool-calling
Jul 19, 2026
Merged

feat: emulate OpenAI tool calling for gemini-web via the webTools prompt shim (#7286)#7727
diegosouzapw merged 3 commits into
release/v3.8.49from
feat/7286-geminiweb-tool-calling

Conversation

@diegosouzapw

@diegosouzapw diegosouzapw commented Jul 19, 2026

Copy link
Copy Markdown
Owner

Summary

Ships Levels 1-2 only from #7286's staged approach:

  • Level 2 (gemini-web) — wires the existing webTools.ts prompt-emulation shim (already proven across 11 other web-cookie executors: adapta-web, blackbox-web, chatgpt-web, deepseek-web, duckduckgo-web, gitlab, inner-ai, muse-spark-web, perplexity-web, qwen-web, t3-chat-web) into open-sse/executors/gemini-web.ts:
    • Request side: prepareToolMessages() serializes the client's tools[] into a <tool> contract, prepended as a synthetic system message. gemini-web concatenates it with the last user message into the single prompt string typed into the Gemini web UI (buildGeminiToolPrompt).
    • Response side: once the buffered Gemini response text is captured, buildGeminiToolResponse() wraps it in the standard OpenAI completion shape and delegates to the shared buildToolModeResponse() (chatgptWebTools.ts), which parses <tool>{...}</tool> blocks into tool_calls. Malformed JSON degrades to ordinary chat content — never a thrown error or a 500, matching the other 11 executors' behavior.
    • Streaming: since gemini-web buffers the whole response before returning (no true token-by-token streaming), tool mode replays either buffered JSON or a single terminal SSE chunk (role chunk + delta.tool_calls/finish_reason: "tool_calls" chunk + [DONE]).
    • No-tools passthrough is byte-for-byte unchanged (regression guard) — the old messages.filter(m => m.role === "user").pop() prompt derivation and formatChatCompletion/formatStreamChunk call sites are untouched when tools is absent.
  • Level 1 (docs) — adds a "Tool calling" column (native/emulated/none) to docs/reference/PROVIDER_REFERENCE.md, populated only for entries with confirmed ground truth in this pass: the 11 already-wired web-cookie executors + gemini-webemulated, claude-webnone (pending its own Level 3 decision). The column is regenerated via npm run gen:provider-referencePROVIDER_REFERENCE.md is never hand-edited.

Explicitly out of scope (per the plan and issue's staged approach):

  • Level 3 (claude-web) — needs the owner's emulate-vs-native decision; claude-web.ts/claude-web/payload.ts are untouched in this diff.
  • Level 4 (supportsTools capability flag in the provider registry) — cross-cutting, tracked separately.

Validation (Hard Rule #18 — TDD)

New test file tests/unit/gemini-web-tool-calling-7286.test.ts (8 tests, all passing):

  • Well-formed tool call → tool_calls, content: null, finish_reason: "tool_calls" (non-streaming).
  • Malformed <tool> JSON degrades to ordinary chat content — never throws, never a 500.
  • Streaming assembly — role chunk, then terminal chunk with delta.tool_calls, then [DONE].
  • No <tool> block + no requested tools → content passthrough.
  • buildGeminiToolPrompt unit tests (with/without a synthetic system message).
  • Full executor integration (Playwright mocked, mirroring the existing gemini-web.test.ts pattern) proving the wiring actually reaches the Playwright-driven request/response code path: tools[] present → tool_calls; tools absent → the typed prompt is byte-identical to the old last-user-message-only derivation and the response shape is unchanged (regression guard).

Red→green proof: confirmed the pre-change open-sse/executors/gemini-web.ts (checked out from origin/release/v3.8.49 into a scratch file) exports neither buildGeminiToolResponse nor buildGeminiToolPrompt — the new test file's exercised surface does not exist on the base branch. After the change, all 8 new tests plus the 17 pre-existing gemini-web tests pass (25/25).

node --import tsx/esm --test tests/unit/gemini-web-tool-calling-7286.test.ts tests/unit/gemini-web.test.ts tests/unit/gemini-web-missing-browser-3516.test.ts
# ℹ tests 25 / pass 25 / fail 0

Gates run (all green)

  • node scripts/check/check-test-discovery.mjs
  • npm run typecheck:core / npm run typecheck:noimplicit:core (no new errors; pre-existing drift in unrelated files unchanged from base)
  • npx eslint --suppressions-location config/quality/eslint-suppressions.json <changed files> — 0 errors
  • npm run check:complexity-ratchets — 2059/890, unchanged from baseline
  • node scripts/check/check-file-size.mjs — clean, no
  • npm run check:cycles — no new cycles
  • npm run check:provider-consistency — OK
  • npm run check:docs-sync — PASS
  • Full npx tsc --noEmit -p tsconfig.json spot-checked: zero errors touch any file in this diff (confirmed the remaining pre-existing errors are on files untouched by this PR via git diff --stat against the base ref)

Not run: npm run test:coverage (heaviest gate, shared devbox) — this change is additive-only (new test file + new exported helpers), CI runs it authoritatively.

What was deferred

  • Level 3 (claude-web) — needs the owner's decision between (a) prompt-emulation via webTools.ts (additive, same pattern as this PR) or (b) merging client tools into the Claude.ai payload + parsing tool_use blocks (closer to native, needs Hard-Rule-fix(ci): add environment for npm token access #18 VPS validation against the live undocumented upstream contract). Tracked as a follow-up once that's settled.
  • Level 4 (supportsTools capability flag) — cross-cutting change touching the whole provider registry, tracked separately.

This PR intentionally does not close #7286 — the issue covers both Level 2 (gemini-web, shipped here) and Level 3 (claude-web, deferred). Refs #7286.

… shim (#7286)

Level 2 of the staged approach in #7286: wire the existing webTools.ts
prompt-emulation shim (already proven across 11 other web-cookie
executors) into gemini-web.ts. The client's tools[] array is now
serialized into the prompt typed into the Gemini web UI, and
<tool>{...}</tool> blocks in the response are parsed back into OpenAI
tool_calls -- including for streaming requests, replayed as a single
terminal SSE chunk since gemini-web buffers the whole response by
construction. Malformed tool JSON degrades to ordinary chat content,
never an error, matching the existing behavior of the other 11
executors. The no-tools code path is unchanged (regression guard).

Also Level 1: adds a "Tool calling" column (native/emulated/none) to
docs/reference/PROVIDER_REFERENCE.md for providers with confirmed
ground truth (the 11 already-wired web-cookie executors + gemini-web
-> emulated, claude-web -> none pending its own Level 3 decision).

Level 3 (claude-web) and Level 4 (supportsTools capability flag) are
explicitly out of scope -- claude-web/payload.ts is untouched.
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

Resolved the docs/reference/PROVIDER_REFERENCE.md conflict by regenerating it
from the merged registry (npm run gen:provider-reference) rather than hand-
resolving the generated file: 268 unique provider IDs.
@diegosouzapw
diegosouzapw merged commit f3277f2 into release/v3.8.49 Jul 19, 2026
11 of 12 checks passed
@diegosouzapw
diegosouzapw deleted the feat/7286-geminiweb-tool-calling branch July 19, 2026 21:00
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.

feat(providers): tool calling for claude-web / gemini-web — extend the webTools emulation shim (#2820 never covered these two)

1 participant