jeo-code 0.8.1 → 0.8.3

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
@@ -6,6 +6,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
6
6
 
7
7
  The README mirrors the latest 5 entries — regenerate with `bun run changelog:sync`.
8
8
 
9
+ ## [0.8.3] - 2026-07-08
10
+ _"잘못된 모델 설정하는 경우가 있는데" — the static Antigravity catalog had drifted badly from the LIVE Cloud Code Assist agent backend, and several current wire ids actively LIE about their tier: `gemini-3-flash-agent` is really "Gemini 3.5 Flash (High)" (the flagship agent model, which the name-suffix heuristic filed under the SMALL flash tier), `gemini-pro-agent` is really "Gemini 3.1 Pro (High)" (the code-agent model), and `gemini-3.1-pro-high` is DEPRECATED by the backend in favor of `gemini-pro-agent`. Probed the real `fetchAvailableModels` response (displayNames + agentModelSorts + deprecatedModelIds) and made it the single source of truth._
11
+
12
+ ### Fixed
13
+ - **Antigravity catalog rebuilt from the live agent set** (`src/ai/model-catalog.ts`) — the 17 stale tuple rows (`antigravity/gpt-5.5`, `claude-opus-4-8*`, `gemini-3-pro-*`, `gemini-2.5-*`, deprecated `gemini-3.1-pro-high`, …) are replaced by the 8 rows the backend actually serves today, each pinned to its displayName truth with a new explicit `CatalogModel.sizeClass`: `gemini-3-flash-agent` ("Gemini 3.5 Flash (High)") = **large/high**, `gemini-3.5-flash-low` (Medium) = mid, `gemini-3.5-flash-extra-low` (Low) = small, `gemini-pro-agent` ("Gemini 3.1 Pro (High)", the **code-agent** model) = mid + RECOMMENDED badge (replaces `antigravity/gpt-5.5`), `gemini-3.1-pro-low` = **small/low**, plus selectable `claude-sonnet-4-6` (Thinking) = mid and `claude-opus-4-6-thinking` = large.
14
+ - **`sizeClassFor` honors the explicit `sizeClass`** (`src/agent/prompt-router.ts`) — the suffix heuristic misfiled every "agent" wire id (flash⇒small even when the model is the flagship; pro⇒mid even for the Low tier). Routing pools now place each Antigravity model in the tier its displayName declares: trivial→{3.1-pro-low, 3.5-flash-extra-low}, standard/high→{gemini-pro-agent, claude-sonnet-4-6, …}, complex→{gemini-3-flash-agent, claude-opus-4-6-thinking}. The now-meaningless `isAntigravityGeminiBelow31` version floor (wire ids no longer encode versions honestly) is deleted — the catalog rows themselves are the floor.
15
+ - **`cheapestCredentialed` prefers the smaller size class on a flat-price tie** — inside one subscription family every row shares the same family price (e.g. all Antigravity Gemini rows), so the old newest-first tiebreak picked the FLAGSHIP (`gemini-3-flash-agent`) as "cheapest". Trivial-tier auto-select now lands on the actual Low tier (`gemini-3.5-flash-extra-low`).
16
+ - **Deprecated wire id keeps working** (`src/ai/model-registry.ts`) — `antigravity/gemini-3.1-pro-high` → `antigravity/gemini-pro-agent` added to `BUILTIN_ALIASES`, mirroring the backend's own `deprecatedModelIds.newModelId` mapping, so existing configs/roles pinned to the old id resolve to the successor instead of erroring. `model-manager.ts`'s stale duplicate `ALIAS_DEFAULTS` table is deleted in favor of the single `BUILTIN_ALIASES` source (the call path now also expands the `kimi` alias, previously missing there).
17
+
18
+ ## [0.8.2] - 2026-07-08
19
+ _"프롬프트라우팅이 api 입력을 요구하면서 동작안해" — prompt routing kept demanding an API key even when the user was already logged in via OAuth (GPT showed the same symptom). Root cause: every routing gate was PROVIDER-level ("any stored credential exists") while OAuth credentials are MODEL-level (ChatGPT OAuth serves only Codex ids, Kimi OAuth only the Kimi Code catalog, gemini OAuth nothing at all since the Cloud Code Assist masquerade was removed) — so routing happily picked models the stored login could never serve, and the turn then failed at call time asking for `<PROVIDER>_API_KEY`. Also restored a missing export that had broken `bun run typecheck` on HEAD._
20
+
21
+ ### Fixed
22
+ - **Model-level credential gate for prompt routing** (`src/ai/model-manager.ts`, `src/agent/prompt-router.ts`) — new shared `modelServableWithConfig(provider, model, config)` predicate mirrors `resolveCall`'s real serving rules: an API key serves the provider's full catalog; OAuth serves exactly what `oauthServesModel` verifies end-to-end (openai→`gpt-5.5`/`gpt-5.4` only, kimi→Kimi Code ids only, gemini→none without `GEMINI_API_KEY`); antigravity is OAuth-only (an `providers.antigravity` API key alone cannot serve calls — previously counted as credentialed); a configured `openaiBaseUrl` stays the keyless local-proxy path. `isAutoSelectCandidate` now applies this predicate, so auto-routing only ever selects models the stored OAuth **or** API-key login can actually serve — both auth modes enable models, each scoped to what it really serves.
23
+ - **Routing veto gate closes the explicit-pin blind spot** (`src/commands/launch.ts`) — the per-turn gate previously checked only provider-level readiness (`describeProvider().ready`), so an explicitly configured `routing.tiers.*.model`/`roles.*` pointing at a model the stored OAuth cannot serve (e.g. `gpt-4o` with a ChatGPT-OAuth-only login) passed the gate and then failed mid-turn demanding an API key. The gate now also verifies `modelServableWithConfig` and falls back to the session/default model with an actionable one-time notice ("set `<PROVIDER>_API_KEY` or reconfigure routing.tiers/roles"), keeping routing strictly fail-open.
24
+ - **Gemini models are exposed ONLY under API-key auth** (`src/ai/model-discovery.ts`) — a gemini OAuth token can still LIST models from `generativelanguage.googleapis.com` but can no longer SERVE them, so `/model`/`/provider` pickers offered models every call would reject. `listProviderModels("gemini")` now short-circuits an OAuth-only credential with an actionable error (set `GEMINI_API_KEY`, or use `antigravity/*`) before any fetch, and `catalogOr` no longer resurrects the static gemini catalog for oauth-sourced failures. With `GEMINI_API_KEY` set (alone or alongside OAuth — the existing API-key-preference swap), live discovery is unchanged.
25
+ - **Restored the missing `compareReleaseDate` export** (`src/ai/model-catalog.ts`) — HEAD (`bb1b6c0`) imported it in `prompt-router.ts` but the helper itself never landed (another shared-worktree partial-stage, same failure mode as the v0.8.0 postmortem), so `bun run typecheck` failed on a pristine checkout. Reimplemented per its documented contract: lexicographic `"YYYY-MM"` compare, missing date sorts oldest.
26
+
27
+ ### Added
28
+ - **`/route` documented in the README slash-command table** — `/route [status|on|off|why]`: session-local routing toggle + last-decision explainer, alongside the existing `/model`/`/thinking` rows.
29
+
9
30
  ## [0.8.1] - 2026-07-08
10
31
  _Fix-forward for a functional regression this same session shipped in v0.8.0. This repo's working tree is shared with a concurrent interactive session; `git add`-ing `src/ai/provider-status.ts` for v0.8.0's routing-veto-gate fix also swept up that session's already-modified, uncommitted copy of the SAME file (containing an unrelated design change: gemini OAuth alone no longer serving `google/gemini-*` models) without its companion piece in `src/auth/flows/index.ts` — so v0.8.0 shipped with a test suite (`provider-status.test.ts`) asserting behavior whose supporting data flag never landed. CI caught this immediately post-release: 2 failing tests on the exact tagged commit. Investigated, confirmed the missing piece was real (and independently observable live via `jeo doctor` before this session had made ANY of its own edits — not a stray/speculative change), and completed it._
11
32
 
package/README.ja.md CHANGED
@@ -38,7 +38,7 @@
38
38
 
39
39
  ## ハイライト
40
40
 
41
- - **マルチプロバイダ・単一ループ** — Anthropic / OpenAI(+Codex) / Gemini / Antigravity / Ollama / LM Studio に加え、OpenAI・Anthropic 互換クラウド20以上(Groq、DeepSeek、Mistral、OpenRouter、xAI、Kimi、z.ai など)を均一な JSON ツールループで。入力欄から OAuth ログイン(`/provider login`)、モデル選択は即座にデフォルトとして永続化。
41
+ - **マルチプロバイダ・単一ループ** — Anthropic / OpenAI(+Codex) / Gemini / Antigravity / Ollama / LM Studio に加え、OpenAI・Anthropic 互換クラウド20以上(Groq、DeepSeek、Mistral、OpenRouter、xAI、Kimi、z.ai など)を均一な JSON ツールループで。入力欄から OAuth ログイン(`/provider login`)、モデル選択は即座にデフォルトとして永続化。プロンプトルーティングは実際に使える認証済み経路だけを自動選択します: Gemini OAuth は provider-qualified な `antigravity/*` エージェントセット(Gemini 3.5 Flash 各ティア、Gemini 3.1 Pro、Claude Sonnet/Opus 4.6)へ向かい、`GEMINI_API_KEY` が必要な public `google/gemini-*` 行は選びません。
42
42
  - **編集の完全性** — read 出力にコンテンツアンカー(`42ab|`)が付き、アンカー付き編集は現在のファイルと照合・行移動時は自動再マッピング・不一致時は最新内容と共に拒否されます。
43
43
  - **自己修正の検証ループ** — post-edit フック(tsc / eslint / テスト)を設定すると、エージェントが診断を*自ら読み*ループ内で修正します。フックが赤のままだと `done` はブロックされます。
44
44
  - **芝居なしの本物のゲート** — `ralplan` の合議はリポジトリを実際に読む critic サブエージェントで、`[OKAY]` 評決が永続化され `jeo approve` がそれを*要求*します。`ultragoal` は誠実に報告します(スイート1回実行はグローバル信号であり、基準ごとの合格を捏造しません)。
@@ -46,7 +46,7 @@
46
46
  - **動的ステップ予算** — 直近のツール呼び出しが新規の進捗を示す間は延長され、停滞すれば要約に収束。サブエージェントは厳密なステップ契約を維持。
47
47
  - **インライン TUI** — 完了した作業は実スクロールバックに流れ(ターン中も tmux ホイール可)、エージェント実行中も通常のクエリ入力欄が表示されたまま編集できます。Ctrl+O の詳細トグル、テーマ、クリップボード画像貼り付け(Ctrl+V)、CJK/絵文字対応の幅計算。
48
48
  - **ブラウザツール** — Playwright によるヘッドレス Chromium 自動化を第一級のエージェントツールとして搭載: 名前付きタブを再利用しつつ `open`/`close`/`run`/`act`、スクリーンショットより `observe` でタグ付けした要素 id を優先。`npx playwright install chromium` を一度実行する必要があります(バンドルされていません — jeo 自体はネイティブ依存ゼロのまま、ブラウザバイナリは Playwright 側の別ダウンロードです)。
