cascade-ai 0.53.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.53.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";
@@ -5909,7 +5915,21 @@ var ModelSelector = class {
5909
5915
  this.availableProviders = availableProviders;
5910
5916
  this.availableModels = new Map(Object.entries(MODELS));
5911
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
+ }
5912
5931
  addDynamicModel(model) {
5932
+ if (this.deadModels?.isDead(model.provider, model.id)) return;
5913
5933
  this.availableModels.set(model.id, model);
5914
5934
  if (!(model.id in MODELS)) this.discovered.add(normalizeModelId(model.id));
5915
5935
  }
@@ -6316,6 +6336,121 @@ var LocalRequestQueue = class {
6316
6336
  }
6317
6337
  };
6318
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
+
6319
6454
  // src/core/router/index.ts
6320
6455
  init_constants();
6321
6456
  init_cost();
@@ -6905,6 +7040,26 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
6905
7040
  localQueue;
6906
7041
  taskAnalyzer;
6907
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
+ }
6908
7063
  worldStateDB;
6909
7064
  privacyPaths;
6910
7065
  guidanceQueue;
@@ -7269,6 +7424,8 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
7269
7424
  }
7270
7425
  if (isModelNotFoundError(errMsg)) {
7271
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 });
7272
7429
  const depth = (options._notFoundDepth ?? 0) + 1;
7273
7430
  const next = depth <= 3 ? this.selector.selectForTier(tier) : null;
7274
7431
  if (next && next.id !== model.id) {
@@ -11072,9 +11229,16 @@ var CAPABILITIES = [
11072
11229
  api: "openai-images"
11073
11230
  },
11074
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.
11075
11239
  modality: "image",
11076
11240
  provider: "gemini",
11077
- modelId: "gemini-3-pro-image",
11241
+ modelId: "imagen-4.0-generate-001",
11078
11242
  api: "gemini-predict"
11079
11243
  },
11080
11244
  // ── Speech (text → audio) ──
@@ -16177,6 +16341,13 @@ var Cascade = class _Cascade extends EventEmitter__default.default {
16177
16341
  }
16178
16342
  });
16179
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
+ }
16180
16351
  this.multimodal = new MultimodalRegistry(
16181
16352
  (this.config.providers ?? []).map((p) => p.type)
16182
16353
  );