Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

kimi-oauth-proxy

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.

Build

go build -o kimi-oauth-proxy ./cmd/kimi-oauth-proxy

Run

./kimi-oauth-proxy            # listens on 127.0.0.1:8463
./kimi-oauth-proxy -port 9000

Then point your client at http://127.0.0.1:8463/v1/... — any Authorization header you send is replaced by the proxy.

Client configuration

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
Loading

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 CLI

# ~/.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 Code

// ~/.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.

OpenCode

// ~/.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 }
        }
      }
    }
  }
}

crush

// ~/.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 matrix

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.

/v1/responses shim (Codex CLI)

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.

/v1/models translation (Codex CLI)

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_lengthcontext_window/max_context_window, think_effortssupported_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.

Codex client pin

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.go

and 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:

  1. pkg/models/prompt.md — re-copy from the new codex source.
  2. pinnedCodexVersion in pkg/models/base_instructions.go — bump to the new codex version.
  3. The byte count (20903) and sha256 in base_instructions.go and models_translator_test.go — update if the prompt changed.
  4. models_handler_test.go — references pinnedCodexVersion (no hardcoded version), but re-check its expectations on a bump.
  5. This README's version and byte mentions — keep in sync.
  6. ModelInfo fields vs protocol/src/openai_models.rs — re-check.
  7. 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.

Forced token rotation

./kimi-oauth-proxy -refresh-now

Performs one refresh under the cross-process lock, prints the resulting expiry, and exits. Useful as a health check.

Trust boundary

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.

Reference implementation

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.

Environment variables

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)

systemd user service

# ~/.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.target
systemctl --user daemon-reload
systemctl --user enable --now kimi-oauth-proxy

Behavior notes

  • 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_token is unchanged, the token is tombstoned on disk and requests fail with 401 JSON telling you to run kimi login.
  • Authorization is replaced with the fresh bearer; X-Api-Key is deleted outright (the upstream validates it with priority over Authorization and would 401 on a dummy client key).
  • Responses stream verbatim (SSE works); only ReadHeaderTimeout is set on the server, since any WriteTimeout would 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.

About

Lets Codex CLI, Claude Code, OpenCode and crush use the Kimi Code API through the kimi CLI's OAuth login

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages