mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-18 12:54:47 +00:00
fix(transport): strip empty-string reasoning_content from OpenRouter assistant replay
DeepSeek V4 via OpenRouter injects reasoning_content: "" on assistant messages that contain tool_calls. The sanitizer only deleted the field when it was not a string, so empty strings slipped through and were replayed on follow-up turns. OpenRouter rejects the field with an HTTP 500 Internal Server Error instead of a descriptive 4xx, breaking every subsequent tool-call turn for the session. Also strip empty-string reasoning for the same reason. Closes #82150
This commit is contained in:
committed by
Peter Steinberger
parent
127156a88a
commit
25864ee540
@@ -5609,6 +5609,60 @@ describe("buildOpenAICompletionsParams sanitizes reasoning replay fields", () =>
|
||||
expect(assistant.reasoning_content).toBe("Need to answer politely.");
|
||||
});
|
||||
|
||||
it("strips empty-string reasoning_content from OpenRouter assistant replay", () => {
|
||||
// DeepSeek V4 via OpenRouter injects reasoning_content: "" on tool-call
|
||||
// assistant messages. An empty string is technically a "string" but the
|
||||
// Chat Completions schema does not accept it, and OpenRouter rejects the
|
||||
// replayed message with HTTP 500.
|
||||
const params = buildOpenAICompletionsParams(
|
||||
openRouterModel,
|
||||
{
|
||||
systemPrompt: "system",
|
||||
messages: [
|
||||
{ role: "user", content: "read config" },
|
||||
{
|
||||
role: "assistant",
|
||||
provider: "openrouter",
|
||||
api: "openai-completions",
|
||||
model: "deepseek/deepseek-v4-pro",
|
||||
stopReason: "toolUse",
|
||||
timestamp: 0,
|
||||
content: [
|
||||
{
|
||||
type: "thinking",
|
||||
thinking: "",
|
||||
thinkingSignature: "reasoning_content",
|
||||
},
|
||||
{
|
||||
type: "toolCall",
|
||||
id: "call_1",
|
||||
name: "read_file",
|
||||
arguments: { path: "config.json" },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
role: "toolResult",
|
||||
toolCallId: "call_1",
|
||||
toolName: "read_file",
|
||||
content: [{ type: "text", text: "{ }" }],
|
||||
isError: false,
|
||||
timestamp: 1,
|
||||
},
|
||||
{ role: "user", content: "continue" },
|
||||
],
|
||||
tools: [],
|
||||
} as never,
|
||||
undefined,
|
||||
) as { messages: unknown };
|
||||
|
||||
const msgs = params.messages as Record<string, unknown>[];
|
||||
const assistantMsgs = msgs.filter((m) => m.role === "assistant");
|
||||
for (const msg of assistantMsgs) {
|
||||
expect(msg).not.toHaveProperty("reasoning_content");
|
||||
}
|
||||
});
|
||||
|
||||
it("preserves OpenRouter array reasoning_details from tool-call signatures", () => {
|
||||
const reasoningDetail = { type: "reasoning.encrypted", id: "rs_1", data: "ciphertext" };
|
||||
const params = buildOpenAICompletionsParams(
|
||||
|
||||
@@ -2448,10 +2448,16 @@ function sanitizeOpenRouterReasoningReplayFields(record: Record<string, unknown>
|
||||
delete record.reasoning_details;
|
||||
}
|
||||
|
||||
if ("reasoning" in record && typeof record.reasoning !== "string") {
|
||||
// Strip non-string AND empty-string reasoning fields — empty strings are
|
||||
// response artifacts that upstream providers (OpenRouter, DeepSeek) reject
|
||||
// with HTTP 500 when replayed on follow-up turns.
|
||||
if ("reasoning" in record && (typeof record.reasoning !== "string" || record.reasoning === "")) {
|
||||
delete record.reasoning;
|
||||
}
|
||||
if ("reasoning_content" in record && typeof record.reasoning_content !== "string") {
|
||||
if (
|
||||
"reasoning_content" in record &&
|
||||
(typeof record.reasoning_content !== "string" || record.reasoning_content === "")
|
||||
) {
|
||||
delete record.reasoning_content;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user