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/index.cjs CHANGED
@@ -87,12 +87,6 @@ var cron__default = /*#__PURE__*/_interopDefault(cron);
87
87
  // Cascade AI — Multi-tier AI Orchestration System
88
88
  var __defProp = Object.defineProperty;
89
89
  var __getOwnPropNames = Object.getOwnPropertyNames;
90
- var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
91
- get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
92
- }) : x)(function(x) {
93
- if (typeof require !== "undefined") return require.apply(this, arguments);
94
- throw Error('Dynamic require of "' + x + '" is not supported');
95
- });
96
90
  var __esm = (fn, res) => function __init() {
97
91
  return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
98
92
  };
@@ -1643,6 +1637,23 @@ var pricing_data_default = {
1643
1637
  ],
1644
1638
  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."
1645
1639
  },
1640
+ {
1641
+ model: "gemini-2.5-flash-image",
1642
+ provider: "gemini",
1643
+ modality: "image",
1644
+ currency: "USD",
1645
+ asOf: "2026-07-27",
1646
+ source: "https://ai.google.dev/gemini-api/docs/pricing#gemini-2.5-flash-image",
1647
+ provenance: "litellm-catalog",
1648
+ rates: [
1649
+ {
1650
+ unit: "per_image",
1651
+ amount: 0.039,
1652
+ variant: "default 1024px output"
1653
+ }
1654
+ ],
1655
+ 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."
1656
+ },
1646
1657
  {
1647
1658
  model: "imagen-4.0-generate-001",
1648
1659
  provider: "vertex",
@@ -1897,7 +1908,7 @@ function reconcilePrice(model, live) {
1897
1908
  }
1898
1909
 
1899
1910
  // src/constants.ts
1900
- var CASCADE_VERSION = "0.55.0";
1911
+ var CASCADE_VERSION = "0.63.0";
1901
1912
  var CASCADE_CONFIG_DIR = ".cascade";
1902
1913
  var CASCADE_MD_FILE = "CASCADE.md";
1903
1914
  var CASCADE_IGNORE_FILE = ".cascadeignore";
@@ -4130,8 +4141,6 @@ var LocalRequestQueue = class {
4130
4141
  };
4131
4142
  }
4132
4143
  };
4133
-
4134
- // src/core/router/dead-models.ts
4135
4144
  var DEAD_MODEL_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
