iicp-proxy 0.2.0__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.
- iicp_proxy-0.2.0/PKG-INFO +23 -0
- iicp_proxy-0.2.0/README.md +224 -0
- iicp_proxy-0.2.0/pyproject.toml +59 -0
- iicp_proxy-0.2.0/setup.cfg +4 -0
- iicp_proxy-0.2.0/src/iicp_proxy.egg-info/PKG-INFO +23 -0
- iicp_proxy-0.2.0/src/iicp_proxy.egg-info/SOURCES.txt +85 -0
- iicp_proxy-0.2.0/src/iicp_proxy.egg-info/dependency_links.txt +1 -0
- iicp_proxy-0.2.0/src/iicp_proxy.egg-info/entry_points.txt +2 -0
- iicp_proxy-0.2.0/src/iicp_proxy.egg-info/requires.txt +19 -0
- iicp_proxy-0.2.0/src/iicp_proxy.egg-info/top_level.txt +1 -0
- iicp_proxy-0.2.0/src/proxy/__init__.py +2 -0
- iicp_proxy-0.2.0/src/proxy/address_state.py +29 -0
- iicp_proxy-0.2.0/src/proxy/anthropic_compat/__init__.py +1 -0
- iicp_proxy-0.2.0/src/proxy/anthropic_compat/server.py +207 -0
- iicp_proxy-0.2.0/src/proxy/anthropic_compat/translator.py +101 -0
- iicp_proxy-0.2.0/src/proxy/auth/__init__.py +4 -0
- iicp_proxy-0.2.0/src/proxy/auth/secrets.py +18 -0
- iicp_proxy-0.2.0/src/proxy/cip/__init__.py +1 -0
- iicp_proxy-0.2.0/src/proxy/cip/aggregation.py +77 -0
- iicp_proxy-0.2.0/src/proxy/cip/consumer.py +109 -0
- iicp_proxy-0.2.0/src/proxy/cip/coordinator.py +108 -0
- iicp_proxy-0.2.0/src/proxy/cip/dispatch.py +141 -0
- iicp_proxy-0.2.0/src/proxy/cip/gates.py +250 -0
- iicp_proxy-0.2.0/src/proxy/cip/receipts.py +154 -0
- iicp_proxy-0.2.0/src/proxy/cip/strategies.py +100 -0
- iicp_proxy-0.2.0/src/proxy/clients/__init__.py +5 -0
- iicp_proxy-0.2.0/src/proxy/clients/did_resolver.py +99 -0
- iicp_proxy-0.2.0/src/proxy/clients/directory.py +360 -0
- iicp_proxy-0.2.0/src/proxy/clients/node.py +130 -0
- iicp_proxy-0.2.0/src/proxy/clients/replica_registry.py +143 -0
- iicp_proxy-0.2.0/src/proxy/clients/replica_sig_verifier.py +98 -0
- iicp_proxy-0.2.0/src/proxy/clients/trust_resolver.py +127 -0
- iicp_proxy-0.2.0/src/proxy/config.py +139 -0
- iicp_proxy-0.2.0/src/proxy/main.py +188 -0
- iicp_proxy-0.2.0/src/proxy/metrics.py +30 -0
- iicp_proxy-0.2.0/src/proxy/network/__init__.py +4 -0
- iicp_proxy-0.2.0/src/proxy/network/peer_cache.py +118 -0
- iicp_proxy-0.2.0/src/proxy/ollama_compat/__init__.py +1 -0
- iicp_proxy-0.2.0/src/proxy/ollama_compat/server.py +187 -0
- iicp_proxy-0.2.0/src/proxy/ollama_compat/translator.py +96 -0
- iicp_proxy-0.2.0/src/proxy/openai_compat/__init__.py +5 -0
- iicp_proxy-0.2.0/src/proxy/openai_compat/server.py +130 -0
- iicp_proxy-0.2.0/src/proxy/openai_compat/translator.py +38 -0
- iicp_proxy-0.2.0/src/proxy/otel_tracer.py +190 -0
- iicp_proxy-0.2.0/src/proxy/plugin/__init__.py +2 -0
- iicp_proxy-0.2.0/src/proxy/plugin/iicp_provider.py +76 -0
- iicp_proxy-0.2.0/src/proxy/routing/__init__.py +15 -0
- iicp_proxy-0.2.0/src/proxy/routing/aggregator.py +115 -0
- iicp_proxy-0.2.0/src/proxy/routing/circuit_breaker.py +92 -0
- iicp_proxy-0.2.0/src/proxy/routing/fallback.py +259 -0
- iicp_proxy-0.2.0/src/proxy/routing/retry.py +96 -0
- iicp_proxy-0.2.0/src/proxy/routing/router.py +100 -0
- iicp_proxy-0.2.0/src/proxy/routing/selector.py +180 -0
- iicp_proxy-0.2.0/tests/test_address_learning.py +199 -0
- iicp_proxy-0.2.0/tests/test_aggregator.py +79 -0
- iicp_proxy-0.2.0/tests/test_anthropic_cip_dispatch.py +104 -0
- iicp_proxy-0.2.0/tests/test_anthropic_compat.py +137 -0
- iicp_proxy-0.2.0/tests/test_cip_aggregation.py +302 -0
- iicp_proxy-0.2.0/tests/test_cip_call_validators.py +261 -0
- iicp_proxy-0.2.0/tests/test_cip_consumer.py +73 -0
- iicp_proxy-0.2.0/tests/test_cip_coordinator.py +1120 -0
- iicp_proxy-0.2.0/tests/test_cip_integration.py +197 -0
- iicp_proxy-0.2.0/tests/test_circuit_breaker.py +35 -0
- iicp_proxy-0.2.0/tests/test_compat_routes.py +269 -0
- iicp_proxy-0.2.0/tests/test_coverage_gates.py +114 -0
- iicp_proxy-0.2.0/tests/test_did_resolver.py +103 -0
- iicp_proxy-0.2.0/tests/test_directory_client.py +446 -0
- iicp_proxy-0.2.0/tests/test_directory_redirect_trust.py +183 -0
- iicp_proxy-0.2.0/tests/test_directory_sig_verify.py +236 -0
- iicp_proxy-0.2.0/tests/test_fallback.py +556 -0
- iicp_proxy-0.2.0/tests/test_node_client.py +166 -0
- iicp_proxy-0.2.0/tests/test_node_client_fallback.py +179 -0
- iicp_proxy-0.2.0/tests/test_ollama_cip_dispatch.py +104 -0
- iicp_proxy-0.2.0/tests/test_ollama_compat.py +117 -0
- iicp_proxy-0.2.0/tests/test_openai_cip_dispatch.py +296 -0
- iicp_proxy-0.2.0/tests/test_otel_proxy_spans.py +174 -0
- iicp_proxy-0.2.0/tests/test_peer_cache.py +66 -0
- iicp_proxy-0.2.0/tests/test_plugin.py +108 -0
- iicp_proxy-0.2.0/tests/test_prometheus_metrics.py +55 -0
- iicp_proxy-0.2.0/tests/test_proxy_config.py +133 -0
- iicp_proxy-0.2.0/tests/test_replica_registry.py +171 -0
- iicp_proxy-0.2.0/tests/test_replica_sig_verifier.py +153 -0
- iicp_proxy-0.2.0/tests/test_retry.py +100 -0
- iicp_proxy-0.2.0/tests/test_router.py +236 -0
- iicp_proxy-0.2.0/tests/test_selector.py +266 -0
- iicp_proxy-0.2.0/tests/test_translator.py +30 -0
- iicp_proxy-0.2.0/tests/test_trust_resolver.py +136 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: iicp-proxy
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: IICP Client Proxy — Client Plane
|
|
5
|
+
License: Apache-2.0
|
|
6
|
+
Requires-Python: >=3.11
|
|
7
|
+
Requires-Dist: fastapi>=0.115
|
|
8
|
+
Requires-Dist: uvicorn[standard]>=0.32
|
|
9
|
+
Requires-Dist: httpx>=0.27
|
|
10
|
+
Requires-Dist: pydantic>=2.9
|
|
11
|
+
Requires-Dist: pydantic-settings>=2.6
|
|
12
|
+
Requires-Dist: prometheus-client>=0.21
|
|
13
|
+
Requires-Dist: cryptography>=42
|
|
14
|
+
Provides-Extra: dev
|
|
15
|
+
Requires-Dist: pytest>=8.3; extra == "dev"
|
|
16
|
+
Requires-Dist: pytest-asyncio>=0.24; extra == "dev"
|
|
17
|
+
Requires-Dist: pytest-cov>=5.0; extra == "dev"
|
|
18
|
+
Requires-Dist: respx>=0.21; extra == "dev"
|
|
19
|
+
Requires-Dist: ruff>=0.8; extra == "dev"
|
|
20
|
+
Requires-Dist: coverage[toml]>=7.6; extra == "dev"
|
|
21
|
+
Requires-Dist: cbor2>=5.4; extra == "dev"
|
|
22
|
+
Provides-Extra: cbor
|
|
23
|
+
Requires-Dist: cbor2>=5.4; extra == "cbor"
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
# IICP Client Proxy
|
|
2
|
+
|
|
3
|
+
**Plane**: Client
|
|
4
|
+
**Language**: Python 3.11+ / FastAPI
|
|
5
|
+
**ADRs**: [ADR-001](../project/decisions/ADR-001-three-plane-architecture.md), [ADR-005](../project/decisions/ADR-005-adapter-proxy-python-fastapi.md), [ADR-008](../project/decisions/ADR-008-node-scoring-formula.md)
|
|
6
|
+
|
|
7
|
+
Routes IICP task requests to discovered adapter nodes. Exposes three inbound LLM API surfaces on port 9483 so existing client tools work without modification:
|
|
8
|
+
|
|
9
|
+
| Surface | Endpoints | Compatible tools |
|
|
10
|
+
|---------|-----------|-----------------|
|
|
11
|
+
| OpenAI-compat | `POST /v1/chat/completions` | OpenAI SDK, LangChain, LlamaIndex, liteLLM, Cursor |
|
|
12
|
+
| Ollama-compat | `POST /api/chat`, `POST /api/generate`, `GET /api/tags`, `GET /api/version` | Open WebUI, Continue.dev, aider, Jan, Chatbox |
|
|
13
|
+
| Anthropic-compat | `POST /v1/messages`, `GET /v1/models` | Anthropic SDK (`base_url` override) |
|
|
14
|
+
|
|
15
|
+
All three surfaces translate to the same IICP `CALL` message and route through the same FallbackChain pipeline.
|
|
16
|
+
|
|
17
|
+
The proxy **does not re-rank nodes** — it trusts the directory's score order and only filters out unavailable nodes (ADR-008 hard rule: score computed server-side only).
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Module layout
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
src/proxy/
|
|
25
|
+
main.py — FastAPI app factory, startup; wires routing stack + mounts compat apps
|
|
26
|
+
config.py — ProxyConfig (pydantic-settings)
|
|
27
|
+
clients/
|
|
28
|
+
directory.py — DirectoryClient: GET /v1/discover, GET /v1/bootstrap
|
|
29
|
+
node.py — NodeClient: POST /v1/task to a specific adapter node
|
|
30
|
+
routing/
|
|
31
|
+
selector.py — NodeSelector: filter-only (available=true); preserves directory order
|
|
32
|
+
router.py — TaskRouter: submit task to a single node via NodeClient
|
|
33
|
+
retry.py — RetryManager: 3 attempts, 200ms base, ×2 backoff, ±20% jitter
|
|
34
|
+
circuit_breaker.py — CircuitBreaker: 5 failures → open, 30s half-open (per node)
|
|
35
|
+
fallback.py — FallbackChain: best → second → local fallback → structured error
|
|
36
|
+
aggregator.py — ResultAggregator: best-of-N / majority vote (Phase 2)
|
|
37
|
+
openai_compat/ — OpenAI API surface (POST /v1/chat/completions)
|
|
38
|
+
server.py — create_compat_app(): base FastAPI app; other surfaces add routes to it
|
|
39
|
+
translator.py — OpenAI body → IicpTask, IicpResponse → OpenAI response
|
|
40
|
+
ollama_compat/ — Ollama API surface (/api/chat, /api/generate, /api/tags, /api/version)
|
|
41
|
+
server.py — add_ollama_routes(app): registers Ollama endpoints on the shared compat app
|
|
42
|
+
translator.py — Ollama body → IicpTask, IicpResponse → Ollama response
|
|
43
|
+
anthropic_compat/ — Anthropic Messages API surface (/v1/messages, /v1/models)
|
|
44
|
+
server.py — add_anthropic_routes(app): registers Anthropic endpoints on the shared compat app
|
|
45
|
+
translator.py — Anthropic body → IicpTask, IicpResponse → Anthropic response
|
|
46
|
+
plugin/
|
|
47
|
+
iicp_provider.py — IicpModelProvider: drop-in OpenAI-compatible provider for host assistants
|
|
48
|
+
(OpenClaw, Moltbot etc.); injects directory + selector + fallback_chain
|
|
49
|
+
network/
|
|
50
|
+
peer_cache.py — Local peer cache (Phase 2: persist discovered peers)
|
|
51
|
+
auth/
|
|
52
|
+
secrets.py — Node token storage (OS keychain integration)
|
|
53
|
+
cip/
|
|
54
|
+
consumer.py — CIPConsumerConfig + CIPCallFields (S.12 §4.1 wire-boundary validator:
|
|
55
|
+
policy ∈ {best_of_n,majority_vote,map_reduce}, replicas ∈ [1,10])
|
|
56
|
+
aggregation.py — CIPAggregationResult (S.12 §4.3): coordinator RESPONSE aggregation
|
|
57
|
+
object; cross-field invariants: responded ≤ dispatched,
|
|
58
|
+
selected_worker_id null when responded == 0
|
|
59
|
+
coordinator.py — CIP coordinator: split task, collect results from workers
|
|
60
|
+
dispatch.py — CIP dispatch helpers: worker selection + envelope construction
|
|
61
|
+
strategies.py — Split/merge strategies (token-split, prompt-prefix, local-first precedence S.12 §2.2)
|
|
62
|
+
address_state.py — AddressState: tracks observed source IP across directory interactions (ADR-011 DIR-ADDR-04)
|
|
63
|
+
metrics.py — Shared Prometheus metric definitions (iicp_proxy_tasks_total etc.)
|
|
64
|
+
otel_tracer.py — OTelTracer: W3C traceparent propagation, ADR-014 TRACE-04, no-op fallback
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## Configuration
|
|
70
|
+
|
|
71
|
+
Environment variables (no TOML file for proxy — secrets should not live in files):
|
|
72
|
+
|
|
73
|
+
| Env var | Default | Description |
|
|
74
|
+
|---------|---------|-------------|
|
|
75
|
+
| `IICP_PROXY_DIRECTORY_URL` | `"https://iicp.network/api"` | Directory service URL |
|
|
76
|
+
| `IICP_NODE_TOKEN` | — | Bearer token for authenticating to adapter nodes |
|
|
77
|
+
| `IICP_PROXY_PREFERRED_REGION` | — | Hint passed to directory `?region=` filter |
|
|
78
|
+
| `IICP_PROXY_MAX_RETRIES` | `3` | Maximum task retry attempts |
|
|
79
|
+
| `IICP_PROXY_DIRECTORY_TIMEOUT_MS` | `5000` | Directory request timeout (ms) |
|
|
80
|
+
| `IICP_PROXY_HOST` | `"127.0.0.1"` | Listen address (loopback only by default) |
|
|
81
|
+
| `IICP_PROXY_PORT` | `9483` | Listen port (reserved IICP proxy band; set `=11434` for Ollama drop-in) |
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## Running
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
# Install
|
|
89
|
+
pip install -e ".[dev]"
|
|
90
|
+
|
|
91
|
+
# Run (listens on 127.0.0.1:9483 by default)
|
|
92
|
+
iicp-proxy
|
|
93
|
+
|
|
94
|
+
# Override the port — e.g. 11434 for literal Ollama drop-in
|
|
95
|
+
IICP_PROXY_PORT=11434 iicp-proxy
|
|
96
|
+
|
|
97
|
+
# Docker
|
|
98
|
+
docker build -t iicp-proxy .
|
|
99
|
+
docker run -p 127.0.0.1:9483:9483 --env-file .env iicp-proxy
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## Protocol surface usage
|
|
105
|
+
|
|
106
|
+
**OpenAI-compat** (ChatGPT SDK, LangChain, liteLLM — point `base_url` at port 9483):
|
|
107
|
+
```bash
|
|
108
|
+
curl http://localhost:9483/v1/chat/completions \
|
|
109
|
+
-H "Content-Type: application/json" \
|
|
110
|
+
-d '{"model":"iicp","messages":[{"role":"user","content":"Hello"}]}'
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
**Ollama-compat** (Open WebUI, Continue.dev, aider — set Ollama URL to `http://localhost:9483`):
|
|
114
|
+
```bash
|
|
115
|
+
curl http://localhost:9483/api/chat \
|
|
116
|
+
-H "Content-Type: application/json" \
|
|
117
|
+
-d '{"model":"iicp","messages":[{"role":"user","content":"Hello"}]}'
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
**Anthropic-compat** (Anthropic SDK — pass `base_url="http://localhost:9483"`):
|
|
121
|
+
```bash
|
|
122
|
+
curl http://localhost:9483/v1/messages \
|
|
123
|
+
-H "Content-Type: application/json" \
|
|
124
|
+
-H "x-api-key: any" \
|
|
125
|
+
-H "anthropic-version: 2023-06-01" \
|
|
126
|
+
-d '{"model":"claude-3-5-sonnet-20241022","max_tokens":100,"messages":[{"role":"user","content":"Hello"}]}'
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
All three translate to the same IICP `CALL` message with intent `urn:iicp:intent:llm:chat:v1`.
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## Testing
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
pytest tests/ -v
|
|
137
|
+
pytest tests/ --cov=proxy --cov-report=term-missing
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## Adding a new LLM API compat layer
|
|
143
|
+
|
|
144
|
+
The proxy translates between client-side LLM API formats and the IICP task wire format.
|
|
145
|
+
`openai_compat/` is the reference implementation; `ollama_compat/` and `anthropic_compat/`
|
|
146
|
+
are working examples. To add support for a new API shape, follow this pattern:
|
|
147
|
+
|
|
148
|
+
**1. Create `src/proxy/<name>_compat/`**
|
|
149
|
+
|
|
150
|
+
```
|
|
151
|
+
<name>_compat/
|
|
152
|
+
__init__.py — empty
|
|
153
|
+
server.py — add_<name>_routes(app): registers endpoints on the shared compat app
|
|
154
|
+
translator.py — your_body → IicpTask, IicpResponse → your_response
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
`translator.py` must export:
|
|
158
|
+
- `to_iicp_task(body: dict) -> tuple[UUID, str, dict]` — produces `(task_id, intent, payload)`
|
|
159
|
+
- `to_<name>_response(iicp_response: dict, ...) -> dict` — converts IICP response to your format
|
|
160
|
+
|
|
161
|
+
Use `ollama_compat/translator.py` as the template — it is ≤50 lines and handles the common
|
|
162
|
+
patterns (flatten messages, forward options, map token counts).
|
|
163
|
+
|
|
164
|
+
**2. Wire it in `main.py`**
|
|
165
|
+
|
|
166
|
+
```python
|
|
167
|
+
from proxy.<name>_compat.server import add_<name>_routes
|
|
168
|
+
|
|
169
|
+
# In create_app(), after create_compat_app():
|
|
170
|
+
compat = create_compat_app()
|
|
171
|
+
add_ollama_routes(compat)
|
|
172
|
+
add_anthropic_routes(compat)
|
|
173
|
+
add_<name>_routes(compat) # add your surface here
|
|
174
|
+
app.mount("/", compat)
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
The routing stack (`selector`, `fallback_chain`, `aggregator`, `directory`) is attached
|
|
178
|
+
to `app.state` during lifespan startup. In your handler:
|
|
179
|
+
|
|
180
|
+
```python
|
|
181
|
+
@app.post("/your-endpoint")
|
|
182
|
+
async def your_handler(request: Request) -> JSONResponse:
|
|
183
|
+
fallback_chain = request.app.state.fallback_chain
|
|
184
|
+
directory = request.app.state.directory
|
|
185
|
+
selector = request.app.state.selector
|
|
186
|
+
|
|
187
|
+
body = await request.json()
|
|
188
|
+
task_id, intent, payload = your_translator.to_iicp_task(body)
|
|
189
|
+
nodes = selector.select(await directory.discover(intent=intent))
|
|
190
|
+
response = await fallback_chain.execute(nodes, task_id, intent, payload, timeout_ms=30_000)
|
|
191
|
+
return JSONResponse(content=your_translator.to_your_response(response))
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
**3. No changes to routing, selector, or circuit breaker are needed.**
|
|
195
|
+
|
|
196
|
+
The routing layer is format-agnostic. The only contract: produce a valid IICP
|
|
197
|
+
`intent` URN and a `payload` dict; get back a response dict with a `result` key.
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## CIP Wire Format Validation (CIP-V01..V06 — S.12)
|
|
202
|
+
|
|
203
|
+
The `cip/` package enforces all normative wire constraints from S.12 before any
|
|
204
|
+
dispatch occurs. Invalid values raise `ValidationError` at parse time → 422 IICP-E028.
|
|
205
|
+
|
|
206
|
+
| Model | File | Validates |
|
|
207
|
+
|-------|------|-----------|
|
|
208
|
+
| `CIPCallFields` | `cip/consumer.py` | `cip.policy` ∈ `{best_of_n, majority_vote, map_reduce}` (V01); `cip.replicas` ∈ [1, 10] (V02) |
|
|
209
|
+
| `CIPAggregationResult` | `cip/aggregation.py` | coordinator RESPONSE `cip_aggregation` object (V06): `replicas_responded` ≤ `replicas_dispatched`; `selected_worker_id` null when `replicas_responded == 0` |
|
|
210
|
+
|
|
211
|
+
`CIPConsumerConfig` (also in `consumer.py`) is operator-level config loaded from
|
|
212
|
+
`proxy.toml` — it silently clamps rather than rejecting. This is intentional: operator
|
|
213
|
+
config is trusted; per-request wire input is not.
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
## Key invariants
|
|
218
|
+
|
|
219
|
+
- Proxy binds to `127.0.0.1` only (loopback) — never exposed directly to internet.
|
|
220
|
+
- TLS certificate validation is **not** bypassable; no `verify=False` anywhere.
|
|
221
|
+
- Node score order from directory is preserved; only `available=false` nodes are filtered.
|
|
222
|
+
- Circuit breaker state is per-node and in-memory; resets on proxy restart (accepted Phase 1 limitation).
|
|
223
|
+
- No task payload content is logged (privacy invariant — mirrors adapter rule).
|
|
224
|
+
- All retries use jittered backoff; `RetryManager` enforces maximum 3 attempts.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "iicp-proxy"
|
|
3
|
+
version = "0.2.0"
|
|
4
|
+
description = "IICP Client Proxy — Client Plane"
|
|
5
|
+
license = {text = "Apache-2.0"}
|
|
6
|
+
requires-python = ">=3.11"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"fastapi>=0.115",
|
|
9
|
+
"uvicorn[standard]>=0.32",
|
|
10
|
+
"httpx>=0.27",
|
|
11
|
+
"pydantic>=2.9",
|
|
12
|
+
"pydantic-settings>=2.6",
|
|
13
|
+
"prometheus-client>=0.21",
|
|
14
|
+
# P6-4.2b: Ed25519 verification for X-IICP-Replica-Sig on discover responses.
|
|
15
|
+
# Pure-Python alternatives exist but are 50x slower; cryptography is well-
|
|
16
|
+
# maintained and widely deployed.
|
|
17
|
+
"cryptography>=42",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[project.optional-dependencies]
|
|
21
|
+
dev = [
|
|
22
|
+
"pytest>=8.3",
|
|
23
|
+
"pytest-asyncio>=0.24",
|
|
24
|
+
"pytest-cov>=5.0",
|
|
25
|
+
"respx>=0.21",
|
|
26
|
+
"ruff>=0.8",
|
|
27
|
+
"coverage[toml]>=7.6",
|
|
28
|
+
"cbor2>=5.4",
|
|
29
|
+
]
|
|
30
|
+
cbor = [
|
|
31
|
+
"cbor2>=5.4",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[project.scripts]
|
|
35
|
+
iicp-proxy = "proxy.main:run"
|
|
36
|
+
|
|
37
|
+
[build-system]
|
|
38
|
+
requires = ["setuptools>=68"]
|
|
39
|
+
build-backend = "setuptools.build_meta"
|
|
40
|
+
|
|
41
|
+
[tool.setuptools.packages.find]
|
|
42
|
+
where = ["src"]
|
|
43
|
+
|
|
44
|
+
[tool.pytest.ini_options]
|
|
45
|
+
asyncio_mode = "auto"
|
|
46
|
+
testpaths = ["tests"]
|
|
47
|
+
|
|
48
|
+
[tool.ruff]
|
|
49
|
+
line-length = 120
|
|
50
|
+
target-version = "py311"
|
|
51
|
+
|
|
52
|
+
[tool.ruff.lint]
|
|
53
|
+
select = ["E", "F", "I", "UP"]
|
|
54
|
+
|
|
55
|
+
[tool.coverage.run]
|
|
56
|
+
source = ["src/proxy"]
|
|
57
|
+
omit = ["tests/*"]
|
|
58
|
+
[tool.coverage.report]
|
|
59
|
+
fail_under = 70
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: iicp-proxy
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: IICP Client Proxy — Client Plane
|
|
5
|
+
License: Apache-2.0
|
|
6
|
+
Requires-Python: >=3.11
|
|
7
|
+
Requires-Dist: fastapi>=0.115
|
|
8
|
+
Requires-Dist: uvicorn[standard]>=0.32
|
|
9
|
+
Requires-Dist: httpx>=0.27
|
|
10
|
+
Requires-Dist: pydantic>=2.9
|
|
11
|
+
Requires-Dist: pydantic-settings>=2.6
|
|
12
|
+
Requires-Dist: prometheus-client>=0.21
|
|
13
|
+
Requires-Dist: cryptography>=42
|
|
14
|
+
Provides-Extra: dev
|
|
15
|
+
Requires-Dist: pytest>=8.3; extra == "dev"
|
|
16
|
+
Requires-Dist: pytest-asyncio>=0.24; extra == "dev"
|
|
17
|
+
Requires-Dist: pytest-cov>=5.0; extra == "dev"
|
|
18
|
+
Requires-Dist: respx>=0.21; extra == "dev"
|
|
19
|
+
Requires-Dist: ruff>=0.8; extra == "dev"
|
|
20
|
+
Requires-Dist: coverage[toml]>=7.6; extra == "dev"
|
|
21
|
+
Requires-Dist: cbor2>=5.4; extra == "dev"
|
|
22
|
+
Provides-Extra: cbor
|
|
23
|
+
Requires-Dist: cbor2>=5.4; extra == "cbor"
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
src/iicp_proxy.egg-info/PKG-INFO
|
|
4
|
+
src/iicp_proxy.egg-info/SOURCES.txt
|
|
5
|
+
src/iicp_proxy.egg-info/dependency_links.txt
|
|
6
|
+
src/iicp_proxy.egg-info/entry_points.txt
|
|
7
|
+
src/iicp_proxy.egg-info/requires.txt
|
|
8
|
+
src/iicp_proxy.egg-info/top_level.txt
|
|
9
|
+
src/proxy/__init__.py
|
|
10
|
+
src/proxy/address_state.py
|
|
11
|
+
src/proxy/config.py
|
|
12
|
+
src/proxy/main.py
|
|
13
|
+
src/proxy/metrics.py
|
|
14
|
+
src/proxy/otel_tracer.py
|
|
15
|
+
src/proxy/anthropic_compat/__init__.py
|
|
16
|
+
src/proxy/anthropic_compat/server.py
|
|
17
|
+
src/proxy/anthropic_compat/translator.py
|
|
18
|
+
src/proxy/auth/__init__.py
|
|
19
|
+
src/proxy/auth/secrets.py
|
|
20
|
+
src/proxy/cip/__init__.py
|
|
21
|
+
src/proxy/cip/aggregation.py
|
|
22
|
+
src/proxy/cip/consumer.py
|
|
23
|
+
src/proxy/cip/coordinator.py
|
|
24
|
+
src/proxy/cip/dispatch.py
|
|
25
|
+
src/proxy/cip/gates.py
|
|
26
|
+
src/proxy/cip/receipts.py
|
|
27
|
+
src/proxy/cip/strategies.py
|
|
28
|
+
src/proxy/clients/__init__.py
|
|
29
|
+
src/proxy/clients/did_resolver.py
|
|
30
|
+
src/proxy/clients/directory.py
|
|
31
|
+
src/proxy/clients/node.py
|
|
32
|
+
src/proxy/clients/replica_registry.py
|
|
33
|
+
src/proxy/clients/replica_sig_verifier.py
|
|
34
|
+
src/proxy/clients/trust_resolver.py
|
|
35
|
+
src/proxy/network/__init__.py
|
|
36
|
+
src/proxy/network/peer_cache.py
|
|
37
|
+
src/proxy/ollama_compat/__init__.py
|
|
38
|
+
src/proxy/ollama_compat/server.py
|
|
39
|
+
src/proxy/ollama_compat/translator.py
|
|
40
|
+
src/proxy/openai_compat/__init__.py
|
|
41
|
+
src/proxy/openai_compat/server.py
|
|
42
|
+
src/proxy/openai_compat/translator.py
|
|
43
|
+
src/proxy/plugin/__init__.py
|
|
44
|
+
src/proxy/plugin/iicp_provider.py
|
|
45
|
+
src/proxy/routing/__init__.py
|
|
46
|
+
src/proxy/routing/aggregator.py
|
|
47
|
+
src/proxy/routing/circuit_breaker.py
|
|
48
|
+
src/proxy/routing/fallback.py
|
|
49
|
+
src/proxy/routing/retry.py
|
|
50
|
+
src/proxy/routing/router.py
|
|
51
|
+
src/proxy/routing/selector.py
|
|
52
|
+
tests/test_address_learning.py
|
|
53
|
+
tests/test_aggregator.py
|
|
54
|
+
tests/test_anthropic_cip_dispatch.py
|
|
55
|
+
tests/test_anthropic_compat.py
|
|
56
|
+
tests/test_cip_aggregation.py
|
|
57
|
+
tests/test_cip_call_validators.py
|
|
58
|
+
tests/test_cip_consumer.py
|
|
59
|
+
tests/test_cip_coordinator.py
|
|
60
|
+
tests/test_cip_integration.py
|
|
61
|
+
tests/test_circuit_breaker.py
|
|
62
|
+
tests/test_compat_routes.py
|
|
63
|
+
tests/test_coverage_gates.py
|
|
64
|
+
tests/test_did_resolver.py
|
|
65
|
+
tests/test_directory_client.py
|
|
66
|
+
tests/test_directory_redirect_trust.py
|
|
67
|
+
tests/test_directory_sig_verify.py
|
|
68
|
+
tests/test_fallback.py
|
|
69
|
+
tests/test_node_client.py
|
|
70
|
+
tests/test_node_client_fallback.py
|
|
71
|
+
tests/test_ollama_cip_dispatch.py
|
|
72
|
+
tests/test_ollama_compat.py
|
|
73
|
+
tests/test_openai_cip_dispatch.py
|
|
74
|
+
tests/test_otel_proxy_spans.py
|
|
75
|
+
tests/test_peer_cache.py
|
|
76
|
+
tests/test_plugin.py
|
|
77
|
+
tests/test_prometheus_metrics.py
|
|
78
|
+
tests/test_proxy_config.py
|
|
79
|
+
tests/test_replica_registry.py
|
|
80
|
+
tests/test_replica_sig_verifier.py
|
|
81
|
+
tests/test_retry.py
|
|
82
|
+
tests/test_router.py
|
|
83
|
+
tests/test_selector.py
|
|
84
|
+
tests/test_translator.py
|
|
85
|
+
tests/test_trust_resolver.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
fastapi>=0.115
|
|
2
|
+
uvicorn[standard]>=0.32
|
|
3
|
+
httpx>=0.27
|
|
4
|
+
pydantic>=2.9
|
|
5
|
+
pydantic-settings>=2.6
|
|
6
|
+
prometheus-client>=0.21
|
|
7
|
+
cryptography>=42
|
|
8
|
+
|
|
9
|
+
[cbor]
|
|
10
|
+
cbor2>=5.4
|
|
11
|
+
|
|
12
|
+
[dev]
|
|
13
|
+
pytest>=8.3
|
|
14
|
+
pytest-asyncio>=0.24
|
|
15
|
+
pytest-cov>=5.0
|
|
16
|
+
respx>=0.21
|
|
17
|
+
ruff>=0.8
|
|
18
|
+
coverage[toml]>=7.6
|
|
19
|
+
cbor2>=5.4
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
proxy
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Runtime holder for the proxy's observed external IP (DIR-ADDR-02)."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class AddressState:
|
|
10
|
+
observed_source_ip: str | None = field(default=None)
|
|
11
|
+
endpoint: str | None = field(default=None)
|
|
12
|
+
node_id: str | None = field(default=None)
|
|
13
|
+
|
|
14
|
+
def update_from_ack(self, ack: dict) -> None:
|
|
15
|
+
self.observed_source_ip = ack.get("observed_source_ip")
|
|
16
|
+
self.node_id = ack.get("node_id")
|
|
17
|
+
|
|
18
|
+
def update_from_me(self, me: dict) -> None:
|
|
19
|
+
self.observed_source_ip = me.get("observed_source_ip")
|
|
20
|
+
self.node_id = me.get("node_id")
|
|
21
|
+
self.endpoint = me.get("endpoint")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# Module-level singleton — set once at startup, read by status commands
|
|
25
|
+
_state: AddressState = AddressState()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def get_address_state() -> AddressState:
|
|
29
|
+
return _state
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|