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.cjs CHANGED
@@ -1908,7 +1908,7 @@ function reconcilePrice(model, live) {
1908
1908
  }
1909
1909
 
1910
1910
  // src/constants.ts
1911
- var CASCADE_VERSION = "0.69.0";
1911
+ var CASCADE_VERSION = "0.71.0";
1912
1912
  var CASCADE_CONFIG_DIR = ".cascade";
1913
1913
  var CASCADE_MD_FILE = "CASCADE.md";
1914
1914
  var CASCADE_IGNORE_FILE = ".cascadeignore";
@@ -2315,9 +2315,6 @@ var DEFAULT_THEME = "cascade";
2315
2315
  var OLLAMA_BASE_URL = "http://localhost:11434";
2316
2316
  var LM_STUDIO_BASE_URL = "http://localhost:1234";
2317
2317
  var AZURE_BASE_URL_TEMPLATE = "https://{resource}.openai.azure.com";
2318
- var GITHUB_MODELS_INFERENCE_URL = "https://models.github.ai/inference";
2319
- var GITHUB_MODELS_CATALOG_URL = "https://models.github.ai/catalog/models";
2320
- var GITHUB_MODELS_API_VERSION = "2026-03-10";
2321
2318
  var TOOL_NAMES = {
2322
2319
  SHELL: "shell",
2323
2320
  FILE_READ: "file_read",
@@ -2352,8 +2349,7 @@ var PROVIDER_DISPLAY_NAMES = {
2352
2349
  gemini: "Google Gemini",
2353
2350
  azure: "Azure OpenAI",
2354
2351
  "openai-compatible": "OpenAI-Compatible",
2355
- ollama: "Ollama (Local)",
2356
- "github-models": "GitHub Models"
2352
+ ollama: "Ollama (Local)"
2357
2353
  };
2358
2354
 
2359
2355
  // src/utils/cost.ts
@@ -3396,206 +3392,6 @@ async function nodeHttpFetch(input, init = {}, redirectCount = 0, opts = {}) {
3396
3392
  });
3397
3393
  }
3398
3394
 
