// When exactly does omitting reasoning_content with tools trigger the documented 400? // Scenario: turn1 = tool call -> tool result -> final answer; turn2 = new user question. Variants strip reasoning_content at different places. import { raw, save } from "./lib.ts"; const m = "deepseek-v4-flash"; const tools = [{ type: "function", function: { name: "get_weather", description: "Get the current weather for a city", parameters: { type: "object", properties: { city: { type: "string" } }, required: ["city"] } } }]; const call = (messages: any[], extra: Record = {}) => raw("/chat/completions", { method: "POST", body: JSON.stringify({ model: m, messages, tools, max_tokens: 300, reasoning_effort: "low", ...extra }) }); const out: Record = {}; // Build a genuine turn 1 const msgs: any[] = [{ role: "user", content: "Weather in Montreal? Use the tool." }]; const r1 = await call(msgs); const a1 = (r1.body as any).choices[0].message; msgs.push(a1); for (const tc of a1.tool_calls ?? []) msgs.push({ role: "tool", tool_call_id: tc.id, content: "21C sunny" }); const r2 = await call(msgs); const a2 = (r2.body as any).choices[0].message; msgs.push(a2); out.turn1 = { a1: { reasoning_len: a1.reasoning_content?.length, tool_calls: a1.tool_calls?.length, content: a1.content }, a2: { reasoning_len: a2.reasoning_content?.length, content: a2.content, finish: (r2.body as any).choices[0].finish_reason, usage: (r2.body as any).usage } }; console.log("turn1 built:", JSON.stringify(out.turn1)); const strip = (arr: any[], which: (i: number, msg: any) => boolean) => arr.map((msg, i) => (msg.role === "assistant" && which(i, msg) ? Object.fromEntries(Object.entries(msg).filter(([k]) => k !== "reasoning_content")) : msg)); const q2 = { role: "user", content: "And in Quebec City? Use the tool." }; const variants: Record = { "full_replay": [...msgs, q2], "strip_all": [...strip(msgs, () => true), q2], "strip_toolcall_msg_only": [...strip(msgs, (_, msg) => !!msg.tool_calls), q2], "strip_final_answer_only": [...strip(msgs, (_, msg) => !msg.tool_calls), q2], "empty_string_reasoning": [...msgs.map((msg) => (msg.role === "assistant" ? { ...msg, reasoning_content: "" } : msg)), q2], "null_reasoning": [...msgs.map((msg) => (msg.role === "assistant" ? { ...msg, reasoning_content: null } : msg)), q2], }; for (const [name, messages] of Object.entries(variants)) { const r = await call(messages); const b: any = r.body; out[name] = { status: r.status, finish: b.choices?.[0]?.finish_reason, tool_calls: b.choices?.[0]?.message?.tool_calls?.length, reasoning_len: b.choices?.[0]?.message?.reasoning_content?.length, usage: b.usage, error: r.status !== 200 ? b : undefined }; console.log(name, "→", r.status, r.status === 200 ? `finish=${b.choices?.[0]?.finish_reason} cache_hit=${b.usage?.prompt_cache_hit_tokens}` : JSON.stringify(b).slice(0, 300)); } // same conversation, tools param removed on turn 2, full replay (reasoning ignored?) const r3 = await raw("/chat/completions", { method: "POST", body: JSON.stringify({ model: m, messages: [...msgs, { role: "user", content: "Summarize what you told me in 5 words." }], max_tokens: 300, reasoning_effort: "low" }) }); out.no_tools_param_after_tool_turn = { status: r3.status, body: r3.status === 200 ? { content: (r3.body as any).choices[0].message.content, usage: (r3.body as any).usage } : r3.body }; console.log("no tools param after tool turn →", r3.status, JSON.stringify(out.no_tools_param_after_tool_turn).slice(0, 300)); // thinking disabled on turn 2 with reasoning_content present in history + tools const r4 = await call([...msgs, q2], { thinking: { type: "disabled" }, reasoning_effort: undefined }); out.thinking_disabled_turn2_with_reasoning_history = { status: r4.status, finish: (r4.body as any).choices?.[0]?.finish_reason, error: r4.status !== 200 ? r4.body : undefined }; console.log("thinking disabled turn2 w/ reasoning history →", r4.status, JSON.stringify(r4.body).slice(0, 200)); save("09-reasoning-replay-tools.json", out);