Expose any Agent Client Protocol (ACP) agent behind an OpenAI-compatible /v1 API
Project description
acp-openai-gateway
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)
- ✅ Streaming (SSE) and non-streaming responses
- ✅
/v1/modelsadvertises the agent (name/version from the ACP handshake) - ✅ Stateful conversations over the stateless OpenAI protocol — maps each chat to a persistent ACP session (survives restarts)
- ✅ Optional bearer-key auth; agent-agnostic; no code changes to the agent
- ✅ Auto-approves ACP permission prompts so headless turns never stall
Status: beta. Verified end-to-end against goose 1.39.0. ACP's HTTP transport is still evolving and partly undocumented — see Compatibility.
Overview
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:
- put it behind a chat UI (Open WebUI, LibreChat, …) without writing any UI code,
- call it from the OpenAI SDKs / LangChain / LlamaIndex or any existing OpenAI integration,
- drop it into a coding tool that accepts a custom OpenAI endpoint (Aider, Continue, …),
- or expose several agents uniformly, each as its own OpenAI endpoint.
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.
This vs. a native ACP client (stdio vs. remote)
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.
Quickstart
Run the agent (example: goose)
goose serve --host 0.0.0.0 --port 3000 # exposes ACP at http://localhost:3000/acp
Run the gateway
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
Use it
# 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.
Clients
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:
- official OpenAI Python SDK — streaming, non-streaming,
models.list()(in CI, viahttpx.ASGITransport) - Aider — one-shot CLI run returns the agent's reply (gated test
pytest -m client; seescripts/smoke_clients.sh) - LibreChat — configured as a custom endpoint; a message sent through LibreChat's API reaches the gateway and the agent's reply comes back (see
scripts/smoke_librechat.sh) - Open WebUI — live, end-to-end against goose
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.
Docker
# 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
Configuration
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). |
How it works
ACP-over-HTTP transport
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 /acpinitialize → response headeracp-connection-id.GET /acp(SSE) withacp-connection-id→ connection channel;session/newresults arrive here.POST /acpsession/new →202, thesessionIdshows up on the channel from step 2.GET /acp(SSE) withacp-connection-id+acp-session-id→ session channel;session/updatenotifications and the terminal prompt result land here.POST /acpsession/prompt (both headers) → streamed asagent_message_chunkframes, 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.
Stateful chat over a stateless protocol
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.
Compatibility
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.
Development
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
Limitations
- ACP HTTP transport is undocumented/evolving — treat agent-version pinning as required.
- Transcript-hash continuity assumes the client echoes prior turns verbatim (Open WebUI does); otherwise it falls back to a freshly-seeded session (context preserved, agent tool-state not).
- Single model per gateway instance (the agent). Selecting among an agent's own sub-models isn't exposed yet.
License
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file acp_openai_gateway-0.1.0.tar.gz.
File metadata
- Download URL: acp_openai_gateway-0.1.0.tar.gz
- Upload date:
- Size: 29.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1756a67537eb146912742b0991101825bd80d79f92da69b2f985354e0146891e
|
|
| MD5 |
129c6b9c8718487c450a11750d2383df
|
|
| BLAKE2b-256 |
0a27e686c33666b1e9d86c8a8445f56cbf44e97c47e229f24dd9d2663000c1aa
|
Provenance
The following attestation bundles were made for acp_openai_gateway-0.1.0.tar.gz:
Publisher:
release.yml on vadim-vyb/acp-openai-gateway
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
acp_openai_gateway-0.1.0.tar.gz -
Subject digest:
1756a67537eb146912742b0991101825bd80d79f92da69b2f985354e0146891e - Sigstore transparency entry: 2064204509
- Sigstore integration time:
-
Permalink:
vadim-vyb/acp-openai-gateway@8c5f278afd304810a93d436d5a9d3f230b18e0c4 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/vadim-vyb
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@8c5f278afd304810a93d436d5a9d3f230b18e0c4 -
Trigger Event:
push
-
Statement type:
File details
Details for the file acp_openai_gateway-0.1.0-py3-none-any.whl.
File metadata
- Download URL: acp_openai_gateway-0.1.0-py3-none-any.whl
- Upload date:
- Size: 18.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7cb7233c1490ca0345f2ac4169a50dcde14fcbeeb6aed31d04857c8e2a3da33c
|
|
| MD5 |
8a77bd797186f71a2a6b2becedea078d
|
|
| BLAKE2b-256 |
a664c52574f4ee8a986e8ff5e241df985c1fc88a029968ce7da47dcb7a350e92
|
Provenance
The following attestation bundles were made for acp_openai_gateway-0.1.0-py3-none-any.whl:
Publisher:
release.yml on vadim-vyb/acp-openai-gateway
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
acp_openai_gateway-0.1.0-py3-none-any.whl -
Subject digest:
7cb7233c1490ca0345f2ac4169a50dcde14fcbeeb6aed31d04857c8e2a3da33c - Sigstore transparency entry: 2064204518
- Sigstore integration time:
-
Permalink:
vadim-vyb/acp-openai-gateway@8c5f278afd304810a93d436d5a9d3f230b18e0c4 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/vadim-vyb
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@8c5f278afd304810a93d436d5a9d3f230b18e0c4 -
Trigger Event:
push
-
Statement type: