cascade-ai 0.64.0 → 0.65.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1753,11 +1753,11 @@ var init_pricing = __esm({
1753
1753
  });
1754
1754
 
1755
1755
  // src/constants.ts
1756
- 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;
1756
+ 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;
1757
1757
  var init_constants = __esm({
1758
1758
  "src/constants.ts"() {
1759
1759
  init_pricing();
1760
- CASCADE_VERSION = "0.64.0";
1760
+ CASCADE_VERSION = "0.65.0";
1761
1761
  CASCADE_CONFIG_FILE = ".cascade/config.json";
1762
1762
  CASCADE_DB_FILE = ".cascade/memory.db";
1763
1763
  CASCADE_DASHBOARD_SECRET_FILE = ".cascade/dashboard-secret";
@@ -2156,6 +2156,9 @@ var init_constants = __esm({
2156
2156
  OLLAMA_BASE_URL = "http://localhost:11434";
2157
2157
  LM_STUDIO_BASE_URL = "http://localhost:1234";
2158
2158
  AZURE_BASE_URL_TEMPLATE = "https://{resource}.openai.azure.com";
2159
+ GITHUB_MODELS_INFERENCE_URL = "https://models.github.ai/inference";
2160
+ GITHUB_MODELS_CATALOG_URL = "https://models.github.ai/catalog/models";
2161
+ GITHUB_MODELS_API_VERSION = "2026-03-10";
2159
2162
  TOOL_NAMES = {
2160
2163
  SHELL: "shell",
2161
2164
  FILE_READ: "file_read",
@@ -3237,7 +3240,7 @@ var init_gemini = __esm({
3237
3240
  function preferIpv4Host(url) {
3238
3241
  return url;
3239
3242
  }
3240
- async function nodeHttpFetch(input, init = {}, redirectCount = 0) {
3243
+ async function nodeHttpFetch(input, init = {}, redirectCount = 0, opts = {}) {
3241
3244
  const urlStr = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
3242
3245
  const u = new URL(preferIpv4Host(urlStr) ?? urlStr);
3243
3246
  const lib = u.protocol === "https:" ? https : http;
@@ -3268,9 +3271,13 @@ async function nodeHttpFetch(input, init = {}, redirectCount = 0) {
3268
3271
  if (status >= 300 && status < 400 && location && redirectCount < MAX_REDIRECTS) {
3269
3272
  res.resume();
3270
3273
  const nextUrl = new URL(location, u).href;
3274
+ if (opts.allowedRedirectOrigin && new URL(nextUrl).origin !== opts.allowedRedirectOrigin) {
3275
+ reject(new Error(`Refusing cross-origin redirect to ${new URL(nextUrl).origin}`));
3276
+ return;
3277
+ }
3271
3278
  const downgrade = status === 303 || (status === 301 || status === 302) && method !== "GET" && method !== "HEAD";
3272
3279
  const nextInit = downgrade ? { ...init, method: "GET", body: void 0 } : init;
3273
- resolve2(nodeHttpFetch(nextUrl, nextInit, redirectCount + 1));
3280
+ resolve2(nodeHttpFetch(nextUrl, nextInit, redirectCount + 1, opts));
3274
3281
  return;
3275
3282
  }
3276
3283
  const encoding = (res.headers["content-encoding"] ?? "").toLowerCase();
@@ -3309,6 +3316,222 @@ var init_net = __esm({
3309
3316
  }
3310
3317
  });
3311
3318
 
3319
+ // src/providers/github-models.ts
3320
+ var github_models_exports = {};
3321
+ __export(github_models_exports, {
3322
+ GITHUB_MODELS_MAX_INPUT_TOKENS: () => GITHUB_MODELS_MAX_INPUT_TOKENS,
3323
+ GITHUB_MODELS_MAX_OUTPUT_TOKENS: () => GITHUB_MODELS_MAX_OUTPUT_TOKENS,
3324
+ GitHubModelsProvider: () => GitHubModelsProvider,
3325
+ stripModelOwnerPrefix: () => stripModelOwnerPrefix
3326
+ });
3327
+ function stripModelOwnerPrefix(id) {
3328
+ const slash = (id || "").indexOf("/");
3329
+ return slash === -1 ? id : id.slice(slash + 1);
3330
+ }
3331
+ function pickString(o, keys) {
3332
+ for (const k of keys) {
3333
+ const v = o[k];
3334
+ if (typeof v === "string" && v.length > 0) return v;
3335
+ }
3336
+ return void 0;
3337
+ }
3338
+ function pickNumber(o, keys) {
3339
+ for (const k of keys) {
3340
+ const v = k.includes(".") ? o[k.split(".")[0]]?.[k.split(".")[1]] : o[k];
3341
+ if (typeof v === "number" && Number.isFinite(v) && v > 0) return v;
3342
+ }
3343
+ return void 0;
3344
+ }
3345
+ function looksVisionCapable(o) {
3346
+ for (const k of ["supported_input_modalities", "input_modalities", "modalities", "capabilities"]) {
3347
+ const v = o[k];
3348
+ if (Array.isArray(v) && v.some((x) => typeof x === "string" && /image|vision/i.test(x))) return true;
3349
+ }
3350
+ return false;
3351
+ }
3352
+ function looksToolCapable(o) {
3353
+ const params = o["supported_parameters"];
3354
+ if (Array.isArray(params) && params.some((x) => typeof x === "string" && /^tools?$|function.?call/i.test(x))) {
3355
+ return true;
3356
+ }
3357
+ for (const k of ["capabilities", "supported_output_modalities"]) {
3358
+ const v = o[k];
3359
+ if (Array.isArray(v) && v.some((x) => typeof x === "string" && /tool|function.?call/i.test(x))) return true;
3360
+ }
3361
+ const caps = o["capabilities"];
3362
+ if (caps && typeof caps === "object" && !Array.isArray(caps)) {
3363
+ const c = caps;
3364
+ if (c["function_calling"] === true || c["tool_calling"] === true || c["tools"] === true) return true;
3365
+ }
3366
+ return false;
3367
+ }
3368
+ function parseNextLink(linkHeader) {
3369
+ if (!linkHeader) return null;
3370
+ for (const part of linkHeader.split(",")) {
3371
+ const match = part.match(/<([^>]+)>\s*;\s*rel="next"/);
3372
+ if (match) return match[1];
3373
+ }
3374
+ return null;
3375
+ }
3376
+ var GITHUB_MODELS_MAX_OUTPUT_TOKENS, GITHUB_MODELS_MAX_INPUT_TOKENS, DEFAULT_CONTEXT_WINDOW, GITHUB_MODELS_MAX_CATALOG_PAGES, GITHUB_MODELS_CATALOG_ORIGIN, GitHubModelsProvider;
3377
+ var init_github_models = __esm({
3378
+ "src/providers/github-models.ts"() {
3379
+ init_constants();
3380
+ init_openai();
3381
+ init_model_filter();
3382
+ init_net();
3383
+ GITHUB_MODELS_MAX_OUTPUT_TOKENS = 4e3;
3384
+ GITHUB_MODELS_MAX_INPUT_TOKENS = 8e3;
3385
+ DEFAULT_CONTEXT_WINDOW = 128e3;
3386
+ GITHUB_MODELS_MAX_CATALOG_PAGES = 20;
3387
+ GITHUB_MODELS_CATALOG_ORIGIN = new URL(GITHUB_MODELS_CATALOG_URL).origin;
3388
+ GitHubModelsProvider = class extends OpenAIProvider {
3389
+ constructor(config, model) {
3390
+ super({ ...config, apiKey: config.apiKey ?? "" }, model);
3391
+ this.useMaxCompletionTokens = isReasoningModel(stripModelOwnerPrefix(model.id));
3392
+ this.client = new OpenAI({
3393
+ apiKey: config.apiKey ?? "",
3394
+ baseURL: GITHUB_MODELS_INFERENCE_URL,
3395
+ defaultHeaders: { "X-GitHub-Api-Version": GITHUB_MODELS_API_VERSION, "User-Agent": "Cascade-AI" }
3396
+ });
3397
+ }
3398
+ /**
3399
+ * `OpenAIProvider.generateStream()` builds its request budget as
3400
+ * `options.maxTokens ?? this.model.maxOutputTokens` — an explicit per-call
3401
+ * `maxTokens` (T1Administrator's final compilation asks for 8,000, well
3402
+ * above GitHub's real ~4K hard cap) wins outright and is sent unclamped.
3403
+ * GitHub then rejects the request instead of answering, so a run routed to
3404
+ * this provider for that step fails outright rather than getting a
3405
+ * (perfectly serviceable, just shorter) completion. Clamp here, once, right
3406
+ * before delegating to the inherited implementation, rather than teaching
3407
+ * every caller GitHub's specific cap.
3408
+ */
3409
+ async generateStream(options, onChunk) {
3410
+ const clamped = options.maxTokens !== void 0 && options.maxTokens > GITHUB_MODELS_MAX_OUTPUT_TOKENS ? { ...options, maxTokens: GITHUB_MODELS_MAX_OUTPUT_TOKENS } : options;
3411
+ return super.generateStream(clamped, onChunk);
3412
+ }
3413
+ /**
3414
+ * The catalog is a GitHub REST resource, not an OpenAI `/models` list, and
3415
+ * wants GitHub's own header trio — the plain `Accept: application/json` the
3416
+ * openai-compatible provider sends is not the same request.
3417
+ */
3418
+ catalogHeaders() {
3419
+ return {
3420
+ Accept: "application/vnd.github+json",
3421
+ Authorization: `Bearer ${this.config.apiKey ?? ""}`,
3422
+ "X-GitHub-Api-Version": GITHUB_MODELS_API_VERSION,
3423
+ // GitHub's REST API rejects any request with no User-Agent — a plain 403
3424
+ // regardless of how valid the PAT is. nodeHttpFetch is a thin wrapper
3425
+ // over node:http/https, neither of which sets one the way a browser's
3426
+ // fetch or a full HTTP client library would, so this has to be explicit.
3427
+ "User-Agent": "Cascade-AI"
3428
+ };
3429
+ }
3430
+ async listModels() {
3431
+ const raw = [];
3432
+ let url = GITHUB_MODELS_CATALOG_URL;
3433
+ let pages = 0;
3434
+ const visited = /* @__PURE__ */ new Set();
3435
+ while (url && pages < GITHUB_MODELS_MAX_CATALOG_PAGES) {
3436
+ if (visited.has(url)) break;
3437
+ visited.add(url);
3438
+ const res = await nodeHttpFetch(url, { headers: this.catalogHeaders() }, 0, {
3439
+ allowedRedirectOrigin: GITHUB_MODELS_CATALOG_ORIGIN
3440
+ });
3441
+ if (!res.ok) throw new Error(`GitHub Models catalog ${url} returned HTTP ${res.status}`);
3442
+ const body = await res.json();
3443
+ const pageItems = Array.isArray(body) ? body : Array.isArray(body?.models) ? body.models : Array.isArray(body?.data) ? body.data : [];
3444
+ raw.push(...pageItems);
3445
+ const next = parseNextLink(res.headers.get("link"));
3446
+ if (next) {
3447
+ const resolved = new URL(next, url);
3448
+ if (resolved.origin !== GITHUB_MODELS_CATALOG_ORIGIN) {
3449
+ throw new Error(`Refusing cross-origin GitHub Models pagination URL: ${resolved.origin}`);
3450
+ }
3451
+ url = resolved.href;
3452
+ } else {
3453
+ url = null;
3454
+ }
3455
+ pages++;
3456
+ }
3457
+ const entries = raw.map((m) => {
3458
+ if (typeof m === "string") {
3459
+ return { id: m, name: m, contextWindow: DEFAULT_CONTEXT_WINDOW, vision: false, tools: false };
3460
+ }
3461
+ if (!m || typeof m !== "object") return void 0;
3462
+ const o = m;
3463
+ const id = pickString(o, ["id", "name", "model"]);
3464
+ if (!id) return void 0;
3465
+ return {
3466
+ id,
3467
+ name: pickString(o, ["friendly_name", "display_name", "name"]) ?? id,
3468
+ contextWindow: pickNumber(o, [
3469
+ "context_window",
3470
+ "context_length",
3471
+ "max_input_tokens",
3472
+ "limits.max_input_tokens",
3473
+ "limits.max_context_tokens"
3474
+ ]) ?? DEFAULT_CONTEXT_WINDOW,
3475
+ vision: looksVisionCapable(o),
3476
+ tools: looksToolCapable(o)
3477
+ };
3478
+ }).filter((e) => e !== void 0);
3479
+ if (entries.length === 0) return [];
3480
+ const chat = entries.filter((e) => isChatModel(e.id));
3481
+ return (chat.length ? chat : entries).map((e) => ({
3482
+ id: e.id,
3483
+ // full `owner/model` — what the API is addressed by
3484
+ name: e.name,
3485
+ provider: "github-models",
3486
+ // GitHub's real per-request input cap, not the base model's larger one
3487
+ // (see GITHUB_MODELS_MAX_INPUT_TOKENS above) — the same reasoning
3488
+ // already applied to maxOutputTokens below, for the input side.
3489
+ contextWindow: Math.min(e.contextWindow, GITHUB_MODELS_MAX_INPUT_TOKENS),
3490
+ isVisionCapable: e.vision,
3491
+ // A REAL $0, not a missing number — usage is bundled into the quota of
3492
+ // the plan the PAT belongs to and is never billed per token, so
3493
+ // `pricingUnknown: false` is the honest value (see the flag's contract in
3494
+ // core/router/pricing.ts: 0 alone is ambiguous). Set here rather than in
3495
+ // pricing-data.json because resolvePricing() matches exactly on
3496
+ // provider|model|region with no wildcard: every catalog model would need
3497
+ // a hand-curated row, and the next one GitHub adds would read as "cost
3498
+ // unknown" — an inconsistent picture for a billing reality that is
3499
+ // identical across the entire catalog.
3500
+ //
3501
+ // `isLocal` is the OTHER way to reach a real $0, and it is the wrong one:
3502
+ // it means "runs on this machine" and carries consequences that are
3503
+ // simply false for a hosted API — the shared concurrency-1
3504
+ // LocalRequestQueue (serialising these calls behind actual Ollama GPU
3505
+ // inference), the 300s local rather than 120s cloud inference timeout,
3506
+ // and a literal "[local]" tag in `cascade models`.
3507
+ inputCostPer1kTokens: 0,
3508
+ outputCostPer1kTokens: 0,
3509
+ pricingUnknown: false,
3510
+ maxOutputTokens: GITHUB_MODELS_MAX_OUTPUT_TOKENS,
3511
+ supportsStreaming: true,
3512
+ // Not every catalog model supports function/tool calling — a hardcoded
3513
+ // `true` sent `tools` to models that reject the parameter outright and
3514
+ // never engaged t3-worker.ts's existing text-tool fallback (only an
3515
+ // explicit `false` does). See looksToolCapable's own comment for why
3516
+ // "not positively advertised" defaults to false rather than true here.
3517
+ supportsToolUse: e.tools,
3518
+ isLocal: false
3519
+ }));
3520
+ }
3521
+ async isAvailable() {
3522
+ try {
3523
+ const res = await nodeHttpFetch(GITHUB_MODELS_CATALOG_URL, { headers: this.catalogHeaders() }, 0, {
3524
+ allowedRedirectOrigin: GITHUB_MODELS_CATALOG_ORIGIN
3525
+ });
3526
+ return res.ok;
3527
+ } catch {
3528
+ return false;
3529
+ }
3530
+ }
3531
+ };
3532
+ }
3533
+ });
3534
+
3312
3535
  // src/providers/ollama.ts
3313
3536
  var ollama_exports = {};
3314
3537
  __export(ollama_exports, {
@@ -4943,7 +5166,7 @@ Original error: ${err.message}`
4943
5166
  }
4944
5167
  };
4945
5168
  var ProviderConfigSchema = z.object({
4946
- type: z.enum(["anthropic", "openai", "gemini", "azure", "openai-compatible", "ollama"]),
5169
+ type: z.enum(["anthropic", "openai", "gemini", "azure", "openai-compatible", "ollama", "github-models"]),
4947
5170
  label: z.string().optional(),
4948
5171
  apiKey: z.string().optional(),
4949
5172
  baseUrl: z.string().url().optional(),
@@ -5123,6 +5346,18 @@ var CascadeConfigSchema = z.object({
5123
5346
  budget: BudgetConfigSchema.default({}),
5124
5347
  theme: z.string().default("cascade"),
5125
5348
  workspace: WorkspaceConfigSchema.default({}),
5349
+ /**
5350
+ * Per-provider tokens-per-minute overrides for the router's TpmLimiter (see
5351
+ * DEFAULT_PROVIDER_TPM in core/router/tpm-limiter.ts). Documented as a raw
5352
+ * config field for a while before this — since CascadeConfigSchema strips
5353
+ * unknown keys, a config file setting it was silently discarded and the
5354
+ * override never reached TpmLimiter's constructor. Optional/permissive on
5355
+ * provider name (not a ProviderType enum) so a config still validates
5356
+ * against a slightly newer/older provider list than this schema knows.
5357
+ */
5358
+ rateLimits: z.object({
5359
+ providerTpm: z.record(z.string(), z.number().positive()).optional()
5360
+ }).optional(),
5126
5361
  /**
5127
5362
  * Cascade Auto: when true, the TaskAnalyzer selects the optimal model for each
5128
5363
  * tier based on task type and complexity, overriding the static priority lists.
@@ -5654,7 +5889,14 @@ var ConfigManager = class {
5654
5889
  anthropic: "ANTHROPIC_API_KEY",
5655
5890
  openai: "OPENAI_API_KEY",
5656
5891
  gemini: "GOOGLE_API_KEY",
5657
- azure: "AZURE_OPENAI_KEY"
5892
+ azure: "AZURE_OPENAI_KEY",
5893
+ // Deliberately NOT `GITHUB_TOKEN`: that one is injected automatically by
5894
+ // GitHub Actions and is commonly exported in developer shells for `gh`
5895
+ // and git auth, and it will essentially never carry the fine-grained
5896
+ // `models: read` scope this provider needs. Adopting it would wire up a
5897
+ // provider that 403s on its first real call — a confusing failure for
5898
+ // something that is meant to be explicit opt-in BYOK.
5899
+ "github-models": "GITHUB_MODELS_TOKEN"
5658
5900
  };
5659
5901
  const envKey = envMap[provider];
5660
5902
  if (envKey && process.env[envKey]) return process.env[envKey];
@@ -5683,7 +5925,10 @@ var ConfigManager = class {
5683
5925
  { env: "ANTHROPIC_API_KEY", type: "anthropic" },
5684
5926
  { env: "OPENAI_API_KEY", type: "openai" },
5685
5927
  { env: "GOOGLE_API_KEY", type: "gemini" },
5686
- { env: "AZURE_OPENAI_KEY", type: "azure" }
5928
+ { env: "AZURE_OPENAI_KEY", type: "azure" },
5929
+ // See getApiKey() above for why this is GITHUB_MODELS_TOKEN and never
5930
+ // the ambient GITHUB_TOKEN.
5931
+ { env: "GITHUB_MODELS_TOKEN", type: "github-models" }
5687
5932
  ];
5688
5933
  for (const { env, type } of envProviders) {
5689
5934
  const key = process.env[env];
@@ -6065,6 +6310,7 @@ init_constants();
6065
6310
  init_anthropic();
6066
6311
  init_azure();
6067
6312
  init_gemini();
6313
+ init_github_models();
6068
6314
  init_ollama();
6069
6315
  init_openai_compatible();
6070
6316
  init_openai();
@@ -6072,6 +6318,7 @@ init_openai();
6072
6318
  // src/core/router/selector.ts
6073
6319
  init_constants();
6074
6320
  init_pricing();
6321
+ init_github_models();
6075
6322
  function normalizeModelId(id) {
6076
6323
  return id.replace(/^models\//, "").toLowerCase();
6077
6324
  }
@@ -6181,16 +6428,23 @@ var ModelSelector = class {
6181
6428
  return model;
6182
6429
  }
6183
6430
  }
6431
+ for (const model of this.availableModels.values()) {
6432
+ if (this.availableProviders.has(model.provider) && model.isVisionCapable) return model;
6433
+ }
6184
6434
  return null;
6185
6435
  }
6186
6436
  getNextFallback(currentModelId, tier) {
6187
6437
  const priority = this.getPriorityList(tier);
6188
6438
  const currentIdx = priority.indexOf(currentModelId);
6189
- if (currentIdx === -1) return null;
6190
- for (let i = currentIdx + 1; i < priority.length; i++) {
6191
- const key = priority[i];
6192
- const model = this.availableModels.get(key);
6193
- if (model && this.isUsable(model)) return model;
6439
+ if (currentIdx !== -1) {
6440
+ for (let i = currentIdx + 1; i < priority.length; i++) {
6441
+ const key = priority[i];
6442
+ const model = this.availableModels.get(key);
6443
+ if (model && this.isUsable(model)) return model;
6444
+ }
6445
+ }
6446
+ for (const [id, model] of this.availableModels) {
6447
+ if (id !== currentModelId && this.isUsable(model)) return model;
6194
6448
  }
6195
6449
  return null;
6196
6450
  }
@@ -6266,7 +6520,7 @@ var ModelSelector = class {
6266
6520
  if (overrideModelId.includes(":")) {
6267
6521
  const parts = overrideModelId.split(":");
6268
6522
  const prefix = parts[0].toLowerCase();
6269
- const validProviders = ["anthropic", "openai", "gemini", "azure", "openai-compatible", "ollama"];
6523
+ const validProviders = ["anthropic", "openai", "gemini", "azure", "openai-compatible", "ollama", "github-models"];
6270
6524
  if (validProviders.includes(prefix)) {
6271
6525
  providerStr = prefix;
6272
6526
  actualId = parts.slice(1).join(":");
@@ -6286,6 +6540,31 @@ var ModelSelector = class {
6286
6540
  else if (this.availableProviders.has("openai-compatible")) providerStr = "openai-compatible";
6287
6541
  else if (this.availableProviders.size === 1) providerStr = Array.from(this.availableProviders)[0];
6288
6542
  }
6543
+ if (providerStr === "github-models" && this.availableProviders.has("github-models")) {
6544
+ const dynamicModel = {
6545
+ id: actualId,
6546
+ name: actualId,
6547
+ provider: "github-models",
6548
+ contextWindow: GITHUB_MODELS_MAX_INPUT_TOKENS,
6549
+ isVisionCapable: false,
6550
+ inputCostPer1kTokens: 0,
6551
+ outputCostPer1kTokens: 0,
6552
+ pricingUnknown: false,
6553
+ maxOutputTokens: GITHUB_MODELS_MAX_OUTPUT_TOKENS,
6554
+ supportsStreaming: true,
6555
+ // Unconfirmed (no live catalog entry to read a real capability from)
6556
+ // must default to false, not left undefined: t3-worker.ts's text-tool
6557
+ // fallback only engages on a strict `=== false`, so an undefined
6558
+ // value here sends native `tools` to an unverified multi-vendor
6559
+ // model and any that don't support function calling reject the
6560
+ // request outright. Same default-to-false-when-unconfirmed policy
6561
+ // listModels() already applies for a real catalog entry.
6562
+ supportsToolUse: false,
6563
+ isLocal: false
6564
+ };
6565
+ this.addDynamicModel(dynamicModel);
6566
+ return dynamicModel;
6567
+ }
6289
6568
  if (providerStr && this.availableProviders.has(providerStr)) {
6290
6569
  const dynamicModel = withResolvedPricing({
6291
6570
  id: actualId,
@@ -6378,7 +6657,20 @@ var DEFAULT_PROVIDER_TPM = {
6378
6657
  gemini: 6e4,
6379
6658
  azure: 3e4,
6380
6659
  "openai-compatible": 3e4,
6381
- ollama: Number.POSITIVE_INFINITY
6660
+ ollama: Number.POSITIVE_INFINITY,
6661
+ // An order of magnitude below the others, on purpose. GitHub Models is
6662
+ // rate-limited by REQUESTS (Free ≈10 RPM, Pro ≈20, plus low daily caps), and
6663
+ // this bucket meters TOKENS — there is no request-count limiter in this
6664
+ // codebase, and building one is not in scope. The approximation works because
6665
+ // the router reserves `model.maxOutputTokens + 512` per call, and the
6666
+ // provider reports GitHub's real ~4K per-request cap rather than the
6667
+ // underlying model's larger one — so every call withdraws a near-constant
6668
+ // ~4.5K regardless of how short the reply actually turns out to be. At 8K/min
6669
+ // that lands around 1–2 calls per minute of headroom: comfortably inside even
6670
+ // the Free tier, with room for the daily cap to matter more than this does.
6671
+ // Users on a higher tier can raise it with no code change through the generic
6672
+ // `config.rateLimits.providerTpm['github-models']` override.
6673
+ "github-models": 8e3
6382
6674
  };
6383
6675
  var TpmLimiter = class {
6384
6676
  buckets = /* @__PURE__ */ new Map();
@@ -6725,6 +7017,7 @@ var ModelProfiler = class {
6725
7017
  (m) => !alreadyProfiled.has(m.id) && !SKIP_PATTERN.test(m.id) && !SKIP_PATTERN.test(m.name)
6726
7018
  );
6727
7019
  if (toProfile.length === 0) return;
7020
+ const probesHitGithubModels = this.router?.getModelForTier("T3")?.provider === "github-models";
6728
7021
  const openRouterModels = await fetchOpenRouterModels();
6729
7022
  const orByNormalizedId = /* @__PURE__ */ new Map();
6730
7023
  for (const m of openRouterModels) {
@@ -6739,7 +7032,7 @@ var ModelProfiler = class {
6739
7032
  if (orMatch?.description) {
6740
7033
  specializations = extractSpecializations(orMatch.description);
6741
7034
  }
6742
- if (specializations.length === 0 && this.router) {
7035
+ if (specializations.length === 0 && this.router && !probesHitGithubModels) {
6743
7036
  specializations = await queryModelDirectly(this.router);
6744
7037
  }
6745
7038
  this.store.saveModelProfile(model.id, model.provider, specializations);
@@ -6964,6 +7257,7 @@ var LiveDataProvider = class {
6964
7257
  */
6965
7258
  applyLivePricing(models) {
6966
7259
  return models.map((m) => {
7260
+ if (m.provider === "github-models") return m;
6967
7261
  const p = this.getLivePrice(m.baseModelId ?? m.id);
6968
7262
  if (!p) return m;
6969
7263
  const reconciled = reconcilePrice(m, p);
@@ -7002,6 +7296,7 @@ var LiveDataProvider = class {
7002
7296
  */
7003
7297
  applyLiveCapabilities(models) {
7004
7298
  return models.map((m) => {
7299
+ if (m.provider === "github-models") return m;
7005
7300
  const c = this.getCapability(m.baseModelId ?? m.id);
7006
7301
  if (!c) return m;
7007
7302
  const next = { ...m };
@@ -7305,13 +7600,16 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
7305
7600
  for (const model of azureDeployments) this.selector.addDynamicModel(model);
7306
7601
  availableProviders.add("azure");
7307
7602
  }
7603
+ if (availableProviders.has("github-models")) {
7604
+ await this.discoverGitHubModelsCatalog(config);
7605
+ }
7308
7606
  await this.validateCloudProviderModels(config);
7309
7607
  for (const tier of ["T1", "T2", "T3"]) {
7310
7608
  const override = tier === "T1" ? config.models.t1 : tier === "T2" ? config.models.t2 : config.models.t3;
7311
7609
  if (!override || override === "auto") continue;
7312
7610
  const model = this.selector.selectForTier(tier, override);
7313
7611
  if (!model) {
7314
- const knownProviders = ["anthropic", "openai", "gemini", "azure", "openai-compatible", "ollama"];
7612
+ const knownProviders = ["anthropic", "openai", "gemini", "azure", "openai-compatible", "ollama", "github-models"];
7315
7613
  const hasProviderPrefix = override.includes(":") && knownProviders.some((p) => override.startsWith(p + ":"));
7316
7614
  if (hasProviderPrefix) {
7317
7615
  const provider = override.split(":")[0];
@@ -7466,7 +7764,7 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
7466
7764
  * models without a package upgrade. Mirrors discoverOllamaModels.
7467
7765
  */
7468
7766
  async discoverProviderModels() {
7469
- const cloud = ["anthropic", "openai", "gemini", "azure", "openai-compatible"];
7767
+ const cloud = ["anthropic", "openai", "gemini", "azure", "openai-compatible", "github-models"];
7470
7768
  const tasks = cloud.map(async (type) => {
7471
7769
  if (!this.selector.isProviderAvailable(type)) return;
7472
7770
  const seed = this.getAnyModelForProvider(type);
@@ -7521,6 +7819,9 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
7521
7819
  this.ensureProvider(options.model, this.config.providers);
7522
7820
  }
7523
7821
  let model = requireVision ? this.selector.selectVisionModel() : options.model ?? this.tierModels.get(tier) ?? this.selector.selectForTier(tier) ?? void 0;
7822
+ if (requireVision && model) {
7823
+ this.ensureProvider(model, this.config.providers);
7824
+ }
7524
7825
  if (options.forceLocal && model && !this.isPrivateModel(model)) {
7525
7826
  const localModel = this.selector.getAllAvailableModels().find((m) => this.isPrivateModel(m));
7526
7827
  if (!localModel) {
@@ -7534,7 +7835,8 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
7534
7835
  if (!model) throw new Error(`No model available for tier ${tier}`);
7535
7836
  const provider = this.getProvider(model);
7536
7837
  if (!provider) throw new Error(`No provider for model ${model.id}`);
7537
- const estimatedTokens = (options.maxTokens ?? model.maxOutputTokens ?? 1024) + 512;
7838
+ const requestedTokens = options.maxTokens ?? model.maxOutputTokens ?? 1024;
7839
+ const estimatedTokens = Math.min(requestedTokens, model.maxOutputTokens ?? requestedTokens) + 512;
7538
7840
  if (this.tpmLimiter) {
7539
7841
  await this.tpmLimiter.acquire(model.provider, estimatedTokens);
7540
7842
  }
@@ -7625,7 +7927,8 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
7625
7927
  });
7626
7928
  releaseLocalSlot?.();
7627
7929
  releaseLocalSlot = void 0;
7628
- return this.generate(tier, options, onChunk, requireVision);
7930
+ const retryOpts = options.model && options.model.id === model.id ? { ...options, model: void 0 } : options;
7931
+ return this.generate(tier, retryOpts, onChunk, requireVision);
7629
7932
  }
7630
7933
  }
7631
7934
  if (isModelNotFoundError(errMsg)) {
@@ -7976,6 +8279,26 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
7976
8279
  return false;
7977
8280
  }
7978
8281
  }
8282
+ /**
8283
+ * Registers GitHub Models' real catalog ids via addDynamicModel so init()'s
8284
+ * tier-fill loop can pick one. Registration alone does not create a bound
8285
+ * BaseProvider instance (see ensureProvider) — the tier-fill loop's own
8286
+ * ensureProvider call does that for whichever model it actually selects,
8287
+ * which is why this runs BEFORE that loop rather than only in the
8288
+ * background discoverProviderModels() refresh.
8289
+ */
8290
+ async discoverGitHubModelsCatalog(config) {
8291
+ const cfg = config.providers.find((p) => p.type === "github-models") ?? { type: "github-models" };
8292
+ const seed = this.getAnyModelForProvider("github-models");
8293
+ if (!seed) return;
8294
+ try {
8295
+ const provider = new GitHubModelsProvider(cfg, seed);
8296
+ const models = await provider.listModels();
8297
+ for (const m of models) this.selector.addDynamicModel(m);
8298
+ } catch (err) {
8299
+ console.warn("[router] GitHub Models catalog discovery failed:", err instanceof Error ? err.message : err);
8300
+ }
8301
+ }
7979
8302
  ensureProvider(model, configs) {
7980
8303
  const key = `${model.provider}:${model.id}`;
7981
8304
  if (this.providers.has(key)) return;
@@ -8000,6 +8323,8 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
8000
8323
  return new OllamaProvider(cfg, model);
8001
8324
  case "openai-compatible":
8002
8325
  return new OpenAICompatibleProvider(cfg, model);
8326
+ case "github-models":
8327
+ return new GitHubModelsProvider(cfg, model);
8003
8328
  default:
8004
8329
  throw new Error(`Unsupported provider type: ${String(cfg.type)}`);
8005
8330
  }
@@ -8007,7 +8332,7 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
8007
8332
  getAnyModelForProvider(type) {
8008
8333
  const fromCatalog = Object.values(MODELS).find((m) => m.provider === type);
8009
8334
  if (fromCatalog) return fromCatalog;
8010
- if (type === "openai-compatible" || type === "azure") {
8335
+ if (type === "openai-compatible" || type === "azure" || type === "github-models") {
8011
8336
  return {
8012
8337
  id: type,
8013
8338
  name: type,
@@ -20097,7 +20422,10 @@ async function refreshModelCache(store, providers) {
20097
20422
  else if (provider.type === "anthropic") instance = new AnthropicProvider(provider, dummyModel);
20098
20423
  else if (provider.type === "ollama") instance = new OllamaProvider(provider, dummyModel);
20099
20424
  else if (provider.type === "openai-compatible") instance = new OpenAICompatibleProvider(provider, dummyModel);
20100
- else if (provider.type === "azure") {
20425
+ else if (provider.type === "github-models") {
20426
+ const { GitHubModelsProvider: GitHubModelsProvider2 } = await Promise.resolve().then(() => (init_github_models(), github_models_exports));
20427
+ instance = new GitHubModelsProvider2(provider, dummyModel);
20428
+ } else if (provider.type === "azure") {
20101
20429
  const { AzureOpenAIProvider: AzureOpenAIProvider2 } = await Promise.resolve().then(() => (init_azure(), azure_exports));
20102
20430
  instance = new AzureOpenAIProvider2(provider, dummyModel);
20103
20431
  }
@@ -21204,7 +21532,7 @@ async function validateConfiguredModels(config) {
21204
21532
  function inferProviderFromModelId(id, providers) {
21205
21533
  if (id.includes(":")) {
21206
21534
  const prefix = id.split(":")[0].toLowerCase();
21207
- const validProviders = ["anthropic", "openai", "gemini", "azure", "openai-compatible", "ollama"];
21535
+ const validProviders = ["anthropic", "openai", "gemini", "azure", "openai-compatible", "ollama", "github-models"];
21208
21536
  if (validProviders.includes(prefix)) {
21209
21537
  return prefix;
21210
21538
  }
@@ -21501,9 +21829,10 @@ var PROVIDER_LABELS = {
21501
21829
  gemini: "Google Gemini",
21502
21830
  azure: "Azure OpenAI",
21503
21831
  ollama: "Ollama (local)",
21504
- "openai-compatible": "OpenAI-Compatible"
21832
+ "openai-compatible": "OpenAI-Compatible",
21833
+ "github-models": "GitHub Models"
21505
21834
  };
21506
- var providerOrder = ["anthropic", "openai", "gemini", "azure", "ollama", "openai-compatible"];
21835
+ var providerOrder = ["anthropic", "openai", "gemini", "azure", "ollama", "openai-compatible", "github-models"];
21507
21836
  function buildInitialEntries(types) {
21508
21837
  return [...types].map((type) => ({
21509
21838
  id: randomUUID(),
@@ -21675,6 +22004,13 @@ function SetupWizard({ workspacePath, onComplete }) {
21675
22004
  const fetched = await p.listModels();
21676
22005
  fetched.forEach((m) => models.push({ id: m.id, name: m.name, providerLabel: entry.label }));
21677
22006
  dispatchRef.current({ type: "SET_FETCH_LOG", line: ` \u2714 ${entry.label} \u2014 ${fetched.length} models` });
22007
+ } else if (type === "github-models") {
22008
+ const { GitHubModelsProvider: GitHubModelsProvider2 } = await Promise.resolve().then(() => (init_github_models(), github_models_exports));
22009
+ const dummyModel = { id: "dummy", name: "dummy", provider: type, contextWindow: 0, isVisionCapable: false, inputCostPer1kTokens: 0, outputCostPer1kTokens: 0, maxOutputTokens: 0, supportsStreaming: false, isLocal: false };
22010
+ const p = new GitHubModelsProvider2({ type, apiKey }, dummyModel);
22011
+ const fetched = await p.listModels();
22012
+ fetched.forEach((m) => models.push({ id: `${type}:${m.id}`, name: m.name, providerLabel: entry.label }));
22013
+ dispatchRef.current({ type: "SET_FETCH_LOG", line: ` \u2714 ${entry.label} \u2014 ${fetched.length} models` });
21678
22014
  } else if (type === "gemini") {
21679
22015
  const { GeminiProvider: GeminiProvider2 } = await Promise.resolve().then(() => (init_gemini(), gemini_exports));
21680
22016
  const dummyModel = { id: "dummy", name: "dummy", provider: type, contextWindow: 0, isVisionCapable: false, inputCostPer1kTokens: 0, outputCostPer1kTokens: 0, maxOutputTokens: 0, supportsStreaming: false, isLocal: false };
@@ -21853,7 +22189,8 @@ function SetupWizard({ workspacePath, onComplete }) {
21853
22189
  /* @__PURE__ */ jsx(Text, { color: focused ? theme.colors.primary : selected ? theme.colors.foreground : theme.colors.muted, children: PROVIDER_LABELS[p] }),
21854
22190
  p === "azure" && /* @__PURE__ */ jsx(Text, { color: theme.colors.muted, dimColor: true, children: " \u2014 multiple deployments supported" }),
21855
22191
  p === "openai-compatible" && /* @__PURE__ */ jsx(Text, { color: theme.colors.muted, dimColor: true, children: " \u2014 Groq, Together, custom" }),
21856
- p === "ollama" && /* @__PURE__ */ jsx(Text, { color: theme.colors.muted, dimColor: true, children: " \u2014 no API key needed" })
22192
+ p === "ollama" && /* @__PURE__ */ jsx(Text, { color: theme.colors.muted, dimColor: true, children: " \u2014 no API key needed" }),
22193
+ p === "github-models" && /* @__PURE__ */ jsx(Text, { color: theme.colors.muted, dimColor: true, children: " \u2014 GitHub PAT with `models: read`" })
21857
22194
  ] }, p);
21858
22195
  }) }),
21859
22196
  /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Text, { color: theme.colors.muted, dimColor: true, children: "space select \xB7 a all \xB7 i invert \xB7 enter continue" }) }),
@@ -22260,7 +22597,8 @@ async function doctorCommand() {
22260
22597
  { type: "anthropic", name: "Anthropic" },
22261
22598
  { type: "openai", name: "OpenAI" },
22262
22599
  { type: "gemini", name: "Google Gemini" },
22263
- { type: "azure", name: "Azure OpenAI" }
22600
+ { type: "azure", name: "Azure OpenAI" },
22601
+ { type: "github-models", name: "GitHub Models" }
22264
22602
  ];
22265
22603
  for (const { type, name } of providers) {
22266
22604
  const key = cm.getApiKey(type);