/** * Tool deferral: stop sending tool definitions the model is not using. * * WHY THIS IS THE BIGGEST LEVER IN THE REQUEST. Measured live, with real Claude * Code routed through this proxy against the real API, a single one-word prompt * produced a 179,564 byte request: tool schema 85,546 bytes across 31 tools * (47.6%), injected context 83,397 (46.4%), system prompt 9,410 (5.2%). The * conversation itself was two messages. Every compression engine in this * package walks `request.messages`, so all of them together were working on the * smaller half of the payload while half of it sat untouched. * * Anthropic's Tool Search Tool exists for exactly this. Tools marked * `defer_loading: true` are not placed in context; the model receives the search * tool (~500 tokens) and pulls definitions in when it needs them. Anthropic * report an 85% reduction in tool-definition tokens and, on large tool * libraries, an accuracy INCREASE -- Opus 4.5 from 79.5% to 88.1%. * * WHAT THIS COSTS, stated up front. Discovery is a round trip: a model that * needs a deferred tool asks for it first. Turns are the most expensive thing * in this system -- one extra turn measured around +30% -- so deferral trades a * large per-request saving against a possible turn. That trade is measured, not * assumed, and this is a separate switch from content compression precisely so * the two effects can never be confused for each other. */ import type { ProviderRequest } from './frontier.js'; import type { EmbeddingCache } from './embedding.js'; /** The regex search tool Anthropic ships for this. */ export declare const TOOL_SEARCH_TYPE = "tool_search_tool_regex_20251119"; export declare const TOOL_SEARCH_NAME = "tool_search_tool_regex"; /** The beta that enables deferred loading. */ export declare const ADVANCED_TOOL_USE_BETA = "advanced-tool-use-2025-11-20"; /** * Below this a definition is not worth deferring. * * MEASURED, NOT ASSUMED, and it replaced a rule that had it backwards. The * first version protected "core" tools by origin -- anything not named * mcp__server__tool -- on the theory that MCP servers carry the bulk. A live * capture said otherwise: all 31 tools in a real Claude Code request were * built-ins, 85,514 characters of them, and that rule would have deferred * nothing at all. * * The same capture showed WHERE the weight is, and it is not the core loop: * PowerShell 9,244 characters, DesignSync 8,930, Monitor 7,492, Workflow * 5,355, SendMessage 4,804 -- the ten largest are about 64% of the schema and * are specialised tools most sessions never call. Read, Edit, Write, Bash, * Grep and Glob are all comfortably smaller. * * So size is the signal. A small definition is cheap to keep and keeping it * guarantees no discovery round trip for the tools an agent actually lives in; * a large one has to earn its place by looking relevant to the task. * * AND THEN MEASUREMENT OVERTURNED THE FLOOR ITSELF, which is why it is 0. * The argument above is per-TOOL and the cost it trades against is per-REQUEST. * At 1,500 characters, 88 of 115 definitions in a real capture were exempt -- * about 55KB held in the cached prefix, in the region that is 66.9% of a * request. Every individual exemption looked cheap and the sum was the largest * thing this proxy declined to touch. * * Live A/B, four arms, sixteen runs, each arm holding each position in the run * order exactly once so prefix-cache carryover cancels. Weighted input is the * provider's own usage at 1.25x for cache writes and 0.1x for reads; the * verdict is pytest re-run afterwards, never the agent's own report: * * control (recorded, uncompressed) 134,706 4/4 pass * floor 1500 (the old default) 100,818 4/4 pass -25% * floor 0 (this default) 49,053 4/4 pass -64% * HeadRoom's shipped proxy 48,626 4/4 pass -64% * * The discovery round trips the floor existed to prevent did not appear. The * floor-0 arm was the FASTEST of the four (32s mean against 36-38s) and emitted * the fewest output tokens (1,024 against control's 1,337) -- the opposite of * an agent hunting for tools it cannot see. * * WHAT THAT DOES NOT SHOW: sixteen runs of one small Python task. It says the * exemption costs more than it saves there, not that discovery can never bite. * A task needing several specialised tools at once is the shape that would, and * is not covered. `TOKEN_OPTIMIZER_PROXY_SMALL_TOOL_CHARS` restores any floor, * 1500 included. */ export declare const SMALL_TOOL_CHARS = 0; /** How many non-core tools to keep loaded when a task hints at what it needs. */ export declare const DEFAULT_KEEP_RELEVANT = 5; export interface DeferOptions { /** The task text, used to decide which non-core tools are worth loading. */ readonly query?: string; /** Non-core tools to keep loaded. */ readonly keepRelevant?: number; /** Definitions at or below this size are always kept. */ readonly smallToolChars?: number; /** Vectors for a semantic ranking, when one has been warmed. */ readonly embeddings?: EmbeddingCache; } export interface DeferralResult { readonly request: ProviderRequest; /** Characters of tool definition removed from context. */ readonly deferredChars: number; readonly deferredCount: number; } /** * Marks tool definitions for on-demand loading and adds the search tool. * * Returns the request UNCHANGED whenever deferral would be wrong rather than * merely unhelpful -- no tools, one tool (the search tool costs more than it * saves), the caller already deferring, or a forced tool_choice. Failing open * is the rule everywhere in this proxy and it matters more here than usual, * because a malformed tools array is a broken session rather than a slow one. */ export declare function deferTools(request: ProviderRequest, options?: DeferOptions): DeferralResult; /** * Adds the beta to an existing `anthropic-beta` header without losing what is * already there. * * APPENDED, NEVER REPLACED. The client may already be enabling betas it needs; * overwriting the header would silently switch those off, and the failure would * appear far from here as a feature quietly not working. */ export declare function withAdvancedToolUse(existing: string | string[] | undefined): string; //# sourceMappingURL=tools.d.ts.map