confamnode 0.2.4__tar.gz → 0.2.6__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 (28) hide show
  1. {confamnode-0.2.4 → confamnode-0.2.6}/PKG-INFO +5 -5
  2. {confamnode-0.2.4 → confamnode-0.2.6}/README.md +4 -4
  3. {confamnode-0.2.4 → confamnode-0.2.6}/confamnode/__init__.py +1 -1
  4. {confamnode-0.2.4 → confamnode-0.2.6}/confamnode/client.py +9 -12
  5. confamnode-0.2.6/confamnode/utils.py +48 -0
  6. {confamnode-0.2.4 → confamnode-0.2.6}/pyproject.toml +1 -1
  7. {confamnode-0.2.4 → confamnode-0.2.6}/tests/test_cache_flag.py +5 -13
  8. {confamnode-0.2.4 → confamnode-0.2.6}/tests/test_client_errors.py +11 -5
  9. {confamnode-0.2.4 → confamnode-0.2.6}/tests/test_utils.py +21 -0
  10. {confamnode-0.2.4 → confamnode-0.2.6}/uv.lock +1 -1
  11. confamnode-0.2.4/confamnode/utils.py +0 -26
  12. {confamnode-0.2.4 → confamnode-0.2.6}/.gitignore +0 -0
  13. {confamnode-0.2.4 → confamnode-0.2.6}/.python-version +0 -0
  14. {confamnode-0.2.4 → confamnode-0.2.6}/LICENSE +0 -0
  15. {confamnode-0.2.4 → confamnode-0.2.6}/confamnode/ansa.py +0 -0
  16. {confamnode-0.2.4 → confamnode-0.2.6}/confamnode/builders.py +0 -0
  17. {confamnode-0.2.4 → confamnode-0.2.6}/confamnode/config.py +0 -0
  18. {confamnode-0.2.4 → confamnode-0.2.6}/confamnode/exceptions.py +0 -0
  19. {confamnode-0.2.4 → confamnode-0.2.6}/confamnode/models.py +0 -0
  20. {confamnode-0.2.4 → confamnode-0.2.6}/confamnode/registry.py +0 -0
  21. {confamnode-0.2.4 → confamnode-0.2.6}/tests/__init__.py +0 -0
  22. {confamnode-0.2.4 → confamnode-0.2.6}/tests/test_ansa.py +0 -0
  23. {confamnode-0.2.4 → confamnode-0.2.6}/tests/test_client.py +0 -0
  24. {confamnode-0.2.4 → confamnode-0.2.6}/tests/test_exceptions.py +0 -0
  25. {confamnode-0.2.4 → confamnode-0.2.6}/tests/test_gist.py +0 -0
  26. {confamnode-0.2.4 → confamnode-0.2.6}/tests/test_init.py +0 -0
  27. {confamnode-0.2.4 → confamnode-0.2.6}/tests/test_models.py +0 -0
  28. {confamnode-0.2.4 → confamnode-0.2.6}/tests/test_stream.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: confamnode
