fix(ai): concatenate openai completions reasoning deltas (#8605)

This commit is contained in:
Cristina Poncela Cubeiro 2026-08-25 11:19:31 +02:00 • committed by GitHub
parent 5cd6a2a502
commit c5ad7c1b0f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 96 additions and 3 deletions

View file

@ -247,6 +247,31 @@ function parseLegacyEncryptedReasoningDetail(
}
}
function fillMissingCommonReasoningDetailFields(
target: OpenAIReasoningDetailBase,
source: OpenAIReasoningDetail,
): void {
target.id ??= source.id;
target.format ||= source.format;
target.index ??= source.index;
}
function appendOpenAIReasoningDetail(details: OpenAIReasoningDetail[], detail: OpenAIReasoningDetail): void {
const lastDetail = details[details.length - 1];
if (detail.type === "reasoning.text" && lastDetail?.type === "reasoning.text") {
lastDetail.text += detail.text;
lastDetail.signature ||= detail.signature;
fillMissingCommonReasoningDetailFields(lastDetail, detail);
return;
}
if (detail.type === "reasoning.summary" && lastDetail?.type === "reasoning.summary") {
lastDetail.summary += detail.summary;
fillMissingCommonReasoningDetailFields(lastDetail, detail);
return;
}
details.push({ ...detail });
}
const OPENAI_COMPLETIONS_REASONING_FIELDS = ["reasoning", "reasoning_content", "reasoning_text"] as const;
type OpenAICompletionsReasoningField = (typeof OPENAI_COMPLETIONS_REASONING_FIELDS)[number];
@ -623,9 +648,10 @@ export const stream: StreamFunction<"openai-completions", OpenAICompletionsOptio
if (!isOpenAIReasoningDetail(detail)) continue;
const block = ensureThinkingBlock("");
const preservedDetails = parseOpenAIReasoningDetails(block.thinkingSignature) ?? [];
preservedDetails.push(detail);
// Keep provider replay data in the existing signature slot. OpenRouter
// requires the complete reasoning_details sequence in its original order.
appendOpenAIReasoningDetail(preservedDetails, detail);
// Keep provider replay data in the existing signature slot. OpenRouter streams
// reasoning_details as deltas: consecutive text/summary deltas are merged into
// logical entries, while encrypted entries remain opaque and discrete.
block.thinkingSignature = JSON.stringify(preservedDetails);
}
}

View file

@ -181,4 +181,71 @@ describe("openai-completions reasoning_details streaming", () => {
expect(payload?.reasoning_details).toEqual(expectedReasoningDetails);
expect(payload?.reasoning).toBeUndefined();
});
it("merges consecutive text and summary reasoning_details deltas before replay", async () => {
const textDelta = { type: "reasoning.text", text: "The", index: 0 };
const textDeltaWithSignature = {
type: "reasoning.text",
text: " user wants the time.",
signature: "sha256:text-signature",
format: "openai-responses-v1",
index: 0,
};
const summaryDelta = { type: "reasoning.summary", summary: "Looked", index: 0 };
const summaryDeltaWithFormat = {
type: "reasoning.summary",
summary: " up time.",
format: "openai-responses-v1",
index: 0,
};
const laterSummaryDelta = {
type: "reasoning.summary",
summary: "After encrypted block.",
format: "openai-responses-v1",
index: 0,
};
const expectedReasoningDetails = [
{
type: "reasoning.text",
text: "The user wants the time.",
index: 0,
signature: "sha256:text-signature",
format: "openai-responses-v1",
},
{
type: "reasoning.summary",
summary: "Looked up time.",
index: 0,
format: "openai-responses-v1",
},
reasoningDetail,
laterSummaryDelta,
];
mockState.chunkSets = [
[
chunk({ reasoning_details: [textDelta] }),
chunk({ reasoning_details: [textDeltaWithSignature] }),
chunk({ reasoning_details: [summaryDelta] }),
chunk({ reasoning_details: [summaryDeltaWithFormat] }),
chunk({ reasoning_details: [reasoningDetail] }),
chunk({ reasoning_details: [laterSummaryDelta] }),
toolCallChunk(),
chunk({}, "tool_calls"),
],
[chunk({ content: "ok" }), chunk({}, "stop")],
];
const assistantMessage = await runOpenAICompletionsStream();
const thinking = assistantMessage.content.find((block) => block.type === "thinking");
expect(thinking).toEqual({
type: "thinking",
thinking: "",
thinkingSignature: JSON.stringify(expectedReasoningDetails),
});
await runOpenAICompletionsStream([assistantMessage]);
expect(getAssistantPayload(mockState.payloads[1])?.reasoning_details).toEqual(expectedReasoningDetails);
});
});