Skip to content

Commit db5cec4

Browse files
kapaleshreyasclaude
andcommitted
feat(llm-proxy-openai): new package — Anthropic ↔ OpenAI Chat Completions translator
Lets ComputerAgent target any OpenAI-Chat-Completions–compatible backend (Lyzr Studio, vLLM, LiteLLM, Together, Ollama, etc.) without engine code changes. Engines whose SDKs speak Anthropic Messages internally — claude-agent-sdk and deepagents (via LangChain ChatAnthropic) — only need ANTHROPIC_BASE_URL pointed at the proxy. gitagent doesn't need this proxy because gitclaw natively speaks both Anthropic and OpenAI via GITCLAW_MODEL_BASE_URL + provider:model@baseUrl syntax. Bidirectional translation covers: - request: system field, content blocks (text + tool_use + tool_result), tools (Anthropic input_schema → OpenAI function.parameters), tool_choice (auto/any/specific-tool) - response: text + tool_calls, finish_reason mapping - streaming: full Anthropic event sequence (message_start → content_block_start/delta/stop → message_delta → message_stop) with text_delta + input_json_delta for tool call argument streaming Exposes both a CLI (npx @computeragent/llm-proxy-openai with env vars) and a programmatic startProxy() API returning a handle with .close(). Default `max_tokens` is dropped on the upstream request — Lyzr Studio blanks the response when it's set; opt back in with FORWARD_MAX_TOKENS=1 for backends that need it. End-to-end verified: claude-agent-sdk + deepagents both wrote real files via Write/write_file tool calls round-tripped through the proxy → Lyzr → back. Lives in /tmp during development; this commit promotes it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 1d61af1 commit db5cec4

7 files changed

Lines changed: 769 additions & 0 deletions

