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.
@@ -291781,7 +291781,7 @@ function reconcilePrice(model, live) {
291781
291781
  }
291782
291782
 
291783
291783
  // src/constants.ts
291784
- var CASCADE_VERSION = "0.69.0";
291784
+ var CASCADE_VERSION = "0.71.0";
291785
291785
  var CASCADE_CONFIG_DIR = ".cascade";
291786
291786
  var CASCADE_MD_FILE = "CASCADE.md";
291787
291787
  var CASCADE_IGNORE_FILE = ".cascadeignore";
@@ -292188,9 +292188,6 @@ var DEFAULT_THEME = "cascade";
292188
292188
  var OLLAMA_BASE_URL = "http://localhost:11434";
292189
292189
  var LM_STUDIO_BASE_URL = "http://localhost:1234";
292190
292190
  var AZURE_BASE_URL_TEMPLATE = "https://{resource}.openai.azure.com";
292191
- var GITHUB_MODELS_INFERENCE_URL = "https://models.github.ai/inference";
292192
- var GITHUB_MODELS_CATALOG_URL = "https://models.github.ai/catalog/models";
292193
- var GITHUB_MODELS_API_VERSION = "2026-03-10";
292194
292191
  var TOOL_NAMES = {
292195
292192
  SHELL: "shell",
292196
292193
  FILE_READ: "file_read",
@@ -292225,8 +292222,7 @@ var PROVIDER_DISPLAY_NAMES = {
292225
292222
  gemini: "Google Gemini",
292226
292223
  azure: "Azure OpenAI",
292227
292224
  "openai-compatible": "OpenAI-Compatible",
292228
- ollama: "Ollama (Local)",
292229
- "github-models": "GitHub Models"
292225
+ ollama: "Ollama (Local)"
292230
292226
  };
292231
292227
 
292232
292228
  // src/utils/cost.ts
@@ -319170,206 +319166,6 @@ async function nodeHttpFetch(input, init2 = {}, redirectCount = 0, opts = {}) {
319170
319166
  });
319171
319167
  }
319172
319168
 
319173
- // src/providers/github-models.ts
319174
- var GITHUB_MODELS_MAX_OUTPUT_TOKENS = 4e3;
319175
- var GITHUB_MODELS_MAX_INPUT_TOKENS = 8e3;
319176
- var DEFAULT_CONTEXT_WINDOW = 128e3;
319177
- var GITHUB_MODELS_MAX_CATALOG_PAGES = 20;
319178
- var GITHUB_MODELS_CATALOG_ORIGIN = new URL(GITHUB_MODELS_CATALOG_URL).origin;
319179
- function stripModelOwnerPrefix(id) {
319180
- const slash = (id || "").indexOf("/");
319181
- return slash === -1 ? id : id.slice(slash + 1);
319182
- }
319183
- function pickString(o3, keys2) {
319184
- for (const k4 of keys2) {
319185
- const v6 = o3[k4];
319186
- if (typeof v6 === "string" && v6.length > 0) return v6;
319187
- }
319188
- return void 0;
319189
- }
319190
- function pickNumber(o3, keys2) {
319191
- for (const k4 of keys2) {
319192
- const v6 = k4.includes(".") ? o3[k4.split(".")[0]]?.[k4.split(".")[1]] : o3[k4];
319193
- if (typeof v6 === "number" && Number.isFinite(v6) && v6 > 0) return v6;
319194
- }
319195
- return void 0;
319196
- }
319197
- function looksVisionCapable(o3) {
319198
- for (const k4 of ["supported_input_modalities", "input_modalities", "modalities", "capabilities"]) {
319199
- const v6 = o3[k4];
319200
- if (Array.isArray(v6) && v6.some((x5) => typeof x5 === "string" && /image|vision/i.test(x5))) return true;
319201
- }
319202
- return false;
319203
- }
319204
- function looksToolCapable(o3) {
319205
- const params = o3["supported_parameters"];
319206
- if (Array.isArray(params) && params.some((x5) => typeof x5 === "string" && /^tools?$|function.?call/i.test(x5))) {
319207
- return true;
319208
- }
319209
- for (const k4 of ["capabilities", "supported_output_modalities"]) {
319210
- const v6 = o3[k4];
319211
- if (Array.isArray(v6) && v6.some((x5) => typeof x5 === "string" && /tool|function.?call/i.test(x5))) return true;
319212
- }
319213
- const caps = o3["capabilities"];
319214
- if (caps && typeof caps === "object" && !Array.isArray(caps)) {
319215
- const c4 = caps;
319216
- if (c4["function_calling"] === true || c4["tool_calling"] === true || c4["tools"] === true) return true;
319217
- }
319218
- return false;
319219
- }
319220
- function parseNextLink(linkHeader) {
319221
- if (!linkHeader) return null;
319222
- for (const part of linkHeader.split(",")) {
319223
- const match = part.match(/<([^>]+)>\s*;\s*rel="next"/);
319224
- if (match) return match[1];
319225
- }
319226
- return null;
319227
- }
319228
- var GitHubModelsProvider = class extends OpenAIProvider {
319229
- constructor(config2, model) {
319230
- super({ ...config2, apiKey: config2.apiKey ?? "" }, model);
319231
- this.useMaxCompletionTokens = isReasoningModel(stripModelOwnerPrefix(model.id));
319232
- this.client = new openai_default({
319233
- apiKey: config2.apiKey ?? "",
319234
- baseURL: GITHUB_MODELS_INFERENCE_URL,
319235
- defaultHeaders: { "X-GitHub-Api-Version": GITHUB_MODELS_API_VERSION, "User-Agent": "Cascade-AI" }
319236
- });
319237
- }
319238
- /**
319239
- * `OpenAIProvider.generateStream()` builds its request budget as
319240
- * `options.maxTokens ?? this.model.maxOutputTokens` — an explicit per-call
319241
- * `maxTokens` (T1Administrator's final compilation asks for 8,000, well
319242
- * above GitHub's real ~4K hard cap) wins outright and is sent unclamped.
319243
- * GitHub then rejects the request instead of answering, so a run routed to
319244
- * this provider for that step fails outright rather than getting a
319245
- * (perfectly serviceable, just shorter) completion. Clamp here, once, right
319246
- * before delegating to the inherited implementation, rather than teaching
319247
- * every caller GitHub's specific cap.
319248
- */
319249
- async generateStream(options, onChunk) {
319250
- const clamped = options.maxTokens !== void 0 && options.maxTokens > GITHUB_MODELS_MAX_OUTPUT_TOKENS ? { ...options, maxTokens: GITHUB_MODELS_MAX_OUTPUT_TOKENS } : options;
319251
- return super.generateStream(clamped, onChunk);
319252
- }
319253
- /**
319254
- * The catalog is a GitHub REST resource, not an OpenAI `/models` list, and
319255
- * wants GitHub's own header trio — the plain `Accept: application/json` the
319256
- * openai-compatible provider sends is not the same request.
319257
- */
319258
- catalogHeaders() {
319259
- return {
319260
- Accept: "application/vnd.github+json",
319261
- Authorization: `Bearer ${this.config.apiKey ?? ""}`,
319262
- "X-GitHub-Api-Version": GITHUB_MODELS_API_VERSION,
319263
- // GitHub's REST API rejects any request with no User-Agent — a plain 403
319264
- // regardless of how valid the PAT is. nodeHttpFetch is a thin wrapper
319265
- // over node:http/https, neither of which sets one the way a browser's
319266
- // fetch or a full HTTP client library would, so this has to be explicit.
319267
- "User-Agent": "Cascade-AI"
319268
- };
319269
- }
319270
- async listModels() {
319271
- const raw = [];
319272
- let url2 = GITHUB_MODELS_CATALOG_URL;
319273
- let pages = 0;
319274
- const visited = /* @__PURE__ */ new Set();
319275
- while (url2 && pages < GITHUB_MODELS_MAX_CATALOG_PAGES) {
319276
- if (visited.has(url2)) break;
319277
- visited.add(url2);
319278
- const res = await nodeHttpFetch(url2, { headers: this.catalogHeaders() }, 0, {
319279
- allowedRedirectOrigin: GITHUB_MODELS_CATALOG_ORIGIN
319280
- });
319281
- if (!res.ok) throw new Error(`GitHub Models catalog ${url2} returned HTTP ${res.status}`);
319282
- const body = await res.json();
319283
- const pageItems = Array.isArray(body) ? body : Array.isArray(body?.models) ? body.models : Array.isArray(body?.data) ? body.data : [];
319284
- raw.push(...pageItems);
319285
- const next = parseNextLink(res.headers.get("link"));
319286
- if (next) {
319287
- const resolved = new URL(next, url2);
319288
- if (resolved.origin !== GITHUB_MODELS_CATALOG_ORIGIN) {
319289
- throw new Error(`Refusing cross-origin GitHub Models pagination URL: ${resolved.origin}`);
319290
- }
319291
- url2 = resolved.href;
319292
- } else {
319293
- url2 = null;
319294
- }
319295
- pages++;
319296
- }
319297
- const entries = raw.map((m3) => {
319298
- if (typeof m3 === "string") {
319299
- return { id: m3, name: m3, contextWindow: DEFAULT_CONTEXT_WINDOW, vision: false, tools: false };
319300
- }
319301
- if (!m3 || typeof m3 !== "object") return void 0;
319302
- const o3 = m3;
319303
- const id = pickString(o3, ["id", "name", "model"]);
319304
- if (!id) return void 0;
319305
- return {
319306
- id,
319307
- name: pickString(o3, ["friendly_name", "display_name", "name"]) ?? id,
319308
- contextWindow: pickNumber(o3, [
319309
- "context_window",
319310
- "context_length",
319311
- "max_input_tokens",
319312
- "limits.max_input_tokens",
319313
- "limits.max_context_tokens"
319314
- ]) ?? DEFAULT_CONTEXT_WINDOW,
319315
- vision: looksVisionCapable(o3),
319316
- tools: looksToolCapable(o3)
319317
- };
319318
- }).filter((e3) => e3 !== void 0);
319319
- if (entries.length === 0) return [];
319320
- const chat = entries.filter((e3) => isChatModel(e3.id));
319321
- return (chat.length ? chat : entries).map((e3) => ({
319322
- id: e3.id,
319323
- // full `owner/model` — what the API is addressed by
319324
- name: e3.name,
319325
- provider: "github-models",
319326
- // GitHub's real per-request input cap, not the base model's larger one
319327
- // (see GITHUB_MODELS_MAX_INPUT_TOKENS above) — the same reasoning
319328
- // already applied to maxOutputTokens below, for the input side.
319329
- contextWindow: Math.min(e3.contextWindow, GITHUB_MODELS_MAX_INPUT_TOKENS),
319330
- isVisionCapable: e3.vision,
319331
- // A REAL $0, not a missing number — usage is bundled into the quota of
319332
- // the plan the PAT belongs to and is never billed per token, so
319333
- // `pricingUnknown: false` is the honest value (see the flag's contract in
319334
- // core/router/pricing.ts: 0 alone is ambiguous). Set here rather than in
319335
- // pricing-data.json because resolvePricing() matches exactly on
319336
- // provider|model|region with no wildcard: every catalog model would need
319337
- // a hand-curated row, and the next one GitHub adds would read as "cost
319338
- // unknown" — an inconsistent picture for a billing reality that is
319339
- // identical across the entire catalog.
319340
- //
319341
- // `isLocal` is the OTHER way to reach a real $0, and it is the wrong one:
319342
- // it means "runs on this machine" and carries consequences that are
319343
- // simply false for a hosted API — the shared concurrency-1
319344
- // LocalRequestQueue (serialising these calls behind actual Ollama GPU
319345
- // inference), the 300s local rather than 120s cloud inference timeout,
319346
- // and a literal "[local]" tag in `cascade models`.
319347
- inputCostPer1kTokens: 0,
319348
- outputCostPer1kTokens: 0,
319349
- pricingUnknown: false,
319350
- maxOutputTokens: GITHUB_MODELS_MAX_OUTPUT_TOKENS,
319351
- supportsStreaming: true,
319352
- // Not every catalog model supports function/tool calling — a hardcoded
319353
- // `true` sent `tools` to models that reject the parameter outright and
319354
- // never engaged t3-worker.ts's existing text-tool fallback (only an
319355
- // explicit `false` does). See looksToolCapable's own comment for why
319356
- // "not positively advertised" defaults to false rather than true here.
319357
- supportsToolUse: e3.tools,
319358
- isLocal: false
319359
- }));
319360
- }
319361
- async isAvailable() {
319362
- try {
319363
- const res = await nodeHttpFetch(GITHUB_MODELS_CATALOG_URL, { headers: this.catalogHeaders() }, 0, {
319364
- allowedRedirectOrigin: GITHUB_MODELS_CATALOG_ORIGIN
319365
- });
319366
- return res.ok;
319367
- } catch {
319368
- return false;
319369
- }
319370
- }
319371
- };
319372
-
319373
319169
  // src/providers/ollama.ts
