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.
Files changed (30) hide show
  1. {confamnode-0.2.5 → confamnode-0.3.0}/.gitignore +1 -0
  2. {confamnode-0.2.5 → confamnode-0.3.0}/PKG-INFO +118 -11
  3. {confamnode-0.2.5 → confamnode-0.3.0}/README.md +117 -10
  4. {confamnode-0.2.5 → confamnode-0.3.0}/confamnode/__init__.py +16 -7
  5. {confamnode-0.2.5 → confamnode-0.3.0}/confamnode/ansa.py +20 -1
  6. {confamnode-0.2.5 → confamnode-0.3.0}/confamnode/client.py +82 -22
  7. {confamnode-0.2.5 → confamnode-0.3.0}/confamnode/exceptions.py +18 -0
  8. {confamnode-0.2.5 → confamnode-0.3.0}/confamnode/models.py +8 -2
  9. {confamnode-0.2.5 → confamnode-0.3.0}/confamnode/registry.py +3 -2
  10. {confamnode-0.2.5 → confamnode-0.3.0}/confamnode/utils.py +43 -1
  11. {confamnode-0.2.5 → confamnode-0.3.0}/pyproject.toml +1 -1
  12. {confamnode-0.2.5 → confamnode-0.3.0}/tests/test_ansa.py +137 -2
  13. {confamnode-0.2.5 → confamnode-0.3.0}/tests/test_cache_flag.py +5 -13
  14. confamnode-0.3.0/tests/test_embed.py +258 -0
  15. confamnode-0.3.0/tests/test_exceptions.py +63 -0
  16. {confamnode-0.2.5 → confamnode-0.3.0}/tests/test_gist.py +2 -2
  17. {confamnode-0.2.5 → confamnode-0.3.0}/tests/test_models.py +20 -9
  18. {confamnode-0.2.5 → confamnode-0.3.0}/tests/test_stream.py +2 -2
  19. confamnode-0.3.0/tests/test_utils.py +179 -0
  20. {confamnode-0.2.5 → confamnode-0.3.0}/uv.lock +1 -1
  21. confamnode-0.2.5/tests/test_exceptions.py +0 -20
  22. confamnode-0.2.5/tests/test_utils.py +0 -67
  23. {confamnode-0.2.5 → confamnode-0.3.0}/.python-version +0 -0
  24. {confamnode-0.2.5 → confamnode-0.3.0}/LICENSE +0 -0
  25. {confamnode-0.2.5 → confamnode-0.3.0}/confamnode/builders.py +0 -0
  26. {confamnode-0.2.5 → confamnode-0.3.0}/confamnode/config.py +0 -0
  27. {confamnode-0.2.5 → confamnode-0.3.0}/tests/__init__.py +0 -0
  28. {confamnode-0.2.5 → confamnode-0.3.0}/tests/test_client.py +0 -0
  29. {confamnode-0.2.5 → confamnode-0.3.0}/tests/test_client_errors.py +0 -0
  30. {confamnode-0.2.5 → confamnode-0.3.0}/tests/test_init.py +0 -0
@@ -20,6 +20,7 @@ dist/
20
20
  .pytest_cache/
21
21
 
22
22
  # Test files
23
+ live_tests/
23
24
  chat_test.py
24
25
  stream_test.py
25
26
  exchange_rate_calc.py
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: confamnode
3
- Version: 0.2.5
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"] # provider response 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-nano` | Local model — data stays in Nigeria | Image-Text-to-Text | ₦500 | ₦1,500 | ₦0.500 | ₦1.500 |
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
- Runs entirely on Nigerian hardware. Data never transmitted abroad.
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-nano** — limited capacity, queue-based
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 read from and write to the cache — useful for idempotent lookups or to save cost on repeated queries:
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 — use a stored response when the request matches,
271
- # and store this response for next time
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 is typically returned near-instantly and at little or no token cost — a quick way to confirm caching is active.
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-nano",
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"] # provider response 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-nano` | Local model — data stays in Nigeria | Image-Text-to-Text | ₦500 | ₦1,500 | ₦0.500 | ₦1.500 |
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
- Runs entirely on Nigerian hardware. Data never transmitted abroad.
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-nano** — limited capacity, queue-based
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 read from and write to the cache — useful for idempotent lookups or to save cost on repeated queries:
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 — use a stored response when the request matches,
247
- # and store this response for next time
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 is typically returned near-instantly and at little or no token cost — a quick way to confirm caching is active.
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-nano",
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
- from confamnode.client import ConfamNode, ConfamStream
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.ansa import Ansa, Usage, Cost
9
- from confamnode import models
12
+ from confamnode.client import ConfamNode, ConfamStream
13
+ from confamnode.ansa import Ansa, Usage, Cost, EmbeddingUsage, EmbeddingResult
10
14
 
11
- __version__ = "0.2.5"
15
+ __version__ = "0.3.0"
12
16
 
13
17
  __all__ = [
14
18
  "ConfamNode",
15
19
  "ConfamStream",
16
- "ConfamNodeError",
17
20
  "ConfamAuthError",
18
- "ConfamRateLimitError",
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 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
@@ -97,11 +94,11 @@ class ConfamNode:
97
94
 
98
95
  if stream_response.status_code >= 400:
99
96
  stream_response.read()
100
- status = stream_response.status_code
101
- error = extract_error(stream_response)
102
- stream_response.close()
103
- http_client.close()
104
- raise Exception(f"ConfamNode error {status}: {error}")
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
- if response.status_code >= 400:
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: