Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions src/api/providers/__tests__/deepseek.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,75 @@ describe("DeepSeekHandler", () => {
expect(usageChunks[0].cacheWriteTokens).toBe(8)
expect(usageChunks[0].cacheReadTokens).toBe(2)
})

it("streams reasoning chunks from delta.reasoning_content", async () => {
mockCreate.mockImplementationOnce(async () => ({
[Symbol.asyncIterator]: async function* () {
yield { choices: [{ delta: { reasoning_content: "thinking..." }, index: 0 }] }
yield { choices: [{ delta: { content: "answer" }, index: 0 }] }
yield {
choices: [{ delta: {}, index: 0 }],
usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 },
}
},
}))

const chunks: any[] = []
for await (const chunk of handler.createMessage(systemPrompt, messages)) {
chunks.push(chunk)
}

expect(chunks).toContainEqual({ type: "reasoning", text: "thinking..." })
})

it("falls back to delta.reasoning when reasoning_content is absent", async () => {
mockCreate.mockImplementationOnce(async () => ({
[Symbol.asyncIterator]: async function* () {
yield { choices: [{ delta: { reasoning: "router-style thought" }, index: 0 }] }
yield {
choices: [{ delta: {}, index: 0 }],
usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 },
}
},
}))

const chunks: any[] = []
for await (const chunk of handler.createMessage(systemPrompt, messages)) {
chunks.push(chunk)
}

expect(chunks).toContainEqual({ type: "reasoning", text: "router-style thought" })
})

it("prefers delta.reasoning_content over delta.reasoning when both are present", async () => {
mockCreate.mockImplementationOnce(async () => ({
[Symbol.asyncIterator]: async function* () {
yield {
choices: [
{
delta: {
reasoning_content: "primary thought",
reasoning: "fallback thought",
},
index: 0,
},
],
}
yield {
choices: [{ delta: {}, index: 0 }],
usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 },
}
},
}))

const chunks: any[] = []
for await (const chunk of handler.createMessage(systemPrompt, messages)) {
chunks.push(chunk)
}

const reasoningChunks = chunks.filter((chunk) => chunk.type === "reasoning")
expect(reasoningChunks).toEqual([{ type: "reasoning", text: "primary thought" }])
})
})

