@absolutejs/ai 0.0.21 → 0.0.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ai/index.js +461 -99
- package/dist/ai/index.js.map +10 -7
- package/dist/ai/providers/anthropic.js +268 -16
- package/dist/ai/providers/anthropic.js.map +7 -5
- package/dist/ai/providers/gemini.js +253 -12
- package/dist/ai/providers/gemini.js.map +6 -4
- package/dist/ai/providers/ollama.js +251 -10
- package/dist/ai/providers/ollama.js.map +6 -4
- package/dist/ai/providers/openai.js +261 -16
- package/dist/ai/providers/openai.js.map +7 -5
- package/dist/ai/providers/openaiCompatible.js +279 -34
- package/dist/ai/providers/openaiCompatible.js.map +7 -5
- package/dist/ai/providers/openaiResponses.js +253 -12
- package/dist/ai/providers/openaiResponses.js.map +6 -4
- package/dist/src/ai/errors/providerError.d.ts +32 -0
- package/dist/src/ai/index.d.ts +6 -0
- package/dist/src/ai/providerStatusMonitor.d.ts +34 -0
- package/dist/src/ai/resilience.d.ts +66 -0
- package/package.json +1 -1
package/dist/ai/index.js
CHANGED
|
@@ -1,15 +1,256 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
var __require = import.meta.require;
|
|
3
3
|
|
|
4
|
-
// src/ai/
|
|
5
|
-
var
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
4
|
+
// src/ai/errors/providerError.ts
|
|
5
|
+
var PROVIDER_STATUS_PAGES = {
|
|
6
|
+
anthropic: "https://status.claude.com",
|
|
7
|
+
gemini: "https://status.cloud.google.com",
|
|
8
|
+
google: "https://status.cloud.google.com",
|
|
9
|
+
openai: "https://status.openai.com"
|
|
10
|
+
};
|
|
11
|
+
var RETRYABLE_STATUSES = new Set([
|
|
12
|
+
408,
|
|
13
|
+
409,
|
|
14
|
+
425,
|
|
15
|
+
429,
|
|
16
|
+
500,
|
|
17
|
+
502,
|
|
18
|
+
503,
|
|
19
|
+
504,
|
|
20
|
+
529
|
|
21
|
+
]);
|
|
22
|
+
var CONNECTION_ERROR_PATTERNS = [
|
|
23
|
+
"econnreset",
|
|
24
|
+
"econnrefused",
|
|
25
|
+
"etimedout",
|
|
26
|
+
"enotfound",
|
|
27
|
+
"eai_again",
|
|
28
|
+
"socket hang up",
|
|
29
|
+
"fetch failed",
|
|
30
|
+
"network",
|
|
31
|
+
"terminated",
|
|
32
|
+
"timed out",
|
|
33
|
+
"timeout",
|
|
34
|
+
"and not retryable",
|
|
35
|
+
"no response body"
|
|
36
|
+
];
|
|
37
|
+
var isAbortError = (err) => err instanceof Error && (err.name === "AbortError" || /\babort(ed)?\b/i.test(err.message));
|
|
38
|
+
var providerStatusPage = (provider) => PROVIDER_STATUS_PAGES[provider] ?? null;
|
|
39
|
+
|
|
40
|
+
class ProviderError extends Error {
|
|
41
|
+
provider;
|
|
42
|
+
status;
|
|
43
|
+
type;
|
|
44
|
+
retryable;
|
|
45
|
+
statusPageUrl;
|
|
46
|
+
constructor(init) {
|
|
47
|
+
super(init.message, init.cause === undefined ? undefined : { cause: init.cause });
|
|
48
|
+
this.name = "ProviderError";
|
|
49
|
+
this.provider = init.provider;
|
|
50
|
+
this.status = init.status ?? null;
|
|
51
|
+
this.type = init.type ?? null;
|
|
52
|
+
this.retryable = init.retryable;
|
|
53
|
+
this.statusPageUrl = providerStatusPage(init.provider);
|
|
54
|
+
}
|
|
55
|
+
static fromResponse(provider, status, body, type) {
|
|
56
|
+
return new ProviderError({
|
|
57
|
+
message: `${capitalize(provider)} API error ${status}: ${body}`,
|
|
58
|
+
provider,
|
|
59
|
+
retryable: RETRYABLE_STATUSES.has(status),
|
|
60
|
+
status,
|
|
61
|
+
type: type ?? null
|
|
62
|
+
});
|
|
11
63
|
}
|
|
64
|
+
static from(err, provider) {
|
|
65
|
+
if (err instanceof ProviderError)
|
|
66
|
+
return err;
|
|
67
|
+
if (isAbortError(err))
|
|
68
|
+
throw err;
|
|
69
|
+
const rawMessage = err instanceof Error ? err.message : String(err ?? "");
|
|
70
|
+
const lower = rawMessage.toLowerCase();
|
|
71
|
+
const statusMatch = lower.match(/api error\s+(\d{3})/);
|
|
72
|
+
const status = statusMatch ? Number(statusMatch[1]) : null;
|
|
73
|
+
const retryable = status !== null && RETRYABLE_STATUSES.has(status) || status === null && CONNECTION_ERROR_PATTERNS.some((pattern) => lower.includes(pattern));
|
|
74
|
+
return new ProviderError({
|
|
75
|
+
cause: err,
|
|
76
|
+
message: rawMessage || `${capitalize(provider)} request failed`,
|
|
77
|
+
provider,
|
|
78
|
+
retryable,
|
|
79
|
+
status
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
var capitalize = (value) => value.charAt(0).toUpperCase() + value.slice(1);
|
|
84
|
+
|
|
85
|
+
// src/ai/resilience.ts
|
|
86
|
+
var DEFAULT_CONFIG = {
|
|
87
|
+
baseDelayMs: 500,
|
|
88
|
+
failureThreshold: 4,
|
|
89
|
+
maxDelayMs: 8000,
|
|
90
|
+
maxRetries: 2,
|
|
91
|
+
openMs: 30000
|
|
92
|
+
};
|
|
93
|
+
var config = { ...DEFAULT_CONFIG };
|
|
94
|
+
var configureProviderResilience = (partial) => {
|
|
95
|
+
config = { ...config, ...partial };
|
|
96
|
+
};
|
|
97
|
+
var health = new Map;
|
|
98
|
+
var recordFor = (provider) => {
|
|
99
|
+
const existing = health.get(provider);
|
|
100
|
+
if (existing)
|
|
101
|
+
return existing;
|
|
102
|
+
const fresh = {
|
|
103
|
+
consecutiveFailures: 0,
|
|
104
|
+
external: null,
|
|
105
|
+
lastError: null,
|
|
106
|
+
lastFailureAt: null,
|
|
107
|
+
lastSuccessAt: null,
|
|
108
|
+
nextProbeAt: null,
|
|
109
|
+
state: "closed"
|
|
110
|
+
};
|
|
111
|
+
health.set(provider, fresh);
|
|
112
|
+
return fresh;
|
|
113
|
+
};
|
|
114
|
+
var setProviderAvailability = (provider, status) => {
|
|
115
|
+
const record = recordFor(provider);
|
|
116
|
+
record.external = {
|
|
117
|
+
available: status.available,
|
|
118
|
+
checkedAt: Date.now(),
|
|
119
|
+
indicator: status.indicator ?? (status.available ? "operational" : "unknown"),
|
|
120
|
+
reason: status.reason ?? ""
|
|
121
|
+
};
|
|
122
|
+
};
|
|
123
|
+
var noteSuccess = (provider) => {
|
|
124
|
+
const record = recordFor(provider);
|
|
125
|
+
record.state = "closed";
|
|
126
|
+
record.consecutiveFailures = 0;
|
|
127
|
+
record.nextProbeAt = null;
|
|
128
|
+
record.lastError = null;
|
|
129
|
+
record.lastSuccessAt = Date.now();
|
|
130
|
+
};
|
|
131
|
+
var noteFailure = (provider, error) => {
|
|
132
|
+
const record = recordFor(provider);
|
|
133
|
+
record.consecutiveFailures += 1;
|
|
134
|
+
record.lastFailureAt = Date.now();
|
|
135
|
+
record.lastError = {
|
|
136
|
+
message: error.message,
|
|
137
|
+
status: error.status,
|
|
138
|
+
type: error.type
|
|
139
|
+
};
|
|
140
|
+
if (record.consecutiveFailures >= config.failureThreshold) {
|
|
141
|
+
record.state = "open";
|
|
142
|
+
record.nextProbeAt = Date.now() + config.openMs;
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
var snapshot = (provider, record) => ({
|
|
146
|
+
consecutiveFailures: record.consecutiveFailures,
|
|
147
|
+
external: record.external,
|
|
148
|
+
healthy: record.state === "closed" && record.external?.available !== false,
|
|
149
|
+
lastError: record.lastError,
|
|
150
|
+
lastFailureAt: record.lastFailureAt,
|
|
151
|
+
lastSuccessAt: record.lastSuccessAt,
|
|
152
|
+
nextRetryAt: record.nextProbeAt,
|
|
153
|
+
provider,
|
|
154
|
+
state: record.state,
|
|
155
|
+
statusPageUrl: providerStatusPage(provider)
|
|
12
156
|
});
|
|
157
|
+
function getProviderHealth(provider) {
|
|
158
|
+
if (provider !== undefined)
|
|
159
|
+
return snapshot(provider, recordFor(provider));
|
|
160
|
+
return [...health.entries()].map(([name, record]) => snapshot(name, record));
|
|
161
|
+
}
|
|
162
|
+
var backoffDelay = (attempt) => {
|
|
163
|
+
const exponential = config.baseDelayMs * 2 ** attempt;
|
|
164
|
+
const capped = Math.min(exponential, config.maxDelayMs);
|
|
165
|
+
return Math.round(capped / 2 + Math.random() * capped / 2);
|
|
166
|
+
};
|
|
167
|
+
var sleep = (ms, signal) => new Promise((resolve, reject) => {
|
|
168
|
+
if (signal?.aborted) {
|
|
169
|
+
reject(signal.reason ?? new Error("aborted"));
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
const timer = setTimeout(() => {
|
|
173
|
+
signal?.removeEventListener("abort", onAbort);
|
|
174
|
+
resolve();
|
|
175
|
+
}, ms);
|
|
176
|
+
const onAbort = () => {
|
|
177
|
+
clearTimeout(timer);
|
|
178
|
+
reject(signal?.reason ?? new Error("aborted"));
|
|
179
|
+
};
|
|
180
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
181
|
+
});
|
|
182
|
+
var circuitOpen = (provider) => {
|
|
183
|
+
const record = recordFor(provider);
|
|
184
|
+
if (record.external && !record.external.available) {
|
|
185
|
+
return new ProviderError({
|
|
186
|
+
message: `${provider} API is reported down${record.external.reason ? `: ${record.external.reason}` : ""}`,
|
|
187
|
+
provider,
|
|
188
|
+
retryable: true,
|
|
189
|
+
status: null,
|
|
190
|
+
type: record.external.indicator
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
if (record.state !== "open")
|
|
194
|
+
return null;
|
|
195
|
+
if (record.nextProbeAt !== null && Date.now() >= record.nextProbeAt) {
|
|
196
|
+
record.state = "half-open";
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
const detail = record.lastError?.message ?? "recent failures";
|
|
200
|
+
return new ProviderError({
|
|
201
|
+
message: `${provider} is temporarily unavailable (circuit open after ${record.consecutiveFailures} failures): ${detail}`,
|
|
202
|
+
provider,
|
|
203
|
+
retryable: true,
|
|
204
|
+
status: record.lastError?.status ?? null,
|
|
205
|
+
type: record.lastError?.type ?? null
|
|
206
|
+
});
|
|
207
|
+
};
|
|
208
|
+
var withResilience = (provider, providerName = "unknown") => {
|
|
209
|
+
const attempt = async function* (params, attemptNo) {
|
|
210
|
+
let yielded = false;
|
|
211
|
+
try {
|
|
212
|
+
for await (const chunk of provider.stream(params)) {
|
|
213
|
+
yielded = true;
|
|
214
|
+
yield chunk;
|
|
215
|
+
}
|
|
216
|
+
noteSuccess(providerName);
|
|
217
|
+
} catch (err) {
|
|
218
|
+
const providerError = ProviderError.from(err, providerName);
|
|
219
|
+
const canRetry = !yielded && providerError.retryable && attemptNo < config.maxRetries && !params.signal?.aborted;
|
|
220
|
+
if (canRetry) {
|
|
221
|
+
await sleep(backoffDelay(attemptNo), params.signal);
|
|
222
|
+
yield* attempt(params, attemptNo + 1);
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
noteFailure(providerName, providerError);
|
|
226
|
+
throw providerError;
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
return {
|
|
230
|
+
stream: (params) => {
|
|
231
|
+
const tripped = circuitOpen(providerName);
|
|
232
|
+
if (tripped) {
|
|
233
|
+
return async function* () {
|
|
234
|
+
throw tripped;
|
|
235
|
+
}();
|
|
236
|
+
}
|
|
237
|
+
return attempt(params, 0);
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
// src/ai/providers/instrumentation.ts
|
|
243
|
+
var instrumentAIProvider = (provider, providerName) => {
|
|
244
|
+
const resilient = withResilience(provider, providerName);
|
|
245
|
+
return {
|
|
246
|
+
stream: (params) => {
|
|
247
|
+
if (!params.onUsage && !params.onSpan) {
|
|
248
|
+
return resilient.stream(params);
|
|
249
|
+
}
|
|
250
|
+
return tapStream(resilient.stream(params), params, providerName);
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
};
|
|
13
254
|
async function* tapStream(source, params, providerName) {
|
|
14
255
|
const startedAt = Date.now();
|
|
15
256
|
let lastUsage;
|
|
@@ -501,23 +742,27 @@ var fetchOpenAIStream = async function* (baseUrl, apiKey, body, signal) {
|
|
|
501
742
|
});
|
|
502
743
|
if (!response.ok) {
|
|
503
744
|
const errorText = await response.text();
|
|
504
|
-
throw
|
|
745
|
+
throw ProviderError.fromResponse("openai", response.status, errorText);
|
|
505
746
|
}
|
|
506
747
|
if (!response.body) {
|
|
507
|
-
throw new
|
|
748
|
+
throw new ProviderError({
|
|
749
|
+
message: "OpenAI API returned no response body",
|
|
750
|
+
provider: "openai",
|
|
751
|
+
retryable: true
|
|
752
|
+
});
|
|
508
753
|
}
|
|
509
754
|
yield* parseSSEStream(response.body, signal);
|
|
510
755
|
};
|
|
511
|
-
var openai = (
|
|
512
|
-
const baseUrl =
|
|
513
|
-
if (!
|
|
756
|
+
var openai = (config2) => {
|
|
757
|
+
const baseUrl = config2.baseUrl ?? DEFAULT_BASE_URL;
|
|
758
|
+
if (!config2.apiKey && !config2.tokenSource) {
|
|
514
759
|
throw new Error("openai() requires either apiKey or tokenSource");
|
|
515
760
|
}
|
|
516
761
|
const resolveKey = async () => {
|
|
517
|
-
if (
|
|
518
|
-
return await Promise.resolve(
|
|
762
|
+
if (config2.tokenSource) {
|
|
763
|
+
return await Promise.resolve(config2.tokenSource());
|
|
519
764
|
}
|
|
520
|
-
return
|
|
765
|
+
return config2.apiKey;
|
|
521
766
|
};
|
|
522
767
|
return instrumentAIProvider({
|
|
523
768
|
stream: (params) => {
|
|
@@ -531,37 +776,37 @@ var openai = (config) => {
|
|
|
531
776
|
};
|
|
532
777
|
|
|
533
778
|
// src/ai/providers/openaiCompatible.ts
|
|
534
|
-
var alibaba = (
|
|
535
|
-
apiKey:
|
|
779
|
+
var alibaba = (config2) => openaiCompatible({
|
|
780
|
+
apiKey: config2.apiKey,
|
|
536
781
|
baseUrl: "https://dashscope-intl.aliyuncs.com/compatible-mode"
|
|
537
782
|
});
|
|
538
|
-
var deepseek = (
|
|
539
|
-
apiKey:
|
|
783
|
+
var deepseek = (config2) => openaiCompatible({
|
|
784
|
+
apiKey: config2.apiKey,
|
|
540
785
|
baseUrl: "https://api.deepseek.com"
|
|
541
786
|
});
|
|
542
|
-
var google = (
|
|
543
|
-
apiKey:
|
|
787
|
+
var google = (config2) => openaiCompatible({
|
|
788
|
+
apiKey: config2.apiKey,
|
|
544
789
|
baseUrl: "https://generativelanguage.googleapis.com/v1beta/openai"
|
|
545
790
|
});
|
|
546
|
-
var meta = (
|
|
547
|
-
apiKey:
|
|
791
|
+
var meta = (config2) => openaiCompatible({
|
|
792
|
+
apiKey: config2.apiKey,
|
|
548
793
|
baseUrl: "https://api.llama.com/compat/v1"
|
|
549
794
|
});
|
|
550
|
-
var mistralai = (
|
|
551
|
-
apiKey:
|
|
795
|
+
var mistralai = (config2) => openaiCompatible({
|
|
796
|
+
apiKey: config2.apiKey,
|
|
552
797
|
baseUrl: "https://api.mistral.ai"
|
|
553
798
|
});
|
|
554
|
-
var moonshot = (
|
|
555
|
-
apiKey:
|
|
799
|
+
var moonshot = (config2) => openaiCompatible({
|
|
800
|
+
apiKey: config2.apiKey,
|
|
556
801
|
baseUrl: "https://api.moonshot.ai"
|
|
557
802
|
});
|
|
558
|
-
var openaiCompatible = (
|
|
559
|
-
apiKey:
|
|
560
|
-
baseUrl:
|
|
561
|
-
tokenSource:
|
|
803
|
+
var openaiCompatible = (config2) => openai({
|
|
804
|
+
apiKey: config2.apiKey,
|
|
805
|
+
baseUrl: config2.baseUrl,
|
|
806
|
+
tokenSource: config2.tokenSource
|
|
562
807
|
});
|
|
563
|
-
var xai = (
|
|
564
|
-
apiKey:
|
|
808
|
+
var xai = (config2) => openaiCompatible({
|
|
809
|
+
apiKey: config2.apiKey,
|
|
565
810
|
baseUrl: "https://api.x.ai"
|
|
566
811
|
});
|
|
567
812
|
|
|
@@ -973,14 +1218,14 @@ var resolveImageModels = (imageModels) => {
|
|
|
973
1218
|
}
|
|
974
1219
|
return new Set(imageModels);
|
|
975
1220
|
};
|
|
976
|
-
var openaiResponses = (
|
|
977
|
-
const baseUrl =
|
|
978
|
-
const imageModels = resolveImageModels(
|
|
1221
|
+
var openaiResponses = (config2) => {
|
|
1222
|
+
const baseUrl = config2.baseUrl ?? DEFAULT_BASE_URL2;
|
|
1223
|
+
const imageModels = resolveImageModels(config2.imageModels);
|
|
979
1224
|
return instrumentAIProvider({
|
|
980
1225
|
stream: (params) => {
|
|
981
1226
|
const isImageModel = imageModels.has(params.model);
|
|
982
1227
|
const body = buildRequestBody2(params, isImageModel);
|
|
983
|
-
return fetchResponsesStream(baseUrl,
|
|
1228
|
+
return fetchResponsesStream(baseUrl, config2.apiKey, body, params.signal);
|
|
984
1229
|
}
|
|
985
1230
|
}, "openai-responses");
|
|
986
1231
|
};
|
|
@@ -1265,14 +1510,14 @@ var resolveImageModels2 = (raw) => {
|
|
|
1265
1510
|
}
|
|
1266
1511
|
return new Set(raw);
|
|
1267
1512
|
};
|
|
1268
|
-
var gemini = (
|
|
1269
|
-
const baseUrl =
|
|
1270
|
-
const imageModels = resolveImageModels2(
|
|
1513
|
+
var gemini = (config2) => {
|
|
1514
|
+
const baseUrl = config2.baseUrl ?? DEFAULT_BASE_URL3;
|
|
1515
|
+
const imageModels = resolveImageModels2(config2.imageModels);
|
|
1271
1516
|
return instrumentAIProvider({
|
|
1272
1517
|
stream: (params) => {
|
|
1273
1518
|
const isImageModel = imageModels.has(params.model);
|
|
1274
1519
|
const body = buildRequestBody3(params, isImageModel);
|
|
1275
|
-
return fetchGeminiStream(baseUrl,
|
|
1520
|
+
return fetchGeminiStream(baseUrl, config2.apiKey, params.model, body, params.signal);
|
|
1276
1521
|
}
|
|
1277
1522
|
}, "gemini");
|
|
1278
1523
|
};
|
|
@@ -1545,7 +1790,14 @@ var handleMessageStart = (parsed, state) => {
|
|
|
1545
1790
|
var handleError = (parsed) => {
|
|
1546
1791
|
const error = getRecord(parsed, "error");
|
|
1547
1792
|
const errorMessage = error ? getString(error, "message") : "";
|
|
1548
|
-
|
|
1793
|
+
const errorType = error ? getString(error, "type") : "";
|
|
1794
|
+
const retryable = errorType === "overloaded_error" || errorType === "rate_limit_error" || errorType === "api_error";
|
|
1795
|
+
throw new ProviderError({
|
|
1796
|
+
message: errorMessage || "Anthropic API error",
|
|
1797
|
+
provider: "anthropic",
|
|
1798
|
+
retryable,
|
|
1799
|
+
type: errorType || null
|
|
1800
|
+
});
|
|
1549
1801
|
};
|
|
1550
1802
|
var processEvent = (eventType, parsed, state) => {
|
|
1551
1803
|
switch (eventType) {
|
|
@@ -1654,7 +1906,7 @@ async function* parseSSEStream4(body, signal) {
|
|
|
1654
1906
|
reader.releaseLock();
|
|
1655
1907
|
}
|
|
1656
1908
|
}
|
|
1657
|
-
var fetchAndStream = async function* (baseUrl,
|
|
1909
|
+
var fetchAndStream = async function* (baseUrl, config2, params) {
|
|
1658
1910
|
const body = buildRequestBody4(params);
|
|
1659
1911
|
const target = `${baseUrl}/v1/messages`;
|
|
1660
1912
|
const response = await fetch(target, {
|
|
@@ -1663,24 +1915,28 @@ var fetchAndStream = async function* (baseUrl, config, params) {
|
|
|
1663
1915
|
headers: {
|
|
1664
1916
|
"anthropic-version": API_VERSION,
|
|
1665
1917
|
"Content-Type": "application/json",
|
|
1666
|
-
"x-api-key":
|
|
1918
|
+
"x-api-key": config2.apiKey
|
|
1667
1919
|
},
|
|
1668
1920
|
method: "POST",
|
|
1669
1921
|
signal: params.signal
|
|
1670
1922
|
});
|
|
1671
1923
|
if (!response.ok) {
|
|
1672
1924
|
const errorText = await response.text();
|
|
1673
|
-
throw
|
|
1925
|
+
throw ProviderError.fromResponse("anthropic", response.status, errorText);
|
|
1674
1926
|
}
|
|
1675
1927
|
if (!response.body) {
|
|
1676
|
-
throw new
|
|
1928
|
+
throw new ProviderError({
|
|
1929
|
+
message: "Anthropic API returned no response body",
|
|
1930
|
+
provider: "anthropic",
|
|
1931
|
+
retryable: true
|
|
1932
|
+
});
|
|
1677
1933
|
}
|
|
1678
1934
|
yield* parseSSEStream4(response.body, params.signal);
|
|
1679
1935
|
};
|
|
1680
|
-
var anthropic = (
|
|
1681
|
-
const baseUrl =
|
|
1936
|
+
var anthropic = (config2) => {
|
|
1937
|
+
const baseUrl = config2.baseUrl ?? DEFAULT_BASE_URL4;
|
|
1682
1938
|
return instrumentAIProvider({
|
|
1683
|
-
stream: (params) => fetchAndStream(baseUrl,
|
|
1939
|
+
stream: (params) => fetchAndStream(baseUrl, config2, params)
|
|
1684
1940
|
}, "anthropic");
|
|
1685
1941
|
};
|
|
1686
1942
|
|
|
@@ -1915,8 +2171,8 @@ var fetchAndStream2 = async function* (baseUrl, params) {
|
|
|
1915
2171
|
}
|
|
1916
2172
|
yield* parseNDJSONStream(response.body, params.signal);
|
|
1917
2173
|
};
|
|
1918
|
-
var ollama = (
|
|
1919
|
-
const baseUrl =
|
|
2174
|
+
var ollama = (config2 = {}) => {
|
|
2175
|
+
const baseUrl = config2.baseUrl ?? DEFAULT_BASE_URL5;
|
|
1920
2176
|
return instrumentAIProvider({
|
|
1921
2177
|
stream: (params) => fetchAndStream2(baseUrl, params)
|
|
1922
2178
|
}, "ollama");
|
|
@@ -2747,24 +3003,24 @@ var buildUserMessage = (content, attachments) => {
|
|
|
2747
3003
|
}
|
|
2748
3004
|
return { content, role: "user" };
|
|
2749
3005
|
};
|
|
2750
|
-
var resolveModel = (
|
|
3006
|
+
var resolveModel = (config2, parsed) => {
|
|
2751
3007
|
if (parsed.model) {
|
|
2752
3008
|
return parsed.model;
|
|
2753
3009
|
}
|
|
2754
|
-
if (typeof
|
|
2755
|
-
return
|
|
3010
|
+
if (typeof config2.model === "string") {
|
|
3011
|
+
return config2.model;
|
|
2756
3012
|
}
|
|
2757
|
-
if (typeof
|
|
2758
|
-
return
|
|
3013
|
+
if (typeof config2.model === "function") {
|
|
3014
|
+
return config2.model(parsed.providerName);
|
|
2759
3015
|
}
|
|
2760
3016
|
return parsed.providerName;
|
|
2761
3017
|
};
|
|
2762
|
-
var resolveTools = (
|
|
2763
|
-
var resolveReasoning = (
|
|
2764
|
-
var aiChat = (
|
|
2765
|
-
const path =
|
|
2766
|
-
const store =
|
|
2767
|
-
const parseProvider =
|
|
3018
|
+
var resolveTools = (config2, providerName, model) => typeof config2.tools === "function" ? config2.tools(providerName, model) : config2.tools;
|
|
3019
|
+
var resolveReasoning = (config2, providerName, model) => typeof config2.reasoning === "function" ? config2.reasoning(providerName, model) : config2.reasoning;
|
|
3020
|
+
var aiChat = (config2) => {
|
|
3021
|
+
const path = config2.path ?? DEFAULT_PATH;
|
|
3022
|
+
const store = config2.store ?? createMemoryStore();
|
|
3023
|
+
const parseProvider = config2.parseProvider ?? defaultParseProvider;
|
|
2768
3024
|
const abortControllers = new Map;
|
|
2769
3025
|
const handleCancel = (conversationId) => {
|
|
2770
3026
|
const controller = abortControllers.get(conversationId);
|
|
@@ -2802,17 +3058,17 @@ var aiChat = (config) => {
|
|
|
2802
3058
|
timestamp: Date.now()
|
|
2803
3059
|
});
|
|
2804
3060
|
await store.set(conversationId, conversation);
|
|
2805
|
-
const model = resolveModel(
|
|
3061
|
+
const model = resolveModel(config2, parsed);
|
|
2806
3062
|
const userMessage = buildUserMessage(content, attachments);
|
|
2807
3063
|
await streamAI(ws, conversationId, messageId, {
|
|
2808
|
-
maxTurns:
|
|
3064
|
+
maxTurns: config2.maxTurns,
|
|
2809
3065
|
messages: [...history, userMessage],
|
|
2810
3066
|
model,
|
|
2811
|
-
provider:
|
|
3067
|
+
provider: config2.provider(providerName),
|
|
2812
3068
|
signal: controller.signal,
|
|
2813
|
-
systemPrompt:
|
|
2814
|
-
reasoning: resolveReasoning(
|
|
2815
|
-
tools: resolveTools(
|
|
3069
|
+
systemPrompt: config2.systemPrompt,
|
|
3070
|
+
reasoning: resolveReasoning(config2, providerName, model),
|
|
3071
|
+
tools: resolveTools(config2, providerName, model),
|
|
2816
3072
|
onComplete: async (fullResponse, usage) => {
|
|
2817
3073
|
const conv = await store.get(conversationId);
|
|
2818
3074
|
if (conv) {
|
|
@@ -2826,15 +3082,15 @@ var aiChat = (config) => {
|
|
|
2826
3082
|
await store.set(conversationId, conv);
|
|
2827
3083
|
}
|
|
2828
3084
|
abortControllers.delete(conversationId);
|
|
2829
|
-
|
|
3085
|
+
config2.onComplete?.(conversationId, fullResponse, usage);
|
|
2830
3086
|
}
|
|
2831
3087
|
});
|
|
2832
3088
|
};
|
|
2833
3089
|
const htmxRoutes = () => {
|
|
2834
|
-
if (!
|
|
3090
|
+
if (!config2.htmx) {
|
|
2835
3091
|
return new Elysia;
|
|
2836
3092
|
}
|
|
2837
|
-
const renderers = resolveRenderers(typeof
|
|
3093
|
+
const renderers = resolveRenderers(typeof config2.htmx === "object" ? config2.htmx.render : undefined);
|
|
2838
3094
|
return new Elysia().post(`${path}/message`, async ({ body }) => {
|
|
2839
3095
|
const requestBody = body && typeof body === "object" ? body : {};
|
|
2840
3096
|
const rawContent = "content" in requestBody ? String(requestBody.content) : undefined;
|
|
@@ -2872,21 +3128,21 @@ var aiChat = (config) => {
|
|
|
2872
3128
|
}
|
|
2873
3129
|
const parsed = parseProvider(conversation.messages.at(EXCLUDE_LAST_OFFSET)?.content ?? "");
|
|
2874
3130
|
const { providerName } = parsed;
|
|
2875
|
-
const model = resolveModel(
|
|
3131
|
+
const model = resolveModel(config2, parsed);
|
|
2876
3132
|
const controller = new AbortController;
|
|
2877
3133
|
abortControllers.set(conversationId, controller);
|
|
2878
3134
|
const history = getHistory(conversation);
|
|
2879
3135
|
const lastMsg = conversation.messages.at(EXCLUDE_LAST_OFFSET);
|
|
2880
3136
|
const userMessage = buildUserMessage(lastMsg?.content ?? "", lastMsg?.attachments);
|
|
2881
3137
|
const sseStream = streamAIToSSE(conversationId, messageId, {
|
|
2882
|
-
maxTurns:
|
|
3138
|
+
maxTurns: config2.maxTurns,
|
|
2883
3139
|
messages: [...history.slice(0, EXCLUDE_LAST_OFFSET), userMessage],
|
|
2884
3140
|
model,
|
|
2885
|
-
provider:
|
|
3141
|
+
provider: config2.provider(providerName),
|
|
2886
3142
|
signal: controller.signal,
|
|
2887
|
-
systemPrompt:
|
|
2888
|
-
reasoning: resolveReasoning(
|
|
2889
|
-
tools: resolveTools(
|
|
3143
|
+
systemPrompt: config2.systemPrompt,
|
|
3144
|
+
reasoning: resolveReasoning(config2, providerName, model),
|
|
3145
|
+
tools: resolveTools(config2, providerName, model)
|
|
2890
3146
|
}, renderers);
|
|
2891
3147
|
for await (const event of sseStream) {
|
|
2892
3148
|
yield event;
|
|
@@ -3742,12 +3998,12 @@ var createAIStream = (path, conversationId) => {
|
|
|
3742
3998
|
let currentMessages = [];
|
|
3743
3999
|
let activeConversationId = conversationId ?? null;
|
|
3744
4000
|
const syncState = () => {
|
|
3745
|
-
const
|
|
3746
|
-
const convId = activeConversationId ??
|
|
3747
|
-
const conversation = convId ?
|
|
3748
|
-
activeConversationId = convId ??
|
|
3749
|
-
currentError =
|
|
3750
|
-
currentIsStreaming =
|
|
4001
|
+
const snapshot2 = store.getSnapshot();
|
|
4002
|
+
const convId = activeConversationId ?? snapshot2.activeConversationId;
|
|
4003
|
+
const conversation = convId ? snapshot2.conversations.get(convId) : undefined;
|
|
4004
|
+
activeConversationId = convId ?? snapshot2.activeConversationId;
|
|
4005
|
+
currentError = snapshot2.error;
|
|
4006
|
+
currentIsStreaming = snapshot2.isStreaming;
|
|
3751
4007
|
currentMessages = conversation?.messages ?? [];
|
|
3752
4008
|
listeners.forEach((listener) => listener());
|
|
3753
4009
|
};
|
|
@@ -3828,35 +4084,35 @@ var readOAuth2ErrorDetail = async (response) => {
|
|
|
3828
4084
|
const text = await response.clone().text().catch(() => "");
|
|
3829
4085
|
return text.trim();
|
|
3830
4086
|
};
|
|
3831
|
-
var createOAuth2ClientCredentialsTokenSource = (
|
|
3832
|
-
const fetchImpl =
|
|
3833
|
-
const now =
|
|
3834
|
-
const skewMs =
|
|
3835
|
-
const fallbackTtl =
|
|
3836
|
-
const scope = Array.isArray(
|
|
4087
|
+
var createOAuth2ClientCredentialsTokenSource = (config2) => {
|
|
4088
|
+
const fetchImpl = config2.fetch ?? fetch;
|
|
4089
|
+
const now = config2.now ?? (() => Date.now());
|
|
4090
|
+
const skewMs = config2.expirySkewMs ?? 60000;
|
|
4091
|
+
const fallbackTtl = config2.fallbackTtlSeconds ?? 3600;
|
|
4092
|
+
const scope = Array.isArray(config2.scope) ? config2.scope.join(" ") : config2.scope;
|
|
3837
4093
|
let cached = null;
|
|
3838
4094
|
let inflight = null;
|
|
3839
4095
|
const requestToken = async () => {
|
|
3840
4096
|
const body = new URLSearchParams({ grant_type: "client_credentials" });
|
|
3841
4097
|
if (scope)
|
|
3842
4098
|
body.set("scope", scope);
|
|
3843
|
-
if (
|
|
3844
|
-
body.set("audience",
|
|
3845
|
-
for (const [key, value] of Object.entries(
|
|
4099
|
+
if (config2.audience)
|
|
4100
|
+
body.set("audience", config2.audience);
|
|
4101
|
+
for (const [key, value] of Object.entries(config2.extraParams ?? {})) {
|
|
3846
4102
|
body.set(key, value);
|
|
3847
4103
|
}
|
|
3848
4104
|
const headers = {
|
|
3849
4105
|
Accept: "application/json",
|
|
3850
4106
|
"Content-Type": "application/x-www-form-urlencoded"
|
|
3851
4107
|
};
|
|
3852
|
-
if (
|
|
3853
|
-
const encoded = btoa(`${
|
|
4108
|
+
if (config2.authStyle === "basic") {
|
|
4109
|
+
const encoded = btoa(`${config2.clientId}:${config2.clientSecret}`);
|
|
3854
4110
|
headers.Authorization = `Basic ${encoded}`;
|
|
3855
4111
|
} else {
|
|
3856
|
-
body.set("client_id",
|
|
3857
|
-
body.set("client_secret",
|
|
4112
|
+
body.set("client_id", config2.clientId);
|
|
4113
|
+
body.set("client_secret", config2.clientSecret);
|
|
3858
4114
|
}
|
|
3859
|
-
const response = await fetchImpl(
|
|
4115
|
+
const response = await fetchImpl(config2.tokenUrl, {
|
|
3860
4116
|
body,
|
|
3861
4117
|
headers,
|
|
3862
4118
|
method: "POST"
|
|
@@ -3888,13 +4144,114 @@ var createOAuth2ClientCredentialsTokenSource = (config) => {
|
|
|
3888
4144
|
return inflight;
|
|
3889
4145
|
};
|
|
3890
4146
|
};
|
|
4147
|
+
// src/ai/providerStatusMonitor.ts
|
|
4148
|
+
var API_COMPONENT_BY_PROVIDER = {
|
|
4149
|
+
anthropic: "Claude API (api.anthropic.com)",
|
|
4150
|
+
openai: "Chat Completions"
|
|
4151
|
+
};
|
|
4152
|
+
var AVAILABLE_COMPONENT_STATUSES = new Set([
|
|
4153
|
+
"operational",
|
|
4154
|
+
"degraded_performance"
|
|
4155
|
+
]);
|
|
4156
|
+
var AVAILABLE_PAGE_INDICATORS = new Set(["none", "minor"]);
|
|
4157
|
+
var isRecord6 = (value) => typeof value === "object" && value !== null;
|
|
4158
|
+
var readString = (record, key) => typeof record[key] === "string" ? record[key] : "";
|
|
4159
|
+
var findComponentStatus = (body, componentName) => {
|
|
4160
|
+
if (!isRecord6(body) || !Array.isArray(body.components))
|
|
4161
|
+
return null;
|
|
4162
|
+
const match = body.components.filter(isRecord6).find((component) => readString(component, "name") === componentName);
|
|
4163
|
+
return match ? readString(match, "status") : null;
|
|
4164
|
+
};
|
|
4165
|
+
var readPageIndicator = (body) => {
|
|
4166
|
+
if (!isRecord6(body) || !isRecord6(body.status))
|
|
4167
|
+
return null;
|
|
4168
|
+
const indicator = readString(body.status, "indicator");
|
|
4169
|
+
return indicator === "" ? null : indicator;
|
|
4170
|
+
};
|
|
4171
|
+
var fetchJson = async (url, signal) => {
|
|
4172
|
+
const response = await fetch(url, { signal });
|
|
4173
|
+
if (!response.ok)
|
|
4174
|
+
throw new Error(`status fetch ${response.status}`);
|
|
4175
|
+
return response.json();
|
|
4176
|
+
};
|
|
4177
|
+
var unknownStatus = (provider) => ({
|
|
4178
|
+
available: true,
|
|
4179
|
+
checkedAt: Date.now(),
|
|
4180
|
+
componentName: null,
|
|
4181
|
+
indicator: "unknown",
|
|
4182
|
+
provider,
|
|
4183
|
+
reason: "",
|
|
4184
|
+
statusPageUrl: providerStatusPage(provider)
|
|
4185
|
+
});
|
|
4186
|
+
var fetchProviderApiStatus = async (provider, signal) => {
|
|
4187
|
+
const base = PROVIDER_STATUS_PAGES[provider];
|
|
4188
|
+
if (base === undefined)
|
|
4189
|
+
return unknownStatus(provider);
|
|
4190
|
+
try {
|
|
4191
|
+
const componentName = API_COMPONENT_BY_PROVIDER[provider] ?? null;
|
|
4192
|
+
if (componentName !== null) {
|
|
4193
|
+
const components = await fetchJson(`${base}/api/v2/components.json`, signal);
|
|
4194
|
+
const status = findComponentStatus(components, componentName);
|
|
4195
|
+
if (status !== null) {
|
|
4196
|
+
const available2 = AVAILABLE_COMPONENT_STATUSES.has(status);
|
|
4197
|
+
return {
|
|
4198
|
+
available: available2,
|
|
4199
|
+
checkedAt: Date.now(),
|
|
4200
|
+
componentName,
|
|
4201
|
+
indicator: status,
|
|
4202
|
+
provider,
|
|
4203
|
+
reason: available2 ? "" : `${status} on ${componentName}`,
|
|
4204
|
+
statusPageUrl: base
|
|
4205
|
+
};
|
|
4206
|
+
}
|
|
4207
|
+
}
|
|
4208
|
+
const summary = await fetchJson(`${base}/api/v2/status.json`, signal);
|
|
4209
|
+
const indicator = readPageIndicator(summary);
|
|
4210
|
+
if (indicator === null)
|
|
4211
|
+
return unknownStatus(provider);
|
|
4212
|
+
const available = AVAILABLE_PAGE_INDICATORS.has(indicator);
|
|
4213
|
+
return {
|
|
4214
|
+
available,
|
|
4215
|
+
checkedAt: Date.now(),
|
|
4216
|
+
componentName: null,
|
|
4217
|
+
indicator,
|
|
4218
|
+
provider,
|
|
4219
|
+
reason: available ? "" : `page status: ${indicator}`,
|
|
4220
|
+
statusPageUrl: base
|
|
4221
|
+
};
|
|
4222
|
+
} catch {
|
|
4223
|
+
return unknownStatus(provider);
|
|
4224
|
+
}
|
|
4225
|
+
};
|
|
4226
|
+
var startProviderStatusMonitor = (options) => {
|
|
4227
|
+
const providers = options?.providers ?? ["anthropic", "openai"];
|
|
4228
|
+
const intervalMs = options?.intervalMs ?? 60000;
|
|
4229
|
+
const tick = async () => {
|
|
4230
|
+
const results = await Promise.all(providers.map((provider) => fetchProviderApiStatus(provider)));
|
|
4231
|
+
for (const status of results) {
|
|
4232
|
+
setProviderAvailability(status.provider, {
|
|
4233
|
+
available: status.available,
|
|
4234
|
+
indicator: status.indicator,
|
|
4235
|
+
reason: status.reason
|
|
4236
|
+
});
|
|
4237
|
+
options?.onUpdate?.(status);
|
|
4238
|
+
}
|
|
4239
|
+
};
|
|
4240
|
+
tick();
|
|
4241
|
+
const timer = setInterval(() => void tick(), intervalMs);
|
|
4242
|
+
return () => clearInterval(timer);
|
|
4243
|
+
};
|
|
3891
4244
|
export {
|
|
3892
4245
|
xai,
|
|
4246
|
+
withResilience,
|
|
3893
4247
|
streamAIToSSE,
|
|
3894
4248
|
streamAI,
|
|
4249
|
+
startProviderStatusMonitor,
|
|
4250
|
+
setProviderAvailability,
|
|
3895
4251
|
serverMessageToAction,
|
|
3896
4252
|
serializeAIMessage,
|
|
3897
4253
|
resolveRenderers,
|
|
4254
|
+
providerStatusPage,
|
|
3898
4255
|
parseAIMessage,
|
|
3899
4256
|
openaiResponses,
|
|
3900
4257
|
openaiCompatible,
|
|
@@ -3904,11 +4261,13 @@ export {
|
|
|
3904
4261
|
mistralai,
|
|
3905
4262
|
meta,
|
|
3906
4263
|
google,
|
|
4264
|
+
getProviderHealth,
|
|
3907
4265
|
generateObjectAI,
|
|
3908
4266
|
generateId,
|
|
3909
4267
|
generateAIWithTools,
|
|
3910
4268
|
generateAI,
|
|
3911
4269
|
gemini,
|
|
4270
|
+
fetchProviderApiStatus,
|
|
3912
4271
|
deepseek,
|
|
3913
4272
|
createSyncConversationStore,
|
|
3914
4273
|
createOAuth2ClientCredentialsTokenSource,
|
|
@@ -3916,10 +4275,13 @@ export {
|
|
|
3916
4275
|
createConversationManager,
|
|
3917
4276
|
createAIStream,
|
|
3918
4277
|
createAIConnection,
|
|
4278
|
+
configureProviderResilience,
|
|
3919
4279
|
anthropic,
|
|
3920
4280
|
alibaba,
|
|
3921
|
-
aiChat
|
|
4281
|
+
aiChat,
|
|
4282
|
+
ProviderError,
|
|
4283
|
+
PROVIDER_STATUS_PAGES
|
|
3922
4284
|
};
|
|
3923
4285
|
|
|
3924
|
-
//# debugId=
|
|
4286
|
+
//# debugId=E077A15D83F1F94464756E2164756E21
|
|
3925
4287
|
//# sourceMappingURL=index.js.map
|