mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-07 08:26:51 +00:00
fix: support reasoning field in OpenAiHandler for OpenAI-compatible providers
This commit is contained in:
parent
dc243e4cf9
commit
198af230f5
2 changed files with 198 additions and 4 deletions
|
|
@ -391,6 +391,190 @@ describe("OpenAiHandler", () => {
|
|||
expect(callArgs.reasoning_effort).toBeUndefined()
|
||||
})
|
||||
|
||||
it("should yield reasoning chunks from streaming response with reasoning_content field", async () => {
|
||||
mockCreate.mockImplementationOnce(async () => ({
|
||||
[Symbol.asyncIterator]: async function* () {
|
||||
yield {
|
||||
choices: [{ delta: { reasoning_content: "Step 1: Think about it" }, index: 0 }],
|
||||
usage: null,
|
||||
}
|
||||
yield {
|
||||
choices: [{ delta: { content: "Final answer" }, index: 0 }],
|
||||
usage: null,
|
||||
}
|
||||
yield {
|
||||
choices: [{ delta: {}, index: 0 }],
|
||||
usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 },
|
||||
}
|
||||
},
|
||||
}))
|
||||
|
||||
const stream = handler.createMessage(systemPrompt, messages)
|
||||
const chunks = []
|
||||
for await (const chunk of stream) {
|
||||
chunks.push(chunk)
|
||||
}
|
||||
|
||||
const reasoningChunks = chunks.filter((c) => c.type === "reasoning")
|
||||
expect(reasoningChunks).toHaveLength(1)
|
||||
expect(reasoningChunks[0]).toEqual({ type: "reasoning", text: "Step 1: Think about it" })
|
||||
|
||||
const textChunks = chunks.filter((c) => c.type === "text")
|
||||
expect(textChunks.some((c) => c.text === "Final answer")).toBe(true)
|
||||
})
|
||||
|
||||
it("should yield reasoning chunks from streaming response with reasoning field", async () => {
|
||||
mockCreate.mockImplementationOnce(async () => ({
|
||||
[Symbol.asyncIterator]: async function* () {
|
||||
yield {
|
||||
choices: [{ delta: { reasoning: "Step 1: Think about it via reasoning field" }, index: 0 }],
|
||||
usage: null,
|
||||
}
|
||||
yield {
|
||||
choices: [{ delta: { content: "Final answer" }, index: 0 }],
|
||||
usage: null,
|
||||
}
|
||||
yield {
|
||||
choices: [{ delta: {}, index: 0 }],
|
||||
usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 },
|
||||
}
|
||||
},
|
||||
}))
|
||||
|
||||
const stream = handler.createMessage(systemPrompt, messages)
|
||||
const chunks = []
|
||||
for await (const chunk of stream) {
|
||||
chunks.push(chunk)
|
||||
}
|
||||
|
||||
const reasoningChunks = chunks.filter((c) => c.type === "reasoning")
|
||||
expect(reasoningChunks).toHaveLength(1)
|
||||
expect(reasoningChunks[0]).toEqual({
|
||||
type: "reasoning",
|
||||
text: "Step 1: Think about it via reasoning field",
|
||||
})
|
||||
})
|
||||
|
||||
it("should prefer reasoning_content over reasoning when both are present in streaming delta", async () => {
|
||||
mockCreate.mockImplementationOnce(async () => ({
|
||||
[Symbol.asyncIterator]: async function* () {
|
||||
yield {
|
||||
choices: [
|
||||
{
|
||||
delta: { reasoning_content: "From reasoning_content", reasoning: "From reasoning" },
|
||||
index: 0,
|
||||
},
|
||||
],
|
||||
usage: null,
|
||||
}
|
||||
yield {
|
||||
choices: [{ delta: {}, index: 0 }],
|
||||
usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 },
|
||||
}
|
||||
},
|
||||
}))
|
||||
|
||||
const stream = handler.createMessage(systemPrompt, messages)
|
||||
const chunks = []
|
||||
for await (const chunk of stream) {
|
||||
chunks.push(chunk)
|
||||
}
|
||||
|
||||
const reasoningChunks = chunks.filter((c) => c.type === "reasoning")
|
||||
expect(reasoningChunks).toHaveLength(1)
|
||||
expect(reasoningChunks[0]).toEqual({ type: "reasoning", text: "From reasoning_content" })
|
||||
})
|
||||
|
||||
it("should not yield reasoning chunk for empty or whitespace-only reasoning in streaming", async () => {
|
||||
mockCreate.mockImplementationOnce(async () => ({
|
||||
[Symbol.asyncIterator]: async function* () {
|
||||
yield {
|
||||
choices: [{ delta: { reasoning_content: " " }, index: 0 }],
|
||||
usage: null,
|
||||
}
|
||||
yield {
|
||||
choices: [{ delta: { content: "Answer" }, index: 0 }],
|
||||
usage: null,
|
||||
}
|
||||
yield {
|
||||
choices: [{ delta: {}, index: 0 }],
|
||||
usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 },
|
||||
}
|
||||
},
|
||||
}))
|
||||
|
||||
const stream = handler.createMessage(systemPrompt, messages)
|
||||
const chunks = []
|
||||
for await (const chunk of stream) {
|
||||
chunks.push(chunk)
|
||||
}
|
||||
|
||||
const reasoningChunks = chunks.filter((c) => c.type === "reasoning")
|
||||
expect(reasoningChunks).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should yield reasoning from non-streaming response with reasoning_content field", async () => {
|
||||
mockCreate.mockImplementationOnce(async () => ({
|
||||
choices: [
|
||||
{
|
||||
message: {
|
||||
role: "assistant",
|
||||
content: "Final answer",
|
||||
reasoning_content: "Non-streaming reasoning",
|
||||
},
|
||||
finish_reason: "stop",
|
||||
index: 0,
|
||||
},
|
||||
],
|
||||
usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 },
|
||||
}))
|
||||
|
||||
const nonStreamHandler = new OpenAiHandler({ ...mockOptions, openAiStreamingEnabled: false })
|
||||
const stream = nonStreamHandler.createMessage(systemPrompt, messages)
|
||||
const chunks = []
|
||||
for await (const chunk of stream) {
|
||||
chunks.push(chunk)
|
||||
}
|
||||
|
||||
const reasoningChunks = chunks.filter((c) => c.type === "reasoning")
|
||||
expect(reasoningChunks).toHaveLength(1)
|
||||
expect(reasoningChunks[0]).toEqual({ type: "reasoning", text: "Non-streaming reasoning" })
|
||||
|
||||
const textChunks = chunks.filter((c) => c.type === "text")
|
||||
expect(textChunks.some((c) => c.text === "Final answer")).toBe(true)
|
||||
})
|
||||
|
||||
it("should yield reasoning from non-streaming response with reasoning field", async () => {
|
||||
mockCreate.mockImplementationOnce(async () => ({
|
||||
choices: [
|
||||
{
|
||||
message: {
|
||||
role: "assistant",
|
||||
content: "Final answer",
|
||||
reasoning: "Non-streaming reasoning via reasoning field",
|
||||
},
|
||||
finish_reason: "stop",
|
||||
index: 0,
|
||||
},
|
||||
],
|
||||
usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 },
|
||||
}))
|
||||
|
||||
const nonStreamHandler = new OpenAiHandler({ ...mockOptions, openAiStreamingEnabled: false })
|
||||
const stream = nonStreamHandler.createMessage(systemPrompt, messages)
|
||||
const chunks = []
|
||||
for await (const chunk of stream) {
|
||||
chunks.push(chunk)
|
||||
}
|
||||
|
||||
const reasoningChunks = chunks.filter((c) => c.type === "reasoning")
|
||||
expect(reasoningChunks).toHaveLength(1)
|
||||
expect(reasoningChunks[0]).toEqual({
|
||||
type: "reasoning",
|
||||
text: "Non-streaming reasoning via reasoning field",
|
||||
})
|
||||
})
|
||||
|
||||
it("should include max_tokens when includeMaxTokens is true", async () => {
|
||||
const optionsWithMaxTokens: ApiHandlerOptions = {
|
||||
...mockOptions,
|
||||
|
|
|
|||
|
|
@ -199,10 +199,13 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
|
|||
}
|
||||
}
|
||||
|
||||
if ("reasoning_content" in delta && delta.reasoning_content) {
|
||||
yield {
|
||||
type: "reasoning",
|
||||
text: (delta.reasoning_content as string | undefined) || "",
|
||||
for (const key of ["reasoning_content", "reasoning"] as const) {
|
||||
if (key in delta) {
|
||||
const reasoning = ((delta as any)[key] as string | undefined) || ""
|
||||
if (reasoning?.trim()) {
|
||||
yield { type: "reasoning", text: reasoning }
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -260,6 +263,13 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
|
|||
}
|
||||
}
|
||||
|
||||
for (const key of ["reasoning_content", "reasoning"] as const) {
|
||||
if (message && key in message && (message as any)[key]) {
|
||||
yield { type: "reasoning", text: (message as any)[key] as string }
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
yield {
|
||||
type: "text",
|
||||
text: message?.content || "",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue