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.js CHANGED
@@ -1624,7 +1624,7 @@ var pricing_data_default = {
1624
1624
  ]
1625
1625
  },
1626
1626
  {
1627
- model: "veo-3.1-generate-001",
1627
+ model: "veo-3.1-generate-preview",
1628
1628
  provider: "gemini",
1629
1629
  modality: "video",
1630
1630
  currency: "USD",
@@ -1862,7 +1862,7 @@ function reconcilePrice(model, live) {
1862
1862
  }
1863
1863
 
1864
1864
  // src/constants.ts
1865
- var CASCADE_VERSION = "0.63.0";
1865
+ var CASCADE_VERSION = "0.65.0";
1866
1866
  var CASCADE_CONFIG_DIR = ".cascade";
1867
1867
  var CASCADE_MD_FILE = "CASCADE.md";
1868
1868
  var CASCADE_IGNORE_FILE = ".cascadeignore";
@@ -2269,6 +2269,9 @@ var DEFAULT_THEME = "cascade";
2269
2269
  var OLLAMA_BASE_URL = "http://localhost:11434";
2270
2270
  var LM_STUDIO_BASE_URL = "http://localhost:1234";
2271
2271
  var AZURE_BASE_URL_TEMPLATE = "https://{resource}.openai.azure.com";
2272
+ var GITHUB_MODELS_INFERENCE_URL = "https://models.github.ai/inference";
2273
+ var GITHUB_MODELS_CATALOG_URL = "https://models.github.ai/catalog/models";
2274
+ var GITHUB_MODELS_API_VERSION = "2026-03-10";
2272
2275
  var TOOL_NAMES = {
2273
2276
  SHELL: "shell",
2274
2277
  FILE_READ: "file_read",
@@ -2303,7 +2306,8 @@ var PROVIDER_DISPLAY_NAMES = {
2303
2306
  gemini: "Google Gemini",
2304
2307
  azure: "Azure OpenAI",
2305
2308
  "openai-compatible": "OpenAI-Compatible",
2306
- ollama: "Ollama (Local)"
2309
+ ollama: "Ollama (Local)",
2310
+ "github-models": "GitHub Models"
2307
2311
  };
2308
2312
 
2309
2313
  // src/utils/cost.ts
@@ -3280,7 +3284,7 @@ function preferIpv4Host(url) {
3280
3284
  return url;
3281
3285
  }
3282
3286
  var MAX_REDIRECTS = 5;
3283
- async function nodeHttpFetch(input, init = {}, redirectCount = 0) {
3287
+ async function nodeHttpFetch(input, init = {}, redirectCount = 0, opts = {}) {
3284
3288
  const urlStr = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
3285
3289
  const u = new URL(preferIpv4Host(urlStr) ?? urlStr);
3286
3290
  const lib = u.protocol === "https:" ? https : http;
@@ -3311,9 +3315,13 @@ async function nodeHttpFetch(input, init = {}, redirectCount = 0) {
3311
3315
  if (status >= 300 && status < 400 && location && redirectCount < MAX_REDIRECTS) {
3312
3316
  res.resume();
3313
3317
  const nextUrl = new URL(location, u).href;
3318
+ if (opts.allowedRedirectOrigin && new URL(nextUrl).origin !== opts.allowedRedirectOrigin) {
3319
+ reject(new Error(`Refusing cross-origin redirect to ${new URL(nextUrl).origin}`));
3320
+ return;
3321
+ }
3314
3322
  const downgrade = status === 303 || (status === 301 || status === 302) && method !== "GET" && method !== "HEAD";
3315
3323
  const nextInit = downgrade ? { ...init, method: "GET", body: void 0 } : init;
3316
- resolve2(nodeHttpFetch(nextUrl, nextInit, redirectCount + 1));
3324
+ resolve2(nodeHttpFetch(nextUrl, nextInit, redirectCount + 1, opts));
3317
3325
  return;
3318
3326
  }
3319
3327
  const encoding = (res.headers["content-encoding"] ?? "").toLowerCase();
@@ -3342,6 +3350,206 @@ async function nodeHttpFetch(input, init = {}, redirectCount = 0) {
3342
3350
  });
3343
3351
  }
3344
3352
 
3353
+ // src/providers/github-models.ts
3354
+ var GITHUB_MODELS_MAX_OUTPUT_TOKENS = 4e3;
3355
+ var GITHUB_MODELS_MAX_INPUT_TOKENS = 8e3;
3356
+ var DEFAULT_CONTEXT_WINDOW = 128e3;
3357
+ var GITHUB_MODELS_MAX_CATALOG_PAGES = 20;
3358
+ var GITHUB_MODELS_CATALOG_ORIGIN = new URL(GITHUB_MODELS_CATALOG_URL).origin;
3359
+ function stripModelOwnerPrefix(id) {
3360
+ const slash = (id || "").indexOf("/");
3361
+ return slash === -1 ? id : id.slice(slash + 1);
3362
+ }
3363
+ function pickString(o, keys) {
3364
+ for (const k of keys) {
3365
+ const v = o[k];
3366
+ if (typeof v === "string" && v.length > 0) return v;
3367
+ }
3368
+ return void 0;
3369
+ }
3370
+ function pickNumber(o, keys) {
3371
+ for (const k of keys) {
3372
+ const v = k.includes(".") ? o[k.split(".")[0]]?.[k.split(".")[1]] : o[k];
3373
+ if (typeof v === "number" && Number.isFinite(v) && v > 0) return v;
3374
+ }
3375
+ return void 0;
3376
+ }
3377
+ function looksVisionCapable(o) {
3378
+ for (const k of ["supported_input_modalities", "input_modalities", "modalities", "capabilities"]) {
3379
+ const v = o[k];
3380
+ if (Array.isArray(v) && v.some((x) => typeof x === "string" && /image|vision/i.test(x))) return true;
3381
+ }
3382
+ return false;
3383
+ }
3384
+ function looksToolCapable(o) {
3385
+ const params = o["supported_parameters"];
3386
+ if (Array.isArray(params) && params.some((x) => typeof x === "string" && /^tools?$|function.?call/i.test(x))) {
3387
+ return true;
3388
+ }
3389
+ for (const k of ["capabilities", "supported_output_modalities"]) {
3390
+ const v = o[k];
3391
+ if (Array.isArray(v) && v.some((x) => typeof x === "string" && /tool|function.?call/i.test(x))) return true;
3392
+ }
3393
+ const caps = o["capabilities"];
3394
+ if (caps && typeof caps === "object" && !Array.isArray(caps)) {
3395
+ const c = caps;
3396
+ if (c["function_calling"] === true || c["tool_calling"] === true || c["tools"] === true) return true;
3397
+ }
3398
+ return false;
3399
+ }
3400
+ function parseNextLink(linkHeader) {
3401
+ if (!linkHeader) return null;
3402
+ for (const part of linkHeader.split(",")) {
3403
+ const match = part.match(/<([^>]+)>\s*;\s*rel="next"/);
3404
+ if (match) return match[1];
3405
+ }
3406
+ return null;
3407
+ }
3408
+ var GitHubModelsProvider = class extends OpenAIProvider {
3409
+ constructor(config, model) {
3410
+ super({ ...config, apiKey: config.apiKey ?? "" }, model);
3411
+ this.useMaxCompletionTokens = isReasoningModel(stripModelOwnerPrefix(model.id));
3412
+ this.client = new OpenAI({
3413
+ apiKey: config.apiKey ?? "",
3414
+ baseURL: GITHUB_MODELS_INFERENCE_URL,
3415
+ defaultHeaders: { "X-GitHub-Api-Version": GITHUB_MODELS_API_VERSION, "User-Agent": "Cascade-AI" }
3416
+ });
3417
+ }
3418
+ /**
3419
+ * `OpenAIProvider.generateStream()` builds its request budget as
3420
+ * `options.maxTokens ?? this.model.maxOutputTokens` — an explicit per-call
3421
+ * `maxTokens` (T1Administrator's final compilation asks for 8,000, well
3422
+ * above GitHub's real ~4K hard cap) wins outright and is sent unclamped.
3423
+ * GitHub then rejects the request instead of answering, so a run routed to
3424
+ * this provider for that step fails outright rather than getting a
3425
+ * (perfectly serviceable, just shorter) completion. Clamp here, once, right
3426
+ * before delegating to the inherited implementation, rather than teaching
3427
+ * every caller GitHub's specific cap.
3428
+ */
3429
+ async generateStream(options, onChunk) {
3430
+ const clamped = options.maxTokens !== void 0 && options.maxTokens > GITHUB_MODELS_MAX_OUTPUT_TOKENS ? { ...options, maxTokens: GITHUB_MODELS_MAX_OUTPUT_TOKENS } : options;
3431
+ return super.generateStream(clamped, onChunk);
3432
+ }
3433
+ /**
3434
+ * The catalog is a GitHub REST resource, not an OpenAI `/models` list, and
3435
+ * wants GitHub's own header trio — the plain `Accept: application/json` the
3436
+ * openai-compatible provider sends is not the same request.
3437
+ */
3438
+ catalogHeaders() {
3439
+ return {
3440
+ Accept: "application/vnd.github+json",
3441
+ Authorization: `Bearer ${this.config.apiKey ?? ""}`,
3442
+ "X-GitHub-Api-Version": GITHUB_MODELS_API_VERSION,
3443
+ // GitHub's REST API rejects any request with no User-Agent — a plain 403
3444
+ // regardless of how valid the PAT is. nodeHttpFetch is a thin wrapper
3445
+ // over node:http/https, neither of which sets one the way a browser's
3446
+ // fetch or a full HTTP client library would, so this has to be explicit.
3447
+ "User-Agent": "Cascade-AI"
3448
+ };
3449
+ }
3450
+ async listModels() {
3451
+ const raw = [];
3452
+ let url = GITHUB_MODELS_CATALOG_URL;
3453
+ let pages = 0;
3454
+ const visited = /* @__PURE__ */ new Set();
3455
+ while (url && pages < GITHUB_MODELS_MAX_CATALOG_PAGES) {
3456
+ if (visited.has(url)) break;
3457
+ visited.add(url);
3458
+ const res = await nodeHttpFetch(url, { headers: this.catalogHeaders() }, 0, {
3459
+ allowedRedirectOrigin: GITHUB_MODELS_CATALOG_ORIGIN
3460
+ });
3461
+ if (!res.ok) throw new Error(`GitHub Models catalog ${url} returned HTTP ${res.status}`);
3462
+ const body = await res.json();
3463
+ const pageItems = Array.isArray(body) ? body : Array.isArray(body?.models) ? body.models : Array.isArray(body?.data) ? body.data : [];
3464
+ raw.push(...pageItems);
3465
+ const next = parseNextLink(res.headers.get("link"));
3466
+ if (next) {
3467
+ const resolved = new URL(next, url);
3468
+ if (resolved.origin !== GITHUB_MODELS_CATALOG_ORIGIN) {
3469
+ throw new Error(`Refusing cross-origin GitHub Models pagination URL: ${resolved.origin}`);
3470
+ }
3471
+ url = resolved.href;
3472
+ } else {
3473
+ url = null;
3474
+ }
3475
+ pages++;
3476
+ }
3477
+ const entries = raw.map((m) => {
3478
+ if (typeof m === "string") {
3479
+ return { id: m, name: m, contextWindow: DEFAULT_CONTEXT_WINDOW, vision: false, tools: false };
3480
+ }
3481
+ if (!m || typeof m !== "object") return void 0;
3482
+ const o = m;
3483
+ const id = pickString(o, ["id", "name", "model"]);
3484
+ if (!id) return void 0;
3485
+ return {
3486
+ id,
3487
+ name: pickString(o, ["friendly_name", "display_name", "name"]) ?? id,
3488
+ contextWindow: pickNumber(o, [
3489
+ "context_window",
3490
+ "context_length",
3491
+ "max_input_tokens",
3492
+ "limits.max_input_tokens",
3493
+ "limits.max_context_tokens"
3494
+ ]) ?? DEFAULT_CONTEXT_WINDOW,
3495
+ vision: looksVisionCapable(o),
3496
+ tools: looksToolCapable(o)
3497
+ };
3498
+ }).filter((e) => e !== void 0);
3499
+ if (entries.length === 0) return [];
3500
+ const chat = entries.filter((e) => isChatModel(e.id));
3501
+ return (chat.length ? chat : entries).map((e) => ({
3502
+ id: e.id,
3503
+ // full `owner/model` — what the API is addressed by
3504
+ name: e.name,
3505
+ provider: "github-models",
3506
+ // GitHub's real per-request input cap, not the base model's larger one
3507
+ // (see GITHUB_MODELS_MAX_INPUT_TOKENS above) — the same reasoning
3508
+ // already applied to maxOutputTokens below, for the input side.
3509
+ contextWindow: Math.min(e.contextWindow, GITHUB_MODELS_MAX_INPUT_TOKENS),
3510
+ isVisionCapable: e.vision,
3511
+ // A REAL $0, not a missing number — usage is bundled into the quota of
3512
+ // the plan the PAT belongs to and is never billed per token, so
3513
+ // `pricingUnknown: false` is the honest value (see the flag's contract in
3514
+ // core/router/pricing.ts: 0 alone is ambiguous). Set here rather than in
3515
+ // pricing-data.json because resolvePricing() matches exactly on
3516
+ // provider|model|region with no wildcard: every catalog model would need
3517
+ // a hand-curated row, and the next one GitHub adds would read as "cost
3518
+ // unknown" — an inconsistent picture for a billing reality that is
3519
+ // identical across the entire catalog.
3520
+ //
3521
+ // `isLocal` is the OTHER way to reach a real $0, and it is the wrong one:
3522
+ // it means "runs on this machine" and carries consequences that are
3523
+ // simply false for a hosted API — the shared concurrency-1
3524
+ // LocalRequestQueue (serialising these calls behind actual Ollama GPU
3525
+ // inference), the 300s local rather than 120s cloud inference timeout,
3526
+ // and a literal "[local]" tag in `cascade models`.
3527
+ inputCostPer1kTokens: 0,
3528
+ outputCostPer1kTokens: 0,
3529
+ pricingUnknown: false,
3530
+ maxOutputTokens: GITHUB_MODELS_MAX_OUTPUT_TOKENS,
3531
+ supportsStreaming: true,
3532
+ // Not every catalog model supports function/tool calling — a hardcoded
3533
+ // `true` sent `tools` to models that reject the parameter outright and
3534
+ // never engaged t3-worker.ts's existing text-tool fallback (only an
3535
+ // explicit `false` does). See looksToolCapable's own comment for why
3536
+ // "not positively advertised" defaults to false rather than true here.
3537
+ supportsToolUse: e.tools,
3538
+ isLocal: false
3539
+ }));
3540
+ }
3541
+ async isAvailable() {
3542
+ try {
3543
+ const res = await nodeHttpFetch(GITHUB_MODELS_CATALOG_URL, { headers: this.catalogHeaders() }, 0, {
3544
+ allowedRedirectOrigin: GITHUB_MODELS_CATALOG_ORIGIN
3545
+ });
3546
+ return res.ok;
3547
+ } catch {
3548
+ return false;
3549
+ }
3550
+ }
3551
+ };
3552
+
3345
3553
  // src/providers/ollama.ts
3346
3554
  var TOOL_CAPABLE_FAMILIES = [
3347
3555
  "llama3.1",
@@ -3758,16 +3966,23 @@ var ModelSelector = class {
3758
3966
  return model;
3759
3967
  }
3760
3968
  }
3969
+ for (const model of this.availableModels.values()) {
3970
+ if (this.availableProviders.has(model.provider) && model.isVisionCapable) return model;
3971
+ }
3761
3972
  return null;
3762
3973
  }
3763
3974
  getNextFallback(currentModelId, tier) {
3764
3975
  const priority = this.getPriorityList(tier);
3765
3976
  const currentIdx = priority.indexOf(currentModelId);
3766
- if (currentIdx === -1) return null;
3767
- for (let i = currentIdx + 1; i < priority.length; i++) {
3768
- const key = priority[i];
3769
- const model = this.availableModels.get(key);
3770
- if (model && this.isUsable(model)) return model;
3977
+ if (currentIdx !== -1) {
3978
+ for (let i = currentIdx + 1; i < priority.length; i++) {
3979
+ const key = priority[i];
3980
+ const model = this.availableModels.get(key);
3981
+ if (model && this.isUsable(model)) return model;
3982
+ }
3983
+ }
3984
+ for (const [id, model] of this.availableModels) {
3985
+ if (id !== currentModelId && this.isUsable(model)) return model;
3771
3986
  }
3772
3987
  return null;
3773
3988
  }
@@ -3843,7 +4058,7 @@ var ModelSelector = class {
3843
4058
  if (overrideModelId.includes(":")) {
3844
4059
  const parts = overrideModelId.split(":");
3845
4060
  const prefix = parts[0].toLowerCase();
3846
- const validProviders = ["anthropic", "openai", "gemini", "azure", "openai-compatible", "ollama"];
4061
+ const validProviders = ["anthropic", "openai", "gemini", "azure", "openai-compatible", "ollama", "github-models"];
3847
4062
  if (validProviders.includes(prefix)) {
3848
4063
  providerStr = prefix;
3849
4064
  actualId = parts.slice(1).join(":");
@@ -3863,6 +4078,31 @@ var ModelSelector = class {
3863
4078
  else if (this.availableProviders.has("openai-compatible")) providerStr = "openai-compatible";
3864
4079
  else if (this.availableProviders.size === 1) providerStr = Array.from(this.availableProviders)[0];
3865
4080
  }
4081
+ if (providerStr === "github-models" && this.availableProviders.has("github-models")) {
4082
+ const dynamicModel = {
4083
+ id: actualId,
4084
+ name: actualId,
4085
+ provider: "github-models",
4086
+ contextWindow: GITHUB_MODELS_MAX_INPUT_TOKENS,
4087
+ isVisionCapable: false,
4088
+ inputCostPer1kTokens: 0,
4089
+ outputCostPer1kTokens: 0,
4090
+ pricingUnknown: false,
4091
+ maxOutputTokens: GITHUB_MODELS_MAX_OUTPUT_TOKENS,
4092
+ supportsStreaming: true,
4093
+ // Unconfirmed (no live catalog entry to read a real capability from)
4094
+ // must default to false, not left undefined: t3-worker.ts's text-tool
4095
+ // fallback only engages on a strict `=== false`, so an undefined
4096
+ // value here sends native `tools` to an unverified multi-vendor
4097
+ // model and any that don't support function calling reject the
4098
+ // request outright. Same default-to-false-when-unconfirmed policy
4099
+ // listModels() already applies for a real catalog entry.
4100
+ supportsToolUse: false,
4101
+ isLocal: false
4102
+ };
4103
+ this.addDynamicModel(dynamicModel);
4104
+ return dynamicModel;
4105
+ }
3866
4106
  if (providerStr && this.availableProviders.has(providerStr)) {
3867
4107
  const dynamicModel = withResolvedPricing({
3868
4108
  id: actualId,
@@ -3955,7 +4195,20 @@ var DEFAULT_PROVIDER_TPM = {
3955
4195
  gemini: 6e4,
3956
4196
  azure: 3e4,
3957
4197
  "openai-compatible": 3e4,
3958
- ollama: Number.POSITIVE_INFINITY
4198
+ ollama: Number.POSITIVE_INFINITY,
4199
+ // An order of magnitude below the others, on purpose. GitHub Models is
4200
+ // rate-limited by REQUESTS (Free ≈10 RPM, Pro ≈20, plus low daily caps), and
4201
+ // this bucket meters TOKENS — there is no request-count limiter in this
4202
+ // codebase, and building one is not in scope. The approximation works because
4203
+ // the router reserves `model.maxOutputTokens + 512` per call, and the
4204
+ // provider reports GitHub's real ~4K per-request cap rather than the
4205
+ // underlying model's larger one — so every call withdraws a near-constant
4206
+ // ~4.5K regardless of how short the reply actually turns out to be. At 8K/min
4207
+ // that lands around 1–2 calls per minute of headroom: comfortably inside even
4208
+ // the Free tier, with room for the daily cap to matter more than this does.
4209
+ // Users on a higher tier can raise it with no code change through the generic
4210
+ // `config.rateLimits.providerTpm['github-models']` override.
4211
+ "github-models": 8e3
3959
4212
  };
3960
4213
  var TpmLimiter = class {
3961
4214
  buckets = /* @__PURE__ */ new Map();
@@ -4311,6 +4564,7 @@ var ModelProfiler = class {
4311
4564
  (m) => !alreadyProfiled.has(m.id) && !SKIP_PATTERN.test(m.id) && !SKIP_PATTERN.test(m.name)
4312
4565
  );
4313
4566
  if (toProfile.length === 0) return;
4567
+ const probesHitGithubModels = this.router?.getModelForTier("T3")?.provider === "github-models";
4314
4568
  const openRouterModels = await fetchOpenRouterModels();
4315
4569
  const orByNormalizedId = /* @__PURE__ */ new Map();
4316
4570
  for (const m of openRouterModels) {
@@ -4325,7 +4579,7 @@ var ModelProfiler = class {
4325
4579
  if (orMatch?.description) {
4326
4580
  specializations = extractSpecializations(orMatch.description);
4327
4581
  }
4328
- if (specializations.length === 0 && this.router) {
4582
+ if (specializations.length === 0 && this.router && !probesHitGithubModels) {
4329
4583
  specializations = await queryModelDirectly(this.router);
4330
4584
  }
4331
4585
  this.store.saveModelProfile(model.id, model.provider, specializations);
@@ -4545,6 +4799,7 @@ var LiveDataProvider = class {
4545
4799
  */
4546
4800
  applyLivePricing(models) {
4547
4801
  return models.map((m) => {
4802
+ if (m.provider === "github-models") return m;
4548
4803
  const p = this.getLivePrice(m.baseModelId ?? m.id);
4549
4804
  if (!p) return m;
4550
4805
  const reconciled = reconcilePrice(m, p);
@@ -4583,6 +4838,7 @@ var LiveDataProvider = class {
4583
4838
  */
4584
4839
  applyLiveCapabilities(models) {
4585
4840
  return models.map((m) => {
4841
+ if (m.provider === "github-models") return m;
4586
4842
  const c = this.getCapability(m.baseModelId ?? m.id);
4587
4843
  if (!c) return m;
4588
4844
  const next = { ...m };
@@ -4886,13 +5142,16 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
4886
5142
  for (const model of azureDeployments) this.selector.addDynamicModel(model);
4887
5143
  availableProviders.add("azure");
4888
5144
  }
5145
+ if (availableProviders.has("github-models")) {
5146
+ await this.discoverGitHubModelsCatalog(config);
5147
+ }
4889
5148
  await this.validateCloudProviderModels(config);
4890
5149
  for (const tier of ["T1", "T2", "T3"]) {
4891
5150
  const override = tier === "T1" ? config.models.t1 : tier === "T2" ? config.models.t2 : config.models.t3;
4892
5151
  if (!override || override === "auto") continue;
4893
5152
  const model = this.selector.selectForTier(tier, override);
4894
5153
  if (!model) {
4895
- const knownProviders = ["anthropic", "openai", "gemini", "azure", "openai-compatible", "ollama"];
5154
+ const knownProviders = ["anthropic", "openai", "gemini", "azure", "openai-compatible", "ollama", "github-models"];
4896
5155
  const hasProviderPrefix = override.includes(":") && knownProviders.some((p) => override.startsWith(p + ":"));
4897
5156
  if (hasProviderPrefix) {
4898
5157
  const provider = override.split(":")[0];
@@ -5047,7 +5306,7 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
5047
5306
  * models without a package upgrade. Mirrors discoverOllamaModels.
5048
5307
  */
5049
5308
  async discoverProviderModels() {
5050
- const cloud = ["anthropic", "openai", "gemini", "azure", "openai-compatible"];
5309
+ const cloud = ["anthropic", "openai", "gemini", "azure", "openai-compatible", "github-models"];
5051
5310
  const tasks = cloud.map(async (type) => {
5052
5311
  if (!this.selector.isProviderAvailable(type)) return;
5053
5312
  const seed = this.getAnyModelForProvider(type);
@@ -5102,6 +5361,9 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
5102
5361
  this.ensureProvider(options.model, this.config.providers);
5103
5362
  }
5104
5363
  let model = requireVision ? this.selector.selectVisionModel() : options.model ?? this.tierModels.get(tier) ?? this.selector.selectForTier(tier) ?? void 0;
5364
+ if (requireVision && model) {
5365
+ this.ensureProvider(model, this.config.providers);
5366
+ }
5105
5367
  if (options.forceLocal && model && !this.isPrivateModel(model)) {
5106
5368
  const localModel = this.selector.getAllAvailableModels().find((m) => this.isPrivateModel(m));
5107
5369
  if (!localModel) {
@@ -5115,7 +5377,8 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
5115
5377
  if (!model) throw new Error(`No model available for tier ${tier}`);
5116
5378
  const provider = this.getProvider(model);
5117
5379
  if (!provider) throw new Error(`No provider for model ${model.id}`);
5118
- const estimatedTokens = (options.maxTokens ?? model.maxOutputTokens ?? 1024) + 512;
5380
+ const requestedTokens = options.maxTokens ?? model.maxOutputTokens ?? 1024;
5381
+ const estimatedTokens = Math.min(requestedTokens, model.maxOutputTokens ?? requestedTokens) + 512;
5119
5382
  if (this.tpmLimiter) {
5120
5383
  await this.tpmLimiter.acquire(model.provider, estimatedTokens);
5121
5384
  }
@@ -5206,7 +5469,8 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
5206
5469
  });
5207
5470
  releaseLocalSlot?.();
5208
5471
  releaseLocalSlot = void 0;
5209
- return this.generate(tier, options, onChunk, requireVision);
5472
+ const retryOpts = options.model && options.model.id === model.id ? { ...options, model: void 0 } : options;
5473
+ return this.generate(tier, retryOpts, onChunk, requireVision);
5210
5474
  }
5211
5475
  }
5212
5476
  if (isModelNotFoundError(errMsg)) {
@@ -5557,6 +5821,26 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
5557
5821
  return false;
5558
5822
  }
5559
5823
  }
5824
+ /**
5825
+ * Registers GitHub Models' real catalog ids via addDynamicModel so init()'s
5826
+ * tier-fill loop can pick one. Registration alone does not create a bound
5827
+ * BaseProvider instance (see ensureProvider) — the tier-fill loop's own
5828
+ * ensureProvider call does that for whichever model it actually selects,
5829
+ * which is why this runs BEFORE that loop rather than only in the
5830
+ * background discoverProviderModels() refresh.
5831
+ */
5832
+ async discoverGitHubModelsCatalog(config) {
5833
+ const cfg = config.providers.find((p) => p.type === "github-models") ?? { type: "github-models" };
5834
+ const seed = this.getAnyModelForProvider("github-models");
5835
+ if (!seed) return;
5836
+ try {
5837
+ const provider = new GitHubModelsProvider(cfg, seed);
5838
+ const models = await provider.listModels();
5839
+ for (const m of models) this.selector.addDynamicModel(m);
5840
+ } catch (err) {
5841
+ console.warn("[router] GitHub Models catalog discovery failed:", err instanceof Error ? err.message : err);
5842
+ }
5843
+ }
5560
5844
  ensureProvider(model, configs) {
5561
5845
  const key = `${model.provider}:${model.id}`;
5562
5846
  if (this.providers.has(key)) return;
@@ -5581,6 +5865,8 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
5581
5865
  return new OllamaProvider(cfg, model);
5582
5866
  case "openai-compatible":
5583
5867
  return new OpenAICompatibleProvider(cfg, model);
5868
+ case "github-models":
5869
+ return new GitHubModelsProvider(cfg, model);
5584
5870
  default:
5585
5871
  throw new Error(`Unsupported provider type: ${String(cfg.type)}`);
5586
5872
  }
@@ -5588,7 +5874,7 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
5588
5874
  getAnyModelForProvider(type) {
5589
5875
  const fromCatalog = Object.values(MODELS).find((m) => m.provider === type);
5590
5876
  if (fromCatalog) return fromCatalog;
5591
- if (type === "openai-compatible" || type === "azure") {
5877
+ if (type === "openai-compatible" || type === "azure" || type === "github-models") {
5592
5878
  return {
5593
5879
  id: type,
5594
5880
  name: type,
@@ -9272,9 +9558,17 @@ var CAPABILITIES = [
9272
9558
  // Long-running: submit returns an operation name, the video lands minutes
9273
9559
  // later, and the bytes come from a short-lived signed URL. generate.ts owns
9274
9560
  // that polling loop; the registry only records the shape via `api`.
9561
+ //
9562
+ // `veo-3.1-generate-001` (no "-preview") is VERTEX AI's id for this model,
9563
+ // not the Gemini Developer API's — this file calls
9564
+ // generativelanguage.googleapis.com (see generate.ts), which 404s on it:
9565
+ // "models/veo-3.1-generate-001 is not found for API version v1beta". On the
9566
+ // Gemini API specifically, Veo 3.1 is still preview-only, so the correct id
9567
+ // there is `veo-3.1-generate-preview`.
9568
+ // https://ai.google.dev/gemini-api/docs/veo
9275
9569
  modality: "video",
9276
9570
  provider: "gemini",
9277
- modelId: "veo-3.1-generate-001",
9571
+ modelId: "veo-3.1-generate-preview",
9278
9572
  api: "gemini-predict-lro"
9279
9573
  }
9280
9574
  ];
@@ -12389,7 +12683,7 @@ var PermissionEscalator = class extends EventEmitter {
12389
12683
  }
12390
12684
  };
12391
12685
  var ProviderConfigSchema = z.object({
12392
- type: z.enum(["anthropic", "openai", "gemini", "azure", "openai-compatible", "ollama"]),
12686
+ type: z.enum(["anthropic", "openai", "gemini", "azure", "openai-compatible", "ollama", "github-models"]),
12393
12687
  label: z.string().optional(),
12394
12688
  apiKey: z.string().optional(),
12395
12689
  baseUrl: z.string().url().optional(),
@@ -12569,6 +12863,18 @@ var CascadeConfigSchema = z.object({
12569
12863
  budget: BudgetConfigSchema.default({}),
12570
12864
  theme: z.string().default("cascade"),
12571
12865
  workspace: WorkspaceConfigSchema.default({}),
12866
+ /**
12867
+ * Per-provider tokens-per-minute overrides for the router's TpmLimiter (see
12868
+ * DEFAULT_PROVIDER_TPM in core/router/tpm-limiter.ts). Documented as a raw
12869
+ * config field for a while before this — since CascadeConfigSchema strips
12870
+ * unknown keys, a config file setting it was silently discarded and the
12871
+ * override never reached TpmLimiter's constructor. Optional/permissive on
12872
+ * provider name (not a ProviderType enum) so a config still validates
12873
+ * against a slightly newer/older provider list than this schema knows.
12874
+ */
12875
+ rateLimits: z.object({
12876
+ providerTpm: z.record(z.string(), z.number().positive()).optional()
12877
+ }).optional(),
12572
12878
  /**
12573
12879
  * Cascade Auto: when true, the TaskAnalyzer selects the optimal model for each
12574
12880
  * tier based on task type and complexity, overriding the static priority lists.
@@ -17603,7 +17909,14 @@ var ConfigManager = class {
17603
17909
  anthropic: "ANTHROPIC_API_KEY",
17604
17910
  openai: "OPENAI_API_KEY",
17605
17911
  gemini: "GOOGLE_API_KEY",
17606
- azure: "AZURE_OPENAI_KEY"
17912
+ azure: "AZURE_OPENAI_KEY",
17913
+ // Deliberately NOT `GITHUB_TOKEN`: that one is injected automatically by
17914
+ // GitHub Actions and is commonly exported in developer shells for `gh`
17915
+ // and git auth, and it will essentially never carry the fine-grained
17916
+ // `models: read` scope this provider needs. Adopting it would wire up a
17917
+ // provider that 403s on its first real call — a confusing failure for
17918
+ // something that is meant to be explicit opt-in BYOK.
17919
+ "github-models": "GITHUB_MODELS_TOKEN"
17607
17920
  };
17608
17921
  const envKey = envMap[provider];
17609
17922
  if (envKey && process.env[envKey]) return process.env[envKey];
@@ -17632,7 +17945,10 @@ var ConfigManager = class {
17632
17945
  { env: "ANTHROPIC_API_KEY", type: "anthropic" },
17633
17946
  { env: "OPENAI_API_KEY", type: "openai" },
17634
17947
  { env: "GOOGLE_API_KEY", type: "gemini" },
17635
- { env: "AZURE_OPENAI_KEY", type: "azure" }
17948
+ { env: "AZURE_OPENAI_KEY", type: "azure" },
17949
+ // See getApiKey() above for why this is GITHUB_MODELS_TOKEN and never
17950
+ // the ambient GITHUB_TOKEN.
17951
+ { env: "GITHUB_MODELS_TOKEN", type: "github-models" }
17636
17952
  ];
17637
17953
  for (const { env, type } of envProviders) {
17638
17954
  const key = process.env[env];
@@ -20301,6 +20617,6 @@ function applySyncBundle(bundle, config) {
20301
20617
  return next;
20302
20618
  }
20303
20619
 
20304
- export { AZURE_BASE_MODELS, AZURE_BASE_URL_TEMPLATE, AuditLogger, CASCADE_AUDIT_FILE, CASCADE_CONFIG_DIR, CASCADE_CONFIG_FILE, CASCADE_DASHBOARD_SECRET_FILE, CASCADE_DB_FILE, CASCADE_IGNORE_FILE, CASCADE_KEYSTORE_FILE, CASCADE_MD_FILE, CASCADE_VERSION, CHARS_PER_TOKEN, COMPLEXITY_T2_COUNT, Cascade, CascadeCancelledError, CascadeIgnore, CascadeRouter, CascadeToolError, CloudClient, CodeSearchTool, ConfigManager, CurrentPageTool, DEFAULT_API_PORT, DEFAULT_APPROVAL_REQUIRED, DEFAULT_AUTO_SUMMARIZE_AT, DEFAULT_CLOUD_URL, DEFAULT_CONTEXT_LIMIT, DEFAULT_DASHBOARD_PORT, DEFAULT_MAX_SESSION_MESSAGES, DEFAULT_RETENTION_DAYS, DEFAULT_THEME, DashboardServer, FileMcpOAuthStore, GLOBAL_CONFIG_DIR, GLOBAL_CREDENTIALS_FILE, GLOBAL_DB_FILE, GLOBAL_KEYSTORE_FILE, GLOBAL_RUNTIME_DB_FILE, GraphRetriever, GraphSearchTool, HooksRunner, Keystore, LLMReranker, LM_STUDIO_BASE_URL, MAX_PAGE_TEXT_CHARS, MCP_TOOL_PREFIX, MODELS, McpClient, McpOAuthProvider, MemoryStore, OLLAMA_BASE_URL, OpenAICompatibleEmbedder, PROVIDER_DISPLAY_NAMES, Retriever, SqliteVectorStore, T1Administrator, T1_MODEL_PRIORITY, T2Manager, T2_MODEL_PRIORITY, T3Worker, T3_MODEL_PRIORITY, THEME_NAMES, TOOL_NAMES, TaskScheduler, Telemetry, ToolRegistry, VISION_MODEL_PRIORITY, WorkspaceIndex, applySyncBundle, azureModelForDeployment, beginMcpOAuth, buildManifest, buildSessionTranscript, cagCharBudget, chatCompleterFromProviders, chunkCode, chunkText2 as chunkText, completeMcpOAuth, connectMcpWithLoopbackOAuth, createCascade, decryptJSON as decryptSyncBlob, diffManifest, disambiguateMcpServerNames, discoverMcpAuthServer, discoverMcpTools, distillSessionFacts, embedderFromProviders, encryptJSON as encryptSyncBlob, fileOAuthProvider, gatherSyncBundle, hashContent, heuristicCodeChunker, inferAzureBaseModel, isMcpToolName, mcpServerPrefix, mcpToolName, nodeHttpFetch, parseRankOrder, planRetrieval, preferIpv4Host, reciprocalRankFusion, refreshMcpToken, removeMcpServerDenials, runCascade, sessionWorthRemembering, streamCascade, uniqueMcpServerName };
20620
+ export { AZURE_BASE_MODELS, AZURE_BASE_URL_TEMPLATE, AuditLogger, CASCADE_AUDIT_FILE, CASCADE_CONFIG_DIR, CASCADE_CONFIG_FILE, CASCADE_DASHBOARD_SECRET_FILE, CASCADE_DB_FILE, CASCADE_IGNORE_FILE, CASCADE_KEYSTORE_FILE, CASCADE_MD_FILE, CASCADE_VERSION, CHARS_PER_TOKEN, COMPLEXITY_T2_COUNT, Cascade, CascadeCancelledError, CascadeIgnore, CascadeRouter, CascadeToolError, CloudClient, CodeSearchTool, ConfigManager, CurrentPageTool, DEFAULT_API_PORT, DEFAULT_APPROVAL_REQUIRED, DEFAULT_AUTO_SUMMARIZE_AT, DEFAULT_CLOUD_URL, DEFAULT_CONTEXT_LIMIT, DEFAULT_DASHBOARD_PORT, DEFAULT_MAX_SESSION_MESSAGES, DEFAULT_RETENTION_DAYS, DEFAULT_THEME, DashboardServer, FileMcpOAuthStore, GITHUB_MODELS_API_VERSION, GITHUB_MODELS_CATALOG_URL, GITHUB_MODELS_INFERENCE_URL, GLOBAL_CONFIG_DIR, GLOBAL_CREDENTIALS_FILE, GLOBAL_DB_FILE, GLOBAL_KEYSTORE_FILE, GLOBAL_RUNTIME_DB_FILE, GraphRetriever, GraphSearchTool, HooksRunner, Keystore, LLMReranker, LM_STUDIO_BASE_URL, MAX_PAGE_TEXT_CHARS, MCP_TOOL_PREFIX, MODELS, McpClient, McpOAuthProvider, MemoryStore, OLLAMA_BASE_URL, OpenAICompatibleEmbedder, PROVIDER_DISPLAY_NAMES, Retriever, SqliteVectorStore, T1Administrator, T1_MODEL_PRIORITY, T2Manager, T2_MODEL_PRIORITY, T3Worker, T3_MODEL_PRIORITY, THEME_NAMES, TOOL_NAMES, TaskScheduler, Telemetry, ToolRegistry, VISION_MODEL_PRIORITY, WorkspaceIndex, applySyncBundle, azureModelForDeployment, beginMcpOAuth, buildManifest, buildSessionTranscript, cagCharBudget, chatCompleterFromProviders, chunkCode, chunkText2 as chunkText, completeMcpOAuth, connectMcpWithLoopbackOAuth, createCascade, decryptJSON as decryptSyncBlob, diffManifest, disambiguateMcpServerNames, discoverMcpAuthServer, discoverMcpTools, distillSessionFacts, embedderFromProviders, encryptJSON as encryptSyncBlob, fileOAuthProvider, gatherSyncBundle, hashContent, heuristicCodeChunker, inferAzureBaseModel, isMcpToolName, mcpServerPrefix, mcpToolName, nodeHttpFetch, parseRankOrder, planRetrieval, preferIpv4Host, reciprocalRankFusion, refreshMcpToken, removeMcpServerDenials, runCascade, sessionWorthRemembering, streamCascade, uniqueMcpServerName };
20305
20621
  //# sourceMappingURL=index.js.map
20306
20622
  //# sourceMappingURL=index.js.map