cascade-ai 0.69.0 → 0.71.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.js CHANGED
@@ -1862,7 +1862,7 @@ function reconcilePrice(model, live) {
1862
1862
  }
1863
1863
 
1864
1864
  // src/constants.ts
1865
- var CASCADE_VERSION = "0.69.0";
1865
+ var CASCADE_VERSION = "0.71.0";
1866
1866
  var CASCADE_CONFIG_DIR = ".cascade";
1867
1867
  var CASCADE_MD_FILE = "CASCADE.md";
1868
1868
  var CASCADE_IGNORE_FILE = ".cascadeignore";
@@ -2269,9 +2269,6 @@ var DEFAULT_THEME = "cascade";
2269
2269
  var OLLAMA_BASE_URL = "http://localhost:11434";
2270
2270
  var LM_STUDIO_BASE_URL = "http://localhost:1234";
2271
2271
  var AZURE_BASE_URL_TEMPLATE = "https://{resource}.openai.azure.com";
2272
- var GITHUB_MODELS_INFERENCE_URL = "https://models.github.ai/inference";
2273
- var GITHUB_MODELS_CATALOG_URL = "https://models.github.ai/catalog/models";
2274
- var GITHUB_MODELS_API_VERSION = "2026-03-10";
2275
2272
  var TOOL_NAMES = {
2276
2273
  SHELL: "shell",
2277
2274
  FILE_READ: "file_read",
@@ -2306,8 +2303,7 @@ var PROVIDER_DISPLAY_NAMES = {
2306
2303
  gemini: "Google Gemini",
2307
2304
  azure: "Azure OpenAI",
2308
2305
  "openai-compatible": "OpenAI-Compatible",
2309
- ollama: "Ollama (Local)",
2310
- "github-models": "GitHub Models"
2306
+ ollama: "Ollama (Local)"
2311
2307
  };
2312
2308
 
2313
2309
  // src/utils/cost.ts
@@ -3350,206 +3346,6 @@ async function nodeHttpFetch(input, init = {}, redirectCount = 0, opts = {}) {
3350
3346
  });
3351
3347
  }
3352
3348
 
