Skip to content

Instantly share code, notes, and snippets.

@Maciejdziuba
Last active August 8, 2026 04:55
Show Gist options
  • Select an option

  • Save Maciejdziuba/038c0c822d2c799cffcfdec805975e66 to your computer and use it in GitHub Desktop.

Select an option

Save Maciejdziuba/038c0c822d2c799cffcfdec805975e66 to your computer and use it in GitHub Desktop.
Kimi Everywhere — run any coding harness (Claude Code, Codex, OpenCode, Cline, Aider) on one flat-fee Kimi subscription. One key, two protocols, zero token bills.

Kimi Everywhere

Run any coding harness on one flat-fee Kimi subscription.

Your Kimi Code subscription ($19+/mo) comes with an API key that speaks both major agent protocols:

Protocol Endpoint Works with
Anthropic-compatible https://api.kimi.com/coding/ Claude Code + anything expecting ANTHROPIC_BASE_URL
OpenAI-compatible https://api.kimi.com/coding/v1 Codex CLI, OpenCode, Cline, Roo Code, Aider + anything expecting an OpenAI base URL

Same key, flat monthly fee, no pay-per-token API bills. This kit wires that key into whatever tool you already use.

Install (30 seconds)

curl -fsSL https://gist.githubusercontent.com/Maciejdziuba/038c0c822d2c799cffcfdec805975e66/raw/install.sh | bash
kimi-on setup     # paste your key from kimi.com/code/console

Get your key: subscribe at kimi.com/membership/pricing, then create an API key in the Kimi Code Console. This is not the pay-per-token key from platform.moonshot.ai — the Code Console key is covered by your subscription.

Use

kimi-on claude            # Claude Code on Kimi K3
kimi-on claude --init     # first run: also skips the Anthropic login screen
kimi-on claude-app on     # Claude Code DESKTOP APP on Kimi (writes ~/.claude/settings.json)
kimi-on claude-app off    # remove those settings again
kimi-on codex             # Codex CLI (auto-adds a 'kimi' profile)
kimi-on codex-app on      # Codex DESKTOP APP on Kimi (macOS: config + launchctl env)
kimi-on codex-app off     # undo the launchctl env
kimi-on opencode          # OpenCode (writes project opencode.json)
kimi-on aider             # Aider
kimi-on <anything>        # any other tool, both protocols injected
kimi-on env openai        # or just print the exports and wire it yourself
kimi-on oauth start       # NO-KEY mode: local proxy reusing your `kimi login` session
kimi-on claude --oauth    # Claude Code on pure Kimi OAuth

Model switches:

KIMI_MODEL=kimi-for-coding kimi-on claude    # K2.7 Code
KIMI_MODEL="k3[1m]" kimi-on claude           # K3 with 1M context (Allegretto+)

The universal rule

Any harness, even ones this kit has never heard of:

  • Tool speaks Anthropic (Claude Code style env vars) → point it at https://api.kimi.com/coding/
  • Tool speaks OpenAI (custom base URL setting) → point it at https://api.kimi.com/coding/v1
  • API key is the same Kimi Code key in both cases.

kimi-on <command> automates exactly this: it exports both sets of variables and runs your command.

Cheat sheet

Full per-tool configs (Cline, Roo Code, Codex config.toml, OpenCode JSON), plan tiers, model IDs, context limits, and the /effort mapping live in CHEATSHEET.md.

What's in the box

kimi-on.sh          the universal launcher
kimi-oauth-proxy.mjs  zero-dependency OAuth proxy (no API key mode)
install.sh          one-line installer
codex.toml          Codex CLI provider + profile block
opencode.json       OpenCode provider config
CHEATSHEET.md       one-page reference for every harness
kimi-router-SKILL.md  the Kimi Router Claude skill (split-brain: frontier model plans, K3 codes)
kimi-k3-harness-setup.md  agent runbook: point your AI agent at it to auto-install K3 on every harness

Related

Pairs with the Kimi Router Claude skill (included here as kimi-router-SKILL.md — save it to ~/.claude/skills/kimi-router/SKILL.md): a frontier model orchestrates and Kimi writes all the code. Router = split-brain inside one session; Everywhere = your whole harness on one subscription.

License

MIT — use it, fork it, ship it.

Kimi Everywhere — Cheat Sheet

One Kimi Code subscription key. Every harness. Copy-paste and go.

Endpoints & key

What Value
Anthropic-compatible base URL https://api.kimi.com/coding/
OpenAI-compatible base URL https://api.kimi.com/coding/v1
API key Create in the Kimi Code Console (subscription required)

Models & plans

Plan Models Context
Andante kimi-for-coding 256K
Moderato ($19/mo) k3, kimi-for-coding 256K
Allegretto+ ($39/mo+) k3, kimi-for-coding, kimi-for-coding-highspeed 1M for K3, 256K for K2.7 Code
  • k3 — Kimi K3, the flagship thinking model.
  • kimi-for-coding — the K2.7 Code series.
  • k3[1m] — Claude Code env vars only: bracket form unlocks the 1M window. Everywhere else use plain k3.
  • Keep thinking mode ON — disabling it silently routes K3/K2.7 down to K2.6.

Claude Code (Anthropic protocol)

export ANTHROPIC_BASE_URL=https://api.kimi.com/coding/
export ANTHROPIC_API_KEY=<your Kimi Code key>
export ANTHROPIC_MODEL=k3                     # or "k3[1m]" / kimi-for-coding
export ANTHROPIC_DEFAULT_OPUS_MODEL=$ANTHROPIC_MODEL
export ANTHROPIC_DEFAULT_SONNET_MODEL=$ANTHROPIC_MODEL
export ANTHROPIC_DEFAULT_HAIKU_MODEL=$ANTHROPIC_MODEL
export CLAUDE_CODE_SUBAGENT_MODEL=$ANTHROPIC_MODEL
export CLAUDE_CODE_EFFORT_LEVEL=high
export CLAUDE_CODE_AUTO_COMPACT_WINDOW=262144   # 1048576 with k3[1m]
export CLAUDE_CODE_MAX_CONTEXT_TOKENS=262144    # 1048576 with k3[1m]
claude

First run only: mark onboarding complete so Claude Code doesn't force an Anthropic login (kimi-on claude --init does this for you).

Effort mapping (/effort in-session):

Claude Code level K3 level
low low
medium / high high
xhigh / max max

Codex CLI (OpenAI protocol)

Append to ~/.codex/config.toml (or use codex.toml):

[model_providers.kimi]
name = "Kimi (subscription)"
base_url = "https://api.kimi.com/coding/v1"
env_key = "KIMI_API_KEY"
wire_api = "chat"

[profiles.kimi]
model_provider = "kimi"
model = "k3"

Then:

export KIMI_API_KEY=<your key>
codex --profile kimi

wire_api = "chat" matters — Kimi's endpoint speaks Chat Completions, not the Responses API.

OpenCode

Drop opencode.json into your project (or kimi-on opencode writes it):

{
  "$schema": "https://opencode.ai/config.json",
  "provider": {
    "kimi": {
      "npm": "@ai-sdk/openai-compatible",
      "name": "Kimi (subscription)",
      "options": {
        "baseURL": "https://api.kimi.com/coding/v1",
        "apiKey": "{env:KIMI_API_KEY}"
      },
      "models": {
        "k3": { "name": "Kimi K3" },
        "kimi-for-coding": { "name": "Kimi K2.7 Code" }
      }
    }
  },
  "model": "kimi/k3"
}

Cline / Roo Code (VS Code)

Settings → API Provider → OpenAI Compatible:

Field Value
Base URL https://api.kimi.com/coding/v1
API Key your Kimi Code key
Model ID k3 (or kimi-for-coding)

Aider

export OPENAI_API_BASE=https://api.kimi.com/coding/v1
export OPENAI_API_KEY=<your key>
aider --model openai/k3

Anything else

Speaks Anthropic → https://api.kimi.com/coding/. Speaks OpenAI → https://api.kimi.com/coding/v1. Same key. Or let the launcher inject both: kimi-on <command>.

Desktop apps

GUI apps don't see your shell env vars, so the desktop apps need a persistent route.

Claude Code appkimi-on claude-app on writes an env object into ~/.claude/settings.json (the same ANTHROPIC_BASE_URL / ANTHROPIC_API_KEY / model / context vars from the Claude Code section above) and sets hasCompletedOnboarding in ~/.claude.json. This affects every Claude Code surface (app, CLI, IDE) until you run kimi-on claude-app off, which deletes exactly those keys and leaves your other settings alone. A one-time backup of your original settings is kept at ~/.kimi-everywhere/claude-settings.backup.json. Restart the app fully after either command.

Codex appkimi-on codex-app on adds the same [model_providers.kimi] + [profiles.kimi] block to ~/.codex/config.toml as the CLI route, then runs launchctl setenv KIMI_API_KEY <key> so the GUI app can resolve env_key (macOS only; clears at logout — rerun after reboot). Fully quit and reopen the app, then pick the custom "kimi" entry in the model picker if offered — the app may only surface it as a "Custom" model. kimi-on codex-app off removes the launchctl var; the config block stays (harmless, the CLI uses it).

Heavier app-native routes, if the simple key route isn't enough: codex-router (native model picker + Kimi OAuth, third-party) and opencodex (universal proxy).

OAuth mode (no API key at all)

Your kimi login session (from the Kimi Code CLI) can power everything directly. kimi-on oauth start runs a tiny local proxy at 127.0.0.1:8790 that reuses and auto-refreshes those OAuth tokens:

Route Speaks For
http://127.0.0.1:8790/v1/chat/completions OpenAI format Codex, OpenCode, Cline, Aider
http://127.0.0.1:8790/v1/messages Anthropic format Claude Code
kimi-on oauth start        # needs: Kimi Code CLI installed + `kimi login` done
kimi-on claude --oauth     # Claude Code, zero API key
kimi-on oauth status       # token health
kimi-on oauth stop

Point any other tool at the proxy with any dummy API key — the proxy injects the real OAuth bearer and refreshes it automatically. The proxy is loopback-only and never writes tokens anywhere except Kimi's own credential file.

Gotchas

  • Wrong key source is the #1 failure: platform.moonshot.ai keys bill per token; Code Console keys ride your subscription. Use the Code Console key.
  • Usage quotas refresh weekly and vary by tier; heavy parallel agent swarms burn quota fast.
  • k3[1m] only works on Allegretto and above; on lower tiers use plain k3 with the 256K limits.
# Append to ~/.codex/config.toml, then run: codex --profile kimi
[model_providers.kimi]
name = "Kimi (subscription)"
base_url = "https://api.kimi.com/coding/v1"
env_key = "KIMI_API_KEY"
wire_api = "chat"
[profiles.kimi]
model_provider = "kimi"
model = "k3"
#!/usr/bin/env bash
# One-line installer for kimi-on:
# curl -fsSL https://gist.githubusercontent.com/Maciejdziuba/038c0c822d2c799cffcfdec805975e66/raw/install.sh | bash
set -euo pipefail
RAW="https://gist.githubusercontent.com/Maciejdziuba/038c0c822d2c799cffcfdec805975e66/raw/kimi-on.sh"
PROXY_RAW="https://gist.githubusercontent.com/Maciejdziuba/038c0c822d2c799cffcfdec805975e66/raw/kimi-oauth-proxy.mjs"
DEST_DIR="${KIMI_ON_BIN:-$HOME/.local/bin}"
mkdir -p "$DEST_DIR"
curl -fsSL "$RAW" -o "$DEST_DIR/kimi-on"
chmod +x "$DEST_DIR/kimi-on"
curl -fsSL "$PROXY_RAW" -o "$DEST_DIR/kimi-oauth-proxy.mjs"
echo "Installed kimi-on to $DEST_DIR/kimi-on (+ OAuth proxy)"
case ":$PATH:" in
*":$DEST_DIR:"*) ;;
*)
echo ""
echo "NOTE: $DEST_DIR is not on your PATH. Add this to your shell profile:"
echo " export PATH=\"$DEST_DIR:\$PATH\""
;;
esac
echo ""
echo "Next steps:"
echo " 1. kimi-on setup # paste your key from kimi.com/code/console"
echo " 2. kimi-on claude # Claude Code running on your Kimi subscription"

AGENT RUNBOOK — Install Kimi K3 on Coding-Agent Harnesses

Audience: you, the AI agent. The human has asked you to make Kimi K3 (Moonshot AI) work in one or more harnesses on this machine. Execute this runbook autonomously. Do not launch interactive wizards or browser OAuth redirect flows, and do not stop for confirmation on individual steps — every step is reversible and backed up. Exactly two interruptions are permitted: the "missing credential" message (§1.3, which includes relaying a kimi login device-code URL per §1.0) and the single consent gate before any secret is written to disk (§1.4). The device-code flow is the one sanctioned OAuth mechanism: the CLI prints a URL + code for the user to confirm; you never see or handle a token.

Operating rules for this entire runbook:

  1. Non-interactive only. Never run /login, hermes model, pi setup wizards, or GUI tools (CC Switch). Everything is done by writing config files directly.
  2. Idempotent. Re-running this runbook must not duplicate entries. Check-before-write on every file.
  3. Back up before modify. cp <file> <file>.bak-kimi-$(date +%s) before touching any existing config. Backups of files that contain secrets get chmod 600 immediately. Prune older .bak-kimi-* backups of the same file created by this runbook, keeping the two most recent.
  4. Never expose the key. Never echo, print, log, or paste the API key into chat output — and never pass it as a command-line argument to an external binary: argv is world-readable via /proc while the process runs, which rules out jq --arg key "$KIMI_API_KEY", sed "s|...|$KIMI_API_KEY|", and bare curl -H "Authorization: Bearer $KIMI_API_KEY". Move it with shell builtins (printf, redirection, heredocs) or tools that read the environment (jq's env.KIMI_API_KEY, awk's ENVIRON, curl -H @- with the header on stdin). When you must show the user a config you wrote, redact the key.
  5. Only configure harnesses that exist. Detect first (§1.2); skip absent ones silently.
  6. Verify each harness non-interactively (§7) and report a final per-harness pass/fail table.
  7. User policy wins. If the user or their harness has standing secret-handling rules (e.g. "ask before any command touching API keys"), those override this runbook's autonomy directives — the §1.4 consent gate is the mechanism that satisfies them.

0. Model facts you need (do not re-derive)

There are three distinct auth routes. Two billing systems: Routes S and O both ride the flat-rate Kimi Code subscription (different credentials); Route P bills per token. Prefer the subscription when the user has one. Route O is the only route that needs no API key at all — it reuses a kimi login OAuth session — but it currently pays off mainly for Codex (S is simpler for Claude Code, Hermes, and Pi).

Route S — Kimi Code subscription ("Kimi For Coding" / KFC) — PREFERRED

Fact Value
Key source https://www.kimi.com/code/console (separate from platform keys)
Base URL https://api.kimi.com/coding/ (Anthropic-compatible; also serves the kimi-coding providers in Hermes/Pi)
Model IDs k3 (256K), k3[1m] (1M, Claude Code env vars only — everywhere else use k3), kimi-for-coding (K2.7 Code, 256K), kimi-for-coding-highspeed
Plan gating Starter/legacy-Andante/Moderato tiers: k3 at 256K only. Explorer/Expert/Master (and legacy Allegretto+): k3 at 1M + highspeed
Thinking effort K3 supports low/high/max; Claude Code CLAUDE_CODE_EFFORT_LEVEL=high recommended; disabling thinking silently routes to K2.6

Route O — Kimi Code OAuth session (subscription login, no API key)

Fact Value
Credential An OAuth session created by kimi login (official Kimi Code CLI, kimi-code) — device-code flow: the CLI prints a URL + code, the user confirms in a browser. No key material passes through you.
Session owner The Kimi Code CLI stores and refreshes the session itself (~/.kimi-code/). Never read, print, or copy its credential files — bridges reuse the session, they don't extract it.
Bridge for Codex duolahypercho/codex-router — purpose-built router that puts kimi-oauth/k3 in the Codex model picker (App + CLI) next to native GPT models, preserving the ChatGPT login. Ships an agent runbook (AGENTS.md) and a non-interactive installer: ./install.sh --auto --providers kimi-oauth --migrate-known. Listens on 127.0.0.1 ports 4100–4103.
Bridge (alternative) lidge-jun/opencodex (npm @bitkyc08/opencodex, proxy on localhost:10100) — universal provider proxy for Codex and Claude Code with Kimi OAuth support. Its ocx init setup is interactive, so under this runbook use it only if codex-router fails and configure ~/.opencodex/config.json directly per its docs.
Plan gating Same subscription tiers as Route S (§1.3 tier heuristic applies).

Route P — Moonshot/Kimi Open Platform (pay-per-token)

Fact Value
Key source https://platform.kimi.ai/console/api-keys
Model ID kimi-k3
OpenAI-compatible endpoint https://api.moonshot.ai/v1
Anthropic-compatible endpoint https://api.moonshot.ai/anthropic
China endpoints https://api.moonshot.cn/v1, https://api.moonshot.cn/anthropic
Thinking effort reasoning_effort supports only max

(OpenRouter — moonshotai/kimi-k3, ~$3/M in, ~$15/M out — is a last-resort fallback only; do not configure it unless the user explicitly has an OpenRouter key and no Kimi key.)

Shared facts (both routes)

Fact Value
K3 context window 1048576 (1M) where the plan allows, else 262144
Thinking Always on for K3; never send the K2.x thinking param
Sampling params temperature, top_p, n, presence_penalty, frequency_penalty are fixed — never send them
Vision base64 or ms://<file-id> only; content must be an array of objects
Open weights Released by 2026-07-27; until confirmed available, self-hosting is out of scope — use the API

1. Preflight

1.0 Detect a Kimi Code OAuth session (Route O; check before hunting keys)

  1. Find the Kimi Code CLI: command -v kimi, else ~/.kimi-code/bin/kimi. Absent → Route O unavailable; continue with §1.1.
  2. If present, test for a valid session with one cheap non-interactive call: kimi -p 'Reply with exactly: OK' (timeout ~60s). Success → a live OAuth session exists; record Route O available. An auth/login error → CLI installed but not logged in.
  3. Installed-but-not-logged-in and Codex is a target: kimi login is a device-code flow, not a browser wizard — you may run it, relay the printed URL + code to the user verbatim, and wait for completion. This counts as part of the §1.3 "missing credential" interruption, not an extra one. If the user is unreachable or declines, drop Route O and fall back to §1.1.
  4. Route O only covers harnesses bridged in §3 Rung O. Even when Route O is available, still run §1.1–1.3 if other harnesses (Claude Code, Hermes, Pi) are targets — they need a key route (S/P). If no key exists anywhere but a Route O session does, configure Codex via Rung O and report the other harnesses as needing a key (§1.3 message), rather than stopping everything.

1.1 Locate a Moonshot API key (in order; stop at first hit)

Check without printing values (test emptiness only, e.g. [ -n "$VAR" ]):

  1. Env vars: KIMI_API_KEY, MOONSHOT_API_KEY, KIMI_CN_API_KEY
  2. ~/.hermes/.env (grep for ^KIMI_API_KEY=)
  3. ~/.pi/agent/auth.json (key kimi-coding, only if "type": "api_key" with a literal key)
  4. ~/.claude/settings.json.env.ANTHROPIC_AUTH_TOKEN only if .env.ANTHROPIC_BASE_URL contains moonshot
  5. ~/.codex/config.toml → a model_providers.* block whose base_url contains moonshot; its env_key names the env var to read
  6. Shell rc files (~/.zshrc, ~/.bashrc, ~/.profile): grep for KIMI_API_KEY= / MOONSHOT_API_KEY= exports

Normalize: export the found key as KIMI_API_KEY for the rest of this runbook (source the file or export from the found location — never echo it). It must be exported, not just a shell variable: later steps read it from the environment (jq's env, LiteLLM's os.environ/).

1.2 Detect installed harnesses

Harness Present if
Claude Code command -v claude
Codex CLI command -v codex
Hermes Agent command -v hermes or ~/.hermes/ exists
Pi command -v pi or ~/.pi/ exists

If the user named specific harnesses, configure exactly those (and report if one isn't installed). Otherwise configure every detected harness.

1.3 Classify and validate the key (the only potentially blocking step)

You must determine whether the key is a subscription (Route S) or platform (Route P) key. Probe in this order and stop at the first 200; the winning probe determines the route used for every section below.

CHECK=$(mktemp)   # never a fixed /tmp path — mktemp gives an unpredictable name with 0600 perms

# Probe S — subscription (Anthropic-style endpoint); auth header via stdin (-H @-), not argv
HTTP_S=$(curl -s -o "$CHECK" -w '%{http_code}' \
  https://api.kimi.com/coding/v1/messages \
  -H @- -H "anthropic-version: 2023-06-01" -H "Content-Type: application/json" \
  -d '{"model":"k3","max_tokens":16,"messages":[{"role":"user","content":"ping"}]}' \
  <<< "x-api-key: $KIMI_API_KEY")

# Probe P — platform (OpenAI-style endpoint), only if Probe S failed
HTTP_P=$(curl -s -o "$CHECK" -w '%{http_code}' \
  https://api.moonshot.ai/v1/chat/completions \
  -H @- -H "Content-Type: application/json" \
  -d '{"model":"kimi-k3","messages":[{"role":"user","content":"ping"}],"max_completion_tokens":16}' \
  <<< "Authorization: Bearer $KIMI_API_KEY")
  • Probe S returns 200Route S (subscription). Then detect the plan tier: attempt Probe S again with model kimi-for-coding-highspeed. A 200 implies an Explorer+/Allegretto+ tier → use the 1M config (k3[1m], window 1048576); a 4xx implies a base tier → use the 256K config (k3, window 262144). Record the tier. This is a heuristic — it assumes highspeed access and 1M K3 remain gated on the same tiers. If k3[1m] is later rejected during verification, downgrade to the 256K config (§8) instead of debugging the endpoint.

  • Probe S fails, Probe P returns 200Route P (platform).

  • Probe P 401 on .ai → retry against https://api.moonshot.cn/v1/...; a 200 means a China-platform key: Route P with .cn endpoints.

  • If multiple keys were found in §1.1, classify each and prefer any Route S key.

  • Nothing validates → check §1.0's result before stopping. A live Route O session + Codex-only target → proceed OAuth-only (§1.0 step 4). Otherwise stop and send the user this single message, then end your turn:

    I need a Kimi credential to finish the K3 setup. If you have a Kimi Code subscription, either (a) create a key at https://www.kimi.com/code/console (flat-rate; every paid tier can mint one), or (b) for a Codex-only setup I can use OAuth instead — install the Kimi Code CLI and I'll run kimi login and give you a URL to confirm, no key to paste. Otherwise create a pay-per-token key at https://platform.kimi.ai/console/api-keys. Then run export KIMI_API_KEY=<key> in this shell (or say "use oauth"), and say "continue" — I'll finish without further questions.

rm -f "$CHECK" afterward — probe responses can include account metadata.

1.4 Consent gate — one message before any secret is written

Before the first write of key material to disk, send the user one message listing:

  1. Every file that will receive the key, plaintext or env-reference — e.g. ~/.claude/settings.json, ~/.zshrc, ~/.hermes/.env, ~/.config/litellm/.kimi-key, ~/.pi/agent/auth.json.
  2. For each harness already routed to another provider: whether you will take over the default in place or create an isolated profile (decision rules in §2/§3).
  3. If §3 Rung O will run: that codex-router will be checked out to ~/.local/share/codex-router, add a marked managed block (openai_base_url + model_catalog_json) to ~/.codex/config.toml, and install a user-level background service on ports 4100–4103. Rung O writes no key material, but it modifies config.toml, so it belongs in this gate.

Proceed on confirmation; apply any adjustments the user makes. Skip this gate only when the user's request already granted it explicitly (e.g. "set up Kimi everywhere, overwrite what you need, don't ask"). After the gate, do not ask again — the remaining steps run uninterrupted.

Throughout §2–§6: SUB blocks apply on Route S, PLAT blocks on Route P. Route O has no per-harness blocks — it applies only as §3 Rung O (Codex); every other harness uses SUB or PLAT.


2. Claude Code

Target file: ~/.claude/settings.json. Back it up, then merge (never overwrite the whole file — preserve permissions, theme, hooks, etc.) the route-appropriate env block into .env.

SUB (Route S) — per the official Kimi Code docs. MODEL is k3[1m] on 1M-tier plans, k3 on 256K tiers (from §1.3); WINDOW is 1048576 or 262144 accordingly. Note the subscription uses ANTHROPIC_API_KEY (not AUTH_TOKEN) and effort high (not max):

cp ~/.claude/settings.json ~/.claude/settings.json.bak-kimi-$(date +%s) 2>/dev/null
chmod 600 ~/.claude/settings.json.bak-kimi-* 2>/dev/null
MODEL='k3[1m]'; WINDOW='1048576'   # or k3 / 262144 for 256K tiers
TMP=$(mktemp)
# key read via jq's env (exported in §1.1) — never via --arg, which exposes it in argv
jq --arg m "$MODEL" --arg w "$WINDOW" '.env = (.env // {}) + {
  "ANTHROPIC_BASE_URL":"https://api.kimi.com/coding/",
  "ANTHROPIC_API_KEY":env.KIMI_API_KEY,
  "ANTHROPIC_MODEL":$m,
  "ANTHROPIC_DEFAULT_FABLE_MODEL":$m,
  "ANTHROPIC_DEFAULT_OPUS_MODEL":$m,
  "ANTHROPIC_DEFAULT_SONNET_MODEL":$m,
  "ANTHROPIC_DEFAULT_HAIKU_MODEL":$m,
  "CLAUDE_CODE_SUBAGENT_MODEL":$m,
  "ENABLE_TOOL_SEARCH":"false",
  "CLAUDE_CODE_EFFORT_LEVEL":"high",
  "CLAUDE_CODE_AUTO_COMPACT_WINDOW":$w,
  "CLAUDE_CODE_MAX_CONTEXT_TOKENS":$w
}' ~/.claude/settings.json > "$TMP" && mv "$TMP" ~/.claude/settings.json && chmod 600 ~/.claude/settings.json

If Claude Code has never been onboarded on this machine, also set hasCompletedOnboarding: true (and penguinModeOrgEnabled: true) in ~/.claude.json so the user isn't dropped into the Anthropic login flow — merge, don't overwrite. Verify these keys against the installed Claude Code version first (an existing ~/.claude.json on this or another machine, or the docs); skip any key you cannot confirm rather than inventing settings.

PLAT (Route P) — platform pay-per-token:

cp ~/.claude/settings.json ~/.claude/settings.json.bak-kimi-$(date +%s) 2>/dev/null
chmod 600 ~/.claude/settings.json.bak-kimi-* 2>/dev/null
TMP=$(mktemp)
jq '.env = (.env // {}) + {
  "ANTHROPIC_BASE_URL":"https://api.moonshot.ai/anthropic",
  "ANTHROPIC_AUTH_TOKEN":env.KIMI_API_KEY,
  "ANTHROPIC_MODEL":"kimi-k3",
  "ANTHROPIC_DEFAULT_OPUS_MODEL":"kimi-k3",
  "ANTHROPIC_DEFAULT_SONNET_MODEL":"kimi-k3",
  "ANTHROPIC_DEFAULT_HAIKU_MODEL":"kimi-k3",
  "CLAUDE_CODE_SUBAGENT_MODEL":"kimi-k3",
  "ENABLE_TOOL_SEARCH":"false",
  "CLAUDE_CODE_AUTO_COMPACT_WINDOW":"1048576",
  "CLAUDE_CODE_EFFORT_LEVEL":"max"
}' ~/.claude/settings.json > "$TMP" && mv "$TMP" ~/.claude/settings.json && chmod 600 ~/.claude/settings.json

(Either route: if the file doesn't exist, create it as {"env": {...}} the same way, seeding from {}. If switching routes over a previous Kimi setup, first delete the other route's stale keys — e.g. remove ANTHROPIC_AUTH_TOKEN when writing SUB, remove ANTHROPIC_API_KEY when writing PLAT.)

Then remove conflicts — settings.json env overrides shell exports, but stale rc-file exports of ANTHROPIC_* pointing at other providers still confuse diagnosis. Grep ~/.zshrc/~/.bashrc for ANTHROPIC_BASE_URL/ANTHROPIC_AUTH_TOKEN exports; if any point to a non-moonshot provider, comment them out (with a # disabled by kimi-k3 setup marker), noting this in your final report.

Every model-tier variable above is required. Missing ones make subagents and background tasks (titles, summarization) fail silently with unknown-model errors. The exact ANTHROPIC_DEFAULT_* roster tracks Claude Code releases — confirm against the installed version's docs; an unrecognized variable is ignored harmlessly, but a missing recognized one is not.

Preserve-existing-setup variant: if settings.json already routes to another provider the user actively uses (e.g. a real Anthropic subscription), do not clobber it. Instead create an isolated profile: write the full config to ~/kimi-home/.claude/settings.json and append to the user's rc file:

alias kimi-claude='CLAUDE_CONFIG_DIR=$HOME/kimi-home/.claude claude'

Decision rule: existing env.ANTHROPIC_BASE_URL already moonshot → in-place merge. Existing non-moonshot base URL, or OAuth login in active use → isolated profile. "OAuth login in active use" is concrete: on Linux, ~/.claude/.credentials.json exists and is non-empty; on macOS, security find-generic-password -s "Claude Code-credentials" succeeds. Base URL absent and no OAuth credentials → in-place merge. State the choice in the §1.4 consent message and repeat it in the final report.


3. Codex CLI

Protocol constraint (verified by deep research, 2026-07): Codex CLI speaks only the OpenAI Responses API for custom providers — wire_api = "chat" was removed from Codex in early 2026. Kimi's endpoints (platform api.moonshot.ai/v1 and subscription api.kimi.com/coding/v1) are Chat Completions, so on a current Codex a direct wiring fails and a local translation bridge is required. Moonshot's official answer is the CC Switch GUI router — which you must NOT use (interactive). Attempt the ladder below in order; stop at the first rung whose §7 verification passes. Rung O first when Route O is available (§1.0) — it is the only rung that surfaces Kimi in the Codex App model picker alongside native GPT models and needs no key; Rungs A/B are the key-based (S/P) fallbacks.

Rung O — codex-router with the Kimi OAuth session (preferred; Route O)

Requires: a valid kimi login session (§1.0), Git, Node.js ≥22.19, and uv or Python 3.10+. Missing runtime → do not install system packages without consent; skip to Rung A/B.

Install per the repo's own agent runbook (AGENTS.md in duolahypercho/codex-router — read it first and follow it where it is stricter than this section):

# 1. Stable checkout — never install the service from a temp clone
git clone https://github.com/duolahypercho/codex-router ~/.local/share/codex-router \
  || git -C ~/.local/share/codex-router pull --ff-only
cd ~/.local/share/codex-router

# 2. Non-interactive install, OAuth provider only.
#    --migrate-known is safe: it only snapshots/migrates router installs the repo itself recognizes.
./install.sh --auto --providers kimi-oauth --migrate-known

# 3. Health — every core line must be OK (unselected credentials may WARN)
./bin/doctor || ./bin/doctor --fix
./bin/providers        # expect: kimi-oauth SHOW + ready

What it changes: only a marked # BEGIN codex-router-managed … # END block in ~/.codex/config.toml (openai_base_url → local router on 127.0.0.1:4102, plus model_catalog_json), first backup kept as config.toml.pre-codex-router. It does not touch model, model_provider, profiles, MCP config, or the ChatGPT login — native GPT models keep working; kimi-oauth/k3 appears as an additional picker entry. Internally: router :4102 → LiteLLM Responses↔Chat-Completions adapter :4100 → OAuth forwarder :4101 → Kimi Code API; all listeners on 127.0.0.1.

Rung O safety boundaries (from the repo's AGENTS.md; they override your defaults):

  • Never print, read, or copy credential files or the generated /_codex-router/<capability>/v1 base URL — treat the managed URL as a local secret; show it only redacted.
  • Never kill unknown processes on ports 4100–4103; never migrate/stop an unrecognized router — --migrate-known handles recognized ones only.
  • Do not restart or quit the Codex App yourself; the catalog loads at app startup, so the final step is the user's: fully quit Codex (not just the window) and reopen. Say this in the report. CLI verification (§7) works without the app restart.
  • If doctor still fails after --fix, run ./bin/support-bundle and report its path; do not upload it.

Rollback: cd ~/.local/share/codex-router && ./bin/rollback (or ./bin/migrate rollback for a migrated older install); the managed config block is removable by deleting the marked lines and restoring config.toml.pre-codex-router.

On success, skip Rungs A/B entirely (no LiteLLM service needed — codex-router embeds its own adapter). If the user also wants a key-based Codex profile as backup, that is out of scope unless requested.

Rung A — direct config (works only on older Codex builds that still accept wire_api = "chat")

Back up ~/.codex/config.toml, then write (update in place if a [model_providers.kimi] block exists; replace, don't duplicate, top-level keys):

model = "k3"                     # Route S; "kimi-k3" on Route P
model_provider = "kimi"
model_reasoning_effort = "high"

[model_providers.kimi]
name = "Kimi"
base_url = "https://api.kimi.com/coding/v1"   # Route S; https://api.moonshot.ai/v1 on Route P
env_key = "KIMI_API_KEY"
wire_api = "chat"

If codex exec errors on the config (unknown/invalid wire_api value) or requests 404, move straight to Rung B — do not debug Rung A further. (There is no direct Responses wiring: api.moonshot.ai/v1/responses returns 404 — the platform endpoint is Chat Completions only.)

Rung B — LiteLLM translation proxy (the non-interactive bridge; fallback on current Codex)

LiteLLM exposes a Responses-API facade locally and translates to Kimi's Chat Completions upstream.

# 1. Install (prefer uv; fall back to pipx/pip)
uv tool install "litellm[proxy]" || pipx install "litellm[proxy]" || pip install --user "litellm[proxy]"

# 2. Config — ~/.config/litellm/kimi.yaml (create dir; key via env reference, not plaintext)
mkdir -p ~/.config/litellm
cat > ~/.config/litellm/kimi.yaml <<'EOF'
model_list:
  - model_name: kimi-k3
    litellm_params:
      model: moonshot/k3                    # Route S; moonshot/kimi-k3 on Route P
      # Must be the moonshot/ prefix. openai/... passes requests through untranslated
      # and every request fails; moonshot/ engages LiteLLM's Moonshot translation.
      api_base: https://api.kimi.com/coding/v1   # Route S; https://api.moonshot.ai/v1 on Route P
      api_key: os.environ/KIMI_API_KEY
EOF

# 3. Key file the unit reads — written via shell builtins only (umask 077 → 0600); never inline the key in the unit
( umask 077; printf 'KIMI_API_KEY=%s\n' "$KIMI_API_KEY" > ~/.config/litellm/.kimi-key )

# 4a. Linux — run as a persistent background service (user-level systemd; survives reboots)
mkdir -p ~/.config/systemd/user
cat > ~/.config/systemd/user/litellm-kimi.service <<EOF
[Unit]
Description=LiteLLM proxy for Kimi (Codex bridge)
[Service]
EnvironmentFile=%h/.config/litellm/.kimi-key
ExecStart=$(command -v litellm) --config %h/.config/litellm/kimi.yaml --port 4141
Restart=on-failure
[Install]
WantedBy=default.target
EOF
systemctl --user daemon-reload && systemctl --user enable --now litellm-kimi

(EnvironmentFile= reads KIMI_API_KEY=... lines from the file — do not use Environment=KIMI_API_KEY=<path>, which would set the variable's value to the file path itself and 401 every request.)

# 4b. macOS — no systemd; use a user-level launchd agent instead.
# launchd has no EnvironmentFile equivalent, so a /bin/sh wrapper sources the 0600
# key file at start — the key itself never appears in the plist.
mkdir -p ~/Library/LaunchAgents
cat > ~/Library/LaunchAgents/com.kimi.litellm.plist <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>Label</key><string>com.kimi.litellm</string>
  <key>ProgramArguments</key>
  <array>
    <string>/bin/sh</string>
    <string>-c</string>
    <string>set -a; . "\$HOME/.config/litellm/.kimi-key"; exec $(command -v litellm) --config "\$HOME/.config/litellm/kimi.yaml" --port 4141</string>
  </array>
  <key>RunAtLoad</key><true/>
  <key>KeepAlive</key><dict><key>SuccessfulExit</key><false/></dict>
</dict>
</plist>
EOF
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.kimi.litellm.plist 2>/dev/null \
  || launchctl load ~/Library/LaunchAgents/com.kimi.litellm.plist   # older macOS fallback

(On hosts with neither systemd nor launchd, use the harness's tracked background-job facility to run litellm --config ... --port 4141 with KIMI_API_KEY exported, and note in the report that it must be relaunched after reboot.)

Then point Codex at the proxy:

model = "kimi-k3"
model_provider = "kimi"
model_reasoning_effort = "high"

[model_providers.kimi]
name = "Kimi via LiteLLM"
base_url = "http://localhost:4141/v1"
env_key = "KIMI_API_KEY"        # value irrelevant to the proxy hop but must be set
wire_api = "responses"

Sanity-check the proxy itself before blaming Codex: curl -s http://localhost:4141/v1/models should list kimi-k3.

Rung C — report-only fallback

If Rung O prerequisites are missing and LiteLLM cannot be installed (no network/python tooling), configure nothing, and in the final report tell the user their manual options: Moonshot's CC Switch GUI routing guide (https://platform.kimi.ai/docs/guide/codex-kimi), or the interactive ocx init of opencodex (https://github.com/lidge-jun/opencodex, npm i -g @bitkyc08/opencodex) which supports Kimi OAuth for Codex and Claude Code. Do not attempt to drive either interactive flow yourself.

Preserving an existing OpenAI setup

Keep the user's ChatGPT/OpenAI Codex auth usable: instead of overwriting top-level model/model_provider, you may define the Kimi rung under a profile. Current Codex versions reject [profiles.kimi] blocks inside config.toml — profiles must live in their own file. Write the profile to ~/.codex/kimi.config.toml:

# ~/.codex/kimi.config.toml — do NOT put this in config.toml (current Codex rejects it there)
model = "kimi-k3"
model_provider = "kimi"
model_reasoning_effort = "high"

(The [model_providers.kimi] block from the winning rung stays in ~/.codex/config.toml as before — only the profile moves.) Verify with codex exec --profile kimi .... Same decision rule as §2: take over the default only if no other provider is actively configured; otherwise use the profile and say so in the report.

Top-level model/model_provider keys: replace existing values rather than appending duplicates (TOML rejects duplicate keys).

Codex reads the key from the environment (env_key), so persist it: if KIMI_API_KEY isn't already exported in a shell rc file, append it to ~/.zshrc with the printf builtin (printf 'export KIMI_API_KEY=%s\n' "$KIMI_API_KEY" >> ~/.zshrc — builtins don't expose argv; do not display the key). This puts the key in plaintext in the rc file, so ~/.zshrc must be listed in the §1.4 consent message.

If the user's Codex is authenticated with a ChatGPT subscription they use, keep that usable: add the Kimi block but note in your report that codex --profile/model_provider switching applies; optionally define the profile in ~/.codex/kimi.config.toml (as above) instead of changing the top-level default — same decision rule as §2 (clobber only if nothing else is actively configured).


4. Hermes Agent

Kimi is a first-class Hermes provider; config is file-based, no wizard needed. Hermes' kimi-coding provider is built for the Kimi coding key — both routes use the same provider name and KIMI_API_KEY variable, so the steps below are route-independent except for the model ID (SUB: k3; PLAT: kimi-k3) and one override: on Route S, if requests fail against the provider's default endpoint, set KIMI_BASE_URL=https://api.kimi.com/coding/v1 in ~/.hermes/.env.

# 1. Key into ~/.hermes/.env (create file if missing; replace existing KIMI_API_KEY line, don't append a duplicate).
# No sed: the key in sed's argv is world-readable via /proc, and |/& in a key break the replacement.
mkdir -p ~/.hermes && touch ~/.hermes/.env && chmod 600 ~/.hermes/.env
TMP=$(mktemp)
grep -v '^KIMI_API_KEY=' ~/.hermes/.env > "$TMP" || true
printf 'KIMI_API_KEY=%s\n' "$KIMI_API_KEY" >> "$TMP"
mv "$TMP" ~/.hermes/.env && chmod 600 ~/.hermes/.env
  1. Set the default model in ~/.hermes/config.yaml (back up first; merge into the existing model: block if present):
model:
  provider: "kimi-coding"
  default: "k3"        # Route S; use "kimi-k3" on Route P
  • China-platform key (from §1.3): use provider kimi-coding-cn and put the key in KIMI_CN_API_KEY instead.
  • If config.yaml already has a provider the user actively uses, leave model: alone and just install the key — the user can switch with /model in-session; say so in the report. Same decision rule as §2.

Usage (for your verification and your report): hermes chat --provider kimi-coding --model k3 (or kimi-k3 on Route P).


5. Pi

Config is file-based. The two routes diverge here — Pi's built-in kimi-coding provider only accepts subscription (Route S) keys; a platform pay-per-token key gets a 401 from it. Route P needs a custom provider in ~/.pi/agent/models.json instead.

SUB (Route S) — use the built-in provider, literally named "Kimi For Coding" (kimi-coding), via ~/.pi/agent/auth.json. Pi's auth.json supports env interpolation, so prefer the no-plaintext form: since you persisted export KIMI_API_KEY in §3 (do it even if Codex is absent, when Pi is present), write:

mkdir -p ~/.pi/agent
[ -f ~/.pi/agent/auth.json ] && cp ~/.pi/agent/auth.json ~/.pi/agent/auth.json.bak-kimi-$(date +%s)
# merge, preserving other providers' credentials
[ -f ~/.pi/agent/auth.json ] || echo '{}' > ~/.pi/agent/auth.json
TMP=$(mktemp)
jq '. + {"kimi-coding": {"type": "api_key", "key": "$KIMI_API_KEY"}}' \
  ~/.pi/agent/auth.json > "$TMP" && mv "$TMP" ~/.pi/agent/auth.json
chmod 600 ~/.pi/agent/auth.json

Note: "$KIMI_API_KEY" here is a literal string — Pi interpolates it at runtime. Do not substitute the real key into this file. auth.json beats env vars in Pi's resolution order, so this entry wins cleanly.

PLAT (Route P) — do not write the key under kimi-coding (401). Define a custom Moonshot provider in ~/.pi/agent/models.json (back up first; merge if the file exists, preserving other custom providers):

mkdir -p ~/.pi/agent
[ -f ~/.pi/agent/models.json ] && cp ~/.pi/agent/models.json ~/.pi/agent/models.json.bak-kimi-$(date +%s)
[ -f ~/.pi/agent/models.json ] || echo '{}' > ~/.pi/agent/models.json
TMP=$(mktemp)
jq '.providers = (.providers // {}) + {
  "moonshot": {
    "name": "Moonshot (Kimi Platform)",
    "baseUrl": "https://api.moonshot.ai/v1",
    "api": "openai-completions",
    "apiKey": "KIMI_API_KEY",
    "models": [
      {"id": "kimi-k3", "name": "Kimi K3", "reasoning": true, "contextWindow": 1048576, "maxTokens": 16384}
    ]
  }
}' ~/.pi/agent/models.json > "$TMP" && mv "$TMP" ~/.pi/agent/models.json
chmod 600 ~/.pi/agent/models.json

"apiKey": "KIMI_API_KEY" names the env var Pi reads at runtime — no plaintext key lands in this file, but the §3 rc-file export must exist. Verify the field names against the installed Pi version's custom-provider docs before writing; if they differ, adapt rather than guessing. Use China endpoint https://api.moonshot.cn/v1 for a .cn key.

Model selection in Pi is per-session (/model) — no default-model file edit is needed or safe to guess. In the report, tell the user: "In Pi, pick Kimi K3 via /model (provider: Kimi For Coding on Route S; Moonshot on Route P)."


6. Other OpenAI-compatible harnesses (OpenCode, Cline, Aider, Zed, ...)

If the user asked for a harness not covered above and it supports custom OpenAI-compatible providers, configure its provider file with:

  • Base URL https://api.moonshot.ai/v1, model kimi-k3, key from $KIMI_API_KEY
  • Context window 1048576 if the harness has such a field
  • Do not configure sampling parameters (they're fixed server-side)

Find that harness's non-interactive config file yourself (docs or --help); apply the same rules: backup, merge, no wizards, no plaintext key in chat.


7. Verification (all non-interactive)

Run for each configured harness; collect results for the final report.

Use MODEL=k3 on Route S (never k3[1m] outside Claude Code env vars) and MODEL=kimi-k3 on Route P.

Harness Command Pass criterion
API direct winning probe from §1.3 HTTP 200
Claude Code claude -p 'Reply with exactly: KIMI-OK' (with the profile's env active; model comes from settings.json) Output contains KIMI-OK
Codex CLI (Rung A/B) codex exec 'Reply with exactly: KIMI-OK' Output contains KIMI-OK; startup banner shows the Kimi model
Codex CLI (Rung O) ~/.local/share/codex-router/bin/doctor, then codex exec -m kimi-oauth/k3 'Reply with exactly: KIMI-OK' Doctor all-OK on core lines; output contains KIMI-OK. App picker entry only verifiable by the user after a full app restart — state it in the report, don't fail on it
Hermes hermes chat --provider kimi-coding --model $MODEL -p 'Reply with exactly: KIMI-OK' (use --print/non-interactive flag per hermes chat --help) Output contains KIMI-OK
Pi pi --provider kimi-coding --model k3 -p 'Reply with exactly: KIMI-OK' on Route S; pi --provider moonshot --model kimi-k3 -p '...' on Route P (check pi --help for the print flag) Output contains KIMI-OK

If a harness's non-interactive flag differs from the above, discover it via --help rather than skipping verification. A verification failure is yours to debug (see §8) — do not hand it back to the user until you've worked through §8.


8. Failure playbook (work through before reporting failure)

Symptom Cause Fix
401 (Claude Code) Stale env entries in settings.json overriding your merge; or wrong-region key Re-read the file you wrote; try .cn endpoints
"model not found" A model-tier variable still points at a Claude/GPT default Re-check all 7 model vars in §2
400 invalid thinking K2.x thinking param sent, or thinking disabled K3: remove thinking, use reasoning_effort; never disable thinking
Tool calls misbehave (Claude Code) Tool Search enabled ENABLE_TOOL_SEARCH must be the string "false"
Premature compaction / context errors Wrong compact window CLAUDE_CODE_AUTO_COMPACT_WINDOW=1048576
Codex ignores provider Duplicate top-level model/model_provider keys, or TOML parse error codex exec surfaces parse errors; validate the TOML
Codex rejects wire_api = "chat" Current Codex builds only support responses Expected — move to §3 Rung B (LiteLLM proxy); there is no direct Responses wiring (api.moonshot.ai/v1/responses is 404)
Codex hangs 15+ min before first output Hooks or MCP servers in ~/.codex/config.toml stall startup when combined with a custom provider Temporarily comment out mcp_servers/hook blocks, retry, then re-enable one at a time to find the culprit
Codex rejects config: unknown [profiles.*] Profile block placed in config.toml; current Codex requires profiles in their own file Move the block to ~/.codex/kimi.config.toml (§3)
Every request via proxy fails / passthrough errors LiteLLM model uses the openai/ prefix, which doesn't translate Use moonshot/k3 (Route S) / moonshot/kimi-k3 (Route P) in kimi.yaml; restart the service
Codex 404/protocol errors via proxy LiteLLM not running, wrong port, or upstream mapping wrong curl http://localhost:4141/v1/models; check systemctl --user status litellm-kimi (Linux) or launchctl list | grep com.kimi.litellm (macOS); verify api_base/model in kimi.yaml
Codex 401 via proxy Unit uses Environment=KIMI_API_KEY=<path> (value = the file path) instead of EnvironmentFile= EnvironmentFile=%h/.config/litellm/.kimi-key with a KIMI_API_KEY=... line inside (0600); restart the unit
Hermes uses wrong model config.yaml model: block not merged / overridden by CLI flags Inspect effective config; pass explicit --provider --model
Pi can't resolve key Literal $KIMI_API_KEY written but env var not exported in the shell Pi runs from Confirm the rc-file export from §3; restart shell context
Pi 401 on Route P Platform pay-per-token key written under the kimi-coding provider, which only accepts subscription keys Remove the kimi-coding auth.json entry; use the custom moonshot provider in models.json (§5 PLAT)
Sampling-param rejection Harness injects temperature etc. Remove from harness config; K3 fixes these server-side
401 with a valid subscription Route mix-up: subscription key sent to api.moonshot.ai, or platform key sent to api.kimi.com/coding/ Re-run §1.3 classification; the two consoles issue different keys
Rung O: auth errors after previously working kimi login OAuth session expired or revoked Re-run §1.0 step 2; if dead, relay a fresh kimi login device-code URL to the user, then ./bin/doctor
Rung O: Kimi missing from the App picker but CLI works Catalog loads only at app startup, or stale catalog User must fully quit Codex (not just the window) and reopen; if still missing, ./bin/refresh-catalog then quit/reopen again
Rung O: doctor FAIL on service/router health Port 4100–4103 conflict or service not started systemctl --user status the router unit; if a foreign process owns the port, report it — never kill unknown processes on those ports
Rung O: codex exec ignores the router Managed block removed/mangled in config.toml, or another tool overwrote openai_base_url Check the marked codex-router-managed block; ./bin/doctor --fix rebuilds it
"model not found: kimi-k3" on Route S Platform model ID used against the subscription endpoint Subscription model IDs are k3 / kimi-for-coding, not kimi-k3
k3[1m] rejected 1M form used outside Claude Code env vars, or plan is a 256K tier Use k3 everywhere else; downgrade to 256K config per §1.3 tier check

9. Final report to the user (required format)

End with a short message containing:

  1. Which auth route was used per harness (S subscription key / O OAuth session / P platform pay-per-token) and, on Route S, which plan tier was detected (1M vs 256K).
  2. Per-harness table: configured (in-place / isolated profile / skipped-not-installed) and verification pass/fail.
  3. Files modified, each with its backup path.
  4. The 1–2 things the user must know: e.g. kimi-claude alias if you made an isolated profile; /model selection note for Pi; restart open harness sessions to pick up new config; on Rung O, fully quit and reopen the Codex App to load the new model catalog.
  5. Rollback one-liner: "To undo, restore the .bak-kimi-* files listed above."

No secrets in the report. No questions in the report.

#!/usr/bin/env node
// kimi-oauth-proxy — run any harness on your Kimi subscription with NO API key.
//
// Reuses the OAuth session created by `kimi login` (Kimi Code CLI):
// ~/.kimi-code/credentials/kimi-code.json access + refresh token
// ~/.kimi-code/device_id device identity header
//
// Serves http://127.0.0.1:8790 and forwards any /v1/* route to Kimi's
// subscription gateway with a fresh OAuth bearer:
// /v1/chat/completions OpenAI format (Codex, OpenCode, Cline, Aider)
// /v1/messages Anthropic format (Claude Code)
//
// Zero dependencies. Node 18+.
import {
chmodSync, closeSync, existsSync, fsyncSync, mkdirSync,
openSync, readFileSync, renameSync, unlinkSync, writeFileSync,
} from "node:fs";
import http from "node:http";
import os from "node:os";
import path from "node:path";
const VERSION = "1.0.0";
// Public OAuth client id of the Kimi Code CLI (same one codex-router uses).
const KIMI_CLIENT_ID = "17e5f671-d194-4dfb-9706-5516cb48c098";
const KIMI_CODE_HOME = process.env.KIMI_CODE_HOME || path.join(os.homedir(), ".kimi-code");
const API_BASE = (process.env.KIMI_CODING_BASE || "https://api.kimi.com/coding/v1").replace(/\/+$/, "");
const OAUTH_HOST = (process.env.KIMI_OAUTH_HOST || "https://auth.kimi.com").replace(/\/+$/, "");
const LISTEN_HOST = "127.0.0.1";
const LISTEN_PORT = Number(process.env.KIMI_OAUTH_PROXY_PORT || 8790);
const QUIET = process.env.KIMI_OAUTH_PROXY_QUIET === "1";
const CREDENTIALS_PATH = path.join(KIMI_CODE_HOME, "credentials", "kimi-code.json");
const DEVICE_ID_PATH = path.join(KIMI_CODE_HOME, "device_id");
const HOP_BY_HOP = new Set([
"connection", "keep-alive", "proxy-authenticate", "proxy-authorization",
"te", "trailer", "transfer-encoding", "upgrade", "host", "content-length",
]);
let refreshInFlight;
function log(message) {
if (!QUIET) console.error(`[kimi-oauth-proxy] ${message}`);
}
function asciiHeader(value, fallback = "unknown") {
const cleaned = String(value).replace(/[^ -~]/g, "").trim();
return cleaned || fallback;
}
function readDeviceId() {
const value = readFileSync(DEVICE_ID_PATH, "utf8").trim();
if (!value) throw new Error("Kimi device id missing; run `kimi login` first.");
return value;
}
// Same identity header shape the Kimi Code CLI ecosystem sends; the gateway
// expects a device identity alongside the bearer.
function identityHeaders() {
return {
"User-Agent": `kimi-oauth-proxy/${VERSION}`,
"X-Msh-Platform": "codex",
"X-Msh-Version": VERSION,
"X-Msh-Device-Name": asciiHeader(os.hostname()),
"X-Msh-Device-Model": asciiHeader(`${os.type()} ${os.release()} ${os.arch()}`),
"X-Msh-Os-Version": asciiHeader(os.release()),
"X-Msh-Device-Id": asciiHeader(readDeviceId()),
};
}
function readToken() {
if (!existsSync(CREDENTIALS_PATH)) {
throw new Error("Kimi OAuth credentials not found; run `kimi login` first.");
}
const value = JSON.parse(readFileSync(CREDENTIALS_PATH, "utf8"));
if (!value?.access_token || !value?.refresh_token) {
throw new Error("Kimi OAuth credentials incomplete; run `kimi login` again.");
}
return {
access_token: value.access_token,
refresh_token: value.refresh_token,
expires_at: Number(value.expires_at) || 0,
expires_in: Number(value.expires_in) || 0,
scope: value.scope || "kimi-code",
token_type: value.token_type || "Bearer",
};
}
function shouldRefresh(token) {
const threshold = Math.max(300, token.expires_in > 0 ? token.expires_in * 0.5 : 0);
return Math.floor(Date.now() / 1000) >= token.expires_at - threshold;
}
function atomicSaveToken(token) {
const dir = path.dirname(CREDENTIALS_PATH);
mkdirSync(dir, { recursive: true, mode: 0o700 });
const tmp = `${CREDENTIALS_PATH}.tmp.${process.pid}`;
const fd = openSync(tmp, "w", 0o600);
try {
writeFileSync(fd, `${JSON.stringify(token, null, 2)}\n`, "utf8");
fsyncSync(fd);
} finally {
closeSync(fd);
}
try {
chmodSync(tmp, 0o600);
renameSync(tmp, CREDENTIALS_PATH);
chmodSync(CREDENTIALS_PATH, 0o600);
} catch (error) {
try { unlinkSync(tmp); } catch {}
throw error;
}
}
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
async function requestRefresh(refreshTokenValue) {
const retryable = new Set([429, 500, 502, 503, 504]);
let lastError;
for (let attempt = 0; attempt < 3; attempt += 1) {
try {
const response = await fetch(`${OAUTH_HOST}/api/oauth/token`, {
method: "POST",
headers: {
...identityHeaders(),
Accept: "application/json",
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({
client_id: KIMI_CLIENT_ID,
grant_type: "refresh_token",
refresh_token: refreshTokenValue,
}),
signal: AbortSignal.timeout(30_000),
});
const payload = await response.json().catch(() => ({}));
if (response.ok) {
const expiresIn = Number(payload.expires_in);
if (!payload.access_token || !payload.refresh_token || !(expiresIn > 0)) {
throw new Error("OAuth refresh returned an incomplete response.");
}
return {
access_token: payload.access_token,
refresh_token: payload.refresh_token,
expires_at: Math.floor(Date.now() / 1000) + expiresIn,
expires_in: expiresIn,
scope: payload.scope || "kimi-code",
token_type: payload.token_type || "Bearer",
};
}
if (response.status === 401 || response.status === 403 || payload.error === "invalid_grant") {
const error = new Error("OAuth refresh rejected; run `kimi login` again.");
error.code = "oauth_unauthorized";
throw error;
}
if (!retryable.has(response.status)) {
throw new Error(`OAuth refresh failed with HTTP ${response.status}.`);
}
lastError = new Error(`Temporary OAuth error: HTTP ${response.status}.`);
} catch (error) {
if (error?.code === "oauth_unauthorized") throw error;
lastError = error instanceof Error ? error : new Error(String(error));
}
if (attempt < 2) await wait(2 ** attempt * 1000);
}
throw lastError || new Error("OAuth refresh failed.");
}
// Single-flight refresh: concurrent requests share one refresh; the token
// file is re-read after acquiring the slot in case another process refreshed.
async function ensureFreshToken({ force = false } = {}) {
if (refreshInFlight) return refreshInFlight;
refreshInFlight = (async () => {
const current = readToken();
if (!force && !shouldRefresh(current)) return current.access_token;
const refreshed = await requestRefresh(current.refresh_token);
atomicSaveToken(refreshed);
log("OAuth token refreshed");
return refreshed.access_token;
})().finally(() => { refreshInFlight = undefined; });
return refreshInFlight;
}
// Kimi routes non-thinking traffic to older models; keep thinking on and map
// OpenAI-style effort levels onto Kimi's low/high/max. Chat Completions only —
// Anthropic-format bodies pass through untouched.
function normalizeChatBody(buffer, contentType) {
if (!buffer.length || !String(contentType || "").includes("application/json")) return buffer;
let payload;
try { payload = JSON.parse(buffer.toString("utf8")); } catch { return buffer; }
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return buffer;
payload.thinking = { type: "enabled" };
const effort = {
minimal: "low", low: "low", medium: "high",
high: "high", xhigh: "max", max: "max", ultra: "max",
}[payload.reasoning_effort];
if (effort) payload.reasoning_effort = effort;
else delete payload.reasoning_effort;
return Buffer.from(JSON.stringify(payload), "utf8");
}
function upstreamHeaders(requestHeaders, body) {
const headers = {};
for (const [name, value] of Object.entries(requestHeaders)) {
const lower = name.toLowerCase();
if (HOP_BY_HOP.has(lower) || lower === "authorization") continue;
if (lower.startsWith("x-msh-") || lower.startsWith("x-codex-")) continue;
if (lower.startsWith("x-openai-") || lower === "chatgpt-account-id") continue;
if (lower === "x-api-key" || lower === "originator") continue;
if (lower === "user-agent" || lower === "accept-encoding") continue;
if (value !== undefined) headers[name] = Array.isArray(value) ? value.join(", ") : value;
}
Object.assign(headers, identityHeaders());
headers["Accept-Encoding"] = "identity";
if (body.length) headers["Content-Length"] = String(body.length);
return headers;
}
function readBody(request) {
return new Promise((resolve, reject) => {
const chunks = [];
request.on("data", (chunk) => chunks.push(chunk));
request.on("end", () => resolve(Buffer.concat(chunks)));
request.on("error", reject);
});
}
function writeJson(response, status, value) {
const body = JSON.stringify(value);
response.writeHead(status, { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(body) });
response.end(body);
}
async function pipeResponse(upstream, response) {
const headers = {};
upstream.headers.forEach((value, name) => {
if (!HOP_BY_HOP.has(name.toLowerCase())) headers[name] = value;
});
response.writeHead(upstream.status, headers);
if (!upstream.body) { response.end(); return; }
for await (const chunk of upstream.body) {
if (!response.write(chunk)) {
await new Promise((resolve) => response.once("drain", resolve));
}
}
response.end();
}
function tokenHealth() {
try {
const token = readToken();
return {
credential_present: true,
scope: token.scope,
expires_in_seconds: Math.max(0, token.expires_at - Math.floor(Date.now() / 1000)),
};
} catch (error) {
return { credential_present: false, error: String(error?.message || error) };
}
}
async function requestUpstream(request, target, body, token, signal) {
return fetch(target, {
method: request.method,
headers: { ...upstreamHeaders(request.headers, body), Authorization: `Bearer ${token}` },
body: body.length ? body : undefined,
signal,
});
}
async function handle(request, response) {
const startedAt = Date.now();
const url = new URL(request.url || "/", `http://${request.headers.host || LISTEN_HOST}`);
if (request.method === "GET" && url.pathname === "/health") {
writeJson(response, 200, { ok: true, version: VERSION, ...tokenHealth() });
return;
}
if (!url.pathname.startsWith("/v1/") || !["GET", "POST"].includes(request.method)) {
writeJson(response, 404, { error: { type: "route_not_found", message: "Only /v1/* is proxied." } });
return;
}
const controller = new AbortController();
request.once("aborted", () => controller.abort());
response.once("close", () => { if (!response.writableEnded) controller.abort(); });
let body = await readBody(request);
const route = url.pathname.slice("/v1".length);
if (route === "/chat/completions") {
body = normalizeChatBody(body, request.headers["content-type"]);
}
const target = `${API_BASE}${route}${url.search}`;
let token = await ensureFreshToken();
let upstream = await requestUpstream(request, target, body, token, controller.signal);
if (upstream.status === 401) {
await upstream.arrayBuffer();
token = await ensureFreshToken({ force: true });
upstream = await requestUpstream(request, target, body, token, controller.signal);
}
await pipeResponse(upstream, response);
log(`${request.method} ${route} -> ${upstream.status} ${Date.now() - startedAt}ms`);
}
const server = http.createServer((request, response) => {
handle(request, response).catch((error) => {
log(`request failed: ${error?.message || error}`);
if (!response.headersSent) {
writeJson(response, 502, {
error: { type: "kimi_oauth_proxy_error", message: String(error?.message || error) },
});
} else if (!response.writableEnded) {
response.destroy(error instanceof Error ? error : undefined);
}
});
});
server.listen(LISTEN_PORT, LISTEN_HOST, () => {
log(`listening on http://${LISTEN_HOST}:${LISTEN_PORT} -> ${API_BASE}`);
});
for (const signal of ["SIGINT", "SIGTERM"]) {
process.on(signal, () => server.close(() => process.exit(0)));
}
#!/usr/bin/env bash
# kimi-on — run any coding harness on your flat-fee Kimi subscription.
#
# One Kimi Code API key speaks two protocols:
# Anthropic-compatible : https://api.kimi.com/coding/
# OpenAI-compatible : https://api.kimi.com/coding/v1
#
# Known tools get a tailored launch; any other command is run with BOTH
# protocol env vars injected, so it works with anything that accepts a
# custom Anthropic or OpenAI endpoint.
set -euo pipefail
CONF="${KIMI_EVERYWHERE_ENV:-$HOME/.kimi-everywhere/env}"
ANTHROPIC_EP="https://api.kimi.com/coding/"
OPENAI_EP="https://api.kimi.com/coding/v1"
DEFAULT_MODEL="${KIMI_MODEL:-k3}"
usage() {
cat <<'EOF'
kimi-on — power any coding harness with one Kimi subscription
Usage:
kimi-on setup store your Kimi Code API key (one time)
kimi-on claude [args...] Claude Code on Kimi (Anthropic endpoint)
kimi-on claude --init also skip Anthropic onboarding (first run)
kimi-on claude-app on|off Claude Code DESKTOP APP on Kimi (persistent settings)
kimi-on codex [args...] Codex CLI on Kimi (adds a 'kimi' profile)
kimi-on codex-app on|off Codex DESKTOP APP on Kimi (macOS, launchctl env)
kimi-on opencode [args...] OpenCode on Kimi (project opencode.json)
kimi-on aider [args...] Aider on Kimi (OpenAI-compatible)
kimi-on env anthropic print export lines for Anthropic-style tools
kimi-on env openai print export lines for OpenAI-style tools
kimi-on <any-command> run any tool with both protocols injected
OAuth mode (no API key — reuses your `kimi login` session):
kimi-on oauth start run the local OAuth proxy (127.0.0.1:8790)
kimi-on oauth stop|status manage / inspect the proxy
kimi-on claude --oauth Claude Code through the OAuth proxy
Model selection (default k3):
KIMI_MODEL=kimi-for-coding kimi-on claude # K2.7 Code
KIMI_MODEL="k3[1m]" kimi-on claude # 1M context (Allegretto+)
EOF
}
load_key() {
# shellcheck disable=SC1090
[ -f "$CONF" ] && . "$CONF"
if [ -z "${KIMI_API_KEY:-}" ]; then
echo "No Kimi Code API key found." >&2
echo "Create one at https://www.kimi.com/code/console (requires a Kimi subscription)." >&2
printf 'Paste your Kimi Code API key: ' >&2
read -r -s KIMI_API_KEY
echo >&2
[ -n "$KIMI_API_KEY" ] || { echo "No key entered, aborting." >&2; exit 1; }
mkdir -p "$(dirname "$CONF")"
printf 'KIMI_API_KEY=%s\n' "$KIMI_API_KEY" > "$CONF"
chmod 600 "$CONF"
echo "Saved to $CONF" >&2
fi
export KIMI_API_KEY
}
# Claude Code reads a bracketed [1m] suffix; context limit must match the model.
context_for_model() {
case "$1" in
*"[1m]"*) echo 1048576 ;;
*) echo 262144 ;;
esac
}
claude_init() {
# Official Moonshot onboarding-skip: marks onboarding complete so Claude
# Code doesn't force an Anthropic login before honoring ANTHROPIC_BASE_URL.
node --eval "
const fs = require('fs'); const os = require('os'); const path = require('path');
const p = path.join(os.homedir(), '.claude.json');
const cur = fs.existsSync(p) ? JSON.parse(fs.readFileSync(p, 'utf-8')) : {};
fs.writeFileSync(p, JSON.stringify({ ...cur, hasCompletedOnboarding: true }, null, 2), 'utf-8');
console.log('Claude Code onboarding marked complete.');
"
}
# ── OAuth proxy (no API key; reuses the `kimi login` session) ──────────────
PROXY_PORT="${KIMI_OAUTH_PROXY_PORT:-8790}"
PROXY_URL="http://127.0.0.1:$PROXY_PORT"
PROXY_RAW_URL="https://gist.githubusercontent.com/Maciejdziuba/038c0c822d2c799cffcfdec805975e66/raw/kimi-oauth-proxy.mjs"
proxy_paths() {
PROXY_DIR="$(dirname "$CONF")"
PROXY_PID_FILE="$PROXY_DIR/oauth-proxy.pid"
PROXY_LOG="$PROXY_DIR/oauth-proxy.log"
}
# The proxy script ships next to kimi-on in the gist; self-download if absent.
proxy_script() {
proxy_paths
local here script
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
for script in "$here/kimi-oauth-proxy.mjs" "$PROXY_DIR/kimi-oauth-proxy.mjs"; do
[ -f "$script" ] && { echo "$script"; return; }
done
mkdir -p "$PROXY_DIR"
curl -fsSL "$PROXY_RAW_URL" -o "$PROXY_DIR/kimi-oauth-proxy.mjs"
echo "$PROXY_DIR/kimi-oauth-proxy.mjs"
}
proxy_healthy() {
curl -s -m 2 "$PROXY_URL/health" 2>/dev/null | grep -q '"ok":true'
}
oauth_creds_check() {
local kimi_home="${KIMI_CODE_HOME:-$HOME/.kimi-code}"
if [ ! -f "$kimi_home/credentials/kimi-code.json" ]; then
echo "No Kimi OAuth session found ($kimi_home/credentials/kimi-code.json)." >&2
echo "Install the Kimi Code CLI and run: kimi login" >&2
exit 1
fi
}
oauth_start() {
command -v node >/dev/null 2>&1 || { echo "node is required for OAuth mode." >&2; exit 1; }
oauth_creds_check
proxy_paths
if proxy_healthy; then echo "OAuth proxy already running at $PROXY_URL"; return; fi
local script
script="$(proxy_script)"
mkdir -p "$PROXY_DIR"
nohup node "$script" >> "$PROXY_LOG" 2>&1 &
echo $! > "$PROXY_PID_FILE"
local tries=0
until proxy_healthy; do
tries=$((tries + 1))
[ "$tries" -gt 20 ] && { echo "Proxy failed to start; see $PROXY_LOG" >&2; exit 1; }
sleep 0.3
done
echo "OAuth proxy running at $PROXY_URL (log: $PROXY_LOG)"
}
oauth_stop() {
proxy_paths
if [ -f "$PROXY_PID_FILE" ]; then
kill "$(cat "$PROXY_PID_FILE")" 2>/dev/null || true
rm -f "$PROXY_PID_FILE"
echo "OAuth proxy stopped."
else
echo "No proxy pid file; nothing to stop."
fi
}
oauth_status() {
if proxy_healthy; then
curl -s -m 2 "$PROXY_URL/health"
echo
else
echo "OAuth proxy is not running. Start it with: kimi-on oauth start"
fi
}
# Claude Code pointed at the local proxy; auth is injected by the proxy, the
# dummy token only satisfies Claude Code's "some auth must be set" check.
run_claude_oauth() {
local model="$DEFAULT_MODEL" ctx
ctx="$(context_for_model "$model")"
proxy_healthy || oauth_start
ANTHROPIC_BASE_URL="$PROXY_URL" \
ANTHROPIC_AUTH_TOKEN="kimi-oauth-proxy" \
ANTHROPIC_MODEL="$model" \
ANTHROPIC_DEFAULT_OPUS_MODEL="$model" \
ANTHROPIC_DEFAULT_SONNET_MODEL="$model" \
ANTHROPIC_DEFAULT_HAIKU_MODEL="$model" \
CLAUDE_CODE_SUBAGENT_MODEL="$model" \
CLAUDE_CODE_EFFORT_LEVEL="${KIMI_EFFORT:-high}" \
CLAUDE_CODE_AUTO_COMPACT_WINDOW="$ctx" \
CLAUDE_CODE_MAX_CONTEXT_TOKENS="$ctx" \
claude "$@"
}
run_claude() {
local model="$DEFAULT_MODEL" ctx
ctx="$(context_for_model "$model")"
ANTHROPIC_BASE_URL="$ANTHROPIC_EP" \
ANTHROPIC_API_KEY="$KIMI_API_KEY" \
ANTHROPIC_MODEL="$model" \
ANTHROPIC_DEFAULT_OPUS_MODEL="$model" \
ANTHROPIC_DEFAULT_SONNET_MODEL="$model" \
ANTHROPIC_DEFAULT_HAIKU_MODEL="$model" \
CLAUDE_CODE_SUBAGENT_MODEL="$model" \
CLAUDE_CODE_EFFORT_LEVEL="${KIMI_EFFORT:-high}" \
CLAUDE_CODE_AUTO_COMPACT_WINDOW="$ctx" \
CLAUDE_CODE_MAX_CONTEXT_TOKENS="$ctx" \
claude "$@"
}
# Env keys claude-app on writes into ~/.claude/settings.json (env object).
# Kept as one list so `off` deletes exactly what `on` added.
CLAUDE_APP_ENV_KEYS="ANTHROPIC_BASE_URL ANTHROPIC_API_KEY ANTHROPIC_MODEL ANTHROPIC_DEFAULT_OPUS_MODEL ANTHROPIC_DEFAULT_SONNET_MODEL ANTHROPIC_DEFAULT_HAIKU_MODEL CLAUDE_CODE_SUBAGENT_MODEL CLAUDE_CODE_EFFORT_LEVEL CLAUDE_CODE_AUTO_COMPACT_WINDOW CLAUDE_CODE_MAX_CONTEXT_TOKENS"
# GUI apps don't inherit shell env, so the desktop app route writes a
# persistent env block into ~/.claude/settings.json instead. Values are
# passed to node via env vars — never embedded in the script text.
claude_app_on() {
load_key
local settings="$HOME/.claude/settings.json"
local backup="$(dirname "$CONF")/claude-settings.backup.json"
local model="$DEFAULT_MODEL" ctx
ctx="$(context_for_model "$model")"
mkdir -p "$HOME/.claude"
# One-time safety net; never overwrite an existing backup.
if [ -f "$settings" ] && [ ! -f "$backup" ]; then
mkdir -p "$(dirname "$backup")"
cp "$settings" "$backup"
echo "Backed up existing settings to $backup"
fi
KIMI_SETTINGS_PATH="$settings" \
KIMI_EP="$ANTHROPIC_EP" \
KIMI_KEY="$KIMI_API_KEY" \
KIMI_MDL="$model" \
KIMI_EFFORT_LVL="${KIMI_EFFORT:-high}" \
KIMI_CTX="$ctx" \
node --eval '
const fs = require("fs");
const p = process.env.KIMI_SETTINGS_PATH;
let cur = {};
if (fs.existsSync(p)) {
const raw = fs.readFileSync(p, "utf-8").trim();
if (raw) cur = JSON.parse(raw); // throws on malformed JSON -> non-zero, no write
}
const m = process.env.KIMI_MDL, ctx = process.env.KIMI_CTX;
const env = { ...(cur.env || {}) };
env.ANTHROPIC_BASE_URL = process.env.KIMI_EP;
env.ANTHROPIC_API_KEY = process.env.KIMI_KEY;
env.ANTHROPIC_MODEL = m;
env.ANTHROPIC_DEFAULT_OPUS_MODEL = m;
env.ANTHROPIC_DEFAULT_SONNET_MODEL = m;
env.ANTHROPIC_DEFAULT_HAIKU_MODEL = m;
env.CLAUDE_CODE_SUBAGENT_MODEL = m;
env.CLAUDE_CODE_EFFORT_LEVEL = process.env.KIMI_EFFORT_LVL;
env.CLAUDE_CODE_AUTO_COMPACT_WINDOW = ctx;
env.CLAUDE_CODE_MAX_CONTEXT_TOKENS = ctx;
cur.env = env;
fs.writeFileSync(p, JSON.stringify(cur, null, 2) + "\n", "utf-8");
'
KIMI_SETTINGS_PATH="$HOME/.claude.json" \
node --eval '
const fs = require("fs");
const p = process.env.KIMI_SETTINGS_PATH;
let cur = {};
if (fs.existsSync(p)) {
const raw = fs.readFileSync(p, "utf-8").trim();
if (raw) cur = JSON.parse(raw);
}
cur.hasCompletedOnboarding = true;
fs.writeFileSync(p, JSON.stringify(cur, null, 2) + "\n", "utf-8");
'
cat <<EOF
Claude Code desktop app: ON (model $model, context $ctx)
wrote env block in $settings
set hasCompletedOnboarding in $HOME/.claude.json
This affects EVERY Claude Code session (app, CLI, IDE) until you run:
kimi-on claude-app off
Restart the Claude Code app fully to apply.
EOF
}
claude_app_off() {
local settings="$HOME/.claude/settings.json"
local backup="$(dirname "$CONF")/claude-settings.backup.json"
if [ ! -f "$settings" ]; then
echo "No $settings found — nothing to remove."
return
fi
KIMI_SETTINGS_PATH="$settings" KIMI_KEYS="$CLAUDE_APP_ENV_KEYS" \
node --eval '
const fs = require("fs");
const p = process.env.KIMI_SETTINGS_PATH;
const raw = fs.readFileSync(p, "utf-8").trim();
const cur = raw ? JSON.parse(raw) : {}; // throws on malformed JSON -> non-zero, no write
const env = { ...(cur.env || {}) };
for (const k of process.env.KIMI_KEYS.split(" ")) delete env[k];
cur.env = env;
fs.writeFileSync(p, JSON.stringify(cur, null, 2) + "\n", "utf-8");
'
cat <<EOF
Claude Code desktop app: OFF
removed the kimi-on env keys from $settings (your other env keys kept)
one-time backup kept at $backup (manual restore if ever needed)
Restart the Claude Code app fully to apply.
EOF
}
ensure_codex_profile() {
local conf="$HOME/.codex/config.toml"
mkdir -p "$HOME/.codex"
touch "$conf"
if ! grep -q 'model_providers.kimi' "$conf"; then
cat >> "$conf" <<EOF
# --- added by kimi-on (kimi-everywhere) ---
[model_providers.kimi]
name = "Kimi (subscription)"
base_url = "$OPENAI_EP"
env_key = "KIMI_API_KEY"
wire_api = "chat"
[profiles.kimi]
model_provider = "kimi"
model = "k3"
# --- end kimi-on ---
EOF
echo "Added 'kimi' provider + profile to $conf" >&2
fi
}
# The Codex desktop app shares ~/.codex/config.toml with the CLI, but as a
# GUI app it can't see shell env vars — launchctl setenv puts KIMI_API_KEY
# into the user session so the app's env_key lookup resolves (until logout).
codex_app_on() {
load_key
ensure_codex_profile
if [ "$(uname)" = "Darwin" ]; then
launchctl setenv KIMI_API_KEY "$KIMI_API_KEY"
echo "Set KIMI_API_KEY for GUI apps via launchctl (clears at logout)."
else
echo "Note: the Codex desktop app path is macOS-only for now."
echo "The CLI (kimi-on codex) is unaffected."
fi
cat <<'EOF'
Codex desktop app: ON
Next steps:
1. Fully quit the Codex app (Cmd+Q, not just close the window).
2. Reopen it.
3. Open the model picker / settings and select the custom "kimi"
provider/profile if offered (it may appear as a "Custom" entry).
Notes:
- launchctl setenv clears at logout — rerun `kimi-on codex-app on` after a reboot.
- This is the simple key-based route. For a native model picker + Kimi
OAuth instead, see https://github.com/duolahypercho/codex-router
EOF
}
codex_app_off() {
if [ "$(uname)" = "Darwin" ]; then
launchctl unsetenv KIMI_API_KEY
echo "Removed KIMI_API_KEY from the GUI session (launchctl unsetenv)."
fi
cat <<'EOF'
Codex desktop app: OFF
The 'kimi' provider + profile block stays in ~/.codex/config.toml — harmless,
the CLI still uses it. To remove it manually, delete the lines between
# --- added by kimi-on (kimi-everywhere) ---
# --- end kimi-on ---
Restart the Codex app to apply.
EOF
}
ensure_opencode_config() {
[ -f opencode.json ] && return
cat > opencode.json <<EOF
{
"\$schema": "https://opencode.ai/config.json",
"provider": {
"kimi": {
"npm": "@ai-sdk/openai-compatible",
"name": "Kimi (subscription)",
"options": {
"baseURL": "$OPENAI_EP",
"apiKey": "{env:KIMI_API_KEY}"
},
"models": {
"k3": { "name": "Kimi K3" },
"kimi-for-coding": { "name": "Kimi K2.7 Code" }
}
}
},
"model": "kimi/k3"
}
EOF
echo "Wrote project opencode.json (Kimi provider, model kimi/k3)" >&2
}
print_env() {
case "${1:-}" in
anthropic)
echo "export ANTHROPIC_BASE_URL=$ANTHROPIC_EP"
echo "export ANTHROPIC_API_KEY=$KIMI_API_KEY"
echo "export ANTHROPIC_MODEL=$DEFAULT_MODEL"
;;
openai)
echo "export OPENAI_BASE_URL=$OPENAI_EP"
echo "export OPENAI_API_BASE=$OPENAI_EP"
echo "export OPENAI_API_KEY=$KIMI_API_KEY"
;;
*)
echo "Usage: kimi-on env anthropic|openai" >&2
exit 1
;;
esac
}
cmd="${1:-}"
[ -n "$cmd" ] || { usage; exit 0; }
shift || true
case "$cmd" in
-h|--help|help) usage ;;
setup) load_key; echo "Key ready. Try: kimi-on claude" ;;
claude)
if [ "${1:-}" = "--oauth" ]; then
shift
run_claude_oauth "$@"
exit 0
fi
load_key
if [ "${1:-}" = "--init" ]; then shift; claude_init; fi
run_claude "$@"
;;
oauth)
case "${1:-}" in
start) oauth_start ;;
stop) oauth_stop ;;
status) oauth_status ;;
*) echo "Usage: kimi-on oauth start|stop|status" >&2; exit 1 ;;
esac
;;
claude-app)
case "${1:-}" in
on) claude_app_on ;;
off) claude_app_off ;;
*) echo "Usage: kimi-on claude-app on|off" >&2; exit 1 ;;
esac
;;
codex)
load_key
ensure_codex_profile
codex --profile kimi "$@"
;;
codex-app)
case "${1:-}" in
on) codex_app_on ;;
off) codex_app_off ;;
*) echo "Usage: kimi-on codex-app on|off" >&2; exit 1 ;;
esac
;;
opencode)
load_key
ensure_opencode_config
opencode "$@"
;;
aider)
load_key
OPENAI_API_BASE="$OPENAI_EP" OPENAI_API_KEY="$KIMI_API_KEY" \
aider --model "openai/$DEFAULT_MODEL" "$@"
;;
env)
load_key
print_env "${1:-}"
;;
*)
# Universal fallback: inject both protocols and run the command as-is.
load_key
command -v "$cmd" >/dev/null 2>&1 || { echo "Unknown command: $cmd" >&2; usage; exit 1; }
ANTHROPIC_BASE_URL="$ANTHROPIC_EP" \
ANTHROPIC_API_KEY="$KIMI_API_KEY" \
ANTHROPIC_MODEL="$DEFAULT_MODEL" \
OPENAI_BASE_URL="$OPENAI_EP" \
OPENAI_API_BASE="$OPENAI_EP" \
OPENAI_API_KEY="$KIMI_API_KEY" \
"$cmd" "$@"
;;
esac
name kimi-router
description Split-brain coding mode — the session model (e.g. Claude Fable) acts as orchestrator only, and ALL code writing is delegated to a Kimi worker running headless Claude Code against Moonshot's Anthropic-compatible API. Use when the user invokes /kimi-router or asks to route coding work to Kimi.

