@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.
@@ -32,6 +32,12 @@ interface AiProvider {
32
32
  readonly name: string;
33
33
  /** Paid or otherwise costly: tried only after every other provider, whatever its speed or health. */
34
34
  readonly lastResort?: boolean;
35
+ /**
36
+ * Position in an order the operator chose (AI_PROVIDER_ORDER): lower goes
37
+ * first, and speed no longer reorders it. A cooling provider still drops
38
+ * behind the healthy ones.
39
+ */
40
+ readonly priority?: number;
35
41
  generate(prompt: ModelPrompt, options?: AiGenerateOptions): Promise<AiOutcome>;
36
42
  }
37
43
  interface AiAttempt {
@@ -106,6 +112,13 @@ declare function createAiService(deps: AiServiceDeps): {
106
112
  * (malformed JSON from a weak model) from beating a slower valid one.
107
113
  */
108
114
  accept?: (text: string) => string | null;
115
+ /**
116
+ * The caller gave up (an admin cancelled, a client disconnected): every
117
+ * running attempt is aborted, no further provider starts, and the result
118
+ * is `errorClass: "aborted"`. An aborted attempt is not the provider's
119
+ * fault, so it is neither logged nor cooled down.
120
+ */
121
+ signal?: AbortSignal;
109
122
  }) => Promise<AiResult>;
110
123
  };
111
124
  /** OpenRouter, OpenAI-compatible. Refuses a paid model id unless the owner opted in. */
@@ -117,12 +130,33 @@ declare function openRouterProvider(config: {
117
130
  fetch?: typeof fetch;
118
131
  name?: string;
119
132
  }): AiProvider;
133
+ /**
134
+ * Any OpenAI-compatible chat completions API: OpenAI itself, DeepSeek, xAI
135
+ * (Grok), Perplexity, a self-hosted gateway (vLLM, Ollama, LiteLLM).
136
+ */
137
+ declare function openAiCompatibleProvider(config: {
138
+ name: string;
139
+ apiKey: string;
140
+ baseUrl: string;
141
+ model: string;
142
+ headers?: Record<string, string>;
143
+ fetch?: typeof fetch;
144
+ }): AiProvider;
120
145
  /** NVIDIA NIM, OpenAI-compatible. */
121
146
  declare function nvidiaProvider(config: {
122
147
  apiKey: string;
123
148
  model?: string;
124
149
  fetch?: typeof fetch;
125
150
  }): AiProvider;
151
+ /** Text of an Anthropic Messages reply; a reply stopped at the token cap is "truncated", a refusal "refused". */
152
+ declare function anthropicOutcome(data: unknown): AiOutcome;
153
+ /** Anthropic's Messages API over plain REST (no extra SDK for one provider). */
154
+ declare function anthropicProvider(config: {
155
+ apiKey: string;
156
+ model: string;
157
+ baseUrl?: string;
158
+ fetchImpl?: typeof fetch;
159
+ }): AiProvider;
126
160
  /** Text of a Gemini/Vertex generateContent reply; a reply stopped at the token cap is "truncated", not a success. */
127
161
  declare function geminiOutcome(data: unknown): AiOutcome;
128
162
  /** Gemini's own REST API (not OpenAI-compatible), called directly so no extra SDK is added for one provider. */
@@ -144,14 +178,116 @@ declare function vertexProvider(config: {
144
178
  model?: string;
145
179
  fetchImpl?: typeof fetch;
146
180
  }): AiProvider;
