Skip to content

Commit 88ff338

Browse files
authored
feat: add Anthropic Messages API proxy (claude-code compatible) (#1)
* refactor(translate): Phase 1 — encapsulate per-request state, extract helpers Fix concurrency bug: module-level toolCallIndex and _messageId were shared across concurrent requests. Replace toOpenAIStreamChunk / toOpenAIErrorChunk with a per-request OpenAIStreamEncoder class. Remove getMessageId/resetMessageId globals from util.ts. Extract applyNoToolsSafeguard for reuse by the upcoming Anthropic translator. Deduplicate OPENAI_FINISH_MAP (was declared twice). * feat: Phase 2 — Anthropic Messages API proxy Add full Anthropic API surface alongside the existing OpenAI-compatible endpoint: - POST /v1/messages (streaming + non-streaming) - POST /v1/messages/count_tokens (CJK-weighted heuristic estimate) - GET /v1/models (content-negotiated via anthropic-version header) Translator (src/translate/anthropic.ts): - toCCRequest: Anthropic → CC request (messages, tools, tool_choice, thinking, system, images, tool_result with name lookup) - AnthropicStreamEncoder: CC events → Anthropic SSE (message_start, content_block_*, signature_delta, ping, message_delta, message_stop, error) - buildAnthropicResponse: non-streaming Anthropic response assembly Supporting modules: - anthropic-types.ts: discriminated-union types (no any) - anthropic-models.ts: env-based claude-* → CC model mapping - validation.ts: validateAnthropicRequest (rejects unsupported blocks/tools) - stream.ts: formatAnthropicSSE for event-typed SSE - server.ts: handleMessages, handleCountTokens, sendAnthropicError, format-aware handleUpstreamError(err, format) 122 tests pass, lint clean, build clean. * refactor(translate): replace env-var model mapping with config-file glob matching * feat(setup): add --setup-claude-code CLI for config file + env instructions * feat(config): load anthropic-models.json at startup, add --setup-claude-code handler * test(setup): add --setup-claude-code tests (create, existing, force) * docs: replace env-var model mapping with config-file + --setup-claude-code * test(e2e): add Anthropic endpoint e2e tests - /v1/messages (non-streaming + streaming) - /v1/messages/count_tokens - /v1/models content-negotiation (Anthropic vs OpenAI shape) - Anthropic error shapes (400 invalid_request_error) * feat(setup): --setup-claude-code auto-appends env vars to shell RC Instead of printing instructions, detect shell (zsh/bash/fish) and append ANTHROPIC_BASE_URL and ANTHROPIC_API_KEY=proxy-managed to the user's RC file. Detect already-set vars to avoid duplicates. * feat(setup): --setup-claude-code creates dedicated settings file - Write model config to ~/.config/commandcode-api-proxy/anthropic-models.json - Write Claude Code settings to ~/.config/commandcode-api-proxy/claude-settings.json (does NOT touch ~/.claude/settings.json) - Print usage: claude --settings <path> or alias shortcut * fix(setup): add model and skipDangerousModePermissionPrompt to settings * refactor: drop glob mapping, proxy pass-through model ID - Remove anthropic-models.json glob config (no longer needed) - --setup-claude-code only writes claude-settings.json with model env vars (ANTHROPIC_DEFAULT_SONNET/OPUS/HAIKU_MODEL) - resolveAnthropicModel uses ANTHROPIC_DEFAULT_MODEL env or fallback - Proxy pass-through model ID, Claude Code maps via its settings * fix: add system role support and full Claude Code env vars * fix(setup): ANTHROPIC_AUTH_TOKEN = proxy-managed, DISABLE_NONESSENTIAL as number * fix: clean up unused imports, 404 error shape, remove fragile JSON merge * chore: cleanup docs folder and finalize Anthropic feature
1 parent 2841d18 commit 88ff338

22 files changed

Lines changed: 2494 additions & 310 deletions

.env.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,6 @@ CC_API_KEY=
44
# Proxy config
55
HOST=127.0.0.1
66
PORT=8787
7+
8+
# Anthropic model mapping (optional — prefer --setup-claude-code)
9+
# ANTHROPIC_DEFAULT_MODEL=deepseek/deepseek-v4-pro

DEVELOPMENT.md

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -46,19 +46,27 @@ src/
4646
├── stream.ts # NDJSON parser & SSE formatter
4747
├── upstream.ts # CC API client
4848
├── setup/
49+
│ ├── opencode.ts # opencode.json bootstrap helper
50+
│ └── claude-code.ts # claude-code config + model mapping setup
51+
├── translate/
52+
│ ├── types.ts # Shared types (OpenAI, CC, UsageData)
53+
│ ├── models.ts # Model resolution & aliasing
54+
│ ├── util.ts # CC helpers (usage, tool pruning, safeguard)
55+
│ ├── validation.ts # Request validation (OpenAI + Anthropic)
56+
│ ├── openai.ts # OpenAI ↔ CC translation
57+
│ ├── anthropic-types.ts # Anthropic API types
58+
│ ├── anthropic-models.ts # Env-based Anthropic model mapping
59+
│ └── anthropic.ts # Anthropic ↔ CC translation
60+
setup/
4961
│ └── opencode.ts # opencode.json bootstrap helper
50-
└── translate/
51-
├── types.ts # Shared types
52-
├── models.ts # Model resolution & aliasing
53-
├── util.ts # CC helpers (messageId, usage, tool pruning)
54-
├── validation.ts # Request validation
55-
└── openai.ts # OpenAI ↔ CC translation
5662
tests/
5763
├── auth.test.ts # Auth module tests
5864
├── config.test.ts # Config loader tests
5965
├── server.test.ts # HTTP server tests
6066
├── stream.test.ts # NDJSON parser & SSE tests
61-
├── translate.test.ts # Translation layer tests
67+
├── translate.test.ts # OpenAI translation layer tests
68+
├── translate-anthropic.test.ts # Anthropic translation tests
69+
├── anthropic-models.test.ts # Model mapping tests
6270
├── upstream.test.ts # Upstream client tests
6371
├── openai-schema.test.ts # ModelMessage[] schema conformance & usage
6472
└── e2e.test.ts # End-to-end integration tests

README.md

Lines changed: 149 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -56,12 +56,13 @@ Get your API key from https://commandcode.ai/settings.
5656

5757
### CLI options
5858

59-
| Option | Description | Default |
60-
| ----------- | -------------------- | ----------- |
61-
| `--host` | Bind address | `127.0.0.1` |
62-
| `--port` | Port | `8787` |
63-
| `--api-key` | Command Code API key ||
64-
| `--setup-opencode` | Generate OpenCode provider config ||
59+
| Option | Description | Default |
60+
| --------------------- | --------------------------------- | ----------- |
61+
| `--host` | Bind address | `127.0.0.1` |
62+
| `--port` | Port | `8787` |
63+
| `--api-key` | Command Code API key ||
64+
| `--setup-opencode` | Generate OpenCode provider config ||
65+
| `--setup-claude-code` | Generate Claude Code model config ||
6566

6667
## Endpoints
6768

@@ -78,6 +79,71 @@ curl http://127.0.0.1:8787/v1/chat/completions \
7879
}'
7980
```
8081

82+
### `POST /v1/messages` (Anthropic)
83+
84+
```bash
85+
curl http://127.0.0.1:8787/v1/messages \
86+
-H "x-api-key: proxy-managed" \
87+
-H "Content-Type: application/json" \
88+
-d '{
89+
"model": "claude-sonnet-4-5-20250929",
90+
"max_tokens": 4096,
91+
"messages": [{"role": "user", "content": "Hello!"}],
92+
"stream": true
93+
}'
94+
```
95+
96+
### `POST /v1/messages/count_tokens` (Anthropic)
97+
98+
```bash
99+
curl http://127.0.0.1:8787/v1/messages/count_tokens \
100+
-H "x-api-key: proxy-managed" \
101+
-H "Content-Type: application/json" \
102+
-d '{
103+
"messages": [{"role": "user", "content": "Hello!"}]
104+
}'
105+
```
106+
107+
### Anthropic model mapping
108+
109+
Anthropic clients send Claude model IDs (e.g., `claude-sonnet-4-5-20250929`).
110+
The proxy maps them to CC models via a config file with glob wildcards:
111+
112+
```bash
113+
npx commandcode-api-proxy --setup-claude-code
114+
```
115+
116+
This writes `~/.config/commandcode-api-proxy/anthropic-models.json`:
117+
118+
```json
119+
{
120+
"default": "deepseek/deepseek-v4-pro",
121+
"mappings": {
122+
"claude-sonnet-*": "deepseek/deepseek-v4-pro",
123+
"claude-opus-*": "deepseek/deepseek-v4-pro",
124+
"claude-haiku-*": "deepseek/deepseek-v4-flash"
125+
}
126+
}
127+
```
128+
129+
**Glob matching:** `*` matches any characters. First matching pattern wins —
130+
put specific patterns before general ones (e.g. `claude-sonnet-*` before
131+
`claude-*`). Edit the file to customize mappings, then restart the proxy.
132+
133+
Non-Claude model IDs pass through unchanged.
134+
135+
**Optional override:** Set `ANTHROPIC_DEFAULT_MODEL` env var to override the
136+
config file's `default` field without editing the file.
137+
138+
### Anthropic limitations
139+
140+
- **Thinking signatures**: placeholder only (not cryptographically signed).
141+
- **Cache control**: stripped (no CC analogue).
142+
- **Count tokens**: heuristic estimate, not exact.
143+
- **Built-in/server tools** (`computer_`, `bash_`, `web_search_`, etc.):
144+
rejected with `invalid_request_error`. Only custom tools supported.
145+
- **`metadata`, `top_k`, `top_p`, `service_tier`**: accepted but dropped.
146+
81147
## Client configuration
82148

83149
### OpenCode
@@ -102,19 +168,37 @@ Or add manually:
102168
"apiKey": "proxy-managed"
103169
},
104170
"models": {
105-
"deepseek-v4-pro": { "name": "DeepSeek V4 Pro", "limit": { "context": 1048576, "output": 393216 } },
106-
"deepseek-v4-flash": { "name": "DeepSeek V4 Flash", "limit": { "context": 1048576, "output": 393216 } },
171+
"deepseek-v4-pro": {
172+
"name": "DeepSeek V4 Pro",
173+
"limit": { "context": 1048576, "output": 393216 }
174+
},
175+
"deepseek-v4-flash": {
176+
"name": "DeepSeek V4 Flash",
177+
"limit": { "context": 1048576, "output": 393216 }
178+
},
107179
"MiniMax-M2.7": { "name": "MiniMax M2.7", "limit": { "context": 204800, "output": 32768 } },
108180
"MiniMax-M2.5": { "name": "MiniMax M2.5", "limit": { "context": 204800, "output": 32768 } },
109181
"GLM-5.1": { "name": "GLM-5.1", "limit": { "context": 200000, "output": 131072 } },
110182
"GLM-5": { "name": "GLM-5", "limit": { "context": 200000, "output": 131072 } },
111183
"Kimi-K2.6": { "name": "Kimi K2.6", "limit": { "context": 262144, "output": 98304 } },
112184
"Kimi-K2.5": { "name": "Kimi K2.5", "limit": { "context": 262144, "output": 98304 } },
113-
"Qwen3.6-Max-Preview": { "name": "Qwen 3.6 Max Preview", "limit": { "context": 262144, "output": 65536 } },
114-
"Qwen3.6-Plus": { "name": "Qwen 3.6 Plus", "limit": { "context": 1048576, "output": 65536 } },
185+
"Qwen3.6-Max-Preview": {
186+
"name": "Qwen 3.6 Max Preview",
187+
"limit": { "context": 262144, "output": 65536 }
188+
},
189+
"Qwen3.6-Plus": {
190+
"name": "Qwen 3.6 Plus",
191+
"limit": { "context": 1048576, "output": 65536 }
192+
},
115193
"Qwen3.7-Max": { "name": "Qwen 3.7 Max", "limit": { "context": 1048576, "output": 65536 } },
116-
"Qwen3.7-Plus": { "name": "Qwen 3.7 Plus", "limit": { "context": 1048576, "output": 65536 } },
117-
"Step-3.5-Flash": { "name": "Step 3.5 Flash", "limit": { "context": 262144, "output": 65536 } },
194+
"Qwen3.7-Plus": {
195+
"name": "Qwen 3.7 Plus",
196+
"limit": { "context": 1048576, "output": 65536 }
197+
},
198+
"Step-3.5-Flash": {
199+
"name": "Step 3.5 Flash",
200+
"limit": { "context": 262144, "output": 65536 }
201+
},
118202
"mimo-v2.5": { "name": "MiMo V2.5", "limit": { "context": 1048576, "output": 131072 } }
119203
}
120204
}
@@ -154,20 +238,63 @@ response = client.chat.completions.create(
154238
}
155239
```
156240

