cascade-ai 0.50.0 → 0.52.0

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/index.js CHANGED
@@ -1845,7 +1845,7 @@ function reconcilePrice(model, live) {
1845
1845
  }
1846
1846
 
1847
1847
  // src/constants.ts
1848
- var CASCADE_VERSION = "0.50.0";
1848
+ var CASCADE_VERSION = "0.52.0";
1849
1849
  var CASCADE_CONFIG_DIR = ".cascade";
1850
1850
  var CASCADE_MD_FILE = "CASCADE.md";
1851
1851
  var CASCADE_IGNORE_FILE = ".cascadeignore";
@@ -2358,46 +2358,37 @@ var BaseProvider = class {
2358
2358
  };
2359
2359
 
2360
2360
  // src/providers/model-filter.ts
2361
- var NON_CHAT_PATTERNS = [
2362
- "embed",
2361
+ var MODALITY_PATTERNS = [
2362
+ ["embedding", ["embed"]],
2363
2363
  // text-embedding-3, nomic-embed, mxbai-embed, snowflake-arctic-embed
2364
- "(?:^|[-_/])tts(?:[-_/]|$)",
2365
- "text-to-speech",
2366
- "speech",
2367
- // text-to-speech
2368
- "whisper",
2369
- "transcrib",
2370
- "(?:^|[-_/])stt(?:[-_/]|$)",
2371
- // speech-to-text
2372
- "dall[-]?e",
2373
- "imagen",
2374
- "image-generation",
2375
- "gpt-image",
2376
- "(?:^|[-_/])image(?:[-_/]|$)",
2377
- "stable-diffusion",
2378
- "sdxl",
2379
- "flux",
2380
- // image gen
2381
- "moderation",
2382
- "(?:^|[-_/])guard(?:[-_/]|$)",
2383
- // moderation / safety
2384
- "rerank",
2385
- "(?:^|[-_/])audio(?:[-_/]|$)",
2386
- "native-audio",
2387
- "realtime",
2388
- // audio / realtime
2389
- "veo",
2390
- "sora",
2391
- "(?:^|[-_/])video(?:[-_/]|$)",
2392
- // video
2393
- "(?:^|[-_/])aqa(?:[-_/]|$)",
2394
- // attributed QA
2395
- "(?:^|[-_/])live(?:[-_/]|$)",
2396
- // live-audio variants
2397
- "(?:^|[-_/])(?:davinci|babbage)(?:[-_/]|$)"
2398
- // legacy completion-only base models
2364
+ ["transcription", ["whisper", "transcrib", "(?:^|[-_/])stt(?:[-_/]|$)"]],
2365
+ ["speech", ["(?:^|[-_/])tts(?:[-_/]|$)", "text-to-speech", "speech"]],
2366
+ ["video", ["veo", "sora", "(?:^|[-_/])video(?:[-_/]|$)"]],
2367
+ ["image", [
2368
+ "dall[-]?e",
2369
+ "imagen",
2370
+ "image-generation",
2371
+ "gpt-image",
2372
+ "(?:^|[-_/])image(?:[-_/]|$)",
2373
+ "stable-diffusion",
2374
+ "sdxl",
2375
+ "flux"
2376
+ ]],
2377
+ ["moderation", ["moderation", "(?:^|[-_/])guard(?:[-_/]|$)"]],
2378
+ ["rerank", ["rerank"]],
2379
+ ["realtime", [
2380
+ "(?:^|[-_/])audio(?:[-_/]|$)",
2381
+ "native-audio",
2382
+ "realtime",
2383
+ "(?:^|[-_/])live(?:[-_/]|$)",
2384
+ // live-audio variants
2385
+ "(?:^|[-_/])aqa(?:[-_/]|$)"
2386
+ // attributed QA
2387
+ ]],
2388
+ ["legacy-completion", ["(?:^|[-_/])(?:davinci|babbage)(?:[-_/]|$)"]]
2399
2389
  ];
2400
- var NON_CHAT = new RegExp(NON_CHAT_PATTERNS.join("|"), "i");
2390
+ MODALITY_PATTERNS.map(([m, pats]) => [m, new RegExp(pats.join("|"), "i")]);
2391
+ var NON_CHAT = new RegExp(MODALITY_PATTERNS.flatMap(([, p]) => p).join("|"), "i");
2401
2392
  function isChatModel(id, methods) {
2402
2393
  if (methods && methods.length > 0) {
2403
2394
  const canGenerate = methods.some((m) => /generatecontent|generatemessage|chat|completion/i.test(m));
@@ -2925,6 +2916,115 @@ var AzureOpenAIProvider = class extends OpenAIProvider {
2925
2916
  }
2926
2917
  }
2927
2918
  };
2919
+
2920
+ // src/providers/gemini-schema.ts
2921
+ var ALLOWED_KEYS = /* @__PURE__ */ new Set([
2922
+ "type",
2923
+ "format",
2924
+ "title",
2925
+ "description",
2926
+ "nullable",
2927
+ "enum",
2928
+ "items",
2929
+ "properties",
2930
+ "required",
2931
+ "propertyOrdering",
2932
+ "minItems",
2933
+ "maxItems",
2934
+ "minProperties",
2935
+ "maxProperties",
2936
+ "minimum",
2937
+ "maximum",
2938
+ "minLength",
2939
+ "maxLength",
2940
+ "pattern",
2941
+ "example",
2942
+ "default",
2943
+ "anyOf"
2944
+ ]);
2945
+ var ALLOWED_TYPES = /* @__PURE__ */ new Set(["string", "number", "integer", "boolean", "array", "object"]);
2946
+ function isPlainObject(v) {
2947
+ return typeof v === "object" && v !== null && !Array.isArray(v);
2948
+ }
2949
+ function normalizeType(value) {
2950
+ if (typeof value === "string") {
2951
+ return ALLOWED_TYPES.has(value) ? { type: value } : {};
2952
+ }
2953
+ if (Array.isArray(value)) {
2954
+ const types = value.filter((t) => typeof t === "string");
2955
+ const nullable = types.includes("null");
2956
+ const concrete = types.find((t) => t !== "null" && ALLOWED_TYPES.has(t));
2957
+ return {
2958
+ ...concrete ? { type: concrete } : {},
2959
+ ...nullable ? { nullable: true } : {}
2960
+ };
2961
+ }
2962
+ return {};
2963
+ }
2964
+ function normalizeEnum(value) {
2965
+ if (!Array.isArray(value) || value.length === 0) return void 0;
2966
+ return value.map((v) => typeof v === "string" ? v : JSON.stringify(v));
2967
+ }
2968
+ function sanitizeGeminiSchema(schema) {
2969
+ if (!isPlainObject(schema)) return void 0;
2970
+ const out = {};
2971
+ for (const [key, value] of Object.entries(schema)) {
2972
+ if (!ALLOWED_KEYS.has(key)) continue;
2973
+ switch (key) {
2974
+ case "type": {
2975
+ Object.assign(out, normalizeType(value));
2976
+ break;
2977
+ }
2978
+ case "enum": {
2979
+ const e = normalizeEnum(value);
2980
+ if (e) out["enum"] = e;
2981
+ break;
2982
+ }
2983
+ case "properties": {
2984
+ if (!isPlainObject(value)) break;
2985
+ const props = {};
2986
+ for (const [propName, propSchema] of Object.entries(value)) {
2987
+ const cleaned = sanitizeGeminiSchema(propSchema);
2988
+ props[propName] = cleaned && Object.keys(cleaned).length ? cleaned : { type: "string" };
2989
+ }
2990
+ out["properties"] = props;
2991
+ break;
2992
+ }
2993
+ case "items": {
2994
+ const cleaned = sanitizeGeminiSchema(value);
2995
+ if (cleaned) out["items"] = cleaned;
2996
+ break;
2997
+ }
2998
+ case "anyOf": {
2999
+ if (!Array.isArray(value)) break;
3000
+ const variants = value.map((v) => sanitizeGeminiSchema(v)).filter((v) => !!v && Object.keys(v).length > 0);
3001
+ if (variants.length) out["anyOf"] = variants;
3002
+ break;
3003
+ }
3004
+ case "required": {
3005
+ if (Array.isArray(value)) {
3006
+ out["required"] = value.filter((r) => typeof r === "string");
3007
+ }
3008
+ break;
3009
+ }
3010
+ default:
3011
+ out[key] = value;
3012
+ }
3013
+ }
3014
+ if (!out["type"] && out["properties"]) out["type"] = "object";
3015
+ if (!out["type"] && out["items"]) out["type"] = "array";
3016
+ return out;
3017
+ }
3018
+ function toGeminiParameters(inputSchema) {
3019
+ const cleaned = sanitizeGeminiSchema(inputSchema);
3020
+ if (!cleaned) return void 0;
3021
+ const props = cleaned["properties"];
3022
+ if (isPlainObject(props) && Object.keys(props).length === 0) return void 0;
3023
+ if (Object.keys(cleaned).length === 0) return void 0;
3024
+ return cleaned;
3025
+ }
3026
+
3027
+ // src/providers/gemini.ts
2928
3028
  var GeminiProvider = class extends BaseProvider {
2929
3029
  client;
2930
3030
  constructor(config, model) {
@@ -3145,7 +3245,12 @@ var GeminiProvider = class extends BaseProvider {
3145
3245
  return {
3146
3246
  name: tool.name,
3147
3247
  description: tool.description,
3148
- parameters: tool.inputSchema
3248
+ // Convert, never cast. Gemini's parameters field is an OpenAPI subset
3249
+ // that 400s on unknown keys, and MCP servers ship schemas carrying their
3250
+ // own extensions (GitHub's adds `x-mcp-header` to every header-bound
3251
+ // property). Casting sent those through verbatim and every request failed
3252
+ // before the model saw it. See providers/gemini-schema.ts.
3253
+ parameters: toGeminiParameters(tool.inputSchema)
3149
3254
  };
3150
3255
  }
3151
3256
  };
@@ -3173,7 +3278,7 @@ async function nodeHttpFetch(input, init = {}, redirectCount = 0) {
3173
3278
  headers["accept-encoding"] = "gzip, deflate, br";
3174
3279
  }
3175
3280
  const body = init.body == null ? void 0 : typeof init.body === "string" ? init.body : Buffer.from(init.body);
3176
- return new Promise((resolve, reject) => {
3281
+ return new Promise((resolve2, reject) => {
3177
3282
  const req = lib.request(
3178
3283
  {
3179
3284
  hostname: u.hostname,
@@ -3190,7 +3295,7 @@ async function nodeHttpFetch(input, init = {}, redirectCount = 0) {
3190
3295
  const nextUrl = new URL(location, u).href;
3191
3296
  const downgrade = status === 303 || (status === 301 || status === 302) && method !== "GET" && method !== "HEAD";
3192
3297
  const nextInit = downgrade ? { ...init, method: "GET", body: void 0 } : init;
3193
- resolve(nodeHttpFetch(nextUrl, nextInit, redirectCount + 1));
3298
+ resolve2(nodeHttpFetch(nextUrl, nextInit, redirectCount + 1));
3194
3299
  return;
3195
3300
  }
3196
3301
  const encoding = (res.headers["content-encoding"] ?? "").toLowerCase();
@@ -3205,7 +3310,7 @@ async function nodeHttpFetch(input, init = {}, redirectCount = 0) {
3205
3310
  if (Array.isArray(v)) respHeaders.set(k, v.join(", "));
3206
3311
  else if (typeof v === "string") respHeaders.set(k, v);
3207
3312
  }
3208
- resolve(new Response(stream, {
3313
+ resolve2(new Response(stream, {
3209
3314
  status,
3210
3315
  statusText: res.statusMessage ?? "",
3211
3316
  headers: respHeaders
@@ -3850,7 +3955,7 @@ var TpmLimiter = class {
3850
3955
  }
3851
3956
  const deficit = want - bucket.available;
3852
3957
  const waitMs = Math.max(50, Math.ceil(deficit / bucket.tokensPerMinute * 6e4));
3853
- await new Promise((resolve) => setTimeout(resolve, waitMs));
3958
+ await new Promise((resolve2) => setTimeout(resolve2, waitMs));
3854
3959
  }
3855
3960
  }
3856
3961
  /**
@@ -3913,14 +4018,14 @@ var LocalRequestQueue = class {
3913
4018
  this.active++;
3914
4019
  return this.makeRelease();
3915
4020
  }
3916
- return new Promise((resolve, reject) => {
4021
+ return new Promise((resolve2, reject) => {
3917
4022
  let settled = false;
3918
4023
  let timer;
3919
4024
  const resolver = (release) => {
3920
4025
  if (settled) return;
3921
4026
  settled = true;
3922
4027
  if (timer !== void 0) clearTimeout(timer);
3923
- resolve(release);
4028
+ resolve2(release);
3924
4029
  };
3925
4030
  if (timeoutMs !== void 0 && timeoutMs > 0) {
3926
4031
  timer = setTimeout(() => {
@@ -4551,6 +4656,7 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
4551
4656
  tpmLimiter;
4552
4657
  localQueue;
4553
4658
  taskAnalyzer;
4659
+ pendingFeedbackSource;
4554
4660
  worldStateDB;
4555
4661
  privacyPaths;
4556
4662
  guidanceQueue;
@@ -5005,8 +5111,18 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
5005
5111
  return windows.length ? Math.min(...windows) : void 0;
5006
5112
  }
5007
5113
  /** Wire the Cascade Auto task analyzer used for per-subtask model routing. */
5114
+ /**
5115
+ * Feed per-model thumbs counts into Auto routing. Forwarded to the analyzer
5116
+ * because that is where the public benchmark score is combined; the prior
5117
+ * adjusts that number rather than sitting anywhere else in the pipeline.
5118
+ */
5119
+ setFeedbackSource(source) {
5120
+ this.taskAnalyzer?.setFeedbackSource(source);
5121
+ this.pendingFeedbackSource = source;
5122
+ }
5008
5123
  setTaskAnalyzer(analyzer) {
5009
5124
  this.taskAnalyzer = analyzer;
5125
+ if (this.pendingFeedbackSource) analyzer.setFeedbackSource(this.pendingFeedbackSource);
5010
5126
  }
5011
5127
  setWorldStateDB(db) {
5012
5128
  this.worldStateDB = db;
@@ -6100,8 +6216,11 @@ Now execute your subtask using this context where relevant.`
6100
6216
  const result = await this.runAgentLoop(systemPrompt, this.tools);
6101
6217
  output = result.output;
6102
6218
  toolCalls = result.toolCalls;
6103
- this.sendStatusUpdate({ progressPct: 65, currentAction: "Verifying required artifacts", status: "IN_PROGRESS" });
6104
- const artifactCheck = await this.verifyArtifacts(assignment);
6219
+ const mustProduceArtifact = this.requiresArtifact();
6220
+ if (mustProduceArtifact) {
6221
+ this.sendStatusUpdate({ progressPct: 65, currentAction: "Verifying required artifacts", status: "IN_PROGRESS" });
6222
+ }
6223
+ const artifactCheck = mustProduceArtifact ? await this.verifyArtifacts(assignment) : { ok: true, issues: [] };
6105
6224
  if (!artifactCheck.ok) {
6106
6225
  correctionAttempts = 1;
6107
6226
  issues.push(...artifactCheck.issues);
@@ -6381,11 +6500,11 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
6381
6500
  if (!wasApproved) return `Tool ${tc.name} was denied by user.`;
6382
6501
  } else {
6383
6502
  const LEGACY_APPROVAL_TIMEOUT_MS = 6e5;
6384
- const legacyDecision = await new Promise((resolve) => {
6503
+ const legacyDecision = await new Promise((resolve2) => {
6385
6504
  const eventName = `tool:approval-response:${this.id}-${tc.id}`;
6386
6505
  const timer = setTimeout(() => {
6387
6506
  this.removeAllListeners(eventName);
6388
- resolve({ approved: false });
6507
+ resolve2({ approved: false });
6389
6508
  }, LEGACY_APPROVAL_TIMEOUT_MS);
6390
6509
  timer.unref?.();
6391
6510
  this.emit("tool:approval-request", {
@@ -6398,7 +6517,7 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
6398
6517
  });
6399
6518
  this.once(eventName, (d) => {
6400
6519
  clearTimeout(timer);
6401
- resolve(d);
6520
+ resolve2(d);
6402
6521
  });
6403
6522
  });
6404
6523
  if (legacyDecision.always) this.sessionApprovals.set(tc.name, legacyDecision.approved);
@@ -6418,6 +6537,9 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
6418
6537
  tierId: this.id,
6419
6538
  sessionId: this.taskId,
6420
6539
  requireApproval: false,
6540
+ // Media generation can run for a minute; without this a cancelled run
6541
+ // still pays for an image nobody will see.
6542
+ ...this.signal ? { signal: this.signal } : {},
6421
6543
  saveSnapshot: async (path26, content) => {
6422
6544
  this.store?.addFileSnapshot(this.taskId, path26, content);
6423
6545
  },
@@ -6465,7 +6587,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
6465
6587
  const result = await this.toolRegistry.execute(altTool, tc.input, {
6466
6588
  tierId: this.id,
6467
6589
  sessionId: this.taskId,
6468
- requireApproval: false
6590
+ requireApproval: false,
6591
+ ...this.signal ? { signal: this.signal } : {}
6469
6592
  });
6470
6593
  const str = typeof result === "string" ? result : JSON.stringify(result);
6471
6594
  if (!str.startsWith("Tool error:") && !str.startsWith("Error:")) {
@@ -6487,7 +6610,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
6487
6610
  const result = await this.toolRegistry.execute(newToolName, tc.input, {
6488
6611
  tierId: this.id,
6489
6612
  sessionId: this.taskId,
6490
- requireApproval: false
6613
+ requireApproval: false,
6614
+ ...this.signal ? { signal: this.signal } : {}
6491
6615
  });
6492
6616
  const str = typeof result === "string" ? result : JSON.stringify(result);
6493
6617
  if (!str.startsWith("Tool error:")) return `[Synthesized ${newToolName}]: ${str}`;
@@ -6905,7 +7029,7 @@ var PeerBus = class extends EventEmitter {
6905
7029
  this.outputs.set(subtaskId, entry);
6906
7030
  this.emit("output:ready", entry);
6907
7031
  const waiting = this.waiters.get(subtaskId) ?? [];
6908
- for (const resolve of waiting) resolve(entry);
7032
+ for (const resolve2 of waiting) resolve2(entry);
6909
7033
  this.waiters.delete(subtaskId);
6910
7034
  }
6911
7035
  /**
@@ -6936,10 +7060,10 @@ var PeerBus = class extends EventEmitter {
6936
7060
  waitFor(subtaskId, timeoutMs = 12e4) {
6937
7061
  const existing = this.outputs.get(subtaskId);
6938
7062
  if (existing && !this.retryPending.has(subtaskId)) return Promise.resolve(existing);
6939
- return new Promise((resolve, reject) => {
7063
+ return new Promise((resolve2, reject) => {
6940
7064
  const resolver = (output) => {
6941
7065
  clearTimeout(timer);
6942
- resolve(output);
7066
+ resolve2(output);
6943
7067
  };
6944
7068
  const timer = setTimeout(() => {
6945
7069
  const waiting = this.waiters.get(subtaskId);
@@ -6993,10 +7117,10 @@ var PeerBus = class extends EventEmitter {
6993
7117
  */
6994
7118
  collect(timeoutMs) {
6995
7119
  const collected = [...this.broadcastLog];
6996
- return new Promise((resolve) => {
7120
+ return new Promise((resolve2) => {
6997
7121
  const timer = setTimeout(() => {
6998
7122
  off();
6999
- resolve(collected);
7123
+ resolve2(collected);
7000
7124
  }, timeoutMs);
7001
7125
  const handler = (msg) => {
7002
7126
  collected.push({ fromId: msg.fromId, payload: msg.payload, timestamp: msg.timestamp });
@@ -7010,7 +7134,7 @@ var PeerBus = class extends EventEmitter {
7010
7134
  const broadcasters = new Set(collected.map((m) => m.fromId));
7011
7135
  if (broadcasters.size >= this.members.size) {
7012
7136
  off();
7013
- resolve(collected);
7137
+ resolve2(collected);
7014
7138
  }
7015
7139
  };
7016
7140
  this.on("broadcast", checkComplete);
@@ -7028,7 +7152,7 @@ var PeerBus = class extends EventEmitter {
7028
7152
  return;
7029
7153
  }
7030
7154
  this.emitCoordination(tierId, `\u23F3 waiting for ${filePath} (held by ${existing.holderId})`);
7031
- return new Promise((resolve, reject) => {
7155
+ return new Promise((resolve2, reject) => {
7032
7156
  const timer = setTimeout(() => {
7033
7157
  reject(new Error(`File lock timeout for ${filePath} (held by ${existing.holderId})`));
7034
7158
  }, timeoutMs);
@@ -7036,7 +7160,7 @@ var PeerBus = class extends EventEmitter {
7036
7160
  clearTimeout(timer);
7037
7161
  this.fileLocks.set(filePath, { holderId: tierId, lockedAt: (/* @__PURE__ */ new Date()).toISOString(), waiters: [] });
7038
7162
  this.emitCoordination(tierId, `\u{1F512} locked ${filePath}`);
7039
- resolve();
7163
+ resolve2();
7040
7164
  });
7041
7165
  });
7042
7166
  }
@@ -7061,11 +7185,11 @@ var PeerBus = class extends EventEmitter {
7061
7185
  */
7062
7186
  waitForFileRelease(filePath, timeoutMs = 3e4) {
7063
7187
  if (!this.fileLocks.has(filePath)) return Promise.resolve();
7064
- return new Promise((resolve, reject) => {
7188
+ return new Promise((resolve2, reject) => {
7065
7189
  const timer = setTimeout(() => reject(new Error(`Timeout waiting for file release: ${filePath}`)), timeoutMs);
7066
7190
  this.once(`file:released:${filePath}`, () => {
7067
7191
  clearTimeout(timer);
7068
- resolve();
7192
+ resolve2();
7069
7193
  });
7070
7194
  });
7071
7195
  }
@@ -7131,13 +7255,13 @@ var PeerBus = class extends EventEmitter {
7131
7255
  this.emit(`barrier:${barrierName}`);
7132
7256
  return;
7133
7257
  }
7134
- return new Promise((resolve, reject) => {
7258
+ return new Promise((resolve2, reject) => {
7135
7259
  const timer = setTimeout(() => {
7136
7260
  reject(new Error(`Barrier timeout: ${barrierName} (${bar.arrived.size}/${bar.total} arrived)`));
7137
7261
  }, 12e4);
7138
7262
  this.once(`barrier:${barrierName}`, () => {
7139
7263
  clearTimeout(timer);
7140
- resolve();
7264
+ resolve2();
7141
7265
  });
7142
7266
  });
7143
7267
  }
@@ -8682,6 +8806,170 @@ Reply with exactly one word: YES, NO, or UNSURE.
8682
8806
  }
8683
8807
  };
8684
8808
 
8809
+ // src/core/multimodal/registry.ts
8810
+ var CAPABILITIES = [
8811
+ // ── Image ──
8812
+ {
8813
+ modality: "image",
8814
+ provider: "openai",
8815
+ modelId: "dall-e-3",
8816
+ api: "openai-images"
8817
+ },
8818
+ {
8819
+ modality: "image",
8820
+ provider: "gemini",
8821
+ modelId: "gemini-3-pro-image",
8822
+ api: "gemini-predict"
8823
+ },
8824
+ // ── Speech (text → audio) ──
8825
+ {
8826
+ modality: "speech",
8827
+ provider: "openai",
8828
+ modelId: "tts-1",
8829
+ api: "openai-speech"
8830
+ },
8831
+ {
8832
+ // The only quality claim in this table that needs no leaderboard: OpenAI
8833
+ // ships these as the same voice at two fidelities and charges double for
8834
+ // this one. That is the vendor's own ordering, not an inference.
8835
+ modality: "speech",
8836
+ provider: "openai",
8837
+ modelId: "tts-1-hd",
8838
+ api: "openai-speech",
8839
+ quality: 70,
8840
+ qualityBasis: "OpenAI's own hd variant of tts-1, priced 2x"
8841
+ },
8842
+ // ── Transcription (audio → text) ──
8843
+ {
8844
+ modality: "transcription",
8845
+ provider: "openai",
8846
+ modelId: "whisper-1",
8847
+ api: "openai-transcriptions"
8848
+ },
8849
+ // ── Video ──
8850
+ {
8851
+ // Long-running: submit returns an operation name, the video lands minutes
8852
+ // later, and the bytes come from a short-lived signed URL. generate.ts owns
8853
+ // that polling loop; the registry only records the shape via `api`.
8854
+ modality: "video",
8855
+ provider: "gemini",
8856
+ modelId: "veo-3.1-generate-001",
8857
+ api: "gemini-predict-lro"
8858
+ }
8859
+ ];
8860
+ function capabilityCost(cap) {
8861
+ const match = findPricing(cap.modelId, cap.provider, {
8862
+ fallbackProviders: cap.provider === "gemini" ? ["vertex"] : []
8863
+ });
8864
+ const rate = match?.entry.rates?.[0];
8865
+ if (!rate) return null;
8866
+ const unitLabel = {
8867
+ per_image: "per image",
8868
+ per_second: "per second of video",
8869
+ per_character: "per character",
8870
+ per_audio_minute: "per audio minute"
8871
+ };
8872
+ const unit = unitLabel[rate.unit] ?? rate.unit;
8873
+ const amount = rate.amount;
8874
+ const shown = amount < 1e-3 ? amount.toExponential(2) : amount.toFixed(4);
8875
+ return { label: `$${shown} ${unit}`, amount, unit };
8876
+ }
8877
+ var MultimodalRegistry = class {
8878
+ /** Provider types the user has actually configured a key for. */
8879
+ available;
8880
+ constructor(availableProviders) {
8881
+ this.available = new Set(availableProviders);
8882
+ }
8883
+ /** Every capability whose provider is configured AND which Cascade can call. */
8884
+ usable(modality) {
8885
+ return CAPABILITIES.filter(
8886
+ (c) => !c.unsupported && this.available.has(c.provider) && (!modality || c.modality === modality)
8887
+ );
8888
+ }
8889
+ /** Modalities this account can actually generate right now. */
8890
+ availableModalities() {
8891
+ return [...new Set(this.usable().map((c) => c.modality))];
8892
+ }
8893
+ /**
8894
+ * Pick a model for a modality.
8895
+ *
8896
+ * Ranked models beat unranked ones, then higher quality, then lower unit
8897
+ * price. An explicit `preferModel` wins outright when it is usable — the user
8898
+ * naming a model is a stronger signal than any prior.
8899
+ */
8900
+ select(modality, opts = {}) {
8901
+ const pool = this.usable(modality);
8902
+ if (!pool.length) return null;
8903
+ if (opts.preferModel) {
8904
+ const wanted = pool.find((c) => c.modelId === opts.preferModel);
8905
+ if (wanted) {
8906
+ return { capability: wanted, cost: capabilityCost(wanted), reason: `Requested explicitly: ${wanted.modelId}.` };
8907
+ }
8908
+ }
8909
+ const scored = pool.map((c) => ({ c, cost: capabilityCost(c) })).sort((a, b) => {
8910
+ const qa = a.c.quality ?? -1;
8911
+ const qb = b.c.quality ?? -1;
8912
+ if (qa !== qb) return qb - qa;
8913
+ const pa = a.cost?.amount ?? Number.POSITIVE_INFINITY;
8914
+ const pb = b.cost?.amount ?? Number.POSITIVE_INFINITY;
8915
+ return pa - pb;
8916
+ });
8917
+ const best = scored[0];
8918
+ const ranked = best.c.quality !== void 0;
8919
+ const reason = ranked ? `${best.c.modelId} (${best.c.provider}) \u2014 ranked highest for ${modality}: ${best.c.qualityBasis}.` + (best.cost ? ` ${best.cost.label}.` : "") : `${best.c.modelId} (${best.c.provider}) \u2014 cheapest usable ${modality} model` + (best.cost ? ` at ${best.cost.label}` : "") + ". No published quality ranking is applied for this modality, so this is a cost choice, not a quality one.";
8920
+ return { capability: best.c, cost: best.cost, reason };
8921
+ }
8922
+ /**
8923
+ * A plain-language account of what this account can and cannot generate —
8924
+ * including the things it cannot, and why. The planner sees this, so being
8925
+ * candid here is what stops it inventing a `generate_music` call.
8926
+ */
8927
+ describe() {
8928
+ const lines = [];
8929
+ const usable = this.usable();
8930
+ if (usable.length) {
8931
+ lines.push("Generation models available on your configured providers:");
8932
+ for (const modality of ["image", "speech", "transcription", "video"]) {
8933
+ const forModality = usable.filter((c) => c.modality === modality);
8934
+ if (!forModality.length) continue;
8935
+ const shown = forModality.map((c) => {
8936
+ const cost = capabilityCost(c);
8937
+ return `${c.modelId}${cost ? ` (${cost.label})` : ""}`;
8938
+ });
8939
+ lines.push(` ${modality}: ${shown.join(", ")}`);
8940
+ }
8941
+ } else {
8942
+ lines.push("No generation models are available \u2014 none of your configured providers offers one.");
8943
+ }
8944
+ const blocked = CAPABILITIES.filter((c) => c.unsupported && this.available.has(c.provider));
8945
+ for (const c of blocked) {
8946
+ lines.push(` ${c.modality}: ${c.modelId} exists on your account but is not callable. ${c.unsupported}`);
8947
+ }
8948
+ lines.push(
8949
+ " music: no supported provider exposes a music-generation API, so Cascade cannot generate music."
8950
+ );
8951
+ return lines.join("\n");
8952
+ }
8953
+ };
8954
+
8955
+ // src/tools/base.ts
8956
+ var BaseTool = class {
8957
+ workspaceRoot = process.cwd();
8958
+ setWorkspaceRoot(root) {
8959
+ this.workspaceRoot = root;
8960
+ }
8961
+ isDangerous() {
8962
+ return false;
8963
+ }
8964
+ getDefinition() {
8965
+ return {
8966
+ name: this.name,
8967
+ description: this.description,
8968
+ inputSchema: this.inputSchema
8969
+ };
8970
+ }
8971
+ };
8972
+
8685
8973
  // src/core/router/provider-errors.ts
8686
8974
  var SYSTEMIC = /* @__PURE__ */ new Set([
8687
8975
  "rate_limit",
@@ -8746,7 +9034,7 @@ function classifyProviderError(err) {
8746
9034
  return { kind, systemic: isSystemicKind(kind), status, raw };
8747
9035
  }
8748
9036
  function describeProviderError(c, modelId) {
8749
- const on = ` on ${modelId}` ;
9037
+ const on = modelId ? ` on ${modelId}` : "";
8750
9038
  const detail = c.raw ? ` Provider said: ${c.raw}` : "";
8751
9039
  switch (c.kind) {
8752
9040
  case "rate_limit":
@@ -8768,6 +9056,417 @@ function describeProviderError(c, modelId) {
8768
9056
  }
8769
9057
  }
8770
9058
 
9059
+ // src/core/multimodal/generate.ts
9060
+ var DEFAULT_TIMEOUT_MS = 12e4;
9061
+ async function callProvider(fn, modelId) {
9062
+ try {
9063
+ return await fn();
9064
+ } catch (err) {
9065
+ const classified = classifyProviderError(err);
9066
+ throw new Error(describeProviderError(classified, modelId));
9067
+ }
9068
+ }
9069
+ async function postJson(url, body, headers, signal) {
9070
+ const ac = new AbortController();
9071
+ const timer = setTimeout(() => ac.abort(), DEFAULT_TIMEOUT_MS);
9072
+ const onAbort = () => ac.abort();
9073
+ signal?.addEventListener("abort", onAbort);
9074
+ try {
9075
+ const res = await fetch(url, {
9076
+ method: "POST",
9077
+ headers: { "Content-Type": "application/json", ...headers },
9078
+ body: JSON.stringify(body),
9079
+ signal: ac.signal
9080
+ });
9081
+ if (!res.ok) {
9082
+ const text = await res.text().catch(() => "");
9083
+ throw Object.assign(new Error(text || res.statusText), { status: res.status });
9084
+ }
9085
+ return res;
9086
+ } finally {
9087
+ clearTimeout(timer);
9088
+ signal?.removeEventListener("abort", onAbort);
9089
+ }
9090
+ }
9091
+ function baseUrlFor(cfg, fallback) {
9092
+ return (cfg.baseUrl ?? fallback).replace(/\/+$/, "");
9093
+ }
9094
+ function stamp(ext) {
9095
+ return `cascade-${Date.now()}.${ext}`;
9096
+ }
9097
+ async function generateImage(cap, cfg, req, signal) {
9098
+ return callProvider(async () => {
9099
+ if (cap.api === "openai-images") {
9100
+ const url = `${baseUrlFor(cfg, "https://api.openai.com/v1")}/images/generations`;
9101
+ const res = await postJson(url, {
9102
+ model: cap.modelId,
9103
+ prompt: req.prompt,
9104
+ size: req.size ?? "1024x1024",
9105
+ n: 1,
9106
+ // b64 rather than a URL: see the file header — provider URLs expire.
9107
+ response_format: "b64_json"
9108
+ }, { Authorization: `Bearer ${cfg.apiKey ?? ""}` }, signal);
9109
+ const body = await res.json();
9110
+ const first = body.data?.[0];
9111
+ if (!first?.b64_json) throw new Error("Image response contained no image data.");
9112
+ return {
9113
+ data: Buffer.from(first.b64_json, "base64"),
9114
+ mimeType: "image/png",
9115
+ filename: stamp("png"),
9116
+ modelId: cap.modelId,
9117
+ provider: cap.provider,
9118
+ ...first.revised_prompt ? { revisedPrompt: first.revised_prompt } : {}
9119
+ };
9120
+ }
9121
+ if (cap.api === "gemini-predict") {
9122
+ const base = baseUrlFor(cfg, "https://generativelanguage.googleapis.com/v1beta");
9123
+ const url = `${base}/models/${encodeURIComponent(cap.modelId)}:predict`;
9124
+ const res = await postJson(url, {
9125
+ instances: [{ prompt: req.prompt }],
9126
+ parameters: { sampleCount: 1 }
9127
+ }, { "x-goog-api-key": cfg.apiKey ?? "" }, signal);
9128
+ const body = await res.json();
9129
+ const first = body.predictions?.[0];
9130
+ if (!first?.bytesBase64Encoded) throw new Error("Image response contained no image data.");
9131
+ const mimeType = first.mimeType ?? "image/png";
9132
+ return {
9133
+ data: Buffer.from(first.bytesBase64Encoded, "base64"),
9134
+ mimeType,
9135
+ filename: stamp(mimeType.includes("jpeg") ? "jpg" : "png"),
9136
+ modelId: cap.modelId,
9137
+ provider: cap.provider
9138
+ };
9139
+ }
9140
+ throw new Error(`${cap.modelId} is not an image generator.`);
9141
+ }, cap.modelId);
9142
+ }
9143
+ async function generateSpeech(cap, cfg, req, signal) {
9144
+ return callProvider(async () => {
9145
+ if (cap.api !== "openai-speech") throw new Error(`${cap.modelId} is not a speech model.`);
9146
+ const format = req.format ?? "mp3";
9147
+ const url = `${baseUrlFor(cfg, "https://api.openai.com/v1")}/audio/speech`;
9148
+ const res = await postJson(url, {
9149
+ model: cap.modelId,
9150
+ input: req.text,
9151
+ voice: req.voice ?? "alloy",
9152
+ response_format: format
9153
+ }, { Authorization: `Bearer ${cfg.apiKey ?? ""}` }, signal);
9154
+ const mime = {
9155
+ mp3: "audio/mpeg",
9156
+ opus: "audio/opus",
9157
+ aac: "audio/aac",
9158
+ flac: "audio/flac",
9159
+ wav: "audio/wav"
9160
+ };
9161
+ return {
9162
+ data: Buffer.from(await res.arrayBuffer()),
9163
+ mimeType: mime[format] ?? "application/octet-stream",
9164
+ filename: stamp(format),
9165
+ modelId: cap.modelId,
9166
+ provider: cap.provider
9167
+ };
9168
+ }, cap.modelId);
9169
+ }
9170
+ var VIDEO_POLL_INTERVAL_MS = 1e4;
9171
+ var VIDEO_TIMEOUT_MS = 8 * 6e4;
9172
+ function sleep(ms, signal) {
9173
+ return new Promise((resolve2, reject) => {
9174
+ if (signal?.aborted) return reject(new Error("Cancelled."));
9175
+ const timer = setTimeout(() => {
9176
+ signal?.removeEventListener("abort", onAbort);
9177
+ resolve2();
9178
+ }, ms);
9179
+ function onAbort() {
9180
+ clearTimeout(timer);
9181
+ reject(new Error("Cancelled."));
9182
+ }
9183
+ signal?.addEventListener("abort", onAbort, { once: true });
9184
+ });
9185
+ }
9186
+ async function generateVideo(cap, cfg, req, signal, onProgress) {
9187
+ return callProvider(async () => {
9188
+ if (cap.api !== "gemini-predict-lro") throw new Error(`${cap.modelId} is not a video generator.`);
9189
+ const base = baseUrlFor(cfg, "https://generativelanguage.googleapis.com/v1beta");
9190
+ const key = cfg.apiKey ?? "";
9191
+ const headers = { "x-goog-api-key": key };
9192
+ const submit = await postJson(
9193
+ `${base}/models/${encodeURIComponent(cap.modelId)}:predictLongRunning`,
9194
+ {
9195
+ instances: [{ prompt: req.prompt }],
9196
+ parameters: {
9197
+ aspectRatio: req.aspectRatio ?? "16:9",
9198
+ ...req.seconds ? { durationSeconds: req.seconds } : {}
9199
+ }
9200
+ },
9201
+ headers,
9202
+ signal
9203
+ );
9204
+ const started = await submit.json();
9205
+ if (!started.name) throw new Error("Video request was accepted but returned no operation to poll.");
9206
+ onProgress?.(`Video generation started (${cap.modelId}). This usually takes 1-3 minutes.`);
9207
+ const deadline = Date.now() + VIDEO_TIMEOUT_MS;
9208
+ let operation = {};
9209
+ while (Date.now() < deadline) {
9210
+ await sleep(VIDEO_POLL_INTERVAL_MS, signal);
9211
+ const poll = await fetch(`${base}/${started.name}`, { headers, signal });
9212
+ if (!poll.ok) {
9213
+ const text = await poll.text().catch(() => "");
9214
+ throw Object.assign(new Error(text || poll.statusText), { status: poll.status });
9215
+ }
9216
+ operation = await poll.json();
9217
+ if (operation.done) break;
9218
+ onProgress?.("Still rendering\u2026");
9219
+ }
9220
+ if (!operation.done) {
9221
+ throw new Error(
9222
+ `Video was still rendering after ${Math.round(VIDEO_TIMEOUT_MS / 6e4)} minutes. It may still complete on the provider side, but Cascade stopped waiting.`
9223
+ );
9224
+ }
9225
+ if (operation.error?.message) throw new Error(operation.error.message);
9226
+ const uri = operation.response?.generateVideoResponse?.generatedSamples?.[0]?.video?.uri;
9227
+ if (!uri) throw new Error("Video finished but the response contained no video.");
9228
+ const media = await fetch(uri, { headers, signal });
9229
+ if (!media.ok) throw new Error(`Video finished but could not be downloaded (HTTP ${media.status}).`);
9230
+ return {
9231
+ data: Buffer.from(await media.arrayBuffer()),
9232
+ mimeType: "video/mp4",
9233
+ filename: stamp("mp4"),
9234
+ modelId: cap.modelId,
9235
+ provider: cap.provider
9236
+ };
9237
+ }, cap.modelId);
9238
+ }
9239
+ async function transcribeAudio(cap, cfg, req, signal) {
9240
+ return callProvider(async () => {
9241
+ if (cap.api !== "openai-transcriptions") throw new Error(`${cap.modelId} is not a transcription model.`);
9242
+ const url = `${baseUrlFor(cfg, "https://api.openai.com/v1")}/audio/transcriptions`;
9243
+ const form = new FormData();
9244
+ form.append("model", cap.modelId);
9245
+ if (req.language) form.append("language", req.language);
9246
+ form.append("file", new Blob([new Uint8Array(req.audio)]), req.filename);
9247
+ const ac = new AbortController();
9248
+ const timer = setTimeout(() => ac.abort(), DEFAULT_TIMEOUT_MS);
9249
+ const onAbort = () => ac.abort();
9250
+ signal?.addEventListener("abort", onAbort);
9251
+ try {
9252
+ const res = await fetch(url, {
9253
+ method: "POST",
9254
+ headers: { Authorization: `Bearer ${cfg.apiKey ?? ""}` },
9255
+ body: form,
9256
+ signal: ac.signal
9257
+ });
9258
+ if (!res.ok) {
9259
+ const text = await res.text().catch(() => "");
9260
+ throw Object.assign(new Error(text || res.statusText), { status: res.status });
9261
+ }
9262
+ const body = await res.json();
9263
+ return { text: body.text ?? "", modelId: cap.modelId };
9264
+ } finally {
9265
+ clearTimeout(timer);
9266
+ signal?.removeEventListener("abort", onAbort);
9267
+ }
9268
+ }, cap.modelId);
9269
+ }
9270
+
9271
+ // src/tools/generate-media.ts
9272
+ function resolve(deps, modality, preferModel) {
9273
+ const selected = deps.registry.select(modality, preferModel ? { preferModel } : {});
9274
+ if (!selected) {
9275
+ return { ok: false, error: `No ${modality} model is available on your configured providers.` };
9276
+ }
9277
+ const cfg = deps.lookupProvider(selected.capability.provider);
9278
+ if (!cfg) {
9279
+ return {
9280
+ ok: false,
9281
+ error: `${selected.capability.modelId} needs the ${selected.capability.provider} provider, which is not configured.`
9282
+ };
9283
+ }
9284
+ return { ok: true, selected, cfg };
9285
+ }
9286
+ var GenerateImageTool = class extends BaseTool {
9287
+ constructor(deps) {
9288
+ super();
9289
+ this.deps = deps;
9290
+ }
9291
+ deps;
9292
+ name = "generate_image";
9293
+ description = "Generate an image from a text description using the best available image model. Returns where the image was saved. Use when the user asks for a picture, illustration, diagram render, logo or any visual asset.";
9294
+ inputSchema = {
9295
+ type: "object",
9296
+ properties: {
9297
+ prompt: { type: "string", description: "What the image should show. Be specific about subject, style and composition." },
9298
+ size: { type: "string", description: 'Pixel size, e.g. "1024x1024" (default), "1792x1024", "1024x1792".' },
9299
+ model: { type: "string", description: "Optional: force a specific image model instead of the automatic choice." }
9300
+ },
9301
+ required: ["prompt"]
9302
+ };
9303
+ async execute(input, options) {
9304
+ const prompt = String(input["prompt"] ?? "").trim();
9305
+ if (!prompt) return 'Provide a "prompt" describing the image.';
9306
+ const r = resolve(this.deps, "image", input["model"] ? String(input["model"]) : void 0);
9307
+ if (!r.ok) return r.error;
9308
+ const size = input["size"] ? String(input["size"]) : void 0;
9309
+ const asset = await generateImage(
9310
+ r.selected.capability,
9311
+ r.cfg,
9312
+ { prompt, ...size ? { size } : {} },
9313
+ options.signal
9314
+ );
9315
+ const location = await this.deps.sink(asset);
9316
+ return [
9317
+ `Image generated and saved to ${location}.`,
9318
+ `Model: ${asset.modelId} \u2014 ${r.selected.reason}`,
9319
+ // DALL·E rewrites the prompt before drawing; the model needs to know what
9320
+ // was actually rendered, not what it asked for, or its description of the
9321
+ // result will not match the image.
9322
+ asset.revisedPrompt ? `The provider rewrote the prompt to: ${asset.revisedPrompt}` : ""
9323
+ ].filter(Boolean).join("\n");
9324
+ }
9325
+ };
9326
+ var GenerateSpeechTool = class extends BaseTool {
9327
+ constructor(deps) {
9328
+ super();
9329
+ this.deps = deps;
9330
+ }
9331
+ deps;
9332
+ name = "generate_speech";
9333
+ description = "Turn text into spoken audio using the best available text-to-speech model. Returns where the audio was saved. Use when the user asks for narration, a voiceover or an audio version of some text.";
9334
+ inputSchema = {
9335
+ type: "object",
9336
+ properties: {
9337
+ text: { type: "string", description: "The text to speak." },
9338
+ voice: { type: "string", description: "Voice id: alloy, echo, fable, onyx, nova, shimmer. Default alloy." },
9339
+ format: { type: "string", description: "Audio format: mp3 (default), opus, aac, flac, wav." },
9340
+ model: { type: "string", description: "Optional: force a specific speech model." }
9341
+ },
9342
+ required: ["text"]
9343
+ };
9344
+ async execute(input, options) {
9345
+ const text = String(input["text"] ?? "").trim();
9346
+ if (!text) return 'Provide "text" to speak.';
9347
+ const r = resolve(this.deps, "speech", input["model"] ? String(input["model"]) : void 0);
9348
+ if (!r.ok) return r.error;
9349
+ const fmt = String(input["format"] ?? "mp3");
9350
+ const allowed = ["mp3", "opus", "aac", "flac", "wav"];
9351
+ const format = allowed.includes(fmt) ? fmt : "mp3";
9352
+ const asset = await generateSpeech(
9353
+ r.selected.capability,
9354
+ r.cfg,
9355
+ { text, voice: input["voice"] ? String(input["voice"]) : void 0, format },
9356
+ options.signal
9357
+ );
9358
+ const location = await this.deps.sink(asset);
9359
+ return `Speech generated and saved to ${location}.
9360
+ Model: ${asset.modelId} \u2014 ${r.selected.reason}`;
9361
+ }
9362
+ };
9363
+ var DEFAULT_VIDEO_SECONDS = 5;
9364
+ var MAX_VIDEO_SECONDS = 8;
9365
+ var GenerateVideoTool = class extends BaseTool {
9366
+ constructor(deps) {
9367
+ super();
9368
+ this.deps = deps;
9369
+ }
9370
+ deps;
9371
+ name = "generate_video";
9372
+ // The price is in the description on purpose. Video is ~1000x the cost of an
9373
+ // image per call, it is billed per SECOND, and the model choosing the
9374
+ // duration is the one deciding the bill — so the number has to be where the
9375
+ // model reads before calling, not only in the result afterwards.
9376
+ description = "Generate a short video clip from a text description. EXPENSIVE and slow: billed per second of output (about $0.40/second, so a 5-second clip costs roughly $2) and takes 1-3 minutes. Only use when the user explicitly asks for a video, and prefer the shortest duration that satisfies the request.";
9377
+ inputSchema = {
9378
+ type: "object",
9379
+ properties: {
9380
+ prompt: { type: "string", description: "What the video should show: subject, action, camera movement, style." },
9381
+ seconds: { type: "number", description: `Clip length, 1-${MAX_VIDEO_SECONDS} seconds (default ${DEFAULT_VIDEO_SECONDS}). Billed per second \u2014 keep it short.` },
9382
+ aspectRatio: { type: "string", description: 'Frame shape: "16:9" (default) or "9:16" for vertical.' },
9383
+ model: { type: "string", description: "Optional: force a specific video model." }
9384
+ },
9385
+ required: ["prompt"]
9386
+ };
9387
+ async execute(input, options) {
9388
+ const prompt = String(input["prompt"] ?? "").trim();
9389
+ if (!prompt) return 'Provide a "prompt" describing the video.';
9390
+ const r = resolve(this.deps, "video", input["model"] ? String(input["model"]) : void 0);
9391
+ if (!r.ok) return r.error;
9392
+ const asked = typeof input["seconds"] === "number" ? input["seconds"] : DEFAULT_VIDEO_SECONDS;
9393
+ const seconds = Math.max(1, Math.min(MAX_VIDEO_SECONDS, Math.round(asked)));
9394
+ const asset = await generateVideo(
9395
+ r.selected.capability,
9396
+ r.cfg,
9397
+ {
9398
+ prompt,
9399
+ seconds,
9400
+ ...input["aspectRatio"] ? { aspectRatio: String(input["aspectRatio"]) } : {}
9401
+ },
9402
+ options.signal,
9403
+ // Progress goes to the run log so a two-minute wait doesn't look like a
9404
+ // hang — the single most likely reason someone kills a working render.
9405
+ (note) => options.onProgress?.(note)
9406
+ );
9407
+ const location = await this.deps.sink(asset);
9408
+ const unit = r.selected.cost;
9409
+ const estimate = unit ? ` Estimated cost: $${(unit.amount * seconds).toFixed(2)} (${seconds}s x ${unit.label}).` : "";
9410
+ const clamped = seconds !== Math.round(asked) ? ` Duration was clamped to ${seconds}s (max ${MAX_VIDEO_SECONDS}).` : "";
9411
+ return `Video generated and saved to ${location}.
9412
+ Model: ${asset.modelId}.${estimate}${clamped}`;
9413
+ }
9414
+ };
9415
+ var TranscribeAudioTool = class extends BaseTool {
9416
+ constructor(deps, readFile) {
9417
+ super();
9418
+ this.deps = deps;
9419
+ this.readFile = readFile;
9420
+ }
9421
+ deps;
9422
+ readFile;
9423
+ name = "transcribe_audio";
9424
+ description = "Transcribe a local audio file to text using the best available speech-to-text model. Use when the user supplies audio and wants its contents in writing.";
9425
+ inputSchema = {
9426
+ type: "object",
9427
+ properties: {
9428
+ path: { type: "string", description: "Path to the audio file to transcribe." },
9429
+ language: { type: "string", description: 'Optional ISO-639-1 language hint, e.g. "en", improves accuracy.' },
9430
+ model: { type: "string", description: "Optional: force a specific transcription model." }
9431
+ },
9432
+ required: ["path"]
9433
+ };
9434
+ async execute(input, options) {
9435
+ const filePath = String(input["path"] ?? "").trim();
9436
+ if (!filePath) return 'Provide a "path" to the audio file.';
9437
+ const r = resolve(this.deps, "transcription", input["model"] ? String(input["model"]) : void 0);
9438
+ if (!r.ok) return r.error;
9439
+ let audio;
9440
+ try {
9441
+ audio = await this.readFile(filePath);
9442
+ } catch {
9443
+ return `Could not read the audio file at ${filePath}.`;
9444
+ }
9445
+ const { text, modelId } = await transcribeAudio(
9446
+ r.selected.capability,
9447
+ r.cfg,
9448
+ {
9449
+ audio,
9450
+ filename: filePath.split(/[\\/]/).pop() || "audio",
9451
+ ...input["language"] ? { language: String(input["language"]) } : {}
9452
+ },
9453
+ options.signal
9454
+ );
9455
+ return text ? `Transcript (${modelId}):
9456
+
9457
+ ${text}` : `${modelId} returned an empty transcript \u2014 the file may be silent or in an unsupported format.`;
9458
+ }
9459
+ };
9460
+ function buildMediaTools(deps, readFile) {
9461
+ const tools = [];
9462
+ const modalities = new Set(deps.registry.availableModalities());
9463
+ if (modalities.has("image")) tools.push(new GenerateImageTool(deps));
9464
+ if (modalities.has("speech")) tools.push(new GenerateSpeechTool(deps));
9465
+ if (modalities.has("video")) tools.push(new GenerateVideoTool(deps));
9466
+ if (modalities.has("transcription") && readFile) tools.push(new TranscribeAudioTool(deps, readFile));
9467
+ return tools;
9468
+ }
9469
+
8771
9470
  // src/core/run-breaker.ts
8772
9471
  var DEFAULT_FAILURE_THRESHOLD = 3;
8773
9472
  var RunBreaker = class {
@@ -8833,26 +9532,6 @@ var RunBreaker = class {
8833
9532
  return out;
8834
9533
  }
8835
9534
  };
8836
-
8837
- // src/tools/base.ts
8838
- var BaseTool = class {
8839
- workspaceRoot = process.cwd();
8840
- setWorkspaceRoot(root) {
8841
- this.workspaceRoot = root;
8842
- }
8843
- isDangerous() {
8844
- return false;
8845
- }
8846
- getDefinition() {
8847
- return {
8848
- name: this.name,
8849
- description: this.description,
8850
- inputSchema: this.inputSchema
8851
- };
8852
- }
8853
- };
8854
-
8855
- // src/tools/shell.ts
8856
9535
  var execAsync = promisify(exec);
8857
9536
  var ShellTool = class extends BaseTool {
8858
9537
  name = "shell";
@@ -9548,7 +10227,7 @@ var PDFCreateTool = class extends BaseTool {
9548
10227
  if (!fs23.existsSync(dir)) {
9549
10228
  fs23.mkdirSync(dir, { recursive: true });
9550
10229
  }
9551
- return new Promise((resolve, reject) => {
10230
+ return new Promise((resolve2, reject) => {
9552
10231
  try {
9553
10232
  const doc = new PDFDocument({ margin: 50 });
9554
10233
  const stream = fs23.createWriteStream(filePath);
@@ -9566,7 +10245,7 @@ var PDFCreateTool = class extends BaseTool {
9566
10245
  });
9567
10246
  doc.end();
9568
10247
  stream.on("finish", () => {
9569
- resolve(`Successfully created PDF at ${filePath} (${content.length} characters)`);
10248
+ resolve2(`Successfully created PDF at ${filePath} (${content.length} characters)`);
9570
10249
  });
9571
10250
  stream.on("error", (err) => {
9572
10251
  reject(new Error(`Failed to write PDF: ${err.message}`));
@@ -9638,7 +10317,7 @@ var CodeInterpreterTool = class extends BaseTool {
9638
10317
  const filePath = path24.join(tmpDir, fileName);
9639
10318
  fs23.writeFileSync(filePath, code, "utf-8");
9640
10319
  const execArgs = [filePath, ...args];
9641
- return new Promise((resolve) => {
10320
+ return new Promise((resolve2) => {
9642
10321
  const startMs = Date.now();
9643
10322
  execFile(cmdPrefix, execArgs, { cwd: this.workspaceRoot, timeout: 3e4 }, (error, stdout, stderr) => {
9644
10323
  const duration = Date.now() - startMs;
@@ -9652,17 +10331,17 @@ var CodeInterpreterTool = class extends BaseTool {
9652
10331
  if (error) {
9653
10332
  const timedOut = error.killed && duration >= 3e4;
9654
10333
  if (timedOut) {
9655
- resolve(`Execution timed out after 30s. Consider breaking the task into smaller pieces.
10334
+ resolve2(`Execution timed out after 30s. Consider breaking the task into smaller pieces.
9656
10335
  Partial stdout: ${stdout}
9657
10336
  Stderr: ${stderr}`);
9658
10337
  } else {
9659
- resolve(`Execution failed (${duration}ms):
10338
+ resolve2(`Execution failed (${duration}ms):
9660
10339
  Error: ${error.message}
9661
10340
  Stderr: ${stderr}
9662
10341
  Stdout: ${stdout}`);
9663
10342
  }
9664
10343
  } else {
9665
- resolve(`Execution successful (${duration}ms):
10344
+ resolve2(`Execution successful (${duration}ms):
9666
10345
  Stdout: ${stdout}
9667
10346
  Stderr: ${stderr}`);
9668
10347
  }
@@ -10316,7 +10995,7 @@ var ToolRegistry = class extends EventEmitter {
10316
10995
  */
10317
10996
  waitForTool(toolName, timeoutMs = 6e4) {
10318
10997
  if (this.tools.has(toolName)) return Promise.resolve();
10319
- return new Promise((resolve, reject) => {
10998
+ return new Promise((resolve2, reject) => {
10320
10999
  const timer = setTimeout(() => {
10321
11000
  this.off("tool:added", handler);
10322
11001
  reject(new Error(`Timeout waiting for tool: ${toolName}`));
@@ -10325,7 +11004,7 @@ var ToolRegistry = class extends EventEmitter {
10325
11004
  if (name === toolName) {
10326
11005
  clearTimeout(timer);
10327
11006
  this.off("tool:added", handler);
10328
- resolve();
11007
+ resolve2();
10329
11008
  }
10330
11009
  };
10331
11010
  this.on("tool:added", handler);
@@ -11000,20 +11679,20 @@ var PermissionEscalator = class extends EventEmitter {
11000
11679
  resolver(decision);
11001
11680
  }
11002
11681
  waitForUserDecision(req) {
11003
- return new Promise((resolve) => {
11682
+ return new Promise((resolve2) => {
11004
11683
  let timer;
11005
11684
  const wrappedResolver = (decision) => {
11006
11685
  if (timer) clearTimeout(timer);
11007
11686
  if (decision.always) {
11008
11687
  this.taskWideCache.set(req.toolName, decision.approved);
11009
11688
  }
11010
- resolve(decision);
11689
+ resolve2(decision);
11011
11690
  };
11012
11691
  this.pendingUserDecisions.set(req.id, wrappedResolver);
11013
11692
  if (this.approvalTimeoutMs > 0 && Number.isFinite(this.approvalTimeoutMs)) {
11014
11693
  timer = setTimeout(() => {
11015
11694
  if (this.pendingUserDecisions.delete(req.id)) {
11016
- resolve({
11695
+ resolve2({
11017
11696
  requestId: req.id,
11018
11697
  approved: false,
11019
11698
  decidedBy: "USER",
@@ -11112,7 +11791,14 @@ var ToolsConfigSchema = z.object({
11112
11791
  * built-in tool (shell/file/git/…) from existing at all, rather than just
11113
11792
  * gating it behind approval. Omitted = full default set (unchanged).
11114
11793
  */
11115
- enabledTools: z.array(z.string()).optional()
11794
+ enabledTools: z.array(z.string()).optional(),
11795
+ /**
11796
+ * Tools to remove even when they would otherwise be registered. Unlike
11797
+ * `enabledTools` (an allowlist over the built-in set), this also reaches
11798
+ * tools registered outside it — MCP tools and media generation — which is
11799
+ * the only way to turn those off.
11800
+ */
11801
+ disabledTools: z.array(z.string()).optional()
11116
11802
  });
11117
11803
  var HookDefinitionSchema = z.object({
11118
11804
  name: z.string().optional(),
@@ -11180,6 +11866,14 @@ var BudgetConfigSchema = z.object({
11180
11866
  maxTokensPerRun: z.number().int().positive().default(2e5),
11181
11867
  /** Optional hard per-task cost ceiling (USD). Unset = only the token cap applies. */
11182
11868
  maxCostPerRunUsd: z.number().positive().optional(),
11869
+ /**
11870
+ * Consecutive systemic failures against ONE model before the run stops
11871
+ * launching work that would fail the same way (dead key, wrong model id,
11872
+ * exhausted quota). Lower reacts sooner and wastes less; higher absorbs a
11873
+ * longer transient before giving up. Only failures that will certainly repeat
11874
+ * are counted — see core/run-breaker.ts.
11875
+ */
11876
+ failureThreshold: z.number().int().min(1).max(20).default(3),
11183
11877
  warnAtPct: z.number().default(80)
11184
11878
  });
11185
11879
  var WorkspaceConfigSchema = z.object({
@@ -11467,6 +12161,23 @@ var noopTelemetry = {
11467
12161
  }
11468
12162
  };
11469
12163
 
12164
+ // src/core/router/feedback-prior.ts
12165
+ var MAX_FEEDBACK_ADJUSTMENT = 0.05;
12166
+ var PRIOR_STRENGTH = 10;
12167
+ function feedbackAdjustment(counts) {
12168
+ const good = Math.max(0, Math.floor(counts.good));
12169
+ const bad = Math.max(0, Math.floor(counts.bad));
12170
+ const n = good + bad;
12171
+ if (n === 0) return 0;
12172
+ const half = PRIOR_STRENGTH / 2;
12173
+ const shrunk = (good + half) / (n + PRIOR_STRENGTH);
12174
+ return (shrunk - 0.5) * 2 * MAX_FEEDBACK_ADJUSTMENT;
12175
+ }
12176
+ function applyFeedback(score01, counts) {
12177
+ const adjusted = score01 + feedbackAdjustment(counts);
12178
+ return Math.max(0, Math.min(1, adjusted));
12179
+ }
12180
+
11470
12181
  // src/core/router/task-analyzer.ts
11471
12182
  var CODE_SIGNALS = [
11472
12183
  /\b(?:function|class|interface|async|await|import|export|const|let|var|def|return|if|else|for|while|try|catch)\b/,
@@ -11541,6 +12252,8 @@ var TASK_TYPE_TAGS = {
11541
12252
  };
11542
12253
  var TaskAnalyzer = class {
11543
12254
  tracker;
12255
+ /** Per-model thumbs counts, when the host collects them. */
12256
+ feedback;
11544
12257
  bias;
11545
12258
  lastProfile = null;
11546
12259
  lastSelectedModels = /* @__PURE__ */ new Map();
@@ -11548,6 +12261,14 @@ var TaskAnalyzer = class {
11548
12261
  this.tracker = tracker;
11549
12262
  this.bias = bias;
11550
12263
  }
12264
+ /**
12265
+ * Supply per-model rating counts. Optional by design: a host that collects no
12266
+ * feedback (the CLI, a fresh install) scores exactly as it did before, since
12267
+ * feedbackAdjustment returns 0 for an empty record.
12268
+ */
12269
+ setFeedbackSource(source) {
12270
+ this.feedback = source;
12271
+ }
11551
12272
  setTracker(tracker) {
11552
12273
  this.tracker = tracker;
11553
12274
  }
@@ -11631,7 +12352,10 @@ var TaskAnalyzer = class {
11631
12352
  const perf = this.tracker?.performanceScore(model.id, profile.type) ?? 0.5;
11632
12353
  const costEff = this.costEfficiency(model, profile.complexity);
11633
12354
  const match = this.taskMatchScore(model, profile);
11634
- const benchmark = 0.3 + 0.7 * benchmarkScore01(model, profile.type);
12355
+ const rated = this.feedback?.(model.id);
12356
+ const publicScore = benchmarkScore01(model, profile.type);
12357
+ const adjusted = rated ? applyFeedback(publicScore, rated) : publicScore;
12358
+ const benchmark = 0.3 + 0.7 * adjusted;
11635
12359
  switch (this.bias) {
11636
12360
  case "quality":
11637
12361
  return perf * match * benchmark ** 2 * (0.85 + 0.15 * costEff);
@@ -12033,10 +12757,10 @@ var DynamicTool = class extends BaseTool {
12033
12757
  `;
12034
12758
  const script = await isolate.compileScript(bootstrap);
12035
12759
  const runPromise = script.run(context, { timeout: timeoutMs, promise: true }).then((v) => String(v ?? ""));
12036
- const timeoutPromise = new Promise((resolve) => {
12760
+ const timeoutPromise = new Promise((resolve2) => {
12037
12761
  const t = setTimeout(() => {
12038
12762
  dispose();
12039
- resolve(`Dynamic tool "${this.name}" timed out after ${timeoutMs}ms and was terminated.`);
12763
+ resolve2(`Dynamic tool "${this.name}" timed out after ${timeoutMs}ms and was terminated.`);
12040
12764
  }, timeoutMs + 500);
12041
12765
  t.unref?.();
12042
12766
  });
@@ -12054,7 +12778,7 @@ var DynamicTool = class extends BaseTool {
12054
12778
  /** Spawn the worker, service its callTool/fetch bridge, enforce the kill timeout. */
12055
12779
  runInWorker(input, callTool) {
12056
12780
  const timeoutMs = Math.max(200, Number(process.env["CASCADE_DYNAMIC_TOOL_TIMEOUT_MS"]) || DYNAMIC_TOOL_TIMEOUT_MS);
12057
- return new Promise((resolve) => {
12781
+ return new Promise((resolve2) => {
12058
12782
  let settled = false;
12059
12783
  const worker = new Worker(HARNESS_SRC, {
12060
12784
  eval: true,
@@ -12066,7 +12790,7 @@ var DynamicTool = class extends BaseTool {
12066
12790
  settled = true;
12067
12791
  clearTimeout(timer);
12068
12792
  void worker.terminate();
12069
- resolve(value);
12793
+ resolve2(value);
12070
12794
  };
12071
12795
  const timer = setTimeout(
12072
12796
  () => finish(`Dynamic tool "${this.name}" timed out after ${timeoutMs}ms and was terminated.`),
@@ -13612,6 +14336,8 @@ ${prompt}`;
13612
14336
  var Cascade = class _Cascade extends EventEmitter {
13613
14337
  router;
13614
14338
  toolRegistry;
14339
+ multimodal;
14340
+ mediaSink;
13615
14341
  mcpClient;
13616
14342
  codeIndex;
13617
14343
  config;
@@ -13650,6 +14376,10 @@ var Cascade = class _Cascade extends EventEmitter {
13650
14376
  }
13651
14377
  });
13652
14378
  this.toolRegistry = new ToolRegistry(this.config.tools, workspacePath);
14379
+ this.multimodal = new MultimodalRegistry(
14380
+ (this.config.providers ?? []).map((p) => p.type)
14381
+ );
14382
+ this.registerMediaTools(workspacePath);
13653
14383
  this.telemetry = config.telemetry?.enabled ? new Telemetry(config.telemetry, config.telemetry.distinctId ?? "anonymous") : noopTelemetry;
13654
14384
  this.worldStateDB = new WorldStateDB(this.workspacePath, this.config.workspace?.debugWorldState ?? false);
13655
14385
  this.router.setWorldStateDB(this.worldStateDB);
@@ -13703,14 +14433,14 @@ var Cascade = class _Cascade extends EventEmitter {
13703
14433
  if (this.listenerCount("mcp:approval-required") === 0) {
13704
14434
  return false;
13705
14435
  }
13706
- return await new Promise((resolve) => {
13707
- this.pendingMcpApprovals.set(server.name, resolve);
14436
+ return await new Promise((resolve2) => {
14437
+ this.pendingMcpApprovals.set(server.name, resolve2);
13708
14438
  const timeout = setTimeout(() => {
13709
- if (this.pendingMcpApprovals.delete(server.name)) resolve(false);
14439
+ if (this.pendingMcpApprovals.delete(server.name)) resolve2(false);
13710
14440
  }, 3e4);
13711
14441
  const wrap = (approved) => {
13712
14442
  clearTimeout(timeout);
13713
- resolve(approved);
14443
+ resolve2(approved);
13714
14444
  };
13715
14445
  this.pendingMcpApprovals.set(server.name, wrap);
13716
14446
  this.emit("mcp:approval-required", { server });
@@ -13784,17 +14514,17 @@ var Cascade = class _Cascade extends EventEmitter {
13784
14514
  }
13785
14515
  const t2Count = plan.sections.length;
13786
14516
  const t3Count = plan.sections.reduce((sum, s) => sum + (s.t3Subtasks?.length ?? 0), 0);
13787
- return await new Promise((resolve) => {
14517
+ return await new Promise((resolve2) => {
13788
14518
  const timeout = setTimeout(() => {
13789
14519
  if (this.pendingPlanApproval) {
13790
14520
  this.pendingPlanApproval = void 0;
13791
- resolve({ approved: true });
14521
+ resolve2({ approved: true });
13792
14522
  }
13793
14523
  }, 12e4);
13794
14524
  this.pendingPlanApproval = (decision) => {
13795
14525
  clearTimeout(timeout);
13796
14526
  this.pendingPlanApproval = void 0;
13797
- resolve(decision);
14527
+ resolve2(decision);
13798
14528
  };
13799
14529
  this.emit("plan:approval-required", {
13800
14530
  taskId,
@@ -13827,17 +14557,17 @@ var Cascade = class _Cascade extends EventEmitter {
13827
14557
  }
13828
14558
  async requestContextApproval(info) {
13829
14559
  if (this.listenerCount("context:approval-required") === 0) return true;
13830
- return await new Promise((resolve) => {
14560
+ return await new Promise((resolve2) => {
13831
14561
  const timeout = setTimeout(() => {
13832
14562
  if (this.pendingContextApproval) {
13833
14563
  this.pendingContextApproval = void 0;
13834
- resolve(true);
14564
+ resolve2(true);
13835
14565
  }
13836
14566
  }, 12e4);
13837
14567
  this.pendingContextApproval = (approved) => {
13838
14568
  clearTimeout(timeout);
13839
14569
  this.pendingContextApproval = void 0;
13840
- resolve(approved);
14570
+ resolve2(approved);
13841
14571
  };
13842
14572
  this.emit("context:approval-required", info);
13843
14573
  });
@@ -13917,6 +14647,68 @@ ${input}` }],
13917
14647
  if (this.store) t1.setStore(this.store);
13918
14648
  return t1.previewPlan(prompt);
13919
14649
  }
14650
+ /**
14651
+ * Register image / speech / transcription tools for the modalities this
14652
+ * config can actually serve.
14653
+ *
14654
+ * The sink writes bytes into the workspace, because that is what a desktop or
14655
+ * CLI run wants and it keeps a generated asset indistinguishable from any
14656
+ * other file the run produced. A host with different storage (the cloud
14657
+ * server stores a file row) constructs the tools itself via buildMediaTools
14658
+ * with its own sink.
14659
+ */
14660
+ /**
14661
+ * Replace where generated media is written.
14662
+ *
14663
+ * The default sink writes into the workspace, which is what a desktop or CLI
14664
+ * run wants. A host with different storage — the cloud server keeps a file
14665
+ * row plus bytes under the tenant's directory — supplies its own here, and
14666
+ * the media tools are rebuilt around it. Re-registering by name replaces the
14667
+ * existing tools rather than duplicating them.
14668
+ */
14669
+ setMediaSink(sink) {
14670
+ this.mediaSink = sink;
14671
+ this.registerMediaTools(this.workspacePath);
14672
+ }
14673
+ registerMediaTools(workspacePath) {
14674
+ const denied = new Set(this.config.tools?.disabledTools ?? []);
14675
+ const permitted = (name) => !denied.has(name);
14676
+ const tools = buildMediaTools(
14677
+ {
14678
+ registry: this.multimodal,
14679
+ lookupProvider: (provider) => (this.config.providers ?? []).find((p) => p.type === provider),
14680
+ sink: this.mediaSink ?? (async (asset) => {
14681
+ const fsp = await import('fs/promises');
14682
+ const nodePath = await import('path');
14683
+ const dir = nodePath.join(workspacePath, "generated");
14684
+ await fsp.mkdir(dir, { recursive: true });
14685
+ const dest = nodePath.join(dir, asset.filename);
14686
+ await fsp.writeFile(dest, asset.data);
14687
+ return nodePath.relative(workspacePath, dest);
14688
+ })
14689
+ },
14690
+ async (p) => {
14691
+ const fsp = await import('fs/promises');
14692
+ return fsp.readFile(p);
14693
+ }
14694
+ );
14695
+ for (const tool of tools) {
14696
+ if (!permitted(tool.name)) continue;
14697
+ tool.setWorkspaceRoot(workspacePath);
14698
+ this.toolRegistry.register(tool);
14699
+ }
14700
+ }
14701
+ /**
14702
+ * Supply per-model thumbs-up/down counts so Auto routing can weigh how models
14703
+ * have actually performed for this user, on top of public benchmarks.
14704
+ *
14705
+ * Optional everywhere: with no source, or an empty record, scoring is
14706
+ * byte-identical to before. The adjustment is capped and sample-size shrunk —
14707
+ * see core/router/feedback-prior.ts.
14708
+ */
14709
+ setFeedbackSource(source) {
14710
+ this.router.setFeedbackSource(source);
14711
+ }
13920
14712
  /** True when a task stopped at the budget cap and can be resumed via /continue. */
13921
14713
  hasResumableRun() {
13922
14714
  return this.lastInterruptedRun != null;
@@ -14406,7 +15198,7 @@ ${prompt}` : prompt;
14406
15198
  if (toolCreator) toolCreator.setPermissionEscalator(escalator);
14407
15199
  const rootPrompt = buildContextualPrompt(options.prompt, options.conversationHistory);
14408
15200
  let finalOutput = "";
14409
- const runBreaker = new RunBreaker();
15201
+ const runBreaker = new RunBreaker(this.config.budget?.failureThreshold);
14410
15202
  let t2Results = [];
14411
15203
  let runError = null;
14412
15204
  let identityPrompt = "";
@@ -16579,12 +17371,12 @@ var DashboardServer = class {
16579
17371
  console.warn("\u26A0 Dashboard auth is DISABLED while bound to a non-loopback interface \u2014 this allows unauthenticated remote task execution.");
16580
17372
  }
16581
17373
  }
16582
- await new Promise((resolve, reject) => {
17374
+ await new Promise((resolve2, reject) => {
16583
17375
  const onError = (err) => reject(err);
16584
17376
  this.httpServer.once("error", onError);
16585
17377
  this.httpServer.listen(this.port, this.host, () => {
16586
17378
  this.httpServer.off("error", onError);
16587
- resolve();
17379
+ resolve2();
16588
17380
  });
16589
17381
  });
16590
17382
  try {
@@ -16608,8 +17400,8 @@ var DashboardServer = class {
16608
17400
  } catch {
16609
17401
  }
16610
17402
  this.globalStore = null;
16611
- await new Promise((resolve, reject) => {
16612
- this.httpServer.close((err) => err ? reject(err) : resolve());
17403
+ await new Promise((resolve2, reject) => {
17404
+ this.httpServer.close((err) => err ? reject(err) : resolve2());
16613
17405
  });
16614
17406
  }
16615
17407
  getSocket() {
@@ -16806,8 +17598,8 @@ var DashboardServer = class {
16806
17598
  * timeout denies it.
16807
17599
  */
16808
17600
  makeApprovalCallback(sessionId) {
16809
- return (request) => new Promise((resolve) => {
16810
- this.pendingApprovals.set(request.id, { resolve, sessionId });
17601
+ return (request) => new Promise((resolve2) => {
17602
+ this.pendingApprovals.set(request.id, { resolve: resolve2, sessionId });
16811
17603
  });
16812
17604
  }
16813
17605
  /**
@@ -17907,7 +18699,7 @@ var CloudClient = class _CloudClient {
17907
18699
  const deadline = Date.now() + start.expiresIn * 1e3;
17908
18700
  while (Date.now() < deadline) {
17909
18701
  if (opts.signal?.aborted) throw new Error("Sign-in cancelled.");
17910
- await sleep(interval * 1e3, opts.signal);
18702
+ await sleep2(interval * 1e3, opts.signal);
17911
18703
  const r = await this.pollDevice(start.deviceCode);
17912
18704
  if (r === "slow_down") {
17913
18705
  interval += 5;
@@ -18118,9 +18910,9 @@ var CloudClient = class _CloudClient {
18118
18910
  this.store.clear();
18119
18911
  }
18120
18912
  };
18121
- function sleep(ms, signal) {
18122
- return new Promise((resolve, reject) => {
18123
- const t = setTimeout(resolve, ms);
18913
+ function sleep2(ms, signal) {
18914
+ return new Promise((resolve2, reject) => {
18915
+ const t = setTimeout(resolve2, ms);
18124
18916
  signal?.addEventListener("abort", () => {
18125
18917
  clearTimeout(t);
18126
18918
  reject(new Error("aborted"));