How it works Pricing Docs Book a demo Login
Documentation

Rouva Docs

Everything you need to connect your app to Rouva — from a quick start to the full API reference.

Updated on October 21, 2026


Quick start

Connect your app to Rouva in three steps. No infrastructure changes required.

  1. Create an account
    Sign up at app.rouva.io and connect one or more AI provider API keys in Settings.
  2. Generate your Rouva gateway key
    Go to Integrations in your dashboard and click Generate API key. Your key starts with rva_.
  3. Route your first request
    Use the SDK or call the gateway directly. Rouva handles the rest.

SDK

The official Node.js SDK is the fastest way to integrate. One client, any connected provider — drop-in compatible with the OpenAI SDK.

Install

npm install @rouvanpm/rouva

Usage

import { Rouva } from '@rouvanpm/rouva'

const rouva = new Rouva({ apiKey: 'rva_your_key' })

const response = await rouva.chat.completions.create({
  messages: [{ role: 'user', content: 'Your prompt here' }],
})
Omit model to let Rouva route automatically to the cheapest capable model. Pass a model to use it as a cost ceiling — Rouva will route cheaper when possible, and savings are tracked against it.

Already using OpenAI or Anthropic?

Change two lines. Rouva uses the same message format — your existing code works as-is.

// Before (OpenAI)
import OpenAI from 'openai'
const openai = new OpenAI({ apiKey: '...' })
const res = await openai.chat.completions.create({ messages, model: 'gpt-6-sol' })

// Before (Anthropic)
import Anthropic from '@anthropic-ai/sdk'
const anthropic = new Anthropic({ apiKey: '...' })
const res = await anthropic.messages.create({ messages, model: 'claude-sonnet-4-6', max_tokens: 1024 })

// After — one client, any provider, intelligently routed
import { Rouva } from '@rouvanpm/rouva'
const rouva = new Rouva({ apiKey: 'rva_...' })
const res = await rouva.chat.completions.create({ messages, model: 'gpt-6-sol' }) // model optional

Options

Option Type Required Description
apiKey string Required Your rva_ gateway key from the dashboard
baseURL string Optional Override the default gateway URL

Framework quickstarts

Any client that speaks OpenAI's Chat Completions works with Rouva: point its base URL at https://app.rouva.io/v1 and use your rva_ key as the API key. On /v1 the model you name is used exactly (routing overrides still apply), and every response says what ran in its response headers. Each example was tested with the framework release current in October 2026.

Vercel AI SDK

Use the OpenAI-compatible provider: it calls Chat Completions, which is what Rouva serves.

npm install ai @ai-sdk/openai-compatible
import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
import { generateText, streamText } from "ai";

const rouva = createOpenAICompatible({
  name: "rouva",
  baseURL: "https://app.rouva.io/v1",
  apiKey: process.env.ROUVA_API_KEY,
});

const { text } = await generateText({
  model: rouva("gpt-6-luna"),
  prompt: "Where is the Eiffel Tower?",
});

const result = streamText({
  model: rouva("gpt-6-luna"),
  prompt: "Where is the Eiffel Tower?",
});
for await (const chunk of result.textStream) process.stdout.write(chunk);

OpenAI SDK (Node)

withResponse() gives you the routing headers alongside the parsed completion.

npm install openai
import OpenAI from "openai";

const client = new OpenAI({
  baseURL: "https://app.rouva.io/v1",
  apiKey: process.env.ROUVA_API_KEY,
});

const { data: completion, response } = await client.chat.completions
  .create({
    model: "gpt-6-luna",
    messages: [{ role: "user", content: "Where is the Eiffel Tower?" }],
  })
  .withResponse();

console.log(completion.choices[0].message.content);
console.log(response.headers.get("x-rouva-model"), response.headers.get("x-rouva-request-id"));

OpenAI SDK (Python)

with_raw_response gives you the routing headers alongside the parsed completion.

pip install openai
import os
from openai import OpenAI

client = OpenAI(
    base_url="https://app.rouva.io/v1",
    api_key=os.environ["ROUVA_API_KEY"],
)

raw = client.chat.completions.with_raw_response.create(
    model="gpt-6-luna",
    messages=[{"role": "user", "content": "Where is the Eiffel Tower?"}],
)
completion = raw.parse()
print(completion.choices[0].message.content)
print(raw.headers.get("x-rouva-model"), raw.headers.get("x-rouva-request-id"))

LangChain (JS)

npm install @langchain/openai
import { ChatOpenAI } from "@langchain/openai";

const llm = new ChatOpenAI({
  model: "gpt-6-luna",
  apiKey: process.env.ROUVA_API_KEY,
  configuration: { baseURL: "https://app.rouva.io/v1" },
});

const reply = await llm.invoke("Where is the Eiffel Tower?");
console.log(reply.content);

for await (const chunk of await llm.stream("Where is the Eiffel Tower?")) process.stdout.write(String(chunk.content));

LangChain (Python)

pip install langchain-openai
import os
from langchain_openai import ChatOpenAI

llm = ChatOpenAI(
    model="gpt-6-luna",
    base_url="https://app.rouva.io/v1",
    api_key=os.environ["ROUVA_API_KEY"],
)

print(llm.invoke("Where is the Eiffel Tower?").content)

for chunk in llm.stream("Where is the Eiffel Tower?"):
    print(chunk.content, end="")

LlamaIndex

is_chat_model=True sends Chat Completions requests.

pip install llama-index-llms-openai-like
import os
from llama_index.llms.openai_like import OpenAILike

llm = OpenAILike(
    model="gpt-6-luna",
    api_base="https://app.rouva.io/v1",
    api_key=os.environ["ROUVA_API_KEY"],
    is_chat_model=True,
)

print(llm.complete("Where is the Eiffel Tower?"))

LiteLLM

The openai/ prefix tells LiteLLM to speak the OpenAI format to Rouva's base URL.

pip install litellm
import os
import litellm

response = litellm.completion(
    model="openai/gpt-6-luna",
    api_base="https://app.rouva.io/v1",
    api_key=os.environ["ROUVA_API_KEY"],
    messages=[{"role": "user", "content": "Where is the Eiffel Tower?"}],
)
print(response.choices[0].message.content)

API reference

Every endpoint requires Authorization: Bearer rva_your_key; the POST endpoints also take Content-Type: application/json.

POST /api/gateway/messages Native gateway — intelligent routing, streaming