241+
### Claude Code
242+
243+
Run the proxy's setup to generate model config and Claude Code settings:
244+
245+
```bash
246+
npx commandcode-api-proxy --setup-claude-code
247+
```
248+
249+
This creates:
250+
251+
1. Model mapping config at `~/.config/commandcode-api-proxy/anthropic-models.json`
252+
2. Claude Code settings at `~/.config/commandcode-api-proxy/claude-settings.json`
253+
254+
Then run:
255+
256+
```bash
257+
claude --settings ~/.config/commandcode-api-proxy/claude-settings.json
258+
```
259+
260+
Or set an alias:
261+
262+
```bash
263+
alias claude-proxy="claude --settings ~/.config/commandcode-api-proxy/claude-settings.json"
264+
claude-proxy
265+
```
266+
267+
### Anthropic SDK (Python)
268+
269+
```python
270+
from anthropic import Anthropic
271+
272+
client = Anthropic(
273+
base_url="http://127.0.0.1:8787/v1",
274+
api_key="proxy-managed",
275+
)
276+
277+
message = client.messages.create(
278+
model="claude-sonnet-4-5-20250929",
279+
max_tokens=4096,
280+
messages=[{"role": "user", "content": "Hello!"}],
281+
)
282+
```
283+
157284
## Model aliases
158285

