This code is LLM-generated.
A loopback HTTP proxy that lets OAuth-only clients (Codex CLI, Claude
Code, plain curl) talk to the Kimi Code API using the kimi CLI's
OAuth login. It listens on 127.0.0.1 only, forwards every request to
https://api.kimi.com/coding, and replaces the Authorization header
with a fresh OAuth access token — refreshing the token through the same
on-disk state (~/.kimi-code/credentials/kimi-code.json) and the same
proper-lockfile-compatible cross-process lock (~/.kimi-code/oauth/ kimi-code.lock) as the TypeScript client, so it never races a refresh
done by the CLI itself.
go build -o kimi-oauth-proxy ./cmd/kimi-oauth-proxy./kimi-oauth-proxy # listens on 127.0.0.1:8463
./kimi-oauth-proxy -port 9000Then point your client at http://127.0.0.1:8463/v1/... — any
Authorization header you send is replaced by the proxy.
flowchart LR
subgraph clients [Clients]
C1[Codex CLI<br>Responses API]
C2[Claude Code<br>Anthropic API]
C3[OpenCode<br>Chat Completions]
C4[crush<br>Chat Completions]
end
P[kimi-oauth-proxy<br>127.0.0.1:8463<br>injects fresh OAuth bearer]
K[api.kimi.com/coding]
F[~/.kimi-code/<br>credentials + refresh lock]
KC[kimi CLI<br>OAuth owner]
C1 -->|POST /v1/responses<br>translated by shim| P
C2 -->|POST /v1/messages<br>verbatim| P
C3 -->|POST /v1/chat/completions<br>verbatim| P
C4 -->|POST /v1/chat/completions<br>verbatim| P
P -->|Authorization: Bearer| K
P <-->|read + refresh under lock| F
KC <-->|shared token file, same lock| F
All four clients share the single kimi CLI login through the proxy; a
placeholder API key is fine everywhere, since the proxy replaces client
credentials upstream (Authorization is overwritten, X-Api-Key is
dropped).
# ~/.codex/config.toml
model = "k3"
model_provider = "kimi"
[model_providers.kimi]
name = "Kimi Code (OAuth via local proxy)"
base_url = "http://127.0.0.1:8463/v1"
wire_api = "responses"Codex speaks only the Responses API, so its traffic goes through the
/v1/responses shim (below), and its model metadata through the
/v1/models translation.
// ~/.claude/settings.json
{
"env": {
"ANTHROPIC_BASE_URL": "http://127.0.0.1:8463",
"ANTHROPIC_API_KEY": "managed-by-kimi-oauth-proxy",
"ANTHROPIC_MODEL": "k3[1m]",
"ANTHROPIC_DEFAULT_FABLE_MODEL": "k3[1m]",
"ANTHROPIC_DEFAULT_OPUS_MODEL": "k3[1m]",
"ANTHROPIC_DEFAULT_SONNET_MODEL": "k3[1m]",
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "k3[1m]",
"CLAUDE_CODE_SUBAGENT_MODEL": "k3[1m]",
"CLAUDE_CODE_AUTO_COMPACT_WINDOW": "1048576",
"CLAUDE_CODE_MAX_CONTEXT_TOKENS": "1048576"
}
}Claude Code talks to the upstream's native Anthropic endpoint verbatim;
k3[1m] selects the 1M-context variant.
// ~/.config/opencode/opencode.json
{
"model": "kimi/k3",
"provider": {
"kimi": {
"npm": "@ai-sdk/openai-compatible",
"name": "Kimi Code (OAuth via local proxy)",
"options": {
"baseURL": "http://127.0.0.1:8463/v1",
"apiKey": "managed-by-kimi-oauth-proxy"
},
"models": {
"k3": {
"name": "Kimi K3",
"limit": { "context": 1048576, "output": 65536 }
}
}
}
}
}// ~/.config/crush/crush.json
{
"providers": {
"kimi": {
"name": "Kimi Code (OAuth via local proxy)",
"type": "openai",
"base_url": "http://127.0.0.1:8463/v1",
"api_key": "managed-by-kimi-oauth-proxy",
"models": [
{
"id": "k3",
"name": "Kimi K3",
"context_window": 1048576,
"default_max_tokens": 65536,
"can_reason": true,
"supports_attachments": true
}
]
}
},
"models": {
"large": { "model": "k3", "provider": "kimi", "max_tokens": 65536 },
"small": { "model": "k3", "provider": "kimi", "max_tokens": 8192 }
}
}| Capability | Codex CLI | Claude Code | OpenCode | crush |
|---|---|---|---|---|
| Transport through the proxy | Responses API shim | Anthropic API, verbatim | Chat Completions, verbatim | Chat Completions, verbatim |
| Text + SSE streaming | ✓ | ✓ | ✓ | ✓ |
| Tool calls (incl. parallel) | ✓ | ✓ native | ✓ | ✓ |
| 1M context (k3) | ✓ via translated metadata | ✓ k3[1m] + env |
✓ declared | ✓ declared |
| Reasoning stream | ✓ mapped to reasoning items | ✓ native thinking blocks | ✓ delivered* | ✓ delivered* |
| Reasoning effort control | ✗ (upstream default max) |
✓ /effort |
✗ | ✗ |
| Image input | ✗ (dropped by shim) | expected ✓* | untested | untested |
| Video input | ✗ | ✗ | ✗ | ✗ |
| Built-in web-search tool | ✗ (dropped by shim) | n/a | n/a | n/a |
* expected/delivered but not yet exercised end-to-end in this
deployment. Reasoning effort on the codex path is intentionally not
forwarded today (supports_reasoning_summaries: false keeps codex from
putting reasoning.effort on the wire, so the upstream applies its own
default max); the shim maps it when that policy changes.
POST /v1/responses is not proxied verbatim: the upstream has no
such route. The shim translates the Responses API request to a streaming
chat completions call and translates the chunks back to Responses SSE
events, so codex-cli (wire_api = "responses") works end to end —
including function calls (namespace tools are flattened to
<namespace>-<name> on the way out and split back on the way in, since
the upstream rejects dots in tool names). Configuration: see
Client configuration.
Codex fetches GET <base>/models?client_version=X and decodes
{"models":[ModelInfo]}; the upstream answers {"data":[...]}, which
fails that decode and leaves codex on fallback metadata
(context_window None). Only requests carrying client_version are
translated — everything else on the mux (/v1/models without the
parameter, and all other routes) relays the upstream verbatim.
The mapping is a fallback-clone: every ModelInfo field codex cannot
live without is copied verbatim from codex's own fallback metadata
(model_info_from_slug), and only what the upstream actually knows is
overridden (slug, display_name, context_length →
context_window/max_context_window, think_efforts →
supported_reasoning_levels/default_reasoning_level, priority =
upstream order index). If the upstream catalog is undecodable or any
entry is invalid, the response relays verbatim — never a partially
fabricated catalog.
The /v1/models translation and the vendored base instructions
(pkg/models/prompt.md, byte-identical copy of codex's compiled-in
system prompt, 20903 bytes) are built against codex-cli 0.144.5
(codex-rs models-manager). On a codex upgrade, refresh with:
cp <codex-src>/models-manager/prompt.md pkg/models/prompt.md
sha256sum pkg/models/prompt.md # update the hash in base_instructions.go and models_translator_test.goand re-check the ModelInfo field set against
protocol/src/openai_models.rs. A client_version that differs from
the pin logs a once-per-process warning at runtime.
Refresh checklist on a codex upgrade:
pkg/models/prompt.md— re-copy from the new codex source.pinnedCodexVersioninpkg/models/base_instructions.go— bump to the new codex version.- The byte count (20903) and sha256 in
base_instructions.goandmodels_translator_test.go— update if the prompt changed. models_handler_test.go— referencespinnedCodexVersion(no hardcoded version), but re-check its expectations on a bump.- This README's version and byte mentions — keep in sync.
- ModelInfo fields vs
protocol/src/openai_models.rs— re-check. grep -rn "<old-version>" .— sweep comment-only mentions of the old version string (provenance comments, test comments) so none drift silently.
Expectation: the stale-pin warning fires on EVERY codex upgrade — that is routine; it only needs action when prompt.md actually changed.
./kimi-oauth-proxy -refresh-nowPerforms one refresh under the cross-process lock, prints the resulting expiry, and exits. Useful as a health check.
This is a single-user, unauthenticated loopback proxy by design: it
binds 127.0.0.1 only and injects your OAuth token into every request
it forwards, so any process that can reach the port acts with your
credentials. That matches the box's convention for local tool proxies
(cf. deepseek_direct on :5000). Never bind it to anything beyond
127.0.0.1.
Token lifecycle, lock protocol and storage format mirror the Kimi Code
CLI 0.27.0 OAuth implementation (source mirror commit
3086e4703992fbbe7a41379405ee243713ad9ced,
packages/oauth/src/). If the CLI changes its refresh protocol or lock
layout, this proxy must be re-checked against the new source.
| Variable | Default | Purpose |
|---|---|---|
KIMI_CODE_HOME |
~/.kimi-code |
Directory holding credentials/ and oauth/ |
KIMI_CODE_OAUTH_HOST |
— | OAuth server override (wins) |
KIMI_OAUTH_HOST |
https://auth.kimi.com |
OAuth server override (fallback) |
KIMI_DISABLE_OAUTH_LOCK |
— | 1 disables the cross-process refresh lock (TS parity) |
# ~/.config/systemd/user/kimi-oauth-proxy.service
[Unit]
Description=Kimi OAuth loopback proxy
After=network-online.target
[Service]
ExecStart=%h/go/src/github.com/xaionaro-go/kimi-oauth-proxy/kimi-oauth-proxy
Restart=on-failure
[Install]
WantedBy=default.targetsystemctl --user daemon-reload
systemctl --user enable --now kimi-oauth-proxy- The token file is re-read on every proxied request; a token inside
the refresh window (
max(300, expires_in/2)seconds left) is refreshed under the lock, and a refresh already done by a peer process is adopted instead of duplicated. - A 401/403 from the refresh endpoint re-reads the file once (peer
rotation race); if the
refresh_tokenis unchanged, the token is tombstoned on disk and requests fail with401JSON telling you to runkimi login. Authorizationis replaced with the fresh bearer;X-Api-Keyis deleted outright (the upstream validates it with priority overAuthorizationand would 401 on a dummy client key).- Responses stream verbatim (SSE works); only
ReadHeaderTimeoutis set on the server, since anyWriteTimeoutwould kill long streams. - Startup logs the build's VCS revision (
build <hash>, with(modified)when built from a dirty tree) so the deployed binary is bound to its source.