Routes your prompt to the cheapest capable model across your connected providers. Streams by default. Omit model to let Rouva auto-route, or pin a model as a cost ceiling.

▸ Request parameters
FieldTypeRequiredDescription
messagesarrayRequiredArray of { role, content } objects. Roles: system, user, assistant, tool. Content must be a string except on tools requests.
modelstringOptionalPin a model. Omit to auto-route. For non-tools requests, Rouva routes cheaper when possible and tracks savings against it. For tools requests, the named model is honored exactly on this endpoint, except when a same-provider routing override applies — in that case the override model is used instead. Workspace members are additionally subject to the administrator’s model allowlist: if the requested model is disallowed, the cheapest permitted model is substituted. Honored exactly for all requests when Intelligent Routing is off.
providerstringOptionalanthropic, openai, gemini, mistral, deepseek, xai, zai, alibaba, moonshot. Without model, Rouva routes within this provider (its default model when Intelligent Routing is off; an account routing override still applies) — 402 if you haven't added its key; with Intelligent Routing on, 400 if none of the models Rouva routes to on it fits the request (name a model instead). With model, the model must belong to this provider (400 otherwise).
systemstring | arrayOptionalSystem prompt — plain string or Anthropic-style text blocks.
max_tokensnumberOptionalDefaults to 4096. max_completion_tokens accepted as alias.
temperaturenumberOptional0–1. Omitted for reasoning models and for Claude models that reject it (Opus 4.7 and newer, Sonnet 5 and newer, Fable).
streambooleanOptionalDefaults to true (SSE). Set false for buffered JSON.
toolsarrayOptionalTool definitions in your provider's format, forwarded verbatim. Requires model.
tool_choicestring | objectOptionalForwarded verbatim. Only valid with tools.
top_pnumberOptionalNucleus sampling. top_p: 1 treated as omitted. Not supported on OpenAI reasoning models. Omitted for the same Claude models as temperature.
stopstring | string[]OptionalUp to 4 stop sequences. Not supported on OpenAI or xAI reasoning models (e.g. grok-4.7): a request pinned to one gets a 400, and Intelligent Routing skips them when stop is set.
seedintegerOptionalOpenAI models only — pins routing to OpenAI.
response_formatobjectOptionalJSON mode ({ type: "json_object" }) or a JSON schema ({ type: "json_schema", json_schema: { name, schema, strict } }), in OpenAI's format. Routing only picks models that support it — see Structured output. Not accepted on /run.
reasoning_effortstringOptionalnone, minimal, low, medium, high, xhigh or max. Routing only picks models that take the value — see Reasoning effort. Not accepted on /run.
parallel_tool_callsbooleanOptionalWith tools only. false: at most one tool call per turn. Forwarded to OpenAI-format providers; for Claude, Rouva sets disable_parallel_tool_use on tool_choice. Not accepted on /run.
metadataobjectOptionalFlat string map stored with the request. Up to 10 keys.
Unsupported options are rejected with an explicit 400, never silently ignored: logit_bias, logprobs / top_logprobs, n ≠ 1, non-zero penalties, and the legacy functions/function_call fields. logit_bias takes token ids that belong to one model's tokenizer, and of the models Rouva routes to only OpenAI's retiring GPT-4 models accept it. logprobs comes back from only a few of them (most OpenAI reasoning models refuse it), so a routed request couldn't count on it.

Response format

Responses use the wire format of the provider Rouva routed to — either OpenAI-format or Anthropic-format. Parse the format that matches the provider, or use the SDK which normalises both.

OpenAI-format stream (SSE)

data: {"id":"chatcmpl-...","choices":[{"delta":{"content":"Hello"},"index":0}]}
data: {"id":"chatcmpl-...","choices":[{"delta":{},"finish_reason":"stop","index":0}]}
data: [DONE]

Anthropic-format stream (SSE)

data: {"type":"content_block_delta","delta":{"type":"text_delta","text":"Hello"}}
data: {"type":"message_delta","delta":{"stop_reason":"end_turn"}}
data: {"type":"message_stop"}

Buffered JSON (stream: false) — OpenAI format

{"id":"chatcmpl-...","choices":[{"message":{"role":"assistant","content":"Hello"},"finish_reason":"stop","index":0}],"usage":{"prompt_tokens":10,"completion_tokens":5,"total_tokens":15}}
POST /v1/chat/completions OpenAI-compatible alias

Drop-in replacement for the OpenAI API. Point any OpenAI SDK or compatible client at https://app.rouva.io/v1 with your rva_ key — no other changes needed.

import OpenAI from "openai";

const client = new OpenAI({
  baseURL: "https://app.rouva.io/v1",
  apiKey: process.env.ROUVA_API_KEY,
});

const completion = await client.chat.completions.create({
  model: "gpt-6-luna",
  messages: [{ role: "user", content: "Hello" }],
});
▸ Differences from /api/gateway/messages
  • model is required
  • provider field is not accepted — provider is inferred from the model
  • stream defaults to false (OpenAI convention)
  • Restricted to OpenAI-format providers (OpenAI, Gemini, DeepSeek, Mistral, Moonshot, xAI, Z.ai, Alibaba Qwen) unless a Routing Override routes to Anthropic — for non-tools requests only, Rouva translates the response transparently. Tools requests always stay within the same provider (tool schemas are provider-specific). When Intelligent Routing is on, the named model acts as a cost ceiling — Rouva may route to a cheaper capable model within the same provider. The named model is honored exactly only when Intelligent Routing is off. When a same-provider override applies, the override model is used instead of the named model
  • Anthropic-format tool definitions (input_schema) are rejected — use OpenAI format
  • stream_options only accepts omitted, {}, or { include_usage: true } — any other value (e.g. { include_usage: false } or extra keys) returns 400
POST /api/gateway/run Agentic loop — server-managed tool dispatch

Hand the entire agent loop to Rouva. Provide tool definitions and handler URLs; the gateway calls the model, dispatches tool calls in parallel to your endpoints, appends results, and loops until the model stops or a limit is reached.

