@aexol/spectral 0.9.251 → 0.9.255

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 (96) hide show
  1. package/dist/extensions/dataset-uploader/analyzer.d.ts +103 -0
  2. package/dist/extensions/dataset-uploader/analyzer.d.ts.map +1 -0
  3. package/dist/extensions/dataset-uploader/analyzer.js +266 -0
  4. package/dist/extensions/dataset-uploader/cleaner.d.ts +59 -0
  5. package/dist/extensions/dataset-uploader/cleaner.d.ts.map +1 -0
  6. package/dist/extensions/dataset-uploader/cleaner.js +118 -0
  7. package/dist/extensions/dataset-uploader/exporter.d.ts +147 -0
  8. package/dist/extensions/dataset-uploader/exporter.d.ts.map +1 -0
  9. package/dist/extensions/dataset-uploader/exporter.js +313 -0
  10. package/dist/extensions/dataset-uploader/extra-values.d.ts +17 -0
  11. package/dist/extensions/dataset-uploader/extra-values.d.ts.map +1 -0
  12. package/dist/extensions/dataset-uploader/extra-values.js +51 -0
  13. package/dist/extensions/dataset-uploader/index.d.ts +22 -0
  14. package/dist/extensions/dataset-uploader/index.d.ts.map +1 -0
  15. package/dist/extensions/dataset-uploader/index.js +247 -0
  16. package/dist/extensions/dataset-uploader/scrubber.d.ts +34 -0
  17. package/dist/extensions/dataset-uploader/scrubber.d.ts.map +1 -0
  18. package/dist/extensions/dataset-uploader/scrubber.js +91 -0
  19. package/dist/extensions/dataset-uploader/upload.d.ts +105 -0
  20. package/dist/extensions/dataset-uploader/upload.d.ts.map +1 -0
  21. package/dist/extensions/dataset-uploader/upload.js +274 -0
  22. package/dist/generated/zeus/const.d.ts.map +1 -1
  23. package/dist/generated/zeus/const.js +80 -6
  24. package/dist/generated/zeus/index.d.ts +432 -36
  25. package/dist/generated/zeus/index.d.ts.map +1 -1
  26. package/dist/mcp/commands.d.ts.map +1 -1
  27. package/dist/mcp/commands.js +3 -0
  28. package/dist/mcp/direct-tools.d.ts.map +1 -1
  29. package/dist/mcp/direct-tools.js +4 -3
  30. package/dist/mcp/index.d.ts.map +1 -1
  31. package/dist/mcp/index.js +5 -1
  32. package/dist/mcp/mcp-auth-flow.d.ts +5 -1
  33. package/dist/mcp/mcp-auth-flow.d.ts.map +1 -1
  34. package/dist/mcp/mcp-auth-flow.js +20 -4
  35. package/dist/mcp/mcp-auth-start.d.ts +64 -0
  36. package/dist/mcp/mcp-auth-start.d.ts.map +1 -0
  37. package/dist/mcp/mcp-auth-start.js +136 -0
  38. package/dist/mcp/proxy-modes.d.ts +7 -0
  39. package/dist/mcp/proxy-modes.d.ts.map +1 -1
  40. package/dist/mcp/proxy-modes.js +71 -11
  41. package/dist/mcp/server-manager.d.ts +2 -1
  42. package/dist/mcp/server-manager.d.ts.map +1 -1
  43. package/dist/mcp/server-manager.js +8 -4
  44. package/dist/memory/commands/status.d.ts.map +1 -1
  45. package/dist/memory/commands/status.js +2 -1
  46. package/dist/memory/hooks/compaction-trigger.d.ts +14 -1
  47. package/dist/memory/hooks/compaction-trigger.d.ts.map +1 -1
  48. package/dist/memory/hooks/compaction-trigger.js +31 -4
  49. package/dist/relay/dispatcher.d.ts.map +1 -1
  50. package/dist/relay/dispatcher.js +4 -47
  51. package/dist/relay/routing.d.ts +1 -1
  52. package/dist/relay/routing.d.ts.map +1 -1
  53. package/dist/relay/routing.js +9 -0
  54. package/dist/sdk/ai/utils/image-source.d.ts +26 -11
  55. package/dist/sdk/ai/utils/image-source.d.ts.map +1 -1
  56. package/dist/sdk/ai/utils/image-source.js +184 -47
  57. package/dist/sdk/ai/utils/transient-error.d.ts +4 -0
  58. package/dist/sdk/ai/utils/transient-error.d.ts.map +1 -1
  59. package/dist/sdk/ai/utils/transient-error.js +15 -0
  60. package/dist/sdk/coding-agent/core/compaction/policy.d.ts +2 -0
  61. package/dist/sdk/coding-agent/core/compaction/policy.d.ts.map +1 -1
  62. package/dist/sdk/coding-agent/core/compaction/policy.js +2 -0
  63. package/dist/sdk/coding-agent/core/extensions/native-extensions.d.ts.map +1 -1
  64. package/dist/sdk/coding-agent/core/extensions/native-extensions.js +10 -0
  65. package/dist/sdk/coding-agent/core/system-prompt.d.ts.map +1 -1
  66. package/dist/sdk/coding-agent/core/system-prompt.js +4 -0
  67. package/dist/sdk/coding-agent/core/tools/read.d.ts.map +1 -1
  68. package/dist/sdk/coding-agent/core/tools/read.js +14 -7
  69. package/dist/sdk/coding-agent/index.d.ts +1 -1
  70. package/dist/sdk/coding-agent/index.d.ts.map +1 -1
  71. package/dist/sdk/coding-agent/index.js +1 -1
  72. package/dist/sdk/coding-agent/utils/image-resize.d.ts +25 -11
  73. package/dist/sdk/coding-agent/utils/image-resize.d.ts.map +1 -1
  74. package/dist/sdk/coding-agent/utils/image-resize.js +54 -36
  75. package/dist/sdk/coding-agent/utils/photon.d.ts.map +1 -1
  76. package/dist/sdk/coding-agent/utils/photon.js +11 -2
  77. package/dist/server/agent-bridge.d.ts +32 -0
  78. package/dist/server/agent-bridge.d.ts.map +1 -1
  79. package/dist/server/agent-bridge.js +59 -9
  80. package/dist/server/handlers/mcp-auth.d.ts +28 -0
  81. package/dist/server/handlers/mcp-auth.d.ts.map +1 -0
  82. package/dist/server/handlers/mcp-auth.js +41 -0
  83. package/dist/server/handlers/mcp-status.d.ts +8 -2
  84. package/dist/server/handlers/mcp-status.d.ts.map +1 -1
  85. package/dist/server/handlers/mcp-status.js +15 -3
  86. package/dist/server/preset-chain.d.ts +0 -13
  87. package/dist/server/preset-chain.d.ts.map +1 -1
  88. package/dist/server/preset-chain.js +0 -25
  89. package/dist/server/session-stream/memory.d.ts.map +1 -1
  90. package/dist/server/session-stream/memory.js +8 -1
  91. package/dist/server/session-stream.d.ts +15 -56
  92. package/dist/server/session-stream.d.ts.map +1 -1
  93. package/dist/server/session-stream.js +41 -271
  94. package/dist/server/wire.d.ts +18 -32
  95. package/dist/server/wire.d.ts.map +1 -1
  96. package/package.json +1 -1
