|
| 1 | +# @computeragent/llm-proxy-openai |
| 2 | + |
| 3 | +Anthropic Messages ↔ OpenAI Chat Completions **translator proxy**. |
| 4 | + |
| 5 | +Accepts `POST /v1/messages` in Anthropic's Messages format and forwards to any OpenAI-Chat-Completions–compatible endpoint, translating both the request and the response (including streaming and tool calls). |
| 6 | + |
| 7 | +Used by ComputerAgent so that engines whose underlying SDKs speak Anthropic Messages — `claude-agent-sdk` and `deepagents` (via `ChatAnthropic`) — can target OpenAI-compat backends like Lyzr Studio, vLLM, LiteLLM, Together, Ollama, etc. |
| 8 | + |
| 9 | +`gitagent` does **not** need this proxy — gitclaw natively speaks both protocols via `GITCLAW_MODEL_BASE_URL` + `provider:model@baseUrl` syntax. |
| 10 | + |
| 11 | +## Run as a CLI |
| 12 | + |
| 13 | +```bash |
| 14 | +UPSTREAM_BASE=https://your-openai-compat-host \ |
| 15 | +UPSTREAM_PATH=/v1/chat/completions \ |
| 16 | +UPSTREAM_TOKEN=sk-... \ |
| 17 | +PORT=8788 \ |
| 18 | + npx @computeragent/llm-proxy-openai |
| 19 | +``` |
| 20 | + |
| 21 | +Then point your ComputerAgent run at the proxy: |
| 22 | + |
| 23 | +```bash |
| 24 | +curl -N -X POST http://127.0.0.1:18800/run \ |
| 25 | + -H 'content-type: application/json' -H 'accept: text/event-stream' \ |
| 26 | + -d '{ |
| 27 | + "source": "github.com/<org>/<gap-repo>", |
| 28 | + "harness": "claude-agent-sdk", |
| 29 | + "envs": { |
| 30 | + "ANTHROPIC_BASE_URL": "http://127.0.0.1:8788", |
| 31 | + "ANTHROPIC_API_KEY": "via-proxy" |
| 32 | + }, |
| 33 | + "message": "Reply: PING" |
| 34 | + }' |
| 35 | +``` |
| 36 | + |
| 37 | +## Run programmatically |
| 38 | + |
| 39 | +```ts |
| 40 | +import { startProxy } from "@computeragent/llm-proxy-openai"; |
| 41 | + |
| 42 | +const proxy = await startProxy({ |
| 43 | + port: 8788, |
| 44 | + upstream: { |
| 45 | + base: "https://agent-dev.test.studio.lyzr.ai", |
| 46 | + path: "/v4/chat/completions", |
| 47 | + token: process.env.LYZR_TOKEN!, |
| 48 | + modelOverride: "697a4a76496e0831bdde546c", // optional; override client model |
| 49 | + }, |
| 50 | +}); |
| 51 | + |
| 52 | +// … |
| 53 | +await proxy.close(); |
| 54 | +``` |
| 55 | + |
| 56 | +## Environment variables (CLI) |
| 57 | + |
| 58 | +| Var | Required | Default | Notes | |
| 59 | +|---|---|---|---| |
| 60 | +| `UPSTREAM_BASE` | ✓ | — | Origin only, e.g. `https://your-host` | |
| 61 | +| `UPSTREAM_TOKEN` | ✓ | — | Bearer token for the upstream | |
| 62 | +| `UPSTREAM_PATH` | | `/v1/chat/completions` | Some hosts use `/v4/chat/completions` | |
| 63 | +| `UPSTREAM_MODEL` | | — | Force this model on every upstream request (overrides client's Anthropic `model`) | |
| 64 | +| `UPSTREAM_AUTH_SCHEME` | | `Bearer` | Replace with e.g. `Token` if your backend wants something else | |
| 65 | +| `PORT` | | `8788` | Bind port | |
| 66 | +| `FORWARD_MAX_TOKENS` | | `0` | Set to `1` to forward `max_tokens` — some backends (Lyzr) blank the response when this is set, so it's off by default | |
| 67 | + |
| 68 | +## What the proxy translates |
| 69 | + |
| 70 | +| Direction | Anthropic shape | OpenAI shape | |
| 71 | +|---|---|---| |
| 72 | +| Request | `system` field | first `system` message | |
| 73 | +| Request | `messages[].content` text blocks | `messages[].content` strings | |
| 74 | +| Request | assistant `tool_use` blocks | `messages[].tool_calls` | |
| 75 | +| Request | user `tool_result` blocks | `messages[]` with `role: "tool"` + `tool_call_id` | |
| 76 | +| Request | `tools[]` (Anthropic `input_schema`) | `tools[]` (OpenAI `function.parameters`) | |
| 77 | +| Request | `tool_choice: {type:"auto" \| "any" \| "tool"}` | `tool_choice: "auto" \| "required" \| {type:"function",function:{name}}` | |
| 78 | +| Response | `id`, `usage`, content blocks | reassembled from `choices[0].message` | |
| 79 | +| Response | `tool_use` content blocks | from `tool_calls` | |
| 80 | +| Response | `stop_reason: "tool_use" \| "max_tokens" \| "end_turn"` | mapped from `finish_reason` | |
| 81 | +| Streaming | `message_start` → `content_block_start/delta/stop` → `message_delta` → `message_stop` | reassembled from `chat.completion.chunk` deltas (text + `tool_calls.function.arguments` partial JSON) | |
| 82 | + |
| 83 | +## Endpoints |
| 84 | + |
| 85 | +- `GET /health` → `{ ok: true, upstream }` |
| 86 | +- `POST /v1/messages` → translated and forwarded to `<UPSTREAM_BASE><UPSTREAM_PATH>` |
| 87 | + |
| 88 | +Anything else → `404`. |
| 89 | + |
| 90 | +## Limitations |
| 91 | + |
| 92 | +- **Text + tool calls only.** No image inputs, no audio. Anthropic's content blocks for those types are passed through as JSON strings if encountered, which most OpenAI-compat backends will reject. |
| 93 | +- **No retry / circuit-breaker.** A single upstream timeout fails the request. |
| 94 | +- **Stateless.** No request-id correlation, no metrics export. Bring your own observability. |
| 95 | +- **`session_id` is informational.** The proxy adds it to outgoing OpenAI requests for backends that track it (e.g. Lyzr Studio), but it's a string derived from the timestamp — proper session continuity comes from the client replaying `messages[]`, not from server-side state in the upstream. |
| 96 | + |
| 97 | +## Verified compatibility |
| 98 | + |
| 99 | +| Upstream | Backend | Tool calls round-trip? | |
| 100 | +|---|---|---| |
| 101 | +| Lyzr Studio (`/v4/chat/completions`) | claude-agent-sdk | ✓ | |
| 102 | +| Lyzr Studio (`/v4/chat/completions`) | deepagents | ✓ | |
0 commit comments