const result = await rouva.run({
  model: 'claude-haiku-4-5-20251001',
  messages: [{ role: 'user', content: 'Weather in Paris and London?' }],
  tools: [{ name: 'get_weather', description: 'Get weather for a city',
    input_schema: { type: 'object', properties: { city: { type: 'string' } }, required: ['city'] } }],
  tool_handlers: { get_weather: 'https://api.example.com/weather' },
  max_turns: 5,
  session_budget_usd: 0.10,
})
▸ Request parameters
Parameter Type Description
messages requiredarrayConversation seed — usually a single user message
tools requiredarrayTool definitions — OpenAI or Anthropic format
tool_handlers requiredobjectTool name → public HTTPS handler URL. Receives POST { name, arguments }
model requiredstringModel to use
providerstringForce a specific provider. Required when the model ID is not in the registry and has no known provider prefix.
max_turnsnumberMax loop iterations (default: 10)
max_tokensnumberMax tokens per turn (default: 4096)
temperaturenumberSampling temperature 0–1
session_budget_usdnumberPost-turn stopping threshold — the loop exits after any turn that causes cumulative spend to reach this value; the triggering turn is still billed in full, so spend can exceed the limit by one turn’s cost

Response

{
  "content": "The weather in Paris is 18°C and partly cloudy.",
  "turns": 2,
  "tool_calls_made": 1,
  "session_id": "rva-sess-abc123xyz",
  "finish_reason": "stop",
  "session_cost_usd": 0.000041
}

Response headers

Responses from /api/gateway/messages and /v1/chat/completions say what Rouva decided, so you can log it or assert on it without opening the dashboard. They're on successful responses, on provider errors, and on Rouva's own refusals once a model is chosen: a spend cap, a deadline, or a chosen model that can't take the request (for example a response_format it doesn't serve, or stop on a reasoning model). On a refusal, X-Rouva-Model is the model Rouva refused to send to. Errors returned before a model is chosen carry none of them: a malformed body, a missing provider key, and every refusal from a workspace member's model allowlist (the model it names is one the member can't use).

HeaderWhenValue
X-Rouva-Request-IdOnce the request is recordedThe request's id in your request log. Use it with GET /api/gateway/requests/{id} below.
X-Rouva-ModelOnce a model is chosenThe model that served the request (after a fallback, the fallback model).
X-Rouva-ProviderOnce a model is chosenIts provider, e.g. openai, anthropic.
X-Rouva-Requested-ModelWhen it differsThe model you named, when Rouva served a different one.
X-Rouva-Task-TypeOnce a model is chosen (empty when not classified, and on replays)The classified task type, e.g. code, simple_qa.
X-Rouva-Route-ReasonOnce a model is chosenWhy this model: intelligent, override, allowlist, fallback, no_key_fallback, direct or escalation; dedup on a replay of an identical request.
X-Rouva-Cost-UsdNon-streamingWhat the request cost, in USD to six decimals. Left out when the provider reported no usage or the model has no price.
X-Rouva-Saved-UsdNon-streamingSaved against the model you named, or against the priciest connected model for the task when you named none. Negative when the route cost more.

A stream's cost is known only after its last chunk, after the headers are sent. Read it back with the request id. A replay of an identical request (X-Rouva-Dedup: 1) costs nothing: it carries X-Rouva-Cost-Usd: 0.000000, X-Rouva-Route-Reason: dedup and the model that served the original, and has no request id or requested model.

GET /api/gateway/requests/8f3c2a1e-4b5d-4c6e-9f70-1a2b3c4d5e6f
Authorization: Bearer rva_your_key

{
  "id": "8f3c2a1e-4b5d-4c6e-9f70-1a2b3c4d5e6f",
  "status": "scored",
  "provider": "openai",
  "model": "gpt-6-luna",
  "requested_model": null,
  "task_type": "simple_qa",
  "route_reason": "intelligent",
  "input_tokens": 12,
  "output_tokens": 48,
  "cost_usd": 0.000025,
  "saved_usd": 0.000310,
  "quality_score": 0.92,
  "judge_status": "scored",
  "finish_reason": "stop",
  "session_id": null,
  "created_at": "2026-10-21T10:00:00Z",
  "dashboard_url": "https://app.rouva.io/dashboard?request=8f3c2a1e-…#request-log"
}

status is pending until the request settles, then settled (tokens and cost are final), then scored once a quality score is recorded (quality_score, 0–1). That's usually Rouva's judge; when the judge didn't run, Rouva's own checks can still score an answer they flag (a refusal or a repetition loop), and judge_status says which. Tools requests aren't scored, so their quality_score stays null. A request whose full charge isn't known is incomplete: a stream cut short (you disconnected, or the provider stopped mid-answer) records what the provider reported before it stopped, a long conversation Rouva summarized for you may include a summary call whose usage wasn't reported, and a model Rouva has no price for (a fine-tune, a new snapshot) is recorded at $0. The provider may have billed more than the recorded cost. Non-streaming responses leave out the cost headers in the same cases. A key reads only its own requests: another account's id is a 404. dashboard_url opens the request in your request log.

In the request log, Copy as curl on an expanded row copies the request as a curl command for POST /api/gateway/messages: the model you named, its session header, and your key as $ROUVA_API_KEY. Rouva doesn't store prompts, so the messages are a placeholder to replace; a comment shows how to pin the model that served it.

Explain a routing decision

POST /api/gateway/explain takes the same body and key as POST /api/gateway/messages and returns what Rouva would do with it, without calling a provider, recording a request or reserving spend: the model and provider it would pick, why (route_reason), how the prompt was classified, and the most the call could cost (max_cost_usd: the input as sent plus max_tokens of output, the same worst case a spend-capped key reserves). Add ?endpoint=v1 to explain the same body sent to /v1/chat/completions.

POST /api/gateway/explain
Authorization: Bearer rva_your_key

{ "model": "gpt-6-sol", "messages": [{ "role": "user", "content": "Summarize this contract…" }] }

{
  "dry_run": true,
  "would_send": true,
  "endpoint": "/api/gateway/messages",
  "provider": "openai",
  "model": "gpt-6-luna",
  "requested_model": "gpt-6-sol",
  "task_type": "summarize",
  "route_reason": "intelligent",
  "classified_by": "jev",
  "classifier_confidence": 0.94,
  "max_tokens": 4096,
  "price_per_1m": { "input": 0.1, "output": 0.5 },
  "max_cost_usd": 0.002061,
  "summarize_history": false,
  "spend_capped": false
}

