caedral 0.1.0__py3-none-any.whl

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/__init__.py ADDED
@@ -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"
caedral/_sse.py ADDED
@@ -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)
caedral/client.py ADDED
@@ -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()
caedral/errors.py ADDED
@@ -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
caedral/http.py ADDED
@@ -0,0 +1,141 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import time
5
+ from typing import Any
6
+
7
+ import httpx
8
+
9
+ from caedral.errors import CaedralAPIError, CaedralNetworkError
10
+
11
+ DEFAULT_BASE_URL = "https://api.caedral.com"
12
+ DEFAULT_MAX_RETRIES = 3
13
+ DEFAULT_TIMEOUT = 120.0
14
+
15
+
16
+ class HttpClient:
17
+ def __init__(
18
+ self,
19
+ *,
20
+ api_key: str,
21
+ base_url: str | None = None,
22
+ max_retries: int = DEFAULT_MAX_RETRIES,
23
+ timeout: float = DEFAULT_TIMEOUT,
24
+ client: httpx.Client | None = None,
25
+ ) -> None:
26
+ self.api_key = api_key
27
+ self.base_url = (base_url or DEFAULT_BASE_URL).rstrip("/")
28
+ self.max_retries = max_retries
29
+ self.timeout = timeout
30
+ self._owns_client = client is None
31
+ self._client = client or httpx.Client(timeout=timeout)
32
+
33
+ def close(self) -> None:
34
+ if self._owns_client:
35
+ self._client.close()
36
+
37
+ def __enter__(self) -> HttpClient:
38
+ return self
39
+
40
+ def __exit__(self, *args: object) -> None:
41
+ self.close()
42
+
43
+ def get(self, path: str) -> Any:
44
+ return self._request_with_retry("GET", path)
45
+
46
+ def post_json(self, path: str, body: dict[str, Any]) -> Any:
47
+ return self._request("POST", path, json_body=body)
48
+
49
+ def post_stream(self, path: str, body: dict[str, Any]) -> httpx.Response:
50
+ request = self._client.build_request(
51
+ "POST",
52
+ f"{self.base_url}{path}",
53
+ headers={
54
+ "Authorization": f"Bearer {self.api_key}",
55
+ "Content-Type": "application/json",
56
+ },
57
+ json=body,
58
+ )
59
+ try:
60
+ return self._client.send(request, stream=True)
61
+ except httpx.TimeoutException as exc:
62
+ raise CaedralNetworkError(
63
+ f"Request timed out after {self.timeout}s",
64
+ cause=exc,
65
+ ) from exc
66
+ except httpx.HTTPError as exc:
67
+ raise CaedralNetworkError(str(exc) or "Network request failed", cause=exc) from exc
68
+
69
+ def _request_with_retry(self, method: str, path: str) -> Any:
70
+ last_error: Exception | None = None
71
+ for attempt in range(self.max_retries + 1):
72
+ try:
73
+ return self._request(method, path)
74
+ except Exception as exc:
75
+ last_error = exc
76
+ if not self._should_retry(exc, attempt):
77
+ raise
78
+ time.sleep(self._backoff_ms(attempt) / 1000)
79
+ assert last_error is not None
80
+ raise last_error
81
+
82
+ def _should_retry(self, err: Exception, attempt: int) -> bool:
83
+ if attempt >= self.max_retries:
84
+ return False
85
+ if isinstance(err, CaedralNetworkError):
86
+ return True
87
+ if isinstance(err, CaedralAPIError):
88
+ return err.status_code in (502, 503)
89
+ return False
90
+
91
+ @staticmethod
92
+ def _backoff_ms(attempt: int) -> int:
93
+ return 100 * (2**attempt)
94
+
95
+ def _request(
96
+ self,
97
+ method: str,
98
+ path: str,
99
+ *,
100
+ json_body: dict[str, Any] | None = None,
101
+ ) -> Any:
102
+ response = self._request_raw(method, path, json_body=json_body)
103
+ text = response.text
104
+ parsed = _safe_json_parse(text) if text else None
105
+ if response.status_code >= 400:
106
+ raise CaedralAPIError.from_response(response.status_code, parsed or text)
107
+ return parsed
108
+
109
+ def _request_raw(
110
+ self,
111
+ method: str,
112
+ path: str,
113
+ *,
114
+ json_body: dict[str, Any] | None = None,
115
+ ) -> httpx.Response:
116
+ headers = {"Authorization": f"Bearer {self.api_key}"}
117
+ if json_body is not None:
118
+ headers["Content-Type"] = "application/json"
119
+
120
+ try:
121
+ return self._client.request(
122
+ method,
123
+ f"{self.base_url}{path}",
124
+ headers=headers,
125
+ json=json_body,
126
+ timeout=self.timeout,
127
+ )
128
+ except httpx.TimeoutException as exc:
129
+ raise CaedralNetworkError(
130
+ f"Request timed out after {self.timeout}s",
131
+ cause=exc,
132
+ ) from exc
133
+ except httpx.HTTPError as exc:
134
+ raise CaedralNetworkError(str(exc) or "Network request failed", cause=exc) from exc
135
+
136
+
137
+ def _safe_json_parse(text: str) -> Any:
138
+ try:
139
+ return json.loads(text)
140
+ except json.JSONDecodeError:
141
+ return text
@@ -0,0 +1,17 @@
1
+ from __future__ import annotations
2
+
3
+ from caedral.resources.chat import ChatResource
4
+ from caedral.resources.embeddings import EmbeddingsResource
5
+ from caedral.resources.images import ImagesResource
6
+ from caedral.resources.models import ModelsResource
7
+ from caedral.resources.rerank import RerankResource
8
+ from caedral.resources.usage import UsageResource
9
+
10
+ __all__ = [
11
+ "ChatResource",
12
+ "ModelsResource",
13
+ "UsageResource",
14
+ "EmbeddingsResource",
15
+ "ImagesResource",
16
+ "RerankResource",
17
+ ]
@@ -0,0 +1,45 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from caedral.http import HttpClient
6
+ from caedral.types import AudioGenerateResponse
7
+
8
+
9
+ class AudioResource:
10
+ """Audio (text-to-speech) endpoint (``POST /v1/audio/speech``)."""
11
+
12
+ def __init__(self, http: HttpClient) -> None:
13
+ self._http = http
14
+
15
+ def generate(
16
+ self,
17
+ *,
18
+ input: str,
19
+ model: str | None = None,
20
+ voice: str | None = None,
21
+ **kwargs: Any,
22
+ ) -> AudioGenerateResponse:
23
+ """Synthesize speech audio from an input text string.
24
+
25
+ Args:
26
+ input: Text to convert to speech.
27
+ model: Optional model identifier. When omitted, the API
28
+ selects a default speech model.
29
+ voice: Optional voice identifier or style hint.
30
+ **kwargs: Additional request fields forwarded to the API.
31
+
32
+ Returns:
33
+ An :class:`AudioGenerateResponse` containing the generated
34
+ audio payload.
35
+
36
+ Raises:
37
+ CaedralAPIError: If the API returns a non-2xx response.
38
+ """
39
+ body: dict[str, Any] = {"input": input, **kwargs}
40
+ if model is not None:
41
+ body["model"] = model
42
+ if voice is not None:
43
+ body["voice"] = voice
44
+ data = self._http.post_json("/v1/audio/speech", body)
45
+ return AudioGenerateResponse.model_validate(data)
@@ -0,0 +1,125 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Iterator
4
+ from typing import Any, Literal, overload
5
+
6
+ from caedral._sse import iter_sse_json
7
+ from caedral.errors import CaedralAPIError
8
+ from caedral.http import HttpClient
9
+ from caedral.types import ChatCompletion, ChatCompletionChunk
10
+
11
+
12
+ class ChatCompletions:
13
+ """Chat completions endpoint (``POST /v1/chat/completions``).
14
+
15
+ Supports both buffered and Server-Sent Events (SSE) streaming
16
+ responses; the return type of :meth:`create` depends on the
17
+ ``stream`` flag.
18
+ """
19
+
20
+ def __init__(self, http: HttpClient) -> None:
21
+ self._http = http
22
+
23
+ @overload
24
+ def create(
25
+ self,
26
+ *,
27
+ model: str,
28
+ messages: list[dict[str, Any]],
29
+ stream: Literal[False] = False,
30
+ **kwargs: Any,
31
+ ) -> ChatCompletion: ...
32
+
33
+ @overload
34
+ def create(
35
+ self,
36
+ *,
37
+ model: str,
38
+ messages: list[dict[str, Any]],
39
+ stream: Literal[True],
40
+ **kwargs: Any,
41
+ ) -> Iterator[ChatCompletionChunk]: ...
42
+
43
+ def create(
44
+ self,
45
+ *,
46
+ model: str,
47
+ messages: list[dict[str, Any]],
48
+ stream: bool = False,
49
+ **kwargs: Any,
50
+ ) -> ChatCompletion | Iterator[ChatCompletionChunk]:
51
+ """Create a chat completion.
52
+
53
+ Args:
54
+ model: Model identifier (for example ``"caedral-base"``).
55
+ messages: Ordered list of chat messages in OpenAI-style
56
+ ``{"role": ..., "content": ...}`` format.
57
+ stream: When ``False`` (default), the full completion is
58
+ returned once ready. When ``True``, an iterator of
59
+ incremental ``ChatCompletionChunk`` deltas is returned
60
+ (SSE).
61
+ **kwargs: Additional request fields (``temperature``,
62
+ ``max_tokens``, ``top_p``, ``stop``, ``user``, etc.)
63
+ forwarded to the API.
64
+
65
+ Returns:
66
+ A :class:`ChatCompletion` when not streaming, or an
67
+ :class:`Iterator` of :class:`ChatCompletionChunk` when
68
+ ``stream=True``.
69
+
70
+ Raises:
71
+ CaedralAPIError: If the API returns a non-2xx response.
72
+
73
+ Example:
74
+ Streaming::
75
+
76
+ stream = client.chat.completions.create(
77
+ model="caedral-base",
78
+ messages=[{"role": "user", "content": "Hi"}],
79
+ stream=True,
80
+ )
81
+ for chunk in stream:
82
+ delta = chunk.choices[0].delta.content
83
+ if delta:
84
+ print(delta, end="", flush=True)
85
+ """
86
+ body: dict[str, Any] = {
87
+ "model": model,
88
+ "messages": messages,
89
+ "stream": stream,
90
+ **kwargs,
91
+ }
92
+
93
+ if stream:
94
+ return self._create_stream(body)
95
+ data = self._http.post_json("/v1/chat/completions", body)
96
+ return ChatCompletion.model_validate(data)
97
+
98
+ def _create_stream(self, body: dict[str, Any]) -> Iterator[ChatCompletionChunk]:
99
+ response = self._http.post_stream("/v1/chat/completions", body)
100
+ try:
101
+ if response.status_code >= 400:
102
+ error_body: Any
103
+ try:
104
+ error_body = response.json()
105
+ except Exception:
106
+ error_body = response.read().decode("utf-8", errors="replace")
107
+ raise CaedralAPIError.from_response(response.status_code, error_body)
108
+
109
+ yield from iter_sse_json(
110
+ response,
111
+ model_factory=ChatCompletionChunk.model_validate,
112
+ )
113
+ finally:
114
+ response.close()
115
+
116
+
117
+ class ChatResource:
118
+ """Namespace grouping chat-related endpoints.
119
+
120
+ Currently exposes :attr:`completions` for
121
+ ``POST /v1/chat/completions``.
122
+ """
123
+
124
+ def __init__(self, http: HttpClient) -> None:
125
+ self.completions = ChatCompletions(http)
@@ -0,0 +1,41 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from caedral.http import HttpClient
6
+ from caedral.types import EmbeddingCreateResponse
7
+
8
+
9
+ class EmbeddingsResource:
10
+ """Text embeddings endpoint (``POST /v1/embeddings``)."""
11
+
12
+ def __init__(self, http: HttpClient) -> None:
13
+ self._http = http
14
+
15
+ def create(
16
+ self,
17
+ *,
18
+ model: str,
19
+ input: str | list[str],
20
+ **kwargs: Any,
21
+ ) -> EmbeddingCreateResponse:
22
+ """Generate dense vector embeddings for one or more inputs.
23
+
24
+ Args:
25
+ model: Embedding model identifier (for example
26
+ ``"caedral-embed"``).
27
+ input: A single string or a list of strings to embed.
28
+ When a list is provided, the response returns one
29
+ vector per input in the same order.
30
+ **kwargs: Additional request fields forwarded to the API.
31
+
32
+ Returns:
33
+ An :class:`EmbeddingCreateResponse` containing the
34
+ generated vectors and token usage.
35
+
36
+ Raises:
37
+ CaedralAPIError: If the API returns a non-2xx response.
38
+ """
39
+ body = {"model": model, "input": input, **kwargs}
40
+ data = self._http.post_json("/v1/embeddings", body)
41
+ return EmbeddingCreateResponse.model_validate(data)
@@ -0,0 +1,42 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from caedral.http import HttpClient
6
+ from caedral.types import ImageGenerateResponse
7
+
8
+
9
+ class ImagesResource:
10
+ """Image generation endpoint (``POST /v1/images/generations``)."""
11
+
12
+ def __init__(self, http: HttpClient) -> None:
13
+ self._http = http
14
+
15
+ def generate(
16
+ self,
17
+ *,
18
+ prompt: str,
19
+ model: str | None = None,
20
+ **kwargs: Any,
21
+ ) -> ImageGenerateResponse:
22
+ """Generate one or more images from a text prompt.
23
+
24
+ Args:
25
+ prompt: Natural-language description of the desired image.
26
+ model: Optional model identifier. When omitted, the API
27
+ selects a default image model.
28
+ **kwargs: Additional request fields such as ``n`` (number
29
+ of images) or ``size`` (output resolution).
30
+
31
+ Returns:
32
+ An :class:`ImageGenerateResponse` containing the generated
33
+ images as URLs or base64-encoded data.
34
+
35
+ Raises:
36
+ CaedralAPIError: If the API returns a non-2xx response.
37
+ """
38
+ body: dict[str, Any] = {"prompt": prompt, **kwargs}
39
+ if model is not None:
40
+ body["model"] = model
41
+ data = self._http.post_json("/v1/images/generations", body)
42
+ return ImageGenerateResponse.model_validate(data)
@@ -0,0 +1,24 @@
1
+ from __future__ import annotations
2
+
3
+ from caedral.http import HttpClient
4
+ from caedral.types import ModelListResponse
5
+
6
+
7
+ class ModelsResource:
8
+ """Model catalog endpoint (``GET /v1/models``)."""
9
+
10
+ def __init__(self, http: HttpClient) -> None:
11
+ self._http = http
12
+
13
+ def list(self) -> ModelListResponse:
14
+ """List every model available to the authenticated account.
15
+
16
+ Returns:
17
+ A :class:`ModelListResponse` describing each model along
18
+ with its context window and pricing tier.
19
+
20
+ Raises:
21
+ CaedralAPIError: If the API returns a non-2xx response.
22
+ """
23
+ data = self._http.get("/v1/models")
24
+ return ModelListResponse.model_validate(data)
@@ -0,0 +1,52 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from caedral.http import HttpClient
6
+ from caedral.types import RerankCreateResponse
7
+
8
+
9
+ class RerankResource:
10
+ """Document rerank endpoint (``POST /v1/rerank``)."""
11
+
12
+ def __init__(self, http: HttpClient) -> None:
13
+ self._http = http
14
+
15
+ def create(
16
+ self,
17
+ *,
18
+ query: str,
19
+ documents: list[str],
20
+ model: str | None = None,
21
+ top_n: int | None = None,
22
+ **kwargs: Any,
23
+ ) -> RerankCreateResponse:
24
+ """Reorder documents by semantic relevance to a query.
25
+
26
+ Args:
27
+ query: Query text used to score the documents.
28
+ documents: List of candidate documents to rerank.
29
+ model: Optional model identifier. When omitted, the API
30
+ selects a default rerank model.
31
+ top_n: Optional cap on the number of results returned,
32
+ keeping only the top-N most relevant documents.
33
+ **kwargs: Additional request fields forwarded to the API.
34
+
35
+ Returns:
36
+ A :class:`RerankCreateResponse` with relevance-scored
37
+ results ordered from most to least relevant.
38
+
39
+ Raises:
40
+ CaedralAPIError: If the API returns a non-2xx response.
41
+ """
42
+ body: dict[str, Any] = {
43
+ "query": query,
44
+ "documents": documents,
45
+ **kwargs,
46
+ }
47
+ if model is not None:
48
+ body["model"] = model
49
+ if top_n is not None:
50
+ body["top_n"] = top_n
51
+ data = self._http.post_json("/v1/rerank", body)
52
+ return RerankCreateResponse.model_validate(data)
@@ -0,0 +1,25 @@
1
+ from __future__ import annotations
2
+
3
+ from caedral.http import HttpClient
4
+ from caedral.types import UsageSummary
5
+
6
+
7
+ class UsageResource:
8
+ """Account usage endpoint (``GET /v1/usage``)."""
9
+
10
+ def __init__(self, http: HttpClient) -> None:
11
+ self._http = http
12
+
13
+ def get(self) -> UsageSummary:
14
+ """Fetch a snapshot of the account's current billing state.
15
+
16
+ Returns:
17
+ A :class:`UsageSummary` describing the current plan,
18
+ weekly free pool utilization, prepaid balance, and
19
+ overage limits.
20
+
21
+ Raises:
22
+ CaedralAPIError: If the API returns a non-2xx response.
23
+ """
24
+ data = self._http.get("/v1/usage")
25
+ return UsageSummary.model_validate(data)
caedral/types.py ADDED
@@ -0,0 +1,129 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Literal
4
+
5
+ from pydantic import BaseModel, ConfigDict, Field
6
+
7
+
8
+ class CaedralBaseModel(BaseModel):
9
+ model_config = ConfigDict(extra="allow")
10
+
11
+
12
+ class ChatMessageParam(CaedralBaseModel):
13
+ role: Literal["system", "user", "assistant", "tool"]
14
+ content: str | None
15
+ name: str | None = None
16
+
17
+
18
+ class ChatCompletionChoice(CaedralBaseModel):
19
+ index: int
20
+ message: dict[str, Any]
21
+ finish_reason: str | None = None
22
+
23
+
24
+ class CompletionUsage(CaedralBaseModel):
25
+ prompt_tokens: int = 0
26
+ completion_tokens: int = 0
27
+ total_tokens: int = 0
28
+
29
+
30
+ class ChatCompletion(CaedralBaseModel):
31
+ id: str
32
+ object: Literal["chat.completion"] = "chat.completion"
33
+ created: int
34
+ model: str
35
+ choices: list[ChatCompletionChoice]
36
+ usage: CompletionUsage | None = None
37
+
38
+
39
+ class ChatCompletionChunkChoice(CaedralBaseModel):
40
+ index: int
41
+ delta: dict[str, Any] = Field(default_factory=dict)
42
+ finish_reason: str | None = None
43
+
44
+
45
+ class ChatCompletionChunk(CaedralBaseModel):
46
+ id: str
47
+ object: Literal["chat.completion.chunk"] = "chat.completion.chunk"
48
+ created: int
49
+ model: str
50
+ choices: list[ChatCompletionChunkChoice]
51
+
52
+
53
+ class Model(CaedralBaseModel):
54
+ id: str
55
+ object: Literal["model"] = "model"
56
+ created: int
57
+ owned_by: str
58
+ name: str
59
+ description: str
60
+ context_window: int
61
+ pricing_tier: str
62
+
63
+
64
+ class ModelListResponse(CaedralBaseModel):
65
+ object: Literal["list"] = "list"
66
+ data: list[Model]
67
+
68
+
69
+ class WeeklyPool(CaedralBaseModel):
70
+ limit: int
71
+ used: int
72
+ remaining: int
73
+
74
+
75
+ class OverageSummary(CaedralBaseModel):
76
+ enabled: bool
77
+ limitCents: int | None = None
78
+ usedCents: int
79
+ remainingCents: int | None = None
80
+
81
+
82
+ class UsageSummary(CaedralBaseModel):
83
+ accountStatus: str
84
+ plan: str
85
+ planStatus: str
86
+ balanceCents: int
87
+ weeklyPool: WeeklyPool
88
+ overage: OverageSummary
89
+ balanceWeightedUnitsAffordable: int
90
+
91
+
92
+ class EmbeddingData(CaedralBaseModel):
93
+ object: str
94
+ embedding: list[float]
95
+ index: int
96
+
97
+
98
+ class EmbeddingCreateResponse(CaedralBaseModel):
99
+ object: str
100
+ model: str
101
+ data: list[EmbeddingData]
102
+ usage: CompletionUsage | None = None
103
+
104
+
105
+ class ImageData(CaedralBaseModel):
106
+ url: str | None = None
107
+ b64_json: str | None = None
108
+
109
+
110
+ class ImageGenerateResponse(CaedralBaseModel):
111
+ model: str
112
+ data: list[ImageData]
113
+ usage: CompletionUsage | None = None
114
+
115
+
116
+ class AudioGenerateResponse(CaedralBaseModel):
117
+ model: str
118
+ choices: list[dict[str, Any]] | None = None
119
+ usage: CompletionUsage | None = None
120
+
121
+
122
+ class RerankResult(CaedralBaseModel):
123
+ index: int
124
+ relevance_score: float
125
+
126
+
127
+ class RerankCreateResponse(CaedralBaseModel):
128
+ model: str
129
+ results: list[RerankResult]
@@ -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,17 @@
1
+ caedral/__init__.py,sha256=ItiwPrxYwEKwBDdPK1TuQov26i_e5bC7Bjm7865fQV8,186
2
+ caedral/_sse.py,sha256=EbGoDY0IfnR1UoGXnvVVBhDxgXqUly_DoBXmbv8gL90,1699
3
+ caedral/client.py,sha256=eeeSry_EALqlwaJUfLAIVseXNFKz0VerU8bwMtOW9pQ,2240
4
+ caedral/errors.py,sha256=EEUUA833txqKsIHoqWW4Pye-M9A8q_cYfIkmDBTrS4A,1925
5
+ caedral/http.py,sha256=NR-Kfd46V1fS_XHeeeIa2gpswB9D3VlyWgZ23koMpu4,4402
6
+ caedral/types.py,sha256=FouFDi9LtGYcRRGH1SbKRt-3Sl727owa9m_5Uja6NvA,2767
7
+ caedral/resources/__init__.py,sha256=0DxdOWoEjotF0oF9y3ddJfZpzi9Doh9cu-hyIhPDKxY,498
8
+ caedral/resources/audio.py,sha256=hub8oWviWMu_Y_bSICDdzh_N-Rbd6bSor_7laVZEMzQ,1390
9
+ caedral/resources/chat.py,sha256=apT7WGuvHLhvNg2Dnl4K5pQzsY3dM1m9j0F0Pzof0-g,3938
10
+ caedral/resources/embeddings.py,sha256=SCKYtTKrKp_4nVgZiLFj7L18ANeQaWI3ZMDMURmqcOc,1301
11
+ caedral/resources/images.py,sha256=90QKuQCj5eHEfCjcN27gSCKScIjinZryPKYX9TicSco,1347
12
+ caedral/resources/models.py,sha256=Ksx_54q5ZpnGW-TzpRto2yIB6jjV3M-vDzUVuEF_uQE,718
13
+ caedral/resources/rerank.py,sha256=-EjEhWtDn1_InCr_c0u1Z5BOo3cDFNWH0XyomvYMNoo,1640
14
+ caedral/resources/usage.py,sha256=zdx5sTT7qOCiIhf2vFBJrkJ3zCSQeg-1GdkYRbppUMI,732
15
+ caedral-0.1.0.dist-info/METADATA,sha256=eTUlnBeTGRQbwPqYX2QAFYEwQgLdy033FHJln2da89E,5679
16
+ caedral-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
17
+ caedral-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any