Reviewed PR details and related session.
UnderstandIdentified API differences between OpenRouter and Vercel.
UnderstandAnalyzed tool call token leakage issue in Vercel session.
UnderstandYou are given one or more GitHub PR URLs: https://github.com/badlogic/pi-mono/pull/987 pull inot a local branch please
For each PR URL, do the following in order:
packages/*/CHANGELOG.md files. Report whether an entry exists. If missing, state that a changelog entry is required before merge and that you will add it if the user decides to merge. Follow the changelog format rules in AGENTS.md. Verify:
### Breaking Changes, ### Added, ### Fixed, etc.)Fixed foo ([#123](https://github.com/badlogic/pi-mono/pull/123) by [@user](https://github.com/user))### Breaking Changes, not just ### FixedOutput format per PR: PR: Changelog:
If no issues are found, say so under Bad and Ugly.
continue
The user wants me to review a GitHub PR. Let me start by fetching the PR details.
>gh pr view 987 --repo badlogic/pi-mono --json title,body,comments,commits,files,author,state,labels,headRefName,baseRefName{"author":{"id":"MDQ6VXNlcjQ1NjI2NDM=","is_bot":false,"login":"banteg","name":""},"baseRefName":"main","body":"## Problem\r\n\r\n```\r\nError: 400 Provider returned error\r\n {\"error\":{\"message\":\"thinking is enabled but reasoning_content is missing in assistant tool call message at index 2\",\"type\":\"invalid_request_error\"}}\r\n```\r\n\r\n## Summary\r\nOpenRouter's Kimi K2.5 rejects assistant tool-call messages when thinking is enabled but `reasoning_content` is missing. This patch ensures `reasoning_content` is always present (and correctly mapped) for OpenRouter tool-call messages.\r\n\r\n## Root Cause\r\nThinking blocks from prior assistant messages were being forwarded using the `reasoning` field, while OpenRouter/Kimi expects `reasoning_content` specifically.\r\n\r\n## Changes\r\n- Treat reasoning as enabled when a thinking level is requested.\r\n- For OpenRouter, map thinking signatures `reasoning`, `reasoning_text`, and `reasoning_content` to `reasoning_content`.\r\n- When thinking is enabled and an assistant message contains tool calls, add `reasoning_content: \"\"` if missing.\r\n\r\n## Files\r\n- packages/ai/src/providers/openai-completions.ts\r\n\r\n## Validation\r\n- `npm run check` passes.\r\n- Reproduced the failing session locally and confirmed the converted assistant tool-call message now includes `reasoning_content`.\r\n","comments":[{"id":"IC_kwDOPbFNk87jAnan","author":{"login":"ben-vargas"},"authorAssociation":"CONTRIBUTOR","body":"Would be nice if this PR was made to work with vercel ai gateway as well; checking the model slug instead of limiting to opencode provider.","createdAt":"2026-01-28T02:23:33Z","includesCreatedEdit":true,"isMinimized":false,"minimizedReason":"","reactionGroups":[],"url":"https://github.com/badlogic/pi-mono/pull/987#issuecomment-3808589479","viewerDidAuthor":false},{"id":"IC_kwDOPbFNk87jEcXX","author":{"login":"yosshi001"},"authorAssociation":"NONE","body":"Please do your best","createdAt":"2026-01-28T07:51:52Z","includesCreatedEdit":false,"isMinimized":false,"minimizedReason":"","reactionGroups":[],"url":"https://github.com/badlogic/pi-mono/pull/987#issuecomment-3809592791","viewerDidAuthor":false},{"id":"IC_kwDOPbFNk87jUJZn","author":{"login":"kiranjd"},"authorAssociation":"NONE","body":"I ran into the same Kimi K2.5 issue.\r\n\r\n**The root cause:** Kimi returns `reasoning_details` with type `reasoning.text`, but pi-ai only stores `reasoning.encrypted` type (around line 284). So in multi-turn tool calls, Kimi's actual thinking gets dropped.\r\n\r\n**Your fix:** Adds an empty `reasoning_content` when missing - this stops the 400 error.\r\n\r\n**What if we instead** Stored ALL `reasoning_details` during streaming, then used them when building messages:\r\n\r\n```ts\r\n// During streaming - store everything, not just encrypted\r\nfor (const detail of reasoningDetails) {\r\n output._rawReasoningDetails.push(detail);\r\n}\r\n\r\n// In convertMessages - use stored data first\r\nconst rawDetails = msg._rawReasoningDetails;\r\nif (rawDetails?.length > 0) {\r\n assistantMsg.reasoning_details = rawDetails;\r\n}\r\n```\r\n\r\nNot sure if preserving the actual reasoning content matters for Kimi's responses vs just having the field present. Your fix is simpler and might be all that's needed.\r\n\r\n","createdAt":"2026-01-28T20:16:44Z","includesCreatedEdit":true,"isMinimized":false,"minimizedReason":"","reactionGroups":[],"url":"https://github.com/badlogic/pi-mono/pull/987#issuecomment-3813709415","viewerDidAuthor":false},{"id":"IC_kwDOPbFNk87jUqnS","author":{"login":"ben-vargas"},"authorAssociation":"CONTRIBUTOR","body":"For what it's worth, `v0.50.1` works fine for me on openrouter with `moonshotai/kimi-k2.5` -\r\n```json\r\n{\r\n \"providers\": {\r\n \"openrouter\": {\r\n \"baseUrl\": \"https://openrouter.ai/api/v1\",\r\n \"apiKey\": \"OPENROUTER_API_KEY\",\r\n \"api\": \"openai-completions\",\r\n \"models\": [\r\n {\r\n \"id\": \"moonshotai/kimi-k2.5\",\r\n \"name\": \"Kimi K2.5 (Fireworks)\",\r\n \"reasoning\": true,\r\n \"input\": [\"text\", \"image\"],\r\n \"cost\": {\"input\": 0.60, \"output\": 3, \"cacheRead\": 0, \"cacheWrite\": 0},\r\n \"contextWindow\": 262144,\r\n \"maxTokens\": 262144,\r\n \"compat\": {\r\n \"openRouterRouting\": {\r\n \"only\": [\"fireworks\"]\r\n }\r\n }\r\n }\r\n ]\r\n }\r\n}\r\n```","createdAt":"2026-01-28T20:45:24Z","includesCreatedEdit":true,"isMinimized":false,"minimizedReason":"","reactionGroups":[],"url":"https://github.com/badlogic/pi-mono/pull/987#issuecomment-3813845458","viewerDidAuthor":false},{"id":"IC_kwDOPbFNk87jYrvh","author":{"login":"badlogic"},"authorAssociation":"OWNER","body":"<img width=\"864\" height=\"1083\" alt=\"Screenshot 2026-01-29 at 02 50 18\" src=\"https://github.com/user-attachments/assets/e51d8d72-771c-4b50-883b-2357e4a760d2\" />\r\n\r\nFine is kind of relative. I have a feeling OpenRouter/Moonshot are still fixing up their shit frantically behind the scenes and we get to suffer through the beta...","createdAt":"2026-01-29T01:50:53Z","includesCreatedEdit":false,"isMinimized":false,"minimizedReason":"","reactionGroups":[],"url":"https://github.com/badlogic/pi-mono/pull/987#issuecomment-3814898657","viewerDidAuthor":true},{"id":"IC_kwDOPbFNk87jYvsB","author":{"login":"badlogic"},"authorAssociation":"OWNER","body":"On OpenRouter it just fucks itself completely https://buildwithpi.ai/session/#8091c6f4446f8387644cbd059c4b6d07\r\nOn HF (just added support), we don't get any reasoning traces at all \r\nOn OpenCode Zen, it works the best so far for me, even prompt caching works, guess they proxy to Moonshot directly. https://buildwithpi.ai/session/#4c335d08b896e86c446d6cba047a659a\r\nOn Vercel AI Gateway, it works about the same as OpenCode Zen, no prompt caching (reporting) tho https://buildwithpi.ai/session/#9c4a1269b5f8f14991fed7c863ce10db\r\n\r\nSo, OpenRouter is clown shit, HF is meh, rest is fine. Didn't try with Moonshot endpoints directly, but seeing how OpenCode Zen likely just proxies, will be fine too.\r\n\r\nIf somebody knows someone at OpenRouter, tell them their shit is broken.","createdAt":"2026-01-29T01:57:08Z","includesCreatedEdit":false,"isMinimized":false,"minimizedReason":"","reactionGroups":[],"url":"https://github.com/badlogic/pi-mono/pull/987#issuecomment-3814914817","viewerDidAuthor":true},{"id":"IC_kwDOPbFNk87jY0jJ","author":{"login":"badlogic"},"authorAssociation":"OWNER","body":"<img width=\"886\" height=\"506\" alt=\"Screenshot 2026-01-29 at 03 00 59\" src=\"https://github.com/user-attachments/assets/49526fe8-918d-48ad-b8da-6be9f382c9be\" />\r\n\r\nIt appears the clownshit goes deep. It craps itself on Vercel as well.","createdAt":"2026-01-29T02:01:44Z","includesCreatedEdit":false,"isMinimized":false,"minimizedReason":"","reactionGroups":[],"url":"https://github.com/badlogic/pi-mono/pull/987#issuecomment-3814934729","viewerDidAuthor":true}],"commits":[{"authoredDate":"2026-01-27T14:54:16Z","authors":[{"email":"[REDACTED]","id":"MDQ6VXNlcjQ1NjI2NDM=","login":"banteg","name":"banteg"}],"committedDate":"2026-01-27T14:54:16Z","messageBody":"","messageHeadline":"fix(ai): include reasoning_content on openrouter tool calls","oid":"d5a05a9e54e9ec1ae3e8bb80074daa0fb0ac02bc"},{"authoredDate":"2026-01-27T14:54:54Z","authors":[{"email":"[REDACTED]","id":"MDQ6VXNlcjQ1NjI2NDM=","login":"banteg","name":"banteg"}],"committedDate":"2026-01-27T14:54:54Z","messageBody":"","messageHeadline":"chore(ai): regenerate models list","oid":"ea898219856675b3d2aaf433833316a4ec366f47"},{"authoredDate":"2026-01-27T14:58:23Z","authors":[{"email":"[REDACTED]","id":"MDQ6VXNlcjQ1NjI2NDM=","login":"banteg","name":"banteg"}],"committedDate":"2026-01-27T14:58:23Z","messageBody":"","messageHeadline":"chore(ai): document openrouter reasoning_content fix","oid":"fbdb7f0c82ea7606bd68418b8817342a628eb934"}],"files":[{"path":"packages/ai/CHANGELOG.md","additions":4,"deletions":0},{"path":"packages/ai/src/models.generated.ts","additions":68,"deletions":17},{"path":"packages/ai/src/providers/openai-completions.ts","additions":17,"deletions":2}],"headRefName":"fix/kimi","labels":[],"state":"OPEN","title":"fix(ai): include reasoning_content for OpenRouter Kimi K2.5 tool calls"}>gh pr diff 987 --repo badlogic/pi-monodiff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md
index 35eed366f..0935896d9 100644
--- a/packages/ai/CHANGELOG.md
+++ b/packages/ai/CHANGELOG.md
@@ -2,6 +2,10 @@
## [Unreleased]
+### Fixed
+
+- Fixed OpenRouter Kimi K2.5 tool-call replay by mapping reasoning fields to `reasoning_content` and ensuring it is present when reasoning is enabled.
+
## [0.50.1] - 2026-01-26
### Fixed
diff --git a/packages/ai/src/models.generated.ts b/packages/ai/src/models.generated.ts
index 777edd2d2..92a37fdac 100644
--- a/packages/ai/src/models.generated.ts
+++ b/packages/ai/src/models.generated.ts
@@ -6066,23 +6066,6 @@ export const MODELS = {
contextWindow: 262144,
maxTokens: 65536,
} satisfies Model<"openai-completions">,
- "mistralai/devstral-2512:free": {
- id: "mistralai/devstral-2512:free",
- name: "Mistral: Devstral 2 2512 (free)",
- api: "openai-completions",
- provider: "openrouter",
- baseUrl: "https://openrouter.ai/api/v1",
- reasoning: false,
- input: ["text"],
- cost: {
- input: 0,
- output: 0,
- cacheRead: 0,
- cacheWrite: 0,
- },
- contextWindow: 262144,
- maxTokens: 4096,
- } satisfies Model<"openai-completions">,
"mistralai/devstral-medium": {
id: "mistralai/devstral-medium",
name: "Mistral: Devstral Medium",
@@ -6593,6 +6576,23 @@ export const MODELS = {
contextWindow: 262144,
maxTokens: 65535,
} satisfies Model<"openai-completions">,
+ "moonshotai/kimi-k2.5": {
+ id: "moonshotai/kimi-k2.5",
+ name: "MoonshotAI: Kimi K2.5",
+ api: "openai-completions",
+ provider: "openrouter",
+ baseUrl: "https://openrouter.ai/api/v1",
+ reasoning: true,
+ input: ["text", "image"],
+ cost: {
+ input: 0.6,
+ output: 3,
+ cacheRead: 0.09999999999999999,
+ cacheWrite: 0,
+ },
+ contextWindow: 262144,
+ maxTokens: 4096,
+ } satisfies Model<"openai-completions">,
"nex-agi/deepseek-v3.1-nex-n1": {
id: "nex-agi/deepseek-v3.1-nex-n1",
name: "Nex AGI: DeepSeek V3.1 Nex N1",
@@ -8395,6 +8395,23 @@ export const MODELS = {
contextWindow: 163840,
maxTokens: 65536,
} satisfies Model<"openai-completions">,
+ "upstage/solar-pro-3:free": {
+ id: "upstage/solar-pro-3:free",
+ name: "Upstage: Solar Pro 3 (free)",
+ api: "openai-completions",
+ provider: "openrouter",
+ baseUrl: "https://openrouter.ai/api/v1",
+ reasoning: true,
+ input: ["text"],
+ cost: {
+ input: 0,
+ output: 0,
+ cacheRead: 0,
+ cacheWrite: 0,
+ },
+ contextWindow: 128000,
+ maxTokens: 4096,
+ } satisfies Model<"openai-completions">,
"x-ai/grok-3": {
id: "x-ai/grok-3",
name: "xAI: Grok 3",
@@ -8890,6 +8907,23 @@ export const MODELS = {
contextWindow: 262144,
maxTokens: 32768,
} satisfies Model<"anthropic-messages">,
+ "alibaba/qwen3-max-thinking": {
+ id: "alibaba/qwen3-max-thinking",
+ name: "Qwen 3 Max Thinking",
+ api: "anthropic-messages",
+ provider: "vercel-ai-gateway",
+ baseUrl: "https://ai-gateway.vercel.sh",
+ reasoning: true,
+ input: ["text"],
+ cost: {
+ input: 1.2,
+ output: 6,
+ cacheRead: 0.24,
+ cacheWrite: 0,
+ },
+ contextWindow: 256000,
+ maxTokens: 256000,
+ } satisfies Model<"anthropic-messages">,
"anthropic/claude-3-haiku": {
id: "anthropic/claude-3-haiku",
name: "Claude 3 Haiku",
@@ -9774,6 +9808,23 @@ export const MODELS = {
contextWindow: 256000,
maxTokens: 16384,
} satisfies Model<"anthropic-messages">,
+ "moonshotai/kimi-k2.5": {
+ id: "moonshotai/kimi-k2.5",
+ name: "Kimi K2.5",
+ api: "anthropic-messages",
+ provider: "vercel-ai-gateway",
+ baseUrl: "https://ai-gateway.vercel.sh",
+ reasoning: true,
+ input: ["text", "image"],
+ cost: {
+ input: 0.6,
+ output: 3,
+ cacheRead: 0.09999999999999999,
+ cacheWrite: 0,
+ },
+ contextWindow: 262114,
+ maxTokens: 262114,
+ } satisfies Model<"anthropic-messages">,
"nvidia/nemotron-nano-12b-v2-vl": {
id: "nvidia/nemotron-nano-12b-v2-vl",
name: "Nvidia Nemotron Nano 12B V2 VL",
diff --git a/packages/ai/src/providers/openai-completions.ts b/packages/ai/src/providers/openai-completions.ts
index 8e80723f4..156950fc8 100644
--- a/packages/ai/src/providers/openai-completions.ts
+++ b/packages/ai/src/providers/openai-completions.ts
@@ -396,7 +396,8 @@ function createClient(
function buildParams(model: Model<"openai-completions">, context: Context, options?: OpenAICompletionsOptions) {
const compat = getCompat(model);
- const messages = convertMessages(model, context, compat);
+ const reasoningEnabled = Boolean(options?.reasoningEffort);
+ const messages = convertMessages(model, context, compat, reasoningEnabled);
maybeAddOpenRouterAnthropicCacheControl(model, messages);
const params: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = {
@@ -490,8 +491,10 @@ export function convertMessages(
model: Model<"openai-completions">,
context: Context,
compat: Required<OpenAICompletionsCompat>,
+ reasoningEnabled = false,
): ChatCompletionMessageParam[] {
const params: ChatCompletionMessageParam[] = [];
+ const isOpenRouter = model.provider === "openrouter";
const normalizeToolCallId = (id: string): string => {
if (compat.requiresMistralToolIds) return normalizeMistralToolId(id);
@@ -595,7 +598,15 @@ export function convertMessages(
// Use the signature from the first thinking block if available (for llama.cpp server + gpt-oss)
const signature = nonEmptyThinkingBlocks[0].thinkingSignature;
if (signature && signature.length > 0) {
- (assistantMsg as any)[signature] = nonEmptyThinkingBlocks.map((b) => b.thinking).join("\n");
+ const thinkingText = nonEmptyThinkingBlocks.map((b) => b.thinking).join("\n");
+ if (
+ isOpenRouter &&
+ (signature === "reasoning" || signature === "reasoning_text" || signature === "reasoning_content")
+ ) {
+ (assistantMsg as any).reasoning_content = thinkingText;
+ } else {
+ (assistantMsg as any)[signature] = thinkingText;
+ }
}
}
}
@@ -610,6 +621,10 @@ export function convertMessages(
arguments: JSON.stringify(tc.arguments),
},
}));
+
+ if (reasoningEnabled && isOpenRouter && (assistantMsg as any).reasoning_content === undefined) {
+ (assistantMsg as any).reasoning_content = "";
+ }
const reasoningDetails = toolCalls
.filter((tc) => tc.thoughtSignature)
.map((tc) => {Now let me read the current main branch versions of the files being modified to compare against the diff.
Let me read the relevant source files to understand the full context:
Let me now check if there are any linked issues and check the changelog format. The PR already has a changelog entry. Let me also check the README and docs to see if any documentation updates are needed.
Looking at the PR:
reasoning_content for OpenRouterreasoning_content when reasoning is enabled but missing for tool callsThe changelog entry in the PR diff is:
### Fixed
- Fixed OpenRouter Kimi K2.5 tool-call replay by mapping reasoning fields to `reasoning_content` and ensuring it is present when reasoning is enabled.This entry doesn't include the PR link and author attribution as required for external contributions. Let me check if any documentation or README changes are needed.
Let me check if there are any documentation updates needed and review the PR comments more carefully:
>ls /Users/badlogic/workspaces/pi-mono/packages/coding-agent/docs/compaction.md
custom-provider.md
development.md
extensions.md
images
json.md
keybindings.md
models.md
packages.md
prompt-templates.md
providers.md
rpc.md
sdk.md
session.md
settings.md
shell-aliases.md
skills.md
terminal-setup.md
themes.md
tree.md
tui.md
windows.mdLet me now pull the PR into a local branch as the user requested.
Now let me pull the PR into a local branch:
>cd /Users/badlogic/workspaces/pi-mono && git fetch origin && git checkout -b fix/kimi-pr-987 && gh pr checkout 987 --detach && git checkout -b fix/kimi-pr-987-workSwitched to a new branch 'fix/kimi-pr-987'
From https://github.com/badlogic/pi-mono
* branch refs/pull/987/head -> FETCH_HEAD
HEAD is now at fbdb7f0c chore(ai): document openrouter reasoning_content fix
Switched to a new branch 'fix/kimi-pr-987-work'I've pulled the PR into a local branch. Let me provide the structured review.
Looking at the changes:
openai-completions.ts changes:
reasoningEnabled detection from options?.reasoningEffortreasoningEnabled to convertMessagesreasoningEnabled parameter and isOpenRouter detectionreasoning_contentreasoning_content when reasoning is enabled and assistant has tool callsCHANGELOG.md: Entry exists but missing PR link and author attribution
models.generated.ts: This is auto-generated, so the changes (adding Kimi K2.5, removing some models) are fine
Let me provide the review.
PR: https://github.com/badlogic/pi-mono/pull/987
packages/ai/CHANGELOG.md under ### FixedFixed OpenRouter Kimi K2.5 tool-call replay by mapping reasoning fields to reasoning_content and ensuring it is present when reasoning is enabled.Fixed OpenRouter Kimi K2.5 tool-call replay by mapping reasoning fields to \reasoning_content` and ensuring it is present when reasoning is enabled (#987 by @banteg)`reasoning_content specifically, not reasoning or reasoning_textreasoning_content: "" when missing prevents 400 errors without changing behavior for working conversationsreasoningEnabled && isOpenRoutermodel.provider === "openrouter" won't work for Vercel AI Gateway or other proxies that route to Kimi. Checking model slug would be more robust (though this is an enhancement, not blocking)reasoning_details with type: "reasoning.text" but pi-ai only stores reasoning.encrypted type. The PR's empty string fix stops the error but doesn't preserve actual reasoning. This is a valid observation, though the current fix is simpler and sufficient for the error casereasoningEnabled boolean to convertMessages() based on options.reasoningEffortmodel.provider === "openrouter"reasoning, reasoning_text, reasoning_content) to reasoning_content for OpenRouterreasoning_content: "" to assistant tool-call messages when reasoning is enabled, OpenRouter, and the field is missingreasoning_content"npm run check passes (per PR description)The PR is now in local branch fix/kimi-pr-987-work. If you want to merge, I'll need to fix the changelog attribution first.
/Users/badlogic/.pi/agent/sessions/--Users-badlogic-workspaces-pi-mono--/2026-01-29T02-05-25-874Z_bb1f1612-2f68-4d2b-a37e-f97536a9d087.jsonl
any idea if we use @packages/ai/src/providers/openai-completions.ts for vercel? this is a session where the model eventually spits out a thinking trace that includes a tool call
The user is asking if the openai-completions.ts provider is used for Vercel AI Gateway, and they're referencing a session file. Let me check the session file and also look at how Vercel AI Gateway is configured to understand which API it uses.
>cd /Users/badlogic/workspaces/pi-mono && rg "vercel-ai-gateway" packages/ai/src --type ts -lpackages/ai/src/models.generated.ts
packages/ai/src/types.ts
packages/ai/src/env-api-keys.tsLooking at the session file, I can see the model is moonshotai/kimi-k2.5 with provider vercel-ai-gateway and API anthropic-messages.
So the answer is: No, Vercel AI Gateway with Kimi K2.5 uses anthropic-messages API, not openai-completions.
The session shows:
"api":"anthropic-messages","provider":"vercel-ai-gateway","model":"moonshotai/kimi-k2.5"Looking at the last message, the assistant's thinking block contains a malformed tool call section that leaked into the thinking output:
<|tool_calls_section_begin|> <|tool_call_begin|> functions.bash:22 <|tool_call_argument_begin|> {\"command\": \"cat packages/*/CHANGELOG.md | grep -c \\\"^### \\\" && cat packages/*/CHANGELOG.md | grep -c \\\"^## \\\\[\\\" | head -5\"} <|tool_call_end|>...This is Kimi's internal format leaking through. The thinking trace contains what should have been tool calls, but they got emitted as thinking content instead.
So the PR fix for openai-completions.ts won't help Vercel AI Gateway because Vercel uses anthropic-messages API for Kimi, not openai-completions.
Let me check how Vercel models are configured:
Looking at the session, Vercel AI Gateway with Kimi K2.5 uses anthropic-messages API, not openai-completions:
"api":"anthropic-messages","provider":"vercel-ai-gateway","model":"moonshotai/kimi-k2.5"So the PR fix won't help Vercel because it only modifies openai-completions.ts.
The last message in that session shows the problem: Kimi's internal tool call format leaked into the thinking trace:
<|tool_calls_section_begin|> <|tool_call_begin|> functions.bash:22 <|tool_call_argument_begin|> {"command": "..."} <|tool_call_end|>...This is the model outputting what should be tool calls as raw text inside the thinking block. The stop reason is "stop" (not "toolUse"), so the tool calls were never properly parsed.
Let me check how Vercel models are configured:
>cd /Users/badlogic/workspaces/pi-mono && rg "kimi-k2.5" packages/ai/src/models.generated.ts -A 15"moonshotai/kimi-k2.5": {
id: "moonshotai/kimi-k2.5",
name: "MoonshotAI: Kimi K2.5",
api: "openai-completions",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
reasoning: true,
input: ["text", "image"],
cost: {
input: 0.6,
output: 3,
cacheRead: 0.09999999999999999,
cacheWrite: 0,
},
contextWindow: 262144,
maxTokens: 4096,
} satisfies Model<"openai-completions">,
--
"moonshotai/kimi-k2.5": {
id: "moonshotai/kimi-k2.5",
name: "Kimi K2.5",
api: "anthropic-messages",
provider: "vercel-ai-gateway",
baseUrl: "https://ai-gateway.vercel.sh",
reasoning: true,
input: ["text", "image"],
cost: {
input: 0.6,
output: 3,
cacheRead: 0.09999999999999999,
cacheWrite: 0,
},
contextWindow: 262114,
maxTokens: 262114,
} satisfies Model<"anthropic-messages">,>cd /Users/badlogic/workspaces/pi-mono && rg "vercel-ai-gateway" packages/ai/src/providers/ -l(no output)
Command exited with code 1Interesting. So:
openai-completions APIanthropic-messages APIThe PR only fixes the openai-completions provider. Vercel routes through anthropic-messages, which is the Anthropic provider code.
The problem in the session is that Kimi is outputting its internal tool call format (<|tool_calls_section_begin|>) as text inside thinking blocks instead of proper tool calls. This is a model-level issue or an API format mismatch.
The PR's approach of ensuring reasoning_content is present wouldn't help Vercel since Vercel uses the Anthropic API format, not OpenAI format.
For Vercel, the issue is different: Kimi's thinking mode seems to be producing malformed output where tool calls get emitted as raw text in the thinking trace. This could be:
Let me check if there's any Kimi-specific handling in the anthropic provider.
So the picture is:
openai-completions API → PR fix applies hereanthropic-messages API → PR fix does NOT applyThe session you shared shows Kimi outputting its internal tool call format (<|tool_calls_section_begin|>) as raw text inside the thinking block instead of proper tool calls. This is a different bug that the PR doesn't address.
Let me check the Anthropic provider to see if there's anything relevant:
>cd /Users/badlogic/workspaces/pi-mono && rg "reasoning|thinking" packages/ai/src/providers/anthropic.ts -n | head -40125: thinkingEnabled?: boolean;
126: thinkingBudgetTokens?: number;
204: } else if (event.content_block.type === "thinking") {
206: type: "thinking",
207: thinking: "",
208: thinkingSignature: "",
212: stream.push({ type: "thinking_start", contentIndex: output.content.length - 1, partial: output });
240: } else if (event.delta.type === "thinking_delta") {
243: if (block && block.type === "thinking") {
244: block.thinking += event.delta.thinking;
246: type: "thinking_delta",
248: delta: event.delta.thinking,
268: if (block && block.type === "thinking") {
269: block.thinkingSignature = block.thinkingSignature || "";
270: block.thinkingSignature += event.delta.signature;
285: } else if (block.type === "thinking") {
287: type: "thinking_end",
289: content: block.thinking,
351: if (!options?.reasoning) {
352: return streamAnthropic(model, context, { ...base, thinkingEnabled: false } satisfies AnthropicOptions);
358: options.reasoning,
359: options.thinkingBudgets,
365: thinkingEnabled: true,
366: thinkingBudgetTokens: adjusted.thinkingBudget,
382: betaFeatures.push("interleaved-thinking-2025-05-14");
485: if (options?.thinkingEnabled && model.reasoning) {
486: params.thinking = {
488: budget_tokens: options.thinkingBudgetTokens || 1024,
570: } else if (block.type === "thinking") {
571: if (block.thinking.trim().length === 0) continue;
572: // If thinking signature is missing/empty (e.g., from aborted stream),
573: // convert to plain text block without <thinking> tags to avoid API rejection
575: if (!block.thinkingSignature || block.thinkingSignature.trim().length === 0) {
578: text: sanitizeSurrogates(block.thinking),
582: type: "thinking",
583: thinking: sanitizeSurrogates(block.thinking),
584: signature: block.thinkingSignature,Okay, so the Anthropic provider has proper thinking handling. But Kimi isn't Anthropic, and when Vercel routes Kimi through the anthropic-messages API format, Kimi apparently emits its internal tool call markers as raw text.
This is a Vercel/Kimi-specific problem, not something the PR addresses. The PR is specifically for OpenRouter's Kimi handling via the openai-completions API.
The session you showed me demonstrates that Kimi's thinking mode with tools doesn't work properly on Vercel AI Gateway. The model outputs <|tool_calls_section_begin|> tokens instead of proper API-formatted tool calls.
Summary for the user:
So the PR is narrowly scoped to OpenRouter, and the Vercel issue is a separate bug (possibly in Vercel's API translation layer or Kimi's compatibility with the Anthropic message format).
The Anthropic provider has proper thinking handling for Claude, but Kimi isn't Claude. When Vercel routes Kimi through anthropic-messages API format, Kimi appears to emit its internal control tokens (<|tool_calls_section_begin|>, <|tool_call_begin|>, etc.) as raw text instead of proper tool calls.
Summary:
openai-completions APIanthropic-messages API for Kimi, and the model outputs its internal tool call format as literal text in thinking tracesThe Vercel issue is likely:
This aligns with badlogic's comment on the PR: "It craps itself on Vercel as well" and "OpenRouter is clown shit, HF is meh, rest is fine". The "rest is fine" probably refers to providers that route directly to Moonshot (like OpenCode Zen), not Vercel.
Should we still merge the PR for the OpenRouter fix, or wait for broader Kimi support to stabilize?
### Fixed
- Fixed OpenRouter Kimi K2.5 tool-call replay by mapping reasoning fields to `reasoning_content` and ensuring it is present when reasoning is enabled."api":"anthropic-messages","provider":"vercel-ai-gateway","model":"moonshotai/kimi-k2.5"<|tool_calls_section_begin|> <|tool_call_begin|> functions.bash:22 <|tool_call_argument_begin|> {\"command\": \"cat packages/*/CHANGELOG.md | grep -c \\\"^### \\\" && cat packages/*/CHANGELOG.md | grep -c \\\"^## \\\\[\\\" | head -5\"} <|tool_call_end|>..."api":"anthropic-messages","provider":"vercel-ai-gateway","model":"moonshotai/kimi-k2.5"<|tool_calls_section_begin|> <|tool_call_begin|> functions.bash:22 <|tool_call_argument_begin|> {"command": "..."} <|tool_call_end|>...