proxyagent 0.2.0__tar.gz → 0.2.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.
- {proxyagent-0.2.0 → proxyagent-0.2.1}/PKG-INFO +10 -1
- {proxyagent-0.2.0 → proxyagent-0.2.1}/README.md +9 -0
- {proxyagent-0.2.0 → proxyagent-0.2.1}/proxyagent/__init__.py +1 -1
- {proxyagent-0.2.0 → proxyagent-0.2.1}/proxyagent/pricing.py +2 -0
- {proxyagent-0.2.0 → proxyagent-0.2.1}/proxyagent/providers.py +65 -2
- {proxyagent-0.2.0 → proxyagent-0.2.1}/pyproject.toml +1 -1
- {proxyagent-0.2.0 → proxyagent-0.2.1}/tests/test_proxy.py +19 -0
- {proxyagent-0.2.0 → proxyagent-0.2.1}/.gitignore +0 -0
- {proxyagent-0.2.0 → proxyagent-0.2.1}/proxyagent/cli.py +0 -0
- {proxyagent-0.2.0 → proxyagent-0.2.1}/proxyagent/config.py +0 -0
- {proxyagent-0.2.0 → proxyagent-0.2.1}/proxyagent/crypto.py +0 -0
- {proxyagent-0.2.0 → proxyagent-0.2.1}/proxyagent/db.py +0 -0
- {proxyagent-0.2.0 → proxyagent-0.2.1}/proxyagent/harness.py +0 -0
- {proxyagent-0.2.0 → proxyagent-0.2.1}/proxyagent/security.py +0 -0
- {proxyagent-0.2.0 → proxyagent-0.2.1}/proxyagent/server.py +0 -0
- {proxyagent-0.2.0 → proxyagent-0.2.1}/proxyagent/store.py +0 -0
- {proxyagent-0.2.0 → proxyagent-0.2.1}/proxyagent/tools.py +0 -0
- {proxyagent-0.2.0 → proxyagent-0.2.1}/proxyagent/ui/index.html +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: proxyagent
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: Run any agent (Claude, Codex, custom) on any machine — with no API key on the machine. A secure, self-hosted proxy for models and tools.
|
|
5
5
|
Project-URL: Homepage, https://github.com/teddyoweh/proxyagent
|
|
6
6
|
Author-email: Spawn Labs <teddy@spawnlabs.ai>
|
|
@@ -61,6 +61,15 @@ proxy and use the **machine token** as the "api key." The proxy authenticates th
|
|
|
61
61
|
checks its scope, **swaps in the real key**, forwards upstream, and logs the call. The
|
|
62
62
|
machine never sees a real credential.
|
|
63
63
|
|
|
64
|
+
## Try it with zero keys (local)
|
|
65
|
+
```bash
|
|
66
|
+
pip install proxyagent && proxyagent serve # prints an admin token
|
|
67
|
+
proxyagent token new local --admin pa_admin_… # mint a token
|
|
68
|
+
# call the built-in `mock` model — full pipeline (auth, scope, usage, cost, log), no real key:
|
|
69
|
+
curl -s localhost:8080/anthropic/v1/messages -H "x-api-key: pa_…" \
|
|
70
|
+
-d '{"model":"mock","max_tokens":50,"messages":[{"role":"user","content":"hi"}]}'
|
|
71
|
+
```
|
|
72
|
+
|
|
64
73
|
## Quickstart
|
|
65
74
|
|
|
66
75
|
**1. Run the proxy** (on a box you control — it holds the real keys):
|
|
@@ -29,6 +29,15 @@ proxy and use the **machine token** as the "api key." The proxy authenticates th
|
|
|
29
29
|
checks its scope, **swaps in the real key**, forwards upstream, and logs the call. The
|
|
30
30
|
machine never sees a real credential.
|
|
31
31
|
|
|
32
|
+
## Try it with zero keys (local)
|
|
33
|
+
```bash
|
|
34
|
+
pip install proxyagent && proxyagent serve # prints an admin token
|
|
35
|
+
proxyagent token new local --admin pa_admin_… # mint a token
|
|
36
|
+
# call the built-in `mock` model — full pipeline (auth, scope, usage, cost, log), no real key:
|
|
37
|
+
curl -s localhost:8080/anthropic/v1/messages -H "x-api-key: pa_…" \
|
|
38
|
+
-d '{"model":"mock","max_tokens":50,"messages":[{"role":"user","content":"hi"}]}'
|
|
39
|
+
```
|
|
40
|
+
|
|
32
41
|
## Quickstart
|
|
33
42
|
|
|
34
43
|
**1. Run the proxy** (on a box you control — it holds the real keys):
|
|
@@ -60,6 +60,23 @@ async def forward(
|
|
|
60
60
|
):
|
|
61
61
|
"""Forward a request upstream. Returns (status, headers, body_iter_or_dict, log_after)."""
|
|
62
62
|
provider = PROVIDERS[provider_name]
|
|
63
|
+
model = body.get("model", "")
|
|
64
|
+
t0 = now_ms()
|
|
65
|
+
|
|
66
|
+
# Offline mock — exercise the full pipeline (auth, scope, log, cost) with NO real
|
|
67
|
+
# key. Use model "mock" (or "mock-…") anywhere a real model would go.
|
|
68
|
+
if model.startswith("mock"):
|
|
69
|
+
payload, (ptok, ctok) = _mock_payload(provider_name, body)
|
|
70
|
+
store.log_request(
|
|
71
|
+
token_id=token["id"], token_label=token.get("label"), provider=provider_name,
|
|
72
|
+
model=model, status=200, prompt_tokens=ptok, completion_tokens=ctok,
|
|
73
|
+
latency_ms=now_ms() - t0, streamed=1 if streaming else 0,
|
|
74
|
+
tools_used=json.dumps(tools_used or []), cost_usd=pricing.cost_usd(model, ptok, ctok),
|
|
75
|
+
error=None)
|
|
76
|
+
if streaming:
|
|
77
|
+
return 200, {"content-type": "text/event-stream"}, _mock_stream(provider_name, payload), None
|
|
78
|
+
return 200, {"content-type": "application/json"}, payload, None
|
|
79
|
+
|
|
63
80
|
auth, ok = resolve_auth(provider, store)
|
|
64
81
|
if not ok:
|
|
65
82
|
return 502, {}, {"error": f"provider '{provider_name}' not configured on the proxy "
|
|
@@ -67,8 +84,6 @@ async def forward(
|
|
|
67
84
|
|
|
68
85
|
url = provider.base_url + upstream_path
|
|
69
86
|
headers = {"content-type": "application/json", **auth}
|
|
70
|
-
model = body.get("model", "")
|
|
71
|
-
t0 = now_ms()
|
|
72
87
|
|
|
73
88
|
def _log(status, ptok, ctok, err=None):
|
|
74
89
|
store.log_request(
|
|
@@ -115,3 +130,51 @@ async def forward(
|
|
|
115
130
|
ptok, ctok = _extract_usage(provider_name, payload if isinstance(payload, dict) else {})
|
|
116
131
|
_log(resp.status_code, ptok, ctok, None if resp.is_success else str(payload)[:300])
|
|
117
132
|
return resp.status_code, {"content-type": "application/json"}, payload, None
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
# ------------------------------------------------------------------ #
|
|
136
|
+
# Offline mock — provider-shaped canned responses for local testing.
|
|
137
|
+
# ------------------------------------------------------------------ #
|
|
138
|
+
|
|
139
|
+
def _last_user_text(body: dict) -> str:
|
|
140
|
+
for m in reversed(body.get("messages", [])):
|
|
141
|
+
if m.get("role") == "user":
|
|
142
|
+
c = m.get("content")
|
|
143
|
+
if isinstance(c, str):
|
|
144
|
+
return c
|
|
145
|
+
if isinstance(c, list):
|
|
146
|
+
return " ".join(p.get("text", "") for p in c if isinstance(p, dict))
|
|
147
|
+
return ""
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _mock_payload(provider: str, body: dict):
|
|
151
|
+
prompt = _last_user_text(body)[:200]
|
|
152
|
+
text = f"[proxyagent mock] received: {prompt!r}. No real key used — the pipeline works."
|
|
153
|
+
ptok, ctok = max(1, len(prompt) // 4), max(1, len(text) // 4)
|
|
154
|
+
if provider == "anthropic":
|
|
155
|
+
return ({
|
|
156
|
+
"id": "msg_mock", "type": "message", "role": "assistant", "model": body.get("model"),
|
|
157
|
+
"content": [{"type": "text", "text": text}], "stop_reason": "end_turn",
|
|
158
|
+
"usage": {"input_tokens": ptok, "output_tokens": ctok},
|
|
159
|
+
}, (ptok, ctok))
|
|
160
|
+
return ({
|
|
161
|
+
"id": "chatcmpl-mock", "object": "chat.completion", "model": body.get("model"),
|
|
162
|
+
"choices": [{"index": 0, "message": {"role": "assistant", "content": text},
|
|
163
|
+
"finish_reason": "stop"}],
|
|
164
|
+
"usage": {"prompt_tokens": ptok, "completion_tokens": ctok, "total_tokens": ptok + ctok},
|
|
165
|
+
}, (ptok, ctok))
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
async def _mock_stream(provider: str, payload: dict):
|
|
169
|
+
import json as _j
|
|
170
|
+
if provider == "anthropic":
|
|
171
|
+
text = payload["content"][0]["text"]
|
|
172
|
+
yield f"event: message_start\ndata: {_j.dumps({'type':'message_start','message':payload})}\n\n".encode()
|
|
173
|
+
yield (f"event: content_block_delta\ndata: "
|
|
174
|
+
f"{_j.dumps({'type':'content_block_delta','delta':{'type':'text_delta','text':text}})}\n\n").encode()
|
|
175
|
+
yield b"event: message_stop\ndata: {\"type\":\"message_stop\"}\n\n"
|
|
176
|
+
else:
|
|
177
|
+
text = payload["choices"][0]["message"]["content"]
|
|
178
|
+
chunk = {"choices": [{"delta": {"content": text}, "index": 0}]}
|
|
179
|
+
yield f"data: {_j.dumps(chunk)}\n\n".encode()
|
|
180
|
+
yield b"data: [DONE]\n\n"
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "proxyagent"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.1"
|
|
8
8
|
description = "Run any agent (Claude, Codex, custom) on any machine — with no API key on the machine. A secure, self-hosted proxy for models and tools."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -98,6 +98,25 @@ def test_credential_storage_and_resolution():
|
|
|
98
98
|
assert s.remove_credential(cid)
|
|
99
99
|
|
|
100
100
|
|
|
101
|
+
def test_mock_provider_offline():
|
|
102
|
+
"""Full pipeline with no real key: mint → call model 'mock' → response + usage + log."""
|
|
103
|
+
c = _client()
|
|
104
|
+
tok = c.post("/admin/tokens", headers=ADMIN, json={"label": "m", "scope": ["*"]}).json()["token"]
|
|
105
|
+
r = c.post("/anthropic/v1/messages", headers={"x-api-key": tok},
|
|
106
|
+
json={"model": "mock", "max_tokens": 50, "messages": [{"role": "user", "content": "hello"}]})
|
|
107
|
+
assert r.status_code == 200
|
|
108
|
+
body = r.json()
|
|
109
|
+
assert body["content"][0]["text"].startswith("[proxyagent mock]")
|
|
110
|
+
assert body["usage"]["input_tokens"] >= 1
|
|
111
|
+
# it was logged (with $0 cost)
|
|
112
|
+
logs = c.get("/admin/logs", headers=ADMIN).json()["logs"]
|
|
113
|
+
assert logs[0]["model"] == "mock" and logs[0]["status"] == 200
|
|
114
|
+
# openai shape too
|
|
115
|
+
r2 = c.post("/openai/v1/chat/completions", headers={"authorization": f"Bearer {tok}"},
|
|
116
|
+
json={"model": "mock", "messages": [{"role": "user", "content": "hi"}]})
|
|
117
|
+
assert r2.json()["choices"][0]["message"]["content"].startswith("[proxyagent mock]")
|
|
118
|
+
|
|
119
|
+
|
|
101
120
|
def test_provider_admin_endpoints():
|
|
102
121
|
c = _client()
|
|
103
122
|
r = c.post("/admin/providers", headers=ADMIN, json={"provider": "anthropic", "secret": "sk-ant-x"})
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|