3353
- // src/providers/github-models.ts
3354
- var GITHUB_MODELS_MAX_OUTPUT_TOKENS = 4e3;
3355
- var GITHUB_MODELS_MAX_INPUT_TOKENS = 8e3;
3356
- var DEFAULT_CONTEXT_WINDOW = 128e3;
3357
- var GITHUB_MODELS_MAX_CATALOG_PAGES = 20;
3358
- var GITHUB_MODELS_CATALOG_ORIGIN = new URL(GITHUB_MODELS_CATALOG_URL).origin;
3359
- function stripModelOwnerPrefix(id) {
3360
- const slash = (id || "").indexOf("/");
3361
- return slash === -1 ? id : id.slice(slash + 1);
3362
- }
3363
- function pickString(o, keys) {
3364
- for (const k of keys) {
3365
- const v = o[k];
3366
- if (typeof v === "string" && v.length > 0) return v;
3367
- }
3368
- return void 0;
3369
- }
3370
- function pickNumber(o, keys) {
3371
- for (const k of keys) {
3372
- const v = k.includes(".") ? o[k.split(".")[0]]?.[k.split(".")[1]] : o[k];
3373
- if (typeof v === "number" && Number.isFinite(v) && v > 0) return v;
3374
- }
3375
- return void 0;
3376
- }
3377
- function looksVisionCapable(o) {
3378
- for (const k of ["supported_input_modalities", "input_modalities", "modalities", "capabilities"]) {
3379
- const v = o[k];
3380
- if (Array.isArray(v) && v.some((x) => typeof x === "string" && /image|vision/i.test(x))) return true;
3381
- }
3382
- return false;
3383
- }
3384
- function looksToolCapable(o) {
3385
- const params = o["supported_parameters"];
3386
- if (Array.isArray(params) && params.some((x) => typeof x === "string" && /^tools?$|function.?call/i.test(x))) {
3387
- return true;
3388
- }
3389
- for (const k of ["capabilities", "supported_output_modalities"]) {
3390
- const v = o[k];
3391
- if (Array.isArray(v) && v.some((x) => typeof x === "string" && /tool|function.?call/i.test(x))) return true;
3392
- }
3393
- const caps = o["capabilities"];
3394
- if (caps && typeof caps === "object" && !Array.isArray(caps)) {
3395
- const c = caps;
3396
- if (c["function_calling"] === true || c["tool_calling"] === true || c["tools"] === true) return true;
3397
- }
3398
- return false;
3399
- }
3400
- function parseNextLink(linkHeader) {
3401
- if (!linkHeader) return null;
3402
- for (const part of linkHeader.split(",")) {
3403
- const match = part.match(/<([^>]+)>\s*;\s*rel="next"/);
3404
- if (match) return match[1];
3405
- }
3406
- return null;
3407
- }
3408
- var GitHubModelsProvider = class extends OpenAIProvider {
3409
- constructor(config, model) {
3410
- super({ ...config, apiKey: config.apiKey ?? "" }, model);
3411
- this.useMaxCompletionTokens = isReasoningModel(stripModelOwnerPrefix(model.id));
3412
- this.client = new OpenAI({
3413
- apiKey: config.apiKey ?? "",
3414
- baseURL: GITHUB_MODELS_INFERENCE_URL,
3415
- defaultHeaders: { "X-GitHub-Api-Version": GITHUB_MODELS_API_VERSION, "User-Agent": "Cascade-AI" }
3416
- });
3417
- }
3418
- /**
3419
- * `OpenAIProvider.generateStream()` builds its request budget as
3420
- * `options.maxTokens ?? this.model.maxOutputTokens` — an explicit per-call
3421
- * `maxTokens` (T1Administrator's final compilation asks for 8,000, well
3422
- * above GitHub's real ~4K hard cap) wins outright and is sent unclamped.
3423
- * GitHub then rejects the request instead of answering, so a run routed to
3424
- * this provider for that step fails outright rather than getting a
3425
- * (perfectly serviceable, just shorter) completion. Clamp here, once, right
3426
- * before delegating to the inherited implementation, rather than teaching
3427
- * every caller GitHub's specific cap.
3428
- */
3429
- async generateStream(options, onChunk) {
3430
- const clamped = options.maxTokens !== void 0 && options.maxTokens > GITHUB_MODELS_MAX_OUTPUT_TOKENS ? { ...options, maxTokens: GITHUB_MODELS_MAX_OUTPUT_TOKENS } : options;
3431
- return super.generateStream(clamped, onChunk);
3432
- }
3433
- /**
3434
- * The catalog is a GitHub REST resource, not an OpenAI `/models` list, and
3435
- * wants GitHub's own header trio — the plain `Accept: application/json` the
3436
- * openai-compatible provider sends is not the same request.
3437
- */
3438
- catalogHeaders() {
3439
- return {
3440
- Accept: "application/vnd.github+json",
3441
- Authorization: `Bearer ${this.config.apiKey ?? ""}`,
3442
- "X-GitHub-Api-Version": GITHUB_MODELS_API_VERSION,
3443
- // GitHub's REST API rejects any request with no User-Agent — a plain 403
3444
- // regardless of how valid the PAT is. nodeHttpFetch is a thin wrapper
3445
- // over node:http/https, neither of which sets one the way a browser's
3446
- // fetch or a full HTTP client library would, so this has to be explicit.
3447
- "User-Agent": "Cascade-AI"
3448
- };
3449
- }
3450
- async listModels() {
3451
- const raw = [];
3452
- let url = GITHUB_MODELS_CATALOG_URL;
3453
- let pages = 0;
3454
- const visited = /* @__PURE__ */ new Set();
3455
- while (url && pages < GITHUB_MODELS_MAX_CATALOG_PAGES) {
3456
- if (visited.has(url)) break;
3457
- visited.add(url);
3458
- const res = await nodeHttpFetch(url, { headers: this.catalogHeaders() }, 0, {
3459
- allowedRedirectOrigin: GITHUB_MODELS_CATALOG_ORIGIN
3460
- });
3461
- if (!res.ok) throw new Error(`GitHub Models catalog ${url} returned HTTP ${res.status}`);
3462
- const body = await res.json();
3463
- const pageItems = Array.isArray(body) ? body : Array.isArray(body?.models) ? body.models : Array.isArray(body?.data) ? body.data : [];
3464
- raw.push(...pageItems);
3465
- const next = parseNextLink(res.headers.get("link"));
3466
- if (next) {
3467
- const resolved = new URL(next, url);
3468
- if (resolved.origin !== GITHUB_MODELS_CATALOG_ORIGIN) {
3469
- throw new Error(`Refusing cross-origin GitHub Models pagination URL: ${resolved.origin}`);
3470
- }
3471
- url = resolved.href;
3472
- } else {
3473
- url = null;
3474
- }
3475
- pages++;
3476
- }
3477
- const entries = raw.map((m) => {
3478
- if (typeof m === "string") {
3479
- return { id: m, name: m, contextWindow: DEFAULT_CONTEXT_WINDOW, vision: false, tools: false };
3480
- }
3481
- if (!m || typeof m !== "object") return void 0;
3482
- const o = m;
3483
- const id = pickString(o, ["id", "name", "model"]);
3484
- if (!id) return void 0;
3485
- return {
3486
- id,
3487
- name: pickString(o, ["friendly_name", "display_name", "name"]) ?? id,
3488
- contextWindow: pickNumber(o, [
3489
- "context_window",
3490
- "context_length",
3491
- "max_input_tokens",
3492
- "limits.max_input_tokens",
3493
- "limits.max_context_tokens"
3494
- ]) ?? DEFAULT_CONTEXT_WINDOW,
3495
- vision: looksVisionCapable(o),
3496
- tools: looksToolCapable(o)
3497
- };
3498
- }).filter((e) => e !== void 0);
3499
- if (entries.length === 0) return [];
3500
- const chat = entries.filter((e) => isChatModel(e.id));
3501
- return (chat.length ? chat : entries).map((e) => ({
3502
- id: e.id,
3503
- // full `owner/model` — what the API is addressed by
3504
- name: e.name,
3505
- provider: "github-models",
3506
- // GitHub's real per-request input cap, not the base model's larger one
3507
- // (see GITHUB_MODELS_MAX_INPUT_TOKENS above) — the same reasoning
3508
- // already applied to maxOutputTokens below, for the input side.
3509
- contextWindow: Math.min(e.contextWindow, GITHUB_MODELS_MAX_INPUT_TOKENS),
3510
- isVisionCapable: e.vision,
3511
- // A REAL $0, not a missing number — usage is bundled into the quota of
3512
- // the plan the PAT belongs to and is never billed per token, so
3513
- // `pricingUnknown: false` is the honest value (see the flag's contract in
3514
- // core/router/pricing.ts: 0 alone is ambiguous). Set here rather than in
3515
- // pricing-data.json because resolvePricing() matches exactly on
3516
- // provider|model|region with no wildcard: every catalog model would need
3517
- // a hand-curated row, and the next one GitHub adds would read as "cost
3518
- // unknown" — an inconsistent picture for a billing reality that is
3519
- // identical across the entire catalog.
3520
- //
3521
- // `isLocal` is the OTHER way to reach a real $0, and it is the wrong one:
3522
- // it means "runs on this machine" and carries consequences that are
3523
- // simply false for a hosted API — the shared concurrency-1
3524
- // LocalRequestQueue (serialising these calls behind actual Ollama GPU
3525
- // inference), the 300s local rather than 120s cloud inference timeout,
3526
- // and a literal "[local]" tag in `cascade models`.
3527
- inputCostPer1kTokens: 0,
3528
- outputCostPer1kTokens: 0,
3529
- pricingUnknown: false,
3530
- maxOutputTokens: GITHUB_MODELS_MAX_OUTPUT_TOKENS,
3531
- supportsStreaming: true,
3532
- // Not every catalog model supports function/tool calling — a hardcoded
3533
- // `true` sent `tools` to models that reject the parameter outright and
3534
- // never engaged t3-worker.ts's existing text-tool fallback (only an
3535
- // explicit `false` does). See looksToolCapable's own comment for why
3536
- // "not positively advertised" defaults to false rather than true here.
3537
- supportsToolUse: e.tools,
3538
- isLocal: false
3539
- }));
3540
- }
3541
- async isAvailable() {
3542
- try {
3543
- const res = await nodeHttpFetch(GITHUB_MODELS_CATALOG_URL, { headers: this.catalogHeaders() }, 0, {
3544
- allowedRedirectOrigin: GITHUB_MODELS_CATALOG_ORIGIN
3545
- });
3546
- return res.ok;
3547
- } catch {
3548
- return false;
3549
- }
3550
- }
3551
- };
3552
-
3553
3349
  // src/providers/ollama.ts
