jeo-code 0.7.26 → 0.7.28
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 +38 -0
- package/README.ja.md +2 -2
- package/README.ko.md +2 -2
- package/README.md +2 -2
- package/README.zh.md +2 -2
- package/package.json +1 -1
- package/src/agent/compaction.ts +24 -0
- package/src/agent/config-schema.ts +1 -0
- package/src/agent/engine.ts +20 -5
- package/src/agent/loop.ts +3 -0
- package/src/agent/state.ts +84 -5
- package/src/agent/task-tool.ts +5 -2
- package/src/ai/model-catalog.ts +23 -0
- package/src/ai/model-discovery.ts +6 -2
- package/src/ai/model-manager.ts +60 -13
- package/src/ai/providers/anthropic.ts +62 -18
- package/src/ai/providers/antigravity.ts +34 -22
- package/src/ai/providers/gemini.ts +81 -27
- package/src/ai/providers/kimi.ts +44 -5
- package/src/ai/providers/openai-responses.ts +36 -2
- package/src/ai/types.ts +10 -0
- package/src/auth/callback-server.ts +19 -5
- package/src/auth/flows/AGENTS.md +7 -6
- package/src/auth/flows/antigravity.ts +3 -0
- package/src/auth/flows/google-project.ts +8 -2
- package/src/auth/flows/google.ts +15 -1
- package/src/auth/flows/index.ts +10 -0
- package/src/auth/flows/kimi.ts +263 -0
- package/src/auth/flows/openai.ts +103 -3
- package/src/auth/oauth.ts +9 -0
- package/src/auth/refresh.ts +6 -1
- package/src/auth/storage.ts +5 -5
- package/src/commands/launch/input.ts +244 -9
- package/src/commands/launch/slash-views.ts +5 -2
- package/src/commands/launch.ts +64 -28
- package/src/commands/team.ts +3 -0
- package/src/tui/clipboard.ts +49 -0
- package/src/tui/components/welcome.ts +98 -39
- package/src/tui/terminal.ts +5 -3
- package/src/util/provider-error.ts +4 -12
- package/src/util/retry.ts +88 -31
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,44 @@ 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.7.28] - 2026-07-02
|
|
10
|
+
_gajae-code 0.7.9–0.7.10 OAuth + model-execution parity: generic long rate-limit windows are retried (never fatal), retry waits are abort-cancellable, Codex prompt caching via session correlation, and the Kimi Code device-flow OAuth is completed end-to-end (catalog, gating, discovery, tests)._
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- **Generic 429 rate limits with a long server `Retry-After` were treated as FATAL — gajae 0.7.10 (#1370) keeps them retryable, and now jeo does too.** The old behavior failed fast on any 429 whose server-directed delay exceeded a hard-coded 5-minute budget, and silently compressed every honored `Retry-After` to 30s (retrying into a still-closed window). gjc's contract (verified against its `agent-session-retry-cap` tests): a transient rate limit honors the provider's wait IN FULL — even a ~3.1h `retry-after-ms=11180000` — while usage/quota/model-limit errors keep failing fast via `isUsageLimitError` → `defaultRetryable`. `withRetry` (`src/util/retry.ts`) now honors server-directed delays uncapped (the 30s `RATE_LIMIT_FLOOR_CAP_MS` only caps the SYNTHETIC escalating floor for server-silent 429s), and `resolveRetryOptions` no longer injects a default `rateLimitMaxServerDelayMs` — it remains available as explicit opt-in config (`retry.rateLimitMaxServerDelayMs`, added to the Config type + zod schema).
|
|
14
|
+
- **A long honored rate-limit wait is now user-cancellable.** `RetryOptions.signal` (new) aborts an in-progress backoff sleep: `resolveCall` threads the caller's ORIGINAL abort signal (Ctrl-C / turn abort) into the retry layer — distinct from the per-attempt fetch timeout, so one attempt's timeout can never cut short the next attempt's backoff, while Esc/Ctrl-C interrupts a multi-hour wait immediately instead of after the sleep elapses.
|
|
15
|
+
- **`isUsageLimitError` missed gajae's usage-exhaustion phrasings.** Added `model.?limit` / `message.?limit` / `limit for this model` / `out_of_credits` / Anthropic's account-level `request would exceed your account's rate limit` and the qualified `Resource has been exhausted (… quota/limit)` form (bare gRPC `resource exhausted` stays transient — deliberate jeo choice for Gemini windows that clear quickly). Without these, an out-of-credits 429 was retried on a pointless backoff ladder instead of failing fast to the model/credential-switch hint.
|
|
16
|
+
- **Anthropic 429s now carry their rate-limit response headers in the error detail** (gjc `getSafeAnthropicHeaderEvidence` parity): `retry-after`, `retry-after-ms`, and every `anthropic-ratelimit-*` header — including the decisive `anthropic-ratelimit-unified-overage-disabled-reason=out_of_credits`, which often arrives ONLY as a header. Without it an out-of-credits 429 read like a generic per-minute rate limit.
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
- **Kimi Code (Moonshot coding subscription) OAuth — completed end-to-end.** The device-code flow (`src/auth/flows/kimi.ts`, RFC 8628: `auth.kimi.com` device_authorization → poll with `authorization_pending`/`slow_down`/`expired_token`/`access_denied` semantics, X-Msh-* device headers, persisted `kimi-device-id`) now has: catalog entries for the Kimi Code models (`kimi-for-coding`, `kimi-k2.7-code`, `kimi/kimi-k2.5`, `kimi/kimi-k2-turbo-preview`, `kimi/kimi-k2` — 262k context, provider-qualified where canonicals collide with tencent's), a `KIMI_CODE_MODELS` export, OAuth gating (`oauthServesModel`: kimi OAuth serves ONLY the Kimi Code set; Moonshot API ids fall back to `KIMI_API_KEY` via `effectiveCredentialForProvider`, with an actionable error when no key exists), and an OAuth-only discovery fallback that lists exactly the Kimi Code models (`catalogOr`) instead of faking moonshot ids. The `kimiAdapter` dispatches on credential kind — OAuth → Anthropic-compatible `api.kimi.com/coding/v1/messages` with plain Bearer + X-Msh headers (no Claude-Code cloaking; gated by `shouldUseClaudeCodeOAuthShape`'s baseUrl check), api_key → the unchanged OpenAI-compatible `api.moonshot.ai/v1`.
|
|
20
|
+
- **Codex/Responses provider-side prompt caching (gjc parity — closes the `ponytail` note in `openai-responses.ts`).** New `CallOptions.sessionKey` threads a stable per-conversation key from `launch.ts` (the session id), `team.ts`, and `task-tool.ts` (per-run UUIDs) through the engine loop into every model call. `codexResponsesRequest` stamps it as `prompt_cache_key` in the body (both the OAuth Codex backend and the public `api.openai.com/v1/responses`) plus `session_id`/`conversation_id`/`x-client-request-id` correlation headers on the Codex backend (`normalizePromptCacheKey`: ≤64 chars verbatim, longer keys hashed `pc_*` — gjc `normalizeOpenAIResponsesPromptCacheKey` parity). An agent loop replays nearly-identical history each step, so cache hits cut both latency and billed input tokens.
|
|
21
|
+
|
|
22
|
+
### Verified
|
|
23
|
+
- `bun run typecheck` clean; full suite green — 2142 pass / 0 fail across 244 files (before the codex/kimi test additions), then targeted suites re-run green: `retry` (26), `rate-limit-handling` (5), `provider-errors`, `round-b`, `kimi-provider` (7 new), `kimi-oauth` (7 new), `codex-responses` (+3 new). The corrected rate-limit contract is pinned by tests that mirror gjc's own fixtures (3.1h retry-after honored + recovered; model-limit fails fast; mid-wait abort interrupts the sleep).
|
|
24
|
+
|
|
25
|
+
## [0.7.27] - 2026-07-02
|
|
26
|
+
_Extended-keyboard decode layer: Esc/Ctrl+C work again on kitty-protocol terminals, Shift+Enter breaks lines on every terminal incl. Windows, row/draft-aware Cmd+←→/↑↓ + Home/End/Ctrl+Home/End, Ctrl+V text-paste fallback. Plus Anthropic 5th-gen omp-parity: catalog-derived output budgets end thinking-vs-reply starvation; refusals fail fast at the transport and the recovery ladder strips replayed thinking blocks._
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
- **Esc and Ctrl+C went DEAD after 0.7.25's keyboard-protocol push — the reported "입력후 esc 키 (또 ctrl+c) 입력이 안되는 문제".** Root cause: `CSI >1u` (kitty disambiguate) makes the terminal re-encode Esc as `CSI 27u` and Ctrl+C as `CSI 99;5u` (xterm modifyOtherKeys=2: `CSI 27;5;99~`) — forms nothing in jeo decoded: readline saw an unknown sequence (key `name: undefined`), the raw-`\u0003` scans never matched, and the mid-turn harness classified both as "noise". New decode layer in `src/commands/launch/input.ts` — `matchExtendedKey`/`decodeExtendedKeys` (kitty CSI-u + xterm modifyOtherKeys → legacy bytes), `chunkHasCtrlC` (all three Ctrl+C encodings), `normalizeKeypress` (CSI-u keypress events → the `{name, ctrl, meta}` shape handlers match; includes the Bun quirk where an unknown CSI emits the literal STRING `"undefined"` as `key.name`) — wired into the prompt key filter, the footer/picker keypress handlers, the raw Ctrl+C byte scan, the pre-prompt typed-input drain, and the mid-turn ESC/Ctrl+C abort harness. Key-release reports (`:3`) and unknown functional codes are swallowed instead of leaking `[57376u`-style garbage into the draft. Cmd/Super+letter no longer types the bare letter.
|
|
30
|
+
- **Copy/paste reliability ("복사붙여넣기가 잘 동작안하는 경우"):** (1) Ctrl+V arrived as `CSI 118;5u` under the kitty protocol — binding dead; now normalized. (2) Ctrl+V with a TEXT clipboard was a silent no-op (binding was image-only); it now falls back to `readClipboardText()` (`pbpaste` / PowerShell `Get-Clipboard -Raw` / `wl-paste`/`xclip`/`xsel`) and inserts at the caret with paste-grade sanitation (ANSI stripped, line breaks → box sentinel — a multi-line clipboard can never self-submit). (3) A trailing `\r\n` outside a paste (Windows CRLF tails) forwarded BOTH bytes — the `\r` submitted and the `\n` leaked into the next prompt as a phantom break/submit; now folds to ONE Enter, including when the CRLF is split across stdin chunk reads (`crTail` filter state). (4) `disableKittyKeyboard()` now POPS the kitty keyboard-mode stack (`CSI <u`) instead of pushing a zero-flags entry that leaked onto the outer shell.
|
|
31
|
+
- **Shift+Enter coverage gaps.** `SHIFT_ENTER_SEQS` covered only 2 encodings (`CSI 27;2;13~`, `CSI 13;2u`); now every modified-Enter form breaks the line: kitty mods 2–6 (Shift/Alt/Ctrl combinations), the xterm modifyOtherKeys equivalents, and legacy alt-as-meta `ESC \r` (macOS Option+Enter, Windows Terminal Alt+Enter).
|
|
32
|
+
- **Adaptive-thinking turns died with "output budget exhausted" because `max_tokens` was the THINKING-LEVEL table (4k–32k), not the model's real output capacity.** On Anthropic adaptive models (Opus 4.6+, Sonnet/Fable/Mythos 5) thinking tokens are spent INSIDE `max_tokens`, so a deep-thinking step competed with its own visible reply inside a 16k–32k budget and could stop at `stop_reason=max_tokens` with zero content. New `resolveMaxOutputTokens(model, level)` (`src/ai/model-manager.ts`) resolves the CATALOG max-output (fable-5: 128k) capped by `JEO_MAX_OUTPUT_TOKENS` (default 64k); the thinking level keeps steering DEPTH via `reasoningEffort`/`output_config` only. Wired through `resolveCall`, both `launch.ts` engine call sites, and `task-tool.ts` subagent spawns.
|
|
33
|
+
- **Safety refusals were retried as "transient provider error" at the transport layer, burning 2 extra billed calls per recovery-ladder rung (observed: 392k input tokens for one refused turn).** A refusal is deterministic for identical conversation content. `isRefusalError` moved to `src/util/retry.ts` (re-exported from `provider-error.ts`) and `defaultRetryable` now fails fast on `stop_reason=refusal` / `finish_reason=content_filter` / Gemini `SAFETY`-family markers — the engine's bounded ladder (which MUTATES context between attempts) is the only retry layer. Same failing turn now costs 190k → recovers.
|
|
34
|
+
- **The refusal ladder could never recover when replayed thinking blocks were the classifier trigger.** Stage 2 ("context reset") only elided tool-result bodies; assistant turns kept `reasoningArtifacts`/`toolUse`, and `buildAnthropicMessages` resent the flagged model-authored thinking blocks verbatim — so a fable-5 turn re-refused through the whole ladder (reproduced deterministically at both 32k and 64k budgets, and at minimal thinking). New `stripReasoningArtifactsInPlace` (`src/agent/compaction.ts`) drops the native replay channel (display `reasoning` text survives) at stage 2, so the next request falls back to plain-text history. Verified live: the exact turn that previously exhausted the ladder now recovers at stage 2 and completes.
|
|
35
|
+
- **`LaunchTui.setLivePromptHighlight` test failed on hosts exporting `NO_COLOR`** (agent harnesses, some CI): `detectColorLevel` honors `NO_COLOR` before `FORCE_COLOR` (no-color.org contract), so the test's `FORCE_COLOR=3` never produced a colored theme. The test now pins `JEO_TUI_THEME=cosmic` and clears `NO_COLOR` for the constructor — deterministic on every host.
|
|
36
|
+
|
|
37
|
+
### Added
|
|
38
|
+
- **A line-break key on EVERY terminal — the Windows answer ("윈도우환경에 알맞는 단축키").** The lone-LF break rule (`\n` → line break) is now DEFAULT-ON (`JEO_MULTILINE=0` opts out): raw-mode Enter is always `\r`, so a bare `\n` can only be Ctrl+J (universal) or Ctrl+Enter (Windows Terminal/conhost) — both now insert a break with zero terminal configuration. Plus the Claude-Code `\`+Enter convention: Enter on a draft ending in `\` (caret at end) replaces the backslash with a line break — works even on terminals with no Shift+Enter protocol at all (conhost, Terminal.app).
|
|
39
|
+
- **Whole-draft jumps: Ctrl+Home/Ctrl+End (Windows/Linux) and Cmd+↑/Cmd+↓ (macOS kitty-protocol) move the caret to the start/end of the entire draft (`DOC_HOME_SEQS`/`DOC_END_SEQS`), complementing the row-aware Cmd+←/→ and Home/End from 0.7.25.** Sequences are consumed even when no readline is attached, so modifier-arrow params can never leak into the draft as literal text.
|
|
40
|
+
- `/hotkeys` documents the break keys, row/draft jumps, and the Ctrl+V text fallback (18 → 21 rows).
|
|
41
|
+
|
|
42
|
+
### Verified
|
|
43
|
+
- `bun run typecheck` clean; full suite green — 2104 pass / 0 fail across 240 files. New `test/extended-key-decode.test.ts`: 21 tests / 85 assertions across all six consumer layers (decoder, whole-string decode, Ctrl+C matcher, keypress normalizer, prompt filter, abort harness, clipboard paste command) — mutation-checked (flipping the ctrl-bit test reddens 6 tests).
|
|
44
|
+
- Live probes through the REAL implementation (Bun eval, no PTY): kitty Esc keypress normalizes to `escape`; all 3 Ctrl+C encodings detected; 7 break encodings → sentinel; Cmd+←/→ land on row boundaries; Ctrl+Home/Cmd+↓ land on draft edges; trailing and split CRLF fold to one Enter; `\`+Enter emits backspace+break; mid-turn kitty Esc aborts with "ESC pressed…", kitty Ctrl+C hard-exits; Cmd+C is swallowed as input yet visible as `meta+c` to the clear-input binding.
|
|
45
|
+
- Anthropic budget/refusal fixes: affected suites green (`model-manager`, `anthropic-stream`, `model-catalog`, `retry`, `provider-error`, `refusal-recovery`, `compaction`) with 6 new test cases; live E2E (fable-5, xhigh, OAuth) — the previously-blocked multi-step turn completes end-to-end, stage 2 logs "reset 2 tool result(s) and dropped thinking replay from 3 turn(s)".
|
|
46
|
+
|
|
9
47
|
## [0.7.26] - 2026-07-02
|
|
10
48
|
_Esc/Ctrl+C reliably clears the input box (fixes a gate the earlier reorder missed), and un-bracketed multi-line pastes no longer shred into premature per-line submits._
|
|
11
49
|
|
package/README.ja.md
CHANGED
|
@@ -205,11 +205,11 @@ CI は `.github/workflows/npm-publish.yml` で公開します — GitHub リリ
|
|
|
205
205
|
## 変更履歴 (Changelog)
|
|
206
206
|
|
|
207
207
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
208
|
+
- **[0.7.28]** (2026-07-02) — gajae-code 0.7.9–0.7.10 OAuth + model-execution parity: generic long rate-limit windows are retried (never fatal), retry waits are abort-cancellable, Codex prompt caching via session correlation, and the Kimi Code device-flow OAuth is completed end-to-end (catalog, gating, discovery, tests).
|
|
209
|
+
- **[0.7.27]** (2026-07-02) — Extended-keyboard decode layer: Esc/Ctrl+C work again on kitty-protocol terminals, Shift+Enter breaks lines on every terminal incl. Windows, row/draft-aware Cmd+←→/↑↓ + Home/End/Ctrl+Home/End, Ctrl+V text-paste fallback. Plus Anthropic 5th-gen omp-parity: catalog-derived output budgets end thinking-vs-reply starvation; refusals fail fast at the transport and the recovery ladder strips replayed thinking blocks.
|
|
208
210
|
- **[0.7.26]** (2026-07-02) — Esc/Ctrl+C reliably clears the input box (fixes a gate the earlier reorder missed), and un-bracketed multi-line pastes no longer shred into premature per-line submits.
|
|
209
211
|
- **[0.7.25]** (2026-07-02) — Shift+Enter now reliably distinguishable from Enter on kitty/WezTerm/iTerm2/xterm via modifyOtherKeys + kitty keyboard protocol; row-aware Home/End and Cmd+Left/Right in multi-row drafts.
|
|
210
212
|
- **[0.7.24]** (2026-07-02) — OAuth callback page redesign with embedded jeo wordmark + Close button; memory budget O(n²)→O(n) incremental tracking fix.
|
|
211
|
-
- **[0.7.23]** (2026-07-01) — Anthropic 5th-generation model catalog: adds Sonnet 5, Fable 5, and Mythos 5, promotes the 4.6 generation to 1M context / 128k output, and retires the superseded 4.5/3.5/4.1 ids. Bumps the default model to Sonnet 4.6, prices the new tiers, and fixes the thinking transport for Haiku 4.5 and every 5th-gen model.
|
|
212
|
-
- **[0.7.22]** (2026-06-30) — gajae-code 0.7.3–0.7.8 parity sweep: terminal-bell notifications, a gruvbox-dark TUI theme, a resilient `jeo update` runtime check, and an ultragoal artifact gate. Ports the user-facing improvements from upstream gajae-code that jeo did not yet have, re-implemented to fit jeo's config, command, theming, and skill surfaces.
|
|
213
213
|
|
|
214
214
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
215
215
|
<!-- CHANGELOG:END -->
|
package/README.ko.md
CHANGED
|
@@ -205,11 +205,11 @@ CI는 `.github/workflows/npm-publish.yml`로 배포합니다 — GitHub 릴리
|
|
|
205
205
|
## 변경 이력 (Changelog)
|
|
206
206
|
|
|
207
207
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
208
|
+
- **[0.7.28]** (2026-07-02) — gajae-code 0.7.9–0.7.10 OAuth + model-execution parity: generic long rate-limit windows are retried (never fatal), retry waits are abort-cancellable, Codex prompt caching via session correlation, and the Kimi Code device-flow OAuth is completed end-to-end (catalog, gating, discovery, tests).
|
|
209
|
+
- **[0.7.27]** (2026-07-02) — Extended-keyboard decode layer: Esc/Ctrl+C work again on kitty-protocol terminals, Shift+Enter breaks lines on every terminal incl. Windows, row/draft-aware Cmd+←→/↑↓ + Home/End/Ctrl+Home/End, Ctrl+V text-paste fallback. Plus Anthropic 5th-gen omp-parity: catalog-derived output budgets end thinking-vs-reply starvation; refusals fail fast at the transport and the recovery ladder strips replayed thinking blocks.
|
|
208
210
|
- **[0.7.26]** (2026-07-02) — Esc/Ctrl+C reliably clears the input box (fixes a gate the earlier reorder missed), and un-bracketed multi-line pastes no longer shred into premature per-line submits.
|
|
209
211
|
- **[0.7.25]** (2026-07-02) — Shift+Enter now reliably distinguishable from Enter on kitty/WezTerm/iTerm2/xterm via modifyOtherKeys + kitty keyboard protocol; row-aware Home/End and Cmd+Left/Right in multi-row drafts.
|
|
210
212
|
- **[0.7.24]** (2026-07-02) — OAuth callback page redesign with embedded jeo wordmark + Close button; memory budget O(n²)→O(n) incremental tracking fix.
|
|
211
|
-
- **[0.7.23]** (2026-07-01) — Anthropic 5th-generation model catalog: adds Sonnet 5, Fable 5, and Mythos 5, promotes the 4.6 generation to 1M context / 128k output, and retires the superseded 4.5/3.5/4.1 ids. Bumps the default model to Sonnet 4.6, prices the new tiers, and fixes the thinking transport for Haiku 4.5 and every 5th-gen model.
|
|
212
|
-
- **[0.7.22]** (2026-06-30) — gajae-code 0.7.3–0.7.8 parity sweep: terminal-bell notifications, a gruvbox-dark TUI theme, a resilient `jeo update` runtime check, and an ultragoal artifact gate. Ports the user-facing improvements from upstream gajae-code that jeo did not yet have, re-implemented to fit jeo's config, command, theming, and skill surfaces.
|
|
213
213
|
|
|
214
214
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
215
215
|
<!-- CHANGELOG:END -->
|
package/README.md
CHANGED
|
@@ -232,11 +232,11 @@ Huge thanks to [gajae-code](https://github.com/Yeachan-Heo/gajae-code) for the i
|
|
|
232
232
|
## Changelog
|
|
233
233
|
|
|
234
234
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
235
|
+
- **[0.7.28]** (2026-07-02) — gajae-code 0.7.9–0.7.10 OAuth + model-execution parity: generic long rate-limit windows are retried (never fatal), retry waits are abort-cancellable, Codex prompt caching via session correlation, and the Kimi Code device-flow OAuth is completed end-to-end (catalog, gating, discovery, tests).
|
|
236
|
+
- **[0.7.27]** (2026-07-02) — Extended-keyboard decode layer: Esc/Ctrl+C work again on kitty-protocol terminals, Shift+Enter breaks lines on every terminal incl. Windows, row/draft-aware Cmd+←→/↑↓ + Home/End/Ctrl+Home/End, Ctrl+V text-paste fallback. Plus Anthropic 5th-gen omp-parity: catalog-derived output budgets end thinking-vs-reply starvation; refusals fail fast at the transport and the recovery ladder strips replayed thinking blocks.
|
|
235
237
|
- **[0.7.26]** (2026-07-02) — Esc/Ctrl+C reliably clears the input box (fixes a gate the earlier reorder missed), and un-bracketed multi-line pastes no longer shred into premature per-line submits.
|
|
236
238
|
- **[0.7.25]** (2026-07-02) — Shift+Enter now reliably distinguishable from Enter on kitty/WezTerm/iTerm2/xterm via modifyOtherKeys + kitty keyboard protocol; row-aware Home/End and Cmd+Left/Right in multi-row drafts.
|
|
237
239
|
- **[0.7.24]** (2026-07-02) — OAuth callback page redesign with embedded jeo wordmark + Close button; memory budget O(n²)→O(n) incremental tracking fix.
|
|
238
|
-
- **[0.7.23]** (2026-07-01) — Anthropic 5th-generation model catalog: adds Sonnet 5, Fable 5, and Mythos 5, promotes the 4.6 generation to 1M context / 128k output, and retires the superseded 4.5/3.5/4.1 ids. Bumps the default model to Sonnet 4.6, prices the new tiers, and fixes the thinking transport for Haiku 4.5 and every 5th-gen model.
|
|
239
|
-
- **[0.7.22]** (2026-06-30) — gajae-code 0.7.3–0.7.8 parity sweep: terminal-bell notifications, a gruvbox-dark TUI theme, a resilient `jeo update` runtime check, and an ultragoal artifact gate. Ports the user-facing improvements from upstream gajae-code that jeo did not yet have, re-implemented to fit jeo's config, command, theming, and skill surfaces.
|
|
240
240
|
|
|
241
241
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
242
242
|
<!-- CHANGELOG:END -->
|
package/README.zh.md
CHANGED
|
@@ -205,11 +205,11 @@ CI 通过 `.github/workflows/npm-publish.yml` 发布 — GitHub 发布 release
|
|
|
205
205
|
## 更新日志 (Changelog)
|
|
206
206
|
|
|
207
207
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
208
|
+
- **[0.7.28]** (2026-07-02) — gajae-code 0.7.9–0.7.10 OAuth + model-execution parity: generic long rate-limit windows are retried (never fatal), retry waits are abort-cancellable, Codex prompt caching via session correlation, and the Kimi Code device-flow OAuth is completed end-to-end (catalog, gating, discovery, tests).
|
|
209
|
+
- **[0.7.27]** (2026-07-02) — Extended-keyboard decode layer: Esc/Ctrl+C work again on kitty-protocol terminals, Shift+Enter breaks lines on every terminal incl. Windows, row/draft-aware Cmd+←→/↑↓ + Home/End/Ctrl+Home/End, Ctrl+V text-paste fallback. Plus Anthropic 5th-gen omp-parity: catalog-derived output budgets end thinking-vs-reply starvation; refusals fail fast at the transport and the recovery ladder strips replayed thinking blocks.
|
|
208
210
|
- **[0.7.26]** (2026-07-02) — Esc/Ctrl+C reliably clears the input box (fixes a gate the earlier reorder missed), and un-bracketed multi-line pastes no longer shred into premature per-line submits.
|
|
209
211
|
- **[0.7.25]** (2026-07-02) — Shift+Enter now reliably distinguishable from Enter on kitty/WezTerm/iTerm2/xterm via modifyOtherKeys + kitty keyboard protocol; row-aware Home/End and Cmd+Left/Right in multi-row drafts.
|
|
210
212
|
- **[0.7.24]** (2026-07-02) — OAuth callback page redesign with embedded jeo wordmark + Close button; memory budget O(n²)→O(n) incremental tracking fix.
|
|
211
|
-
- **[0.7.23]** (2026-07-01) — Anthropic 5th-generation model catalog: adds Sonnet 5, Fable 5, and Mythos 5, promotes the 4.6 generation to 1M context / 128k output, and retires the superseded 4.5/3.5/4.1 ids. Bumps the default model to Sonnet 4.6, prices the new tiers, and fixes the thinking transport for Haiku 4.5 and every 5th-gen model.
|
|
212
|
-
- **[0.7.22]** (2026-06-30) — gajae-code 0.7.3–0.7.8 parity sweep: terminal-bell notifications, a gruvbox-dark TUI theme, a resilient `jeo update` runtime check, and an ultragoal artifact gate. Ports the user-facing improvements from upstream gajae-code that jeo did not yet have, re-implemented to fit jeo's config, command, theming, and skill surfaces.
|
|
213
213
|
|
|
214
214
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
215
215
|
<!-- CHANGELOG:END -->
|
package/package.json
CHANGED
package/src/agent/compaction.ts
CHANGED
|
@@ -147,6 +147,30 @@ export function trimToolResultsInPlace(
|
|
|
147
147
|
return { trimmed, tokens };
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
+
/**
|
|
151
|
+
* Refusal-recovery companion to `trimToolResultsInPlace`: drop provider-native
|
|
152
|
+
* reasoning artifacts (and native tool-use replay records) from ASSISTANT turns
|
|
153
|
+
* so the next request replays plain text instead of native thinking blocks.
|
|
154
|
+
*
|
|
155
|
+
* Why: Anthropic's refusal classifier judges the WHOLE resent conversation, and
|
|
156
|
+
* replayed `thinking` blocks are model-authored content — the usual trip wire
|
|
157
|
+
* that tool-result eliding cannot clear (field case: a fable-5 turn re-refused
|
|
158
|
+
* through the entire ladder because every resend replayed the flagged thinking
|
|
159
|
+
* blocks verbatim). Messages are REPLACED, never mutated (identity caches).
|
|
160
|
+
* Display fields (`reasoning`) are kept — only the wire-replay channel is cut.
|
|
161
|
+
*/
|
|
162
|
+
export function stripReasoningArtifactsInPlace(history: Message[]): number {
|
|
163
|
+
let stripped = 0;
|
|
164
|
+
for (let i = 0; i < history.length; i++) {
|
|
165
|
+
const m = history[i]!;
|
|
166
|
+
if (m.role !== "assistant" || (!m.reasoningArtifacts?.length && !m.toolUse?.length)) continue;
|
|
167
|
+
const { reasoningArtifacts: _artifacts, toolUse: _toolUse, ...rest } = m;
|
|
168
|
+
history[i] = rest;
|
|
169
|
+
stripped++;
|
|
170
|
+
}
|
|
171
|
+
return stripped;
|
|
172
|
+
}
|
|
173
|
+
|
|
150
174
|
/**
|
|
151
175
|
* Accurate BPE token total for a history, summing `countTokensAccurate` per
|
|
152
176
|
* message (+1 per message for role/separator overhead, mirroring
|
|
@@ -105,6 +105,7 @@ export const ConfigSchema = z
|
|
|
105
105
|
maxDelayMs: z.number().int().min(0).optional(),
|
|
106
106
|
rateLimitRetries: z.number().int().min(0).optional(),
|
|
107
107
|
rateLimitMinDelayMs: z.number().int().min(0).optional(),
|
|
108
|
+
rateLimitMaxServerDelayMs: z.number().int().min(0).optional(),
|
|
108
109
|
})
|
|
109
110
|
.optional(),
|
|
110
111
|
/**
|
package/src/agent/engine.ts
CHANGED
|
@@ -21,7 +21,7 @@ import { runPreToolHooks, runPostTurnHooksForBatch } from "./hooks";
|
|
|
21
21
|
import { truncateToolOutput, formatToolResultBody } from "./tool-output";
|
|
22
22
|
export { TOOL_OUTPUT_MAX, READ_OUTPUT_MAX, TOOL_SPILL_THRESHOLD, MAX_TOOL_ARTIFACTS, truncateToolOutput, spillToolResult } from "./tool-output";
|
|
23
23
|
import { StepBudget, dynamicStepBudgetConfig, resolveStepBudgetConfig, hashSignature, type StepBudgetConfig } from "./step-budget";
|
|
24
|
-
import { historyTokens, trimToolResultsInPlace } from "./compaction";
|
|
24
|
+
import { historyTokens, trimToolResultsInPlace, stripReasoningArtifactsInPlace } from "./compaction";
|
|
25
25
|
import { jeoEnv } from "../util/env";
|
|
26
26
|
import { GUARD_LIMITS, isVerificationSignal, repeatHint, classifyDoneGate } from "./loop-guards";
|
|
27
27
|
import { stripLeakedReasoningTags } from "../ai/think-tags";
|
|
@@ -40,6 +40,7 @@ async function invokeCallLlm(history: Message[], options: {
|
|
|
40
40
|
onReasoningStart?: () => void;
|
|
41
41
|
onReasoningArtifact?: (artifact: import("../ai/types").ReasoningArtifact) => void;
|
|
42
42
|
tools?: import("../ai/types").NativeToolSchema[];
|
|
43
|
+
sessionKey?: string;
|
|
43
44
|
}): Promise<string> {
|
|
44
45
|
const mod = await import("./loop");
|
|
45
46
|
return mod.callLlm(history, options);
|
|
@@ -265,6 +266,11 @@ export interface AgentLoopOptions {
|
|
|
265
266
|
* so an additional query typed while the turn runs steers the live turn instead of
|
|
266
267
|
* waiting for the next prompt. Return [] when nothing is pending. */
|
|
267
268
|
steer?: () => string[];
|
|
269
|
+
/** Stable per-conversation key (the session id): threaded to every model call for
|
|
270
|
+
* provider-side prompt caching (gjc parity — Codex prompt_cache_key/session_id).
|
|
271
|
+
* An agent loop replays nearly-identical history each step, so cache hits here cut
|
|
272
|
+
* both latency and billed input tokens. */
|
|
273
|
+
sessionKey?: string;
|
|
268
274
|
}
|
|
269
275
|
|
|
270
276
|
export interface AgentLoopResult {
|
|
@@ -388,7 +394,7 @@ export async function runAgentLoop(history: Message[], opts: AgentLoopOptions):
|
|
|
388
394
|
"with what you have already found this turn, and state explicitly anything that is still uncertain.",
|
|
389
395
|
},
|
|
390
396
|
],
|
|
391
|
-
{ jsonMode: false, model: opts.model, maxTokens: opts.maxTokens, signal: opts.signal },
|
|
397
|
+
{ jsonMode: false, model: opts.model, maxTokens: opts.maxTokens, signal: opts.signal, sessionKey: opts.sessionKey },
|
|
392
398
|
);
|
|
393
399
|
const consolidated = wrapUp.trim();
|
|
394
400
|
if (consolidated) {
|
|
@@ -557,6 +563,7 @@ export async function runAgentLoop(history: Message[], opts: AgentLoopOptions):
|
|
|
557
563
|
maxTokens: opts.maxTokens,
|
|
558
564
|
reasoningEffort: opts.reasoningEffort,
|
|
559
565
|
signal: opts.signal,
|
|
566
|
+
sessionKey: opts.sessionKey,
|
|
560
567
|
onUsage: u => { acc.inputTokens += u.inputTokens ?? 0; acc.outputTokens += u.outputTokens ?? 0; sawUsage = true; },
|
|
561
568
|
onToken,
|
|
562
569
|
onReasoning,
|
|
@@ -611,9 +618,17 @@ export async function runAgentLoop(history: Message[], opts: AgentLoopOptions):
|
|
|
611
618
|
}
|
|
612
619
|
if (refusalRetries === 2) {
|
|
613
620
|
const res = trimToolResultsInPlace(history, { budgetTokens: 0, keepRecent: 0 });
|
|
621
|
+
// Also cut the native thinking-block replay channel: refusals judge the WHOLE
|
|
622
|
+
// resent conversation, and replayed (model-authored) thinking blocks are the
|
|
623
|
+
// trip wire tool-result eliding can't clear — with artifacts stripped the next
|
|
624
|
+
// request falls back to plain-text history (same shape as the 400 fail-safe).
|
|
625
|
+
const strippedTurns = stripReasoningArtifactsInPlace(history);
|
|
626
|
+
const parts: string[] = [];
|
|
627
|
+
if (res.trimmed > 0) parts.push(`reset ${res.trimmed} tool result(s)`);
|
|
628
|
+
if (strippedTurns > 0) parts.push(`dropped thinking replay from ${strippedTurns} turn(s)`);
|
|
614
629
|
ev.onNotice?.(
|
|
615
|
-
|
|
616
|
-
? `provider refused again —
|
|
630
|
+
parts.length
|
|
631
|
+
? `provider refused again — ${parts.join(" and ")} and retrying (refusals require a context reset)`
|
|
617
632
|
: "provider refused again — continuing with a fresh instruction",
|
|
618
633
|
);
|
|
619
634
|
history.push({
|
|
@@ -1124,7 +1139,7 @@ export async function runAgentLoop(history: Message[], opts: AgentLoopOptions):
|
|
|
1124
1139
|
"the key findings/changes so far, and what remains to be done next.",
|
|
1125
1140
|
},
|
|
1126
1141
|
],
|
|
1127
|
-
{ jsonMode: false, model: opts.model, maxTokens: opts.maxTokens, signal: opts.signal },
|
|
1142
|
+
{ jsonMode: false, model: opts.model, maxTokens: opts.maxTokens, signal: opts.signal, sessionKey: opts.sessionKey },
|
|
1128
1143
|
);
|
|
1129
1144
|
const consolidated = wrapUp.trim();
|
|
1130
1145
|
if (consolidated) {
|
package/src/agent/loop.ts
CHANGED
|
@@ -31,6 +31,9 @@ export interface ChatOptions {
|
|
|
31
31
|
onReasoningArtifact?: (artifact: import("../ai/types").ReasoningArtifact) => void;
|
|
32
32
|
/** NATIVE tool-calling function declarations (forwarded to capable adapters). */
|
|
33
33
|
tools?: import("../ai/types").NativeToolSchema[];
|
|
34
|
+
/** Stable per-conversation key (session id) for provider-side prompt caching
|
|
35
|
+
* (gjc parity: prompt_cache_key / session_id correlation on Codex/Responses). */
|
|
36
|
+
sessionKey?: string;
|
|
34
37
|
}
|
|
35
38
|
|
|
36
39
|
const manager = createModelManager();
|
package/src/agent/state.ts
CHANGED
|
@@ -78,6 +78,11 @@ export interface Config {
|
|
|
78
78
|
rateLimitRetries?: number;
|
|
79
79
|
/** Minimum backoff (ms) for a 429 when the server sends no Retry-After. */
|
|
80
80
|
rateLimitMinDelayMs?: number;
|
|
81
|
+
/** Opt-in ceiling (ms) on a 429's server-directed Retry-After: a delay beyond it fails
|
|
82
|
+
* fast instead of sleeping. Unset (the default) honors any server-directed delay in
|
|
83
|
+
* full, however long — a generic rate limit is retried forever, not treated as fatal
|
|
84
|
+
* past some budget. Set this only if unbounded rate-limit waits are undesirable. */
|
|
85
|
+
rateLimitMaxServerDelayMs?: number;
|
|
81
86
|
/** HTTP statuses to treat as NON-retryable even when defaultRetryable would
|
|
82
87
|
* retry them (e.g. pin 503 to fail fast instead of riding the backoff ladder). */
|
|
83
88
|
failFastStatuses?: number[];
|
|
@@ -385,14 +390,88 @@ export async function readRawGlobalConfig(): Promise<Config> {
|
|
|
385
390
|
return salvageCredentials(clean, parsed);
|
|
386
391
|
}
|
|
387
392
|
|
|
393
|
+
/**
|
|
394
|
+
* Global config write lock — HIGH-1 lost-update fix (gjc parity: auth-storage.ts
|
|
395
|
+
* SqliteAuthCredentialStore serializes credential writes inside SQLite). jeo keeps
|
|
396
|
+
* everything in one config.json, so EVERY read-modify-write must serialize through one
|
|
397
|
+
* lock file; otherwise two concurrent per-provider OAuth refreshes (e.g. doctor.ts
|
|
398
|
+
* Promise.all) read the same base config and the later write clobbers the earlier one's
|
|
399
|
+
* just-rotated refresh token → silent logout.
|
|
400
|
+
*
|
|
401
|
+
* Semantics mirror src/auth/storage.ts acquireLock (stale-lock detection + bounded wait
|
|
402
|
+
* + single steal at the deadline) but are implemented locally: storage.ts imports
|
|
403
|
+
* state.ts, so importing it back would create a cycle.
|
|
404
|
+
* // ponytail: extract a shared lockfile helper module if a third lockfile user appears.
|
|
405
|
+
*/
|
|
406
|
+
const CONFIG_LOCK_STALE_MS = 5_000;
|
|
407
|
+
|
|
408
|
+
function configLockPath(): string {
|
|
409
|
+
return `${globalConfigPath()}.lock`;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
async function acquireConfigLock(timeoutMs = CONFIG_LOCK_STALE_MS): Promise<void> {
|
|
413
|
+
const lockPath = configLockPath();
|
|
414
|
+
await fs.mkdir(path.dirname(lockPath), { recursive: true, mode: 0o700 });
|
|
415
|
+
// `timeoutMs` is the STALENESS threshold for a dead holder's lock file; the
|
|
416
|
+
// acquisition wait itself is bounded at 2× that (same policy as storage.ts).
|
|
417
|
+
const deadline = Date.now() + Math.max(timeoutMs * 2, 1_000);
|
|
418
|
+
while (true) {
|
|
419
|
+
try {
|
|
420
|
+
const handle = await fs.open(lockPath, "wx");
|
|
421
|
+
await handle.writeFile(JSON.stringify({ pid: process.pid, createdAt: Date.now() }), "utf-8");
|
|
422
|
+
await handle.close();
|
|
423
|
+
return;
|
|
424
|
+
} catch (err: any) {
|
|
425
|
+
if (err.code !== "EEXIST") throw err;
|
|
426
|
+
try {
|
|
427
|
+
const info = JSON.parse(await fs.readFile(lockPath, "utf-8"));
|
|
428
|
+
if (typeof info.createdAt === "number" && info.createdAt + timeoutMs < Date.now()) {
|
|
429
|
+
await fs.unlink(lockPath).catch(() => {});
|
|
430
|
+
}
|
|
431
|
+
} catch {
|
|
432
|
+
try {
|
|
433
|
+
const stat = await fs.stat(lockPath);
|
|
434
|
+
if (stat.mtimeMs + timeoutMs < Date.now()) {
|
|
435
|
+
await fs.unlink(lockPath).catch(() => {});
|
|
436
|
+
}
|
|
437
|
+
} catch {}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
if (Date.now() >= deadline) {
|
|
441
|
+
// Deadline reached: the holder is dead or wedged. Steal once — the lock guards
|
|
442
|
+
// a short config read-modify-write, so waiting longer only hangs the caller.
|
|
443
|
+
await fs.unlink(lockPath).catch(() => {});
|
|
444
|
+
const handle = await fs.open(lockPath, "wx").catch(() => null);
|
|
445
|
+
if (handle) {
|
|
446
|
+
await handle.writeFile(JSON.stringify({ pid: process.pid, createdAt: Date.now(), stolen: true }), "utf-8");
|
|
447
|
+
await handle.close();
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
throw new Error(`config lock could not be acquired within ${Math.max(timeoutMs * 2, 1_000)}ms (${lockPath})`);
|
|
451
|
+
}
|
|
452
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
async function releaseConfigLock(): Promise<void> {
|
|
457
|
+
await fs.unlink(configLockPath()).catch(() => {});
|
|
458
|
+
}
|
|
459
|
+
|
|
388
460
|
/** Merge a patch onto the RAW on-disk config and persist. The `build` callback
|
|
389
461
|
* receives the raw config so partial updates (subagents/roles maps) are derived
|
|
390
|
-
* from on-disk state, never from the env-overlaid runtime config.
|
|
462
|
+
* from on-disk state, never from the env-overlaid runtime config. The whole
|
|
463
|
+
* read→merge→write cycle holds the global config lock so concurrent patches
|
|
464
|
+
* (per-provider OAuth refreshes, /model save, doctor sweeps) never lose updates. */
|
|
391
465
|
export async function saveConfigPatch(build: (raw: Config) => Partial<Config>): Promise<Config> {
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
466
|
+
await acquireConfigLock();
|
|
467
|
+
try {
|
|
468
|
+
const raw = await readRawGlobalConfig();
|
|
469
|
+
const next = { ...raw, ...build(raw) };
|
|
470
|
+
await saveGlobalConfig(next);
|
|
471
|
+
return next;
|
|
472
|
+
} finally {
|
|
473
|
+
await releaseConfigLock();
|
|
474
|
+
}
|
|
396
475
|
}
|
|
397
476
|
|
|
398
477
|
export function getLocalJeoDir(cwd: string = process.cwd()): string {
|
package/src/agent/task-tool.ts
CHANGED
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
subagentRoleIds,
|
|
28
28
|
validateSubagentDoneReason,
|
|
29
29
|
} from "./subagents";
|
|
30
|
-
import {
|
|
30
|
+
import { resolveMaxOutputTokens } from "../ai/model-manager";
|
|
31
31
|
import type { SubagentRegistry } from "./subagent-registry";
|
|
32
32
|
|
|
33
33
|
/** Lifecycle event emitted while a delegated subagent runs. */
|
|
@@ -219,7 +219,10 @@ export function createTaskTool(opts: TaskToolOptions): ToolHandler {
|
|
|
219
219
|
cwd,
|
|
220
220
|
model,
|
|
221
221
|
maxSteps,
|
|
222
|
-
maxTokens:
|
|
222
|
+
maxTokens: resolveMaxOutputTokens(model, thinking),
|
|
223
|
+
// Per-run prompt-cache key: the subagent replays its own growing history each
|
|
224
|
+
// step, so a stable per-run key gets provider cache hits (gjc sub-session parity).
|
|
225
|
+
sessionKey: crypto.randomUUID(),
|
|
223
226
|
// Bounded delegation: a subagent's step contract stays exact — the parent
|
|
224
227
|
// owns any retry/extension decision, so the gjc retry flow is disabled here.
|
|
225
228
|
budget: { maxExtensions: 0 },
|
package/src/ai/model-catalog.ts
CHANGED
|
@@ -93,6 +93,14 @@ export const MODEL_CATALOG: readonly CatalogModel[] = [
|
|
|
93
93
|
{ canonical: "kimi-thinking-preview", provider: "kimi", providerModel: "kimi-thinking-preview", contextTokens: 128_000, maxOutputTokens: 32_000, thinking: FULL, images: true },
|
|
94
94
|
{ canonical: "kimi-latest", provider: "kimi", providerModel: "kimi-latest", contextTokens: 128_000, maxOutputTokens: 16_384, thinking: [], images: true },
|
|
95
95
|
{ canonical: "moonshot-v1-128k", provider: "kimi", providerModel: "moonshot-v1-128k", contextTokens: 128_000, maxOutputTokens: 16_384, thinking: [], images: false },
|
|
96
|
+
// Kimi Code (Moonshot coding subscription, device-code OAuth) — Anthropic-compatible
|
|
97
|
+
// at https://api.kimi.com/coding. Provider-qualified (`kimi/…`) to avoid canonical
|
|
98
|
+
// collisions (kimi-k2.5 also exists under tencent). gjc catalog parity: kimi-code.
|
|
99
|
+
{ canonical: "kimi-for-coding", provider: "kimi", providerModel: "kimi/kimi-for-coding", contextTokens: 262_144, maxOutputTokens: 32_000, thinking: FULL, images: true, company: "Moonshot (Kimi Code)" },
|
|
100
|
+
{ canonical: "kimi-k2.7-code", provider: "kimi", providerModel: "kimi/kimi-k2.7-code", contextTokens: 262_144, maxOutputTokens: 65_536, thinking: FULL, images: true, company: "Moonshot (Kimi Code)" },
|
|
101
|
+
{ canonical: "kimi/kimi-k2.5", provider: "kimi", providerModel: "kimi/kimi-k2.5", contextTokens: 262_144, maxOutputTokens: 65_536, thinking: FULL, images: true, company: "Moonshot (Kimi Code)" },
|
|
102
|
+
{ canonical: "kimi/kimi-k2-turbo-preview", provider: "kimi", providerModel: "kimi/kimi-k2-turbo-preview", contextTokens: 262_144, maxOutputTokens: 32_000, thinking: FULL, images: false, company: "Moonshot (Kimi Code)" },
|
|
103
|
+
{ canonical: "kimi/kimi-k2", provider: "kimi", providerModel: "kimi/kimi-k2", contextTokens: 262_144, maxOutputTokens: 262_144, thinking: [], images: false, company: "Moonshot (Kimi Code)" },
|
|
96
104
|
// Google
|
|
97
105
|
{ canonical: "gemini-1.5-pro", provider: "gemini", providerModel: "gemini-1.5-pro", contextTokens: 1_000_000, maxOutputTokens: 8_192, thinking: [], images: true },
|
|
98
106
|
{ canonical: "gemini-2.0-flash", provider: "gemini", providerModel: "gemini-2.0-flash", contextTokens: 1_000_000, maxOutputTokens: 8_192, thinking: [], images: true },
|
|
@@ -144,6 +152,21 @@ export const MODEL_CATALOG: readonly CatalogModel[] = [
|
|
|
144
152
|
*/
|
|
145
153
|
export const CODEX_MODELS: readonly string[] = ["gpt-5.5", "gpt-5.4"];
|
|
146
154
|
|
|
155
|
+
/**
|
|
156
|
+
* Model ids the Kimi Code OAuth backend (api.kimi.com/coding, Anthropic Messages
|
|
157
|
+
* format) actually serves. The subscription endpoint rejects Moonshot API-platform
|
|
158
|
+
* ids (kimi-latest, moonshot-v1-*, kimi-thinking-preview), so a Kimi OAuth login is
|
|
159
|
+
* limited to exactly these; an API key serves the api.moonshot.ai catalog instead.
|
|
160
|
+
* gjc parity: models.json `kimi-code` provider entries (wire ids, `kimi/` stripped).
|
|
161
|
+
*/
|
|
162
|
+
export const KIMI_CODE_MODELS: readonly string[] = [
|
|
163
|
+
"kimi-for-coding",
|
|
164
|
+
"kimi-k2.7-code",
|
|
165
|
+
"kimi-k2.5",
|
|
166
|
+
"kimi-k2-turbo-preview",
|
|
167
|
+
"kimi-k2",
|
|
168
|
+
];
|
|
169
|
+
|
|
147
170
|
/** Format a token count compactly (1000 → 1K, 1_000_000 → 1M). */
|
|
148
171
|
export function formatTokens(n: number): string {
|
|
149
172
|
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(n % 1_000_000 === 0 ? 0 : 1)}M`;
|
|
@@ -11,7 +11,7 @@ import { readGlobalConfig, type Config } from "../agent/state";
|
|
|
11
11
|
import { resolveCredential, type AuthProvider, type Credential } from "../auth";
|
|
12
12
|
import type { ProviderName } from "./types";
|
|
13
13
|
import { PROVIDER_NAMES } from "./provider-status";
|
|
14
|
-
import { catalogByProvider, CODEX_MODELS } from "./model-catalog";
|
|
14
|
+
import { catalogByProvider, CODEX_MODELS, KIMI_CODE_MODELS } from "./model-catalog";
|
|
15
15
|
import { extractChatgptAccountId } from "./providers/openai-responses";
|
|
16
16
|
import { openaiCompatDef } from "./providers/openai-compatible-catalog";
|
|
17
17
|
|
|
@@ -291,7 +291,11 @@ export function catalogOr(result: ProviderModelsResult): ProviderModelsResult {
|
|
|
291
291
|
if (!eligible) return result;
|
|
292
292
|
const ids = result.provider === "openai" && result.source === "oauth"
|
|
293
293
|
? [...CODEX_MODELS]
|
|
294
|
-
:
|
|
294
|
+
: result.provider === "kimi" && result.source === "oauth"
|
|
295
|
+
// Kimi OAuth (Kimi Code subscription) serves ONLY the Kimi Code catalog —
|
|
296
|
+
// moonshot API-platform ids would 404 against api.kimi.com/coding.
|
|
297
|
+
? KIMI_CODE_MODELS.map(id => `kimi/${id}`)
|
|
298
|
+
: catalogByProvider(result.provider).map(m => m.providerModel);
|
|
295
299
|
if (ids.length === 0) return result;
|
|
296
300
|
return { ...result, models: ids, ok: true, fallback: true };
|
|
297
301
|
}
|
package/src/ai/model-manager.ts
CHANGED
|
@@ -6,7 +6,7 @@ import "./register-providers"; // side-effect: registers built-in adapters into
|
|
|
6
6
|
import type { CallOptions, Message, ProviderAdapter, ProviderName } from "./types";
|
|
7
7
|
import { expandAlias, resolveModelId, effectiveAliasesFor } from "./model-registry";
|
|
8
8
|
import { findCatalogEntry, type ModelCatalogEntry } from "./model-catalog-compat";
|
|
9
|
-
import { toProviderModel, CODEX_MODELS } from "./model-catalog";
|
|
9
|
+
import { toProviderModel, CODEX_MODELS, KIMI_CODE_MODELS, findCatalogModel } from "./model-catalog";
|
|
10
10
|
import { xaiCredential } from "./providers/xai";
|
|
11
11
|
import { OPENAI_COMPAT_NAMES, isOpenAICompatProvider } from "./providers/openai-compatible-catalog";
|
|
12
12
|
import { withRetry, defaultRetryable, type RetryOptions } from "../util/retry";
|
|
@@ -100,6 +100,32 @@ export function thinkingMaxTokens(level?: "minimal" | "low" | "medium" | "high"
|
|
|
100
100
|
return 16000;
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
+
/** Default cap for the catalog-derived output budget. 64k covers every real coding
|
|
104
|
+
* turn (large writes + deep adaptive thinking) while staying clear of the admission
|
|
105
|
+
* 429 risk that a blanket 128k `max_tokens` carries on low-tier API keys. */
|
|
106
|
+
const DEFAULT_MAX_OUTPUT_CAP = 64_000;
|
|
107
|
+
|
|
108
|
+
/** Output-token budget for a call (gjc/omp parity): the model's CATALOG max-output,
|
|
109
|
+
* capped by JEO_MAX_OUTPUT_TOKENS (default 64k) — NOT the thinking-level table.
|
|
110
|
+
*
|
|
111
|
+
* Rationale: on Anthropic adaptive-thinking models (Sonnet/Fable/Mythos 5, Opus 4.6+)
|
|
112
|
+
* thinking tokens are spent INSIDE `max_tokens`. Deriving `max_tokens` from the
|
|
113
|
+
* thinking level (4k–32k) makes thinking and the visible reply compete for the same
|
|
114
|
+
* small budget, so a deep-thinking step dies with `stop_reason=max_tokens` and no
|
|
115
|
+
* content ("output budget exhausted" dead turns). The thinking level keeps steering
|
|
116
|
+
* DEPTH via reasoningEffort/output_config; it no longer constrains output size.
|
|
117
|
+
* Uncatalogued models (local/live ids) keep the legacy thinking-table budget. */
|
|
118
|
+
export function resolveMaxOutputTokens(
|
|
119
|
+
model?: string,
|
|
120
|
+
level?: "minimal" | "low" | "medium" | "high" | "xhigh",
|
|
121
|
+
): number {
|
|
122
|
+
const meta = model ? findCatalogEntry(expandAlias(model, ALIAS_DEFAULTS)) : undefined;
|
|
123
|
+
if (!meta) return thinkingMaxTokens(level);
|
|
124
|
+
const envCap = Number(jeoEnv("MAX_OUTPUT_TOKENS"));
|
|
125
|
+
const cap = Number.isFinite(envCap) && envCap > 0 ? envCap : DEFAULT_MAX_OUTPUT_CAP;
|
|
126
|
+
return Math.min(findCatalogModel(meta.id)?.maxOutputTokens ?? cap, cap);
|
|
127
|
+
}
|
|
128
|
+
|
|
103
129
|
/** Map the thinking level to an OpenAI reasoning-effort tier. minimal/low/medium/high pass
|
|
104
130
|
* through unchanged and xhigh folds to high (the deepest tier the provider APIs accept), so
|
|
105
131
|
* reasoning works at EVERY thinking level (gajae parity: minimal is a real effort). Only an
|
|
@@ -168,19 +194,18 @@ const ALIAS_DEFAULTS = { fast: "ollama/qwen2.5:0.5b", local: "ollama/qwen2.5:0.5
|
|
|
168
194
|
* requestMaxRetries + 1. When unset, the `withRetry` defaults apply (3 attempts),
|
|
169
195
|
* but rate-limit (429) errors get a more generous budget + a backoff floor so a
|
|
170
196
|
* transient per-minute window can clear instead of the very first 429 instantly
|
|
171
|
-
* exhausting auto-retry. A server-directed retry delay
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
197
|
+
* exhausting auto-retry. A server-directed retry delay is honored IN FULL — no
|
|
198
|
+
* default ceiling — because a generic 429 is retried forever, not treated as fatal
|
|
199
|
+
* past some arbitrary budget (gjc parity: even a multi-hour Anthropic rate-limit
|
|
200
|
+
* window is retried, not bailed on); `rateLimitMaxServerDelayMs` remains available
|
|
201
|
+
* as an explicit opt-in for a caller that truly cannot wait that long. Explicit
|
|
202
|
+
* config (`requestMaxRetries`/`maxDelayMs`) always wins and disables the matching
|
|
203
|
+
* rate-limit default. `maxDelayMs` caps per-attempt backoff when provided.
|
|
176
204
|
*/
|
|
177
205
|
const DEFAULT_RATE_LIMIT_RETRIES = 6; // total attempts for 429 (initial + 5 retries)
|
|
178
206
|
// 429 floor when the server sends no Retry-After. Escalates per attempt inside
|
|
179
207
|
// withRetry (2s → 4s → 8s → 16s → 30s ≈ 60s total), spanning a per-minute window.
|
|
180
208
|
const DEFAULT_RATE_LIMIT_MIN_DELAY_MS = 2000;
|
|
181
|
-
// GJC parity for server-directed 429s: retry short windows, but do not hang a CLI
|
|
182
|
-
// through long subscription/account resets.
|
|
183
|
-
const DEFAULT_RATE_LIMIT_MAX_SERVER_DELAY_MS = 5 * 60 * 1000;
|
|
184
209
|
export function resolveRetryOptions(retry: Config["retry"], kind: "request" | "stream" = "request"): RetryOptions {
|
|
185
210
|
const opts: RetryOptions = { isRetryable: defaultRetryable };
|
|
186
211
|
|
|
@@ -218,7 +243,12 @@ export function resolveRetryOptions(retry: Config["retry"], kind: "request" | "s
|
|
|
218
243
|
// 429 backoff floor: explicit wins; else default UNLESS the user pinned maxDelayMs.
|
|
219
244
|
if (typeof retry?.rateLimitMinDelayMs === "number") opts.rateLimitMinDelayMs = retry.rateLimitMinDelayMs;
|
|
220
245
|
else if (typeof retry?.maxDelayMs !== "number") opts.rateLimitMinDelayMs = DEFAULT_RATE_LIMIT_MIN_DELAY_MS;
|
|
221
|
-
|
|
246
|
+
// No default `rateLimitMaxServerDelayMs` ceiling: gjc parity retries a generic 429
|
|
247
|
+
// forever, honoring however long the server asks (see the doc comment above). Usage
|
|
248
|
+
// limits fail fast on their own via isUsageLimitError → defaultRetryable, well before a
|
|
249
|
+
// wait is ever computed, so this is safe. An explicit config value is still honored for
|
|
250
|
+
// a caller that truly cannot wait an unbounded server-directed delay.
|
|
251
|
+
if (typeof retry?.rateLimitMaxServerDelayMs === "number") opts.rateLimitMaxServerDelayMs = retry.rateLimitMaxServerDelayMs;
|
|
222
252
|
|
|
223
253
|
// Config-driven fail-fast overrides: a status in `failFastStatuses` or a message
|
|
224
254
|
// matching any `failFastPattern` is forced non-retryable, layered on top of the
|
|
@@ -255,12 +285,18 @@ export function resolveRetryOptions(retry: Config["retry"], kind: "request" | "s
|
|
|
255
285
|
/**
|
|
256
286
|
* Whether the bundled adapter can serve this provider+model over OAuth end-to-end.
|
|
257
287
|
* OpenAI OAuth (ChatGPT/Codex) only serves Codex models, so any other OpenAI model
|
|
258
|
-
* must use an API key.
|
|
288
|
+
* must use an API key. Kimi OAuth (Kimi Code subscription) only serves the Kimi Code
|
|
289
|
+
* catalog (api.kimi.com/coding) — Moonshot API-platform ids (kimi-latest, moonshot-v1-*)
|
|
290
|
+
* need a KIMI_API_KEY. A provider whose OAuth backend is not verified end-to-end
|
|
259
291
|
* cannot serve any model over OAuth. Everything else (Anthropic Messages, Gemini /
|
|
260
292
|
* Antigravity Cloud Code Assist) is served end-to-end.
|
|
261
293
|
*/
|
|
262
294
|
function oauthServesModel(provider: AuthProvider, model: string): boolean {
|
|
263
295
|
if (provider === "openai") return CODEX_MODELS.includes(model);
|
|
296
|
+
if (provider === "kimi") {
|
|
297
|
+
const wire = model.startsWith("kimi/") ? model.slice(5) : model;
|
|
298
|
+
return KIMI_CODE_MODELS.includes(wire);
|
|
299
|
+
}
|
|
264
300
|
if (isOAuthProvider(provider) && OAUTH_FLOW_REGISTRY[provider].verifiedEndToEnd === false) return false;
|
|
265
301
|
return true;
|
|
266
302
|
}
|
|
@@ -339,7 +375,7 @@ async function resolveCall(options: Partial<CallOptions>, kind: "request" | "str
|
|
|
339
375
|
model: providerModelFor(model),
|
|
340
376
|
systemPrompt: options.systemPrompt,
|
|
341
377
|
temperature: options.temperature ?? 0.2,
|
|
342
|
-
maxTokens: options.maxTokens ??
|
|
378
|
+
maxTokens: options.maxTokens ?? resolveMaxOutputTokens(model, config.thinkingLevel),
|
|
343
379
|
jsonMode: options.jsonMode,
|
|
344
380
|
baseUrl,
|
|
345
381
|
numCtx: options.numCtx ?? (provider === "ollama" ? (config as { ollamaNumCtx?: number }).ollamaNumCtx : undefined),
|
|
@@ -349,6 +385,7 @@ async function resolveCall(options: Partial<CallOptions>, kind: "request" | "str
|
|
|
349
385
|
onReasoning: options.onReasoning,
|
|
350
386
|
onReasoningArtifact: options.onReasoningArtifact,
|
|
351
387
|
tools: options.tools,
|
|
388
|
+
sessionKey: options.sessionKey,
|
|
352
389
|
};
|
|
353
390
|
// Caller-supplied retry sink rides on the config-derived retry budget so the
|
|
354
391
|
// engine/TUI can surface "rate limited — retrying in Ns" instead of a silent wait.
|
|
@@ -357,7 +394,12 @@ async function resolveCall(options: Partial<CallOptions>, kind: "request" | "str
|
|
|
357
394
|
// never replays after the first emitted chunk). Both fall back to `maxRetries`,
|
|
358
395
|
// and an unset stream budget keeps the conservative withRetry default — the
|
|
359
396
|
// generous gjc default of 100 only applies when the user configures it.
|
|
360
|
-
|
|
397
|
+
// `retry.signal` is the caller's ORIGINAL signal (Ctrl-C / turn abort) — not the
|
|
398
|
+
// per-attempt fetch timeout composed below — so a long, honored server-directed
|
|
399
|
+
// retry wait (see resolveRetryOptions) can still be cancelled by the user without
|
|
400
|
+
// waiting out the full delay, while a single attempt's timeout never cuts short
|
|
401
|
+
// the NEXT attempt's backoff sleep.
|
|
402
|
+
const retry: RetryOptions = { ...resolveRetryOptions(config.retry, kind), ...(options.onRetry ? { onRetry: options.onRetry } : {}), ...(callOptions.signal ? { signal: callOptions.signal } : {}) };
|
|
361
403
|
|
|
362
404
|
if (provider === "ollama" || provider === "lmstudio") {
|
|
363
405
|
return { adapter, callOptions, credential: { kind: "none", provider: "openai" }, retry };
|
|
@@ -390,6 +432,11 @@ async function resolveCall(options: Partial<CallOptions>, kind: "request" | "str
|
|
|
390
432
|
"OpenAI OAuth 자격증명은 Codex 모델(gpt-5.5/gpt-5.4)만 지원. OPENAI_API_KEY를 설정하거나 모델을 변경하세요"
|
|
391
433
|
);
|
|
392
434
|
}
|
|
435
|
+
if (provider === "kimi" && effective.kind === "oauth" && !oauthServesModel("kimi", model)) {
|
|
436
|
+
throw new Error(
|
|
437
|
+
`Kimi OAuth (Kimi Code subscription) serves only the Kimi Code models (${KIMI_CODE_MODELS.join(", ")}). Set KIMI_API_KEY for Moonshot API models, or pick a Kimi Code model with /model.`,
|
|
438
|
+
);
|
|
439
|
+
}
|
|
393
440
|
if (effective.kind === "none" && !isLocalOpenAi) {
|
|
394
441
|
throw new Error(
|
|
395
442
|
`No credential for provider '${provider}'. Run 'jeo setup', 'jeo auth login', or set ${provider.toUpperCase()}_API_KEY / ${provider.toUpperCase()}_OAUTH_TOKEN.`
|