jeo-code 0.12.2 → 0.12.4

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/CHANGELOG.md CHANGED
@@ -9,6 +9,32 @@ The README mirrors the latest 5 entries — regenerate with `bun run changelog:s
9
9
 
10
10
  ## [Unreleased]
11
11
 
12
+ ## [0.12.4] - 2026-10-17
13
+ _Refresh Antigravity provider (`antigravity/*`) account catalogs with `antigravity/hub/2.23.1` so entitled Gemini 4 Argon and Gemini 3.8 Flash tiers appear directly in `/model provider antigravity`._
14
+
15
+ ### Fixed
16
+ - **Antigravity provider client identity & Argon catalog visibility** (`src/ai/providers/antigravity.ts`, `src/ai/model-discovery.ts`) — updated `getAntigravityUserAgent()` from `antigravity/1.104.0` to `antigravity/hub/2.23.1` (`jeopi` v16.6.3 parity) so `daily-cloudcode-pa.googleapis.com/v1internal:fetchAvailableModels` returns entitled `gemini-4-argon-{low,medium,high}` and `gemini-3.8-flash-*` tiers. Preserved named non-recommended chat models (`displayName`) alongside `agentModelSorts`/`tieredModelIds`/`defaultAgentModelId` while excluding explicit non-chat (`tabModelIds`, `imageGenerationModelIds`, `audioTranscriptionModelIds`, `deprecatedModelIds`), internal, and unnamed rows.
17
+ - **Client-UA-scoped Antigravity model cache invalidation** (`src/ai/model-cache.ts`) — included `getAntigravityUserAgent()` in `antigravityAccountScope` so client User-Agent updates immediately invalidate stale cached model lists without waiting for the 6-hour TTL.
18
+ - **In-name `-medium` thinking depth recognition** (`src/ai/providers/gemini.ts`, `src/ai/model-catalog.ts`) — `geminiNamedEffort` and `inferCatalogMetadata` now recognize `-medium` model suffixes (`gemini-4-argon-medium`, `gemini-3.8-flash-medium`) so unset `reasoningEffort` defaults to `medium` thinking depth.
19
+
20
+ ### Verified
21
+ - Focused Antigravity/Gemini/discovery/catalog regression suites (`175 pass, 0 fail`), `bun run typecheck`, `bun run build`, `bun run pack:check`, and live `antigravity/gemini-4-argon-{low,medium,high}` discovery + `antigravity/gemini-4-argon-medium` inference (`HTTP 200`).
22
+
23
+ ## [0.12.3] - 2026-10-17
24
+ _Anthropic static model catalog trimmed to claude-5.x with live API discovery, and `/route off` strictly fixes the active model with no failover._
25
+
26
+ ### Changed
27
+ - **Anthropic claude-4.x entries removed from the static catalog** (`src/ai/model-catalog.ts`) — `claude-opus-4-8`, `claude-opus-4-7`, `claude-sonnet-4-6`, `claude-opus-4-6` dropped. Static catalog is now a fallback-only claude-5.x set (`sonnet-5-5`, `opus-5-5`, `sonnet-5`, `fable-5`, `mythos-5`).
28
+ - **`sonnet` / `opus` built-in aliases updated to claude-5.5** (`src/ai/model-registry.ts`) — `sonnet` → `claude-sonnet-5-5`, `opus` → `claude-opus-5-5`.
29
+ - **Live `https://api.anthropic.com/v1/models` is the primary model source** — when logged in, the API returns the account's actual available models; the static catalog serves as fallback only.
30
+ - **`/route off` fixes the model with no failover** (`src/commands/launch.ts`, `src/commands/launch/route-slash.ts`, `src/agent/task-tool.ts`) — when routing is off (via config or `/route off`), neither main turns nor delegated subagents bail early or fail over to equivalent models on 429/refusal, and `/route` status reports `routing: off (this session) — model fixed, no failover`.
31
+
32
+ ### Fixed
33
+ - **Provider-qualified catalog entries no longer hijack bare model IDs** (`src/ai/model-catalog.ts`, `src/ai/model-catalog-compat.ts`) — `findCatalogModel`, `findCatalogEntry`, and `catalogMetadata` only match a bare `providerModel` when the catalog row itself is not provider-qualified (`!canonical.startsWith(`${provider}/`)`), preventing `antigravity/claude-sonnet-4-6` from misrouting bare `claude-sonnet-4-6` to `antigravity`.
34
+
35
+ ### Verified
36
+ - `bun run typecheck`, `bun run pack:check`, and full regression suites green.
37
+
12
38
  ## [0.12.2] - 2026-10-16
13
39
  _Select Argon through the official agy full agent, with its own live catalog and session, separately from jeo's Cloud Code Assist backend._
14
40
 
package/README.ja.md CHANGED
@@ -416,10 +416,10 @@ CI は `.github/workflows/npm-publish.yml` で公開します — GitHub リリ
416
416
 
417
417
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
418
418
  - **[Unreleased]**
419
+ - **[0.12.4]** (2026-10-17) — Refresh Antigravity provider (`antigravity/*`) account catalogs with `antigravity/hub/2.23.1` so entitled Gemini 4 Argon and Gemini 3.8 Flash tiers appear directly in `/model provider antigravity`.
420
+ - **[0.12.3]** (2026-10-17) — Anthropic static model catalog trimmed to claude-5.x with live API discovery, and `/route off` strictly fixes the active model with no failover.
419
421
  - **[0.12.2]** (2026-10-16) — Select Argon through the official agy full agent, with its own live catalog and session, separately from jeo's Cloud Code Assist backend.
420
422
  - **[0.12.1]** (2026-10-16) — Logged-in OpenAI subscription models were invisible on proxy-configured machines.
421
- - **[0.12.0]** (2026-10-16) — Karpathy behavioral guardrails become always-on discipline, and skill routing gains an ECC single-harness inventory plus a Jev-style advisory router.
422
- - **[0.11.6]** (2026-10-16) — Live Antigravity account model discovery now serves the actual account's available models instead of assuming static availability.
423
423
 
424
424
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
425
425
  <!-- CHANGELOG:END -->
package/README.ko.md CHANGED
@@ -417,10 +417,10 @@ CI는 `.github/workflows/npm-publish.yml`로 배포합니다 — GitHub 릴리
417
417
 
418
418
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
419
419
  - **[Unreleased]**
420
+ - **[0.12.4]** (2026-10-17) — Refresh Antigravity provider (`antigravity/*`) account catalogs with `antigravity/hub/2.23.1` so entitled Gemini 4 Argon and Gemini 3.8 Flash tiers appear directly in `/model provider antigravity`.
421
+ - **[0.12.3]** (2026-10-17) — Anthropic static model catalog trimmed to claude-5.x with live API discovery, and `/route off` strictly fixes the active model with no failover.
420
422
  - **[0.12.2]** (2026-10-16) — Select Argon through the official agy full agent, with its own live catalog and session, separately from jeo's Cloud Code Assist backend.
421
423
  - **[0.12.1]** (2026-10-16) — Logged-in OpenAI subscription models were invisible on proxy-configured machines.
422
- - **[0.12.0]** (2026-10-16) — Karpathy behavioral guardrails become always-on discipline, and skill routing gains an ECC single-harness inventory plus a Jev-style advisory router.
423
- - **[0.11.6]** (2026-10-16) — Live Antigravity account model discovery now serves the actual account's available models instead of assuming static availability.
424
424
 
425
425
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
426
426
  <!-- CHANGELOG:END -->
package/README.md CHANGED
@@ -409,10 +409,10 @@ Huge thanks to [gajae-code](https://github.com/Yeachan-Heo/gajae-code) for the i
409
409
 
410
410
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
411
411
  - **[Unreleased]**
412
+ - **[0.12.4]** (2026-10-17) — Refresh Antigravity provider (`antigravity/*`) account catalogs with `antigravity/hub/2.23.1` so entitled Gemini 4 Argon and Gemini 3.8 Flash tiers appear directly in `/model provider antigravity`.
413
+ - **[0.12.3]** (2026-10-17) — Anthropic static model catalog trimmed to claude-5.x with live API discovery, and `/route off` strictly fixes the active model with no failover.
412
414
  - **[0.12.2]** (2026-10-16) — Select Argon through the official agy full agent, with its own live catalog and session, separately from jeo's Cloud Code Assist backend.
413
415
  - **[0.12.1]** (2026-10-16) — Logged-in OpenAI subscription models were invisible on proxy-configured machines.
414
- - **[0.12.0]** (2026-10-16) — Karpathy behavioral guardrails become always-on discipline, and skill routing gains an ECC single-harness inventory plus a Jev-style advisory router.
415
- - **[0.11.6]** (2026-10-16) — Live Antigravity account model discovery now serves the actual account's available models instead of assuming static availability.
416
416
 
417
417
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
418
418
  <!-- CHANGELOG:END -->
package/README.zh.md CHANGED
@@ -414,10 +414,10 @@ CI 通过 `.github/workflows/npm-publish.yml` 发布 — GitHub 发布 release
414
414
 
415
415
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
416
416
  - **[Unreleased]**
417
+ - **[0.12.4]** (2026-10-17) — Refresh Antigravity provider (`antigravity/*`) account catalogs with `antigravity/hub/2.23.1` so entitled Gemini 4 Argon and Gemini 3.8 Flash tiers appear directly in `/model provider antigravity`.
418
+ - **[0.12.3]** (2026-10-17) — Anthropic static model catalog trimmed to claude-5.x with live API discovery, and `/route off` strictly fixes the active model with no failover.
417
419
  - **[0.12.2]** (2026-10-16) — Select Argon through the official agy full agent, with its own live catalog and session, separately from jeo's Cloud Code Assist backend.
418
420
  - **[0.12.1]** (2026-10-16) — Logged-in OpenAI subscription models were invisible on proxy-configured machines.
419
- - **[0.12.0]** (2026-10-16) — Karpathy behavioral guardrails become always-on discipline, and skill routing gains an ECC single-harness inventory plus a Jev-style advisory router.
420
- - **[0.11.6]** (2026-10-16) — Live Antigravity account model discovery now serves the actual account's available models instead of assuming static availability.
421
421
 
422
422
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
423
423
  <!-- CHANGELOG:END -->
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jeo-code",
3
- "version": "0.12.2",
3
+ "version": "0.12.4",
4
4
  "description": "Clean, highly optimized AI coding agent using spec-first loop",
5
5
  "type": "module",
6
6
  "main": "src/cli.ts",
@@ -433,6 +433,10 @@ export async function runSubagentOnce(
433
433
  // "false positive -> try a different model" design this implements.
434
434
  const safetyFallbackAvailable = (): boolean => safetyFallbackCandidates().length > 0;
435
435
 
436
+ // Failover (reroute to an equivalent model) is part of routing: it only runs when
437
+ // `routing.enabled` is on. `launch.ts` passes the parent session's EFFECTIVE state
438
+ // (`/route on|off` override ?? config) in `config.routing.enabled`.
439
+ const failoverEnabled = !!opts.config.routing?.enabled;
436
440
  let activeModel = initialModel;
437
441
  emit({ role: role.id, kind: "start", detail: taskText, maxSteps, model: activeModel });
438
442
  const runOnce = () => {
@@ -448,8 +452,7 @@ export async function runSubagentOnce(
448
452
  signal,
449
453
  steer,
450
454
  tools: subagentToolset(role),
451
- rateLimitFallbackAvailable,
452
- safetyFallbackAvailable,
455
+ ...(failoverEnabled ? { rateLimitFallbackAvailable, safetyFallbackAvailable } : {}),
453
456
  events: {
454
457
  onStep: n => { currentStep = n; },
455
458
  // Live reasoning preview (native extended-thinking models only — the JSON-protocol
@@ -494,7 +497,10 @@ export async function runSubagentOnce(
494
497
  };
495
498
 
496
499
  let result = await runOnce();
497
- for (let attempt = 0; attempt < MAX_SUBAGENT_REROUTES; attempt++) {
500
+ // `routing.enabled` off = the model is FIXED: no failover reroute at all (the
501
+ // engine bail predicates above are withheld too, so a 429 rides its normal
502
+ // backoff ladder instead of bailing early with nowhere to go).
503
+ for (let attempt = 0; failoverEnabled && attempt < MAX_SUBAGENT_REROUTES; attempt++) {
498
504
  const rateLimited = !result.done && isRateLimitError(new Error(result.doneReason ?? ""));
499
505
  const safetyBail = !result.done && !rateLimited && isSafetyFallbackBail(result.doneReason);
500
506
  if (!rateLimited && !safetyBail) break;
@@ -22,6 +22,7 @@ import { jeoEnv } from "../util/env";
22
22
  import { recordLiveCodexModels, recordLiveProviderModels, setAntigravityAccountScope, setOpenAIOauthAccountScope } from "./model-catalog";
23
23
  import type { Credential } from "../auth";
24
24
  import { getStoredOAuth, snapshotProvider } from "../auth/storage";
25
+ import { getAntigravityUserAgent } from "./providers/antigravity";
25
26
  import { extractChatgptAccountId } from "./providers/openai-responses";
26
27
  import type { ProviderName } from "./types";
27
28
 
@@ -87,7 +88,7 @@ export async function antigravityAccountScope(credential?: Credential): Promise<
87
88
  || process.env.GOOGLE_CLOUD_PROJECT?.trim()
88
89
  || process.env.GOOGLE_CLOUD_PROJECT_ID?.trim()
89
90
  || "";
90
- return createHash("sha256").update(`${providerSlot}\u0000${identity}\u0000${projectId}`, "utf8").digest("hex");
91
+ return createHash("sha256").update(`${providerSlot}\u0000${identity}\u0000${projectId}\u0000${getAntigravityUserAgent()}`, "utf8").digest("hex");
91
92
  } catch {
92
93
  return undefined;
93
94
  }
@@ -55,7 +55,11 @@ export function findCatalogEntry(id: string): ModelCatalogEntry | undefined {
55
55
  const direct = findCatalogModel(id);
56
56
  if (direct) return adapt(direct);
57
57
  const n = normalizeModelId(id);
58
- const hit = MODEL_CATALOG.find(m => normalizeModelId(m.canonical) === n || normalizeModelId(m.providerModel) === n);
58
+ const hit = MODEL_CATALOG.find(m =>
59
+ normalizeModelId(m.canonical) === n
60
+ || normalizeModelId(`${m.provider}/${m.providerModel}`) === n
61
+ || (!m.canonical.startsWith(`${m.provider}/`) && normalizeModelId(m.providerModel) === n),
62
+ );
59
63
  return hit ? adapt(hit) : undefined;
60
64
  }
61
65
 
@@ -80,7 +80,9 @@ const STD: ThinkLevel[] = ["low", "medium", "high"];
80
80
  * (`cheapestCredentialed`/`strongestCredentialed`/`compareStrengthAscending`)
81
81
  * now tiebreak on `releaseDate` directly rather than relying on position. */
