Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Command Code API key (if not set, reads from ~/.commandcode/auth.json)
# Command Code API key (if not set, reads from ~/.config/commandcode-api-proxy/auth.json)
CC_API_KEY=

# Proxy config
Expand Down
86 changes: 57 additions & 29 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,14 +73,21 @@ commandcode-api-proxy auth logout

Equivalent env vars (lower priority than CLI flags):

| Env var | Description |
| ---------------- | -------------------------------------------- |
| `HOST` | Bind address |
| `PORT` | Port |
| `CC_API_KEY` | Command Code API key |
| `CC_API_BASE` | Upstream API base URL |
| `CC_CLI_VERSION` | CLI version sent upstream |
| `LOG_LEVEL` | Log level (`info`, `debug`, etc.) |
| Env var | Description |
| ---------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `HOST` | Bind address |
| `PORT` | Port |
| `CC_API_KEY` | Command Code API key |
| `CC_API_BASE` | Upstream API base URL |
| `CC_CLI_VERSION` | CLI version sent upstream |
| `LOG_LEVEL` | Log level (`info`, `debug`, etc.) |
| `CORS_ORIGIN` | `Access-Control-Allow-Origin` value. `*` by default; empty string disables CORS. Restrict before exposing on a network. |

> **Security:** the proxy forwards your paid Command Code key upstream and
> accepts any auth token from clients (`proxy-managed`), so it is designed for
> **localhost** use (`HOST=127.0.0.1`). Do not bind it to `0.0.0.0` on an
> untrusted network without restricting `CORS_ORIGIN` and putting your own auth
> in front.

## Endpoints

Expand Down Expand Up @@ -140,11 +147,11 @@ npx commandcode-api-proxy --setup-claude-code

If a settings file already exists, re-run with `--force` to overwrite it.

| Claude Code tier | Env var | Maps to |
| ---------------- | -------------------------------- | --------------------------- |
| `sonnet` | `ANTHROPIC_DEFAULT_SONNET_MODEL` | `deepseek/deepseek-v4-pro` |
| `opus` | `ANTHROPIC_DEFAULT_OPUS_MODEL` | `deepseek/deepseek-v4-pro` |
| `haiku` | `ANTHROPIC_DEFAULT_HAIKU_MODEL` | `deepseek/deepseek-v4-flash`|
| Claude Code tier | Env var | Maps to |
| ---------------- | -------------------------------- | ---------------------------- |
| `sonnet` | `ANTHROPIC_DEFAULT_SONNET_MODEL` | `deepseek/deepseek-v4-pro` |
| `opus` | `ANTHROPIC_DEFAULT_OPUS_MODEL` | `deepseek/deepseek-v4-pro` |
| `haiku` | `ANTHROPIC_DEFAULT_HAIKU_MODEL` | `deepseek/deepseek-v4-flash` |

Edit those env vars in the settings file to point at other CC models. To force
every `claude-*` request to a single model regardless of tier, set
Expand All @@ -165,25 +172,46 @@ claude-proxy

Short names work in addition to full model IDs:

| Alias | Maps to |
| -------------------------------------------------- | ------------------------------ |
| `deepseek-v4-pro`, `deepseek-v4`, `deepseek-pro` | `deepseek/deepseek-v4-pro` |
| `deepseek-v4-flash`, `deepseek-flash` | `deepseek/deepseek-v4-flash` |
| `kimi-k2.6`, `kimi2.6` | `moonshotai/Kimi-K2.6` |
| `kimi-k2.5`, `kimi2.5` | `moonshotai/Kimi-K2.5` |
| `glm-5.1` | `zai-org/GLM-5.1` |
| `glm-5` | `zai-org/GLM-5` |
| `minimax-m2.7`, `minimax2.7` | `MiniMaxAI/MiniMax-M2.7` |
| `minimax-m2.5`, `minimax2.5` | `MiniMaxAI/MiniMax-M2.5` |
| `qwen3.6-max`, `qwen-3.6-max` | `Qwen/Qwen3.6-Max-Preview` |
| `qwen3.6-plus`, `qwen-3.6-plus` | `Qwen/Qwen3.6-Plus` |
| `qwen3.7-max`, `qwen-3.7-max` | `Qwen/Qwen3.7-Max` |
| `qwen3.7-plus`, `qwen-3.7-plus` | `Qwen/Qwen3.7-Plus` |
| `step3.5`, `step-3.5-flash` | `stepfun/Step-3.5-Flash` |
| `mimo-v2.5`, `mimo2.5` | `xiaomi/mimo-v2.5` |
| Alias | Maps to |
| ------------------------------------------------ | ------------------------------------- |
| `deepseek-v4-pro`, `deepseek-v4`, `deepseek-pro` | `deepseek/deepseek-v4-pro` |
| `deepseek-v4-flash`, `deepseek-flash` | `deepseek/deepseek-v4-flash` |
| `glm-5.2`, `glm5.2` | `zai-org/GLM-5.2` |
| `glm-5.2-fast`, `glm5.2-fast` | `zai-org/GLM-5.2-Fast` |
| `glm-5.1` | `zai-org/GLM-5.1` |
| `glm-5` | `zai-org/GLM-5` |
| `minimax-m3`, `minimax3` | `MiniMaxAI/MiniMax-M3` |
| `minimax-m2.7`, `minimax2.7` | `MiniMaxAI/MiniMax-M2.7` |
| `minimax-m2.5`, `minimax2.5` | `MiniMaxAI/MiniMax-M2.5` |
| `kimi-k3`, `kimi3` | `moonshotai/Kimi-K3` |
| `kimi-k2.7-code`, `kimi-code` | `moonshotai/Kimi-K2.7-Code` |
| `kimi-k2.7-highspeed`, `kimi-highspeed` | `moonshotai/Kimi-K2.7-Code-Highspeed` |
| `kimi-k2.6`, `kimi2.6` | `moonshotai/Kimi-K2.6` |
| `kimi-k2.5`, `kimi2.5` | `moonshotai/Kimi-K2.5` |
| `qwen3.7-max`, `qwen-3.7-max` | `Qwen/Qwen3.7-Max` |
| `qwen3.7-plus`, `qwen-3.7-plus` | `Qwen/Qwen3.7-Plus` |
| `qwen3.6-max`, `qwen-3.6-max` | `Qwen/Qwen3.6-Max-Preview` |
| `qwen3.6-plus`, `qwen-3.6-plus` | `Qwen/Qwen3.6-Plus` |
| `step-3.7-flash`, `step3.7` | `stepfun/Step-3.7-Flash` |
| `step3.5`, `step-3.5-flash` | `stepfun/Step-3.5-Flash` |
| `mimo-v2.5-pro`, `mimo-pro` | `xiaomi/mimo-v2.5-pro` |
| `mimo-v2.5`, `mimo2.5` | `xiaomi/mimo-v2.5` |
| `grok-4.5`, `grok4.5` | `xai/grok-4.5` |
| `nemotron`, `nemotron-3-ultra` | `nvidia/nemotron-3-ultra-550b-a55b` |
| `inkling` | `thinkingmachines/inkling` |
| `hy3` | `tencent/Hy3` |

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

### Reasoning effort

Some models support a `reasoning_effort` (`low` | `medium` | `high` | `xhigh` | `max`), and
each accepts a different subset. The proxy sends the closest valid level for the model
(e.g. `deepseek-v4-pro` supports only `high`/`max`, so `low` is clipped up to `high` and
`max` is reachable). Models without a discrete effort set ignore the value. Pass the
effort via OpenAI's `reasoning_effort`, or Anthropic's `thinking.budget_tokens`
(larger budget → higher effort).

## License

MIT
8 changes: 6 additions & 2 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,15 @@ services:
container_name: commandcode-api-proxy
restart: unless-stopped
ports:
- "8787:8787"
# Bind host-side to localhost only so the proxy (which forwards your paid
# CC key with no real auth) is not exposed to the LAN. Remove the
# 127.0.0.1: prefix only if you intentionally want network access.
- "127.0.0.1:8787:8787"
environment:
- HOST=0.0.0.0
- PORT=8787
- CC_API_KEY=${CC_API_KEY:-}
- LOG_LEVEL=${LOG_LEVEL:-info}
volumes:
- ~/.commandcode:/home/node/.commandcode:ro
# auth.json lives at ~/.config/commandcode-api-proxy/auth.json (see auth.ts).
- ~/.config/commandcode-api-proxy:/home/node/.config/commandcode-api-proxy:ro
6 changes: 5 additions & 1 deletion src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export interface Config {
ccApiBase: string;
ccVersion: string;
logLevel: string;
corsOrigin: string;
}

