@rouvanpm/rouva 0.4.1 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -24,7 +24,7 @@ console.log(response.choices[0].message.content)
24
24
 
25
25
  ## Provider agnostic
26
26
 
27
- Rouva works with all connected providers — Anthropic, OpenAI, Gemini, DeepSeek, Mistral, Moonshot, xAI, Z.ai and Alibaba Qwen. You can request a specific model, force a specific provider, or omit both and let Rouva route to the cheapest capable model automatically.
27
+ Rouva works with all connected providers — Anthropic, OpenAI, Gemini, DeepSeek, Mistral, Moonshot, xAI, Z.ai, Alibaba Qwen and MiniMax. You can request a specific model, force a specific provider, or omit both and let Rouva route to the cheapest capable model automatically.
28
28
 
29
29
  ```typescript
30
30
  // Request a specific model
@@ -98,14 +98,14 @@ const res = await rouva.chat.completions.create({
98
98
  })
99
99
  ```
100
100
 
101
- - **`temperature`** — sampling temperature between 0 and 1. The gateway omits it for models that only accept their default: OpenAI reasoning models (gpt-5 / gpt-6 families) and the Claude models that reject it (Opus 4.7 and newer, Sonnet 5 and newer, Fable).
101
+ - **`temperature`** — sampling temperature between 0 and 1. The gateway omits it for models that only accept their default: OpenAI reasoning models (gpt-5 / gpt-6 families) and the Claude models that reject it (Opus 4.7 and newer, Sonnet 5 and newer, Haiku 5.5, Fable), and models whose sampling is fixed (Kimi K3 / K3.1 Pro, MiniMax).
102
102
  - **`max_tokens`** / **`max_completion_tokens`** — either name works. Values below 1024 also steer auto-routing away from reasoning models, which would otherwise spend the whole budget on hidden reasoning tokens.
103
103
  - **`top_p`** — nucleus sampling; `top_p: 1` is treated as omitted. OpenAI reasoning models don't support it: routing avoids them when it's set, and pinning one returns a 400. The gateway omits it for the Claude models that reject it (as for `temperature`).
104
104
  - **`stop`** — a string or up to 4 stop sequences (Anthropic receives them as `stop_sequences`). OpenAI and xAI reasoning models (e.g. gpt-5, grok-4.5 / 4.6 / 4.7) reject it: routing avoids them when it's set, and pinning one returns a 400.
105
105
  - **`metadata`** — up to 10 string key-value pairs (keys up to 64 characters, values up to 256, no null bytes) stored with the request's usage record; not sent to the provider.
106
106
  - **`seed`** — best-effort deterministic sampling. Only OpenAI honors it, so it requires an OpenAI `model` and pins the request to that exact model (no routing, no fallbacks).
