Skill v1.0.2
currentLLM-judged scan90/1003 files
version: "1.0.2" name: using-ccproxy-api description: >- Guides users through ccproxy as an OpenAI-compatible and Anthropic-compatible LLM API server with SDK integration, OAuth authentication, sentinel key substitution, model routing, and troubleshooting. Use when installing ccproxy, configuring SDK clients (Anthropic, OpenAI, LiteLLM, Agent SDK) against ccproxy, setting up per-project instances, debugging authentication errors, setting up OAuth token forwarding, or understanding the hook pipeline and shaping system.
Using ccproxy as an LLM API Server
ccproxy exposes an OpenAI-compatible and Anthropic-compatible API via a mitmproxy-based interceptor. Any SDK or HTTP client that supports custom base_url can use it.
Installation
System-wide (Home Manager)
Add ccproxy as a flake input and enable the Home Manager module:
# flake.nixinputs.ccproxy.url = "github:starbaser/ccproxy";# home configurationprograms.ccproxy = {enable = true;settings = {# Override defaults here (port, providers, transforms, etc.)};};
This installs the ccproxy binary, generates sibling ccproxy.yaml and LiteLLM-compatible config.yaml files from Nix, and creates a systemd --user service that auto-restarts when either changes.
Standalone (any Linux)
# Clone and enter devShellgit clone https://github.com/starbaser/ccproxycd ccproxynix develop # or: direnv allow# Initialize configccproxy init # copies both templates to ~/.config/ccproxy/ccproxy init --force # overwrites both existing files# Edit native services and model declarations$EDITOR ~/.config/ccproxy/ccproxy.yaml$EDITOR ~/.config/ccproxy/config.yaml# Startccproxy start
Per-project instance
Each project can run its own ccproxy with isolated config, port, and transforms via the flake's mkConfig. Use ccproxy.defaultSettings.settings (top-level, no ${system} selector needed) as the base to inherit all defaults (hooks, shaping, providers, otel).
# project flake.nix{inputs.ccproxy.url = "github:starbaser/ccproxy";inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";inputs.flake-utils.url = "github:numtide/flake-utils";outputs = { self, nixpkgs, flake-utils, ccproxy }:letdefaults = ccproxy.defaultSettings.settings;inflake-utils.lib.eachDefaultSystem (system:letpkgs = nixpkgs.legacyPackages.${system};proxyConfig = ccproxy.lib.${system}.mkConfig {settings = {port = 4010; # per-project: use 4010+ to avoid collisionsinspector = {port = 8090;cert_dir = "./.ccproxy";};lightllm = {transforms = [{ match_path = "/v1/messages"; action = "redirect";dest_provider = "anthropic"; dest_base_url = "https://api.anthropic.com";dest_path = "/v1/messages"; }] ++ defaults.lightllm.transforms;};};};in {devShells.default = pkgs.mkShell {packages = with pkgs; [ccproxy.packages.${system}.defaultjust process-compose];shellHook = proxyConfig.shellHook;};});}
mkConfig generates Nix-store ccproxy.yaml and config.yaml files; its shellHook symlinks both into .ccproxy/ and exports CCPROXY_CONFIG_DIR. The .envrc just needs use flake.
Add .ccproxy/ to .gitignore — the directory contains a Nix-generated symlink that is machine-specific and regenerated on nix develop:
# .gitignore.ccproxy/
Port assignment conventions
| Port | Use | |
|---|---|---|
| 4000 | System-wide ccproxy (Home Manager, default) | |
| 4001 | ccproxy project's own devShell | |
| 4010+ | Per-project instances | |
| 8083 | System inspector UI (default) | |
| 8084 | ccproxy dev inspector | |
| 8090+ | Per-project inspector UI |
Running the instance
# Foregroundccproxy start# Via process-compose (recommended for dev)just up # process-compose up --detachedjust down # process-compose down# Check healthccproxy status # Rich panelccproxy status --json # Machine-readableccproxy status --proxy # Exit 0 if proxy up, 1 if downccproxy status --inspect # Exit 0 if inspector up, 2 if down
process-compose.yml
Use ccproxy status --proxy as the readiness probe so dependent processes wait for the proxy to be healthy:
# process-compose.ymlversion: "0.5"processes:ccproxy:command: "ccproxy start"readiness_probe:exec:command: "ccproxy status --proxy"initial_delay_seconds: 5period_seconds: 30timeout_seconds: 10failure_threshold: 6availability:restart: on_failurebackoff_seconds: 2max_restarts: 5myapp:command: "python -m myapp"depends_on:ccproxy:condition: process_healthy
Wiring SDK clients
Point any SDK at the per-project port with a sentinel key:
import anthropicclient = anthropic.Anthropic(api_key="sk-ant-oat-ccproxy-anthropic",base_url="http://localhost:4010", # per-project port)
Or via environment variables in shellHook / .envrc:
export ANTHROPIC_BASE_URL="http://localhost:4010"export ANTHROPIC_API_KEY="sk-ant-oat-ccproxy-anthropic"
Configuration
Configuration lives under $CCPROXY_CONFIG_DIR (default ~/.config/ccproxy/): ccproxy.yaml owns native services and config.yaml owns LiteLLM-compatible model declarations.
ccproxy:host: 127.0.0.1port: 4000providers:anthropic:auth:type: commandcommand: "jq -r '.claudeAiOauth.accessToken' ~/.claude/.credentials.json"base_url: https://api.anthropic.compath: /v1/messagestype: anthropicgemini:auth:type: commandcommand: "jq -r '.access_token' ~/.gemini/oauth_creds.json"base_url: https://cloudcode-pa.googleapis.compath: "/v1internal:{action}"type: geminihooks:inbound:- ccproxy.hooks.inject_auth- ccproxy.hooks.extract_session_idoutbound:- ccproxy.hooks.inject_mcp_notifications- ccproxy.hooks.verbose_mode- ccproxy.hooks.shapeshaping:enabled: trueshapes_dir: ~/.config/ccproxy/shapesinspector:port: 8083cert_dir: ~/.config/ccproxylightllm:transforms:- match_path: /v1/messagesaction: redirectdest_provider: anthropicdest_base_url: https://api.anthropic.comdest_path: /v1/messages
See reference/routing-and-config.md for transform rules, providers patterns, and hook parameters.
How authentication works
OAuth mode (subscription accounts -- Claude Max, Team, Enterprise):
- Client sends sentinel key
sk-ant-oat-ccproxy-{provider}as API key inject_authhook detects sentinel prefix, looks up real token fromproviders[name].authshapehook replays a captured{provider}.mflowshape: strips configured headers, injectscontent_fieldsfrom the incoming request, runs shape inner-DAG hooks (UUID regeneration, Anthropic billing-header re-signing, cache breakpoint normalization), stamps the result onto the outbound flow- Request reaches provider API with valid OAuth Bearer token and full identity envelope (user-agent, anthropic-beta, x-stainless-*, billing header, system prompt prefix)
API key mode (direct API keys):
- Client sends real API key via
x-api-keyorAuthorizationheader - Key passes through to the provider unchanged
Sentinel key format
sk-ant-oat-ccproxy-{provider}
Where {provider} matches a key in providers config. Common values:
sk-ant-oat-ccproxy-anthropic-- usesproviders.anthropic.authtokensk-ant-oat-ccproxy-gemini-- usesproviders.gemini.authtoken
Default hooks
hooks:inbound:- ccproxy.hooks.inject_auth- ccproxy.hooks.extract_session_idoutbound:- ccproxy.hooks.gemini_cli- ccproxy.hooks.inject_mcp_notifications- ccproxy.hooks.verbose_mode- ccproxy.hooks.shape- ccproxy.hooks.commitbee_compat
inject_auth-- substitutes sentinel key with real token, setsAuthorization: Bearer {token}(or the customauth.header), clears other auth headers, and stamps ccproxy auth metadata for routing/retryextract_session_id-- parsesmetadata.user_idfor MCP notification routinggemini_cli-- wraps Gemini sentinel-key bodies in thev1internalenvelope, conditionally masqueradesgoogle-genai-sdk/*UAs, rewrites paths tocloudcode-pa.googleapis.cominject_mcp_notifications-- injects buffered MCP terminal events as tool_use/tool_result pairsverbose_mode-- stripsredact-thinking-*fromanthropic-betato enable full thinking outputshape-- replays a captured shape ({provider}.mflow) onto the outbound flow, stamping identity headers, billing header, and system prompt prefixcommitbee_compat-- last-mile compatibility shim for the commitbee tool
AuthAddon and GeminiAddon are full mitmproxy addons (not pipeline hooks) registered after the outbound stage: AuthAddon handles 401 detection / refresh / replay; GeminiAddon handles capacity fallback + cloudcode-pa envelope unwrap.
Shape replay -- where identity comes from
ccproxy does not synthesize Claude Code identity headers in code. Anthropic-bound traffic depends on a shape: a real mitmproxy.http.HTTPFlow from the Claude CLI persisted as a .mflow file. ccproxy ships a packaged default shape for Anthropic; a user-captured shape at ~/.config/ccproxy/shapes/anthropic.mflow overrides it. The shape hook replays the shape on every outbound flow, providing user-agent, anthropic-beta, x-stainless-*, the signed x-anthropic-billing-header, and the system prompt prefix.
If the shape in effect is from an outdated Claude CLI release, Anthropic will reject the request with 401/400. Capture (or refresh) a local override with:
ccproxy run --inspect -- claude -p "shape capture"ccproxy shapes save anthropic
See `docs/shaping.md` for the canonical reference (capture workflow, shape inner-DAG hooks, billing salt configuration, custom hooks).
Quick start
# Anthropic SDK (OAuth via sentinel key)import anthropicclient = anthropic.Anthropic(api_key="sk-ant-oat-ccproxy-anthropic",base_url="http://localhost:4000",)# OpenAI SDKfrom openai import OpenAIclient = OpenAI(api_key="sk-ant-oat-ccproxy-anthropic",base_url="http://localhost:4000",)
SDK integration
Anthropic Python SDK
import anthropicclient = anthropic.Anthropic(api_key="sk-ant-oat-ccproxy-anthropic",base_url="http://localhost:4000",)response = client.messages.create(model="claude-sonnet-4-5-20250929",max_tokens=1024,messages=[{"role": "user", "content": "Hello"}],)
No extra headers needed -- the shape hook replays the captured Anthropic shape, supplying anthropic-beta, anthropic-version, the signed billing header, and the system prompt prefix automatically.
Streaming:
with client.messages.stream(model="claude-sonnet-4-5-20250929",max_tokens=1024,messages=[{"role": "user", "content": "Hello"}],) as stream:for text in stream.text_stream:print(text, end="")
OpenAI Python SDK
from openai import OpenAIclient = OpenAI(api_key="sk-ant-oat-ccproxy-anthropic",base_url="http://localhost:4000",)response = client.chat.completions.create(model="claude-sonnet-4-5-20250929",messages=[{"role": "user", "content": "Hello"}],)
Requires a transform rule to rewrite from OpenAI format to the destination provider format via lightllm.
LiteLLM SDK
import asyncio, litellmasync def main():response = await litellm.acompletion(model="claude-sonnet-4-5-20250929",messages=[{"role": "user", "content": "Hello"}],api_base="http://127.0.0.1:4000",api_key="sk-ant-oat-ccproxy-anthropic",)print(response.choices[0].message.content)asyncio.run(main())
Note: litellm.anthropic.messages bypasses proxies. Always use litellm.acompletion().
Claude Agent SDK
import osos.environ["ANTHROPIC_BASE_URL"] = "http://localhost:4000"os.environ["ANTHROPIC_API_KEY"] = "sk-ant-oat-ccproxy-anthropic"from claude_agent_sdk import query, ClaudeAgentOptionsasync for message in query(prompt="Your prompt here",options=ClaudeAgentOptions(allowed_tools=["Read", "Glob"],permission_mode="default",cwd=os.getcwd(),),):# Handle AssistantMessage, ResultMessage, etc.pass
Environment variables (any SDK)
export ANTHROPIC_BASE_URL="http://localhost:4000"export ANTHROPIC_API_KEY="sk-ant-oat-ccproxy-anthropic"# OpenAI compatexport OPENAI_BASE_URL="http://localhost:4000"export OPENAI_API_BASE="http://localhost:4000"
curl (raw HTTP)
curl http://localhost:4000/v1/messages \-H "Content-Type: application/json" \-H "x-api-key: sk-ant-oat-ccproxy-anthropic" \-H "anthropic-version: 2023-06-01" \-d '{"model": "claude-sonnet-4-5-20250929","max_tokens": 100,"messages": [{"role": "user", "content": "Hello"}]}'
Model routing
Normal model routing comes from LiteLLM-compatible model_list declarations in config.yaml. Selection order is: explicit lightllm.transforms override, exact compiled model binding, wildcard compiled binding, then sentinel-key Provider fallback. Transform overrides remain the edge-case escape hatch for host/path/model regex matching. Unmatched reverse proxy flows get a 501 error; unmatched WireGuard flows pass through unchanged.
See reference/routing-and-config.md for transform configuration patterns.
Troubleshooting
Authentication failures are the most common issue. Follow this decision tree:
Error message?│├─ "This credential is only authorized for use with Claude Code"│ ▶ See: Missing or stale captured shape (system prompt prefix not stamped)│├─ "OAuth is not supported" / "invalid x-api-key"│ ▶ See: Missing or stale captured shape (anthropic-beta not stamped)│├─ 401 Unauthorized / token errors│ ▶ See: Token issues│├─ Connection refused / timeout│ ▶ See: Connectivity│└─ Other / unclear▶ See: General diagnostics
See reference/troubleshooting.md for the full diagnostic guide with resolution steps for each branch.
Quick diagnostic commands
ccproxy status # Verify proxy is runningccproxy status --json # Machine-readable status with URLccproxy logs -f # Stream logs in real-timeccproxy logs -n 50 # Last 50 lines
Known limitations (upstream flake issues)
- Shape required for Anthropic — there is no synthetic-identity fallback. If the packaged default (or a user-captured override at
~/.config/ccproxy/shapes/anthropic.mflow) is stale for the current Claude CLI release, requests fail with 401/400. Refresh viaccproxy shapes save anthropic. - `devConfig` overwrites `inspector` atomically — top-level
//merge oninspectordrops sub-keys not re-specified. Deep merge each nested attrset explicitly:defaults.inspector // { ... }. - `supportedSystems` limited — only
x86_64-linuxandaarch64-linux;aarch64-darwinnot supported.
Reference files
- reference/troubleshooting.md -- Full diagnostic decision tree with error-specific resolution steps
- reference/routing-and-config.md -- Model routing, config.yaml patterns, hook pipeline details