82
82
  export const MODEL_CATALOG: readonly CatalogModel[] = [
83
- // Anthropic (newest first: sonnet-5-5 > opus-5-5 > sonnet-5 > fable-5 > opus-4-8 > opus-4-7 > sonnet-4-6 > opus-4-6). 4.5-and-below dropped from jeo's exposed model list per user direction.
83
+ // Anthropic (newest first: sonnet-5-5 > opus-5-5 > sonnet-5 > fable-5 > mythos-5).
84
+ // claude-4.x and below removed: live API discovery (https://api.anthropic.com/v1/models)
85
+ // returns the account's actual available models; the static catalog is fallback-only.
84
86
  // 5.5 generation (platform.claude.com/docs/en/models/{opus-5-5,sonnet-5-5}/overview): dateless pinned wire ids
85
87
  // `claude-opus-5-5` (released 2026-09-22) / `claude-sonnet-5-5` (2026-09-28), 1M ctx / 128k sync max output, adaptive
86
88
  // thinking (Opus 5.5 always on; Sonnet 5.5 on by default), text+image input. Canonical == wire id; anthropic.ts already
@@ -92,17 +94,7 @@ export const MODEL_CATALOG: readonly CatalogModel[] = [
92
94
  // Mythos 5 is limited-availability (Project Glasswing) but callable by id for approved accounts.
93
95
  { canonical: "claude-fable-5", provider: "anthropic", providerModel: "claude-fable-5", contextTokens: 1_000_000, maxOutputTokens: 128_000, thinking: FULL, images: true, releaseDate: "2026-06" },
94
96
  { canonical: "claude-mythos-5", provider: "anthropic", providerModel: "claude-mythos-5", contextTokens: 1_000_000, maxOutputTokens: 128_000, thinking: FULL, images: true, limitedAvailability: true, releaseDate: "2026-06" },
95
- // NOTE: opus 4.6+ use Anthropic ADAPTIVE thinking (type:"adaptive" + output_config.effort).
96
- // opus 4.7/4.8 OMIT visible thought unless the request opts into `display: "summarized"` —
97
- // anthropic.ts sets that on the adaptive transport so reasoning streams again (gjc parity).
98
- // The nativizable path still replays signature-only thinking blocks for cross-turn continuity.
99
- // The 4.6 generation onward is 1M context / 128k sync max output (Anthropic docs:
100
- // Opus 4.8 & Sonnet 5 comparison table; Opus 4.6/4.7, Sonnet 4.6 share the gen's
101
- // dateless-snapshot 1M/128k + 300k batch-output beta).
102
- { canonical: "claude-opus-4-8", provider: "anthropic", providerModel: "claude-opus-4-8", contextTokens: 1_000_000, maxOutputTokens: 128_000, thinking: FULL, images: true, releaseDate: "2026-05" },
103
- { canonical: "claude-opus-4-7", provider: "anthropic", providerModel: "claude-opus-4-7", contextTokens: 1_000_000, maxOutputTokens: 128_000, thinking: FULL, images: true, releaseDate: "2026-04" },
104
- { canonical: "claude-sonnet-4-6", provider: "anthropic", providerModel: "claude-sonnet-4-6", contextTokens: 1_000_000, maxOutputTokens: 128_000, thinking: FULL, images: true, releaseDate: "2026-02" },
105
- { canonical: "claude-opus-4-6", provider: "anthropic", providerModel: "claude-opus-4-6", contextTokens: 1_000_000, maxOutputTokens: 128_000, thinking: FULL, images: true, releaseDate: "2026-02" },
97
+
106
98
  // OpenAI (newest first: gpt-5.6 > gpt-5.5 > gpt-5.4 > o4-mini/o3/gpt-4.1 > o3-mini > gpt-4o-mini > gpt-4o)
107
99
  { canonical: "gpt-5.6", provider: "openai", providerModel: "gpt-5.6", contextTokens: 400_000, maxOutputTokens: 128_000, thinking: FULL, images: true, releaseDate: "2026-06" },
108
100
  { canonical: "gpt-5.5", provider: "openai", providerModel: "gpt-5.5", contextTokens: 400_000, maxOutputTokens: 128_000, thinking: FULL, images: true, releaseDate: "2026-04" },
@@ -526,7 +518,11 @@ export function formatTokens(n: number): string {
526
518
  /** Exact lookup by canonical id or provider model id. */
527
519
  export function findCatalogModel(idOrModel: string): CatalogModel | undefined {
528
520
  const q = idOrModel.trim();
529
- return MODEL_CATALOG.find(m => m.canonical === q || m.providerModel === q || `${m.provider}/${m.providerModel}` === q);
521
+ return MODEL_CATALOG.find(m =>
522
+ m.canonical === q
523
+ || `${m.provider}/${m.providerModel}` === q
524
+ || (!m.canonical.startsWith(`${m.provider}/`) && m.providerModel === q),
525
+ );
530
526
  }
531
527
 
532
528
  /**
@@ -622,7 +618,7 @@ export function inferCatalogMetadata(modelId: string): CatalogModel | undefined
622
618
  providerModel: id,
623
619
  contextTokens: 1_000_000,
624
620
  maxOutputTokens: 65_536,
625
- thinking: !reasons ? [] : big3 || id.includes("thinking") || id.includes("-high") || id.includes("-low") ? FULL : STD,
621
+ thinking: !reasons ? [] : big3 || id.includes("thinking") || id.includes("-high") || id.includes("-medium") || id.includes("-low") ? FULL : STD,
626
622
  images: true,
627
623
  company: antigravity ? "Google Antigravity" : "Google",
628
624
  };
@@ -653,7 +649,10 @@ export function catalogMetadata(modelId: string): CatalogModel | undefined {
653
649
  if (direct) return direct;
654
650
  // Tolerate provider-prefixed or bare provider model ids.
655
651
  const bare = modelId.replace(/^[a-z-]+\//, "");
656
- const hit = MODEL_CATALOG.find(m => m.providerModel === bare || m.providerModel.endsWith(`/${bare}`) || m.canonical === bare);
652
+ const hit = MODEL_CATALOG.find(m =>
653
+ m.canonical === bare
654
+ || (!m.canonical.startsWith(`${m.provider}/`) && (m.providerModel === bare || m.providerModel.endsWith(`/${bare}`))),
655
+ );
657
656
  if (hit) return hit;
658
657
  // Last resort: infer capabilities from the model family so a brand-new
659
658
  // revision still surfaces reasoning/thinking like its catalogued siblings.
@@ -212,7 +212,7 @@ function isGeminiChatModel(id: string): boolean {
212
212
  }
213
213
 
214
214
  type CodexModelRow = { slug?: string; id?: string; visibility?: string; supported_in_api?: boolean; priority?: number };
215
- type AntigravityModelRow = { slug?: string; id?: string; name?: string; isInternal?: boolean; model?: string };
215
+ type AntigravityModelRow = { slug?: string; id?: string; name?: string; displayName?: string; isInternal?: boolean; model?: string };
216
216
 
217
217
  function normalizeAntigravityModelId(id: string): string {
218
218
  return id.replace(/^models\//, "").replace(/^antigravity\//, "");
@@ -250,8 +250,9 @@ export function parseModelsBody(provider: ProviderName, body: unknown, opts: { o
250
250
  // (MODEL_PLACEHOLDER_*) and must never be surfaced. The response's OWN
251
251
  // metadata decides what to show — no hard-coded model lists:
252
252
  // Agent sorts and tiered model ids are complementary chat offerings, not
253
- // interchangeable allowlists. Include both plus the declared default;
254
- // only older responses without these fields need non-chat role filtering.
253
+ // interchangeable allowlists. Include both plus the declared default and
254
+ // named non-recommended chat models; exclude explicit non-chat roles and
255
+ // unnamed unreferenced rows.
255
256
  const payload = body as {
256
257
  models?: Record<string, AntigravityModelRow> | AntigravityModelRow[];
257
258
  agentModelSorts?: { groups?: { modelIds?: string[] }[] }[];
@@ -280,13 +281,16 @@ export function parseModelsBody(provider: ProviderName, body: unknown, opts: { o
280
281
  if (typeof payload.defaultAgentModelId === "string") agentIds.add(normalizeAntigravityModelId(payload.defaultAgentModelId));
281
282
  const hasChatModelMetadata = Array.isArray(payload.agentModelSorts)
282
283
  || payload.tieredModelIds !== undefined || payload.defaultAgentModelId !== undefined;
283
- const nonChat = new Set([
284
+ const explicitNonChat = new Set([
284
285
  ...roleIds(payload.tabModelIds),
285
286
  ...roleIds(payload.imageGenerationModelIds),
286
287
  ...roleIds(payload.audioTranscriptionModelIds),
288
+ ...deprecated,
289
+ ]);
290
+ const nonChat = new Set([
291
+ ...explicitNonChat,
287
292
  ...roleIds(payload.commitMessageModelIds),
288
293
  ...roleIds(payload.mqueryModelIds),
289
- ...deprecated,
290
294
  ]);
291
295
  const rawModels = payload.models;
292
296
  const entries = Array.isArray(rawModels)
@@ -297,7 +301,10 @@ export function parseModelsBody(provider: ProviderName, body: unknown, opts: { o
297
301
  .filter(([id, model]) =>
298
302
  id.length > 0 &&
299
303
  model?.isInternal !== true &&
300
- (hasChatModelMetadata ? agentIds.has(id) : !nonChat.has(id)))
304
+ !explicitNonChat.has(id) &&
305
+ (hasChatModelMetadata
306
+ ? agentIds.size > 0 && (agentIds.has(id) || (!nonChat.has(id) && typeof model?.displayName === "string" && model.displayName.trim().length > 0))
307
+ : !nonChat.has(id)))
301
308
  .map(([id]) => `antigravity/${id}`);
302
309
  }
303
310
  if (provider === "gemini") {
@@ -9,18 +9,18 @@ export interface ModelAliases {
9
9
  export const BUILTIN_ALIASES: ModelAliases = {
10
10
  fast: "ollama/qwen2.5:0.5b",
11
11
  local: "ollama/qwen2.5:0.5b",
12
- sonnet: "claude-sonnet-4-6",
13
- opus: "claude-opus-4-6",
12
+ sonnet: "claude-sonnet-5-5",
13
+ opus: "claude-opus-5-5",
14
14
  gpt: "gpt-5.5",
15
15
  flash: "gemini-2.5-flash",
16
16
  grok: "grok-4.3",
17
17
  kimi: "kimi-k2-0711-preview",
18
- // Anthropic 5.5 generation: spellings that name the version. Bare `opus`/`sonnet` above stay on the
19
- // 4.6 ids (pinned backward-compat); users opt into 5.5 by naming the version.
18
+ // Anthropic 5.5 generation: versioned spellings as convenience aliases.
20
19
  "opus-5.5": "claude-opus-5-5",
21
20
  "sonnet-5.5": "claude-sonnet-5-5",
22
21
  "opus5.5": "claude-opus-5-5",
23
22
  "sonnet5.5": "claude-sonnet-5-5",
23
+
24
24
  // Antigravity's backend DEPRECATED this wire id (deprecatedModelIds:
25
25
  // { "gemini-3.1-pro-high": { newModelId: "gemini-pro-agent" } }) — keep
26
26
  // existing configs/roles pinned to the old id working on the successor.
@@ -28,10 +28,10 @@ function antigravityClaudeThinkingBudget(effort: CallOptions["reasoningEffort"])
28
28
  const ENDPOINTS = [ANTIGRAVITY_DAILY_ENDPOINT, ANTIGRAVITY_SANDBOX_ENDPOINT] as const;
29
29
 
30
30
  export function getAntigravityUserAgent(): string {
31
- const version = process.env.PI_AI_ANTIGRAVITY_VERSION || "1.104.0";
31
+ const version = process.env.PI_AI_ANTIGRAVITY_VERSION || "2.23.1";
32
32
  const os = process.platform === "win32" ? "windows" : process.platform;
33
33
  const arch = process.arch === "x64" ? "amd64" : process.arch === "ia32" ? "386" : process.arch;
34
- return `antigravity/${version} ${os}/${arch}`;
34
+ return `antigravity/hub/${version} ${os}/${arch}`;
35
35
  }
36
36
 
37
37
  function antigravityModelId(model: string): string {
@@ -48,6 +48,7 @@ export function geminiThinkingBudget(model: string, effort?: CallOptions["reason
48
48
  * the documented thought-token burn protection). Expects a lowercased id. */
49
49
  function geminiNamedEffort(m: string): CallOptions["reasoningEffort"] | undefined {
50
50
  return m.includes("-high") ? "high"
51
+ : m.includes("-medium") ? "medium"
51
52
  : m.includes("-low") ? "low"
52
53
  : m.includes("thinking") ? "medium"
53
54
  : undefined;
@@ -44,6 +44,11 @@ export interface RouteSlashResult {
44
44
  lines: string[];
45
45
  }
46
46
 
47
+ /** Off means a FIXED model: no prompt routing AND no equivalent-model failover. */
48
+ function stateLabel(enabled: boolean): string {
49
+ return enabled ? "routing: on (this session)" : "routing: off (this session) — model fixed, no failover";
50
+ }
51
+
47
52
  const USAGE = "Usage: /route [status|on|off|why|history [n]|save|on save|off save]";
48
53
 
49
54
  function isRealDecision(decision: RouteDecision | { note: string } | null): decision is RouteDecision {
@@ -92,7 +97,7 @@ export async function runRouteSlash(input: string, ctx: RouteSlashCtx): Promise<
92
97
  const effective = ctx.sessionRouteOverride ?? ctx.routingConfigEnabled;
93
98
 
94
99
  if (!sub || sub === "status") {
95
- const lines = [`routing: ${effective ? "on" : "off"} (this session)`];
100
+ const lines = [stateLabel(effective)];
96
101
  if (isRealDecision(ctx.lastRouteDecision)) {
97
102
  lines.push(`last decision: ${ctx.lastRouteDecision.tier} → ${ctx.lastRouteDecision.model} (${ctx.lastRouteDecision.source}: ${ctx.lastRouteDecision.signals.join(", ") || "none"}, confidence ${ctx.lastRouteDecision.confidence.toFixed(2)})`);
98
103
  }
@@ -113,9 +118,9 @@ export async function runRouteSlash(input: string, ctx: RouteSlashCtx): Promise<
113
118
  const enabled = sub === "on";
114
119
  if (arg === "save") {
115
120
  await saveConfigPatch(raw => ({ routing: { ...raw.routing, enabled } }));
116
- return { sessionRouteOverride: enabled, lines: [`routing: ${enabled ? "on" : "off"} (this session) — saved to ~/.jeo/config.json`] };
121
+ return { sessionRouteOverride: enabled, lines: [`${stateLabel(enabled)} — saved to ~/.jeo/config.json`] };
117
122
  }
118
- return { sessionRouteOverride: enabled, lines: [`routing: ${enabled ? "on" : "off"} (this session)`] };
123
+ return { sessionRouteOverride: enabled, lines: [stateLabel(enabled)] };
119
124
  }
120
125
 
121
126
  // Bare `/route save` persists whatever is CURRENTLY effective (a prior
@@ -1659,7 +1659,10 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
1659
1659
  const fullTools = {
1660
1660
  ...DEFAULT_TOOLS,
1661
1661
  task: createTaskTool({
1662
- config: { ...turnConfig, defaultModel: activeModel },
1662
+ // Subagents inherit the parent's EFFECTIVE routing state (`/route on|off`
1663
+ // override ?? config) — task-tool.ts gates its failover reroute on it.
1664
+ config: { ...turnConfig, defaultModel: activeModel, routing: { ...turnConfig.routing, enabled: routingEnabled } },
1665
+
1663
1666
  signal: ac.signal,
1664
1667
  steer: drainSteer,
1665
1668
  registry: subagentRegistry,
@@ -1710,12 +1713,13 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
1710
1713
  // mutated in place (`.add`) as the fallback loop below tries more models — this
1711
1714
  // closure always reads its live contents, never a stale snapshot.
1712
1715
  const attemptedRouteModels = new Set<string>([activeModel]);
1713
- // A NON-routed turn (routing disabled, or the user pinned a model via /model)
1714
- // still needs the equivalent-pool fallback on a usage-limit/rate-limit/credential
1715
- // failure — the exact "5-hour usage window exhausted, model just stops working"
1716
- // case this loop exists to fix. Synthesize a same-size-class RouteDecision
1717
- // (inferTierForModel) so `equivalentRouteFallback` has a tier/pool to search even
1718
- // with routing off; a real `routed` decision (routing enabled) is used unchanged.
1716
+ // The synthesized decision below is only the BASE for `equivalentRouteFallback`'s
1717
+ // tier/pool search when the pre-call router produced no decision (routing on but
1718
+ // fail-open, or a /model pin with `/route on`). It is inert when routing is off:
1719
+ // the reroute loop and the engine's bail predicates are both gated on
1720
+ // `routingEnabled`, so `/route off` means a FIXED model with no failover.
1721
+ // (`inferTierForModel` gives the same-size-class tier/pool to search.)
1722
+
1719
1723
  let fallbackBaseDecision: RouteDecision = routed ?? {
1720
1724
  model: activeModel,
1721
1725
  thinking: activeThinking,
@@ -1785,8 +1789,10 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
1785
1789
  signal: ac.signal,
1786
1790
  sessionKey: cacheKey,
1787
1791
  steer: drainSteer,
1788
- rateLimitFallbackAvailable,
1789
- safetyFallbackAvailable,
1792
+ // Withheld when routing is off: the bail predicates only make sense paired
1793
+ // with the reroute loop below, which `routingEnabled` also gates.
1794
+ ...(routingEnabled ? { rateLimitFallbackAvailable, safetyFallbackAvailable } : {}),
1795
+
1790
1796
  events: wrapEvents(withStepPersistence({ ...withToolDetailCapture(tui ? tui.events() : streamEvents), onBeforeDone }, persistTurnTail), opik),
1791
1797
  });
1792
1798
  const routeFailureReason = (doneReason: unknown): string | null => {
@@ -1866,7 +1872,8 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
1866
1872
  const routeFallbackMax = Number.isFinite(routeFallbackMaxRaw) && routeFallbackMaxRaw >= 0
1867
1873
  ? Math.floor(routeFallbackMaxRaw)
1868
1874
  : 3;
1869
- for (let routeFallbackAttempt = 0; routeFallbackAttempt < routeFallbackMax; routeFallbackAttempt++) {
1875
+ for (let routeFallbackAttempt = 0; routingEnabled && routeFallbackAttempt < routeFallbackMax; routeFallbackAttempt++) {
1876
+
1870
1877
  // A loop guard is a local safety boundary, not a provider failure. A
1871
1878
  // fresh loop on an equivalent model loses the guard state and could
1872
1879
  // re-execute a previously skipped mutation.
@@ -1962,10 +1969,20 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
1962
1969
  }
1963
1970
  // A completed turn with an empty done-reason must NOT masquerade as a step-limit
1964
1971
  // failure ("reached the 3-step limit" after the model called done at step 3).
1965
- const reply = stripSafetyFallbackTag(result.doneReason ?? "")
1972
+ const baseReply = stripSafetyFallbackTag(result.doneReason ?? "")
1966
1973
  || (result.done
1967
1974
  ? `(done in ${result.steps} step${result.steps === 1 ? "" : "s"} — the model returned no summary)`
1968
1975
  : `(reached the ${result.steps}-step limit without signaling done)`);
1976
+ // Routing off = the model is fixed and nothing fails over. When the turn died on
1977
+ // an account-level limit, say so and name the one switch that re-enables failover.
1978
+ const failoverHint = !routingEnabled && !result.done && (() => {
1979
+ const failure = new Error(result.doneReason ?? "");
1980
+ return isRateLimitError(failure) || isUsageLimitError(failure);
1981
+ })()
1982
+ ? "\n(routing is off — the model is fixed and no equivalent-model failover was attempted. Run '/route on' to allow failover.)"
1983
+ : "";
1984
+ const reply = `${baseReply}${failoverHint}`;
1985
+
1969
1986
  if (sessionNotifyEndpoint) {
1970
1987
  const usageStr = result.usage ? formatUsage(result.usage) : undefined;
1971
1988
  if (notifyRedact) {