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.
@@ -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
@@ -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,5 @@
1
+ from caedral.client import Caedral
2
+ from caedral.errors import CaedralAPIError, CaedralNetworkError
3
+
4
+ __all__ = ["Caedral", "CaedralAPIError", "CaedralNetworkError"]
5
+ __version__ = "0.1.0"
@@ -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