feat: add native Anthropic provider#11
Open
torbitmatsudakh wants to merge 98 commits into
Open
Conversation
Lead the README with the main user benefit: using Claude Code with an existing ChatGPT Plus or Pro subscription instead of being pushed toward Codex or Opencode. This removes low-level protocol details from the opening so the first screen explains why someone would use the project before it explains how it works.
actions/checkout, actions/upload-artifact, and actions/download-artifact v4 run on Node.js 20, which GitHub deprecated. Bump to v5 to silence the deprecation warnings in release runs.
Apple Silicon kills unsigned binaries with SIGKILL, so the verification step at the end of the install script failed with "Killed: 9" even after removing the com.apple.quarantine xattr. Bun's compiled binary ships unsigned, and the ubuntu-based release runner can't codesign darwin output. Run `codesign --remove-signature` then `codesign --sign -` on the installed binary so it gets an ad-hoc signature macOS accepts.
This lets proxy users keep portable Claude-oriented model names in settings and skills while still targeting the small set of OpenAI models allowed by the Codex OAuth backend. It fixes cases like cheap subagents requesting haiku or skills pinning sonnet and failing before the request reaches upstream. The proxy now resolves a small explicit alias map before allowlist validation and upstream dispatch, while continuing to report the originally requested model back to Claude Code. The README documents the supported aliases and clarifies that they are convenience shorthands rather than semantic model matches.
GPT-5.4 sometimes emits Read tool calls with an empty string for the optional pages field, and Claude Code rejects that payload before the tool can run. This repairs that compatibility gap in the proxy instead of relying on the model to always satisfy the schema. The reducer now buffers Read tool argument deltas until the tool block is complete, removes pages when it is the empty string, and then emits the sanitized JSON once. Other tools keep their existing streaming behavior, so the fix stays narrow and preserves the normal incremental path outside Read.
The README opened with text only, which made it harder to immediately show what this proxy enables in practice. Add a screenshot near the top so visitors can see Claude Code running through the proxy before they get into setup details. The image is stored as a lossless WebP under meta/ to keep the repo size down without degrading the terminal UI capture. The README references that asset near the intro so it appears prominently on the project page.
Stop sending the Codex CLI originator string on upstream requests and identify this project as claude-codex-proxy instead. This keeps the proxy accurate about what it is rather than pretending to be codex-tui or opencode. Update the README sequence diagram to reflect the actual originator header value so the documented request shape matches the implementation.
Claude Code uses /v1/messages/count_tokens to decide when to compact, so counting the raw Anthropic request could drift from the prompt shape the proxy actually sends upstream. That made the estimator include data such as the rotating billing header even though the request translator strips it before the Codex call. Reuse the existing request-normalization helpers for system text, message content, and tool-result flattening inside the local token counter. This keeps counting aligned with the translation path without changing the proxy's upstream behavior, and should reduce premature compaction caused by systematic mismatch between the two code paths.
We still need to understand why Claude Code compacts earlier than expected through the proxy, and the shared-normalization change alone does not explain whether the remaining drift comes from local counting, translated prompt shape, or upstream usage. Without side-by-side numbers the logs are not enough to tell which layer is responsible. Add opt-in telemetry behind CCP_LOG_COMPACTION so the proxy can log the local /count_tokens result, the translated prompt estimate, and the final upstream usage for the same turn. This keeps normal runs quiet while making it much simpler to compare Claude Code's compaction inputs against what the proxy actually sends and what Codex reports back.
AbortError logs did not identify which in-flight tool request was being canceled, which made the Read failures hard to correlate in proxy logs. Thread reqId and sessionId into the streaming translator and track active tool ids and names while SSE events are in flight. Include that context in upstream stream error and stream translation error logs so canceled Read calls can be matched back to a specific request. This does not change translated responses or tool behavior. It only adds diagnostic context to proxy logging.
The new compaction telemetry does not need its own environment variable, and a separate flag makes the logging behavior harder to discover while we are still actively debugging the proxy. The existing verbose mode is already the place where we opt into noisier request and stream diagnostics. Fold the compaction telemetry into CCP_LOG_VERBOSE so one switch enables the full debugging view. This keeps the runtime configuration simpler and matches how the rest of the proxy's detailed logging is controlled.
Capture per-session request sequencing in verbose compaction logs so we can see which /v1/messages/count_tokens result Claude Code saw before each translated request and upstream completion. This makes it possible to reconstruct the compaction decision boundary from logs instead of inferring it indirectly from isolated samples. The implementation keeps lightweight in-memory snapshots per session ID, recording the latest count_tokens and messages request metadata and linking them through sessionSeq and previous request IDs. This does not change request handling or upstream payloads; it only enriches existing verbose telemetry.
Claude Code decides when to compact from the last response usage plus new-message estimates. The proxy was passing Codex input_tokens through unchanged while also exposing cached_tokens as cache_read_input_tokens, which made Claude Code count cached prompt tokens twice once prompt caching started to hit. Normalize the usage mapping to Anthropic semantics by subtracting cached prompt tokens from input_tokens and reporting them only through cache_read_input_tokens. This keeps downstream context-window calculations aligned with the real prompt size without changing request translation or stream behavior. There are no intended breaking changes beyond lower reported context usage on cached turns. The local /v1/messages/count_tokens path is still only an estimate, but the main auto-compact path should no longer inflate after cache hits.
Claude Code calls /v1/messages/count_tokens for some sizing and warning paths, but the proxy was estimating tokens from the raw Anthropic-shaped request instead of the translated Codex payload. That let the local estimate drift from what the proxy actually sends upstream, especially for structured output requests. Count the translated request shape instead so the estimate includes the same instructions, input items, tools, tool choice, and JSON schema format that the upstream API sees. Also stop treating inline images as raw base64 text, which could wildly overestimate prompt size and trigger premature warnings when image content was present. This is still a local estimate rather than a provider-native count endpoint, so small drift is still possible. The intended behavior change is better alignment between proxy-side token warnings and the real upstream prompt shape.
Claude Code sends max_tokens and then uses the returned assistant usage to decide when to auto-compact. The proxy was dropping that output budget entirely, which let the upstream choose its own longer completion limit and could inflate output_tokens enough to trigger proactive compaction earlier than the direct Anthropic path. Pass the Anthropic max_tokens field through as the upstream max_output_tokens setting, and extend verbose telemetry so one run can show whether remaining drift comes from hidden reasoning tokens or lost context-management behavior. The new logs include requested vs translated output limits, whether context_management was present, raw upstream reasoning token counts, and the mapped Anthropic usage totals Claude Code actually sees. This change does not alter normal response translation beyond preserving Claude Code's requested output budget. The extra telemetry is only emitted in verbose mode and is intended to confirm or rule out the remaining compaction hypotheses without changing user-facing behavior.
We already tracked whether context_management was present, but that was not enough to verify which edits Claude Code was actually sending. Add the exact incoming context_management payload to verbose compaction telemetry so proxy logs can be grepped directly for the request-side context pruning instructions. This keeps normal request handling unchanged and only enriches verbose diagnostics. The goal is to confirm whether Anthropic-side context management is active in the sessions that compact early, which helps separate request-shape drift from missing API-native context pruning behavior.
The ChatGPT Codex responses endpoint rejects the max_output_tokens field, so the recent output-budget passthrough caused normal requests to fail with a 400 unsupported-parameter error. That broke ordinary Claude Code turns immediately instead of just changing compaction behavior. Remove the unsupported field from the translated upstream request and drop the matching telemetry field that depended on it. We still log the incoming Anthropic max_tokens value for debugging, but the proxy no longer tries to enforce it upstream until we know the correct supported parameter for this backend. This restores request compatibility with the current upstream endpoint. The compaction investigation remains open, but the output-budget hypothesis needs a backend-compatible implementation before it can be tested safely.
The proxy was always requesting reasoning.encrypted_content from the upstream responses API, even when the translated request did not enable reasoning. That diverged from Codex client behavior and added unnecessary reasoning-specific request shape to ordinary turns. Only attach the include field when output_config.effort produces a translated reasoning config, and add a focused translator test that locks in both paths. This keeps non-reasoning requests cleaner while preserving encrypted reasoning content when reasoning is actually enabled.
Add verbose compaction telemetry for the upstream OpenAI-Model and X-Reasoning-Included response headers so proxy logs capture the requested model, resolved model, and actual serving model together. This helps investigate compaction drift and usage mismatches by showing when upstream routing differs from the proxy's resolved model and whether the server reports that reasoning is already included in its accounting.
Document the upstream request type as a constrained Codex ResponsesApiRequest shape and add a regression test that locks the translated top-level fields to that allowlist. This reduces request-shape drift after the max_output_tokens regression and makes future upstream field additions more deliberate by requiring source support or an explicit verified change.
Treat X-Reasoning-Included as a presence flag in verbose telemetry so the proxy matches Codex's header handling instead of requiring the literal string value true. This keeps compaction investigation logs accurate when upstream includes the header with any non-empty value and avoids false negatives in the server-side reasoning signal.
Users running Claude Code through the proxy can see premature auto-compaction when Claude Code assumes a smaller local context window than the upstream Codex model actually supports. That behavior is confusing because the backend may still have room while the client decides to compact early. Document the practical workaround of setting DISABLE_AUTO_COMPACT=1 so users can keep full history longer and compact manually with /compact when needed. The note also calls out the tradeoff: disabling proactive compaction can lead to prompt-too-long failures if the session is allowed to grow too far. This is a documentation-only change with no runtime behavior impact.
Rename the project from claude-codex-proxy to claude-code-proxy to reflect that the Codex ChatGPT backend will be one of several supported providers (Kimi is next, per history/2026-04-20-kimi-proxy-research-notes.md). Restructure the codebase around a Provider abstraction so each backend owns its own translation, upstream client, auth, and CLI surface: - Move src/codex/, src/auth/, src/translate/ into src/providers/codex/. - Promote src/translate/sse.ts to src/sse.ts (generic SSE plumbing is shared; translation is not). - Introduce src/providers/types.ts (Provider interface) and src/providers/registry.ts (provider lookup via CCP_PROVIDER env or --provider flag, default codex). - Move handleMessages + handleCountTokens bodies and their telemetry (including session timeline) into src/providers/codex/index.ts. The session timeline tracks codex-specific usage fields, so it belongs in the provider. - server.ts becomes provider-agnostic: it parses requests, tracks a session sequence number, and dispatches to the selected provider. - CLI commands are provider-namespaced: `codex auth login|device|status |logout`. Storage path moves to `~/.config/claude-code-proxy/codex/` and the macOS keychain service to `claude-code-proxy.codex` so each provider keeps its own credentials. Breaking changes: - Previously-stored credentials at the old path are ignored; users must run `claude-code-proxy codex auth login` again. - The log directory moves to `$XDG_STATE_HOME/claude-code-proxy/`. - Homebrew tap and install script URLs change; the GitHub repo itself needs to be renamed (and the homebrew-claude-codex-proxy tap repo) outside this commit.
Adds a new "kimi" provider under src/providers/kimi/ that currently implements only the auth surface. Chat handlers return 501 until the translate layer lands. Auth details (reverse-engineered from kimi-cli 1.37.0): - Device-code grant against https://auth.kimi.com (no PKCE, no scope). - Persistent device_id stored under ~/.config/claude-code-proxy/kimi/; bound into the issued JWT, must be stable across sessions. - Masquerades as kimi-cli via User-Agent and X-Msh-* fingerprint headers — the server appears to use these on both auth and chat endpoints. - Tokens stored in macOS Keychain (or file elsewhere), mirroring the codex provider's token-store shape. - Refresh manager: single-flight in-memory cache, refresh 5 min before expiry (default TTL is 15 min), handles rotating refresh tokens, retries 429/5xx with exponential backoff, clears local state on 401/403. CLI: claude-code-proxy kimi auth login claude-code-proxy kimi auth status claude-code-proxy kimi auth logout
Fills out the translate layer so the kimi provider can serve real
/v1/messages traffic from Claude Code. Confirmed working end-to-end:
device login, token refresh on 401, streaming with thinking blocks
forwarded, tool calls, and per-turn reasoning_effort honoring Claude
Code's /effort setting.
Translate layer (Anthropic Messages <-> Kimi OpenAI-style):
- request.ts: system blocks -> role="system"; splits user messages on
tool_result boundaries into separate role="tool" entries; assistant
text + tool_use blocks collapse to a single assistant message; images
pass through as image_url parts in both user and tool content (Kimi
accepts this — verified against kimi-cli's ReadMediaFile capture).
Sends reasoning_effort (from output_config.effort, default "high")
and thinking:{type:"enabled"} unless the client disables thinking.
- reducer.ts: OpenAI chat.completion.chunk stream -> typed ReducerEvents.
Opens a thinking block on first reasoning_content delta, closes it
when content/tool_calls arrive, streams tool-call arguments as
input_json_delta fragments keyed by upstream tool_calls[].index.
- stream.ts: emits Anthropic SSE events (message_start, thinking_delta,
text_delta, input_json_delta, message_delta with mapped usage).
- accumulate.ts: non-streaming variant producing a full Anthropic Message.
- model-allowlist.ts: every alias (haiku/sonnet/opus/claude-*) collapses
to kimi-for-coding — the only id the /coding/v1/models endpoint
exposes (display_name: Kimi-k2.6).
Client + wiring:
- client.ts: POSTs to /coding/v1/chat/completions with Bearer + X-Msh-*
fingerprint headers, triggers forceRefresh on 401 and retries once,
surfaces 429 with retry-after.
- count-tokens.ts: gpt-tokenizer approximation over the translated
request; good enough for Claude Code's compaction heuristic.
- index.ts: full handleMessages / handleCountTokens, logs
reasoning_effort / thinking / max_tokens for debugging.
- cli.ts: prints ANTHROPIC_MODEL="kimi-for-coding" hint when the
provider is kimi.
…ault Each provider now declares a supportedModels set listing the concrete wire ids it claims (codex: gpt-5.2 / gpt-5.3-codex / gpt-5.4 / gpt-5.4-mini; kimi: kimi-for-coding / kimi-k2.6 / k2.6). On each /v1/messages and /v1/messages/count_tokens request the server looks up body.model in the registry and dispatches to the matching provider. Unknown model ids return HTTP 400 with the full list of supported ids per provider — there is no implicit default, so CCP_PROVIDER is gone. This lets one `serve` process handle both providers at once; users switch upstream purely by changing ANTHROPIC_MODEL. The removed cross-provider aliases (haiku / sonnet / opus / claude-*) were ambiguous and silently collapsed to whichever provider was the current default; they're dropped to force an explicit choice. Startup banner now enumerates the model → provider mapping and prints a hint for ANTHROPIC_SMALL_FAST_MODEL too, since Claude Code sends its hardcoded haiku id for background / title-gen calls and those would also 400 without pointing at a concrete model.
mkdir was called without a mode, leaving directory permissions subject to the process umask. With a default umask of 0022 the result is 0755, making auth.json world-traversable even though the file itself is 0600. On shared Unix hosts this is a meaningful exposure.
maybeRotate closed the stream before attempting rename. If rename threw, the stream was left as undefined and the file still exceeded MAX_LOG_BYTES. Every subsequent log write would re-open the file, re-trigger maybeRotate, close the stream again, and fail the rename again. Fix: rename first, then close the stream; the surrounding catch block suppresses any rename error and leaves the stream intact.
readPtr returned a plain number, but Bun FFI types pointer arguments as
the nominal Pointer type ({ __pointer__: null }). At runtime a pointer is
just a number, so cast through unknown to satisfy the type checker without
changing the runtime behavior.
The project was renamed from consult-llm-mcp to consult-llm.
Both reducers previously silenced JSON.parse errors and continued through the stream. If every upstream event was corrupt the translator would still emit a normal finish event, producing an apparent empty success instead of surfacing the upstream malfunction. Threading a logger through reduceUpstream lets the reducers warn (with a truncated preview) when a chunk fails to parse. This is purely diagnostic; the skip behavior is unchanged.
Both providers cast resp.json() directly to TokenResponse and stored the result. A malformed response with a missing access_token, missing refresh_token, or non-numeric expires_in would be saved without complaint and only manifest later as Bearer undefined upstream calls or as a token whose expiry computes to NaN (refreshed every request). Validate the shape on refresh and on initial login so a bad response fails loudly with a clear message instead of poisoning the token store.
keychainDelete previously discarded the status from SecKeychainItemDelete, so a failed deletion would still report success to callers. A failed logout could leave credentials in the keychain while telling the user everything was cleared. Surface non-success statuses as a thrown KeychainError so callers can see the failure.
Reading the originator header for codex requests and the user-agent header for both codex and kimi from env vars allows users to avoid fingerprinting by upstream providers. The originator header (claude-code-proxy by default) is a unique marker that can be used to identify proxy traffic. - CCP_ORIGINATOR: overrides the originator header sent to codex (default: claude-code-proxy) - CCP_USER_AGENT: overrides the user-agent header for both codex and kimi (default for codex: unset; default for kimi: KimiCLI/<version>)
provider-specific env vars take precedence over the general ones: - CCP_CODEX_ORIGINATOR > CCP_ORIGINATOR > claude-code-proxy - CCP_CODEX_USER_AGENT > CCP_USER_AGENT > unset - CCP_KIMI_USER_AGENT > CCP_USER_AGENT > KimiCLI/<version>
uses BUILD_VERSION compile-time define with "dev" fallback, same pattern as cli.ts.
the git tag is v0.0.7 but the version string should be 0.0.7. the homebrew formula already strips it, but the BUILD_VERSION define used by --version and the default user-agent was including the v.
When the Codex or Kimi upstream returns 429, retry up to 3 times with exponential backoff (2s, 4s, 8s, capped at 30s) before propagating the 429 to the client. Honor the upstream Retry-After header (numeric seconds or HTTP-date), but if it exceeds the 30s budget, give up immediately rather than capping silently. Shared logic lives in src/providers/retry.ts so both providers stay in sync. Sleep is cancellable via the request signal so a client disconnect doesn't keep us waiting. Mid-stream rate-limit errors (UpstreamStreamError in the SSE accumulator) are intentionally left untouched — by then the HTTP response has already started.
Without jitter, concurrent requests hitting the same upstream rate limit would all retry at exactly 2s/4s/8s and recreate the spike. Apply equal jitter (random in [cap/2, cap]) to the exponential fallback only — explicit Retry-After delays are still honored as-is.
Settings can now come from a JSON file at:
macOS: ~/.config/claude-code-proxy/config.json (deliberately not
~/Library — matches where auth tokens live on macOS)
other: ${XDG_CONFIG_HOME:-$HOME/.config}/claude-code-proxy/config.json
Precedence per setting is env var > config file > built-in default. All
existing env-var-only setups keep working unchanged, including the
fallback chains (CCP_CODEX_USER_AGENT > CCP_USER_AGENT > ...).
Implementation notes:
- src/paths.ts centralizes XDG-aware directory resolution. macOS opts
out of XDG_CONFIG_HOME (because tokens have always been at
~/.config/claude-code-proxy on macOS) but still honors XDG_STATE_HOME
for the log dir, preserving the prior log.ts behavior.
- src/config.ts loads config.json once (lazy, cached). A malformed file
warns on stderr and falls back to defaults; per-key type mismatches
warn and skip the bad value without affecting the rest.
- Token stores and the kimi device_id file now resolve via configDir().
To avoid logging existing users out when XDG_CONFIG_HOME is set, both
loadAuth() and getDeviceId() also try the legacy
~/.config/claude-code-proxy/<provider>/... path as a read-only
fallback.
- Empty-string env semantics are preserved exactly: legacy `??` callers
treat "" as a real value; CCP_CODEX_MODEL and CCP_CODEX_EFFORT
continue to treat "" as unset (the long-standing escape hatch).
- KIMI_OAUTH_HOST and KIMI_BASE_URL move from import-time constants to
thin getter functions so config.json values apply at runtime.
README's Configuration section now documents the new file alongside the
env-var table and lists the JSON key for each setting.
shows config file path when present and lists any non-default settings with their source (env or config).
Document the Claude Code environment variable that disables streaming-to-non-streaming fallback when routing through the proxy. The proxy always uses streaming upstream requests, so the fallback path does not avoid upstream streaming and can retry after a partial tool-use stream in a way that duplicates tool calls.
Consistency: all other project-specific env vars use the CCP_ prefix. These were the only two that didn't. No backward compat needed — these are debugging-only overrides unlikely to be set by anyone.
Document the user-facing changes for the pending v0.0.9 release so readers can see the startup config visibility improvements, Kimi env var rename, and Claude Code fallback guidance in one place.
This lets users request Codex service tiers from config or environment without hand-editing requests. The fast tier follows Codex's own behavior by accepting fast as the config value while sending priority to the upstream API. The request translator validates supported service tier values and keeps the wire request constrained to the tiers currently supported by Codex source. References raine#9
This lets users select Codex fast mode per request by using model names such as gpt-5.4-fast, avoiding a proxy restart for service tier changes. Fast aliases are registered for routing, normalized back to the base upstream model, and translated to the same priority service tier used by first-party Codex. Explicit service tier config still takes precedence.
Allow Codex traffic to target a configured Responses endpoint instead of always posting to the built-in chatgpt.com URL. This lets local debugging proxies, load balancers, and managed egress paths receive the same request body and headers without requiring a fork or binary patch. The new codex.baseUrl setting follows the existing config precedence and is reported in the startup override summary when active.
Document the user-facing Codex changes accumulated since v0.0.9 so the release notes cover service tiers, per-request fast model aliases, and the new upstream endpoint override.
…el coverage Adds an "anthropic" provider that forwards claude-* requests directly to api.anthropic.com using the Claude Pro OAuth token Claude Code stores in the macOS keychain. Registered in providers/registry.ts so the model router resolves claude-* names to this provider instead of returning "unknown model" 400s. Supported models cover the full Claude 4 family including legacy names that Claude Code 2.1.x still emits via /model and background tasks: - claude-opus-4-7, claude-opus-4-6 - claude-sonnet-4-6, claude-sonnet-4-5 - claude-haiku-4-5, claude-haiku-4-5-20251001, claude-haiku-4-4 Authentication is read-only against the Claude Code keychain entry — the proxy never stores or refreshes tokens itself; refresh is delegated to Claude Code. Streaming and count_tokens endpoints are passed through without translation since the request/response shape already matches Anthropic's API. This unblocks running gpt-5.5 (codex) and claude-opus-4-7 (anthropic) in the same Claude Code session — pick the right model per task via /model.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds an
anthropicprovider that forwardsclaude-*requests toapi.anthropic.comusing the OAuth token Claude Code already stores in the macOS keychain. The codex and kimi providers are unaffected.Use case: route hard tasks to a real Claude model and lighter tasks to ChatGPT or Kimi inside the same Claude Code session via
/model— without quitting and re-launching. Today, switching between proxy-routed and Anthropic-direct requires exiting Claude Code and starting a fresh non-proxied session.What's included
src/providers/anthropic/index.ts— provider definition with the full Claude 4 model family (Opus 4.7, Sonnet 4.5/4.6, Haiku 4.4/4.5)src/providers/anthropic/client.ts— minimal HTTP client (passthrough — request/response shape already matches the Anthropic API, no format translation needed)src/providers/anthropic/auth/token-store.ts— read-only keychain reader; refresh is delegated to Claude Code itselfsrc/providers/registry.tsto register the providerStreaming and
count_tokenswork via plain passthrough.Notes for review
Claude Code-credentials) read-only and never stores or refreshes tokens itself. If Claude Code isn't installed/signed in, the provider returns 401 with a clear message.SUPPORTED_MODELSincludes both current and older names (e.g.claude-sonnet-4-5) since Claude Code 2.1.x still emits older ids via/modeland background tasks.Scope question
This expands the project beyond "use cheaper alternatives to Anthropic", so totally understand if it doesn't fit the project's vision. I'll keep this on my fork either way — wanted to share since the wiring turned out simpler than expected (~190 lines, mostly passthrough).