openclaude
An OpenAI-compatible API over the local Claude CLI, so agent frameworks run on a Claude login, no API key.
Every agent framework worth using speaks OpenAI's /v1/chat/completions
protocol. LangChain, LangGraph, Deep Agents — they all take a base_url and
an API key and get on with it. Claude is not in that shape, so using it means
an Anthropic API key and a billing account, separate from the Claude
subscription you might already be paying for.
But there's already an authenticated Claude on the machine: the claude CLI,
logged in through Claude Code. openclaude is the adapter between those two
facts — an OpenAI-compatible server that spawns the CLI per request and
translates in both directions.
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(
base_url="http://localhost:8787/v1",
api_key="not-needed", # only checked if you set API_KEY in .env
model="claude-sonnet",
streaming=True,
)How the translation works
The CLI's print mode is not a chat API, so most of the work is reconciling two different shapes.
| OpenAI concept | How it maps |
|---|---|
model |
Alias table → claude --model. Unknown values pass through verbatim, so you aren't limited to the built-in aliases. |
messages[] |
Print mode takes one prompt and is stateless, so history is re-sent every call: system becomes --system-prompt, longer histories flatten into a User:/Assistant: transcript. |
stream: true |
--output-format stream-json, forwarding only text_delta events as choices[0].delta.content, terminated with [DONE]. |
reasoning_effort |
An extension, passed to claude --effort. Accepts low/medium/high plus the CLI's own xhigh/max. |
temperature, max_tokens |
Accepted for client compatibility but not forwarded — print mode has no sampling flags. Silently ignored rather than erroring. |
That last row is the kind of thing worth being explicit about. Quietly accepting a parameter you can't honour is a lie the caller will eventually trip over, so it's documented as a known limitation rather than hidden.
Tools are off by default, deliberately
The obvious risk with wrapping a coding agent in an HTTP API is that the model
can reach the filesystem on behalf of whoever calls it. Every CLI invocation
runs with --tools "", so it behaves like a plain chat model, not an agent
with shell access.
The single exception is attachments. Plain text under 300KB is inlined into
the prompt directly — cheap, no tool round-trip. Anything else (images, PDFs)
gets written to a per-request temp directory, and only then does the CLI get a
scoped, read-only Read tool with --add-dir pointed at that directory. It
costs one extra agentic turn, and it's the only documented way to feed binary
content to Claude Code in print mode, since there is no --image flag. The
directory is deleted afterwards.
Remote image_url values are deliberately not fetched — that's an SSRF
hole, and there's no CLI primitive for it either. You get the URL echoed back
with a note that it wasn't loaded.
Treating it like a real service
A wrapper around a subprocess fails in ways a normal API client doesn't expect, so the unglamorous parts got the attention:
- Concurrency is capped (
MAX_CONCURRENCY, default 3) with an in-memory FIFO queue, so a burst of requests doesn't spawn unbounded processes. - Timeouts SIGTERM a stuck call, then SIGKILL after a grace period, and return a 504 rather than hanging the caller.
- Errors — bad model, auth failure, non-zero exit, unparseable output, a
wrong
CLAUDE_CLI_PATH— all map to OpenAI's{ error: { message, type, code } }shape, so client SDKs handle them exactly as they'd handle a real OpenAI error. - The working directory is a neutral scratch dir, not the repo, so the
model is never implicitly exposed to openclaude's own source through
CLAUDE.mdproject discovery.
Known limitations
Written down in the README rather than discovered later:
- No OpenAI function/tool calling.
temperatureandmax_tokensare accepted but not enforced.- Multi-turn history is flattened to text, so it won't reproduce the Messages API's multi-turn caching behaviour.
- Each request is a fresh process, so there's real CLI startup latency per call — typically a few seconds. This is not a low-latency inference server.
It's pinned to a specific CLI version (2.1.220 on macOS) because CLI flags
move. All the flag and model knowledge lives in two files, src/claudeCli.ts
and src/models.ts, so when something drifts there's exactly one place to
diff against claude --help.