3399
- // src/providers/github-models.ts
3400
- var GITHUB_MODELS_MAX_OUTPUT_TOKENS = 4e3;
3401
- var GITHUB_MODELS_MAX_INPUT_TOKENS = 8e3;
3402
- var DEFAULT_CONTEXT_WINDOW = 128e3;
3403
- var GITHUB_MODELS_MAX_CATALOG_PAGES = 20;
3404
- var GITHUB_MODELS_CATALOG_ORIGIN = new URL(GITHUB_MODELS_CATALOG_URL).origin;
3405
- function stripModelOwnerPrefix(id) {
3406
- const slash = (id || "").indexOf("/");
3407
- return slash === -1 ? id : id.slice(slash + 1);
3408
- }
3409
- function pickString(o, keys) {
3410
- for (const k of keys) {
3411
- const v = o[k];
3412
- if (typeof v === "string" && v.length > 0) return v;
3413
- }
3414
- return void 0;
3415
- }
3416
- function pickNumber(o, keys) {
3417
- for (const k of keys) {
3418
- const v = k.includes(".") ? o[k.split(".")[0]]?.[k.split(".")[1]] : o[k];
3419
- if (typeof v === "number" && Number.isFinite(v) && v > 0) return v;
3420
- }
3421
- return void 0;
3422
- }
3423
- function looksVisionCapable(o) {
3424
- for (const k of ["supported_input_modalities", "input_modalities", "modalities", "capabilities"]) {
3425
- const v = o[k];
3426
- if (Array.isArray(v) && v.some((x) => typeof x === "string" && /image|vision/i.test(x))) return true;
3427
- }
3428
- return false;
3429
- }
3430
- function looksToolCapable(o) {
3431
- const params = o["supported_parameters"];
3432
- if (Array.isArray(params) && params.some((x) => typeof x === "string" && /^tools?$|function.?call/i.test(x))) {
3433
- return true;
3434
- }
3435
- for (const k of ["capabilities", "supported_output_modalities"]) {
3436
- const v = o[k];
3437
- if (Array.isArray(v) && v.some((x) => typeof x === "string" && /tool|function.?call/i.test(x))) return true;
3438
- }
3439
- const caps = o["capabilities"];
3440
- if (caps && typeof caps === "object" && !Array.isArray(caps)) {
3441
- const c = caps;
3442
- if (c["function_calling"] === true || c["tool_calling"] === true || c["tools"] === true) return true;
3443
- }
3444
- return false;
3445
- }
3446
- function parseNextLink(linkHeader) {
3447
- if (!linkHeader) return null;
3448
- for (const part of linkHeader.split(",")) {
3449
- const match = part.match(/<([^>]+)>\s*;\s*rel="next"/);
3450
- if (match) return match[1];
3451
- }
3452
- return null;
3453
- }
3454
- var GitHubModelsProvider = class extends OpenAIProvider {
3455
- constructor(config, model) {
3456
- super({ ...config, apiKey: config.apiKey ?? "" }, model);
3457
- this.useMaxCompletionTokens = isReasoningModel(stripModelOwnerPrefix(model.id));
3458
- this.client = new OpenAI__default.default({
3459
- apiKey: config.apiKey ?? "",
3460
- baseURL: GITHUB_MODELS_INFERENCE_URL,
3461
- defaultHeaders: { "X-GitHub-Api-Version": GITHUB_MODELS_API_VERSION, "User-Agent": "Cascade-AI" }
3462
- });
3463
- }
3464
- /**
3465
- * `OpenAIProvider.generateStream()` builds its request budget as
3466
- * `options.maxTokens ?? this.model.maxOutputTokens` — an explicit per-call
3467
- * `maxTokens` (T1Administrator's final compilation asks for 8,000, well
3468
- * above GitHub's real ~4K hard cap) wins outright and is sent unclamped.
3469
- * GitHub then rejects the request instead of answering, so a run routed to
3470
- * this provider for that step fails outright rather than getting a
3471
- * (perfectly serviceable, just shorter) completion. Clamp here, once, right
3472
- * before delegating to the inherited implementation, rather than teaching
3473
- * every caller GitHub's specific cap.
3474
- */
3475
- async generateStream(options, onChunk) {
3476
- const clamped = options.maxTokens !== void 0 && options.maxTokens > GITHUB_MODELS_MAX_OUTPUT_TOKENS ? { ...options, maxTokens: GITHUB_MODELS_MAX_OUTPUT_TOKENS } : options;
3477
- return super.generateStream(clamped, onChunk);
3478
- }
3479
- /**
3480
- * The catalog is a GitHub REST resource, not an OpenAI `/models` list, and
3481
- * wants GitHub's own header trio — the plain `Accept: application/json` the
3482
- * openai-compatible provider sends is not the same request.
3483
- */
3484
- catalogHeaders() {
3485
- return {
3486
- Accept: "application/vnd.github+json",
3487
- Authorization: `Bearer ${this.config.apiKey ?? ""}`,
3488
- "X-GitHub-Api-Version": GITHUB_MODELS_API_VERSION,
3489
- // GitHub's REST API rejects any request with no User-Agent — a plain 403
3490
- // regardless of how valid the PAT is. nodeHttpFetch is a thin wrapper
3491
- // over node:http/https, neither of which sets one the way a browser's
3492
- // fetch or a full HTTP client library would, so this has to be explicit.
3493
- "User-Agent": "Cascade-AI"
3494
- };
3495
- }
3496
- async listModels() {
3497
- const raw = [];
3498
- let url = GITHUB_MODELS_CATALOG_URL;
3499
- let pages = 0;
3500
- const visited = /* @__PURE__ */ new Set();
3501
- while (url && pages < GITHUB_MODELS_MAX_CATALOG_PAGES) {
3502
- if (visited.has(url)) break;
3503
- visited.add(url);
3504
- const res = await nodeHttpFetch(url, { headers: this.catalogHeaders() }, 0, {
3505
- allowedRedirectOrigin: GITHUB_MODELS_CATALOG_ORIGIN
3506
- });
3507
- if (!res.ok) throw new Error(`GitHub Models catalog ${url} returned HTTP ${res.status}`);
3508
- const body = await res.json();
3509
- const pageItems = Array.isArray(body) ? body : Array.isArray(body?.models) ? body.models : Array.isArray(body?.data) ? body.data : [];
3510
- raw.push(...pageItems);
3511
- const next = parseNextLink(res.headers.get("link"));
3512
- if (next) {
3513
- const resolved = new URL(next, url);
3514
- if (resolved.origin !== GITHUB_MODELS_CATALOG_ORIGIN) {
3515
- throw new Error(`Refusing cross-origin GitHub Models pagination URL: ${resolved.origin}`);
3516
- }
3517
- url = resolved.href;
3518
- } else {
3519
- url = null;
3520
- }
3521
- pages++;
3522
- }
3523
- const entries = raw.map((m) => {
3524
- if (typeof m === "string") {
3525
- return { id: m, name: m, contextWindow: DEFAULT_CONTEXT_WINDOW, vision: false, tools: false };
3526
- }
3527
- if (!m || typeof m !== "object") return void 0;
3528
- const o = m;
3529
- const id = pickString(o, ["id", "name", "model"]);
3530
- if (!id) return void 0;
3531
- return {
3532
- id,
3533
- name: pickString(o, ["friendly_name", "display_name", "name"]) ?? id,
3534
- contextWindow: pickNumber(o, [
3535
- "context_window",
3536
- "context_length",
3537
- "max_input_tokens",
3538
- "limits.max_input_tokens",
3539
- "limits.max_context_tokens"
3540
- ]) ?? DEFAULT_CONTEXT_WINDOW,
3541
- vision: looksVisionCapable(o),
3542
- tools: looksToolCapable(o)
3543
- };
3544
- }).filter((e) => e !== void 0);
3545
- if (entries.length === 0) return [];
3546
- const chat = entries.filter((e) => isChatModel(e.id));
3547
- return (chat.length ? chat : entries).map((e) => ({
3548
- id: e.id,
3549
- // full `owner/model` — what the API is addressed by
3550
- name: e.name,
3551
- provider: "github-models",
3552
- // GitHub's real per-request input cap, not the base model's larger one
3553
- // (see GITHUB_MODELS_MAX_INPUT_TOKENS above) — the same reasoning
3554
- // already applied to maxOutputTokens below, for the input side.
3555
- contextWindow: Math.min(e.contextWindow, GITHUB_MODELS_MAX_INPUT_TOKENS),
3556
- isVisionCapable: e.vision,
3557
- // A REAL $0, not a missing number — usage is bundled into the quota of
3558
- // the plan the PAT belongs to and is never billed per token, so
3559
- // `pricingUnknown: false` is the honest value (see the flag's contract in
3560
- // core/router/pricing.ts: 0 alone is ambiguous). Set here rather than in
3561
- // pricing-data.json because resolvePricing() matches exactly on
3562
- // provider|model|region with no wildcard: every catalog model would need
3563
- // a hand-curated row, and the next one GitHub adds would read as "cost
3564
- // unknown" — an inconsistent picture for a billing reality that is
3565
- // identical across the entire catalog.
3566
- //
3567
- // `isLocal` is the OTHER way to reach a real $0, and it is the wrong one:
3568
- // it means "runs on this machine" and carries consequences that are
3569
- // simply false for a hosted API — the shared concurrency-1
3570
- // LocalRequestQueue (serialising these calls behind actual Ollama GPU
3571
- // inference), the 300s local rather than 120s cloud inference timeout,
3572
- // and a literal "[local]" tag in `cascade models`.
3573
- inputCostPer1kTokens: 0,
3574
- outputCostPer1kTokens: 0,
3575
- pricingUnknown: false,
3576
- maxOutputTokens: GITHUB_MODELS_MAX_OUTPUT_TOKENS,
3577
- supportsStreaming: true,
3578
- // Not every catalog model supports function/tool calling — a hardcoded
3579
- // `true` sent `tools` to models that reject the parameter outright and
3580
- // never engaged t3-worker.ts's existing text-tool fallback (only an
3581
- // explicit `false` does). See looksToolCapable's own comment for why
3582
- // "not positively advertised" defaults to false rather than true here.
3583
- supportsToolUse: e.tools,
3584
- isLocal: false
3585
- }));
3586
- }
3587
- async isAvailable() {
3588
- try {
3589
- const res = await nodeHttpFetch(GITHUB_MODELS_CATALOG_URL, { headers: this.catalogHeaders() }, 0, {
3590
- allowedRedirectOrigin: GITHUB_MODELS_CATALOG_ORIGIN
3591
- });
3592
- return res.ok;
3593
- } catch {
3594
- return false;
3595
- }
3596
- }
3597
- };
3598
-
3599
3395
  // src/providers/ollama.ts