4136
4145
  function deadModelKey(provider, modelId) {
4137
4146
  return `${provider}:${modelId}`;
@@ -4232,16 +4241,13 @@ var DeadModelStore = class {
4232
4241
  function fileDeadModelPersistence(filePath) {
4233
4242
  return {
4234
4243
  load() {
4235
- const fs25 = __require("fs");
4236
- if (!fs25.existsSync(filePath)) return [];
4237
- const raw = JSON.parse(fs25.readFileSync(filePath, "utf-8"));
4244
+ if (!fs23.existsSync(filePath)) return [];
4245
+ const raw = JSON.parse(fs23.readFileSync(filePath, "utf-8"));
4238
4246
  return Array.isArray(raw) ? raw : [];
4239
4247
  },
4240
4248
  save(records) {
4241
- const fs25 = __require("fs");
4242
- const path26 = __require("path");
4243
- fs25.mkdirSync(path26.dirname(filePath), { recursive: true });
4244
- fs25.writeFileSync(filePath, JSON.stringify(records, null, 2));
4249
+ fs23.mkdirSync(path24.dirname(filePath), { recursive: true });
4250
+ fs23.writeFileSync(filePath, JSON.stringify(records, null, 2));
4245
4251
  }
4246
4252
  };
4247
4253
  }
@@ -4675,6 +4681,12 @@ var FAMILY_BENCHMARKS = {
4675
4681
  // Google — strongest at analysis/data and long-context.
4676
4682
  "gemini-2.5-pro": { code: 90, analysis: 93, creative: 86, data: 92 },
4677
4683
  "gemini-2.5-flash": { code: 82, analysis: 83, creative: 80, data: 82 },
4684
+ // The 2.5-lite tier scores below full 2.5-flash but above the older 2.0
4685
+ // lite tier — it exists because a naive `gemini-?2\.5-flash` regex swallows
4686
+ // "gemini-2.5-flash-lite" as a substring match (no lite variant of its own
4687
+ // to fall into), so the weaker lite model got credited with the full
4688
+ // flash's benchmark score and kept winning Cascade Auto's "best value" pick.
4689
+ "gemini-2.5-flash-lite": { code: 72, analysis: 72, creative: 74, data: 72 },
4678
4690
  "gemini-1.5-pro": { code: 82, analysis: 84, creative: 82, data: 85 },
4679
4691
  "gemini-2.0-flash": { code: 79, analysis: 80, creative: 79, data: 80 },
4680
4692
  "gemini-flash-lite": { code: 68, analysis: 68, creative: 70, data: 68 },
@@ -4707,6 +4719,11 @@ var FAMILY_MATCHERS = [
4707
4719
  [/gpt-?4o-mini/i, "gpt-4o-mini"],
4708
4720
  [/gpt-?4o/i, "gpt-4o"],
4709
4721
  [/gemini-?2\.5-pro/i, "gemini-2.5-pro"],
4722
+ // Must precede the bare `2\.5-flash` rule below: that pattern has no word
4723
+ // boundary after "flash" and matches "gemini-2.5-flash-lite" as a plain
4724
+ // substring, so the lite variant would otherwise be scored as the full
4725
+ // flash model (see the FAMILY_BENCHMARKS comment above).
4726
+ [/gemini-?2\.5-flash-lite/i, "gemini-2.5-flash-lite"],
4710
4727
  [/gemini-?2\.5-flash/i, "gemini-2.5-flash"],
4711
4728
  [/gemini-?1\.5-pro/i, "gemini-1.5-pro"],
4712
4729
  [/gemini-?2\.0-flash-lite/i, "gemini-flash-lite"],
@@ -6213,6 +6230,92 @@ function truncateForContext(text, maxChars = 12e3) {
6213
6230
  ${tail}`;
6214
6231
  }
6215
6232
 
6233
+ // src/core/router/provider-errors.ts
6234
+ var SYSTEMIC = /* @__PURE__ */ new Set([
6235
+ "rate_limit",
6236
+ "quota_exhausted",
6237
+ "auth",
6238
+ "model_unavailable"
6239
+ ]);
6240
+ function isSystemicKind(kind) {
6241
+ return SYSTEMIC.has(kind);
6242
+ }
6243
+ function statusOf(err) {
6244
+ if (!err || typeof err !== "object") return void 0;
6245
+ const e = err;
6246
+ for (const key of ["status", "statusCode", "code"]) {
6247
+ const v = e[key];
6248
+ if (typeof v === "number" && v >= 100 && v < 600) return v;
6249
+ }
6250
+ const resp = e["response"];
6251
+ if (resp && typeof resp["status"] === "number") return resp["status"];
6252
+ const inner = e["error"];
6253
+ if (inner && typeof inner["code"] === "number") return inner["code"];
6254
+ return void 0;
6255
+ }
6256
+ function messageOf(err) {
6257
+ if (err instanceof Error) return err.message;
6258
+ if (typeof err === "string") return err;
6259
+ if (err && typeof err === "object") {
6260
+ const e = err;
6261
+ if (typeof e["message"] === "string") return e["message"];
6262
+ const inner = e["error"];
6263
+ if (inner && typeof inner["message"] === "string") return inner["message"];
6264
+ try {
6265
+ return JSON.stringify(err);
6266
+ } catch {
6267
+ }
6268
+ }
6269
+ return String(err);
6270
+ }
6271
+ function classifyProviderError(err) {
6272
+ const raw = messageOf(err).trim();
6273
+ const status = statusOf(err);
6274
+ const m = raw.toLowerCase();
6275
+ const kind = (() => {
6276
+ if (status === 429) {
6277
+ return /quota|billing|credit|exceeded your current quota|insufficient/.test(m) ? "quota_exhausted" : "rate_limit";
6278
+ }
6279
+ if (status === 401 || status === 403) return "auth";
6280
+ if (status === 404) return "model_unavailable";
6281
+ if (/rate limit|too many requests|resource_exhausted/.test(m)) {
6282
+ return /quota|billing|credit|insufficient/.test(m) ? "quota_exhausted" : "rate_limit";
6283
+ }
6284
+ if (/quota|billing|insufficient_quota|insufficient credit|payment required/.test(m)) return "quota_exhausted";
6285
+ if (/api key|unauthorized|authentication|permission denied|invalid[_ ]api/.test(m)) return "auth";
6286
+ if (/model .*(not found|does not exist|unavailable)|no such model|unknown model|not supported for/.test(m)) {
6287
+ return "model_unavailable";
6288
+ }
6289
+ if (/context length|too many tokens|maximum context|prompt is too long|input too long/.test(m)) return "context_length";
6290
+ if (/safety|content filter|blocked|recitation|prohibited/.test(m)) return "content_filter";
6291
+ if (/econnreset|etimedout|enotfound|socket hang up|network|fetch failed|aborted/.test(m)) return "network";
6292
+ return "unknown";
6293
+ })();
6294
+ return { kind, systemic: isSystemicKind(kind), status, raw };
6295
+ }
6296
+ function describeProviderError(c, modelId) {
6297
+ const on = modelId ? ` on ${modelId}` : "";
6298
+ const detail = c.raw ? ` Provider said: ${c.raw}` : "";
6299
+ switch (c.kind) {
6300
+ case "rate_limit":
6301
+ 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}`;
6302
+ case "quota_exhausted":
6303
+ 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}`;
6304
+ case "auth":
6305
+ 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}`;
6306
+ case "model_unavailable":
6307
+ 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}`;
6308
+ case "context_length":
6309
+ return `The prompt was too long${on}. Reduce the attached context, or use a model with a larger context window for this tier.${detail}`;
6310
+ case "content_filter":
6311
+ return `The provider's safety filter refused this request${on}.${detail}`;
6312
+ case "network":
6313
+ return `Network error reaching the provider${on}. This is usually transient.${detail}`;
6314
+ default:
6315
+ return `The model call failed${on}.${detail}`;
6316
+ }
6317
+ }
6318
+
6216
6319
  // src/core/tiers/t3-worker.ts
6217
6320
  var CriticalToolError = class extends Error {
6218
6321
  constructor(message, toolName) {
@@ -6246,8 +6349,15 @@ var KNOWN_TOOLS = [
6246
6349
  "web_search",
6247
6350
  "glob",
6248
6351
  "grep",
6249
- "web_fetch"
6352
+ "web_fetch",
6353
+ "generate_image"
6250
6354
  ];
6355
+ var PROVIDER_BACKED_TOOLS = /* @__PURE__ */ new Set([
6356
+ "generate_image",
6357
+ "generate_speech",
6358
+ "generate_video",
6359
+ "transcribe_audio"
6360
+ ]);
6251
6361
  function buildWorkerRules(has) {
6252
6362
  const canWriteFiles = has("file_write") || has("file_edit") || has("run_code");
6253
6363
  const hasAnyTool = KNOWN_TOOLS.some(has);
@@ -6257,9 +6367,22 @@ function buildWorkerRules(has) {
6257
6367
  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.",
6258
6368
  has("web_search") && '- Use the "web_search" tool to find current information, documentation, news, or general web data.',
6259
6369
  has("pdf_create") && '- Use the "pdf_create" tool for PDF requests.',
6370
+ // Without this the model "writes the image" as prose — a bracketed
6371
+ // description sitting in the deck where the picture should be — even with a
6372
+ // working image model registered. Naming the reference syntax matters as
6373
+ // much as naming the tool: a generated image nobody embeds is still a
6374
+ // missing image.
6375
+ //
6376
+ // Scoped to PowerPoint/Word specifically: those are the only exporters
6377
+ // that actually embed a Markdown image reference (cloud/web/exporters.ts
6378
+ // toPptx/toDocx). PDF and plain-text/Markdown deliverables flatten a
6379
+ // reference straight to caption text — telling the model to call
6380
+ // generate_image for THOSE just pays for an image nobody ever sees.
6381
+ 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.',
6260
6382
  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.',
6261
6383
  "- If you are not making meaningful progress, stop and escalate rather than looping or padding the response.",
6262
6384
  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.',
6385
+ 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.",
6263
6386
  "- Return structured output that directly addresses the expected output specification."
6264
6387
  ];
6265
6388
  return `You are a T3 Worker agent in the Cascade AI system. Your job is to execute a specific subtask completely and accurately.
@@ -6793,7 +6916,9 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
6793
6916
  const durationMs = Date.now() - toolStartMs;
6794
6917
  const errMsg = err instanceof Error ? err.message : String(err);
6795
6918
  this.emit("tool:result", { id: tc.id, tierId: this.id, toolName: tc.name, error: errMsg, durationMs });
6796
- if (/\b(429|rate.?limit|authentication|api.?key|forbidden|401|403)\b/i.test(errMsg)) {
6919
+ const isTaggedSystemic = err instanceof Error && err.systemic === true;
6920
+ const isProviderBacked = PROVIDER_BACKED_TOOLS.has(tc.name);
6921
+ if (isTaggedSystemic || isProviderBacked && classifyProviderError(err).systemic) {
6797
6922
  throw new CriticalToolError(errMsg, tc.name);
6798
6923
  }
6799
6924
  return await this.adaptiveFallback(tc, `Tool error: ${errMsg}`);
@@ -7081,8 +7206,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
7081
7206
  } catch {
7082
7207
  return {
7083
7208
  checksRun: ["completeness", "correctness", "compliance"],
7084
- passed: ["completeness", "correctness", "compliance"],
7085
- failed: []
7209
+ passed: [],
7210
+ failed: ["completeness"]
7086
7211
  };
7087
7212
  }
7088
7213
  }
@@ -7872,7 +7997,7 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
7872
7997
  const jsonMatch = /\[[\s\S]*\]/.exec(result.content);
7873
7998
  if (!jsonMatch) throw new Error("No JSON array found");
7874
7999
  const parsed = JSON.parse(jsonMatch[0]);
7875
- return parsed.map((a) => ({ ...a, sectionTitle: assignment.sectionTitle }));
8000
+ return parsed.map((a) => ({ ...a, sectionTitle: assignment.sectionTitle, constraints: a.constraints ?? [] }));
7876
8001
  } catch {
7877
8002
  return [{
7878
8003
  subtaskId: crypto4.randomUUID(),
@@ -8830,6 +8955,12 @@ SPEC RULES \u2014 each subtask is a self-contained spec slice (workers execute f
8830
8955
  if (!plan.sections || !Array.isArray(plan.sections) || plan.sections.length === 0) {
8831
8956
  throw new Error("Invalid plan: no sections");
8832
8957
  }
8958
+ for (const section of plan.sections) {
8959
+ section.constraints ??= [];
8960
+ for (const subtask of section.t3Subtasks ?? []) {
8961
+ subtask.constraints ??= [];
8962
+ }
8963
+ }
8833
8964
  const [min, max] = COMPLEXITY_T2_COUNT[plan.complexity] ?? [1, 8];
8834
8965
  if (plan.sections.length < min) ;
8835
8966
  }
@@ -9145,17 +9276,17 @@ var CAPABILITIES = [
9145
9276
  api: "openai-images"
9146
9277
  },
9147
9278
  {
9148
- // Imagen, not `gemini-3-pro-image`. That id appears on Google's PRICING
9149
- // page but 404s on `:predict` ("not found for API version v1beta, or is not
9150
- // supported for predict") — the Gemini image models are called through
9151
- // generateContent with an image response modality, while `:predict` is the
9152
- // Imagen shape this executor implements. A billing line item is not proof
9153
- // of a callable endpoint, which is the assumption that put the wrong id
9154
- // here in the first place.
9279
+ // Was `imagen-4.0-generate-001` on `:predict`, which now 404s for new keys
9280
+ // ("no longer available to new users") — Imagen's :predict API is deprecated
9281
+ // and shuts down 2026-08-17. Google's own migration path is this model on
9282
+ // the ordinary generateContent endpoint, so the fix was never another Imagen
9283
+ // id: the whole request/response SHAPE changes, which is why `api` moved to
9284
+ // 'gemini-generate-content' rather than just the modelId moving.
9285
+ // https://ai.google.dev/gemini-api/docs/imagen
9155
9286
  modality: "image",
9156
9287
  provider: "gemini",
9157
- modelId: "imagen-4.0-generate-001",
9158
- api: "gemini-predict"
9288
+ modelId: "gemini-2.5-flash-image",
9289
+ api: "gemini-generate-content"
9159
9290
  },
9160
9291
  // ── Speech (text → audio) ──
9161
9292
  {
@@ -9210,6 +9341,14 @@ function capabilityCost(cap) {
9210
9341
  const shown = amount < 1e-3 ? amount.toExponential(2) : amount.toFixed(4);
9211
9342
  return { label: `$${shown} ${unit}`, amount, unit };
9212
9343
  }
9344
+ function explainChoice(c, cost, modality, isBest) {
9345
+ const priced = cost ? ` ${cost.label}.` : "";
9346
+ if (c.quality !== void 0) {
9347
+ 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}`;
9348
+ }
9349
+ const noRanking = ". No published quality ranking is applied for this modality, so this is a cost choice, not a quality one.";
9350
+ 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;
9351
+ }
9213
9352
  var MultimodalRegistry = class {
9214
9353
  /** Provider types the user has actually configured a key for. */
9215
9354
  available;
@@ -9227,22 +9366,20 @@ var MultimodalRegistry = class {
9227
9366
  return [...new Set(this.usable().map((c) => c.modality))];
9228
9367
  }
9229
9368
  /**
9230
- * Pick a model for a modality.
9369
+ * Every usable capability for a modality, best first.
9231
9370
  *
9232
9371
  * Ranked models beat unranked ones, then higher quality, then lower unit
9233
- * price. An explicit `preferModel` wins outright when it is usable — the user
9234
- * naming a model is a stronger signal than any prior.
9372
+ * price. An explicit `preferModel` is moved to the head when it is usable —
9373
+ * the user naming a model is a stronger signal than any prior.
9374
+ *
9375
+ * `select()` is just the head of this list. The TAIL is the point: a systemic
9376
+ * provider failure (dead key, 404, exhausted quota) is scoped to one
9377
+ * provider+model, so "this model cannot draw" is not the same fact as "this
9378
+ * account cannot draw". A caller holding the ordered alternatives can try the
9379
+ * next provider instead of aborting the whole run — see generate-media.ts.
9235
9380
  */
9236
- select(modality, opts = {}) {
9237
- const pool = this.usable(modality);
9238
- if (!pool.length) return null;
9239
- if (opts.preferModel) {
9240
- const wanted = pool.find((c) => c.modelId === opts.preferModel);
9241
- if (wanted) {
9242
- return { capability: wanted, cost: capabilityCost(wanted), reason: `Requested explicitly: ${wanted.modelId}.` };
9243
- }
9244
- }
9245
- const scored = pool.map((c) => ({ c, cost: capabilityCost(c) })).sort((a, b) => {
9381
+ rank(modality, opts = {}) {
9382
+ const scored = this.usable(modality).map((c) => ({ c, cost: capabilityCost(c) })).sort((a, b) => {
9246
9383
  const qa = a.c.quality ?? -1;
9247
9384
  const qb = b.c.quality ?? -1;
9248
9385
  if (qa !== qb) return qb - qa;
@@ -9250,10 +9387,25 @@ var MultimodalRegistry = class {
9250
9387
  const pb = b.cost?.amount ?? Number.POSITIVE_INFINITY;
9251
9388
  return pa - pb;
9252
9389
  });
9253
- const best = scored[0];
9254
- const ranked = best.c.quality !== void 0;
9255
- 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.";
9256
- return { capability: best.c, cost: best.cost, reason };
9390
+ const results = scored.map(({ c, cost }, i) => ({
9391
+ capability: c,
9392
+ cost,
9393
+ reason: explainChoice(c, cost, modality, i === 0)
9394
+ }));
9395
+ if (opts.preferModel) {
9396
+ const i = results.findIndex((r) => r.capability.modelId === opts.preferModel);
9397
+ if (i > -1) {
9398
+ const [wanted] = results.splice(i, 1);
9399
+ results.unshift({ ...wanted, reason: `Requested explicitly: ${wanted.capability.modelId}.` });
9400
+ }
9401
+ }
9402
+ return results;
9403
+ }
9404
+ /**
9405
+ * Pick a model for a modality — the best usable one, or null if there is none.
9406
+ */
9407
+ select(modality, opts = {}) {
9408
+ return this.rank(modality, opts)[0] ?? null;
9257
9409
  }
9258
9410
  /**
9259
9411
  * A plain-language account of what this account can and cannot generate —
@@ -9306,92 +9458,6 @@ var BaseTool = class {
9306
9458
  }
9307
9459
  };
9308
9460
 
9309
- // src/core/router/provider-errors.ts
9310
- var SYSTEMIC = /* @__PURE__ */ new Set([
9311
- "rate_limit",
9312
- "quota_exhausted",
9313
- "auth",
9314
- "model_unavailable"
9315
- ]);
9316
- function isSystemicKind(kind) {
9317
- return SYSTEMIC.has(kind);
9318
- }
9319
- function statusOf(err) {
9320
- if (!err || typeof err !== "object") return void 0;
9321
- const e = err;
9322
- for (const key of ["status", "statusCode", "code"]) {
9323
- const v = e[key];
9324
- if (typeof v === "number" && v >= 100 && v < 600) return v;
9325
- }
9326
- const resp = e["response"];
9327
- if (resp && typeof resp["status"] === "number") return resp["status"];
9328
- const inner = e["error"];
9329
- if (inner && typeof inner["code"] === "number") return inner["code"];
9330
- return void 0;
9331
- }
9332
- function messageOf(err) {
9333
- if (err instanceof Error) return err.message;
9334
- if (typeof err === "string") return err;
9335
- if (err && typeof err === "object") {
9336
- const e = err;
9337
- if (typeof e["message"] === "string") return e["message"];
9338
- const inner = e["error"];
9339
- if (inner && typeof inner["message"] === "string") return inner["message"];
9340
- try {
9341
- return JSON.stringify(err);
9342
- } catch {
9343
- }
9344
- }
9345
- return String(err);
9346
- }
9347
- function classifyProviderError(err) {
9348
- const raw = messageOf(err).trim();
9349
- const status = statusOf(err);
9350
- const m = raw.toLowerCase();
9351
- const kind = (() => {
9352
- if (status === 429) {
9353
- return /quota|billing|credit|exceeded your current quota|insufficient/.test(m) ? "quota_exhausted" : "rate_limit";
9354
- }
9355
- if (status === 401 || status === 403) return "auth";
9356
- if (status === 404) return "model_unavailable";
9357
- if (/rate limit|too many requests|resource_exhausted/.test(m)) {
9358
- return /quota|billing|credit|insufficient/.test(m) ? "quota_exhausted" : "rate_limit";
9359
- }
9360
- if (/quota|billing|insufficient_quota|insufficient credit|payment required/.test(m)) return "quota_exhausted";
9361
- if (/api key|unauthorized|authentication|permission denied|invalid[_ ]api/.test(m)) return "auth";
9362
- if (/model .*(not found|does not exist|unavailable)|no such model|unknown model|not supported for/.test(m)) {
9363
- return "model_unavailable";
9364
- }
9365
- if (/context length|too many tokens|maximum context|prompt is too long|input too long/.test(m)) return "context_length";
9366
- if (/safety|content filter|blocked|recitation|prohibited/.test(m)) return "content_filter";
9367
- if (/econnreset|etimedout|enotfound|socket hang up|network|fetch failed|aborted/.test(m)) return "network";
9368
- return "unknown";
9369
- })();
9370
- return { kind, systemic: isSystemicKind(kind), status, raw };
9371
- }
9372
- function describeProviderError(c, modelId) {
9373
- const on = modelId ? ` on ${modelId}` : "";
9374
- const detail = c.raw ? ` Provider said: ${c.raw}` : "";
9375
- switch (c.kind) {
9376
- case "rate_limit":
9377
- 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}`;
9378
- case "quota_exhausted":
9379
- 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}`;
9380
- case "auth":
9381
- 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}`;
9382
- case "model_unavailable":
9383
- 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}`;
9384
- case "context_length":
9385
- return `The prompt was too long${on}. Reduce the attached context, or use a model with a larger context window for this tier.${detail}`;
9386
- case "content_filter":
9387
- return `The provider's safety filter refused this request${on}.${detail}`;
9388
- case "network":
9389
- return `Network error reaching the provider${on}. This is usually transient.${detail}`;
9390
- default:
9391
- return `The model call failed${on}.${detail}`;
9392
- }
9393
- }
9394
-
9395
9461
  // src/core/multimodal/generate.ts
9396
9462
  var DEFAULT_TIMEOUT_MS = 12e4;
9397
9463
  async function callProvider(fn, modelId) {
@@ -9430,6 +9496,29 @@ function baseUrlFor(cfg, fallback) {
9430
9496
  function stamp(ext) {
9431
9497
  return `cascade-${Date.now()}.${ext}`;
9432
9498
  }
9499
+ var GEMINI_ASPECT_RATIOS = [
9500
+ ["1:1", 1 / 1],
9501
+ ["3:2", 3 / 2],
9502
+ ["2:3", 2 / 3],
9503
+ ["3:4", 3 / 4],
9504
+ ["4:3", 4 / 3],
9505
+ ["4:5", 4 / 5],
9506
+ ["5:4", 5 / 4],
9507
+ ["9:16", 9 / 16],
9508
+ ["16:9", 16 / 9],
9509
+ ["21:9", 21 / 9]
9510
+ ];
9511
+ function closestGeminiAspectRatio(size) {
9512
+ const m = size.match(/^(\d+)\s*[x×]\s*(\d+)$/i);
9513
+ if (!m) return void 0;
9514
+ const w = Number(m[1]);
9515
+ const h = Number(m[2]);
9516
+ if (!w || !h) return void 0;
9517
+ const ratio = w / h;
9518
+ return GEMINI_ASPECT_RATIOS.reduce(
9519
+ (best, cand) => Math.abs(cand[1] - ratio) < Math.abs(best[1] - ratio) ? cand : best
9520
+ )[0];
9521
+ }
9433
9522
  async function generateImage(cap, cfg, req, signal) {
9434
9523
  return callProvider(async () => {
9435
9524
  if (cap.api === "openai-images") {
@@ -9454,19 +9543,37 @@ async function generateImage(cap, cfg, req, signal) {
9454
9543
  ...first.revised_prompt ? { revisedPrompt: first.revised_prompt } : {}
9455
9544
  };
9456
9545
  }
9457
- if (cap.api === "gemini-predict") {
9546
+ if (cap.api === "gemini-generate-content") {
9458
9547
  const base = baseUrlFor(cfg, "https://generativelanguage.googleapis.com/v1beta");
9459
- const url = `${base}/models/${encodeURIComponent(cap.modelId)}:predict`;
9548
+ const url = `${base}/models/${encodeURIComponent(cap.modelId)}:generateContent`;
9549
+ const aspectRatio = req.size ? closestGeminiAspectRatio(req.size) : void 0;
9460
9550
  const res = await postJson(url, {
9461
- instances: [{ prompt: req.prompt }],
9462
- parameters: { sampleCount: 1 }
9551
+ contents: [{ parts: [{ text: req.prompt }] }],
9552
+ generationConfig: {
9553
+ // Asked for explicitly: these models can answer with text only, and a
9554
+ // text-only reply to "draw me a cat" is a failed image generation.
9555
+ responseModalities: ["IMAGE", "TEXT"],
9556
+ // Gemini has no free-form pixel size — only a named aspect ratio (see
9557
+ // closestGeminiAspectRatio). Omitted entirely when there's no
9558
+ // requested size, or no supported ratio close enough to bother with.
9559
+ ...aspectRatio ? { imageConfig: { aspectRatio } } : {}
9560
+ }
9463
9561
  }, { "x-goog-api-key": cfg.apiKey ?? "" }, signal);
9464
9562
  const body = await res.json();
9465
- const first = body.predictions?.[0];
9466
- if (!first?.bytesBase64Encoded) throw new Error("Image response contained no image data.");
9467
- const mimeType = first.mimeType ?? "image/png";
9563
+ const candidate = body.candidates?.[0];
9564
+ const parts = candidate?.content?.parts ?? [];
9565
+ const image = parts.find((p) => p.inlineData?.data)?.inlineData;
9566
+ if (!image?.data) {
9567
+ const blocked = body.promptFeedback?.blockReason ?? (candidate?.finishReason && candidate.finishReason !== "STOP" ? candidate.finishReason : void 0);
9568
+ if (blocked) throw new Error(`Image request was blocked by the provider (${blocked}).`);
9569
+ const said = parts.find((p) => p.text)?.text?.trim();
9570
+ throw new Error(
9571
+ "Image response contained no image data." + (said ? ` The model replied with text instead: ${said}` : "")
9572
+ );
9573
+ }
9574
+ const mimeType = image.mimeType ?? "image/png";
9468
9575
  return {
9469
- data: Buffer.from(first.bytesBase64Encoded, "base64"),
9576
+ data: Buffer.from(image.data, "base64"),
9470
9577
  mimeType,
9471
9578
  filename: stamp(mimeType.includes("jpeg") ? "jpg" : "png"),
9472
9579
  modelId: cap.modelId,
@@ -9619,6 +9726,49 @@ function resolve(deps, modality, preferModel) {
9619
9726
  }
9620
9727
  return { ok: true, selected, cfg };
9621
9728
  }
9729
+ function messageOf2(err) {
9730
+ return err instanceof Error ? err.message : String(err);
9731
+ }
9732
+ async function runWithProviderFallback(deps, modality, first, run, signal) {
9733
+ const failed = first.selected.capability;
9734
+ try {
9735
+ return { value: await run(failed, first.cfg), selected: first.selected };
9736
+ } catch (err) {
9737
+ if (signal?.aborted || !classifyProviderError(err).systemic) throw err;
9738
+ const tried = [`${failed.modelId} (${failed.provider}): ${messageOf2(err)}`];
9739
+ let allSystemic = true;
9740
+ let lastNonSystemic;
9741
+ const seen = /* @__PURE__ */ new Set([failed.provider]);
9742
+ for (const alt of deps.registry.rank(modality)) {
9743
+ if (seen.has(alt.capability.provider)) continue;
9744
+ const cfg = deps.lookupProvider(alt.capability.provider);
9745
+ if (!cfg) continue;
9746
+ seen.add(alt.capability.provider);
9747
+ try {
9748
+ return {
9749
+ value: await run(alt.capability, cfg),
9750
+ selected: alt,
9751
+ fellBackFrom: { capability: failed, error: messageOf2(err) }
9752
+ };
9753
+ } catch (retryErr) {
9754
+ if (signal?.aborted) throw retryErr;
9755
+ tried.push(`${alt.capability.modelId} (${alt.capability.provider}): ${messageOf2(retryErr)}`);
9756
+ if (classifyProviderError(retryErr).systemic) {
9757
+ continue;
9758
+ }
9759
+ allSystemic = false;
9760
+ lastNonSystemic = retryErr;
9761
+ }
9762
+ }
9763
+ if (tried.length === 1) throw err;
9764
+ if (!allSystemic) throw lastNonSystemic;
9765
+ const exhausted = new Error(
9766
+ `${modality} generation failed on every configured provider (${seen.size} tried). ${tried.join(" ")}`
9767
+ );
9768
+ Object.assign(exhausted, { systemic: true });
9769
+ throw exhausted;
9770
+ }
9771
+ }
9622
9772
  var GenerateImageTool = class extends BaseTool {
9623
9773
  constructor(deps) {
9624
9774
  super();
@@ -9642,16 +9792,32 @@ var GenerateImageTool = class extends BaseTool {
9642
9792
  const r = resolve(this.deps, "image", input["model"] ? String(input["model"]) : void 0);
9643
9793
  if (!r.ok) return r.error;
9644
9794
  const size = input["size"] ? String(input["size"]) : void 0;
9645
- const asset = await generateImage(
9646
- r.selected.capability,
9647
- r.cfg,
9648
- { prompt, ...size ? { size } : {} },
9795
+ const attempt = await runWithProviderFallback(
9796
+ this.deps,
9797
+ "image",
9798
+ r,
9799
+ (cap, cfg) => generateImage(cap, cfg, { prompt, ...size ? { size } : {} }, options.signal),
9649
9800
  options.signal
9650
9801
  );
9802
+ const asset = attempt.value;
9803
+ const fell = attempt.fellBackFrom;
9651
9804
  const location = await this.deps.sink(asset);
9652
9805
  return [
9653
9806
  `Image generated and saved to ${location}.`,
9654
- `Model: ${asset.modelId} \u2014 ${r.selected.reason}`,
9807
+ // Knowing WHERE the image is isn't the same as knowing how to use it: a
9808
+ // model told only "saved to X" tends to describe the picture in prose
9809
+ // instead of embedding it. Spelling out the reference syntax here makes
9810
+ // the worker's "never write a placeholder" rule concretely actionable,
9811
+ // and it works for every host because `location` is whatever that host's
9812
+ // sink returned — a workspace path on desktop/CLI, a URL in the cloud.
9813
+ `To include this image in your output, reference it as: ![description](${location})`,
9814
+ // The reported model has to be the one that actually drew the picture, not
9815
+ // the one that was picked and then failed.
9816
+ `Model: ${asset.modelId} \u2014 ${fell ? `used as a fallback after ${fell.capability.modelId} (${fell.capability.provider}) failed with a systemic error.` : attempt.selected.reason}`,
9817
+ // Worth one line: the run succeeded, so nothing else will report that a
9818
+ // configured provider is broken, and "your Gemini key is dead" is exactly
9819
+ // the kind of thing the user wants to hear once rather than never.
9820
+ fell ? `Note: ${fell.capability.provider} failed first \u2014 ${fell.error}` : "",
9655
9821
  // DALL·E rewrites the prompt before drawing; the model needs to know what
9656
9822
  // was actually rendered, not what it asked for, or its description of the
9657
9823
  // result will not match the image.
@@ -9894,6 +10060,87 @@ function mcpToolName(serverName, toolName) {
9894
10060
  function isMcpToolName(name) {
9895
10061
  return name.startsWith(MCP_TOOL_PREFIX);
9896
10062
  }
10063
+ var READ_ONLY_MCP_VERBS = ["list", "get", "search", "read", "describe", "find", "query", "fetch", "show", "view"];
10064
+ var MUTATING_MCP_TOKENS = /* @__PURE__ */ new Set([
10065
+ "create",
10066
+ "delete",
10067
+ "remove",
10068
+ "update",
10069
+ "write",
10070
+ "modify",
10071
+ "set",
10072
+ "put",
10073
+ "post",
10074
+ "patch",
10075
+ "push",
10076
+ "merge",
10077
+ "send",
10078
+ "execute",
10079
+ "exec",
10080
+ "run",
10081
+ "deploy",
10082
+ "install",
10083
+ "uninstall",
10084
+ "upload",
10085
+ "publish",
10086
+ "revoke",
10087
+ "grant",
10088
+ "approve",
10089
+ "reject",
10090
+ "cancel",
10091
+ "enable",
10092
+ "disable",
10093
+ "add",
10094
+ "insert",
10095
+ "drop",
10096
+ "truncate",
10097
+ "clear",
10098
+ "reset",
10099
+ "restart",
10100
+ "stop",
10101
+ "kill",
10102
+ "terminate",
10103
+ "rename",
10104
+ "move",
10105
+ "fork",
10106
+ "sync",
10107
+ "replace",
10108
+ "overwrite",
10109
+ "invite",
10110
+ "ban",
10111
+ "block",
10112
+ "assign",
10113
+ "lock",
10114
+ "unlock",
10115
+ "archive",
10116
+ "star",
10117
+ "comment",
10118
+ "reply",
10119
+ "submit",
10120
+ "apply",
10121
+ "trigger",
10122
+ "invoke",
10123
+ "purge",
10124
+ "migrate",
10125
+ "edit",
10126
+ "save",
10127
+ "commit"
10128
+ ]);
10129
+ function mcpTokens(rawToolName) {
10130
+ return rawToolName.split(/[_-]+/).flatMap((segment) => segment.split(/(?<=[a-z0-9])(?=[A-Z])/)).map((t) => t.toLowerCase()).filter(Boolean);
10131
+ }
10132
+ function isReadOnlyMcpToolName(rawToolName) {
10133
+ if (mcpTokens(rawToolName).some((t) => MUTATING_MCP_TOKENS.has(t))) return false;
10134
+ const lower = rawToolName.toLowerCase();
10135
+ for (const verb of READ_ONLY_MCP_VERBS) {
10136
+ if (!lower.startsWith(verb)) continue;
10137
+ const boundary = rawToolName[verb.length];
10138
+ if (boundary === void 0 || boundary === "_" || boundary === "-" || boundary >= "A" && boundary <= "Z") {
10139
+ return true;
10140
+ }
10141
+ }
10142
+ return false;
10143
+ }
9897
10144
  function uniqueMcpServerName(desired, existingNames) {
9898
10145
  const taken = new Set(existingNames.map(mcpServerPrefix));
9899
10146
  if (!taken.has(mcpServerPrefix(desired))) return desired;
@@ -10987,9 +11234,11 @@ var WebSearchTool = class extends BaseTool {
10987
11234
  );
10988
11235
  const errors = [];
10989
11236
  let results = [];
11237
+ let anyBackendReachable = false;
10990
11238
  if (this.config.searxngUrl) {
10991
11239
  try {
10992
11240
  results = await searchSearXNG(query, this.config.searxngUrl, maxResults);
11241
+ anyBackendReachable = true;
10993
11242
  if (results.length > 0) return this.formatResults(query, results);
10994
11243
  errors.push("SearXNG: returned 0 results");
10995
11244
  } catch (err) {
@@ -10999,6 +11248,7 @@ var WebSearchTool = class extends BaseTool {
10999
11248
  if (this.config.braveApiKey) {
11000
11249
  try {
11001
11250
  results = await searchBrave(query, this.config.braveApiKey, maxResults);
11251
+ anyBackendReachable = true;
11002
11252
  if (results.length > 0) return this.formatResults(query, results);
11003
11253
  errors.push("Brave: returned 0 results");
11004
11254
  } catch (err) {
@@ -11008,6 +11258,7 @@ var WebSearchTool = class extends BaseTool {
11008
11258
  if (this.config.tavilyApiKey) {
11009
11259
  try {
11010
11260
  results = await searchTavily(query, this.config.tavilyApiKey, maxResults);
11261
+ anyBackendReachable = true;
11011
11262
  if (results.length > 0) return this.formatResults(query, results);
11012
11263
  errors.push("Tavily: returned 0 results");
11013
11264
  } catch (err) {
@@ -11017,6 +11268,7 @@ var WebSearchTool = class extends BaseTool {
11017
11268
  for (const variant of ["html", "lite"]) {
11018
11269
  try {
11019
11270
  results = await searchDuckDuckGo(query, maxResults, variant);
11271
+ anyBackendReachable = true;
11020
11272
  if (results.length > 0) return this.formatResults(query, results);
11021
11273
  errors.push(`DuckDuckGo ${variant}: returned 0 results`);
11022
11274
  } catch (err) {
@@ -11024,11 +11276,11 @@ var WebSearchTool = class extends BaseTool {
11024
11276
  }
11025
11277
  }
11026
11278
  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" : "";
11027
- return [
11028
- `Web search for "${query}" failed across all backends:`,
11279
+ throw Object.assign(new Error([
11280
+ `Web search for "${query}" found nothing across all backends:`,
11029
11281
  ...errors.map((e) => ` \u2022 ${e}`),
11030
11282
  configHint
11031
- ].join("\n");
11283
+ ].join("\n")), { systemic: !anyBackendReachable });
11032
11284
  }
11033
11285
  formatResults(query, results) {
11034
11286
  const lines = [`Web search results for: "${query}"`, ""];
@@ -11396,6 +11648,15 @@ var McpToolWrapper = class extends BaseTool {
11396
11648
  this.description = `[MCP:${serverName}] ${description}`;
11397
11649
  this.inputSchema = inputSchema;
11398
11650
  }
11651
+ // Every built-in tool of comparable risk (shell, git, file writes, the
11652
+ // built-in github tool) overrides this to true. An MCP tool never did —
11653
+ // BaseTool's default (false) meant a connected server's create/delete/push
11654
+ // actions ran with zero human approval, whatever they did. MCP carries no
11655
+ // standard "this mutates state" flag, so default to dangerous and name the
11656
+ // exception (a read-only-looking verb) rather than the other way around.
11657
+ isDangerous() {
11658
+ return !isReadOnlyMcpToolName(this.toolName);
11659
+ }
11399
11660
  async execute(input, _options) {
11400
11661
  return this.mcpClient.callTool(this.serverName, this.toolName, input);
11401
11662
  }
@@ -11508,7 +11769,7 @@ var ToolRegistry = class extends EventEmitter__default.default {
11508
11769
  requiresApproval(toolName) {
11509
11770
  const defaults = DEFAULT_APPROVAL_REQUIRED;
11510
11771
  const configured = this.config.requireApprovalFor;
11511
- return defaults.includes(toolName) || configured.includes(toolName);
11772
+ return defaults.includes(toolName) || configured.includes(toolName) || this.isDangerous(toolName);
11512
11773
  }
11513
11774
  isDangerous(toolName) {
11514
11775
  return this.tools.get(toolName)?.isDangerous() ?? false;
@@ -14876,9 +15137,8 @@ var Cascade = class _Cascade extends EventEmitter__default.default {
14876
15137
  });
14877
15138
  this.toolRegistry = new ToolRegistry(this.config.tools, workspacePath);
14878
15139
  try {
14879
- const nodePath = __require("path");
14880
15140
  this.router.setDeadModelStore(new DeadModelStore(
14881
- fileDeadModelPersistence(nodePath.join(workspacePath, ".cascade", "dead-models.json"))
15141
+ fileDeadModelPersistence(path24__default.default.join(workspacePath, ".cascade", "dead-models.json"))
14882
15142
  ));
14883
15143
  } catch {
14884
15144
  }