/**
Expand Down Expand Up @@ -81,6 +82,9 @@ export function loadConfig(): Config {
const ccApiBase = process.env.CC_API_BASE || "https://api.commandcode.ai";
const ccVersion = process.env.CC_CLI_VERSION || cachedVersion || DEFAULT_CC_VERSION;
const logLevel = process.env.LOG_LEVEL || "info";
// `*` is fine for a localhost proxy; restrict (e.g. to an origin or leave
// empty to disable) before exposing the proxy on a network.
const corsOrigin = process.env.CORS_ORIGIN ?? "*";

return { host, port, apiKey, ccApiBase, ccVersion, logLevel };
return { host, port, apiKey, ccApiBase, ccVersion, logLevel, corsOrigin };
}
111 changes: 88 additions & 23 deletions src/models.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,79 +2,144 @@
"builtin": [
"deepseek/deepseek-v4-pro",
"deepseek/deepseek-v4-flash",
"MiniMaxAI/MiniMax-M2.7",
"MiniMaxAI/MiniMax-M2.5",
"zai-org/GLM-5.2",
"zai-org/GLM-5.2-Fast",
"zai-org/GLM-5.1",
"zai-org/GLM-5",
"MiniMaxAI/MiniMax-M3",
"MiniMaxAI/MiniMax-M2.7",
"MiniMaxAI/MiniMax-M2.5",
"moonshotai/Kimi-K3",
"moonshotai/Kimi-K2.7-Code",
"moonshotai/Kimi-K2.7-Code-Highspeed",
"moonshotai/Kimi-K2.6",
"moonshotai/Kimi-K2.5",
"Qwen/Qwen3.7-Max",
"Qwen/Qwen3.7-Plus",
"Qwen/Qwen3.6-Max-Preview",
"Qwen/Qwen3.6-Plus",
"stepfun/Step-3.7-Flash",
"stepfun/Step-3.5-Flash",
"xiaomi/mimo-v2.5-pro",
"xiaomi/mimo-v2.5",
"Qwen/Qwen3.7-Max",
"Qwen/Qwen3.7-Plus"
"xai/grok-4.5",
"nvidia/nemotron-3-ultra-550b-a55b",
"thinkingmachines/inkling",
"tencent/Hy3"
],
"shortAliases": {
"deepseek-v4-pro": "deepseek/deepseek-v4-pro",
"deepseek-v4": "deepseek/deepseek-v4-pro",
"deepseek-pro": "deepseek/deepseek-v4-pro",
"deepseek-v4-flash": "deepseek/deepseek-v4-flash",
"deepseek-flash": "deepseek/deepseek-v4-flash",
"kimi-k2.6": "moonshotai/Kimi-K2.6",
"kimi-k2.5": "moonshotai/Kimi-K2.5",
"kimi2.6": "moonshotai/Kimi-K2.6",
"kimi2.5": "moonshotai/Kimi-K2.5",
"glm-5.2": "zai-org/GLM-5.2",
"glm5.2": "zai-org/GLM-5.2",
"glm-5.2-fast": "zai-org/GLM-5.2-Fast",
"glm5.2-fast": "zai-org/GLM-5.2-Fast",
"glm-5.1": "zai-org/GLM-5.1",
"glm-5": "zai-org/GLM-5",
"minimax-m3": "MiniMaxAI/MiniMax-M3",
"minimax3": "MiniMaxAI/MiniMax-M3",
"minimax-m2.7": "MiniMaxAI/MiniMax-M2.7",
"minimax2.7": "MiniMaxAI/MiniMax-M2.7",
"minimax-m2.5": "MiniMaxAI/MiniMax-M2.5",
"minimax2.5": "MiniMaxAI/MiniMax-M2.5",
"kimi-k3": "moonshotai/Kimi-K3",
"kimi3": "moonshotai/Kimi-K3",
"kimi-k2.7-code": "moonshotai/Kimi-K2.7-Code",
"kimi2.7-code": "moonshotai/Kimi-K2.7-Code",
"kimi-code": "moonshotai/Kimi-K2.7-Code",
"kimi-k2.7-highspeed": "moonshotai/Kimi-K2.7-Code-Highspeed",
"kimi-highspeed": "moonshotai/Kimi-K2.7-Code-Highspeed",
"kimi-k2.6": "moonshotai/Kimi-K2.6",
"kimi2.6": "moonshotai/Kimi-K2.6",
"kimi-k2.5": "moonshotai/Kimi-K2.5",
"kimi2.5": "moonshotai/Kimi-K2.5",
"qwen3.7-max": "Qwen/Qwen3.7-Max",
"qwen-3.7-max": "Qwen/Qwen3.7-Max",
"qwen3.7-plus": "Qwen/Qwen3.7-Plus",
"qwen-3.7-plus": "Qwen/Qwen3.7-Plus",
"qwen-3.6-max": "Qwen/Qwen3.6-Max-Preview",
"qwen3.6-max": "Qwen/Qwen3.6-Max-Preview",
"qwen-3.6-plus": "Qwen/Qwen3.6-Plus",
"qwen3.6-plus": "Qwen/Qwen3.6-Plus",
"step-3.7-flash": "stepfun/Step-3.7-Flash",
"step3.7": "stepfun/Step-3.7-Flash",
"step-3.5-flash": "stepfun/Step-3.5-Flash",
"step3.5": "stepfun/Step-3.5-Flash",
"mimo-v2.5-pro": "xiaomi/mimo-v2.5-pro",
"mimo-pro": "xiaomi/mimo-v2.5-pro",
"mimo-v2.5": "xiaomi/mimo-v2.5",
"mimo2.5": "xiaomi/mimo-v2.5",
"qwen3.7-max": "Qwen/Qwen3.7-Max",
"qwen-3.7-max": "Qwen/Qwen3.7-Max",
"qwen3.7-plus": "Qwen/Qwen3.7-Plus",
"qwen-3.7-plus": "Qwen/Qwen3.7-Plus"
"grok-4.5": "xai/grok-4.5",
"grok4.5": "xai/grok-4.5",
"nemotron": "nvidia/nemotron-3-ultra-550b-a55b",
"nemotron-3-ultra": "nvidia/nemotron-3-ultra-550b-a55b",
"inkling": "thinkingmachines/inkling",
"hy3": "tencent/Hy3"
},
"contextWindows": {
"deepseek/deepseek-v4-pro": 1048576,
"deepseek/deepseek-v4-flash": 1048576,
"MiniMaxAI/MiniMax-M2.7": 204800,
"MiniMaxAI/MiniMax-M2.5": 204800,
"zai-org/GLM-5.2": 1048576,
"zai-org/GLM-5.2-Fast": 1048576,
"zai-org/GLM-5.1": 200000,
"zai-org/GLM-5": 200000,
"moonshotai/Kimi-K2.6": 262144,
"moonshotai/Kimi-K2.5": 262144,
"MiniMaxAI/MiniMax-M3": 1048576,
"MiniMaxAI/MiniMax-M2.7": 204800,
"MiniMaxAI/MiniMax-M2.5": 200000,
"moonshotai/Kimi-K3": 1048576,
"moonshotai/Kimi-K2.7-Code": 256000,
"moonshotai/Kimi-K2.7-Code-Highspeed": 262000,
"moonshotai/Kimi-K2.6": 256000,
"moonshotai/Kimi-K2.5": 256000,
"Qwen/Qwen3.7-Max": 1048576,
"Qwen/Qwen3.7-Plus": 1048576,
"Qwen/Qwen3.6-Max-Preview": 262144,
"Qwen/Qwen3.6-Plus": 1048576,
"stepfun/Step-3.5-Flash": 262144,
"stepfun/Step-3.7-Flash": 256000,
"stepfun/Step-3.5-Flash": 1048576,
"xiaomi/mimo-v2.5-pro": 1048576,
"xiaomi/mimo-v2.5": 1048576,
"Qwen/Qwen3.7-Max": 1048576,
"Qwen/Qwen3.7-Plus": 1048576
"xai/grok-4.5": 500000,
"nvidia/nemotron-3-ultra-550b-a55b": 1048576,
"thinkingmachines/inkling": 256000,
"tencent/Hy3": 262144
},
"modelNames": {
"deepseek/deepseek-v4-pro": "DeepSeek V4 Pro",
"deepseek/deepseek-v4-flash": "DeepSeek V4 Flash",
"MiniMaxAI/MiniMax-M2.7": "MiniMax M2.7",
"MiniMaxAI/MiniMax-M2.5": "MiniMax M2.5",
"zai-org/GLM-5.2": "GLM-5.2",
"zai-org/GLM-5.2-Fast": "GLM-5.2 Fast",
"zai-org/GLM-5.1": "GLM-5.1",
"zai-org/GLM-5": "GLM-5",
"MiniMaxAI/MiniMax-M3": "MiniMax M3",
"MiniMaxAI/MiniMax-M2.7": "MiniMax M2.7",
"MiniMaxAI/MiniMax-M2.5": "MiniMax M2.5",
"moonshotai/Kimi-K3": "Kimi K3",
"moonshotai/Kimi-K2.7-Code": "Kimi K2.7 Code",
"moonshotai/Kimi-K2.7-Code-Highspeed": "Kimi K2.7 Code Highspeed",
"moonshotai/Kimi-K2.6": "Kimi K2.6",
"moonshotai/Kimi-K2.5": "Kimi K2.5",
"Qwen/Qwen3.7-Max": "Qwen 3.7 Max",
"Qwen/Qwen3.7-Plus": "Qwen 3.7 Plus",
"Qwen/Qwen3.6-Max-Preview": "Qwen 3.6 Max Preview",
"Qwen/Qwen3.6-Plus": "Qwen 3.6 Plus",
"stepfun/Step-3.7-Flash": "Step 3.7 Flash",
"stepfun/Step-3.5-Flash": "Step 3.5 Flash",
"xiaomi/mimo-v2.5-pro": "MiMo V2.5 Pro",
"xiaomi/mimo-v2.5": "MiMo V2.5",
"Qwen/Qwen3.7-Max": "Qwen 3.7 Max",
"Qwen/Qwen3.7-Plus": "Qwen 3.7 Plus"
"xai/grok-4.5": "Grok 4.5",
"nvidia/nemotron-3-ultra-550b-a55b": "Nemotron 3 Ultra",
"thinkingmachines/inkling": "Inkling",
"tencent/Hy3": "Hy3"
},
"reasoningEfforts": {
"deepseek/deepseek-v4-pro": ["high", "max"],
"deepseek/deepseek-v4-flash": ["high", "max"],
"zai-org/GLM-5.2": ["high", "max"],
"xai/grok-4.5": ["low", "medium", "high"]
},
"maxOutputTokens": {
"deepseek/deepseek-v4-pro": 393216,
Expand Down
24 changes: 19 additions & 5 deletions src/proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,11 +89,25 @@ server.listen(config.port, config.host, () => {
console.log(" Press Ctrl+C to stop\n");
});

process.on("SIGINT", () => {
console.log("\n Shutting down...");
server.close(() => process.exit(0));
// Never let an unexpected async error crash the proxy silently — log and keep
// serving. (Route handlers already catch their own errors; this is a backstop.)
process.on("unhandledRejection", (reason) => {
logger.error("[fatal] Unhandled promise rejection:", reason);
});
process.on("uncaughtException", (err) => {
logger.error("[fatal] Uncaught exception:", err);
});

process.on("SIGTERM", () => {
const shutdown = (signal: string): void => {
logger.info(`Received ${signal}, shutting down...`);
// Don't hang forever waiting on a stuck streaming connection.
const force = setTimeout(() => {
logger.warn("Forcing shutdown after 10s timeout");
process.exit(1);
}, 10_000);
force.unref();
server.close(() => process.exit(0));
});
};

process.on("SIGINT", () => shutdown("SIGINT"));
process.on("SIGTERM", () => shutdown("SIGTERM"));
Loading
Loading