jeo-code 0.8.0 → 0.8.2
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 +23 -0
- package/README.ja.md +10 -7
- package/README.ko.md +9 -6
- package/README.md +6 -5
- package/README.zh.md +10 -7
- package/package.json +1 -1
- package/src/agent/prompt-router.ts +48 -30
- package/src/ai/model-catalog.ts +7 -0
- package/src/ai/model-discovery.ts +12 -0
- package/src/ai/model-manager.ts +47 -8
- package/src/auth/flows/index.ts +3 -3
- package/src/commands/launch.ts +16 -2
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,29 @@ 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.2] - 2026-07-08
|
|
10
|
+
_"프롬프트라우팅이 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._
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- **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.
|
|
14
|
+
- **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.
|
|
15
|
+
- **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.
|
|
16
|
+
- **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.
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
- **`/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.
|
|
20
|
+
|
|
21
|
+
## [0.8.1] - 2026-07-08
|
|
22
|
+
_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._
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
- **`OAUTH_FLOW_REGISTRY.gemini.verifiedEndToEnd` flipped `true` → `false`** (`src/auth/flows/index.ts`) — Google removed the gemini-cli/Cloud Code Assist masquerade that let a plain `jeo auth login gemini` OAuth token serve `google/gemini-*` calls directly; a `GEMINI_API_KEY` is now required (the same models remain OAuth-only reachable via `antigravity/*`). Without this flag, `effectiveCredentialForProvider`/`oauthServesModel` (`src/ai/model-manager.ts`) treated a gemini-OAuth-only credential as usable, so a real turn's API call hit Google's endpoint with a token it no longer accepts — a raw provider error instead of jeo's actionable "set GEMINI_API_KEY, or use antigravity/\*" message. Confirmed via live smoke test on the isolated fixed tree: `jeo doctor` now correctly reports `status: "fail"` with the full actionable hint for a gemini-OAuth-only config, instead of the false `"ok"` the stale flag produced.
|
|
26
|
+
- **`test/launch-role-model.test.ts`'s antigravity-readiness test updated** to match the (already-shipped, correct) v0.8.0 behavior: a live gemini-fallback OAuth credential now makes `antigravity/*` genuinely `ready: true` (matching `model-manager.ts`'s real call-path fallback), not just catalog-listed-with-a-warning. Split into two tests — one asserting the new ready-via-fallback path, one preserving the original "genuinely no credential at all" not-ready/warned-not-refused coverage.
|
|
27
|
+
- **`test/model-manager.test.ts`/`test/doctor.test.ts`** — updated the 2 tests whose expectations depended on the stale `verifiedEndToEnd` flag (gemini OAuth+key now correctly prefers the API key; gemini OAuth-only now correctly fails with the actionable hint instead of asserting the removed Cloud Code Assist probe response).
|
|
28
|
+
|
|
29
|
+
### Verified
|
|
30
|
+
Built a fully isolated tree (`git archive` of the tagged commit + only these 5 fix files copied in — zero contamination from the still-active concurrent session) and ran the complete suite there: `bun run typecheck` clean, **2712/2712 tests pass, 0 fail** (up from 2 failures on the as-shipped v0.8.0 tag, reproduced first to confirm they were real before fixing). Live end-to-end smoke test on that same isolated tree: `jeo doctor --json` against a gemini-OAuth-only config now correctly reports `"status": "fail"` with the actionable `GEMINI_API_KEY`/`antigravity` hint.
|
|
31
|
+
|
|
9
32
|
## [0.8.0] - 2026-07-08
|
|
10
33
|
_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._
|
|
11
34
|
|
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.1+` モデルへ向かい、`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
|
-
| `/
|
|
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
|
|
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=
|
|
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.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.
|
|
266
|
+
- **[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.
|
|
262
267
|
- **[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.
|
|
263
268
|
- **[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.
|
|
264
269
|
- **[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.
|
|
265
|
-
- **[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.
|
|
266
|
-
- **[0.7.55]** (2026-07-07) — Cross-provider routing (PromptRouter, v0.7.47+) already named the routed tier in the status bar (v0.7.52), but nothing surfaced WHICH model/provider actually produced a turn's reasoning in the Thinking-block surfaces — a gap noticed while manually verifying the v0.7.52 status-bar work end-to-end.
|
|
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.1+` 모델로만 가고, `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
|
-
| `/
|
|
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
|
|
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=
|
|
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.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
|
+
- **[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
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.
|
|
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
|
-
- **[0.7.55]** (2026-07-07) — Cross-provider routing (PromptRouter, v0.7.47+) already named the routed tier in the status bar (v0.7.52), but nothing surfaced WHICH model/provider actually produced a turn's reasoning in the Thinking-block surfaces — a gap noticed while manually verifying the v0.7.52 status-bar work end-to-end.
|
|
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 provider-qualified `antigravity/gemini-3.1+` models, 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
|
-
| `/
|
|
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
|
|
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.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.
|
|
269
|
+
- **[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.
|
|
267
270
|
- **[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.
|
|
268
271
|
- **[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.
|
|
269
272
|
- **[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.
|
|
270
|
-
- **[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.
|
|
271
|
-
- **[0.7.55]** (2026-07-07) — Cross-provider routing (PromptRouter, v0.7.47+) already named the routed tier in the status bar (v0.7.52), but nothing surfaced WHICH model/provider actually produced a turn's reasoning in the Thinking-block surfaces — a gap noticed while manually verifying the v0.7.52 status-bar work end-to-end.
|
|
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.1+` 模型,绝不选择需要 `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`)
|
|
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
|
-
| `/
|
|
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
|
|
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=
|
|
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.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.
|
|
266
|
+
- **[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.
|
|
262
267
|
- **[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.
|
|
263
268
|
- **[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.
|
|
264
269
|
- **[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.
|
|
265
|
-
- **[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.
|
|
266
|
-
- **[0.7.55]** (2026-07-07) — Cross-provider routing (PromptRouter, v0.7.47+) already named the routed tier in the status bar (v0.7.52), but nothing surfaced WHICH model/provider actually produced a turn's reasoning in the Thinking-block surfaces — a gap noticed while manually verifying the v0.7.52 status-bar work end-to-end.
|
|
267
270
|
|
|
268
271
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
269
272
|
<!-- CHANGELOG:END -->
|
package/package.json
CHANGED
|
@@ -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,6 +211,38 @@ 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
|
+
/** Cloud Code Assist's current agent-quality floor for OAuth-routed Gemini models:
|
|
225
|
+
* keep auto-routing on Gemini 3.1+ instead of cheaper/older Antigravity Gemini 2.5
|
|
226
|
+
* or 3.0 rows. Non-Gemini Antigravity rows (Claude/GPT) are not version-comparable
|
|
227
|
+
* to Gemini and are left to the normal strength/price ranking. */
|
|
228
|
+
function isAntigravityGeminiBelow31(canonical: string): boolean {
|
|
229
|
+
const m = /^antigravity\/gemini-(\d+)(?:\.(\d+))?(?:-|$)/.exec(canonical.toLowerCase());
|
|
230
|
+
if (!m) return false;
|
|
231
|
+
const major = Number(m[1]);
|
|
232
|
+
const minor = Number(m[2] ?? "0");
|
|
233
|
+
return major < 3 || (major === 3 && minor < 1);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function isAutoSelectCandidate(m: CatalogModel, config: RoutingConfig): boolean {
|
|
237
|
+
// Shared model-LEVEL credential gate (the full auth matrix is documented on
|
|
238
|
+
// modelServableWithConfig; keep aligned with resolveCall).
|
|
239
|
+
if (!isCloudProvider(m.provider) || m.limitedAvailability || !modelServableWithConfig(m.provider, m.canonical, config)) return false;
|
|
240
|
+
if (hasAntigravityOauth(config) && m.provider === "gemini") return false;
|
|
241
|
+
if (hasAntigravityOauth(config) && m.provider === "antigravity" && isAntigravityGeminiBelow31(m.canonical)) return false;
|
|
242
|
+
return true;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
|
|
214
246
|
/** Size class jeo infers for a catalog model, used to group EQUIVALENT models
|
|
215
247
|
* across DIFFERENT providers for pool-based routing (see `tierModelPool`) — the
|
|
216
248
|
* cross-provider counterpart to `cheapestCredentialed`/`strongestCredentialed`'s
|
|
@@ -233,21 +265,6 @@ function sizeClassFor(canonical: string): "small" | "mid" | "large" | null {
|
|
|
233
265
|
|
|
234
266
|
const TIER_TO_SIZE_CLASS: Record<PromptTier, "small" | "mid" | "large"> = { trivial: "small", standard: "mid", high: "mid", complex: "large" };
|
|
235
267
|
|
|
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
268
|
/** Multi-key strength comparator (ascending: weakest first) — same ranking signals
|
|
252
269
|
* as `strongestCredentialed`'s single-winner tiebreak, reused here to bucket
|
|
253
270
|
* UNCLASSIFIED (no size-suffix) models into a tercile fallback. */
|
|
@@ -257,7 +274,7 @@ function compareStrengthAscending(a: CatalogModel, b: CatalogModel): number {
|
|
|
257
274
|
if (xhighA !== xhighB) return xhighA - xhighB;
|
|
258
275
|
if (a.maxOutputTokens !== b.maxOutputTokens) return a.maxOutputTokens - b.maxOutputTokens;
|
|
259
276
|
if (a.contextTokens !== b.contextTokens) return a.contextTokens - b.contextTokens;
|
|
260
|
-
const recency =
|
|
277
|
+
const recency = compareReleaseDate(a.releaseDate, b.releaseDate);
|
|
261
278
|
if (recency !== 0) return recency;
|
|
262
279
|
return a.canonical < b.canonical ? -1 : a.canonical > b.canonical ? 1 : 0;
|
|
263
280
|
}
|
|
@@ -272,7 +289,7 @@ function compareStrengthAscending(a: CatalogModel, b: CatalogModel): number {
|
|
|
272
289
|
* for deterministic pool ordering (required by `selectFromPool`'s index math). */
|
|
273
290
|
export function tierModelPool(tier: PromptTier, config: RoutingConfig): string[] {
|
|
274
291
|
const targetClass = TIER_TO_SIZE_CLASS[tier];
|
|
275
|
-
const credentialed = MODEL_CATALOG.filter(m =>
|
|
292
|
+
const credentialed = MODEL_CATALOG.filter(m => isAutoSelectCandidate(m, config));
|
|
276
293
|
|
|
277
294
|
const suffixPool = credentialed.filter(m => sizeClassFor(m.canonical) === targetClass);
|
|
278
295
|
|
|
@@ -322,12 +339,12 @@ export function cheapestCredentialed(config: RoutingConfig): string | null {
|
|
|
322
339
|
let best: CatalogModel | null = null;
|
|
323
340
|
let bestCost = Infinity;
|
|
324
341
|
for (const m of MODEL_CATALOG) {
|
|
325
|
-
//
|
|
326
|
-
//
|
|
327
|
-
//
|
|
328
|
-
// real veto gate in launch.ts's `runTurn`
|
|
329
|
-
// live OAuth expiry) before a turn
|
|
330
|
-
if (!
|
|
342
|
+
// Candidate eligibility is centralized in `isAutoSelectCandidate`: it keeps
|
|
343
|
+
// OAuth-backed Antigravity routing on provider-qualified 3.1+ Gemini rows and
|
|
344
|
+
// prevents public `gemini` catalog rows from winning when Antigravity OAuth is
|
|
345
|
+
// the available Gemini lane. The real veto gate in launch.ts's `runTurn` still
|
|
346
|
+
// re-verifies full readiness (including live OAuth expiry) before a turn uses it.
|
|
347
|
+
if (!isAutoSelectCandidate(m, config)) continue;
|
|
331
348
|
const price = priceForModel(m.canonical);
|
|
332
349
|
if (!price) continue;
|
|
333
350
|
const cost = price.inPerM + price.outPerM;
|
|
@@ -335,7 +352,7 @@ export function cheapestCredentialed(config: RoutingConfig): string | null {
|
|
|
335
352
|
best = m;
|
|
336
353
|
bestCost = cost;
|
|
337
354
|
} else if (cost === bestCost && best) {
|
|
338
|
-
const recency =
|
|
355
|
+
const recency = compareReleaseDate(m.releaseDate, best.releaseDate);
|
|
339
356
|
if (recency > 0 || (recency === 0 && m.canonical < best.canonical)) best = m;
|
|
340
357
|
}
|
|
341
358
|
}
|
|
@@ -360,8 +377,9 @@ export function strongestCredentialed(
|
|
|
360
377
|
): string | null {
|
|
361
378
|
let best: CatalogModel | null = null;
|
|
362
379
|
for (const m of MODEL_CATALOG) {
|
|
363
|
-
//
|
|
364
|
-
|
|
380
|
+
// Candidate eligibility is centralized in `isAutoSelectCandidate` (see
|
|
381
|
+
// cheapestCredentialed above for the Antigravity/Gemini OAuth rationale).
|
|
382
|
+
if (!isAutoSelectCandidate(m, config)) continue;
|
|
365
383
|
if (filter && !filter(m)) continue;
|
|
366
384
|
if (!best) { best = m; continue; }
|
|
367
385
|
const xhighM = m.thinking.includes("xhigh") ? 1 : 0;
|
|
@@ -369,7 +387,7 @@ export function strongestCredentialed(
|
|
|
369
387
|
if (xhighM !== xhighBest) { if (xhighM > xhighBest) best = m; continue; }
|
|
370
388
|
if (m.maxOutputTokens !== best.maxOutputTokens) { if (m.maxOutputTokens > best.maxOutputTokens) best = m; continue; }
|
|
371
389
|
if (m.contextTokens !== best.contextTokens) { if (m.contextTokens > best.contextTokens) best = m; continue; }
|
|
372
|
-
const recency =
|
|
390
|
+
const recency = compareReleaseDate(m.releaseDate, best.releaseDate);
|
|
373
391
|
if (recency !== 0) { if (recency > 0) best = m; continue; }
|
|
374
392
|
if (m.canonical < best.canonical) best = m;
|
|
375
393
|
}
|
|
@@ -388,7 +406,7 @@ export function strongestCredentialed(
|
|
|
388
406
|
* `roles.medium`/`defaultModel` — never to `complex`'s flagship pick). */
|
|
389
407
|
function strongestMidTierCredentialed(config: RoutingConfig): string | null {
|
|
390
408
|
const midClass = MODEL_CATALOG.filter(
|
|
391
|
-
m =>
|
|
409
|
+
m => isAutoSelectCandidate(m, config) && sizeClassFor(m.canonical) === "mid",
|
|
392
410
|
);
|
|
393
411
|
if (midClass.length === 0) return null;
|
|
394
412
|
return midClass.sort(compareStrengthAscending).at(-1)!.canonical;
|
package/src/ai/model-catalog.ts
CHANGED
|
@@ -201,6 +201,13 @@ export const KIMI_CODE_MODELS: readonly string[] = [
|
|
|
201
201
|
"kimi-k2",
|
|
202
202
|
];
|
|
203
203
|
|
|
204
|
+
/** Recency compare for `releaseDate` ("YYYY-MM"): `>0` when `a` is newer; a
|
|
205
|
+
* missing date sorts oldest, so an unconfirmed date never outranks a
|
|
206
|
+
* verified-newer model. */
|
|
207
|
+
export function compareReleaseDate(a: string | undefined, b: string | undefined): number {
|
|
208
|
+
return (a ?? "").localeCompare(b ?? "");
|
|
209
|
+
}
|
|
210
|
+
|
|
204
211
|
/** Format a token count compactly (1000 → 1K, 1_000_000 → 1M). */
|
|
205
212
|
export function formatTokens(n: number): string {
|
|
206
213
|
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" };
|
package/src/ai/model-manager.ts
CHANGED
|
@@ -299,12 +299,20 @@ export function resolveRetryOptions(retry: Config["retry"], kind: "request" | "s
|
|
|
299
299
|
* OpenAI OAuth (ChatGPT/Codex) only serves Codex models, so any other OpenAI model
|
|
300
300
|
* must use an API key. Kimi OAuth (Kimi Code subscription) only serves the Kimi Code
|
|
301
301
|
* catalog (api.kimi.com/coding) — Moonshot API-platform ids (kimi-latest, moonshot-v1-*)
|
|
302
|
-
* need a KIMI_API_KEY.
|
|
303
|
-
*
|
|
304
|
-
*
|
|
302
|
+
* need a KIMI_API_KEY. Gemini OAuth no longer masquerades as gemini-cli against Cloud
|
|
303
|
+
* Code Assist, so it never serves google/gemini-* models on its own — a GEMINI_API_KEY
|
|
304
|
+
* is required (the SAME models remain OAuth-only reachable via antigravity/*). A
|
|
305
|
+
* provider whose OAuth backend is not verified end-to-end cannot serve any model over
|
|
306
|
+
* OAuth. Everything else (Anthropic Messages, Antigravity Cloud Code Assist) is served
|
|
307
|
+
* end-to-end.
|
|
305
308
|
*/
|
|
306
309
|
function oauthServesModel(provider: AuthProvider, model: string): boolean {
|
|
307
|
-
if (provider === "openai")
|
|
310
|
+
if (provider === "openai") {
|
|
311
|
+
// Provider-qualified ids (`openai/gpt-5.5`) are valid route targets — the
|
|
312
|
+
// adapters strip the prefix on the wire, so servability must match.
|
|
313
|
+
const wire = model.startsWith("openai/") ? model.slice(7) : model;
|
|
314
|
+
return CODEX_MODELS.includes(wire);
|
|
315
|
+
}
|
|
308
316
|
if (provider === "kimi") {
|
|
309
317
|
const wire = model.startsWith("kimi/") ? model.slice(5) : model;
|
|
310
318
|
return KIMI_CODE_MODELS.includes(wire);
|
|
@@ -313,6 +321,32 @@ function oauthServesModel(provider: AuthProvider, model: string): boolean {
|
|
|
313
321
|
return true;
|
|
314
322
|
}
|
|
315
323
|
|
|
324
|
+
/**
|
|
325
|
+
* Whether jeo's STORED credentials can actually serve `model` — the shared
|
|
326
|
+
* model-LEVEL gate behind auto-routing candidacy (prompt-router's
|
|
327
|
+
* `isAutoSelectCandidate`) and launch.ts's routing veto. Mirrors `resolveCall`'s
|
|
328
|
+
* real rules instead of the provider-level "any credential exists" check that
|
|
329
|
+
* let OAuth-only OpenAI route to non-Codex models (then fail at call time with
|
|
330
|
+
* "set OPENAI_API_KEY" — the exact key-demand-despite-OAuth bug):
|
|
331
|
+
* - local providers (ollama/lmstudio) are keyless — always servable;
|
|
332
|
+
* - antigravity is OAuth-ONLY (its own login or the gemini OAuth fallback);
|
|
333
|
+
* an `providers.antigravity` API key cannot serve calls (resolveCall throws);
|
|
334
|
+
* - an API key serves the provider's FULL catalog (API keys are never
|
|
335
|
+
* model-scoped);
|
|
336
|
+
* - OAuth serves exactly what `oauthServesModel` says it serves end-to-end
|
|
337
|
+
* (openai→Codex ids only, kimi→Kimi Code ids only, gemini→nothing);
|
|
338
|
+
* - a configured OpenAI base URL is the keyless local-proxy path — any model.
|
|
339
|
+
*/
|
|
340
|
+
export function modelServableWithConfig(provider: ProviderName, model: string, config: Partial<Pick<Config, "providers" | "oauth" | "openaiBaseUrl">>): boolean {
|
|
341
|
+
if (provider === "ollama" || provider === "lmstudio") return true;
|
|
342
|
+
if (provider === "antigravity") return !!(config.oauth?.antigravity || config.oauth?.gemini);
|
|
343
|
+
if (provider === "openai" && config.openaiBaseUrl) return true;
|
|
344
|
+
const auth = provider as AuthProvider;
|
|
345
|
+
if (config.providers?.[auth]) return true;
|
|
346
|
+
if (config.oauth?.[auth]) return oauthServesModel(auth, model);
|
|
347
|
+
return false;
|
|
348
|
+
}
|
|
349
|
+
|
|
316
350
|
/**
|
|
317
351
|
* Pick the credential to actually use for a provider call / live discovery.
|
|
318
352
|
* OAuth is the user's explicit login, so it wins whenever the bundled adapter can
|
|
@@ -331,8 +365,11 @@ export function effectiveCredentialForProvider(
|
|
|
331
365
|
const apiKey = config.providers[provider];
|
|
332
366
|
if (apiKey) return { kind: "api_key", provider, token: apiKey };
|
|
333
367
|
if (isOAuthProvider(provider) && OAUTH_FLOW_REGISTRY[provider].verifiedEndToEnd === false) {
|
|
368
|
+
const hint = provider === "gemini"
|
|
369
|
+
? " Or use an antigravity/* model instead — the SAME Gemini models served OAuth-only via Cloud Code Assist (run 'jeo auth login antigravity')."
|
|
370
|
+
: "";
|
|
334
371
|
throw new Error(
|
|
335
|
-
`Provider '${provider}' has only an OAuth token, but its OAuth backend is not compatible with the bundled adapter. Set ${provider.toUpperCase()}_API_KEY (or run 'jeo setup') to use ${model}
|
|
372
|
+
`Provider '${provider}' has only an OAuth token, but its OAuth backend is not compatible with the bundled adapter. Set ${provider.toUpperCase()}_API_KEY (or run 'jeo setup') to use ${model}.${hint}`,
|
|
336
373
|
);
|
|
337
374
|
}
|
|
338
375
|
}
|
|
@@ -425,8 +462,10 @@ async function resolveCall(options: Partial<CallOptions>, kind: "request" | "str
|
|
|
425
462
|
|
|
426
463
|
if (provider === "antigravity") {
|
|
427
464
|
// Prefer the dedicated Antigravity login (its client is what the agent
|
|
428
|
-
// backend authorizes); fall back to a gemini
|
|
429
|
-
// their own project/permissions
|
|
465
|
+
// backend authorizes); fall back to a plain gemini OAuth token for users with
|
|
466
|
+
// their own project/permissions — this is now the DEFAULT OAuth-served path for
|
|
467
|
+
// Gemini models (google/gemini-* itself requires GEMINI_API_KEY, see
|
|
468
|
+
// `oauthServesModel` above).
|
|
430
469
|
let credential = await resolveCredential("antigravity");
|
|
431
470
|
if (credential.kind !== "oauth") credential = await resolveCredential("gemini");
|
|
432
471
|
if (credential.kind !== "oauth") {
|
|
@@ -439,7 +478,7 @@ async function resolveCall(options: Partial<CallOptions>, kind: "request" | "str
|
|
|
439
478
|
const credential = await resolveCredential(credentialProvider);
|
|
440
479
|
const effective = effectiveCredentialForProvider(credentialProvider, credential, config, model);
|
|
441
480
|
const isLocalOpenAi = provider === "openai" && !!baseUrl;
|
|
442
|
-
if (provider === "openai" && effective.kind === "oauth" && !isLocalOpenAi && !
|
|
481
|
+
if (provider === "openai" && effective.kind === "oauth" && !isLocalOpenAi && !oauthServesModel("openai", model)) {
|
|
443
482
|
throw new Error(
|
|
444
483
|
"OpenAI OAuth 자격증명은 Codex 모델(gpt-5.5/gpt-5.4)만 지원. OPENAI_API_KEY를 설정하거나 모델을 변경하세요"
|
|
445
484
|
);
|
package/src/auth/flows/index.ts
CHANGED
|
@@ -39,11 +39,11 @@ export const OAUTH_FLOW_REGISTRY: Record<OAuthProvider, OAuthFlow> = {
|
|
|
39
39
|
},
|
|
40
40
|
gemini: {
|
|
41
41
|
provider: "gemini",
|
|
42
|
-
label: "Google (Gemini
|
|
42
|
+
label: "Google (Gemini API)",
|
|
43
43
|
login: loginGoogle,
|
|
44
44
|
refresh: refreshGoogleToken,
|
|
45
|
-
verifiedEndToEnd:
|
|
46
|
-
note: "
|
|
45
|
+
verifiedEndToEnd: false,
|
|
46
|
+
note: "This OAuth login alone no longer serves google/gemini-* models (the Cloud Code Assist / gemini-cli masquerade was removed) — set GEMINI_API_KEY to use them. The SAME Gemini models remain reachable OAuth-only via antigravity/* (run 'jeo auth login antigravity'; this gemini login also works as its fallback credential).",
|
|
47
47
|
},
|
|
48
48
|
antigravity: {
|
|
49
49
|
provider: "antigravity",
|
package/src/commands/launch.ts
CHANGED
|
@@ -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
|
-
|
|
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" });
|