confamnode 0.1.2__tar.gz → 0.2.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.
Files changed (29) hide show
  1. {confamnode-0.1.2 → confamnode-0.2.0}/PKG-INFO +30 -23
  2. {confamnode-0.1.2 → confamnode-0.2.0}/README.md +28 -21
  3. {confamnode-0.1.2 → confamnode-0.2.0}/confamnode/__init__.py +1 -1
  4. {confamnode-0.1.2 → confamnode-0.2.0}/confamnode/ansa.py +22 -1
  5. confamnode-0.2.0/confamnode/builders.py +21 -0
  6. confamnode-0.2.0/confamnode/client.py +210 -0
  7. confamnode-0.2.0/confamnode/config.py +3 -0
  8. {confamnode-0.1.2 → confamnode-0.2.0}/pyproject.toml +3 -3
  9. {confamnode-0.1.2 → confamnode-0.2.0}/tests/test_ansa.py +60 -2
  10. confamnode-0.2.0/tests/test_client.py +80 -0
  11. confamnode-0.2.0/tests/test_gist.py +250 -0
  12. confamnode-0.2.0/tests/test_stream.py +159 -0
  13. confamnode-0.2.0/uv.lock +276 -0
  14. confamnode-0.1.2/confamnode/client.py +0 -183
  15. confamnode-0.1.2/confamnode/prompts.py +0 -32
  16. confamnode-0.1.2/tests/test_client.py +0 -56
  17. confamnode-0.1.2/tests/test_gist.py +0 -312
  18. confamnode-0.1.2/tests/test_stream.py +0 -170
  19. confamnode-0.1.2/uv.lock +0 -2324
  20. {confamnode-0.1.2 → confamnode-0.2.0}/.gitignore +0 -0
  21. {confamnode-0.1.2 → confamnode-0.2.0}/.python-version +0 -0
  22. {confamnode-0.1.2 → confamnode-0.2.0}/LICENSE +0 -0
  23. {confamnode-0.1.2 → confamnode-0.2.0}/confamnode/exceptions.py +0 -0
  24. {confamnode-0.1.2 → confamnode-0.2.0}/confamnode/models.py +0 -0
  25. {confamnode-0.1.2 → confamnode-0.2.0}/confamnode/registry.py +0 -0
  26. {confamnode-0.1.2 → confamnode-0.2.0}/tests/__init__.py +0 -0
  27. {confamnode-0.1.2 → confamnode-0.2.0}/tests/test_exceptions.py +0 -0
  28. {confamnode-0.1.2 → confamnode-0.2.0}/tests/test_init.py +0 -0
  29. {confamnode-0.1.2 → confamnode-0.2.0}/tests/test_models.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: confamnode
3
- Version: 0.1.2
3
+ Version: 0.2.0
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
@@ -18,7 +18,7 @@ Classifier: Programming Language :: Python :: 3.12
18
18
  Classifier: Programming Language :: Python :: 3.13
19
19
  Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
20
20
  Requires-Python: >=3.10
21
- Requires-Dist: litellm>=1.87.1
21
+ Requires-Dist: httpx>=0.28.1
22
22
  Requires-Dist: python-dotenv>=1.2.2
23
23
  Description-Content-Type: text/markdown
24
24
 
@@ -85,7 +85,7 @@ pip install confamnode
85
85
  ```python
86
86
  from confamnode import ConfamNode
87
87
 
88
- client = ConfamNode(api_key="confam-sk-xxx")
88
+ client = ConfamNode(api_key="confam-xxx")
89
89
 
90
90
  ansa = client.gist(
91
91
  model="confam-speed",
@@ -93,9 +93,9 @@ ansa = client.gist(
93
93
  )
94
94
 
95
95
  print(ansa.text)
96
- print(f"Cost: ₦{ansa.cost.naira:.6f}")
96
+ print(f"Cost: ₦{ansa.cost.naira:.6f}")
97
97
  print(f"Tokens: {ansa.usage.total_tokens}")
98
- print(f"ID: {ansa.id}")
98
+ print(f"ID: {ansa.id}")
99
99
  ```
