caedral 0.1.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.
- caedral-0.1.0/.gitignore +24 -0
- caedral-0.1.0/PKG-INFO +228 -0
- caedral-0.1.0/README.md +200 -0
- caedral-0.1.0/pyproject.toml +50 -0
- caedral-0.1.0/src/caedral/__init__.py +5 -0
- caedral-0.1.0/src/caedral/_sse.py +66 -0
- caedral-0.1.0/src/caedral/client.py +68 -0
- caedral-0.1.0/src/caedral/errors.py +59 -0
- caedral-0.1.0/src/caedral/http.py +141 -0
- caedral-0.1.0/src/caedral/resources/__init__.py +17 -0
- caedral-0.1.0/src/caedral/resources/audio.py +45 -0
- caedral-0.1.0/src/caedral/resources/chat.py +125 -0
- caedral-0.1.0/src/caedral/resources/embeddings.py +41 -0
- caedral-0.1.0/src/caedral/resources/images.py +42 -0
- caedral-0.1.0/src/caedral/resources/models.py +24 -0
- caedral-0.1.0/src/caedral/resources/rerank.py +52 -0
- caedral-0.1.0/src/caedral/resources/usage.py +25 -0
- caedral-0.1.0/src/caedral/types.py +129 -0
- caedral-0.1.0/tests/__init__.py +0 -0
- caedral-0.1.0/tests/conftest.py +26 -0
- caedral-0.1.0/tests/helpers.py +101 -0
- caedral-0.1.0/tests/test_integration.py +90 -0
caedral-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.egg-info/
|
|
6
|
+
dist/
|
|
7
|
+
build/
|
|
8
|
+
.venv/
|
|
9
|
+
venv/
|
|
10
|
+
*.egg
|
|
11
|
+
|
|
12
|
+
# Environment
|
|
13
|
+
.env
|
|
14
|
+
.env.*
|
|
15
|
+
|
|
16
|
+
# IDE
|
|
17
|
+
.idea/
|
|
18
|
+
.vscode/
|
|
19
|
+
*.swp
|
|
20
|
+
|
|
21
|
+
# Testing
|
|
22
|
+
.pytest_cache/
|
|
23
|
+
.coverage
|
|
24
|
+
htmlcov/
|
caedral-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: caedral
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official Python client for the Caedral API
|
|
5
|
+
Project-URL: Homepage, https://caedral.com/docs/python
|
|
6
|
+
Project-URL: Repository, https://github.com/caedral/caedral-python
|
|
7
|
+
Project-URL: Issues, https://github.com/caedral/caedral-python/issues
|
|
8
|
+
Author-email: Caedral <hello@caedral.com>
|
|
9
|
+
License: MIT
|
|
10
|
+
Keywords: ai,api,caedral,llm
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Typing :: Typed
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Requires-Dist: httpx>=0.27.0
|
|
21
|
+
Requires-Dist: pydantic>=2.0.0
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: bcrypt>=4.0.0; extra == 'dev'
|
|
24
|
+
Requires-Dist: psycopg[binary]>=3.1.0; extra == 'dev'
|
|
25
|
+
Requires-Dist: pytest>=8.0.0; extra == 'dev'
|
|
26
|
+
Requires-Dist: python-dotenv>=1.0.0; extra == 'dev'
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# Caedral Python SDK
|
|
30
|
+
|
|
31
|
+
Official Python client for the [Caedral API](https://caedral.com). OpenAI-compatible request shapes — point your existing code at Caedral with minimal changes.
|
|
32
|
+
|
|
33
|
+
## Installation
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install caedral
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Local development (editable install)
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
cd sdk-python
|
|
43
|
+
python -m venv .venv
|
|
44
|
+
source .venv/bin/activate # Windows: .venv\Scripts\activate
|
|
45
|
+
pip install -e ".[dev]"
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Quickstart
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
from caedral import Caedral
|
|
52
|
+
|
|
53
|
+
caedral = Caedral(
|
|
54
|
+
api_key="cd_live_...",
|
|
55
|
+
base_url="http://localhost:5001", # local API gateway
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
completion = caedral.chat.completions.create(
|
|
59
|
+
model="caedral-titan",
|
|
60
|
+
messages=[{"role": "user", "content": "Hello!"}],
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
print(completion.choices[0].message["content"])
|
|
64
|
+
caedral.close()
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Or use a context manager:
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
with Caedral(api_key="cd_live_...", base_url="http://localhost:5001") as caedral:
|
|
71
|
+
usage = caedral.usage.get()
|
|
72
|
+
print(usage.weeklyPool.remaining)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Production default base URL: `https://api.caedral.com`.
|
|
76
|
+
|
|
77
|
+
## Configuration
|
|
78
|
+
|
|
79
|
+
| Parameter | Default | Description |
|
|
80
|
+
|-----------|---------|-------------|
|
|
81
|
+
| `api_key` | — | Required. Your `cd_live_...` API key |
|
|
82
|
+
| `base_url` | `https://api.caedral.com` | API gateway base URL |
|
|
83
|
+
| `max_retries` | `3` | Retries for idempotent GET requests (exponential backoff) |
|
|
84
|
+
| `timeout` | `120.0` | Request timeout in seconds |
|
|
85
|
+
|
|
86
|
+
## Methods
|
|
87
|
+
|
|
88
|
+
### `caedral.chat.completions.create(...)`
|
|
89
|
+
|
|
90
|
+
OpenAI-compatible chat completions.
|
|
91
|
+
|
|
92
|
+
**Non-streaming:**
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
response = caedral.chat.completions.create(
|
|
96
|
+
model="caedral-olympus",
|
|
97
|
+
messages=[
|
|
98
|
+
{"role": "system", "content": "You are a helpful assistant."},
|
|
99
|
+
{"role": "user", "content": "Explain quantum computing briefly."},
|
|
100
|
+
],
|
|
101
|
+
temperature=0.7,
|
|
102
|
+
max_tokens=500,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
print(response.choices[0].message["content"])
|
|
106
|
+
print(response.usage.total_tokens if response.usage else None)
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
**Streaming** (generator):
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
stream = caedral.chat.completions.create(
|
|
113
|
+
model="caedral-titan",
|
|
114
|
+
messages=[{"role": "user", "content": "Write a haiku about code."}],
|
|
115
|
+
stream=True,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
for chunk in stream:
|
|
119
|
+
delta = chunk.choices[0].delta.get("content")
|
|
120
|
+
if delta:
|
|
121
|
+
print(delta, end="", flush=True)
|
|
122
|
+
print()
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Models: `caedral-base`, `caedral-titan`, `caedral-olympus`, `caedral-primordial`.
|
|
126
|
+
|
|
127
|
+
### `caedral.models.list()`
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
models = caedral.models.list()
|
|
131
|
+
for model in models.data:
|
|
132
|
+
print(model.id, model.name, model.pricing_tier)
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### `caedral.usage.get()`
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
usage = caedral.usage.get()
|
|
139
|
+
print("Pool remaining:", usage.weeklyPool.remaining)
|
|
140
|
+
print("Balance (cents):", usage.balanceCents)
|
|
141
|
+
print("Overage used:", usage.overage.usedCents)
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### `caedral.embeddings.create(...)`
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
result = caedral.embeddings.create(
|
|
148
|
+
model="caedral-embed",
|
|
149
|
+
input="Caedral unifies frontier models behind one API.",
|
|
150
|
+
)
|
|
151
|
+
print(len(result.data[0].embedding))
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### `caedral.images.generate(...)`
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
image = caedral.images.generate(
|
|
158
|
+
model="caedral-vision",
|
|
159
|
+
prompt="A minimal geometric logo on a dark background",
|
|
160
|
+
)
|
|
161
|
+
print(image.data[0].url or "b64 payload returned")
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### `caedral.audio.generate(...)`
|
|
165
|
+
|
|
166
|
+
```python
|
|
167
|
+
audio = caedral.audio.generate(
|
|
168
|
+
model="caedral-voice",
|
|
169
|
+
input="Welcome to Caedral.",
|
|
170
|
+
voice="alloy",
|
|
171
|
+
)
|
|
172
|
+
print(audio.model)
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### `caedral.rerank.create(...)`
|
|
176
|
+
|
|
177
|
+
```python
|
|
178
|
+
ranked = caedral.rerank.create(
|
|
179
|
+
model="caedral-rerank",
|
|
180
|
+
query="billing and subscriptions",
|
|
181
|
+
documents=[
|
|
182
|
+
"Caedral pricing tiers include Starter and Pro.",
|
|
183
|
+
"The API gateway runs on port 5001 in local dev.",
|
|
184
|
+
],
|
|
185
|
+
top_n=2,
|
|
186
|
+
)
|
|
187
|
+
for item in ranked.results:
|
|
188
|
+
print(item.index, item.relevance_score)
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## Error handling
|
|
192
|
+
|
|
193
|
+
```python
|
|
194
|
+
from caedral import Caedral, CaedralAPIError
|
|
195
|
+
|
|
196
|
+
try:
|
|
197
|
+
caedral.chat.completions.create(
|
|
198
|
+
model="caedral-base",
|
|
199
|
+
messages=[{"role": "user", "content": "Hi"}],
|
|
200
|
+
)
|
|
201
|
+
except CaedralAPIError as err:
|
|
202
|
+
print(err.status_code, err.type, err.message)
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## Async client
|
|
206
|
+
|
|
207
|
+
`AsyncCaedral` is planned as a fast-follow. The synchronous client covers all endpoints today.
|
|
208
|
+
|
|
209
|
+
## Integration tests
|
|
210
|
+
|
|
211
|
+
Requires a running local gateway (`http://localhost:5001`) and `DATABASE_URL` in the repo root `.env` (tests create a temporary API key automatically).
|
|
212
|
+
|
|
213
|
+
```bash
|
|
214
|
+
cd sdk-python
|
|
215
|
+
pip install -e ".[dev]"
|
|
216
|
+
pytest -v
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Optional environment variables:
|
|
220
|
+
|
|
221
|
+
| Variable | Description |
|
|
222
|
+
|----------|-------------|
|
|
223
|
+
| `CAEDRAL_BASE_URL` | Gateway URL (default `http://localhost:5001`) |
|
|
224
|
+
| `CAEDRAL_TEST_API_KEY` | Skip auto key creation and use an existing key |
|
|
225
|
+
|
|
226
|
+
## License
|
|
227
|
+
|
|
228
|
+
MIT
|
caedral-0.1.0/README.md
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# Caedral Python SDK
|
|
2
|
+
|
|
3
|
+
Official Python client for the [Caedral API](https://caedral.com). OpenAI-compatible request shapes — point your existing code at Caedral with minimal changes.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install caedral
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
### Local development (editable install)
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
cd sdk-python
|
|
15
|
+
python -m venv .venv
|
|
16
|
+
source .venv/bin/activate # Windows: .venv\Scripts\activate
|
|
17
|
+
pip install -e ".[dev]"
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Quickstart
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
from caedral import Caedral
|
|
24
|
+
|
|
25
|
+
caedral = Caedral(
|
|
26
|
+
api_key="cd_live_...",
|
|
27
|
+
base_url="http://localhost:5001", # local API gateway
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
completion = caedral.chat.completions.create(
|
|
31
|
+
model="caedral-titan",
|
|
32
|
+
messages=[{"role": "user", "content": "Hello!"}],
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
print(completion.choices[0].message["content"])
|
|
36
|
+
caedral.close()
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Or use a context manager:
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
with Caedral(api_key="cd_live_...", base_url="http://localhost:5001") as caedral:
|
|
43
|
+
usage = caedral.usage.get()
|
|
44
|
+
print(usage.weeklyPool.remaining)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Production default base URL: `https://api.caedral.com`.
|
|
48
|
+
|
|
49
|
+
## Configuration
|
|
50
|
+
|
|
51
|
+
| Parameter | Default | Description |
|
|
52
|
+
|-----------|---------|-------------|
|
|
53
|
+
| `api_key` | — | Required. Your `cd_live_...` API key |
|
|
54
|
+
| `base_url` | `https://api.caedral.com` | API gateway base URL |
|
|
55
|
+
| `max_retries` | `3` | Retries for idempotent GET requests (exponential backoff) |
|
|
56
|
+
| `timeout` | `120.0` | Request timeout in seconds |
|
|
57
|
+
|
|
58
|
+
## Methods
|
|
59
|
+
|
|
60
|
+
### `caedral.chat.completions.create(...)`
|
|
61
|
+
|
|
62
|
+
OpenAI-compatible chat completions.
|
|
63
|
+
|
|
64
|
+
**Non-streaming:**
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
response = caedral.chat.completions.create(
|
|
68
|
+
model="caedral-olympus",
|
|
69
|
+
messages=[
|
|
70
|
+
{"role": "system", "content": "You are a helpful assistant."},
|
|
71
|
+
{"role": "user", "content": "Explain quantum computing briefly."},
|
|
72
|
+
],
|
|
73
|
+
temperature=0.7,
|
|
74
|
+
max_tokens=500,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
print(response.choices[0].message["content"])
|
|
78
|
+
print(response.usage.total_tokens if response.usage else None)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
**Streaming** (generator):
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
stream = caedral.chat.completions.create(
|
|
85
|
+
model="caedral-titan",
|
|
86
|
+
messages=[{"role": "user", "content": "Write a haiku about code."}],
|
|
87
|
+
stream=True,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
for chunk in stream:
|
|
91
|
+
delta = chunk.choices[0].delta.get("content")
|
|
92
|
+
if delta:
|
|
93
|
+
print(delta, end="", flush=True)
|
|
94
|
+
print()
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Models: `caedral-base`, `caedral-titan`, `caedral-olympus`, `caedral-primordial`.
|
|
98
|
+
|
|
99
|
+
### `caedral.models.list()`
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
models = caedral.models.list()
|
|
103
|
+
for model in models.data:
|
|
104
|
+
print(model.id, model.name, model.pricing_tier)
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### `caedral.usage.get()`
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
usage = caedral.usage.get()
|
|
111
|
+
print("Pool remaining:", usage.weeklyPool.remaining)
|
|
112
|
+
print("Balance (cents):", usage.balanceCents)
|
|
113
|
+
print("Overage used:", usage.overage.usedCents)
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### `caedral.embeddings.create(...)`
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
result = caedral.embeddings.create(
|
|
120
|
+
model="caedral-embed",
|
|
121
|
+
input="Caedral unifies frontier models behind one API.",
|
|
122
|
+
)
|
|
123
|
+
print(len(result.data[0].embedding))
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### `caedral.images.generate(...)`
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
image = caedral.images.generate(
|
|
130
|
+
model="caedral-vision",
|
|
131
|
+
prompt="A minimal geometric logo on a dark background",
|
|
132
|
+
)
|
|
133
|
+
print(image.data[0].url or "b64 payload returned")
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### `caedral.audio.generate(...)`
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
audio = caedral.audio.generate(
|
|
140
|
+
model="caedral-voice",
|
|
141
|
+
input="Welcome to Caedral.",
|
|
142
|
+
voice="alloy",
|
|
143
|
+
)
|
|
144
|
+
print(audio.model)
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### `caedral.rerank.create(...)`
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
ranked = caedral.rerank.create(
|
|
151
|
+
model="caedral-rerank",
|
|
152
|
+
query="billing and subscriptions",
|
|
153
|
+
documents=[
|
|
154
|
+
"Caedral pricing tiers include Starter and Pro.",
|
|
155
|
+
"The API gateway runs on port 5001 in local dev.",
|
|
156
|
+
],
|
|
157
|
+
top_n=2,
|
|
158
|
+
)
|
|
159
|
+
for item in ranked.results:
|
|
160
|
+
print(item.index, item.relevance_score)
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## Error handling
|
|
164
|
+
|
|
165
|
+
```python
|
|
166
|
+
from caedral import Caedral, CaedralAPIError
|
|
167
|
+
|
|
168
|
+
try:
|
|
169
|
+
caedral.chat.completions.create(
|
|
170
|
+
model="caedral-base",
|
|
171
|
+
messages=[{"role": "user", "content": "Hi"}],
|
|
172
|
+
)
|
|
173
|
+
except CaedralAPIError as err:
|
|
174
|
+
print(err.status_code, err.type, err.message)
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## Async client
|
|
178
|
+
|
|
179
|
+
`AsyncCaedral` is planned as a fast-follow. The synchronous client covers all endpoints today.
|
|
180
|
+
|
|
181
|
+
## Integration tests
|
|
182
|
+
|
|
183
|
+
Requires a running local gateway (`http://localhost:5001`) and `DATABASE_URL` in the repo root `.env` (tests create a temporary API key automatically).
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
cd sdk-python
|
|
187
|
+
pip install -e ".[dev]"
|
|
188
|
+
pytest -v
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Optional environment variables:
|
|
192
|
+
|
|
193
|
+
| Variable | Description |
|
|
194
|
+
|----------|-------------|
|
|
195
|
+
| `CAEDRAL_BASE_URL` | Gateway URL (default `http://localhost:5001`) |
|
|
196
|
+
| `CAEDRAL_TEST_API_KEY` | Skip auto key creation and use an existing key |
|
|
197
|
+
|
|
198
|
+
## License
|
|
199
|
+
|
|
200
|
+
MIT
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "caedral"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Official Python client for the Caedral API"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "Caedral", email = "hello@caedral.com" }]
|
|
13
|
+
keywords = ["caedral", "llm", "api", "ai"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.10",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Typing :: Typed",
|
|
23
|
+
]
|
|
24
|
+
dependencies = [
|
|
25
|
+
"httpx>=0.27.0",
|
|
26
|
+
"pydantic>=2.0.0",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[project.optional-dependencies]
|
|
30
|
+
dev = [
|
|
31
|
+
"pytest>=8.0.0",
|
|
32
|
+
"bcrypt>=4.0.0",
|
|
33
|
+
"psycopg[binary]>=3.1.0",
|
|
34
|
+
"python-dotenv>=1.0.0",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
[tool.hatch.build.targets.wheel]
|
|
38
|
+
packages = ["src/caedral"]
|
|
39
|
+
|
|
40
|
+
[tool.hatch.build.targets.sdist]
|
|
41
|
+
include = ["src/caedral", "README.md", "tests"]
|
|
42
|
+
|
|
43
|
+
[tool.pytest.ini_options]
|
|
44
|
+
testpaths = ["tests"]
|
|
45
|
+
filterwarnings = ["ignore::DeprecationWarning"]
|
|
46
|
+
|
|
47
|
+
[project.urls]
|
|
48
|
+
Homepage = "https://caedral.com/docs/python"
|
|
49
|
+
Repository = "https://github.com/caedral/caedral-python"
|
|
50
|
+
Issues = "https://github.com/caedral/caedral-python/issues"
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from collections.abc import Iterator
|
|
5
|
+
from typing import Any, Callable, TypeVar
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
from caedral.errors import CaedralAPIError
|
|
10
|
+
|
|
11
|
+
T = TypeVar("T")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def iter_sse_json(
|
|
15
|
+
response: httpx.Response,
|
|
16
|
+
*,
|
|
17
|
+
model_factory: Callable[[dict[str, Any]], T],
|
|
18
|
+
) -> Iterator[T]:
|
|
19
|
+
"""Parse Server-Sent Events lines into JSON objects."""
|
|
20
|
+
if response.is_closed:
|
|
21
|
+
raise CaedralAPIError("Streaming response has no body", status_code=502)
|
|
22
|
+
|
|
23
|
+
buffer = ""
|
|
24
|
+
for chunk in response.iter_text():
|
|
25
|
+
buffer += chunk
|
|
26
|
+
while "\n" in buffer:
|
|
27
|
+
line, buffer = buffer.split("\n", 1)
|
|
28
|
+
parsed = _parse_sse_line(line.strip(), model_factory)
|
|
29
|
+
if parsed is not None:
|
|
30
|
+
yield parsed
|
|
31
|
+
|
|
32
|
+
trailing = buffer.strip()
|
|
33
|
+
if trailing:
|
|
34
|
+
parsed = _parse_sse_line(trailing, model_factory)
|
|
35
|
+
if parsed is not None:
|
|
36
|
+
yield parsed
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _parse_sse_line(
|
|
40
|
+
line: str,
|
|
41
|
+
model_factory: Callable[[dict[str, Any]], T],
|
|
42
|
+
) -> T | None:
|
|
43
|
+
if not line.startswith("data:"):
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
data = line[len("data:") :].strip()
|
|
47
|
+
if not data or data == "[DONE]":
|
|
48
|
+
return None
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
payload = json.loads(data)
|
|
52
|
+
except json.JSONDecodeError as exc:
|
|
53
|
+
raise CaedralAPIError(
|
|
54
|
+
"Failed to parse streaming response chunk",
|
|
55
|
+
status_code=502,
|
|
56
|
+
error_type="upstream_error",
|
|
57
|
+
) from exc
|
|
58
|
+
|
|
59
|
+
if not isinstance(payload, dict):
|
|
60
|
+
raise CaedralAPIError(
|
|
61
|
+
"Invalid streaming chunk payload",
|
|
62
|
+
status_code=502,
|
|
63
|
+
error_type="upstream_error",
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
return model_factory(payload)
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from caedral.http import HttpClient
|
|
6
|
+
from caedral.resources.audio import AudioResource
|
|
7
|
+
from caedral.resources.chat import ChatResource
|
|
8
|
+
from caedral.resources.embeddings import EmbeddingsResource
|
|
9
|
+
from caedral.resources.images import ImagesResource
|
|
10
|
+
from caedral.resources.models import ModelsResource
|
|
11
|
+
from caedral.resources.rerank import RerankResource
|
|
12
|
+
from caedral.resources.usage import UsageResource
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Caedral:
|
|
16
|
+
"""Official synchronous Python client for the Caedral API."""
|
|
17
|
+
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
api_key: str,
|
|
21
|
+
*,
|
|
22
|
+
base_url: str = "https://api.caedral.com",
|
|
23
|
+
max_retries: int = 3,
|
|
24
|
+
timeout: float = 120.0,
|
|
25
|
+
**_: Any,
|
|
26
|
+
) -> None:
|
|
27
|
+
"""Create a new Caedral client.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
api_key: Caedral API key used to authenticate every request.
|
|
31
|
+
Must be a non-empty, non-blank string.
|
|
32
|
+
base_url: Base URL of the Caedral API gateway. Defaults to
|
|
33
|
+
the production endpoint; use ``http://localhost:5001``
|
|
34
|
+
for local development.
|
|
35
|
+
max_retries: Maximum number of automatic retries for
|
|
36
|
+
idempotent (GET) requests. Defaults to ``3``.
|
|
37
|
+
timeout: Per-request timeout in seconds. Defaults to
|
|
38
|
+
``120.0``.
|
|
39
|
+
|
|
40
|
+
Raises:
|
|
41
|
+
ValueError: If ``api_key`` is missing or blank.
|
|
42
|
+
"""
|
|
43
|
+
if not api_key or not api_key.strip():
|
|
44
|
+
raise ValueError("Caedral: api_key is required")
|
|
45
|
+
|
|
46
|
+
self._http = HttpClient(
|
|
47
|
+
api_key=api_key.strip(),
|
|
48
|
+
base_url=base_url,
|
|
49
|
+
max_retries=max_retries,
|
|
50
|
+
timeout=timeout,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
self.chat = ChatResource(self._http)
|
|
54
|
+
self.models = ModelsResource(self._http)
|
|
55
|
+
self.usage = UsageResource(self._http)
|
|
56
|
+
self.embeddings = EmbeddingsResource(self._http)
|
|
57
|
+
self.images = ImagesResource(self._http)
|
|
58
|
+
self.audio = AudioResource(self._http)
|
|
59
|
+
self.rerank = RerankResource(self._http)
|
|
60
|
+
|
|
61
|
+
def close(self) -> None:
|
|
62
|
+
self._http.close()
|
|
63
|
+
|
|
64
|
+
def __enter__(self) -> Caedral:
|
|
65
|
+
return self
|
|
66
|
+
|
|
67
|
+
def __exit__(self, *args: object) -> None:
|
|
68
|
+
self.close()
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class CaedralAPIError(Exception):
|
|
7
|
+
"""Raised when the Caedral API returns an error response."""
|
|
8
|
+
|
|
9
|
+
def __init__(
|
|
10
|
+
self,
|
|
11
|
+
message: str,
|
|
12
|
+
*,
|
|
13
|
+
status_code: int = 0,
|
|
14
|
+
error_type: str = "unknown",
|
|
15
|
+
raw_body: Any | None = None,
|
|
16
|
+
) -> None:
|
|
17
|
+
super().__init__(message)
|
|
18
|
+
self.message = message
|
|
19
|
+
self.status_code = status_code
|
|
20
|
+
self.type = error_type
|
|
21
|
+
self.raw_body = raw_body
|
|
22
|
+
|
|
23
|
+
@classmethod
|
|
24
|
+
def from_response(cls, status_code: int, body: Any) -> CaedralAPIError:
|
|
25
|
+
if isinstance(body, dict) and isinstance(body.get("error"), dict):
|
|
26
|
+
error = body["error"]
|
|
27
|
+
message = error.get("message") or f"Request failed with status {status_code}"
|
|
28
|
+
return cls(
|
|
29
|
+
message,
|
|
30
|
+
status_code=error.get("code") or status_code,
|
|
31
|
+
error_type=error.get("type") or "unknown",
|
|
32
|
+
raw_body=body,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
if isinstance(body, dict) and isinstance(body.get("message"), str):
|
|
36
|
+
return cls(body["message"], status_code=status_code, raw_body=body)
|
|
37
|
+
|
|
38
|
+
if isinstance(body, str) and body.strip():
|
|
39
|
+
return cls(body, status_code=status_code, raw_body=body)
|
|
40
|
+
|
|
41
|
+
return cls(
|
|
42
|
+
f"Request failed with status {status_code}",
|
|
43
|
+
status_code=status_code,
|
|
44
|
+
raw_body=body,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
def __repr__(self) -> str:
|
|
48
|
+
return (
|
|
49
|
+
f"CaedralAPIError(message={self.message!r}, "
|
|
50
|
+
f"status_code={self.status_code}, type={self.type!r})"
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class CaedralNetworkError(CaedralAPIError):
|
|
55
|
+
"""Raised on network failures or timeouts."""
|
|
56
|
+
|
|
57
|
+
def __init__(self, message: str, *, cause: BaseException | None = None) -> None:
|
|
58
|
+
super().__init__(message, status_code=0, error_type="network_error")
|
|
59
|
+
self.__cause__ = cause
|