@@ -1,4 +1,4 @@
1
- import { DEFAULT_COMPACTION_POLICY } from "../../sdk/coding-agent/core/compaction/index.js";
1
+ import { AUTO_COMPACTION_WINDOW_RATIO, DEFAULT_COMPACTION_POLICY, } from "../../sdk/coding-agent/core/compaction/index.js";
2
2
  import { rawTokensSinceLastCompaction } from "../branch.js";
3
3
  /**
4
4
  * Regex matching spectral's internal retryable error detection.
@@ -9,8 +9,35 @@ const RETRYABLE_ERROR_RE = /overloaded|provider.?returned.?error|rate.?limit|too
9
9
  function getCompactionPolicy(ctx) {
10
10
  return ctx.settingsManager?.getCompactionPolicy?.() ?? DEFAULT_COMPACTION_POLICY;
11
11
  }
12
- function getPostRunCompactionThresholdTokens(runtime, policy) {
13
- return Math.max(runtime.config.compactionThresholdTokens, policy.autoOlderHistory.minRawTokens);
12
+ /**
13
+ * Resolve the active model's context window. Prefers the live usage report,
14
+ * falls back to the model definition; returns undefined when unknown.
15
+ */
16
+ export function getContextWindow(ctx) {
17
+ try {
18
+ const usage = ctx.getContextUsage();
19
+ if (usage && typeof usage.contextWindow === "number" && usage.contextWindow > 0) {
20
+ return usage.contextWindow;
21
+ }
22
+ }
23
+ catch {
24
+ // getContextUsage is optional in some headless harnesses.
25
+ }
26
+ const modelWindow = ctx.model?.contextWindow;
27
+ return typeof modelWindow === "number" && modelWindow > 0 ? modelWindow : undefined;
28
+ }
29
+ /**
30
+ * Effective post-run compaction threshold: the configured/policy floor, capped
31
+ * at AUTO_COMPACTION_WINDOW_RATIO × the model context window when the window
32
+ * is known. An unknown window (0/undefined, e.g. synthetic/proxy models) keeps
33
+ * the uncapped formula.
34
+ */
35
+ export function getPostRunCompactionThresholdTokens(runtime, policy, contextWindow) {
36
+ const uncapped = Math.max(runtime.config.compactionThresholdTokens, policy.autoOlderHistory.minRawTokens);
37
+ if (typeof contextWindow !== "number" || !Number.isFinite(contextWindow) || contextWindow <= 0) {
38
+ return uncapped;
39
+ }
40
+ return Math.min(uncapped, Math.floor(contextWindow * AUTO_COMPACTION_WINDOW_RATIO));
14
41
  }