49
- - **リモートサブエージェント可視化(Telegram)** — ボットを一度ペアリング(`jeo notify setup`)すれば、`jeo daemon start` がサブエージェントの状態遷移(開始 → 完了/失敗/キャンセル)ごとにメッセージを送り、`/subagents`、`/steer <id> <subagentId> <msg>`、`/cancel <id> <subagentId>` を受け付けます — コマンドはペアリングされたチャットのみ許可されます。
49
+ - **リモートサブエージェント可視化(Telegram)** — ボットを一度ペアリング(`jeo notify setup`)すれば、`jeo daemon start` がサブエージェントの状態遷移(開始 → 完了/失敗/キャンセル)ごとにメッセージを送り、`/subagents`、`/steer <id> <subagentId> <msg>`、`/cancel <id> <subagentId>` を受け付けます。Telegram フォーラムトピック、インラインキーボード、画像添付を含む `gjc` 完全パリティを提供し、コマンドはペアリングされたチャットのみ許可されます。
50
50
 
51
51
  ## インストール
52
52
 
@@ -79,11 +79,12 @@ jeo --tmux # 独立した tmux セッションで実行
79
79
  | `/provider login <name>` · `/logout` | 入力欄から OAuth ログイン/ログアウト |
80
80
  | `/agents [role]` · `/subagent` | ロール別(executor/planner/architect/critic)モデル・thinking・ステップ設定 |
81
81
  | `/thinking [level]` | デフォルト推論予算(low…xhigh)の表示/設定 |
82
- | `/fast [on|off|status]` | 現在のモデルが low 推論をサポートする場合に fast thinking モードを切替 |
82
+ | `/route [status\|on\|off\|why]` | セッション単位のプロンプトベース・モデルルーティングの切替 · 直近のルーティング判断の説明(設定済み資格情報 — OAuth または API キー — が実際に提供するモデルの中でのみ自動ルーティング) |
83
+ | `/fast [on\|off\|status]` | 現在のモデルが low 推論をサポートする場合に fast thinking モードを切替 |
83
84
  | `/skill` · `$<skill> [intent]` | ワークフロースキルの一覧/実行(`$team "task"` 形式) |
84
85
  | `/view` · `/diff` · `/find` · `/search` | コード表示、git diff、ファイル/パターン検索 |
85
86
  | `/new` · `/resume` · `/sessions` | セッション管理 |
86
- | `/history [n|all]` · `/export` | 作業アクティビティ履歴を読みやすくスクロールバックへ再出力・トランスクリプト出力 |
87
+ | `/history [n\|all]` · `/export` | 作業アクティビティ履歴を読みやすくスクロールバックへ再出力・トランスクリプト出力 |
87
88
  | `/retry` · `/btw <q>` | 直前要求の再試行 · 履歴に残らないサイド質問 |
88
89
  | `/usage` · `/context` · `/compact` | トークン使用量、コンテキスト内訳、手動コンパクション |
89
90
  | `/theme` · `/config` · `/help` | テーマ、ランタイム設定、ヘルプ |
@@ -212,8 +213,10 @@ JEO_TUI_THEME=cosmic # cosmic/matrix/solar/red-claw/blue-crab/mono/au
212
213
  JEO_TUI_ALT_SCREEN=1 # レガシー alt-screen ターン(デフォルト: インラインスクロールバック)
213
214
  JEO_STEP_BASE=24 # 動的ステップ予算のローリングベース
214
215
  JEO_STEP_HARD_CAP=600 # 絶対終了保証
215
- JEO_STREAM_MAX_MS=300000 # オプトインの全体ストリーム期限(デフォルト off; スロードリップ遮断)
216
+ JEO_STREAM_MAX_MS=1800000 # 全体ストリーム期限(デフォルト30分; スロードリップストリームを制限し、能動中のストリームそのものを止めるための値ではありません); 0 で無効化
216
217
  JEO_STREAM_IDLE_MS=300000 # チャンク単位のアイドル上限(デフォルト300秒); 最初のトークン前が長い低速/ローカルバックエンドでは引き上げてください
218
+ JEO_CALL_TIMEOUT_MS=1800000 # 非ストリーミング呼び出しの壁時計上限(デフォルト30分; compaction/subagents/goal-verify)
219
+ JEO_TURN_MAX_MS=1800000 # ターン停滞予算: ツール進捗がない最大時間(デフォルト30分); 0 で無効化
217
220
  JEO_TOOL_OUTPUT_MAX=4000 # モデル可視のツール出力上限(全文はアーティファクトへ)
