Skip to content

Commit bf66fdb

Browse files
author
SqlRush
committed
Memoize ToolSearch token counts
1 parent f83de77 commit bf66fdb

5 files changed

Lines changed: 182 additions & 14 deletions

File tree

docs/cc-100-roadmap.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@ 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`、以及 `ENABLE_TOOL_SEARCH=auto` 下 deferred 工具描述体积低于阈值都会回落为标准 inline tool schema;显式 `ENABLE_TOOL_SEARCH=true` 仍可让支持 beta shape 的自定义网关 opt in。auto 模式已接入 Anthropic `/v1/messages/count_tokens` 精确计数路径,成功时减去官方固定 tool overhead 后按 token 阈值判断,失败时会用 Haiku message-create usage 作为官方第二层 fallback,再失败才按官方字符 fallback 计算阈值,覆盖 `auto:N` 百分比、`[1m]` context 和 ant-only context-window override;token-count VCR/cache memoization 仍未宣称完成
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`、以及 `ENABLE_TOOL_SEARCH=auto` 下 deferred 工具描述体积低于阈值都会回落为标准 inline tool schema;显式 `ENABLE_TOOL_SEARCH=true` 仍可让支持 beta shape 的自定义网关 opt in。auto 模式已接入 Anthropic `/v1/messages/count_tokens` 精确计数路径,成功时减去官方固定 tool overhead 后按 token 阈值判断,失败时会用 Haiku message-create usage 作为官方第二层 fallback,再失败才按官方字符 fallback 计算阈值,覆盖 `auto:N` 百分比、`[1m]` context 和 ant-only context-window override;deferred tool token count 现在按官方 deferred tool name 列表做 in-process memoization,成功值和不可用结果都会缓存。token-count VCR 以及更完整的 MCP 生命周期失效策略仍未宣称完成
203203

204204
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 工具恢复。
205205

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -294,7 +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`、以及 `ENABLE_TOOL_SEARCH=auto` 下 deferred 工具描述体积低于阈值都会回落为标准 inline tool schema;显式 `ENABLE_TOOL_SEARCH=true` 仍可让支持 beta shape 的自定义网关 opt in。auto 模式已接入 Anthropic `/v1/messages/count_tokens` 精确计数路径,成功时减去官方固定 tool overhead 后按 token 阈值判断,失败时会用 Haiku message-create usage 作为官方第二层 fallback,再失败才按官方字符 fallback 计算阈值,覆盖 `auto:N` 百分比、`[1m]` context 和 ant-only context-window override;token-count VCR/cache memoization 仍未宣称完成
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`、以及 `ENABLE_TOOL_SEARCH=auto` 下 deferred 工具描述体积低于阈值都会回落为标准 inline tool schema;显式 `ENABLE_TOOL_SEARCH=true` 仍可让支持 beta shape 的自定义网关 opt in。auto 模式已接入 Anthropic `/v1/messages/count_tokens` 精确计数路径,成功时减去官方固定 tool overhead 后按 token 阈值判断,失败时会用 Haiku message-create usage 作为官方第二层 fallback,再失败才按官方字符 fallback 计算阈值,覆盖 `auto:N` 百分比、`[1m]` context 和 ant-only context-window override;deferred tool token count 现在按官方 deferred tool name 列表做 in-process memoization,成功值和不可用结果都会缓存。token-count VCR 以及更完整的 MCP 生命周期失效策略仍未宣称完成
298298
- 本轮补充:当本次 request 未启用 `ToolSearch` 时,API message normalization 会从历史 user `tool_result.content` 中剥离 `tool_reference` blocks;纯 reference 结果会替换成官方占位文本 `[Tool references removed - tool search not enabled]`,但剥离发生在 discovered-tool 扫描之后,不影响后续启用 ToolSearch 时恢复 loaded 工具。
299299
- 本轮补充: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 仍未宣称完成。
300300
- 本轮补充: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
@@ -93,6 +93,7 @@ Anthropic API 和 conversation:
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.
9595
- 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`, unset ToolSearch behind non-first-party `ANTHROPIC_BASE_URL`, and `ENABLE_TOOL_SEARCH=auto` requests below the deferred-tool size threshold all fall back to standard inline tool schemas; explicit `ENABLE_TOOL_SEARCH=true` can still opt a custom gateway back in. Auto mode now uses the Anthropic `/v1/messages/count_tokens` endpoint when the client supports it, subtracts the official fixed tool overhead, falls back to Haiku message-create usage when count-tokens fails, and then falls back to the official character threshold when token counting is unavailable, including `auto:N` percentages, 1M context suffixes, and ant-only context-window overrides.
96+
- Deferred tool token counts in ToolSearch auto mode are now memoized in-process by the official deferred tool name list key, including both successful counts and unavailable/null-equivalent results.
9697
- 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.
9798
- 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`.
9899
- 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.
@@ -114,7 +115,7 @@ The following items remain incomplete and must not be treated as done:
114115
- Auto mode / YOLO classifier: transcript construction, two-stage classifier, XML/tool-use parsing, prompt dump, denial circuit breaker, model gating, and fallback behavior.
115116
- Interactive permission prompt flow: REPL dialogs, bridge/channel/swarm permission relays, user feedback images, prompt race handling, cancellation.
116117
- 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.
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, model/env/proxy ToolSearch gates, remaining `ENABLE_TOOL_SEARCH=auto` edge behavior such as official token-count VCR/cache memoization, remaining delta-attachment details, mid-call cancellation for concrete tools, background task behavior, telemetry, and concrete tool-specific semantics.
118+
- 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, model/env/proxy ToolSearch gates, remaining `ENABLE_TOOL_SEARCH=auto` edge behavior such as official token-count VCR and full MCP lifecycle invalidation details, remaining delta-attachment details, mid-call cancellation for concrete tools, background task behavior, telemetry, and concrete tool-specific semantics.
118119
- 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.
119120
- 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.
120121
- 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: 51 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"sort"
99
"strconv"
1010
"strings"
11+
"sync"
1112
"time"
1213

