SPB Git

Repository Public Research preview

lint.mjs 25.1 KB 235 lines javascript

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
Files main
Menu

Theme