218
221
  ```
219
222
 
@@ -259,11 +262,11 @@ CI は `.github/workflows/npm-publish.yml` で公開します — GitHub リリ
259
262
  ## 変更履歴 (Changelog)
260
263
 
261
264
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
265
+ - **[0.8.3]** (2026-07-08) — "잘못된 모델 설정하는 경우가 있는데" — the static Antigravity catalog had drifted badly from the LIVE Cloud Code Assist agent backend, and several current wire ids actively LIE about their tier: `gemini-3-flash-agent` is really "Gemini 3.5 Flash (High)" (the flagship agent model, which the name-suffix heuristic filed under the SMALL flash tier), `gemini-pro-agent` is really "Gemini 3.1 Pro (High)" (the code-agent model), and `gemini-3.1-pro-high` is DEPRECATED by the backend in favor of `gemini-pro-agent`. Probed the real `fetchAvailableModels` response (displayNames + agentModelSorts + deprecatedModelIds) and made it the single source of truth.
266
+ - **[0.8.2]** (2026-07-08) — "프롬프트라우팅이 api 입력을 요구하면서 동작안해" — prompt routing kept demanding an API key even when the user was already logged in via OAuth (GPT showed the same symptom). Root cause: every routing gate was PROVIDER-level ("any stored credential exists") while OAuth credentials are MODEL-level (ChatGPT OAuth serves only Codex ids, Kimi OAuth only the Kimi Code catalog, gemini OAuth nothing at all since the Cloud Code Assist masquerade was removed) — so routing happily picked models the stored login could never serve, and the turn then failed at call time asking for `<PROVIDER>_API_KEY`. Also restored a missing export that had broken `bun run typecheck` on HEAD.
262
267
  - **[0.8.1]** (2026-07-08) — Fix-forward for a functional regression this same session shipped in v0.8.0. This repo's working tree is shared with a concurrent interactive session; `git add`-ing `src/ai/provider-status.ts` for v0.8.0's routing-veto-gate fix also swept up that session's already-modified, uncommitted copy of the SAME file (containing an unrelated design change: gemini OAuth alone no longer serving `google/gemini-*` models) without its companion piece in `src/auth/flows/index.ts` — so v0.8.0 shipped with a test suite (`provider-status.test.ts`) asserting behavior whose supporting data flag never landed. CI caught this immediately post-release: 2 failing tests on the exact tagged commit. Investigated, confirmed the missing piece was real (and independently observable live via `jeo doctor` before this session had made ANY of its own edits — not a stray/speculative change), and completed it.
263
268
  - **[0.8.0]** (2026-07-08) — Root-caused a genuine reliability bug in the routing veto gate ("라우트가 동작후 api 키인증오류가나오는데 근본적인 원인 개선해"), then regenerated the animated README mascot to visualize the smart cross-provider routing this release cycle has been building: prompt routing that finds the cheapest usable path, with a "money saver" visual motif.
264
269
  - **[0.7.58]** (2026-07-07) — Applied the previous entry's `routing.crossProviderPool` to the real live session config, then deep-researched every catalogued model's actual public release date (2026-07 sourced, per-provider parallel research with citations) to answer "모델 설정은 2026년도 기준으로 딥서치해서 모델리스트 최신순으로 정렬" — while researching, found and fixed a genuine ranking bug this surfaced: `strongestCredentialed`'s tiebreak on a full capability tie (identical thinking/output/context) fell through to a raw alphabetical canonical-id compare, which silently picked the OLDEST model in a same-spec family (`claude-opus-4-6` over `claude-opus-4-7`/`4-8`, purely because `"4-6" < "4-8"` as a string) — confirmed reproducible before the fix, confirmed corrected after.
265
- - **[0.7.57]** (2026-07-07) — "동급모델간 크로스 프로바이더 라우팅이 동작하게 개선" — v0.7.56's tier auto-select was single-winner by design (always the one cheapest/strongest model), so a user with `roles.smol` pinned (or simply multiple credentialed providers) never saw actual distribution across equivalent-class models. Deep-research-informed redesign: production LLM routers (LiteLLM, Bifrost/VoidLLM) model this as a named "model group" spanning provider deployments with a distribution strategy, not a single deterministic pick — jeo now supports both, opt-in.
266
- - **[0.7.56]** (2026-07-07) — "서로다른 프로바이더로 급에 따라 나눠서 설정하도록 개선" — PromptRouter's `trivial`/`complex` tiers previously collapsed to `defaultModel` whenever `roles.smol`/`roles.slow`/`routing.tiers.*` were left unconfigured (the documented "safe no-op absent configuration" contract), which meant a user with multiple providers credentialed never actually got cross-provider routing without hand-picking a model per tier. Also the long-term ask: routing must stay correct as jeo's model catalog evolves, never a hand-maintained tier→model table that goes stale.
267
270
 
268
271
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
269
272
  <!-- CHANGELOG:END -->
package/README.ko.md CHANGED
@@ -38,7 +38,7 @@
38
38
 
39
39
  ## 하이라이트
40
40
 
41
- - **멀티 프로바이더, 단일 루프** — Anthropic / OpenAI(+Codex) / Gemini / Antigravity / Ollama / LM Studio, 그리고 OpenAI·Anthropic 호환 클라우드 20종 이상(Groq, DeepSeek, Mistral, OpenRouter, xAI, Kimi, z.ai 등)까지 균일한 JSON 도구 루프로. 입력창에서 바로 OAuth 로그인(`/provider login`), 모델 선택은 즉시 기본값으로 영속.
41
+ - **멀티 프로바이더, 단일 루프** — Anthropic / OpenAI(+Codex) / Gemini / Antigravity / Ollama / LM Studio, 그리고 OpenAI·Anthropic 호환 클라우드 20종 이상(Groq, DeepSeek, Mistral, OpenRouter, xAI, Kimi, z.ai 등)까지 균일한 JSON 도구 루프로. 입력창에서 바로 OAuth 로그인(`/provider login`), 모델 선택은 즉시 기본값으로 영속됩니다. 프롬프트 라우팅은 실제 사용 가능한 인증 경로만 자동 선택합니다: Gemini OAuth는 provider-qualified `antigravity/*` 에이전트 세트(Gemini 3.5 Flash 등급, Gemini 3.1 Pro, Claude Sonnet/Opus 4.6)로만 가고, `GEMINI_API_KEY`가 필요한 public `google/gemini-*` 행은 고르지 않습니다.
42
42
  - **편집 무결성** — read 출력에 콘텐츠 앵커(`42ab|`)가 붙고, 앵커 편집은 현재 파일과 대조 검증·줄 이동 시 자동 재매핑·불일치 시 최신 내용과 함께 거부 — 파일을 오염시키지 않습니다.
43
43
  - **자기수정 검증 루프** — post-edit 훅(tsc / eslint / 테스트)을 설정하면 에이전트가 진단을 *직접 읽고* 루프 안에서 수정합니다. 훅이 빨간 상태면 `done`이 차단됩니다.
44
44
  - **연극 없는 진짜 게이트** — `ralplan` 합의는 실제 저장소를 읽는 critic 서브에이전트이며 `[OKAY]` 평결이 영속되고 `jeo approve`가 이를 *요구*합니다. `ultragoal`은 정직하게 보고합니다(스위트 1회 실행은 전역 신호일 뿐, 기준별 통과를 조작하지 않음).
@@ -80,11 +80,12 @@ jeo --tmux # 독립 tmux 세션에서 실행
80
80
  | `/provider login <name>` · `/logout` | 입력창에서 OAuth 로그인/로그아웃 |
81
81
  | `/agents [role]` · `/subagent` | 역할별(executor/planner/architect/critic) 모델·thinking·스텝 구성 |
82
82
  | `/thinking [level]` | 기본 추론 예산(low…xhigh) 조회/설정 |
83
- | `/fast [on|off|status]` | 현재 모델이 low 추론을 지원하면 fast thinking 모드를 켜고 끔 |
83
+ | `/route [status\|on\|off\|why]` | 세션별 프롬프트 기반 모델 라우팅 켜기/끄기 · 마지막 라우팅 결정 설명(설정된 자격증명 — OAuth 또는 API 키 — 이 실제로 서비스하는 모델 안에서만 자동 라우팅) |
84
+ | `/fast [on\|off\|status]` | 현재 모델이 low 추론을 지원하면 fast thinking 모드를 켜고 끔 |
84
85
  | `/skill` · `$<skill> [intent]` | 워크플로 스킬 목록/실행(`$team "작업"` 스타일) |
85
86
  | `/view` · `/diff` · `/find` · `/search` | 코드 보기, git diff, 파일/패턴 검색 |
86
87
  | `/new` · `/resume` · `/sessions` | 세션 관리 |
87
- | `/history [n|all]` · `/export` | 작업 활동 히스토리를 읽기 좋게 스크롤백에 재출력 · 트랜스크립트 내보내기 |
88
+ | `/history [n\|all]` · `/export` | 작업 활동 히스토리를 읽기 좋게 스크롤백에 재출력 · 트랜스크립트 내보내기 |
88
89
  | `/retry` · `/btw <질문>` | 마지막 요청 재시도 · 히스토리에 안 남는 사이드 질문 |
89
90
  | `/usage` · `/context` · `/compact` | 토큰 사용량, 컨텍스트 내역, 수동 컴팩션 |
90
91
  | `/theme` · `/config` · `/help` | 테마, 런타임 설정, 도움말 |
@@ -213,8 +214,10 @@ JEO_TUI_THEME=cosmic # cosmic/matrix/solar/red-claw/blue-crab/mono/au
213
214
  JEO_TUI_ALT_SCREEN=1 # 레거시 alt-screen 턴(기본: 인라인 스크롤백)
214
215
  JEO_STEP_BASE=24 # 동적 스텝 버짓의 롤링 베이스
215
216
  JEO_STEP_HARD_CAP=600 # 절대 종료 보증
216
- JEO_STREAM_MAX_MS=300000 # 옵트인 전체 스트림 데드라인(기본 off; 슬로우드립 차단)
217
+ JEO_STREAM_MAX_MS=1800000 # 전체 스트림 데드라인(기본 30분; 슬로우드립 스트림 제한, 활성 스트림 자체를 끊기 위한 값은 아님); 0이면 비활성화
217
218
  JEO_STREAM_IDLE_MS=300000 # 청크당 유휴 타임아웃(기본 300초); 첫 토큰 전 침묵이 긴 느린/로컬 백엔드는 값을 높이세요
219
+ JEO_CALL_TIMEOUT_MS=1800000 # 비스트리밍 호출 벽시계 제한(기본 30분; 컴팩션/서브에이전트/goal-verify)
220
+ JEO_TURN_MAX_MS=1800000 # 턴 정체 예산: 도구 진전이 없는 최대 시간(기본 30분); 0이면 비활성화
218
221
  JEO_TOOL_OUTPUT_MAX=4000 # 모델 가시 도구 출력 캡(전체는 아티팩트로 스필)
219
222
  ```
220
223
 
@@ -260,11 +263,11 @@ CI는 `.github/workflows/npm-publish.yml`로 배포합니다 — GitHub 릴리
260
263
  ## 변경 이력 (Changelog)
261
264
 
262
265
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
266
+ - **[0.8.3]** (2026-07-08) — "잘못된 모델 설정하는 경우가 있는데" — the static Antigravity catalog had drifted badly from the LIVE Cloud Code Assist agent backend, and several current wire ids actively LIE about their tier: `gemini-3-flash-agent` is really "Gemini 3.5 Flash (High)" (the flagship agent model, which the name-suffix heuristic filed under the SMALL flash tier), `gemini-pro-agent` is really "Gemini 3.1 Pro (High)" (the code-agent model), and `gemini-3.1-pro-high` is DEPRECATED by the backend in favor of `gemini-pro-agent`. Probed the real `fetchAvailableModels` response (displayNames + agentModelSorts + deprecatedModelIds) and made it the single source of truth.
267
+ - **[0.8.2]** (2026-07-08) — "프롬프트라우팅이 api 입력을 요구하면서 동작안해" — prompt routing kept demanding an API key even when the user was already logged in via OAuth (GPT showed the same symptom). Root cause: every routing gate was PROVIDER-level ("any stored credential exists") while OAuth credentials are MODEL-level (ChatGPT OAuth serves only Codex ids, Kimi OAuth only the Kimi Code catalog, gemini OAuth nothing at all since the Cloud Code Assist masquerade was removed) — so routing happily picked models the stored login could never serve, and the turn then failed at call time asking for `<PROVIDER>_API_KEY`. Also restored a missing export that had broken `bun run typecheck` on HEAD.
263
268
  - **[0.8.1]** (2026-07-08) — Fix-forward for a functional regression this same session shipped in v0.8.0. This repo's working tree is shared with a concurrent interactive session; `git add`-ing `src/ai/provider-status.ts` for v0.8.0's routing-veto-gate fix also swept up that session's already-modified, uncommitted copy of the SAME file (containing an unrelated design change: gemini OAuth alone no longer serving `google/gemini-*` models) without its companion piece in `src/auth/flows/index.ts` — so v0.8.0 shipped with a test suite (`provider-status.test.ts`) asserting behavior whose supporting data flag never landed. CI caught this immediately post-release: 2 failing tests on the exact tagged commit. Investigated, confirmed the missing piece was real (and independently observable live via `jeo doctor` before this session had made ANY of its own edits — not a stray/speculative change), and completed it.
264
269
  - **[0.8.0]** (2026-07-08) — Root-caused a genuine reliability bug in the routing veto gate ("라우트가 동작후 api 키인증오류가나오는데 근본적인 원인 개선해"), then regenerated the animated README mascot to visualize the smart cross-provider routing this release cycle has been building: prompt routing that finds the cheapest usable path, with a "money saver" visual motif.
265
270
  - **[0.7.58]** (2026-07-07) — Applied the previous entry's `routing.crossProviderPool` to the real live session config, then deep-researched every catalogued model's actual public release date (2026-07 sourced, per-provider parallel research with citations) to answer "모델 설정은 2026년도 기준으로 딥서치해서 모델리스트 최신순으로 정렬" — while researching, found and fixed a genuine ranking bug this surfaced: `strongestCredentialed`'s tiebreak on a full capability tie (identical thinking/output/context) fell through to a raw alphabetical canonical-id compare, which silently picked the OLDEST model in a same-spec family (`claude-opus-4-6` over `claude-opus-4-7`/`4-8`, purely because `"4-6" < "4-8"` as a string) — confirmed reproducible before the fix, confirmed corrected after.
266
- - **[0.7.57]** (2026-07-07) — "동급모델간 크로스 프로바이더 라우팅이 동작하게 개선" — v0.7.56's tier auto-select was single-winner by design (always the one cheapest/strongest model), so a user with `roles.smol` pinned (or simply multiple credentialed providers) never saw actual distribution across equivalent-class models. Deep-research-informed redesign: production LLM routers (LiteLLM, Bifrost/VoidLLM) model this as a named "model group" spanning provider deployments with a distribution strategy, not a single deterministic pick — jeo now supports both, opt-in.
267
- - **[0.7.56]** (2026-07-07) — "서로다른 프로바이더로 급에 따라 나눠서 설정하도록 개선" — PromptRouter's `trivial`/`complex` tiers previously collapsed to `defaultModel` whenever `roles.smol`/`roles.slow`/`routing.tiers.*` were left unconfigured (the documented "safe no-op absent configuration" contract), which meant a user with multiple providers credentialed never actually got cross-provider routing without hand-picking a model per tier. Also the long-term ask: routing must stay correct as jeo's model catalog evolves, never a hand-maintained tier→model table that goes stale.
268
271
 
269
272
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
270
273
  <!-- CHANGELOG:END -->
package/README.md CHANGED
@@ -38,7 +38,7 @@ Run `jeo` inside a repository and it reads files, edits them, runs commands, and
38
38
 
39
39
  ## Highlights
40
40
 
41
- - **Multi-provider, one loop** — Anthropic / OpenAI (+Codex) / Gemini / Antigravity / Ollama / LM Studio, plus 20+ OpenAI- and Anthropic-compatible clouds (Groq, DeepSeek, Mistral, OpenRouter, xAI, Kimi, z.ai, …), all behind one uniform JSON tool loop. OAuth login from the input box (`/provider login`), every model pick persists as the new default.
41
+ - **Multi-provider, one loop** — Anthropic / OpenAI (+Codex) / Gemini / Antigravity / Ollama / LM Studio, plus 20+ OpenAI- and Anthropic-compatible clouds (Groq, DeepSeek, Mistral, OpenRouter, xAI, Kimi, z.ai, …), all behind one uniform JSON tool loop. OAuth login happens from the input box (`/provider login`), every model pick persists as the new default, and prompt routing only auto-selects usable credentialed paths: Gemini OAuth goes through the provider-qualified `antigravity/*` agent set (Gemini 3.5 Flash tiers, Gemini 3.1 Pro, Claude Sonnet/Opus 4.6), never public `google/gemini-*` rows that require `GEMINI_API_KEY`.
42
42
  - **Edit integrity** — read output carries content anchors (`42ab|`); anchored edits are verified against the current file, re-mapped when lines shifted, and rejected with fresh content instead of corrupting.
43
43
  - **Self-correcting verification loop** — configure a post-edit hook (tsc / eslint / tests) and the agent *sees* the diagnostics and fixes them in-loop; a red hook blocks `done` until resolved.
44
44
  - **Real gates, no theater** — `ralplan` consensus is a repo-grounded critic subagent whose `[OKAY]` verdict is persisted and *required* by `jeo approve`; `ultragoal` reports honestly (a suite run is a global signal, never fabricated per-criterion passes).
@@ -80,11 +80,12 @@ Inside the `jeo` REPL (Tab autocompletes; `/` opens the palette).
80
80
  | `/provider login <name>` · `/logout` | OAuth login/logout from the input box |
81
81
  | `/agents [role]` · `/subagent` | Per-role (executor/planner/architect/critic) model · thinking · step config |
82
82
  | `/thinking [level]` | Show/set default reasoning budget (low…xhigh) |
83
- | `/fast [on|off|status]` | Toggle fast thinking mode when the active model advertises low reasoning |
83
+ | `/route [status\|on\|off\|why]` | Toggle prompt-based model routing for this session · explain the last routing decision (auto-routes each turn to a tier-appropriate model among the models your configured credentials — OAuth or API key — actually serve) |
84
+ | `/fast [on\|off\|status]` | Toggle fast thinking mode when the active model advertises low reasoning |
84
85
  | `/skill` · `$<skill> [intent]` | List/run workflow skills (`$team "task"` style) |
85
86
  | `/view` · `/diff` · `/find` · `/search` | Code view, git diff, file/pattern search |
86
87
  | `/new` · `/resume` · `/sessions` | Session management |
87
- | `/history [n|all]` · `/export` | Reprint readable worked activity history into scrollback · transcript export |
88
+ | `/history [n\|all]` · `/export` | Reprint readable worked activity history into scrollback · transcript export |
88
89
  | `/retry` · `/btw <q>` | Retry last request · side question without touching history |
89
90
  | `/usage` · `/context` · `/compact` | Token usage, context breakdown, manual compaction |
90
91
  | `/theme` · `/config` · `/help` | Theme, runtime config, help |
@@ -264,11 +265,11 @@ Huge thanks to [gajae-code](https://github.com/Yeachan-Heo/gajae-code) for the i
264
265
  ## Changelog
265
266
 
266
267
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
268
+ - **[0.8.3]** (2026-07-08) — "잘못된 모델 설정하는 경우가 있는데" — the static Antigravity catalog had drifted badly from the LIVE Cloud Code Assist agent backend, and several current wire ids actively LIE about their tier: `gemini-3-flash-agent` is really "Gemini 3.5 Flash (High)" (the flagship agent model, which the name-suffix heuristic filed under the SMALL flash tier), `gemini-pro-agent` is really "Gemini 3.1 Pro (High)" (the code-agent model), and `gemini-3.1-pro-high` is DEPRECATED by the backend in favor of `gemini-pro-agent`. Probed the real `fetchAvailableModels` response (displayNames + agentModelSorts + deprecatedModelIds) and made it the single source of truth.
269
+ - **[0.8.2]** (2026-07-08) — "프롬프트라우팅이 api 입력을 요구하면서 동작안해" — prompt routing kept demanding an API key even when the user was already logged in via OAuth (GPT showed the same symptom). Root cause: every routing gate was PROVIDER-level ("any stored credential exists") while OAuth credentials are MODEL-level (ChatGPT OAuth serves only Codex ids, Kimi OAuth only the Kimi Code catalog, gemini OAuth nothing at all since the Cloud Code Assist masquerade was removed) — so routing happily picked models the stored login could never serve, and the turn then failed at call time asking for `<PROVIDER>_API_KEY`. Also restored a missing export that had broken `bun run typecheck` on HEAD.
267
270
  - **[0.8.1]** (2026-07-08) — Fix-forward for a functional regression this same session shipped in v0.8.0. This repo's working tree is shared with a concurrent interactive session; `git add`-ing `src/ai/provider-status.ts` for v0.8.0's routing-veto-gate fix also swept up that session's already-modified, uncommitted copy of the SAME file (containing an unrelated design change: gemini OAuth alone no longer serving `google/gemini-*` models) without its companion piece in `src/auth/flows/index.ts` — so v0.8.0 shipped with a test suite (`provider-status.test.ts`) asserting behavior whose supporting data flag never landed. CI caught this immediately post-release: 2 failing tests on the exact tagged commit. Investigated, confirmed the missing piece was real (and independently observable live via `jeo doctor` before this session had made ANY of its own edits — not a stray/speculative change), and completed it.
268
271
  - **[0.8.0]** (2026-07-08) — Root-caused a genuine reliability bug in the routing veto gate ("라우트가 동작후 api 키인증오류가나오는데 근본적인 원인 개선해"), then regenerated the animated README mascot to visualize the smart cross-provider routing this release cycle has been building: prompt routing that finds the cheapest usable path, with a "money saver" visual motif.
269
272
  - **[0.7.58]** (2026-07-07) — Applied the previous entry's `routing.crossProviderPool` to the real live session config, then deep-researched every catalogued model's actual public release date (2026-07 sourced, per-provider parallel research with citations) to answer "모델 설정은 2026년도 기준으로 딥서치해서 모델리스트 최신순으로 정렬" — while researching, found and fixed a genuine ranking bug this surfaced: `strongestCredentialed`'s tiebreak on a full capability tie (identical thinking/output/context) fell through to a raw alphabetical canonical-id compare, which silently picked the OLDEST model in a same-spec family (`claude-opus-4-6` over `claude-opus-4-7`/`4-8`, purely because `"4-6" < "4-8"` as a string) — confirmed reproducible before the fix, confirmed corrected after.
270
- - **[0.7.57]** (2026-07-07) — "동급모델간 크로스 프로바이더 라우팅이 동작하게 개선" — v0.7.56's tier auto-select was single-winner by design (always the one cheapest/strongest model), so a user with `roles.smol` pinned (or simply multiple credentialed providers) never saw actual distribution across equivalent-class models. Deep-research-informed redesign: production LLM routers (LiteLLM, Bifrost/VoidLLM) model this as a named "model group" spanning provider deployments with a distribution strategy, not a single deterministic pick — jeo now supports both, opt-in.
271
- - **[0.7.56]** (2026-07-07) — "서로다른 프로바이더로 급에 따라 나눠서 설정하도록 개선" — PromptRouter's `trivial`/`complex` tiers previously collapsed to `defaultModel` whenever `roles.smol`/`roles.slow`/`routing.tiers.*` were left unconfigured (the documented "safe no-op absent configuration" contract), which meant a user with multiple providers credentialed never actually got cross-provider routing without hand-picking a model per tier. Also the long-term ask: routing must stay correct as jeo's model catalog evolves, never a hand-maintained tier→model table that goes stale.
272
273
 
273
274
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
274
275
  <!-- CHANGELOG:END -->
package/README.zh.md CHANGED
@@ -38,7 +38,7 @@
38
38
 
39
39
  ## 亮点
40
40
 
41
- - **多提供商、单一循环** — Anthropic / OpenAI(+Codex) / Gemini / Antigravity / Ollama / LM Studio,以及 20+ 个 OpenAI、Anthropic 兼容云服务(Groq、DeepSeek、Mistral、OpenRouter、xAI、Kimi、z.ai 等),统一在一个 JSON 工具循环中。输入框内直接 OAuth 登录(`/provider login`),模型选择即刻持久化为默认值。
41
+ - **多提供商、单一循环** — Anthropic / OpenAI(+Codex) / Gemini / Antigravity / Ollama / LM Studio,以及 20+ 个 OpenAI、Anthropic 兼容云服务(Groq、DeepSeek、Mistral、OpenRouter、xAI、Kimi、z.ai 等),统一在一个 JSON 工具循环中。输入框内直接 OAuth 登录(`/provider login`),模型选择即刻持久化为默认值。Prompt routing 只会自动选择真实可用的凭据路径: Gemini OAuth 会走 provider-qualified 的 `antigravity/*` 代理模型集(Gemini 3.5 Flash 各档、Gemini 3.1 Pro、Claude Sonnet/Opus 4.6),绝不选择需要 `GEMINI_API_KEY` 的 public `google/gemini-*` 行。
42
42
  - **编辑完整性** — read 输出携带内容锚点(`42ab|`);带锚点的编辑会与当前文件校验、行移动时自动重映射、不匹配时连同最新内容一起拒绝 — 绝不污染文件。
43
43
  - **自我修正的验证循环** — 配置 post-edit 钩子(tsc / eslint / 测试),代理会*亲自读取*诊断并在循环内修复;钩子未通过时 `done` 会被阻断。
44
44
  - **没有表演的真实门禁** — `ralplan` 共识由真正读取仓库的 critic 子代理执行,`[OKAY]` 裁决被持久化且 `jeo approve` *强制要求*它;`ultragoal` 诚实报告(套件运行只是全局信号,绝不伪造逐条通过)。
@@ -46,7 +46,7 @@
46
46
  - **动态步数预算** — 只要近期工具调用展现新的进展就持续延长,停滞时优雅收敛为总结;子代理保持精确的步数契约。
47
47
  - **内联 TUI** — 已完成的工作流入真实滚动缓冲区(回合中也可用 tmux 滚轮),代理运行时普通查询输入框仍保持可见并可编辑。Ctrl+O 详细信息切换、主题、剪贴板图片粘贴(Ctrl+V)、CJK/表情安全的宽度计算。
48
48
  - **浏览器工具** — 基于 Playwright 的无头 Chromium 自动化,作为一等代理工具:对命名并复用的标签页执行 `open`/`close`/`run`/`act`,优先使用 `observe` 标记的元素 id 而非截图来驱动页面。需要执行一次 `npx playwright install chromium`(未捆绑 — jeo 本身仍是零原生依赖,浏览器二进制文件是 Playwright 的独立下载项)。
49
- - **远程子代理可见性(Telegram)** — 配对一次机器人(`jeo notify setup`)后,`jeo daemon start` 会在子代理每次状态变化(启动 → 完成/失败/取消)时推送消息,并接受 `/subagents`、`/steer <id> <subagentId> <msg>`、`/cancel <id> <subagentId>` 回传 — 命令仅对配对的聊天授权。
49
+ - **远程子代理可见性(Telegram)** — 配对一次机器人(`jeo notify setup`)后,`jeo daemon start` 会在子代理每次状态变化(启动 → 完成/失败/取消)时推送消息,并接受 `/subagents`、`/steer <id> <subagentId> <msg>`、`/cancel <id> <subagentId>` 回传。现在提供 Telegram 论坛主题、内联键盘、图片附件等完整 `gjc` parity;命令仅对配对的聊天授权。
50
50
 
51
51
  ## 安装
52
52
 
@@ -79,11 +79,12 @@ jeo --tmux # 在独立 tmux 会话中运行
79
79
  | `/provider login <name>` · `/logout` | 在输入框内 OAuth 登录/登出 |
80
80
  | `/agents [role]` · `/subagent` | 按角色(executor/planner/architect/critic)配置模型·thinking·步数 |
81
81
  | `/thinking [level]` | 查看/设置默认推理预算(low…xhigh) |
82
- | `/fast [on|off|status]` | 当前模型支持 low 推理时切换 fast thinking 模式 |
82
+ | `/route [status\|on\|off\|why]` | 切换本会话的基于提示词的模型路由 · 解释最近一次路由决策(仅在已配置凭证 — OAuth 或 API 密钥 — 实际可用的模型内自动路由) |
83
+ | `/fast [on\|off\|status]` | 当前模型支持 low 推理时切换 fast thinking 模式 |
83
84
  | `/skill` · `$<skill> [intent]` | 列出/运行工作流技能(`$team "任务"` 风格) |
84
85
  | `/view` · `/diff` · `/find` · `/search` | 代码查看、git diff、文件/模式搜索 |
85
86
  | `/new` · `/resume` · `/sessions` | 会话管理 |
86
- | `/history [n|all]` · `/export` | 将可读的工作活动历史重新输出到滚动区 · 导出记录 |
87
+ | `/history [n\|all]` · `/export` | 将可读的工作活动历史重新输出到滚动区 · 导出记录 |
87
88
  | `/retry` · `/btw <问题>` | 重试上次请求 · 不写入历史的旁路提问 |
88
89
  | `/usage` · `/context` · `/compact` | Token 用量、上下文明细、手动压缩 |
89
90
  | `/theme` · `/config` · `/help` | 主题、运行时配置、帮助 |
@@ -212,8 +213,10 @@ JEO_TUI_THEME=cosmic # cosmic/matrix/solar/red-claw/blue-crab/mono/au
212
213
  JEO_TUI_ALT_SCREEN=1 # 旧版 alt-screen 回合(默认: 内联滚动缓冲)
213
214
  JEO_STEP_BASE=24 # 动态步数预算的滚动基数
214
215
  JEO_STEP_HARD_CAP=600 # 绝对终止保证
215
- JEO_STREAM_MAX_MS=300000 # 可选的整体流截止(默认关闭; 约束慢滴流)
216
+ JEO_STREAM_MAX_MS=1800000 # 整体流截止(默认30分钟; 约束 slow-drip 流,不是为了中断仍在活跃输出的流); 0 表示禁用
216
217
  JEO_STREAM_IDLE_MS=300000 # 单次分块空闲上限(默认300秒); 首个 token 前静默较久的慢速/本地后端可调高
218
+ JEO_CALL_TIMEOUT_MS=1800000 # 非流式调用墙钟上限(默认30分钟; compaction/subagents/goal-verify)
219
+ JEO_TURN_MAX_MS=1800000 # 回合停滞预算: 没有工具进展的最长时间(默认30分钟); 0 表示禁用
217
220
  JEO_TOOL_OUTPUT_MAX=4000 # 模型可见的工具输出上限(全文溢出到 artifacts)
218
221
  ```
219
222
 
@@ -259,11 +262,11 @@ CI 通过 `.github/workflows/npm-publish.yml` 发布 — GitHub 发布 release
259
262
  ## 更新日志 (Changelog)
260
263
 
261
264
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
265
+ - **[0.8.3]** (2026-07-08) — "잘못된 모델 설정하는 경우가 있는데" — the static Antigravity catalog had drifted badly from the LIVE Cloud Code Assist agent backend, and several current wire ids actively LIE about their tier: `gemini-3-flash-agent` is really "Gemini 3.5 Flash (High)" (the flagship agent model, which the name-suffix heuristic filed under the SMALL flash tier), `gemini-pro-agent` is really "Gemini 3.1 Pro (High)" (the code-agent model), and `gemini-3.1-pro-high` is DEPRECATED by the backend in favor of `gemini-pro-agent`. Probed the real `fetchAvailableModels` response (displayNames + agentModelSorts + deprecatedModelIds) and made it the single source of truth.
266
+ - **[0.8.2]** (2026-07-08) — "프롬프트라우팅이 api 입력을 요구하면서 동작안해" — prompt routing kept demanding an API key even when the user was already logged in via OAuth (GPT showed the same symptom). Root cause: every routing gate was PROVIDER-level ("any stored credential exists") while OAuth credentials are MODEL-level (ChatGPT OAuth serves only Codex ids, Kimi OAuth only the Kimi Code catalog, gemini OAuth nothing at all since the Cloud Code Assist masquerade was removed) — so routing happily picked models the stored login could never serve, and the turn then failed at call time asking for `<PROVIDER>_API_KEY`. Also restored a missing export that had broken `bun run typecheck` on HEAD.
262
267
  - **[0.8.1]** (2026-07-08) — Fix-forward for a functional regression this same session shipped in v0.8.0. This repo's working tree is shared with a concurrent interactive session; `git add`-ing `src/ai/provider-status.ts` for v0.8.0's routing-veto-gate fix also swept up that session's already-modified, uncommitted copy of the SAME file (containing an unrelated design change: gemini OAuth alone no longer serving `google/gemini-*` models) without its companion piece in `src/auth/flows/index.ts` — so v0.8.0 shipped with a test suite (`provider-status.test.ts`) asserting behavior whose supporting data flag never landed. CI caught this immediately post-release: 2 failing tests on the exact tagged commit. Investigated, confirmed the missing piece was real (and independently observable live via `jeo doctor` before this session had made ANY of its own edits — not a stray/speculative change), and completed it.
263
268
  - **[0.8.0]** (2026-07-08) — Root-caused a genuine reliability bug in the routing veto gate ("라우트가 동작후 api 키인증오류가나오는데 근본적인 원인 개선해"), then regenerated the animated README mascot to visualize the smart cross-provider routing this release cycle has been building: prompt routing that finds the cheapest usable path, with a "money saver" visual motif.
264
269
  - **[0.7.58]** (2026-07-07) — Applied the previous entry's `routing.crossProviderPool` to the real live session config, then deep-researched every catalogued model's actual public release date (2026-07 sourced, per-provider parallel research with citations) to answer "모델 설정은 2026년도 기준으로 딥서치해서 모델리스트 최신순으로 정렬" — while researching, found and fixed a genuine ranking bug this surfaced: `strongestCredentialed`'s tiebreak on a full capability tie (identical thinking/output/context) fell through to a raw alphabetical canonical-id compare, which silently picked the OLDEST model in a same-spec family (`claude-opus-4-6` over `claude-opus-4-7`/`4-8`, purely because `"4-6" < "4-8"` as a string) — confirmed reproducible before the fix, confirmed corrected after.
265
- - **[0.7.57]** (2026-07-07) — "동급모델간 크로스 프로바이더 라우팅이 동작하게 개선" — v0.7.56's tier auto-select was single-winner by design (always the one cheapest/strongest model), so a user with `roles.smol` pinned (or simply multiple credentialed providers) never saw actual distribution across equivalent-class models. Deep-research-informed redesign: production LLM routers (LiteLLM, Bifrost/VoidLLM) model this as a named "model group" spanning provider deployments with a distribution strategy, not a single deterministic pick — jeo now supports both, opt-in.
266
- - **[0.7.56]** (2026-07-07) — "서로다른 프로바이더로 급에 따라 나눠서 설정하도록 개선" — PromptRouter's `trivial`/`complex` tiers previously collapsed to `defaultModel` whenever `roles.smol`/`roles.slow`/`routing.tiers.*` were left unconfigured (the documented "safe no-op absent configuration" contract), which meant a user with multiple providers credentialed never actually got cross-provider routing without hand-picking a model per tier. Also the long-term ask: routing must stay correct as jeo's model catalog evolves, never a hand-maintained tier→model table that goes stale.
267
270
 
268
271
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
269
272
  <!-- CHANGELOG:END -->
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jeo-code",
3
- "version": "0.8.1",
3
+ "version": "0.8.3",
4
4
  "description": "Clean, highly optimized AI coding agent using spec-first loop",
5
5
  "type": "module",
6
6
  "main": "src/cli.ts",
@@ -48,8 +48,8 @@
48
48
  * contract for users who never opted in.
49
49
  */