1314
"ccgo/internal/api/anthropic"
@@ -82,6 +83,13 @@ func (r Runner) buildRequest(ctx context.Context, history []contracts.Message, m
8283

8384
type deferredToolTokenCounter func([]contracts.ToolDefinition) (int, bool)
8485

86+
type deferredToolTokenCountCacheEntry struct {
87+
Tokens int
88+
OK bool
89+
}
90+
91+
var deferredToolTokenCountCache sync.Map
92+
8593
func filterToolSearchDefinitions(definitions []contracts.ToolDefinition, history []contracts.Message, model string, tokenCounter deferredToolTokenCounter) ([]contracts.ToolDefinition, []string, bool) {
8694
if len(definitions) == 0 {
8795
return definitions, nil, false
@@ -151,21 +159,53 @@ func (r Runner) deferredToolTokenCounter(ctx context.Context, modelName string)
151159
if len(tools) == 0 {
152160
return 0, true
153161
}
154-
if counter, ok := r.Client.(TokenCountingMessageClient); ok && counter != nil {
155-
response, err := counter.CountTokens(ctx, anthropic.CountTokensRequest{
156-
Model: modelName,
157-
Messages: []contracts.APIMessage{{Role: "user", Content: []contracts.ContentBlock{contracts.NewTextBlock("foo")}}},
158-
Tools: tools,
159-
})
160-
if err == nil && response != nil && response.InputTokens > 0 {
161-
return normalizeToolTokenCount(response.InputTokens), true
162+
cacheKey := deferredToolTokenCountCacheKey(definitions)
163+
if cacheKey != "" {
164+
if cached, ok := deferredToolTokenCountCache.Load(cacheKey); ok {
165+
entry := cached.(deferredToolTokenCountCacheEntry)
166+
return entry.Tokens, entry.OK
162167
}
163168
}
164-
if tokens, ok := r.countToolTokensViaHaikuFallback(ctx, tools); ok {
165-
return tokens, true
169+
tokens, ok := r.countDeferredToolTokens(ctx, modelName, tools)
170+
if cacheKey != "" {
171+
deferredToolTokenCountCache.Store(cacheKey, deferredToolTokenCountCacheEntry{Tokens: tokens, OK: ok})
172+
}
173+
return tokens, ok
174+
}
175+
}
176+
177+
func (r Runner) countDeferredToolTokens(ctx context.Context, modelName string, tools []anthropic.ToolDefinition) (int, bool) {
178+
if len(tools) == 0 {
179+
return 0, true
180+
}
181+
if counter, ok := r.Client.(TokenCountingMessageClient); ok && counter != nil {
182+
response, err := counter.CountTokens(ctx, anthropic.CountTokensRequest{
183+
Model: modelName,
184+
Messages: []contracts.APIMessage{{Role: "user", Content: []contracts.ContentBlock{contracts.NewTextBlock("foo")}}},
185+
Tools: tools,
186+
})
187+
if err == nil && response != nil && response.InputTokens > 0 {
188+
return normalizeToolTokenCount(response.InputTokens), true
166189
}
167-
return 0, false
168190
}
191+
if tokens, ok := r.countToolTokensViaHaikuFallback(ctx, tools); ok {
192+
return tokens, true
193+
}
194+
return 0, false
195+
}
196+
197+
func deferredToolTokenCountCacheKey(definitions []contracts.ToolDefinition) string {
198+
names := make([]string, 0, len(definitions))
199+
for _, definition := range definitions {
200+
if toolDefinitionDeferred(definition) {
201+
names = append(names, definition.Name)
202+
}
203+
}
204+
return strings.Join(names, ",")
205+
}
206+
207+
func resetDeferredToolTokenCountCache() {
208+
deferredToolTokenCountCache = sync.Map{}
169209
}
170210

171211
func (r Runner) countToolTokensViaHaikuFallback(ctx context.Context, tools []anthropic.ToolDefinition) (int, bool) {

0 commit comments

Comments
 (0)