cascade-ai 0.55.0 → 0.63.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.
@@ -191819,7 +191819,7 @@ var require_view = __commonJS({
191819
191819
  var debug3 = require_src9()("express:view");
191820
191820
  var path28 = __require("path");
191821
191821
  var fs31 = __require("fs");
191822
- var dirname2 = path28.dirname;
191822
+ var dirname3 = path28.dirname;
191823
191823
  var basename4 = path28.basename;
191824
191824
  var extname = path28.extname;
191825
191825
  var join = path28.join;
@@ -191858,7 +191858,7 @@ var require_view = __commonJS({
191858
191858
  for (var i4 = 0; i4 < roots.length && !path29; i4++) {
191859
191859
  var root = roots[i4];
191860
191860
  var loc = resolve2(root, name);
191861
- var dir = dirname2(loc);
191861
+ var dir = dirname3(loc);
191862
191862
  var file = basename4(loc);
191863
191863
  path29 = this.resolve(dir, file);
191864
191864
  }
@@ -223993,6 +223993,23 @@ var pricing_data_default = {
223993
223993
  ],
223994
223994
  notes: "Google bills image output per output-token ($120 per 1M image output tokens); $0.134/image is that rate applied to one default-resolution image. Cost per image therefore varies with resolution."
223995
223995
  },
223996
+ {
223997
+ model: "gemini-2.5-flash-image",
223998
+ provider: "gemini",
223999
+ modality: "image",
224000
+ currency: "USD",
224001
+ asOf: "2026-07-27",
224002
+ source: "https://ai.google.dev/gemini-api/docs/pricing#gemini-2.5-flash-image",
224003
+ provenance: "litellm-catalog",
224004
+ rates: [
224005
+ {
224006
+ unit: "per_image",
224007
+ amount: 0.039,
224008
+ variant: "default 1024px output"
224009
+ }
224010
+ ],
224011
+ notes: "Google bills image output per output-token ($30 per 1M image output tokens); a default 1024px image is 1290 output tokens, i.e. $0.039. Cost per image therefore varies with resolution."
224012
+ },
223996
224013
  {
223997
224014
  model: "imagen-4.0-generate-001",
223998
224015
  provider: "vertex",
@@ -224247,7 +224264,7 @@ function reconcilePrice(model, live) {
224247
224264
  }
224248
224265
 
224249
224266
  // src/constants.ts
224250
- var CASCADE_VERSION = "0.55.0";
224267
+ var CASCADE_VERSION = "0.63.0";
224251
224268
  var CASCADE_CONFIG_DIR = ".cascade";
224252
224269
  var CASCADE_MD_FILE = "CASCADE.md";
224253
224270
  var CASCADE_IGNORE_FILE = ".cascadeignore";
@@ -252383,8 +252400,6 @@ var LocalRequestQueue = class {
252383
252400
  };
252384
252401
  }
252385
252402
  };
252386
-
252387
- // src/core/router/dead-models.ts
252388
252403
  var DEAD_MODEL_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
252389
252404
  function deadModelKey(provider, modelId) {
252390
252405
  return `${provider}:${modelId}`;
@@ -252485,16 +252500,13 @@ var DeadModelStore = class {
252485
252500
  function fileDeadModelPersistence(filePath) {
252486
252501
  return {
252487
252502
  load() {
252488
- const fs31 = __require("fs");
252489
- if (!fs31.existsSync(filePath)) return [];
252490
- const raw = JSON.parse(fs31.readFileSync(filePath, "utf-8"));
252503
+ if (!fs11.existsSync(filePath)) return [];
252504
+ const raw = JSON.parse(fs11.readFileSync(filePath, "utf-8"));
252491
252505
  return Array.isArray(raw) ? raw : [];
252492
252506
  },
252493
252507
  save(records) {
252494
- const fs31 = __require("fs");
252495
- const path28 = __require("path");
252496
- fs31.mkdirSync(path28.dirname(filePath), { recursive: true });
252497
- fs31.writeFileSync(filePath, JSON.stringify(records, null, 2));
252508
+ fs11.mkdirSync(path26.dirname(filePath), { recursive: true });
252509
+ fs11.writeFileSync(filePath, JSON.stringify(records, null, 2));
252498
252510
  }
252499
252511
  };
252500
252512
  }
@@ -252928,6 +252940,12 @@ var FAMILY_BENCHMARKS = {
252928
252940
  // Google — strongest at analysis/data and long-context.
252929
252941
  "gemini-2.5-pro": { code: 90, analysis: 93, creative: 86, data: 92 },
252930
252942
  "gemini-2.5-flash": { code: 82, analysis: 83, creative: 80, data: 82 },
252943
+ // The 2.5-lite tier scores below full 2.5-flash but above the older 2.0
252944
+ // lite tier — it exists because a naive `gemini-?2\.5-flash` regex swallows
252945
+ // "gemini-2.5-flash-lite" as a substring match (no lite variant of its own
252946
+ // to fall into), so the weaker lite model got credited with the full
252947
+ // flash's benchmark score and kept winning Cascade Auto's "best value" pick.
252948
+ "gemini-2.5-flash-lite": { code: 72, analysis: 72, creative: 74, data: 72 },
252931
252949
  "gemini-1.5-pro": { code: 82, analysis: 84, creative: 82, data: 85 },
252932
252950
  "gemini-2.0-flash": { code: 79, analysis: 80, creative: 79, data: 80 },
252933
252951
  "gemini-flash-lite": { code: 68, analysis: 68, creative: 70, data: 68 },
@@ -252960,6 +252978,11 @@ var FAMILY_MATCHERS = [
252960
252978
  [/gpt-?4o-mini/i, "gpt-4o-mini"],
252961
252979
  [/gpt-?4o/i, "gpt-4o"],
252962
252980
  [/gemini-?2\.5-pro/i, "gemini-2.5-pro"],
252981
+ // Must precede the bare `2\.5-flash` rule below: that pattern has no word
252982
+ // boundary after "flash" and matches "gemini-2.5-flash-lite" as a plain
252983
+ // substring, so the lite variant would otherwise be scored as the full
252984
+ // flash model (see the FAMILY_BENCHMARKS comment above).
252985
+ [/gemini-?2\.5-flash-lite/i, "gemini-2.5-flash-lite"],
252963
252986
  [/gemini-?2\.5-flash/i, "gemini-2.5-flash"],
252964
252987
  [/gemini-?1\.5-pro/i, "gemini-1.5-pro"],
252965
252988
  [/gemini-?2\.0-flash-lite/i, "gemini-flash-lite"],
@@ -254466,6 +254489,92 @@ function truncateForContext(text, maxChars = 12e3) {
254466
254489
  ${tail}`;
254467
254490
  }
254468
254491
 
254492
+ // src/core/router/provider-errors.ts
254493
+ var SYSTEMIC = /* @__PURE__ */ new Set([
254494
+ "rate_limit",
254495
+ "quota_exhausted",
254496
+ "auth",
254497
+ "model_unavailable"
254498
+ ]);
254499
+ function isSystemicKind(kind2) {
254500
+ return SYSTEMIC.has(kind2);
254501
+ }
254502
+ function statusOf(err) {
254503
+ if (!err || typeof err !== "object") return void 0;
254504
+ const e3 = err;
254505
+ for (const key of ["status", "statusCode", "code"]) {
254506
+ const v6 = e3[key];
254507
+ if (typeof v6 === "number" && v6 >= 100 && v6 < 600) return v6;
254508
+ }
254509
+ const resp = e3["response"];
254510
+ if (resp && typeof resp["status"] === "number") return resp["status"];
254511
+ const inner = e3["error"];
254512
+ if (inner && typeof inner["code"] === "number") return inner["code"];
254513
+ return void 0;
254514
+ }
254515
+ function messageOf(err) {
254516
+ if (err instanceof Error) return err.message;
254517
+ if (typeof err === "string") return err;
254518
+ if (err && typeof err === "object") {
254519
+ const e3 = err;
254520
+ if (typeof e3["message"] === "string") return e3["message"];
254521
+ const inner = e3["error"];
254522
+ if (inner && typeof inner["message"] === "string") return inner["message"];
254523
+ try {
254524
+ return JSON.stringify(err);
254525
+ } catch {
254526
+ }
254527
+ }
254528
+ return String(err);
254529
+ }
254530
+ function classifyProviderError(err) {
254531
+ const raw = messageOf(err).trim();
254532
+ const status = statusOf(err);
254533
+ const m3 = raw.toLowerCase();
254534
+ const kind2 = (() => {
254535
+ if (status === 429) {
254536
+ return /quota|billing|credit|exceeded your current quota|insufficient/.test(m3) ? "quota_exhausted" : "rate_limit";
254537
+ }
254538
+ if (status === 401 || status === 403) return "auth";
254539
+ if (status === 404) return "model_unavailable";
254540
+ if (/rate limit|too many requests|resource_exhausted/.test(m3)) {
254541
+ return /quota|billing|credit|insufficient/.test(m3) ? "quota_exhausted" : "rate_limit";
254542
+ }
254543
+ if (/quota|billing|insufficient_quota|insufficient credit|payment required/.test(m3)) return "quota_exhausted";
254544
+ if (/api key|unauthorized|authentication|permission denied|invalid[_ ]api/.test(m3)) return "auth";
254545
+ if (/model .*(not found|does not exist|unavailable)|no such model|unknown model|not supported for/.test(m3)) {
254546
+ return "model_unavailable";
254547
+ }
254548
+ if (/context length|too many tokens|maximum context|prompt is too long|input too long/.test(m3)) return "context_length";
254549
+ if (/safety|content filter|blocked|recitation|prohibited/.test(m3)) return "content_filter";
254550
+ if (/econnreset|etimedout|enotfound|socket hang up|network|fetch failed|aborted/.test(m3)) return "network";
254551
+ return "unknown";
254552
+ })();
254553
+ return { kind: kind2, systemic: isSystemicKind(kind2), status, raw };
254554
+ }
254555
+ function describeProviderError(c4, modelId) {
254556
+ const on = modelId ? ` on ${modelId}` : "";
254557
+ const detail = c4.raw ? ` Provider said: ${c4.raw}` : "";
254558
+ switch (c4.kind) {
254559
+ case "rate_limit":
254560
+ return `Rate limit hit${on}. The provider is throttling requests \u2014 lower the number of parallel workers, or use a model with a higher requests-per-minute allowance.${detail}`;
254561
+ case "quota_exhausted":
254562
+ return `Quota or billing limit reached${on}. This will not recover on its own \u2014 check the provider's billing page, or switch this tier to a different provider.${detail}`;
254563
+ case "auth":
254564
+ return `Authentication failed${on}. The API key is missing, expired, or lacks access to this model \u2014 re-check the key in Settings > Providers.${detail}`;
254565
+ case "model_unavailable":
254566
+ return `Model unavailable${on}. The id may be wrong, retired, or not enabled for this API key \u2014 pick a different model for this tier.${detail}`;
254567
+ case "context_length":
254568
+ return `The prompt was too long${on}. Reduce the attached context, or use a model with a larger context window for this tier.${detail}`;
254569
+ case "content_filter":
254570
+ return `The provider's safety filter refused this request${on}.${detail}`;
254571
+ case "network":
254572
+ return `Network error reaching the provider${on}. This is usually transient.${detail}`;
254573
+ default:
254574
+ return `The model call failed${on}.${detail}`;
254575
+ }
254576
+ }
254577
+
254469
254578
  // src/core/tiers/t3-worker.ts
254470
254579
  var CriticalToolError = class extends Error {
254471
254580
  constructor(message, toolName) {
@@ -254499,8 +254608,15 @@ var KNOWN_TOOLS = [
254499
254608
  "web_search",
254500
254609
  "glob",
254501
254610
  "grep",
254502
- "web_fetch"
254611
+ "web_fetch",
254612
+ "generate_image"
254503
254613
  ];
254614
+ var PROVIDER_BACKED_TOOLS = /* @__PURE__ */ new Set([
254615
+ "generate_image",
254616
+ "generate_speech",
254617
+ "generate_video",
254618
+ "transcribe_audio"
254619
+ ]);
254504
254620
  function buildWorkerRules(has2) {
254505
254621
  const canWriteFiles = has2("file_write") || has2("file_edit") || has2("run_code");
254506
254622
  const hasAnyTool = KNOWN_TOOLS.some(has2);
@@ -254510,9 +254626,22 @@ function buildWorkerRules(has2) {
254510
254626
  canWriteFiles && "- If the task asks for a file or artifact, you must actually create it in the workspace, verify that it exists, and inspect it before claiming success.",
254511
254627
  has2("web_search") && '- Use the "web_search" tool to find current information, documentation, news, or general web data.',
254512
254628
  has2("pdf_create") && '- Use the "pdf_create" tool for PDF requests.',
254629
+ // Without this the model "writes the image" as prose — a bracketed
254630
+ // description sitting in the deck where the picture should be — even with a
254631
+ // working image model registered. Naming the reference syntax matters as
254632
+ // much as naming the tool: a generated image nobody embeds is still a
254633
+ // missing image.
254634
+ //
254635
+ // Scoped to PowerPoint/Word specifically: those are the only exporters
254636
+ // that actually embed a Markdown image reference (cloud/web/exporters.ts
254637
+ // toPptx/toDocx). PDF and plain-text/Markdown deliverables flatten a
254638
+ // reference straight to caption text — telling the model to call
254639
+ // generate_image for THOSE just pays for an image nobody ever sees.
254640
+ has2("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.',
254513
254641
  has2("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.',
254514
254642
  "- If you are not making meaningful progress, stop and escalate rather than looping or padding the response.",
254515
254643
  has2("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.',
254644
+ hasAnyTool && "- Only use tools directly relevant to THIS subtask. Do not reach for an unrelated connected-service action (e.g. creating, deleting, or modifying a repository, issue, or PR; sending a message) unless the subtask explicitly calls for it.",
254516
254645
  "- Return structured output that directly addresses the expected output specification."
254517
254646
  ];
254518
254647
  return `You are a T3 Worker agent in the Cascade AI system. Your job is to execute a specific subtask completely and accurately.
@@ -255046,7 +255175,9 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
255046
255175
  const durationMs = Date.now() - toolStartMs;
255047
255176
  const errMsg = err instanceof Error ? err.message : String(err);
255048
255177
  this.emit("tool:result", { id: tc.id, tierId: this.id, toolName: tc.name, error: errMsg, durationMs });
255049
- if (/\b(429|rate.?limit|authentication|api.?key|forbidden|401|403)\b/i.test(errMsg)) {
255178
+ const isTaggedSystemic = err instanceof Error && err.systemic === true;
255179
+ const isProviderBacked = PROVIDER_BACKED_TOOLS.has(tc.name);
255180
+ if (isTaggedSystemic || isProviderBacked && classifyProviderError(err).systemic) {
255050
255181
  throw new CriticalToolError(errMsg, tc.name);
255051
255182
  }
255052
255183
  return await this.adaptiveFallback(tc, `Tool error: ${errMsg}`);
@@ -255334,8 +255465,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
255334
255465
  } catch {
255335
255466
  return {
255336
255467
  checksRun: ["completeness", "correctness", "compliance"],
255337
- passed: ["completeness", "correctness", "compliance"],
255338
- failed: []
255468
+ passed: [],
255469
+ failed: ["completeness"]
255339
255470
  };
255340
255471
  }
255341
255472
  }
@@ -256125,7 +256256,7 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
256125
256256
  const jsonMatch = /\[[\s\S]*\]/.exec(result.content);
256126
256257
  if (!jsonMatch) throw new Error("No JSON array found");
256127
256258
  const parsed = JSON.parse(jsonMatch[0]);
256128
- return parsed.map((a2) => ({ ...a2, sectionTitle: assignment.sectionTitle }));
256259
+ return parsed.map((a2) => ({ ...a2, sectionTitle: assignment.sectionTitle, constraints: a2.constraints ?? [] }));
256129
256260
  } catch {
256130
256261
  return [{
256131
256262
  subtaskId: crypto6.randomUUID(),
@@ -257083,6 +257214,12 @@ SPEC RULES \u2014 each subtask is a self-contained spec slice (workers execute f
257083
257214
  if (!plan.sections || !Array.isArray(plan.sections) || plan.sections.length === 0) {
257084
257215
  throw new Error("Invalid plan: no sections");
257085
257216
  }
257217
+ for (const section of plan.sections) {
257218
+ section.constraints ??= [];
257219
+ for (const subtask of section.t3Subtasks ?? []) {
257220
+ subtask.constraints ??= [];
257221
+ }
257222
+ }
257086
257223
  const [min, max] = COMPLEXITY_T2_COUNT[plan.complexity] ?? [1, 8];
257087
257224
  if (plan.sections.length < min) ;
257088
257225
  }
@@ -257398,17 +257535,17 @@ var CAPABILITIES = [
257398
257535
  api: "openai-images"
257399
257536
  },
257400
257537
  {
257401
- // Imagen, not `gemini-3-pro-image`. That id appears on Google's PRICING
257402
- // page but 404s on `:predict` ("not found for API version v1beta, or is not
257403
- // supported for predict") — the Gemini image models are called through
257404
- // generateContent with an image response modality, while `:predict` is the
257405
- // Imagen shape this executor implements. A billing line item is not proof
257406
- // of a callable endpoint, which is the assumption that put the wrong id
257407
- // here in the first place.
257538
+ // Was `imagen-4.0-generate-001` on `:predict`, which now 404s for new keys
257539
+ // ("no longer available to new users") — Imagen's :predict API is deprecated
257540
+ // and shuts down 2026-08-17. Google's own migration path is this model on
257541
+ // the ordinary generateContent endpoint, so the fix was never another Imagen
257542
+ // id: the whole request/response SHAPE changes, which is why `api` moved to
257543
+ // 'gemini-generate-content' rather than just the modelId moving.
257544
+ // https://ai.google.dev/gemini-api/docs/imagen
257408
257545
  modality: "image",
257409
257546
  provider: "gemini",
257410
- modelId: "imagen-4.0-generate-001",
257411
- api: "gemini-predict"
257547
+ modelId: "gemini-2.5-flash-image",
257548
+ api: "gemini-generate-content"
257412
257549
  },
257413
257550
  // ── Speech (text → audio) ──
257414
257551
  {
@@ -257463,6 +257600,14 @@ function capabilityCost(cap) {
257463
257600
  const shown = amount < 1e-3 ? amount.toExponential(2) : amount.toFixed(4);
257464
257601
  return { label: `$${shown} ${unit}`, amount, unit };
257465
257602
  }
257603
+ function explainChoice(c4, cost, modality, isBest) {
257604
+ const priced = cost ? ` ${cost.label}.` : "";
257605
+ if (c4.quality !== void 0) {
257606
+ return isBest ? `${c4.modelId} (${c4.provider}) \u2014 ranked highest for ${modality}: ${c4.qualityBasis}.${priced}` : `${c4.modelId} (${c4.provider}) \u2014 ranked alternative for ${modality}: ${c4.qualityBasis}.${priced}`;
257607
+ }
257608
+ const noRanking = ". No published quality ranking is applied for this modality, so this is a cost choice, not a quality one.";
257609
+ return isBest ? `${c4.modelId} (${c4.provider}) \u2014 cheapest usable ${modality} model` + (cost ? ` at ${cost.label}` : "") + noRanking : `${c4.modelId} (${c4.provider}) \u2014 alternative usable ${modality} model` + (cost ? ` at ${cost.label}` : "") + noRanking;
257610
+ }
257466
257611
  var MultimodalRegistry = class {
257467
257612
  /** Provider types the user has actually configured a key for. */
257468
257613
  available;
@@ -257480,22 +257625,20 @@ var MultimodalRegistry = class {
257480
257625
  return [...new Set(this.usable().map((c4) => c4.modality))];
257481
257626
  }
257482
257627
  /**
257483
- * Pick a model for a modality.
257628
+ * Every usable capability for a modality, best first.
257484
257629
  *
257485
257630
  * Ranked models beat unranked ones, then higher quality, then lower unit
257486
- * price. An explicit `preferModel` wins outright when it is usable — the user
257487
- * naming a model is a stronger signal than any prior.
257631
+ * price. An explicit `preferModel` is moved to the head when it is usable —
257632
+ * the user naming a model is a stronger signal than any prior.
257633
+ *
257634
+ * `select()` is just the head of this list. The TAIL is the point: a systemic
257635
+ * provider failure (dead key, 404, exhausted quota) is scoped to one
257636
+ * provider+model, so "this model cannot draw" is not the same fact as "this
257637
+ * account cannot draw". A caller holding the ordered alternatives can try the
257638
+ * next provider instead of aborting the whole run — see generate-media.ts.
257488
257639
  */
257489
- select(modality, opts = {}) {
257490
- const pool = this.usable(modality);
257491
- if (!pool.length) return null;
257492
- if (opts.preferModel) {
257493
- const wanted = pool.find((c4) => c4.modelId === opts.preferModel);
257494
- if (wanted) {
257495
- return { capability: wanted, cost: capabilityCost(wanted), reason: `Requested explicitly: ${wanted.modelId}.` };
257496
- }
257497
- }
257498
- const scored = pool.map((c4) => ({ c: c4, cost: capabilityCost(c4) })).sort((a2, b3) => {
257640
+ rank(modality, opts = {}) {
257641
+ const scored = this.usable(modality).map((c4) => ({ c: c4, cost: capabilityCost(c4) })).sort((a2, b3) => {
257499
257642
  const qa = a2.c.quality ?? -1;
257500
257643
  const qb = b3.c.quality ?? -1;
257501
257644
  if (qa !== qb) return qb - qa;
@@ -257503,10 +257646,25 @@ var MultimodalRegistry = class {
257503
257646
  const pb = b3.cost?.amount ?? Number.POSITIVE_INFINITY;
257504
257647
  return pa - pb;
257505
257648
  });
257506
- const best = scored[0];
257507
- const ranked = best.c.quality !== void 0;
257508
- 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.";
257509
- return { capability: best.c, cost: best.cost, reason };
257649
+ const results = scored.map(({ c: c4, cost }, i4) => ({
257650
+ capability: c4,
257651
+ cost,
257652
+ reason: explainChoice(c4, cost, modality, i4 === 0)
257653
+ }));
257654
+ if (opts.preferModel) {
257655
+ const i4 = results.findIndex((r4) => r4.capability.modelId === opts.preferModel);
257656
+ if (i4 > -1) {
257657
+ const [wanted] = results.splice(i4, 1);
257658
+ results.unshift({ ...wanted, reason: `Requested explicitly: ${wanted.capability.modelId}.` });
257659
+ }
257660
+ }
257661
+ return results;
257662
+ }
257663
+ /**
257664
+ * Pick a model for a modality — the best usable one, or null if there is none.
257665
+ */
257666
+ select(modality, opts = {}) {
257667
+ return this.rank(modality, opts)[0] ?? null;
257510
257668
  }
257511
257669
  /**
257512
257670
  * A plain-language account of what this account can and cannot generate —
@@ -257559,92 +257717,6 @@ var BaseTool = class {
257559
257717
  }
257560
257718
  };
257561
257719
 
257562
- // src/core/router/provider-errors.ts
257563
- var SYSTEMIC = /* @__PURE__ */ new Set([
257564
- "rate_limit",
257565
- "quota_exhausted",
257566
- "auth",
257567
- "model_unavailable"
257568
- ]);
257569
- function isSystemicKind(kind2) {
257570
- return SYSTEMIC.has(kind2);
257571
- }
257572
- function statusOf(err) {
257573
- if (!err || typeof err !== "object") return void 0;
257574
- const e3 = err;
257575
- for (const key of ["status", "statusCode", "code"]) {
257576
- const v6 = e3[key];
257577
- if (typeof v6 === "number" && v6 >= 100 && v6 < 600) return v6;
257578
- }
257579
- const resp = e3["response"];
257580
- if (resp && typeof resp["status"] === "number") return resp["status"];
257581
- const inner = e3["error"];
257582
- if (inner && typeof inner["code"] === "number") return inner["code"];
257583
- return void 0;
257584
- }
257585
- function messageOf(err) {
257586
- if (err instanceof Error) return err.message;
257587
- if (typeof err === "string") return err;
257588
- if (err && typeof err === "object") {
257589
- const e3 = err;
257590
- if (typeof e3["message"] === "string") return e3["message"];
257591
- const inner = e3["error"];
257592
- if (inner && typeof inner["message"] === "string") return inner["message"];
257593
- try {
257594
- return JSON.stringify(err);
257595
- } catch {
257596
- }
257597
- }
257598
- return String(err);
257599
- }
257600
- function classifyProviderError(err) {
257601
- const raw = messageOf(err).trim();
257602
- const status = statusOf(err);
257603
- const m3 = raw.toLowerCase();
257604
- const kind2 = (() => {
257605
- if (status === 429) {
257606
- return /quota|billing|credit|exceeded your current quota|insufficient/.test(m3) ? "quota_exhausted" : "rate_limit";
257607
- }
257608
- if (status === 401 || status === 403) return "auth";
257609
- if (status === 404) return "model_unavailable";
257610
- if (/rate limit|too many requests|resource_exhausted/.test(m3)) {
257611
- return /quota|billing|credit|insufficient/.test(m3) ? "quota_exhausted" : "rate_limit";
257612
- }
257613
- if (/quota|billing|insufficient_quota|insufficient credit|payment required/.test(m3)) return "quota_exhausted";
257614
- if (/api key|unauthorized|authentication|permission denied|invalid[_ ]api/.test(m3)) return "auth";
257615
- if (/model .*(not found|does not exist|unavailable)|no such model|unknown model|not supported for/.test(m3)) {
257616
- return "model_unavailable";
257617
- }
257618
- if (/context length|too many tokens|maximum context|prompt is too long|input too long/.test(m3)) return "context_length";
257619
- if (/safety|content filter|blocked|recitation|prohibited/.test(m3)) return "content_filter";
257620
- if (/econnreset|etimedout|enotfound|socket hang up|network|fetch failed|aborted/.test(m3)) return "network";
257621
- return "unknown";
257622
- })();
257623
- return { kind: kind2, systemic: isSystemicKind(kind2), status, raw };
257624
- }
257625
- function describeProviderError(c4, modelId) {
257626
- const on = modelId ? ` on ${modelId}` : "";
257627
- const detail = c4.raw ? ` Provider said: ${c4.raw}` : "";
257628
- switch (c4.kind) {
257629
- case "rate_limit":
257630
- return `Rate limit hit${on}. The provider is throttling requests \u2014 lower the number of parallel workers, or use a model with a higher requests-per-minute allowance.${detail}`;
257631
- case "quota_exhausted":
257632
- return `Quota or billing limit reached${on}. This will not recover on its own \u2014 check the provider's billing page, or switch this tier to a different provider.${detail}`;
257633
- case "auth":
257634
- return `Authentication failed${on}. The API key is missing, expired, or lacks access to this model \u2014 re-check the key in Settings > Providers.${detail}`;
257635
- case "model_unavailable":
257636
- return `Model unavailable${on}. The id may be wrong, retired, or not enabled for this API key \u2014 pick a different model for this tier.${detail}`;
257637
- case "context_length":
257638
- return `The prompt was too long${on}. Reduce the attached context, or use a model with a larger context window for this tier.${detail}`;
257639
- case "content_filter":
257640
- return `The provider's safety filter refused this request${on}.${detail}`;
257641
- case "network":
257642
- return `Network error reaching the provider${on}. This is usually transient.${detail}`;
257643
- default:
257644
- return `The model call failed${on}.${detail}`;
257645
- }
257646
- }
257647
-
257648
257720
  // src/core/multimodal/generate.ts
257649
257721
  var DEFAULT_TIMEOUT_MS = 12e4;
257650
257722
  async function callProvider(fn, modelId) {
@@ -257683,6 +257755,29 @@ function baseUrlFor(cfg, fallback) {
257683
257755
  function stamp(ext) {
257684
257756
  return `cascade-${Date.now()}.${ext}`;
257685
257757
  }
257758
+ var GEMINI_ASPECT_RATIOS = [
257759
+ ["1:1", 1 / 1],
257760
+ ["3:2", 3 / 2],
257761
+ ["2:3", 2 / 3],
257762
+ ["3:4", 3 / 4],
257763
+ ["4:3", 4 / 3],
257764
+ ["4:5", 4 / 5],
257765
+ ["5:4", 5 / 4],
257766
+ ["9:16", 9 / 16],
257767
+ ["16:9", 16 / 9],
257768
+ ["21:9", 21 / 9]
257769
+ ];
257770
+ function closestGeminiAspectRatio(size) {
257771
+ const m3 = size.match(/^(\d+)\s*[x×]\s*(\d+)$/i);
257772
+ if (!m3) return void 0;
257773
+ const w2 = Number(m3[1]);
257774
+ const h4 = Number(m3[2]);
257775
+ if (!w2 || !h4) return void 0;
257776
+ const ratio = w2 / h4;
257777
+ return GEMINI_ASPECT_RATIOS.reduce(
257778
+ (best, cand) => Math.abs(cand[1] - ratio) < Math.abs(best[1] - ratio) ? cand : best
257779
+ )[0];
257780
+ }
257686
257781
  async function generateImage(cap, cfg, req, signal) {
257687
257782
  return callProvider(async () => {
257688
257783
  if (cap.api === "openai-images") {
@@ -257707,19 +257802,37 @@ async function generateImage(cap, cfg, req, signal) {
257707
257802
  ...first3.revised_prompt ? { revisedPrompt: first3.revised_prompt } : {}
257708
257803
  };
257709
257804
  }
257710
- if (cap.api === "gemini-predict") {
257805
+ if (cap.api === "gemini-generate-content") {
257711
257806
  const base = baseUrlFor(cfg, "https://generativelanguage.googleapis.com/v1beta");
257712
- const url2 = `${base}/models/${encodeURIComponent(cap.modelId)}:predict`;
257807
+ const url2 = `${base}/models/${encodeURIComponent(cap.modelId)}:generateContent`;
257808
+ const aspectRatio = req.size ? closestGeminiAspectRatio(req.size) : void 0;
257713
257809
  const res = await postJson(url2, {
257714
- instances: [{ prompt: req.prompt }],
257715
- parameters: { sampleCount: 1 }
257810
+ contents: [{ parts: [{ text: req.prompt }] }],
257811
+ generationConfig: {
257812
+ // Asked for explicitly: these models can answer with text only, and a
257813
+ // text-only reply to "draw me a cat" is a failed image generation.
257814
+ responseModalities: ["IMAGE", "TEXT"],
257815
+ // Gemini has no free-form pixel size — only a named aspect ratio (see
257816
+ // closestGeminiAspectRatio). Omitted entirely when there's no
257817
+ // requested size, or no supported ratio close enough to bother with.
257818
+ ...aspectRatio ? { imageConfig: { aspectRatio } } : {}
257819
+ }
257716
257820
  }, { "x-goog-api-key": cfg.apiKey ?? "" }, signal);
257717
257821
  const body = await res.json();
257718
- const first3 = body.predictions?.[0];
257719
- if (!first3?.bytesBase64Encoded) throw new Error("Image response contained no image data.");
257720
- const mimeType = first3.mimeType ?? "image/png";
257822
+ const candidate = body.candidates?.[0];
257823
+ const parts = candidate?.content?.parts ?? [];
257824
+ const image = parts.find((p3) => p3.inlineData?.data)?.inlineData;
257825
+ if (!image?.data) {
257826
+ const blocked = body.promptFeedback?.blockReason ?? (candidate?.finishReason && candidate.finishReason !== "STOP" ? candidate.finishReason : void 0);
257827
+ if (blocked) throw new Error(`Image request was blocked by the provider (${blocked}).`);
257828
+ const said = parts.find((p3) => p3.text)?.text?.trim();
257829
+ throw new Error(
257830
+ "Image response contained no image data." + (said ? ` The model replied with text instead: ${said}` : "")
257831
+ );
257832
+ }
257833
+ const mimeType = image.mimeType ?? "image/png";
257721
257834
  return {
257722
- data: Buffer.from(first3.bytesBase64Encoded, "base64"),
257835
+ data: Buffer.from(image.data, "base64"),
257723
257836
  mimeType,
257724
257837
  filename: stamp(mimeType.includes("jpeg") ? "jpg" : "png"),
257725
257838
  modelId: cap.modelId,
@@ -257872,6 +257985,49 @@ function resolve(deps, modality, preferModel) {
257872
257985
  }
257873
257986
  return { ok: true, selected, cfg };
257874
257987
  }
257988
+ function messageOf2(err) {
257989
+ return err instanceof Error ? err.message : String(err);
257990
+ }
257991
+ async function runWithProviderFallback(deps, modality, first3, run, signal) {
257992
+ const failed = first3.selected.capability;
257993
+ try {
257994
+ return { value: await run(failed, first3.cfg), selected: first3.selected };
257995
+ } catch (err) {
257996
+ if (signal?.aborted || !classifyProviderError(err).systemic) throw err;
257997
+ const tried = [`${failed.modelId} (${failed.provider}): ${messageOf2(err)}`];
257998
+ let allSystemic = true;
257999
+ let lastNonSystemic;
258000
+ const seen = /* @__PURE__ */ new Set([failed.provider]);
258001
+ for (const alt of deps.registry.rank(modality)) {
258002
+ if (seen.has(alt.capability.provider)) continue;
258003
+ const cfg = deps.lookupProvider(alt.capability.provider);
258004
+ if (!cfg) continue;
258005
+ seen.add(alt.capability.provider);
258006
+ try {
258007
+ return {
258008
+ value: await run(alt.capability, cfg),
258009
+ selected: alt,
258010
+ fellBackFrom: { capability: failed, error: messageOf2(err) }
258011
+ };
258012
+ } catch (retryErr) {
258013
+ if (signal?.aborted) throw retryErr;
258014
+ tried.push(`${alt.capability.modelId} (${alt.capability.provider}): ${messageOf2(retryErr)}`);
258015
+ if (classifyProviderError(retryErr).systemic) {
258016
+ continue;
258017
+ }
258018
+ allSystemic = false;
258019
+ lastNonSystemic = retryErr;
258020
+ }
258021
+ }
258022
+ if (tried.length === 1) throw err;
258023
+ if (!allSystemic) throw lastNonSystemic;
258024
+ const exhausted = new Error(
258025
+ `${modality} generation failed on every configured provider (${seen.size} tried). ${tried.join(" ")}`
258026
+ );
258027
+ Object.assign(exhausted, { systemic: true });
258028
+ throw exhausted;
258029
+ }
258030
+ }
257875
258031
  var GenerateImageTool = class extends BaseTool {
257876
258032
  constructor(deps) {
257877
258033
  super();
@@ -257895,16 +258051,32 @@ var GenerateImageTool = class extends BaseTool {
257895
258051
  const r4 = resolve(this.deps, "image", input["model"] ? String(input["model"]) : void 0);
257896
258052
  if (!r4.ok) return r4.error;
257897
258053
  const size = input["size"] ? String(input["size"]) : void 0;
257898
- const asset = await generateImage(
257899
- r4.selected.capability,
257900
- r4.cfg,
257901
- { prompt, ...size ? { size } : {} },
258054
+ const attempt = await runWithProviderFallback(
258055
+ this.deps,
258056
+ "image",
258057
+ r4,
258058
+ (cap, cfg) => generateImage(cap, cfg, { prompt, ...size ? { size } : {} }, options.signal),
257902
258059
  options.signal
257903
258060
  );
258061
+ const asset = attempt.value;
258062
+ const fell = attempt.fellBackFrom;
257904
258063
  const location = await this.deps.sink(asset);
257905
258064
  return [
257906
258065
  `Image generated and saved to ${location}.`,
257907
- `Model: ${asset.modelId} \u2014 ${r4.selected.reason}`,
258066
+ // Knowing WHERE the image is isn't the same as knowing how to use it: a
258067
+ // model told only "saved to X" tends to describe the picture in prose
258068
+ // instead of embedding it. Spelling out the reference syntax here makes
258069
+ // the worker's "never write a placeholder" rule concretely actionable,
258070
+ // and it works for every host because `location` is whatever that host's
258071
+ // sink returned — a workspace path on desktop/CLI, a URL in the cloud.
258072
+ `To include this image in your output, reference it as: ![description](${location})`,
258073
+ // The reported model has to be the one that actually drew the picture, not
258074
+ // the one that was picked and then failed.
258075
+ `Model: ${asset.modelId} \u2014 ${fell ? `used as a fallback after ${fell.capability.modelId} (${fell.capability.provider}) failed with a systemic error.` : attempt.selected.reason}`,
258076
+ // Worth one line: the run succeeded, so nothing else will report that a
258077
+ // configured provider is broken, and "your Gemini key is dead" is exactly
258078
+ // the kind of thing the user wants to hear once rather than never.
258079
+ fell ? `Note: ${fell.capability.provider} failed first \u2014 ${fell.error}` : "",
257908
258080
  // DALL·E rewrites the prompt before drawing; the model needs to know what
257909
258081
  // was actually rendered, not what it asked for, or its description of the
257910
258082
  // result will not match the image.
@@ -258150,6 +258322,87 @@ function mcpToolName(serverName, toolName) {
258150
258322
  function isMcpToolName(name) {
258151
258323
  return name.startsWith(MCP_TOOL_PREFIX);
258152
258324
  }
258325
+ var READ_ONLY_MCP_VERBS = ["list", "get", "search", "read", "describe", "find", "query", "fetch", "show", "view"];
258326
+ var MUTATING_MCP_TOKENS = /* @__PURE__ */ new Set([
258327
+ "create",
258328
+ "delete",
258329
+ "remove",
258330
+ "update",
258331
+ "write",
258332
+ "modify",
258333
+ "set",
258334
+ "put",
258335
+ "post",
258336
+ "patch",
258337
+ "push",
258338
+ "merge",
258339
+ "send",
258340
+ "execute",
258341
+ "exec",
258342
+ "run",
258343
+ "deploy",
258344
+ "install",
258345
+ "uninstall",
258346
+ "upload",
258347
+ "publish",
258348
+ "revoke",
258349
+ "grant",
258350
+ "approve",
258351
+ "reject",
258352
+ "cancel",
258353
+ "enable",
258354
+ "disable",
258355
+ "add",
258356
+ "insert",
258357
+ "drop",
258358
+ "truncate",
258359
+ "clear",
258360
+ "reset",
258361
+ "restart",
258362
+ "stop",
258363
+ "kill",
258364
+ "terminate",
258365
+ "rename",
258366
+ "move",
258367
+ "fork",
258368
+ "sync",
258369
+ "replace",
258370
+ "overwrite",
258371
+ "invite",
258372
+ "ban",
258373
+ "block",
258374
+ "assign",
258375
+ "lock",
258376
+ "unlock",
258377
+ "archive",
258378
+ "star",
258379
+ "comment",
258380
+ "reply",
258381
+ "submit",
258382
+ "apply",
258383
+ "trigger",
258384
+ "invoke",
258385
+ "purge",
258386
+ "migrate",
258387
+ "edit",
258388
+ "save",
258389
+ "commit"
258390
+ ]);
258391
+ function mcpTokens(rawToolName) {
258392
+ return rawToolName.split(/[_-]+/).flatMap((segment) => segment.split(/(?<=[a-z0-9])(?=[A-Z])/)).map((t4) => t4.toLowerCase()).filter(Boolean);
258393
+ }
258394
+ function isReadOnlyMcpToolName(rawToolName) {
258395
+ if (mcpTokens(rawToolName).some((t4) => MUTATING_MCP_TOKENS.has(t4))) return false;
258396
+ const lower2 = rawToolName.toLowerCase();
258397
+ for (const verb of READ_ONLY_MCP_VERBS) {
258398
+ if (!lower2.startsWith(verb)) continue;
258399
+ const boundary = rawToolName[verb.length];
258400
+ if (boundary === void 0 || boundary === "_" || boundary === "-" || boundary >= "A" && boundary <= "Z") {
258401
+ return true;
258402
+ }
258403
+ }
258404
+ return false;
258405
+ }
258153
258406
  function uniqueMcpServerName(desired, existingNames) {
258154
258407
  const taken = new Set(existingNames.map(mcpServerPrefix));
258155
258408
  if (!taken.has(mcpServerPrefix(desired))) return desired;
@@ -286129,9 +286382,11 @@ var WebSearchTool = class extends BaseTool {
286129
286382
  );
286130
286383
  const errors = [];
286131
286384
  let results = [];
286385
+ let anyBackendReachable = false;
286132
286386
  if (this.config.searxngUrl) {
286133
286387
  try {
286134
286388
  results = await searchSearXNG(query, this.config.searxngUrl, maxResults);
286389
+ anyBackendReachable = true;
286135
286390
  if (results.length > 0) return this.formatResults(query, results);
286136
286391
  errors.push("SearXNG: returned 0 results");
286137
286392
  } catch (err) {
@@ -286141,6 +286396,7 @@ var WebSearchTool = class extends BaseTool {
286141
286396
  if (this.config.braveApiKey) {
286142
286397
  try {
286143
286398
  results = await searchBrave(query, this.config.braveApiKey, maxResults);
286399
+ anyBackendReachable = true;
286144
286400
  if (results.length > 0) return this.formatResults(query, results);
286145
286401
  errors.push("Brave: returned 0 results");
286146
286402
  } catch (err) {
@@ -286150,6 +286406,7 @@ var WebSearchTool = class extends BaseTool {
286150
286406
  if (this.config.tavilyApiKey) {
286151
286407
  try {
286152
286408
  results = await searchTavily(query, this.config.tavilyApiKey, maxResults);
286409
+ anyBackendReachable = true;
286153
286410
  if (results.length > 0) return this.formatResults(query, results);
286154
286411
  errors.push("Tavily: returned 0 results");
286155
286412
  } catch (err) {
@@ -286159,6 +286416,7 @@ var WebSearchTool = class extends BaseTool {
286159
286416
  for (const variant of ["html", "lite"]) {
286160
286417
  try {
286161
286418
  results = await searchDuckDuckGo(query, maxResults, variant);
286419
+ anyBackendReachable = true;
286162
286420
  if (results.length > 0) return this.formatResults(query, results);
286163
286421
  errors.push(`DuckDuckGo ${variant}: returned 0 results`);
286164
286422
  } catch (err) {
@@ -286166,11 +286424,11 @@ var WebSearchTool = class extends BaseTool {
286166
286424
  }
286167
286425
  }
286168
286426
  const configHint = !this.config.searxngUrl && !this.config.braveApiKey && !this.config.tavilyApiKey ? "\nTip: Configure a search backend for better results:\n \u2022 Self-hosted: set SEARXNG_URL in your environment\n \u2022 Brave Search API: set BRAVE_SEARCH_API_KEY\n \u2022 Tavily API: set TAVILY_API_KEY" : "";
286169
- return [
286170
- `Web search for "${query}" failed across all backends:`,
286427
+ throw Object.assign(new Error([
286428
+ `Web search for "${query}" found nothing across all backends:`,
286171
286429
  ...errors.map((e3) => ` \u2022 ${e3}`),
286172
286430
  configHint
286173
- ].join("\n");
286431
+ ].join("\n")), { systemic: !anyBackendReachable });
286174
286432
  }
286175
286433
  formatResults(query, results) {
286176
286434
  const lines = [`Web search results for: "${query}"`, ""];
@@ -286538,6 +286796,15 @@ var McpToolWrapper = class extends BaseTool {
286538
286796
  this.description = `[MCP:${serverName}] ${description}`;
286539
286797
  this.inputSchema = inputSchema;
286540
286798
  }
286799
+ // Every built-in tool of comparable risk (shell, git, file writes, the
286800
+ // built-in github tool) overrides this to true. An MCP tool never did —
286801
+ // BaseTool's default (false) meant a connected server's create/delete/push
286802
+ // actions ran with zero human approval, whatever they did. MCP carries no
286803
+ // standard "this mutates state" flag, so default to dangerous and name the
286804
+ // exception (a read-only-looking verb) rather than the other way around.
286805
+ isDangerous() {
286806
+ return !isReadOnlyMcpToolName(this.toolName);
286807
+ }
286541
286808
  async execute(input, _options) {
286542
286809
  return this.mcpClient.callTool(this.serverName, this.toolName, input);
286543
286810
  }
@@ -286650,7 +286917,7 @@ var ToolRegistry = class extends EventEmitter__default.default {
286650
286917
  requiresApproval(toolName) {
286651
286918
  const defaults2 = DEFAULT_APPROVAL_REQUIRED;
286652
286919
  const configured = this.config.requireApprovalFor;
286653
- return defaults2.includes(toolName) || configured.includes(toolName);
286920
+ return defaults2.includes(toolName) || configured.includes(toolName) || this.isDangerous(toolName);
286654
286921
  }
286655
286922
  isDangerous(toolName) {
286656
286923
  return this.tools.get(toolName)?.isDangerous() ?? false;
@@ -303433,9 +303700,8 @@ var Cascade = class _Cascade extends EventEmitter__default.default {
303433
303700
  });
303434
303701
  this.toolRegistry = new ToolRegistry(this.config.tools, workspacePath);
303435
303702
  try {
303436
- const nodePath = __require("path");
303437
303703
  this.router.setDeadModelStore(new DeadModelStore(
303438
- fileDeadModelPersistence(nodePath.join(workspacePath, ".cascade", "dead-models.json"))
303704
+ fileDeadModelPersistence(path26__namespace.default.join(workspacePath, ".cascade", "dead-models.json"))
303439
303705
  ));
303440
303706
  } catch {
303441
303707
  }