@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.
- package/dist/extensions/dataset-uploader/analyzer.d.ts +103 -0
- package/dist/extensions/dataset-uploader/analyzer.d.ts.map +1 -0
- package/dist/extensions/dataset-uploader/analyzer.js +266 -0
- package/dist/extensions/dataset-uploader/cleaner.d.ts +59 -0
- package/dist/extensions/dataset-uploader/cleaner.d.ts.map +1 -0
- package/dist/extensions/dataset-uploader/cleaner.js +118 -0
- package/dist/extensions/dataset-uploader/exporter.d.ts +147 -0
- package/dist/extensions/dataset-uploader/exporter.d.ts.map +1 -0
- package/dist/extensions/dataset-uploader/exporter.js +313 -0
- package/dist/extensions/dataset-uploader/extra-values.d.ts +17 -0
- package/dist/extensions/dataset-uploader/extra-values.d.ts.map +1 -0
- package/dist/extensions/dataset-uploader/extra-values.js +51 -0
- package/dist/extensions/dataset-uploader/index.d.ts +22 -0
- package/dist/extensions/dataset-uploader/index.d.ts.map +1 -0
- package/dist/extensions/dataset-uploader/index.js +247 -0
- package/dist/extensions/dataset-uploader/scrubber.d.ts +34 -0
- package/dist/extensions/dataset-uploader/scrubber.d.ts.map +1 -0
- package/dist/extensions/dataset-uploader/scrubber.js +91 -0
- package/dist/extensions/dataset-uploader/upload.d.ts +105 -0
- package/dist/extensions/dataset-uploader/upload.d.ts.map +1 -0
- package/dist/extensions/dataset-uploader/upload.js +274 -0
- package/dist/generated/zeus/const.d.ts.map +1 -1
- package/dist/generated/zeus/const.js +80 -6
- package/dist/generated/zeus/index.d.ts +432 -36
- package/dist/generated/zeus/index.d.ts.map +1 -1
- package/dist/mcp/commands.d.ts.map +1 -1
- package/dist/mcp/commands.js +3 -0
- package/dist/mcp/direct-tools.d.ts.map +1 -1
- package/dist/mcp/direct-tools.js +4 -3
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +5 -1
- package/dist/mcp/mcp-auth-flow.d.ts +5 -1
- package/dist/mcp/mcp-auth-flow.d.ts.map +1 -1
- package/dist/mcp/mcp-auth-flow.js +20 -4
- package/dist/mcp/mcp-auth-start.d.ts +64 -0
- package/dist/mcp/mcp-auth-start.d.ts.map +1 -0
- package/dist/mcp/mcp-auth-start.js +136 -0
- package/dist/mcp/proxy-modes.d.ts +7 -0
- package/dist/mcp/proxy-modes.d.ts.map +1 -1
- package/dist/mcp/proxy-modes.js +71 -11
- package/dist/mcp/server-manager.d.ts +2 -1
- package/dist/mcp/server-manager.d.ts.map +1 -1
- package/dist/mcp/server-manager.js +8 -4
- package/dist/memory/commands/status.d.ts.map +1 -1
- package/dist/memory/commands/status.js +2 -1
- package/dist/memory/hooks/compaction-trigger.d.ts +14 -1
- package/dist/memory/hooks/compaction-trigger.d.ts.map +1 -1
- package/dist/memory/hooks/compaction-trigger.js +31 -4
- package/dist/relay/dispatcher.d.ts.map +1 -1
- package/dist/relay/dispatcher.js +4 -47
- package/dist/relay/routing.d.ts +1 -1
- package/dist/relay/routing.d.ts.map +1 -1
- package/dist/relay/routing.js +9 -0
- package/dist/sdk/ai/utils/image-source.d.ts +26 -11
- package/dist/sdk/ai/utils/image-source.d.ts.map +1 -1
- package/dist/sdk/ai/utils/image-source.js +184 -47
- package/dist/sdk/ai/utils/transient-error.d.ts +4 -0
- package/dist/sdk/ai/utils/transient-error.d.ts.map +1 -1
- package/dist/sdk/ai/utils/transient-error.js +15 -0
- package/dist/sdk/coding-agent/core/compaction/policy.d.ts +2 -0
- package/dist/sdk/coding-agent/core/compaction/policy.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/compaction/policy.js +2 -0
- package/dist/sdk/coding-agent/core/extensions/native-extensions.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/extensions/native-extensions.js +10 -0
- package/dist/sdk/coding-agent/core/system-prompt.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/system-prompt.js +4 -0
- package/dist/sdk/coding-agent/core/tools/read.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/tools/read.js +14 -7
- package/dist/sdk/coding-agent/index.d.ts +1 -1
- package/dist/sdk/coding-agent/index.d.ts.map +1 -1
- package/dist/sdk/coding-agent/index.js +1 -1
- package/dist/sdk/coding-agent/utils/image-resize.d.ts +25 -11
- package/dist/sdk/coding-agent/utils/image-resize.d.ts.map +1 -1
- package/dist/sdk/coding-agent/utils/image-resize.js +54 -36
- package/dist/sdk/coding-agent/utils/photon.d.ts.map +1 -1
- package/dist/sdk/coding-agent/utils/photon.js +11 -2
- package/dist/server/agent-bridge.d.ts +32 -0
- package/dist/server/agent-bridge.d.ts.map +1 -1
- package/dist/server/agent-bridge.js +59 -9
- package/dist/server/handlers/mcp-auth.d.ts +28 -0
- package/dist/server/handlers/mcp-auth.d.ts.map +1 -0
- package/dist/server/handlers/mcp-auth.js +41 -0
- package/dist/server/handlers/mcp-status.d.ts +8 -2
- package/dist/server/handlers/mcp-status.d.ts.map +1 -1
- package/dist/server/handlers/mcp-status.js +15 -3
- package/dist/server/preset-chain.d.ts +0 -13
- package/dist/server/preset-chain.d.ts.map +1 -1
- package/dist/server/preset-chain.js +0 -25
- package/dist/server/session-stream/memory.d.ts.map +1 -1
- package/dist/server/session-stream/memory.js +8 -1
- package/dist/server/session-stream.d.ts +15 -56
- package/dist/server/session-stream.d.ts.map +1 -1
- package/dist/server/session-stream.js +41 -271
- package/dist/server/wire.d.ts +18 -32
- package/dist/server/wire.d.ts.map +1 -1
- 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
|
-
|
|
13
|
-
|
|
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":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AAEH,OAAO,KAAK,EACV,iBAAiB,EACjB,iBAAiB,EACjB,eAAe,EACf,kBAAkB,EAClB,cAAc,EACd,iBAAiB,EAClB,MAAM,uBAAuB,CAAC;AAE/B,OAAO,KAAK,EAEV,oBAAoB,EACpB,UAAU,EACX,MAAM,6BAA6B,CAAC;AAErC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAEzD,OAAO,EACL,YAAY,EACZ,SAAS,EACT,6BAA6B,EAC9B,MAAM,gCAAgC,CAAC;AAExC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAS/C,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,YAAY,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AACtE,OAAO,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAC9C,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AACrD,YAAY,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAM3D;;;;;;;;;GASG;AACH,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,6BAA6B,EAAE,CAAC;AAMlE,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,oBAAoB,CAAC;IAC9B,2DAA2D;IAC3D,KAAK,EAAE,IAAI,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC;IACjC;;;;;;;;;OASG;IACH,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IACrC,yEAAyE;IACzE,MAAM,CAAC,EAAE;QAAE,KAAK,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,CAAA;KAAE,CAAC;CAClD;
|
|
1
|
+
{"version":3,"file":"dispatcher.d.ts","sourceRoot":"","sources":["../../src/relay/dispatcher.ts"],"names":[],"mappings":"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"}
|
package/dist/relay/dispatcher.js
CHANGED
|
@@ -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
|
|
231
|
-
if (
|
|
228
|
+
const intervalControl = message.intervalControl;
|
|
229
|
+
if (intervalControl === "stop") {
|
|
232
230
|
manager.stopInterval(sessionId);
|
|
233
231
|
return;
|
|
234
232
|
}
|
|
235
|
-
if (
|
|
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,
|
package/dist/relay/routing.d.ts
CHANGED
|
@@ -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":"AAAA;;;;;;;;;;GAUG;AAIH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"routing.d.ts","sourceRoot":"","sources":["../../src/relay/routing.ts"],"names":[],"mappings":"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"}
|
package/dist/relay/routing.js
CHANGED
|
@@ -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
|
|
18
|
-
* fails
|
|
19
|
-
*
|
|
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":"AAAA
|
|
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
|
|
18
|
-
* fails
|
|
19
|
-
*
|
|
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
|
-
|
|
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
|
-
//
|
|
396
|
-
//
|
|
397
|
-
|
|
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;
|
|
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
|
}
|