fix: strip XML tags from kimi-k2-thinking model reasoning output

- Added stripXmlTags method to remove XML tags from reasoning content
- Applied XML tag stripping specifically for moonshot/kimi-k2-thinking model
- Added tests to verify XML tags are stripped for kimi model but preserved for others
- Fixes #9172 where XML tags were displayed in thinking prompts
This commit is contained in:
Roo Code 2025-11-11 18:57:22 +00:00
parent 6e6341346e
commit bfabe5da76
2 changed files with 125 additions and 6 deletions

View file

@ -130,6 +130,109 @@ describe("OpenRouterHandler", () => {
})
describe("createMessage", () => {
it("strips XML tags from reasoning content for kimi-k2-thinking model", async () => {
const handler = new OpenRouterHandler({
...mockOptions,
openRouterModelId: "moonshot/kimi-k2-thinking",
})
const mockStream = {
async *[Symbol.asyncIterator]() {
yield {
choices: [
{
delta: {
reasoning:
"<thinking>This is reasoning content with <tag>XML tags</tag></thinking>",
},
},
],
}
yield {
choices: [
{
delta: {
content: "Regular content",
},
},
],
}
yield {
usage: {
prompt_tokens: 10,
completion_tokens: 20,
},
}
},
}
const mockCreate = vitest.fn().mockResolvedValue(mockStream)
;(OpenAI as any).prototype.chat = {
completions: { create: mockCreate },
} as any
const chunks = []
const generator = handler.createMessage("test", [])
for await (const chunk of generator) {
chunks.push(chunk)
}
// Check that reasoning content has XML tags stripped
expect(chunks[0]).toEqual({
type: "reasoning",
text: "This is reasoning content with XML tags",
})
expect(chunks[1]).toEqual({
type: "text",
text: "Regular content",
})
})
it("does not strip XML tags from reasoning content for other models", async () => {
const handler = new OpenRouterHandler({
...mockOptions,
openRouterModelId: "openai/gpt-4",
})
const mockStream = {
async *[Symbol.asyncIterator]() {
yield {
choices: [
{
delta: {
reasoning:
"<thinking>This is reasoning content with <tag>XML tags</tag></thinking>",
},
},
],
}
yield {
usage: {
prompt_tokens: 10,
completion_tokens: 20,
},
}
},
}
const mockCreate = vitest.fn().mockResolvedValue(mockStream)
;(OpenAI as any).prototype.chat = {
completions: { create: mockCreate },
} as any
const chunks = []
const generator = handler.createMessage("test", [])
for await (const chunk of generator) {
chunks.push(chunk)
}
// Check that reasoning content keeps XML tags for non-kimi models
expect(chunks[0]).toEqual({
type: "reasoning",
text: "<thinking>This is reasoning content with <tag>XML tags</tag></thinking>",
})
})
it("generates correct stream chunks", async () => {
const handler = new OpenRouterHandler(mockOptions)

View file

@ -98,6 +98,15 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
this.client = new OpenAI({ baseURL, apiKey, defaultHeaders: DEFAULT_HEADERS })
}
/**
* Strip XML tags from reasoning content for models that include them
* Some models like kimi-k2-thinking include XML tags in their reasoning output
*/
private stripXmlTags(text: string): string {
// Remove XML tags but preserve the content between them
return text.replace(/<[^>]*>/g, "")
}
override async *createMessage(
systemPrompt: string,
messages: Anthropic.Messages.MessageParam[],
@ -180,14 +189,21 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
throw new Error(`OpenRouter API Error ${error?.code}: ${error?.message}`)
}
const delta = chunk.choices[0]?.delta
const delta = chunk.choices?.[0]?.delta
if ("reasoning" in delta && delta.reasoning && typeof delta.reasoning === "string") {
yield { type: "reasoning", text: delta.reasoning }
}
if (delta) {
const modelId = this.options.openRouterModelId ?? openRouterDefaultModelId
if (delta?.content) {
yield { type: "text", text: delta.content }
if ("reasoning" in delta && delta.reasoning && typeof delta.reasoning === "string") {
// Strip XML tags for kimi-k2-thinking model which includes them in reasoning output
const reasoningText =
modelId === "moonshot/kimi-k2-thinking" ? this.stripXmlTags(delta.reasoning) : delta.reasoning
yield { type: "reasoning", text: reasoningText }
}
if (delta.content) {
yield { type: "text", text: delta.content }
}
}
if (chunk.usage) {