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.
-
Create an account
Sign up at app.rouva.io and connect one or more AI provider API keys in Settings. -
Generate your Rouva gateway key
Go to Integrations in your dashboard and click Generate API key. Your key starts with rva_. -
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' }],
})
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.
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).
| Header | When | Value |
|---|---|---|
| X-Rouva-Request-Id | Once the request is recorded | The request's id in your request log. Use it with GET /api/gateway/requests/{id} below. |
| X-Rouva-Model | Once a model is chosen | The model that served the request (after a fallback, the fallback model). |
| X-Rouva-Provider | Once a model is chosen | Its provider, e.g. openai, anthropic. |
| X-Rouva-Requested-Model | When it differs | The model you named, when Rouva served a different one. |
| X-Rouva-Task-Type | Once a model is chosen (empty when not classified, and on replays) | The classified task type, e.g. code, simple_qa. |
| X-Rouva-Route-Reason | Once a model is chosen | Why this model: intelligent, override, allowlist, fallback, no_key_fallback, direct or escalation; dedup on a replay of an identical request. |
| X-Rouva-Cost-Usd | Non-streaming | What 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-Usd | Non-streaming | Saved 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
}
}
}
}
| Provider | json_object | json_schema |
|---|---|---|
| OpenAI | Yes | Yes |
| Anthropic | No — 400 | Yes, within Anthropic's schema rules |
| Google Gemini | — | Yes |
| DeepSeek | Yes | — |
| Mistral, Moonshot, xAI | Yes | Yes |
| Z.ai | Yes (not GLM-4.7-Flash) | — |
| Alibaba | Yes | Qwen3.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):
| Models | Values |
|---|---|
| GPT-5, GPT-5 Mini, GPT-5 Nano | minimal, low, medium, high |
| GPT-5.4 Mini, GPT-5.6 Luna / Terra / Sol, GPT-6 Luna / Sol | none, low, medium, high, xhigh |
| GPT-6.1 Sol | low, medium, high, xhigh |
| Claude Sonnet 4.6, Opus 4.6 | low, medium, high, max |
| Claude Sonnet 5 and newer, Opus 4.7 and newer, Fable | low, medium, high, xhigh, max |
| DeepSeek V4 Pro | none, minimal, low, medium, high, xhigh, max |
| DeepSeek Flash Thinking | minimal, low, medium, high, xhigh, max |
| DeepSeek Flash (thinking off) | none |
| GPT-4o, GPT-4.1 families, Claude Haiku 4.5 | None |
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 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:
- 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.
- Normalizes all messages into a provider-neutral canonical form, preserving tool call IDs, arguments, names, and result content verbatim.
- 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.
- 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.
- 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.
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.
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
| Type | Sent when |
|---|---|
| budget.threshold_reached | A 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_reached | A 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.test | You 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.
| Backend | Endpoint | Header |
|---|---|---|
| Honeycomb | https://api.honeycomb.io/v1/traces | x-honeycomb-team: your-api-key |
| OpenTelemetry Collector | Your collector's OTLP/HTTP receiver over HTTPS, e.g. https://otel.example.com/v1/traces | Whatever 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.
| Attribute | Value |
|---|---|
| gen_ai.operation.name | chat |
| gen_ai.provider.name, gen_ai.system | The provider that served the request, e.g. openai, anthropic |
| gen_ai.request.model, gen_ai.response.model | The model you asked for and the model that answered |
| gen_ai.usage.input_tokens, gen_ai.usage.output_tokens | Token counts as the provider reported them |
| gen_ai.response.id, gen_ai.response.finish_reasons | The provider's response id and finish reason |
| rouva.request_id | The request's id (X-Rouva-Request-Id) |
| rouva.cost_usd, rouva.saved_usd, rouva.cost_final | What 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_model | Why this model was chosen, how the prompt was classified, and the model you named |
| rouva.quality_score, rouva.judge_status | The quality score (0–1) when one was computed |
| rouva.session_id, rouva.member.id | The 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.
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 →