@rouvanpm/rouva 0.2.13 → 0.3.0
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 +60 -30
- package/dist/index.d.mts +122 -41
- package/dist/index.d.ts +122 -41
- package/dist/index.js +66 -39
- package/dist/index.mjs +64 -38
- package/package.json +1 -1
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,
|
|
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.
|
|
28
28
|
|
|
29
29
|
```typescript
|
|
30
30
|
// Request a specific model
|
|
@@ -89,43 +89,47 @@ const res = await rouva.chat.completions.create({
|
|
|
89
89
|
model: 'gpt-4o-mini', // required when using seed (OpenAI only)
|
|
90
90
|
messages: [{ role: 'user', content: 'Write a haiku about routing.' }],
|
|
91
91
|
system: 'You are a concise assistant.', // string or Anthropic-style text blocks
|
|
92
|
-
max_tokens: 1024, // default 4096
|
|
92
|
+
max_tokens: 1024, // default 4096 (or max_completion_tokens)
|
|
93
93
|
temperature: 0.7, // 0–1
|
|
94
94
|
top_p: 0.9, // nucleus sampling, (0, 1]
|
|
95
95
|
stop: ['END'], // up to 4 stop sequences
|
|
96
96
|
seed: 42, // pins the request to this exact model
|
|
97
|
+
metadata: { feature: 'search' }, // your labels, stored with the usage record
|
|
97
98
|
})
|
|
98
99
|
```
|
|
99
100
|
|
|
100
|
-
- **`temperature`** — sampling temperature between 0 and 1.
|
|
101
|
-
- **`max_tokens`** —
|
|
102
|
-
- **`top_p`** — nucleus sampling
|
|
103
|
-
- **`stop`** — a string or up to 4 stop sequences
|
|
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).
|
|
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
|
+
- **`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
|
+
- **`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
|
+
- **`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.
|
|
104
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).
|
|
105
107
|
|
|
106
108
|
Unsupported OpenAI options (`response_format`, `logit_bias`, `n > 1`, the legacy `functions`/`function_call` fields, …) are rejected by the gateway with an explicit 400 rather than silently ignored.
|
|
107
109
|
|
|
108
110
|
## Unlisted models
|
|
109
111
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
Models not recognised by the gateway return a `400` immediately:
|
|
112
|
+
`chat.completions.create()` serves the models the gateway can price — its built-in list (see `RouvaModel` and rouva.io/docs) plus models added to its synced model registry, such as new snapshots. Any other ID returns a `404` immediately:
|
|
113
113
|
|
|
114
114
|
```json
|
|
115
|
-
{ "error": "
|
|
115
|
+
{ "error": "The model `<id>` does not exist or is not supported. See rouva.io/docs for the supported model list." }
|
|
116
116
|
```
|
|
117
117
|
|
|
118
|
+
`run()` also accepts an ID whose provider it can infer from the name (e.g. a dated snapshot); without a known price it's logged with no cost, and it's refused for spend-capped keys or runs with `session_budget_usd`.
|
|
119
|
+
|
|
120
|
+
`'gpt-5.6'` is a deprecated alias for `'gpt-5.6-sol'` — use the full name.
|
|
121
|
+
|
|
118
122
|
## OpenAI-compatible endpoint
|
|
119
123
|
|
|
120
124
|
Using the OpenAI SDK (or LangChain, the Vercel AI SDK, …) instead of this one? The gateway is also served at `POST /v1/chat/completions` — point `baseURL` at `https://app.rouva.io/v1` with your `rva_` key and it behaves exactly like OpenAI: `model` is required, responses are buffered JSON unless `stream: true`, and only OpenAI-format providers are available (use this SDK or the native endpoint for Anthropic models).
|
|
121
125
|
|
|
122
|
-
Non-tools requests on `/v1`
|
|
126
|
+
Non-tools requests on `/v1` honor the named model exactly unless the workspace owner has set an account routing override for the task — overrides apply as policy on every endpoint, and on `/v1` one may route to Anthropic (the response is translated back to the OpenAI format). **Tools requests on `/v1` benefit from within-provider intelligent routing** when Intelligent Routing is enabled in your dashboard — Rouva picks the cheapest capable model within the same provider, using the named `model` as a cost ceiling. The provider never changes (tool schemas are provider-specific), and `response.model` reflects the model that actually served the request. Turn Intelligent Routing off in dashboard Settings → Gateway to always pin the exact model.
|
|
123
127
|
|
|
124
128
|
## Tool use
|
|
125
129
|
|
|
126
|
-
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.
|
|
130
|
+
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.
|
|
127
131
|
|
|
128
|
-
|
|
132
|
+
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.
|
|
129
133
|
|
|
130
134
|
```typescript
|
|
131
135
|
const res = await rouva.chat.completions.create({
|
|
@@ -167,19 +171,19 @@ Responses are normalized to the OpenAI shape regardless of provider: Anthropic `
|
|
|
167
171
|
|
|
168
172
|
**Don't branch on `finish_reason` to detect tool calls** — check for the presence of `message.tool_calls` instead. `finish_reason` follows each provider's own semantics, and OpenAI notably returns `"stop"` (not `"tool_calls"`) when `tool_choice` forces a specific function. The SDK passes OpenAI's values through unchanged so behavior matches calling OpenAI directly.
|
|
169
173
|
|
|
170
|
-
Tools requests record usage and cost. Savings are recorded when within-provider routing substitutes a cheaper model on `/v1`. Tools responses are not quality-scored
|
|
174
|
+
Tools requests record usage and cost. Savings are recorded when within-provider routing substitutes a cheaper model on `/v1`. Tools responses are not quality-scored, and streaming tools requests are never served from the duplicate-request cache.
|
|
171
175
|
|
|
172
176
|
## Cost optimisations
|
|
173
177
|
|
|
174
178
|
Rouva applies several cost-saving layers automatically on every non-tools request — no configuration required.
|
|
175
179
|
|
|
176
|
-
**
|
|
180
|
+
**Duplicate-request cache** — an identical request repeated within 60 seconds (typically a client retry) gets the earlier response instead of a second provider call, and isn't charged. Such responses carry `_rouva.dedup: true`.
|
|
177
181
|
|
|
178
|
-
**Prompt caching
|
|
182
|
+
**Prompt caching** — Anthropic requests automatically include cache-control markers on large system prompts, tool definitions and conversation prefixes, reducing input token costs on repeated turns.
|
|
179
183
|
|
|
180
|
-
**Conversation summarisation
|
|
184
|
+
**Conversation summarisation** — long Anthropic conversations are summarised by a cheap model before forwarding, compressing the context window and reducing input tokens. The summarisation cost is included in the request's spend snapshot. Only runs when the conversation exceeds ~2,000 tokens of non-system content and the request is not a tools request.
|
|
181
185
|
|
|
182
|
-
|
|
186
|
+
Tools requests are forwarded verbatim: no summarisation or whitespace trimming.
|
|
183
187
|
|
|
184
188
|
## Options
|
|
185
189
|
|
|
@@ -226,16 +230,16 @@ console.log(result.session_cost_usd) // total USD spent
|
|
|
226
230
|
### How it works
|
|
227
231
|
|
|
228
232
|
1. The gateway sends your `messages` to the model (non-streaming)
|
|
229
|
-
2. When the model returns tool calls, all handlers are dispatched **in parallel** via POST — the request body is the
|
|
233
|
+
2. When the model returns tool calls, all handlers are dispatched **in parallel** via POST — the request body is `{ "name": "<tool>", "arguments": { …the model's input… } }`, and the response body is used as the tool result
|
|
230
234
|
3. Results are appended and the loop continues
|
|
231
|
-
4. The loop exits when `finish_reason
|
|
235
|
+
4. The loop exits when the model stops (`finish_reason: "stop"`, for every provider), `max_turns` is reached (`"max_turns"`), or `session_budget_usd` is reached (`"session_budget_exceeded"`). If the last turn's output is truncated, the run returns with `error` set and the truncation reason (e.g. `"length"`)
|
|
232
236
|
|
|
233
|
-
Every turn is logged as a usage snapshot in the Rouva dashboard under the shared `session_id`, giving you a full session replay with per-turn cost, model used
|
|
237
|
+
Every turn is logged as a usage snapshot in the Rouva dashboard under the shared `session_id`, giving you a full session replay with per-turn cost, model used and tool calls. (Tool-loop turns aren't quality-scored.)
|
|
234
238
|
|
|
235
239
|
### Handler URL requirements
|
|
236
240
|
|
|
237
241
|
- Must be a public HTTPS URL (private/local addresses are rejected)
|
|
238
|
-
- The gateway sends a `POST` with `Content-Type: application/json` and the tool
|
|
242
|
+
- The gateway sends a `POST` with `Content-Type: application/json` and the body `{ "name": "<tool>", "arguments": { … } }`
|
|
239
243
|
- The response body (text or JSON) is returned to the model as the tool result
|
|
240
244
|
|
|
241
245
|
### Parameters
|
|
@@ -256,16 +260,21 @@ Every turn is logged as a usage snapshot in the Rouva dashboard under the shared
|
|
|
256
260
|
|
|
257
261
|
```typescript
|
|
258
262
|
interface RunResult {
|
|
259
|
-
content: string | null
|
|
260
|
-
turns: number
|
|
261
|
-
tool_calls_made: number
|
|
262
|
-
session_id: string
|
|
263
|
-
finish_reason: string | null
|
|
264
|
-
|
|
265
|
-
|
|
263
|
+
content: string | null // final model response text
|
|
264
|
+
turns: number // turns executed
|
|
265
|
+
tool_calls_made: number // total tool calls dispatched
|
|
266
|
+
session_id: string // groups all turns in the dashboard
|
|
267
|
+
finish_reason: string | null // "stop", "max_turns", "session_budget_exceeded", or a truncation reason ("length", …)
|
|
268
|
+
session_cost_usd: number // cumulative cost across all turns
|
|
269
|
+
session_cost_incomplete?: boolean // a turn reported no usage — cost is a lower bound
|
|
270
|
+
routed_model?: string // when Intelligent Routing chose a different model
|
|
271
|
+
requested_model?: string
|
|
272
|
+
error?: string // why a run that still returned 200 stopped early
|
|
266
273
|
}
|
|
267
274
|
```
|
|
268
275
|
|
|
276
|
+
A non-2xx response throws a `RouvaError` (see [Errors](#errors)). Once the loop has started, its `body` also carries the partial `turns`, `session_id` and `session_cost_usd`; errors before the first turn (validation, no provider key, allowlist) carry only `error`. A run can take several minutes — pass `{ signal }` as the second argument to cancel it.
|
|
277
|
+
|
|
269
278
|
### When to use `run()` vs `chat.completions.create()`
|
|
270
279
|
|
|
271
280
|
| | `rouva.run()` | `chat.completions.create()` |
|
|
@@ -313,10 +322,31 @@ console.log(res._rouva)
|
|
|
313
322
|
// {
|
|
314
323
|
// model_used: 'gpt-4o-mini',
|
|
315
324
|
// provider_used: 'openai',
|
|
316
|
-
// task_type: 'summarize'
|
|
325
|
+
// task_type: 'summarize',
|
|
326
|
+
// dedup: true // only when served from the duplicate-request cache
|
|
317
327
|
// }
|
|
318
328
|
```
|
|
319
329
|
|
|
330
|
+
## Errors
|
|
331
|
+
|
|
332
|
+
Any non-2xx response throws a `RouvaError` (a subclass of `Error`, so existing `catch` blocks keep working):
|
|
333
|
+
|
|
334
|
+
```typescript
|
|
335
|
+
import { Rouva, RouvaError } from '@rouvanpm/rouva'
|
|
336
|
+
|
|
337
|
+
try {
|
|
338
|
+
await rouva.chat.completions.create({ messages })
|
|
339
|
+
} catch (e) {
|
|
340
|
+
if (e instanceof RouvaError && e.status === 429) {
|
|
341
|
+
console.log(e.refusal) // 'key_cap' | 'member_cap' | 'rate' — Rouva refused before calling the provider
|
|
342
|
+
console.log(e.retryAfter) // seconds, from Retry-After
|
|
343
|
+
console.log(e.error) // the gateway's message
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
`refusal` (header `X-Rouva-Refusal`) names why Rouva itself refused the request before contacting the provider, when the gateway marks it: a monthly spend cap would be exceeded (`key_cap`, `member_cap`), the rate limit (`rate`), or — for spend-capped keys — a model with no known price (`unpriced`) or input whose cost can't be bounded (`unbounded`). Not every refusal is marked yet: a `429` with `retryAfter` but no `refusal` can still be a Rouva cap or rate-limit refusal — read `error`. Every call also takes `{ signal }` as a second argument to abort it.
|
|
349
|
+
|
|
320
350
|
## Getting your API key
|
|
321
351
|
|
|
322
352
|
1. Sign in to [app.rouva.io](https://app.rouva.io)
|
package/dist/index.d.mts
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
/** Per-call options. */
|
|
2
|
+
interface RequestOptions {
|
|
3
|
+
/** Abort the request (e.g. a long `run()` the user navigated away from). */
|
|
4
|
+
signal?: AbortSignal;
|
|
5
|
+
}
|
|
1
6
|
interface RouvaOptions {
|
|
2
7
|
/** Your Rouva gateway API key (rva_...) */
|
|
3
8
|
apiKey: string;
|
|
@@ -47,16 +52,19 @@ interface ContentBlockMessage {
|
|
|
47
52
|
type ChatMessage = Message | AssistantToolCallMessage | ToolResultMessage | ContentBlockMessage;
|
|
48
53
|
type RouvaProvider = 'anthropic' | 'openai' | 'gemini' | 'deepseek' | 'mistral' | 'moonshot' | 'xai' | 'zai' | 'alibaba' | (string & {});
|
|
49
54
|
/**
|
|
50
|
-
* Supported models across all providers.
|
|
55
|
+
* Supported models across all providers (the gateway's list as of SDK 0.3.0).
|
|
51
56
|
* Omit `model` entirely to let Rouva route to the cheapest capable model automatically.
|
|
52
57
|
*
|
|
53
|
-
* IDs
|
|
54
|
-
* (`
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
+
* Other IDs are accepted by the type for forward compatibility. On
|
|
59
|
+
* `chat.completions.create()` the gateway only serves models it can price
|
|
60
|
+
* (its built-in list plus its synced model registry); any other ID returns a
|
|
61
|
+
* 404. `run()` also accepts IDs whose provider it can infer from the name,
|
|
62
|
+
* logging them without a cost unless the key is spend-capped or the run has a
|
|
63
|
+
* `session_budget_usd` (then an unpriced model is refused).
|
|
64
|
+
*
|
|
65
|
+
* `'gpt-5.6'` is a deprecated alias the gateway maps to `'gpt-5.6-sol'`.
|
|
58
66
|
*/
|
|
59
|
-
type RouvaModel = 'claude-opus-4-
|
|
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-4.1-nano' | '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-4-1' | 'deepseek-v4-pro' | 'deepseek-v4-flash' | 'deepseek-v4-flash-think' | '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' | 'qwen-max' | 'qwen-plus' | 'qwen-turbo' | 'qwen3-235b-a22b' | 'qwen3-32b' | 'qwen3-30b-a3b' | (string & {});
|
|
60
68
|
/** Anthropic-style text block — the array form of the `system` param */
|
|
61
69
|
interface SystemTextBlock {
|
|
62
70
|
type: 'text';
|
|
@@ -72,8 +80,13 @@ interface ChatCompletionParams {
|
|
|
72
80
|
*/
|
|
73
81
|
system?: string | SystemTextBlock[];
|
|
74
82
|
/**
|
|
75
|
-
* Target model — omit to let Rouva route intelligently to the cheapest
|
|
76
|
-
*
|
|
83
|
+
* Target model — omit to let Rouva route intelligently to the cheapest
|
|
84
|
+
* capable model. With Intelligent Routing on, a named model is a price
|
|
85
|
+
* ceiling for Rouva's own choices: it may substitute a cheaper capable model,
|
|
86
|
+
* never a pricier one (retries after a provider error included). Two
|
|
87
|
+
* exceptions: an account routing override set by the workspace owner applies
|
|
88
|
+
* as policy, even if pricier; and with `provider` also set, the exact model
|
|
89
|
+
* is used (no substitution).
|
|
77
90
|
*/
|
|
78
91
|
model?: RouvaModel;
|
|
79
92
|
/**
|
|
@@ -83,23 +96,34 @@ interface ChatCompletionParams {
|
|
|
83
96
|
provider?: RouvaProvider;
|
|
84
97
|
/** Maximum tokens to generate */
|
|
85
98
|
max_tokens?: number;
|
|
99
|
+
/** OpenAI's newer name for `max_tokens`; the gateway accepts either. */
|
|
100
|
+
max_completion_tokens?: number;
|
|
101
|
+
/**
|
|
102
|
+
* Your own labels for the request, stored with its usage record and not
|
|
103
|
+
* sent to the provider: up to 10 string key-value pairs, keys up to 64
|
|
104
|
+
* characters, values up to 256 characters, no null bytes.
|
|
105
|
+
*/
|
|
106
|
+
metadata?: Record<string, string>;
|
|
86
107
|
/**
|
|
87
108
|
* Sampling temperature 0–1.
|
|
88
|
-
*
|
|
89
|
-
*
|
|
109
|
+
* The gateway omits it for models that only accept their default: OpenAI
|
|
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).
|
|
90
112
|
*/
|
|
91
113
|
temperature?: number;
|
|
92
114
|
/**
|
|
93
|
-
* Nucleus sampling — greater than 0 and at most 1
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
115
|
+
* Nucleus sampling — greater than 0 and at most 1. `top_p: 1` (the no-op
|
|
116
|
+
* default) is treated as omitted. OpenAI reasoning models (gpt-5 / gpt-6
|
|
117
|
+
* families, o-series) don't support it: routing avoids them when top_p is
|
|
118
|
+
* 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).
|
|
97
120
|
*/
|
|
98
121
|
top_p?: number;
|
|
99
122
|
/**
|
|
100
|
-
* Up to 4 stop sequences — a string or array of non-empty strings
|
|
101
|
-
*
|
|
102
|
-
* `
|
|
123
|
+
* Up to 4 stop sequences — a string or array of non-empty strings
|
|
124
|
+
* (Anthropic receives them as `stop_sequences`). OpenAI and xAI reasoning
|
|
125
|
+
* models (e.g. gpt-5, grok-4.5 / 4.6 / 4.7) reject `stop`: routing avoids
|
|
126
|
+
* them when it is set, and pinning one returns a 400.
|
|
103
127
|
*/
|
|
104
128
|
stop?: string | string[];
|
|
105
129
|
/**
|
|
@@ -117,9 +141,11 @@ interface ChatCompletionParams {
|
|
|
117
141
|
/**
|
|
118
142
|
* Tool definitions in your target provider's own format, forwarded to the
|
|
119
143
|
* provider verbatim (OpenAI `{ type: "function", function: {...} }` or
|
|
120
|
-
* Anthropic `{ name, description, input_schema }`). Requires `model
|
|
121
|
-
*
|
|
122
|
-
*
|
|
144
|
+
* Anthropic `{ name, description, input_schema }`). Requires `model`.
|
|
145
|
+
* Tool schemas are provider-specific, so a tools request only ever moves to
|
|
146
|
+
* another model of the SAME provider (a routing override, or cheaper-model
|
|
147
|
+
* routing on /v1). Models that don't support tools on Chat Completions
|
|
148
|
+
* (gpt-6.1-sol, gpt-5.6-*) return a 400.
|
|
123
149
|
*/
|
|
124
150
|
tools?: Array<Record<string, unknown>>;
|
|
125
151
|
/**
|
|
@@ -161,7 +187,16 @@ interface RouvaResponseMeta {
|
|
|
161
187
|
provider_used?: string;
|
|
162
188
|
/** Task type classified by Rouva when exposed by the gateway */
|
|
163
189
|
task_type?: string;
|
|
164
|
-
/**
|
|
190
|
+
/**
|
|
191
|
+
* True when the gateway answered from its duplicate-request cache (an
|
|
192
|
+
* identical request within the last 60 seconds) instead of calling the
|
|
193
|
+
* provider — that request wasn't charged. Header: `X-Rouva-Dedup`.
|
|
194
|
+
*/
|
|
195
|
+
dedup?: boolean;
|
|
196
|
+
/**
|
|
197
|
+
* @deprecated The semantic cache was removed from the gateway (2026-10-08),
|
|
198
|
+
* so this is never set. Use `dedup`.
|
|
199
|
+
*/
|
|
165
200
|
cache?: string;
|
|
166
201
|
}
|
|
167
202
|
interface RunParams {
|
|
@@ -186,9 +221,9 @@ interface RunParams {
|
|
|
186
221
|
tools: Array<Record<string, unknown>>;
|
|
187
222
|
/**
|
|
188
223
|
* Map of tool name → handler URL. When the model calls a tool, the gateway
|
|
189
|
-
* POSTs
|
|
190
|
-
*
|
|
191
|
-
* endpoints.
|
|
224
|
+
* POSTs `{ "name": <tool name>, "arguments": <the model's input object> }`
|
|
225
|
+
* as JSON to the corresponding URL and feeds the response body back as the
|
|
226
|
+
* tool result. All handlers must be public HTTPS endpoints.
|
|
192
227
|
*
|
|
193
228
|
* @example
|
|
194
229
|
* { search: 'https://api.example.com/search', calculator: 'https://api.example.com/calc' }
|
|
@@ -199,10 +234,9 @@ interface RunParams {
|
|
|
199
234
|
* the loop begins — the session stays on that provider for all turns. When
|
|
200
235
|
* the session history is fully translatable, routing may cross the
|
|
201
236
|
* Anthropic ↔ OpenAI-compatible boundary; the canonical layer handles tool
|
|
202
|
-
* schema translation automatically.
|
|
203
|
-
* capable model automatically.
|
|
237
|
+
* schema translation automatically. Required by /run.
|
|
204
238
|
*/
|
|
205
|
-
model
|
|
239
|
+
model: RouvaModel;
|
|
206
240
|
/**
|
|
207
241
|
* Force a specific provider when paired with `model`.
|
|
208
242
|
*/
|
|
@@ -222,8 +256,9 @@ interface RunParams {
|
|
|
222
256
|
*/
|
|
223
257
|
temperature?: number;
|
|
224
258
|
/**
|
|
225
|
-
* Hard cap on total session spend in USD. The loop exits cleanly
|
|
226
|
-
*
|
|
259
|
+
* Hard cap on total session spend in USD. The loop exits cleanly (with
|
|
260
|
+
* `finish_reason: "session_budget_exceeded"`) when the cumulative cost of
|
|
261
|
+
* all turns reaches this threshold.
|
|
227
262
|
*/
|
|
228
263
|
session_budget_usd?: number;
|
|
229
264
|
}
|
|
@@ -237,15 +272,27 @@ interface RunResult {
|
|
|
237
272
|
/** Session ID — all turns are grouped under this ID in the Rouva dashboard. */
|
|
238
273
|
session_id: string;
|
|
239
274
|
/**
|
|
240
|
-
* Why the loop exited: `"stop"` (model finished
|
|
241
|
-
*
|
|
242
|
-
*
|
|
275
|
+
* Why the loop exited: `"stop"` (the model finished — reported as "stop" for
|
|
276
|
+
* every provider), `"max_turns"` (turn limit reached),
|
|
277
|
+
* `"session_budget_exceeded"` (session_budget_usd hit), or a truncation
|
|
278
|
+
* reason such as `"length"` / `"max_tokens"` (then `error` explains it).
|
|
243
279
|
*/
|
|
244
280
|
finish_reason: string | null;
|
|
245
|
-
/** USD cost of the final turn. */
|
|
246
|
-
cost_usd: number;
|
|
247
281
|
/** Cumulative USD cost of all turns in the session. */
|
|
248
282
|
session_cost_usd: number;
|
|
283
|
+
/**
|
|
284
|
+
* True when a turn reported no usage, so `session_cost_usd` is a lower
|
|
285
|
+
* bound rather than the full cost.
|
|
286
|
+
*/
|
|
287
|
+
session_cost_incomplete?: boolean;
|
|
288
|
+
/** Set when Intelligent Routing chose a different model than requested. */
|
|
289
|
+
routed_model?: string;
|
|
290
|
+
requested_model?: string;
|
|
291
|
+
/**
|
|
292
|
+
* Explanation when the run stopped early but still returned 200 — e.g.
|
|
293
|
+
* output truncated on the last turn (raise `max_tokens`).
|
|
294
|
+
*/
|
|
295
|
+
error?: string;
|
|
249
296
|
}
|
|
250
297
|
|
|
251
298
|
declare class Rouva {
|
|
@@ -254,7 +301,7 @@ declare class Rouva {
|
|
|
254
301
|
private _sessionId;
|
|
255
302
|
readonly chat: {
|
|
256
303
|
completions: {
|
|
257
|
-
create(params: ChatCompletionParams): Promise<ChatCompletion | ReadableStream>;
|
|
304
|
+
create(params: ChatCompletionParams, options?: RequestOptions): Promise<ChatCompletion | ReadableStream>;
|
|
258
305
|
};
|
|
259
306
|
};
|
|
260
307
|
constructor(options: RouvaOptions);
|
|
@@ -273,9 +320,15 @@ declare class Rouva {
|
|
|
273
320
|
/** The active session ID, or undefined if no session is in progress. */
|
|
274
321
|
get sessionId(): string | undefined;
|
|
275
322
|
/**
|
|
276
|
-
* Run a server-side agentic loop. The gateway
|
|
277
|
-
*
|
|
278
|
-
*
|
|
323
|
+
* Run a server-side agentic loop (`POST /api/gateway/run`). The gateway
|
|
324
|
+
* calls the model, POSTs each tool call to its handler URL as
|
|
325
|
+
* `{ name, arguments }` (in parallel), feeds the response bodies back as tool
|
|
326
|
+
* results, and loops until the model stops or a limit is reached — no
|
|
327
|
+
* streaming, no client-side loop required. Throws `RouvaError` on a non-2xx
|
|
328
|
+
* response; once the loop has started, its `body` also carries the partial
|
|
329
|
+
* `turns`, `session_id` and `session_cost_usd` (errors before the first turn
|
|
330
|
+
* — validation, no key, allowlist — carry only `error`). Pass
|
|
331
|
+
* `{ signal }` to cancel; a run can take several minutes.
|
|
279
332
|
*
|
|
280
333
|
* Every turn is logged as a usage snapshot grouped by `session_id` in the
|
|
281
334
|
* Rouva dashboard, giving you a full session replay with per-turn cost,
|
|
@@ -293,8 +346,36 @@ declare class Rouva {
|
|
|
293
346
|
* console.log(result.turns) // how many turns it took
|
|
294
347
|
* console.log(result.session_id) // dashboard session ID
|
|
295
348
|
*/
|
|
296
|
-
run(params: RunParams): Promise<RunResult>;
|
|
349
|
+
run(params: RunParams, options?: RequestOptions): Promise<RunResult>;
|
|
350
|
+
private _post;
|
|
297
351
|
private _createChatCompletion;
|
|
298
352
|
}
|
|
299
353
|
|
|
300
|
-
|
|
354
|
+
/**
|
|
355
|
+
* Thrown for any non-2xx gateway response. Extends Error and keeps the
|
|
356
|
+
* message format of earlier versions (`[Rouva] Gateway error 429: …`), so
|
|
357
|
+
* existing `catch` blocks keep working; the fields make refusals actionable.
|
|
358
|
+
*/
|
|
359
|
+
declare class RouvaError extends Error {
|
|
360
|
+
/** HTTP status from the gateway. */
|
|
361
|
+
readonly status: number;
|
|
362
|
+
/**
|
|
363
|
+
* Why Rouva itself refused the request before contacting the provider, when
|
|
364
|
+
* the gateway marks it (header `X-Rouva-Refusal`): `"key_cap"` /
|
|
365
|
+
* `"member_cap"` (a monthly spend cap would be exceeded), `"rate"` (rate
|
|
366
|
+
* limit), `"unpriced"` (a capped key can't use a model with no known price),
|
|
367
|
+
* `"unbounded"` (a capped key can't send input whose cost can't be bounded).
|
|
368
|
+
* Not every refusal carries it yet — a 429 with `retryAfter` and no
|
|
369
|
+
* `refusal` can still be a Rouva cap or rate refusal; read `error`.
|
|
370
|
+
*/
|
|
371
|
+
readonly refusal?: string;
|
|
372
|
+
/** Seconds to wait before retrying, from `Retry-After`, when present. */
|
|
373
|
+
readonly retryAfter?: number;
|
|
374
|
+
/** The gateway's error message (`error` field), when the body is JSON. */
|
|
375
|
+
readonly error?: string;
|
|
376
|
+
/** The parsed JSON body, or the raw text when it isn't JSON. */
|
|
377
|
+
readonly body: unknown;
|
|
378
|
+
constructor(kind: 'Gateway' | 'Run', status: number, headers: Headers, rawBody: string);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
export { type AssistantToolCallMessage, type ChatCompletion, type ChatCompletionChoice, type ChatCompletionParams, type ChatCompletionUsage, type ChatMessage, type ContentBlockMessage, type FunctionToolCall, type Message, type RequestOptions, type ResponseMessage, Rouva, RouvaError, type RouvaModel, type RouvaOptions, type RouvaProvider, type RouvaResponseMeta, type RunParams, type RunResult, type SystemTextBlock, type ToolResultMessage };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
/** Per-call options. */
|
|
2
|
+
interface RequestOptions {
|
|
3
|
+
/** Abort the request (e.g. a long `run()` the user navigated away from). */
|
|
4
|
+
signal?: AbortSignal;
|
|
5
|
+
}
|
|
1
6
|
interface RouvaOptions {
|
|
2
7
|
/** Your Rouva gateway API key (rva_...) */
|
|
3
8
|
apiKey: string;
|
|
@@ -47,16 +52,19 @@ interface ContentBlockMessage {
|
|
|
47
52
|
type ChatMessage = Message | AssistantToolCallMessage | ToolResultMessage | ContentBlockMessage;
|
|
48
53
|
type RouvaProvider = 'anthropic' | 'openai' | 'gemini' | 'deepseek' | 'mistral' | 'moonshot' | 'xai' | 'zai' | 'alibaba' | (string & {});
|
|
49
54
|
/**
|
|
50
|
-
* Supported models across all providers.
|
|
55
|
+
* Supported models across all providers (the gateway's list as of SDK 0.3.0).
|
|
51
56
|
* Omit `model` entirely to let Rouva route to the cheapest capable model automatically.
|
|
52
57
|
*
|
|
53
|
-
* IDs
|
|
54
|
-
* (`
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
+
* Other IDs are accepted by the type for forward compatibility. On
|
|
59
|
+
* `chat.completions.create()` the gateway only serves models it can price
|
|
60
|
+
* (its built-in list plus its synced model registry); any other ID returns a
|
|
61
|
+
* 404. `run()` also accepts IDs whose provider it can infer from the name,
|
|
62
|
+
* logging them without a cost unless the key is spend-capped or the run has a
|
|
63
|
+
* `session_budget_usd` (then an unpriced model is refused).
|
|
64
|
+
*
|
|
65
|
+
* `'gpt-5.6'` is a deprecated alias the gateway maps to `'gpt-5.6-sol'`.
|
|
58
66
|
*/
|
|
59
|
-
type RouvaModel = 'claude-opus-4-
|
|
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-4.1-nano' | '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-4-1' | 'deepseek-v4-pro' | 'deepseek-v4-flash' | 'deepseek-v4-flash-think' | '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' | 'qwen-max' | 'qwen-plus' | 'qwen-turbo' | 'qwen3-235b-a22b' | 'qwen3-32b' | 'qwen3-30b-a3b' | (string & {});
|
|
60
68
|
/** Anthropic-style text block — the array form of the `system` param */
|
|
61
69
|
interface SystemTextBlock {
|
|
62
70
|
type: 'text';
|
|
@@ -72,8 +80,13 @@ interface ChatCompletionParams {
|
|
|
72
80
|
*/
|
|
73
81
|
system?: string | SystemTextBlock[];
|
|
74
82
|
/**
|
|
75
|
-
* Target model — omit to let Rouva route intelligently to the cheapest
|
|
76
|
-
*
|
|
83
|
+
* Target model — omit to let Rouva route intelligently to the cheapest
|
|
84
|
+
* capable model. With Intelligent Routing on, a named model is a price
|
|
85
|
+
* ceiling for Rouva's own choices: it may substitute a cheaper capable model,
|
|
86
|
+
* never a pricier one (retries after a provider error included). Two
|
|
87
|
+
* exceptions: an account routing override set by the workspace owner applies
|
|
88
|
+
* as policy, even if pricier; and with `provider` also set, the exact model
|
|
89
|
+
* is used (no substitution).
|
|
77
90
|
*/
|
|
78
91
|
model?: RouvaModel;
|
|
79
92
|
/**
|
|
@@ -83,23 +96,34 @@ interface ChatCompletionParams {
|
|
|
83
96
|
provider?: RouvaProvider;
|
|
84
97
|
/** Maximum tokens to generate */
|
|
85
98
|
max_tokens?: number;
|
|
99
|
+
/** OpenAI's newer name for `max_tokens`; the gateway accepts either. */
|
|
100
|
+
max_completion_tokens?: number;
|
|
101
|
+
/**
|
|
102
|
+
* Your own labels for the request, stored with its usage record and not
|
|
103
|
+
* sent to the provider: up to 10 string key-value pairs, keys up to 64
|
|
104
|
+
* characters, values up to 256 characters, no null bytes.
|
|
105
|
+
*/
|
|
106
|
+
metadata?: Record<string, string>;
|
|
86
107
|
/**
|
|
87
108
|
* Sampling temperature 0–1.
|
|
88
|
-
*
|
|
89
|
-
*
|
|
109
|
+
* The gateway omits it for models that only accept their default: OpenAI
|
|
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).
|
|
90
112
|
*/
|
|
91
113
|
temperature?: number;
|
|
92
114
|
/**
|
|
93
|
-
* Nucleus sampling — greater than 0 and at most 1
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
115
|
+
* Nucleus sampling — greater than 0 and at most 1. `top_p: 1` (the no-op
|
|
116
|
+
* default) is treated as omitted. OpenAI reasoning models (gpt-5 / gpt-6
|
|
117
|
+
* families, o-series) don't support it: routing avoids them when top_p is
|
|
118
|
+
* 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).
|
|
97
120
|
*/
|
|
98
121
|
top_p?: number;
|
|
99
122
|
/**
|
|
100
|
-
* Up to 4 stop sequences — a string or array of non-empty strings
|
|
101
|
-
*
|
|
102
|
-
* `
|
|
123
|
+
* Up to 4 stop sequences — a string or array of non-empty strings
|
|
124
|
+
* (Anthropic receives them as `stop_sequences`). OpenAI and xAI reasoning
|
|
125
|
+
* models (e.g. gpt-5, grok-4.5 / 4.6 / 4.7) reject `stop`: routing avoids
|
|
126
|
+
* them when it is set, and pinning one returns a 400.
|
|
103
127
|
*/
|
|
104
128
|
stop?: string | string[];
|
|
105
129
|
/**
|
|
@@ -117,9 +141,11 @@ interface ChatCompletionParams {
|
|
|
117
141
|
/**
|
|
118
142
|
* Tool definitions in your target provider's own format, forwarded to the
|
|
119
143
|
* provider verbatim (OpenAI `{ type: "function", function: {...} }` or
|
|
120
|
-
* Anthropic `{ name, description, input_schema }`). Requires `model
|
|
121
|
-
*
|
|
122
|
-
*
|
|
144
|
+
* Anthropic `{ name, description, input_schema }`). Requires `model`.
|
|
145
|
+
* Tool schemas are provider-specific, so a tools request only ever moves to
|
|
146
|
+
* another model of the SAME provider (a routing override, or cheaper-model
|
|
147
|
+
* routing on /v1). Models that don't support tools on Chat Completions
|
|
148
|
+
* (gpt-6.1-sol, gpt-5.6-*) return a 400.
|
|
123
149
|
*/
|
|
124
150
|
tools?: Array<Record<string, unknown>>;
|
|
125
151
|
/**
|
|
@@ -161,7 +187,16 @@ interface RouvaResponseMeta {
|
|
|
161
187
|
provider_used?: string;
|
|
162
188
|
/** Task type classified by Rouva when exposed by the gateway */
|
|
163
189
|
task_type?: string;
|
|
164
|
-
/**
|
|
190
|
+
/**
|
|
191
|
+
* True when the gateway answered from its duplicate-request cache (an
|
|
192
|
+
* identical request within the last 60 seconds) instead of calling the
|
|
193
|
+
* provider — that request wasn't charged. Header: `X-Rouva-Dedup`.
|
|
194
|
+
*/
|
|
195
|
+
dedup?: boolean;
|
|
196
|
+
/**
|
|
197
|
+
* @deprecated The semantic cache was removed from the gateway (2026-10-08),
|
|
198
|
+
* so this is never set. Use `dedup`.
|
|
199
|
+
*/
|
|
165
200
|
cache?: string;
|
|
166
201
|
}
|
|
167
202
|
interface RunParams {
|
|
@@ -186,9 +221,9 @@ interface RunParams {
|
|
|
186
221
|
tools: Array<Record<string, unknown>>;
|
|
187
222
|
/**
|
|
188
223
|
* Map of tool name → handler URL. When the model calls a tool, the gateway
|
|
189
|
-
* POSTs
|
|
190
|
-
*
|
|
191
|
-
* endpoints.
|
|
224
|
+
* POSTs `{ "name": <tool name>, "arguments": <the model's input object> }`
|
|
225
|
+
* as JSON to the corresponding URL and feeds the response body back as the
|
|
226
|
+
* tool result. All handlers must be public HTTPS endpoints.
|
|
192
227
|
*
|
|
193
228
|
* @example
|
|
194
229
|
* { search: 'https://api.example.com/search', calculator: 'https://api.example.com/calc' }
|
|
@@ -199,10 +234,9 @@ interface RunParams {
|
|
|
199
234
|
* the loop begins — the session stays on that provider for all turns. When
|
|
200
235
|
* the session history is fully translatable, routing may cross the
|
|
201
236
|
* Anthropic ↔ OpenAI-compatible boundary; the canonical layer handles tool
|
|
202
|
-
* schema translation automatically.
|
|
203
|
-
* capable model automatically.
|
|
237
|
+
* schema translation automatically. Required by /run.
|
|
204
238
|
*/
|
|
205
|
-
model
|
|
239
|
+
model: RouvaModel;
|
|
206
240
|
/**
|
|
207
241
|
* Force a specific provider when paired with `model`.
|
|
208
242
|
*/
|
|
@@ -222,8 +256,9 @@ interface RunParams {
|
|
|
222
256
|
*/
|
|
223
257
|
temperature?: number;
|
|
224
258
|
/**
|
|
225
|
-
* Hard cap on total session spend in USD. The loop exits cleanly
|
|
226
|
-
*
|
|
259
|
+
* Hard cap on total session spend in USD. The loop exits cleanly (with
|
|
260
|
+
* `finish_reason: "session_budget_exceeded"`) when the cumulative cost of
|
|
261
|
+
* all turns reaches this threshold.
|
|
227
262
|
*/
|
|
228
263
|
session_budget_usd?: number;
|
|
229
264
|
}
|
|
@@ -237,15 +272,27 @@ interface RunResult {
|
|
|
237
272
|
/** Session ID — all turns are grouped under this ID in the Rouva dashboard. */
|
|
238
273
|
session_id: string;
|
|
239
274
|
/**
|
|
240
|
-
* Why the loop exited: `"stop"` (model finished
|
|
241
|
-
*
|
|
242
|
-
*
|
|
275
|
+
* Why the loop exited: `"stop"` (the model finished — reported as "stop" for
|
|
276
|
+
* every provider), `"max_turns"` (turn limit reached),
|
|
277
|
+
* `"session_budget_exceeded"` (session_budget_usd hit), or a truncation
|
|
278
|
+
* reason such as `"length"` / `"max_tokens"` (then `error` explains it).
|
|
243
279
|
*/
|
|
244
280
|
finish_reason: string | null;
|
|
245
|
-
/** USD cost of the final turn. */
|
|
246
|
-
cost_usd: number;
|
|
247
281
|
/** Cumulative USD cost of all turns in the session. */
|
|
248
282
|
session_cost_usd: number;
|
|
283
|
+
/**
|
|
284
|
+
* True when a turn reported no usage, so `session_cost_usd` is a lower
|
|
285
|
+
* bound rather than the full cost.
|
|
286
|
+
*/
|
|
287
|
+
session_cost_incomplete?: boolean;
|
|
288
|
+
/** Set when Intelligent Routing chose a different model than requested. */
|
|
289
|
+
routed_model?: string;
|
|
290
|
+
requested_model?: string;
|
|
291
|
+
/**
|
|
292
|
+
* Explanation when the run stopped early but still returned 200 — e.g.
|
|
293
|
+
* output truncated on the last turn (raise `max_tokens`).
|
|
294
|
+
*/
|
|
295
|
+
error?: string;
|
|
249
296
|
}
|
|
250
297
|
|
|
251
298
|
declare class Rouva {
|
|
@@ -254,7 +301,7 @@ declare class Rouva {
|
|
|
254
301
|
private _sessionId;
|
|
255
302
|
readonly chat: {
|
|
256
303
|
completions: {
|
|
257
|
-
create(params: ChatCompletionParams): Promise<ChatCompletion | ReadableStream>;
|
|
304
|
+
create(params: ChatCompletionParams, options?: RequestOptions): Promise<ChatCompletion | ReadableStream>;
|
|
258
305
|
};
|
|
259
306
|
};
|
|
260
307
|
constructor(options: RouvaOptions);
|
|
@@ -273,9 +320,15 @@ declare class Rouva {
|
|
|
273
320
|
/** The active session ID, or undefined if no session is in progress. */
|
|
274
321
|
get sessionId(): string | undefined;
|
|
275
322
|
/**
|
|
276
|
-
* Run a server-side agentic loop. The gateway
|
|
277
|
-
*
|
|
278
|
-
*
|
|
323
|
+
* Run a server-side agentic loop (`POST /api/gateway/run`). The gateway
|
|
324
|
+
* calls the model, POSTs each tool call to its handler URL as
|
|
325
|
+
* `{ name, arguments }` (in parallel), feeds the response bodies back as tool
|
|
326
|
+
* results, and loops until the model stops or a limit is reached — no
|
|
327
|
+
* streaming, no client-side loop required. Throws `RouvaError` on a non-2xx
|
|
328
|
+
* response; once the loop has started, its `body` also carries the partial
|
|
329
|
+
* `turns`, `session_id` and `session_cost_usd` (errors before the first turn
|
|
330
|
+
* — validation, no key, allowlist — carry only `error`). Pass
|
|
331
|
+
* `{ signal }` to cancel; a run can take several minutes.
|
|
279
332
|
*
|
|
280
333
|
* Every turn is logged as a usage snapshot grouped by `session_id` in the
|
|
281
334
|
* Rouva dashboard, giving you a full session replay with per-turn cost,
|
|
@@ -293,8 +346,36 @@ declare class Rouva {
|
|
|
293
346
|
* console.log(result.turns) // how many turns it took
|
|
294
347
|
* console.log(result.session_id) // dashboard session ID
|
|
295
348
|
*/
|
|
296
|
-
run(params: RunParams): Promise<RunResult>;
|
|
349
|
+
run(params: RunParams, options?: RequestOptions): Promise<RunResult>;
|
|
350
|
+
private _post;
|
|
297
351
|
private _createChatCompletion;
|
|
298
352
|
}
|
|
299
353
|
|
|
300
|
-
|
|
354
|
+
/**
|
|
355
|
+
* Thrown for any non-2xx gateway response. Extends Error and keeps the
|
|
356
|
+
* message format of earlier versions (`[Rouva] Gateway error 429: …`), so
|
|
357
|
+
* existing `catch` blocks keep working; the fields make refusals actionable.
|
|
358
|
+
*/
|
|
359
|
+
declare class RouvaError extends Error {
|
|
360
|
+
/** HTTP status from the gateway. */
|
|
361
|
+
readonly status: number;
|
|
362
|
+
/**
|
|
363
|
+
* Why Rouva itself refused the request before contacting the provider, when
|
|
364
|
+
* the gateway marks it (header `X-Rouva-Refusal`): `"key_cap"` /
|
|
365
|
+
* `"member_cap"` (a monthly spend cap would be exceeded), `"rate"` (rate
|
|
366
|
+
* limit), `"unpriced"` (a capped key can't use a model with no known price),
|
|
367
|
+
* `"unbounded"` (a capped key can't send input whose cost can't be bounded).
|
|
368
|
+
* Not every refusal carries it yet — a 429 with `retryAfter` and no
|
|
369
|
+
* `refusal` can still be a Rouva cap or rate refusal; read `error`.
|
|
370
|
+
*/
|
|
371
|
+
readonly refusal?: string;
|
|
372
|
+
/** Seconds to wait before retrying, from `Retry-After`, when present. */
|
|
373
|
+
readonly retryAfter?: number;
|
|
374
|
+
/** The gateway's error message (`error` field), when the body is JSON. */
|
|
375
|
+
readonly error?: string;
|
|
376
|
+
/** The parsed JSON body, or the raw text when it isn't JSON. */
|
|
377
|
+
readonly body: unknown;
|
|
378
|
+
constructor(kind: 'Gateway' | 'Run', status: number, headers: Headers, rawBody: string);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
export { type AssistantToolCallMessage, type ChatCompletion, type ChatCompletionChoice, type ChatCompletionParams, type ChatCompletionUsage, type ChatMessage, type ContentBlockMessage, type FunctionToolCall, type Message, type RequestOptions, type ResponseMessage, Rouva, RouvaError, type RouvaModel, type RouvaOptions, type RouvaProvider, type RouvaResponseMeta, type RunParams, type RunResult, type SystemTextBlock, type ToolResultMessage };
|
package/dist/index.js
CHANGED
|
@@ -20,7 +20,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/index.ts
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
|
-
Rouva: () => Rouva
|
|
23
|
+
Rouva: () => Rouva,
|
|
24
|
+
RouvaError: () => RouvaError
|
|
24
25
|
});
|
|
25
26
|
module.exports = __toCommonJS(index_exports);
|
|
26
27
|
|
|
@@ -246,8 +247,9 @@ function normalizeGatewayStream(stream) {
|
|
|
246
247
|
}
|
|
247
248
|
}
|
|
248
249
|
}
|
|
249
|
-
} finally {
|
|
250
250
|
controller.close();
|
|
251
|
+
} catch (err) {
|
|
252
|
+
controller.error(err);
|
|
251
253
|
}
|
|
252
254
|
}
|
|
253
255
|
});
|
|
@@ -354,19 +356,47 @@ async function readChatCompletionFromSse(stream, metadata) {
|
|
|
354
356
|
return response;
|
|
355
357
|
}
|
|
356
358
|
function getRouvaMetadata(headers) {
|
|
357
|
-
const
|
|
358
|
-
const
|
|
359
|
-
const
|
|
360
|
-
const
|
|
361
|
-
|
|
359
|
+
const read = (name) => headers.get(name) || void 0;
|
|
360
|
+
const modelUsed = read("x-rouva-model");
|
|
361
|
+
const providerUsed = read("x-rouva-provider");
|
|
362
|
+
const taskType = read("x-rouva-task");
|
|
363
|
+
const dedup = headers.get("x-rouva-dedup") === "1" ? true : void 0;
|
|
364
|
+
if (!modelUsed && !providerUsed && !taskType && !dedup) return void 0;
|
|
362
365
|
return {
|
|
363
366
|
model_used: modelUsed,
|
|
364
367
|
provider_used: providerUsed,
|
|
365
368
|
task_type: taskType,
|
|
366
|
-
|
|
369
|
+
...dedup ? { dedup } : {}
|
|
367
370
|
};
|
|
368
371
|
}
|
|
369
372
|
|
|
373
|
+
// src/errors.ts
|
|
374
|
+
var RouvaError = class extends Error {
|
|
375
|
+
constructor(kind, status, headers, rawBody) {
|
|
376
|
+
super(`[Rouva] ${kind} error ${status}: ${rawBody}`);
|
|
377
|
+
this.name = "RouvaError";
|
|
378
|
+
this.status = status;
|
|
379
|
+
this.refusal = headers.get("x-rouva-refusal") ?? void 0;
|
|
380
|
+
const retry = Number(headers.get("retry-after"));
|
|
381
|
+
this.retryAfter = headers.get("retry-after") !== null && Number.isFinite(retry) ? retry : void 0;
|
|
382
|
+
let parsed = rawBody;
|
|
383
|
+
try {
|
|
384
|
+
parsed = JSON.parse(rawBody);
|
|
385
|
+
} catch {
|
|
386
|
+
}
|
|
387
|
+
this.body = parsed;
|
|
388
|
+
const e = parsed?.error;
|
|
389
|
+
this.error = typeof e === "string" ? e : void 0;
|
|
390
|
+
}
|
|
391
|
+
};
|
|
392
|
+
async function toRouvaError(kind, res) {
|
|
393
|
+
const raw = await res.text().catch((err) => {
|
|
394
|
+
if (err?.name === "AbortError") throw err;
|
|
395
|
+
return res.statusText;
|
|
396
|
+
});
|
|
397
|
+
return new RouvaError(kind, res.status, res.headers, raw);
|
|
398
|
+
}
|
|
399
|
+
|
|
370
400
|
// src/client.ts
|
|
371
401
|
var DEFAULT_BASE_URL = "https://app.rouva.io";
|
|
372
402
|
var Rouva = class {
|
|
@@ -379,7 +409,7 @@ var Rouva = class {
|
|
|
379
409
|
this.baseURL = (options.baseURL ?? DEFAULT_BASE_URL).replace(/\/$/, "");
|
|
380
410
|
this.chat = {
|
|
381
411
|
completions: {
|
|
382
|
-
create: (params) => this._createChatCompletion(params)
|
|
412
|
+
create: (params, options2) => this._createChatCompletion(params, options2)
|
|
383
413
|
}
|
|
384
414
|
};
|
|
385
415
|
}
|
|
@@ -405,9 +435,15 @@ var Rouva = class {
|
|
|
405
435
|
return this._sessionId;
|
|
406
436
|
}
|
|
407
437
|
/**
|
|
408
|
-
* Run a server-side agentic loop. The gateway
|
|
409
|
-
*
|
|
410
|
-
*
|
|
438
|
+
* Run a server-side agentic loop (`POST /api/gateway/run`). The gateway
|
|
439
|
+
* calls the model, POSTs each tool call to its handler URL as
|
|
440
|
+
* `{ name, arguments }` (in parallel), feeds the response bodies back as tool
|
|
441
|
+
* results, and loops until the model stops or a limit is reached — no
|
|
442
|
+
* streaming, no client-side loop required. Throws `RouvaError` on a non-2xx
|
|
443
|
+
* response; once the loop has started, its `body` also carries the partial
|
|
444
|
+
* `turns`, `session_id` and `session_cost_usd` (errors before the first turn
|
|
445
|
+
* — validation, no key, allowlist — carry only `error`). Pass
|
|
446
|
+
* `{ signal }` to cancel; a run can take several minutes.
|
|
411
447
|
*
|
|
412
448
|
* Every turn is logged as a usage snapshot grouped by `session_id` in the
|
|
413
449
|
* Rouva dashboard, giving you a full session replay with per-turn cost,
|
|
@@ -425,39 +461,29 @@ var Rouva = class {
|
|
|
425
461
|
* console.log(result.turns) // how many turns it took
|
|
426
462
|
* console.log(result.session_id) // dashboard session ID
|
|
427
463
|
*/
|
|
428
|
-
async run(params) {
|
|
429
|
-
const
|
|
430
|
-
|
|
464
|
+
async run(params, options) {
|
|
465
|
+
const res = await this._post("/api/gateway/run", params, {}, options);
|
|
466
|
+
if (!res.ok) throw await toRouvaError("Run", res);
|
|
467
|
+
return res.json();
|
|
468
|
+
}
|
|
469
|
+
// One request path for every endpoint: auth, JSON body, optional abort signal.
|
|
470
|
+
_post(path, body, extraHeaders, options) {
|
|
471
|
+
return fetch(`${this.baseURL}${path}`, {
|
|
431
472
|
method: "POST",
|
|
432
473
|
headers: {
|
|
433
474
|
"Content-Type": "application/json",
|
|
434
|
-
"Authorization": `Bearer ${this.apiKey}
|
|
475
|
+
"Authorization": `Bearer ${this.apiKey}`,
|
|
476
|
+
...extraHeaders
|
|
435
477
|
},
|
|
436
|
-
body: JSON.stringify(
|
|
478
|
+
body: JSON.stringify(body),
|
|
479
|
+
...options?.signal ? { signal: options.signal } : {}
|
|
437
480
|
});
|
|
438
|
-
if (!res.ok) {
|
|
439
|
-
const body = await res.text().catch(() => res.statusText);
|
|
440
|
-
throw new Error(`[Rouva] Run error ${res.status}: ${body}`);
|
|
441
|
-
}
|
|
442
|
-
return res.json();
|
|
443
481
|
}
|
|
444
|
-
async _createChatCompletion(params) {
|
|
445
|
-
const url = `${this.baseURL}/api/gateway/messages`;
|
|
482
|
+
async _createChatCompletion(params, options) {
|
|
446
483
|
const { stream: _stream, ...body } = params;
|
|
447
|
-
const
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
};
|
|
451
|
-
if (this._sessionId) headers["x-rouva-session-id"] = this._sessionId;
|
|
452
|
-
const res = await fetch(url, {
|
|
453
|
-
method: "POST",
|
|
454
|
-
headers,
|
|
455
|
-
body: JSON.stringify(body)
|
|
456
|
-
});
|
|
457
|
-
if (!res.ok) {
|
|
458
|
-
const body2 = await res.text().catch(() => res.statusText);
|
|
459
|
-
throw new Error(`[Rouva] Gateway error ${res.status}: ${body2}`);
|
|
460
|
-
}
|
|
484
|
+
const sessionHeader = this._sessionId ? { "x-rouva-session-id": this._sessionId } : {};
|
|
485
|
+
const res = await this._post("/api/gateway/messages", body, sessionHeader, options);
|
|
486
|
+
if (!res.ok) throw await toRouvaError("Gateway", res);
|
|
461
487
|
if (!res.body) throw new Error("[Rouva] No response body from gateway");
|
|
462
488
|
const normalizedStream = normalizeGatewayStream(res.body);
|
|
463
489
|
if (params.stream) return normalizedStream;
|
|
@@ -466,5 +492,6 @@ var Rouva = class {
|
|
|
466
492
|
};
|
|
467
493
|
// Annotate the CommonJS export names for ESM import in node:
|
|
468
494
|
0 && (module.exports = {
|
|
469
|
-
Rouva
|
|
495
|
+
Rouva,
|
|
496
|
+
RouvaError
|
|
470
497
|
});
|
package/dist/index.mjs
CHANGED
|
@@ -220,8 +220,9 @@ function normalizeGatewayStream(stream) {
|
|
|
220
220
|
}
|
|
221
221
|
}
|
|
222
222
|
}
|
|
223
|
-
} finally {
|
|
224
223
|
controller.close();
|
|
224
|
+
} catch (err) {
|
|
225
|
+
controller.error(err);
|
|
225
226
|
}
|
|
226
227
|
}
|
|
227
228
|
});
|
|
@@ -328,19 +329,47 @@ async function readChatCompletionFromSse(stream, metadata) {
|
|
|
328
329
|
return response;
|
|
329
330
|
}
|
|
330
331
|
function getRouvaMetadata(headers) {
|
|
331
|
-
const
|
|
332
|
-
const
|
|
333
|
-
const
|
|
334
|
-
const
|
|
335
|
-
|
|
332
|
+
const read = (name) => headers.get(name) || void 0;
|
|
333
|
+
const modelUsed = read("x-rouva-model");
|
|
334
|
+
const providerUsed = read("x-rouva-provider");
|
|
335
|
+
const taskType = read("x-rouva-task");
|
|
336
|
+
const dedup = headers.get("x-rouva-dedup") === "1" ? true : void 0;
|
|
337
|
+
if (!modelUsed && !providerUsed && !taskType && !dedup) return void 0;
|
|
336
338
|
return {
|
|
337
339
|
model_used: modelUsed,
|
|
338
340
|
provider_used: providerUsed,
|
|
339
341
|
task_type: taskType,
|
|
340
|
-
|
|
342
|
+
...dedup ? { dedup } : {}
|
|
341
343
|
};
|
|
342
344
|
}
|
|
343
345
|
|
|
346
|
+
// src/errors.ts
|
|
347
|
+
var RouvaError = class extends Error {
|
|
348
|
+
constructor(kind, status, headers, rawBody) {
|
|
349
|
+
super(`[Rouva] ${kind} error ${status}: ${rawBody}`);
|
|
350
|
+
this.name = "RouvaError";
|
|
351
|
+
this.status = status;
|
|
352
|
+
this.refusal = headers.get("x-rouva-refusal") ?? void 0;
|
|
353
|
+
const retry = Number(headers.get("retry-after"));
|
|
354
|
+
this.retryAfter = headers.get("retry-after") !== null && Number.isFinite(retry) ? retry : void 0;
|
|
355
|
+
let parsed = rawBody;
|
|
356
|
+
try {
|
|
357
|
+
parsed = JSON.parse(rawBody);
|
|
358
|
+
} catch {
|
|
359
|
+
}
|
|
360
|
+
this.body = parsed;
|
|
361
|
+
const e = parsed?.error;
|
|
362
|
+
this.error = typeof e === "string" ? e : void 0;
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
async function toRouvaError(kind, res) {
|
|
366
|
+
const raw = await res.text().catch((err) => {
|
|
367
|
+
if (err?.name === "AbortError") throw err;
|
|
368
|
+
return res.statusText;
|
|
369
|
+
});
|
|
370
|
+
return new RouvaError(kind, res.status, res.headers, raw);
|
|
371
|
+
}
|
|
372
|
+
|
|
344
373
|
// src/client.ts
|
|
345
374
|
var DEFAULT_BASE_URL = "https://app.rouva.io";
|
|
346
375
|
var Rouva = class {
|
|
@@ -353,7 +382,7 @@ var Rouva = class {
|
|
|
353
382
|
this.baseURL = (options.baseURL ?? DEFAULT_BASE_URL).replace(/\/$/, "");
|
|
354
383
|
this.chat = {
|
|
355
384
|
completions: {
|
|
356
|
-
create: (params) => this._createChatCompletion(params)
|
|
385
|
+
create: (params, options2) => this._createChatCompletion(params, options2)
|
|
357
386
|
}
|
|
358
387
|
};
|
|
359
388
|
}
|
|
@@ -379,9 +408,15 @@ var Rouva = class {
|
|
|
379
408
|
return this._sessionId;
|
|
380
409
|
}
|
|
381
410
|
/**
|
|
382
|
-
* Run a server-side agentic loop. The gateway
|
|
383
|
-
*
|
|
384
|
-
*
|
|
411
|
+
* Run a server-side agentic loop (`POST /api/gateway/run`). The gateway
|
|
412
|
+
* calls the model, POSTs each tool call to its handler URL as
|
|
413
|
+
* `{ name, arguments }` (in parallel), feeds the response bodies back as tool
|
|
414
|
+
* results, and loops until the model stops or a limit is reached — no
|
|
415
|
+
* streaming, no client-side loop required. Throws `RouvaError` on a non-2xx
|
|
416
|
+
* response; once the loop has started, its `body` also carries the partial
|
|
417
|
+
* `turns`, `session_id` and `session_cost_usd` (errors before the first turn
|
|
418
|
+
* — validation, no key, allowlist — carry only `error`). Pass
|
|
419
|
+
* `{ signal }` to cancel; a run can take several minutes.
|
|
385
420
|
*
|
|
386
421
|
* Every turn is logged as a usage snapshot grouped by `session_id` in the
|
|
387
422
|
* Rouva dashboard, giving you a full session replay with per-turn cost,
|
|
@@ -399,39 +434,29 @@ var Rouva = class {
|
|
|
399
434
|
* console.log(result.turns) // how many turns it took
|
|
400
435
|
* console.log(result.session_id) // dashboard session ID
|
|
401
436
|
*/
|
|
402
|
-
async run(params) {
|
|
403
|
-
const
|
|
404
|
-
|
|
437
|
+
async run(params, options) {
|
|
438
|
+
const res = await this._post("/api/gateway/run", params, {}, options);
|
|
439
|
+
if (!res.ok) throw await toRouvaError("Run", res);
|
|
440
|
+
return res.json();
|
|
441
|
+
}
|
|
442
|
+
// One request path for every endpoint: auth, JSON body, optional abort signal.
|
|
443
|
+
_post(path, body, extraHeaders, options) {
|
|
444
|
+
return fetch(`${this.baseURL}${path}`, {
|
|
405
445
|
method: "POST",
|
|
406
446
|
headers: {
|
|
407
447
|
"Content-Type": "application/json",
|
|
408
|
-
"Authorization": `Bearer ${this.apiKey}
|
|
448
|
+
"Authorization": `Bearer ${this.apiKey}`,
|
|
449
|
+
...extraHeaders
|
|
409
450
|
},
|
|
410
|
-
body: JSON.stringify(
|
|
451
|
+
body: JSON.stringify(body),
|
|
452
|
+
...options?.signal ? { signal: options.signal } : {}
|
|
411
453
|
});
|
|
412
|
-
if (!res.ok) {
|
|
413
|
-
const body = await res.text().catch(() => res.statusText);
|
|
414
|
-
throw new Error(`[Rouva] Run error ${res.status}: ${body}`);
|
|
415
|
-
}
|
|
416
|
-
return res.json();
|
|
417
454
|
}
|
|
418
|
-
async _createChatCompletion(params) {
|
|
419
|
-
const url = `${this.baseURL}/api/gateway/messages`;
|
|
455
|
+
async _createChatCompletion(params, options) {
|
|
420
456
|
const { stream: _stream, ...body } = params;
|
|
421
|
-
const
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
};
|
|
425
|
-
if (this._sessionId) headers["x-rouva-session-id"] = this._sessionId;
|
|
426
|
-
const res = await fetch(url, {
|
|
427
|
-
method: "POST",
|
|
428
|
-
headers,
|
|
429
|
-
body: JSON.stringify(body)
|
|
430
|
-
});
|
|
431
|
-
if (!res.ok) {
|
|
432
|
-
const body2 = await res.text().catch(() => res.statusText);
|
|
433
|
-
throw new Error(`[Rouva] Gateway error ${res.status}: ${body2}`);
|
|
434
|
-
}
|
|
457
|
+
const sessionHeader = this._sessionId ? { "x-rouva-session-id": this._sessionId } : {};
|
|
458
|
+
const res = await this._post("/api/gateway/messages", body, sessionHeader, options);
|
|
459
|
+
if (!res.ok) throw await toRouvaError("Gateway", res);
|
|
435
460
|
if (!res.body) throw new Error("[Rouva] No response body from gateway");
|
|
436
461
|
const normalizedStream = normalizeGatewayStream(res.body);
|
|
437
462
|
if (params.stream) return normalizedStream;
|
|
@@ -439,5 +464,6 @@ var Rouva = class {
|
|
|
439
464
|
}
|
|
440
465
|
};
|
|
441
466
|
export {
|
|
442
|
-
Rouva
|
|
467
|
+
Rouva,
|
|
468
|
+
RouvaError
|
|
443
469
|
};
|
package/package.json
CHANGED