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