vin-guard-sdk 0.3.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.
Files changed (35) hide show
  1. vin_guard_sdk-0.3.0/MANIFEST.in +3 -0
  2. vin_guard_sdk-0.3.0/PKG-INFO +268 -0
  3. vin_guard_sdk-0.3.0/README.md +256 -0
  4. vin_guard_sdk-0.3.0/RESEARCH.md +95 -0
  5. vin_guard_sdk-0.3.0/examples/async_chat.py +22 -0
  6. vin_guard_sdk-0.3.0/examples/basic_chat.py +23 -0
  7. vin_guard_sdk-0.3.0/examples/dashboard_reads.py +20 -0
  8. vin_guard_sdk-0.3.0/examples/langchain_async.py +21 -0
  9. vin_guard_sdk-0.3.0/examples/langchain_chat.py +25 -0
  10. vin_guard_sdk-0.3.0/examples/openai_compat.py +21 -0
  11. vin_guard_sdk-0.3.0/pyproject.toml +27 -0
  12. vin_guard_sdk-0.3.0/setup.cfg +4 -0
  13. vin_guard_sdk-0.3.0/src/ai_gateway/__init__.py +23 -0
  14. vin_guard_sdk-0.3.0/src/ai_gateway/_shared.py +116 -0
  15. vin_guard_sdk-0.3.0/src/ai_gateway/_sse.py +59 -0
  16. vin_guard_sdk-0.3.0/src/ai_gateway/async_client.py +99 -0
  17. vin_guard_sdk-0.3.0/src/ai_gateway/client.py +169 -0
  18. vin_guard_sdk-0.3.0/src/ai_gateway/control_plane.py +161 -0
  19. vin_guard_sdk-0.3.0/src/ai_gateway/errors.py +264 -0
  20. vin_guard_sdk-0.3.0/src/ai_gateway/langchain.py +210 -0
  21. vin_guard_sdk-0.3.0/src/ai_gateway/py.typed +0 -0
  22. vin_guard_sdk-0.3.0/src/ai_gateway/types.py +19 -0
  23. vin_guard_sdk-0.3.0/src/vin_guard_sdk.egg-info/PKG-INFO +268 -0
  24. vin_guard_sdk-0.3.0/src/vin_guard_sdk.egg-info/SOURCES.txt +33 -0
  25. vin_guard_sdk-0.3.0/src/vin_guard_sdk.egg-info/dependency_links.txt +1 -0
  26. vin_guard_sdk-0.3.0/src/vin_guard_sdk.egg-info/requires.txt +7 -0
  27. vin_guard_sdk-0.3.0/src/vin_guard_sdk.egg-info/top_level.txt +1 -0
  28. vin_guard_sdk-0.3.0/tests/test_chat.py +99 -0
  29. vin_guard_sdk-0.3.0/tests/test_error_contract.py +376 -0
  30. vin_guard_sdk-0.3.0/tests/test_errors.py +36 -0
  31. vin_guard_sdk-0.3.0/tests/test_expansion.py +226 -0
  32. vin_guard_sdk-0.3.0/tests/test_langchain.py +232 -0
  33. vin_guard_sdk-0.3.0/tests/test_models.py +51 -0
  34. vin_guard_sdk-0.3.0/tests/test_optional_dependency.py +27 -0
  35. vin_guard_sdk-0.3.0/tests/test_sse.py +20 -0
@@ -0,0 +1,3 @@
1
+ include RESEARCH.md
2
+ include src/ai_gateway/py.typed
3
+ recursive-include examples *.py
@@ -0,0 +1,268 @@
1
+ Metadata-Version: 2.4
2
+ Name: vin-guard-sdk
3
+ Version: 0.3.0
4
+ Summary: Sync and async AI Gateway clients with routing metadata and dashboard reads
5
+ Requires-Python: >=3.10
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: httpx<1,>=0.27
8
+ Provides-Extra: dev
9
+ Requires-Dist: pytest>=8; extra == "dev"
10
+ Provides-Extra: langchain
11
+ Requires-Dist: langchain-core<2,>=1; extra == "langchain"
12
+
13
+ # AI Gateway — Python SDK (Level 1)
14
+
15
+ Thin wrapper over the OpenAI-compatible data plane (`docs/API_DESIGN.md §3`,
16
+ `docs/ai-gateway-design.md §4.9`).
17
+
18
+ ## Level 0 — use any OpenAI SDK as-is
19
+
20
+ ```python
21
+ from openai import OpenAI
22
+
23
+ client = OpenAI(
24
+ api_key="gw_live_...",
25
+ base_url="https://gateway.example.com/v1",
26
+ )
27
+ resp = client.chat.completions.create(
28
+ model="auto", # or policy/<name> | <provider>/<model> | alias
29
+ messages=[{"role": "user", "content": "Summarize this ticket..."}],
30
+ )
31
+ ```
32
+
33
+ See `examples/openai_compat.py`.
34
+
35
+ ## Level 1 — this package
36
+
37
+ ```python
38
+ from ai_gateway import GatewayClient
39
+
40
+ client = GatewayClient(
41
+ api_key="gw_live_...",
42
+ base_url="https://gateway.example.com/v1",
43
+ )
44
+ resp = client.chat_completions(
45
+ model="auto",
46
+ messages=[{"role": "user", "content": "hi"}],
47
+ tags={"team": "support", "env": "prod"},
48
+ metadata={"ticket": "t-123"},
49
+ )
50
+ print(resp.content)
51
+ print(resp.gateway.request_id, resp.gateway.tier)
52
+ print(resp.headers.get("x-gateway-provider"))
53
+ ```
54
+
55
+ Request options map to `API_DESIGN §2.5` headers:
56
+
57
+ | Arg | Header |
58
+ |---|---|
59
+ | `tags: dict \| str` | `X-Gateway-Tags: team=support,env=prod` |
60
+ | `metadata: dict` | `X-Gateway-Metadata: {...}` (≤4KB, opaque JSON) |
61
+ | `route_policy: str` | `X-Gateway-Route-Policy` |
62
+ | `no_store: bool` | `X-Gateway-No-Store: true` |
63
+ | `idempotency_key: str` | `Idempotency-Key` (non-streaming retries) |
64
+
65
+ Errors keep the fields `GatewayError` always had (`.message`, `.code`, `.type`,
66
+ `.param`, `.status`, `.request_id`, `.retry_after`) and understand both the
67
+ OpenAI envelope and RFC 7807 problem details (`detail`/`title`/`type`,
68
+ `errors`, extensions). Header lookups are case-insensitive and the HTTP status
69
+ is authoritative. Request ID priority is `X-Gateway-Request-Id` →
70
+ `X-Request-Id` → `traceId`/`requestId`/`request_id` in the body. The parsed body
71
+ stays on `err.payload` (plus `err.errors`/`err.extensions`) so callers can
72
+ inspect it; it may contain sensitive server data, so the SDK never logs it.
73
+
74
+ Failures before a response arrives raise `GatewayTransportError` subclasses:
75
+ `GatewayConnectionError` (connection) and `GatewayTimeoutError` (timeout with a
76
+ `phase` of `connect`/`read`/`write`/`pool`). Their `status` is `0` and the
77
+ original HTTPX exception is on `__cause__`/`.cause`. Migration note: code that
78
+ caught `httpx.TransportError`/`httpx.TimeoutException` should catch
79
+ `GatewayTransportError` (or the narrower classes) instead.
80
+
81
+ No automatic retries are performed; the current Java hot path does not enforce
82
+ the documented idempotency header. Retrying a chat POST may duplicate billing.
83
+
84
+ ## Install and verify
85
+
86
+ ```bash
87
+ python -m pip install -e '.[dev]'
88
+ python -m pytest -q
89
+ ```
90
+
91
+ The default run skips `tests/integration` (real-stack smoke). CI has separate
92
+ `sdk-core` (no LangChain), `sdk-langchain` (extra required) and `sdk-package`
93
+ (wheel/sdist installed into a clean venv outside the source tree) jobs, across
94
+ Python 3.10/3.12 and HTTPX 0.27.0/0.28.1, with `langchain-core` 1.0.0/1.6.7.
95
+ The adapter tests import LangChain directly, so a missing dependency fails
96
+ collection instead of skipping green. Publishing a GitHub Release runs
97
+ `.github/workflows/publish.yml`, which rebuilds the same artifacts and uploads
98
+ them to PyPI with Trusted Publishing (no stored token).
99
+
100
+ ## Async data plane
101
+
102
+ ```python
103
+ import asyncio
104
+ import os
105
+ from ai_gateway import AsyncGatewayClient
106
+
107
+ async def main():
108
+ async with AsyncGatewayClient(
109
+ api_key=os.environ["GATEWAY_API_KEY"],
110
+ base_url=os.getenv("GATEWAY_BASE_URL", "http://localhost:8080/v1"),
111
+ ) as client:
112
+ response = await client.chat_completions(
113
+ model="auto", messages=[{"role": "user", "content": "Hello"}],
114
+ )
115
+ print(response.content, response.gateway.request_id)
116
+
117
+ asyncio.run(main())
118
+ ```
119
+
120
+ `AsyncGatewayClient` mirrors chat, models, health, ready, streaming and text
121
+ collection. Use `aclose()` or `async with`; the sync client uses `close()` or `with`.
122
+ Both accept an HTTPX timeout and transport for contract testing.
123
+
124
+ Health probes call `/healthz` and `/readyz` at the service root, without the API
125
+ key. A proxy mount prefix is retained (`/gateway/v1` → `/gateway/healthz`).
126
+
127
+ `temperature` and `max_tokens` work with the current Java DTO. `top_p`, `stop`,
128
+ `tools`, `tool_choice`, `response_format`, `user`, and `seed` are documented
129
+ pass-through options for future backends: the current backend ignores them.
130
+ Gateway headers are forwarded; setting tags/no-store/metadata does not by itself
131
+ implement server-side enforcement. Metadata is checked against the 4KB limit.
132
+ Tool-only completions with `content: null` expose an empty `.content` string and
133
+ preserve tool calls in `.choices` and `.raw`.
134
+
135
+ Streaming helpers parse SSE including multiline events and surface malformed
136
+ events and stream errors. The current backend returns JSON even with
137
+ `stream: true`; helpers raise `GatewayError(code="streaming_not_supported")`
138
+ instead of silently returning no text. They do not simulate token streaming.
139
+
140
+ ## Dashboard reads (serverless control plane)
141
+
142
+ This is a separate service and uses an OIDC access token, not a `gw_live_` key.
143
+ These endpoints require the serverless control-plane deployment. Both SDK
144
+ branches now include its source after synchronization with `origin/main` on
145
+ 2026-10-07; source presence does not mean the service is already deployed.
146
+
147
+ ```python
148
+ import os
149
+ from ai_gateway import ControlPlaneClient
150
+
151
+ with ControlPlaneClient(
152
+ base_url=os.environ["CONTROL_PLANE_BASE_URL"], # includes /api/v1
153
+ access_token=os.environ["CONTROL_PLANE_ACCESS_TOKEN"],
154
+ ) as dashboard:
155
+ print(dashboard.me())
156
+ for request in dashboard.iter_items(
157
+ dashboard.list_requests, "org_example", limit=100,
158
+ **{"from": "2026-10-01T00:00:00Z", "projectId": "prj_example"},
159
+ ):
160
+ print(request["requestId"])
161
+ print(dashboard.get_usage("org_example", group_by="project").data)
162
+ ```
163
+
164
+ An access-token callable can supply a refreshed token on every request. Local
165
+ development explicitly uses `dev_subject="..."` instead of `access_token`.
166
+ The client does not authenticate users or refresh tokens itself.
167
+
168
+ Read helpers: `me`, `list_orgs`, `list_projects`, `list_api_keys`,
169
+ `list_providers`, `list_models`, `list_routing_policies`, `list_policy_versions`,
170
+ `get_usage`, `list_requests`, `get_request`, `get_routing_insights`,
171
+ `list_audit_logs`. List helpers return `Page(data, next_cursor, has_more)`;
172
+ `iter_items` traverses all pages and fails on missing/repeated cursors. Query
173
+ kwargs use wire names (`projectId`, `minLatencyMs`, `cursor`, `limit`, etc.).
174
+ Usage is daily only; hourly granularity is not implemented by the control plane.
175
+ Wire dictionaries preserve nullable and additional fields. Key reads return
176
+ metadata only; no SDK secret persistence or key mutations are added here.
177
+
178
+ Research and prioritized endpoint/function suggestions:
179
+ [RESEARCH.md](RESEARCH.md).
180
+
181
+ Vietnamese architecture guide with component flows, methods, compatibility,
182
+ examples and troubleshooting: [SDK guide](../docs/SDK_PYTHON_GUIDE.vi.md).
183
+ For a documentation-style reading view, open the [offline HTML guide](../docs/sdk-python-guide.html).
184
+
185
+ ## LangChain integration
186
+
187
+ The `sdk-python` branch contains the core SDK plus an optional native
188
+ `ChatGateway` model.
189
+
190
+ ```bash
191
+ python -m pip install -e '.[dev,langchain]'
192
+ python -m pytest -q
193
+ ```
194
+
195
+ ```python
196
+ import os
197
+ from ai_gateway.langchain import ChatGateway
198
+ from langchain_core.prompts import ChatPromptTemplate
199
+
200
+ prompt = ChatPromptTemplate.from_messages([
201
+ ("system", "Answer briefly."), ("human", "{question}"),
202
+ ])
203
+ with ChatGateway(
204
+ api_key=os.environ["GATEWAY_API_KEY"],
205
+ base_url="http://localhost:8080/v1",
206
+ model="auto", max_tokens=200,
207
+ gateway_tags={"team": "support"},
208
+ gateway_metadata={"ticket": "t-123"},
209
+ ) as model:
210
+ message = (prompt | model).invoke({"question": "How does routing work?"})
211
+ print(message.content)
212
+ print(message.response_metadata["_gateway"])
213
+ print(message.response_metadata["gateway_headers"])
214
+ print(message.usage_metadata)
215
+ ```
216
+
217
+ `ChatGateway` supports `invoke`, native async `ainvoke`, `batch`/`abatch` and
218
+ prompt pipelines. `max_tokens` is sent unchanged to the Java DTO. Responses
219
+ preserve additional `_gateway` fields, headers, selected model, finish reason,
220
+ and LangChain token usage. `stream`/`astream` emit one **buffered full result**
221
+ via LangChain's fallback; they do not request server SSE or token streaming.
222
+
223
+ ```python
224
+ async with ChatGateway(api_key=os.environ["GATEWAY_API_KEY"],
225
+ base_url="http://localhost:8080/v1") as model:
226
+ messages = await model.abatch(["Explain routing", "Explain budgets"],
227
+ config={"max_concurrency": 2})
228
+ ```
229
+
230
+ Use `gateway_tags` and `gateway_metadata` for gateway headers; LangChain's
231
+ `config={"tags": [...], "metadata": {...}}` controls tracing only. Per-call
232
+ overrides work through `.bind(gateway_tags=..., route_policy=..., no_store=...)`
233
+ or invoke kwargs. Do not bind a fixed idempotency key to a reusable model.
234
+
235
+ Tools, stop sequences, structured output and other advanced chat options fail
236
+ clearly by default because the current backend ignores them. For a future
237
+ backend that implements them, `allow_advanced_options=True` enables forwarding,
238
+ `bind_tools`, tool-history conversion, and inherited `with_structured_output`.
239
+ This switch does not add server capabilities or execute tools.
240
+
241
+ Sync `with`/`close()` closes the owned sync client; async `with`/`aclose()` closes
242
+ both owned clients. If you use both sync and async methods on one instance,
243
+ finish with `await model.aclose()`. Injected `client`/`async_client` SDK clients
244
+ are caller-owned and remain open. API keys and clients are excluded from model
245
+ repr/dumps. LangChain caching is disabled by default; enable caching only with a
246
+ cache scoped to the same tenant/key and routing policy.
247
+
248
+ Only `from ai_gateway.langchain import ChatGateway` imports LangChain. Core
249
+ SDK users can install without the `langchain` extra. See
250
+ `examples/langchain_chat.py` and `examples/langchain_async.py`.
251
+
252
+ ## End-to-end smoke (real stack)
253
+
254
+ `tests/integration/test_e2e_smoke.py` exercises the real
255
+ control plane → publisher → Valkey → Java gateway path with the backend's
256
+ `StubAdapter` as the only substituted LLM upstream. It creates an isolated
257
+ org/project/key over the control plane, publishes a snapshot, checks
258
+ `healthz`/`readyz`/models, chats sync and async (`openai/stub-sdk-smoke`),
259
+ reads project/key metadata, verifies tenant isolation, runs the LangChain
260
+ adapter, rejects a bad key, then revokes the key, republishes and expects
261
+ `401 key_revoked`.
262
+
263
+ The harness is opt-in: without `AIGW_SMOKE=1` the tests skip. With it set, a
264
+ missing config or unreachable service fails loudly. See
265
+ [tests/integration/README.md](tests/integration/README.md) for prerequisites
266
+ and the exact command. Authentication uses the local dev principal
267
+ (`x-dev-sub`/`x-dev-email`), which is not proof of Cognito JWT, and the smoke
268
+ does not assert trace/usage while the usage producer is not wired.
@@ -0,0 +1,256 @@
1
+ # AI Gateway — Python SDK (Level 1)
2
+
3
+ Thin wrapper over the OpenAI-compatible data plane (`docs/API_DESIGN.md §3`,
4
+ `docs/ai-gateway-design.md §4.9`).
5
+
6
+ ## Level 0 — use any OpenAI SDK as-is
7
+
8
+ ```python
9
+ from openai import OpenAI
10
+
11
+ client = OpenAI(
12
+ api_key="gw_live_...",
13
+ base_url="https://gateway.example.com/v1",
14
+ )
15
+ resp = client.chat.completions.create(
16
+ model="auto", # or policy/<name> | <provider>/<model> | alias
17
+ messages=[{"role": "user", "content": "Summarize this ticket..."}],
18
+ )
19
+ ```
20
+
21
+ See `examples/openai_compat.py`.
22
+
23
+ ## Level 1 — this package
24
+
25
+ ```python
26
+ from ai_gateway import GatewayClient
27
+
28
+ client = GatewayClient(
29
+ api_key="gw_live_...",
30
+ base_url="https://gateway.example.com/v1",
31
+ )
32
+ resp = client.chat_completions(
33
+ model="auto",
34
+ messages=[{"role": "user", "content": "hi"}],
35
+ tags={"team": "support", "env": "prod"},
36
+ metadata={"ticket": "t-123"},
37
+ )
38
+ print(resp.content)
39
+ print(resp.gateway.request_id, resp.gateway.tier)
40
+ print(resp.headers.get("x-gateway-provider"))
41
+ ```
42
+
43
+ Request options map to `API_DESIGN §2.5` headers:
44
+
45
+ | Arg | Header |
46
+ |---|---|
47
+ | `tags: dict \| str` | `X-Gateway-Tags: team=support,env=prod` |
48
+ | `metadata: dict` | `X-Gateway-Metadata: {...}` (≤4KB, opaque JSON) |
49
+ | `route_policy: str` | `X-Gateway-Route-Policy` |
50
+ | `no_store: bool` | `X-Gateway-No-Store: true` |
51
+ | `idempotency_key: str` | `Idempotency-Key` (non-streaming retries) |
52
+
53
+ Errors keep the fields `GatewayError` always had (`.message`, `.code`, `.type`,
54
+ `.param`, `.status`, `.request_id`, `.retry_after`) and understand both the
55
+ OpenAI envelope and RFC 7807 problem details (`detail`/`title`/`type`,
56
+ `errors`, extensions). Header lookups are case-insensitive and the HTTP status
57
+ is authoritative. Request ID priority is `X-Gateway-Request-Id` →
58
+ `X-Request-Id` → `traceId`/`requestId`/`request_id` in the body. The parsed body
59
+ stays on `err.payload` (plus `err.errors`/`err.extensions`) so callers can
60
+ inspect it; it may contain sensitive server data, so the SDK never logs it.
61
+
62
+ Failures before a response arrives raise `GatewayTransportError` subclasses:
63
+ `GatewayConnectionError` (connection) and `GatewayTimeoutError` (timeout with a
64
+ `phase` of `connect`/`read`/`write`/`pool`). Their `status` is `0` and the
65
+ original HTTPX exception is on `__cause__`/`.cause`. Migration note: code that
66
+ caught `httpx.TransportError`/`httpx.TimeoutException` should catch
67
+ `GatewayTransportError` (or the narrower classes) instead.
68
+
69
+ No automatic retries are performed; the current Java hot path does not enforce
70
+ the documented idempotency header. Retrying a chat POST may duplicate billing.
71
+
72
+ ## Install and verify
73
+
74
+ ```bash
75
+ python -m pip install -e '.[dev]'
76
+ python -m pytest -q
77
+ ```
78
+
79
+ The default run skips `tests/integration` (real-stack smoke). CI has separate
80
+ `sdk-core` (no LangChain), `sdk-langchain` (extra required) and `sdk-package`
81
+ (wheel/sdist installed into a clean venv outside the source tree) jobs, across
82
+ Python 3.10/3.12 and HTTPX 0.27.0/0.28.1, with `langchain-core` 1.0.0/1.6.7.
83
+ The adapter tests import LangChain directly, so a missing dependency fails
84
+ collection instead of skipping green. Publishing a GitHub Release runs
85
+ `.github/workflows/publish.yml`, which rebuilds the same artifacts and uploads
86
+ them to PyPI with Trusted Publishing (no stored token).
87
+
88
+ ## Async data plane
89
+
90
+ ```python
91
+ import asyncio
92
+ import os
93
+ from ai_gateway import AsyncGatewayClient
94
+
95
+ async def main():
96
+ async with AsyncGatewayClient(
97
+ api_key=os.environ["GATEWAY_API_KEY"],
98
+ base_url=os.getenv("GATEWAY_BASE_URL", "http://localhost:8080/v1"),
99
+ ) as client:
100
+ response = await client.chat_completions(
101
+ model="auto", messages=[{"role": "user", "content": "Hello"}],
102
+ )
103
+ print(response.content, response.gateway.request_id)
104
+
105
+ asyncio.run(main())
106
+ ```
107
+
108
+ `AsyncGatewayClient` mirrors chat, models, health, ready, streaming and text
109
+ collection. Use `aclose()` or `async with`; the sync client uses `close()` or `with`.
110
+ Both accept an HTTPX timeout and transport for contract testing.
111
+
112
+ Health probes call `/healthz` and `/readyz` at the service root, without the API
113
+ key. A proxy mount prefix is retained (`/gateway/v1` → `/gateway/healthz`).
114
+
115
+ `temperature` and `max_tokens` work with the current Java DTO. `top_p`, `stop`,
116
+ `tools`, `tool_choice`, `response_format`, `user`, and `seed` are documented
117
+ pass-through options for future backends: the current backend ignores them.
118
+ Gateway headers are forwarded; setting tags/no-store/metadata does not by itself
119
+ implement server-side enforcement. Metadata is checked against the 4KB limit.
120
+ Tool-only completions with `content: null` expose an empty `.content` string and
121
+ preserve tool calls in `.choices` and `.raw`.
122
+
123
+ Streaming helpers parse SSE including multiline events and surface malformed
124
+ events and stream errors. The current backend returns JSON even with
125
+ `stream: true`; helpers raise `GatewayError(code="streaming_not_supported")`
126
+ instead of silently returning no text. They do not simulate token streaming.
127
+
128
+ ## Dashboard reads (serverless control plane)
129
+
130
+ This is a separate service and uses an OIDC access token, not a `gw_live_` key.
131
+ These endpoints require the serverless control-plane deployment. Both SDK
132
+ branches now include its source after synchronization with `origin/main` on
133
+ 2026-10-07; source presence does not mean the service is already deployed.
134
+
135
+ ```python
136
+ import os
137
+ from ai_gateway import ControlPlaneClient
138
+
139
+ with ControlPlaneClient(
140
+ base_url=os.environ["CONTROL_PLANE_BASE_URL"], # includes /api/v1
141
+ access_token=os.environ["CONTROL_PLANE_ACCESS_TOKEN"],
142
+ ) as dashboard:
143
+ print(dashboard.me())
144
+ for request in dashboard.iter_items(
145
+ dashboard.list_requests, "org_example", limit=100,
146
+ **{"from": "2026-10-01T00:00:00Z", "projectId": "prj_example"},
147
+ ):
148
+ print(request["requestId"])
149
+ print(dashboard.get_usage("org_example", group_by="project").data)
150
+ ```
151
+
152
+ An access-token callable can supply a refreshed token on every request. Local
153
+ development explicitly uses `dev_subject="..."` instead of `access_token`.
154
+ The client does not authenticate users or refresh tokens itself.
155
+
156
+ Read helpers: `me`, `list_orgs`, `list_projects`, `list_api_keys`,
157
+ `list_providers`, `list_models`, `list_routing_policies`, `list_policy_versions`,
158
+ `get_usage`, `list_requests`, `get_request`, `get_routing_insights`,
159
+ `list_audit_logs`. List helpers return `Page(data, next_cursor, has_more)`;
160
+ `iter_items` traverses all pages and fails on missing/repeated cursors. Query
161
+ kwargs use wire names (`projectId`, `minLatencyMs`, `cursor`, `limit`, etc.).
162
+ Usage is daily only; hourly granularity is not implemented by the control plane.
163
+ Wire dictionaries preserve nullable and additional fields. Key reads return
164
+ metadata only; no SDK secret persistence or key mutations are added here.
165
+
166
+ Research and prioritized endpoint/function suggestions:
167
+ [RESEARCH.md](RESEARCH.md).
168
+
169
+ Vietnamese architecture guide with component flows, methods, compatibility,
170
+ examples and troubleshooting: [SDK guide](../docs/SDK_PYTHON_GUIDE.vi.md).
171
+ For a documentation-style reading view, open the [offline HTML guide](../docs/sdk-python-guide.html).
172
+
173
+ ## LangChain integration
174
+
175
+ The `sdk-python` branch contains the core SDK plus an optional native
176
+ `ChatGateway` model.
177
+
178
+ ```bash
179
+ python -m pip install -e '.[dev,langchain]'
180
+ python -m pytest -q
181
+ ```
182
+
183
+ ```python
184
+ import os
185
+ from ai_gateway.langchain import ChatGateway
186
+ from langchain_core.prompts import ChatPromptTemplate
187
+
188
+ prompt = ChatPromptTemplate.from_messages([
189
+ ("system", "Answer briefly."), ("human", "{question}"),
190
+ ])
191
+ with ChatGateway(
192
+ api_key=os.environ["GATEWAY_API_KEY"],
193
+ base_url="http://localhost:8080/v1",
194
+ model="auto", max_tokens=200,
195
+ gateway_tags={"team": "support"},
196
+ gateway_metadata={"ticket": "t-123"},
197
+ ) as model:
198
+ message = (prompt | model).invoke({"question": "How does routing work?"})
199
+ print(message.content)
200
+ print(message.response_metadata["_gateway"])
201
+ print(message.response_metadata["gateway_headers"])
202
+ print(message.usage_metadata)
203
+ ```
204
+
205
+ `ChatGateway` supports `invoke`, native async `ainvoke`, `batch`/`abatch` and
206
+ prompt pipelines. `max_tokens` is sent unchanged to the Java DTO. Responses
207
+ preserve additional `_gateway` fields, headers, selected model, finish reason,
208
+ and LangChain token usage. `stream`/`astream` emit one **buffered full result**
209
+ via LangChain's fallback; they do not request server SSE or token streaming.
210
+
211
+ ```python
212
+ async with ChatGateway(api_key=os.environ["GATEWAY_API_KEY"],
213
+ base_url="http://localhost:8080/v1") as model:
214
+ messages = await model.abatch(["Explain routing", "Explain budgets"],
215
+ config={"max_concurrency": 2})
216
+ ```
217
+
218
+ Use `gateway_tags` and `gateway_metadata` for gateway headers; LangChain's
219
+ `config={"tags": [...], "metadata": {...}}` controls tracing only. Per-call
220
+ overrides work through `.bind(gateway_tags=..., route_policy=..., no_store=...)`
221
+ or invoke kwargs. Do not bind a fixed idempotency key to a reusable model.
222
+
223
+ Tools, stop sequences, structured output and other advanced chat options fail
224
+ clearly by default because the current backend ignores them. For a future
225
+ backend that implements them, `allow_advanced_options=True` enables forwarding,
226
+ `bind_tools`, tool-history conversion, and inherited `with_structured_output`.
227
+ This switch does not add server capabilities or execute tools.
228
+
229
+ Sync `with`/`close()` closes the owned sync client; async `with`/`aclose()` closes
230
+ both owned clients. If you use both sync and async methods on one instance,
231
+ finish with `await model.aclose()`. Injected `client`/`async_client` SDK clients
232
+ are caller-owned and remain open. API keys and clients are excluded from model
233
+ repr/dumps. LangChain caching is disabled by default; enable caching only with a
234
+ cache scoped to the same tenant/key and routing policy.
235
+
236
+ Only `from ai_gateway.langchain import ChatGateway` imports LangChain. Core
237
+ SDK users can install without the `langchain` extra. See
238
+ `examples/langchain_chat.py` and `examples/langchain_async.py`.
239
+
240
+ ## End-to-end smoke (real stack)
241
+
242
+ `tests/integration/test_e2e_smoke.py` exercises the real
243
+ control plane → publisher → Valkey → Java gateway path with the backend's
244
+ `StubAdapter` as the only substituted LLM upstream. It creates an isolated
245
+ org/project/key over the control plane, publishes a snapshot, checks
246
+ `healthz`/`readyz`/models, chats sync and async (`openai/stub-sdk-smoke`),
247
+ reads project/key metadata, verifies tenant isolation, runs the LangChain
248
+ adapter, rejects a bad key, then revokes the key, republishes and expects
249
+ `401 key_revoked`.
250
+
251
+ The harness is opt-in: without `AIGW_SMOKE=1` the tests skip. With it set, a
252
+ missing config or unreachable service fails loudly. See
253
+ [tests/integration/README.md](tests/integration/README.md) for prerequisites
254
+ and the exact command. Authentication uses the local dev principal
255
+ (`x-dev-sub`/`x-dev-email`), which is not proof of Cognito JWT, and the smoke
256
+ does not assert trace/usage while the usage producer is not wired.
@@ -0,0 +1,95 @@
1
+ # Python SDK expansion and LangChain integration
2
+
3
+ Research date: 2026-10-05. Base: `feat/sdk-python` at `6f7414b`.
4
+ Two deliverables: `sdk-python-expand` and `sdk-python-langchain`.
5
+ LangChain builds on the expansion commit so transport/error handling is shared.
6
+
7
+ Architecture update: 2026-10-07. Both branches now contain `origin/main` at
8
+ `dd0129f`, including the serverless control plane and direct Valkey snapshot
9
+ reads. The historical base constraints below describe the initial research.
10
+ Current flows, scaffold limitations, methods and compatibility are explained in
11
+ [the Vietnamese SDK guide](../docs/SDK_PYTHON_GUIDE.vi.md).
12
+
13
+ ## Evidence and constraints
14
+
15
+ - `docs/API_DESIGN.md` §§2–4 defines chat, model catalog, health, headers,
16
+ control-plane auth, pagination and errors. Backend code takes precedence
17
+ over features described only in the design.
18
+ - `backend/.../gateway/web/ChatDtos.java` only reads model/messages/temperature/
19
+ max_tokens/stream. Provider input currently has no tools, stop or structured
20
+ output. The controller returns JSON, not SSE.
21
+ - `HealthController` serves `/healthz` and `/readyz` at the root. The previous
22
+ SDK incorrectly requested `/v1/healthz` and `/v1/readyz`.
23
+ - The serverless route tables and read handlers were inspected on
24
+ `codex/clarify-firehose-raw` at `8cbbf4f`. That checkout includes
25
+ `docs/API_CONTRACT_AUDIT.md`; this SDK base does not include the control plane.
26
+ M1/M2 make message-based routing simulation unreliable; M4 is daily-only
27
+ usage; M5 requires preserving nulls; M7 requires visible pagination.
28
+ - [HTTPX async guide](https://www.python-httpx.org/async/) supports pooled
29
+ `AsyncClient` requests and streaming with explicit closure.
30
+ - [HTTPX transports](https://www.python-httpx.org/advanced/transports/) supplies
31
+ `MockTransport` for deterministic wire-contract tests without live credentials.
32
+ - [LangChain ChatOpenAI documentation](https://docs.langchain.com/oss/python/integrations/chat/openai)
33
+ supports a custom `base_url`, but notes that nonstandard response fields are
34
+ not preserved. It also converts `max_tokens` to `max_completion_tokens`.
35
+ These are concrete reasons for a native gateway adapter: keep `_gateway` and
36
+ send the `max_tokens` that the current Java DTO actually reads.
37
+ - [LangChain BaseChatModel reference](https://reference.langchain.com/python/langchain-core/language_models/chat_models)
38
+ defines the custom model contract. Its installed source is checked alongside
39
+ tests for invoke/ainvoke, prompt pipelines, batching and usage metadata.
40
+
41
+ ## Recommended SDK functions and endpoints
42
+
43
+ | Priority | Function / endpoint | Why | Decision |
44
+ | --- | --- | --- | --- |
45
+ | P0 | Async chat, models, probes | FastAPI/async apps need nonblocking I/O | Implemented |
46
+ | P0 | Fix health URLs | Existing SDK probes target nonexistent paths | Implemented |
47
+ | P0 | Error details + Retry-After | Preserve failure context for callers | Implemented |
48
+ | P0 | Null content + raw tool calls | Prevent `None` becoming literal text | Implemented |
49
+ | P0 | Streaming content-type/error validation | Avoid silently losing JSON responses | Implemented; server SSE still pending |
50
+ | P1 | `GET /me`, orgs/projects | Discover identity and tenant scope | Read helpers implemented |
51
+ | P1 | `GET /orgs/{id}/usage` | Cost reports and budget automation | Implemented daily reads |
52
+ | P1 | Requests + `GET /requests/{id}` | Explain routing/fallbacks after a call | Implemented with cursor iteration |
53
+ | P1 | Providers, catalog, policies/versions | Discover available models and routing config | Read helpers implemented |
54
+ | P1 | Routing insights + audit logs | Compare route behavior and admin activity | Read helpers implemented |
55
+ | P1 | API-key metadata lists | Inspect keys without returning secrets | Read helper implemented |
56
+ | P1 | Native LangChain chat model | Preserve route/cost/usage in Runnable flows | Separate integration branch |
57
+ | P2 | Create/rotate/revoke keys | CI provisioning and service automation | Next iteration; idempotency + one-time secret handling required |
58
+ | P2 | Policy create/update/publish/simulate | Programmatic routing rollout | Defer until simulate M1/M2 resolved; honor If-Match/412 |
59
+ | P2 | Async control-plane reads | Large asynchronous monitoring clients | Follow-up after sync contract is exercised |
60
+ | P2 | Tools + response_format end to end | LangChain agents and structured output | SDK pass-through only; extend backend DTO/adapters first |
61
+ | P2 | `POST /v1/embeddings` | RAG/vector indexing | Design P2; no server implementation, no callable SDK stub |
62
+ | P3 | `/v1/responses`, `/v1/messages` | Native advanced provider protocols | Explicitly deferred by API_DESIGN §3.5 |
63
+
64
+ ## LangChain choice (implemented on sdk-python-langchain)
65
+
66
+ Use a small `BaseChatModel` adapter over `GatewayClient` and
67
+ `AsyncGatewayClient`, with `langchain-core` as an optional dependency. Avoid a
68
+ mandatory full `langchain`/`langchain-openai` dependency for ordinary SDK users.
69
+ Support `invoke`, `ainvoke`, prompt pipes and batching; return gateway fields,
70
+ headers, selected model, finish reason and token usage on `AIMessage`.
71
+ Keep API keys excluded from repr/serialization and expose explicit closure.
72
+ The implementation uses `langchain-core>=1,<2` as an optional extra; the default
73
+ SDK import does not load it. HTTP requests remain on `/v1/chat/completions`.
74
+
75
+ For this Java backend, `stream`/`astream` must use LangChain's buffered fallback
76
+ and tools/stop/structured-output requests must fail clearly unless the caller
77
+ explicitly enables future-backend pass-through. Do not advertise token streaming,
78
+ agent tool execution, or embeddings as current server capabilities.
79
+
80
+ ## Validation boundary
81
+
82
+ Contract tests validate paths, payloads, auth headers, token refresh hooks,
83
+ pagination, SSE parsing, error envelopes and LangChain conversion using mocked
84
+ HTTP responses. Wheel/sdist builds validate packaging. These do not prove an
85
+ OIDC deployment, a live provider, actual SSE, or gateway header enforcement.
86
+ No live requests with real credentials or changes to backend/schema are needed.
87
+
88
+ Verified results on Python 3.12 / HTTPX 0.28.1:
89
+
90
+ - Expansion commit: 34 tests passed; wheel and sdist built.
91
+ - LangChain branch: 50 tests passed with both `langchain-core` 1.6.6 and 1.0.0
92
+ (the minimum package was isolated on PYTHONPATH with the same dependencies).
93
+ - Integration wheel/sdist built; wheel metadata keeps LangChain optional.
94
+ - All SDK modules parse with Python 3.10 syntax rules. Python 3.10 runtime and
95
+ dependency combinations beyond the versions above were not exercised.
@@ -0,0 +1,22 @@
1
+ """Native async chat; configure GATEWAY_API_KEY and GATEWAY_BASE_URL."""
2
+
3
+ import asyncio
4
+ import os
5
+
6
+ from ai_gateway import AsyncGatewayClient
7
+
8
+
9
+ async def main():
10
+ async with AsyncGatewayClient(
11
+ api_key=os.environ["GATEWAY_API_KEY"],
12
+ base_url=os.getenv("GATEWAY_BASE_URL", "http://localhost:8080/v1"),
13
+ ) as client:
14
+ response = await client.chat_completions(
15
+ model="auto", messages=[{"role": "user", "content": "Explain gateway routing."}],
16
+ )
17
+ print(response.content)
18
+ print(response.gateway)
19
+
20
+
21
+ if __name__ == "__main__":
22
+ asyncio.run(main())
@@ -0,0 +1,23 @@
1
+ """Basic Level 1 usage (needs a live gateway + GW_API_KEY env)."""
2
+
3
+ import os
4
+
5
+ from ai_gateway import GatewayClient
6
+
7
+
8
+ def main() -> None:
9
+ api_key = os.environ.get("GW_API_KEY", "gw_live_demo")
10
+ base_url = os.environ.get("GW_BASE_URL", "http://localhost:8080/v1")
11
+ with GatewayClient(api_key=api_key, base_url=base_url) as client:
12
+ resp = client.chat_completions(
13
+ model="auto",
14
+ messages=[{"role": "user", "content": "Summarize this ticket..."}],
15
+ tags={"team": "support", "env": "dev"},
16
+ )
17
+ print(resp.content)
18
+ print("request:", resp.gateway.request_id, "tier:", resp.gateway.tier)
19
+ print("provider:", resp.headers.get("x-gateway-provider"))
20
+
21
+
22
+ if __name__ == "__main__":
23
+ main()