> This page is for Model APIs.

> Sarvam Voice Agents (Samvaad) is a voice AI agent platform for Indian languages: build, test and deploy AI calling agents on your own phone numbers, from ₹3 a minute. Start here: https://docs.sarvam.ai/voice-agents/why-voice-agents.
> For clean Markdown of any page, append `.md` to the page URL.
> For a complete documentation index, see https://docs.sarvam.ai/llms.txt. For a section index, append `/llms.txt` to any section URL (for example https://docs.sarvam.ai/voice-agents/llms.txt).
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.sarvam.ai/_mcp/server.

# Migrating Chat Completion from OpenRouter to Sarvam

> Request fields, reasoning, Responses API, and OpenRouter-only features to change when moving chat traffic from OpenRouter to Sarvam's /v2 API, with before/after code.

*Last updated: October 23, 2026*

This guide covers moving OpenRouter Chat Completions and Responses calls for open-weight
models to Sarvam's [`/v2` API](/api/token-factory/chat-completions).

> **Tip**
>
> **In short:** change `base_url`, `api_key`, and `model`. Then remove OpenRouter-only
> fields such as `provider`, `models`, `reasoning`, and `plugins`: Sarvam returns
> `400` for request fields it doesn't support instead of ignoring them. Read reasoning from
> `reasoning_content` instead of `reasoning`.

## Migration checklist