When the request would be refused, the answer says so instead: "would_send": false, the status and error the real request would get, and the model it would have gone to when one had been chosen. The prompt is classified exactly as for a real request (cached for 24 hours). Explain calls have their own rate limit, the same per minute as the gateway's (429 with X-Rouva-Refusal: rate beyond it). Not covered: the duplicate-request cache, a spend cap's remaining budget, and what the provider answers (errors and the fallback chain).

Agent tools

The gateway passes tool conversations through without translation: tools and tool_choice, OpenAI-style assistant tool_calls and role: "tool" messages, and Anthropic-style tool_use/tool_result content blocks are forwarded to the chosen model in the format you sent, with their content and whitespace unchanged. For Claude, each message is rebuilt from the fields Anthropic accepts (unrecognized message fields are dropped); for OpenAI-format providers, messages are passed through as sent, so an unrecognized field reaches the provider and may be rejected there. In addition, system messages are moved to Anthropic's top-level system parameter when routing to Claude, and requests to listed Claude models may get prompt-caching markers (see Request handling). Write your tools in the schema of the provider that owns your pinned model.

GPT-5.6 and GPT-6.1 Sol: OpenAI serves their tool calling only through its Responses API, and GPT-6 Luna / Sol's tool calling with reasoning_effort other than none too. For those requests Rouva calls the Responses API for you (with store: false, so OpenAI keeps no conversation state) and converts both ways: you send and receive Chat Completions — tool_calls, role: "tool" results, streaming chunks and usage — as with any OpenAI model. The Responses API has no seed or stop and needs max_tokens of at least 16, so those requests get a 400. Not on /api/gateway/run, which still refuses these models.

Stop reason caveat: When tool_choice forces a specific function on an OpenAI-format provider, the response may return finish_reason: "stop" even though tool_calls are present. Detect tool invocations from the response content, not the stop reason — for OpenAI-format providers check for tool_calls in the deltas (or on message when stream: false); for Anthropic check for tool_use content blocks.

Structured output

Send response_format in OpenAI's format on POST /api/gateway/messages or /v1/chat/completions. { type: "json_object" } asks for any valid JSON and needs the word "json" somewhere in your messages or system prompt (400 otherwise — several providers require it). { type: "json_schema", json_schema: { name, schema, strict: true } } asks for JSON that matches your schema. For Claude, Rouva sends the schema as Anthropic's output_config.format; the JSON arrives as the answer text.

{
  "messages": [{ "role": "user", "content": "Where is the Eiffel Tower?" }],
  "response_format": {
    "type": "json_schema",
    "json_schema": {
      "name": "place",
      "strict": true,
      "schema": {
        "type": "object",
        "properties": { "city": { "type": "string" }, "country": { "type": "string" } },
        "required": ["city", "country"],
        "additionalProperties": false
      }
    }
  }
}
Providerjson_objectjson_schema
OpenAIYesYes
AnthropicNo — 400Yes, within Anthropic's schema rules
Google Gemini—Yes
DeepSeekYes—
Mistral, Moonshot, xAIYesYes
Z.aiYes (not GLM-4.7-Flash)—
AlibabaYesQwen3.7 / Qwen3.8 models (not Omni)

Intelligent Routing, routing overrides, the fallback chain and member allowlists only pick a model marked Yes. A Claude model is picked for a schema only when every object sets additionalProperties: false and the schema avoids keywords Anthropic rejects (such as minimum, maxLength and maxItems). When none of your connected models qualifies, the request gets a 400 — unless you named a model, which is then used as named. Claude also isn't picked when the conversation ends with an assistant message (a prefill), which Anthropic doesn't allow with a schema. A model you name yourself is sent the request as written, and the provider reports anything it doesn't support — except json_object or a prefill on a Claude model, which get Rouva's 400.

Reasoning effort

Send reasoning_effort in OpenAI's format on POST /api/gateway/messages or /v1/chat/completions. OpenAI and DeepSeek models get it as is; for Claude, Rouva sends it as Anthropic's output_config.effort. Which values a model takes depends on the model (checked against each provider on October 21–22, 2026):

ModelsValues
GPT-5, GPT-5 Mini, GPT-5 Nanominimal, low, medium, high
GPT-5.4 Mini, GPT-5.6 Luna / Terra / Sol, GPT-6 Luna / Solnone, low, medium, high, xhigh
GPT-6.1 Sollow, medium, high, xhigh
Claude Sonnet 4.6, Opus 4.6low, medium, high, max
Claude Sonnet 5 and newer, Opus 4.7 and newer, Fablelow, medium, high, xhigh, max
DeepSeek V4 Pronone, minimal, low, medium, high, xhigh, max
DeepSeek Flash Thinkingminimal, low, medium, high, xhigh, max
DeepSeek Flash (thinking off)none
GPT-4o, GPT-4.1 families, Claude Haiku 4.5None

Intelligent Routing, routing overrides, the fallback chain and member allowlists only pick a model from this table that takes your value; when none of your connected models does, the request gets a 400 — unless you named a model, which is then used as named. A model above that doesn't take the value gets Rouva's 400 listing the values it does take. Models of other providers are sent the value as written, and the provider reports anything it doesn't support.

On GPT-6 Luna and Sol, none turns reasoning off: temperature, top_p and function tools then work. Any other value turns it on: top_p then gets a 400, and function tools are served through OpenAI's Responses API (see Agent tools). Without reasoning_effort, Rouva turns reasoning off for you when a request needs it (tools, top_p, or a small max_tokens).

Session tracking

Tag all turns of an agent run with a shared session ID. Pass x-rouva-session-id as a request header, or use rouva.startSession() in the SDK — the ID is stored on each snapshot and visible in the request log. Call rouva.endSession() when the run is complete so subsequent requests are not grouped into the same session.

POST /api/gateway/messages
Authorization: Bearer rva_your_key
x-rouva-session-id: my-agent-run-001
Content-Type: application/json

Cross-provider harness

The agentic /run endpoint includes a canonical translation layer — a provider-neutral representation of messages, tool calls, and tool results. With Intelligent Routing enabled, the gateway selects a model across your connected Anthropic and OpenAI-compatible providers once before the loop starts, then uses that provider for all turns. Automatic routing always picks a model at or below the pinned model's cost; Routing Overrides are applied as policy and are not price-constrained. You can write tools in either OpenAI or Anthropic format — the gateway detects the dialect by shape and re-serializes to whatever the selected model expects.