15
42
  export function registerCompactionTrigger(ext, runtime) {
16
43
  ext.on("agent_end", (event, ctx) => {
@@ -34,7 +61,7 @@ export function registerCompactionTrigger(ext, runtime) {
34
61
  }
35
62
  const entries = ctx.sessionManager.getBranch();
36
63
  const tokens = rawTokensSinceLastCompaction(entries);
37
- const thresholdTokens = getPostRunCompactionThresholdTokens(runtime, policy);
64
+ const thresholdTokens = getPostRunCompactionThresholdTokens(runtime, policy, getContextWindow(ctx));
38
65
  if (tokens < thresholdTokens)
39
66
  return;
40
67
  // Capture ctx properties synchronously — the setTimeout + async work below
@@ -1 +1 @@
1
- {"version":3,"file":"dispatcher.d.ts","sourceRoot":"","sources":["../../src/relay/dispatcher.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"dispatcher.d.ts","sourceRoot":"","sources":["../../src/relay/dispatcher.ts"],"names":[],"mappings":"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"}
@@ -130,8 +130,6 @@ async function emitChunkedSessionReady(relay, sessionId, history, attachResult,
130
130
  intervalMinutes: attachResult.intervalMinutes,
131
131
  intervalNextTickAt: attachResult.intervalNextTickAt,
132
132
  intervalLaunchCount: attachResult.intervalLaunchCount,
133
- intervalHasLoop: attachResult.intervalHasLoop || undefined,
134
- intervalGoal: attachResult.intervalGoal,
135
133
  runState: attachResult.runState,
136
134
  },
137
135
  });
@@ -227,19 +225,16 @@ export async function handleClientMessage(frame, deps) {
227
225
  return;
228
226
  }
229
227
  // Interval scheduler control (sent as a content-less user_message).
230
- const loopControl = message.loopControl;
231
- if (loopControl === "stop") {
228
+ const intervalControl = message.intervalControl;
229
+ if (intervalControl === "stop") {
232
230
  manager.stopInterval(sessionId);
233
231
  return;
234
232
  }
235
- if (loopControl === "run_now") {
233
+ if (intervalControl === "run_now") {
236
234
  manager.triggerIntervalNow(sessionId);
237
235
  return;
238
236
  }
239
237
  const content = message.content;
240
- const isLoop = message.loop === true;
241
- const loopMaxIterations = message.loopMaxIterations;
242
- const loopGoal = message.loopGoal;
243
238
  // Rejected attachments (oversize inline base64, missing url+data, ...) are
244
239
  // surfaced to the client instead of silently vanishing from the turn.
245
240
  //
@@ -288,14 +283,6 @@ export async function handleClientMessage(frame, deps) {
288
283
  });
289
284
  return;
290
285
  }
291
- // Set autonomous iterative loop state before firing the prompt.
292
- // loop:true → start/renew loop with the current content as original prompt;
293
- // loop:false → stop any active loop.
294
- // Arm loop state BEFORE any await: for non-reference prompts this is
295
- // synchronous, preserving the client_message/subscribe interleave below.
296
- // For "@preset:<name>" prompts this returns a promise that we await just
297
- // before firing the turn, so the loop replays the EXPANDED prompt.
298
- const armed = manager.setLoopActive(sessionId, isLoop, content, loopMaxIterations, loopGoal);
299
286
  // 2. Attach (idempotent). On first attach we capture the replay payload
300
287
  // and synthesize a `session_ready` ws_event so the browser sees the
301
288
  // same first frame it would have on a direct WS connection.
@@ -354,40 +341,13 @@ export async function handleClientMessage(frame, deps) {
354
341
  });
355
342
  }
356
343
  }
357
- // "@preset:<name>" loop prompts: resolve the preset before the turn fires
358
- // (undefined = already armed synchronously — no yield, no reordering).
359
- // The arm path resolves to the EXPANDED prompt; `null` means it failed or
360
- // was aborted (loop state changed mid-resolution) — fall back to the raw
361
- // content in those cases. The promise is caught, not just awaited: serve
362
- // calls this handler fire-and-forget, so a rejection (e.g. a sync throw
363
- // from teamIdForSession inside the manager) would otherwise surface as an
364
- // unhandled rejection instead of a visible error event.
365
- let expandedLoopPrompt = null;
366
- if (armed instanceof Promise) {
367
- try {
368
- expandedLoopPrompt = await armed;
369
- }
370
- catch (err) {
371
- const msg = err instanceof Error ? err.message : String(err);
372
- logger.error?.(`[dispatcher] loop arming failed for ${sessionId}:`, err);
373
- relay.send({
374
- kind: "ws_event",
375
- sessionId,
376
- event: { type: "error", message: humanizeProviderError(msg) },
377
- });
378
- }
379
- }
380
- const promptContent = expandedLoopPrompt ?? content;
381
344
  // 3. Fire the prompt. `prompt()` resolves on `agent_end`; errors are
382
345
  // converted to `error` events by the manager itself, so we just log.
383
346
  // `modelId` (Phase 3 — Available Models whitelist) is forwarded as
384
347
  // sticky next-prompt selection. The backend has already validated it
385
348
  // against the team-scoped whitelist; the CLI resolves it via spectral's
386
349
  // own model registry inside the manager → bridge.
