ai-lcr 0.2.2 → 0.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +108 -0
- package/README.md +2 -3
- package/README.zh-CN.md +2 -3
- package/dist/index.cjs +256 -132
- package/dist/index.d.cts +48 -38
- package/dist/index.d.ts +48 -38
- package/dist/index.js +255 -131
- package/package.json +5 -3
package/dist/index.cjs
CHANGED
|
@@ -21,10 +21,10 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
DEFAULT_REFERENCE: () => DEFAULT_REFERENCE,
|
|
24
|
-
FalMediaError: () => FalMediaError,
|
|
25
24
|
MEDIA_PRICING: () => MEDIA_PRICING,
|
|
26
25
|
cheapestRoute: () => cheapestRoute,
|
|
27
26
|
classifyError: () => classifyError,
|
|
27
|
+
classifyErrorKind: () => classifyErrorKind,
|
|
28
28
|
comparePrices: () => comparePrices,
|
|
29
29
|
createFalMediaAdapter: () => createFalMediaAdapter,
|
|
30
30
|
createKunavoMediaAdapter: () => createKunavoMediaAdapter,
|
|
@@ -58,35 +58,128 @@ var RETRYABLE_PATTERNS = [
|
|
|
58
58
|
"504",
|
|
59
59
|
"429",
|
|
60
60
|
// Billing caps — a capped provider should fall over, not kill the request.
|
|
61
|
+
// Include non-English wording: Chinese providers (e.g. Kunavo) report a failed
|
|
62
|
+
// charge as "余额不足"/"账户欠费"/"扣费失败" with a 200/400 body, which no
|
|
63
|
+
// English keyword and no HTTP status would catch — so without these a billing
|
|
64
|
+
// failure would die instead of failing over, the exact opposite of what we want.
|
|
61
65
|
"insufficient",
|
|
62
66
|
"credit",
|
|
63
67
|
"quota",
|
|
64
68
|
"billing",
|
|
65
|
-
"payment required"
|
|
69
|
+
"payment required",
|
|
70
|
+
"balance",
|
|
71
|
+
"\u4F59\u989D",
|
|
72
|
+
"\u6B20\u8D39",
|
|
73
|
+
"\u6263\u8D39",
|
|
74
|
+
"\u6263\u6B3E"
|
|
66
75
|
];
|
|
76
|
+
var NETWORK_CODES = /* @__PURE__ */ new Set([
|
|
77
|
+
"ECONNREFUSED",
|
|
78
|
+
"ECONNRESET",
|
|
79
|
+
"ECONNABORTED",
|
|
80
|
+
"ENOTFOUND",
|
|
81
|
+
"EAI_AGAIN",
|
|
82
|
+
"ETIMEDOUT",
|
|
83
|
+
"EPIPE",
|
|
84
|
+
"EHOSTUNREACH",
|
|
85
|
+
"ENETUNREACH",
|
|
86
|
+
"EPROTO",
|
|
87
|
+
"UND_ERR_SOCKET",
|
|
88
|
+
"UND_ERR_CONNECT_TIMEOUT",
|
|
89
|
+
"UND_ERR_HEADERS_TIMEOUT",
|
|
90
|
+
"UND_ERR_BODY_TIMEOUT"
|
|
91
|
+
]);
|
|
92
|
+
var NETWORK_PATTERNS = [
|
|
93
|
+
"fetch failed",
|
|
94
|
+
"failed to fetch",
|
|
95
|
+
"socket hang up",
|
|
96
|
+
"socket disconnected",
|
|
97
|
+
"econnrefused",
|
|
98
|
+
"econnreset",
|
|
99
|
+
"enotfound",
|
|
100
|
+
"etimedout",
|
|
101
|
+
"ehostunreach",
|
|
102
|
+
"enetunreach",
|
|
103
|
+
"eai_again",
|
|
104
|
+
"getaddrinfo",
|
|
105
|
+
"connect timeout",
|
|
106
|
+
"connection refused",
|
|
107
|
+
"connection reset",
|
|
108
|
+
"connection error",
|
|
109
|
+
"network error",
|
|
110
|
+
"dns"
|
|
111
|
+
];
|
|
112
|
+
function safeStringify(value) {
|
|
113
|
+
try {
|
|
114
|
+
return JSON.stringify(value) ?? "";
|
|
115
|
+
} catch {
|
|
116
|
+
return String(value);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
function errorSignals(error) {
|
|
120
|
+
const parts = [];
|
|
121
|
+
const codes = [];
|
|
122
|
+
const seen = /* @__PURE__ */ new Set();
|
|
123
|
+
let cur = error;
|
|
124
|
+
for (let depth = 0; depth < 6 && cur && typeof cur === "object" && !seen.has(cur); depth++) {
|
|
125
|
+
seen.add(cur);
|
|
126
|
+
const e = cur;
|
|
127
|
+
if (typeof e.message === "string") parts.push(e.message);
|
|
128
|
+
if (typeof e.name === "string") parts.push(e.name);
|
|
129
|
+
if (typeof e.code === "string") {
|
|
130
|
+
parts.push(e.code);
|
|
131
|
+
codes.push(e.code);
|
|
132
|
+
}
|
|
133
|
+
cur = e.cause;
|
|
134
|
+
}
|
|
135
|
+
if (parts.length === 0) parts.push(safeStringify(error));
|
|
136
|
+
return { text: parts.join(" ").toLowerCase(), codes };
|
|
137
|
+
}
|
|
138
|
+
function isNetworkError(error) {
|
|
139
|
+
const { text, codes } = errorSignals(error);
|
|
140
|
+
if (codes.some((c) => NETWORK_CODES.has(c))) return true;
|
|
141
|
+
return NETWORK_PATTERNS.some((p) => text.includes(p));
|
|
142
|
+
}
|
|
67
143
|
function isRetryableError(error) {
|
|
68
144
|
const e = error;
|
|
69
145
|
const status = e?.statusCode ?? e?.status;
|
|
70
146
|
if (typeof status === "number" && (RETRYABLE_STATUS.has(status) || status > 500)) {
|
|
71
147
|
return true;
|
|
72
148
|
}
|
|
73
|
-
|
|
149
|
+
if (isNetworkError(error)) return true;
|
|
150
|
+
const { text } = errorSignals(error);
|
|
74
151
|
return RETRYABLE_PATTERNS.some((p) => text.includes(p));
|
|
75
152
|
}
|
|
76
|
-
function safeStringify(value) {
|
|
77
|
-
try {
|
|
78
|
-
return JSON.stringify(value) ?? "";
|
|
79
|
-
} catch {
|
|
80
|
-
return String(value);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
153
|
function classifyError(error) {
|
|
84
154
|
const e = error;
|
|
85
155
|
const status = e?.statusCode ?? e?.status;
|
|
86
156
|
if (typeof status === "number") return String(status);
|
|
87
|
-
|
|
157
|
+
if (isNetworkError(error)) return "network";
|
|
158
|
+
const { text } = errorSignals(error);
|
|
88
159
|
return RETRYABLE_PATTERNS.find((p) => text.includes(p)) ?? "error";
|
|
89
160
|
}
|
|
161
|
+
var AUTH_STATUS = /* @__PURE__ */ new Set([401, 403]);
|
|
162
|
+
var BILLING_PATTERNS = [
|
|
163
|
+
"insufficient",
|
|
164
|
+
"credit",
|
|
165
|
+
"quota",
|
|
166
|
+
"billing",
|
|
167
|
+
"payment required",
|
|
168
|
+
"balance",
|
|
169
|
+
"exhausted",
|
|
170
|
+
"\u4F59\u989D",
|
|
171
|
+
"\u6B20\u8D39",
|
|
172
|
+
"\u6263\u8D39",
|
|
173
|
+
"\u6263\u6B3E"
|
|
174
|
+
];
|
|
175
|
+
function classifyErrorKind(error) {
|
|
176
|
+
const e = error;
|
|
177
|
+
const status = e?.statusCode ?? e?.status;
|
|
178
|
+
const { text } = errorSignals(error);
|
|
179
|
+
if (status === 402 || BILLING_PATTERNS.some((p) => text.includes(p))) return "billing";
|
|
180
|
+
if (typeof status === "number" && AUTH_STATUS.has(status)) return "auth";
|
|
181
|
+
return isRetryableError(error) ? "transient" : "client";
|
|
182
|
+
}
|
|
90
183
|
var callSeq = 0;
|
|
91
184
|
function newCallId() {
|
|
92
185
|
const c = globalThis.crypto;
|
|
@@ -103,11 +196,20 @@ var LcrFallbackModel = class {
|
|
|
103
196
|
}
|
|
104
197
|
opts;
|
|
105
198
|
specificationVersion = "v3";
|
|
106
|
-
|
|
107
|
-
|
|
199
|
+
// Cross-request *hint* for where the next request starts: after a failover we
|
|
200
|
+
// remember the provider that worked so we don't re-probe a dead cheap one on
|
|
201
|
+
// every call. This is the ONLY shared mutable state — and crucially it is read
|
|
202
|
+
// once per request (snapshotted into a local cursor) and written once on
|
|
203
|
+
// settle, never used as a per-request loop bound. The within-request iteration
|
|
204
|
+
// is fully local, so concurrent requests can't corrupt each other's routing.
|
|
205
|
+
sticky = 0;
|
|
206
|
+
// When `sticky` was last advanced (a failover). The re-probe timer measures
|
|
207
|
+
// from THIS, not from the last call — so it fires under sustained traffic too,
|
|
208
|
+
// instead of being pushed forward forever by a busy stream of requests.
|
|
209
|
+
lastFailoverAt = Date.now();
|
|
108
210
|
resetIntervalMs;
|
|
109
211
|
get current() {
|
|
110
|
-
return this.opts.providers[this.
|
|
212
|
+
return this.opts.providers[this.sticky];
|
|
111
213
|
}
|
|
112
214
|
get modelId() {
|
|
113
215
|
return this.current.model.modelId;
|
|
@@ -118,18 +220,53 @@ var LcrFallbackModel = class {
|
|
|
118
220
|
get supportedUrls() {
|
|
119
221
|
return this.current.model.supportedUrls;
|
|
120
222
|
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
223
|
+
/**
|
|
224
|
+
* Index a new request should start at. If we're parked on a non-cheapest
|
|
225
|
+
* provider and it's been `resetIntervalMs` since the failover, snap back to
|
|
226
|
+
* the cheapest and re-probe it — this is what lets routing recover to the
|
|
227
|
+
* cheap source even during continuous traffic.
|
|
228
|
+
*/
|
|
229
|
+
startIndex() {
|
|
230
|
+
if (this.sticky !== 0 && Date.now() - this.lastFailoverAt >= this.resetIntervalMs) {
|
|
231
|
+
this.sticky = 0;
|
|
124
232
|
}
|
|
125
|
-
this.
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
|
|
233
|
+
return this.sticky;
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* A request settled on `winIndex`. Park there so the next request skips the
|
|
237
|
+
* providers we just learned are down. Stamp the failover time only when the
|
|
238
|
+
* parked provider actually CHANGES — so a steady stream of successful calls
|
|
239
|
+
* on the same fallback doesn't keep pushing the re-probe timer forward.
|
|
240
|
+
*/
|
|
241
|
+
settleSticky(winIndex) {
|
|
242
|
+
if (winIndex === this.sticky) return;
|
|
243
|
+
this.sticky = winIndex;
|
|
244
|
+
this.lastFailoverAt = Date.now();
|
|
129
245
|
}
|
|
130
246
|
shouldRetry(error) {
|
|
131
247
|
return (this.opts.shouldRetry ?? isRetryableError)(error);
|
|
132
248
|
}
|
|
249
|
+
// Observer callbacks are caller-supplied logging hooks: a throw from one of
|
|
250
|
+
// them must NEVER turn a successful (or already-failed) request into a
|
|
251
|
+
// different outcome. Swallow anything they throw — they are fire-and-forget.
|
|
252
|
+
emitError(error, provider) {
|
|
253
|
+
try {
|
|
254
|
+
this.opts.onError?.(error, provider);
|
|
255
|
+
} catch {
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
emitCost(event) {
|
|
259
|
+
try {
|
|
260
|
+
this.opts.onCost?.(event);
|
|
261
|
+
} catch {
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
emitCall(record) {
|
|
265
|
+
try {
|
|
266
|
+
this.opts.onCall?.(record);
|
|
267
|
+
} catch {
|
|
268
|
+
}
|
|
269
|
+
}
|
|
133
270
|
startCall() {
|
|
134
271
|
return { id: newCallId(), attempts: [], startedAt: Date.now() };
|
|
135
272
|
}
|
|
@@ -139,7 +276,8 @@ var LcrFallbackModel = class {
|
|
|
139
276
|
provider: provider.label,
|
|
140
277
|
ok: false,
|
|
141
278
|
latencyMs: Date.now() - attemptStart,
|
|
142
|
-
errorClass: classifyError(error)
|
|
279
|
+
errorClass: classifyError(error),
|
|
280
|
+
kind: classifyErrorKind(error)
|
|
143
281
|
});
|
|
144
282
|
}
|
|
145
283
|
/** Winner settled: record the attempt, fire `onCost` (compat) + `onCall`. */
|
|
@@ -148,14 +286,14 @@ var LcrFallbackModel = class {
|
|
|
148
286
|
const inputTokens = usage?.inputTokens?.total ?? 0;
|
|
149
287
|
const outputTokens = usage?.outputTokens?.total ?? 0;
|
|
150
288
|
const costUsd = provider.cost ? inputTokens / 1e6 * provider.cost.input + outputTokens / 1e6 * provider.cost.output : 0;
|
|
151
|
-
this.
|
|
289
|
+
this.emitCost({
|
|
152
290
|
model: this.opts.modelName,
|
|
153
291
|
provider: provider.label,
|
|
154
292
|
inputTokens,
|
|
155
293
|
outputTokens,
|
|
156
294
|
costUsd
|
|
157
295
|
});
|
|
158
|
-
this.
|
|
296
|
+
this.emitCall({
|
|
159
297
|
id: ctx.id,
|
|
160
298
|
model: this.opts.modelName,
|
|
161
299
|
attempts: ctx.attempts,
|
|
@@ -170,7 +308,7 @@ var LcrFallbackModel = class {
|
|
|
170
308
|
}
|
|
171
309
|
/** Every provider failed: fire `onCall` with no winner. */
|
|
172
310
|
finalizeFail(ctx) {
|
|
173
|
-
this.
|
|
311
|
+
this.emitCall({
|
|
174
312
|
id: ctx.id,
|
|
175
313
|
model: this.opts.modelName,
|
|
176
314
|
attempts: ctx.attempts,
|
|
@@ -184,15 +322,18 @@ var LcrFallbackModel = class {
|
|
|
184
322
|
});
|
|
185
323
|
}
|
|
186
324
|
async doGenerate(options) {
|
|
187
|
-
this.checkReset();
|
|
188
325
|
const ctx = this.startCall();
|
|
189
|
-
const
|
|
326
|
+
const providers = this.opts.providers;
|
|
327
|
+
const n = providers.length;
|
|
328
|
+
const start = this.startIndex();
|
|
190
329
|
let lastError;
|
|
191
|
-
for (; ; ) {
|
|
192
|
-
const
|
|
330
|
+
for (let tried = 0; tried < n; tried++) {
|
|
331
|
+
const idx = (start + tried) % n;
|
|
332
|
+
const provider = providers[idx];
|
|
193
333
|
const attemptStart = Date.now();
|
|
194
334
|
try {
|
|
195
335
|
const result = await provider.model.doGenerate(options);
|
|
336
|
+
this.settleSticky(idx);
|
|
196
337
|
this.finalizeOk(ctx, provider, attemptStart, result.usage);
|
|
197
338
|
return result;
|
|
198
339
|
} catch (error) {
|
|
@@ -202,31 +343,32 @@ var LcrFallbackModel = class {
|
|
|
202
343
|
this.finalizeFail(ctx);
|
|
203
344
|
throw error;
|
|
204
345
|
}
|
|
205
|
-
this.
|
|
346
|
+
this.emitError(error, provider.label);
|
|
206
347
|
this.recordFail(ctx, provider, attemptStart, error);
|
|
207
|
-
this.switchNext();
|
|
208
|
-
if (this.index === start) {
|
|
209
|
-
this.finalizeFail(ctx);
|
|
210
|
-
throw lastError;
|
|
211
|
-
}
|
|
212
348
|
}
|
|
213
349
|
}
|
|
350
|
+
this.finalizeFail(ctx);
|
|
351
|
+
throw lastError;
|
|
214
352
|
}
|
|
215
353
|
async doStream(options) {
|
|
216
|
-
this.
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
//
|
|
220
|
-
//
|
|
221
|
-
//
|
|
222
|
-
|
|
354
|
+
return this.doStreamWithCtx(options, this.startCall(), this.startIndex(), 0);
|
|
355
|
+
}
|
|
356
|
+
// The stream's failover recursion re-enters here with the SAME `ctx` and a
|
|
357
|
+
// threaded-through local cursor (`idx`/`tried`), so a mid-stream switch keeps
|
|
358
|
+
// appending to one CallRecord and bounds itself on the local `tried` count —
|
|
359
|
+
// never on shared instance state. `finalizeOk`/`finalizeFail` fire exactly
|
|
360
|
+
// once per outer request.
|
|
361
|
+
async doStreamWithCtx(options, ctx, startIdx, alreadyTried) {
|
|
223
362
|
const self = this;
|
|
224
|
-
const
|
|
363
|
+
const providers = this.opts.providers;
|
|
364
|
+
const n = providers.length;
|
|
225
365
|
let result;
|
|
226
366
|
let serving;
|
|
227
367
|
let servingStart;
|
|
368
|
+
let idx = startIdx;
|
|
369
|
+
let tried = alreadyTried;
|
|
228
370
|
for (; ; ) {
|
|
229
|
-
serving =
|
|
371
|
+
serving = providers[idx];
|
|
230
372
|
servingStart = Date.now();
|
|
231
373
|
try {
|
|
232
374
|
result = await serving.model.doStream(options);
|
|
@@ -237,17 +379,20 @@ var LcrFallbackModel = class {
|
|
|
237
379
|
this.finalizeFail(ctx);
|
|
238
380
|
throw error;
|
|
239
381
|
}
|
|
240
|
-
this.
|
|
382
|
+
this.emitError(error, serving.label);
|
|
241
383
|
this.recordFail(ctx, serving, servingStart, error);
|
|
242
|
-
|
|
243
|
-
if (
|
|
384
|
+
tried++;
|
|
385
|
+
if (tried >= n) {
|
|
244
386
|
this.finalizeFail(ctx);
|
|
245
387
|
throw error;
|
|
246
388
|
}
|
|
389
|
+
idx = (idx + 1) % n;
|
|
247
390
|
}
|
|
248
391
|
}
|
|
249
392
|
const servingProvider = serving;
|
|
250
393
|
const servingAttemptStart = servingStart;
|
|
394
|
+
const servingIdx = idx;
|
|
395
|
+
const triedBeforeServing = tried;
|
|
251
396
|
let usage;
|
|
252
397
|
let streamedAny = false;
|
|
253
398
|
const stream = new ReadableStream({
|
|
@@ -266,20 +411,26 @@ var LcrFallbackModel = class {
|
|
|
266
411
|
controller.enqueue(value);
|
|
267
412
|
if (value.type !== "stream-start") streamedAny = true;
|
|
268
413
|
}
|
|
414
|
+
self.settleSticky(servingIdx);
|
|
269
415
|
self.finalizeOk(ctx, servingProvider, servingAttemptStart, usage);
|
|
270
416
|
controller.close();
|
|
271
417
|
} catch (error) {
|
|
272
|
-
self.
|
|
418
|
+
self.emitError(error, servingProvider.label);
|
|
273
419
|
self.recordFail(ctx, servingProvider, servingAttemptStart, error);
|
|
274
420
|
if (!streamedAny) {
|
|
275
|
-
|
|
276
|
-
if (
|
|
421
|
+
const nextTried = triedBeforeServing + 1;
|
|
422
|
+
if (nextTried >= n) {
|
|
277
423
|
self.finalizeFail(ctx);
|
|
278
424
|
controller.error(error);
|
|
279
425
|
return;
|
|
280
426
|
}
|
|
281
427
|
try {
|
|
282
|
-
const next = await self.doStreamWithCtx(
|
|
428
|
+
const next = await self.doStreamWithCtx(
|
|
429
|
+
options,
|
|
430
|
+
ctx,
|
|
431
|
+
(servingIdx + 1) % n,
|
|
432
|
+
nextTried
|
|
433
|
+
);
|
|
283
434
|
const nextReader = next.stream.getReader();
|
|
284
435
|
try {
|
|
285
436
|
for (; ; ) {
|
|
@@ -388,6 +539,24 @@ function newMediaCallId() {
|
|
|
388
539
|
}
|
|
389
540
|
function createMediaLCR(config) {
|
|
390
541
|
const { registry, adapters, reference = DEFAULT_REFERENCE, onError, onCost, onCall } = config;
|
|
542
|
+
const safeError = (error, provider) => {
|
|
543
|
+
try {
|
|
544
|
+
onError?.(error, provider);
|
|
545
|
+
} catch {
|
|
546
|
+
}
|
|
547
|
+
};
|
|
548
|
+
const safeCost = (event) => {
|
|
549
|
+
try {
|
|
550
|
+
onCost?.(event);
|
|
551
|
+
} catch {
|
|
552
|
+
}
|
|
553
|
+
};
|
|
554
|
+
const safeCall = (record) => {
|
|
555
|
+
try {
|
|
556
|
+
onCall?.(record);
|
|
557
|
+
} catch {
|
|
558
|
+
}
|
|
559
|
+
};
|
|
391
560
|
return async function generate(modelId, input) {
|
|
392
561
|
const def = registry[modelId];
|
|
393
562
|
if (!def) {
|
|
@@ -398,7 +567,7 @@ function createMediaLCR(config) {
|
|
|
398
567
|
const startedAt = Date.now();
|
|
399
568
|
const attempts = [];
|
|
400
569
|
let lastErr;
|
|
401
|
-
const emitFail = () =>
|
|
570
|
+
const emitFail = () => safeCall({
|
|
402
571
|
id: newMediaCallId(),
|
|
403
572
|
model: modelId,
|
|
404
573
|
attempts,
|
|
@@ -420,8 +589,8 @@ function createMediaLCR(config) {
|
|
|
420
589
|
const estimated = result.costCents === void 0;
|
|
421
590
|
const costCents = estimated ? route.refCents * (result.units ?? 1) : result.costCents;
|
|
422
591
|
attempts.push({ provider: route.provider, ok: true, latencyMs: Date.now() - attemptStart });
|
|
423
|
-
|
|
424
|
-
|
|
592
|
+
safeCost({ modelId, provider: route.provider, costCents, estimated });
|
|
593
|
+
safeCall({
|
|
425
594
|
id: newMediaCallId(),
|
|
426
595
|
model: modelId,
|
|
427
596
|
attempts,
|
|
@@ -443,7 +612,7 @@ function createMediaLCR(config) {
|
|
|
443
612
|
latencyMs: Date.now() - attemptStart,
|
|
444
613
|
errorClass: classifyError(err)
|
|
445
614
|
});
|
|
446
|
-
|
|
615
|
+
safeError(err, route.provider);
|
|
447
616
|
if (!isRetryableError(err)) {
|
|
448
617
|
emitFail();
|
|
449
618
|
throw err;
|
|
@@ -734,84 +903,49 @@ var RunwareMediaError = class extends Error {
|
|
|
734
903
|
};
|
|
735
904
|
|
|
736
905
|
// src/adapters/fal-media.ts
|
|
737
|
-
var DEFAULT_BASE3 = "https://
|
|
738
|
-
function
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
const
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
if (
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
if (Array.isArray(data.videos)) {
|
|
750
|
-
for (const v of data.videos) pushUrl(v?.url, "video");
|
|
906
|
+
var DEFAULT_BASE3 = "https://fal.run";
|
|
907
|
+
function extractImageUrls2(body) {
|
|
908
|
+
const fromArray = (body.images ?? []).map((im) => im?.url).filter((u) => typeof u === "string" && u.length > 0);
|
|
909
|
+
if (fromArray.length > 0) return fromArray;
|
|
910
|
+
const single = body.image?.url;
|
|
911
|
+
return typeof single === "string" && single.length > 0 ? [single] : [];
|
|
912
|
+
}
|
|
913
|
+
function errorMessage2(body) {
|
|
914
|
+
if (typeof body.detail === "string") return body.detail;
|
|
915
|
+
if (Array.isArray(body.detail)) {
|
|
916
|
+
const msgs = body.detail.map((d) => d?.msg).filter(Boolean);
|
|
917
|
+
if (msgs.length > 0) return msgs.join("; ");
|
|
751
918
|
}
|
|
752
|
-
|
|
753
|
-
return out;
|
|
919
|
+
return body.error || body.message || "unknown";
|
|
754
920
|
}
|
|
755
921
|
function createFalMediaAdapter(config) {
|
|
756
|
-
const {
|
|
757
|
-
apiKey,
|
|
758
|
-
baseUrl = DEFAULT_BASE3,
|
|
759
|
-
pollIntervalMs = 3e3,
|
|
760
|
-
pollTimeoutMs = 3e5,
|
|
761
|
-
fetchImpl = fetch
|
|
762
|
-
} = config;
|
|
763
|
-
const headers = {
|
|
764
|
-
"content-type": "application/json",
|
|
765
|
-
authorization: `Key ${apiKey}`
|
|
766
|
-
};
|
|
922
|
+
const { apiKey, baseUrl = DEFAULT_BASE3, fetchImpl = fetch } = config;
|
|
767
923
|
return {
|
|
768
924
|
provider: "fal",
|
|
769
925
|
async run(req) {
|
|
770
|
-
const
|
|
926
|
+
const res = await fetchImpl(`${baseUrl}/${req.externalId}`, {
|
|
771
927
|
method: "POST",
|
|
772
|
-
headers
|
|
928
|
+
headers: {
|
|
929
|
+
"content-type": "application/json",
|
|
930
|
+
authorization: `Key ${apiKey}`,
|
|
931
|
+
accept: "application/json"
|
|
932
|
+
},
|
|
773
933
|
body: JSON.stringify(req.input)
|
|
774
934
|
});
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
const responseUrl = submit.response_url;
|
|
781
|
-
if (!statusUrl || !responseUrl) {
|
|
782
|
-
throw new Error(
|
|
783
|
-
`ai-lcr: fal submit for "${req.externalId}" returned no status/response URL (keys: ${Object.keys(
|
|
784
|
-
submit
|
|
785
|
-
).join(", ")})`
|
|
786
|
-
);
|
|
787
|
-
}
|
|
788
|
-
const deadline = Date.now() + pollTimeoutMs;
|
|
789
|
-
let completed = false;
|
|
790
|
-
while (Date.now() < deadline) {
|
|
791
|
-
const statusRes = await fetchImpl(statusUrl, { headers });
|
|
792
|
-
if (!statusRes.ok) {
|
|
793
|
-
throw new FalMediaError(statusRes.status, await safeText2(statusRes));
|
|
794
|
-
}
|
|
795
|
-
const status = String((await statusRes.json()).status ?? "");
|
|
796
|
-
if (status === "COMPLETED") {
|
|
797
|
-
completed = true;
|
|
798
|
-
break;
|
|
799
|
-
}
|
|
800
|
-
await sleep2(pollIntervalMs);
|
|
801
|
-
}
|
|
802
|
-
if (!completed) {
|
|
803
|
-
throw new Error(
|
|
804
|
-
`ai-lcr: fal job for "${req.externalId}" timed out after ${pollTimeoutMs}ms`
|
|
805
|
-
);
|
|
935
|
+
let body;
|
|
936
|
+
try {
|
|
937
|
+
body = await res.json();
|
|
938
|
+
} catch {
|
|
939
|
+
body = {};
|
|
806
940
|
}
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
throw new FalMediaError(resultRes.status, await safeText2(resultRes));
|
|
941
|
+
if (!res.ok) {
|
|
942
|
+
throw new FalMediaError(res.status, errorMessage2(body));
|
|
810
943
|
}
|
|
811
|
-
const
|
|
812
|
-
if (
|
|
813
|
-
throw new Error(`ai-lcr: fal returned no
|
|
944
|
+
const urls = extractImageUrls2(body);
|
|
945
|
+
if (urls.length === 0) {
|
|
946
|
+
throw new Error(`ai-lcr: fal returned no image URL for "${req.externalId}"`);
|
|
814
947
|
}
|
|
948
|
+
const outputs = urls.map((url) => ({ url, type: "image" }));
|
|
815
949
|
return { outputs, units: outputs.length };
|
|
816
950
|
}
|
|
817
951
|
};
|
|
@@ -824,16 +958,6 @@ var FalMediaError = class extends Error {
|
|
|
824
958
|
}
|
|
825
959
|
status;
|
|
826
960
|
};
|
|
827
|
-
function sleep2(ms) {
|
|
828
|
-
return new Promise((r) => setTimeout(r, ms));
|
|
829
|
-
}
|
|
830
|
-
async function safeText2(res) {
|
|
831
|
-
try {
|
|
832
|
-
return await res.text();
|
|
833
|
-
} catch {
|
|
834
|
-
return "<no body>";
|
|
835
|
-
}
|
|
836
|
-
}
|
|
837
961
|
|
|
838
962
|
// src/index.ts
|
|
839
963
|
function isLanguageModel(entry) {
|
|
@@ -878,10 +1002,10 @@ function createLCR(config) {
|
|
|
878
1002
|
// Annotate the CommonJS export names for ESM import in node:
|
|
879
1003
|
0 && (module.exports = {
|
|
880
1004
|
DEFAULT_REFERENCE,
|
|
881
|
-
FalMediaError,
|
|
882
1005
|
MEDIA_PRICING,
|
|
883
1006
|
cheapestRoute,
|
|
884
1007
|
classifyError,
|
|
1008
|
+
classifyErrorKind,
|
|
885
1009
|
comparePrices,
|
|
886
1010
|
createFalMediaAdapter,
|
|
887
1011
|
createKunavoMediaAdapter,
|