@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 +5 -23
- package/dist/index.d.mts +25 -17
- package/dist/index.d.ts +25 -17
- package/dist/index.js +6 -272
- package/dist/index.mjs +6 -272
- package/package.json +2 -2
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
|
|
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
|
-
//
|
|
36
|
+
// Request a specific Anthropic model
|
|
37
37
|
const res = await rouva.chat.completions.create({
|
|
38
|
-
|
|
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
|
-
##
|
|
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-
|
|
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
|
-
*
|
|
21
|
-
*
|
|
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
|
-
|
|
29
|
+
system?: string | SystemTextBlock[];
|
|
24
30
|
/**
|
|
25
|
-
*
|
|
26
|
-
*
|
|
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
|
-
|
|
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
|
|
58
|
-
model_used
|
|
59
|
-
/**
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
|
|
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
|
|
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-
|
|
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
|
-
*
|
|
21
|
-
*
|
|
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
|
-
|
|
29
|
+
system?: string | SystemTextBlock[];
|
|
24
30
|
/**
|
|
25
|
-
*
|
|
26
|
-
*
|
|
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
|
-
|
|
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
|
|
58
|
-
model_used
|
|
59
|
-
/**
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
|
|
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
|
|
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
|
-
"
|
|
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 (
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
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
|
-
"
|
|
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 (
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
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.
|
|
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
|
+
}
|