cascade-ai 0.64.0 → 0.65.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.
@@ -224264,7 +224264,7 @@ function reconcilePrice(model, live) {
224264
224264
  }
224265
224265
 
224266
224266
  // src/constants.ts
224267
- var CASCADE_VERSION = "0.64.0";
224267
+ var CASCADE_VERSION = "0.65.0";
224268
224268
  var CASCADE_CONFIG_DIR = ".cascade";
224269
224269
  var CASCADE_MD_FILE = "CASCADE.md";
224270
224270
  var CASCADE_IGNORE_FILE = ".cascadeignore";
@@ -224671,6 +224671,9 @@ var DEFAULT_THEME = "cascade";
224671
224671
  var OLLAMA_BASE_URL = "http://localhost:11434";
224672
224672
  var LM_STUDIO_BASE_URL = "http://localhost:1234";
224673
224673
  var AZURE_BASE_URL_TEMPLATE = "https://{resource}.openai.azure.com";
224674
+ var GITHUB_MODELS_INFERENCE_URL = "https://models.github.ai/inference";
224675
+ var GITHUB_MODELS_CATALOG_URL = "https://models.github.ai/catalog/models";
224676
+ var GITHUB_MODELS_API_VERSION = "2026-03-10";
224674
224677
  var TOOL_NAMES = {
224675
224678
  SHELL: "shell",
224676
224679
  FILE_READ: "file_read",
@@ -224705,7 +224708,8 @@ var PROVIDER_DISPLAY_NAMES = {
224705
224708
  gemini: "Google Gemini",
224706
224709
  azure: "Azure OpenAI",
224707
224710
  "openai-compatible": "OpenAI-Compatible",
224708
- ollama: "Ollama (Local)"
224711
+ ollama: "Ollama (Local)",
224712
+ "github-models": "GitHub Models"
224709
224713
  };
224710
224714
 
224711
224715
  // src/utils/cost.ts
@@ -251583,7 +251587,7 @@ function preferIpv4Host(url2) {
251583
251587
  return url2;
251584
251588
  }
251585
251589
  var MAX_REDIRECTS = 5;
251586
- async function nodeHttpFetch(input, init2 = {}, redirectCount = 0) {
251590
+ async function nodeHttpFetch(input, init2 = {}, redirectCount = 0, opts = {}) {
251587
251591
  const urlStr = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
251588
251592
  const u2 = new URL(preferIpv4Host(urlStr) ?? urlStr);
251589
251593
  const lib = u2.protocol === "https:" ? https2__default.default : http2__default.default;
@@ -251614,9 +251618,13 @@ async function nodeHttpFetch(input, init2 = {}, redirectCount = 0) {
251614
251618
  if (status >= 300 && status < 400 && location && redirectCount < MAX_REDIRECTS) {
251615
251619
  res.resume();
251616
251620
  const nextUrl = new URL(location, u2).href;
251621
+ if (opts.allowedRedirectOrigin && new URL(nextUrl).origin !== opts.allowedRedirectOrigin) {
251622
+ reject(new Error(`Refusing cross-origin redirect to ${new URL(nextUrl).origin}`));
251623
+ return;
251624
+ }
251617
251625
  const downgrade = status === 303 || (status === 301 || status === 302) && method !== "GET" && method !== "HEAD";
251618
251626
  const nextInit = downgrade ? { ...init2, method: "GET", body: void 0 } : init2;
251619
- resolve2(nodeHttpFetch(nextUrl, nextInit, redirectCount + 1));
251627
+ resolve2(nodeHttpFetch(nextUrl, nextInit, redirectCount + 1, opts));
251620
251628
  return;
251621
251629
  }
251622
251630
  const encoding = (res.headers["content-encoding"] ?? "").toLowerCase();
@@ -251645,6 +251653,206 @@ async function nodeHttpFetch(input, init2 = {}, redirectCount = 0) {
251645
251653
  });
251646
251654
  }
251647
251655
 
251656
+ // src/providers/github-models.ts
251657
+ var GITHUB_MODELS_MAX_OUTPUT_TOKENS = 4e3;
251658
+ var GITHUB_MODELS_MAX_INPUT_TOKENS = 8e3;
251659
+ var DEFAULT_CONTEXT_WINDOW = 128e3;
251660
+ var GITHUB_MODELS_MAX_CATALOG_PAGES = 20;
251661
+ var GITHUB_MODELS_CATALOG_ORIGIN = new URL(GITHUB_MODELS_CATALOG_URL).origin;
251662
+ function stripModelOwnerPrefix(id) {
251663
+ const slash = (id || "").indexOf("/");
251664
+ return slash === -1 ? id : id.slice(slash + 1);
251665
+ }
251666
+ function pickString(o3, keys) {
251667
+ for (const k4 of keys) {
251668
+ const v6 = o3[k4];
251669
+ if (typeof v6 === "string" && v6.length > 0) return v6;
251670
+ }
251671
+ return void 0;
251672
+ }
251673
+ function pickNumber(o3, keys) {
251674
+ for (const k4 of keys) {
251675
+ const v6 = k4.includes(".") ? o3[k4.split(".")[0]]?.[k4.split(".")[1]] : o3[k4];
251676
+ if (typeof v6 === "number" && Number.isFinite(v6) && v6 > 0) return v6;
251677
+ }
251678
+ return void 0;
251679
+ }
251680
+ function looksVisionCapable(o3) {
251681
+ for (const k4 of ["supported_input_modalities", "input_modalities", "modalities", "capabilities"]) {
251682
+ const v6 = o3[k4];
251683
+ if (Array.isArray(v6) && v6.some((x5) => typeof x5 === "string" && /image|vision/i.test(x5))) return true;
251684
+ }
251685
+ return false;
251686
+ }
251687
+ function looksToolCapable(o3) {
251688
+ const params = o3["supported_parameters"];
251689
+ if (Array.isArray(params) && params.some((x5) => typeof x5 === "string" && /^tools?$|function.?call/i.test(x5))) {
251690
+ return true;
251691
+ }
251692
+ for (const k4 of ["capabilities", "supported_output_modalities"]) {
251693
+ const v6 = o3[k4];
251694
+ if (Array.isArray(v6) && v6.some((x5) => typeof x5 === "string" && /tool|function.?call/i.test(x5))) return true;
251695
+ }
251696
+ const caps = o3["capabilities"];
251697
+ if (caps && typeof caps === "object" && !Array.isArray(caps)) {
251698
+ const c4 = caps;
251699
+ if (c4["function_calling"] === true || c4["tool_calling"] === true || c4["tools"] === true) return true;
251700
+ }
251701
+ return false;
251702
+ }
251703
+ function parseNextLink(linkHeader) {
251704
+ if (!linkHeader) return null;
251705
+ for (const part of linkHeader.split(",")) {
251706
+ const match = part.match(/<([^>]+)>\s*;\s*rel="next"/);
251707
+ if (match) return match[1];
251708
+ }
251709
+ return null;
251710
+ }
251711
+ var GitHubModelsProvider = class extends OpenAIProvider {
251712
+ constructor(config2, model) {
251713
+ super({ ...config2, apiKey: config2.apiKey ?? "" }, model);
251714
+ this.useMaxCompletionTokens = isReasoningModel(stripModelOwnerPrefix(model.id));
251715
+ this.client = new openai_default({
251716
+ apiKey: config2.apiKey ?? "",
251717
+ baseURL: GITHUB_MODELS_INFERENCE_URL,
251718
+ defaultHeaders: { "X-GitHub-Api-Version": GITHUB_MODELS_API_VERSION, "User-Agent": "Cascade-AI" }
251719
+ });
251720
+ }
251721
+ /**
251722
+ * `OpenAIProvider.generateStream()` builds its request budget as
251723
+ * `options.maxTokens ?? this.model.maxOutputTokens` — an explicit per-call
251724
+ * `maxTokens` (T1Administrator's final compilation asks for 8,000, well
251725
+ * above GitHub's real ~4K hard cap) wins outright and is sent unclamped.
251726
+ * GitHub then rejects the request instead of answering, so a run routed to
251727
+ * this provider for that step fails outright rather than getting a
251728
+ * (perfectly serviceable, just shorter) completion. Clamp here, once, right
251729
+ * before delegating to the inherited implementation, rather than teaching
251730
+ * every caller GitHub's specific cap.
251731
+ */
251732
+ async generateStream(options, onChunk) {
251733
+ const clamped = options.maxTokens !== void 0 && options.maxTokens > GITHUB_MODELS_MAX_OUTPUT_TOKENS ? { ...options, maxTokens: GITHUB_MODELS_MAX_OUTPUT_TOKENS } : options;
251734
+ return super.generateStream(clamped, onChunk);
251735
+ }
251736
+ /**
251737
+ * The catalog is a GitHub REST resource, not an OpenAI `/models` list, and
251738
+ * wants GitHub's own header trio — the plain `Accept: application/json` the
251739
+ * openai-compatible provider sends is not the same request.
251740
+ */
251741
+ catalogHeaders() {
251742
+ return {
251743
+ Accept: "application/vnd.github+json",
251744
+ Authorization: `Bearer ${this.config.apiKey ?? ""}`,
251745
+ "X-GitHub-Api-Version": GITHUB_MODELS_API_VERSION,
251746
+ // GitHub's REST API rejects any request with no User-Agent — a plain 403
251747
+ // regardless of how valid the PAT is. nodeHttpFetch is a thin wrapper
251748
+ // over node:http/https, neither of which sets one the way a browser's
251749
+ // fetch or a full HTTP client library would, so this has to be explicit.
251750
+ "User-Agent": "Cascade-AI"
251751
+ };
251752
+ }
251753
+ async listModels() {
251754
+ const raw = [];
251755
+ let url2 = GITHUB_MODELS_CATALOG_URL;
251756
+ let pages = 0;
251757
+ const visited = /* @__PURE__ */ new Set();
251758
+ while (url2 && pages < GITHUB_MODELS_MAX_CATALOG_PAGES) {
251759
+ if (visited.has(url2)) break;
251760
+ visited.add(url2);
251761
+ const res = await nodeHttpFetch(url2, { headers: this.catalogHeaders() }, 0, {
251762
+ allowedRedirectOrigin: GITHUB_MODELS_CATALOG_ORIGIN
251763
+ });
251764
+ if (!res.ok) throw new Error(`GitHub Models catalog ${url2} returned HTTP ${res.status}`);
251765
+ const body = await res.json();
251766
+ const pageItems = Array.isArray(body) ? body : Array.isArray(body?.models) ? body.models : Array.isArray(body?.data) ? body.data : [];
251767
+ raw.push(...pageItems);
251768
+ const next = parseNextLink(res.headers.get("link"));
251769
+ if (next) {
251770
+ const resolved = new URL(next, url2);
251771
+ if (resolved.origin !== GITHUB_MODELS_CATALOG_ORIGIN) {
251772
+ throw new Error(`Refusing cross-origin GitHub Models pagination URL: ${resolved.origin}`);
251773
+ }
251774
+ url2 = resolved.href;
251775
+ } else {
251776
+ url2 = null;
251777
+ }
251778
+ pages++;
251779
+ }
251780
+ const entries = raw.map((m3) => {
251781
+ if (typeof m3 === "string") {
251782
+ return { id: m3, name: m3, contextWindow: DEFAULT_CONTEXT_WINDOW, vision: false, tools: false };
251783
+ }
251784
+ if (!m3 || typeof m3 !== "object") return void 0;
251785
+ const o3 = m3;
251786
+ const id = pickString(o3, ["id", "name", "model"]);
251787
+ if (!id) return void 0;
251788
+ return {
251789
+ id,
251790
+ name: pickString(o3, ["friendly_name", "display_name", "name"]) ?? id,
251791
+ contextWindow: pickNumber(o3, [
251792
+ "context_window",
251793
+ "context_length",
251794
+ "max_input_tokens",
251795
+ "limits.max_input_tokens",
251796
+ "limits.max_context_tokens"
251797
+ ]) ?? DEFAULT_CONTEXT_WINDOW,
251798
+ vision: looksVisionCapable(o3),
251799
+ tools: looksToolCapable(o3)
251800
+ };
251801
+ }).filter((e3) => e3 !== void 0);
251802
+ if (entries.length === 0) return [];
251803
+ const chat = entries.filter((e3) => isChatModel(e3.id));
251804
+ return (chat.length ? chat : entries).map((e3) => ({
251805
+ id: e3.id,
251806
+ // full `owner/model` — what the API is addressed by
251807
+ name: e3.name,
251808
+ provider: "github-models",
251809
+ // GitHub's real per-request input cap, not the base model's larger one
251810
+ // (see GITHUB_MODELS_MAX_INPUT_TOKENS above) — the same reasoning
251811
+ // already applied to maxOutputTokens below, for the input side.
251812
+ contextWindow: Math.min(e3.contextWindow, GITHUB_MODELS_MAX_INPUT_TOKENS),
251813
+ isVisionCapable: e3.vision,
251814
+ // A REAL $0, not a missing number — usage is bundled into the quota of
251815
+ // the plan the PAT belongs to and is never billed per token, so
251816
+ // `pricingUnknown: false` is the honest value (see the flag's contract in
251817
+ // core/router/pricing.ts: 0 alone is ambiguous). Set here rather than in
251818
+ // pricing-data.json because resolvePricing() matches exactly on
251819
+ // provider|model|region with no wildcard: every catalog model would need
251820
+ // a hand-curated row, and the next one GitHub adds would read as "cost
251821
+ // unknown" — an inconsistent picture for a billing reality that is
251822
+ // identical across the entire catalog.
251823
+ //
251824
+ // `isLocal` is the OTHER way to reach a real $0, and it is the wrong one:
251825
+ // it means "runs on this machine" and carries consequences that are
251826
+ // simply false for a hosted API — the shared concurrency-1
251827
+ // LocalRequestQueue (serialising these calls behind actual Ollama GPU
251828
+ // inference), the 300s local rather than 120s cloud inference timeout,
251829
+ // and a literal "[local]" tag in `cascade models`.
251830
+ inputCostPer1kTokens: 0,
251831
+ outputCostPer1kTokens: 0,
251832
+ pricingUnknown: false,
251833
+ maxOutputTokens: GITHUB_MODELS_MAX_OUTPUT_TOKENS,
251834
+ supportsStreaming: true,
251835
+ // Not every catalog model supports function/tool calling — a hardcoded
251836
+ // `true` sent `tools` to models that reject the parameter outright and
251837
+ // never engaged t3-worker.ts's existing text-tool fallback (only an
251838
+ // explicit `false` does). See looksToolCapable's own comment for why
251839
+ // "not positively advertised" defaults to false rather than true here.
251840
+ supportsToolUse: e3.tools,
251841
+ isLocal: false
251842
+ }));
251843
+ }
251844
+ async isAvailable() {
251845
+ try {
251846
+ const res = await nodeHttpFetch(GITHUB_MODELS_CATALOG_URL, { headers: this.catalogHeaders() }, 0, {
251847
+ allowedRedirectOrigin: GITHUB_MODELS_CATALOG_ORIGIN
251848
+ });
251849
+ return res.ok;
251850
+ } catch {
251851
+ return false;
251852
+ }
251853
+ }
251854
+ };
251855
+
251648
251856
  // src/providers/ollama.ts
251649
251857
  var TOOL_CAPABLE_FAMILIES = [
251650
251858
  "llama3.1",
@@ -252063,16 +252271,23 @@ var ModelSelector = class {
252063
252271
  return model;
252064
252272
  }
252065
252273
  }
252274
+ for (const model of this.availableModels.values()) {
252275
+ if (this.availableProviders.has(model.provider) && model.isVisionCapable) return model;
252276
+ }
252066
252277
  return null;
252067
252278
  }
252068
252279
  getNextFallback(currentModelId, tier) {
252069
252280
  const priority = this.getPriorityList(tier);
252070
252281
  const currentIdx = priority.indexOf(currentModelId);
252071
- if (currentIdx === -1) return null;
252072
- for (let i4 = currentIdx + 1; i4 < priority.length; i4++) {
252073
- const key = priority[i4];
252074
- const model = this.availableModels.get(key);
252075
- if (model && this.isUsable(model)) return model;
252282
+ if (currentIdx !== -1) {
252283
+ for (let i4 = currentIdx + 1; i4 < priority.length; i4++) {
252284
+ const key = priority[i4];
252285
+ const model = this.availableModels.get(key);
252286
+ if (model && this.isUsable(model)) return model;
252287
+ }
252288
+ }
252289
+ for (const [id, model] of this.availableModels) {
252290
+ if (id !== currentModelId && this.isUsable(model)) return model;
252076
252291
  }
252077
252292
  return null;
252078
252293
  }
@@ -252148,7 +252363,7 @@ var ModelSelector = class {
252148
252363
  if (overrideModelId.includes(":")) {
252149
252364
  const parts = overrideModelId.split(":");
252150
252365
  const prefix = parts[0].toLowerCase();
252151
- const validProviders = ["anthropic", "openai", "gemini", "azure", "openai-compatible", "ollama"];
252366
+ const validProviders = ["anthropic", "openai", "gemini", "azure", "openai-compatible", "ollama", "github-models"];
252152
252367
  if (validProviders.includes(prefix)) {
252153
252368
  providerStr = prefix;
252154
252369
  actualId = parts.slice(1).join(":");
@@ -252168,6 +252383,31 @@ var ModelSelector = class {
252168
252383
  else if (this.availableProviders.has("openai-compatible")) providerStr = "openai-compatible";
252169
252384
  else if (this.availableProviders.size === 1) providerStr = Array.from(this.availableProviders)[0];
252170
252385
  }
252386
+ if (providerStr === "github-models" && this.availableProviders.has("github-models")) {
252387
+ const dynamicModel = {
252388
+ id: actualId,
252389
+ name: actualId,
252390
+ provider: "github-models",
252391
+ contextWindow: GITHUB_MODELS_MAX_INPUT_TOKENS,
252392
+ isVisionCapable: false,
252393
+ inputCostPer1kTokens: 0,
252394
+ outputCostPer1kTokens: 0,
252395
+ pricingUnknown: false,
252396
+ maxOutputTokens: GITHUB_MODELS_MAX_OUTPUT_TOKENS,
252397
+ supportsStreaming: true,
252398
+ // Unconfirmed (no live catalog entry to read a real capability from)
252399
+ // must default to false, not left undefined: t3-worker.ts's text-tool
252400
+ // fallback only engages on a strict `=== false`, so an undefined
252401
+ // value here sends native `tools` to an unverified multi-vendor
252402
+ // model and any that don't support function calling reject the
252403
+ // request outright. Same default-to-false-when-unconfirmed policy
252404
+ // listModels() already applies for a real catalog entry.
252405
+ supportsToolUse: false,
252406
+ isLocal: false
252407
+ };
252408
+ this.addDynamicModel(dynamicModel);
252409
+ return dynamicModel;
252410
+ }
252171
252411
  if (providerStr && this.availableProviders.has(providerStr)) {
252172
252412
  const dynamicModel = withResolvedPricing({
252173
252413
  id: actualId,
@@ -252260,7 +252500,20 @@ var DEFAULT_PROVIDER_TPM = {
252260
252500
  gemini: 6e4,
252261
252501
  azure: 3e4,
252262
252502
  "openai-compatible": 3e4,
252263
- ollama: Number.POSITIVE_INFINITY
252503
+ ollama: Number.POSITIVE_INFINITY,
252504
+ // An order of magnitude below the others, on purpose. GitHub Models is
252505
+ // rate-limited by REQUESTS (Free ≈10 RPM, Pro ≈20, plus low daily caps), and
252506
+ // this bucket meters TOKENS — there is no request-count limiter in this
252507
+ // codebase, and building one is not in scope. The approximation works because
252508
+ // the router reserves `model.maxOutputTokens + 512` per call, and the
252509
+ // provider reports GitHub's real ~4K per-request cap rather than the
252510
+ // underlying model's larger one — so every call withdraws a near-constant
252511
+ // ~4.5K regardless of how short the reply actually turns out to be. At 8K/min
252512
+ // that lands around 1–2 calls per minute of headroom: comfortably inside even
252513
+ // the Free tier, with room for the daily cap to matter more than this does.
252514
+ // Users on a higher tier can raise it with no code change through the generic
252515
+ // `config.rateLimits.providerTpm['github-models']` override.
252516
+ "github-models": 8e3
252264
252517
  };
252265
252518
  var TpmLimiter = class {
252266
252519
  buckets = /* @__PURE__ */ new Map();
@@ -252616,6 +252869,7 @@ var ModelProfiler = class {
252616
252869
  (m3) => !alreadyProfiled.has(m3.id) && !SKIP_PATTERN.test(m3.id) && !SKIP_PATTERN.test(m3.name)
252617
252870
  );
252618
252871
  if (toProfile.length === 0) return;
252872
+ const probesHitGithubModels = this.router?.getModelForTier("T3")?.provider === "github-models";
252619
252873
  const openRouterModels = await fetchOpenRouterModels();
252620
252874
  const orByNormalizedId = /* @__PURE__ */ new Map();
252621
252875
  for (const m3 of openRouterModels) {
@@ -252630,7 +252884,7 @@ var ModelProfiler = class {
252630
252884
  if (orMatch?.description) {
252631
252885
  specializations = extractSpecializations(orMatch.description);
252632
252886
  }
252633
- if (specializations.length === 0 && this.router) {
252887
+ if (specializations.length === 0 && this.router && !probesHitGithubModels) {
252634
252888
  specializations = await queryModelDirectly(this.router);
252635
252889
  }
252636
252890
  this.store.saveModelProfile(model.id, model.provider, specializations);
@@ -252850,6 +253104,7 @@ var LiveDataProvider = class {
252850
253104
  */
252851
253105
  applyLivePricing(models) {
252852
253106
  return models.map((m3) => {
253107
+ if (m3.provider === "github-models") return m3;
252853
253108
  const p3 = this.getLivePrice(m3.baseModelId ?? m3.id);
252854
253109
  if (!p3) return m3;
252855
253110
  const reconciled = reconcilePrice(m3, p3);
@@ -252888,6 +253143,7 @@ var LiveDataProvider = class {
252888
253143
  */
252889
253144
  applyLiveCapabilities(models) {
252890
253145
  return models.map((m3) => {
253146
+ if (m3.provider === "github-models") return m3;
252891
253147
  const c4 = this.getCapability(m3.baseModelId ?? m3.id);
252892
253148
  if (!c4) return m3;
252893
253149
  const next = { ...m3 };
@@ -253191,13 +253447,16 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
253191
253447
  for (const model of azureDeployments) this.selector.addDynamicModel(model);
253192
253448
  availableProviders.add("azure");
253193
253449
  }
253450
+ if (availableProviders.has("github-models")) {
253451
+ await this.discoverGitHubModelsCatalog(config2);
253452
+ }
253194
253453
  await this.validateCloudProviderModels(config2);
253195
253454
  for (const tier of ["T1", "T2", "T3"]) {
253196
253455
  const override = tier === "T1" ? config2.models.t1 : tier === "T2" ? config2.models.t2 : config2.models.t3;
253197
253456
  if (!override || override === "auto") continue;
253198
253457
  const model = this.selector.selectForTier(tier, override);
253199
253458
  if (!model) {
253200
- const knownProviders = ["anthropic", "openai", "gemini", "azure", "openai-compatible", "ollama"];
253459
+ const knownProviders = ["anthropic", "openai", "gemini", "azure", "openai-compatible", "ollama", "github-models"];
253201
253460
  const hasProviderPrefix = override.includes(":") && knownProviders.some((p3) => override.startsWith(p3 + ":"));
253202
253461
  if (hasProviderPrefix) {
253203
253462
  const provider = override.split(":")[0];
@@ -253352,7 +253611,7 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
253352
253611
  * models without a package upgrade. Mirrors discoverOllamaModels.
253353
253612
  */
253354
253613
  async discoverProviderModels() {
253355
- const cloud = ["anthropic", "openai", "gemini", "azure", "openai-compatible"];
253614
+ const cloud = ["anthropic", "openai", "gemini", "azure", "openai-compatible", "github-models"];
253356
253615
  const tasks = cloud.map(async (type) => {
253357
253616
  if (!this.selector.isProviderAvailable(type)) return;
253358
253617
  const seed = this.getAnyModelForProvider(type);
@@ -253407,6 +253666,9 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
253407
253666
  this.ensureProvider(options.model, this.config.providers);
253408
253667
  }
253409
253668
  let model = requireVision ? this.selector.selectVisionModel() : options.model ?? this.tierModels.get(tier) ?? this.selector.selectForTier(tier) ?? void 0;
253669
+ if (requireVision && model) {
253670
+ this.ensureProvider(model, this.config.providers);
253671
+ }
253410
253672
  if (options.forceLocal && model && !this.isPrivateModel(model)) {
253411
253673
  const localModel = this.selector.getAllAvailableModels().find((m3) => this.isPrivateModel(m3));
253412
253674
  if (!localModel) {
@@ -253420,7 +253682,8 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
253420
253682
  if (!model) throw new Error(`No model available for tier ${tier}`);
253421
253683
  const provider = this.getProvider(model);
253422
253684
  if (!provider) throw new Error(`No provider for model ${model.id}`);
253423
- const estimatedTokens = (options.maxTokens ?? model.maxOutputTokens ?? 1024) + 512;
253685
+ const requestedTokens = options.maxTokens ?? model.maxOutputTokens ?? 1024;
253686
+ const estimatedTokens = Math.min(requestedTokens, model.maxOutputTokens ?? requestedTokens) + 512;
253424
253687
  if (this.tpmLimiter) {
253425
253688
  await this.tpmLimiter.acquire(model.provider, estimatedTokens);
253426
253689
  }
@@ -253511,7 +253774,8 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
253511
253774
  });
253512
253775
  releaseLocalSlot?.();
253513
253776
  releaseLocalSlot = void 0;
253514
- return this.generate(tier, options, onChunk, requireVision);
253777
+ const retryOpts = options.model && options.model.id === model.id ? { ...options, model: void 0 } : options;
253778
+ return this.generate(tier, retryOpts, onChunk, requireVision);
253515
253779
  }
253516
253780
  }
253517
253781
  if (isModelNotFoundError(errMsg)) {
@@ -253862,6 +254126,26 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
253862
254126
  return false;
253863
254127
  }
253864
254128
  }
254129
+ /**
254130
+ * Registers GitHub Models' real catalog ids via addDynamicModel so init()'s
254131
+ * tier-fill loop can pick one. Registration alone does not create a bound
254132
+ * BaseProvider instance (see ensureProvider) — the tier-fill loop's own
254133
+ * ensureProvider call does that for whichever model it actually selects,
254134
+ * which is why this runs BEFORE that loop rather than only in the
254135
+ * background discoverProviderModels() refresh.
254136
+ */
254137
+ async discoverGitHubModelsCatalog(config2) {
254138
+ const cfg = config2.providers.find((p3) => p3.type === "github-models") ?? { type: "github-models" };
254139
+ const seed = this.getAnyModelForProvider("github-models");
254140
+ if (!seed) return;
254141
+ try {
254142
+ const provider = new GitHubModelsProvider(cfg, seed);
254143
+ const models = await provider.listModels();
254144
+ for (const m3 of models) this.selector.addDynamicModel(m3);
254145
+ } catch (err) {
254146
+ console.warn("[router] GitHub Models catalog discovery failed:", err instanceof Error ? err.message : err);
254147
+ }
254148
+ }
253865
254149
  ensureProvider(model, configs) {
253866
254150
  const key = `${model.provider}:${model.id}`;
253867
254151
  if (this.providers.has(key)) return;
@@ -253886,6 +254170,8 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
253886
254170
  return new OllamaProvider(cfg, model);
253887
254171
  case "openai-compatible":
253888
254172
  return new OpenAICompatibleProvider(cfg, model);
254173
+ case "github-models":
254174
+ return new GitHubModelsProvider(cfg, model);
253889
254175
  default:
253890
254176
  throw new Error(`Unsupported provider type: ${String(cfg.type)}`);
253891
254177
  }
@@ -253893,7 +254179,7 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
253893
254179
  getAnyModelForProvider(type) {
253894
254180
  const fromCatalog = Object.values(MODELS).find((m3) => m3.provider === type);
253895
254181
  if (fromCatalog) return fromCatalog;
253896
- if (type === "openai-compatible" || type === "azure") {
254182
+ if (type === "openai-compatible" || type === "azure" || type === "github-models") {
253897
254183
  return {
253898
254184
  id: type,
253899
254185
  name: type,
@@ -300996,7 +301282,7 @@ var PermissionEscalator = class extends EventEmitter__default.default {
300996
301282
 
300997
301283
  // src/config/schema.ts
300998
301284
  var ProviderConfigSchema = external_exports.object({
300999
- type: external_exports.enum(["anthropic", "openai", "gemini", "azure", "openai-compatible", "ollama"]),
301285
+ type: external_exports.enum(["anthropic", "openai", "gemini", "azure", "openai-compatible", "ollama", "github-models"]),
301000
301286
  label: external_exports.string().optional(),
301001
301287
  apiKey: external_exports.string().optional(),
301002
301288
  baseUrl: external_exports.string().url().optional(),
@@ -301176,6 +301462,18 @@ var CascadeConfigSchema = external_exports.object({
301176
301462
  budget: BudgetConfigSchema.default({}),
301177
301463
  theme: external_exports.string().default("cascade"),
301178
301464
  workspace: WorkspaceConfigSchema.default({}),
301465
+ /**
301466
+ * Per-provider tokens-per-minute overrides for the router's TpmLimiter (see
301467
+ * DEFAULT_PROVIDER_TPM in core/router/tpm-limiter.ts). Documented as a raw
301468
+ * config field for a while before this — since CascadeConfigSchema strips
301469
+ * unknown keys, a config file setting it was silently discarded and the
301470
+ * override never reached TpmLimiter's constructor. Optional/permissive on
301471
+ * provider name (not a ProviderType enum) so a config still validates
301472
+ * against a slightly newer/older provider list than this schema knows.
301473
+ */
301474
+ rateLimits: external_exports.object({
301475
+ providerTpm: external_exports.record(external_exports.string(), external_exports.number().positive()).optional()
301476
+ }).optional(),
301179
301477
  /**
301180
301478
  * Cascade Auto: when true, the TaskAnalyzer selects the optimal model for each
301181
301479
  * tier based on task type and complexity, overriding the static priority lists.
@@ -306220,7 +306518,14 @@ var ConfigManager = class {
306220
306518
  anthropic: "ANTHROPIC_API_KEY",
306221
306519
  openai: "OPENAI_API_KEY",
306222
306520
  gemini: "GOOGLE_API_KEY",
306223
- azure: "AZURE_OPENAI_KEY"
306521
+ azure: "AZURE_OPENAI_KEY",
306522
+ // Deliberately NOT `GITHUB_TOKEN`: that one is injected automatically by
306523
+ // GitHub Actions and is commonly exported in developer shells for `gh`
306524
+ // and git auth, and it will essentially never carry the fine-grained
306525
+ // `models: read` scope this provider needs. Adopting it would wire up a
306526
+ // provider that 403s on its first real call — a confusing failure for
306527
+ // something that is meant to be explicit opt-in BYOK.
306528
+ "github-models": "GITHUB_MODELS_TOKEN"
306224
306529
  };
306225
306530
  const envKey = envMap[provider];
306226
306531
  if (envKey && process.env[envKey]) return process.env[envKey];
@@ -306249,7 +306554,10 @@ var ConfigManager = class {
306249
306554
  { env: "ANTHROPIC_API_KEY", type: "anthropic" },
306250
306555
  { env: "OPENAI_API_KEY", type: "openai" },
306251
306556
  { env: "GOOGLE_API_KEY", type: "gemini" },
306252
- { env: "AZURE_OPENAI_KEY", type: "azure" }
306557
+ { env: "AZURE_OPENAI_KEY", type: "azure" },
306558
+ // See getApiKey() above for why this is GITHUB_MODELS_TOKEN and never
306559
+ // the ambient GITHUB_TOKEN.
306560
+ { env: "GITHUB_MODELS_TOKEN", type: "github-models" }
306253
306561
  ];
306254
306562
  for (const { env, type } of envProviders) {
306255
306563
  const key = process.env[env];
@@ -310136,6 +310444,9 @@ exports.DEFAULT_RETENTION_DAYS = DEFAULT_RETENTION_DAYS;
310136
310444
  exports.DEFAULT_THEME = DEFAULT_THEME;
310137
310445
  exports.DashboardServer = DashboardServer;
310138
310446
  exports.FileMcpOAuthStore = FileMcpOAuthStore;
310447
+ exports.GITHUB_MODELS_API_VERSION = GITHUB_MODELS_API_VERSION;
310448
+ exports.GITHUB_MODELS_CATALOG_URL = GITHUB_MODELS_CATALOG_URL;
310449
+ exports.GITHUB_MODELS_INFERENCE_URL = GITHUB_MODELS_INFERENCE_URL;
310139
310450
  exports.GLOBAL_CONFIG_DIR = GLOBAL_CONFIG_DIR;
310140
310451
  exports.GLOBAL_CREDENTIALS_FILE = GLOBAL_CREDENTIALS_FILE;
310141
310452
  exports.GLOBAL_DB_FILE = GLOBAL_DB_FILE;