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.
- {confamnode-0.1.2 → confamnode-0.2.0}/PKG-INFO +30 -23
- {confamnode-0.1.2 → confamnode-0.2.0}/README.md +28 -21
- {confamnode-0.1.2 → confamnode-0.2.0}/confamnode/__init__.py +1 -1
- {confamnode-0.1.2 → confamnode-0.2.0}/confamnode/ansa.py +22 -1
- confamnode-0.2.0/confamnode/builders.py +21 -0
- confamnode-0.2.0/confamnode/client.py +210 -0
- confamnode-0.2.0/confamnode/config.py +3 -0
- {confamnode-0.1.2 → confamnode-0.2.0}/pyproject.toml +3 -3
- {confamnode-0.1.2 → confamnode-0.2.0}/tests/test_ansa.py +60 -2
- confamnode-0.2.0/tests/test_client.py +80 -0
- confamnode-0.2.0/tests/test_gist.py +250 -0
- confamnode-0.2.0/tests/test_stream.py +159 -0
- confamnode-0.2.0/uv.lock +276 -0
- confamnode-0.1.2/confamnode/client.py +0 -183
- confamnode-0.1.2/confamnode/prompts.py +0 -32
- confamnode-0.1.2/tests/test_client.py +0 -56
- confamnode-0.1.2/tests/test_gist.py +0 -312
- confamnode-0.1.2/tests/test_stream.py +0 -170
- confamnode-0.1.2/uv.lock +0 -2324
- {confamnode-0.1.2 → confamnode-0.2.0}/.gitignore +0 -0
- {confamnode-0.1.2 → confamnode-0.2.0}/.python-version +0 -0
- {confamnode-0.1.2 → confamnode-0.2.0}/LICENSE +0 -0
- {confamnode-0.1.2 → confamnode-0.2.0}/confamnode/exceptions.py +0 -0
- {confamnode-0.1.2 → confamnode-0.2.0}/confamnode/models.py +0 -0
- {confamnode-0.1.2 → confamnode-0.2.0}/confamnode/registry.py +0 -0
- {confamnode-0.1.2 → confamnode-0.2.0}/tests/__init__.py +0 -0
- {confamnode-0.1.2 → confamnode-0.2.0}/tests/test_exceptions.py +0 -0
- {confamnode-0.1.2 → confamnode-0.2.0}/tests/test_init.py +0 -0
- {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.
|
|
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:
|
|
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-
|
|
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:
|
|
96
|
+
print(f"Cost: ₦{ansa.cost.naira:.6f}")
|
|
97
97
|
print(f"Tokens: {ansa.usage.total_tokens}")
|
|
98
|
-
print(f"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-
|
|
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:
|
|
124
|
+
print(f"\nModel: {ansa.model}")
|
|
125
125
|
print(f"Tokens: {ansa.usage.total_tokens}")
|
|
126
|
-
print(f"Cost:
|
|
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
|
|
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
|
-
|
|
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 —
|
|
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` |
|
|
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
|
-
|
|
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
|
-
|
|
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-
|
|
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-
|
|
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
|
|
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-
|
|
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:
|
|
72
|
+
print(f"Cost: ₦{ansa.cost.naira:.6f}")
|
|
73
73
|
print(f"Tokens: {ansa.usage.total_tokens}")
|
|
74
|
-
print(f"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-
|
|
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:
|
|
100
|
+
print(f"\nModel: {ansa.model}")
|
|
101
101
|
print(f"Tokens: {ansa.usage.total_tokens}")
|
|
102
|
-
print(f"Cost:
|
|
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
|
|
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
|
-
|
|
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 —
|
|
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` |
|
|
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
|
-
|
|
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
|
-
|
|
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-
|
|
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-
|
|
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
|
|
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
|
+
---
|
|
@@ -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
|
+
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "confamnode"
|
|
3
|
-
version = "0.
|
|
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
|
-
"
|
|
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"
|