50
50
  import { callLlm } from "./loop";
51
- import { resolveRoleModel } from "../ai/model-manager";
52
- import { catalogMetadata, MODEL_CATALOG, type CatalogModel } from "../ai/model-catalog";
51
+ import { resolveRoleModel, modelServableWithConfig } from "../ai/model-manager";
52
+ import { catalogMetadata, MODEL_CATALOG, compareReleaseDate, type CatalogModel } from "../ai/model-catalog";
53
53
  import { priceForModel } from "../ai/pricing";
54
54
  import { tryExtractJsonObject } from "./json";
55
55
  import type { Config } from "./state";
@@ -211,18 +211,41 @@ function isCloudProvider(p: ProviderName): p is AuthProvider {
211
211
  return p !== "ollama" && p !== "lmstudio";
212
212
  }
213
213
 
214
+
215
+ /** Antigravity OAuth is the Cloud Code Assist lane for Gemini-family OAuth routing.
216
+ * When present, auto-select must prefer provider-qualified `antigravity/*` ids and
217
+ * never silently fall back to public `gemini`/`google-gemini` catalog rows that need
218
+ * a Gemini API key at execution time. A plain `oauth.gemini` token counts because
219
+ * Antigravity deliberately accepts it as a fallback credential. */
220
+ function hasAntigravityOauth(config: RoutingConfig): boolean {
221
+ return !!(config.oauth?.antigravity || config.oauth?.gemini);
222
+ }
223
+
224
+ function isAutoSelectCandidate(m: CatalogModel, config: RoutingConfig): boolean {
225
+ // Shared model-LEVEL credential gate (the full auth matrix is documented on
226
+ // modelServableWithConfig; keep aligned with resolveCall).
227
+ if (!isCloudProvider(m.provider) || m.limitedAvailability || !modelServableWithConfig(m.provider, m.canonical, config)) return false;
228
+ if (hasAntigravityOauth(config) && m.provider === "gemini") return false;
229
+ return true;
230
+ }
231
+
232
+
214
233
  /** Size class jeo infers for a catalog model, used to group EQUIVALENT models
215
234
  * across DIFFERENT providers for pool-based routing (see `tierModelPool`) — the
216
235
  * cross-provider counterpart to `cheapestCredentialed`/`strongestCredentialed`'s
217
- * single-winner selection. Matches PROVIDER-DECLARED size-tier suffixes
218
- * (Anthropic's haiku/sonnet/opus, Google's flash/pro, OpenAI's mini) against
236
+ * single-winner selection. An explicit catalog `sizeClass` wins outright — the
237
+ * authoritative tier for ids whose NAME misleads the suffix heuristic (e.g.
238
+ * Antigravity's `gemini-3-flash-agent` is "Gemini 3.5 Flash (High)", a flagship).
239
+ * Otherwise matches PROVIDER-DECLARED size-tier suffixes (Anthropic's
240
+ * haiku/sonnet/opus, Google's flash/pro, OpenAI's mini) against
219
241
  * hyphen/dot-delimited SEGMENTS of the canonical id — NOT a raw substring match,
220
242
  * which would false-positive on "gemini" containing "mini" as a substring.
221
243
  * `null` for ids with no size-tier suffix at all (gpt-5.5, o3, grok-4.3, kimi-*,
222
244
  * glm-*, deepseek-*, minimax-* — providers whose naming doesn't encode size);
223
245
  * `tierModelPool` falls those back to a strength-tercile split instead. */
