@sahan-sac/ai-core 0.2.0 → 0.3.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/CHANGELOG.md +8 -0
- package/README.md +75 -10
- package/dist/adapters.d.ts +6 -0
- package/dist/adapters.js +621 -0
- package/dist/availability.d.ts +8 -3
- package/dist/availability.js +423 -8
- package/dist/env.d.ts +30 -0
- package/dist/env.js +39 -0
- package/dist/guard.d.ts +15 -3
- package/dist/guard.js +0 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +39 -0
- package/dist/providers.d.ts +144 -8
- package/dist/providers.js +233 -70
- package/package.json +6 -2
package/dist/availability.js
CHANGED
|
@@ -1,13 +1,36 @@
|
|
|
1
|
-
// src/image.ts
|
|
2
|
-
import "server-only";
|
|
3
|
-
|
|
4
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
|
+
};
|
|
5
18
|
var DEFAULT_IMAGE_MODELS = {
|
|
6
19
|
nvidia: "black-forest-labs/flux.1-dev",
|
|
7
20
|
gemini: "gemini-3.1-flash-image",
|
|
8
21
|
vertex: "gemini-3.1-flash-image"
|
|
9
22
|
};
|
|
10
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
|
+
}
|
|
11
34
|
function imageModels(env) {
|
|
12
35
|
return {
|
|
13
36
|
nvidia: pick(env.IMAGE_NVIDIA_MODEL, DEFAULT_IMAGE_MODELS.nvidia),
|
|
@@ -19,8 +42,400 @@ var paidAllowed = (env) => env.AI_ALLOW_PAID === true;
|
|
|
19
42
|
function vertexConfigured(env) {
|
|
20
43
|
return Boolean(env.GOOGLE_CLIENT_EMAIL && env.GOOGLE_PRIVATE_KEY && env.GOOGLE_CLOUD_PROJECT);
|
|
21
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/providers.ts
|
|
54
|
+
import "server-only";
|
|
55
|
+
import { createOpenAI } from "@ai-sdk/openai";
|
|
56
|
+
import { generateText } from "ai";
|
|
57
|
+
|
|
58
|
+
// src/vertex.ts
|
|
59
|
+
import "server-only";
|
|
60
|
+
var cachedToken = null;
|
|
61
|
+
function base64UrlEncode(input) {
|
|
62
|
+
return Buffer.from(input).toString("base64url");
|
|
63
|
+
}
|
|
64
|
+
async function getVertexAccessToken(account, fetchImpl = fetch) {
|
|
65
|
+
const now = Date.now();
|
|
66
|
+
if (cachedToken && cachedToken.expiresAt - 6e4 > now) return cachedToken.accessToken;
|
|
67
|
+
const { createSign } = await import("crypto");
|
|
68
|
+
const issuedAt = Math.floor(now / 1e3);
|
|
69
|
+
const expiresIn = 3600;
|
|
70
|
+
const header = base64UrlEncode(JSON.stringify({ alg: "RS256", typ: "JWT" }));
|
|
71
|
+
const claims = base64UrlEncode(
|
|
72
|
+
JSON.stringify({
|
|
73
|
+
iss: account.clientEmail,
|
|
74
|
+
scope: "https://www.googleapis.com/auth/cloud-platform",
|
|
75
|
+
aud: account.tokenUri,
|
|
76
|
+
iat: issuedAt,
|
|
77
|
+
exp: issuedAt + expiresIn
|
|
78
|
+
})
|
|
79
|
+
);
|
|
80
|
+
const signingInput = `${header}.${claims}`;
|
|
81
|
+
const signature = createSign("RSA-SHA256").update(signingInput).sign(account.privateKey);
|
|
82
|
+
const jwt = `${signingInput}.${base64UrlEncode(signature)}`;
|
|
83
|
+
const response = await fetchImpl(account.tokenUri, {
|
|
84
|
+
method: "POST",
|
|
85
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
86
|
+
body: new URLSearchParams({
|
|
87
|
+
grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
|
|
88
|
+
assertion: jwt
|
|
89
|
+
})
|
|
90
|
+
});
|
|
91
|
+
if (!response.ok) {
|
|
92
|
+
throw new Error(`Vertex token exchange failed: HTTP ${response.status}`);
|
|
93
|
+
}
|
|
94
|
+
const data = await response.json();
|
|
95
|
+
cachedToken = { accessToken: data.access_token, expiresAt: now + data.expires_in * 1e3 };
|
|
96
|
+
return data.access_token;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// src/providers.ts
|
|
100
|
+
function jsonConfig(jsonMode) {
|
|
101
|
+
if (!jsonMode) return {};
|
|
102
|
+
return typeof jsonMode === "object" ? { responseMimeType: "application/json", responseSchema: jsonMode.schema } : { responseMimeType: "application/json" };
|
|
103
|
+
}
|
|
104
|
+
var createAiHealth = () => ({ cooldownUntil: /* @__PURE__ */ new Map(), latencyMs: /* @__PURE__ */ new Map() });
|
|
105
|
+
var sharedAiHealth = globalThis.sahanAiHealth ??= createAiHealth();
|
|
106
|
+
function httpOutcome(status) {
|
|
107
|
+
return { ok: false, errorClass: `http_${status}`, retryable: true, status };
|
|
108
|
+
}
|
|
109
|
+
var FREE_SUFFIX = ":free";
|
|
110
|
+
function openRouterProvider(config) {
|
|
111
|
+
const model = config.model || DEFAULT_TEXT_MODELS.blog.openrouter;
|
|
112
|
+
const client = createOpenAI({
|
|
113
|
+
apiKey: config.apiKey,
|
|
114
|
+
baseURL: config.baseUrl || "https://openrouter.ai/api/v1",
|
|
115
|
+
fetch: config.fetch
|
|
116
|
+
});
|
|
117
|
+
return {
|
|
118
|
+
name: config.name ?? "openrouter",
|
|
119
|
+
async generate(prompt, options) {
|
|
120
|
+
if (!config.allowPaidModels && !model.endsWith(FREE_SUFFIX)) {
|
|
121
|
+
return { ok: false, errorClass: "paid_model_blocked", retryable: true };
|
|
122
|
+
}
|
|
123
|
+
return sdkGenerate(client.chat(model), prompt, options);
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
async function sdkGenerate(model, prompt, options) {
|
|
128
|
+
try {
|
|
129
|
+
const result = await generateText({
|
|
130
|
+
model,
|
|
131
|
+
system: prompt.system,
|
|
132
|
+
prompt: prompt.user,
|
|
133
|
+
maxOutputTokens: options?.maxTokens ?? 1200,
|
|
134
|
+
maxRetries: 0,
|
|
135
|
+
abortSignal: options?.signal
|
|
136
|
+
});
|
|
137
|
+
if (!result.text) return { ok: false, errorClass: "empty_response", retryable: true };
|
|
138
|
+
if (result.finishReason === "length") return { ok: false, errorClass: "truncated", retryable: true };
|
|
139
|
+
return { ok: true, text: result.text };
|
|
140
|
+
} catch (error) {
|
|
141
|
+
const status = error?.statusCode ?? error?.status;
|
|
142
|
+
if (status) return httpOutcome(status);
|
|
143
|
+
return { ok: false, errorClass: options?.signal?.aborted ? "timeout" : "provider_error", retryable: true };
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
function openAiCompatibleProvider(config) {
|
|
147
|
+
const client = createOpenAI({ apiKey: config.apiKey, baseURL: config.baseUrl, headers: config.headers, fetch: config.fetch });
|
|
148
|
+
return {
|
|
149
|
+
name: config.name,
|
|
150
|
+
generate: (prompt, options) => sdkGenerate(client.chat(config.model), prompt, options)
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
function anthropicOutcome(data) {
|
|
154
|
+
const reply = data;
|
|
155
|
+
if (reply.stop_reason === "max_tokens") return { ok: false, errorClass: "truncated", retryable: true };
|
|
156
|
+
if (reply.stop_reason === "refusal") return { ok: false, errorClass: "refused", retryable: true };
|
|
157
|
+
const text = reply.content?.filter((block) => block.type === "text").map((block) => block.text ?? "").join("") ?? "";
|
|
158
|
+
if (!text) return { ok: false, errorClass: "empty_response", retryable: true };
|
|
159
|
+
return { ok: true, text };
|
|
160
|
+
}
|
|
161
|
+
function anthropicProvider(config) {
|
|
162
|
+
const fetchImpl = config.fetchImpl ?? fetch;
|
|
163
|
+
const base = (config.baseUrl || "https://api.anthropic.com").replace(/\/+$/, "");
|
|
164
|
+
return {
|
|
165
|
+
name: "anthropic",
|
|
166
|
+
async generate(prompt, options) {
|
|
167
|
+
try {
|
|
168
|
+
const system = options?.jsonMode ? `${prompt.system}
|
|
169
|
+
|
|
170
|
+
Reply with one JSON value only, no prose and no code fences.` : prompt.system;
|
|
171
|
+
const response = await fetchImpl(`${base}/v1/messages`, {
|
|
172
|
+
method: "POST",
|
|
173
|
+
headers: { "content-type": "application/json", "x-api-key": config.apiKey, "anthropic-version": "2023-06-01" },
|
|
174
|
+
signal: options?.signal,
|
|
175
|
+
body: JSON.stringify({
|
|
176
|
+
model: config.model,
|
|
177
|
+
max_tokens: options?.maxTokens ?? 1800,
|
|
178
|
+
system,
|
|
179
|
+
messages: [{ role: "user", content: prompt.user }]
|
|
180
|
+
})
|
|
181
|
+
});
|
|
182
|
+
if (!response.ok) return httpOutcome(response.status);
|
|
183
|
+
return anthropicOutcome(await response.json());
|
|
184
|
+
} catch {
|
|
185
|
+
return { ok: false, errorClass: options?.signal?.aborted ? "timeout" : "transport", retryable: true };
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
function geminiOutcome(data) {
|
|
191
|
+
const candidate = data.candidates?.[0];
|
|
192
|
+
const text = candidate?.content?.parts?.filter((part) => !part.thought).map((part) => part.text ?? "").join("") ?? "";
|
|
193
|
+
if (candidate?.finishReason === "MAX_TOKENS") return { ok: false, errorClass: "truncated", retryable: true };
|
|
194
|
+
if (!text) return { ok: false, errorClass: "empty_response", retryable: true };
|
|
195
|
+
return { ok: true, text };
|
|
196
|
+
}
|
|
197
|
+
function geminiProvider(config) {
|
|
198
|
+
const model = config.model || DEFAULT_TEXT_MODELS.blog.gemini;
|
|
199
|
+
const fetchImpl = config.fetchImpl ?? fetch;
|
|
200
|
+
return {
|
|
201
|
+
name: "gemini",
|
|
202
|
+
async generate(prompt, options) {
|
|
203
|
+
try {
|
|
204
|
+
const maxOutputTokens = options?.maxTokens ?? 2400;
|
|
205
|
+
const generationConfig = {
|
|
206
|
+
maxOutputTokens,
|
|
207
|
+
thinkingConfig: thinkingConfigFor(model, maxOutputTokens),
|
|
208
|
+
...jsonConfig(options?.jsonMode)
|
|
209
|
+
};
|
|
210
|
+
let response = await fetchImpl(
|
|
211
|
+
`https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent`,
|
|
212
|
+
{
|
|
213
|
+
method: "POST",
|
|
214
|
+
// Header, not ?key=: query strings end up in proxy and access logs.
|
|
215
|
+
headers: { "content-type": "application/json", "x-goog-api-key": config.apiKey },
|
|
216
|
+
signal: options?.signal,
|
|
217
|
+
body: JSON.stringify({
|
|
218
|
+
systemInstruction: { parts: [{ text: prompt.system }] },
|
|
219
|
+
contents: [{ role: "user", parts: [{ text: prompt.user }] }],
|
|
220
|
+
generationConfig
|
|
221
|
+
})
|
|
222
|
+
}
|
|
223
|
+
);
|
|
224
|
+
if (response.status === 503 && !options?.signal?.aborted) {
|
|
225
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
226
|
+
if (!options?.signal?.aborted) {
|
|
227
|
+
response = await fetchImpl(
|
|
228
|
+
`https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent`,
|
|
229
|
+
{
|
|
230
|
+
method: "POST",
|
|
231
|
+
headers: { "content-type": "application/json", "x-goog-api-key": config.apiKey },
|
|
232
|
+
signal: options?.signal,
|
|
233
|
+
body: JSON.stringify({
|
|
234
|
+
systemInstruction: { parts: [{ text: prompt.system }] },
|
|
235
|
+
contents: [{ role: "user", parts: [{ text: prompt.user }] }],
|
|
236
|
+
generationConfig
|
|
237
|
+
})
|
|
238
|
+
}
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
if (!response.ok) return httpOutcome(response.status);
|
|
243
|
+
return geminiOutcome(await response.json());
|
|
244
|
+
} catch {
|
|
245
|
+
return { ok: false, errorClass: options?.signal?.aborted ? "timeout" : "transport", retryable: true };
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
function vertexProvider(config) {
|
|
251
|
+
const model = config.model || DEFAULT_TEXT_MODELS.blog.vertex;
|
|
252
|
+
const location = config.location ?? (/^gemini-[3-9]/.test(model) ? "global" : "us-central1");
|
|
253
|
+
const host = location === "global" ? "aiplatform.googleapis.com" : `${location}-aiplatform.googleapis.com`;
|
|
254
|
+
const fetchImpl = config.fetchImpl ?? fetch;
|
|
255
|
+
return {
|
|
256
|
+
name: "vertex",
|
|
257
|
+
lastResort: true,
|
|
258
|
+
async generate(prompt, options) {
|
|
259
|
+
try {
|
|
260
|
+
const accessToken = await getVertexAccessToken(
|
|
261
|
+
{ clientEmail: config.clientEmail, privateKey: config.privateKey, tokenUri: config.tokenUri },
|
|
262
|
+
fetchImpl
|
|
263
|
+
);
|
|
264
|
+
const response = await fetchImpl(
|
|
265
|
+
`https://${host}/v1/projects/${config.project}/locations/${location}/publishers/google/models/${model}:generateContent`,
|
|
266
|
+
{
|
|
267
|
+
method: "POST",
|
|
268
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${accessToken}` },
|
|
269
|
+
signal: options?.signal,
|
|
270
|
+
body: JSON.stringify({
|
|
271
|
+
systemInstruction: { parts: [{ text: prompt.system }] },
|
|
272
|
+
contents: [{ role: "user", parts: [{ text: prompt.user }] }],
|
|
273
|
+
generationConfig: {
|
|
274
|
+
maxOutputTokens: options?.maxTokens ?? 1800,
|
|
275
|
+
thinkingConfig: thinkingConfigFor(model, options?.maxTokens ?? 1800),
|
|
276
|
+
...jsonConfig(options?.jsonMode)
|
|
277
|
+
}
|
|
278
|
+
})
|
|
279
|
+
}
|
|
280
|
+
);
|
|
281
|
+
if (!response.ok) return httpOutcome(response.status);
|
|
282
|
+
return geminiOutcome(await response.json());
|
|
283
|
+
} catch {
|
|
284
|
+
return { ok: false, errorClass: "transport", retryable: true };
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// src/adapters.ts
|
|
291
|
+
function modelFor(env, provider, purpose, fallback) {
|
|
292
|
+
const record = env;
|
|
293
|
+
return record[`${purpose.toUpperCase()}_${provider}_MODEL`] || record[`${provider}_MODEL`] || fallback[purpose];
|
|
294
|
+
}
|
|
295
|
+
var need = (env, ...names) => names.filter((name) => !env[name]);
|
|
296
|
+
var PAID_TEXT_MODELS = {
|
|
297
|
+
openai: { blog: "gpt-5-mini", chat: "gpt-5-nano" },
|
|
298
|
+
anthropic: { blog: "claude-sonnet-5-5", chat: "claude-haiku-4-5-20251001" },
|
|
299
|
+
deepseek: { blog: "deepseek-chat", chat: "deepseek-chat" },
|
|
300
|
+
xai: { blog: "grok-3-mini", chat: "grok-3-mini" },
|
|
301
|
+
perplexity: { blog: "sonar", chat: "sonar" }
|
|
302
|
+
};
|
|
303
|
+
function compatibleAdapter(spec) {
|
|
304
|
+
return {
|
|
305
|
+
id: spec.id,
|
|
306
|
+
label: spec.label,
|
|
307
|
+
paid: () => true,
|
|
308
|
+
missing: (env) => need(env, spec.keyVar),
|
|
309
|
+
create: ({ env, purpose, fetch: fetch2 }) => openAiCompatibleProvider({
|
|
310
|
+
name: spec.id,
|
|
311
|
+
apiKey: env[spec.keyVar],
|
|
312
|
+
baseUrl: spec.baseUrlVar && env[spec.baseUrlVar] || spec.baseUrl,
|
|
313
|
+
model: modelFor(env, spec.envPrefix, purpose, PAID_TEXT_MODELS[spec.id]),
|
|
314
|
+
fetch: fetch2
|
|
315
|
+
})
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
var openRouterAdapter = (id, keyVar) => ({
|
|
319
|
+
id,
|
|
320
|
+
label: id === "openrouter" ? "OpenRouter" : "OpenRouter (second key)",
|
|
321
|
+
// Free models only, unless OPENROUTER_ALLOW_PAID_MODELS: the adapter itself refuses a paid model id.
|
|
322
|
+
paid: () => false,
|
|
323
|
+
missing: (env) => need(env, keyVar),
|
|
324
|
+
create: ({ env, purpose, fetch: fetch2 }) => openRouterProvider({
|
|
325
|
+
apiKey: env[keyVar],
|
|
326
|
+
model: textModels(env, purpose).openrouter,
|
|
327
|
+
baseUrl: env.OPENROUTER_BASE_URL,
|
|
328
|
+
allowPaidModels: env.OPENROUTER_ALLOW_PAID_MODELS,
|
|
329
|
+
fetch: fetch2,
|
|
330
|
+
name: id
|
|
331
|
+
})
|
|
332
|
+
});
|
|
333
|
+
var BUILTIN_ADAPTERS = [
|
|
334
|
+
{
|
|
335
|
+
id: "gemini",
|
|
336
|
+
label: "Gemini",
|
|
337
|
+
paid: () => false,
|
|
338
|
+
missing: (env) => need(env, "GEMINI_API_KEY"),
|
|
339
|
+
create: ({ env, purpose, fetch: fetch2 }) => geminiProvider({ apiKey: env.GEMINI_API_KEY, model: textModels(env, purpose).gemini, fetchImpl: fetch2 })
|
|
340
|
+
},
|
|
341
|
+
openRouterAdapter("openrouter", "OPENROUTER_API_KEY"),
|
|
342
|
+
openRouterAdapter("openrouter-2", "OPENROUTER_API_KEY_2"),
|
|
343
|
+
{
|
|
344
|
+
id: "nvidia",
|
|
345
|
+
label: "NVIDIA",
|
|
346
|
+
paid: () => false,
|
|
347
|
+
missing: (env) => need(env, "NVIDIA_API_KEY"),
|
|
348
|
+
create: ({ env, purpose, fetch: fetch2 }) => openAiCompatibleProvider({
|
|
349
|
+
name: "nvidia",
|
|
350
|
+
apiKey: env.NVIDIA_API_KEY,
|
|
351
|
+
baseUrl: "https://integrate.api.nvidia.com/v1",
|
|
352
|
+
model: textModels(env, purpose).nvidia,
|
|
353
|
+
fetch: fetch2
|
|
354
|
+
})
|
|
355
|
+
},
|
|
356
|
+
compatibleAdapter({ id: "openai", label: "OpenAI", envPrefix: "OPENAI", keyVar: "OPENAI_API_KEY", baseUrl: "https://api.openai.com/v1", baseUrlVar: "OPENAI_BASE_URL" }),
|
|
357
|
+
{
|
|
358
|
+
id: "anthropic",
|
|
359
|
+
label: "Anthropic (Claude)",
|
|
360
|
+
paid: () => true,
|
|
361
|
+
missing: (env) => need(env, "ANTHROPIC_API_KEY"),
|
|
362
|
+
create: ({ env, purpose, fetch: fetch2 }) => anthropicProvider({
|
|
363
|
+
apiKey: env.ANTHROPIC_API_KEY,
|
|
364
|
+
baseUrl: env.ANTHROPIC_BASE_URL,
|
|
365
|
+
model: modelFor(env, "ANTHROPIC", purpose, PAID_TEXT_MODELS.anthropic),
|
|
366
|
+
fetchImpl: fetch2
|
|
367
|
+
})
|
|
368
|
+
},
|
|
369
|
+
compatibleAdapter({ id: "deepseek", label: "DeepSeek", envPrefix: "DEEPSEEK", keyVar: "DEEPSEEK_API_KEY", baseUrl: "https://api.deepseek.com/v1" }),
|
|
370
|
+
compatibleAdapter({ id: "xai", label: "xAI (Grok)", envPrefix: "XAI", keyVar: "XAI_API_KEY", baseUrl: "https://api.x.ai/v1" }),
|
|
371
|
+
compatibleAdapter({ id: "perplexity", label: "Perplexity", envPrefix: "PERPLEXITY", keyVar: "PERPLEXITY_API_KEY", baseUrl: "https://api.perplexity.ai" }),
|
|
372
|
+
{
|
|
373
|
+
id: "custom",
|
|
374
|
+
label: "Custom OpenAI-compatible",
|
|
375
|
+
paid: (env) => !env.AI_CUSTOM_FREE,
|
|
376
|
+
missing: (env) => need(env, "AI_CUSTOM_BASE_URL", "AI_CUSTOM_MODEL"),
|
|
377
|
+
create: ({ env, fetch: fetch2 }) => openAiCompatibleProvider({
|
|
378
|
+
name: env.AI_CUSTOM_NAME ? `custom:${env.AI_CUSTOM_NAME}` : "custom",
|
|
379
|
+
// A local server may take no key; the header must still be well formed.
|
|
380
|
+
apiKey: env.AI_CUSTOM_API_KEY ?? "none",
|
|
381
|
+
baseUrl: env.AI_CUSTOM_BASE_URL,
|
|
382
|
+
model: env.AI_CUSTOM_MODEL,
|
|
383
|
+
fetch: fetch2
|
|
384
|
+
})
|
|
385
|
+
},
|
|
386
|
+
{
|
|
387
|
+
id: "vertex",
|
|
388
|
+
label: "Vertex AI",
|
|
389
|
+
paid: () => true,
|
|
390
|
+
missing: (env) => vertexConfigured(env) ? [] : need(env, "GOOGLE_CLIENT_EMAIL", "GOOGLE_PRIVATE_KEY", "GOOGLE_CLOUD_PROJECT"),
|
|
391
|
+
create: ({ env, purpose, fetch: fetch2 }) => vertexProvider({
|
|
392
|
+
clientEmail: env.GOOGLE_CLIENT_EMAIL,
|
|
393
|
+
privateKey: env.GOOGLE_PRIVATE_KEY,
|
|
394
|
+
tokenUri: env.GOOGLE_TOKEN_URI,
|
|
395
|
+
project: env.GOOGLE_CLOUD_PROJECT,
|
|
396
|
+
model: textModels(env, purpose).vertex,
|
|
397
|
+
fetchImpl: fetch2
|
|
398
|
+
})
|
|
399
|
+
}
|
|
400
|
+
];
|
|
401
|
+
var DEFAULT_ORDER = BUILTIN_ADAPTERS.map((adapter) => adapter.id);
|
|
402
|
+
function configuredOrder(env, purpose) {
|
|
403
|
+
const raw = (purpose === "blog" ? env.AI_PROVIDER_ORDER_BLOG : env.AI_PROVIDER_ORDER_CHAT) ?? env.AI_PROVIDER_ORDER;
|
|
404
|
+
if (!raw) return null;
|
|
405
|
+
const ids = raw.split(",").map((id) => id.trim().toLowerCase()).filter(Boolean);
|
|
406
|
+
return ids.length ? [...new Set(ids)] : null;
|
|
407
|
+
}
|
|
408
|
+
function providerStatuses(env, purpose, adapters = BUILTIN_ADAPTERS) {
|
|
409
|
+
const order = configuredOrder(env, purpose);
|
|
410
|
+
const ids = order ?? adapters.map((adapter) => adapter.id);
|
|
411
|
+
const byId = new Map(adapters.map((adapter) => [adapter.id, adapter]));
|
|
412
|
+
let position = 0;
|
|
413
|
+
const statuses = [];
|
|
414
|
+
for (const id of ids) {
|
|
415
|
+
const adapter = byId.get(id);
|
|
416
|
+
if (!adapter) {
|
|
417
|
+
statuses.push({ id, label: id, paid: false, state: "unknown", missing: [] });
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
const missing = adapter.missing(env);
|
|
421
|
+
const paid = adapter.paid(env);
|
|
422
|
+
const state = missing.length ? "not_configured" : paid && !paidAllowed(env) ? "needs_paid" : "active";
|
|
423
|
+
statuses.push({ id, label: adapter.label, paid, state, missing, ...state === "active" ? { position: position++ } : {} });
|
|
424
|
+
}
|
|
425
|
+
for (const adapter of adapters) {
|
|
426
|
+
if (!ids.includes(adapter.id)) {
|
|
427
|
+
statuses.push({ id: adapter.id, label: adapter.label, paid: adapter.paid(env), state: "not_in_order", missing: adapter.missing(env) });
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
return statuses;
|
|
431
|
+
}
|
|
432
|
+
function chainPlan(env, purpose, adapters = BUILTIN_ADAPTERS) {
|
|
433
|
+
const byId = new Map(adapters.map((adapter) => [adapter.id, adapter]));
|
|
434
|
+
return providerStatuses(env, purpose, adapters).filter((status) => status.state === "active").map((status) => byId.get(status.id));
|
|
435
|
+
}
|
|
22
436
|
|
|
23
437
|
// src/image.ts
|
|
438
|
+
import "server-only";
|
|
24
439
|
function vertexImageAvailable(env) {
|
|
25
440
|
return vertexConfigured(env);
|
|
26
441
|
}
|
|
@@ -43,18 +458,18 @@ function imageConfigFromEnv(env) {
|
|
|
43
458
|
}
|
|
44
459
|
|
|
45
460
|
// src/availability.ts
|
|
46
|
-
function textAiConfigured(env) {
|
|
47
|
-
if (
|
|
48
|
-
return
|
|
461
|
+
function textAiConfigured(env, purpose) {
|
|
462
|
+
if (purpose) return chainPlan(env, purpose).length > 0;
|
|
463
|
+
return chainPlan(env, "blog").length > 0 || chainPlan(env, "chat").length > 0;
|
|
49
464
|
}
|
|
50
465
|
function blogAiEnabled(env) {
|
|
51
|
-
return env.ENABLE_BLOG_AI && textAiConfigured(env);
|
|
466
|
+
return env.ENABLE_BLOG_AI && textAiConfigured(env, "blog");
|
|
52
467
|
}
|
|
53
468
|
function blogAiImagesEnabled(env) {
|
|
54
469
|
return blogAiEnabled(env) && imageConfigFromEnv(env) !== null;
|
|
55
470
|
}
|
|
56
471
|
function chatbotEnabled(env) {
|
|
57
|
-
return env.ENABLE_CHATBOT && textAiConfigured(env);
|
|
472
|
+
return env.ENABLE_CHATBOT && textAiConfigured(env, "chat");
|
|
58
473
|
}
|
|
59
474
|
export {
|
|
60
475
|
blogAiEnabled,
|
package/dist/env.d.ts
CHANGED
|
@@ -27,6 +27,36 @@ declare const aiEnvSchema: z.ZodObject<{
|
|
|
27
27
|
IMAGE_NVIDIA_MODEL: z.ZodPipe<z.ZodOptional<z.ZodString>, z.ZodTransform<string | undefined, string | undefined>>;
|
|
28
28
|
IMAGE_GEMINI_MODEL: z.ZodPipe<z.ZodOptional<z.ZodString>, z.ZodTransform<string | undefined, string | undefined>>;
|
|
29
29
|
IMAGE_VERTEX_MODEL: z.ZodPipe<z.ZodOptional<z.ZodString>, z.ZodTransform<string | undefined, string | undefined>>;
|
|
30
|
+
AI_PROVIDER_ORDER: z.ZodPipe<z.ZodOptional<z.ZodString>, z.ZodTransform<string | undefined, string | undefined>>;
|
|
31
|
+
AI_PROVIDER_ORDER_BLOG: z.ZodPipe<z.ZodOptional<z.ZodString>, z.ZodTransform<string | undefined, string | undefined>>;
|
|
32
|
+
AI_PROVIDER_ORDER_CHAT: z.ZodPipe<z.ZodOptional<z.ZodString>, z.ZodTransform<string | undefined, string | undefined>>;
|
|
33
|
+
OPENAI_API_KEY: z.ZodPipe<z.ZodOptional<z.ZodString>, z.ZodTransform<string | undefined, string | undefined>>;
|
|
34
|
+
OPENAI_BASE_URL: z.ZodPipe<z.ZodOptional<z.ZodString>, z.ZodTransform<string | undefined, string | undefined>>;
|
|
35
|
+
OPENAI_MODEL: z.ZodPipe<z.ZodOptional<z.ZodString>, z.ZodTransform<string | undefined, string | undefined>>;
|
|
36
|
+
BLOG_OPENAI_MODEL: z.ZodPipe<z.ZodOptional<z.ZodString>, z.ZodTransform<string | undefined, string | undefined>>;
|
|
37
|
+
CHAT_OPENAI_MODEL: z.ZodPipe<z.ZodOptional<z.ZodString>, z.ZodTransform<string | undefined, string | undefined>>;
|
|
38
|
+
ANTHROPIC_API_KEY: z.ZodPipe<z.ZodOptional<z.ZodString>, z.ZodTransform<string | undefined, string | undefined>>;
|
|
39
|
+
ANTHROPIC_BASE_URL: z.ZodPipe<z.ZodOptional<z.ZodString>, z.ZodTransform<string | undefined, string | undefined>>;
|
|
40
|
+
ANTHROPIC_MODEL: z.ZodPipe<z.ZodOptional<z.ZodString>, z.ZodTransform<string | undefined, string | undefined>>;
|
|
41
|
+
BLOG_ANTHROPIC_MODEL: z.ZodPipe<z.ZodOptional<z.ZodString>, z.ZodTransform<string | undefined, string | undefined>>;
|
|
42
|
+
CHAT_ANTHROPIC_MODEL: z.ZodPipe<z.ZodOptional<z.ZodString>, z.ZodTransform<string | undefined, string | undefined>>;
|
|
43
|
+
DEEPSEEK_API_KEY: z.ZodPipe<z.ZodOptional<z.ZodString>, z.ZodTransform<string | undefined, string | undefined>>;
|
|
44
|
+
DEEPSEEK_MODEL: z.ZodPipe<z.ZodOptional<z.ZodString>, z.ZodTransform<string | undefined, string | undefined>>;
|
|
45
|
+
BLOG_DEEPSEEK_MODEL: z.ZodPipe<z.ZodOptional<z.ZodString>, z.ZodTransform<string | undefined, string | undefined>>;
|
|
46
|
+
CHAT_DEEPSEEK_MODEL: z.ZodPipe<z.ZodOptional<z.ZodString>, z.ZodTransform<string | undefined, string | undefined>>;
|
|
47
|
+
XAI_API_KEY: z.ZodPipe<z.ZodOptional<z.ZodString>, z.ZodTransform<string | undefined, string | undefined>>;
|
|
48
|
+
XAI_MODEL: z.ZodPipe<z.ZodOptional<z.ZodString>, z.ZodTransform<string | undefined, string | undefined>>;
|
|
49
|
+
BLOG_XAI_MODEL: z.ZodPipe<z.ZodOptional<z.ZodString>, z.ZodTransform<string | undefined, string | undefined>>;
|
|
50
|
+
CHAT_XAI_MODEL: z.ZodPipe<z.ZodOptional<z.ZodString>, z.ZodTransform<string | undefined, string | undefined>>;
|
|
51
|
+
PERPLEXITY_API_KEY: z.ZodPipe<z.ZodOptional<z.ZodString>, z.ZodTransform<string | undefined, string | undefined>>;
|
|
52
|
+
PERPLEXITY_MODEL: z.ZodPipe<z.ZodOptional<z.ZodString>, z.ZodTransform<string | undefined, string | undefined>>;
|
|
53
|
+
BLOG_PERPLEXITY_MODEL: z.ZodPipe<z.ZodOptional<z.ZodString>, z.ZodTransform<string | undefined, string | undefined>>;
|
|
54
|
+
CHAT_PERPLEXITY_MODEL: z.ZodPipe<z.ZodOptional<z.ZodString>, z.ZodTransform<string | undefined, string | undefined>>;
|
|
55
|
+
AI_CUSTOM_BASE_URL: z.ZodPipe<z.ZodOptional<z.ZodString>, z.ZodTransform<string | undefined, string | undefined>>;
|
|
56
|
+
AI_CUSTOM_API_KEY: z.ZodPipe<z.ZodOptional<z.ZodString>, z.ZodTransform<string | undefined, string | undefined>>;
|
|
57
|
+
AI_CUSTOM_MODEL: z.ZodPipe<z.ZodOptional<z.ZodString>, z.ZodTransform<string | undefined, string | undefined>>;
|
|
58
|
+
AI_CUSTOM_NAME: z.ZodPipe<z.ZodOptional<z.ZodString>, z.ZodTransform<string | undefined, string | undefined>>;
|
|
59
|
+
AI_CUSTOM_FREE: z.ZodPipe<z.ZodOptional<z.ZodString>, z.ZodTransform<boolean, string | undefined>>;
|
|
30
60
|
GOOGLE_CLIENT_EMAIL: z.ZodPipe<z.ZodOptional<z.ZodString>, z.ZodTransform<string | undefined, string | undefined>>;
|
|
31
61
|
GOOGLE_PRIVATE_KEY: z.ZodPipe<z.ZodPipe<z.ZodOptional<z.ZodString>, z.ZodTransform<string | undefined, string | undefined>>, z.ZodTransform<string | undefined, string | undefined>>;
|
|
32
62
|
GOOGLE_CLOUD_PROJECT: z.ZodPipe<z.ZodOptional<z.ZodString>, z.ZodTransform<string | undefined, string | undefined>>;
|
package/dist/env.js
CHANGED
|
@@ -44,6 +44,45 @@ var aiEnvSchema = z.object({
|
|
|
44
44
|
IMAGE_NVIDIA_MODEL: text,
|
|
45
45
|
IMAGE_GEMINI_MODEL: text,
|
|
46
46
|
IMAGE_VERTEX_MODEL: text,
|
|
47
|
+
// Provider order (./adapters). Comma-separated adapter ids; only the listed
|
|
48
|
+
// ones run, in that order. Per purpose first, then the global one; unset =
|
|
49
|
+
// the built-in order (free tiers first, paid last).
|
|
50
|
+
AI_PROVIDER_ORDER: text,
|
|
51
|
+
AI_PROVIDER_ORDER_BLOG: text,
|
|
52
|
+
AI_PROVIDER_ORDER_CHAT: text,
|
|
53
|
+
// Paid OpenAI-compatible and Anthropic providers: each joins a chain only
|
|
54
|
+
// with its key set and AI_ALLOW_PAID on. Models: purpose variable, then the
|
|
55
|
+
// provider-wide one, then the default in ./adapters.
|
|
56
|
+
OPENAI_API_KEY: text,
|
|
57
|
+
OPENAI_BASE_URL: text,
|
|
58
|
+
OPENAI_MODEL: text,
|
|
59
|
+
BLOG_OPENAI_MODEL: text,
|
|
60
|
+
CHAT_OPENAI_MODEL: text,
|
|
61
|
+
ANTHROPIC_API_KEY: text,
|
|
62
|
+
ANTHROPIC_BASE_URL: text,
|
|
63
|
+
ANTHROPIC_MODEL: text,
|
|
64
|
+
BLOG_ANTHROPIC_MODEL: text,
|
|
65
|
+
CHAT_ANTHROPIC_MODEL: text,
|
|
66
|
+
DEEPSEEK_API_KEY: text,
|
|
67
|
+
DEEPSEEK_MODEL: text,
|
|
68
|
+
BLOG_DEEPSEEK_MODEL: text,
|
|
69
|
+
CHAT_DEEPSEEK_MODEL: text,
|
|
70
|
+
XAI_API_KEY: text,
|
|
71
|
+
XAI_MODEL: text,
|
|
72
|
+
BLOG_XAI_MODEL: text,
|
|
73
|
+
CHAT_XAI_MODEL: text,
|
|
74
|
+
PERPLEXITY_API_KEY: text,
|
|
75
|
+
PERPLEXITY_MODEL: text,
|
|
76
|
+
BLOG_PERPLEXITY_MODEL: text,
|
|
77
|
+
CHAT_PERPLEXITY_MODEL: text,
|
|
78
|
+
// One custom OpenAI-compatible endpoint (a gateway, vLLM, Ollama, LiteLLM).
|
|
79
|
+
// Treated as paid unless AI_CUSTOM_FREE is on; the key is optional for a
|
|
80
|
+
// local server.
|
|
81
|
+
AI_CUSTOM_BASE_URL: text,
|
|
82
|
+
AI_CUSTOM_API_KEY: text,
|
|
83
|
+
AI_CUSTOM_MODEL: text,
|
|
84
|
+
AI_CUSTOM_NAME: text,
|
|
85
|
+
AI_CUSTOM_FREE: flag,
|
|
47
86
|
// Google Vertex service account (paid).
|
|
48
87
|
GOOGLE_CLIENT_EMAIL: text,
|
|
49
88
|
GOOGLE_PRIVATE_KEY: privateKey,
|
package/dist/guard.d.ts
CHANGED
|
@@ -1,8 +1,20 @@
|
|
|
1
1
|
declare const DATA_START = "<<<SAHAN_USER_DATA_START>>>";
|
|
2
2
|
declare const DATA_END = "<<<SAHAN_USER_DATA_END>>>";
|
|
3
|
-
/** Strips control characters, caps length, and neutralizes any attempt to
|
|
3
|
+
/** Strips control characters, caps length (4000 by default), and neutralizes any attempt to
|
|
4
4
|
* forge the delimiter markers themselves before fencing the text as data. */
|
|
5
|
-
declare function wrapUserData(text: string): string;
|
|
5
|
+
declare function wrapUserData(text: string, maxLength?: number): string;
|
|
6
|
+
/** Longest owner guidance (per scope, after merging) that reaches a prompt. */
|
|
7
|
+
declare const MAX_GUIDANCE_LENGTH = 6000;
|
|
8
|
+
/**
|
|
9
|
+
* Standing guidance the site owner wrote in the admin settings (tone, facts,
|
|
10
|
+
* do and don't), appended to a system prompt. Unlike visitor or editor data
|
|
11
|
+
* it is meant to steer the model, so it is not fenced; it is still capped,
|
|
12
|
+
* stripped of control characters and forged fence markers, and placed after
|
|
13
|
+
* the fixed rules with a note that those rules win. Empty input gives "".
|
|
14
|
+
*/
|
|
15
|
+
declare function guidanceSection(guidance: string | null | undefined): string;
|
|
16
|
+
/** Joins owner guidance from broad to specific (global, then a feature's own), skipping empty parts. */
|
|
17
|
+
declare function mergeGuidance(...parts: Array<string | null | undefined>): string;
|
|
6
18
|
interface ModelPrompt {
|
|
7
19
|
system: string;
|
|
8
20
|
user: string;
|
|
@@ -15,4 +27,4 @@ interface ModelPrompt {
|
|
|
15
27
|
*/
|
|
16
28
|
declare function looksLikeLeak(text: string): boolean;
|
|
17
29
|
|
|
18
|
-
export { DATA_END, DATA_START, type ModelPrompt, looksLikeLeak, wrapUserData };
|
|
30
|
+
export { DATA_END, DATA_START, MAX_GUIDANCE_LENGTH, type ModelPrompt, guidanceSection, looksLikeLeak, mergeGuidance, wrapUserData };
|
package/dist/guard.js
CHANGED
|
Binary file
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { AiEnv, aiEnvFromProcess, aiEnvSchema, parseAiEnv } from './env.js';
|
|
2
2
|
export { ImageModels, TextModels, TextPurpose } from './models.js';
|
|
3
|
-
export { AiHealth, AiProvider, AiResult } from './providers.js';
|
|
3
|
+
export { a as AdapterState, b as AdapterStatus, AiHealth, AiProvider, AiResult, c as ProviderAdapter } from './providers.js';
|
|
4
4
|
export { ModelPrompt } from './guard.js';
|
|
5
5
|
export { AiLogger } from './log.js';
|
|
6
6
|
import 'zod';
|
package/dist/index.js
CHANGED
|
@@ -44,6 +44,45 @@ var aiEnvSchema = z.object({
|
|
|
44
44
|
IMAGE_NVIDIA_MODEL: text,
|
|
45
45
|
IMAGE_GEMINI_MODEL: text,
|
|
46
46
|
IMAGE_VERTEX_MODEL: text,
|
|
47
|
+
// Provider order (./adapters). Comma-separated adapter ids; only the listed
|
|
48
|
+
// ones run, in that order. Per purpose first, then the global one; unset =
|
|
49
|
+
// the built-in order (free tiers first, paid last).
|
|
50
|
+
AI_PROVIDER_ORDER: text,
|
|
51
|
+
AI_PROVIDER_ORDER_BLOG: text,
|
|
52
|
+
AI_PROVIDER_ORDER_CHAT: text,
|
|
53
|
+
// Paid OpenAI-compatible and Anthropic providers: each joins a chain only
|
|
54
|
+
// with its key set and AI_ALLOW_PAID on. Models: purpose variable, then the
|
|
55
|
+
// provider-wide one, then the default in ./adapters.
|
|
56
|
+
OPENAI_API_KEY: text,
|
|
57
|
+
OPENAI_BASE_URL: text,
|
|
58
|
+
OPENAI_MODEL: text,
|
|
59
|
+
BLOG_OPENAI_MODEL: text,
|
|
60
|
+
CHAT_OPENAI_MODEL: text,
|
|
61
|
+
ANTHROPIC_API_KEY: text,
|
|
62
|
+
ANTHROPIC_BASE_URL: text,
|
|
63
|
+
ANTHROPIC_MODEL: text,
|
|
64
|
+
BLOG_ANTHROPIC_MODEL: text,
|
|
65
|
+
CHAT_ANTHROPIC_MODEL: text,
|
|
66
|
+
DEEPSEEK_API_KEY: text,
|
|
67
|
+
DEEPSEEK_MODEL: text,
|
|
68
|
+
BLOG_DEEPSEEK_MODEL: text,
|
|
69
|
+
CHAT_DEEPSEEK_MODEL: text,
|
|
70
|
+
XAI_API_KEY: text,
|
|
71
|
+
XAI_MODEL: text,
|
|
72
|
+
BLOG_XAI_MODEL: text,
|
|
73
|
+
CHAT_XAI_MODEL: text,
|
|
74
|
+
PERPLEXITY_API_KEY: text,
|
|
75
|
+
PERPLEXITY_MODEL: text,
|
|
76
|
+
BLOG_PERPLEXITY_MODEL: text,
|
|
77
|
+
CHAT_PERPLEXITY_MODEL: text,
|
|
78
|
+
// One custom OpenAI-compatible endpoint (a gateway, vLLM, Ollama, LiteLLM).
|
|
79
|
+
// Treated as paid unless AI_CUSTOM_FREE is on; the key is optional for a
|
|
80
|
+
// local server.
|
|
81
|
+
AI_CUSTOM_BASE_URL: text,
|
|
82
|
+
AI_CUSTOM_API_KEY: text,
|
|
83
|
+
AI_CUSTOM_MODEL: text,
|
|
84
|
+
AI_CUSTOM_NAME: text,
|
|
85
|
+
AI_CUSTOM_FREE: flag,
|
|
47
86
|
// Google Vertex service account (paid).
|
|
48
87
|
GOOGLE_CLIENT_EMAIL: text,
|
|
49
88
|
GOOGLE_PRIVATE_KEY: privateKey,
|