Cross-dialect routing is conditional on the session being translatable. The restrictions apply to cross-provider candidate selection — the automatic within-provider fallback (substituting a cheaper model on the same provider) is unaffected. Routing Overrides targeting the same provider are also subject to the dialect gate: an override whose target model uses the opposite dialect may be skipped when the session contains opaque content for that dialect. Each set of conditions blocks cross-provider selection targeting a particular dialect while leaving the other eligible:

  • Blocks routing to OpenAI-compatible providers (Anthropic targets remain eligible): Anthropic images, document blocks, thinking blocks, or cache_control on messages, text blocks, tool schemas, or tool results; is_error on tool results; documents or images nested in tool result content; thought_signature or other Anthropic-specific fields on tool calls.
  • Blocks routing to Anthropic (OpenAI-compatible targets remain eligible): developer-role messages; untranslatable OpenAI content parts (file, audio blocks — image_url is translated automatically only in tool-result content; in user or assistant messages it is opaque); reasoning_content, audio, or refusal fields; participant name; strict or other OpenAI-specific fields on tool schemas (top-level or inside function); malformed or non-object tool-call arguments; untranslatable tool-result content parts or tool-result metadata extensions.
  • Never crosses dialect groups: routing between two OpenAI-compatible providers (e.g. OpenAI → DeepSeek, Gemini → Alibaba) is not performed — vendor-specific extensions would not transfer safely across those providers.

Tool schema formats

Both tool schema formats are accepted in the tools array:

OpenAI format (also accepted by /run)
// OpenAI tool format
{
  "type": "function",
  "function": {
    "name": "get_weather",
    "description": "Get weather for a city",
    "parameters": {
      "type": "object",
      "properties": { "city": { "type": "string" } },
      "required": ["city"]
    }
  }
}

// Anthropic tool format — also accepted
{
  "name": "get_weather",
  "description": "Get weather for a city",
  "input_schema": {
    "type": "object",
    "properties": { "city": { "type": "string" } },
    "required": ["city"]
  }
}

What the canonical layer does

On every /run turn, the gateway:

  1. Detects each tool's format by shape — OpenAI (type:"function" or a function key) or Anthropic (input_schema) — regardless of which provider the model is on.
  2. Normalizes all messages into a provider-neutral canonical form, preserving tool call IDs, arguments, names, and result content verbatim.
  3. Selects the target provider once before the loop starts — via Intelligent Routing across all your connected providers, or your pinned model. That provider is used for all turns.
  4. Re-serializes the full conversation history for the target provider's wire format — OpenAI tool_calls/role:"tool" or Anthropic tool_use/tool_result blocks — automatically.
  5. Forwards to the upstream model and merges the response back into the session history.

System messages are hoisted to Anthropic's top-level system parameter when routing to Claude, and inlined as role:"system" for OpenAI — no client changes required.

Same-provider verbatim replay

Tool schemas whose format matches the target provider are replayed verbatim — preserving vendor extensions like strict (OpenAI) or cache_control (Anthropic) exactly as you defined them. Cross-format schemas are normalized through the canonical layer.

Tip: Cross-provider routing requires Intelligent Routing to be enabled. The selected provider is fixed for the entire session — all turns use the same provider. Untranslatable content blocks routing to the incompatible target dialect (see the direction-specific restrictions above) while leaving the other dialect eligible.


Request handling

Besides routing, the gateway does a few things to your requests that can affect the response you get back or the calls made with your provider keys. How request data is stored and shared is covered in the Privacy Policy; who pays for what is covered in the Terms of Service.

Duplicate requests

On /api/gateway/messages and /v1/chat/completions, if an identical request from the same account (same messages, parameters, routing settings and x-rouva-session-id) arrives within 60 seconds after a previous one has completed — typically a client retry — Rouva returns the earlier response instead of calling the provider again, so that retry isn't charged. Retries sent while the original request is still in progress, or in the moment before its response is cached, are not deduplicated: they reach the provider and are charged normally. Deduplicated responses carry the X-Rouva-Dedup: 1 header, plus X-Rouva-Model and X-Rouva-Provider. Streaming tools requests and /api/gateway/run are never deduplicated. To force a fresh response, change something that affects the output: the message content (for non-tools requests, whitespace-only changes don't count because whitespace is normalized first; for tools requests they do), the model, or a generation parameter such as temperature, max_tokens, top_p, stop or seed. Changing fields such as metadata does not. Switching stream depends on direction: retrying a non-streaming request with stream: true can still be served from the cache, but retrying a streamed request with stream: false always reaches the provider (and is charged), because a streamed response can't be returned as JSON.

Long-conversation summarization

