cascade-ai 0.64.0 → 0.66.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/cli.cjs CHANGED
@@ -1806,11 +1806,11 @@ var init_pricing = __esm({
1806
1806
  });
1807
1807
 
1808
1808
  // src/constants.ts
1809
- var CASCADE_VERSION, CASCADE_CONFIG_FILE, CASCADE_DB_FILE, CASCADE_DASHBOARD_SECRET_FILE, GLOBAL_CONFIG_DIR, GLOBAL_DB_FILE, GLOBAL_KEYSTORE_FILE, GLOBAL_CREDENTIALS_FILE, GLOBAL_RUNTIME_DB_FILE, DEFAULT_DASHBOARD_PORT, DEFAULT_CONTEXT_LIMIT, DEFAULT_AUTO_SUMMARIZE_AT, MODELS, T1_MODEL_PRIORITY, T2_MODEL_PRIORITY, T3_MODEL_PRIORITY, VISION_MODEL_PRIORITY, COMPLEXITY_T2_COUNT, THEME_NAMES, DEFAULT_THEME, OLLAMA_BASE_URL, LM_STUDIO_BASE_URL, AZURE_BASE_URL_TEMPLATE, TOOL_NAMES, DEFAULT_APPROVAL_REQUIRED;
1809
+ var CASCADE_VERSION, CASCADE_CONFIG_FILE, CASCADE_DB_FILE, CASCADE_DASHBOARD_SECRET_FILE, GLOBAL_CONFIG_DIR, GLOBAL_DB_FILE, GLOBAL_KEYSTORE_FILE, GLOBAL_CREDENTIALS_FILE, GLOBAL_RUNTIME_DB_FILE, DEFAULT_DASHBOARD_PORT, DEFAULT_CONTEXT_LIMIT, DEFAULT_AUTO_SUMMARIZE_AT, MODELS, T1_MODEL_PRIORITY, T2_MODEL_PRIORITY, T3_MODEL_PRIORITY, VISION_MODEL_PRIORITY, COMPLEXITY_T2_COUNT, THEME_NAMES, DEFAULT_THEME, OLLAMA_BASE_URL, LM_STUDIO_BASE_URL, AZURE_BASE_URL_TEMPLATE, GITHUB_MODELS_INFERENCE_URL, GITHUB_MODELS_CATALOG_URL, GITHUB_MODELS_API_VERSION, TOOL_NAMES, DEFAULT_APPROVAL_REQUIRED;
1810
1810
  var init_constants = __esm({
1811
1811
  "src/constants.ts"() {
1812
1812
  init_pricing();
1813
- CASCADE_VERSION = "0.64.0";
1813
+ CASCADE_VERSION = "0.66.0";
1814
1814
  CASCADE_CONFIG_FILE = ".cascade/config.json";
1815
1815
  CASCADE_DB_FILE = ".cascade/memory.db";
1816
1816
  CASCADE_DASHBOARD_SECRET_FILE = ".cascade/dashboard-secret";
@@ -2209,6 +2209,9 @@ var init_constants = __esm({
2209
2209
  OLLAMA_BASE_URL = "http://localhost:11434";
2210
2210
  LM_STUDIO_BASE_URL = "http://localhost:1234";
2211
2211
  AZURE_BASE_URL_TEMPLATE = "https://{resource}.openai.azure.com";
2212
+ GITHUB_MODELS_INFERENCE_URL = "https://models.github.ai/inference";
2213
+ GITHUB_MODELS_CATALOG_URL = "https://models.github.ai/catalog/models";
2214
+ GITHUB_MODELS_API_VERSION = "2026-03-10";
2212
2215
  TOOL_NAMES = {
2213
2216
  SHELL: "shell",
2214
2217
  FILE_READ: "file_read",
@@ -3290,7 +3293,7 @@ var init_gemini = __esm({
3290
3293
  function preferIpv4Host(url) {
3291
3294
  return url;
3292
3295
  }
3293
- async function nodeHttpFetch(input, init = {}, redirectCount = 0) {
3296
+ async function nodeHttpFetch(input, init = {}, redirectCount = 0, opts = {}) {
3294
3297
  const urlStr = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
3295
3298
  const u = new URL(preferIpv4Host(urlStr) ?? urlStr);
3296
3299
  const lib = u.protocol === "https:" ? https__default.default : http__default.default;
@@ -3321,9 +3324,13 @@ async function nodeHttpFetch(input, init = {}, redirectCount = 0) {
3321
3324
  if (status >= 300 && status < 400 && location && redirectCount < MAX_REDIRECTS) {
3322
3325
  res.resume();
3323
3326
  const nextUrl = new URL(location, u).href;
3327
+ if (opts.allowedRedirectOrigin && new URL(nextUrl).origin !== opts.allowedRedirectOrigin) {
3328
+ reject(new Error(`Refusing cross-origin redirect to ${new URL(nextUrl).origin}`));
3329
+ return;
3330
+ }
3324
3331
  const downgrade = status === 303 || (status === 301 || status === 302) && method !== "GET" && method !== "HEAD";
3325
3332
  const nextInit = downgrade ? { ...init, method: "GET", body: void 0 } : init;
3326
- resolve2(nodeHttpFetch(nextUrl, nextInit, redirectCount + 1));
3333
+ resolve2(nodeHttpFetch(nextUrl, nextInit, redirectCount + 1, opts));
3327
3334
  return;
3328
3335
  }
3329
3336
  const encoding = (res.headers["content-encoding"] ?? "").toLowerCase();
@@ -3362,6 +3369,222 @@ var init_net = __esm({
3362
3369
  }
3363
3370
  });
3364
3371
 
3372
+ // src/providers/github-models.ts
3373
+ var github_models_exports = {};
3374
+ __export(github_models_exports, {
3375
+ GITHUB_MODELS_MAX_INPUT_TOKENS: () => GITHUB_MODELS_MAX_INPUT_TOKENS,
3376
+ GITHUB_MODELS_MAX_OUTPUT_TOKENS: () => GITHUB_MODELS_MAX_OUTPUT_TOKENS,
3377
+ GitHubModelsProvider: () => GitHubModelsProvider,
3378
+ stripModelOwnerPrefix: () => stripModelOwnerPrefix
3379
+ });
3380
+ function stripModelOwnerPrefix(id) {
3381
+ const slash = (id || "").indexOf("/");
3382
+ return slash === -1 ? id : id.slice(slash + 1);
3383
+ }
3384
+ function pickString(o, keys) {
3385
+ for (const k of keys) {
3386
+ const v = o[k];
3387
+ if (typeof v === "string" && v.length > 0) return v;
3388
+ }
3389
+ return void 0;
3390
+ }
3391
+ function pickNumber(o, keys) {
3392
+ for (const k of keys) {
3393
+ const v = k.includes(".") ? o[k.split(".")[0]]?.[k.split(".")[1]] : o[k];
3394
+ if (typeof v === "number" && Number.isFinite(v) && v > 0) return v;
3395
+ }
3396
+ return void 0;
3397
+ }
3398
+ function looksVisionCapable(o) {
3399
+ for (const k of ["supported_input_modalities", "input_modalities", "modalities", "capabilities"]) {
3400
+ const v = o[k];
3401
+ if (Array.isArray(v) && v.some((x) => typeof x === "string" && /image|vision/i.test(x))) return true;
3402
+ }
3403
+ return false;
3404
+ }
3405
+ function looksToolCapable(o) {
3406
+ const params = o["supported_parameters"];
3407
+ if (Array.isArray(params) && params.some((x) => typeof x === "string" && /^tools?$|function.?call/i.test(x))) {
3408
+ return true;
3409
+ }
3410
+ for (const k of ["capabilities", "supported_output_modalities"]) {
3411
+ const v = o[k];
3412
+ if (Array.isArray(v) && v.some((x) => typeof x === "string" && /tool|function.?call/i.test(x))) return true;
3413
+ }
3414
+ const caps = o["capabilities"];
3415
+ if (caps && typeof caps === "object" && !Array.isArray(caps)) {
3416
+ const c = caps;
3417
+ if (c["function_calling"] === true || c["tool_calling"] === true || c["tools"] === true) return true;
3418
+ }
3419
+ return false;
3420
+ }
3421
+ function parseNextLink(linkHeader) {
3422
+ if (!linkHeader) return null;
3423
+ for (const part of linkHeader.split(",")) {
3424
+ const match = part.match(/<([^>]+)>\s*;\s*rel="next"/);
3425
+ if (match) return match[1];
3426
+ }
3427
+ return null;
3428
+ }
3429
+ var GITHUB_MODELS_MAX_OUTPUT_TOKENS, GITHUB_MODELS_MAX_INPUT_TOKENS, DEFAULT_CONTEXT_WINDOW, GITHUB_MODELS_MAX_CATALOG_PAGES, GITHUB_MODELS_CATALOG_ORIGIN, GitHubModelsProvider;
3430
+ var init_github_models = __esm({
3431
+ "src/providers/github-models.ts"() {
3432
+ init_constants();
3433
+ init_openai();
3434
+ init_model_filter();
3435
+ init_net();
3436
+ GITHUB_MODELS_MAX_OUTPUT_TOKENS = 4e3;
3437
+ GITHUB_MODELS_MAX_INPUT_TOKENS = 8e3;
3438
+ DEFAULT_CONTEXT_WINDOW = 128e3;
3439
+ GITHUB_MODELS_MAX_CATALOG_PAGES = 20;
3440
+ GITHUB_MODELS_CATALOG_ORIGIN = new URL(GITHUB_MODELS_CATALOG_URL).origin;
3441
+ GitHubModelsProvider = class extends OpenAIProvider {
3442
+ constructor(config, model) {
3443
+ super({ ...config, apiKey: config.apiKey ?? "" }, model);
3444
+ this.useMaxCompletionTokens = isReasoningModel(stripModelOwnerPrefix(model.id));
3445
+ this.client = new OpenAI__default.default({
3446
+ apiKey: config.apiKey ?? "",
3447
+ baseURL: GITHUB_MODELS_INFERENCE_URL,
3448
+ defaultHeaders: { "X-GitHub-Api-Version": GITHUB_MODELS_API_VERSION, "User-Agent": "Cascade-AI" }
3449
+ });
3450
+ }
3451
+ /**
3452
+ * `OpenAIProvider.generateStream()` builds its request budget as
3453
+ * `options.maxTokens ?? this.model.maxOutputTokens` — an explicit per-call
3454
+ * `maxTokens` (T1Administrator's final compilation asks for 8,000, well
3455
+ * above GitHub's real ~4K hard cap) wins outright and is sent unclamped.
3456
+ * GitHub then rejects the request instead of answering, so a run routed to
3457
+ * this provider for that step fails outright rather than getting a
3458
+ * (perfectly serviceable, just shorter) completion. Clamp here, once, right
3459
+ * before delegating to the inherited implementation, rather than teaching
3460
+ * every caller GitHub's specific cap.
3461
+ */
3462
+ async generateStream(options, onChunk) {
3463
+ const clamped = options.maxTokens !== void 0 && options.maxTokens > GITHUB_MODELS_MAX_OUTPUT_TOKENS ? { ...options, maxTokens: GITHUB_MODELS_MAX_OUTPUT_TOKENS } : options;
3464
+ return super.generateStream(clamped, onChunk);
3465
+ }
3466
+ /**
3467
+ * The catalog is a GitHub REST resource, not an OpenAI `/models` list, and
3468
+ * wants GitHub's own header trio — the plain `Accept: application/json` the
3469
+ * openai-compatible provider sends is not the same request.
3470
+ */
3471
+ catalogHeaders() {
3472
+ return {
3473
+ Accept: "application/vnd.github+json",
3474
+ Authorization: `Bearer ${this.config.apiKey ?? ""}`,
3475
+ "X-GitHub-Api-Version": GITHUB_MODELS_API_VERSION,
3476
+ // GitHub's REST API rejects any request with no User-Agent — a plain 403
3477
+ // regardless of how valid the PAT is. nodeHttpFetch is a thin wrapper
3478
+ // over node:http/https, neither of which sets one the way a browser's
3479
+ // fetch or a full HTTP client library would, so this has to be explicit.
3480
+ "User-Agent": "Cascade-AI"
3481
+ };
3482
+ }
3483
+ async listModels() {
3484
+ const raw = [];
3485
+ let url = GITHUB_MODELS_CATALOG_URL;
3486
+ let pages = 0;
3487
+ const visited = /* @__PURE__ */ new Set();
3488
+ while (url && pages < GITHUB_MODELS_MAX_CATALOG_PAGES) {
3489
+ if (visited.has(url)) break;
3490
+ visited.add(url);
3491
+ const res = await nodeHttpFetch(url, { headers: this.catalogHeaders() }, 0, {
3492
+ allowedRedirectOrigin: GITHUB_MODELS_CATALOG_ORIGIN
3493
+ });
3494
+ if (!res.ok) throw new Error(`GitHub Models catalog ${url} returned HTTP ${res.status}`);
3495
+ const body = await res.json();
3496
+ const pageItems = Array.isArray(body) ? body : Array.isArray(body?.models) ? body.models : Array.isArray(body?.data) ? body.data : [];
3497
+ raw.push(...pageItems);
3498
+ const next = parseNextLink(res.headers.get("link"));
3499
+ if (next) {
3500
+ const resolved = new URL(next, url);
3501
+ if (resolved.origin !== GITHUB_MODELS_CATALOG_ORIGIN) {
3502
+ throw new Error(`Refusing cross-origin GitHub Models pagination URL: ${resolved.origin}`);
3503
+ }
3504
+ url = resolved.href;
3505
+ } else {
3506
+ url = null;
3507
+ }
3508
+ pages++;
3509
+ }
3510
+ const entries = raw.map((m) => {
3511
+ if (typeof m === "string") {
3512
+ return { id: m, name: m, contextWindow: DEFAULT_CONTEXT_WINDOW, vision: false, tools: false };
3513
+ }
3514
+ if (!m || typeof m !== "object") return void 0;
3515
+ const o = m;
3516
+ const id = pickString(o, ["id", "name", "model"]);
3517
+ if (!id) return void 0;
3518
+ return {
3519
+ id,
3520
+ name: pickString(o, ["friendly_name", "display_name", "name"]) ?? id,
3521
+ contextWindow: pickNumber(o, [
3522
+ "context_window",
3523
+ "context_length",
3524
+ "max_input_tokens",
3525
+ "limits.max_input_tokens",
3526
+ "limits.max_context_tokens"
3527
+ ]) ?? DEFAULT_CONTEXT_WINDOW,
3528
+ vision: looksVisionCapable(o),
3529
+ tools: looksToolCapable(o)
3530
+ };
3531
+ }).filter((e) => e !== void 0);
3532
+ if (entries.length === 0) return [];
3533
+ const chat = entries.filter((e) => isChatModel(e.id));
3534
+ return (chat.length ? chat : entries).map((e) => ({
3535
+ id: e.id,
3536
+ // full `owner/model` — what the API is addressed by
3537
+ name: e.name,
3538
+ provider: "github-models",
3539
+ // GitHub's real per-request input cap, not the base model's larger one
3540
+ // (see GITHUB_MODELS_MAX_INPUT_TOKENS above) — the same reasoning
3541
+ // already applied to maxOutputTokens below, for the input side.
3542
+ contextWindow: Math.min(e.contextWindow, GITHUB_MODELS_MAX_INPUT_TOKENS),
3543
+ isVisionCapable: e.vision,
3544
+ // A REAL $0, not a missing number — usage is bundled into the quota of
3545
+ // the plan the PAT belongs to and is never billed per token, so
3546
+ // `pricingUnknown: false` is the honest value (see the flag's contract in
3547
+ // core/router/pricing.ts: 0 alone is ambiguous). Set here rather than in
3548
+ // pricing-data.json because resolvePricing() matches exactly on
3549
+ // provider|model|region with no wildcard: every catalog model would need
3550
+ // a hand-curated row, and the next one GitHub adds would read as "cost
3551
+ // unknown" — an inconsistent picture for a billing reality that is
3552
+ // identical across the entire catalog.
3553
+ //
3554
+ // `isLocal` is the OTHER way to reach a real $0, and it is the wrong one:
3555
+ // it means "runs on this machine" and carries consequences that are
3556
+ // simply false for a hosted API — the shared concurrency-1
3557
+ // LocalRequestQueue (serialising these calls behind actual Ollama GPU
3558
+ // inference), the 300s local rather than 120s cloud inference timeout,
3559
+ // and a literal "[local]" tag in `cascade models`.
3560
+ inputCostPer1kTokens: 0,
3561
+ outputCostPer1kTokens: 0,
3562
+ pricingUnknown: false,
3563
+ maxOutputTokens: GITHUB_MODELS_MAX_OUTPUT_TOKENS,
3564
+ supportsStreaming: true,
3565
+ // Not every catalog model supports function/tool calling — a hardcoded
3566
+ // `true` sent `tools` to models that reject the parameter outright and
3567
+ // never engaged t3-worker.ts's existing text-tool fallback (only an
3568
+ // explicit `false` does). See looksToolCapable's own comment for why
3569
+ // "not positively advertised" defaults to false rather than true here.
3570
+ supportsToolUse: e.tools,
3571
+ isLocal: false
3572
+ }));
3573
+ }
3574
+ async isAvailable() {
3575
+ try {
3576
+ const res = await nodeHttpFetch(GITHUB_MODELS_CATALOG_URL, { headers: this.catalogHeaders() }, 0, {
3577
+ allowedRedirectOrigin: GITHUB_MODELS_CATALOG_ORIGIN
3578
+ });
3579
+ return res.ok;
3580
+ } catch {
3581
+ return false;
3582
+ }
3583
+ }
3584
+ };
3585
+ }
3586
+ });
3587
+
3365
3588
  // src/providers/ollama.ts
3366
3589
  var ollama_exports = {};
3367
3590
  __export(ollama_exports, {
@@ -4996,7 +5219,7 @@ Original error: ${err.message}`
4996
5219
  }
4997
5220
  };
4998
5221
  var ProviderConfigSchema = zod.z.object({
4999
- type: zod.z.enum(["anthropic", "openai", "gemini", "azure", "openai-compatible", "ollama"]),
5222
+ type: zod.z.enum(["anthropic", "openai", "gemini", "azure", "openai-compatible", "ollama", "github-models"]),
5000
5223
  label: zod.z.string().optional(),
5001
5224
  apiKey: zod.z.string().optional(),
5002
5225
  baseUrl: zod.z.string().url().optional(),
@@ -5176,6 +5399,18 @@ var CascadeConfigSchema = zod.z.object({
5176
5399
  budget: BudgetConfigSchema.default({}),
5177
5400
  theme: zod.z.string().default("cascade"),
5178
5401
  workspace: WorkspaceConfigSchema.default({}),
5402
+ /**
5403
+ * Per-provider tokens-per-minute overrides for the router's TpmLimiter (see
5404
+ * DEFAULT_PROVIDER_TPM in core/router/tpm-limiter.ts). Documented as a raw
5405
+ * config field for a while before this — since CascadeConfigSchema strips
5406
+ * unknown keys, a config file setting it was silently discarded and the
5407
+ * override never reached TpmLimiter's constructor. Optional/permissive on
5408
+ * provider name (not a ProviderType enum) so a config still validates
5409
+ * against a slightly newer/older provider list than this schema knows.
5410
+ */
5411
+ rateLimits: zod.z.object({
5412
+ providerTpm: zod.z.record(zod.z.string(), zod.z.number().positive()).optional()
5413
+ }).optional(),
5179
5414
  /**
5180
5415
  * Cascade Auto: when true, the TaskAnalyzer selects the optimal model for each
5181
5416
  * tier based on task type and complexity, overriding the static priority lists.
@@ -5707,7 +5942,14 @@ var ConfigManager = class {
5707
5942
  anthropic: "ANTHROPIC_API_KEY",
5708
5943
  openai: "OPENAI_API_KEY",
5709
5944
  gemini: "GOOGLE_API_KEY",
5710
- azure: "AZURE_OPENAI_KEY"
5945
+ azure: "AZURE_OPENAI_KEY",
5946
+ // Deliberately NOT `GITHUB_TOKEN`: that one is injected automatically by
5947
+ // GitHub Actions and is commonly exported in developer shells for `gh`
5948
+ // and git auth, and it will essentially never carry the fine-grained
5949
+ // `models: read` scope this provider needs. Adopting it would wire up a
5950
+ // provider that 403s on its first real call — a confusing failure for
5951
+ // something that is meant to be explicit opt-in BYOK.
5952
+ "github-models": "GITHUB_MODELS_TOKEN"
5711
5953
  };
5712
5954
  const envKey = envMap[provider];
5713
5955
  if (envKey && process.env[envKey]) return process.env[envKey];
@@ -5736,7 +5978,10 @@ var ConfigManager = class {
5736
5978
  { env: "ANTHROPIC_API_KEY", type: "anthropic" },
5737
5979
  { env: "OPENAI_API_KEY", type: "openai" },
5738
5980
  { env: "GOOGLE_API_KEY", type: "gemini" },
5739
- { env: "AZURE_OPENAI_KEY", type: "azure" }
5981
+ { env: "AZURE_OPENAI_KEY", type: "azure" },
5982
+ // See getApiKey() above for why this is GITHUB_MODELS_TOKEN and never
5983
+ // the ambient GITHUB_TOKEN.
5984
+ { env: "GITHUB_MODELS_TOKEN", type: "github-models" }
5740
5985
  ];
5741
5986
  for (const { env, type } of envProviders) {
5742
5987
  const key = process.env[env];
@@ -6118,6 +6363,7 @@ init_constants();
6118
6363
  init_anthropic();
6119
6364
  init_azure();
6120
6365
  init_gemini();
6366
+ init_github_models();
6121
6367
  init_ollama();
6122
6368
  init_openai_compatible();
6123
6369
  init_openai();
@@ -6125,6 +6371,7 @@ init_openai();
6125
6371
  // src/core/router/selector.ts
6126
6372
  init_constants();
6127
6373
  init_pricing();
6374
+ init_github_models();
6128
6375
  function normalizeModelId(id) {
6129
6376
  return id.replace(/^models\//, "").toLowerCase();
6130
6377
  }
@@ -6234,16 +6481,23 @@ var ModelSelector = class {
6234
6481
  return model;
6235
6482
  }
6236
6483
  }
6484
+ for (const model of this.availableModels.values()) {
6485
+ if (this.availableProviders.has(model.provider) && model.isVisionCapable) return model;
6486
+ }
6237
6487
  return null;
6238
6488
  }
6239
6489
  getNextFallback(currentModelId, tier) {
6240
6490
  const priority = this.getPriorityList(tier);
6241
6491
  const currentIdx = priority.indexOf(currentModelId);
6242
- if (currentIdx === -1) return null;
6243
- for (let i = currentIdx + 1; i < priority.length; i++) {
6244
- const key = priority[i];
6245
- const model = this.availableModels.get(key);
6246
- if (model && this.isUsable(model)) return model;
6492
+ if (currentIdx !== -1) {
6493
+ for (let i = currentIdx + 1; i < priority.length; i++) {
6494
+ const key = priority[i];
6495
+ const model = this.availableModels.get(key);
6496
+ if (model && this.isUsable(model)) return model;
6497
+ }
6498
+ }
6499
+ for (const [id, model] of this.availableModels) {
6500
+ if (id !== currentModelId && this.isUsable(model)) return model;
6247
6501
  }
6248
6502
  return null;
6249
6503
  }
@@ -6319,7 +6573,7 @@ var ModelSelector = class {
6319
6573
  if (overrideModelId.includes(":")) {
6320
6574
  const parts = overrideModelId.split(":");
6321
6575
  const prefix = parts[0].toLowerCase();
6322
- const validProviders = ["anthropic", "openai", "gemini", "azure", "openai-compatible", "ollama"];
6576
+ const validProviders = ["anthropic", "openai", "gemini", "azure", "openai-compatible", "ollama", "github-models"];
6323
6577
  if (validProviders.includes(prefix)) {
6324
6578
  providerStr = prefix;
6325
6579
  actualId = parts.slice(1).join(":");
@@ -6339,6 +6593,31 @@ var ModelSelector = class {
6339
6593
  else if (this.availableProviders.has("openai-compatible")) providerStr = "openai-compatible";
6340
6594
  else if (this.availableProviders.size === 1) providerStr = Array.from(this.availableProviders)[0];
6341
6595
  }
6596
+ if (providerStr === "github-models" && this.availableProviders.has("github-models")) {
6597
+ const dynamicModel = {
6598
+ id: actualId,
6599
+ name: actualId,
6600
+ provider: "github-models",
6601
+ contextWindow: GITHUB_MODELS_MAX_INPUT_TOKENS,
6602
+ isVisionCapable: false,
6603
+ inputCostPer1kTokens: 0,
6604
+ outputCostPer1kTokens: 0,
6605
+ pricingUnknown: false,
6606
+ maxOutputTokens: GITHUB_MODELS_MAX_OUTPUT_TOKENS,
6607
+ supportsStreaming: true,
6608
+ // Unconfirmed (no live catalog entry to read a real capability from)
6609
+ // must default to false, not left undefined: t3-worker.ts's text-tool
6610
+ // fallback only engages on a strict `=== false`, so an undefined
6611
+ // value here sends native `tools` to an unverified multi-vendor
6612
+ // model and any that don't support function calling reject the
6613
+ // request outright. Same default-to-false-when-unconfirmed policy
6614
+ // listModels() already applies for a real catalog entry.
6615
+ supportsToolUse: false,
6616
+ isLocal: false
6617
+ };
6618
+ this.addDynamicModel(dynamicModel);
6619
+ return dynamicModel;
6620
+ }
6342
6621
  if (providerStr && this.availableProviders.has(providerStr)) {
6343
6622
  const dynamicModel = withResolvedPricing({
6344
6623
  id: actualId,
@@ -6431,7 +6710,20 @@ var DEFAULT_PROVIDER_TPM = {
6431
6710
  gemini: 6e4,
6432
6711
  azure: 3e4,
6433
6712
  "openai-compatible": 3e4,
6434
- ollama: Number.POSITIVE_INFINITY
6713
+ ollama: Number.POSITIVE_INFINITY,
6714
+ // An order of magnitude below the others, on purpose. GitHub Models is
6715
+ // rate-limited by REQUESTS (Free ≈10 RPM, Pro ≈20, plus low daily caps), and
6716
+ // this bucket meters TOKENS — there is no request-count limiter in this
6717
+ // codebase, and building one is not in scope. The approximation works because
6718
+ // the router reserves `model.maxOutputTokens + 512` per call, and the
6719
+ // provider reports GitHub's real ~4K per-request cap rather than the
6720
+ // underlying model's larger one — so every call withdraws a near-constant
6721
+ // ~4.5K regardless of how short the reply actually turns out to be. At 8K/min
6722
+ // that lands around 1–2 calls per minute of headroom: comfortably inside even
6723
+ // the Free tier, with room for the daily cap to matter more than this does.
6724
+ // Users on a higher tier can raise it with no code change through the generic
6725
+ // `config.rateLimits.providerTpm['github-models']` override.
6726
+ "github-models": 8e3
6435
6727
  };
6436
6728
  var TpmLimiter = class {
6437
6729
  buckets = /* @__PURE__ */ new Map();
@@ -6778,6 +7070,7 @@ var ModelProfiler = class {
6778
7070
  (m) => !alreadyProfiled.has(m.id) && !SKIP_PATTERN.test(m.id) && !SKIP_PATTERN.test(m.name)
6779
7071
  );
6780
7072
  if (toProfile.length === 0) return;
7073
+ const probesHitGithubModels = this.router?.getModelForTier("T3")?.provider === "github-models";
6781
7074
  const openRouterModels = await fetchOpenRouterModels();
6782
7075
  const orByNormalizedId = /* @__PURE__ */ new Map();
6783
7076
  for (const m of openRouterModels) {
@@ -6792,7 +7085,7 @@ var ModelProfiler = class {
6792
7085
  if (orMatch?.description) {
6793
7086
  specializations = extractSpecializations(orMatch.description);
6794
7087
  }
6795
- if (specializations.length === 0 && this.router) {
7088
+ if (specializations.length === 0 && this.router && !probesHitGithubModels) {
6796
7089
  specializations = await queryModelDirectly(this.router);
6797
7090
  }
6798
7091
  this.store.saveModelProfile(model.id, model.provider, specializations);
@@ -7017,6 +7310,7 @@ var LiveDataProvider = class {
7017
7310
  */
7018
7311
  applyLivePricing(models) {
7019
7312
  return models.map((m) => {
7313
+ if (m.provider === "github-models") return m;
7020
7314
  const p = this.getLivePrice(m.baseModelId ?? m.id);
7021
7315
  if (!p) return m;
7022
7316
  const reconciled = reconcilePrice(m, p);
@@ -7055,6 +7349,7 @@ var LiveDataProvider = class {
7055
7349
  */
7056
7350
  applyLiveCapabilities(models) {
7057
7351
  return models.map((m) => {
7352
+ if (m.provider === "github-models") return m;
7058
7353
  const c = this.getCapability(m.baseModelId ?? m.id);
7059
7354
  if (!c) return m;
7060
7355
  const next = { ...m };
@@ -7358,13 +7653,16 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
7358
7653
  for (const model of azureDeployments) this.selector.addDynamicModel(model);
7359
7654
  availableProviders.add("azure");
7360
7655
  }
7656
+ if (availableProviders.has("github-models")) {
7657
+ await this.discoverGitHubModelsCatalog(config);
7658
+ }
7361
7659
  await this.validateCloudProviderModels(config);
7362
7660
  for (const tier of ["T1", "T2", "T3"]) {
7363
7661
  const override = tier === "T1" ? config.models.t1 : tier === "T2" ? config.models.t2 : config.models.t3;
7364
7662
  if (!override || override === "auto") continue;
7365
7663
  const model = this.selector.selectForTier(tier, override);
7366
7664
  if (!model) {
7367
- const knownProviders = ["anthropic", "openai", "gemini", "azure", "openai-compatible", "ollama"];
7665
+ const knownProviders = ["anthropic", "openai", "gemini", "azure", "openai-compatible", "ollama", "github-models"];
7368
7666
  const hasProviderPrefix = override.includes(":") && knownProviders.some((p) => override.startsWith(p + ":"));
7369
7667
  if (hasProviderPrefix) {
7370
7668
  const provider = override.split(":")[0];
@@ -7519,7 +7817,7 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
7519
7817
  * models without a package upgrade. Mirrors discoverOllamaModels.
7520
7818
  */
7521
7819
  async discoverProviderModels() {
7522
- const cloud = ["anthropic", "openai", "gemini", "azure", "openai-compatible"];
7820
+ const cloud = ["anthropic", "openai", "gemini", "azure", "openai-compatible", "github-models"];
7523
7821
  const tasks = cloud.map(async (type) => {
7524
7822
  if (!this.selector.isProviderAvailable(type)) return;
7525
7823
  const seed = this.getAnyModelForProvider(type);
@@ -7574,6 +7872,9 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
7574
7872
  this.ensureProvider(options.model, this.config.providers);
7575
7873
  }
7576
7874
  let model = requireVision ? this.selector.selectVisionModel() : options.model ?? this.tierModels.get(tier) ?? this.selector.selectForTier(tier) ?? void 0;
7875
+ if (requireVision && model) {
7876
+ this.ensureProvider(model, this.config.providers);
7877
+ }
7577
7878
  if (options.forceLocal && model && !this.isPrivateModel(model)) {
7578
7879
  const localModel = this.selector.getAllAvailableModels().find((m) => this.isPrivateModel(m));
7579
7880
  if (!localModel) {
@@ -7587,7 +7888,8 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
7587
7888
  if (!model) throw new Error(`No model available for tier ${tier}`);
7588
7889
  const provider = this.getProvider(model);
7589
7890
  if (!provider) throw new Error(`No provider for model ${model.id}`);
7590
- const estimatedTokens = (options.maxTokens ?? model.maxOutputTokens ?? 1024) + 512;
7891
+ const requestedTokens = options.maxTokens ?? model.maxOutputTokens ?? 1024;
7892
+ const estimatedTokens = Math.min(requestedTokens, model.maxOutputTokens ?? requestedTokens) + 512;
7591
7893
  if (this.tpmLimiter) {
7592
7894
  await this.tpmLimiter.acquire(model.provider, estimatedTokens);
7593
7895
  }
@@ -7678,7 +7980,8 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
7678
7980
  });
7679
7981
  releaseLocalSlot?.();
7680
7982
  releaseLocalSlot = void 0;
7681
- return this.generate(tier, options, onChunk, requireVision);
7983
+ const retryOpts = options.model && options.model.id === model.id ? { ...options, model: void 0 } : options;
7984
+ return this.generate(tier, retryOpts, onChunk, requireVision);
7682
7985
  }
7683
7986
  }
7684
7987
  if (isModelNotFoundError(errMsg)) {
@@ -8029,6 +8332,26 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
8029
8332
  return false;
8030
8333
  }
8031
8334
  }
8335
+ /**
8336
+ * Registers GitHub Models' real catalog ids via addDynamicModel so init()'s
8337
+ * tier-fill loop can pick one. Registration alone does not create a bound
8338
+ * BaseProvider instance (see ensureProvider) — the tier-fill loop's own
8339
+ * ensureProvider call does that for whichever model it actually selects,
8340
+ * which is why this runs BEFORE that loop rather than only in the
8341
+ * background discoverProviderModels() refresh.
8342
+ */
8343
+ async discoverGitHubModelsCatalog(config) {
8344
+ const cfg = config.providers.find((p) => p.type === "github-models") ?? { type: "github-models" };
8345
+ const seed = this.getAnyModelForProvider("github-models");
8346
+ if (!seed) return;
8347
+ try {
8348
+ const provider = new GitHubModelsProvider(cfg, seed);
8349
+ const models = await provider.listModels();
8350
+ for (const m of models) this.selector.addDynamicModel(m);
8351
+ } catch (err) {
8352
+ console.warn("[router] GitHub Models catalog discovery failed:", err instanceof Error ? err.message : err);
8353
+ }
8354
+ }
8032
8355
  ensureProvider(model, configs) {
8033
8356
  const key = `${model.provider}:${model.id}`;
8034
8357
  if (this.providers.has(key)) return;
@@ -8053,6 +8376,8 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
8053
8376
  return new OllamaProvider(cfg, model);
8054
8377
  case "openai-compatible":
8055
8378
  return new OpenAICompatibleProvider(cfg, model);
8379
+ case "github-models":
8380
+ return new GitHubModelsProvider(cfg, model);
8056
8381
  default:
8057
8382
  throw new Error(`Unsupported provider type: ${String(cfg.type)}`);
8058
8383
  }
@@ -8060,7 +8385,7 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
8060
8385
  getAnyModelForProvider(type) {
8061
8386
  const fromCatalog = Object.values(MODELS).find((m) => m.provider === type);
8062
8387
  if (fromCatalog) return fromCatalog;
8063
- if (type === "openai-compatible" || type === "azure") {
8388
+ if (type === "openai-compatible" || type === "azure" || type === "github-models") {
8064
8389
  return {
8065
8390
  id: type,
8066
8391
  name: type,
@@ -20150,7 +20475,10 @@ async function refreshModelCache(store, providers) {
20150
20475
  else if (provider.type === "anthropic") instance = new AnthropicProvider(provider, dummyModel);
20151
20476
  else if (provider.type === "ollama") instance = new OllamaProvider(provider, dummyModel);
20152
20477
  else if (provider.type === "openai-compatible") instance = new OpenAICompatibleProvider(provider, dummyModel);
20153
- else if (provider.type === "azure") {
20478
+ else if (provider.type === "github-models") {
20479
+ const { GitHubModelsProvider: GitHubModelsProvider2 } = await Promise.resolve().then(() => (init_github_models(), github_models_exports));
20480
+ instance = new GitHubModelsProvider2(provider, dummyModel);
20481
+ } else if (provider.type === "azure") {
20154
20482
  const { AzureOpenAIProvider: AzureOpenAIProvider2 } = await Promise.resolve().then(() => (init_azure(), azure_exports));
20155
20483
  instance = new AzureOpenAIProvider2(provider, dummyModel);
20156
20484
  }
@@ -21257,7 +21585,7 @@ async function validateConfiguredModels(config) {
21257
21585
  function inferProviderFromModelId(id, providers) {
21258
21586
  if (id.includes(":")) {
21259
21587
  const prefix = id.split(":")[0].toLowerCase();
21260
- const validProviders = ["anthropic", "openai", "gemini", "azure", "openai-compatible", "ollama"];
21588
+ const validProviders = ["anthropic", "openai", "gemini", "azure", "openai-compatible", "ollama", "github-models"];
21261
21589
  if (validProviders.includes(prefix)) {
21262
21590
  return prefix;
21263
21591
  }
@@ -21554,9 +21882,10 @@ var PROVIDER_LABELS = {
21554
21882
  gemini: "Google Gemini",
21555
21883
  azure: "Azure OpenAI",
21556
21884
  ollama: "Ollama (local)",
21557
- "openai-compatible": "OpenAI-Compatible"
21885
+ "openai-compatible": "OpenAI-Compatible",
21886
+ "github-models": "GitHub Models"
21558
21887
  };
21559
- var providerOrder = ["anthropic", "openai", "gemini", "azure", "ollama", "openai-compatible"];
21888
+ var providerOrder = ["anthropic", "openai", "gemini", "azure", "ollama", "openai-compatible", "github-models"];
21560
21889
  function buildInitialEntries(types) {
21561
21890
  return [...types].map((type) => ({
21562
21891
  id: crypto.randomUUID(),
@@ -21728,6 +22057,13 @@ function SetupWizard({ workspacePath, onComplete }) {
21728
22057
  const fetched = await p.listModels();
21729
22058
  fetched.forEach((m) => models.push({ id: m.id, name: m.name, providerLabel: entry.label }));
21730
22059
  dispatchRef.current({ type: "SET_FETCH_LOG", line: ` \u2714 ${entry.label} \u2014 ${fetched.length} models` });
22060
+ } else if (type === "github-models") {
22061
+ const { GitHubModelsProvider: GitHubModelsProvider2 } = await Promise.resolve().then(() => (init_github_models(), github_models_exports));
22062
+ const dummyModel = { id: "dummy", name: "dummy", provider: type, contextWindow: 0, isVisionCapable: false, inputCostPer1kTokens: 0, outputCostPer1kTokens: 0, maxOutputTokens: 0, supportsStreaming: false, isLocal: false };
22063
+ const p = new GitHubModelsProvider2({ type, apiKey }, dummyModel);
22064
+ const fetched = await p.listModels();
22065
+ fetched.forEach((m) => models.push({ id: `${type}:${m.id}`, name: m.name, providerLabel: entry.label }));
22066
+ dispatchRef.current({ type: "SET_FETCH_LOG", line: ` \u2714 ${entry.label} \u2014 ${fetched.length} models` });
21731
22067
  } else if (type === "gemini") {
21732
22068
  const { GeminiProvider: GeminiProvider2 } = await Promise.resolve().then(() => (init_gemini(), gemini_exports));
21733
22069
  const dummyModel = { id: "dummy", name: "dummy", provider: type, contextWindow: 0, isVisionCapable: false, inputCostPer1kTokens: 0, outputCostPer1kTokens: 0, maxOutputTokens: 0, supportsStreaming: false, isLocal: false };
@@ -21906,7 +22242,8 @@ function SetupWizard({ workspacePath, onComplete }) {
21906
22242
  /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: focused ? theme.colors.primary : selected ? theme.colors.foreground : theme.colors.muted, children: PROVIDER_LABELS[p] }),
21907
22243
  p === "azure" && /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: theme.colors.muted, dimColor: true, children: " \u2014 multiple deployments supported" }),
21908
22244
  p === "openai-compatible" && /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: theme.colors.muted, dimColor: true, children: " \u2014 Groq, Together, custom" }),
21909
- p === "ollama" && /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: theme.colors.muted, dimColor: true, children: " \u2014 no API key needed" })
22245
+ p === "ollama" && /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: theme.colors.muted, dimColor: true, children: " \u2014 no API key needed" }),
22246
+ p === "github-models" && /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: theme.colors.muted, dimColor: true, children: " \u2014 GitHub PAT with `models: read`" })
21910
22247
  ] }, p);
21911
22248
  }) }),
21912
22249
  /* @__PURE__ */ jsxRuntime.jsx(ink.Box, { marginTop: 1, children: /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: theme.colors.muted, dimColor: true, children: "space select \xB7 a all \xB7 i invert \xB7 enter continue" }) }),
@@ -22313,7 +22650,8 @@ async function doctorCommand() {
22313
22650
  { type: "anthropic", name: "Anthropic" },
22314
22651
  { type: "openai", name: "OpenAI" },
22315
22652
  { type: "gemini", name: "Google Gemini" },
22316
- { type: "azure", name: "Azure OpenAI" }
22653
+ { type: "azure", name: "Azure OpenAI" },
22654
+ { type: "github-models", name: "GitHub Models" }
22317
22655
  ];
22318
22656
  for (const { type, name } of providers) {
22319
22657
  const key = cm.getApiKey(type);