387
- //
388
- // When `loop: true`, loop state is set before prompting; the normal
389
- // model is used — loop replay is handled by session-stream on agent_end.
390
- manager.prompt(sessionId, promptContent, modelId, validImages, reasoningEffort).catch((err) => {
350
+ manager.prompt(sessionId, content, modelId, validImages, reasoningEffort).catch((err) => {
391
351
  logger.error?.(`[dispatcher] manager.prompt failed for ${sessionId}:`, err);
392
352
  });
393
353
  // Arm the interval scheduler after the first (immediate) launch.
@@ -395,9 +355,6 @@ export async function handleClientMessage(frame, deps) {
395
355
  if (typeof intervalMinutes === "number" && intervalMinutes > 0) {
396
356
  manager.armInterval(sessionId, {
397
357
  prompt: content,
398
- loop: isLoop,
399
- loopMaxIterations,
400
- loopGoal,
401
358
  modelId,
402
359
  reasoningEffort,
403
360
  images: validImages,
@@ -20,7 +20,7 @@ import { type WizardLoginResult } from "./wizard-login.js";
20
20
  * `id` is populated when the route had an `:id` placeholder.
21
21
  */
22
22
  interface RouteMatch {
23
- route: "list_projects" | "create_project" | "update_project" | "delete_project" | "bind_studio" | "unbind_studio" | "get_project_observations" | "list_project_sessions" | "cleanup_project_sessions" | "cleanup_all_sessions" | "create_session" | "get_session" | "get_session_active_agent" | "update_session_active_agent" | "get_session_messages" | "update_session" | "delete_session" | "get_session_memory" | "get_session_memory_details" | "compact_session" | "remember_and_delete_session" | "fork_session" | "list_path_autocomplete" | "list_directory" | "get_home_directory" | "create_directory" | "search_project_files" | "enqueue_prompt" | "get_prompt_queue" | "remove_prompt" | "clear_prompt_queue" | "list_mcp_status" | "reload_mcp_config" | "refresh_native_extensions" | "refresh_cloud_agents" | "get_settings" | "put_settings" | "get_agent_settings" | "put_agent_settings" | "get_prompt_mutation_settings" | "put_prompt_mutation_settings" | "wizard_login" | "list_agents" | "list_dev_processes" | "start_dev_process" | "kill_dev_process" | "restart_dev_process" | "scan_dev_servers" | "kill_dev_server" | "list_secrets" | "create_secret" | "delete_secret";
23
+ route: "list_projects" | "create_project" | "update_project" | "delete_project" | "bind_studio" | "unbind_studio" | "get_project_observations" | "list_project_sessions" | "cleanup_project_sessions" | "cleanup_all_sessions" | "create_session" | "get_session" | "get_session_active_agent" | "update_session_active_agent" | "get_session_messages" | "update_session" | "delete_session" | "get_session_memory" | "get_session_memory_details" | "compact_session" | "remember_and_delete_session" | "fork_session" | "list_path_autocomplete" | "list_directory" | "get_home_directory" | "create_directory" | "search_project_files" | "enqueue_prompt" | "get_prompt_queue" | "remove_prompt" | "clear_prompt_queue" | "list_mcp_status" | "reload_mcp_config" | "start_mcp_auth" | "refresh_native_extensions" | "refresh_cloud_agents" | "get_settings" | "put_settings" | "get_agent_settings" | "put_agent_settings" | "get_prompt_mutation_settings" | "put_prompt_mutation_settings" | "wizard_login" | "list_agents" | "list_dev_processes" | "start_dev_process" | "kill_dev_process" | "restart_dev_process" | "scan_dev_servers" | "kill_dev_server" | "list_secrets" | "create_secret" | "delete_secret";
24
24
  id?: string;
25
25
  /** Parsed query params, if the path carried a `?...` suffix. */
26
26
  query?: URLSearchParams;
@@ -1 +1 @@
1
- {"version":3,"file":"routing.d.ts","sourceRoot":"","sources":["../../src/relay/routing.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"routing.d.ts","sourceRoot":"","sources":["../../src/relay/routing.ts"],"names":[],"mappings":"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"}
@@ -11,6 +11,7 @@
11
11
  */
12
12
  import { homedir } from "node:os";
13
13
  import { BadRequestError } from "../server/handlers/errors.js";
14
+ import { handleStartMcpAuth } from "../server/handlers/mcp-auth.js";
14
15
  import { handleListMcpStatus } from "../server/handlers/mcp-status.js";
15
16
  import { handleReloadMcpConfig } from "../server/handlers/mcp-reload.js";
16
17
  import { handlePathAutocomplete } from "../server/handlers/paths-autocomplete.js";
@@ -68,6 +69,11 @@ export function matchRoute(method, path) {
68
69
  return { route: "reload_mcp_config" };
69
70
  return null;
70
71
  }
72
+ if (cleanPath === "/api/mcp-auth") {
73
+ if (method === "POST")
74
+ return { route: "start_mcp_auth" };
75
+ return null;
76
+ }
71
77
  if (cleanPath === "/api/native-extensions/refresh") {
72
78
  if (method === "POST")
73
79
  return { route: "refresh_native_extensions" };
@@ -655,6 +661,9 @@ export async function dispatchRoute(match, body, deps) {
655
661
  case "reload_mcp_config": {
656
662
  return await handleReloadMcpConfig(deps.mcpState);
657
663
  }
664
+ case "start_mcp_auth": {
665
+ return await handleStartMcpAuth(deps.mcpState, asObject(body));
666
+ }
658
667
  case "refresh_native_extensions": {
659
668
  if (!deps.refreshNativeExtensions) {
660
669
  return { ok: true };
@@ -14,12 +14,19 @@
14
14
  * as a base64 data URI — OpenRouter's server-side
15
15
  * media fetcher gets HTTP 400 from S3 on
16
16
  * presigned urls, so the raw presigned url must
17
- * never reach the provider. If the S3 download
18
- * fails, the presigned url is sent as a fallback
19
- * instead of dropping the image.
17
+ * never reach the provider. If the download
18
+ * fails the image is dropped — a presigned url
19
+ * is never sent as a fallback.
20
20
  * (`/generated-images/<id>` is what the backend
21
21
  * returns; it is AUTH-GATED, so handing it to a
22
22
  * provider only yields a 401)
23
+ * - `url` S3 presigned -> STALE/EXPIRED signatures must never reach the
24
+ * provider ("Failed to fetch media from URL ...
25
+ * parameter=image_url" + 8x retry spam in old
26
+ * sessions): presigned sources are mapped back
27
+ * to the hosted ref (`/generated-images/<id>`)
28
+ * and re-signed fresh, else fetched + inlined
29
+ * directly, else DROPPED.
23
30
  * - `url` relative -> resolved against the backend base url first
24
31
  * - `url` local/private -> fetched and inlined **at send time only**
25
32
  *
@@ -85,14 +92,6 @@ export interface ResolvedImageSource {
85
92
  * (inline `data` present, or fetched from a local url).
86
93
  */
87
94
  data?: string;
88
- /**
89
- * True when `url` is a presigned url returned by `resolveHostedUrl` (i.e.
90
- * it replaced an auth-gated `/generated-images/<id>` reference). Only set
91
- * when the presigned url itself is handed to the provider — which only
92
- * happens as a fallback when inlining its bytes failed; a successful
93
- * download is inlined (`inlined: true`) and the flag stays unset.
94
- */
95
- hostedResolved?: boolean;
96
95
  }
97
96
  /**
98
97
  * True when `rawUrl` has no scheme, i.e. it is relative and therefore
@@ -104,6 +103,21 @@ export declare function isRelativeImageUrl(rawUrl: string): boolean;
104
103
  * url. Absolute urls and missing base urls are returned untouched.
105
104
  */
106
105
  export declare function resolveImageUrlAgainstBase(rawUrl: string, baseUrl?: string): string;
106
+ /**
107
+ * True when `rawUrl` is an AWS S3 presigned GET (SigV4): its query string
108
+ * carries `X-Amz-Algorithm`/`X-Amz-Signature`/`X-Amz-Date` (case-insensitive
109
+ * per the SigV4 spec). Such urls are short-lived — a stale one sitting in
110
+ * persisted session history would fail every provider request forever.
111
+ */
112
+ export declare function isS3PresignedUrl(rawUrl: string): boolean;
113
+ /**
114
+ * Stable backend-hosted reference (`/generated-images/<uuid>`) for a
115
+ * presigned S3 url whose key follows the conversation-image convention
116
+ * (`<ownerPrefix>/images/<uuid>.<ext>`), or `null` when the path does not
117
+ * match. Resolving the hosted ref lets a request re-sign a FRESH presigned
118
+ * url — and rewrite the stale/expired signature out of the request entirely.
119
+ */
120
+ export declare function hostedRefFromPresignedUrl(rawUrl: string): string | null;
107
121
  /**
108
122
  * True when a provider is unlikely to be able to fetch this URL itself and
109
123
  * the image must therefore be inlined as base64 before sending.
@@ -148,6 +162,7 @@ export declare function base64FromImageData(data: string): string;
148
162
  * Returns `null` when the block carries neither data nor url (nothing sendable).
149
163
  * Local/private urls are fetched and inlined; failures fall back to the raw
150
164
  * url so the caller still emits *something* rather than dropping the image.
165
+ * S3 presigned urls are never sent as-is — see `resolvePresignedSourceImage`.
151
166
  */
152
167
  export declare function resolveImageForProvider(image: ImageContent, options?: ResolveImageOptions): Promise<ResolvedImageSource | null>;
153
168
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"image-source.d.ts","sourceRoot":"","sources":["../../../../src/sdk/ai/utils/image-source.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"image-source.d.ts","sourceRoot":"","sources":["../../../../src/sdk/ai/utils/image-source.ts"],"names":[],"mappings":"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"}
@@ -14,12 +14,19 @@
14
14
  * as a base64 data URI — OpenRouter's server-side
15
15
  * media fetcher gets HTTP 400 from S3 on
16
16
  * presigned urls, so the raw presigned url must
17
- * never reach the provider. If the S3 download
18
- * fails, the presigned url is sent as a fallback
19
- * instead of dropping the image.
17
+ * never reach the provider. If the download
18
+ * fails the image is dropped — a presigned url
19
+ * is never sent as a fallback.
20
20
  * (`/generated-images/<id>` is what the backend
21
21
  * returns; it is AUTH-GATED, so handing it to a
22
22
  * provider only yields a 401)
23
+ * - `url` S3 presigned -> STALE/EXPIRED signatures must never reach the
24
+ * provider ("Failed to fetch media from URL ...
25
+ * parameter=image_url" + 8x retry spam in old
26
+ * sessions): presigned sources are mapped back
27
+ * to the hosted ref (`/generated-images/<id>`)
28
+ * and re-signed fresh, else fetched + inlined
29
+ * directly, else DROPPED.
23
30
  * - `url` relative -> resolved against the backend base url first
24
31
  * - `url` local/private -> fetched and inlined **at send time only**
25
32
  *
@@ -101,6 +108,77 @@ export function resolveImageUrlAgainstBase(rawUrl, baseUrl) {
101
108
  return trimmed;
102
109
  }
103
110
  }
111
+ /**
112
+ * Query parameter names AWS SigV4 presigned urls carry in their query string.
113
+ * A url carrying any of these is a signed S3 GET that stops working the
114
+ * moment its (typically 3600s) signature expires.
115
+ */
116
+ const S3_PRESIGNED_QUERY_PARAMS = ["x-amz-algorithm", "x-amz-signature", "x-amz-date"];
117
+ /**
118
+ * True when `rawUrl` is an AWS S3 presigned GET (SigV4): its query string
119
+ * carries `X-Amz-Algorithm`/`X-Amz-Signature`/`X-Amz-Date` (case-insensitive
120
+ * per the SigV4 spec). Such urls are short-lived — a stale one sitting in
121
+ * persisted session history would fail every provider request forever.
122
+ */
123
+ export function isS3PresignedUrl(rawUrl) {
124
+ const trimmed = typeof rawUrl === "string" ? rawUrl.trim() : "";
125
+ if (!trimmed)
126
+ return false;
127
+ let url;
128
+ try {
129
+ url = new URL(trimmed);
130
+ }
131
+ catch {
132
+ return false;
133
+ }
134
+ if (url.protocol !== "http:" && url.protocol !== "https:")
135
+ return false;
136
+ const present = new Set();
137
+ url.searchParams.forEach((_value, key) => present.add(key.toLowerCase()));
138
+ return S3_PRESIGNED_QUERY_PARAMS.some((param) => present.has(param));
139
+ }
140
+ /**
141
+ * Conversation-image key convention on S3:
142
+ * `<ownerPrefix>/images/<uuid>.<ext>` — the uuid is the final path segment.
143
+ */
144
+ const PRESIGNED_S3_IMAGE_PATH_RE = /\/images\/([^/?#]+)$/;
145
+ /**
146
+ * Stable backend-hosted reference (`/generated-images/<uuid>`) for a
147
+ * presigned S3 url whose key follows the conversation-image convention
148
+ * (`<ownerPrefix>/images/<uuid>.<ext>`), or `null` when the path does not
149
+ * match. Resolving the hosted ref lets a request re-sign a FRESH presigned
150
+ * url — and rewrite the stale/expired signature out of the request entirely.
151
+ */
152
+ export function hostedRefFromPresignedUrl(rawUrl) {
153
+ if (typeof rawUrl !== "string")
154
+ return null;
155
+ const trimmed = rawUrl.trim();
156
+ if (!trimmed)
157
+ return null;
158
+ let url;
159
+ try {
160
+ url = new URL(trimmed);
161
+ }
162
+ catch {
163
+ return null;
164
+ }
165
+ const raw = url.pathname.match(PRESIGNED_S3_IMAGE_PATH_RE)?.[1];
166
+ if (!raw)
167
+ return null;
168
+ // Strip an optional extension (`<uuid>.png`) — the id is the first part.
169
+ const dot = raw.lastIndexOf(".");
170
+ let id = dot > 0 ? raw.slice(0, dot) : raw;
171
+ try {
172
+ id = decodeURIComponent(id);
173
+ }
174
+ catch {
175
+ // Malformed percent-encoding: keep the raw segment.
176
+ }
177
+ // Mirror the backend id shape (`HOSTED_IMAGE_ID`): [A-Za-z0-9_-]{1,64}.
178
+ if (!/^[A-Za-z0-9_-]{1,64}$/.test(id))
179
+ return null;
180
+ return `/generated-images/${id}`;
181
+ }
104
182
  /**
105
183
  * Loopback / unique-local (fc00::/7) / link-local (fe80::/10) IPv6 are
106
184
  * unreachable for a provider; every other literal (e.g. `2001:4860::8888`)
@@ -257,12 +335,97 @@ function normalizeMimeType(value, fallback) {
257
335
  const base = value.split(";")[0].trim().toLowerCase();
258
336
  return base || fallback || "image/png";
259
337
  }
338
+ /**
339
+ * Download `downloadUrl`, apply the size guards and inline the bytes as a
340
+ * base64 data URI. Throws on any failure — the caller decides whether to
341
+ * fall back, pass the url through or drop the image.
342
+ */
343
+ async function fetchAndInlineImage(downloadUrl, image, requestHeaders, options) {
344
+ const doFetch = options.fetchImpl ?? fetch;
345
+ const timeoutMs = options.timeoutMs ?? DEFAULT_INLINE_FETCH_TIMEOUT_MS;
346
+ const maxInlineBytes = options.maxInlineBytes ?? DEFAULT_MAX_INLINE_BYTES;
347
+ const controller = new AbortController();
348
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
349
+ const onAbort = () => controller.abort();
350
+ options.signal?.addEventListener("abort", onAbort, { once: true });
351
+ try {
352
+ // An already-aborted signal never fires the listener above.
353
+ if (options.signal?.aborted)
354
+ throw new Error("aborted");
355
+ // `requestHeaders` is only set when the url resolves to the backend
356
+ // origin — a url on any other host is fetched anonymously, so the JWT
357
+ // cannot leak to a third party.
358
+ const response = await doFetch(downloadUrl, requestHeaders
359
+ ? { signal: controller.signal, headers: requestHeaders }
360
+ : { signal: controller.signal });
361
+ if (!response.ok)
362
+ throw new Error(`HTTP ${response.status}`);
363
+ // Reject oversized payloads from the declared length BEFORE reading
364
+ // the body — otherwise the whole payload lands in memory first.
365
+ const declaredLength = response.headers?.get?.("content-length");
366
+ if (declaredLength !== null && declaredLength !== undefined) {
367
+ const declared = Number(declaredLength);
368
+ if (Number.isFinite(declared) && declared > maxInlineBytes) {
369
+ throw new Error(`image too large to inline (${declared} bytes)`);
370
+ }
371
+ }
372
+ const buffer = await response.arrayBuffer();
373
+ if (buffer.byteLength > maxInlineBytes) {
374
+ throw new Error(`image too large to inline (${buffer.byteLength} bytes)`);
375
+ }
376
+ const mimeType = normalizeMimeType(response.headers?.get?.("content-type"), image.mimeType);
377
+ const data = bytesToBase64(new Uint8Array(buffer));
378
+ return {
379
+ url: dataUriForImage(data, mimeType),
380
+ inlined: true,
381
+ mimeType,
382
+ data,
383
+ };
384
+ }
385
+ finally {
386
+ clearTimeout(timer);
387
+ options.signal?.removeEventListener("abort", onAbort);
388
+ }
389
+ }
390
+ /**
391
+ * Resolve an S3 presigned SOURCE url (typically a stale/expired one persisted
392
+ * in session history). A presigned url must NEVER reach the provider as
393
+ * `image_url`: once its signature expires every request fails with
394
+ * "Failed to fetch media from URL ... parameter=image_url" and the agent loop
395
+ * retries the doomed request 8x per turn. Order of attempts:
396
+ *
397
+ * 1. Map back to the stable hosted ref (`/generated-images/<id>`) and run the
398
+ * hosted path — it re-signs a FRESH presigned url and inlines the bytes.
399
+ * 2. Else fetch + inline the presigned bytes directly (the signature may
400
+ * still be valid).
401
+ * 3. Else DROP the image — never fall back to sending the presigned url.
402
+ */
403
+ async function resolvePresignedSourceImage(url, image, options) {
404
+ const hostedRef = hostedRefFromPresignedUrl(url);
405
+ if (hostedRef) {
406
+ const hosted = await resolveImageForProvider({ type: "image", url: hostedRef, mimeType: image.mimeType }, options);
407
+ // Only an INLINED result counts: the hosted path can also return the
408
+ // hosted url verbatim (auth-gated — the provider would get a 401),
409
+ // which is just as doomed as the expired presigned url.
410
+ if (hosted?.inlined)
411
+ return hosted;
412
+ }
413
+ try {
414
+ return await fetchAndInlineImage(url, image, undefined, options);
415
+ }
416
+ catch (error) {
417
+ const reason = error instanceof Error ? error.message : String(error);
418
+ options.onWarn?.(`Dropping image with stale/expired presigned url (${reason})`);
419
+ return null;
420
+ }
421
+ }
260
422
  /**
261
423
  * Resolve an image block into a provider-sendable url.
262
424
  *
263
425
  * Returns `null` when the block carries neither data nor url (nothing sendable).
264
426
  * Local/private urls are fetched and inlined; failures fall back to the raw
265
427
  * url so the caller still emits *something* rather than dropping the image.
428
+ * S3 presigned urls are never sent as-is — see `resolvePresignedSourceImage`.
266
429
  */
267
430
  export async function resolveImageForProvider(image, options = {}) {
268
431
  if (hasInlineImageData(image)) {
@@ -279,6 +442,12 @@ export async function resolveImageForProvider(image, options = {}) {
279
442
  let url = typeof image.url === "string" ? image.url.trim() : "";
280
443
  if (!url)
281
444
  return null;
445
+ // A presigned S3 url must NEVER reach the provider as `image_url` — route
446
+ // it through the hosted-ref path (fresh signature + inline) and drop the
447
+ // image when that fails too. See `resolvePresignedSourceImage`.
448
+ if (isS3PresignedUrl(url)) {
449
+ return resolvePresignedSourceImage(url, image, options);
450
+ }
282
451
  // Resolve the backend base url lazily, and ONLY when the url even looks
283
452
  // hosted (path-wise): public cdn urls must not pay for a config/machine
284
453
  // store read. The base url doubles as the origin an absolute url has to
@@ -349,59 +518,21 @@ export async function resolveImageForProvider(image, options = {}) {
349
518
  return { url, inlined: false, mimeType: image.mimeType };
350
519
  }
351
520
  const requestHeaders = authenticated ? authHeaders : undefined;
352
- const doFetch = options.fetchImpl ?? fetch;
353
- const timeoutMs = options.timeoutMs ?? DEFAULT_INLINE_FETCH_TIMEOUT_MS;
354
- const maxInlineBytes = options.maxInlineBytes ?? DEFAULT_MAX_INLINE_BYTES;
355
- const controller = new AbortController();
356
- const timer = setTimeout(() => controller.abort(), timeoutMs);
357
- const onAbort = () => controller.abort();
358
- options.signal?.addEventListener("abort", onAbort, { once: true });
359
521
  try {
360
- // An already-aborted signal never fires the listener above.
361
- if (options.signal?.aborted)
362
- throw new Error("aborted");
363
- // `requestHeaders` is only set when the (possibly rewritten) url still
364
- // resolves to the backend origin — a url on any other host is fetched
365
- // anonymously, so the JWT cannot leak to a third party.
366
- const response = await doFetch(downloadUrl, requestHeaders ? { signal: controller.signal, headers: requestHeaders } : { signal: controller.signal });
367
- if (!response.ok)
368
- throw new Error(`HTTP ${response.status}`);
369
- // Reject oversized payloads from the declared length BEFORE reading
370
- // the body — otherwise the whole payload lands in memory first.
371
- const declaredLength = response.headers?.get?.("content-length");
372
- if (declaredLength !== null && declaredLength !== undefined) {
373
- const declared = Number(declaredLength);
374
- if (Number.isFinite(declared) && declared > maxInlineBytes) {
375
- throw new Error(`image too large to inline (${declared} bytes)`);
376
- }
377
- }
378
- const buffer = await response.arrayBuffer();
379
- if (buffer.byteLength > maxInlineBytes) {
380
- throw new Error(`image too large to inline (${buffer.byteLength} bytes)`);
381
- }
382
- const mimeType = normalizeMimeType(response.headers?.get?.("content-type"), image.mimeType);
383
- const data = bytesToBase64(new Uint8Array(buffer));
384
- return {
385
- url: dataUriForImage(data, mimeType),
386
- inlined: true,
387
- mimeType,
388
- data,
389
- };
522
+ return await fetchAndInlineImage(downloadUrl, image, requestHeaders, options);
390
523
  }
391
524
  catch (error) {
392
525
  const reason = error instanceof Error ? error.message : String(error);
393
526
  options.onWarn?.(`Could not inline image ${presignedResolved ? downloadUrl : url} (${reason}); sending url as-is${hosted && !presignedResolved ? " — the provider may not be able to fetch it (auth required)" : ""}`);
394
527
  if (presignedResolved) {
395
- // The S3 download failed — degrade to the old verbatim behavior
396
- // (send the presigned url) rather than dropping the image.
397
- return { url: downloadUrl, inlined: false, mimeType: image.mimeType, hostedResolved: true };
528
+ // A presigned url — stale, expired or freshly minted — must NEVER be
529
+ // handed to the provider: OpenRouter's server-side media fetcher
530
+ // answers HTTP 400 on presigned urls and the agent loop retries the
531
+ // doomed request 8x per turn. Drop the image instead.
532
+ return null;
398
533
  }
399
534
  return { url, inlined: false, mimeType: image.mimeType };
400
535
  }
401
- finally {
402
- clearTimeout(timer);
403
- options.signal?.removeEventListener("abort", onAbort);
404
- }
405
536
  }
406
537
  function inlineImageContentBlock(block, resolved) {
407
538
  const image = block;
@@ -525,5 +656,11 @@ export function imageSourceUrlForProvider(image, baseUrl) {
525
656
  // something it can never fetch.
526
657
  if (isRelativeImageUrl(url))
527
658
  return undefined;
659
+ // A stale/expired S3 presigned url is unfetchable for the provider
660
+ // (HTTP 400 "parameter=image_url" on every request). The
661
+ // `resolveImagesForContext` pre-pass should have replaced it with inline
662
+ // bytes; if one still lands here, drop the block.
663
+ if (isS3PresignedUrl(url))
664
+ return undefined;
528
665
  return url;
529
666
  }
@@ -23,6 +23,10 @@ export { isRateLimitError } from "./rate-limit-retry.js";
23
23
  *
24
24
  * Rate limits (429) are intentionally NOT matched here; call sites must
25
25
  * check `isRateLimitError` first so they keep their own long schedule.
26
+ *
27
+ * Media-fetch failures ("Failed to fetch media from URL ...",
28
+ * "parameter=image_url") are classified NON-transient: the provider fails
29
+ * the same way on every retry, so the request must fail fast.
26
30
  */
27
31
  export declare function isTransientApiError(message: string): boolean;
28
32
  //# sourceMappingURL=transient-error.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"transient-error.d.ts","sourceRoot":"","sources":["../../../../src/sdk/ai/utils/transient-error.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AA8BzD;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAE5D"}
1
+ {"version":3,"file":"transient-error.d.ts","sourceRoot":"","sources":["../../../../src/sdk/ai/utils/transient-error.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAwCzD;;;;;;;;;;;GAWG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAG5D"}
@@ -42,6 +42,15 @@ export { isRateLimitError } from "./rate-limit-retry.js";
42
42
  * "rate limit" / "too many requests".
43
43
  */
44
44
  const TRANSIENT_API_ERROR_RE = /overloaded|\b529\b|timeout|timed?.?out|network|econnreset|fetch failed|socket connection was closed|connection.?closed|connection.?reset|connection.?error|connection.?refused|connection.?lost|socket hang up|websocket.?closed|websocket.?error|other side closed|reset before headers|upstream.?connect|http2 request did not get a response|provider.?returned.?error|ended without|stream ended before message_stop|terminated|retry delay|\b5(?:00|02|03|04)\b|unavailable|temporarily|internal.?error|server.?error/i;
45
+ /**
46
+ * Provider media-fetch failures are PERMANENT, not transient: a provider that
47
+ * cannot fetch a media url (stale/expired S3 presigned url, 401-gated backend
48
+ * ref, ...) fails the SAME way on every retry — retrying only burns 8 rounds
49
+ * per turn ("[Retrying after transient error (N/8): ...]" spam in old/long
50
+ * sessions). Classified as non-transient so a doomed image request fails
51
+ * fast once instead of being retried.
52
+ */
53
+ const NON_TRANSIENT_MEDIA_ERROR_RE = /failed to fetch media|parameter=image_url/i;
45
54
  /**
46
55
  * Check whether an error message (which may embed a raw provider JSON blob)
47
56
  * describes a transient API failure worth retrying — overload, HTTP 5xx,
@@ -49,7 +58,13 @@ const TRANSIENT_API_ERROR_RE = /overloaded|\b529\b|timeout|timed?.?out|network|e
49
58
  *
50
59
  * Rate limits (429) are intentionally NOT matched here; call sites must
51
60
  * check `isRateLimitError` first so they keep their own long schedule.
61
+ *
62
+ * Media-fetch failures ("Failed to fetch media from URL ...",
63
+ * "parameter=image_url") are classified NON-transient: the provider fails
64
+ * the same way on every retry, so the request must fail fast.
52
65
  */
53
66
  export function isTransientApiError(message) {
67
+ if (NON_TRANSIENT_MEDIA_ERROR_RE.test(message))
68
+ return false;
54
69
  return TRANSIENT_API_ERROR_RE.test(message);
55
70
  }