Skip to main content

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/models advertises 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, via httpx.ASGITransport)
  • Aider — one-shot CLI run returns the agent's reply (gated test pytest -m client; see scripts/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):

  1. POST /acp initialize → response header acp-connection-id.
  2. GET /acp (SSE) with acp-connection-id → connection channel; session/new results arrive here.
  3. POST /acp session/new → 202, the sessionId shows up on the channel from step 2.
  4. GET /acp (SSE) with acp-connection-id + acp-session-id → session channel; session/update notifications and the terminal prompt result land here.
  5. 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.

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

MIT

Metadata

Release files for acp-openai-gateway 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for acp-openai-gateway 0.1.0
File Size Uploaded Provenance
acp_openai_gateway-0.1.0.tar.gz 29.7 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for acp-openai-gateway 0.1.0
File Interpreter ABI Platform
acp_openai_gateway-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 47.6 kB

Release files / acp_openai_gateway-0.1.0.tar.gz

Download URL acp_openai_gateway-0.1.0.tar.gz
Size 29.7 kB
Tags Source
SHA-256 checksum
How to use checksums
1756a67537eb146912742b0991101825bd80d79f92da69b2f985354e0146891e
BLAKE2b-256 checksum
How to use checksums
0a27e686c33666b1e9d86c8a8445f56cbf44e97c47e229f24dd9d2663000c1aa
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.12

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jul 3, 2026.

Transparency log

Release files / acp_openai_gateway-0.1.0-py3-none-any.whl

Download URL acp_openai_gateway-0.1.0-py3-none-any.whl
Size 18.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
7cb7233c1490ca0345f2ac4169a50dcde14fcbeeb6aed31d04857c8e2a3da33c
BLAKE2b-256 checksum
How to use checksums
a664c52574f4ee8a986e8ff5e241df985c1fc88a029968ce7da47dcb7a350e92
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.12

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jul 3, 2026.

Transparency log

Security for acp-openai-gateway 0.1.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for acp_openai_gateway-0.1.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jul 3, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page