Kimi Router

You are now the orchestrator. Kimi is the executor. This mode stays on for the rest of the session.

The one rule

You never write or edit code. Not one line, not a "quick fix". Every code change — new files, edits, refactors, config, tests — is delegated to a Kimi worker. Your job is direction: understand the task, write the brief, review the result.

You MAY still directly: read files, search, run tests/builds, run git commands, answer questions, and edit non-code documents the user asks you for (e.g. this brief, notes).

Setup (once)

The worker needs these env vars (put them in ~/.kimi/env or similar, never commit them):

export KIMI_API_KEY="sk-..."                                # Moonshot platform key
export KIMI_BASE_URL="https://api.moonshot.ai/anthropic"    # Anthropic-compatible endpoint
export KIMI_MODEL="kimi-k3"                                 # exact model name from Moonshot docs

If KIMI_API_KEY is not set in the environment, try source ~/.kimi/env first; if still missing, stop and ask the user for the key.

The loop (per coding task)

  1. Understand — read the relevant files and any AGENTS.md/CLAUDE.md yourself. Decide what should change and where.

  2. Brief — write a self-contained task brief. The worker has zero conversation context, so the brief must include:

    • Goal in one sentence, then exact requirements.
    • Absolute paths of files to change (and files to read for context/conventions).
    • Constraints (style, patterns to follow, what NOT to touch).
    • Acceptance criteria (what must pass/work when done).
  3. Delegate — spawn one worker (serial, never parallel):

    env ANTHROPIC_BASE_URL="$KIMI_BASE_URL" \
        ANTHROPIC_AUTH_TOKEN="$KIMI_API_KEY" \
        ANTHROPIC_MODEL="$KIMI_MODEL" \
        ANTHROPIC_SMALL_FAST_MODEL="$KIMI_MODEL" \
        claude -p "<brief>" \
        --model "$KIMI_MODEL" \
        --permission-mode acceptEdits \
        --settings '{"disableAllHooks": true}' \
        --output-format stream-json --verbose \
        --add-dir <project-root>

    --model is required: without it, headless Claude Code uses the user's saved default model (an Anthropic model), which the Kimi endpoint rejects. The env vars alone are not enough.

    --settings '{"disableAllHooks": true}' stops the user's global hooks (e.g. an autogit Stop hook) from firing inside the worker — otherwise the worker's changes can get auto-committed and even auto-pushed with the brief as the commit message before the orchestrator has reviewed them. The orchestrator owns git; workers must not commit. (Trade-off: guardrail hooks are also skipped, so never combine this with a broad Bash allowlist.)

    --output-format stream-json --verbose makes the worker emit one JSON line per step (tool calls, edits, messages) instead of staying silent until the end. Run the worker in the background, poll its output file every ~30s, and report progress to the user in plain English (e.g. "Kimi is editing index.js now"). The final result JSON line is the worker's report.

    Run it from the project root. For anything non-trivial, run it in the background (it can take minutes) and poll the output instead of blocking.

  4. Review — after the worker exits:

    • Read the full diff (git diff).
    • Check it against the acceptance criteria.
    • Run the relevant tests/build.
  5. Iterate — if something is wrong or missing, do NOT fix it yourself. Write a new fix-it brief (quote the failing test output / the specific defect and file:line) and spawn another worker. Repeat until the acceptance criteria pass.

  6. Report — tell the user what Kimi changed, what you verified, and anything still open.

Brief-writing tips

  • One task per worker. Split big features into sequential briefs; each builds on the reviewed result of the last.
  • Over-specify. Vague briefs produce vague code. Name functions, name files, paste relevant snippets.
  • Tell the worker to follow existing patterns in named files rather than inventing new abstractions.
  • Tell the worker its stdout is a report, not a chat: end with a summary of files changed.

Failure handling

  • Worker returns an API/auth error → surface it to the user; it's a setup problem, not a coding problem.
  • Worker produces wrong code → fix-it brief (step 5). There is no "orchestrator takes over" fallback.
{
"$schema": "https://opencode.ai/config.json",
"provider": {
"kimi": {
"npm": "@ai-sdk/openai-compatible",
"name": "Kimi (subscription)",
"options": {
"baseURL": "https://api.kimi.com/coding/v1",
"apiKey": "{env:KIMI_API_KEY}"
},
"models": {
"k3": { "name": "Kimi K3" },
"kimi-for-coding": { "name": "Kimi K2.7 Code" }
}
}
},
"model": "kimi/k3"
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment