@sahan-sac/ai-core 0.2.0 → 0.3.1

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.
@@ -0,0 +1,571 @@
1
+ import {
2
+ getVertexAccessToken
3
+ } from "./chunk-BL4J2JNX.js";
4
+ import {
5
+ consoleAiLogger
6
+ } from "./chunk-PMC3UWQ2.js";
7
+ import {
8
+ DEFAULT_TEXT_MODELS,
9
+ paidAllowed,
10
+ textModels,
11
+ thinkingConfigFor,
12
+ vertexConfigured
13
+ } from "./chunk-RBSLNDFW.js";
14
+
15
+ // src/providers.ts
16
+ import "server-only";
17
+ import { createOpenAI } from "@ai-sdk/openai";
18
+ import { generateText } from "ai";
19
+
20
+ // src/adapters.ts
21
+ function modelFor(env, provider, purpose, fallback) {
22
+ const record = env;
23
+ return record[`${purpose.toUpperCase()}_${provider}_MODEL`] || record[`${provider}_MODEL`] || fallback[purpose];
24
+ }
25
+ var need = (env, ...names) => names.filter((name) => !env[name]);
26
+ var PAID_TEXT_MODELS = {
27
+ openai: { blog: "gpt-5-mini", chat: "gpt-5-nano" },
28
+ anthropic: { blog: "claude-sonnet-5-5", chat: "claude-haiku-4-5-20251001" },
29
+ deepseek: { blog: "deepseek-chat", chat: "deepseek-chat" },
30
+ xai: { blog: "grok-3-mini", chat: "grok-3-mini" },
31
+ perplexity: { blog: "sonar", chat: "sonar" }
32
+ };
33
+ function compatibleAdapter(spec) {
34
+ return {
35
+ id: spec.id,
36
+ label: spec.label,
37
+ paid: () => true,
38
+ missing: (env) => need(env, spec.keyVar),
39
+ create: ({ env, purpose, fetch: fetch2 }) => openAiCompatibleProvider({
40
+ name: spec.id,
41
+ apiKey: env[spec.keyVar],
42
+ baseUrl: spec.baseUrlVar && env[spec.baseUrlVar] || spec.baseUrl,
43
+ model: modelFor(env, spec.envPrefix, purpose, PAID_TEXT_MODELS[spec.id]),
44
+ fetch: fetch2
45
+ })
46
+ };
47
+ }
48
+ var openRouterAdapter = (id, keyVar) => ({
49
+ id,
50
+ label: id === "openrouter" ? "OpenRouter" : "OpenRouter (second key)",
51
+ // Free models only, unless OPENROUTER_ALLOW_PAID_MODELS: the adapter itself refuses a paid model id.
52
+ paid: () => false,
53
+ missing: (env) => need(env, keyVar),
54
+ create: ({ env, purpose, fetch: fetch2 }) => openRouterProvider({
55
+ apiKey: env[keyVar],
56
+ model: textModels(env, purpose).openrouter,
57
+ baseUrl: env.OPENROUTER_BASE_URL,
58
+ allowPaidModels: env.OPENROUTER_ALLOW_PAID_MODELS,
59
+ fetch: fetch2,
60
+ name: id
61
+ })
62
+ });
63
+ var BUILTIN_ADAPTERS = [
64
+ {
65
+ id: "gemini",
66
+ label: "Gemini",
67
+ paid: () => false,
68
+ missing: (env) => need(env, "GEMINI_API_KEY"),
69
+ create: ({ env, purpose, fetch: fetch2 }) => geminiProvider({ apiKey: env.GEMINI_API_KEY, model: textModels(env, purpose).gemini, fetchImpl: fetch2 })
70
+ },
71
+ openRouterAdapter("openrouter", "OPENROUTER_API_KEY"),
72
+ openRouterAdapter("openrouter-2", "OPENROUTER_API_KEY_2"),
73
+ {
74
+ id: "nvidia",
75
+ label: "NVIDIA",
76
+ paid: () => false,
77
+ missing: (env) => need(env, "NVIDIA_API_KEY"),
78
+ create: ({ env, purpose, fetch: fetch2 }) => openAiCompatibleProvider({
79
+ name: "nvidia",
80
+ apiKey: env.NVIDIA_API_KEY,
81
+ baseUrl: "https://integrate.api.nvidia.com/v1",
82
+ model: textModels(env, purpose).nvidia,
83
+ fetch: fetch2
84
+ })
85
+ },
86
+ compatibleAdapter({ id: "openai", label: "OpenAI", envPrefix: "OPENAI", keyVar: "OPENAI_API_KEY", baseUrl: "https://api.openai.com/v1", baseUrlVar: "OPENAI_BASE_URL" }),
87
+ {
88
+ id: "anthropic",
89
+ label: "Anthropic (Claude)",
90
+ paid: () => true,
91
+ missing: (env) => need(env, "ANTHROPIC_API_KEY"),
92
+ create: ({ env, purpose, fetch: fetch2 }) => anthropicProvider({
93
+ apiKey: env.ANTHROPIC_API_KEY,
94
+ baseUrl: env.ANTHROPIC_BASE_URL,
95
+ model: modelFor(env, "ANTHROPIC", purpose, PAID_TEXT_MODELS.anthropic),
96
+ fetchImpl: fetch2
97
+ })
98
+ },
99
+ compatibleAdapter({ id: "deepseek", label: "DeepSeek", envPrefix: "DEEPSEEK", keyVar: "DEEPSEEK_API_KEY", baseUrl: "https://api.deepseek.com/v1" }),
100
+ compatibleAdapter({ id: "xai", label: "xAI (Grok)", envPrefix: "XAI", keyVar: "XAI_API_KEY", baseUrl: "https://api.x.ai/v1" }),
101
+ compatibleAdapter({ id: "perplexity", label: "Perplexity", envPrefix: "PERPLEXITY", keyVar: "PERPLEXITY_API_KEY", baseUrl: "https://api.perplexity.ai" }),
102
+ {
103
+ id: "custom",
104
+ label: "Custom OpenAI-compatible",
105
+ paid: (env) => !env.AI_CUSTOM_FREE,
106
+ missing: (env) => need(env, "AI_CUSTOM_BASE_URL", "AI_CUSTOM_MODEL"),
107
+ create: ({ env, fetch: fetch2 }) => openAiCompatibleProvider({
108
+ name: env.AI_CUSTOM_NAME ? `custom:${env.AI_CUSTOM_NAME}` : "custom",
109
+ // A local server may take no key; the header must still be well formed.
110
+ apiKey: env.AI_CUSTOM_API_KEY ?? "none",
111
+ baseUrl: env.AI_CUSTOM_BASE_URL,
112
+ model: env.AI_CUSTOM_MODEL,
113
+ fetch: fetch2
114
+ })
115
+ },
116
+ {
117
+ id: "vertex",
118
+ label: "Vertex AI",
119
+ paid: () => true,
120
+ missing: (env) => vertexConfigured(env) ? [] : need(env, "GOOGLE_CLIENT_EMAIL", "GOOGLE_PRIVATE_KEY", "GOOGLE_CLOUD_PROJECT"),
121
+ create: ({ env, purpose, fetch: fetch2 }) => vertexProvider({
122
+ clientEmail: env.GOOGLE_CLIENT_EMAIL,
123
+ privateKey: env.GOOGLE_PRIVATE_KEY,
124
+ tokenUri: env.GOOGLE_TOKEN_URI,
125
+ project: env.GOOGLE_CLOUD_PROJECT,
126
+ model: textModels(env, purpose).vertex,
127
+ fetchImpl: fetch2
128
+ })
129
+ }
130
+ ];
131
+ var DEFAULT_ORDER = BUILTIN_ADAPTERS.map((adapter) => adapter.id);
132
+ function configuredOrder(env, purpose) {
133
+ const raw = (purpose === "blog" ? env.AI_PROVIDER_ORDER_BLOG : env.AI_PROVIDER_ORDER_CHAT) ?? env.AI_PROVIDER_ORDER;
134
+ if (!raw) return null;
135
+ const ids = raw.split(",").map((id) => id.trim().toLowerCase()).filter(Boolean);
136
+ return ids.length ? [...new Set(ids)] : null;
137
+ }
138
+ function providerStatuses(env, purpose, adapters = BUILTIN_ADAPTERS) {
139
+ const order = configuredOrder(env, purpose);
140
+ const ids = order ?? adapters.map((adapter) => adapter.id);
141
+ const byId = new Map(adapters.map((adapter) => [adapter.id, adapter]));
142
+ let position = 0;
143
+ const statuses = [];
144
+ for (const id of ids) {
145
+ const adapter = byId.get(id);
146
+ if (!adapter) {
147
+ statuses.push({ id, label: id, paid: false, state: "unknown", missing: [] });
148
+ continue;
149
+ }
150
+ const missing = adapter.missing(env);
151
+ const paid = adapter.paid(env);
152
+ const state = missing.length ? "not_configured" : paid && !paidAllowed(env) ? "needs_paid" : "active";
153
+ statuses.push({ id, label: adapter.label, paid, state, missing, ...state === "active" ? { position: position++ } : {} });
154
+ }
155
+ for (const adapter of adapters) {
156
+ if (!ids.includes(adapter.id)) {
157
+ statuses.push({ id: adapter.id, label: adapter.label, paid: adapter.paid(env), state: "not_in_order", missing: adapter.missing(env) });
158
+ }
159
+ }
160
+ return statuses;
161
+ }
162
+ function chainPlan(env, purpose, adapters = BUILTIN_ADAPTERS) {
163
+ const byId = new Map(adapters.map((adapter) => [adapter.id, adapter]));
164
+ return providerStatuses(env, purpose, adapters).filter((status) => status.state === "active").map((status) => byId.get(status.id));
165
+ }
166
+ var CHECK_PROMPT = { system: "You are a connectivity check. Reply with the single word OK.", user: "Reply with OK." };
167
+ async function checkProvider(env, id, options = {}) {
168
+ const now = options.now ?? Date.now;
169
+ const adapter = (options.adapters ?? BUILTIN_ADAPTERS).find((candidate) => candidate.id === id);
170
+ if (!adapter) return { id, ok: false, errorClass: "unknown_provider", ms: 0 };
171
+ if (adapter.missing(env).length) return { id, ok: false, errorClass: "not_configured", ms: 0 };
172
+ const started = now();
173
+ try {
174
+ const provider = adapter.create({ env, purpose: options.purpose ?? "chat", fetch: options.fetch });
175
+ const outcome = await provider.generate(CHECK_PROMPT, { maxTokens: 32, signal: AbortSignal.timeout(options.timeoutMs ?? 2e4) });
176
+ return outcome.ok ? { id, ok: true, provider: provider.name, ms: now() - started } : { id, ok: false, provider: provider.name, errorClass: outcome.errorClass, ms: now() - started };
177
+ } catch {
178
+ return { id, ok: false, errorClass: "transport", ms: now() - started };
179
+ }
180
+ }
181
+ async function checkChain(env, purpose, options = {}) {
182
+ const now = options.now ?? Date.now;
183
+ const id = `chain:${purpose}`;
184
+ const providers = realProviders(env, purpose, options.fetch, options.adapters);
185
+ if (providers.length === 0) return { id, ok: false, errorClass: "no_provider", ms: 0, attempts: [] };
186
+ const started = now();
187
+ const result = await createAiService({
188
+ providers,
189
+ timeoutMs: 15e3,
190
+ deadlineMs: options.deadlineMs ?? 4e4,
191
+ logger: { warn: () => void 0 }
192
+ }).generate(CHECK_PROMPT, { maxTokens: 32 });
193
+ return {
194
+ id,
195
+ ok: result.ok,
196
+ ...result.provider ? { provider: result.provider } : {},
197
+ ...result.ok ? {} : { errorClass: result.errorClass },
198
+ ms: now() - started,
199
+ attempts: result.attempts.map(({ provider, ok, errorClass }) => ({ provider, ok, ...errorClass ? { errorClass } : {} }))
200
+ };
201
+ }
202
+ function realProviders(env, purpose, fetchImpl, adapters = BUILTIN_ADAPTERS) {
203
+ const explicit = configuredOrder(env, purpose) !== null;
204
+ return chainPlan(env, purpose, adapters).map((adapter, index) => {
205
+ const provider = adapter.create({ env, purpose, fetch: fetchImpl });
206
+ return explicit ? { name: provider.name, generate: provider.generate, priority: index } : { name: provider.name, generate: provider.generate, lastResort: adapter.paid(env) || provider.lastResort };
207
+ });
208
+ }
209
+
210
+ // src/providers.ts
211
+ function jsonConfig(jsonMode) {
212
+ if (!jsonMode) return {};
213
+ return typeof jsonMode === "object" ? { responseMimeType: "application/json", responseSchema: jsonMode.schema } : { responseMimeType: "application/json" };
214
+ }
215
+ var AI_TIMEOUT_MS = 25e3;
216
+ var AI_COOLDOWN_MS = 6e4;
217
+ async function attempt(provider, prompt, maxTokens, ms, cancel, jsonMode) {
218
+ const controller = new AbortController();
219
+ const onCancel = () => controller.abort();
220
+ cancel.addEventListener("abort", onCancel, { once: true });
221
+ let timer;
222
+ const timeout = new Promise((resolve) => {
223
+ timer = setTimeout(() => {
224
+ controller.abort();
225
+ resolve({ ok: false, errorClass: "timeout", retryable: true });
226
+ }, ms);
227
+ });
228
+ try {
229
+ return await Promise.race([provider.generate(prompt, { maxTokens, signal: controller.signal, jsonMode }), timeout]);
230
+ } catch {
231
+ return { ok: false, errorClass: "transport", retryable: true };
232
+ } finally {
233
+ clearTimeout(timer);
234
+ cancel.removeEventListener("abort", onCancel);
235
+ }
236
+ }
237
+ var createAiHealth = () => ({ cooldownUntil: /* @__PURE__ */ new Map(), latencyMs: /* @__PURE__ */ new Map() });
238
+ var sharedAiHealth = globalThis.sahanAiHealth ??= createAiHealth();
239
+ function cooldownFor(outcome) {
240
+ if (outcome.ok) return 0;
241
+ if (outcome.errorClass === "timeout" || outcome.status === 429) return AI_COOLDOWN_MS;
242
+ if (outcome.status === 401 || outcome.status === 403 || outcome.status === 404) return AI_COOLDOWN_MS * 10;
243
+ return 0;
244
+ }
245
+ var HEDGE = /* @__PURE__ */ Symbol("hedge");
246
+ function createAiService(deps) {
247
+ const timeoutMs = deps.timeoutMs ?? AI_TIMEOUT_MS;
248
+ const now = deps.now ?? Date.now;
249
+ const health = deps.health ?? createAiHealth();
250
+ const logger = deps.logger ?? consoleAiLogger;
251
+ function rank(at) {
252
+ const cooling = (p) => Number((health.cooldownUntil.get(p.name) ?? 0) > at);
253
+ const speed = (p) => health.latencyMs.get(p.name) ?? Number.POSITIVE_INFINITY;
254
+ const costly = (p) => Number(Boolean(p.lastResort));
255
+ const chosen = (a, b) => a.priority !== void 0 && b.priority !== void 0 ? a.priority - b.priority : speed(a) - speed(b);
256
+ return [...deps.providers].sort((a, b) => costly(a) - costly(b) || cooling(a) - cooling(b) || chosen(a, b));
257
+ }
258
+ async function generate(prompt, options) {
259
+ if (deps.providers.length === 0) {
260
+ return { ok: false, provider: null, errorClass: "no_provider", attempts: [] };
261
+ }
262
+ const external = options?.signal;
263
+ if (external?.aborted) return { ok: false, provider: null, errorClass: "aborted", attempts: [] };
264
+ const startedAll = now();
265
+ const ordered = rank(startedAll);
266
+ const attempts = [];
267
+ const cancelLosers = new AbortController();
268
+ const running = /* @__PURE__ */ new Set();
269
+ let next = 0;
270
+ let stopped = false;
271
+ let result = null;
272
+ let rejected;
273
+ let onAbort;
274
+ const aborted = new Promise((resolve) => {
275
+ onAbort = () => {
276
+ stopped = true;
277
+ cancelLosers.abort();
278
+ resolve();
279
+ };
280
+ external?.addEventListener("abort", onAbort, { once: true });
281
+ });
282
+ const launch = () => {
283
+ if (result || stopped || next >= ordered.length) return false;
284
+ const remaining = deps.deadlineMs === void 0 ? timeoutMs : deps.deadlineMs - (now() - startedAll);
285
+ if (remaining <= 250) return false;
286
+ const provider = ordered[next++];
287
+ options?.onAttempt?.({ provider: provider.name, stage: "start" });
288
+ const started = now();
289
+ const run = attempt(provider, prompt, options?.maxTokens, Math.min(timeoutMs, remaining), cancelLosers.signal, options?.jsonMode).then((raw) => {
290
+ if (result || external?.aborted) return;
291
+ const ms = now() - started;
292
+ let outcome = raw;
293
+ if (outcome.ok && options?.accept) {
294
+ const reason = options.accept(outcome.text);
295
+ if (reason) {
296
+ rejected = { provider: provider.name, text: outcome.text, reason };
297
+ outcome = { ok: false, errorClass: "invalid_output", retryable: true };
298
+ }
299
+ }
300
+ if (outcome.ok) {
301
+ health.cooldownUntil.delete(provider.name);
302
+ const previous = health.latencyMs.get(provider.name);
303
+ health.latencyMs.set(provider.name, previous === void 0 ? ms : Math.round(previous * 0.7 + ms * 0.3));
304
+ attempts.push({ provider: provider.name, ok: true, ms });
305
+ result = { ok: true, provider: provider.name, text: outcome.text, attempts };
306
+ options?.onAttempt?.({ provider: provider.name, stage: "success" });
307
+ cancelLosers.abort();
308
+ return;
309
+ }
310
+ attempts.push({ provider: provider.name, ok: false, errorClass: outcome.errorClass, ms });
311
+ const fallbackTo = next < ordered.length ? ordered[next].name : void 0;
312
+ options?.onAttempt?.({
313
+ provider: provider.name,
314
+ stage: fallbackTo ? "fallback" : "failure",
315
+ errorClass: outcome.errorClass,
316
+ fallbackTo
317
+ });
318
+ const cooldown = cooldownFor(outcome);
319
+ if (cooldown) health.cooldownUntil.set(provider.name, now() + cooldown);
320
+ logger.warn("ai provider failed", {
321
+ provider: provider.name,
322
+ errorClass: outcome.errorClass,
323
+ ms,
324
+ ...outcome.errorClass === "invalid_output" && rejected ? { reason: rejected.reason.slice(0, 160) } : {}
325
+ });
326
+ if (!outcome.retryable) stopped = true;
327
+ });
328
+ const tracked = run.finally(() => running.delete(tracked));
329
+ running.add(tracked);
330
+ return true;
331
+ };
332
+ try {
333
+ while (!result && !external?.aborted) {
334
+ if (running.size === 0 && !launch()) break;
335
+ let timer;
336
+ const racers = [...running, aborted];
337
+ if (deps.hedgeAfterMs !== void 0 && next < ordered.length) {
338
+ racers.push(new Promise((resolve) => timer = setTimeout(() => resolve(HEDGE), deps.hedgeAfterMs)));
339
+ }
340
+ const first = await Promise.race(racers);
341
+ clearTimeout(timer);
342
+ if (first === HEDGE) launch();
343
+ }
344
+ } finally {
345
+ if (onAbort) external?.removeEventListener("abort", onAbort);
346
+ }
347
+ if (result) return result;
348
+ if (external?.aborted) return { ok: false, provider: null, errorClass: "aborted", attempts, rejected };
349
+ return { ok: false, provider: null, errorClass: attempts[attempts.length - 1]?.errorClass ?? "deadline", attempts, rejected };
350
+ }
351
+ return { generate };
352
+ }
353
+ function httpOutcome(status) {
354
+ return { ok: false, errorClass: `http_${status}`, retryable: true, status };
355
+ }
356
+ var FREE_SUFFIX = ":free";
357
+ function openRouterProvider(config) {
358
+ const model = config.model || DEFAULT_TEXT_MODELS.blog.openrouter;
359
+ const client = createOpenAI({
360
+ apiKey: config.apiKey,
361
+ baseURL: config.baseUrl || "https://openrouter.ai/api/v1",
362
+ fetch: config.fetch
363
+ });
364
+ return {
365
+ name: config.name ?? "openrouter",
366
+ async generate(prompt, options) {
367
+ if (!config.allowPaidModels && !model.endsWith(FREE_SUFFIX)) {
368
+ return { ok: false, errorClass: "paid_model_blocked", retryable: true };
369
+ }
370
+ return sdkGenerate(client.chat(model), prompt, options);
371
+ }
372
+ };
373
+ }
374
+ async function sdkGenerate(model, prompt, options) {
375
+ try {
376
+ const result = await generateText({
377
+ model,
378
+ system: prompt.system,
379
+ prompt: prompt.user,
380
+ maxOutputTokens: options?.maxTokens ?? 1200,
381
+ maxRetries: 0,
382
+ abortSignal: options?.signal
383
+ });
384
+ if (!result.text) return { ok: false, errorClass: "empty_response", retryable: true };
385
+ if (result.finishReason === "length") return { ok: false, errorClass: "truncated", retryable: true };
386
+ return { ok: true, text: result.text };
387
+ } catch (error) {
388
+ const status = error?.statusCode ?? error?.status;
389
+ if (status) return httpOutcome(status);
390
+ return { ok: false, errorClass: options?.signal?.aborted ? "timeout" : "provider_error", retryable: true };
391
+ }
392
+ }
393
+ function openAiCompatibleProvider(config) {
394
+ const client = createOpenAI({ apiKey: config.apiKey, baseURL: config.baseUrl, headers: config.headers, fetch: config.fetch });
395
+ return {
396
+ name: config.name,
397
+ generate: (prompt, options) => sdkGenerate(client.chat(config.model), prompt, options)
398
+ };
399
+ }
400
+ function nvidiaProvider(config) {
401
+ return openAiCompatibleProvider({
402
+ name: "nvidia",
403
+ apiKey: config.apiKey,
404
+ baseUrl: "https://integrate.api.nvidia.com/v1",
405
+ model: config.model || DEFAULT_TEXT_MODELS.blog.nvidia,
406
+ fetch: config.fetch
407
+ });
408
+ }
409
+ function anthropicOutcome(data) {
410
+ const reply = data;
411
+ if (reply.stop_reason === "max_tokens") return { ok: false, errorClass: "truncated", retryable: true };
412
+ if (reply.stop_reason === "refusal") return { ok: false, errorClass: "refused", retryable: true };
413
+ const text = reply.content?.filter((block) => block.type === "text").map((block) => block.text ?? "").join("") ?? "";
414
+ if (!text) return { ok: false, errorClass: "empty_response", retryable: true };
415
+ return { ok: true, text };
416
+ }
417
+ function anthropicProvider(config) {
418
+ const fetchImpl = config.fetchImpl ?? fetch;
419
+ const base = (config.baseUrl || "https://api.anthropic.com").replace(/\/+$/, "");
420
+ return {
421
+ name: "anthropic",
422
+ async generate(prompt, options) {
423
+ try {
424
+ const system = options?.jsonMode ? `${prompt.system}
425
+
426
+ Reply with one JSON value only, no prose and no code fences.` : prompt.system;
427
+ const response = await fetchImpl(`${base}/v1/messages`, {
428
+ method: "POST",
429
+ headers: { "content-type": "application/json", "x-api-key": config.apiKey, "anthropic-version": "2023-06-01" },
430
+ signal: options?.signal,
431
+ body: JSON.stringify({
432
+ model: config.model,
433
+ max_tokens: options?.maxTokens ?? 1800,
434
+ system,
435
+ messages: [{ role: "user", content: prompt.user }]
436
+ })
437
+ });
438
+ if (!response.ok) return httpOutcome(response.status);
439
+ return anthropicOutcome(await response.json());
440
+ } catch {
441
+ return { ok: false, errorClass: options?.signal?.aborted ? "timeout" : "transport", retryable: true };
442
+ }
443
+ }
444
+ };
445
+ }
446
+ function geminiOutcome(data) {
447
+ const candidate = data.candidates?.[0];
448
+ const text = candidate?.content?.parts?.filter((part) => !part.thought).map((part) => part.text ?? "").join("") ?? "";
449
+ if (candidate?.finishReason === "MAX_TOKENS") return { ok: false, errorClass: "truncated", retryable: true };
450
+ if (!text) return { ok: false, errorClass: "empty_response", retryable: true };
451
+ return { ok: true, text };
452
+ }
453
+ function geminiProvider(config) {
454
+ const model = config.model || DEFAULT_TEXT_MODELS.blog.gemini;
455
+ const fetchImpl = config.fetchImpl ?? fetch;
456
+ return {
457
+ name: "gemini",
458
+ async generate(prompt, options) {
459
+ try {
460
+ const maxOutputTokens = options?.maxTokens ?? 2400;
461
+ const generationConfig = {
462
+ maxOutputTokens,
463
+ thinkingConfig: thinkingConfigFor(model, maxOutputTokens),
464
+ ...jsonConfig(options?.jsonMode)
465
+ };
466
+ let response = await fetchImpl(
467
+ `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent`,
468
+ {
469
+ method: "POST",
470
+ // Header, not ?key=: query strings end up in proxy and access logs.
471
+ headers: { "content-type": "application/json", "x-goog-api-key": config.apiKey },
472
+ signal: options?.signal,
473
+ body: JSON.stringify({
474
+ systemInstruction: { parts: [{ text: prompt.system }] },
475
+ contents: [{ role: "user", parts: [{ text: prompt.user }] }],
476
+ generationConfig
477
+ })
478
+ }
479
+ );
480
+ if (response.status === 503 && !options?.signal?.aborted) {
481
+ await new Promise((r) => setTimeout(r, 500));
482
+ if (!options?.signal?.aborted) {
483
+ response = await fetchImpl(
484
+ `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent`,
485
+ {
486
+ method: "POST",
487
+ headers: { "content-type": "application/json", "x-goog-api-key": config.apiKey },
488
+ signal: options?.signal,
489
+ body: JSON.stringify({
490
+ systemInstruction: { parts: [{ text: prompt.system }] },
491
+ contents: [{ role: "user", parts: [{ text: prompt.user }] }],
492
+ generationConfig
493
+ })
494
+ }
495
+ );
496
+ }
497
+ }
498
+ if (!response.ok) return httpOutcome(response.status);
499
+ return geminiOutcome(await response.json());
500
+ } catch {
501
+ return { ok: false, errorClass: options?.signal?.aborted ? "timeout" : "transport", retryable: true };
502
+ }
503
+ }
504
+ };
505
+ }
506
+ function vertexProvider(config) {
507
+ const model = config.model || DEFAULT_TEXT_MODELS.blog.vertex;
508
+ const location = config.location ?? (/^gemini-[3-9]/.test(model) ? "global" : "us-central1");
509
+ const host = location === "global" ? "aiplatform.googleapis.com" : `${location}-aiplatform.googleapis.com`;
510
+ const fetchImpl = config.fetchImpl ?? fetch;
511
+ return {
512
+ name: "vertex",
513
+ lastResort: true,
514
+ async generate(prompt, options) {
515
+ try {
516
+ const accessToken = await getVertexAccessToken(
517
+ { clientEmail: config.clientEmail, privateKey: config.privateKey, tokenUri: config.tokenUri },
518
+ fetchImpl
519
+ );
520
+ const response = await fetchImpl(
521
+ `https://${host}/v1/projects/${config.project}/locations/${location}/publishers/google/models/${model}:generateContent`,
522
+ {
523
+ method: "POST",
524
+ headers: { "content-type": "application/json", authorization: `Bearer ${accessToken}` },
525
+ signal: options?.signal,
526
+ body: JSON.stringify({
527
+ systemInstruction: { parts: [{ text: prompt.system }] },
528
+ contents: [{ role: "user", parts: [{ text: prompt.user }] }],
529
+ generationConfig: {
530
+ maxOutputTokens: options?.maxTokens ?? 1800,
531
+ thinkingConfig: thinkingConfigFor(model, options?.maxTokens ?? 1800),
532
+ ...jsonConfig(options?.jsonMode)
533
+ }
534
+ })
535
+ }
536
+ );
537
+ if (!response.ok) return httpOutcome(response.status);
538
+ return geminiOutcome(await response.json());
539
+ } catch {
540
+ return { ok: false, errorClass: "transport", retryable: true };
541
+ }
542
+ }
543
+ };
544
+ }
545
+
546
+ export {
547
+ AI_TIMEOUT_MS,
548
+ AI_COOLDOWN_MS,
549
+ createAiHealth,
550
+ sharedAiHealth,
551
+ HEDGE,
552
+ createAiService,
553
+ openRouterProvider,
554
+ openAiCompatibleProvider,
555
+ nvidiaProvider,
556
+ anthropicOutcome,
557
+ anthropicProvider,
558
+ geminiOutcome,
559
+ geminiProvider,
560
+ vertexProvider,
561
+ PAID_TEXT_MODELS,
562
+ BUILTIN_ADAPTERS,
563
+ DEFAULT_ORDER,
564
+ configuredOrder,
565
+ providerStatuses,
566
+ chainPlan,
567
+ CHECK_PROMPT,
568
+ checkProvider,
569
+ checkChain,
570
+ realProviders
571
+ };
@@ -0,0 +1,61 @@
1
+ // src/models.ts
2
+ var DEFAULT_TEXT_MODELS = {
3
+ // Long structured JSON: a full flash model, not lite.
4
+ blog: {
5
+ gemini: "gemini-2.5-flash",
6
+ openrouter: "nvidia/nemotron-3-super-120b-a12b:free",
7
+ nvidia: "nvidia/nemotron-3-super-120b-a12b",
8
+ vertex: "gemini-2.5-flash"
9
+ },
10
+ // Short, latency-sensitive replies: the fastest free model first.
11
+ chat: {
12
+ gemini: "gemini-3.1-flash-lite",
13
+ openrouter: "nvidia/nemotron-3-super-120b-a12b:free",
14
+ nvidia: "nvidia/nemotron-3-super-120b-a12b",
15
+ vertex: "gemini-2.5-flash"
16
+ }
17
+ };
18
+ var DEFAULT_IMAGE_MODELS = {
19
+ nvidia: "black-forest-labs/flux.1-dev",
20
+ gemini: "gemini-3.1-flash-image",
21
+ vertex: "gemini-3.1-flash-image"
22
+ };
23
+ var pick = (...values) => values.find((value) => value && value.trim()).trim();
24
+ function textModels(env, purpose) {
25
+ const defaults = DEFAULT_TEXT_MODELS[purpose];
26
+ const own = purpose === "blog" ? { gemini: env.BLOG_GEMINI_MODEL, openrouter: env.BLOG_OPENROUTER_MODEL, nvidia: env.BLOG_NVIDIA_MODEL, vertex: env.BLOG_VERTEX_MODEL } : { gemini: env.CHAT_GEMINI_MODEL, openrouter: env.CHAT_OPENROUTER_MODEL, nvidia: env.CHAT_NVIDIA_MODEL, vertex: env.CHAT_VERTEX_MODEL };
27
+ return {
28
+ gemini: pick(own.gemini, env.GEMINI_MODEL, defaults.gemini),
29
+ openrouter: pick(own.openrouter, env.OPENROUTER_MODEL, defaults.openrouter),
30
+ nvidia: pick(own.nvidia, env.NVIDIA_MODEL, defaults.nvidia),
31
+ vertex: pick(own.vertex, env.VERTEX_MODEL, defaults.vertex)
32
+ };
33
+ }
34
+ function imageModels(env) {
35
+ return {
36
+ nvidia: pick(env.IMAGE_NVIDIA_MODEL, DEFAULT_IMAGE_MODELS.nvidia),
37
+ gemini: pick(env.IMAGE_GEMINI_MODEL, DEFAULT_IMAGE_MODELS.gemini),
38
+ vertex: pick(env.IMAGE_VERTEX_MODEL, env.IMAGEN_MODEL, DEFAULT_IMAGE_MODELS.vertex)
39
+ };
40
+ }
41
+ var paidAllowed = (env) => env.AI_ALLOW_PAID === true;
42
+ function vertexConfigured(env) {
43
+ return Boolean(env.GOOGLE_CLIENT_EMAIL && env.GOOGLE_PRIVATE_KEY && env.GOOGLE_CLOUD_PROJECT);
44
+ }
45
+ function thinkingConfigFor(model, maxOutputTokens) {
46
+ const long = maxOutputTokens >= 2e3;
47
+ if (/^gemini-[3-9]/.test(model)) {
48
+ return { thinkingLevel: long ? "low" : /flash/.test(model) ? "minimal" : "low" };
49
+ }
50
+ return { thinkingBudget: long ? Math.min(800, Math.floor(maxOutputTokens / 5)) : 0 };
51
+ }
52
+
53
+ export {
54
+ DEFAULT_TEXT_MODELS,
55
+ DEFAULT_IMAGE_MODELS,
56
+ textModels,
57
+ imageModels,
58
+ paidAllowed,
59
+ vertexConfigured,
60
+ thinkingConfigFor
61
+ };