@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.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/openai.ts
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").replace(/\/+$/, "");
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 = [{ id: options.model }, ...(options.models ?? []).filter((m) => m.id !== options.model)];
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 ? { max_completion_tokens: 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 headers = { "Content-Type": "application/json", ...options.headers };
139
- if (options.apiKey) headers["Authorization"] = `Bearer ${options.apiKey}`;
140
- if (options.organization) headers["OpenAI-Organization"] = options.organization;
141
- if (options.project) headers["OpenAI-Project"] = options.project;
142
- const maxRetries = options.maxRetries ?? 4;
143
- let res;
144
- for (let attempt = 0; ; attempt++) {
145
- res = await doFetch(`${base}/chat/completions`, {
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
- for (; ; ) {
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, { id: model.id, label: model.label, models: model.models ?? [] });
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 model.complete(chat, { signal: request.signal, onText: (delta) => send({ type: "text", delta }) });
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", ...options.headers },
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" };