confamnode 0.2.6__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.6 → confamnode-0.3.0}/.gitignore +1 -0
  2. {confamnode-0.2.6 → confamnode-0.3.0}/PKG-INFO +114 -7
  3. {confamnode-0.2.6 → confamnode-0.3.0}/README.md +113 -6
  4. {confamnode-0.2.6 → confamnode-0.3.0}/confamnode/__init__.py +16 -7
  5. {confamnode-0.2.6 → confamnode-0.3.0}/confamnode/ansa.py +20 -1
  6. {confamnode-0.2.6 → confamnode-0.3.0}/confamnode/client.py +73 -10
  7. {confamnode-0.2.6 → confamnode-0.3.0}/confamnode/exceptions.py +18 -0
  8. {confamnode-0.2.6 → confamnode-0.3.0}/confamnode/models.py +8 -2
  9. {confamnode-0.2.6 → confamnode-0.3.0}/confamnode/registry.py +3 -2
  10. {confamnode-0.2.6 → confamnode-0.3.0}/confamnode/utils.py +43 -1
  11. {confamnode-0.2.6 → confamnode-0.3.0}/pyproject.toml +1 -1
  12. {confamnode-0.2.6 → confamnode-0.3.0}/tests/test_ansa.py +137 -2
  13. confamnode-0.3.0/tests/test_embed.py +258 -0
  14. confamnode-0.3.0/tests/test_exceptions.py +63 -0
  15. {confamnode-0.2.6 → confamnode-0.3.0}/tests/test_gist.py +2 -2
  16. {confamnode-0.2.6 → confamnode-0.3.0}/tests/test_models.py +20 -9
  17. {confamnode-0.2.6 → confamnode-0.3.0}/tests/test_stream.py +2 -2
  18. confamnode-0.3.0/tests/test_utils.py +179 -0
  19. {confamnode-0.2.6 → confamnode-0.3.0}/uv.lock +1 -1
  20. confamnode-0.2.6/tests/test_exceptions.py +0 -20
  21. confamnode-0.2.6/tests/test_utils.py +0 -67
  22. {confamnode-0.2.6 → confamnode-0.3.0}/.python-version +0 -0
  23. {confamnode-0.2.6 → confamnode-0.3.0}/LICENSE +0 -0
  24. {confamnode-0.2.6 → confamnode-0.3.0}/confamnode/builders.py +0 -0
  25. {confamnode-0.2.6 → confamnode-0.3.0}/confamnode/config.py +0 -0
  26. {confamnode-0.2.6 → confamnode-0.3.0}/tests/__init__.py +0 -0
  27. {confamnode-0.2.6 → confamnode-0.3.0}/tests/test_cache_flag.py +0 -0
  28. {confamnode-0.2.6 → confamnode-0.3.0}/tests/test_client.py +0 -0
  29. {confamnode-0.2.6 → confamnode-0.3.0}/tests/test_client_errors.py +0 -0
  30. {confamnode-0.2.6 → 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.6
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
 
@@ -278,6 +356,8 @@ ansa = client.gist(
278
356
 
279
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.
280
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.
360
+
281
361
  ---
282
362
 
283
363
  ## Reasoning Models
@@ -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
 
@@ -254,6 +332,8 @@ ansa = client.gist(
254
332
 
255
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.
256
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.
336
+
257
337
  ---
258
338
 
259
339
  ## Reasoning Models
@@ -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.6"
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
 
@@ -94,11 +94,11 @@ class ConfamNode:
94
94
 
95
95
  if stream_response.status_code >= 400:
96
96
  stream_response.read()
97
- status = stream_response.status_code
98
- error = extract_error(stream_response)
99
- stream_response.close()
100
- http_client.close()
101
- 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()
102
102
 
103
103
  return ConfamStream(stream_response, http_client, model)
104
104
 
@@ -112,9 +112,7 @@ class ConfamNode:
112
112
  timeout=httpx.Timeout(DEFAULT_TIMEOUT, connect=DEFAULT_CONNECT_TIMEOUT)
113
113
  )
114
114
 
115
- if response.status_code >= 400:
116
- error = extract_error(response)
117
- raise Exception(f"ConfamNode error {response.status_code}: {error}")
115
+ raise_for_status(response)
118
116
 
119
117
  data = response.json()
120
118
  msg = data["choices"][0]["message"]
@@ -150,6 +148,71 @@ class ConfamNode:
150
148
  is_local=confam.get("is_local", False),
151
149
  is_ngn_data_residency=confam.get("is_ngn_data_residency", False),
152
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
+ )
153
216
 
154
217
 
155
218
  class ConfamStream:
@@ -20,3 +20,21 @@ class ConfamModelError(ConfamNodeError):
20
20
  """Raised when an invalid model name is used"""
21
21
  def __init__(self, model_name: str):
22
22
  super().__init__(f"Invalid model name: {model_name}")
23
+
24
+
25
+ class ConfamInsufficientBalanceError(ConfamNodeError):
26
+ """Raised when the wallet balance is too low for requested model (HTTP 402)"""
27
+ def __init__(self, message: str = "Insufficient wallet balance. Top up to continue using paid models."):
28
+ super().__init__(message)
29
+
30
+
31
+ class ConfamProviderError(ConfamNodeError):
32
+ """Raised when the upstream provider is unreachable or fails (HTTP 502/504)"""
33
+ def __init__(self, message: str = "The AI provider is temporarily unavailable. Please try again."):
34
+ super().__init__(message)
35
+
36
+
37
+ class ConfamRequestError(ConfamNodeError):
38
+ """Raised for other client-side request errors (HTTP 400/422/other 4xx)"""
39
+ def __init__(self, message: str = "Your request could not be processed."):
40
+ super().__init__(message)
@@ -19,7 +19,13 @@ TTS = "confam-tts"
19
19
  # =========================================================================
20
20
  # 🛠️ LOCAL PAID (NDPA Compliant)
21
21
  # =========================================================================
22
- NANO = "confam-nano"
22
+ # NOTE: `confam-nano` was retired and replaced by two models, per the
23
+ # July 2026 Jetson AGX Orin work — see confamnode-changelog.md. This SDK
24
+ # copy of models.py was missed during that retirement (a separate file
25
+ # from the API's own app/core/models.py, despite the shared name).
26
+ HEALTH = "confam-health"
27
+ LOCAL = "confam-local"
28
+ # NANO = "confam-nano" # RETIRED - remove once confirmed no callers reference it
23
29
 
24
30
  # =========================================================================
25
31
  # 📦 FREE EMBEDDINGS
@@ -36,7 +42,7 @@ EMBED_MULTIMODAL_PRO = "confam-embed-multimodal-pro"
36
42
  EMBED_MULTILINGUAL = "confam-embed-multilingual"
37
43
  EMBED_SMALL = "confam-embed-small"
38
44
  EMBED_0_6B = "confam-embed-0.6b"
39
- EMBED_TEXT_LOCAL = "confam-embed-text-local"
45
+ EMBED_LOCAL = "confam-embed-local"
40
46
 
41
47
  # =========================================================================
42
48
  # 🔍 RERANK
@@ -39,8 +39,9 @@ CLOUD_MODELS = {
39
39
  # Local Models — inference on Nigerian hardware, Nigeria data residency
40
40
  # =========================================================================
41
41
  LOCAL_MODELS = {
42
- models.NANO,
43
- models.EMBED_TEXT_LOCAL,
42
+ models.HEALTH,
43
+ models.LOCAL,
44
+ models.EMBED_LOCAL,
44
45
  }
45
46
 
46
47
  # =========================================================================