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.js CHANGED
@@ -51,6 +51,12 @@ import cron from 'node-cron';
51
51
  // Cascade AI — Multi-tier AI Orchestration System
52
52
  var __defProp = Object.defineProperty;
53
53
  var __getOwnPropNames = Object.getOwnPropertyNames;
54
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
55
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
56
+ }) : x)(function(x) {
57
+ if (typeof require !== "undefined") return require.apply(this, arguments);
58
+ throw Error('Dynamic require of "' + x + '" is not supported');
59
+ });
54
60
  var __esm = (fn, res) => function __init() {
55
61
  return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
56
62
  };
@@ -1740,7 +1746,7 @@ var CASCADE_VERSION, CASCADE_CONFIG_FILE, CASCADE_DB_FILE, CASCADE_DASHBOARD_SEC
1740
1746
  var init_constants = __esm({
1741
1747
  "src/constants.ts"() {
1742
1748
  init_pricing();
1743
- CASCADE_VERSION = "0.52.0";
1749
+ CASCADE_VERSION = "0.54.0";
1744
1750
  CASCADE_CONFIG_FILE = ".cascade/config.json";
1745
1751
  CASCADE_DB_FILE = ".cascade/memory.db";
1746
1752
  CASCADE_DASHBOARD_SECRET_FILE = ".cascade/dashboard-secret";
@@ -2739,26 +2745,14 @@ var init_openai = __esm({
2739
2745
  // src/providers/azure.ts
2740
2746
  var azure_exports = {};
2741
2747
  __export(azure_exports, {
2748
+ AZURE_BASE_MODELS: () => AZURE_BASE_MODELS,
2742
2749
  AzureOpenAIProvider: () => AzureOpenAIProvider,
2743
2750
  azureModelForDeployment: () => azureModelForDeployment,
2744
2751
  inferAzureBaseModel: () => inferAzureBaseModel
2745
2752
  });
2746
2753
  function inferAzureBaseModel(deploymentName) {
2747
2754
  const n = deploymentName.toLowerCase();
2748
- const rules = [
2749
- [/gpt-?5\.5/, "gpt-5.5"],
2750
- [/gpt-?5\.4.*mini/, "gpt-5.4-mini"],
2751
- [/gpt-?5\.4/, "gpt-5.4"],
2752
- [/gpt-?5.*nano/, "gpt-5-nano"],
2753
- [/gpt-?5.*mini/, "gpt-5-mini"],
2754
- [/gpt-?5/, "gpt-5"],
2755
- [/gpt-?4\.1-nano/, "gpt-4.1-nano"],
2756
- [/gpt-?4\.1-mini/, "gpt-4.1-mini"],
2757
- [/gpt-?4\.1/, "gpt-4.1"],
2758
- [/gpt-?4o-mini/, "gpt-4o-mini"],
2759
- [/gpt-?4o/, "gpt-4o"]
2760
- ];
2761
- for (const [re, base] of rules) if (re.test(n)) return base;
2755
+ for (const [re, base] of AZURE_BASE_MODEL_RULES) if (re.test(n)) return base;
2762
2756
  return null;
2763
2757
  }
2764
2758
  function azureModelForDeployment(cfg) {
@@ -2803,13 +2797,27 @@ function azureModelForDeployment(cfg) {
2803
2797
  supportsToolUse: true
2804
2798
  };
2805
2799
  }
2806
- var DEFAULT_AZURE_API_VERSION, AzureOpenAIProvider;
2800
+ var DEFAULT_AZURE_API_VERSION, AZURE_BASE_MODEL_RULES, AZURE_BASE_MODELS, AzureOpenAIProvider;
2807
2801
  var init_azure = __esm({
2808
2802
  "src/providers/azure.ts"() {
2809
2803
  init_constants();
2810
2804
  init_openai();
2811
2805
  init_pricing();
2812
2806
  DEFAULT_AZURE_API_VERSION = "2024-12-01-preview";
2807
+ AZURE_BASE_MODEL_RULES = [
2808
+ [/gpt-?5\.5/, "gpt-5.5"],
2809
+ [/gpt-?5\.4.*mini/, "gpt-5.4-mini"],
2810
+ [/gpt-?5\.4/, "gpt-5.4"],
2811
+ [/gpt-?5.*nano/, "gpt-5-nano"],
2812
+ [/gpt-?5.*mini/, "gpt-5-mini"],
2813
+ [/gpt-?5/, "gpt-5"],
2814
+ [/gpt-?4\.1-nano/, "gpt-4.1-nano"],
2815
+ [/gpt-?4\.1-mini/, "gpt-4.1-mini"],
2816
+ [/gpt-?4\.1/, "gpt-4.1"],
2817
+ [/gpt-?4o-mini/, "gpt-4o-mini"],
2818
+ [/gpt-?4o/, "gpt-4o"]
2819
+ ];
2820
+ AZURE_BASE_MODELS = AZURE_BASE_MODEL_RULES.map(([, id]) => id);
2813
2821
  AzureOpenAIProvider = class extends OpenAIProvider {
2814
2822
  constructor(config, model) {
2815
2823
  const rawUrl = config.baseUrl ?? AZURE_BASE_URL_TEMPLATE.replace("{resource}", "YOUR_RESOURCE");
@@ -5510,6 +5518,26 @@ var ConfigManager = class {
5510
5518
  }
5511
5519
  };
5512
5520
 
5521
+ // src/tools/tool-name.ts
5522
+ var MCP_TOOL_PREFIX = "mcp__";
5523
+ function sanitizeToolNameSegment(segment) {
5524
+ const cleaned = segment.replace(/[^a-zA-Z0-9-]+/g, "_");
5525
+ let start = 0;
5526
+ let end = cleaned.length;
5527
+ while (start < end && cleaned[start] === "_") start++;
5528
+ while (end > start && cleaned[end - 1] === "_") end--;
5529
+ return cleaned.slice(start, end) || "_";
5530
+ }
5531
+ function mcpToolName(serverName, toolName) {
5532
+ return `${MCP_TOOL_PREFIX}${sanitizeToolNameSegment(serverName)}__${sanitizeToolNameSegment(toolName)}`;
5533
+ }
5534
+ function isMcpToolName(name) {
5535
+ return name.startsWith(MCP_TOOL_PREFIX);
5536
+ }
5537
+ function mcpServerPrefix(serverName) {
5538
+ return `${MCP_TOOL_PREFIX}${sanitizeToolNameSegment(serverName)}__`;
5539
+ }
5540
+
5513
5541
  // src/cli/utils/terminal-input.ts
5514
5542
  var PASTE_START = "\x1B[200~";
5515
5543
  var PASTE_END = "\x1B[201~";
@@ -5834,7 +5862,21 @@ var ModelSelector = class {
5834
5862
  this.availableProviders = availableProviders;
5835
5863
  this.availableModels = new Map(Object.entries(MODELS));
5836
5864
  }
5865
+ /**
5866
+ * Verdicts from earlier runs about ids that 404 on this account. Consulted
5867
+ * BEFORE a model enters the pool, which is the only placement that helps: a
5868
+ * T3 wave runs concurrently, so filtering after the first failure still lets
5869
+ * every other worker in the wave call the dead id.
5870
+ */
5871
+ deadModels;
5872
+ setDeadModelStore(store) {
5873
+ this.deadModels = store;
5874
+ for (const [id, m] of [...this.availableModels]) {
5875
+ if (this.deadModels.isDead(m.provider, m.id)) this.availableModels.delete(id);
5876
+ }
5877
+ }
5837
5878
  addDynamicModel(model) {
5879
+ if (this.deadModels?.isDead(model.provider, model.id)) return;
5838
5880
  this.availableModels.set(model.id, model);
5839
5881
  if (!(model.id in MODELS)) this.discovered.add(normalizeModelId(model.id));
5840
5882
  }
@@ -6241,6 +6283,121 @@ var LocalRequestQueue = class {
6241
6283
  }
6242
6284
  };
6243
6285
 
6286
+ // src/core/router/dead-models.ts
6287
+ var DEAD_MODEL_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
6288
+ function deadModelKey(provider, modelId) {
6289
+ return `${provider}:${modelId}`;
6290
+ }
6291
+ var DeadModelStore = class {
6292
+ constructor(persistence, opts = {}) {
6293
+ this.persistence = persistence;
6294
+ this.ttlMs = opts.ttlMs ?? DEAD_MODEL_TTL_MS;
6295
+ this.now = opts.now ?? (() => Date.now());
6296
+ this.restore();
6297
+ }
6298
+ persistence;
6299
+ records = /* @__PURE__ */ new Map();
6300
+ ttlMs;
6301
+ now;
6302
+ restore() {
6303
+ if (!this.persistence) return;
6304
+ try {
6305
+ for (const r of this.persistence.load()) {
6306
+ if (!this.isExpired(r)) this.records.set(r.key, r);
6307
+ }
6308
+ } catch {
6309
+ }
6310
+ }
6311
+ isExpired(r) {
6312
+ return this.now() - r.lastSeenAt > this.ttlMs;
6313
+ }
6314
+ flush() {
6315
+ if (!this.persistence) return;
6316
+ try {
6317
+ this.persistence.save([...this.records.values()]);
6318
+ } catch {
6319
+ }
6320
+ }
6321
+ /**
6322
+ * Record that a model was found dead. Returns true when this is the first
6323
+ * time (so the caller can log it once rather than once per concurrent
6324
+ * worker in the wave that discovered it).
6325
+ */
6326
+ record(provider, modelId, reason) {
6327
+ const key = deadModelKey(provider, modelId);
6328
+ const now = this.now();
6329
+ const existing = this.records.get(key);
6330
+ if (existing && !this.isExpired(existing)) {
6331
+ existing.lastSeenAt = now;
6332
+ existing.hits += 1;
6333
+ this.flush();
6334
+ return false;
6335
+ }
6336
+ this.records.set(key, {
6337
+ key,
6338
+ provider,
6339
+ modelId,
6340
+ reason,
6341
+ firstSeenAt: now,
6342
+ lastSeenAt: now,
6343
+ hits: 1
6344
+ });
6345
+ this.flush();
6346
+ return true;
6347
+ }
6348
+ /** True when this model is known dead and the verdict has not expired. */
6349
+ isDead(provider, modelId) {
6350
+ const r = this.records.get(deadModelKey(provider, modelId));
6351
+ if (!r) return false;
6352
+ if (this.isExpired(r)) {
6353
+ this.records.delete(r.key);
6354
+ this.flush();
6355
+ return false;
6356
+ }
6357
+ return true;
6358
+ }
6359
+ /** Why a model is considered dead, for /why and the models command. */
6360
+ reasonFor(provider, modelId) {
6361
+ const r = this.records.get(deadModelKey(provider, modelId));
6362
+ return r && !this.isExpired(r) ? r.reason : null;
6363
+ }
6364
+ /** Live records, expired ones filtered out. */
6365
+ list() {
6366
+ const live = [...this.records.values()].filter((r) => !this.isExpired(r));
6367
+ return live.sort((a, b) => b.lastSeenAt - a.lastSeenAt);
6368
+ }
6369
+ /**
6370
+ * Forget a verdict — used when the user fixes the underlying problem (adds
6371
+ * the model to their project, gets quota) and wants it retried now rather
6372
+ * than after the TTL.
6373
+ */
6374
+ forget(provider, modelId) {
6375
+ const removed = this.records.delete(deadModelKey(provider, modelId));
6376
+ if (removed) this.flush();
6377
+ return removed;
6378
+ }
6379
+ clear() {
6380
+ this.records.clear();
6381
+ this.flush();
6382
+ }
6383
+ };
6384
+ function fileDeadModelPersistence(filePath) {
6385
+ return {
6386
+ load() {
6387
+ const fs32 = __require("fs");
6388
+ if (!fs32.existsSync(filePath)) return [];
6389
+ const raw = JSON.parse(fs32.readFileSync(filePath, "utf-8"));
6390
+ return Array.isArray(raw) ? raw : [];
6391
+ },
6392
+ save(records) {
6393
+ const fs32 = __require("fs");
6394
+ const path37 = __require("path");
6395
+ fs32.mkdirSync(path37.dirname(filePath), { recursive: true });
6396
+ fs32.writeFileSync(filePath, JSON.stringify(records, null, 2));
6397
+ }
6398
+ };
6399
+ }
6400
+
6244
6401
  // src/core/router/index.ts
6245
6402
  init_constants();
6246
6403
  init_cost();
@@ -6830,6 +6987,26 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
6830
6987
  localQueue;
6831
6988
  taskAnalyzer;
6832
6989
  pendingFeedbackSource;
6990
+ /** Models known dead for this account (404 / retired / not enabled). */
6991
+ deadModels = new DeadModelStore();
6992
+ /**
6993
+ * Back the dead-model memory with durable storage (a JSON file on desktop, a
6994
+ * per-user table in the cloud). Without one the verdicts still work, but only
6995
+ * for the life of the process — so tomorrow's run pays the same wave of 404s
6996
+ * to learn the same fact.
6997
+ */
6998
+ setDeadModelStore(store) {
6999
+ this.deadModels = store;
7000
+ this.selector.setDeadModelStore(store);
7001
+ }
7002
+ /** Models known dead for this account, for `cascade models` and /why. */
7003
+ listDeadModels() {
7004
+ return this.deadModels.list();
7005
+ }
7006
+ /** Retry a model the user has since fixed, without waiting out the TTL. */
7007
+ forgetDeadModel(provider, modelId) {
7008
+ return this.deadModels.forget(provider, modelId);
7009
+ }
6833
7010
  worldStateDB;
6834
7011
  privacyPaths;
6835
7012
  guidanceQueue;
@@ -7194,6 +7371,8 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
7194
7371
  }
7195
7372
  if (isModelNotFoundError(errMsg)) {
7196
7373
  this.selector.removeModel(model.id);
7374
+ const firstSighting = this.deadModels.record(model.provider, model.id, errMsg);
7375
+ if (firstSighting) this.emit("model:dead", { provider: model.provider, modelId: model.id, reason: errMsg });
7197
7376
  const depth = (options._notFoundDepth ?? 0) + 1;
7198
7377
  const next = depth <= 3 ? this.selector.selectForTier(tier) : null;
7199
7378
  if (next && next.id !== model.id) {
@@ -10997,9 +11176,16 @@ var CAPABILITIES = [
10997
11176
  api: "openai-images"
10998
11177
  },
10999
11178
  {
11179
+ // Imagen, not `gemini-3-pro-image`. That id appears on Google's PRICING
11180
+ // page but 404s on `:predict` ("not found for API version v1beta, or is not
11181
+ // supported for predict") — the Gemini image models are called through
11182
+ // generateContent with an image response modality, while `:predict` is the
11183
+ // Imagen shape this executor implements. A billing line item is not proof
11184
+ // of a callable endpoint, which is the assumption that put the wrong id
11185
+ // here in the first place.
11000
11186
  modality: "image",
11001
11187
  provider: "gemini",
11002
- modelId: "gemini-3-pro-image",
11188
+ modelId: "imagen-4.0-generate-001",
11003
11189
  api: "gemini-predict"
11004
11190
  },
11005
11191
  // ── Speech (text → audio) ──
@@ -13144,7 +13330,7 @@ var McpToolWrapper = class extends BaseTool {
13144
13330
  this.mcpClient = mcpClient;
13145
13331
  this.serverName = serverName;
13146
13332
  this.toolName = toolName;
13147
- this.name = `mcp::${serverName}::${toolName}`;
13333
+ this.name = mcpToolName(serverName, toolName);
13148
13334
  this.description = `[MCP:${serverName}] ${description}`;
13149
13335
  this.inputSchema = inputSchema;
13150
13336
  }
@@ -16102,6 +16288,13 @@ var Cascade = class _Cascade extends EventEmitter {
16102
16288
  }
16103
16289
  });
16104
16290
  this.toolRegistry = new ToolRegistry(this.config.tools, workspacePath);
16291
+ try {
16292
+ const nodePath = __require("path");
16293
+ this.router.setDeadModelStore(new DeadModelStore(
16294
+ fileDeadModelPersistence(nodePath.join(workspacePath, ".cascade", "dead-models.json"))
16295
+ ));
16296
+ } catch {
16297
+ }
16105
16298
  this.multimodal = new MultimodalRegistry(
16106
16299
  (this.config.providers ?? []).map((p) => p.type)
16107
16300
  );
@@ -19505,13 +19698,13 @@ ${msg.content}`).join("\n\n");
19505
19698
  const cascade = cascadeRef.current;
19506
19699
  if (!cascade) return "Cascade not initialized.";
19507
19700
  const toolRegistry = cascade.getToolRegistry();
19508
- const tools = toolRegistry.getToolDefinitions().filter((t) => t.name.startsWith("mcp::"));
19701
+ const tools = toolRegistry.getToolDefinitions().filter((t) => isMcpToolName(t.name));
19509
19702
  if (tools.length === 0) return "No MCP tools connected.";
19510
19703
  const servers = /* @__PURE__ */ new Set();
19511
19704
  tools.forEach((t) => servers.add(t.name.split("::")[1]));
19512
19705
  const lines = [`Connected MCP Servers (${servers.size}):`];
19513
19706
  for (const server of servers) {
19514
- const serverTools = tools.filter((t) => t.name.startsWith(`mcp::${server}::`));
19707
+ const serverTools = tools.filter((t) => t.name.startsWith(mcpServerPrefix(server)));
19515
19708
  lines.push(`
19516
19709
  \u25CF ${server} (${serverTools.length} tools)`);
19517
19710
  serverTools.forEach((t) => lines.push(` - ${t.name.split("::")[2]} \u2014 ${t.description.replace(`[MCP:${server}] `, "")}`));