Root cause of 'Invalid signature in thinking block' (400 at
messages.1.content.0, Claude models): build_messages_request replayed
EVERY stored Reasoning item as a thinking block with no origin check —
history synthesized by other backends (encrypted_content: None → the
mandatory signature field serialized as ""), Responses-API tco_* blobs
(signature bytes, no text), and blocks signed by a DIFFERENT model after
a mid-session /model switch. Anthropic validates every replayed
signature (model-bound), so such histories 400 deterministically. The
platform split was circumstantial: Windows sessions started on the
default model and switched to Claude; macOS sessions were Claude-native
from turn 1. Adversarially verified — no platform-divergent byte path
exists in capture, storage, or replay.
New prune_replayed_thinking pass (Pi/Claude Code replay policy): keep
exactly the final assistant message's thinking when its tool loop is
still open (request ends on the tool results — an open loop can never
span a model switch) and the block is genuinely signed; strip every
other thinking block (the API ignores valid prior-turn thinking and
rejects invalid). Assistant messages emptied by the strip (thinking-only
aborted turns) are removed — empty content arrays are rejected too.
Tests: three unit tests pin strip-outside-loop (unsigned, tco_*, stale
signed), keep-in-active-loop (verbatim text+signature at content.0), and
emptied-message removal; the legacy-upgrade integration test now proves
both wire fidelity in the active loop AND stripping once the loop
closes.
Verified: sampling-types + sampler + chat-state + shell 6072 tests
green, clippy clean.
The README always shipped graph engineering enabled; the enablement was
delegated to installer env plumbing that diverged per platform:
install.sh exported KIGI_GRAPH=1 into shell rc (worked), install.ps1
wrote the User registry variable — which running Windows terminals (and
new tabs of an open Windows Terminal) never pick up, so /graph was
'missing on Windows' despite a successful install.
With resolve_graph() defaulting true in the binary (e53a66d), the env
writes are redundant complexity: drop them from both installers, keep
KIGI_GRAPH=0 as the documented opt-out (env still beats the default),
and correct the flag comment to tell this story instead of a
'gray release' one. Both scripts syntax-checked (sh -n / pwsh parser).
Installers are served from main (raw.githubusercontent), so this takes
effect for all new installs immediately — no retag needed; the running
v0.1.5 build already carries the binary-default fix.
resolve_graph() read only the KIGI_GRAPH env var with default(false)
(plan.md G0 gate), so /graph existed solely on machines whose environment
exported the dev flag — which presented as '/graph is missing on
Windows'. There was never any platform-conditional code: the Mac worked
because the dev env var was set there.
Default is now true (matching /goal's shipped state); KIGI_GRAPH=0
remains the off-switch, and availability still requires the goal harness
(BuiltinGate::Graph). AGENTS.md updated; new test pins fresh-install-on
plus env-zero-off.
Verified: kigi-shell 5262 + kigi-tui 6874 tests green, clippy clean.
A single HTTP 502 from the GitHub release CDN killed the entire v0.1.5
tag build (one job of five, ~50 minutes wasted). The build-script
download now retries twice with backoff on 5xx/429/network errors;
genuine failures (404, offline) still fail fast with the
KIGI_SHELL_BUNDLE_RG_PATH hint, and every retry prints a cargo:warning
so flakiness stays observable.
GitHub Actions cache isolation lets a run restore caches only from its own
ref or the default branch. Releases run on tag refs and nothing ever ran
on main, so release.yml's rust-cache never restored anything — every
release compiled all 62 crates cold on five targets (~50 min, gated by
Windows at ~49.5 min).
warm-cache.yml builds the same release-dist profile with the same
rust-cache key on main (dep-affecting pushes — including each release's
version-bump commit — plus a weekly refresh against 7-day eviction and
manual dispatch). Tag builds then restore a warm default-branch cache;
the remaining cost is workspace-crate compilation + the codegen-units=1
thin-LTO link, which is the deliberate release-hardening tradeoff.
Since v0.1.4:
- fix(fs): Windows-safe atomic replace everywhere (util::fs::replace_file)
— model+effort switches now persist on Windows; models cache and session
state writes no longer fail silently under AV/indexer file locks
- fix(tui): a default-model persist failure keeps the live session's model
instead of reverting the pick
- docs: AGENTS.md records the replace_file contract
Persist-failure ≠ switch-failure. The default_model rollback arm re-ran
set_default_model_inner(prev) AND issued a reverse SwitchModel whenever
the config.toml write failed — the only mechanism in the codebase that
deliberately re-shows the ORIGINAL model after a successful pick. On
Windows, where AV/indexer file locks routinely failed that write (until
27d009c), every /model selection appeared to not take.
The session switch succeeds independently and reports its own failures
via handle_switch_model_complete; a disk-persist failure now keeps the
live model, logs, and surfaces only the 'Could not save' toast — the
same policy PersistPreferredModel already ships ('still active for this
session').
Verified: kigi-tui 6874 tests green, clippy clean.
Root cause of 'model+effort switch works on Mac, not on Windows': the
switch APPLIES in-session (the dispatch/apply chain is platform-identical,
verified adversarially) but its persistence never sticks on Windows.
Every tmp+rename atomic write except auth/storage.rs committed with a
bare fs::rename, and Windows MoveFileExW(REPLACE_EXISTING) fails with a
sharing violation whenever AV/search-indexer/cloud-sync transiently holds
the destination open. Consequences: [models].default never persisted
(next launch = original model), the session summary's current model never
persisted (resume = original model), and the models cache went silently
stale (all its write errors were swallowed).
- New kigi_shell_base::util::fs::replace_file — THE commit step for
tmp+rename: plain rename on Unix; on Windows delete-first + two short
backoffs (the pattern auth/storage.rs shipped first), tmp cleaned on
failure, error always returned. Windows branch type-checked against
x86_64-pc-windows-msvc.
- Adopted at every replace site: config.toml (save_config /
atomic_write_string / mcp saves), models cache (plus unique tmp
suffixes and tracing::warn on failure — writes were fully silent),
session storage (summary/current-model, jsonl, plan/signals/
announcement/goal/graph state), auth.json, active-sessions registry,
prompt history, claude/kimi import, campaigns state, goal artifacts.
Directory-move renames (worktree pool, corrupt-file backups) keep
plain rename — their destinations don't pre-exist.
Verified: kigi-shell + kigi-shell-base 5318 tests green, clippy clean,
msvc-target check of the new cfg(windows) code clean.
Since v0.1.3:
- refactor(auth): CredentialAuthority — one chokepoint for inference
credential routing; SessionCredential is unforgeable outside it
- fix(tui): login picker scrolls in a capped viewport; moon never clips
- fix(tui): each OAuth picker row routes to its own provider (LoginWith),
no more every-row-opens-kimi.com
- feat(login): /login opens the provider picker with green connected
badges; mid-session picker cancels back to the session
- fix(models): stored subscription-OAuth sessions are catalog fetch
sources — a claude-only login gets claude models and a claude default,
not the bundled Kimi table with an 'unknown' model
- feat(tui): /model + Enter opens the model picker (required-args
commands block-and-reopen); rows name their connected provider
The shell stamps meta.provider (platform display name via
parse_managed_model_key) on every managed {platform}/{model} catalog
entry in to_acp_model_info; user-defined [model.*] entries stay
provider-less. The /model dropdown surfaces it in the description column
(a model's own description still wins), so the picker reads as 'choose a
model from your connected providers'.
Verified: kigi-shell 5261 + kigi-tui 6873 tests green, clippy clean.
A required-args slash command submitted bare (/model, or /m) ran straight
into its usage error. The completeness contract already existed —
is_command_complete's documented 'Blocks' row — but had zero non-test
callers; both dispatch paths ran the command unconditionally.
Both the session prompt path and the dashboard dispatch box now consult
is_command_complete before running: an incomplete required-args command
re-opens the input as '/{command} ' with the cursor in the args phase, so
the existing suggestion dropdown lists the choices (for /model: the
catalog — by construction the connected providers' models, after
1bb10ef). Generalizes to every future required-args command; no new UI.
Verified: kigi-tui 6872 tests green, clippy clean.
Root cause of 'connected Claude, still shows Kimi / unknown model': every
fetch-plan decision consulted only the primary (Kimi) session and API keys
— never the stored subscription-OAuth sessions:
- on_auth_changed's wipe guard: a claude-pro-max-only login satisfied
'no session, no keys' → catalog wiped, early return BEFORE the fetch
and BEFORE notify_models_updated. The TUI kept an empty picker and the
prompt bar rendered 'unknown'. Guard decision extracted into the pure
should_wipe_catalog_on_auth_change (matrix-tested); a stored OAuth
session now vetoes the wipe, so the fetch runs, the first real catalog
reselects the default model (first entry = the connected provider's),
and kigi/models/update reaches the client.
- Startup prefetch: the arming gate ignored stored OAuth sessions and the
prefetch thread passed an empty token map — a claude-only user booted
onto the bundled Kimi table until a later refresh. The gate now takes
has_stored_oauth and the thread resolves each stored session's bearer
(refresh-on-expiry) via a current-thread runtime.
- Cache origins: from_config's startup cache load and cache_origin()
computed the fetch-plan origin with an empty token map, so a
claude-inclusive cached catalog never matched at startup. Both now use
presence-only stubs (stored_oauth_token_stubs — names only, no bearers)
proven equal to the real-token origin by test.
New probes in models_fetch: stored_oauth_platforms / stored_oauth_token_
stubs (sync auth.json scope scan; no AuthManager, no secrets).
Verified: kigi-shell 5260 tests green, clippy clean.
/login previously fired the resolved method's flow immediately — there was
no way to see providers or their status. It now lands on the provider
picker; already-connected providers show a green 'connected' badge in the
key column.
Shell: initialize() probes stored credentials once (primary session flag,
auth.json oauth/<provider> scopes, resolved platform keys env>auth.json>
config) and stamps _meta.connected on each advertised method
(connected_method_ids + stamp_connected_meta, pure and unit-tested).
Display state only — never an authorization input.
TUI:
- PendingMenuItem::connected() reads the badge from method meta; the
picker renders it green (accent_success), replacing the shortcut hint.
- New Action::OpenLoginPicker: /login shows the picker; mid-session it
stashes the view like dispatch_login, and starts no flow by itself.
- Mid-session picker: last row reads 'Cancel' and dispatches CancelLogin
(clicking it must not exit the app); Esc also returns to the session.
- After a successful login, the just-authenticated method is stamped
connected in the TUI's advertised-methods copy (auth_in_flight_method →
AuthComplete), so a later /login shows the badge without re-initialize.
Verified: kigi-shell 5256 + kigi-tui 6870 tests green, clippy clean.
Root cause: PendingMenuItem::Login carried only a label, so every provider
row collapsed to the id-less Action::Login, and dispatch_login resolved
the FIRST advertised interactive method — the Kimi device flow. Selecting
Grok/Claude/Copilot/Codex all opened kimi.com. (The shell side was already
correct: authenticate() dispatches each method id to its own OAuth flow.)
- PendingMenuItem::Login now carries the advertised method id (None only
on the no-interactive-method fallback row).
- New Action::LoginWith(AuthMethodId); the picker dispatches it with the
row's own id. Action::Login keeps its meaning (resolved/default method)
for /login, auto-login, and re-auth.
- dispatch_login_with resolves the id against the advertised methods and
FAILS CLOSED on an unknown id — no silent first-method fallback — then
adopts the method's label and start mode.
Tests: picker rows pinned to their method ids; LoginWith(claude-pro-max)
must authenticate with claude-pro-max even when kimi-code was previously
resolved; unknown ids surface an error and start nothing.
Verified: kigi-tui 6867 tests green, clippy clean.
Root cause (issues: unscrollable list + half-blocked moon): the stacked
welcome layout gave the menu one Length row per item. With ~30 advertised
providers the vertical layout overflowed and the constraint solver
squeezed every Length — clipping the logo — while rows past the fold were
silently unreachable.
- WelcomeLayout::compute_inner caps the menu to the rows genuinely left
over after logo/prompt/version, so the chrome never gets squeezed.
- render_menu is now a minimal-scroll viewport: it scrolls only when the
selection exits it (stable under hover — no re-centering feedback
loop), draws the picker-style scrollbar, and returns index-aligned hit
rects (zero Rect for off-screen rows) plus the offset, which AppView
feeds back next frame.
- Mouse wheel moves the login-picker selection (clamped, not wrapping).
Test: pending_menu_scrolls_and_never_clips_the_moon renders the full
method list at 40 rows and asserts the whole moon (10 braille rows), the
version badge, and that selecting Quit scrolls it into view.
Verified: kigi-tui 6864 tests green, clippy clean.
One authority answers 'which credential may ride this request':
credential_class / manager_for / credential_for / bearer_resolver_for,
keyed by (platform, base_url). SessionCredential is an opaque type with
no production constructor, so a new call site cannot re-introduce the
session-bearer leak. Platform-scoped tests extended across all bearer
channels (session, aux, summary, subagent override).
Verified: cargo check --workspace --all-targets clean; kigi-shell and
kigi-tui suites green (6611+ tests).
29th platform `openai-codex` (uses_oauth, Responses wire). PKCE-localhost login at
auth.openai.com (client app_EMoamEEZ73f0CkXaXp7hrann, redirect localhost:1455/auth/
callback, form token exchange, fresh-random state) reusing the claude-pro-max flow;
OAuthFlow::PkceLocalhost gained a redirect_path and OAuthConfig an authorize_extra
(empty elsewhere, so claude/xai/copilot authorize URLs stay byte-identical).
Codex-specific: the access token is a JWT carrying chatgpt_account_id, which becomes
the `chatgpt-account-id` inference header. It is derived STATELESSLY from whichever
bearer rides each request (so a rotated token needs no persisted field), and BOTH
login and refresh fail fast when the claim is absent — gated on the explicit
OAuthConfig.requires_chatgpt_account_id fact, never inferred from the token-body
encoding (a plain form endpoint is the OAuth norm and must not inherit this).
Inference rides the existing Responses wire at chatgpt.com/backend-api/codex →
/responses, with codex headers (chatgpt-account-id, originator, OpenAI-Beta
responses=experimental, codex UA) gated on SamplerConfig.openai_codex so API-key
`openai` stays byte-identical; store:false was already the global Responses default.
Catalog is HARDCODED (no live endpoint exists for this backend; read from the
official Codex CLI's model cache): gpt-5.6-sol/terra/luna + gpt-5.5, ctx 272000,
each with its real reasoning levels (low..ultra — ReasoningEffort gained Ultra).
Excluded: gpt-5.3-codex-spark (supported_in_api=false), gpt-5.4/-mini and
codex-auto-review (hidden) — they would list but fail at inference. The fetch
short-circuits before any HTTP; Kigi never shells out to the codex CLI or reads
~/.codex.
Security review fixes: redact any `account-id` header from request logs (it was
reaching debug logs), strict 3-segment JWT check (fail closed), refresh no longer
fails open on a missing claim. Inherits leak-safe pooled routing (scope
oauth/openai-codex) — never the Kimi token. Full gate green (234 suites, 0 warnings).
28th platform `github-copilot` (uses_oauth, ChatCompletions wire). Two-stage auth:
RFC-8628 GitHub device flow (client Iv1.b507a08c87ecfe98, scope read:user, errors
in a 200 body) mints the DURABLE github token; a GET api.github.com/copilot_internal/
v2/token exchange re-mints the SHORT-LIVED copilot token. Persisted as key=copilot
token, refresh_token=github token, expires_at=copilot expiry; the "refresh" is a
copilot-token re-mint (not a refresh_token grant), dispatched via
OAuthTokenBody::GithubCopilotExchange in the generic refresher.
VS Code editor-identity headers on /models + /chat/completions, gated on
SamplerConfig.github_copilot / PlatformId::sends_copilot_editor_headers() so every
other ChatCompletions provider stays byte-identical. Live /models filtered
(parse_github_copilot_listing) to the openai-completions-served models: keep iff
model_picker_enabled && policy.state!="disabled" && tool_calls!=false AND not a
claude-4.x/5.x (messages) or gpt-5/oswe/mai- (responses-only) id — those need
per-model wire routing (documented debt), excluded rather than mis-routed.
Inherits the leak-safe pooled routing (scope oauth/github-copilot); its bearer/
refresh/api_key never touch the Kimi token (regression test added). Fail-fast on
an out-of-range copilot expires_at (would otherwise silently 401 mid-session).
Adversarial security review: GO, no CRITICAL/HIGH. Known limitation: Pi's
per-model policy-enablement POST is not ported (documented in AGENTS.md).
27th registry variant, 2nd subscription-OAuth provider. Log in with a Claude
Pro/Max subscription via PKCE authorization-code + S256 (loopback callback on
127.0.0.1:53692, with a manual code-paste fallback), then use it against
api.anthropic.com — reusing the existing Anthropic Messages wire + Anthropic
listing + the multi-provider OAuth foundation (dbce6bf). Sourced from Pi
(earendil-works/pi auth/oauth/anthropic.ts): client 9d1c250a..., authorize
claude.ai/oauth/authorize, token platform.claude.com/v1/oauth/token, scope
'…user:inference user:sessions:claude_code…'.
New machinery (foundation handles token routing — claude-pro-max is a
uses_oauth platform so its bearer/refresh/api_key already route to its own
pooled manager, never Kimi):
- OAuthConfig gains flow{DeviceCode|PkceLocalhost} + token_host + token_body
{Form|JSON}; xai/kimi rows unchanged (DeviceCode/Form).
- auth/oauth_pkce.rs: PKCE S256 wire — loopback listener with STRICT state
validation (CSRF, fail-closed), manual-paste fallback, JSON code→token
exchange + rotating-refresh. Never logs code/verifier/tokens.
- Messages OAuth adaptation gated on SamplerConfig.anthropic_oauth (true only
for a claude-pro-max managed key): Authorization: Bearer + anthropic-beta
oauth + user-agent claude-cli + x-app cli, and the required 'You are Claude
Code' system prefix. API-key anthropic/minimax Messages requests are
BYTE-IDENTICAL (regression-guarded).
- Live /models under the OAuth Bearer + oauth-beta headers (Anthropic listing,
enriched from models.dev anthropic); persistent 401 → 0 models + WARN, NO
hardcoded fallback list (honest failure).
Adversarial review: no blocking findings (secret handling, CSRF/state, the
anthropic_oauth gate, token routing, non-regression all CONFIRMED). Full gate
green. Registry at 27; picker updated. Residual (unverifiable without a real
Claude Pro/Max account): whether GET /v1/models accepts the OAuth bearer, and
the real endpoint's acceptance of the OAuth Messages request.
First subscription-OAuth provider beyond Kimi Code (26th registry variant).
Log in with a Grok/SuperGrok/X subscription via RFC-8628 device-code OAuth
(auth.x.ai), then use it against api.x.ai/v1 — reusing the existing xai wire
(ChatCompletions + OpenAI listing + Passthrough + restrict + models_dev_id
xai). Sourced from Pi (earendil-works/pi auth/oauth/xai.ts): client
b1a00492..., scope 'openid profile email offline_access grok-cli:access
api:access', standard Bearer (no x-xai-token-auth).
Foundation (generalizes Kigi's Kimi-singleton OAuth to per-provider, root
cause, not a patch):
- Registry: OAuthConfig on PlatformSpec (client_id/host/device+token
paths/scope/scope_key); XAI_OAUTH_CONFIG + XAI_GROK_SPEC (uses_oauth, method
id 'xai-grok', an interactive login after kimi-code).
- Generic device-code wire (auth/oauth_device.rs) + GenericDeviceRefresher,
sharing the RFC-8628 core with Kimi; Kimi's bespoke flow is byte-identical
(X-Msh headers, KIMI_CODE_OAUTH_SCOPE, keyring gating unchanged).
- Per-provider AuthManager via a process-global pool (auth/oauth_registry.rs):
build-on-demand with start_proactive_refresh, keyed by scope. The session
resolves the AuthManager for the ACTIVE model's platform for bearer/refresh/
401-recovery/api_key — an oauth-platform model always uses its OWN token,
never the primary.
- Live /models under OAuth; base routes oauth().is_some() -> platform.base_url()
(kimi-code stays on proxy_url).
Security: adversarial review + a systematic token-leak audit found and closed
FIVE channels where the primary Kimi token could reach api.x.ai (bearer
resolver, api_key stamping, aux summary/classifier/image-describe models, and
subagent model-override). Each fix routes through the platform-aware resolver
(the oauth model's pooled token or None, NEVER the primary) and is revert-to-red
verified. No access/refresh token is ever logged.
Registry at 26; picker updated (xai-grok interactive login row); TUI
context-window already auto-updates per model. Full gate green (234 suites,
fmt, clippy -D warnings, deny). GPT/Claude/Grok officially permit third-party
subscription use.
Kigi started as an unofficial Kimi Code CLI community build; by request it
now works with 25 providers. Replace the stale three-platform table with the
full supported list (Kimi Code OAuth + 24 API-key providers, each with its
platform id and env var). Unsupported/not-yet-built providers are omitted.
Providers 21-22 (24th & 25th registry variants), sourced from Pi
(earendil-works/pi). Pi drives MiniMax through its Anthropic-COMPATIBLE
surface (baseUrl .../anthropic), so Kigi reuses the existing Anthropic
Messages machinery (wire_api=Messages, listing=Anthropic, key_header=XApiKey
x-api-key+anthropic-version) rather than the OpenAI path. Global:
api.minimax.io/anthropic, MINIMAX_API_KEY, models.dev minimax. China:
api.minimaxi.com/anthropic, MINIMAX_CN_API_KEY, models.dev minimax-cn.
The base carries the /v1 suffix (.../anthropic/v1) since Kigi appends bare
paths → listing .../anthropic/v1/models?limit=1000, inference
.../anthropic/v1/messages (matches the live-probed x-api-key-gated endpoint).
restrict_to_enriched=FALSE: the 7 MiniMax-M* models are clean (no pollution)
and restrict would drop launch-day models not yet in models.dev.
Also HARDENS parse_anthropic_listing to tolerate a bare array in addition to
the {data:[...]} envelope (mirrors parse_openai_listing's sniff that Together
taught us) — so MiniMax's Anthropic-compatible /models can't silently empty
the catalog if it serves a bare array. A bare object without data still errors.
Review found no defects (5 areas CONFIRMED incl. the /v1 non-doubling, the
additive parser change, restrict=false rationale). Residual (logged): the live
200 body / anthropic-version acceptance is unverifiable without a key; the
bare-array tolerance + restrict=false hedge most shapes.
Tests: e2e mocks the x-api-key-gated Anthropic listing (proves the auth header
+ enrichment + keying under minimax/); both validation tests reject 401 with
the per-variant console host; new parser test covers envelope + bare array +
bare-object-errors. Registry at 25; picker 26 rows.
Providers 19-20 (22nd & 23rd registry variants), sourced from Pi
(earendil-works/pi). Pi's xiaomi.ts / xiaomi-token-plan-cn.ts use plain
openAICompletionsApi (no thinking dialect) → Kigi Passthrough. Global: MiMo
at api.xiaomimimo.com/v1, XIAOMI_API_KEY, models.dev xiaomi. China token plan:
token-plan-cn.xiaomimimo.com/v1, XIAOMI_TOKEN_PLAN_CN_API_KEY, models.dev
xiaomi-token-plan-cn. Both Bearer + OpenAI listing + ChatCompletions +
restrict_to_enriched; /models auth-gated → validator.
id-match proven vs Pi's static ids (mimo-v2.5-pro, mimo-v2-flash, ...). The
CN token plan lists 4 mimo TTS models (tool_call=false); restrict's tool_call
cut drops them, keeping only the 3 chat models — the e2e proves this
non-vacuously with the real mimo-v2-tts. Review found no defects.
Registry at 23; picker 24 rows (welcome picker verified rendering all rows at
a taller viewport).
Providers 17-18 (20th & 21st registry variants), sourced authoritatively
from Pi (earendil-works/pi), the open-source agent whose provider list is
being mirrored. Pi's zai.ts / zai-coding-cn.ts use plain openAICompletionsApi
(NO special thinking dialect — overturns the matrix's 'thinking:{type} → new
dialect' concern), so Kigi maps them to Passthrough. Global: api.z.ai/api/
coding/paas/v4, ZAI_API_KEY, models.dev zai-coding-plan. China (Zhipu
BigModel): open.bigmodel.cn/api/coding/paas/v4, ZAI_CODING_CN_API_KEY,
models.dev zhipuai-coding-plan. Both Bearer + OpenAI listing + ChatCompletions
+ restrict_to_enriched; /models is auth-gated → validator.
id-match is PROVEN (not just assumed like Qwen): Pi's static model ids
[glm-4.5-air, glm-4.7, glm-5-turbo, glm-5.1, glm-5.2, glm-5v-turbo] are
byte-identical to the models.dev zai-coding-plan keys, all tool_call=true, so
restrict keeps every model with no silent-empty risk. Review found no defects.
GLM thinking is not lost (reasoning_content is parsed regardless of dialect).
Tests: e2e proves enrichment-supplied context + non-vacuous restrict (a
non-enriched wire model is dropped) + Passthrough; both variants' validation
tests hit /models and assert the per-variant console host (z.ai vs
open.bigmodel.cn). Registry at 21; picker 22 rows.
Also fixes the welcome login-picker test for the now-taller menu (renders at
a taller viewport to verify content coverage) and logs the real menu-overflow
UX debt: the picker clips rows past the fold with no scroll (q/l shortcuts
still work; only shown when unauthenticated) — deferred to its own cycle.
Provider 16 (19th registry variant). Same endpoint + models + Kimi dialect
as the existing OAuth kimi-code platform (api.kimi.com/coding/v1 via the
KIGI_CODE_BASE_URL override), but authenticated with a static KIMI_API_KEY
instead of the device flow — for users who have a Kimi For Coding key rather
than an OAuth subscription. Bearer, OpenAI listing, ChatCompletions,
ChatCompat::Kimi, wire_serves_metadata=true (Kimi /models self-serves
context/thinking), restrict_to_enriched=false (clean 3-model catalog).
/coding/v1/models is auth-gated (401) so it doubles as the validator.
No collision: KIMI_API_KEY was previously unused (grep-verified), and the
house BYOK reads only KIGI_API_KEY/XAI_API_KEY/legacy. kimi-code (OAuth) and
kimi-coding (static key) are independently gated (OAuth-token vs key) and
their models get distinct managed keys (kimi-code/k3 vs kimi-coding/k3) — a
user with both simply sees each Kimi model twice; no dedup collision, no crash.
Review (6 areas): no blocking defects; confirmed the spec correctly mirrors
KIMI_CODE_SPEC (differing only in uses_oauth/api_key_envs/console_host/labels)
and the coexistence is benign. Strengthened the e2e's dialect assertion (Kimi
is the default ChatCompat, so it did not discriminate a parse failure) by
also asserting parse_managed_model_key attributes the key to KimiCoding.
Tests: e2e proves wire-served context (1_048_576 from the wire) with the
models.dev fetch SKIPPED (all-wire-metadata provider, .expect(0)), bare-id
round-trip under kimi-coding/, Kimi dialect; validation test rejects a 401
from /models. Registry at 19; picker 20 rows; snapshot already bundles
kimi-for-coding.
Providers 14-15 (17th & 18th registry variants), Alibaba DashScope
compatible-mode. Global: token-plan.ap-southeast-1.maas.aliyuncs.com,
QWEN_TOKEN_PLAN_API_KEY, models.dev alibaba-token-plan. China:
token-plan.cn-beijing.maas.aliyuncs.com, QWEN_TOKEN_PLAN_CN_API_KEY,
alibaba-token-plan-cn. Both Bearer + OpenAI listing + ChatCompletions +
Passthrough (stream_options.include_usage is documented-supported).
/models is auth-gated (401 without a key) so it doubles as the validator;
metadata from models.dev enrichment. The token plan is a multi-vendor
catalog (deepseek/kimi/minimax/glm/qwen); restrict_to_enriched keeps the 15
tool-calling chat models and drops the 4 qwen-image/wan image generators.
Review downgraded two flagged concerns: the enable_thinking non-streaming
400 cannot occur (Kigi never issues non-streaming ChatCompletions in
production — all inference streams), and Qwen thinking is NOT invisible
(reasoning_content is parsed regardless of the Passthrough dialect). No
defects. Residual (logged): restrict does an exact id-match of live /models
ids vs the models.dev keys; a mismatch fails safe (0 models) — verify with a
real key. Snapshot already bundles both providers (regenerated in ebf1105).
Tests: global e2e proves enrichment-supplied context (wire carries none),
non-vacuous tool_call restriction (qwen-image dropped), bare-id round-trip,
Passthrough; both variants' validation tests hit /models (401 reject) and
assert the correct per-variant console host. Registry at 18; picker 19 rows.
13th provider (16th registry variant). Also reconciles a naming collision
the matrix flagged: this fork is house-branded "xai" (cf. xai.dev metadata,
KIGI_CODE_XAI_API_KEY legacy env), so XAI_API_KEY + method xai.api_key were
the GENERIC house BYOK, not x.ai/Grok. The provider table wants xai/XAI_API_KEY
for Grok.
Resolution (user-approved): XAI_API_KEY now keys the x.ai/Grok provider; the
house BYOK primary env moves to KIGI_API_KEY, keeping XAI_API_KEY and
KIGI_CODE_XAI_API_KEY as back-compat fallbacks (read_xai_api_key_env checks
KIGI_API_KEY first). The xai.api_key method id is unchanged (persisted-session
compat); the platform method id is the bare "xai", distinct from it.
xAI spec: api.x.ai/v1, Bearer, OpenAI listing + ChatCompletions, Passthrough
(docs confirm stream_options.include_usage accepted). /v1/models is minimal
(ids only) and requires auth, so it doubles as the key validator (401 on bad
key, no override) and metadata comes from models.dev enrichment. Live ids
match the models.dev "xai" keys byte-for-byte, so restrict_to_enriched keeps
the 5 tool-calling chat models (grok-4.5/4.3/4.20-0309-*/build-0.1) and drops
the grok-imagine-* generators + the non-tool multi-agent model. Snapshot
regenerated to include the xai provider (was stale; gen script already listed
it in TARGETS).
Env migration is comprehensive to avoid keying the xai platform (which would
trigger a live api.x.ai fetch) or leaving house-key reads stranded: routed
the trace CLI resolver + acp_agent/auth.json bridge + paste-key ext handler
through the new primary; moved all leader/pager/e2e harness setters to
KIGI_API_KEY; made every house-key isolation test unset KIGI_API_KEY too;
updated user-facing hints to name KIGI_API_KEY.
Tests: e2e proves enrichment-supplied context (wire carries none), non-vacuous
tool_call restriction, bare-id round-trip under xai/, Passthrough; validation
tests hit /models (401 reject, 200 accept); house_env_var_takes_precedence_over_xai
pins the new precedence. Registry at 16; picker 17 rows; 4 auth arrays + xai.
Review (16 findings, all fixed): caught a missed else-branch env clear in the
paste-key handler (would leak the house key past a clear) and a non-hermetic
credential-priority test; both fixed.
12th provider. API-key via AI_GATEWAY_API_KEY, Bearer, OpenAI listing +
ChatCompletions, Passthrough dialect. Second wire-metadata provider but
takes the enrichment path instead: Vercel serves context under
context_window, which WireModel ignores (reads context_length), so
wire_serves_metadata=false + restrict_to_enriched pulls context/limits
from the models.dev "vercel" snapshot (302/306 live ids match snapshot
keys byte-for-byte, so restrict keeps essentially the whole catalog).
/models is public (200 for any key), so login validation targets
/credits (key_validation_path) which 401s on a bad bearer — avoids
false-accepting invalid keys against the public listing.
Tests: e2e proves enrichment-wins (wire context_window=999 distinct from
enrichment context=400000, asserts 400000) and non-vacuous tool_call
restriction; validation test proves /credits (not /models) is hit.
Registry at 15 (ordinal/VARIANT_COUNT/ALL), 4 auth arrays + 16-row picker.
The 14th registry row: id "nvidia", NVIDIA_API_KEY > auth.json "nvidia"
scope, https://integrate.api.nvidia.com/v1 with KIGI_NVIDIA_BASE_URL
override, Bearer, ChatCompletions, enrichment-backed metadata
(models_dev_id nvidia — the quirk matrix's earlier 'absent' claim was
wrong; models.dev has 84 nvidia models), restrict_to_enriched=true (the
NIM listing mixes chat/embedding/rerank/vision/image; keep the 45
tool-calling chat models). Slashed org/model ids
(nvidia/meta/llama-3.3-70b-instruct) round-trip via the first-slash split;
the native id rides the wire.
NIM exposes raw vLLM behavior and stream_options support varies per model
(some strict vLLM backends 4xx on it), so chat_compat=StrictOpenAi strips
stream_options — streaming works across the whole fleet, usage falls back
to estimation. Snapshot reasoning models carry no effort menus, so kigi
sends no reasoning_effort (which an unsupported strict validator would
400 on). Review: no defects. Logged note: a key lacking the org 'Public
API Endpoints' permission passes /models validation but 403s on chat
(user-fixable edge case).
The 13th registry row: id "cerebras", CEREBRAS_API_KEY > auth.json
"cerebras" scope, https://api.cerebras.ai/v1 with KIGI_CEREBRAS_BASE_URL
override, Bearer, ChatCompletions, enrichment-backed metadata
(models_dev_id cerebras).
Cerebras' catalog is all chat LLMs (no embedding/tts pollution) and its
/models is minimal (ids only), so restrict_to_enriched=FALSE: keep every
live model, enrich the known ones (context + effort menus low/medium/high),
unknown ones keep the default context. The e2e pins this enrich-without-
restrict path (new — prior enrichment providers all used restrict=true).
Review caught a likely-DOA defect: Cerebras uses strict
additionalProperties:false validation (confirmed 400-rejecting store,
maxTokens, thinking, nested reasoning_content), and stream_options is not
in its schema — so Passthrough (which keeps the stream_options.include_usage
kigi injects on every streaming request) would very likely 400 all
streaming. Generalized ChatCompat::Mistral -> ChatCompat::StrictOpenAi
(serde alias "mistral" keeps pre-rename persisted sessions loading), which
strips stream_options + private fields for any strict OpenAI-compat
validator; both Mistral and Cerebras now map to it. Future strict-validator
candidates (NVIDIA/Azure/Xiaomi/OpenCode) noted for the same check.
reasoning_effort (incl. "none") passes through; /v1/models requires auth
so key validation works; console cloud.cerebras.ai.
The 12th registry row: id "together", TOGETHER_API_KEY > auth.json
"together" scope, https://api.together.xyz/v1 with KIGI_TOGETHER_BASE_URL
override, Bearer, ChatCompletions + Passthrough, enrichment-backed
metadata (models_dev_id togetherai) with the tool-calling listing
restriction (Together's listing mixes chat/embedding/rerank/image types).
Together's GET /v1/models returns a BARE JSON ARRAY, not the OpenAI
{object:list,data:[]} envelope. New shared parser parse_openai_listing
tolerates both shapes (sniffs the top-level [ vs { for accurate
diagnostics); every OpenAI-listing provider now routes through it, with
byte-equivalent envelope behavior (verified: Groq/Google/OpenRouter
unchanged) and non-silent errors. Together's org/Model ids match the
togetherai snapshot keys exactly (verified), so restrict_to_enriched keeps
the ~27 tool-calling models without the id-shape trap.
Review: ship-ready, shared-parser change proven strictly-additive and safe,
registry/e2e/dialect all correct. Backlog logged: kigi ignores the wire
per-model field, so a live Together chat model absent from
models.dev is dropped until indexed — a future wire- filter would
unlock the fresh full catalog.
The 11th registry row and the first THIRD-PARTY wire_serves_metadata=true
provider: id "openrouter", OPENROUTER_API_KEY > auth.json "openrouter"
scope, https://openrouter.ai/api/v1 (note /api/v1) with
KIGI_OPENROUTER_BASE_URL override, Bearer, OpenAI listing +
ChatCompletions + Passthrough.
OpenRouter's public /models serves context_length for every model
(verified live: 340/340), so it needs NO enrichment: models_dev_id=None,
wire_serves_metadata=true, restrict_to_enriched=false. An OpenRouter-only
user makes zero models.dev calls; context comes straight from the listing.
Slashed ids (anthropic/claude-opus-4.8) round-trip through the managed key
via the first-slash split; the native id rides the wire. The e2e pins all
of this with the models.dev refresh disabled.
Review-confirmed defect fixed (and independently re-verified with live
curls): OpenRouter's /models is PUBLIC — GET /models returns 200 for ANY
key — so login key-validation would false-accept a bad key, deferring the
failure to the first chat 401. New spec field key_validation_path lets a
public-listing platform validate against an auth-requiring endpoint;
OpenRouter uses /key (401s for bad keys). Reusable for Vercel (also
public). Tests pin the /key validation and no regression to the default
/models path.
Gate caught a fixture regression: the kimi_import test used openrouter.ai
to represent a CUSTOM provider, which now correctly dedupes to the builtin
OpenRouter — moved the fixture to a reserved llm.example.test host that no
future platform can shadow.
The 10th registry row: id "google", GEMINI_API_KEY > auth.json "google"
scope, https://generativelanguage.googleapis.com/v1beta/openai (Gemini's
OpenAI-compatibility shim) with KIGI_GOOGLE_BASE_URL override, Bearer,
OpenAI listing + ChatCompletions + Passthrough, enrichment-backed
metadata (models_dev_id google), tool-calling listing restriction.
Review caught a ship-blocking defect: Gemini's compat /models returns
-PREFIXED ids (confirmed via Google's own cookbook), but the
models.dev snapshot keys and the chat endpoint use the BARE id. Without
normalization the enrichment lookup misses and restrict_to_enriched
silently empties the Gemini catalog (login works, zero models
selectable). A doc WebFetch had hidden this — the compat docs show bare
INPUT ids to retrieve/chat but never print the list OUTPUT.
Fix: new spec field strip_listing_id_prefix, applied in the fetch before
filter/enrich/keying. Google sets Some("models/") (defensive: a no-op if
an id is already bare, so correct regardless of the live shape); all other
rows None. The e2e now feeds the REAL prefixed listing ids and asserts
they survive as the bare managed key google/gemini-2.5-pro with the bare
id on the wire (chat rejects the prefix); a registry test pins the config.
Note: models.dev models Gemini reasoning as budget_tokens (not
effort-type), so no auto effort-menu — models still fully work; reasoning
is dynamic. Gemini compat applies default safety filters (no BLOCK_NONE).
The 9th registry row, pure Groq pattern: id "fireworks",
FIREWORKS_API_KEY > auth.json "fireworks" scope,
https://api.fireworks.ai/inference/v1 (note /inference/v1) with
KIGI_FIREWORKS_BASE_URL override, OpenAI listing + ChatCompletions +
Passthrough dialect, enrichment-backed metadata (models_dev_id
fireworks-ai) with the tool-calling listing restriction.
Fireworks native ids are deeply slashed (accounts/fireworks/models/glm-5p2);
the e2e pins the full round-trip: the managed key
(fireworks/accounts/fireworks/models/glm-5p2) parses back on the first
slash, and — the 404-risk property — the NATIVE id rides the inference
wire (entry.model and the resolved SamplerConfig.model) while the
fireworks/ prefix stays internal routing only.
Review (models.dev provider.toml + Fireworks docs): row facts confirmed,
registry integrity at 9, counts complete, e2e strong on all axes, zero
defects. One tradeoff logged as debt: restrict_to_enriched drops
fine-tuned/account-scoped deployed models (a headline Fireworks feature)
that can never be in models.dev.
The 8th registry row: id "mistral", MISTRAL_API_KEY > auth.json
"mistral" scope, https://api.mistral.ai/v1 with KIGI_MISTRAL_BASE_URL
override, enrichment-backed metadata with the tool-calling listing
restriction (embed/moderation/OCR noise).
Mistral is NOT a pure-pattern provider — an adversarial review found two
doc-confirmed blockers that no test exercises (no e2e covers a chat POST),
so the gate-green registry row alone would have shipped it DOA. A research
workflow pinned the exact wire shapes against the mistralai/client-python
SDK source (adversarially verified), then both were fixed:
1. stream_options 422: Mistral's strict Pydantic validator rejects the
stream_options.include_usage field kigi injects on every streaming
request (the SDK's request model has no such field). New
ChatCompat::Mistral dialect strips it (plus the kigi-private message
fields, like Passthrough). Streaming usage falls back to token
estimation.
2. Reasoning content arrays: Mistral reasoning models return content as
Union[str, List[ContentChunk]] on both streaming and non-streaming,
which the flat Option<String> path could not decode -> aborted turn.
A UNIVERSAL lenient deserializer (#[serde(from = "Raw..")] on
ChatResponseMessage + ChatChunkDelta) accepts string-or-array, routing
{type:text} chunks to the answer and the nested text of {type:thinking}
chunks to reasoning_content, tolerant of the OPEN chunk union (unknown
types ignored, never fatal). String content stays byte-identical for
every other provider (kimi/deepseek/groq/BYOK).
Review refuted all seven attack lines (no regression, no crash, exhaustive)
and flagged one coverage gap, now closed: a stream-consumer integration
test drives a full thinking -> transition -> answer chunk sequence and
proves it yields the same reasoning-sibling + assistant-answer result as
the reasoning_content string path.
Also folds a verified quirk matrix for all 23 remaining API providers into
providers-plan.md, tiered by real difficulty (self-enriching OpenRouter/
Vercel; bare-array Together listing; Messages-dialect MiniMax reusing the
Anthropic machinery; non-Bearer Azure/Bedrock; router wildcards; the OAuth
block).
The 7th registry row and the first pure-pattern cycle: id "groq",
GROQ_API_KEY > auth.json "groq" scope, https://api.groq.com/openai/v1
with KIGI_GROQ_BASE_URL override, OpenAI listing + ChatCompletions +
Passthrough dialect (Groq accepts the OpenAI-style reasoning_effort
scalar verbatim), enrichment-backed metadata with the tool-calling
restriction (the listing carries whisper/tts/guard noise — 8 of 15
enrichment entries are non-chat).
Review verdict: faithful pattern repeat, zero blocking findings — row
facts verified against live Groq docs, registry integrity at 7, e2e
proven strong on both axes (restriction + dialect mapping). Added the
recommended pin: managed keys split on the FIRST slash, so Groq's
provider-native slashed ids (openai/gpt-oss-120b and 10 more) round-trip
as groq/openai/gpt-oss-120b → (Groq, openai/gpt-oss-120b).
Also fixes a pre-existing test race this cycle surfaced: the enterprise-
endpoints test asserted moonshot's fixed base while non-serial, racing
serial tests that legitimately point KIGI_MOONSHOT_CN_BASE_URL at
wiremock; now serial + env-unset like its documented siblings. Two
display-only advisories logged as tracked debt (Groq's delta.reasoning
field invisible in the TUI; platform-generic rate-limit copy).
The 6th registry row: id "deepseek", DEEPSEEK_API_KEY > auth.json
"deepseek" scope, base https://api.deepseek.com (chat rides
{base}/chat/completions per official docs) with KIGI_DEEPSEEK_BASE_URL
override, enrichment-backed metadata (1M context, 384k output cap,
high/max effort menu).
Structural fix the cycle exposed: kigi's Kimi-specific body adaptation
ran UNCONDITIONALLY on every ChatCompletions request. New ChatCompat
dialect, declared per platform row and threaded through SamplerConfig,
ClientDefaults, and the session-persisted SamplingConfig (serde-default
Kimi keeps restored pre-field sessions and BYOK endpoints byte-identical;
production persist seams copy it; subagents inherit it):
- Kimi: full legacy pipeline (dispatch ≡ legacy pinned)
- DeepSeek: thinking:{type, reasoning_effort} per api-docs.deepseek.com
(server maps low/medium→high, xhigh→max itself; none disables; absent
leaves the server default)
- Passthrough: OpenAI-style reasoning_effort scalar untouched (unblocks
Groq and the rest of the OpenAI-compatible list)
Review-confirmed release blocker fixed: kigi replays Kimi's
reasoning_content (and its private model_id) on input assistant
messages — Kimi consumes these, but DeepSeek documents input
reasoning_content as prefix-mode-only (historically a 400) and other
providers don't know either field. The DeepSeek and Passthrough arms now
strip both; Kimi's own pipeline is untouched. Pinned on both message
shapes.
The 5th registry row: id "anthropic", ANTHROPIC_API_KEY > auth.json
"anthropic" scope, api.anthropic.com/v1 with KIGI_ANTHROPIC_BASE_URL
override, Messages dialect. Two new spec dimensions most future rows
reuse: ListingDialect (Anthropic's /v1/models wants x-api-key +
anthropic-version headers, ?limit=1000, and its own response shape) and
PlatformKeyHeader (Bearer vs x-api-key across listing/validation/
inference, with auth_scheme stamped onto entries).
The 2026 Anthropic listing serves real metadata: the adapter maps
max_input_tokens, per-level effort capabilities (low..max as the menu,
xhigh/max distinct), thinking/image flags — and enrichment fills only
genuine wire gaps (e2e pins wire-1M beating enrichment, and a zero
context filled to 200k).
Two review-confirmed defects fixed red-green:
- Output caps were dropped at three layers, so every sub-128K-output
model (64k Haiku, legacy models) would 400 on EVERY request against
the sampler's 128K max_tokens default. Wire max_tokens and enrichment
limit.output now flow to entry.max_completion_tokens.
- An explicit wire effort-decline was indistinguishable from wire
silence, letting enrichment inject effort menus pre-4.6 models reject
(adaptive thinking 400). The adapter now emits a decline sentinel
(support:false) that enrichment respects — proven end to end.
Also: the Messages client now sends anthropic-version (previously never
sent — real api.anthropic.com rejects such requests; pinned across all
three scheme/backend quadrants), key validation builds per-key-header
requests, missing listing data fails fast, empty-id ghosts drop with a
warning, kimi-import recognizes api.anthropic.com as built-in
automatically.
The 4th registry row: id "openai", OPENAI_API_KEY env > auth.json
"openai" scope (login picker/paste/validation all registry-generic —
zero TUI changes needed, pinned by the picker test), base
https://api.openai.com/v1 with KIGI_OPENAI_BASE_URL override, Responses
dialect via the new PlatformWireApi spec field, enrichment-backed
metadata (wire_serves_metadata=false).
OpenAI's GET /v1/models returns bare ids and is polluted with
tts/whisper/embeddings entries: the listing is restricted to
enrichment-known TOOL-CALLING models (review caught that membership
alone admitted models.dev-known embeddings models, which would 400 on
every agentic request; dropped ids are debug-logged for launch-day
diagnosability). Context windows, effort menus, display names, and
thinking capability come from the enrichment pipeline — wiremock e2e
pins the full contract: polluted live listing + models.dev →
one Responses-backed chat model with a 400k documented context window.
Responses max-effort wiring (closes the P0c-1 debt): canonical effort
rides a CreateResponseWrapper sidecar and patch_reasoning_effort writes
it onto the serialized body at both send sites (all seven levels pinned,
xhigh/max distinct, summary preserved); normalize_effort_echo drops
echoes async-openai's typed enum cannot represent at both the non-stream
and SSE parse seams; the dead typed to_responses_api converter is
deleted. Kimi/moonshot stay byte-identical (ChatCompletions untouched,
wire_api maps to the same default; kimi wire tests green).
kimi-import now recognizes ANY registry platform host as built-in
(was hardcoded moonshot), covering openai and future rows.
Provider /models listings that return bare ids (OpenAI-style) get context
windows, thinking levels, image support, and display names from models.dev:
kigi-models owns the transform (parse_api_json — ONE field interpretation
for the bundled snapshot AND runtime refreshes), enrich_wire_model fills
gaps with wire values always winning and model availability strictly
wire-truth. Spec rows gained models_dev_id + wire_serves_metadata; all
three current platforms are wire-served, so this pipeline is provably
inert for them (byte-identical catalogs, zero egress, zero ~/.kigi writes
— adversarially verified).
Shell side: enrichment_fetch with a 24h disk cache guarded by binary
version + keep-set + future-stamp sanity (a registry change or downgrade
refetches instead of serving a catalog missing new providers), refresh of
https://models.dev/api.json filtered to registry ids, KIGI_MODELS_DEV_URL
override with case/whitespace-tolerant kill switch, fallback chain
fresh-cache > refresh > stale-cache > bundled (each step logged). The
fast path returns an empty catalog without forcing the bundled parse.
From the review: blast-radius-confined parsing (one drifted provider on
models.dev warn-skips instead of failing the whole refresh), registry-
coverage and field-coverage tests guarding script/parser drift, a path-
injectable core with 8 state-machine tests (one of which caught a guard
patch that had failed to apply), _meta provenance stamp in the snapshot,
and models.dev (MIT) attribution in NOTICE. Snapshot: 29 providers, 1124
models, 246KB, regenerated by scripts/gen_enrichment_snapshot.py (pure
filter, no transform).
OpenAI (Responses) and Anthropic (Messages) treat xhigh and max as
DISTINCT effort levels in 2026, and the Kimi K3 wire's top tier is max —
the old parse alias (max→Xhigh) conflated them. Canonical Max now exists:
parse/as_str/serde split, Messages mapping sends xhigh and max as their
own tokens (was Xhigh→"max"), and the K3 menu token max carries
canonical Max end to end.
Kimi wire is byte-identical in all four flows (menu pick, restored
legacy xhigh session, --reasoning-effort flag, /effort command) —
adversarially traced and pinned: kimi_compat's string-level xhigh→max
rename covers legacy tokens, max passes through verbatim.
From the review:
- Rollback safety: persisted reasoning_effort (session summaries, chat
history) deserializes leniently — unknown future tokens degrade to
None with a warning instead of hiding sessions or failing resume.
- Restore migration: a pre-split xhigh override onto a model whose menu
offers max but not xhigh (K3) migrates once, healing display/active-row
drift and re-persisting the live vocabulary.
- /effort max now rejects (with the offered list) on models whose menu
lacks a max row instead of silently applying xhigh; deliberate, tested.
- The interim Responses-backend Max→xhigh downgrade (async-openai has no
Max variant through 0.41) warns loudly; real max wiring lands with the
OpenAI provider cycle via post-serialize body patch.
- Two rusted ignored-e2e wire pins asserted the pre-adapt reasoning_effort
key (deleted by the body adapter since ea0ce9d); they now pin the real
thinking.effort=max shape.
Platform API keys now live in auth.json under the platform-id scope (the
per-provider auth.json key contract), resolved env > auth.json > legacy
[platforms.*] config.toml (read-only fallback). The TUI login picker,
paste box, auth-method advertising, and authenticate handler are all
registry-generic: a new PlatformSpec row appears in the login UI and
authenticates with zero UI changes. Spec rows gained vendor/console_host/
login_label display fields (moonshot strings byte-identical, pinned by
tests).
Adversarial review caught that auth.json keys were validated at login but
never stamped onto catalog entries (completions would 401; restart lost
eager auth). Fixed red-green: resolve_model_list/resolve_model_catalog now
take a resolved PlatformApiKeys snapshot consumed by the credential-
stamping layer (auth.json beats stale config.toml, matching the login
validator), with production callers resolving fresh per catalog build.
Also from review: the new auth.json writer takes the manager's cross-
process flock (bounded retry — an unlocked RMW racing a token refresh
could revert a rotated refresh token); the oauth-401 wiremock test is
hermetic (KIGI_SHARE_DIR tempdir; it could read a dev's real auth.json
and hit live moonshot); cli_models resolves real keys; auth.json is read
once per registry sweep; caller-less lock_config_writes deleted; catalog
resolvers tightened to pub(crate); stale config.toml doc comments and the
no-credentials error copy updated.
Replace PlatformId's per-method match arms with static PlatformSpec rows
read through a single spec() accessor. Zero behavior change (adversarially
reviewed: ids, display names, base URLs + env override pairing, oauth flag,
prefix filters, and api-key env precedence order are byte-identical; all
public signatures unchanged). Groundwork for the multi-provider expansion:
adding a platform becomes variant + ALL entry + spec() arm + row.
Guards added for the row-addition workflow: spec-id uniqueness test (parse
scans rows) and a compile-forced ALL-completeness test (a variant missing
from ALL would otherwise be silently unparseable and skipped by model
sync). Future-proofed two assertions that hardcoded 'openai' as an
unknown-platform example.
README leads with the claim under the title — the world's first CLI with
built-in graph engineering — plus a dedicated section covering the /graph
command set, the closed loop it runs (plan → parallel worktree workers →
adversarial verify → 3-way merge-back → replan → optimize → terminal
whole-objective verification), the repo-following .kigi/graph.jsonl state,
copy-paste disable commands for macOS/Linux and Windows, and the tuning
knobs.
install.sh: the rc-file resolution and persist_line helper are hoisted out
of the not-on-PATH branch and generalized (guard + label), so PATH
persistence is unchanged while KIGI_GRAPH=1 is persisted unconditionally
and idempotently for zsh/bash/fish/POSIX profiles — an existing
KIGI_GRAPH line (e.g. a user's =0 opt-out) is left untouched on
reinstall. install.ps1 mirrors it with a User-scope environment variable,
also only when unset. No binary change — installers and README ship from
main, so this needs no rebuild or release.
libc::EWOULDBLOCK is unix-only (kigi-shell links libc behind cfg(unix));
the v0.1.3 windows-msvc release build failed on it. fs2 exposes
lock_contended_error() precisely as the cross-platform classifier
(EWOULDBLOCK on unix, ERROR_LOCK_VIOLATION on Windows) — use it instead
of the raw errno. No behavior change on unix; graph_project lock tests
green.
A restricted optimizer pass now reviews the graph at plan boundaries —
right after initial planning and piggybacked on each replan version
boundary, never mid-execution. An optimizer subagent may emit four ops
over Waiting/Ready nodes only: remove_dep (delete a false dependency,
restoring parallelism — the highest-value edit), reorder (pending
priority for the serial scheduler), merge (fold two tiny nodes; specs
concatenate, deps union, dependents re-point, absorbed self-deps drop),
and split (2-3 focused replacements inheriting the original's deps and
dependents). The optimizer changes graph DATA only; the executor stays
pure deterministic Rust. KIGI_GRAPH_OPTIMIZER=0 disables it entirely.
apply_optimization enforces the contract twice: per-op checks
(pending-only targets, known ids, terminal node untouchable, dead-node
deps rejected as DeadDep, merge/split targets with non-pending
dependents rejected with the true reason instead of tripping the
immutable invariant later), then FINAL invariants — every non-pending
node byte-identical in the result, the gn-final gate rebuilt over all
survivors, node cap, whole-graph acyclicity, and a BIDIRECTIONAL status
re-derivation for pending nodes (adversarial review caught the critical
hole: a merge grafting unsatisfied deps onto a Ready node would
otherwise dispatch it ahead of its new prerequisites, since
recompute_ready is promote-only). Applied passes bump plan_version,
freeze an immutable baseline, and consume a slot of the SHARED replan
cap; an explicit {"ops": []} is a respected free no-op; any failure
degrades to keeping the current plan. Plumbing reuses a new shared
artifact-pass runner (stale-artifact delete, size cap, missing-file
fail-closed) extracted from the replanner.
Tests: remove_dep parallelism restore + loud no-such-dep, immutable and
terminal-node rejections across all four ops, merge/split dependent
rewiring incl. final-gate rebuild and intra-split dep resolution,
result-cycle rejection, dead-dep splits, Ready-demote-on-merge, and
three e2e flows — false-dep removal proven ACTUALLY parallel by the
held-reply fan-out gate, OPTIMIZER=0 spawning zero passes, and the
shared-cap guard. kigi-shell 4959 lib tests green; clippy clean.
/graph show now renders the dependency graph as box-drawing text via a
deterministic Sugiyama-lite pipeline in session/graph_render.rs:
longest-path layering, dagre-style dummy pass-throughs so every drawn
edge spans exactly one layer gap (long edges route as vertical lanes
through intermediate bands), one-pass barycenter ordering, and greedy
bus-lane allocation in the connector gutters with box-drawing-aware
glyph merging (a bus crossing a pass-through renders ┼). Node boxes
carry a status glyph (✓ ▶ ○ · ✗ ⊘) and a clamped title; a legend line
closes the view. Only Blocks edges are drawn — DiscoveredFrom is audit
metadata whose origin is always terminal, so drawing it would double
edges without scheduling meaning.
The output rides ordinary scrollback (the pager already scrolls it), so
no new wire variant or pager view was needed. Honest ceiling: a graph
wider than the 120-column budget — or an empty/pre-planning graph —
degrades to the indented status tree, because box art wrapped by the
terminal is worse than no art; a cycle reaching the renderer (upstream
validation bypassed) refuses to render rather than looping.
Tests: six-node structural snapshot (layering order, fan-in bands,
arrowheads, corners, legend, no trailing whitespace, width bound),
determinism across runs, too-wide fallback, dummy-lane routing through
intermediate bands, DiscoveredFrom exclusion, title clamping, empty
graph; plus a handle_prompt e2e driving /graph show end to end (no-graph
degrade + seeded chain rendering). kigi-shell 4949 lib tests green;
workspace clippy clean.
The graph now follows the REPOSITORY, not the session. Every checkpoint
projects the orchestration to .kigi/graph.jsonl at the git root in a
beads-style, line-mergeable shape: line 1 is the header (orchestration
minus nodes — omitted entirely, and a header carrying inline nodes is
rejected on load rather than silently duplicating the per-line entries),
then one content-hash-id node per line. The session tracker stays the
single source of truth; projection failures warn loudly but never block
progress. kigi only writes the file — committing it stays a user decision.
Single-writer discipline via an fs2 flock on a sidecar .lock: the session
that creates or resumes a graph owns the projection; other instances get a
read-only /graph status view rendered from the file and an explicit
refusal on resume. Cross-session revive: /graph resume in a fresh session
loads the file UNDER the lock (locking after reading raced the owner's
final checkpoint and could resurrect a just-cleared graph), sanitizes it
with the from_snapshot demotions, and re-dispatches.
Holding the lock proves nobody writes NOW — not that the file's content is
yours. Every lock-then-mutate site therefore identity-checks the projected
graph_id: /graph <objective> refuses to overwrite a foreign non-Complete
projection (revive-or-clear guidance, mirroring the session-level guard);
session-restored graphs claim writership on resume (and best-effort at
spawn re-emit, so the shared file learns the demoted truth immediately)
but refuse when the projection belongs to a different graph; /graph clear
skips projection teardown entirely when no session graph exists, leaves
foreign projections in place, and warns instead of swallowing lock errors.
Resume arms validate their flags before taking the lock.
Tests: projection round-trip pinning the one-line-per-node shape,
inline-nodes rejection, malformed-content loud errors, exclusive-lock
semantics across handles, and an e2e driving create → checkpoint
projection → second-instance read-only refusal → owner death → fresh-
session revive (demoted node relaunches) → clear removing the projection.
kigi-shell 4941 lib tests green; workspace clippy clean.
Workers, verifiers, and serial node goals can now surface out-of-scope work
as line-anchored 'DISCOVERED: <text>' markers (fence-stripped and
placeholder-filtered — the templates' own examples are fenced so verbatim
echoes never parse; worker summaries embedded in verifier prompts get the
marker neutralized alongside NODE_RESULT/NODE_VERDICT). Discoveries queue on
the orchestration as persisted state and fold into the graph at dispatch
boundaries: a replanner subagent produces a strictly APPEND-ONLY appendix,
validated against the live graph (existing-id deps allowed; edges onto
gn-final rejected — they would cycle the moment the final-gating extension
lands; Blocks deps on Failed/Blocked nodes rejected as DeadDep so the
attempt-2 feedback loop repairs the artifact). Installing an appendix bumps
plan_version, freezes an immutable graph.baseline.v{N}.json next to the
prior versions, extends gn-final's gate (demoting a Ready final back to
Waiting), and recomputes readiness.
DiscoveredFrom edges are audit metadata, never scheduling gates: an origin
is always terminal at replan time, so gating on it is either a no-op or a
permanent wedge — and a failed node's discoveries are still real work.
Replanning is bounded by KIGI_GRAPH_REPLAN_CAP (default 3; 0 disables it
quietly): past the cap, after the final node has achieved, or on replan
failure, discoveries drain to history only — a working graph is never
paused for a failed enhancement pass, and it always converges. The budget
gate now precedes the replan boundary (a budget-dead graph keeps its
discoveries queued for a later --budget top-up instead of spending two
replanner runs first), and both planner runners delete stale artifacts
before spawning so a child that responds without writing can never get a
previous pass's file validated as its own output.
Tests: validate_replan unit coverage (existing-id resolution,
DiscoveredFrom dedup, collisions, dead deps vs dead origins, terminal-node
edges, combined-graph cycles), tracker appendix/regate/audit-edge tests,
and two e2e flows — discovery → replan → appended node runs to Achieved
with both baselines frozen, and cap-0 draining to history while the graph
still converges. kigi-shell 4935 lib tests green; workspace clippy clean.
BudgetLimited is now a resumable state: a budget trip demotes in-flight
nodes to Ready (a resource stop, not a verdict — no forever-Running node is
ever persisted) and '/graph resume --budget <tokens>' re-arms the graph with
fresh headroom (new budget = spent-so-far + extra). The tripped node's
partial burn is charged into tokens_spent_nodes at BOTH cascade sites before
the demotion clears current_node, so the top-up arithmetic never runs on an
under-counted ledger. Any input starting with 'resume' resolves to a resume
(case-insensitive; malformed top-ups surface the usage hint) and setup_graph
refuses to replace any non-Complete graph — a typo can no longer silently
destroy a resumable graph. An explicit --budget on a merely-paused graph is
rejected loudly instead of silently discarded; all trip-time messages now
advertise the top-up.
The pager gains a graph status chip: a new GraphUpdated wire variant
(extensions/notification.rs, old pagers degrade via #[serde(other)]) is
emitted from the single persist_graph_state chokepoint — every transition is
both a checkpoint and a badge tick — with a 'cleared' sentinel on /graph
clear and a one-shot re-emit after session restore (the replayed updates log
otherwise shows the pre-shutdown Active state that from_snapshot just
demoted in memory). TUI side: GraphDisplayState, session-notification arm,
and a goal-idiom chip with node progress, clamped current-node title, and
budget-aware spend. Pre-session command availability now advertises /graph
from the flags (it was fail-closed to the in-session path only, so the
welcome-screen slash menu never showed it).
Coverage: GraphUpdated wire round-trip + minimal-payload + unknown-tag
tests; PTY scenarios graph_slash_presession{,_disabled}.yaml (both run
green against the real pager binary); handle_prompt-level e2e for terminal
slash outcomes (/graph status|resume|pause, /goal refusals while the graph
owns the engine); budget top-up e2e driving a BudgetLimited diamond back to
Complete. Not shimmed: pre-G2 persisted snapshots with budget-Failed nodes
(the KIGI_GRAPH flag has never shipped enabled, so none exist).
kigi-shell 4927 and kigi-tui 6610 lib tests green; workspace clippy clean.
With KIGI_GRAPH_CONCURRENCY > 1 (default 3, clamp [1,8]) and >=2 Ready nodes,
drive_graph — the single dispatch loop shared by setup/advance/resume — runs
parallel batches: each node executes as a bounded worker<->verifier subagent
loop (KIGI_GRAPH_NODE_ROUNDS, default 3; general-purpose children with the
full implementer toolset; worktree isolation on round 1, resume keeps context
and worktree on later rounds; NODE_RESULT/NODE_VERDICT terminal contracts
parsed fail-closed with fence-stripping and line anchoring). Achieved nodes
merge back SEQUENTIALLY via kigi_workspace apply_worktree in Merge mode; a
conflict fails the node, block_dependents fires, and surviving chains keep
going. gn-final always runs serially on the full goal engine. Concurrency=1
is byte-identical to the serial G0 path; non-git projects degrade to serial.
Merge primitive hardened for real use (kigi-workspace): the 3-way apply is
now byte-safe (binary files no longer read as UTF-8 and silently deleted)
and gains the identical-content rule (ours==theirs => already present, not a
conflict) — without it, any dirty file inherited via PreserveWorkingTree
false-conflicted every node merge and multi-wave graphs self-poisoned.
Adversarial review pass (16 confirmed findings, all fixed): in-session
resume demotes orphaned Running nodes instead of wedge-pausing forever after
a mid-batch Esc; cancel re-sweeps subagents AFTER the turn abort so workers
spawned in the cancel window die too; empty child ids are never adopted as
resume targets (no unisolated escape to the shared tree); a successful
isolated round returning no worktree fails the node (soft-fallback can no
longer put N writers in one tree); main-HEAD movement during a batch aborts
merges instead of reverse-applying external commits; failed nodes still
charge the token budget; budget trips terminally fail the in-flight node;
runaway (>600s) rounds are cancelled by spawn id and retried via resume;
worker summaries are marker-sanitized before verifier embedding. Merged
worktrees are removed immediately (storage discipline); failed nodes keep
theirs for postmortem.
Tests: 4922 kigi-shell lib tests green (58 graph-specific), including
fan-out proven by a held-reply gate, real-git batch merge with cleanup
assertions, budget charging across verdicts, cap trimming, resume-after-
cancelled-batch, and backgrounded-round cancel semantics.
A deterministic DAG scheduler layered on the existing goal engine: /graph
<objective> decomposes the objective via a graph-planner subagent, gates the
result through Agentproof-style static validation (cycles, unknown deps,
duplicate slugs, caps), appends a structural final-verification node, then
executes each node as one ordinary goal — planner, worker loop, adversarial
verifier, budget and pause machinery all reused verbatim. The in-turn loop
advances nodes within the same turn (multi-loop closed loop); goal-side
auto-pauses cascade to the graph at a single chokepoint; node goals are armed
with the remaining graph budget so mid-node overruns trip graph-wide.
Gated by KIGI_GRAPH=1 (default off) + the goal harness. State persists to
<session_dir>/graph/state.json with a clear-tombstone; per-version immutable
baselines and per-node artifact archives live under graph/<graph_id>/.
Restore demotes Active->UserPaused and Running->Ready (verifier-gated re-run).
Review hardening (adversarial multi-agent pass, 24 confirmed findings fixed):
the goal-inactive loop break now consults the graph seam (mid-turn
classifier-disabled completions can no longer strand an Active graph), the
pause cascade fires even when no node goal is in flight (cancel during
planning), node bookkeeping precedes the long setup await, /graph clear only
resets the engine it owns, budget trips terminally fail the in-flight node,
and the pause transitions in /goal pause + /graph pause no longer hide inside
debug_assert! (a release-build no-op inherited from upstream).
Tests: 4908 kigi-shell lib tests green, including a serial 4-node closed-loop
e2e, restore/resume, cascade, mutual-exclusion, planning-retry, persistence
round-trip + tombstone, and status rendering.
Kigi never publishes CDN changelogs, so the entire inherited feature
was dead weight: the welcome-menu Changelog row, the hero-box info
slot (bullets + clickable CTA), /release-notes with its /changelog
alias, and the ChangelogManager CDN-fetch/disk-cache pipeline
(Effect::FetchChangelog, TaskResult::ChangelogFetched, startup and
post-login fetch kickoffs, AppView cache fields, mouse hover/click
handling). Welcome menu is now [Import] / New worktree / Resume
session / Quit; the hero box keeps title + version + subtitle and its
layout math simplifies to 3 + menu rows (verified equivalent by the
surviving boundary tests). Action::ShowReleaseNotes and the DocViewer
modal stay — /docs uses them. builtin.rs keeps deleting stale
CHANGELOG.{json,md} caches written by kigi ≤ 0.1.0.
kigi-shell-base drops its reqwest 'blocking' feature (only the deleted
module used it). -1206 lines net.
Gates: fmt clean, workspace check/clippy --all-targets 0/0, kigi-tui
lib 6609 / shell-base 56 / shell util:: 258 all passing, welcome pty
e2e (3 tests incl. braille logo) passing.
install.sh: append the export line to the login shell's rc file
(zsh: $ZDOTDIR/.zshrc; bash: ~/.bash_profile on macOS, ~/.bashrc on
Linux; fish: fish_add_path in config.fish; otherwise ~/.profile).
Idempotent — a second run detects the existing entry and skips. On an
unwritable rc it fails loudly with the manual command.
install.ps1: prepend the bin dir to the per-user PATH via
[Environment]::SetEnvironmentVariable(..., 'User') — registry-backed,
picked up by every new terminal. Strict-mode-safe for a null user Path.
Verified end-to-end on macOS with a fake HOME: install → rc appended →
fresh zsh sources it → kigi resolves and runs. All shell branches,
ZDOTDIR/XDG overrides, idempotency, and the write-failure path covered
by a harness driving the real script tail; install.ps1 parse-checked
and its path-construction logic exercised under pwsh strict mode.
The active-turn spinner next to "Waiting for response…" now cycles
the moon phases 🌑🌒🌓🌔🌕🌖🌗🌘 — the official kimi-cli's lunation
animation (rich's `moon` spinner) — matching the welcome-screen moon
logo. New glyphs::moon_spinner_frames() with the same legacy-ConHost
ASCII fallback as every other spinner set; emoji frames are 2 columns
and the status row already measures the rendered frame, so layout
adapts. The ambient "Starting session…" row and other spinners stay
braille.
Verified: kigi-tui lib 6621 + pager-render 963 tests green; real-binary
PTY e2e waiting_for_model_label_shows_before_first_token passes with
the new spinner rendering in a live vt.
Feedback about an unofficial community build belongs on its own issue
tracker, not Moonshot's feedback endpoint — and this mirrors the
official kimi-cli, whose /feedback opens its repo's issues page
(ISSUE_URL in ui/shell/slash.py). /feedback now returns
Action::OpenUrl(https://github.com/ZacharyZhang-NY/Kigi-CLI/issues),
the same battle-tested browser path /docs web uses.
The now-dead TUI text-feedback pipeline is excised: PromptInputMode::
Feedback (~ prefix composer mode), Action::{EnterFeedbackMode,
SendFeedback}, Effect::SendFeedback (the kigi/feedback ACP POST),
TaskResult::{FeedbackComplete,FeedbackFailed}, and their dispatchers.
The shell-side kigi/feedback ACP extension stays: it is protocol
surface for editor embeddings, OAuth-gated, and shared with kigi/btw.
Gates: workspace check/clippy --all-targets 0/0, fmt, kigi-tui lib
6621 passed / 0 failed.
Under the script's own Set-StrictMode -Version Latest, .Count on the
result of Where-Object throws when the filter matches nothing — which
is precisely the fresh-install case (bin dir not on PATH yet), so every
first-time Windows install ended with an error after an otherwise
successful install. Use -contains on the split arrays instead; no
member access on a possibly-null pipeline result.
Repro + fix verified under pwsh 7.5.2 with StrictMode Latest: old
expression reproduces the user's exact error, new one returns
False/True correctly for missing/present PATH entries.
The post-install guidance previously showed a one-time `export PATH=…`
that dies with the terminal. Detect $SHELL and print the persistent
command instead: append to ~/.zshrc / ~/.bash_profile (macOS) /
~/.bashrc (Linux), fish_add_path for fish (already persistent via
universal variables), ~/.profile as the POSIX fallback.
The third Windows release failure was a genuine cross-version type
mismatch: process-wrap 9.1.0 wraps windows 0.62's
PROCESS_CREATION_FLAGS in its CreationFlags wrapper, while the
workspace pinned windows 0.61 — the CREATE_NEW_PROCESS_GROUP |
CREATE_NO_WINDOW constants we pass came from a different nominal type.
Aligning the workspace on 0.62 leaves a single windows crate in the
graph.
Windows code cannot be compiled on this host (a full cross-typecheck
dies in ring's C build), so the change is verified by auditing every
windows-crate item our cfg(windows) code imports (25 items across 9
files) plus all seven Win32_* feature names against the actual
windows-0.62.2 registry sources — all present — and by confirming
process-wrap's CreationFlags field is the same 0.62 type our call site
now names.
emit_rerun_if_changed piped protoc output through /dev/stdout and
/dev/null, which do not exist on Windows — the release build's Windows
job failed there even with protoc on PATH. Both protoc outputs now go
to real files under the build script's OUT_DIR (deterministic names, so
reruns overwrite), the dependency list is read from disk, and the
make-style target prefix is matched with normalized path separators
since protoc may spell the target with forward slashes on Windows.
Verified locally by forcing a fresh build-script run: the .d file lands
in OUT_DIR and kigi-tools-api builds cleanly.
The Windows release build failed with os error 193: Windows cannot exec
the dotslash bin/protoc launcher directly (no shebang support), and the
runner had no protoc on PATH for the build script's documented fallback.
Install the exact version the dotslash file pins (v29.3) from the
official protobuf release and put it on PATH for the Windows job only.
The four Unix targets built successfully and are unaffected.
The unauthenticated welcome screen previously offered only 'Login with
Kimi Code'. It now lists every interactive platform the shell
advertises, matching the official kimi-cli picker:
Kimi Code (OAuth)
Moonshot Open Platform (API key · moonshot.cn)
Moonshot Open Platform (API key · moonshot.ai)
Quit
- Shell: new ACP auth methods moonshot-cn / moonshot-ai (advertised
after kimi-code; the BYOK first-position invariant holds).
authenticate(moonshot-*) reloads keys from env>config, fails with an
actionable message when none is configured, validates the key against
GET {base}/models (401 → 'invalid API key' naming the console), then
swaps the fresh config in, triggers the model sync, and reports
auth_mode api_key so the pager treats it like other API-key logins.
Never session-based; keys never logged.
- Config: save_platform_api_key persists [platforms.<id>].api_key via an
atomic mode-preserving write under the config lock; refuses OAuth
platforms, blank keys, and unparseable files.
- TUI: the Pending welcome renders the picker rows from the advertised
methods (arrows/Enter/mouse; 'l' keeps selecting the OAuth row).
Choosing a Moonshot row opens a masked paste box ('Paste your Moonshot
API key (from platform.moonshot.cn)'); Esc returns to the picker,
Enter persists the key and authenticates in one sequential effect;
failures return to the picker with the error line, success lands on
the normal welcome. Startup eager-auth is unchanged: a key already in
the environment authenticates exactly as before, and single-method
shells keep the historical auto-device-flow.
Gates: workspace check/clippy 0/0; shell 4870 + tui 6620 lib tests
green; headless probe advertises [kimi-code, moonshot-cn, moonshot-ai]
for a fresh user and xai.api_key-first with a key configured.
Raw links to repo-committed mp4s render as plain links in READMEs —
GitHub's inline player only works for web-UI-uploaded assets. The demo
is now an optimized 3.3MB GIF (8fps, 880px, 128-color palette) embedded
as an image, with the full-quality mp4 linked below it.
The previous commit referenced the video but a cwd reset kept it out of
the staging area; the raw URL 404'd. 2.3MB, rendered inline by GitHub at
the top of the README.
- Embed the demo recording (video/Kigi.mp4, 2.3MB) at the top.
- Installation section with the curl | bash one-liner for macOS/Linux and
the irm | iex one-liner for Windows, both pointing at this repo's
install scripts, plus the SHA256SUMS/self-update notes.
- Drop the stale 'M0 complete, M1 in progress' status; fold coexistence,
the read-only ~/.kimi import, and zero-telemetry into their own
section; note the OAuth-only search/fetch tools and K3 thinking levels
in the providers section.
The PRD's first acceptance gate now holds: grep -RinE '\bx\.ai\b|grok'
crates/ --include='*.rs' → 0 matches (exempt: NOTICE and third-party
license archives, README provenance, and the required 'Based on Grok
Build Open Source' attribution, now sourced from version_attribution.txt).
Wire-visible renames (both sides in this repo, changed in lockstep):
- Auth method id 'grok.com' → 'kimi-code' (AuthMethodKind::KimiCode).
- Every x.ai/* and _x.ai/* ACP ext method and meta key → kigi/* /
_kigi/* (~200 names; grokShell → kigiShell). Session-file replay keeps
a read-side alias for the legacy '_x.ai/session/update' method so
existing updates.jsonl histories load; writes emit only the new name
(both directions test-pinned).
- Agent types grok-build* → kigi* with a documented legacy-prefix alias
at resolution time so persisted sessions keep resolving.
- ToolNamespace/BuiltinAgentName GrokBuild* → Kigi* (wire snake_case
kigi/kigi_concise/kigi_hashline; schema regenerated); grok_build
implementation dirs renamed to kigi*.
- x-grok-* headers → x-kigi-*, __GROK_* sentinels → __KIGI_*, themes
grokday/groknight → kigiday/kiginight (old persisted values fall back
to the default theme), web_fetch allowlist xAI hosts → kimi.com +
moonshot platforms, changelog CDN → this repo, grok-build changelog
archives deleted.
- BYOK default endpoint removed: [endpoints] api_base_url is now truly
optional with NO default — consumers fail fast with the flag name when
unset (no silent x.ai egress). Mock harnesses inject it explicitly.
- System-prompt identity fixed: 'released by xAI' → 'an unofficial
community CLI for Kimi' (template + regenerated encrypted form).
Also repaired pre-existing grok-era test debt found by the sweep: the
stale trace_classify default-model pin, the grok-pager UA label test,
pty-harness stale-binary reuse and non-hermetic moonshot routing (a PTY
test could previously reach the real api.moonshot.cn), and the outdated
oauth fixture scope key.
Gates: §9 grep 0; fmt clean; workspace check/clippy 0/0 (-D warnings);
FULL cargo test --workspace: 234 suites, 21,961 passed, 0 failed;
deny advisories ok.
- .github/workflows/release.yml: on tag v* build all 5 targets (macOS
arm64/x86_64, Linux arm64/x86_64 incl. free arm runners, Windows
x86_64) with the release-dist profile, archive kigi-<version>-<triple>
with LICENSE/NOTICE/THIRD-PARTY-NOTICES, generate SHA256SUMS, publish
the release (prerelease for tags containing '-'), with a tag↔workspace
version guard.
- install.sh / install.ps1 (repo root): platform detection, latest or
--version download from GitHub Releases, SHA-256 verification against
SHA256SUMS, install into the kigi home's downloads/ + bin/kigi symlink
(the same layout the self-updater manages), smoke test, PATH guidance.
- kigi-update rewritten onto the GitHub Releases API (documented wire
shape; stable=/latest, alpha=semver-max across the list, pinned=/tags):
SHA-256 gate before any binary swap, tar.gz/zip extraction per
platform, atomic bin/kigi symlink swap, channel/rollback semantics and
the KIGI_AUTO_UPDATE gate preserved verbatim; every x.ai/GCS/npm
endpoint deleted, npm/gh-release installers removed, legacy grok/agent
links retired on install. kigi-env owns the update base URL with a
KIGI_UPDATE_BASE_URL override (this is what the test artifact server
injects).
- .cargo/config.toml: removed the non-portable neoverse-v2 CPU pin on
Linux arm64 (fleet-specific); RELRO/NX hardening link-args now apply
to the gnu targets too, matching the release-dist profile's contract.
- THIRD-PARTY-NOTICES regenerated via cargo-about (about.toml +
template); the M0 hand-built file is dropped and README points at the
generated one. docs/RELEASE.md carries the release checklist.
- Deleted xAI-era leftovers: kigi-tui/scripts/install*.{sh,ps1} (x.ai
CDN) and the @xai-official/grok npm skeleton (PRD F8: no npm).
Gates: fmt clean; workspace check/clippy 0/0 (--locked, -D warnings);
kigi-update 58 lib + 86 integration tests green; deny ok;
release-dist build of kigi-bin succeeds and reports 'kigi 0.1.0'.
Removed root-and-branch for the zero-egress guarantee (the hub was xAI's
remote-workspace/cloud-sandbox service):
- Crates deleted: kigi-computer-hub-core, kigi-computer-hub-sdk,
kigi-computer-hub-mcp-adapter, kigi-workspace-client (hub-proxied
workspace RPC client), and kigi-tracing (its sole network path was the
OTLP gRPC exporter; zero consumers remained). kigi-tracing-macros
(purely local) stays.
- kigi-workspace: every hub surface deleted — hub server/channel/auth,
HITL-over-hub permissions, donation/metrics pumps, file upload RPCs,
hub tool-snapshot merge (resolve pipeline is MCP-only now),
WorkspaceOps::Proxy. Local worktrees, sessions, leader IPC, MCP, and
the ACP permission prompt path are untouched; LocalRegistry re-homed
into kigi-tool-runtime on the existing ToolDyn types so in-process
tool dispatch is unchanged.
- kigi-shell: leader workspace-exposure control surface (incl. the
wss://computer-hub... URL), [hub] config, ObservabilityBridge, hub
WebSocket proxy, dead OTLP config knobs. ClientMode::Headless (never
constructed) removed.
- kigi-tui/bin: hidden `kigi workspace` command removed (`kigi
worktree` stays).
- Renames: --xai-api-base-url → --api-base-url / KIGI_API_BASE_URL /
[endpoints] api_base_url (serde alias keeps old configs working; the
flag feeds BYOK/custom-endpoint routing, not main inference);
grok_version → kigi_version in inspect/models-cache/trace metadata
(old caches self-heal via version-mismatch refetch).
- Dependency tree: dropped fastrace*, opentelemetry-otlp/http/proto,
tokio-tungstenite from the workspace; fixed the 4 real useless_format
violations the fastrace lint allowance was masking and removed the
allowance.
- marketplaceAllowlist kept: it gates the LOCAL plugin-marketplace
feature, not an xAI service.
Known §9 leftover (deliberate, for the M3 sweep): the BYOK default base
URL string. Gates: workspace check/clippy 0/0, fmt, deny ok; suites
green (workspace 1042, shell 4918, tui 6634, tools 2608, tool-runtime
47, mcp 154).
The welcome and prompt model labels (and /effort's 'current' hint)
rendered the canonical internal level name, so K3 at its default effort
showed 'K3 (xhigh)' even though the server's vocabulary for that level is
'max' (live /models think_efforts: low/high/max).
New ModelState::reasoning_effort_display() resolves the current effort
through the model's own effort menu (option id whose value matches),
falling back to the canonical name only when the model has no entry for
the level. All three display sites route through it; test pins the K3
mapping (Xhigh → 'max', Low → 'low', no-menu-entry → canonical).
- docs/SMOKE.md: the F9 capability checklist — every carried-over harness
capability mapped to at least one automated case (test target) or a
manual probe with its observable, per the PRD acceptance rule.
- scripts/bench.sh: enforces both PRD performance budgets and fails CI on
a miss — `kigi --version` p95 <= 50ms via hyperfine, and TUI first
frame <= 300ms measured through the pty harness (real vt100 emulator
answering terminal queries) via the new scripts/first-frame.scenario.json.
Local run: p95 10.1ms, first frame 134ms.
- pty harness: StepOutcome gains elapsed_ms (stamped per step by the
scripted runner) — a leading wait_for_text step's elapsed IS the
spawn-to-first-paint latency the budget reads.
- CI: new `perf` job (macOS + Linux) building the release binaries and
running scripts/bench.sh.
New `kigi import-kimi` (with --dry-run) plus a one-time welcome-screen
hint when ~/.kimi/config.toml is present and unimported.
- Sources: ~/.kimi/config.toml (default_model, [models.*], [providers.*])
and ~/.kimi/mcp.json ({"mcpServers": ...} — parsed by the existing
McpConfig machinery). Shapes ported from kimi-cli 1.49.0 config.py with
citations.
- Strictly read-only over ~/.kimi: plain reads only; the test suite pins
byte-identical contents AND unchanged mtimes across scan+apply. Keyring
credentials are never imported; KIMI_SHARE_DIR / any KIMI_* env var is
never consulted (official dir hardcoded to ~/.kimi).
- Mapping: MCP servers merge into kigi [mcp_servers.*] without clobbering
existing names; models on non-built-in providers become kigi [model.*]
custom entries (base_url + api_key + context_window); providers that
duplicate kigi's built-in Kimi/Moonshot platforms are skipped with a
note; default_model maps to the imported alias or the managed catalog
key and never overwrites an existing default.
- One-time marker ~/.kigi/kimi_import_done (claude-import convention);
the startup hint and re-runs no-op once set. api_key values flow only
into the user's own config.toml and are redacted in every summary.
Verified end-to-end with the real binary in a sandboxed home: dry-run,
apply, `kigi mcp list` shows both imported servers, second run no-ops,
~/.kimi mtimes unchanged. 8 unit tests + CLI parse test.
- `kigi acp`: top-level alias for the stdio ACP server (kimi-cli `acp`).
AgentArgs.mode is now optional — bare `kigi agent` and `kigi acp` both
default to stdio at dispatch; `kigi acp <mode>` is rejected.
- `kigi mcp auth <name>`: authorize an OAuth-enabled remote MCP server
(kimi-cli `mcp auth`). Reuses the doctor's interactive connection path:
starts the named server with OauthInteractivity::Interactive (browser
flow when required), completes the handshake, and reports the tool
count. Stdio servers and unknown names fail with actionable errors.
- `--mcp-config-file <PATH>` (repeatable, global): extra MCP config files
in the .mcp.json shape ({"mcpServers": {...}}), kimi-cli semantics.
Files are validated at parse time (fail fast on unreadable/invalid
JSON), carried across the TUI -> shell boundary via
KIGI_MCP_CONFIG_FILES, and merged at HIGHEST priority — an explicitly
passed file overrides every config scope. Covered by loader unit tests
and verified live: an injected server surfaces in the session's
x.ai/mcp/servers_updated notification via both the flag and the env.
The stale bare-agent-requires-mode CLI test is re-contracted to the new
default-to-stdio behavior.
web_search now speaks the Kimi search service (kimi-cli tools/web/search.py,
wire-verified against api.kimi.com):
- POST {coding_base}/search with {text_query, limit 1-20 (default 5),
enable_page_crawling, timeout_seconds: 30}, OAuth bearer +
X-Msh-Tool-Call-Id; results render in kimi-cli's Title/Date/URL/Summary
schema with result URLs as citations.
- The old implementation called the xAI Responses API with a search model;
that client is fully replaced and the entire model-based config surface
is excised root-and-branch: web_search_model config keys/env/CLI plumbing,
resolve_web_search_sampling_config, toolset web_search SamplerConfig,
RemoteSettings.web_search_model, default_web_search_model.
- Enablement is now purely structural: the service exists only on the Kimi
Code subscription channel, so OAuth sessions get Enabled and API-key-only
sessions get Disabled (tool absent) — per PRD F5.
web_fetch gains the Kimi fetch service as its primary path (kimi-cli
tools/web/fetch.py): POST {coding_base}/fetch with {url}, Accept:
text/markdown, OAuth bearer + X-Msh-Tool-Call-Id; the 200 body is the
extracted markdown (still overflow-budgeted). Any service failure falls
back to the existing local pipeline (SSRF guards, cache, extraction).
The tool gate defaults ON now (kimi-cli always offers FetchURL) and the
egress User-Agent no longer claims grok-agent/x.ai.
Verified end-to-end against the scripted mock service: a headless session
drove web_search (limit/crawling/call-id observed on the wire) then
web_fetch (Accept + call-id observed) to completion.
The headless not-signed-in message still pointed at `grok login` and
XAI_API_KEY; error copy across the shell (re-auth prompts, managed-config
rejection, mcp doctor, trace classifier) and kigi-bin did too. All now
reference `kigi login` / `kigi logout`, and the headless fallback points
at the Moonshot open-platform key (KIGI_MOONSHOT_API_KEY).
Root cause of today's repeated logouts: the keyring entry (service kigi /
oauth/kimi-code) is global per OS user, but the enable gate keyed off ENV
VARS while an AuthManager's identity is its constructor path. Integration
test binaries (compiled without cfg(test), no KIGI_SHARE_DIR in env)
constructed managers on tempdirs whose remove_scope() then deleted the
developer's REAL keychain credential — the unified log shows 13 such
wipes in one day, one per test run.
Structural fix: keyring participation is now a property of the manager's
own path. AuthManager captures keyring_path_scoped at construction
(path == default ~/.kigi/auth.json) and every keyring touch — the
constructor read, update()'s write, remove_scope()'s delete — requires it,
with the dynamic keyring_enabled() gate (env kill-switch, cfg(test) mock
toggle) layered on top. A tempdir-rooted manager can no longer read,
write, or delete the global entry no matter what process type it runs in.
Regression test tempdir_manager_never_touches_global_keyring pins the
incident: a foreign manager's logout must leave the (mock) keyring entry
intact. Keyring behavior tests keep constructor-read coverage via a
thread-local path-scope test seam.
The live /models wire (verified against api.kimi.com) marks every Kimi
Code model supports_thinking_type: "only" and gives K3 a think_efforts
block {support, valid_efforts: [low, high, max], default_effort: max} —
both of which the F4 sync discarded, hardcoding
supports_reasoning_effort: false. Every effort selection was therefore
rejected with 'current model does not support reasoning effort'.
- kigi-models: WireModel gains supports_thinking_type + WireThinkEfforts;
"only" forces the always_thinking capability.
- models_fetch: think_efforts maps into the catalog entry — wire tokens
stay the option ids/labels (max/Max), canonical values map via the
ReasoningEffort parser (max → Xhigh), default_effort marks the default.
This lights up the existing /model <model> [effort] two-phase completion
and the /effort menu with the server's own vocabulary.
- kimi_compat: the effort level rides the wire as thinking.effort
({"type": "enabled", "effort": "low"} is accepted live; invalid
levels are a 400). Only the canonical-vs-wire spelling divergence
(xhigh → max) is renamed; levels pass through verbatim so a contract
violation surfaces instead of being clamped away.
Live acceptance: kigi -m kimi-code/k3 --reasoning-effort max -p ... round
trips against api.kimi.com, and the refreshed models_cache.json carries
low/high/max with the max default.
The welcome logo (hero box, stacked layout, and minimal's welcome card) is
now a braille moon that waxes and wanes through a full 8s lunation,
echoing the Kimi CLI's moon-phase spinner. The disc is rasterized into the
2x4 braille dot grid at render time — round at any size, no art assets —
with the terminator at x = cos(2*pi*p)*sqrt(1-y^2) and a faint outline
ring so the silhouette survives new moon. The full moon grows to 20x10
cells (from the 14x7 slashed-circle art) and the small tier to 10x5; the
old logo*.txt assets are deleted. Geometry is unit-tested (wax/wane
monotonicity, quarter-phase symmetry, right-limb-first waxing, silhouette
at new moon, round raster).
The welcome hero subtitle still read "Thanks for trying Grok Build";
theme display names, the NO_COLOR/COLORTERM diagnostics notes, the
permission notification title, the self-update restart hints, and the
clipboard-wrap protocol marker all still said Grok. Canonical theme keys
(groknight/grokday) are unchanged — only display strings move.
macOS ld cannot encode >16MB of __eh_frame in its compact unwind table and
says so on every debug link of the large binary. All profiles build with
panic=abort, so Rust never unwinds and the note's exception-handling caveat
does not apply. Allow rust.linker_messages at the workspace level with that
rationale, and give kigi-bin the missing [lints] workspace = true (it was
not inheriting workspace lints at all). Also drop the stale xAI authors
field and Grok wording from the kigi-bin manifest.
Sampler / inference (PRD F3):
- kimi_compat.rs: single adaptation point for the Kimi chat/completions
dialect (thinking-field mapping, model_id stripping, empty-content
tool-call message fix, stream_options.include_usage), with kimi-cli
source citations
- Rate-limit handling reworked for Kimi/Moonshot semantics; UA kigi/{version}
- /models replaces the xAI models-v2 endpoint everywhere; idle model
refresh carries X-Msh-* device headers only (X-XAI-Token-Auth and
x-grok-client-mode/CLIENT_MODE_HEADER machinery deleted)
Cloud-surface excision (PRD §5, zero-egress):
- remote/ conversations lane, cli-chat-proxy-types crate, prod/ dir,
share command, credit bar: deleted (single local session lane;
paginate() replaces merge_and_paginate)
- Subscription/tier gate stack deleted end-to-end: AppView
gate/tier/team/ZDR fields, app/subscription.rs watch loop,
dispatch/billing.rs paywall + SuperGrok upsell, free-usage-exhausted
chain, tier-restricted commands, GateInfo, RemoteSettings gate fields,
SettingsUpdateNotification gate fields
- /privacy + coding-data-sharing setting deleted (backed by a dead xAI
RPC; Kigi is zero-egress — nothing to share or retain remotely)
Auth UX correctness (user-reported):
- Device-flow fixtures now mirror the live Kimi payload shape
(https://www.kimi.com/code/authorize_device?user_code=..., verified
against auth.kimi.com); the fabricated auth.kimi.com/device?code=...
URLs are gone
- open_browser_detached is a no-op under cfg(test): unit tests drove
wiremock fixture URLs into the real browser (root cause of the
"garbage mock link" ABCD-1234 tabs)
- Welcome/pager-minimal rebrand: Grok Build -> Kigi, grok.com ->
kimi.com, "Sign in to Grok" -> "Sign in to Kimi"
F2 — fixed three-platform registry in kigi-models: kimi-code
(subscription, OAuth bearer, base kigi_env::coding_api_base_url()),
moonshot-cn (https://api.moonshot.cn/v1), moonshot-ai
(https://api.moonshot.ai/v1) with kimi-k model-prefix filtering.
Moonshot API keys via KIGI_MOONSHOT_CN_API_KEY / KIGI_MOONSHOT_AI_API_KEY
(+ KIGI_MOONSHOT_API_KEY shared fallback) or ~/.kigi/config.toml;
values redacted from logs/display.
F4 — model catalog now syncs from GET {base}/models (Bearer auth,
wire shape per official kimi-cli: id/context_length/supports_reasoning/
supports_image_in/supports_video_in/display_name) with the official
capability-derivation rules (thinking / always_thinking-in-name /
kimi-k2 implicit set). Managed keys {platform_id}/{model_id}; default
model = first list entry; default thinking iff capabilities contain
thinking/always_thinking. Sync failure → last cache; no cache →
built-in fallback table seeded from ids sourced in official kimi-cli
(kimi-for-coding, kimi-k2-turbo-preview, kimi-k2-thinking-turbo).
401 during sync forces one token refresh and retries.
Model resolution priority preserved: CLI > env > config > server >
fallback. Grok model artifacts (grok-4*/grok-build catalog, tier
gating remnants) removed from non-test code.
All first-party endpoints re-verified live: device_authorization mints
real codes; /models on all three platforms answers with real API auth
errors when unauthenticated.
Gates: check/clippy --all-targets 0/0, fmt clean, deny ok,
kigi-shell lib 5136 green, kigi-tui lib 6819 green, kigi-models 8.