@bolt-foundry/gambit 0.8.0 → 0.8.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/CHANGELOG.md +82 -2
- package/README.md +31 -9
- package/esm/gambit/simulator-ui/dist/bundle.js +4744 -4360
- package/esm/gambit/simulator-ui/dist/bundle.js.map +4 -4
- package/esm/gambit/simulator-ui/dist/favicon.ico +0 -0
- package/esm/mod.d.ts +7 -3
- package/esm/mod.d.ts.map +1 -1
- package/esm/mod.js +5 -1
- package/esm/src/cli_utils.d.ts +3 -2
- package/esm/src/cli_utils.d.ts.map +1 -1
- package/esm/src/cli_utils.js +43 -27
- package/esm/src/openai_compat.d.ts +63 -0
- package/esm/src/openai_compat.d.ts.map +1 -0
- package/esm/src/openai_compat.js +277 -0
- package/esm/src/providers/google.d.ts +16 -0
- package/esm/src/providers/google.d.ts.map +1 -0
- package/esm/src/providers/google.js +352 -0
- package/esm/src/providers/ollama.d.ts +17 -0
- package/esm/src/providers/ollama.d.ts.map +1 -0
- package/esm/src/providers/ollama.js +509 -0
- package/esm/src/providers/openrouter.d.ts +14 -1
- package/esm/src/providers/openrouter.d.ts.map +1 -1
- package/esm/src/providers/openrouter.js +460 -463
- package/esm/src/server.d.ts +4 -0
- package/esm/src/server.d.ts.map +1 -1
- package/esm/src/server.js +623 -164
- package/esm/src/trace.d.ts.map +1 -1
- package/esm/src/trace.js +3 -6
- package/package.json +2 -2
- package/script/gambit/simulator-ui/dist/bundle.js +4744 -4360
- package/script/gambit/simulator-ui/dist/bundle.js.map +4 -4
- package/script/gambit/simulator-ui/dist/favicon.ico +0 -0
- package/script/mod.d.ts +7 -3
- package/script/mod.d.ts.map +1 -1
- package/script/mod.js +9 -3
- package/script/src/cli_utils.d.ts +3 -2
- package/script/src/cli_utils.d.ts.map +1 -1
- package/script/src/cli_utils.js +42 -26
- package/script/src/openai_compat.d.ts +63 -0
- package/script/src/openai_compat.d.ts.map +1 -0
- package/script/src/openai_compat.js +281 -0
- package/script/src/providers/google.d.ts +16 -0
- package/script/src/providers/google.d.ts.map +1 -0
- package/script/src/providers/google.js +359 -0
- package/script/src/providers/ollama.d.ts +17 -0
- package/script/src/providers/ollama.d.ts.map +1 -0
- package/script/src/providers/ollama.js +551 -0
- package/script/src/providers/openrouter.d.ts +14 -1
- package/script/src/providers/openrouter.d.ts.map +1 -1
- package/script/src/providers/openrouter.js +461 -463
- package/script/src/server.d.ts +4 -0
- package/script/src/server.d.ts.map +1 -1
- package/script/src/server.js +623 -164
- package/script/src/trace.d.ts.map +1 -1
- package/script/src/trace.js +3 -6
- package/esm/src/compat/openai.d.ts +0 -2
- package/esm/src/compat/openai.d.ts.map +0 -1
- package/esm/src/compat/openai.js +0 -1
- package/script/src/compat/openai.d.ts +0 -2
- package/script/src/compat/openai.d.ts.map +0 -1
- package/script/src/compat/openai.js +0 -5
|
@@ -1,80 +1,12 @@
|
|
|
1
1
|
import * as dntShim from "../../_dnt.shims.js";
|
|
2
2
|
import OpenAI from "openai";
|
|
3
|
+
import { GAMBIT_TOOL_CONTEXT, GAMBIT_TOOL_INIT, } from "@bolt-foundry/gambit-core";
|
|
3
4
|
const logger = console;
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const flushText = () => {
|
|
10
|
-
if (!textParts.length)
|
|
11
|
-
return;
|
|
12
|
-
out.push({ type: "text", text: textParts.join("") });
|
|
13
|
-
textParts.length = 0;
|
|
14
|
-
};
|
|
15
|
-
for (const part of content) {
|
|
16
|
-
switch (part.type) {
|
|
17
|
-
case "input_image":
|
|
18
|
-
if (part.image_url) {
|
|
19
|
-
flushText();
|
|
20
|
-
out.push({
|
|
21
|
-
type: "image_url",
|
|
22
|
-
image_url: {
|
|
23
|
-
url: part.image_url,
|
|
24
|
-
detail: part.detail,
|
|
25
|
-
},
|
|
26
|
-
});
|
|
27
|
-
}
|
|
28
|
-
break;
|
|
29
|
-
case "input_file": {
|
|
30
|
-
const label = part.file_url ?? part.filename;
|
|
31
|
-
if (label)
|
|
32
|
-
textParts.push(`[file] ${label}`);
|
|
33
|
-
break;
|
|
34
|
-
}
|
|
35
|
-
case "input_video":
|
|
36
|
-
textParts.push(`[video] ${part.video_url}`);
|
|
37
|
-
break;
|
|
38
|
-
case "refusal":
|
|
39
|
-
textParts.push(part.refusal);
|
|
40
|
-
break;
|
|
41
|
-
case "input_text":
|
|
42
|
-
case "output_text":
|
|
43
|
-
case "text":
|
|
44
|
-
case "summary_text":
|
|
45
|
-
case "reasoning_text":
|
|
46
|
-
textParts.push(part.text);
|
|
47
|
-
break;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
if (!out.length)
|
|
51
|
-
return textParts.join("");
|
|
52
|
-
flushText();
|
|
53
|
-
return out;
|
|
54
|
-
}
|
|
55
|
-
function contentPartsToText(content) {
|
|
56
|
-
if (typeof content === "string" || content === null)
|
|
57
|
-
return content;
|
|
58
|
-
return content.map((part) => {
|
|
59
|
-
switch (part.type) {
|
|
60
|
-
case "input_text":
|
|
61
|
-
case "output_text":
|
|
62
|
-
case "text":
|
|
63
|
-
case "summary_text":
|
|
64
|
-
case "reasoning_text":
|
|
65
|
-
return part.text;
|
|
66
|
-
case "refusal":
|
|
67
|
-
return part.refusal;
|
|
68
|
-
case "input_file": {
|
|
69
|
-
const label = part.file_url ?? part.filename;
|
|
70
|
-
return label ? `[file] ${label}` : "";
|
|
71
|
-
}
|
|
72
|
-
case "input_video":
|
|
73
|
-
return `[video] ${part.video_url}`;
|
|
74
|
-
default:
|
|
75
|
-
return "";
|
|
76
|
-
}
|
|
77
|
-
}).join("");
|
|
5
|
+
export const OPENROUTER_PREFIX = "openrouter/";
|
|
6
|
+
function normalizeOpenRouterModel(model) {
|
|
7
|
+
return model.startsWith(OPENROUTER_PREFIX)
|
|
8
|
+
? model.slice(OPENROUTER_PREFIX.length)
|
|
9
|
+
: model;
|
|
78
10
|
}
|
|
79
11
|
function normalizeMessage(content) {
|
|
80
12
|
const toolCalls = content.tool_calls ??
|
|
@@ -93,273 +25,465 @@ function normalizeMessage(content) {
|
|
|
93
25
|
tool_calls: toolCalls && toolCalls.length > 0 ? toolCalls : undefined,
|
|
94
26
|
};
|
|
95
27
|
}
|
|
96
|
-
function
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
28
|
+
function safeJson(input) {
|
|
29
|
+
try {
|
|
30
|
+
const parsed = JSON.parse(input);
|
|
31
|
+
if (parsed && typeof parsed === "object") {
|
|
32
|
+
return parsed;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
// fall through
|
|
37
|
+
}
|
|
38
|
+
return {};
|
|
39
|
+
}
|
|
40
|
+
function isAsyncIterable(value) {
|
|
41
|
+
return Boolean(value &&
|
|
42
|
+
typeof value === "object" &&
|
|
43
|
+
Symbol.asyncIterator in value);
|
|
44
|
+
}
|
|
45
|
+
function mapUsage(usage) {
|
|
46
|
+
if (!usage)
|
|
47
|
+
return undefined;
|
|
48
|
+
return {
|
|
49
|
+
promptTokens: usage.input_tokens ?? 0,
|
|
50
|
+
completionTokens: usage.output_tokens ?? 0,
|
|
51
|
+
totalTokens: usage.total_tokens ?? 0,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function mapStatus(status) {
|
|
55
|
+
if (!status)
|
|
56
|
+
return undefined;
|
|
57
|
+
if (status === "completed")
|
|
58
|
+
return "completed";
|
|
59
|
+
if (status === "in_progress" || status === "queued")
|
|
60
|
+
return "in_progress";
|
|
61
|
+
return "failed";
|
|
62
|
+
}
|
|
63
|
+
function mapError(error) {
|
|
64
|
+
if (!error)
|
|
65
|
+
return undefined;
|
|
66
|
+
return { code: error.code, message: error.message };
|
|
67
|
+
}
|
|
68
|
+
function mapTools(tools) {
|
|
69
|
+
if (!tools || tools.length === 0)
|
|
70
|
+
return undefined;
|
|
71
|
+
return tools.map((tool) => ({
|
|
72
|
+
type: "function",
|
|
73
|
+
name: tool.function.name,
|
|
74
|
+
description: tool.function.description ?? null,
|
|
75
|
+
parameters: normalizeToolParameters(tool.function.parameters),
|
|
76
|
+
strict: false,
|
|
77
|
+
}));
|
|
78
|
+
}
|
|
79
|
+
function normalizeToolParameters(parameters) {
|
|
80
|
+
const normalized = structuredClone(parameters ?? {});
|
|
81
|
+
if (normalized.type !== "object") {
|
|
82
|
+
return normalized;
|
|
83
|
+
}
|
|
84
|
+
if (normalized.properties === undefined) {
|
|
85
|
+
normalized.properties = {};
|
|
86
|
+
}
|
|
87
|
+
const props = normalized.properties;
|
|
88
|
+
if (props && typeof props === "object" && !Array.isArray(props)) {
|
|
89
|
+
const requiredKeys = Array.isArray(normalized.required)
|
|
90
|
+
? normalized.required.filter((key) => typeof key === "string" && key in props)
|
|
91
|
+
: [];
|
|
92
|
+
for (const [key, value] of Object.entries(props)) {
|
|
93
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
94
|
+
continue;
|
|
95
|
+
if (!("type" in value))
|
|
96
|
+
continue;
|
|
97
|
+
if (value.type === "object" && value.additionalProperties !== false) {
|
|
98
|
+
props[key] = {
|
|
99
|
+
...value,
|
|
100
|
+
additionalProperties: false,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (requiredKeys.length > 0) {
|
|
105
|
+
normalized.required = requiredKeys;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const additional = normalized.additionalProperties;
|
|
109
|
+
if (additional !== false) {
|
|
110
|
+
normalized.additionalProperties = false;
|
|
111
|
+
}
|
|
112
|
+
return normalized;
|
|
113
|
+
}
|
|
114
|
+
function appendSyntheticTools(tools, input) {
|
|
115
|
+
const needed = new Set();
|
|
116
|
+
for (const item of input) {
|
|
117
|
+
if (item.type !== "function_call")
|
|
118
|
+
continue;
|
|
119
|
+
if (item.name === GAMBIT_TOOL_CONTEXT || item.name === GAMBIT_TOOL_INIT) {
|
|
120
|
+
needed.add(item.name);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
for (const name of needed) {
|
|
124
|
+
if (tools.some((tool) => tool.name === name))
|
|
125
|
+
continue;
|
|
126
|
+
tools.push({
|
|
127
|
+
type: "function",
|
|
128
|
+
name,
|
|
129
|
+
description: "Synthetic Gambit context payload.",
|
|
130
|
+
parameters: {
|
|
131
|
+
type: "object",
|
|
132
|
+
properties: {},
|
|
133
|
+
additionalProperties: false,
|
|
134
|
+
required: [],
|
|
112
135
|
},
|
|
113
|
-
|
|
114
|
-
|
|
136
|
+
strict: false,
|
|
137
|
+
});
|
|
115
138
|
}
|
|
116
|
-
return items;
|
|
117
139
|
}
|
|
118
|
-
function
|
|
119
|
-
|
|
140
|
+
function mapToolChoice(toolChoice) {
|
|
141
|
+
if (!toolChoice)
|
|
142
|
+
return undefined;
|
|
143
|
+
if (toolChoice === "auto" || toolChoice === "required")
|
|
144
|
+
return toolChoice;
|
|
145
|
+
return { type: "function", name: toolChoice.function.name };
|
|
146
|
+
}
|
|
147
|
+
function mapOpenAIOutputItem(item) {
|
|
148
|
+
const itemType = item.type;
|
|
149
|
+
if (itemType === "message") {
|
|
150
|
+
const message = item;
|
|
151
|
+
const content = [];
|
|
152
|
+
for (const part of message.content ?? []) {
|
|
153
|
+
if (part.type === "output_text") {
|
|
154
|
+
content.push({ type: "output_text", text: part.text });
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (content.length === 0)
|
|
158
|
+
return null;
|
|
159
|
+
return {
|
|
160
|
+
type: "message",
|
|
161
|
+
role: "assistant",
|
|
162
|
+
content,
|
|
163
|
+
id: message.id,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
if (itemType === "function_call") {
|
|
167
|
+
const call = item;
|
|
168
|
+
return {
|
|
169
|
+
type: "function_call",
|
|
170
|
+
call_id: call.call_id,
|
|
171
|
+
name: call.name,
|
|
172
|
+
arguments: call.arguments,
|
|
173
|
+
id: call.id,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
function normalizeOpenAIResponse(response) {
|
|
179
|
+
const outputItems = (response.output ?? [])
|
|
180
|
+
.map(mapOpenAIOutputItem)
|
|
181
|
+
.filter((item) => Boolean(item));
|
|
182
|
+
return {
|
|
183
|
+
id: response.id,
|
|
184
|
+
object: "response",
|
|
185
|
+
model: response.model,
|
|
186
|
+
created: response.created_at,
|
|
187
|
+
status: mapStatus(response.status ?? undefined),
|
|
188
|
+
output: outputItems,
|
|
189
|
+
usage: mapUsage(response.usage),
|
|
190
|
+
error: mapError(response.error),
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
function toOpenAIInputItems(items) {
|
|
194
|
+
const mapped = [];
|
|
120
195
|
for (const item of items) {
|
|
121
|
-
if (item.type
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
196
|
+
if (item.type === "message") {
|
|
197
|
+
const isAssistant = item.role === "assistant";
|
|
198
|
+
const content = item.content
|
|
199
|
+
.map((part) => {
|
|
200
|
+
if (part.type === "output_text") {
|
|
201
|
+
return {
|
|
202
|
+
type: "output_text",
|
|
203
|
+
text: part.text,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
if (part.type === "input_text") {
|
|
207
|
+
return {
|
|
208
|
+
type: isAssistant ? "output_text" : "input_text",
|
|
209
|
+
text: part.text,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
return null;
|
|
213
|
+
})
|
|
214
|
+
.filter((part) => Boolean(part));
|
|
215
|
+
if (content.length === 0)
|
|
125
216
|
continue;
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
role:
|
|
217
|
+
mapped.push({
|
|
218
|
+
type: "message",
|
|
219
|
+
role: item.role,
|
|
129
220
|
content,
|
|
130
|
-
|
|
221
|
+
id: item.id,
|
|
131
222
|
});
|
|
132
223
|
continue;
|
|
133
224
|
}
|
|
134
|
-
if (item.
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
225
|
+
if (item.type === "function_call") {
|
|
226
|
+
mapped.push({
|
|
227
|
+
type: "function_call",
|
|
228
|
+
call_id: item.call_id,
|
|
229
|
+
name: item.name,
|
|
230
|
+
arguments: item.arguments,
|
|
231
|
+
id: item.id,
|
|
141
232
|
});
|
|
142
233
|
continue;
|
|
143
234
|
}
|
|
144
|
-
if (item.
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
235
|
+
if (item.type === "function_call_output") {
|
|
236
|
+
mapped.push({
|
|
237
|
+
type: "function_call_output",
|
|
238
|
+
call_id: item.call_id,
|
|
239
|
+
output: item.output,
|
|
240
|
+
id: item.id,
|
|
150
241
|
});
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return mapped;
|
|
245
|
+
}
|
|
246
|
+
function chatMessagesToResponseItems(messages) {
|
|
247
|
+
const items = [];
|
|
248
|
+
for (const message of messages) {
|
|
249
|
+
if (message.role === "tool") {
|
|
250
|
+
if (message.tool_call_id &&
|
|
251
|
+
typeof message.content === "string") {
|
|
252
|
+
items.push({
|
|
253
|
+
type: "function_call_output",
|
|
254
|
+
call_id: message.tool_call_id,
|
|
255
|
+
output: message.content,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
151
258
|
continue;
|
|
152
259
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
content
|
|
157
|
-
|
|
158
|
-
|
|
260
|
+
if (message.role === "system" || message.role === "user" ||
|
|
261
|
+
message.role === "assistant") {
|
|
262
|
+
const content = [];
|
|
263
|
+
if (typeof message.content === "string" && message.content.length > 0) {
|
|
264
|
+
content.push({
|
|
265
|
+
type: message.role === "assistant" ? "output_text" : "input_text",
|
|
266
|
+
text: message.content,
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
if (content.length > 0) {
|
|
270
|
+
items.push({
|
|
271
|
+
type: "message",
|
|
272
|
+
role: message.role,
|
|
273
|
+
content,
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
if (message.role === "assistant" && message.tool_calls) {
|
|
278
|
+
for (const call of message.tool_calls) {
|
|
279
|
+
items.push({
|
|
280
|
+
type: "function_call",
|
|
281
|
+
call_id: call.id,
|
|
282
|
+
name: call.function.name,
|
|
283
|
+
arguments: call.function.arguments,
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return items;
|
|
289
|
+
}
|
|
290
|
+
function responseItemsToChat(items) {
|
|
291
|
+
const textParts = [];
|
|
292
|
+
const toolCalls = [];
|
|
293
|
+
const messageToolCalls = [];
|
|
294
|
+
for (const item of items) {
|
|
295
|
+
if (item.type === "message" && item.role === "assistant") {
|
|
296
|
+
for (const part of item.content) {
|
|
297
|
+
if (part.type === "output_text") {
|
|
298
|
+
textParts.push(part.text);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
if (item.type === "function_call") {
|
|
303
|
+
toolCalls.push({
|
|
304
|
+
id: item.call_id,
|
|
305
|
+
name: item.name,
|
|
306
|
+
args: safeJson(item.arguments),
|
|
307
|
+
});
|
|
308
|
+
messageToolCalls.push({
|
|
309
|
+
id: item.call_id,
|
|
310
|
+
type: "function",
|
|
311
|
+
function: { name: item.name, arguments: item.arguments },
|
|
312
|
+
});
|
|
313
|
+
}
|
|
159
314
|
}
|
|
160
|
-
|
|
315
|
+
const content = textParts.length > 0 ? textParts.join("") : null;
|
|
316
|
+
const message = {
|
|
317
|
+
role: "assistant",
|
|
318
|
+
content,
|
|
319
|
+
tool_calls: messageToolCalls.length > 0 ? messageToolCalls : undefined,
|
|
320
|
+
};
|
|
321
|
+
return {
|
|
322
|
+
message,
|
|
323
|
+
toolCalls: toolCalls.length > 0 ? toolCalls : undefined,
|
|
324
|
+
};
|
|
161
325
|
}
|
|
162
|
-
function
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
326
|
+
async function createResponse(client, request, onStreamEvent) {
|
|
327
|
+
const baseParams = {
|
|
328
|
+
model: normalizeOpenRouterModel(request.model),
|
|
329
|
+
input: toOpenAIInputItems(request.input),
|
|
330
|
+
instructions: request.instructions,
|
|
331
|
+
tools: undefined,
|
|
332
|
+
tool_choice: mapToolChoice(request.tool_choice),
|
|
333
|
+
stream: request.stream,
|
|
334
|
+
max_output_tokens: request.max_output_tokens,
|
|
335
|
+
metadata: request.metadata,
|
|
168
336
|
};
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
337
|
+
const mappedTools = mapTools(request.tools) ?? [];
|
|
338
|
+
appendSyntheticTools(mappedTools, request.input);
|
|
339
|
+
if (mappedTools.length > 0) {
|
|
340
|
+
baseParams.tools = mappedTools;
|
|
341
|
+
}
|
|
342
|
+
const params = { ...(request.params ?? {}), ...baseParams };
|
|
343
|
+
const debugResponses = dntShim.Deno.env.get("GAMBIT_DEBUG_RESPONSES") === "1";
|
|
344
|
+
let responseOrStream;
|
|
345
|
+
try {
|
|
346
|
+
responseOrStream = await client.responses.create(params);
|
|
347
|
+
}
|
|
348
|
+
catch (err) {
|
|
349
|
+
if (debugResponses) {
|
|
350
|
+
logger.error("[responses-debug] request", params);
|
|
351
|
+
if (err instanceof OpenAI.APIError) {
|
|
352
|
+
logger.error("[responses-debug] error", err.error);
|
|
353
|
+
}
|
|
354
|
+
else {
|
|
355
|
+
logger.error("[responses-debug] error", err);
|
|
356
|
+
}
|
|
181
357
|
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
358
|
+
throw err;
|
|
359
|
+
}
|
|
360
|
+
if (request.stream &&
|
|
361
|
+
isAsyncIterable(responseOrStream)) {
|
|
362
|
+
let completed = null;
|
|
363
|
+
for await (const event of responseOrStream) {
|
|
364
|
+
if (!event || typeof event !== "object" || !("type" in event)) {
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
switch (event.type) {
|
|
368
|
+
case "response.created": {
|
|
369
|
+
const mapped = normalizeOpenAIResponse(event.response);
|
|
370
|
+
onStreamEvent?.({ type: "response.created", response: mapped });
|
|
371
|
+
break;
|
|
372
|
+
}
|
|
373
|
+
case "response.output_text.delta":
|
|
374
|
+
onStreamEvent?.({
|
|
375
|
+
type: "response.output_text.delta",
|
|
376
|
+
output_index: event.output_index,
|
|
377
|
+
delta: event.delta,
|
|
378
|
+
item_id: event.item_id,
|
|
379
|
+
});
|
|
380
|
+
break;
|
|
381
|
+
case "response.output_text.done":
|
|
382
|
+
onStreamEvent?.({
|
|
383
|
+
type: "response.output_text.done",
|
|
384
|
+
output_index: event.output_index,
|
|
385
|
+
text: event.text,
|
|
386
|
+
item_id: event.item_id,
|
|
387
|
+
});
|
|
388
|
+
break;
|
|
389
|
+
case "response.output_item.added": {
|
|
390
|
+
const item = mapOpenAIOutputItem(event.item);
|
|
391
|
+
if (item) {
|
|
392
|
+
onStreamEvent?.({
|
|
393
|
+
type: "response.output_item.added",
|
|
394
|
+
output_index: event.output_index,
|
|
395
|
+
item,
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
break;
|
|
399
|
+
}
|
|
400
|
+
case "response.output_item.done": {
|
|
401
|
+
const item = mapOpenAIOutputItem(event.item);
|
|
402
|
+
if (item) {
|
|
403
|
+
onStreamEvent?.({
|
|
404
|
+
type: "response.output_item.done",
|
|
405
|
+
output_index: event.output_index,
|
|
406
|
+
item,
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
break;
|
|
410
|
+
}
|
|
411
|
+
case "response.completed": {
|
|
412
|
+
completed = normalizeOpenAIResponse(event.response);
|
|
413
|
+
onStreamEvent?.({ type: "response.completed", response: completed });
|
|
414
|
+
break;
|
|
415
|
+
}
|
|
416
|
+
case "response.failed": {
|
|
417
|
+
const error = mapError(event.response?.error ?? undefined);
|
|
418
|
+
onStreamEvent?.({
|
|
419
|
+
type: "response.failed",
|
|
420
|
+
error: error ?? {},
|
|
421
|
+
});
|
|
422
|
+
break;
|
|
187
423
|
}
|
|
188
|
-
:
|
|
424
|
+
default:
|
|
425
|
+
break;
|
|
426
|
+
}
|
|
189
427
|
}
|
|
428
|
+
if (completed)
|
|
429
|
+
return completed;
|
|
430
|
+
throw new Error("OpenRouter responses stream ended without completion.");
|
|
190
431
|
}
|
|
191
|
-
return
|
|
432
|
+
return normalizeOpenAIResponse(responseOrStream);
|
|
192
433
|
}
|
|
193
434
|
export function createOpenRouterProvider(opts) {
|
|
194
435
|
const debugStream = dntShim.Deno.env.get("GAMBIT_DEBUG_STREAM") === "1";
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
apiKey: opts.apiKey,
|
|
205
|
-
baseURL: opts.baseURL ?? "https://openrouter.ai/api/v1",
|
|
206
|
-
defaultHeaders: {
|
|
207
|
-
"HTTP-Referer": opts.referer ?? "https://gambit.local",
|
|
208
|
-
"X-Title": opts.title ?? "Gambit CLI",
|
|
209
|
-
},
|
|
210
|
-
});
|
|
211
|
-
const openResponseEventTypes = new Set([
|
|
212
|
-
"response.output_text.delta",
|
|
213
|
-
"response.output_text.done",
|
|
214
|
-
"response.output_item.added",
|
|
215
|
-
"response.output_item.done",
|
|
216
|
-
"response.content_part.added",
|
|
217
|
-
"response.content_part.done",
|
|
218
|
-
"response.function_call_arguments.delta",
|
|
219
|
-
"response.function_call_arguments.done",
|
|
220
|
-
"response.refusal.delta",
|
|
221
|
-
"response.refusal.done",
|
|
222
|
-
"response.reasoning.delta",
|
|
223
|
-
"response.reasoning.done",
|
|
224
|
-
"response.reasoning_summary_text.delta",
|
|
225
|
-
"response.reasoning_summary_text.done",
|
|
226
|
-
"response.reasoning_summary_part.added",
|
|
227
|
-
"response.reasoning_summary_part.done",
|
|
228
|
-
"response.created",
|
|
229
|
-
"response.queued",
|
|
230
|
-
"response.in_progress",
|
|
231
|
-
"response.failed",
|
|
232
|
-
"response.incomplete",
|
|
233
|
-
"response.completed",
|
|
234
|
-
"error",
|
|
235
|
-
]);
|
|
236
|
-
const buildResponsesRequest = (input) => {
|
|
237
|
-
const { params: _params, state: _state, onStreamEvent: _onStreamEvent, ...request } = input;
|
|
238
|
-
return request;
|
|
239
|
-
};
|
|
436
|
+
const client = (opts.client ??
|
|
437
|
+
new OpenAI({
|
|
438
|
+
apiKey: opts.apiKey,
|
|
439
|
+
baseURL: opts.baseURL ?? "https://openrouter.ai/api/v1",
|
|
440
|
+
defaultHeaders: {
|
|
441
|
+
"HTTP-Referer": opts.referer ?? "https://gambit.local",
|
|
442
|
+
"X-Title": opts.title ?? "Gambit CLI",
|
|
443
|
+
},
|
|
444
|
+
}));
|
|
240
445
|
return {
|
|
241
446
|
async responses(input) {
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
input.onStreamEvent?.({
|
|
257
|
-
...streamEvent,
|
|
258
|
-
sequence_number: streamEvent.sequence_number ?? ++sequence,
|
|
259
|
-
});
|
|
260
|
-
}
|
|
261
|
-
if (event.type === "response.completed" ||
|
|
262
|
-
event.type === "response.failed" ||
|
|
263
|
-
event.type === "response.incomplete") {
|
|
264
|
-
terminalResponse = event.response;
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
if (!terminalResponse) {
|
|
268
|
-
throw new Error("OpenRouter responses stream ended without terminal response.");
|
|
447
|
+
return await createResponse(client, input.request, input.onStreamEvent);
|
|
448
|
+
},
|
|
449
|
+
async chat(input) {
|
|
450
|
+
const params = input.params ?? {};
|
|
451
|
+
if (opts.enableResponses) {
|
|
452
|
+
const response = await createResponse(client, {
|
|
453
|
+
model: normalizeOpenRouterModel(input.model),
|
|
454
|
+
input: chatMessagesToResponseItems(input.messages),
|
|
455
|
+
tools: input.tools,
|
|
456
|
+
stream: input.stream,
|
|
457
|
+
params,
|
|
458
|
+
}, (event) => {
|
|
459
|
+
if (event.type === "response.output_text.delta") {
|
|
460
|
+
input.onStreamText?.(event.delta);
|
|
269
461
|
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
return
|
|
462
|
+
});
|
|
463
|
+
const mapped = responseItemsToChat(response.output);
|
|
464
|
+
return {
|
|
465
|
+
message: mapped.message,
|
|
466
|
+
finishReason: mapped.toolCalls ? "tool_calls" : "stop",
|
|
467
|
+
toolCalls: mapped.toolCalls,
|
|
468
|
+
usage: response.usage,
|
|
469
|
+
};
|
|
273
470
|
}
|
|
274
|
-
const items = normalizeInputItems(input.input, input.instructions ?? null);
|
|
275
|
-
const messages = messagesFromResponseItems(items);
|
|
276
|
-
const requestParams = applyRequestParams(input, input.params ?? {});
|
|
277
|
-
const toolChoice = requestParams.tool_choice ?? "auto";
|
|
278
|
-
delete requestParams.tool_choice;
|
|
279
471
|
if (input.stream) {
|
|
280
472
|
if (debugStream) {
|
|
281
|
-
logger.log(`[stream-debug] requesting stream model=${input.model} messages=${messages.length} tools=${input.tools?.length ?? 0}`);
|
|
473
|
+
logger.log(`[stream-debug] requesting stream model=${input.model} messages=${input.messages.length} tools=${input.tools?.length ?? 0}`);
|
|
282
474
|
}
|
|
283
|
-
const
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
};
|
|
293
|
-
const responseSkeletonBase = {
|
|
294
|
-
id: responseId,
|
|
295
|
-
object: "response",
|
|
296
|
-
created_at: createdAt,
|
|
297
|
-
model: input.model,
|
|
298
|
-
previous_response_id: input.previous_response_id ?? null,
|
|
299
|
-
instructions: input.instructions ?? null,
|
|
300
|
-
tool_choice: input.tool_choice,
|
|
301
|
-
truncation: input.truncation,
|
|
302
|
-
parallel_tool_calls: input.parallel_tool_calls,
|
|
303
|
-
text: input.text,
|
|
304
|
-
max_output_tokens: input.max_output_tokens,
|
|
305
|
-
max_tool_calls: input.max_tool_calls,
|
|
306
|
-
store: input.store,
|
|
307
|
-
background: input.background,
|
|
308
|
-
service_tier: input.service_tier,
|
|
309
|
-
metadata: input.metadata,
|
|
310
|
-
safety_identifier: input.safety_identifier,
|
|
311
|
-
prompt_cache_key: input.prompt_cache_key,
|
|
312
|
-
tools: input.tools,
|
|
313
|
-
output: [],
|
|
314
|
-
};
|
|
315
|
-
emit({
|
|
316
|
-
type: "response.queued",
|
|
317
|
-
response: { ...responseSkeletonBase, status: "queued" },
|
|
318
|
-
});
|
|
319
|
-
const responseSkeleton = {
|
|
320
|
-
...responseSkeletonBase,
|
|
321
|
-
status: "in_progress",
|
|
322
|
-
};
|
|
323
|
-
emit({ type: "response.created", response: responseSkeleton });
|
|
324
|
-
emit({ type: "response.in_progress", response: responseSkeleton });
|
|
325
|
-
emit({
|
|
326
|
-
type: "response.output_item.added",
|
|
327
|
-
output_index: 0,
|
|
328
|
-
item: {
|
|
329
|
-
type: "message",
|
|
330
|
-
id: itemId,
|
|
331
|
-
status: "in_progress",
|
|
332
|
-
role: "assistant",
|
|
333
|
-
content: [],
|
|
334
|
-
},
|
|
475
|
+
const stream = await client.chat.completions.create({
|
|
476
|
+
model: normalizeOpenRouterModel(input.model),
|
|
477
|
+
messages: input
|
|
478
|
+
.messages,
|
|
479
|
+
tools: input
|
|
480
|
+
.tools,
|
|
481
|
+
tool_choice: "auto",
|
|
482
|
+
stream: true,
|
|
483
|
+
...params,
|
|
335
484
|
});
|
|
336
|
-
let stream = null;
|
|
337
|
-
try {
|
|
338
|
-
stream = await client.chat.completions.create({
|
|
339
|
-
model: input.model,
|
|
340
|
-
messages: messages,
|
|
341
|
-
tools: input.tools,
|
|
342
|
-
tool_choice: toolChoice,
|
|
343
|
-
stream: true,
|
|
344
|
-
...requestParams,
|
|
345
|
-
});
|
|
346
|
-
}
|
|
347
|
-
catch (err) {
|
|
348
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
349
|
-
emit({ type: "error", error: { code: "openrouter_error", message } });
|
|
350
|
-
emit({
|
|
351
|
-
type: "response.failed",
|
|
352
|
-
response: {
|
|
353
|
-
...responseSkeleton,
|
|
354
|
-
status: "failed",
|
|
355
|
-
error: { code: "openrouter_error", message },
|
|
356
|
-
},
|
|
357
|
-
});
|
|
358
|
-
throw err;
|
|
359
|
-
}
|
|
360
485
|
let finishReason = null;
|
|
361
486
|
const contentParts = [];
|
|
362
|
-
let contentPartStarted = false;
|
|
363
487
|
const toolCallMap = new Map();
|
|
364
488
|
let chunkCount = 0;
|
|
365
489
|
let streamedChars = 0;
|
|
@@ -373,24 +497,8 @@ export function createOpenRouterProvider(opts) {
|
|
|
373
497
|
}
|
|
374
498
|
const delta = choice.delta;
|
|
375
499
|
if (typeof delta.content === "string") {
|
|
376
|
-
if (!contentPartStarted) {
|
|
377
|
-
emit({
|
|
378
|
-
type: "response.content_part.added",
|
|
379
|
-
item_id: itemId,
|
|
380
|
-
output_index: 0,
|
|
381
|
-
content_index: 0,
|
|
382
|
-
part: { type: "output_text", text: "" },
|
|
383
|
-
});
|
|
384
|
-
contentPartStarted = true;
|
|
385
|
-
}
|
|
386
500
|
contentParts.push(delta.content);
|
|
387
|
-
|
|
388
|
-
type: "response.output_text.delta",
|
|
389
|
-
item_id: itemId,
|
|
390
|
-
output_index: 0,
|
|
391
|
-
content_index: 0,
|
|
392
|
-
delta: delta.content,
|
|
393
|
-
});
|
|
501
|
+
input.onStreamText?.(delta.content);
|
|
394
502
|
streamedChars += delta.content.length;
|
|
395
503
|
}
|
|
396
504
|
else if (Array.isArray(delta.content)) {
|
|
@@ -398,24 +506,8 @@ export function createOpenRouterProvider(opts) {
|
|
|
398
506
|
.map((c) => (typeof c === "string" ? c : ""))
|
|
399
507
|
.join("");
|
|
400
508
|
if (chunkStr) {
|
|
401
|
-
if (!contentPartStarted) {
|
|
402
|
-
emit({
|
|
403
|
-
type: "response.content_part.added",
|
|
404
|
-
item_id: itemId,
|
|
405
|
-
output_index: 0,
|
|
406
|
-
content_index: 0,
|
|
407
|
-
part: { type: "output_text", text: "" },
|
|
408
|
-
});
|
|
409
|
-
contentPartStarted = true;
|
|
410
|
-
}
|
|
411
509
|
contentParts.push(chunkStr);
|
|
412
|
-
|
|
413
|
-
type: "response.output_text.delta",
|
|
414
|
-
item_id: itemId,
|
|
415
|
-
output_index: 0,
|
|
416
|
-
content_index: 0,
|
|
417
|
-
delta: chunkStr,
|
|
418
|
-
});
|
|
510
|
+
input.onStreamText?.(chunkStr);
|
|
419
511
|
streamedChars += chunkStr.length;
|
|
420
512
|
}
|
|
421
513
|
}
|
|
@@ -426,20 +518,12 @@ export function createOpenRouterProvider(opts) {
|
|
|
426
518
|
id: tc.id,
|
|
427
519
|
function: { name: tc.function?.name, arguments: "" },
|
|
428
520
|
};
|
|
429
|
-
if (
|
|
430
|
-
existing.id = tc.id
|
|
431
|
-
crypto.randomUUID().replace(/-/g, "").slice(0, 24);
|
|
432
|
-
}
|
|
521
|
+
if (tc.id)
|
|
522
|
+
existing.id = tc.id;
|
|
433
523
|
if (tc.function?.name)
|
|
434
524
|
existing.function.name = tc.function.name;
|
|
435
525
|
if (tc.function?.arguments) {
|
|
436
526
|
existing.function.arguments += tc.function.arguments;
|
|
437
|
-
emit({
|
|
438
|
-
type: "response.function_call_arguments.delta",
|
|
439
|
-
item_id: existing.id,
|
|
440
|
-
output_index: 0,
|
|
441
|
-
delta: tc.function.arguments,
|
|
442
|
-
});
|
|
443
527
|
}
|
|
444
528
|
toolCallMap.set(idx, existing);
|
|
445
529
|
}
|
|
@@ -455,135 +539,48 @@ export function createOpenRouterProvider(opts) {
|
|
|
455
539
|
arguments: tc.function.arguments,
|
|
456
540
|
},
|
|
457
541
|
}));
|
|
458
|
-
for (const call of tool_calls) {
|
|
459
|
-
emit({
|
|
460
|
-
type: "response.function_call_arguments.done",
|
|
461
|
-
item_id: call.id,
|
|
462
|
-
output_index: 0,
|
|
463
|
-
arguments: call.function.arguments,
|
|
464
|
-
});
|
|
465
|
-
}
|
|
466
|
-
const text = contentParts.length ? contentParts.join("") : "";
|
|
467
|
-
const outputPart = {
|
|
468
|
-
type: "output_text",
|
|
469
|
-
text,
|
|
470
|
-
};
|
|
471
542
|
const message = normalizeMessage({
|
|
472
543
|
role: "assistant",
|
|
473
|
-
content:
|
|
544
|
+
content: contentParts.length ? contentParts.join("") : null,
|
|
474
545
|
tool_calls,
|
|
475
546
|
});
|
|
476
|
-
const
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
tool_call_id: message.tool_call_id,
|
|
484
|
-
tool_calls: message.tool_calls,
|
|
485
|
-
};
|
|
486
|
-
if (contentPartStarted && text.length > 0) {
|
|
487
|
-
emit({
|
|
488
|
-
type: "response.output_text.done",
|
|
489
|
-
item_id: itemId,
|
|
490
|
-
output_index: 0,
|
|
491
|
-
content_index: 0,
|
|
492
|
-
text,
|
|
493
|
-
});
|
|
494
|
-
emit({
|
|
495
|
-
type: "response.content_part.done",
|
|
496
|
-
item_id: itemId,
|
|
497
|
-
output_index: 0,
|
|
498
|
-
content_index: 0,
|
|
499
|
-
part: outputPart,
|
|
500
|
-
});
|
|
501
|
-
}
|
|
502
|
-
emit({
|
|
503
|
-
type: "response.output_item.done",
|
|
504
|
-
output_index: 0,
|
|
505
|
-
item: outputItem,
|
|
506
|
-
});
|
|
507
|
-
const completedAt = Math.floor(Date.now() / 1000);
|
|
508
|
-
const status = finishReason === "length" ? "incomplete" : "completed";
|
|
509
|
-
const responseResource = {
|
|
510
|
-
...responseSkeleton,
|
|
511
|
-
completed_at: completedAt,
|
|
512
|
-
status,
|
|
513
|
-
output: [outputItem],
|
|
514
|
-
finishReason: finishReason ?? "stop",
|
|
515
|
-
};
|
|
516
|
-
if (status === "incomplete") {
|
|
517
|
-
emit({
|
|
518
|
-
type: "response.incomplete",
|
|
519
|
-
response: responseResource,
|
|
520
|
-
});
|
|
521
|
-
}
|
|
522
|
-
else {
|
|
523
|
-
emit({
|
|
524
|
-
type: "response.completed",
|
|
525
|
-
response: responseResource,
|
|
526
|
-
});
|
|
527
|
-
}
|
|
547
|
+
const toolCalls = tool_calls.length > 0
|
|
548
|
+
? tool_calls.map((tc) => ({
|
|
549
|
+
id: tc.id,
|
|
550
|
+
name: tc.function.name,
|
|
551
|
+
args: safeJson(tc.function.arguments),
|
|
552
|
+
}))
|
|
553
|
+
: undefined;
|
|
528
554
|
return {
|
|
529
|
-
|
|
555
|
+
message,
|
|
556
|
+
finishReason: finishReason ?? "stop",
|
|
557
|
+
toolCalls,
|
|
530
558
|
};
|
|
531
559
|
}
|
|
532
560
|
const response = await client.chat.completions.create({
|
|
533
|
-
model: input.model,
|
|
534
|
-
messages:
|
|
535
|
-
|
|
536
|
-
|
|
561
|
+
model: normalizeOpenRouterModel(input.model),
|
|
562
|
+
messages: input
|
|
563
|
+
.messages,
|
|
564
|
+
tools: input
|
|
565
|
+
.tools,
|
|
566
|
+
tool_choice: "auto",
|
|
537
567
|
stream: false,
|
|
538
|
-
...
|
|
568
|
+
...params,
|
|
539
569
|
});
|
|
540
570
|
const choice = response.choices[0];
|
|
541
571
|
const message = choice.message;
|
|
542
572
|
const normalizedMessage = normalizeMessage(message);
|
|
543
|
-
const
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
status: "completed",
|
|
549
|
-
role: normalizedMessage.role,
|
|
550
|
-
content: normalizedMessage.content,
|
|
551
|
-
name: normalizedMessage.name,
|
|
552
|
-
tool_call_id: normalizedMessage.tool_call_id,
|
|
553
|
-
tool_calls: normalizedMessage.tool_calls,
|
|
554
|
-
};
|
|
555
|
-
const finishReason = choice.finish_reason ??
|
|
556
|
-
"stop";
|
|
557
|
-
const status = finishReason === "length" ? "incomplete" : "completed";
|
|
573
|
+
const toolCalls = message.tool_calls?.map((tc) => ({
|
|
574
|
+
id: tc.id,
|
|
575
|
+
name: tc.function.name,
|
|
576
|
+
args: safeJson(tc.function.arguments),
|
|
577
|
+
}));
|
|
558
578
|
return {
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
completed_at: createdAt,
|
|
563
|
-
status,
|
|
564
|
-
model: input.model,
|
|
565
|
-
previous_response_id: input.previous_response_id ?? null,
|
|
566
|
-
instructions: input.instructions ?? null,
|
|
567
|
-
tool_choice: input.tool_choice,
|
|
568
|
-
truncation: input.truncation,
|
|
569
|
-
parallel_tool_calls: input.parallel_tool_calls,
|
|
570
|
-
text: input.text,
|
|
571
|
-
max_output_tokens: input.max_output_tokens,
|
|
572
|
-
max_tool_calls: input.max_tool_calls,
|
|
573
|
-
store: input.store,
|
|
574
|
-
background: input.background,
|
|
575
|
-
service_tier: input.service_tier,
|
|
576
|
-
metadata: input.metadata,
|
|
577
|
-
safety_identifier: input.safety_identifier,
|
|
578
|
-
prompt_cache_key: input.prompt_cache_key,
|
|
579
|
-
tools: input.tools,
|
|
580
|
-
output: [outputItem],
|
|
581
|
-
finishReason,
|
|
579
|
+
message: normalizedMessage,
|
|
580
|
+
finishReason: (choice.finish_reason ?? "stop"),
|
|
581
|
+
toolCalls,
|
|
582
582
|
usage: response.usage
|
|
583
583
|
? {
|
|
584
|
-
input_tokens: response.usage.prompt_tokens ?? 0,
|
|
585
|
-
output_tokens: response.usage.completion_tokens ?? 0,
|
|
586
|
-
total_tokens: response.usage.total_tokens ?? 0,
|
|
587
584
|
promptTokens: response.usage.prompt_tokens ?? 0,
|
|
588
585
|
completionTokens: response.usage.completion_tokens ?? 0,
|
|
589
586
|
totalTokens: response.usage.total_tokens ?? 0,
|