

























@@ -799,6 +799,123 @@ describe("openai-completions stop-reason tool-call guard", () => {
799799expect(result.content.some((block) => block.type === "thinking")).toBe(false);
800800});
801801802+it("seals the native reasoning block before the answer text begins", async () => {
803+// deepseek streams reasoning_content, then switches to content with no
804+// boundary event; thinking_end must precede the answer so channels do not
805+// merge the answer into the reasoning block.
806+mockChunksRef.chunks = [
807+{
808+id: "chatcmpl-test",
809+choices: [{ index: 0, delta: { reasoning_content: "Let me think." } }],
810+},
811+{
812+id: "chatcmpl-test",
813+choices: [{ index: 0, delta: { reasoning_content: " Still thinking." } }],
814+},
815+makeTextChunk("The answer"),
816+makeTextChunk(" is 42."),
817+makeFinishChunk("stop"),
818+];
819+820+const stream = streamOpenAICompletions(reasoningModel, context, {
821+apiKey: "sk-test",
822+reasoningEffort: "medium",
823+});
824+const eventTypes: string[] = [];
825+for await (const event of stream as AsyncIterable<{ type: string }>) {
826+eventTypes.push(event.type);
827+}
828+const result = await stream.result();
829+830+const thinkingEndIndex = eventTypes.indexOf("thinking_end");
831+const textStartIndex = eventTypes.indexOf("text_start");
832+const firstTextDeltaIndex = eventTypes.indexOf("text_delta");
833+expect(thinkingEndIndex).toBeGreaterThanOrEqual(0);
834+expect(textStartIndex).toBeGreaterThanOrEqual(0);
835+expect(thinkingEndIndex).toBeLessThan(textStartIndex);
836+expect(thinkingEndIndex).toBeLessThan(firstTextDeltaIndex);
837+// thinking_end is emitted exactly once even though the block is also
838+// visited by the end-of-stream finish loop.
839+expect(eventTypes.filter((type) => type === "thinking_end")).toHaveLength(1);
840+841+expect(result.content).toContainEqual({
842+type: "thinking",
843+thinking: "Let me think. Still thinking.",
844+thinkingSignature: "reasoning_content",
845+});
846+expect(result.content).toContainEqual({ type: "text", text: "The answer is 42." });
847+});
848+849+it("seals the native reasoning block before a following tool call", async () => {
850+mockChunksRef.chunks = [
851+{
852+id: "chatcmpl-test",
853+choices: [{ index: 0, delta: { reasoning_content: "I should call a tool." } }],
854+},
855+makeToolCallChunk("call_1", "bash", '{"cmd":"ls"}'),
856+makeFinishChunk("tool_calls"),
857+];
858+859+const stream = streamOpenAICompletions(reasoningModel, context, {
860+apiKey: "sk-test",
861+reasoningEffort: "medium",
862+});
863+const eventTypes: string[] = [];
864+for await (const event of stream as AsyncIterable<{ type: string }>) {
865+eventTypes.push(event.type);
866+}
867+await stream.result();
868+869+const thinkingEndIndex = eventTypes.indexOf("thinking_end");
870+const toolCallStartIndex = eventTypes.indexOf("toolcall_start");
871+expect(thinkingEndIndex).toBeGreaterThanOrEqual(0);
872+expect(toolCallStartIndex).toBeGreaterThanOrEqual(0);
873+expect(thinkingEndIndex).toBeLessThan(toolCallStartIndex);
874+expect(eventTypes.filter((type) => type === "thinking_end")).toHaveLength(1);
875+});
876+877+it("keeps one native reasoning block when content and reasoning co-occur", async () => {
878+mockChunksRef.chunks = [
879+{
880+id: "chatcmpl-test",
881+choices: [{ index: 0, delta: { reasoning_content: "First thought." } }],
882+},
883+{
884+id: "chatcmpl-test",
885+choices: [
886+{
887+index: 0,
888+delta: {
889+content: "Visible text that shares the reasoning chunk.",
890+reasoning_content: " Second thought.",
891+},
892+},
893+],
894+},
895+makeTextChunk(" Final answer."),
896+makeFinishChunk("stop"),
897+];
898+899+const stream = streamOpenAICompletions(reasoningModel, context, {
900+apiKey: "sk-test",
901+reasoningEffort: "medium",
902+});
903+const eventTypes: string[] = [];
904+for await (const event of stream as AsyncIterable<{ type: string }>) {
905+eventTypes.push(event.type);
906+}
907+const result = await stream.result();
908+909+expect(eventTypes.filter((type) => type === "thinking_start")).toHaveLength(1);
910+expect(eventTypes.filter((type) => type === "thinking_end")).toHaveLength(1);
911+expect(eventTypes.indexOf("thinking_end")).toBeLessThan(eventTypes.indexOf("text_start"));
912+expect(result.content).toContainEqual({
913+type: "thinking",
914+thinking: "First thought. Second thought.",
915+thinkingSignature: "reasoning_content",
916+});
917+});
918+802919it("promotes silent tool_calls with finish_reason stop to toolUse", async () => {
803920mockChunksRef.chunks = [
804921makeToolCallChunk("call_1", "bash", '{"cmd":"ls"}'),
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。