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.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.53.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";
@@ -5856,7 +5862,21 @@ var ModelSelector = class {
5856
5862
  this.availableProviders = availableProviders;
5857
5863
  this.availableModels = new Map(Object.entries(MODELS));
5858
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
+ }
5859
5878
  addDynamicModel(model) {
5879
+ if (this.deadModels?.isDead(model.provider, model.id)) return;
5860
5880
  this.availableModels.set(model.id, model);
5861
5881
  if (!(model.id in MODELS)) this.discovered.add(normalizeModelId(model.id));
5862
5882
  }
@@ -6263,6 +6283,121 @@ var LocalRequestQueue = class {
6263
6283
  }
6264
6284
  };
6265
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
+
6266
6401
  // src/core/router/index.ts
6267
6402
  init_constants();
6268
6403
  init_cost();
@@ -6852,6 +6987,26 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
6852
6987
  localQueue;
6853
6988
  taskAnalyzer;
6854
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
+ }
6855
7010
  worldStateDB;
6856
7011
  privacyPaths;
6857
7012
  guidanceQueue;
@@ -7216,6 +7371,8 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
7216
7371
  }
7217
7372
  if (isModelNotFoundError(errMsg)) {
7218
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 });
7219
7376
  const depth = (options._notFoundDepth ?? 0) + 1;
7220
7377
  const next = depth <= 3 ? this.selector.selectForTier(tier) : null;
7221
7378
  if (next && next.id !== model.id) {
@@ -11019,9 +11176,16 @@ var CAPABILITIES = [
11019
11176
  api: "openai-images"
11020
11177
  },
11021
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.
11022
11186
  modality: "image",
11023
11187
  provider: "gemini",
11024
- modelId: "gemini-3-pro-image",
11188
+ modelId: "imagen-4.0-generate-001",
11025
11189
  api: "gemini-predict"
11026
11190
  },
11027
11191
  // ── Speech (text → audio) ──
@@ -16124,6 +16288,13 @@ var Cascade = class _Cascade extends EventEmitter {
16124
16288
  }
16125
16289
  });
16126
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
+ }
16127
16298
  this.multimodal = new MultimodalRegistry(
16128
16299
  (this.config.providers ?? []).map((p) => p.type)
16129
16300
  );