@jini-ai/integrations 0.3.2 → 0.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/media-providers/dispatch/async-operation-store.d.ts +166 -0
- package/dist/media-providers/dispatch/async-operation-store.d.ts.map +1 -0
- package/dist/media-providers/dispatch/async-operation-store.js +291 -0
- package/dist/media-providers/dispatch/async-operation-store.js.map +1 -0
- package/dist/media-providers/dispatch/index.d.ts +8 -0
- package/dist/media-providers/dispatch/index.d.ts.map +1 -1
- package/dist/media-providers/dispatch/index.js +7 -0
- package/dist/media-providers/dispatch/index.js.map +1 -1
- package/dist/media-providers/dispatch/operation-runtime.d.ts +101 -0
- package/dist/media-providers/dispatch/operation-runtime.d.ts.map +1 -0
- package/dist/media-providers/dispatch/operation-runtime.js +311 -0
- package/dist/media-providers/dispatch/operation-runtime.js.map +1 -0
- package/dist/media-providers/dispatch/polling-adapter.d.ts +103 -0
- package/dist/media-providers/dispatch/polling-adapter.d.ts.map +1 -0
- package/dist/media-providers/dispatch/polling-adapter.js +30 -0
- package/dist/media-providers/dispatch/polling-adapter.js.map +1 -0
- package/dist/media-providers/dispatch/providers/imagerouter-video-async.d.ts +21 -0
- package/dist/media-providers/dispatch/providers/imagerouter-video-async.d.ts.map +1 -0
- package/dist/media-providers/dispatch/providers/imagerouter-video-async.js +141 -0
- package/dist/media-providers/dispatch/providers/imagerouter-video-async.js.map +1 -0
- package/package.json +3 -3
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The submit-then-poll runtime: the piece that turns a `PollingVendorAdapter` plus an
|
|
3
|
+
* `AsyncOperationStore` into an operation that survives a restart.
|
|
4
|
+
*
|
|
5
|
+
* ## Persist first, then race
|
|
6
|
+
*
|
|
7
|
+
* `startOperation` writes the operation row **before** it issues any HTTP call, then races the
|
|
8
|
+
* in-flight submit against a short interactive grace window. If the vendor answers in time the
|
|
9
|
+
* caller gets `{done: true, result}` in the same round trip — indistinguishable from the
|
|
10
|
+
* synchronous tier. If it does not, the caller gets `{done: false, operationId}` and the *same*
|
|
11
|
+
* in-flight request keeps running and writes its own outcome into the row.
|
|
12
|
+
*
|
|
13
|
+
* That ordering is what makes crash safety fall out for free rather than being bolted on: there is
|
|
14
|
+
* no window in which work exists only inside a process. It is also why this is a race and not a
|
|
15
|
+
* fork — there is exactly one submit code path, and `expectedLatencyClass` only decides how long
|
|
16
|
+
* the caller is willing to wait on it, never what happens.
|
|
17
|
+
*
|
|
18
|
+
* Human callers and agents get the identical contract: a result, or an id to come back with.
|
|
19
|
+
*
|
|
20
|
+
* ## What is deliberately NOT here
|
|
21
|
+
*
|
|
22
|
+
* No scheduler. `pollDueOperations` is a single leased tick a host drives from whatever timer or
|
|
23
|
+
* queue it already runs — this package performs no I/O of its own beyond the vendor calls it is
|
|
24
|
+
* handed a `fetchImpl` for, a standing invariant across `@jini-ai/integrations/media-providers`.
|
|
25
|
+
*/
|
|
26
|
+
import { FETCH_TIMEOUT_MS, fetchWithTimeout } from '@jini-ai/platform';
|
|
27
|
+
/** Interactive budget for the inline attempt. Deliberately NOT `FETCH_TIMEOUT_MS.GENERATE` (10min) — that is the backstop for the *request*, this is the ceiling on making a human wait. */
|
|
28
|
+
export const DEFAULT_GRACE_MS = 4_000;
|
|
29
|
+
export const DEFAULT_MAX_ATTEMPTS = 60;
|
|
30
|
+
export const DEFAULT_DEADLINE_MS = 15 * 60_000;
|
|
31
|
+
export const DEFAULT_POLL_INTERVAL_MS = 5_000;
|
|
32
|
+
/**
|
|
33
|
+
* Backoff between retries of the write that records a vendor job handle after a successful
|
|
34
|
+
* submit. A handle that made it past the vendor round trip must not be dropped just because the
|
|
35
|
+
* very next write is transiently unlucky — see `startOperation`'s post-submit persistence.
|
|
36
|
+
*/
|
|
37
|
+
export const DEFAULT_PERSIST_RETRY_DELAYS_MS = [50, 150, 400];
|
|
38
|
+
/** The lease identity used by boot recovery's claim-then-release read, distinct from any real worker. */
|
|
39
|
+
const RECOVERY_LEASE_OWNER = '__recovery__';
|
|
40
|
+
function resultToPayload(result) {
|
|
41
|
+
return {
|
|
42
|
+
bytesBase64: result.bytes.toString('base64'),
|
|
43
|
+
providerNote: result.providerNote,
|
|
44
|
+
...(result.suggestedExt !== undefined ? { suggestedExt: result.suggestedExt } : {}),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function toOperationError(error) {
|
|
48
|
+
return { message: error instanceof Error ? error.message : String(error) };
|
|
49
|
+
}
|
|
50
|
+
async function performSigned(deps, request, timeoutMs) {
|
|
51
|
+
const signed = await deps.signer(request);
|
|
52
|
+
const doFetch = deps.fetchImpl;
|
|
53
|
+
if (doFetch)
|
|
54
|
+
return doFetch(signed.url, signed.init);
|
|
55
|
+
return fetchWithTimeout(signed.url, signed.init, { timeoutMs });
|
|
56
|
+
}
|
|
57
|
+
/** Issues the submit call and parses it. Isolated from persistence so callers can tell "nothing was obtained from the vendor" apart from "a handle was obtained but recording it failed" — only the former is safe to treat as terminal. */
|
|
58
|
+
async function submitAndParse(deps, params) {
|
|
59
|
+
try {
|
|
60
|
+
const request = params.adapter.buildSubmitRequest(params.ctx);
|
|
61
|
+
const resp = await performSigned(deps, request, FETCH_TIMEOUT_MS.GENERATE);
|
|
62
|
+
const outcome = await params.adapter.parseSubmitResponse(resp, params.ctx, request);
|
|
63
|
+
return { ok: true, outcome };
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
return { ok: false, error };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Applies a store patch, retrying on failure per `retryDelaysMs`. Never throws — callers past
|
|
71
|
+
* this point already hold a real vendor job handle or a finished result, so the write is retried
|
|
72
|
+
* rather than let a transient failure fall through to a `'failed'` label that would erase it.
|
|
73
|
+
*
|
|
74
|
+
* @returns Whether the write eventually succeeded. On `false`, the row is left exactly as it was
|
|
75
|
+
* before this call — for the post-submit write, that means `'submitted'`, which
|
|
76
|
+
* `recoverAfterRestart`'s crash-gap handling already knows how to reconcile safely, rather than
|
|
77
|
+
* a `'failed'` row that recovery ignores as terminal.
|
|
78
|
+
* @complexity O(retryDelaysMs.length) store round trips in the worst case.
|
|
79
|
+
*/
|
|
80
|
+
async function persistWithRetry(store, operationId, patch, retryDelaysMs) {
|
|
81
|
+
for (let attempt = 0; attempt <= retryDelaysMs.length; attempt += 1) {
|
|
82
|
+
try {
|
|
83
|
+
await store.update(operationId, patch);
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
if (attempt === retryDelaysMs.length)
|
|
88
|
+
return false;
|
|
89
|
+
await new Promise((resolve) => setTimeout(resolve, retryDelaysMs[attempt]));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Persists the operation row, issues the submit, and races it against the grace window.
|
|
96
|
+
*
|
|
97
|
+
* @returns `{done: true, result}` when the vendor finished inside the grace window, else
|
|
98
|
+
* `{done: false, operationId}` — the row is already durable in both cases.
|
|
99
|
+
* @complexity O(1) plus one vendor round trip.
|
|
100
|
+
*/
|
|
101
|
+
export async function startOperation(deps, params) {
|
|
102
|
+
const now = deps.now ?? Date.now;
|
|
103
|
+
const newId = deps.newId ?? (() => `op_${Math.random().toString(36).slice(2, 10)}_${now()}`);
|
|
104
|
+
const startedAt = now();
|
|
105
|
+
const operationId = newId();
|
|
106
|
+
// Before any HTTP call — this ordering IS the crash-safety mechanism.
|
|
107
|
+
await deps.store.create({
|
|
108
|
+
id: operationId,
|
|
109
|
+
providerId: params.providerId,
|
|
110
|
+
routeKey: params.routeKey,
|
|
111
|
+
ownerRef: params.ownerRef,
|
|
112
|
+
maxAttempts: params.maxAttempts ?? DEFAULT_MAX_ATTEMPTS,
|
|
113
|
+
deadlineAt: startedAt + (params.deadlineMs ?? DEFAULT_DEADLINE_MS),
|
|
114
|
+
nextPollAt: startedAt,
|
|
115
|
+
});
|
|
116
|
+
const settle = (async () => {
|
|
117
|
+
const submitted = await submitAndParse(deps, params);
|
|
118
|
+
if (!submitted.ok) {
|
|
119
|
+
// Nothing was obtained from the vendor — no job handle exists to protect, so a single
|
|
120
|
+
// terminal write is safe. Best-effort even here: a write failure at this point must not
|
|
121
|
+
// reject `settle` (see the `finally` below) and leaves the row `'submitted'`, which is at
|
|
122
|
+
// least as safe as `'failed'` would have been.
|
|
123
|
+
await deps.store.update(operationId, { status: 'failed', error: toOperationError(submitted.error) }).catch(() => undefined);
|
|
124
|
+
return { done: false, operationId };
|
|
125
|
+
}
|
|
126
|
+
const { outcome } = submitted;
|
|
127
|
+
// Past this point a vendor job handle exists (or the job already finished) — losing either
|
|
128
|
+
// to a transient store write is strictly worse than the failure it would otherwise record,
|
|
129
|
+
// so the write is retried before anything is allowed to fall back to a `'failed'` label.
|
|
130
|
+
const patch = outcome.kind === 'complete'
|
|
131
|
+
? { status: 'succeeded', result: resultToPayload(outcome.result) }
|
|
132
|
+
: { status: 'polling', state: outcome.state, nextPollAt: now() + (outcome.retryAfterMs ?? DEFAULT_POLL_INTERVAL_MS) };
|
|
133
|
+
await persistWithRetry(deps.store, operationId, patch, deps.persistRetryDelaysMs ?? DEFAULT_PERSIST_RETRY_DELAYS_MS);
|
|
134
|
+
// Even on exhausted retries there is nothing safer left to do here: the row stays exactly as
|
|
135
|
+
// `create()` left it (`'submitted'`), and `recoverAfterRestart`'s crash-gap handling exists
|
|
136
|
+
// precisely for a row whose outcome could not be confirmed durable.
|
|
137
|
+
if (outcome.kind === 'complete')
|
|
138
|
+
return { done: true, operationId, result: outcome.result };
|
|
139
|
+
return { done: false, operationId };
|
|
140
|
+
})();
|
|
141
|
+
const graceMs = params.graceMs ?? DEFAULT_GRACE_MS;
|
|
142
|
+
// A `'slow'` vendor is not worth making anyone wait on: the race is still the same single code
|
|
143
|
+
// path, its window is just zero.
|
|
144
|
+
const effectiveGraceMs = params.adapter.expectedLatencyClass === 'slow' ? 0 : graceMs;
|
|
145
|
+
let graceTimer;
|
|
146
|
+
const grace = new Promise((resolve) => {
|
|
147
|
+
graceTimer = setTimeout(() => resolve({ done: false, operationId }), effectiveGraceMs);
|
|
148
|
+
});
|
|
149
|
+
try {
|
|
150
|
+
// The losing side is never cancelled — an abandoned submit keeps running and settles the row
|
|
151
|
+
// the caller already holds an id for.
|
|
152
|
+
return await Promise.race([settle, grace]);
|
|
153
|
+
}
|
|
154
|
+
finally {
|
|
155
|
+
if (graceTimer)
|
|
156
|
+
clearTimeout(graceTimer);
|
|
157
|
+
// The caller may have walked away; the row still gets written, but an unobserved rejection
|
|
158
|
+
// must not take the process down.
|
|
159
|
+
void settle.catch(() => undefined);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* One leased worker tick: claims every due operation, advances each by exactly one vendor poll,
|
|
164
|
+
* and releases the lease. Safe to run concurrently on several workers — `claimDue` is the
|
|
165
|
+
* exclusion mechanism.
|
|
166
|
+
*
|
|
167
|
+
* Both bounds are enforced here, and in this order: the absolute deadline is checked *before* any
|
|
168
|
+
* vendor call (a blown deadline must not cost another request), the attempt cap after it.
|
|
169
|
+
*
|
|
170
|
+
* @complexity O(k) vendor round trips for k claimed operations.
|
|
171
|
+
*/
|
|
172
|
+
export async function pollDueOperations(deps, params) {
|
|
173
|
+
const now = deps.now ?? Date.now;
|
|
174
|
+
const claimed = await deps.store.claimDue({
|
|
175
|
+
now: now(),
|
|
176
|
+
leaseOwner: params.leaseOwner,
|
|
177
|
+
leaseMs: params.leaseMs,
|
|
178
|
+
...(params.limit !== undefined ? { limit: params.limit } : {}),
|
|
179
|
+
});
|
|
180
|
+
let completed = 0;
|
|
181
|
+
let failed = 0;
|
|
182
|
+
let pending = 0;
|
|
183
|
+
let unknown = 0;
|
|
184
|
+
for (const row of claimed) {
|
|
185
|
+
try {
|
|
186
|
+
const at = now();
|
|
187
|
+
if (row.deadlineAt <= at) {
|
|
188
|
+
await deps.store.update(row.id, {
|
|
189
|
+
status: 'unknown',
|
|
190
|
+
error: {
|
|
191
|
+
message: 'async operation passed its absolute deadline — vendor-side effect is undetermined',
|
|
192
|
+
code: 'DEADLINE_EXPIRED',
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
unknown += 1;
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
if (row.attempts >= row.maxAttempts) {
|
|
199
|
+
await deps.store.update(row.id, {
|
|
200
|
+
status: 'failed',
|
|
201
|
+
error: {
|
|
202
|
+
message: `async operation exhausted its ${row.maxAttempts} poll attempts without a terminal answer`,
|
|
203
|
+
code: 'ATTEMPTS_EXHAUSTED',
|
|
204
|
+
},
|
|
205
|
+
});
|
|
206
|
+
failed += 1;
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
const adapter = params.adapters(row.providerId, row.routeKey);
|
|
210
|
+
if (!adapter) {
|
|
211
|
+
await deps.store.update(row.id, {
|
|
212
|
+
status: 'failed',
|
|
213
|
+
error: { message: `no polling adapter registered for "${row.providerId}" / "${row.routeKey}"`, code: 'NO_ADAPTER' },
|
|
214
|
+
});
|
|
215
|
+
failed += 1;
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
const ctx = params.resolveContext(row);
|
|
219
|
+
const request = adapter.buildPollRequest(row.state ?? {}, ctx);
|
|
220
|
+
// Credentials are resolved here, on this tick — never read from the row.
|
|
221
|
+
const resp = await performSigned(deps, request, FETCH_TIMEOUT_MS.QUICK);
|
|
222
|
+
const outcome = await adapter.parsePollResponse(resp, ctx, row.state ?? {});
|
|
223
|
+
if (outcome.kind === 'complete') {
|
|
224
|
+
await deps.store.update(row.id, {
|
|
225
|
+
status: 'succeeded',
|
|
226
|
+
attempts: row.attempts + 1,
|
|
227
|
+
result: resultToPayload(outcome.result),
|
|
228
|
+
});
|
|
229
|
+
completed += 1;
|
|
230
|
+
}
|
|
231
|
+
else if (outcome.kind === 'failed') {
|
|
232
|
+
await deps.store.update(row.id, {
|
|
233
|
+
status: 'failed',
|
|
234
|
+
attempts: row.attempts + 1,
|
|
235
|
+
error: { message: outcome.message, ...(outcome.code !== undefined ? { code: outcome.code } : {}) },
|
|
236
|
+
});
|
|
237
|
+
failed += 1;
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
await deps.store.update(row.id, {
|
|
241
|
+
status: 'polling',
|
|
242
|
+
attempts: row.attempts + 1,
|
|
243
|
+
nextPollAt: at + (outcome.retryAfterMs ?? DEFAULT_POLL_INTERVAL_MS),
|
|
244
|
+
});
|
|
245
|
+
pending += 1;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
catch (error) {
|
|
249
|
+
// A transport failure is not a vendor verdict: keep the row pollable and let the attempt cap
|
|
250
|
+
// or the deadline end it, rather than reporting a definitive failure we cannot support.
|
|
251
|
+
await deps.store.update(row.id, {
|
|
252
|
+
status: 'polling',
|
|
253
|
+
attempts: row.attempts + 1,
|
|
254
|
+
nextPollAt: now() + DEFAULT_POLL_INTERVAL_MS,
|
|
255
|
+
error: toOperationError(error),
|
|
256
|
+
});
|
|
257
|
+
pending += 1;
|
|
258
|
+
}
|
|
259
|
+
finally {
|
|
260
|
+
await deps.store.releaseLease(row.id, params.leaseOwner);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return { claimed: claimed.length, completed, failed, pending, unknown };
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Boot-time recovery. Releases dead leases and expires blown deadlines via the store, then handles
|
|
267
|
+
* the one case the store cannot decide alone: a row still at `submitted`, meaning the process died
|
|
268
|
+
* between persisting the row and learning whether the vendor accepted the submit.
|
|
269
|
+
*
|
|
270
|
+
* Retry safety is gated on idempotency, not assumed. An adapter that has not declared
|
|
271
|
+
* `submitIsIdempotent` sends that row to `unknown` for reconciliation — media generation is billed
|
|
272
|
+
* per call, so a silent re-issue can double a real charge.
|
|
273
|
+
*
|
|
274
|
+
* @complexity O(n) in stored operations.
|
|
275
|
+
*/
|
|
276
|
+
export async function recoverAfterRestart(deps, params) {
|
|
277
|
+
const at = params.now ?? (deps.now ?? Date.now)();
|
|
278
|
+
const reconciled = await deps.store.reconcileOnBoot({ now: at });
|
|
279
|
+
const resubmittable = [];
|
|
280
|
+
let unknownCrashGap = 0;
|
|
281
|
+
for (const row of await collectSubmitted(deps.store)) {
|
|
282
|
+
const adapter = params.adapters(row.providerId, row.routeKey);
|
|
283
|
+
if (adapter?.submitIsIdempotent === true) {
|
|
284
|
+
resubmittable.push(row.id);
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
await deps.store.update(row.id, {
|
|
288
|
+
status: 'unknown',
|
|
289
|
+
error: {
|
|
290
|
+
message: 'process died before the vendor submit was confirmed, and this adapter does not declare the submit idempotent — reconcile manually rather than risk a duplicate charge',
|
|
291
|
+
code: 'CRASH_GAP_NOT_IDEMPOTENT',
|
|
292
|
+
},
|
|
293
|
+
});
|
|
294
|
+
unknownCrashGap += 1;
|
|
295
|
+
}
|
|
296
|
+
return { ...reconciled, unknownCrashGap, resubmittable };
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* The `submitted` rows a restart orphaned. Uses the store's own claim-free read path so recovery
|
|
300
|
+
* never competes with a live worker for a lease.
|
|
301
|
+
*/
|
|
302
|
+
async function collectSubmitted(store) {
|
|
303
|
+
const seen = new Map();
|
|
304
|
+
for (const row of await store.claimDue({ now: Number.MAX_SAFE_INTEGER, leaseOwner: RECOVERY_LEASE_OWNER, leaseMs: 0, limit: Number.MAX_SAFE_INTEGER })) {
|
|
305
|
+
if (row.status === 'submitted')
|
|
306
|
+
seen.set(row.id, row);
|
|
307
|
+
await store.releaseLease(row.id, RECOVERY_LEASE_OWNER);
|
|
308
|
+
}
|
|
309
|
+
return [...seen.values()];
|
|
310
|
+
}
|
|
311
|
+
//# sourceMappingURL=operation-runtime.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"operation-runtime.js","sourceRoot":"","sources":["../../../src/media-providers/dispatch/operation-runtime.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `PollingVendorAdapter` — the submit-then-poll tier, added alongside (never replacing)
|
|
3
|
+
* `vendor-adapter.ts`'s `VendorAdapter`.
|
|
4
|
+
*
|
|
5
|
+
* This is purely additive. `VendorAdapter` is proven across all 18 live registrations and is
|
|
6
|
+
* untouched: a vendor that finishes inside one request/response keeps using it. This interface is
|
|
7
|
+
* for the other shape — a vendor that hands back a job handle and makes you come back for the
|
|
8
|
+
* result — which `dispatchVendorRequest`'s fixed `buildRequest -> one fetch -> parseResponse`
|
|
9
|
+
* cannot express at all.
|
|
10
|
+
*
|
|
11
|
+
* ## The credential seam
|
|
12
|
+
*
|
|
13
|
+
* The deliberate difference from `VendorAdapter`: **these builders never receive credentials.**
|
|
14
|
+
* `VendorAdapter.buildRequest(ctx, credentials)` hands the adapter a `ProviderCredentials` and the
|
|
15
|
+
* adapter writes its own `authorization` header. That is fine for one in-process round trip, but a
|
|
16
|
+
* polled operation outlives the request that started it, so a secret reachable from the adapter is
|
|
17
|
+
* a secret that wants to be persisted next to the job handle. Here the adapter builds an
|
|
18
|
+
* *unsigned* request and a `RequestSigner` attaches auth beneath it, re-resolved on every tick —
|
|
19
|
+
* which is also what makes a token that expires mid-poll a non-event.
|
|
20
|
+
*/
|
|
21
|
+
import type { MediaGenerationRequestInit, ProviderCredentials, RenderContext, RenderResult } from './types.js';
|
|
22
|
+
/**
|
|
23
|
+
* Whether this vendor is expected to answer within an interactive grace window. It is a property
|
|
24
|
+
* of the *vendor*, declared once per adapter — never a property of the caller, which is what keeps
|
|
25
|
+
* the human UI and an agent on one identical contract.
|
|
26
|
+
*/
|
|
27
|
+
export type ExpectedLatencyClass = 'fast' | 'slow';
|
|
28
|
+
/** A vendor request with no auth applied. `init.headers` carries content negotiation only. */
|
|
29
|
+
export interface UnsignedVendorRequest<Meta = undefined> {
|
|
30
|
+
readonly url: string;
|
|
31
|
+
readonly init: RequestInit;
|
|
32
|
+
readonly meta: Meta;
|
|
33
|
+
}
|
|
34
|
+
/** What a submit response turned out to be: the finished artifact, or a handle to come back for. */
|
|
35
|
+
export type SubmitOutcome = {
|
|
36
|
+
readonly kind: 'complete';
|
|
37
|
+
readonly result: RenderResult;
|
|
38
|
+
} | {
|
|
39
|
+
readonly kind: 'pending';
|
|
40
|
+
readonly state: Readonly<Record<string, unknown>>;
|
|
41
|
+
readonly retryAfterMs?: number;
|
|
42
|
+
};
|
|
43
|
+
export type PollOutcome = {
|
|
44
|
+
readonly kind: 'complete';
|
|
45
|
+
readonly result: RenderResult;
|
|
46
|
+
} | {
|
|
47
|
+
readonly kind: 'pending';
|
|
48
|
+
readonly retryAfterMs?: number;
|
|
49
|
+
} | {
|
|
50
|
+
readonly kind: 'failed';
|
|
51
|
+
readonly message: string;
|
|
52
|
+
readonly code?: string;
|
|
53
|
+
};
|
|
54
|
+
export interface PollingVendorAdapter<Meta = undefined> {
|
|
55
|
+
readonly expectedLatencyClass: ExpectedLatencyClass;
|
|
56
|
+
/**
|
|
57
|
+
* Whether re-issuing the submit request is safe when a crash left us unable to tell whether the
|
|
58
|
+
* first one reached the vendor. Defaults to `false`, which routes that row to `unknown` for
|
|
59
|
+
* reconciliation instead. There is no unqualified retry-once rule: media generation costs real
|
|
60
|
+
* money per call, so a retry must be *proven* safe, not assumed.
|
|
61
|
+
*/
|
|
62
|
+
readonly submitIsIdempotent?: boolean;
|
|
63
|
+
buildSubmitRequest(ctx: RenderContext): UnsignedVendorRequest<Meta>;
|
|
64
|
+
parseSubmitResponse(resp: Response, ctx: RenderContext, request: UnsignedVendorRequest<Meta>): Promise<SubmitOutcome>;
|
|
65
|
+
buildPollRequest(state: Readonly<Record<string, unknown>>, ctx: RenderContext): UnsignedVendorRequest<Meta>;
|
|
66
|
+
parsePollResponse(resp: Response, ctx: RenderContext, state: Readonly<Record<string, unknown>>): Promise<PollOutcome>;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* A `PollingVendorAdapter` with its `Meta` erased, for heterogeneous lookup by
|
|
70
|
+
* `(providerId, routeKey)`. `Meta` never crosses this boundary — it only flows from one adapter's
|
|
71
|
+
* own `build*` to that same adapter's `parse*` — so erasing it here is sound, unlike the `never`
|
|
72
|
+
* that `VendorAdapterRegistry` uses (which is only sound because every caller re-casts).
|
|
73
|
+
*/
|
|
74
|
+
export type AnyPollingVendorAdapter = PollingVendorAdapter<any>;
|
|
75
|
+
/** A signed request, ready to hand to `fetch`. */
|
|
76
|
+
export interface SignedVendorRequest {
|
|
77
|
+
readonly url: string;
|
|
78
|
+
readonly init: RequestInit;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* The broker seam: attaches auth to an unsigned request. Async because a real broker resolves from
|
|
82
|
+
* a vault / refreshes an OAuth token, and is called once per tick rather than once per operation.
|
|
83
|
+
*/
|
|
84
|
+
export type RequestSigner = (request: UnsignedVendorRequest<unknown>) => Promise<SignedVendorRequest> | SignedVendorRequest;
|
|
85
|
+
/**
|
|
86
|
+
* Reference signer for the `Authorization: Bearer <key>` scheme every vendor in this package uses.
|
|
87
|
+
*
|
|
88
|
+
* `resolve` is invoked per call, never cached here — caching a resolved key inside the signer would
|
|
89
|
+
* reintroduce exactly the mid-stream-expiry problem this seam exists to remove.
|
|
90
|
+
*
|
|
91
|
+
* @throws When `resolve` yields no `apiKey`, using `missingCredentialMessage` — the failure surfaces
|
|
92
|
+
* at signing time rather than as an opaque vendor 401.
|
|
93
|
+
* @complexity O(1) plus whatever `resolve` costs.
|
|
94
|
+
*/
|
|
95
|
+
export declare function createBearerSigner(required: {
|
|
96
|
+
resolve: () => Promise<ProviderCredentials> | ProviderCredentials;
|
|
97
|
+
missingCredentialMessage: string;
|
|
98
|
+
}): RequestSigner;
|
|
99
|
+
/** Carries a caller-supplied `dispatcher` onto an unsigned request, matching `withRequestInit`'s role on the sync tier. */
|
|
100
|
+
export declare function withUnsignedRequestInit(ctx: {
|
|
101
|
+
readonly requestInit: MediaGenerationRequestInit;
|
|
102
|
+
}, init: RequestInit): RequestInit;
|
|
103
|
+
//# sourceMappingURL=polling-adapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"polling-adapter.d.ts","sourceRoot":"","sources":["../../../src/media-providers/dispatch/polling-adapter.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reference signer for the `Authorization: Bearer <key>` scheme every vendor in this package uses.
|
|
3
|
+
*
|
|
4
|
+
* `resolve` is invoked per call, never cached here — caching a resolved key inside the signer would
|
|
5
|
+
* reintroduce exactly the mid-stream-expiry problem this seam exists to remove.
|
|
6
|
+
*
|
|
7
|
+
* @throws When `resolve` yields no `apiKey`, using `missingCredentialMessage` — the failure surfaces
|
|
8
|
+
* at signing time rather than as an opaque vendor 401.
|
|
9
|
+
* @complexity O(1) plus whatever `resolve` costs.
|
|
10
|
+
*/
|
|
11
|
+
export function createBearerSigner(required) {
|
|
12
|
+
return async (request) => {
|
|
13
|
+
const credentials = await required.resolve();
|
|
14
|
+
if (!credentials.apiKey) {
|
|
15
|
+
throw new Error(required.missingCredentialMessage);
|
|
16
|
+
}
|
|
17
|
+
return {
|
|
18
|
+
url: request.url,
|
|
19
|
+
init: {
|
|
20
|
+
...request.init,
|
|
21
|
+
headers: { ...request.init.headers, authorization: `Bearer ${credentials.apiKey}` },
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/** Carries a caller-supplied `dispatcher` onto an unsigned request, matching `withRequestInit`'s role on the sync tier. */
|
|
27
|
+
export function withUnsignedRequestInit(ctx, init) {
|
|
28
|
+
return { ...init, ...(ctx.requestInit.dispatcher ? { dispatcher: ctx.requestInit.dispatcher } : {}) };
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=polling-adapter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"polling-adapter.js","sourceRoot":"","sources":["../../../src/media-providers/dispatch/polling-adapter.ts"],"names":[],"mappings":"AAkFA;;;;;;;;;GASG;AACH,MAAM,UAAU,kBAAkB,CAChC,QAAiH;IAEjH,OAAO,KAAK,EAAE,OAAO,EAAE,EAAE;QACvB,MAAM,WAAW,GAAG,MAAM,QAAQ,CAAC,OAAO,EAAE,CAAC;QAC7C,IAAI,CAAC,WAAW,CAAC,MAAM,EAAE,CAAC;YACxB,MAAM,IAAI,KAAK,CAAC,QAAQ,CAAC,wBAAwB,CAAC,CAAC;QACrD,CAAC;QACD,OAAO;YACL,GAAG,EAAE,OAAO,CAAC,GAAG;YAChB,IAAI,EAAE;gBACJ,GAAG,OAAO,CAAC,IAAI;gBACf,OAAO,EAAE,EAAE,GAAI,OAAO,CAAC,IAAI,CAAC,OAA8C,EAAE,aAAa,EAAE,UAAU,WAAW,CAAC,MAAM,EAAE,EAAE;aAC5H;SACF,CAAC;IACJ,CAAC,CAAC;AACJ,CAAC;AAED,2HAA2H;AAC3H,MAAM,UAAU,uBAAuB,CAAC,GAAyD,EAAE,IAAiB;IAClH,OAAO,EAAE,GAAG,IAAI,EAAE,GAAG,CAAC,GAAG,CAAC,WAAW,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,GAAG,CAAC,WAAW,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC;AACxG,CAAC"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { PollingVendorAdapter } from '../polling-adapter.js';
|
|
2
|
+
/** Non-secret endpoint configuration. Sourceable from a config file or a DB row — never the API key. */
|
|
3
|
+
export interface ImageRouterVideoConfig {
|
|
4
|
+
readonly baseUrl?: string;
|
|
5
|
+
/** Overrides the catalog-derived `ctx.wireModel` for the wire request. */
|
|
6
|
+
readonly wireModel?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface ImageRouterVideoMeta {
|
|
9
|
+
readonly wireModel: string;
|
|
10
|
+
readonly size: string;
|
|
11
|
+
readonly seconds: number | 'auto';
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Builds the ImageRouter video polling adapter for a given endpoint configuration.
|
|
15
|
+
*
|
|
16
|
+
* @param config Non-secret endpoint overrides; defaults to ImageRouter's public base URL and the
|
|
17
|
+
* context's own `wireModel`.
|
|
18
|
+
* @complexity O(1) per built request; one vendor round trip per parse.
|
|
19
|
+
*/
|
|
20
|
+
export declare function createImageRouterVideoPollingAdapter(config?: ImageRouterVideoConfig): PollingVendorAdapter<ImageRouterVideoMeta>;
|
|
21
|
+
//# sourceMappingURL=imagerouter-video-async.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"imagerouter-video-async.d.ts","sourceRoot":"","sources":["../../../../src/media-providers/dispatch/providers/imagerouter-video-async.ts"],"names":[],"mappings":"AAkCA,OAAO,KAAK,EAAe,oBAAoB,EAAwC,MAAM,uBAAuB,CAAC;AAKrH,wGAAwG;AACxG,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,0EAA0E;IAC1E,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC;CACnC;AAgBD;;;;;;GAMG;AACH,wBAAgB,oCAAoC,CAClD,MAAM,GAAE,sBAA2B,GAClC,oBAAoB,CAAC,oBAAoB,CAAC,CAqG5C"}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ImageRouter video on the submit-then-poll tier.
|
|
3
|
+
*
|
|
4
|
+
* ## Why this vendor
|
|
5
|
+
*
|
|
6
|
+
* `providers/imagerouter.ts`'s video adapter is `buildRequest -> one fetch -> parseResponse` with
|
|
7
|
+
* no poll loop at all: it holds a socket open for the entire generation, bounded only by the
|
|
8
|
+
* blanket `FETCH_TIMEOUT_MS.GENERATE` (10 minute) backstop, and a restart loses the work with no
|
|
9
|
+
* way to ask the vendor what happened. That sync adapter is left registered and untouched — this
|
|
10
|
+
* is an additive second registration, not a replacement.
|
|
11
|
+
*
|
|
12
|
+
* ## Both response shapes, deliberately
|
|
13
|
+
*
|
|
14
|
+
* `parseSubmitResponse` accepts either shape:
|
|
15
|
+
*
|
|
16
|
+
* - `{ data: [{ b64_json | url }] }` — the artifact arrived on the submit itself. This is exactly
|
|
17
|
+
* what the existing sync adapter expects, so today's behaviour is preserved bit for bit and the
|
|
18
|
+
* operation completes inside the grace window.
|
|
19
|
+
* - `{ id, status }` — an OpenAI-videos-style job handle, so the operation moves to `polling`.
|
|
20
|
+
*
|
|
21
|
+
* Handling both is not hedging: **which one ImageRouter actually returns for video is unverified
|
|
22
|
+
* against the live vendor** (see this file's entry in the handoff). Implementing only the job-handle
|
|
23
|
+
* shape would have been a guess that silently broke the working path; implementing both is correct
|
|
24
|
+
* under either answer and costs one `if`.
|
|
25
|
+
*
|
|
26
|
+
* ## Config is data
|
|
27
|
+
*
|
|
28
|
+
* The endpoint config is a plain object argument, not baked into the module, so the same adapter
|
|
29
|
+
* can later be instantiated from a database row to make a vendor runtime-addable with no redesign.
|
|
30
|
+
* Note what is NOT in it: the API key. Endpoint routing is config; the secret is the signer's.
|
|
31
|
+
*/
|
|
32
|
+
import { bytesFromOpenAICompatibleData, parseOpenAICompatibleJson } from '../openai-compatible.js';
|
|
33
|
+
import { imageRouterSizeFor } from './imagerouter.js';
|
|
34
|
+
import { withUnsignedRequestInit } from '../polling-adapter.js';
|
|
35
|
+
const DEFAULT_BASE_URL = 'https://api.imagerouter.io/v1/openai';
|
|
36
|
+
const TERMINAL_FAILED_STATUSES = new Set(['failed', 'error', 'cancelled', 'canceled']);
|
|
37
|
+
function trimTrailingSlash(value) {
|
|
38
|
+
return value.replace(/\/+$/, '');
|
|
39
|
+
}
|
|
40
|
+
/** Reads a vendor-supplied `Retry-After` (seconds) header, ignoring anything non-numeric. */
|
|
41
|
+
function retryAfterMsFrom(resp) {
|
|
42
|
+
const raw = resp.headers.get('retry-after');
|
|
43
|
+
if (!raw)
|
|
44
|
+
return undefined;
|
|
45
|
+
const seconds = Number(raw);
|
|
46
|
+
return Number.isFinite(seconds) && seconds > 0 ? seconds * 1_000 : undefined;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Builds the ImageRouter video polling adapter for a given endpoint configuration.
|
|
50
|
+
*
|
|
51
|
+
* @param config Non-secret endpoint overrides; defaults to ImageRouter's public base URL and the
|
|
52
|
+
* context's own `wireModel`.
|
|
53
|
+
* @complexity O(1) per built request; one vendor round trip per parse.
|
|
54
|
+
*/
|
|
55
|
+
export function createImageRouterVideoPollingAdapter(config = {}) {
|
|
56
|
+
const baseUrl = trimTrailingSlash((config.baseUrl || DEFAULT_BASE_URL).trim());
|
|
57
|
+
return {
|
|
58
|
+
expectedLatencyClass: 'slow',
|
|
59
|
+
// ImageRouter's generation endpoint accepts no idempotency key, so a submit that may or may
|
|
60
|
+
// not have landed cannot be safely re-issued — a duplicate submit is a duplicate charge.
|
|
61
|
+
submitIsIdempotent: false,
|
|
62
|
+
buildSubmitRequest(ctx) {
|
|
63
|
+
const wireModel = (config.wireModel || ctx.wireModel).trim();
|
|
64
|
+
const seconds = typeof ctx.length === 'number' ? ctx.length : 'auto';
|
|
65
|
+
const size = imageRouterSizeFor(ctx.aspect, 'video');
|
|
66
|
+
return {
|
|
67
|
+
url: `${baseUrl}/videos/generations`,
|
|
68
|
+
init: withUnsignedRequestInit(ctx, {
|
|
69
|
+
method: 'POST',
|
|
70
|
+
headers: { 'content-type': 'application/json' },
|
|
71
|
+
body: JSON.stringify({
|
|
72
|
+
prompt: ctx.prompt || 'A short cinematic clip.',
|
|
73
|
+
model: wireModel,
|
|
74
|
+
size,
|
|
75
|
+
seconds,
|
|
76
|
+
response_format: 'b64_json',
|
|
77
|
+
}),
|
|
78
|
+
}),
|
|
79
|
+
meta: { wireModel, size, seconds },
|
|
80
|
+
};
|
|
81
|
+
},
|
|
82
|
+
async parseSubmitResponse(resp, ctx, request) {
|
|
83
|
+
const data = (await parseOpenAICompatibleJson(resp, 'imagerouter video'));
|
|
84
|
+
if (Array.isArray(data.data)) {
|
|
85
|
+
const bytes = await bytesFromOpenAICompatibleData(data, 'imagerouter video', ctx.requestInit);
|
|
86
|
+
const { wireModel, size, seconds } = request.meta;
|
|
87
|
+
return {
|
|
88
|
+
kind: 'complete',
|
|
89
|
+
result: {
|
|
90
|
+
bytes,
|
|
91
|
+
providerNote: `imagerouter/${wireModel} · ${size} · ${seconds === 'auto' ? 'auto' : `${seconds}s`} · ${bytes.length} bytes`,
|
|
92
|
+
suggestedExt: '.mp4',
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
if (typeof data.id === 'string' && data.id) {
|
|
97
|
+
// Only the job handle is persisted — no credential, no request body.
|
|
98
|
+
const retryAfterMs = retryAfterMsFrom(resp);
|
|
99
|
+
return { kind: 'pending', state: { jobId: data.id }, ...(retryAfterMs !== undefined ? { retryAfterMs } : {}) };
|
|
100
|
+
}
|
|
101
|
+
throw new Error('imagerouter video submit returned neither generated data nor a job id');
|
|
102
|
+
},
|
|
103
|
+
buildPollRequest(state, ctx) {
|
|
104
|
+
const jobId = state.jobId;
|
|
105
|
+
if (typeof jobId !== 'string' || !jobId) {
|
|
106
|
+
throw new Error('imagerouter video poll requires a persisted jobId');
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
url: `${baseUrl}/videos/${encodeURIComponent(jobId)}`,
|
|
110
|
+
init: withUnsignedRequestInit(ctx, { method: 'GET', headers: { accept: 'application/json' } }),
|
|
111
|
+
meta: { wireModel: (config.wireModel || ctx.wireModel).trim(), size: imageRouterSizeFor(ctx.aspect, 'video'), seconds: typeof ctx.length === 'number' ? ctx.length : 'auto' },
|
|
112
|
+
};
|
|
113
|
+
},
|
|
114
|
+
async parsePollResponse(resp, ctx, _state) {
|
|
115
|
+
const data = (await parseOpenAICompatibleJson(resp, 'imagerouter video'));
|
|
116
|
+
const status = typeof data.status === 'string' ? data.status.toLowerCase() : '';
|
|
117
|
+
if (TERMINAL_FAILED_STATUSES.has(status)) {
|
|
118
|
+
const error = data.error;
|
|
119
|
+
const detail = typeof error?.message === 'string' && error.message ? error.message : status;
|
|
120
|
+
return { kind: 'failed', message: `imagerouter video job failed: ${detail}` };
|
|
121
|
+
}
|
|
122
|
+
if (Array.isArray(data.data)) {
|
|
123
|
+
const bytes = await bytesFromOpenAICompatibleData(data, 'imagerouter video', ctx.requestInit);
|
|
124
|
+
const wireModel = (config.wireModel || ctx.wireModel).trim();
|
|
125
|
+
const size = imageRouterSizeFor(ctx.aspect, 'video');
|
|
126
|
+
const seconds = typeof ctx.length === 'number' ? ctx.length : 'auto';
|
|
127
|
+
return {
|
|
128
|
+
kind: 'complete',
|
|
129
|
+
result: {
|
|
130
|
+
bytes,
|
|
131
|
+
providerNote: `imagerouter/${wireModel} · ${size} · ${seconds === 'auto' ? 'auto' : `${seconds}s`} · ${bytes.length} bytes`,
|
|
132
|
+
suggestedExt: '.mp4',
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
const retryAfterMs = retryAfterMsFrom(resp);
|
|
137
|
+
return { kind: 'pending', ...(retryAfterMs !== undefined ? { retryAfterMs } : {}) };
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
//# sourceMappingURL=imagerouter-video-async.js.map
|