Skip to content

Commit 0929072

Browse files
committed
fix: strip thinking blocks when converting messages, causing DeepSeek 400 errors on follow-up requests
1 parent 270fb48 commit 0929072

4 files changed

Lines changed: 32 additions & 10 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@mohanscodex/spectra-ai": patch
3+
---
4+
5+
Fix thinking/reasoning blocks being stripped when sending follow-up API requests to providers that require reasoning_content echo-back (DeepSeek, Qwen, etc.)

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,13 @@ coverage/
4646
# Temporary
4747
*.tmp
4848
*.temp
49+
*.diff
4950
.tmp/
5051

52+
# Analysis scratch files (root-level scripts for one-off debugging)
53+
trace_detail.py
54+
trace_query.py
55+
5156
# Planning (local only)
5257
.planning/
5358

packages/ai/src/providers/openai-completions.ts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -320,13 +320,18 @@ function convertMessages(model: Model, context: Context): ChatCompletionMessageP
320320
}
321321
} else if (msg.role === "assistant") {
322322
const textBlocks = msg.content.filter((b) => b.type === "text") as TextContent[];
323+
const thinkingBlocks = msg.content.filter((b) => b.type === "thinking" && !(b as ThinkingContent).redacted) as ThinkingContent[];
323324
const toolCalls = msg.content.filter((b) => b.type === "toolCall") as ToolCall[];
324325

325-
if (textBlocks.length > 0 || toolCalls.length > 0) {
326-
const assistantMsg: ChatCompletionAssistantMessageParam = {
327-
role: "assistant",
328-
content: textBlocks.map((b) => sanitizeSurrogates(b.text)).join(""),
329-
};
326+
if (textBlocks.length > 0 || thinkingBlocks.length > 0 || toolCalls.length > 0) {
327+
const assistantMsg = {
328+
role: "assistant" as const,
329+
content: textBlocks.map((b) => sanitizeSurrogates(b.text)).join("") || null,
330+
} as Record<string, unknown>;
331+
332+
if (thinkingBlocks.length > 0) {
333+
assistantMsg.reasoning_content = thinkingBlocks.map((t) => t.thinking).join("\n");
334+
}
330335

331336
if (toolCalls.length > 0) {
332337
assistantMsg.tool_calls = toolCalls.map((tc) => ({
@@ -336,7 +341,7 @@ function convertMessages(model: Model, context: Context): ChatCompletionMessageP
336341
}));
337342
}
338343

339-
params.push(assistantMsg);
344+
params.push(assistantMsg as unknown as ChatCompletionAssistantMessageParam);
340345
}
341346
} else if (msg.role === "toolResult") {
342347
const textResult = msg.content

packages/ai/src/providers/openai-responses.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -321,12 +321,19 @@ function convertResponsesMessages(model: Model, context: Context): unknown[] {
321321
messages.push({ role: "user", content });
322322
}
323323
} else if (msg.role === "assistant") {
324+
const textBlocks = msg.content.filter((b) => b.type === "text") as TextContent[];
325+
if (textBlocks.length > 0) {
326+
messages.push({
327+
type: "message",
328+
role: "assistant",
329+
content: textBlocks.map((b) => ({ type: "output_text" as const, text: sanitizeSurrogates(b.text) })),
330+
});
331+
}
324332
for (const block of msg.content) {
325-
if (block.type === "text") {
333+
if (block.type === "thinking" && !(block as ThinkingContent).redacted) {
326334
messages.push({
327-
type: "message",
328-
role: "assistant",
329-
content: [{ type: "output_text", text: sanitizeSurrogates((block as TextContent).text) }],
335+
type: "reasoning",
336+
summary: [{ type: "summary_text" as const, text: (block as ThinkingContent).thinking }],
330337
});
331338
} else if (block.type === "toolCall") {
332339
const tc = block as ToolCall;

0 commit comments

Comments
 (0)