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/index.d.cts CHANGED
@@ -904,6 +904,16 @@ declare class ModelSelector {
904
904
  */
905
905
  private discovered;
906
906
  constructor(availableProviders: Set<ProviderType>);
907
+ /**
908
+ * Verdicts from earlier runs about ids that 404 on this account. Consulted
909
+ * BEFORE a model enters the pool, which is the only placement that helps: a
910
+ * T3 wave runs concurrently, so filtering after the first failure still lets
911
+ * every other worker in the wave call the dead id.
912
+ */
913
+ private deadModels?;
914
+ setDeadModelStore(store: {
915
+ isDead(provider: string, modelId: string): boolean;
916
+ }): void;
907
917
  addDynamicModel(model: ModelInfo): void;
908
918
  /** Record the ids a provider's API actually serves (discovery). Empty ⇒ ignored. */
909
919
  setValidatedModels(provider: ProviderType, ids: string[]): void;
@@ -1077,6 +1087,60 @@ declare class TaskAnalyzer {
1077
1087
  static clearCache(): void;
1078
1088
  }
1079
1089
 
1090
+ interface DeadModelRecord {
1091
+ /** `provider:modelId` — the same key shape the failover event reports. */
1092
+ key: string;
1093
+ provider: string;
1094
+ modelId: string;
1095
+ /** Provider's own words, kept verbatim so the user can act on them. */
1096
+ reason: string;
1097
+ firstSeenAt: number;
1098
+ lastSeenAt: number;
1099
+ /** How many times this model has been found dead. */
1100
+ hits: number;
1101
+ }
1102
+ /**
1103
+ * Where records live between runs. Desktop/CLI back this with a JSON file,
1104
+ * the cloud server with a per-user table. Both are synchronous-optional: a
1105
+ * store that fails to load must never stop a run.
1106
+ */
1107
+ interface DeadModelPersistence {
1108
+ load(): DeadModelRecord[];
1109
+ save(records: DeadModelRecord[]): void;
1110
+ }
1111
+ declare class DeadModelStore {
1112
+ private persistence?;
1113
+ private records;
1114
+ private readonly ttlMs;
1115
+ private readonly now;
1116
+ constructor(persistence?: DeadModelPersistence | undefined, opts?: {
1117
+ ttlMs?: number;
1118
+ now?: () => number;
1119
+ });
1120
+ private restore;
1121
+ private isExpired;
1122
+ private flush;
1123
+ /**
1124
+ * Record that a model was found dead. Returns true when this is the first
1125
+ * time (so the caller can log it once rather than once per concurrent
1126
+ * worker in the wave that discovered it).
1127
+ */
1128
+ record(provider: string, modelId: string, reason: string): boolean;
1129
+ /** True when this model is known dead and the verdict has not expired. */
1130
+ isDead(provider: string, modelId: string): boolean;
1131
+ /** Why a model is considered dead, for /why and the models command. */
1132
+ reasonFor(provider: string, modelId: string): string | null;
1133
+ /** Live records, expired ones filtered out. */
1134
+ list(): DeadModelRecord[];
1135
+ /**
1136
+ * Forget a verdict — used when the user fixes the underlying problem (adds
1137
+ * the model to their project, gets quota) and wants it retried now rather
1138
+ * than after the TTL.
1139
+ */
1140
+ forget(provider: string, modelId: string): boolean;
1141
+ clear(): void;
1142
+ }
1143
+
1080
1144
  declare class MemoryStore {
1081
1145
  private db;
1082
1146
  constructor(dbPath: string);
@@ -1509,6 +1573,19 @@ declare class CascadeRouter extends EventEmitter {
1509
1573
  private localQueue;
1510
1574
  private taskAnalyzer?;
1511
1575
  private pendingFeedbackSource?;
1576
+ /** Models known dead for this account (404 / retired / not enabled). */
1577
+ private deadModels;
1578
+ /**
1579
+ * Back the dead-model memory with durable storage (a JSON file on desktop, a
1580
+ * per-user table in the cloud). Without one the verdicts still work, but only
1581
+ * for the life of the process — so tomorrow's run pays the same wave of 404s
1582
+ * to learn the same fact.
1583
+ */
1584
+ setDeadModelStore(store: DeadModelStore): void;
1585
+ /** Models known dead for this account, for `cascade models` and /why. */
1586
+ listDeadModels(): ReturnType<DeadModelStore['list']>;
1587
+ /** Retry a model the user has since fixed, without waiting out the TTL. */
1588
+ forgetDeadModel(provider: string, modelId: string): boolean;
1512
1589
  private worldStateDB?;
1513
1590
  private privacyPaths?;
1514
1591
  private guidanceQueue?;
package/dist/index.d.ts CHANGED
@@ -904,6 +904,16 @@ declare class ModelSelector {
904
904
  */
905
905
  private discovered;
906
906
  constructor(availableProviders: Set<ProviderType>);
907
+ /**
908
+ * Verdicts from earlier runs about ids that 404 on this account. Consulted
909
+ * BEFORE a model enters the pool, which is the only placement that helps: a
910
+ * T3 wave runs concurrently, so filtering after the first failure still lets
911
+ * every other worker in the wave call the dead id.
912
+ */
913
+ private deadModels?;
914
+ setDeadModelStore(store: {
915
+ isDead(provider: string, modelId: string): boolean;
916
+ }): void;
907
917
  addDynamicModel(model: ModelInfo): void;
908
918
  /** Record the ids a provider's API actually serves (discovery). Empty ⇒ ignored. */
909
919
  setValidatedModels(provider: ProviderType, ids: string[]): void;
@@ -1077,6 +1087,60 @@ declare class TaskAnalyzer {
1077
1087
  static clearCache(): void;
1078
1088
  }
1079
1089
 
1090
+ interface DeadModelRecord {
1091
+ /** `provider:modelId` — the same key shape the failover event reports. */
1092
+ key: string;
1093
+ provider: string;
1094
+ modelId: string;
1095
+ /** Provider's own words, kept verbatim so the user can act on them. */
1096
+ reason: string;
1097
+ firstSeenAt: number;
1098
+ lastSeenAt: number;
1099
+ /** How many times this model has been found dead. */
1100
+ hits: number;
1101
+ }
1102
+ /**
1103
+ * Where records live between runs. Desktop/CLI back this with a JSON file,
1104
+ * the cloud server with a per-user table. Both are synchronous-optional: a
1105
+ * store that fails to load must never stop a run.
1106
+ */
1107
+ interface DeadModelPersistence {
1108
+ load(): DeadModelRecord[];
1109
+ save(records: DeadModelRecord[]): void;
1110
+ }
1111
+ declare class DeadModelStore {
1112
+ private persistence?;
1113
+ private records;
1114
+ private readonly ttlMs;
1115
+ private readonly now;
1116
+ constructor(persistence?: DeadModelPersistence | undefined, opts?: {
1117
+ ttlMs?: number;
1118
+ now?: () => number;
1119
+ });
1120
+ private restore;
1121
+ private isExpired;
1122
+ private flush;
1123
+ /**
1124
+ * Record that a model was found dead. Returns true when this is the first
1125
+ * time (so the caller can log it once rather than once per concurrent
1126
+ * worker in the wave that discovered it).
1127
+ */
1128
+ record(provider: string, modelId: string, reason: string): boolean;
1129
+ /** True when this model is known dead and the verdict has not expired. */
1130
+ isDead(provider: string, modelId: string): boolean;
1131
+ /** Why a model is considered dead, for /why and the models command. */
1132
+ reasonFor(provider: string, modelId: string): string | null;
1133
+ /** Live records, expired ones filtered out. */
1134
+ list(): DeadModelRecord[];
1135
+ /**
1136
+ * Forget a verdict — used when the user fixes the underlying problem (adds
1137
+ * the model to their project, gets quota) and wants it retried now rather
1138
+ * than after the TTL.
1139
+ */
1140
+ forget(provider: string, modelId: string): boolean;
1141
+ clear(): void;
1142
+ }
1143
+
1080
1144
  declare class MemoryStore {
1081
1145
  private db;
1082
1146
  constructor(dbPath: string);
@@ -1509,6 +1573,19 @@ declare class CascadeRouter extends EventEmitter {
1509
1573
  private localQueue;
1510
1574
  private taskAnalyzer?;
1511
1575
  private pendingFeedbackSource?;
1576
+ /** Models known dead for this account (404 / retired / not enabled). */
1577
+ private deadModels;
1578
+ /**
1579
+ * Back the dead-model memory with durable storage (a JSON file on desktop, a
1580
+ * per-user table in the cloud). Without one the verdicts still work, but only
1581
+ * for the life of the process — so tomorrow's run pays the same wave of 404s
1582
+ * to learn the same fact.
1583
+ */
1584
+ setDeadModelStore(store: DeadModelStore): void;
1585
+ /** Models known dead for this account, for `cascade models` and /why. */
1586
+ listDeadModels(): ReturnType<DeadModelStore['list']>;
1587
+ /** Retry a model the user has since fixed, without waiting out the TTL. */
1588
+ forgetDeadModel(provider: string, modelId: string): boolean;
1512
1589
  private worldStateDB?;
1513
1590
  private privacyPaths?;
1514
1591
  private guidanceQueue?;
package/dist/index.js CHANGED
@@ -41,6 +41,12 @@ 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
+ });
44
50
  var __esm = (fn, res) => function __init() {
45
51
  return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
46
52
  };
@@ -1845,7 +1851,7 @@ function reconcilePrice(model, live) {
1845
1851
  }
1846
1852
 
1847
1853
  // src/constants.ts
1848
- var CASCADE_VERSION = "0.53.0";
1854
+ var CASCADE_VERSION = "0.54.0";
1849
1855
  var CASCADE_CONFIG_DIR = ".cascade";
1850
1856
  var CASCADE_MD_FILE = "CASCADE.md";
1851
1857
  var CASCADE_IGNORE_FILE = ".cascadeignore";
@@ -3658,7 +3664,21 @@ var ModelSelector = class {
3658
3664
  this.availableProviders = availableProviders;
3659
3665
  this.availableModels = new Map(Object.entries(MODELS));
3660
3666
  }
3667
+ /**
3668
+ * Verdicts from earlier runs about ids that 404 on this account. Consulted
3669
+ * BEFORE a model enters the pool, which is the only placement that helps: a
3670
+ * T3 wave runs concurrently, so filtering after the first failure still lets
3671
+ * every other worker in the wave call the dead id.
3672
+ */
3673
+ deadModels;
3674
+ setDeadModelStore(store) {
3675
+ this.deadModels = store;
3676
+ for (const [id, m] of [...this.availableModels]) {
3677
+ if (this.deadModels.isDead(m.provider, m.id)) this.availableModels.delete(id);
3678
+ }
3679
+ }
3661
3680
  addDynamicModel(model) {
3681
+ if (this.deadModels?.isDead(model.provider, model.id)) return;
3662
3682
  this.availableModels.set(model.id, model);
3663
3683
  if (!(model.id in MODELS)) this.discovered.add(normalizeModelId(model.id));
3664
3684
  }
@@ -4065,6 +4085,121 @@ var LocalRequestQueue = class {
4065
4085
  }
4066
4086
  };
4067
4087
 
4088
+ // src/core/router/dead-models.ts
4089
+ var DEAD_MODEL_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
4090
+ function deadModelKey(provider, modelId) {
4091
+ return `${provider}:${modelId}`;
4092
+ }
4093
+ var DeadModelStore = class {
4094
+ constructor(persistence, opts = {}) {
4095
+ this.persistence = persistence;
4096
+ this.ttlMs = opts.ttlMs ?? DEAD_MODEL_TTL_MS;
4097
+ this.now = opts.now ?? (() => Date.now());
4098
+ this.restore();
4099
+ }
4100
+ persistence;
4101
+ records = /* @__PURE__ */ new Map();
4102
+ ttlMs;
4103
+ now;
4104
+ restore() {
4105
+ if (!this.persistence) return;
4106
+ try {
4107
+ for (const r of this.persistence.load()) {
4108
+ if (!this.isExpired(r)) this.records.set(r.key, r);
4109
+ }
4110
+ } catch {
4111
+ }
4112
+ }
4113
+ isExpired(r) {
4114
+ return this.now() - r.lastSeenAt > this.ttlMs;
4115
+ }
4116
+ flush() {
4117
+ if (!this.persistence) return;
4118
+ try {
4119
+ this.persistence.save([...this.records.values()]);
4120
+ } catch {
4121
+ }
4122
+ }
4123
+ /**
4124
+ * Record that a model was found dead. Returns true when this is the first
4125
+ * time (so the caller can log it once rather than once per concurrent
4126
+ * worker in the wave that discovered it).
4127
+ */
4128
+ record(provider, modelId, reason) {
4129
+ const key = deadModelKey(provider, modelId);
4130
+ const now = this.now();
4131
+ const existing = this.records.get(key);
4132
+ if (existing && !this.isExpired(existing)) {
4133
+ existing.lastSeenAt = now;
4134
+ existing.hits += 1;
4135
+ this.flush();
4136
+ return false;
4137
+ }
4138
+ this.records.set(key, {
4139
+ key,
4140
+ provider,
4141
+ modelId,
4142
+ reason,
4143
+ firstSeenAt: now,
4144
+ lastSeenAt: now,
4145
+ hits: 1
4146
+ });
4147
+ this.flush();
4148
+ return true;
4149
+ }
4150
+ /** True when this model is known dead and the verdict has not expired. */
4151
+ isDead(provider, modelId) {
4152
+ const r = this.records.get(deadModelKey(provider, modelId));
4153
+ if (!r) return false;
4154
+ if (this.isExpired(r)) {
4155
+ this.records.delete(r.key);
4156
+ this.flush();
4157
+ return false;
4158
+ }
4159
+ return true;
4160
+ }
4161
+ /** Why a model is considered dead, for /why and the models command. */
4162
+ reasonFor(provider, modelId) {
4163
+ const r = this.records.get(deadModelKey(provider, modelId));
4164
+ return r && !this.isExpired(r) ? r.reason : null;
4165
+ }
4166
+ /** Live records, expired ones filtered out. */
4167
+ list() {
4168
+ const live = [...this.records.values()].filter((r) => !this.isExpired(r));
4169
+ return live.sort((a, b) => b.lastSeenAt - a.lastSeenAt);
4170
+ }
4171
+ /**
4172
+ * Forget a verdict — used when the user fixes the underlying problem (adds
4173
+ * the model to their project, gets quota) and wants it retried now rather
4174
+ * than after the TTL.
4175
+ */
4176
+ forget(provider, modelId) {
4177
+ const removed = this.records.delete(deadModelKey(provider, modelId));
4178
+ if (removed) this.flush();
4179
+ return removed;
4180
+ }
4181
+ clear() {
4182
+ this.records.clear();
4183
+ this.flush();
4184
+ }
4185
+ };
4186
+ function fileDeadModelPersistence(filePath) {
4187
+ return {
4188
+ load() {
4189
+ const fs25 = __require("fs");
4190
+ if (!fs25.existsSync(filePath)) return [];
4191
+ const raw = JSON.parse(fs25.readFileSync(filePath, "utf-8"));
4192
+ return Array.isArray(raw) ? raw : [];
4193
+ },
4194
+ 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));
4199
+ }
4200
+ };
4201
+ }
4202
+
4068
4203
  // src/utils/retry.ts