File tree

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
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 ||
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
{
2+
"name": "@computeragent/llm-proxy-openai",
3+
"version": "0.1.0",
4+
"description": "Translator proxy: accepts POST /v1/messages (Anthropic Messages format) and forwards to /v1/chat/completions (OpenAI-compat). Lets engines that speak Anthropic Messages (claude-agent-sdk, deepagents) target any OpenAI-Chat-Completions backend — Lyzr Studio, vLLM, LiteLLM, Together, etc.",
5+
"license": "MIT",
6+
"type": "module",
7+
"main": "./dist/index.js",
8+
"types": "./dist/index.d.ts",
9+
"bin": {
10+
"anth-to-openai-proxy": "./dist/cli.js"
11+
},
12+
"exports": {
13+
".": {
14+
"types": "./dist/index.d.ts",
15+
"import": "./dist/index.js"
16+
}
17+
},
18+
"files": [
19+
"dist",
20+
"src",
21+
"README.md"
22+
],
23+
"scripts": {
24+
"build": "tsc -p tsconfig.json",
25+
"typecheck": "tsc -p tsconfig.json --noEmit",
26+
"test": "vitest run --passWithNoTests",
27+
"clean": "rm -rf dist .turbo *.tsbuildinfo",
28+
"start": "node dist/cli.js"
29+
},
30+
"dependencies": {},
31+
"devDependencies": {
32+
"@types/node": "^22.0.0",
33+
"typescript": "^5.5.0",
34+
"vitest": "^2.0.0"
35+
},
36+
"keywords": [
37+
"anthropic",
38+
"openai",
39+
"proxy",
40+
"llm",
41+
"computeragent",
42+
"lyzr"
43+
],
44+
"homepage": "https://github.com/open-gitagent/ComputerAgent",
45+
"repository": {
46+
"type": "git",
47+
"url": "https://github.com/open-gitagent/ComputerAgent.git",
48+
"directory": "packages/llm-proxy-openai"
49+
}
50+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
#!/usr/bin/env node
2+
/**
3+
* CLI for `@computeragent/llm-proxy-openai`.
4+
*
5+
* Wires env vars to programmatic options + handles SIGINT/SIGTERM cleanly.
6+
*
7+
* UPSTREAM_BASE=https://your-host \
8+
* UPSTREAM_PATH=/v1/chat/completions \
9+
* UPSTREAM_TOKEN=sk-... \
10+
* UPSTREAM_MODEL=optional-model-override \
11+
* PORT=8788 \
12+
* anth-to-openai-proxy
13+
*
14+
* Or via `npx @computeragent/llm-proxy-openai`.
15+
*/
16+
import { startProxy } from "./proxy.js";
17+
18+
const port = Number(process.env.PORT ?? 8788);
19+
const upstreamBase = process.env.UPSTREAM_BASE;
20+
const upstreamToken = process.env.UPSTREAM_TOKEN;
21+
22+
if (!upstreamBase) {
23+
console.error("ERROR: UPSTREAM_BASE is required (e.g. https://agent-dev.test.studio.lyzr.ai)");
24+
process.exit(2);
25+
}
26+
if (!upstreamToken) {
27+
console.error("ERROR: UPSTREAM_TOKEN is required (Bearer token for the upstream)");
28+
process.exit(2);
29+
}
30+
31+
const handle = await startProxy({
32+
port,
33+
upstream: {
34+
base: upstreamBase,
35+
path: process.env.UPSTREAM_PATH,
36+
token: upstreamToken,
37+
...(process.env.UPSTREAM_MODEL ? { modelOverride: process.env.UPSTREAM_MODEL } : {}),
38+
...(process.env.UPSTREAM_AUTH_SCHEME ? { authScheme: process.env.UPSTREAM_AUTH_SCHEME } : {}),
39+
},
40+
forwardMaxTokens: process.env.FORWARD_MAX_TOKENS === "1",
41+
});
42+
43+
const shutdown = async (sig: string) => {
44+
console.error(`[proxy] received ${sig}, shutting down`);
45+
await handle.close().catch(() => {});
46+
process.exit(0);
47+
};
48+
process.on("SIGINT", () => void shutdown("SIGINT"));
49+
process.on("SIGTERM", () => void shutdown("SIGTERM"));
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/**
2+
* `@computeragent/llm-proxy-openai` — Anthropic Messages ↔ OpenAI Chat
3+
* Completions translator. Accepts the Anthropic Messages wire shape that
4+
* `claude-agent-sdk` and `deepagents` speak natively, and forwards to any
5+
* OpenAI-Chat-Completions endpoint (Lyzr Studio, vLLM, LiteLLM, Together,
6+
* Ollama, etc.).
7+
*
8+
* Two ways to use:
9+
*
10+
* # CLI:
11+
* UPSTREAM_BASE=https://your-openai-compat-host \
12+
* UPSTREAM_PATH=/v1/chat/completions \
13+
* UPSTREAM_TOKEN=sk-... \
14+
* PORT=8788 \
15+
* npx anth-to-openai-proxy
16+
*
17+
* # Programmatic:
18+
* import { startProxy } from "@computeragent/llm-proxy-openai";
19+
* const proxy = await startProxy({
20+
* port: 8788,
21+
* upstream: {
22+
* base: "https://your-openai-compat-host",
23+
* path: "/v1/chat/completions",
24+
* token: "sk-...",
25+
* },
26+
* });
27+
* // … later:
28+
* await proxy.close();
29+
*
30+
* Then point any ComputerAgent run at the proxy:
31+
*
32+
* envs: {
33+
* ANTHROPIC_BASE_URL: "http://127.0.0.1:8788",
34+
* ANTHROPIC_API_KEY: "via-proxy", // anything; the upstream token is in the proxy
35+
* }
36+
*
37+
* Tool calls round-trip in both directions — verified end-to-end against
38+
* Lyzr Studio with claude-agent-sdk + deepagents both writing real files
39+
* through their tool surfaces.
40+
*/
41+
export { startProxy } from "./proxy.js";
42+
export type { ProxyOptions, ProxyHandle, UpstreamConfig } from "./proxy.js";

0 commit comments

Comments
 (0)