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.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import Database from 'better-sqlite3';
2
- import path24 from 'path';
3
- import fs23 from 'fs';
2
+ import path24, { dirname } from 'path';
3
+ import fs23, { mkdirSync, writeFileSync, existsSync, readFileSync } from 'fs';
4
4
  import os6 from 'os';
5
5
  import crypto4, { randomUUID, randomBytes, createHash, timingSafeEqual, webcrypto } from 'crypto';
6
6
  import EventEmitter, { setMaxListeners } from 'events';
@@ -41,12 +41,6 @@ import cron from 'node-cron';
41
41
  // Cascade AI — Multi-tier AI Orchestration System
42
42
  var __defProp = Object.defineProperty;
43
43
  var __getOwnPropNames = Object.getOwnPropertyNames;
44
- var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
45
- get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
46
- }) : x)(function(x) {
47
- if (typeof require !== "undefined") return require.apply(this, arguments);
48
- throw Error('Dynamic require of "' + x + '" is not supported');
49
- });
50
44
  var __esm = (fn, res) => function __init() {
51
45
  return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
52
46
  };
@@ -1597,6 +1591,23 @@ var pricing_data_default = {
1597
1591
  ],
1598
1592
  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."
1599
1593
  },
1594
+ {
1595
+ model: "gemini-2.5-flash-image",
1596
+ provider: "gemini",
1597
+ modality: "image",
1598
+ currency: "USD",
1599
+ asOf: "2026-07-27",
1600
+ source: "https://ai.google.dev/gemini-api/docs/pricing#gemini-2.5-flash-image",
1601
+ provenance: "litellm-catalog",
1602
+ rates: [
1603
+ {
1604
+ unit: "per_image",
1605
+ amount: 0.039,
1606
+ variant: "default 1024px output"
1607
+ }
1608
+ ],
1609
+ 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."
1610
+ },
1600
1611
  {
1601
1612
  model: "imagen-4.0-generate-001",
1602
1613
  provider: "vertex",
@@ -1851,7 +1862,7 @@ function reconcilePrice(model, live) {
1851
1862
  }
1852
1863
 
1853
1864
  // src/constants.ts
1854
- var CASCADE_VERSION = "0.55.0";
1865
+ var CASCADE_VERSION = "0.63.0";
1855
1866
  var CASCADE_CONFIG_DIR = ".cascade";
1856
1867
  var CASCADE_MD_FILE = "CASCADE.md";
1857
1868
  var CASCADE_IGNORE_FILE = ".cascadeignore";
@@ -4084,8 +4095,6 @@ var LocalRequestQueue = class {
4084
4095
  };
4085
4096
  }
4086
4097
  };
4087
-
4088
- // src/core/router/dead-models.ts
4089
4098
  var DEAD_MODEL_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