100
100
 
101
101
  ---
@@ -105,7 +105,7 @@ print(f"ID: {ansa.id}")
105
105
  ```python
106
106
  from confamnode import ConfamNode
107
107
 
108
- client = ConfamNode(api_key="confam-sk-xxx")
108
+ client = ConfamNode(api_key="confam-xxx")
109
109
 
110
110
  stream = client.gist(
111
111
  model="confam-speed",
@@ -121,9 +121,9 @@ for yarn in stream:
121
121
 
122
122
  # Get full Ansa after stream completes
123
123
  ansa = stream.get_ansa()
124
- print(f"\nModel: {ansa.model}")
124
+ print(f"\nModel: {ansa.model}")
125
125
  print(f"Tokens: {ansa.usage.total_tokens}")
126
- print(f"Cost: ₦{ansa.cost.naira:.6f}")
126
+ print(f"Cost: ₦{ansa.cost.naira:.6f}")
127
127
  if ansa.cost.dollars:
128
128
  print(f"${ansa.cost.dollars:.8f}")
129
129
  print(f"ID: {ansa.id}")
@@ -136,7 +136,7 @@ print(f"ID: {ansa.id}")
136
136
  Every `gist()` call returns an `Ansa` object:
137
137
 
138
138
  ```python
139
- ansa = client.gist(model="confam-speed", messages="How far?")
139
+ ansa = client.gist(model="confam-speed", messages="How you dey?")
140
140
 
141
141
  # Response
142
142
  ansa.text # response text
@@ -147,7 +147,7 @@ ansa.citations # citations (search models only)
147
147
  ansa.finish_reason # why generation stopped
148
148
 
149
149
  # Usage
150
- ansa.usage.prompt_tokens # input tokens used
150
+ ansa.usage.prompt_tokens # input tokens used (includes system message)
151
151
  ansa.usage.completion_tokens # output tokens used
152
152
  ansa.usage.total_tokens # total tokens used
153
153
 
@@ -161,9 +161,16 @@ ansa.cost.dollars # cost in USD (if available)
161
161
  ansa.is_local # True — runs on Nigerian hardware
162
162
  ansa.is_ngn_data_residency # True — data never leaves Nigeria
163
163
  ansa.id # unique request ID (confam-xxxx-xxxx)
