cascade-ai 0.52.0 → 0.54.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,6 +104,12 @@ 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
+ });
107
113
  var __esm = (fn, res) => function __init() {
108
114
  return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
109
115
  };
@@ -1793,7 +1799,7 @@ var CASCADE_VERSION, CASCADE_CONFIG_FILE, CASCADE_DB_FILE, CASCADE_DASHBOARD_SEC
1793
1799
  var init_constants = __esm({
1794
1800
  "src/constants.ts"() {
1795
1801
  init_pricing();
1796
- CASCADE_VERSION = "0.52.0";
1802
+ CASCADE_VERSION = "0.54.0";
1797
1803
  CASCADE_CONFIG_FILE = ".cascade/config.json";
1798
1804
  CASCADE_DB_FILE = ".cascade/memory.db";
1799
1805
  CASCADE_DASHBOARD_SECRET_FILE = ".cascade/dashboard-secret";
@@ -2792,26 +2798,14 @@ var init_openai = __esm({
2792
2798
  // src/providers/azure.ts
2793
2799
  var azure_exports = {};
2794
2800
  __export(azure_exports, {
2801
+ AZURE_BASE_MODELS: () => AZURE_BASE_MODELS,
2795
2802
  AzureOpenAIProvider: () => AzureOpenAIProvider,
2796
2803
  azureModelForDeployment: () => azureModelForDeployment,
2797
2804
  inferAzureBaseModel: () => inferAzureBaseModel
2798
2805
  });
2799
2806
  function inferAzureBaseModel(deploymentName) {
2800
2807
  const n = deploymentName.toLowerCase();
2801
- const rules = [
2802
- [/gpt-?5\.5/, "gpt-5.5"],
2803
- [/gpt-?5\.4.*mini/, "gpt-5.4-mini"],
2804
- [/gpt-?5\.4/, "gpt-5.4"],
2805
- [/gpt-?5.*nano/, "gpt-5-nano"],
2806
- [/gpt-?5.*mini/, "gpt-5-mini"],
2807
- [/gpt-?5/, "gpt-5"],
2808
- [/gpt-?4\.1-nano/, "gpt-4.1-nano"],
2809
- [/gpt-?4\.1-mini/, "gpt-4.1-mini"],
2810
- [/gpt-?4\.1/, "gpt-4.1"],
2811
- [/gpt-?4o-mini/, "gpt-4o-mini"],
2812
- [/gpt-?4o/, "gpt-4o"]
2813
- ];
2814
- for (const [re, base] of rules) if (re.test(n)) return base;
2808
+ for (const [re, base] of AZURE_BASE_MODEL_RULES) if (re.test(n)) return base;
2815
2809
  return null;
2816
2810
  }
2817
2811
  function azureModelForDeployment(cfg) {
@@ -2856,13 +2850,27 @@ function azureModelForDeployment(cfg) {
2856
2850
  supportsToolUse: true
2857
2851
  };
2858
2852
  }
2859
- var DEFAULT_AZURE_API_VERSION, AzureOpenAIProvider;
2853
+ var DEFAULT_AZURE_API_VERSION, AZURE_BASE_MODEL_RULES, AZURE_BASE_MODELS, AzureOpenAIProvider;
2860
2854
  var init_azure = __esm({
2861
2855
  "src/providers/azure.ts"() {
2862
2856
  init_constants();
2863
2857
  init_openai();
2864
2858
  init_pricing();
2865
2859
  DEFAULT_AZURE_API_VERSION = "2024-12-01-preview";
2860
+ AZURE_BASE_MODEL_RULES = [
2861
+ [/gpt-?5\.5/, "gpt-5.5"],
2862
+ [/gpt-?5\.4.*mini/, "gpt-5.4-mini"],
2863
+ [/gpt-?5\.4/, "gpt-5.4"],
2864
+ [/gpt-?5.*nano/, "gpt-5-nano"],
2865
+ [/gpt-?5.*mini/, "gpt-5-mini"],
2866
+ [/gpt-?5/, "gpt-5"],
2867
+ [/gpt-?4\.1-nano/, "gpt-4.1-nano"],
2868
+ [/gpt-?4\.1-mini/, "gpt-4.1-mini"],
2869
+ [/gpt-?4\.1/, "gpt-4.1"],
2870
+ [/gpt-?4o-mini/, "gpt-4o-mini"],
2871
+ [/gpt-?4o/, "gpt-4o"]
2872
+ ];
2873
+ AZURE_BASE_MODELS = AZURE_BASE_MODEL_RULES.map(([, id]) => id);
2866
2874
  AzureOpenAIProvider = class extends OpenAIProvider {
2867
2875
  constructor(config, model) {
2868
2876
  const rawUrl = config.baseUrl ?? AZURE_BASE_URL_TEMPLATE.replace("{resource}", "YOUR_RESOURCE");
@@ -5563,6 +5571,26 @@ var ConfigManager = class {
5563
5571
  }
5564
5572
  };
5565
5573
 
5574
+ // src/tools/tool-name.ts
5575
+ var MCP_TOOL_PREFIX = "mcp__";
5576
+ function sanitizeToolNameSegment(segment) {
5577
+ const cleaned = segment.replace(/[^a-zA-Z0-9-]+/g, "_");
5578
+ let start = 0;
5579
+ let end = cleaned.length;
5580
+ while (start < end && cleaned[start] === "_") start++;
5581
+ while (end > start && cleaned[end - 1] === "_") end--;
5582
+ return cleaned.slice(start, end) || "_";
5583
+ }
5584
+ function mcpToolName(serverName, toolName) {
5585
+ return `${MCP_TOOL_PREFIX}${sanitizeToolNameSegment(serverName)}__${sanitizeToolNameSegment(toolName)}`;
5586
+ }
5587
+ function isMcpToolName(name) {
5588
+ return name.startsWith(MCP_TOOL_PREFIX);
5589
+ }
5590
+ function mcpServerPrefix(serverName) {
5591
+ return `${MCP_TOOL_PREFIX}${sanitizeToolNameSegment(serverName)}__`;
5592
+ }
5593
+
5566
5594
  // src/cli/utils/terminal-input.ts
5567
5595
  var PASTE_START = "\x1B[200~";
5568
5596
  var PASTE_END = "\x1B[201~";
@@ -5887,7 +5915,21 @@ var ModelSelector = class {
5887
5915
  this.availableProviders = availableProviders;
5888
5916
  this.availableModels = new Map(Object.entries(MODELS));
5889
5917
  }
5918
+ /**
5919
+ * Verdicts from earlier runs about ids that 404 on this account. Consulted
5920
+ * BEFORE a model enters the pool, which is the only placement that helps: a
5921
+ * T3 wave runs concurrently, so filtering after the first failure still lets
5922
+ * every other worker in the wave call the dead id.
5923
+ */
5924
+ deadModels;
5925
+ setDeadModelStore(store) {
5926
+ this.deadModels = store;
5927
+ for (const [id, m] of [...this.availableModels]) {
5928
+ if (this.deadModels.isDead(m.provider, m.id)) this.availableModels.delete(id);
5929
+ }
5930
+ }
5890
5931
  addDynamicModel(model) {
5932
+ if (this.deadModels?.isDead(model.provider, model.id)) return;
5891
5933
  this.availableModels.set(model.id, model);
5892
5934
  if (!(model.id in MODELS)) this.discovered.add(normalizeModelId(model.id));
5893
5935
  }
@@ -6294,6 +6336,121 @@ var LocalRequestQueue = class {
6294
6336
  }
6295
6337
  };
6296
6338
 
6339
+ // src/core/router/dead-models.ts
6340
+ var DEAD_MODEL_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
6341
+ function deadModelKey(provider, modelId) {
6342
+ return `${provider}:${modelId}`;
6343
+ }
6344
+ var DeadModelStore = class {
6345
+ constructor(persistence, opts = {}) {
6346
+ this.persistence = persistence;
6347
+ this.ttlMs = opts.ttlMs ?? DEAD_MODEL_TTL_MS;
6348
+ this.now = opts.now ?? (() => Date.now());
6349
+ this.restore();
6350
+ }
6351
+ persistence;
6352
+ records = /* @__PURE__ */ new Map();
6353
+ ttlMs;
6354
+ now;
6355
+ restore() {
6356
+ if (!this.persistence) return;
6357
+ try {
6358
+ for (const r of this.persistence.load()) {
6359
+ if (!this.isExpired(r)) this.records.set(r.key, r);
6360
+ }
6361
+ } catch {
6362
+ }
6363
+ }
6364
+ isExpired(r) {
6365
+ return this.now() - r.lastSeenAt > this.ttlMs;
6366
+ }
6367
+ flush() {
6368
+ if (!this.persistence) return;
6369
+ try {
6370
+ this.persistence.save([...this.records.values()]);
6371
+ } catch {
6372
+ }
6373
+ }
6374
+ /**
6375
+ * Record that a model was found dead. Returns true when this is the first
6376
+ * time (so the caller can log it once rather than once per concurrent
6377
+ * worker in the wave that discovered it).
6378
+ */
6379
+ record(provider, modelId, reason) {
6380
+ const key = deadModelKey(provider, modelId);
6381
+ const now = this.now();
6382
+ const existing = this.records.get(key);
6383
+ if (existing && !this.isExpired(existing)) {
6384
+ existing.lastSeenAt = now;
6385
+ existing.hits += 1;
6386
+ this.flush();
6387
+ return false;
6388
+ }
6389
+ this.records.set(key, {
6390
+ key,
6391
+ provider,
6392
+ modelId,
6393
+ reason,
6394
+ firstSeenAt: now,
6395
+ lastSeenAt: now,
6396
+ hits: 1
6397
+ });
6398
+ this.flush();
6399
+ return true;
6400
+ }
6401
+ /** True when this model is known dead and the verdict has not expired. */
6402
+ isDead(provider, modelId) {
6403
+ const r = this.records.get(deadModelKey(provider, modelId));
6404
+ if (!r) return false;
6405
+ if (this.isExpired(r)) {
6406
+ this.records.delete(r.key);
6407
+ this.flush();
6408
+ return false;
6409
+ }
6410
+ return true;
6411
+ }
6412
+ /** Why a model is considered dead, for /why and the models command. */
6413
+ reasonFor(provider, modelId) {
6414
+ const r = this.records.get(deadModelKey(provider, modelId));
6415
+ return r && !this.isExpired(r) ? r.reason : null;
6416
+ }
6417
+ /** Live records, expired ones filtered out. */
6418
+ list() {
6419
+ const live = [...this.records.values()].filter((r) => !this.isExpired(r));
6420
+ return live.sort((a, b) => b.lastSeenAt - a.lastSeenAt);
6421
+ }
6422
+ /**
6423
+ * Forget a verdict — used when the user fixes the underlying problem (adds
6424
+ * the model to their project, gets quota) and wants it retried now rather
6425
+ * than after the TTL.
6426
+ */
6427
+ forget(provider, modelId) {
6428
+ const removed = this.records.delete(deadModelKey(provider, modelId));
6429
+ if (removed) this.flush();
6430
+ return removed;
6431
+ }
6432
+ clear() {
6433
+ this.records.clear();
6434
+ this.flush();
6435
+ }
6436
+ };
6437
+ function fileDeadModelPersistence(filePath) {
6438
+ return {
6439
+ load() {
6440
+ const fs32 = __require("fs");
6441
+ if (!fs32.existsSync(filePath)) return [];
6442
+ const raw = JSON.parse(fs32.readFileSync(filePath, "utf-8"));
6443
+ return Array.isArray(raw) ? raw : [];
6444
+ },
6445
+ save(records) {
6446
+ const fs32 = __require("fs");
6447
+ const path37 = __require("path");
6448
+ fs32.mkdirSync(path37.dirname(filePath), { recursive: true });
6449
+ fs32.writeFileSync(filePath, JSON.stringify(records, null, 2));
6450
+ }
6451
+ };
6452
+ }
6453
+
6297
6454
  // src/core/router/index.ts
6298
6455
  init_constants();
6299
6456
  init_cost();
@@ -6883,6 +7040,26 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
6883
7040
  localQueue;
6884
7041
  taskAnalyzer;
6885
7042
  pendingFeedbackSource;
7043
+ /** Models known dead for this account (404 / retired / not enabled). */
7044
+ deadModels = new DeadModelStore();
7045
+ /**
7046
+ * Back the dead-model memory with durable storage (a JSON file on desktop, a
7047
+ * per-user table in the cloud). Without one the verdicts still work, but only
7048
+ * for the life of the process — so tomorrow's run pays the same wave of 404s
7049
+ * to learn the same fact.
7050
+ */
7051
+ setDeadModelStore(store) {
7052
+ this.deadModels = store;
7053
+ this.selector.setDeadModelStore(store);
7054
+ }
7055
+ /** Models known dead for this account, for `cascade models` and /why. */
7056
+ listDeadModels() {
7057
+ return this.deadModels.list();
7058
+ }
7059
+ /** Retry a model the user has since fixed, without waiting out the TTL. */
7060
+ forgetDeadModel(provider, modelId) {
7061
+ return this.deadModels.forget(provider, modelId);
7062
+ }
6886
7063
  worldStateDB;
6887
7064
  privacyPaths;
6888
7065
  guidanceQueue;
@@ -7247,6 +7424,8 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
7247
7424
  }
7248
7425
  if (isModelNotFoundError(errMsg)) {
7249
7426
  this.selector.removeModel(model.id);
7427
+ const firstSighting = this.deadModels.record(model.provider, model.id, errMsg);
7428
+ if (firstSighting) this.emit("model:dead", { provider: model.provider, modelId: model.id, reason: errMsg });
7250
7429
  const depth = (options._notFoundDepth ?? 0) + 1;
7251
7430
  const next = depth <= 3 ? this.selector.selectForTier(tier) : null;
7252
7431
  if (next && next.id !== model.id) {
@@ -11050,9 +11229,16 @@ var CAPABILITIES = [
11050
11229
  api: "openai-images"
11051
11230
  },
11052
11231
  {
11232
+ // Imagen, not `gemini-3-pro-image`. That id appears on Google's PRICING
11233
+ // page but 404s on `:predict` ("not found for API version v1beta, or is not
11234
+ // supported for predict") — the Gemini image models are called through
11235
+ // generateContent with an image response modality, while `:predict` is the
11236
+ // Imagen shape this executor implements. A billing line item is not proof
11237
+ // of a callable endpoint, which is the assumption that put the wrong id
11238
+ // here in the first place.
11053
11239
  modality: "image",
11054
11240
  provider: "gemini",
11055
- modelId: "gemini-3-pro-image",
11241
+ modelId: "imagen-4.0-generate-001",
11056
11242
  api: "gemini-predict"
11057
11243
  },
11058
11244
  // ── Speech (text → audio) ──
@@ -13197,7 +13383,7 @@ var McpToolWrapper = class extends BaseTool {
13197
13383
  this.mcpClient = mcpClient;
13198
13384
  this.serverName = serverName;
13199
13385
  this.toolName = toolName;
13200
- this.name = `mcp::${serverName}::${toolName}`;
13386
+ this.name = mcpToolName(serverName, toolName);
13201
13387
  this.description = `[MCP:${serverName}] ${description}`;
13202
13388
  this.inputSchema = inputSchema;
13203
13389
  }
@@ -16155,6 +16341,13 @@ var Cascade = class _Cascade extends EventEmitter__default.default {
16155
16341
  }
16156
16342
  });
16157
16343
  this.toolRegistry = new ToolRegistry(this.config.tools, workspacePath);
16344
+ try {
16345
+ const nodePath = __require("path");
16346
+ this.router.setDeadModelStore(new DeadModelStore(
16347
+ fileDeadModelPersistence(nodePath.join(workspacePath, ".cascade", "dead-models.json"))
16348
+ ));
16349
+ } catch {
16350
+ }
16158
16351
  this.multimodal = new MultimodalRegistry(
16159
16352
  (this.config.providers ?? []).map((p) => p.type)
16160
16353
  );
@@ -19558,13 +19751,13 @@ ${msg.content}`).join("\n\n");
19558
19751
  const cascade = cascadeRef.current;
19559
19752
  if (!cascade) return "Cascade not initialized.";
19560
19753
  const toolRegistry = cascade.getToolRegistry();
19561
- const tools = toolRegistry.getToolDefinitions().filter((t) => t.name.startsWith("mcp::"));
19754
+ const tools = toolRegistry.getToolDefinitions().filter((t) => isMcpToolName(t.name));
19562
19755
  if (tools.length === 0) return "No MCP tools connected.";
19563
19756
  const servers = /* @__PURE__ */ new Set();
19564
19757
  tools.forEach((t) => servers.add(t.name.split("::")[1]));
19565
19758
  const lines = [`Connected MCP Servers (${servers.size}):`];
19566
19759
  for (const server of servers) {
19567
- const serverTools = tools.filter((t) => t.name.startsWith(`mcp::${server}::`));
19760
+ const serverTools = tools.filter((t) => t.name.startsWith(mcpServerPrefix(server)));
19568
19761
  lines.push(`
19569
19762
  \u25CF ${server} (${serverTools.length} tools)`);
19570
19763
  serverTools.forEach((t) => lines.push(` - ${t.name.split("::")[2]} \u2014 ${t.description.replace(`[MCP:${server}] `, "")}`));