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 +5 -0
- caedral/_sse.py +66 -0
- caedral/client.py +68 -0
- caedral/errors.py +59 -0
- caedral/http.py +141 -0
- caedral/resources/__init__.py +17 -0
- caedral/resources/audio.py +45 -0
- caedral/resources/chat.py +125 -0
- caedral/resources/embeddings.py +41 -0
- caedral/resources/images.py +42 -0
- caedral/resources/models.py +24 -0
- caedral/resources/rerank.py +52 -0
- caedral/resources/usage.py +25 -0
- caedral/types.py +129 -0
- caedral-0.1.0.dist-info/METADATA +228 -0
- caedral-0.1.0.dist-info/RECORD +17 -0
- caedral-0.1.0.dist-info/WHEEL +4 -0
caedral/__init__.py
ADDED
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,,
|