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.cjs +364 -26
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +364 -26
- package/dist/cli.js.map +1 -1
- package/dist/desktop-core.cjs +331 -20
- package/dist/index.cjs +331 -20
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +33 -3
- package/dist/index.d.ts +33 -3
- package/dist/index.js +329 -21
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -1908,7 +1908,7 @@ function reconcilePrice(model, live) {
|
|
|
1908
1908
|
}
|
|
1909
1909
|
|
|
1910
1910
|
// src/constants.ts
|
|
1911
|
-
var CASCADE_VERSION = "0.
|
|
1911
|
+
var CASCADE_VERSION = "0.65.0";
|
|
1912
1912
|
var CASCADE_CONFIG_DIR = ".cascade";
|
|
1913
1913
|
var CASCADE_MD_FILE = "CASCADE.md";
|
|
1914
1914
|
var CASCADE_IGNORE_FILE = ".cascadeignore";
|
|
@@ -2315,6 +2315,9 @@ var DEFAULT_THEME = "cascade";
|
|
|
2315
2315
|
var OLLAMA_BASE_URL = "http://localhost:11434";
|
|
2316
2316
|
var LM_STUDIO_BASE_URL = "http://localhost:1234";
|
|
2317
2317
|
var AZURE_BASE_URL_TEMPLATE = "https://{resource}.openai.azure.com";
|
|
2318
|
+
var GITHUB_MODELS_INFERENCE_URL = "https://models.github.ai/inference";
|
|
2319
|
+
var GITHUB_MODELS_CATALOG_URL = "https://models.github.ai/catalog/models";
|
|
2320
|
+
var GITHUB_MODELS_API_VERSION = "2026-03-10";
|
|
2318
2321
|
var TOOL_NAMES = {
|
|
2319
2322
|
SHELL: "shell",
|
|
2320
2323
|
FILE_READ: "file_read",
|
|
@@ -2349,7 +2352,8 @@ var PROVIDER_DISPLAY_NAMES = {
|
|
|
2349
2352
|
gemini: "Google Gemini",
|
|
2350
2353
|
azure: "Azure OpenAI",
|
|
2351
2354
|
"openai-compatible": "OpenAI-Compatible",
|
|
2352
|
-
ollama: "Ollama (Local)"
|
|
2355
|
+
ollama: "Ollama (Local)",
|
|
2356
|
+
"github-models": "GitHub Models"
|
|
2353
2357
|
};
|
|
2354
2358
|
|
|
2355
2359
|
// src/utils/cost.ts
|
|
@@ -3326,7 +3330,7 @@ function preferIpv4Host(url) {
|
|
|
3326
3330
|
return url;
|
|
3327
3331
|
}
|
|
3328
3332
|
var MAX_REDIRECTS = 5;
|
|
3329
|
-
async function nodeHttpFetch(input, init = {}, redirectCount = 0) {
|
|
3333
|
+
async function nodeHttpFetch(input, init = {}, redirectCount = 0, opts = {}) {
|
|
3330
3334
|
const urlStr = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
|
3331
3335
|
const u = new URL(preferIpv4Host(urlStr) ?? urlStr);
|
|
3332
3336
|
const lib = u.protocol === "https:" ? https__default.default : http__default.default;
|
|
@@ -3357,9 +3361,13 @@ async function nodeHttpFetch(input, init = {}, redirectCount = 0) {
|
|
|
3357
3361
|
if (status >= 300 && status < 400 && location && redirectCount < MAX_REDIRECTS) {
|
|
3358
3362
|
res.resume();
|
|
3359
3363
|
const nextUrl = new URL(location, u).href;
|
|
3364
|
+
if (opts.allowedRedirectOrigin && new URL(nextUrl).origin !== opts.allowedRedirectOrigin) {
|
|
3365
|
+
reject(new Error(`Refusing cross-origin redirect to ${new URL(nextUrl).origin}`));
|
|
3366
|
+
return;
|
|
3367
|
+
}
|
|
3360
3368
|
const downgrade = status === 303 || (status === 301 || status === 302) && method !== "GET" && method !== "HEAD";
|
|
3361
3369
|
const nextInit = downgrade ? { ...init, method: "GET", body: void 0 } : init;
|
|
3362
|
-
resolve2(nodeHttpFetch(nextUrl, nextInit, redirectCount + 1));
|
|
3370
|
+
resolve2(nodeHttpFetch(nextUrl, nextInit, redirectCount + 1, opts));
|
|
3363
3371
|
return;
|
|
3364
3372
|
}
|
|
3365
3373
|
const encoding = (res.headers["content-encoding"] ?? "").toLowerCase();
|
|
@@ -3388,6 +3396,206 @@ async function nodeHttpFetch(input, init = {}, redirectCount = 0) {
|
|
|
3388
3396
|
});
|
|
3389
3397
|
}
|
|
3390
3398
|
|
|
3399
|
+
// src/providers/github-models.ts
|
|
3400
|
+
var GITHUB_MODELS_MAX_OUTPUT_TOKENS = 4e3;
|
|
3401
|
+
var GITHUB_MODELS_MAX_INPUT_TOKENS = 8e3;
|
|
3402
|
+
var DEFAULT_CONTEXT_WINDOW = 128e3;
|
|
3403
|
+
var GITHUB_MODELS_MAX_CATALOG_PAGES = 20;
|
|
3404
|
+
var GITHUB_MODELS_CATALOG_ORIGIN = new URL(GITHUB_MODELS_CATALOG_URL).origin;
|
|
3405
|
+
function stripModelOwnerPrefix(id) {
|
|
3406
|
+
const slash = (id || "").indexOf("/");
|
|
3407
|
+
return slash === -1 ? id : id.slice(slash + 1);
|
|
3408
|
+
}
|
|
3409
|
+
function pickString(o, keys) {
|
|
3410
|
+
for (const k of keys) {
|
|
3411
|
+
const v = o[k];
|
|
3412
|
+
if (typeof v === "string" && v.length > 0) return v;
|
|
3413
|
+
}
|
|
3414
|
+
return void 0;
|
|
3415
|
+
}
|
|
3416
|
+
function pickNumber(o, keys) {
|
|
3417
|
+
for (const k of keys) {
|
|
3418
|
+
const v = k.includes(".") ? o[k.split(".")[0]]?.[k.split(".")[1]] : o[k];
|
|
3419
|
+
if (typeof v === "number" && Number.isFinite(v) && v > 0) return v;
|
|
3420
|
+
}
|
|
3421
|
+
return void 0;
|
|
3422
|
+
}
|
|
3423
|
+
function looksVisionCapable(o) {
|
|
3424
|
+
for (const k of ["supported_input_modalities", "input_modalities", "modalities", "capabilities"]) {
|
|
3425
|
+
const v = o[k];
|
|
3426
|
+
if (Array.isArray(v) && v.some((x) => typeof x === "string" && /image|vision/i.test(x))) return true;
|
|
3427
|
+
}
|
|
3428
|
+
return false;
|
|
3429
|
+
}
|
|
3430
|
+
function looksToolCapable(o) {
|
|
3431
|
+
const params = o["supported_parameters"];
|
|
3432
|
+
if (Array.isArray(params) && params.some((x) => typeof x === "string" && /^tools?$|function.?call/i.test(x))) {
|
|
3433
|
+
return true;
|
|
3434
|
+
}
|
|
3435
|
+
for (const k of ["capabilities", "supported_output_modalities"]) {
|
|
3436
|
+
const v = o[k];
|
|
3437
|
+
if (Array.isArray(v) && v.some((x) => typeof x === "string" && /tool|function.?call/i.test(x))) return true;
|
|
3438
|
+
}
|
|
3439
|
+
const caps = o["capabilities"];
|
|
3440
|
+
if (caps && typeof caps === "object" && !Array.isArray(caps)) {
|
|
3441
|
+
const c = caps;
|
|
3442
|
+
if (c["function_calling"] === true || c["tool_calling"] === true || c["tools"] === true) return true;
|
|
3443
|
+
}
|
|
3444
|
+
return false;
|
|
3445
|
+
}
|
|
3446
|
+
function parseNextLink(linkHeader) {
|
|
3447
|
+
if (!linkHeader) return null;
|
|
3448
|
+
for (const part of linkHeader.split(",")) {
|
|
3449
|
+
const match = part.match(/<([^>]+)>\s*;\s*rel="next"/);
|
|
3450
|
+
if (match) return match[1];
|
|
3451
|
+
}
|
|
3452
|
+
return null;
|
|
3453
|
+
}
|
|
3454
|
+
var GitHubModelsProvider = class extends OpenAIProvider {
|
|
3455
|
+
constructor(config, model) {
|
|
3456
|
+
super({ ...config, apiKey: config.apiKey ?? "" }, model);
|
|
3457
|
+
this.useMaxCompletionTokens = isReasoningModel(stripModelOwnerPrefix(model.id));
|
|
3458
|
+
this.client = new OpenAI__default.default({
|
|
3459
|
+
apiKey: config.apiKey ?? "",
|
|
3460
|
+
baseURL: GITHUB_MODELS_INFERENCE_URL,
|
|
3461
|
+
defaultHeaders: { "X-GitHub-Api-Version": GITHUB_MODELS_API_VERSION, "User-Agent": "Cascade-AI" }
|
|
3462
|
+
});
|
|
3463
|
+
}
|
|
3464
|
+
/**
|
|
3465
|
+
* `OpenAIProvider.generateStream()` builds its request budget as
|
|
3466
|
+
* `options.maxTokens ?? this.model.maxOutputTokens` — an explicit per-call
|
|
3467
|
+
* `maxTokens` (T1Administrator's final compilation asks for 8,000, well
|
|
3468
|
+
* above GitHub's real ~4K hard cap) wins outright and is sent unclamped.
|
|
3469
|
+
* GitHub then rejects the request instead of answering, so a run routed to
|
|
3470
|
+
* this provider for that step fails outright rather than getting a
|
|
3471
|
+
* (perfectly serviceable, just shorter) completion. Clamp here, once, right
|
|
3472
|
+
* before delegating to the inherited implementation, rather than teaching
|
|
3473
|
+
* every caller GitHub's specific cap.
|
|
3474
|
+
*/
|
|
3475
|
+
async generateStream(options, onChunk) {
|
|
3476
|
+
const clamped = options.maxTokens !== void 0 && options.maxTokens > GITHUB_MODELS_MAX_OUTPUT_TOKENS ? { ...options, maxTokens: GITHUB_MODELS_MAX_OUTPUT_TOKENS } : options;
|
|
3477
|
+
return super.generateStream(clamped, onChunk);
|
|
3478
|
+
}
|
|
3479
|
+
/**
|
|
3480
|
+
* The catalog is a GitHub REST resource, not an OpenAI `/models` list, and
|
|
3481
|
+
* wants GitHub's own header trio — the plain `Accept: application/json` the
|
|
3482
|
+
* openai-compatible provider sends is not the same request.
|
|
3483
|
+
*/
|
|
3484
|
+
catalogHeaders() {
|
|
3485
|
+
return {
|
|
3486
|
+
Accept: "application/vnd.github+json",
|
|
3487
|
+
Authorization: `Bearer ${this.config.apiKey ?? ""}`,
|
|
3488
|
+
"X-GitHub-Api-Version": GITHUB_MODELS_API_VERSION,
|
|
3489
|
+
// GitHub's REST API rejects any request with no User-Agent — a plain 403
|
|
3490
|
+
// regardless of how valid the PAT is. nodeHttpFetch is a thin wrapper
|
|
3491
|
+
// over node:http/https, neither of which sets one the way a browser's
|
|
3492
|
+
// fetch or a full HTTP client library would, so this has to be explicit.
|
|
3493
|
+
"User-Agent": "Cascade-AI"
|
|
3494
|
+
};
|
|
3495
|
+
}
|
|
3496
|
+
async listModels() {
|
|
3497
|
+
const raw = [];
|
|
3498
|
+
let url = GITHUB_MODELS_CATALOG_URL;
|
|
3499
|
+
let pages = 0;
|
|
3500
|
+
const visited = /* @__PURE__ */ new Set();
|
|
3501
|
+
while (url && pages < GITHUB_MODELS_MAX_CATALOG_PAGES) {
|
|
3502
|
+
if (visited.has(url)) break;
|
|
3503
|
+
visited.add(url);
|
|
3504
|
+
const res = await nodeHttpFetch(url, { headers: this.catalogHeaders() }, 0, {
|
|
3505
|
+
allowedRedirectOrigin: GITHUB_MODELS_CATALOG_ORIGIN
|
|
3506
|
+
});
|
|
3507
|
+
if (!res.ok) throw new Error(`GitHub Models catalog ${url} returned HTTP ${res.status}`);
|
|
3508
|
+
const body = await res.json();
|
|
3509
|
+
const pageItems = Array.isArray(body) ? body : Array.isArray(body?.models) ? body.models : Array.isArray(body?.data) ? body.data : [];
|
|
3510
|
+
raw.push(...pageItems);
|
|
3511
|
+
const next = parseNextLink(res.headers.get("link"));
|
|
3512
|
+
if (next) {
|
|
3513
|
+
const resolved = new URL(next, url);
|
|
3514
|
+
if (resolved.origin !== GITHUB_MODELS_CATALOG_ORIGIN) {
|
|
3515
|
+
throw new Error(`Refusing cross-origin GitHub Models pagination URL: ${resolved.origin}`);
|
|
3516
|
+
}
|
|
3517
|
+
url = resolved.href;
|
|
3518
|
+
} else {
|
|
3519
|
+
url = null;
|
|
3520
|
+
}
|
|
3521
|
+
pages++;
|
|
3522
|
+
}
|
|
3523
|
+
const entries = raw.map((m) => {
|
|
3524
|
+
if (typeof m === "string") {
|
|
3525
|
+
return { id: m, name: m, contextWindow: DEFAULT_CONTEXT_WINDOW, vision: false, tools: false };
|
|
3526
|
+
}
|
|
3527
|
+
if (!m || typeof m !== "object") return void 0;
|
|
3528
|
+
const o = m;
|
|
3529
|
+
const id = pickString(o, ["id", "name", "model"]);
|
|
3530
|
+
if (!id) return void 0;
|
|
3531
|
+
return {
|
|
3532
|
+
id,
|
|
3533
|
+
name: pickString(o, ["friendly_name", "display_name", "name"]) ?? id,
|
|
3534
|
+
contextWindow: pickNumber(o, [
|
|
3535
|
+
"context_window",
|
|
3536
|
+
"context_length",
|
|
3537
|
+
"max_input_tokens",
|
|
3538
|
+
"limits.max_input_tokens",
|
|
3539
|
+
"limits.max_context_tokens"
|
|
3540
|
+
]) ?? DEFAULT_CONTEXT_WINDOW,
|
|
3541
|
+
vision: looksVisionCapable(o),
|
|
3542
|
+
tools: looksToolCapable(o)
|
|
3543
|
+
};
|
|
3544
|
+
}).filter((e) => e !== void 0);
|
|
3545
|
+
if (entries.length === 0) return [];
|
|
3546
|
+
const chat = entries.filter((e) => isChatModel(e.id));
|
|
3547
|
+
return (chat.length ? chat : entries).map((e) => ({
|
|
3548
|
+
id: e.id,
|
|
3549
|
+
// full `owner/model` — what the API is addressed by
|
|
3550
|
+
name: e.name,
|
|
3551
|
+
provider: "github-models",
|
|
3552
|
+
// GitHub's real per-request input cap, not the base model's larger one
|
|
3553
|
+
// (see GITHUB_MODELS_MAX_INPUT_TOKENS above) — the same reasoning
|
|
3554
|
+
// already applied to maxOutputTokens below, for the input side.
|
|
3555
|
+
contextWindow: Math.min(e.contextWindow, GITHUB_MODELS_MAX_INPUT_TOKENS),
|
|
3556
|
+
isVisionCapable: e.vision,
|
|
3557
|
+
// A REAL $0, not a missing number — usage is bundled into the quota of
|
|
3558
|
+
// the plan the PAT belongs to and is never billed per token, so
|
|
3559
|
+
// `pricingUnknown: false` is the honest value (see the flag's contract in
|
|
3560
|
+
// core/router/pricing.ts: 0 alone is ambiguous). Set here rather than in
|
|
3561
|
+
// pricing-data.json because resolvePricing() matches exactly on
|
|
3562
|
+
// provider|model|region with no wildcard: every catalog model would need
|
|
3563
|
+
// a hand-curated row, and the next one GitHub adds would read as "cost
|
|
3564
|
+
// unknown" — an inconsistent picture for a billing reality that is
|
|
3565
|
+
// identical across the entire catalog.
|
|
3566
|
+
//
|
|
3567
|
+
// `isLocal` is the OTHER way to reach a real $0, and it is the wrong one:
|
|
3568
|
+
// it means "runs on this machine" and carries consequences that are
|
|
3569
|
+
// simply false for a hosted API — the shared concurrency-1
|
|
3570
|
+
// LocalRequestQueue (serialising these calls behind actual Ollama GPU
|
|
3571
|
+
// inference), the 300s local rather than 120s cloud inference timeout,
|
|
3572
|
+
// and a literal "[local]" tag in `cascade models`.
|
|
3573
|
+
inputCostPer1kTokens: 0,
|
|
3574
|
+
outputCostPer1kTokens: 0,
|
|
3575
|
+
pricingUnknown: false,
|
|
3576
|
+
maxOutputTokens: GITHUB_MODELS_MAX_OUTPUT_TOKENS,
|
|
3577
|
+
supportsStreaming: true,
|
|
3578
|
+
// Not every catalog model supports function/tool calling — a hardcoded
|
|
3579
|
+
// `true` sent `tools` to models that reject the parameter outright and
|
|
3580
|
+
// never engaged t3-worker.ts's existing text-tool fallback (only an
|
|
3581
|
+
// explicit `false` does). See looksToolCapable's own comment for why
|
|
3582
|
+
// "not positively advertised" defaults to false rather than true here.
|
|
3583
|
+
supportsToolUse: e.tools,
|
|
3584
|
+
isLocal: false
|
|
3585
|
+
}));
|
|
3586
|
+
}
|
|
3587
|
+
async isAvailable() {
|
|
3588
|
+
try {
|
|
3589
|
+
const res = await nodeHttpFetch(GITHUB_MODELS_CATALOG_URL, { headers: this.catalogHeaders() }, 0, {
|
|
3590
|
+
allowedRedirectOrigin: GITHUB_MODELS_CATALOG_ORIGIN
|
|
3591
|
+
});
|
|
3592
|
+
return res.ok;
|
|
3593
|
+
} catch {
|
|
3594
|
+
return false;
|
|
3595
|
+
}
|
|
3596
|
+
}
|
|
3597
|
+
};
|
|
3598
|
+
|
|
3391
3599
|
// src/providers/ollama.ts
|
|
3392
3600
|
var TOOL_CAPABLE_FAMILIES = [
|
|
3393
3601
|
"llama3.1",
|
|
@@ -3804,16 +4012,23 @@ var ModelSelector = class {
|
|
|
3804
4012
|
return model;
|
|
3805
4013
|
}
|
|
3806
4014
|
}
|
|
4015
|
+
for (const model of this.availableModels.values()) {
|
|
4016
|
+
if (this.availableProviders.has(model.provider) && model.isVisionCapable) return model;
|
|
4017
|
+
}
|
|
3807
4018
|
return null;
|
|
3808
4019
|
}
|
|
3809
4020
|
getNextFallback(currentModelId, tier) {
|
|
3810
4021
|
const priority = this.getPriorityList(tier);
|
|
3811
4022
|
const currentIdx = priority.indexOf(currentModelId);
|
|
3812
|
-
if (currentIdx
|
|
3813
|
-
|
|
3814
|
-
|
|
3815
|
-
|
|
3816
|
-
|
|
4023
|
+
if (currentIdx !== -1) {
|
|
4024
|
+
for (let i = currentIdx + 1; i < priority.length; i++) {
|
|
4025
|
+
const key = priority[i];
|
|
4026
|
+
const model = this.availableModels.get(key);
|
|
4027
|
+
if (model && this.isUsable(model)) return model;
|
|
4028
|
+
}
|
|
4029
|
+
}
|
|
4030
|
+
for (const [id, model] of this.availableModels) {
|
|
4031
|
+
if (id !== currentModelId && this.isUsable(model)) return model;
|
|
3817
4032
|
}
|
|
3818
4033
|
return null;
|
|
3819
4034
|
}
|
|
@@ -3889,7 +4104,7 @@ var ModelSelector = class {
|
|
|
3889
4104
|
if (overrideModelId.includes(":")) {
|
|
3890
4105
|
const parts = overrideModelId.split(":");
|
|
3891
4106
|
const prefix = parts[0].toLowerCase();
|
|
3892
|
-
const validProviders = ["anthropic", "openai", "gemini", "azure", "openai-compatible", "ollama"];
|
|
4107
|
+
const validProviders = ["anthropic", "openai", "gemini", "azure", "openai-compatible", "ollama", "github-models"];
|
|
3893
4108
|
if (validProviders.includes(prefix)) {
|
|
3894
4109
|
providerStr = prefix;
|
|
3895
4110
|
actualId = parts.slice(1).join(":");
|
|
@@ -3909,6 +4124,31 @@ var ModelSelector = class {
|
|
|
3909
4124
|
else if (this.availableProviders.has("openai-compatible")) providerStr = "openai-compatible";
|
|
3910
4125
|
else if (this.availableProviders.size === 1) providerStr = Array.from(this.availableProviders)[0];
|
|
3911
4126
|
}
|
|
4127
|
+
if (providerStr === "github-models" && this.availableProviders.has("github-models")) {
|
|
4128
|
+
const dynamicModel = {
|
|
4129
|
+
id: actualId,
|
|
4130
|
+
name: actualId,
|
|
4131
|
+
provider: "github-models",
|
|
4132
|
+
contextWindow: GITHUB_MODELS_MAX_INPUT_TOKENS,
|
|
4133
|
+
isVisionCapable: false,
|
|
4134
|
+
inputCostPer1kTokens: 0,
|
|
4135
|
+
outputCostPer1kTokens: 0,
|
|
4136
|
+
pricingUnknown: false,
|
|
4137
|
+
maxOutputTokens: GITHUB_MODELS_MAX_OUTPUT_TOKENS,
|
|
4138
|
+
supportsStreaming: true,
|
|
4139
|
+
// Unconfirmed (no live catalog entry to read a real capability from)
|
|
4140
|
+
// must default to false, not left undefined: t3-worker.ts's text-tool
|
|
4141
|
+
// fallback only engages on a strict `=== false`, so an undefined
|
|
4142
|
+
// value here sends native `tools` to an unverified multi-vendor
|
|
4143
|
+
// model and any that don't support function calling reject the
|
|
4144
|
+
// request outright. Same default-to-false-when-unconfirmed policy
|
|
4145
|
+
// listModels() already applies for a real catalog entry.
|
|
4146
|
+
supportsToolUse: false,
|
|
4147
|
+
isLocal: false
|
|
4148
|
+
};
|
|
4149
|
+
this.addDynamicModel(dynamicModel);
|
|
4150
|
+
return dynamicModel;
|
|
4151
|
+
}
|
|
3912
4152
|
if (providerStr && this.availableProviders.has(providerStr)) {
|
|
3913
4153
|
const dynamicModel = withResolvedPricing({
|
|
3914
4154
|
id: actualId,
|
|
@@ -4001,7 +4241,20 @@ var DEFAULT_PROVIDER_TPM = {
|
|
|
4001
4241
|
gemini: 6e4,
|
|
4002
4242
|
azure: 3e4,
|
|
4003
4243
|
"openai-compatible": 3e4,
|
|
4004
|
-
ollama: Number.POSITIVE_INFINITY
|
|
4244
|
+
ollama: Number.POSITIVE_INFINITY,
|
|
4245
|
+
// An order of magnitude below the others, on purpose. GitHub Models is
|
|
4246
|
+
// rate-limited by REQUESTS (Free ≈10 RPM, Pro ≈20, plus low daily caps), and
|
|
4247
|
+
// this bucket meters TOKENS — there is no request-count limiter in this
|
|
4248
|
+
// codebase, and building one is not in scope. The approximation works because
|
|
4249
|
+
// the router reserves `model.maxOutputTokens + 512` per call, and the
|
|
4250
|
+
// provider reports GitHub's real ~4K per-request cap rather than the
|
|
4251
|
+
// underlying model's larger one — so every call withdraws a near-constant
|
|
4252
|
+
// ~4.5K regardless of how short the reply actually turns out to be. At 8K/min
|
|
4253
|
+
// that lands around 1–2 calls per minute of headroom: comfortably inside even
|
|
4254
|
+
// the Free tier, with room for the daily cap to matter more than this does.
|
|
4255
|
+
// Users on a higher tier can raise it with no code change through the generic
|
|
4256
|
+
// `config.rateLimits.providerTpm['github-models']` override.
|
|
4257
|
+
"github-models": 8e3
|
|
4005
4258
|
};
|
|
4006
4259
|
var TpmLimiter = class {
|
|
4007
4260
|
buckets = /* @__PURE__ */ new Map();
|
|
@@ -4357,6 +4610,7 @@ var ModelProfiler = class {
|
|
|
4357
4610
|
(m) => !alreadyProfiled.has(m.id) && !SKIP_PATTERN.test(m.id) && !SKIP_PATTERN.test(m.name)
|
|
4358
4611
|
);
|
|
4359
4612
|
if (toProfile.length === 0) return;
|
|
4613
|
+
const probesHitGithubModels = this.router?.getModelForTier("T3")?.provider === "github-models";
|
|
4360
4614
|
const openRouterModels = await fetchOpenRouterModels();
|
|
4361
4615
|
const orByNormalizedId = /* @__PURE__ */ new Map();
|
|
4362
4616
|
for (const m of openRouterModels) {
|
|
@@ -4371,7 +4625,7 @@ var ModelProfiler = class {
|
|
|
4371
4625
|
if (orMatch?.description) {
|
|
4372
4626
|
specializations = extractSpecializations(orMatch.description);
|
|
4373
4627
|
}
|
|
4374
|
-
if (specializations.length === 0 && this.router) {
|
|
4628
|
+
if (specializations.length === 0 && this.router && !probesHitGithubModels) {
|
|
4375
4629
|
specializations = await queryModelDirectly(this.router);
|
|
4376
4630
|
}
|
|
4377
4631
|
this.store.saveModelProfile(model.id, model.provider, specializations);
|
|
@@ -4591,6 +4845,7 @@ var LiveDataProvider = class {
|
|
|
4591
4845
|
*/
|
|
4592
4846
|
applyLivePricing(models) {
|
|
4593
4847
|
return models.map((m) => {
|
|
4848
|
+
if (m.provider === "github-models") return m;
|
|
4594
4849
|
const p = this.getLivePrice(m.baseModelId ?? m.id);
|
|
4595
4850
|
if (!p) return m;
|
|
4596
4851
|
const reconciled = reconcilePrice(m, p);
|
|
@@ -4629,6 +4884,7 @@ var LiveDataProvider = class {
|
|
|
4629
4884
|
*/
|
|
4630
4885
|
applyLiveCapabilities(models) {
|
|
4631
4886
|
return models.map((m) => {
|
|
4887
|
+
if (m.provider === "github-models") return m;
|
|
4632
4888
|
const c = this.getCapability(m.baseModelId ?? m.id);
|
|
4633
4889
|
if (!c) return m;
|
|
4634
4890
|
const next = { ...m };
|
|
@@ -4932,13 +5188,16 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
4932
5188
|
for (const model of azureDeployments) this.selector.addDynamicModel(model);
|
|
4933
5189
|
availableProviders.add("azure");
|
|
4934
5190
|
}
|
|
5191
|
+
if (availableProviders.has("github-models")) {
|
|
5192
|
+
await this.discoverGitHubModelsCatalog(config);
|
|
5193
|
+
}
|
|
4935
5194
|
await this.validateCloudProviderModels(config);
|
|
4936
5195
|
for (const tier of ["T1", "T2", "T3"]) {
|
|
4937
5196
|
const override = tier === "T1" ? config.models.t1 : tier === "T2" ? config.models.t2 : config.models.t3;
|
|
4938
5197
|
if (!override || override === "auto") continue;
|
|
4939
5198
|
const model = this.selector.selectForTier(tier, override);
|
|
4940
5199
|
if (!model) {
|
|
4941
|
-
const knownProviders = ["anthropic", "openai", "gemini", "azure", "openai-compatible", "ollama"];
|
|
5200
|
+
const knownProviders = ["anthropic", "openai", "gemini", "azure", "openai-compatible", "ollama", "github-models"];
|
|
4942
5201
|
const hasProviderPrefix = override.includes(":") && knownProviders.some((p) => override.startsWith(p + ":"));
|
|
4943
5202
|
if (hasProviderPrefix) {
|
|
4944
5203
|
const provider = override.split(":")[0];
|
|
@@ -5093,7 +5352,7 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
5093
5352
|
* models without a package upgrade. Mirrors discoverOllamaModels.
|
|
5094
5353
|
*/
|
|
5095
5354
|
async discoverProviderModels() {
|
|
5096
|
-
const cloud = ["anthropic", "openai", "gemini", "azure", "openai-compatible"];
|
|
5355
|
+
const cloud = ["anthropic", "openai", "gemini", "azure", "openai-compatible", "github-models"];
|
|
5097
5356
|
const tasks = cloud.map(async (type) => {
|
|
5098
5357
|
if (!this.selector.isProviderAvailable(type)) return;
|
|
5099
5358
|
const seed = this.getAnyModelForProvider(type);
|
|
@@ -5148,6 +5407,9 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
5148
5407
|
this.ensureProvider(options.model, this.config.providers);
|
|
5149
5408
|
}
|
|
5150
5409
|
let model = requireVision ? this.selector.selectVisionModel() : options.model ?? this.tierModels.get(tier) ?? this.selector.selectForTier(tier) ?? void 0;
|
|
5410
|
+
if (requireVision && model) {
|
|
5411
|
+
this.ensureProvider(model, this.config.providers);
|
|
5412
|
+
}
|
|
5151
5413
|
if (options.forceLocal && model && !this.isPrivateModel(model)) {
|
|
5152
5414
|
const localModel = this.selector.getAllAvailableModels().find((m) => this.isPrivateModel(m));
|
|
5153
5415
|
if (!localModel) {
|
|
@@ -5161,7 +5423,8 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
5161
5423
|
if (!model) throw new Error(`No model available for tier ${tier}`);
|
|
5162
5424
|
const provider = this.getProvider(model);
|
|
5163
5425
|
if (!provider) throw new Error(`No provider for model ${model.id}`);
|
|
5164
|
-
const
|
|
5426
|
+
const requestedTokens = options.maxTokens ?? model.maxOutputTokens ?? 1024;
|
|
5427
|
+
const estimatedTokens = Math.min(requestedTokens, model.maxOutputTokens ?? requestedTokens) + 512;
|
|
5165
5428
|
if (this.tpmLimiter) {
|
|
5166
5429
|
await this.tpmLimiter.acquire(model.provider, estimatedTokens);
|
|
5167
5430
|
}
|
|
@@ -5252,7 +5515,8 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
5252
5515
|
});
|
|
5253
5516
|
releaseLocalSlot?.();
|
|
5254
5517
|
releaseLocalSlot = void 0;
|
|
5255
|
-
|
|
5518
|
+
const retryOpts = options.model && options.model.id === model.id ? { ...options, model: void 0 } : options;
|
|
5519
|
+
return this.generate(tier, retryOpts, onChunk, requireVision);
|
|
5256
5520
|
}
|
|
5257
5521
|
}
|
|
5258
5522
|
if (isModelNotFoundError(errMsg)) {
|
|
@@ -5603,6 +5867,26 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
5603
5867
|
return false;
|
|
5604
5868
|
}
|
|
5605
5869
|
}
|
|
5870
|
+
/**
|
|
5871
|
+
* Registers GitHub Models' real catalog ids via addDynamicModel so init()'s
|
|
5872
|
+
* tier-fill loop can pick one. Registration alone does not create a bound
|
|
5873
|
+
* BaseProvider instance (see ensureProvider) — the tier-fill loop's own
|
|
5874
|
+
* ensureProvider call does that for whichever model it actually selects,
|
|
5875
|
+
* which is why this runs BEFORE that loop rather than only in the
|
|
5876
|
+
* background discoverProviderModels() refresh.
|
|
5877
|
+
*/
|
|
5878
|
+
async discoverGitHubModelsCatalog(config) {
|
|
5879
|
+
const cfg = config.providers.find((p) => p.type === "github-models") ?? { type: "github-models" };
|
|
5880
|
+
const seed = this.getAnyModelForProvider("github-models");
|
|
5881
|
+
if (!seed) return;
|
|
5882
|
+
try {
|
|
5883
|
+
const provider = new GitHubModelsProvider(cfg, seed);
|
|
5884
|
+
const models = await provider.listModels();
|
|
5885
|
+
for (const m of models) this.selector.addDynamicModel(m);
|
|
5886
|
+
} catch (err) {
|
|
5887
|
+
console.warn("[router] GitHub Models catalog discovery failed:", err instanceof Error ? err.message : err);
|
|
5888
|
+
}
|
|
5889
|
+
}
|
|
5606
5890
|
ensureProvider(model, configs) {
|
|
5607
5891
|
const key = `${model.provider}:${model.id}`;
|
|
5608
5892
|
if (this.providers.has(key)) return;
|
|
@@ -5627,6 +5911,8 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
5627
5911
|
return new OllamaProvider(cfg, model);
|
|
5628
5912
|
case "openai-compatible":
|
|
5629
5913
|
return new OpenAICompatibleProvider(cfg, model);
|
|
5914
|
+
case "github-models":
|
|
5915
|
+
return new GitHubModelsProvider(cfg, model);
|
|
5630
5916
|
default:
|
|
5631
5917
|
throw new Error(`Unsupported provider type: ${String(cfg.type)}`);
|
|
5632
5918
|
}
|
|
@@ -5634,7 +5920,7 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
5634
5920
|
getAnyModelForProvider(type) {
|
|
5635
5921
|
const fromCatalog = Object.values(MODELS).find((m) => m.provider === type);
|
|
5636
5922
|
if (fromCatalog) return fromCatalog;
|
|
5637
|
-
if (type === "openai-compatible" || type === "azure") {
|
|
5923
|
+
if (type === "openai-compatible" || type === "azure" || type === "github-models") {
|
|
5638
5924
|
return {
|
|
5639
5925
|
id: type,
|
|
5640
5926
|
name: type,
|
|
@@ -12443,7 +12729,7 @@ var PermissionEscalator = class extends EventEmitter__default.default {
|
|
|
12443
12729
|
}
|
|
12444
12730
|
};
|
|
12445
12731
|
var ProviderConfigSchema = zod.z.object({
|
|
12446
|
-
type: zod.z.enum(["anthropic", "openai", "gemini", "azure", "openai-compatible", "ollama"]),
|
|
12732
|
+
type: zod.z.enum(["anthropic", "openai", "gemini", "azure", "openai-compatible", "ollama", "github-models"]),
|
|
12447
12733
|
label: zod.z.string().optional(),
|
|
12448
12734
|
apiKey: zod.z.string().optional(),
|
|
12449
12735
|
baseUrl: zod.z.string().url().optional(),
|
|
@@ -12623,6 +12909,18 @@ var CascadeConfigSchema = zod.z.object({
|
|
|
12623
12909
|
budget: BudgetConfigSchema.default({}),
|
|
12624
12910
|
theme: zod.z.string().default("cascade"),
|
|
12625
12911
|
workspace: WorkspaceConfigSchema.default({}),
|
|
12912
|
+
/**
|
|
12913
|
+
* Per-provider tokens-per-minute overrides for the router's TpmLimiter (see
|
|
12914
|
+
* DEFAULT_PROVIDER_TPM in core/router/tpm-limiter.ts). Documented as a raw
|
|
12915
|
+
* config field for a while before this — since CascadeConfigSchema strips
|
|
12916
|
+
* unknown keys, a config file setting it was silently discarded and the
|
|
12917
|
+
* override never reached TpmLimiter's constructor. Optional/permissive on
|
|
12918
|
+
* provider name (not a ProviderType enum) so a config still validates
|
|
12919
|
+
* against a slightly newer/older provider list than this schema knows.
|
|
12920
|
+
*/
|
|
12921
|
+
rateLimits: zod.z.object({
|
|
12922
|
+
providerTpm: zod.z.record(zod.z.string(), zod.z.number().positive()).optional()
|
|
12923
|
+
}).optional(),
|
|
12626
12924
|
/**
|
|
12627
12925
|
* Cascade Auto: when true, the TaskAnalyzer selects the optimal model for each
|
|
12628
12926
|
* tier based on task type and complexity, overriding the static priority lists.
|
|
@@ -17657,7 +17955,14 @@ var ConfigManager = class {
|
|
|
17657
17955
|
anthropic: "ANTHROPIC_API_KEY",
|
|
17658
17956
|
openai: "OPENAI_API_KEY",
|
|
17659
17957
|
gemini: "GOOGLE_API_KEY",
|
|
17660
|
-
azure: "AZURE_OPENAI_KEY"
|
|
17958
|
+
azure: "AZURE_OPENAI_KEY",
|
|
17959
|
+
// Deliberately NOT `GITHUB_TOKEN`: that one is injected automatically by
|
|
17960
|
+
// GitHub Actions and is commonly exported in developer shells for `gh`
|
|
17961
|
+
// and git auth, and it will essentially never carry the fine-grained
|
|
17962
|
+
// `models: read` scope this provider needs. Adopting it would wire up a
|
|
17963
|
+
// provider that 403s on its first real call — a confusing failure for
|
|
17964
|
+
// something that is meant to be explicit opt-in BYOK.
|
|
17965
|
+
"github-models": "GITHUB_MODELS_TOKEN"
|
|
17661
17966
|
};
|
|
17662
17967
|
const envKey = envMap[provider];
|
|
17663
17968
|
if (envKey && process.env[envKey]) return process.env[envKey];
|
|
@@ -17686,7 +17991,10 @@ var ConfigManager = class {
|
|
|
17686
17991
|
{ env: "ANTHROPIC_API_KEY", type: "anthropic" },
|
|
17687
17992
|
{ env: "OPENAI_API_KEY", type: "openai" },
|
|
17688
17993
|
{ env: "GOOGLE_API_KEY", type: "gemini" },
|
|
17689
|
-
{ env: "AZURE_OPENAI_KEY", type: "azure" }
|
|
17994
|
+
{ env: "AZURE_OPENAI_KEY", type: "azure" },
|
|
17995
|
+
// See getApiKey() above for why this is GITHUB_MODELS_TOKEN and never
|
|
17996
|
+
// the ambient GITHUB_TOKEN.
|
|
17997
|
+
{ env: "GITHUB_MODELS_TOKEN", type: "github-models" }
|
|
17690
17998
|
];
|
|
17691
17999
|
for (const { env, type } of envProviders) {
|
|
17692
18000
|
const key = process.env[env];
|
|
@@ -20389,6 +20697,9 @@ exports.DEFAULT_RETENTION_DAYS = DEFAULT_RETENTION_DAYS;
|
|
|
20389
20697
|
exports.DEFAULT_THEME = DEFAULT_THEME;
|
|
20390
20698
|
exports.DashboardServer = DashboardServer;
|
|
20391
20699
|
exports.FileMcpOAuthStore = FileMcpOAuthStore;
|
|
20700
|
+
exports.GITHUB_MODELS_API_VERSION = GITHUB_MODELS_API_VERSION;
|
|
20701
|
+
exports.GITHUB_MODELS_CATALOG_URL = GITHUB_MODELS_CATALOG_URL;
|
|
20702
|
+
exports.GITHUB_MODELS_INFERENCE_URL = GITHUB_MODELS_INFERENCE_URL;
|
|
20392
20703
|
exports.GLOBAL_CONFIG_DIR = GLOBAL_CONFIG_DIR;
|
|
20393
20704
|
exports.GLOBAL_CREDENTIALS_FILE = GLOBAL_CREDENTIALS_FILE;
|
|
20394
20705
|
exports.GLOBAL_DB_FILE = GLOBAL_DB_FILE;
|