Skip to content

fix(server-tools): leave a client-declared web_fetch to the client - #153

Merged
orangeboyChen merged 2 commits into
mainfrom
worktree-fix-codex-webfetch-passthrough
Sep 17, 2026
Merged

orangeboyChen merged 2 commits into
mainfrom
worktree-fix-codex-webfetch-passthrough

Conversation

@orangeboyChen

Copy link
Copy Markdown
Owner

Problem

The Responses API has no web_fetch server tool — only web_search. A client that declares one as a plain function owns it and resolves it itself.

But the fetch branch of replaceServerTools took the call over whenever a backend was configured, before asking whether the declaration was client-owned:

if (isWebFetchTool(tool)) {
  if (fetchEnabled && fetchProvider) { /* take over */ }
  if (!isServerDeclaredFetchTool(tool)) { return [tool]; }

So choosing codebuddy or codebuddy2api silently disabled a tool the client had declared and depended on — the client never saw the call come back. Only the passthrough default kept it working, which is why this went unnoticed.

Observed on the Responses route with CODEBUDDY_WEB_FETCH_BACKEND=codebuddy and a Codex-declared {type:"function", name:"web_fetch"}:

declaration backend proxy executed?
codex function passthrough no
codex function codebuddy yes ← bug
anthropic web_fetch_20250910 passthrough no
anthropic type codebuddy yes (correct)

Fix

Reorder so the client-owned check comes first, matching what the search branch already does. The backend setting chooses who runs the proxy's tool; it is not a licence to take the client's.

Verified end to end on the Responses route: the call is now handed back unresolved, with the proxy neither fetching the page nor re-asking upstream.

Tests

  • New (Responses route): a client-declared web_fetch is handed back even with an executable backend — asserts no fetch, no second upstream call, no open_page. Confirmed red before the fix (pageFetches 0 → 5).
  • New (chat route): the same at the call level — the unresolved tool_calls reach the client.
  • Two existing tests encoded the old behaviour and are updated:
    • takes over a client-declared web_fetch function when a backend is set asserted the takeover directly; it now asserts the loop declines to touch the request.
    • Two runOnce cases built the proxy's own definition by hand instead of going through the Responses translator, so it arrived unmarked and looked client-owned. They now use translateResponsesToolsToChat, which is what actually marks an Anthropic-typed declaration as server-side.

All 677 tests pass; typecheck, eslint, and prettier are clean. Changed-branch coverage is 100%.

Note

This also applies to /v1/chat/completions, which is fine — a client-owned tool is client-owned on either route.

The Responses API has no `web_fetch` server tool — only `web_search`. A
client that declares one as a plain function owns it and resolves it
itself. But the fetch branch of `replaceServerTools` took the call over
whenever a backend was configured, before it ever asked whether the
declaration was client-owned:

    if (isWebFetchTool(tool)) {
      if (fetchEnabled && fetchProvider) { ...take over... }
      if (!isServerDeclaredFetchTool(tool)) { return [tool]; }

So choosing `codebuddy` or `codebuddy2api` silently disabled a tool the
client had declared and depended on — the client never saw the call come
back. Only the `passthrough` default kept it working, which is why this
went unnoticed.

Reorder so the client-owned check comes first, matching what the search
branch already does. The backend setting chooses who runs the *proxy's*
tool; it is not a licence to take the client's.

Verified end to end on the Responses route: a Codex-declared `web_fetch`
is now handed back unresolved, with the proxy neither fetching the page
nor re-asking upstream.

Two existing tests encoded the old behaviour and are updated:

- `takes over a client-declared web_fetch function when a backend is set`
  asserted the takeover directly. It now asserts the loop declines to
  touch the request, which is the behaviour it was named for.
- Two `runOnce` cases built the proxy's own definition by hand instead of
  going through the Responses translator, so it arrived unmarked and
  looked client-owned. They now use `translateResponsesToolsToChat`, which
  is what actually marks an Anthropic-typed declaration as server-side.

@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: 7de71a3e22

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread lib/server/proxy/web-search-loop.ts
@codecov

codecov Bot commented Sep 17, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 95.42%. Comparing base (7feb4ed) to head (5649a44).
⚠️ Report is 2 commits behind head on main.
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@           Coverage Diff           @@
##             main     #153   +/-   ##
=======================================
  Coverage   95.41%   95.42%           
=======================================
  Files          36       36           
  Lines        6349     6355    +6     
  Branches     1813     1813           
=======================================
+ Hits         6058     6064    +6     
  Misses        291      291           
Flag Coverage Δ
unittests 95.42% <100.00%> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

The declaration-level guard kept a client's `web_fetch` out of the
proxy's hands, but the synchronous loop still classified calls by name
and backend alone:

    (Boolean(fetchProvider) && isWebFetchToolCall(toolCall))

So a request carrying both a server-declared `web_search` and a
client-owned `web_fetch` — search makes the loop run, which activates
the fetch backend — had the client's fetch executed instead of handed
back. Ownership is decided when the declarations are rewritten, and the
streaming paths already consulted it via `ownedNames`; only this path
did not.

Route it through `isLocalServerToolCall` like the streaming paths, and
thread `ownedNames` into the loop scope.

Verified by inspection at the decision point — `ownedNames` is
`["websearch"]`, so `web_search` is executed and `web_fetch` is
returned — and by a test that fails (two upstream rounds instead of
one) when the classification is reverted.
@orangeboyChen
orangeboyChen enabled auto-merge (squash) September 17, 2026 08:57
@orangeboyChen
orangeboyChen merged commit 4dfe6f1 into main Sep 17, 2026
7 checks passed
@orangeboyChen
orangeboyChen deleted the worktree-fix-codex-webfetch-passthrough branch September 17, 2026 09: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.

1 participant