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.
package/dist/cli.cjs CHANGED
@@ -104,12 +104,6 @@ var cron__default = /*#__PURE__*/_interopDefault(cron);
104
104
  // Cascade AI — Multi-tier AI Orchestration System
105
105
  var __defProp = Object.defineProperty;
106
106
  var __getOwnPropNames = Object.getOwnPropertyNames;
107
- var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
108
- get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
109
- }) : x)(function(x) {
110
- if (typeof require !== "undefined") return require.apply(this, arguments);
111
- throw Error('Dynamic require of "' + x + '" is not supported');
112
- });
113
107
  var __esm = (fn, res) => function __init() {
114
108
  return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
115
109
  };
@@ -1529,6 +1523,23 @@ var init_pricing_data = __esm({
1529
1523
  ],
1530
1524
  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."
1531
1525
  },
1526
+ {
1527
+ model: "gemini-2.5-flash-image",
1528
+ provider: "gemini",
1529
+ modality: "image",
1530
+ currency: "USD",
1531
+ asOf: "2026-07-27",
1532
+ source: "https://ai.google.dev/gemini-api/docs/pricing#gemini-2.5-flash-image",
1533
+ provenance: "litellm-catalog",
1534
+ rates: [
1535
+ {
1536
+ unit: "per_image",
1537
+ amount: 0.039,
1538
+ variant: "default 1024px output"
1539
+ }
1540
+ ],
1541
+ 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."
1542
+ },
1532
1543
  {
1533
1544
  model: "imagen-4.0-generate-001",
1534
1545
  provider: "vertex",
@@ -1799,7 +1810,7 @@ var CASCADE_VERSION, CASCADE_CONFIG_FILE, CASCADE_DB_FILE, CASCADE_DASHBOARD_SEC
1799
1810
  var init_constants = __esm({
1800
1811
  "src/constants.ts"() {
1801
1812
  init_pricing();
1802
- CASCADE_VERSION = "0.55.0";
1813
+ CASCADE_VERSION = "0.63.0";
1803
1814
  CASCADE_CONFIG_FILE = ".cascade/config.json";
1804
1815
  CASCADE_DB_FILE = ".cascade/memory.db";
1805
1816
  CASCADE_DASHBOARD_SECRET_FILE = ".cascade/dashboard-secret";
@@ -5468,6 +5479,87 @@ function mcpToolName(serverName, toolName) {
5468
5479
  function isMcpToolName(name) {
5469
5480
  return name.startsWith(MCP_TOOL_PREFIX);
5470
5481
  }
5482
+ var READ_ONLY_MCP_VERBS = ["list", "get", "search", "read", "describe", "find", "query", "fetch", "show", "view"];
5483
+ var MUTATING_MCP_TOKENS = /* @__PURE__ */ new Set([
5484
+ "create",
5485
+ "delete",
5486
+ "remove",
5487
+ "update",
5488
+ "write",
5489
+ "modify",
5490
+ "set",
5491
+ "put",
5492
+ "post",
5493
+ "patch",
5494
+ "push",
5495
+ "merge",
5496
+ "send",
5497
+ "execute",
5498
+ "exec",
5499
+ "run",
5500
+ "deploy",
5501
+ "install",
5502
+ "uninstall",
5503
+ "upload",
5504
+ "publish",
5505
+ "revoke",
5506
+ "grant",
5507
+ "approve",
5508
+ "reject",
5509
+ "cancel",
5510
+ "enable",
5511
+ "disable",
5512
+ "add",
5513
+ "insert",
5514
+ "drop",
5515
+ "truncate",
5516
+ "clear",
5517
+ "reset",
5518
+ "restart",
5519
+ "stop",
5520
+ "kill",
5521
+ "terminate",
5522
+ "rename",
5523
+ "move",
5524
+ "fork",
5525
+ "sync",
5526
+ "replace",
5527
+ "overwrite",
5528
+ "invite",
5529
+ "ban",
5530
+ "block",
5531
+ "assign",
5532
+ "lock",
5533
+ "unlock",
5534
+ "archive",
5535
+ "star",
5536
+ "comment",
5537
+ "reply",
5538
+ "submit",
5539
+ "apply",
5540
+ "trigger",
5541
+ "invoke",
5542
+ "purge",
5543
+ "migrate",
5544
+ "edit",
5545
+ "save",
5546
+ "commit"
5547
+ ]);
5548
+ function mcpTokens(rawToolName) {
5549
+ return rawToolName.split(/[_-]+/).flatMap((segment) => segment.split(/(?<=[a-z0-9])(?=[A-Z])/)).map((t) => t.toLowerCase()).filter(Boolean);
5550
+ }
5551
+ function isReadOnlyMcpToolName(rawToolName) {
5552
+ if (mcpTokens(rawToolName).some((t) => MUTATING_MCP_TOKENS.has(t))) return false;
5553
+ const lower = rawToolName.toLowerCase();
5554
+ for (const verb of READ_ONLY_MCP_VERBS) {
5555
+ if (!lower.startsWith(verb)) continue;
5556
+ const boundary = rawToolName[verb.length];
5557
+ if (boundary === void 0 || boundary === "_" || boundary === "-" || boundary >= "A" && boundary <= "Z") {
5558
+ return true;
5559
+ }
5560
+ }
5561
+ return false;
5562
+ }
5471
5563
  function uniqueMcpServerName(desired, existingNames) {
5472
5564
  const taken = new Set(existingNames.map(mcpServerPrefix));
5473
5565
  if (!taken.has(mcpServerPrefix(desired))) return desired;
@@ -6479,8 +6571,6 @@ var LocalRequestQueue = class {
6479
6571
  };
6480
6572
  }
6481
6573
  };
6482
-
6483
- // src/core/router/dead-models.ts
6484
6574
  var DEAD_MODEL_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
6485
6575
  function deadModelKey(provider, modelId) {
6486
6576
  return `${provider}:${modelId}`;
@@ -6581,16 +6671,13 @@ var DeadModelStore = class {
6581
6671
  function fileDeadModelPersistence(filePath) {
6582
6672
  return {
6583
6673
  load() {
6584
- const fs32 = __require("fs");
6585
- if (!fs32.existsSync(filePath)) return [];
6586
- const raw = JSON.parse(fs32.readFileSync(filePath, "utf-8"));
6674
+ if (!fs29.existsSync(filePath)) return [];
6675
+ const raw = JSON.parse(fs29.readFileSync(filePath, "utf-8"));
6587
6676
  return Array.isArray(raw) ? raw : [];
6588
6677
  },
6589
6678
  save(records) {
6590
- const fs32 = __require("fs");
6591
- const path37 = __require("path");
6592
- fs32.mkdirSync(path37.dirname(filePath), { recursive: true });
6593
- fs32.writeFileSync(filePath, JSON.stringify(records, null, 2));
6679
+ fs29.mkdirSync(path32.dirname(filePath), { recursive: true });
6680
+ fs29.writeFileSync(filePath, JSON.stringify(records, null, 2));
6594
6681
  }
6595
6682
  };
6596
6683
  }
@@ -7020,6 +7107,12 @@ var FAMILY_BENCHMARKS = {
7020
7107
  // Google — strongest at analysis/data and long-context.
7021
7108
  "gemini-2.5-pro": { code: 90, analysis: 93, creative: 86, data: 92 },
7022
7109
  "gemini-2.5-flash": { code: 82, analysis: 83, creative: 80, data: 82 },
7110
+ // The 2.5-lite tier scores below full 2.5-flash but above the older 2.0
7111
+ // lite tier — it exists because a naive `gemini-?2\.5-flash` regex swallows
7112
+ // "gemini-2.5-flash-lite" as a substring match (no lite variant of its own
7113
+ // to fall into), so the weaker lite model got credited with the full
7114
+ // flash's benchmark score and kept winning Cascade Auto's "best value" pick.
7115
+ "gemini-2.5-flash-lite": { code: 72, analysis: 72, creative: 74, data: 72 },
7023
7116
  "gemini-1.5-pro": { code: 82, analysis: 84, creative: 82, data: 85 },
7024
7117
  "gemini-2.0-flash": { code: 79, analysis: 80, creative: 79, data: 80 },
7025
7118
  "gemini-flash-lite": { code: 68, analysis: 68, creative: 70, data: 68 },
@@ -7052,6 +7145,11 @@ var FAMILY_MATCHERS = [
7052
7145
  [/gpt-?4o-mini/i, "gpt-4o-mini"],
7053
7146
  [/gpt-?4o/i, "gpt-4o"],
7054
7147
  [/gemini-?2\.5-pro/i, "gemini-2.5-pro"],
7148
+ // Must precede the bare `2\.5-flash` rule below: that pattern has no word
7149
+ // boundary after "flash" and matches "gemini-2.5-flash-lite" as a plain
7150
+ // substring, so the lite variant would otherwise be scored as the full
7151
+ // flash model (see the FAMILY_BENCHMARKS comment above).
7152
+ [/gemini-?2\.5-flash-lite/i, "gemini-2.5-flash-lite"],
7055
7153
  [/gemini-?2\.5-flash/i, "gemini-2.5-flash"],
7056
7154
  [/gemini-?1\.5-pro/i, "gemini-1.5-pro"],
7057
7155
  [/gemini-?2\.0-flash-lite/i, "gemini-flash-lite"],
@@ -8559,6 +8657,92 @@ function truncateForContext(text, maxChars = 12e3) {
8559
8657
  ${tail}`;
8560
8658
  }
8561
8659
 
8660
+ // src/core/router/provider-errors.ts
8661
+ var SYSTEMIC = /* @__PURE__ */ new Set([
8662
+ "rate_limit",
8663
+ "quota_exhausted",
8664
+ "auth",
8665
+ "model_unavailable"
8666
+ ]);
8667
+ function isSystemicKind(kind) {
8668
+ return SYSTEMIC.has(kind);
8669
+ }
8670
+ function statusOf(err) {
8671
+ if (!err || typeof err !== "object") return void 0;
8672
+ const e = err;
8673
+ for (const key of ["status", "statusCode", "code"]) {
8674
+ const v = e[key];
8675
+ if (typeof v === "number" && v >= 100 && v < 600) return v;
8676
+ }
8677
+ const resp = e["response"];
8678
+ if (resp && typeof resp["status"] === "number") return resp["status"];
8679
+ const inner = e["error"];
8680
+ if (inner && typeof inner["code"] === "number") return inner["code"];
8681
+ return void 0;
8682
+ }
8683
+ function messageOf(err) {
8684
+ if (err instanceof Error) return err.message;
8685
+ if (typeof err === "string") return err;
8686
+ if (err && typeof err === "object") {
8687
+ const e = err;
8688
+ if (typeof e["message"] === "string") return e["message"];
8689
+ const inner = e["error"];
8690
+ if (inner && typeof inner["message"] === "string") return inner["message"];
8691
+ try {
8692
+ return JSON.stringify(err);
8693
+ } catch {
8694
+ }
8695
+ }
8696
+ return String(err);
8697
+ }
8698
+ function classifyProviderError(err) {
8699
+ const raw = messageOf(err).trim();
8700
+ const status = statusOf(err);
8701
+ const m = raw.toLowerCase();
8702
+ const kind = (() => {
8703
+ if (status === 429) {
8704
+ return /quota|billing|credit|exceeded your current quota|insufficient/.test(m) ? "quota_exhausted" : "rate_limit";
8705
+ }
8706
+ if (status === 401 || status === 403) return "auth";
8707
+ if (status === 404) return "model_unavailable";
8708
+ if (/rate limit|too many requests|resource_exhausted/.test(m)) {
8709
+ return /quota|billing|credit|insufficient/.test(m) ? "quota_exhausted" : "rate_limit";
8710
+ }
8711
+ if (/quota|billing|insufficient_quota|insufficient credit|payment required/.test(m)) return "quota_exhausted";
8712
+ if (/api key|unauthorized|authentication|permission denied|invalid[_ ]api/.test(m)) return "auth";
8713
+ if (/model .*(not found|does not exist|unavailable)|no such model|unknown model|not supported for/.test(m)) {
8714
+ return "model_unavailable";
8715
+ }
8716
+ if (/context length|too many tokens|maximum context|prompt is too long|input too long/.test(m)) return "context_length";
8717
+ if (/safety|content filter|blocked|recitation|prohibited/.test(m)) return "content_filter";
8718
+ if (/econnreset|etimedout|enotfound|socket hang up|network|fetch failed|aborted/.test(m)) return "network";
8719
+ return "unknown";
8720
+ })();
8721
+ return { kind, systemic: isSystemicKind(kind), status, raw };
8722
+ }
8723
+ function describeProviderError(c, modelId) {
8724
+ const on = modelId ? ` on ${modelId}` : "";
8725
+ const detail = c.raw ? ` Provider said: ${c.raw}` : "";
8726
+ switch (c.kind) {
8727
+ case "rate_limit":
8728
+ 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}`;
8729
+ case "quota_exhausted":
8730
+ 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}`;
8731
+ case "auth":
8732
+ 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}`;
8733
+ case "model_unavailable":
8734
+ 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}`;
8735
+ case "context_length":
8736
+ return `The prompt was too long${on}. Reduce the attached context, or use a model with a larger context window for this tier.${detail}`;
8737
+ case "content_filter":
8738
+ return `The provider's safety filter refused this request${on}.${detail}`;
8739
+ case "network":
8740
+ return `Network error reaching the provider${on}. This is usually transient.${detail}`;
8741
+ default:
8742
+ return `The model call failed${on}.${detail}`;
8743
+ }
8744
+ }
8745
+
8562
8746
  // src/core/tiers/t3-worker.ts
8563
8747
  var CriticalToolError = class extends Error {
8564
8748
  constructor(message, toolName) {
@@ -8592,8 +8776,15 @@ var KNOWN_TOOLS = [
8592
8776
  "web_search",
8593
8777
  "glob",
8594
8778
  "grep",
8595
- "web_fetch"
8779
+ "web_fetch",
8780
+ "generate_image"
8596
8781
  ];
8782
+ var PROVIDER_BACKED_TOOLS = /* @__PURE__ */ new Set([
8783
+ "generate_image",
8784
+ "generate_speech",
8785
+ "generate_video",
8786
+ "transcribe_audio"
8787
+ ]);
8597
8788
  function buildWorkerRules(has) {
8598
8789
  const canWriteFiles = has("file_write") || has("file_edit") || has("run_code");
8599
8790
  const hasAnyTool = KNOWN_TOOLS.some(has);
@@ -8603,9 +8794,22 @@ function buildWorkerRules(has) {
8603
8794
  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.",
8604
8795
  has("web_search") && '- Use the "web_search" tool to find current information, documentation, news, or general web data.',
8605
8796
  has("pdf_create") && '- Use the "pdf_create" tool for PDF requests.',
8797
+ // Without this the model "writes the image" as prose — a bracketed
8798
+ // description sitting in the deck where the picture should be — even with a
8799
+ // working image model registered. Naming the reference syntax matters as
8800
+ // much as naming the tool: a generated image nobody embeds is still a
8801
+ // missing image.
8802
+ //
8803
+ // Scoped to PowerPoint/Word specifically: those are the only exporters
8804
+ // that actually embed a Markdown image reference (cloud/web/exporters.ts
8805
+ // toPptx/toDocx). PDF and plain-text/Markdown deliverables flatten a
8806
+ // reference straight to caption text — telling the model to call
8807
+ // generate_image for THOSE just pays for an image nobody ever sees.
8808
+ 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.',
8606
8809
  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.',
8607
8810
  "- If you are not making meaningful progress, stop and escalate rather than looping or padding the response.",
8608
8811
  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.',
8812
+ 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.",
8609
8813
  "- Return structured output that directly addresses the expected output specification."
8610
8814
  ];
8611
8815
  return `You are a T3 Worker agent in the Cascade AI system. Your job is to execute a specific subtask completely and accurately.
@@ -9139,7 +9343,9 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
9139
9343
  const durationMs = Date.now() - toolStartMs;
9140
9344
  const errMsg = err instanceof Error ? err.message : String(err);
9141
9345
  this.emit("tool:result", { id: tc.id, tierId: this.id, toolName: tc.name, error: errMsg, durationMs });
9142
- if (/\b(429|rate.?limit|authentication|api.?key|forbidden|401|403)\b/i.test(errMsg)) {
9346
+ const isTaggedSystemic = err instanceof Error && err.systemic === true;
9347
+ const isProviderBacked = PROVIDER_BACKED_TOOLS.has(tc.name);
9348
+ if (isTaggedSystemic || isProviderBacked && classifyProviderError(err).systemic) {
9143
9349
  throw new CriticalToolError(errMsg, tc.name);
9144
9350
  }
9145
9351
  return await this.adaptiveFallback(tc, `Tool error: ${errMsg}`);
@@ -9427,8 +9633,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
9427
9633
  } catch {
9428
9634
  return {
9429
9635
  checksRun: ["completeness", "correctness", "compliance"],
9430
- passed: ["completeness", "correctness", "compliance"],
9431
- failed: []
9636
+ passed: [],
9637
+ failed: ["completeness"]
9432
9638
  };
9433
9639
  }
9434
9640
  }
@@ -10218,7 +10424,7 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
10218
10424
  const jsonMatch = /\[[\s\S]*\]/.exec(result.content);
10219
10425
  if (!jsonMatch) throw new Error("No JSON array found");
10220
10426
  const parsed = JSON.parse(jsonMatch[0]);
10221
- return parsed.map((a) => ({ ...a, sectionTitle: assignment.sectionTitle }));
10427
+ return parsed.map((a) => ({ ...a, sectionTitle: assignment.sectionTitle, constraints: a.constraints ?? [] }));
10222
10428
  } catch {
10223
10429
  return [{
10224
10430
  subtaskId: crypto.randomUUID(),
@@ -11179,6 +11385,12 @@ SPEC RULES \u2014 each subtask is a self-contained spec slice (workers execute f
11179
11385
  if (!plan.sections || !Array.isArray(plan.sections) || plan.sections.length === 0) {
11180
11386
  throw new Error("Invalid plan: no sections");
11181
11387
  }
11388
+ for (const section of plan.sections) {
11389
+ section.constraints ??= [];
11390
+ for (const subtask of section.t3Subtasks ?? []) {
11391
+ subtask.constraints ??= [];
11392
+ }
11393
+ }
11182
11394
  const [min, max] = COMPLEXITY_T2_COUNT[plan.complexity] ?? [1, 8];
11183
11395
  if (plan.sections.length < min) ;
11184
11396
  }
@@ -11498,17 +11710,17 @@ var CAPABILITIES = [
11498
11710
  api: "openai-images"
11499
11711
  },
11500
11712
  {
11501
- // Imagen, not `gemini-3-pro-image`. That id appears on Google's PRICING
11502
- // page but 404s on `:predict` ("not found for API version v1beta, or is not
11503
- // supported for predict") — the Gemini image models are called through
11504
- // generateContent with an image response modality, while `:predict` is the
11505
- // Imagen shape this executor implements. A billing line item is not proof
11506
- // of a callable endpoint, which is the assumption that put the wrong id
11507
- // here in the first place.
11713
+ // Was `imagen-4.0-generate-001` on `:predict`, which now 404s for new keys
11714
+ // ("no longer available to new users") — Imagen's :predict API is deprecated
11715
+ // and shuts down 2026-08-17. Google's own migration path is this model on
11716
+ // the ordinary generateContent endpoint, so the fix was never another Imagen
11717
+ // id: the whole request/response SHAPE changes, which is why `api` moved to
11718
+ // 'gemini-generate-content' rather than just the modelId moving.
11719
+ // https://ai.google.dev/gemini-api/docs/imagen
11508
11720
  modality: "image",
11509
11721
  provider: "gemini",
11510
- modelId: "imagen-4.0-generate-001",
11511
- api: "gemini-predict"
11722
+ modelId: "gemini-2.5-flash-image",
11723
+ api: "gemini-generate-content"
11512
11724
  },
11513
11725
  // ── Speech (text → audio) ──
11514
11726
  {
@@ -11563,6 +11775,14 @@ function capabilityCost(cap) {
11563
11775
  const shown = amount < 1e-3 ? amount.toExponential(2) : amount.toFixed(4);
11564
11776
  return { label: `$${shown} ${unit}`, amount, unit };
11565
11777
  }
11778
+ function explainChoice(c, cost, modality, isBest) {
11779
+ const priced = cost ? ` ${cost.label}.` : "";
11780
+ if (c.quality !== void 0) {
11781
+ 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}`;
11782
+ }
11783
+ const noRanking = ". No published quality ranking is applied for this modality, so this is a cost choice, not a quality one.";
11784
+ 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;
11785
+ }
11566
11786
  var MultimodalRegistry = class {
11567
11787
  /** Provider types the user has actually configured a key for. */
11568
11788
  available;
@@ -11580,22 +11800,20 @@ var MultimodalRegistry = class {
11580
11800
  return [...new Set(this.usable().map((c) => c.modality))];
11581
11801
  }
11582
11802
  /**
11583
- * Pick a model for a modality.
11803
+ * Every usable capability for a modality, best first.
11584
11804
  *
11585
11805
  * Ranked models beat unranked ones, then higher quality, then lower unit
11586
- * price. An explicit `preferModel` wins outright when it is usable — the user
11587
- * naming a model is a stronger signal than any prior.
11806
+ * price. An explicit `preferModel` is moved to the head when it is usable —
11807
+ * the user naming a model is a stronger signal than any prior.
11808
+ *
11809
+ * `select()` is just the head of this list. The TAIL is the point: a systemic
11810
+ * provider failure (dead key, 404, exhausted quota) is scoped to one
11811
+ * provider+model, so "this model cannot draw" is not the same fact as "this
11812
+ * account cannot draw". A caller holding the ordered alternatives can try the
11813
+ * next provider instead of aborting the whole run — see generate-media.ts.
11588
11814
  */
11589
- select(modality, opts = {}) {
11590
- const pool = this.usable(modality);
11591
- if (!pool.length) return null;
11592
- if (opts.preferModel) {
11593
- const wanted = pool.find((c) => c.modelId === opts.preferModel);
11594
- if (wanted) {
11595
- return { capability: wanted, cost: capabilityCost(wanted), reason: `Requested explicitly: ${wanted.modelId}.` };
11596
- }
11597
- }
11598
- const scored = pool.map((c) => ({ c, cost: capabilityCost(c) })).sort((a, b) => {
11815
+ rank(modality, opts = {}) {
11816
+ const scored = this.usable(modality).map((c) => ({ c, cost: capabilityCost(c) })).sort((a, b) => {
11599
11817
  const qa = a.c.quality ?? -1;
11600
11818
  const qb = b.c.quality ?? -1;
11601
11819
  if (qa !== qb) return qb - qa;
@@ -11603,10 +11821,25 @@ var MultimodalRegistry = class {
11603
11821
  const pb = b.cost?.amount ?? Number.POSITIVE_INFINITY;
11604
11822
  return pa - pb;
11605
11823
  });
11606
- const best = scored[0];
11607
- const ranked = best.c.quality !== void 0;
11608
- 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.";
11609
- return { capability: best.c, cost: best.cost, reason };
11824
+ const results = scored.map(({ c, cost }, i) => ({
11825
+ capability: c,
11826
+ cost,
11827
+ reason: explainChoice(c, cost, modality, i === 0)
11828
+ }));
11829
+ if (opts.preferModel) {
11830
+ const i = results.findIndex((r) => r.capability.modelId === opts.preferModel);
11831
+ if (i > -1) {
11832
+ const [wanted] = results.splice(i, 1);
11833
+ results.unshift({ ...wanted, reason: `Requested explicitly: ${wanted.capability.modelId}.` });
11834
+ }
11835
+ }
11836
+ return results;
11837
+ }
11838
+ /**
11839
+ * Pick a model for a modality — the best usable one, or null if there is none.
11840
+ */
11841
+ select(modality, opts = {}) {
11842
+ return this.rank(modality, opts)[0] ?? null;
11610
11843
  }
11611
11844
  /**
11612
11845
  * A plain-language account of what this account can and cannot generate —
@@ -11659,92 +11892,6 @@ var BaseTool = class {
11659
11892
  }
11660
11893
  };
11661
11894
 
11662
- // src/core/router/provider-errors.ts
11663
- var SYSTEMIC = /* @__PURE__ */ new Set([
11664
- "rate_limit",
11665
- "quota_exhausted",
11666
- "auth",
11667
- "model_unavailable"
11668
- ]);
11669
- function isSystemicKind(kind) {
11670
- return SYSTEMIC.has(kind);
11671
- }
11672
- function statusOf(err) {
11673
- if (!err || typeof err !== "object") return void 0;
11674
- const e = err;
11675
- for (const key of ["status", "statusCode", "code"]) {
11676
- const v = e[key];
11677
- if (typeof v === "number" && v >= 100 && v < 600) return v;
11678
- }
11679
- const resp = e["response"];
11680
- if (resp && typeof resp["status"] === "number") return resp["status"];
11681
- const inner = e["error"];
11682
- if (inner && typeof inner["code"] === "number") return inner["code"];
11683
- return void 0;
11684
- }
11685
- function messageOf(err) {
11686
- if (err instanceof Error) return err.message;
11687
- if (typeof err === "string") return err;
11688
- if (err && typeof err === "object") {
11689
- const e = err;
11690
- if (typeof e["message"] === "string") return e["message"];
11691
- const inner = e["error"];
11692
- if (inner && typeof inner["message"] === "string") return inner["message"];
11693
- try {
11694
- return JSON.stringify(err);
11695
- } catch {
11696
- }
11697
- }
11698
- return String(err);
11699
- }
11700
- function classifyProviderError(err) {
11701
- const raw = messageOf(err).trim();
11702
- const status = statusOf(err);
11703
- const m = raw.toLowerCase();
11704
- const kind = (() => {
11705
- if (status === 429) {
11706
- return /quota|billing|credit|exceeded your current quota|insufficient/.test(m) ? "quota_exhausted" : "rate_limit";
11707
- }
11708
- if (status === 401 || status === 403) return "auth";
11709
- if (status === 404) return "model_unavailable";
11710
- if (/rate limit|too many requests|resource_exhausted/.test(m)) {
11711
- return /quota|billing|credit|insufficient/.test(m) ? "quota_exhausted" : "rate_limit";
11712
- }
11713
- if (/quota|billing|insufficient_quota|insufficient credit|payment required/.test(m)) return "quota_exhausted";
11714
- if (/api key|unauthorized|authentication|permission denied|invalid[_ ]api/.test(m)) return "auth";
11715
- if (/model .*(not found|does not exist|unavailable)|no such model|unknown model|not supported for/.test(m)) {
11716
- return "model_unavailable";
11717
- }
11718
- if (/context length|too many tokens|maximum context|prompt is too long|input too long/.test(m)) return "context_length";
11719
- if (/safety|content filter|blocked|recitation|prohibited/.test(m)) return "content_filter";
11720
- if (/econnreset|etimedout|enotfound|socket hang up|network|fetch failed|aborted/.test(m)) return "network";
11721
- return "unknown";
11722
- })();
11723
- return { kind, systemic: isSystemicKind(kind), status, raw };
11724
- }
11725
- function describeProviderError(c, modelId) {
11726
- const on = modelId ? ` on ${modelId}` : "";
11727
- const detail = c.raw ? ` Provider said: ${c.raw}` : "";
11728
- switch (c.kind) {
11729
- case "rate_limit":
11730
- 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}`;
11731
- case "quota_exhausted":
11732
- 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}`;
11733
- case "auth":
11734
- 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}`;
11735
- case "model_unavailable":
11736
- 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}`;
11737
- case "context_length":
11738
- return `The prompt was too long${on}. Reduce the attached context, or use a model with a larger context window for this tier.${detail}`;
11739
- case "content_filter":
11740
- return `The provider's safety filter refused this request${on}.${detail}`;
11741
- case "network":
11742
- return `Network error reaching the provider${on}. This is usually transient.${detail}`;
11743
- default:
11744
- return `The model call failed${on}.${detail}`;
11745
- }
11746
- }
11747
-
11748
11895
  // src/core/multimodal/generate.ts
11749
11896
  var DEFAULT_TIMEOUT_MS = 12e4;
11750
11897
  async function callProvider(fn, modelId) {
@@ -11783,6 +11930,29 @@ function baseUrlFor(cfg, fallback) {
11783
11930
  function stamp(ext) {
11784
11931
  return `cascade-${Date.now()}.${ext}`;
11785
11932
  }
11933
+ var GEMINI_ASPECT_RATIOS = [
11934
+ ["1:1", 1 / 1],
11935
+ ["3:2", 3 / 2],
11936
+ ["2:3", 2 / 3],
11937
+ ["3:4", 3 / 4],
11938
+ ["4:3", 4 / 3],
11939
+ ["4:5", 4 / 5],
11940
+ ["5:4", 5 / 4],
11941
+ ["9:16", 9 / 16],
11942
+ ["16:9", 16 / 9],
11943
+ ["21:9", 21 / 9]
11944
+ ];
11945
+ function closestGeminiAspectRatio(size) {
11946
+ const m = size.match(/^(\d+)\s*[x×]\s*(\d+)$/i);
11947
+ if (!m) return void 0;
11948
+ const w = Number(m[1]);
11949
+ const h = Number(m[2]);
11950
+ if (!w || !h) return void 0;
11951
+ const ratio = w / h;
11952
+ return GEMINI_ASPECT_RATIOS.reduce(
11953
+ (best, cand) => Math.abs(cand[1] - ratio) < Math.abs(best[1] - ratio) ? cand : best
11954
+ )[0];
11955
+ }
11786
11956
  async function generateImage(cap, cfg, req, signal) {
11787
11957
  return callProvider(async () => {
11788
11958
  if (cap.api === "openai-images") {
@@ -11807,19 +11977,37 @@ async function generateImage(cap, cfg, req, signal) {
11807
11977
  ...first.revised_prompt ? { revisedPrompt: first.revised_prompt } : {}
11808
11978
  };
11809
11979
  }
11810
- if (cap.api === "gemini-predict") {
11980
+ if (cap.api === "gemini-generate-content") {
11811
11981
  const base = baseUrlFor(cfg, "https://generativelanguage.googleapis.com/v1beta");
11812
- const url = `${base}/models/${encodeURIComponent(cap.modelId)}:predict`;
11982
+ const url = `${base}/models/${encodeURIComponent(cap.modelId)}:generateContent`;
11983
+ const aspectRatio = req.size ? closestGeminiAspectRatio(req.size) : void 0;
11813
11984
  const res = await postJson(url, {
11814
- instances: [{ prompt: req.prompt }],
11815
- parameters: { sampleCount: 1 }
11985
+ contents: [{ parts: [{ text: req.prompt }] }],
11986
+ generationConfig: {
11987
+ // Asked for explicitly: these models can answer with text only, and a
11988
+ // text-only reply to "draw me a cat" is a failed image generation.
11989
+ responseModalities: ["IMAGE", "TEXT"],
11990
+ // Gemini has no free-form pixel size — only a named aspect ratio (see
11991
+ // closestGeminiAspectRatio). Omitted entirely when there's no
11992
+ // requested size, or no supported ratio close enough to bother with.
11993
+ ...aspectRatio ? { imageConfig: { aspectRatio } } : {}
11994
+ }
11816
11995
  }, { "x-goog-api-key": cfg.apiKey ?? "" }, signal);
11817
11996
  const body = await res.json();
11818
- const first = body.predictions?.[0];
11819
- if (!first?.bytesBase64Encoded) throw new Error("Image response contained no image data.");
11820
- const mimeType = first.mimeType ?? "image/png";
11997
+ const candidate = body.candidates?.[0];
11998
+ const parts = candidate?.content?.parts ?? [];
11999
+ const image = parts.find((p) => p.inlineData?.data)?.inlineData;
12000
+ if (!image?.data) {
12001
+ const blocked = body.promptFeedback?.blockReason ?? (candidate?.finishReason && candidate.finishReason !== "STOP" ? candidate.finishReason : void 0);
12002
+ if (blocked) throw new Error(`Image request was blocked by the provider (${blocked}).`);
12003
+ const said = parts.find((p) => p.text)?.text?.trim();
12004
+ throw new Error(
12005
+ "Image response contained no image data." + (said ? ` The model replied with text instead: ${said}` : "")
12006
+ );
12007
+ }
12008
+ const mimeType = image.mimeType ?? "image/png";
11821
12009
  return {
11822
- data: Buffer.from(first.bytesBase64Encoded, "base64"),
12010
+ data: Buffer.from(image.data, "base64"),
11823
12011
  mimeType,
11824
12012
  filename: stamp(mimeType.includes("jpeg") ? "jpg" : "png"),
11825
12013
  modelId: cap.modelId,
@@ -11972,6 +12160,49 @@ function resolve(deps, modality, preferModel) {
11972
12160
  }
11973
12161
  return { ok: true, selected, cfg };
11974
12162
  }
12163
+ function messageOf2(err) {
12164
+ return err instanceof Error ? err.message : String(err);
12165
+ }
12166
+ async function runWithProviderFallback(deps, modality, first, run, signal) {
12167
+ const failed = first.selected.capability;
12168
+ try {
12169
+ return { value: await run(failed, first.cfg), selected: first.selected };
12170
+ } catch (err) {
12171
+ if (signal?.aborted || !classifyProviderError(err).systemic) throw err;
12172
+ const tried = [`${failed.modelId} (${failed.provider}): ${messageOf2(err)}`];
12173
+ let allSystemic = true;
12174
+ let lastNonSystemic;
12175
+ const seen = /* @__PURE__ */ new Set([failed.provider]);
12176
+ for (const alt of deps.registry.rank(modality)) {
12177
+ if (seen.has(alt.capability.provider)) continue;
12178
+ const cfg = deps.lookupProvider(alt.capability.provider);
12179
+ if (!cfg) continue;
12180
+ seen.add(alt.capability.provider);
12181
+ try {
12182
+ return {
12183
+ value: await run(alt.capability, cfg),
12184
+ selected: alt,
12185
+ fellBackFrom: { capability: failed, error: messageOf2(err) }
12186
+ };
12187
+ } catch (retryErr) {
12188
+ if (signal?.aborted) throw retryErr;
12189
+ tried.push(`${alt.capability.modelId} (${alt.capability.provider}): ${messageOf2(retryErr)}`);
12190
+ if (classifyProviderError(retryErr).systemic) {
12191
+ continue;
12192
+ }
12193
+ allSystemic = false;
12194
+ lastNonSystemic = retryErr;
12195
+ }
12196
+ }
12197
+ if (tried.length === 1) throw err;
12198
+ if (!allSystemic) throw lastNonSystemic;
12199
+ const exhausted = new Error(
12200
+ `${modality} generation failed on every configured provider (${seen.size} tried). ${tried.join(" ")}`
12201
+ );
12202
+ Object.assign(exhausted, { systemic: true });
12203
+ throw exhausted;
12204
+ }
12205
+ }
11975
12206
  var GenerateImageTool = class extends BaseTool {
11976
12207
  constructor(deps) {
11977
12208
  super();
@@ -11995,16 +12226,32 @@ var GenerateImageTool = class extends BaseTool {
11995
12226
  const r = resolve(this.deps, "image", input["model"] ? String(input["model"]) : void 0);
11996
12227
  if (!r.ok) return r.error;
11997
12228
  const size = input["size"] ? String(input["size"]) : void 0;
11998
- const asset = await generateImage(
11999
- r.selected.capability,
12000
- r.cfg,
12001
- { prompt, ...size ? { size } : {} },
12229
+ const attempt = await runWithProviderFallback(
12230
+ this.deps,
12231
+ "image",
12232
+ r,
12233
+ (cap, cfg) => generateImage(cap, cfg, { prompt, ...size ? { size } : {} }, options.signal),
12002
12234
  options.signal
12003
12235
  );
12236
+ const asset = attempt.value;
12237
+ const fell = attempt.fellBackFrom;
12004
12238
  const location = await this.deps.sink(asset);
12005
12239
  return [
12006
12240
  `Image generated and saved to ${location}.`,
12007
- `Model: ${asset.modelId} \u2014 ${r.selected.reason}`,
12241
+ // Knowing WHERE the image is isn't the same as knowing how to use it: a
12242
+ // model told only "saved to X" tends to describe the picture in prose
12243
+ // instead of embedding it. Spelling out the reference syntax here makes
12244
+ // the worker's "never write a placeholder" rule concretely actionable,
12245
+ // and it works for every host because `location` is whatever that host's
12246
+ // sink returned — a workspace path on desktop/CLI, a URL in the cloud.
12247
+ `To include this image in your output, reference it as: ![description](${location})`,
12248
+ // The reported model has to be the one that actually drew the picture, not
12249
+ // the one that was picked and then failed.
12250
+ `Model: ${asset.modelId} \u2014 ${fell ? `used as a fallback after ${fell.capability.modelId} (${fell.capability.provider}) failed with a systemic error.` : attempt.selected.reason}`,
12251
+ // Worth one line: the run succeeded, so nothing else will report that a
12252
+ // configured provider is broken, and "your Gemini key is dead" is exactly
12253
+ // the kind of thing the user wants to hear once rather than never.
12254
+ fell ? `Note: ${fell.capability.provider} failed first \u2014 ${fell.error}` : "",
12008
12255
  // DALL·E rewrites the prompt before drawing; the model needs to know what
12009
12256
  // was actually rendered, not what it asked for, or its description of the
12010
12257
  // result will not match the image.
@@ -13247,9 +13494,11 @@ var WebSearchTool = class extends BaseTool {
13247
13494
  );
13248
13495
  const errors = [];
13249
13496
  let results = [];
13497
+ let anyBackendReachable = false;
13250
13498
  if (this.config.searxngUrl) {
13251
13499
  try {
13252
13500
  results = await searchSearXNG(query, this.config.searxngUrl, maxResults);
13501
+ anyBackendReachable = true;
13253
13502
  if (results.length > 0) return this.formatResults(query, results);
13254
13503
  errors.push("SearXNG: returned 0 results");
13255
13504
  } catch (err) {
@@ -13259,6 +13508,7 @@ var WebSearchTool = class extends BaseTool {
13259
13508
  if (this.config.braveApiKey) {
13260
13509
  try {
13261
13510
  results = await searchBrave(query, this.config.braveApiKey, maxResults);
13511
+ anyBackendReachable = true;
13262
13512
  if (results.length > 0) return this.formatResults(query, results);
13263
13513
  errors.push("Brave: returned 0 results");
13264
13514
  } catch (err) {
@@ -13268,6 +13518,7 @@ var WebSearchTool = class extends BaseTool {
13268
13518
  if (this.config.tavilyApiKey) {
13269
13519
  try {
13270
13520
  results = await searchTavily(query, this.config.tavilyApiKey, maxResults);
13521
+ anyBackendReachable = true;
13271
13522
  if (results.length > 0) return this.formatResults(query, results);
13272
13523
  errors.push("Tavily: returned 0 results");
13273
13524
  } catch (err) {
@@ -13277,6 +13528,7 @@ var WebSearchTool = class extends BaseTool {
13277
13528
  for (const variant of ["html", "lite"]) {
13278
13529
  try {
13279
13530
  results = await searchDuckDuckGo(query, maxResults, variant);
13531
+ anyBackendReachable = true;
13280
13532
  if (results.length > 0) return this.formatResults(query, results);
13281
13533
  errors.push(`DuckDuckGo ${variant}: returned 0 results`);
13282
13534
  } catch (err) {
@@ -13284,11 +13536,11 @@ var WebSearchTool = class extends BaseTool {
13284
13536
  }
13285
13537
  }
13286
13538
  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" : "";
13287
- return [
13288
- `Web search for "${query}" failed across all backends:`,
13539
+ throw Object.assign(new Error([
13540
+ `Web search for "${query}" found nothing across all backends:`,
13289
13541
  ...errors.map((e) => ` \u2022 ${e}`),
13290
13542
  configHint
13291
- ].join("\n");
13543
+ ].join("\n")), { systemic: !anyBackendReachable });
13292
13544
  }
13293
13545
  formatResults(query, results) {
13294
13546
  const lines = [`Web search results for: "${query}"`, ""];
@@ -13656,6 +13908,15 @@ var McpToolWrapper = class extends BaseTool {
13656
13908
  this.description = `[MCP:${serverName}] ${description}`;
13657
13909
  this.inputSchema = inputSchema;
13658
13910
  }
13911
+ // Every built-in tool of comparable risk (shell, git, file writes, the
13912
+ // built-in github tool) overrides this to true. An MCP tool never did —
13913
+ // BaseTool's default (false) meant a connected server's create/delete/push
13914
+ // actions ran with zero human approval, whatever they did. MCP carries no
13915
+ // standard "this mutates state" flag, so default to dangerous and name the
13916
+ // exception (a read-only-looking verb) rather than the other way around.
13917
+ isDangerous() {
13918
+ return !isReadOnlyMcpToolName(this.toolName);
13919
+ }
13659
13920
  async execute(input, _options) {
13660
13921
  return this.mcpClient.callTool(this.serverName, this.toolName, input);
13661
13922
  }
@@ -13768,7 +14029,7 @@ var ToolRegistry = class extends EventEmitter__default.default {
13768
14029
  requiresApproval(toolName) {
13769
14030
  const defaults = DEFAULT_APPROVAL_REQUIRED;
13770
14031
  const configured = this.config.requireApprovalFor;
13771
- return defaults.includes(toolName) || configured.includes(toolName);
14032
+ return defaults.includes(toolName) || configured.includes(toolName) || this.isDangerous(toolName);
13772
14033
  }
13773
14034
  isDangerous(toolName) {
13774
14035
  return this.tools.get(toolName)?.isDangerous() ?? false;
@@ -16678,9 +16939,8 @@ var Cascade = class _Cascade extends EventEmitter__default.default {
16678
16939
  });
16679
16940
  this.toolRegistry = new ToolRegistry(this.config.tools, workspacePath);
16680
16941
  try {
16681
- const nodePath = __require("path");
16682
16942
  this.router.setDeadModelStore(new DeadModelStore(
16683
- fileDeadModelPersistence(nodePath.join(workspacePath, ".cascade", "dead-models.json"))
16943
+ fileDeadModelPersistence(path32__default.default.join(workspacePath, ".cascade", "dead-models.json"))
16684
16944
  ));
16685
16945
  } catch {
16686
16946
  }