@rouvanpm/rouva 0.1.2 → 0.1.3
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 +10 -9
- package/dist/index.d.mts +79 -0
- package/dist/index.d.ts +79 -0
- package/dist/index.js +333 -0
- package/dist/index.mjs +306 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -24,7 +24,7 @@ console.log(response.choices[0].message.content)
|
|
|
24
24
|
|
|
25
25
|
## Provider agnostic
|
|
26
26
|
|
|
27
|
-
Rouva works with all connected providers — Anthropic
|
|
27
|
+
Rouva works with all connected providers — Anthropic, OpenAI, Gemini, DeepSeek, and Mistral. You can request a specific model, force a specific provider, or omit both and let Rouva route to the cheapest capable model automatically.
|
|
28
28
|
|
|
29
29
|
```typescript
|
|
30
30
|
// Request a specific model
|
|
@@ -33,9 +33,10 @@ const res = await rouva.chat.completions.create({
|
|
|
33
33
|
messages,
|
|
34
34
|
})
|
|
35
35
|
|
|
36
|
-
//
|
|
36
|
+
// Force a specific provider + model
|
|
37
37
|
const res = await rouva.chat.completions.create({
|
|
38
|
-
|
|
38
|
+
provider: 'gemini',
|
|
39
|
+
model: 'gemini-2.5-pro',
|
|
39
40
|
messages,
|
|
40
41
|
})
|
|
41
42
|
|
|
@@ -45,7 +46,7 @@ const res = await rouva.chat.completions.create({
|
|
|
45
46
|
})
|
|
46
47
|
```
|
|
47
48
|
|
|
48
|
-
##
|
|
49
|
+
## OpenAI-style request shape
|
|
49
50
|
|
|
50
51
|
```typescript
|
|
51
52
|
// Before
|
|
@@ -77,6 +78,8 @@ while (true) {
|
|
|
77
78
|
}
|
|
78
79
|
```
|
|
79
80
|
|
|
81
|
+
Streaming responses are normalized to OpenAI-style SSE chunks, even when Rouva routes the request to Anthropic.
|
|
82
|
+
|
|
80
83
|
## Options
|
|
81
84
|
|
|
82
85
|
```typescript
|
|
@@ -88,7 +91,7 @@ const rouva = new Rouva({
|
|
|
88
91
|
|
|
89
92
|
## Response metadata
|
|
90
93
|
|
|
91
|
-
|
|
94
|
+
Parsed non-stream responses may include a `_rouva` field with gateway header metadata when available:
|
|
92
95
|
|
|
93
96
|
```typescript
|
|
94
97
|
const res = await rouva.chat.completions.create({ messages })
|
|
@@ -96,9 +99,7 @@ const res = await rouva.chat.completions.create({ messages })
|
|
|
96
99
|
console.log(res._rouva)
|
|
97
100
|
// {
|
|
98
101
|
// model_used: 'gpt-4o-mini',
|
|
99
|
-
//
|
|
100
|
-
// savings: 0.000088,
|
|
101
|
-
// intelligently_routed: true,
|
|
102
|
+
// provider_used: 'openai',
|
|
102
103
|
// task_type: 'summarize'
|
|
103
104
|
// }
|
|
104
105
|
```
|
|
@@ -111,4 +112,4 @@ console.log(res._rouva)
|
|
|
111
112
|
|
|
112
113
|
## License
|
|
113
114
|
|
|
114
|
-
MIT
|
|
115
|
+
MIT
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
interface RouvaOptions {
|
|
2
|
+
/** Your Rouva gateway API key (rva_...) */
|
|
3
|
+
apiKey: string;
|
|
4
|
+
/** Override the default gateway URL — useful for self-hosted or testing */
|
|
5
|
+
baseURL?: string;
|
|
6
|
+
}
|
|
7
|
+
interface Message {
|
|
8
|
+
role: 'system' | 'user' | 'assistant';
|
|
9
|
+
content: string;
|
|
10
|
+
}
|
|
11
|
+
type RouvaProvider = 'anthropic' | 'openai' | 'gemini' | 'deepseek' | 'mistral' | (string & {});
|
|
12
|
+
/**
|
|
13
|
+
* Supported models across all providers.
|
|
14
|
+
* Omit `model` entirely to let Rouva route to the cheapest capable model automatically.
|
|
15
|
+
*/
|
|
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' | (string & {});
|
|
17
|
+
interface ChatCompletionParams {
|
|
18
|
+
messages: Message[];
|
|
19
|
+
/**
|
|
20
|
+
* Target model — omit to let Rouva route intelligently to the cheapest capable model.
|
|
21
|
+
* Supports models from any connected provider (Anthropic, OpenAI).
|
|
22
|
+
*/
|
|
23
|
+
model?: RouvaModel;
|
|
24
|
+
/**
|
|
25
|
+
* Force an exact provider when paired with `model`.
|
|
26
|
+
* Omit to let Rouva auto-route based on your connected keys.
|
|
27
|
+
*/
|
|
28
|
+
provider?: RouvaProvider;
|
|
29
|
+
/** Maximum tokens to generate */
|
|
30
|
+
max_tokens?: number;
|
|
31
|
+
/** Sampling temperature 0–1 */
|
|
32
|
+
temperature?: number;
|
|
33
|
+
/** Stream the response */
|
|
34
|
+
stream?: boolean;
|
|
35
|
+
}
|
|
36
|
+
interface ChatCompletionChoice {
|
|
37
|
+
index: number;
|
|
38
|
+
message: Message;
|
|
39
|
+
finish_reason: string | null;
|
|
40
|
+
}
|
|
41
|
+
interface ChatCompletionUsage {
|
|
42
|
+
prompt_tokens: number;
|
|
43
|
+
completion_tokens: number;
|
|
44
|
+
total_tokens: number;
|
|
45
|
+
}
|
|
46
|
+
interface ChatCompletion {
|
|
47
|
+
id: string;
|
|
48
|
+
object: 'chat.completion';
|
|
49
|
+
created: number;
|
|
50
|
+
model: string;
|
|
51
|
+
choices: ChatCompletionChoice[];
|
|
52
|
+
usage: ChatCompletionUsage;
|
|
53
|
+
/** Rouva metadata — cost, savings, routing decision */
|
|
54
|
+
_rouva?: RouvaResponseMeta;
|
|
55
|
+
}
|
|
56
|
+
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;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
declare class Rouva {
|
|
68
|
+
private apiKey;
|
|
69
|
+
private baseURL;
|
|
70
|
+
readonly chat: {
|
|
71
|
+
completions: {
|
|
72
|
+
create(params: ChatCompletionParams): Promise<ChatCompletion | ReadableStream>;
|
|
73
|
+
};
|
|
74
|
+
};
|
|
75
|
+
constructor(options: RouvaOptions);
|
|
76
|
+
private _createChatCompletion;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export { type ChatCompletion, type ChatCompletionChoice, type ChatCompletionParams, type ChatCompletionUsage, type Message, Rouva, type RouvaOptions, type RouvaProvider, type RouvaResponseMeta };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
interface RouvaOptions {
|
|
2
|
+
/** Your Rouva gateway API key (rva_...) */
|
|
3
|
+
apiKey: string;
|
|
4
|
+
/** Override the default gateway URL — useful for self-hosted or testing */
|
|
5
|
+
baseURL?: string;
|
|
6
|
+
}
|
|
7
|
+
interface Message {
|
|
8
|
+
role: 'system' | 'user' | 'assistant';
|
|
9
|
+
content: string;
|
|
10
|
+
}
|
|
11
|
+
type RouvaProvider = 'anthropic' | 'openai' | 'gemini' | 'deepseek' | 'mistral' | (string & {});
|
|
12
|
+
/**
|
|
13
|
+
* Supported models across all providers.
|
|
14
|
+
* Omit `model` entirely to let Rouva route to the cheapest capable model automatically.
|
|
15
|
+
*/
|
|
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' | (string & {});
|
|
17
|
+
interface ChatCompletionParams {
|
|
18
|
+
messages: Message[];
|
|
19
|
+
/**
|
|
20
|
+
* Target model — omit to let Rouva route intelligently to the cheapest capable model.
|
|
21
|
+
* Supports models from any connected provider (Anthropic, OpenAI).
|
|
22
|
+
*/
|
|
23
|
+
model?: RouvaModel;
|
|
24
|
+
/**
|
|
25
|
+
* Force an exact provider when paired with `model`.
|
|
26
|
+
* Omit to let Rouva auto-route based on your connected keys.
|
|
27
|
+
*/
|
|
28
|
+
provider?: RouvaProvider;
|
|
29
|
+
/** Maximum tokens to generate */
|
|
30
|
+
max_tokens?: number;
|
|
31
|
+
/** Sampling temperature 0–1 */
|
|
32
|
+
temperature?: number;
|
|
33
|
+
/** Stream the response */
|
|
34
|
+
stream?: boolean;
|
|
35
|
+
}
|
|
36
|
+
interface ChatCompletionChoice {
|
|
37
|
+
index: number;
|
|
38
|
+
message: Message;
|
|
39
|
+
finish_reason: string | null;
|
|
40
|
+
}
|
|
41
|
+
interface ChatCompletionUsage {
|
|
42
|
+
prompt_tokens: number;
|
|
43
|
+
completion_tokens: number;
|
|
44
|
+
total_tokens: number;
|
|
45
|
+
}
|
|
46
|
+
interface ChatCompletion {
|
|
47
|
+
id: string;
|
|
48
|
+
object: 'chat.completion';
|
|
49
|
+
created: number;
|
|
50
|
+
model: string;
|
|
51
|
+
choices: ChatCompletionChoice[];
|
|
52
|
+
usage: ChatCompletionUsage;
|
|
53
|
+
/** Rouva metadata — cost, savings, routing decision */
|
|
54
|
+
_rouva?: RouvaResponseMeta;
|
|
55
|
+
}
|
|
56
|
+
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;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
declare class Rouva {
|
|
68
|
+
private apiKey;
|
|
69
|
+
private baseURL;
|
|
70
|
+
readonly chat: {
|
|
71
|
+
completions: {
|
|
72
|
+
create(params: ChatCompletionParams): Promise<ChatCompletion | ReadableStream>;
|
|
73
|
+
};
|
|
74
|
+
};
|
|
75
|
+
constructor(options: RouvaOptions);
|
|
76
|
+
private _createChatCompletion;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export { type ChatCompletion, type ChatCompletionChoice, type ChatCompletionParams, type ChatCompletionUsage, type Message, Rouva, type RouvaOptions, type RouvaProvider, type RouvaResponseMeta };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
Rouva: () => Rouva
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(index_exports);
|
|
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
|
+
// src/client.ts
|
|
295
|
+
var DEFAULT_BASE_URL = "https://app.rouva.io";
|
|
296
|
+
var Rouva = class {
|
|
297
|
+
constructor(options) {
|
|
298
|
+
if (!options.apiKey) throw new Error("[Rouva] apiKey is required");
|
|
299
|
+
if (!options.apiKey.startsWith("rva_")) {
|
|
300
|
+
throw new Error("[Rouva] apiKey must start with rva_");
|
|
301
|
+
}
|
|
302
|
+
this.apiKey = options.apiKey;
|
|
303
|
+
this.baseURL = (options.baseURL ?? DEFAULT_BASE_URL).replace(/\/$/, "");
|
|
304
|
+
this.chat = {
|
|
305
|
+
completions: {
|
|
306
|
+
create: (params) => this._createChatCompletion(params)
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
async _createChatCompletion(params) {
|
|
311
|
+
const url = `${this.baseURL}/api/gateway/messages`;
|
|
312
|
+
const res = await fetch(url, {
|
|
313
|
+
method: "POST",
|
|
314
|
+
headers: {
|
|
315
|
+
"Content-Type": "application/json",
|
|
316
|
+
"Authorization": `Bearer ${this.apiKey}`
|
|
317
|
+
},
|
|
318
|
+
body: JSON.stringify(params)
|
|
319
|
+
});
|
|
320
|
+
if (!res.ok) {
|
|
321
|
+
const body = await res.text().catch(() => res.statusText);
|
|
322
|
+
throw new Error(`[Rouva] Gateway error ${res.status}: ${body}`);
|
|
323
|
+
}
|
|
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));
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
331
|
+
0 && (module.exports = {
|
|
332
|
+
Rouva
|
|
333
|
+
});
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
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
|
+
// src/client.ts
|
|
269
|
+
var DEFAULT_BASE_URL = "https://app.rouva.io";
|
|
270
|
+
var Rouva = class {
|
|
271
|
+
constructor(options) {
|
|
272
|
+
if (!options.apiKey) throw new Error("[Rouva] apiKey is required");
|
|
273
|
+
if (!options.apiKey.startsWith("rva_")) {
|
|
274
|
+
throw new Error("[Rouva] apiKey must start with rva_");
|
|
275
|
+
}
|
|
276
|
+
this.apiKey = options.apiKey;
|
|
277
|
+
this.baseURL = (options.baseURL ?? DEFAULT_BASE_URL).replace(/\/$/, "");
|
|
278
|
+
this.chat = {
|
|
279
|
+
completions: {
|
|
280
|
+
create: (params) => this._createChatCompletion(params)
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
async _createChatCompletion(params) {
|
|
285
|
+
const url = `${this.baseURL}/api/gateway/messages`;
|
|
286
|
+
const res = await fetch(url, {
|
|
287
|
+
method: "POST",
|
|
288
|
+
headers: {
|
|
289
|
+
"Content-Type": "application/json",
|
|
290
|
+
"Authorization": `Bearer ${this.apiKey}`
|
|
291
|
+
},
|
|
292
|
+
body: JSON.stringify(params)
|
|
293
|
+
});
|
|
294
|
+
if (!res.ok) {
|
|
295
|
+
const body = await res.text().catch(() => res.statusText);
|
|
296
|
+
throw new Error(`[Rouva] Gateway error ${res.status}: ${body}`);
|
|
297
|
+
}
|
|
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));
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
export {
|
|
305
|
+
Rouva
|
|
306
|
+
};
|
package/package.json
CHANGED