cascade-ai 0.63.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.
package/dist/index.cjs CHANGED
@@ -1670,7 +1670,7 @@ var pricing_data_default = {
1670
1670
  ]
1671
1671
  },
1672
1672
  {
1673
- model: "veo-3.1-generate-001",
1673
+ model: "veo-3.1-generate-preview",
1674
1674
  provider: "gemini",
1675
1675
  modality: "video",
1676
1676
  currency: "USD",
@@ -1908,7 +1908,7 @@ function reconcilePrice(model, live) {
1908
1908
  }
1909
1909
 
1910
1910
  // src/constants.ts
1911
- var CASCADE_VERSION = "0.63.0";
1911
+ var CASCADE_VERSION = "0.65.0";
1912
1912
  var CASCADE_CONFIG_DIR = ".cascade";
1913
1913
  var CASCADE_MD_FILE = "CASCADE.md";
1914
1914
  var CASCADE_IGNORE_FILE = ".cascadeignore";
@@ -2315,6 +2315,9 @@ 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";
2318
2321
  var TOOL_NAMES = {
2319
2322
  SHELL: "shell",
2320
2323
  FILE_READ: "file_read",
@@ -2349,7 +2352,8 @@ var PROVIDER_DISPLAY_NAMES = {
2349
2352
  gemini: "Google Gemini",
2350
2353
  azure: "Azure OpenAI",
2351
2354
  "openai-compatible": "OpenAI-Compatible",
2352
- ollama: "Ollama (Local)"
2355
+ ollama: "Ollama (Local)",
2356
+ "github-models": "GitHub Models"
2353
2357
  };
2354
2358
 
2355
2359
  // src/utils/cost.ts
@@ -3326,7 +3330,7 @@ function preferIpv4Host(url) {
3326
3330
  return url;
3327
3331
  }
3328
3332
  var MAX_REDIRECTS = 5;
3329
- async function nodeHttpFetch(input, init = {}, redirectCount = 0) {
3333
+ async function nodeHttpFetch(input, init = {}, redirectCount = 0, opts = {}) {
3330
3334
  const urlStr = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
3331
3335
  const u = new URL(preferIpv4Host(urlStr) ?? urlStr);
3332
3336
  const lib = u.protocol === "https:" ? https__default.default : http__default.default;
@@ -3357,9 +3361,13 @@ async function nodeHttpFetch(input, init = {}, redirectCount = 0) {
3357
3361
  if (status >= 300 && status < 400 && location && redirectCount < MAX_REDIRECTS) {
3358
3362
  res.resume();
3359
3363
  const nextUrl = new URL(location, u).href;
3364
+ if (opts.allowedRedirectOrigin && new URL(nextUrl).origin !== opts.allowedRedirectOrigin) {
3365
+ reject(new Error(`Refusing cross-origin redirect to ${new URL(nextUrl).origin}`));
3366
+ return;
3367
+ }
3360
3368
  const downgrade = status === 303 || (status === 301 || status === 302) && method !== "GET" && method !== "HEAD";
3361
3369
  const nextInit = downgrade ? { ...init, method: "GET", body: void 0 } : init;
3362
- resolve2(nodeHttpFetch(nextUrl, nextInit, redirectCount + 1));
3370
+ resolve2(nodeHttpFetch(nextUrl, nextInit, redirectCount + 1, opts));
3363
3371
  return;
3364
3372
  }
3365
3373
  const encoding = (res.headers["content-encoding"] ?? "").toLowerCase();
@@ -3388,6 +3396,206 @@ async function nodeHttpFetch(input, init = {}, redirectCount = 0) {
3388
3396
  });
3389
3397
  }
3390
3398
 
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
+
3391
3599
  // src/providers/ollama.ts
3392
3600
  var TOOL_CAPABLE_FAMILIES = [
3393
3601
  "llama3.1",
@@ -3804,16 +4012,23 @@ var ModelSelector = class {
3804
4012
  return model;
3805
4013
  }
3806
4014
  }
4015
+ for (const model of this.availableModels.values()) {
4016
+ if (this.availableProviders.has(model.provider) && model.isVisionCapable) return model;
4017
+ }
3807
4018
  return null;
3808
4019
  }
3809
4020
  getNextFallback(currentModelId, tier) {
3810
4021
  const priority = this.getPriorityList(tier);
3811
4022
  const currentIdx = priority.indexOf(currentModelId);
3812
- if (currentIdx === -1) return null;
3813
- for (let i = currentIdx + 1; i < priority.length; i++) {
3814
- const key = priority[i];
3815
- const model = this.availableModels.get(key);
3816
- if (model && this.isUsable(model)) return model;
4023
+ if (currentIdx !== -1) {
4024
+ for (let i = currentIdx + 1; i < priority.length; i++) {
4025
+ const key = priority[i];
4026
+ const model = this.availableModels.get(key);
4027
+ if (model && this.isUsable(model)) return model;
4028
+ }
4029
+ }
4030
+ for (const [id, model] of this.availableModels) {
4031
+ if (id !== currentModelId && this.isUsable(model)) return model;
3817
4032
  }
3818
4033
  return null;
3819
4034
  }
@@ -3889,7 +4104,7 @@ var ModelSelector = class {
3889
4104
  if (overrideModelId.includes(":")) {
3890
4105
  const parts = overrideModelId.split(":");
3891
4106
  const prefix = parts[0].toLowerCase();
3892
- const validProviders = ["anthropic", "openai", "gemini", "azure", "openai-compatible", "ollama"];
4107
+ const validProviders = ["anthropic", "openai", "gemini", "azure", "openai-compatible", "ollama", "github-models"];
3893
4108
  if (validProviders.includes(prefix)) {
3894
4109
  providerStr = prefix;
3895
4110
  actualId = parts.slice(1).join(":");
@@ -3909,6 +4124,31 @@ var ModelSelector = class {
3909
4124
  else if (this.availableProviders.has("openai-compatible")) providerStr = "openai-compatible";
3910
4125
  else if (this.availableProviders.size === 1) providerStr = Array.from(this.availableProviders)[0];
3911
4126
  }
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
+ }
3912
4152
  if (providerStr && this.availableProviders.has(providerStr)) {
3913
4153
  const dynamicModel = withResolvedPricing({
3914
4154
  id: actualId,
@@ -4001,7 +4241,20 @@ var DEFAULT_PROVIDER_TPM = {
4001
4241
  gemini: 6e4,
4002
4242
  azure: 3e4,
4003
4243
  "openai-compatible": 3e4,
4004
- ollama: Number.POSITIVE_INFINITY
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
4005
4258
  };
4006
4259
  var TpmLimiter = class {
4007
4260
  buckets = /* @__PURE__ */ new Map();
@@ -4357,6 +4610,7 @@ var ModelProfiler = class {
4357
4610
  (m) => !alreadyProfiled.has(m.id) && !SKIP_PATTERN.test(m.id) && !SKIP_PATTERN.test(m.name)
4358
4611
  );
4359
4612
  if (toProfile.length === 0) return;
4613
+ const probesHitGithubModels = this.router?.getModelForTier("T3")?.provider === "github-models";
4360
4614
  const openRouterModels = await fetchOpenRouterModels();
4361
4615
  const orByNormalizedId = /* @__PURE__ */ new Map();
4362
4616
  for (const m of openRouterModels) {
@@ -4371,7 +4625,7 @@ var ModelProfiler = class {
4371
4625
  if (orMatch?.description) {
4372
4626
  specializations = extractSpecializations(orMatch.description);
4373
4627
  }
4374
- if (specializations.length === 0 && this.router) {
4628
+ if (specializations.length === 0 && this.router && !probesHitGithubModels) {
4375
4629
  specializations = await queryModelDirectly(this.router);
4376
4630
  }
4377
4631
  this.store.saveModelProfile(model.id, model.provider, specializations);
@@ -4591,6 +4845,7 @@ var LiveDataProvider = class {
4591
4845
  */
4592
4846
  applyLivePricing(models) {
4593
4847
  return models.map((m) => {
4848
+ if (m.provider === "github-models") return m;
4594
4849
  const p = this.getLivePrice(m.baseModelId ?? m.id);
4595
4850
  if (!p) return m;
4596
4851
  const reconciled = reconcilePrice(m, p);
@@ -4629,6 +4884,7 @@ var LiveDataProvider = class {
4629
4884
  */
4630
4885
  applyLiveCapabilities(models) {
4631
4886
  return models.map((m) => {
4887
+ if (m.provider === "github-models") return m;
4632
4888
  const c = this.getCapability(m.baseModelId ?? m.id);
4633
4889
  if (!c) return m;
4634
4890
  const next = { ...m };
@@ -4932,13 +5188,16 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
4932
5188
  for (const model of azureDeployments) this.selector.addDynamicModel(model);
4933
5189
  availableProviders.add("azure");
4934
5190
  }
5191
+ if (availableProviders.has("github-models")) {
5192
+ await this.discoverGitHubModelsCatalog(config);
5193
+ }
4935
5194
  await this.validateCloudProviderModels(config);
4936
5195
  for (const tier of ["T1", "T2", "T3"]) {
4937
5196
  const override = tier === "T1" ? config.models.t1 : tier === "T2" ? config.models.t2 : config.models.t3;
4938
5197
  if (!override || override === "auto") continue;
4939
5198
  const model = this.selector.selectForTier(tier, override);
4940
5199
  if (!model) {
4941
- const knownProviders = ["anthropic", "openai", "gemini", "azure", "openai-compatible", "ollama"];
5200
+ const knownProviders = ["anthropic", "openai", "gemini", "azure", "openai-compatible", "ollama", "github-models"];
4942
5201
  const hasProviderPrefix = override.includes(":") && knownProviders.some((p) => override.startsWith(p + ":"));
4943
5202
  if (hasProviderPrefix) {
4944
5203
  const provider = override.split(":")[0];
@@ -5093,7 +5352,7 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
5093
5352
  * models without a package upgrade. Mirrors discoverOllamaModels.
5094
5353
  */
5095
5354
  async discoverProviderModels() {
5096
- const cloud = ["anthropic", "openai", "gemini", "azure", "openai-compatible"];
5355
+ const cloud = ["anthropic", "openai", "gemini", "azure", "openai-compatible", "github-models"];
5097
5356
  const tasks = cloud.map(async (type) => {
5098
5357
  if (!this.selector.isProviderAvailable(type)) return;
5099
5358
  const seed = this.getAnyModelForProvider(type);
@@ -5148,6 +5407,9 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
5148
5407
  this.ensureProvider(options.model, this.config.providers);
5149
5408
  }
5150
5409
  let model = requireVision ? this.selector.selectVisionModel() : options.model ?? this.tierModels.get(tier) ?? this.selector.selectForTier(tier) ?? void 0;
5410
+ if (requireVision && model) {
5411
+ this.ensureProvider(model, this.config.providers);
5412
+ }
5151
5413
  if (options.forceLocal && model && !this.isPrivateModel(model)) {
5152
5414
  const localModel = this.selector.getAllAvailableModels().find((m) => this.isPrivateModel(m));
5153
5415
  if (!localModel) {
@@ -5161,7 +5423,8 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
5161
5423
  if (!model) throw new Error(`No model available for tier ${tier}`);
5162
5424
  const provider = this.getProvider(model);
5163
5425
  if (!provider) throw new Error(`No provider for model ${model.id}`);
5164
- const estimatedTokens = (options.maxTokens ?? model.maxOutputTokens ?? 1024) + 512;
5426
+ const requestedTokens = options.maxTokens ?? model.maxOutputTokens ?? 1024;
5427
+ const estimatedTokens = Math.min(requestedTokens, model.maxOutputTokens ?? requestedTokens) + 512;
5165
5428
  if (this.tpmLimiter) {
5166
5429
  await this.tpmLimiter.acquire(model.provider, estimatedTokens);
5167
5430
  }
@@ -5252,7 +5515,8 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
5252
5515
  });
5253
5516
  releaseLocalSlot?.();
5254
5517
  releaseLocalSlot = void 0;
5255
- return this.generate(tier, options, onChunk, requireVision);
5518
+ const retryOpts = options.model && options.model.id === model.id ? { ...options, model: void 0 } : options;
5519
+ return this.generate(tier, retryOpts, onChunk, requireVision);
5256
5520
  }
5257
5521
  }
5258
5522
  if (isModelNotFoundError(errMsg)) {
@@ -5603,6 +5867,26 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
5603
5867
  return false;
5604
5868
  }
5605
5869
  }
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
+ }
5606
5890
  ensureProvider(model, configs) {
5607
5891
  const key = `${model.provider}:${model.id}`;
5608
5892
  if (this.providers.has(key)) return;
@@ -5627,6 +5911,8 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
5627
5911
  return new OllamaProvider(cfg, model);
5628
5912
  case "openai-compatible":
5629
5913
  return new OpenAICompatibleProvider(cfg, model);
5914
+ case "github-models":
5915
+ return new GitHubModelsProvider(cfg, model);
5630
5916
  default:
5631
5917
  throw new Error(`Unsupported provider type: ${String(cfg.type)}`);
5632
5918
  }
@@ -5634,7 +5920,7 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
5634
5920
  getAnyModelForProvider(type) {
5635
5921
  const fromCatalog = Object.values(MODELS).find((m) => m.provider === type);
5636
5922
  if (fromCatalog) return fromCatalog;
5637
- if (type === "openai-compatible" || type === "azure") {
5923
+ if (type === "openai-compatible" || type === "azure" || type === "github-models") {
5638
5924
  return {
5639
5925
  id: type,
5640
5926
  name: type,
@@ -9318,9 +9604,17 @@ var CAPABILITIES = [
9318
9604
  // Long-running: submit returns an operation name, the video lands minutes
9319
9605
  // later, and the bytes come from a short-lived signed URL. generate.ts owns
9320
9606
  // that polling loop; the registry only records the shape via `api`.
9607
+ //
9608
+ // `veo-3.1-generate-001` (no "-preview") is VERTEX AI's id for this model,
9609
+ // not the Gemini Developer API's — this file calls
9610
+ // generativelanguage.googleapis.com (see generate.ts), which 404s on it:
9611
+ // "models/veo-3.1-generate-001 is not found for API version v1beta". On the
9612
+ // Gemini API specifically, Veo 3.1 is still preview-only, so the correct id
9613
+ // there is `veo-3.1-generate-preview`.
9614
+ // https://ai.google.dev/gemini-api/docs/veo
9321
9615
  modality: "video",
9322
9616
  provider: "gemini",
9323
- modelId: "veo-3.1-generate-001",
9617
+ modelId: "veo-3.1-generate-preview",
9324
9618
  api: "gemini-predict-lro"
9325
9619
  }
9326
9620
  ];
@@ -12435,7 +12729,7 @@ var PermissionEscalator = class extends EventEmitter__default.default {
12435
12729
  }
12436
12730
  };
12437
12731
  var ProviderConfigSchema = zod.z.object({
12438
- type: zod.z.enum(["anthropic", "openai", "gemini", "azure", "openai-compatible", "ollama"]),
12732
+ type: zod.z.enum(["anthropic", "openai", "gemini", "azure", "openai-compatible", "ollama", "github-models"]),
12439
12733
  label: zod.z.string().optional(),
12440
12734
  apiKey: zod.z.string().optional(),
12441
12735
  baseUrl: zod.z.string().url().optional(),
@@ -12615,6 +12909,18 @@ var CascadeConfigSchema = zod.z.object({
12615
12909
  budget: BudgetConfigSchema.default({}),
12616
12910
  theme: zod.z.string().default("cascade"),
12617
12911
  workspace: WorkspaceConfigSchema.default({}),
12912
+ /**
12913
+ * Per-provider tokens-per-minute overrides for the router's TpmLimiter (see
12914
+ * DEFAULT_PROVIDER_TPM in core/router/tpm-limiter.ts). Documented as a raw
12915
+ * config field for a while before this — since CascadeConfigSchema strips
12916
+ * unknown keys, a config file setting it was silently discarded and the
12917
+ * override never reached TpmLimiter's constructor. Optional/permissive on
12918
+ * provider name (not a ProviderType enum) so a config still validates
12919
+ * against a slightly newer/older provider list than this schema knows.
12920
+ */
12921
+ rateLimits: zod.z.object({
12922
+ providerTpm: zod.z.record(zod.z.string(), zod.z.number().positive()).optional()
12923
+ }).optional(),
12618
12924
  /**
12619
12925
  * Cascade Auto: when true, the TaskAnalyzer selects the optimal model for each
12620
12926
  * tier based on task type and complexity, overriding the static priority lists.
@@ -17649,7 +17955,14 @@ var ConfigManager = class {
17649
17955
  anthropic: "ANTHROPIC_API_KEY",
17650
17956
  openai: "OPENAI_API_KEY",
17651
17957
  gemini: "GOOGLE_API_KEY",
17652
- azure: "AZURE_OPENAI_KEY"
17958
+ azure: "AZURE_OPENAI_KEY",
17959
+ // Deliberately NOT `GITHUB_TOKEN`: that one is injected automatically by
17960
+ // GitHub Actions and is commonly exported in developer shells for `gh`
17961
+ // and git auth, and it will essentially never carry the fine-grained
17962
+ // `models: read` scope this provider needs. Adopting it would wire up a
17963
+ // provider that 403s on its first real call — a confusing failure for
17964
+ // something that is meant to be explicit opt-in BYOK.
17965
+ "github-models": "GITHUB_MODELS_TOKEN"
17653
17966
  };
17654
17967
  const envKey = envMap[provider];
17655
17968
  if (envKey && process.env[envKey]) return process.env[envKey];
@@ -17678,7 +17991,10 @@ var ConfigManager = class {
17678
17991
  { env: "ANTHROPIC_API_KEY", type: "anthropic" },
17679
17992
  { env: "OPENAI_API_KEY", type: "openai" },
17680
17993
  { env: "GOOGLE_API_KEY", type: "gemini" },
17681
- { env: "AZURE_OPENAI_KEY", type: "azure" }
17994
+ { env: "AZURE_OPENAI_KEY", type: "azure" },
17995
+ // See getApiKey() above for why this is GITHUB_MODELS_TOKEN and never
17996
+ // the ambient GITHUB_TOKEN.
17997
+ { env: "GITHUB_MODELS_TOKEN", type: "github-models" }
17682
17998
  ];
17683
17999
  for (const { env, type } of envProviders) {
17684
18000
  const key = process.env[env];
@@ -20381,6 +20697,9 @@ exports.DEFAULT_RETENTION_DAYS = DEFAULT_RETENTION_DAYS;
20381
20697
  exports.DEFAULT_THEME = DEFAULT_THEME;
20382
20698
  exports.DashboardServer = DashboardServer;
20383
20699
  exports.FileMcpOAuthStore = FileMcpOAuthStore;
20700
+ exports.GITHUB_MODELS_API_VERSION = GITHUB_MODELS_API_VERSION;
20701
+ exports.GITHUB_MODELS_CATALOG_URL = GITHUB_MODELS_CATALOG_URL;
20702
+ exports.GITHUB_MODELS_INFERENCE_URL = GITHUB_MODELS_INFERENCE_URL;
20384
20703
  exports.GLOBAL_CONFIG_DIR = GLOBAL_CONFIG_DIR;
20385
20704
  exports.GLOBAL_CREDENTIALS_FILE = GLOBAL_CREDENTIALS_FILE;
20386
20705
  exports.GLOBAL_DB_FILE = GLOBAL_DB_FILE;