acpbox 0.1.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- acpbox-0.1.1/.dockerignore +13 -0
- acpbox-0.1.1/.env.example +28 -0
- acpbox-0.1.1/.github/workflows/publish-pypi.yaml +78 -0
- acpbox-0.1.1/.github/workflows/tag-on-merge.yaml +84 -0
- acpbox-0.1.1/.github/workflows/tests-on-pr.yaml +34 -0
- acpbox-0.1.1/.gitignore +11 -0
- acpbox-0.1.1/Dockerfile +53 -0
- acpbox-0.1.1/LICENSE +21 -0
- acpbox-0.1.1/PKG-INFO +113 -0
- acpbox-0.1.1/README.md +91 -0
- acpbox-0.1.1/acpbox/__init__.py +1 -0
- acpbox-0.1.1/acpbox/acp_stdio.py +486 -0
- acpbox-0.1.1/acpbox/cli.py +7 -0
- acpbox-0.1.1/acpbox/config.py +112 -0
- acpbox-0.1.1/acpbox/errors.py +25 -0
- acpbox-0.1.1/acpbox/main.py +119 -0
- acpbox-0.1.1/acpbox/mapping.py +412 -0
- acpbox-0.1.1/acpbox/routes/__init__.py +1 -0
- acpbox-0.1.1/acpbox/routes/chat.py +195 -0
- acpbox-0.1.1/acpbox/routes/models.py +58 -0
- acpbox-0.1.1/acpbox/routes/responses.py +101 -0
- acpbox-0.1.1/acpbox/schemas.py +134 -0
- acpbox-0.1.1/acpbox/session_store.py +40 -0
- acpbox-0.1.1/acpbox.egg-info/PKG-INFO +113 -0
- acpbox-0.1.1/acpbox.egg-info/SOURCES.txt +50 -0
- acpbox-0.1.1/acpbox.egg-info/dependency_links.txt +1 -0
- acpbox-0.1.1/acpbox.egg-info/entry_points.txt +2 -0
- acpbox-0.1.1/acpbox.egg-info/requires.txt +12 -0
- acpbox-0.1.1/acpbox.egg-info/top_level.txt +1 -0
- acpbox-0.1.1/config.example.yaml +16 -0
- acpbox-0.1.1/docker-compose.yaml +34 -0
- acpbox-0.1.1/docs/acp-lifecycle.md +32 -0
- acpbox-0.1.1/docs/api-mapping.md +48 -0
- acpbox-0.1.1/docs/config.md +75 -0
- acpbox-0.1.1/docs/deployment.md +114 -0
- acpbox-0.1.1/docs/spec.md +37 -0
- acpbox-0.1.1/pyproject.toml +38 -0
- acpbox-0.1.1/requirements-dev.txt +1 -0
- acpbox-0.1.1/requirements.txt +9 -0
- acpbox-0.1.1/setup.cfg +4 -0
- acpbox-0.1.1/tests/__init__.py +0 -0
- acpbox-0.1.1/tests/conftest.py +81 -0
- acpbox-0.1.1/tests/test_chat.py +257 -0
- acpbox-0.1.1/tests/test_models.py +46 -0
- acpbox-0.1.1/tests/test_responses.py +77 -0
- acpbox-0.1.1/tests/test_sessions.py +27 -0
- acpbox-0.1.1/tests/unit/__init__.py +0 -0
- acpbox-0.1.1/tests/unit/test_config.py +45 -0
- acpbox-0.1.1/tests/unit/test_errors.py +35 -0
- acpbox-0.1.1/tests/unit/test_mapping.py +273 -0
- acpbox-0.1.1/tests/unit/test_session_store.py +38 -0
- acpbox-0.1.1/workspace/.gitignore +2 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Gateway config via environment. Copy to .env and set values.
|
|
2
|
+
# All options can be set here; they override config.yaml.
|
|
3
|
+
# ACP agent runs per-request over stdio (no HTTP, no /ping).
|
|
4
|
+
|
|
5
|
+
# Docker image build only. Comma-separated: opencode, cursor (case-insensitive).
|
|
6
|
+
# Examples: opencode | cursor | opencode,cursor
|
|
7
|
+
# Match ACP_COMMAND below to an installed binary, e.g. ["opencode","acp"] or ["agent","acp"].
|
|
8
|
+
AGENTS=opencode
|
|
9
|
+
|
|
10
|
+
CONFIG_PATH=config.yaml
|
|
11
|
+
|
|
12
|
+
# ACP agent command (stdio). JSON array, e.g. ["opencode", "acp"]
|
|
13
|
+
ACP_COMMAND=["opencode", "acp"]
|
|
14
|
+
# Extra env for ACP process as JSON object
|
|
15
|
+
ACP_ENV={}
|
|
16
|
+
# ACP session/new project directory (relative to process cwd or absolute). Default ./workspace; Docker image and compose use /workspace (see Dockerfile, docker-compose.yaml).
|
|
17
|
+
ACP_WORKSPACE=./workspace
|
|
18
|
+
|
|
19
|
+
GATEWAY_HOST=0.0.0.0
|
|
20
|
+
GATEWAY_PORT=8080
|
|
21
|
+
# Uvicorn process workers (one ACP subprocess per worker). Not OS threads.
|
|
22
|
+
GATEWAY_WORKERS=1
|
|
23
|
+
# Only has effect if uvicorn.run supports threads= (current releases usually do not).
|
|
24
|
+
GATEWAY_THREADS=1
|
|
25
|
+
|
|
26
|
+
# Agent API keys
|
|
27
|
+
# CURSOR_API_KEY=sk-xxx
|
|
28
|
+
# ANTHROPIC_API_KEY=sk-xxx
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Build and publish acpbox to PyPI when a release tag is pushed.
|
|
2
|
+
# Expects tags vMAJOR.MINOR.PATCH (same shape as tag-on-merge.yaml).
|
|
3
|
+
# Version in artifacts matches the tag (v stripped). Configure PyPI Trusted Publishing
|
|
4
|
+
# for this repository (publish workflow permission id-token write is required).
|
|
5
|
+
|
|
6
|
+
name: Publish to PyPI
|
|
7
|
+
|
|
8
|
+
on:
|
|
9
|
+
workflow_dispatch:
|
|
10
|
+
workflow_call:
|
|
11
|
+
push:
|
|
12
|
+
tags:
|
|
13
|
+
- "v*"
|
|
14
|
+
|
|
15
|
+
defaults:
|
|
16
|
+
run:
|
|
17
|
+
shell: bash
|
|
18
|
+
|
|
19
|
+
permissions:
|
|
20
|
+
contents: read
|
|
21
|
+
id-token: write
|
|
22
|
+
|
|
23
|
+
jobs:
|
|
24
|
+
publish:
|
|
25
|
+
name: Build and upload to PyPI
|
|
26
|
+
runs-on: ubuntu-latest
|
|
27
|
+
|
|
28
|
+
steps:
|
|
29
|
+
- name: Checkout repository
|
|
30
|
+
uses: actions/checkout@v4
|
|
31
|
+
with:
|
|
32
|
+
fetch-depth: 0
|
|
33
|
+
|
|
34
|
+
- name: Resolve release version from tag
|
|
35
|
+
id: rel
|
|
36
|
+
run: |
|
|
37
|
+
TAG="${GITHUB_REF_NAME}"
|
|
38
|
+
VERSION="${TAG#v}"
|
|
39
|
+
if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
|
40
|
+
echo "Release tag must look like vMAJOR.MINOR.PATCH, got: $TAG"
|
|
41
|
+
exit 1
|
|
42
|
+
fi
|
|
43
|
+
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
|
44
|
+
echo "Publishing acpbox $VERSION (tag $TAG)"
|
|
45
|
+
|
|
46
|
+
- name: Ensure tag points to main
|
|
47
|
+
run: |
|
|
48
|
+
git fetch origin main
|
|
49
|
+
if ! git merge-base --is-ancestor "${GITHUB_SHA}" origin/main; then
|
|
50
|
+
echo "Tagged commit is not on main, refusing to publish."
|
|
51
|
+
exit 1
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
- name: Set up Python
|
|
55
|
+
uses: actions/setup-python@v5
|
|
56
|
+
with:
|
|
57
|
+
python-version: "3.11"
|
|
58
|
+
|
|
59
|
+
- name: Install build tooling
|
|
60
|
+
run: |
|
|
61
|
+
python -m pip install --upgrade pip
|
|
62
|
+
pip install "build>=1.0"
|
|
63
|
+
|
|
64
|
+
- name: Run tests
|
|
65
|
+
run: |
|
|
66
|
+
pip install -e ".[dev]"
|
|
67
|
+
pytest tests/ -v -m "not integration" --tb=short
|
|
68
|
+
|
|
69
|
+
- name: Build distributions
|
|
70
|
+
env:
|
|
71
|
+
SETUPTOOLS_SCM_PRETEND_VERSION: ${{ steps.rel.outputs.version }}
|
|
72
|
+
run: python -m build
|
|
73
|
+
|
|
74
|
+
- name: Publish to PyPI
|
|
75
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
76
|
+
with:
|
|
77
|
+
packages-dir: dist/
|
|
78
|
+
password: ${{ secrets.PYPI_API_TOKEN }}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# Tag a new SemVer version when a PR is merged into the default branch (not on direct push to main).
|
|
2
|
+
# Starts from v0.1.0 if no tags exist; bumps patch (e.g. v0.1.0 -> v0.1.1) on each merge.
|
|
3
|
+
|
|
4
|
+
name: Tag release on merge
|
|
5
|
+
|
|
6
|
+
on:
|
|
7
|
+
pull_request:
|
|
8
|
+
types: [closed]
|
|
9
|
+
branches:
|
|
10
|
+
- main
|
|
11
|
+
workflow_dispatch:
|
|
12
|
+
|
|
13
|
+
defaults:
|
|
14
|
+
run:
|
|
15
|
+
shell: bash
|
|
16
|
+
|
|
17
|
+
jobs:
|
|
18
|
+
tag-version:
|
|
19
|
+
name: Bump and push version tag
|
|
20
|
+
runs-on: ubuntu-latest
|
|
21
|
+
if: github.event_name == 'workflow_dispatch' || github.event.pull_request.merged == true
|
|
22
|
+
permissions:
|
|
23
|
+
contents: write
|
|
24
|
+
actions: write
|
|
25
|
+
|
|
26
|
+
steps:
|
|
27
|
+
- name: Checkout repository
|
|
28
|
+
uses: actions/checkout@v4
|
|
29
|
+
with:
|
|
30
|
+
fetch-depth: 0
|
|
31
|
+
ref: ${{ github.event_name == 'workflow_dispatch' && github.ref || github.event.pull_request.merge_commit_sha }}
|
|
32
|
+
|
|
33
|
+
- name: Fetch all tags
|
|
34
|
+
run: git fetch --tags
|
|
35
|
+
|
|
36
|
+
- name: Check if commit already has a tag
|
|
37
|
+
id: check
|
|
38
|
+
run: |
|
|
39
|
+
if [ -n "$(git tag --points-at HEAD)" ]; then
|
|
40
|
+
echo "skip=true" >> "$GITHUB_OUTPUT"
|
|
41
|
+
echo "Commit already has a tag, skipping."
|
|
42
|
+
else
|
|
43
|
+
echo "skip=false" >> "$GITHUB_OUTPUT"
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
- name: Get latest SemVer tag and bump patch
|
|
47
|
+
id: version
|
|
48
|
+
if: steps.check.outputs.skip != 'true'
|
|
49
|
+
run: |
|
|
50
|
+
set -e
|
|
51
|
+
# Latest tag matching v* (SemVer), version-sorted; default v0.1.0
|
|
52
|
+
LATEST=$(git tag -l 'v*' | sort -V | tail -1 || true)
|
|
53
|
+
if [ -z "$LATEST" ]; then
|
|
54
|
+
LATEST="v0.1.0"
|
|
55
|
+
fi
|
|
56
|
+
VERSION="${LATEST#v}"
|
|
57
|
+
# Bump patch: 0.1.0 -> 0.1.1
|
|
58
|
+
NEW_VERSION=$(echo "$VERSION" | awk -F. '{$3=$3+1; OFS="."; print $1,$2,$3}')
|
|
59
|
+
echo "previous=$LATEST" >> "$GITHUB_OUTPUT"
|
|
60
|
+
echo "version=$NEW_VERSION" >> "$GITHUB_OUTPUT"
|
|
61
|
+
echo "tag=v${NEW_VERSION}" >> "$GITHUB_OUTPUT"
|
|
62
|
+
echo "Bumping $LATEST -> v${NEW_VERSION}"
|
|
63
|
+
|
|
64
|
+
- name: Create and push tag
|
|
65
|
+
if: steps.check.outputs.skip != 'true'
|
|
66
|
+
run: |
|
|
67
|
+
git config user.name "github-actions[bot]"
|
|
68
|
+
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
69
|
+
TAG="${{ steps.version.outputs.tag }}"
|
|
70
|
+
git tag -a "$TAG" -m "Release $TAG (auto-tag on merge)"
|
|
71
|
+
git push origin "$TAG"
|
|
72
|
+
|
|
73
|
+
- name: Trigger Docker build
|
|
74
|
+
if: steps.check.outputs.skip != 'true'
|
|
75
|
+
uses: actions/github-script@v7
|
|
76
|
+
with:
|
|
77
|
+
script: |
|
|
78
|
+
await github.rest.actions.createWorkflowDispatch({
|
|
79
|
+
owner: context.repo.owner,
|
|
80
|
+
repo: context.repo.repo,
|
|
81
|
+
workflow_id: 'docker-build-push.yaml',
|
|
82
|
+
ref: 'main',
|
|
83
|
+
inputs: { tag: '${{ steps.version.outputs.tag }}' }
|
|
84
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Run tests on every pull request (opened, updated, reopened).
|
|
2
|
+
|
|
3
|
+
name: Tests on PR
|
|
4
|
+
|
|
5
|
+
on:
|
|
6
|
+
pull_request:
|
|
7
|
+
types: [opened, synchronize, reopened]
|
|
8
|
+
workflow_dispatch:
|
|
9
|
+
|
|
10
|
+
defaults:
|
|
11
|
+
run:
|
|
12
|
+
shell: bash
|
|
13
|
+
|
|
14
|
+
jobs:
|
|
15
|
+
test:
|
|
16
|
+
name: Test suite
|
|
17
|
+
runs-on: ubuntu-latest
|
|
18
|
+
|
|
19
|
+
steps:
|
|
20
|
+
- name: Checkout repository
|
|
21
|
+
uses: actions/checkout@v4
|
|
22
|
+
|
|
23
|
+
- name: Set up Python
|
|
24
|
+
uses: actions/setup-python@v5
|
|
25
|
+
with:
|
|
26
|
+
python-version: "3.11"
|
|
27
|
+
|
|
28
|
+
- name: Install dependencies
|
|
29
|
+
run: |
|
|
30
|
+
python -m pip install --upgrade pip
|
|
31
|
+
pip install -e ".[dev]"
|
|
32
|
+
|
|
33
|
+
- name: Run tests
|
|
34
|
+
run: pytest tests/ -v -m "not integration" --cov=acpbox --cov-report=term-missing
|
acpbox-0.1.1/.gitignore
ADDED
acpbox-0.1.1/Dockerfile
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
FROM python:3.11-slim
|
|
2
|
+
|
|
3
|
+
# Comma-separated, case-insensitive: opencode, cursor. Pass at build time (e.g. docker compose build.args from .env).
|
|
4
|
+
ARG AGENTS=
|
|
5
|
+
|
|
6
|
+
RUN groupadd --gid 1000 user \
|
|
7
|
+
&& useradd --uid 1000 --gid 1000 --create-home --home-dir /home/user user
|
|
8
|
+
|
|
9
|
+
WORKDIR /app
|
|
10
|
+
|
|
11
|
+
ENV DEBIAN_FRONTEND=noninteractive
|
|
12
|
+
ENV HOME=/home/user
|
|
13
|
+
ENV PATH="/home/user/.local/bin:/home/user/.opencode/bin:${PATH}"
|
|
14
|
+
|
|
15
|
+
RUN set -eux; \
|
|
16
|
+
agents="$(printf '%s' "${AGENTS}" | tr '[:upper:]' '[:lower:]' | tr -d '[:space:]')"; \
|
|
17
|
+
agents=",${agents},"; \
|
|
18
|
+
install_opencode=0; \
|
|
19
|
+
install_cursor=0; \
|
|
20
|
+
case "${agents}" in (*,opencode,*) install_opencode=1 ;; esac; \
|
|
21
|
+
case "${agents}" in (*,cursor,*) install_cursor=1 ;; esac; \
|
|
22
|
+
if [ "${install_opencode}" = "0" ] && [ "${install_cursor}" = "0" ]; then \
|
|
23
|
+
echo "AGENTS build-arg empty or without opencode|cursor; image contains acpbox only. Install an ACP agent in a derived image or mount a binary."; \
|
|
24
|
+
else \
|
|
25
|
+
apt-get update; \
|
|
26
|
+
apt-get install -y --no-install-recommends curl ca-certificates bash; \
|
|
27
|
+
if [ "${install_opencode}" = "1" ]; then \
|
|
28
|
+
su user -s /bin/bash -c 'curl -fsSL https://opencode.ai/install | bash'; \
|
|
29
|
+
su user -s /bin/bash -c 'command -v opencode'; \
|
|
30
|
+
fi; \
|
|
31
|
+
if [ "${install_cursor}" = "1" ]; then \
|
|
32
|
+
su user -s /bin/bash -c 'curl -fsSL https://cursor.com/install | bash'; \
|
|
33
|
+
su user -s /bin/bash -c 'command -v agent'; \
|
|
34
|
+
fi; \
|
|
35
|
+
apt-get purge -y curl; \
|
|
36
|
+
apt-get autoremove -y; \
|
|
37
|
+
rm -rf /var/lib/apt/lists/*; \
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
COPY . .
|
|
41
|
+
RUN pip install --no-cache-dir . \
|
|
42
|
+
&& chown -R user:user /app \
|
|
43
|
+
&& mkdir -p /workspace \
|
|
44
|
+
&& chown user:user /workspace
|
|
45
|
+
|
|
46
|
+
ENV ACP_WORKSPACE=/workspace
|
|
47
|
+
ENV CONFIG_PATH=/app/config.yaml
|
|
48
|
+
|
|
49
|
+
EXPOSE 8080
|
|
50
|
+
|
|
51
|
+
USER user
|
|
52
|
+
|
|
53
|
+
CMD ["acpbox"]
|
acpbox-0.1.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Pavel Rykov
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
acpbox-0.1.1/PKG-INFO
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: acpbox
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: OpenAI-compatible HTTP API gateway for Agent Client Protocol (ACP) over stdio
|
|
5
|
+
Author: acpbox contributors
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.11
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Dist: fastapi>=0.115.0
|
|
11
|
+
Requires-Dist: uvicorn[standard]>=0.32.0
|
|
12
|
+
Requires-Dist: httpx>=0.27.0
|
|
13
|
+
Requires-Dist: pydantic>=2.0
|
|
14
|
+
Requires-Dist: pydantic-settings>=2.0
|
|
15
|
+
Requires-Dist: pyyaml>=6.0
|
|
16
|
+
Requires-Dist: agent-client-protocol>=0.8.1
|
|
17
|
+
Provides-Extra: dev
|
|
18
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
19
|
+
Requires-Dist: pytest-asyncio>=0.24; extra == "dev"
|
|
20
|
+
Requires-Dist: pytest-cov>=5.0; extra == "dev"
|
|
21
|
+
Dynamic: license-file
|
|
22
|
+
|
|
23
|
+
# ACPBox
|
|
24
|
+
|
|
25
|
+
OpenAI-compatible HTTP API that acts as a **gateway to the Agent Client Protocol (ACP)**. Clients use the usual OpenAI endpoints (`/v1/models`, `/v1/chat/completions`, `/v1/responses`); the gateway runs the API with **uvicorn** and keeps **one ACP agent process per worker** over **stdio** (JSON-RPC), not HTTP.
|
|
26
|
+
|
|
27
|
+
## Problem
|
|
28
|
+
|
|
29
|
+
Many tools and SDKs expect an OpenAI-style API. ACP agents (e.g. [OpenCode](https://github.com/sst/opencode) via `opencode acp`, or **Cursor Agent** via `agent acp`) speak the [Agent Client Protocol](https://agentclientprotocol.com) over stdin/stdout. This gateway provides a single HTTP entry point: one base URL, OpenAI-shaped API, with one ACP binary instance per uvicorn worker.
|
|
30
|
+
|
|
31
|
+
## How it works
|
|
32
|
+
|
|
33
|
+
1. **Config** – YAML and env define the agent command, env vars, and **workspace** (`ACP_WORKSPACE`, default `./workspace`; Docker `/workspace`) passed as ACP `session/new` `cwd`.
|
|
34
|
+
2. **One ACP process per worker** – The app runs under **uvicorn**. In lifespan each worker starts **one** ACP agent subprocess and keeps it for the worker's lifetime. With 8 workers you get 8 ACP binary instances. ACP uses **stdio** only (JSON-RPC, newline-delimited).
|
|
35
|
+
3. **Reuse per request** – Each request in that worker uses the same process: `session/new` -> optional `session/set_mode` -> `session/prompt`, then the response is returned. The process is not terminated after each request.
|
|
36
|
+
4. **Translation** – OpenAI requests are converted to ACP JSON-RPC; ACP content (e.g. `session/update` agent_message_chunk) is converted back to OpenAI chat/responses format.
|
|
37
|
+
- `GET /v1/models` – Uses the worker's ACP process: `initialize` (once) and `session/new`, returns **modes** (`modes.availableModes[].id`, e.g. OpenCode's `plan`, `build`) as the list of models.
|
|
38
|
+
- `POST /v1/chat/completions` – Uses the worker's ACP process; `model` selects the ACP mode. Reply as chat completion, or **Server-Sent Events** when `"stream": true` (OpenAI-style `chat.completion.chunk` lines and `data: [DONE]`). With `stream: false`, optional **`acp`** is `{ "steps": [ reasoning + command summaries ] }` (no raw chunks). With `stream: true`, each chunk may still carry raw **`acp`** from the wire (see `docs/api-mapping.md`).
|
|
39
|
+
- `POST /v1/responses` – Same; optional `chat_id` for client-side continuity (non-streaming only); **`acp.steps`** when present, like chat.
|
|
40
|
+
|
|
41
|
+
See [docs/spec.md](docs/spec.md) and [docs/agent-client-protocol/docs/protocol/transports.mdx](docs/agent-client-protocol/docs/protocol/transports.mdx) for details.
|
|
42
|
+
|
|
43
|
+
```mermaid
|
|
44
|
+
sequenceDiagram
|
|
45
|
+
participant Client
|
|
46
|
+
participant Gateway
|
|
47
|
+
participant AgentProcess
|
|
48
|
+
|
|
49
|
+
Note over Gateway,AgentProcess: One ACP process per uvicorn worker (started in lifespan)
|
|
50
|
+
Client->>Gateway: GET /v1/models
|
|
51
|
+
Gateway->>AgentProcess: initialize, session/new (reuse process)
|
|
52
|
+
AgentProcess-->>Gateway: modes (e.g. plan, build)
|
|
53
|
+
Gateway-->>Client: list of models
|
|
54
|
+
|
|
55
|
+
Client->>Gateway: POST /v1/chat/completions
|
|
56
|
+
Gateway->>AgentProcess: session/new, session/prompt (same process)
|
|
57
|
+
AgentProcess-->>Gateway: session/update, session/prompt result
|
|
58
|
+
Gateway-->>Client: chat completion
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Quick setup
|
|
62
|
+
|
|
63
|
+
1. **Config** – Copy `config.example.yaml` to `config.yaml` and adjust. Every option can also be set via environment (see `.env.example`).
|
|
64
|
+
|
|
65
|
+
2. **Env** – Copy `.env.example` to `.env` and set values. All options (`CONFIG_PATH`, `ACP_*`, `GATEWAY_*`) can be configured via env.
|
|
66
|
+
|
|
67
|
+
**Agent command (OpenCode vs Cursor)** – set `acp.command` in `config.yaml` or `ACP_COMMAND` as a JSON array of strings.
|
|
68
|
+
|
|
69
|
+
| Backend | `config.yaml` | Shell (env) |
|
|
70
|
+
|---------|---------------|-------------|
|
|
71
|
+
| OpenCode | `command: ["opencode", "acp"]` | `export ACP_COMMAND='["opencode","acp"]'` |
|
|
72
|
+
| Cursor Agent | `command: ["agent", "acp"]` | `export ACP_COMMAND='["agent","acp"]'` |
|
|
73
|
+
|
|
74
|
+
Use an absolute path if the binary is not on `PATH` (e.g. `["/home/you/.local/bin/agent","acp"]`). Cursor Agent must be installed and logged in (`agent login`) so the subprocess can reach your account.
|
|
75
|
+
|
|
76
|
+
3. **Run** – Install the package (adds the **`acpbox`** CLI). The process manager is **uvicorn** (see `pyproject.toml`). Use **`gateway.workers`** in YAML or **`GATEWAY_WORKERS`** in the environment for process count (one ACP subprocess per worker). **`GATEWAY_THREADS`** is forwarded into **`uvicorn.run`** only if your installed uvicorn exposes a matching `threads=` argument (current releases usually do not; ASGI uses **asyncio** per process).
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
pip install .
|
|
80
|
+
CONFIG_PATH=config.yaml acpbox
|
|
81
|
+
# Or from a checkout without installing the script:
|
|
82
|
+
pip install -r requirements.txt
|
|
83
|
+
CONFIG_PATH=config.yaml python -m acpbox.main
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Or with Docker Compose (reads `.env` and runs the **`acpbox`** service). Set **`AGENTS`** in `.env` for the image build (comma-separated `opencode`, `cursor`); the compose file passes it as a build-arg. After changing `AGENTS`, run `docker compose build --no-cache acpbox` so installers run again. Runtime **`ACP_COMMAND`** must match the installed binary (see Agent command table above). The image **CMD** is **`acpbox`** (uvicorn inside **`acpbox.main.run`**), with **`GATEWAY_WORKERS`** and **`GATEWAY_THREADS`** passed through the environment.
|
|
87
|
+
|
|
88
|
+
4. **Use** – Point any OpenAI client at `http://localhost:8080/v1` (or your host/port). List models, call chat completions or responses; the gateway translates to ACP and back.
|
|
89
|
+
|
|
90
|
+
## Tests
|
|
91
|
+
|
|
92
|
+
Tests use a mock ACP over stdio (fake subprocess that responds with JSON-RPC). Route tests: `tests/test_models.py`, `tests/test_chat.py`, `tests/test_responses.py`, `tests/test_sessions.py`. Unit tests for mapping, errors, session_store, config, and stdio client in `tests/unit/`. Fixtures in `tests/conftest.py`.
|
|
93
|
+
|
|
94
|
+
From repo root:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
pip install -e ".[dev]"
|
|
98
|
+
pytest tests/ -v
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Adding your own ACP in Docker
|
|
102
|
+
|
|
103
|
+
Build an image that includes **acpbox** and your ACP agent binary (e.g. `opencode acp`). Set `acp.command` and `acp.env` in config or `.env`. The server runs with **uvicorn** via **`acpbox`**; each worker starts one ACP process in lifespan. To run 8 ACP instances, set **`GATEWAY_WORKERS=8`** (or **`gateway.workers`** in YAML). See [docs/deployment.md](docs/deployment.md).
|
|
104
|
+
|
|
105
|
+
## Specifications
|
|
106
|
+
|
|
107
|
+
- [docs/spec.md](docs/spec.md) – This gateway: OpenAI HTTP API to Agent Client Protocol (stdio).
|
|
108
|
+
- [OpenAI API OpenAPI spec](https://github.com/openai/openai-openapi/tree/manual_spec) – OpenAI REST API specification (OpenAPI).
|
|
109
|
+
- [Agent Client Protocol](https://agentclientprotocol.com) – Protocol for agent-client communication over stdio (JSON-RPC); see `docs/agent-client-protocol/`.
|
|
110
|
+
|
|
111
|
+
## License
|
|
112
|
+
|
|
113
|
+
This project is licensed under the MIT License, see the [LICENSE](LICENSE) file in the repository root for details.
|
acpbox-0.1.1/README.md
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# ACPBox
|
|
2
|
+
|
|
3
|
+
OpenAI-compatible HTTP API that acts as a **gateway to the Agent Client Protocol (ACP)**. Clients use the usual OpenAI endpoints (`/v1/models`, `/v1/chat/completions`, `/v1/responses`); the gateway runs the API with **uvicorn** and keeps **one ACP agent process per worker** over **stdio** (JSON-RPC), not HTTP.
|
|
4
|
+
|
|
5
|
+
## Problem
|
|
6
|
+
|
|
7
|
+
Many tools and SDKs expect an OpenAI-style API. ACP agents (e.g. [OpenCode](https://github.com/sst/opencode) via `opencode acp`, or **Cursor Agent** via `agent acp`) speak the [Agent Client Protocol](https://agentclientprotocol.com) over stdin/stdout. This gateway provides a single HTTP entry point: one base URL, OpenAI-shaped API, with one ACP binary instance per uvicorn worker.
|
|
8
|
+
|
|
9
|
+
## How it works
|
|
10
|
+
|
|
11
|
+
1. **Config** – YAML and env define the agent command, env vars, and **workspace** (`ACP_WORKSPACE`, default `./workspace`; Docker `/workspace`) passed as ACP `session/new` `cwd`.
|
|
12
|
+
2. **One ACP process per worker** – The app runs under **uvicorn**. In lifespan each worker starts **one** ACP agent subprocess and keeps it for the worker's lifetime. With 8 workers you get 8 ACP binary instances. ACP uses **stdio** only (JSON-RPC, newline-delimited).
|
|
13
|
+
3. **Reuse per request** – Each request in that worker uses the same process: `session/new` -> optional `session/set_mode` -> `session/prompt`, then the response is returned. The process is not terminated after each request.
|
|
14
|
+
4. **Translation** – OpenAI requests are converted to ACP JSON-RPC; ACP content (e.g. `session/update` agent_message_chunk) is converted back to OpenAI chat/responses format.
|
|
15
|
+
- `GET /v1/models` – Uses the worker's ACP process: `initialize` (once) and `session/new`, returns **modes** (`modes.availableModes[].id`, e.g. OpenCode's `plan`, `build`) as the list of models.
|
|
16
|
+
- `POST /v1/chat/completions` – Uses the worker's ACP process; `model` selects the ACP mode. Reply as chat completion, or **Server-Sent Events** when `"stream": true` (OpenAI-style `chat.completion.chunk` lines and `data: [DONE]`). With `stream: false`, optional **`acp`** is `{ "steps": [ reasoning + command summaries ] }` (no raw chunks). With `stream: true`, each chunk may still carry raw **`acp`** from the wire (see `docs/api-mapping.md`).
|
|
17
|
+
- `POST /v1/responses` – Same; optional `chat_id` for client-side continuity (non-streaming only); **`acp.steps`** when present, like chat.
|
|
18
|
+
|
|
19
|
+
See [docs/spec.md](docs/spec.md) and [docs/agent-client-protocol/docs/protocol/transports.mdx](docs/agent-client-protocol/docs/protocol/transports.mdx) for details.
|
|
20
|
+
|
|
21
|
+
```mermaid
|
|
22
|
+
sequenceDiagram
|
|
23
|
+
participant Client
|
|
24
|
+
participant Gateway
|
|
25
|
+
participant AgentProcess
|
|
26
|
+
|
|
27
|
+
Note over Gateway,AgentProcess: One ACP process per uvicorn worker (started in lifespan)
|
|
28
|
+
Client->>Gateway: GET /v1/models
|
|
29
|
+
Gateway->>AgentProcess: initialize, session/new (reuse process)
|
|
30
|
+
AgentProcess-->>Gateway: modes (e.g. plan, build)
|
|
31
|
+
Gateway-->>Client: list of models
|
|
32
|
+
|
|
33
|
+
Client->>Gateway: POST /v1/chat/completions
|
|
34
|
+
Gateway->>AgentProcess: session/new, session/prompt (same process)
|
|
35
|
+
AgentProcess-->>Gateway: session/update, session/prompt result
|
|
36
|
+
Gateway-->>Client: chat completion
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Quick setup
|
|
40
|
+
|
|
41
|
+
1. **Config** – Copy `config.example.yaml` to `config.yaml` and adjust. Every option can also be set via environment (see `.env.example`).
|
|
42
|
+
|
|
43
|
+
2. **Env** – Copy `.env.example` to `.env` and set values. All options (`CONFIG_PATH`, `ACP_*`, `GATEWAY_*`) can be configured via env.
|
|
44
|
+
|
|
45
|
+
**Agent command (OpenCode vs Cursor)** – set `acp.command` in `config.yaml` or `ACP_COMMAND` as a JSON array of strings.
|
|
46
|
+
|
|
47
|
+
| Backend | `config.yaml` | Shell (env) |
|
|
48
|
+
|---------|---------------|-------------|
|
|
49
|
+
| OpenCode | `command: ["opencode", "acp"]` | `export ACP_COMMAND='["opencode","acp"]'` |
|
|
50
|
+
| Cursor Agent | `command: ["agent", "acp"]` | `export ACP_COMMAND='["agent","acp"]'` |
|
|
51
|
+
|
|
52
|
+
Use an absolute path if the binary is not on `PATH` (e.g. `["/home/you/.local/bin/agent","acp"]`). Cursor Agent must be installed and logged in (`agent login`) so the subprocess can reach your account.
|
|
53
|
+
|
|
54
|
+
3. **Run** – Install the package (adds the **`acpbox`** CLI). The process manager is **uvicorn** (see `pyproject.toml`). Use **`gateway.workers`** in YAML or **`GATEWAY_WORKERS`** in the environment for process count (one ACP subprocess per worker). **`GATEWAY_THREADS`** is forwarded into **`uvicorn.run`** only if your installed uvicorn exposes a matching `threads=` argument (current releases usually do not; ASGI uses **asyncio** per process).
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
pip install .
|
|
58
|
+
CONFIG_PATH=config.yaml acpbox
|
|
59
|
+
# Or from a checkout without installing the script:
|
|
60
|
+
pip install -r requirements.txt
|
|
61
|
+
CONFIG_PATH=config.yaml python -m acpbox.main
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Or with Docker Compose (reads `.env` and runs the **`acpbox`** service). Set **`AGENTS`** in `.env` for the image build (comma-separated `opencode`, `cursor`); the compose file passes it as a build-arg. After changing `AGENTS`, run `docker compose build --no-cache acpbox` so installers run again. Runtime **`ACP_COMMAND`** must match the installed binary (see Agent command table above). The image **CMD** is **`acpbox`** (uvicorn inside **`acpbox.main.run`**), with **`GATEWAY_WORKERS`** and **`GATEWAY_THREADS`** passed through the environment.
|
|
65
|
+
|
|
66
|
+
4. **Use** – Point any OpenAI client at `http://localhost:8080/v1` (or your host/port). List models, call chat completions or responses; the gateway translates to ACP and back.
|
|
67
|
+
|
|
68
|
+
## Tests
|
|
69
|
+
|
|
70
|
+
Tests use a mock ACP over stdio (fake subprocess that responds with JSON-RPC). Route tests: `tests/test_models.py`, `tests/test_chat.py`, `tests/test_responses.py`, `tests/test_sessions.py`. Unit tests for mapping, errors, session_store, config, and stdio client in `tests/unit/`. Fixtures in `tests/conftest.py`.
|
|
71
|
+
|
|
72
|
+
From repo root:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
pip install -e ".[dev]"
|
|
76
|
+
pytest tests/ -v
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Adding your own ACP in Docker
|
|
80
|
+
|
|
81
|
+
Build an image that includes **acpbox** and your ACP agent binary (e.g. `opencode acp`). Set `acp.command` and `acp.env` in config or `.env`. The server runs with **uvicorn** via **`acpbox`**; each worker starts one ACP process in lifespan. To run 8 ACP instances, set **`GATEWAY_WORKERS=8`** (or **`gateway.workers`** in YAML). See [docs/deployment.md](docs/deployment.md).
|
|
82
|
+
|
|
83
|
+
## Specifications
|
|
84
|
+
|
|
85
|
+
- [docs/spec.md](docs/spec.md) – This gateway: OpenAI HTTP API to Agent Client Protocol (stdio).
|
|
86
|
+
- [OpenAI API OpenAPI spec](https://github.com/openai/openai-openapi/tree/manual_spec) – OpenAI REST API specification (OpenAPI).
|
|
87
|
+
- [Agent Client Protocol](https://agentclientprotocol.com) – Protocol for agent-client communication over stdio (JSON-RPC); see `docs/agent-client-protocol/`.
|
|
88
|
+
|
|
89
|
+
## License
|
|
90
|
+
|
|
91
|
+
This project is licensed under the MIT License, see the [LICENSE](LICENSE) file in the repository root for details.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# ACP OpenAI API Gateway
|