Repository Public Research preview
1import { RECOMMENDED, resolveModel, suggestModels } from "./knowledge.mjs";23/**4 * Static linter for OpenAI requests (Responses API and Chat Completions). Every "error" rule5 * mirrors a rejection observed from the live API for that model (knowledge.json → observed_errors).6 * Pure function: no network, no key.7 */8export function detectApi(req) {9 if (req && typeof req === "object") {10 if ("messages" in req) return "chat";11 if ("input" in req || "instructions" in req || "previous_response_id" in req || "max_output_tokens" in req) return "responses";12 }13 return "responses";14}1516export function lintRequest(req, opts = {}) {17 const findings = [];18 const add = (severity, code, path, message, fix, observed) => findings.push({ severity, code, path, message, fix: fix ?? null, api_error: observed ?? null });19 if (!req || typeof req !== "object" || Array.isArray(req)) { add("error", "request-not-object", "", "The request must be a JSON object."); return finish(findings, null, null); }20 const api = opts.api ?? detectApi(req);21 const m = resolveModel(req.model);22 if (!req.model) add("error", "model-missing", "model", "`model` is required.", `Set "model": "${RECOMMENDED.balanced}".`);23 else if (!m) { const s = suggestModels(req.model); add("error", "model-unknown", "model", `"${req.model}" is not a text model this server has probed.`, 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."); }24 const E = m?.observed_errors ?? {};2526 if (api === "responses") {27 const r = m?.responses;28 if (req.messages !== undefined) add("error", "responses-uses-messages", "messages", "The Responses API takes `input`, not `messages`.", "Rename `messages` to `input` (or call Chat Completions).");29 if (req.input === undefined && !req.previous_response_id && !req.prompt && !req.conversation) add("error", "input-missing", "input", "`input` is required (a string or an array of input items).");30 if (req.max_tokens !== undefined || req.max_completion_tokens !== undefined) add("error", "responses-max-tokens-name", req.max_tokens !== undefined ? "max_tokens" : "max_completion_tokens", "The Responses API names this parameter `max_output_tokens`.", "Rename it to `max_output_tokens`.");31 if (req.max_output_tokens !== undefined) {32 if (!Number.isInteger(req.max_output_tokens) || req.max_output_tokens < 16) add("error", "max-output-too-small", "max_output_tokens", "max_output_tokens must be an integer of at least 16.", "Use 16 or more.", E.max_output_8);33 else if (m?.max_output_tokens && req.max_output_tokens > m.max_output_tokens) add("error", "max-output-over-cap", "max_output_tokens", `max_output_tokens ${req.max_output_tokens} exceeds the documented ${m.max_output_tokens} cap of ${m.id}.`, `Use at most ${m.max_output_tokens}.`);34 else if (m?.reasoning_model && req.max_output_tokens < 1024 && (req.reasoning?.effort ?? (r.default_effort_is_none ? "none" : "default")) !== "none") add("warning", "max-output-small-for-reasoning", "max_output_tokens", "Reasoning tokens count against max_output_tokens: a small cap can end the response as `incomplete` with no visible text.", "Leave room for reasoning (several thousand tokens) or use reasoning.effort \"none\"/\"low\".");35 }36 if (req.response_format !== undefined) add("error", "responses-response-format", "response_format", "The Responses API takes structured output under `text.format`, not `response_format`.", 'Use text: {format: {type: "json_schema", name, schema, strict: true}}.');37 if (req.reasoning_effort !== undefined) add("error", "responses-reasoning-effort-name", "reasoning_effort", "The Responses API takes `reasoning: {effort}`, not `reasoning_effort`.", "Use reasoning: {effort: …}.");38 if (req.system !== undefined) add("error", "responses-system-field", "system", "There is no `system` field.", "Use `instructions`, or a developer message in `input`.");39 const effort = req.reasoning?.effort;40 if (m && effort !== undefined) {41 if (r.effort_levels.length === 0) add("error", "effort-unsupported", "reasoning.effort", `${m.id} is not a reasoning model: reasoning.effort is rejected.`, "Remove `reasoning`.", E.effort_low);42 else if (!r.effort_levels.includes(effort)) add("error", "effort-level-unsupported", "reasoning.effort", `${m.id} does not support reasoning effort "${effort}". Supported: ${r.effort_levels.join(", ")}.`, `Use one of: ${r.effort_levels.join(", ")}.`, E[`effort_${effort}`]);43 }44 if (m && req.reasoning?.summary !== undefined && r.effort_levels.length === 0) add("warning", "reasoning-summary-ignored", "reasoning.summary", `${m.id} is not a reasoning model: the API accepts reasoning.summary but there is nothing to summarise.`, "Remove `reasoning`.");45 const effective = effort ?? (r?.default_effort_is_none ? "none" : "default");46 for (const k of ["temperature", "top_p"]) {47 if (req[k] === undefined || !m) continue;48 if (r.sampling_params === "never") add("error", "sampling-param-rejected", k, `${m.id} rejects \`${k}\`.`, `Remove \`${k}\`; steer with instructions, text.verbosity and reasoning.effort.`, E[k] ?? E.temperature_effort_low);49 else if (r.sampling_params === "effort_none_only" && effective !== "none") add("error", "sampling-needs-effort-none", k, `${m.id} accepts \`${k}\` only while reasoning effort is "none"${r.default_effort_is_none ? "" : " (its default effort is not none)"}.`, `Remove \`${k}\`, or set reasoning: {effort: "none"}.`, E[k] ?? E.temperature_effort_low);50 }51 const verbosity = req.text?.verbosity;52 if (m && verbosity !== undefined && !r.verbosity_levels.includes(verbosity)) add("error", "verbosity-unsupported", "text.verbosity", `${m.id} does not support text.verbosity "${verbosity}". Supported: ${r.verbosity_levels.join(", ")}.`, "Remove text.verbosity.", E[`verbosity_${verbosity}`]);53 const fmt = req.text?.format;54 if (fmt?.type === "json_schema") {55 if (!fmt.name) add("error", "format-name-missing", "text.format.name", "A json_schema format needs a `name`.");56 if (!fmt.schema) add("error", "format-schema-missing", "text.format.schema", "A json_schema format needs a `schema`.");57 else if (fmt.strict !== false) checkStrictSchema(fmt.schema, "text.format.schema", add);58 if (fmt.json_schema) add("error", "format-chat-shape", "text.format.json_schema", "In the Responses API name/schema/strict sit directly under text.format (the nested json_schema wrapper is the Chat Completions shape).", "Flatten it.");59 }60 if (fmt?.type === "json_object" && !/json/i.test(JSON.stringify(req.input ?? "") + (req.instructions ?? ""))) add("error", "json-object-needs-json-word", "text.format", 'With json_object the input must mention "JSON".', "Prefer json_schema, or mention JSON in the instructions.");61 lintTools(req, m, "responses", add, E);62 if (m && req.top_logprobs !== undefined && !r.logprobs) add("error", "logprobs-unsupported", "top_logprobs", `${m.id} does not return logprobs.`, "Remove top_logprobs.", E.top_logprobs);63 if (m && Array.isArray(req.include) && req.include.includes("message.output_text.logprobs") && !r.logprobs) add("error", "logprobs-unsupported", "include", `${m.id} does not return logprobs.`, "Remove the logprobs include.", E.top_logprobs);64 if (m && req.service_tier === "flex" && !r.flex_tier) add("error", "flex-unsupported", "service_tier", `Flex processing is not available for ${m.id}.`, "Remove service_tier or use a model with flex pricing.", E.service_tier_flex);65 if (Array.isArray(req.include) && req.include.includes("reasoning.encrypted_content") && req.store !== false) add("warning", "encrypted-reasoning-needs-store-false", "include", "reasoning.encrypted_content is meant for stateless use: set store: false and pass the items back yourself.");66 if (req.background === true && req.store === false) add("error", "background-needs-store", "store", "Background responses must be stored.", "Remove store: false.");67 if (req.previous_response_id && req.conversation) add("error", "previous-response-and-conversation", "conversation", "Use previous_response_id or conversation, not both.");68 if (Array.isArray(req.input)) req.input.forEach((it, i) => {69 if (it?.role && !["user", "assistant", "system", "developer"].includes(it.role)) add("error", "role-invalid", `input[${i}].role`, `Unknown role "${it.role}".`, it.role === "tool" ? "Return tool results as {type: \"function_call_output\", call_id, output} items." : null);70 if (Array.isArray(it?.content)) it.content.forEach((c, j) => {71 if (c?.type === "text" || c?.type === "image_url") add("error", "content-part-chat-shape", `input[${i}].content[${j}].type`, `"${c.type}" is the Chat Completions content type.`, c.type === "text" ? 'Use "input_text" (or "output_text" for assistant turns).' : 'Use {type: "input_image", image_url: "<url or data URL>"}.');72 });73 if (it?.type === "function_call_output" && !it.call_id) add("error", "tool-output-call-id", `input[${i}]`, "function_call_output needs the call_id of the function_call it answers.");74 });75 const outs = Array.isArray(req.input) ? req.input.filter((x) => x?.type === "function_call_output").map((x) => x.call_id) : [];76 const calls = new Set(Array.isArray(req.input) ? req.input.filter((x) => x?.type === "function_call").map((x) => x.call_id) : []);77 if (!req.previous_response_id && !req.conversation) for (const id of outs) if (id && !calls.has(id)) add("error", "tool-output-orphan", "input", `function_call_output "${id}" has no matching function_call in input (and no previous_response_id).`, "Include the model's function_call item, or pass previous_response_id.");78 } else {79 const c = m?.chat_completions;80 if (m && !c.supported) { add("error", "chat-endpoint-unsupported", "model", `${m.id} is not available on Chat Completions.`, "Call the Responses API (client.responses.create).", E.chat_basic); return finish(findings, m, api); }81 if (req.input !== undefined) add("error", "chat-uses-input", "input", "Chat Completions takes `messages`, not `input`.");82 if (!Array.isArray(req.messages) || req.messages.length === 0) add("error", "messages-missing", "messages", "`messages` must be a non-empty array.");83 if (req.max_output_tokens !== undefined) add("error", "chat-max-output-name", "max_output_tokens", "Chat Completions names this parameter `max_completion_tokens`.", "Rename it.");84 if (m && req.max_tokens !== undefined && !c.max_tokens_legacy) add("error", "max-tokens-legacy", "max_tokens", `${m.id} rejects the legacy \`max_tokens\`.`, "Use `max_completion_tokens`.", E.chat_max_tokens_legacy);85 const effort = req.reasoning_effort;86 if (req.reasoning !== undefined) add("error", "chat-reasoning-object", "reasoning", "Chat Completions takes `reasoning_effort` (a string), not a `reasoning` object.");87 if (m && effort !== undefined) {88 if (m.responses.effort_levels.length === 0) add("error", "effort-unsupported", "reasoning_effort", `${m.id} does not accept reasoning_effort.`, "Remove it.", E.chat_reasoning_effort);89 else if (!m.responses.effort_levels.includes(effort)) add("error", "effort-level-unsupported", "reasoning_effort", `${m.id} does not support reasoning effort "${effort}". Supported: ${m.responses.effort_levels.join(", ")}.`, `Use one of: ${m.responses.effort_levels.join(", ")}.`, E.chat_tools_effort_none ?? E[`effort_${effort}`]);90 }91 const effective = effort ?? (c?.default_effort_is_none ? "none" : "default");92 if (m && req.temperature !== undefined && req.temperature !== 1) {93 if (c.temperature === "never") add("error", "sampling-param-rejected", "temperature", `${m.id} only supports the default temperature (1).`, "Remove `temperature`.", E.chat_temperature);94 else if (c.temperature === "effort_none_only" && effective !== "none") add("error", "sampling-needs-effort-none", "temperature", `${m.id} accepts a custom temperature only with reasoning_effort "none".`, 'Remove `temperature`, or set reasoning_effort: "none".', E.chat_temperature);95 }96 if (m && req.stop !== undefined && !c.stop) add("error", "stop-unsupported", "stop", `${m.id} rejects \`stop\`.`, "Remove `stop`; truncate in your own code if needed.", E.chat_stop);97 if (m && (req.logprobs === true || req.top_logprobs !== undefined) && !c.logprobs) add("error", "logprobs-unsupported", "logprobs", `${m.id} does not return logprobs.`, "Remove logprobs/top_logprobs.", E.chat_logprobs);98 if (m && Array.isArray(req.tools) && req.tools.some((t) => t?.type === "function")) {99 if (c.function_tools === "no") add("error", "chat-tools-unsupported", "tools", `${m.id} does not support function tools on Chat Completions.`, "Use the Responses API.", E.chat_tools);100 else if (c.function_tools === "effort_none_only" && effective !== "none") add("error", "chat-tools-need-effort-none", "tools", `${m.id} supports function tools on Chat Completions only with reasoning_effort "none".`, 'Use the Responses API (recommended), or set reasoning_effort: "none".', E.chat_tools);101 }102 const rf = req.response_format;103 if (rf?.type === "json_schema") {104 if (!rf.json_schema?.name || !rf.json_schema?.schema) add("error", "format-chat-shape", "response_format", "Chat Completions needs response_format: {type: \"json_schema\", json_schema: {name, schema, strict}}.");105 else if (rf.json_schema.strict !== false) checkStrictSchema(rf.json_schema.schema, "response_format.json_schema.schema", add);106 }107 if (req.text !== undefined) add("error", "chat-text-field", "text", "`text` (format, verbosity) is a Responses API field.", "Use response_format / verbosity on Chat Completions, or call the Responses API.");108 lintTools(req, m, "chat", add, E);109 (req.messages ?? []).forEach((x, i) => { if (!["system", "developer", "user", "assistant", "tool", "function"].includes(x?.role)) add("error", "role-invalid", `messages[${i}].role`, `Unknown role "${x?.role}".`); });110 if (m && m.reasoning_model) add("info", "prefer-responses", "", `For ${m.id} the Responses API is the recommended surface (reasoning state, built-in tools).`);111 }112 if (!m) return finish(findings, m, api);113 return finish(findings, m, api);114}115116function checkStrictSchema(schema, path, add) {117 const walk = (s, p) => {118 if (!s || typeof s !== "object") return;119 if (s.type === "object" || s.properties) {120 if (s.additionalProperties !== false) add("error", "strict-additional-properties", p, "Strict schemas need additionalProperties: false on every object.", 'Add "additionalProperties": false.');121 const props = Object.keys(s.properties ?? {}), req = s.required ?? [];122 const missing = props.filter((k) => !req.includes(k));123 if (missing.length) add("error", "strict-required-all", `${p}.required`, `Strict schemas must list every property in required (missing: ${missing.join(", ")}).`, 'List all properties; model optional values as a union with "null".');124 for (const [k, v] of Object.entries(s.properties ?? {})) walk(v, `${p}.properties.${k}`);125 }126 if (s.items) walk(s.items, `${p}.items`);127 for (const k of ["anyOf", "oneOf"]) if (Array.isArray(s[k])) s[k].forEach((v, i) => walk(v, `${p}.${k}[${i}]`));128 };129 walk(schema, path);130}131132function lintTools(req, m, api, add, E) {133 const tools = Array.isArray(req.tools) ? req.tools : [];134 const names = new Set();135 tools.forEach((t, i) => {136 const p = `tools[${i}]`;137 if (!t || typeof t !== "object") return add("error", "tool-invalid", p, "Each tool must be an object.");138 if (t.type === "function") {139 const nested = t.function && typeof t.function === "object";140 if (api === "responses" && nested) add("error", "tool-chat-shape", p, "In the Responses API name/description/parameters sit directly on the tool (the nested `function` wrapper is the Chat Completions shape).", "Flatten it: {type: \"function\", name, description, parameters, strict}.");141 if (api === "chat" && !nested) add("error", "tool-responses-shape", p, "Chat Completions needs {type: \"function\", function: {name, description, parameters}}.", "Wrap the fields in `function`.");142 const f = nested ? t.function : t;143 if (!f.name || !/^[a-zA-Z0-9_-]{1,64}$/.test(f.name)) add("error", "tool-name-invalid", `${p}.name`, "Function names must match ^[a-zA-Z0-9_-]{1,64}$.");144 else { if (names.has(f.name)) add("error", "tool-duplicate-name", `${p}.name`, `Duplicate function name "${f.name}".`); names.add(f.name); }145 if (f.strict === true && f.parameters) checkStrictSchema(f.parameters, `${p}.parameters`, add);146 if (!f.description || f.description.length < 20) add("warning", "tool-description-thin", `${p}.description`, "Tool descriptions drive tool selection; say what it does, when to use it and what it returns.");147 } else if (api === "chat" && ["web_search", "web_search_preview", "code_interpreter", "file_search", "image_generation", "mcp", "computer_use_preview"].includes(t.type)) {148 add("error", "builtin-tool-on-chat", `${p}.type`, `Built-in tool "${t.type}" is a Responses API feature.`, "Call the Responses API.");149 } else if (api === "responses" && m) {150 const r = m.responses;151 if (t.type === "code_interpreter") {152 if (r.code_interpreter === "no") add("error", "code-interpreter-unsupported", p, `${m.id} does not support the code_interpreter tool.`, "Remove it or use another model.", E.code_interpreter);153 if (!t.container) add("error", "code-interpreter-container", `${p}.container`, "code_interpreter needs a container.", 'Add container: {type: "auto"}.');154 }155 if (t.type === "file_search" && !(Array.isArray(t.vector_store_ids) && t.vector_store_ids.length)) add("error", "file-search-vector-store", `${p}.vector_store_ids`, "file_search needs vector_store_ids.");156 if (t.type === "mcp" && !t.server_url && !t.connector_id) add("error", "mcp-server-url", p, "An mcp tool needs server_url (or connector_id) and server_label.");157 }158 });159 const tc = req.tool_choice;160 if (tc !== undefined) {161 if (tools.length === 0 && tc !== "none" && tc !== "auto") add("error", "tool-choice-without-tools", "tool_choice", "tool_choice is set but no tools are defined.");162 const hosted = tools.some((t) => t?.type && t.type !== "function");163 if (m && api === "responses" && hosted && tc !== "auto" && m.responses.hosted_tools_need_auto_choice) add("error", "hosted-tools-need-auto", "tool_choice", `${m.id} only accepts tool_choice "auto" when hosted tools are present.`, 'Remove tool_choice or set it to "auto".', E.web_search ?? E.code_interpreter);164 if (tc && typeof tc === "object" && tc.type === "function") {165 const name = api === "chat" ? tc.function?.name : tc.name;166 if (api === "responses" && tc.function) add("error", "tool-choice-chat-shape", "tool_choice", 'In the Responses API use {type: "function", name}.');167 if (api === "chat" && !tc.function) add("error", "tool-choice-responses-shape", "tool_choice", 'Chat Completions needs {type: "function", function: {name}}.');168 if (name && !names.has(name)) add("error", "tool-choice-unknown-tool", "tool_choice", `tool_choice names "${name}", which is not in tools.`);169 }170 if (tc === "any") add("error", "tool-choice-any", "tool_choice", '"any" is not an OpenAI tool_choice value.', 'Use "required".');171 }172}173174function finish(findings, m, api) {175 const errors = findings.filter((f) => f.severity === "error").length;176 return { valid: errors === 0, api, model: m?.id ?? null, errors, warnings: findings.filter((f) => f.severity === "warning").length, findings };177}178179/** Apply mechanical fixes. Returns the corrected request, the changes and what is left for a human. */180export function fixRequest(req, opts = {}) {181 const r = structuredClone(req), changes = [], note = (s) => changes.push(s);182 const api = opts.api ?? detectApi(r);183 const m = resolveModel(r.model);184 if (!m) return { request: r, changes, remaining: lintRequest(r, opts) };185 const R = m.responses;186 const lower = (e) => { const order = ["none", "minimal", "low", "medium", "high", "xhigh", "max"]; const i = order.indexOf(e); return [...order.slice(0, i).reverse(), ...order.slice(i + 1)].find((x) => R.effort_levels.includes(x)); };187 if (api === "responses") {188 for (const k of ["max_tokens", "max_completion_tokens"]) if (r[k] !== undefined) { r.max_output_tokens ??= r[k]; delete r[k]; note(`renamed ${k} to max_output_tokens`); }189 if (r.reasoning_effort !== undefined) { r.reasoning = { ...(r.reasoning ?? {}), effort: r.reasoning_effort }; delete r.reasoning_effort; note("moved reasoning_effort into reasoning.effort"); }190 if (r.system !== undefined) { r.instructions ??= r.system; delete r.system; note("moved system into instructions"); }191 if (r.response_format !== undefined) {192 const rf = r.response_format;193 r.text = { ...(r.text ?? {}), format: rf.type === "json_schema" && rf.json_schema ? { type: "json_schema", ...rf.json_schema } : rf };194 delete r.response_format; note("moved response_format into text.format");195 }196 if (r.text?.format?.json_schema) { const { json_schema, ...rest } = r.text.format; r.text.format = { ...rest, ...json_schema }; note("flattened text.format.json_schema"); }197 if (Number.isInteger(r.max_output_tokens) && r.max_output_tokens < 16) { r.max_output_tokens = 16; note("raised max_output_tokens to the minimum (16)"); }198 if (m.max_output_tokens && r.max_output_tokens > m.max_output_tokens) { r.max_output_tokens = m.max_output_tokens; note(`capped max_output_tokens at ${m.max_output_tokens}`); }199 if (r.reasoning?.effort !== undefined) {200 if (R.effort_levels.length === 0) { delete r.reasoning; note("removed reasoning (not a reasoning model)"); }201 else if (!R.effort_levels.includes(r.reasoning.effort)) { const e = lower(r.reasoning.effort); note(`changed reasoning effort "${r.reasoning.effort}" to "${e}"`); r.reasoning.effort = e; }202 }203 const effective = r.reasoning?.effort ?? (R.default_effort_is_none ? "none" : "default");204 for (const k of ["temperature", "top_p"]) if (r[k] !== undefined && (R.sampling_params === "never" || (R.sampling_params === "effort_none_only" && effective !== "none"))) { delete r[k]; note(`removed ${k}`); }205 if (r.text?.verbosity !== undefined && !R.verbosity_levels.includes(r.text.verbosity)) { delete r.text.verbosity; note("removed unsupported text.verbosity"); }206 if (r.top_logprobs !== undefined && !R.logprobs) { delete r.top_logprobs; note("removed top_logprobs"); }207 if (Array.isArray(r.include) && !R.logprobs) { const n0 = r.include.length; r.include = r.include.filter((x) => x !== "message.output_text.logprobs"); if (r.include.length !== n0) note("removed logprobs include"); if (!r.include.length) delete r.include; }208 if (r.service_tier === "flex" && !R.flex_tier) { delete r.service_tier; note("removed service_tier flex"); }209 for (const t of r.tools ?? []) {210 if (t?.type === "function" && t.function) { Object.assign(t, t.function); delete t.function; note(`flattened function tool "${t.name}"`); }211 if (t?.type === "code_interpreter" && !t.container) { t.container = { type: "auto" }; note("added code_interpreter container"); }212 }213 if (r.tools) { const n0 = r.tools.length; r.tools = r.tools.filter((t) => !(t?.type === "code_interpreter" && R.code_interpreter === "no")); if (r.tools.length !== n0) note("removed unsupported code_interpreter tool"); }214 if (r.tool_choice?.function) { r.tool_choice = { type: "function", name: r.tool_choice.function.name }; note("flattened tool_choice"); }215 if (r.tool_choice === "any") { r.tool_choice = "required"; note('changed tool_choice "any" to "required"'); }216 if (R.hosted_tools_need_auto_choice && r.tool_choice !== undefined && r.tool_choice !== "auto" && (r.tools ?? []).some((t) => t?.type !== "function")) { delete r.tool_choice; note("removed tool_choice (hosted tools need auto on this model)"); }217 if (Array.isArray(r.input)) for (const it of r.input) for (const c of Array.isArray(it?.content) ? it.content : []) {218 if (c.type === "text") { c.type = it.role === "assistant" ? "output_text" : "input_text"; note("renamed content type text"); }219 if (c.type === "image_url") { c.type = "input_image"; if (c.image_url && typeof c.image_url === "object") c.image_url = c.image_url.url; note("converted image_url content part to input_image"); }220 }221 if (r.text && Object.keys(r.text).length === 0) delete r.text;222 } else if (m.chat_completions.supported) {223 const C = m.chat_completions;224 if (r.max_output_tokens !== undefined) { r.max_completion_tokens ??= r.max_output_tokens; delete r.max_output_tokens; note("renamed max_output_tokens to max_completion_tokens"); }225 if (r.max_tokens !== undefined && !C.max_tokens_legacy) { r.max_completion_tokens ??= r.max_tokens; delete r.max_tokens; note("renamed max_tokens to max_completion_tokens"); }226 if (r.reasoning?.effort !== undefined) { r.reasoning_effort = r.reasoning.effort; delete r.reasoning; note("moved reasoning.effort to reasoning_effort"); }227 if (r.reasoning_effort !== undefined) {228 if (R.effort_levels.length === 0) { delete r.reasoning_effort; note("removed reasoning_effort (not a reasoning model)"); }229 else if (!R.effort_levels.includes(r.reasoning_effort)) { const e = lower(r.reasoning_effort); note(`changed reasoning_effort "${r.reasoning_effort}" to "${e}"`); r.reasoning_effort = e; }230 }231 const effective = r.reasoning_effort ?? (C.default_effort_is_none ? "none" : "default");232 if (r.temperature !== undefined && r.temperature !== 1 && (C.temperature === "never" || (C.temperature === "effort_none_only" && effective !== "none"))) { delete r.temperature; note("removed temperature"); }233 if (r.stop !== undefined && !C.stop) { delete r.stop; note("removed stop"); }234 if ((r.logprobs !== undefined || r.top_logprobs !== undefined) && !C.logprobs) { delete r.logprobs; delete r.top_logprobs; note("removed logprobs"); }235 for (const t of r.tools ?? []) if (t?.type === "function" && !t.function) { const { type, ...f } = t; for (const k of Object.keys(f)) delete t[k]; t.function = f; note(`wrapped function tool "${f.name}"`); }236 if (r.tool_choice?.type === "function" && r.tool_choice.name) { r.tool_choice = { type: "function", function: { name: r.tool_choice.name } }; note("wrapped tool_choice"); }237 }238 return { request: r, changes, remaining: lintRequest(r, opts) };239}240241/** Convert a Chat Completions request into the equivalent Responses request (then fix it). */242export function chatToResponses(req) {243 const r = { model: req.model }, notes = [];244 const sys = (req.messages ?? []).filter((x) => x.role === "system" || x.role === "developer");245 if (sys.length) r.instructions = sys.map((x) => (typeof x.content === "string" ? x.content : (x.content ?? []).map((c) => c.text ?? "").join("\n"))).join("\n\n");246 r.input = [];247 for (const x of req.messages ?? []) {248 if (x.role === "system" || x.role === "developer") continue;249 if (x.role === "tool") { r.input.push({ type: "function_call_output", call_id: x.tool_call_id, output: typeof x.content === "string" ? x.content : JSON.stringify(x.content) }); continue; }250 if (x.role === "assistant" && Array.isArray(x.tool_calls)) { if (x.content) r.input.push({ role: "assistant", content: x.content }); for (const tc of x.tool_calls) r.input.push({ type: "function_call", call_id: tc.id, name: tc.function?.name, arguments: tc.function?.arguments ?? "{}" }); continue; }251 const content = typeof x.content === "string" ? x.content : (x.content ?? []).map((c) => (c.type === "text" ? { type: x.role === "assistant" ? "output_text" : "input_text", text: c.text } : c.type === "image_url" ? { type: "input_image", image_url: typeof c.image_url === "string" ? c.image_url : c.image_url?.url } : c));252 r.input.push({ role: x.role, content });253 }254 if (r.input.length === 1 && r.input[0].role === "user" && typeof r.input[0].content === "string") r.input = r.input[0].content;255 const max = req.max_completion_tokens ?? req.max_tokens;256 if (max !== undefined) r.max_output_tokens = Math.max(16, max);257 if (req.reasoning_effort) r.reasoning = { effort: req.reasoning_effort };258 if (req.response_format) r.text = { format: req.response_format.type === "json_schema" ? { type: "json_schema", ...req.response_format.json_schema } : req.response_format };259 if (req.verbosity) r.text = { ...(r.text ?? {}), verbosity: req.verbosity };260 if (Array.isArray(req.tools)) r.tools = req.tools.map((t) => (t.type === "function" && t.function ? { type: "function", ...t.function } : t));261 if (req.tool_choice !== undefined) r.tool_choice = req.tool_choice?.function ? { type: "function", name: req.tool_choice.function.name } : req.tool_choice;262 for (const k of ["temperature", "top_p", "parallel_tool_calls", "metadata", "store", "service_tier", "prompt_cache_key", "user", "safety_identifier", "top_logprobs"]) if (req[k] !== undefined) r[k] = req[k];263 for (const k of ["n", "stop", "seed", "frequency_penalty", "presence_penalty", "logit_bias", "logprobs"]) if (req[k] !== undefined) notes.push(`\`${k}\` has no Responses API equivalent and was dropped.`);264 if (req.stream) { r.stream = true; notes.push("Streaming events differ: Responses emits typed events (response.output_text.delta, response.completed, …)."); }265 notes.push("Read the result from response.output_text (or response.output items), not choices[0].message.");266 const fixed = fixRequest(r, { api: "responses" });267 return { request: fixed.request, changes: fixed.changes, notes, remaining: fixed.remaining };268}269