181
+
182
+ interface AdapterContext {
183
+ env: AiEnv;
184
+ purpose: TextPurpose;
185
+ fetch?: typeof fetch;
186
+ }
187
+ interface ProviderAdapter {
188
+ /** Lowercase id used in AI_PROVIDER_ORDER. */
189
+ readonly id: string;
190
+ readonly label: string;
191
+ /** Costs money: runs only with AI_ALLOW_PAID. */
192
+ paid(env: AiEnv): boolean;
193
+ /** Env variable names still missing; empty means configured. Never values. */
194
+ missing(env: AiEnv): string[];
195
+ create(context: AdapterContext): AiProvider;
196
+ }
197
+ /**
198
+ * Defaults for the paid adapters. Model names change often: set the
199
+ * provider's *_MODEL variable rather than relying on these.
200
+ */
201
+ declare const PAID_TEXT_MODELS: {
202
+ readonly openai: {
203
+ readonly blog: "gpt-5-mini";
204
+ readonly chat: "gpt-5-nano";
205
+ };
206
+ readonly anthropic: {
207
+ readonly blog: "claude-sonnet-5-5";
208
+ readonly chat: "claude-haiku-4-5-20251001";
209
+ };
210
+ readonly deepseek: {
211
+ readonly blog: "deepseek-chat";
212
+ readonly chat: "deepseek-chat";
213
+ };
214
+ readonly xai: {
215
+ readonly blog: "grok-3-mini";
216
+ readonly chat: "grok-3-mini";
217
+ };
218
+ readonly perplexity: {
219
+ readonly blog: "sonar";
220
+ readonly chat: "sonar";
221
+ };
222
+ };
223
+ declare const BUILTIN_ADAPTERS: readonly ProviderAdapter[];
224
+ /** Free tiers first (fastest, most reliable first), paid last. */
225
+ declare const DEFAULT_ORDER: string[];
226
+ /** The order variable that applies to a purpose, parsed: lowercase ids, no repeats. Null when unset. */
227
+ declare function configuredOrder(env: AiEnv, purpose: TextPurpose): string[] | null;
228
+ type AdapterState = "active" | "not_configured" | "needs_paid" | "not_in_order" | "unknown";
229
+ interface AdapterStatus {
230
+ id: string;
231
+ label: string;
232
+ paid: boolean;
233
+ state: AdapterState;
234
+ /** Position in the chain (0 first) when active. */
235
+ position?: number;
236
+ /** Missing variable names when not configured. */
237
+ missing: string[];
238
+ }
239
+ /** Every adapter's place for a purpose: in the chain or why not. Pure, no network; for health screens. */
240
+ declare function providerStatuses(env: AiEnv, purpose: TextPurpose, adapters?: readonly ProviderAdapter[]): AdapterStatus[];
241
+ /** The adapters that run for a purpose, in order, without building anything. */
242
+ declare function chainPlan(env: AiEnv, purpose: TextPurpose, adapters?: readonly ProviderAdapter[]): ProviderAdapter[];
243
+ /** The prompt a manual check sends: one word back, a few tokens at most. */
244
+ declare const CHECK_PROMPT: {
245
+ readonly system: "You are a connectivity check. Reply with the single word OK.";
246
+ readonly user: "Reply with OK.";
247
+ };
248
+ interface ProviderCheck {
249
+ id: string;
250
+ ok: boolean;
251
+ /** Provider name that answered (for a chain check, the one that won). */
252
+ provider?: string;
253
+ errorClass?: string;
254
+ ms: number;
255
+ /** Every provider tried, for a chain check. */
256
+ attempts?: Array<{
257
+ provider: string;
258
+ ok: boolean;
259
+ errorClass?: string;
260
+ }>;
261
+ }
262
+ /**
263
+ * Sends CHECK_PROMPT through one adapter, whatever the chain order or
264
+ * AI_ALLOW_PAID says: an operator asked for it, and it spends a few tokens.
265
+ * Never throws; a missing variable answers `not_configured` without a call.
266
+ */
267
+ declare function checkProvider(env: AiEnv, id: string, options?: {
268
+ purpose?: TextPurpose;
269
+ fetch?: typeof fetch;
270
+ timeoutMs?: number;
271
+ adapters?: readonly ProviderAdapter[];
272
+ now?: () => number;
273
+ }): Promise<ProviderCheck>;
274
+ /**
275
+ * Sends CHECK_PROMPT through a purpose's whole chain, the same way a real
276
+ * request goes: the first provider that answers wins, and `attempts` shows
277
+ * every provider that failed before it. A fresh health record, so a check
278
+ * never cools down a provider for real traffic.
279
+ */
280
+ declare function checkChain(env: AiEnv, purpose: TextPurpose, options?: {
281
+ fetch?: typeof fetch;
282
+ deadlineMs?: number;
283
+ adapters?: readonly ProviderAdapter[];
284
+ now?: () => number;
285
+ }): Promise<ProviderCheck>;
147
286
  /**
148
- * The chain from decision D15, free tiers first and paid last:
149
- * 1. Gemini (free API key, fast)
150
- * 2. OpenRouter (free-tier models, heavily rate-limited)
151
- * 3. NVIDIA (free developer API; key may be dead or model retired)
152
- * 4. Vertex (pay-as-you-go): only with AI_ALLOW_PAID, and only after the free ones
153
- * Models per purpose come from lib/ai/models.ts (env override, else a verified free default).
287
+ * The provider chain for a purpose. Pass `adapters` to add your own (for
288
+ * example `[...BUILTIN_ADAPTERS, myAdapter]`) and list its id in
289
+ * AI_PROVIDER_ORDER, or rely on the default order (built-ins, then yours).
154
290
  */
