OpenAI-compatible proxy server for the CommandCode API. Exposes /v1/chat/completions, /chat/completions, /v1/responses, and /v1/models so OpenAI-shaped clients can call CommandCode models through a local HTTP server.
Repository: https://github.com/bermudi/cmd-code-proxy
Version: v1.0.8
For project conventions, scope, goals, and the upstream-request fidelity table, see AGENTS.md (the table itself lives in MAINTAINING.md for now). For the time-bound development plan, see ROADMAP.md.
- OpenAI-compatible chat completions endpoint and alias
- OpenAI Responses-compatible endpoint shim
- Streaming and non-streaming responses
- OpenAI-compatible model list endpoint
- Optional closed/premium model listing
- Short model name mapping
- Optional default API key from CLI
- Per-request API key via
Authorizationheader - Configurable host and port
- Tools, tool calls, images, and thinking/reasoning content round-trip cleanly
reasoning_contentexposed on both message and delta (DeepSeek/Qwen/Anthropic-protocol style)
This is a personal-use adapter, not a complete OpenAI-API shape-preserving proxy.
Supported in the upstream request: model, messages (including system hoisting, content parts, images, tool calls, tool results, thinking blocks), tools, temperature, max_tokens/max_completion_tokens, stream. Everything a Claude-Code-style client actually exercises.
Not supported in the upstream request: tool_choice, parallel_tool_calls, response_format, stop, top_p, presence_penalty, frequency_penalty. These are accepted by the JSON parser and dropped before reaching CommandCode. If you need one, file an issue.
Client-facing output: byte-equivalent to this proxy's verified pre-refactor behavior for every event class covered by the parity test (17 fixtures, all event types and combinations). Streaming errors from upstream are logged but not surfaced to the client.
- Go 1.26.2 or newer
go run main.goDefault server address:
http://127.0.0.1:55990
go run main.go [options]| Option | Default | Description |
|---|---|---|
-host |
127.0.0.1 |
Host to bind the server to |
-port |
55990 |
Port to run the server on |
-api-key |
empty | Optional default CommandCode API key |
-list-closed-models |
false |
Include closed/premium models, such as Claude and GPT, in /v1/models |
-working-dir |
proxy process working directory | Working directory/project context to send to CommandCode |
-taste-learning |
true |
Default value for the upstream x-taste-learning header. Override per-request with x_command_code_taste_learning in the request body. |
-capture-dir |
empty | Directory to save raw upstream request and response NDJSON for debugging |
-debug |
false |
Enable debug-level logging (verbose per-event NDJSON) |
-version |
false |
Print version and exit |
Examples:
# Run on default host and port
go run main.go
# Run on a custom port
go run main.go -port 8080
# Expose on all interfaces
go run main.go -host 0.0.0.0
# Use a default API key for all requests that do not include Authorization
go run main.go -api-key your-commandcode-api-key
# Include closed/premium models in /v1/models
go run main.go -list-closed-models
# Capture upstream request/response NDJSON for debugging
go run main.go -capture-dir ./captures
# Print version
go run main.go -versionBuild for the current platform:
go build -o bin/command-code-proxyCross-compile for Windows and Linux:
GOOS=linux GOARCH=amd64 go build -o bin/command-code-proxy
GOOS=linux GOARCH=arm64 go build -o bin/command-code-proxy-arm64
GOOS=windows GOARCH=amd64 go build -o bin/command-code-proxy.exeThe proxy uses the API key in this order:
Authorizationheader from the incoming client request-api-keyCLI value- If neither exists, the request returns
401 Unauthorized
Header format:
Authorization: Bearer your-commandcode-api-keyGET /healthResponse:
{"status":"ok"}GET /v1/modelsReturns an OpenAI-compatible model list. By default, closed/premium models are filtered out; start the proxy with -list-closed-models to include them.
The list is hand-curated (see internal/proxy/proxy.go — fallbackModels); new CommandCode models need a code update to appear.
POST /v1/chat/completionsPOST /chat/completions is also registered as an OpenAI-compatible alias.
Example non-streaming request:
curl http://127.0.0.1:55990/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer your-commandcode-api-key" \
-d '{
"model": "deepseek-v4-pro",
"messages": [
{"role": "system", "content": "You are helpful."},
{"role": "user", "content": "Hello"}
],
"stream": false
}'Example streaming request:
curl -N http://127.0.0.1:55990/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer your-commandcode-api-key" \
-d '{
"model": "deepseek-v4-pro",
"messages": [
{"role": "user", "content": "Write a short poem."}
],
"stream": true
}'Reasoning content, if any, is exposed in choices[0].message.reasoning_content (non-streaming) and in choices[0].delta.reasoning_content (streaming).
POST /v1/responsesPartial OpenAI Responses API shim. input can be a string or an array of role/content items; instructions is converted to a system message; max_output_tokens / max_completion_tokens are forwarded. truncation, metadata, previous_response_id, store, user, and the reasoning parameter are not supported.
Example request:
curl http://127.0.0.1:55990/v1/responses \
-H "Content-Type: application/json" \
-H "Authorization: Bearer your-commandcode-api-key" \
-d '{
"model": "deepseek-v4-pro",
"instructions": "You are concise.",
"input": "Explain what this proxy does in one sentence.",
"max_output_tokens": 200
}'The proxy accepts full model IDs and these short aliases:
| Alias | Maps to |
|---|---|
deepseek-v4-pro, deepseek-v4, deepseek-pro |
deepseek/deepseek-v4-pro |
deepseek-v4-flash, deepseek-flash |
deepseek/deepseek-v4-flash |
minimax-m2.7, minimax2.7 |
MiniMaxAI/MiniMax-M2.7 |
minimax-m2.5, minimax2.5, minimax |
MiniMaxAI/MiniMax-M2.5 |
glm-5.1 |
zai-org/GLM-5.1 |
glm-5 |
zai-org/GLM-5 |
kimi-k2.6, kimi2.6 |
moonshotai/Kimi-K2.6 |
kimi-k2.5, kimi2.5 |
moonshotai/Kimi-K2.5 |
qwen-3.6-max-preview, qwen3.6-max |
Qwen/Qwen3.6-Max-Preview |
qwen-3.6-plus, qwen3.6-plus, qwen3.6 |
Qwen/Qwen3.6-Plus |
step-3.5-flash, step3.5 |
stepfun/Step-3.5-Flash |
gemini-3.1-flash-lite, gemini-flash-lite |
google/gemini-3.1-flash-lite |
minimax-m3, minimax3 |
MiniMaxAI/MiniMax-M3 |
qwen-3.7-max-free, qwen3.7-max-free |
Qwen/Qwen3.7-Max-Free |
qwen-3.7-max, qwen3.7-max |
Qwen/Qwen3.7-Max |
step-3.7-flash, step3.7 |
stepfun/Step-3.7-Flash |
mimo-v2.5-pro, mimo-pro |
xiaomi/mimo-v2.5-pro |
mimo-v2.5, mimo |
xiaomi/mimo-v2.5 |
Unknown model names are passed through unchanged.
.
├── README.md
├── AGENTS.md # goals, scope, process discipline
├── MAINTAINING.md # parity-test mechanics, upstream-request fidelity table, release checklist
├── ROADMAP.md # time-bound development plan
├── go.mod
├── go.sum
├── main.go
├── scripts
│ └── diff-captures.sh # diff proxy captures against real binary captures
└── internal
├── api
│ ├── commandcode.go
│ └── openai.go
├── proxy
│ ├── adapter.go # real CommandCode API client
│ ├── assembler.go # client-facing dispatcher (stream + non-stream)
│ ├── assembler_test.go # per-event-class unit tests
│ ├── config.go # config FS population + porcelain parser
│ ├── convert.go # OpenAI ↔ CommandCode message/tool format
│ ├── convert_test.go
│ ├── handler.go # per-endpoint HTTP handlers
│ ├── handler_test.go # end-to-end HTTP tests
│ ├── logging.go # slog + request-scoped logging middleware
│ ├── model.go
│ ├── model_test.go
│ ├── proxy.go # Proxy struct + request body builder
│ ├── router.go # route registration + middleware
│ ├── translator.go # NDJSON event decoder
│ ├── config_test.go # config FS + porcelain tests
│ ├── adapter_test.go # adapter retry + backoff tests
│ └── paritytest/ # vendored pre-refactor code + parity harness
└── version
└── version.go
- Client sends an OpenAI-shaped request to the local proxy.
- The proxy extracts system messages, maps the model name, and converts messages to CommandCode format.
- The proxy sends the request to
https://api.commandcode.ai/alpha/generate. - CommandCode streaming NDJSON events are converted back to OpenAI-shaped SSE chunks or collected into a single JSON response.
- The client-facing output has a parity test (in
internal/proxy/paritytest/) that pins the wire format byte-for-byte against a vendored copy of the pre-refactor dispatcher, so any future change to streaming or finish-reason semantics must either match the old behavior or be explicitly classified as an intentional improvement.
Every upstream request is sent with stream: true. For non-streaming clients, the proxy buffers the NDJSON stream and assembles the final JSON response.
Every request gets a unique X-Request-Id (returned in the response header) and the same ID is forwarded to CommandCode as x-request-id.
All log lines are structured (Go slog). Default is human-readable text; set PROXY_LOG_JSON=1 for JSON output suitable for jq.
Request capture — -capture-dir writes two files per request:
chatcmpl-xxx.request.json ← full request body sent to CommandCode
chatcmpl-xxx-*.ndjson ← raw upstream response (if upstream responds)
The request is captured before the upstream call, so it exists even on 401s or transport errors.
Diff against real binary — scripts/diff-captures.sh compares proxy captures against real command-code binary captures from cmd-recorder:
# Capture from the proxy
./bin/command-code-proxy -capture-dir ./proxy-captures
# Capture from the real binary (see MAINTAINING.md)
cd /path/to/project
COMMANDCODE_SANDBOX=true COMMANDCODE_API_URL=http://127.0.0.1:9090 \
command-code --skip-onboarding -p "hello"
# Diff
./scripts/diff-captures.sh ./proxy-captures ../cmd-recorder/capturesThe script normalizes both captures with jq -S and runs unified diff. Known expected differences (memory, taste, skills) are called out in the summary. (Note: threadId is no longer a difference — the proxy omits it, matching callServerAPI().)
The proxy impersonates the real command-code binary. The upstream request includes:
Config fields (sent in config):
workingDir— from the pi cc-cwd extension or-working-dirflag (not the proxy's own checkout dir)environment— hardcoded"linux-x64, Node.js v26.2.0"(matches the real CLI, not the proxy's actual runtime)date— current date (YYYY-MM-DD)structure— top-level subdirectory names fromworkingDir(filtered and sorted)isGitRepo—trueif.gitexists inworkingDircurrentBranch—git branch --show-currentmainBranch—mainormasterfromgit branch -rgitStatus—git status --porcelainsummary ("M N, A N, D N, ?? N"or"Working tree clean")recentCommits—git log --oneline -3
These fields are read from the live filesystem if the pi extension does not send a pre-populated x_command_code_config. The proxy's own checkout dir is intentionally not used — using it would leak the proxy's go.mod and internal/ into the gateway's system prompt.
Other request fields:
permissionMode—"auto-accept"params.stream— alwaystrueupstreammemory— AGENTS.md content from the project (sent by the pi extension)skills— XML from project-level.agents/skills/or.pi/skills/or.commandcode/skills/(sent by the pi extension). Gap: user-global skills from~/.commandcode/skills/,~/.agents/skills/,~/.pi/skills/are not forwarded.taste—.commandcode/taste/taste.mdif it exists (sent by the pi extension, usually null)
The upstream request includes:
x-command-code-version: <latest npm command-code version>The value is fetched from:
https://registry.npmjs.org/command-code/latest
The fetched version is cached for 30 minutes. If the registry request fails, the proxy uses the last cached version, or unknown if no version has been fetched yet.