@rouvanpm/rouva 0.3.1 → 0.4.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 +57 -8
- package/dist/index.d.mts +143 -10
- package/dist/index.d.ts +143 -10
- package/dist/index.js +46 -16
- package/dist/index.mjs +46 -16
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -80,13 +80,13 @@ while (true) {
|
|
|
80
80
|
|
|
81
81
|
Streaming responses are normalized to OpenAI-style SSE chunks, even when Rouva routes the request to Anthropic.
|
|
82
82
|
|
|
83
|
-
`stream` is a client-side toggle: `stream: true` returns the raw `ReadableStream
|
|
83
|
+
`stream` is a client-side toggle: `stream: true` returns the raw `ReadableStream` (with the routing metadata on `_rouva` — see [Response metadata](#response-metadata)), omitting it (or `stream: false`) returns a buffered `ChatCompletion`. It is never sent to the gateway.
|
|
84
84
|
|
|
85
85
|
## Request parameters
|
|
86
86
|
|
|
87
87
|
```typescript
|
|
88
88
|
const res = await rouva.chat.completions.create({
|
|
89
|
-
model: 'gpt-
|
|
89
|
+
model: 'gpt-6-luna', // 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
92
|
max_tokens: 1024, // default 4096 (or max_completion_tokens)
|
|
@@ -104,8 +104,9 @@ const res = await rouva.chat.completions.create({
|
|
|
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
|
+
- **`response_format`** — JSON mode or a JSON schema; see [Structured output](#structured-output).
|
|
107
108
|
|
|
108
|
-
Unsupported OpenAI options (`
|
|
109
|
+
Unsupported OpenAI options (`logit_bias`, `logprobs`, `reasoning_effort`, `n > 1`, the legacy `functions`/`function_call` fields, …) are rejected by the gateway with an explicit 400 rather than silently ignored.
|
|
109
110
|
|
|
110
111
|
## Unlisted models
|
|
111
112
|
|
|
@@ -313,20 +314,67 @@ Sessions are optional — requests without an active session are tracked individ
|
|
|
313
314
|
|
|
314
315
|
## Response metadata
|
|
315
316
|
|
|
316
|
-
|
|
317
|
+
Every response says what Rouva decided, in `_rouva` — on a buffered completion, and on the stream `stream: true` returns (the headers arrive before the first chunk):
|
|
317
318
|
|
|
318
319
|
```typescript
|
|
319
320
|
const res = await rouva.chat.completions.create({ messages })
|
|
320
321
|
|
|
321
322
|
console.log(res._rouva)
|
|
322
323
|
// {
|
|
323
|
-
//
|
|
324
|
+
// request_id: '7c0e…', // your request log's id for it
|
|
325
|
+
// model_used: 'gpt-6-luna',
|
|
324
326
|
// provider_used: 'openai',
|
|
327
|
+
// requested_model: 'gpt-6-sol', // only when Rouva served a different model
|
|
325
328
|
// task_type: 'summarize',
|
|
326
|
-
//
|
|
329
|
+
// route_reason: 'intelligent', // override | allowlist | fallback | no_key_fallback | direct | escalation | dedup
|
|
330
|
+
// dedup: true // only when served from the duplicate-request cache
|
|
327
331
|
// }
|
|
328
332
|
```
|
|
329
333
|
|
|
334
|
+
### Cost, savings and quality score — `rouva.requests.get(id)`
|
|
335
|
+
|
|
336
|
+
A request's cost is known only once its last chunk has arrived, and its quality score once the judge has run, so read them back by `request_id`:
|
|
337
|
+
|
|
338
|
+
```typescript
|
|
339
|
+
import type { RouvaStream } from '@rouvanpm/rouva'
|
|
340
|
+
|
|
341
|
+
const stream = await rouva.chat.completions.create({ messages, stream: true }) as RouvaStream
|
|
342
|
+
// ...consume the stream...
|
|
343
|
+
|
|
344
|
+
const id = stream._rouva?.request_id // absent on a duplicate-cache replay (cost 0)
|
|
345
|
+
if (id) {
|
|
346
|
+
const req = await rouva.requests.get(id)
|
|
347
|
+
console.log(req.status) // 'pending' | 'incomplete' | 'settled' | 'scored'
|
|
348
|
+
console.log(req.cost_usd) // e.g. 0.000412
|
|
349
|
+
console.log(req.saved_usd) // vs the model you named (negative when the route cost more)
|
|
350
|
+
console.log(req.quality_score) // 0–1, once scored
|
|
351
|
+
console.log(req.dashboard_url) // opens it in your request log
|
|
352
|
+
}
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
`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).
|
|
356
|
+
|
|
357
|
+
## Structured output
|
|
358
|
+
|
|
359
|
+
`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.
|
|
360
|
+
|
|
361
|
+
```typescript
|
|
362
|
+
import type { ChatCompletion } from '@rouvanpm/rouva'
|
|
363
|
+
|
|
364
|
+
const res = await rouva.chat.completions.create({
|
|
365
|
+
messages: [{ role: 'user', content: 'Extract the city: "Flying to Paris on Monday."' }],
|
|
366
|
+
response_format: {
|
|
367
|
+
type: 'json_schema',
|
|
368
|
+
json_schema: {
|
|
369
|
+
name: 'city',
|
|
370
|
+
strict: true,
|
|
371
|
+
schema: { type: 'object', properties: { city: { type: 'string' } }, required: ['city'], additionalProperties: false },
|
|
372
|
+
},
|
|
373
|
+
},
|
|
374
|
+
}) as ChatCompletion
|
|
375
|
+
const { city } = JSON.parse(res.choices[0].message.content!)
|
|
376
|
+
```
|
|
377
|
+
|
|
330
378
|
## Errors
|
|
331
379
|
|
|
332
380
|
Any non-2xx response throws a `RouvaError` (a subclass of `Error`, so existing `catch` blocks keep working):
|
|
@@ -338,14 +386,15 @@ try {
|
|
|
338
386
|
await rouva.chat.completions.create({ messages })
|
|
339
387
|
} catch (e) {
|
|
340
388
|
if (e instanceof RouvaError && e.status === 429) {
|
|
341
|
-
console.log(e.refusal) // 'key_cap' | 'member_cap' | 'rate' — Rouva refused before calling the provider
|
|
389
|
+
console.log(e.refusal) // 'key_cap' | 'member_cap' | 'rate' | … — Rouva refused before calling the provider
|
|
342
390
|
console.log(e.retryAfter) // seconds, from Retry-After
|
|
343
391
|
console.log(e.error) // the gateway's message
|
|
392
|
+
console.log(e.requestId) // when the request was recorded — rouva.requests.get(e.requestId)
|
|
344
393
|
}
|
|
345
394
|
}
|
|
346
395
|
```
|
|
347
396
|
|
|
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`),
|
|
397
|
+
`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`), for spend-capped keys a model with no known price (`unpriced`) or input or a model whose cost can't be bounded (`unbounded`), or — a `504` — the request ran out of its time budget before the provider was called (`deadline`; nothing was sent, so it's safe to retry, and `retryAfter` is set). Every Rouva refusal is marked, so a `429` or `504` without `refusal` came from the provider, not from Rouva — with one exception: `rouva.run()` returns an unmarked `504` "Run deadline exceeded before completion" when the run runs out of time, because earlier turns were already billed and their tool handlers ran; check what completed before retrying it. Every call also takes `{ signal }` as a second argument to abort it.
|
|
349
398
|
|
|
350
399
|
## Getting your API key
|
|
351
400
|
|
package/dist/index.d.mts
CHANGED
|
@@ -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-
|
|
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 & {});
|
|
68
68
|
/** Anthropic-style text block — the array form of the `system` param */
|
|
69
69
|
interface SystemTextBlock {
|
|
70
70
|
type: 'text';
|
|
@@ -132,6 +132,17 @@ interface ChatCompletionParams {
|
|
|
132
132
|
* no cheaper-model substitution, no cross-provider fallbacks.
|
|
133
133
|
*/
|
|
134
134
|
seed?: number;
|
|
135
|
+
/**
|
|
136
|
+
* Ask for JSON output (gateway support since 2026-10-20). `json_object` is
|
|
137
|
+
* JSON mode: the prompt (a message or `system`) must contain the word
|
|
138
|
+
* "json", or the gateway returns a 400. `json_schema` is Structured Outputs:
|
|
139
|
+
* the answer follows your schema. Routing only picks a model that serves the
|
|
140
|
+
* format; Claude models serve `json_schema` only (every object needs
|
|
141
|
+
* `additionalProperties: false`), and not when the conversation ends with an
|
|
142
|
+
* assistant message. `{ type: 'text' }` or `null` is the default (no-op).
|
|
143
|
+
* `rouva.run()` doesn't accept it. See https://rouva.io/docs#structured-output
|
|
144
|
+
*/
|
|
145
|
+
response_format?: ResponseFormat | null;
|
|
135
146
|
/**
|
|
136
147
|
* Stream the response. Client-side only — controls whether `create()`
|
|
137
148
|
* returns a ReadableStream or a buffered ChatCompletion; never sent to
|
|
@@ -154,6 +165,22 @@ interface ChatCompletionParams {
|
|
|
154
165
|
*/
|
|
155
166
|
tool_choice?: string | Record<string, unknown>;
|
|
156
167
|
}
|
|
168
|
+
/** `response_format` values the gateway accepts. */
|
|
169
|
+
type ResponseFormat = {
|
|
170
|
+
type: 'text';
|
|
171
|
+
} | {
|
|
172
|
+
type: 'json_object';
|
|
173
|
+
} | {
|
|
174
|
+
type: 'json_schema';
|
|
175
|
+
json_schema: {
|
|
176
|
+
/** 1–64 letters, digits, underscores or dashes. */
|
|
177
|
+
name: string;
|
|
178
|
+
/** A JSON Schema object. */
|
|
179
|
+
schema: Record<string, unknown>;
|
|
180
|
+
strict?: boolean;
|
|
181
|
+
description?: string;
|
|
182
|
+
};
|
|
183
|
+
};
|
|
157
184
|
/** Assistant message in a completed response; `tool_calls` present when the model requested tools */
|
|
158
185
|
interface ResponseMessage {
|
|
159
186
|
role: 'assistant';
|
|
@@ -180,13 +207,42 @@ interface ChatCompletion {
|
|
|
180
207
|
/** Rouva metadata — cost, savings, routing decision */
|
|
181
208
|
_rouva?: RouvaResponseMeta;
|
|
182
209
|
}
|
|
210
|
+
/**
|
|
211
|
+
* Why Rouva sent a request to the model it did (header `X-Rouva-Route-Reason`):
|
|
212
|
+
* `intelligent` (Intelligent Routing chose it), `override` (an account routing
|
|
213
|
+
* override), `allowlist` (a workspace member's allowed-model list),
|
|
214
|
+
* `fallback` (a retry after a provider error), `escalation` (`run()` stepped
|
|
215
|
+
* up a model), `no_key_fallback` (the named provider has no key, so a
|
|
216
|
+
* connected one stood in), `direct` (the model you named, as named), or
|
|
217
|
+
* `dedup` (a replay of an identical request — no new call was made).
|
|
218
|
+
*/
|
|
219
|
+
type RouvaRouteReason = 'intelligent' | 'override' | 'allowlist' | 'fallback' | 'escalation' | 'no_key_fallback' | 'direct' | 'dedup' | (string & {});
|
|
183
220
|
interface RouvaResponseMeta {
|
|
184
|
-
/**
|
|
221
|
+
/**
|
|
222
|
+
* The request's id in your Rouva request log (`X-Rouva-Request-Id`). Pass it
|
|
223
|
+
* to `rouva.requests.get(id)` for the request's cost, savings and quality
|
|
224
|
+
* score. Absent on a duplicate-cache replay (no new request was recorded).
|
|
225
|
+
*/
|
|
226
|
+
request_id?: string;
|
|
227
|
+
/** The model that served the request — after a fallback, the fallback model. */
|
|
185
228
|
model_used?: string;
|
|
186
|
-
/**
|
|
229
|
+
/** That model's provider, e.g. `openai`, `anthropic`. */
|
|
187
230
|
provider_used?: string;
|
|
188
|
-
/**
|
|
231
|
+
/** The model you named, when Rouva served a different one. */
|
|
232
|
+
requested_model?: string;
|
|
233
|
+
/** Task type classified by Rouva, e.g. `code`; absent when not classified. */
|
|
189
234
|
task_type?: string;
|
|
235
|
+
/** Why this model was chosen. */
|
|
236
|
+
route_reason?: RouvaRouteReason;
|
|
237
|
+
/**
|
|
238
|
+
* What the request cost in USD, when the gateway sent it in a header: only
|
|
239
|
+
* on non-streaming responses once the cost is final, and as 0 on a
|
|
240
|
+
* duplicate-cache replay (`dedup: true`). The SDK streams, so otherwise read
|
|
241
|
+
* cost with `rouva.requests.get(request_id)`.
|
|
242
|
+
*/
|
|
243
|
+
cost_usd?: number;
|
|
244
|
+
/** Saved against the model you named (or the priciest candidate), in USD; negative when the route cost more. Same availability as `cost_usd`. */
|
|
245
|
+
saved_usd?: number;
|
|
190
246
|
/**
|
|
191
247
|
* True when the gateway answered from its duplicate-request cache (an
|
|
192
248
|
* identical request within the last 60 seconds) instead of calling the
|
|
@@ -199,6 +255,48 @@ interface RouvaResponseMeta {
|
|
|
199
255
|
*/
|
|
200
256
|
cache?: string;
|
|
201
257
|
}
|
|
258
|
+
/**
|
|
259
|
+
* The stream `chat.completions.create({ stream: true })` returns: OpenAI-shaped
|
|
260
|
+
* SSE bytes, plus the routing metadata from the response headers (known
|
|
261
|
+
* before the first chunk).
|
|
262
|
+
*/
|
|
263
|
+
type RouvaStream = ReadableStream<Uint8Array> & {
|
|
264
|
+
_rouva?: RouvaResponseMeta;
|
|
265
|
+
};
|
|
266
|
+
/**
|
|
267
|
+
* A request's settlement state: `pending` (not settled yet — in flight, or it
|
|
268
|
+
* never reached the provider), `incomplete` (settled from partial usage, e.g. a
|
|
269
|
+
* stream cut short — the provider may have billed more), `settled` (tokens and
|
|
270
|
+
* cost are final), `scored` (final, and a quality score is recorded).
|
|
271
|
+
*/
|
|
272
|
+
type RouvaRequestStatus = 'pending' | 'incomplete' | 'settled' | 'scored' | (string & {});
|
|
273
|
+
/** One request from your request log — `rouva.requests.get(id)`. */
|
|
274
|
+
interface RouvaRequest {
|
|
275
|
+
id: string;
|
|
276
|
+
/** ISO timestamp. */
|
|
277
|
+
created_at: string;
|
|
278
|
+
status: RouvaRequestStatus;
|
|
279
|
+
provider: string;
|
|
280
|
+
model: string;
|
|
281
|
+
requested_model: string | null;
|
|
282
|
+
task_type: string | null;
|
|
283
|
+
route_reason: RouvaRouteReason | null;
|
|
284
|
+
/** 0 while pending. */
|
|
285
|
+
input_tokens: number;
|
|
286
|
+
output_tokens: number;
|
|
287
|
+
/** USD; 0 while pending. Final once `status` is `settled` or `scored`. */
|
|
288
|
+
cost_usd: number;
|
|
289
|
+
/** USD saved against the model you named (or the priciest candidate); negative when the route cost more. */
|
|
290
|
+
saved_usd: number;
|
|
291
|
+
/** 0–1, or null until a score is recorded (or when none is). */
|
|
292
|
+
quality_score: number | null;
|
|
293
|
+
/** Whether the judge scored it, or why not (then `quality_score` is the heuristic checks'). */
|
|
294
|
+
judge_status: string | null;
|
|
295
|
+
finish_reason: string | null;
|
|
296
|
+
session_id: string | null;
|
|
297
|
+
/** Opens this request in the dashboard's request log. */
|
|
298
|
+
dashboard_url: string;
|
|
299
|
+
}
|
|
202
300
|
interface RunParams {
|
|
203
301
|
/**
|
|
204
302
|
* Conversation history to seed the agent loop with.
|
|
@@ -301,9 +399,30 @@ declare class Rouva {
|
|
|
301
399
|
private _sessionId;
|
|
302
400
|
readonly chat: {
|
|
303
401
|
completions: {
|
|
304
|
-
create(params: ChatCompletionParams, options?: RequestOptions): Promise<ChatCompletion |
|
|
402
|
+
create(params: ChatCompletionParams, options?: RequestOptions): Promise<ChatCompletion | RouvaStream>;
|
|
305
403
|
};
|
|
306
404
|
};
|
|
405
|
+
readonly requests: {
|
|
406
|
+
/**
|
|
407
|
+
* Read one request from your request log (`GET /api/gateway/requests/{id}`)
|
|
408
|
+
* by the id a response carried (`_rouva.request_id`, header
|
|
409
|
+
* `X-Rouva-Request-Id`): its model, routing reason, tokens, cost, savings and
|
|
410
|
+
* quality score. A key reads only its own requests (another's id is a 404).
|
|
411
|
+
*
|
|
412
|
+
* A stream's cost is recorded just after its last chunk, and the quality
|
|
413
|
+
* score once the judge has run (a few seconds), so check `status`:
|
|
414
|
+
* `pending` → not settled yet, retry shortly; `incomplete` → settled from
|
|
415
|
+
* partial usage; `settled` → cost final; `scored` → cost final and scored.
|
|
416
|
+
* Throws `RouvaError` on a non-2xx response (a 503 carries `retryAfter`).
|
|
417
|
+
*
|
|
418
|
+
* @example
|
|
419
|
+
* const stream = await rouva.chat.completions.create({ messages, stream: true }) as RouvaStream
|
|
420
|
+
* // ...consume the stream...
|
|
421
|
+
* const req = await rouva.requests.get(stream._rouva!.request_id!)
|
|
422
|
+
* console.log(req.status, req.cost_usd, req.saved_usd)
|
|
423
|
+
*/
|
|
424
|
+
get(id: string, options?: RequestOptions): Promise<RouvaRequest>;
|
|
425
|
+
};
|
|
307
426
|
constructor(options: RouvaOptions);
|
|
308
427
|
/**
|
|
309
428
|
* Start a new agent session. Every subsequent `chat.completions.create()`
|
|
@@ -347,7 +466,9 @@ declare class Rouva {
|
|
|
347
466
|
* console.log(result.session_id) // dashboard session ID
|
|
348
467
|
*/
|
|
349
468
|
run(params: RunParams, options?: RequestOptions): Promise<RunResult>;
|
|
469
|
+
private _getRequest;
|
|
350
470
|
private _post;
|
|
471
|
+
private _fetch;
|
|
351
472
|
private _createChatCompletion;
|
|
352
473
|
}
|
|
353
474
|
|
|
@@ -364,18 +485,30 @@ declare class RouvaError extends Error {
|
|
|
364
485
|
* the gateway marks it (header `X-Rouva-Refusal`): `"key_cap"` /
|
|
365
486
|
* `"member_cap"` (a monthly spend cap would be exceeded), `"rate"` (rate
|
|
366
487
|
* limit), `"unpriced"` (a capped key can't use a model with no known price),
|
|
367
|
-
* `"unbounded"` (a capped key can't send input
|
|
368
|
-
*
|
|
369
|
-
*
|
|
488
|
+
* `"unbounded"` (a capped key can't send input, or use a model, whose cost
|
|
489
|
+
* can't be bounded), `"deadline"` (a 504: the request ran out of its time
|
|
490
|
+
* budget before the provider was called — nothing was sent, so it's safe to
|
|
491
|
+
* retry; `retryAfter` is set). Every Rouva cap, rate-limit, pricing and time
|
|
492
|
+
* refusal carries it, so a 429 or 504 without `refusal` came from the
|
|
493
|
+
* provider — except `rouva.run()`'s "Run deadline exceeded" 504, which is
|
|
494
|
+
* Rouva's but deliberately unmarked: earlier turns were billed and their
|
|
495
|
+
* tool handlers ran, so it must not be retried blindly.
|
|
370
496
|
*/
|
|
371
497
|
readonly refusal?: string;
|
|
372
498
|
/** Seconds to wait before retrying, from `Retry-After`, when present. */
|
|
373
499
|
readonly retryAfter?: number;
|
|
374
500
|
/** The gateway's error message (`error` field), when the body is JSON. */
|
|
375
501
|
readonly error?: string;
|
|
502
|
+
/**
|
|
503
|
+
* The request's id in your Rouva request log (`X-Rouva-Request-Id`), when
|
|
504
|
+
* the error came after the request was recorded — e.g. a provider error.
|
|
505
|
+
* Look it up with `rouva.requests.get(requestId)`. Absent on errors Rouva
|
|
506
|
+
* returns before recording the request (validation 400s, auth).
|
|
507
|
+
*/
|
|
508
|
+
readonly requestId?: string;
|
|
376
509
|
/** The parsed JSON body, or the raw text when it isn't JSON. */
|
|
377
510
|
readonly body: unknown;
|
|
378
|
-
constructor(kind: 'Gateway' | 'Run', status: number, headers: Headers, rawBody: string);
|
|
511
|
+
constructor(kind: 'Gateway' | 'Run' | 'Request', status: number, headers: Headers, rawBody: string);
|
|
379
512
|
}
|
|
380
513
|
|
|
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 };
|
|
514
|
+
export { type AssistantToolCallMessage, type ChatCompletion, type ChatCompletionChoice, type ChatCompletionParams, type ChatCompletionUsage, type ChatMessage, type ContentBlockMessage, type FunctionToolCall, type Message, 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 };
|
package/dist/index.d.ts
CHANGED
|
@@ -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-
|
|
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 & {});
|
|
68
68
|
/** Anthropic-style text block — the array form of the `system` param */
|
|
69
69
|
interface SystemTextBlock {
|
|
70
70
|
type: 'text';
|
|
@@ -132,6 +132,17 @@ interface ChatCompletionParams {
|
|
|
132
132
|
* no cheaper-model substitution, no cross-provider fallbacks.
|
|
133
133
|
*/
|
|
134
134
|
seed?: number;
|
|
135
|
+
/**
|
|
136
|
+
* Ask for JSON output (gateway support since 2026-10-20). `json_object` is
|
|
137
|
+
* JSON mode: the prompt (a message or `system`) must contain the word
|
|
138
|
+
* "json", or the gateway returns a 400. `json_schema` is Structured Outputs:
|
|
139
|
+
* the answer follows your schema. Routing only picks a model that serves the
|
|
140
|
+
* format; Claude models serve `json_schema` only (every object needs
|
|
141
|
+
* `additionalProperties: false`), and not when the conversation ends with an
|
|
142
|
+
* assistant message. `{ type: 'text' }` or `null` is the default (no-op).
|
|
143
|
+
* `rouva.run()` doesn't accept it. See https://rouva.io/docs#structured-output
|
|
144
|
+
*/
|
|
145
|
+
response_format?: ResponseFormat | null;
|
|
135
146
|
/**
|
|
136
147
|
* Stream the response. Client-side only — controls whether `create()`
|
|
137
148
|
* returns a ReadableStream or a buffered ChatCompletion; never sent to
|
|
@@ -154,6 +165,22 @@ interface ChatCompletionParams {
|
|
|
154
165
|
*/
|
|
155
166
|
tool_choice?: string | Record<string, unknown>;
|
|
156
167
|
}
|
|
168
|
+
/** `response_format` values the gateway accepts. */
|
|
169
|
+
type ResponseFormat = {
|
|
170
|
+
type: 'text';
|
|
171
|
+
} | {
|
|
172
|
+
type: 'json_object';
|
|
173
|
+
} | {
|
|
174
|
+
type: 'json_schema';
|
|
175
|
+
json_schema: {
|
|
176
|
+
/** 1–64 letters, digits, underscores or dashes. */
|
|
177
|
+
name: string;
|
|
178
|
+
/** A JSON Schema object. */
|
|
179
|
+
schema: Record<string, unknown>;
|
|
180
|
+
strict?: boolean;
|
|
181
|
+
description?: string;
|
|
182
|
+
};
|
|
183
|
+
};
|
|
157
184
|
/** Assistant message in a completed response; `tool_calls` present when the model requested tools */
|
|
158
185
|
interface ResponseMessage {
|
|
159
186
|
role: 'assistant';
|
|
@@ -180,13 +207,42 @@ interface ChatCompletion {
|
|
|
180
207
|
/** Rouva metadata — cost, savings, routing decision */
|
|
181
208
|
_rouva?: RouvaResponseMeta;
|
|
182
209
|
}
|
|
210
|
+
/**
|
|
211
|
+
* Why Rouva sent a request to the model it did (header `X-Rouva-Route-Reason`):
|
|
212
|
+
* `intelligent` (Intelligent Routing chose it), `override` (an account routing
|
|
213
|
+
* override), `allowlist` (a workspace member's allowed-model list),
|
|
214
|
+
* `fallback` (a retry after a provider error), `escalation` (`run()` stepped
|
|
215
|
+
* up a model), `no_key_fallback` (the named provider has no key, so a
|
|
216
|
+
* connected one stood in), `direct` (the model you named, as named), or
|
|
217
|
+
* `dedup` (a replay of an identical request — no new call was made).
|
|
218
|
+
*/
|
|
219
|
+
type RouvaRouteReason = 'intelligent' | 'override' | 'allowlist' | 'fallback' | 'escalation' | 'no_key_fallback' | 'direct' | 'dedup' | (string & {});
|
|
183
220
|
interface RouvaResponseMeta {
|
|
184
|
-
/**
|
|
221
|
+
/**
|
|
222
|
+
* The request's id in your Rouva request log (`X-Rouva-Request-Id`). Pass it
|
|
223
|
+
* to `rouva.requests.get(id)` for the request's cost, savings and quality
|
|
224
|
+
* score. Absent on a duplicate-cache replay (no new request was recorded).
|
|
225
|
+
*/
|
|
226
|
+
request_id?: string;
|
|
227
|
+
/** The model that served the request — after a fallback, the fallback model. */
|
|
185
228
|
model_used?: string;
|
|
186
|
-
/**
|
|
229
|
+
/** That model's provider, e.g. `openai`, `anthropic`. */
|
|
187
230
|
provider_used?: string;
|
|
188
|
-
/**
|
|
231
|
+
/** The model you named, when Rouva served a different one. */
|
|
232
|
+
requested_model?: string;
|
|
233
|
+
/** Task type classified by Rouva, e.g. `code`; absent when not classified. */
|
|
189
234
|
task_type?: string;
|
|
235
|
+
/** Why this model was chosen. */
|
|
236
|
+
route_reason?: RouvaRouteReason;
|
|
237
|
+
/**
|
|
238
|
+
* What the request cost in USD, when the gateway sent it in a header: only
|
|
239
|
+
* on non-streaming responses once the cost is final, and as 0 on a
|
|
240
|
+
* duplicate-cache replay (`dedup: true`). The SDK streams, so otherwise read
|
|
241
|
+
* cost with `rouva.requests.get(request_id)`.
|
|
242
|
+
*/
|
|
243
|
+
cost_usd?: number;
|
|
244
|
+
/** Saved against the model you named (or the priciest candidate), in USD; negative when the route cost more. Same availability as `cost_usd`. */
|
|
245
|
+
saved_usd?: number;
|
|
190
246
|
/**
|
|
191
247
|
* True when the gateway answered from its duplicate-request cache (an
|
|
192
248
|
* identical request within the last 60 seconds) instead of calling the
|
|
@@ -199,6 +255,48 @@ interface RouvaResponseMeta {
|
|
|
199
255
|
*/
|
|
200
256
|
cache?: string;
|
|
201
257
|
}
|
|
258
|
+
/**
|
|
259
|
+
* The stream `chat.completions.create({ stream: true })` returns: OpenAI-shaped
|
|
260
|
+
* SSE bytes, plus the routing metadata from the response headers (known
|
|
261
|
+
* before the first chunk).
|
|
262
|
+
*/
|
|
263
|
+
type RouvaStream = ReadableStream<Uint8Array> & {
|
|
264
|
+
_rouva?: RouvaResponseMeta;
|
|
265
|
+
};
|
|
266
|
+
/**
|
|
267
|
+
* A request's settlement state: `pending` (not settled yet — in flight, or it
|
|
268
|
+
* never reached the provider), `incomplete` (settled from partial usage, e.g. a
|
|
269
|
+
* stream cut short — the provider may have billed more), `settled` (tokens and
|
|
270
|
+
* cost are final), `scored` (final, and a quality score is recorded).
|
|
271
|
+
*/
|
|
272
|
+
type RouvaRequestStatus = 'pending' | 'incomplete' | 'settled' | 'scored' | (string & {});
|
|
273
|
+
/** One request from your request log — `rouva.requests.get(id)`. */
|
|
274
|
+
interface RouvaRequest {
|
|
275
|
+
id: string;
|
|
276
|
+
/** ISO timestamp. */
|
|
277
|
+
created_at: string;
|
|
278
|
+
status: RouvaRequestStatus;
|
|
279
|
+
provider: string;
|
|
280
|
+
model: string;
|
|
281
|
+
requested_model: string | null;
|
|
282
|
+
task_type: string | null;
|
|
283
|
+
route_reason: RouvaRouteReason | null;
|
|
284
|
+
/** 0 while pending. */
|
|
285
|
+
input_tokens: number;
|
|
286
|
+
output_tokens: number;
|
|
287
|
+
/** USD; 0 while pending. Final once `status` is `settled` or `scored`. */
|
|
288
|
+
cost_usd: number;
|
|
289
|
+
/** USD saved against the model you named (or the priciest candidate); negative when the route cost more. */
|
|
290
|
+
saved_usd: number;
|
|
291
|
+
/** 0–1, or null until a score is recorded (or when none is). */
|
|
292
|
+
quality_score: number | null;
|
|
293
|
+
/** Whether the judge scored it, or why not (then `quality_score` is the heuristic checks'). */
|
|
294
|
+
judge_status: string | null;
|
|
295
|
+
finish_reason: string | null;
|
|
296
|
+
session_id: string | null;
|
|
297
|
+
/** Opens this request in the dashboard's request log. */
|
|
298
|
+
dashboard_url: string;
|
|
299
|
+
}
|
|
202
300
|
interface RunParams {
|
|
203
301
|
/**
|
|
204
302
|
* Conversation history to seed the agent loop with.
|
|
@@ -301,9 +399,30 @@ declare class Rouva {
|
|
|
301
399
|
private _sessionId;
|
|
302
400
|
readonly chat: {
|
|
303
401
|
completions: {
|
|
304
|
-
create(params: ChatCompletionParams, options?: RequestOptions): Promise<ChatCompletion |
|
|
402
|
+
create(params: ChatCompletionParams, options?: RequestOptions): Promise<ChatCompletion | RouvaStream>;
|
|
305
403
|
};
|
|
306
404
|
};
|
|
405
|
+
readonly requests: {
|
|
406
|
+
/**
|
|
407
|
+
* Read one request from your request log (`GET /api/gateway/requests/{id}`)
|
|
408
|
+
* by the id a response carried (`_rouva.request_id`, header
|
|
409
|
+
* `X-Rouva-Request-Id`): its model, routing reason, tokens, cost, savings and
|
|
410
|
+
* quality score. A key reads only its own requests (another's id is a 404).
|
|
411
|
+
*
|
|
412
|
+
* A stream's cost is recorded just after its last chunk, and the quality
|
|
413
|
+
* score once the judge has run (a few seconds), so check `status`:
|
|
414
|
+
* `pending` → not settled yet, retry shortly; `incomplete` → settled from
|
|
415
|
+
* partial usage; `settled` → cost final; `scored` → cost final and scored.
|
|
416
|
+
* Throws `RouvaError` on a non-2xx response (a 503 carries `retryAfter`).
|
|
417
|
+
*
|
|
418
|
+
* @example
|
|
419
|
+
* const stream = await rouva.chat.completions.create({ messages, stream: true }) as RouvaStream
|
|
420
|
+
* // ...consume the stream...
|
|
421
|
+
* const req = await rouva.requests.get(stream._rouva!.request_id!)
|
|
422
|
+
* console.log(req.status, req.cost_usd, req.saved_usd)
|
|
423
|
+
*/
|
|
424
|
+
get(id: string, options?: RequestOptions): Promise<RouvaRequest>;
|
|
425
|
+
};
|
|
307
426
|
constructor(options: RouvaOptions);
|
|
308
427
|
/**
|
|
309
428
|
* Start a new agent session. Every subsequent `chat.completions.create()`
|
|
@@ -347,7 +466,9 @@ declare class Rouva {
|
|
|
347
466
|
* console.log(result.session_id) // dashboard session ID
|
|
348
467
|
*/
|
|
349
468
|
run(params: RunParams, options?: RequestOptions): Promise<RunResult>;
|
|
469
|
+
private _getRequest;
|
|
350
470
|
private _post;
|
|
471
|
+
private _fetch;
|
|
351
472
|
private _createChatCompletion;
|
|
352
473
|
}
|
|
353
474
|
|
|
@@ -364,18 +485,30 @@ declare class RouvaError extends Error {
|
|
|
364
485
|
* the gateway marks it (header `X-Rouva-Refusal`): `"key_cap"` /
|
|
365
486
|
* `"member_cap"` (a monthly spend cap would be exceeded), `"rate"` (rate
|
|
366
487
|
* limit), `"unpriced"` (a capped key can't use a model with no known price),
|
|
367
|
-
* `"unbounded"` (a capped key can't send input
|
|
368
|
-
*
|
|
369
|
-
*
|
|
488
|
+
* `"unbounded"` (a capped key can't send input, or use a model, whose cost
|
|
489
|
+
* can't be bounded), `"deadline"` (a 504: the request ran out of its time
|
|
490
|
+
* budget before the provider was called — nothing was sent, so it's safe to
|
|
491
|
+
* retry; `retryAfter` is set). Every Rouva cap, rate-limit, pricing and time
|
|
492
|
+
* refusal carries it, so a 429 or 504 without `refusal` came from the
|
|
493
|
+
* provider — except `rouva.run()`'s "Run deadline exceeded" 504, which is
|
|
494
|
+
* Rouva's but deliberately unmarked: earlier turns were billed and their
|
|
495
|
+
* tool handlers ran, so it must not be retried blindly.
|
|
370
496
|
*/
|
|
371
497
|
readonly refusal?: string;
|
|
372
498
|
/** Seconds to wait before retrying, from `Retry-After`, when present. */
|
|
373
499
|
readonly retryAfter?: number;
|
|
374
500
|
/** The gateway's error message (`error` field), when the body is JSON. */
|
|
375
501
|
readonly error?: string;
|
|
502
|
+
/**
|
|
503
|
+
* The request's id in your Rouva request log (`X-Rouva-Request-Id`), when
|
|
504
|
+
* the error came after the request was recorded — e.g. a provider error.
|
|
505
|
+
* Look it up with `rouva.requests.get(requestId)`. Absent on errors Rouva
|
|
506
|
+
* returns before recording the request (validation 400s, auth).
|
|
507
|
+
*/
|
|
508
|
+
readonly requestId?: string;
|
|
376
509
|
/** The parsed JSON body, or the raw text when it isn't JSON. */
|
|
377
510
|
readonly body: unknown;
|
|
378
|
-
constructor(kind: 'Gateway' | 'Run', status: number, headers: Headers, rawBody: string);
|
|
511
|
+
constructor(kind: 'Gateway' | 'Run' | 'Request', status: number, headers: Headers, rawBody: string);
|
|
379
512
|
}
|
|
380
513
|
|
|
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 };
|
|
514
|
+
export { type AssistantToolCallMessage, type ChatCompletion, type ChatCompletionChoice, type ChatCompletionParams, type ChatCompletionUsage, type ChatMessage, type ContentBlockMessage, type FunctionToolCall, type Message, 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 };
|
package/dist/index.js
CHANGED
|
@@ -357,17 +357,30 @@ async function readChatCompletionFromSse(stream, metadata) {
|
|
|
357
357
|
}
|
|
358
358
|
function getRouvaMetadata(headers) {
|
|
359
359
|
const read = (name) => headers.get(name) || void 0;
|
|
360
|
-
const
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
360
|
+
const usd = (name) => {
|
|
361
|
+
const raw = read(name);
|
|
362
|
+
const n = raw === void 0 ? NaN : Number(raw);
|
|
363
|
+
return Number.isFinite(n) ? n : void 0;
|
|
364
|
+
};
|
|
365
|
+
const meta = {
|
|
366
|
+
// X-Rouva-Row-Id is the same id under its older name.
|
|
367
|
+
request_id: read("x-rouva-request-id") ?? read("x-rouva-row-id"),
|
|
368
|
+
model_used: read("x-rouva-model"),
|
|
369
|
+
provider_used: read("x-rouva-provider"),
|
|
370
|
+
requested_model: read("x-rouva-requested-model"),
|
|
371
|
+
// X-Rouva-Task is the older name, still sent alongside.
|
|
372
|
+
task_type: read("x-rouva-task-type") ?? read("x-rouva-task"),
|
|
373
|
+
route_reason: read("x-rouva-route-reason"),
|
|
374
|
+
cost_usd: usd("x-rouva-cost-usd"),
|
|
375
|
+
saved_usd: usd("x-rouva-saved-usd"),
|
|
376
|
+
// Duplicate-request cache hit (an identical request within 60s, not charged).
|
|
377
|
+
// (The old x-rouva-cache header went away with the semantic cache.)
|
|
378
|
+
dedup: headers.get("x-rouva-dedup") === "1" ? true : void 0
|
|
370
379
|
};
|
|
380
|
+
for (const key of Object.keys(meta)) {
|
|
381
|
+
if (meta[key] === void 0) delete meta[key];
|
|
382
|
+
}
|
|
383
|
+
return Object.keys(meta).length > 0 ? meta : void 0;
|
|
371
384
|
}
|
|
372
385
|
|
|
373
386
|
// src/errors.ts
|
|
@@ -379,6 +392,7 @@ var RouvaError = class extends Error {
|
|
|
379
392
|
this.refusal = headers.get("x-rouva-refusal") ?? void 0;
|
|
380
393
|
const retry = Number(headers.get("retry-after"));
|
|
381
394
|
this.retryAfter = headers.get("retry-after") !== null && Number.isFinite(retry) ? retry : void 0;
|
|
395
|
+
this.requestId = headers.get("x-rouva-request-id") || headers.get("x-rouva-row-id") || void 0;
|
|
382
396
|
let parsed = rawBody;
|
|
383
397
|
try {
|
|
384
398
|
parsed = JSON.parse(rawBody);
|
|
@@ -412,6 +426,9 @@ var Rouva = class {
|
|
|
412
426
|
create: (params, options2) => this._createChatCompletion(params, options2)
|
|
413
427
|
}
|
|
414
428
|
};
|
|
429
|
+
this.requests = {
|
|
430
|
+
get: (id, options2) => this._getRequest(id, options2)
|
|
431
|
+
};
|
|
415
432
|
}
|
|
416
433
|
/**
|
|
417
434
|
* Start a new agent session. Every subsequent `chat.completions.create()`
|
|
@@ -466,16 +483,25 @@ var Rouva = class {
|
|
|
466
483
|
if (!res.ok) throw await toRouvaError("Run", res);
|
|
467
484
|
return res.json();
|
|
468
485
|
}
|
|
469
|
-
|
|
486
|
+
async _getRequest(id, options) {
|
|
487
|
+
if (typeof id !== "string" || !id) throw new Error("[Rouva] request id is required");
|
|
488
|
+
const res = await this._fetch("GET", `/api/gateway/requests/${encodeURIComponent(id)}`, void 0, {}, options);
|
|
489
|
+
if (!res.ok) throw await toRouvaError("Request", res);
|
|
490
|
+
return res.json();
|
|
491
|
+
}
|
|
470
492
|
_post(path, body, extraHeaders, options) {
|
|
493
|
+
return this._fetch("POST", path, body, extraHeaders, options);
|
|
494
|
+
}
|
|
495
|
+
// One request path for every endpoint: auth, JSON body, optional abort signal.
|
|
496
|
+
_fetch(method, path, body, extraHeaders, options) {
|
|
471
497
|
return fetch(`${this.baseURL}${path}`, {
|
|
472
|
-
method
|
|
498
|
+
method,
|
|
473
499
|
headers: {
|
|
474
|
-
"Content-Type": "application/json",
|
|
500
|
+
...method === "POST" ? { "Content-Type": "application/json" } : {},
|
|
475
501
|
"Authorization": `Bearer ${this.apiKey}`,
|
|
476
502
|
...extraHeaders
|
|
477
503
|
},
|
|
478
|
-
body: JSON.stringify(body),
|
|
504
|
+
...method === "POST" ? { body: JSON.stringify(body) } : {},
|
|
479
505
|
...options?.signal ? { signal: options.signal } : {}
|
|
480
506
|
});
|
|
481
507
|
}
|
|
@@ -486,8 +512,12 @@ var Rouva = class {
|
|
|
486
512
|
if (!res.ok) throw await toRouvaError("Gateway", res);
|
|
487
513
|
if (!res.body) throw new Error("[Rouva] No response body from gateway");
|
|
488
514
|
const normalizedStream = normalizeGatewayStream(res.body);
|
|
489
|
-
|
|
490
|
-
|
|
515
|
+
const metadata = getRouvaMetadata(res.headers);
|
|
516
|
+
if (params.stream) {
|
|
517
|
+
if (metadata) normalizedStream._rouva = metadata;
|
|
518
|
+
return normalizedStream;
|
|
519
|
+
}
|
|
520
|
+
return readChatCompletionFromSse(normalizedStream, metadata);
|
|
491
521
|
}
|
|
492
522
|
};
|
|
493
523
|
// Annotate the CommonJS export names for ESM import in node:
|
package/dist/index.mjs
CHANGED
|
@@ -330,17 +330,30 @@ async function readChatCompletionFromSse(stream, metadata) {
|
|
|
330
330
|
}
|
|
331
331
|
function getRouvaMetadata(headers) {
|
|
332
332
|
const read = (name) => headers.get(name) || void 0;
|
|
333
|
-
const
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
333
|
+
const usd = (name) => {
|
|
334
|
+
const raw = read(name);
|
|
335
|
+
const n = raw === void 0 ? NaN : Number(raw);
|
|
336
|
+
return Number.isFinite(n) ? n : void 0;
|
|
337
|
+
};
|
|
338
|
+
const meta = {
|
|
339
|
+
// X-Rouva-Row-Id is the same id under its older name.
|
|
340
|
+
request_id: read("x-rouva-request-id") ?? read("x-rouva-row-id"),
|
|
341
|
+
model_used: read("x-rouva-model"),
|
|
342
|
+
provider_used: read("x-rouva-provider"),
|
|
343
|
+
requested_model: read("x-rouva-requested-model"),
|
|
344
|
+
// X-Rouva-Task is the older name, still sent alongside.
|
|
345
|
+
task_type: read("x-rouva-task-type") ?? read("x-rouva-task"),
|
|
346
|
+
route_reason: read("x-rouva-route-reason"),
|
|
347
|
+
cost_usd: usd("x-rouva-cost-usd"),
|
|
348
|
+
saved_usd: usd("x-rouva-saved-usd"),
|
|
349
|
+
// Duplicate-request cache hit (an identical request within 60s, not charged).
|
|
350
|
+
// (The old x-rouva-cache header went away with the semantic cache.)
|
|
351
|
+
dedup: headers.get("x-rouva-dedup") === "1" ? true : void 0
|
|
343
352
|
};
|
|
353
|
+
for (const key of Object.keys(meta)) {
|
|
354
|
+
if (meta[key] === void 0) delete meta[key];
|
|
355
|
+
}
|
|
356
|
+
return Object.keys(meta).length > 0 ? meta : void 0;
|
|
344
357
|
}
|
|
345
358
|
|
|
346
359
|
// src/errors.ts
|
|
@@ -352,6 +365,7 @@ var RouvaError = class extends Error {
|
|
|
352
365
|
this.refusal = headers.get("x-rouva-refusal") ?? void 0;
|
|
353
366
|
const retry = Number(headers.get("retry-after"));
|
|
354
367
|
this.retryAfter = headers.get("retry-after") !== null && Number.isFinite(retry) ? retry : void 0;
|
|
368
|
+
this.requestId = headers.get("x-rouva-request-id") || headers.get("x-rouva-row-id") || void 0;
|
|
355
369
|
let parsed = rawBody;
|
|
356
370
|
try {
|
|
357
371
|
parsed = JSON.parse(rawBody);
|
|
@@ -385,6 +399,9 @@ var Rouva = class {
|
|
|
385
399
|
create: (params, options2) => this._createChatCompletion(params, options2)
|
|
386
400
|
}
|
|
387
401
|
};
|
|
402
|
+
this.requests = {
|
|
403
|
+
get: (id, options2) => this._getRequest(id, options2)
|
|
404
|
+
};
|
|
388
405
|
}
|
|
389
406
|
/**
|
|
390
407
|
* Start a new agent session. Every subsequent `chat.completions.create()`
|
|
@@ -439,16 +456,25 @@ var Rouva = class {
|
|
|
439
456
|
if (!res.ok) throw await toRouvaError("Run", res);
|
|
440
457
|
return res.json();
|
|
441
458
|
}
|
|
442
|
-
|
|
459
|
+
async _getRequest(id, options) {
|
|
460
|
+
if (typeof id !== "string" || !id) throw new Error("[Rouva] request id is required");
|
|
461
|
+
const res = await this._fetch("GET", `/api/gateway/requests/${encodeURIComponent(id)}`, void 0, {}, options);
|
|
462
|
+
if (!res.ok) throw await toRouvaError("Request", res);
|
|
463
|
+
return res.json();
|
|
464
|
+
}
|
|
443
465
|
_post(path, body, extraHeaders, options) {
|
|
466
|
+
return this._fetch("POST", path, body, extraHeaders, options);
|
|
467
|
+
}
|
|
468
|
+
// One request path for every endpoint: auth, JSON body, optional abort signal.
|
|
469
|
+
_fetch(method, path, body, extraHeaders, options) {
|
|
444
470
|
return fetch(`${this.baseURL}${path}`, {
|
|
445
|
-
method
|
|
471
|
+
method,
|
|
446
472
|
headers: {
|
|
447
|
-
"Content-Type": "application/json",
|
|
473
|
+
...method === "POST" ? { "Content-Type": "application/json" } : {},
|
|
448
474
|
"Authorization": `Bearer ${this.apiKey}`,
|
|
449
475
|
...extraHeaders
|
|
450
476
|
},
|
|
451
|
-
body: JSON.stringify(body),
|
|
477
|
+
...method === "POST" ? { body: JSON.stringify(body) } : {},
|
|
452
478
|
...options?.signal ? { signal: options.signal } : {}
|
|
453
479
|
});
|
|
454
480
|
}
|
|
@@ -459,8 +485,12 @@ var Rouva = class {
|
|
|
459
485
|
if (!res.ok) throw await toRouvaError("Gateway", res);
|
|
460
486
|
if (!res.body) throw new Error("[Rouva] No response body from gateway");
|
|
461
487
|
const normalizedStream = normalizeGatewayStream(res.body);
|
|
462
|
-
|
|
463
|
-
|
|
488
|
+
const metadata = getRouvaMetadata(res.headers);
|
|
489
|
+
if (params.stream) {
|
|
490
|
+
if (metadata) normalizedStream._rouva = metadata;
|
|
491
|
+
return normalizedStream;
|
|
492
|
+
}
|
|
493
|
+
return readChatCompletionFromSse(normalizedStream, metadata);
|
|
464
494
|
}
|
|
465
495
|
};
|
|
466
496
|
export {
|
package/package.json
CHANGED