@umituz/web-ai 0.0.0-stage → 1.0.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/LICENSE +21 -0
- package/README.md +98 -2
- package/dist/AIErrors-B2ogd1PB.d.ts +197 -0
- package/dist/AIErrors-C5EasV_c.d.cts +197 -0
- package/dist/ContentGeneration-B6iK2B33.d.cts +93 -0
- package/dist/ContentGeneration-B6iK2B33.d.ts +93 -0
- package/dist/ProviderConfig-DIFa9nmG.d.cts +108 -0
- package/dist/ProviderConfig-DIFa9nmG.d.ts +108 -0
- package/dist/cache-manager-Df9LLYGA.d.cts +31 -0
- package/dist/cache-manager-Df9LLYGA.d.ts +31 -0
- package/dist/chat/index.cjs +625 -0
- package/dist/chat/index.cjs.map +1 -0
- package/dist/chat/index.d.cts +25 -0
- package/dist/chat/index.d.ts +25 -0
- package/dist/chat/index.js +618 -0
- package/dist/chat/index.js.map +1 -0
- package/dist/chat.service-CL1qtYxe.d.cts +124 -0
- package/dist/chat.service-q05GnhEm.d.ts +124 -0
- package/dist/content/index.cjs +3976 -0
- package/dist/content/index.cjs.map +1 -0
- package/dist/content/index.d.cts +224 -0
- package/dist/content/index.d.ts +224 -0
- package/dist/content/index.js +3945 -0
- package/dist/content/index.js.map +1 -0
- package/dist/core/index.cjs +1137 -0
- package/dist/core/index.cjs.map +1 -0
- package/dist/core/index.d.cts +204 -0
- package/dist/core/index.d.ts +204 -0
- package/dist/core/index.js +1064 -0
- package/dist/core/index.js.map +1 -0
- package/dist/create-ai-DOI9jD3v.d.cts +251 -0
- package/dist/create-ai-DXEoke8-.d.ts +251 -0
- package/dist/hooks/index.cjs +5246 -0
- package/dist/hooks/index.cjs.map +1 -0
- package/dist/hooks/index.d.cts +210 -0
- package/dist/hooks/index.d.ts +210 -0
- package/dist/hooks/index.js +5235 -0
- package/dist/hooks/index.js.map +1 -0
- package/dist/index-DIHz8_OJ.d.ts +48 -0
- package/dist/index-DKHNcMbl.d.cts +48 -0
- package/dist/index-fXjxTR5W.d.cts +66 -0
- package/dist/index-fXjxTR5W.d.ts +66 -0
- package/dist/index.cjs +3309 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +11 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +3197 -0
- package/dist/index.js.map +1 -0
- package/dist/provider.factory-BxsfK4WW.d.cts +110 -0
- package/dist/provider.factory-DfDfk0dK.d.ts +110 -0
- package/dist/providers/index.cjs +2567 -0
- package/dist/providers/index.cjs.map +1 -0
- package/dist/providers/index.d.cts +332 -0
- package/dist/providers/index.d.ts +332 -0
- package/dist/providers/index.js +2533 -0
- package/dist/providers/index.js.map +1 -0
- package/dist/request-deduplicator.util-NDo0_n1y.d.cts +50 -0
- package/dist/request-deduplicator.util-NDo0_n1y.d.ts +50 -0
- package/dist/wizard/index.cjs +230 -0
- package/dist/wizard/index.cjs.map +1 -0
- package/dist/wizard/index.d.cts +86 -0
- package/dist/wizard/index.d.ts +86 -0
- package/dist/wizard/index.js +223 -0
- package/dist/wizard/index.js.map +1 -0
- package/package.json +114 -4
package/dist/index.js
ADDED
|
@@ -0,0 +1,3197 @@
|
|
|
1
|
+
// src/core/tiers/index.ts
|
|
2
|
+
var TEXT_TIERS = ["fast", "balanced", "powerful"];
|
|
3
|
+
function isTextTier(value) {
|
|
4
|
+
return typeof value === "string" && TEXT_TIERS.includes(value);
|
|
5
|
+
}
|
|
6
|
+
function resolveModel(tiers, tier, providerId) {
|
|
7
|
+
const model = tiers[tier] ?? tiers.balanced;
|
|
8
|
+
if (!model) {
|
|
9
|
+
throw new Error(
|
|
10
|
+
`${providerId} has no model configured for tier "${tier}" \u2014 pass an explicit model or configure the tier map`
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
return model;
|
|
14
|
+
}
|
|
15
|
+
function mergeTierOverrides(defaults, overrides) {
|
|
16
|
+
if (!overrides) return defaults;
|
|
17
|
+
return { ...defaults, ...overrides };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// src/core/errors/ai-error-types.ts
|
|
21
|
+
var AIErrorType = /* @__PURE__ */ ((AIErrorType2) => {
|
|
22
|
+
AIErrorType2["INVALID_API_KEY"] = "INVALID_API_KEY";
|
|
23
|
+
AIErrorType2["MISSING_CONFIG"] = "MISSING_CONFIG";
|
|
24
|
+
AIErrorType2["NETWORK_ERROR"] = "NETWORK_ERROR";
|
|
25
|
+
AIErrorType2["ABORT_ERROR"] = "ABORT_ERROR";
|
|
26
|
+
AIErrorType2["RATE_LIMIT_ERROR"] = "RATE_LIMIT_ERROR";
|
|
27
|
+
AIErrorType2["SERVER_ERROR"] = "SERVER_ERROR";
|
|
28
|
+
AIErrorType2["CLIENT_ERROR"] = "CLIENT_ERROR";
|
|
29
|
+
AIErrorType2["UNKNOWN_ERROR"] = "UNKNOWN_ERROR";
|
|
30
|
+
return AIErrorType2;
|
|
31
|
+
})(AIErrorType || {});
|
|
32
|
+
var USER_FRIENDLY_ERROR_MESSAGES = {
|
|
33
|
+
["INVALID_API_KEY" /* INVALID_API_KEY */]: "Please check your API key and try again.",
|
|
34
|
+
["MISSING_CONFIG" /* MISSING_CONFIG */]: "The provider is not configured. Please create a provider with createAI first.",
|
|
35
|
+
["NETWORK_ERROR" /* NETWORK_ERROR */]: "Network error. Please check your connection and try again.",
|
|
36
|
+
["ABORT_ERROR" /* ABORT_ERROR */]: "Request was cancelled.",
|
|
37
|
+
["RATE_LIMIT_ERROR" /* RATE_LIMIT_ERROR */]: "You've reached the rate limit. Please wait a moment and try again.",
|
|
38
|
+
["SERVER_ERROR" /* SERVER_ERROR */]: "The AI service is temporarily unavailable. Please try again later.",
|
|
39
|
+
["CLIENT_ERROR" /* CLIENT_ERROR */]: "The request was invalid. Please check your input and try again.",
|
|
40
|
+
["UNKNOWN_ERROR" /* UNKNOWN_ERROR */]: "An unexpected error occurred. Please try again."
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// src/core/errors/ai-error.ts
|
|
44
|
+
var KNOWN_ERROR_TYPES = new Set(Object.values(AIErrorType));
|
|
45
|
+
function mapCodeToType(code) {
|
|
46
|
+
switch (code) {
|
|
47
|
+
case "VALIDATION_ERROR":
|
|
48
|
+
case "CLIENT_ERROR":
|
|
49
|
+
return "CLIENT_ERROR" /* CLIENT_ERROR */;
|
|
50
|
+
case "INVALID_API_KEY":
|
|
51
|
+
return "INVALID_API_KEY" /* INVALID_API_KEY */;
|
|
52
|
+
case "MISSING_CONFIG":
|
|
53
|
+
case "PROVIDER_UNAVAILABLE":
|
|
54
|
+
return "MISSING_CONFIG" /* MISSING_CONFIG */;
|
|
55
|
+
case "PROVIDER_QUOTA_EXHAUSTED":
|
|
56
|
+
case "RATE_LIMIT_ERROR":
|
|
57
|
+
return "RATE_LIMIT_ERROR" /* RATE_LIMIT_ERROR */;
|
|
58
|
+
case "NETWORK_ERROR":
|
|
59
|
+
case "TIMEOUT":
|
|
60
|
+
return "NETWORK_ERROR" /* NETWORK_ERROR */;
|
|
61
|
+
case "ABORT_ERROR":
|
|
62
|
+
case "ABORTED":
|
|
63
|
+
return "ABORT_ERROR" /* ABORT_ERROR */;
|
|
64
|
+
case "SERVER_ERROR":
|
|
65
|
+
case "PROVIDER_ERROR":
|
|
66
|
+
case "NO_RESULT":
|
|
67
|
+
return "SERVER_ERROR" /* SERVER_ERROR */;
|
|
68
|
+
default:
|
|
69
|
+
return "UNKNOWN_ERROR" /* UNKNOWN_ERROR */;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function mapMessageToCode(message, fallback = "UNKNOWN") {
|
|
73
|
+
const lower = message.toLowerCase();
|
|
74
|
+
if (lower.includes("validation")) return "VALIDATION_ERROR";
|
|
75
|
+
if (lower.includes("quota")) return "PROVIDER_QUOTA_EXHAUSTED";
|
|
76
|
+
if (lower.includes("unavailable")) return "PROVIDER_UNAVAILABLE";
|
|
77
|
+
if (lower.includes("timeout") || lower.includes("aborted")) {
|
|
78
|
+
return lower.includes("aborted") ? "ABORTED" : "TIMEOUT";
|
|
79
|
+
}
|
|
80
|
+
if (lower.includes("parse") || lower.includes("json")) return "PARSE_ERROR";
|
|
81
|
+
if (lower.includes("no result")) return "NO_RESULT";
|
|
82
|
+
if (lower.includes("network") || lower.includes("fetch")) return "NETWORK_ERROR";
|
|
83
|
+
return fallback;
|
|
84
|
+
}
|
|
85
|
+
var AIError = class _AIError extends Error {
|
|
86
|
+
type;
|
|
87
|
+
code;
|
|
88
|
+
cause;
|
|
89
|
+
metadata;
|
|
90
|
+
constructor(typeOrMessage, messageOrCode, cause, metadata = {}) {
|
|
91
|
+
let effectiveType;
|
|
92
|
+
let effectiveMessage;
|
|
93
|
+
let effectiveCode;
|
|
94
|
+
if (KNOWN_ERROR_TYPES.has(typeOrMessage)) {
|
|
95
|
+
effectiveType = typeOrMessage;
|
|
96
|
+
effectiveMessage = messageOrCode ?? "";
|
|
97
|
+
effectiveCode = effectiveType;
|
|
98
|
+
} else {
|
|
99
|
+
effectiveMessage = typeOrMessage;
|
|
100
|
+
effectiveCode = messageOrCode ?? "UNKNOWN";
|
|
101
|
+
effectiveType = mapCodeToType(effectiveCode);
|
|
102
|
+
}
|
|
103
|
+
super(effectiveMessage);
|
|
104
|
+
this.name = "AIError";
|
|
105
|
+
this.type = effectiveType;
|
|
106
|
+
this.code = effectiveCode;
|
|
107
|
+
this.cause = cause;
|
|
108
|
+
this.metadata = metadata;
|
|
109
|
+
}
|
|
110
|
+
static from(error, fallbackCode = "UNKNOWN") {
|
|
111
|
+
if (error instanceof _AIError) return error;
|
|
112
|
+
if (error instanceof Error) {
|
|
113
|
+
const code = mapMessageToCode(error.message, fallbackCode);
|
|
114
|
+
return new _AIError(error.message, code, error);
|
|
115
|
+
}
|
|
116
|
+
return new _AIError(String(error), fallbackCode, error);
|
|
117
|
+
}
|
|
118
|
+
toString() {
|
|
119
|
+
const statusPart = typeof this.metadata.status === "number" ? ` (HTTP ${this.metadata.status})` : "";
|
|
120
|
+
return `${this.name} [${this.type}]: ${this.message}${statusPart}`;
|
|
121
|
+
}
|
|
122
|
+
toJSON() {
|
|
123
|
+
return {
|
|
124
|
+
name: this.name,
|
|
125
|
+
type: this.type,
|
|
126
|
+
code: this.code,
|
|
127
|
+
message: this.message,
|
|
128
|
+
metadata: this.metadata
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
// src/core/errors/error-classifier.ts
|
|
134
|
+
var DEFAULT_RETRYABLE_TYPES = /* @__PURE__ */ new Set([
|
|
135
|
+
"NETWORK_ERROR" /* NETWORK_ERROR */,
|
|
136
|
+
"RATE_LIMIT_ERROR" /* RATE_LIMIT_ERROR */,
|
|
137
|
+
"SERVER_ERROR" /* SERVER_ERROR */
|
|
138
|
+
]);
|
|
139
|
+
var DEFAULT_AUTH_TYPES = /* @__PURE__ */ new Set([
|
|
140
|
+
"INVALID_API_KEY" /* INVALID_API_KEY */
|
|
141
|
+
]);
|
|
142
|
+
var DEFAULT_ERROR_CLASSIFICATION_RULE = {
|
|
143
|
+
retryable: DEFAULT_RETRYABLE_TYPES,
|
|
144
|
+
auth: DEFAULT_AUTH_TYPES
|
|
145
|
+
};
|
|
146
|
+
function isRetryableErrorType(type, rule = DEFAULT_ERROR_CLASSIFICATION_RULE) {
|
|
147
|
+
return rule.retryable.has(type);
|
|
148
|
+
}
|
|
149
|
+
function isAuthErrorType(type, rule = DEFAULT_ERROR_CLASSIFICATION_RULE) {
|
|
150
|
+
return rule.auth.has(type);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// src/core/errors/http-status-classifier.ts
|
|
154
|
+
var DEFAULT_HTTP_STATUS_RULE = {
|
|
155
|
+
unauthorizedStatuses: /* @__PURE__ */ new Set([401, 403]),
|
|
156
|
+
rateLimitedStatus: 429,
|
|
157
|
+
serverErrorThreshold: 500,
|
|
158
|
+
clientErrorThreshold: 400
|
|
159
|
+
};
|
|
160
|
+
function classifyHttpStatus(status, rule = DEFAULT_HTTP_STATUS_RULE) {
|
|
161
|
+
if (rule.unauthorizedStatuses.has(status)) return "INVALID_API_KEY" /* INVALID_API_KEY */;
|
|
162
|
+
if (status === rule.rateLimitedStatus) return "RATE_LIMIT_ERROR" /* RATE_LIMIT_ERROR */;
|
|
163
|
+
if (status >= rule.serverErrorThreshold) return "SERVER_ERROR" /* SERVER_ERROR */;
|
|
164
|
+
if (status >= rule.clientErrorThreshold) return "CLIENT_ERROR" /* CLIENT_ERROR */;
|
|
165
|
+
return "UNKNOWN_ERROR" /* UNKNOWN_ERROR */;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// src/core/errors/user-friendly-error-resolver.ts
|
|
169
|
+
var KEYWORD_FALLBACK_RULES = [
|
|
170
|
+
{ keywords: ["api key", "unauthorized"], type: "INVALID_API_KEY" /* INVALID_API_KEY */ },
|
|
171
|
+
{ keywords: ["rate limit"], type: "RATE_LIMIT_ERROR" /* RATE_LIMIT_ERROR */ },
|
|
172
|
+
{ keywords: ["network", "fetch"], type: "NETWORK_ERROR" /* NETWORK_ERROR */ },
|
|
173
|
+
{ keywords: ["abort"], type: "ABORT_ERROR" /* ABORT_ERROR */ }
|
|
174
|
+
];
|
|
175
|
+
function resolveUserFriendlyErrorMessage(error) {
|
|
176
|
+
if (error instanceof AIError) {
|
|
177
|
+
return USER_FRIENDLY_ERROR_MESSAGES[error.type];
|
|
178
|
+
}
|
|
179
|
+
if (error instanceof Error) {
|
|
180
|
+
const lower = error.message.toLowerCase();
|
|
181
|
+
for (const rule of KEYWORD_FALLBACK_RULES) {
|
|
182
|
+
if (rule.keywords.some((kw) => lower.includes(kw))) {
|
|
183
|
+
return USER_FRIENDLY_ERROR_MESSAGES[rule.type];
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
if (error.message.length > 0) return error.message;
|
|
187
|
+
}
|
|
188
|
+
return USER_FRIENDLY_ERROR_MESSAGES["UNKNOWN_ERROR" /* UNKNOWN_ERROR */];
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// src/core/errors/error-resolver.ts
|
|
192
|
+
var resolveUserFriendlyMessage = resolveUserFriendlyErrorMessage;
|
|
193
|
+
function formatErrorForLogging(error) {
|
|
194
|
+
if (error instanceof AIError) {
|
|
195
|
+
return {
|
|
196
|
+
message: error.message,
|
|
197
|
+
type: error.type,
|
|
198
|
+
stack: error.stack,
|
|
199
|
+
metadata: error.metadata
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
if (error instanceof Error) {
|
|
203
|
+
return { message: error.message, type: error.name, stack: error.stack };
|
|
204
|
+
}
|
|
205
|
+
return { message: String(error), type: typeof error };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// src/core/errors/response-error-message-extractor.ts
|
|
209
|
+
function extractErrorMessageFromJsonBody(body) {
|
|
210
|
+
if (!body || typeof body !== "object") return null;
|
|
211
|
+
const candidate = body;
|
|
212
|
+
const message = candidate.error?.message ?? candidate.message;
|
|
213
|
+
if (typeof message === "string" && message.length > 0) return message;
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
function buildHttpStatusFallback(status, statusText) {
|
|
217
|
+
const trimmedStatusText = statusText.trim();
|
|
218
|
+
if (trimmedStatusText.length > 0) {
|
|
219
|
+
return `HTTP ${status}: ${trimmedStatusText}`;
|
|
220
|
+
}
|
|
221
|
+
return `HTTP ${status}`;
|
|
222
|
+
}
|
|
223
|
+
async function extractErrorMessageFromResponse(response) {
|
|
224
|
+
const body = await response.json().catch(() => null);
|
|
225
|
+
const fromBody = extractErrorMessageFromJsonBody(body);
|
|
226
|
+
if (fromBody) return fromBody;
|
|
227
|
+
return buildHttpStatusFallback(response.status, response.statusText);
|
|
228
|
+
}
|
|
229
|
+
function describeAIErrorType(type) {
|
|
230
|
+
return `AIError[${type}]`;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// src/core/executor/retry-executor.strategy.ts
|
|
234
|
+
var RetryExecutor = class {
|
|
235
|
+
constructor(retryManager2, options) {
|
|
236
|
+
this.retryManager = retryManager2;
|
|
237
|
+
this.options = options;
|
|
238
|
+
}
|
|
239
|
+
retryManager;
|
|
240
|
+
options;
|
|
241
|
+
execute(fn, ...args) {
|
|
242
|
+
return this.retryManager.execute(() => fn(...args), this.options);
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
// src/core/executor/queue-executor.strategy.ts
|
|
247
|
+
var QueueExecutor = class {
|
|
248
|
+
constructor(queue, priority) {
|
|
249
|
+
this.queue = queue;
|
|
250
|
+
this.priority = priority;
|
|
251
|
+
}
|
|
252
|
+
queue;
|
|
253
|
+
priority;
|
|
254
|
+
execute(fn, ...args) {
|
|
255
|
+
return this.queue.add(() => fn(...args), this.priority);
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
// src/core/executor/dedupe-executor.strategy.ts
|
|
260
|
+
var DedupeExecutor = class {
|
|
261
|
+
constructor(deduplicator, options) {
|
|
262
|
+
this.deduplicator = deduplicator;
|
|
263
|
+
this.options = options;
|
|
264
|
+
}
|
|
265
|
+
deduplicator;
|
|
266
|
+
options;
|
|
267
|
+
execute(fn, ...args) {
|
|
268
|
+
const key = this.options.generateKey(...args);
|
|
269
|
+
return this.deduplicator.execute(key, () => fn(...args));
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
// src/core/executor/composite-executor.strategy.ts
|
|
274
|
+
var CompositeExecutor = class {
|
|
275
|
+
executors;
|
|
276
|
+
constructor(executors) {
|
|
277
|
+
this.executors = executors;
|
|
278
|
+
}
|
|
279
|
+
execute(fn, ...args) {
|
|
280
|
+
const composed = this.compress(fn);
|
|
281
|
+
return composed(...args);
|
|
282
|
+
}
|
|
283
|
+
compress(fn) {
|
|
284
|
+
return this.executors.reduceRight(
|
|
285
|
+
(innerFn, executor) => (...a) => executor.execute(innerFn, ...a),
|
|
286
|
+
fn
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
// src/core/calculations/lru-key-selector.ts
|
|
292
|
+
function selectLeastRecentlyUsedKey(entries) {
|
|
293
|
+
let oldestKey = null;
|
|
294
|
+
let oldestAccess = Number.POSITIVE_INFINITY;
|
|
295
|
+
for (const [key, entry] of entries.entries()) {
|
|
296
|
+
if (entry.lastAccess < oldestAccess) {
|
|
297
|
+
oldestAccess = entry.lastAccess;
|
|
298
|
+
oldestKey = key;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return oldestKey;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// src/core/calculations/cache-hit-rate.ts
|
|
305
|
+
function calculateCacheHitRate(entries) {
|
|
306
|
+
if (entries.length === 0) return 0;
|
|
307
|
+
const totalAccess = entries.reduce((sum, e) => sum + e.accessCount, 0);
|
|
308
|
+
return totalAccess / entries.length;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// src/core/utils/cache-key-builder.ts
|
|
312
|
+
function buildCacheKey(params) {
|
|
313
|
+
const sortedKeys = Object.keys(params).sort();
|
|
314
|
+
const sortedParams = {};
|
|
315
|
+
for (const key of sortedKeys) {
|
|
316
|
+
sortedParams[key] = params[key];
|
|
317
|
+
}
|
|
318
|
+
return JSON.stringify(sortedParams);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// src/core/utils/cache-manager.ts
|
|
322
|
+
var DEFAULT_CACHE_MAX_SIZE = 100;
|
|
323
|
+
var DEFAULT_CACHE_TTL_MS = 3e5;
|
|
324
|
+
var CacheManager = class {
|
|
325
|
+
cache = /* @__PURE__ */ new Map();
|
|
326
|
+
maxSize;
|
|
327
|
+
ttl;
|
|
328
|
+
constructor(options = {}) {
|
|
329
|
+
this.maxSize = options.maxSize ?? DEFAULT_CACHE_MAX_SIZE;
|
|
330
|
+
this.ttl = options.ttl ?? DEFAULT_CACHE_TTL_MS;
|
|
331
|
+
}
|
|
332
|
+
get(key) {
|
|
333
|
+
const entry = this.cache.get(key);
|
|
334
|
+
if (!entry) return null;
|
|
335
|
+
if (this.isExpired(entry)) {
|
|
336
|
+
this.cache.delete(key);
|
|
337
|
+
return null;
|
|
338
|
+
}
|
|
339
|
+
entry.accessCount += 1;
|
|
340
|
+
entry.lastAccess = Date.now();
|
|
341
|
+
return entry.value;
|
|
342
|
+
}
|
|
343
|
+
set(key, value) {
|
|
344
|
+
if (this.cache.size >= this.maxSize && !this.cache.has(key)) {
|
|
345
|
+
const lruKey = selectLeastRecentlyUsedKey(this.cache);
|
|
346
|
+
if (lruKey !== null) this.cache.delete(lruKey);
|
|
347
|
+
}
|
|
348
|
+
this.cache.set(key, {
|
|
349
|
+
value,
|
|
350
|
+
timestamp: Date.now(),
|
|
351
|
+
accessCount: 1,
|
|
352
|
+
lastAccess: Date.now()
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
has(key) {
|
|
356
|
+
const entry = this.cache.get(key);
|
|
357
|
+
return entry !== void 0 && !this.isExpired(entry);
|
|
358
|
+
}
|
|
359
|
+
delete(key) {
|
|
360
|
+
return this.cache.delete(key);
|
|
361
|
+
}
|
|
362
|
+
clear() {
|
|
363
|
+
this.cache.clear();
|
|
364
|
+
}
|
|
365
|
+
generateKey(params) {
|
|
366
|
+
return buildCacheKey(params);
|
|
367
|
+
}
|
|
368
|
+
getStats() {
|
|
369
|
+
const now = Date.now();
|
|
370
|
+
const entries = Array.from(this.cache.entries()).map(([key, entry]) => ({
|
|
371
|
+
key,
|
|
372
|
+
accessCount: entry.accessCount,
|
|
373
|
+
ageMs: now - entry.timestamp
|
|
374
|
+
}));
|
|
375
|
+
const hitRate = calculateCacheHitRate(
|
|
376
|
+
Array.from(this.cache.values()).map((e) => ({ accessCount: e.accessCount }))
|
|
377
|
+
);
|
|
378
|
+
return {
|
|
379
|
+
size: this.cache.size,
|
|
380
|
+
maxSize: this.maxSize,
|
|
381
|
+
hitRate,
|
|
382
|
+
entries
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
isExpired(entry) {
|
|
386
|
+
return Date.now() - entry.timestamp > this.ttl;
|
|
387
|
+
}
|
|
388
|
+
};
|
|
389
|
+
var cacheManager = new CacheManager();
|
|
390
|
+
|
|
391
|
+
// src/core/calculations/exponential-backoff.ts
|
|
392
|
+
function calculateExponentialBackoff(params) {
|
|
393
|
+
const {
|
|
394
|
+
attempt,
|
|
395
|
+
initialDelay,
|
|
396
|
+
maxDelay,
|
|
397
|
+
backoffMultiplier,
|
|
398
|
+
jitterFactor,
|
|
399
|
+
random = Math.random
|
|
400
|
+
} = params;
|
|
401
|
+
const base = initialDelay * Math.pow(backoffMultiplier, Math.max(0, attempt - 1));
|
|
402
|
+
const capped = Math.min(base, maxDelay);
|
|
403
|
+
const jitterMultiplier = random() * 2 - 1;
|
|
404
|
+
const jitter = capped * jitterFactor * jitterMultiplier;
|
|
405
|
+
return Math.max(0, capped + jitter);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// src/core/constants/identifier.constants.ts
|
|
409
|
+
var MESSAGE_ID_PREFIX = "msg-";
|
|
410
|
+
var CHAT_MESSAGE_ID_SUFFIX_LENGTH = 9;
|
|
411
|
+
var USER_ROLE = "user";
|
|
412
|
+
var ASSISTANT_ROLE = "assistant";
|
|
413
|
+
var QUEUE_PRIORITY_USER_REQUEST = 10;
|
|
414
|
+
var DEFAULT_RETRY_MAX_ATTEMPTS = 3;
|
|
415
|
+
var DEFAULT_RETRY_INITIAL_DELAY_MS = 1e3;
|
|
416
|
+
var DEFAULT_RETRY_MAX_DELAY_MS = 1e4;
|
|
417
|
+
var DEFAULT_RETRY_BACKOFF_MULTIPLIER = 2;
|
|
418
|
+
var DEFAULT_RETRY_JITTER_FACTOR = 0.1;
|
|
419
|
+
|
|
420
|
+
// src/core/utils/retry.ts
|
|
421
|
+
var RETRYABLE_MESSAGE_KEYWORDS = [
|
|
422
|
+
"network",
|
|
423
|
+
"timeout",
|
|
424
|
+
"econnrefused",
|
|
425
|
+
"enotfound",
|
|
426
|
+
"etimedout"
|
|
427
|
+
];
|
|
428
|
+
var RetryManager = class {
|
|
429
|
+
defaults;
|
|
430
|
+
constructor() {
|
|
431
|
+
this.defaults = {
|
|
432
|
+
maxAttempts: DEFAULT_RETRY_MAX_ATTEMPTS,
|
|
433
|
+
initialDelay: DEFAULT_RETRY_INITIAL_DELAY_MS,
|
|
434
|
+
maxDelay: DEFAULT_RETRY_MAX_DELAY_MS,
|
|
435
|
+
backoffMultiplier: DEFAULT_RETRY_BACKOFF_MULTIPLIER,
|
|
436
|
+
jitterFactor: DEFAULT_RETRY_JITTER_FACTOR
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
async execute(fn, options = {}) {
|
|
440
|
+
const maxAttempts = options.maxAttempts ?? this.defaults.maxAttempts;
|
|
441
|
+
let lastError;
|
|
442
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
443
|
+
try {
|
|
444
|
+
return await fn();
|
|
445
|
+
} catch (error) {
|
|
446
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
447
|
+
if (attempt >= maxAttempts || !this.shouldRetry(lastError, options)) {
|
|
448
|
+
throw lastError;
|
|
449
|
+
}
|
|
450
|
+
const delay = calculateExponentialBackoff({
|
|
451
|
+
attempt,
|
|
452
|
+
initialDelay: options.initialDelay ?? this.defaults.initialDelay,
|
|
453
|
+
maxDelay: options.maxDelay ?? this.defaults.maxDelay,
|
|
454
|
+
backoffMultiplier: options.backoffMultiplier ?? this.defaults.backoffMultiplier,
|
|
455
|
+
jitterFactor: options.jitterFactor ?? this.defaults.jitterFactor
|
|
456
|
+
});
|
|
457
|
+
await this.delay(delay);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
throw lastError ?? new Error("Retry loop exited without result");
|
|
461
|
+
}
|
|
462
|
+
shouldRetry(error, options) {
|
|
463
|
+
if (options.shouldRetry) return options.shouldRetry(error);
|
|
464
|
+
if (error instanceof AIError) {
|
|
465
|
+
return isRetryableErrorType(error.type, DEFAULT_ERROR_CLASSIFICATION_RULE);
|
|
466
|
+
}
|
|
467
|
+
const lower = error.message.toLowerCase();
|
|
468
|
+
return RETRYABLE_MESSAGE_KEYWORDS.some((kw) => lower.includes(kw));
|
|
469
|
+
}
|
|
470
|
+
delay(ms) {
|
|
471
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
472
|
+
}
|
|
473
|
+
};
|
|
474
|
+
var retryManager = new RetryManager();
|
|
475
|
+
async function withRetry(fn, options) {
|
|
476
|
+
return retryManager.execute(fn, options);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// src/core/utils/request-queue.util.ts
|
|
480
|
+
var DEFAULT_MAX_CONCURRENT = 3;
|
|
481
|
+
var DEFAULT_PRIORITY = 0;
|
|
482
|
+
var DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
|
|
483
|
+
var RequestQueue = class {
|
|
484
|
+
queue = [];
|
|
485
|
+
activeRequests = /* @__PURE__ */ new Set();
|
|
486
|
+
maxConcurrent;
|
|
487
|
+
defaultPriority;
|
|
488
|
+
requestTimeout;
|
|
489
|
+
requestIdCounter = 0;
|
|
490
|
+
constructor(options = {}) {
|
|
491
|
+
this.maxConcurrent = options.maxConcurrent ?? DEFAULT_MAX_CONCURRENT;
|
|
492
|
+
this.defaultPriority = options.defaultPriority ?? DEFAULT_PRIORITY;
|
|
493
|
+
this.requestTimeout = options.requestTimeout ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
494
|
+
}
|
|
495
|
+
add(requestFn, priority) {
|
|
496
|
+
return new Promise((resolve, reject) => {
|
|
497
|
+
const id = `req-${++this.requestIdCounter}`;
|
|
498
|
+
const queuedRequest = {
|
|
499
|
+
id,
|
|
500
|
+
priority: priority ?? this.defaultPriority,
|
|
501
|
+
requestFn,
|
|
502
|
+
resolve,
|
|
503
|
+
reject,
|
|
504
|
+
timestamp: Date.now()
|
|
505
|
+
};
|
|
506
|
+
this.queue.push(queuedRequest);
|
|
507
|
+
this.sortQueue();
|
|
508
|
+
this.process();
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
cancelAll() {
|
|
512
|
+
for (const request of this.queue) {
|
|
513
|
+
request.reject(new Error("Request was cancelled"));
|
|
514
|
+
}
|
|
515
|
+
this.queue = [];
|
|
516
|
+
}
|
|
517
|
+
sortQueue() {
|
|
518
|
+
this.queue.sort((a, b) => {
|
|
519
|
+
if (a.priority !== b.priority) {
|
|
520
|
+
return b.priority - a.priority;
|
|
521
|
+
}
|
|
522
|
+
return a.timestamp - b.timestamp;
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
process() {
|
|
526
|
+
while (this.queue.length > 0 && this.activeRequests.size < this.maxConcurrent) {
|
|
527
|
+
const request = this.queue.shift();
|
|
528
|
+
if (!request) break;
|
|
529
|
+
this.executeRequest(request);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
executeRequest(request) {
|
|
533
|
+
this.activeRequests.add(request.id);
|
|
534
|
+
const timeoutId = setTimeout(() => {
|
|
535
|
+
this.activeRequests.delete(request.id);
|
|
536
|
+
request.reject(new Error(`Request ${request.id} timed out`));
|
|
537
|
+
this.process();
|
|
538
|
+
}, this.requestTimeout);
|
|
539
|
+
request.requestFn().then((result) => {
|
|
540
|
+
clearTimeout(timeoutId);
|
|
541
|
+
request.resolve(result);
|
|
542
|
+
}).catch((error) => {
|
|
543
|
+
clearTimeout(timeoutId);
|
|
544
|
+
request.reject(error);
|
|
545
|
+
}).finally(() => {
|
|
546
|
+
this.activeRequests.delete(request.id);
|
|
547
|
+
this.process();
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
};
|
|
551
|
+
var requestQueue = new RequestQueue();
|
|
552
|
+
|
|
553
|
+
// src/core/utils/request-deduplicator.util.ts
|
|
554
|
+
var DEFAULT_DEDUPE_TTL_MS = 3e4;
|
|
555
|
+
var RequestDeduplicator = class {
|
|
556
|
+
pendingRequests = /* @__PURE__ */ new Map();
|
|
557
|
+
ttl;
|
|
558
|
+
constructor(ttl = DEFAULT_DEDUPE_TTL_MS) {
|
|
559
|
+
this.ttl = ttl;
|
|
560
|
+
}
|
|
561
|
+
execute(key, requestFn) {
|
|
562
|
+
this.cleanup();
|
|
563
|
+
const existing = this.pendingRequests.get(key);
|
|
564
|
+
if (existing) {
|
|
565
|
+
return existing.promise;
|
|
566
|
+
}
|
|
567
|
+
const promise = requestFn();
|
|
568
|
+
this.pendingRequests.set(key, {
|
|
569
|
+
promise,
|
|
570
|
+
timestamp: Date.now()
|
|
571
|
+
});
|
|
572
|
+
promise.catch(() => void 0).then(() => {
|
|
573
|
+
this.pendingRequests.delete(key);
|
|
574
|
+
});
|
|
575
|
+
return promise;
|
|
576
|
+
}
|
|
577
|
+
cancelAll() {
|
|
578
|
+
this.pendingRequests.clear();
|
|
579
|
+
}
|
|
580
|
+
cleanup() {
|
|
581
|
+
const now = Date.now();
|
|
582
|
+
for (const [key, request] of this.pendingRequests.entries()) {
|
|
583
|
+
if (now - request.timestamp > this.ttl) {
|
|
584
|
+
this.pendingRequests.delete(key);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
};
|
|
589
|
+
var requestDeduplicator = new RequestDeduplicator();
|
|
590
|
+
|
|
591
|
+
// src/core/calculations/timing-window.ts
|
|
592
|
+
function shouldCallOnLeadingEdge(params) {
|
|
593
|
+
const { now, lastCallTime, interval, leadingEnabled, isThrottled } = params;
|
|
594
|
+
if (!leadingEnabled) return false;
|
|
595
|
+
if (isThrottled) return false;
|
|
596
|
+
return now - lastCallTime >= interval;
|
|
597
|
+
}
|
|
598
|
+
function calculateTrailingDelay(params) {
|
|
599
|
+
return Math.max(0, params.interval - params.timeSinceLastCall);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// src/core/utils/debounce.ts
|
|
603
|
+
var DEFAULT_DEBOUNCE_DELAY_MS = 300;
|
|
604
|
+
var DEFAULT_THROTTLE_INTERVAL_MS = 1e3;
|
|
605
|
+
var DebounceManager = class {
|
|
606
|
+
timers = /* @__PURE__ */ new Map();
|
|
607
|
+
lastArgs = /* @__PURE__ */ new Map();
|
|
608
|
+
debounce(fn, options = {}) {
|
|
609
|
+
const delay = options.delay ?? DEFAULT_DEBOUNCE_DELAY_MS;
|
|
610
|
+
const leading = options.leading ?? false;
|
|
611
|
+
const trailing = options.trailing ?? true;
|
|
612
|
+
let timer = null;
|
|
613
|
+
let lastCallTime = 0;
|
|
614
|
+
let lastResult;
|
|
615
|
+
return ((...args) => {
|
|
616
|
+
const now = Date.now();
|
|
617
|
+
const shouldCallLeading = shouldCallOnLeadingEdge({
|
|
618
|
+
now,
|
|
619
|
+
lastCallTime,
|
|
620
|
+
interval: delay,
|
|
621
|
+
leadingEnabled: leading,
|
|
622
|
+
isThrottled: timer !== null
|
|
623
|
+
});
|
|
624
|
+
if (timer) {
|
|
625
|
+
clearTimeout(timer);
|
|
626
|
+
timer = null;
|
|
627
|
+
}
|
|
628
|
+
if (shouldCallLeading) {
|
|
629
|
+
lastCallTime = now;
|
|
630
|
+
lastResult = fn(...args);
|
|
631
|
+
return lastResult;
|
|
632
|
+
}
|
|
633
|
+
if (trailing) {
|
|
634
|
+
timer = setTimeout(() => {
|
|
635
|
+
timer = null;
|
|
636
|
+
lastCallTime = Date.now();
|
|
637
|
+
lastResult = fn(...args);
|
|
638
|
+
}, delay);
|
|
639
|
+
}
|
|
640
|
+
return lastResult;
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
debounceByKey(key, fn, delay = DEFAULT_DEBOUNCE_DELAY_MS) {
|
|
644
|
+
return ((...args) => {
|
|
645
|
+
this.lastArgs.set(key, args);
|
|
646
|
+
const existingTimer = this.timers.get(key);
|
|
647
|
+
if (existingTimer) clearTimeout(existingTimer);
|
|
648
|
+
const timer = setTimeout(() => {
|
|
649
|
+
this.timers.delete(key);
|
|
650
|
+
const pendingArgs = this.lastArgs.get(key);
|
|
651
|
+
this.lastArgs.delete(key);
|
|
652
|
+
if (pendingArgs) fn(...pendingArgs);
|
|
653
|
+
}, delay);
|
|
654
|
+
this.timers.set(key, timer);
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
cancelDebounce(key) {
|
|
658
|
+
const timer = this.timers.get(key);
|
|
659
|
+
if (timer) {
|
|
660
|
+
clearTimeout(timer);
|
|
661
|
+
this.timers.delete(key);
|
|
662
|
+
}
|
|
663
|
+
this.lastArgs.delete(key);
|
|
664
|
+
}
|
|
665
|
+
flushDebounce(key, fn) {
|
|
666
|
+
this.cancelDebounce(key);
|
|
667
|
+
const args = this.lastArgs.get(key);
|
|
668
|
+
if (args) {
|
|
669
|
+
fn(...args);
|
|
670
|
+
this.lastArgs.delete(key);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
};
|
|
674
|
+
var ThrottleManager = class {
|
|
675
|
+
timers = /* @__PURE__ */ new Map();
|
|
676
|
+
lastArgs = /* @__PURE__ */ new Map();
|
|
677
|
+
throttle(fn, options = {}) {
|
|
678
|
+
const interval = options.interval ?? DEFAULT_THROTTLE_INTERVAL_MS;
|
|
679
|
+
const leading = options.leading ?? true;
|
|
680
|
+
const trailing = options.trailing ?? false;
|
|
681
|
+
let timer = null;
|
|
682
|
+
let lastCallTime = 0;
|
|
683
|
+
let lastResult;
|
|
684
|
+
let pendingArgs = null;
|
|
685
|
+
return ((...args) => {
|
|
686
|
+
const now = Date.now();
|
|
687
|
+
const timeSinceLastCall = now - lastCallTime;
|
|
688
|
+
const shouldCall = shouldCallOnLeadingEdge({
|
|
689
|
+
now,
|
|
690
|
+
lastCallTime,
|
|
691
|
+
interval,
|
|
692
|
+
leadingEnabled: leading,
|
|
693
|
+
isThrottled: timer !== null
|
|
694
|
+
});
|
|
695
|
+
pendingArgs = args;
|
|
696
|
+
if (shouldCall) {
|
|
697
|
+
lastCallTime = now;
|
|
698
|
+
lastResult = fn(...args);
|
|
699
|
+
return lastResult;
|
|
700
|
+
}
|
|
701
|
+
if (trailing && !timer) {
|
|
702
|
+
const delay = calculateTrailingDelay({ interval, timeSinceLastCall });
|
|
703
|
+
timer = setTimeout(() => {
|
|
704
|
+
timer = null;
|
|
705
|
+
lastCallTime = Date.now();
|
|
706
|
+
if (pendingArgs) {
|
|
707
|
+
lastResult = fn(...pendingArgs);
|
|
708
|
+
pendingArgs = null;
|
|
709
|
+
}
|
|
710
|
+
}, delay);
|
|
711
|
+
}
|
|
712
|
+
return lastResult;
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
throttleByKey(key, fn, interval = DEFAULT_THROTTLE_INTERVAL_MS) {
|
|
716
|
+
return ((...args) => {
|
|
717
|
+
const existingTimer = this.timers.get(key);
|
|
718
|
+
if (existingTimer) {
|
|
719
|
+
this.lastArgs.set(key, args);
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
fn(...args);
|
|
723
|
+
const timer = setTimeout(() => {
|
|
724
|
+
this.timers.delete(key);
|
|
725
|
+
const pendingArgs = this.lastArgs.get(key);
|
|
726
|
+
if (pendingArgs) {
|
|
727
|
+
this.lastArgs.delete(key);
|
|
728
|
+
fn(...pendingArgs);
|
|
729
|
+
const nextTimer = setTimeout(() => {
|
|
730
|
+
this.timers.delete(key);
|
|
731
|
+
}, interval);
|
|
732
|
+
this.timers.set(key, nextTimer);
|
|
733
|
+
}
|
|
734
|
+
}, interval);
|
|
735
|
+
this.timers.set(key, timer);
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
cancelThrottle(key) {
|
|
739
|
+
const timer = this.timers.get(key);
|
|
740
|
+
if (timer) {
|
|
741
|
+
clearTimeout(timer);
|
|
742
|
+
this.timers.delete(key);
|
|
743
|
+
}
|
|
744
|
+
this.lastArgs.delete(key);
|
|
745
|
+
}
|
|
746
|
+
isThrottled(key) {
|
|
747
|
+
return this.timers.has(key);
|
|
748
|
+
}
|
|
749
|
+
};
|
|
750
|
+
var debounceManager = new DebounceManager();
|
|
751
|
+
var throttleManager = new ThrottleManager();
|
|
752
|
+
function debounce(fn, delay) {
|
|
753
|
+
return debounceManager.debounce(fn, { delay });
|
|
754
|
+
}
|
|
755
|
+
function throttle(fn, interval) {
|
|
756
|
+
return throttleManager.throttle(fn, { interval });
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// src/core/utils/api-key-validator.ts
|
|
760
|
+
var DEFAULT_API_KEY_RULE = {
|
|
761
|
+
minLength: 10
|
|
762
|
+
};
|
|
763
|
+
function isValidApiKey(apiKey, rule = DEFAULT_API_KEY_RULE) {
|
|
764
|
+
if (!apiKey) return false;
|
|
765
|
+
return apiKey.trim().length >= rule.minLength;
|
|
766
|
+
}
|
|
767
|
+
function validateApiKey(apiKey, rule = DEFAULT_API_KEY_RULE) {
|
|
768
|
+
if (!apiKey) {
|
|
769
|
+
return { valid: false, reason: "API key is required" };
|
|
770
|
+
}
|
|
771
|
+
const trimmed = apiKey.trim();
|
|
772
|
+
if (trimmed.length === 0) {
|
|
773
|
+
return { valid: false, reason: "API key cannot be empty" };
|
|
774
|
+
}
|
|
775
|
+
if (trimmed.length < rule.minLength) {
|
|
776
|
+
return {
|
|
777
|
+
valid: false,
|
|
778
|
+
reason: `API key must be at least ${rule.minLength} characters`
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
return { valid: true };
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// src/core/utils/coercion.util.ts
|
|
785
|
+
function asRecord(value) {
|
|
786
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return {};
|
|
787
|
+
return value;
|
|
788
|
+
}
|
|
789
|
+
function asStringArray(value, max) {
|
|
790
|
+
if (!Array.isArray(value)) return [];
|
|
791
|
+
return value.filter((entry) => typeof entry === "string" && entry.trim().length > 0).slice(0, max);
|
|
792
|
+
}
|
|
793
|
+
function asObjectArray(value) {
|
|
794
|
+
if (!Array.isArray(value)) return [];
|
|
795
|
+
return value.filter(
|
|
796
|
+
(entry) => typeof entry === "object" && entry !== null && !Array.isArray(entry)
|
|
797
|
+
);
|
|
798
|
+
}
|
|
799
|
+
function asTrimmedOptionalString(value) {
|
|
800
|
+
if (typeof value !== "string") return void 0;
|
|
801
|
+
const trimmed = value.trim();
|
|
802
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
803
|
+
}
|
|
804
|
+
function clampScore(value, min, max) {
|
|
805
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return null;
|
|
806
|
+
return Math.max(min, Math.min(max, Math.round(value)));
|
|
807
|
+
}
|
|
808
|
+
function oneOfCatalog(catalog, value) {
|
|
809
|
+
return catalog.includes(value) ? value : null;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
// src/core/constants/protocol.constants.ts
|
|
813
|
+
var SSE_PREFIX = "data: ";
|
|
814
|
+
var SSE_DONE_MARKER = "[DONE]";
|
|
815
|
+
var JSON_CODE_BLOCK_PATTERN = /```(?:json)?\s*\n?([\s\S]*?)\n?```/;
|
|
816
|
+
var JSON_OBJECT_PATTERN = /\{[\s\S]*\}/;
|
|
817
|
+
var JSON_ARRAY_PATTERN = /\[[\s\S]*\]/;
|
|
818
|
+
|
|
819
|
+
// src/core/parsers/sse-lines.ts
|
|
820
|
+
function isSseDataLine(line) {
|
|
821
|
+
return line.startsWith(SSE_PREFIX) || line.startsWith("data:");
|
|
822
|
+
}
|
|
823
|
+
function extractSseDataPayload(line) {
|
|
824
|
+
if (line.startsWith(SSE_PREFIX)) {
|
|
825
|
+
return line.slice(SSE_PREFIX.length);
|
|
826
|
+
}
|
|
827
|
+
if (line.startsWith("data:")) {
|
|
828
|
+
return line.slice(5);
|
|
829
|
+
}
|
|
830
|
+
return line;
|
|
831
|
+
}
|
|
832
|
+
function isSseDoneMarker(payload) {
|
|
833
|
+
return payload === SSE_DONE_MARKER;
|
|
834
|
+
}
|
|
835
|
+
function splitSseLines(rawChunk) {
|
|
836
|
+
return rawChunk.split("\n").map((l) => l.trim()).filter((l) => l.length > 0);
|
|
837
|
+
}
|
|
838
|
+
function normalizeSseChunk(rawChunk) {
|
|
839
|
+
const lines = splitSseLines(rawChunk);
|
|
840
|
+
const result = [];
|
|
841
|
+
for (const line of lines) {
|
|
842
|
+
if (!isSseDataLine(line)) continue;
|
|
843
|
+
const payload = extractSseDataPayload(line);
|
|
844
|
+
result.push({ payload, isDone: isSseDoneMarker(payload) });
|
|
845
|
+
}
|
|
846
|
+
return result;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// src/core/parsers/streaming-response.parser.ts
|
|
850
|
+
var StreamingResponseParser = class {
|
|
851
|
+
constructor(callbacks, extractDelta) {
|
|
852
|
+
this.callbacks = callbacks;
|
|
853
|
+
this.extractDelta = extractDelta;
|
|
854
|
+
}
|
|
855
|
+
callbacks;
|
|
856
|
+
extractDelta;
|
|
857
|
+
decoder = new TextDecoder();
|
|
858
|
+
async parse(stream) {
|
|
859
|
+
const reader = stream.getReader();
|
|
860
|
+
const contentChunks = [];
|
|
861
|
+
let buffer = "";
|
|
862
|
+
const consumeLine = (rawLine) => {
|
|
863
|
+
const line = rawLine.endsWith("\r") ? rawLine.slice(0, -1) : rawLine;
|
|
864
|
+
const trimmed = line.trim();
|
|
865
|
+
if (trimmed.length === 0 || !isSseDataLine(trimmed)) return;
|
|
866
|
+
const payload = extractSseDataPayload(trimmed);
|
|
867
|
+
if (isSseDoneMarker(payload)) return;
|
|
868
|
+
const content = this.extractDelta(payload);
|
|
869
|
+
if (content !== null) {
|
|
870
|
+
contentChunks.push(content);
|
|
871
|
+
this.callbacks.onChunk(content);
|
|
872
|
+
}
|
|
873
|
+
};
|
|
874
|
+
try {
|
|
875
|
+
while (true) {
|
|
876
|
+
const { done, value } = await reader.read();
|
|
877
|
+
if (done) break;
|
|
878
|
+
buffer += this.decoder.decode(value, { stream: true });
|
|
879
|
+
const lines = buffer.split("\n");
|
|
880
|
+
buffer = lines.pop() ?? "";
|
|
881
|
+
for (const line of lines) consumeLine(line);
|
|
882
|
+
}
|
|
883
|
+
buffer += this.decoder.decode();
|
|
884
|
+
if (buffer.length > 0) consumeLine(buffer);
|
|
885
|
+
} catch (error) {
|
|
886
|
+
if (error instanceof AIError) throw error;
|
|
887
|
+
throw new AIError(
|
|
888
|
+
"NETWORK_ERROR" /* NETWORK_ERROR */,
|
|
889
|
+
"Stream was interrupted",
|
|
890
|
+
error instanceof Error ? error : void 0
|
|
891
|
+
);
|
|
892
|
+
} finally {
|
|
893
|
+
reader.releaseLock();
|
|
894
|
+
}
|
|
895
|
+
return contentChunks.join("");
|
|
896
|
+
}
|
|
897
|
+
};
|
|
898
|
+
|
|
899
|
+
// src/core/parsers/json-content-extractor.ts
|
|
900
|
+
function extractJsonFromCodeFence(content) {
|
|
901
|
+
const match = content.match(JSON_CODE_BLOCK_PATTERN);
|
|
902
|
+
if (match && match[1]) return match[1].trim();
|
|
903
|
+
return null;
|
|
904
|
+
}
|
|
905
|
+
function extractJsonObject(content) {
|
|
906
|
+
const match = content.match(JSON_OBJECT_PATTERN);
|
|
907
|
+
if (match && match[0]) return match[0];
|
|
908
|
+
return null;
|
|
909
|
+
}
|
|
910
|
+
function extractJsonArray(content) {
|
|
911
|
+
const match = content.match(JSON_ARRAY_PATTERN);
|
|
912
|
+
if (match && match[0]) return match[0];
|
|
913
|
+
return null;
|
|
914
|
+
}
|
|
915
|
+
function repairCommonJsonDefects(text) {
|
|
916
|
+
return text.replace(/(:\s*-?\d+(?:\.\d+)?)\s*"/g, "$1").replace(/(:\s*"[^"]*")"/g, "$1");
|
|
917
|
+
}
|
|
918
|
+
function extractJsonCandidate(content) {
|
|
919
|
+
const fromFence = extractJsonFromCodeFence(content);
|
|
920
|
+
if (fromFence) return fromFence;
|
|
921
|
+
const objectIndex = content.indexOf("{");
|
|
922
|
+
const arrayIndex = content.indexOf("[");
|
|
923
|
+
if (objectIndex !== -1 && (arrayIndex === -1 || objectIndex < arrayIndex)) {
|
|
924
|
+
const obj = extractJsonObject(content);
|
|
925
|
+
if (obj) return obj;
|
|
926
|
+
} else if (arrayIndex !== -1) {
|
|
927
|
+
const arr = extractJsonArray(content);
|
|
928
|
+
if (arr) return arr;
|
|
929
|
+
}
|
|
930
|
+
return content.trim();
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
// src/core/parsers/json-response.parser.ts
|
|
934
|
+
var JsonResponseParser = class {
|
|
935
|
+
parse(rawContent) {
|
|
936
|
+
const candidate = extractJsonCandidate(rawContent);
|
|
937
|
+
try {
|
|
938
|
+
return JSON.parse(candidate);
|
|
939
|
+
} catch {
|
|
940
|
+
try {
|
|
941
|
+
return JSON.parse(repairCommonJsonDefects(candidate));
|
|
942
|
+
} catch (error) {
|
|
943
|
+
throw new AIError(
|
|
944
|
+
"UNKNOWN_ERROR" /* UNKNOWN_ERROR */,
|
|
945
|
+
"Failed to parse structured response as JSON",
|
|
946
|
+
error instanceof Error ? error : void 0,
|
|
947
|
+
{ rawContent }
|
|
948
|
+
);
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
};
|
|
953
|
+
|
|
954
|
+
// src/core/parsers/structured-prompt.builder.ts
|
|
955
|
+
var StructuredPromptBuilder = class _StructuredPromptBuilder {
|
|
956
|
+
schema;
|
|
957
|
+
request;
|
|
958
|
+
constructor(request) {
|
|
959
|
+
this.request = request;
|
|
960
|
+
}
|
|
961
|
+
static for(request) {
|
|
962
|
+
return new _StructuredPromptBuilder(request);
|
|
963
|
+
}
|
|
964
|
+
withSchema(schema) {
|
|
965
|
+
this.schema = schema;
|
|
966
|
+
return this;
|
|
967
|
+
}
|
|
968
|
+
build() {
|
|
969
|
+
const schemaSection = this.schema ? `Expected JSON schema:
|
|
970
|
+
${JSON.stringify(this.schema, null, 2)}
|
|
971
|
+
|
|
972
|
+
` : "";
|
|
973
|
+
return "Please generate a JSON response for the following request.\n\n" + schemaSection + `Request:
|
|
974
|
+
${this.request}
|
|
975
|
+
|
|
976
|
+
Respond only with valid JSON, formatted as:
|
|
977
|
+
\`\`\`json
|
|
978
|
+
{...}
|
|
979
|
+
\`\`\``;
|
|
980
|
+
}
|
|
981
|
+
};
|
|
982
|
+
|
|
983
|
+
// src/core/sse/sse-response.service.ts
|
|
984
|
+
var SSE_BRIDGE_EVENTS = {
|
|
985
|
+
delta: "delta",
|
|
986
|
+
done: "done",
|
|
987
|
+
error: "error"
|
|
988
|
+
};
|
|
989
|
+
function createSseResponse(openUpstream, options) {
|
|
990
|
+
const encoder = new TextEncoder();
|
|
991
|
+
const decoder = new TextDecoder();
|
|
992
|
+
const stream = new ReadableStream({
|
|
993
|
+
async start(controller) {
|
|
994
|
+
let closed = false;
|
|
995
|
+
const emit = (event, data) => {
|
|
996
|
+
if (closed) return;
|
|
997
|
+
try {
|
|
998
|
+
controller.enqueue(
|
|
999
|
+
encoder.encode(`event: ${event}
|
|
1000
|
+
data: ${JSON.stringify(data)}
|
|
1001
|
+
|
|
1002
|
+
`)
|
|
1003
|
+
);
|
|
1004
|
+
} catch {
|
|
1005
|
+
closed = true;
|
|
1006
|
+
}
|
|
1007
|
+
};
|
|
1008
|
+
const finish = () => {
|
|
1009
|
+
if (closed) return;
|
|
1010
|
+
closed = true;
|
|
1011
|
+
try {
|
|
1012
|
+
controller.close();
|
|
1013
|
+
} catch {
|
|
1014
|
+
closed = true;
|
|
1015
|
+
}
|
|
1016
|
+
};
|
|
1017
|
+
let delivered = false;
|
|
1018
|
+
const consumeLine = (line) => {
|
|
1019
|
+
const trimmed = line.trim();
|
|
1020
|
+
if (!isSseDataLine(trimmed)) return false;
|
|
1021
|
+
const payload = extractSseDataPayload(trimmed);
|
|
1022
|
+
if (isSseDoneMarker(payload)) return true;
|
|
1023
|
+
const text = options.extractDelta(payload);
|
|
1024
|
+
if (text) {
|
|
1025
|
+
delivered = true;
|
|
1026
|
+
emit(SSE_BRIDGE_EVENTS.delta, { text });
|
|
1027
|
+
}
|
|
1028
|
+
return false;
|
|
1029
|
+
};
|
|
1030
|
+
try {
|
|
1031
|
+
const upstream = await openUpstream();
|
|
1032
|
+
const reader = upstream.getReader();
|
|
1033
|
+
let buffer = "";
|
|
1034
|
+
let finished = false;
|
|
1035
|
+
while (!finished) {
|
|
1036
|
+
const { value, done: readerDone } = await reader.read();
|
|
1037
|
+
if (readerDone) break;
|
|
1038
|
+
buffer += decoder.decode(value, { stream: true });
|
|
1039
|
+
const lines = buffer.split("\n");
|
|
1040
|
+
buffer = lines.pop() ?? "";
|
|
1041
|
+
for (const line of lines) {
|
|
1042
|
+
finished = consumeLine(line);
|
|
1043
|
+
if (finished) break;
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
if (!finished && buffer.length > 0) consumeLine(buffer);
|
|
1047
|
+
emit(SSE_BRIDGE_EVENTS.done, options.donePayload);
|
|
1048
|
+
finish();
|
|
1049
|
+
} catch (err) {
|
|
1050
|
+
if (!delivered && options.onStreamFailure) {
|
|
1051
|
+
await options.onStreamFailure(err);
|
|
1052
|
+
}
|
|
1053
|
+
emit(SSE_BRIDGE_EVENTS.error, { code: options.errorCode(err) });
|
|
1054
|
+
finish();
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
});
|
|
1058
|
+
return new Response(stream, {
|
|
1059
|
+
headers: {
|
|
1060
|
+
"content-type": "text/event-stream; charset=utf-8",
|
|
1061
|
+
"cache-control": "no-cache, no-transform",
|
|
1062
|
+
"x-accel-buffering": "no"
|
|
1063
|
+
}
|
|
1064
|
+
});
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
// src/providers/base/provider.types.ts
|
|
1068
|
+
function isTextProvider(provider) {
|
|
1069
|
+
return provider.capabilities.includes("text");
|
|
1070
|
+
}
|
|
1071
|
+
function isMediaProvider(provider) {
|
|
1072
|
+
return provider.capabilities.includes("image") || provider.capabilities.includes("video");
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
// src/content/domain/limits/ProviderTimingConfig.ts
|
|
1076
|
+
var ProviderTimingConfig = {
|
|
1077
|
+
DEFAULT_TIMEOUT_MS: 3e4,
|
|
1078
|
+
DEFAULT_RETRY_ATTEMPTS: 3,
|
|
1079
|
+
FAL_TIMEOUT_MS: 12e4,
|
|
1080
|
+
FAL_RETRY_ATTEMPTS: 2,
|
|
1081
|
+
GEMINI_TIMEOUT_MS: 6e4,
|
|
1082
|
+
GEMINI_RETRY_ATTEMPTS: 3,
|
|
1083
|
+
HEALTH_CHECK_TTL_MS: 5 * 60 * 1e3,
|
|
1084
|
+
RETRY_BACKOFF_BASE_MS: 1e3
|
|
1085
|
+
};
|
|
1086
|
+
|
|
1087
|
+
// src/content/domain/calculations/RetryBackoffDelay.ts
|
|
1088
|
+
function calculateRetryBackoffDelayMs(attemptIndex) {
|
|
1089
|
+
return ProviderTimingConfig.RETRY_BACKOFF_BASE_MS * Math.pow(2, attemptIndex);
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
// src/providers/base/base.provider.ts
|
|
1093
|
+
var BaseProvider = class {
|
|
1094
|
+
config;
|
|
1095
|
+
constructor(config) {
|
|
1096
|
+
this.config = config;
|
|
1097
|
+
}
|
|
1098
|
+
async healthCheck() {
|
|
1099
|
+
if (!this.config.apiKey) {
|
|
1100
|
+
return "unavailable";
|
|
1101
|
+
}
|
|
1102
|
+
return "healthy";
|
|
1103
|
+
}
|
|
1104
|
+
async estimateCost(_request) {
|
|
1105
|
+
return 0;
|
|
1106
|
+
}
|
|
1107
|
+
async fetchWithTimeout(url, options, timeout) {
|
|
1108
|
+
const effectiveTimeout = timeout ?? this.config.timeout ?? ProviderTimingConfig.DEFAULT_TIMEOUT_MS;
|
|
1109
|
+
const controller = new AbortController();
|
|
1110
|
+
const timeoutId = setTimeout(() => controller.abort(), effectiveTimeout);
|
|
1111
|
+
try {
|
|
1112
|
+
const response = await fetch(url, {
|
|
1113
|
+
...options,
|
|
1114
|
+
signal: controller.signal
|
|
1115
|
+
});
|
|
1116
|
+
clearTimeout(timeoutId);
|
|
1117
|
+
return response;
|
|
1118
|
+
} catch (error) {
|
|
1119
|
+
clearTimeout(timeoutId);
|
|
1120
|
+
throw error;
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
async withRetry(fn, maxAttempts) {
|
|
1124
|
+
const attempts = maxAttempts ?? this.config.retryAttempts ?? ProviderTimingConfig.DEFAULT_RETRY_ATTEMPTS;
|
|
1125
|
+
let lastError;
|
|
1126
|
+
for (let i = 0; i < attempts; i++) {
|
|
1127
|
+
try {
|
|
1128
|
+
return await fn();
|
|
1129
|
+
} catch (error) {
|
|
1130
|
+
lastError = error;
|
|
1131
|
+
if (i < attempts - 1) {
|
|
1132
|
+
await new Promise((resolve) => setTimeout(resolve, calculateRetryBackoffDelayMs(i)));
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
throw lastError || new Error("Retry attempts exhausted");
|
|
1137
|
+
}
|
|
1138
|
+
};
|
|
1139
|
+
var ProviderError = class extends AIError {
|
|
1140
|
+
constructor(message, providerId, originalError) {
|
|
1141
|
+
super(message, "PROVIDER_ERROR", originalError);
|
|
1142
|
+
this.providerId = providerId;
|
|
1143
|
+
this.name = "ProviderError";
|
|
1144
|
+
}
|
|
1145
|
+
providerId;
|
|
1146
|
+
};
|
|
1147
|
+
|
|
1148
|
+
// src/providers/factory/provider.factory.ts
|
|
1149
|
+
var ProviderFactory = class {
|
|
1150
|
+
providers = /* @__PURE__ */ new Map();
|
|
1151
|
+
healthCache = /* @__PURE__ */ new Map();
|
|
1152
|
+
priority;
|
|
1153
|
+
fallbackEnabled;
|
|
1154
|
+
defaultTier;
|
|
1155
|
+
constructor(config = {}) {
|
|
1156
|
+
this.priority = config.priority ?? [];
|
|
1157
|
+
this.fallbackEnabled = config.fallbackEnabled ?? true;
|
|
1158
|
+
this.defaultTier = config.defaultTier ?? "balanced";
|
|
1159
|
+
}
|
|
1160
|
+
register(provider) {
|
|
1161
|
+
this.providers.set(provider.id, provider);
|
|
1162
|
+
}
|
|
1163
|
+
getProvider(id) {
|
|
1164
|
+
return this.providers.get(id);
|
|
1165
|
+
}
|
|
1166
|
+
getAllProviders() {
|
|
1167
|
+
return Array.from(this.providers.values());
|
|
1168
|
+
}
|
|
1169
|
+
getByCapability(capability) {
|
|
1170
|
+
return this.getAllProviders().filter(
|
|
1171
|
+
(provider) => provider.capabilities.includes(capability)
|
|
1172
|
+
);
|
|
1173
|
+
}
|
|
1174
|
+
getTextProviders() {
|
|
1175
|
+
return this.getAllProviders().filter(isTextProvider);
|
|
1176
|
+
}
|
|
1177
|
+
hasTextProvider() {
|
|
1178
|
+
return this.getByCapability("text").length > 0;
|
|
1179
|
+
}
|
|
1180
|
+
/* ---------- neutral text surface ---------- */
|
|
1181
|
+
/**
|
|
1182
|
+
* Single-chat entry point: resolves a healthy text provider by priority,
|
|
1183
|
+
* injects the default tier when the request names neither model nor tier,
|
|
1184
|
+
* and walks the fallback chain on failure. Providers map tiers onto their
|
|
1185
|
+
* own models, so switching Groq→Anthropic is a config change.
|
|
1186
|
+
*/
|
|
1187
|
+
async chat(request) {
|
|
1188
|
+
const effective = request.model || request.tier ? request : { ...request, tier: this.defaultTier };
|
|
1189
|
+
return this.executeWithFallback(
|
|
1190
|
+
"text",
|
|
1191
|
+
(provider) => provider.generate(effective)
|
|
1192
|
+
);
|
|
1193
|
+
}
|
|
1194
|
+
async chatStream(request, callbacks) {
|
|
1195
|
+
const effective = request.model || request.tier ? request : { ...request, tier: this.defaultTier };
|
|
1196
|
+
let lastError;
|
|
1197
|
+
const attemptCallbacks = {
|
|
1198
|
+
onChunk: callbacks.onChunk,
|
|
1199
|
+
onComplete: callbacks.onComplete
|
|
1200
|
+
};
|
|
1201
|
+
const providers = this.getByCapability("text");
|
|
1202
|
+
if (providers.length === 0) {
|
|
1203
|
+
throw this.noProviderError("text");
|
|
1204
|
+
}
|
|
1205
|
+
const ordered = this.orderByPriority(providers);
|
|
1206
|
+
for (const provider of ordered) {
|
|
1207
|
+
if (!this.isProviderHealthy(provider.id)) continue;
|
|
1208
|
+
try {
|
|
1209
|
+
return await provider.stream(effective, attemptCallbacks);
|
|
1210
|
+
} catch (error) {
|
|
1211
|
+
lastError = error;
|
|
1212
|
+
this.markDegraded(provider.id);
|
|
1213
|
+
if (!this.fallbackEnabled) {
|
|
1214
|
+
callbacks.onError?.(error instanceof Error ? error : new Error(String(error)));
|
|
1215
|
+
throw error;
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
const failure = new AIError(
|
|
1220
|
+
"SERVER_ERROR" /* SERVER_ERROR */,
|
|
1221
|
+
`All providers failed for capability 'text'`,
|
|
1222
|
+
lastError instanceof Error ? lastError : void 0
|
|
1223
|
+
);
|
|
1224
|
+
callbacks.onError?.(lastError instanceof Error ? lastError : failure);
|
|
1225
|
+
throw failure;
|
|
1226
|
+
}
|
|
1227
|
+
/** AsyncGenerator adapter over chatStream — A's familiar consumption shape. */
|
|
1228
|
+
async *streamText(request) {
|
|
1229
|
+
const queue = [];
|
|
1230
|
+
let wake = null;
|
|
1231
|
+
let finished = false;
|
|
1232
|
+
let failure;
|
|
1233
|
+
const controller = new AbortController();
|
|
1234
|
+
const onAbort = () => controller.abort();
|
|
1235
|
+
request.signal?.addEventListener("abort", onAbort);
|
|
1236
|
+
const effectiveRequest = { ...request, signal: controller.signal };
|
|
1237
|
+
const notify = () => {
|
|
1238
|
+
const resolve = wake;
|
|
1239
|
+
wake = null;
|
|
1240
|
+
resolve?.();
|
|
1241
|
+
};
|
|
1242
|
+
const completion = this.chatStream(effectiveRequest, {
|
|
1243
|
+
onChunk: (chunk) => {
|
|
1244
|
+
queue.push(chunk);
|
|
1245
|
+
notify();
|
|
1246
|
+
}
|
|
1247
|
+
}).then(
|
|
1248
|
+
() => {
|
|
1249
|
+
finished = true;
|
|
1250
|
+
notify();
|
|
1251
|
+
},
|
|
1252
|
+
(error) => {
|
|
1253
|
+
failure = error;
|
|
1254
|
+
finished = true;
|
|
1255
|
+
notify();
|
|
1256
|
+
}
|
|
1257
|
+
);
|
|
1258
|
+
try {
|
|
1259
|
+
while (!finished || queue.length > 0) {
|
|
1260
|
+
if (queue.length === 0 && !finished) {
|
|
1261
|
+
await new Promise((resolve) => {
|
|
1262
|
+
wake = resolve;
|
|
1263
|
+
});
|
|
1264
|
+
}
|
|
1265
|
+
while (queue.length > 0) {
|
|
1266
|
+
yield queue.shift();
|
|
1267
|
+
}
|
|
1268
|
+
if (failure) throw failure;
|
|
1269
|
+
}
|
|
1270
|
+
await completion;
|
|
1271
|
+
} finally {
|
|
1272
|
+
controller.abort();
|
|
1273
|
+
request.signal?.removeEventListener("abort", onAbort);
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
async generateJson(request) {
|
|
1277
|
+
const effective = request.model || request.tier ? request : { ...request, tier: this.defaultTier };
|
|
1278
|
+
return this.executeWithFallback(
|
|
1279
|
+
"text",
|
|
1280
|
+
(provider) => provider.generateStructured(effective)
|
|
1281
|
+
);
|
|
1282
|
+
}
|
|
1283
|
+
/* ---------- content-layer string facade (ITextGenerator) ---------- */
|
|
1284
|
+
async generateText(prompt, options = {}) {
|
|
1285
|
+
const result = await this.chat({
|
|
1286
|
+
messages: [{ role: "user", content: prompt }],
|
|
1287
|
+
model: options.model,
|
|
1288
|
+
maxTokens: options.maxTokens,
|
|
1289
|
+
temperature: options.temperature,
|
|
1290
|
+
topP: options.topP
|
|
1291
|
+
});
|
|
1292
|
+
return result.text;
|
|
1293
|
+
}
|
|
1294
|
+
generateTextStream(prompt, options = {}) {
|
|
1295
|
+
return this.streamText({
|
|
1296
|
+
messages: [{ role: "user", content: prompt }],
|
|
1297
|
+
model: options.model,
|
|
1298
|
+
maxTokens: options.maxTokens,
|
|
1299
|
+
temperature: options.temperature,
|
|
1300
|
+
topP: options.topP
|
|
1301
|
+
});
|
|
1302
|
+
}
|
|
1303
|
+
/* ---------- media surface ---------- */
|
|
1304
|
+
async generateImage(request) {
|
|
1305
|
+
return this.executeWithFallback(
|
|
1306
|
+
"image",
|
|
1307
|
+
(provider) => provider.generateImage(request)
|
|
1308
|
+
);
|
|
1309
|
+
}
|
|
1310
|
+
async generateVideo(request) {
|
|
1311
|
+
return this.executeWithFallback(
|
|
1312
|
+
"video",
|
|
1313
|
+
(provider) => provider.generateVideo(request)
|
|
1314
|
+
);
|
|
1315
|
+
}
|
|
1316
|
+
async imageToVideo(request) {
|
|
1317
|
+
return this.executeWithFallback(
|
|
1318
|
+
"video",
|
|
1319
|
+
(provider) => provider.imageToVideo(request)
|
|
1320
|
+
);
|
|
1321
|
+
}
|
|
1322
|
+
async videoToVideo(request) {
|
|
1323
|
+
return this.executeWithFallback(
|
|
1324
|
+
"video",
|
|
1325
|
+
(provider) => provider.videoToVideo(request)
|
|
1326
|
+
);
|
|
1327
|
+
}
|
|
1328
|
+
/* ---------- health ---------- */
|
|
1329
|
+
async checkProviderHealth(providerId, forceRefresh = false) {
|
|
1330
|
+
const provider = this.providers.get(providerId);
|
|
1331
|
+
if (!provider) {
|
|
1332
|
+
return "unavailable";
|
|
1333
|
+
}
|
|
1334
|
+
const cached = this.healthCache.get(providerId);
|
|
1335
|
+
const now = Date.now();
|
|
1336
|
+
const ttl = ProviderTimingConfig.HEALTH_CHECK_TTL_MS;
|
|
1337
|
+
if (!forceRefresh && cached && now - cached.timestamp < ttl) {
|
|
1338
|
+
return cached.health;
|
|
1339
|
+
}
|
|
1340
|
+
try {
|
|
1341
|
+
const health = await provider.healthCheck();
|
|
1342
|
+
this.healthCache.set(providerId, { health, timestamp: now });
|
|
1343
|
+
return health;
|
|
1344
|
+
} catch {
|
|
1345
|
+
this.healthCache.set(providerId, { health: "unavailable", timestamp: now });
|
|
1346
|
+
return "unavailable";
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
isProviderHealthy(providerId) {
|
|
1350
|
+
const cached = this.healthCache.get(providerId);
|
|
1351
|
+
if (!cached) {
|
|
1352
|
+
return true;
|
|
1353
|
+
}
|
|
1354
|
+
return cached.health === "healthy" || cached.health === "degraded";
|
|
1355
|
+
}
|
|
1356
|
+
async refreshAllHealth() {
|
|
1357
|
+
await Promise.all(
|
|
1358
|
+
Array.from(this.providers.keys()).map((id) => this.checkProviderHealth(id))
|
|
1359
|
+
);
|
|
1360
|
+
}
|
|
1361
|
+
/* ---------- internals ---------- */
|
|
1362
|
+
async executeWithFallback(capability, fn) {
|
|
1363
|
+
const providers = this.getByCapability(capability);
|
|
1364
|
+
if (providers.length === 0) {
|
|
1365
|
+
throw this.noProviderError(capability);
|
|
1366
|
+
}
|
|
1367
|
+
let lastError;
|
|
1368
|
+
for (const provider of this.orderByPriority(providers)) {
|
|
1369
|
+
if (!this.isProviderHealthy(provider.id)) {
|
|
1370
|
+
continue;
|
|
1371
|
+
}
|
|
1372
|
+
try {
|
|
1373
|
+
return await fn(provider);
|
|
1374
|
+
} catch (error) {
|
|
1375
|
+
lastError = error;
|
|
1376
|
+
this.markDegraded(provider.id);
|
|
1377
|
+
if (!this.fallbackEnabled) {
|
|
1378
|
+
throw error;
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
throw new AIError(
|
|
1383
|
+
"SERVER_ERROR" /* SERVER_ERROR */,
|
|
1384
|
+
`All providers failed for capability '${capability}'`,
|
|
1385
|
+
lastError instanceof Error ? lastError : void 0
|
|
1386
|
+
);
|
|
1387
|
+
}
|
|
1388
|
+
orderByPriority(providers) {
|
|
1389
|
+
if (this.priority.length === 0) {
|
|
1390
|
+
return providers;
|
|
1391
|
+
}
|
|
1392
|
+
const ordered = [];
|
|
1393
|
+
for (const providerId of this.priority) {
|
|
1394
|
+
const provider = providers.find((p) => p.id === providerId);
|
|
1395
|
+
if (provider) ordered.push(provider);
|
|
1396
|
+
}
|
|
1397
|
+
for (const provider of providers) {
|
|
1398
|
+
if (!this.priority.includes(provider.id)) {
|
|
1399
|
+
ordered.push(provider);
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
return ordered;
|
|
1403
|
+
}
|
|
1404
|
+
markDegraded(providerId) {
|
|
1405
|
+
const cached = this.healthCache.get(providerId);
|
|
1406
|
+
if (cached) {
|
|
1407
|
+
this.healthCache.set(providerId, { health: "degraded", timestamp: cached.timestamp });
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
noProviderError(capability) {
|
|
1411
|
+
return new AIError(
|
|
1412
|
+
"MISSING_CONFIG" /* MISSING_CONFIG */,
|
|
1413
|
+
`No provider registered for capability '${capability}' \u2014 create one with createAI() and include its config block`
|
|
1414
|
+
);
|
|
1415
|
+
}
|
|
1416
|
+
};
|
|
1417
|
+
|
|
1418
|
+
// src/providers/anthropic/anthropic.constants.ts
|
|
1419
|
+
var ANTHROPIC_MODELS = {
|
|
1420
|
+
OPUS_5_5: "claude-opus-5-5",
|
|
1421
|
+
SONNET_5_5: "claude-sonnet-5-5",
|
|
1422
|
+
HAIKU_5_5: "claude-haiku-5-5"
|
|
1423
|
+
};
|
|
1424
|
+
var ANTHROPIC_TIERS = {
|
|
1425
|
+
fast: ANTHROPIC_MODELS.HAIKU_5_5,
|
|
1426
|
+
balanced: ANTHROPIC_MODELS.SONNET_5_5,
|
|
1427
|
+
powerful: ANTHROPIC_MODELS.OPUS_5_5
|
|
1428
|
+
};
|
|
1429
|
+
var ANTHROPIC_API_VERSION = "2026-10-21";
|
|
1430
|
+
var ANTHROPIC_DEFAULT_MAX_TOKENS = 16e3;
|
|
1431
|
+
var ANTHROPIC_BASE_URL = "https://api.anthropic.com";
|
|
1432
|
+
var ANTHROPIC_MESSAGES_PATH = "/v1/messages";
|
|
1433
|
+
var ANTHROPIC_MODELS_PATH = "/v1/models";
|
|
1434
|
+
var ANTHROPIC_TIMEOUTS = {
|
|
1435
|
+
DEFAULT: 6e4,
|
|
1436
|
+
STREAMING: 12e4
|
|
1437
|
+
};
|
|
1438
|
+
var ANTHROPIC_HEADERS = {
|
|
1439
|
+
CONTENT_TYPE: "Content-Type",
|
|
1440
|
+
APPLICATION_JSON: "application/json",
|
|
1441
|
+
API_KEY: "x-api-key",
|
|
1442
|
+
VERSION: "anthropic-version"
|
|
1443
|
+
};
|
|
1444
|
+
var ANTHROPIC_DEFAULT_EFFORT = {
|
|
1445
|
+
[ANTHROPIC_MODELS.OPUS_5_5]: "medium",
|
|
1446
|
+
[ANTHROPIC_MODELS.SONNET_5_5]: "high",
|
|
1447
|
+
[ANTHROPIC_MODELS.HAIKU_5_5]: "medium"
|
|
1448
|
+
};
|
|
1449
|
+
|
|
1450
|
+
// src/providers/anthropic/anthropic-http.transport.ts
|
|
1451
|
+
var AnthropicHttpTransport = class {
|
|
1452
|
+
constructor(config) {
|
|
1453
|
+
this.config = config;
|
|
1454
|
+
}
|
|
1455
|
+
config;
|
|
1456
|
+
async postMessages(request, options = {}) {
|
|
1457
|
+
const url = `${this.baseUrl()}${ANTHROPIC_MESSAGES_PATH}`;
|
|
1458
|
+
const timeout = this.config.timeoutMs ?? ANTHROPIC_TIMEOUTS.DEFAULT;
|
|
1459
|
+
const controller = new AbortController();
|
|
1460
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
1461
|
+
const onExternalAbort = () => controller.abort();
|
|
1462
|
+
options.signal?.addEventListener("abort", onExternalAbort);
|
|
1463
|
+
try {
|
|
1464
|
+
const response = await fetch(url, {
|
|
1465
|
+
method: "POST",
|
|
1466
|
+
headers: this.buildHeaders(),
|
|
1467
|
+
body: JSON.stringify({ ...request, stream: false }),
|
|
1468
|
+
signal: controller.signal
|
|
1469
|
+
});
|
|
1470
|
+
if (!response.ok) await this.throwFromErrorResponse(response);
|
|
1471
|
+
return await response.json();
|
|
1472
|
+
} catch (error) {
|
|
1473
|
+
throw this.normalizeError(error);
|
|
1474
|
+
} finally {
|
|
1475
|
+
clearTimeout(timeoutId);
|
|
1476
|
+
options.signal?.removeEventListener("abort", onExternalAbort);
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
async streamMessages(request, options = {}) {
|
|
1480
|
+
const url = `${this.baseUrl()}${ANTHROPIC_MESSAGES_PATH}`;
|
|
1481
|
+
const timeout = this.config.timeoutMs ?? ANTHROPIC_TIMEOUTS.STREAMING;
|
|
1482
|
+
const controller = new AbortController();
|
|
1483
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
1484
|
+
const onExternalAbort = () => controller.abort();
|
|
1485
|
+
options.signal?.addEventListener("abort", onExternalAbort);
|
|
1486
|
+
try {
|
|
1487
|
+
const response = await fetch(url, {
|
|
1488
|
+
method: "POST",
|
|
1489
|
+
headers: this.buildHeaders(),
|
|
1490
|
+
body: JSON.stringify({ ...request, stream: true }),
|
|
1491
|
+
signal: controller.signal
|
|
1492
|
+
});
|
|
1493
|
+
if (!response.ok) await this.throwFromErrorResponse(response);
|
|
1494
|
+
if (!response.body) {
|
|
1495
|
+
throw new AIError("NETWORK_ERROR" /* NETWORK_ERROR */, "Response body is null");
|
|
1496
|
+
}
|
|
1497
|
+
return response.body;
|
|
1498
|
+
} catch (error) {
|
|
1499
|
+
options.signal?.removeEventListener("abort", onExternalAbort);
|
|
1500
|
+
throw this.normalizeError(error);
|
|
1501
|
+
} finally {
|
|
1502
|
+
clearTimeout(timeoutId);
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
async listModels() {
|
|
1506
|
+
const url = `${this.baseUrl()}${ANTHROPIC_MODELS_PATH}`;
|
|
1507
|
+
return fetch(url, {
|
|
1508
|
+
method: "GET",
|
|
1509
|
+
headers: this.buildHeaders()
|
|
1510
|
+
});
|
|
1511
|
+
}
|
|
1512
|
+
baseUrl() {
|
|
1513
|
+
return this.config.baseUrl ?? ANTHROPIC_BASE_URL;
|
|
1514
|
+
}
|
|
1515
|
+
buildHeaders() {
|
|
1516
|
+
return {
|
|
1517
|
+
[ANTHROPIC_HEADERS.CONTENT_TYPE]: ANTHROPIC_HEADERS.APPLICATION_JSON,
|
|
1518
|
+
[ANTHROPIC_HEADERS.API_KEY]: this.config.apiKey,
|
|
1519
|
+
[ANTHROPIC_HEADERS.VERSION]: ANTHROPIC_API_VERSION
|
|
1520
|
+
};
|
|
1521
|
+
}
|
|
1522
|
+
async throwFromErrorResponse(response) {
|
|
1523
|
+
const errorMessage = await extractErrorMessageFromResponse(response);
|
|
1524
|
+
const metadata = {
|
|
1525
|
+
status: response.status,
|
|
1526
|
+
url: response.url
|
|
1527
|
+
};
|
|
1528
|
+
throw new AIError(
|
|
1529
|
+
classifyHttpStatus(response.status),
|
|
1530
|
+
errorMessage,
|
|
1531
|
+
void 0,
|
|
1532
|
+
metadata
|
|
1533
|
+
);
|
|
1534
|
+
}
|
|
1535
|
+
normalizeError(error) {
|
|
1536
|
+
if (error instanceof AIError) return error;
|
|
1537
|
+
if (error instanceof Error) {
|
|
1538
|
+
if (error.name === "AbortError") {
|
|
1539
|
+
return new AIError(
|
|
1540
|
+
"ABORT_ERROR" /* ABORT_ERROR */,
|
|
1541
|
+
"Request was aborted by the client",
|
|
1542
|
+
error
|
|
1543
|
+
);
|
|
1544
|
+
}
|
|
1545
|
+
if (error.name === "TypeError" && error.message.toLowerCase().includes("network")) {
|
|
1546
|
+
return new AIError(
|
|
1547
|
+
"NETWORK_ERROR" /* NETWORK_ERROR */,
|
|
1548
|
+
"Network error: Unable to connect to the AI service",
|
|
1549
|
+
error
|
|
1550
|
+
);
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
return new AIError(
|
|
1554
|
+
"UNKNOWN_ERROR" /* UNKNOWN_ERROR */,
|
|
1555
|
+
error instanceof Error ? error.message : "Unknown error occurred",
|
|
1556
|
+
error instanceof Error ? error : void 0
|
|
1557
|
+
);
|
|
1558
|
+
}
|
|
1559
|
+
};
|
|
1560
|
+
|
|
1561
|
+
// src/providers/anthropic/anthropic-request.builder.ts
|
|
1562
|
+
var AnthropicRequestBuilder = class _AnthropicRequestBuilder {
|
|
1563
|
+
model;
|
|
1564
|
+
messages = [];
|
|
1565
|
+
systemPrompt;
|
|
1566
|
+
maxTokens = ANTHROPIC_DEFAULT_MAX_TOKENS;
|
|
1567
|
+
effort;
|
|
1568
|
+
stopSequences;
|
|
1569
|
+
streamEnabled = false;
|
|
1570
|
+
constructor(model) {
|
|
1571
|
+
this.model = model;
|
|
1572
|
+
}
|
|
1573
|
+
static for(model) {
|
|
1574
|
+
return new _AnthropicRequestBuilder(model);
|
|
1575
|
+
}
|
|
1576
|
+
withRequest(request) {
|
|
1577
|
+
this.messages = toAnthropicMessages(request.messages);
|
|
1578
|
+
this.systemPrompt = request.systemPrompt;
|
|
1579
|
+
if (request.maxTokens !== void 0) this.maxTokens = request.maxTokens;
|
|
1580
|
+
if (request.stopSequences !== void 0 && request.stopSequences.length > 0) {
|
|
1581
|
+
this.stopSequences = [...request.stopSequences];
|
|
1582
|
+
}
|
|
1583
|
+
return this;
|
|
1584
|
+
}
|
|
1585
|
+
withEffort(effort) {
|
|
1586
|
+
this.effort = effort;
|
|
1587
|
+
return this;
|
|
1588
|
+
}
|
|
1589
|
+
withStream(stream) {
|
|
1590
|
+
this.streamEnabled = stream;
|
|
1591
|
+
return this;
|
|
1592
|
+
}
|
|
1593
|
+
withSystemPromptOverride(systemPrompt) {
|
|
1594
|
+
this.systemPrompt = systemPrompt;
|
|
1595
|
+
return this;
|
|
1596
|
+
}
|
|
1597
|
+
withMessagesOverride(messages) {
|
|
1598
|
+
this.messages = messages;
|
|
1599
|
+
return this;
|
|
1600
|
+
}
|
|
1601
|
+
build() {
|
|
1602
|
+
const request = {
|
|
1603
|
+
model: this.model,
|
|
1604
|
+
messages: this.messages,
|
|
1605
|
+
max_tokens: this.maxTokens,
|
|
1606
|
+
output_config: {
|
|
1607
|
+
effort: this.effort ?? defaultEffortForModel(this.model)
|
|
1608
|
+
}
|
|
1609
|
+
};
|
|
1610
|
+
if (this.systemPrompt !== void 0 && this.systemPrompt.length > 0) {
|
|
1611
|
+
request.system = this.systemPrompt;
|
|
1612
|
+
}
|
|
1613
|
+
if (this.stopSequences !== void 0) {
|
|
1614
|
+
request.stop_sequences = this.stopSequences;
|
|
1615
|
+
}
|
|
1616
|
+
if (this.streamEnabled) {
|
|
1617
|
+
request.stream = true;
|
|
1618
|
+
}
|
|
1619
|
+
return request;
|
|
1620
|
+
}
|
|
1621
|
+
};
|
|
1622
|
+
function toAnthropicMessages(messages) {
|
|
1623
|
+
return messages.filter((message) => message.role !== "system" && message.content.length > 0).map((message) => ({
|
|
1624
|
+
role: message.role,
|
|
1625
|
+
content: message.content
|
|
1626
|
+
}));
|
|
1627
|
+
}
|
|
1628
|
+
function defaultEffortForModel(model) {
|
|
1629
|
+
return ANTHROPIC_DEFAULT_EFFORT[model] ?? "medium";
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
// src/providers/anthropic/anthropic-stream.parser.ts
|
|
1633
|
+
function mapAnthropicStopReason(reason) {
|
|
1634
|
+
if (reason === "end_turn") return "stop";
|
|
1635
|
+
if (reason === "stop_sequence") return "stop";
|
|
1636
|
+
if (reason === "max_tokens") return "length";
|
|
1637
|
+
if (reason === "refusal") return "refusal";
|
|
1638
|
+
return void 0;
|
|
1639
|
+
}
|
|
1640
|
+
async function consumeAnthropicStream(stream, callbacks) {
|
|
1641
|
+
const reader = stream.getReader();
|
|
1642
|
+
const decoder = new TextDecoder();
|
|
1643
|
+
const chunks = [];
|
|
1644
|
+
let buffer = "";
|
|
1645
|
+
let model = null;
|
|
1646
|
+
let inputTokens;
|
|
1647
|
+
let outputTokens;
|
|
1648
|
+
let finishReason;
|
|
1649
|
+
const consumeLine = (rawLine) => {
|
|
1650
|
+
const line = rawLine.endsWith("\r") ? rawLine.slice(0, -1) : rawLine;
|
|
1651
|
+
const trimmed = line.trim();
|
|
1652
|
+
if (trimmed.length === 0 || !isSseDataLine(trimmed)) return;
|
|
1653
|
+
const payload = extractSseDataPayload(trimmed);
|
|
1654
|
+
let event;
|
|
1655
|
+
try {
|
|
1656
|
+
event = JSON.parse(payload);
|
|
1657
|
+
} catch {
|
|
1658
|
+
return;
|
|
1659
|
+
}
|
|
1660
|
+
switch (event.type) {
|
|
1661
|
+
case "message_start":
|
|
1662
|
+
model = event.message.model;
|
|
1663
|
+
if (event.message.usage?.input_tokens !== void 0) {
|
|
1664
|
+
inputTokens = event.message.usage.input_tokens;
|
|
1665
|
+
}
|
|
1666
|
+
break;
|
|
1667
|
+
case "content_block_delta":
|
|
1668
|
+
if (typeof event.delta.text === "string" && event.delta.text.length > 0) {
|
|
1669
|
+
chunks.push(event.delta.text);
|
|
1670
|
+
callbacks.onChunk?.(event.delta.text);
|
|
1671
|
+
}
|
|
1672
|
+
break;
|
|
1673
|
+
case "message_delta":
|
|
1674
|
+
if (event.delta?.stop_reason !== void 0 && event.delta.stop_reason !== null) {
|
|
1675
|
+
finishReason = mapAnthropicStopReason(event.delta.stop_reason);
|
|
1676
|
+
}
|
|
1677
|
+
if (event.usage?.output_tokens !== void 0) {
|
|
1678
|
+
outputTokens = event.usage.output_tokens;
|
|
1679
|
+
}
|
|
1680
|
+
break;
|
|
1681
|
+
case "message_stop":
|
|
1682
|
+
break;
|
|
1683
|
+
case "error": {
|
|
1684
|
+
const errType = event.error.type;
|
|
1685
|
+
const mappedType = errType === "rate_limit_error" ? "RATE_LIMIT_ERROR" /* RATE_LIMIT_ERROR */ : errType === "authentication_error" ? "INVALID_API_KEY" /* INVALID_API_KEY */ : errType === "invalid_request_error" ? "CLIENT_ERROR" /* CLIENT_ERROR */ : "SERVER_ERROR" /* SERVER_ERROR */;
|
|
1686
|
+
throw new AIError(
|
|
1687
|
+
mappedType,
|
|
1688
|
+
event.error.message || "Anthropic stream failed mid-flight"
|
|
1689
|
+
);
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
};
|
|
1693
|
+
try {
|
|
1694
|
+
while (true) {
|
|
1695
|
+
const { done, value } = await reader.read();
|
|
1696
|
+
if (done) break;
|
|
1697
|
+
buffer += decoder.decode(value, { stream: true });
|
|
1698
|
+
const lines = buffer.split("\n");
|
|
1699
|
+
buffer = lines.pop() ?? "";
|
|
1700
|
+
for (const line of lines) consumeLine(line);
|
|
1701
|
+
}
|
|
1702
|
+
buffer += decoder.decode();
|
|
1703
|
+
if (buffer.length > 0) consumeLine(buffer);
|
|
1704
|
+
} catch (error) {
|
|
1705
|
+
if (error instanceof AIError) throw error;
|
|
1706
|
+
throw new AIError(
|
|
1707
|
+
"NETWORK_ERROR" /* NETWORK_ERROR */,
|
|
1708
|
+
"Stream was interrupted",
|
|
1709
|
+
error instanceof Error ? error : void 0
|
|
1710
|
+
);
|
|
1711
|
+
} finally {
|
|
1712
|
+
reader.releaseLock();
|
|
1713
|
+
}
|
|
1714
|
+
return {
|
|
1715
|
+
text: chunks.join(""),
|
|
1716
|
+
model,
|
|
1717
|
+
inputTokens,
|
|
1718
|
+
outputTokens,
|
|
1719
|
+
finishReason
|
|
1720
|
+
};
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
// src/providers/anthropic/anthropic.provider.ts
|
|
1724
|
+
var REFUSAL_FALLBACK_TEXT = "I'm not able to help with that request. Could you rephrase or ask something else?";
|
|
1725
|
+
var AnthropicProvider = class {
|
|
1726
|
+
constructor(config) {
|
|
1727
|
+
this.config = config;
|
|
1728
|
+
this.transport = new AnthropicHttpTransport(config);
|
|
1729
|
+
this.tiers = mergeTierOverrides(ANTHROPIC_TIERS, config.models);
|
|
1730
|
+
}
|
|
1731
|
+
config;
|
|
1732
|
+
id = "anthropic";
|
|
1733
|
+
name = "Anthropic";
|
|
1734
|
+
capabilities = ["text"];
|
|
1735
|
+
transport;
|
|
1736
|
+
tiers;
|
|
1737
|
+
jsonParser = new JsonResponseParser();
|
|
1738
|
+
resolveModel(tier) {
|
|
1739
|
+
return resolveModel(this.tiers, tier, this.id);
|
|
1740
|
+
}
|
|
1741
|
+
async healthCheck() {
|
|
1742
|
+
if (!this.config.apiKey) return "unavailable";
|
|
1743
|
+
try {
|
|
1744
|
+
const response = await this.transport.listModels();
|
|
1745
|
+
if (response.ok) return "healthy";
|
|
1746
|
+
if (response.status === 401 || response.status === 403) return "unavailable";
|
|
1747
|
+
return "degraded";
|
|
1748
|
+
} catch {
|
|
1749
|
+
return "unavailable";
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
async estimateCost() {
|
|
1753
|
+
return 0;
|
|
1754
|
+
}
|
|
1755
|
+
async generate(request) {
|
|
1756
|
+
const model = request.model ?? this.resolveModel(request.tier ?? "balanced");
|
|
1757
|
+
const wireRequest = new AnthropicRequestBuilder(model).withRequest(request).withEffort(this.config.effort).build();
|
|
1758
|
+
const response = await this.transport.postMessages(wireRequest, {
|
|
1759
|
+
signal: request.signal
|
|
1760
|
+
});
|
|
1761
|
+
return this.toGenerationResult(response, model);
|
|
1762
|
+
}
|
|
1763
|
+
async generateStructured(request) {
|
|
1764
|
+
const model = request.model ?? this.resolveModel(request.tier ?? "balanced");
|
|
1765
|
+
const messages = [...request.messages];
|
|
1766
|
+
const lastUserIndex = findLastIndex(messages, (m) => m.role === "user");
|
|
1767
|
+
if (lastUserIndex === -1) {
|
|
1768
|
+
throw new AIError(
|
|
1769
|
+
"UNKNOWN_ERROR" /* UNKNOWN_ERROR */,
|
|
1770
|
+
"generateStructured requires at least one user message"
|
|
1771
|
+
);
|
|
1772
|
+
}
|
|
1773
|
+
const structuredPrompt = new StructuredPromptBuilder(
|
|
1774
|
+
messages[lastUserIndex].content
|
|
1775
|
+
).withSchema(request.schema).build();
|
|
1776
|
+
messages[lastUserIndex] = { role: "user", content: structuredPrompt };
|
|
1777
|
+
const wireRequest = new AnthropicRequestBuilder(model).withRequest({ ...request, messages }).withEffort(this.config.effort).build();
|
|
1778
|
+
const response = await this.transport.postMessages(wireRequest, {
|
|
1779
|
+
signal: request.signal
|
|
1780
|
+
});
|
|
1781
|
+
const result = this.toGenerationResult(response, model);
|
|
1782
|
+
return { value: this.jsonParser.parse(result.text), result };
|
|
1783
|
+
}
|
|
1784
|
+
async stream(request, callbacks) {
|
|
1785
|
+
const model = request.model ?? this.resolveModel(request.tier ?? "balanced");
|
|
1786
|
+
const wireRequest = new AnthropicRequestBuilder(model).withRequest(request).withEffort(this.config.effort).withStream(true).build();
|
|
1787
|
+
const streamBody = await this.transport.streamMessages(wireRequest, {
|
|
1788
|
+
signal: request.signal
|
|
1789
|
+
});
|
|
1790
|
+
try {
|
|
1791
|
+
const outcome = await consumeAnthropicStream(streamBody, callbacks);
|
|
1792
|
+
const text = outcome.text.length > 0 ? outcome.text : refusalTextFor(outcome.finishReason);
|
|
1793
|
+
const result = {
|
|
1794
|
+
text,
|
|
1795
|
+
usage: {
|
|
1796
|
+
inputTokens: outcome.inputTokens ?? 0,
|
|
1797
|
+
outputTokens: outcome.outputTokens ?? 0
|
|
1798
|
+
},
|
|
1799
|
+
model: outcome.model ?? model,
|
|
1800
|
+
providerId: this.id,
|
|
1801
|
+
finishReason: outcome.finishReason
|
|
1802
|
+
};
|
|
1803
|
+
callbacks.onComplete?.(result);
|
|
1804
|
+
return result;
|
|
1805
|
+
} catch (error) {
|
|
1806
|
+
callbacks.onError?.(error instanceof Error ? error : new Error(String(error)));
|
|
1807
|
+
throw error;
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
toGenerationResult(response, fallbackModel) {
|
|
1811
|
+
const text = extractText(response.content);
|
|
1812
|
+
const finishReason = mapAnthropicStopReason(response.stop_reason);
|
|
1813
|
+
if (text.length === 0) {
|
|
1814
|
+
if (finishReason === "refusal") {
|
|
1815
|
+
return {
|
|
1816
|
+
text: REFUSAL_FALLBACK_TEXT,
|
|
1817
|
+
usage: mapAnthropicUsage(response.usage),
|
|
1818
|
+
model: response.model || fallbackModel,
|
|
1819
|
+
providerId: this.id,
|
|
1820
|
+
finishReason
|
|
1821
|
+
};
|
|
1822
|
+
}
|
|
1823
|
+
throw new AIError(
|
|
1824
|
+
"SERVER_ERROR" /* SERVER_ERROR */,
|
|
1825
|
+
"No content generated from Anthropic API",
|
|
1826
|
+
void 0,
|
|
1827
|
+
{ model: response.model || fallbackModel, stopReason: response.stop_reason ?? void 0 }
|
|
1828
|
+
);
|
|
1829
|
+
}
|
|
1830
|
+
return {
|
|
1831
|
+
text,
|
|
1832
|
+
usage: mapAnthropicUsage(response.usage),
|
|
1833
|
+
model: response.model || fallbackModel,
|
|
1834
|
+
providerId: this.id,
|
|
1835
|
+
finishReason
|
|
1836
|
+
};
|
|
1837
|
+
}
|
|
1838
|
+
};
|
|
1839
|
+
function extractText(blocks) {
|
|
1840
|
+
return blocks.filter((block) => block.type === "text").map((block) => block.text).join("");
|
|
1841
|
+
}
|
|
1842
|
+
function refusalTextFor(finishReason) {
|
|
1843
|
+
return finishReason === "refusal" ? REFUSAL_FALLBACK_TEXT : "";
|
|
1844
|
+
}
|
|
1845
|
+
function mapAnthropicUsage(usage) {
|
|
1846
|
+
if (!usage) return void 0;
|
|
1847
|
+
return {
|
|
1848
|
+
inputTokens: usage.input_tokens,
|
|
1849
|
+
outputTokens: usage.output_tokens
|
|
1850
|
+
};
|
|
1851
|
+
}
|
|
1852
|
+
function findLastIndex(items, predicate) {
|
|
1853
|
+
for (let i = items.length - 1; i >= 0; i--) {
|
|
1854
|
+
if (predicate(items[i])) return i;
|
|
1855
|
+
}
|
|
1856
|
+
return -1;
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1859
|
+
// src/providers/groq/groq.constants.ts
|
|
1860
|
+
var GROQ_MODELS = {
|
|
1861
|
+
LLAMA_3_1_8B_INSTANT: "llama-3.1-8b-instant",
|
|
1862
|
+
LLAMA_3_3_70B_VERSATILE: "llama-3.3-70b-versatile",
|
|
1863
|
+
LLAMA_3_1_70B_VERSATILE: "llama-3.1-70b-versatile",
|
|
1864
|
+
GPT_OSS_20B: "openai/gpt-oss-20b",
|
|
1865
|
+
GPT_OSS_120B: "openai/gpt-oss-120b",
|
|
1866
|
+
MIXTRAL_8X7B: "mixtral-8x7b-32768",
|
|
1867
|
+
GEMMA_2_9B: "gemma2-9b-it",
|
|
1868
|
+
LLAMA_4_SCOUT_17B: "meta-llama/llama-4-scout-17b-16e-instruct",
|
|
1869
|
+
KIMI_K2_INSTRUCT: "moonshotai/kimi-k2-instruct",
|
|
1870
|
+
QWEN3_32B: "qwen/qwen3-32b"
|
|
1871
|
+
};
|
|
1872
|
+
var GROQ_TIERS = {
|
|
1873
|
+
fast: GROQ_MODELS.LLAMA_3_1_8B_INSTANT,
|
|
1874
|
+
balanced: GROQ_MODELS.LLAMA_3_3_70B_VERSATILE,
|
|
1875
|
+
powerful: GROQ_MODELS.GPT_OSS_120B
|
|
1876
|
+
};
|
|
1877
|
+
var DEFAULT_GENERATION_CONFIG = {
|
|
1878
|
+
temperature: 0.7,
|
|
1879
|
+
maxTokens: 1024,
|
|
1880
|
+
topP: 1
|
|
1881
|
+
};
|
|
1882
|
+
var GROQ_BASE_URL = "https://api.groq.com/openai/v1";
|
|
1883
|
+
var CHAT_COMPLETIONS_PATH = "/chat/completions";
|
|
1884
|
+
var GROQ_TIMEOUTS = {
|
|
1885
|
+
DEFAULT: 3e4,
|
|
1886
|
+
STREAMING: 6e4
|
|
1887
|
+
};
|
|
1888
|
+
var HTTP_HEADERS = {
|
|
1889
|
+
CONTENT_TYPE: "Content-Type",
|
|
1890
|
+
APPLICATION_JSON: "application/json",
|
|
1891
|
+
AUTHORIZATION: "Authorization",
|
|
1892
|
+
BEARER_PREFIX: "Bearer "
|
|
1893
|
+
};
|
|
1894
|
+
|
|
1895
|
+
// src/providers/groq/groq-request.builder.ts
|
|
1896
|
+
var GroqChatRequestBuilder = class _GroqChatRequestBuilder {
|
|
1897
|
+
model;
|
|
1898
|
+
messages = [];
|
|
1899
|
+
generationConfig = {};
|
|
1900
|
+
streamEnabled = false;
|
|
1901
|
+
jsonResponse = false;
|
|
1902
|
+
constructor(model) {
|
|
1903
|
+
this.model = model;
|
|
1904
|
+
}
|
|
1905
|
+
static for(model) {
|
|
1906
|
+
return new _GroqChatRequestBuilder(model);
|
|
1907
|
+
}
|
|
1908
|
+
withMessages(messages) {
|
|
1909
|
+
this.messages = [...messages];
|
|
1910
|
+
return this;
|
|
1911
|
+
}
|
|
1912
|
+
withConfig(config) {
|
|
1913
|
+
this.generationConfig = config ? { ...config } : {};
|
|
1914
|
+
return this;
|
|
1915
|
+
}
|
|
1916
|
+
withStream(stream) {
|
|
1917
|
+
this.streamEnabled = stream;
|
|
1918
|
+
return this;
|
|
1919
|
+
}
|
|
1920
|
+
withJsonResponse(enabled) {
|
|
1921
|
+
this.jsonResponse = enabled;
|
|
1922
|
+
return this;
|
|
1923
|
+
}
|
|
1924
|
+
build() {
|
|
1925
|
+
const cfg = this.generationConfig;
|
|
1926
|
+
const request = {
|
|
1927
|
+
model: this.model,
|
|
1928
|
+
messages: this.messages,
|
|
1929
|
+
temperature: cfg.temperature ?? DEFAULT_GENERATION_CONFIG.temperature,
|
|
1930
|
+
max_tokens: cfg.maxTokens ?? DEFAULT_GENERATION_CONFIG.maxTokens,
|
|
1931
|
+
top_p: cfg.topP ?? DEFAULT_GENERATION_CONFIG.topP
|
|
1932
|
+
};
|
|
1933
|
+
if (cfg.n !== void 0) request.n = cfg.n;
|
|
1934
|
+
if (cfg.stop !== void 0) request.stop = cfg.stop;
|
|
1935
|
+
if (cfg.frequencyPenalty !== void 0) request.frequency_penalty = cfg.frequencyPenalty;
|
|
1936
|
+
if (cfg.presencePenalty !== void 0) request.presence_penalty = cfg.presencePenalty;
|
|
1937
|
+
if (cfg.reasoningEffort !== void 0) request.reasoning_effort = cfg.reasoningEffort;
|
|
1938
|
+
if (this.jsonResponse) {
|
|
1939
|
+
request.response_format = { type: "json_object" };
|
|
1940
|
+
}
|
|
1941
|
+
if (this.streamEnabled) request.stream = true;
|
|
1942
|
+
return request;
|
|
1943
|
+
}
|
|
1944
|
+
};
|
|
1945
|
+
|
|
1946
|
+
// src/providers/groq/groq-http-transport.ts
|
|
1947
|
+
var GroqHttpTransport = class {
|
|
1948
|
+
constructor(config) {
|
|
1949
|
+
this.config = config;
|
|
1950
|
+
}
|
|
1951
|
+
config;
|
|
1952
|
+
pendingAbortControllers = /* @__PURE__ */ new Set();
|
|
1953
|
+
cancelPendingRequests() {
|
|
1954
|
+
for (const controller of this.pendingAbortControllers) {
|
|
1955
|
+
controller.abort();
|
|
1956
|
+
}
|
|
1957
|
+
this.pendingAbortControllers.clear();
|
|
1958
|
+
}
|
|
1959
|
+
async postChatCompletion(request) {
|
|
1960
|
+
return this.request(CHAT_COMPLETIONS_PATH, {
|
|
1961
|
+
...request,
|
|
1962
|
+
stream: false
|
|
1963
|
+
});
|
|
1964
|
+
}
|
|
1965
|
+
async streamChatCompletion(request, options = {}) {
|
|
1966
|
+
const url = `${this.baseUrl()}${CHAT_COMPLETIONS_PATH}`;
|
|
1967
|
+
const timeout = this.config.timeoutMs ?? GROQ_TIMEOUTS.STREAMING;
|
|
1968
|
+
const controller = new AbortController();
|
|
1969
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
1970
|
+
const onExternalAbort = () => controller.abort();
|
|
1971
|
+
options.signal?.addEventListener("abort", onExternalAbort);
|
|
1972
|
+
this.pendingAbortControllers.add(controller);
|
|
1973
|
+
try {
|
|
1974
|
+
const response = await fetch(url, {
|
|
1975
|
+
method: "POST",
|
|
1976
|
+
headers: this.buildHeaders(),
|
|
1977
|
+
body: JSON.stringify({ ...request, stream: true }),
|
|
1978
|
+
signal: controller.signal
|
|
1979
|
+
});
|
|
1980
|
+
if (!response.ok) await this.throwFromErrorResponse(response);
|
|
1981
|
+
if (!response.body) {
|
|
1982
|
+
throw new AIError("NETWORK_ERROR" /* NETWORK_ERROR */, "Response body is null");
|
|
1983
|
+
}
|
|
1984
|
+
return response.body;
|
|
1985
|
+
} catch (error) {
|
|
1986
|
+
options.signal?.removeEventListener("abort", onExternalAbort);
|
|
1987
|
+
throw this.normalizeError(error);
|
|
1988
|
+
} finally {
|
|
1989
|
+
clearTimeout(timeoutId);
|
|
1990
|
+
this.pendingAbortControllers.delete(controller);
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
baseUrl() {
|
|
1994
|
+
return this.config.baseUrl ?? GROQ_BASE_URL;
|
|
1995
|
+
}
|
|
1996
|
+
async request(endpoint, body) {
|
|
1997
|
+
const url = `${this.baseUrl()}${endpoint}`;
|
|
1998
|
+
const timeout = this.config.timeoutMs ?? GROQ_TIMEOUTS.DEFAULT;
|
|
1999
|
+
const controller = new AbortController();
|
|
2000
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
2001
|
+
this.pendingAbortControllers.add(controller);
|
|
2002
|
+
try {
|
|
2003
|
+
const response = await fetch(url, {
|
|
2004
|
+
method: "POST",
|
|
2005
|
+
headers: this.buildHeaders(),
|
|
2006
|
+
body: JSON.stringify(body),
|
|
2007
|
+
signal: controller.signal
|
|
2008
|
+
});
|
|
2009
|
+
if (!response.ok) await this.throwFromErrorResponse(response);
|
|
2010
|
+
return await response.json();
|
|
2011
|
+
} catch (error) {
|
|
2012
|
+
throw this.normalizeError(error);
|
|
2013
|
+
} finally {
|
|
2014
|
+
clearTimeout(timeoutId);
|
|
2015
|
+
this.pendingAbortControllers.delete(controller);
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
buildHeaders() {
|
|
2019
|
+
return {
|
|
2020
|
+
[HTTP_HEADERS.CONTENT_TYPE]: HTTP_HEADERS.APPLICATION_JSON,
|
|
2021
|
+
[HTTP_HEADERS.AUTHORIZATION]: `${HTTP_HEADERS.BEARER_PREFIX}${this.config.apiKey}`
|
|
2022
|
+
};
|
|
2023
|
+
}
|
|
2024
|
+
async throwFromErrorResponse(response) {
|
|
2025
|
+
const errorMessage = await extractErrorMessageFromResponse(response);
|
|
2026
|
+
const metadata = {
|
|
2027
|
+
status: response.status,
|
|
2028
|
+
url: response.url
|
|
2029
|
+
};
|
|
2030
|
+
throw new AIError(
|
|
2031
|
+
classifyHttpStatus(response.status),
|
|
2032
|
+
errorMessage,
|
|
2033
|
+
void 0,
|
|
2034
|
+
metadata
|
|
2035
|
+
);
|
|
2036
|
+
}
|
|
2037
|
+
normalizeError(error) {
|
|
2038
|
+
if (error instanceof AIError) return error;
|
|
2039
|
+
if (error instanceof Error) {
|
|
2040
|
+
if (error.name === "AbortError") {
|
|
2041
|
+
return new AIError(
|
|
2042
|
+
"ABORT_ERROR" /* ABORT_ERROR */,
|
|
2043
|
+
"Request was aborted by the client",
|
|
2044
|
+
error
|
|
2045
|
+
);
|
|
2046
|
+
}
|
|
2047
|
+
if (error.name === "TypeError" && error.message.toLowerCase().includes("network")) {
|
|
2048
|
+
return new AIError(
|
|
2049
|
+
"NETWORK_ERROR" /* NETWORK_ERROR */,
|
|
2050
|
+
"Network error: Unable to connect to the AI service",
|
|
2051
|
+
error
|
|
2052
|
+
);
|
|
2053
|
+
}
|
|
2054
|
+
}
|
|
2055
|
+
return new AIError(
|
|
2056
|
+
"UNKNOWN_ERROR" /* UNKNOWN_ERROR */,
|
|
2057
|
+
error instanceof Error ? error.message : "Unknown error occurred",
|
|
2058
|
+
error instanceof Error ? error : void 0
|
|
2059
|
+
);
|
|
2060
|
+
}
|
|
2061
|
+
};
|
|
2062
|
+
|
|
2063
|
+
// src/providers/groq/groq-http-client.ts
|
|
2064
|
+
var GroqHttpClient = class {
|
|
2065
|
+
transport;
|
|
2066
|
+
executor;
|
|
2067
|
+
constructor(config, deps = {}) {
|
|
2068
|
+
this.transport = deps.transport ?? new GroqHttpTransport(config);
|
|
2069
|
+
const retryManager2 = deps.retryManager ?? new RetryManager();
|
|
2070
|
+
const queue = deps.queue ?? new RequestQueue();
|
|
2071
|
+
const deduplicator = deps.deduplicator ?? new RequestDeduplicator();
|
|
2072
|
+
this.executor = new CompositeExecutor([
|
|
2073
|
+
new DedupeExecutor(deduplicator, {
|
|
2074
|
+
generateKey: (request) => deduplicatorKey(request, config.apiKey)
|
|
2075
|
+
}),
|
|
2076
|
+
new RetryExecutor(retryManager2),
|
|
2077
|
+
new QueueExecutor(queue, QUEUE_PRIORITY_USER_REQUEST)
|
|
2078
|
+
]);
|
|
2079
|
+
}
|
|
2080
|
+
cancelPendingRequests() {
|
|
2081
|
+
this.transport.cancelPendingRequests();
|
|
2082
|
+
}
|
|
2083
|
+
postChatCompletion(request) {
|
|
2084
|
+
return this.executor.execute(
|
|
2085
|
+
(req) => this.transport.postChatCompletion(req),
|
|
2086
|
+
request
|
|
2087
|
+
);
|
|
2088
|
+
}
|
|
2089
|
+
streamChatCompletion(request, options) {
|
|
2090
|
+
return this.transport.streamChatCompletion(request, options);
|
|
2091
|
+
}
|
|
2092
|
+
};
|
|
2093
|
+
function deduplicatorKey(request, apiKey) {
|
|
2094
|
+
const { model, messages, stream: _stream, ...rest } = request;
|
|
2095
|
+
const stable = JSON.stringify({
|
|
2096
|
+
apiKey,
|
|
2097
|
+
model,
|
|
2098
|
+
messages: messages.map((m) => ({ role: m.role, content: m.content })),
|
|
2099
|
+
...rest
|
|
2100
|
+
});
|
|
2101
|
+
return stable;
|
|
2102
|
+
}
|
|
2103
|
+
|
|
2104
|
+
// src/providers/groq/groq-sse.ts
|
|
2105
|
+
function extractContentFromSsePayload(payload) {
|
|
2106
|
+
try {
|
|
2107
|
+
const parsed = JSON.parse(payload);
|
|
2108
|
+
const delta = parsed.choices?.[0]?.delta?.content;
|
|
2109
|
+
return typeof delta === "string" && delta.length > 0 ? delta : null;
|
|
2110
|
+
} catch {
|
|
2111
|
+
return null;
|
|
2112
|
+
}
|
|
2113
|
+
}
|
|
2114
|
+
function mapGroqFinishReason(reason) {
|
|
2115
|
+
if (reason === "stop") return "stop";
|
|
2116
|
+
if (reason === "length") return "length";
|
|
2117
|
+
if (reason === "content_filter") return "content_filter";
|
|
2118
|
+
return void 0;
|
|
2119
|
+
}
|
|
2120
|
+
function mapGroqUsage(usage) {
|
|
2121
|
+
if (!usage) return void 0;
|
|
2122
|
+
return {
|
|
2123
|
+
inputTokens: usage.prompt_tokens,
|
|
2124
|
+
outputTokens: usage.completion_tokens,
|
|
2125
|
+
totalTokens: usage.total_tokens
|
|
2126
|
+
};
|
|
2127
|
+
}
|
|
2128
|
+
async function consumeGroqStream(stream, callbacks) {
|
|
2129
|
+
const reader = stream.getReader();
|
|
2130
|
+
const decoder = new TextDecoder();
|
|
2131
|
+
const chunks = [];
|
|
2132
|
+
let buffer = "";
|
|
2133
|
+
let model = null;
|
|
2134
|
+
let finishReason;
|
|
2135
|
+
let usage;
|
|
2136
|
+
const consumeLine = (rawLine) => {
|
|
2137
|
+
const line = rawLine.endsWith("\r") ? rawLine.slice(0, -1) : rawLine;
|
|
2138
|
+
const trimmed = line.trim();
|
|
2139
|
+
if (trimmed.length === 0 || !isSseDataLine(trimmed)) return;
|
|
2140
|
+
const payload = extractSseDataPayload(trimmed);
|
|
2141
|
+
if (isSseDoneMarker(payload)) return;
|
|
2142
|
+
let chunk;
|
|
2143
|
+
try {
|
|
2144
|
+
chunk = JSON.parse(payload);
|
|
2145
|
+
} catch {
|
|
2146
|
+
return;
|
|
2147
|
+
}
|
|
2148
|
+
const errorBody = chunk.error;
|
|
2149
|
+
if (errorBody) {
|
|
2150
|
+
throw new AIError(
|
|
2151
|
+
errorBody.message?.toLowerCase().includes("rate limit") ? "RATE_LIMIT_ERROR" /* RATE_LIMIT_ERROR */ : "SERVER_ERROR" /* SERVER_ERROR */,
|
|
2152
|
+
errorBody.message || "Groq stream failed mid-flight"
|
|
2153
|
+
);
|
|
2154
|
+
}
|
|
2155
|
+
if (chunk.model) model = chunk.model;
|
|
2156
|
+
const choice = chunk.choices?.[0];
|
|
2157
|
+
const delta = choice?.delta?.content;
|
|
2158
|
+
if (typeof delta === "string" && delta.length > 0) {
|
|
2159
|
+
chunks.push(delta);
|
|
2160
|
+
callbacks.onChunk?.(delta);
|
|
2161
|
+
}
|
|
2162
|
+
if (choice?.finish_reason) {
|
|
2163
|
+
finishReason = mapGroqFinishReason(choice.finish_reason);
|
|
2164
|
+
}
|
|
2165
|
+
const tailUsage = chunk.x_groq?.usage;
|
|
2166
|
+
if (tailUsage) {
|
|
2167
|
+
usage = mapGroqUsage(tailUsage);
|
|
2168
|
+
}
|
|
2169
|
+
};
|
|
2170
|
+
try {
|
|
2171
|
+
while (true) {
|
|
2172
|
+
const { done, value } = await reader.read();
|
|
2173
|
+
if (done) break;
|
|
2174
|
+
buffer += decoder.decode(value, { stream: true });
|
|
2175
|
+
const lines = buffer.split("\n");
|
|
2176
|
+
buffer = lines.pop() ?? "";
|
|
2177
|
+
for (const line of lines) consumeLine(line);
|
|
2178
|
+
}
|
|
2179
|
+
buffer += decoder.decode();
|
|
2180
|
+
if (buffer.length > 0) consumeLine(buffer);
|
|
2181
|
+
} catch (error) {
|
|
2182
|
+
if (error instanceof AIError) throw error;
|
|
2183
|
+
throw new AIError(
|
|
2184
|
+
"NETWORK_ERROR" /* NETWORK_ERROR */,
|
|
2185
|
+
"Stream was interrupted",
|
|
2186
|
+
error instanceof Error ? error : void 0
|
|
2187
|
+
);
|
|
2188
|
+
} finally {
|
|
2189
|
+
reader.releaseLock();
|
|
2190
|
+
}
|
|
2191
|
+
return { text: chunks.join(""), model, finishReason, usage };
|
|
2192
|
+
}
|
|
2193
|
+
|
|
2194
|
+
// src/providers/groq/groq.provider.ts
|
|
2195
|
+
var CACHE_LAYOUT_VERSION = 2;
|
|
2196
|
+
var STRUCTURED_TEMPERATURE = 0.1;
|
|
2197
|
+
var GroqProvider = class {
|
|
2198
|
+
constructor(config) {
|
|
2199
|
+
this.config = config;
|
|
2200
|
+
this.client = new GroqHttpClient(config);
|
|
2201
|
+
this.tiers = mergeTierOverrides(GROQ_TIERS, config.models);
|
|
2202
|
+
}
|
|
2203
|
+
config;
|
|
2204
|
+
id = "groq";
|
|
2205
|
+
name = "Groq";
|
|
2206
|
+
capabilities = ["text"];
|
|
2207
|
+
client;
|
|
2208
|
+
tiers;
|
|
2209
|
+
cache = new CacheManager();
|
|
2210
|
+
jsonParser = new JsonResponseParser();
|
|
2211
|
+
resolveModel(tier) {
|
|
2212
|
+
return resolveModel(this.tiers, tier, this.id);
|
|
2213
|
+
}
|
|
2214
|
+
async healthCheck() {
|
|
2215
|
+
return this.config.apiKey ? "healthy" : "unavailable";
|
|
2216
|
+
}
|
|
2217
|
+
async estimateCost() {
|
|
2218
|
+
return 0;
|
|
2219
|
+
}
|
|
2220
|
+
async generate(request) {
|
|
2221
|
+
const model = request.model ?? this.resolveModel(request.tier ?? "balanced");
|
|
2222
|
+
const cacheKey = this.cache.generateKey({
|
|
2223
|
+
v: CACHE_LAYOUT_VERSION,
|
|
2224
|
+
provider: this.id,
|
|
2225
|
+
op: "generate",
|
|
2226
|
+
model,
|
|
2227
|
+
messages: request.messages,
|
|
2228
|
+
systemPrompt: request.systemPrompt ?? null,
|
|
2229
|
+
temperature: request.temperature ?? null,
|
|
2230
|
+
maxTokens: request.maxTokens ?? null,
|
|
2231
|
+
topP: request.topP ?? null
|
|
2232
|
+
});
|
|
2233
|
+
const cached = this.cache.get(cacheKey);
|
|
2234
|
+
if (cached) return cached;
|
|
2235
|
+
const response = await this.client.postChatCompletion(
|
|
2236
|
+
this.buildWireRequest(model, request)
|
|
2237
|
+
);
|
|
2238
|
+
const choice = response.choices?.[0];
|
|
2239
|
+
const text = choice?.message?.content ?? "";
|
|
2240
|
+
if (text.length === 0) {
|
|
2241
|
+
throw new AIError(
|
|
2242
|
+
"SERVER_ERROR" /* SERVER_ERROR */,
|
|
2243
|
+
"No content generated from Groq API",
|
|
2244
|
+
void 0,
|
|
2245
|
+
{ model }
|
|
2246
|
+
);
|
|
2247
|
+
}
|
|
2248
|
+
const result = {
|
|
2249
|
+
text,
|
|
2250
|
+
usage: mapGroqUsage(response.usage),
|
|
2251
|
+
model: response.model || model,
|
|
2252
|
+
providerId: this.id,
|
|
2253
|
+
finishReason: mapGroqFinishReason(choice?.finish_reason)
|
|
2254
|
+
};
|
|
2255
|
+
this.cache.set(cacheKey, result);
|
|
2256
|
+
return result;
|
|
2257
|
+
}
|
|
2258
|
+
async generateStructured(request) {
|
|
2259
|
+
const model = request.model ?? this.resolveModel(request.tier ?? "balanced");
|
|
2260
|
+
const messages = toGroqMessages(request.messages, request.systemPrompt);
|
|
2261
|
+
const lastUserIndex = findLastIndex2(messages, (m) => m.role === "user");
|
|
2262
|
+
if (lastUserIndex === -1) {
|
|
2263
|
+
throw new AIError(
|
|
2264
|
+
"UNKNOWN_ERROR" /* UNKNOWN_ERROR */,
|
|
2265
|
+
"generateStructured requires at least one user message"
|
|
2266
|
+
);
|
|
2267
|
+
}
|
|
2268
|
+
const structuredPrompt = new StructuredPromptBuilder(
|
|
2269
|
+
messages[lastUserIndex].content
|
|
2270
|
+
).withSchema(request.schema).build();
|
|
2271
|
+
messages[lastUserIndex] = { role: "user", content: structuredPrompt };
|
|
2272
|
+
const wireRequest = GroqChatRequestBuilder.for(model).withMessages(messages).withConfig(this.generationConfig(request, STRUCTURED_TEMPERATURE)).withJsonResponse(true).build();
|
|
2273
|
+
const response = await this.client.postChatCompletion(wireRequest);
|
|
2274
|
+
const choice = response.choices?.[0];
|
|
2275
|
+
const text = choice?.message?.content ?? "";
|
|
2276
|
+
if (text.length === 0) {
|
|
2277
|
+
throw new AIError(
|
|
2278
|
+
"SERVER_ERROR" /* SERVER_ERROR */,
|
|
2279
|
+
"No content generated from Groq API",
|
|
2280
|
+
void 0,
|
|
2281
|
+
{ model }
|
|
2282
|
+
);
|
|
2283
|
+
}
|
|
2284
|
+
return {
|
|
2285
|
+
value: this.jsonParser.parse(text),
|
|
2286
|
+
result: {
|
|
2287
|
+
text,
|
|
2288
|
+
usage: mapGroqUsage(response.usage),
|
|
2289
|
+
model: response.model || model,
|
|
2290
|
+
providerId: this.id,
|
|
2291
|
+
finishReason: mapGroqFinishReason(choice?.finish_reason)
|
|
2292
|
+
}
|
|
2293
|
+
};
|
|
2294
|
+
}
|
|
2295
|
+
async stream(request, callbacks) {
|
|
2296
|
+
const model = request.model ?? this.resolveModel(request.tier ?? "balanced");
|
|
2297
|
+
const streamBody = await this.client.streamChatCompletion(
|
|
2298
|
+
this.buildWireRequest(model, request),
|
|
2299
|
+
{ signal: request.signal }
|
|
2300
|
+
);
|
|
2301
|
+
try {
|
|
2302
|
+
const outcome = await consumeGroqStream(streamBody, callbacks);
|
|
2303
|
+
if (outcome.text.length === 0) {
|
|
2304
|
+
throw new AIError(
|
|
2305
|
+
"SERVER_ERROR" /* SERVER_ERROR */,
|
|
2306
|
+
"No content generated from Groq API",
|
|
2307
|
+
void 0,
|
|
2308
|
+
{ model }
|
|
2309
|
+
);
|
|
2310
|
+
}
|
|
2311
|
+
const result = {
|
|
2312
|
+
text: outcome.text,
|
|
2313
|
+
usage: outcome.usage,
|
|
2314
|
+
model: outcome.model ?? model,
|
|
2315
|
+
providerId: this.id,
|
|
2316
|
+
finishReason: outcome.finishReason
|
|
2317
|
+
};
|
|
2318
|
+
callbacks.onComplete?.(result);
|
|
2319
|
+
return result;
|
|
2320
|
+
} catch (error) {
|
|
2321
|
+
callbacks.onError?.(error instanceof Error ? error : new Error(String(error)));
|
|
2322
|
+
throw error;
|
|
2323
|
+
}
|
|
2324
|
+
}
|
|
2325
|
+
getCacheStats() {
|
|
2326
|
+
return this.cache.getStats();
|
|
2327
|
+
}
|
|
2328
|
+
clearCache() {
|
|
2329
|
+
this.cache.clear();
|
|
2330
|
+
}
|
|
2331
|
+
cancelPendingRequests() {
|
|
2332
|
+
this.client.cancelPendingRequests();
|
|
2333
|
+
}
|
|
2334
|
+
buildWireRequest(model, request) {
|
|
2335
|
+
return GroqChatRequestBuilder.for(model).withMessages(toGroqMessages(request.messages, request.systemPrompt)).withConfig(this.generationConfig(request)).build();
|
|
2336
|
+
}
|
|
2337
|
+
generationConfig(request, temperatureOverride) {
|
|
2338
|
+
return {
|
|
2339
|
+
...request.temperature !== void 0 ? { temperature: request.temperature } : {},
|
|
2340
|
+
...temperatureOverride !== void 0 ? { temperature: temperatureOverride } : {},
|
|
2341
|
+
...request.maxTokens !== void 0 ? { maxTokens: request.maxTokens } : {},
|
|
2342
|
+
...request.topP !== void 0 ? { topP: request.topP } : {},
|
|
2343
|
+
...request.stopSequences !== void 0 && request.stopSequences.length > 0 ? { stop: [...request.stopSequences] } : {}
|
|
2344
|
+
};
|
|
2345
|
+
}
|
|
2346
|
+
};
|
|
2347
|
+
function toGroqMessages(messages, systemPrompt) {
|
|
2348
|
+
const wire = [];
|
|
2349
|
+
if (systemPrompt && systemPrompt.length > 0) {
|
|
2350
|
+
wire.push({ role: "system", content: systemPrompt });
|
|
2351
|
+
}
|
|
2352
|
+
for (const message of messages) {
|
|
2353
|
+
if (message.role === "system") {
|
|
2354
|
+
wire.push({ role: "system", content: message.content });
|
|
2355
|
+
continue;
|
|
2356
|
+
}
|
|
2357
|
+
wire.push({ role: message.role, content: message.content });
|
|
2358
|
+
}
|
|
2359
|
+
return wire;
|
|
2360
|
+
}
|
|
2361
|
+
function findLastIndex2(items, predicate) {
|
|
2362
|
+
for (let i = items.length - 1; i >= 0; i--) {
|
|
2363
|
+
if (predicate(items[i])) return i;
|
|
2364
|
+
}
|
|
2365
|
+
return -1;
|
|
2366
|
+
}
|
|
2367
|
+
|
|
2368
|
+
// src/content/domain/predicates/ProviderCostEstimator.ts
|
|
2369
|
+
var FAL_FIXED_COST = 1;
|
|
2370
|
+
var GEMINI_IMAGE_COST = 0.02;
|
|
2371
|
+
var GEMINI_TEXT_PER_1K_CHARS_COST = 1e-4;
|
|
2372
|
+
function estimateFalGenerationCost(_request) {
|
|
2373
|
+
return FAL_FIXED_COST;
|
|
2374
|
+
}
|
|
2375
|
+
function estimateGeminiGenerationCost(request) {
|
|
2376
|
+
if (request.type === "image") return GEMINI_IMAGE_COST;
|
|
2377
|
+
return GEMINI_TEXT_PER_1K_CHARS_COST;
|
|
2378
|
+
}
|
|
2379
|
+
|
|
2380
|
+
// src/content/domain/limits/ModelDefaults.ts
|
|
2381
|
+
var ModelDefaults = {
|
|
2382
|
+
FAL_IMAGE: "fal-ai/flux/schnell",
|
|
2383
|
+
FAL_VIDEO: "fal-ai/hunyuan-video/1",
|
|
2384
|
+
FAL_IMAGE_TO_VIDEO: "fal-ai/fast-animatediff/image-to-video",
|
|
2385
|
+
GEMINI_TEXT: "gemini-2.0-flash",
|
|
2386
|
+
GEMINI_IMAGE: "imagen-4.0-generate-001"};
|
|
2387
|
+
|
|
2388
|
+
// src/providers/gemini/gemini.provider.ts
|
|
2389
|
+
var GEMINI_TIERS = {
|
|
2390
|
+
fast: "gemini-2.0-flash-lite",
|
|
2391
|
+
balanced: ModelDefaults.GEMINI_TEXT,
|
|
2392
|
+
powerful: "gemini-2.5-pro"
|
|
2393
|
+
};
|
|
2394
|
+
var GeminiProvider = class extends BaseProvider {
|
|
2395
|
+
id = "gemini";
|
|
2396
|
+
name = "Google Gemini";
|
|
2397
|
+
capabilities = ["text", "image"];
|
|
2398
|
+
models;
|
|
2399
|
+
tiers;
|
|
2400
|
+
jsonParser = new JsonResponseParser();
|
|
2401
|
+
constructor(config) {
|
|
2402
|
+
super({
|
|
2403
|
+
apiKey: config.apiKey,
|
|
2404
|
+
baseUrl: config.baseUrl || "https://generativelanguage.googleapis.com/v1beta",
|
|
2405
|
+
timeout: config.timeout || ProviderTimingConfig.GEMINI_TIMEOUT_MS,
|
|
2406
|
+
retryAttempts: config.retryAttempts || ProviderTimingConfig.GEMINI_RETRY_ATTEMPTS
|
|
2407
|
+
});
|
|
2408
|
+
this.models = {
|
|
2409
|
+
text: ModelDefaults.GEMINI_TEXT,
|
|
2410
|
+
image: ModelDefaults.GEMINI_IMAGE
|
|
2411
|
+
};
|
|
2412
|
+
if (config.models) {
|
|
2413
|
+
this.models = { ...this.models, ...config.models };
|
|
2414
|
+
}
|
|
2415
|
+
this.tiers = mergeTierOverrides(
|
|
2416
|
+
GEMINI_TIERS,
|
|
2417
|
+
config.models ? { balanced: config.models.text } : void 0
|
|
2418
|
+
);
|
|
2419
|
+
}
|
|
2420
|
+
resolveModel(tier) {
|
|
2421
|
+
return resolveModel(this.tiers, tier, this.id);
|
|
2422
|
+
}
|
|
2423
|
+
async healthCheck() {
|
|
2424
|
+
try {
|
|
2425
|
+
if (!this.config.apiKey) {
|
|
2426
|
+
return "unavailable";
|
|
2427
|
+
}
|
|
2428
|
+
const response = await this.fetchWithTimeout(
|
|
2429
|
+
`${this.config.baseUrl}/models`,
|
|
2430
|
+
{
|
|
2431
|
+
method: "GET",
|
|
2432
|
+
headers: this.authHeaders()
|
|
2433
|
+
}
|
|
2434
|
+
);
|
|
2435
|
+
if (response.ok) return "healthy";
|
|
2436
|
+
if (response.status === 401 || response.status === 403) return "unavailable";
|
|
2437
|
+
return "degraded";
|
|
2438
|
+
} catch {
|
|
2439
|
+
return "unavailable";
|
|
2440
|
+
}
|
|
2441
|
+
}
|
|
2442
|
+
async estimateCost(request) {
|
|
2443
|
+
return estimateGeminiGenerationCost(request);
|
|
2444
|
+
}
|
|
2445
|
+
async generate(request) {
|
|
2446
|
+
try {
|
|
2447
|
+
const model = request.model ?? this.resolveModel(request.tier ?? "balanced");
|
|
2448
|
+
const systemMessages = request.messages.filter((m) => m.role === "system");
|
|
2449
|
+
const systemPrompt = request.systemPrompt && request.systemPrompt.length > 0 ? request.systemPrompt : systemMessages.length > 0 ? systemMessages.map((m) => m.content).join("\n") : void 0;
|
|
2450
|
+
const response = await this.withRetry(async () => {
|
|
2451
|
+
return await this.fetchWithTimeout(
|
|
2452
|
+
`${this.config.baseUrl}/models/${model}:generateContent`,
|
|
2453
|
+
{
|
|
2454
|
+
method: "POST",
|
|
2455
|
+
headers: this.authHeaders(),
|
|
2456
|
+
body: JSON.stringify({
|
|
2457
|
+
systemInstruction: systemPrompt && systemPrompt.length > 0 ? { parts: [{ text: systemPrompt }] } : void 0,
|
|
2458
|
+
contents: toGeminiContents(request.messages),
|
|
2459
|
+
generationConfig: {
|
|
2460
|
+
temperature: request.temperature ?? 0.7,
|
|
2461
|
+
maxOutputTokens: request.maxTokens ?? 2048,
|
|
2462
|
+
topP: request.topP ?? 0.9
|
|
2463
|
+
}
|
|
2464
|
+
})
|
|
2465
|
+
}
|
|
2466
|
+
);
|
|
2467
|
+
});
|
|
2468
|
+
if (!response.ok) {
|
|
2469
|
+
const message = await extractErrorMessageFromResponse(response);
|
|
2470
|
+
throw new AIError(
|
|
2471
|
+
classifyHttpStatus(response.status),
|
|
2472
|
+
`Gemini API error (${response.status}): ${message}`,
|
|
2473
|
+
void 0,
|
|
2474
|
+
{ status: response.status, model }
|
|
2475
|
+
);
|
|
2476
|
+
}
|
|
2477
|
+
const data = await response.json();
|
|
2478
|
+
const text = data.candidates?.[0]?.content?.parts?.map((part) => part.text ?? "").join("") ?? "";
|
|
2479
|
+
if (text.length === 0) {
|
|
2480
|
+
throw new AIError("SERVER_ERROR" /* SERVER_ERROR */, "No content generated from Gemini API", void 0, { model });
|
|
2481
|
+
}
|
|
2482
|
+
return {
|
|
2483
|
+
text,
|
|
2484
|
+
usage: data.usageMetadata ? {
|
|
2485
|
+
inputTokens: data.usageMetadata.promptTokenCount,
|
|
2486
|
+
outputTokens: data.usageMetadata.candidatesTokenCount,
|
|
2487
|
+
totalTokens: data.usageMetadata.totalTokenCount
|
|
2488
|
+
} : void 0,
|
|
2489
|
+
model,
|
|
2490
|
+
providerId: this.id,
|
|
2491
|
+
finishReason: mapGeminiFinishReason(data.candidates?.[0]?.finishReason)
|
|
2492
|
+
};
|
|
2493
|
+
} catch (error) {
|
|
2494
|
+
if (error instanceof AIError) throw error;
|
|
2495
|
+
throw new AIError(
|
|
2496
|
+
"UNKNOWN_ERROR" /* UNKNOWN_ERROR */,
|
|
2497
|
+
`Gemini text generation failed: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
2498
|
+
error instanceof Error ? error : void 0
|
|
2499
|
+
);
|
|
2500
|
+
}
|
|
2501
|
+
}
|
|
2502
|
+
async generateStructured(request) {
|
|
2503
|
+
const messages = [...request.messages];
|
|
2504
|
+
const lastUserIndex = findLastIndex3(messages, (m) => m.role === "user");
|
|
2505
|
+
if (lastUserIndex === -1) {
|
|
2506
|
+
throw new AIError(
|
|
2507
|
+
"UNKNOWN_ERROR" /* UNKNOWN_ERROR */,
|
|
2508
|
+
"generateStructured requires at least one user message"
|
|
2509
|
+
);
|
|
2510
|
+
}
|
|
2511
|
+
messages[lastUserIndex] = {
|
|
2512
|
+
role: "user",
|
|
2513
|
+
content: new StructuredPromptBuilder(messages[lastUserIndex].content).withSchema(request.schema).build()
|
|
2514
|
+
};
|
|
2515
|
+
const result = await this.generate({ ...request, messages, temperature: 0.1 });
|
|
2516
|
+
return { value: this.jsonParser.parse(result.text), result };
|
|
2517
|
+
}
|
|
2518
|
+
async stream(request, callbacks) {
|
|
2519
|
+
const result = await this.generate(request);
|
|
2520
|
+
callbacks.onChunk?.(result.text);
|
|
2521
|
+
callbacks.onComplete?.(result);
|
|
2522
|
+
return result;
|
|
2523
|
+
}
|
|
2524
|
+
async generateImage(request) {
|
|
2525
|
+
try {
|
|
2526
|
+
const response = await this.withRetry(async () => {
|
|
2527
|
+
return await this.fetchWithTimeout(
|
|
2528
|
+
`${this.config.baseUrl}/models/${this.models.image}:generateContent`,
|
|
2529
|
+
{
|
|
2530
|
+
method: "POST",
|
|
2531
|
+
headers: this.authHeaders(),
|
|
2532
|
+
body: JSON.stringify({
|
|
2533
|
+
contents: [{
|
|
2534
|
+
parts: [{
|
|
2535
|
+
text: this.buildImagePrompt(request)
|
|
2536
|
+
}]
|
|
2537
|
+
}]
|
|
2538
|
+
})
|
|
2539
|
+
}
|
|
2540
|
+
);
|
|
2541
|
+
});
|
|
2542
|
+
if (!response.ok) {
|
|
2543
|
+
const message = await extractErrorMessageFromResponse(response);
|
|
2544
|
+
throw new AIError(
|
|
2545
|
+
classifyHttpStatus(response.status),
|
|
2546
|
+
`Gemini image generation error (${response.status}): ${message}`,
|
|
2547
|
+
void 0,
|
|
2548
|
+
{ status: response.status }
|
|
2549
|
+
);
|
|
2550
|
+
}
|
|
2551
|
+
const data = await response.json();
|
|
2552
|
+
const imageData = data.candidates?.[0]?.content?.parts?.[0]?.inlineData;
|
|
2553
|
+
if (!imageData) {
|
|
2554
|
+
throw new AIError("SERVER_ERROR" /* SERVER_ERROR */, "No image data in Gemini response");
|
|
2555
|
+
}
|
|
2556
|
+
const url = `data:${imageData.mimeType};base64,${imageData.data}`;
|
|
2557
|
+
return {
|
|
2558
|
+
id: Date.now().toString(),
|
|
2559
|
+
type: "image",
|
|
2560
|
+
url,
|
|
2561
|
+
metadata: {
|
|
2562
|
+
provider: "gemini",
|
|
2563
|
+
model: this.models.image,
|
|
2564
|
+
mimeType: imageData.mimeType
|
|
2565
|
+
},
|
|
2566
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2567
|
+
};
|
|
2568
|
+
} catch (error) {
|
|
2569
|
+
throw new Error(`Gemini image generation failed: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
2570
|
+
}
|
|
2571
|
+
}
|
|
2572
|
+
async generateVideo(_request) {
|
|
2573
|
+
throw new Error("Gemini provider does not support video generation");
|
|
2574
|
+
}
|
|
2575
|
+
async imageToVideo(_request) {
|
|
2576
|
+
throw new Error("Gemini provider does not support image-to-video conversion");
|
|
2577
|
+
}
|
|
2578
|
+
async videoToVideo(_request) {
|
|
2579
|
+
throw new Error("Gemini provider does not support video-to-video conversion");
|
|
2580
|
+
}
|
|
2581
|
+
authHeaders() {
|
|
2582
|
+
return {
|
|
2583
|
+
"Content-Type": "application/json",
|
|
2584
|
+
"x-goog-api-key": this.config.apiKey
|
|
2585
|
+
};
|
|
2586
|
+
}
|
|
2587
|
+
buildImagePrompt(request) {
|
|
2588
|
+
let prompt = request.prompt;
|
|
2589
|
+
if (request.style) {
|
|
2590
|
+
const styles = {
|
|
2591
|
+
realistic: "photorealistic, highly detailed, 8k resolution",
|
|
2592
|
+
cartoon: "cartoon style, colorful, animated",
|
|
2593
|
+
abstract: "abstract art, modern, artistic",
|
|
2594
|
+
minimalist: "minimalist, clean, simple",
|
|
2595
|
+
vintage: "vintage style, retro, classic"
|
|
2596
|
+
};
|
|
2597
|
+
prompt += `, ${styles[request.style]}`;
|
|
2598
|
+
}
|
|
2599
|
+
if (request.quality === "ultra") {
|
|
2600
|
+
prompt += ", ultra high quality, masterpiece";
|
|
2601
|
+
}
|
|
2602
|
+
return prompt;
|
|
2603
|
+
}
|
|
2604
|
+
};
|
|
2605
|
+
function toGeminiContents(messages) {
|
|
2606
|
+
return messages.filter((message) => message.role !== "system" && message.content.length > 0).map((message) => ({
|
|
2607
|
+
role: message.role === "assistant" ? "model" : "user",
|
|
2608
|
+
parts: [{ text: message.content }]
|
|
2609
|
+
}));
|
|
2610
|
+
}
|
|
2611
|
+
function mapGeminiFinishReason(reason) {
|
|
2612
|
+
if (reason === "STOP") return "stop";
|
|
2613
|
+
if (reason === "MAX_TOKENS") return "length";
|
|
2614
|
+
if (reason === "SAFETY" || reason === "PROHIBITED_CONTENT") return "content_filter";
|
|
2615
|
+
return void 0;
|
|
2616
|
+
}
|
|
2617
|
+
function findLastIndex3(items, predicate) {
|
|
2618
|
+
for (let i = items.length - 1; i >= 0; i--) {
|
|
2619
|
+
if (predicate(items[i])) return i;
|
|
2620
|
+
}
|
|
2621
|
+
return -1;
|
|
2622
|
+
}
|
|
2623
|
+
function createGeminiProvider(config) {
|
|
2624
|
+
return new GeminiProvider(config);
|
|
2625
|
+
}
|
|
2626
|
+
|
|
2627
|
+
// src/content/domain/predicates/FalImageRenderingOptions.ts
|
|
2628
|
+
var FAL_FORMAT_TO_IMAGE_SIZE = {
|
|
2629
|
+
square: "square_hd",
|
|
2630
|
+
landscape: "landscape_16_9",
|
|
2631
|
+
portrait: "portrait_9_16",
|
|
2632
|
+
story: "portrait_9_16",
|
|
2633
|
+
banner: "landscape_16_9"
|
|
2634
|
+
};
|
|
2635
|
+
var FAL_DEFAULT_IMAGE_SIZE = "square_hd";
|
|
2636
|
+
var FAL_QUALITY_TO_STEPS = {
|
|
2637
|
+
standard: 4,
|
|
2638
|
+
high: 6,
|
|
2639
|
+
ultra: 8
|
|
2640
|
+
};
|
|
2641
|
+
var FAL_DEFAULT_QUALITY_STEPS = 6;
|
|
2642
|
+
function resolveFalImageSize(format) {
|
|
2643
|
+
if (!format) return FAL_DEFAULT_IMAGE_SIZE;
|
|
2644
|
+
return FAL_FORMAT_TO_IMAGE_SIZE[format] ?? FAL_DEFAULT_IMAGE_SIZE;
|
|
2645
|
+
}
|
|
2646
|
+
function resolveFalInferenceSteps(quality) {
|
|
2647
|
+
if (!quality) return FAL_DEFAULT_QUALITY_STEPS;
|
|
2648
|
+
return FAL_QUALITY_TO_STEPS[quality] ?? FAL_DEFAULT_QUALITY_STEPS;
|
|
2649
|
+
}
|
|
2650
|
+
|
|
2651
|
+
// src/providers/fal/fal.provider.ts
|
|
2652
|
+
var FalProvider = class extends BaseProvider {
|
|
2653
|
+
id = "fal";
|
|
2654
|
+
name = "FAL";
|
|
2655
|
+
capabilities = ["image", "video"];
|
|
2656
|
+
models;
|
|
2657
|
+
constructor(config) {
|
|
2658
|
+
super({
|
|
2659
|
+
apiKey: config.apiKey,
|
|
2660
|
+
baseUrl: config.baseUrl || "https://queue.fal.run",
|
|
2661
|
+
timeout: config.timeout || ProviderTimingConfig.FAL_TIMEOUT_MS,
|
|
2662
|
+
retryAttempts: config.retryAttempts || ProviderTimingConfig.FAL_RETRY_ATTEMPTS
|
|
2663
|
+
});
|
|
2664
|
+
this.models = {
|
|
2665
|
+
image: ModelDefaults.FAL_IMAGE,
|
|
2666
|
+
video: ModelDefaults.FAL_VIDEO,
|
|
2667
|
+
imageToVideo: ModelDefaults.FAL_IMAGE_TO_VIDEO
|
|
2668
|
+
};
|
|
2669
|
+
if (config.models) {
|
|
2670
|
+
this.models = { ...this.models, ...config.models };
|
|
2671
|
+
}
|
|
2672
|
+
}
|
|
2673
|
+
async healthCheck() {
|
|
2674
|
+
try {
|
|
2675
|
+
if (!this.config.apiKey) {
|
|
2676
|
+
return "unavailable";
|
|
2677
|
+
}
|
|
2678
|
+
const response = await this.fetchWithTimeout(
|
|
2679
|
+
`${this.config.baseUrl}/fal-ai/flux/schnell/status`,
|
|
2680
|
+
{
|
|
2681
|
+
method: "HEAD",
|
|
2682
|
+
headers: {
|
|
2683
|
+
"Authorization": `Key ${this.config.apiKey}`
|
|
2684
|
+
}
|
|
2685
|
+
}
|
|
2686
|
+
);
|
|
2687
|
+
return response.ok ? "healthy" : "degraded";
|
|
2688
|
+
} catch {
|
|
2689
|
+
return "unavailable";
|
|
2690
|
+
}
|
|
2691
|
+
}
|
|
2692
|
+
async estimateCost(request) {
|
|
2693
|
+
return estimateFalGenerationCost();
|
|
2694
|
+
}
|
|
2695
|
+
async generateImage(request) {
|
|
2696
|
+
try {
|
|
2697
|
+
const response = await this.withRetry(async () => {
|
|
2698
|
+
return await this.fetchWithTimeout(
|
|
2699
|
+
`${this.config.baseUrl}/${this.models.image}`,
|
|
2700
|
+
{
|
|
2701
|
+
method: "POST",
|
|
2702
|
+
headers: {
|
|
2703
|
+
"Authorization": `Key ${this.config.apiKey}`,
|
|
2704
|
+
"Content-Type": "application/json"
|
|
2705
|
+
},
|
|
2706
|
+
body: JSON.stringify({
|
|
2707
|
+
prompt: request.prompt,
|
|
2708
|
+
image_size: resolveFalImageSize(request.format),
|
|
2709
|
+
num_inference_steps: resolveFalInferenceSteps(request.quality),
|
|
2710
|
+
num_images: request.quantity || 1
|
|
2711
|
+
})
|
|
2712
|
+
}
|
|
2713
|
+
);
|
|
2714
|
+
});
|
|
2715
|
+
if (!response.ok) {
|
|
2716
|
+
throw new Error(`FAL API error: ${response.statusText}`);
|
|
2717
|
+
}
|
|
2718
|
+
const data = await response.json();
|
|
2719
|
+
if (data.error) {
|
|
2720
|
+
throw new Error(data.error);
|
|
2721
|
+
}
|
|
2722
|
+
return {
|
|
2723
|
+
id: Date.now().toString(),
|
|
2724
|
+
type: "image",
|
|
2725
|
+
url: data.images?.[0]?.url,
|
|
2726
|
+
metadata: {
|
|
2727
|
+
provider: "fal",
|
|
2728
|
+
model: this.models.image
|
|
2729
|
+
},
|
|
2730
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2731
|
+
};
|
|
2732
|
+
} catch (error) {
|
|
2733
|
+
throw new Error(`FAL image generation failed: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
2734
|
+
}
|
|
2735
|
+
}
|
|
2736
|
+
async generateVideo(request) {
|
|
2737
|
+
try {
|
|
2738
|
+
const response = await this.withRetry(async () => {
|
|
2739
|
+
return await this.fetchWithTimeout(
|
|
2740
|
+
`${this.config.baseUrl}/${this.models.video}`,
|
|
2741
|
+
{
|
|
2742
|
+
method: "POST",
|
|
2743
|
+
headers: {
|
|
2744
|
+
"Authorization": `Key ${this.config.apiKey}`,
|
|
2745
|
+
"Content-Type": "application/json"
|
|
2746
|
+
},
|
|
2747
|
+
body: JSON.stringify({
|
|
2748
|
+
prompt: request.prompt,
|
|
2749
|
+
aspect_ratio: request.aspectRatio || "16:9"
|
|
2750
|
+
})
|
|
2751
|
+
}
|
|
2752
|
+
);
|
|
2753
|
+
});
|
|
2754
|
+
if (!response.ok) {
|
|
2755
|
+
throw new Error(`FAL API error: ${response.statusText}`);
|
|
2756
|
+
}
|
|
2757
|
+
const data = await response.json();
|
|
2758
|
+
if (data.error) {
|
|
2759
|
+
throw new Error(data.error);
|
|
2760
|
+
}
|
|
2761
|
+
return {
|
|
2762
|
+
id: Date.now().toString(),
|
|
2763
|
+
type: "video",
|
|
2764
|
+
url: data.video?.url,
|
|
2765
|
+
metadata: {
|
|
2766
|
+
provider: "fal",
|
|
2767
|
+
model: this.models.video
|
|
2768
|
+
},
|
|
2769
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2770
|
+
};
|
|
2771
|
+
} catch (error) {
|
|
2772
|
+
throw new Error(`FAL video generation failed: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
2773
|
+
}
|
|
2774
|
+
}
|
|
2775
|
+
async imageToVideo(request) {
|
|
2776
|
+
try {
|
|
2777
|
+
const response = await this.withRetry(async () => {
|
|
2778
|
+
return await this.fetchWithTimeout(
|
|
2779
|
+
`${this.config.baseUrl}/${this.models.imageToVideo}`,
|
|
2780
|
+
{
|
|
2781
|
+
method: "POST",
|
|
2782
|
+
headers: {
|
|
2783
|
+
"Authorization": `Key ${this.config.apiKey}`,
|
|
2784
|
+
"Content-Type": "application/json"
|
|
2785
|
+
},
|
|
2786
|
+
body: JSON.stringify({
|
|
2787
|
+
image_url: request.imageUrl,
|
|
2788
|
+
motion: request.motion || "medium",
|
|
2789
|
+
prompt: request.prompt
|
|
2790
|
+
})
|
|
2791
|
+
}
|
|
2792
|
+
);
|
|
2793
|
+
});
|
|
2794
|
+
if (!response.ok) {
|
|
2795
|
+
throw new Error(`FAL API error: ${response.statusText}`);
|
|
2796
|
+
}
|
|
2797
|
+
const data = await response.json();
|
|
2798
|
+
if (data.error) {
|
|
2799
|
+
throw new Error(data.error);
|
|
2800
|
+
}
|
|
2801
|
+
return {
|
|
2802
|
+
id: Date.now().toString(),
|
|
2803
|
+
type: "video",
|
|
2804
|
+
url: data.video?.url,
|
|
2805
|
+
metadata: {
|
|
2806
|
+
provider: "fal",
|
|
2807
|
+
model: this.models.imageToVideo
|
|
2808
|
+
},
|
|
2809
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2810
|
+
};
|
|
2811
|
+
} catch (error) {
|
|
2812
|
+
throw new Error(`FAL image-to-video failed: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
2813
|
+
}
|
|
2814
|
+
}
|
|
2815
|
+
async videoToVideo(_request) {
|
|
2816
|
+
throw new Error("FAL provider does not support video-to-video conversion");
|
|
2817
|
+
}
|
|
2818
|
+
};
|
|
2819
|
+
function createFalProvider(config) {
|
|
2820
|
+
return new FalProvider(config);
|
|
2821
|
+
}
|
|
2822
|
+
|
|
2823
|
+
// src/providers/pruna/pruna-generate.ts
|
|
2824
|
+
var prunaCorePromise = null;
|
|
2825
|
+
async function loadPrunaCore() {
|
|
2826
|
+
if (!prunaCorePromise) {
|
|
2827
|
+
const specifier = "@umituz/pruna-provider/core";
|
|
2828
|
+
prunaCorePromise = import(specifier);
|
|
2829
|
+
}
|
|
2830
|
+
return prunaCorePromise;
|
|
2831
|
+
}
|
|
2832
|
+
async function* generateWithPruna(request, apiKey, signal, onProgress) {
|
|
2833
|
+
const prunaCore = await loadPrunaCore();
|
|
2834
|
+
if (prunaCore.generateWithPruna) {
|
|
2835
|
+
yield* prunaCore.generateWithPruna(request, apiKey, signal, onProgress);
|
|
2836
|
+
return;
|
|
2837
|
+
}
|
|
2838
|
+
if (!prunaCore.submitPrediction || !prunaCore.pollForResult) {
|
|
2839
|
+
throw new Error("@umituz/pruna-provider/core does not export required functions");
|
|
2840
|
+
}
|
|
2841
|
+
const modelId = request.model || (request.image ? "p-image-edit" : "p-image");
|
|
2842
|
+
const prediction = await prunaCore.submitPrediction(modelId, request, apiKey, signal, onProgress);
|
|
2843
|
+
if (prediction.output && typeof prediction.output === "object" && "url" in prediction.output) {
|
|
2844
|
+
yield {
|
|
2845
|
+
stage: "predicting",
|
|
2846
|
+
result: { url: prediction.output.url, model: modelId }
|
|
2847
|
+
};
|
|
2848
|
+
return;
|
|
2849
|
+
}
|
|
2850
|
+
const pollUrl = prediction.status_url || prediction.generation_url;
|
|
2851
|
+
if (!pollUrl) {
|
|
2852
|
+
throw new Error("Pruna prediction response missing polling URL");
|
|
2853
|
+
}
|
|
2854
|
+
const resultUrl = await prunaCore.pollForResult(pollUrl, apiKey, 60, 2e3, signal, onProgress);
|
|
2855
|
+
yield { stage: "polling", result: { url: resultUrl, model: modelId } };
|
|
2856
|
+
}
|
|
2857
|
+
|
|
2858
|
+
// src/providers/pruna/pruna.provider.ts
|
|
2859
|
+
var PrunaProvider = class {
|
|
2860
|
+
constructor(config) {
|
|
2861
|
+
this.config = config;
|
|
2862
|
+
}
|
|
2863
|
+
config;
|
|
2864
|
+
id = "pruna";
|
|
2865
|
+
name = "Pruna AI";
|
|
2866
|
+
capabilities = ["image", "video"];
|
|
2867
|
+
async healthCheck() {
|
|
2868
|
+
return this.config.apiKey ? "healthy" : "unavailable";
|
|
2869
|
+
}
|
|
2870
|
+
async estimateCost(_request) {
|
|
2871
|
+
return 1;
|
|
2872
|
+
}
|
|
2873
|
+
async generateImage(request) {
|
|
2874
|
+
let finalUrl;
|
|
2875
|
+
const model = this.config.models?.textToImage;
|
|
2876
|
+
for await (const stage of generateWithPruna(
|
|
2877
|
+
{ prompt: request.prompt, model, quality: request.quality },
|
|
2878
|
+
this.config.apiKey
|
|
2879
|
+
)) {
|
|
2880
|
+
if (stage.error) throw stage.error;
|
|
2881
|
+
if (stage.result?.url) finalUrl = stage.result.url;
|
|
2882
|
+
}
|
|
2883
|
+
if (!finalUrl) {
|
|
2884
|
+
throw new AIError("SERVER_ERROR" /* SERVER_ERROR */, "No image generated by Pruna AI");
|
|
2885
|
+
}
|
|
2886
|
+
return {
|
|
2887
|
+
id: Date.now().toString(),
|
|
2888
|
+
type: "image",
|
|
2889
|
+
url: finalUrl,
|
|
2890
|
+
metadata: { provider: this.id, model: model ?? "p-image" },
|
|
2891
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2892
|
+
};
|
|
2893
|
+
}
|
|
2894
|
+
async generateVideo(request) {
|
|
2895
|
+
let finalUrl;
|
|
2896
|
+
const model = this.config.models?.textToVideo;
|
|
2897
|
+
for await (const stage of generateWithPruna(
|
|
2898
|
+
{ prompt: request.prompt, model, duration: request.duration, aspectRatio: request.aspectRatio },
|
|
2899
|
+
this.config.apiKey
|
|
2900
|
+
)) {
|
|
2901
|
+
if (stage.error) throw stage.error;
|
|
2902
|
+
if (stage.result?.url) finalUrl = stage.result.url;
|
|
2903
|
+
}
|
|
2904
|
+
if (!finalUrl) {
|
|
2905
|
+
throw new AIError("SERVER_ERROR" /* SERVER_ERROR */, "No video generated by Pruna AI");
|
|
2906
|
+
}
|
|
2907
|
+
return {
|
|
2908
|
+
id: Date.now().toString(),
|
|
2909
|
+
type: "video",
|
|
2910
|
+
url: finalUrl,
|
|
2911
|
+
metadata: { provider: this.id, model: model ?? "p-video" },
|
|
2912
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2913
|
+
};
|
|
2914
|
+
}
|
|
2915
|
+
async imageToVideo(request) {
|
|
2916
|
+
let finalUrl;
|
|
2917
|
+
for await (const stage of generateWithPruna(
|
|
2918
|
+
{ image: request.imageUrl, prompt: request.prompt, duration: request.duration, motion: request.motion },
|
|
2919
|
+
this.config.apiKey
|
|
2920
|
+
)) {
|
|
2921
|
+
if (stage.error) throw stage.error;
|
|
2922
|
+
if (stage.result?.url) finalUrl = stage.result.url;
|
|
2923
|
+
}
|
|
2924
|
+
if (!finalUrl) {
|
|
2925
|
+
throw new AIError("SERVER_ERROR" /* SERVER_ERROR */, "No video generated from image by Pruna AI");
|
|
2926
|
+
}
|
|
2927
|
+
return {
|
|
2928
|
+
id: Date.now().toString(),
|
|
2929
|
+
type: "video",
|
|
2930
|
+
url: finalUrl,
|
|
2931
|
+
metadata: { provider: this.id, model: "p-image-to-video" },
|
|
2932
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2933
|
+
};
|
|
2934
|
+
}
|
|
2935
|
+
async videoToVideo(_request) {
|
|
2936
|
+
throw new AIError("CLIENT_ERROR" /* CLIENT_ERROR */, "Pruna AI provider does not support video-to-video conversion");
|
|
2937
|
+
}
|
|
2938
|
+
};
|
|
2939
|
+
function createPrunaProvider(config) {
|
|
2940
|
+
return new PrunaProvider(config);
|
|
2941
|
+
}
|
|
2942
|
+
|
|
2943
|
+
// src/providers/factory/create-ai.ts
|
|
2944
|
+
function createAI(config = {}) {
|
|
2945
|
+
const factory = new ProviderFactory({
|
|
2946
|
+
priority: config.priority,
|
|
2947
|
+
fallbackEnabled: config.fallbackEnabled,
|
|
2948
|
+
defaultTier: config.defaultTier
|
|
2949
|
+
});
|
|
2950
|
+
if (config.anthropic?.apiKey) {
|
|
2951
|
+
factory.register(new AnthropicProvider(config.anthropic));
|
|
2952
|
+
}
|
|
2953
|
+
if (config.groq?.apiKey) {
|
|
2954
|
+
factory.register(new GroqProvider(config.groq));
|
|
2955
|
+
}
|
|
2956
|
+
if (config.gemini?.apiKey) {
|
|
2957
|
+
factory.register(new GeminiProvider({ ...config.gemini, enabled: true }));
|
|
2958
|
+
}
|
|
2959
|
+
if (config.fal?.apiKey) {
|
|
2960
|
+
factory.register(new FalProvider({ ...config.fal, enabled: true }));
|
|
2961
|
+
}
|
|
2962
|
+
if (config.pruna?.apiKey) {
|
|
2963
|
+
factory.register(new PrunaProvider({ ...config.pruna, enabled: true }));
|
|
2964
|
+
}
|
|
2965
|
+
return factory;
|
|
2966
|
+
}
|
|
2967
|
+
|
|
2968
|
+
// src/core/calculations/random-base36-suffix.ts
|
|
2969
|
+
var BASE_36_RADIX = 36;
|
|
2970
|
+
var DECIMAL_OFFSET = 2;
|
|
2971
|
+
function generateRandomBase36Suffix(length, random = Math.random) {
|
|
2972
|
+
if (length <= 0) return "";
|
|
2973
|
+
const raw = random().toString(BASE_36_RADIX).slice(DECIMAL_OFFSET);
|
|
2974
|
+
if (raw.length >= length) return raw.slice(0, length);
|
|
2975
|
+
let result = raw;
|
|
2976
|
+
while (result.length < length) {
|
|
2977
|
+
result += random().toString(BASE_36_RADIX).slice(DECIMAL_OFFSET);
|
|
2978
|
+
}
|
|
2979
|
+
return result.slice(0, length);
|
|
2980
|
+
}
|
|
2981
|
+
|
|
2982
|
+
// src/chat/utils/chat-id-generator.ts
|
|
2983
|
+
var ID_PART_DELIMITER = "-";
|
|
2984
|
+
var ChatIdGenerator = class {
|
|
2985
|
+
counter = 0;
|
|
2986
|
+
randomLength;
|
|
2987
|
+
constructor(randomLength = CHAT_MESSAGE_ID_SUFFIX_LENGTH) {
|
|
2988
|
+
this.randomLength = randomLength;
|
|
2989
|
+
}
|
|
2990
|
+
next(sender) {
|
|
2991
|
+
this.counter += 1;
|
|
2992
|
+
const random = generateRandomBase36Suffix(this.randomLength);
|
|
2993
|
+
return [
|
|
2994
|
+
MESSAGE_ID_PREFIX,
|
|
2995
|
+
Date.now(),
|
|
2996
|
+
sender,
|
|
2997
|
+
this.counter,
|
|
2998
|
+
random
|
|
2999
|
+
].join(ID_PART_DELIMITER);
|
|
3000
|
+
}
|
|
3001
|
+
};
|
|
3002
|
+
var chatIdGenerator = new ChatIdGenerator();
|
|
3003
|
+
|
|
3004
|
+
// src/chat/utils/message-formatter.ts
|
|
3005
|
+
var MessageFormatter = class {
|
|
3006
|
+
constructor(idGenerator = chatIdGenerator) {
|
|
3007
|
+
this.idGenerator = idGenerator;
|
|
3008
|
+
}
|
|
3009
|
+
idGenerator;
|
|
3010
|
+
/** Maps chat-domain history to neutral core messages; system senders are
|
|
3011
|
+
* dropped — the system prompt travels on the request's own slot. */
|
|
3012
|
+
toCoreMessages(messages) {
|
|
3013
|
+
const coreMessages = [];
|
|
3014
|
+
for (const message of messages) {
|
|
3015
|
+
if (message.sender === "system") continue;
|
|
3016
|
+
coreMessages.push({
|
|
3017
|
+
role: message.sender === "user" ? USER_ROLE : ASSISTANT_ROLE,
|
|
3018
|
+
content: message.content
|
|
3019
|
+
});
|
|
3020
|
+
}
|
|
3021
|
+
return coreMessages;
|
|
3022
|
+
}
|
|
3023
|
+
toChatMessage(content, sender) {
|
|
3024
|
+
return {
|
|
3025
|
+
id: this.idGenerator.next(sender),
|
|
3026
|
+
sender,
|
|
3027
|
+
content: content.trim(),
|
|
3028
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3029
|
+
type: "text"
|
|
3030
|
+
};
|
|
3031
|
+
}
|
|
3032
|
+
};
|
|
3033
|
+
var messageFormatter = new MessageFormatter();
|
|
3034
|
+
|
|
3035
|
+
// src/chat/utils/prompt-strategy.ts
|
|
3036
|
+
var DEFAULT_PROMPT_STRATEGY = {
|
|
3037
|
+
resolve: (config) => {
|
|
3038
|
+
if (config.systemPrompt && config.systemPrompt.length > 0) {
|
|
3039
|
+
return config.systemPrompt;
|
|
3040
|
+
}
|
|
3041
|
+
return void 0;
|
|
3042
|
+
}
|
|
3043
|
+
};
|
|
3044
|
+
|
|
3045
|
+
// src/chat/services/chat.service.ts
|
|
3046
|
+
var DEFAULT_CONFIG = {
|
|
3047
|
+
temperature: 0.8,
|
|
3048
|
+
maxTokens: 500
|
|
3049
|
+
};
|
|
3050
|
+
var ChatService = class {
|
|
3051
|
+
constructor(engine) {
|
|
3052
|
+
this.engine = engine;
|
|
3053
|
+
}
|
|
3054
|
+
engine;
|
|
3055
|
+
config = DEFAULT_CONFIG;
|
|
3056
|
+
promptStrategy = DEFAULT_PROMPT_STRATEGY;
|
|
3057
|
+
storage = null;
|
|
3058
|
+
initialize(config) {
|
|
3059
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
3060
|
+
}
|
|
3061
|
+
updateConfig(config) {
|
|
3062
|
+
this.config = { ...this.config, ...config };
|
|
3063
|
+
}
|
|
3064
|
+
setPromptStrategy(strategy) {
|
|
3065
|
+
this.promptStrategy = strategy;
|
|
3066
|
+
}
|
|
3067
|
+
setStorage(storage) {
|
|
3068
|
+
this.storage = storage;
|
|
3069
|
+
}
|
|
3070
|
+
async getConversation(conversationId) {
|
|
3071
|
+
const storage = this.requireStorage();
|
|
3072
|
+
const messages = await storage.getMessages(conversationId);
|
|
3073
|
+
if (messages.length === 0) return null;
|
|
3074
|
+
const last = messages[messages.length - 1];
|
|
3075
|
+
const first = messages[0];
|
|
3076
|
+
const companionId = String(first.metadata?.["companionId"] ?? "unknown");
|
|
3077
|
+
const companionName = String(first.metadata?.["companionName"] ?? "Unknown");
|
|
3078
|
+
return {
|
|
3079
|
+
id: conversationId,
|
|
3080
|
+
companionId,
|
|
3081
|
+
companionName,
|
|
3082
|
+
lastMessage: last,
|
|
3083
|
+
messageCount: messages.length,
|
|
3084
|
+
updatedAt: last.timestamp
|
|
3085
|
+
};
|
|
3086
|
+
}
|
|
3087
|
+
async getConversations() {
|
|
3088
|
+
if (this.storage && "getConversations" in this.storage && typeof this.storage.getConversations === "function") {
|
|
3089
|
+
return this.storage.getConversations();
|
|
3090
|
+
}
|
|
3091
|
+
return [];
|
|
3092
|
+
}
|
|
3093
|
+
async sendMessage(input, signal) {
|
|
3094
|
+
const userMessage = {
|
|
3095
|
+
...messageFormatter.toChatMessage(input.text, "user"),
|
|
3096
|
+
type: input.type ?? "text",
|
|
3097
|
+
...input.imageUrl !== void 0 ? { imageUrl: input.imageUrl } : {}
|
|
3098
|
+
};
|
|
3099
|
+
const aiResponse = await this.generateAIResponse(
|
|
3100
|
+
input.companionId,
|
|
3101
|
+
input.text,
|
|
3102
|
+
[],
|
|
3103
|
+
signal
|
|
3104
|
+
);
|
|
3105
|
+
return { userMessage, aiResponse };
|
|
3106
|
+
}
|
|
3107
|
+
async generateAIResponse(companionId, userMessage, context = [], signal) {
|
|
3108
|
+
const result = await this.run({
|
|
3109
|
+
...this.buildRequest(userMessage, context, signal)
|
|
3110
|
+
});
|
|
3111
|
+
return {
|
|
3112
|
+
...messageFormatter.toChatMessage(result.text, "assistant"),
|
|
3113
|
+
metadata: {
|
|
3114
|
+
companionId,
|
|
3115
|
+
model: result.model,
|
|
3116
|
+
usage: {
|
|
3117
|
+
input_tokens: result.usage?.inputTokens,
|
|
3118
|
+
output_tokens: result.usage?.outputTokens,
|
|
3119
|
+
total_tokens: result.usage?.totalTokens
|
|
3120
|
+
},
|
|
3121
|
+
finishReason: result.finishReason
|
|
3122
|
+
}
|
|
3123
|
+
};
|
|
3124
|
+
}
|
|
3125
|
+
async generateStreamingResponse(userMessage, context, onChunk, signal) {
|
|
3126
|
+
const request = this.buildRequest(userMessage, context, signal);
|
|
3127
|
+
const result = await this.runStream(request, { onChunk });
|
|
3128
|
+
return result.text;
|
|
3129
|
+
}
|
|
3130
|
+
buildRequest(userMessage, context, signal) {
|
|
3131
|
+
const messages = messageFormatter.toCoreMessages(context);
|
|
3132
|
+
messages.push({ role: "user", content: userMessage });
|
|
3133
|
+
return {
|
|
3134
|
+
messages,
|
|
3135
|
+
systemPrompt: this.resolveSystemPrompt(),
|
|
3136
|
+
model: this.config.model,
|
|
3137
|
+
tier: this.config.tier,
|
|
3138
|
+
temperature: this.config.temperature,
|
|
3139
|
+
maxTokens: this.config.maxTokens,
|
|
3140
|
+
...signal !== void 0 ? { signal } : {}
|
|
3141
|
+
};
|
|
3142
|
+
}
|
|
3143
|
+
resolveSystemPrompt() {
|
|
3144
|
+
const prompt = this.promptStrategy.resolve(this.config);
|
|
3145
|
+
return prompt && prompt.length > 0 ? prompt : void 0;
|
|
3146
|
+
}
|
|
3147
|
+
async run(request) {
|
|
3148
|
+
if (this.engine instanceof ProviderFactory) {
|
|
3149
|
+
return this.engine.chat(request);
|
|
3150
|
+
}
|
|
3151
|
+
return this.engine.generate(request);
|
|
3152
|
+
}
|
|
3153
|
+
async runStream(request, callbacks) {
|
|
3154
|
+
if (this.engine instanceof ProviderFactory) {
|
|
3155
|
+
return this.engine.chatStream(request, callbacks);
|
|
3156
|
+
}
|
|
3157
|
+
return this.engine.stream(request, callbacks);
|
|
3158
|
+
}
|
|
3159
|
+
requireStorage() {
|
|
3160
|
+
if (!this.storage) {
|
|
3161
|
+
throw new AIError(
|
|
3162
|
+
"MISSING_CONFIG" /* MISSING_CONFIG */,
|
|
3163
|
+
"ChatService.getConversation requires setStorage() to be called first."
|
|
3164
|
+
);
|
|
3165
|
+
}
|
|
3166
|
+
return this.storage;
|
|
3167
|
+
}
|
|
3168
|
+
};
|
|
3169
|
+
function extractTokenUsage(message) {
|
|
3170
|
+
const meta = message.metadata;
|
|
3171
|
+
if (!meta) return null;
|
|
3172
|
+
const usage = meta["usage"];
|
|
3173
|
+
if (!usage || typeof usage !== "object") return null;
|
|
3174
|
+
const u = usage;
|
|
3175
|
+
const inputTokens = readNumber(u, ["input_tokens", "prompt_tokens"]);
|
|
3176
|
+
const outputTokens = readNumber(u, ["output_tokens", "completion_tokens"]);
|
|
3177
|
+
const totalTokens = readNumber(u, ["total_tokens"]);
|
|
3178
|
+
if (inputTokens === null || outputTokens === null || totalTokens === null) {
|
|
3179
|
+
return null;
|
|
3180
|
+
}
|
|
3181
|
+
return {
|
|
3182
|
+
promptTokens: inputTokens,
|
|
3183
|
+
completionTokens: outputTokens,
|
|
3184
|
+
totalTokens
|
|
3185
|
+
};
|
|
3186
|
+
}
|
|
3187
|
+
function readNumber(source, keys) {
|
|
3188
|
+
for (const key of keys) {
|
|
3189
|
+
const value = source[key];
|
|
3190
|
+
if (typeof value === "number") return value;
|
|
3191
|
+
}
|
|
3192
|
+
return null;
|
|
3193
|
+
}
|
|
3194
|
+
|
|
3195
|
+
export { AIError, AIErrorType, ANTHROPIC_API_VERSION, ANTHROPIC_DEFAULT_MAX_TOKENS, ANTHROPIC_MODELS, ANTHROPIC_TIERS, AnthropicHttpTransport, AnthropicProvider, AnthropicRequestBuilder, BaseProvider, CacheManager, ChatIdGenerator, ChatService, CompositeExecutor, DEFAULT_API_KEY_RULE, DEFAULT_ERROR_CLASSIFICATION_RULE, DEFAULT_HTTP_STATUS_RULE, DEFAULT_PROMPT_STRATEGY, DEFAULT_RETRY_BACKOFF_MULTIPLIER, DEFAULT_RETRY_INITIAL_DELAY_MS, DEFAULT_RETRY_JITTER_FACTOR, DEFAULT_RETRY_MAX_ATTEMPTS, DEFAULT_RETRY_MAX_DELAY_MS, DebounceManager, DedupeExecutor, FalProvider, GEMINI_TIERS, GROQ_MODELS, GROQ_TIERS, GeminiProvider, GroqChatRequestBuilder, GroqHttpClient, GroqHttpTransport, GroqProvider, JSON_CODE_BLOCK_PATTERN, JSON_OBJECT_PATTERN, JsonResponseParser, ProviderError, ProviderFactory, PrunaProvider, QueueExecutor, RequestDeduplicator, RequestQueue, RetryExecutor, RetryManager, SSE_BRIDGE_EVENTS, SSE_DONE_MARKER, SSE_PREFIX, StreamingResponseParser, StructuredPromptBuilder, TEXT_TIERS, ThrottleManager, USER_FRIENDLY_ERROR_MESSAGES, asObjectArray, asRecord, asStringArray, asTrimmedOptionalString, buildCacheKey, buildHttpStatusFallback, cacheManager, chatIdGenerator, clampScore, classifyHttpStatus, consumeAnthropicStream, consumeGroqStream, createAI, createFalProvider, createGeminiProvider, createPrunaProvider, createSseResponse, debounce, debounceManager, defaultEffortForModel, describeAIErrorType, extractContentFromSsePayload, extractErrorMessageFromJsonBody, extractErrorMessageFromResponse, extractJsonArray, extractJsonCandidate, extractJsonFromCodeFence, extractJsonObject, extractSseDataPayload, extractTokenUsage, formatErrorForLogging, generateWithPruna, isAuthErrorType, isMediaProvider, isRetryableErrorType, isSseDataLine, isSseDoneMarker, isTextProvider, isTextTier, isValidApiKey, mapAnthropicStopReason, mergeTierOverrides, messageFormatter, normalizeSseChunk, oneOfCatalog, repairCommonJsonDefects, requestDeduplicator, requestQueue, resolveModel, resolveUserFriendlyErrorMessage, resolveUserFriendlyMessage, retryManager, splitSseLines, throttle, throttleManager, toGroqMessages, validateApiKey, withRetry };
|
|
3196
|
+
//# sourceMappingURL=index.js.map
|
|
3197
|
+
//# sourceMappingURL=index.js.map
|