II.
Page JSON
Structured · livepage:docs-adapters-archive-design-provider-adapter
`adapters-proxy` — Transport Protocol Bridge json
Inspect the normalized record payload exactly as the atlas UI reads it.
{
"id": "page:docs-adapters-archive-design-provider-adapter",
"_kind": "Page",
"_file": "wiki/docs/adapters/archive/design/provider-adapter.md",
"_cluster": "wiki",
"attributes": {
"nodeKind": "Page",
"sourcePath": "docs/adapters/archive/design/provider-adapter.md",
"sourceKind": "repo-docs",
"title": "`adapters-proxy` — Transport Protocol Bridge",
"displayName": "`adapters-proxy` — Transport Protocol Bridge",
"slug": "docs/adapters/archive/design/provider-adapter",
"articlePath": "wiki/docs/adapters/archive/design/provider-adapter.md",
"article": "\n# `adapters-proxy` — Transport Protocol Bridge\n\n> Archived design document. Preserved for historical context; not part of the current normative `reference/` contract.\n\n**Specification v1.0** | `adapters-proxy` (Python package)\n\n---\n\n## 1. Overview\n\n`adapters-proxy` is a lightweight Python package that bridges between LLM transport protocols. Each instance serves a single purpose: accept requests in one protocol format (the **exposed transport**) and forward them to a target provider in its native format, translating between the two using [LiteLLM](https://github.com/BerriAI/litellm) as the translation engine.\n\nThis enables vendor-locked harnesses (Claude Code speaks only Anthropic API, Codex speaks only OpenAI API, Gemini speaks only Google API) to use **any** LLM provider — Bedrock, Vertex, Ollama, Groq, Together, DeepSeek, or 140+ others supported by LiteLLM.\n\nAdditionally, `adapters-proxy` supports pulling and serving local models via [Ollama](https://github.com/ollama/ollama-python), enabling fully offline / air-gapped coding agent usage.\n\n### 1.1 Design Principles\n\n- **Single-purpose instances**: Each proxy instance bridges exactly one (exposed transport, target provider) pair. No multi-tenant routing. This keeps configuration simple and failure domains small.\n- **Ephemeral by default**: Designed to be spawned on-demand by `adapters launch` and terminated when the harness exits. Can also run as a persistent service.\n- **Env-var configured**: All configuration via environment variables for container/CI/CD friendliness.\n- **Zero state**: No database, no persistence, no session management. Pure translation proxy.\n- **LiteLLM for heavy lifting**: Protocol translation, provider auth, error mapping — all delegated to LiteLLM.\n\n### 1.2 Cross-References\n\n| Concept | Spec |\n|---|---|\n| `adapters launch` command | `docs/launcher.md` |\n| Provider configuration | `docs/adapters-provider-config.md` |\n| LiteLLM docs | `https://docs.litellm.ai` |\n| Ollama Python | `https://github.com/ollama/ollama-python` |\n\n---\n\n## 2. Package Structure\n\n```\nadapters-proxy/\n├── pyproject.toml # Package metadata, dependencies, entry points\n├── Dockerfile # Container image\n├── docker-compose.yml # Example compose for persistent deployment\n├── .github/\n│ └── workflows/\n│ ├── ci.yml # Lint, test, type-check\n│ ├── publish.yml # PyPI publish on tag\n│ └── docker.yml # Container image build + push\n├── src/\n│ └── adapters_proxy/\n│ ├── __init__.py\n│ ├── __main__.py # Entry point: python -m adapters_proxy\n│ ├── cli.py # CLI argument parsing (click)\n│ ├── config.py # Configuration from env vars\n│ ├── server.py # ASGI server (uvicorn + FastAPI)\n│ ├── errors.py # Shared exception types and error helpers\n│ ├── auth.py # Request authentication (bearer token)\n│ ├── transports/\n│ │ ├── __init__.py\n│ │ ├── anthropic.py # /v1/messages endpoint\n│ │ ├── openai_chat.py # /v1/chat/completions endpoint\n│ │ ├── openai_responses.py # /v1/responses endpoint\n│ │ └── google.py # /v1beta/models/:model:generateContent endpoint\n│ └── providers/\n│ ├── __init__.py\n│ ├── resolver.py # LiteLLM provider resolution\n│ └── ollama_server.py # Ollama server lifecycle management (start, stop)\n├── tests/\n│ ├── conftest.py\n│ ├── test_config.py\n│ ├── test_anthropic_transport.py\n│ ├── test_openai_chat_transport.py\n│ ├── test_openai_responses_transport.py\n│ ├── test_google_transport.py\n│ ├── test_translation.py\n│ ├── test_ollama_mgr.py\n│ └── test_health.py\n└── README.md\n```\n\n---\n\n## 3. Configuration\n\nAll configuration is via environment variables. No config files.\n\n### 3.1 Required Variables\n\n| Variable | Description | Example |\n|---|---|---|\n| `ADAPTERS_PROXY_TARGET_PROVIDER` | LiteLLM provider prefix for the target | `bedrock`, `vertex_ai`, `anthropic`, `ollama`, `groq`, `together_ai`, `openai` |\n| `ADAPTERS_PROXY_TARGET_MODEL` | Model identifier in LiteLLM format | `bedrock/anthropic.claude-sonnet-4-20250514-v1:0`, `ollama/qwen3:32b` |\n| `ADAPTERS_PROXY_EXPOSED_TRANSPORT` | Which transport to expose to the harness | `anthropic`, `openai-chat`, `openai-responses`, `google` |\n\n### 3.2 Optional Variables\n\n| Variable | Default | Description |\n|---|---|---|\n| `ADAPTERS_PROXY_PORT` | `0` (ephemeral) | Port to listen on. `0` = OS-assigned. |\n| `ADAPTERS_PROXY_HOST` | `127.0.0.1` | Bind address. `127.0.0.1` for security. |\n| `ADAPTERS_PROXY_AUTH_TOKEN` | (auto-generated) | Bearer token required on incoming requests. If unset, a random UUID is generated and printed to stderr on startup. |\n| `ADAPTERS_PROXY_LOG_LEVEL` | `warn` | Logging level: `debug`, `info`, `warn`, `error`. |\n| `ADAPTERS_PROXY_TIMEOUT` | `600` | Request timeout in seconds. |\n| `ADAPTERS_PROXY_MAX_RETRIES` | `2` | Number of retries to the target provider on transient failures. |\n| `ADAPTERS_PROXY_STREAM` | `true` | Enable streaming responses. |\n| `ADAPTERS_PROXY_DROP_UNSUPPORTED_PARAMS` | `true` | Let LiteLLM silently drop params the target doesn't support. |\n\n### 3.3 Provider Auth Variables\n\nProvider authentication uses the standard LiteLLM environment variables:\n\n| Provider | Variables |\n|---|---|\n| `anthropic` | `ANTHROPIC_API_KEY` |\n| `openai` | `OPENAI_API_KEY` |\n| `bedrock` | `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_REGION_NAME` (or `AWS_PROFILE`) |\n| `vertex_ai` | `GOOGLE_APPLICATION_CREDENTIALS` or ADC, `VERTEXAI_PROJECT`, `VERTEXAI_LOCATION` |\n| `azure` | `AZURE_API_KEY`, `AZURE_API_BASE`, `AZURE_API_VERSION` |\n| `ollama` | (none — localhost) `OLLAMA_HOST` if non-default |\n| `groq` | `GROQ_API_KEY` |\n| `together_ai` | `TOGETHERAI_API_KEY` |\n| `fireworks_ai` | `FIREWORKS_AI_API_KEY` |\n| `deepseek` | `DEEPSEEK_API_KEY` |\n| `mistral` | `MISTRAL_API_KEY` |\n| `openrouter` | `OPENROUTER_API_KEY` |\n| `cerebras` | `CEREBRAS_API_KEY` |\n| `sambanova` | `SAMBANOVA_API_KEY` |\n\n### 3.4 Ollama-Specific Variables\n\n| Variable | Default | Description |\n|---|---|---|\n| `ADAPTERS_PROXY_OLLAMA_AUTO_PULL` | `true` | Auto-pull model if not available locally. |\n| `ADAPTERS_PROXY_OLLAMA_HOST` | `http://localhost:11434` | Ollama server URL. |\n| `ADAPTERS_PROXY_OLLAMA_MANAGE_SERVER` | `false` | Start/stop Ollama server with the proxy lifecycle. |\n| `ADAPTERS_PROXY_OLLAMA_KEEP_ALIVE` | `5m` | Keep model loaded in memory after last request. |\n\n---\n\n## 4. Architecture\n\n### 4.1 Request Flow\n\n```\n┌─────────────┐ ┌──────────────────────────────────────────────┐ ┌──────────────┐\n│ Harness │ │ adapters-proxy │ │ Provider │\n│ (Claude/ │────▶│ │────▶│ (Bedrock/ │\n│ Codex/ │ │ ┌───────────┐ ┌───────────┐ ┌─────────┐ │ │ Vertex/ │\n│ Gemini) │◀────│ │ Transport │ │ LiteLLM │ │ HTTP │ │◀────│ Ollama/...) │\n│ │ │ │ Endpoint │─▶│ Translate │─▶│ Client │ │ │ │\n│ │ │ └───────────┘ └───────────┘ └─────────┘ │ │ │\n│ │ │ ▲ │ │ │\n│ │ │ │ Bearer token auth │ │ │\n│ │ └──────────────────────────────────────────────┘ └──────────────┘\n└─────────────┘ \n```\n\n### 4.2 Translation Pipeline\n\nFor each incoming request:\n\n```python\nasync def handle_request(request: TransportRequest) -> TransportResponse:\n # 1. Authenticate: verify bearer token\n verify_auth(request)\n \n # 2. Parse: transport-specific request → normalized form\n messages, params = parse_transport_request(request) # transport-specific\n \n # 3. Translate: normalized form → LiteLLM completion() call\n litellm_response = await litellm.acompletion(\n model=config.target_model, # e.g., \"bedrock/anthropic.claude-sonnet-4-...\"\n messages=messages,\n stream=config.stream,\n timeout=config.timeout,\n num_retries=config.max_retries,\n drop_params=config.drop_unsupported_params,\n **params, # tool_choice, temperature, etc.\n )\n \n # 4. Format: LiteLLM response → transport-specific response\n return format_transport_response(litellm_response) # transport-specific\n```\n\n### 4.3 Streaming Pipeline\n\nFor streaming requests, the proxy translates chunk-by-chunk:\n\n```python\nasync def handle_streaming_request(request: TransportRequest) -> StreamingResponse:\n verify_auth(request)\n messages, params = parse_transport_request(request)\n \n response_stream = await litellm.acompletion(\n model=config.target_model,\n messages=messages,\n stream=True,\n **params,\n )\n \n async def generate():\n async for chunk in response_stream:\n yield format_transport_chunk(chunk) # transport-specific SSE/NDJSON\n \n return StreamingResponse(generate(), media_type=transport_media_type())\n```\n\n---\n\n## 5. Transport Endpoints\n\n### 5.1 Anthropic Transport (`/v1/messages`)\n\nExposes the [Anthropic Messages API](https://docs.anthropic.com/en/api/messages) format:\n\n```\nPOST /v1/messages\nContent-Type: application/json\nX-Api-Key: <auth_token>\n```\n\n**Request body**: Anthropic Messages format with `model`, `messages`, `max_tokens`, `system`, `tools`, `tool_choice`, `stream`, `metadata`, `thinking`.\n\n**Response**: Anthropic Messages response or SSE stream.\n\n**Translation**:\n- Incoming Anthropic format → LiteLLM `messages` (OpenAI format internally)\n- LiteLLM handles the translation to the target provider\n- LiteLLM response (OpenAI format) → Anthropic response format\n\nKey translation details:\n- `thinking` blocks → handled by LiteLLM's thinking/reasoning support\n- `tool_use` / `tool_result` content blocks → LiteLLM `tools` / `tool_calls`\n- SSE event types: `message_start`, `content_block_start`, `content_block_delta`, `content_block_stop`, `message_delta`, `message_stop`\n- `anthropic-version` header is accepted but not forwarded (the proxy speaks the target provider's protocol)\n\n### 5.2 OpenAI Chat Completions Transport (`/v1/chat/completions`)\n\nExposes the [OpenAI Chat Completions API](https://platform.openai.com/docs/api-reference/chat):\n\n```\nPOST /v1/chat/completions\nContent-Type: application/json\nAuthorization: Bearer <auth_token>\n```\n\n**Request body**: OpenAI Chat Completions format with `model`, `messages`, `max_tokens`, `temperature`, `tools`, `tool_choice`, `stream`, `response_format`.\n\n**Response**: OpenAI Chat Completions response or SSE stream.\n\n**Translation**: This is the most straightforward path since LiteLLM's internal format is OpenAI-compatible. The request is passed nearly verbatim to `litellm.completion()`, and the response is returned as-is.\n\n### 5.3 OpenAI Responses Transport (`/v1/responses`)\n\nExposes the [OpenAI Responses API](https://platform.openai.com/docs/api-reference/responses):\n\n```\nPOST /v1/responses\nContent-Type: application/json\nAuthorization: Bearer <auth_token>\n```\n\n**Request body**: OpenAI Responses format with `model`, `input`, `instructions`, `tools`, `max_output_tokens`, `stream`.\n\n**Response**: OpenAI Responses response or SSE stream.\n\n**Translation**:\n- `input` (string or array) → LiteLLM `messages` array\n- `instructions` → `system` message\n- Response items → reconstructed from LiteLLM response\n- Streaming events: `response.created`, `response.output_item.added`, `response.content_part.added`, `response.output_text.delta`, `response.output_text.done`, `response.completed`\n\nNote: `previous_response_id` and multi-turn context are not maintained (the proxy is stateless). The harness is expected to manage conversation history.\n\n### 5.4 Google GenerateContent Transport (`/v1beta/models/:model:generateContent`)\n\nExposes the [Google Generative AI API](https://ai.google.dev/api/rest):\n\n```\nPOST /v1beta/models/{model}:generateContent\nPOST /v1beta/models/{model}:streamGenerateContent\nContent-Type: application/json\nX-Goog-Api-Key: <auth_token>\n```\n\n**Request body**: Google GenerateContent format with `contents`, `systemInstruction`, `tools`, `toolConfig`, `generationConfig`.\n\n**Response**: Google GenerateContent response or SSE stream.\n\n**Translation**:\n- `contents[].parts[]` → LiteLLM `messages[].content` (text, image, function_call, function_response)\n- `generationConfig` → LiteLLM params (temperature, maxOutputTokens, etc.)\n- `tools[].functionDeclarations` → LiteLLM `tools` (OpenAI format)\n- Response `candidates[].content.parts[]` → reconstructed from LiteLLM response\n- `streamGenerateContent` → SSE chunks\n\n### 5.5 Common Endpoints\n\nAll transports also serve:\n\n```\nGET /health → { \"status\": \"ok\", \"transport\": \"anthropic\", \"provider\": \"bedrock\" }\nGET /v1/models → List available models from the target provider\nPOST /v1/count_tokens → Token count estimation (where provider supports it)\n```\n\n---\n\n## 6. Ollama Integration (Local Models)\n\n### 6.1 Model Management\n\n`adapters-proxy` includes an Ollama model manager that handles local model lifecycle:\n\n```python\n# src/adapters_proxy/providers/ollama_mgr.py\n\nimport ollama\n\nclass OllamaManager:\n def __init__(self, host: str = \"http://localhost:11434\"):\n self.client = ollama.Client(host=host)\n \n async def ensure_model(self, model: str, auto_pull: bool = True) -> bool:\n \"\"\"Ensure model is available locally. Pull if needed.\"\"\"\n available = self.list_models()\n if model in available:\n return True\n if not auto_pull:\n raise ModelNotAvailable(f\"Model '{model}' not found locally. \"\n \"Set ADAPTERS_PROXY_OLLAMA_AUTO_PULL=true to auto-pull.\")\n await self.pull_model(model)\n return True\n \n def list_models(self) -> list[str]:\n \"\"\"List locally available model names.\"\"\"\n response = self.client.list()\n return [m.model for m in response.models]\n \n async def pull_model(self, model: str) -> None:\n \"\"\"Pull a model with progress reporting to stderr.\"\"\"\n import sys\n for progress in self.client.pull(model, stream=True):\n status = progress.get('status', '')\n completed = progress.get('completed', 0)\n total = progress.get('total', 0)\n if total > 0:\n pct = (completed / total) * 100\n print(f\"\\r[adapters-proxy] Pulling {model}: {status} {pct:.1f}%\", \n end='', file=sys.stderr)\n else:\n print(f\"\\r[adapters-proxy] Pulling {model}: {status}\", \n end='', file=sys.stderr)\n print(file=sys.stderr) # newline after progress\n \n def health_check(self) -> bool:\n \"\"\"Check if Ollama server is reachable.\"\"\"\n try:\n self.client.list()\n return True\n except Exception:\n return False\n```\n\n### 6.2 Server Lifecycle Management\n\nWhen `ADAPTERS_PROXY_OLLAMA_MANAGE_SERVER=true`:\n\n```python\nimport subprocess\nimport time\n\nclass OllamaServerManager:\n def __init__(self):\n self.process: subprocess.Popen | None = None\n \n def start(self, host: str = \"127.0.0.1\", port: int = 11434) -> None:\n \"\"\"Start Ollama server as a subprocess.\"\"\"\n self.process = subprocess.Popen(\n [\"ollama\", \"serve\"],\n env={**os.environ, \"OLLAMA_HOST\": f\"{host}:{port}\"},\n stdout=subprocess.PIPE,\n stderr=subprocess.PIPE,\n )\n # Wait for server readiness\n for _ in range(30): # 30s timeout\n if OllamaManager(f\"http://{host}:{port}\").health_check():\n return\n time.sleep(1)\n raise RuntimeError(\"Ollama server failed to start within 30s\")\n \n def stop(self) -> None:\n \"\"\"Stop the managed Ollama server.\"\"\"\n if self.process:\n self.process.terminate()\n try:\n self.process.wait(timeout=5)\n except subprocess.TimeoutExpired:\n self.process.kill()\n self.process = None\n```\n\n### 6.3 LiteLLM + Ollama\n\nWhen the target provider is `ollama`, LiteLLM routes through Ollama's OpenAI-compatible endpoint:\n\n```python\n# Model format: \"ollama/<model_name>\"\nresponse = await litellm.acompletion(\n model=\"ollama/qwen3:32b\",\n messages=messages,\n api_base=\"http://localhost:11434\",\n stream=True,\n)\n```\n\nLiteLLM handles the translation between whatever the exposed transport format is and the Ollama-compatible OpenAI Chat format.\n\n---\n\n## 7. Server Implementation\n\n### 7.1 ASGI Application\n\n```python\n# src/adapters_proxy/server.py\n\nfrom fastapi import FastAPI, Request, HTTPException\nfrom fastapi.responses import StreamingResponse\nimport uvicorn\n\nfrom .config import ProxyConfig\nfrom .auth import verify_bearer_token\nfrom .translate import create_translator\n\ndef create_app(config: ProxyConfig) -> FastAPI:\n app = FastAPI(\n title=\"adapters-proxy\",\n description=\"Transport protocol bridge for coding agent harnesses\",\n version=\"1.0.0\",\n )\n \n translator = create_translator(config)\n \n # Mount transport-specific routes based on config\n if config.exposed_transport == \"anthropic\":\n from .transports.anthropic import create_router\n app.include_router(create_router(translator, config))\n elif config.exposed_transport == \"openai-chat\":\n from .transports.openai_chat import create_router\n app.include_router(create_router(translator, config))\n elif config.exposed_transport == \"openai-responses\":\n from .transports.openai_responses import create_router\n app.include_router(create_router(translator, config))\n elif config.exposed_transport == \"google\":\n from .transports.google import create_router\n app.include_router(create_router(translator, config))\n \n @app.get(\"/health\")\n async def health():\n return {\n \"status\": \"ok\",\n \"transport\": config.exposed_transport,\n \"provider\": config.target_provider,\n \"model\": config.target_model,\n }\n \n return app\n\n\ndef run_server(config: ProxyConfig) -> None:\n app = create_app(config)\n \n # Print startup info to stderr (stdout is reserved for structured output)\n import sys\n print(f\"[adapters-proxy] Listening on {config.host}:{config.port}\", file=sys.stderr)\n print(f\"[adapters-proxy] Transport: {config.exposed_transport} → {config.target_provider}\", file=sys.stderr)\n print(f\"[adapters-proxy] Model: {config.target_model}\", file=sys.stderr)\n \n # Print structured startup info to stdout for adapters launch to parse\n import json\n print(json.dumps({\n \"event\": \"ready\",\n \"port\": config.port,\n \"auth_token\": config.auth_token,\n \"url\": f\"http://{config.host}:{config.port}\",\n }), flush=True)\n \n uvicorn.run(\n app,\n host=config.host,\n port=config.port,\n log_level=config.log_level,\n access_log=config.log_level == \"debug\",\n )\n```\n\n### 7.2 Startup Protocol\n\nWhen spawned by `adapters launch`, the proxy communicates readiness via stdout:\n\n1. Proxy starts, binds to port\n2. Proxy prints a JSON line to stdout: `{\"event\": \"ready\", \"port\": <actual_port>, \"auth_token\": \"<token>\", \"url\": \"http://127.0.0.1:<port>\"}`\n3. `adapters launch` reads this line, extracts port and token\n4. `adapters launch` configures the harness with the proxy URL and token\n5. All subsequent proxy output goes to stderr (at configured log level)\n\nThis protocol allows `adapters launch` to use ephemeral ports (`port=0`) and receive the OS-assigned port.\n\n---\n\n## 8. CLI Interface\n\n### 8.1 Entry Points\n\n```bash\n# As Python module\npython -m adapters_proxy\n\n# As installed script\nadapters-proxy\n\n# As pip-installed command\npip install adapters-proxy && adapters-proxy\n```\n\n### 8.2 CLI Arguments\n\n```\nUsage: adapters-proxy [OPTIONS]\n\nOptions:\n --target-provider TEXT LiteLLM provider name [env: ADAPTERS_PROXY_TARGET_PROVIDER]\n --target-model TEXT LiteLLM model identifier [env: ADAPTERS_PROXY_TARGET_MODEL]\n --transport TEXT Exposed transport protocol [env: ADAPTERS_PROXY_EXPOSED_TRANSPORT]\n --port INTEGER Listen port (0=auto) [env: ADAPTERS_PROXY_PORT] [default: 0]\n --host TEXT Bind address [env: ADAPTERS_PROXY_HOST] [default: 127.0.0.1]\n --auth-token TEXT Bearer token for auth [env: ADAPTERS_PROXY_AUTH_TOKEN]\n --log-level TEXT Log level [env: ADAPTERS_PROXY_LOG_LEVEL] [default: warn]\n --timeout INTEGER Request timeout seconds [env: ADAPTERS_PROXY_TIMEOUT] [default: 600]\n --version Show version and exit\n --help Show this message and exit\n```\n\nCLI arguments take precedence over environment variables.\n\n### 8.3 Examples\n\n```bash\n# Bridge Anthropic API → Bedrock\nADAPTERS_PROXY_TARGET_PROVIDER=bedrock \\\nADAPTERS_PROXY_TARGET_MODEL=\"bedrock/anthropic.claude-sonnet-4-20250514-v1:0\" \\\nADAPTERS_PROXY_EXPOSED_TRANSPORT=anthropic \\\nAWS_REGION_NAME=us-east-1 \\\nadapters-proxy --port 8080\n\n# Bridge OpenAI Responses API → Vertex AI\nadapters-proxy \\\n --target-provider vertex_ai \\\n --target-model \"vertex_ai/claude-sonnet-4@20250514\" \\\n --transport openai-responses \\\n --port 8080\n\n# Bridge Anthropic API → local Ollama\nadapters-proxy \\\n --target-provider ollama \\\n --target-model \"ollama/qwen3:32b\" \\\n --transport anthropic \\\n --port 8080\n\n# Bridge Google GenerateContent → OpenAI\nOPENAI_API_KEY=sk-... \\\nadapters-proxy \\\n --target-provider openai \\\n --target-model \"openai/gpt-4o\" \\\n --transport google \\\n --port 8080\n```\n\n---\n\n## 9. Dependencies\n\n### 9.1 Python Requirements\n\n```toml\n# pyproject.toml\n[project]\nname = \"adapters-proxy\"\nversion = \"1.0.0\"\ndescription = \"Transport protocol bridge for coding agent harnesses\"\nrequires-python = \">=3.11\"\nlicense = { text = \"MIT\" }\nauthors = [{ name = \"A5C AI\", email = \"eng@a5c.ai\" }]\n\ndependencies = [\n \"litellm>=1.60.0\",\n \"fastapi>=0.115.0\",\n \"uvicorn[standard]>=0.32.0\",\n \"click>=8.1.0\",\n \"pydantic>=2.0.0\",\n \"httpx>=0.27.0\",\n]\n\n[project.optional-dependencies]\nollama = [\n \"ollama>=0.4.0\",\n]\ndev = [\n \"pytest>=8.0\",\n \"pytest-asyncio>=0.24\",\n \"pytest-httpx>=0.30\",\n \"ruff>=0.8.0\",\n \"mypy>=1.13\",\n \"httpx>=0.27.0\",\n]\nall = [\n \"adapters-proxy[ollama,dev]\",\n]\n\n[project.scripts]\nadapters-proxy = \"adapters_proxy.cli:main\"\n\n[build-system]\nrequires = [\"hatchling\"]\nbuild-backend = \"hatchling.build\"\n\n[tool.hatch.build.targets.wheel]\npackages = [\"src/adapters_proxy\"]\n\n[tool.ruff]\ntarget-version = \"py311\"\nline-length = 120\n\n[tool.mypy]\npython_version = \"3.11\"\nstrict = true\n\n[tool.pytest.ini_options]\ntestpaths = [\"tests\"]\nasyncio_mode = \"auto\"\n```\n\n### 9.2 Key Dependency Rationale\n\n| Dependency | Why |\n|---|---|\n| `litellm` | Core translation engine. 140+ providers, format translation, error mapping, retry logic. |\n| `fastapi` | ASGI framework for transport endpoints. Pydantic integration for request validation. |\n| `uvicorn` | High-performance ASGI server. Standard extras include `uvloop` + `httptools`. |\n| `click` | CLI framework. Consistent arg parsing with env var integration. |\n| `pydantic` | Request/response validation matching API schemas. |\n| `httpx` | HTTP client for health checks and proxy-internal communication. |\n| `ollama` | Optional. Local model management (pull, serve, list). |\n\n---\n\n## 10. CI/CD\n\n### 10.1 GitHub Actions Workflows\n\n#### `ci.yml` — Lint, Type-Check, Test\n\n```yaml\nname: CI\non:\n push:\n branches: [main]\n paths: [\"packages/adapters-proxy/**\"]\n pull_request:\n paths: [\"packages/adapters-proxy/**\"]\n\njobs:\n lint:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n - uses: actions/setup-python@v5\n with:\n python-version: \"3.11\"\n - run: pip install -e \".[dev]\"\n working-directory: packages/adapters-proxy\n - run: ruff check .\n working-directory: packages/adapters-proxy\n - run: ruff format --check .\n working-directory: packages/adapters-proxy\n - run: mypy src/\n working-directory: packages/adapters-proxy\n\n test:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n - uses: actions/setup-python@v5\n with:\n python-version: \"3.11\"\n - run: pip install -e \".[all]\"\n working-directory: packages/adapters-proxy\n - run: pytest --tb=short -q\n working-directory: packages/adapters-proxy\n```\n\n#### `publish.yml` — PyPI Release\n\n```yaml\nname: Publish\non:\n push:\n tags: [\"adapters-proxy-v*\"]\n\njobs:\n publish:\n runs-on: ubuntu-latest\n permissions:\n id-token: write # trusted publishing\n steps:\n - uses: actions/checkout@v4\n - uses: actions/setup-python@v5\n with:\n python-version: \"3.11\"\n - run: pip install build\n - run: python -m build\n working-directory: packages/adapters-proxy\n - uses: pypa/gh-action-pypi-publish@release/v1\n with:\n packages-dir: packages/adapters-proxy/dist/\n```\n\n#### `docker.yml` — Container Image\n\n```yaml\nname: Docker\non:\n push:\n tags: [\"adapters-proxy-v*\"]\n\njobs:\n docker:\n runs-on: ubuntu-latest\n permissions:\n packages: write\n steps:\n - uses: actions/checkout@v4\n - uses: docker/setup-buildx-action@v3\n - uses: docker/login-action@v3\n with:\n registry: ghcr.io\n username: ${{ github.actor }}\n password: ${{ secrets.GITHUB_TOKEN }}\n - uses: docker/build-push-action@v5\n with:\n context: packages/adapters-proxy\n push: true\n tags: ghcr.io/a5c-ai/adapters-proxy:${{ github.ref_name }},ghcr.io/a5c-ai/adapters-proxy:latest\n platforms: linux/amd64,linux/arm64\n```\n\n### 10.2 Dockerfile\n\n```dockerfile\nFROM python:3.11-slim AS base\n\nWORKDIR /app\n\n# Install dependencies first for caching\nCOPY pyproject.toml .\nRUN pip install --no-cache-dir \".[ollama]\"\n\n# Copy source\nCOPY src/ src/\n\n# Non-root user\nRUN useradd -m -r proxy && chown -R proxy:proxy /app\nUSER proxy\n\nEXPOSE 8080\n\nENTRYPOINT [\"adapters-proxy\"]\nCMD [\"--port\", \"8080\", \"--host\", \"0.0.0.0\"]\n```\n\n### 10.3 Docker Compose Example\n\n```yaml\nversion: \"3.9\"\n\nservices:\n adapters-proxy:\n build: .\n ports:\n - \"8080:8080\"\n environment:\n ADAPTERS_PROXY_TARGET_PROVIDER: bedrock\n ADAPTERS_PROXY_TARGET_MODEL: \"bedrock/anthropic.claude-sonnet-4-20250514-v1:0\"\n ADAPTERS_PROXY_EXPOSED_TRANSPORT: anthropic\n ADAPTERS_PROXY_PORT: \"8080\"\n ADAPTERS_PROXY_HOST: \"0.0.0.0\"\n AWS_REGION_NAME: us-east-1\n AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID}\n AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY}\n restart: unless-stopped\n healthcheck:\n test: [\"CMD\", \"curl\", \"-f\", \"http://localhost:8080/health\"]\n interval: 30s\n timeout: 5s\n retries: 3\n```\n\n---\n\n## 11. Testing Strategy\n\n### 11.1 Unit Tests\n\nEach transport endpoint has dedicated tests that verify request parsing, response formatting, and streaming behavior using mock LiteLLM responses.\n\n```python\n# tests/test_anthropic_transport.py\n\nimport pytest\nfrom httpx import AsyncClient\nfrom adapters_proxy.server import create_app\nfrom adapters_proxy.config import ProxyConfig\nfrom unittest.mock import patch, AsyncMock\n\n@pytest.fixture\ndef app():\n config = ProxyConfig(\n target_provider=\"openai\",\n target_model=\"openai/gpt-4o\",\n exposed_transport=\"anthropic\",\n auth_token=\"test-token\",\n )\n return create_app(config)\n\n@pytest.fixture\nasync def client(app):\n async with AsyncClient(app=app, base_url=\"http://test\") as client:\n yield client\n\nasync def test_messages_endpoint(client):\n \"\"\"Anthropic Messages request is translated and forwarded.\"\"\"\n mock_response = MockLiteLLMResponse(content=\"Hello from GPT-4o\")\n \n with patch(\"litellm.acompletion\", new_callable=AsyncMock, return_value=mock_response):\n resp = await client.post(\"/v1/messages\", json={\n \"model\": \"claude-sonnet-4-20250514\",\n \"max_tokens\": 1024,\n \"messages\": [{\"role\": \"user\", \"content\": \"Hello\"}],\n }, headers={\"X-Api-Key\": \"test-token\"})\n \n assert resp.status_code == 200\n body = resp.json()\n assert body[\"type\"] == \"message\"\n assert body[\"content\"][0][\"text\"] == \"Hello from GPT-4o\"\n\nasync def test_auth_required(client):\n \"\"\"Requests without valid auth token are rejected.\"\"\"\n resp = await client.post(\"/v1/messages\", json={\n \"model\": \"claude-sonnet-4-20250514\",\n \"max_tokens\": 1024,\n \"messages\": [{\"role\": \"user\", \"content\": \"Hello\"}],\n })\n assert resp.status_code == 401\n```\n\n### 11.2 Integration Tests\n\nIntegration tests spin up a real proxy instance and verify end-to-end behavior with mocked LiteLLM backends.\n\n### 11.3 Transport Conformance Tests\n\nEach transport has conformance tests that validate the proxy's responses match the official API spec. These test suites can be run against the real provider APIs to verify translation fidelity.\n\n---\n\n## 12. Error Handling\n\n### 12.1 Error Translation\n\nLiteLLM exceptions are translated into transport-appropriate error responses:\n\n| LiteLLM Exception | HTTP Status | Anthropic Error | OpenAI Error |\n|---|---|---|---|\n| `AuthenticationError` | 401 | `authentication_error` | `invalid_api_key` |\n| `RateLimitError` | 429 | `rate_limit_error` | `rate_limit_exceeded` |\n| `BadRequestError` | 400 | `invalid_request_error` | `invalid_request_error` |\n| `NotFoundError` | 404 | `not_found_error` | `model_not_found` |\n| `APIError` | 500 | `api_error` | `server_error` |\n| `Timeout` | 408 | `timeout_error` | `timeout` |\n| `ServiceUnavailableError` | 503 | `overloaded_error` | `server_error` |\n\n### 12.2 Proxy-Specific Errors\n\n| Condition | HTTP Status | Body |\n|---|---|---|\n| Invalid auth token | 401 | `{\"error\": {\"type\": \"authentication_error\", \"message\": \"Invalid bearer token\"}}` |\n| Proxy misconfigured | 500 | `{\"error\": {\"type\": \"proxy_error\", \"message\": \"...\"}}` |\n| Ollama model not found | 404 | `{\"error\": {\"type\": \"not_found_error\", \"message\": \"Model not available locally\"}}` |\n| Ollama server down | 503 | `{\"error\": {\"type\": \"service_unavailable\", \"message\": \"Ollama server not reachable\"}}` |\n\n---\n\n## 13. End-to-End Examples\n\n### 13.1 Claude Code via Bedrock\n\n```bash\n# The full adapters launch command\nadapters launch claude bedrock \\\n --region us-east-1 \\\n --model anthropic.claude-sonnet-4-20250514-v1:0\n\n# What happens internally:\n# 1. adapters detects claude supports bedrock natively (CLAUDE_CODE_USE_BEDROCK)\n# 2. No proxy needed\n# 3. Spawns: CLAUDE_CODE_USE_BEDROCK=1 AWS_REGION=us-east-1 claude\n```\n\n### 13.2 Codex via Bedrock (Proxy Required)\n\n```bash\n# The full adapters launch command\nadapters launch codex bedrock \\\n --region us-east-1 \\\n --model anthropic.claude-sonnet-4-20250514-v1:0 \\\n --with-proxy-if-needed\n\n# What happens internally:\n# 1. adapters detects codex does NOT support bedrock natively\n# 2. Codex speaks openai-responses, bedrock speaks anthropic → proxy needed\n# 3. Spawns proxy: ADAPTERS_PROXY_TARGET_PROVIDER=bedrock\n# ADAPTERS_PROXY_TARGET_MODEL=bedrock/anthropic.claude-sonnet-4-20250514-v1:0\n# ADAPTERS_PROXY_EXPOSED_TRANSPORT=openai-responses\n# AWS_REGION_NAME=us-east-1\n# adapters-proxy --port 0\n# 4. Proxy reports: {\"event\":\"ready\",\"port\":54321,\"auth_token\":\"uuid-xxx\"}\n# 5. Spawns codex: OPENAI_BASE_URL=http://127.0.0.1:54321\n# OPENAI_API_KEY=uuid-xxx\n# codex\n```\n\n### 13.3 Gemini via Anthropic API (Proxy Required)\n\n```bash\nadapters launch gemini anthropic \\\n --api-key $ANTHROPIC_API_KEY \\\n --model claude-sonnet-4-20250514 \\\n --with-proxy-if-needed\n\n# 1. Gemini speaks google, Anthropic API speaks anthropic → proxy needed\n# 2. Proxy: anthropic → google translation\n# 3. Gemini gets: CODE_ASSIST_ENDPOINT=http://127.0.0.1:<port>\n```\n\n### 13.4 Claude Code via Local Ollama\n\n```bash\nadapters launch claude ollama \\\n --model qwen3:32b \\\n --with-proxy-if-needed\n\n# 1. Claude can use ANTHROPIC_BASE_URL but Ollama speaks openai-chat, not anthropic\n# 2. Proxy: ollama → anthropic translation\n# 3. Proxy auto-pulls qwen3:32b if not available\n# 4. Claude gets: ANTHROPIC_BASE_URL=http://127.0.0.1:<port>\n```\n\n### 13.5 Standalone Proxy (Persistent Service)\n\n```bash\n# Run as a persistent Anthropic-to-Bedrock bridge\nADAPTERS_PROXY_TARGET_PROVIDER=bedrock \\\nADAPTERS_PROXY_TARGET_MODEL=\"bedrock/anthropic.claude-sonnet-4-20250514-v1:0\" \\\nADAPTERS_PROXY_EXPOSED_TRANSPORT=anthropic \\\nADAPTERS_PROXY_PORT=8080 \\\nADAPTERS_PROXY_HOST=0.0.0.0 \\\nADAPTERS_PROXY_AUTH_TOKEN=my-secret-token \\\nAWS_REGION_NAME=us-east-1 \\\nadapters-proxy\n\n# Then point any Anthropic-speaking tool at it:\nANTHROPIC_BASE_URL=http://proxy-host:8080 \\\nANTHROPIC_API_KEY=my-secret-token \\\nclaude -p \"hello\"\n```\n\n---\n\n## 14. Monorepo Integration\n\nThe `adapters-proxy` package lives at `packages/adapters-proxy/` within the adapters monorepo. It is a Python package in a Node.js monorepo, so:\n\n- It has its own `pyproject.toml` (not managed by npm workspaces)\n- It has its own CI workflows (triggered by path filters)\n- It is published to PyPI independently\n- The `@a5c-ai/adapters-cli` package lists `adapters-proxy` as a suggested dependency with install guidance\n- `adapters launch --with-proxy-if-needed` detects `adapters-proxy` availability via `which adapters-proxy` or `python -m adapters_proxy --version`\n\n### 14.1 Installation\n\n```bash\n# Standalone\npip install adapters-proxy\n\n# With Ollama support\npip install \"adapters-proxy[ollama]\"\n\n# From source (development)\ncd packages/adapters-proxy\npip install -e \".[dev]\"\n```\n\n---\n\n## 15. Future Considerations\n\n### 15.1 Caching\n\nAdd response caching (exact match on messages + model) for development workflows where the same prompt is re-run. Use `ADAPTERS_PROXY_CACHE=true` with a SQLite or file-based cache.\n\n### 15.2 Cost Tracking\n\nLiteLLM provides cost data in responses. The proxy could aggregate and report costs via `/metrics` endpoint or structured logs.\n\n### 15.3 Multi-Model Routing\n\nA future version could support LiteLLM Router for load balancing across multiple model deployments (e.g., primary Bedrock + fallback Vertex).\n\n### 15.4 WebSocket Transport\n\nAdd WebSocket support for the Responses API transport (`ws://host/v1/responses`) for lower-latency persistent connections.\n\n### 15.5 MCP Passthrough\n\nSome transports may need to forward MCP-related endpoints. This is not needed in v1.0 since MCP is handled by the harness directly, not through the LLM API.\n",
"documents": []
},
"outgoingEdges": [],
"incomingEdges": [
{
"from": "page:docs-adapters-archive-design",
"to": "page:docs-adapters-archive-design-provider-adapter",
"kind": "contains_page"
}
]
}