107
107
  - **`response_format`** — JSON mode or a JSON schema; see [Structured output](#structured-output).
108
- - **`reasoning_effort`** — `'none'` … `'xhigh'`, or `'max'` for Claude. Which values a model takes depends on the model; routing only picks one that takes yours; a listed OpenAI or Claude model you name that doesn't gets a 400, and other providers' models get the value as sent. On gpt-6-luna / gpt-6-sol, `'none'` turns reasoning off. See [the docs](https://rouva.io/docs#reasoning-effort).
108
+ - **`reasoning_effort`** — `'none'` … `'xhigh'`, or `'max'` for Claude. Which values a model takes depends on the model; routing only picks one that takes yours; a listed OpenAI or Claude model you name that doesn't gets a 400, and other providers' models get the value as sent. On gpt-6-luna / gpt-6-sol / gpt-6.1-terra, `'none'` turns reasoning off. See [the docs](https://rouva.io/docs#reasoning-effort).
109
109
  - **`parallel_tool_calls`** — with `tools`: `false` allows at most one tool call per turn (Claude included).
110
110
 
111
111
  Unsupported OpenAI options (`logit_bias`, `logprobs`, `n > 1`, the legacy `functions`/`function_call` fields, …) are rejected by the gateway with an explicit 400 rather than silently ignored.
@@ -130,7 +130,7 @@ Non-tools requests on `/v1` honor the named model exactly unless the workspace o
130
130
 
131
131
  ## Tool use
132
132
 
133
- Tools are forwarded to your target provider verbatim — define them in the **provider's own format** (OpenAI `{ type: "function", function: {...} }` or Anthropic `{ name, description, input_schema }`) and pin the matching `model`. Tool schemas are provider-specific, so `model` is required and the gateway returns a 400 without it. Models that don't support tools on Chat Completions (`gpt-6.1-sol`, `gpt-5.6-*`) also return a 400.
133
+ Tools are forwarded to your target provider verbatim — define them in the **provider's own format** (OpenAI `{ type: "function", function: {...} }` or Anthropic `{ name, description, input_schema }`) and pin the matching `model`. Tool schemas are provider-specific, so `model` is required and the gateway returns a 400 without it. `gpt-6.1-sol`, `gpt-6-astra` and `gpt-5.6-*` take tools only through OpenAI's Responses API: the gateway uses it for you and converts both ways, so you still send and receive Chat Completions (`seed`, `stop` and a `max_tokens` below 16 are a 400 there). `rouva.run()` still refuses these models.
134
134
 
135
135
  A tools request only ever moves to another model of the **same provider**: an account routing override for the task (when it targets the same provider), or — on the `/v1` endpoint with Intelligent Routing enabled — the cheapest capable model within that provider, using the named `model` as a cost ceiling (see [OpenAI-compatible endpoint](#openai-compatible-endpoint)). Otherwise the named model is honored exactly.
136
136
 
@@ -356,6 +356,25 @@ if (id) {
356
356
 
357
357
  `pending` means it hasn't settled yet — retry after a moment. `incomplete` means it settled from partial usage (a stream cut short), so the provider may have billed more; `settled` and `scored` costs are final. A key reads only its own requests. A duplicate-cache replay has no `request_id` (nothing new was recorded or charged).
358
358
 
359
+ ## Explain a routing decision — `rouva.explain()`
360
+
361
+ A dry run: the model Rouva would pick for a request, why, and the most it could cost — without calling a provider, recording a request or reserving spend.
362
+
363
+ ```typescript
364
+ const e = await rouva.explain({ model: 'gpt-6-sol', messages })
365
+
366
+ if (e.would_send) {
367
+ console.log(e.model, e.provider) // e.g. 'gpt-6-luna', 'openai'
368
+ console.log(e.route_reason) // 'intelligent' | 'override' | 'allowlist' | 'direct' | …
369
+ console.log(e.task_type) // e.g. 'summarize'
370
+ console.log(e.max_cost_usd) // what a spend-capped key would reserve
371
+ } else {
372
+ console.log(e.status, e.error) // the refusal the real request would get
373
+ }
374
+ ```
375
+
376
+ It takes the same params as `chat.completions.create()`; pass `{ endpoint: 'v1' }` to explain the same body sent to `/v1/chat/completions`. The prompt is classified as for a real request, and explain has its own per-minute rate limit (a `RouvaError` with `refusal: 'rate'`). Not covered: the duplicate-request cache, a spend cap's remaining budget, and what the provider answers.
377
+
359
378
  ## Structured output
360
379
 
361
380
  `response_format` asks for JSON: `{ type: 'json_object' }` (JSON mode — your prompt must mention "json") or `{ type: 'json_schema', json_schema: { name, schema, strict } }` (the answer follows your schema). Rouva only routes to models that serve the format; see [the docs](https://rouva.io/docs#structured-output) for which models do.
package/dist/index.d.mts CHANGED
@@ -50,9 +50,9 @@ interface ContentBlockMessage {
50
50
  * valid when `tools` is set on the request.
51
51
  */
52
52
  type ChatMessage = Message | AssistantToolCallMessage | ToolResultMessage | ContentBlockMessage;
53
- type RouvaProvider = 'anthropic' | 'openai' | 'gemini' | 'deepseek' | 'mistral' | 'moonshot' | 'xai' | 'zai' | 'alibaba' | (string & {});
53
+ type RouvaProvider = 'anthropic' | 'openai' | 'gemini' | 'deepseek' | 'mistral' | 'moonshot' | 'xai' | 'zai' | 'alibaba' | 'minimax' | (string & {});
54
54
  /**
55
- * Supported models across all providers (the gateway's list as of SDK 0.3.0).
55
+ * Supported models across all providers (the gateway's list as of SDK 0.5.1).
56
56
  * Omit `model` entirely to let Rouva route to the cheapest capable model automatically.
57
57
  *
58
58
  * Other IDs are accepted by the type for forward compatibility. On
@@ -64,7 +64,7 @@ type RouvaProvider = 'anthropic' | 'openai' | 'gemini' | 'deepseek' | 'mistral'
64
64
  *
65
65
  * `'gpt-5.6'` is a deprecated alias the gateway maps to `'gpt-5.6-sol'`.
66
66
  */
67
- type RouvaModel = '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' | 'gpt-6.1-sol' | 'gpt-6-sol' | 'gpt-6-luna' | 'gpt-5.6-sol' | 'gpt-5.6-terra' | 'gpt-5.6-luna' | 'gpt-5.6' | 'gpt-5' | 'gpt-5.4-mini' | 'gpt-5-mini' | 'gpt-5-nano' | 'gpt-4.1' | 'gpt-4.1-mini' | 'gpt-4o' | 'gpt-4o-mini' | 'gemini-3.8-flash' | 'gemini-3.7-flash' | 'gemini-3.6-flash' | 'gemini-3.5-flash-lite' | 'gemini-2.5-pro' | 'gemini-2.5-flash' | 'deepseek-flash' | 'deepseek-flash-think' | 'deepseek-v4-pro' | 'mistral-large-latest' | 'mistral-small-latest' | 'kimi-k3' | 'kimi-k2.6' | 'grok-4.7' | 'grok-4.6' | 'grok-4.5' | 'grok-4.3' | 'grok-4.20-0309-reasoning' | 'glm-5.3' | 'glm-5.2' | 'glm-4.7-flash' | 'qwen3.8-omni-flash' | 'qwen3.8-flash' | 'qwen3.7-plus' | 'qwen-max' | 'qwen-plus' | 'qwen-flash' | (string & {});
67
+ type RouvaModel = '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-5-5' | 'claude-haiku-4-5-20251001' | 'gpt-6-astra' | 'gpt-6.1-sol' | 'gpt-6.1-terra' | 'gpt-6-sol' | 'gpt-6-luna' | 'gpt-5.6-sol' | 'gpt-5.6-terra' | 'gpt-5.6-luna' | 'gpt-5.6' | 'gpt-5' | 'gpt-5.4-mini' | 'gpt-5-mini' | 'gpt-5-nano' | 'gpt-4.1' | 'gpt-4.1-mini' | 'gpt-4o' | 'gpt-4o-mini' | 'gemini-4-argon' | 'gemini-3.8-flash' | 'gemini-3.7-flash' | 'gemini-3.6-flash' | 'gemini-3.5-flash-lite' | 'gemini-2.5-pro' | 'gemini-2.5-flash' | 'deepseek-flash' | 'deepseek-flash-think' | 'deepseek-v4-pro' | 'mistral-large-latest' | 'mistral-small-latest' | 'kimi-k3.1-pro' | 'kimi-k3' | 'kimi-k2.6' | 'grok-4.7' | 'grok-4.6' | 'grok-4.5' | 'grok-4.3' | 'grok-4.20-0309-reasoning' | 'glm-5.3' | 'glm-5.2' | 'glm-4.7-flash' | 'qwen3.8-omni-flash' | 'qwen3.8-flash' | 'qwen3.7-plus' | 'qwen-max' | 'qwen-plus' | 'qwen-flash' | 'MiniMax-M3.1-Flash' | 'MiniMax-M3' | (string & {});
68
68
  /** Anthropic-style text block — the array form of the `system` param */
69
69
  interface SystemTextBlock {
70
70
  type: 'text';
@@ -108,7 +108,8 @@ interface ChatCompletionParams {
108
108
  * Sampling temperature 0–1.
109
109
  * The gateway omits it for models that only accept their default: OpenAI
110
110
  * reasoning models (gpt-5 / gpt-6 families) and the Claude models that reject
111
- * it (Opus 4.7 and newer, Sonnet 5 and newer, Fable).
111
+ * it (Opus 4.7 and newer, Sonnet 5 and newer, Haiku 5.5, Fable), and for
112
+ * models whose sampling is fixed (Kimi K3 / K3.1 Pro, MiniMax).
112
113
  */
113
114
  temperature?: number;
114
115
  /**
@@ -116,7 +117,8 @@ interface ChatCompletionParams {
116
117
  * default) is treated as omitted. OpenAI reasoning models (gpt-5 / gpt-6
117
118
  * families, o-series) don't support it: routing avoids them when top_p is
118
119
  * set, and pinning one returns a 400. The gateway omits it for the Claude
119
- * models that reject it (Opus 4.7 and newer, Sonnet 5 and newer, Fable).
120
+ * models that reject it (Opus 4.7 and newer, Sonnet 5 and newer, Haiku 5.5, Fable),
121
+ * and for models whose sampling is fixed (Kimi K3 / K3.1 Pro, MiniMax).
120
122
  */
121
123
  top_p?: number;
122
124
  /**
@@ -147,13 +149,14 @@ interface ChatCompletionParams {
147
149
  * How hard a reasoning model thinks (gateway support since 2026-10-21).
148
150
  * OpenAI models take it as is; Claude gets it as `output_config.effort`.
149
151
  * Values depend on the model: gpt-5 / -mini / -nano take minimal–high;
150
- * gpt-5.4-mini, gpt-5.6-*, gpt-6-luna / -sol take none–xhigh; gpt-6.1-sol
151
- * low–xhigh; Claude Sonnet / Opus 4.6 low, medium, high, max; newer Claude
152
- * models also xhigh; GPT-4 models and Claude Haiku 4.5 don't take it. Routing
152
+ * gpt-5.4-mini, gpt-5.6-*, gpt-6-luna / -sol, gpt-6.1-terra take none–xhigh;
153
+ * gpt-6.1-sol and gpt-6-astra low–xhigh; Claude Sonnet / Opus 4.6 low, medium,
154
+ * high, max; newer Claude models (incl. Haiku 5.5) also xhigh; GPT-4 models and Claude Haiku 4.5 don't take it. Routing
153
155
  * only picks a model that takes the value; a listed OpenAI or Claude model you
154
- * name that doesn't is a 400 (other providers' models get the value as sent). On gpt-6-luna / -sol, `'none'` turns reasoning off (temperature,
155
- * top_p and tools then work) and any other value turns it on (tools and
156
- * top_p are then a 400). `rouva.run()` doesn't accept it.
156
+ * name that doesn't is a 400 (other providers' models get the value as sent). On gpt-6-luna / -sol and gpt-6.1-terra, `'none'` turns reasoning off (temperature,
157
+ * top_p and tools then work) and any other value turns it on (top_p is then
158
+ * a 400; tools are served through OpenAI's Responses API). `rouva.run()`
159
+ * doesn't accept it.
157
160
  * See https://rouva.io/docs#reasoning-effort
158
161
  */
159
162
  reasoning_effort?: ReasoningEffort | null;
@@ -175,8 +178,10 @@ interface ChatCompletionParams {
175
178
  * Anthropic `{ name, description, input_schema }`). Requires `model`.
176
179
  * Tool schemas are provider-specific, so a tools request only ever moves to
177
180
  * another model of the SAME provider (a routing override, or cheaper-model
178
- * routing on /v1). Models that don't support tools on Chat Completions
179
- * (gpt-6.1-sol, gpt-5.6-*) return a 400.
181
+ * routing on /v1). gpt-6.1-sol, gpt-6-astra and gpt-5.6-* take tools only through
182
+ * OpenAI's Responses API: the gateway uses it for you and converts both ways
183
+ * (you still send and receive Chat Completions), and there `seed`, `stop` and
184
+ * a `max_tokens` below 16 are a 400. `rouva.run()` still refuses these models.
180
185
  */
181
186
  tools?: Array<Record<string, unknown>>;
182
187
  /**
@@ -185,6 +190,64 @@ interface ChatCompletionParams {
185
190
  */
186
191
  tool_choice?: string | Record<string, unknown>;
187
192
  }
193
+ /**
194
+ * What `rouva.explain()` returns when the request would be sent: the model
195
+ * Rouva would pick, why, and the most it could cost — nothing was sent,
196
+ * recorded or reserved.
197
+ */
198
+ interface RouvaExplanation {
199
+ dry_run: true;
200
+ would_send: true;
201
+ endpoint: '/api/gateway/messages' | '/v1/chat/completions';
202
+ provider: string;
203
+ model: string;
204
+ /** The model you named, or null. */
205
+ requested_model: string | null;
206
+ task_type: string | null;
207
+ route_reason: RouvaRouteReason;
208
+ /** How the prompt was classified (e.g. `jev`, `embedding`, `cache`). */
209
+ classified_by: string | null;
210
+ /** Jev's confidence (0–1), when Jev classified it. */
211
+ classifier_confidence: number | null;
212
+ classifier_latency_ms: number | null;
213
+ max_tokens: number;
214
+ stream: boolean;
215
+ /** The model's billed rates in USD per million tokens, or null when it has no known price. */
216
+ price_per_1m: {
217
+ input: number;
218
+ output: number;
219
+ } | null;
220
+ /**
221
+ * The most the request could cost in USD — what a spend-capped key reserves
222
+ * (the input as sent plus max_tokens of output, plus reasoning and
223
+ * summarization allowances where they apply). null: no known price.
224
+ */
225
+ max_cost_usd: number | null;
226
+ /** A long conversation's older turns would be summarized first (Claude). */
227
+ summarize_history: boolean;
228
+ spend_capped: boolean;
229
+ }
230
+ /** What `rouva.explain()` returns when the real request would be refused. */
231
+ interface RouvaExplainedRefusal {
232
+ dry_run: true;
233
+ would_send: false;
234
+ /** The status the real request would get. */
235
+ status: number;
236
+ /** The error it would get. */
237
+ error: unknown;
238
+ /** `X-Rouva-Refusal`, when Rouva marks it (e.g. `unbounded`, `key_cap`). */
239
+ refusal: string | null;
240
+ /** The model Rouva had chosen before refusing, when it had chosen one. */
241
+ provider: string | null;
242
+ model: string | null;
243
+ route_reason: RouvaRouteReason | null;
244
+ }
245
+ type RouvaExplainResult = RouvaExplanation | RouvaExplainedRefusal;
246
+ /** Options for `rouva.explain()`. */
247
+ interface ExplainOptions extends RequestOptions {
248
+ /** `'v1'` explains the same body sent to /v1/chat/completions (which pins the named model). */
249
+ endpoint?: 'messages' | 'v1';
250
+ }
188
251
  /** `reasoning_effort` values the gateway accepts (which ones a model takes varies). */
189
252
  type ReasoningEffort = 'none' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh' | 'max';
190
253
  /** `response_format` values the gateway accepts. */
@@ -490,6 +553,25 @@ declare class Rouva {
490
553
  run(params: RunParams, options?: RequestOptions): Promise<RunResult>;
491
554
  private _getRequest;
492
555
  private _post;
556
+ /**
557
+ * Dry run (`POST /api/gateway/explain`): what Rouva would do with this
558
+ * request — the model and provider it would pick, why (`route_reason`), how
559
+ * the prompt was classified, and the most it could cost (`max_cost_usd`) —
560
+ * without calling a provider, recording a request or reserving spend. Takes
561
+ * the same params as `chat.completions.create()` (`stream` is ignored, as
562
+ * there). When the real request would be refused, resolves with
563
+ * `would_send: false` and the status and error it would get. Throws
564
+ * `RouvaError` for a bad key or the explain rate limit (429, `refusal: 'rate'`).
565
+ *
566
+ * The prompt is classified as for a real request. Not covered: the
567
+ * duplicate-request cache, a spend cap's remaining budget, provider errors.
568
+ *
569
+ * @example
570
+ * const e = await rouva.explain({ model: 'gpt-6-sol', messages })
571
+ * if (e.would_send) console.log(e.model, e.route_reason, e.max_cost_usd)
572
+ * else console.log(e.status, e.error)
573
+ */
574
+ explain(params: ChatCompletionParams, options?: ExplainOptions): Promise<RouvaExplainResult>;
493
575
  private _fetch;
494
576
  private _createChatCompletion;
495
577
  }
@@ -530,7 +612,7 @@ declare class RouvaError extends Error {
530
612
  readonly requestId?: string;
531
613
  /** The parsed JSON body, or the raw text when it isn't JSON. */
532
614
  readonly body: unknown;
533
- constructor(kind: 'Gateway' | 'Run' | 'Request', status: number, headers: Headers, rawBody: string);
615
+ constructor(kind: 'Gateway' | 'Run' | 'Request' | 'Explain', status: number, headers: Headers, rawBody: string);
534
616
  }
535
617
 
536
- export { type AssistantToolCallMessage, type ChatCompletion, type ChatCompletionChoice, type ChatCompletionParams, type ChatCompletionUsage, type ChatMessage, type ContentBlockMessage, type FunctionToolCall, type Message, type ReasoningEffort, type RequestOptions, type ResponseFormat, type ResponseMessage, Rouva, RouvaError, type RouvaModel, type RouvaOptions, type RouvaProvider, type RouvaRequest, type RouvaRequestStatus, type RouvaResponseMeta, type RouvaRouteReason, type RouvaStream, type RunParams, type RunResult, type SystemTextBlock, type ToolResultMessage };
618
+ export { type AssistantToolCallMessage, type ChatCompletion, type ChatCompletionChoice, type ChatCompletionParams, type ChatCompletionUsage, type ChatMessage, type ContentBlockMessage, type ExplainOptions, type FunctionToolCall, type Message, type ReasoningEffort, type RequestOptions, type ResponseFormat, type ResponseMessage, Rouva, RouvaError, type RouvaExplainResult, type RouvaExplainedRefusal, type RouvaExplanation, type RouvaModel, type RouvaOptions, type RouvaProvider, type RouvaRequest, type RouvaRequestStatus, type RouvaResponseMeta, type RouvaRouteReason, type RouvaStream, type RunParams, type RunResult, type SystemTextBlock, type ToolResultMessage };
package/dist/index.d.ts CHANGED
@@ -50,9 +50,9 @@ interface ContentBlockMessage {
50
50
  * valid when `tools` is set on the request.
51
51
  */
52
52
  type ChatMessage = Message | AssistantToolCallMessage | ToolResultMessage | ContentBlockMessage;
53
- type RouvaProvider = 'anthropic' | 'openai' | 'gemini' | 'deepseek' | 'mistral' | 'moonshot' | 'xai' | 'zai' | 'alibaba' | (string & {});
53
+ type RouvaProvider = 'anthropic' | 'openai' | 'gemini' | 'deepseek' | 'mistral' | 'moonshot' | 'xai' | 'zai' | 'alibaba' | 'minimax' | (string & {});
54
54
  /**
55
- * Supported models across all providers (the gateway's list as of SDK 0.3.0).
55
+ * Supported models across all providers (the gateway's list as of SDK 0.5.1).
56
56
  * Omit `model` entirely to let Rouva route to the cheapest capable model automatically.
57
57
  *
58
58
  * Other IDs are accepted by the type for forward compatibility. On
@@ -64,7 +64,7 @@ type RouvaProvider = 'anthropic' | 'openai' | 'gemini' | 'deepseek' | 'mistral'
64
64
  *
65
65
  * `'gpt-5.6'` is a deprecated alias the gateway maps to `'gpt-5.6-sol'`.
66
66
  */
67
- type RouvaModel = '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' | 'gpt-6.1-sol' | 'gpt-6-sol' | 'gpt-6-luna' | 'gpt-5.6-sol' | 'gpt-5.6-terra' | 'gpt-5.6-luna' | 'gpt-5.6' | 'gpt-5' | 'gpt-5.4-mini' | 'gpt-5-mini' | 'gpt-5-nano' | 'gpt-4.1' | 'gpt-4.1-mini' | 'gpt-4o' | 'gpt-4o-mini' | 'gemini-3.8-flash' | 'gemini-3.7-flash' | 'gemini-3.6-flash' | 'gemini-3.5-flash-lite' | 'gemini-2.5-pro' | 'gemini-2.5-flash' | 'deepseek-flash' | 'deepseek-flash-think' | 'deepseek-v4-pro' | 'mistral-large-latest' | 'mistral-small-latest' | 'kimi-k3' | 'kimi-k2.6' | 'grok-4.7' | 'grok-4.6' | 'grok-4.5' | 'grok-4.3' | 'grok-4.20-0309-reasoning' | 'glm-5.3' | 'glm-5.2' | 'glm-4.7-flash' | 'qwen3.8-omni-flash' | 'qwen3.8-flash' | 'qwen3.7-plus' | 'qwen-max' | 'qwen-plus' | 'qwen-flash' | (string & {});
67
+ type RouvaModel = '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-5-5' | 'claude-haiku-4-5-20251001' | 'gpt-6-astra' | 'gpt-6.1-sol' | 'gpt-6.1-terra' | 'gpt-6-sol' | 'gpt-6-luna' | 'gpt-5.6-sol' | 'gpt-5.6-terra' | 'gpt-5.6-luna' | 'gpt-5.6' | 'gpt-5' | 'gpt-5.4-mini' | 'gpt-5-mini' | 'gpt-5-nano' | 'gpt-4.1' | 'gpt-4.1-mini' | 'gpt-4o' | 'gpt-4o-mini' | 'gemini-4-argon' | 'gemini-3.8-flash' | 'gemini-3.7-flash' | 'gemini-3.6-flash' | 'gemini-3.5-flash-lite' | 'gemini-2.5-pro' | 'gemini-2.5-flash' | 'deepseek-flash' | 'deepseek-flash-think' | 'deepseek-v4-pro' | 'mistral-large-latest' | 'mistral-small-latest' | 'kimi-k3.1-pro' | 'kimi-k3' | 'kimi-k2.6' | 'grok-4.7' | 'grok-4.6' | 'grok-4.5' | 'grok-4.3' | 'grok-4.20-0309-reasoning' | 'glm-5.3' | 'glm-5.2' | 'glm-4.7-flash' | 'qwen3.8-omni-flash' | 'qwen3.8-flash' | 'qwen3.7-plus' | 'qwen-max' | 'qwen-plus' | 'qwen-flash' | 'MiniMax-M3.1-Flash' | 'MiniMax-M3' | (string & {});
68
68
  /** Anthropic-style text block — the array form of the `system` param */
69
69
  interface SystemTextBlock {
70
70
  type: 'text';
@@ -108,7 +108,8 @@ interface ChatCompletionParams {
108
108
  * Sampling temperature 0–1.
109
109
  * The gateway omits it for models that only accept their default: OpenAI
110
110
  * reasoning models (gpt-5 / gpt-6 families) and the Claude models that reject
111
- * it (Opus 4.7 and newer, Sonnet 5 and newer, Fable).
111
+ * it (Opus 4.7 and newer, Sonnet 5 and newer, Haiku 5.5, Fable), and for
112
+ * models whose sampling is fixed (Kimi K3 / K3.1 Pro, MiniMax).
112
113
  */
113
114
  temperature?: number;
114
115
  /**
@@ -116,7 +117,8 @@ interface ChatCompletionParams {
116
117
  * default) is treated as omitted. OpenAI reasoning models (gpt-5 / gpt-6
117
118
  * families, o-series) don't support it: routing avoids them when top_p is
118
119
  * set, and pinning one returns a 400. The gateway omits it for the Claude
119
- * models that reject it (Opus 4.7 and newer, Sonnet 5 and newer, Fable).
120
+ * models that reject it (Opus 4.7 and newer, Sonnet 5 and newer, Haiku 5.5, Fable),
121
+ * and for models whose sampling is fixed (Kimi K3 / K3.1 Pro, MiniMax).
120
122
  */
121
123
  top_p?: number;
122
124
  /**
@@ -147,13 +149,14 @@ interface ChatCompletionParams {
147
149
  * How hard a reasoning model thinks (gateway support since 2026-10-21).
148
150
  * OpenAI models take it as is; Claude gets it as `output_config.effort`.
149
151
  * Values depend on the model: gpt-5 / -mini / -nano take minimal–high;
150
- * gpt-5.4-mini, gpt-5.6-*, gpt-6-luna / -sol take none–xhigh; gpt-6.1-sol
151
- * low–xhigh; Claude Sonnet / Opus 4.6 low, medium, high, max; newer Claude
152
- * models also xhigh; GPT-4 models and Claude Haiku 4.5 don't take it. Routing
152
+ * gpt-5.4-mini, gpt-5.6-*, gpt-6-luna / -sol, gpt-6.1-terra take none–xhigh;
153
+ * gpt-6.1-sol and gpt-6-astra low–xhigh; Claude Sonnet / Opus 4.6 low, medium,
154
+ * high, max; newer Claude models (incl. Haiku 5.5) also xhigh; GPT-4 models and Claude Haiku 4.5 don't take it. Routing
153
155
  * only picks a model that takes the value; a listed OpenAI or Claude model you
154
- * name that doesn't is a 400 (other providers' models get the value as sent). On gpt-6-luna / -sol, `'none'` turns reasoning off (temperature,
155
- * top_p and tools then work) and any other value turns it on (tools and
156
- * top_p are then a 400). `rouva.run()` doesn't accept it.
156
+ * name that doesn't is a 400 (other providers' models get the value as sent). On gpt-6-luna / -sol and gpt-6.1-terra, `'none'` turns reasoning off (temperature,
157
+ * top_p and tools then work) and any other value turns it on (top_p is then
158
+ * a 400; tools are served through OpenAI's Responses API). `rouva.run()`
159
+ * doesn't accept it.
157
160
  * See https://rouva.io/docs#reasoning-effort
158
161
  */
159
162
  reasoning_effort?: ReasoningEffort | null;
@@ -175,8 +178,10 @@ interface ChatCompletionParams {
175
178
  * Anthropic `{ name, description, input_schema }`). Requires `model`.
176
179
  * Tool schemas are provider-specific, so a tools request only ever moves to
177
180
  * another model of the SAME provider (a routing override, or cheaper-model
178
- * routing on /v1). Models that don't support tools on Chat Completions
179
- * (gpt-6.1-sol, gpt-5.6-*) return a 400.
181
+ * routing on /v1). gpt-6.1-sol, gpt-6-astra and gpt-5.6-* take tools only through
182
+ * OpenAI's Responses API: the gateway uses it for you and converts both ways
183
+ * (you still send and receive Chat Completions), and there `seed`, `stop` and
184
+ * a `max_tokens` below 16 are a 400. `rouva.run()` still refuses these models.
180
185
  */
181
186
  tools?: Array<Record<string, unknown>>;
182
187
  /**
@@ -185,6 +190,64 @@ interface ChatCompletionParams {
185
190
  */
186
191
  tool_choice?: string | Record<string, unknown>;
187
192
  }
193
+ /**
194
+ * What `rouva.explain()` returns when the request would be sent: the model
195
+ * Rouva would pick, why, and the most it could cost — nothing was sent,
196
+ * recorded or reserved.
197
+ */
198
+ interface RouvaExplanation {
199
+ dry_run: true;
200
+ would_send: true;
201
+ endpoint: '/api/gateway/messages' | '/v1/chat/completions';
202
+ provider: string;
203
+ model: string;
204
+ /** The model you named, or null. */
205
+ requested_model: string | null;
206
+ task_type: string | null;
207
+ route_reason: RouvaRouteReason;
208
+ /** How the prompt was classified (e.g. `jev`, `embedding`, `cache`). */
209
+ classified_by: string | null;
210
+ /** Jev's confidence (0–1), when Jev classified it. */
211
+ classifier_confidence: number | null;
212
+ classifier_latency_ms: number | null;
213
+ max_tokens: number;
214
+ stream: boolean;
215
+ /** The model's billed rates in USD per million tokens, or null when it has no known price. */
216
+ price_per_1m: {
217
+ input: number;
218
+ output: number;
219
+ } | null;
220
+ /**
221
+ * The most the request could cost in USD — what a spend-capped key reserves
222
+ * (the input as sent plus max_tokens of output, plus reasoning and
223
+ * summarization allowances where they apply). null: no known price.
224
+ */
225
+ max_cost_usd: number | null;
226
+ /** A long conversation's older turns would be summarized first (Claude). */
227
+ summarize_history: boolean;
228
+ spend_capped: boolean;
229
+ }
230
+ /** What `rouva.explain()` returns when the real request would be refused. */
231
+ interface RouvaExplainedRefusal {
232
+ dry_run: true;
233
+ would_send: false;
234
+ /** The status the real request would get. */
235
+ status: number;
236
+ /** The error it would get. */
237
+ error: unknown;
238
+ /** `X-Rouva-Refusal`, when Rouva marks it (e.g. `unbounded`, `key_cap`). */
239
+ refusal: string | null;
240
+ /** The model Rouva had chosen before refusing, when it had chosen one. */
241
+ provider: string | null;
242
+ model: string | null;
243
+ route_reason: RouvaRouteReason | null;
244
+ }
245
+ type RouvaExplainResult = RouvaExplanation | RouvaExplainedRefusal;
246
+ /** Options for `rouva.explain()`. */
247
+ interface ExplainOptions extends RequestOptions {
248
+ /** `'v1'` explains the same body sent to /v1/chat/completions (which pins the named model). */
249
+ endpoint?: 'messages' | 'v1';
250
+ }
188
251
  /** `reasoning_effort` values the gateway accepts (which ones a model takes varies). */
189
252
  type ReasoningEffort = 'none' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh' | 'max';
190
253
  /** `response_format` values the gateway accepts. */
@@ -490,6 +553,25 @@ declare class Rouva {
490
553
  run(params: RunParams, options?: RequestOptions): Promise<RunResult>;
491
554
  private _getRequest;
492
555
  private _post;
556
+ /**
557
+ * Dry run (`POST /api/gateway/explain`): what Rouva would do with this
558
+ * request — the model and provider it would pick, why (`route_reason`), how
559
+ * the prompt was classified, and the most it could cost (`max_cost_usd`) —
560
+ * without calling a provider, recording a request or reserving spend. Takes
561
+ * the same params as `chat.completions.create()` (`stream` is ignored, as
562
+ * there). When the real request would be refused, resolves with
563
+ * `would_send: false` and the status and error it would get. Throws
564
+ * `RouvaError` for a bad key or the explain rate limit (429, `refusal: 'rate'`).
565
+ *
566
+ * The prompt is classified as for a real request. Not covered: the
567
+ * duplicate-request cache, a spend cap's remaining budget, provider errors.
568
+ *
569
+ * @example
570
+ * const e = await rouva.explain({ model: 'gpt-6-sol', messages })
571
+ * if (e.would_send) console.log(e.model, e.route_reason, e.max_cost_usd)
572
+ * else console.log(e.status, e.error)
573
+ */
574
+ explain(params: ChatCompletionParams, options?: ExplainOptions): Promise<RouvaExplainResult>;
493
575
  private _fetch;
494
576
  private _createChatCompletion;
495
577
  }
@@ -530,7 +612,7 @@ declare class RouvaError extends Error {
530
612
  readonly requestId?: string;
531
613
  /** The parsed JSON body, or the raw text when it isn't JSON. */
532
614
  readonly body: unknown;
533
- constructor(kind: 'Gateway' | 'Run' | 'Request', status: number, headers: Headers, rawBody: string);
615
+ constructor(kind: 'Gateway' | 'Run' | 'Request' | 'Explain', status: number, headers: Headers, rawBody: string);
534
616
  }
535
617
 
536
- export { type AssistantToolCallMessage, type ChatCompletion, type ChatCompletionChoice, type ChatCompletionParams, type ChatCompletionUsage, type ChatMessage, type ContentBlockMessage, type FunctionToolCall, type Message, type ReasoningEffort, type RequestOptions, type ResponseFormat, type ResponseMessage, Rouva, RouvaError, type RouvaModel, type RouvaOptions, type RouvaProvider, type RouvaRequest, type RouvaRequestStatus, type RouvaResponseMeta, type RouvaRouteReason, type RouvaStream, type RunParams, type RunResult, type SystemTextBlock, type ToolResultMessage };
618
+ export { type AssistantToolCallMessage, type ChatCompletion, type ChatCompletionChoice, type ChatCompletionParams, type ChatCompletionUsage, type ChatMessage, type ContentBlockMessage, type ExplainOptions, type FunctionToolCall, type Message, type ReasoningEffort, type RequestOptions, type ResponseFormat, type ResponseMessage, Rouva, RouvaError, type RouvaExplainResult, type RouvaExplainedRefusal, type RouvaExplanation, type RouvaModel, type RouvaOptions, type RouvaProvider, type RouvaRequest, type RouvaRequestStatus, type RouvaResponseMeta, type RouvaRouteReason, type RouvaStream, type RunParams, type RunResult, type SystemTextBlock, type ToolResultMessage };
package/dist/index.js CHANGED
@@ -492,6 +492,31 @@ var Rouva = class {
492
492
  _post(path, body, extraHeaders, options) {
493
493
  return this._fetch("POST", path, body, extraHeaders, options);
494
494
  }
495
+ /**
496
+ * Dry run (`POST /api/gateway/explain`): what Rouva would do with this
497
+ * request — the model and provider it would pick, why (`route_reason`), how
498
+ * the prompt was classified, and the most it could cost (`max_cost_usd`) —
499
+ * without calling a provider, recording a request or reserving spend. Takes
500
+ * the same params as `chat.completions.create()` (`stream` is ignored, as
501
+ * there). When the real request would be refused, resolves with
502
+ * `would_send: false` and the status and error it would get. Throws
503
+ * `RouvaError` for a bad key or the explain rate limit (429, `refusal: 'rate'`).
504
+ *
505
+ * The prompt is classified as for a real request. Not covered: the
506
+ * duplicate-request cache, a spend cap's remaining budget, provider errors.
507
+ *
508
+ * @example
509
+ * const e = await rouva.explain({ model: 'gpt-6-sol', messages })
510
+ * if (e.would_send) console.log(e.model, e.route_reason, e.max_cost_usd)
511
+ * else console.log(e.status, e.error)
512
+ */
513
+ async explain(params, options) {
514
+ const { stream: _stream, ...body } = params;
515
+ const query = options?.endpoint === "v1" ? "?endpoint=v1" : "";
516
+ const res = await this._post(`/api/gateway/explain${query}`, body, {}, options);
517
+ if (!res.ok) throw await toRouvaError("Explain", res);
518
+ return res.json();
519
+ }
495
520
  // One request path for every endpoint: auth, JSON body, optional abort signal.
496
521
  _fetch(method, path, body, extraHeaders, options) {
497
522
  return fetch(`${this.baseURL}${path}`, {
package/dist/index.mjs CHANGED
@@ -465,6 +465,31 @@ var Rouva = class {
465
465
  _post(path, body, extraHeaders, options) {
466
466
  return this._fetch("POST", path, body, extraHeaders, options);
467
467
  }
468
+ /**
469
+ * Dry run (`POST /api/gateway/explain`): what Rouva would do with this
470
+ * request — the model and provider it would pick, why (`route_reason`), how
471
+ * the prompt was classified, and the most it could cost (`max_cost_usd`) —
472
+ * without calling a provider, recording a request or reserving spend. Takes
473
+ * the same params as `chat.completions.create()` (`stream` is ignored, as
474
+ * there). When the real request would be refused, resolves with
475
+ * `would_send: false` and the status and error it would get. Throws
476
+ * `RouvaError` for a bad key or the explain rate limit (429, `refusal: 'rate'`).
477
+ *
478
+ * The prompt is classified as for a real request. Not covered: the
479
+ * duplicate-request cache, a spend cap's remaining budget, provider errors.
480
+ *
481
+ * @example
482
+ * const e = await rouva.explain({ model: 'gpt-6-sol', messages })
483
+ * if (e.would_send) console.log(e.model, e.route_reason, e.max_cost_usd)
484
+ * else console.log(e.status, e.error)
485
+ */
486
+ async explain(params, options) {
487
+ const { stream: _stream, ...body } = params;
488
+ const query = options?.endpoint === "v1" ? "?endpoint=v1" : "";
489
+ const res = await this._post(`/api/gateway/explain${query}`, body, {}, options);
490
+ if (!res.ok) throw await toRouvaError("Explain", res);
491
+ return res.json();
492
+ }
468
493
  // One request path for every endpoint: auth, JSON body, optional abort signal.
469
494
  _fetch(method, path, body, extraHeaders, options) {
470
495
  return fetch(`${this.baseURL}${path}`, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rouvanpm/rouva",
3
- "version": "0.4.1",
3
+ "version": "0.5.1",
4
4
  "description": "Official Node.js SDK for Rouva — managed AI gateway with intelligent routing and spend tracking",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",