4069
4204
  var CascadeCancelledError = class extends Error {
4070
4205
  constructor(reason) {
@@ -4658,6 +4793,26 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
4658
4793
  localQueue;
4659
4794
  taskAnalyzer;
4660
4795
  pendingFeedbackSource;
4796
+ /** Models known dead for this account (404 / retired / not enabled). */
4797
+ deadModels = new DeadModelStore();
4798
+ /**
4799
+ * Back the dead-model memory with durable storage (a JSON file on desktop, a
4800
+ * per-user table in the cloud). Without one the verdicts still work, but only
4801
+ * for the life of the process — so tomorrow's run pays the same wave of 404s
4802
+ * to learn the same fact.
4803
+ */
4804
+ setDeadModelStore(store) {
4805
+ this.deadModels = store;
4806
+ this.selector.setDeadModelStore(store);
4807
+ }
4808
+ /** Models known dead for this account, for `cascade models` and /why. */
4809
+ listDeadModels() {
4810
+ return this.deadModels.list();
4811
+ }
4812
+ /** Retry a model the user has since fixed, without waiting out the TTL. */
4813
+ forgetDeadModel(provider, modelId) {
4814
+ return this.deadModels.forget(provider, modelId);
4815
+ }
4661
4816
  worldStateDB;
4662
4817
  privacyPaths;
4663
4818
  guidanceQueue;
@@ -5022,6 +5177,8 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
5022
5177
  }
5023
5178
  if (isModelNotFoundError(errMsg)) {
5024
5179
  this.selector.removeModel(model.id);
5180
+ const firstSighting = this.deadModels.record(model.provider, model.id, errMsg);
5181
+ if (firstSighting) this.emit("model:dead", { provider: model.provider, modelId: model.id, reason: errMsg });
5025
5182
  const depth = (options._notFoundDepth ?? 0) + 1;
5026
5183
  const next = depth <= 3 ? this.selector.selectForTier(tier) : null;
5027
5184
  if (next && next.id !== model.id) {
@@ -8817,9 +8974,16 @@ var CAPABILITIES = [
8817
8974
  api: "openai-images"
8818
8975
  },
8819
8976
  {
8977
+ // Imagen, not `gemini-3-pro-image`. That id appears on Google's PRICING
8978
+ // page but 404s on `:predict` ("not found for API version v1beta, or is not
8979
+ // supported for predict") — the Gemini image models are called through
8980
+ // generateContent with an image response modality, while `:predict` is the
8981
+ // Imagen shape this executor implements. A billing line item is not proof
8982
+ // of a callable endpoint, which is the assumption that put the wrong id
8983
+ // here in the first place.
8820
8984
  modality: "image",
8821
8985
  provider: "gemini",
8822
- modelId: "gemini-3-pro-image",
8986
+ modelId: "imagen-4.0-generate-001",
8823
8987
  api: "gemini-predict"
8824
8988
  },
8825
8989
  // ── Speech (text → audio) ──
@@ -14391,6 +14555,13 @@ var Cascade = class _Cascade extends EventEmitter {
14391
14555
  }
14392
14556
  });
14393
14557
  this.toolRegistry = new ToolRegistry(this.config.tools, workspacePath);
14558
+ try {
14559
+ const nodePath = __require("path");
14560
+ this.router.setDeadModelStore(new DeadModelStore(
14561
+ fileDeadModelPersistence(nodePath.join(workspacePath, ".cascade", "dead-models.json"))
14562
+ ));
14563
+ } catch {
14564
+ }
14394
14565
  this.multimodal = new MultimodalRegistry(
14395
14566
  (this.config.providers ?? []).map((p) => p.type)
14396
14567
  );