3554
3350
  var TOOL_CAPABLE_FAMILIES = [
3555
3351
  "llama3.1",
@@ -4058,7 +3854,7 @@ var ModelSelector = class {
4058
3854
  if (overrideModelId.includes(":")) {
4059
3855
  const parts = overrideModelId.split(":");
4060
3856
  const prefix = parts[0].toLowerCase();
4061
- const validProviders = ["anthropic", "openai", "gemini", "azure", "openai-compatible", "ollama", "github-models"];
3857
+ const validProviders = ["anthropic", "openai", "gemini", "azure", "openai-compatible", "ollama"];
4062
3858
  if (validProviders.includes(prefix)) {
4063
3859
  providerStr = prefix;
4064
3860
  actualId = parts.slice(1).join(":");
@@ -4078,31 +3874,6 @@ var ModelSelector = class {
4078
3874
  else if (this.availableProviders.has("openai-compatible")) providerStr = "openai-compatible";
4079
3875
  else if (this.availableProviders.size === 1) providerStr = Array.from(this.availableProviders)[0];
4080
3876
  }
4081
- if (providerStr === "github-models" && this.availableProviders.has("github-models")) {
4082
- const dynamicModel = {
4083
- id: actualId,
4084
- name: actualId,
4085
- provider: "github-models",
4086
- contextWindow: GITHUB_MODELS_MAX_INPUT_TOKENS,
4087
- isVisionCapable: false,
4088
- inputCostPer1kTokens: 0,
4089
- outputCostPer1kTokens: 0,
4090
- pricingUnknown: false,
4091
- maxOutputTokens: GITHUB_MODELS_MAX_OUTPUT_TOKENS,
4092
- supportsStreaming: true,
4093
- // Unconfirmed (no live catalog entry to read a real capability from)
4094
- // must default to false, not left undefined: t3-worker.ts's text-tool
4095
- // fallback only engages on a strict `=== false`, so an undefined
4096
- // value here sends native `tools` to an unverified multi-vendor
4097
- // model and any that don't support function calling reject the
4098
- // request outright. Same default-to-false-when-unconfirmed policy
4099
- // listModels() already applies for a real catalog entry.
4100
- supportsToolUse: false,
4101
- isLocal: false
4102
- };
4103
- this.addDynamicModel(dynamicModel);
4104
- return dynamicModel;
4105
- }
4106
3877
  if (providerStr && this.availableProviders.has(providerStr)) {
4107
3878
  const dynamicModel = withResolvedPricing({
4108
3879
  id: actualId,
@@ -4195,20 +3966,7 @@ var DEFAULT_PROVIDER_TPM = {
4195
3966
  gemini: 6e4,
4196
3967
  azure: 3e4,
4197
3968
  "openai-compatible": 3e4,
4198
- ollama: Number.POSITIVE_INFINITY,
4199
- // An order of magnitude below the others, on purpose. GitHub Models is
4200
- // rate-limited by REQUESTS (Free ≈10 RPM, Pro ≈20, plus low daily caps), and
4201
- // this bucket meters TOKENS — there is no request-count limiter in this
4202
- // codebase, and building one is not in scope. The approximation works because
4203
- // the router reserves `model.maxOutputTokens + 512` per call, and the
4204
- // provider reports GitHub's real ~4K per-request cap rather than the
4205
- // underlying model's larger one — so every call withdraws a near-constant
4206
- // ~4.5K regardless of how short the reply actually turns out to be. At 8K/min
4207
- // that lands around 1–2 calls per minute of headroom: comfortably inside even
4208
- // the Free tier, with room for the daily cap to matter more than this does.
4209
- // Users on a higher tier can raise it with no code change through the generic
4210
- // `config.rateLimits.providerTpm['github-models']` override.
4211
- "github-models": 8e3
3969
+ ollama: Number.POSITIVE_INFINITY
4212
3970
  };
4213
3971
  var TpmLimiter = class {
4214
3972
  buckets = /* @__PURE__ */ new Map();
@@ -4564,7 +4322,6 @@ var ModelProfiler = class {
4564
4322
  (m) => !alreadyProfiled.has(m.id) && !SKIP_PATTERN.test(m.id) && !SKIP_PATTERN.test(m.name)
4565
4323
  );
4566
4324
  if (toProfile.length === 0) return;
4567
- const probesHitGithubModels = this.router?.getModelForTier("T3")?.provider === "github-models";
4568
4325
  const openRouterModels = await fetchOpenRouterModels();
4569
4326
  const orByNormalizedId = /* @__PURE__ */ new Map();
4570
4327
  for (const m of openRouterModels) {
@@ -4579,7 +4336,7 @@ var ModelProfiler = class {
4579
4336
  if (orMatch?.description) {
4580
4337
  specializations = extractSpecializations(orMatch.description);
4581
4338
  }
4582
- if (specializations.length === 0 && this.router && !probesHitGithubModels) {
4339
+ if (specializations.length === 0 && this.router) {
4583
4340
  specializations = await queryModelDirectly(this.router);
4584
4341
  }
4585
4342
  this.store.saveModelProfile(model.id, model.provider, specializations);
@@ -4799,7 +4556,6 @@ var LiveDataProvider = class {
4799
4556
  */
4800
4557
  applyLivePricing(models) {
4801
4558
  return models.map((m) => {
4802
- if (m.provider === "github-models") return m;
4803
4559
  const p = this.getLivePrice(m.baseModelId ?? m.id);
4804
4560
  if (!p) return m;
4805
4561
  const reconciled = reconcilePrice(m, p);
@@ -4838,7 +4594,6 @@ var LiveDataProvider = class {
4838
4594
  */
4839
4595
  applyLiveCapabilities(models) {
4840
4596
  return models.map((m) => {
4841
- if (m.provider === "github-models") return m;
4842
4597
  const c = this.getCapability(m.baseModelId ?? m.id);
4843
4598
  if (!c) return m;
4844
4599
  const next = { ...m };
@@ -5142,16 +4897,13 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
5142
4897
  for (const model of azureDeployments) this.selector.addDynamicModel(model);
5143
4898
  availableProviders.add("azure");
5144
4899
  }
5145
- if (availableProviders.has("github-models")) {
5146
- await this.discoverGitHubModelsCatalog(config);
5147
- }
5148
4900
  await this.validateCloudProviderModels(config);
5149
4901
  for (const tier of ["T1", "T2", "T3"]) {
5150
4902
  const override = tier === "T1" ? config.models.t1 : tier === "T2" ? config.models.t2 : config.models.t3;
5151
4903
  if (!override || override === "auto") continue;
5152
4904
  const model = this.selector.selectForTier(tier, override);
5153
4905
  if (!model) {
5154
- const knownProviders = ["anthropic", "openai", "gemini", "azure", "openai-compatible", "ollama", "github-models"];
4906
+ const knownProviders = ["anthropic", "openai", "gemini", "azure", "openai-compatible", "ollama"];
5155
4907
  const hasProviderPrefix = override.includes(":") && knownProviders.some((p) => override.startsWith(p + ":"));
5156
4908
  if (hasProviderPrefix) {
5157
4909
  const provider = override.split(":")[0];
@@ -5306,7 +5058,7 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
5306
5058
  * models without a package upgrade. Mirrors discoverOllamaModels.
5307
5059
  */
5308
5060
  async discoverProviderModels() {
5309
- const cloud = ["anthropic", "openai", "gemini", "azure", "openai-compatible", "github-models"];
5061
+ const cloud = ["anthropic", "openai", "gemini", "azure", "openai-compatible"];
5310
5062
  const tasks = cloud.map(async (type) => {
5311
5063
  if (!this.selector.isProviderAvailable(type)) return;
5312
5064
  const seed = this.getAnyModelForProvider(type);
@@ -5821,26 +5573,6 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
5821
5573
  return false;
5822
5574
  }
5823
5575
  }
5824
- /**
5825
- * Registers GitHub Models' real catalog ids via addDynamicModel so init()'s
5826
- * tier-fill loop can pick one. Registration alone does not create a bound
5827
- * BaseProvider instance (see ensureProvider) — the tier-fill loop's own
5828
- * ensureProvider call does that for whichever model it actually selects,
5829
- * which is why this runs BEFORE that loop rather than only in the
5830
- * background discoverProviderModels() refresh.
5831
- */
5832
- async discoverGitHubModelsCatalog(config) {
5833
- const cfg = config.providers.find((p) => p.type === "github-models") ?? { type: "github-models" };
5834
- const seed = this.getAnyModelForProvider("github-models");
5835
- if (!seed) return;
5836
- try {
5837
- const provider = new GitHubModelsProvider(cfg, seed);
5838
- const models = await provider.listModels();
5839
- for (const m of models) this.selector.addDynamicModel(m);
5840
- } catch (err) {
5841
- console.warn("[router] GitHub Models catalog discovery failed:", err instanceof Error ? err.message : err);
5842
- }
5843
- }
5844
5576
  ensureProvider(model, configs) {
5845
5577
  const key = `${model.provider}:${model.id}`;
5846
5578
  if (this.providers.has(key)) return;
@@ -5865,8 +5597,6 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
5865
5597
  return new OllamaProvider(cfg, model);
5866
5598
  case "openai-compatible":
5867
5599
  return new OpenAICompatibleProvider(cfg, model);
5868
- case "github-models":
5869
- return new GitHubModelsProvider(cfg, model);
5870
5600
  default:
5871
5601
  throw new Error(`Unsupported provider type: ${String(cfg.type)}`);
5872
5602
  }
@@ -5874,7 +5604,7 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
5874
5604
  getAnyModelForProvider(type) {
5875
5605
  const fromCatalog = Object.values(MODELS).find((m) => m.provider === type);
5876
5606
  if (fromCatalog) return fromCatalog;
5877
- if (type === "openai-compatible" || type === "azure" || type === "github-models") {
5607
+ if (type === "openai-compatible" || type === "azure") {
5878
5608
  return {
5879
5609
  id: type,
5880
5610
  name: type,
@@ -14007,7 +13737,7 @@ var PermissionEscalator = class extends EventEmitter {
14007
13737
  }
14008
13738
  };
14009
13739
  var ProviderConfigSchema = z.object({
14010
- type: z.enum(["anthropic", "openai", "gemini", "azure", "openai-compatible", "ollama", "github-models"]),
13740
+ type: z.enum(["anthropic", "openai", "gemini", "azure", "openai-compatible", "ollama"]),
14011
13741
  label: z.string().optional(),
14012
13742
  apiKey: z.string().optional(),
14013
13743
  baseUrl: z.string().url().optional(),
@@ -19283,6 +19013,19 @@ Original error: ${err.message}`
19283
19013
  const rows = provider ? this.db.prepare("SELECT metadata FROM model_cache WHERE provider = ?").all(provider) : this.db.prepare("SELECT metadata FROM model_cache").all();
19284
19014
  return rows.map((r) => JSON.parse(r.metadata));
19285
19015
  }
19016
+ /**
19017
+ * Deletes cached rows for a provider type that no longer exists in
19018
+ * `ProviderType`, so it takes a raw string rather than the union.
19019
+ *
19020
+ * Needed because the cache outlives the code: the REPL's loadCache() only
19021
+ * re-discovers when the cache is EMPTY or older than 24h, so rows left
19022
+ * behind by a removed provider read as "cache populated" and suppress
19023
+ * discovery for the providers that replaced it — `/model` then shows a
19024
+ * configured provider with zero models until the rows age out.
19025
+ */
19026
+ purgeCachedModelsForRetiredProvider(providerType) {
19027
+ return this.db.prepare("DELETE FROM model_cache WHERE provider = ?").run(providerType).changes;
19028
+ }
19286
19029
  clearModelCache(provider) {
19287
19030
  if (provider) {
19288
19031
  this.db.prepare("DELETE FROM model_cache WHERE provider = ?").run(provider);
@@ -19630,7 +19373,73 @@ function mergeGlobalCredentials(workspaceProviders, globalProviders) {
19630
19373
  return merged;
19631
19374
  }
19632
19375
 
19376
+ // src/config/retired-providers.ts
19377
+ var RETIRED_PROVIDER_TYPES = {
19378
+ "github-models": "GitHub retired GitHub Models on 30 July 2026; models.github.ai no longer responds."
19379
+ };
19380
+ function isRetiredProviderType(type) {
19381
+ return typeof type === "string" && Object.hasOwn(RETIRED_PROVIDER_TYPES, type);
19382
+ }
19383
+ function didCleanupChangeAnything(c) {
19384
+ return c.removed.length > 0 || c.clearedPins.length > 0;
19385
+ }
19386
+ function stripRetiredProviders(raw) {
19387
+ const cleanup = { removed: [], clearedPins: [] };
19388
+ if (typeof raw !== "object" || raw === null) return cleanup;
19389
+ const cfg = raw;
19390
+ if (Array.isArray(cfg["providers"])) {
19391
+ const kept = cfg["providers"].filter((p) => {
19392
+ const type = p?.type;
19393
+ if (!isRetiredProviderType(type)) return true;
19394
+ if (!cleanup.removed.includes(type)) cleanup.removed.push(type);
19395
+ return false;
19396
+ });
19397
+ cfg["providers"] = kept;
19398
+ }
19399
+ cleanup.clearedPins = clearRetiredPins(cfg["models"]);
19400
+ return cleanup;
19401
+ }
19402
+ function clearRetiredPins(models) {
19403
+ const cleared = [];
19404
+ if (typeof models !== "object" || models === null) return cleared;
19405
+ const tiers = models;
19406
+ for (const tier of ["t1", "t2", "t3"]) {
19407
+ const pin = tiers[tier];
19408
+ if (typeof pin !== "string") continue;
19409
+ const prefix = pin.slice(0, pin.indexOf(":")).toLowerCase();
19410
+ if (pin.includes(":") && isRetiredProviderType(prefix)) {
19411
+ delete tiers[tier];
19412
+ cleared.push(tier);
19413
+ }
19414
+ }
19415
+ return cleared;
19416
+ }
19417
+ function filterRetiredCredentials(providers) {
19418
+ const removed = [];
19419
+ const kept = providers.filter((p) => {
19420
+ if (!isRetiredProviderType(p.type)) return true;
19421
+ if (!removed.includes(p.type)) removed.push(p.type);
19422
+ return false;
19423
+ });
19424
+ return { kept, removed };
19425
+ }
19426
+ function describeCleanup(c) {
19427
+ const parts = [];
19428
+ for (const type of c.removed) {
19429
+ parts.push(`removed the retired "${type}" provider (${RETIRED_PROVIDER_TYPES[type]})`);
19430
+ }
19431
+ if (c.clearedPins.length > 0) {
19432
+ parts.push(`reset ${c.clearedPins.map((t) => t.toUpperCase()).join("/")} to Auto, since the pin named it`);
19433
+ }
19434
+ return `Cascade config migration: ${parts.join("; ")}.`;
19435
+ }
19436
+
19633
19437
  // src/config/index.ts
19438
+ var KEY_OPTIONAL_PROVIDER_TYPES = /* @__PURE__ */ new Set(["ollama", "openai-compatible"]);
19439
+ function hasUsableProvider(providers) {
19440
+ if (!providers?.length) return false;
19441
+ return providers.some((p) => KEY_OPTIONAL_PROVIDER_TYPES.has(p.type) || !!p.apiKey || !!p.authToken);
19442
+ }
19634
19443
  var ConfigManager = class {
19635
19444
  config;
19636
19445
  keystore;
@@ -19639,6 +19448,29 @@ var ConfigManager = class {
19639
19448
  cascadeMd = null;
19640
19449
  workspacePath;
19641
19450
  globalDir;
19451
+ /**
19452
+ * What THIS load migrated out, if anything. Strictly per-load state: reset
19453
+ * at the top of every `load()`, because two places read it as a statement
19454
+ * about the load in progress — `injectEnvKeys()` asks whether an empty
19455
+ * provider list was emptied by a retirement, and the end of `load()` warns
19456
+ * and builds the user-facing notice from it. Left set, a second `load()` on
19457
+ * the same instance (`startRepl()` does one after its setup wizard) would
19458
+ * re-announce a migration that already happened and suppress the Ollama
19459
+ * fallback for a list that is empty for some unrelated reason.
19460
+ */
19461
+ retiredCleanup;
19462
+ /**
19463
+ * Human-readable migration notice held for a UI to display. `console.warn`
19464
+ * is not sufficient: the REPL clears the TTY immediately after load(), and
19465
+ * the desktop emits from the main process where nothing is rendered.
19466
+ */
19467
+ pendingRetiredNotice;
19468
+ /** Returns the pending migration notice once, then forgets it. */
19469
+ takeRetiredNotice() {
19470
+ const n = this.pendingRetiredNotice;
19471
+ this.pendingRetiredNotice = void 0;
19472
+ return n;
19473
+ }
19642
19474
  /** `globalDirOverride` exists for tests — never point it at the real home dir there. */
19643
19475
  constructor(workspacePath = process.cwd(), globalDirOverride) {
19644
19476
  this.workspacePath = workspacePath;
@@ -19658,10 +19490,32 @@ var ConfigManager = class {
19658
19490
  this.cascadeMd = await loadCascadeMd(this.workspacePath);
19659
19491
  this.keystore = new Keystore(path26.join(this.globalDir, GLOBAL_KEYSTORE_FILE));
19660
19492
  this.store = new MemoryStore(path26.join(this.workspacePath, CASCADE_DB_FILE));
19493
+ const globalCreds = filterRetiredCredentials(loadGlobalCredentials(this.globalDir));
19494
+ if (globalCreds.removed.length > 0) {
19495
+ try {
19496
+ saveGlobalCredentials(this.globalDir, globalCreds.kept);
19497
+ } catch (err) {
19498
+ console.warn(`Could not rewrite the global credential store: ${err instanceof Error ? err.message : String(err)}`);
19499
+ }
19500
+ this.retiredCleanup = {
19501
+ removed: [.../* @__PURE__ */ new Set([...this.retiredCleanup?.removed ?? [], ...globalCreds.removed])],
19502
+ clearedPins: this.retiredCleanup?.clearedPins ?? []
19503
+ };
19504
+ }
19661
19505
  await this.injectEnvKeys();
19662
- this.config.providers = mergeGlobalCredentials(this.config.providers, loadGlobalCredentials(this.globalDir));
19506
+ this.config.providers = mergeGlobalCredentials(this.config.providers, globalCreds.kept);
19507
+ for (const type of Object.keys(RETIRED_PROVIDER_TYPES)) {
19508
+ try {
19509
+ this.store.purgeCachedModelsForRetiredProvider(type);
19510
+ } catch {
19511
+ }
19512
+ }
19663
19513
  await this.ensureDefaultIdentity();
19664
19514
  if (mcpNamesChanged) await this.save();
19515
+ if (this.retiredCleanup) {
19516
+ console.warn(describeCleanup(this.retiredCleanup));
19517
+ this.pendingRetiredNotice = describeCleanup(this.retiredCleanup);
19518
+ }
19665
19519
  }
19666
19520
  getConfig() {
19667
19521
  return this.config;
@@ -19700,14 +19554,13 @@ var ConfigManager = class {
19700
19554
  anthropic: "ANTHROPIC_API_KEY",
19701
19555
  openai: "OPENAI_API_KEY",
19702
19556
  gemini: "GOOGLE_API_KEY",
19703
- azure: "AZURE_OPENAI_KEY",
19557
+ azure: "AZURE_OPENAI_KEY"
19704
19558
  // Deliberately NOT `GITHUB_TOKEN`: that one is injected automatically by
19705
19559
  // GitHub Actions and is commonly exported in developer shells for `gh`
19706
19560
  // and git auth, and it will essentially never carry the fine-grained
19707
19561
  // `models: read` scope this provider needs. Adopting it would wire up a
19708
19562
  // provider that 403s on its first real call — a confusing failure for
19709
19563
  // something that is meant to be explicit opt-in BYOK.
19710
- "github-models": "GITHUB_MODELS_TOKEN"
19711
19564
  };
19712
19565
  const envKey = envMap[provider];
19713
19566
  if (envKey && process.env[envKey]) return process.env[envKey];
@@ -19719,10 +19572,21 @@ var ConfigManager = class {
19719
19572
  return configProvider?.apiKey;
19720
19573
  }
19721
19574
  async loadConfig() {
19575
+ this.retiredCleanup = void 0;
19722
19576
  const configPath = path26.join(this.workspacePath, CASCADE_CONFIG_FILE);
19723
19577
  try {
19724
19578
  const raw = await fs5.readFile(configPath, "utf-8");
19725
- return validateConfig(JSON.parse(raw));
19579
+ const parsed = JSON.parse(raw);
19580
+ const cleanup = stripRetiredProviders(parsed);
19581
+ if (didCleanupChangeAnything(cleanup)) {
19582
+ this.retiredCleanup = cleanup;
19583
+ try {
19584
+ await fs5.writeFile(configPath, JSON.stringify(parsed, null, 2), "utf-8");
19585
+ } catch (err) {
19586
+ console.warn(`Could not persist the retired-provider migration: ${err instanceof Error ? err.message : String(err)}`);
19587
+ }
19588
+ }
19589
+ return validateConfig(parsed);
19726
19590
  } catch (err) {
19727
19591
  if (err.code === "ENOENT") {
19728
19592
  return validateConfig({});
@@ -19731,27 +19595,25 @@ var ConfigManager = class {
19731
19595
  }
19732
19596
  }
19733
19597
  async injectEnvKeys() {
19734
- const isFirstRun = this.config.providers.length === 0;
19598
+ const wasEmpty = this.config.providers.length === 0;
19599
+ const emptiedByRetirement = wasEmpty && !!this.retiredCleanup;
19735
19600
  const envProviders = [
19736
19601
  { env: "ANTHROPIC_API_KEY", type: "anthropic" },
19737
19602
  { env: "OPENAI_API_KEY", type: "openai" },
19738
19603
  { env: "GOOGLE_API_KEY", type: "gemini" },
19739
- { env: "AZURE_OPENAI_KEY", type: "azure" },
19740
- // See getApiKey() above for why this is GITHUB_MODELS_TOKEN and never
19741
- // the ambient GITHUB_TOKEN.
19742
- { env: "GITHUB_MODELS_TOKEN", type: "github-models" }
19604
+ { env: "AZURE_OPENAI_KEY", type: "azure" }
19743
19605
  ];
19744
19606
  for (const { env, type } of envProviders) {
19745
19607
  const key = process.env[env];
19746
19608
  if (!key) continue;
19747
19609
  const existing = this.config.providers.find((p) => p.type === type);
19748
- if (!existing && isFirstRun) {
19610
+ if (!existing && wasEmpty) {
19749
19611
  this.config.providers.push({ type, apiKey: key });
19750
19612
  } else if (existing && !existing.apiKey) {
19751
19613
  existing.apiKey = key;
19752
19614
  }
19753
19615
  }
19754
- if (isFirstRun && !this.config.providers.find((p) => p.type === "ollama")) {
19616
+ if (wasEmpty && !emptiedByRetirement && !this.config.providers.find((p) => p.type === "ollama")) {
19755
19617
  this.config.providers.push({ type: "ollama" });
19756
19618
  }
19757
19619
  }
@@ -22388,7 +22250,9 @@ function gatherSyncBundle(config) {
22388
22250
  if (Object.keys(prefs).length) bundle.prefs = prefs;
22389
22251
  return bundle;
22390
22252
  }
22391
- function applySyncBundle(bundle, config) {
22253
+ function applySyncBundle(bundle, config, cleanup) {
22254
+ const dropped = stripRetiredProviders(bundle);
22255
+ if (cleanup) cleanup.removed = dropped.removed;
22392
22256
  const next = { ...config, tools: { ...config.tools } };
22393
22257
  if (bundle.providers) next.providers = mergeByKey(config.providers, bundle.providers, providerSig) ?? [];
22394
22258
  if (bundle.webSearch) next.tools.webSearch = { ...config.tools?.webSearch, ...bundle.webSearch };
@@ -22404,6 +22268,14 @@ function applySyncBundle(bundle, config) {
22404
22268
  }
22405
22269
  if (bundle.models) next.models = { ...config.models, ...bundle.models };
22406
22270
  if (bundle.budget) next.budget = { ...config.budget, ...bundle.budget };
22271
+ const merged = { ...next.models };
22272
+ const clearedFromMerge = clearRetiredPins(merged);
22273
+ if (clearedFromMerge.length > 0) next.models = merged;
22274
+ if (cleanup) {
22275
+ cleanup.clearedPins = ["t1", "t2", "t3"].filter(
22276
+ (t) => (dropped.clearedPins.includes(t) || clearedFromMerge.includes(t)) && next.models?.[t] === void 0
22277
+ );
22278
+ }
22407
22279
  if (bundle.prefs) {
22408
22280
  const p = bundle.prefs;
22409
22281
  if (p.autoBias !== void 0) next.autoBias = p.autoBias;
@@ -22414,6 +22286,6 @@ function applySyncBundle(bundle, config) {
22414
22286
  return next;
22415
22287
  }
22416
22288
 
22417
- export { AZURE_BASE_MODELS, AZURE_BASE_URL_TEMPLATE, AuditLogger, CASCADE_AUDIT_FILE, CASCADE_CONFIG_DIR, CASCADE_CONFIG_FILE, CASCADE_DASHBOARD_SECRET_FILE, CASCADE_DB_FILE, CASCADE_IGNORE_FILE, CASCADE_KEYSTORE_FILE, CASCADE_MD_FILE, CASCADE_VERSION, CHARS_PER_TOKEN, COMPLEXITY_T2_COUNT, Cascade, CascadeCancelledError, CascadeIgnore, CascadeRouter, CascadeToolError, CloudClient, CodeSearchTool, ConfigManager, CurrentPageTool, DEFAULT_API_PORT, DEFAULT_APPROVAL_REQUIRED, DEFAULT_AUTO_SUMMARIZE_AT, DEFAULT_CLOUD_URL, DEFAULT_CONTEXT_LIMIT, DEFAULT_DASHBOARD_PORT, DEFAULT_MAX_SESSION_MESSAGES, DEFAULT_RETENTION_DAYS, DEFAULT_THEME, DOCUMENT_MIME, DashboardServer, DependencyScheduler, FileMcpOAuthStore, GITHUB_MODELS_API_VERSION, GITHUB_MODELS_CATALOG_URL, GITHUB_MODELS_INFERENCE_URL, GLOBAL_CONFIG_DIR, GLOBAL_CREDENTIALS_FILE, GLOBAL_DB_FILE, GLOBAL_KEYSTORE_FILE, GLOBAL_RUNTIME_DB_FILE, GenerateDocumentTool, GraphRetriever, GraphSearchTool, HooksRunner, Keystore, LLMReranker, LM_STUDIO_BASE_URL, MAX_PAGE_TEXT_CHARS, MCP_TOOL_PREFIX, MODELS, McpClient, McpOAuthProvider, MemoryStore, OLLAMA_BASE_URL, OpenAICompatibleEmbedder, PROVIDER_DISPLAY_NAMES, RESUME_SCHEMA_VERSION, ResumeStore, Retriever, SqliteVectorStore, T1Administrator, T1_MODEL_PRIORITY, T2Manager, T2_MODEL_PRIORITY, T3Worker, T3_MODEL_PRIORITY, THEME_NAMES, TOOL_NAMES, TaskGraphValidationError, TaskScheduler, Telemetry, ToolRegistry, VISION_MODEL_PRIORITY, WorkspaceIndex, applySyncBundle, azureModelForDeployment, beginMcpOAuth, buildDocumentTools, buildManifest, buildSessionTranscript, bytesToBase64, cagCharBudget, chartKind, chartToTableRows, chatCompleterFromProviders, chunkCode, chunkText2 as chunkText, compileSectionGraph, compileSubtaskGraph, compileTaskGraph, completeMcpOAuth, connectMcpWithLoopbackOAuth, createCascade, decryptJSON as decryptSyncBlob, diffManifest, disambiguateMcpServerNames, discoverMcpAuthServer, discoverMcpTools, distillSessionFacts, embedderFromProviders, encryptJSON as encryptSyncBlob, evaluateAcceptance, extractCharts, failures, fileExt, fileOAuthProvider, gatherSyncBundle, hashContent, heuristicCodeChunker, inferAzureBaseModel, inlineRuns, isDocumentFormat, isMcpToolName, matchImageLine, mcpServerPrefix, mcpToolName, nodeHttpFetch, parseBlocks, parseChartSpec, parseDelimited, parseRankOrder, parseSlide, planRetrieval, preferIpv4Host, reciprocalRankFusion, refreshMcpToken, removeMcpServerDenials, renderDocument, renderDocx, renderPptx, renderXlsx, runCascade, sessionWorthRemembering, sniffImage, splitSlides, streamCascade, stripInline, summarizeCompleted, undecided, uniqueMcpServerName };
22289
+ export { AZURE_BASE_MODELS, AZURE_BASE_URL_TEMPLATE, AuditLogger, CASCADE_AUDIT_FILE, CASCADE_CONFIG_DIR, CASCADE_CONFIG_FILE, CASCADE_DASHBOARD_SECRET_FILE, CASCADE_DB_FILE, CASCADE_IGNORE_FILE, CASCADE_KEYSTORE_FILE, CASCADE_MD_FILE, CASCADE_VERSION, CHARS_PER_TOKEN, COMPLEXITY_T2_COUNT, Cascade, CascadeCancelledError, CascadeIgnore, CascadeRouter, CascadeToolError, CloudClient, CodeSearchTool, ConfigManager, CurrentPageTool, DEFAULT_API_PORT, DEFAULT_APPROVAL_REQUIRED, DEFAULT_AUTO_SUMMARIZE_AT, DEFAULT_CLOUD_URL, DEFAULT_CONTEXT_LIMIT, DEFAULT_DASHBOARD_PORT, DEFAULT_MAX_SESSION_MESSAGES, DEFAULT_RETENTION_DAYS, DEFAULT_THEME, DOCUMENT_MIME, DashboardServer, DependencyScheduler, FileMcpOAuthStore, GLOBAL_CONFIG_DIR, GLOBAL_CREDENTIALS_FILE, GLOBAL_DB_FILE, GLOBAL_KEYSTORE_FILE, GLOBAL_RUNTIME_DB_FILE, GenerateDocumentTool, GraphRetriever, GraphSearchTool, HooksRunner, Keystore, LLMReranker, LM_STUDIO_BASE_URL, MAX_PAGE_TEXT_CHARS, MCP_TOOL_PREFIX, MODELS, McpClient, McpOAuthProvider, MemoryStore, OLLAMA_BASE_URL, OpenAICompatibleEmbedder, PROVIDER_DISPLAY_NAMES, RESUME_SCHEMA_VERSION, ResumeStore, Retriever, SqliteVectorStore, T1Administrator, T1_MODEL_PRIORITY, T2Manager, T2_MODEL_PRIORITY, T3Worker, T3_MODEL_PRIORITY, THEME_NAMES, TOOL_NAMES, TaskGraphValidationError, TaskScheduler, Telemetry, ToolRegistry, VISION_MODEL_PRIORITY, WorkspaceIndex, applySyncBundle, azureModelForDeployment, beginMcpOAuth, buildDocumentTools, buildManifest, buildSessionTranscript, bytesToBase64, cagCharBudget, chartKind, chartToTableRows, chatCompleterFromProviders, chunkCode, chunkText2 as chunkText, compileSectionGraph, compileSubtaskGraph, compileTaskGraph, completeMcpOAuth, connectMcpWithLoopbackOAuth, createCascade, decryptJSON as decryptSyncBlob, diffManifest, disambiguateMcpServerNames, discoverMcpAuthServer, discoverMcpTools, distillSessionFacts, embedderFromProviders, encryptJSON as encryptSyncBlob, evaluateAcceptance, extractCharts, failures, fileExt, fileOAuthProvider, gatherSyncBundle, hasUsableProvider, hashContent, heuristicCodeChunker, inferAzureBaseModel, inlineRuns, isDocumentFormat, isMcpToolName, matchImageLine, mcpServerPrefix, mcpToolName, nodeHttpFetch, parseBlocks, parseChartSpec, parseDelimited, parseRankOrder, parseSlide, planRetrieval, preferIpv4Host, reciprocalRankFusion, refreshMcpToken, removeMcpServerDenials, renderDocument, renderDocx, renderPptx, renderXlsx, runCascade, sessionWorthRemembering, sniffImage, splitSlides, streamCascade, stripInline, summarizeCompleted, undecided, uniqueMcpServerName };
22418
22290
  //# sourceMappingURL=index.js.map
22419
22291
  //# sourceMappingURL=index.js.map