On /api/gateway/messages and /v1/chat/completions, when a non-tools request is routed to a Claude model other than Claude Haiku and its older turns are long, Rouva condenses those older turns into a short summary before forwarding. The most recent turns are never summarized (they still get the whitespace trimming and prompt-caching markers described below). The summary is generated by an extra request to Claude Haiku using your Anthropic API key (for workspace members, the owner's key), charged to that Anthropic account, and reused for 24 hours for the same conversation prefix, which usually lowers the total cost of long conversations. Tools requests and /api/gateway/run are never summarized.

Anthropic prompt caching

When a request is routed to one of the Claude models listed under Supported models, Rouva adds Anthropic prompt-caching markers (cache_control, 5-minute TTL) in two ways. The system prompt and the last tool definition are marked whenever they are present, whatever the request's length, as long as the request has room left in Anthropic's limit of 4 cache markers per request (markers you set count toward it). The conversation prefix is marked only in multi-turn conversations long enough to benefit (the threshold depends on the model). No markers are added when your system prompt or message content already carries longer-lived markers, and a system block or tool you have already marked is left as is. To set your own markers on a request without tools, send the system prompt as a top-level system array of text blocks with cache_control (message content must be plain strings unless the request includes tools). Markers you set count toward the limit of 4. If the request is routed to a provider other than Anthropic, the blocks are joined into plain text and the markers are dropped. When caching takes effect, Anthropic bills writing to the cache at a higher rate than normal input and reading from it at a much lower rate, so repeated prompts usually cost less overall, but a long prompt sent only once can cost slightly more. Anthropic only caches prompts above a minimum length, so markers on short prompts typically have no billing effect. This applies to tools requests too, and the charges appear on the Anthropic account whose key is used. Rouva also marks claude-sonnet-4-5-20250929, which isn't listed. What counts is the model the request is finally routed to (after any routing override), not the one you named: if that model is any other Claude model ID (for example a new release not yet listed), the request is sent without any markers Rouva adds.

Whitespace trimming

For non-tools requests, leading and trailing whitespace is trimmed and runs of blank lines are collapsed before forwarding. Tools requests are not trimmed.


Task classification

Before routing, Rouva classifies requests into one of six task types: Summarize, Q&A, Code, Research, Creative and Complex. The task type decides which routing override applies, and for most requests which models Intelligent Routing chooses from. (Tools requests on /v1 are routed among the pinned model's provider instead, and a duplicate request answered from the cache isn't classified.)

How a request is classified

Requests are classified by Jev, a classification model from TypeSafe. When Jev is unavailable or less than 60% certain, Rouva compares the prompt with examples of each task type (using OpenAI embeddings) instead. If that is inconclusive too, Rouva uses Jev's less certain answer, and a keyword rule only when neither is available.

A request with no earlier assistant turns reuses the result for the same prompt from the last 24 hours (10 minutes when a classifier was unavailable and a fallback answered); conversations with earlier turns are classified fresh each time. A request whose latest message is blank and that has no system or developer prompt is never sent for classification. With either, that prompt is classified.

Jev receives the system prompt and the latest message (up to 32,000 characters) and returns only a task type and a confidence score. TypeSafe doesn't use this data to train models and has zero data retention for Rouva's account. See the Privacy Policy.

In the dashboard

Classified requests in the dashboard log show their task type and what classified it, and those classified by Jev also show its certainty, for example 93% certain.


Routing overrides

Routing overrides let you pin specific models to specific task types at the account level — as an enforced policy, not a hint. When an override is active for a task type, Rouva ignores the model the caller requested and routes to your override model instead, regardless of which endpoint or SDK is used.

How to configure

In your dashboard, go to Settings → Model overrides. For each task type (Summarize, Q&A, Code, Research, Creative and Complex) you can select any model from your connected providers. Save the override — it takes effect immediately on all subsequent requests.

How it works

Every non-tools request is classified by task type (see Task classification) before routing. If an override exists for the detected task type and Intelligent Routing is enabled, the override model is selected — even if the caller explicitly requested a different model. The dashboard request log marks these rows with an ⊕ override badge so you can see when policy applied.

Policy wins over the caller. If your app requests gpt-5 and your Creative override is set to claude-sonnet-5, a creative prompt routes to Claude Sonnet 5. The caller's model becomes a no-op for that task type.

Caveats

  • Intelligent Routing must be enabled. Overrides are inactive when IR is turned off — the caller's model is used as-is.
  • Sampling compatibility. Overrides are skipped when the request includes parameters that would cause an upstream error — a seed with a non-OpenAI override, or top_p/stop with an OpenAI reasoning model. temperature on a reasoning-model override is silently omitted rather than a skip condition.
  • Tools overrides stay within the same provider. Tool schemas are provider-specific, so an override only applies when it targets the same provider as the pinned model — for example, a gpt-5 tools request can override to gpt-5-mini (both OpenAI), but not to claude-sonnet-5 (Anthropic schemas differ). Cross-provider overrides are silently skipped and the pinned model is used.
  • Cross-provider on /v1 is translated. When a /v1 request routes to an Anthropic override, Claude's answer comes back in OpenAI's format, streaming or not: text, finish reason and usage (text only — tools requests never cross providers).

Team controls

Rouva supports multi-member workspaces. As the workspace owner you can invite team members, set per-member spend caps, and restrict which models each member can access.

Inviting members

Go to the Team page in your dashboard and enter a colleague's email address. An invite link is sent — invite links expire after 7 days. Once the invitee accepts, they are added as a workspace member and can route requests through your connected provider keys. If the invitee already has an rva_… key, it becomes workspace-aware automatically — no action needed. If they don't yet have a key, they must go to the Gateway page in their dashboard and generate one after accepting.

Spend caps

Click Edit next to any member to open their settings panel. Enter a monthly USD cap in the Spend cap field and click Save cap. Once a member's month-to-date spend reaches the cap, all subsequent gateway requests from their key are rejected with 429 Too Many Requests and a Retry-After header set to the number of seconds until the cap resets at the next UTC month start. The cap resets automatically on the 1st of each month (UTC), or the workspace owner can raise it at any time. When Rouva refuses a request itself for a spend cap, the rate limit, pricing or time — before anything is sent to the provider — the response also carries an X-Rouva-Refusal header naming why: key_cap, member_cap, rate, unpriced (the model has no usable price where one is required: a spend-capped key, or a /run with session_budget_usd) or unbounded (a capped key sent input, or chose a model, whose cost can't be bounded) or deadline (a 504 with Retry-After: a single request — not a /run — ran out of its time budget before the provider was called: about 285 seconds, or about 260 for a streamed one, which needs 30 seconds left to start; nothing was sent, so retry it as a new request. A /run that runs out of time returns a plain 504 "Run deadline exceeded" instead — earlier turns and their tool calls already ran). Errors returned by the provider never carry it (the Node SDK exposes it as RouvaError.refusal). Other Rouva errors — such as a temporary 503 when spend can't be verified — don't carry it either; read the error message.