159286
Short names work in addition to full model IDs:
160287

161-
| Alias | Maps to |
162-
| -------------------------------------- | ---------------------------- |
163-
| `deepseek-v4-pro`, `deepseek-v4` | `deepseek/deepseek-v4-pro` |
164-
| `deepseek-v4-flash`, `deepseek-flash` | `deepseek/deepseek-v4-flash` |
165-
| `minimax-m2.7`, `minimax-m2.5` | `MiniMaxAI/MiniMax-*` |
166-
| `glm-5.1`, `glm-5` | `zai-org/GLM-*` |
167-
| `kimi-k2.6`, `kimi-k2.5` | `moonshotai/Kimi-*` |
168-
| `qwen3.6-max`, `qwen3.6-plus` | `Qwen/Qwen3.6-*` |
169-
| `step3.5` | `stepfun/Step-3.5-Flash` |
170-
| `mimo-v2.5` | `xiaomi/mimo-v2.5` |
288+
| Alias | Maps to |
289+
| ------------------------------------- | ---------------------------- |
290+
| `deepseek-v4-pro`, `deepseek-v4` | `deepseek/deepseek-v4-pro` |
291+
| `deepseek-v4-flash`, `deepseek-flash` | `deepseek/deepseek-v4-flash` |
292+
| `minimax-m2.7`, `minimax-m2.5` | `MiniMaxAI/MiniMax-*` |
293+
| `glm-5.1`, `glm-5` | `zai-org/GLM-*` |
294+
| `kimi-k2.6`, `kimi-k2.5` | `moonshotai/Kimi-*` |
295+
| `qwen3.6-max`, `qwen3.6-plus` | `Qwen/Qwen3.6-*` |
296+
| `step3.5` | `stepfun/Step-3.5-Flash` |
297+
| `mimo-v2.5` | `xiaomi/mimo-v2.5` |
171298