155
- declare function realProviders(env: AiEnv, purpose: TextPurpose, fetchImpl?: typeof fetch): AiProvider[];
291
+ declare function realProviders(env: AiEnv, purpose: TextPurpose, fetchImpl?: typeof fetch, adapters?: readonly ProviderAdapter[]): AiProvider[];
156
292
 
157
- export { AI_COOLDOWN_MS, AI_TIMEOUT_MS, type AiAttempt, type AiAttemptStatus, type AiGenerateOptions, type AiHealth, type AiOutcome, type AiProvider, type AiResult, type AiServiceDeps, HEDGE, type JsonMode, createAiHealth, createAiService, geminiOutcome, geminiProvider, nvidiaProvider, openRouterProvider, realProviders, sharedAiHealth, vertexProvider };
293
+ export { type AdapterContext as A, AI_COOLDOWN_MS, AI_TIMEOUT_MS, type AiAttempt, type AiAttemptStatus, type AiGenerateOptions, type AiHealth, type AiOutcome, type AiProvider, type AiResult, type AiServiceDeps, BUILTIN_ADAPTERS as B, CHECK_PROMPT as C, DEFAULT_ORDER as D, HEDGE, type JsonMode, PAID_TEXT_MODELS as P, type AdapterState as a, anthropicOutcome, anthropicProvider, type AdapterStatus as b, type ProviderAdapter as c, createAiHealth, createAiService, type ProviderCheck as d, chainPlan as e, checkChain as f, checkProvider as g, geminiOutcome, geminiProvider, configuredOrder as h, nvidiaProvider, openAiCompatibleProvider, openRouterProvider, providerStatuses as p, realProviders, sharedAiHealth, vertexProvider };
package/dist/providers.js CHANGED
@@ -1,434 +1,35 @@
1
- // src/providers.ts
2
- import "server-only";
3
- import { createOpenAI } from "@ai-sdk/openai";
4
- import { generateText } from "ai";
5
-
6
- // src/log.ts
7
- var consoleAiLogger = {
8
- warn(message, meta) {
9
- console.warn(JSON.stringify({ level: "warn", message, ...meta }));
10
- }
11
- };
12
-
13
- // src/models.ts
14
- var DEFAULT_TEXT_MODELS = {
15
- // Long structured JSON: a full flash model, not lite.
16
- blog: {
17
- gemini: "gemini-2.5-flash",
18
- openrouter: "nvidia/nemotron-3-super-120b-a12b:free",
19
- nvidia: "nvidia/nemotron-3-super-120b-a12b",
20
- vertex: "gemini-2.5-flash"
21
- },
22
- // Short, latency-sensitive replies: the fastest free model first.
23
- chat: {
24
- gemini: "gemini-3.1-flash-lite",
25
- openrouter: "nvidia/nemotron-3-super-120b-a12b:free",
26
- nvidia: "nvidia/nemotron-3-super-120b-a12b",
27
- vertex: "gemini-2.5-flash"
28
- }
29
- };
30
- var pick = (...values) => values.find((value) => value && value.trim()).trim();
31
- function textModels(env, purpose) {
32
- const defaults = DEFAULT_TEXT_MODELS[purpose];
33
- 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 };
34
- return {
35
- gemini: pick(own.gemini, env.GEMINI_MODEL, defaults.gemini),
36
- openrouter: pick(own.openrouter, env.OPENROUTER_MODEL, defaults.openrouter),
37
- nvidia: pick(own.nvidia, env.NVIDIA_MODEL, defaults.nvidia),
38
- vertex: pick(own.vertex, env.VERTEX_MODEL, defaults.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
- // src/vertex.ts
54
- import "server-only";
55
- var cachedToken = null;
56
- function base64UrlEncode(input) {
57
- return Buffer.from(input).toString("base64url");
58
- }
59
- async function getVertexAccessToken(account, fetchImpl = fetch) {
60
- const now = Date.now();
61
- if (cachedToken && cachedToken.expiresAt - 6e4 > now) return cachedToken.accessToken;
62
- const { createSign } = await import("crypto");
63
- const issuedAt = Math.floor(now / 1e3);
64
- const expiresIn = 3600;
65
- const header = base64UrlEncode(JSON.stringify({ alg: "RS256", typ: "JWT" }));
66
- const claims = base64UrlEncode(
67
- JSON.stringify({
68
- iss: account.clientEmail,
69
- scope: "https://www.googleapis.com/auth/cloud-platform",
70
- aud: account.tokenUri,
71
- iat: issuedAt,
72
- exp: issuedAt + expiresIn
73
- })
74
- );
75
- const signingInput = `${header}.${claims}`;
76
- const signature = createSign("RSA-SHA256").update(signingInput).sign(account.privateKey);
77
- const jwt = `${signingInput}.${base64UrlEncode(signature)}`;
78
- const response = await fetchImpl(account.tokenUri, {
79
- method: "POST",
80
- headers: { "content-type": "application/x-www-form-urlencoded" },
81
- body: new URLSearchParams({
82
- grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
83
- assertion: jwt
84
- })
85
- });
86
- if (!response.ok) {
87
- throw new Error(`Vertex token exchange failed: HTTP ${response.status}`);
88
- }
89
- const data = await response.json();
90
- cachedToken = { accessToken: data.access_token, expiresAt: now + data.expires_in * 1e3 };
91
- return data.access_token;
92
- }
93
-
94
- // src/providers.ts
95
- function jsonConfig(jsonMode) {
96
- if (!jsonMode) return {};
97
- return typeof jsonMode === "object" ? { responseMimeType: "application/json", responseSchema: jsonMode.schema } : { responseMimeType: "application/json" };
98
- }
99
- var AI_TIMEOUT_MS = 25e3;
100
- var AI_COOLDOWN_MS = 6e4;
101
- async function attempt(provider, prompt, maxTokens, ms, cancel, jsonMode) {
102
- const controller = new AbortController();
103
- const onCancel = () => controller.abort();
104
- cancel.addEventListener("abort", onCancel, { once: true });
105
- let timer;
106
- const timeout = new Promise((resolve) => {
107
- timer = setTimeout(() => {
108
- controller.abort();
109
- resolve({ ok: false, errorClass: "timeout", retryable: true });
110
- }, ms);
111
- });
112
- try {
113
- return await Promise.race([provider.generate(prompt, { maxTokens, signal: controller.signal, jsonMode }), timeout]);
114
- } catch {
115
- return { ok: false, errorClass: "transport", retryable: true };
116
- } finally {
117
- clearTimeout(timer);
118
- cancel.removeEventListener("abort", onCancel);
119
- }
120
- }
121
- var createAiHealth = () => ({ cooldownUntil: /* @__PURE__ */ new Map(), latencyMs: /* @__PURE__ */ new Map() });
122
- var sharedAiHealth = globalThis.sahanAiHealth ??= createAiHealth();
123
- function cooldownFor(outcome) {
124
- if (outcome.ok) return 0;
125
- if (outcome.errorClass === "timeout" || outcome.status === 429) return AI_COOLDOWN_MS;
126
- if (outcome.status === 401 || outcome.status === 403 || outcome.status === 404) return AI_COOLDOWN_MS * 10;
127
- return 0;
128
- }
129
- var HEDGE = /* @__PURE__ */ Symbol("hedge");
130
- function createAiService(deps) {
131
- const timeoutMs = deps.timeoutMs ?? AI_TIMEOUT_MS;
132
- const now = deps.now ?? Date.now;
133
- const health = deps.health ?? createAiHealth();
134
- const logger = deps.logger ?? consoleAiLogger;
135
- function rank(at) {
136
- const cooling = (p) => Number((health.cooldownUntil.get(p.name) ?? 0) > at);
137
- const speed = (p) => health.latencyMs.get(p.name) ?? Number.POSITIVE_INFINITY;
138
- const costly = (p) => Number(Boolean(p.lastResort));
139
- return [...deps.providers].sort((a, b) => costly(a) - costly(b) || cooling(a) - cooling(b) || speed(a) - speed(b));
140
- }
141
- async function generate(prompt, options) {
142
- if (deps.providers.length === 0) {
143
- return { ok: false, provider: null, errorClass: "no_provider", attempts: [] };
144
- }
145
- const startedAll = now();
146
- const ordered = rank(startedAll);
147
- const attempts = [];
148
- const cancelLosers = new AbortController();
149
- const running = /* @__PURE__ */ new Set();
150
- let next = 0;
151
- let stopped = false;
152
- let result = null;
153
- let rejected;
154
- const launch = () => {
155
- if (result || stopped || next >= ordered.length) return false;
156
- const remaining = deps.deadlineMs === void 0 ? timeoutMs : deps.deadlineMs - (now() - startedAll);
157
- if (remaining <= 250) return false;
158
- const provider = ordered[next++];
159
- options?.onAttempt?.({ provider: provider.name, stage: "start" });
160
- const started = now();
161
- const run = attempt(provider, prompt, options?.maxTokens, Math.min(timeoutMs, remaining), cancelLosers.signal, options?.jsonMode).then((raw) => {
162
- if (result) return;
163
- const ms = now() - started;
164
- let outcome = raw;
165
- if (outcome.ok && options?.accept) {
166
- const reason = options.accept(outcome.text);
167
- if (reason) {
168
- rejected = { provider: provider.name, text: outcome.text, reason };
169
- outcome = { ok: false, errorClass: "invalid_output", retryable: true };
170
- }
171
- }
172
- if (outcome.ok) {
173
- health.cooldownUntil.delete(provider.name);
174
- const previous = health.latencyMs.get(provider.name);
175
- health.latencyMs.set(provider.name, previous === void 0 ? ms : Math.round(previous * 0.7 + ms * 0.3));
176
- attempts.push({ provider: provider.name, ok: true, ms });
177
- result = { ok: true, provider: provider.name, text: outcome.text, attempts };
178
- options?.onAttempt?.({ provider: provider.name, stage: "success" });
179
- cancelLosers.abort();
180
- return;
181
- }
182
- attempts.push({ provider: provider.name, ok: false, errorClass: outcome.errorClass, ms });
183
- const fallbackTo = next < ordered.length ? ordered[next].name : void 0;
184
- options?.onAttempt?.({
185
- provider: provider.name,
186
- stage: fallbackTo ? "fallback" : "failure",
187
- errorClass: outcome.errorClass,
188
- fallbackTo
189
- });
190
- const cooldown = cooldownFor(outcome);
191
- if (cooldown) health.cooldownUntil.set(provider.name, now() + cooldown);
192
- logger.warn("ai provider failed", {
193
- provider: provider.name,
194
- errorClass: outcome.errorClass,
195
- ms,
196
- ...outcome.errorClass === "invalid_output" && rejected ? { reason: rejected.reason.slice(0, 160) } : {}
197
- });
198
- if (!outcome.retryable) stopped = true;
199
- });
200
- const tracked = run.finally(() => running.delete(tracked));
201
- running.add(tracked);
202
- return true;
203
- };
204
- while (!result) {
205
- if (running.size === 0 && !launch()) break;
206
- let timer;
207
- const racers = [...running];
208
- if (deps.hedgeAfterMs !== void 0 && next < ordered.length) {
209
- racers.push(new Promise((resolve) => timer = setTimeout(() => resolve(HEDGE), deps.hedgeAfterMs)));
210
- }
211
- const first = await Promise.race(racers);
212
- clearTimeout(timer);
213
- if (first === HEDGE) launch();
214
- }
215
- return result ?? { ok: false, provider: null, errorClass: attempts[attempts.length - 1]?.errorClass ?? "deadline", attempts, rejected };
216
- }
217
- return { generate };
218
- }
219
- function httpOutcome(status) {
220
- return { ok: false, errorClass: `http_${status}`, retryable: true, status };
221
- }
222
- var FREE_SUFFIX = ":free";
223
- function openRouterProvider(config) {
224
- const model = config.model || DEFAULT_TEXT_MODELS.blog.openrouter;
225
- const client = createOpenAI({
226
- apiKey: config.apiKey,
227
- baseURL: config.baseUrl || "https://openrouter.ai/api/v1",
228
- fetch: config.fetch
229
- });
230
- return {
231
- name: config.name ?? "openrouter",
232
- async generate(prompt, options) {
233
- if (!config.allowPaidModels && !model.endsWith(FREE_SUFFIX)) {
234
- return { ok: false, errorClass: "paid_model_blocked", retryable: true };
235
- }
236
- return sdkGenerate(client.chat(model), prompt, options);
237
- }
238
- };
239
- }
240
- async function sdkGenerate(model, prompt, options) {
241
- try {
242
- const result = await generateText({
243
- model,
244
- system: prompt.system,
245
- prompt: prompt.user,
246
- maxOutputTokens: options?.maxTokens ?? 1200,
247
- maxRetries: 0,
248
- abortSignal: options?.signal
249
- });
250
- if (!result.text) return { ok: false, errorClass: "empty_response", retryable: true };
251
- if (result.finishReason === "length") return { ok: false, errorClass: "truncated", retryable: true };
252
- return { ok: true, text: result.text };
253
- } catch (error) {
254
- const status = error?.statusCode ?? error?.status;
255
- if (status) return httpOutcome(status);
256
- return { ok: false, errorClass: options?.signal?.aborted ? "timeout" : "provider_error", retryable: true };
257
- }
258
- }
259
- function nvidiaProvider(config) {
260
- const model = config.model || DEFAULT_TEXT_MODELS.blog.nvidia;
261
- const client = createOpenAI({
262
- apiKey: config.apiKey,
263
- baseURL: "https://integrate.api.nvidia.com/v1",
264
- fetch: config.fetch
265
- });
266
- return {
267
- name: "nvidia",
268
- generate: (prompt, options) => sdkGenerate(client.chat(model), prompt, options)
269
- };
270
- }
271
- function geminiOutcome(data) {
272
- const candidate = data.candidates?.[0];
273
- const text = candidate?.content?.parts?.filter((part) => !part.thought).map((part) => part.text ?? "").join("") ?? "";
274
- if (candidate?.finishReason === "MAX_TOKENS") return { ok: false, errorClass: "truncated", retryable: true };
275
- if (!text) return { ok: false, errorClass: "empty_response", retryable: true };
276
- return { ok: true, text };
277
- }
278
- function geminiProvider(config) {
279
- const model = config.model || DEFAULT_TEXT_MODELS.blog.gemini;
280
- const fetchImpl = config.fetchImpl ?? fetch;
281
- return {
282
- name: "gemini",
283
- async generate(prompt, options) {
284
- try {
285
- const maxOutputTokens = options?.maxTokens ?? 2400;
286
- const generationConfig = {
287
- maxOutputTokens,
288
- thinkingConfig: thinkingConfigFor(model, maxOutputTokens),
289
- ...jsonConfig(options?.jsonMode)
290
- };
291
- let response = await fetchImpl(
292
- `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent`,
293
- {
294
- method: "POST",
295
- // Header, not ?key=: query strings end up in proxy and access logs.
296
- headers: { "content-type": "application/json", "x-goog-api-key": config.apiKey },
297
- signal: options?.signal,
298
- body: JSON.stringify({
299
- systemInstruction: { parts: [{ text: prompt.system }] },
300
- contents: [{ role: "user", parts: [{ text: prompt.user }] }],
301
- generationConfig
302
- })
303
- }
304
- );
305
- if (response.status === 503 && !options?.signal?.aborted) {
306
- await new Promise((r) => setTimeout(r, 500));
307
- if (!options?.signal?.aborted) {
308
- response = await fetchImpl(
309
- `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent`,
310
- {
311
- method: "POST",
312
- headers: { "content-type": "application/json", "x-goog-api-key": config.apiKey },
313
- signal: options?.signal,
314
- body: JSON.stringify({
315
- systemInstruction: { parts: [{ text: prompt.system }] },
316
- contents: [{ role: "user", parts: [{ text: prompt.user }] }],
317
- generationConfig
318
- })
319
- }
320
- );
321
- }
322
- }
323
- if (!response.ok) return httpOutcome(response.status);
324
- return geminiOutcome(await response.json());
325
- } catch {
326
- return { ok: false, errorClass: options?.signal?.aborted ? "timeout" : "transport", retryable: true };
327
- }
328
- }
329
- };
330
- }
331
- function vertexProvider(config) {
332
- const model = config.model || DEFAULT_TEXT_MODELS.blog.vertex;
333
- const location = config.location ?? (/^gemini-[3-9]/.test(model) ? "global" : "us-central1");
334
- const host = location === "global" ? "aiplatform.googleapis.com" : `${location}-aiplatform.googleapis.com`;
335
- const fetchImpl = config.fetchImpl ?? fetch;
336
- return {
337
- name: "vertex",
338
- lastResort: true,
339
- async generate(prompt, options) {
340
- try {
341
- const accessToken = await getVertexAccessToken(
342
- { clientEmail: config.clientEmail, privateKey: config.privateKey, tokenUri: config.tokenUri },
343
- fetchImpl
344
- );
345
- const response = await fetchImpl(
346
- `https://${host}/v1/projects/${config.project}/locations/${location}/publishers/google/models/${model}:generateContent`,
347
- {
348
- method: "POST",
349
- headers: { "content-type": "application/json", authorization: `Bearer ${accessToken}` },
350
- signal: options?.signal,
351
- body: JSON.stringify({
352
- systemInstruction: { parts: [{ text: prompt.system }] },
353
- contents: [{ role: "user", parts: [{ text: prompt.user }] }],
354
- generationConfig: {
355
- maxOutputTokens: options?.maxTokens ?? 1800,
356
- thinkingConfig: thinkingConfigFor(model, options?.maxTokens ?? 1800),
357
- ...jsonConfig(options?.jsonMode)
358
- }
359
- })
360
- }
361
- );
362
- if (!response.ok) return httpOutcome(response.status);
363
- return geminiOutcome(await response.json());
364
- } catch {
365
- return { ok: false, errorClass: "transport", retryable: true };
366
- }
367
- }
368
- };
369
- }
370
- function realProviders(env, purpose, fetchImpl) {
371
- const providers = [];
372
- const models = textModels(env, purpose);
373
- if (env.GEMINI_API_KEY) {
374
- providers.push(geminiProvider({ apiKey: env.GEMINI_API_KEY, model: models.gemini, fetchImpl }));
375
- }
376
- const openRouterModel = models.openrouter;
377
- if (env.OPENROUTER_API_KEY) {
378
- providers.push(
379
- openRouterProvider({
380
- apiKey: env.OPENROUTER_API_KEY,
381
- model: openRouterModel,
382
- baseUrl: env.OPENROUTER_BASE_URL,
383
- allowPaidModels: env.OPENROUTER_ALLOW_PAID_MODELS,
384
- fetch: fetchImpl
385
- })
386
- );
387
- }
388
- if (env.OPENROUTER_API_KEY_2) {
389
- providers.push(
390
- openRouterProvider({
391
- apiKey: env.OPENROUTER_API_KEY_2,
392
- model: openRouterModel,
393
- baseUrl: env.OPENROUTER_BASE_URL,
394
- allowPaidModels: env.OPENROUTER_ALLOW_PAID_MODELS,
395
- fetch: fetchImpl,
396
- name: "openrouter-2"
397
- })
398
- );
399
- }
400
- if (env.NVIDIA_API_KEY) {
401
- providers.push(
402
- nvidiaProvider({
403
- apiKey: env.NVIDIA_API_KEY,
404
- model: models.nvidia,
405
- fetch: fetchImpl
406
- })
407
- );
408
- }
409
- if (paidAllowed(env) && vertexConfigured(env)) {
410
- providers.push(
411
- vertexProvider({
412
- clientEmail: env.GOOGLE_CLIENT_EMAIL,
413
- privateKey: env.GOOGLE_PRIVATE_KEY,
414
- tokenUri: env.GOOGLE_TOKEN_URI,
415
- project: env.GOOGLE_CLOUD_PROJECT,
416
- model: models.vertex,
417
- fetchImpl
418
- })
419
- );
420
- }
421
- return providers;
422
- }
1
+ import {
2
+ AI_COOLDOWN_MS,
3
+ AI_TIMEOUT_MS,
4
+ HEDGE,
5
+ anthropicOutcome,
6
+ anthropicProvider,
7
+ createAiHealth,
8
+ createAiService,
9
+ geminiOutcome,
10
+ geminiProvider,
11
+ nvidiaProvider,
12
+ openAiCompatibleProvider,
13
+ openRouterProvider,
14
+ realProviders,
15
+ sharedAiHealth,
16
+ vertexProvider
17
+ } from "./chunk-R3YY2Y7S.js";
18
+ import "./chunk-BL4J2JNX.js";
19
+ import "./chunk-PMC3UWQ2.js";
20
+ import "./chunk-RBSLNDFW.js";
423
21
  export {
424
22
  AI_COOLDOWN_MS,
425
23
  AI_TIMEOUT_MS,
426
24
  HEDGE,
25
+ anthropicOutcome,
26
+ anthropicProvider,
427
27
  createAiHealth,
428
28
  createAiService,
429
29
  geminiOutcome,
430
30
  geminiProvider,
431
31
  nvidiaProvider,
32
+ openAiCompatibleProvider,
432
33
  openRouterProvider,
433
34
  realProviders,
434
35
  sharedAiHealth,