thaipass 0.0.0 → 0.1.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
@@ -1,9 +1,1379 @@
1
- // src/index.ts
2
- var name = "thaipass";
3
- var description = "AI SDK provider for AI Pass, using your own session cookie.";
4
- var src_default = { name, description };
5
- export {
6
- src_default as default,
7
- description,
8
- name
1
+ import { APICallError, NoSuchModelError } from "@ai-sdk/provider";
2
+ import { z } from "zod";
3
+ import { createHash } from "node:crypto";
4
+ import { parseJsonEventStream } from "@ai-sdk/provider-utils";
5
+ //#region ../core/src/aipass/models.ts
6
+ /**
7
+ * The chat ids the proxy knew when it was built. The account's catalog on
8
+ * `/loaders/list-models` decides what a chat request may name. This list
9
+ * stands in when the catalog cannot be read, seeds the docs, and feeds the
10
+ * drift warning. A media id needs per-model knowledge the catalog does not
11
+ * carry, so the media lists below stay the gate for their routes.
12
+ */
13
+ const CHAT_MODELS = [
14
+ "gpt-5.6-terra",
15
+ "gpt-5.6-sol",
16
+ "claude-opus-5@azure",
17
+ "claude-sonnet-5@default",
18
+ "gemini-3.1-pro-preview",
19
+ "gemini-3.7-flash",
20
+ "gemini-3.1-flash-lite",
21
+ "glm-5.2",
22
+ "grok-4.3",
23
+ "DeepSeek-V3.2",
24
+ "minimax-m2-maas",
25
+ "qwen3-next-80b-a3b-instruct-maas",
26
+ "Kimi-K2.7-Code",
27
+ "Mistral-Large-3",
28
+ "Mistral-Medium-3",
29
+ "Llama-4-Scout-17B-16E-Instruct-1",
30
+ "Llama-4-Maverick-17B-128E-Instruct-FP8-1",
31
+ "pathumma-thaillm-8b",
32
+ "openai-deep-research",
33
+ "openthai2.0-legal@jts",
34
+ "sonar",
35
+ "sonar-reasoning-pro",
36
+ "sonar-deep-research"
37
+ ];
38
+ /**
39
+ * AI Pass has no model category on `/loaders/list-models`; the web UI builds
40
+ * its tabs client-side, so the split lives here. Video is its own list
41
+ * because it is a polled job rather than a stream.
42
+ */
43
+ const IMAGE_MODELS = [
44
+ "gpt-image-2",
45
+ "gemini-3-pro-image",
46
+ "gemini-2.5-flash-image",
47
+ "seedream-4.0",
48
+ "seedream-5.0-lite"
49
+ ];
50
+ const VIDEO_MODELS = [
51
+ "seedance-2.0",
52
+ "seedance-2.0-fast",
53
+ "seedance-2.0-mini",
54
+ "veo-3.1-fast-generate-001"
55
+ ];
56
+ const MUSIC_MODELS = ["lyria-3-pro-preview", "lyria-3-clip-preview"];
57
+ const MEDIA_MODELS = [
58
+ ...IMAGE_MODELS,
59
+ ...VIDEO_MODELS,
60
+ ...MUSIC_MODELS
61
+ ];
62
+ z.enum(IMAGE_MODELS, { error: "unknown image model, see GET /v1/models" });
63
+ z.enum(VIDEO_MODELS, { error: "unknown video model, see GET /v1/models" });
64
+ z.enum(MUSIC_MODELS, { error: "unknown music model, see GET /v1/models" });
65
+ const KINDS = new Map([
66
+ ...IMAGE_MODELS.map((id) => [id, "image"]),
67
+ ...VIDEO_MODELS.map((id) => [id, "video"]),
68
+ ...MUSIC_MODELS.map((id) => [id, "music"])
69
+ ]);
70
+ const kindOf = (modelId) => KINDS.get(modelId) ?? "chat";
71
+ z.string().min(1, { error: "model must not be empty, see GET /v1/models" }).meta({
72
+ description: "A chat model id from GET /v1/models. Ids are case-sensitive and Claude carries a @provider suffix.",
73
+ examples: [...CHAT_MODELS]
74
+ });
75
+ const DEFAULT_MODEL = "gemini-3.1-flash-lite";
76
+ const ANY_MODELS = [...CHAT_MODELS, ...MEDIA_MODELS];
77
+ z.enum(ANY_MODELS, { error: "unknown model, see GET /v1/models" });
78
+ const current = {
79
+ origin: "https://de.aipass.net",
80
+ userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/151.0.0.0 Safari/537.36"
9
81
  };
82
+ const config = current;
83
+ /** Process-wide; the server calls it once at startup, a library consumer when the defaults are wrong. */
84
+ const configure = (changes) => {
85
+ Object.assign(current, changes);
86
+ };
87
+ //#endregion
88
+ //#region ../core/src/lib/cache.ts
89
+ const ttlCache = (ttlMs) => {
90
+ const entries = /* @__PURE__ */ new Map();
91
+ return {
92
+ get: (key) => {
93
+ const now = Date.now();
94
+ for (const [id, entry] of entries) if (entry.expiresAt <= now) entries.delete(id);
95
+ return entries.get(key)?.value;
96
+ },
97
+ set: (key, value) => {
98
+ entries.set(key, {
99
+ expiresAt: Date.now() + ttlMs,
100
+ value
101
+ });
102
+ }
103
+ };
104
+ };
105
+ //#endregion
106
+ //#region ../core/src/aipass/request.ts
107
+ const browserGetHeaders = (cookie, referer) => ({
108
+ accept: "*/*",
109
+ "accept-language": "th-TH,th;q=0.9,en;q=0.8",
110
+ cookie,
111
+ origin: config.origin,
112
+ referer,
113
+ "sec-fetch-dest": "empty",
114
+ "sec-fetch-mode": "cors",
115
+ "sec-fetch-site": "same-origin",
116
+ "user-agent": config.userAgent
117
+ });
118
+ const browserPostHeaders = (cookie, referer, contentType) => ({
119
+ ...browserGetHeaders(cookie, referer),
120
+ "content-type": contentType
121
+ });
122
+ /** Headers for a JSON action made from inside one conversation's page. */
123
+ const conversationJsonHeaders = (cookie, conversationId) => browserPostHeaders(cookie, `${config.origin}/chat/${conversationId}`, "application/json");
124
+ const loadJson = async (path, schema, cookie, signal) => {
125
+ let payload;
126
+ try {
127
+ const response = await fetch(`${config.origin}${path}`, {
128
+ headers: browserGetHeaders(cookie, `${config.origin}/chat`),
129
+ redirect: "manual",
130
+ signal
131
+ });
132
+ if (!response.ok) return null;
133
+ payload = await response.json();
134
+ } catch {
135
+ return null;
136
+ }
137
+ const decoded = schema.safeParse(payload);
138
+ return decoded.success ? decoded.data : null;
139
+ };
140
+ //#endregion
141
+ //#region ../core/src/aipass/catalog.ts
142
+ const CATALOG_PATH = "/loaders/list-models";
143
+ const CACHE_TTL_MS = 3e5;
144
+ const catalogSchema = z.object({ data: z.array(z.object({
145
+ id: z.string(),
146
+ isFreeCredit: z.boolean().optional(),
147
+ ready: z.boolean().optional(),
148
+ thinkingConfig: z.object({ supportedLevels: z.array(z.string()).optional() }).optional()
149
+ })) });
150
+ const cache = ttlCache(CACHE_TTL_MS);
151
+ const knownChat = new Set(CHAT_MODELS);
152
+ const served = /* @__PURE__ */ new Set([...CHAT_MODELS, ...MEDIA_MODELS]);
153
+ const MEDIA_ROUTES = {
154
+ image: "/v1/images/generations",
155
+ music: "/v1/audio/generations",
156
+ video: "/v1/videos"
157
+ };
158
+ /**
159
+ * Why a chat request may not name the model, or null when it may. The
160
+ * account's catalog decides: an id upstream added after the build passes,
161
+ * and one upstream retired does not. When the catalog cannot be read the
162
+ * built-in list stands in rather than refusing everything.
163
+ */
164
+ const chatModelProblem = (model, catalog) => {
165
+ const kind = kindOf(model);
166
+ if (kind !== "chat") return `${model} makes ${kind}, send it to ${MEDIA_ROUTES[kind]}`;
167
+ if (catalog) return catalog.has(model) ? null : `unknown model ${model}, the account's catalog does not list it, see GET /v1/models`;
168
+ return knownChat.has(model) ? null : `unknown model ${model}, see GET /v1/models`;
169
+ };
170
+ const reportDrift = (catalog) => {
171
+ const unknown = [...catalog.keys()].filter((id) => !served.has(id));
172
+ if (unknown.length > 0) config.logger?.warn({
173
+ models: unknown.join(", "),
174
+ msg: "catalog has unserved models"
175
+ });
176
+ };
177
+ const load = async (cookie, signal) => {
178
+ const listed = await loadJson(CATALOG_PATH, catalogSchema, cookie, signal);
179
+ if (!listed) return null;
180
+ return new Map(listed.data.map((entry) => [entry.id, {
181
+ free: entry.isFreeCredit ?? false,
182
+ ready: entry.ready ?? true,
183
+ thinking: entry.thinkingConfig?.supportedLevels ?? null
184
+ }]));
185
+ };
186
+ const fetchCatalog = async (clientId, cookie, signal) => {
187
+ const cached = cache.get(clientId);
188
+ if (cached) return cached;
189
+ const catalog = await load(cookie, signal);
190
+ if (!catalog) return null;
191
+ cache.set(clientId, catalog);
192
+ reportDrift(catalog);
193
+ return catalog;
194
+ };
195
+ //#endregion
196
+ //#region ../core/src/aipass/refusal.ts
197
+ /**
198
+ * The AI Pass edge answers some requests itself with a 403 before the chat
199
+ * backend runs, scoring what the prompt contains rather than its length. A
200
+ * retry of the same body gets the same verdict, so the proxy reports it as a
201
+ * request error and hands back the edge's own body, since the trigger set is
202
+ * undocumented.
203
+ */
204
+ const EDGE_REFUSAL_STATUS = 403;
205
+ const EDGE_REFUSAL_HINT = "; the AI Pass edge refused this before the model ran, on what the prompt contains rather than how long it is — resending the same text will be refused again";
206
+ const isEdgeRefusal = (status) => status === EDGE_REFUSAL_STATUS;
207
+ //#endregion
208
+ //#region ../core/src/aipass/upload.ts
209
+ /**
210
+ * Three calls: `initiate` reserves a place and answers with a signed bucket
211
+ * URL, the bytes go there with a plain PUT and no cookie, and `confirm` tells
212
+ * AI Pass the object landed. Only the storage key travels on the message.
213
+ */
214
+ const INITIATE_PATH = "/actions/upload-file/initiate";
215
+ const CONFIRM_PATH = "/actions/upload-file/confirm";
216
+ /** What the upstream composer accepts. */
217
+ const MAX_ATTACHMENT_BYTES = 20971520;
218
+ const initiateSchema = z.object({
219
+ error: z.string().optional(),
220
+ sizeBytes: z.number().optional(),
221
+ storageKey: z.string(),
222
+ uploadToken: z.string(),
223
+ uploadUrl: z.string()
224
+ });
225
+ const confirmSchema = z.object({
226
+ error: z.string().optional(),
227
+ storageKey: z.string().optional()
228
+ });
229
+ var UploadError = class extends Error {
230
+ name = "UploadError";
231
+ };
232
+ const readJson = async (response, schema, step) => {
233
+ if (!response.ok) {
234
+ const detail = await response.text().catch(() => "");
235
+ throw new UploadError(`upload ${step} returned ${response.status}: ${detail.slice(0, 200)}`);
236
+ }
237
+ const parsed = schema.safeParse(await response.json().catch(() => null));
238
+ if (!parsed.success) throw new UploadError(`upload ${step} answered in an unreadable shape`);
239
+ return parsed.data;
240
+ };
241
+ /** The bucket answers 412 when a retry re-sends bytes that already landed. */
242
+ const PRECONDITION_FAILED = 412;
243
+ const uploadAttachment = async (cookie, conversationId, modelId, attachment, signal) => {
244
+ const { bytes, filename, mediaType } = attachment;
245
+ if (bytes.length > 20971520) throw new UploadError(`${filename} is ${bytes.length} bytes, over the ${MAX_ATTACHMENT_BYTES} byte limit`);
246
+ const initiated = await readJson(await fetch(`${config.origin}${INITIATE_PATH}`, {
247
+ body: JSON.stringify({
248
+ contentFilename: filename,
249
+ contentType: mediaType,
250
+ conversationId,
251
+ filename,
252
+ modelId,
253
+ sizeBytes: bytes.length
254
+ }),
255
+ headers: conversationJsonHeaders(cookie, conversationId),
256
+ method: "POST",
257
+ redirect: "manual",
258
+ signal
259
+ }), initiateSchema, "initiate");
260
+ if (initiated.error) throw new UploadError(`upload initiate refused: ${initiated.error}`);
261
+ const putHeaders = { "content-type": mediaType };
262
+ if (initiated.sizeBytes !== void 0) {
263
+ putHeaders["x-goog-content-length-range"] = `${initiated.sizeBytes},${initiated.sizeBytes}`;
264
+ putHeaders["x-goog-if-generation-match"] = "0";
265
+ }
266
+ const put = await fetch(initiated.uploadUrl, {
267
+ body: new Blob([bytes], { type: mediaType }),
268
+ headers: new Headers(Object.entries(putHeaders)),
269
+ method: "PUT",
270
+ signal
271
+ });
272
+ await put.arrayBuffer().catch(() => /* @__PURE__ */ new ArrayBuffer(0));
273
+ if (!put.ok && put.status !== PRECONDITION_FAILED) throw new UploadError(`upload PUT returned ${put.status}`);
274
+ const confirmed = await readJson(await fetch(`${config.origin}${CONFIRM_PATH}`, {
275
+ body: JSON.stringify({ uploadToken: initiated.uploadToken }),
276
+ headers: conversationJsonHeaders(cookie, conversationId),
277
+ method: "POST",
278
+ redirect: "manual",
279
+ signal
280
+ }), confirmSchema, "confirm");
281
+ if (confirmed.error) throw new UploadError(`upload confirm refused: ${confirmed.error}`);
282
+ return {
283
+ filename,
284
+ mediaType,
285
+ storageKey: confirmed.storageKey ?? initiated.storageKey
286
+ };
287
+ };
288
+ //#endregion
289
+ //#region ../core/src/aipass/client.ts
290
+ const CONVERSATION_ID_LENGTH = 16;
291
+ const TITLE_PREVIEW_LENGTH = 400;
292
+ const postAction = (cookie, path, referer, fields, signal) => fetch(`${config.origin}${path}`, {
293
+ body: new URLSearchParams(fields).toString(),
294
+ headers: browserPostHeaders(cookie, referer, "application/x-www-form-urlencoded;charset=UTF-8"),
295
+ method: "POST",
296
+ redirect: "manual",
297
+ signal
298
+ });
299
+ /** Frees the socket for a response nobody is going to read. */
300
+ const drain = async (response) => {
301
+ await response.arrayBuffer().catch(() => /* @__PURE__ */ new ArrayBuffer(0));
302
+ return response.status;
303
+ };
304
+ const createConversation = async (cookie, reqUuid, modelId, text, signal) => {
305
+ const title = text.slice(0, TITLE_PREVIEW_LENGTH) || "hi";
306
+ const response = await postAction(cookie, "/chat.data", `${config.origin}/chat`, {
307
+ clientCreateRequestId: reqUuid,
308
+ folderId: "",
309
+ intent: "create-conversation",
310
+ message: title,
311
+ modelId
312
+ }, signal);
313
+ const conversationId = reqUuid.replaceAll("-", "").slice(0, CONVERSATION_ID_LENGTH);
314
+ /**
315
+ * The title carries the head of the prompt, so the edge can refuse the
316
+ * conversation here before any turn is sent.
317
+ */
318
+ if (isEdgeRefusal(response.status)) return {
319
+ conversationId,
320
+ refusal: response
321
+ };
322
+ await drain(response);
323
+ return {
324
+ conversationId,
325
+ refusal: null
326
+ };
327
+ };
328
+ const deleteConversation = async (cookie, conversationId) => {
329
+ try {
330
+ const status = await drain(await postAction(cookie, "/actions/update-conversation.data", `${config.origin}/chat/${conversationId}`, {
331
+ conversationId,
332
+ intent: "delete"
333
+ }));
334
+ if (status >= 400) config.logger?.warn({
335
+ conversationId,
336
+ msg: "delete rejected",
337
+ status
338
+ });
339
+ } catch (error) {
340
+ config.logger?.warn({
341
+ conversationId,
342
+ err: String(error),
343
+ msg: "delete failed"
344
+ });
345
+ }
346
+ };
347
+ const sendBody = (modelId, messages, options) => {
348
+ const body = {
349
+ messages,
350
+ modelId
351
+ };
352
+ if (options.imageAspectRatio) body.imageAspectRatio = options.imageAspectRatio;
353
+ if (options.thinkingLevel) body.thinkingLevel = options.thinkingLevel;
354
+ return body;
355
+ };
356
+ const lastText = (messages) => messages.at(-1)?.parts.find((part) => part.type === "text")?.text ?? "";
357
+ /** The composer puts attachments above the prompt, so files go first. */
358
+ const withFiles = (messages, files) => messages.map((message, index) => index === messages.length - 1 ? {
359
+ ...message,
360
+ parts: [...files, ...message.parts]
361
+ } : message);
362
+ const sendMessage = async (cookie, modelId, messages, signal, options = {}) => {
363
+ const { conversationId, refusal } = await createConversation(cookie, crypto.randomUUID(), modelId, lastText(messages), signal);
364
+ if (refusal) return {
365
+ conversationId,
366
+ created: false,
367
+ response: refusal
368
+ };
369
+ const uploaded = await Promise.all((options.attachments ?? []).map(async (attachment) => {
370
+ const file = await uploadAttachment(cookie, conversationId, modelId, attachment, signal);
371
+ return {
372
+ filename: file.filename,
373
+ mediaType: file.mediaType,
374
+ storageKey: file.storageKey,
375
+ type: "file",
376
+ url: file.storageKey
377
+ };
378
+ }));
379
+ const sent = uploaded.length > 0 ? withFiles(messages, uploaded) : messages;
380
+ return {
381
+ conversationId,
382
+ created: true,
383
+ response: await fetch(`${config.origin}/actions/send-message/${conversationId}`, {
384
+ body: JSON.stringify(sendBody(modelId, sent, options)),
385
+ headers: conversationJsonHeaders(cookie, conversationId),
386
+ method: "POST",
387
+ redirect: "manual",
388
+ signal
389
+ })
390
+ };
391
+ };
392
+ //#endregion
393
+ //#region ../core/src/aipass/media.ts
394
+ const INLINE_CAP = {
395
+ audio: 26214400,
396
+ file: 10485760,
397
+ image: 5242880,
398
+ video: 52428800
399
+ };
400
+ /** Music carries `url`; video carries only `snapshotUrl`, so both are read. */
401
+ const fileEventSchema = z.object({
402
+ filename: z.string().optional(),
403
+ mediaType: z.string().optional(),
404
+ snapshotUrl: z.string().optional(),
405
+ url: z.string().optional()
406
+ });
407
+ const kindOfMediaType = (mediaType) => {
408
+ if (mediaType.startsWith("image/")) return "image";
409
+ if (mediaType.startsWith("video/")) return "video";
410
+ return mediaType.startsWith("audio/") ? "audio" : "file";
411
+ };
412
+ const EXTENSIONS = {
413
+ audio: "mp3",
414
+ file: "bin",
415
+ image: "png",
416
+ video: "mp4"
417
+ };
418
+ const nameFor$1 = (kind, mediaType) => {
419
+ return `${kind}.${mediaType.split("/")[1]?.split(";")[0] ?? EXTENSIONS[kind]}`;
420
+ };
421
+ const toDataUri = (bytes, mediaType) => `data:${mediaType};base64,${Buffer.from(bytes).toString("base64")}`;
422
+ const isAbsolute = (url) => /^https?:\/\//iu.test(url);
423
+ const inlineBase64 = (asset) => asset.href.slice(asset.href.indexOf(",") + 1);
424
+ const renderAsset = (asset) => {
425
+ const link = asset.kind === "image" ? "!" : "";
426
+ const note = asset.note ? `\n\n_${asset.note}_` : "";
427
+ return `\n\n${link}[${asset.filename}](${asset.href})${note}\n\n`;
428
+ };
429
+ /**
430
+ * Only a same-origin asset is fetched; an absolute URL elsewhere is public
431
+ * or needs its own credential, and is not this proxy's to get.
432
+ */
433
+ const resolveAsset = async (cookie, event, signal) => {
434
+ const url = event.url ?? event.snapshotUrl;
435
+ if (!url) return null;
436
+ const mediaType = event.mediaType ?? "application/octet-stream";
437
+ const kind = kindOfMediaType(mediaType);
438
+ const base = {
439
+ filename: event.filename ?? nameFor$1(kind, mediaType),
440
+ kind,
441
+ mediaType
442
+ };
443
+ if (url.startsWith("data:")) return {
444
+ ...base,
445
+ href: url,
446
+ inline: true
447
+ };
448
+ const absolute = new URL(url, config.origin).href;
449
+ if (isAbsolute(url) && !absolute.startsWith(config.origin)) return {
450
+ ...base,
451
+ href: absolute,
452
+ inline: false
453
+ };
454
+ const needsLogin = "the file is over the size the proxy will carry inline, so this link needs a logged-in browser";
455
+ try {
456
+ const response = await fetch(absolute, {
457
+ headers: browserGetHeaders(cookie, `${config.origin}/chat`),
458
+ redirect: "manual",
459
+ signal
460
+ });
461
+ if (!response.ok) return {
462
+ ...base,
463
+ href: absolute,
464
+ inline: false,
465
+ note: `the proxy could not read the file (${response.status}); this link needs a logged-in browser`
466
+ };
467
+ const bytes = await response.arrayBuffer();
468
+ if (bytes.byteLength > INLINE_CAP[kind]) return {
469
+ ...base,
470
+ href: absolute,
471
+ inline: false,
472
+ note: needsLogin
473
+ };
474
+ return {
475
+ ...base,
476
+ href: toDataUri(bytes, mediaType),
477
+ inline: true
478
+ };
479
+ } catch (error) {
480
+ return {
481
+ ...base,
482
+ href: absolute,
483
+ inline: false,
484
+ note: `the proxy could not read the file (${error}); this link needs a logged-in browser`
485
+ };
486
+ }
487
+ };
488
+ //#endregion
489
+ //#region ../core/src/aipass/quotas.ts
490
+ const QUOTA_PATH = "/loaders/get-usage-quota";
491
+ const SETTLE_TIMEOUT_MS = 5e3;
492
+ const amountSchema = z.union([z.string(), z.number()]);
493
+ const quotaResponseSchema = z.object({ creditStatus: z.object({
494
+ credits: z.object({
495
+ available: amountSchema,
496
+ limit: amountSchema,
497
+ used: amountSchema
498
+ }),
499
+ creditsDecimals: z.number().int().min(0).max(18),
500
+ periodEndsAt: z.string()
501
+ }) });
502
+ const fetchCredits = async (cookie, signal) => {
503
+ const quota = await loadJson(QUOTA_PATH, quotaResponseSchema, cookie, signal);
504
+ if (!quota) return null;
505
+ const { credits, creditsDecimals, periodEndsAt } = quota.creditStatus;
506
+ const scale = 10 ** creditsDecimals;
507
+ return {
508
+ creditsAvailable: Number(credits.available) / scale,
509
+ creditsLimit: Number(credits.limit) / scale,
510
+ creditsResetAt: periodEndsAt,
511
+ creditsUsed: Number(credits.used) / scale
512
+ };
513
+ };
514
+ z.object({
515
+ available: z.number(),
516
+ limit: z.number(),
517
+ reset_at: z.string(),
518
+ used: z.number()
519
+ }).extend({ spent: z.number().optional() });
520
+ const toCreditBalance = (credits) => ({
521
+ available: credits.creditsAvailable,
522
+ limit: credits.creditsLimit,
523
+ reset_at: credits.creditsResetAt,
524
+ used: credits.creditsUsed
525
+ });
526
+ const toCreditUsage = (before, after) => {
527
+ const latest = after ?? before;
528
+ if (!latest) return;
529
+ const balance = toCreditBalance(latest);
530
+ if (!(before && after)) return balance;
531
+ return {
532
+ ...balance,
533
+ spent: Math.max(0, after.creditsUsed - before.creditsUsed)
534
+ };
535
+ };
536
+ /** The read has its own timeout because the request signal may already be aborted. */
537
+ const settleCredits = async (cookie, before) => {
538
+ const [start, after] = await Promise.all([before, fetchCredits(cookie, AbortSignal.timeout(SETTLE_TIMEOUT_MS))]);
539
+ return {
540
+ after,
541
+ usage: toCreditUsage(start, after)
542
+ };
543
+ };
544
+ const CLIENT_ID_LENGTH = 16;
545
+ const clientIdFromCookie = (cookie) => createHash("sha256").update(cookie).digest("hex").slice(0, CLIENT_ID_LENGTH);
546
+ const thinkingLevelSchema = z.enum([
547
+ "low",
548
+ "medium",
549
+ "high",
550
+ "max"
551
+ ]);
552
+ /** The levels to assume for a model whose catalog entry could not be read. */
553
+ const COMMON_LEVELS = [
554
+ "low",
555
+ "medium",
556
+ "high"
557
+ ];
558
+ /**
559
+ * An unsupported level is dropped and named, like any other unsupported
560
+ * setting. An unread catalog falls back to the common three so a cold cache
561
+ * does not strip a level the model would have taken.
562
+ */
563
+ const resolveThinking = (asked, entry) => {
564
+ if (asked === void 0) return { level: null };
565
+ const advertised = entry?.thinking;
566
+ const allowed = advertised && advertised.length > 0 ? advertised : COMMON_LEVELS;
567
+ if (allowed.includes(asked)) return { level: asked };
568
+ return {
569
+ dropped: `the model does not offer thinking level ${asked}, it offers ${allowed.join(", ")}`,
570
+ level: null
571
+ };
572
+ };
573
+ //#endregion
574
+ //#region ../core/src/lib/stream.ts
575
+ const guardController = (controller) => {
576
+ let open = true;
577
+ const attempt = (action) => {
578
+ if (!open) return;
579
+ try {
580
+ action();
581
+ } catch {
582
+ open = false;
583
+ }
584
+ };
585
+ return {
586
+ abandon: () => {
587
+ open = false;
588
+ },
589
+ close: () => attempt(() => controller.close()),
590
+ enqueue: (chunk) => attempt(() => controller.enqueue(chunk)),
591
+ isOpen: () => open
592
+ };
593
+ };
594
+ //#endregion
595
+ //#region ../core/src/aipass/inline.ts
596
+ /**
597
+ * A remote URL is refused rather than fetched: a deployment that fetches any
598
+ * URL a caller names is a request forger for the network it sits in.
599
+ */
600
+ const DATA_URI = /^data:(?<mediaType>[^;,]*)(?<base64>;base64)?,/iu;
601
+ var InlineError = class extends Error {
602
+ name = "InlineError";
603
+ };
604
+ /** atob yields one byte per code unit, so every code point here is below 256. */
605
+ const decodeBase64 = (value) => {
606
+ const binary = atob(value);
607
+ const bytes = new Uint8Array(binary.length);
608
+ for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.codePointAt(index) ?? 0;
609
+ return bytes;
610
+ };
611
+ const fromBase64 = (data, mediaType, filename) => {
612
+ try {
613
+ return {
614
+ bytes: decodeBase64(data),
615
+ filename,
616
+ mediaType
617
+ };
618
+ } catch {
619
+ throw new InlineError(`${filename} is not readable base64`);
620
+ }
621
+ };
622
+ const nameFor = (mediaType, index) => {
623
+ const subtype = mediaType.split("/")[1]?.split(";")[0] ?? "bin";
624
+ const extension = subtype === "jpeg" ? "jpg" : subtype;
625
+ return `${mediaType.startsWith("image/") ? "image" : "attachment"}-${index + 1}.${extension}`;
626
+ };
627
+ /** Percent-encoded data URIs are decoded too; small SVG and text files arrive that way. */
628
+ const fromDataUri = (uri, filename, index = 0) => {
629
+ const match = DATA_URI.exec(uri);
630
+ if (!match) throw new InlineError("only inline data is accepted; send the file as a data: URI or base64, not as a URL the proxy would have to fetch");
631
+ const mediaType = match.groups?.mediaType || "application/octet-stream";
632
+ const payload = uri.slice(match[0].length);
633
+ const name = filename ?? nameFor(mediaType, index);
634
+ if (match.groups?.base64) return fromBase64(payload, mediaType, name);
635
+ return {
636
+ bytes: new TextEncoder().encode(decodeURIComponent(payload)),
637
+ filename: name,
638
+ mediaType
639
+ };
640
+ };
641
+ //#endregion
642
+ //#region ../core/src/aipass/stream.ts
643
+ const optionalText = z.union([z.string(), z.unknown().transform(() => void 0)]).optional();
644
+ /** Some models put the file's fields at the top level, others under `data`; the outer wins. */
645
+ const fileFrameSchema = fileEventSchema.extend({
646
+ data: fileEventSchema.optional(),
647
+ type: z.literal("file")
648
+ });
649
+ const upstreamEventSchema = z.discriminatedUnion("type", [
650
+ z.object({
651
+ delta: z.string(),
652
+ type: z.literal("text-delta")
653
+ }),
654
+ z.object({
655
+ delta: z.string(),
656
+ type: z.literal("reasoning-delta")
657
+ }),
658
+ fileFrameSchema,
659
+ z.object({
660
+ finishReason: optionalText,
661
+ type: z.literal("finish")
662
+ }),
663
+ z.object({
664
+ error: optionalText,
665
+ errorText: optionalText,
666
+ type: z.literal("error")
667
+ })
668
+ ]);
669
+ const skippedTypeSchema = z.object({ type: z.string() });
670
+ const parseAipassSSE = async function* parseAipassSSE(body, skips) {
671
+ const chunks = parseJsonEventStream({
672
+ schema: upstreamEventSchema,
673
+ stream: body
674
+ });
675
+ for await (const chunk of chunks) {
676
+ if (!chunk.success) {
677
+ if (skips) {
678
+ skips.count += 1;
679
+ const named = skippedTypeSchema.safeParse(chunk.rawValue);
680
+ if (named.success) skips.types.add(named.data.type);
681
+ }
682
+ continue;
683
+ }
684
+ const event = chunk.value;
685
+ switch (event.type) {
686
+ case "text-delta":
687
+ yield {
688
+ kind: "delta",
689
+ text: event.delta
690
+ };
691
+ break;
692
+ case "reasoning-delta":
693
+ yield {
694
+ kind: "reasoning",
695
+ text: event.delta
696
+ };
697
+ break;
698
+ case "file": {
699
+ const { data } = event;
700
+ yield {
701
+ file: {
702
+ filename: event.filename ?? data?.filename,
703
+ mediaType: event.mediaType ?? data?.mediaType,
704
+ snapshotUrl: event.snapshotUrl ?? data?.snapshotUrl,
705
+ url: event.url ?? data?.url
706
+ },
707
+ kind: "file"
708
+ };
709
+ break;
710
+ }
711
+ case "finish":
712
+ yield {
713
+ kind: "finish",
714
+ reason: event.finishReason ?? "stop"
715
+ };
716
+ break;
717
+ default: yield {
718
+ kind: "error",
719
+ message: event.errorText ?? event.error ?? "upstream error"
720
+ };
721
+ }
722
+ }
723
+ };
724
+ //#endregion
725
+ //#region ../core/src/tokens.ts
726
+ /**
727
+ * AI Pass reports no token counts. ASCII runs about four characters per
728
+ * token; Thai, other non-Latin scripts and emoji nearer one and a half, so
729
+ * the two are counted apart.
730
+ */
731
+ const ASCII = /[\u0020-\u007E\s]/gu;
732
+ const ASCII_CHARS_PER_TOKEN = 4;
733
+ const OTHER_CHARS_PER_TOKEN = 1.5;
734
+ const newCharCount = () => ({
735
+ ascii: 0,
736
+ other: 0
737
+ });
738
+ const addText = (count, text) => {
739
+ const ascii = text.match(ASCII)?.length ?? 0;
740
+ count.ascii += ascii;
741
+ count.other += text.length - ascii;
742
+ };
743
+ const charTokens = (count) => Math.ceil(count.ascii / ASCII_CHARS_PER_TOKEN + count.other / OTHER_CHARS_PER_TOKEN);
744
+ const estimateTokens = (text) => {
745
+ const count = newCharCount();
746
+ addText(count, text);
747
+ return charTokens(count);
748
+ };
749
+ //#endregion
750
+ //#region ../core/src/lib/id.ts
751
+ /** The first `length` hex characters of a random UUID, for wire-visible ids. */
752
+ const randomHex = (length) => crypto.randomUUID().replaceAll("-", "").slice(0, length);
753
+ //#endregion
754
+ //#region ../core/src/tools.ts
755
+ const toolInputSchema = z.record(z.string(), z.unknown());
756
+ const FENCE_OPEN = "```tool_call\n";
757
+ const FENCE_CLOSE = "\n```";
758
+ const CALL_ID_LENGTH = 24;
759
+ const callBodySchema = z.object({
760
+ input: toolInputSchema.default({}),
761
+ name: z.string().min(1)
762
+ });
763
+ const text = (value) => value.length > 0 ? [{
764
+ text: value,
765
+ type: "text"
766
+ }] : [];
767
+ const newCallId = () => randomHex(CALL_ID_LENGTH);
768
+ const renderCall = (call) => `${FENCE_OPEN}${JSON.stringify({
769
+ input: call.input,
770
+ name: call.name
771
+ })}${FENCE_CLOSE}`;
772
+ const renderTool = (tool) => {
773
+ const summary = tool.description && tool.description.length > 0 ? `- ${tool.name}: ${tool.description.trim()}` : `- ${tool.name}`;
774
+ return tool.inputSchema === void 0 ? summary : `${summary}\n input schema: ${JSON.stringify(tool.inputSchema)}`;
775
+ };
776
+ const renderToolGuide = (tools) => [
777
+ "You can call tools. To call one, write a block exactly like this, one block per call, then stop and wait for the result, which arrives in the next turn:",
778
+ `${FENCE_OPEN}{"name": "<tool name>", "input": {<arguments matching the tool's input schema>}}${FENCE_CLOSE}`,
779
+ `Tools:\n\n${tools.map(renderTool).join("\n")}`
780
+ ].join("\n\n");
781
+ const parseCall = (json, names) => {
782
+ let raw;
783
+ try {
784
+ raw = JSON.parse(json);
785
+ } catch {
786
+ return;
787
+ }
788
+ const parsed = callBodySchema.safeParse(raw);
789
+ if (!parsed.success || !names.has(parsed.data.name)) return;
790
+ return {
791
+ id: newCallId(),
792
+ input: parsed.data.input,
793
+ name: parsed.data.name
794
+ };
795
+ };
796
+ /** How many trailing characters could still turn out to open a fence. */
797
+ const heldLength = (tail) => {
798
+ const longest = Math.min(tail.length, 12);
799
+ for (let length = longest; length > 0; length -= 1) if ("```tool_call\n".startsWith(tail.slice(-length))) return length;
800
+ return 0;
801
+ };
802
+ /**
803
+ * Text is released as soon as it can no longer open a fence. A fence that
804
+ * never closes, or holds no call to an offered tool, is released as text.
805
+ */
806
+ const splitReply = (tools) => {
807
+ if (tools.length === 0) return {
808
+ flush: () => [],
809
+ push: text
810
+ };
811
+ const names = new Set(tools.map((tool) => tool.name));
812
+ let buffer = "";
813
+ let inBlock = false;
814
+ /** Ends the open block; `fence` is what closed it, nothing when the reply ran out first. */
815
+ const closeBlock = (json, fence) => {
816
+ inBlock = false;
817
+ const call = parseCall(json.trim(), names);
818
+ return call ? [{
819
+ call,
820
+ type: "call"
821
+ }] : text(`${FENCE_OPEN}${json}${fence}`);
822
+ };
823
+ const drain = (final) => {
824
+ const parts = [];
825
+ for (;;) {
826
+ if (inBlock) {
827
+ const end = buffer.indexOf(FENCE_CLOSE);
828
+ if (end === -1) {
829
+ if (!final) return parts;
830
+ const json = buffer;
831
+ buffer = "";
832
+ parts.push(...closeBlock(json, ""));
833
+ return parts;
834
+ }
835
+ const json = buffer.slice(0, end);
836
+ buffer = buffer.slice(end + 4);
837
+ if (buffer.startsWith("\n")) buffer = buffer.slice(1);
838
+ parts.push(...closeBlock(json, FENCE_CLOSE));
839
+ continue;
840
+ }
841
+ const start = buffer.indexOf(FENCE_OPEN);
842
+ if (start === -1) {
843
+ const hold = final ? 0 : heldLength(buffer);
844
+ const released = buffer.slice(0, buffer.length - hold);
845
+ buffer = buffer.slice(buffer.length - hold);
846
+ parts.push(...text(released));
847
+ return parts;
848
+ }
849
+ parts.push(...text(buffer.slice(0, start)));
850
+ buffer = buffer.slice(start + 13);
851
+ inBlock = true;
852
+ }
853
+ };
854
+ return {
855
+ flush: () => drain(true),
856
+ push: (chunk) => {
857
+ buffer += chunk;
858
+ return drain(false);
859
+ }
860
+ };
861
+ };
862
+ //#endregion
863
+ //#region ../core/src/translate.ts
864
+ const filesOf = (conversation) => conversation.turns.flatMap((turn) => turn.files ?? []);
865
+ const toolNames = (turns) => {
866
+ const names = /* @__PURE__ */ new Map();
867
+ for (const turn of turns) for (const call of turn.calls ?? []) names.set(call.id, call.name);
868
+ return names;
869
+ };
870
+ const roleLabel = (turn, names) => {
871
+ if (turn.role === "assistant") return "Assistant";
872
+ if (turn.role === "tool") {
873
+ const name = turn.callId === void 0 ? void 0 : names.get(turn.callId);
874
+ return name === void 0 ? "Tool" : `Tool (${name})`;
875
+ }
876
+ return "User";
877
+ };
878
+ const turnText = (turn) => {
879
+ return [turn.content.trim(), ...(turn.calls ?? []).map(renderCall)].filter((part) => part.length > 0).join("\n\n");
880
+ };
881
+ const flattenPrompt = ({ tools, turns }) => {
882
+ const system = turns.filter((turn) => turn.role === "system").map((turn) => turn.content).filter((text) => text.length > 0).join("\n\n");
883
+ const convo = turns.filter((turn) => turn.role !== "system");
884
+ if (system.length === 0 && tools.length === 0 && convo.length === 1 && convo[0]?.role === "user" && convo[0].calls === void 0) return convo[0].content;
885
+ const blocks = [];
886
+ if (system.length > 0) blocks.push(system.trim());
887
+ if (tools.length > 0) blocks.push(renderToolGuide(tools));
888
+ const names = toolNames(convo);
889
+ const lines = [];
890
+ for (const turn of convo) {
891
+ const text = turnText(turn);
892
+ if (text.length === 0) continue;
893
+ lines.push(`${roleLabel(turn, names)}: ${text}`);
894
+ }
895
+ if (lines.length > 0) blocks.push(lines.join("\n\n"));
896
+ return blocks.join("\n\n");
897
+ };
898
+ const toAipassMessages = (prompt, modelId) => [{
899
+ id: crypto.randomUUID(),
900
+ metadata: { modelId },
901
+ parts: [{
902
+ text: prompt,
903
+ type: "text"
904
+ }],
905
+ role: "user"
906
+ }];
907
+ //#endregion
908
+ //#region ../core/src/reply.ts
909
+ /** Only a request that asked for a level reads the catalog; on a cold cache that is a round trip. */
910
+ const prepareTurn = async (conversation, model, thinking, catalog) => {
911
+ const prompt = flattenPrompt(conversation);
912
+ const inputTokens = estimateTokens(prompt);
913
+ let thinkingLevel = null;
914
+ let thinkingDropped;
915
+ if (thinking !== void 0) {
916
+ const entries = await catalog();
917
+ const resolved = resolveThinking(thinking, entries?.get(model));
918
+ thinkingLevel = resolved.level;
919
+ thinkingDropped = resolved.dropped;
920
+ }
921
+ const sendOptions = { attachments: filesOf(conversation).map((file, index) => fromDataUri(file.uri, file.filename, index)) };
922
+ if (thinkingLevel) sendOptions.thinkingLevel = thinkingLevel;
923
+ return {
924
+ inputTokens,
925
+ prompt,
926
+ sendOptions,
927
+ thinkingDropped,
928
+ thinkingLevel
929
+ };
930
+ };
931
+ const finishReasonOf = (tally) => tally.calls > 0 && tally.finishReason === "stop" ? "tool-calls" : tally.finishReason;
932
+ const replyTokens = (tally) => charTokens(tally.reply);
933
+ /**
934
+ * Text goes through the tool-call splitter, so a fenced call never reaches the
935
+ * caller as text. An upstream error is yielded rather than thrown, since each
936
+ * caller handles one differently.
937
+ */
938
+ const readReply = (options) => {
939
+ const { body, cookie, signal, tools } = options;
940
+ const startedAt = options.startedAt ?? Date.now();
941
+ const tally = {
942
+ calls: 0,
943
+ chars: 0,
944
+ deltas: 0,
945
+ files: 0,
946
+ finishReason: "stop",
947
+ msToFirstChunk: void 0,
948
+ reasoningChars: 0,
949
+ reply: newCharCount(),
950
+ skips: {
951
+ count: 0,
952
+ types: /* @__PURE__ */ new Set()
953
+ }
954
+ };
955
+ const splitter = splitReply(tools);
956
+ const fromParts = function* fromParts(parts) {
957
+ for (const part of parts) if (part.type === "text") {
958
+ tally.chars += part.text.length;
959
+ addText(tally.reply, part.text);
960
+ yield {
961
+ kind: "text",
962
+ text: part.text
963
+ };
964
+ } else {
965
+ tally.calls += 1;
966
+ addText(tally.reply, renderCall(part.call));
967
+ yield {
968
+ call: part.call,
969
+ kind: "call"
970
+ };
971
+ }
972
+ };
973
+ return {
974
+ events: async function* events() {
975
+ for await (const event of parseAipassSSE(body, tally.skips)) if (event.kind === "delta") {
976
+ tally.msToFirstChunk ??= Date.now() - startedAt;
977
+ tally.deltas += 1;
978
+ yield* fromParts(splitter.push(event.text));
979
+ } else if (event.kind === "reasoning") {
980
+ tally.reasoningChars += event.text.length;
981
+ yield {
982
+ kind: "reasoning",
983
+ text: event.text
984
+ };
985
+ } else if (event.kind === "file") {
986
+ const asset = await resolveAsset(cookie, event.file, signal);
987
+ if (asset) {
988
+ tally.files += 1;
989
+ yield {
990
+ asset,
991
+ kind: "file"
992
+ };
993
+ }
994
+ } else if (event.kind === "finish") tally.finishReason = event.reason;
995
+ else yield {
996
+ kind: "error",
997
+ message: event.message
998
+ };
999
+ yield* fromParts(splitter.flush());
1000
+ }(),
1001
+ tally
1002
+ };
1003
+ };
1004
+ //#endregion
1005
+ //#region src/prompt.ts
1006
+ const resultText = (output) => {
1007
+ switch (output.type) {
1008
+ case "text":
1009
+ case "error-text": return output.value;
1010
+ case "json":
1011
+ case "error-json": return JSON.stringify(output.value);
1012
+ default: return output.value.filter((part) => part.type === "text").map((part) => part.text).join("");
1013
+ }
1014
+ };
1015
+ /** The SDK types a call's input as unknown; a call whose input is not an object carries none. */
1016
+ const inputOf = (part) => {
1017
+ const parsed = toolInputSchema.safeParse(part.input);
1018
+ return parsed.success ? parsed.data : {};
1019
+ };
1020
+ const droppedWarning = (dropped) => dropped.size > 0 ? [{
1021
+ message: `the provider dropped ${[...dropped].toSorted().join(", ")} parts because AI Pass reads text only`,
1022
+ type: "other"
1023
+ }] : [];
1024
+ /** Inline bytes become a data URI; a URL is passed through to be refused where every protocol's files decode. */
1025
+ const fileUri = (part) => {
1026
+ const { data, mediaType } = part;
1027
+ if (data instanceof URL) return data.href;
1028
+ if (data instanceof Uint8Array) return `data:${mediaType};base64,${Buffer.from(data).toString("base64")}`;
1029
+ return data.startsWith("data:") ? data : `data:${mediaType};base64,${data}`;
1030
+ };
1031
+ const toTurnFile = (part, index) => ({
1032
+ filename: part.filename ?? nameFor(part.mediaType, index),
1033
+ uri: fileUri(part)
1034
+ });
1035
+ const convertPrompt = (prompt) => {
1036
+ const turns = [];
1037
+ const dropped = /* @__PURE__ */ new Set();
1038
+ for (const message of prompt) {
1039
+ if (message.role === "system") {
1040
+ turns.push({
1041
+ content: message.content,
1042
+ role: "system"
1043
+ });
1044
+ continue;
1045
+ }
1046
+ if (message.role === "tool") {
1047
+ for (const part of message.content) turns.push({
1048
+ callId: part.toolCallId,
1049
+ content: resultText(part.output),
1050
+ role: "tool"
1051
+ });
1052
+ continue;
1053
+ }
1054
+ const text = [];
1055
+ const calls = [];
1056
+ const files = [];
1057
+ for (const part of message.content) if (part.type === "text" || part.type === "reasoning") text.push(part.text);
1058
+ else if (part.type === "file") files.push(toTurnFile(part, files.length));
1059
+ else if (part.type === "tool-call") calls.push({
1060
+ id: part.toolCallId,
1061
+ input: inputOf(part),
1062
+ name: part.toolName
1063
+ });
1064
+ else dropped.add(part.type);
1065
+ const turn = {
1066
+ content: text.join(""),
1067
+ role: message.role
1068
+ };
1069
+ const withFiles = files.length > 0 ? {
1070
+ ...turn,
1071
+ files
1072
+ } : turn;
1073
+ turns.push(calls.length > 0 ? {
1074
+ ...withFiles,
1075
+ calls
1076
+ } : withFiles);
1077
+ }
1078
+ return {
1079
+ turns,
1080
+ warnings: droppedWarning(dropped)
1081
+ };
1082
+ };
1083
+ /** Function tools are offered through the text protocol; provider-defined ones have no upstream to run on. */
1084
+ const convertTools = (options) => {
1085
+ const tools = [];
1086
+ const warnings = [];
1087
+ for (const tool of options.tools ?? []) if (tool.type === "function") tools.push({
1088
+ description: tool.description,
1089
+ inputSchema: tool.inputSchema,
1090
+ name: tool.name
1091
+ });
1092
+ else warnings.push({
1093
+ tool,
1094
+ type: "unsupported-tool"
1095
+ });
1096
+ return {
1097
+ tools,
1098
+ warnings
1099
+ };
1100
+ };
1101
+ //#endregion
1102
+ //#region src/model.ts
1103
+ const PROVIDER = "aipass";
1104
+ const REASONING_ID = "reasoning";
1105
+ const DETAIL_LIMIT = 300;
1106
+ const estimatedUsage = (inputTokens, outputTokens) => ({
1107
+ inputTokens,
1108
+ outputTokens,
1109
+ totalTokens: inputTokens + outputTokens
1110
+ });
1111
+ const FINISH_REASONS = /* @__PURE__ */ new Map([
1112
+ ["content-filter", "content-filter"],
1113
+ ["error", "error"],
1114
+ ["length", "length"],
1115
+ ["other", "other"],
1116
+ ["stop", "stop"],
1117
+ ["tool-calls", "tool-calls"],
1118
+ ["unknown", "unknown"]
1119
+ ]);
1120
+ const UNSUPPORTED_SETTINGS = [
1121
+ "frequencyPenalty",
1122
+ "maxOutputTokens",
1123
+ "presencePenalty",
1124
+ "responseFormat",
1125
+ "seed",
1126
+ "stopSequences",
1127
+ "temperature",
1128
+ "toolChoice",
1129
+ "topK",
1130
+ "topP"
1131
+ ];
1132
+ const toFinishReason = (reason) => FINISH_REASONS.get(reason) ?? "unknown";
1133
+ /** AI Pass takes a level, not a token budget, so the SDK's own reasoning settings do not map. */
1134
+ const askedThinking = (options) => {
1135
+ const asked = options.providerOptions?.aipass?.thinkingLevel;
1136
+ const parsed = thinkingLevelSchema.safeParse(asked);
1137
+ return parsed.success ? parsed.data : void 0;
1138
+ };
1139
+ const settingWarnings = (options) => {
1140
+ const warnings = [];
1141
+ for (const setting of UNSUPPORTED_SETTINGS) if (options[setting] !== void 0) warnings.push({
1142
+ setting,
1143
+ type: "unsupported-setting"
1144
+ });
1145
+ return warnings;
1146
+ };
1147
+ /** Provider metadata allows no undefined, so `spent` is left out when unknown. */
1148
+ const creditMetadata = (usage) => {
1149
+ if (!usage) return;
1150
+ const { spent, ...balance } = usage;
1151
+ return { aipass: { credits: spent === void 0 ? balance : {
1152
+ ...balance,
1153
+ spent
1154
+ } } };
1155
+ };
1156
+ const toolCallPart = (call) => ({
1157
+ input: JSON.stringify(call.input),
1158
+ toolCallId: call.id,
1159
+ toolName: call.name,
1160
+ type: "tool-call"
1161
+ });
1162
+ const startTurn = async (cookie, modelId, options) => {
1163
+ const prompt = convertPrompt(options.prompt);
1164
+ const offered = convertTools(options);
1165
+ const { tools } = offered;
1166
+ const catalog = fetchCatalog(clientIdFromCookie(cookie), cookie, options.abortSignal);
1167
+ const problem = chatModelProblem(modelId, await catalog);
1168
+ if (problem) throw new NoSuchModelError({
1169
+ message: problem,
1170
+ modelId,
1171
+ modelType: "languageModel"
1172
+ });
1173
+ const credits = fetchCredits(cookie, options.abortSignal);
1174
+ const prepared = await prepareTurn({
1175
+ tools,
1176
+ turns: prompt.turns
1177
+ }, modelId, askedThinking(options), () => catalog);
1178
+ const body = toAipassMessages(prepared.prompt, modelId);
1179
+ const { conversationId, created, response } = await sendMessage(cookie, modelId, body, options.abortSignal, prepared.sendOptions);
1180
+ const contentType = response.headers.get("content-type") ?? "";
1181
+ if (response.ok && response.body && contentType.includes("event-stream")) {
1182
+ const warnings = [
1183
+ ...prompt.warnings,
1184
+ ...offered.warnings,
1185
+ ...settingWarnings(options)
1186
+ ];
1187
+ if (prepared.thinkingDropped) warnings.push({
1188
+ message: prepared.thinkingDropped,
1189
+ type: "other"
1190
+ });
1191
+ return {
1192
+ conversationId,
1193
+ credits,
1194
+ inputTokens: prepared.inputTokens,
1195
+ reader: readReply({
1196
+ body: response.body,
1197
+ cookie,
1198
+ signal: options.abortSignal,
1199
+ tools
1200
+ }),
1201
+ warnings
1202
+ };
1203
+ }
1204
+ const detail = response.body ? await response.text().catch(() => "") : "";
1205
+ if (created) await deleteConversation(cookie, conversationId);
1206
+ const hint = isEdgeRefusal(response.status) ? EDGE_REFUSAL_HINT : "";
1207
+ throw new APICallError({
1208
+ message: `AI Pass answered ${response.status} (${contentType || "no content-type"})${hint}`,
1209
+ requestBodyValues: {
1210
+ messages: body,
1211
+ modelId
1212
+ },
1213
+ responseBody: detail.slice(0, DETAIL_LIMIT),
1214
+ statusCode: response.status,
1215
+ url: created ? `${config.origin}/actions/send-message/${conversationId}` : `${config.origin}/chat.data`
1216
+ });
1217
+ };
1218
+ /** An inline asset becomes the SDK's file part; a link is rendered as text with the reason. */
1219
+ const assetPart = (asset) => {
1220
+ if (!asset.inline) return { text: renderAsset(asset) };
1221
+ return {
1222
+ data: inlineBase64(asset),
1223
+ mediaType: asset.mediaType,
1224
+ type: "file"
1225
+ };
1226
+ };
1227
+ const streamTurn = (cookie, turn) => new ReadableStream({ async start(raw) {
1228
+ const out = guardController(raw);
1229
+ const { reader } = turn;
1230
+ const { tally } = reader;
1231
+ out.enqueue({
1232
+ type: "stream-start",
1233
+ warnings: turn.warnings
1234
+ });
1235
+ let textId;
1236
+ let textBlocks = 0;
1237
+ let reasoningOpen = false;
1238
+ const closeText = () => {
1239
+ if (textId !== void 0) {
1240
+ out.enqueue({
1241
+ id: textId,
1242
+ type: "text-end"
1243
+ });
1244
+ textId = void 0;
1245
+ }
1246
+ };
1247
+ const text = (delta) => {
1248
+ if (textId === void 0) {
1249
+ textId = `text-${textBlocks}`;
1250
+ textBlocks += 1;
1251
+ out.enqueue({
1252
+ id: textId,
1253
+ type: "text-start"
1254
+ });
1255
+ }
1256
+ out.enqueue({
1257
+ delta,
1258
+ id: textId,
1259
+ type: "text-delta"
1260
+ });
1261
+ };
1262
+ try {
1263
+ for await (const event of reader.events) if (event.kind === "text") text(event.text);
1264
+ else if (event.kind === "call") {
1265
+ closeText();
1266
+ out.enqueue(toolCallPart(event.call));
1267
+ } else if (event.kind === "reasoning") {
1268
+ if (!reasoningOpen) {
1269
+ out.enqueue({
1270
+ id: REASONING_ID,
1271
+ type: "reasoning-start"
1272
+ });
1273
+ reasoningOpen = true;
1274
+ }
1275
+ out.enqueue({
1276
+ delta: event.text,
1277
+ id: REASONING_ID,
1278
+ type: "reasoning-delta"
1279
+ });
1280
+ } else if (event.kind === "file") {
1281
+ const part = assetPart(event.asset);
1282
+ if ("type" in part) {
1283
+ closeText();
1284
+ out.enqueue(part);
1285
+ } else text(part.text);
1286
+ } else {
1287
+ tally.finishReason = "error";
1288
+ out.enqueue({
1289
+ error: new Error(event.message),
1290
+ type: "error"
1291
+ });
1292
+ }
1293
+ } catch (error) {
1294
+ tally.finishReason = "error";
1295
+ out.enqueue({
1296
+ error,
1297
+ type: "error"
1298
+ });
1299
+ } finally {
1300
+ if (reasoningOpen) out.enqueue({
1301
+ id: REASONING_ID,
1302
+ type: "reasoning-end"
1303
+ });
1304
+ closeText();
1305
+ const [{ usage }] = await Promise.all([settleCredits(cookie, turn.credits), deleteConversation(cookie, turn.conversationId)]);
1306
+ out.enqueue({
1307
+ finishReason: toFinishReason(finishReasonOf(tally)),
1308
+ providerMetadata: creditMetadata(usage),
1309
+ type: "finish",
1310
+ usage: estimatedUsage(turn.inputTokens, replyTokens(tally))
1311
+ });
1312
+ out.close();
1313
+ }
1314
+ } });
1315
+ const aipassModel = (cookie, modelId) => ({
1316
+ doGenerate: async (options) => {
1317
+ const turn = await startTurn(cookie, modelId, options);
1318
+ const { reader } = turn;
1319
+ const { tally } = reader;
1320
+ const calls = [];
1321
+ const files = [];
1322
+ let text = "";
1323
+ let reasoning = "";
1324
+ try {
1325
+ for await (const event of reader.events) if (event.kind === "text") text += event.text;
1326
+ else if (event.kind === "call") calls.push(event.call);
1327
+ else if (event.kind === "reasoning") reasoning += event.text;
1328
+ else if (event.kind === "file") {
1329
+ const part = assetPart(event.asset);
1330
+ if ("type" in part) files.push(part);
1331
+ else text += part.text;
1332
+ } else tally.finishReason = "error";
1333
+ } finally {
1334
+ await deleteConversation(cookie, turn.conversationId);
1335
+ }
1336
+ const { usage } = await settleCredits(cookie, turn.credits);
1337
+ const content = [];
1338
+ if (reasoning.length > 0) content.push({
1339
+ text: reasoning,
1340
+ type: "reasoning"
1341
+ });
1342
+ if (text.length > 0) content.push({
1343
+ text,
1344
+ type: "text"
1345
+ });
1346
+ content.push(...files, ...calls.map(toolCallPart));
1347
+ return {
1348
+ content,
1349
+ finishReason: toFinishReason(finishReasonOf(tally)),
1350
+ providerMetadata: creditMetadata(usage),
1351
+ usage: estimatedUsage(turn.inputTokens, replyTokens(tally)),
1352
+ warnings: turn.warnings
1353
+ };
1354
+ },
1355
+ doStream: async (options) => {
1356
+ const turn = await startTurn(cookie, modelId, options);
1357
+ return { stream: streamTurn(cookie, turn) };
1358
+ },
1359
+ modelId,
1360
+ provider: PROVIDER,
1361
+ specificationVersion: "v2",
1362
+ supportedUrls: {}
1363
+ });
1364
+ //#endregion
1365
+ //#region src/index.ts
1366
+ const createAipass = (settings) => {
1367
+ if (settings.origin !== void 0) configure({ origin: settings.origin });
1368
+ if (settings.logger !== void 0) configure({ logger: settings.logger });
1369
+ const languageModel = (modelId = DEFAULT_MODEL) => {
1370
+ if (modelId.length === 0 || kindOf(modelId) !== "chat") throw new NoSuchModelError({
1371
+ modelId,
1372
+ modelType: "languageModel"
1373
+ });
1374
+ return aipassModel(settings.cookie, modelId);
1375
+ };
1376
+ return Object.assign(languageModel, { languageModel });
1377
+ };
1378
+ //#endregion
1379
+ export { createAipass };