319374
319170
  var TOOL_CAPABLE_FAMILIES = [
319375
319171
  "llama3.1",
@@ -319880,7 +319676,7 @@ var ModelSelector = class {
319880
319676
  if (overrideModelId.includes(":")) {
319881
319677
  const parts = overrideModelId.split(":");
319882
319678
  const prefix = parts[0].toLowerCase();
319883
- const validProviders = ["anthropic", "openai", "gemini", "azure", "openai-compatible", "ollama", "github-models"];
319679
+ const validProviders = ["anthropic", "openai", "gemini", "azure", "openai-compatible", "ollama"];
319884
319680
  if (validProviders.includes(prefix)) {
319885
319681
  providerStr = prefix;
319886
319682
  actualId = parts.slice(1).join(":");
@@ -319900,31 +319696,6 @@ var ModelSelector = class {
319900
319696
  else if (this.availableProviders.has("openai-compatible")) providerStr = "openai-compatible";
319901
319697
  else if (this.availableProviders.size === 1) providerStr = Array.from(this.availableProviders)[0];
319902
319698
  }
319903
- if (providerStr === "github-models" && this.availableProviders.has("github-models")) {
319904
- const dynamicModel = {
319905
- id: actualId,
319906
- name: actualId,
319907
- provider: "github-models",
319908
- contextWindow: GITHUB_MODELS_MAX_INPUT_TOKENS,
319909
- isVisionCapable: false,
319910
- inputCostPer1kTokens: 0,
319911
- outputCostPer1kTokens: 0,
319912
- pricingUnknown: false,
319913
- maxOutputTokens: GITHUB_MODELS_MAX_OUTPUT_TOKENS,
319914
- supportsStreaming: true,
319915
- // Unconfirmed (no live catalog entry to read a real capability from)
319916
- // must default to false, not left undefined: t3-worker.ts's text-tool
319917
- // fallback only engages on a strict `=== false`, so an undefined
319918
- // value here sends native `tools` to an unverified multi-vendor
319919
- // model and any that don't support function calling reject the
319920
- // request outright. Same default-to-false-when-unconfirmed policy
319921
- // listModels() already applies for a real catalog entry.
319922
- supportsToolUse: false,
319923
- isLocal: false
319924
- };
319925
- this.addDynamicModel(dynamicModel);
319926
- return dynamicModel;
319927
- }
319928
319699
  if (providerStr && this.availableProviders.has(providerStr)) {
319929
319700
  const dynamicModel = withResolvedPricing({
319930
319701
  id: actualId,
@@ -320017,20 +319788,7 @@ var DEFAULT_PROVIDER_TPM = {
320017
319788
  gemini: 6e4,
320018
319789
  azure: 3e4,
320019
319790
  "openai-compatible": 3e4,
320020
- ollama: Number.POSITIVE_INFINITY,
320021
- // An order of magnitude below the others, on purpose. GitHub Models is
320022
- // rate-limited by REQUESTS (Free ≈10 RPM, Pro ≈20, plus low daily caps), and
320023
- // this bucket meters TOKENS — there is no request-count limiter in this
320024
- // codebase, and building one is not in scope. The approximation works because
320025
- // the router reserves `model.maxOutputTokens + 512` per call, and the
320026
- // provider reports GitHub's real ~4K per-request cap rather than the
320027
- // underlying model's larger one — so every call withdraws a near-constant
320028
- // ~4.5K regardless of how short the reply actually turns out to be. At 8K/min
320029
- // that lands around 1–2 calls per minute of headroom: comfortably inside even
320030
- // the Free tier, with room for the daily cap to matter more than this does.
320031
- // Users on a higher tier can raise it with no code change through the generic
320032
- // `config.rateLimits.providerTpm['github-models']` override.
320033
- "github-models": 8e3
319791
+ ollama: Number.POSITIVE_INFINITY
320034
319792
  };
320035
319793
  var TpmLimiter = class {
320036
319794
  buckets = /* @__PURE__ */ new Map();
@@ -320386,7 +320144,6 @@ var ModelProfiler = class {
320386
320144
  (m3) => !alreadyProfiled.has(m3.id) && !SKIP_PATTERN.test(m3.id) && !SKIP_PATTERN.test(m3.name)
320387
320145
  );
320388
320146
  if (toProfile.length === 0) return;
320389
- const probesHitGithubModels = this.router?.getModelForTier("T3")?.provider === "github-models";
320390
320147
  const openRouterModels = await fetchOpenRouterModels();
320391
320148
  const orByNormalizedId = /* @__PURE__ */ new Map();
320392
320149
  for (const m3 of openRouterModels) {
@@ -320401,7 +320158,7 @@ var ModelProfiler = class {
320401
320158
  if (orMatch?.description) {
320402
320159
  specializations = extractSpecializations(orMatch.description);
320403
320160
  }
320404
- if (specializations.length === 0 && this.router && !probesHitGithubModels) {
320161
+ if (specializations.length === 0 && this.router) {
320405
320162
  specializations = await queryModelDirectly(this.router);
320406
320163
  }
320407
320164
  this.store.saveModelProfile(model.id, model.provider, specializations);
@@ -320621,7 +320378,6 @@ var LiveDataProvider = class {
320621
320378
  */
320622
320379
  applyLivePricing(models) {
320623
320380
  return models.map((m3) => {
320624
- if (m3.provider === "github-models") return m3;
320625
320381
  const p3 = this.getLivePrice(m3.baseModelId ?? m3.id);
320626
320382
  if (!p3) return m3;
320627
320383
  const reconciled = reconcilePrice(m3, p3);
@@ -320660,7 +320416,6 @@ var LiveDataProvider = class {
320660
320416
  */
320661
320417
  applyLiveCapabilities(models) {
320662
320418
  return models.map((m3) => {
320663
- if (m3.provider === "github-models") return m3;
320664
320419
  const c4 = this.getCapability(m3.baseModelId ?? m3.id);
320665
320420
  if (!c4) return m3;
320666
320421
  const next = { ...m3 };
@@ -320964,16 +320719,13 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
320964
320719
  for (const model of azureDeployments) this.selector.addDynamicModel(model);
320965
320720
  availableProviders.add("azure");
320966
320721
  }
320967
- if (availableProviders.has("github-models")) {
320968
- await this.discoverGitHubModelsCatalog(config2);
320969
- }
320970
320722
  await this.validateCloudProviderModels(config2);
320971
320723
  for (const tier of ["T1", "T2", "T3"]) {
320972
320724
  const override = tier === "T1" ? config2.models.t1 : tier === "T2" ? config2.models.t2 : config2.models.t3;
320973
320725
  if (!override || override === "auto") continue;
320974
320726
  const model = this.selector.selectForTier(tier, override);
320975
320727
  if (!model) {
320976
- const knownProviders = ["anthropic", "openai", "gemini", "azure", "openai-compatible", "ollama", "github-models"];
320728
+ const knownProviders = ["anthropic", "openai", "gemini", "azure", "openai-compatible", "ollama"];
320977
320729
  const hasProviderPrefix = override.includes(":") && knownProviders.some((p3) => override.startsWith(p3 + ":"));
320978
320730
  if (hasProviderPrefix) {
320979
320731
  const provider = override.split(":")[0];
@@ -321128,7 +320880,7 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
321128
320880
  * models without a package upgrade. Mirrors discoverOllamaModels.
321129
320881
  */
321130
320882
  async discoverProviderModels() {
321131
- const cloud = ["anthropic", "openai", "gemini", "azure", "openai-compatible", "github-models"];
320883
+ const cloud = ["anthropic", "openai", "gemini", "azure", "openai-compatible"];
321132
320884
  const tasks = cloud.map(async (type) => {
321133
320885
  if (!this.selector.isProviderAvailable(type)) return;
321134
320886
  const seed = this.getAnyModelForProvider(type);
@@ -321643,26 +321395,6 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
321643
321395
  return false;
321644
321396
  }
321645
321397
  }
321646
- /**
321647
- * Registers GitHub Models' real catalog ids via addDynamicModel so init()'s
321648
- * tier-fill loop can pick one. Registration alone does not create a bound
321649
- * BaseProvider instance (see ensureProvider) — the tier-fill loop's own
321650
- * ensureProvider call does that for whichever model it actually selects,
321651
- * which is why this runs BEFORE that loop rather than only in the
321652
- * background discoverProviderModels() refresh.
321653
- */
321654
- async discoverGitHubModelsCatalog(config2) {
321655
- const cfg = config2.providers.find((p3) => p3.type === "github-models") ?? { type: "github-models" };
321656
- const seed = this.getAnyModelForProvider("github-models");
321657
- if (!seed) return;
321658
- try {
321659
- const provider = new GitHubModelsProvider(cfg, seed);
321660
- const models = await provider.listModels();
321661
- for (const m3 of models) this.selector.addDynamicModel(m3);
321662
- } catch (err) {
321663
- console.warn("[router] GitHub Models catalog discovery failed:", err instanceof Error ? err.message : err);
321664
- }
321665
- }
321666
321398
  ensureProvider(model, configs) {
321667
321399
  const key = `${model.provider}:${model.id}`;
321668
321400
  if (this.providers.has(key)) return;
@@ -321687,8 +321419,6 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
321687
321419
  return new OllamaProvider(cfg, model);
321688
321420
  case "openai-compatible":
321689
321421
  return new OpenAICompatibleProvider(cfg, model);
321690
- case "github-models":
321691
- return new GitHubModelsProvider(cfg, model);
321692
321422
  default:
321693
321423
  throw new Error(`Unsupported provider type: ${String(cfg.type)}`);
321694
321424
  }
@@ -321696,7 +321426,7 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
321696
321426
  getAnyModelForProvider(type) {
321697
321427
  const fromCatalog = Object.values(MODELS).find((m3) => m3.provider === type);
321698
321428
  if (fromCatalog) return fromCatalog;
321699
- if (type === "openai-compatible" || type === "azure" || type === "github-models") {
321429
+ if (type === "openai-compatible" || type === "azure") {
321700
321430
  return {
321701
321431
  id: type,
321702
321432
  name: type,
@@ -370123,7 +369853,7 @@ var PermissionEscalator = class extends EventEmitter__default.default {
370123
369853
 
370124
369854
  // src/config/schema.ts
370125
369855
  var ProviderConfigSchema = external_exports.object({
370126
- type: external_exports.enum(["anthropic", "openai", "gemini", "azure", "openai-compatible", "ollama", "github-models"]),
369856
+ type: external_exports.enum(["anthropic", "openai", "gemini", "azure", "openai-compatible", "ollama"]),
370127
369857
  label: external_exports.string().optional(),
370128
369858
  apiKey: external_exports.string().optional(),
370129
369859
  baseUrl: external_exports.string().url().optional(),
@@ -375409,6 +375139,19 @@ Original error: ${err.message}`
375409
375139
  const rows = provider ? this.db.prepare("SELECT metadata FROM model_cache WHERE provider = ?").all(provider) : this.db.prepare("SELECT metadata FROM model_cache").all();
375410
375140
  return rows.map((r4) => JSON.parse(r4.metadata));
375411
375141
  }
375142
+ /**
375143
+ * Deletes cached rows for a provider type that no longer exists in
375144
+ * `ProviderType`, so it takes a raw string rather than the union.
375145
+ *
375146
+ * Needed because the cache outlives the code: the REPL's loadCache() only
375147
+ * re-discovers when the cache is EMPTY or older than 24h, so rows left
375148
+ * behind by a removed provider read as "cache populated" and suppress
375149
+ * discovery for the providers that replaced it — `/model` then shows a
375150
+ * configured provider with zero models until the rows age out.
375151
+ */
375152
+ purgeCachedModelsForRetiredProvider(providerType) {
375153
+ return this.db.prepare("DELETE FROM model_cache WHERE provider = ?").run(providerType).changes;
375154
+ }
375412
375155
  clearModelCache(provider) {
375413
375156
  if (provider) {
375414
375157
  this.db.prepare("DELETE FROM model_cache WHERE provider = ?").run(provider);
@@ -375756,7 +375499,73 @@ function mergeGlobalCredentials(workspaceProviders, globalProviders) {
375756
375499
  return merged;
375757
375500
  }
375758
375501
 
375502
+ // src/config/retired-providers.ts
375503
+ var RETIRED_PROVIDER_TYPES = {
375504
+ "github-models": "GitHub retired GitHub Models on 30 July 2026; models.github.ai no longer responds."
375505
+ };
375506
+ function isRetiredProviderType(type) {
375507
+ return typeof type === "string" && Object.hasOwn(RETIRED_PROVIDER_TYPES, type);
375508
+ }
375509
+ function didCleanupChangeAnything(c4) {
375510
+ return c4.removed.length > 0 || c4.clearedPins.length > 0;
375511
+ }
375512
+ function stripRetiredProviders(raw) {
375513
+ const cleanup = { removed: [], clearedPins: [] };
375514
+ if (typeof raw !== "object" || raw === null) return cleanup;
375515
+ const cfg = raw;
375516
+ if (Array.isArray(cfg["providers"])) {
375517
+ const kept = cfg["providers"].filter((p3) => {
375518
+ const type = p3?.type;
375519
+ if (!isRetiredProviderType(type)) return true;
375520
+ if (!cleanup.removed.includes(type)) cleanup.removed.push(type);
375521
+ return false;
375522
+ });
375523
+ cfg["providers"] = kept;
375524
+ }
375525
+ cleanup.clearedPins = clearRetiredPins(cfg["models"]);
375526
+ return cleanup;
375527
+ }
375528
+ function clearRetiredPins(models) {
375529
+ const cleared = [];
375530
+ if (typeof models !== "object" || models === null) return cleared;
375531
+ const tiers = models;
375532
+ for (const tier of ["t1", "t2", "t3"]) {
375533
+ const pin = tiers[tier];
375534
+ if (typeof pin !== "string") continue;
375535
+ const prefix = pin.slice(0, pin.indexOf(":")).toLowerCase();
375536
+ if (pin.includes(":") && isRetiredProviderType(prefix)) {
375537
+ delete tiers[tier];
375538
+ cleared.push(tier);
375539
+ }
375540
+ }
375541
+ return cleared;
375542
+ }
375543
+ function filterRetiredCredentials(providers) {
375544
+ const removed = [];
375545
+ const kept = providers.filter((p3) => {
375546
+ if (!isRetiredProviderType(p3.type)) return true;
375547
+ if (!removed.includes(p3.type)) removed.push(p3.type);
375548
+ return false;
375549
+ });
375550
+ return { kept, removed };
375551
+ }
375552
+ function describeCleanup(c4) {
375553
+ const parts = [];
375554
+ for (const type of c4.removed) {
375555
+ parts.push(`removed the retired "${type}" provider (${RETIRED_PROVIDER_TYPES[type]})`);
375556
+ }
375557
+ if (c4.clearedPins.length > 0) {
375558
+ parts.push(`reset ${c4.clearedPins.map((t4) => t4.toUpperCase()).join("/")} to Auto, since the pin named it`);
375559
+ }
375560
+ return `Cascade config migration: ${parts.join("; ")}.`;
375561
+ }
375562
+
375759
375563
  // src/config/index.ts
375564
+ var KEY_OPTIONAL_PROVIDER_TYPES = /* @__PURE__ */ new Set(["ollama", "openai-compatible"]);
375565
+ function hasUsableProvider(providers) {
375566
+ if (!providers?.length) return false;
375567
+ return providers.some((p3) => KEY_OPTIONAL_PROVIDER_TYPES.has(p3.type) || !!p3.apiKey || !!p3.authToken);
375568
+ }
375760
375569
  var ConfigManager = class {
375761
375570
  config;
375762
375571
  keystore;
@@ -375765,6 +375574,29 @@ var ConfigManager = class {
375765
375574
  cascadeMd = null;
375766
375575
  workspacePath;
375767
375576
  globalDir;
375577
+ /**
375578
+ * What THIS load migrated out, if anything. Strictly per-load state: reset
375579
+ * at the top of every `load()`, because two places read it as a statement
375580
+ * about the load in progress — `injectEnvKeys()` asks whether an empty
375581
+ * provider list was emptied by a retirement, and the end of `load()` warns
375582
+ * and builds the user-facing notice from it. Left set, a second `load()` on
375583
+ * the same instance (`startRepl()` does one after its setup wizard) would
375584
+ * re-announce a migration that already happened and suppress the Ollama
375585
+ * fallback for a list that is empty for some unrelated reason.
375586
+ */
375587
+ retiredCleanup;
375588
+ /**
375589
+ * Human-readable migration notice held for a UI to display. `console.warn`
375590
+ * is not sufficient: the REPL clears the TTY immediately after load(), and
375591
+ * the desktop emits from the main process where nothing is rendered.
375592
+ */
375593
+ pendingRetiredNotice;
375594
+ /** Returns the pending migration notice once, then forgets it. */
375595
+ takeRetiredNotice() {
375596
+ const n8 = this.pendingRetiredNotice;
375597
+ this.pendingRetiredNotice = void 0;
375598
+ return n8;
375599
+ }
375768
375600
  /** `globalDirOverride` exists for tests — never point it at the real home dir there. */
375769
375601
  constructor(workspacePath = process.cwd(), globalDirOverride) {
375770
375602
  this.workspacePath = workspacePath;
@@ -375784,10 +375616,32 @@ var ConfigManager = class {
375784
375616
  this.cascadeMd = await loadCascadeMd(this.workspacePath);
375785
375617
  this.keystore = new Keystore(path28__namespace.default.join(this.globalDir, GLOBAL_KEYSTORE_FILE));
375786
375618
  this.store = new MemoryStore(path28__namespace.default.join(this.workspacePath, CASCADE_DB_FILE));
375619
+ const globalCreds = filterRetiredCredentials(loadGlobalCredentials(this.globalDir));
375620
+ if (globalCreds.removed.length > 0) {
375621
+ try {
375622
+ saveGlobalCredentials(this.globalDir, globalCreds.kept);
375623
+ } catch (err) {
375624
+ console.warn(`Could not rewrite the global credential store: ${err instanceof Error ? err.message : String(err)}`);
375625
+ }
375626
+ this.retiredCleanup = {
375627
+ removed: [.../* @__PURE__ */ new Set([...this.retiredCleanup?.removed ?? [], ...globalCreds.removed])],
375628
+ clearedPins: this.retiredCleanup?.clearedPins ?? []
375629
+ };
375630
+ }
375787
375631
  await this.injectEnvKeys();
375788
- this.config.providers = mergeGlobalCredentials(this.config.providers, loadGlobalCredentials(this.globalDir));
375632
+ this.config.providers = mergeGlobalCredentials(this.config.providers, globalCreds.kept);
375633
+ for (const type of Object.keys(RETIRED_PROVIDER_TYPES)) {
375634
+ try {
375635
+ this.store.purgeCachedModelsForRetiredProvider(type);
375636
+ } catch {
375637
+ }
375638
+ }
375789
375639
  await this.ensureDefaultIdentity();
375790
375640
  if (mcpNamesChanged) await this.save();
375641
+ if (this.retiredCleanup) {
375642
+ console.warn(describeCleanup(this.retiredCleanup));
375643
+ this.pendingRetiredNotice = describeCleanup(this.retiredCleanup);
375644
+ }
375791
375645
  }
375792
375646
  getConfig() {
375793
375647
  return this.config;
@@ -375826,14 +375680,13 @@ var ConfigManager = class {
375826
375680
  anthropic: "ANTHROPIC_API_KEY",
375827
375681
  openai: "OPENAI_API_KEY",
375828
375682
  gemini: "GOOGLE_API_KEY",
375829
- azure: "AZURE_OPENAI_KEY",
375683
+ azure: "AZURE_OPENAI_KEY"
375830
375684
  // Deliberately NOT `GITHUB_TOKEN`: that one is injected automatically by
375831
375685
  // GitHub Actions and is commonly exported in developer shells for `gh`
375832
375686
  // and git auth, and it will essentially never carry the fine-grained
375833
375687
  // `models: read` scope this provider needs. Adopting it would wire up a
375834
375688
  // provider that 403s on its first real call — a confusing failure for
375835
375689
  // something that is meant to be explicit opt-in BYOK.
375836
- "github-models": "GITHUB_MODELS_TOKEN"
375837
375690
  };
375838
375691
  const envKey = envMap[provider];
375839
375692
  if (envKey && process.env[envKey]) return process.env[envKey];
@@ -375845,10 +375698,21 @@ var ConfigManager = class {
375845
375698
  return configProvider?.apiKey;
375846
375699
  }
375847
375700
  async loadConfig() {
375701
+ this.retiredCleanup = void 0;
375848
375702
  const configPath = path28__namespace.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
375849
375703
  try {
375850
375704
  const raw = await fs9__namespace.default.readFile(configPath, "utf-8");
375851
- return validateConfig(JSON.parse(raw));
375705
+ const parsed = JSON.parse(raw);
375706
+ const cleanup = stripRetiredProviders(parsed);
375707
+ if (didCleanupChangeAnything(cleanup)) {
375708
+ this.retiredCleanup = cleanup;
375709
+ try {
375710
+ await fs9__namespace.default.writeFile(configPath, JSON.stringify(parsed, null, 2), "utf-8");
375711
+ } catch (err) {
375712
+ console.warn(`Could not persist the retired-provider migration: ${err instanceof Error ? err.message : String(err)}`);
375713
+ }
375714
+ }
375715
+ return validateConfig(parsed);
375852
375716
  } catch (err) {
375853
375717
  if (err.code === "ENOENT") {
375854
375718
  return validateConfig({});
@@ -375857,27 +375721,25 @@ var ConfigManager = class {
375857
375721
  }
375858
375722
  }
375859
375723
  async injectEnvKeys() {
375860
- const isFirstRun = this.config.providers.length === 0;
375724
+ const wasEmpty = this.config.providers.length === 0;
375725
+ const emptiedByRetirement = wasEmpty && !!this.retiredCleanup;
375861
375726
  const envProviders = [
375862
375727
  { env: "ANTHROPIC_API_KEY", type: "anthropic" },
375863
375728
  { env: "OPENAI_API_KEY", type: "openai" },
375864
375729
  { env: "GOOGLE_API_KEY", type: "gemini" },
375865
- { env: "AZURE_OPENAI_KEY", type: "azure" },
375866
- // See getApiKey() above for why this is GITHUB_MODELS_TOKEN and never
375867
- // the ambient GITHUB_TOKEN.
375868
- { env: "GITHUB_MODELS_TOKEN", type: "github-models" }
375730
+ { env: "AZURE_OPENAI_KEY", type: "azure" }
375869
375731
  ];
375870
375732
  for (const { env, type } of envProviders) {
375871
375733
  const key = process.env[env];
375872
375734
  if (!key) continue;
375873
375735
  const existing = this.config.providers.find((p3) => p3.type === type);
375874
- if (!existing && isFirstRun) {
375736
+ if (!existing && wasEmpty) {
375875
375737
  this.config.providers.push({ type, apiKey: key });
375876
375738
  } else if (existing && !existing.apiKey) {
375877
375739
  existing.apiKey = key;
375878
375740
  }
375879
375741
  }
375880
- if (isFirstRun && !this.config.providers.find((p3) => p3.type === "ollama")) {
375742
+ if (wasEmpty && !emptiedByRetirement && !this.config.providers.find((p3) => p3.type === "ollama")) {
375881
375743
  this.config.providers.push({ type: "ollama" });
375882
375744
  }
375883
375745
  }
@@ -379298,7 +379160,9 @@ function gatherSyncBundle(config2) {
379298
379160
  if (Object.keys(prefs).length) bundle.prefs = prefs;
379299
379161
  return bundle;
379300
379162
  }
379301
- function applySyncBundle(bundle, config2) {
379163
+ function applySyncBundle(bundle, config2, cleanup) {
379164
+ const dropped = stripRetiredProviders(bundle);
379165
+ if (cleanup) cleanup.removed = dropped.removed;
379302
379166
  const next = { ...config2, tools: { ...config2.tools } };
379303
379167
  if (bundle.providers) next.providers = mergeByKey(config2.providers, bundle.providers, providerSig) ?? [];
379304
379168
  if (bundle.webSearch) next.tools.webSearch = { ...config2.tools?.webSearch, ...bundle.webSearch };
@@ -379314,6 +379178,14 @@ function applySyncBundle(bundle, config2) {
379314
379178
  }
379315
379179
  if (bundle.models) next.models = { ...config2.models, ...bundle.models };
379316
379180
  if (bundle.budget) next.budget = { ...config2.budget, ...bundle.budget };
379181
+ const merged = { ...next.models };
379182
+ const clearedFromMerge = clearRetiredPins(merged);
379183
+ if (clearedFromMerge.length > 0) next.models = merged;
379184
+ if (cleanup) {
379185
+ cleanup.clearedPins = ["t1", "t2", "t3"].filter(
379186
+ (t4) => (dropped.clearedPins.includes(t4) || clearedFromMerge.includes(t4)) && next.models?.[t4] === void 0
379187
+ );
379188
+ }
379317
379189
  if (bundle.prefs) {
379318
379190
  const p3 = bundle.prefs;
379319
379191
  if (p3.autoBias !== void 0) next.autoBias = p3.autoBias;
@@ -379785,9 +379657,6 @@ exports.DOCUMENT_MIME = DOCUMENT_MIME;
379785
379657
  exports.DashboardServer = DashboardServer;
379786
379658
  exports.DependencyScheduler = DependencyScheduler;
379787
379659
  exports.FileMcpOAuthStore = FileMcpOAuthStore;
379788
- exports.GITHUB_MODELS_API_VERSION = GITHUB_MODELS_API_VERSION;
379789
- exports.GITHUB_MODELS_CATALOG_URL = GITHUB_MODELS_CATALOG_URL;
379790
- exports.GITHUB_MODELS_INFERENCE_URL = GITHUB_MODELS_INFERENCE_URL;
379791
379660
  exports.GLOBAL_CONFIG_DIR = GLOBAL_CONFIG_DIR;
379792
379661
  exports.GLOBAL_CREDENTIALS_FILE = GLOBAL_CREDENTIALS_FILE;
379793
379662
  exports.GLOBAL_DB_FILE = GLOBAL_DB_FILE;
@@ -379860,6 +379729,7 @@ exports.failures = failures;
379860
379729
  exports.fileExt = fileExt;
379861
379730
  exports.fileOAuthProvider = fileOAuthProvider;
379862
379731
  exports.gatherSyncBundle = gatherSyncBundle;
379732
+ exports.hasUsableProvider = hasUsableProvider;
379863
379733
  exports.hashContent = hashContent;
379864
379734
  exports.heuristicCodeChunker = heuristicCodeChunker;
379865
379735
  exports.inferAzureBaseModel = inferAzureBaseModel;