cascade-ai 0.70.0 → 0.72.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 +248 -357
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +248 -357
- package/dist/cli.js.map +1 -1
- package/dist/desktop-core.cjs +207 -311
- package/dist/index.cjs +207 -311
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +69 -15
- package/dist/index.d.ts +69 -15
- package/dist/index.js +207 -309
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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,
|
|
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;
|
|
1757
1757
|
var init_constants = __esm({
|
|
1758
1758
|
"src/constants.ts"() {
|
|
1759
1759
|
init_pricing();
|
|
1760
|
-
CASCADE_VERSION = "0.
|
|
1760
|
+
CASCADE_VERSION = "0.72.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,9 +2156,6 @@ 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";
|
|
2162
2159
|
TOOL_NAMES = {
|
|
2163
2160
|
SHELL: "shell",
|
|
2164
2161
|
FILE_READ: "file_read",
|
|
@@ -3316,222 +3313,6 @@ var init_net = __esm({
|
|
|
3316
3313
|
}
|
|
3317
3314
|
});
|
|
3318
3315
|
|
|
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
|
-
|
|
3535
3316
|
// src/providers/ollama.ts
|
|
3536
3317
|
var ollama_exports = {};
|
|
3537
3318
|
__export(ollama_exports, {
|
|
@@ -4873,6 +4654,19 @@ Original error: ${err.message}`
|
|
|
4873
4654
|
const rows = provider ? this.db.prepare("SELECT metadata FROM model_cache WHERE provider = ?").all(provider) : this.db.prepare("SELECT metadata FROM model_cache").all();
|
|
4874
4655
|
return rows.map((r) => JSON.parse(r.metadata));
|
|
4875
4656
|
}
|
|
4657
|
+
/**
|
|
4658
|
+
* Deletes cached rows for a provider type that no longer exists in
|
|
4659
|
+
* `ProviderType`, so it takes a raw string rather than the union.
|
|
4660
|
+
*
|
|
4661
|
+
* Needed because the cache outlives the code: the REPL's loadCache() only
|
|
4662
|
+
* re-discovers when the cache is EMPTY or older than 24h, so rows left
|
|
4663
|
+
* behind by a removed provider read as "cache populated" and suppress
|
|
4664
|
+
* discovery for the providers that replaced it — `/model` then shows a
|
|
4665
|
+
* configured provider with zero models until the rows age out.
|
|
4666
|
+
*/
|
|
4667
|
+
purgeCachedModelsForRetiredProvider(providerType) {
|
|
4668
|
+
return this.db.prepare("DELETE FROM model_cache WHERE provider = ?").run(providerType).changes;
|
|
4669
|
+
}
|
|
4876
4670
|
clearModelCache(provider) {
|
|
4877
4671
|
if (provider) {
|
|
4878
4672
|
this.db.prepare("DELETE FROM model_cache WHERE provider = ?").run(provider);
|
|
@@ -5166,7 +4960,7 @@ Original error: ${err.message}`
|
|
|
5166
4960
|
}
|
|
5167
4961
|
};
|
|
5168
4962
|
var ProviderConfigSchema = z.object({
|
|
5169
|
-
type: z.enum(["anthropic", "openai", "gemini", "azure", "openai-compatible", "ollama"
|
|
4963
|
+
type: z.enum(["anthropic", "openai", "gemini", "azure", "openai-compatible", "ollama"]),
|
|
5170
4964
|
label: z.string().optional(),
|
|
5171
4965
|
apiKey: z.string().optional(),
|
|
5172
4966
|
baseUrl: z.string().url().optional(),
|
|
@@ -5635,6 +5429,67 @@ function mergeGlobalCredentials(workspaceProviders, globalProviders) {
|
|
|
5635
5429
|
}
|
|
5636
5430
|
return merged;
|
|
5637
5431
|
}
|
|
5432
|
+
|
|
5433
|
+
// src/config/retired-providers.ts
|
|
5434
|
+
var RETIRED_PROVIDER_TYPES = {
|
|
5435
|
+
"github-models": "GitHub retired GitHub Models on 30 July 2026; models.github.ai no longer responds."
|
|
5436
|
+
};
|
|
5437
|
+
function isRetiredProviderType(type) {
|
|
5438
|
+
return typeof type === "string" && Object.hasOwn(RETIRED_PROVIDER_TYPES, type);
|
|
5439
|
+
}
|
|
5440
|
+
function didCleanupChangeAnything(c) {
|
|
5441
|
+
return c.removed.length > 0 || c.clearedPins.length > 0;
|
|
5442
|
+
}
|
|
5443
|
+
function stripRetiredProviders(raw) {
|
|
5444
|
+
const cleanup = { removed: [], clearedPins: [] };
|
|
5445
|
+
if (typeof raw !== "object" || raw === null) return cleanup;
|
|
5446
|
+
const cfg = raw;
|
|
5447
|
+
if (Array.isArray(cfg["providers"])) {
|
|
5448
|
+
const kept = cfg["providers"].filter((p) => {
|
|
5449
|
+
const type = p?.type;
|
|
5450
|
+
if (!isRetiredProviderType(type)) return true;
|
|
5451
|
+
if (!cleanup.removed.includes(type)) cleanup.removed.push(type);
|
|
5452
|
+
return false;
|
|
5453
|
+
});
|
|
5454
|
+
cfg["providers"] = kept;
|
|
5455
|
+
}
|
|
5456
|
+
cleanup.clearedPins = clearRetiredPins(cfg["models"]);
|
|
5457
|
+
return cleanup;
|
|
5458
|
+
}
|
|
5459
|
+
function clearRetiredPins(models) {
|
|
5460
|
+
const cleared = [];
|
|
5461
|
+
if (typeof models !== "object" || models === null) return cleared;
|
|
5462
|
+
const tiers = models;
|
|
5463
|
+
for (const tier of ["t1", "t2", "t3"]) {
|
|
5464
|
+
const pin = tiers[tier];
|
|
5465
|
+
if (typeof pin !== "string") continue;
|
|
5466
|
+
const prefix = pin.slice(0, pin.indexOf(":")).toLowerCase();
|
|
5467
|
+
if (pin.includes(":") && isRetiredProviderType(prefix)) {
|
|
5468
|
+
delete tiers[tier];
|
|
5469
|
+
cleared.push(tier);
|
|
5470
|
+
}
|
|
5471
|
+
}
|
|
5472
|
+
return cleared;
|
|
5473
|
+
}
|
|
5474
|
+
function filterRetiredCredentials(providers) {
|
|
5475
|
+
const removed = [];
|
|
5476
|
+
const kept = providers.filter((p) => {
|
|
5477
|
+
if (!isRetiredProviderType(p.type)) return true;
|
|
5478
|
+
if (!removed.includes(p.type)) removed.push(p.type);
|
|
5479
|
+
return false;
|
|
5480
|
+
});
|
|
5481
|
+
return { kept, removed };
|
|
5482
|
+
}
|
|
5483
|
+
function describeCleanup(c) {
|
|
5484
|
+
const parts = [];
|
|
5485
|
+
for (const type of c.removed) {
|
|
5486
|
+
parts.push(`removed the retired "${type}" provider (${RETIRED_PROVIDER_TYPES[type]})`);
|
|
5487
|
+
}
|
|
5488
|
+
if (c.clearedPins.length > 0) {
|
|
5489
|
+
parts.push(`reset ${c.clearedPins.map((t) => t.toUpperCase()).join("/")} to Auto, since the pin named it`);
|
|
5490
|
+
}
|
|
5491
|
+
return `Cascade config migration: ${parts.join("; ")}.`;
|
|
5492
|
+
}
|
|
5638
5493
|
var MCP_TOOL_PREFIX = "mcp__";
|
|
5639
5494
|
var MAX_SEGMENT_LENGTH = 24;
|
|
5640
5495
|
var SEGMENT_HASH_LENGTH = 8;
|
|
@@ -5818,7 +5673,7 @@ init_constants();
|
|
|
5818
5673
|
var KEY_OPTIONAL_PROVIDER_TYPES = /* @__PURE__ */ new Set(["ollama", "openai-compatible"]);
|
|
5819
5674
|
function hasUsableProvider(providers) {
|
|
5820
5675
|
if (!providers?.length) return false;
|
|
5821
|
-
return providers.some((p) => KEY_OPTIONAL_PROVIDER_TYPES.has(p.type) || !!p.apiKey);
|
|
5676
|
+
return providers.some((p) => KEY_OPTIONAL_PROVIDER_TYPES.has(p.type) || !!p.apiKey || !!p.authToken);
|
|
5822
5677
|
}
|
|
5823
5678
|
var ConfigManager = class {
|
|
5824
5679
|
config;
|
|
@@ -5828,6 +5683,29 @@ var ConfigManager = class {
|
|
|
5828
5683
|
cascadeMd = null;
|
|
5829
5684
|
workspacePath;
|
|
5830
5685
|
globalDir;
|
|
5686
|
+
/**
|
|
5687
|
+
* What THIS load migrated out, if anything. Strictly per-load state: reset
|
|
5688
|
+
* at the top of every `load()`, because two places read it as a statement
|
|
5689
|
+
* about the load in progress — `injectEnvKeys()` asks whether an empty
|
|
5690
|
+
* provider list was emptied by a retirement, and the end of `load()` warns
|
|
5691
|
+
* and builds the user-facing notice from it. Left set, a second `load()` on
|
|
5692
|
+
* the same instance (`startRepl()` does one after its setup wizard) would
|
|
5693
|
+
* re-announce a migration that already happened and suppress the Ollama
|
|
5694
|
+
* fallback for a list that is empty for some unrelated reason.
|
|
5695
|
+
*/
|
|
5696
|
+
retiredCleanup;
|
|
5697
|
+
/**
|
|
5698
|
+
* Human-readable migration notice held for a UI to display. `console.warn`
|
|
5699
|
+
* is not sufficient: the REPL clears the TTY immediately after load(), and
|
|
5700
|
+
* the desktop emits from the main process where nothing is rendered.
|
|
5701
|
+
*/
|
|
5702
|
+
pendingRetiredNotice;
|
|
5703
|
+
/** Returns the pending migration notice once, then forgets it. */
|
|
5704
|
+
takeRetiredNotice() {
|
|
5705
|
+
const n = this.pendingRetiredNotice;
|
|
5706
|
+
this.pendingRetiredNotice = void 0;
|
|
5707
|
+
return n;
|
|
5708
|
+
}
|
|
5831
5709
|
/** `globalDirOverride` exists for tests — never point it at the real home dir there. */
|
|
5832
5710
|
constructor(workspacePath = process.cwd(), globalDirOverride) {
|
|
5833
5711
|
this.workspacePath = workspacePath;
|
|
@@ -5847,10 +5725,32 @@ var ConfigManager = class {
|
|
|
5847
5725
|
this.cascadeMd = await loadCascadeMd(this.workspacePath);
|
|
5848
5726
|
this.keystore = new Keystore(path34.join(this.globalDir, GLOBAL_KEYSTORE_FILE));
|
|
5849
5727
|
this.store = new MemoryStore(path34.join(this.workspacePath, CASCADE_DB_FILE));
|
|
5728
|
+
const globalCreds = filterRetiredCredentials(loadGlobalCredentials(this.globalDir));
|
|
5729
|
+
if (globalCreds.removed.length > 0) {
|
|
5730
|
+
try {
|
|
5731
|
+
saveGlobalCredentials(this.globalDir, globalCreds.kept);
|
|
5732
|
+
} catch (err) {
|
|
5733
|
+
console.warn(`Could not rewrite the global credential store: ${err instanceof Error ? err.message : String(err)}`);
|
|
5734
|
+
}
|
|
5735
|
+
this.retiredCleanup = {
|
|
5736
|
+
removed: [.../* @__PURE__ */ new Set([...this.retiredCleanup?.removed ?? [], ...globalCreds.removed])],
|
|
5737
|
+
clearedPins: this.retiredCleanup?.clearedPins ?? []
|
|
5738
|
+
};
|
|
5739
|
+
}
|
|
5850
5740
|
await this.injectEnvKeys();
|
|
5851
|
-
this.config.providers = mergeGlobalCredentials(this.config.providers,
|
|
5741
|
+
this.config.providers = mergeGlobalCredentials(this.config.providers, globalCreds.kept);
|
|
5742
|
+
for (const type of Object.keys(RETIRED_PROVIDER_TYPES)) {
|
|
5743
|
+
try {
|
|
5744
|
+
this.store.purgeCachedModelsForRetiredProvider(type);
|
|
5745
|
+
} catch {
|
|
5746
|
+
}
|
|
5747
|
+
}
|
|
5852
5748
|
await this.ensureDefaultIdentity();
|
|
5853
5749
|
if (mcpNamesChanged) await this.save();
|
|
5750
|
+
if (this.retiredCleanup) {
|
|
5751
|
+
console.warn(describeCleanup(this.retiredCleanup));
|
|
5752
|
+
this.pendingRetiredNotice = describeCleanup(this.retiredCleanup);
|
|
5753
|
+
}
|
|
5854
5754
|
}
|
|
5855
5755
|
getConfig() {
|
|
5856
5756
|
return this.config;
|
|
@@ -5889,14 +5789,13 @@ var ConfigManager = class {
|
|
|
5889
5789
|
anthropic: "ANTHROPIC_API_KEY",
|
|
5890
5790
|
openai: "OPENAI_API_KEY",
|
|
5891
5791
|
gemini: "GOOGLE_API_KEY",
|
|
5892
|
-
azure: "AZURE_OPENAI_KEY"
|
|
5792
|
+
azure: "AZURE_OPENAI_KEY"
|
|
5893
5793
|
// Deliberately NOT `GITHUB_TOKEN`: that one is injected automatically by
|
|
5894
5794
|
// GitHub Actions and is commonly exported in developer shells for `gh`
|
|
5895
5795
|
// and git auth, and it will essentially never carry the fine-grained
|
|
5896
5796
|
// `models: read` scope this provider needs. Adopting it would wire up a
|
|
5897
5797
|
// provider that 403s on its first real call — a confusing failure for
|
|
5898
5798
|
// something that is meant to be explicit opt-in BYOK.
|
|
5899
|
-
"github-models": "GITHUB_MODELS_TOKEN"
|
|
5900
5799
|
};
|
|
5901
5800
|
const envKey = envMap[provider];
|
|
5902
5801
|
if (envKey && process.env[envKey]) return process.env[envKey];
|
|
@@ -5908,10 +5807,21 @@ var ConfigManager = class {
|
|
|
5908
5807
|
return configProvider?.apiKey;
|
|
5909
5808
|
}
|
|
5910
5809
|
async loadConfig() {
|
|
5810
|
+
this.retiredCleanup = void 0;
|
|
5911
5811
|
const configPath = path34.join(this.workspacePath, CASCADE_CONFIG_FILE);
|
|
5912
5812
|
try {
|
|
5913
5813
|
const raw = await fs11.readFile(configPath, "utf-8");
|
|
5914
|
-
|
|
5814
|
+
const parsed = JSON.parse(raw);
|
|
5815
|
+
const cleanup = stripRetiredProviders(parsed);
|
|
5816
|
+
if (didCleanupChangeAnything(cleanup)) {
|
|
5817
|
+
this.retiredCleanup = cleanup;
|
|
5818
|
+
try {
|
|
5819
|
+
await fs11.writeFile(configPath, JSON.stringify(parsed, null, 2), "utf-8");
|
|
5820
|
+
} catch (err) {
|
|
5821
|
+
console.warn(`Could not persist the retired-provider migration: ${err instanceof Error ? err.message : String(err)}`);
|
|
5822
|
+
}
|
|
5823
|
+
}
|
|
5824
|
+
return validateConfig(parsed);
|
|
5915
5825
|
} catch (err) {
|
|
5916
5826
|
if (err.code === "ENOENT") {
|
|
5917
5827
|
return validateConfig({});
|
|
@@ -5920,27 +5830,25 @@ var ConfigManager = class {
|
|
|
5920
5830
|
}
|
|
5921
5831
|
}
|
|
5922
5832
|
async injectEnvKeys() {
|
|
5923
|
-
const
|
|
5833
|
+
const wasEmpty = this.config.providers.length === 0;
|
|
5834
|
+
const emptiedByRetirement = wasEmpty && !!this.retiredCleanup;
|
|
5924
5835
|
const envProviders = [
|
|
5925
5836
|
{ env: "ANTHROPIC_API_KEY", type: "anthropic" },
|
|
5926
5837
|
{ env: "OPENAI_API_KEY", type: "openai" },
|
|
5927
5838
|
{ env: "GOOGLE_API_KEY", type: "gemini" },
|
|
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" }
|
|
5839
|
+
{ env: "AZURE_OPENAI_KEY", type: "azure" }
|
|
5932
5840
|
];
|
|
5933
5841
|
for (const { env, type } of envProviders) {
|
|
5934
5842
|
const key = process.env[env];
|
|
5935
5843
|
if (!key) continue;
|
|
5936
5844
|
const existing = this.config.providers.find((p) => p.type === type);
|
|
5937
|
-
if (!existing &&
|
|
5845
|
+
if (!existing && wasEmpty) {
|
|
5938
5846
|
this.config.providers.push({ type, apiKey: key });
|
|
5939
5847
|
} else if (existing && !existing.apiKey) {
|
|
5940
5848
|
existing.apiKey = key;
|
|
5941
5849
|
}
|
|
5942
5850
|
}
|
|
5943
|
-
if (
|
|
5851
|
+
if (wasEmpty && !emptiedByRetirement && !this.config.providers.find((p) => p.type === "ollama")) {
|
|
5944
5852
|
this.config.providers.push({ type: "ollama" });
|
|
5945
5853
|
}
|
|
5946
5854
|
}
|
|
@@ -6310,7 +6218,6 @@ init_constants();
|
|
|
6310
6218
|
init_anthropic();
|
|
6311
6219
|
init_azure();
|
|
6312
6220
|
init_gemini();
|
|
6313
|
-
init_github_models();
|
|
6314
6221
|
init_ollama();
|
|
6315
6222
|
init_openai_compatible();
|
|
6316
6223
|
init_openai();
|
|
@@ -6318,7 +6225,6 @@ init_openai();
|
|
|
6318
6225
|
// src/core/router/selector.ts
|
|
6319
6226
|
init_constants();
|
|
6320
6227
|
init_pricing();
|
|
6321
|
-
init_github_models();
|
|
6322
6228
|
function normalizeModelId(id) {
|
|
6323
6229
|
return id.replace(/^models\//, "").toLowerCase();
|
|
6324
6230
|
}
|
|
@@ -6520,7 +6426,7 @@ var ModelSelector = class {
|
|
|
6520
6426
|
if (overrideModelId.includes(":")) {
|
|
6521
6427
|
const parts = overrideModelId.split(":");
|
|
6522
6428
|
const prefix = parts[0].toLowerCase();
|
|
6523
|
-
const validProviders = ["anthropic", "openai", "gemini", "azure", "openai-compatible", "ollama"
|
|
6429
|
+
const validProviders = ["anthropic", "openai", "gemini", "azure", "openai-compatible", "ollama"];
|
|
6524
6430
|
if (validProviders.includes(prefix)) {
|
|
6525
6431
|
providerStr = prefix;
|
|
6526
6432
|
actualId = parts.slice(1).join(":");
|
|
@@ -6540,31 +6446,6 @@ var ModelSelector = class {
|
|
|
6540
6446
|
else if (this.availableProviders.has("openai-compatible")) providerStr = "openai-compatible";
|
|
6541
6447
|
else if (this.availableProviders.size === 1) providerStr = Array.from(this.availableProviders)[0];
|
|
6542
6448
|
}
|
|
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
|
-
}
|
|
6568
6449
|
if (providerStr && this.availableProviders.has(providerStr)) {
|
|
6569
6450
|
const dynamicModel = withResolvedPricing({
|
|
6570
6451
|
id: actualId,
|
|
@@ -6657,20 +6538,7 @@ var DEFAULT_PROVIDER_TPM = {
|
|
|
6657
6538
|
gemini: 6e4,
|
|
6658
6539
|
azure: 3e4,
|
|
6659
6540
|
"openai-compatible": 3e4,
|
|
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
|
|
6541
|
+
ollama: Number.POSITIVE_INFINITY
|
|
6674
6542
|
};
|
|
6675
6543
|
var TpmLimiter = class {
|
|
6676
6544
|
buckets = /* @__PURE__ */ new Map();
|
|
@@ -7017,7 +6885,6 @@ var ModelProfiler = class {
|
|
|
7017
6885
|
(m) => !alreadyProfiled.has(m.id) && !SKIP_PATTERN.test(m.id) && !SKIP_PATTERN.test(m.name)
|
|
7018
6886
|
);
|
|
7019
6887
|
if (toProfile.length === 0) return;
|
|
7020
|
-
const probesHitGithubModels = this.router?.getModelForTier("T3")?.provider === "github-models";
|
|
7021
6888
|
const openRouterModels = await fetchOpenRouterModels();
|
|
7022
6889
|
const orByNormalizedId = /* @__PURE__ */ new Map();
|
|
7023
6890
|
for (const m of openRouterModels) {
|
|
@@ -7032,7 +6899,7 @@ var ModelProfiler = class {
|
|
|
7032
6899
|
if (orMatch?.description) {
|
|
7033
6900
|
specializations = extractSpecializations(orMatch.description);
|
|
7034
6901
|
}
|
|
7035
|
-
if (specializations.length === 0 && this.router
|
|
6902
|
+
if (specializations.length === 0 && this.router) {
|
|
7036
6903
|
specializations = await queryModelDirectly(this.router);
|
|
7037
6904
|
}
|
|
7038
6905
|
this.store.saveModelProfile(model.id, model.provider, specializations);
|
|
@@ -7257,7 +7124,6 @@ var LiveDataProvider = class {
|
|
|
7257
7124
|
*/
|
|
7258
7125
|
applyLivePricing(models) {
|
|
7259
7126
|
return models.map((m) => {
|
|
7260
|
-
if (m.provider === "github-models") return m;
|
|
7261
7127
|
const p = this.getLivePrice(m.baseModelId ?? m.id);
|
|
7262
7128
|
if (!p) return m;
|
|
7263
7129
|
const reconciled = reconcilePrice(m, p);
|
|
@@ -7296,7 +7162,6 @@ var LiveDataProvider = class {
|
|
|
7296
7162
|
*/
|
|
7297
7163
|
applyLiveCapabilities(models) {
|
|
7298
7164
|
return models.map((m) => {
|
|
7299
|
-
if (m.provider === "github-models") return m;
|
|
7300
7165
|
const c = this.getCapability(m.baseModelId ?? m.id);
|
|
7301
7166
|
if (!c) return m;
|
|
7302
7167
|
const next = { ...m };
|
|
@@ -7600,16 +7465,13 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
|
|
|
7600
7465
|
for (const model of azureDeployments) this.selector.addDynamicModel(model);
|
|
7601
7466
|
availableProviders.add("azure");
|
|
7602
7467
|
}
|
|
7603
|
-
if (availableProviders.has("github-models")) {
|
|
7604
|
-
await this.discoverGitHubModelsCatalog(config);
|
|
7605
|
-
}
|
|
7606
7468
|
await this.validateCloudProviderModels(config);
|
|
7607
7469
|
for (const tier of ["T1", "T2", "T3"]) {
|
|
7608
7470
|
const override = tier === "T1" ? config.models.t1 : tier === "T2" ? config.models.t2 : config.models.t3;
|
|
7609
7471
|
if (!override || override === "auto") continue;
|
|
7610
7472
|
const model = this.selector.selectForTier(tier, override);
|
|
7611
7473
|
if (!model) {
|
|
7612
|
-
const knownProviders = ["anthropic", "openai", "gemini", "azure", "openai-compatible", "ollama"
|
|
7474
|
+
const knownProviders = ["anthropic", "openai", "gemini", "azure", "openai-compatible", "ollama"];
|
|
7613
7475
|
const hasProviderPrefix = override.includes(":") && knownProviders.some((p) => override.startsWith(p + ":"));
|
|
7614
7476
|
if (hasProviderPrefix) {
|
|
7615
7477
|
const provider = override.split(":")[0];
|
|
@@ -7764,7 +7626,7 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
|
|
|
7764
7626
|
* models without a package upgrade. Mirrors discoverOllamaModels.
|
|
7765
7627
|
*/
|
|
7766
7628
|
async discoverProviderModels() {
|
|
7767
|
-
const cloud = ["anthropic", "openai", "gemini", "azure", "openai-compatible"
|
|
7629
|
+
const cloud = ["anthropic", "openai", "gemini", "azure", "openai-compatible"];
|
|
7768
7630
|
const tasks = cloud.map(async (type) => {
|
|
7769
7631
|
if (!this.selector.isProviderAvailable(type)) return;
|
|
7770
7632
|
const seed = this.getAnyModelForProvider(type);
|
|
@@ -8279,26 +8141,6 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
|
|
|
8279
8141
|
return false;
|
|
8280
8142
|
}
|
|
8281
8143
|
}
|
|
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
|
-
}
|
|
8302
8144
|
ensureProvider(model, configs) {
|
|
8303
8145
|
const key = `${model.provider}:${model.id}`;
|
|
8304
8146
|
if (this.providers.has(key)) return;
|
|
@@ -8323,8 +8165,6 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
|
|
|
8323
8165
|
return new OllamaProvider(cfg, model);
|
|
8324
8166
|
case "openai-compatible":
|
|
8325
8167
|
return new OpenAICompatibleProvider(cfg, model);
|
|
8326
|
-
case "github-models":
|
|
8327
|
-
return new GitHubModelsProvider(cfg, model);
|
|
8328
8168
|
default:
|
|
8329
8169
|
throw new Error(`Unsupported provider type: ${String(cfg.type)}`);
|
|
8330
8170
|
}
|
|
@@ -8332,7 +8172,7 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
|
|
|
8332
8172
|
getAnyModelForProvider(type) {
|
|
8333
8173
|
const fromCatalog = Object.values(MODELS).find((m) => m.provider === type);
|
|
8334
8174
|
if (fromCatalog) return fromCatalog;
|
|
8335
|
-
if (type === "openai-compatible" || type === "azure"
|
|
8175
|
+
if (type === "openai-compatible" || type === "azure") {
|
|
8336
8176
|
return {
|
|
8337
8177
|
id: type,
|
|
8338
8178
|
name: type,
|
|
@@ -9981,6 +9821,22 @@ ${assignment.expectedOutput}`;
|
|
|
9981
9821
|
requiresArtifact() {
|
|
9982
9822
|
return shouldRequireArtifact(this.assignment, this.tools.map((t) => t.name));
|
|
9983
9823
|
}
|
|
9824
|
+
/**
|
|
9825
|
+
* Where a file a tool wrote actually lives.
|
|
9826
|
+
*
|
|
9827
|
+
* This used to be `process.cwd()`, which is only the workspace in a plain
|
|
9828
|
+
* CLI run. The hosted server runs with cwd at the app directory while the
|
|
9829
|
+
* run's workspace is the tenant's scratch dir, and Electron's cwd is the app
|
|
9830
|
+
* bundle rather than the folder the user picked. So the tool wrote
|
|
9831
|
+
* `<workspace>/report.docx`, verification stat'd `<cwd>/report.docx`, found
|
|
9832
|
+
* nothing, and the worker looped and then threw WorkerStallError — surfacing
|
|
9833
|
+
* as "no file was created" on a file that had in fact been created, followed
|
|
9834
|
+
* by a failed node. Asking the registry is exact: it is the same root every
|
|
9835
|
+
* registered tool was configured with (registry.registerDefaults).
|
|
9836
|
+
*/
|
|
9837
|
+
artifactRoot() {
|
|
9838
|
+
return this.toolRegistry.getWorkspaceRoot?.() ?? process.cwd();
|
|
9839
|
+
}
|
|
9984
9840
|
extractArtifactPaths(assignment) {
|
|
9985
9841
|
const declared = (assignment.files ?? []).map((f) => f.trim()).filter((f) => f.includes("."));
|
|
9986
9842
|
const haystack = `${assignment.description}
|
|
@@ -9992,11 +9848,11 @@ ${assignment.expectedOutput}`;
|
|
|
9992
9848
|
const artifactPaths = this.extractArtifactPaths(assignment);
|
|
9993
9849
|
if (!artifactPaths.length) return { ok: true, issues: [] };
|
|
9994
9850
|
const issues = [];
|
|
9995
|
-
const {
|
|
9851
|
+
const { execFile: execFile3 } = await import('child_process');
|
|
9996
9852
|
const { promisify: promisify4 } = await import('util');
|
|
9997
|
-
const
|
|
9853
|
+
const execFileAsync2 = promisify4(execFile3);
|
|
9998
9854
|
for (const artifactPath of artifactPaths) {
|
|
9999
|
-
const absolutePath = path34.resolve(
|
|
9855
|
+
const absolutePath = path34.resolve(this.artifactRoot(), artifactPath);
|
|
10000
9856
|
try {
|
|
10001
9857
|
const stat = await fs11.stat(absolutePath);
|
|
10002
9858
|
if (!stat.isFile()) {
|
|
@@ -10032,20 +9888,18 @@ ${assignment.expectedOutput}`;
|
|
|
10032
9888
|
continue;
|
|
10033
9889
|
}
|
|
10034
9890
|
const ext = path34.extname(absolutePath).toLowerCase();
|
|
10035
|
-
|
|
10036
|
-
|
|
10037
|
-
|
|
10038
|
-
|
|
10039
|
-
|
|
10040
|
-
|
|
10041
|
-
|
|
9891
|
+
const check = ext === ".ts" || ext === ".tsx" ? { cmd: "npx", args: ["tsc", "--noEmit", absolutePath] } : ext === ".js" || ext === ".jsx" ? { cmd: process.execPath, args: ["--check", absolutePath] } : ext === ".py" ? { cmd: "python", args: ["-m", "py_compile", absolutePath] } : null;
|
|
9892
|
+
if (check) {
|
|
9893
|
+
try {
|
|
9894
|
+
await execFileAsync2(check.cmd, check.args, { timeout: 1e4 });
|
|
9895
|
+
} catch (err) {
|
|
9896
|
+
const code = err.code;
|
|
9897
|
+
if (code === "ENOENT" || code === "EINVAL") continue;
|
|
9898
|
+
const e = err;
|
|
9899
|
+
issues.push(`Semantic error in ${artifactPath}:
|
|
9900
|
+
${e.stderr || String(err)}
|
|
9901
|
+
${e.stdout || ""}`);
|
|
10042
9902
|
}
|
|
10043
|
-
} catch (err) {
|
|
10044
|
-
const stderr = err?.stderr || String(err);
|
|
10045
|
-
const stdout = err?.stdout || "";
|
|
10046
|
-
issues.push(`Semantic error in ${artifactPath}:
|
|
10047
|
-
${stderr}
|
|
10048
|
-
${stdout}`);
|
|
10049
9903
|
}
|
|
10050
9904
|
} catch {
|
|
10051
9905
|
issues.push(`Required artifact was not created: ${artifactPath}`);
|
|
@@ -10124,7 +9978,7 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
10124
9978
|
async checkAcceptance(assignment) {
|
|
10125
9979
|
const criteria = assignment.acceptance ?? [];
|
|
10126
9980
|
if (!criteria.length) return [];
|
|
10127
|
-
const resolve2 = (target) => path34.resolve(
|
|
9981
|
+
const resolve2 = (target) => path34.resolve(this.artifactRoot(), target);
|
|
10128
9982
|
return evaluateAcceptance(criteria, assignment.files ?? [], {
|
|
10129
9983
|
stat: async (target) => {
|
|
10130
9984
|
try {
|
|
@@ -15540,6 +15394,18 @@ var ToolRegistry = class extends EventEmitter {
|
|
|
15540
15394
|
this.tools.set(tool.name, tool);
|
|
15541
15395
|
this.emit("tool:added", tool.name);
|
|
15542
15396
|
}
|
|
15397
|
+
/**
|
|
15398
|
+
* The directory every registered tool resolves relative paths against.
|
|
15399
|
+
*
|
|
15400
|
+
* Exposed so a caller that has to reason about a file a tool WROTE can look
|
|
15401
|
+
* in the same place the tool wrote it. `process.cwd()` is not that place
|
|
15402
|
+
* outside a plain CLI run: the hosted server's cwd is the app directory
|
|
15403
|
+
* while the run's workspace is the tenant's scratch dir, and Electron's cwd
|
|
15404
|
+
* is the app bundle rather than the user's chosen folder.
|
|
15405
|
+
*/
|
|
15406
|
+
getWorkspaceRoot() {
|
|
15407
|
+
return this.workspaceRoot;
|
|
15408
|
+
}
|
|
15543
15409
|
/**
|
|
15544
15410
|
* Wait until a named tool is registered, resolving immediately if it already exists.
|
|
15545
15411
|
* T3 workers can call this after encountering a missing-tool error to resume
|
|
@@ -22210,10 +22076,7 @@ async function refreshModelCache(store, providers) {
|
|
|
22210
22076
|
else if (provider.type === "anthropic") instance = new AnthropicProvider(provider, dummyModel);
|
|
22211
22077
|
else if (provider.type === "ollama") instance = new OllamaProvider(provider, dummyModel);
|
|
22212
22078
|
else if (provider.type === "openai-compatible") instance = new OpenAICompatibleProvider(provider, dummyModel);
|
|
22213
|
-
else if (provider.type === "
|
|
22214
|
-
const { GitHubModelsProvider: GitHubModelsProvider2 } = await Promise.resolve().then(() => (init_github_models(), github_models_exports));
|
|
22215
|
-
instance = new GitHubModelsProvider2(provider, dummyModel);
|
|
22216
|
-
} else if (provider.type === "azure") {
|
|
22079
|
+
else if (provider.type === "azure") {
|
|
22217
22080
|
const { AzureOpenAIProvider: AzureOpenAIProvider2 } = await Promise.resolve().then(() => (init_azure(), azure_exports));
|
|
22218
22081
|
instance = new AzureOpenAIProvider2(provider, dummyModel);
|
|
22219
22082
|
}
|
|
@@ -22226,7 +22089,7 @@ async function refreshModelCache(store, providers) {
|
|
|
22226
22089
|
}
|
|
22227
22090
|
}
|
|
22228
22091
|
}
|
|
22229
|
-
function Repl({ config, workspacePath, themeName, initialPrompt, identityName, altScreen = false }) {
|
|
22092
|
+
function Repl({ config, workspacePath, themeName, initialPrompt, identityName, startupNotice, altScreen = false }) {
|
|
22230
22093
|
const { exit } = useApp();
|
|
22231
22094
|
const { stdout } = useStdout();
|
|
22232
22095
|
const [theme, setTheme] = useState(() => getTheme(themeName));
|
|
@@ -22397,17 +22260,29 @@ function Repl({ config, workspacePath, themeName, initialPrompt, identityName, a
|
|
|
22397
22260
|
}
|
|
22398
22261
|
setCurrentIdentityId(initialIdentityId);
|
|
22399
22262
|
const loadCache = async () => {
|
|
22263
|
+
const toMap = (models2) => {
|
|
22264
|
+
const map = /* @__PURE__ */ new Map();
|
|
22265
|
+
for (const m of models2) {
|
|
22266
|
+
const list = map.get(m.provider) ?? [];
|
|
22267
|
+
list.push(m);
|
|
22268
|
+
map.set(m.provider, list);
|
|
22269
|
+
}
|
|
22270
|
+
return map;
|
|
22271
|
+
};
|
|
22400
22272
|
const models = store.getCachedModels();
|
|
22401
|
-
|
|
22402
|
-
|
|
22403
|
-
|
|
22404
|
-
|
|
22405
|
-
map.set(m.provider, list);
|
|
22273
|
+
setCachedModels(toMap(models));
|
|
22274
|
+
if (models.length === 0 || store.getCacheAge() > 24 * 60 * 60 * 1e3) {
|
|
22275
|
+
await refreshModelCache(store, config.providers);
|
|
22276
|
+
setCachedModels(toMap(store.getCachedModels()));
|
|
22406
22277
|
}
|
|
22407
|
-
setCachedModels(map);
|
|
22408
|
-
if (models.length === 0 || store.getCacheAge() > 24 * 60 * 60 * 1e3) await refreshModelCache(store, config.providers);
|
|
22409
22278
|
};
|
|
22410
22279
|
loadCache();
|
|
22280
|
+
if (startupNotice) {
|
|
22281
|
+
dispatch({
|
|
22282
|
+
type: "ADD_MESSAGE",
|
|
22283
|
+
message: { id: randomUUID(), role: "system", content: `\u26A0 ${startupNotice}`, timestamp: (/* @__PURE__ */ new Date()).toISOString() }
|
|
22284
|
+
});
|
|
22285
|
+
}
|
|
22411
22286
|
store.createSession({ id: sessionIdRef.current, title: "Cascade Session", createdAt: startedAtRef.current, updatedAt: startedAtRef.current, identityId: config.defaultIdentityId ?? "default", workspacePath, messages: [], metadata: { totalTokens: 0, totalCostUsd: 0, modelsUsed: [], toolsUsed: [], taskCount: 0 } });
|
|
22412
22287
|
persistRuntimeSession("ACTIVE");
|
|
22413
22288
|
try {
|
|
@@ -23330,7 +23205,7 @@ async function validateConfiguredModels(config) {
|
|
|
23330
23205
|
function inferProviderFromModelId(id, providers) {
|
|
23331
23206
|
if (id.includes(":")) {
|
|
23332
23207
|
const prefix = id.split(":")[0].toLowerCase();
|
|
23333
|
-
const validProviders = ["anthropic", "openai", "gemini", "azure", "openai-compatible", "ollama"
|
|
23208
|
+
const validProviders = ["anthropic", "openai", "gemini", "azure", "openai-compatible", "ollama"];
|
|
23334
23209
|
if (validProviders.includes(prefix)) {
|
|
23335
23210
|
return prefix;
|
|
23336
23211
|
}
|
|
@@ -23627,10 +23502,9 @@ var PROVIDER_LABELS = {
|
|
|
23627
23502
|
gemini: "Google Gemini",
|
|
23628
23503
|
azure: "Azure OpenAI",
|
|
23629
23504
|
ollama: "Ollama (local)",
|
|
23630
|
-
"openai-compatible": "OpenAI-Compatible"
|
|
23631
|
-
"github-models": "GitHub Models"
|
|
23505
|
+
"openai-compatible": "OpenAI-Compatible"
|
|
23632
23506
|
};
|
|
23633
|
-
var providerOrder = ["anthropic", "openai", "gemini", "azure", "ollama", "openai-compatible"
|
|
23507
|
+
var providerOrder = ["anthropic", "openai", "gemini", "azure", "ollama", "openai-compatible"];
|
|
23634
23508
|
function buildInitialEntries(types) {
|
|
23635
23509
|
return [...types].map((type) => ({
|
|
23636
23510
|
id: randomUUID(),
|
|
@@ -23802,13 +23676,6 @@ function SetupWizard({ workspacePath, onComplete }) {
|
|
|
23802
23676
|
const fetched = await p.listModels();
|
|
23803
23677
|
fetched.forEach((m) => models.push({ id: m.id, name: m.name, providerLabel: entry.label }));
|
|
23804
23678
|
dispatchRef.current({ type: "SET_FETCH_LOG", line: ` \u2714 ${entry.label} \u2014 ${fetched.length} models` });
|
|
23805
|
-
} else if (type === "github-models") {
|
|
23806
|
-
const { GitHubModelsProvider: GitHubModelsProvider2 } = await Promise.resolve().then(() => (init_github_models(), github_models_exports));
|
|
23807
|
-
const dummyModel = { id: "dummy", name: "dummy", provider: type, contextWindow: 0, isVisionCapable: false, inputCostPer1kTokens: 0, outputCostPer1kTokens: 0, maxOutputTokens: 0, supportsStreaming: false, isLocal: false };
|
|
23808
|
-
const p = new GitHubModelsProvider2({ type, apiKey }, dummyModel);
|
|
23809
|
-
const fetched = await p.listModels();
|
|
23810
|
-
fetched.forEach((m) => models.push({ id: `${type}:${m.id}`, name: m.name, providerLabel: entry.label }));
|
|
23811
|
-
dispatchRef.current({ type: "SET_FETCH_LOG", line: ` \u2714 ${entry.label} \u2014 ${fetched.length} models` });
|
|
23812
23679
|
} else if (type === "gemini") {
|
|
23813
23680
|
const { GeminiProvider: GeminiProvider2 } = await Promise.resolve().then(() => (init_gemini(), gemini_exports));
|
|
23814
23681
|
const dummyModel = { id: "dummy", name: "dummy", provider: type, contextWindow: 0, isVisionCapable: false, inputCostPer1kTokens: 0, outputCostPer1kTokens: 0, maxOutputTokens: 0, supportsStreaming: false, isLocal: false };
|
|
@@ -23987,8 +23854,7 @@ function SetupWizard({ workspacePath, onComplete }) {
|
|
|
23987
23854
|
/* @__PURE__ */ jsx(Text, { color: focused ? theme.colors.primary : selected ? theme.colors.foreground : theme.colors.muted, children: PROVIDER_LABELS[p] }),
|
|
23988
23855
|
p === "azure" && /* @__PURE__ */ jsx(Text, { color: theme.colors.muted, dimColor: true, children: " \u2014 multiple deployments supported" }),
|
|
23989
23856
|
p === "openai-compatible" && /* @__PURE__ */ jsx(Text, { color: theme.colors.muted, dimColor: true, children: " \u2014 Groq, Together, custom" }),
|
|
23990
|
-
p === "ollama" && /* @__PURE__ */ jsx(Text, { color: theme.colors.muted, dimColor: true, children: " \u2014 no API key needed" })
|
|
23991
|
-
p === "github-models" && /* @__PURE__ */ jsx(Text, { color: theme.colors.muted, dimColor: true, children: " \u2014 GitHub PAT with `models: read`" })
|
|
23857
|
+
p === "ollama" && /* @__PURE__ */ jsx(Text, { color: theme.colors.muted, dimColor: true, children: " \u2014 no API key needed" })
|
|
23992
23858
|
] }, p);
|
|
23993
23859
|
}) }),
|
|
23994
23860
|
/* @__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" }) }),
|
|
@@ -24395,8 +24261,7 @@ async function doctorCommand() {
|
|
|
24395
24261
|
{ type: "anthropic", name: "Anthropic" },
|
|
24396
24262
|
{ type: "openai", name: "OpenAI" },
|
|
24397
24263
|
{ type: "gemini", name: "Google Gemini" },
|
|
24398
|
-
{ type: "azure", name: "Azure OpenAI" }
|
|
24399
|
-
{ type: "github-models", name: "GitHub Models" }
|
|
24264
|
+
{ type: "azure", name: "Azure OpenAI" }
|
|
24400
24265
|
];
|
|
24401
24266
|
for (const { type, name } of providers) {
|
|
24402
24267
|
const key = cm.getApiKey(type);
|
|
@@ -24657,7 +24522,9 @@ function gatherSyncBundle(config) {
|
|
|
24657
24522
|
if (Object.keys(prefs).length) bundle.prefs = prefs;
|
|
24658
24523
|
return bundle;
|
|
24659
24524
|
}
|
|
24660
|
-
function applySyncBundle(bundle, config) {
|
|
24525
|
+
function applySyncBundle(bundle, config, cleanup) {
|
|
24526
|
+
const dropped = stripRetiredProviders(bundle);
|
|
24527
|
+
if (cleanup) cleanup.removed = dropped.removed;
|
|
24661
24528
|
const next = { ...config, tools: { ...config.tools } };
|
|
24662
24529
|
if (bundle.providers) next.providers = mergeByKey(config.providers, bundle.providers, providerSig) ?? [];
|
|
24663
24530
|
if (bundle.webSearch) next.tools.webSearch = { ...config.tools?.webSearch, ...bundle.webSearch };
|
|
@@ -24673,6 +24540,14 @@ function applySyncBundle(bundle, config) {
|
|
|
24673
24540
|
}
|
|
24674
24541
|
if (bundle.models) next.models = { ...config.models, ...bundle.models };
|
|
24675
24542
|
if (bundle.budget) next.budget = { ...config.budget, ...bundle.budget };
|
|
24543
|
+
const merged = { ...next.models };
|
|
24544
|
+
const clearedFromMerge = clearRetiredPins(merged);
|
|
24545
|
+
if (clearedFromMerge.length > 0) next.models = merged;
|
|
24546
|
+
if (cleanup) {
|
|
24547
|
+
cleanup.clearedPins = ["t1", "t2", "t3"].filter(
|
|
24548
|
+
(t) => (dropped.clearedPins.includes(t) || clearedFromMerge.includes(t)) && next.models?.[t] === void 0
|
|
24549
|
+
);
|
|
24550
|
+
}
|
|
24676
24551
|
if (bundle.prefs) {
|
|
24677
24552
|
const p = bundle.prefs;
|
|
24678
24553
|
if (p.autoBias !== void 0) next.autoBias = p.autoBias;
|
|
@@ -24980,14 +24855,28 @@ async function syncPullCommand() {
|
|
|
24980
24855
|
console.log(chalk7.dim("\n Cancelled.\n"));
|
|
24981
24856
|
return;
|
|
24982
24857
|
}
|
|
24858
|
+
let bundle;
|
|
24859
|
+
try {
|
|
24860
|
+
bundle = await decryptJSON(blob, pass);
|
|
24861
|
+
} catch {
|
|
24862
|
+
console.log(chalk7.red("\n Could not decrypt \u2014 check your passphrase and try again.\n"));
|
|
24863
|
+
process.exitCode = 1;
|
|
24864
|
+
return;
|
|
24865
|
+
}
|
|
24983
24866
|
try {
|
|
24984
|
-
const bundle = await decryptJSON(blob, pass);
|
|
24985
24867
|
const cm = new ConfigManager(process.cwd());
|
|
24986
24868
|
await cm.load();
|
|
24987
|
-
|
|
24869
|
+
const cleanup = { removed: [], clearedPins: [] };
|
|
24870
|
+
await cm.updateConfig(applySyncBundle(bundle, cm.getConfig(), cleanup));
|
|
24988
24871
|
console.log(chalk7.green("\n \u2713 Applied your synced settings. Your keys are ready here.\n"));
|
|
24989
|
-
|
|
24990
|
-
|
|
24872
|
+
if (didCleanupChangeAnything(cleanup)) {
|
|
24873
|
+
console.log(chalk7.yellow(` ${describeCleanup(cleanup)}
|
|
24874
|
+
`));
|
|
24875
|
+
}
|
|
24876
|
+
} catch (err) {
|
|
24877
|
+
console.log(chalk7.red(`
|
|
24878
|
+
Could not apply your synced settings: ${err instanceof Error ? err.message : String(err)}
|
|
24879
|
+
`));
|
|
24991
24880
|
process.exitCode = 1;
|
|
24992
24881
|
}
|
|
24993
24882
|
}
|
|
@@ -27614,6 +27503,7 @@ async function startRepl(options) {
|
|
|
27614
27503
|
process.exit(1);
|
|
27615
27504
|
}
|
|
27616
27505
|
let config = cm.getConfig();
|
|
27506
|
+
const startupNotice = cm.takeRetiredNotice();
|
|
27617
27507
|
const needsSetup = !hasUsableProvider(config.providers);
|
|
27618
27508
|
if (needsSetup) {
|
|
27619
27509
|
console.log(chalk7.magenta(" \u25C8 No providers configured \u2014 launching setup wizard\u2026"));
|
|
@@ -27636,6 +27526,7 @@ async function startRepl(options) {
|
|
|
27636
27526
|
themeName: options.theme ?? config.theme ?? DEFAULT_THEME,
|
|
27637
27527
|
initialPrompt: options.prompt,
|
|
27638
27528
|
identityName: options.identity,
|
|
27529
|
+
startupNotice,
|
|
27639
27530
|
altScreen: useAltScreen
|
|
27640
27531
|
}),
|
|
27641
27532
|
{ exitOnCtrlC: false }
|