@rouvanpm/rouva 0.1.4 → 0.2.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 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, and xAI Grok. You can request a specific model, force a specific provider, or omit both and let Rouva route to the cheapest capable model automatically.
27
+ Rouva works with all connected providers — Anthropic and OpenAI today, more coming. You can request a specific model or omit it entirely and let Rouva route to the cheapest capable model automatically.
28
28
 
29
29
  ```typescript
30
30
  // Request a specific model
@@ -33,10 +33,9 @@ const res = await rouva.chat.completions.create({
33
33
  messages,
34
34
  })
35
35
 
36
- // Force a specific provider + model
36
+ // Request a specific Anthropic model
37
37
  const res = await rouva.chat.completions.create({
38
- provider: 'xai',
39
- model: 'grok-4.3',
38
+ model: 'claude-sonnet-4-6',
40
39
  messages,
41
40
  })
42
41
 
@@ -46,7 +45,7 @@ const res = await rouva.chat.completions.create({
46
45
  })
47
46
  ```
48
47
 
49
- ## OpenAI-style request shape
48
+ ## Drop-in replacement for OpenAI
50
49
 
51
50
  ```typescript
52
51
  // Before
@@ -78,8 +77,6 @@ while (true) {
78
77
  }
79
78
  ```
80
79
 
81
- Streaming responses are normalized to OpenAI-style SSE chunks, even when Rouva routes the request to Anthropic.
82
-
83
80
  ## Options
84
81
 
85
82
  ```typescript
@@ -89,21 +86,6 @@ const rouva = new Rouva({
89
86
  })
90
87
  ```
91
88
 
92
- ## Response metadata
93
-
94
- Parsed non-stream responses may include a `_rouva` field with gateway header metadata when available:
95
-
96
- ```typescript
97
- const res = await rouva.chat.completions.create({ messages })
98
-
99
- console.log(res._rouva)
100
- // {
101
- // model_used: 'gpt-4o-mini',
102
- // provider_used: 'openai',
103
- // task_type: 'summarize'
104
- // }
105
- ```
106
-
107
89
  ## Getting your API key
108
90
 
