Skip to navigation

Migrating Chat Completion from OpenRouter to Sarvam

View as Markdown

Last updated: October 23, 2026

This guide covers moving OpenRouter Chat Completions and Responses calls for open-weight models to Sarvam’s /v2 API.

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.
  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.
  4. Replace the reasoning object with reasoning_effort (Reasoning).
  5. Remove OpenRouter-only fields: provider, models, plugins, usage, session_id, and others (list).
  6. Read reasoning from reasoning_content instead of reasoning.
  7. Replace models fallbacks with retries on 429 and 503 (full example).
  8. Set max_tokens explicitly, then run your tests and watch for 400 responses that name a field.

Quick start

OpenRouterSarvam
Base URLhttps://openrouter.ai/api/v1https://api.sarvam.ai/v2
Auth headerAuthorization: Bearer <key>api-subscription-key: <key> or Authorization: Bearer <key>
Model IDmoonshotai/kimi-k3kimi-k3 (see the model ID table)
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)

Reasoning

OpenRouterSarvamNote
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_effortSame 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.
reasoning: {"max_tokens": N}Not supportedChoose an effort level instead, and size max_tokens to cover reasoning plus the answer.
reasoning: {"exclude": true}Not supportedIgnore reasoning_content in your code. Reasoning tokens are billed either way.
include_reasoning: trueRemoveReasoning is always returned when the model thinks.
choices[].message.reasoningchoices[].message.reasoning_contentAlso delta.reasoning_content when streaming.
choices[].message.reasoning_detailsNot returned

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

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 fieldWhat 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).
reasoning objectReplace with reasoning_effort (see Reasoning).
include_reasoningRemove.
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_idRemove. For multi-turn caching on gemma4, send the x-session-affinity header instead.
cache_control at the top levelRemove. cache_control inside a message content block is accepted.
:free, :nitro, :floor, :online model suffixesUse the plain Sarvam model ID.

Parameter concept map

OpenRouterSarvamNote
messages, max_tokens, temperature, top_p, stop, seedSame
tools, tool_choice, parallel_tool_callsSameparallel_tool_calls: false is enforced on every model; see V2 feature support.
response_format (json_schema, json_object)Same
logprobs, top_logprobsSameSupported on every model except glm5.3.
nnSupported on most models; OpenRouter doesn’t support it.
top_k, repetition_penaltySame
stream: trueSameNo : OPENROUTER PROCESSING keep-alive comments. Usage arrives in the final chunk.
image_url content partsSameInline base64 data URIs only, on models with image input. See each model page.
input_audio content partsSame, on the MiMo modelswav or mp3. For FLAC, OGG, or Opus, send an audio_url part with a data URI. See Media input.
video_url content partsSame, as a base64 data URIOn the MiMo models and kimi-k3 (beta, short clips). Sarvam doesn’t fetch video URLs.
usage.costNot returnedToken counts are in usage. Check spend in the dashboard.
GET /api/v1/generation?id=Not availableLog 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.

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.

Full example

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

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

Common mistakes

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.

moonshotai/kimi-k3 returns 404 on Sarvam. Use kimi-k3. See the model ID table.

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

Sarvam doesn’t retry on another model for you. Handle 429 and 503 in your code, as in the full example.

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