164
- ansa.raw # original LiteLLM response
164
+
165
+ # Response metadata
166
+ ansa.raw # dict with id, finish_reason, usage
167
+ ansa.raw["id"] # provider response ID
168
+ ansa.raw["finish_reason"] # why generation stopped
169
+ ansa.raw["usage"] # prompt and completion token counts
165
170
  ```
166
171
 
172
+ > **Note:** `prompt_tokens` includes any system message tokens. This is standard behaviour across all LLM providers (OpenAI, Anthropic, etc.).
173
+
167
174
  ---
168
175
 
169
176
  ## Models
@@ -184,15 +191,14 @@ ansa.raw # original LiteLLM response
184
191
  | `confam-deep-reasoning` | Complex thinking, multi-step analysis | ₦234 | ₦468 | ₦0.234 | ₦0.468 |
185
192
  | `confam-code` | Coding assistance, 1M context | ₦234 | ₦468 | ₦0.234 | ₦0.468 |
186
193
 
187
- ### Local Models — Nigeria Data Residency
194
+ ### Local Models — Nigerian Data Residency
188
195
 
189
196
  | Model | Description | Input ₦/1M | Output ₦/1M | Input ₦/1K | Output ₦/1K |
190
197
  |---|---|---|---|---|---|
191
- | `confam-nano` | Qwen3.5 4B on Jetson Orin Nano | ₦500 | ₦1,500 | ₦0.500 | ₦1.500 |
198
+ | `confam-nano` | Local model data stays in Nigeria | ₦500 | ₦1,500 | ₦0.500 | ₦1.500 |
192
199
 
193
200
  Runs entirely on Nigerian hardware. Data never transmitted abroad.
194
-
195
- Data never leaves Nigeria. Ideal for banks, fintechs, hospitals, law firms, and government agencies.
201
+ Ideal for banks, fintechs, hospitals, law firms, and government agencies.
196
202
 
197
203
  More models coming soon. Contact [hello@confamnode.com](mailto:hello@confamnode.com) for early access.
198
204
 
@@ -259,9 +265,10 @@ Enable extended thinking for complex problems:
259
265
  ansa = client.gist(
260
266
  model="confam-reasoning",
261
267
  messages="One trader buy goods for ₦50,000 sell am for ₦75,000. After e pay ₦5,000 for transport and ₦3,000 for market, wetin be the real profit? Show how you calculate am.",
262
- # Explicit reasoning control
263
268
  allowed_openai_params=["reasoning_effort"],
264
- rreasoning_effort={"effort": "low", "summary": "detailed"} # effort can be "low", "mid", "high", or "xhigh". summary can be "detailed", or "concise"
269
+ reasoning_effort={"effort": "low", "summary": "detailed"}
270
+ # effort: "low", "medium", "high", or "xhigh"
271
+ # summary: "detailed" or "concise"
265
272
  )
266
273
 
267
274
  print(ansa.reasoning) # thinking trace
@@ -274,9 +281,8 @@ Also available on `confam-deep-reasoning` for more complex multi-step problems:
274
281
  ansa = client.gist(
275
282
  model="confam-deep-reasoning",
276
283
  messages="Analyse the financial risk of a Nigerian fintech expanding to Ghana...",
277
- # Explicit reasoning control
278
284
  allowed_openai_params=["reasoning_effort"],
279
- rreasoning_effort={"effort": "low", "summary": "detailed"} # effort can be "low", "mid", "high", or "xhigh". summary can be "detailed", or "concise"
285
+ reasoning_effort={"effort": "high", "summary": "detailed"}
280
286
  )
281
287
 
282
288
  print(ansa.reasoning) # full thinking trace
@@ -324,7 +330,7 @@ print(ansa.text)
324
330
  ## Environment Variable
325
331
 
326
332
  ```bash
327
- export CONFAMNODE_API_KEY="confam-sk-xxx"
333
+ export CONFAMNODE_API_KEY="confam-xxx"
328
334
  ```
329
335
 
330
336
  ```python
@@ -340,7 +346,7 @@ For enterprise clients running ConfamNode on private infrastructure:
340
346
 
341
347
  ```python
342
348
  client = ConfamNode(
343
- api_key="confam-sk-xxx",
349
+ api_key="confam-xxx",
344
350
  base_url="http://your-private-server:4000/v1"
345
351
  )
346
352
  ```
@@ -376,7 +382,7 @@ except ConfamNodeError as e:
376
382
 
377
383
  ## Private AI Deployment
378
384
 
379
- Need Data-residential private AI on your own infrastructure?
385
+ Need data-residential private AI on your own infrastructure?
380
386
 
381
387
  JoTeq the First offers:
382
388
  - On-premise deployment on Jetson devices and GPUs
@@ -391,6 +397,7 @@ Contact: [hello@confamnode.com](mailto:hello@confamnode.com)
391
397
 
392
398
  ## Links
393
399
 