109
91
  1. Sign in to [app.rouva.io](https://app.rouva.io)
@@ -112,4 +94,4 @@ console.log(res._rouva)
112
94
 
113
95
  ## License
114
96
 
115
- MIT
97
+ MIT
package/dist/index.d.mts CHANGED
@@ -8,24 +8,30 @@ interface Message {
8
8
  role: 'system' | 'user' | 'assistant';
9
9
  content: string;
10
10
  }
11
- type RouvaProvider = 'anthropic' | 'openai' | 'gemini' | 'deepseek' | 'mistral' | 'xai' | (string & {});
12
11
  /**
13
12
  * Supported models across all providers.
14
13
  * Omit `model` entirely to let Rouva route to the cheapest capable model automatically.
15
14
  */
16
- type RouvaModel = 'claude-opus-4-6' | 'claude-opus-4-7' | 'claude-opus-4-8' | 'claude-sonnet-4-6' | 'claude-haiku-4-5-20251001' | 'claude-fable-5' | 'gpt-5-nano' | 'gpt-5-mini' | 'gpt-5' | 'gpt-5.5' | 'gpt-5.5-pro' | 'gpt-4.1-nano' | 'gpt-4.1-mini' | 'gpt-4.1' | 'gpt-4o' | 'gpt-4o-mini' | 'gemini-2.5-flash' | 'gemini-2.5-pro' | 'deepseek-chat' | 'deepseek-reasoner' | 'mistral-small-latest' | 'mistral-large-latest' | 'grok-4.3' | 'grok-4.20-0309-reasoning' | (string & {});
15
+ type RouvaModel = 'claude-opus-4-6' | 'claude-sonnet-4-6' | 'claude-haiku-4-5-20251001' | 'gpt-4o' | 'gpt-4o-mini' | 'gpt-4-turbo' | 'gpt-3.5-turbo' | (string & {});
16
+ /** Anthropic-style text block — the array form of the `system` param */
17
+ interface SystemTextBlock {
18
+ type: 'text';
19
+ text: string;
20
+ }
17
21
  interface ChatCompletionParams {
18
22
  messages: Message[];
19
23
  /**
20
- * Target model — omit to let Rouva route intelligently to the cheapest capable model.
21
- * Supports models from any connected provider.
24
+ * System prompt — a plain string or Anthropic-style text blocks.
25
+ * Equivalent to a `role: "system"` message at the head of `messages`;
26
+ * the gateway accepts both forms and re-emits whichever dialect the
27
+ * routed provider expects, so it survives cross-provider routing.
22
28
  */
23
- model?: RouvaModel;
29
+ system?: string | SystemTextBlock[];
24
30
  /**
25
- * Force an exact provider when paired with `model`.
26
- * Omit to let Rouva auto-route based on your connected keys.
31
+ * Target model — omit to let Rouva route intelligently to the cheapest capable model.
32
+ * Supports models from any connected provider (Anthropic, OpenAI).
27
33
  */
28
- provider?: RouvaProvider;
34
+ model?: RouvaModel;
29
35
  /** Maximum tokens to generate */
30
36
  max_tokens?: number;
31
37
  /** Sampling temperature 0–1 */
@@ -54,14 +60,16 @@ interface ChatCompletion {
54
60
  _rouva?: RouvaResponseMeta;
55
61
  }
56
62
  interface RouvaResponseMeta {
57
- /** Actual model used when exposed by the gateway */
58
- model_used?: string;
59
- /** Actual provider used when exposed by the gateway */
60
- provider_used?: string;
61
- /** Task type classified by Rouva when exposed by the gateway */
62
- task_type?: string;
63
- /** Semantic cache status when exposed by the gateway */
64
- cache?: string;
63
+ /** Actual model used (may differ from requested when intelligently routed) */
64
+ model_used: string;
65
+ /** USD cost for this request */
66
+ cost: number;
67
+ /** USD saved vs your baseline model */
68
+ savings: number;
69
+ /** Whether intelligent routing selected the model */
70
+ intelligently_routed: boolean;
71
+ /** Task type classified by Rouva */
72
+ task_type: string;
65
73
  }
66
74
 
67
75
  declare class Rouva {
@@ -76,4 +84,4 @@ declare class Rouva {
76
84
  private _createChatCompletion;
77
85
  }
78
86
 
79
- export { type ChatCompletion, type ChatCompletionChoice, type ChatCompletionParams, type ChatCompletionUsage, type Message, Rouva, type RouvaOptions, type RouvaProvider, type RouvaResponseMeta };
87
+ export { type ChatCompletion, type ChatCompletionChoice, type ChatCompletionParams, type ChatCompletionUsage, type Message, Rouva, type RouvaOptions, type RouvaResponseMeta, type SystemTextBlock };
package/dist/index.d.ts CHANGED
@@ -8,24 +8,30 @@ interface Message {
8
8
  role: 'system' | 'user' | 'assistant';
9
9
  content: string;
10
10
  }
11
- type RouvaProvider = 'anthropic' | 'openai' | 'gemini' | 'deepseek' | 'mistral' | 'xai' | (string & {});
12
11
  /**
13
12
  * Supported models across all providers.
14
13
  * Omit `model` entirely to let Rouva route to the cheapest capable model automatically.
15
14
  */
16
- type RouvaModel = 'claude-opus-4-6' | 'claude-opus-4-7' | 'claude-opus-4-8' | 'claude-sonnet-4-6' | 'claude-haiku-4-5-20251001' | 'claude-fable-5' | 'gpt-5-nano' | 'gpt-5-mini' | 'gpt-5' | 'gpt-5.5' | 'gpt-5.5-pro' | 'gpt-4.1-nano' | 'gpt-4.1-mini' | 'gpt-4.1' | 'gpt-4o' | 'gpt-4o-mini' | 'gemini-2.5-flash' | 'gemini-2.5-pro' | 'deepseek-chat' | 'deepseek-reasoner' | 'mistral-small-latest' | 'mistral-large-latest' | 'grok-4.3' | 'grok-4.20-0309-reasoning' | (string & {});
15
+ type RouvaModel = 'claude-opus-4-6' | 'claude-sonnet-4-6' | 'claude-haiku-4-5-20251001' | 'gpt-4o' | 'gpt-4o-mini' | 'gpt-4-turbo' | 'gpt-3.5-turbo' | (string & {});
16
+ /** Anthropic-style text block — the array form of the `system` param */
17
+ interface SystemTextBlock {
18
+ type: 'text';
19
+ text: string;
20
+ }
17
21
  interface ChatCompletionParams {
18
22
  messages: Message[];
19
23
  /**
20
- * Target model — omit to let Rouva route intelligently to the cheapest capable model.
21
- * Supports models from any connected provider.
24
+ * System prompt — a plain string or Anthropic-style text blocks.
25
+ * Equivalent to a `role: "system"` message at the head of `messages`;
26
+ * the gateway accepts both forms and re-emits whichever dialect the
27
+ * routed provider expects, so it survives cross-provider routing.
22
28
  */
23
- model?: RouvaModel;
29
+ system?: string | SystemTextBlock[];
24
30
  /**
25
- * Force an exact provider when paired with `model`.
26
- * Omit to let Rouva auto-route based on your connected keys.
31
+ * Target model — omit to let Rouva route intelligently to the cheapest capable model.
32
+ * Supports models from any connected provider (Anthropic, OpenAI).
27
33
  */
28
- provider?: RouvaProvider;
34
+ model?: RouvaModel;
29
35
  /** Maximum tokens to generate */
30
36
  max_tokens?: number;
31
37
  /** Sampling temperature 0–1 */
@@ -54,14 +60,16 @@ interface ChatCompletion {
54
60
  _rouva?: RouvaResponseMeta;
55
61
  }
56
62
  interface RouvaResponseMeta {
57
- /** Actual model used when exposed by the gateway */
58
- model_used?: string;
59
- /** Actual provider used when exposed by the gateway */
60
- provider_used?: string;
61
- /** Task type classified by Rouva when exposed by the gateway */
62
- task_type?: string;
63
- /** Semantic cache status when exposed by the gateway */
64
- cache?: string;
63
+ /** Actual model used (may differ from requested when intelligently routed) */
64
+ model_used: string;
65
+ /** USD cost for this request */
66
+ cost: number;
67
+ /** USD saved vs your baseline model */
68
+ savings: number;
69
+ /** Whether intelligent routing selected the model */
70
+ intelligently_routed: boolean;
71
+ /** Task type classified by Rouva */
72
+ task_type: string;
65
73
  }
66
74
 
67
75
  declare class Rouva {
@@ -76,4 +84,4 @@ declare class Rouva {
76
84
  private _createChatCompletion;
77
85
  }
78
86
 
79
- export { type ChatCompletion, type ChatCompletionChoice, type ChatCompletionParams, type ChatCompletionUsage, type Message, Rouva, type RouvaOptions, type RouvaProvider, type RouvaResponseMeta };
87
+ export { type ChatCompletion, type ChatCompletionChoice, type ChatCompletionParams, type ChatCompletionUsage, type Message, Rouva, type RouvaOptions, type RouvaResponseMeta, type SystemTextBlock };
package/dist/index.js CHANGED
@@ -24,273 +24,6 @@ __export(index_exports, {
24
24
  });
25
25
  module.exports = __toCommonJS(index_exports);
26
26
 
27
- // src/sse.ts
28
- function parseSseEvent(line) {
29
- const raw = line.trim();
30
- if (!raw.startsWith("data:")) return null;
31
- const data = raw.slice(5).trim();
32
- if (!data) return null;
33
- try {
34
- return { raw, data, json: data === "[DONE]" ? null : JSON.parse(data) };
35
- } catch {
36
- return { raw, data, json: null };
37
- }
38
- }
39
- function stringifySse(payload) {
40
- return `data: ${JSON.stringify(payload)}
41
-
42
- `;
43
- }
44
- function detectProvider(payload) {
45
- if (!payload || typeof payload !== "object") return null;
46
- if ("choices" in payload || "usage" in payload) return "openai";
47
- if ("type" in payload) return "anthropic";
48
- return null;
49
- }
50
- function normalizeChatId(id) {
51
- return id.startsWith("chatcmpl_") ? id : `chatcmpl_${id}`;
52
- }
53
- function toFinishReason(stopReason) {
54
- if (!stopReason) return null;
55
- if (stopReason === "end_turn" || stopReason === "stop_sequence") return "stop";
56
- if (stopReason === "max_tokens") return "length";
57
- return stopReason;
58
- }
59
- function initialState() {
60
- return {
61
- provider: null,
62
- id: `chatcmpl_${Date.now()}`,
63
- model: "unknown",
64
- created: Math.floor(Date.now() / 1e3),
65
- promptTokens: 0,
66
- completionTokens: 0,
67
- roleEmitted: false,
68
- finalChunkEmitted: false
69
- };
70
- }
71
- function normalizeAnthropicPayload(payload, state) {
72
- const eventType = typeof payload.type === "string" ? payload.type : null;
73
- const chunks = [];
74
- if (eventType === "message_start") {
75
- const message = payload.message ?? {};
76
- const usage = message.usage ?? {};
77
- state.id = normalizeChatId(typeof message.id === "string" ? message.id : state.id);
78
- state.model = typeof message.model === "string" ? message.model : state.model;
79
- state.promptTokens = typeof usage.input_tokens === "number" ? usage.input_tokens : 0;
80
- if (!state.roleEmitted) {
81
- chunks.push(stringifySse({
82
- id: state.id,
83
- object: "chat.completion.chunk",
84
- created: state.created,
85
- model: state.model,
86
- choices: [{ index: 0, delta: { role: "assistant" }, finish_reason: null }]
87
- }));
88
- state.roleEmitted = true;
89
- }
90
- }
91
- if (eventType === "content_block_delta") {
92
- const delta = payload.delta ?? {};
93
- if (delta.type === "text_delta" && typeof delta.text === "string") {
94
- if (!state.roleEmitted) {
95
- chunks.push(stringifySse({
96
- id: state.id,
97
- object: "chat.completion.chunk",
98
- created: state.created,
99
- model: state.model,
100
- choices: [{ index: 0, delta: { role: "assistant" }, finish_reason: null }]
101
- }));
102
- state.roleEmitted = true;
103
- }
104
- chunks.push(stringifySse({
105
- id: state.id,
106
- object: "chat.completion.chunk",
107
- created: state.created,
108
- model: state.model,
109
- choices: [{ index: 0, delta: { content: delta.text }, finish_reason: null }]
110
- }));
111
- }
112
- }
113
- if (eventType === "message_delta") {
114
- const delta = payload.delta ?? {};
115
- const usage = payload.usage ?? {};
116
- state.completionTokens = typeof usage.output_tokens === "number" ? usage.output_tokens : state.completionTokens;
117
- chunks.push(stringifySse({
118
- id: state.id,
119
- object: "chat.completion.chunk",
120
- created: state.created,
121
- model: state.model,
122
- choices: [{ index: 0, delta: {}, finish_reason: toFinishReason(typeof delta.stop_reason === "string" ? delta.stop_reason : null) }],
123
- usage: {
124
- prompt_tokens: state.promptTokens,
125
- completion_tokens: state.completionTokens,
126
- total_tokens: state.promptTokens + state.completionTokens
127
- }
128
- }));
129
- state.finalChunkEmitted = true;
130
- }
131
- if (eventType === "message_stop") {
132
- chunks.push("data: [DONE]\n\n");
133
- }
134
- if (eventType === "error") {
135
- chunks.push(stringifySse(payload));
136
- }
137
- return chunks;
138
- }
139
- function normalizeGatewayStream(stream) {
140
- const encoder = new TextEncoder();
141
- const decoder = new TextDecoder();
142
- let buffer = "";
143
- const state = initialState();
144
- return new ReadableStream({
145
- async start(controller) {
146
- const reader = stream.getReader();
147
- try {
148
- while (true) {
149
- const { done, value } = await reader.read();
150
- if (done) break;
151
- if (!value) continue;
152
- buffer += decoder.decode(value, { stream: true });
153
- const lines = buffer.split("\n");
154
- buffer = lines.pop() ?? "";
155
- for (const line of lines) {
156
- const event = parseSseEvent(line);
157
- if (!event) continue;
158
- if (event.data === "[DONE]") {
159
- controller.enqueue(encoder.encode("data: [DONE]\n\n"));
160
- continue;
161
- }
162
- const provider = detectProvider(event.json);
163
- if (provider && !state.provider) state.provider = provider;
164
- if (state.provider === "anthropic" && event.json && typeof event.json === "object") {
165
- for (const chunk of normalizeAnthropicPayload(event.json, state)) {
166
- controller.enqueue(encoder.encode(chunk));
167
- }
168
- continue;
169
- }
170
- controller.enqueue(encoder.encode(`${event.raw}
171
-
172
- `));
173
- }
174
- }
175
- if (buffer.trim()) {
176
- const event = parseSseEvent(buffer);
177
- if (event) {
178
- if (event.data === "[DONE]") {
179
- controller.enqueue(encoder.encode("data: [DONE]\n\n"));
180
- } else if (state.provider === "anthropic" && event.json && typeof event.json === "object") {
181
- for (const chunk of normalizeAnthropicPayload(event.json, state)) {
182
- controller.enqueue(encoder.encode(chunk));
183
- }
184
- } else {
185
- controller.enqueue(encoder.encode(`${event.raw}
186
-
187
- `));
188
- }
189
- }
190
- }
191
- } finally {
192
- controller.close();
193
- }
194
- }
195
- });
196
- }
197
- async function readChatCompletionFromSse(stream, metadata) {
198
- const reader = stream.getReader();
199
- const decoder = new TextDecoder();
200
- let buffer = "";
201
- let id = `chatcmpl_${Date.now()}`;
202
- let model = metadata?.model_used ?? "unknown";
203
- let created = Math.floor(Date.now() / 1e3);
204
- let role = "assistant";
205
- let content = "";
206
- let finishReason = null;
207
- let usage = {
208
- prompt_tokens: 0,
209
- completion_tokens: 0,
210
- total_tokens: 0
211
- };
212
- try {
213
- while (true) {
214
- const { done, value } = await reader.read();
215
- if (done) break;
216
- if (!value) continue;
217
- buffer += decoder.decode(value, { stream: true });
218
- const lines = buffer.split("\n");
219
- buffer = lines.pop() ?? "";
220
- for (const line of lines) {
221
- const event = parseSseEvent(line);
222
- if (!event || event.data === "[DONE]") continue;
223
- if (!event.json || typeof event.json !== "object") continue;
224
- const payload = event.json;
225
- if (payload.type === "error" && payload.error) {
226
- const error = payload.error;
227
- throw new Error(
228
- `[Rouva] Upstream error in stream: ${typeof error.message === "string" ? error.message : "Unknown error"}`
229
- );
230
- }
231
- if (typeof payload.id === "string") id = payload.id;
232
- if (typeof payload.model === "string") model = payload.model;
233
- if (typeof payload.created === "number") created = payload.created;
234
- const choices = Array.isArray(payload.choices) ? payload.choices : [];
235
- const firstChoice = choices[0];
236
- const delta = firstChoice?.delta;
237
- if (delta?.role === "assistant") {
238
- role = "assistant";
239
- }
240
- if (typeof delta?.content === "string") {
241
- content += delta.content;
242
- }
243
- if (typeof firstChoice?.finish_reason === "string" || firstChoice?.finish_reason === null) {
244
- finishReason = firstChoice.finish_reason;
245
- }
246
- const payloadUsage = payload.usage;
247
- if (payloadUsage) {
248
- const promptTokens = typeof payloadUsage.prompt_tokens === "number" ? payloadUsage.prompt_tokens : 0;
249
- const completionTokens = typeof payloadUsage.completion_tokens === "number" ? payloadUsage.completion_tokens : 0;
250
- usage = {
251
- prompt_tokens: promptTokens,
252
- completion_tokens: completionTokens,
253
- total_tokens: typeof payloadUsage.total_tokens === "number" ? payloadUsage.total_tokens : promptTokens + completionTokens
254
- };
255
- }
256
- }
257
- }
258
- } finally {
259
- reader.releaseLock();
260
- }
261
- const response = {
262
- id,
263
- object: "chat.completion",
264
- created,
265
- model,
266
- choices: [
267
- {
268
- index: 0,
269
- message: { role, content },
270
- finish_reason: finishReason
271
- }
272
- ],
273
- usage
274
- };
275
- if (metadata && Object.keys(metadata).length > 0) {
276
- response._rouva = metadata;
277
- }
278
- return response;
279
- }
280
- function getRouvaMetadata(headers) {
281
- const modelUsed = headers.get("x-rouva-model") ?? void 0;
282
- const providerUsed = headers.get("x-rouva-provider") ?? void 0;
283
- const taskType = headers.get("x-rouva-task") ?? void 0;
284
- const cache = headers.get("x-rouva-cache") ?? void 0;
285
- if (!modelUsed && !providerUsed && !taskType && !cache) return void 0;
286
- return {
287
- model_used: modelUsed,
288
- provider_used: providerUsed,
289
- task_type: taskType,
290
- cache
291
- };
292
- }
293
-
294
27
  // src/client.ts
295
28
  var DEFAULT_BASE_URL = "https://app.rouva.io";
296
29
  var Rouva = class {
@@ -313,7 +46,7 @@ var Rouva = class {
313
46
  method: "POST",
314
47
  headers: {
315
48
  "Content-Type": "application/json",
316
- "Authorization": `Bearer ${this.apiKey}`
49
+ "x-api-key": this.apiKey
317
50
  },
318
51
  body: JSON.stringify(params)
319
52
  });
@@ -321,10 +54,11 @@ var Rouva = class {
321
54
  const body = await res.text().catch(() => res.statusText);
322
55
  throw new Error(`[Rouva] Gateway error ${res.status}: ${body}`);
323
56
  }
324
- if (!res.body) throw new Error("[Rouva] No response body from gateway");
325
- const normalizedStream = normalizeGatewayStream(res.body);
326
- if (params.stream) return normalizedStream;
327
- return readChatCompletionFromSse(normalizedStream, getRouvaMetadata(res.headers));
57
+ if (params.stream) {
58
+ if (!res.body) throw new Error("[Rouva] No response body for streaming request");
59
+ return res.body;
60
+ }
61
+ return res.json();
328
62
  }
329
63
  };
330
64
  // Annotate the CommonJS export names for ESM import in node:
package/dist/index.mjs CHANGED
@@ -1,270 +1,3 @@
1
- // src/sse.ts
2
- function parseSseEvent(line) {
3
- const raw = line.trim();
4
- if (!raw.startsWith("data:")) return null;
5
- const data = raw.slice(5).trim();
6
- if (!data) return null;
7
- try {
8
- return { raw, data, json: data === "[DONE]" ? null : JSON.parse(data) };
9
- } catch {
10
- return { raw, data, json: null };
11
- }
12
- }
13
- function stringifySse(payload) {
14
- return `data: ${JSON.stringify(payload)}
15
-
16
- `;
17
- }
18
- function detectProvider(payload) {
19
- if (!payload || typeof payload !== "object") return null;
20
- if ("choices" in payload || "usage" in payload) return "openai";
21
- if ("type" in payload) return "anthropic";
22
- return null;
23
- }
24
- function normalizeChatId(id) {
25
- return id.startsWith("chatcmpl_") ? id : `chatcmpl_${id}`;
26
- }
27
- function toFinishReason(stopReason) {
28
- if (!stopReason) return null;
29
- if (stopReason === "end_turn" || stopReason === "stop_sequence") return "stop";
30
- if (stopReason === "max_tokens") return "length";
31
- return stopReason;
32
- }
33
- function initialState() {
34
- return {
35
- provider: null,
36
- id: `chatcmpl_${Date.now()}`,
37
- model: "unknown",
38
- created: Math.floor(Date.now() / 1e3),
39
- promptTokens: 0,
40
- completionTokens: 0,
41
- roleEmitted: false,
42
- finalChunkEmitted: false
43
- };
44
- }
45
- function normalizeAnthropicPayload(payload, state) {
46
- const eventType = typeof payload.type === "string" ? payload.type : null;
47
- const chunks = [];
48
- if (eventType === "message_start") {
49
- const message = payload.message ?? {};
50
- const usage = message.usage ?? {};
51
- state.id = normalizeChatId(typeof message.id === "string" ? message.id : state.id);
52
- state.model = typeof message.model === "string" ? message.model : state.model;
53
- state.promptTokens = typeof usage.input_tokens === "number" ? usage.input_tokens : 0;
54
- if (!state.roleEmitted) {
55
- chunks.push(stringifySse({
56
- id: state.id,
57
- object: "chat.completion.chunk",
58
- created: state.created,
59
- model: state.model,
60
- choices: [{ index: 0, delta: { role: "assistant" }, finish_reason: null }]
61
- }));
62
- state.roleEmitted = true;
63
- }
64
- }
65
- if (eventType === "content_block_delta") {
66
- const delta = payload.delta ?? {};
67
- if (delta.type === "text_delta" && typeof delta.text === "string") {
68
- if (!state.roleEmitted) {
69
- chunks.push(stringifySse({
70
- id: state.id,
71
- object: "chat.completion.chunk",
72
- created: state.created,
73
- model: state.model,
74
- choices: [{ index: 0, delta: { role: "assistant" }, finish_reason: null }]
75
- }));
76
- state.roleEmitted = true;
77
- }
78
- chunks.push(stringifySse({
79
- id: state.id,
80
- object: "chat.completion.chunk",
81
- created: state.created,
82
- model: state.model,
83
- choices: [{ index: 0, delta: { content: delta.text }, finish_reason: null }]
84
- }));
85
- }
86
- }
87
- if (eventType === "message_delta") {
88
- const delta = payload.delta ?? {};
89
- const usage = payload.usage ?? {};
90
- state.completionTokens = typeof usage.output_tokens === "number" ? usage.output_tokens : state.completionTokens;
91
- chunks.push(stringifySse({
92
- id: state.id,
93
- object: "chat.completion.chunk",
94
- created: state.created,
95
- model: state.model,
96
- choices: [{ index: 0, delta: {}, finish_reason: toFinishReason(typeof delta.stop_reason === "string" ? delta.stop_reason : null) }],
97
- usage: {
98
- prompt_tokens: state.promptTokens,
99
- completion_tokens: state.completionTokens,
100
- total_tokens: state.promptTokens + state.completionTokens
101
- }
102
- }));
103
- state.finalChunkEmitted = true;
104
- }
105
- if (eventType === "message_stop") {
106
- chunks.push("data: [DONE]\n\n");
107
- }
108
- if (eventType === "error") {
109
- chunks.push(stringifySse(payload));
110
- }
111
- return chunks;
112
- }
113
- function normalizeGatewayStream(stream) {
114
- const encoder = new TextEncoder();
115
- const decoder = new TextDecoder();
116
- let buffer = "";
117
- const state = initialState();
118
- return new ReadableStream({
119
- async start(controller) {
120
- const reader = stream.getReader();
121
- try {
122
- while (true) {
123
- const { done, value } = await reader.read();
124
- if (done) break;
125
- if (!value) continue;
126
- buffer += decoder.decode(value, { stream: true });
127
- const lines = buffer.split("\n");
128
- buffer = lines.pop() ?? "";
129
- for (const line of lines) {
130
- const event = parseSseEvent(line);
131
- if (!event) continue;
132
- if (event.data === "[DONE]") {
133
- controller.enqueue(encoder.encode("data: [DONE]\n\n"));
134
- continue;
135
- }
136
- const provider = detectProvider(event.json);
137
- if (provider && !state.provider) state.provider = provider;
138
- if (state.provider === "anthropic" && event.json && typeof event.json === "object") {
139
- for (const chunk of normalizeAnthropicPayload(event.json, state)) {
140
- controller.enqueue(encoder.encode(chunk));
141
- }
142
- continue;
143
- }
144
- controller.enqueue(encoder.encode(`${event.raw}
145
-
146
- `));
147
- }
148
- }
149
- if (buffer.trim()) {
150
- const event = parseSseEvent(buffer);
151
- if (event) {
152
- if (event.data === "[DONE]") {
153
- controller.enqueue(encoder.encode("data: [DONE]\n\n"));
154
- } else if (state.provider === "anthropic" && event.json && typeof event.json === "object") {
155
- for (const chunk of normalizeAnthropicPayload(event.json, state)) {
156
- controller.enqueue(encoder.encode(chunk));
157
- }
158
- } else {
159
- controller.enqueue(encoder.encode(`${event.raw}
160
-
161
- `));
162
- }
163
- }
164
- }
165
- } finally {
166
- controller.close();
167
- }
168
- }
169
- });
170
- }
171
- async function readChatCompletionFromSse(stream, metadata) {
172
- const reader = stream.getReader();
173
- const decoder = new TextDecoder();
174
- let buffer = "";
175
- let id = `chatcmpl_${Date.now()}`;
176
- let model = metadata?.model_used ?? "unknown";
177
- let created = Math.floor(Date.now() / 1e3);
178
- let role = "assistant";
179
- let content = "";
180
- let finishReason = null;
181
- let usage = {
182
- prompt_tokens: 0,
183
- completion_tokens: 0,
184
- total_tokens: 0
185
- };
186
- try {
187
- while (true) {
188
- const { done, value } = await reader.read();
189
- if (done) break;
190
- if (!value) continue;
191
- buffer += decoder.decode(value, { stream: true });
192
- const lines = buffer.split("\n");
193
- buffer = lines.pop() ?? "";
194
- for (const line of lines) {
195
- const event = parseSseEvent(line);
196
- if (!event || event.data === "[DONE]") continue;
197
- if (!event.json || typeof event.json !== "object") continue;
198
- const payload = event.json;
199
- if (payload.type === "error" && payload.error) {
200
- const error = payload.error;
201
- throw new Error(
202
- `[Rouva] Upstream error in stream: ${typeof error.message === "string" ? error.message : "Unknown error"}`
203
- );
204
- }
205
- if (typeof payload.id === "string") id = payload.id;
206
- if (typeof payload.model === "string") model = payload.model;
207
- if (typeof payload.created === "number") created = payload.created;
208
- const choices = Array.isArray(payload.choices) ? payload.choices : [];
209
- const firstChoice = choices[0];
210
- const delta = firstChoice?.delta;
211
- if (delta?.role === "assistant") {
212
- role = "assistant";
213
- }
214
- if (typeof delta?.content === "string") {
215
- content += delta.content;
216
- }
217
- if (typeof firstChoice?.finish_reason === "string" || firstChoice?.finish_reason === null) {
218
- finishReason = firstChoice.finish_reason;
219
- }
220
- const payloadUsage = payload.usage;
221
- if (payloadUsage) {
222
- const promptTokens = typeof payloadUsage.prompt_tokens === "number" ? payloadUsage.prompt_tokens : 0;
223
- const completionTokens = typeof payloadUsage.completion_tokens === "number" ? payloadUsage.completion_tokens : 0;
224
- usage = {
225
- prompt_tokens: promptTokens,
226
- completion_tokens: completionTokens,
227
- total_tokens: typeof payloadUsage.total_tokens === "number" ? payloadUsage.total_tokens : promptTokens + completionTokens
228
- };
229
- }
230
- }
231
- }
232
- } finally {
233
- reader.releaseLock();
234
- }
235
- const response = {
236
- id,
237
- object: "chat.completion",
238
- created,
239
- model,
240
- choices: [
241
- {
242
- index: 0,
243
- message: { role, content },
244
- finish_reason: finishReason
245
- }
246
- ],
247
- usage
248
- };
249
- if (metadata && Object.keys(metadata).length > 0) {
250
- response._rouva = metadata;
251
- }
252
- return response;
253
- }
254
- function getRouvaMetadata(headers) {
255
- const modelUsed = headers.get("x-rouva-model") ?? void 0;
256
- const providerUsed = headers.get("x-rouva-provider") ?? void 0;
257
- const taskType = headers.get("x-rouva-task") ?? void 0;
258
- const cache = headers.get("x-rouva-cache") ?? void 0;
259
- if (!modelUsed && !providerUsed && !taskType && !cache) return void 0;
260
- return {
261
- model_used: modelUsed,
262
- provider_used: providerUsed,
263
- task_type: taskType,
264
- cache
265
- };
266
- }
267
-
268
1
  // src/client.ts
269
2
  var DEFAULT_BASE_URL = "https://app.rouva.io";
270
3
  var Rouva = class {
@@ -287,7 +20,7 @@ var Rouva = class {
287
20
  method: "POST",
288
21
  headers: {
289
22
  "Content-Type": "application/json",
290
- "Authorization": `Bearer ${this.apiKey}`
23
+ "x-api-key": this.apiKey
291
24
  },
292
25
  body: JSON.stringify(params)
293
26
  });
@@ -295,10 +28,11 @@ var Rouva = class {
295
28
  const body = await res.text().catch(() => res.statusText);
296
29
  throw new Error(`[Rouva] Gateway error ${res.status}: ${body}`);
297
30
  }
298
- if (!res.body) throw new Error("[Rouva] No response body from gateway");
299
- const normalizedStream = normalizeGatewayStream(res.body);
300
- if (params.stream) return normalizedStream;
301
- return readChatCompletionFromSse(normalizedStream, getRouvaMetadata(res.headers));
31
+ if (params.stream) {
32
+ if (!res.body) throw new Error("[Rouva] No response body for streaming request");
33
+ return res.body;
34
+ }
35
+ return res.json();
302
36
  }
303
37
  };
304
38
  export {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rouvanpm/rouva",
3
- "version": "0.1.4",
3
+ "version": "0.2.0",
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",
@@ -38,4 +38,4 @@
38
38
  "typescript": "^5.0.0",
39
39
  "vitest": "^1.0.0"
40
40
  }
41
- }
41
+ }