@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/dist/index.js CHANGED
@@ -21,20 +21,7 @@ function defineChatModel(model) {
21
21
  return model;
22
22
  }
23
23
 
24
- // src/openai.ts
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").replace(/\/+$/, "");
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 = [{ id: options.model }, ...(options.models ?? []).filter((m) => m.id !== options.model)];
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 ? { max_completion_tokens: 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 headers = { "Content-Type": "application/json", ...options.headers };
90
- if (options.apiKey) headers["Authorization"] = `Bearer ${options.apiKey}`;
91
- if (options.organization) headers["OpenAI-Organization"] = options.organization;
92
- if (options.project) headers["OpenAI-Project"] = options.project;
93
- const maxRetries = options.maxRetries ?? 4;
94
- let res;
95
- for (let attempt = 0; ; attempt++) {
96
- res = await doFetch(`${base}/chat/completions`, {
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
- for (; ; ) {
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, { id: model.id, label: model.label, models: model.models ?? [] });
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 model.complete(chat, { signal: request.signal, onText: (delta) => send({ type: "text", delta }) });
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", ...options.headers },
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