cascade-ai 0.66.1 → 0.66.2

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/cli.cjs CHANGED
@@ -1810,7 +1810,7 @@ var CASCADE_VERSION, CASCADE_CONFIG_FILE, CASCADE_DB_FILE, CASCADE_DASHBOARD_SEC
1810
1810
  var init_constants = __esm({
1811
1811
  "src/constants.ts"() {
1812
1812
  init_pricing();
1813
- CASCADE_VERSION = "0.66.1";
1813
+ CASCADE_VERSION = "0.66.2";
1814
1814
  CASCADE_CONFIG_FILE = ".cascade/config.json";
1815
1815
  CASCADE_DB_FILE = ".cascade/memory.db";
1816
1816
  CASCADE_DASHBOARD_SECRET_FILE = ".cascade/dashboard-secret";
@@ -9102,7 +9102,15 @@ var KNOWN_TOOLS = [
9102
9102
  "glob",
9103
9103
  "grep",
9104
9104
  "web_fetch",
9105
- "generate_image"
9105
+ // Every media tool buildMediaTools can register, not just the image one.
9106
+ // Omitting three of the four meant a run whose ONLY tools were media ones
9107
+ // (a video-generation subtask on an account with no file/web tools) counted
9108
+ // as "no tools registered" and dropped all tool guidance — the worker was
9109
+ // then told nothing about using tools at all, and wrote the video as prose.
9110
+ "generate_image",
9111
+ "generate_video",
9112
+ "generate_speech",
9113
+ "transcribe_audio"
9106
9114
  ];
9107
9115
  var PROVIDER_BACKED_TOOLS = /* @__PURE__ */ new Set([
9108
9116
  "generate_image",
@@ -9131,6 +9139,15 @@ function buildWorkerRules(has) {
9131
9139
  // reference straight to caption text — telling the model to call
9132
9140
  // generate_image for THOSE just pays for an image nobody ever sees.
9133
9141
  has("generate_image") && '- When a slide deck (PowerPoint) or Word document deliverable calls for a decorative image or illustration, you MUST call the "generate_image" tool and then reference its result using Markdown image syntax: ![description](location), where "location" is exactly the string the tool reported back. NEVER write a text placeholder or a bracketed description such as [image: a cat] in its place. Do NOT use "generate_image" for a chart, graph, or diagram that must show exact data (numbers, axes, labels) \u2014 an image model cannot guarantee those values are correct, so render that data as a Markdown table instead. For any OTHER deliverable format (PDF, plain text, code, etc.) do NOT call "generate_image" \u2014 those cannot embed the result, so describe the visual in words instead.',
9142
+ // The video counterpart of the image rule above, and the fix for the
9143
+ // reported "it writes scripts forever and never makes the video" run. The
9144
+ // failure mode is identical — the model narrates the deliverable instead of
9145
+ // producing it — but the stakes are not: a video subtask that ends in prose
9146
+ // has burned the whole pre-production plan for nothing. Unlike the image
9147
+ // rule this is NOT scoped to a document format, because the clip itself is
9148
+ // the deliverable rather than an illustration inside one, and the reference
9149
+ // syntax is a plain link: Markdown image syntax does not embed a video.
9150
+ has("generate_video") && '- When the subtask deliverable is a video, you MUST call the "generate_video" tool \u2014 that call IS the deliverable, and the subtask is not done until it has returned. NEVER deliver the video as prose, a script, a storyboard, or a bracketed placeholder such as [video: a cat skating], and NEVER claim a video exists unless the tool reported a location for it. Call it exactly ONCE: it is billed per second of output and renders for minutes, so a second call charges the user again. Then report the location string the tool returned VERBATIM as your result, referencing it as [description](location) if it goes inside a document. If the tool reports a failure or a timeout, report that failure verbatim and stop \u2014 do NOT call it again, and do NOT substitute a written description of a video that was never made.',
9134
9151
  has("run_code") && '- Use the "run_code" tool for any file types (Excel, Zip, csv, etc.) or complex processing not covered by other tools. Always cleanup after code execution.',
9135
9152
  "- If you are not making meaningful progress, stop and escalate rather than looping or padding the response.",
9136
9153
  has("peer_message") && '- Use the "peer_message" tool to communicate with other T3 workers if your tasks have dependencies or shared state. You can send updates or wait for signals.',
@@ -9673,6 +9690,12 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
9673
9690
  if (isTaggedSystemic || isProviderBacked && classifyProviderError(err).systemic) {
9674
9691
  throw new CriticalToolError(errMsg, tc.name);
9675
9692
  }
9693
+ if (isProviderBacked) {
9694
+ throw new CriticalToolError(
9695
+ `${tc.name} failed and was not retried \u2014 media generation is billed per attempt, so Cascade stops on the first failure instead of paying for a second. Reason: ${errMsg}`,
9696
+ tc.name
9697
+ );
9698
+ }
9676
9699
  return await this.adaptiveFallback(tc, `Tool error: ${errMsg}`);
9677
9700
  }
9678
9701
  }
@@ -9682,6 +9705,14 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
9682
9705
  * 1. Find a semantically similar registered tool and retry with same input
9683
9706
  * 2. Synthesize a new tool via ToolCreator (if available) and run it
9684
9707
  * 3. Return the original error so the agent loop can decide what to do next
9708
+ *
9709
+ * Scoped to CHEAP, task-scoped failures — the class of problem it was built
9710
+ * for is a wrong or missing tool name, where a name-similar sibling plausibly
9711
+ * does the same job for the same (near-zero) cost. PROVIDER_BACKED_TOOLS never
9712
+ * reach it: their siblings are keyword-similar but semantically different
9713
+ * (generate_image is not a stand-in for generate_video), and every attempt —
9714
+ * theirs, or a synthesized replacement's — is separately billed. See the
9715
+ * fast-fail branch in executeTool().
9685
9716
  */
9686
9717
  async adaptiveFallback(tc, originalError) {
9687
9718
  const altTool = this.findAlternativeTool(tc.name);
@@ -10434,15 +10465,245 @@ _(incomplete: ${t2Result.issues.join("; ")})_`;
10434
10465
  return output;
10435
10466
  }
10436
10467
 
10468
+ // src/core/multimodal/registry.ts
10469
+ init_pricing();
10470
+ var CAPABILITIES = [
10471
+ // ── Image ──
10472
+ {
10473
+ modality: "image",
10474
+ provider: "openai",
10475
+ modelId: "dall-e-3",
10476
+ api: "openai-images"
10477
+ },
10478
+ {
10479
+ // Was `imagen-4.0-generate-001` on `:predict`, which now 404s for new keys
10480
+ // ("no longer available to new users") — Imagen's :predict API is deprecated
10481
+ // and shuts down 2026-08-17. Google's own migration path is this model on
10482
+ // the ordinary generateContent endpoint, so the fix was never another Imagen
10483
+ // id: the whole request/response SHAPE changes, which is why `api` moved to
10484
+ // 'gemini-generate-content' rather than just the modelId moving.
10485
+ // https://ai.google.dev/gemini-api/docs/imagen
10486
+ modality: "image",
10487
+ provider: "gemini",
10488
+ modelId: "gemini-2.5-flash-image",
10489
+ api: "gemini-generate-content"
10490
+ },
10491
+ // ── Speech (text → audio) ──
10492
+ {
10493
+ modality: "speech",
10494
+ provider: "openai",
10495
+ modelId: "tts-1",
10496
+ api: "openai-speech"
10497
+ },
10498
+ {
10499
+ // The only quality claim in this table that needs no leaderboard: OpenAI
10500
+ // ships these as the same voice at two fidelities and charges double for
10501
+ // this one. That is the vendor's own ordering, not an inference.
10502
+ modality: "speech",
10503
+ provider: "openai",
10504
+ modelId: "tts-1-hd",
10505
+ api: "openai-speech",
10506
+ quality: 70,
10507
+ qualityBasis: "OpenAI's own hd variant of tts-1, priced 2x"
10508
+ },
10509
+ // ── Transcription (audio → text) ──
10510
+ {
10511
+ modality: "transcription",
10512
+ provider: "openai",
10513
+ modelId: "whisper-1",
10514
+ api: "openai-transcriptions"
10515
+ },
10516
+ // ── Video ──
10517
+ {
10518
+ // Long-running: submit returns an operation name, the video lands minutes
10519
+ // later, and the bytes come from a short-lived signed URL. generate.ts owns
10520
+ // that polling loop; the registry only records the shape via `api`.
10521
+ //
10522
+ // `veo-3.1-generate-001` (no "-preview") is VERTEX AI's id for this model,
10523
+ // not the Gemini Developer API's — this file calls
10524
+ // generativelanguage.googleapis.com (see generate.ts), which 404s on it:
10525
+ // "models/veo-3.1-generate-001 is not found for API version v1beta". On the
10526
+ // Gemini API specifically, Veo 3.1 is still preview-only, so the correct id
10527
+ // there is `veo-3.1-generate-preview`.
10528
+ // https://ai.google.dev/gemini-api/docs/veo
10529
+ modality: "video",
10530
+ provider: "gemini",
10531
+ modelId: "veo-3.1-generate-preview",
10532
+ api: "gemini-predict-lro"
10533
+ }
10534
+ ];
10535
+ function capabilityCost(cap) {
10536
+ const match = findPricing(cap.modelId, cap.provider, {
10537
+ fallbackProviders: cap.provider === "gemini" ? ["vertex"] : []
10538
+ });
10539
+ const rate = match?.entry.rates?.[0];
10540
+ if (!rate) return null;
10541
+ const unitLabel = {
10542
+ per_image: "per image",
10543
+ per_second: "per second of video",
10544
+ per_character: "per character",
10545
+ per_audio_minute: "per audio minute"
10546
+ };
10547
+ const unit = unitLabel[rate.unit] ?? rate.unit;
10548
+ const amount = rate.amount;
10549
+ const shown = amount < 1e-3 ? amount.toExponential(2) : amount.toFixed(4);
10550
+ return { label: `$${shown} ${unit}`, amount, unit };
10551
+ }
10552
+ function explainChoice(c, cost, modality, isBest) {
10553
+ const priced = cost ? ` ${cost.label}.` : "";
10554
+ if (c.quality !== void 0) {
10555
+ return isBest ? `${c.modelId} (${c.provider}) \u2014 ranked highest for ${modality}: ${c.qualityBasis}.${priced}` : `${c.modelId} (${c.provider}) \u2014 ranked alternative for ${modality}: ${c.qualityBasis}.${priced}`;
10556
+ }
10557
+ const noRanking = ". No published quality ranking is applied for this modality, so this is a cost choice, not a quality one.";
10558
+ return isBest ? `${c.modelId} (${c.provider}) \u2014 cheapest usable ${modality} model` + (cost ? ` at ${cost.label}` : "") + noRanking : `${c.modelId} (${c.provider}) \u2014 alternative usable ${modality} model` + (cost ? ` at ${cost.label}` : "") + noRanking;
10559
+ }
10560
+ var MultimodalRegistry = class {
10561
+ /** Provider types the user has actually configured a key for. */
10562
+ available;
10563
+ constructor(availableProviders) {
10564
+ this.available = new Set(availableProviders);
10565
+ }
10566
+ /** Every capability whose provider is configured AND which Cascade can call. */
10567
+ usable(modality) {
10568
+ return CAPABILITIES.filter(
10569
+ (c) => !c.unsupported && this.available.has(c.provider) && (!modality || c.modality === modality)
10570
+ );
10571
+ }
10572
+ /** Modalities this account can actually generate right now. */
10573
+ availableModalities() {
10574
+ return [...new Set(this.usable().map((c) => c.modality))];
10575
+ }
10576
+ /**
10577
+ * Every usable capability for a modality, best first.
10578
+ *
10579
+ * Ranked models beat unranked ones, then higher quality, then lower unit
10580
+ * price. An explicit `preferModel` is moved to the head when it is usable —
10581
+ * the user naming a model is a stronger signal than any prior.
10582
+ *
10583
+ * `select()` is just the head of this list. The TAIL is the point: a systemic
10584
+ * provider failure (dead key, 404, exhausted quota) is scoped to one
10585
+ * provider+model, so "this model cannot draw" is not the same fact as "this
10586
+ * account cannot draw". A caller holding the ordered alternatives can try the
10587
+ * next provider instead of aborting the whole run — see generate-media.ts.
10588
+ */
10589
+ rank(modality, opts = {}) {
10590
+ const scored = this.usable(modality).map((c) => ({ c, cost: capabilityCost(c) })).sort((a, b) => {
10591
+ const qa = a.c.quality ?? -1;
10592
+ const qb = b.c.quality ?? -1;
10593
+ if (qa !== qb) return qb - qa;
10594
+ const pa = a.cost?.amount ?? Number.POSITIVE_INFINITY;
10595
+ const pb = b.cost?.amount ?? Number.POSITIVE_INFINITY;
10596
+ return pa - pb;
10597
+ });
10598
+ const results = scored.map(({ c, cost }, i) => ({
10599
+ capability: c,
10600
+ cost,
10601
+ reason: explainChoice(c, cost, modality, i === 0)
10602
+ }));
10603
+ if (opts.preferModel) {
10604
+ const i = results.findIndex((r) => r.capability.modelId === opts.preferModel);
10605
+ if (i > -1) {
10606
+ const [wanted] = results.splice(i, 1);
10607
+ results.unshift({ ...wanted, reason: `Requested explicitly: ${wanted.capability.modelId}.` });
10608
+ }
10609
+ }
10610
+ return results;
10611
+ }
10612
+ /**
10613
+ * Pick a model for a modality — the best usable one, or null if there is none.
10614
+ */
10615
+ select(modality, opts = {}) {
10616
+ return this.rank(modality, opts)[0] ?? null;
10617
+ }
10618
+ /**
10619
+ * A plain-language account of what this account can and cannot generate —
10620
+ * including the things it cannot, and why.
10621
+ *
10622
+ * This is the USER-facing inventory (model ids and unit prices), keyed on
10623
+ * which providers are configured. It is deliberately NOT what the planner
10624
+ * reads: see `describeGenerationForPlanner()` below for that, and the note
10625
+ * there for why the two cannot be the same string.
10626
+ */
10627
+ describe() {
10628
+ const lines = [];
10629
+ const usable = this.usable();
10630
+ if (usable.length) {
10631
+ lines.push("Generation models available on your configured providers:");
10632
+ for (const modality of ["image", "speech", "transcription", "video"]) {
10633
+ const forModality = usable.filter((c) => c.modality === modality);
10634
+ if (!forModality.length) continue;
10635
+ const shown = forModality.map((c) => {
10636
+ const cost = capabilityCost(c);
10637
+ return `${c.modelId}${cost ? ` (${cost.label})` : ""}`;
10638
+ });
10639
+ lines.push(` ${modality}: ${shown.join(", ")}`);
10640
+ }
10641
+ } else {
10642
+ lines.push("No generation models are available \u2014 none of your configured providers offers one.");
10643
+ }
10644
+ const blocked = CAPABILITIES.filter((c) => c.unsupported && this.available.has(c.provider));
10645
+ for (const c of blocked) {
10646
+ lines.push(` ${c.modality}: ${c.modelId} exists on your account but is not callable. ${c.unsupported}`);
10647
+ }
10648
+ lines.push(
10649
+ " music: no supported provider exposes a music-generation API, so Cascade cannot generate music."
10650
+ );
10651
+ return lines.join("\n");
10652
+ }
10653
+ };
10654
+ var PLANNER_FACTS = [
10655
+ { tool: "generate_image", modality: "image", shape: "one call returns one finished image" },
10656
+ { tool: "generate_speech", modality: "speech", shape: "one call returns one finished audio file" },
10657
+ {
10658
+ tool: "generate_video",
10659
+ modality: "video",
10660
+ shape: "one call returns one finished clip, after rendering for 1-3 minutes (Cascade stops waiting at 8)"
10661
+ },
10662
+ {
10663
+ tool: "transcribe_audio",
10664
+ modality: "transcription",
10665
+ shape: "one call returns the transcript of one audio file"
10666
+ }
10667
+ ];
10668
+ function plannerPriceNote(modality) {
10669
+ const costs = CAPABILITIES.filter((c) => c.modality === modality && !c.unsupported).map((c) => capabilityCost(c)).filter((c) => c !== null);
10670
+ if (!costs.length) return "";
10671
+ const labels = new Set(costs.map((c) => c.label));
10672
+ if (labels.size === 1) return ` Billed ${costs[0].label}.`;
10673
+ const units = new Set(costs.map((c) => c.unit));
10674
+ return units.size === 1 ? ` Billed ${[...units][0]}.` : "";
10675
+ }
10676
+ function describeGenerationForPlanner(has) {
10677
+ const present = PLANNER_FACTS.filter((f) => has(f.tool));
10678
+ if (!present.length) return "";
10679
+ const lines = ["MEDIA GENERATION \u2014 the only generation tools this run can call:"];
10680
+ for (const f of present) {
10681
+ lines.push(`- "${f.tool}" (${f.modality}): ${f.shape}.${plannerPriceNote(f.modality)}`);
10682
+ }
10683
+ lines.push(
10684
+ "Each one is a single ATOMIC tool call, made by ONE T3 worker, that produces the finished file. It is never a multi-step pipeline, and no worker can satisfy it by describing the result in words.",
10685
+ "No other modality has a tool here \u2014 never plan a step that generates music, 3D or anything not listed above."
10686
+ );
10687
+ if (has("generate_video")) {
10688
+ lines.push(
10689
+ 'VIDEO PLANS MUST END IN THE TOOL CALL. Creative pre-production \u2014 a script, a shot list, direction notes \u2014 is expected and worth planning, but it is NOT the video. Any plan that involves video MUST contain exactly ONE subtask whose deliverable is the "generate_video" call itself; it MUST be the last subtask on that path (dependsOn the pre-production ones), and nothing after it may be required to produce the clip. Never end a video plan on a script, a storyboard or a review step. Never split one clip across several "generate_video" subtasks, and never plan a render \u2192 critique \u2192 re-render loop: every render is billed again and takes minutes.'
10690
+ );
10691
+ }
10692
+ return lines.join("\n");
10693
+ }
10694
+
10437
10695
  // src/core/tiers/t2-manager.ts
10438
- function buildT2SystemPrompt(hasPeerMessage) {
10696
+ function buildT2SystemPrompt(has) {
10697
+ const generation = describeGenerationForPlanner(has);
10439
10698
  return [
10440
10699
  "You are a T2 Manager agent in the Cascade AI system.",
10441
10700
  "Your role is to analyze a section of a task and decompose it into 2-5 discrete subtasks for T3 Workers.",
10442
10701
  'If subtasks have dependencies, you can specify "executionMode": "sequential" for the section.',
10443
- hasPeerMessage && 'Provide "peerT3Ids" to subtasks so they can coordinate using the peer_message tool.',
10444
- "Return ONLY valid JSON matching the T3 subtask array schema \u2014 no other text."
10445
- ].filter((l) => l !== false).join("\n");
10702
+ has("peer_message") && 'Provide "peerT3Ids" to subtasks so they can coordinate using the peer_message tool.',
10703
+ "Return ONLY valid JSON matching the T3 subtask array schema \u2014 no other text.",
10704
+ generation && `
10705
+ ${generation}`
10706
+ ].filter((l) => l !== false && l !== "").join("\n");
10446
10707
  }
10447
10708
  var T2Manager = class extends BaseTier {
10448
10709
  router;
@@ -10739,7 +11000,7 @@ Return ONLY the JSON array.`;
10739
11000
  const messages = [{ role: "user", content: prompt }];
10740
11001
  const result = await this.router.generate("T2", {
10741
11002
  messages,
10742
- systemPrompt: this.systemPromptOverride + buildT2SystemPrompt(this.toolRegistry.hasTool("peer_message")) + (this.hierarchyContext ? `
11003
+ systemPrompt: this.systemPromptOverride + buildT2SystemPrompt((name) => this.toolRegistry.hasTool(name)) + (this.hierarchyContext ? `
10743
11004
 
10744
11005
  HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
10745
11006
  maxTokens: 2e3,
@@ -11292,6 +11553,7 @@ function isStrongKeywordOverlap(a = [], b = []) {
11292
11553
  }
11293
11554
  function buildT1SystemPrompt(has) {
11294
11555
  const canWriteArtifacts = has("file_write") || has("file_edit") || has("run_code") || has("pdf_create");
11556
+ const generation = describeGenerationForPlanner(has);
11295
11557
  const rules = [
11296
11558
  "- Simple \u2192 1 T3, Moderate \u2192 2-3 T2s, Complex \u2192 3-5 T2s, Highly Complex \u2192 5+ T2s",
11297
11559
  "- Return ONLY valid JSON \u2014 no other text",
@@ -11326,7 +11588,9 @@ DEPENDENCY GUIDANCE:
11326
11588
  QUALITY RULES:
11327
11589
  - Each section must have a clear, testable "expectedOutput" so T2 knows when it is done.
11328
11590
  - Do NOT create trivial sections that only move files or print summaries \u2014 fold those into adjacent sections.
11329
- - If the plan would naturally produce fewer than 2 independent sections, prefer Moderate routing (single T2).`;
11591
+ - If the plan would naturally produce fewer than 2 independent sections, prefer Moderate routing (single T2).${generation ? `
11592
+
11593
+ ${generation}` : ""}`;
11330
11594
  }
11331
11595
  var T1Administrator = class extends BaseTier {
11332
11596
  router;
@@ -12024,189 +12288,6 @@ Reply with exactly one word: YES, NO, or UNSURE.
12024
12288
  // src/core/cascade.ts
12025
12289
  init_cost();
12026
12290
 
12027
- // src/core/multimodal/registry.ts
12028
- init_pricing();
12029
- var CAPABILITIES = [
12030
- // ── Image ──
12031
- {
12032
- modality: "image",
12033
- provider: "openai",
12034
- modelId: "dall-e-3",
12035
- api: "openai-images"
12036
- },
12037
- {
12038
- // Was `imagen-4.0-generate-001` on `:predict`, which now 404s for new keys
12039
- // ("no longer available to new users") — Imagen's :predict API is deprecated
12040
- // and shuts down 2026-08-17. Google's own migration path is this model on
12041
- // the ordinary generateContent endpoint, so the fix was never another Imagen
12042
- // id: the whole request/response SHAPE changes, which is why `api` moved to
12043
- // 'gemini-generate-content' rather than just the modelId moving.
12044
- // https://ai.google.dev/gemini-api/docs/imagen
12045
- modality: "image",
12046
- provider: "gemini",
12047
- modelId: "gemini-2.5-flash-image",
12048
- api: "gemini-generate-content"
12049
- },
12050
- // ── Speech (text → audio) ──
12051
- {
12052
- modality: "speech",
12053
- provider: "openai",
12054
- modelId: "tts-1",
12055
- api: "openai-speech"
12056
- },
12057
- {
12058
- // The only quality claim in this table that needs no leaderboard: OpenAI
12059
- // ships these as the same voice at two fidelities and charges double for
12060
- // this one. That is the vendor's own ordering, not an inference.
12061
- modality: "speech",
12062
- provider: "openai",
12063
- modelId: "tts-1-hd",
12064
- api: "openai-speech",
12065
- quality: 70,
12066
- qualityBasis: "OpenAI's own hd variant of tts-1, priced 2x"
12067
- },
12068
- // ── Transcription (audio → text) ──
12069
- {
12070
- modality: "transcription",
12071
- provider: "openai",
12072
- modelId: "whisper-1",
12073
- api: "openai-transcriptions"
12074
- },
12075
- // ── Video ──
12076
- {
12077
- // Long-running: submit returns an operation name, the video lands minutes
12078
- // later, and the bytes come from a short-lived signed URL. generate.ts owns
12079
- // that polling loop; the registry only records the shape via `api`.
12080
- //
12081
- // `veo-3.1-generate-001` (no "-preview") is VERTEX AI's id for this model,
12082
- // not the Gemini Developer API's — this file calls
12083
- // generativelanguage.googleapis.com (see generate.ts), which 404s on it:
12084
- // "models/veo-3.1-generate-001 is not found for API version v1beta". On the
12085
- // Gemini API specifically, Veo 3.1 is still preview-only, so the correct id
12086
- // there is `veo-3.1-generate-preview`.
12087
- // https://ai.google.dev/gemini-api/docs/veo
12088
- modality: "video",
12089
- provider: "gemini",
12090
- modelId: "veo-3.1-generate-preview",
12091
- api: "gemini-predict-lro"
12092
- }
12093
- ];
12094
- function capabilityCost(cap) {
12095
- const match = findPricing(cap.modelId, cap.provider, {
12096
- fallbackProviders: cap.provider === "gemini" ? ["vertex"] : []
12097
- });
12098
- const rate = match?.entry.rates?.[0];
12099
- if (!rate) return null;
12100
- const unitLabel = {
12101
- per_image: "per image",
12102
- per_second: "per second of video",
12103
- per_character: "per character",
12104
- per_audio_minute: "per audio minute"
12105
- };
12106
- const unit = unitLabel[rate.unit] ?? rate.unit;
12107
- const amount = rate.amount;
12108
- const shown = amount < 1e-3 ? amount.toExponential(2) : amount.toFixed(4);
12109
- return { label: `$${shown} ${unit}`, amount, unit };
12110
- }
12111
- function explainChoice(c, cost, modality, isBest) {
12112
- const priced = cost ? ` ${cost.label}.` : "";
12113
- if (c.quality !== void 0) {
12114
- return isBest ? `${c.modelId} (${c.provider}) \u2014 ranked highest for ${modality}: ${c.qualityBasis}.${priced}` : `${c.modelId} (${c.provider}) \u2014 ranked alternative for ${modality}: ${c.qualityBasis}.${priced}`;
12115
- }
12116
- const noRanking = ". No published quality ranking is applied for this modality, so this is a cost choice, not a quality one.";
12117
- return isBest ? `${c.modelId} (${c.provider}) \u2014 cheapest usable ${modality} model` + (cost ? ` at ${cost.label}` : "") + noRanking : `${c.modelId} (${c.provider}) \u2014 alternative usable ${modality} model` + (cost ? ` at ${cost.label}` : "") + noRanking;
12118
- }
12119
- var MultimodalRegistry = class {
12120
- /** Provider types the user has actually configured a key for. */
12121
- available;
12122
- constructor(availableProviders) {
12123
- this.available = new Set(availableProviders);
12124
- }
12125
- /** Every capability whose provider is configured AND which Cascade can call. */
12126
- usable(modality) {
12127
- return CAPABILITIES.filter(
12128
- (c) => !c.unsupported && this.available.has(c.provider) && (!modality || c.modality === modality)
12129
- );
12130
- }
12131
- /** Modalities this account can actually generate right now. */
12132
- availableModalities() {
12133
- return [...new Set(this.usable().map((c) => c.modality))];
12134
- }
12135
- /**
12136
- * Every usable capability for a modality, best first.
12137
- *
12138
- * Ranked models beat unranked ones, then higher quality, then lower unit
12139
- * price. An explicit `preferModel` is moved to the head when it is usable —
12140
- * the user naming a model is a stronger signal than any prior.
12141
- *
12142
- * `select()` is just the head of this list. The TAIL is the point: a systemic
12143
- * provider failure (dead key, 404, exhausted quota) is scoped to one
12144
- * provider+model, so "this model cannot draw" is not the same fact as "this
12145
- * account cannot draw". A caller holding the ordered alternatives can try the
12146
- * next provider instead of aborting the whole run — see generate-media.ts.
12147
- */
12148
- rank(modality, opts = {}) {
12149
- const scored = this.usable(modality).map((c) => ({ c, cost: capabilityCost(c) })).sort((a, b) => {
12150
- const qa = a.c.quality ?? -1;
12151
- const qb = b.c.quality ?? -1;
12152
- if (qa !== qb) return qb - qa;
12153
- const pa = a.cost?.amount ?? Number.POSITIVE_INFINITY;
12154
- const pb = b.cost?.amount ?? Number.POSITIVE_INFINITY;
12155
- return pa - pb;
12156
- });
12157
- const results = scored.map(({ c, cost }, i) => ({
12158
- capability: c,
12159
- cost,
12160
- reason: explainChoice(c, cost, modality, i === 0)
12161
- }));
12162
- if (opts.preferModel) {
12163
- const i = results.findIndex((r) => r.capability.modelId === opts.preferModel);
12164
- if (i > -1) {
12165
- const [wanted] = results.splice(i, 1);
12166
- results.unshift({ ...wanted, reason: `Requested explicitly: ${wanted.capability.modelId}.` });
12167
- }
12168
- }
12169
- return results;
12170
- }
12171
- /**
12172
- * Pick a model for a modality — the best usable one, or null if there is none.
12173
- */
12174
- select(modality, opts = {}) {
12175
- return this.rank(modality, opts)[0] ?? null;
12176
- }
12177
- /**
12178
- * A plain-language account of what this account can and cannot generate —
12179
- * including the things it cannot, and why. The planner sees this, so being
12180
- * candid here is what stops it inventing a `generate_music` call.
12181
- */
12182
- describe() {
12183
- const lines = [];
12184
- const usable = this.usable();
12185
- if (usable.length) {
12186
- lines.push("Generation models available on your configured providers:");
12187
- for (const modality of ["image", "speech", "transcription", "video"]) {
12188
- const forModality = usable.filter((c) => c.modality === modality);
12189
- if (!forModality.length) continue;
12190
- const shown = forModality.map((c) => {
12191
- const cost = capabilityCost(c);
12192
- return `${c.modelId}${cost ? ` (${cost.label})` : ""}`;
12193
- });
12194
- lines.push(` ${modality}: ${shown.join(", ")}`);
12195
- }
12196
- } else {
12197
- lines.push("No generation models are available \u2014 none of your configured providers offers one.");
12198
- }
12199
- const blocked = CAPABILITIES.filter((c) => c.unsupported && this.available.has(c.provider));
12200
- for (const c of blocked) {
12201
- lines.push(` ${c.modality}: ${c.modelId} exists on your account but is not callable. ${c.unsupported}`);
12202
- }
12203
- lines.push(
12204
- " music: no supported provider exposes a music-generation API, so Cascade cannot generate music."
12205
- );
12206
- return lines.join("\n");
12207
- }
12208
- };
12209
-
12210
12291
  // src/tools/base.ts
12211
12292
  var BaseTool = class {
12212
12293
  workspaceRoot = process.cwd();
@@ -12227,10 +12308,17 @@ var BaseTool = class {
12227
12308
 
12228
12309
  // src/core/multimodal/generate.ts
12229
12310
  var DEFAULT_TIMEOUT_MS = 12e4;
12311
+ var GenerationGaveUpError = class extends Error {
12312
+ constructor(message) {
12313
+ super(message);
12314
+ this.name = "GenerationGaveUpError";
12315
+ }
12316
+ };
12230
12317
  async function callProvider(fn, modelId) {
12231
12318
  try {
12232
12319
  return await fn();
12233
12320
  } catch (err) {
12321
+ if (err instanceof GenerationGaveUpError) throw err;
12234
12322
  const classified = classifyProviderError(err);
12235
12323
  throw new Error(describeProviderError(classified, modelId));
12236
12324
  }
@@ -12428,8 +12516,8 @@ async function generateVideo(cap, cfg, req, signal, onProgress) {
12428
12516
  onProgress?.("Still rendering\u2026");
12429
12517
  }
12430
12518
  if (!operation.done) {
12431
- throw new Error(
12432
- `Video was still rendering after ${Math.round(VIDEO_TIMEOUT_MS / 6e4)} minutes. It may still complete on the provider side, but Cascade stopped waiting.`
12519
+ throw new GenerationGaveUpError(
12520
+ `${cap.modelId} timed out after ${Math.round(VIDEO_TIMEOUT_MS / 6e4)} minutes \u2014 no video was produced. Cascade stopped waiting; the render may still finish on the provider side, but nothing was saved. Try again with a shorter clip or a simpler prompt.`
12433
12521
  );
12434
12522
  }
12435
12523
  if (operation.error?.message) throw new Error(operation.error.message);