modelgov 1.0.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.
- modelgov-1.0.0/.gitignore +7 -0
- modelgov-1.0.0/PKG-INFO +272 -0
- modelgov-1.0.0/README.md +256 -0
- modelgov-1.0.0/modelgov/__init__.py +82 -0
- modelgov-1.0.0/modelgov/client.py +564 -0
- modelgov-1.0.0/modelgov/errors.py +96 -0
- modelgov-1.0.0/modelgov/py.typed +0 -0
- modelgov-1.0.0/modelgov/types.py +236 -0
- modelgov-1.0.0/pyproject.toml +32 -0
- modelgov-1.0.0/tests/test_client.py +587 -0
modelgov-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: modelgov
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Modelgov API client. `feature` and `userType` are mandatory on every request.
|
|
5
|
+
Project-URL: Homepage, https://github.com/mml555/modelgov
|
|
6
|
+
Author: Modelgov
|
|
7
|
+
License: MIT
|
|
8
|
+
Keywords: budget,gateway,llm,modelgov,policy,safety
|
|
9
|
+
Requires-Python: >=3.9
|
|
10
|
+
Requires-Dist: httpx>=0.27
|
|
11
|
+
Requires-Dist: typing-extensions>=4.0; python_version < '3.11'
|
|
12
|
+
Provides-Extra: dev
|
|
13
|
+
Requires-Dist: pytest; extra == 'dev'
|
|
14
|
+
Requires-Dist: respx; extra == 'dev'
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
# Modelgov Python SDK
|
|
18
|
+
|
|
19
|
+
Package: `modelgov` (module `modelgov`). The Python counterpart to
|
|
20
|
+
[`@modelgov/sdk`](../sdk-typescript).
|
|
21
|
+
|
|
22
|
+
The SDK is a **thin HTTP client** to the Modelgov API. Policy enforcement is
|
|
23
|
+
always server-side. Every request declares a **user**, **user type**, and
|
|
24
|
+
**feature**; policy is checked **before** the model call.
|
|
25
|
+
|
|
26
|
+
## Install
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install modelgov
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
> Note: `modelgov` is not yet published to PyPI. Until then, install from
|
|
33
|
+
> source with the editable install below (see also [self-host.md](../../docs/self-host.md)).
|
|
34
|
+
|
|
35
|
+
From the monorepo (editable, with test deps):
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install -e "packages/sdk-python[dev]"
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Requires Python >= 3.9. Depends on [`httpx`](https://www.python-httpx.org/).
|
|
42
|
+
|
|
43
|
+
## Create a client
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
import os
|
|
47
|
+
from modelgov import ModelgovClient
|
|
48
|
+
|
|
49
|
+
ai = ModelgovClient(
|
|
50
|
+
base_url=os.environ.get("MODELGOV_URL", "http://localhost:3000"),
|
|
51
|
+
api_key=os.environ["MODELGOV_API_KEY"],
|
|
52
|
+
)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`ModelgovClient` is a context manager and closes its connection pool on exit:
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
with ModelgovClient(base_url=..., api_key=...) as ai:
|
|
59
|
+
...
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Chat
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
res = ai.chat(
|
|
66
|
+
user_id="user_123", # your end-user id
|
|
67
|
+
user_type="logged_in", # must match modelgov.yaml budgets
|
|
68
|
+
feature="support_chat", # required — registered feature
|
|
69
|
+
model_class="cheap",
|
|
70
|
+
messages=[{"role": "user", "content": "Help me reset my password"}],
|
|
71
|
+
# optional:
|
|
72
|
+
# input_tokens_estimate=120,
|
|
73
|
+
# temperature=0.7,
|
|
74
|
+
# project_id="checkout",
|
|
75
|
+
# environment="production",
|
|
76
|
+
# metadata={"trace_id": "abc"},
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
print(res["message"]["content"])
|
|
80
|
+
print(res["model"], res["decision"], res["requestId"])
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Snake_case keyword args are converted to the camelCase JSON the API expects
|
|
84
|
+
(`user_id` → `userId`, `model_class` → `modelClass`, etc.). `None`-valued
|
|
85
|
+
optional args are omitted from the request body.
|
|
86
|
+
|
|
87
|
+
### Response
|
|
88
|
+
|
|
89
|
+
`chat()` returns a `ChatResponse` (a `TypedDict`), so it is a plain `dict` with
|
|
90
|
+
typed keys:
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
{
|
|
94
|
+
"message": {"role": "assistant", "content": "..."},
|
|
95
|
+
"model": "openai/gpt-4o-mini",
|
|
96
|
+
"decision": "allow", # "allow" | "degrade" | "fallback"
|
|
97
|
+
"usage": {"inputTokens": 12, "outputTokens": 8},
|
|
98
|
+
"cost": {"estimatedUsd": 0.0001, "actualUsd": 0.00008},
|
|
99
|
+
"budgetRemaining": {"userDailyUsd": 0.24, "featureMonthlyUsd": None, "globalMonthlyUsd": 499.5},
|
|
100
|
+
"safety": {"piiMasked": False, "injectionBlocked": False},
|
|
101
|
+
"requestId": "req_42", # audit id — log with your domain ids
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Vision (multimodal)
|
|
106
|
+
|
|
107
|
+
Pass content parts instead of a string to send images to a vision model. The
|
|
108
|
+
gateway governs budget/audit and still runs safety on the text parts:
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
res = ai.chat(
|
|
112
|
+
user_id="user_123",
|
|
113
|
+
user_type="logged_in",
|
|
114
|
+
feature="document_extraction",
|
|
115
|
+
messages=[{
|
|
116
|
+
"role": "user",
|
|
117
|
+
"content": [
|
|
118
|
+
{"type": "text", "text": "Extract the total from this receipt."},
|
|
119
|
+
{"type": "image_url", "image_url": {"url": "data:image/png;base64,..."}},
|
|
120
|
+
],
|
|
121
|
+
}],
|
|
122
|
+
)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Grounding
|
|
126
|
+
|
|
127
|
+
For a feature with safety `grounding: strict`, pass retrieved passages as
|
|
128
|
+
`context`. The gateway answers only from them, forces verbatim citations, and
|
|
129
|
+
verifies them — unverifiable answers become a safe refusal, and
|
|
130
|
+
`res["safety"]["grounded"]` reports whether the citations checked out:
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
res = ai.chat(
|
|
134
|
+
user_id="user_123",
|
|
135
|
+
user_type="logged_in",
|
|
136
|
+
feature="grounded_support",
|
|
137
|
+
messages=[{"role": "user", "content": "How long do refunds take?"}],
|
|
138
|
+
context=["Refunds are issued within 5 business days of approval."],
|
|
139
|
+
)
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Streaming
|
|
143
|
+
|
|
144
|
+
`chat_stream()` yields incremental text chunks over Server-Sent Events. It
|
|
145
|
+
sends `"stream": true` and iterates `data:` lines until the `[DONE]` sentinel.
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
for chunk in ai.chat_stream(
|
|
149
|
+
user_id="user_123",
|
|
150
|
+
user_type="logged_in",
|
|
151
|
+
feature="support_chat",
|
|
152
|
+
messages=[{"role": "user", "content": "Write a haiku about budgets"}],
|
|
153
|
+
):
|
|
154
|
+
print(chunk, end="", flush=True)
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
**SSE framing assumption:** OpenAI-style events — one JSON payload per `data:`
|
|
158
|
+
line, terminated by `data: [DONE]`. Text is read from
|
|
159
|
+
`choices[0].delta.content` (or a simpler `delta` / `content` / `text` field).
|
|
160
|
+
Non-JSON `data:` payloads are yielded verbatim. See the `chat_stream` docstring
|
|
161
|
+
if the server's framing differs.
|
|
162
|
+
|
|
163
|
+
The generator holds the connection open until fully consumed. Policy/safety
|
|
164
|
+
blocks that occur before the stream begins raise the usual typed errors.
|
|
165
|
+
|
|
166
|
+
## Embeddings
|
|
167
|
+
|
|
168
|
+
`embed()` runs governed embeddings (`POST /v1/embeddings`) — policy-checked,
|
|
169
|
+
budget-reserved, and audited like `chat()`. Pass one string or a batch:
|
|
170
|
+
|
|
171
|
+
```python
|
|
172
|
+
res = ai.embed(
|
|
173
|
+
user_id="user_123",
|
|
174
|
+
user_type="logged_in",
|
|
175
|
+
feature="rag_ingest",
|
|
176
|
+
input=["first passage", "second passage"], # or a single string
|
|
177
|
+
)
|
|
178
|
+
vectors = res["embeddings"] # one vector per input, in request order
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## Idempotency
|
|
182
|
+
|
|
183
|
+
Pass a stable key to retry safely without double-charging budget or re-calling
|
|
184
|
+
the model:
|
|
185
|
+
|
|
186
|
+
```python
|
|
187
|
+
ai.chat(
|
|
188
|
+
user_id="user_123",
|
|
189
|
+
user_type="logged_in",
|
|
190
|
+
feature="support_chat",
|
|
191
|
+
messages=[{"role": "user", "content": "..."}],
|
|
192
|
+
idempotency_key=f"chat-{user_id}-{session_id}",
|
|
193
|
+
)
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
The API returns `x-idempotent-replay: true` on cache hits; a same-key request
|
|
197
|
+
with a different body returns `422 idempotency_key_reuse`.
|
|
198
|
+
|
|
199
|
+
## Explain (dry run)
|
|
200
|
+
|
|
201
|
+
Evaluate policy without calling the model or reserving budget:
|
|
202
|
+
|
|
203
|
+
```python
|
|
204
|
+
plan = ai.explain(
|
|
205
|
+
user_id="user_123",
|
|
206
|
+
user_type="logged_in",
|
|
207
|
+
feature="support_chat",
|
|
208
|
+
model_class="premium",
|
|
209
|
+
)
|
|
210
|
+
print(plan["decision"], plan["summary"])
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
## Usage
|
|
214
|
+
|
|
215
|
+
Requires an API key with `usage:read`.
|
|
216
|
+
|
|
217
|
+
```python
|
|
218
|
+
usage = ai.get_usage(user_id="user_123")
|
|
219
|
+
summary = ai.get_usage_summary(feature="support_chat", since="7d")
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
## Errors
|
|
223
|
+
|
|
224
|
+
| Class | When |
|
|
225
|
+
| --- | --- |
|
|
226
|
+
| `PolicyBlockedError` | 403 `policy_blocked` or `budget_exceeded` |
|
|
227
|
+
| `SafetyBlockedError` | 403 `safety_blocked` (PII or prompt injection) |
|
|
228
|
+
| `ModelgovError` | Other 4xx / 5xx |
|
|
229
|
+
|
|
230
|
+
`PolicyBlockedError` and `SafetyBlockedError` subclass `ModelgovError`. Each
|
|
231
|
+
error carries the API's structured envelope:
|
|
232
|
+
|
|
233
|
+
```python
|
|
234
|
+
from modelgov import ModelgovError, PolicyBlockedError, SafetyBlockedError
|
|
235
|
+
|
|
236
|
+
try:
|
|
237
|
+
ai.chat(
|
|
238
|
+
user_id="user_123",
|
|
239
|
+
user_type="logged_in",
|
|
240
|
+
feature="support_chat",
|
|
241
|
+
messages=[{"role": "user", "content": "..."}],
|
|
242
|
+
)
|
|
243
|
+
except PolicyBlockedError as err:
|
|
244
|
+
print(err.status) # 403
|
|
245
|
+
print(err.code) # "policy_blocked" | "budget_exceeded"
|
|
246
|
+
print(err.message) # human-readable
|
|
247
|
+
print(err.details) # error.details object
|
|
248
|
+
print(err.audit_request_id) # "req_<n>" — modelgov requests show
|
|
249
|
+
print(err.request_id) # HTTP trace id (UUID)
|
|
250
|
+
print(err.body) # full parsed envelope
|
|
251
|
+
except ModelgovError as err:
|
|
252
|
+
...
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
## Integration pattern
|
|
256
|
+
|
|
257
|
+
```text
|
|
258
|
+
1. Authenticate user (your app)
|
|
259
|
+
2. Authorize product action (your app)
|
|
260
|
+
3. ai.chat(user_id=..., user_type=..., feature=..., messages=...)
|
|
261
|
+
4. Return res["message"]["content"] to the user
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
Never call Modelgov before your app has decided the user may use this feature.
|
|
265
|
+
|
|
266
|
+
## Development
|
|
267
|
+
|
|
268
|
+
```bash
|
|
269
|
+
pip install -e "packages/sdk-python[dev]"
|
|
270
|
+
cd packages/sdk-python
|
|
271
|
+
pytest
|
|
272
|
+
```
|
modelgov-1.0.0/README.md
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
# Modelgov Python SDK
|
|
2
|
+
|
|
3
|
+
Package: `modelgov` (module `modelgov`). The Python counterpart to
|
|
4
|
+
[`@modelgov/sdk`](../sdk-typescript).
|
|
5
|
+
|
|
6
|
+
The SDK is a **thin HTTP client** to the Modelgov API. Policy enforcement is
|
|
7
|
+
always server-side. Every request declares a **user**, **user type**, and
|
|
8
|
+
**feature**; policy is checked **before** the model call.
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
pip install modelgov
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
> Note: `modelgov` is not yet published to PyPI. Until then, install from
|
|
17
|
+
> source with the editable install below (see also [self-host.md](../../docs/self-host.md)).
|
|
18
|
+
|
|
19
|
+
From the monorepo (editable, with test deps):
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install -e "packages/sdk-python[dev]"
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Requires Python >= 3.9. Depends on [`httpx`](https://www.python-httpx.org/).
|
|
26
|
+
|
|
27
|
+
## Create a client
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
import os
|
|
31
|
+
from modelgov import ModelgovClient
|
|
32
|
+
|
|
33
|
+
ai = ModelgovClient(
|
|
34
|
+
base_url=os.environ.get("MODELGOV_URL", "http://localhost:3000"),
|
|
35
|
+
api_key=os.environ["MODELGOV_API_KEY"],
|
|
36
|
+
)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
`ModelgovClient` is a context manager and closes its connection pool on exit:
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
with ModelgovClient(base_url=..., api_key=...) as ai:
|
|
43
|
+
...
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Chat
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
res = ai.chat(
|
|
50
|
+
user_id="user_123", # your end-user id
|
|
51
|
+
user_type="logged_in", # must match modelgov.yaml budgets
|
|
52
|
+
feature="support_chat", # required — registered feature
|
|
53
|
+
model_class="cheap",
|
|
54
|
+
messages=[{"role": "user", "content": "Help me reset my password"}],
|
|
55
|
+
# optional:
|
|
56
|
+
# input_tokens_estimate=120,
|
|
57
|
+
# temperature=0.7,
|
|
58
|
+
# project_id="checkout",
|
|
59
|
+
# environment="production",
|
|
60
|
+
# metadata={"trace_id": "abc"},
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
print(res["message"]["content"])
|
|
64
|
+
print(res["model"], res["decision"], res["requestId"])
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Snake_case keyword args are converted to the camelCase JSON the API expects
|
|
68
|
+
(`user_id` → `userId`, `model_class` → `modelClass`, etc.). `None`-valued
|
|
69
|
+
optional args are omitted from the request body.
|
|
70
|
+
|
|
71
|
+
### Response
|
|
72
|
+
|
|
73
|
+
`chat()` returns a `ChatResponse` (a `TypedDict`), so it is a plain `dict` with
|
|
74
|
+
typed keys:
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
{
|
|
78
|
+
"message": {"role": "assistant", "content": "..."},
|
|
79
|
+
"model": "openai/gpt-4o-mini",
|
|
80
|
+
"decision": "allow", # "allow" | "degrade" | "fallback"
|
|
81
|
+
"usage": {"inputTokens": 12, "outputTokens": 8},
|
|
82
|
+
"cost": {"estimatedUsd": 0.0001, "actualUsd": 0.00008},
|
|
83
|
+
"budgetRemaining": {"userDailyUsd": 0.24, "featureMonthlyUsd": None, "globalMonthlyUsd": 499.5},
|
|
84
|
+
"safety": {"piiMasked": False, "injectionBlocked": False},
|
|
85
|
+
"requestId": "req_42", # audit id — log with your domain ids
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Vision (multimodal)
|
|
90
|
+
|
|
91
|
+
Pass content parts instead of a string to send images to a vision model. The
|
|
92
|
+
gateway governs budget/audit and still runs safety on the text parts:
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
res = ai.chat(
|
|
96
|
+
user_id="user_123",
|
|
97
|
+
user_type="logged_in",
|
|
98
|
+
feature="document_extraction",
|
|
99
|
+
messages=[{
|
|
100
|
+
"role": "user",
|
|
101
|
+
"content": [
|
|
102
|
+
{"type": "text", "text": "Extract the total from this receipt."},
|
|
103
|
+
{"type": "image_url", "image_url": {"url": "data:image/png;base64,..."}},
|
|
104
|
+
],
|
|
105
|
+
}],
|
|
106
|
+
)
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Grounding
|
|
110
|
+
|
|
111
|
+
For a feature with safety `grounding: strict`, pass retrieved passages as
|
|
112
|
+
`context`. The gateway answers only from them, forces verbatim citations, and
|
|
113
|
+
verifies them — unverifiable answers become a safe refusal, and
|
|
114
|
+
`res["safety"]["grounded"]` reports whether the citations checked out:
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
res = ai.chat(
|
|
118
|
+
user_id="user_123",
|
|
119
|
+
user_type="logged_in",
|
|
120
|
+
feature="grounded_support",
|
|
121
|
+
messages=[{"role": "user", "content": "How long do refunds take?"}],
|
|
122
|
+
context=["Refunds are issued within 5 business days of approval."],
|
|
123
|
+
)
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Streaming
|
|
127
|
+
|
|
128
|
+
`chat_stream()` yields incremental text chunks over Server-Sent Events. It
|
|
129
|
+
sends `"stream": true` and iterates `data:` lines until the `[DONE]` sentinel.
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
for chunk in ai.chat_stream(
|
|
133
|
+
user_id="user_123",
|
|
134
|
+
user_type="logged_in",
|
|
135
|
+
feature="support_chat",
|
|
136
|
+
messages=[{"role": "user", "content": "Write a haiku about budgets"}],
|
|
137
|
+
):
|
|
138
|
+
print(chunk, end="", flush=True)
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
**SSE framing assumption:** OpenAI-style events — one JSON payload per `data:`
|
|
142
|
+
line, terminated by `data: [DONE]`. Text is read from
|
|
143
|
+
`choices[0].delta.content` (or a simpler `delta` / `content` / `text` field).
|
|
144
|
+
Non-JSON `data:` payloads are yielded verbatim. See the `chat_stream` docstring
|
|
145
|
+
if the server's framing differs.
|
|
146
|
+
|
|
147
|
+
The generator holds the connection open until fully consumed. Policy/safety
|
|
148
|
+
blocks that occur before the stream begins raise the usual typed errors.
|
|
149
|
+
|
|
150
|
+
## Embeddings
|
|
151
|
+
|
|
152
|
+
`embed()` runs governed embeddings (`POST /v1/embeddings`) — policy-checked,
|
|
153
|
+
budget-reserved, and audited like `chat()`. Pass one string or a batch:
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
res = ai.embed(
|
|
157
|
+
user_id="user_123",
|
|
158
|
+
user_type="logged_in",
|
|
159
|
+
feature="rag_ingest",
|
|
160
|
+
input=["first passage", "second passage"], # or a single string
|
|
161
|
+
)
|
|
162
|
+
vectors = res["embeddings"] # one vector per input, in request order
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## Idempotency
|
|
166
|
+
|
|
167
|
+
Pass a stable key to retry safely without double-charging budget or re-calling
|
|
168
|
+
the model:
|
|
169
|
+
|
|
170
|
+
```python
|
|
171
|
+
ai.chat(
|
|
172
|
+
user_id="user_123",
|
|
173
|
+
user_type="logged_in",
|
|
174
|
+
feature="support_chat",
|
|
175
|
+
messages=[{"role": "user", "content": "..."}],
|
|
176
|
+
idempotency_key=f"chat-{user_id}-{session_id}",
|
|
177
|
+
)
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
The API returns `x-idempotent-replay: true` on cache hits; a same-key request
|
|
181
|
+
with a different body returns `422 idempotency_key_reuse`.
|
|
182
|
+
|
|
183
|
+
## Explain (dry run)
|
|
184
|
+
|
|
185
|
+
Evaluate policy without calling the model or reserving budget:
|
|
186
|
+
|
|
187
|
+
```python
|
|
188
|
+
plan = ai.explain(
|
|
189
|
+
user_id="user_123",
|
|
190
|
+
user_type="logged_in",
|
|
191
|
+
feature="support_chat",
|
|
192
|
+
model_class="premium",
|
|
193
|
+
)
|
|
194
|
+
print(plan["decision"], plan["summary"])
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
## Usage
|
|
198
|
+
|
|
199
|
+
Requires an API key with `usage:read`.
|
|
200
|
+
|
|
201
|
+
```python
|
|
202
|
+
usage = ai.get_usage(user_id="user_123")
|
|
203
|
+
summary = ai.get_usage_summary(feature="support_chat", since="7d")
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
## Errors
|
|
207
|
+
|
|
208
|
+
| Class | When |
|
|
209
|
+
| --- | --- |
|
|
210
|
+
| `PolicyBlockedError` | 403 `policy_blocked` or `budget_exceeded` |
|
|
211
|
+
| `SafetyBlockedError` | 403 `safety_blocked` (PII or prompt injection) |
|
|
212
|
+
| `ModelgovError` | Other 4xx / 5xx |
|
|
213
|
+
|
|
214
|
+
`PolicyBlockedError` and `SafetyBlockedError` subclass `ModelgovError`. Each
|
|
215
|
+
error carries the API's structured envelope:
|
|
216
|
+
|
|
217
|
+
```python
|
|
218
|
+
from modelgov import ModelgovError, PolicyBlockedError, SafetyBlockedError
|
|
219
|
+
|
|
220
|
+
try:
|
|
221
|
+
ai.chat(
|
|
222
|
+
user_id="user_123",
|
|
223
|
+
user_type="logged_in",
|
|
224
|
+
feature="support_chat",
|
|
225
|
+
messages=[{"role": "user", "content": "..."}],
|
|
226
|
+
)
|
|
227
|
+
except PolicyBlockedError as err:
|
|
228
|
+
print(err.status) # 403
|
|
229
|
+
print(err.code) # "policy_blocked" | "budget_exceeded"
|
|
230
|
+
print(err.message) # human-readable
|
|
231
|
+
print(err.details) # error.details object
|
|
232
|
+
print(err.audit_request_id) # "req_<n>" — modelgov requests show
|
|
233
|
+
print(err.request_id) # HTTP trace id (UUID)
|
|
234
|
+
print(err.body) # full parsed envelope
|
|
235
|
+
except ModelgovError as err:
|
|
236
|
+
...
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
## Integration pattern
|
|
240
|
+
|
|
241
|
+
```text
|
|
242
|
+
1. Authenticate user (your app)
|
|
243
|
+
2. Authorize product action (your app)
|
|
244
|
+
3. ai.chat(user_id=..., user_type=..., feature=..., messages=...)
|
|
245
|
+
4. Return res["message"]["content"] to the user
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
Never call Modelgov before your app has decided the user may use this feature.
|
|
249
|
+
|
|
250
|
+
## Development
|
|
251
|
+
|
|
252
|
+
```bash
|
|
253
|
+
pip install -e "packages/sdk-python[dev]"
|
|
254
|
+
cd packages/sdk-python
|
|
255
|
+
pytest
|
|
256
|
+
```
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Modelgov Python SDK.
|
|
2
|
+
|
|
3
|
+
A typed, idiomatic Python client for the Modelgov AI policy gateway. Mirrors
|
|
4
|
+
the TypeScript SDK's surface (``@modelgov/sdk``).
|
|
5
|
+
|
|
6
|
+
Example:
|
|
7
|
+
>>> from modelgov import ModelgovClient
|
|
8
|
+
>>> client = ModelgovClient(base_url="http://localhost:3000", api_key="sk-...")
|
|
9
|
+
>>> res = client.chat(
|
|
10
|
+
... user_id="user_123",
|
|
11
|
+
... user_type="logged_in",
|
|
12
|
+
... feature="support_chat",
|
|
13
|
+
... model_class="cheap",
|
|
14
|
+
... messages=[{"role": "user", "content": "Help me reset my password"}],
|
|
15
|
+
... )
|
|
16
|
+
>>> print(res["message"]["content"])
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from .client import ModelgovClient
|
|
20
|
+
from .errors import ModelgovError, PolicyBlockedError, SafetyBlockedError
|
|
21
|
+
from .types import (
|
|
22
|
+
BudgetRemaining,
|
|
23
|
+
ChatMessage,
|
|
24
|
+
ChatResponse,
|
|
25
|
+
ChatResult,
|
|
26
|
+
ContentPart,
|
|
27
|
+
Cost,
|
|
28
|
+
EmbeddingsResponse,
|
|
29
|
+
EmbeddingsResult,
|
|
30
|
+
EmbeddingsUsage,
|
|
31
|
+
ExplainBudget,
|
|
32
|
+
ExplainBudgetUsed,
|
|
33
|
+
ExplainCost,
|
|
34
|
+
ExplainRequested,
|
|
35
|
+
ExplainResolved,
|
|
36
|
+
ExplainResponse,
|
|
37
|
+
ExplainResult,
|
|
38
|
+
ExplainSafety,
|
|
39
|
+
ImagePart,
|
|
40
|
+
ImageUrl,
|
|
41
|
+
ResponseMessage,
|
|
42
|
+
Safety,
|
|
43
|
+
TextPart,
|
|
44
|
+
Usage,
|
|
45
|
+
UsageResponse,
|
|
46
|
+
UsageResult,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
__version__ = "1.0.0"
|
|
50
|
+
|
|
51
|
+
__all__ = [
|
|
52
|
+
"ModelgovClient",
|
|
53
|
+
"ModelgovError",
|
|
54
|
+
"PolicyBlockedError",
|
|
55
|
+
"SafetyBlockedError",
|
|
56
|
+
"ChatMessage",
|
|
57
|
+
"ChatResponse",
|
|
58
|
+
"ChatResult",
|
|
59
|
+
"TextPart",
|
|
60
|
+
"ImageUrl",
|
|
61
|
+
"ImagePart",
|
|
62
|
+
"ContentPart",
|
|
63
|
+
"EmbeddingsResponse",
|
|
64
|
+
"EmbeddingsResult",
|
|
65
|
+
"EmbeddingsUsage",
|
|
66
|
+
"Usage",
|
|
67
|
+
"Cost",
|
|
68
|
+
"BudgetRemaining",
|
|
69
|
+
"Safety",
|
|
70
|
+
"ResponseMessage",
|
|
71
|
+
"ExplainRequested",
|
|
72
|
+
"ExplainResolved",
|
|
73
|
+
"ExplainSafety",
|
|
74
|
+
"ExplainCost",
|
|
75
|
+
"ExplainBudgetUsed",
|
|
76
|
+
"ExplainBudget",
|
|
77
|
+
"ExplainResponse",
|
|
78
|
+
"ExplainResult",
|
|
79
|
+
"UsageResponse",
|
|
80
|
+
"UsageResult",
|
|
81
|
+
"__version__",
|
|
82
|
+
]
|