@aexol/spectral 0.9.231 → 0.9.234

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.
Files changed (106) hide show
  1. package/dist/agent/index.d.ts.map +1 -1
  2. package/dist/agent/index.js +89 -14
  3. package/dist/backend/generated-images.d.ts +1 -1
  4. package/dist/backend/generated-images.d.ts.map +1 -1
  5. package/dist/commands/serve.d.ts.map +1 -1
  6. package/dist/commands/serve.js +12 -1
  7. package/dist/extensions/logic-trace.d.ts +1 -1
  8. package/dist/extensions/logic-trace.d.ts.map +1 -1
  9. package/dist/extensions/logic-trace.js +199 -70
  10. package/dist/extensions/video-generation/index.d.ts +3 -0
  11. package/dist/extensions/video-generation/index.d.ts.map +1 -0
  12. package/dist/extensions/video-generation/index.js +299 -0
  13. package/dist/generated/zeus/const.d.ts.map +1 -1
  14. package/dist/generated/zeus/const.js +30 -0
  15. package/dist/generated/zeus/index.d.ts +140 -0
  16. package/dist/generated/zeus/index.d.ts.map +1 -1
  17. package/dist/generated/zeus/index.js +5 -0
  18. package/dist/relay/dispatcher.d.ts +13 -1
  19. package/dist/relay/dispatcher.d.ts.map +1 -1
  20. package/dist/relay/dispatcher.js +11 -0
  21. package/dist/sdk/agent-core/agent-loop.js +13 -3
  22. package/dist/sdk/agent-core/agent.d.ts +11 -4
  23. package/dist/sdk/agent-core/agent.d.ts.map +1 -1
  24. package/dist/sdk/agent-core/agent.js +16 -4
  25. package/dist/sdk/agent-core/tool-call-registry.d.ts +75 -0
  26. package/dist/sdk/agent-core/tool-call-registry.d.ts.map +1 -0
  27. package/dist/sdk/agent-core/tool-call-registry.js +131 -0
  28. package/dist/sdk/agent-core/tool-execution.d.ts.map +1 -1
  29. package/dist/sdk/agent-core/tool-execution.js +103 -4
  30. package/dist/sdk/agent-core/types.d.ts +15 -4
  31. package/dist/sdk/agent-core/types.d.ts.map +1 -1
  32. package/dist/sdk/ai/index.d.ts +3 -0
  33. package/dist/sdk/ai/index.d.ts.map +1 -1
  34. package/dist/sdk/ai/index.js +3 -0
  35. package/dist/sdk/ai/providers/faux.d.ts.map +1 -1
  36. package/dist/sdk/ai/providers/faux.js +3 -0
  37. package/dist/sdk/ai/providers/leaked-tool-call-parser.d.ts +39 -6
  38. package/dist/sdk/ai/providers/leaked-tool-call-parser.d.ts.map +1 -1
  39. package/dist/sdk/ai/providers/leaked-tool-call-parser.js +159 -18
  40. package/dist/sdk/ai/providers/openai-completions.d.ts.map +1 -1
  41. package/dist/sdk/ai/providers/openai-completions.js +22 -8
  42. package/dist/sdk/ai/providers/openrouter-videos.d.ts +3 -0
  43. package/dist/sdk/ai/providers/openrouter-videos.d.ts.map +1 -0
  44. package/dist/sdk/ai/providers/openrouter-videos.js +412 -0
  45. package/dist/sdk/ai/providers/register-builtins.d.ts.map +1 -1
  46. package/dist/sdk/ai/providers/register-builtins.js +6 -0
  47. package/dist/sdk/ai/providers/simple-options.d.ts +1 -6
  48. package/dist/sdk/ai/providers/simple-options.d.ts.map +1 -1
  49. package/dist/sdk/ai/providers/simple-options.js +0 -22
  50. package/dist/sdk/ai/providers/transform-messages.d.ts.map +1 -1
  51. package/dist/sdk/ai/providers/transform-messages.js +52 -1
  52. package/dist/sdk/ai/types.d.ts +91 -10
  53. package/dist/sdk/ai/types.d.ts.map +1 -1
  54. package/dist/sdk/ai/video-models.d.ts +10 -0
  55. package/dist/sdk/ai/video-models.d.ts.map +1 -0
  56. package/dist/sdk/ai/video-models.generated.d.ts +440 -0
  57. package/dist/sdk/ai/video-models.generated.d.ts.map +1 -0
  58. package/dist/sdk/ai/video-models.generated.js +441 -0
  59. package/dist/sdk/ai/video-models.js +22 -0
  60. package/dist/sdk/ai/videos-api-registry.d.ts +14 -0
  61. package/dist/sdk/ai/videos-api-registry.d.ts.map +1 -0
  62. package/dist/sdk/ai/videos-api-registry.js +21 -0
  63. package/dist/sdk/ai/videos.d.ts +4 -0
  64. package/dist/sdk/ai/videos.d.ts.map +1 -0
  65. package/dist/sdk/ai/videos.js +13 -0
  66. package/dist/sdk/coding-agent/core/agent-session.d.ts +8 -0
  67. package/dist/sdk/coding-agent/core/agent-session.d.ts.map +1 -1
  68. package/dist/sdk/coding-agent/core/agent-session.js +11 -0
  69. package/dist/sdk/coding-agent/core/extensions/loader.d.ts.map +1 -1
  70. package/dist/sdk/coding-agent/core/extensions/loader.js +14 -0
  71. package/dist/sdk/coding-agent/core/extensions/native-extensions.d.ts +26 -0
  72. package/dist/sdk/coding-agent/core/extensions/native-extensions.d.ts.map +1 -1
  73. package/dist/sdk/coding-agent/core/extensions/native-extensions.js +23 -0
  74. package/dist/sdk/coding-agent/core/extensions/session-events.d.ts +2 -2
  75. package/dist/sdk/coding-agent/core/extensions/session-events.d.ts.map +1 -1
  76. package/dist/sdk/coding-agent/core/extensions/types.d.ts +4 -2
  77. package/dist/sdk/coding-agent/core/extensions/types.d.ts.map +1 -1
  78. package/dist/sdk/coding-agent/core/messages.d.ts.map +1 -1
  79. package/dist/sdk/coding-agent/core/messages.js +42 -2
  80. package/dist/sdk/coding-agent/core/resource-loader.d.ts +11 -0
  81. package/dist/sdk/coding-agent/core/resource-loader.d.ts.map +1 -1
  82. package/dist/sdk/coding-agent/core/resource-loader.js +23 -6
  83. package/dist/sdk/coding-agent/core/sdk.d.ts +24 -2
  84. package/dist/sdk/coding-agent/core/sdk.d.ts.map +1 -1
  85. package/dist/sdk/coding-agent/core/sdk.js +15 -3
  86. package/dist/sdk/coding-agent/core/settings-manager.d.ts +0 -8
  87. package/dist/sdk/coding-agent/core/settings-manager.d.ts.map +1 -1
  88. package/dist/sdk/coding-agent/core/settings-manager.js +3 -3
  89. package/dist/sdk/coding-agent/index.d.ts +2 -2
  90. package/dist/sdk/coding-agent/index.d.ts.map +1 -1
  91. package/dist/sdk/coding-agent/index.js +1 -1
  92. package/dist/sdk-public.d.ts +96 -0
  93. package/dist/sdk-public.d.ts.map +1 -0
  94. package/dist/sdk-public.js +109 -0
  95. package/dist/server/agent-bridge/rehydrate.d.ts +2 -2
  96. package/dist/server/agent-bridge/rehydrate.d.ts.map +1 -1
  97. package/dist/server/agent-bridge/rehydrate.js +14 -0
  98. package/dist/server/agent-bridge.d.ts +8 -2
  99. package/dist/server/agent-bridge.d.ts.map +1 -1
  100. package/dist/server/agent-bridge.js +25 -2
  101. package/dist/server/session-stream.d.ts +19 -0
  102. package/dist/server/session-stream.d.ts.map +1 -1
  103. package/dist/server/session-stream.js +35 -0
  104. package/dist/server/wire.d.ts +13 -0
  105. package/dist/server/wire.d.ts.map +1 -1
  106. package/package.json +7 -1
