Observed since 20 Oct, 18:23 UTC
No superseded metadata states collected. Unchanged observations do not create new revisions.
Expose any Agent Client Protocol (ACP) agent behind an OpenAI-compatible /v1 API
Expose any agent that speaks the Agent Client Protocol (ACP) behind an OpenAI-compatible /v1/chat/completions + /v1/models API.
ACP agents (e.g. goose's goose serve) talk JSON-RPC over a streamable-HTTP /acp endpoint — which OpenAI-native tools like Open WebUI, the OpenAI SDKs, LibreChat, or curl can't consume. This gateway is the thin translator in between, so those agents show up as ordinary "models" wherever an OpenAI base URL is accepted.
OpenAI client ──► /v1/chat/completions ──► acp-openai-gateway ──► POST /acp (ACP) ──► agent
(Open WebUI, (this service) (goose, etc.)
SDK, curl)
/v1/models advertises the agent (name/version from the ACP handshake)Status: beta. Verified end-to-end against goose 1.39.0. ACP's HTTP transport is still evolving and partly undocumented — see Compatibility.
A growing number of AI agents (goose, and other tools adopting ACP) run as a local or remote service that speaks the Agent Client Protocol — a rich, session-based protocol designed for editors and IDEs. That's great for those integrations, but it means the huge ecosystem of OpenAI-compatible software — chat UIs, CLIs, coding assistants, and the official SDKs — can't talk to them, because all of that software only knows how to call OpenAI's simple REST API.
This project is the adapter in between. It runs a small HTTP server that looks exactly like OpenAI (/v1/chat/completions, /v1/models, streaming and all), and behind the scenes drives the agent over ACP. To the agent it looks like a normal client; to your tools the agent looks like just another model. Nothing about the agent changes.
Two things make it more than a dumb proxy: it advertises the agent's real name/version as the model, and it bridges OpenAI's stateless request model onto the agent's stateful session — so multi-turn chats keep the agent's memory and tool state (and survive restarts).
Use it when you have an ACP agent and want to:
You don't need it if your agent already offers an OpenAI-compatible API, or you're happy using its native ACP client (e.g. Zed/JetBrains driving a local goose). It's a translation layer, not an agent or a model host — it needs a running ACP agent to point at, and it adds no reasoning of its own.
Editors like Zed and JetBrains speak ACP as JSON-RPC over stdio — they spawn the agent as a local subprocess. ACP's HTTP transport for remote agents is still a work in progress. That leaves two different bridging problems, and this project only solves one of them:
| You have… | …and want to reach | Use |
|---|---|---|
| An OpenAI-compatible client (chat UI, SDK, coding tool) | a remote ACP agent (over HTTP) | this gateway ✅ |
| A stdio-only ACP editor (bare Zed/JetBrains ACP) | a remote ACP agent | a stdio↔HTTP ACP proxy — not this gateway |
In short: the gateway's client-facing side is OpenAI, not stdio ACP, and its agent-facing side is ACP-over-HTTP, not stdio. So it makes a remote agent reachable by the OpenAI ecosystem — including letting an IDE that supports a custom OpenAI endpoint use a remote agent through that path, instead of ACP's local-subprocess one. It does not let a stdio-only ACP editor connect to a remote agent; that needs the other bridge.
goose serve --host 0.0.0.0 --port 3000 # exposes ACP at http://localhost:3000/acp
pip install acp-openai-gateway # or: pip install -e . from a clone
ACP_URL=http://localhost:3000 acp-openai-gateway
# serving OpenAI API at http://localhost:8000/v1
# list models — reports the agent's own name/version
curl -s http://localhost:8000/v1/models
# chat (streaming)
curl -N http://localhost:8000/v1/chat/completions \
-H 'Content-Type: application/json' \
-d '{"model":"goose","stream":true,"messages":[{"role":"user","content":"Say hi in three words"}]}'
Point any OpenAI client at http://localhost:8000/v1. For Open WebUI, add it under Admin → Settings → Connections → OpenAI (or via OPENAI_API_BASE_URL); see docker-compose.example.yml.
The gateway implements the OpenAI Chat Completions (/v1/chat/completions, streaming + non-streaming) and Models (/v1/models) endpoints.
Verified — a full chat round-trip through the gateway:
models.list() (in CI, via httpx.ASGITransport)pytest -m client; see scripts/smoke_clients.sh)scripts/smoke_librechat.sh)The gateway needed no LibreChat-specific code: it's standard OpenAI, and LibreChat's custom-endpoint chat posts a standard request to it.
Expected to work (speak the same two endpoints; not individually tested): Jan, Continue.dev, LlamaIndex/LangChain, the Vercel AI SDK, curl, and most OpenAI-compatible tools. Clients that additionally require /v1/completions (legacy), /v1/embeddings, or capability metadata will see those endpoints 404 (usually harmless). Reports welcome.
# published image (after a release):
docker run --rm -p 8000:8000 -e ACP_URL=http://host.docker.internal:3000 \
ghcr.io/vadim-vyb/acp-openai-gateway:latest
# or build locally:
docker build -t acp-openai-gateway .
docker run --rm -p 8000:8000 -e ACP_URL=http://host.docker.internal:3000 acp-openai-gateway
All via environment variables (or a .env file — see .env.example):
| Variable | Default | Description |
|---|---|---|
ACP_URL | http://localhost:3000 | Agent base URL; the gateway calls {ACP_URL}/acp. |
ACP_CWD | /workspace | Working dir passed to session/new. |
ACP_MODE | auto | Session mode (goose: auto/smart_approve/approve/chat). auto avoids permission stalls. |
GATEWAY_HOST / GATEWAY_PORT | 0.0.0.0 / 8000 | Bind address. |
GATEWAY_API_KEY | (empty) | If set, require Authorization: Bearer <key>. |
MODEL_ID | (empty) | Model id on /v1/models. Empty → derived from the agent's name/version. |
EMIT_THOUGHTS | false | Stream reasoning wrapped in <think></think>. |
EMIT_TOOL_STATUS | false | Emit a status line per tool call. |
SESSION_STATE_PATH | .acp_sessions.json | Where the conversation→session map is persisted (empty = memory only). |
COLD_SEED_HISTORY | true | Seed a new session with the full transcript when a chat can't be matched. |
CONNECT_TIMEOUT / READ_TIMEOUT | 10 / 1800 | Socket timeouts (seconds). |
ACP is JSON-RPC 2.0. The streamable-HTTP transport routes messages by two headers — pinned empirically against goose (it isn't formally documented):
POST /acp initialize → response header acp-connection-id.GET /acp (SSE) with acp-connection-id → connection channel; session/new results arrive here.POST /acp session/new → 202, the sessionId shows up on the channel from step 2.GET /acp (SSE) with acp-connection-id + acp-session-id → session channel; session/update notifications and the terminal prompt result land here.POST /acp session/prompt (both headers) → streamed as agent_message_chunk frames, re-emitted as OpenAI deltas.session/request_permission is auto-answered with allow (and auto mode usually pre-empts it), so a headless turn can't block.
OpenAI clients resend the whole transcript each turn; ACP agents keep server-side session state. The gateway bridges them by hashing the transcript: after each turn it stores hash(messages + reply) → sessionId, so the next turn's history hash matches and it reuses the session — sending only the new message. Fresh chats create a new session; unmatched ones (restart, edited/branched history) get a new session seeded with the full transcript. The map is persisted (SESSION_STATE_PATH), so restarts resume via ACP session/load.
Built and verified against goose 1.39.0's goose serve. The gateway targets standard ACP methods (initialize, session/new, session/load, session/prompt, session/set_mode, session/request_permission) and the streamable-HTTP header routing described above. Other ACP agents that implement the same HTTP transport should work; the transport is still stabilizing upstream, so pin your agent version and file an issue if frames differ.
pip install -e ".[dev]"
ruff check .
pytest -q --cov=acp_openai_gateway --cov-report=term-missing
Tests are split into tests/unit/ (pure translation + session logic, incl.
property-based checks with Hypothesis) and tests/integration/ (the real
AcpClient driven against an in-memory fake ACP agent via httpx.MockTransport,
and the FastAPI app via TestClient). No network or live agent is needed — CI
runs the whole suite. To also run against a real agent:
ACP_LIVE_URL=http://localhost:3000 pytest -m live
Download counts and popularity will appear here as the collector receives them.
| Version | Published | Direct dependencies | Open version |
|---|---|---|---|
| 0.1.0 | 3 Jul 2026 | 12 |
This describes our collected data; it is not a security assessment.
Shared repository data · GitHub
Observed since 20 Oct, 18:23 UTC
No superseded metadata states collected. Unchanged observations do not create new revisions.
package:7100d4a469858dd9eddc2afc9f25b765916bd2cfaf068b971a97c6a7ebaa5116