From 2125221bfce8e1a6fa02f8db5ff7820890f986a8 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Tue, 2 Jun 2026 15:36:52 +0200 Subject: [PATCH] Fix OpenRouter Kimi reasoning compat closes #5309 --- packages/ai/CHANGELOG.md | 1 + packages/ai/scripts/generate-models.ts | 6 ++- packages/ai/src/models.generated.ts | 4 +- .../ai/src/providers/openai-completions.ts | 4 +- .../openai-completions-tool-choice.test.ts | 37 ++++++++++++++++++- 5 files changed, 47 insertions(+), 5 deletions(-) diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 4a616f89..e927bdca 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -10,6 +10,7 @@ - Fixed Amazon Bedrock requests to replace blank required user/tool-result text with a placeholder and skip blank replay text blocks ([#4975](https://github.com/earendil-works/pi/issues/4975)). - Fixed OpenAI GPT-5.5 generated metadata to omit unsupported minimal thinking ([#5243](https://github.com/earendil-works/pi/issues/5243)). +- Fixed OpenRouter Kimi K2.6 thinking replay and preserved developer-role instructions for OpenRouter OpenAI and Anthropic models ([#5309](https://github.com/earendil-works/pi/issues/5309)). - Fixed GitHub Copilot and OpenRouter test model references that became stale after model regeneration. ## [0.78.0] - 2026-05-29 diff --git a/packages/ai/scripts/generate-models.ts b/packages/ai/scripts/generate-models.ts index 12c926d1..b0f49571 100644 --- a/packages/ai/scripts/generate-models.ts +++ b/packages/ai/scripts/generate-models.ts @@ -1343,7 +1343,11 @@ async function generateModels() { candidate.maxTokens = 4096; } if (candidate.provider === "openrouter" && candidate.id.startsWith("moonshotai/kimi-k2.6")) { - candidate.compat = { ...candidate.compat, supportsDeveloperRole: false }; + candidate.compat = { + ...candidate.compat, + supportsDeveloperRole: false, + requiresReasoningContentOnAssistantMessages: true, + }; } if (candidate.provider === "openrouter" && candidate.id === "z-ai/glm-5") { candidate.cost.input = 0.6; diff --git a/packages/ai/src/models.generated.ts b/packages/ai/src/models.generated.ts index 1dacf48d..b3ff24a9 100644 --- a/packages/ai/src/models.generated.ts +++ b/packages/ai/src/models.generated.ts @@ -10281,7 +10281,7 @@ export const MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", - compat: {"supportsDeveloperRole":false}, + compat: {"supportsDeveloperRole":false,"requiresReasoningContentOnAssistantMessages":true}, reasoning: true, input: ["text", "image"], cost: { @@ -10299,7 +10299,7 @@ export const MODELS = { api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", - compat: {"supportsDeveloperRole":false}, + compat: {"supportsDeveloperRole":false,"requiresReasoningContentOnAssistantMessages":true}, reasoning: true, input: ["text", "image"], cost: { diff --git a/packages/ai/src/providers/openai-completions.ts b/packages/ai/src/providers/openai-completions.ts index c58cbf46..ae6d583e 100644 --- a/packages/ai/src/providers/openai-completions.ts +++ b/packages/ai/src/providers/openai-completions.ts @@ -1100,11 +1100,13 @@ function detectCompat(model: Model<"openai-completions">): ResolvedOpenAIComplet const isGrok = provider === "xai" || baseUrl.includes("api.x.ai"); const isDeepSeek = provider === "deepseek" || baseUrl.includes("deepseek.com"); + const isOpenRouterDeveloperRoleModel = + isOpenRouter && (model.id.startsWith("anthropic/") || model.id.startsWith("openai/")); const cacheControlFormat = provider === "openrouter" && model.id.startsWith("anthropic/") ? "anthropic" : undefined; return { supportsStore: !isNonStandard, - supportsDeveloperRole: !isNonStandard && !isOpenRouter, + supportsDeveloperRole: isOpenRouterDeveloperRoleModel || (!isNonStandard && !isOpenRouter), supportsReasoningEffort: !isGrok && !isZai && !isMoonshot && !isTogether && !isCloudflareAiGateway && !isNvidia, supportsUsageInStreaming: true, maxTokensField: useMaxTokens ? "max_tokens" : "max_completion_tokens", diff --git a/packages/ai/test/openai-completions-tool-choice.test.ts b/packages/ai/test/openai-completions-tool-choice.test.ts index 5f40c817..c06ac344 100644 --- a/packages/ai/test/openai-completions-tool-choice.test.ts +++ b/packages/ai/test/openai-completions-tool-choice.test.ts @@ -817,7 +817,7 @@ describe("openai-completions tool_choice", () => { expect(writeCall).not.toHaveProperty("partialArgs"); }); - it("uses system messages for OpenRouter reasoning model instructions", async () => { + it("uses system messages for non-OpenAI/Anthropic OpenRouter reasoning model instructions", async () => { const model = getModel("openrouter", "deepseek/deepseek-v4-pro")!; let payload: unknown; @@ -839,6 +839,33 @@ describe("openai-completions tool_choice", () => { expect(params.messages?.[0]?.role).toBe("system"); }); + it("keeps developer messages for OpenAI and Anthropic OpenRouter reasoning model instructions", async () => { + for (const model of [ + getModel("openrouter", "openai/gpt-5.2-codex"), + getModel("openrouter", "anthropic/claude-sonnet-4.5"), + ]) { + expect(model).toBeDefined(); + let payload: unknown; + + await streamSimple( + model!, + { + systemPrompt: "Follow instructions.", + messages: [{ role: "user", content: "Hi", timestamp: Date.now() }], + }, + { + apiKey: "test", + onPayload: (params: unknown) => { + payload = params; + }, + }, + ).result(); + + const params = payload as { messages?: Array<{ role?: string }> }; + expect(params.messages?.[0]?.role).toBe("developer"); + } + }); + it("keeps developer messages for OpenAI reasoning model instructions", async () => { const { compat: _compat, ...baseModel } = getModel("openai", "gpt-5.5")!; const model = { ...baseModel, api: "openai-completions" } as const; @@ -862,6 +889,14 @@ describe("openai-completions tool_choice", () => { expect(params.messages?.[0]?.role).toBe("developer"); }); + it("stores OpenRouter Kimi K2.6 reasoning replay compat in built-in metadata", () => { + for (const modelId of ["moonshotai/kimi-k2.6", "moonshotai/kimi-k2.6:free"] as const) { + const model = getModel("openrouter", modelId)!; + expect(model.compat?.supportsDeveloperRole).toBe(false); + expect(model.compat?.requiresReasoningContentOnAssistantMessages).toBe(true); + } + }); + it("stores Xiaomi MiMo reasoning replay compat in built-in metadata", () => { const providers = ["xiaomi", "xiaomi-token-plan-cn", "xiaomi-token-plan-ams", "xiaomi-token-plan-sgp"] as const;