@@ -0,0 +1,412 @@
1
+ import { getEnvApiKey } from "../env-api-keys.js";
2
+ import { imageSourceUrlForProvider } from "../utils/image-source.js";
3
+ /**
4
+ * Video generation routing (mirrors `openrouter-images.ts`).
5
+ *
6
+ * PRIMARY PATH: when `options.backendUrl` and `options.apiKey` (the machine
7
+ * JWT) are provided, generation is submitted to the backend proxy at
8
+ * `POST ${backendUrl}/video/generation` and polled at
9
+ * `GET ${backendUrl}/video/generation/{videoId}`. The backend runs the
10
+ * upstream OpenRouter job with ITS OWN `OPENROUTER_API_KEY`, stores the
11
+ * finished video in its object storage (`/generated-videos/<id>`) and reports
12
+ * cost — the CLI never needs a local key and never receives video bytes.
13
+ *
14
+ * FALLBACK PATH: when the proxy is unavailable (404/501/5xx, connection
15
+ * errors — the endpoint may not exist yet) or no backend routing is
16
+ * configured (standalone SDK use), we submit + poll + download directly
17
+ * against OpenRouter with a local `OPENROUTER_API_KEY`:
18
+ *
19
+ * POST {baseUrl}/videos -> 202 { id, polling_url, status }
20
+ * GET polling_url (every ~30s, <= 15 min) -> { status: completed|failed, ... }
21
+ * GET {baseUrl}/videos/{id}/content?index=0 (Authorization header; bytes)
22
+ */
23
+ const DEFAULT_TIMEOUT_MS = 120_000;
24
+ const DEFAULT_MAX_POLL_MS = 900_000; // 15 minutes overall budget
25
+ const DIRECT_POLL_INTERVAL_MS = 30_000;
26
+ const PROXY_POLL_INTERVAL_MS = 15_000; // the backend polls upstream itself
27
+ const VIDEO_MIME_TYPE = "video/mp4";
28
+ const PASSTHROUGH_KEYS = [
29
+ "duration",
30
+ "resolution",
31
+ "aspect_ratio",
32
+ "size",
33
+ "generate_audio",
34
+ "seed",
35
+ ];
36
+ function textFromContext(context) {
37
+ return context.input
38
+ .filter((item) => item.type === "text")
39
+ .map((item) => item.text.trim())
40
+ .filter(Boolean)
41
+ .join("\n\n");
42
+ }
43
+ function framesFromContext(context) {
44
+ return context.input.filter((item) => item.type === "image");
45
+ }
46
+ function usageFromCost(cost) {
47
+ const total = Number(cost ?? 0) || 0;
48
+ return {
49
+ input: 0,
50
+ output: 0,
51
+ cacheRead: 0,
52
+ cacheWrite: 0,
53
+ totalTokens: 0,
54
+ cost: { input: 0, output: total, cacheRead: 0, cacheWrite: 0, total },
55
+ credits: total || undefined,
56
+ };
57
+ }
58
+ function errorResult(model, message, aborted) {
59
+ return {
60
+ api: model.api,
61
+ provider: model.provider,
62
+ model: model.id,
63
+ output: [],
64
+ stopReason: aborted ? "aborted" : "error",
65
+ errorMessage: message,
66
+ timestamp: Date.now(),
67
+ };
68
+ }
69
+ /** True for statuses that mean "the proxy endpoint does not exist / is down". */
70
+ function isProxyUnavailableStatus(status) {
71
+ return status === 404 || status === 501 || status === 502 || status === 503 || status === 504;
72
+ }
73
+ function errorMessageFromJson(json, fallback) {
74
+ return json?.error?.message ?? json?.message ?? fallback;
75
+ }
76
+ function fetchJson(url, init) {
77
+ return (async () => {
78
+ const response = await fetch(url, init);
79
+ const rawText = await response.text();
80
+ let json;
81
+ try {
82
+ json = rawText ? JSON.parse(rawText) : {};
83
+ }
84
+ catch {
85
+ throw new Error(`Response was not JSON: ${rawText.slice(0, 500)}`);
86
+ }
87
+ return { response, json };
88
+ })();
89
+ }
90
+ function makeSignal(options, deadline) {
91
+ const controller = new AbortController();
92
+ const timeout = setTimeout(() => controller.abort(), Math.max(1, Math.min(options.timeoutMs ?? DEFAULT_TIMEOUT_MS, deadline.remaining())));
93
+ if (options.signal) {
94
+ if (options.signal.aborted)
95
+ controller.abort();
96
+ else
97
+ options.signal.addEventListener("abort", () => controller.abort(), { once: true });
98
+ }
99
+ return { signal: controller.signal, done: () => clearTimeout(timeout) };
100
+ }
101
+ async function sleep(ms, signal) {
102
+ await new Promise((resolvePromise, reject) => {
103
+ const timer = setTimeout(resolvePromise, ms);
104
+ if (signal) {
105
+ if (signal.aborted) {
106
+ clearTimeout(timer);
107
+ reject(new DOMException("Aborted", "AbortError"));
108
+ return;
109
+ }
110
+ signal.addEventListener("abort", () => {
111
+ clearTimeout(timer);
112
+ reject(new DOMException("Aborted", "AbortError"));
113
+ }, { once: true });
114
+ }
115
+ });
116
+ }
117
+ /**
118
+ * Backend proxy path. Returns `undefined` when the proxy is unavailable so
119
+ * the caller can fall back to direct OpenRouter — never for genuine
120
+ * generation failures (those return an error result).
121
+ */
122
+ async function generateViaBackendProxy(model, options, backendUrl, machineJwt, prompt, frames) {
123
+ const base = backendUrl.replace(/\/$/, "");
124
+ const body = { model: model.id, prompt };
125
+ for (const key of PASSTHROUGH_KEYS) {
126
+ if (options[key] !== undefined)
127
+ body[key] = options[key];
128
+ }
129
+ // Backend contract: frameImages is [{ data?, mimeType?, path? }] with raw
130
+ // (non data-URI) base64 in `data`.
131
+ if (frames.length > 0) {
132
+ body.frameImages = frames.map((frame) => ({
133
+ data: frame.data,
134
+ mimeType: frame.mimeType,
135
+ }));
136
+ }
137
+ const start = Date.now();
138
+ const maxPollMs = options.maxPollMs ?? DEFAULT_MAX_POLL_MS;
139
+ const deadline = { remaining: () => maxPollMs - (Date.now() - start) };
140
+ const submit = makeSignal(options, deadline);
141
+ let videoId;
142
+ try {
143
+ const { response, json } = await fetchJson(`${base}/video/generation`, {
144
+ method: "POST",
145
+ headers: {
146
+ "Content-Type": "application/json",
147
+ Authorization: `Bearer ${machineJwt}`,
148
+ ...(options.headers ?? {}),
149
+ },
150
+ body: JSON.stringify(body),
151
+ signal: submit.signal,
152
+ });
153
+ if (!response.ok) {
154
+ const message = errorMessageFromJson(json, `Backend video generation failed: HTTP ${response.status}`);
155
+ // Fall back to direct OpenRouter only when the proxy itself is
156
+ // missing (bare 404/501/5xx without an upstream error body — e.g.
157
+ // an older backend without the video route). A structured error
158
+ // body means the proxy exists and the upstream rejected the
159
+ // request (e.g. OpenRouter workspace guardrails); surfacing that
160
+ // beats a fallback that would fail identically.
161
+ const hasUpstreamError = Boolean(json?.error?.message ?? json?.message);
162
+ if (isProxyUnavailableStatus(response.status) && !hasUpstreamError)
163
+ return undefined;
164
+ return errorResult(model, message, false);
165
+ }
166
+ videoId = typeof json?.videoId === "string" ? json.videoId : undefined;
167
+ if (!videoId)
168
+ return errorResult(model, "Backend video generation response had no videoId", false);
169
+ }
170
+ catch (error) {
171
+ // Connection errors (endpoint may not exist yet) → fall back.
172
+ return undefined;
173
+ }
174
+ finally {
175
+ submit.done();
176
+ }
177
+ // Poll until COMPLETED/FAILED.
178
+ for (;;) {
179
+ const remaining = deadline.remaining();
180
+ if (remaining <= 0) {
181
+ return errorResult(model, `Video generation timed out after ${maxPollMs / 1000}s (proxy)`, false);
182
+ }
183
+ const pollInterval = options.pollIntervalMs ?? PROXY_POLL_INTERVAL_MS;
184
+ await sleep(Math.min(pollInterval, remaining), options.signal).catch(() => null);
185
+ if (options.signal?.aborted)
186
+ return errorResult(model, "Video generation aborted", true);
187
+ // The sleep may have consumed the rest of the budget — fail with a
188
+ // clean timeout instead of issuing a request that aborts immediately.
189
+ if (deadline.remaining() <= 0) {
190
+ return errorResult(model, `Video generation timed out after ${maxPollMs / 1000}s (proxy)`, false);
191
+ }
192
+ const poll = makeSignal(options, deadline);
193
+ try {
194
+ const { response, json } = await fetchJson(`${base}/video/generation/${videoId}`, {
195
+ method: "GET",
196
+ headers: {
197
+ Authorization: `Bearer ${machineJwt}`,
198
+ ...(options.headers ?? {}),
199
+ },
200
+ signal: poll.signal,
201
+ });
202
+ if (!response.ok) {
203
+ if (isProxyUnavailableStatus(response.status))
204
+ return undefined;
205
+ return errorResult(model, errorMessageFromJson(json, `Video polling failed: HTTP ${response.status}`), false);
206
+ }
207
+ const status = typeof json?.status === "string" ? json.status.toUpperCase() : "";
208
+ if (status === "COMPLETED") {
209
+ const url = typeof json?.url === "string" && json.url ? json.url : `/generated-videos/${videoId}`;
210
+ const output = [
211
+ { type: "video", url, videoId, mimeType: VIDEO_MIME_TYPE },
212
+ ];
213
+ return {
214
+ api: model.api,
215
+ provider: model.provider,
216
+ model: model.id,
217
+ output,
218
+ usage: usageFromCost(Number(json?.cost)),
219
+ stopReason: "stop",
220
+ responseId: videoId,
221
+ timestamp: Date.now(),
222
+ };
223
+ }
224
+ if (status === "FAILED") {
225
+ return errorResult(model, json?.error ?? "Video generation failed (proxy)", false);
226
+ }
227
+ options.onProgress?.(`video ${videoId}: ${status || "PENDING"} (${Math.round((Date.now() - start) / 1000)}s)`);
228
+ }
229
+ catch (error) {
230
+ // Poll request failure (network) → fall back to direct.
231
+ return undefined;
232
+ }
233
+ finally {
234
+ poll.done();
235
+ }
236
+ }
237
+ }
238
+ /**
239
+ * Direct OpenRouter path: submit -> poll -> download. The download returns
240
+ * raw bytes (not base64), which are carried as base64 in the video block so
241
+ * the extension can save a local copy.
242
+ */
243
+ async function generateDirect(model, options, apiKey, prompt, frames) {
244
+ const base = model.baseUrl.replace(/\/$/, "");
245
+ const body = { model: model.id, prompt };
246
+ for (const key of PASSTHROUGH_KEYS) {
247
+ if (options[key] !== undefined)
248
+ body[key] = options[key];
249
+ }
250
+ if (frames.length > 0) {
251
+ const frameImages = frames
252
+ .map((frame) => {
253
+ const url = imageSourceUrlForProvider(frame);
254
+ if (!url) {
255
+ options.onProgress?.("Skipping frame image without a resolvable url");
256
+ return null;
257
+ }
258
+ return { type: "image_url", image_url: { url } };
259
+ })
260
+ .filter((reference) => reference !== null);
261
+ if (frameImages.length > 0)
262
+ body.frame_images = frameImages;
263
+ }
264
+ const start = Date.now();
265
+ const maxPollMs = options.maxPollMs ?? DEFAULT_MAX_POLL_MS;
266
+ const deadline = { remaining: () => maxPollMs - (Date.now() - start) };
267
+ let videoId;
268
+ let pollingUrl;
269
+ try {
270
+ const submit = makeSignal(options, deadline);
271
+ try {
272
+ const { response, json } = await fetchJson(`${base}/videos`, {
273
+ method: "POST",
274
+ headers: {
275
+ "Content-Type": "application/json",
276
+ Authorization: `Bearer ${apiKey}`,
277
+ "HTTP-Referer": "https://aexol.ai",
278
+ "X-OpenRouter-Title": "Aexol.ai",
279
+ "X-OpenRouter-Categories": "cli-agent",
280
+ ...(options.headers ?? {}),
281
+ },
282
+ body: JSON.stringify(body),
283
+ signal: submit.signal,
284
+ });
285
+ options.onResponse?.({ status: response.status, headers: Object.fromEntries(response.headers.entries()) }, model);
286
+ if (!response.ok) {
287
+ return errorResult(model, errorMessageFromJson(json, `OpenRouter video generation failed: HTTP ${response.status}`), false);
288
+ }
289
+ videoId = typeof json?.id === "string" ? json.id : undefined;
290
+ pollingUrl = typeof json?.polling_url === "string" && json.polling_url ? json.polling_url : undefined;
291
+ if (!videoId && pollingUrl) {
292
+ // Best effort: recover the id from the polling url's last path segment.
293
+ const last = pollingUrl.replace(/\/$/, "").split("/").pop();
294
+ if (last)
295
+ videoId = last;
296
+ }
297
+ if (!videoId) {
298
+ return errorResult(model, "OpenRouter video response had no id or polling_url", false);
299
+ }
300
+ }
301
+ finally {
302
+ submit.done();
303
+ }
304
+ // Poll every ~30s (OpenRouter guidance) until completed/failed.
305
+ for (;;) {
306
+ const remaining = deadline.remaining();
307
+ if (remaining <= 0) {
308
+ return errorResult(model, `Video generation timed out after ${maxPollMs / 1000}s`, false);
309
+ }
310
+ const pollInterval = options.pollIntervalMs ?? DIRECT_POLL_INTERVAL_MS;
311
+ await sleep(Math.min(pollInterval, remaining), options.signal).catch(() => null);
312
+ if (options.signal?.aborted)
313
+ return errorResult(model, "Video generation aborted", true);
314
+ // The sleep may have consumed the rest of the budget — fail with a
315
+ // clean timeout instead of issuing a request that aborts immediately.
316
+ if (deadline.remaining() <= 0) {
317
+ return errorResult(model, `Video generation timed out after ${maxPollMs / 1000}s`, false);
318
+ }
319
+ const poll = makeSignal(options, deadline);
320
+ try {
321
+ const { response, json } = await fetchJson(pollingUrl ?? `${base}/videos/${videoId}`, {
322
+ method: "GET",
323
+ headers: {
324
+ Authorization: `Bearer ${apiKey}`,
325
+ ...(options.headers ?? {}),
326
+ },
327
+ signal: poll.signal,
328
+ });
329
+ if (!response.ok) {
330
+ return errorResult(model, errorMessageFromJson(json, `OpenRouter video polling failed: HTTP ${response.status}`), false);
331
+ }
332
+ const status = typeof json?.status === "string" ? json.status.toLowerCase() : "";
333
+ if (status === "completed" || status === "succeeded") {
334
+ const cost = Number(json?.usage?.cost ?? json?.cost ?? 0) || 0;
335
+ // Download the finished video: raw bytes WITH Authorization.
336
+ const download = makeSignal(options, deadline);
337
+ try {
338
+ const downloadResponse = await fetch(`${base}/videos/${videoId}/content?index=0`, {
339
+ method: "GET",
340
+ headers: {
341
+ Authorization: `Bearer ${apiKey}`,
342
+ ...(options.headers ?? {}),
343
+ },
344
+ signal: download.signal,
345
+ });
346
+ if (!downloadResponse.ok) {
347
+ return errorResult(model, `OpenRouter video download failed: HTTP ${downloadResponse.status}`, false);
348
+ }
349
+ const buffer = await downloadResponse.arrayBuffer();
350
+ const mimeType = downloadResponse.headers.get("content-type")?.split(";")[0]?.trim() || VIDEO_MIME_TYPE;
351
+ const output = [
352
+ {
353
+ type: "video",
354
+ data: Buffer.from(buffer).toString("base64"),
355
+ mimeType: mimeType.startsWith("video/") ? mimeType : VIDEO_MIME_TYPE,
356
+ },
357
+ ];
358
+ return {
359
+ api: model.api,
360
+ provider: model.provider,
361
+ model: model.id,
362
+ output,
363
+ usage: usageFromCost(cost),
364
+ stopReason: "stop",
365
+ responseId: videoId,
366
+ timestamp: Date.now(),
367
+ };
368
+ }
369
+ finally {
370
+ download.done();
371
+ }
372
+ }
373
+ if (status === "failed") {
374
+ return errorResult(model, json?.error?.message ?? "OpenRouter video generation failed", false);
375
+ }
376
+ options.onProgress?.(`video ${videoId}: ${status || "pending"} (${Math.round((Date.now() - start) / 1000)}s)`);
377
+ }
378
+ finally {
379
+ poll.done();
380
+ }
381
+ }
382
+ }
383
+ catch (error) {
384
+ return errorResult(model, error instanceof Error ? error.message : String(error), Boolean(options.signal?.aborted));
385
+ }
386
+ }
387
+ export async function generateOpenRouterVideos(model, context, options = {}) {
388
+ const backendUrl = typeof options.backendUrl === "string" ? options.backendUrl : undefined;
389
+ const machineJwt = options.apiKey;
390
+ const routeThroughBackend = Boolean(backendUrl && machineJwt);
391
+ const localApiKey = !routeThroughBackend ? getEnvApiKey("openrouter") : undefined;
392
+ if (!routeThroughBackend && !localApiKey) {
393
+ throw new Error("Missing OPENROUTER_API_KEY for OpenRouter video generation (no backend routing configured)");
394
+ }
395
+ const prompt = textFromContext(context);
396
+ if (!prompt)
397
+ throw new Error("Video generation prompt is required");
398
+ const frames = framesFromContext(context);
399
+ if (routeThroughBackend) {
400
+ const viaProxy = await generateViaBackendProxy(model, options, backendUrl, machineJwt, prompt, frames);
401
+ if (viaProxy)
402
+ return viaProxy;
403
+ }
404
+ // Fallback (not routed, or the proxy was unavailable): direct OpenRouter
405
+ // requires a LOCAL OPENROUTER_API_KEY — the machine JWT must never be
406
+ // sent upstream.
407
+ const fallbackKey = localApiKey ?? getEnvApiKey("openrouter");
408
+ if (!fallbackKey) {
409
+ return errorResult(model, "Backend video proxy unavailable and no OPENROUTER_API_KEY configured for direct fallback", false);
410
+ }
411
+ return generateDirect(model, options, fallbackKey, prompt, frames);
412
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"register-builtins.d.ts","sourceRoot":"","sources":["../../../../src/sdk/ai/providers/register-builtins.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAMX,mBAAmB,EACnB,cAAc,EAEd,MAAM,aAAa,CAAC;AAErB,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,yBAAyB,CAAC;AAwFxE,eAAO,MAAM,uBAAuB,gEAA0C,CAAC;AAC/E,eAAO,MAAM,6BAA6B,2DAAgD,CAAC;AAE3F;;;GAGG;AACH,wBAAgB,2BAA2B,IAAI,IAAI,CAUlD;AAED,wBAAgB,iBAAiB,IAAI,IAAI,CAGxC"}
1
+ {"version":3,"file":"register-builtins.d.ts","sourceRoot":"","sources":["../../../../src/sdk/ai/providers/register-builtins.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAMX,mBAAmB,EACnB,cAAc,EAEd,MAAM,aAAa,CAAC;AAErB,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,yBAAyB,CAAC;AAyFxE,eAAO,MAAM,uBAAuB,gEAA0C,CAAC;AAC/E,eAAO,MAAM,6BAA6B,2DAAgD,CAAC;AAE3F;;;GAGG;AACH,wBAAgB,2BAA2B,IAAI,IAAI,CAclD;AAED,wBAAgB,iBAAiB,IAAI,IAAI,CAGxC"}
@@ -1,7 +1,9 @@
1
1
  import { clearApiProviders, registerApiProvider } from "../api-registry.js";
2
2
  import { registerImagesApiProvider } from "../images-api-registry.js";
3
+ import { registerVideosApiProvider } from "../videos-api-registry.js";
3
4
  import { AssistantMessageEventStream } from "../utils/event-stream.js";
4
5
  import { generateOpenRouterImages } from "./openrouter-images.js";
6
+ import { generateOpenRouterVideos } from "./openrouter-videos.js";
5
7
  function createLazyErrorMessage(model, error) {
6
8
  return {
7
9
  role: "assistant",
@@ -79,6 +81,10 @@ export function registerBuiltInApiProviders() {
79
81
  api: "openrouter-images",
80
82
  generateImages: generateOpenRouterImages,
81
83
  });
84
+ registerVideosApiProvider({
85
+ api: "openrouter-videos",
86
+ generateVideos: generateOpenRouterVideos,
87
+ });
82
88
  }
83
89
  export function resetApiProviders() {
84
90
  clearApiProviders();
@@ -1,8 +1,3 @@
1
- import type { Api, Model, SimpleStreamOptions, StreamOptions, ThinkingBudgets, ThinkingLevel } from "../types.js";
1
+ import type { Api, Model, SimpleStreamOptions, StreamOptions } from "../types.js";
2
2
  export declare function buildBaseOptions(_model: Model<Api>, options?: SimpleStreamOptions, apiKey?: string): StreamOptions;
3
- export declare function clampReasoning(effort: ThinkingLevel | undefined): Exclude<ThinkingLevel, "xhigh"> | undefined;
4
- export declare function adjustMaxTokensForThinking(baseMaxTokens: number | undefined, modelMaxTokens: number, reasoningLevel: ThinkingLevel, customBudgets?: ThinkingBudgets): {
5
- maxTokens: number;
6
- thinkingBudget: number;
7
- };
8
3
  //# sourceMappingURL=simple-options.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"simple-options.d.ts","sourceRoot":"","sources":["../../../../src/sdk/ai/providers/simple-options.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,mBAAmB,EAAE,aAAa,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAElH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,KAAK,CAAC,GAAG,CAAC,EAAE,OAAO,CAAC,EAAE,mBAAmB,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,aAAa,CAmBlH;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,aAAa,GAAG,SAAS,GAAG,OAAO,CAAC,aAAa,EAAE,OAAO,CAAC,GAAG,SAAS,CAE7G;AAED,wBAAgB,0BAA0B,CAEzC,aAAa,EAAE,MAAM,GAAG,SAAS,EACjC,cAAc,EAAE,MAAM,EACtB,cAAc,EAAE,aAAa,EAC7B,aAAa,CAAC,EAAE,eAAe,GAC7B;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,cAAc,EAAE,MAAM,CAAA;CAAE,CAoB/C"}
1
+ {"version":3,"file":"simple-options.d.ts","sourceRoot":"","sources":["../../../../src/sdk/ai/providers/simple-options.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,mBAAmB,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAElF,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,KAAK,CAAC,GAAG,CAAC,EAAE,OAAO,CAAC,EAAE,mBAAmB,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,aAAa,CAmBlH"}
@@ -18,25 +18,3 @@ export function buildBaseOptions(_model, options, apiKey) {
18
18
  metadata: options?.metadata,
19
19
  };
20
20
  }
21
- export function clampReasoning(effort) {
22
- return effort === "xhigh" ? "high" : effort;
23
- }
24
- export function adjustMaxTokensForThinking(
25
- // Undefined means no explicit caller cap. Use the model cap and fit thinking inside it.
26
- baseMaxTokens, modelMaxTokens, reasoningLevel, customBudgets) {
27
- const defaultBudgets = {
28
- minimal: 1024,
29
- low: 2048,
30
- medium: 8192,
31
- high: 16384,
32
- };
33
- const budgets = { ...defaultBudgets, ...customBudgets };
34
- const minOutputTokens = 1024;
35
- const level = clampReasoning(reasoningLevel);
36
- let thinkingBudget = budgets[level];
37
- const maxTokens = baseMaxTokens === undefined ? modelMaxTokens : Math.min(baseMaxTokens + thinkingBudget, modelMaxTokens);
38
- if (maxTokens <= thinkingBudget) {
39
- thinkingBudget = Math.max(0, maxTokens - minOutputTokens);
40
- }
41
- return { maxTokens, thinkingBudget };
42
- }
@@ -1 +1 @@
1
- {"version":3,"file":"transform-messages.d.ts","sourceRoot":"","sources":["../../../../src/sdk/ai/providers/transform-messages.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACX,GAAG,EACH,gBAAgB,EAEhB,OAAO,EACP,KAAK,EAIL,MAAM,aAAa,CAAC;AAqDrB;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,SAAS,GAAG,EACjD,QAAQ,EAAE,OAAO,EAAE,EACnB,KAAK,EAAE,KAAK,CAAC,IAAI,CAAC,EAClB,mBAAmB,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,gBAAgB,KAAK,MAAM,GACxF,OAAO,EAAE,CA2LX"}
1
+ {"version":3,"file":"transform-messages.d.ts","sourceRoot":"","sources":["../../../../src/sdk/ai/providers/transform-messages.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACX,GAAG,EACH,gBAAgB,EAEhB,OAAO,EACP,KAAK,EAKL,MAAM,aAAa,CAAC;AAgHrB;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,SAAS,GAAG,EACjD,QAAQ,EAAE,OAAO,EAAE,EACnB,KAAK,EAAE,KAAK,CAAC,IAAI,CAAC,EAClB,mBAAmB,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,gBAAgB,KAAK,MAAM,GACxF,OAAO,EAAE,CA8LX"}
@@ -1,6 +1,8 @@
1
1
  const NON_VISION_USER_IMAGE_PLACEHOLDER = "(image omitted: model does not support images)";
2
2
  const NON_VISION_TOOL_IMAGE_PLACEHOLDER = "(tool image omitted: model does not support images)";
3
3
  const IMAGE_GENERATE_SUCCESS_TEXT = "Image generated successfully and shown to the user.";
4
+ const VIDEO_USER_PLACEHOLDER = "(video omitted: models cannot process video)";
5
+ const VIDEO_GENERATE_SUCCESS_TEXT = "Video generated successfully and shown to the user.";
4
6
  function replaceImagesWithPlaceholder(content, placeholder) {
5
7
  const result = [];
6
8
  let previousWasPlaceholder = false;
@@ -12,6 +14,10 @@ function replaceImagesWithPlaceholder(content, placeholder) {
12
14
  previousWasPlaceholder = true;
13
15
  continue;
14
16
  }
17
+ // Video parts are stripped unconditionally earlier (models cannot read
18
+ // video) — defensively skip any that slip through.
19
+ if (block.type === "video")
20
+ continue;
15
21
  result.push(block);
16
22
  previousWasPlaceholder = block.text === placeholder;
17
23
  }
@@ -40,6 +46,48 @@ function downgradeUnsupportedImages(messages, model) {
40
46
  return msg;
41
47
  });
42
48
  }
49
+ function replaceVideosWithPlaceholder(content, placeholder) {
50
+ const result = [];
51
+ let previousWasPlaceholder = false;
52
+ for (const block of content) {
53
+ if (block.type === "video") {
54
+ if (!previousWasPlaceholder) {
55
+ result.push({ type: "text", text: placeholder });
56
+ }
57
+ previousWasPlaceholder = true;
58
+ continue;
59
+ }
60
+ result.push(block);
61
+ previousWasPlaceholder = block.type === "text" && block.text === placeholder;
62
+ }
63
+ return result;
64
+ }
65
+ /**
66
+ * UNCONDITIONAL video strip: models cannot read video, so video parts are
67
+ * NEVER sent to any model — for every model, not just non-vision ones.
68
+ * The raw block (hosted url or inline bytes) stays on the AgentToolResult for
69
+ * the UI; here it is replaced with a short text note.
70
+ */
71
+ function stripVideoParts(messages) {
72
+ return messages.map((msg) => {
73
+ if (msg.role === "user" && Array.isArray(msg.content)) {
74
+ return {
75
+ ...msg,
76
+ content: replaceVideosWithPlaceholder(msg.content, VIDEO_USER_PLACEHOLDER),
77
+ };
78
+ }
79
+ if (msg.role === "toolResult") {
80
+ const replacementText = msg.toolName === "video_generate"
81
+ ? VIDEO_GENERATE_SUCCESS_TEXT
82
+ : VIDEO_USER_PLACEHOLDER;
83
+ return {
84
+ ...msg,
85
+ content: replaceVideosWithPlaceholder(msg.content, replacementText),
86
+ };
87
+ }
88
+ return msg;
89
+ });
90
+ }
43
91
  /**
44
92
  * Normalize tool call ID for cross-provider compatibility.
45
93
  * OpenAI Responses API generates IDs that are 450+ chars with special characters like `|`.
@@ -48,7 +96,10 @@ function downgradeUnsupportedImages(messages, model) {
48
96
  export function transformMessages(messages, model, normalizeToolCallId) {
49
97
  // Build a map of original tool call IDs to normalized IDs
50
98
  const toolCallIdMap = new Map();
51
- const imageAwareMessages = downgradeUnsupportedImages(messages, model);
99
+ // Video blocks are stripped for EVERY model (models cannot read video);
100
+ // images are downgraded only for models without vision support.
101
+ const videoAwareMessages = stripVideoParts(messages);
102
+ const imageAwareMessages = downgradeUnsupportedImages(videoAwareMessages, model);
52
103
  // First pass: transform messages (unsupported image downgrade, thinking blocks, tool call ID normalization)
53
104
  const transformed = imageAwareMessages.map((msg) => {
54
105
  // User messages pass through unchanged