agentrelay-cli 0.5.1__py3-none-any.whl
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.
- agentrelay_cli-0.5.1.dist-info/METADATA +233 -0
- agentrelay_cli-0.5.1.dist-info/RECORD +9 -0
- agentrelay_cli-0.5.1.dist-info/WHEEL +4 -0
- agentrelay_cli-0.5.1.dist-info/entry_points.txt +3 -0
- agentrelay_cli-0.5.1.dist-info/licenses/LICENSE +21 -0
- claude_relay/__init__.py +3 -0
- claude_relay/__main__.py +93 -0
- claude_relay/server.py +856 -0
- claude_relay/service.py +233 -0
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agentrelay-cli
|
|
3
|
+
Version: 0.5.1
|
|
4
|
+
Summary: OpenAI- and Anthropic-compatible API server that routes through agent CLIs
|
|
5
|
+
License: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Requires-Dist: fastapi>=0.115
|
|
9
|
+
Requires-Dist: uvicorn>=0.34
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
12
|
+
# agent-relay
|
|
13
|
+
|
|
14
|
+
[](https://github.com/npow/claude-relay/actions/workflows/ci.yml)
|
|
15
|
+
[](https://pypi.org/project/agentrelay-cli/)
|
|
16
|
+
[](LICENSE)
|
|
17
|
+
[](https://www.python.org/downloads/)
|
|
18
|
+
|
|
19
|
+
Drop-in OpenAI **and Anthropic** API server that routes through agent CLIs (currently [Claude Code](https://docs.anthropic.com/en/docs/claude-code)).
|
|
20
|
+
|
|
21
|
+
> Compatibility note: `claude-relay` remains available as a compatibility package/command alias.
|
|
22
|
+
|
|
23
|
+
## Why
|
|
24
|
+
|
|
25
|
+
You have tools that speak the OpenAI or Anthropic API. You have Claude Code with its tools, MCP servers, and agentic capabilities. **agent-relay** bridges the two — point any compatible client at it and every request flows through `claude -p` under the hood.
|
|
26
|
+
|
|
27
|
+
- **Use Claude Code from any OpenAI or Anthropic client** — Cursor, Continue, aider, LangChain, custom scripts
|
|
28
|
+
- **Keep Claude Code's superpowers** — tool use, MCP servers, file access, shell execution
|
|
29
|
+
- **Zero config** — if `claude` works on your machine, so does this
|
|
30
|
+
- **Real token usage** — reports actual token counts from Claude (not zeros)
|
|
31
|
+
- **Token-level streaming** — uses `--include-partial-messages` for true real-time deltas
|
|
32
|
+
|
|
33
|
+
## Install
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
# With uv (recommended)
|
|
37
|
+
uvx agent-relay serve
|
|
38
|
+
|
|
39
|
+
# Or install globally
|
|
40
|
+
uv tool install agentrelay-cli
|
|
41
|
+
agent-relay serve
|
|
42
|
+
|
|
43
|
+
# Or from source
|
|
44
|
+
git clone https://github.com/npow/claude-relay.git
|
|
45
|
+
cd claude-relay
|
|
46
|
+
uv sync
|
|
47
|
+
uv run agent-relay serve
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Quick start
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
agent-relay serve
|
|
54
|
+
# Server starts on http://localhost:18082
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Run as background service (macOS)
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
# Install and auto-start on login
|
|
61
|
+
agent-relay service install
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
The installer will offer to add these to your `~/.zshrc` (or `~/.bashrc`) so every SDK and agent picks up the relay automatically:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
export ANTHROPIC_BASE_URL="http://127.0.0.1:18082"
|
|
68
|
+
export OPENAI_BASE_URL="http://127.0.0.1:18082/v1"
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
# Check status
|
|
73
|
+
agent-relay service status
|
|
74
|
+
|
|
75
|
+
# Update
|
|
76
|
+
uv tool upgrade agentrelay-cli
|
|
77
|
+
agent-relay service restart
|
|
78
|
+
|
|
79
|
+
# Stop and remove
|
|
80
|
+
agent-relay service uninstall
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Point any OpenAI-compatible client at it:
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
from openai import OpenAI
|
|
87
|
+
|
|
88
|
+
client = OpenAI(base_url="http://localhost:18082/v1", api_key="unused")
|
|
89
|
+
|
|
90
|
+
# Streaming
|
|
91
|
+
for chunk in client.chat.completions.create(
|
|
92
|
+
model="sonnet",
|
|
93
|
+
messages=[{"role": "user", "content": "Hello!"}],
|
|
94
|
+
stream=True,
|
|
95
|
+
):
|
|
96
|
+
print(chunk.choices[0].delta.content or "", end="")
|
|
97
|
+
|
|
98
|
+
# Non-streaming
|
|
99
|
+
resp = client.chat.completions.create(
|
|
100
|
+
model="sonnet",
|
|
101
|
+
messages=[{"role": "user", "content": "Hello!"}],
|
|
102
|
+
)
|
|
103
|
+
print(resp.choices[0].message.content)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Anthropic SDK
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
import anthropic
|
|
110
|
+
|
|
111
|
+
# Just set the base URL — the SDK reads ANTHROPIC_BASE_URL automatically
|
|
112
|
+
# export ANTHROPIC_BASE_URL=http://localhost:18082
|
|
113
|
+
client = anthropic.Anthropic(base_url="http://localhost:18082")
|
|
114
|
+
|
|
115
|
+
# Streaming
|
|
116
|
+
with client.messages.stream(
|
|
117
|
+
model="sonnet",
|
|
118
|
+
max_tokens=1024,
|
|
119
|
+
messages=[{"role": "user", "content": "Hello!"}],
|
|
120
|
+
) as stream:
|
|
121
|
+
for text in stream.text_stream:
|
|
122
|
+
print(text, end="")
|
|
123
|
+
|
|
124
|
+
# Non-streaming
|
|
125
|
+
resp = client.messages.create(
|
|
126
|
+
model="sonnet",
|
|
127
|
+
max_tokens=1024,
|
|
128
|
+
messages=[{"role": "user", "content": "Hello!"}],
|
|
129
|
+
)
|
|
130
|
+
print(resp.content[0].text)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### LangChain
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
from langchain_anthropic import ChatAnthropic
|
|
137
|
+
|
|
138
|
+
# export ANTHROPIC_BASE_URL=http://localhost:18082
|
|
139
|
+
llm = ChatAnthropic(model="sonnet")
|
|
140
|
+
print(llm.invoke("Hello!").content)
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### curl
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
# OpenAI format
|
|
147
|
+
curl http://localhost:18082/v1/chat/completions \
|
|
148
|
+
-H "Content-Type: application/json" \
|
|
149
|
+
-d '{"model":"sonnet","messages":[{"role":"user","content":"Hello"}],"stream":true}'
|
|
150
|
+
|
|
151
|
+
# OpenAI Responses format
|
|
152
|
+
curl http://localhost:18082/v1/responses \
|
|
153
|
+
-H "Content-Type: application/json" \
|
|
154
|
+
-d '{"model":"sonnet","input":"Hello"}'
|
|
155
|
+
|
|
156
|
+
# Anthropic format
|
|
157
|
+
curl http://localhost:18082/v1/messages \
|
|
158
|
+
-H "Content-Type: application/json" \
|
|
159
|
+
-d '{"model":"sonnet","max_tokens":1024,"messages":[{"role":"user","content":"Hello"}]}'
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## Configuration
|
|
163
|
+
|
|
164
|
+
```
|
|
165
|
+
agent-relay serve [--host HOST] [--port PORT]
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
| Flag | Default | Description |
|
|
169
|
+
|---|---|---|
|
|
170
|
+
| `--host` | `0.0.0.0` | Bind address |
|
|
171
|
+
| `--port` | `18082` | Bind port |
|
|
172
|
+
|
|
173
|
+
## API
|
|
174
|
+
|
|
175
|
+
| Endpoint | Method | Description |
|
|
176
|
+
|---|---|---|
|
|
177
|
+
| `/v1/chat/completions` | POST | Chat completions (OpenAI-compatible) |
|
|
178
|
+
| `/v1/responses` | POST | Responses API (OpenAI-compatible) |
|
|
179
|
+
| `/v1/messages` | POST | Messages (Anthropic-compatible) |
|
|
180
|
+
| `/v1/models` | GET | List available models |
|
|
181
|
+
| `/health` | GET | Server and CLI status |
|
|
182
|
+
|
|
183
|
+
All endpoints also work without the `/v1` prefix. CORS is enabled for all origins.
|
|
184
|
+
|
|
185
|
+
### Supported features
|
|
186
|
+
|
|
187
|
+
| Feature | Status |
|
|
188
|
+
|---|---|
|
|
189
|
+
| Streaming (SSE) | Yes |
|
|
190
|
+
| System messages | Yes (via `--system-prompt`) |
|
|
191
|
+
| Multi-turn conversations | Yes |
|
|
192
|
+
| Multimodal (text parts) | Yes |
|
|
193
|
+
| Model selection | Yes |
|
|
194
|
+
| Token usage reporting | Yes |
|
|
195
|
+
| CORS | Yes |
|
|
196
|
+
|
|
197
|
+
### Models
|
|
198
|
+
|
|
199
|
+
Pass any model name — it goes directly to `claude --model`:
|
|
200
|
+
|
|
201
|
+
| Model | Description |
|
|
202
|
+
|---|---|
|
|
203
|
+
| `opus` | Most capable |
|
|
204
|
+
| `sonnet` | Balanced (default) |
|
|
205
|
+
| `haiku` | Fastest |
|
|
206
|
+
|
|
207
|
+
## Limitations
|
|
208
|
+
|
|
209
|
+
- `temperature`, `max_tokens`, `top_p`, and other sampling parameters are ignored (Claude Code CLI does not expose them)
|
|
210
|
+
- No tool/function calling passthrough (Claude Code uses its own tools internally, but they aren't exposed via the OpenAI tool-calling protocol)
|
|
211
|
+
- Each request spawns a new `claude` process (~2-3s overhead on top of API latency)
|
|
212
|
+
- No image/audio content forwarding — only text parts of multimodal messages are extracted
|
|
213
|
+
|
|
214
|
+
## How it works
|
|
215
|
+
|
|
216
|
+
```
|
|
217
|
+
OpenAI client ─┐
|
|
218
|
+
├→ claude-relay → claude -p → Anthropic API
|
|
219
|
+
Anthropic client ─┘ (FastAPI) (stream-json)
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
Each request spawns a `claude -p` process with `--output-format stream-json --include-partial-messages`. The proxy translates between the OpenAI or Anthropic wire format and Claude Code's streaming JSON protocol. Requests are stateless — no conversation history bleeds between calls.
|
|
223
|
+
|
|
224
|
+
## Development
|
|
225
|
+
|
|
226
|
+
```bash
|
|
227
|
+
uv sync
|
|
228
|
+
uv run pytest tests/ -v
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
## License
|
|
232
|
+
|
|
233
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
claude_relay/__init__.py,sha256=j-jSCPmUIKrgyLFLndirleNcsiMDAblpOXe0Nk3qD4s,102
|
|
2
|
+
claude_relay/__main__.py,sha256=wWlLVucqVV_aaOSTH2-O-6UQtQvjFi0GLTK_j900fOo,3656
|
|
3
|
+
claude_relay/server.py,sha256=0t-J5yDVqvGbfUZRRJoow2RGQ9nCh6eOQA1JDI22BP8,30437
|
|
4
|
+
claude_relay/service.py,sha256=or5K5DU-jC_CTGKybmzG-_TglpRo-pxGm0LKBiqjCdc,7039
|
|
5
|
+
agentrelay_cli-0.5.1.dist-info/METADATA,sha256=U7bNzHXoj_7YXJTr6g2PZOZJzWSCpIIgOeafsQTOEcA,6632
|
|
6
|
+
agentrelay_cli-0.5.1.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
7
|
+
agentrelay_cli-0.5.1.dist-info/entry_points.txt,sha256=K07kg2FzlhDrv7158kblVE4CHVd9OxcAIRvWlATsfWM,101
|
|
8
|
+
agentrelay_cli-0.5.1.dist-info/licenses/LICENSE,sha256=ptZw4XxMsq-0L4naAIqBDTgCXvWh1Ut4oARyxdD9HB4,1061
|
|
9
|
+
agentrelay_cli-0.5.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 npow
|
|
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.
|
claude_relay/__init__.py
ADDED
claude_relay/__main__.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""Entry point for `python -m claude_relay` and the `agent-relay` CLI command."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
DEFAULT_PORT = 18082
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _cpu_count() -> int:
|
|
11
|
+
"""Return the number of usable CPUs (respects cgroup / affinity masks)."""
|
|
12
|
+
try:
|
|
13
|
+
return len(os.sched_getaffinity(0))
|
|
14
|
+
except (AttributeError, OSError):
|
|
15
|
+
return os.cpu_count() or 1
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def main():
|
|
19
|
+
parser = argparse.ArgumentParser(prog="agent-relay", description="OpenAI-compatible API server for agent CLIs")
|
|
20
|
+
sub = parser.add_subparsers(dest="command")
|
|
21
|
+
|
|
22
|
+
# --- serve (default) ---
|
|
23
|
+
serve_p = sub.add_parser("serve", help="Start the relay server")
|
|
24
|
+
serve_p.add_argument("--host", default="0.0.0.0", help="Bind host (default: 0.0.0.0)")
|
|
25
|
+
serve_p.add_argument("--port", type=int, default=DEFAULT_PORT, help=f"Bind port (default: {DEFAULT_PORT})")
|
|
26
|
+
serve_p.add_argument("--max-concurrent", type=int, default=10, help="Max concurrent subprocess requests per worker (default: 10)")
|
|
27
|
+
serve_p.add_argument("--request-timeout", type=float, default=300, help="Per-request timeout in seconds (default: 300)")
|
|
28
|
+
serve_p.add_argument("--workers", type=int, default=_cpu_count(), help="Number of uvicorn workers (default: CPU count)")
|
|
29
|
+
serve_p.add_argument(
|
|
30
|
+
"--backend",
|
|
31
|
+
choices=["claude", "codex"],
|
|
32
|
+
default=os.environ.get("AGENT_RELAY_BACKEND", "claude"),
|
|
33
|
+
help="Backend CLI to execute (default: claude). Codex is reserved for future adapter support.",
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
# --- service management ---
|
|
37
|
+
svc_p = sub.add_parser("service", help="Manage background service (macOS launchd)")
|
|
38
|
+
svc_sub = svc_p.add_subparsers(dest="action")
|
|
39
|
+
install_p = svc_sub.add_parser("install", help="Install and start the launchd service")
|
|
40
|
+
install_p.add_argument("--port", type=int, default=DEFAULT_PORT, help=f"Port for the service (default: {DEFAULT_PORT})")
|
|
41
|
+
install_p.add_argument("--host", default="127.0.0.1", help="Bind host for the service (default: 127.0.0.1)")
|
|
42
|
+
install_p.add_argument(
|
|
43
|
+
"--backend",
|
|
44
|
+
choices=["claude", "codex"],
|
|
45
|
+
default=os.environ.get("AGENT_RELAY_BACKEND", "claude"),
|
|
46
|
+
help="Backend CLI to execute (default: claude).",
|
|
47
|
+
)
|
|
48
|
+
svc_sub.add_parser("restart", help="Restart the launchd service")
|
|
49
|
+
svc_sub.add_parser("uninstall", help="Stop and remove the launchd service")
|
|
50
|
+
svc_sub.add_parser("status", help="Show service status")
|
|
51
|
+
|
|
52
|
+
args = parser.parse_args()
|
|
53
|
+
|
|
54
|
+
# Default to serve when no subcommand given.
|
|
55
|
+
if args.command is None:
|
|
56
|
+
args = parser.parse_args(["serve"])
|
|
57
|
+
|
|
58
|
+
if args.command == "serve":
|
|
59
|
+
import uvicorn
|
|
60
|
+
|
|
61
|
+
os.environ["CLAUDE_RELAY_MAX_CONCURRENT"] = str(args.max_concurrent)
|
|
62
|
+
os.environ["CLAUDE_RELAY_REQUEST_TIMEOUT"] = str(args.request_timeout)
|
|
63
|
+
os.environ["AGENT_RELAY_BACKEND"] = args.backend
|
|
64
|
+
|
|
65
|
+
uvicorn.run(
|
|
66
|
+
"claude_relay.server:app",
|
|
67
|
+
host=args.host,
|
|
68
|
+
port=args.port,
|
|
69
|
+
workers=args.workers,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
elif args.command == "service":
|
|
73
|
+
from claude_relay.service import service_install, service_restart, service_status, service_uninstall
|
|
74
|
+
|
|
75
|
+
if args.action == "install":
|
|
76
|
+
service_install(host=args.host, port=args.port, backend=args.backend)
|
|
77
|
+
elif args.action == "restart":
|
|
78
|
+
service_restart()
|
|
79
|
+
elif args.action == "uninstall":
|
|
80
|
+
service_uninstall()
|
|
81
|
+
elif args.action == "status":
|
|
82
|
+
service_status()
|
|
83
|
+
else:
|
|
84
|
+
svc_p.print_help()
|
|
85
|
+
sys.exit(1)
|
|
86
|
+
|
|
87
|
+
else:
|
|
88
|
+
parser.print_help()
|
|
89
|
+
sys.exit(1)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
if __name__ == "__main__":
|
|
93
|
+
main()
|