172299
Any model ID is passed through as-is — the proxy does not validate against a fixed list.
173300

src/auth.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,5 +77,3 @@ export async function promptForApiKey(): Promise<string> {
7777
stdin.on("data", onData);
7878
});
7979
}
80-
81-

src/proxy.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { loadConfig, fetchLatestCliVersion } from "@/config.js";
44
import { createServer } from "@/server.js";
55
import { saveApiKey, promptForApiKey, readAuthKey, deleteAuth } from "@/auth.js";
66
import { setupOpenCodeConfig } from "@/setup/opencode.js";
7+
import { setupClaudeCodeConfig } from "@/setup/claude-code.js";
78
import { logger, initLogger } from "@/logger.js";
89

910
const args = process.argv.slice(2);
@@ -39,10 +40,18 @@ if (args.includes("--setup-opencode")) {
3940
process.exit(0);
4041
}
4142

43+
if (args.includes("--setup-claude-code")) {
44+
const force = args.includes("--force");
45+
await setupClaudeCodeConfig(force);
46+
process.exit(0);
47+
}
48+
4249
const config = loadConfig();
4350
initLogger(config.logLevel);
4451

45-
logger.info(`API key source: ${process.env.CC_API_KEY ? "env CC_API_KEY" : config.apiKey ? "auth.json" : "none"} (length: ${config.apiKey?.length ?? 0})`);
52+
logger.info(
53+
`API key source: ${process.env.CC_API_KEY ? "env CC_API_KEY" : config.apiKey ? "auth.json" : "none"} (length: ${config.apiKey?.length ?? 0})`,
54+
);
4655

4756
if (!process.env.CC_CLI_VERSION) {
4857
const latest = await fetchLatestCliVersion();
@@ -74,6 +83,8 @@ server.listen(config.port, config.host, () => {
7483
console.log(" GET /health");
7584
console.log(" GET /v1/models");
7685
console.log(" POST /v1/chat/completions (OpenAI format)");
86+
console.log(" POST /v1/messages (Anthropic format)");
87+
console.log(" POST /v1/messages/count_tokens (Anthropic format)");
7788
console.log("");
7889
console.log(" Press Ctrl+C to stop\n");
7990
});

0 commit comments

Comments
 (0)