@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.
- package/CHANGELOG.md +14 -0
- package/README.md +75 -10
- package/dist/adapters.d.ts +6 -0
- package/dist/adapters.js +27 -0
- package/dist/availability.d.ts +8 -3
- package/dist/availability.js +14 -48
- package/dist/chunk-BL4J2JNX.js +48 -0
- package/dist/chunk-MRNQPHWI.js +194 -0
- package/dist/chunk-PMC3UWQ2.js +10 -0
- package/dist/chunk-R3YY2Y7S.js +571 -0
- package/dist/chunk-RBSLNDFW.js +61 -0
- package/dist/chunk-ZIHRZXPE.js +109 -0
- package/dist/env.d.ts +30 -0
- package/dist/env.js +6 -63
- package/dist/guard.d.ts +15 -3
- package/dist/guard.js +0 -0
- package/dist/image.js +14 -236
- package/dist/index.d.ts +1 -1
- package/dist/index.js +5 -63
- package/dist/log.js +3 -6
- package/dist/models.js +9 -51
- package/dist/providers.d.ts +144 -8
- package/dist/providers.js +23 -422
- package/dist/vertex.js +4 -43
- package/package.json +6 -2
package/dist/providers.d.ts
CHANGED
|
@@ -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
|
|
149
|
-
*
|
|
150
|
-
*
|
|
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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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,
|