@rouvanpm/rouva 0.2.10 → 0.2.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -119,13 +119,13 @@ Models not recognised by the gateway return a `400` immediately:
119
119
 
120
120
  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
121
 
122
- Intelligent routing applies to `/v1` too — Rouva may substitute a cheaper model within the same provider using your requested model as a cost ceiling. For tool call requests specifically, routing only considers non-reasoning models (reasoning models add unpredictable hidden token costs on tool dispatch). To pin a model exactly, disable Intelligent Routing in your dashboard under **Settings**.
122
+ Non-tools requests on `/v1` always honor the named model exactly. **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
123
 
124
124
  ## Tool use
125
125
 
126
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.
127
127
 
128
- On the native endpoint (`/api/gateway/messages`), the pinned model is always honored exactly — no substitution. On the `/v1` endpoint with Intelligent Routing enabled, Rouva may route to a cheaper non-reasoning model within the same provider. Disable Intelligent Routing in **Settings** to pin the exact model on `/v1`.
128
+ When using the `/v1` endpoint (OpenAI SDK, LangChain, Vercel AI SDK, …) with Intelligent Routing enabled, tools requests are routed to the cheapest capable model within the same provider — see [OpenAI-compatible endpoint](#openai-compatible-endpoint) for details. On the native SDK endpoint (`/api/gateway/messages`), the named model is always honored exactly.
129
129
 
130
130
  ```typescript
131
131
  const res = await rouva.chat.completions.create({
@@ -167,7 +167,7 @@ Responses are normalized to the OpenAI shape regardless of provider: Anthropic `
167
167
 
168
168
  **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
169
 
170
- Tools requests record usage and cost but no savings, and are not quality-scored or served from the semantic cache.
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 or served from the semantic cache.
171
171
 
172
172
  ## Cost optimisations
173
173
 
@@ -190,6 +190,118 @@ const rouva = new Rouva({
190
190
  })
191
191
  ```
192
192
 
193
+ ## Agentic runs — `rouva.run()`
194
+
195
+ `rouva.run()` hands the entire agent loop to the Rouva gateway. You define your tools and handler URLs; Rouva calls the model, dispatches tool calls in parallel to your endpoints, and loops until the model stops or a limit is reached — no client-side loop, no streaming required.
196
+
197
+ ```typescript
198
+ const result = await rouva.run({
199
+ model: 'claude-haiku-4-5-20251001',
200
+ messages: [{ role: 'user', content: 'What is the weather in Paris and London?' }],
201
+ tools: [
202
+ {
203
+ name: 'get_weather',
204
+ description: 'Get current weather for a city',
205
+ input_schema: {
206
+ type: 'object',
207
+ properties: { city: { type: 'string' } },
208
+ required: ['city'],
209
+ },
210
+ },
211
+ ],
212
+ tool_handlers: {
213
+ get_weather: 'https://api.example.com/weather', // gateway POSTs tool input here
214
+ },
215
+ max_turns: 5,
216
+ session_budget_usd: 0.10, // optional hard cost cap
217
+ })
218
+
219
+ console.log(result.content) // final model response
220
+ console.log(result.turns) // number of turns taken
221
+ console.log(result.tool_calls_made) // total tool calls dispatched
222
+ console.log(result.session_id) // dashboard session ID
223
+ console.log(result.session_cost_usd) // total USD spent
224
+ ```
225
+
226
+ ### How it works
227
+
228
+ 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 tool's JSON input, the response body is used as the tool result
230
+ 3. Results are appended and the loop continues
231
+ 4. The loop exits when `finish_reason` is `"stop"` / `"end_turn"`, `max_turns` is reached, or `session_budget_usd` is exceeded
232
+
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, tool calls, and quality score.
234
+
235
+ ### Handler URL requirements
236
+
237
+ - 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's input object as the body
239
+ - The response body (text or JSON) is returned to the model as the tool result
240
+
241
+ ### Parameters
242
+
243
+ | Parameter | Type | Required | Description |
244
+ |---|---|---|---|
245
+ | `messages` | `ChatMessage[]` | ✓ | Conversation seed — usually a single user message |
246
+ | `tools` | `object[]` | ✓ | Tool definitions (OpenAI or Anthropic format) |
247
+ | `tool_handlers` | `Record<string, string>` | ✓ | Tool name → handler URL mapping |
248
+ | `model` | `RouvaModel` | recommended | Model to use — required for Anthropic tools |
249
+ | `provider` | `RouvaProvider` | | Force a specific provider |
250
+ | `max_turns` | `number` | | Max loop iterations (default: 10) |
251
+ | `max_tokens` | `number` | | Max tokens per turn (default: 4096) |
252
+ | `temperature` | `number` | | Sampling temperature 0–1 |
253
+ | `session_budget_usd` | `number` | | Hard USD cap across all turns |
254
+
255
+ ### Result shape
256
+
257
+ ```typescript
258
+ interface RunResult {
259
+ content: string | null // final model response text
260
+ turns: number // turns executed
261
+ tool_calls_made: number // total tool calls dispatched
262
+ session_id: string // groups all turns in the dashboard
263
+ finish_reason: string | null // "stop", "end_turn", "max_turns", "budget_exceeded", "length"
264
+ cost_usd: number // cost of the last turn
265
+ session_cost_usd: number // cumulative cost across all turns
266
+ }
267
+ ```
268
+
269
+ ### When to use `run()` vs `chat.completions.create()`
270
+
271
+ | | `rouva.run()` | `chat.completions.create()` |
272
+ |---|---|---|
273
+ | Tool loop | Gateway-managed | Client-managed |
274
+ | Session grouping | Automatic | Manual (`startSession`) |
275
+ | Streaming | No | Yes |
276
+ | Use when | Fully delegating an agentic task | Building your own loop or need streaming |
277
+
278
+ ## Agent session tracking
279
+
280
+ Group all turns of an agent run under a single session ID so the Rouva dashboard can show per-session cost, token usage, and quality. Use this with `chat.completions.create()` when you manage the loop yourself.
281
+
282
+ ```typescript
283
+ const rouva = new Rouva({ apiKey: 'rva_...' })
284
+
285
+ // Start a session — auto-generates an ID and attaches it to every request
286
+ const sessionId = rouva.startSession()
287
+
288
+ // All turns carry the same session ID automatically
289
+ await rouva.chat.completions.create({ messages: [...], tools: [...] })
290
+ await rouva.chat.completions.create({ messages: [...], tools: [...] })
291
+ await rouva.chat.completions.create({ messages: [...], tools: [...] })
292
+
293
+ // End the session when the agent run is complete
294
+ rouva.endSession()
295
+ ```
296
+
297
+ You can also read the active session ID at any time:
298
+
299
+ ```typescript
300
+ console.log(rouva.sessionId) // 'rva-sess-abc123' or undefined
301
+ ```
302
+
303
+ Sessions are optional — requests without an active session are tracked individually as before.
304
+
193
305
  ## Response metadata
194
306
 
195
307
  Parsed non-stream responses may include a `_rouva` field with gateway header metadata when available:
package/dist/index.d.mts CHANGED
@@ -164,6 +164,80 @@ interface RouvaResponseMeta {
164
164
  /** Semantic cache status when exposed by the gateway */
165
165
  cache?: string;
166
166
  }
167
+ interface RunParams {
168
+ /**
169
+ * Conversation history to seed the agent loop with.
170
+ * Typically a single user message, but may include prior turns for
171
+ * multi-shot agents.
172
+ */
173
+ messages: ChatMessage[];
174
+ /**
175
+ * Tool definitions to expose to the model. Accepts both OpenAI-style
176
+ * `{ type: "function", function: { name, description, parameters } }` and
177
+ * Anthropic-style `{ name, description, input_schema }` objects.
178
+ * At least one tool is required.
179
+ */
180
+ tools: Array<Record<string, unknown>>;
181
+ /**
182
+ * Map of tool name → handler URL. When the model calls a tool, the gateway
183
+ * POSTs the tool input as JSON to the corresponding URL and feeds the
184
+ * response body back as the tool result. All handlers must be public HTTPS
185
+ * endpoints.
186
+ *
187
+ * @example
188
+ * { search: 'https://api.example.com/search', calculator: 'https://api.example.com/calc' }
189
+ */
190
+ tool_handlers: Record<string, string>;
191
+ /**
192
+ * Target model. Must be specified for tool calls — the gateway never
193
+ * re-routes tool requests across providers since tool schemas are
194
+ * provider-specific.
195
+ */
196
+ model?: RouvaModel;
197
+ /**
198
+ * Force a specific provider when paired with `model`.
199
+ */
200
+ provider?: RouvaProvider;
201
+ /**
202
+ * Maximum number of agentic turns before the loop exits.
203
+ * Defaults to 10. Each turn = one model call (plus parallel tool dispatch).
204
+ */
205
+ max_turns?: number;
206
+ /**
207
+ * Maximum tokens the model may generate per turn.
208
+ * Defaults to 4096.
209
+ */
210
+ max_tokens?: number;
211
+ /**
212
+ * Sampling temperature 0–1.
213
+ */
214
+ temperature?: number;
215
+ /**
216
+ * Hard cap on total session spend in USD. The loop exits cleanly when the
217
+ * cumulative cost of all turns reaches this threshold.
218
+ */
219
+ session_budget_usd?: number;
220
+ }
221
+ interface RunResult {
222
+ /** Final model response text after all tool loops complete. */
223
+ content: string | null;
224
+ /** Number of agentic turns executed. */
225
+ turns: number;
226
+ /** Total number of tool calls dispatched across all turns. */
227
+ tool_calls_made: number;
228
+ /** Session ID — all turns are grouped under this ID in the Rouva dashboard. */
229
+ session_id: string;
230
+ /**
231
+ * Why the loop exited: `"stop"` (model finished), `"max_turns"` (turn limit
232
+ * reached), `"budget_exceeded"` (session_budget_usd hit), or a provider
233
+ * finish reason such as `"end_turn"` or `"length"`.
234
+ */
235
+ finish_reason: string | null;
236
+ /** USD cost of the final turn. */
237
+ cost_usd: number;
238
+ /** Cumulative USD cost of all turns in the session. */
239
+ session_cost_usd: number;
240
+ }
167
241
 
168
242
  declare class Rouva {
169
243
  private apiKey;
@@ -189,7 +263,29 @@ declare class Rouva {
189
263
  endSession(): void;
190
264
  /** The active session ID, or undefined if no session is in progress. */
191
265
  get sessionId(): string | undefined;
266
+ /**
267
+ * Run a server-side agentic loop. The gateway calls the model, dispatches
268
+ * tool calls in parallel to your handler URLs, and loops until the model
269
+ * stops or limits are reached — no streaming, no client-side loop required.
270
+ *
271
+ * Every turn is logged as a usage snapshot grouped by `session_id` in the
272
+ * Rouva dashboard, giving you a full session replay with per-turn cost,
273
+ * model, and tool call details.
274
+ *
275
+ * @example
276
+ * const result = await rouva.run({
277
+ * model: 'claude-haiku-4-5-20251001',
278
+ * messages: [{ role: 'user', content: 'What is the weather in Paris?' }],
279
+ * tools: [{ name: 'get_weather', description: '...', input_schema: { ... } }],
280
+ * tool_handlers: { get_weather: 'https://api.example.com/weather' },
281
+ * max_turns: 5,
282
+ * })
283
+ * console.log(result.content) // final model response
284
+ * console.log(result.turns) // how many turns it took
285
+ * console.log(result.session_id) // dashboard session ID
286
+ */
287
+ run(params: RunParams): Promise<RunResult>;
192
288
  private _createChatCompletion;
193
289
  }
194
290
 
195
- export { type AssistantToolCallMessage, type ChatCompletion, type ChatCompletionChoice, type ChatCompletionParams, type ChatCompletionUsage, type ChatMessage, type ContentBlockMessage, type FunctionToolCall, type Message, type ResponseMessage, Rouva, type RouvaOptions, type RouvaProvider, type RouvaResponseMeta, type SystemTextBlock, type ToolResultMessage };
291
+ export { type AssistantToolCallMessage, type ChatCompletion, type ChatCompletionChoice, type ChatCompletionParams, type ChatCompletionUsage, type ChatMessage, type ContentBlockMessage, type FunctionToolCall, type Message, type ResponseMessage, Rouva, type RouvaOptions, type RouvaProvider, type RouvaResponseMeta, type RunParams, type RunResult, type SystemTextBlock, type ToolResultMessage };
package/dist/index.d.ts CHANGED
@@ -164,6 +164,80 @@ interface RouvaResponseMeta {
164
164
  /** Semantic cache status when exposed by the gateway */
165
165
  cache?: string;
166
166
  }
167
+ interface RunParams {
168
+ /**
169
+ * Conversation history to seed the agent loop with.
170
+ * Typically a single user message, but may include prior turns for
171
+ * multi-shot agents.
172
+ */
173
+ messages: ChatMessage[];
174
+ /**
175
+ * Tool definitions to expose to the model. Accepts both OpenAI-style
176
+ * `{ type: "function", function: { name, description, parameters } }` and
177
+ * Anthropic-style `{ name, description, input_schema }` objects.
178
+ * At least one tool is required.
179
+ */
180
+ tools: Array<Record<string, unknown>>;
181
+ /**
182
+ * Map of tool name → handler URL. When the model calls a tool, the gateway
183
+ * POSTs the tool input as JSON to the corresponding URL and feeds the
184
+ * response body back as the tool result. All handlers must be public HTTPS
185
+ * endpoints.
186
+ *
187
+ * @example
188
+ * { search: 'https://api.example.com/search', calculator: 'https://api.example.com/calc' }
189
+ */
190
+ tool_handlers: Record<string, string>;
191
+ /**
192
+ * Target model. Must be specified for tool calls — the gateway never
193
+ * re-routes tool requests across providers since tool schemas are
194
+ * provider-specific.
195
+ */
196
+ model?: RouvaModel;
197
+ /**
198
+ * Force a specific provider when paired with `model`.
199
+ */
200
+ provider?: RouvaProvider;
201
+ /**
202
+ * Maximum number of agentic turns before the loop exits.
203
+ * Defaults to 10. Each turn = one model call (plus parallel tool dispatch).
204
+ */
205
+ max_turns?: number;
206
+ /**
207
+ * Maximum tokens the model may generate per turn.
208
+ * Defaults to 4096.
209
+ */
210
+ max_tokens?: number;
211
+ /**
212
+ * Sampling temperature 0–1.
213
+ */
214
+ temperature?: number;
215
+ /**
216
+ * Hard cap on total session spend in USD. The loop exits cleanly when the
217
+ * cumulative cost of all turns reaches this threshold.
218
+ */
219
+ session_budget_usd?: number;
220
+ }
221
+ interface RunResult {
222
+ /** Final model response text after all tool loops complete. */
223
+ content: string | null;
224
+ /** Number of agentic turns executed. */
225
+ turns: number;
226
+ /** Total number of tool calls dispatched across all turns. */
227
+ tool_calls_made: number;
228
+ /** Session ID — all turns are grouped under this ID in the Rouva dashboard. */
229
+ session_id: string;
230
+ /**
231
+ * Why the loop exited: `"stop"` (model finished), `"max_turns"` (turn limit
232
+ * reached), `"budget_exceeded"` (session_budget_usd hit), or a provider
233
+ * finish reason such as `"end_turn"` or `"length"`.
234
+ */
235
+ finish_reason: string | null;
236
+ /** USD cost of the final turn. */
237
+ cost_usd: number;
238
+ /** Cumulative USD cost of all turns in the session. */
239
+ session_cost_usd: number;
240
+ }
167
241
 
168
242
  declare class Rouva {
169
243
  private apiKey;
@@ -189,7 +263,29 @@ declare class Rouva {
189
263
  endSession(): void;
190
264
  /** The active session ID, or undefined if no session is in progress. */
191
265
  get sessionId(): string | undefined;
266
+ /**
267
+ * Run a server-side agentic loop. The gateway calls the model, dispatches
268
+ * tool calls in parallel to your handler URLs, and loops until the model
269
+ * stops or limits are reached — no streaming, no client-side loop required.
270
+ *
271
+ * Every turn is logged as a usage snapshot grouped by `session_id` in the
272
+ * Rouva dashboard, giving you a full session replay with per-turn cost,
273
+ * model, and tool call details.
274
+ *
275
+ * @example
276
+ * const result = await rouva.run({
277
+ * model: 'claude-haiku-4-5-20251001',
278
+ * messages: [{ role: 'user', content: 'What is the weather in Paris?' }],
279
+ * tools: [{ name: 'get_weather', description: '...', input_schema: { ... } }],
280
+ * tool_handlers: { get_weather: 'https://api.example.com/weather' },
281
+ * max_turns: 5,
282
+ * })
283
+ * console.log(result.content) // final model response
284
+ * console.log(result.turns) // how many turns it took
285
+ * console.log(result.session_id) // dashboard session ID
286
+ */
287
+ run(params: RunParams): Promise<RunResult>;
192
288
  private _createChatCompletion;
193
289
  }
194
290
 
195
- export { type AssistantToolCallMessage, type ChatCompletion, type ChatCompletionChoice, type ChatCompletionParams, type ChatCompletionUsage, type ChatMessage, type ContentBlockMessage, type FunctionToolCall, type Message, type ResponseMessage, Rouva, type RouvaOptions, type RouvaProvider, type RouvaResponseMeta, type SystemTextBlock, type ToolResultMessage };
291
+ export { type AssistantToolCallMessage, type ChatCompletion, type ChatCompletionChoice, type ChatCompletionParams, type ChatCompletionUsage, type ChatMessage, type ContentBlockMessage, type FunctionToolCall, type Message, type ResponseMessage, Rouva, type RouvaOptions, type RouvaProvider, type RouvaResponseMeta, type RunParams, type RunResult, type SystemTextBlock, type ToolResultMessage };
package/dist/index.js CHANGED
@@ -404,6 +404,43 @@ var Rouva = class {
404
404
  get sessionId() {
405
405
  return this._sessionId;
406
406
  }
407
+ /**
408
+ * Run a server-side agentic loop. The gateway calls the model, dispatches
409
+ * tool calls in parallel to your handler URLs, and loops until the model
410
+ * stops or limits are reached — no streaming, no client-side loop required.
411
+ *
412
+ * Every turn is logged as a usage snapshot grouped by `session_id` in the
413
+ * Rouva dashboard, giving you a full session replay with per-turn cost,
414
+ * model, and tool call details.
415
+ *
416
+ * @example
417
+ * const result = await rouva.run({
418
+ * model: 'claude-haiku-4-5-20251001',
419
+ * messages: [{ role: 'user', content: 'What is the weather in Paris?' }],
420
+ * tools: [{ name: 'get_weather', description: '...', input_schema: { ... } }],
421
+ * tool_handlers: { get_weather: 'https://api.example.com/weather' },
422
+ * max_turns: 5,
423
+ * })
424
+ * console.log(result.content) // final model response
425
+ * console.log(result.turns) // how many turns it took
426
+ * console.log(result.session_id) // dashboard session ID
427
+ */
428
+ async run(params) {
429
+ const url = `${this.baseURL}/api/gateway/run`;
430
+ const res = await fetch(url, {
431
+ method: "POST",
432
+ headers: {
433
+ "Content-Type": "application/json",
434
+ "Authorization": `Bearer ${this.apiKey}`
435
+ },
436
+ body: JSON.stringify(params)
437
+ });
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
+ }
407
444
  async _createChatCompletion(params) {
408
445
  const url = `${this.baseURL}/api/gateway/messages`;
409
446
  const { stream: _stream, ...body } = params;
package/dist/index.mjs CHANGED
@@ -378,6 +378,43 @@ var Rouva = class {
378
378
  get sessionId() {
379
379
  return this._sessionId;
380
380
  }
381
+ /**
382
+ * Run a server-side agentic loop. The gateway calls the model, dispatches
383
+ * tool calls in parallel to your handler URLs, and loops until the model
384
+ * stops or limits are reached — no streaming, no client-side loop required.
385
+ *
386
+ * Every turn is logged as a usage snapshot grouped by `session_id` in the
387
+ * Rouva dashboard, giving you a full session replay with per-turn cost,
388
+ * model, and tool call details.
389
+ *
390
+ * @example
391
+ * const result = await rouva.run({
392
+ * model: 'claude-haiku-4-5-20251001',
393
+ * messages: [{ role: 'user', content: 'What is the weather in Paris?' }],
394
+ * tools: [{ name: 'get_weather', description: '...', input_schema: { ... } }],
395
+ * tool_handlers: { get_weather: 'https://api.example.com/weather' },
396
+ * max_turns: 5,
397
+ * })
398
+ * console.log(result.content) // final model response
399
+ * console.log(result.turns) // how many turns it took
400
+ * console.log(result.session_id) // dashboard session ID
401
+ */
402
+ async run(params) {
403
+ const url = `${this.baseURL}/api/gateway/run`;
404
+ const res = await fetch(url, {
405
+ method: "POST",
406
+ headers: {
407
+ "Content-Type": "application/json",
408
+ "Authorization": `Bearer ${this.apiKey}`
409
+ },
410
+ body: JSON.stringify(params)
411
+ });
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
+ }
381
418
  async _createChatCompletion(params) {
382
419
  const url = `${this.baseURL}/api/gateway/messages`;
383
420
  const { stream: _stream, ...body } = params;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rouvanpm/rouva",
3
- "version": "0.2.10",
3
+ "version": "0.2.12",
4
4
  "description": "Official Node.js SDK for Rouva — managed AI gateway with intelligent routing and spend tracking",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",