Repository Public Research preview
1import { CURRENT, DEPRECATIONS, resolveModel, suggestModels } from "./knowledge.mjs";23/**4 * Static linter for Messages API requests. Every "error" rule mirrors a 400 that the live API was5 * observed to return for that model (see knowledge.json → observed_errors); "warning" rules flag6 * requests that are accepted but likely wrong. Pure function: no network, no key.7 */8const BETA = {9 task_budget: "task-budgets-2026-03-13", compaction: "compact-2026-01-12", context_editing: "context-management-2025-06-27",10 fallbacks: "server-side-fallback-2026-07-01", fast: "fast-mode-2026-02-01", mcp: "mcp-client-2025-11-20", display_updates: "thinking-display-updates-2026-08-18",11};12export const KNOWN_BETAS = BETA;1314export function lintRequest(req, opts = {}) {15 const apiVersion = opts.apiVersion ?? "2026-10-21";16 const findings = [];17 const add = (severity, code, path, message, fix, observed) => findings.push({ severity, code, path, message, fix: fix ?? null, api_error: observed ?? null });18 if (!req || typeof req !== "object" || Array.isArray(req)) {19 add("error", "request-not-object", "", "The request must be a JSON object.");20 return finish(findings, null);21 }22 const m = resolveModel(req.model);23 const betas = new Set(Array.isArray(req.betas) ? req.betas : []);24 if (!req.model) add("error", "model-missing", "model", "`model` is required.", `Set "model": "${CURRENT.opus}".`);25 else if (!m) {26 const s = suggestModels(req.model);27 add("error", "model-unknown", "model", `"${req.model}" is not a model id this server has verified.${/-\d{8}$/.test(req.model) ? " Current-generation ids carry no date suffix." : ""}`, s.length ? `Did you mean ${s.map((x) => `"${x}"`).join(" or ")}? Call list_models for the live list.` : "Call list_models for the live list.");28 } else if (DEPRECATIONS[m.id]) add("warning", "model-deprecated", "model", `${m.id} is ${DEPRECATIONS[m.id]}.`, `Migrate to "${CURRENT[m.tier] ?? CURRENT.sonnet}".`);2930 // --- max_tokens31 if (req.max_tokens === undefined) add("error", "max-tokens-missing", "max_tokens", "`max_tokens` is required.", 'Add "max_tokens": 16000 (non-streaming) or up to the model cap when streaming.');32 else if (!Number.isInteger(req.max_tokens) || req.max_tokens < 0) add("error", "max-tokens-invalid", "max_tokens", "`max_tokens` must be a non-negative integer.");33 else if (m && req.max_tokens > m.max_output_tokens) add("error", "max-tokens-over-cap", "max_tokens", `max_tokens ${req.max_tokens} exceeds the ${m.max_output_tokens} output cap of ${m.id}.`, `Use at most ${m.max_output_tokens}.`, m.observed_errors.max_tokens_over_cap);34 else if (req.max_tokens > 21333 && req.stream !== true && !opts.streaming) add("warning", "max-tokens-needs-streaming", "max_tokens", "Large max_tokens on a non-streaming request risks HTTP timeouts; the SDKs refuse some of these outright.", "Stream the request (SDK: client.messages.stream(...).finalMessage()).");3536 // --- messages37 const msgs = req.messages;38 if (!Array.isArray(msgs) || msgs.length === 0) add("error", "messages-missing", "messages", "`messages` must be a non-empty array.");39 else {40 if (msgs[0]?.role !== "user") add("error", msgs[0]?.role === "system" ? "system-first-message" : "first-message-not-user", "messages[0].role", msgs[0]?.role === "system" ? "A system instruction cannot be the first entry of `messages`." : "The conversation must start with a user message.", msgs[0]?.role === "system" ? "Move it to the top-level `system` field." : null);41 msgs.forEach((x, i) => {42 if (!["user", "assistant", "system"].includes(x?.role)) add("error", "role-invalid", `messages[${i}].role`, `Unknown role "${x?.role}". Use "user" or "assistant" (tool results travel in a user message as tool_result blocks).`, x?.role === "tool" ? 'Send {"role":"user","content":[{"type":"tool_result","tool_use_id":…,"content":…}]}.' : null);43 if (x?.role === "system" && i > 0) {44 if (m && !m.mid_conversation_system) add("error", "mid-system-unsupported", `messages[${i}].role`, `${m.id} does not accept system messages inside \`messages\`.`, "Put the instruction in the top-level `system` field or in the next user message.", m.observed_errors.mid_system);45 else if (msgs[i - 1]?.role !== "user") add("error", "mid-system-placement", `messages[${i}]`, "A mid-conversation system message must come directly after a user message (the API rejects it after an assistant turn).", "Place it right after the user message it applies to, as the last entry or followed by the assistant turn.", "messages.N: role 'system' must follow a 'user' message or an 'assistant' message ending in a server tool result");46 }47 });48 const last = msgs[msgs.length - 1];49 if (last?.role === "assistant" && m && !m.assistant_prefill) add("error", "prefill-unsupported", `messages[${msgs.length - 1}]`, `${m.id} rejects assistant prefill: the conversation must end with a user message.`, "Remove the trailing assistant message; use output_config.format (structured outputs) or a system instruction to shape the reply.", m.observed_errors.prefill);50 const used = new Set(), results = [];51 msgs.forEach((x, i) => Array.isArray(x?.content) && x.content.forEach((b, j) => {52 if (b?.type === "tool_use") used.add(b.id);53 if (b?.type === "tool_result") results.push([b.tool_use_id, `messages[${i}].content[${j}]`, x.role]);54 }));55 for (const [id, path, role] of results) {56 if (role !== "user") add("error", "tool-result-wrong-role", path, "tool_result blocks must be sent in a user message.");57 if (!used.has(id)) add("error", "tool-result-orphan", path, `tool_result references tool_use_id "${id}", which no earlier assistant message contains.`);58 }59 }6061 // --- thinking & effort62 const t = req.thinking, effort = req.output_config?.effort;63 if (req.effort !== undefined) add("error", "effort-top-level", "effort", "`effort` is not a top-level parameter.", 'Move it to "output_config": {"effort": …}.');64 if (m && t && typeof t === "object") {65 if (t.type === "enabled" || t.budget_tokens !== undefined) {66 if (!m.thinking.budget_tokens) add("error", "thinking-budget-unsupported", "thinking", `${m.id} rejects thinking.type "enabled" / budget_tokens.`, `Use "thinking": {"type": "adaptive"} and control depth with output_config.effort.`, m.observed_errors.thinking_budget);67 else {68 if (m.thinking.adaptive) add("warning", "thinking-budget-deprecated", "thinking", `budget_tokens still works on ${m.id} but is deprecated there.`, 'Prefer {"type": "adaptive"} with output_config.effort.');69 if (typeof t.budget_tokens !== "number" || t.budget_tokens < 1024) add("error", "thinking-budget-min", "thinking.budget_tokens", "budget_tokens must be at least 1024.");70 else if (Number.isInteger(req.max_tokens) && t.budget_tokens >= req.max_tokens) add("error", "thinking-budget-vs-max", "thinking.budget_tokens", "budget_tokens must be smaller than max_tokens.", `Raise max_tokens above ${t.budget_tokens} or lower the budget.`);71 }72 } else if (t.type === "adaptive") {73 if (!m.thinking.adaptive) add("error", "thinking-adaptive-unsupported", "thinking", `${m.id} does not support adaptive thinking.`, 'Use {"type": "enabled", "budget_tokens": N} (N ≥ 1024, below max_tokens).', m.observed_errors.thinking_adaptive);74 if (t.display === "updates" && !betas.has(BETA.display_updates)) add("error", "beta-missing", "thinking.display", 'thinking.display "updates" needs a beta flag.', `Add "${BETA.display_updates}" to betas and call the beta endpoint.`);75 } else if (t.type === "disabled") {76 if (m.thinking.disabled === "rejected") add("error", "thinking-disable-unsupported", "thinking", `Thinking cannot be disabled on ${m.id}.`, 'Omit `thinking` and lower output_config.effort (e.g. "low") instead.', m.observed_errors.thinking_disabled);77 else if (m.thinking.disabled === "use_between_tools") add("error", "thinking-disable-use-between-tools", "thinking", `${m.id} rejects {"type": "disabled"}.`, 'Send "thinking": {"type": "between_tools"} (effort "high" or below, no other thinking field), or keep thinking on at low effort.', m.observed_errors.thinking_disabled);78 else if (effort && m.thinking.disabled_blocked_at_effort.includes(effort)) add("error", "thinking-disabled-high-effort", "output_config.effort", `${m.id} does not allow effort "${effort}" with thinking disabled.`, 'Use effort "high" or below, or enable thinking.', m.observed_errors.disabled_at_xhigh);79 } else if (t.type === "between_tools") {80 if (!m.thinking.between_tools) add("error", "thinking-between-tools-unsupported", "thinking", `${m.id} does not support thinking.type "between_tools".`, m.thinking.disabled === "accepted" ? 'Use {"type": "disabled"} on this model.' : "Lower output_config.effort instead.", m.observed_errors.thinking_between_tools);81 else {82 if (Object.keys(t).length > 1) add("error", "thinking-between-tools-extra", "thinking", '"between_tools" accepts no other field (display, budget_tokens, …).', 'Send exactly {"type": "between_tools"}.');83 if (effort === "xhigh" || effort === "max") add("error", "thinking-between-tools-effort", "output_config.effort", '"between_tools" is only accepted at effort "high" or below.');84 }85 } else add("error", "thinking-type-unknown", "thinking.type", `Unknown thinking.type "${t.type}".`);86 }87 if (m && !t && !m.thinking.adaptive && m.effort.supported === false && effort === undefined && opts.expectThinking) add("info", "thinking-off-by-default", "thinking", `${m.id} does not think unless thinking is enabled explicitly.`);88 if (m && effort !== undefined) {89 if (!m.effort.supported) add("error", "effort-unsupported", "output_config.effort", `${m.id} does not support the effort parameter.`, "Remove output_config.effort.", m.observed_errors.effort_low);90 else if (!m.effort.levels.includes(effort)) add("error", "effort-level-unsupported", "output_config.effort", `${m.id} does not support effort "${effort}". Supported: ${m.effort.levels.join(", ")}.`, `Use one of: ${m.effort.levels.join(", ")}.`, m.observed_errors[`effort_${effort}`]);91 }9293 // --- sampling94 for (const k of ["temperature", "top_p", "top_k"]) {95 if (req[k] === undefined || !m) continue;96 const ok = apiVersion >= "2026-10-21" ? m.sampling_params.api_2026_10_21 : m.sampling_params.api_2023_06_01;97 if (!ok) add("error", "sampling-param-rejected", k, apiVersion >= "2026-10-21" ? `\`${k}\` is rejected on every model under API version ${apiVersion}.` : `\`${k}\` is rejected by ${m.id}.`, `Remove \`${k}\`; steer determinism through the prompt, structured outputs, and effort.`, apiVersion >= "2026-10-21" ? m.observed_errors[k] : m.observed_errors[`legacy_${k}`] ?? null);98 }99100 // --- tools101 const tools = Array.isArray(req.tools) ? req.tools : [];102 const tc = req.tool_choice;103 if (tc && typeof tc === "object") {104 if ((tc.type === "any" || tc.type === "tool") && m && !m.forced_tool_choice) add("error", "tool-choice-forced-unsupported", "tool_choice", `${m.id} rejects tool_choice "${tc.type}".`, 'Use {"type": "auto"}, name the tool in the prompt, add strict: true to the tool; or use output_config.format when the forced call only existed to get JSON.', m.observed_errors.tool_any);105 if (tc.type === "tool" && !tools.some((x) => x?.name === tc.name)) add("error", "tool-choice-unknown-tool", "tool_choice.name", `tool_choice names "${tc.name}", which is not in tools.`);106 if (tc.type !== "none" && tools.length === 0 && tc.type) add("error", "tool-choice-without-tools", "tool_choice", "tool_choice is set but no tools are defined.");107 if (tc.strict !== undefined) add("error", "strict-on-tool-choice", "tool_choice.strict", "`strict` belongs on the tool definition, not on tool_choice.");108 }109 const names = new Set();110 let deferred = 0, hasSearchTool = false, hasMcpToolset = false;111 tools.forEach((x, i) => {112 const p = `tools[${i}]`;113 if (!x || typeof x !== "object") return add("error", "tool-invalid", p, "Each tool must be an object.");114 if (x.name) { if (names.has(x.name)) add("error", "tool-duplicate-name", `${p}.name`, `Duplicate tool name "${x.name}".`); names.add(x.name); }115 if (x.defer_loading) deferred++;116 const type = x.type;117 if (type?.startsWith("tool_search_tool")) { hasSearchTool = true; if (x.defer_loading) add("error", "tool-search-deferred", p, "The tool search tool itself must not be deferred."); }118 if (type === "mcp_toolset") hasMcpToolset = true;119 if (!type || type === "custom") {120 if (!x.name || !/^[a-zA-Z0-9_-]{1,128}$/.test(x.name)) add("error", "tool-name-invalid", `${p}.name`, "Tool names must match ^[a-zA-Z0-9_-]{1,128}$.");121 if (!x.input_schema || x.input_schema.type !== "object") add("error", "tool-schema-missing", `${p}.input_schema`, 'Custom tools need an input_schema of type "object".');122 else if (x.strict === true) {123 if (x.input_schema.additionalProperties !== false) add("error", "strict-needs-additional-properties", `${p}.input_schema`, "strict: true requires additionalProperties: false.", 'Add "additionalProperties": false.');124 const props = Object.keys(x.input_schema.properties ?? {}), reqd = x.input_schema.required ?? [];125 if (props.some((k) => !reqd.includes(k))) add("warning", "strict-optional-properties", `${p}.input_schema.required`, "With strict: true, list every property in `required` (model optional values as nullable types).");126 }127 if (!x.description || x.description.length < 20) add("warning", "tool-description-thin", `${p}.description`, "Tool descriptions drive tool selection; say what the tool does, when to use it and what it returns.");128 if (x.input_schema && /^(bash|str_replace_based_edit_tool|memory|computer)$/.test(x.name ?? "")) add("warning", "tool-shadows-anthropic-tool", `${p}.name`, `A custom tool named "${x.name}" is not the Anthropic-defined tool; that one is declared by type and has no input_schema.`);129 } else if (m) {130 if (type === "web_search_20260209" && !m.server_tools.web_search_20260209) add("error", "server-tool-version", `${p}.type`, `${m.id} does not support ${type}.`, 'Use "web_search_20250305".', m.observed_errors.web_search_2026);131 if (type === "web_fetch_20260209" && !m.server_tools.web_fetch_20260209) add("error", "server-tool-version", `${p}.type`, `${m.id} does not support ${type}.`, 'Use "web_fetch_20250910".', m.observed_errors.web_fetch_2026);132 if (type?.startsWith("code_execution") && !m.server_tools.code_execution_20260521 && type === "code_execution_20260521") add("warning", "server-tool-unverified", `${p}.type`, `${type} was not accepted by ${m.id} in probes.`);133 if (type === "computer_20251124" && ["claude-opus-5-5", "claude-sonnet-5-5", "claude-fable-5-2"].includes(m.id)) add("warning", "computer-tool-version", `${p}.type`, `Documentation states ${m.id} only accepts computer_toolset_20260801 on the Claude API (not probed by this server).`);134 }135 });136 if (deferred && !hasSearchTool) add("error", "defer-without-tool-search", "tools", "defer_loading is set but no tool search tool is declared.");137 if (deferred && deferred === tools.length) add("error", "all-tools-deferred", "tools", "At least one tool must be non-deferred.");138 if (tools.some((x) => x?.type === "web_search_20260209" || x?.type === "web_fetch_20260209") && tools.some((x) => x?.type?.startsWith("code_execution"))) add("warning", "redundant-code-execution", "tools", "The 20260209 web tools already run code for dynamic filtering; declaring code_execution too gives the model two execution environments.");139 if (Array.isArray(req.mcp_servers) && req.mcp_servers.length) {140 if (!hasMcpToolset) add("error", "mcp-toolset-missing", "tools", "mcp_servers needs a matching {type: \"mcp_toolset\", mcp_server_name} entry in tools.");141 if (!betas.has(BETA.mcp)) add("error", "beta-missing", "mcp_servers", "The MCP connector needs a beta flag.", `Add "${BETA.mcp}" to betas.`);142 }143144 // --- output145 if (req.output_format !== undefined) add("error", "output-format-deprecated", "output_format", "`output_format` is deprecated.", "Use output_config: {format: {type: \"json_schema\", schema: …}}.");146 const fmt = req.output_config?.format;147 if (fmt) {148 if (fmt.type !== "json_schema" || !fmt.schema) add("error", "output-format-invalid", "output_config.format", 'format must be {type: "json_schema", schema: {...}}.');149 else if (fmt.schema.type === "object" && fmt.schema.additionalProperties !== false) add("error", "output-schema-additional-properties", "output_config.format.schema", "Structured-output object schemas require additionalProperties: false.");150 if (Array.isArray(msgs) && msgs.some((x) => Array.isArray(x?.content) && x.content.some((b) => b?.citations?.enabled))) add("error", "citations-with-structured-output", "output_config.format", "Citations cannot be combined with structured outputs.");151 }152 const tb = req.output_config?.task_budget;153 if (tb) {154 if (m && !m.task_budget_beta) add("error", "task-budget-unsupported", "output_config.task_budget", `${m.id} does not support task budgets.`, null, m.observed_errors.task_budget);155 if (!betas.has(BETA.task_budget)) add("error", "beta-missing", "output_config.task_budget", "Task budgets need a beta flag.", `Add "${BETA.task_budget}" to betas and call the beta endpoint.`);156 if (tb.type !== "tokens" || !(tb.total >= 20000)) add("error", "task-budget-invalid", "output_config.task_budget", 'task_budget must be {type: "tokens", total ≥ 20000}.');157 }158159 // --- context management, speed, fallbacks, caching160 for (const e of req.context_management?.edits ?? []) {161 if (e?.type === "compact_20260112") {162 if (m && !m.compaction_beta) add("error", "compaction-unsupported", "context_management", `${m.id} does not support compaction.`, null, m.observed_errors.compaction);163 if (!betas.has(BETA.compaction)) add("error", "beta-missing", "context_management", "Compaction needs a beta flag.", `Add "${BETA.compaction}" to betas.`);164 } else if (e?.type?.startsWith("clear_") && !betas.has(BETA.context_editing)) add("error", "beta-missing", "context_management", "Context editing needs a beta flag.", `Add "${BETA.context_editing}" to betas.`);165 }166 if (req.speed === "fast" && m) {167 if (m.fast_mode === "unsupported") add("error", "fast-mode-unsupported", "speed", `${m.id} does not support fast mode.`, "Remove `speed` or use claude-opus-5-5 / claude-fable-5-2.", m.observed_errors.speed_fast);168 else if (m.fast_mode === "beta_header_required" && !betas.has(BETA.fast)) add("error", "beta-missing", "speed", `Fast mode on ${m.id} needs a beta flag.`, `Add "${BETA.fast}" to betas and call the beta endpoint.`, m.observed_errors.speed_fast);169 if (opts.batch) add("error", "fast-mode-in-batch", "speed", "Fast mode is not available in Message Batches.");170 }171 if (req.fallbacks !== undefined && !betas.has(BETA.fallbacks)) add("error", "beta-missing", "fallbacks", "Server-side fallbacks need a beta flag.", `Add "${BETA.fallbacks}" to betas.`);172 if (req.inference_geo !== undefined && m && !m.inference_geo) add("error", "inference-geo-unsupported", "inference_geo", `${m.id} does not support inference_geo.`, null, m.observed_errors.inference_geo_us);173 const breakpoints = JSON.stringify(req).split('"cache_control"').length - 1;174 if (breakpoints > 4) add("error", "cache-breakpoints", "", `${breakpoints} cache_control breakpoints; the maximum is 4.`);175 if (typeof req.system === "string" && /\b(20\d\d-\d\d-\d\dT\d\d:\d\d|\d{10,13})\b/.test(req.system) && breakpoints) add("warning", "cache-volatile-prefix", "system", "A timestamp in the system prompt changes the cached prefix on every request.", "Move volatile values after the last cache breakpoint.");176 if (m && ["claude-fable-5-2", "claude-fable-5-1", "claude-opus-5-5", "claude-opus-5", "claude-sonnet-5-5"].includes(m.id) && req.fallbacks === undefined) add("info", "refusal-fallback", "fallbacks", `${m.id} can end a turn with stop_reason "refusal". Check stop_reason before reading content; consider betas ["${BETA.fallbacks}"] with fallbacks: "default".`);177 return finish(findings, m);178}179180function finish(findings, m) {181 const errors = findings.filter((f) => f.severity === "error").length;182 return { valid: errors === 0, model: m?.id ?? null, errors, warnings: findings.filter((f) => f.severity === "warning").length, findings };183}184185/** Apply the mechanical fixes. Returns the corrected request and the list of changes; never guesses intent. */186export function fixRequest(req, opts = {}) {187 const r = structuredClone(req), changes = [];188 const m = resolveModel(r.model);189 const note = (s) => changes.push(s);190 if (!m) return { request: r, changes, remaining: lintRequest(r, opts) };191 if (r.effort !== undefined) { r.output_config = { ...(r.output_config ?? {}), effort: r.effort }; delete r.effort; note("moved top-level effort into output_config.effort"); }192 if (r.output_format !== undefined) { r.output_config = { ...(r.output_config ?? {}), format: r.output_format }; delete r.output_format; note("moved output_format into output_config.format"); }193 const t = r.thinking;194 if (t && (t.type === "enabled" || t.budget_tokens !== undefined) && !m.thinking.budget_tokens) { r.thinking = { type: "adaptive" }; note('replaced budget_tokens thinking with {"type": "adaptive"}'); }195 if (t?.type === "adaptive" && !m.thinking.adaptive) {196 const b = Math.max(1024, Math.min(8000, (r.max_tokens ?? 16000) - 1024));197 r.thinking = { type: "enabled", budget_tokens: b };198 note(`replaced adaptive thinking with enabled + budget_tokens ${b}`);199 if (Number.isInteger(r.max_tokens) && r.max_tokens <= b) { r.max_tokens = b + 1024; note(`raised max_tokens to ${r.max_tokens} (must exceed budget_tokens)`); }200 }201 if (t?.type === "disabled") {202 if (m.thinking.disabled === "rejected") { delete r.thinking; r.output_config = { ...(r.output_config ?? {}), effort: "low" }; note('removed thinking: disabled (not allowed) and set effort "low"'); }203 else if (m.thinking.disabled === "use_between_tools") { r.thinking = { type: "between_tools" }; if (["xhigh", "max"].includes(r.output_config?.effort)) r.output_config.effort = "high"; note('replaced thinking: disabled with {"type": "between_tools"}'); }204 else if (m.thinking.disabled_blocked_at_effort.includes(r.output_config?.effort)) { r.output_config.effort = "high"; note('lowered effort to "high" (required while thinking is disabled)'); }205 }206 if (t?.type === "between_tools" && !m.thinking.between_tools) { if (m.thinking.disabled === "accepted") r.thinking = { type: "disabled" }; else delete r.thinking; note("replaced unsupported between_tools thinking"); }207 const e = r.output_config?.effort;208 if (e !== undefined) {209 if (!m.effort.supported) { delete r.output_config.effort; note("removed unsupported effort"); }210 else if (!m.effort.levels.includes(e)) { const order = ["low", "medium", "high", "xhigh", "max"]; const lower = order.slice(0, order.indexOf(e)).reverse().find((x) => m.effort.levels.includes(x)) ?? m.effort.levels.at(-1); r.output_config.effort = lower; note(`changed effort "${e}" to "${lower}"`); }211 }212 if (r.output_config && Object.keys(r.output_config).length === 0) delete r.output_config;213 const v = opts.apiVersion ?? "2026-10-21";214 for (const k of ["temperature", "top_p", "top_k"]) if (r[k] !== undefined && !(v >= "2026-10-21" ? m.sampling_params.api_2026_10_21 : m.sampling_params.api_2023_06_01)) { delete r[k]; note(`removed ${k}`); }215 if (r.tool_choice && ["any", "tool"].includes(r.tool_choice.type) && !m.forced_tool_choice) { const name = r.tool_choice.name; r.tool_choice = { type: "auto" }; note(`replaced forced tool_choice with auto${name ? ` (instruct the model in the prompt to call "${name}")` : ""}`); }216 if (Array.isArray(r.messages) && r.messages.at(-1)?.role === "assistant" && !m.assistant_prefill) { r.messages.pop(); note("removed trailing assistant prefill"); }217 if (Number.isInteger(r.max_tokens) && r.max_tokens > m.max_output_tokens) { r.max_tokens = m.max_output_tokens; note(`capped max_tokens at ${m.max_output_tokens}`); }218 if (r.max_tokens === undefined) { r.max_tokens = 16000; note("added max_tokens 16000"); }219 for (const x of r.tools ?? []) {220 if (x?.type === "web_search_20260209" && !m.server_tools.web_search_20260209) { x.type = "web_search_20250305"; note("switched web search to the 20250305 version this model supports"); }221 if (x?.type === "web_fetch_20260209" && !m.server_tools.web_fetch_20260209) { x.type = "web_fetch_20250910"; note("switched web fetch to the 20250910 version this model supports"); }222 }223 for (const x of r.tools ?? []) if (x?.strict === true && x.input_schema?.type === "object" && x.input_schema.additionalProperties !== false) { x.input_schema.additionalProperties = false; note(`set additionalProperties: false on strict tool "${x.name}"`); }224 const need = new Set(r.betas ?? []);225 const before = need.size;226 if (r.output_config?.task_budget) need.add(BETA.task_budget);227 for (const ed of r.context_management?.edits ?? []) need.add(ed?.type === "compact_20260112" ? BETA.compaction : BETA.context_editing);228 if (r.fallbacks !== undefined) need.add(BETA.fallbacks);229 if (r.speed === "fast" && m.fast_mode === "beta_header_required") need.add(BETA.fast);230 if (r.mcp_servers?.length) need.add(BETA.mcp);231 if (r.thinking?.display === "updates") need.add(BETA.display_updates);232 if (need.size !== before) { r.betas = [...need]; note(`added beta flags: ${[...need].join(", ")}`); }233 return { request: r, changes, remaining: lintRequest(r, opts) };234}235