@miraiclip/assistant 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -4
- package/dist/index.cjs +727 -62
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +350 -23
- package/dist/index.d.ts +350 -23
- package/dist/index.js +727 -62
- package/dist/index.js.map +1 -1
- package/package.json +11 -5
package/dist/index.cjs
CHANGED
|
@@ -20,29 +20,41 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/index.ts
|
|
21
21
|
var src_exports = {};
|
|
22
22
|
__export(src_exports, {
|
|
23
|
+
AI_PROVIDERS: () => AI_PROVIDERS,
|
|
23
24
|
BACKGROUND_TEMPLATE: () => BACKGROUND_TEMPLATE,
|
|
24
25
|
ChatModelError: () => ChatModelError,
|
|
25
26
|
DEFAULT_INSTRUCTIONS: () => DEFAULT_INSTRUCTIONS,
|
|
27
|
+
USER_KEY_HEADERS: () => USER_KEY_HEADERS,
|
|
28
|
+
aiProvider: () => aiProvider,
|
|
29
|
+
anthropicChatModel: () => anthropicChatModel,
|
|
26
30
|
applyChecked: () => applyChecked,
|
|
27
31
|
applyCommandsSchema: () => applyCommandsSchema,
|
|
32
|
+
chatModelsFromEnv: () => chatModelsFromEnv,
|
|
28
33
|
commitFork: () => commitFork,
|
|
29
34
|
createAssistant: () => createAssistant,
|
|
30
35
|
createChatHandler: () => createChatHandler,
|
|
36
|
+
createChatModel: () => createChatModel,
|
|
31
37
|
defineChatModel: () => defineChatModel,
|
|
32
38
|
defineTool: () => defineTool,
|
|
33
39
|
describeCommands: () => describeCommands,
|
|
34
40
|
describeForAssistant: () => describeForAssistant,
|
|
35
41
|
editorTools: () => editorTools,
|
|
36
42
|
forkProject: () => forkProject,
|
|
43
|
+
geminiChatModel: () => geminiChatModel,
|
|
37
44
|
openAIChatModel: () => openAIChatModel,
|
|
38
45
|
parseToolArguments: () => parseToolArguments,
|
|
39
46
|
rateLimitedChatModel: () => rateLimitedChatModel,
|
|
40
47
|
remoteChatModel: () => remoteChatModel,
|
|
41
48
|
retryDelayMs: () => retryDelayMs,
|
|
49
|
+
routedChatModel: () => routedChatModel,
|
|
42
50
|
scriptedChatModel: () => scriptedChatModel,
|
|
51
|
+
splitModelRef: () => splitModelRef,
|
|
52
|
+
toAnthropicMessages: () => toAnthropicMessages,
|
|
53
|
+
toGeminiContents: () => toGeminiContents,
|
|
43
54
|
toOpenAIMessages: () => toOpenAIMessages,
|
|
44
55
|
toolsFromDefinitions: () => toolsFromDefinitions,
|
|
45
56
|
unknownFields: () => unknownFields,
|
|
57
|
+
userKeyHeaders: () => userKeyHeaders,
|
|
46
58
|
withExplicitIds: () => withExplicitIds
|
|
47
59
|
});
|
|
48
60
|
module.exports = __toCommonJS(src_exports);
|
|
@@ -70,20 +82,7 @@ function defineChatModel(model) {
|
|
|
70
82
|
return model;
|
|
71
83
|
}
|
|
72
84
|
|
|
73
|
-
// src/
|
|
74
|
-
function toOpenAIMessages(messages) {
|
|
75
|
-
return messages.map((m) => {
|
|
76
|
-
if (m.role === "assistant") {
|
|
77
|
-
const out = { role: "assistant", content: m.content || null };
|
|
78
|
-
if (m.toolCalls?.length) {
|
|
79
|
-
out.tool_calls = m.toolCalls.map((c) => ({ id: c.id, type: "function", function: { name: c.name, arguments: JSON.stringify(c.arguments) } }));
|
|
80
|
-
}
|
|
81
|
-
return out;
|
|
82
|
-
}
|
|
83
|
-
if (m.role === "tool") return { role: "tool", tool_call_id: m.toolCallId, content: m.content };
|
|
84
|
-
return { role: m.role, content: m.content };
|
|
85
|
-
});
|
|
86
|
-
}
|
|
85
|
+
// src/wire.ts
|
|
87
86
|
function parseToolArguments(raw) {
|
|
88
87
|
if (!raw.trim()) return {};
|
|
89
88
|
try {
|
|
@@ -93,7 +92,6 @@ function parseToolArguments(raw) {
|
|
|
93
92
|
return { __invalidArguments: raw };
|
|
94
93
|
}
|
|
95
94
|
}
|
|
96
|
-
var RETRYABLE = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
|
|
97
95
|
function retryDelayMs(res, message, attempt) {
|
|
98
96
|
const header = Number(res.headers.get("retry-after-ms") ?? NaN) || Number(res.headers.get("retry-after") ?? NaN) * 1e3;
|
|
99
97
|
const m = /try again in ([\d.]+)\s*(ms|s)/i.exec(message);
|
|
@@ -108,6 +106,73 @@ function sleep(ms, signal) {
|
|
|
108
106
|
signal?.addEventListener("abort", () => (clearTimeout(t), reject(signal.reason ?? new Error("aborted"))), { once: true });
|
|
109
107
|
});
|
|
110
108
|
}
|
|
109
|
+
var RETRYABLE = /* @__PURE__ */ new Set([429, 500, 502, 503, 504, 529]);
|
|
110
|
+
function errorMessage(body, status) {
|
|
111
|
+
const b = body;
|
|
112
|
+
const msg = typeof b?.error === "string" ? b.error : b?.error?.message ?? b?.message;
|
|
113
|
+
return msg || `HTTP ${status}`;
|
|
114
|
+
}
|
|
115
|
+
async function sendWithRetry(provider, doFetch, url, init, options = {}) {
|
|
116
|
+
const maxRetries = options.maxRetries ?? 4;
|
|
117
|
+
for (let attempt = 0; ; attempt++) {
|
|
118
|
+
let res;
|
|
119
|
+
try {
|
|
120
|
+
res = await doFetch(url, { ...init, ...options.signal ? { signal: options.signal } : {} });
|
|
121
|
+
} catch (err) {
|
|
122
|
+
if (err?.name === "AbortError" || options.signal?.aborted) throw err;
|
|
123
|
+
throw new ChatModelError(provider, `request failed: ${err?.message ?? err}`);
|
|
124
|
+
}
|
|
125
|
+
if (res.ok) return res;
|
|
126
|
+
const message = errorMessage(await res.json().catch(() => null), res.status);
|
|
127
|
+
const permanent = /quota|credit|billing/i.test(message);
|
|
128
|
+
if (attempt >= maxRetries || !RETRYABLE.has(res.status) || permanent) throw new ChatModelError(provider, message, res.status);
|
|
129
|
+
await sleep(retryDelayMs(res, message, attempt), options.signal);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
async function getJson(provider, doFetch, url, headers, signal) {
|
|
133
|
+
const res = await sendWithRetry(provider, doFetch, url, { method: "GET", headers }, { maxRetries: 1, ...signal ? { signal } : {} });
|
|
134
|
+
return res.json();
|
|
135
|
+
}
|
|
136
|
+
async function readSse(body, onData) {
|
|
137
|
+
const reader = body.getReader();
|
|
138
|
+
const decoder = new TextDecoder();
|
|
139
|
+
let buffer = "";
|
|
140
|
+
const line = (raw) => {
|
|
141
|
+
const l = raw.replace(/\r$/, "");
|
|
142
|
+
if (l.startsWith("data:")) onData(l.slice(5).trim());
|
|
143
|
+
};
|
|
144
|
+
for (; ; ) {
|
|
145
|
+
const { done, value } = await reader.read();
|
|
146
|
+
if (value) buffer += decoder.decode(value, { stream: true });
|
|
147
|
+
let nl;
|
|
148
|
+
while ((nl = buffer.indexOf("\n")) >= 0) {
|
|
149
|
+
line(buffer.slice(0, nl));
|
|
150
|
+
buffer = buffer.slice(nl + 1);
|
|
151
|
+
}
|
|
152
|
+
if (done) break;
|
|
153
|
+
}
|
|
154
|
+
if (buffer) line(buffer);
|
|
155
|
+
}
|
|
156
|
+
var isEventStream = (res) => !!res.body && (res.headers.get("content-type") ?? "").includes("event-stream");
|
|
157
|
+
var trimSlash = (url) => url.replace(/\/+$/, "");
|
|
158
|
+
function modelList(model, others = []) {
|
|
159
|
+
return [others.find((m) => m.id === model) ?? { id: model }, ...others.filter((m) => m.id !== model)];
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// src/openai.ts
|
|
163
|
+
function toOpenAIMessages(messages) {
|
|
164
|
+
return messages.map((m) => {
|
|
165
|
+
if (m.role === "assistant") {
|
|
166
|
+
const out = { role: "assistant", content: m.content || null };
|
|
167
|
+
if (m.toolCalls?.length) {
|
|
168
|
+
out.tool_calls = m.toolCalls.map((c) => ({ id: c.id, type: "function", function: { name: c.name, arguments: JSON.stringify(c.arguments) } }));
|
|
169
|
+
}
|
|
170
|
+
return out;
|
|
171
|
+
}
|
|
172
|
+
if (m.role === "tool") return { role: "tool", tool_call_id: m.toolCallId, content: m.content };
|
|
173
|
+
return { role: m.role, content: m.content };
|
|
174
|
+
});
|
|
175
|
+
}
|
|
111
176
|
var FINISH = {
|
|
112
177
|
stop: "stop",
|
|
113
178
|
tool_calls: "tool_calls",
|
|
@@ -116,48 +181,45 @@ var FINISH = {
|
|
|
116
181
|
content_filter: "content_filter"
|
|
117
182
|
};
|
|
118
183
|
function openAIChatModel(options) {
|
|
119
|
-
const base = (options.baseUrl ?? "https://api.openai.com/v1")
|
|
184
|
+
const base = trimSlash(options.baseUrl ?? "https://api.openai.com/v1");
|
|
120
185
|
const id = options.id ?? "openai";
|
|
121
186
|
const doFetch = options.fetch ?? defaultFetch();
|
|
122
187
|
const stream = options.stream ?? true;
|
|
123
|
-
const models =
|
|
188
|
+
const models = modelList(options.model, options.models);
|
|
189
|
+
const maxTokensField = options.maxTokensField ?? "max_completion_tokens";
|
|
190
|
+
const headers = () => {
|
|
191
|
+
const h = { "Content-Type": "application/json", ...options.headers };
|
|
192
|
+
if (options.apiKey) h["Authorization"] = `Bearer ${options.apiKey}`;
|
|
193
|
+
if (options.organization) h["OpenAI-Organization"] = options.organization;
|
|
194
|
+
if (options.project) h["OpenAI-Project"] = options.project;
|
|
195
|
+
return h;
|
|
196
|
+
};
|
|
124
197
|
return defineChatModel({
|
|
125
198
|
id,
|
|
126
199
|
label: options.label ?? "OpenAI",
|
|
127
200
|
models,
|
|
201
|
+
async listModels(opts = {}) {
|
|
202
|
+
const json2 = await getJson(id, doFetch, `${base}/models`, headers(), opts.signal);
|
|
203
|
+
return (json2.data ?? []).map((m) => ({ id: m.id })).sort((a, b) => a.id.localeCompare(b.id));
|
|
204
|
+
},
|
|
128
205
|
async complete(request, opts = {}) {
|
|
129
206
|
const body = {
|
|
130
207
|
model: request.model ?? options.model,
|
|
131
208
|
messages: toOpenAIMessages(request.messages),
|
|
132
209
|
...request.tools?.length ? { tools: request.tools.map((t) => ({ type: "function", function: { name: t.name, description: t.description, parameters: t.inputSchema } })) } : {},
|
|
133
|
-
...request.maxOutputTokens ? {
|
|
134
|
-
...stream ? { stream: true, stream_options: { include_usage: true } } : {},
|
|
210
|
+
...request.maxOutputTokens ? { [maxTokensField]: request.maxOutputTokens } : {},
|
|
211
|
+
...stream ? { stream: true, ...options.streamUsage === false ? {} : { stream_options: { include_usage: true } } } : {},
|
|
135
212
|
...options.params,
|
|
136
213
|
...request.params
|
|
137
214
|
};
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
method: "POST",
|
|
147
|
-
headers,
|
|
148
|
-
body: JSON.stringify(body),
|
|
149
|
-
...opts.signal ? { signal: opts.signal } : {}
|
|
150
|
-
});
|
|
151
|
-
if (res.ok) break;
|
|
152
|
-
const err = await res.json().catch(() => null);
|
|
153
|
-
const message = err?.error?.message ?? `HTTP ${res.status}`;
|
|
154
|
-
const permanent = /quota|credits|billing/i.test(message);
|
|
155
|
-
if (attempt >= maxRetries || !RETRYABLE.has(res.status) || permanent) throw new ChatModelError(id, message, res.status);
|
|
156
|
-
await sleep(retryDelayMs(res, message, attempt), opts.signal);
|
|
157
|
-
}
|
|
158
|
-
if (!stream || !res.body || !(res.headers.get("content-type") ?? "").includes("event-stream")) {
|
|
159
|
-
return fromCompletion(await res.json(), opts.onText);
|
|
160
|
-
}
|
|
215
|
+
const res = await sendWithRetry(
|
|
216
|
+
id,
|
|
217
|
+
doFetch,
|
|
218
|
+
`${base}/chat/completions`,
|
|
219
|
+
{ method: "POST", headers: headers(), body: JSON.stringify(body) },
|
|
220
|
+
{ ...options.maxRetries !== void 0 ? { maxRetries: options.maxRetries } : {}, ...opts.signal ? { signal: opts.signal } : {} }
|
|
221
|
+
);
|
|
222
|
+
if (!stream || !isEventStream(res)) return fromCompletion(await res.json(), opts.onText);
|
|
161
223
|
return readStream(res.body, opts.onText);
|
|
162
224
|
}
|
|
163
225
|
});
|
|
@@ -180,9 +242,6 @@ var usage = (u) => ({
|
|
|
180
242
|
...u.completion_tokens !== void 0 ? { outputTokens: u.completion_tokens } : {}
|
|
181
243
|
});
|
|
182
244
|
async function readStream(body, onText) {
|
|
183
|
-
const reader = body.getReader();
|
|
184
|
-
const decoder = new TextDecoder();
|
|
185
|
-
let buffer = "";
|
|
186
245
|
let content = "";
|
|
187
246
|
let finish = "other";
|
|
188
247
|
let model;
|
|
@@ -214,18 +273,7 @@ async function readStream(body, onText) {
|
|
|
214
273
|
if (choice.finish_reason) finish = FINISH[choice.finish_reason] ?? "other";
|
|
215
274
|
}
|
|
216
275
|
};
|
|
217
|
-
|
|
218
|
-
const { done, value } = await reader.read();
|
|
219
|
-
if (value) buffer += decoder.decode(value, { stream: true });
|
|
220
|
-
let nl;
|
|
221
|
-
while ((nl = buffer.indexOf("\n")) >= 0) {
|
|
222
|
-
const line = buffer.slice(0, nl).replace(/\r$/, "");
|
|
223
|
-
buffer = buffer.slice(nl + 1);
|
|
224
|
-
if (line.startsWith("data:")) handle(line.slice(5).trim());
|
|
225
|
-
}
|
|
226
|
-
if (done) break;
|
|
227
|
-
}
|
|
228
|
-
if (buffer.startsWith("data:")) handle(buffer.slice(5).trim());
|
|
276
|
+
await readSse(body, handle);
|
|
229
277
|
const toolCalls = [...calls.entries()].sort(([a], [b]) => a - b).map(([index, c]) => ({ id: c.id || `call_${index}`, name: c.name, arguments: parseToolArguments(c.args) }));
|
|
230
278
|
const out = { content, toolCalls, finish };
|
|
231
279
|
if (model) out.model = model;
|
|
@@ -233,10 +281,585 @@ async function readStream(body, onText) {
|
|
|
233
281
|
return out;
|
|
234
282
|
}
|
|
235
283
|
|
|
284
|
+
// src/anthropic.ts
|
|
285
|
+
function toAnthropicMessages(messages) {
|
|
286
|
+
const system = [];
|
|
287
|
+
const out = [];
|
|
288
|
+
const push = (role, blocks) => {
|
|
289
|
+
if (!blocks.length) return;
|
|
290
|
+
const last = out[out.length - 1];
|
|
291
|
+
if (last && last.role === role) last.content.push(...blocks);
|
|
292
|
+
else out.push({ role, content: blocks });
|
|
293
|
+
};
|
|
294
|
+
for (const m of messages) {
|
|
295
|
+
if (m.role === "system") system.push(m.content);
|
|
296
|
+
else if (m.role === "user") push("user", m.content.trim() ? [{ type: "text", text: m.content }] : []);
|
|
297
|
+
else if (m.role === "tool") push("user", [{ type: "tool_result", tool_use_id: m.toolCallId, content: m.content }]);
|
|
298
|
+
else {
|
|
299
|
+
const thinking = m.toolCalls?.[0]?.meta?.anthropicThinking ?? [];
|
|
300
|
+
push("assistant", [
|
|
301
|
+
...thinking,
|
|
302
|
+
...m.content.trim() ? [{ type: "text", text: m.content }] : [],
|
|
303
|
+
...(m.toolCalls ?? []).map((c) => ({ type: "tool_use", id: c.id, name: c.name, input: c.arguments }))
|
|
304
|
+
]);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return { system: system.join("\n\n"), messages: out };
|
|
308
|
+
}
|
|
309
|
+
var FINISH2 = {
|
|
310
|
+
end_turn: "stop",
|
|
311
|
+
stop_sequence: "stop",
|
|
312
|
+
tool_use: "tool_calls",
|
|
313
|
+
max_tokens: "length",
|
|
314
|
+
model_context_window_exceeded: "length",
|
|
315
|
+
refusal: "content_filter"
|
|
316
|
+
};
|
|
317
|
+
var usageOf = (u) => {
|
|
318
|
+
const input = (u.input_tokens ?? 0) + (u.cache_creation_input_tokens ?? 0) + (u.cache_read_input_tokens ?? 0);
|
|
319
|
+
return {
|
|
320
|
+
...u.input_tokens !== void 0 ? { inputTokens: input } : {},
|
|
321
|
+
...u.output_tokens !== void 0 ? { outputTokens: u.output_tokens } : {}
|
|
322
|
+
};
|
|
323
|
+
};
|
|
324
|
+
function fromBlocks(blocks, stopReason, model, usage2) {
|
|
325
|
+
const thinking = blocks.filter((b) => b.type === "thinking" || b.type === "redacted_thinking");
|
|
326
|
+
const toolCalls = blocks.filter((b) => b.type === "tool_use").map((b, i) => ({ id: b.id, name: b.name, arguments: b.input ?? {}, ...i === 0 && thinking.length ? { meta: { anthropicThinking: thinking } } : {} }));
|
|
327
|
+
const out = {
|
|
328
|
+
content: blocks.map((b) => b.type === "text" ? b.text : "").join(""),
|
|
329
|
+
toolCalls,
|
|
330
|
+
finish: FINISH2[stopReason ?? ""] ?? "other"
|
|
331
|
+
};
|
|
332
|
+
if (model) out.model = model;
|
|
333
|
+
if (usage2) out.usage = usageOf(usage2);
|
|
334
|
+
return out;
|
|
335
|
+
}
|
|
336
|
+
function anthropicChatModel(options) {
|
|
337
|
+
const base = trimSlash(options.baseUrl ?? "https://api.anthropic.com");
|
|
338
|
+
const id = options.id ?? "anthropic";
|
|
339
|
+
const doFetch = options.fetch ?? defaultFetch();
|
|
340
|
+
const stream = options.stream ?? true;
|
|
341
|
+
const models = modelList(options.model, options.models);
|
|
342
|
+
const headers = () => {
|
|
343
|
+
const h = { "Content-Type": "application/json", "anthropic-version": options.version ?? "2023-06-01", ...options.headers };
|
|
344
|
+
if (options.apiKey) h["x-api-key"] = options.apiKey;
|
|
345
|
+
if (options.betas?.length) h["anthropic-beta"] = options.betas.join(",");
|
|
346
|
+
if (options.browser) h["anthropic-dangerous-direct-browser-access"] = "true";
|
|
347
|
+
return h;
|
|
348
|
+
};
|
|
349
|
+
return defineChatModel({
|
|
350
|
+
id,
|
|
351
|
+
label: options.label ?? "Anthropic",
|
|
352
|
+
models,
|
|
353
|
+
async listModels(opts = {}) {
|
|
354
|
+
const json2 = await getJson(id, doFetch, `${base}/v1/models?limit=1000`, headers(), opts.signal);
|
|
355
|
+
return (json2.data ?? []).map((m) => ({ id: m.id, ...m.display_name ? { label: m.display_name } : {} }));
|
|
356
|
+
},
|
|
357
|
+
async complete(request, opts = {}) {
|
|
358
|
+
const { system, messages } = toAnthropicMessages(request.messages);
|
|
359
|
+
const body = {
|
|
360
|
+
model: request.model ?? options.model,
|
|
361
|
+
max_tokens: request.maxOutputTokens ?? options.maxOutputTokens ?? 8192,
|
|
362
|
+
messages,
|
|
363
|
+
...system ? { system } : {},
|
|
364
|
+
...request.tools?.length ? { tools: request.tools.map((t) => ({ name: t.name, description: t.description, input_schema: t.inputSchema })) } : {},
|
|
365
|
+
...stream ? { stream: true } : {},
|
|
366
|
+
...options.params,
|
|
367
|
+
...request.params
|
|
368
|
+
};
|
|
369
|
+
const res = await sendWithRetry(
|
|
370
|
+
id,
|
|
371
|
+
doFetch,
|
|
372
|
+
`${base}/v1/messages`,
|
|
373
|
+
{ method: "POST", headers: headers(), body: JSON.stringify(body) },
|
|
374
|
+
{ ...options.maxRetries !== void 0 ? { maxRetries: options.maxRetries } : {}, ...opts.signal ? { signal: opts.signal } : {} }
|
|
375
|
+
);
|
|
376
|
+
if (!stream || !isEventStream(res)) {
|
|
377
|
+
const json2 = await res.json();
|
|
378
|
+
const out = fromBlocks(json2.content ?? [], json2.stop_reason, json2.model, json2.usage);
|
|
379
|
+
if (out.content) opts.onText?.(out.content);
|
|
380
|
+
return out;
|
|
381
|
+
}
|
|
382
|
+
return readStream2(id, res.body, opts.onText);
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
async function readStream2(id, body, onText) {
|
|
387
|
+
const blocks = /* @__PURE__ */ new Map();
|
|
388
|
+
let model;
|
|
389
|
+
let stopReason;
|
|
390
|
+
const usage2 = {};
|
|
391
|
+
let failure;
|
|
392
|
+
await readSse(body, (data) => {
|
|
393
|
+
let event;
|
|
394
|
+
try {
|
|
395
|
+
event = JSON.parse(data);
|
|
396
|
+
} catch {
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
switch (event.type) {
|
|
400
|
+
case "message_start":
|
|
401
|
+
if (event.message?.model) model = event.message.model;
|
|
402
|
+
Object.assign(usage2, event.message?.usage ?? {});
|
|
403
|
+
break;
|
|
404
|
+
case "content_block_start": {
|
|
405
|
+
const b = event.content_block;
|
|
406
|
+
blocks.set(event.index ?? blocks.size, {
|
|
407
|
+
type: b.type,
|
|
408
|
+
...b.id ? { id: b.id } : {},
|
|
409
|
+
...b.name ? { name: b.name } : {},
|
|
410
|
+
text: b.text ?? b.thinking ?? "",
|
|
411
|
+
json: "",
|
|
412
|
+
signature: b.signature ?? "",
|
|
413
|
+
...b.data ? { data: b.data } : {}
|
|
414
|
+
});
|
|
415
|
+
if (b.type === "text" && b.text) onText?.(b.text);
|
|
416
|
+
break;
|
|
417
|
+
}
|
|
418
|
+
case "content_block_delta": {
|
|
419
|
+
const block = blocks.get(event.index ?? 0);
|
|
420
|
+
const d = event.delta ?? {};
|
|
421
|
+
if (!block) break;
|
|
422
|
+
if (d.type === "text_delta" && d.text) {
|
|
423
|
+
block.text += d.text;
|
|
424
|
+
onText?.(d.text);
|
|
425
|
+
} else if (d.type === "input_json_delta" && d.partial_json) block.json += d.partial_json;
|
|
426
|
+
else if (d.type === "thinking_delta" && d.thinking) block.text += d.thinking;
|
|
427
|
+
else if (d.type === "signature_delta" && d.signature) block.signature += d.signature;
|
|
428
|
+
break;
|
|
429
|
+
}
|
|
430
|
+
case "message_delta":
|
|
431
|
+
if (event.delta?.stop_reason) stopReason = event.delta.stop_reason;
|
|
432
|
+
Object.assign(usage2, Object.fromEntries(Object.entries(event.usage ?? {}).filter(([, v]) => v !== null && v !== void 0)));
|
|
433
|
+
break;
|
|
434
|
+
case "error":
|
|
435
|
+
failure = event.error?.message ?? event.error?.type ?? "stream error";
|
|
436
|
+
break;
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
if (failure) throw new ChatModelError(id, failure);
|
|
440
|
+
const ordered = [...blocks.entries()].sort(([a], [b]) => a - b).map(([, b]) => b);
|
|
441
|
+
const out = ordered.flatMap((b) => {
|
|
442
|
+
if (b.type === "text") return [{ type: "text", text: b.text }];
|
|
443
|
+
if (b.type === "tool_use") return [{ type: "tool_use", id: b.id ?? "", name: b.name ?? "", input: parseToolArguments(b.json) }];
|
|
444
|
+
if (b.type === "thinking") return [{ type: "thinking", thinking: b.text, signature: b.signature }];
|
|
445
|
+
if (b.type === "redacted_thinking") return [{ type: "redacted_thinking", data: b.data ?? "" }];
|
|
446
|
+
return [];
|
|
447
|
+
});
|
|
448
|
+
return fromBlocks(out, stopReason, model, Object.keys(usage2).length ? usage2 : void 0);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// src/gemini.ts
|
|
452
|
+
var UNSIGNED = "skip_thought_signature_validator";
|
|
453
|
+
var metaOf = (call) => call.meta?.gemini ?? {};
|
|
454
|
+
function responseObject(content) {
|
|
455
|
+
try {
|
|
456
|
+
const value = JSON.parse(content);
|
|
457
|
+
if (value && typeof value === "object" && !Array.isArray(value)) return value;
|
|
458
|
+
return { result: value };
|
|
459
|
+
} catch {
|
|
460
|
+
return { result: content };
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
function toGeminiContents(messages) {
|
|
464
|
+
const system = [];
|
|
465
|
+
const contents = [];
|
|
466
|
+
const callIds = /* @__PURE__ */ new Map();
|
|
467
|
+
const push = (role, parts) => {
|
|
468
|
+
if (!parts.length) return;
|
|
469
|
+
const last = contents[contents.length - 1];
|
|
470
|
+
if (last && last.role === role) last.parts.push(...parts);
|
|
471
|
+
else contents.push({ role, parts });
|
|
472
|
+
};
|
|
473
|
+
for (const m of messages) {
|
|
474
|
+
if (m.role === "system") system.push(m.content);
|
|
475
|
+
else if (m.role === "user") push("user", m.content ? [{ text: m.content }] : []);
|
|
476
|
+
else if (m.role === "tool") {
|
|
477
|
+
const id = callIds.get(m.toolCallId);
|
|
478
|
+
push("user", [{ functionResponse: { ...id ? { id } : {}, name: m.name, response: responseObject(m.content) } }]);
|
|
479
|
+
} else {
|
|
480
|
+
const calls = m.toolCalls ?? [];
|
|
481
|
+
const signed = calls.some((c) => metaOf(c).signature);
|
|
482
|
+
push("model", [
|
|
483
|
+
...m.content ? [{ text: m.content }] : [],
|
|
484
|
+
...calls.map((c, i) => {
|
|
485
|
+
const meta = metaOf(c);
|
|
486
|
+
callIds.set(c.id, meta.id);
|
|
487
|
+
const signature = meta.signature ?? (i === 0 && !signed ? UNSIGNED : void 0);
|
|
488
|
+
return { functionCall: { ...meta.id ? { id: meta.id } : {}, name: c.name, args: c.arguments }, ...signature ? { thoughtSignature: signature } : {} };
|
|
489
|
+
})
|
|
490
|
+
]);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
return { system: system.join("\n\n"), contents };
|
|
494
|
+
}
|
|
495
|
+
var FINISH3 = {
|
|
496
|
+
STOP: "stop",
|
|
497
|
+
MAX_TOKENS: "length",
|
|
498
|
+
SAFETY: "content_filter",
|
|
499
|
+
RECITATION: "content_filter",
|
|
500
|
+
BLOCKLIST: "content_filter",
|
|
501
|
+
PROHIBITED_CONTENT: "content_filter",
|
|
502
|
+
SPII: "content_filter",
|
|
503
|
+
IMAGE_SAFETY: "content_filter"
|
|
504
|
+
};
|
|
505
|
+
function collector(onText) {
|
|
506
|
+
let content = "";
|
|
507
|
+
let finishReason;
|
|
508
|
+
let blocked = false;
|
|
509
|
+
let model;
|
|
510
|
+
let usage2;
|
|
511
|
+
const toolCalls = [];
|
|
512
|
+
const batch = Math.random().toString(36).slice(2, 10);
|
|
513
|
+
return {
|
|
514
|
+
add(json2) {
|
|
515
|
+
if (json2.modelVersion) model = json2.modelVersion;
|
|
516
|
+
if (json2.promptFeedback?.blockReason) blocked = true;
|
|
517
|
+
const u = json2.usageMetadata;
|
|
518
|
+
if (u) {
|
|
519
|
+
usage2 = {
|
|
520
|
+
...u.promptTokenCount !== void 0 ? { inputTokens: u.promptTokenCount } : {},
|
|
521
|
+
...u.candidatesTokenCount !== void 0 || u.thoughtsTokenCount !== void 0 ? { outputTokens: (u.candidatesTokenCount ?? 0) + (u.thoughtsTokenCount ?? 0) } : {}
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
const candidate = json2.candidates?.[0];
|
|
525
|
+
if (candidate?.finishReason) finishReason = candidate.finishReason;
|
|
526
|
+
for (const part of candidate?.content?.parts ?? []) {
|
|
527
|
+
if (part.functionCall) {
|
|
528
|
+
const gemini = { ...part.functionCall.id ? { id: part.functionCall.id } : {}, ...part.thoughtSignature ? { signature: part.thoughtSignature } : {} };
|
|
529
|
+
toolCalls.push({
|
|
530
|
+
// Gemini often gives no id; ours must still be unique across the conversation.
|
|
531
|
+
id: part.functionCall.id ?? `call_${batch}_${toolCalls.length}`,
|
|
532
|
+
name: part.functionCall.name,
|
|
533
|
+
arguments: part.functionCall.args ?? {},
|
|
534
|
+
...Object.keys(gemini).length ? { meta: { gemini } } : {}
|
|
535
|
+
});
|
|
536
|
+
} else if (part.text && !part.thought) {
|
|
537
|
+
content += part.text;
|
|
538
|
+
onText?.(part.text);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
},
|
|
542
|
+
result() {
|
|
543
|
+
const finish = blocked ? "content_filter" : toolCalls.length && (finishReason === "STOP" || finishReason === void 0) ? "tool_calls" : FINISH3[finishReason ?? ""] ?? "other";
|
|
544
|
+
return { content, toolCalls, finish, ...model ? { model } : {}, ...usage2 ? { usage: usage2 } : {} };
|
|
545
|
+
}
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
var bareModel = (model) => model.replace(/^models\//, "");
|
|
549
|
+
function geminiChatModel(options) {
|
|
550
|
+
const base = trimSlash(options.baseUrl ?? "https://generativelanguage.googleapis.com/v1beta");
|
|
551
|
+
const id = options.id ?? "gemini";
|
|
552
|
+
const doFetch = options.fetch ?? defaultFetch();
|
|
553
|
+
const stream = options.stream ?? true;
|
|
554
|
+
const models = modelList(bareModel(options.model), options.models?.map((m) => ({ ...m, id: bareModel(m.id) })));
|
|
555
|
+
const headers = () => ({
|
|
556
|
+
"Content-Type": "application/json",
|
|
557
|
+
...options.apiKey ? { "x-goog-api-key": options.apiKey } : {},
|
|
558
|
+
...options.headers
|
|
559
|
+
});
|
|
560
|
+
return defineChatModel({
|
|
561
|
+
id,
|
|
562
|
+
label: options.label ?? "Google Gemini",
|
|
563
|
+
models,
|
|
564
|
+
async listModels(opts = {}) {
|
|
565
|
+
const json2 = await getJson(id, doFetch, `${base}/models?pageSize=1000`, headers(), opts.signal);
|
|
566
|
+
return (json2.models ?? []).filter((m) => !m.supportedGenerationMethods || m.supportedGenerationMethods.includes("generateContent")).map((m) => ({ id: bareModel(m.name), ...m.displayName ? { label: m.displayName } : {} }));
|
|
567
|
+
},
|
|
568
|
+
async complete(request, opts = {}) {
|
|
569
|
+
const { system, contents } = toGeminiContents(request.messages);
|
|
570
|
+
const params = { ...options.params, ...request.params };
|
|
571
|
+
const generationConfig = {
|
|
572
|
+
...request.maxOutputTokens ? { maxOutputTokens: request.maxOutputTokens } : {},
|
|
573
|
+
...options.params?.generationConfig,
|
|
574
|
+
...request.params?.generationConfig
|
|
575
|
+
};
|
|
576
|
+
const body = {
|
|
577
|
+
...params,
|
|
578
|
+
contents,
|
|
579
|
+
...system ? { systemInstruction: { parts: [{ text: system }] } } : {},
|
|
580
|
+
...request.tools?.length ? { tools: [{ functionDeclarations: request.tools.map((t) => ({ name: t.name, description: t.description, parametersJsonSchema: t.inputSchema })) }] } : {},
|
|
581
|
+
...Object.keys(generationConfig).length ? { generationConfig } : {}
|
|
582
|
+
};
|
|
583
|
+
const model = bareModel(request.model ?? options.model);
|
|
584
|
+
const url = `${base}/models/${encodeURIComponent(model)}:${stream ? "streamGenerateContent?alt=sse" : "generateContent"}`;
|
|
585
|
+
const res = await sendWithRetry(
|
|
586
|
+
id,
|
|
587
|
+
doFetch,
|
|
588
|
+
url,
|
|
589
|
+
{ method: "POST", headers: headers(), body: JSON.stringify(body) },
|
|
590
|
+
{ ...options.maxRetries !== void 0 ? { maxRetries: options.maxRetries } : {}, ...opts.signal ? { signal: opts.signal } : {} }
|
|
591
|
+
);
|
|
592
|
+
const collect = collector(opts.onText);
|
|
593
|
+
if (!stream || !isEventStream(res)) collect.add(await res.json());
|
|
594
|
+
else {
|
|
595
|
+
await readSse(res.body, (data) => {
|
|
596
|
+
try {
|
|
597
|
+
collect.add(JSON.parse(data));
|
|
598
|
+
} catch {
|
|
599
|
+
}
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
const out = collect.result();
|
|
603
|
+
out.model ??= model;
|
|
604
|
+
return out;
|
|
605
|
+
}
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// src/providers.ts
|
|
610
|
+
var AI_PROVIDERS = [
|
|
611
|
+
{
|
|
612
|
+
id: "openai",
|
|
613
|
+
label: "OpenAI",
|
|
614
|
+
api: "openai",
|
|
615
|
+
baseUrl: "https://api.openai.com/v1",
|
|
616
|
+
needsKey: true,
|
|
617
|
+
keyUrl: "https://platform.openai.com/api-keys",
|
|
618
|
+
env: "OPENAI",
|
|
619
|
+
defaultModel: "gpt-5.4-mini",
|
|
620
|
+
models: [{ id: "gpt-5.4-mini" }, { id: "gpt-6-luna" }, { id: "gpt-6.1-sol" }],
|
|
621
|
+
browser: true
|
|
622
|
+
},
|
|
623
|
+
{
|
|
624
|
+
id: "anthropic",
|
|
625
|
+
label: "Anthropic",
|
|
626
|
+
api: "anthropic",
|
|
627
|
+
baseUrl: "https://api.anthropic.com",
|
|
628
|
+
needsKey: true,
|
|
629
|
+
keyUrl: "https://console.anthropic.com/settings/keys",
|
|
630
|
+
env: "ANTHROPIC",
|
|
631
|
+
defaultModel: "claude-sonnet-5-5",
|
|
632
|
+
models: [
|
|
633
|
+
{ id: "claude-sonnet-5-5", label: "Claude Sonnet 5.5" },
|
|
634
|
+
{ id: "claude-haiku-5-5", label: "Claude Haiku 5.5" },
|
|
635
|
+
{ id: "claude-opus-5-5", label: "Claude Opus 5.5" }
|
|
636
|
+
],
|
|
637
|
+
browser: true
|
|
638
|
+
},
|
|
639
|
+
{
|
|
640
|
+
id: "gemini",
|
|
641
|
+
label: "Google Gemini",
|
|
642
|
+
api: "gemini",
|
|
643
|
+
baseUrl: "https://generativelanguage.googleapis.com/v1beta",
|
|
644
|
+
needsKey: true,
|
|
645
|
+
keyUrl: "https://aistudio.google.com/apikey",
|
|
646
|
+
env: "GEMINI",
|
|
647
|
+
defaultModel: "gemini-3.8-flash",
|
|
648
|
+
models: [
|
|
649
|
+
{ id: "gemini-3.8-flash", label: "Gemini 3.8 Flash" },
|
|
650
|
+
{ id: "gemini-3.5-flash-lite", label: "Gemini 3.5 Flash-Lite" },
|
|
651
|
+
{ id: "gemini-3.1-pro-preview", label: "Gemini 3.1 Pro (preview)" }
|
|
652
|
+
],
|
|
653
|
+
browser: true
|
|
654
|
+
},
|
|
655
|
+
{
|
|
656
|
+
id: "openrouter",
|
|
657
|
+
label: "OpenRouter",
|
|
658
|
+
api: "openai",
|
|
659
|
+
baseUrl: "https://openrouter.ai/api/v1",
|
|
660
|
+
needsKey: true,
|
|
661
|
+
keyUrl: "https://openrouter.ai/keys",
|
|
662
|
+
env: "OPENROUTER",
|
|
663
|
+
defaultModel: "openrouter/auto",
|
|
664
|
+
models: [{ id: "openrouter/auto", label: "Auto (OpenRouter picks)" }],
|
|
665
|
+
browser: true,
|
|
666
|
+
maxTokensField: "max_tokens"
|
|
667
|
+
},
|
|
668
|
+
{ id: "groq", label: "Groq", api: "openai", baseUrl: "https://api.groq.com/openai/v1", needsKey: true, keyUrl: "https://console.groq.com/keys", env: "GROQ", maxTokensField: "max_tokens" },
|
|
669
|
+
{
|
|
670
|
+
id: "mistral",
|
|
671
|
+
label: "Mistral",
|
|
672
|
+
api: "openai",
|
|
673
|
+
baseUrl: "https://api.mistral.ai/v1",
|
|
674
|
+
needsKey: true,
|
|
675
|
+
keyUrl: "https://console.mistral.ai/api-keys",
|
|
676
|
+
env: "MISTRAL",
|
|
677
|
+
defaultModel: "mistral-medium-latest",
|
|
678
|
+
maxTokensField: "max_tokens",
|
|
679
|
+
streamUsage: false
|
|
680
|
+
},
|
|
681
|
+
{
|
|
682
|
+
id: "deepseek",
|
|
683
|
+
label: "DeepSeek",
|
|
684
|
+
api: "openai",
|
|
685
|
+
baseUrl: "https://api.deepseek.com/v1",
|
|
686
|
+
needsKey: true,
|
|
687
|
+
keyUrl: "https://platform.deepseek.com/api_keys",
|
|
688
|
+
env: "DEEPSEEK",
|
|
689
|
+
defaultModel: "deepseek-chat",
|
|
690
|
+
maxTokensField: "max_tokens"
|
|
691
|
+
},
|
|
692
|
+
{ id: "xai", label: "xAI", api: "openai", baseUrl: "https://api.x.ai/v1", needsKey: true, keyUrl: "https://console.x.ai", env: "XAI", maxTokensField: "max_tokens" },
|
|
693
|
+
{
|
|
694
|
+
id: "together",
|
|
695
|
+
label: "Together AI",
|
|
696
|
+
api: "openai",
|
|
697
|
+
baseUrl: "https://api.together.xyz/v1",
|
|
698
|
+
needsKey: true,
|
|
699
|
+
keyUrl: "https://api.together.ai/settings/api-keys",
|
|
700
|
+
env: "TOGETHER",
|
|
701
|
+
maxTokensField: "max_tokens"
|
|
702
|
+
},
|
|
703
|
+
{ id: "ollama", label: "Ollama (local)", api: "openai", baseUrl: "http://localhost:11434/v1", needsKey: false, local: true, env: "OLLAMA", maxTokensField: "max_tokens" },
|
|
704
|
+
{ id: "lmstudio", label: "LM Studio (local)", api: "openai", baseUrl: "http://localhost:1234/v1", needsKey: false, local: true, env: "LMSTUDIO", maxTokensField: "max_tokens" },
|
|
705
|
+
{ id: "openai-compatible", label: "OpenAI-compatible", api: "openai", needsKey: false, env: "OPENAI_COMPATIBLE", maxTokensField: "max_tokens" }
|
|
706
|
+
];
|
|
707
|
+
function aiProvider(id) {
|
|
708
|
+
return AI_PROVIDERS.find((p) => p.id === id);
|
|
709
|
+
}
|
|
710
|
+
function createChatModel(options) {
|
|
711
|
+
const info = aiProvider(options.provider);
|
|
712
|
+
if (!info) throw new Error(`Unknown AI provider "${options.provider}". Known: ${AI_PROVIDERS.map((p) => p.id).join(", ")}`);
|
|
713
|
+
const model = options.model || info.defaultModel;
|
|
714
|
+
if (!model) throw new ChatModelError(info.id, `pick a model (${info.label} has no default; set ${info.env}_MODEL or choose one)`, 400);
|
|
715
|
+
if (info.needsKey && !options.apiKey) throw new ChatModelError(info.id, `needs an API key${info.keyUrl ? ` (create one at ${info.keyUrl})` : ""}`, 401);
|
|
716
|
+
const baseUrl = options.baseUrl || info.baseUrl;
|
|
717
|
+
if (!baseUrl) throw new ChatModelError(info.id, "needs a base URL (the server's /v1 address)", 400);
|
|
718
|
+
const common = {
|
|
719
|
+
model,
|
|
720
|
+
models: options.models ?? info.models ?? [],
|
|
721
|
+
baseUrl,
|
|
722
|
+
id: options.id ?? info.id,
|
|
723
|
+
label: options.label ?? info.label,
|
|
724
|
+
...options.apiKey ? { apiKey: options.apiKey } : {},
|
|
725
|
+
...options.headers ? { headers: options.headers } : {},
|
|
726
|
+
...options.params ? { params: options.params } : {},
|
|
727
|
+
...options.stream !== void 0 ? { stream: options.stream } : {},
|
|
728
|
+
...options.maxRetries !== void 0 ? { maxRetries: options.maxRetries } : {},
|
|
729
|
+
...options.fetch ? { fetch: options.fetch } : {}
|
|
730
|
+
};
|
|
731
|
+
switch (info.api) {
|
|
732
|
+
case "anthropic":
|
|
733
|
+
return anthropicChatModel({ ...common, ...options.browser ? { browser: true } : {} });
|
|
734
|
+
case "gemini":
|
|
735
|
+
return geminiChatModel(common);
|
|
736
|
+
case "openai":
|
|
737
|
+
return openAIChatModel({
|
|
738
|
+
...common,
|
|
739
|
+
...info.maxTokensField ? { maxTokensField: info.maxTokensField } : {},
|
|
740
|
+
...info.streamUsage !== void 0 ? { streamUsage: info.streamUsage } : {}
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
function chatModelsFromEnv(env, options = {}) {
|
|
745
|
+
const out = [];
|
|
746
|
+
const wanted = options.providers ? new Set(options.providers) : null;
|
|
747
|
+
for (const info of AI_PROVIDERS) {
|
|
748
|
+
if (wanted && !wanted.has(info.id)) continue;
|
|
749
|
+
const p = info.env;
|
|
750
|
+
let apiKey = env[`${p}_API_KEY`] || (info.id === "gemini" ? env.GOOGLE_API_KEY : void 0);
|
|
751
|
+
let baseUrl = env[`${p}_BASE_URL`];
|
|
752
|
+
let label;
|
|
753
|
+
if (info.id === "openai" && env.OPENAI_BASE_URL) continue;
|
|
754
|
+
if (info.id === "openai-compatible" && !baseUrl && env.OPENAI_BASE_URL) {
|
|
755
|
+
baseUrl = env.OPENAI_BASE_URL;
|
|
756
|
+
apiKey ||= env.OPENAI_API_KEY;
|
|
757
|
+
label = env.OPENAI_LABEL || void 0;
|
|
758
|
+
}
|
|
759
|
+
const model = env[`${p}_MODEL`] || (info.id === "openai-compatible" ? env.OPENAI_MODEL : void 0);
|
|
760
|
+
const enabled = info.needsKey ? !!apiKey : info.local ? !!(model || baseUrl) : !!baseUrl;
|
|
761
|
+
if (!enabled) continue;
|
|
762
|
+
const models = (env[`${p}_MODELS`] || (info.id === "openai-compatible" && env.OPENAI_BASE_URL ? env.OPENAI_MODELS : "") || "").split(",").map((m) => m.trim()).filter(Boolean).map((m) => ({ id: m }));
|
|
763
|
+
try {
|
|
764
|
+
out.push(
|
|
765
|
+
createChatModel({
|
|
766
|
+
provider: info.id,
|
|
767
|
+
...apiKey ? { apiKey } : {},
|
|
768
|
+
...model ? { model } : {},
|
|
769
|
+
...models.length ? { models } : {},
|
|
770
|
+
...baseUrl ? { baseUrl } : {},
|
|
771
|
+
...label ? { label } : {},
|
|
772
|
+
...options.fetch ? { fetch: options.fetch } : {}
|
|
773
|
+
})
|
|
774
|
+
);
|
|
775
|
+
} catch (err) {
|
|
776
|
+
options.onSkip?.(info.id, err.message);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
return out;
|
|
780
|
+
}
|
|
781
|
+
function splitModelRef(ref, providerIds) {
|
|
782
|
+
const slash = ref.indexOf("/");
|
|
783
|
+
if (slash > 0) {
|
|
784
|
+
const provider = ref.slice(0, slash);
|
|
785
|
+
for (const id of providerIds) if (id === provider) return { provider, model: ref.slice(slash + 1) };
|
|
786
|
+
}
|
|
787
|
+
return { model: ref };
|
|
788
|
+
}
|
|
789
|
+
function routedChatModel(models, options = {}) {
|
|
790
|
+
if (!models.length) throw new Error("routedChatModel needs at least one model");
|
|
791
|
+
const byId = new Map(models.map((m) => [m.id, m]));
|
|
792
|
+
if (byId.size !== models.length) throw new Error("routedChatModel: adapter ids must be unique");
|
|
793
|
+
const fallback = options.default && byId.get(options.default) || models[0];
|
|
794
|
+
const ordered = [fallback, ...models.filter((m) => m !== fallback)];
|
|
795
|
+
const prefixed = (m, list) => list.map((e) => ({ id: `${m.id}/${e.id}`, label: `${m.label} \xB7 ${e.label ?? e.id}` }));
|
|
796
|
+
const resolve = (ref) => {
|
|
797
|
+
if (!ref) return { adapter: fallback };
|
|
798
|
+
const { provider, model } = splitModelRef(ref, byId.keys());
|
|
799
|
+
if (provider) return { adapter: byId.get(provider), model };
|
|
800
|
+
const lister = ordered.find((m) => m.models?.some((e) => e.id === ref));
|
|
801
|
+
return { adapter: lister ?? fallback, model: ref };
|
|
802
|
+
};
|
|
803
|
+
return defineChatModel({
|
|
804
|
+
id: options.id ?? "router",
|
|
805
|
+
label: options.label ?? (models.length === 1 ? fallback.label : "AI models"),
|
|
806
|
+
models: ordered.flatMap((m) => prefixed(m, m.models ?? [])),
|
|
807
|
+
providers: ordered.map((m) => ({ id: m.id, label: m.label })),
|
|
808
|
+
async complete(request, opts) {
|
|
809
|
+
const { adapter, model } = resolve(request.model);
|
|
810
|
+
const { model: _drop, ...rest } = request;
|
|
811
|
+
return adapter.complete(model ? { ...rest, model } : rest, opts);
|
|
812
|
+
},
|
|
813
|
+
async listModels(opts) {
|
|
814
|
+
const lists = await Promise.allSettled(ordered.map(async (m) => prefixed(m, m.listModels ? await m.listModels(opts) : m.models ?? [])));
|
|
815
|
+
return lists.flatMap((r, i) => r.status === "fulfilled" ? r.value : prefixed(ordered[i], ordered[i].models ?? []));
|
|
816
|
+
}
|
|
817
|
+
});
|
|
818
|
+
}
|
|
819
|
+
|
|
236
820
|
// src/http.ts
|
|
821
|
+
var USER_KEY_HEADERS = { provider: "x-ai-provider", key: "x-ai-key", baseUrl: "x-ai-base-url" };
|
|
822
|
+
var hostedProviders = () => AI_PROVIDERS.filter((p) => !p.local && p.baseUrl).map((p) => p.id);
|
|
237
823
|
function createChatHandler(model, options = {}) {
|
|
238
824
|
const base = (options.basePath ?? "").replace(/\/+$/, "");
|
|
239
825
|
const maxBody = options.maxBodyBytes ?? 2e6;
|
|
826
|
+
const byok = options.userKeys ? options.userKeys === true ? {} : options.userKeys : null;
|
|
827
|
+
if (!model && !byok) throw new Error("createChatHandler needs a model, or userKeys so users can bring their own");
|
|
828
|
+
const allowed = new Set(byok?.providers ?? hostedProviders());
|
|
829
|
+
if (byok?.allowBaseUrl) allowed.add("openai-compatible");
|
|
830
|
+
const userKeyInfo = byok ? {
|
|
831
|
+
providers: [...allowed].flatMap((id) => {
|
|
832
|
+
const p = aiProvider(id);
|
|
833
|
+
return p ? [{ id: p.id, label: p.label, needsKey: p.needsKey, ...p.keyUrl ? { keyUrl: p.keyUrl } : {}, ...p.defaultModel ? { defaultModel: p.defaultModel } : {}, ...p.models ? { models: p.models } : {} }] : [];
|
|
834
|
+
}),
|
|
835
|
+
allowBaseUrl: !!byok.allowBaseUrl,
|
|
836
|
+
required: !!byok.required
|
|
837
|
+
} : void 0;
|
|
838
|
+
const userModel = (request, modelRef) => {
|
|
839
|
+
if (!byok) return null;
|
|
840
|
+
const provider = request.headers.get(USER_KEY_HEADERS.provider)?.trim();
|
|
841
|
+
if (!provider) return null;
|
|
842
|
+
if (!allowed.has(provider)) return json(403, { error: `bringing your own key isn't enabled for "${provider}" here` });
|
|
843
|
+
const apiKey = request.headers.get(USER_KEY_HEADERS.key)?.trim() || void 0;
|
|
844
|
+
const baseUrl = request.headers.get(USER_KEY_HEADERS.baseUrl)?.trim() || void 0;
|
|
845
|
+
if (baseUrl && !byok.allowBaseUrl) return json(403, { error: "custom API addresses aren't allowed here" });
|
|
846
|
+
const ref = modelRef ? splitModelRef(modelRef, [provider]) : void 0;
|
|
847
|
+
const config = {
|
|
848
|
+
provider,
|
|
849
|
+
...apiKey ? { apiKey } : {},
|
|
850
|
+
// Listing models needs no model; the catalog default (or a placeholder) is fine.
|
|
851
|
+
model: ref?.model || aiProvider(provider)?.defaultModel || "default",
|
|
852
|
+
...baseUrl ? { baseUrl } : {},
|
|
853
|
+
...byok.fetch ? { fetch: byok.fetch } : {}
|
|
854
|
+
};
|
|
855
|
+
try {
|
|
856
|
+
return byok.create ? byok.create(config, request) : createChatModel(config);
|
|
857
|
+
} catch (err) {
|
|
858
|
+
const status = err instanceof ChatModelError && err.status ? err.status : 400;
|
|
859
|
+
return json(status, { error: err.message });
|
|
860
|
+
}
|
|
861
|
+
};
|
|
862
|
+
const noModel = () => json(401, { error: "Add your own API key to use AI here.", needsKey: true });
|
|
240
863
|
return async function handle(request) {
|
|
241
864
|
const path = new URL(request.url).pathname;
|
|
242
865
|
if (base && path !== base && !path.startsWith(`${base}/`)) return null;
|
|
@@ -244,7 +867,27 @@ function createChatHandler(model, options = {}) {
|
|
|
244
867
|
const refused = await options.authorize?.(request);
|
|
245
868
|
if (refused) return refused;
|
|
246
869
|
if (rest === "" && request.method === "GET") {
|
|
247
|
-
return json(200, {
|
|
870
|
+
return json(200, {
|
|
871
|
+
id: model?.id ?? "byok",
|
|
872
|
+
label: model?.label ?? "Your API key",
|
|
873
|
+
models: model?.models ?? [],
|
|
874
|
+
...model?.providers ? { providers: model.providers } : {},
|
|
875
|
+
...userKeyInfo ? { userKeys: userKeyInfo } : {},
|
|
876
|
+
...!model || byok?.required ? { needsKey: true } : {}
|
|
877
|
+
});
|
|
878
|
+
}
|
|
879
|
+
if (rest === "/models" && request.method === "GET") {
|
|
880
|
+
const mine2 = userModel(request, void 0);
|
|
881
|
+
if (mine2 instanceof Response) return mine2;
|
|
882
|
+
const target2 = mine2 ?? (byok?.required ? null : model);
|
|
883
|
+
if (!target2) return noModel();
|
|
884
|
+
try {
|
|
885
|
+
const models = target2.listModels ? await target2.listModels({ signal: request.signal }) : target2.models ?? [];
|
|
886
|
+
return json(200, { models });
|
|
887
|
+
} catch (err) {
|
|
888
|
+
const status = err?.status;
|
|
889
|
+
return json(status && status >= 400 && status < 600 ? status : 502, { error: err?.message ?? String(err) });
|
|
890
|
+
}
|
|
248
891
|
}
|
|
249
892
|
if (rest !== "/chat" || request.method !== "POST") return json(404, { error: "not found" });
|
|
250
893
|
const text = await request.text();
|
|
@@ -257,13 +900,18 @@ function createChatHandler(model, options = {}) {
|
|
|
257
900
|
}
|
|
258
901
|
if (!chat || !Array.isArray(chat.messages)) return json(400, { error: "request.messages is required" });
|
|
259
902
|
if (options.prepare) chat = options.prepare(chat);
|
|
903
|
+
const mine = userModel(request, chat.model);
|
|
904
|
+
if (mine instanceof Response) return mine;
|
|
905
|
+
const target = mine ?? (byok?.required ? null : model);
|
|
906
|
+
if (!target) return noModel();
|
|
907
|
+
if (mine && chat.model) chat = { ...chat, model: splitModelRef(chat.model, [request.headers.get(USER_KEY_HEADERS.provider).trim(), mine.id]).model };
|
|
260
908
|
const encoder = new TextEncoder();
|
|
261
909
|
const body = new ReadableStream({
|
|
262
910
|
async start(controller) {
|
|
263
911
|
const send = (value) => controller.enqueue(encoder.encode(`${JSON.stringify(value)}
|
|
264
912
|
`));
|
|
265
913
|
try {
|
|
266
|
-
const response = await
|
|
914
|
+
const response = await target.complete(chat, { signal: request.signal, onText: (delta) => send({ type: "text", delta }) });
|
|
267
915
|
send({ type: "done", response });
|
|
268
916
|
} catch (err) {
|
|
269
917
|
const status = err?.status;
|
|
@@ -277,6 +925,14 @@ function createChatHandler(model, options = {}) {
|
|
|
277
925
|
};
|
|
278
926
|
}
|
|
279
927
|
var json = (status, body) => new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
|
|
928
|
+
function userKeyHeaders(credentials) {
|
|
929
|
+
if (!credentials?.provider) return {};
|
|
930
|
+
return {
|
|
931
|
+
[USER_KEY_HEADERS.provider]: credentials.provider,
|
|
932
|
+
...credentials.apiKey ? { [USER_KEY_HEADERS.key]: credentials.apiKey } : {},
|
|
933
|
+
...credentials.baseUrl ? { [USER_KEY_HEADERS.baseUrl]: credentials.baseUrl } : {}
|
|
934
|
+
};
|
|
935
|
+
}
|
|
280
936
|
async function remoteChatModel(endpoint, options = {}) {
|
|
281
937
|
const doFetch = options.fetch ?? defaultFetch();
|
|
282
938
|
const root = endpoint.replace(/\/+$/, "");
|
|
@@ -286,15 +942,24 @@ async function remoteChatModel(endpoint, options = {}) {
|
|
|
286
942
|
if (!res.ok) throw new ChatModelError("remote", `HTTP ${res.status}`, res.status);
|
|
287
943
|
info = await res.json();
|
|
288
944
|
}
|
|
289
|
-
const { id, label, models } = info;
|
|
945
|
+
const { id, label, models, providers } = info;
|
|
946
|
+
const headers = (extra) => ({ ...extra, ...options.headers, ...userKeyHeaders(options.credentials?.()) });
|
|
290
947
|
return {
|
|
291
948
|
id,
|
|
292
949
|
label,
|
|
950
|
+
info,
|
|
293
951
|
...models ? { models } : {},
|
|
952
|
+
...providers ? { providers } : {},
|
|
953
|
+
async listModels(opts = {}) {
|
|
954
|
+
const res = await doFetch(`${root}/models`, { headers: headers({ Accept: "application/json" }), ...opts.signal ? { signal: opts.signal } : {} });
|
|
955
|
+
const body = await res.json().catch(() => null);
|
|
956
|
+
if (!res.ok) throw new ChatModelError(id, body?.error ?? `HTTP ${res.status}`, res.status);
|
|
957
|
+
return body?.models ?? [];
|
|
958
|
+
},
|
|
294
959
|
async complete(request, opts = {}) {
|
|
295
960
|
const res = await doFetch(`${root}/chat`, {
|
|
296
961
|
method: "POST",
|
|
297
|
-
headers: { "Content-Type": "application/json", Accept: "application/x-ndjson"
|
|
962
|
+
headers: headers({ "Content-Type": "application/json", Accept: "application/x-ndjson" }),
|
|
298
963
|
body: JSON.stringify({ request }),
|
|
299
964
|
...opts.signal ? { signal: opts.signal } : {}
|
|
300
965
|
});
|
|
@@ -498,7 +1163,7 @@ function toolsFromDefinitions(definitions, run, options = {}) {
|
|
|
498
1163
|
return defineTool({
|
|
499
1164
|
name,
|
|
500
1165
|
description: def.function?.description ?? def.description ?? "",
|
|
501
|
-
inputSchema: def.function?.parameters ?? def.input_schema ?? { type: "object", properties: {} },
|
|
1166
|
+
inputSchema: def.function?.parameters ?? def.input_schema ?? def.parametersJsonSchema ?? { type: "object", properties: {} },
|
|
502
1167
|
async run(input, ctx) {
|
|
503
1168
|
const out = await run(name, input, ctx);
|
|
504
1169
|
if (!out.ok) return { ok: false, error: out.error ?? "failed" };
|