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.
@@ -0,0 +1,7 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ .pytest_cache/
6
+ dist/
7
+ build/
@@ -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
+ ```
@@ -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
+ ]