Skip to content

feat(websearch): forward web_search tool requests to a configurable target model#4094

Open
GreenTeodoro839 wants to merge 1 commit into
router-for-me:devfrom
GreenTeodoro839:feature/anthropic-web-search
Open

feat(websearch): forward web_search tool requests to a configurable target model#4094
GreenTeodoro839 wants to merge 1 commit into
router-for-me:devfrom
GreenTeodoro839:feature/anthropic-web-search

Conversation

@GreenTeodoro839

Copy link
Copy Markdown

Summary

Adds a global websearch-forward config ({enable, model}) that reroutes requests carrying an Anthropic web_search server tool (any tools[].type starting with web_search, e.g. web_search_20250305) to a configurable target model. Forwarding is transparent: the request body is forwarded unchanged to the existing pipeline (translation, auth selection, headers, upstream model remapping), and the response model field is rewritten back to the client's originally requested model so the client never sees the reroute.

No search shim is added — this is purely a routing override plus one response fix-up.

Motivation

Many upstreams don't support Anthropic's server-side web search. Today, when Claude Code sends a /v1/messages request with a web_search_* tool to such an upstream, the tool is silently dropped or the call misbehaves. This lets operators reroute those requests to a model whose upstream can handle them (e.g. a claude-api-key whose upstream speaks the Anthropic protocol and can execute the tool), without building a search shim — just a routing decision.

How it works

  1. Handler detects a web_search_* tool via gjson prefix match on tools[].type (future-proofs dated variants).
  2. The routing model is overridden to the configured target; the client's original model is preserved.
  3. The original model is carried via a new executor metadata key (ForcedResponseModelMetadataKey).
  4. The conductor applies that metadata at its existing aliasResult rewrite step, so both non-streaming (rewriteForceMappedResponse) and streaming (wrapStreamResult / StreamRewriter) responses report the client's model.

The existing force-mapping rewrite machinery is reused unchanged — no signature changes to it.

Configuration

websearch-forward:
  enable: true
  model: "deepseek"   # target model (alias), resolved by the existing pipeline

Global (SDKConfig), hot-reloadable. A request whose model already equals the configured target is not forwarded (avoids a self-loop). No target-capability validation is performed — forwarding to a target that cannot handle the request is existing pipeline behavior.

Changes

  • internal/config/sdk_config.goWebSearchForwardConfig + field
  • config.example.yaml — documented block
  • sdk/cliproxy/executor/types.goForcedResponseModelMetadataKey
  • sdk/api/handlers/web_search_forward.go (new) — hasWebSearchTool, webSearchForwardTarget
  • sdk/api/handlers/handlers.go — routing override + metadata in the 3 execute paths
  • sdk/cliproxy/auth/conductor.goapplyForcedResponseModel applied at the 5 aliasResult produce sites
  • 3 test files