describe("processUsageMetrics", () => {
Expand Down
73 changes: 60 additions & 13 deletions src/api/providers/__tests__/mimo.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -470,21 +470,70 @@ describe("MimoHandler", () => {
expect(usageChunks[0].outputTokens).toBe(5)
})

it("should handle reasoning_content in stream", async () => {
// Override mock to return reasoning_content
it("streams reasoning chunks from delta.reasoning_content", async () => {
mockCreate.mockImplementationOnce(async () => ({
[Symbol.asyncIterator]: async function* () {
yield { choices: [{ delta: { reasoning_content: "thinking..." }, index: 0 }] }
yield { choices: [{ delta: { content: "answer" }, index: 0 }] }
yield {
choices: [{ delta: { reasoning_content: "Thinking..." }, index: 0 }],
usage: null,
choices: [{ delta: {}, index: 0 }],
usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 },
}
},
}))

const messages: Anthropic.Messages.MessageParam[] = [
{ role: "user", content: [{ type: "text", text: "Hello" }] },
]

const chunks: any[] = []
for await (const chunk of handler.createMessage("System prompt", messages)) {
chunks.push(chunk)
}

expect(chunks).toContainEqual({ type: "reasoning", text: "thinking..." })
})

it("falls back to delta.reasoning when reasoning_content is absent", async () => {
mockCreate.mockImplementationOnce(async () => ({
[Symbol.asyncIterator]: async function* () {
yield { choices: [{ delta: { reasoning: "router-style thought" }, index: 0 }] }
yield {
choices: [{ delta: { content: "Done" }, index: 0 }],
usage: null,
choices: [{ delta: {}, index: 0 }],
usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 },
}
},
}))

const messages: Anthropic.Messages.MessageParam[] = [
{ role: "user", content: [{ type: "text", text: "Hello" }] },
]

const chunks: any[] = []
for await (const chunk of handler.createMessage("System prompt", messages)) {
chunks.push(chunk)
}

expect(chunks).toContainEqual({ type: "reasoning", text: "router-style thought" })
})

it("prefers delta.reasoning_content over delta.reasoning when both are present", async () => {
mockCreate.mockImplementationOnce(async () => ({
[Symbol.asyncIterator]: async function* () {
yield {
choices: [{ delta: {}, index: 0, finish_reason: "stop" }],
usage: { prompt_tokens: 5, completion_tokens: 3, total_tokens: 8 },
choices: [
{
delta: {
reasoning_content: "primary thought",
reasoning: "fallback thought",
},
index: 0,
},
],
}
yield {
choices: [{ delta: {}, index: 0 }],
usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 },
}
},
}))
Expand All @@ -494,14 +543,12 @@ describe("MimoHandler", () => {
]

const chunks: any[] = []
const stream = handler.createMessage("System prompt", messages)
for await (const chunk of stream) {
for await (const chunk of handler.createMessage("System prompt", messages)) {
chunks.push(chunk)
}

const reasoningChunks = chunks.filter((c) => c.type === "reasoning")
expect(reasoningChunks).toHaveLength(1)
expect(reasoningChunks[0].text).toBe("Thinking...")
const reasoningChunks = chunks.filter((chunk) => chunk.type === "reasoning")
expect(reasoningChunks).toEqual([{ type: "reasoning", text: "primary thought" }])
})

it("should yield tool_call_partial chunks from stream", async () => {
Expand Down
71 changes: 71 additions & 0 deletions src/api/providers/__tests__/openai.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,77 @@ describe("OpenAiHandler", () => {
expect(textChunks[0].text).toBe("Test response")
})

it("streams reasoning chunks from delta.reasoning_content", async () => {
mockCreate.mockImplementationOnce(async () => ({
[Symbol.asyncIterator]: async function* () {
yield { choices: [{ delta: { reasoning_content: "thinking..." }, index: 0 }] }
yield { choices: [{ delta: { content: "answer" }, index: 0 }] }
yield {
choices: [{ delta: {}, index: 0 }],
usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 },
}
},
}))

const chunks: any[] = []
for await (const chunk of handler.createMessage(systemPrompt, messages)) {
chunks.push(chunk)
}

expect(chunks).toContainEqual({ type: "reasoning", text: "thinking..." })
})

it("falls back to delta.reasoning when reasoning_content is absent", async () => {
mockCreate.mockImplementationOnce(async () => ({
[Symbol.asyncIterator]: async function* () {
yield { choices: [{ delta: { reasoning: "router-style thought" }, index: 0 }] }
yield {
choices: [{ delta: {}, index: 0 }],
usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 },
}
},
}))

const chunks: any[] = []
for await (const chunk of handler.createMessage(systemPrompt, messages)) {
chunks.push(chunk)
}

expect(chunks).toContainEqual({ type: "reasoning", text: "router-style thought" })
})
Comment on lines +244 to +261

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does the helper priority rule get exercised here? If a delta has both reasoning_content and reasoning populated, the expected winner is reasoning_content — but that case isn't covered at the provider level. The unit tests for extractReasoningFromDelta cover it, but a provider-level check would catch a future regression where the helper call gets bypassed or re-inlined.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. I added a provider-level priority test to cover the case where both delta.reasoning_content and delta.reasoning are present. The test asserts that only reasoning_content is emitted as the reasoning chunk, so it should catch regressions where the helper is bypassed or the logic is re-inlined incorrectly.


it("prefers delta.reasoning_content over delta.reasoning when both are present", async () => {
mockCreate.mockImplementationOnce(async () => ({
[Symbol.asyncIterator]: async function* () {
yield {
choices: [
{
delta: {
reasoning_content: "primary thought",
reasoning: "fallback thought",
},
index: 0,
},
],
}
yield {
choices: [{ delta: {}, index: 0 }],
usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 },
}
},
}))

const chunks: any[] = []

for await (const chunk of handler.createMessage(systemPrompt, messages)) {
chunks.push(chunk)
}

const reasoningChunks = chunks.filter((chunk) => chunk.type === "reasoning")

expect(reasoningChunks).toEqual([{ type: "reasoning", text: "primary thought" }])
})

it("should handle tool calls in streaming responses", async () => {
mockCreate.mockImplementation(async (options) => {
return {
Expand Down
78 changes: 78 additions & 0 deletions src/api/providers/__tests__/opencode-go.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,84 @@ describe("OpencodeGoHandler", () => {
}),
)
})

it("streams reasoning chunks from delta.reasoning_content", async () => {
mockCreate.mockImplementationOnce(async () => ({
[Symbol.asyncIterator]: async function* () {
yield { choices: [{ delta: { reasoning_content: "thinking..." }, index: 0 }] }
yield { choices: [{ delta: { content: "answer" }, index: 0 }] }
yield {
choices: [{ delta: {}, index: 0 }],
usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 },
}
},
}))

const handler = new OpencodeGoHandler(mockOptions)
const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hi" }]

const chunks: any[] = []
for await (const chunk of handler.createMessage("sys", messages)) {
chunks.push(chunk)
}

expect(chunks).toContainEqual({ type: "reasoning", text: "thinking..." })
})

it("falls back to delta.reasoning when reasoning_content is absent", async () => {
mockCreate.mockImplementationOnce(async () => ({
[Symbol.asyncIterator]: async function* () {
yield { choices: [{ delta: { reasoning: "router-style thought" }, index: 0 }] }
yield {
choices: [{ delta: {}, index: 0 }],
usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 },
}
},
}))

const handler = new OpencodeGoHandler(mockOptions)
const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hi" }]

const chunks: any[] = []
for await (const chunk of handler.createMessage("sys", messages)) {
chunks.push(chunk)
}

expect(chunks).toContainEqual({ type: "reasoning", text: "router-style thought" })
})

it("prefers delta.reasoning_content over delta.reasoning when both are present", async () => {
mockCreate.mockImplementationOnce(async () => ({
[Symbol.asyncIterator]: async function* () {
yield {
choices: [
{
delta: {
reasoning_content: "primary thought",
reasoning: "fallback thought",
},
index: 0,
},
],
}
yield {
choices: [{ delta: {}, index: 0 }],
usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 },
}
},
}))

const handler = new OpencodeGoHandler(mockOptions)
const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hi" }]

const chunks: any[] = []
for await (const chunk of handler.createMessage("sys", messages)) {
chunks.push(chunk)
}

const reasoningChunks = chunks.filter((chunk) => chunk.type === "reasoning")
expect(reasoningChunks).toEqual([{ type: "reasoning", text: "primary thought" }])
})
})

describe("completePrompt", () => {
Expand Down
Loading
Loading