confamnode 0.1.0__tar.gz → 0.1.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {confamnode-0.1.0 → confamnode-0.1.1}/.gitignore +8 -0
- {confamnode-0.1.0 → confamnode-0.1.1}/PKG-INFO +46 -12
- {confamnode-0.1.0 → confamnode-0.1.1}/README.md +44 -10
- {confamnode-0.1.0 → confamnode-0.1.1}/confamnode/__init__.py +7 -2
- confamnode-0.1.1/confamnode/ansa.py +31 -0
- confamnode-0.1.1/confamnode/client.py +187 -0
- confamnode-0.1.1/confamnode/models.py +45 -0
- {confamnode-0.1.0 → confamnode-0.1.1}/pyproject.toml +3 -2
- confamnode-0.1.1/tests/test_ansa.py +231 -0
- {confamnode-0.1.0 → confamnode-0.1.1}/tests/test_client.py +1 -0
- {confamnode-0.1.0 → confamnode-0.1.1}/tests/test_gist.py +100 -9
- {confamnode-0.1.0 → confamnode-0.1.1}/tests/test_init.py +6 -1
- confamnode-0.1.1/tests/test_models.py +84 -0
- {confamnode-0.1.0 → confamnode-0.1.1}/uv.lock +12 -1
- confamnode-0.1.0/confamnode/client.py +0 -63
- confamnode-0.1.0/confamnode/models.py +0 -22
- confamnode-0.1.0/main.py +0 -6
- confamnode-0.1.0/tests/test_models.py +0 -55
- confamnode-0.1.0/tests/test_stream.py +0 -63
- {confamnode-0.1.0 → confamnode-0.1.1}/.python-version +0 -0
- {confamnode-0.1.0 → confamnode-0.1.1}/LICENSE +0 -0
- {confamnode-0.1.0 → confamnode-0.1.1}/confamnode/exceptions.py +0 -0
- {confamnode-0.1.0 → confamnode-0.1.1}/tests/__init__.py +0 -0
- {confamnode-0.1.0 → confamnode-0.1.1}/tests/test_exceptions.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: confamnode
|
|
3
|
-
Version: 0.1.
|
|
4
|
-
Summary: The Nigerian AI inference
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: The Nigerian AI inference gateway — access powerful AI models
|
|
5
5
|
Project-URL: Repository, https://github.com/joteqthefirst/confamnode-sdk
|
|
6
6
|
Project-URL: Bug Tracker, https://github.com/joteqthefirst/confamnode-sdk/issues
|
|
7
7
|
Author-email: JoTeq the First <joteqthefirst@gmail.com>
|
|
@@ -24,7 +24,7 @@ Description-Content-Type: text/markdown
|
|
|
24
24
|
|
|
25
25
|
# ConfamNode
|
|
26
26
|
|
|
27
|
-
The Nigerian AI inference
|
|
27
|
+
The Nigerian AI inference gateway — access powerful AI models.
|
|
28
28
|
|
|
29
29
|
Built by **JoTeq the First**
|
|
30
30
|
|
|
@@ -32,10 +32,44 @@ Built by **JoTeq the First**
|
|
|
32
32
|
|
|
33
33
|
## Installation
|
|
34
34
|
|
|
35
|
+
### Using pip
|
|
35
36
|
```bash
|
|
36
37
|
pip install confamnode
|
|
37
38
|
```
|
|
38
39
|
|
|
40
|
+
### Using uv
|
|
41
|
+
```bash
|
|
42
|
+
uv add confamnode
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Using virtualenv
|
|
46
|
+
```bash
|
|
47
|
+
# Create virtual environment
|
|
48
|
+
python -m venv .venv
|
|
49
|
+
|
|
50
|
+
# Activate — Linux/Mac
|
|
51
|
+
source .venv/bin/activate
|
|
52
|
+
|
|
53
|
+
# Activate — Windows
|
|
54
|
+
.venv\Scripts\activate
|
|
55
|
+
|
|
56
|
+
# Install
|
|
57
|
+
pip install confamnode
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Using uv with virtual environment
|
|
61
|
+
```bash
|
|
62
|
+
# Create project with uv
|
|
63
|
+
uv init my-project
|
|
64
|
+
cd my-project
|
|
65
|
+
|
|
66
|
+
# Add confamnode
|
|
67
|
+
uv add confamnode
|
|
68
|
+
|
|
69
|
+
# Run your script
|
|
70
|
+
uv run python main.py
|
|
71
|
+
```
|
|
72
|
+
|
|
39
73
|
---
|
|
40
74
|
|
|
41
75
|
## Quick Start
|
|
@@ -45,12 +79,12 @@ from confamnode import ConfamNode
|
|
|
45
79
|
|
|
46
80
|
client = ConfamNode(api_key="confam-sk-xxx")
|
|
47
81
|
|
|
48
|
-
|
|
82
|
+
ansa = client.gist(
|
|
49
83
|
model="confam-speed",
|
|
50
84
|
messages="How far?"
|
|
51
85
|
)
|
|
52
86
|
|
|
53
|
-
print(
|
|
87
|
+
print(ansa.choices[0].message.content)
|
|
54
88
|
```
|
|
55
89
|
|
|
56
90
|
---
|
|
@@ -58,12 +92,12 @@ print(response.choices[0].message.content)
|
|
|
58
92
|
## Streaming
|
|
59
93
|
|
|
60
94
|
```python
|
|
61
|
-
for
|
|
95
|
+
for yarn in client.gist(
|
|
62
96
|
model="confam-speed",
|
|
63
97
|
messages="How far?",
|
|
64
98
|
stream=True
|
|
65
99
|
):
|
|
66
|
-
print(
|
|
100
|
+
print(yarn.choices[0].delta.content, end="")
|
|
67
101
|
```
|
|
68
102
|
|
|
69
103
|
---
|
|
@@ -98,23 +132,23 @@ client = ConfamNode()
|
|
|
98
132
|
from confamnode import ConfamAuthError, ConfamRateLimitError, ConfamModelError
|
|
99
133
|
|
|
100
134
|
try:
|
|
101
|
-
|
|
135
|
+
ansa = client.gist(
|
|
102
136
|
model="confam-speed",
|
|
103
137
|
messages="How far?"
|
|
104
138
|
)
|
|
105
139
|
except ConfamAuthError:
|
|
106
|
-
print("
|
|
140
|
+
print("You sure say na the correct API Key be that")
|
|
107
141
|
except ConfamRateLimitError:
|
|
108
|
-
print("You don reach your limit.
|
|
142
|
+
print("You don reach your limit. Contact us at confamnode@gmail.com make we upgrade your plan")
|
|
109
143
|
except ConfamModelError:
|
|
110
|
-
print("
|
|
144
|
+
print("This model name no dey valid")
|
|
111
145
|
```
|
|
112
146
|
|
|
113
147
|
---
|
|
114
148
|
|
|
115
149
|
## Private Deployment
|
|
116
150
|
|
|
117
|
-
Need NDPA-compliant private AI on your own infrastructure? JoTeq the First offers on-premise deployment on Jetson devices and GPUs, RAG pipelines, fine-tuning, and dedicated hosted models.
|
|
151
|
+
Need NDPA-compliant private AI on your own infrastructure? JoTeq the First offers on-premise deployment on Jetson devices and GPUs, RAG pipelines, fine-tuning, eevaluation, monitoring, and dedicated hosted models.
|
|
118
152
|
|
|
119
153
|
Contact us at **joteqthefirst@gmail.com**
|
|
120
154
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# ConfamNode
|
|
2
2
|
|
|
3
|
-
The Nigerian AI inference
|
|
3
|
+
The Nigerian AI inference gateway — access powerful AI models.
|
|
4
4
|
|
|
5
5
|
Built by **JoTeq the First**
|
|
6
6
|
|
|
@@ -8,10 +8,44 @@ Built by **JoTeq the First**
|
|
|
8
8
|
|
|
9
9
|
## Installation
|
|
10
10
|
|
|
11
|
+
### Using pip
|
|
11
12
|
```bash
|
|
12
13
|
pip install confamnode
|
|
13
14
|
```
|
|
14
15
|
|
|
16
|
+
### Using uv
|
|
17
|
+
```bash
|
|
18
|
+
uv add confamnode
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### Using virtualenv
|
|
22
|
+
```bash
|
|
23
|
+
# Create virtual environment
|
|
24
|
+
python -m venv .venv
|
|
25
|
+
|
|
26
|
+
# Activate — Linux/Mac
|
|
27
|
+
source .venv/bin/activate
|
|
28
|
+
|
|
29
|
+
# Activate — Windows
|
|
30
|
+
.venv\Scripts\activate
|
|
31
|
+
|
|
32
|
+
# Install
|
|
33
|
+
pip install confamnode
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Using uv with virtual environment
|
|
37
|
+
```bash
|
|
38
|
+
# Create project with uv
|
|
39
|
+
uv init my-project
|
|
40
|
+
cd my-project
|
|
41
|
+
|
|
42
|
+
# Add confamnode
|
|
43
|
+
uv add confamnode
|
|
44
|
+
|
|
45
|
+
# Run your script
|
|
46
|
+
uv run python main.py
|
|
47
|
+
```
|
|
48
|
+
|
|
15
49
|
---
|
|
16
50
|
|
|
17
51
|
## Quick Start
|
|
@@ -21,12 +55,12 @@ from confamnode import ConfamNode
|
|
|
21
55
|
|
|
22
56
|
client = ConfamNode(api_key="confam-sk-xxx")
|
|
23
57
|
|
|
24
|
-
|
|
58
|
+
ansa = client.gist(
|
|
25
59
|
model="confam-speed",
|
|
26
60
|
messages="How far?"
|
|
27
61
|
)
|
|
28
62
|
|
|
29
|
-
print(
|
|
63
|
+
print(ansa.choices[0].message.content)
|
|
30
64
|
```
|
|
31
65
|
|
|
32
66
|
---
|
|
@@ -34,12 +68,12 @@ print(response.choices[0].message.content)
|
|
|
34
68
|
## Streaming
|
|
35
69
|
|
|
36
70
|
```python
|
|
37
|
-
for
|
|
71
|
+
for yarn in client.gist(
|
|
38
72
|
model="confam-speed",
|
|
39
73
|
messages="How far?",
|
|
40
74
|
stream=True
|
|
41
75
|
):
|
|
42
|
-
print(
|
|
76
|
+
print(yarn.choices[0].delta.content, end="")
|
|
43
77
|
```
|
|
44
78
|
|
|
45
79
|
---
|
|
@@ -74,23 +108,23 @@ client = ConfamNode()
|
|
|
74
108
|
from confamnode import ConfamAuthError, ConfamRateLimitError, ConfamModelError
|
|
75
109
|
|
|
76
110
|
try:
|
|
77
|
-
|
|
111
|
+
ansa = client.gist(
|
|
78
112
|
model="confam-speed",
|
|
79
113
|
messages="How far?"
|
|
80
114
|
)
|
|
81
115
|
except ConfamAuthError:
|
|
82
|
-
print("
|
|
116
|
+
print("You sure say na the correct API Key be that")
|
|
83
117
|
except ConfamRateLimitError:
|
|
84
|
-
print("You don reach your limit.
|
|
118
|
+
print("You don reach your limit. Contact us at confamnode@gmail.com make we upgrade your plan")
|
|
85
119
|
except ConfamModelError:
|
|
86
|
-
print("
|
|
120
|
+
print("This model name no dey valid")
|
|
87
121
|
```
|
|
88
122
|
|
|
89
123
|
---
|
|
90
124
|
|
|
91
125
|
## Private Deployment
|
|
92
126
|
|
|
93
|
-
Need NDPA-compliant private AI on your own infrastructure? JoTeq the First offers on-premise deployment on Jetson devices and GPUs, RAG pipelines, fine-tuning, and dedicated hosted models.
|
|
127
|
+
Need NDPA-compliant private AI on your own infrastructure? JoTeq the First offers on-premise deployment on Jetson devices and GPUs, RAG pipelines, fine-tuning, eevaluation, monitoring, and dedicated hosted models.
|
|
94
128
|
|
|
95
129
|
Contact us at **joteqthefirst@gmail.com**
|
|
96
130
|
|
|
@@ -1,20 +1,25 @@
|
|
|
1
|
-
from confamnode.client import ConfamNode
|
|
1
|
+
from confamnode.client import ConfamNode, ConfamStream
|
|
2
2
|
from confamnode.exceptions import (
|
|
3
3
|
ConfamNodeError,
|
|
4
4
|
ConfamAuthError,
|
|
5
5
|
ConfamRateLimitError,
|
|
6
6
|
ConfamModelError,
|
|
7
7
|
)
|
|
8
|
+
from confamnode.ansa import Ansa, Usage, Cost
|
|
8
9
|
from confamnode import models
|
|
9
10
|
|
|
10
|
-
__version__ = "0.1.
|
|
11
|
+
__version__ = "0.1.1"
|
|
11
12
|
|
|
12
13
|
__all__ = [
|
|
13
14
|
"ConfamNode",
|
|
15
|
+
"ConfamStream",
|
|
14
16
|
"ConfamNodeError",
|
|
15
17
|
"ConfamAuthError",
|
|
16
18
|
"ConfamRateLimitError",
|
|
17
19
|
"ConfamModelError",
|
|
20
|
+
"Ansa",
|
|
21
|
+
"Usage",
|
|
22
|
+
"Cost",
|
|
18
23
|
"models",
|
|
19
24
|
"__version__"
|
|
20
25
|
]
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
from dataclasses import dataclass, field
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@dataclass
|
|
6
|
+
class Usage:
|
|
7
|
+
prompt_tokens: int
|
|
8
|
+
completion_tokens: int
|
|
9
|
+
total_tokens: int
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class Cost:
|
|
14
|
+
naira: float
|
|
15
|
+
naira_input: float = 0.0
|
|
16
|
+
naira_output: float = 0.0
|
|
17
|
+
dollars: float | None = None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class Ansa:
|
|
22
|
+
text: str
|
|
23
|
+
model: str
|
|
24
|
+
usage: Usage
|
|
25
|
+
cost: Cost
|
|
26
|
+
finish_reason: str
|
|
27
|
+
raw: object
|
|
28
|
+
reasoning: str | None = None
|
|
29
|
+
tools: list = field(default_factory=list)
|
|
30
|
+
citations: list = field(default_factory=list)
|
|
31
|
+
id: str = field(default_factory=lambda: f"confam-{uuid.uuid4()}")
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import litellm
|
|
3
|
+
|
|
4
|
+
from typing import Union, List, Dict
|
|
5
|
+
|
|
6
|
+
from confamnode import models
|
|
7
|
+
from confamnode.ansa import Ansa, Usage, Cost
|
|
8
|
+
from confamnode.exceptions import ConfamAuthError, ConfamModelError
|
|
9
|
+
|
|
10
|
+
VALID_MODELS = [
|
|
11
|
+
models.LITE,
|
|
12
|
+
models.SPEED,
|
|
13
|
+
models.REASONING,
|
|
14
|
+
|
|
15
|
+
models.INTELLIGENCE,
|
|
16
|
+
models.DEEP_REASONING,
|
|
17
|
+
models.CODE,
|
|
18
|
+
models.CODE_PRO,
|
|
19
|
+
models.VISION,
|
|
20
|
+
models.AUDIO,
|
|
21
|
+
models.TTS,
|
|
22
|
+
|
|
23
|
+
models.NANO,
|
|
24
|
+
|
|
25
|
+
models.EMBED_TEXT,
|
|
26
|
+
models.EMBED_MULTIMODAL,
|
|
27
|
+
models.EMBED_MULTIMODAL_2,
|
|
28
|
+
|
|
29
|
+
# Paid embeddings
|
|
30
|
+
models.EMBED_TEXT_PRO,
|
|
31
|
+
models.EMBED_MULTIMODAL_PRO,
|
|
32
|
+
models.EMBED_MULTILINGUAL,
|
|
33
|
+
models.EMBED_SMALL,
|
|
34
|
+
models.EMBED_0_6B,
|
|
35
|
+
models.EMBED_TEXT_LOCAL,
|
|
36
|
+
|
|
37
|
+
# Rerank
|
|
38
|
+
models.RERANK,
|
|
39
|
+
models.RERANK_FAST,
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
DEFAULT_BASE_URL = "https://api.confamnode.com/v1"
|
|
43
|
+
DEFAULT_USD_TO_NAIRA = 1400.0
|
|
44
|
+
|
|
45
|
+
class ConfamNode:
|
|
46
|
+
def __init__(
|
|
47
|
+
self,
|
|
48
|
+
api_key: str = None,
|
|
49
|
+
base_url: str = None
|
|
50
|
+
):
|
|
51
|
+
# Pick up from environment if not provided
|
|
52
|
+
api_key = api_key or os.environ.get("CONFAMNODE_API_KEY")
|
|
53
|
+
|
|
54
|
+
if not api_key:
|
|
55
|
+
raise ValueError("api_key is required")
|
|
56
|
+
|
|
57
|
+
if not api_key.startswith("confam-sk-"):
|
|
58
|
+
raise ConfamAuthError()
|
|
59
|
+
|
|
60
|
+
self.api_key = api_key
|
|
61
|
+
self.litellm_key = api_key.removeprefix("confam-")
|
|
62
|
+
self.base_url = base_url or DEFAULT_BASE_URL
|
|
63
|
+
|
|
64
|
+
def gist(
|
|
65
|
+
self,
|
|
66
|
+
model: str,
|
|
67
|
+
messages: Union[str, List[Dict[str, str]]],
|
|
68
|
+
**kwargs
|
|
69
|
+
) -> object:
|
|
70
|
+
if model not in VALID_MODELS:
|
|
71
|
+
raise ConfamModelError(model)
|
|
72
|
+
|
|
73
|
+
if isinstance(messages, str):
|
|
74
|
+
messages = [{"role": "user", "content": messages}]
|
|
75
|
+
elif not isinstance(messages, list):
|
|
76
|
+
raise ValueError("messages must be a string or list")
|
|
77
|
+
|
|
78
|
+
raw = litellm.completion(
|
|
79
|
+
model=f"openai/{model}",
|
|
80
|
+
messages=messages,
|
|
81
|
+
api_key=self.litellm_key,
|
|
82
|
+
base_url=self.base_url,
|
|
83
|
+
**kwargs
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
if kwargs.get("stream", False):
|
|
87
|
+
return ConfamStream(raw, model)
|
|
88
|
+
|
|
89
|
+
hidden = getattr(raw, "_hidden_params", {})
|
|
90
|
+
naira_cost = float(hidden.get("x_confam_naira_cost", 0.0))
|
|
91
|
+
naira_input = float(hidden.get("x_confam_naira_input", 0.0))
|
|
92
|
+
naira_output = float(hidden.get("x_confam_naira_output", 0.0))
|
|
93
|
+
usd_cost = hidden.get("x_confam_usd_cost", None)
|
|
94
|
+
if usd_cost is not None:
|
|
95
|
+
usd_cost = float(usd_cost)
|
|
96
|
+
|
|
97
|
+
# Extract text
|
|
98
|
+
text = raw.choices[0].message.content or ""
|
|
99
|
+
|
|
100
|
+
# Extract reasoning if present
|
|
101
|
+
reasoning = getattr(raw.choices[0].message, "reasoning_content", None)
|
|
102
|
+
|
|
103
|
+
# Extract tool calls if present
|
|
104
|
+
tools = []
|
|
105
|
+
if hasattr(raw.choices[0].message, "tool_calls") and raw.choices[0].message.tool_calls:
|
|
106
|
+
tools = raw.choices[0].message.tool_calls
|
|
107
|
+
|
|
108
|
+
# Extract citations
|
|
109
|
+
citations = []
|
|
110
|
+
if hasattr(raw.choices[0].message, "citations") and raw.choices[0].message.citations:
|
|
111
|
+
citations = raw.choices[0].message.citations
|
|
112
|
+
|
|
113
|
+
# Extract usage
|
|
114
|
+
usage = Usage(
|
|
115
|
+
prompt_tokens=raw.usage.prompt_tokens,
|
|
116
|
+
completion_tokens=raw.usage.completion_tokens,
|
|
117
|
+
total_tokens=raw.usage.total_tokens,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
cost = Cost(
|
|
121
|
+
naira=naira_cost,
|
|
122
|
+
naira_input=naira_input,
|
|
123
|
+
naira_output=naira_output,
|
|
124
|
+
dollars=usd_cost
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
return Ansa(
|
|
128
|
+
text=text,
|
|
129
|
+
model=model,
|
|
130
|
+
reasoning=reasoning,
|
|
131
|
+
tools=tools,
|
|
132
|
+
citations=citations,
|
|
133
|
+
usage=usage,
|
|
134
|
+
cost=cost,
|
|
135
|
+
finish_reason=raw.choices[0].finish_reason,
|
|
136
|
+
raw=raw,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class ConfamStream:
|
|
141
|
+
def __init__(self, raw_stream, model: str):
|
|
142
|
+
self._raw_stream = raw_stream
|
|
143
|
+
self._model = model
|
|
144
|
+
self._chunks = []
|
|
145
|
+
self._ansa = None
|
|
146
|
+
|
|
147
|
+
def __iter__(self):
|
|
148
|
+
for yarn in self._raw_stream:
|
|
149
|
+
self._chunks.append(yarn)
|
|
150
|
+
yield yarn
|
|
151
|
+
# Build Ansa after stream completes
|
|
152
|
+
self._ansa = self._build_ansa()
|
|
153
|
+
|
|
154
|
+
def get_ansa(self) -> Ansa:
|
|
155
|
+
if self._ansa is None:
|
|
156
|
+
raise RuntimeError("Stream not complete yet. Iterate through all chunks first.")
|
|
157
|
+
return self._ansa
|
|
158
|
+
|
|
159
|
+
def _build_ansa(self) -> Ansa:
|
|
160
|
+
# Collect text from all chunks
|
|
161
|
+
text = "".join([
|
|
162
|
+
c.choices[0].delta.content or ""
|
|
163
|
+
for c in self._chunks
|
|
164
|
+
if c.choices[0].delta.content
|
|
165
|
+
])
|
|
166
|
+
|
|
167
|
+
# Get finish reason from last chunk
|
|
168
|
+
finish_reason = self._chunks[-1].choices[0].finish_reason if self._chunks else "stop"
|
|
169
|
+
|
|
170
|
+
# Usage — only available in last chunk for some providers
|
|
171
|
+
last_chunk = self._chunks[-1] if self._chunks else None
|
|
172
|
+
usage = Usage(
|
|
173
|
+
prompt_tokens=getattr(getattr(last_chunk, "usage", None), "prompt_tokens", 0),
|
|
174
|
+
completion_tokens=getattr(getattr(last_chunk, "usage", None), "completion_tokens", 0),
|
|
175
|
+
total_tokens=getattr(getattr(last_chunk, "usage", None), "total_tokens", 0),
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
cost = Cost(naira=0.0)
|
|
179
|
+
|
|
180
|
+
return Ansa(
|
|
181
|
+
text=text,
|
|
182
|
+
model=self._model,
|
|
183
|
+
usage=usage,
|
|
184
|
+
cost=cost,
|
|
185
|
+
finish_reason=finish_reason,
|
|
186
|
+
raw=self._chunks,
|
|
187
|
+
)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# =========================================================================
|
|
2
|
+
# 🆓 FREE TIER — CHAT
|
|
3
|
+
# =========================================================================
|
|
4
|
+
LITE = "confam-lite"
|
|
5
|
+
SPEED = "confam-speed"
|
|
6
|
+
REASONING = "confam-reasoning"
|
|
7
|
+
|
|
8
|
+
# =========================================================================
|
|
9
|
+
# 💎 PAID TIER — CHAT
|
|
10
|
+
# =========================================================================
|
|
11
|
+
INTELLIGENCE = "confam-intelligence"
|
|
12
|
+
DEEP_REASONING = "confam-deep-reasoning"
|
|
13
|
+
CODE = "confam-code"
|
|
14
|
+
CODE_PRO = "confam-code-pro"
|
|
15
|
+
VISION = "confam-vision"
|
|
16
|
+
AUDIO = "confam-audio"
|
|
17
|
+
TTS = "confam-tts"
|
|
18
|
+
|
|
19
|
+
# =========================================================================
|
|
20
|
+
# 🛠️ LOCAL PAID (NDPA Compliant)
|
|
21
|
+
# =========================================================================
|
|
22
|
+
NANO = "confam-nano"
|
|
23
|
+
|
|
24
|
+
# =========================================================================
|
|
25
|
+
# 📦 FREE EMBEDDINGS
|
|
26
|
+
# =========================================================================
|
|
27
|
+
EMBED_TEXT = "confam-embed-text-1"
|
|
28
|
+
EMBED_MULTIMODAL = "confam-embed-multimodal-1"
|
|
29
|
+
EMBED_MULTIMODAL_2 = "confam-embed-multimodal-2"
|
|
30
|
+
|
|
31
|
+
# =========================================================================
|
|
32
|
+
# 💎 PAID EMBEDDINGS
|
|
33
|
+
# =========================================================================
|
|
34
|
+
EMBED_TEXT_PRO = "confam-embed-text-pro"
|
|
35
|
+
EMBED_MULTIMODAL_PRO = "confam-embed-multimodal-pro"
|
|
36
|
+
EMBED_MULTILINGUAL = "confam-embed-multilingual"
|
|
37
|
+
EMBED_SMALL = "confam-embed-small"
|
|
38
|
+
EMBED_0_6B = "confam-embed-0.6b"
|
|
39
|
+
EMBED_TEXT_LOCAL = "confam-embed-text-local"
|
|
40
|
+
|
|
41
|
+
# =========================================================================
|
|
42
|
+
# 🔍 RERANK
|
|
43
|
+
# =========================================================================
|
|
44
|
+
RERANK = "confam-rerank"
|
|
45
|
+
RERANK_FAST = "confam-rerank-fast"
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "confamnode"
|
|
3
|
-
version = "0.1.
|
|
4
|
-
description = "The Nigerian AI inference
|
|
3
|
+
version = "0.1.1"
|
|
4
|
+
description = "The Nigerian AI inference gateway — access powerful AI models"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.10"
|
|
7
7
|
license = { text = "MIT" }
|
|
@@ -34,6 +34,7 @@ dev = [
|
|
|
34
34
|
"build>=1.5.0",
|
|
35
35
|
"pytest>=9.0.3",
|
|
36
36
|
"pytest-dotenv>=0.5.2",
|
|
37
|
+
"setuptools>=82.0.1",
|
|
37
38
|
"twine>=6.2.0",
|
|
38
39
|
]
|
|
39
40
|
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from confamnode.ansa import Ansa, Usage, Cost
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def test_usage_has_prompt_tokens():
|
|
6
|
+
usage = Usage(prompt_tokens=10, completion_tokens=20, total_tokens=30)
|
|
7
|
+
assert usage.prompt_tokens == 10
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def test_usage_has_completion_tokens():
|
|
11
|
+
usage = Usage(prompt_tokens=10, completion_tokens=20, total_tokens=30)
|
|
12
|
+
assert usage.completion_tokens == 20
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_usage_has_total_tokens():
|
|
16
|
+
usage = Usage(prompt_tokens=10, completion_tokens=20, total_tokens=30)
|
|
17
|
+
assert usage.total_tokens == 30
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_cost_has_naira():
|
|
21
|
+
cost = Cost(naira=1.23)
|
|
22
|
+
assert cost.naira == 1.23
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_cost_dollars_is_none_by_default():
|
|
26
|
+
cost = Cost(naira=1.23)
|
|
27
|
+
assert cost.dollars is None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_cost_accepts_dollars():
|
|
31
|
+
cost = Cost(naira=1.23, dollars=0.0008)
|
|
32
|
+
assert cost.dollars == 0.0008
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_ansa_has_text():
|
|
36
|
+
usage = Usage(prompt_tokens=10, completion_tokens=20, total_tokens=30)
|
|
37
|
+
cost = Cost(naira=1.23)
|
|
38
|
+
ansa = Ansa(
|
|
39
|
+
text="How far!",
|
|
40
|
+
model="confam-speed",
|
|
41
|
+
usage=usage,
|
|
42
|
+
cost=cost,
|
|
43
|
+
finish_reason="stop",
|
|
44
|
+
raw={}
|
|
45
|
+
)
|
|
46
|
+
assert ansa.text == "How far!"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_ansa_has_model():
|
|
50
|
+
usage = Usage(prompt_tokens=10, completion_tokens=20, total_tokens=30)
|
|
51
|
+
cost = Cost(naira=1.23)
|
|
52
|
+
ansa = Ansa(
|
|
53
|
+
text="How far!",
|
|
54
|
+
model="confam-speed",
|
|
55
|
+
usage=usage,
|
|
56
|
+
cost=cost,
|
|
57
|
+
finish_reason="stop",
|
|
58
|
+
raw={}
|
|
59
|
+
)
|
|
60
|
+
assert ansa.model == "confam-speed"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_ansa_reasoning_is_none_by_default():
|
|
64
|
+
usage = Usage(prompt_tokens=10, completion_tokens=20, total_tokens=30)
|
|
65
|
+
cost = Cost(naira=1.23)
|
|
66
|
+
ansa = Ansa(
|
|
67
|
+
text="How far!",
|
|
68
|
+
model="confam-speed",
|
|
69
|
+
usage=usage,
|
|
70
|
+
cost=cost,
|
|
71
|
+
finish_reason="stop",
|
|
72
|
+
raw={}
|
|
73
|
+
)
|
|
74
|
+
assert ansa.reasoning is None
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_ansa_tools_is_empty_list_by_default():
|
|
78
|
+
usage = Usage(prompt_tokens=10, completion_tokens=20, total_tokens=30)
|
|
79
|
+
cost = Cost(naira=1.23)
|
|
80
|
+
ansa = Ansa(
|
|
81
|
+
text="How far!",
|
|
82
|
+
model="confam-speed",
|
|
83
|
+
usage=usage,
|
|
84
|
+
cost=cost,
|
|
85
|
+
finish_reason="stop",
|
|
86
|
+
raw={}
|
|
87
|
+
)
|
|
88
|
+
assert ansa.tools == []
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def test_ansa_has_usage():
|
|
92
|
+
usage = Usage(prompt_tokens=10, completion_tokens=20, total_tokens=30)
|
|
93
|
+
cost = Cost(naira=1.23)
|
|
94
|
+
ansa = Ansa(
|
|
95
|
+
text="How far!",
|
|
96
|
+
model="confam-speed",
|
|
97
|
+
usage=usage,
|
|
98
|
+
cost=cost,
|
|
99
|
+
finish_reason="stop",
|
|
100
|
+
raw={}
|
|
101
|
+
)
|
|
102
|
+
assert ansa.usage.total_tokens == 30
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def test_ansa_has_finish_reason():
|
|
106
|
+
usage = Usage(prompt_tokens=10, completion_tokens=20, total_tokens=30)
|
|
107
|
+
cost = Cost(naira=1.23)
|
|
108
|
+
ansa = Ansa(
|
|
109
|
+
text="How far!",
|
|
110
|
+
model="confam-speed",
|
|
111
|
+
usage=usage,
|
|
112
|
+
cost=cost,
|
|
113
|
+
finish_reason="stop",
|
|
114
|
+
raw={}
|
|
115
|
+
)
|
|
116
|
+
assert ansa.finish_reason == "stop"
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def test_ansa_has_raw():
|
|
120
|
+
usage = Usage(prompt_tokens=10, completion_tokens=20, total_tokens=30)
|
|
121
|
+
cost = Cost(naira=1.23)
|
|
122
|
+
raw = {"id": "abc123"}
|
|
123
|
+
ansa = Ansa(
|
|
124
|
+
text="How far!",
|
|
125
|
+
model="confam-speed",
|
|
126
|
+
usage=usage,
|
|
127
|
+
cost=cost,
|
|
128
|
+
finish_reason="stop",
|
|
129
|
+
raw=raw
|
|
130
|
+
)
|
|
131
|
+
assert ansa.raw == {"id": "abc123"}
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def test_ansa_accepts_reasoning():
|
|
135
|
+
usage = Usage(prompt_tokens=10, completion_tokens=20, total_tokens=30)
|
|
136
|
+
cost = Cost(naira=1.23)
|
|
137
|
+
ansa = Ansa(
|
|
138
|
+
text="The answer is 42",
|
|
139
|
+
model="confam-reasoning",
|
|
140
|
+
usage=usage,
|
|
141
|
+
cost=cost,
|
|
142
|
+
finish_reason="stop",
|
|
143
|
+
raw={},
|
|
144
|
+
reasoning="First I thought about it..."
|
|
145
|
+
)
|
|
146
|
+
assert ansa.reasoning == "First I thought about it..."
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def test_ansa_accepts_tools():
|
|
150
|
+
usage = Usage(prompt_tokens=10, completion_tokens=20, total_tokens=30)
|
|
151
|
+
cost = Cost(naira=1.23)
|
|
152
|
+
ansa = Ansa(
|
|
153
|
+
text="",
|
|
154
|
+
model="confam-agents",
|
|
155
|
+
usage=usage,
|
|
156
|
+
cost=cost,
|
|
157
|
+
finish_reason="tool_calls",
|
|
158
|
+
raw={},
|
|
159
|
+
tools=[{"name": "search", "arguments": {"query": "Lagos weather"}}]
|
|
160
|
+
)
|
|
161
|
+
assert len(ansa.tools) == 1
|
|
162
|
+
assert ansa.tools[0]["name"] == "search"
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def test_ansa_has_id():
|
|
166
|
+
usage = Usage(prompt_tokens=10, completion_tokens=20, total_tokens=30)
|
|
167
|
+
cost = Cost(naira=1.23)
|
|
168
|
+
ansa = Ansa(
|
|
169
|
+
text="How far!",
|
|
170
|
+
model="confam-speed",
|
|
171
|
+
usage=usage,
|
|
172
|
+
cost=cost,
|
|
173
|
+
finish_reason="stop",
|
|
174
|
+
raw={},
|
|
175
|
+
)
|
|
176
|
+
assert ansa.id.startswith("confam-")
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def test_ansa_id_is_unique():
|
|
180
|
+
usage = Usage(prompt_tokens=10, completion_tokens=20, total_tokens=30)
|
|
181
|
+
cost = Cost(naira=1.23)
|
|
182
|
+
ansa1 = Ansa(
|
|
183
|
+
text="How far!",
|
|
184
|
+
model="confam-speed",
|
|
185
|
+
usage=usage,
|
|
186
|
+
cost=cost,
|
|
187
|
+
finish_reason="stop",
|
|
188
|
+
raw={},
|
|
189
|
+
)
|
|
190
|
+
ansa2 = Ansa(
|
|
191
|
+
text="How far!",
|
|
192
|
+
model="confam-speed",
|
|
193
|
+
usage=usage,
|
|
194
|
+
cost=cost,
|
|
195
|
+
finish_reason="stop",
|
|
196
|
+
raw={},
|
|
197
|
+
)
|
|
198
|
+
assert ansa1.id != ansa2.id
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def test_ansa_citations_is_empty_list_by_default():
|
|
202
|
+
usage = Usage(prompt_tokens=10, completion_tokens=20, total_tokens=30)
|
|
203
|
+
cost = Cost(naira=1.23)
|
|
204
|
+
ansa = Ansa(
|
|
205
|
+
text="How far!",
|
|
206
|
+
model="confam-speed",
|
|
207
|
+
usage=usage,
|
|
208
|
+
cost=cost,
|
|
209
|
+
finish_reason="stop",
|
|
210
|
+
raw={}
|
|
211
|
+
)
|
|
212
|
+
assert ansa.citations == []
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def test_ansa_accepts_citations():
|
|
216
|
+
usage = Usage(prompt_tokens=10, completion_tokens=20, total_tokens=30)
|
|
217
|
+
cost = Cost(naira=1.23)
|
|
218
|
+
ansa = Ansa(
|
|
219
|
+
text="Nigerian GDP grew by 3.4%",
|
|
220
|
+
model="confam-speed",
|
|
221
|
+
usage=usage,
|
|
222
|
+
cost=cost,
|
|
223
|
+
finish_reason="stop",
|
|
224
|
+
raw={},
|
|
225
|
+
citations=[
|
|
226
|
+
{"source": "World Bank", "url": "https://worldbank.org/"},
|
|
227
|
+
{"source": "NBS Nigeria", "url": "https://nigerianstat.gov.ng/"}
|
|
228
|
+
]
|
|
229
|
+
)
|
|
230
|
+
assert len(ansa.citations) == 2
|
|
231
|
+
assert ansa.citations[0]["source"] == "World Bank"
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import pytest
|
|
2
2
|
from unittest.mock import patch, MagicMock
|
|
3
|
-
|
|
3
|
+
|
|
4
4
|
from confamnode import models
|
|
5
|
+
from confamnode.client import ConfamNode
|
|
6
|
+
from confamnode.ansa import Ansa, Usage, Cost
|
|
5
7
|
from confamnode.exceptions import ConfamModelError
|
|
6
8
|
|
|
7
9
|
|
|
10
|
+
|
|
8
11
|
@pytest.fixture
|
|
9
12
|
def client():
|
|
10
13
|
return ConfamNode(api_key="confam-sk-test-abc123")
|
|
@@ -41,11 +44,11 @@ def test_gist_rejects_invalid_model(client):
|
|
|
41
44
|
|
|
42
45
|
def test_gist_returns_content(client, mock_litellm_response):
|
|
43
46
|
with patch("confamnode.client.litellm.completion", return_value=mock_litellm_response):
|
|
44
|
-
|
|
47
|
+
ansa = client.gist(
|
|
45
48
|
model=models.SPEED,
|
|
46
49
|
messages=[{"role": "user", "content": "How far?"}]
|
|
47
50
|
)
|
|
48
|
-
assert
|
|
51
|
+
assert ansa.text == "How far! I dey fine."
|
|
49
52
|
|
|
50
53
|
|
|
51
54
|
def test_gist_accepts_all_valid_models(client, mock_litellm_response):
|
|
@@ -57,11 +60,11 @@ def test_gist_accepts_all_valid_models(client, mock_litellm_response):
|
|
|
57
60
|
]
|
|
58
61
|
with patch("confamnode.client.litellm.completion", return_value=mock_litellm_response):
|
|
59
62
|
for model in valid_models:
|
|
60
|
-
|
|
63
|
+
ansa = client.gist(
|
|
61
64
|
model=model,
|
|
62
65
|
messages=[{"role": "user", "content": "How far?"}]
|
|
63
66
|
)
|
|
64
|
-
assert
|
|
67
|
+
assert ansa is not None
|
|
65
68
|
|
|
66
69
|
|
|
67
70
|
def test_gist_sends_correct_model_to_litellm(client, mock_litellm_response):
|
|
@@ -77,11 +80,11 @@ def test_gist_sends_correct_model_to_litellm(client, mock_litellm_response):
|
|
|
77
80
|
|
|
78
81
|
def test_gist_accepts_string_messages(client, mock_litellm_response):
|
|
79
82
|
with patch("confamnode.client.litellm.completion", return_value=mock_litellm_response):
|
|
80
|
-
|
|
83
|
+
ansa = client.gist(
|
|
81
84
|
model=models.SPEED,
|
|
82
85
|
messages="How far?"
|
|
83
86
|
)
|
|
84
|
-
assert
|
|
87
|
+
assert ansa is not None
|
|
85
88
|
|
|
86
89
|
|
|
87
90
|
def test_gist_converts_string_to_message_list(client, mock_litellm_response):
|
|
@@ -96,11 +99,11 @@ def test_gist_converts_string_to_message_list(client, mock_litellm_response):
|
|
|
96
99
|
|
|
97
100
|
def test_gist_accepts_lists_messages(client, mock_litellm_response):
|
|
98
101
|
with patch("confamnode.client.litellm.completion", return_value=mock_litellm_response):
|
|
99
|
-
|
|
102
|
+
ansa = client.gist(
|
|
100
103
|
model=models.SPEED,
|
|
101
104
|
messages=[{"role": "user", "content": "How far?"}]
|
|
102
105
|
)
|
|
103
|
-
assert
|
|
106
|
+
assert ansa is not None
|
|
104
107
|
|
|
105
108
|
|
|
106
109
|
def test_gist_rejects_invalid_messages_type(client):
|
|
@@ -122,3 +125,91 @@ def test_gist_passes_kwargs_to_litellm(client, mock_litellm_response):
|
|
|
122
125
|
call_args = mock_call.call_args
|
|
123
126
|
assert call_args.kwargs["temperature"] == 0.7
|
|
124
127
|
assert call_args.kwargs["max_tokens"] == 500
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def test_gist_prepends_openai_prefix_to_model(client, mock_litellm_response):
|
|
131
|
+
with patch("confamnode.client.litellm.completion", return_value=mock_litellm_response) as mock_call:
|
|
132
|
+
client.gist(
|
|
133
|
+
model="confam-speed",
|
|
134
|
+
messages="How far?"
|
|
135
|
+
)
|
|
136
|
+
call_args = mock_call.call_args
|
|
137
|
+
assert call_args.kwargs["model"] == "openai/confam-speed"
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def test_gist_returns_ansa_object(client, mock_litellm_response):
|
|
141
|
+
with patch("confamnode.client.litellm.completion", return_value=mock_litellm_response):
|
|
142
|
+
ansa = client.gist(
|
|
143
|
+
model="confam-speed",
|
|
144
|
+
messages="How far?"
|
|
145
|
+
)
|
|
146
|
+
assert isinstance(ansa, Ansa)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def test_gist_has_test(client, mock_litellm_response):
|
|
150
|
+
with patch("confamnode.client.litellm.completion", return_value=mock_litellm_response):
|
|
151
|
+
ansa = client.gist(
|
|
152
|
+
model="confam-speed",
|
|
153
|
+
messages="How far?"
|
|
154
|
+
)
|
|
155
|
+
assert ansa.text == "How far! I dey fine."
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def test_gist_has_model(client, mock_litellm_response):
|
|
159
|
+
with patch("confamnode.client.litellm.completion", return_value=mock_litellm_response):
|
|
160
|
+
ansa = client.gist(
|
|
161
|
+
model="confam-speed",
|
|
162
|
+
messages="How far?"
|
|
163
|
+
)
|
|
164
|
+
assert ansa.model == "confam-speed"
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def test_gist_has_usage(client, mock_litellm_response):
|
|
168
|
+
with patch("confamnode.client.litellm.completion", return_value=mock_litellm_response):
|
|
169
|
+
ansa = client.gist(
|
|
170
|
+
model="confam-speed",
|
|
171
|
+
messages="How far?"
|
|
172
|
+
)
|
|
173
|
+
assert isinstance(ansa.usage, Usage)
|
|
174
|
+
assert ansa.usage.prompt_tokens == 10
|
|
175
|
+
assert ansa.usage.completion_tokens == 20
|
|
176
|
+
assert ansa.usage.total_tokens == 30
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def test_gist_has_cost(client, mock_litellm_response):
|
|
180
|
+
with patch("confamnode.client.litellm.completion", return_value=mock_litellm_response):
|
|
181
|
+
ansa = client.gist(
|
|
182
|
+
model="confam-speed",
|
|
183
|
+
messages="How far?"
|
|
184
|
+
)
|
|
185
|
+
assert isinstance(ansa.cost, Cost)
|
|
186
|
+
assert isinstance(ansa.cost.naira, float)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def test_gist_has_finish_reason(client, mock_litellm_response):
|
|
190
|
+
with patch("confamnode.client.litellm.completion", return_value=mock_litellm_response):
|
|
191
|
+
ansa = client.gist(
|
|
192
|
+
model="confam-speed",
|
|
193
|
+
messages="How far?"
|
|
194
|
+
)
|
|
195
|
+
assert ansa.finish_reason is not None
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def test_gist_has_id(client, mock_litellm_response):
|
|
199
|
+
with patch("confamnode.client.litellm.completion", return_value=mock_litellm_response):
|
|
200
|
+
ansa = client.gist(
|
|
201
|
+
model="confam-speed",
|
|
202
|
+
messages="How far?"
|
|
203
|
+
)
|
|
204
|
+
assert ansa.id.startswith("confam-")
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def test_gist_has_raw(client, mock_litellm_response):
|
|
208
|
+
with patch("confamnode.client.litellm.completion", return_value=mock_litellm_response):
|
|
209
|
+
ansa = client.gist(
|
|
210
|
+
model="confam-speed",
|
|
211
|
+
messages="How far?"
|
|
212
|
+
)
|
|
213
|
+
assert ansa.raw is not None
|
|
214
|
+
|
|
215
|
+
|
|
@@ -19,4 +19,9 @@ def test_exceptions_importable_from_package():
|
|
|
19
19
|
def test_version_is_defined():
|
|
20
20
|
import confamnode
|
|
21
21
|
assert hasattr(confamnode, "__version__")
|
|
22
|
-
assert isinstance(confamnode.__version__, str)
|
|
22
|
+
assert isinstance(confamnode.__version__, str)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_confamstream_importable_from_package():
|
|
26
|
+
from confamnode import ConfamStream
|
|
27
|
+
assert ConfamStream is not None
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
from confamnode.models import (
|
|
2
|
+
LITE, SPEED, REASONING,
|
|
3
|
+
INTELLIGENCE, DEEP_REASONING, CODE, CODE_PRO, VISION, AUDIO, TTS,
|
|
4
|
+
NANO,
|
|
5
|
+
EMBED_TEXT, EMBED_MULTIMODAL, EMBED_MULTIMODAL_2, EMBED_TEXT_LOCAL,
|
|
6
|
+
EMBED_TEXT_PRO, EMBED_MULTIMODAL_PRO, EMBED_MULTILINGUAL,
|
|
7
|
+
EMBED_SMALL, EMBED_0_6B,
|
|
8
|
+
RERANK, RERANK_FAST,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_model_constants_are_strings():
|
|
13
|
+
assert isinstance(LITE, str)
|
|
14
|
+
assert isinstance(SPEED, str)
|
|
15
|
+
assert isinstance(REASONING, str)
|
|
16
|
+
assert isinstance(INTELLIGENCE, str)
|
|
17
|
+
assert isinstance(DEEP_REASONING, str)
|
|
18
|
+
assert isinstance(CODE, str)
|
|
19
|
+
assert isinstance(CODE_PRO, str)
|
|
20
|
+
assert isinstance(VISION, str)
|
|
21
|
+
assert isinstance(AUDIO, str)
|
|
22
|
+
assert isinstance(TTS, str)
|
|
23
|
+
assert isinstance(NANO, str)
|
|
24
|
+
assert isinstance(EMBED_TEXT, str)
|
|
25
|
+
assert isinstance(EMBED_MULTIMODAL, str)
|
|
26
|
+
assert isinstance(EMBED_MULTIMODAL_2, str)
|
|
27
|
+
assert isinstance(EMBED_TEXT_LOCAL, str)
|
|
28
|
+
assert isinstance(EMBED_TEXT_PRO, str)
|
|
29
|
+
assert isinstance(EMBED_MULTIMODAL_PRO, str)
|
|
30
|
+
assert isinstance(EMBED_MULTILINGUAL, str)
|
|
31
|
+
assert isinstance(EMBED_SMALL, str)
|
|
32
|
+
assert isinstance(EMBED_0_6B, str)
|
|
33
|
+
assert isinstance(RERANK, str)
|
|
34
|
+
assert isinstance(RERANK_FAST, str)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_model_constants_start_with_confam():
|
|
38
|
+
assert LITE.startswith("confam-")
|
|
39
|
+
assert SPEED.startswith("confam-")
|
|
40
|
+
assert REASONING.startswith("confam-")
|
|
41
|
+
assert INTELLIGENCE.startswith("confam-")
|
|
42
|
+
assert DEEP_REASONING.startswith("confam-")
|
|
43
|
+
assert CODE.startswith("confam-")
|
|
44
|
+
assert CODE_PRO.startswith("confam-")
|
|
45
|
+
assert VISION.startswith("confam-")
|
|
46
|
+
assert AUDIO.startswith("confam-")
|
|
47
|
+
assert TTS.startswith("confam-")
|
|
48
|
+
assert NANO.startswith("confam-")
|
|
49
|
+
assert EMBED_TEXT.startswith("confam-")
|
|
50
|
+
assert EMBED_MULTIMODAL.startswith("confam-")
|
|
51
|
+
assert EMBED_MULTIMODAL_2.startswith("confam-")
|
|
52
|
+
assert EMBED_TEXT_LOCAL.startswith("confam-")
|
|
53
|
+
assert EMBED_TEXT_PRO.startswith("confam-")
|
|
54
|
+
assert EMBED_MULTIMODAL_PRO.startswith("confam-")
|
|
55
|
+
assert EMBED_MULTILINGUAL.startswith("confam-")
|
|
56
|
+
assert EMBED_SMALL.startswith("confam-")
|
|
57
|
+
assert EMBED_0_6B.startswith("confam-")
|
|
58
|
+
assert RERANK.startswith("confam-")
|
|
59
|
+
assert RERANK_FAST.startswith("confam-")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_model_constants_have_correct_values():
|
|
63
|
+
assert LITE == "confam-lite"
|
|
64
|
+
assert SPEED == "confam-speed"
|
|
65
|
+
assert REASONING == "confam-reasoning"
|
|
66
|
+
assert INTELLIGENCE == "confam-intelligence"
|
|
67
|
+
assert DEEP_REASONING == "confam-deep-reasoning"
|
|
68
|
+
assert CODE == "confam-code"
|
|
69
|
+
assert CODE_PRO == "confam-code-pro"
|
|
70
|
+
assert VISION == "confam-vision"
|
|
71
|
+
assert AUDIO == "confam-audio"
|
|
72
|
+
assert TTS == "confam-tts"
|
|
73
|
+
assert NANO == "confam-nano"
|
|
74
|
+
assert EMBED_TEXT == "confam-embed-text-1"
|
|
75
|
+
assert EMBED_MULTIMODAL == "confam-embed-multimodal-1"
|
|
76
|
+
assert EMBED_MULTIMODAL_2 == "confam-embed-multimodal-2"
|
|
77
|
+
assert EMBED_TEXT_LOCAL == "confam-embed-text-local"
|
|
78
|
+
assert EMBED_TEXT_PRO == "confam-embed-text-pro"
|
|
79
|
+
assert EMBED_MULTIMODAL_PRO == "confam-embed-multimodal-pro"
|
|
80
|
+
assert EMBED_MULTILINGUAL == "confam-embed-multilingual"
|
|
81
|
+
assert EMBED_SMALL == "confam-embed-small"
|
|
82
|
+
assert EMBED_0_6B == "confam-embed-0.6b"
|
|
83
|
+
assert RERANK == "confam-rerank"
|
|
84
|
+
assert RERANK_FAST == "confam-rerank-fast"
|
|
@@ -418,7 +418,7 @@ wheels = [
|
|
|
418
418
|
|
|
419
419
|
[[package]]
|
|
420
420
|
name = "confamnode"
|
|
421
|
-
version = "0.1.
|
|
421
|
+
version = "0.1.1"
|
|
422
422
|
source = { editable = "." }
|
|
423
423
|
dependencies = [
|
|
424
424
|
{ name = "litellm" },
|
|
@@ -430,6 +430,7 @@ dev = [
|
|
|
430
430
|
{ name = "build" },
|
|
431
431
|
{ name = "pytest" },
|
|
432
432
|
{ name = "pytest-dotenv" },
|
|
433
|
+
{ name = "setuptools" },
|
|
433
434
|
{ name = "twine" },
|
|
434
435
|
]
|
|
435
436
|
|
|
@@ -444,6 +445,7 @@ dev = [
|
|
|
444
445
|
{ name = "build", specifier = ">=1.5.0" },
|
|
445
446
|
{ name = "pytest", specifier = ">=9.0.3" },
|
|
446
447
|
{ name = "pytest-dotenv", specifier = ">=0.5.2" },
|
|
448
|
+
{ name = "setuptools", specifier = ">=82.0.1" },
|
|
447
449
|
{ name = "twine", specifier = ">=6.2.0" },
|
|
448
450
|
]
|
|
449
451
|
|
|
@@ -2283,6 +2285,15 @@ wheels = [
|
|
|
2283
2285
|
{ url = "https://files.pythonhosted.org/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-py3-none-any.whl", hash = "sha256:0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137", size = 15554, upload-time = "2025-11-23T19:02:51.545Z" },
|
|
2284
2286
|
]
|
|
2285
2287
|
|
|
2288
|
+
[[package]]
|
|
2289
|
+
name = "setuptools"
|
|
2290
|
+
version = "82.0.1"
|
|
2291
|
+
source = { registry = "https://pypi.org/simple" }
|
|
2292
|
+
sdist = { url = "https://files.pythonhosted.org/packages/4f/db/cfac1baf10650ab4d1c111714410d2fbb77ac5a616db26775db562c8fab2/setuptools-82.0.1.tar.gz", hash = "sha256:7d872682c5d01cfde07da7bccc7b65469d3dca203318515ada1de5eda35efbf9", size = 1152316, upload-time = "2026-03-09T12:47:17.221Z" }
|
|
2293
|
+
wheels = [
|
|
2294
|
+
{ url = "https://files.pythonhosted.org/packages/9d/76/f789f7a86709c6b087c5a2f52f911838cad707cc613162401badc665acfe/setuptools-82.0.1-py3-none-any.whl", hash = "sha256:a59e362652f08dcd477c78bb6e7bd9d80a7995bc73ce773050228a348ce2e5bb", size = 1006223, upload-time = "2026-03-09T12:47:15.026Z" },
|
|
2295
|
+
]
|
|
2296
|
+
|
|
2286
2297
|
[[package]]
|
|
2287
2298
|
name = "shellingham"
|
|
2288
2299
|
version = "1.5.4"
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
import litellm
|
|
3
|
-
|
|
4
|
-
from typing import Union, List, Dict
|
|
5
|
-
|
|
6
|
-
from confamnode import models
|
|
7
|
-
from confamnode.exceptions import ConfamAuthError, ConfamModelError
|
|
8
|
-
|
|
9
|
-
VALID_MODELS = [
|
|
10
|
-
models.LITE,
|
|
11
|
-
models.SPEED,
|
|
12
|
-
models.REASONING,
|
|
13
|
-
models.VISION,
|
|
14
|
-
models.AUDIO,
|
|
15
|
-
models.TTS,
|
|
16
|
-
models.NANO
|
|
17
|
-
]
|
|
18
|
-
|
|
19
|
-
DEFAULT_BASE_URL = "https://api.confamnode.com/v1"
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
class ConfamNode:
|
|
23
|
-
def __init__(
|
|
24
|
-
self,
|
|
25
|
-
api_key: str = None,
|
|
26
|
-
base_url: str = None
|
|
27
|
-
):
|
|
28
|
-
# Pick up from environment if not provided
|
|
29
|
-
api_key = api_key or os.environ.get("CONFAMNODE_API_KEY")
|
|
30
|
-
|
|
31
|
-
if not api_key:
|
|
32
|
-
raise ValueError("api_key is required")
|
|
33
|
-
|
|
34
|
-
if not api_key.startswith("confam-sk-"):
|
|
35
|
-
raise ConfamAuthError()
|
|
36
|
-
|
|
37
|
-
self.api_key = api_key
|
|
38
|
-
self.litellm_key = api_key.removeprefix("confam-")
|
|
39
|
-
self.base_url = base_url or DEFAULT_BASE_URL
|
|
40
|
-
|
|
41
|
-
def gist(
|
|
42
|
-
self,
|
|
43
|
-
model: str,
|
|
44
|
-
messages: Union[str, List[Dict[str, str]]],
|
|
45
|
-
**kwargs
|
|
46
|
-
) -> object:
|
|
47
|
-
if model not in VALID_MODELS:
|
|
48
|
-
raise ConfamModelError(model)
|
|
49
|
-
|
|
50
|
-
if isinstance(messages, str):
|
|
51
|
-
messages = [{"role": "user", "content": messages}]
|
|
52
|
-
elif not isinstance(messages, list):
|
|
53
|
-
raise ValueError("messages must be a string or list")
|
|
54
|
-
|
|
55
|
-
response = litellm.completion(
|
|
56
|
-
model=model,
|
|
57
|
-
messages=messages,
|
|
58
|
-
api_key=self.litellm_key,
|
|
59
|
-
base_url=self.base_url,
|
|
60
|
-
**kwargs
|
|
61
|
-
)
|
|
62
|
-
|
|
63
|
-
return response
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
# Chat models
|
|
2
|
-
LITE = "confam-lite"
|
|
3
|
-
SPEED = "confam-speed"
|
|
4
|
-
REASONING = "confam-reasoning"
|
|
5
|
-
VISION = "confam-vision"
|
|
6
|
-
AUDIO = "confam-audio"
|
|
7
|
-
TTS = "confam-tts"
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
# Embedding models
|
|
11
|
-
EMBED_TEXT = "confam-embed-text-1"
|
|
12
|
-
EMBED_MULTIMODAL = "confam-embed-multimodal-1"
|
|
13
|
-
EMBED_MULTIMODAL_2 = "confam-embed-multimodal-2"
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
# Local models
|
|
17
|
-
NANO = "confam-nano"
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
# Local Embedding models
|
|
21
|
-
EMBED_TEXT_LOCAL = "confam-embed-text-local"
|
|
22
|
-
|
confamnode-0.1.0/main.py
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
from confamnode.models import (
|
|
2
|
-
LITE,
|
|
3
|
-
SPEED,
|
|
4
|
-
REASONING,
|
|
5
|
-
VISION,
|
|
6
|
-
AUDIO,
|
|
7
|
-
TTS,
|
|
8
|
-
NANO,
|
|
9
|
-
EMBED_TEXT,
|
|
10
|
-
EMBED_MULTIMODAL,
|
|
11
|
-
EMBED_MULTIMODAL_2,
|
|
12
|
-
EMBED_TEXT_LOCAL
|
|
13
|
-
)
|
|
14
|
-
|
|
15
|
-
def test_model_constants_are_strings():
|
|
16
|
-
assert isinstance(LITE, str)
|
|
17
|
-
assert isinstance(SPEED, str)
|
|
18
|
-
assert isinstance(REASONING, str)
|
|
19
|
-
assert isinstance(VISION, str)
|
|
20
|
-
assert isinstance(AUDIO, str)
|
|
21
|
-
assert isinstance(TTS, str)
|
|
22
|
-
assert isinstance(NANO, str)
|
|
23
|
-
assert isinstance(EMBED_TEXT, str)
|
|
24
|
-
assert isinstance(EMBED_MULTIMODAL, str)
|
|
25
|
-
assert isinstance(EMBED_MULTIMODAL_2, str)
|
|
26
|
-
assert isinstance(EMBED_TEXT_LOCAL, str)
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
def test_model_constants_start_with_confam():
|
|
30
|
-
assert LITE.startswith("confam-")
|
|
31
|
-
assert SPEED.startswith("confam-")
|
|
32
|
-
assert REASONING.startswith("confam-")
|
|
33
|
-
assert VISION.startswith("confam-")
|
|
34
|
-
assert AUDIO.startswith("confam-")
|
|
35
|
-
assert TTS.startswith("confam-")
|
|
36
|
-
assert NANO.startswith("confam-")
|
|
37
|
-
assert EMBED_TEXT.startswith("confam-")
|
|
38
|
-
assert EMBED_MULTIMODAL.startswith("confam-")
|
|
39
|
-
assert EMBED_MULTIMODAL_2.startswith("confam-")
|
|
40
|
-
assert EMBED_TEXT_LOCAL.startswith("confam-")
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
def test_model_constants_have_correct_values():
|
|
44
|
-
assert LITE == "confam-lite"
|
|
45
|
-
assert SPEED == "confam-speed"
|
|
46
|
-
assert REASONING == "confam-reasoning"
|
|
47
|
-
assert VISION == "confam-vision"
|
|
48
|
-
assert AUDIO == "confam-audio"
|
|
49
|
-
assert TTS == "confam-tts"
|
|
50
|
-
assert NANO == "confam-nano"
|
|
51
|
-
assert EMBED_TEXT == "confam-embed-text-1"
|
|
52
|
-
assert EMBED_MULTIMODAL == "confam-embed-multimodal-1"
|
|
53
|
-
assert EMBED_MULTIMODAL_2 == "confam-embed-multimodal-2"
|
|
54
|
-
assert EMBED_TEXT_LOCAL == "confam-embed-text-local"
|
|
55
|
-
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
import pytest
|
|
2
|
-
from unittest.mock import patch, MagicMock
|
|
3
|
-
from confamnode.client import ConfamNode
|
|
4
|
-
from confamnode import models
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
@pytest.fixture
|
|
8
|
-
def client():
|
|
9
|
-
return ConfamNode(api_key="confam-sk-abc123")
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
@pytest.fixture
|
|
13
|
-
def mock_stream_response():
|
|
14
|
-
chunk1 = MagicMock()
|
|
15
|
-
chunk1.choices[0].delta.content = "How"
|
|
16
|
-
chunk2 = MagicMock()
|
|
17
|
-
chunk2.choices[0].delta.content = " far!"
|
|
18
|
-
chunk3 = MagicMock()
|
|
19
|
-
chunk3.choices[0].delta.content = None
|
|
20
|
-
return iter([chunk1, chunk2, chunk3])
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def test_gist_accepts_stream_true(client, mock_stream_response):
|
|
24
|
-
with patch("confamnode.client.litellm.completion", return_value=mock_stream_response):
|
|
25
|
-
response = client.gist(
|
|
26
|
-
model=models.SPEED,
|
|
27
|
-
messages="How far?",
|
|
28
|
-
stream=True
|
|
29
|
-
)
|
|
30
|
-
assert response is not None
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
def test_gist_stream_returns_chunks(client, mock_stream_response):
|
|
34
|
-
with patch("confamnode.client.litellm.completion", return_value=mock_stream_response):
|
|
35
|
-
chunks = list(client.gist(
|
|
36
|
-
model=models.SPEED,
|
|
37
|
-
messages="How far?",
|
|
38
|
-
stream=True
|
|
39
|
-
))
|
|
40
|
-
assert len(chunks) == 3
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
def test_gist_stream_chunks_have_content(client, mock_stream_response):
|
|
44
|
-
with patch("confamnode.client.litellm.completion", return_value=mock_stream_response):
|
|
45
|
-
chunks = list(client.gist(
|
|
46
|
-
model=models.SPEED,
|
|
47
|
-
messages="How far?",
|
|
48
|
-
stream=True
|
|
49
|
-
))
|
|
50
|
-
assert chunks[0].choices[0].delta.content == "How"
|
|
51
|
-
assert chunks[1].choices[0].delta.content == " far!"
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
def test_gist_stream_passes_stream_true_to_litellm(client, mock_stream_response):
|
|
55
|
-
with patch("confamnode.client.litellm.completion", return_value=mock_stream_response) as mock_call:
|
|
56
|
-
client.gist(
|
|
57
|
-
model=models.SPEED,
|
|
58
|
-
messages="How far?",
|
|
59
|
-
stream=True
|
|
60
|
-
)
|
|
61
|
-
call_args = mock_call.call_args
|
|
62
|
-
assert call_args.kwargs.get("stream") == True
|
|
63
|
-
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|