1. Create an API key in the [Sarvam dashboard](https://indus.sarvam.ai/key-management?ref=docs).
2. Change `base_url` to `https://api.sarvam.ai/v2` and `api_key` to your Sarvam key.
3. Replace each model ID using the [model ID table](/api/token-factory/migrations/from-openrouter/overview#model-ids).
4. Replace the `reasoning` object with `reasoning_effort` ([Reasoning](#reasoning)).
5. Remove OpenRouter-only fields: `provider`, `models`, `plugins`, `usage`, `session_id`, and others ([list](#openrouter-only-fields-to-remove)).
6. Read reasoning from `reasoning_content` instead of `reasoning`.
7. Replace `models` fallbacks with retries on `429` and `503` ([full example](#full-example)).
8. Set `max_tokens` explicitly, then run your tests and watch for `400` responses that name a field.

## Quick start

|             | OpenRouter                     | Sarvam                                                                                                 |
| ----------- | ------------------------------ | ------------------------------------------------------------------------------------------------------ |
| Base URL    | `https://openrouter.ai/api/v1` | `https://api.sarvam.ai/v2`                                                                             |
| Auth header | `Authorization: Bearer <key>`  | `api-subscription-key: <key>` or `Authorization: Bearer <key>`                                         |
| Model ID    | `moonshotai/kimi-k3`           | `kimi-k3` (see the [model ID table](/api/token-factory/migrations/from-openrouter/overview#model-ids)) |

#### Before (OpenRouter)

#### Python (OpenAI SDK)

```python
from openai import OpenAI

client = OpenAI(
    api_key="YOUR_OPENROUTER_API_KEY",
    base_url="https://openrouter.ai/api/v1",
)

response = client.chat.completions.create(
    model="moonshotai/kimi-k3",
    messages=[
        {"role": "system", "content": "You are a helpful assistant."},
        {"role": "user", "content": "Suggest a name for a coffee shop in Bengaluru."},
    ],
    max_tokens=2000,
    extra_body={
        "reasoning": {"effort": "low"},
        "provider": {"sort": "throughput"},
    },
)

print(response.choices[0].message.content)
```

#### JavaScript (OpenAI SDK)

```javascript
import OpenAI from "openai";

const client = new OpenAI({
  apiKey: "YOUR_OPENROUTER_API_KEY",
  baseURL: "https://openrouter.ai/api/v1",
});

const response = await client.chat.completions.create({
  model: "moonshotai/kimi-k3",
  messages: [
    { role: "system", content: "You are a helpful assistant." },
    { role: "user", content: "Suggest a name for a coffee shop in Bengaluru." },
  ],
  max_tokens: 2000,
  reasoning: { effort: "low" },
  provider: { sort: "throughput" },
});

console.log(response.choices[0].message.content);
```

#### cURL

```bash
curl https://openrouter.ai/api/v1/chat/completions \
  -H "Authorization: Bearer YOUR_OPENROUTER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "moonshotai/kimi-k3",
    "messages": [{"role": "user", "content": "Suggest a name for a coffee shop in Bengaluru."}],
    "max_tokens": 2000,
    "reasoning": {"effort": "low"},
    "provider": {"sort": "throughput"}
  }'
```

#### After (Sarvam)

#### Python (OpenAI SDK)

```python
from openai import OpenAI

client = OpenAI(
    api_key="YOUR_SARVAM_API_KEY",
    base_url="https://api.sarvam.ai/v2",
)

response = client.chat.completions.create(
    model="kimi-k3",
    messages=[
        {"role": "system", "content": "You are a helpful assistant."},
        {"role": "user", "content": "Suggest a name for a coffee shop in Bengaluru."},
    ],
    max_tokens=2000,
    reasoning_effort="low",
)

print(response.choices[0].message.content)
```

#### JavaScript (OpenAI SDK)

```javascript
import OpenAI from "openai";

const client = new OpenAI({
  apiKey: "YOUR_SARVAM_API_KEY",
  baseURL: "https://api.sarvam.ai/v2",
});

const response = await client.chat.completions.create({
  model: "kimi-k3",
  messages: [
    { role: "system", content: "You are a helpful assistant." },
    { role: "user", content: "Suggest a name for a coffee shop in Bengaluru." },
  ],
  max_tokens: 2000,
  reasoning_effort: "low",
});

console.log(response.choices[0].message.content);
```

#### Python (Sarvam SDK)

```python
from sarvamai import SarvamAI

client = SarvamAI(api_subscription_key="YOUR_SARVAM_API_KEY")

response = client.chat.completions_v2(
    model="kimi-k3",
    messages=[
        {"role": "system", "content": "You are a helpful assistant."},
        {"role": "user", "content": "Suggest a name for a coffee shop in Bengaluru."},
    ],
    max_tokens=2000,
    reasoning_effort="low",
)

print(response.choices[0].message.content)
```

#### JavaScript (Sarvam SDK)

```javascript
import { SarvamAIClient } from "sarvamai";

const client = new SarvamAIClient({ apiSubscriptionKey: "YOUR_SARVAM_API_KEY" });

const response = await client.chat.completionsV2({
  model: "kimi-k3",
  messages: [
    { role: "system", content: "You are a helpful assistant." },
    { role: "user", content: "Suggest a name for a coffee shop in Bengaluru." },
  ],
  max_tokens: 2000,
  reasoning_effort: "low",
});

console.log(response.choices[0].message.content);
```

#### cURL

```bash
curl https://api.sarvam.ai/v2/chat/completions \
  -H "api-subscription-key: YOUR_SARVAM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "kimi-k3",
    "messages": [{"role": "user", "content": "Suggest a name for a coffee shop in Bengaluru."}],
    "max_tokens": 2000,
    "reasoning_effort": "low"
  }'
```

## Reasoning

| OpenRouter                                          | Sarvam                                | Note                                                                                                                                                                             |
| --------------------------------------------------- | ------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `reasoning: {"effort": "low"}`                      | `reasoning_effort: "low"`             | Sarvam uses the top-level field only; the `reasoning` object returns `400` on chat. Use `low`, `high`, or `max`.                                                                 |
| `reasoning_effort` (top level)                      | `reasoning_effort`                    | Same field. `medium` is accepted on some models and returns `400` on others, such as `glm5.3`.                                                                                   |
| `reasoning: {"enabled": false}` or `effort: "none"` | `reasoning_effort: "none"`            | Turns thinking off on `deepseekv4-pro`, `mimo-v2.6-flash`, and `mimo-v2.6-pro`. `kimi-k3` and `glm5.3-flash` always think. See [Reasoning](/api/token-factory/models#reasoning). |
| `reasoning: {"max_tokens": N}`                      | Not supported                         | Choose an effort level instead, and size `max_tokens` to cover reasoning plus the answer.                                                                                        |
| `reasoning: {"exclude": true}`                      | Not supported                         | Ignore `reasoning_content` in your code. Reasoning tokens are billed either way.                                                                                                 |
| `include_reasoning: true`                           | Remove                                | Reasoning is always returned when the model thinks.                                                                                                                              |
| `choices[].message.reasoning`                       | `choices[].message.reasoning_content` | Also `delta.reasoning_content` when streaming.                                                                                                                                   |
| `choices[].message.reasoning_details`               | Not returned                          |                                                                                                                                                                                  |

With the `openai` Python package, read the field with `getattr`, since it isn't part of the
package's typed response:

```python
reasoning = getattr(response.choices[0].message, "reasoning_content", None)
```

## OpenRouter-only fields to remove

Sarvam validates request fields and returns `400 invalid_request_error` with
`Unsupported parameter(s)` for fields it doesn't support, so leftover OpenRouter options
fail loudly instead of being ignored.

| OpenRouter field                                                           | What to do on Sarvam                                                                                                                                                                |
| -------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `provider` (`order`, `sort`, `only`, `allow_fallbacks`, …)                 | Remove. Sarvam serves each model itself, so there's no upstream provider to pick.                                                                                                   |
| `models` (fallback list)                                                   | Remove. Retry on `429` and `503` with backoff, or fall back to a second Sarvam model in your own code (see the [full example](#full-example)).                                      |
| `reasoning` object                                                         | Replace with `reasoning_effort` (see [Reasoning](#reasoning)).                                                                                                                      |
| `include_reasoning`                                                        | Remove.                                                                                                                                                                             |
| `usage: {"include": true}`                                                 | Remove. Usage is always returned.                                                                                                                                                   |
| `plugins` (web search, file parser, response healing, context compression) | Remove. Run web search or PDF parsing yourself and pass the text in `messages`; use `response_format` with a JSON schema instead of response healing; trim long histories yourself. |
| `session_id`                                                               | Remove. For multi-turn caching on `gemma4`, send the [`x-session-affinity`](/api/token-factory/chat-completions#session-affinity-for-multi-turn-conversations) header instead.      |
| `cache_control` at the top level                                           | Remove. `cache_control` inside a message content block is accepted.                                                                                                                 |
| `:free`, `:nitro`, `:floor`, `:online` model suffixes                      | Use the plain Sarvam model ID.                                                                                                                                                      |

## Parameter concept map

| OpenRouter                                                       | Sarvam                     | Note                                                                                                                                                                                      |
| ---------------------------------------------------------------- | -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `messages`, `max_tokens`, `temperature`, `top_p`, `stop`, `seed` | Same                       |                                                                                                                                                                                           |
| `tools`, `tool_choice`, `parallel_tool_calls`                    | Same                       | `parallel_tool_calls: false` is enforced on every model; see [V2 feature support](/api/token-factory/chat-completions#v2-feature-support).                                                |
| `response_format` (`json_schema`, `json_object`)                 | Same                       |                                                                                                                                                                                           |
| `logprobs`, `top_logprobs`                                       | Same                       | Supported on every model except `glm5.3`.                                                                                                                                                 |
| `n`                                                              | `n`                        | Supported on most models; OpenRouter doesn't support it.                                                                                                                                  |
| `top_k`, `repetition_penalty`                                    | Same                       |                                                                                                                                                                                           |
| `stream: true`                                                   | Same                       | No `: OPENROUTER PROCESSING` keep-alive comments. Usage arrives in the final chunk.                                                                                                       |
| `image_url` content parts                                        | Same                       | Inline base64 data URIs only, on models with image input. See each [model page](/api/token-factory/models).                                                                               |
| `input_audio` content parts                                      | Same, on the MiMo models   | `wav` or `mp3`. For FLAC, OGG, or Opus, send an `audio_url` part with a data URI. See [Media input](/api/token-factory/migrations/from-openrouter/overview#media-input-and-request-size). |
| `video_url` content parts                                        | Same, as a base64 data URI | On the MiMo models and `kimi-k3` (beta, short clips). Sarvam doesn't fetch video URLs.                                                                                                    |
| `usage.cost`                                                     | Not returned               | Token counts are in `usage`. Check spend in the [dashboard](https://indus.sarvam.ai/billing?ref=docs).                                                                                    |
| `GET /api/v1/generation?id=`                                     | Not available              | Log the `id` and `usage` from each response yourself.                                                                                                                                     |

## Responses API

Both platforms' Responses APIs are stateless. For base URL and field changes, reasoning items, function tools, and server-side tools, see [Migrating the Responses API from OpenRouter](/api/token-factory/migrations/from-openrouter/responses-api).

## What's different by design

* **One provider, no routing layer.** Sarvam runs each model on its own infrastructure, so
  there are no upstream providers to rank, pin, or fail over between, and a model's
  behaviour doesn't change from request to request.
* **Strict request validation.** Unsupported fields return `400` with the field named,
  instead of being dropped silently. This catches stale options early. `gemma4` is less
  strict: it accepts some fields, such as `prediction` and `logit_bias`, and ignores them.
* **Reasoning in `reasoning_content`.** Sarvam uses the same field name as many open-model
  servers, rather than OpenRouter's `reasoning` and `reasoning_details`.
* **Billing in your Sarvam credits.** Usage is metered per token against your Sarvam
  account. See [Pricing](/api/getting-started/pricing).

## Full example

A wrapper that replaces OpenRouter's `models` fallback list with retries and a fallback
model in your own code:

#### Before (OpenRouter)

```python
from openai import OpenAI

client = OpenAI(
    api_key="YOUR_OPENROUTER_API_KEY",
    base_url="https://openrouter.ai/api/v1",
)

def ask(prompt: str) -> str:
    response = client.chat.completions.create(
        model="moonshotai/kimi-k3",
        messages=[{"role": "user", "content": prompt}],
        max_tokens=2000,
        extra_body={
            "models": ["moonshotai/kimi-k3", "z-ai/glm-5.3"],
            "reasoning": {"effort": "low"},
        },
    )
    return response.choices[0].message.content
```

#### After (OpenAI SDK)

```python
import time
from openai import OpenAI, APIStatusError

client = OpenAI(
    api_key="YOUR_SARVAM_API_KEY",
    base_url="https://api.sarvam.ai/v2",
)

MODELS = ["kimi-k3", "glm5.3"]  # tried in order

def ask(prompt: str) -> str:
    for model in MODELS:
        for attempt in range(3):
            try:
                response = client.chat.completions.create(
                    model=model,
                    messages=[{"role": "user", "content": prompt}],
                    max_tokens=2000,
                    reasoning_effort="low",
                )
                return response.choices[0].message.content
            except APIStatusError as e:
                if e.status_code in (429, 503):
                    time.sleep(2 ** attempt)  # back off, then retry
                    continue
                if e.status_code == 404:
                    break  # model not enabled for this key; try the next one
                raise  # 400, 402 and 403 won't succeed on retry
    raise RuntimeError("No model is available. Try again later.")
```

#### After (Sarvam SDK)

```python
import time
from sarvamai import SarvamAI
from sarvamai.core.api_error import ApiError

client = SarvamAI(api_subscription_key="YOUR_SARVAM_API_KEY")

MODELS = ["kimi-k3", "glm5.3"]  # tried in order

def ask(prompt: str) -> str:
    for model in MODELS:
        for attempt in range(3):
            try:
                response = client.chat.completions_v2(
                    model=model,
                    messages=[{"role": "user", "content": prompt}],
                    max_tokens=2000,
                    reasoning_effort="low",
                )
                return response.choices[0].message.content
            except ApiError as e:
                if e.status_code in (429, 503):
                    time.sleep(2 ** attempt)  # back off, then retry
                    continue
                if e.status_code == 404:
                    break  # model not enabled for this key; try the next one
                raise  # 400, 402 and 403 won't succeed on retry
    raise RuntimeError("No model is available. Try again later.")
```

## Common mistakes

#### Leaving OpenRouter fields in extra_body

Fields like `provider`, `models`, `reasoning`, and `plugins` are often passed through
`extra_body` in the `openai` package and are easy to miss. Sarvam returns `400` with the
field name in the error message; remove each one it names.

#### Keeping the provider prefix in the model ID

`moonshotai/kimi-k3` returns `404` on Sarvam. Use `kimi-k3`. See the
[model ID table](/api/token-factory/migrations/from-openrouter/overview#model-ids).

#### Reading message.reasoning

Sarvam returns reasoning in `reasoning_content`. Code that reads `reasoning` gets
`None`.

#### Expecting a fallback to happen automatically

Sarvam doesn't retry on another model for you. Handle `429` and `503` in your code, as in
the [full example](#full-example).

#### Setting max_tokens too low on reasoning models

Reasoning tokens count against `max_tokens`. If the model spends the whole budget
thinking, `content` is empty and `finish_reason` is `"length"`. Raise `max_tokens` or
lower `reasoning_effort`.

## See also

* [Migration overview](/api/token-factory/migrations/from-openrouter/overview): base URL, auth, and model IDs
* [Chat Completion overview](/api/token-factory/chat-completions)
* [Responses API guide](/api/token-factory/responses-api)
* [Open-Weight Models](/api/token-factory/models)
* [Errors & Troubleshooting](/api/getting-started/errors-troubleshooting)