@rouvanpm/rouva 0.2.2 → 0.2.4
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 +63 -1
- package/dist/index.d.mts +59 -3
- package/dist/index.d.ts +59 -3
- package/dist/index.js +82 -6
- package/dist/index.mjs +82 -6
- 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, OpenAI, Gemini, DeepSeek, and
|
|
27
|
+
Rouva works with all connected providers — Anthropic, OpenAI, Gemini, DeepSeek, Mistral, Moonshot, xAI, and Z.ai. 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
|
|
@@ -80,6 +80,68 @@ while (true) {
|
|
|
80
80
|
|
|
81
81
|
Streaming responses are normalized to OpenAI-style SSE chunks, even when Rouva routes the request to Anthropic.
|
|
82
82
|
|
|
83
|
+
`stream` is a client-side toggle: `stream: true` returns the raw `ReadableStream`, omitting it (or `stream: false`) returns a buffered `ChatCompletion`. It is never sent to the gateway.
|
|
84
|
+
|
|
85
|
+
## Request parameters
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
const res = await rouva.chat.completions.create({
|
|
89
|
+
messages: [{ role: 'user', content: 'Write a haiku about routing.' }],
|
|
90
|
+
system: 'You are a concise assistant.', // string or Anthropic-style text blocks
|
|
91
|
+
max_tokens: 1024, // default 4096
|
|
92
|
+
temperature: 0.7, // 0–1
|
|
93
|
+
})
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
- **`temperature`** — sampling temperature between 0 and 1. Reasoning models (the gpt-5 family) only support their default temperature, so the gateway omits it when routing to one.
|
|
97
|
+
- **`max_tokens`** — values below 1024 also steer auto-routing away from reasoning models, which would otherwise spend the whole budget on hidden reasoning tokens.
|
|
98
|
+
|
|
99
|
+
## Tool use
|
|
100
|
+
|
|
101
|
+
Tools are forwarded to your target provider verbatim — define them in the **provider's own format** (OpenAI `{ type: "function", function: {...} }` or Anthropic `{ name, description, input_schema }`) and pin the matching `model`. Tools requests are never re-routed: tool schemas are provider-specific, so `model` is required and the gateway returns a 400 without it.
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
const res = await rouva.chat.completions.create({
|
|
105
|
+
model: 'gpt-4o',
|
|
106
|
+
messages: [{ role: 'user', content: 'What is the weather in SF?' }],
|
|
107
|
+
tools: [{
|
|
108
|
+
type: 'function',
|
|
109
|
+
function: {
|
|
110
|
+
name: 'get_weather',
|
|
111
|
+
description: 'Get current weather for a city',
|
|
112
|
+
parameters: {
|
|
113
|
+
type: 'object',
|
|
114
|
+
properties: { city: { type: 'string' } },
|
|
115
|
+
required: ['city'],
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
}],
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
const toolCall = res.choices[0].message.tool_calls?.[0]
|
|
122
|
+
if (toolCall) {
|
|
123
|
+
const args = JSON.parse(toolCall.function.arguments)
|
|
124
|
+
const weather = await getWeather(args.city)
|
|
125
|
+
|
|
126
|
+
// Send the result back — same messages array plus the tool turn
|
|
127
|
+
const followUp = await rouva.chat.completions.create({
|
|
128
|
+
model: 'gpt-4o',
|
|
129
|
+
messages: [
|
|
130
|
+
{ role: 'user', content: 'What is the weather in SF?' },
|
|
131
|
+
res.choices[0].message,
|
|
132
|
+
{ role: 'tool', content: JSON.stringify(weather), tool_call_id: toolCall.id },
|
|
133
|
+
],
|
|
134
|
+
tools: [/* same tools every turn */],
|
|
135
|
+
})
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Responses are normalized to the OpenAI shape regardless of provider: Anthropic `tool_use` blocks arrive as `message.tool_calls` (buffered) or `delta.tool_calls` chunks (streaming), with Anthropic's `stop_reason: "tool_use"` mapped to `finish_reason: "tool_calls"`. When sending Anthropic results back, use the Anthropic dialect in your messages (`tool_result` content blocks) — message payloads pass through to the provider verbatim.
|
|
140
|
+
|
|
141
|
+
**Don't branch on `finish_reason` to detect tool calls** — check for the presence of `message.tool_calls` instead. `finish_reason` follows each provider's own semantics, and OpenAI notably returns `"stop"` (not `"tool_calls"`) when `tool_choice` forces a specific function. The SDK passes OpenAI's values through unchanged so behavior matches calling OpenAI directly.
|
|
142
|
+
|
|
143
|
+
Tools requests record usage and cost but no savings, and are not quality-scored or served from the semantic cache.
|
|
144
|
+
|
|
83
145
|
## Options
|
|
84
146
|
|
|
85
147
|
```typescript
|
package/dist/index.d.mts
CHANGED
|
@@ -8,6 +8,43 @@ interface Message {
|
|
|
8
8
|
role: 'system' | 'user' | 'assistant';
|
|
9
9
|
content: string;
|
|
10
10
|
}
|
|
11
|
+
/** OpenAI-dialect tool call, as emitted on assistant messages */
|
|
12
|
+
interface FunctionToolCall {
|
|
13
|
+
id: string;
|
|
14
|
+
type: 'function';
|
|
15
|
+
function: {
|
|
16
|
+
name: string;
|
|
17
|
+
/** JSON-encoded arguments string */
|
|
18
|
+
arguments: string;
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
/** OpenAI-dialect assistant turn that requested tool calls */
|
|
22
|
+
interface AssistantToolCallMessage {
|
|
23
|
+
role: 'assistant';
|
|
24
|
+
content: string | null;
|
|
25
|
+
tool_calls: FunctionToolCall[];
|
|
26
|
+
}
|
|
27
|
+
/** OpenAI-dialect tool result turn */
|
|
28
|
+
interface ToolResultMessage {
|
|
29
|
+
role: 'tool';
|
|
30
|
+
content: string;
|
|
31
|
+
tool_call_id: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Anthropic-dialect turn whose content is an array of content blocks
|
|
35
|
+
* (e.g. `tool_use` on assistant turns, `tool_result` on user turns).
|
|
36
|
+
* Blocks are forwarded to the gateway verbatim.
|
|
37
|
+
*/
|
|
38
|
+
interface ContentBlockMessage {
|
|
39
|
+
role: 'user' | 'assistant';
|
|
40
|
+
content: Array<Record<string, unknown>>;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Any message the gateway accepts. Plain chat uses `Message`; tool
|
|
44
|
+
* conversations additionally use the tool-shaped variants, which are only
|
|
45
|
+
* valid when `tools` is set on the request.
|
|
46
|
+
*/
|
|
47
|
+
type ChatMessage = Message | AssistantToolCallMessage | ToolResultMessage | ContentBlockMessage;
|
|
11
48
|
type RouvaProvider = 'anthropic' | 'openai' | 'gemini' | 'deepseek' | 'mistral' | 'moonshot' | 'xai' | 'zai' | (string & {});
|
|
12
49
|
/**
|
|
13
50
|
* Supported models across all providers.
|
|
@@ -20,7 +57,7 @@ interface SystemTextBlock {
|
|
|
20
57
|
text: string;
|
|
21
58
|
}
|
|
22
59
|
interface ChatCompletionParams {
|
|
23
|
-
messages:
|
|
60
|
+
messages: ChatMessage[];
|
|
24
61
|
/**
|
|
25
62
|
* System prompt — a plain string or Anthropic-style text blocks.
|
|
26
63
|
* Equivalent to a `role: "system"` message at the head of `messages`;
|
|
@@ -52,10 +89,29 @@ interface ChatCompletionParams {
|
|
|
52
89
|
* the gateway.
|
|
53
90
|
*/
|
|
54
91
|
stream?: boolean;
|
|
92
|
+
/**
|
|
93
|
+
* Tool definitions in your target provider's own format, forwarded to the
|
|
94
|
+
* provider verbatim (OpenAI `{ type: "function", function: {...} }` or
|
|
95
|
+
* Anthropic `{ name, description, input_schema }`). Requires `model` —
|
|
96
|
+
* tool schemas are provider-specific, so tools requests are never
|
|
97
|
+
* re-routed by the gateway.
|
|
98
|
+
*/
|
|
99
|
+
tools?: Array<Record<string, unknown>>;
|
|
100
|
+
/**
|
|
101
|
+
* Tool choice in your target provider's own format, forwarded verbatim.
|
|
102
|
+
* Only valid alongside `tools`.
|
|
103
|
+
*/
|
|
104
|
+
tool_choice?: string | Record<string, unknown>;
|
|
105
|
+
}
|
|
106
|
+
/** Assistant message in a completed response; `tool_calls` present when the model requested tools */
|
|
107
|
+
interface ResponseMessage {
|
|
108
|
+
role: 'assistant';
|
|
109
|
+
content: string | null;
|
|
110
|
+
tool_calls?: FunctionToolCall[];
|
|
55
111
|
}
|
|
56
112
|
interface ChatCompletionChoice {
|
|
57
113
|
index: number;
|
|
58
|
-
message:
|
|
114
|
+
message: ResponseMessage;
|
|
59
115
|
finish_reason: string | null;
|
|
60
116
|
}
|
|
61
117
|
interface ChatCompletionUsage {
|
|
@@ -96,4 +152,4 @@ declare class Rouva {
|
|
|
96
152
|
private _createChatCompletion;
|
|
97
153
|
}
|
|
98
154
|
|
|
99
|
-
export { type ChatCompletion, type ChatCompletionChoice, type ChatCompletionParams, type ChatCompletionUsage, type Message, Rouva, type RouvaOptions, type RouvaProvider, type RouvaResponseMeta, type SystemTextBlock };
|
|
155
|
+
export { type AssistantToolCallMessage, type ChatCompletion, type ChatCompletionChoice, type ChatCompletionParams, type ChatCompletionUsage, type ChatMessage, type ContentBlockMessage, type FunctionToolCall, type Message, type ResponseMessage, Rouva, type RouvaOptions, type RouvaProvider, type RouvaResponseMeta, type SystemTextBlock, type ToolResultMessage };
|
package/dist/index.d.ts
CHANGED
|
@@ -8,6 +8,43 @@ interface Message {
|
|
|
8
8
|
role: 'system' | 'user' | 'assistant';
|
|
9
9
|
content: string;
|
|
10
10
|
}
|
|
11
|
+
/** OpenAI-dialect tool call, as emitted on assistant messages */
|
|
12
|
+
interface FunctionToolCall {
|
|
13
|
+
id: string;
|
|
14
|
+
type: 'function';
|
|
15
|
+
function: {
|
|
16
|
+
name: string;
|
|
17
|
+
/** JSON-encoded arguments string */
|
|
18
|
+
arguments: string;
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
/** OpenAI-dialect assistant turn that requested tool calls */
|
|
22
|
+
interface AssistantToolCallMessage {
|
|
23
|
+
role: 'assistant';
|
|
24
|
+
content: string | null;
|
|
25
|
+
tool_calls: FunctionToolCall[];
|
|
26
|
+
}
|
|
27
|
+
/** OpenAI-dialect tool result turn */
|
|
28
|
+
interface ToolResultMessage {
|
|
29
|
+
role: 'tool';
|
|
30
|
+
content: string;
|
|
31
|
+
tool_call_id: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Anthropic-dialect turn whose content is an array of content blocks
|
|
35
|
+
* (e.g. `tool_use` on assistant turns, `tool_result` on user turns).
|
|
36
|
+
* Blocks are forwarded to the gateway verbatim.
|
|
37
|
+
*/
|
|
38
|
+
interface ContentBlockMessage {
|
|
39
|
+
role: 'user' | 'assistant';
|
|
40
|
+
content: Array<Record<string, unknown>>;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Any message the gateway accepts. Plain chat uses `Message`; tool
|
|
44
|
+
* conversations additionally use the tool-shaped variants, which are only
|
|
45
|
+
* valid when `tools` is set on the request.
|
|
46
|
+
*/
|
|
47
|
+
type ChatMessage = Message | AssistantToolCallMessage | ToolResultMessage | ContentBlockMessage;
|
|
11
48
|
type RouvaProvider = 'anthropic' | 'openai' | 'gemini' | 'deepseek' | 'mistral' | 'moonshot' | 'xai' | 'zai' | (string & {});
|
|
12
49
|
/**
|
|
13
50
|
* Supported models across all providers.
|
|
@@ -20,7 +57,7 @@ interface SystemTextBlock {
|
|
|
20
57
|
text: string;
|
|
21
58
|
}
|
|
22
59
|
interface ChatCompletionParams {
|
|
23
|
-
messages:
|
|
60
|
+
messages: ChatMessage[];
|
|
24
61
|
/**
|
|
25
62
|
* System prompt — a plain string or Anthropic-style text blocks.
|
|
26
63
|
* Equivalent to a `role: "system"` message at the head of `messages`;
|
|
@@ -52,10 +89,29 @@ interface ChatCompletionParams {
|
|
|
52
89
|
* the gateway.
|
|
53
90
|
*/
|
|
54
91
|
stream?: boolean;
|
|
92
|
+
/**
|
|
93
|
+
* Tool definitions in your target provider's own format, forwarded to the
|
|
94
|
+
* provider verbatim (OpenAI `{ type: "function", function: {...} }` or
|
|
95
|
+
* Anthropic `{ name, description, input_schema }`). Requires `model` —
|
|
96
|
+
* tool schemas are provider-specific, so tools requests are never
|
|
97
|
+
* re-routed by the gateway.
|
|
98
|
+
*/
|
|
99
|
+
tools?: Array<Record<string, unknown>>;
|
|
100
|
+
/**
|
|
101
|
+
* Tool choice in your target provider's own format, forwarded verbatim.
|
|
102
|
+
* Only valid alongside `tools`.
|
|
103
|
+
*/
|
|
104
|
+
tool_choice?: string | Record<string, unknown>;
|
|
105
|
+
}
|
|
106
|
+
/** Assistant message in a completed response; `tool_calls` present when the model requested tools */
|
|
107
|
+
interface ResponseMessage {
|
|
108
|
+
role: 'assistant';
|
|
109
|
+
content: string | null;
|
|
110
|
+
tool_calls?: FunctionToolCall[];
|
|
55
111
|
}
|
|
56
112
|
interface ChatCompletionChoice {
|
|
57
113
|
index: number;
|
|
58
|
-
message:
|
|
114
|
+
message: ResponseMessage;
|
|
59
115
|
finish_reason: string | null;
|
|
60
116
|
}
|
|
61
117
|
interface ChatCompletionUsage {
|
|
@@ -96,4 +152,4 @@ declare class Rouva {
|
|
|
96
152
|
private _createChatCompletion;
|
|
97
153
|
}
|
|
98
154
|
|
|
99
|
-
export { type ChatCompletion, type ChatCompletionChoice, type ChatCompletionParams, type ChatCompletionUsage, type Message, Rouva, type RouvaOptions, type RouvaProvider, type RouvaResponseMeta, type SystemTextBlock };
|
|
155
|
+
export { type AssistantToolCallMessage, type ChatCompletion, type ChatCompletionChoice, type ChatCompletionParams, type ChatCompletionUsage, type ChatMessage, type ContentBlockMessage, type FunctionToolCall, type Message, type ResponseMessage, Rouva, type RouvaOptions, type RouvaProvider, type RouvaResponseMeta, type SystemTextBlock, type ToolResultMessage };
|
package/dist/index.js
CHANGED
|
@@ -54,6 +54,7 @@ function toFinishReason(stopReason) {
|
|
|
54
54
|
if (!stopReason) return null;
|
|
55
55
|
if (stopReason === "end_turn" || stopReason === "stop_sequence") return "stop";
|
|
56
56
|
if (stopReason === "max_tokens") return "length";
|
|
57
|
+
if (stopReason === "tool_use") return "tool_calls";
|
|
57
58
|
return stopReason;
|
|
58
59
|
}
|
|
59
60
|
function initialState() {
|
|
@@ -65,7 +66,9 @@ function initialState() {
|
|
|
65
66
|
promptTokens: 0,
|
|
66
67
|
completionTokens: 0,
|
|
67
68
|
roleEmitted: false,
|
|
68
|
-
finalChunkEmitted: false
|
|
69
|
+
finalChunkEmitted: false,
|
|
70
|
+
toolCallIndexByBlock: {},
|
|
71
|
+
nextToolCallIndex: 0
|
|
69
72
|
};
|
|
70
73
|
}
|
|
71
74
|
function normalizeAnthropicPayload(payload, state) {
|
|
@@ -88,8 +91,45 @@ function normalizeAnthropicPayload(payload, state) {
|
|
|
88
91
|
state.roleEmitted = true;
|
|
89
92
|
}
|
|
90
93
|
}
|
|
94
|
+
if (eventType === "content_block_start") {
|
|
95
|
+
const block = payload.content_block ?? {};
|
|
96
|
+
const blockIndex = typeof payload.index === "number" ? payload.index : null;
|
|
97
|
+
if (block.type === "tool_use" && blockIndex !== null) {
|
|
98
|
+
const toolCallIndex = state.nextToolCallIndex++;
|
|
99
|
+
state.toolCallIndexByBlock[blockIndex] = toolCallIndex;
|
|
100
|
+
if (!state.roleEmitted) {
|
|
101
|
+
chunks.push(stringifySse({
|
|
102
|
+
id: state.id,
|
|
103
|
+
object: "chat.completion.chunk",
|
|
104
|
+
created: state.created,
|
|
105
|
+
model: state.model,
|
|
106
|
+
choices: [{ index: 0, delta: { role: "assistant" }, finish_reason: null }]
|
|
107
|
+
}));
|
|
108
|
+
state.roleEmitted = true;
|
|
109
|
+
}
|
|
110
|
+
chunks.push(stringifySse({
|
|
111
|
+
id: state.id,
|
|
112
|
+
object: "chat.completion.chunk",
|
|
113
|
+
created: state.created,
|
|
114
|
+
model: state.model,
|
|
115
|
+
choices: [{
|
|
116
|
+
index: 0,
|
|
117
|
+
delta: {
|
|
118
|
+
tool_calls: [{
|
|
119
|
+
index: toolCallIndex,
|
|
120
|
+
id: typeof block.id === "string" ? block.id : `call_${toolCallIndex}`,
|
|
121
|
+
type: "function",
|
|
122
|
+
function: { name: typeof block.name === "string" ? block.name : "", arguments: "" }
|
|
123
|
+
}]
|
|
124
|
+
},
|
|
125
|
+
finish_reason: null
|
|
126
|
+
}]
|
|
127
|
+
}));
|
|
128
|
+
}
|
|
129
|
+
}
|
|
91
130
|
if (eventType === "content_block_delta") {
|
|
92
131
|
const delta = payload.delta ?? {};
|
|
132
|
+
const blockIndex = typeof payload.index === "number" ? payload.index : null;
|
|
93
133
|
if (delta.type === "text_delta" && typeof delta.text === "string") {
|
|
94
134
|
if (!state.roleEmitted) {
|
|
95
135
|
chunks.push(stringifySse({
|
|
@@ -109,6 +149,24 @@ function normalizeAnthropicPayload(payload, state) {
|
|
|
109
149
|
choices: [{ index: 0, delta: { content: delta.text }, finish_reason: null }]
|
|
110
150
|
}));
|
|
111
151
|
}
|
|
152
|
+
if (delta.type === "input_json_delta" && typeof delta.partial_json === "string" && blockIndex !== null && state.toolCallIndexByBlock[blockIndex] !== void 0) {
|
|
153
|
+
chunks.push(stringifySse({
|
|
154
|
+
id: state.id,
|
|
155
|
+
object: "chat.completion.chunk",
|
|
156
|
+
created: state.created,
|
|
157
|
+
model: state.model,
|
|
158
|
+
choices: [{
|
|
159
|
+
index: 0,
|
|
160
|
+
delta: {
|
|
161
|
+
tool_calls: [{
|
|
162
|
+
index: state.toolCallIndexByBlock[blockIndex],
|
|
163
|
+
function: { arguments: delta.partial_json }
|
|
164
|
+
}]
|
|
165
|
+
},
|
|
166
|
+
finish_reason: null
|
|
167
|
+
}]
|
|
168
|
+
}));
|
|
169
|
+
}
|
|
112
170
|
}
|
|
113
171
|
if (eventType === "message_delta") {
|
|
114
172
|
const delta = payload.delta ?? {};
|
|
@@ -201,9 +259,9 @@ async function readChatCompletionFromSse(stream, metadata) {
|
|
|
201
259
|
let id = `chatcmpl_${Date.now()}`;
|
|
202
260
|
let model = metadata?.model_used ?? "unknown";
|
|
203
261
|
let created = Math.floor(Date.now() / 1e3);
|
|
204
|
-
let role = "assistant";
|
|
205
262
|
let content = "";
|
|
206
263
|
let finishReason = null;
|
|
264
|
+
const toolCalls = [];
|
|
207
265
|
let usage = {
|
|
208
266
|
prompt_tokens: 0,
|
|
209
267
|
completion_tokens: 0,
|
|
@@ -234,12 +292,24 @@ async function readChatCompletionFromSse(stream, metadata) {
|
|
|
234
292
|
const choices = Array.isArray(payload.choices) ? payload.choices : [];
|
|
235
293
|
const firstChoice = choices[0];
|
|
236
294
|
const delta = firstChoice?.delta;
|
|
237
|
-
if (delta?.role === "assistant") {
|
|
238
|
-
role = "assistant";
|
|
239
|
-
}
|
|
240
295
|
if (typeof delta?.content === "string") {
|
|
241
296
|
content += delta.content;
|
|
242
297
|
}
|
|
298
|
+
const deltaToolCalls = Array.isArray(delta?.tool_calls) ? delta.tool_calls : [];
|
|
299
|
+
for (const raw of deltaToolCalls) {
|
|
300
|
+
const tc = raw;
|
|
301
|
+
const index = typeof tc.index === "number" ? tc.index : 0;
|
|
302
|
+
const fn = tc.function ?? {};
|
|
303
|
+
const existing = toolCalls[index] ?? {
|
|
304
|
+
id: "",
|
|
305
|
+
type: "function",
|
|
306
|
+
function: { name: "", arguments: "" }
|
|
307
|
+
};
|
|
308
|
+
if (typeof tc.id === "string" && tc.id) existing.id = tc.id;
|
|
309
|
+
if (typeof fn.name === "string" && fn.name) existing.function.name = fn.name;
|
|
310
|
+
if (typeof fn.arguments === "string") existing.function.arguments += fn.arguments;
|
|
311
|
+
toolCalls[index] = existing;
|
|
312
|
+
}
|
|
243
313
|
if (typeof firstChoice?.finish_reason === "string" || firstChoice?.finish_reason === null) {
|
|
244
314
|
finishReason = firstChoice.finish_reason;
|
|
245
315
|
}
|
|
@@ -258,6 +328,7 @@ async function readChatCompletionFromSse(stream, metadata) {
|
|
|
258
328
|
} finally {
|
|
259
329
|
reader.releaseLock();
|
|
260
330
|
}
|
|
331
|
+
const assembledToolCalls = toolCalls.filter(Boolean);
|
|
261
332
|
const response = {
|
|
262
333
|
id,
|
|
263
334
|
object: "chat.completion",
|
|
@@ -266,7 +337,12 @@ async function readChatCompletionFromSse(stream, metadata) {
|
|
|
266
337
|
choices: [
|
|
267
338
|
{
|
|
268
339
|
index: 0,
|
|
269
|
-
message: {
|
|
340
|
+
message: {
|
|
341
|
+
role: "assistant",
|
|
342
|
+
// OpenAI convention: content is null on pure tool-call turns
|
|
343
|
+
content: content || (assembledToolCalls.length > 0 ? null : ""),
|
|
344
|
+
...assembledToolCalls.length > 0 ? { tool_calls: assembledToolCalls } : {}
|
|
345
|
+
},
|
|
270
346
|
finish_reason: finishReason
|
|
271
347
|
}
|
|
272
348
|
],
|
package/dist/index.mjs
CHANGED
|
@@ -28,6 +28,7 @@ function toFinishReason(stopReason) {
|
|
|
28
28
|
if (!stopReason) return null;
|
|
29
29
|
if (stopReason === "end_turn" || stopReason === "stop_sequence") return "stop";
|
|
30
30
|
if (stopReason === "max_tokens") return "length";
|
|
31
|
+
if (stopReason === "tool_use") return "tool_calls";
|
|
31
32
|
return stopReason;
|
|
32
33
|
}
|
|
33
34
|
function initialState() {
|
|
@@ -39,7 +40,9 @@ function initialState() {
|
|
|
39
40
|
promptTokens: 0,
|
|
40
41
|
completionTokens: 0,
|
|
41
42
|
roleEmitted: false,
|
|
42
|
-
finalChunkEmitted: false
|
|
43
|
+
finalChunkEmitted: false,
|
|
44
|
+
toolCallIndexByBlock: {},
|
|
45
|
+
nextToolCallIndex: 0
|
|
43
46
|
};
|
|
44
47
|
}
|
|
45
48
|
function normalizeAnthropicPayload(payload, state) {
|
|
@@ -62,8 +65,45 @@ function normalizeAnthropicPayload(payload, state) {
|
|
|
62
65
|
state.roleEmitted = true;
|
|
63
66
|
}
|
|
64
67
|
}
|
|
68
|
+
if (eventType === "content_block_start") {
|
|
69
|
+
const block = payload.content_block ?? {};
|
|
70
|
+
const blockIndex = typeof payload.index === "number" ? payload.index : null;
|
|
71
|
+
if (block.type === "tool_use" && blockIndex !== null) {
|
|
72
|
+
const toolCallIndex = state.nextToolCallIndex++;
|
|
73
|
+
state.toolCallIndexByBlock[blockIndex] = toolCallIndex;
|
|
74
|
+
if (!state.roleEmitted) {
|
|
75
|
+
chunks.push(stringifySse({
|
|
76
|
+
id: state.id,
|
|
77
|
+
object: "chat.completion.chunk",
|
|
78
|
+
created: state.created,
|
|
79
|
+
model: state.model,
|
|
80
|
+
choices: [{ index: 0, delta: { role: "assistant" }, finish_reason: null }]
|
|
81
|
+
}));
|
|
82
|
+
state.roleEmitted = true;
|
|
83
|
+
}
|
|
84
|
+
chunks.push(stringifySse({
|
|
85
|
+
id: state.id,
|
|
86
|
+
object: "chat.completion.chunk",
|
|
87
|
+
created: state.created,
|
|
88
|
+
model: state.model,
|
|
89
|
+
choices: [{
|
|
90
|
+
index: 0,
|
|
91
|
+
delta: {
|
|
92
|
+
tool_calls: [{
|
|
93
|
+
index: toolCallIndex,
|
|
94
|
+
id: typeof block.id === "string" ? block.id : `call_${toolCallIndex}`,
|
|
95
|
+
type: "function",
|
|
96
|
+
function: { name: typeof block.name === "string" ? block.name : "", arguments: "" }
|
|
97
|
+
}]
|
|
98
|
+
},
|
|
99
|
+
finish_reason: null
|
|
100
|
+
}]
|
|
101
|
+
}));
|
|
102
|
+
}
|
|
103
|
+
}
|
|
65
104
|
if (eventType === "content_block_delta") {
|
|
66
105
|
const delta = payload.delta ?? {};
|
|
106
|
+
const blockIndex = typeof payload.index === "number" ? payload.index : null;
|
|
67
107
|
if (delta.type === "text_delta" && typeof delta.text === "string") {
|
|
68
108
|
if (!state.roleEmitted) {
|
|
69
109
|
chunks.push(stringifySse({
|
|
@@ -83,6 +123,24 @@ function normalizeAnthropicPayload(payload, state) {
|
|
|
83
123
|
choices: [{ index: 0, delta: { content: delta.text }, finish_reason: null }]
|
|
84
124
|
}));
|
|
85
125
|
}
|
|
126
|
+
if (delta.type === "input_json_delta" && typeof delta.partial_json === "string" && blockIndex !== null && state.toolCallIndexByBlock[blockIndex] !== void 0) {
|
|
127
|
+
chunks.push(stringifySse({
|
|
128
|
+
id: state.id,
|
|
129
|
+
object: "chat.completion.chunk",
|
|
130
|
+
created: state.created,
|
|
131
|
+
model: state.model,
|
|
132
|
+
choices: [{
|
|
133
|
+
index: 0,
|
|
134
|
+
delta: {
|
|
135
|
+
tool_calls: [{
|
|
136
|
+
index: state.toolCallIndexByBlock[blockIndex],
|
|
137
|
+
function: { arguments: delta.partial_json }
|
|
138
|
+
}]
|
|
139
|
+
},
|
|
140
|
+
finish_reason: null
|
|
141
|
+
}]
|
|
142
|
+
}));
|
|
143
|
+
}
|
|
86
144
|
}
|
|
87
145
|
if (eventType === "message_delta") {
|
|
88
146
|
const delta = payload.delta ?? {};
|
|
@@ -175,9 +233,9 @@ async function readChatCompletionFromSse(stream, metadata) {
|
|
|
175
233
|
let id = `chatcmpl_${Date.now()}`;
|
|
176
234
|
let model = metadata?.model_used ?? "unknown";
|
|
177
235
|
let created = Math.floor(Date.now() / 1e3);
|
|
178
|
-
let role = "assistant";
|
|
179
236
|
let content = "";
|
|
180
237
|
let finishReason = null;
|
|
238
|
+
const toolCalls = [];
|
|
181
239
|
let usage = {
|
|
182
240
|
prompt_tokens: 0,
|
|
183
241
|
completion_tokens: 0,
|
|
@@ -208,12 +266,24 @@ async function readChatCompletionFromSse(stream, metadata) {
|
|
|
208
266
|
const choices = Array.isArray(payload.choices) ? payload.choices : [];
|
|
209
267
|
const firstChoice = choices[0];
|
|
210
268
|
const delta = firstChoice?.delta;
|
|
211
|
-
if (delta?.role === "assistant") {
|
|
212
|
-
role = "assistant";
|
|
213
|
-
}
|
|
214
269
|
if (typeof delta?.content === "string") {
|
|
215
270
|
content += delta.content;
|
|
216
271
|
}
|
|
272
|
+
const deltaToolCalls = Array.isArray(delta?.tool_calls) ? delta.tool_calls : [];
|
|
273
|
+
for (const raw of deltaToolCalls) {
|
|
274
|
+
const tc = raw;
|
|
275
|
+
const index = typeof tc.index === "number" ? tc.index : 0;
|
|
276
|
+
const fn = tc.function ?? {};
|
|
277
|
+
const existing = toolCalls[index] ?? {
|
|
278
|
+
id: "",
|
|
279
|
+
type: "function",
|
|
280
|
+
function: { name: "", arguments: "" }
|
|
281
|
+
};
|
|
282
|
+
if (typeof tc.id === "string" && tc.id) existing.id = tc.id;
|
|
283
|
+
if (typeof fn.name === "string" && fn.name) existing.function.name = fn.name;
|
|
284
|
+
if (typeof fn.arguments === "string") existing.function.arguments += fn.arguments;
|
|
285
|
+
toolCalls[index] = existing;
|
|
286
|
+
}
|
|
217
287
|
if (typeof firstChoice?.finish_reason === "string" || firstChoice?.finish_reason === null) {
|
|
218
288
|
finishReason = firstChoice.finish_reason;
|
|
219
289
|
}
|
|
@@ -232,6 +302,7 @@ async function readChatCompletionFromSse(stream, metadata) {
|
|
|
232
302
|
} finally {
|
|
233
303
|
reader.releaseLock();
|
|
234
304
|
}
|
|
305
|
+
const assembledToolCalls = toolCalls.filter(Boolean);
|
|
235
306
|
const response = {
|
|
236
307
|
id,
|
|
237
308
|
object: "chat.completion",
|
|
@@ -240,7 +311,12 @@ async function readChatCompletionFromSse(stream, metadata) {
|
|
|
240
311
|
choices: [
|
|
241
312
|
{
|
|
242
313
|
index: 0,
|
|
243
|
-
message: {
|
|
314
|
+
message: {
|
|
315
|
+
role: "assistant",
|
|
316
|
+
// OpenAI convention: content is null on pure tool-call turns
|
|
317
|
+
content: content || (assembledToolCalls.length > 0 ? null : ""),
|
|
318
|
+
...assembledToolCalls.length > 0 ? { tool_calls: assembledToolCalls } : {}
|
|
319
|
+
},
|
|
244
320
|
finish_reason: finishReason
|
|
245
321
|
}
|
|
246
322
|
],
|
package/package.json
CHANGED