Talk to your local Claude CLI through an OpenAI-compatible API.
ccal ("Claude Code Adapter for LLMs") is a small, standalone HTTP server that
proxies OpenAI-compatible requests to your local
claude CLI. It lets any OpenAI-style
client — an SDK, a chat UI, curl, or an app like
Warren — talk to your Claude subscription as if
it were a regular model endpoint.
Under the hood it shells out to claude -p --output-format stream-json ... and
translates the CLI's streaming output into OpenAI Server-Sent Events.
- Use your Claude Code subscription anywhere.
ccaldoes not call the Anthropic API directly — it drives the CLI you already have logged in, so no extra API key or billing is involved. - Drop-in OpenAI compatibility. Point any OpenAI client at
http://127.0.0.1:8787/v1and it just works — models, chat completions, streaming. - Local and simple. Binds to localhost by default, no daemon, no config files. One command starts it.
- Node 20+
- The
claudeCLI installed and logged in.ccaldrives your local Claude Code, so your normal Claude Code auth/subscription is what's used. Install it from https://docs.claude.com/claude-code and runclaudeonce to sign in.
No install required — run it straight from npm:
npx @rousan/ccal serve --port 8787You should see something like:
ccal listening on http://127.0.0.1:8787
OpenAI-compatible base URL: http://127.0.0.1:8787/v1
Using claude CLI at /Users/you/.local/bin/claude (2.x.x ...)
The OpenAI-compatible base URL is:
http://127.0.0.1:8787/v1
The API key is ignored (auth is handled by your local claude login), so any
placeholder works.
curl (streaming):
curl -N http://127.0.0.1:8787/v1/chat/completions \
-H 'Content-Type: application/json' \
-d '{
"model": "sonnet",
"stream": true,
"messages": [{ "role": "user", "content": "Say hello in one word." }]
}'OpenAI Python SDK:
from openai import OpenAI
client = OpenAI(base_url="http://127.0.0.1:8787/v1", api_key="ccal")
resp = client.chat.completions.create(
model="sonnet",
messages=[{"role": "user", "content": "Say hello in one word."}],
)
print(resp.choices[0].message.content)Warren (or any app with an OpenAI-compatible provider setting): add a custom
provider with base URL http://127.0.0.1:8787/v1, any API key, and pick one of
sonnet / opus / haiku as the model.
GET /health→{ "ok": true }GET /v1/models→ the OpenAI model list. Three ids are advertised, matching theclaudeCLI's--modelaliases:sonnet,opus,haiku. When the server was started with--vanilla,supported_parametersdrops"tools"for every model, since the backing agent has none to use.POST /v1/chat/completions→ OpenAI-compatible chat completions. Supports bothstream: true(SSEdata: {choices:[{delta:{content}}]}frames terminated bydata: [DONE]) and the non-streaming case (a single assembledchoices[0].message.content). Images work too: includeimage_urlcontent parts (adata:base64 URL or a remote http(s) URL) and they are forwarded toclaudeas image blocks for the vision models to see.
systemmessages are combined and passed to claude via--append-system-prompt(appended to the default agent prompt, so tool use still works). In--vanillamode they go through--system-promptinstead, replacing the prompt rather than extending it.user/assistantmessages are flattened into a single text transcript sent on stdin. A lone message is sent verbatim; a multi-turn conversation is rendered as aUser:/Assistant:labeled transcript. This is one prompt in, one response out — claude's streaming JSON output is translated back into OpenAI chunks (assistant text plus compact one-line notes for any tool calls).
ccal serve [options] Start the OpenAI-compatible server
ccal update Update ccal to the latest published version
ccal version Print the installed version (also: --version, -v)
ccal serve [options]
--port <n> Port to bind (default: 8787)
--host <addr> Host to bind (default: 127.0.0.1)
--cwd <dir> Working directory for the claude process
--permission-mode <m> Permission mode passed to claude (non-default only)
--allow-origin <origin> Let a site on the public internet call this server
(repeatable)
--vanilla Serve plain model calls instead of a full Claude
Code agent (no tools, no MCP servers, no CLAUDE.md)
--help Show this help
--cwdcontrols where claude runs, which determines the tools, MCP servers, andCLAUDE.mdit loads (agentic mode only —--vanillaturns all three off regardless of--cwd).CCAL_CLAUDE_PATH(environment variable) forces a specificclaudebinary when auto-detection does not find the right one.
By default ccal runs claude as a full Claude Code agent: it has its
built-in tools, connects to whatever MCP servers your claude config defines,
and loads CLAUDE.md from --cwd. That is the right shape for most uses of
ccal — a chat UI or SDK that just wants the model to actually do things.
It is the wrong shape when the caller is itself an agent with its own tool
loop — for example opencode. Point opencode at ccal
without --vanilla and you get two independent agents fighting over the same
turn: opencode issues tool calls of its own while the claude process
underneath is separately reading files, running bash, and picking up whatever
CLAUDE.md sits in --cwd.
ccal serve --vanillaturns every request into a plain model call:
- No built-in tools (
--tools ""on theclaudeinvocation). - No
CLAUDE.md, skills, plugins, hooks, or MCP servers (--safe-mode, plus--strict-mcp-configas a second guarantee against MCP). - The caller's
systemmessage replaces the agent's default system prompt (--system-prompt) instead of being appended to it — so the model isn't told it's a coding agent with tools it no longer has. If a request carries no system message, ccal substitutes a small neutral one rather than leaving the agent framing in place.
Your own subscription login still works — vanilla mode does not use
--bare, which would require ANTHROPIC_API_KEY and stop reading your
OAuth/keychain login. GET /v1/models also drops "tools" from
supported_parameters in this mode, since the backing model genuinely can't
use any.
Off by default; existing consumers that rely on the agentic behaviour are unaffected.
If the page calling ccal is served over the public internet — a deployed app
pointed at your local ccal, rather than something on localhost — you need
--allow-origin:
ccal serve --allow-origin https://example.com
Chrome applies Private Network Access on top of ordinary CORS: a public page
reaching a private address must send a preflight and be answered
Access-Control-Allow-Private-Network: true. Without that, the request does
not fail — it hangs, with no console error and nothing to search for. If a
deployed app sits forever on "connecting", this is almost always why.
Two things worth knowing before you use it:
- Only name sites you trust. An allowed origin can drive your
claudeCLI, which means your subscription and whatever--cwdpoints at. - You will not reproduce the problem locally. A page on
localhostcalling127.0.0.1is private-to-private, so PNA never engages and everything works without the flag. It only appears once the page is deployed.
- docs/architecture.md — the request lifecycle end to end, module map, and binary resolution.
- docs/development.md — local setup, scripts, testing, and publishing to npm.
MIT © Rousan Ali