3
- Version: 0.2.4
3
+ Version: 0.2.6
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
@@ -258,7 +258,7 @@ ansa = client.gist(
258
258
 
259
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
260
 
261
- Pass `cache=True` to read from and write to the cache — useful for idempotent lookups or to save cost on repeated queries:
261
+ Pass `cache=True` to let the gateway serve and store a cached response — useful for idempotent lookups:
262
262
 
263
263
  ```python
264
264
  # Default — caching off, fresh response every call
@@ -267,8 +267,8 @@ ansa = client.gist(
267
267
  messages="How you dey?"
268
268
  )
269
269
 
270
- # Enable caching — use a stored response when the request matches,
271
- # and store this response for next time
270
+ # Enable caching — the gateway may return a stored response for a
271
+ # matching request, and store this one for next time
272
272
  ansa = client.gist(
273
273
  model="confam-speed",
274
274
  messages="How you dey?",
@@ -276,7 +276,7 @@ ansa = client.gist(
276
276
  )
277
277
  ```
278
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.
279
+ Caching must be enabled for your account for `cache=True` to take effect. A cache hit returns near-instantly the quickest way to see it is to send the **same** request twice with `cache=True`: the first call populates the cache, the second is served from it.
280
280
 
281
281
  ---
282
282
 
@@ -234,7 +234,7 @@ ansa = client.gist(
234
234
 
235
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
236
 
237
- Pass `cache=True` to read from and write to the cache — useful for idempotent lookups or to save cost on repeated queries:
237
+ Pass `cache=True` to let the gateway serve and store a cached response — useful for idempotent lookups:
238
238
 
239
239
  ```python
240
240
  # Default — caching off, fresh response every call
@@ -243,8 +243,8 @@ ansa = client.gist(
243
243
  messages="How you dey?"
244
244
  )
245
245
 
246
- # Enable caching — use a stored response when the request matches,
247
- # and store this response for next time
246
+ # Enable caching — the gateway may return a stored response for a
247
+ # matching request, and store this one for next time
248
248
  ansa = client.gist(
249
249
  model="confam-speed",
250
250
  messages="How you dey?",
@@ -252,7 +252,7 @@ ansa = client.gist(
252
252
  )
253
253
  ```
254
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.
255
+ Caching must be enabled for your account for `cache=True` to take effect. A cache hit returns near-instantly the quickest way to see it is to send the **same** request twice with `cache=True`: the first call populates the cache, the second is served from it.
256
256
 
257
257
  ---
258
258
 
@@ -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.4"
11
+ __version__ = "0.2.6"
12
12
 
13
13
  __all__ = [
14
14
  "ConfamNode",
@@ -61,20 +61,17 @@ class ConfamNode:
61
61
  else:
62
62
  body["system"] = system # None or custom string
63
63
 
64
- # Caching is controlled per request. By default the SDK opts OUT, so
65
- # identical requests (same model + messages + system) each return a
64
+ # Caching is opt-in. By default the SDK sends an explicit cache-bypass,
65
+ # so identical requests (same model + messages + system) each return a
66
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).
67
+ # same prompt expecting varied output.
69
68
  #
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:
69
+ # With cache=True we send NO cache field at all, letting the gateway
70
+ # apply its normal caching. That absent-field shape is exactly what
71
+ # produced cached responses before this flag existed, so it's the
72
+ # request form known to engage the cache -- more reliable than sending
73
+ # explicit "false" flags the gateway may not interpret identically.
74
+ if not cache:
78
75
  body["cache"] = {
79
76
  "no-cache": True, # Skip cache check, get fresh response
80
77
  "no-store": True # Don't cache this response
@@ -0,0 +1,48 @@
1
+ """
2
+ Small internal helpers shared across the confamnode client.
3
+ """
4
+
5
+
6
+ import re
7
+
8
+
9
+ def extract_error(response) -> str:
10
+ """
11
+ Best-effort error detail from a non-2xx response, WITHOUT raising.
12
+
13
+ The server's normal error shape is {"detail": "..."}, but gateway-level
14
+ errors (e.g. a 429 throttle, a 502/504 from a proxy, a 524 origin timeout)
15
+ often return an empty or non-JSON body. Calling response.json() directly on
16
+ those bodies raises JSONDecodeError and swallows the status code -- the one
17
+ thing the caller actually needs. So we parse defensively and always fall
18
+ back to something readable.
19
+
20
+ HTML error pages (Cloudflare, nginx, ...) are summarised to their <title>
21
+ rather than dumped in full, so error messages and logs stay readable.
22
+ """
23
+ # 1. Normal JSON error shape.
24
+ try:
25
+ payload = response.json()
26
+ if isinstance(payload, dict):
27
+ detail = payload.get("detail") or payload.get("error") or payload.get("message")
28
+ if detail:
29
+ return str(detail)
30
+ except Exception:
31
+ pass
32
+
33
+ text = (getattr(response, "text", "") or "").strip()
34
+ if not text:
35
+ return "no response body"
36
+
37
+ # 2. HTML error page: the full page is noise. Pull the <title>, which
38
+ # carries the human-readable status (e.g. "524: A timeout occurred").
39
+ if "<html" in text[:200].lower() or "<!doctype html" in text[:200].lower():
40
+ m = re.search(r"<title[^>]*>(.*?)</title>", text, re.IGNORECASE | re.DOTALL)
41
+ if m:
42
+ title = re.sub(r"\s+", " ", m.group(1)).strip()
43
+ if title:
44
+ return title
45
+ return "HTML error page (no title)"
46
+
47
+ # 3. Plain-text body: return it, truncated so we never dump a wall.
48
+ return text if len(text) <= 300 else text[:297] + "..."
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "confamnode"
3
- version = "0.2.4"
3
+ version = "0.2.6"
4
4
  description = "The Nigerian AI inference gateway"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -18,7 +18,6 @@ API_KEY = "confam-test"
18
18
  MODEL = next(iter(VALID_MODELS))
19
19
 
20
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
21
 
23
22
 
24
23
  @pytest.fixture
@@ -55,20 +54,13 @@ def test_cache_off_by_default(captured_body):
55
54
  assert captured_body["body"]["cache"] == CACHE_OFF
56
55
 
57
56
 
58
- def test_cache_true_uses_and_stores(captured_body):
57
+ def test_cache_true_omits_field(captured_body):
58
+ # cache=True lets the gateway apply its normal caching by sending NO cache
59
+ # field -- the request shape proven to engage the cache.
59
60
  _gist(cache=True)
60
- assert captured_body["body"]["cache"] == CACHE_ON
61
+ assert "cache" not in captured_body["body"]
61
62
 
62
63
 
63
64
  def test_cache_false_is_explicit_off(captured_body):
64
65
  _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"]
66
+ assert captured_body["body"]["cache"] == CACHE_OFF
@@ -58,14 +58,20 @@ def test_json_detail_error_surfaces_detail(monkeypatch):
58
58
  assert "invalid model" in str(exc.value)
59
59
 
60
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>"))
61
+ def test_html_gateway_error_surfaces_title(monkeypatch):
62
+ html = (
63
+ "<!DOCTYPE html><html><head>"
64
+ "<title>confamnode.com | 524: A timeout occurred</title>"
65
+ "</head><body>...</body></html>"
66
+ )
67
+ _patch_post(monkeypatch, FakeResponse(524, json_raises=True, text=html))
64
68
  client = ConfamNode(api_key=API_KEY)
65
69
  with pytest.raises(Exception) as exc:
66
70
  client.gist(model=MODEL, messages="hi")
67
- assert "502" in str(exc.value)
68
- assert "Bad Gateway" in str(exc.value)
71
+ msg = str(exc.value)
72
+ assert "524" in msg
73
+ assert "A timeout occurred" in msg
74
+ assert "<html" not in msg # the page itself must NOT be dumped into the error
69
75
 
70
76
 
71
77
  def test_success_path_still_returns_ansa(monkeypatch):
@@ -29,6 +29,27 @@ def test_non_json_text_body_is_returned_stripped():
29
29
  assert extract_error(_Resp(json_raises=True, text=" upstream timeout ")) == "upstream timeout"
30
30
 
31
31
 
32
+ def test_html_error_page_summarised_to_title():
33
+ # A Cloudflare 524 page: don't dump the whole page, surface the <title>.
34
+ html = (
35
+ "<!DOCTYPE html><html><head>"
36
+ "<title>confamnode.com | 524: A timeout occurred</title>"
37
+ "</head><body>...lots of markup...</body></html>"
38
+ )
39
+ assert extract_error(_Resp(json_raises=True, text=html)) == "confamnode.com | 524: A timeout occurred"
40
+
41
+
42
+ def test_html_without_title_falls_back():
43
+ html = "<html><body>502 Bad Gateway</body></html>"
44
+ assert extract_error(_Resp(json_raises=True, text=html)) == "HTML error page (no title)"
45
+
46
+
47
+ def test_long_plain_text_is_truncated():
48
+ body = "x" * 500
49
+ out = extract_error(_Resp(json_raises=True, text=body))
50
+ assert len(out) == 300 and out.endswith("...")
51
+
52
+
32
53
  def test_detail_key_is_preferred():
33
54
  assert extract_error(_Resp(payload={"detail": "invalid model"})) == "invalid model"
34
55
 
@@ -40,7 +40,7 @@ wheels = [
40
40
 
41
41
  [[package]]
42
42
  name = "confamnode"
43
- version = "0.2.3"
43
+ version = "0.2.6"
44
44
  source = { editable = "." }
45
45
  dependencies = [
46
46
  { name = "httpx" },
@@ -1,26 +0,0 @@
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"
File without changes
File without changes
File without changes
File without changes