Skip to main content
kRouter
All posts
How kRouter works

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.

Kodelyth · The team behind kRouter
· Updated
7 min read

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.

FormatWho speaks it
OpenAI Chat CompletionsMost clients and every OpenAI-compatible provider. kRouter's hub format
OpenAI ResponsesCodex as a client; GitHub Copilot, Codex and grok-cli as providers
Anthropic MessagesClaude Code as a client; Anthropic and other Claude-format providers
Gemini generateContentThe Gemini API, and Gemini-format clients on /api/v1beta
Gemini CLIGemini's Cloud Code envelope
AntigravityGoogle's Cloud Code backend, with its own envelope
Vertex AIGoogle Cloud Vertex
KiroAWS's CodeWhisperer streaming API
CursorCursor's own API
OllamaLocal Ollama, plus an Ollama-style /v1/api/chat endpoint
Command CodeCommand 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 toolUseId it 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_content deltas, 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_effort becomes a thinking budget. low is 4,096 tokens, medium 8,192, high 16,384 and xhigh 32,768. none sends 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/responses now 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 -t

Common 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.

Kodelyth · The team behind kRouter

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 kRouter