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.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, GITHUB_MODELS_INFERENCE_URL, GITHUB_MODELS_CATALOG_URL, GITHUB_MODELS_API_VERSION, 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, TOOL_NAMES, DEFAULT_APPROVAL_REQUIRED;
1757
1757
  var init_constants = __esm({
1758
1758
  "src/constants.ts"() {
1759
1759
  init_pricing();
1760
- CASCADE_VERSION = "0.70.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", "github-models"]),
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, loadGlobalCredentials(this.globalDir));
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
- return validateConfig(JSON.parse(raw));
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 isFirstRun = this.config.providers.length === 0;
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 && isFirstRun) {
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 (isFirstRun && !this.config.providers.find((p) => p.type === "ollama")) {
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", "github-models"];
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 && !probesHitGithubModels) {
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", "github-models"];
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", "github-models"];
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" || type === "github-models") {
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 { exec: exec3 } = await import('child_process');
9851
+ const { execFile: execFile3 } = await import('child_process');
9996
9852
  const { promisify: promisify4 } = await import('util');
9997
- const execAsync3 = promisify4(exec3);
9853
+ const execFileAsync2 = promisify4(execFile3);
9998
9854
  for (const artifactPath of artifactPaths) {
9999
- const absolutePath = path34.resolve(process.cwd(), artifactPath);
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
- try {
10036
- if (ext === ".ts" || ext === ".tsx") {
10037
- await execAsync3(`npx tsc --noEmit ${absolutePath}`, { timeout: 1e4 });
10038
- } else if (ext === ".js" || ext === ".jsx") {
10039
- await execAsync3(`node --check ${absolutePath}`, { timeout: 1e4 });
10040
- } else if (ext === ".py") {
10041
- await execAsync3(`python -m py_compile ${absolutePath}`, { timeout: 1e4 });
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(process.cwd(), target);
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 === "github-models") {
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
- const map = /* @__PURE__ */ new Map();
22402
- for (const m of models) {
22403
- const list = map.get(m.provider) ?? [];
22404
- list.push(m);
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", "github-models"];
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", "github-models"];
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
- await cm.updateConfig(applySyncBundle(bundle, cm.getConfig()));
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
- } catch {
24990
- console.log(chalk7.red("\n Could not decrypt \u2014 check your passphrase and try again.\n"));
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 }