OpenAI to Claude to Gemini: how kRouter translates formats
Every AI provider speaks its own JSON dialect. How kRouter converts OpenAI, Claude, Gemini, Responses and Kiro requests live, with before/after JSON.
You configure your IDE to send OpenAI-format requests. Your fallback provider speaks Claude format. Nothing matches. Tool calls explode. System prompts get rejected.
This is the format translation problem. kRouter handles it on every request: your tool sends whatever it speaks, and the provider receives whatever it expects.
The formats kRouter speaks
kRouter's translator knows eleven wire formats. Tools usually send one of the first four; providers can use any of them.
| Format | Who speaks it |
|---|---|
| OpenAI Chat Completions | Most clients and every OpenAI-compatible provider. kRouter's hub format |
| OpenAI Responses | Codex as a client; GitHub Copilot, Codex and grok-cli as providers |
| Anthropic Messages | Claude Code as a client; Anthropic and other Claude-format providers |
Gemini generateContent | The Gemini API, and Gemini-format clients on /api/v1beta |
| Gemini CLI | Gemini's Cloud Code envelope |
| Antigravity | Google's Cloud Code backend, with its own envelope |
| Vertex AI | Google Cloud Vertex |
| Kiro | AWS's CodeWhisperer streaming API |
| Cursor | Cursor's own API |
| Ollama | Local Ollama, plus an Ollama-style /v1/api/chat endpoint |
| Command Code | Command Code |
Your tool picks one. Your provider speaks another. kRouter translates between them on every request, and translates the reply back.
How a request travels
kRouter does not keep a translator for every pair. OpenAI Chat Completions is the hub: a request is converted from the client's format into OpenAI shape, then from OpenAI shape into the provider's. The reply makes the same trip in reverse, chunk by chunk as it streams.
Where the double hop loses something, there is a direct route instead. Since v0.5.117, a Claude Code request bound for Kiro goes straight from Anthropic Messages to Kiro's format, without passing through OpenAI shape in between.
What actually gets translated
Roles: OpenAI uses system | user | assistant | tool. Claude uses user | assistant, with system as a top-level field and tool results as tool_result blocks inside a user turn. Gemini uses user | model, with a separate systemInstruction. kRouter maps these in both directions.
Tool calls: OpenAI tools live in tool_calls arrays. Claude tools live in content blocks with type: "tool_use". Gemini tools live in functionCall parts, and their results go back as functionResponse parts. Each has different ID rules, argument formats and result-passing rules. kRouter rewrites them all.
Reasoning: Claude returns thinking blocks. OpenAI-style clients ask for reasoning with reasoning_effort. Gemini marks reasoning text with thought: true. kRouter turns Claude thinking and Gemini thoughts into reasoning_content on the OpenAI side, and reports Gemini's thought tokens as reasoning_tokens in the usage.
Image content: OpenAI uses image_url objects. Claude uses source blocks with base64. Gemini uses inlineData. Same image, three encodings. kRouter converts between them.
Streaming: OpenAI sends data: chunks with deltas. Claude sends typed events (message_start, content_block_delta, message_stop). Gemini streams response objects. Responses streams typed events that must end in response.completed. Kiro answers in the binary AWS EventStream format. All of it is converted to whatever your tool expects as it arrives.
Required fields: Anthropic requires max_tokens; OpenAI does not. When a client leaves it out, kRouter fills in a default, raises it when tools are present so tool arguments are not cut off, and keeps it above any thinking budget.
Concrete example: OpenAI to Claude
Your IDE sends an OpenAI-format tool call:
{
"model": "cc/claude-sonnet-4-6",
"messages": [
{ "role": "system", "content": "You are a coding assistant." },
{ "role": "user", "content": "Read the file src/index.ts" }
],
"tools": [{
"type": "function",
"function": {
"name": "read_file",
"parameters": { "type": "object", "properties": { "path": { "type": "string" } } }
}
}]
}kRouter resolves cc/ to the Claude Code provider and rewrites the body to Anthropic Messages format before it reaches Claude:
{
"model": "claude-sonnet-4-6",
"max_tokens": 64000,
"system": [{ "type": "text", "text": "You are a coding assistant." }],
"messages": [
{ "role": "user", "content": "Read the file src/index.ts" }
],
"tools": [{
"name": "read_file",
"description": "",
"input_schema": { "type": "object", "properties": { "path": { "type": "string" } } }
}]
}This is trimmed for readability. The real request also carries stream and prompt-cache markers, and the max_tokens value is kRouter's default, not something the client sent.
The system message moves to a top-level field. The tools array drops the function wrapper and parameters becomes input_schema. The provider prefix in the model name is gone, because Anthropic only knows claude-sonnet-4-6.
Tool IDs get the same treatment. Anthropic only accepts IDs matching ^[a-zA-Z0-9_-]+$, and other providers emit IDs with dots, colons or slashes. kRouter strips the illegal characters and generates a fresh ID if nothing is left. Since v0.5.63 this also runs on the direct Claude path, which is what lets a conversation that started on Gemini continue on Claude instead of failing with a 400.
The response goes through the reverse transformation before it reaches your IDE.
Concrete example: Gemini to OpenAI
When the answer comes from a Gemini model, kRouter translates Gemini's functionCall shape back into OpenAI's tool_calls shape:
// Gemini response (what the provider returns)
{
"candidates": [{
"content": {
"parts": [{
"functionCall": { "name": "read_file", "args": { "path": "src/index.ts" } }
}],
"role": "model"
}
}]
}
// OpenAI response (what your IDE sees)
{
"choices": [{
"message": {
"role": "assistant",
"tool_calls": [{
"id": "read_file-1759651200000-0",
"type": "function",
"function": { "name": "read_file", "arguments": "{\"path\":\"src/index.ts\"}" }
}]
}
}]
}Gemini's args is a native object, while OpenAI's arguments is a JSON string, so kRouter serializes it. It also builds an ID from the function name, a timestamp and the call's index, because the OpenAI side needs one to match the tool result to the call on the next turn.
Kiro's AWS format
Kiro talks to AWS's CodeWhisperer streaming API (generateAssistantResponse) and replies in binary AWS EventStream frames, which kRouter decodes into a normal stream. Three things need care:
- No system slot. Kiro's request has nowhere to put a system prompt, so the translator folds system text into the user message content. Since v0.5.65 that includes the prompts kRouter's own Caveman and Ponytail savers add.
- Tool ID sanitization. Kiro serves Claude models, and those enforce Anthropic's ID pattern. Since v0.5.74 kRouter cleans every
toolUseIdit writes, so a tool call that started on Gemini or OpenAI does not come back as a validation error. - MITM traffic. Kiro IDE requests captured through MITM go through the same translator before they reach Kiro's API. v0.5.74 fixed a path that skipped it and sent OpenAI-shaped bodies to AWS, which rejected them.
How thinking translates
- Claude to an OpenAI client: thinking text arrives as
reasoning_contentdeltas, the field many OpenAI-compatible clients already show as reasoning. A client that ignores the field simply does not show it. - An OpenAI client to Claude:
reasoning_effortbecomes a thinking budget.lowis 4,096 tokens,medium8,192,high16,384 andxhigh32,768.nonesends no thinking at all. - Any client to Antigravity: kRouter strips fields Antigravity's backend does not use, and Claude- and OpenAI-style thinking settings are among them. Since v0.5.57 it first converts them to Gemini's own
thinkingConfig, so a request that asked for reasoning still gets it.
The Responses API, the newest shape
The Responses API is where most of the recent fixes landed, because it differs from Chat Completions in more than field names. Replies arrive as typed events, system and developer instructions are separate inputs, and a stream is only finished when response.completed arrives.
v0.5.163 fixed four problems for every provider reached through it (GitHub Copilot, Codex and grok-cli):
- A reply cut off by the token limit now ends with a finish reason and its usage, instead of a stream that never properly closed.
- A failed reply now comes back as a real error (429 for rate limits), not as a successful reply whose text is the error.
- Every system and developer message reaches the model, in order. Before, only the first system message did, which mattered for Codex.
- A non-streaming request to
/v1/responsesnow gets a proper Responses object instead of a chat reply.
The same release added a per-model Protocol control for OpenCode Go (Auto, Chat Completions, Messages or Responses), because seven of its models only answer on the Responses API. The OpenCode Go protocol guide covers that, and Responses API vs Chat Completions explains the two formats side by side.
Why this matters for fallbacks
Without format translation, a fallback chain breaks the moment it switches providers mid-conversation. The history is encoded in the first provider's format, and the second provider rejects it.
Chat-style clients send the whole conversation with every request, so on each turn kRouter rebuilds that history in whatever shape the next provider needs. When a combo falls through from Claude (Anthropic format) to GLM (OpenAI format), GLM receives a valid conversation, tool IDs included. The IDE never knows the swap happened.
The IDE-side benefit
Because translation runs inside kRouter, your tool only has to speak one format it already supports: OpenAI Chat Completions on /v1/chat/completions, Anthropic Messages on /v1/messages, or Responses on /v1/responses. The API reference lists every endpoint.
# Works with every provider you have connected
OPENAI_BASE_URL=http://localhost:20128/v1
OPENAI_API_KEY=<your-krouter-key>From the machine kRouter runs on, no key is needed unless you set REQUIRE_API_KEY=true; from anywhere else, create one on the dashboard's Endpoint page. To move from GPT-5.5 to Claude to Gemini, you change only the model name: cx/gpt-5.5, cc/claude-sonnet-4-6, gemini/gemini-2.5-pro. The architecture page shows where translation sits among the other steps of a request.
npm install -g @sifxprime/krouter
krouter -tCommon questions
Which endpoint should my tool use?
Whichever format it already speaks. Claude Code uses /v1/messages, Codex uses /v1/responses, and most other tools use /v1/chat/completions. kRouter converts from any of them to any provider, so there is no need to make a tool speak a format it was not built for.
Does translation lose anything?
Sometimes, when one side has a feature the other lacks. OpenAI's JSON mode, for example, has no Claude equivalent, so kRouter turns response_format into a system instruction that asks for JSON. Features both sides share, such as tools, images, streaming and reasoning, are mapped field by field.
Does it add a network hop?
No. Translation is a JSON rewrite inside the kRouter process, so it adds no network call of its own.
Why did my tool call fail with an invalid tool ID?
Usually because a conversation moved between providers with different ID rules, for example from Gemini to Claude, through a path that did not clean the IDs. kRouter sanitizes IDs on the translated paths and, since v0.5.63, on the direct Claude path too. If you still see it, update kRouter.
Related
Published by Kodelyth, the team that builds kRouter. Posts are drafted with AI assistance and reviewed by a person before they go out. kRouter is free and MIT licensed.
Install kRouterRelated posts
- How kRouter worksClaude Cowork with other models: third-party inference setupClaude Desktop can run Cowork through any Messages API gateway. Setting it up with kRouter, what the app assumes about your models, and what you give up.
- How kRouter worksOpenCode Go in Claude Code and Codex: three protocolsOpenCode Go serves its models on three APIs. Which model needs which, why the wrong one fails with ModelProtocolUnsupported, and how to fix it.
- How kRouter worksXcode with any model: chat, Claude Agent and CodexXcode 26 and 27 can run chat, Claude Agent and Codex on other models, but each reads its own config. How to point all three at one local router.
Relevant docs
- Core conceptsProviders, combos, account routing, token savers, MITM mode, quota tracking, the response cache and remote access: the eight ideas behind kRouter.
- The Zenith Routing EngineHow kRouter picks which of your accounts serves a request: Zenith scoring, conversation stickiness, cooldowns, and the round-robin, P2C and random options.
- Token savers: RTK, Caveman, Ponytail, Headroom, PXPIPEFive ways kRouter can shrink a request before it reaches the provider. Only RTK is on by default. What each saver does, what it needs, and its limits.