3600
3396
  var TOOL_CAPABLE_FAMILIES = [
3601
3397
  "llama3.1",
@@ -4104,7 +3900,7 @@ var ModelSelector = class {
4104
3900
  if (overrideModelId.includes(":")) {
4105
3901
  const parts = overrideModelId.split(":");
4106
3902
  const prefix = parts[0].toLowerCase();
4107
- const validProviders = ["anthropic", "openai", "gemini", "azure", "openai-compatible", "ollama", "github-models"];
3903
+ const validProviders = ["anthropic", "openai", "gemini", "azure", "openai-compatible", "ollama"];
4108
3904
  if (validProviders.includes(prefix)) {
4109
3905
  providerStr = prefix;
4110
3906
  actualId = parts.slice(1).join(":");
@@ -4124,31 +3920,6 @@ var ModelSelector = class {
4124
3920
  else if (this.availableProviders.has("openai-compatible")) providerStr = "openai-compatible";
4125
3921
  else if (this.availableProviders.size === 1) providerStr = Array.from(this.availableProviders)[0];
4126
3922
  }
4127
- if (providerStr === "github-models" && this.availableProviders.has("github-models")) {
4128
- const dynamicModel = {
4129
- id: actualId,
4130
- name: actualId,
4131
- provider: "github-models",
4132
- contextWindow: GITHUB_MODELS_MAX_INPUT_TOKENS,
4133
- isVisionCapable: false,
4134
- inputCostPer1kTokens: 0,
4135
- outputCostPer1kTokens: 0,
4136
- pricingUnknown: false,
4137
- maxOutputTokens: GITHUB_MODELS_MAX_OUTPUT_TOKENS,
4138
- supportsStreaming: true,
4139
- // Unconfirmed (no live catalog entry to read a real capability from)
4140
- // must default to false, not left undefined: t3-worker.ts's text-tool
4141
- // fallback only engages on a strict `=== false`, so an undefined
4142
- // value here sends native `tools` to an unverified multi-vendor
4143
- // model and any that don't support function calling reject the
4144
- // request outright. Same default-to-false-when-unconfirmed policy
4145
- // listModels() already applies for a real catalog entry.
4146
- supportsToolUse: false,
4147
- isLocal: false
4148
- };
4149
- this.addDynamicModel(dynamicModel);
4150
- return dynamicModel;
4151
- }
4152
3923
  if (providerStr && this.availableProviders.has(providerStr)) {
4153
3924
  const dynamicModel = withResolvedPricing({
4154
3925
  id: actualId,
@@ -4241,20 +4012,7 @@ var DEFAULT_PROVIDER_TPM = {
4241
4012
  gemini: 6e4,
4242
4013
  azure: 3e4,
4243
4014
  "openai-compatible": 3e4,
4244
- ollama: Number.POSITIVE_INFINITY,
4245
- // An order of magnitude below the others, on purpose. GitHub Models is
4246
- // rate-limited by REQUESTS (Free ≈10 RPM, Pro ≈20, plus low daily caps), and
4247
- // this bucket meters TOKENS — there is no request-count limiter in this
4248
- // codebase, and building one is not in scope. The approximation works because
4249
- // the router reserves `model.maxOutputTokens + 512` per call, and the
4250
- // provider reports GitHub's real ~4K per-request cap rather than the
4251
- // underlying model's larger one — so every call withdraws a near-constant
4252
- // ~4.5K regardless of how short the reply actually turns out to be. At 8K/min
4253
- // that lands around 1–2 calls per minute of headroom: comfortably inside even
4254
- // the Free tier, with room for the daily cap to matter more than this does.
4255
- // Users on a higher tier can raise it with no code change through the generic
4256
- // `config.rateLimits.providerTpm['github-models']` override.
4257
- "github-models": 8e3
4015
+ ollama: Number.POSITIVE_INFINITY
4258
4016
  };
4259
4017
  var TpmLimiter = class {
4260
4018
  buckets = /* @__PURE__ */ new Map();
@@ -4610,7 +4368,6 @@ var ModelProfiler = class {
4610
4368
  (m) => !alreadyProfiled.has(m.id) && !SKIP_PATTERN.test(m.id) && !SKIP_PATTERN.test(m.name)
4611
4369
  );
4612
4370
  if (toProfile.length === 0) return;
4613
- const probesHitGithubModels = this.router?.getModelForTier("T3")?.provider === "github-models";
4614
4371
  const openRouterModels = await fetchOpenRouterModels();
4615
4372
  const orByNormalizedId = /* @__PURE__ */ new Map();
4616
4373
  for (const m of openRouterModels) {
@@ -4625,7 +4382,7 @@ var ModelProfiler = class {
4625
4382
  if (orMatch?.description) {
4626
4383
  specializations = extractSpecializations(orMatch.description);
4627
4384
  }
4628
- if (specializations.length === 0 && this.router && !probesHitGithubModels) {
4385
+ if (specializations.length === 0 && this.router) {
4629
4386
  specializations = await queryModelDirectly(this.router);
4630
4387
  }
4631
4388
  this.store.saveModelProfile(model.id, model.provider, specializations);
@@ -4845,7 +4602,6 @@ var LiveDataProvider = class {
4845
4602
  */
4846
4603
  applyLivePricing(models) {
4847
4604
  return models.map((m) => {
4848
- if (m.provider === "github-models") return m;
4849
4605
  const p = this.getLivePrice(m.baseModelId ?? m.id);
4850
4606
  if (!p) return m;
4851
4607
  const reconciled = reconcilePrice(m, p);
@@ -4884,7 +4640,6 @@ var LiveDataProvider = class {
4884
4640
  */
4885
4641
  applyLiveCapabilities(models) {
4886
4642
  return models.map((m) => {
4887
- if (m.provider === "github-models") return m;
4888
4643
  const c = this.getCapability(m.baseModelId ?? m.id);
4889
4644
  if (!c) return m;
4890
4645
  const next = { ...m };
@@ -5188,16 +4943,13 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
5188
4943
  for (const model of azureDeployments) this.selector.addDynamicModel(model);
5189
4944
  availableProviders.add("azure");
5190
4945
  }
5191
- if (availableProviders.has("github-models")) {
5192
- await this.discoverGitHubModelsCatalog(config);
5193
- }
5194
4946
  await this.validateCloudProviderModels(config);
5195
4947
  for (const tier of ["T1", "T2", "T3"]) {
5196
4948
  const override = tier === "T1" ? config.models.t1 : tier === "T2" ? config.models.t2 : config.models.t3;
5197
4949
  if (!override || override === "auto") continue;
5198
4950
  const model = this.selector.selectForTier(tier, override);
5199
4951
  if (!model) {
5200
- const knownProviders = ["anthropic", "openai", "gemini", "azure", "openai-compatible", "ollama", "github-models"];
4952
+ const knownProviders = ["anthropic", "openai", "gemini", "azure", "openai-compatible", "ollama"];
5201
4953
  const hasProviderPrefix = override.includes(":") && knownProviders.some((p) => override.startsWith(p + ":"));
5202
4954
  if (hasProviderPrefix) {
5203
4955
  const provider = override.split(":")[0];
@@ -5352,7 +5104,7 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
5352
5104
  * models without a package upgrade. Mirrors discoverOllamaModels.
5353
5105
  */
5354
5106
  async discoverProviderModels() {
5355
- const cloud = ["anthropic", "openai", "gemini", "azure", "openai-compatible", "github-models"];
5107
+ const cloud = ["anthropic", "openai", "gemini", "azure", "openai-compatible"];
5356
5108
  const tasks = cloud.map(async (type) => {
5357
5109
  if (!this.selector.isProviderAvailable(type)) return;
5358
5110
  const seed = this.getAnyModelForProvider(type);
@@ -5867,26 +5619,6 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
5867
5619
  return false;
5868
5620
  }
5869
5621
  }
5870
- /**
5871
- * Registers GitHub Models' real catalog ids via addDynamicModel so init()'s
5872
- * tier-fill loop can pick one. Registration alone does not create a bound
5873
- * BaseProvider instance (see ensureProvider) — the tier-fill loop's own
5874
- * ensureProvider call does that for whichever model it actually selects,
5875
- * which is why this runs BEFORE that loop rather than only in the
5876
- * background discoverProviderModels() refresh.
5877
- */
5878
- async discoverGitHubModelsCatalog(config) {
5879
- const cfg = config.providers.find((p) => p.type === "github-models") ?? { type: "github-models" };
5880
- const seed = this.getAnyModelForProvider("github-models");
5881
- if (!seed) return;
5882
- try {
5883
- const provider = new GitHubModelsProvider(cfg, seed);
5884
- const models = await provider.listModels();
5885
- for (const m of models) this.selector.addDynamicModel(m);
5886
- } catch (err) {
5887
- console.warn("[router] GitHub Models catalog discovery failed:", err instanceof Error ? err.message : err);
5888
- }
5889
- }
5890
5622
  ensureProvider(model, configs) {
5891
5623
  const key = `${model.provider}:${model.id}`;
5892
5624
  if (this.providers.has(key)) return;
@@ -5911,8 +5643,6 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
5911
5643
  return new OllamaProvider(cfg, model);
5912
5644
  case "openai-compatible":
5913
5645
  return new OpenAICompatibleProvider(cfg, model);
5914
- case "github-models":
5915
- return new GitHubModelsProvider(cfg, model);
5916
5646
  default:
5917
5647
  throw new Error(`Unsupported provider type: ${String(cfg.type)}`);
5918
5648
  }
@@ -5920,7 +5650,7 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
5920
5650
  getAnyModelForProvider(type) {
5921
5651
  const fromCatalog = Object.values(MODELS).find((m) => m.provider === type);
5922
5652
  if (fromCatalog) return fromCatalog;
5923
- if (type === "openai-compatible" || type === "azure" || type === "github-models") {
5653
+ if (type === "openai-compatible" || type === "azure") {
5924
5654
  return {
5925
5655
  id: type,
5926
5656
  name: type,
@@ -14053,7 +13783,7 @@ var PermissionEscalator = class extends EventEmitter__default.default {
14053
13783
  }
14054
13784
  };
14055
13785
  var ProviderConfigSchema = zod.z.object({
14056
- type: zod.z.enum(["anthropic", "openai", "gemini", "azure", "openai-compatible", "ollama", "github-models"]),
13786
+ type: zod.z.enum(["anthropic", "openai", "gemini", "azure", "openai-compatible", "ollama"]),
14057
13787
  label: zod.z.string().optional(),
14058
13788
  apiKey: zod.z.string().optional(),
14059
13789
  baseUrl: zod.z.string().url().optional(),
@@ -19329,6 +19059,19 @@ Original error: ${err.message}`
19329
19059
  const rows = provider ? this.db.prepare("SELECT metadata FROM model_cache WHERE provider = ?").all(provider) : this.db.prepare("SELECT metadata FROM model_cache").all();
19330
19060
  return rows.map((r) => JSON.parse(r.metadata));
19331
19061
  }
19062
+ /**
19063
+ * Deletes cached rows for a provider type that no longer exists in
19064
+ * `ProviderType`, so it takes a raw string rather than the union.
19065
+ *
19066
+ * Needed because the cache outlives the code: the REPL's loadCache() only
19067
+ * re-discovers when the cache is EMPTY or older than 24h, so rows left
19068
+ * behind by a removed provider read as "cache populated" and suppress
19069
+ * discovery for the providers that replaced it — `/model` then shows a
19070
+ * configured provider with zero models until the rows age out.
19071
+ */
19072
+ purgeCachedModelsForRetiredProvider(providerType) {
19073
+ return this.db.prepare("DELETE FROM model_cache WHERE provider = ?").run(providerType).changes;
19074
+ }
19332
19075
  clearModelCache(provider) {
19333
19076
  if (provider) {
19334
19077
  this.db.prepare("DELETE FROM model_cache WHERE provider = ?").run(provider);
@@ -19676,7 +19419,73 @@ function mergeGlobalCredentials(workspaceProviders, globalProviders) {
19676
19419
  return merged;
19677
19420
  }
19678
19421
 
19422
+ // src/config/retired-providers.ts
19423
+ var RETIRED_PROVIDER_TYPES = {
19424
+ "github-models": "GitHub retired GitHub Models on 30 July 2026; models.github.ai no longer responds."
19425
+ };
19426
+ function isRetiredProviderType(type) {
19427
+ return typeof type === "string" && Object.hasOwn(RETIRED_PROVIDER_TYPES, type);
19428
+ }
19429
+ function didCleanupChangeAnything(c) {
19430
+ return c.removed.length > 0 || c.clearedPins.length > 0;
19431
+ }
19432
+ function stripRetiredProviders(raw) {
19433
+ const cleanup = { removed: [], clearedPins: [] };
19434
+ if (typeof raw !== "object" || raw === null) return cleanup;
19435
+ const cfg = raw;
19436
+ if (Array.isArray(cfg["providers"])) {
19437
+ const kept = cfg["providers"].filter((p) => {
19438
+ const type = p?.type;
19439
+ if (!isRetiredProviderType(type)) return true;
19440
+ if (!cleanup.removed.includes(type)) cleanup.removed.push(type);
19441
+ return false;
19442
+ });
19443
+ cfg["providers"] = kept;
19444
+ }
19445
+ cleanup.clearedPins = clearRetiredPins(cfg["models"]);
19446
+ return cleanup;
19447
+ }
19448
+ function clearRetiredPins(models) {
19449
+ const cleared = [];
19450
+ if (typeof models !== "object" || models === null) return cleared;
19451
+ const tiers = models;
19452
+ for (const tier of ["t1", "t2", "t3"]) {
19453
+ const pin = tiers[tier];
19454
+ if (typeof pin !== "string") continue;
19455
+ const prefix = pin.slice(0, pin.indexOf(":")).toLowerCase();
19456
+ if (pin.includes(":") && isRetiredProviderType(prefix)) {
19457
+ delete tiers[tier];
19458
+ cleared.push(tier);
19459
+ }
19460
+ }
19461
+ return cleared;
19462
+ }
19463
+ function filterRetiredCredentials(providers) {
19464
+ const removed = [];
19465
+ const kept = providers.filter((p) => {
19466
+ if (!isRetiredProviderType(p.type)) return true;
19467
+ if (!removed.includes(p.type)) removed.push(p.type);
19468
+ return false;
19469
+ });
19470
+ return { kept, removed };
19471
+ }
19472
+ function describeCleanup(c) {
19473
+ const parts = [];
19474
+ for (const type of c.removed) {
19475
+ parts.push(`removed the retired "${type}" provider (${RETIRED_PROVIDER_TYPES[type]})`);
19476
+ }
19477
+ if (c.clearedPins.length > 0) {
19478
+ parts.push(`reset ${c.clearedPins.map((t) => t.toUpperCase()).join("/")} to Auto, since the pin named it`);
19479
+ }
19480
+ return `Cascade config migration: ${parts.join("; ")}.`;
19481
+ }
19482
+
19679
19483
  // src/config/index.ts
19484
+ var KEY_OPTIONAL_PROVIDER_TYPES = /* @__PURE__ */ new Set(["ollama", "openai-compatible"]);
19485
+ function hasUsableProvider(providers) {
19486
+ if (!providers?.length) return false;
19487
+ return providers.some((p) => KEY_OPTIONAL_PROVIDER_TYPES.has(p.type) || !!p.apiKey || !!p.authToken);
19488
+ }
19680
19489
  var ConfigManager = class {
19681
19490
  config;
19682
19491
  keystore;
@@ -19685,6 +19494,29 @@ var ConfigManager = class {
19685
19494
  cascadeMd = null;
19686
19495
  workspacePath;
19687
19496
  globalDir;
19497
+ /**
19498
+ * What THIS load migrated out, if anything. Strictly per-load state: reset
19499
+ * at the top of every `load()`, because two places read it as a statement
19500
+ * about the load in progress — `injectEnvKeys()` asks whether an empty
19501
+ * provider list was emptied by a retirement, and the end of `load()` warns
19502
+ * and builds the user-facing notice from it. Left set, a second `load()` on
19503
+ * the same instance (`startRepl()` does one after its setup wizard) would
19504
+ * re-announce a migration that already happened and suppress the Ollama
19505
+ * fallback for a list that is empty for some unrelated reason.
19506
+ */
19507
+ retiredCleanup;
19508
+ /**
19509
+ * Human-readable migration notice held for a UI to display. `console.warn`
19510
+ * is not sufficient: the REPL clears the TTY immediately after load(), and
19511
+ * the desktop emits from the main process where nothing is rendered.
19512
+ */
19513
+ pendingRetiredNotice;
19514
+ /** Returns the pending migration notice once, then forgets it. */
19515
+ takeRetiredNotice() {
19516
+ const n = this.pendingRetiredNotice;
19517
+ this.pendingRetiredNotice = void 0;
19518
+ return n;
19519
+ }
19688
19520
  /** `globalDirOverride` exists for tests — never point it at the real home dir there. */
19689
19521
  constructor(workspacePath = process.cwd(), globalDirOverride) {
19690
19522
  this.workspacePath = workspacePath;
@@ -19704,10 +19536,32 @@ var ConfigManager = class {
19704
19536
  this.cascadeMd = await loadCascadeMd(this.workspacePath);
19705
19537
  this.keystore = new Keystore(path26__default.default.join(this.globalDir, GLOBAL_KEYSTORE_FILE));
19706
19538
  this.store = new MemoryStore(path26__default.default.join(this.workspacePath, CASCADE_DB_FILE));
19539
+ const globalCreds = filterRetiredCredentials(loadGlobalCredentials(this.globalDir));
19540
+ if (globalCreds.removed.length > 0) {
19541
+ try {
19542
+ saveGlobalCredentials(this.globalDir, globalCreds.kept);
19543
+ } catch (err) {
19544
+ console.warn(`Could not rewrite the global credential store: ${err instanceof Error ? err.message : String(err)}`);
19545
+ }
19546
+ this.retiredCleanup = {
19547
+ removed: [.../* @__PURE__ */ new Set([...this.retiredCleanup?.removed ?? [], ...globalCreds.removed])],
19548
+ clearedPins: this.retiredCleanup?.clearedPins ?? []
19549
+ };
19550
+ }
19707
19551
  await this.injectEnvKeys();
19708
- this.config.providers = mergeGlobalCredentials(this.config.providers, loadGlobalCredentials(this.globalDir));
19552
+ this.config.providers = mergeGlobalCredentials(this.config.providers, globalCreds.kept);
19553
+ for (const type of Object.keys(RETIRED_PROVIDER_TYPES)) {
19554
+ try {
19555
+ this.store.purgeCachedModelsForRetiredProvider(type);
19556
+ } catch {
19557
+ }
19558
+ }
19709
19559
  await this.ensureDefaultIdentity();
19710
19560
  if (mcpNamesChanged) await this.save();
19561
+ if (this.retiredCleanup) {
19562
+ console.warn(describeCleanup(this.retiredCleanup));
19563
+ this.pendingRetiredNotice = describeCleanup(this.retiredCleanup);
19564
+ }
19711
19565
  }
19712
19566
  getConfig() {
19713
19567
  return this.config;
@@ -19746,14 +19600,13 @@ var ConfigManager = class {
19746
19600
  anthropic: "ANTHROPIC_API_KEY",
19747
19601
  openai: "OPENAI_API_KEY",
19748
19602
  gemini: "GOOGLE_API_KEY",
19749
- azure: "AZURE_OPENAI_KEY",
19603
+ azure: "AZURE_OPENAI_KEY"
19750
19604
  // Deliberately NOT `GITHUB_TOKEN`: that one is injected automatically by
19751
19605
  // GitHub Actions and is commonly exported in developer shells for `gh`
19752
19606
  // and git auth, and it will essentially never carry the fine-grained
19753
19607
  // `models: read` scope this provider needs. Adopting it would wire up a
19754
19608
  // provider that 403s on its first real call — a confusing failure for
19755
19609
  // something that is meant to be explicit opt-in BYOK.
19756
- "github-models": "GITHUB_MODELS_TOKEN"
19757
19610
  };
19758
19611
  const envKey = envMap[provider];
19759
19612
  if (envKey && process.env[envKey]) return process.env[envKey];
@@ -19765,10 +19618,21 @@ var ConfigManager = class {
19765
19618
  return configProvider?.apiKey;
19766
19619
  }
19767
19620
  async loadConfig() {
19621
+ this.retiredCleanup = void 0;
19768
19622
  const configPath = path26__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
19769
19623
  try {
19770
19624
  const raw = await fs5__default.default.readFile(configPath, "utf-8");
19771
- return validateConfig(JSON.parse(raw));
19625
+ const parsed = JSON.parse(raw);
19626
+ const cleanup = stripRetiredProviders(parsed);
19627
+ if (didCleanupChangeAnything(cleanup)) {
19628
+ this.retiredCleanup = cleanup;
19629
+ try {
19630
+ await fs5__default.default.writeFile(configPath, JSON.stringify(parsed, null, 2), "utf-8");
19631
+ } catch (err) {
19632
+ console.warn(`Could not persist the retired-provider migration: ${err instanceof Error ? err.message : String(err)}`);
19633
+ }
19634
+ }
19635
+ return validateConfig(parsed);
19772
19636
  } catch (err) {
19773
19637
  if (err.code === "ENOENT") {
19774
19638
  return validateConfig({});
@@ -19777,27 +19641,25 @@ var ConfigManager = class {
19777
19641
  }
19778
19642
  }
19779
19643
  async injectEnvKeys() {
19780
- const isFirstRun = this.config.providers.length === 0;
19644
+ const wasEmpty = this.config.providers.length === 0;
19645
+ const emptiedByRetirement = wasEmpty && !!this.retiredCleanup;
19781
19646
  const envProviders = [
19782
19647
  { env: "ANTHROPIC_API_KEY", type: "anthropic" },
19783
19648
  { env: "OPENAI_API_KEY", type: "openai" },
19784
19649
  { env: "GOOGLE_API_KEY", type: "gemini" },
19785
- { env: "AZURE_OPENAI_KEY", type: "azure" },
19786
- // See getApiKey() above for why this is GITHUB_MODELS_TOKEN and never
19787
- // the ambient GITHUB_TOKEN.
19788
- { env: "GITHUB_MODELS_TOKEN", type: "github-models" }
19650
+ { env: "AZURE_OPENAI_KEY", type: "azure" }
19789
19651
  ];
19790
19652
  for (const { env, type } of envProviders) {
19791
19653
  const key = process.env[env];
19792
19654
  if (!key) continue;
19793
19655
  const existing = this.config.providers.find((p) => p.type === type);
19794
- if (!existing && isFirstRun) {
19656
+ if (!existing && wasEmpty) {
19795
19657
  this.config.providers.push({ type, apiKey: key });
19796
19658
  } else if (existing && !existing.apiKey) {
19797
19659
  existing.apiKey = key;
19798
19660
  }
19799
19661
  }
19800
- if (isFirstRun && !this.config.providers.find((p) => p.type === "ollama")) {
19662
+ if (wasEmpty && !emptiedByRetirement && !this.config.providers.find((p) => p.type === "ollama")) {
19801
19663
  this.config.providers.push({ type: "ollama" });
19802
19664
  }
19803
19665
  }
@@ -22434,7 +22296,9 @@ function gatherSyncBundle(config) {
22434
22296
  if (Object.keys(prefs).length) bundle.prefs = prefs;
22435
22297
  return bundle;
22436
22298
  }
22437
- function applySyncBundle(bundle, config) {
22299
+ function applySyncBundle(bundle, config, cleanup) {
22300
+ const dropped = stripRetiredProviders(bundle);
22301
+ if (cleanup) cleanup.removed = dropped.removed;
22438
22302
  const next = { ...config, tools: { ...config.tools } };
22439
22303
  if (bundle.providers) next.providers = mergeByKey(config.providers, bundle.providers, providerSig) ?? [];
22440
22304
  if (bundle.webSearch) next.tools.webSearch = { ...config.tools?.webSearch, ...bundle.webSearch };
@@ -22450,6 +22314,14 @@ function applySyncBundle(bundle, config) {
22450
22314
  }
22451
22315
  if (bundle.models) next.models = { ...config.models, ...bundle.models };
22452
22316
  if (bundle.budget) next.budget = { ...config.budget, ...bundle.budget };
22317
+ const merged = { ...next.models };
22318
+ const clearedFromMerge = clearRetiredPins(merged);
22319
+ if (clearedFromMerge.length > 0) next.models = merged;
22320
+ if (cleanup) {
22321
+ cleanup.clearedPins = ["t1", "t2", "t3"].filter(
22322
+ (t) => (dropped.clearedPins.includes(t) || clearedFromMerge.includes(t)) && next.models?.[t] === void 0
22323
+ );
22324
+ }
22453
22325
  if (bundle.prefs) {
22454
22326
  const p = bundle.prefs;
22455
22327
  if (p.autoBias !== void 0) next.autoBias = p.autoBias;
@@ -22496,9 +22368,6 @@ exports.DOCUMENT_MIME = DOCUMENT_MIME;
22496
22368
  exports.DashboardServer = DashboardServer;
22497
22369
  exports.DependencyScheduler = DependencyScheduler;
22498
22370
  exports.FileMcpOAuthStore = FileMcpOAuthStore;
22499
- exports.GITHUB_MODELS_API_VERSION = GITHUB_MODELS_API_VERSION;
22500
- exports.GITHUB_MODELS_CATALOG_URL = GITHUB_MODELS_CATALOG_URL;
22501
- exports.GITHUB_MODELS_INFERENCE_URL = GITHUB_MODELS_INFERENCE_URL;
22502
22371
  exports.GLOBAL_CONFIG_DIR = GLOBAL_CONFIG_DIR;
22503
22372
  exports.GLOBAL_CREDENTIALS_FILE = GLOBAL_CREDENTIALS_FILE;
22504
22373
  exports.GLOBAL_DB_FILE = GLOBAL_DB_FILE;
@@ -22571,6 +22440,7 @@ exports.failures = failures;
22571
22440
  exports.fileExt = fileExt;
22572
22441
  exports.fileOAuthProvider = fileOAuthProvider;
22573
22442
  exports.gatherSyncBundle = gatherSyncBundle;
22443
+ exports.hasUsableProvider = hasUsableProvider;
22574
22444
  exports.hashContent = hashContent;
22575
22445
  exports.heuristicCodeChunker = heuristicCodeChunker;
22576
22446
  exports.inferAzureBaseModel = inferAzureBaseModel;