confamnode 0.2.5__tar.gz → 0.3.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.2.5 → confamnode-0.3.0}/.gitignore +1 -0
- {confamnode-0.2.5 → confamnode-0.3.0}/PKG-INFO +118 -11
- {confamnode-0.2.5 → confamnode-0.3.0}/README.md +117 -10
- {confamnode-0.2.5 → confamnode-0.3.0}/confamnode/__init__.py +16 -7
- {confamnode-0.2.5 → confamnode-0.3.0}/confamnode/ansa.py +20 -1
- {confamnode-0.2.5 → confamnode-0.3.0}/confamnode/client.py +82 -22
- {confamnode-0.2.5 → confamnode-0.3.0}/confamnode/exceptions.py +18 -0
- {confamnode-0.2.5 → confamnode-0.3.0}/confamnode/models.py +8 -2
- {confamnode-0.2.5 → confamnode-0.3.0}/confamnode/registry.py +3 -2
- {confamnode-0.2.5 → confamnode-0.3.0}/confamnode/utils.py +43 -1
- {confamnode-0.2.5 → confamnode-0.3.0}/pyproject.toml +1 -1
- {confamnode-0.2.5 → confamnode-0.3.0}/tests/test_ansa.py +137 -2
- {confamnode-0.2.5 → confamnode-0.3.0}/tests/test_cache_flag.py +5 -13
- confamnode-0.3.0/tests/test_embed.py +258 -0
- confamnode-0.3.0/tests/test_exceptions.py +63 -0
- {confamnode-0.2.5 → confamnode-0.3.0}/tests/test_gist.py +2 -2
- {confamnode-0.2.5 → confamnode-0.3.0}/tests/test_models.py +20 -9
- {confamnode-0.2.5 → confamnode-0.3.0}/tests/test_stream.py +2 -2
- confamnode-0.3.0/tests/test_utils.py +179 -0
- {confamnode-0.2.5 → confamnode-0.3.0}/uv.lock +1 -1
- confamnode-0.2.5/tests/test_exceptions.py +0 -20
- confamnode-0.2.5/tests/test_utils.py +0 -67
- {confamnode-0.2.5 → confamnode-0.3.0}/.python-version +0 -0
- {confamnode-0.2.5 → confamnode-0.3.0}/LICENSE +0 -0
- {confamnode-0.2.5 → confamnode-0.3.0}/confamnode/builders.py +0 -0
- {confamnode-0.2.5 → confamnode-0.3.0}/confamnode/config.py +0 -0
- {confamnode-0.2.5 → confamnode-0.3.0}/tests/__init__.py +0 -0
- {confamnode-0.2.5 → confamnode-0.3.0}/tests/test_client.py +0 -0
- {confamnode-0.2.5 → confamnode-0.3.0}/tests/test_client_errors.py +0 -0
- {confamnode-0.2.5 → confamnode-0.3.0}/tests/test_init.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: confamnode
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.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
|
|
@@ -161,7 +161,7 @@ ansa.id # unique request ID (confam-xxxx-xxxx)
|
|
|
161
161
|
|
|
162
162
|
# Response metadata
|
|
163
163
|
ansa.raw # dict with id, finish_reason, usage
|
|
164
|
-
ansa.raw["id"]
|
|
164
|
+
ansa.raw["id"] # provider response ID
|
|
165
165
|
ansa.raw["finish_reason"] # why generation stopped
|
|
166
166
|
ansa.raw["usage"] # prompt and completion token counts
|
|
167
167
|
```
|
|
@@ -170,13 +170,82 @@ ansa.raw["usage"] # prompt and completion token counts
|
|
|
170
170
|
|
|
171
171
|
---
|
|
172
172
|
|
|
173
|
+
## Embeddings
|
|
174
|
+
|
|
175
|
+
Generate embeddings with `client.embed()`. The shape of what you get back mirrors the shape of what you send in: a single string returns one flat vector, a list returns a list of vectors.
|
|
176
|
+
|
|
177
|
+
```python
|
|
178
|
+
from confamnode import ConfamNode
|
|
179
|
+
|
|
180
|
+
client = ConfamNode(api_key="confam-xxx")
|
|
181
|
+
|
|
182
|
+
# Single string in → one flat vector out
|
|
183
|
+
result = client.embed(
|
|
184
|
+
model="confam-embed-local",
|
|
185
|
+
input="How you dey?"
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
print(result.embeddings) # [0.123, -0.456, ...]
|
|
189
|
+
print(f"Cost: ₦{result.cost.naira:.6f}")
|
|
190
|
+
print(f"Tokens: {result.usage.total_tokens}")
|
|
191
|
+
|
|
192
|
+
# List in → list of vectors out, always — even with one item
|
|
193
|
+
result = client.embed(
|
|
194
|
+
model="confam-embed-local",
|
|
195
|
+
input=["How you dey?", "Wetin dey happen?"]
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
print(len(result.embeddings)) # 2
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
`confam-embed-local` is currently the only available embedding model. More coming soon.
|
|
202
|
+
|
|
203
|
+
### Task types
|
|
204
|
+
|
|
205
|
+
Some embedding models expect a task-type hint (e.g. `"query"` vs. `"document"`) to produce better embeddings for retrieval use cases. This defaults sensibly server-side if omitted — only pass it if you need a specific one:
|
|
206
|
+
|
|
207
|
+
```python
|
|
208
|
+
result = client.embed(
|
|
209
|
+
model="confam-embed-local",
|
|
210
|
+
input="find me a good jollof rice recipe",
|
|
211
|
+
task_type="query"
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
print(result.task_type_applied) # confirms what actually happened for this call
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
`task_type_applied` is `None` when the requested model doesn't use task-type hints at all — the field itself tells you whether it had any effect, no separate capability lookup needed.
|
|
218
|
+
|
|
219
|
+
### The EmbeddingResult Object
|
|
220
|
+
|
|
221
|
+
```python
|
|
222
|
+
result = client.embed(model="confam-embed-local", input="How you dey?")
|
|
223
|
+
|
|
224
|
+
result.embeddings # vector(s) — shape matches your input shape
|
|
225
|
+
result.model # model that served the request
|
|
226
|
+
result.usage.prompt_tokens # input tokens used
|
|
227
|
+
result.usage.total_tokens # total tokens used (embeddings have no output tokens to bill)
|
|
228
|
+
result.cost.naira # total cost in Naira
|
|
229
|
+
result.cost.naira_input # input cost in Naira
|
|
230
|
+
result.is_local # True — runs on Nigerian hardware
|
|
231
|
+
result.is_ngn_data_residency # True — data never leaves Nigeria
|
|
232
|
+
result.task_type_applied # which task-type hint was actually applied, if any
|
|
233
|
+
result.id # unique request ID
|
|
234
|
+
result.raw # full raw response
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
### Batch limits (`confam-embed-local`)
|
|
238
|
+
|
|
239
|
+
Self-hosted embedding requests to `confam-embed-local` are served in batches of up to 4 concurrent requests at a time. If you're embedding a large number of documents, being aware of this batching ceiling will give you a more predictable sense of total time than assuming unlimited concurrency.
|
|
240
|
+
|
|
241
|
+
---
|
|
242
|
+
|
|
173
243
|
## Models
|
|
174
244
|
|
|
175
245
|
### Free Tier
|
|
176
246
|
|
|
177
247
|
| Model | Description | Modality | Price |
|
|
178
248
|
|---|---|---|---|
|
|
179
|
-
| `confam-lite` | Light text and general chat | Text-to-Text | Free |
|
|
180
249
|
| `confam-speed` | Fast, high quality responses | Image-Text-to-Text | Free |
|
|
181
250
|
| `confam-reasoning` | Standard reasoning and analysis | Text-to-Text | Free |
|
|
182
251
|
|
|
@@ -192,9 +261,18 @@ ansa.raw["usage"] # prompt and completion token counts
|
|
|
192
261
|
|
|
193
262
|
| Model | Description | Modality | Input ₦/1M | Output ₦/1M | Input ₦/1K | Output ₦/1K |
|
|
194
263
|
|---|---|---|---|---|---|---|
|
|
195
|
-
| `confam-
|
|
264
|
+
| `confam-health` | Local model, health information use cases — data stays in Nigeria | Image-Text-to-Text | ₦500 | ₦1,500 | ₦0.500 | ₦1.500 |
|
|
265
|
+
| `confam-local` | Local model, general-purpose — data stays in Nigeria | Text-to-Text | ₦500 | ₦1,500 | ₦0.500 | ₦1.500 |
|
|
266
|
+
|
|
267
|
+
### Embeddings
|
|
268
|
+
|
|
269
|
+
| Model | Description | Where it runs |
|
|
270
|
+
|---|---|---|
|
|
271
|
+
| `confam-embed-local` | Self-hosted text embeddings — data stays in Nigeria | Local |
|
|
272
|
+
|
|
273
|
+
More embedding models coming soon.
|
|
196
274
|
|
|
197
|
-
|
|
275
|
+
Local models run entirely on Nigerian hardware. Data never transmitted abroad.
|
|
198
276
|
Ideal for banks, fintechs, hospitals, law firms, and government agencies.
|
|
199
277
|
|
|
200
278
|
More models coming soon. Contact [hello@confamnode.com](mailto:hello@confamnode.com) for early access.
|
|
@@ -221,7 +299,7 @@ Rate limits vary by plan. Contact [hello@confamnode.com](mailto:hello@confamnode
|
|
|
221
299
|
|
|
222
300
|
- **Free tier** — shared capacity with lower limits
|
|
223
301
|
- **Paid tier** — dedicated higher limits
|
|
224
|
-
- **confam-
|
|
302
|
+
- **Local models** (`confam-health`, `confam-local`) — deployed on dedicated Nigerian hardware (Jetson AGX Orin), with a shared concurrency cap across both models. Under heavy simultaneous load, requests queue briefly rather than failing outright.
|
|
225
303
|
|
|
226
304
|
---
|
|
227
305
|
|
|
@@ -258,7 +336,7 @@ ansa = client.gist(
|
|
|
258
336
|
|
|
259
337
|
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
338
|
|
|
261
|
-
Pass `cache=True` to
|
|
339
|
+
Pass `cache=True` to let the gateway serve and store a cached response — useful for idempotent lookups:
|
|
262
340
|
|
|
263
341
|
```python
|
|
264
342
|
# Default — caching off, fresh response every call
|
|
@@ -267,8 +345,8 @@ ansa = client.gist(
|
|
|
267
345
|
messages="How you dey?"
|
|
268
346
|
)
|
|
269
347
|
|
|
270
|
-
# Enable caching —
|
|
271
|
-
# and store this
|
|
348
|
+
# Enable caching — the gateway may return a stored response for a
|
|
349
|
+
# matching request, and store this one for next time
|
|
272
350
|
ansa = client.gist(
|
|
273
351
|
model="confam-speed",
|
|
274
352
|
messages="How you dey?",
|
|
@@ -276,7 +354,9 @@ ansa = client.gist(
|
|
|
276
354
|
)
|
|
277
355
|
```
|
|
278
356
|
|
|
279
|
-
A cache hit
|
|
357
|
+
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.
|
|
358
|
+
|
|
359
|
+
> **Note:** a cache hit still bills the full Naira cost of the request — you're paying for inference value, not raw compute. Caching saves time, not money.
|
|
280
360
|
|
|
281
361
|
---
|
|
282
362
|
|
|
@@ -309,6 +389,18 @@ print(ansa.reasoning) # full thinking trace
|
|
|
309
389
|
print(ansa.text) # final analysis
|
|
310
390
|
```
|
|
311
391
|
|
|
392
|
+
### Controlling reasoning mode (`confam-local`)
|
|
393
|
+
|
|
394
|
+
`confam-local` reasons in `<think>` blocks by default. To disable this per-request, pass `enable_thinking` nested under `chat_template_kwargs` via `extra_body` — this must be nested this way rather than passed as a top-level kwarg, or it will be silently ignored:
|
|
395
|
+
|
|
396
|
+
```python
|
|
397
|
+
result = client.gist(
|
|
398
|
+
model="confam-local",
|
|
399
|
+
messages="How you dey?",
|
|
400
|
+
extra_body={"chat_template_kwargs": {"enable_thinking": False}}
|
|
401
|
+
)
|
|
402
|
+
```
|
|
403
|
+
|
|
312
404
|
---
|
|
313
405
|
|
|
314
406
|
## RAG (Retrieval-Augmented Generation)
|
|
@@ -321,6 +413,8 @@ print(ansa.text) # final analysis
|
|
|
321
413
|
| `confam-code` | Code search, documentation RAG | 1M tokens |
|
|
322
414
|
| `confam-deep-reasoning` | Complex RAG, multi-hop reasoning | 1M tokens |
|
|
323
415
|
|
|
416
|
+
`confam-embed-local` pairs well with any of the above for retrieval — see [Embeddings](#embeddings).
|
|
417
|
+
|
|
324
418
|
---
|
|
325
419
|
|
|
326
420
|
## Data Residency
|
|
@@ -329,7 +423,7 @@ Nigerian businesses handling sensitive data can use local models that run entire
|
|
|
329
423
|
|
|
330
424
|
```python
|
|
331
425
|
ansa = client.gist(
|
|
332
|
-
model="confam-
|
|
426
|
+
model="confam-local",
|
|
333
427
|
messages="Analyse this sensitive document..."
|
|
334
428
|
)
|
|
335
429
|
|
|
@@ -338,6 +432,8 @@ print(ansa.is_ngn_data_residency) # True — data never leaves Nigeria
|
|
|
338
432
|
print(ansa.text)
|
|
339
433
|
```
|
|
340
434
|
|
|
435
|
+
For health information use cases, `confam-health` offers the same data-residency guarantee on a model tuned for that domain.
|
|
436
|
+
|
|
341
437
|
**Ideal for:**
|
|
342
438
|
- Nigerian banks and fintechs
|
|
343
439
|
- Healthcare companies
|
|
@@ -380,6 +476,9 @@ from confamnode import (
|
|
|
380
476
|
ConfamAuthError,
|
|
381
477
|
ConfamRateLimitError,
|
|
382
478
|
ConfamModelError,
|
|
479
|
+
ConfamInsufficientBalanceError,
|
|
480
|
+
ConfamProviderError,
|
|
481
|
+
ConfamRequestError,
|
|
383
482
|
ConfamNodeError
|
|
384
483
|
)
|
|
385
484
|
|
|
@@ -390,14 +489,22 @@ try:
|
|
|
390
489
|
)
|
|
391
490
|
except ConfamAuthError:
|
|
392
491
|
print("Check your API key")
|
|
492
|
+
except ConfamInsufficientBalanceError:
|
|
493
|
+
print("Wallet balance too low. Top up to continue.")
|
|
393
494
|
except ConfamRateLimitError:
|
|
394
495
|
print("You don reach your limit. Contact hello@confamnode.com")
|
|
395
496
|
except ConfamModelError:
|
|
396
497
|
print("Invalid model name")
|
|
498
|
+
except ConfamProviderError:
|
|
499
|
+
print("The AI provider is temporarily unavailable. Try again shortly.")
|
|
500
|
+
except ConfamRequestError:
|
|
501
|
+
print("Request could not be processed — check your parameters")
|
|
397
502
|
except ConfamNodeError as e:
|
|
398
503
|
print(f"Something went wrong: {e}")
|
|
399
504
|
```
|
|
400
505
|
|
|
506
|
+
All specific exceptions above are subclasses of `ConfamNodeError` — catching that alone is enough if you don't need to distinguish error types.
|
|
507
|
+
|
|
401
508
|
---
|
|
402
509
|
|
|
403
510
|
## Private AI Deployment
|
|
@@ -137,7 +137,7 @@ ansa.id # unique request ID (confam-xxxx-xxxx)
|
|
|
137
137
|
|
|
138
138
|
# Response metadata
|
|
139
139
|
ansa.raw # dict with id, finish_reason, usage
|
|
140
|
-
ansa.raw["id"]
|
|
140
|
+
ansa.raw["id"] # provider response ID
|
|
141
141
|
ansa.raw["finish_reason"] # why generation stopped
|
|
142
142
|
ansa.raw["usage"] # prompt and completion token counts
|
|
143
143
|
```
|
|
@@ -146,13 +146,82 @@ ansa.raw["usage"] # prompt and completion token counts
|
|
|
146
146
|
|
|
147
147
|
---
|
|
148
148
|
|
|
149
|
+
## Embeddings
|
|
150
|
+
|
|
151
|
+
Generate embeddings with `client.embed()`. The shape of what you get back mirrors the shape of what you send in: a single string returns one flat vector, a list returns a list of vectors.
|
|
152
|
+
|
|
153
|
+
```python
|
|
154
|
+
from confamnode import ConfamNode
|
|
155
|
+
|
|
156
|
+
client = ConfamNode(api_key="confam-xxx")
|
|
157
|
+
|
|
158
|
+
# Single string in → one flat vector out
|
|
159
|
+
result = client.embed(
|
|
160
|
+
model="confam-embed-local",
|
|
161
|
+
input="How you dey?"
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
print(result.embeddings) # [0.123, -0.456, ...]
|
|
165
|
+
print(f"Cost: ₦{result.cost.naira:.6f}")
|
|
166
|
+
print(f"Tokens: {result.usage.total_tokens}")
|
|
167
|
+
|
|
168
|
+
# List in → list of vectors out, always — even with one item
|
|
169
|
+
result = client.embed(
|
|
170
|
+
model="confam-embed-local",
|
|
171
|
+
input=["How you dey?", "Wetin dey happen?"]
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
print(len(result.embeddings)) # 2
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
`confam-embed-local` is currently the only available embedding model. More coming soon.
|
|
178
|
+
|
|
179
|
+
### Task types
|
|
180
|
+
|
|
181
|
+
Some embedding models expect a task-type hint (e.g. `"query"` vs. `"document"`) to produce better embeddings for retrieval use cases. This defaults sensibly server-side if omitted — only pass it if you need a specific one:
|
|
182
|
+
|
|
183
|
+
```python
|
|
184
|
+
result = client.embed(
|
|
185
|
+
model="confam-embed-local",
|
|
186
|
+
input="find me a good jollof rice recipe",
|
|
187
|
+
task_type="query"
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
print(result.task_type_applied) # confirms what actually happened for this call
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
`task_type_applied` is `None` when the requested model doesn't use task-type hints at all — the field itself tells you whether it had any effect, no separate capability lookup needed.
|
|
194
|
+
|
|
195
|
+
### The EmbeddingResult Object
|
|
196
|
+
|
|
197
|
+
```python
|
|
198
|
+
result = client.embed(model="confam-embed-local", input="How you dey?")
|
|
199
|
+
|
|
200
|
+
result.embeddings # vector(s) — shape matches your input shape
|
|
201
|
+
result.model # model that served the request
|
|
202
|
+
result.usage.prompt_tokens # input tokens used
|
|
203
|
+
result.usage.total_tokens # total tokens used (embeddings have no output tokens to bill)
|
|
204
|
+
result.cost.naira # total cost in Naira
|
|
205
|
+
result.cost.naira_input # input cost in Naira
|
|
206
|
+
result.is_local # True — runs on Nigerian hardware
|
|
207
|
+
result.is_ngn_data_residency # True — data never leaves Nigeria
|
|
208
|
+
result.task_type_applied # which task-type hint was actually applied, if any
|
|
209
|
+
result.id # unique request ID
|
|
210
|
+
result.raw # full raw response
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### Batch limits (`confam-embed-local`)
|
|
214
|
+
|
|
215
|
+
Self-hosted embedding requests to `confam-embed-local` are served in batches of up to 4 concurrent requests at a time. If you're embedding a large number of documents, being aware of this batching ceiling will give you a more predictable sense of total time than assuming unlimited concurrency.
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
149
219
|
## Models
|
|
150
220
|
|
|
151
221
|
### Free Tier
|
|
152
222
|
|
|
153
223
|
| Model | Description | Modality | Price |
|
|
154
224
|
|---|---|---|---|
|
|
155
|
-
| `confam-lite` | Light text and general chat | Text-to-Text | Free |
|
|
156
225
|
| `confam-speed` | Fast, high quality responses | Image-Text-to-Text | Free |
|
|
157
226
|
| `confam-reasoning` | Standard reasoning and analysis | Text-to-Text | Free |
|
|
158
227
|
|
|
@@ -168,9 +237,18 @@ ansa.raw["usage"] # prompt and completion token counts
|
|
|
168
237
|
|
|
169
238
|
| Model | Description | Modality | Input ₦/1M | Output ₦/1M | Input ₦/1K | Output ₦/1K |
|
|
170
239
|
|---|---|---|---|---|---|---|
|
|
171
|
-
| `confam-
|
|
240
|
+
| `confam-health` | Local model, health information use cases — data stays in Nigeria | Image-Text-to-Text | ₦500 | ₦1,500 | ₦0.500 | ₦1.500 |
|
|
241
|
+
| `confam-local` | Local model, general-purpose — data stays in Nigeria | Text-to-Text | ₦500 | ₦1,500 | ₦0.500 | ₦1.500 |
|
|
242
|
+
|
|
243
|
+
### Embeddings
|
|
244
|
+
|
|
245
|
+
| Model | Description | Where it runs |
|
|
246
|
+
|---|---|---|
|
|
247
|
+
| `confam-embed-local` | Self-hosted text embeddings — data stays in Nigeria | Local |
|
|
248
|
+
|
|
249
|
+
More embedding models coming soon.
|
|
172
250
|
|
|
173
|
-
|
|
251
|
+
Local models run entirely on Nigerian hardware. Data never transmitted abroad.
|
|
174
252
|
Ideal for banks, fintechs, hospitals, law firms, and government agencies.
|
|
175
253
|
|
|
176
254
|
More models coming soon. Contact [hello@confamnode.com](mailto:hello@confamnode.com) for early access.
|
|
@@ -197,7 +275,7 @@ Rate limits vary by plan. Contact [hello@confamnode.com](mailto:hello@confamnode
|
|
|
197
275
|
|
|
198
276
|
- **Free tier** — shared capacity with lower limits
|
|
199
277
|
- **Paid tier** — dedicated higher limits
|
|
200
|
-
- **confam-
|
|
278
|
+
- **Local models** (`confam-health`, `confam-local`) — deployed on dedicated Nigerian hardware (Jetson AGX Orin), with a shared concurrency cap across both models. Under heavy simultaneous load, requests queue briefly rather than failing outright.
|
|
201
279
|
|
|
202
280
|
---
|
|
203
281
|
|
|
@@ -234,7 +312,7 @@ ansa = client.gist(
|
|
|
234
312
|
|
|
235
313
|
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
314
|
|
|
237
|
-
Pass `cache=True` to
|
|
315
|
+
Pass `cache=True` to let the gateway serve and store a cached response — useful for idempotent lookups:
|
|
238
316
|
|
|
239
317
|
```python
|
|
240
318
|
# Default — caching off, fresh response every call
|
|
@@ -243,8 +321,8 @@ ansa = client.gist(
|
|
|
243
321
|
messages="How you dey?"
|
|
244
322
|
)
|
|
245
323
|
|
|
246
|
-
# Enable caching —
|
|
247
|
-
# and store this
|
|
324
|
+
# Enable caching — the gateway may return a stored response for a
|
|
325
|
+
# matching request, and store this one for next time
|
|
248
326
|
ansa = client.gist(
|
|
249
327
|
model="confam-speed",
|
|
250
328
|
messages="How you dey?",
|
|
@@ -252,7 +330,9 @@ ansa = client.gist(
|
|
|
252
330
|
)
|
|
253
331
|
```
|
|
254
332
|
|
|
255
|
-
A cache hit
|
|
333
|
+
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.
|
|
334
|
+
|
|
335
|
+
> **Note:** a cache hit still bills the full Naira cost of the request — you're paying for inference value, not raw compute. Caching saves time, not money.
|
|
256
336
|
|
|
257
337
|
---
|
|
258
338
|
|
|
@@ -285,6 +365,18 @@ print(ansa.reasoning) # full thinking trace
|
|
|
285
365
|
print(ansa.text) # final analysis
|
|
286
366
|
```
|
|
287
367
|
|
|
368
|
+
### Controlling reasoning mode (`confam-local`)
|
|
369
|
+
|
|
370
|
+
`confam-local` reasons in `<think>` blocks by default. To disable this per-request, pass `enable_thinking` nested under `chat_template_kwargs` via `extra_body` — this must be nested this way rather than passed as a top-level kwarg, or it will be silently ignored:
|
|
371
|
+
|
|
372
|
+
```python
|
|
373
|
+
result = client.gist(
|
|
374
|
+
model="confam-local",
|
|
375
|
+
messages="How you dey?",
|
|
376
|
+
extra_body={"chat_template_kwargs": {"enable_thinking": False}}
|
|
377
|
+
)
|
|
378
|
+
```
|
|
379
|
+
|
|
288
380
|
---
|
|
289
381
|
|
|
290
382
|
## RAG (Retrieval-Augmented Generation)
|
|
@@ -297,6 +389,8 @@ print(ansa.text) # final analysis
|
|
|
297
389
|
| `confam-code` | Code search, documentation RAG | 1M tokens |
|
|
298
390
|
| `confam-deep-reasoning` | Complex RAG, multi-hop reasoning | 1M tokens |
|
|
299
391
|
|
|
392
|
+
`confam-embed-local` pairs well with any of the above for retrieval — see [Embeddings](#embeddings).
|
|
393
|
+
|
|
300
394
|
---
|
|
301
395
|
|
|
302
396
|
## Data Residency
|
|
@@ -305,7 +399,7 @@ Nigerian businesses handling sensitive data can use local models that run entire
|
|
|
305
399
|
|
|
306
400
|
```python
|
|
307
401
|
ansa = client.gist(
|
|
308
|
-
model="confam-
|
|
402
|
+
model="confam-local",
|
|
309
403
|
messages="Analyse this sensitive document..."
|
|
310
404
|
)
|
|
311
405
|
|
|
@@ -314,6 +408,8 @@ print(ansa.is_ngn_data_residency) # True — data never leaves Nigeria
|
|
|
314
408
|
print(ansa.text)
|
|
315
409
|
```
|
|
316
410
|
|
|
411
|
+
For health information use cases, `confam-health` offers the same data-residency guarantee on a model tuned for that domain.
|
|
412
|
+
|
|
317
413
|
**Ideal for:**
|
|
318
414
|
- Nigerian banks and fintechs
|
|
319
415
|
- Healthcare companies
|
|
@@ -356,6 +452,9 @@ from confamnode import (
|
|
|
356
452
|
ConfamAuthError,
|
|
357
453
|
ConfamRateLimitError,
|
|
358
454
|
ConfamModelError,
|
|
455
|
+
ConfamInsufficientBalanceError,
|
|
456
|
+
ConfamProviderError,
|
|
457
|
+
ConfamRequestError,
|
|
359
458
|
ConfamNodeError
|
|
360
459
|
)
|
|
361
460
|
|
|
@@ -366,14 +465,22 @@ try:
|
|
|
366
465
|
)
|
|
367
466
|
except ConfamAuthError:
|
|
368
467
|
print("Check your API key")
|
|
468
|
+
except ConfamInsufficientBalanceError:
|
|
469
|
+
print("Wallet balance too low. Top up to continue.")
|
|
369
470
|
except ConfamRateLimitError:
|
|
370
471
|
print("You don reach your limit. Contact hello@confamnode.com")
|
|
371
472
|
except ConfamModelError:
|
|
372
473
|
print("Invalid model name")
|
|
474
|
+
except ConfamProviderError:
|
|
475
|
+
print("The AI provider is temporarily unavailable. Try again shortly.")
|
|
476
|
+
except ConfamRequestError:
|
|
477
|
+
print("Request could not be processed — check your parameters")
|
|
373
478
|
except ConfamNodeError as e:
|
|
374
479
|
print(f"Something went wrong: {e}")
|
|
375
480
|
```
|
|
376
481
|
|
|
482
|
+
All specific exceptions above are subclasses of `ConfamNodeError` — catching that alone is enough if you don't need to distinguish error types.
|
|
483
|
+
|
|
377
484
|
---
|
|
378
485
|
|
|
379
486
|
## Private AI Deployment
|
|
@@ -1,25 +1,34 @@
|
|
|
1
|
-
|
|
1
|
+
|
|
2
|
+
from confamnode import models
|
|
2
3
|
from confamnode.exceptions import (
|
|
3
4
|
ConfamNodeError,
|
|
4
5
|
ConfamAuthError,
|
|
5
|
-
ConfamRateLimitError,
|
|
6
6
|
ConfamModelError,
|
|
7
|
+
ConfamRequestError,
|
|
8
|
+
ConfamProviderError,
|
|
9
|
+
ConfamRateLimitError,
|
|
10
|
+
ConfamInsufficientBalanceError,
|
|
7
11
|
)
|
|
8
|
-
from confamnode.
|
|
9
|
-
from confamnode import
|
|
12
|
+
from confamnode.client import ConfamNode, ConfamStream
|
|
13
|
+
from confamnode.ansa import Ansa, Usage, Cost, EmbeddingUsage, EmbeddingResult
|
|
10
14
|
|
|
11
|
-
__version__ = "0.
|
|
15
|
+
__version__ = "0.3.0"
|
|
12
16
|
|
|
13
17
|
__all__ = [
|
|
14
18
|
"ConfamNode",
|
|
15
19
|
"ConfamStream",
|
|
16
|
-
"ConfamNodeError",
|
|
17
20
|
"ConfamAuthError",
|
|
18
|
-
"
|
|
21
|
+
"ConfamNodeError",
|
|
19
22
|
"ConfamModelError",
|
|
23
|
+
"ConfamRequestError",
|
|
24
|
+
"ConfamProviderError",
|
|
25
|
+
"ConfamRateLimitError",
|
|
26
|
+
"ConfamInsufficientBalanceError",
|
|
20
27
|
"Ansa",
|
|
21
28
|
"Usage",
|
|
22
29
|
"Cost",
|
|
30
|
+
"EmbeddingUsage",
|
|
31
|
+
"EmbeddingResult",
|
|
23
32
|
"models",
|
|
24
33
|
"__version__"
|
|
25
34
|
]
|
|
@@ -51,4 +51,23 @@ class StreamChoice:
|
|
|
51
51
|
class StreamChunk:
|
|
52
52
|
id: str = ""
|
|
53
53
|
model: str = ""
|
|
54
|
-
choices: list = field(default_factory=list)
|
|
54
|
+
choices: list = field(default_factory=list)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass
|
|
58
|
+
class EmbeddingUsage:
|
|
59
|
+
prompt_tokens: int
|
|
60
|
+
total_tokens: int
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass
|
|
64
|
+
class EmbeddingResult:
|
|
65
|
+
embeddings: list
|
|
66
|
+
model: str
|
|
67
|
+
usage: EmbeddingUsage
|
|
68
|
+
cost: Cost
|
|
69
|
+
raw: object
|
|
70
|
+
id: str = field(default_factory=lambda: f"confam-{uuid.uuid4()}")
|
|
71
|
+
is_local: bool = False
|
|
72
|
+
is_ngn_data_residency: bool = False
|
|
73
|
+
task_type_applied: str | None = None
|
|
@@ -4,11 +4,11 @@ import httpx
|
|
|
4
4
|
|
|
5
5
|
from typing import Union, List, Dict
|
|
6
6
|
|
|
7
|
-
from confamnode.utils import extract_error
|
|
8
7
|
from confamnode.builders import parse_chunk
|
|
9
|
-
from confamnode.ansa import Ansa, Usage, Cost
|
|
10
8
|
from confamnode.registry import VALID_MODELS
|
|
9
|
+
from confamnode.utils import raise_for_status
|
|
11
10
|
from confamnode.exceptions import ConfamAuthError, ConfamModelError
|
|
11
|
+
from confamnode.ansa import Ansa, Usage, Cost, EmbeddingResult, EmbeddingUsage
|
|
12
12
|
from confamnode.config import DEFAULT_BASE_URL, DEFAULT_TIMEOUT, DEFAULT_CONNECT_TIMEOUT
|
|
13
13
|
|
|
14
14
|
|
|
@@ -61,20 +61,17 @@ class ConfamNode:
|
|
|
61
61
|
else:
|
|
62
62
|
body["system"] = system # None or custom string
|
|
63
63
|
|
|
64
|
-
# Caching is
|
|
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.
|
|
68
|
-
# write to the cache (idempotent lookups, cost savings on repeats).
|
|
67
|
+
# same prompt expecting varied output.
|
|
69
68
|
#
|
|
70
|
-
#
|
|
71
|
-
#
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
|
@@ -97,11 +94,11 @@ class ConfamNode:
|
|
|
97
94
|
|
|
98
95
|
if stream_response.status_code >= 400:
|
|
99
96
|
stream_response.read()
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
97
|
+
try:
|
|
98
|
+
raise_for_status(stream_response)
|
|
99
|
+
finally:
|
|
100
|
+
stream_response.close()
|
|
101
|
+
http_client.close()
|
|
105
102
|
|
|
106
103
|
return ConfamStream(stream_response, http_client, model)
|
|
107
104
|
|
|
@@ -115,9 +112,7 @@ class ConfamNode:
|
|
|
115
112
|
timeout=httpx.Timeout(DEFAULT_TIMEOUT, connect=DEFAULT_CONNECT_TIMEOUT)
|
|
116
113
|
)
|
|
117
114
|
|
|
118
|
-
|
|
119
|
-
error = extract_error(response)
|
|
120
|
-
raise Exception(f"ConfamNode error {response.status_code}: {error}")
|
|
115
|
+
raise_for_status(response)
|
|
121
116
|
|
|
122
117
|
data = response.json()
|
|
123
118
|
msg = data["choices"][0]["message"]
|
|
@@ -153,6 +148,71 @@ class ConfamNode:
|
|
|
153
148
|
is_local=confam.get("is_local", False),
|
|
154
149
|
is_ngn_data_residency=confam.get("is_ngn_data_residency", False),
|
|
155
150
|
)
|
|
151
|
+
|
|
152
|
+
def embed(
|
|
153
|
+
self,
|
|
154
|
+
model: str,
|
|
155
|
+
input: Union[str, List[str]],
|
|
156
|
+
task_type: str | None = None,
|
|
157
|
+
**kwargs
|
|
158
|
+
) -> EmbeddingResult:
|
|
159
|
+
if model not in VALID_MODELS:
|
|
160
|
+
raise ConfamModelError(model)
|
|
161
|
+
|
|
162
|
+
body = {
|
|
163
|
+
"model": model,
|
|
164
|
+
"input": input,
|
|
165
|
+
**kwargs
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
# task_type is optional here — the API defaults to "document"
|
|
169
|
+
# server-side if omitted, so the SDK doesn't duplicate that
|
|
170
|
+
# default; only send it if the caller actually specified one.
|
|
171
|
+
if task_type is not None:
|
|
172
|
+
body["task_type"] = task_type
|
|
173
|
+
|
|
174
|
+
response = httpx.post(
|
|
175
|
+
f"{self.base_url}/embeddings",
|
|
176
|
+
headers={
|
|
177
|
+
"Authorization": f"Bearer {self.api_key}",
|
|
178
|
+
"Content-Type": "application/json",
|
|
179
|
+
},
|
|
180
|
+
json=body,
|
|
181
|
+
timeout=httpx.Timeout(DEFAULT_TIMEOUT, connect=DEFAULT_CONNECT_TIMEOUT)
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
raise_for_status(response)
|
|
185
|
+
|
|
186
|
+
data = response.json()
|
|
187
|
+
usage_data = data.get("usage", {})
|
|
188
|
+
confam = data.get("confam", {})
|
|
189
|
+
cost_data = confam.get("cost", {})
|
|
190
|
+
|
|
191
|
+
raw_embeddings = [item["embedding"] for item in data.get("data", [])]
|
|
192
|
+
|
|
193
|
+
# Convenience unwrapping: a caller who passed a single string
|
|
194
|
+
# expects one vector back, not a one-item list. A caller who
|
|
195
|
+
# passed a list always gets a list back, even with one item —
|
|
196
|
+
# input shape determines output shape, consistently.
|
|
197
|
+
embeddings = raw_embeddings[0] if isinstance(input, str) else raw_embeddings
|
|
198
|
+
|
|
199
|
+
return EmbeddingResult(
|
|
200
|
+
id=confam.get("request_id", ""),
|
|
201
|
+
embeddings=embeddings,
|
|
202
|
+
model=data.get("model", model),
|
|
203
|
+
usage=EmbeddingUsage(
|
|
204
|
+
prompt_tokens=usage_data.get("prompt_tokens", 0),
|
|
205
|
+
total_tokens=usage_data.get("total_tokens", 0),
|
|
206
|
+
),
|
|
207
|
+
cost=Cost(
|
|
208
|
+
naira=cost_data.get("naira", 0.0),
|
|
209
|
+
naira_input=cost_data.get("naira_input", 0.0),
|
|
210
|
+
),
|
|
211
|
+
raw=data,
|
|
212
|
+
is_local=confam.get("is_local", False),
|
|
213
|
+
is_ngn_data_residency=confam.get("is_ngn_data_residency", False),
|
|
214
|
+
task_type_applied=confam.get("task_type_applied"),
|
|
215
|
+
)
|
|
156
216
|
|
|
157
217
|
|
|
158
218
|
class ConfamStream:
|