224
- function sizeClassFor(canonical: string): "small" | "mid" | "large" | null {
225
- const id = canonical.toLowerCase();
246
+ function sizeClassFor(model: CatalogModel): "small" | "mid" | "large" | null {
247
+ if (model.sizeClass) return model.sizeClass;
248
+ const id = model.canonical.toLowerCase();
226
249
  const bare = id.includes("/") ? id.slice(id.indexOf("/") + 1) : id;
227
250
  const segments = bare.split(/[-.]/);
228
251
  if (segments.includes("haiku") || segments.includes("flash") || segments.includes("mini") || segments.includes("nano")) return "small";
@@ -233,21 +256,6 @@ function sizeClassFor(canonical: string): "small" | "mid" | "large" | null {
233
256
 
234
257
  const TIER_TO_SIZE_CLASS: Record<PromptTier, "small" | "mid" | "large"> = { trivial: "small", standard: "mid", high: "mid", complex: "large" };
235
258
 
236
- /** Compares `releaseDate` ("YYYY-MM", lexicographically sortable). `>0` when `a`
237
- * is NEWER than `b`. A missing date sorts as OLDEST on that side — an
238
- * unconfirmed date can never silently outrank a verified-newer model. Shared
239
- * recency tiebreak for `compareStrengthAscending`/`cheapestCredentialed`/
240
- * `strongestCredentialed`, replacing the arbitrary alphabetical canonical-id
241
- * tiebreak those used before (which, on a full capability tie, picked the
242
- * OLDEST id alphabetically — e.g. `claude-opus-4-6` beating `claude-opus-4-8`
243
- * as "strongest" purely because "4-6" < "4-8" as a string). */
244
- function compareRecency(a: CatalogModel, b: CatalogModel): number {
245
- if (a.releaseDate === b.releaseDate) return 0;
246
- if (!a.releaseDate) return -1;
247
- if (!b.releaseDate) return 1;
248
- return a.releaseDate < b.releaseDate ? -1 : 1;
249
- }
250
-
251
259
  /** Multi-key strength comparator (ascending: weakest first) — same ranking signals
252
260
  * as `strongestCredentialed`'s single-winner tiebreak, reused here to bucket
253
261
  * UNCLASSIFIED (no size-suffix) models into a tercile fallback. */
@@ -257,7 +265,7 @@ function compareStrengthAscending(a: CatalogModel, b: CatalogModel): number {
257
265
  if (xhighA !== xhighB) return xhighA - xhighB;
258
266
  if (a.maxOutputTokens !== b.maxOutputTokens) return a.maxOutputTokens - b.maxOutputTokens;
259
267
  if (a.contextTokens !== b.contextTokens) return a.contextTokens - b.contextTokens;
260
- const recency = compareRecency(a, b);
268
+ const recency = compareReleaseDate(a.releaseDate, b.releaseDate);
261
269
  if (recency !== 0) return recency;
262
270
  return a.canonical < b.canonical ? -1 : a.canonical > b.canonical ? 1 : 0;
263
271
  }
@@ -272,11 +280,11 @@ function compareStrengthAscending(a: CatalogModel, b: CatalogModel): number {
272
280
  * for deterministic pool ordering (required by `selectFromPool`'s index math). */
273
281
  export function tierModelPool(tier: PromptTier, config: RoutingConfig): string[] {
274
282
  const targetClass = TIER_TO_SIZE_CLASS[tier];
275
- const credentialed = MODEL_CATALOG.filter(m => isCloudProvider(m.provider) && !m.limitedAvailability && (config.providers?.[m.provider] || config.oauth?.[m.provider]));
283
+ const credentialed = MODEL_CATALOG.filter(m => isAutoSelectCandidate(m, config));
276
284
 
277
- const suffixPool = credentialed.filter(m => sizeClassFor(m.canonical) === targetClass);
285
+ const suffixPool = credentialed.filter(m => sizeClassFor(m) === targetClass);
278
286
 
279
- const unclassified = credentialed.filter(m => sizeClassFor(m.canonical) === null).sort(compareStrengthAscending);
287
+ const unclassified = credentialed.filter(m => sizeClassFor(m) === null).sort(compareStrengthAscending);
280
288
  const third = Math.ceil(unclassified.length / 3);
281
289
  // "high" shares "standard"'s unclassified-model tercile share: the catalog's
282
290
  // size-suffix classification only has 3 real buckets (small/mid/large — see
@@ -322,12 +330,12 @@ export function cheapestCredentialed(config: RoutingConfig): string | null {
322
330
  let best: CatalogModel | null = null;
323
331
  let bestCost = Infinity;
324
332
  for (const m of MODEL_CATALOG) {
325
- // "ANY stored credential" (API key OR OAuth) is enough here — auto-select only
326
- // needs "can this provider serve a request at all", not WHICH credential kind
327
- // wins (unlike provider-status.ts's `configuredCredential` precedence). The
328
- // real veto gate in launch.ts's `runTurn` re-verifies full readiness (including
329
- // live OAuth expiry) before a turn actually uses the model.
330
- if (!isCloudProvider(m.provider) || m.limitedAvailability || !(config.providers?.[m.provider] || config.oauth?.[m.provider])) continue;
333
+ // Candidate eligibility is centralized in `isAutoSelectCandidate`: it keeps
334
+ // OAuth-backed Antigravity routing on provider-qualified 3.1+ Gemini rows and
335
+ // prevents public `gemini` catalog rows from winning when Antigravity OAuth is
336
+ // the available Gemini lane. The real veto gate in launch.ts's `runTurn` still
337
+ // re-verifies full readiness (including live OAuth expiry) before a turn uses it.
338
+ if (!isAutoSelectCandidate(m, config)) continue;
331
339
  const price = priceForModel(m.canonical);
332
340
  if (!price) continue;
333
341
  const cost = price.inPerM + price.outPerM;
@@ -335,8 +343,18 @@ export function cheapestCredentialed(config: RoutingConfig): string | null {
335
343
  best = m;
336
344
  bestCost = cost;
337
345
  } else if (cost === bestCost && best) {
338
- const recency = compareRecency(m, best);
339
- if (recency > 0 || (recency === 0 && m.canonical < best.canonical)) best = m;
346
+ // Same price (common inside one subscription family, e.g. every Antigravity
347
+ // Gemini row shares the flat "gemini" price): prefer the SMALLER declared
348
+ // size class first — "cheapest" is the trivial tier's pick, and a family's
349
+ // Low/(High) variants are priced identically while consuming very different
350
+ // quota — then NEWER release, then canonical id (deterministic).
351
+ const sizeRank = (m2: CatalogModel): number => {
352
+ const cls = sizeClassFor(m2);
353
+ return cls === "small" ? 0 : cls === "mid" ? 1 : cls === "large" ? 2 : 3;
354
+ };
355
+ const bySize = sizeRank(m) - sizeRank(best);
356
+ const recency = compareReleaseDate(m.releaseDate, best.releaseDate);
357
+ if (bySize < 0 || (bySize === 0 && (recency > 0 || (recency === 0 && m.canonical < best.canonical)))) best = m;
340
358
  }
341
359
  }
342
360
  return best?.canonical ?? null;
@@ -360,8 +378,9 @@ export function strongestCredentialed(
360
378
  ): string | null {
361
379
  let best: CatalogModel | null = null;
362
380
  for (const m of MODEL_CATALOG) {
363
- // "ANY stored credential" is enough (see cheapestCredentialed's comment above).
364
- if (!isCloudProvider(m.provider) || m.limitedAvailability || !(config.providers?.[m.provider] || config.oauth?.[m.provider])) continue;
381
+ // Candidate eligibility is centralized in `isAutoSelectCandidate` (see
382
+ // cheapestCredentialed above for the Antigravity/Gemini OAuth rationale).
383
+ if (!isAutoSelectCandidate(m, config)) continue;
365
384
  if (filter && !filter(m)) continue;
366
385
  if (!best) { best = m; continue; }
367
386
  const xhighM = m.thinking.includes("xhigh") ? 1 : 0;
@@ -369,7 +388,7 @@ export function strongestCredentialed(
369
388
  if (xhighM !== xhighBest) { if (xhighM > xhighBest) best = m; continue; }
370
389
  if (m.maxOutputTokens !== best.maxOutputTokens) { if (m.maxOutputTokens > best.maxOutputTokens) best = m; continue; }
371
390
  if (m.contextTokens !== best.contextTokens) { if (m.contextTokens > best.contextTokens) best = m; continue; }
372
- const recency = compareRecency(m, best);
391
+ const recency = compareReleaseDate(m.releaseDate, best.releaseDate);
373
392
  if (recency !== 0) { if (recency > 0) best = m; continue; }
374
393
  if (m.canonical < best.canonical) best = m;
375
394
  }
@@ -388,7 +407,7 @@ export function strongestCredentialed(
388
407
  * `roles.medium`/`defaultModel` — never to `complex`'s flagship pick). */
389
408
  function strongestMidTierCredentialed(config: RoutingConfig): string | null {
390
409
  const midClass = MODEL_CATALOG.filter(
391
- m => isCloudProvider(m.provider) && !m.limitedAvailability && sizeClassFor(m.canonical) === "mid" && (config.providers?.[m.provider] || config.oauth?.[m.provider]),
410
+ m => isAutoSelectCandidate(m, config) && sizeClassFor(m) === "mid",
392
411
  );
393
412
  if (midClass.length === 0) return null;
394
413
  return midClass.sort(compareStrengthAscending).at(-1)!.canonical;
@@ -30,7 +30,7 @@ export interface ModelCatalogEntry {
30
30
  * actual top tier, claude-fable-5, is pricier than the "recommended" sonnet-5 —
31
31
  * recommended means "solid everyday default", not "strongest"). Re-verify and
32
32
  * update whenever a provider ships a new default-tier release. */
33
- const RECOMMENDED: Record<string, true> = { "claude-sonnet-5": true, "gpt-5.5": true, "gemini-3-flash": true, "antigravity/gpt-5.5": true, "qwen2.5": true };
33
+ const RECOMMENDED: Record<string, true> = { "claude-sonnet-5": true, "gpt-5.5": true, "gemini-3-flash": true, "antigravity/gemini-pro-agent": true, "qwen2.5": true };
34
34
 
35
35
  export function normalizeModelId(id: string): string {
36
36
  return (id ?? "").trim().toLowerCase();
@@ -45,34 +45,31 @@ export interface CatalogModel {
45
45
  * date — these sort AFTER every dated entry within their block, never before,
46
46
  * so an unconfirmed date can never silently outrank a verified-newer model. */
47
47
  releaseDate?: string;
48
+ /** Explicit routing size class, for ids whose NAME misleads the suffix
49
+ * heuristic in prompt-router.ts's `sizeClassFor` (e.g. Antigravity's wire id
50
+ * `gemini-3-flash-agent` is actually "Gemini 3.5 Flash (High)" — a flagship,
51
+ * not a small flash tier). Unset = derive from the name suffix as before. */
52
+ sizeClass?: "small" | "mid" | "large";
48
53
  }
49
54
 
50
55
  const FULL: ThinkLevel[] = ["low", "medium", "high", "xhigh"];
51
56
  const STD: ThinkLevel[] = ["low", "medium", "high"];
52
57
 
53
- // [canonical model id, release YYYY-MM] tuples, deep-research sourced (Anthropic/
54
- // OpenAI/Google base-model announcement dates — antigravity is a hosting layer over
55
- // the SAME underlying models, not a separate release). Newest-first per family below;
56
- // -thinking/-high/-low suffixes are reasoning-effort variants of the SAME release.
57
- export const ANTIGRAVITY_MODELS = [
58
- ["gpt-5.5", "2026-04"],
59
- ["gemini-3.1-pro-high", "2026-02"],
60
- ["gemini-3.1-pro-low", "2026-02"],
61
- ["claude-opus-4-8", "2026-05"],
62
- ["claude-opus-4-8-thinking", "2026-05"],
63
- ["claude-opus-4-7", "2026-04"],
64
- ["claude-opus-4-7-thinking", "2026-04"],
65
- ["claude-sonnet-4-6", "2026-02"],
66
- ["claude-sonnet-4-6-thinking", "2026-02"],
67
- ["claude-opus-4-6-thinking", "2026-02"],
68
- ["gemini-3-flash", "2025-12"],
69
- ["gemini-3-pro-high", "2025-11"],
70
- ["gemini-3-pro-low", "2025-11"],
71
- ["gpt-oss-120b-medium", "2025-08"],
72
- ["gemini-2.5-flash", "2025-06"],
73
- ["gemini-2.5-flash-thinking", "2025-06"],
74
- ["gemini-2.5-pro", "2025-03"],
75
- ] as const;
58
+ // Antigravity (Cloud Code Assist) agent-mode model rows, verified against the
59
+ // LIVE fetchAvailableModels response (agentModelSorts "Recommended" group +
60
+ // per-id displayName, probed 2026-07-08). CRITICAL: several wire ids do NOT
61
+ // mean what their name suggests — the displayName is the truth:
62
+ // gemini-3-flash-agent → "Gemini 3.5 Flash (High)" (flagship agent tier)
63
+ // gemini-3.5-flash-low → "Gemini 3.5 Flash (Medium)"
64
+ // gemini-3.5-flash-extra-low→ "Gemini 3.5 Flash (Low)"
65
+ // gemini-pro-agent → "Gemini 3.1 Pro (High)" (the code-agent model;
66
+ // replaces DEPRECATED gemini-3.1-pro-high, per the
67
+ // backend's deprecatedModelIds.newModelId mapping —
68
+ // BUILTIN_ALIASES keeps the old id working)
69
+ // claude-sonnet-4-6 → "Claude Sonnet 4.6 (Thinking)"
70
+ // `sizeClass` pins each row's routing tier to that display truth so the name
71
+ // suffix heuristic can't misfile them (releaseDate = the underlying base-model
72
+ // announcement; Antigravity is a hosting layer, not a separate release).
76
73
 
77
74
  /** A curated set of common public models with their documented capabilities.
78
75
  * Ordered NEWEST-FIRST within each provider block per `releaseDate`
@@ -139,21 +136,22 @@ export const MODEL_CATALOG: readonly CatalogModel[] = [
139
136
  { canonical: "gemini-2.5-pro", provider: "gemini", providerModel: "gemini-2.5-pro", contextTokens: 1_000_000, maxOutputTokens: 65_536, thinking: STD, images: true, releaseDate: "2025-03" },
140
137
  { canonical: "gemini-2.0-flash", provider: "gemini", providerModel: "gemini-2.0-flash", contextTokens: 1_000_000, maxOutputTokens: 8_192, thinking: [], images: true, releaseDate: "2025-02" },
141
138
  { canonical: "gemini-1.5-pro", provider: "gemini", providerModel: "gemini-1.5-pro", contextTokens: 1_000_000, maxOutputTokens: 8_192, thinking: [], images: true, releaseDate: "2024-02" },
142
- // Google Antigravity / Gemini CLI (Cloud Code Assist) — provider-qualified to avoid
143
- // collisions with public Gemini, Anthropic, and OpenAI/Codex ids. A hosting layer
144
- // over the SAME underlying models researched above, not a separate release —
145
- // releaseDate comes from the [id, date] tuple, not re-derived.
146
- ...ANTIGRAVITY_MODELS.map(([id, releaseDate]): CatalogModel => ({
147
- canonical: `antigravity/${id}`,
148
- provider: "antigravity",
149
- providerModel: id,
150
- contextTokens: id.includes("claude") ? 200_000 : id.startsWith("gpt-5") ? 400_000 : id.includes("gemini-3") ? 1_000_000 : 1_000_000,
151
- maxOutputTokens: id.includes("claude") ? 64_000 : id.startsWith("gpt-5") ? 128_000 : 65_536,
152
- thinking: id.includes("thinking") || id.includes("-high") || id.includes("-low") || id.includes("gemini-3") || id.startsWith("gpt-5") ? FULL : STD,
153
- images: !id.includes("gpt-oss"),
154
- company: id.includes("claude") ? "Anthropic via Antigravity" : id.includes("gpt") ? "OpenAI via Antigravity" : "Google Antigravity",
155
- releaseDate,
156
- })),
139
+ // Google Antigravity / Cloud Code Assist agent set — provider-qualified to avoid
140
+ // collisions with public Gemini/Anthropic ids. Rows mirror the LIVE
141
+ // fetchAvailableModels agent list (see the block comment above ANTIGRAVITY rows'
142
+ // sizeClass rationale near the top of this file). Newest first.
143
+ // Gemini 3.5 Flash tiers (base model released 2026-05, Google I/O):
144
+ { canonical: "antigravity/gemini-3-flash-agent", provider: "antigravity", providerModel: "gemini-3-flash-agent", contextTokens: 1_000_000, maxOutputTokens: 65_536, thinking: FULL, images: true, company: "Google Antigravity", releaseDate: "2026-05", sizeClass: "large" }, // "Gemini 3.5 Flash (High)" — the HIGH agent model
145
+ { canonical: "antigravity/gemini-3.5-flash-low", provider: "antigravity", providerModel: "gemini-3.5-flash-low", contextTokens: 1_000_000, maxOutputTokens: 65_536, thinking: FULL, images: true, company: "Google Antigravity", releaseDate: "2026-05", sizeClass: "mid" }, // "Gemini 3.5 Flash (Medium)"
146
+ { canonical: "antigravity/gemini-3.5-flash-extra-low", provider: "antigravity", providerModel: "gemini-3.5-flash-extra-low", contextTokens: 1_000_000, maxOutputTokens: 65_536, thinking: FULL, images: true, company: "Google Antigravity", releaseDate: "2026-05", sizeClass: "small" }, // "Gemini 3.5 Flash (Low)"
147
+ // Gemini 3.1 Pro tiers (base model released 2026-02):
148
+ { canonical: "antigravity/gemini-pro-agent", provider: "antigravity", providerModel: "gemini-pro-agent", contextTokens: 1_000_000, maxOutputTokens: 65_536, thinking: FULL, images: true, company: "Google Antigravity", releaseDate: "2026-02", sizeClass: "mid" }, // "Gemini 3.1 Pro (High)" — the CODE-AGENT model (replaces deprecated gemini-3.1-pro-high)
149
+ { canonical: "antigravity/gemini-3.1-pro-low", provider: "antigravity", providerModel: "gemini-3.1-pro-low", contextTokens: 1_000_000, maxOutputTokens: 65_536, thinking: FULL, images: true, company: "Google Antigravity", releaseDate: "2026-02", sizeClass: "small" }, // "Gemini 3.1 Pro (Low)" — the LOW agent model
150
+ // Anthropic via Antigravity (wire id claude-sonnet-4-6 IS the thinking variant per its displayName):
151
+ { canonical: "antigravity/claude-sonnet-4-6", provider: "antigravity", providerModel: "claude-sonnet-4-6", contextTokens: 200_000, maxOutputTokens: 64_000, thinking: FULL, images: true, company: "Anthropic via Antigravity", releaseDate: "2026-02", sizeClass: "mid" }, // "Claude Sonnet 4.6 (Thinking)"
152
+ { canonical: "antigravity/claude-opus-4-6-thinking", provider: "antigravity", providerModel: "claude-opus-4-6-thinking", contextTokens: 200_000, maxOutputTokens: 64_000, thinking: FULL, images: true, company: "Anthropic via Antigravity", releaseDate: "2026-02", sizeClass: "large" }, // "Claude Opus 4.6 (Thinking)"
153
+ // OpenAI OSS via Antigravity:
154
+ { canonical: "antigravity/gpt-oss-120b-medium", provider: "antigravity", providerModel: "gpt-oss-120b-medium", contextTokens: 1_000_000, maxOutputTokens: 65_536, thinking: FULL, images: false, company: "OpenAI via Antigravity", releaseDate: "2025-08", sizeClass: "mid" }, // "GPT-OSS 120B (Medium)"
157
155
  // Tencent — hosts the same underlying models under its own cloud brand; dates
158
156
  // below are the ORIGINAL developer's release (DeepSeek/MiniMax/Zhipu/Moonshot),
159
157
  // not Tencent's hosting-catalog appearance date, except where the id itself pins
@@ -201,6 +199,13 @@ export const KIMI_CODE_MODELS: readonly string[] = [
201
199
  "kimi-k2",
202
200
  ];
203
201
 
202
+ /** Recency compare for `releaseDate` ("YYYY-MM"): `>0` when `a` is newer; a
203
+ * missing date sorts oldest, so an unconfirmed date never outranks a
204
+ * verified-newer model. */
205
+ export function compareReleaseDate(a: string | undefined, b: string | undefined): number {
206
+ return (a ?? "").localeCompare(b ?? "");
207
+ }
208
+
204
209
  /** Format a token count compactly (1000 → 1K, 1_000_000 → 1M). */
205
210
  export function formatTokens(n: number): string {
206
211
  if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(n % 1_000_000 === 0 ? 0 : 1)}M`;
@@ -285,6 +285,9 @@ export function catalogOr(result: ProviderModelsResult): ProviderModelsResult {
285
285
  // Antigravity's available models depend on the Cloud Code Assist agent backend
286
286
  // and must never be faked from a hard-coded catalog.
287
287
  if (result.provider === "antigravity") return result;
288
+ // No catalog-fallback for oauth-sourced gemini failures: OAuth can LIST but
289
+ // not SERVE gemini models (see listProviderModels' gemini gate).
290
+ if (result.provider === "gemini" && result.source === "oauth") return result;
288
291
  // OAuth: OpenAI/Codex legitimately rejects the standard /models endpoint while
289
292
  // the fixed Codex ids still work; other OAuth providers fall back to their full
290
293
  // static catalog too. API-key providers only fall back when the provider has NO
@@ -362,6 +365,15 @@ export async function listProviderModels(
362
365
  cred = { kind: "api_key", provider: prov, token: config.providers[prov]! };
363
366
  source = "api_key";
364
367
  }
368
+ // Gemini models are exposed ONLY under API-key auth: a gemini OAuth token can
369
+ // still list models from generativelanguage.googleapis.com, but it can no
370
+ // longer SERVE them (the gemini-cli/Cloud Code Assist masquerade was removed
371
+ // — see OAUTH_FLOW_REGISTRY.gemini.verifiedEndToEnd), so surfacing them in
372
+ // pickers sells models every call would reject. The API-key swap above
373
+ // already ran, so reaching here with an oauth credential means NO key exists.
374
+ if (provider === "gemini" && cred.kind === "oauth") {
375
+ return { provider, models: [], ok: false, source, error: "OAuth cannot serve gemini models — set GEMINI_API_KEY, or use antigravity/* (Cloud Code Assist)" };
376
+ }
365
377
  const isLocalOpenAi = provider === "openai" && !!(opts.baseUrl ?? process.env.OPENAI_BASE_URL);
366
378
  if (source === "none" && !isLocalOpenAi) {
367
379
  return { provider, models: [], ok: false, source, error: "not logged in" };
@@ -4,7 +4,7 @@ import { readGlobalConfig } from "../agent/state";
4
4
  import { resolveCredential, isOAuthProvider, type AuthProvider, type Credential } from "../auth";
5
5
  import "./register-providers"; // side-effect: registers built-in adapters into providerRegistry
6
6
  import type { CallOptions, Message, ProviderAdapter, ProviderName } from "./types";
7
- import { expandAlias, resolveModelId, effectiveAliasesFor } from "./model-registry";
7
+ import { expandAlias, resolveModelId, effectiveAliasesFor, BUILTIN_ALIASES } from "./model-registry";
8
8
  import { findCatalogEntry, type ModelCatalogEntry } from "./model-catalog-compat";
9
9
  import { toProviderModel, CODEX_MODELS, KIMI_CODE_MODELS, findCatalogModel } from "./model-catalog";
10
10
  import { xaiCredential } from "./providers/xai";
@@ -118,7 +118,7 @@ export function resolveMaxOutputTokens(
118
118
  model?: string,
119
119
  level?: "low" | "medium" | "high" | "xhigh",
120
120
  ): number {
121
- const meta = model ? findCatalogEntry(expandAlias(model, ALIAS_DEFAULTS)) : undefined;
121
+ const meta = model ? findCatalogEntry(expandAlias(model, BUILTIN_ALIASES)) : undefined;
122
122
  if (!meta) return thinkingMaxTokens(level);
123
123
  const envCap = Number(jeoEnv("MAX_OUTPUT_TOKENS"));
124
124
  const cap = Number.isFinite(envCap) && envCap > 0 ? envCap : DEFAULT_MAX_OUTPUT_CAP;
@@ -198,7 +198,6 @@ export interface ModelManager {
198
198
  resolveProvider: typeof resolveProvider;
199
199
  }
200
200
 
201
- const ALIAS_DEFAULTS = { fast: "ollama/qwen2.5:0.5b", local: "ollama/qwen2.5:0.5b", sonnet: "claude-sonnet-4-6", opus: "claude-opus-4-6", haiku: "claude-haiku-4-5", gpt: "gpt-5.5", flash: "gemini-2.5-flash", grok: "grok-4.3" };
202
201
 
203
202
  /**
204
203
  * Build retry options from a config `retry` budget (gjc parity). `requestMaxRetries`
@@ -307,7 +306,12 @@ export function resolveRetryOptions(retry: Config["retry"], kind: "request" | "s
307
306
  * end-to-end.
308
307
  */
309
308
  function oauthServesModel(provider: AuthProvider, model: string): boolean {
310
- if (provider === "openai") return CODEX_MODELS.includes(model);
309
+ if (provider === "openai") {
310
+ // Provider-qualified ids (`openai/gpt-5.5`) are valid route targets — the
311
+ // adapters strip the prefix on the wire, so servability must match.
312
+ const wire = model.startsWith("openai/") ? model.slice(7) : model;
313
+ return CODEX_MODELS.includes(wire);
314
+ }
311
315
  if (provider === "kimi") {
312
316
  const wire = model.startsWith("kimi/") ? model.slice(5) : model;
313
317
  return KIMI_CODE_MODELS.includes(wire);
@@ -316,6 +320,32 @@ function oauthServesModel(provider: AuthProvider, model: string): boolean {
316
320
  return true;
317
321
  }
318
322
 
323
+ /**
324
+ * Whether jeo's STORED credentials can actually serve `model` — the shared
325
+ * model-LEVEL gate behind auto-routing candidacy (prompt-router's
326
+ * `isAutoSelectCandidate`) and launch.ts's routing veto. Mirrors `resolveCall`'s
327
+ * real rules instead of the provider-level "any credential exists" check that
328
+ * let OAuth-only OpenAI route to non-Codex models (then fail at call time with
329
+ * "set OPENAI_API_KEY" — the exact key-demand-despite-OAuth bug):
330
+ * - local providers (ollama/lmstudio) are keyless — always servable;
331
+ * - antigravity is OAuth-ONLY (its own login or the gemini OAuth fallback);
332
+ * an `providers.antigravity` API key cannot serve calls (resolveCall throws);
333
+ * - an API key serves the provider's FULL catalog (API keys are never
334
+ * model-scoped);
335
+ * - OAuth serves exactly what `oauthServesModel` says it serves end-to-end
336
+ * (openai→Codex ids only, kimi→Kimi Code ids only, gemini→nothing);
337
+ * - a configured OpenAI base URL is the keyless local-proxy path — any model.
338
+ */
339
+ export function modelServableWithConfig(provider: ProviderName, model: string, config: Partial<Pick<Config, "providers" | "oauth" | "openaiBaseUrl">>): boolean {
340
+ if (provider === "ollama" || provider === "lmstudio") return true;
341
+ if (provider === "antigravity") return !!(config.oauth?.antigravity || config.oauth?.gemini);
342
+ if (provider === "openai" && config.openaiBaseUrl) return true;
343
+ const auth = provider as AuthProvider;
344
+ if (config.providers?.[auth]) return true;
345
+ if (config.oauth?.[auth]) return oauthServesModel(auth, model);
346
+ return false;
347
+ }
348
+
319
349
  /**
320
350
  * Pick the credential to actually use for a provider call / live discovery.
321
351
  * OAuth is the user's explicit login, so it wins whenever the bundled adapter can
@@ -377,7 +407,7 @@ export function credentialForCall(
377
407
  async function resolveCall(options: Partial<CallOptions>, kind: "request" | "stream" = "request"): Promise<Resolved> {
378
408
  const config = await readGlobalConfig();
379
409
  const aliases = { ...((config as { modelAliases?: Record<string, string> }).modelAliases ?? {}) };
380
- const model = expandAlias(options.model ?? config.defaultModel, { ...ALIAS_DEFAULTS, ...aliases });
410
+ const model = expandAlias(options.model ?? config.defaultModel, { ...BUILTIN_ALIASES, ...aliases });
381
411
  const provider = resolveProvider(model);
382
412
  const adapter = providerRegistry.get(provider)!;
383
413
 
@@ -447,7 +477,7 @@ async function resolveCall(options: Partial<CallOptions>, kind: "request" | "str
447
477
  const credential = await resolveCredential(credentialProvider);
448
478
  const effective = effectiveCredentialForProvider(credentialProvider, credential, config, model);
449
479
  const isLocalOpenAi = provider === "openai" && !!baseUrl;
450
- if (provider === "openai" && effective.kind === "oauth" && !isLocalOpenAi && !CODEX_MODELS.includes(model)) {
480
+ if (provider === "openai" && effective.kind === "oauth" && !isLocalOpenAi && !oauthServesModel("openai", model)) {
451
481
  throw new Error(
452
482
  "OpenAI OAuth 자격증명은 Codex 모델(gpt-5.5/gpt-5.4)만 지원. OPENAI_API_KEY를 설정하거나 모델을 변경하세요"
453
483
  );
@@ -16,6 +16,10 @@ export const BUILTIN_ALIASES: ModelAliases = {
16
16
  flash: "gemini-2.5-flash",
17
17
  grok: "grok-4.3",
18
18
  kimi: "kimi-k2-0711-preview",
19
+ // Antigravity's backend DEPRECATED this wire id (deprecatedModelIds:
20
+ // { "gemini-3.1-pro-high": { newModelId: "gemini-pro-agent" } }) — keep
21
+ // existing configs/roles pinned to the old id working on the successor.
22
+ "antigravity/gemini-3.1-pro-high": "antigravity/gemini-pro-agent",
19
23
  };
20
24
 
21
25
  // Expand an alias to a concrete model id. Unknown input passes through unchanged.
@@ -45,7 +45,7 @@ import { callLlm, type Message } from "../agent/loop";
45
45
  import { friendlyProviderError } from "../util/provider-error";
46
46
  import { readGlobalConfig, saveConfigPatch, resolveWikiRoot } from "../agent/state";
47
47
  import { rememberModelPatch, recentModelsForDisplay } from "../agent/model-recency";
48
- import { describeModel, describeAllProviders, describeProvider, resolveProvider, thinkingMaxTokens, resolveMaxOutputTokens, thinkingToReasoningEffort, discoverModels, flattenModels, resolveSelection, catalogMetadata, catalogByProvider, resolveRoleModel, CODEX_MODELS, qualifyModelId } from "../ai";
48
+ import { describeModel, describeAllProviders, describeProvider, resolveProvider, thinkingMaxTokens, resolveMaxOutputTokens, thinkingToReasoningEffort, discoverModels, flattenModels, resolveSelection, catalogMetadata, catalogByProvider, resolveRoleModel, CODEX_MODELS, qualifyModelId, modelServableWithConfig } from "../ai";
49
49
  import type { ProviderModelsResult, PickEntry, ProviderName, ModelRole, ThinkLevel } from "../ai";
50
50
  import { readGoalState, writeGoalState, clearGoalState, verifyGoal } from "../agent/goal-verifier";
51
51
  import { routePrompt, deriveCacheSessionKey, warnOnce, type RouteDecision } from "../agent/prompt-router";
@@ -905,7 +905,10 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
905
905
  let routeCredentialNotice: string | undefined;
906
906
  let routeVetoNote: string | undefined;
907
907
  if (routed) {
908
- const routedProvider = resolveProvider(routed.model);
908
+ // describeModel expands aliases (e.g. a hand-written `roles.high: "gpt"`)
909
+ // exactly like the real call path, so the servability check below judges
910
+ // the id the provider will actually see — never a raw alias string.
911
+ const { resolved: routedResolved, provider: routedProvider } = await describeModel(routed.model, turnConfig);
909
912
  const status = await describeProvider(routedProvider, turnConfig);
910
913
  if (!status.ready) {
911
914
  routeVetoNote = `routed model '${routed.model}' (${routedProvider}) has no usable credential this turn — fell back to '${sessionModel || defaultModel}'`;
@@ -914,6 +917,17 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
914
917
  `[route] routed to '${routed.model}' (${routedProvider}) but that provider has no usable credential — falling back to the default model this turn. Run 'jeo auth login ${routedProvider}' or reconfigure routing.tiers/roles for a provider you're logged into.`,
915
918
  );
916
919
  routed = null;
920
+ } else if (!modelServableWithConfig(routedProvider, routedResolved, turnConfig)) {
921
+ // Provider readiness is necessary but not sufficient: an OAuth login only
922
+ // serves specific models (auth matrix on modelServableWithConfig). Only
923
+ // user-pinned routing.tiers/roles models can hit this branch —
924
+ // auto-selected models already passed the same predicate.
925
+ routeVetoNote = `routed model '${routed.model}' (${routedProvider}) is not servable by the stored ${routedProvider} credential this turn — fell back to '${sessionModel || defaultModel}'`;
926
+ routeCredentialNotice = warnOnce(
927
+ `prompt-router:model-not-servable:${routed.model}`,
928
+ `[route] routed to '${routed.model}' but the stored ${routedProvider} OAuth login cannot serve that model — falling back to the default model this turn. Set ${routedProvider.toUpperCase()}_API_KEY or reconfigure routing.tiers/roles to a model your login serves.`,
929
+ );
930
+ routed = null;
917
931
  }
918
932
  }
919
933
  lastRouteDecision = routed ?? (routeVetoNote ? { note: routeVetoNote } : routingEnabled && !sessionModel ? { note: "routing produced no decision (fail-open)" } : { note: "routing not active this turn" });