Skip to content

Commit 1d7c21d

Browse files
author
SqlRush
committed
Gate ToolSearch by model and env
1 parent c1ebb09 commit 1d7c21d

6 files changed

Lines changed: 261 additions & 5 deletions

File tree

cmd/claude/main_test.go

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,8 @@ func TestRunPrintSendsPromptAndPrintsAssistantText(t *testing.T) {
9090
if !ok || len(messages) < 1 {
9191
t.Fatalf("messages = %#v", requestBody["messages"])
9292
}
93-
if !hasAvailableDeferredToolsMessage(messages) {
94-
t.Fatalf("missing deferred tools message: %#v", messages)
93+
if hasAvailableDeferredToolsMessage(messages) {
94+
t.Fatalf("unexpected deferred tools message for haiku: %#v", messages)
9595
}
9696
if got := messageTextAt(t, messages, 0); got != "say hello" {
9797
t.Fatalf("prompt = %q", got)
@@ -100,6 +100,9 @@ func TestRunPrintSendsPromptAndPrintsAssistantText(t *testing.T) {
100100
if !ok || len(tools) == 0 {
101101
t.Fatalf("missing builtin tools: %#v", requestBody["tools"])
102102
}
103+
if requestToolsContainName(tools, "ToolSearch") {
104+
t.Fatalf("haiku request should not include ToolSearch: %#v", tools)
105+
}
103106
}
104107

105108
func TestRunChromeNativeHostRespondsToMessages(t *testing.T) {
@@ -3965,6 +3968,19 @@ func isAvailableDeferredToolsMessage(message any) bool {
39653968
return strings.HasPrefix(text, "<available-deferred-tools>\n")
39663969
}
39673970

3971+
func requestToolsContainName(tools []any, name string) bool {
3972+
for _, item := range tools {
3973+
tool, ok := item.(map[string]any)
3974+
if !ok {
3975+
continue
3976+
}
3977+
if tool["name"] == name {
3978+
return true
3979+
}
3980+
}
3981+
return false
3982+
}
3983+
39683984
func containsAnyString(values []any, want string) bool {
39693985
for _, value := range values {
39703986
if text, ok := value.(string); ok && text == want {

docs/cc-100-roadmap.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,8 @@ M8/M6 补充:compact plan 现在会把 compact 前已发现的 `tool_reference
199199

200200
M8/M2 补充:当 `ToolSearch` 可用且存在 deferred 工具时,conversation request 现在会按官方 dynamic tool loading 过滤请求工具:未发现 deferred 工具不发送 schema,已发现 deferred 工具作为 loaded tool 发送,`ToolSearch` 保持可调用,并在 API messages 前置 `<available-deferred-tools>` 名称列表;没有 deferred 工具时会从请求中移除 `ToolSearch`
201201

202+
M8/M2 补充:conversation request 现在会在发送 beta `defer_loading` / `tool_reference` shape 前执行官方 ToolSearch enablement gate:Haiku 模型、`CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS`、falsy `ENABLE_TOOL_SEARCH``ENABLE_TOOL_SEARCH=auto:100`、以及未显式启用 ToolSearch 时的非一方 `ANTHROPIC_BASE_URL` 都会回落为标准 inline tool schema;显式 `ENABLE_TOOL_SEARCH=true` 仍可让支持 beta shape 的自定义网关 opt in。
203+
202204
M8/M2 补充:当本次 request 未启用 `ToolSearch` 时,conversation request 现在会从 API user `tool_result.content` 中剥离历史 `tool_reference` blocks;纯 reference 结果会替换为官方占位文本 `[Tool references removed - tool search not enabled]`,剥离发生在 discovered-tool 扫描之后,避免影响后续 loaded 工具恢复。
203205

204206
M8/M2 补充:Anthropic request tool 转换现在会保留 contract 的 `strict``eager_input_streaming``cache_control``should_defer`,将 deferred 工具序列化为 API `defer_loading`,并用 `always_load` 覆盖 deferred hint;API tool description 会按 description、prompt、searchHint 顺序 fallback,conversation runner 构造请求时会把 `Task` 等 deferred tool 的 strict/defer_loading 元数据带到最终请求。完整 deferred/lazy tool discovery 仍未完成。

docs/claude-code-go-rewrite-plan.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,7 @@ test/parity/ # golden tests against TS/official behavior
294294
- 本轮补充:conversation `BuildRequest` 会扫描历史 `tool_result.content` 中的 `tool_reference`,并把已发现工具在后续 API request 中作为 loaded tool 发送,不再携带 `defer_loading`;扫描兼容运行时 `ToolReference` 值和 transcript/JSON 解码后的 map 形态。完整官方 tool-reference expansion/filtering 仍未宣称完成。
295295
- 本轮补充:compact plan 会把 compact 前已发现的 `tool_reference` 名称快照进 `compactMetadata.preCompactDiscoveredTools`,session transcript alias/resume 转换会保留该 metadata,conversation `BuildRequest` 可在 tool-result 消息被 summary 替换后继续从 compact boundary 恢复已发现工具并取消 `defer_loading`。完整官方 compact/snipping 边界策略仍未宣称完成。
296296
- 本轮补充:当 request registry 同时存在 `ToolSearch` 和 deferred 工具时,conversation request 会按官方 dynamic tool loading 过滤工具列表:未发现 deferred 工具不再发送 schema,已发现 deferred 工具作为 loaded tool 发送,`ToolSearch` 保持可调用,并在首条 API user message 前注入 `<available-deferred-tools>` 名称列表;没有 deferred 工具时会从请求中移除 `ToolSearch`
297+
- 本轮补充:conversation request 现在会在发送 beta `defer_loading` / `tool_reference` shape 前执行官方 ToolSearch enablement gate:Haiku 模型、`CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS`、falsy `ENABLE_TOOL_SEARCH``ENABLE_TOOL_SEARCH=auto:100`、以及未显式启用 ToolSearch 时的非一方 `ANTHROPIC_BASE_URL` 都会回落为标准 inline tool schema;显式 `ENABLE_TOOL_SEARCH=true` 仍可让支持 beta shape 的自定义网关 opt in。
297298
- 本轮补充:当本次 request 未启用 `ToolSearch` 时,API message normalization 会从历史 user `tool_result.content` 中剥离 `tool_reference` blocks;纯 reference 结果会替换成官方占位文本 `[Tool references removed - tool search not enabled]`,但剥离发生在 discovered-tool 扫描之后,不影响后续启用 ToolSearch 时恢复 loaded 工具。
298299
- 本轮补充:Anthropic request tool 转换会保留 contract 的 `strict``eager_input_streaming``cache_control``should_defer`,把 deferred 工具序列化为 API `defer_loading`,并让 `always_load` 覆盖 deferred hint;API tool description 会按 description、prompt、searchHint 顺序 fallback,conversation runner 构造请求时会保留 `Task` 等 deferred tool 的 strict/defer_loading 元数据。完整 deferred/lazy tool discovery 仍未宣称完成。
299300
- 本轮补充:tool executor 会在未通过 `ToolSearch` 发现的 deferred 工具发生输入 schema 校验失败时追加 schema-not-sent 恢复提示,引导模型先调用 `ToolSearch``select:<tool>` 再重试;runner 会把当前 turn messages 传入工具 metadata,提示判断兼容 `tool_reference` 结果和 compact boundary 的已发现工具快照。

docs/first-second-parity-audit.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ Anthropic API 和 conversation:
9292
- Conversation request construction now scans previous `tool_result` blocks for `tool_reference` content from `ToolSearch`; referenced tools are copied into the next API request without `defer_loading`, including both in-memory `ToolReference` values and decoded transcript map forms.
9393
- Compact planning now snapshots discovered `tool_reference` names into `compactMetadata.preCompactDiscoveredTools`; resume/transcript conversion preserves that metadata and request construction reloads those tools after compacted `tool_result` messages have been summarized away.
9494
- When `ToolSearch` is available and deferred tools exist, request construction now follows dynamic tool loading: undiscovered deferred schemas are omitted from `tools`, discovered deferred tools are loaded, `ToolSearch` stays callable, and an `<available-deferred-tools>` user meta message advertises the deferred tool names; if no deferred tools exist, `ToolSearch` is omitted from the request.
95+
- Request construction now also applies the official ToolSearch enablement gates before emitting beta `defer_loading` / `tool_reference` shapes: Haiku models, `CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS`, falsy `ENABLE_TOOL_SEARCH`, `ENABLE_TOOL_SEARCH=auto:100`, and unset ToolSearch behind non-first-party `ANTHROPIC_BASE_URL` all fall back to standard inline tool schemas; explicit `ENABLE_TOOL_SEARCH=true` can still opt a custom gateway back in.
9596
- When ToolSearch is inactive for a request, API message normalization strips historical `tool_reference` blocks from user `tool_result` content and replaces pure-reference results with the official placeholder text, while preserving history scanning before stripping so discovered tools can still be loaded when ToolSearch is active.
9697
- Anthropic request tool conversion now preserves contract-level `strict`, `eager_input_streaming`, `cache_control`, and API-level `defer_loading` hints from deferred tool definitions, with `always_load` overriding `should_defer`; it also falls back from `description` to `prompt` to `searchHint` when building API tool descriptions. Runner request construction preserves these fields for tools such as `Task`.
9798
- Tool validation now appends an official-style schema-not-sent recovery hint when an undiscovered deferred tool is invoked with invalid typed input and `ToolSearch` is available, including compact-boundary discovered-tool metadata so already loaded tools do not get a false hint.
@@ -113,7 +114,7 @@ The following items remain incomplete and must not be treated as done:
113114
- Auto mode / YOLO classifier: transcript construction, two-stage classifier, XML/tool-use parsing, prompt dump, denial circuit breaker, model gating, and fallback behavior.
114115
- Interactive permission prompt flow: REPL dialogs, bridge/channel/swarm permission relays, user feedback images, prompt race handling, cancellation.
115116
- Full filesystem permission parity gaps that remain: skill-scope allow suggestions, remaining local/local-jsx slash-command execution wiring beyond the currently covered no-query builtins, command permission UI/SDK surfacing, forked/remote/MCP/bundled SkillTool behavior and remaining plugin SkillTool UI/SDK polish beyond current metadata preservation, skill prompt shell injection, complete auto-memory override policy, and deeper platform-specific Windows/WSL bypass handling.
116-
- Full tool execution parity gaps that remain: complete hook runtime policy beyond the current settings command hook path, MCP elicitation, complete SDK control event surface beyond current progress NDJSON, full deferred/lazy tool discovery beyond current local registry BM25 search and API `defer_loading` hints, mid-call cancellation for concrete tools, background task behavior, telemetry, and concrete tool-specific semantics.
117+
- Full tool execution parity gaps that remain: complete hook runtime policy beyond the current settings command hook path, MCP elicitation, complete SDK control event surface beyond current progress NDJSON, full deferred/lazy tool discovery beyond current local registry BM25 search, API `defer_loading` hints, and model/env/proxy ToolSearch gates, including the official `ENABLE_TOOL_SEARCH=auto` token-threshold decision and remaining delta-attachment details, mid-call cancellation for concrete tools, background task behavior, telemetry, and concrete tool-specific semantics.
117118
- Complete Anthropic API parity gaps that remain: dynamic beta-header latching for any remaining provider-specific feature gates beyond prompt-cache/cache-editing/strict-output/1M-context requests, ant-only dump gating and remote `/issue` submission integration beyond the current local context summary, full official cost tracker/session restore lifecycle beyond current transcript usage restore, post-handshake streaming failure semantics, deeper gateway/proxy auth and routing behavior beyond static custom headers, first-party/Bedrock/Vertex/Foundry client setup, deeper OAuth account/profile lifecycle beyond request-time refresh retry, fast-mode retry/cooldown semantics, persistent unattended retry heartbeats, full prompt-cache editing lifecycle, and provider-specific cache behavior.
118119
- Full conversation/query loop: stop hooks, compact/auto-compact, token budget escalation, resume, SDK JSON/NDJSON control events, status updates, rate-limit handling, model switch breadcrumbs, side questions.
119120
- Full settings parity gaps that remain: complete Zod-equivalent validation messages, full remote managed-settings watcher/non-daemon background refresh lifecycle beyond current turn-start and daemon-heartbeat refresh, remaining forked agent/frontmatter execution and UI edge cases beyond current policy metadata filtering, complete marketplace TUI/UI/background lifecycle beyond current local/settings/directory/file/URL catalog/git/github/npm cache manifest load, URL catalog cache fallback, git/github fetch/pull cache refresh, npm pack cache refresh, and background watcher/continuous app-state sync beyond current turn-start local settings reload.

internal/conversation/request.go

Lines changed: 120 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
package conversation
22

33
import (
4+
"net/url"
5+
"os"
46
"sort"
7+
"strconv"
58
"strings"
69
"time"
710

@@ -44,7 +47,7 @@ func (r Runner) buildRequest(history []contracts.Message, model string, relevant
4447
if err != nil {
4548
return anthropic.Request{}, err
4649
}
47-
definitions, deferredToolNames, toolSearchActive = filterToolSearchDefinitions(defs, history)
50+
definitions, deferredToolNames, toolSearchActive = filterToolSearchDefinitions(defs, history, model)
4851
}
4952
apiMessages := msgs.NormalizeForAPI(history)
5053
if !toolSearchActive {
@@ -67,13 +70,16 @@ func (r Runner) buildRequest(history []contracts.Message, model string, relevant
6770
return request, nil
6871
}
6972

70-
func filterToolSearchDefinitions(definitions []contracts.ToolDefinition, history []contracts.Message) ([]contracts.ToolDefinition, []string, bool) {
73+
func filterToolSearchDefinitions(definitions []contracts.ToolDefinition, history []contracts.Message, model string) ([]contracts.ToolDefinition, []string, bool) {
7174
if len(definitions) == 0 {
7275
return definitions, nil, false
7376
}
7477
if !hasToolSearchDefinition(definitions) {
7578
return applyDiscoveredToolReferences(definitions, history), nil, false
7679
}
80+
if !toolSearchEnabledForRequest(model) {
81+
return loadAllDeferredTools(withoutToolSearchDefinition(definitions)), nil, false
82+
}
7783
deferredNames := deferredToolNames(definitions)
7884
if len(deferredNames) == 0 {
7985
return withoutToolSearchDefinition(definitions), nil, false
@@ -94,6 +100,118 @@ func filterToolSearchDefinitions(definitions []contracts.ToolDefinition, history
94100
return out, deferredNames, true
95101
}
96102

103+
type toolSearchMode string
104+
105+
const (
106+
toolSearchModeTST toolSearchMode = "tst"
107+
toolSearchModeTSTAuto toolSearchMode = "tst-auto"
108+
toolSearchModeStandard toolSearchMode = "standard"
109+
)
110+
111+
func toolSearchEnabledForRequest(model string) bool {
112+
if !modelSupportsToolReference(model) {
113+
return false
114+
}
115+
mode := toolSearchModeFromEnv()
116+
if mode == toolSearchModeStandard {
117+
return false
118+
}
119+
if os.Getenv("ENABLE_TOOL_SEARCH") == "" && !isFirstPartyAnthropicBaseURL(os.Getenv("ANTHROPIC_BASE_URL")) {
120+
return false
121+
}
122+
return true
123+
}
124+
125+
func modelSupportsToolReference(model string) bool {
126+
return !strings.Contains(strings.ToLower(strings.TrimSpace(model)), "haiku")
127+
}
128+
129+
func toolSearchModeFromEnv() toolSearchMode {
130+
if session.IsEnvTruthy(os.Getenv("CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS")) {
131+
return toolSearchModeStandard
132+
}
133+
value := os.Getenv("ENABLE_TOOL_SEARCH")
134+
if percent, ok := autoToolSearchPercent(value); ok {
135+
if percent == 0 {
136+
return toolSearchModeTST
137+
}
138+
if percent == 100 {
139+
return toolSearchModeStandard
140+
}
141+
}
142+
if isAutoToolSearchMode(value) {
143+
return toolSearchModeTSTAuto
144+
}
145+
if session.IsEnvTruthy(value) {
146+
return toolSearchModeTST
147+
}
148+
if isEnvDefinedFalsy(value) {
149+
return toolSearchModeStandard
150+
}
151+
return toolSearchModeTST
152+
}
153+
154+
func autoToolSearchPercent(value string) (int, bool) {
155+
if !strings.HasPrefix(value, "auto:") {
156+
return 0, false
157+
}
158+
percent, err := strconv.Atoi(value[len("auto:"):])
159+
if err != nil {
160+
return 0, false
161+
}
162+
if percent < 0 {
163+
return 0, true
164+
}
165+
if percent > 100 {
166+
return 100, true
167+
}
168+
return percent, true
169+
}
170+
171+
func isAutoToolSearchMode(value string) bool {
172+
return value == "auto" || strings.HasPrefix(value, "auto:")
173+
}
174+
175+
func isEnvDefinedFalsy(value string) bool {
176+
switch strings.ToLower(strings.TrimSpace(value)) {
177+
case "0", "false", "no", "off":
178+
return true
179+
default:
180+
return false
181+
}
182+
}
183+
184+
func isFirstPartyAnthropicBaseURL(raw string) bool {
185+
raw = strings.TrimSpace(raw)
186+
if raw == "" {
187+
return true
188+
}
189+
parsed, err := url.Parse(raw)
190+
if err != nil {
191+
return false
192+
}
193+
host := parsed.Host
194+
if host == "" {
195+
return false
196+
}
197+
if host == "api.anthropic.com" {
198+
return true
199+
}
200+
return os.Getenv("USER_TYPE") == "ant" && host == "api-staging.anthropic.com"
201+
}
202+
203+
func loadAllDeferredTools(definitions []contracts.ToolDefinition) []contracts.ToolDefinition {
204+
out := make([]contracts.ToolDefinition, len(definitions))
205+
copy(out, definitions)
206+
for i := range out {
207+
if toolDefinitionDeferred(out[i]) {
208+
out[i].AlwaysLoad = true
209+
out[i].ShouldDefer = false
210+
}
211+
}
212+
return out
213+
}
214+
97215
func applyDiscoveredToolReferences(definitions []contracts.ToolDefinition, history []contracts.Message) []contracts.ToolDefinition {
98216
discovered := discoveredToolReferenceNames(history)
99217
if len(discovered) == 0 || len(definitions) == 0 {

0 commit comments

Comments
 (0)