No changes to internal/translator/ (the forwarded request's tool translation is existing behavior).

Testing

  • gofmt clean; go build ./cmd/server OK
  • go test ./... — all packages pass; new unit tests cover detection (dated variants, mixed tools, no-op cases), the self-forward guard, config round-trip, and the forced-response-model override (applied / no-op).

…odel

Add a global `websearch-forward` config ({enable, model}) that reroutes
requests carrying an Anthropic web_search server tool (tools[].type with
the "web_search" prefix) to a configured target model. The request body
is forwarded unchanged; the existing translation/auth/header pipeline
handles the target. The response model field is rewritten back to the
client's originally requested model so forwarding stays transparent.

- Detection is a gjson prefix match on tools[].type, future-proofing
  dated variants (web_search_20250305, web_search_20260209, ...).
- Routing override is applied in the three standard execute paths
  (non-stream / count / stream). The client model is carried via a new
  ForcedResponseModelMetadataKey and applied at the conductor's existing
  aliasResult rewrite step (all 5 sites, streaming + non-streaming),
  reusing rewriteForceMappedResponse / wrapStreamResult unchanged.
- Self-forward guard: skip forwarding when the target equals the
  client-requested model (avoids target -> target loops).
- No target-capability validation; success or failure on the chosen
  target is existing CLIProxyAPI behavior.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 3, 2026 02:49
@github-actions github-actions Bot changed the base branch from main to dev July 3, 2026 02:49
@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown

This pull request targeted main.

The base branch has been automatically changed to dev.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a feature to transparently forward requests containing Anthropic's web_search server tool to a different configured model, rewriting the response model field back to the client's original request. The feedback suggests enhancing the self-forwarding loop guard in webSearchForwardTarget to resolve auto-models and parse thinking suffixes (e.g., deepseek(thinking)) before comparison, along with adding corresponding imports and unit tests.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +58 to +60
if strings.EqualFold(target, strings.TrimSpace(requestedModel)) {
return ""
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

Comparing the raw target and requestedModel strings directly can lead to unintended forwarding loops or accidental stripping of model suffixes (such as (thinking)). For example, if the target model is deepseek and the client requests deepseek(thinking), they will not match under strings.EqualFold, causing the request to be forwarded to deepseek and stripping the thinking suffix from the upstream execution.

To prevent this, resolve auto-models and parse suffixes on both models before comparing their base names.

	requestedBase := strings.TrimSpace(thinking.ParseSuffix(util.ResolveAutoModel(requestedModel)).ModelName)
	targetBase := strings.TrimSpace(thinking.ParseSuffix(util.ResolveAutoModel(target)).ModelName)
	if strings.EqualFold(targetBase, requestedBase) {
		return ""
	}

Comment on lines +3 to +7
import (
"strings"

"github.com/tidwall/gjson"
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Import thinking and util packages to support resolving auto-models and parsing suffixes in webSearchForwardTarget.

Suggested change
import (
"strings"
"github.com/tidwall/gjson"
)
import (
"strings"
"github.com/router-for-me/CLIProxyAPI/v7/internal/thinking"
"github.com/router-for-me/CLIProxyAPI/v7/internal/util"
"github.com/tidwall/gjson"
)

Comment on lines +51 to +53
{"self-forward guard same model", true, "glm", "glm", wsBody, ""},
{"self-forward guard case-insensitive", true, "DeepSeek", "deepseek", wsBody, ""},
{"requested model trimmed before compare", true, "deepseek", " glm ", wsBody, "deepseek"},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Add a test case to verify that the self-forward guard correctly handles models with thinking suffixes (e.g., deepseek(thinking) vs deepseek).

		{"self-forward guard same model", true, "glm", "glm", wsBody, ""},
		{"self-forward guard case-insensitive", true, "DeepSeek", "deepseek", wsBody, ""},
		{"self-forward guard with thinking suffix", true, "deepseek", "deepseek(thinking)", wsBody, ""},
		{"requested model trimmed before compare", true, "deepseek", " glm ", wsBody, "deepseek"},

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds a configurable routing override that transparently forwards Anthropic web_search_* tool requests to a chosen target model, while rewriting the response model back to the client’s originally requested model so clients don’t observe the reroute.

Changes:

  • Introduces websearch-forward: { enable, model } in global SDKConfig, documents it in config.example.yaml, and adds config parsing tests.
  • Detects web_search_* tools in request JSON and overrides routing to the configured target model, attaching metadata to force response-model rewrite.
  • Extends the auth conductor’s existing force-mapped response rewrite machinery to optionally apply a forced response model via new executor metadata, with unit tests.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
sdk/cliproxy/executor/types.go Adds a metadata key for forcing response model rewriting.
sdk/cliproxy/auth/conductor.go Applies forced-response-model metadata to alias rewrite results across execution paths.
sdk/cliproxy/auth/conductor_forced_response_model_test.go Unit tests for forced response model application behavior.
sdk/api/handlers/web_search_forward.go Adds web_search tool detection and forwarding target selection.
sdk/api/handlers/web_search_forward_test.go Unit tests for tool detection and forwarding decision logic.
sdk/api/handlers/handlers.go Integrates forwarding decision into execute/count/stream paths and sets forced-response metadata when rerouting.
internal/config/web_search_forward_test.go Verifies YAML parsing defaults and configured values for websearch-forward.
internal/config/sdk_config.go Adds WebSearchForwardConfig to SDKConfig.
config.example.yaml Documents the new websearch-forward configuration block.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +26 to +29
toolType := strings.ToLower(strings.TrimSpace(tool.Get("type").String()))
if strings.HasPrefix(toolType, webSearchToolTypePrefix) {
return true
}
Comment on lines +2954 to +2958
if model = strings.TrimSpace(model); model == "" {
return
}
aliasResult.ForceMapping = true
aliasResult.OriginalAlias = model

@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: 5a1c82f2b1

ℹ️ 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".

}
for _, tool := range tools.Array() {
toolType := strings.ToLower(strings.TrimSpace(tool.Get("type").String()))
if strings.HasPrefix(toolType, webSearchToolTypePrefix) {

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 Exclude OpenAI web_search tools from forwarding

When websearch-forward is enabled, this prefix check also matches OpenAI/Codex built-in tools such as {"type":"web_search"} (the repo already preserves that shape in test/builtin_tools_translation_test.go lines 16 and 25), not just Anthropic's dated web_search_YYYYMMDD tools. Because the handler does not check the entry protocol before calling this function, OpenAI requests that intentionally use the existing built-in web search path will be rerouted to the configured target model and have their response model rewritten, which can send them to an incompatible provider.

Useful? React with 👍 / 👎.

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.

2 participants