Skip to main content

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/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/new202, 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

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

acp_openai_gateway-0.1.0.tar.gz (29.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

acp_openai_gateway-0.1.0-py3-none-any.whl (18.0 kB view details)

Uploaded Python 3

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

Hashes for acp_openai_gateway-0.1.0.tar.gz
Algorithm Hash digest
SHA256 1756a67537eb146912742b0991101825bd80d79f92da69b2f985354e0146891e
MD5 129c6b9c8718487c450a11750d2383df
BLAKE2b-256 0a27e686c33666b1e9d86c8a8445f56cbf44e97c47e229f24dd9d2663000c1aa

See more details on using hashes here.

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

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file acp_openai_gateway-0.1.0-py3-none-any.whl.

File metadata

File hashes

Hashes for acp_openai_gateway-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 7cb7233c1490ca0345f2ac4169a50dcde14fcbeeb6aed31d04857c8e2a3da33c
MD5 8a77bd797186f71a2a6b2becedea078d
BLAKE2b-256 a664c52574f4ee8a986e8ff5e241df985c1fc88a029968ce7da47dcb7a350e92

See more details on using hashes here.

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

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page