confamnode 0.2.2__tar.gz → 0.2.4__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.
- {confamnode-0.2.2 → confamnode-0.2.4}/PKG-INFO +41 -15
- {confamnode-0.2.2 → confamnode-0.2.4}/README.md +40 -14
- {confamnode-0.2.2 → confamnode-0.2.4}/confamnode/__init__.py +1 -1
- {confamnode-0.2.2 → confamnode-0.2.4}/confamnode/client.py +25 -3
- confamnode-0.2.4/confamnode/utils.py +26 -0
- {confamnode-0.2.2 → confamnode-0.2.4}/pyproject.toml +1 -1
- confamnode-0.2.4/tests/test_cache_flag.py +74 -0
- confamnode-0.2.4/tests/test_client_errors.py +82 -0
- confamnode-0.2.4/tests/test_utils.py +46 -0
- {confamnode-0.2.2 → confamnode-0.2.4}/uv.lock +1 -1
- {confamnode-0.2.2 → confamnode-0.2.4}/.gitignore +0 -0
- {confamnode-0.2.2 → confamnode-0.2.4}/.python-version +0 -0
- {confamnode-0.2.2 → confamnode-0.2.4}/LICENSE +0 -0
- {confamnode-0.2.2 → confamnode-0.2.4}/confamnode/ansa.py +0 -0
- {confamnode-0.2.2 → confamnode-0.2.4}/confamnode/builders.py +0 -0
- {confamnode-0.2.2 → confamnode-0.2.4}/confamnode/config.py +0 -0
- {confamnode-0.2.2 → confamnode-0.2.4}/confamnode/exceptions.py +0 -0
- {confamnode-0.2.2 → confamnode-0.2.4}/confamnode/models.py +0 -0
- {confamnode-0.2.2 → confamnode-0.2.4}/confamnode/registry.py +0 -0
- {confamnode-0.2.2 → confamnode-0.2.4}/tests/__init__.py +0 -0
- {confamnode-0.2.2 → confamnode-0.2.4}/tests/test_ansa.py +0 -0
- {confamnode-0.2.2 → confamnode-0.2.4}/tests/test_client.py +0 -0
- {confamnode-0.2.2 → confamnode-0.2.4}/tests/test_exceptions.py +0 -0
- {confamnode-0.2.2 → confamnode-0.2.4}/tests/test_gist.py +0 -0
- {confamnode-0.2.2 → confamnode-0.2.4}/tests/test_init.py +0 -0
- {confamnode-0.2.2 → confamnode-0.2.4}/tests/test_models.py +0 -0
- {confamnode-0.2.2 → confamnode-0.2.4}/tests/test_stream.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: confamnode
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
4
4
|
Summary: The Nigerian AI inference gateway
|
|
5
5
|
Project-URL: Repository, https://github.com/confamnodeai/confamnode
|
|
6
6
|
Project-URL: Bug Tracker, https://github.com/confamnodeai/confamnode/issues
|
|
@@ -174,25 +174,25 @@ ansa.raw["usage"] # prompt and completion token counts
|
|
|
174
174
|
|
|
175
175
|
### Free Tier
|
|
176
176
|
|
|
177
|
-
| Model | Description | Price |
|
|
178
|
-
|
|
179
|
-
| `confam-lite` | Light text and general chat | Free |
|
|
180
|
-
| `confam-speed` | Fast, high quality responses | Free |
|
|
181
|
-
| `confam-reasoning` | Standard reasoning and analysis | Free |
|
|
177
|
+
| Model | Description | Modality | Price |
|
|
178
|
+
|---|---|---|---|
|
|
179
|
+
| `confam-lite` | Light text and general chat | Text-to-Text | Free |
|
|
180
|
+
| `confam-speed` | Fast, high quality responses | Image-Text-to-Text | Free |
|
|
181
|
+
| `confam-reasoning` | Standard reasoning and analysis | Text-to-Text | Free |
|
|
182
182
|
|
|
183
183
|
### Paid Tier
|
|
184
184
|
|
|
185
|
-
| Model | Description | Input ₦/1M | Output ₦/1M | Input ₦/1K | Output ₦/1K |
|
|
186
|
-
|
|
187
|
-
| `confam-intelligence` | General smart tasks, 1M context | ₦596 | ₦3,571 | ₦0.596 | ₦3.571 |
|
|
188
|
-
| `confam-deep-reasoning` | Complex thinking, multi-step analysis | ₦234 | ₦468 | ₦0.234 | ₦0.468 |
|
|
189
|
-
| `confam-code` | Coding assistance, 1M context | ₦234 | ₦468 | ₦0.234 | ₦0.468 |
|
|
185
|
+
| Model | Description | Modality | Input ₦/1M | Output ₦/1M | Input ₦/1K | Output ₦/1K |
|
|
186
|
+
|---|---|---|---|---|---|---|
|
|
187
|
+
| `confam-intelligence` | General smart tasks, 1M context | Image-Text-to-Text | ₦596 | ₦3,571 | ₦0.596 | ₦3.571 |
|
|
188
|
+
| `confam-deep-reasoning` | Complex thinking, multi-step analysis | Image-Text-to-Text | ₦234 | ₦468 | ₦0.234 | ₦0.468 |
|
|
189
|
+
| `confam-code` | Coding assistance, 1M context | Image-Text-to-Text | ₦234 | ₦468 | ₦0.234 | ₦0.468 |
|
|
190
190
|
|
|
191
191
|
### Local Models — Nigerian Data Residency
|
|
192
192
|
|
|
193
|
-
| Model | Description | Input ₦/1M | Output ₦/1M | Input ₦/1K | Output ₦/1K |
|
|
194
|
-
|
|
195
|
-
| `confam-nano` | Local model — data stays in Nigeria | ₦500 | ₦1,500 | ₦0.500 | ₦1.500 |
|
|
193
|
+
| Model | Description | Modality | Input ₦/1M | Output ₦/1M | Input ₦/1K | Output ₦/1K |
|
|
194
|
+
|---|---|---|---|---|---|---|
|
|
195
|
+
| `confam-nano` | Local model — data stays in Nigeria | Image-Text-to-Text | ₦500 | ₦1,500 | ₦0.500 | ₦1.500 |
|
|
196
196
|
|
|
197
197
|
Runs entirely on Nigerian hardware. Data never transmitted abroad.
|
|
198
198
|
Ideal for banks, fintechs, hospitals, law firms, and government agencies.
|
|
@@ -254,6 +254,32 @@ ansa = client.gist(
|
|
|
254
254
|
|
|
255
255
|
---
|
|
256
256
|
|
|
257
|
+
## Caching
|
|
258
|
+
|
|
259
|
+
Caching is controlled **per request** and is **off by default** — every call returns a fresh response, even when the request is identical. This keeps data-generation loops and any workflow that resends the same prompt from getting the same cached answer back each time.
|
|
260
|
+
|
|
261
|
+
Pass `cache=True` to read from and write to the cache — useful for idempotent lookups or to save cost on repeated queries:
|
|
262
|
+
|
|
263
|
+
```python
|
|
264
|
+
# Default — caching off, fresh response every call
|
|
265
|
+
ansa = client.gist(
|
|
266
|
+
model="confam-speed",
|
|
267
|
+
messages="How you dey?"
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
# Enable caching — use a stored response when the request matches,
|
|
271
|
+
# and store this response for next time
|
|
272
|
+
ansa = client.gist(
|
|
273
|
+
model="confam-speed",
|
|
274
|
+
messages="How you dey?",
|
|
275
|
+
cache=True
|
|
276
|
+
)
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
A cache hit is typically returned near-instantly and at little or no token cost — a quick way to confirm caching is active.
|
|
280
|
+
|
|
281
|
+
---
|
|
282
|
+
|
|
257
283
|
## Reasoning Models
|
|
258
284
|
|
|
259
285
|
Enable extended thinking for complex problems:
|
|
@@ -404,4 +430,4 @@ Contact: [hello@confamnode.com](mailto:hello@confamnode.com)
|
|
|
404
430
|
|
|
405
431
|
Apache 2.0
|
|
406
432
|
|
|
407
|
-
---
|
|
433
|
+
---
|
|
@@ -150,25 +150,25 @@ ansa.raw["usage"] # prompt and completion token counts
|
|
|
150
150
|
|
|
151
151
|
### Free Tier
|
|
152
152
|
|
|
153
|
-
| Model | Description | Price |
|
|
154
|
-
|
|
155
|
-
| `confam-lite` | Light text and general chat | Free |
|
|
156
|
-
| `confam-speed` | Fast, high quality responses | Free |
|
|
157
|
-
| `confam-reasoning` | Standard reasoning and analysis | Free |
|
|
153
|
+
| Model | Description | Modality | Price |
|
|
154
|
+
|---|---|---|---|
|
|
155
|
+
| `confam-lite` | Light text and general chat | Text-to-Text | Free |
|
|
156
|
+
| `confam-speed` | Fast, high quality responses | Image-Text-to-Text | Free |
|
|
157
|
+
| `confam-reasoning` | Standard reasoning and analysis | Text-to-Text | Free |
|
|
158
158
|
|
|
159
159
|
### Paid Tier
|
|
160
160
|
|
|
161
|
-
| Model | Description | Input ₦/1M | Output ₦/1M | Input ₦/1K | Output ₦/1K |
|
|
162
|
-
|
|
163
|
-
| `confam-intelligence` | General smart tasks, 1M context | ₦596 | ₦3,571 | ₦0.596 | ₦3.571 |
|
|
164
|
-
| `confam-deep-reasoning` | Complex thinking, multi-step analysis | ₦234 | ₦468 | ₦0.234 | ₦0.468 |
|
|
165
|
-
| `confam-code` | Coding assistance, 1M context | ₦234 | ₦468 | ₦0.234 | ₦0.468 |
|
|
161
|
+
| Model | Description | Modality | Input ₦/1M | Output ₦/1M | Input ₦/1K | Output ₦/1K |
|
|
162
|
+
|---|---|---|---|---|---|---|
|
|
163
|
+
| `confam-intelligence` | General smart tasks, 1M context | Image-Text-to-Text | ₦596 | ₦3,571 | ₦0.596 | ₦3.571 |
|
|
164
|
+
| `confam-deep-reasoning` | Complex thinking, multi-step analysis | Image-Text-to-Text | ₦234 | ₦468 | ₦0.234 | ₦0.468 |
|
|
165
|
+
| `confam-code` | Coding assistance, 1M context | Image-Text-to-Text | ₦234 | ₦468 | ₦0.234 | ₦0.468 |
|
|
166
166
|
|
|
167
167
|
### Local Models — Nigerian Data Residency
|
|
168
168
|
|
|
169
|
-
| Model | Description | Input ₦/1M | Output ₦/1M | Input ₦/1K | Output ₦/1K |
|
|
170
|
-
|
|
171
|
-
| `confam-nano` | Local model — data stays in Nigeria | ₦500 | ₦1,500 | ₦0.500 | ₦1.500 |
|
|
169
|
+
| Model | Description | Modality | Input ₦/1M | Output ₦/1M | Input ₦/1K | Output ₦/1K |
|
|
170
|
+
|---|---|---|---|---|---|---|
|
|
171
|
+
| `confam-nano` | Local model — data stays in Nigeria | Image-Text-to-Text | ₦500 | ₦1,500 | ₦0.500 | ₦1.500 |
|
|
172
172
|
|
|
173
173
|
Runs entirely on Nigerian hardware. Data never transmitted abroad.
|
|
174
174
|
Ideal for banks, fintechs, hospitals, law firms, and government agencies.
|
|
@@ -230,6 +230,32 @@ ansa = client.gist(
|
|
|
230
230
|
|
|
231
231
|
---
|
|
232
232
|
|
|
233
|
+
## Caching
|
|
234
|
+
|
|
235
|
+
Caching is controlled **per request** and is **off by default** — every call returns a fresh response, even when the request is identical. This keeps data-generation loops and any workflow that resends the same prompt from getting the same cached answer back each time.
|
|
236
|
+
|
|
237
|
+
Pass `cache=True` to read from and write to the cache — useful for idempotent lookups or to save cost on repeated queries:
|
|
238
|
+
|
|
239
|
+
```python
|
|
240
|
+
# Default — caching off, fresh response every call
|
|
241
|
+
ansa = client.gist(
|
|
242
|
+
model="confam-speed",
|
|
243
|
+
messages="How you dey?"
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
# Enable caching — use a stored response when the request matches,
|
|
247
|
+
# and store this response for next time
|
|
248
|
+
ansa = client.gist(
|
|
249
|
+
model="confam-speed",
|
|
250
|
+
messages="How you dey?",
|
|
251
|
+
cache=True
|
|
252
|
+
)
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
A cache hit is typically returned near-instantly and at little or no token cost — a quick way to confirm caching is active.
|
|
256
|
+
|
|
257
|
+
---
|
|
258
|
+
|
|
233
259
|
## Reasoning Models
|
|
234
260
|
|
|
235
261
|
Enable extended thinking for complex problems:
|
|
@@ -380,4 +406,4 @@ Contact: [hello@confamnode.com](mailto:hello@confamnode.com)
|
|
|
380
406
|
|
|
381
407
|
Apache 2.0
|
|
382
408
|
|
|
383
|
-
---
|
|
409
|
+
---
|
|
@@ -4,6 +4,7 @@ import httpx
|
|
|
4
4
|
|
|
5
5
|
from typing import Union, List, Dict
|
|
6
6
|
|
|
7
|
+
from confamnode.utils import extract_error
|
|
7
8
|
from confamnode.builders import parse_chunk
|
|
8
9
|
from confamnode.ansa import Ansa, Usage, Cost
|
|
9
10
|
from confamnode.registry import VALID_MODELS
|
|
@@ -34,6 +35,7 @@ class ConfamNode:
|
|
|
34
35
|
model: str,
|
|
35
36
|
messages: Union[str, List[Dict[str, str]]],
|
|
36
37
|
system: str | None = "default",
|
|
38
|
+
cache: bool = False,
|
|
37
39
|
**kwargs
|
|
38
40
|
) -> "Ansa | ConfamStream":
|
|
39
41
|
if model not in VALID_MODELS:
|
|
@@ -59,6 +61,25 @@ class ConfamNode:
|
|
|
59
61
|
else:
|
|
60
62
|
body["system"] = system # None or custom string
|
|
61
63
|
|
|
64
|
+
# Caching is controlled per request. By default the SDK opts OUT, so
|
|
65
|
+
# identical requests (same model + messages + system) each return a
|
|
66
|
+
# FRESH response -- important for data-generation loops that resend the
|
|
67
|
+
# same prompt expecting varied output. Pass cache=True to read from and
|
|
68
|
+
# write to the cache (idempotent lookups, cost savings on repeats).
|
|
69
|
+
#
|
|
70
|
+
# These flags only tell the gateway whether to use a cache for THIS
|
|
71
|
+
# request; whether one exists at all is a gateway-side capability.
|
|
72
|
+
if cache:
|
|
73
|
+
body["cache"] = {
|
|
74
|
+
"no-cache": False, # Cache the response
|
|
75
|
+
"no-store": False # Store the response
|
|
76
|
+
}
|
|
77
|
+
else:
|
|
78
|
+
body["cache"] = {
|
|
79
|
+
"no-cache": True, # Skip cache check, get fresh response
|
|
80
|
+
"no-store": True # Don't cache this response
|
|
81
|
+
}
|
|
82
|
+
|
|
62
83
|
if kwargs.get("stream", False):
|
|
63
84
|
http_client = httpx.Client(
|
|
64
85
|
timeout=httpx.Timeout(DEFAULT_TIMEOUT, connect=DEFAULT_CONNECT_TIMEOUT)
|
|
@@ -76,10 +97,11 @@ class ConfamNode:
|
|
|
76
97
|
|
|
77
98
|
if stream_response.status_code >= 400:
|
|
78
99
|
stream_response.read()
|
|
100
|
+
status = stream_response.status_code
|
|
101
|
+
error = extract_error(stream_response)
|
|
79
102
|
stream_response.close()
|
|
80
103
|
http_client.close()
|
|
81
|
-
|
|
82
|
-
raise Exception(f"ConfamNode error {stream_response.status_code}: {error}")
|
|
104
|
+
raise Exception(f"ConfamNode error {status}: {error}")
|
|
83
105
|
|
|
84
106
|
return ConfamStream(stream_response, http_client, model)
|
|
85
107
|
|
|
@@ -94,7 +116,7 @@ class ConfamNode:
|
|
|
94
116
|
)
|
|
95
117
|
|
|
96
118
|
if response.status_code >= 400:
|
|
97
|
-
error = response
|
|
119
|
+
error = extract_error(response)
|
|
98
120
|
raise Exception(f"ConfamNode error {response.status_code}: {error}")
|
|
99
121
|
|
|
100
122
|
data = response.json()
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Small internal helpers shared across the confamnode client.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def extract_error(response) -> str:
|
|
7
|
+
"""
|
|
8
|
+
Best-effort error detail from a non-2xx response, WITHOUT raising.
|
|
9
|
+
|
|
10
|
+
The server's normal error shape is {"detail": "..."}, but gateway-level
|
|
11
|
+
errors (e.g. a 429 throttle, a 502/504 from a proxy) often return an
|
|
12
|
+
empty or non-JSON body. Calling response.json() directly on those bodies
|
|
13
|
+
raises JSONDecodeError and swallows the status code -- the one thing the
|
|
14
|
+
caller actually needs. So we parse defensively and always fall back to
|
|
15
|
+
the raw text, then to a status-only message.
|
|
16
|
+
"""
|
|
17
|
+
try:
|
|
18
|
+
payload = response.json()
|
|
19
|
+
if isinstance(payload, dict):
|
|
20
|
+
detail = payload.get("detail") or payload.get("error") or payload.get("message")
|
|
21
|
+
if detail:
|
|
22
|
+
return str(detail)
|
|
23
|
+
except Exception:
|
|
24
|
+
pass
|
|
25
|
+
text = (getattr(response, "text", "") or "").strip()
|
|
26
|
+
return text or "no response body"
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests for the per-request cache flag on ConfamNode.gist().
|
|
3
|
+
|
|
4
|
+
Caching is opt-OUT by default: identical requests must each return a fresh
|
|
5
|
+
response (critical for data-generation loops). cache=True flips the request
|
|
6
|
+
to read-from + write-to the cache. These tests pin down the exact payload
|
|
7
|
+
sent in each case so a future refactor can't silently flip the default.
|
|
8
|
+
|
|
9
|
+
No network: httpx.post is monkeypatched to capture the request body.
|
|
10
|
+
"""
|
|
11
|
+
import httpx
|
|
12
|
+
import pytest
|
|
13
|
+
|
|
14
|
+
from confamnode.client import ConfamNode
|
|
15
|
+
from confamnode.registry import VALID_MODELS
|
|
16
|
+
|
|
17
|
+
API_KEY = "confam-test"
|
|
18
|
+
MODEL = next(iter(VALID_MODELS))
|
|
19
|
+
|
|
20
|
+
CACHE_OFF = {"no-cache": True, "no-store": True} # skip read + skip store
|
|
21
|
+
CACHE_ON = {"no-cache": False, "no-store": False} # use + store
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@pytest.fixture
|
|
25
|
+
def captured_body(monkeypatch):
|
|
26
|
+
"""Patch httpx.post to record the JSON body and return a valid 200."""
|
|
27
|
+
box = {}
|
|
28
|
+
|
|
29
|
+
def fake_post(url, headers=None, json=None, timeout=None):
|
|
30
|
+
box["body"] = json
|
|
31
|
+
|
|
32
|
+
class _Resp:
|
|
33
|
+
status_code = 200
|
|
34
|
+
text = ""
|
|
35
|
+
|
|
36
|
+
def json(self):
|
|
37
|
+
return {
|
|
38
|
+
"choices": [{"message": {"content": "hi"}, "finish_reason": "stop"}],
|
|
39
|
+
"usage": {},
|
|
40
|
+
"confam": {"cost": {"naira": 0.0}},
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return _Resp()
|
|
44
|
+
|
|
45
|
+
monkeypatch.setattr(httpx, "post", fake_post)
|
|
46
|
+
return box
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _gist(**kwargs):
|
|
50
|
+
ConfamNode(api_key=API_KEY).gist(model=MODEL, messages="x", **kwargs)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_cache_off_by_default(captured_body):
|
|
54
|
+
_gist()
|
|
55
|
+
assert captured_body["body"]["cache"] == CACHE_OFF
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_cache_true_uses_and_stores(captured_body):
|
|
59
|
+
_gist(cache=True)
|
|
60
|
+
assert captured_body["body"]["cache"] == CACHE_ON
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_cache_false_is_explicit_off(captured_body):
|
|
64
|
+
_gist(cache=False)
|
|
65
|
+
assert captured_body["body"]["cache"] == CACHE_OFF
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_cache_field_always_present(captured_body):
|
|
69
|
+
# The request should always state its caching intent in both directions,
|
|
70
|
+
# never leave it implied by omission.
|
|
71
|
+
_gist()
|
|
72
|
+
assert "cache" in captured_body["body"]
|
|
73
|
+
_gist(cache=True)
|
|
74
|
+
assert "cache" in captured_body["body"]
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests for ConfamNode.gist() error handling.
|
|
3
|
+
|
|
4
|
+
The original bug: a non-2xx response with an empty / non-JSON body made the
|
|
5
|
+
SDK call response.json() in its error branch, which raised JSONDecodeError
|
|
6
|
+
and hid the HTTP status. These tests pin down that:
|
|
7
|
+
- an empty error body raises a clean "ConfamNode error <status>: ..." with
|
|
8
|
+
the status code intact (NOT a JSONDecodeError), and
|
|
9
|
+
- the normal success path still works after the refactor.
|
|
10
|
+
|
|
11
|
+
No network: httpx.post is monkeypatched to return a fake response.
|
|
12
|
+
"""
|
|
13
|
+
import httpx
|
|
14
|
+
import pytest
|
|
15
|
+
|
|
16
|
+
from confamnode.client import ConfamNode
|
|
17
|
+
from confamnode.registry import VALID_MODELS
|
|
18
|
+
|
|
19
|
+
API_KEY = "confam-test"
|
|
20
|
+
MODEL = next(iter(VALID_MODELS)) # any valid model; we never really call out
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class FakeResponse:
|
|
24
|
+
def __init__(self, status_code, payload=None, *, json_raises=False, text=""):
|
|
25
|
+
self.status_code = status_code
|
|
26
|
+
self._payload = payload
|
|
27
|
+
self._json_raises = json_raises
|
|
28
|
+
self.text = text
|
|
29
|
+
|
|
30
|
+
def json(self):
|
|
31
|
+
if self._json_raises:
|
|
32
|
+
raise ValueError("Expecting value: line 1 column 1 (char 0)")
|
|
33
|
+
return self._payload
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _patch_post(monkeypatch, response):
|
|
37
|
+
monkeypatch.setattr(httpx, "post", lambda *a, **k: response)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_empty_error_body_raises_with_status_not_jsondecodeerror(monkeypatch):
|
|
41
|
+
# The reproduction of the reported crash: 429, empty body.
|
|
42
|
+
_patch_post(monkeypatch, FakeResponse(429, json_raises=True, text=""))
|
|
43
|
+
client = ConfamNode(api_key=API_KEY)
|
|
44
|
+
with pytest.raises(Exception) as exc:
|
|
45
|
+
client.gist(model=MODEL, messages="hi")
|
|
46
|
+
msg = str(exc.value)
|
|
47
|
+
assert "429" in msg
|
|
48
|
+
assert "no response body" in msg
|
|
49
|
+
assert "JSONDecode" not in type(exc.value).__name__
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_json_detail_error_surfaces_detail(monkeypatch):
|
|
53
|
+
_patch_post(monkeypatch, FakeResponse(400, payload={"detail": "invalid model"}))
|
|
54
|
+
client = ConfamNode(api_key=API_KEY)
|
|
55
|
+
with pytest.raises(Exception) as exc:
|
|
56
|
+
client.gist(model=MODEL, messages="hi")
|
|
57
|
+
assert "400" in str(exc.value)
|
|
58
|
+
assert "invalid model" in str(exc.value)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_html_gateway_error_surfaces_text(monkeypatch):
|
|
62
|
+
_patch_post(monkeypatch, FakeResponse(502, json_raises=True,
|
|
63
|
+
text="<html>502 Bad Gateway</html>"))
|
|
64
|
+
client = ConfamNode(api_key=API_KEY)
|
|
65
|
+
with pytest.raises(Exception) as exc:
|
|
66
|
+
client.gist(model=MODEL, messages="hi")
|
|
67
|
+
assert "502" in str(exc.value)
|
|
68
|
+
assert "Bad Gateway" in str(exc.value)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_success_path_still_returns_ansa(monkeypatch):
|
|
72
|
+
payload = {
|
|
73
|
+
"id": "abc",
|
|
74
|
+
"choices": [{"message": {"content": "hello"}, "finish_reason": "stop"}],
|
|
75
|
+
"usage": {"prompt_tokens": 1, "completion_tokens": 2, "total_tokens": 3},
|
|
76
|
+
"confam": {"request_id": "r1", "cost": {"naira": 0.0}},
|
|
77
|
+
}
|
|
78
|
+
_patch_post(monkeypatch, FakeResponse(200, payload=payload))
|
|
79
|
+
client = ConfamNode(api_key=API_KEY)
|
|
80
|
+
ansa = client.gist(model=MODEL, messages="hi")
|
|
81
|
+
assert ansa.text == "hello"
|
|
82
|
+
assert ansa.cost.naira == 0.0
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Unit tests for confamnode.utils.extract_error.
|
|
3
|
+
|
|
4
|
+
Pure function, no network: just feed it fake responses and assert it never
|
|
5
|
+
raises and always produces a usable message.
|
|
6
|
+
"""
|
|
7
|
+
from confamnode.utils import extract_error
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class _Resp:
|
|
11
|
+
"""Minimal stand-in for an httpx.Response."""
|
|
12
|
+
def __init__(self, payload=None, *, json_raises=False, text=""):
|
|
13
|
+
self._payload = payload
|
|
14
|
+
self._json_raises = json_raises
|
|
15
|
+
self.text = text
|
|
16
|
+
|
|
17
|
+
def json(self):
|
|
18
|
+
if self._json_raises:
|
|
19
|
+
raise ValueError("Expecting value: line 1 column 1 (char 0)")
|
|
20
|
+
return self._payload
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_empty_body_returns_placeholder():
|
|
24
|
+
# The exact case that crashed the SDK: empty 429 body.
|
|
25
|
+
assert extract_error(_Resp(json_raises=True, text="")) == "no response body"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_non_json_text_body_is_returned_stripped():
|
|
29
|
+
assert extract_error(_Resp(json_raises=True, text=" upstream timeout ")) == "upstream timeout"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_detail_key_is_preferred():
|
|
33
|
+
assert extract_error(_Resp(payload={"detail": "invalid model"})) == "invalid model"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_error_key_fallback():
|
|
37
|
+
assert extract_error(_Resp(payload={"error": "nope"})) == "nope"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_message_key_fallback():
|
|
41
|
+
assert extract_error(_Resp(payload={"message": "hmm"})) == "hmm"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_non_dict_json_falls_back_to_text():
|
|
45
|
+
# A JSON array (not a dict) shouldn't blow up; fall back to text.
|
|
46
|
+
assert extract_error(_Resp(payload=["a", "b"], text="listy")) == "listy"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|