4090
4099
  function deadModelKey(provider, modelId) {
4091
4100
  return `${provider}:${modelId}`;
@@ -4186,16 +4195,13 @@ var DeadModelStore = class {
4186
4195
  function fileDeadModelPersistence(filePath) {
4187
4196
  return {
4188
4197
  load() {
4189
- const fs25 = __require("fs");
4190
- if (!fs25.existsSync(filePath)) return [];
4191
- const raw = JSON.parse(fs25.readFileSync(filePath, "utf-8"));
4198
+ if (!existsSync(filePath)) return [];
4199
+ const raw = JSON.parse(readFileSync(filePath, "utf-8"));
4192
4200
  return Array.isArray(raw) ? raw : [];
4193
4201
  },
4194
4202
  save(records) {
4195
- const fs25 = __require("fs");
4196
- const path26 = __require("path");
4197
- fs25.mkdirSync(path26.dirname(filePath), { recursive: true });
4198
- fs25.writeFileSync(filePath, JSON.stringify(records, null, 2));
4203
+ mkdirSync(dirname(filePath), { recursive: true });
4204
+ writeFileSync(filePath, JSON.stringify(records, null, 2));
4199
4205
  }
4200
4206
  };
4201
4207
  }
@@ -4629,6 +4635,12 @@ var FAMILY_BENCHMARKS = {
4629
4635
  // Google — strongest at analysis/data and long-context.
4630
4636
  "gemini-2.5-pro": { code: 90, analysis: 93, creative: 86, data: 92 },
4631
4637
  "gemini-2.5-flash": { code: 82, analysis: 83, creative: 80, data: 82 },
4638
+ // The 2.5-lite tier scores below full 2.5-flash but above the older 2.0
4639
+ // lite tier — it exists because a naive `gemini-?2\.5-flash` regex swallows
4640
+ // "gemini-2.5-flash-lite" as a substring match (no lite variant of its own
4641
+ // to fall into), so the weaker lite model got credited with the full
4642
+ // flash's benchmark score and kept winning Cascade Auto's "best value" pick.
4643
+ "gemini-2.5-flash-lite": { code: 72, analysis: 72, creative: 74, data: 72 },
4632
4644
  "gemini-1.5-pro": { code: 82, analysis: 84, creative: 82, data: 85 },
4633
4645
  "gemini-2.0-flash": { code: 79, analysis: 80, creative: 79, data: 80 },
4634
4646
  "gemini-flash-lite": { code: 68, analysis: 68, creative: 70, data: 68 },
@@ -4661,6 +4673,11 @@ var FAMILY_MATCHERS = [
4661
4673
  [/gpt-?4o-mini/i, "gpt-4o-mini"],
4662
4674
  [/gpt-?4o/i, "gpt-4o"],
4663
4675
  [/gemini-?2\.5-pro/i, "gemini-2.5-pro"],
4676
+ // Must precede the bare `2\.5-flash` rule below: that pattern has no word
4677
+ // boundary after "flash" and matches "gemini-2.5-flash-lite" as a plain
4678
+ // substring, so the lite variant would otherwise be scored as the full
4679
+ // flash model (see the FAMILY_BENCHMARKS comment above).
4680
+ [/gemini-?2\.5-flash-lite/i, "gemini-2.5-flash-lite"],
4664
4681
  [/gemini-?2\.5-flash/i, "gemini-2.5-flash"],
4665
4682
  [/gemini-?1\.5-pro/i, "gemini-1.5-pro"],
4666
4683
  [/gemini-?2\.0-flash-lite/i, "gemini-flash-lite"],
@@ -6167,6 +6184,92 @@ function truncateForContext(text, maxChars = 12e3) {
6167
6184
  ${tail}`;
6168
6185
  }
6169
6186
 
6187
+ // src/core/router/provider-errors.ts
6188
+ var SYSTEMIC = /* @__PURE__ */ new Set([
6189
+ "rate_limit",
6190
+ "quota_exhausted",
6191
+ "auth",
6192
+ "model_unavailable"
6193
+ ]);
6194
+ function isSystemicKind(kind) {
6195
+ return SYSTEMIC.has(kind);
6196
+ }
6197
+ function statusOf(err) {
6198
+ if (!err || typeof err !== "object") return void 0;
6199
+ const e = err;
6200
+ for (const key of ["status", "statusCode", "code"]) {
6201
+ const v = e[key];
6202
+ if (typeof v === "number" && v >= 100 && v < 600) return v;
6203
+ }
6204
+ const resp = e["response"];
6205
+ if (resp && typeof resp["status"] === "number") return resp["status"];
6206
+ const inner = e["error"];
6207
+ if (inner && typeof inner["code"] === "number") return inner["code"];
6208
+ return void 0;
6209
+ }
6210
+ function messageOf(err) {
6211
+ if (err instanceof Error) return err.message;
6212
+ if (typeof err === "string") return err;
6213
+ if (err && typeof err === "object") {
6214
+ const e = err;
6215
+ if (typeof e["message"] === "string") return e["message"];
6216
+ const inner = e["error"];
6217
+ if (inner && typeof inner["message"] === "string") return inner["message"];
6218
+ try {
6219
+ return JSON.stringify(err);
6220
+ } catch {
6221
+ }
6222
+ }
6223
+ return String(err);
6224
+ }
6225
+ function classifyProviderError(err) {
6226
+ const raw = messageOf(err).trim();
6227
+ const status = statusOf(err);
6228
+ const m = raw.toLowerCase();
6229
+ const kind = (() => {
6230
+ if (status === 429) {
6231
+ return /quota|billing|credit|exceeded your current quota|insufficient/.test(m) ? "quota_exhausted" : "rate_limit";
6232
+ }
6233
+ if (status === 401 || status === 403) return "auth";
6234
+ if (status === 404) return "model_unavailable";
6235
+ if (/rate limit|too many requests|resource_exhausted/.test(m)) {
6236
+ return /quota|billing|credit|insufficient/.test(m) ? "quota_exhausted" : "rate_limit";
6237
+ }
6238
+ if (/quota|billing|insufficient_quota|insufficient credit|payment required/.test(m)) return "quota_exhausted";
6239
+ if (/api key|unauthorized|authentication|permission denied|invalid[_ ]api/.test(m)) return "auth";
6240
+ if (/model .*(not found|does not exist|unavailable)|no such model|unknown model|not supported for/.test(m)) {
6241
+ return "model_unavailable";
6242
+ }
6243
+ if (/context length|too many tokens|maximum context|prompt is too long|input too long/.test(m)) return "context_length";
6244
+ if (/safety|content filter|blocked|recitation|prohibited/.test(m)) return "content_filter";
6245
+ if (/econnreset|etimedout|enotfound|socket hang up|network|fetch failed|aborted/.test(m)) return "network";
6246
+ return "unknown";
6247
+ })();
6248
+ return { kind, systemic: isSystemicKind(kind), status, raw };
6249
+ }
6250
+ function describeProviderError(c, modelId) {
6251
+ const on = modelId ? ` on ${modelId}` : "";
6252
+ const detail = c.raw ? ` Provider said: ${c.raw}` : "";
6253
+ switch (c.kind) {
6254
+ case "rate_limit":
6255
+ 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}`;
6256
+ case "quota_exhausted":
6257
+ 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}`;
6258
+ case "auth":
6259
+ 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}`;
6260
+ case "model_unavailable":
6261
+ 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}`;
6262
+ case "context_length":
6263
+ return `The prompt was too long${on}. Reduce the attached context, or use a model with a larger context window for this tier.${detail}`;
6264
+ case "content_filter":
6265
+ return `The provider's safety filter refused this request${on}.${detail}`;
6266
+ case "network":
6267
+ return `Network error reaching the provider${on}. This is usually transient.${detail}`;
6268
+ default:
6269
+ return `The model call failed${on}.${detail}`;
6270
+ }
6271
+ }
6272
+
6170
6273
  // src/core/tiers/t3-worker.ts
6171
6274
  var CriticalToolError = class extends Error {
6172
6275
  constructor(message, toolName) {
@@ -6200,8 +6303,15 @@ var KNOWN_TOOLS = [
6200
6303
  "web_search",
6201
6304
  "glob",
6202
6305
  "grep",
6203
- "web_fetch"
6306
+ "web_fetch",
6307
+ "generate_image"
6204
6308
  ];
6309
+ var PROVIDER_BACKED_TOOLS = /* @__PURE__ */ new Set([
6310
+ "generate_image",
6311
+ "generate_speech",
6312
+ "generate_video",
6313
+ "transcribe_audio"
6314
+ ]);
6205
6315
  function buildWorkerRules(has) {
6206
6316
  const canWriteFiles = has("file_write") || has("file_edit") || has("run_code");
6207
6317
  const hasAnyTool = KNOWN_TOOLS.some(has);
@@ -6211,9 +6321,22 @@ function buildWorkerRules(has) {
6211
6321
  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.",
6212
6322
  has("web_search") && '- Use the "web_search" tool to find current information, documentation, news, or general web data.',
6213
6323
  has("pdf_create") && '- Use the "pdf_create" tool for PDF requests.',
6324
+ // Without this the model "writes the image" as prose — a bracketed
6325
+ // description sitting in the deck where the picture should be — even with a
6326
+ // working image model registered. Naming the reference syntax matters as
6327
+ // much as naming the tool: a generated image nobody embeds is still a
6328
+ // missing image.
6329
+ //
6330
+ // Scoped to PowerPoint/Word specifically: those are the only exporters
6331
+ // that actually embed a Markdown image reference (cloud/web/exporters.ts
6332
+ // toPptx/toDocx). PDF and plain-text/Markdown deliverables flatten a
6333
+ // reference straight to caption text — telling the model to call
6334
+ // generate_image for THOSE just pays for an image nobody ever sees.
6335
+ 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.',
6214
6336
  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.',
6215
6337
  "- If you are not making meaningful progress, stop and escalate rather than looping or padding the response.",
6216
6338
  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.',
6339
+ 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.",
6217
6340
  "- Return structured output that directly addresses the expected output specification."
6218
6341
  ];
6219
6342
  return `You are a T3 Worker agent in the Cascade AI system. Your job is to execute a specific subtask completely and accurately.
@@ -6747,7 +6870,9 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
6747
6870
  const durationMs = Date.now() - toolStartMs;
6748
6871
  const errMsg = err instanceof Error ? err.message : String(err);
6749
6872
  this.emit("tool:result", { id: tc.id, tierId: this.id, toolName: tc.name, error: errMsg, durationMs });
6750
- if (/\b(429|rate.?limit|authentication|api.?key|forbidden|401|403)\b/i.test(errMsg)) {
6873
+ const isTaggedSystemic = err instanceof Error && err.systemic === true;
6874
+ const isProviderBacked = PROVIDER_BACKED_TOOLS.has(tc.name);
6875
+ if (isTaggedSystemic || isProviderBacked && classifyProviderError(err).systemic) {
6751
6876
  throw new CriticalToolError(errMsg, tc.name);
6752
6877
  }
6753
6878
  return await this.adaptiveFallback(tc, `Tool error: ${errMsg}`);
@@ -7035,8 +7160,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
7035
7160
  } catch {
7036
7161
  return {
7037
7162
  checksRun: ["completeness", "correctness", "compliance"],
7038
- passed: ["completeness", "correctness", "compliance"],
7039
- failed: []
7163
+ passed: [],
7164
+ failed: ["completeness"]
7040
7165
  };
7041
7166
  }
7042
7167
  }
@@ -7826,7 +7951,7 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
7826
7951
  const jsonMatch = /\[[\s\S]*\]/.exec(result.content);
7827
7952
  if (!jsonMatch) throw new Error("No JSON array found");
7828
7953
  const parsed = JSON.parse(jsonMatch[0]);
7829
- return parsed.map((a) => ({ ...a, sectionTitle: assignment.sectionTitle }));
7954
+ return parsed.map((a) => ({ ...a, sectionTitle: assignment.sectionTitle, constraints: a.constraints ?? [] }));
7830
7955
  } catch {
7831
7956
  return [{
7832
7957
  subtaskId: randomUUID(),
@@ -8784,6 +8909,12 @@ SPEC RULES \u2014 each subtask is a self-contained spec slice (workers execute f
8784
8909
  if (!plan.sections || !Array.isArray(plan.sections) || plan.sections.length === 0) {
8785
8910
  throw new Error("Invalid plan: no sections");
8786
8911
  }
8912
+ for (const section of plan.sections) {
8913
+ section.constraints ??= [];
8914
+ for (const subtask of section.t3Subtasks ?? []) {
8915
+ subtask.constraints ??= [];
8916
+ }
8917
+ }
8787
8918
  const [min, max] = COMPLEXITY_T2_COUNT[plan.complexity] ?? [1, 8];
8788
8919
  if (plan.sections.length < min) ;
8789
8920
  }
@@ -9099,17 +9230,17 @@ var CAPABILITIES = [
9099
9230
  api: "openai-images"
9100
9231
  },
9101
9232
  {
9102
- // Imagen, not `gemini-3-pro-image`. That id appears on Google's PRICING
9103
- // page but 404s on `:predict` ("not found for API version v1beta, or is not
9104
- // supported for predict") — the Gemini image models are called through
9105
- // generateContent with an image response modality, while `:predict` is the
9106
- // Imagen shape this executor implements. A billing line item is not proof
9107
- // of a callable endpoint, which is the assumption that put the wrong id
9108
- // here in the first place.
9233
+ // Was `imagen-4.0-generate-001` on `:predict`, which now 404s for new keys
9234
+ // ("no longer available to new users") — Imagen's :predict API is deprecated
9235
+ // and shuts down 2026-08-17. Google's own migration path is this model on
9236
+ // the ordinary generateContent endpoint, so the fix was never another Imagen
9237
+ // id: the whole request/response SHAPE changes, which is why `api` moved to
9238
+ // 'gemini-generate-content' rather than just the modelId moving.
9239
+ // https://ai.google.dev/gemini-api/docs/imagen
9109
9240
  modality: "image",
9110
9241
  provider: "gemini",
9111
- modelId: "imagen-4.0-generate-001",
9112
- api: "gemini-predict"
9242
+ modelId: "gemini-2.5-flash-image",
9243
+ api: "gemini-generate-content"
9113
9244
  },
9114
9245
  // ── Speech (text → audio) ──
9115
9246
  {
@@ -9164,6 +9295,14 @@ function capabilityCost(cap) {
9164
9295
  const shown = amount < 1e-3 ? amount.toExponential(2) : amount.toFixed(4);
9165
9296
  return { label: `$${shown} ${unit}`, amount, unit };
9166
9297
  }
9298
+ function explainChoice(c, cost, modality, isBest) {
9299
+ const priced = cost ? ` ${cost.label}.` : "";
9300
+ if (c.quality !== void 0) {
9301
+ 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}`;
9302
+ }
9303
+ const noRanking = ". No published quality ranking is applied for this modality, so this is a cost choice, not a quality one.";
9304
+ 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;
9305
+ }
9167
9306
  var MultimodalRegistry = class {
9168
9307
  /** Provider types the user has actually configured a key for. */
9169
9308
  available;
@@ -9181,22 +9320,20 @@ var MultimodalRegistry = class {
9181
9320
  return [...new Set(this.usable().map((c) => c.modality))];
9182
9321
  }
9183
9322
  /**
9184
- * Pick a model for a modality.
9323
+ * Every usable capability for a modality, best first.
9185
9324
  *
9186
9325
  * Ranked models beat unranked ones, then higher quality, then lower unit
9187
- * price. An explicit `preferModel` wins outright when it is usable — the user
9188
- * naming a model is a stronger signal than any prior.
9326
+ * price. An explicit `preferModel` is moved to the head when it is usable —
9327
+ * the user naming a model is a stronger signal than any prior.
9328
+ *
9329
+ * `select()` is just the head of this list. The TAIL is the point: a systemic
9330
+ * provider failure (dead key, 404, exhausted quota) is scoped to one
9331
+ * provider+model, so "this model cannot draw" is not the same fact as "this
9332
+ * account cannot draw". A caller holding the ordered alternatives can try the
9333
+ * next provider instead of aborting the whole run — see generate-media.ts.
9189
9334
  */
9190
- select(modality, opts = {}) {
9191
- const pool = this.usable(modality);
9192
- if (!pool.length) return null;
9193
- if (opts.preferModel) {
9194
- const wanted = pool.find((c) => c.modelId === opts.preferModel);
9195
- if (wanted) {
9196
- return { capability: wanted, cost: capabilityCost(wanted), reason: `Requested explicitly: ${wanted.modelId}.` };
9197
- }
9198
- }
9199
- const scored = pool.map((c) => ({ c, cost: capabilityCost(c) })).sort((a, b) => {
9335
+ rank(modality, opts = {}) {
9336
+ const scored = this.usable(modality).map((c) => ({ c, cost: capabilityCost(c) })).sort((a, b) => {
9200
9337
  const qa = a.c.quality ?? -1;
9201
9338
  const qb = b.c.quality ?? -1;
9202
9339
  if (qa !== qb) return qb - qa;
@@ -9204,10 +9341,25 @@ var MultimodalRegistry = class {
9204
9341
  const pb = b.cost?.amount ?? Number.POSITIVE_INFINITY;
9205
9342
  return pa - pb;
9206
9343
  });
9207
- const best = scored[0];
9208
- const ranked = best.c.quality !== void 0;
9209
- 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.";
9210
- return { capability: best.c, cost: best.cost, reason };
9344
+ const results = scored.map(({ c, cost }, i) => ({
9345
+ capability: c,
9346
+ cost,
9347
+ reason: explainChoice(c, cost, modality, i === 0)
9348
+ }));
9349
+ if (opts.preferModel) {
9350
+ const i = results.findIndex((r) => r.capability.modelId === opts.preferModel);
9351
+ if (i > -1) {
9352
+ const [wanted] = results.splice(i, 1);
9353
+ results.unshift({ ...wanted, reason: `Requested explicitly: ${wanted.capability.modelId}.` });
9354
+ }
9355
+ }
9356
+ return results;
9357
+ }
9358
+ /**
9359
+ * Pick a model for a modality — the best usable one, or null if there is none.
9360
+ */
9361
+ select(modality, opts = {}) {
9362
+ return this.rank(modality, opts)[0] ?? null;
9211
9363
  }
9212
9364
  /**
9213
9365
  * A plain-language account of what this account can and cannot generate —
@@ -9260,92 +9412,6 @@ var BaseTool = class {
9260
9412
  }
9261
9413
  };
9262
9414
 
9263
- // src/core/router/provider-errors.ts
9264
- var SYSTEMIC = /* @__PURE__ */ new Set([
9265
- "rate_limit",
9266
- "quota_exhausted",
9267
- "auth",
9268
- "model_unavailable"
9269
- ]);
9270
- function isSystemicKind(kind) {
9271
- return SYSTEMIC.has(kind);
9272
- }
9273
- function statusOf(err) {
9274
- if (!err || typeof err !== "object") return void 0;
9275
- const e = err;
9276
- for (const key of ["status", "statusCode", "code"]) {
9277
- const v = e[key];
9278
- if (typeof v === "number" && v >= 100 && v < 600) return v;
9279
- }
9280
- const resp = e["response"];
9281
- if (resp && typeof resp["status"] === "number") return resp["status"];
9282
- const inner = e["error"];
9283
- if (inner && typeof inner["code"] === "number") return inner["code"];
9284
- return void 0;
9285
- }
9286
- function messageOf(err) {
9287
- if (err instanceof Error) return err.message;
9288
- if (typeof err === "string") return err;
9289
- if (err && typeof err === "object") {
9290
- const e = err;
9291
- if (typeof e["message"] === "string") return e["message"];
9292
- const inner = e["error"];
9293
- if (inner && typeof inner["message"] === "string") return inner["message"];
9294
- try {
9295
- return JSON.stringify(err);
9296
- } catch {
9297
- }
9298
- }
9299
- return String(err);
9300
- }
9301
- function classifyProviderError(err) {
9302
- const raw = messageOf(err).trim();
9303
- const status = statusOf(err);
9304
- const m = raw.toLowerCase();
9305
- const kind = (() => {
9306
- if (status === 429) {
9307
- return /quota|billing|credit|exceeded your current quota|insufficient/.test(m) ? "quota_exhausted" : "rate_limit";
9308
- }
9309
- if (status === 401 || status === 403) return "auth";
9310
- if (status === 404) return "model_unavailable";
9311
- if (/rate limit|too many requests|resource_exhausted/.test(m)) {
9312
- return /quota|billing|credit|insufficient/.test(m) ? "quota_exhausted" : "rate_limit";
9313
- }
9314
- if (/quota|billing|insufficient_quota|insufficient credit|payment required/.test(m)) return "quota_exhausted";
9315
- if (/api key|unauthorized|authentication|permission denied|invalid[_ ]api/.test(m)) return "auth";
9316
- if (/model .*(not found|does not exist|unavailable)|no such model|unknown model|not supported for/.test(m)) {
9317
- return "model_unavailable";
9318
- }
9319
- if (/context length|too many tokens|maximum context|prompt is too long|input too long/.test(m)) return "context_length";
9320
- if (/safety|content filter|blocked|recitation|prohibited/.test(m)) return "content_filter";
9321
- if (/econnreset|etimedout|enotfound|socket hang up|network|fetch failed|aborted/.test(m)) return "network";
9322
- return "unknown";
9323
- })();
9324
- return { kind, systemic: isSystemicKind(kind), status, raw };
9325
- }
9326
- function describeProviderError(c, modelId) {
9327
- const on = modelId ? ` on ${modelId}` : "";
9328
- const detail = c.raw ? ` Provider said: ${c.raw}` : "";
9329
- switch (c.kind) {
9330
- case "rate_limit":
9331
- 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}`;
9332
- case "quota_exhausted":
9333
- 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}`;
9334
- case "auth":
9335
- 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}`;
9336
- case "model_unavailable":
9337
- 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}`;
9338
- case "context_length":
9339
- return `The prompt was too long${on}. Reduce the attached context, or use a model with a larger context window for this tier.${detail}`;
9340
- case "content_filter":
9341
- return `The provider's safety filter refused this request${on}.${detail}`;
9342
- case "network":
9343
- return `Network error reaching the provider${on}. This is usually transient.${detail}`;
9344
- default:
9345
- return `The model call failed${on}.${detail}`;
9346
- }
9347
- }
9348
-
9349
9415
  // src/core/multimodal/generate.ts
9350
9416
  var DEFAULT_TIMEOUT_MS = 12e4;
9351
9417
  async function callProvider(fn, modelId) {
@@ -9384,6 +9450,29 @@ function baseUrlFor(cfg, fallback) {
9384
9450
  function stamp(ext) {
9385
9451
  return `cascade-${Date.now()}.${ext}`;
9386
9452
  }
9453
+ var GEMINI_ASPECT_RATIOS = [
9454
+ ["1:1", 1 / 1],
9455
+ ["3:2", 3 / 2],
9456
+ ["2:3", 2 / 3],
9457
+ ["3:4", 3 / 4],
9458
+ ["4:3", 4 / 3],
9459
+ ["4:5", 4 / 5],
9460
+ ["5:4", 5 / 4],
9461
+ ["9:16", 9 / 16],
9462
+ ["16:9", 16 / 9],
9463
+ ["21:9", 21 / 9]
9464
+ ];
9465
+ function closestGeminiAspectRatio(size) {
9466
+ const m = size.match(/^(\d+)\s*[x×]\s*(\d+)$/i);
9467
+ if (!m) return void 0;
9468
+ const w = Number(m[1]);
9469
+ const h = Number(m[2]);
9470
+ if (!w || !h) return void 0;
9471
+ const ratio = w / h;
9472
+ return GEMINI_ASPECT_RATIOS.reduce(
9473
+ (best, cand) => Math.abs(cand[1] - ratio) < Math.abs(best[1] - ratio) ? cand : best
9474
+ )[0];
9475
+ }
9387
9476
  async function generateImage(cap, cfg, req, signal) {
9388
9477
  return callProvider(async () => {
9389
9478
  if (cap.api === "openai-images") {
@@ -9408,19 +9497,37 @@ async function generateImage(cap, cfg, req, signal) {
9408
9497
  ...first.revised_prompt ? { revisedPrompt: first.revised_prompt } : {}
9409
9498
  };
9410
9499
  }
9411
- if (cap.api === "gemini-predict") {
9500
+ if (cap.api === "gemini-generate-content") {
9412
9501
  const base = baseUrlFor(cfg, "https://generativelanguage.googleapis.com/v1beta");
9413
- const url = `${base}/models/${encodeURIComponent(cap.modelId)}:predict`;
9502
+ const url = `${base}/models/${encodeURIComponent(cap.modelId)}:generateContent`;
9503
+ const aspectRatio = req.size ? closestGeminiAspectRatio(req.size) : void 0;
9414
9504
  const res = await postJson(url, {
9415
- instances: [{ prompt: req.prompt }],
9416
- parameters: { sampleCount: 1 }
9505
+ contents: [{ parts: [{ text: req.prompt }] }],
9506
+ generationConfig: {
9507
+ // Asked for explicitly: these models can answer with text only, and a
9508
+ // text-only reply to "draw me a cat" is a failed image generation.
9509
+ responseModalities: ["IMAGE", "TEXT"],
9510
+ // Gemini has no free-form pixel size — only a named aspect ratio (see
9511
+ // closestGeminiAspectRatio). Omitted entirely when there's no
9512
+ // requested size, or no supported ratio close enough to bother with.
9513
+ ...aspectRatio ? { imageConfig: { aspectRatio } } : {}
9514
+ }
9417
9515
  }, { "x-goog-api-key": cfg.apiKey ?? "" }, signal);
9418
9516
  const body = await res.json();
9419
- const first = body.predictions?.[0];
9420
- if (!first?.bytesBase64Encoded) throw new Error("Image response contained no image data.");
9421
- const mimeType = first.mimeType ?? "image/png";
9517
+ const candidate = body.candidates?.[0];
9518
+ const parts = candidate?.content?.parts ?? [];
9519
+ const image = parts.find((p) => p.inlineData?.data)?.inlineData;
9520
+ if (!image?.data) {
9521
+ const blocked = body.promptFeedback?.blockReason ?? (candidate?.finishReason && candidate.finishReason !== "STOP" ? candidate.finishReason : void 0);
9522
+ if (blocked) throw new Error(`Image request was blocked by the provider (${blocked}).`);
9523
+ const said = parts.find((p) => p.text)?.text?.trim();
9524
+ throw new Error(
9525
+ "Image response contained no image data." + (said ? ` The model replied with text instead: ${said}` : "")
9526
+ );
9527
+ }
9528
+ const mimeType = image.mimeType ?? "image/png";
9422
9529
  return {
9423
- data: Buffer.from(first.bytesBase64Encoded, "base64"),
9530
+ data: Buffer.from(image.data, "base64"),
9424
9531
  mimeType,
9425
9532
  filename: stamp(mimeType.includes("jpeg") ? "jpg" : "png"),
9426
9533
  modelId: cap.modelId,
@@ -9573,6 +9680,49 @@ function resolve(deps, modality, preferModel) {
9573
9680
  }
9574
9681
  return { ok: true, selected, cfg };
9575
9682
  }
9683
+ function messageOf2(err) {
9684
+ return err instanceof Error ? err.message : String(err);
9685
+ }
9686
+ async function runWithProviderFallback(deps, modality, first, run, signal) {
9687
+ const failed = first.selected.capability;
9688
+ try {
9689
+ return { value: await run(failed, first.cfg), selected: first.selected };
9690
+ } catch (err) {
9691
+ if (signal?.aborted || !classifyProviderError(err).systemic) throw err;
9692
+ const tried = [`${failed.modelId} (${failed.provider}): ${messageOf2(err)}`];
9693
+ let allSystemic = true;
9694
+ let lastNonSystemic;
9695
+ const seen = /* @__PURE__ */ new Set([failed.provider]);
9696
+ for (const alt of deps.registry.rank(modality)) {
9697
+ if (seen.has(alt.capability.provider)) continue;
9698
+ const cfg = deps.lookupProvider(alt.capability.provider);
9699
+ if (!cfg) continue;
9700
+ seen.add(alt.capability.provider);
9701
+ try {
9702
+ return {
9703
+ value: await run(alt.capability, cfg),
9704
+ selected: alt,
9705
+ fellBackFrom: { capability: failed, error: messageOf2(err) }
9706
+ };
9707
+ } catch (retryErr) {
9708
+ if (signal?.aborted) throw retryErr;
9709
+ tried.push(`${alt.capability.modelId} (${alt.capability.provider}): ${messageOf2(retryErr)}`);
9710
+ if (classifyProviderError(retryErr).systemic) {
9711
+ continue;
9712
+ }
9713
+ allSystemic = false;
9714
+ lastNonSystemic = retryErr;
9715
+ }
9716
+ }
9717
+ if (tried.length === 1) throw err;
9718
+ if (!allSystemic) throw lastNonSystemic;
9719
+ const exhausted = new Error(
9720
+ `${modality} generation failed on every configured provider (${seen.size} tried). ${tried.join(" ")}`
9721
+ );
9722
+ Object.assign(exhausted, { systemic: true });
9723
+ throw exhausted;
9724
+ }
9725
+ }
9576
9726
  var GenerateImageTool = class extends BaseTool {
9577
9727
  constructor(deps) {
9578
9728
  super();
@@ -9596,16 +9746,32 @@ var GenerateImageTool = class extends BaseTool {
9596
9746
  const r = resolve(this.deps, "image", input["model"] ? String(input["model"]) : void 0);
9597
9747
  if (!r.ok) return r.error;
9598
9748
  const size = input["size"] ? String(input["size"]) : void 0;
9599
- const asset = await generateImage(
9600
- r.selected.capability,
9601
- r.cfg,
9602
- { prompt, ...size ? { size } : {} },
9749
+ const attempt = await runWithProviderFallback(
9750
+ this.deps,
9751
+ "image",
9752
+ r,
9753
+ (cap, cfg) => generateImage(cap, cfg, { prompt, ...size ? { size } : {} }, options.signal),
9603
9754
  options.signal
9604
9755
  );
9756
+ const asset = attempt.value;
9757
+ const fell = attempt.fellBackFrom;
9605
9758
  const location = await this.deps.sink(asset);
9606
9759
  return [
9607
9760
  `Image generated and saved to ${location}.`,
9608
- `Model: ${asset.modelId} \u2014 ${r.selected.reason}`,
9761
+ // Knowing WHERE the image is isn't the same as knowing how to use it: a
9762
+ // model told only "saved to X" tends to describe the picture in prose
9763
+ // instead of embedding it. Spelling out the reference syntax here makes
9764
+ // the worker's "never write a placeholder" rule concretely actionable,
9765
+ // and it works for every host because `location` is whatever that host's
9766
+ // sink returned — a workspace path on desktop/CLI, a URL in the cloud.
9767
+ `To include this image in your output, reference it as: ![description](${location})`,
9768
+ // The reported model has to be the one that actually drew the picture, not
9769
+ // the one that was picked and then failed.
9770
+ `Model: ${asset.modelId} \u2014 ${fell ? `used as a fallback after ${fell.capability.modelId} (${fell.capability.provider}) failed with a systemic error.` : attempt.selected.reason}`,
9771
+ // Worth one line: the run succeeded, so nothing else will report that a
9772
+ // configured provider is broken, and "your Gemini key is dead" is exactly
9773
+ // the kind of thing the user wants to hear once rather than never.
9774
+ fell ? `Note: ${fell.capability.provider} failed first \u2014 ${fell.error}` : "",
9609
9775
  // DALL·E rewrites the prompt before drawing; the model needs to know what
9610
9776
  // was actually rendered, not what it asked for, or its description of the
9611
9777
  // result will not match the image.
@@ -9848,6 +10014,87 @@ function mcpToolName(serverName, toolName) {
9848
10014
  function isMcpToolName(name) {
9849
10015
  return name.startsWith(MCP_TOOL_PREFIX);
9850
10016
  }
10017
+ var READ_ONLY_MCP_VERBS = ["list", "get", "search", "read", "describe", "find", "query", "fetch", "show", "view"];
10018
+ var MUTATING_MCP_TOKENS = /* @__PURE__ */ new Set([
10019
+ "create",
10020
+ "delete",
10021
+ "remove",
10022
+ "update",
10023
+ "write",
10024
+ "modify",
10025
+ "set",
10026
+ "put",
10027
+ "post",
10028
+ "patch",
10029
+ "push",
10030
+ "merge",
10031
+ "send",
10032
+ "execute",
10033
+ "exec",
10034
+ "run",
10035
+ "deploy",
10036
+ "install",
10037
+ "uninstall",
10038
+ "upload",
10039
+ "publish",
10040
+ "revoke",
10041
+ "grant",
10042
+ "approve",
10043
+ "reject",
10044
+ "cancel",
10045
+ "enable",
10046
+ "disable",
10047
+ "add",
10048
+ "insert",
10049
+ "drop",
10050
+ "truncate",
10051
+ "clear",
10052
+ "reset",
10053
+ "restart",
10054
+ "stop",
10055
+ "kill",
10056
+ "terminate",
10057
+ "rename",
10058
+ "move",
10059
+ "fork",
10060
+ "sync",
10061
+ "replace",
10062
+ "overwrite",
10063
+ "invite",
10064
+ "ban",
10065
+ "block",
10066
+ "assign",
10067
+ "lock",
10068
+ "unlock",
10069
+ "archive",
10070
+ "star",
10071
+ "comment",
10072
+ "reply",
10073
+ "submit",
10074
+ "apply",
10075
+ "trigger",
10076
+ "invoke",
10077
+ "purge",
10078
+ "migrate",
10079
+ "edit",
10080
+ "save",
10081
+ "commit"
10082
+ ]);
10083
+ function mcpTokens(rawToolName) {
10084
+ return rawToolName.split(/[_-]+/).flatMap((segment) => segment.split(/(?<=[a-z0-9])(?=[A-Z])/)).map((t) => t.toLowerCase()).filter(Boolean);
10085
+ }
10086
+ function isReadOnlyMcpToolName(rawToolName) {
10087
+ if (mcpTokens(rawToolName).some((t) => MUTATING_MCP_TOKENS.has(t))) return false;
10088
+ const lower = rawToolName.toLowerCase();
10089
+ for (const verb of READ_ONLY_MCP_VERBS) {
10090
+ if (!lower.startsWith(verb)) continue;
10091
+ const boundary = rawToolName[verb.length];
10092
+ if (boundary === void 0 || boundary === "_" || boundary === "-" || boundary >= "A" && boundary <= "Z") {
10093
+ return true;
10094
+ }
10095
+ }
10096
+ return false;
10097
+ }
9851
10098
  function uniqueMcpServerName(desired, existingNames) {
9852
10099
  const taken = new Set(existingNames.map(mcpServerPrefix));
9853
10100
  if (!taken.has(mcpServerPrefix(desired))) return desired;
@@ -10941,9 +11188,11 @@ var WebSearchTool = class extends BaseTool {
10941
11188
  );
10942
11189
  const errors = [];
10943
11190
  let results = [];
11191
+ let anyBackendReachable = false;
10944
11192
  if (this.config.searxngUrl) {
10945
11193
  try {
10946
11194
  results = await searchSearXNG(query, this.config.searxngUrl, maxResults);
11195
+ anyBackendReachable = true;
10947
11196
  if (results.length > 0) return this.formatResults(query, results);
10948
11197
  errors.push("SearXNG: returned 0 results");
10949
11198
  } catch (err) {
@@ -10953,6 +11202,7 @@ var WebSearchTool = class extends BaseTool {
10953
11202
  if (this.config.braveApiKey) {
10954
11203
  try {
10955
11204
  results = await searchBrave(query, this.config.braveApiKey, maxResults);
11205
+ anyBackendReachable = true;
10956
11206
  if (results.length > 0) return this.formatResults(query, results);
10957
11207
  errors.push("Brave: returned 0 results");
10958
11208
  } catch (err) {
@@ -10962,6 +11212,7 @@ var WebSearchTool = class extends BaseTool {
10962
11212
  if (this.config.tavilyApiKey) {
10963
11213
  try {
10964
11214
  results = await searchTavily(query, this.config.tavilyApiKey, maxResults);
11215
+ anyBackendReachable = true;
10965
11216
  if (results.length > 0) return this.formatResults(query, results);
10966
11217
  errors.push("Tavily: returned 0 results");
10967
11218
  } catch (err) {
@@ -10971,6 +11222,7 @@ var WebSearchTool = class extends BaseTool {
10971
11222
  for (const variant of ["html", "lite"]) {
10972
11223
  try {
10973
11224
  results = await searchDuckDuckGo(query, maxResults, variant);
11225
+ anyBackendReachable = true;
10974
11226
  if (results.length > 0) return this.formatResults(query, results);
10975
11227
  errors.push(`DuckDuckGo ${variant}: returned 0 results`);
10976
11228
  } catch (err) {
@@ -10978,11 +11230,11 @@ var WebSearchTool = class extends BaseTool {
10978
11230
  }
10979
11231
  }
10980
11232
  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" : "";
10981
- return [
10982
- `Web search for "${query}" failed across all backends:`,
11233
+ throw Object.assign(new Error([
11234
+ `Web search for "${query}" found nothing across all backends:`,
10983
11235
  ...errors.map((e) => ` \u2022 ${e}`),
10984
11236
  configHint
10985
- ].join("\n");
11237
+ ].join("\n")), { systemic: !anyBackendReachable });
10986
11238
  }
10987
11239
  formatResults(query, results) {
10988
11240
  const lines = [`Web search results for: "${query}"`, ""];
@@ -11350,6 +11602,15 @@ var McpToolWrapper = class extends BaseTool {
11350
11602
  this.description = `[MCP:${serverName}] ${description}`;
11351
11603
  this.inputSchema = inputSchema;
11352
11604
  }
11605
+ // Every built-in tool of comparable risk (shell, git, file writes, the
11606
+ // built-in github tool) overrides this to true. An MCP tool never did —
11607
+ // BaseTool's default (false) meant a connected server's create/delete/push
11608
+ // actions ran with zero human approval, whatever they did. MCP carries no
11609
+ // standard "this mutates state" flag, so default to dangerous and name the
11610
+ // exception (a read-only-looking verb) rather than the other way around.
11611
+ isDangerous() {
11612
+ return !isReadOnlyMcpToolName(this.toolName);
11613
+ }
11353
11614
  async execute(input, _options) {
11354
11615
  return this.mcpClient.callTool(this.serverName, this.toolName, input);
11355
11616
  }
@@ -11462,7 +11723,7 @@ var ToolRegistry = class extends EventEmitter {
11462
11723
  requiresApproval(toolName) {
11463
11724
  const defaults = DEFAULT_APPROVAL_REQUIRED;
11464
11725
  const configured = this.config.requireApprovalFor;
11465
- return defaults.includes(toolName) || configured.includes(toolName);
11726
+ return defaults.includes(toolName) || configured.includes(toolName) || this.isDangerous(toolName);
11466
11727
  }
11467
11728
  isDangerous(toolName) {
11468
11729
  return this.tools.get(toolName)?.isDangerous() ?? false;
@@ -14830,9 +15091,8 @@ var Cascade = class _Cascade extends EventEmitter {
14830
15091
  });
14831
15092
  this.toolRegistry = new ToolRegistry(this.config.tools, workspacePath);
14832
15093
  try {
14833
- const nodePath = __require("path");
14834
15094
  this.router.setDeadModelStore(new DeadModelStore(
14835
- fileDeadModelPersistence(nodePath.join(workspacePath, ".cascade", "dead-models.json"))
15095
+ fileDeadModelPersistence(path24.join(workspacePath, ".cascade", "dead-models.json"))
14836
15096
  ));
14837
15097
  } catch {
14838
15098
  }