400
+ - Website: [confamnode.com](https://confamnode.com)
394
401
  - PyPI: [pypi.org/project/confamnode](https://pypi.org/project/confamnode)
395
402
  - GitHub: [github.com/confamnodeai/confamnode](https://github.com/confamnodeai/confamnode)
396
403
  - General: [hello@confamnode.com](mailto:hello@confamnode.com)
@@ -403,4 +410,4 @@ Contact: [hello@confamnode.com](mailto:hello@confamnode.com)
403
410
 
404
411
  Apache 2.0
405
412
 
406
- ---
413
+ ---
@@ -61,7 +61,7 @@ pip install confamnode
61
61
  ```python
62
62
  from confamnode import ConfamNode
63
63
 
64
- client = ConfamNode(api_key="confam-sk-xxx")
64
+ client = ConfamNode(api_key="confam-xxx")
65
65
 
66
66
  ansa = client.gist(
67
67
  model="confam-speed",
@@ -69,9 +69,9 @@ ansa = client.gist(
69
69
  )
70
70
 
71
71
  print(ansa.text)
72
- print(f"Cost: ₦{ansa.cost.naira:.6f}")
72
+ print(f"Cost: ₦{ansa.cost.naira:.6f}")
73
73
  print(f"Tokens: {ansa.usage.total_tokens}")
74
- print(f"ID: {ansa.id}")
74
+ print(f"ID: {ansa.id}")
75
75
  ```
76
76
 
77
77
  ---
@@ -81,7 +81,7 @@ print(f"ID: {ansa.id}")
81
81
  ```python
82
82
  from confamnode import ConfamNode
83
83
 
84
- client = ConfamNode(api_key="confam-sk-xxx")
84
+ client = ConfamNode(api_key="confam-xxx")
85
85
 
86
86
  stream = client.gist(
87
87
  model="confam-speed",
@@ -97,9 +97,9 @@ for yarn in stream:
97
97
 
98
98
  # Get full Ansa after stream completes
99
99
  ansa = stream.get_ansa()
100
- print(f"\nModel: {ansa.model}")
100
+ print(f"\nModel: {ansa.model}")
101
101
  print(f"Tokens: {ansa.usage.total_tokens}")
102
- print(f"Cost: ₦{ansa.cost.naira:.6f}")
102
+ print(f"Cost: ₦{ansa.cost.naira:.6f}")
103
103
  if ansa.cost.dollars:
104
104
  print(f"${ansa.cost.dollars:.8f}")
105
105
  print(f"ID: {ansa.id}")
@@ -112,7 +112,7 @@ print(f"ID: {ansa.id}")
112
112
  Every `gist()` call returns an `Ansa` object:
113
113
 
114
114
  ```python
115
- ansa = client.gist(model="confam-speed", messages="How far?")
115
+ ansa = client.gist(model="confam-speed", messages="How you dey?")
116
116
 
117
117
  # Response
118
118
  ansa.text # response text
@@ -123,7 +123,7 @@ ansa.citations # citations (search models only)
123
123
  ansa.finish_reason # why generation stopped
124
124
 
125
125
  # Usage
126
- ansa.usage.prompt_tokens # input tokens used
126
+ ansa.usage.prompt_tokens # input tokens used (includes system message)
127
127
  ansa.usage.completion_tokens # output tokens used
128
128
  ansa.usage.total_tokens # total tokens used
129
129
 
@@ -137,9 +137,16 @@ ansa.cost.dollars # cost in USD (if available)
137
137
  ansa.is_local # True — runs on Nigerian hardware
138
138
  ansa.is_ngn_data_residency # True — data never leaves Nigeria
139
139
  ansa.id # unique request ID (confam-xxxx-xxxx)
140
- ansa.raw # original LiteLLM response
140
+
141
+ # Response metadata
142
+ ansa.raw # dict with id, finish_reason, usage
143
+ ansa.raw["id"] # provider response ID
144
+ ansa.raw["finish_reason"] # why generation stopped
145
+ ansa.raw["usage"] # prompt and completion token counts
141
146
  ```
142
147
 
148
+ > **Note:** `prompt_tokens` includes any system message tokens. This is standard behaviour across all LLM providers (OpenAI, Anthropic, etc.).
149
+
143
150
  ---
144
151
 
145
152
  ## Models
@@ -160,15 +167,14 @@ ansa.raw # original LiteLLM response
160
167
  | `confam-deep-reasoning` | Complex thinking, multi-step analysis | ₦234 | ₦468 | ₦0.234 | ₦0.468 |
161
168
  | `confam-code` | Coding assistance, 1M context | ₦234 | ₦468 | ₦0.234 | ₦0.468 |
162
169
 
163
- ### Local Models — Nigeria Data Residency
170
+ ### Local Models — Nigerian Data Residency
164
171
 
165
172
  | Model | Description | Input ₦/1M | Output ₦/1M | Input ₦/1K | Output ₦/1K |
166
173
  |---|---|---|---|---|---|
167
- | `confam-nano` | Qwen3.5 4B on Jetson Orin Nano | ₦500 | ₦1,500 | ₦0.500 | ₦1.500 |
174
+ | `confam-nano` | Local model data stays in Nigeria | ₦500 | ₦1,500 | ₦0.500 | ₦1.500 |
168
175
 
169
176
  Runs entirely on Nigerian hardware. Data never transmitted abroad.
170
-
171
- Data never leaves Nigeria. Ideal for banks, fintechs, hospitals, law firms, and government agencies.
177
+ Ideal for banks, fintechs, hospitals, law firms, and government agencies.
172
178
 
173
179
  More models coming soon. Contact [hello@confamnode.com](mailto:hello@confamnode.com) for early access.
174
180
 
@@ -235,9 +241,10 @@ Enable extended thinking for complex problems:
235
241
  ansa = client.gist(
236
242
  model="confam-reasoning",
237
243
  messages="One trader buy goods for ₦50,000 sell am for ₦75,000. After e pay ₦5,000 for transport and ₦3,000 for market, wetin be the real profit? Show how you calculate am.",
238
- # Explicit reasoning control
239
244
  allowed_openai_params=["reasoning_effort"],
240
- rreasoning_effort={"effort": "low", "summary": "detailed"} # effort can be "low", "mid", "high", or "xhigh". summary can be "detailed", or "concise"
245
+ reasoning_effort={"effort": "low", "summary": "detailed"}
246
+ # effort: "low", "medium", "high", or "xhigh"
247
+ # summary: "detailed" or "concise"
241
248
  )
242
249
 
243
250
  print(ansa.reasoning) # thinking trace
@@ -250,9 +257,8 @@ Also available on `confam-deep-reasoning` for more complex multi-step problems:
250
257
  ansa = client.gist(
251
258
  model="confam-deep-reasoning",
252
259
  messages="Analyse the financial risk of a Nigerian fintech expanding to Ghana...",
253
- # Explicit reasoning control
254
260
  allowed_openai_params=["reasoning_effort"],
255
- rreasoning_effort={"effort": "low", "summary": "detailed"} # effort can be "low", "mid", "high", or "xhigh". summary can be "detailed", or "concise"
261
+ reasoning_effort={"effort": "high", "summary": "detailed"}
256
262
  )
257
263
 
258
264
  print(ansa.reasoning) # full thinking trace
@@ -300,7 +306,7 @@ print(ansa.text)
300
306
  ## Environment Variable
301
307
 
302
308
  ```bash
303
- export CONFAMNODE_API_KEY="confam-sk-xxx"
309
+ export CONFAMNODE_API_KEY="confam-xxx"
304
310
  ```
305
311
 
306
312
  ```python
@@ -316,7 +322,7 @@ For enterprise clients running ConfamNode on private infrastructure:
316
322
 
317
323
  ```python
318
324
  client = ConfamNode(
319
- api_key="confam-sk-xxx",
325
+ api_key="confam-xxx",
320
326
  base_url="http://your-private-server:4000/v1"
321
327
  )
322
328
  ```
@@ -352,7 +358,7 @@ except ConfamNodeError as e:
352
358
 
353
359
  ## Private AI Deployment
354
360
 
355
- Need Data-residential private AI on your own infrastructure?
361
+ Need data-residential private AI on your own infrastructure?
356
362
 
357
363
  JoTeq the First offers:
358
364
  - On-premise deployment on Jetson devices and GPUs
@@ -367,6 +373,7 @@ Contact: [hello@confamnode.com](mailto:hello@confamnode.com)
367
373
 
368
374
  ## Links
369
375
 
376
+ - Website: [confamnode.com](https://confamnode.com)
370
377
  - PyPI: [pypi.org/project/confamnode](https://pypi.org/project/confamnode)
371
378
  - GitHub: [github.com/confamnodeai/confamnode](https://github.com/confamnodeai/confamnode)
372
379
  - General: [hello@confamnode.com](mailto:hello@confamnode.com)
@@ -379,4 +386,4 @@ Contact: [hello@confamnode.com](mailto:hello@confamnode.com)
379
386
 
380
387
  Apache 2.0
381
388
 
382
- ---
389
+ ---
@@ -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.1.2"
11
+ __version__ = "0.2.0"
12
12
 
13
13
  __all__ = [
14
14
  "ConfamNode",
@@ -30,4 +30,25 @@ class Ansa:
30
30
  citations: list = field(default_factory=list)
31
31
  id: str = field(default_factory=lambda: f"confam-{uuid.uuid4()}")
32
32
  is_local: bool = False
33
- is_ngn_data_residency: bool = False
33
+ is_ngn_data_residency: bool = False
34
+
35
+
36
+ @dataclass
37
+ class StreamDelta:
38
+ role: str | None = None
39
+ content: str | None = None
40
+ reasoning: str | None = None
41
+
42
+
43
+ @dataclass
44
+ class StreamChoice:
45
+ index: int = 0
46
+ delta: StreamDelta = field(default_factory=StreamDelta)
47
+ finish_reason: str | None = None
48
+
49
+
50
+ @dataclass
51
+ class StreamChunk:
52
+ id: str = ""
53
+ model: str = ""
54
+ choices: list = field(default_factory=list)
@@ -0,0 +1,21 @@
1
+ from confamnode.ansa import StreamChunk, StreamChoice, StreamDelta
2
+
3
+
4
+ def parse_chunk(raw: dict) -> StreamChunk:
5
+ choices = []
6
+ for c in raw.get("choices", []):
7
+ d = c.get("delta", {})
8
+ choices.append(StreamChoice(
9
+ index=c.get("index", 0),
10
+ finish_reason=c.get("finish_reason"),
11
+ delta=StreamDelta(
12
+ role=d.get("role"),
13
+ content=d.get("content"),
14
+ reasoning=d.get("reasoning")
15
+ )
16
+ ))
17
+ return StreamChunk(
18
+ id=raw.get("id", ""),
19
+ model=raw.get("model", ""),
20
+ choices=choices,
21
+ )
@@ -0,0 +1,210 @@
1
+ import os
2
+ import json
3
+ import httpx
4
+
5
+ from typing import Union, List, Dict
6
+
7
+ from confamnode.builders import parse_chunk
8
+ from confamnode.ansa import Ansa, Usage, Cost
9
+ from confamnode.registry import VALID_MODELS
10
+ from confamnode.exceptions import ConfamAuthError, ConfamModelError
11
+ from confamnode.config import DEFAULT_BASE_URL, DEFAULT_TIMEOUT, DEFAULT_CONNECT_TIMEOUT
12
+
13
+
14
+ class ConfamNode:
15
+ def __init__(
16
+ self,
17
+ api_key: str = None,
18
+ base_url: str = None
19
+ ):
20
+ # Pick up from environment if not provided
21
+ api_key = api_key or os.environ.get("CONFAMNODE_API_KEY")
22
+
23
+ if not api_key:
24
+ raise ValueError("api_key is required")
25
+
26
+ if not api_key.startswith("confam-"):
27
+ raise ConfamAuthError()
28
+
29
+ self.api_key = api_key
30
+ self.base_url = base_url or DEFAULT_BASE_URL
31
+
32
+ def gist(
33
+ self,
34
+ model: str,
35
+ messages: Union[str, List[Dict[str, str]]],
36
+ system: str | None = "default",
37
+ **kwargs
38
+ ) -> "Ansa | ConfamStream":
39
+ if model not in VALID_MODELS:
40
+ raise ConfamModelError(model)
41
+
42
+ # Handle string messages
43
+ if isinstance(messages, str):
44
+ messages = [{"role": "user", "content": messages}]
45
+ elif not isinstance(messages, list):
46
+ raise ValueError("messages must be a string or list")
47
+
48
+ body = {
49
+ "model": model,
50
+ "messages": messages,
51
+ **kwargs
52
+ }
53
+
54
+ # System message tri-state
55
+ has_system_in_messages = any(m.get("role") == "system" for m in messages)
56
+ if not has_system_in_messages:
57
+ if system == "default":
58
+ pass
59
+ else:
60
+ body["system"] = system # None or custom string
61
+
62
+ if kwargs.get("stream", False):
63
+ http_client = httpx.Client(
64
+ timeout=httpx.Timeout(DEFAULT_TIMEOUT, connect=DEFAULT_CONNECT_TIMEOUT)
65
+ )
66
+ req = http_client.build_request(
67
+ "POST",
68
+ f"{self.base_url}/chat/completions",
69
+ headers={
70
+ "Authorization": f"Bearer {self.api_key}",
71
+ "Content-Type": "application/json",
72
+ },
73
+ json=body,
74
+ )
75
+ stream_response = http_client.send(req, stream=True)
76
+
77
+ if stream_response.status_code >= 400:
78
+ stream_response.read()
79
+ stream_response.close()
80
+ http_client.close()
81
+ error = stream_response.json().get("detail", "Requeest failed")
82
+ raise Exception(f"ConfamNode error {stream_response.status_code}: {error}")
83
+
84
+ return ConfamStream(stream_response, http_client, model)
85
+
86
+ response = httpx.post(
87
+ f"{self.base_url}/chat/completions",
88
+ headers={
89
+ "Authorization": f"Bearer {self.api_key}",
90
+ "Content-Type": "application/json",
91
+ },
92
+ json=body,
93
+ timeout=httpx.Timeout(DEFAULT_TIMEOUT, connect=DEFAULT_CONNECT_TIMEOUT)
94
+ )
95
+
96
+ if response.status_code >= 400:
97
+ error = response.json().get("detail", "Request failed")
98
+ raise Exception(f"ConfamNode error {response.status_code}: {error}")
99
+
100
+ data = response.json()
101
+ msg = data["choices"][0]["message"]
102
+ usage_data = data.get("usage", {})
103
+ confam = data.get("confam", {})
104
+ cost_data = confam.get("cost", {})
105
+
106
+ return Ansa(
107
+ id=confam.get("request_id", data.get("id", "")),
108
+ text=msg.get("content") or "",
109
+ model=model,
110
+ reasoning=msg.get("reasoning"),
111
+ tools=msg.get("tool_calls") or [],
112
+ citations=msg.get("citations") or [],
113
+ usage=Usage(
114
+ prompt_tokens=usage_data.get("prompt_tokens", 0),
115
+ completion_tokens=usage_data.get("completion_tokens", 0),
116
+ total_tokens=usage_data.get("total_tokens", 0),
117
+ ),
118
+ cost=Cost(
119
+ naira=cost_data.get("naira", 0.0),
120
+ naira_input=cost_data.get("naira_input", 0.0),
121
+ naira_output=cost_data.get("naira_output", 0.0),
122
+ ),
123
+ finish_reason=data["choices"][0].get("finish_reason", "stop"),
124
+ raw={
125
+ "id": data.get("id"),
126
+ "usage": {
127
+ "prompt_tokens": usage_data.get("prompt_tokens", 0),
128
+ "completion_tokens": usage_data.get("completion_tokens", 0)
129
+ }
130
+ },
131
+ is_local=confam.get("is_local", False),
132
+ is_ngn_data_residency=confam.get("is_ngn_data_residency", False),
133
+ )
134
+
135
+
136
+ class ConfamStream:
137
+ def __init__(self, stream_response, http_client, model: str):
138
+ self._stream_response = stream_response
139
+ self._http_client = http_client
140
+ self._model = model
141
+ self._chunks = []
142
+ self._ansa = None
143
+ self._confam_meta = {}
144
+
145
+ def __iter__(self):
146
+ try:
147
+ for line in self._stream_response.iter_lines():
148
+ if not line or not line.startswith("data: "):
149
+ continue
150
+ payload_str = line[len("data: "):]
151
+ if payload_str.strip() == "[DONE]":
152
+ break
153
+ try:
154
+ raw = json.loads(payload_str)
155
+ except json.JSONDecodeError:
156
+ continue
157
+
158
+ if "confam" in raw:
159
+ self._confam_meta = raw["confam"]
160
+ continue
161
+
162
+ if not raw.get("choices"):
163
+ continue
164
+
165
+ chunk = parse_chunk(raw)
166
+ self._chunks.append(chunk)
167
+ yield chunk
168
+ finally:
169
+ self._stream_response.close()
170
+ self._http_client.close()
171
+
172
+ self._ansa = self._build_ansa()
173
+
174
+ def get_ansa(self) -> Ansa:
175
+ if self._ansa is None:
176
+ raise RuntimeError("Stream not complete yet. Iterate through all chunks first.")
177
+ return self._ansa
178
+
179
+ def _build_ansa(self) -> Ansa:
180
+ # Collect text from all chunks
181
+ text = "".join([
182
+ c.choices[0].delta.content or ""
183
+ for c in self._chunks
184
+ if c.choices and c.choices[0].delta.content
185
+ ])
186
+
187
+ # Get finish reason from last chunk
188
+ if self._chunks and self._chunks[-1].choices:
189
+ finish_reason = self._chunks[-1].choices[0].finish_reason or "stop"
190
+
191
+ cost_data = self._confam_meta.get("cost", {})
192
+
193
+ return Ansa(
194
+ id=self._confam_meta.get("request_id", ""),
195
+ text=text,
196
+ model=self._model,
197
+ usage=Usage(prompt_tokens=0, completion_tokens=0, total_tokens=0),
198
+ cost=Cost(
199
+ naira=cost_data.get("naira", 0.0),
200
+ naira_input=cost_data.get("naira_input", 0.0),
201
+ naira_output=cost_data.get("naira_output", 0.0),
202
+ ),
203
+ finish_reason=finish_reason,
204
+ raw={
205
+ "id": self._confam_meta.get("request_id", ""),
206
+ "usage": {"prompt_tokens": 0, "completion_tokens": 0}
207
+ },
208
+ is_local=self._confam_meta.get("is_local", False),
209
+ is_ngn_data_residency=self._confam_meta.get("is_ngn_data_residency", False),
210
+ )
@@ -0,0 +1,3 @@
1
+ DEFAULT_BASE_URL = "https://api.confamnode.com/v1"
2
+ DEFAULT_TIMEOUT = 300.0
3
+ DEFAULT_CONNECT_TIMEOUT = 10.0
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "confamnode"
3
- version = "0.1.2"
3
+ version = "0.2.0"
4
4
  description = "The Nigerian AI inference gateway"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -21,7 +21,7 @@ classifiers = [
21
21
  "Topic :: Scientific/Engineering :: Artificial Intelligence",
22
22
  ]
23
23
  dependencies = [
24
- "litellm>=1.87.1",
24
+ "httpx>=0.28.1",
25
25
  "python-dotenv>=1.2.2",
26
26
  ]
27
27
 
@@ -43,4 +43,4 @@ anyio_mode = "auto"
43
43
 
44
44
  [build-system]
45
45
  requires = ["hatchling"]
46
- build-backend = "hatchling.build"
46
+ build-backend = "hatchling.build"