Caps are enforced before a request is sent. Rouva reserves the request's worst-case cost, calculated conservatively: the prompt counted as one token per byte of the request JSON with images left out (well above its real token count), plus a flat 6,000 tokens per image and 1,000 tokens if any tools are present; max_tokens of output; all at the model's listed price; for Claude models, input priced at Anthropic's cache-write rate (1.25×, or 2× if your request sets 1-hour cache markers); for OpenAI models that bill cache writes (GPT-5.6 and newer), input priced at the cache-write rate (1.25×); for Qwen models with explicit caching, when the request carries cache_control markers, input priced at the model's cache-creation rate (Qwen Flash 1.26×, Qwen3.8 Flash 1.33×, Qwen Plus 1.25×, Qwen3.7 Plus 1.25×); for models whose provider charges more for long prompts (GPT-5.6, GPT-6 and GPT-6.1 above 272K tokens, Grok 4.x from 200K, Gemini 2.5 Pro above 200K, Qwen Plus, Qwen3.7 Plus and Qwen Flash above 256K), the long-context rates whenever the prompt's estimate reaches that size — for these listed models, the recorded cost of a request whose prompt reaches the tier uses those rates too; for DeepSeek models, whose price halves outside DeepSeek's peak hours (01:00–04:00 and 06:00–10:00 UTC, Monday to Friday), the peak price — the recorded cost of a request that starts off-peak is half; for reasoning models whose hidden reasoning isn't counted in max_tokens (Gemini and Alibaba Qwen), and for any model Rouva doesn't list from a provider other than OpenAI, Anthropic, Moonshot, Z.ai or xAI, an extra 32,768 output tokens for hidden reasoning; for xAI Grok models, whose reasoning has no documented limit, output up to the model's whole context window (500K or 1M tokens), so near a cap Grok requests are refused sooner; and the cost of a possible summarization call. Near your cap, a request can therefore be refused even when its actual cost would have fit. The reservation is held against the remaining cap and is replaced with the actual cost once the provider reports usage. If a request ends without reported usage — for example a stream that is cut off, a timeout or a reset connection — the provider may still have billed it, so the worst-case reservation is kept until the cap resets at the start of the next month rather than released. If the worst case doesn't fit, the request is refused with 429 and Retry-After: 10 — lower max_tokens, shorten the prompt, or wait for in-flight requests to settle. If Rouva can't check the cap (for example, a database error), the request is refused with 503 rather than sent unchecked.

Because a capped key's cost must be bounded up front, capped keys can't use inputs whose cost isn't known in advance. These return 400 with an explanation: document and file inputs (PDFs, URLs, file IDs), audio inputs, server-side tools (e.g. web search), models whose hidden reasoning Rouva can't limit (currently the Qwen3.8 Omni models, such as Qwen3.8 Omni Flash), and models with no known pricing.

Spend and caps use Rouva's pricing data and the token counts providers report, so they are close estimates — your provider's bill is the authoritative figure.

Spend alerts for members

When a member's month-to-date spend crosses 80% or 100% of their cap, Rouva automatically sends an email alert — no configuration required beyond setting the cap. Two emails go out per threshold crossing:

  • Member alert — sent to the member's account email, showing their current spend, cap, and usage percentage.
  • Owner alert — sent to the workspace owner's account email, identifying which member triggered the alert.

Alerts are deduplicated per threshold per day — each threshold (80% and 100%) fires at most once per member per 24-hour window, even if the member makes many requests. If a single gateway request takes the member past both thresholds at once, only the 100% notification is delivered — the 80% alert is skipped. If your workspace has a Slack webhook configured, the owner also receives a Slack notification for each threshold crossing.

Model restrictions

In the same Edit panel, expand providers under Model restrictions and check the models you want to allow. Saving with no boxes checked means all models are available (no restrictions). When a member requests a disallowed model, the gateway transparently reroutes to the permitted model with the lowest combined input and output price (per million tokens) within an eligible set and only returns 403 if none exists. On /api/gateway/messages, tools requests restrict the fallback to models on the same provider (tool schemas are provider-specific); non-tools requests can cross providers freely. On /api/gateway/run, cross-provider fallbacks are allowed between dialect groups (e.g. OpenAI-compatible → Anthropic) but same-dialect cross-provider routing is excluded (e.g. OpenAI → DeepSeek). Two additional direction-specific exclusions apply: Anthropic providers are excluded as fallbacks when the request contains a developer role message, OpenAI-specific tool schema extensions (e.g. strict), or other OpenAI-dialect-only content; and non-Anthropic providers are excluded when the request contains Anthropic-only metadata such as cache_control, thinking blocks, or is_error on a tool result. If the only permitted model sits in the excluded dialect, the request returns 403. Restrictions are scoped to that member and do not affect other members or your own key.

Restriction scope. Model restrictions apply after Intelligent Routing and overrides have been evaluated. If the chosen model is already on the allowlist it is used as-is. Only when the chosen model is disallowed does the gateway substitute the lowest-input-price permitted model within the eligible set. Savings are recorded separately whenever the final model is cheaper than the model the caller explicitly requested — this happens with ordinary Intelligent Routing too and is not specific to allowlist substitution.

Revoking access

Click Remove on any member row to revoke that member's workspace access. Their existing rva_… key is not deleted immediately — it continues to exist but will no longer authenticate as a workspace member. Pending invites can be cancelled from the Pending invites list before they are accepted.

Deleting your account

Delete your account from your Profile page in the dashboard. Workspace owners must remove all members first, because members' requests run on the owner's provider keys. Members can delete their own account at any time.


Budget webhooks

Besides email and Slack, Rouva can POST each budget alert to your own HTTPS endpoint as JSON, for example to page someone, post to another chat tool, or turn a key off automatically. Connect one under Alerts → Webhook in the dashboard (workspace owners only). Rouva shows the signing secret once, when you connect the endpoint or rotate the secret, so copy it then. Send test delivers a webhook.test event.

Events

TypeSent when
budget.threshold_reachedA provider's spend this month reaches your alert threshold for it (set on the Alerts page) as a percentage of that provider's budget. Workspace members' requests count, since they run on your keys. Sent at most once per provider in 24 hours.
member_cap.threshold_reachedA workspace member reaches 80% or 100% of their monthly spend cap. Sent at most once per member and threshold per day; when both are crossed, only the 100% event is sent.
webhook.testYou click Send test.
POST https://example.com/rouva-webhook
content-type: application/json
webhook-id: evt_4f1c2a9d0b8e47c3a6d51e2f90b7c8a1
webhook-timestamp: 1792641600
webhook-signature: v1,<base64 HMAC-SHA256>

{
  "id": "evt_4f1c2a9d0b8e47c3a6d51e2f90b7c8a1",
  "type": "budget.threshold_reached",
  "created_at": "2026-10-22T04:00:00.000Z",
  "data": {
    "provider": "openai",
    "provider_label": "OpenAI",
    "threshold_pct": 80,
    "spend_pct": 81.4,
    "spend_usd": 40.7,
    "budget_usd": 50,
    "since": "2026-10-01T00:00:00.000Z",
    "dashboard_url": "https://app.rouva.io/dashboard/providers"
  }
}

A member_cap.threshold_reached event's data holds member (id, email, name), threshold_pct, spend_pct, spend_usd, cap_usd, since and dashboard_url. since is when the spend is counted from: the start of the month (UTC), or later when the provider key was added or the member joined this month.

Verifying the signature

