v0.6.0 — point any SDK at localhost
apiplan serve runs a local server that speaks OpenAI's and Anthropic's wire shapes exactly. Change one base URL and existing code answers from the subscriptions you already pay for — no API key, no per-token bill. Still zero dependencies.
apiplan serve # http://127.0.0.1:8787from openai import OpenAI
client = OpenAI(api_key="not-needed", base_url="http://127.0.0.1:8787/v1")
client.chat.completions.create(model="opus", messages=[...]) # Claude, in OpenAI's shapeexport OPENAI_BASE_URL=http://127.0.0.1:8787/v1
export ANTHROPIC_BASE_URL=http://127.0.0.1:8787The dialect and the backend are independent
The path decides the response shape. The model field decides who answers. So /v1/chat/completions with model: "opus" returns Claude in OpenAI's format, and /v1/messages with model: "sol" returns GPT in Anthropic's.
That is the point: most tooling speaks exactly one dialect, and this makes every model reachable from all of it.
| endpoint | shape |
|---|---|
POST /v1/chat/completions |
OpenAI chat, streaming and not |
POST /v1/messages |
Anthropic messages, streaming and not |
POST /v1/audio/speech |
OpenAI speech — instructions steers delivery, like tts --as |
POST /v1/images/generations |
OpenAI images, b64_json |
GET /v1/models |
either shape; the caller's auth header picks which |
Errors come back in the caller's own envelope, so both SDKs parse failures correctly.
Verified with the real SDKs
Not curl — the official openai and @anthropic-ai/sdk packages, installed fresh: both dialects, both directions (Claude through OpenAI's SDK, GPT through Anthropic's), streaming on both, plus audio.speech.create and images.generate.
Two bugs this surfaced, both fixed at the root
- Anthropic replied with an empty string.
build()leavesstreamto the caller and the CLI sets it at call time; without it Anthropic returns a plain JSON body, which the SSE reader parsed as zero events. Silent, not an error. --max-tokenswas broken on every OpenAI model, and had been. The codex backend rejectsmax_output_tokensoutright (400). Rarely hit from the CLI; every API client sets it by default, so the server hit it immediately. The parameter is no longer sent, and the CLI now says the flag is ignored there rather than dropping it quietly.
Safety
Binds 127.0.0.1 only — it hands out your subscription to anything that can reach it. Set APIPLAN_API_KEY to require a key (enforced on Authorization and x-api-key); --port / --host to move it.
134 tests · 7/7 budgets · CI green on ubuntu, macos and windows.