diff --git a/src/api/providers/__tests__/openrouter.spec.ts b/src/api/providers/__tests__/openrouter.spec.ts
index f5067ef34c..5a6111738d 100644
--- a/src/api/providers/__tests__/openrouter.spec.ts
+++ b/src/api/providers/__tests__/openrouter.spec.ts
@@ -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:
+ "This is reasoning content with XML tags",
+ },
+ },
+ ],
+ }
+ 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:
+ "This is reasoning content with XML tags",
+ },
+ },
+ ],
+ }
+ 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: "This is reasoning content with XML tags",
+ })
+ })
+
it("generates correct stream chunks", async () => {
const handler = new OpenRouterHandler(mockOptions)
diff --git a/src/api/providers/openrouter.ts b/src/api/providers/openrouter.ts
index d16a410b13..871cdec3cc 100644
--- a/src/api/providers/openrouter.ts
+++ b/src/api/providers/openrouter.ts
@@ -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) {