/** * Claude Code API constraints. * * Enforces Anthropic API requirements that real Claude Code handles: * 1. Sampling params under extended thinking: temperature=1 and top_p>=0.95 * (or unset) when thinking is enabled/adaptive * 2. Disable thinking when tool_choice forces a specific tool * 3. Enforce max 4 cache_control breakpoints * 4. Normalize cache_control TTL ordering */ /** * Anthropic's extended-thinking contract rejects non-default sampling params: * with thinking enabled/adaptive, `temperature` may only be 1 and `top_p` must * be >= 0.95 (or unset) — otherwise the Messages API returns HTTP 400 * ("`temperature` may only be set to 1 ..." / "`top_p` must be greater than or * equal to 0.95 ..."). Clients such as the VS Code Copilot "Ollama" provider * routinely send other values (e.g. temperature 0.7, top_p 0.9), and thinking * can be injected by per-model requestDefaults *after* the request is built, so * normalize here: pin temperature to 1 and drop top_p (Anthropic's "unset" * branch — which also preserves the "never send both temperature and top_p" * invariant). */ export function enforceThinkingTemperature(body: Record): void { const thinking = body.thinking as Record | undefined; if (thinking?.type === "enabled" || thinking?.type === "adaptive") { body.temperature = 1; if (body.top_p !== undefined) { delete body.top_p; } } } export function disableThinkingIfToolChoiceForced(body: Record): void { const toolChoice = body.tool_choice as Record | string | undefined; if (!toolChoice) return; const isForced = toolChoice === "any" || (typeof toolChoice === "object" && (toolChoice.type === "any" || toolChoice.type === "tool")); if (isForced && body.thinking) { delete body.thinking; delete body.context_management; } } const MAX_CACHE_CONTROL_BLOCKS = 4; export function enforceCacheControlLimit(body: Record): void { let count = 0; // Count in system blocks const system = body.system as Array> | undefined; if (Array.isArray(system)) { for (const block of system) { if (block.cache_control) count++; } } // Count in messages const messages = body.messages as Array> | undefined; if (Array.isArray(messages)) { for (const msg of messages) { const content = msg.content as Array> | undefined; if (!Array.isArray(content)) continue; for (const block of content) { if (block.cache_control) count++; } } } // Count in tools const tools = body.tools as Array> | undefined; if (Array.isArray(tools)) { for (const tool of tools) { if (tool.cache_control) count++; } } if (count <= MAX_CACHE_CONTROL_BLOCKS) return; // Strip excess cache_control blocks from the end (keep first 4) let remaining = MAX_CACHE_CONTROL_BLOCKS; if (Array.isArray(system)) { for (const block of system) { if (block.cache_control) { if (remaining > 0) { remaining--; } else { delete block.cache_control; } } } } if (Array.isArray(messages)) { for (const msg of messages) { const content = msg.content as Array> | undefined; if (!Array.isArray(content)) continue; for (const block of content) { if (block.cache_control) { if (remaining > 0) { remaining--; } else { delete block.cache_control; } } } } } if (Array.isArray(tools)) { for (const tool of tools) { if (tool.cache_control) { if (remaining > 0) { remaining--; } else { delete tool.cache_control; } } } } } export function ensureCacheControlOnLastUserMessage(body: Record): void { const messages = body.messages as Array> | undefined; if (!Array.isArray(messages) || messages.length === 0) return; // Find the last user message for (let i = messages.length - 1; i >= 0; i--) { if (String(messages[i].role) === "user") { const content = messages[i].content; if (Array.isArray(content) && content.length > 0) { const lastBlock = content[content.length - 1] as Record; if (!lastBlock.cache_control) { lastBlock.cache_control = { type: "ephemeral" }; } } break; } } }