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.
Files changed (25) hide show
  1. {confamnode-0.2.1 → confamnode-0.2.3}/PKG-INFO +16 -16
  2. {confamnode-0.2.1 → confamnode-0.2.3}/README.md +15 -15
  3. {confamnode-0.2.1 → confamnode-0.2.3}/confamnode/__init__.py +1 -1
  4. {confamnode-0.2.1 → confamnode-0.2.3}/confamnode/client.py +16 -5
  5. confamnode-0.2.3/confamnode/utils.py +26 -0
  6. {confamnode-0.2.1 → confamnode-0.2.3}/pyproject.toml +1 -1
  7. confamnode-0.2.3/tests/test_client_errors.py +82 -0
  8. confamnode-0.2.3/tests/test_utils.py +46 -0
  9. {confamnode-0.2.1 → confamnode-0.2.3}/uv.lock +1 -1
  10. {confamnode-0.2.1 → confamnode-0.2.3}/.gitignore +0 -0
  11. {confamnode-0.2.1 → confamnode-0.2.3}/.python-version +0 -0
  12. {confamnode-0.2.1 → confamnode-0.2.3}/confamnode/ansa.py +0 -0
  13. {confamnode-0.2.1 → confamnode-0.2.3}/confamnode/builders.py +0 -0
  14. {confamnode-0.2.1 → confamnode-0.2.3}/confamnode/config.py +0 -0
  15. {confamnode-0.2.1 → confamnode-0.2.3}/confamnode/exceptions.py +0 -0
  16. {confamnode-0.2.1 → confamnode-0.2.3}/confamnode/models.py +0 -0
  17. {confamnode-0.2.1 → confamnode-0.2.3}/confamnode/registry.py +0 -0
  18. {confamnode-0.2.1 → confamnode-0.2.3}/tests/__init__.py +0 -0
  19. {confamnode-0.2.1 → confamnode-0.2.3}/tests/test_ansa.py +0 -0
  20. {confamnode-0.2.1 → confamnode-0.2.3}/tests/test_client.py +0 -0
  21. {confamnode-0.2.1 → confamnode-0.2.3}/tests/test_exceptions.py +0 -0
  22. {confamnode-0.2.1 → confamnode-0.2.3}/tests/test_gist.py +0 -0
  23. {confamnode-0.2.1 → confamnode-0.2.3}/tests/test_init.py +0 -0
  24. {confamnode-0.2.1 → confamnode-0.2.3}/tests/test_models.py +0 -0
  25. {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.1
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:4000/v1"
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:4000/v1"
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
+ ---
@@ -8,7 +8,7 @@ from confamnode.exceptions import (
8
8
  from confamnode.ansa import Ansa, Usage, Cost
9
9
  from confamnode import models
10
10
 
11
- __version__ = "0.2.1"
11
+ __version__ = "0.2.3"
12
12
 
13
13
  __all__ = [
14
14
  "ConfamNode",
@@ -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
- error = stream_response.json().get("detail", "Requeest failed")
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.json().get("detail", "Request failed")
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(prompt_tokens=0, completion_tokens=0, total_tokens=0),
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": {"prompt_tokens": 0, "completion_tokens": 0}
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"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "confamnode"
3
- version = "0.2.1"
3
+ version = "0.2.3"
4
4
  description = "The Nigerian AI inference gateway"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -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"
@@ -40,7 +40,7 @@ wheels = [
40
40
 
41
41
  [[package]]
42
42
  name = "confamnode"
43
- version = "0.2.0"
43
+ version = "0.2.3"
44
44
  source = { editable = "." }
45
45
  dependencies = [
46
46
  { name = "httpx" },
File without changes
File without changes
File without changes