Requests are signed the Standard Webhooks way, so you can use one of its libraries: webhook-signature is v1, plus the base64 HMAC-SHA256 of webhook-id.webhook-timestamp.body, keyed with the base64-decoded part of your secret after whsec_. Check it against the raw body, before parsing it, and reject timestamps more than five minutes old.

// npm install standardwebhooks
import { Webhook } from 'standardwebhooks'

const wh = new Webhook(process.env.ROUVA_WEBHOOK_SECRET) // "whsec_…"

export async function POST(req) {
  const body = await req.text()
  let event
  try {
    // Throws on a bad signature or a timestamp more than 5 minutes off
    event = wh.verify(body, Object.fromEntries(req.headers))
  } catch {
    return new Response('invalid signature', { status: 401 })
  }
  if (event.type === 'budget.threshold_reached') {
    // event.data.provider, event.data.spend_usd, event.data.budget_usd …
  }
  return new Response(null, { status: 204 })
}

Delivery

Any 2xx answer within 5 seconds counts as delivered. Anything else (another status, a timeout, a redirect, which isn't followed) is retried while the threshold is still crossed: alert checks run after each request to the gateway, and after a failed delivery they skip your endpoint for 5 minutes (a successful Send test ends that pause). A retry of the same alert on the same day carries the same webhook-id: if your endpoint processed an event but answered too slowly, use the id to drop the repeat. The URL must be HTTPS on a public host. Rouva checks it when you save it and again before each delivery, and refuses localhost and private addresses.


OpenTelemetry export

Rouva can send a span for every request to your tracing backend over OTLP/HTTP (JSON), so model calls show up next to the rest of your traces. Set it up under Settings → OpenTelemetry export (workspace owners): the endpoint, any headers your backend needs (usually an API key), and an optional service name (default rouva). A bare origin gets OTLP's default path, /v1/traces. Header values are stored encrypted and never shown again; when you change the endpoint to another host, re-enter them (saved headers never follow the endpoint to a new host). Send test span checks the connection.

BackendEndpointHeader
Honeycombhttps://api.honeycomb.io/v1/tracesx-honeycomb-team: your-api-key
OpenTelemetry CollectorYour collector's OTLP/HTTP receiver over HTTPS, e.g. https://otel.example.com/v1/tracesWhatever your collector requires

Any backend that accepts OTLP/HTTP with JSON encoding works; a collector can forward to the ones that don't.

Joining your traces

Send a W3C traceparent header with the request and Rouva's span becomes a child of your span. Without one, each request starts its own trace (a retry on a fallback model, or every turn of a /run call, shares it). A traceparent whose sampled flag is off is respected: nothing is exported for that request. OpenTelemetry's HTTP instrumentation adds the header to outgoing requests automatically. On /run, every turn is its own span in the same trace.

What a span contains

One span per row in the request log, named chat {model} (kind CLIENT), from when Rouva received the request until the provider's answer was complete. A failed provider call has an error status and error.type. No prompt or response text is ever sent.

AttributeValue
gen_ai.operation.namechat
gen_ai.provider.name, gen_ai.systemThe provider that served the request, e.g. openai, anthropic
gen_ai.request.model, gen_ai.response.modelThe model you asked for and the model that answered
gen_ai.usage.input_tokens, gen_ai.usage.output_tokensToken counts as the provider reported them
gen_ai.response.id, gen_ai.response.finish_reasonsThe provider's response id and finish reason
rouva.request_idThe request's id (X-Rouva-Request-Id)
rouva.cost_usd, rouva.saved_usd, rouva.cost_finalWhat the request cost, what routing saved, and whether the cost is final (false when the provider's usage was incomplete)
rouva.route_reason, rouva.task_type, rouva.requested_modelWhy this model was chosen, how the prompt was classified, and the model you named
rouva.quality_score, rouva.judge_statusThe quality score (0–1) when one was computed
rouva.session_id, rouva.member.idThe session (x-rouva-session-id), and the workspace member who sent it
rouva.metadata.*Your request's metadata fields (up to 20)

Spans are sent a few seconds after the response, once the quality score is recorded. If your endpoint fails, Rouva pauses exports to it for a minute and shows the error on the settings page. Settings changes take up to a minute to apply everywhere. Workspace members' requests are exported to the owner's endpoint.


Supported models

Pass any of these model IDs in the model field to override routing. Make sure the relevant provider key is connected in your dashboard Settings.

Models not listed here still work when pinned: valid-but-unlisted IDs — new releases, dated snapshots like gpt-5-mini-2025-08-07, fine-tunes like ft:gpt-5-mini:… — are matched to their provider by naming convention and forwarded as-is. Until Rouva's registry knows their pricing, the dashboard records their token counts with zero cost.

Anthropic

Complex reasoning, coding, and long-context work.

claude-fable-5-1 claude-fable-5 claude-opus-5-5 claude-opus-5 claude-opus-4-8 claude-opus-4-7 claude-opus-4-6 claude-sonnet-5-5 claude-sonnet-5 claude-sonnet-4-6 claude-haiku-4-5-20251001

OpenAI

General-purpose, coding, and vision-heavy tasks.

gpt-6.1-sol gpt-6-sol gpt-6-luna gpt-5-nano gpt-5-mini gpt-5.4-mini gpt-5 gpt-5.6-luna gpt-5.6-terra gpt-5.6-sol

Google Gemini

Fast multimodal and long-context reasoning.

gemini-3.8-flash gemini-3.7-flash gemini-3.6-flash gemini-3.5-flash-lite gemini-2.5-flash gemini-2.5-pro

xAI Grok

Chat, coding, and knowledge work.

grok-4.7 grok-4.6 grok-4.5 grok-4.3 grok-4.20-0309-reasoning

DeepSeek

Strong coding and reasoning with efficient pricing.

deepseek-flash deepseek-flash-think deepseek-v4-pro

Mistral

Lightweight European models for deterministic workloads.

mistral-small-latest mistral-large-latest

Moonshot Kimi

Coding, research, and agent workflows.

kimi-k3.1-pro kimi-k3

Z.ai

Long-horizon agent workflows and lightweight tasks.

glm-5.3 glm-5.2 glm-4.7-flash

Alibaba Qwen

High-throughput Chinese frontier models at competitive pricing.

qwen3.8-omni-flash qwen3.8-flash qwen3.7-plus qwen-plus qwen-flash qwen-max

Ready to get started?

Create your account, connect a provider, and make your first routed request in minutes.

Get started free →