confamnode 0.2.1__tar.gz → 0.2.3__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.1 → confamnode-0.2.3}/PKG-INFO +16 -16
- {confamnode-0.2.1 → confamnode-0.2.3}/README.md +15 -15
- {confamnode-0.2.1 → confamnode-0.2.3}/confamnode/__init__.py +1 -1
- {confamnode-0.2.1 → confamnode-0.2.3}/confamnode/client.py +16 -5
- confamnode-0.2.3/confamnode/utils.py +26 -0
- {confamnode-0.2.1 → confamnode-0.2.3}/pyproject.toml +1 -1
- confamnode-0.2.3/tests/test_client_errors.py +82 -0
- confamnode-0.2.3/tests/test_utils.py +46 -0
- {confamnode-0.2.1 → confamnode-0.2.3}/uv.lock +1 -1
- {confamnode-0.2.1 → confamnode-0.2.3}/.gitignore +0 -0
- {confamnode-0.2.1 → confamnode-0.2.3}/.python-version +0 -0
- {confamnode-0.2.1 → confamnode-0.2.3}/confamnode/ansa.py +0 -0
- {confamnode-0.2.1 → confamnode-0.2.3}/confamnode/builders.py +0 -0
- {confamnode-0.2.1 → confamnode-0.2.3}/confamnode/config.py +0 -0
- {confamnode-0.2.1 → confamnode-0.2.3}/confamnode/exceptions.py +0 -0
- {confamnode-0.2.1 → confamnode-0.2.3}/confamnode/models.py +0 -0
- {confamnode-0.2.1 → confamnode-0.2.3}/confamnode/registry.py +0 -0
- {confamnode-0.2.1 → confamnode-0.2.3}/tests/__init__.py +0 -0
- {confamnode-0.2.1 → confamnode-0.2.3}/tests/test_ansa.py +0 -0
- {confamnode-0.2.1 → confamnode-0.2.3}/tests/test_client.py +0 -0
- {confamnode-0.2.1 → confamnode-0.2.3}/tests/test_exceptions.py +0 -0
- {confamnode-0.2.1 → confamnode-0.2.3}/tests/test_gist.py +0 -0
- {confamnode-0.2.1 → confamnode-0.2.3}/tests/test_init.py +0 -0
- {confamnode-0.2.1 → confamnode-0.2.3}/tests/test_models.py +0 -0
- {confamnode-0.2.1 → confamnode-0.2.3}/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.3
|
|
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
|
|
@@ -173,25 +173,25 @@ ansa.raw["usage"] # prompt and completion token counts
|
|
|
173
173
|
|
|
174
174
|
### Free Tier
|
|
175
175
|
|
|
176
|
-
| Model | Description | Price |
|
|
177
|
-
|
|
178
|
-
| `confam-lite` | Light text and general chat | Free |
|
|
179
|
-
| `confam-speed` | Fast, high quality responses | Free |
|
|
180
|
-
| `confam-reasoning` | Standard reasoning and analysis | Free |
|
|
176
|
+
| Model | Description | Modality | Price |
|
|
177
|
+
|---|---|---|---|
|
|
178
|
+
| `confam-lite` | Light text and general chat | Text-to-Text | Free |
|
|
179
|
+
| `confam-speed` | Fast, high quality responses | Image-Text-to-Text | Free |
|
|
180
|
+
| `confam-reasoning` | Standard reasoning and analysis | Text-to-Text | Free |
|
|
181
181
|
|
|
182
182
|
### Paid Tier
|
|
183
183
|
|
|
184
|
-
| Model | Description | Input ₦/1M | Output ₦/1M | Input ₦/1K | Output ₦/1K |
|
|
185
|
-
|
|
186
|
-
| `confam-intelligence` | General smart tasks, 1M context | ₦596 | ₦3,571 | ₦0.596 | ₦3.571 |
|
|
187
|
-
| `confam-deep-reasoning` | Complex thinking, multi-step analysis | ₦234 | ₦468 | ₦0.234 | ₦0.468 |
|
|
188
|
-
| `confam-code` | Coding assistance, 1M context | ₦234 | ₦468 | ₦0.234 | ₦0.468 |
|
|
184
|
+
| Model | Description | Modality | Input ₦/1M | Output ₦/1M | Input ₦/1K | Output ₦/1K |
|
|
185
|
+
|---|---|---|---|---|---|---|
|
|
186
|
+
| `confam-intelligence` | General smart tasks, 1M context | Image-Text-to-Text | ₦596 | ₦3,571 | ₦0.596 | ₦3.571 |
|
|
187
|
+
| `confam-deep-reasoning` | Complex thinking, multi-step analysis | Image-Text-to-Text | ₦234 | ₦468 | ₦0.234 | ₦0.468 |
|
|
188
|
+
| `confam-code` | Coding assistance, 1M context | Image-Text-to-Text | ₦234 | ₦468 | ₦0.234 | ₦0.468 |
|
|
189
189
|
|
|
190
190
|
### Local Models — Nigerian Data Residency
|
|
191
191
|
|
|
192
|
-
| Model | Description | Input ₦/1M | Output ₦/1M | Input ₦/1K | Output ₦/1K |
|
|
193
|
-
|
|
194
|
-
| `confam-nano` | Local model — data stays in Nigeria | ₦500 | ₦1,500 | ₦0.500 | ₦1.500 |
|
|
192
|
+
| Model | Description | Modality | Input ₦/1M | Output ₦/1M | Input ₦/1K | Output ₦/1K |
|
|
193
|
+
|---|---|---|---|---|---|---|
|
|
194
|
+
| `confam-nano` | Local model — data stays in Nigeria | Image-Text-to-Text | ₦500 | ₦1,500 | ₦0.500 | ₦1.500 |
|
|
195
195
|
|
|
196
196
|
Runs entirely on Nigerian hardware. Data never transmitted abroad.
|
|
197
197
|
Ideal for banks, fintechs, hospitals, law firms, and government agencies.
|
|
@@ -340,7 +340,7 @@ For enterprise clients running ConfamNode on private infrastructure:
|
|
|
340
340
|
```python
|
|
341
341
|
client = ConfamNode(
|
|
342
342
|
api_key="confam-xxx",
|
|
343
|
-
base_url="http://your-private-server:
|
|
343
|
+
base_url="http://your-private-server:8000/v1"
|
|
344
344
|
)
|
|
345
345
|
```
|
|
346
346
|
|
|
@@ -403,4 +403,4 @@ Contact: [hello@confamnode.com](mailto:hello@confamnode.com)
|
|
|
403
403
|
|
|
404
404
|
Apache 2.0
|
|
405
405
|
|
|
406
|
-
---
|
|
406
|
+
---
|
|
@@ -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.
|
|
@@ -317,7 +317,7 @@ For enterprise clients running ConfamNode on private infrastructure:
|
|
|
317
317
|
```python
|
|
318
318
|
client = ConfamNode(
|
|
319
319
|
api_key="confam-xxx",
|
|
320
|
-
base_url="http://your-private-server:
|
|
320
|
+
base_url="http://your-private-server:8000/v1"
|
|
321
321
|
)
|
|
322
322
|
```
|
|
323
323
|
|
|
@@ -380,4 +380,4 @@ Contact: [hello@confamnode.com](mailto:hello@confamnode.com)
|
|
|
380
380
|
|
|
381
381
|
Apache 2.0
|
|
382
382
|
|
|
383
|
-
---
|
|
383
|
+
---
|
|
@@ -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
|
|
@@ -76,10 +77,11 @@ class ConfamNode:
|
|
|
76
77
|
|
|
77
78
|
if stream_response.status_code >= 400:
|
|
78
79
|
stream_response.read()
|
|
80
|
+
status = stream_response.status_code
|
|
81
|
+
error = extract_error(stream_response)
|
|
79
82
|
stream_response.close()
|
|
80
83
|
http_client.close()
|
|
81
|
-
|
|
82
|
-
raise Exception(f"ConfamNode error {stream_response.status_code}: {error}")
|
|
84
|
+
raise Exception(f"ConfamNode error {status}: {error}")
|
|
83
85
|
|
|
84
86
|
return ConfamStream(stream_response, http_client, model)
|
|
85
87
|
|
|
@@ -94,7 +96,7 @@ class ConfamNode:
|
|
|
94
96
|
)
|
|
95
97
|
|
|
96
98
|
if response.status_code >= 400:
|
|
97
|
-
error = response
|
|
99
|
+
error = extract_error(response)
|
|
98
100
|
raise Exception(f"ConfamNode error {response.status_code}: {error}")
|
|
99
101
|
|
|
100
102
|
data = response.json()
|
|
@@ -188,13 +190,18 @@ class ConfamStream:
|
|
|
188
190
|
if self._chunks and self._chunks[-1].choices:
|
|
189
191
|
finish_reason = self._chunks[-1].choices[0].finish_reason or "stop"
|
|
190
192
|
|
|
193
|
+
usage_data = self._confam_meta.get("usage", {})
|
|
191
194
|
cost_data = self._confam_meta.get("cost", {})
|
|
192
195
|
|
|
193
196
|
return Ansa(
|
|
194
197
|
id=self._confam_meta.get("request_id", ""),
|
|
195
198
|
text=text,
|
|
196
199
|
model=self._model,
|
|
197
|
-
usage=Usage(
|
|
200
|
+
usage=Usage(
|
|
201
|
+
prompt_tokens=usage_data.get("prompt_tokens", 0),
|
|
202
|
+
completion_tokens=usage_data.get("completion_tokens", 0),
|
|
203
|
+
total_tokens=usage_data.get("total_tokens", 0),
|
|
204
|
+
),
|
|
198
205
|
cost=Cost(
|
|
199
206
|
naira=cost_data.get("naira", 0.0),
|
|
200
207
|
naira_input=cost_data.get("naira_input", 0.0),
|
|
@@ -203,7 +210,11 @@ class ConfamStream:
|
|
|
203
210
|
finish_reason=finish_reason,
|
|
204
211
|
raw={
|
|
205
212
|
"id": self._confam_meta.get("request_id", ""),
|
|
206
|
-
"usage": {
|
|
213
|
+
"usage": {
|
|
214
|
+
"prompt_tokens": usage_data.get("prompt_tokens", 0),
|
|
215
|
+
"completion_tokens": usage_data.get("completion_tokens", 0),
|
|
216
|
+
"total_tokens": usage_data.get("total_tokens", 0)
|
|
217
|
+
}
|
|
207
218
|
},
|
|
208
219
|
is_local=self._confam_meta.get("is_local", False),
|
|
209
220
|
is_ngn_data_residency=self._confam_meta.get("is_ngn_data_residency", False),
|
|
@@ -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,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
|