ciralgo 1.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.
- ciralgo-1.1.0/LICENSE +19 -0
- ciralgo-1.1.0/NOTICE +17 -0
- ciralgo-1.1.0/PKG-INFO +215 -0
- ciralgo-1.1.0/README.md +181 -0
- ciralgo-1.1.0/pyproject.toml +85 -0
- ciralgo-1.1.0/src/ciralgo/__init__.py +49 -0
- ciralgo-1.1.0/src/ciralgo/client.py +374 -0
- ciralgo-1.1.0/src/ciralgo/errors.py +86 -0
ciralgo-1.1.0/LICENSE
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
Copyright 2026 Ciralgo BV
|
|
6
|
+
|
|
7
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
8
|
+
you may not use this file except in compliance with the License.
|
|
9
|
+
You may obtain a copy of the License at
|
|
10
|
+
|
|
11
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
12
|
+
|
|
13
|
+
Unless required by applicable law or agreed to in writing, software
|
|
14
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
15
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
16
|
+
See the License for the specific language governing permissions and
|
|
17
|
+
limitations under the License.
|
|
18
|
+
|
|
19
|
+
The full Apache License 2.0 text is available at the URL above.
|
ciralgo-1.1.0/NOTICE
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
Ciralgo Python SDK
|
|
2
|
+
Copyright 2026 Ciralgo B.V.
|
|
3
|
+
|
|
4
|
+
This product is the official Python client for the Ciralgo Platform API
|
|
5
|
+
(https://www.ciralgo.com).
|
|
6
|
+
|
|
7
|
+
Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
8
|
+
not use this file except in compliance with the License. You may obtain
|
|
9
|
+
a copy of the License at:
|
|
10
|
+
|
|
11
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
12
|
+
|
|
13
|
+
Unless required by applicable law or agreed to in writing, software
|
|
14
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
15
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
16
|
+
See the License for the specific language governing permissions and
|
|
17
|
+
limitations under the License.
|
ciralgo-1.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ciralgo
|
|
3
|
+
Version: 1.1.0
|
|
4
|
+
Summary: Official Python SDK for the Ciralgo Platform API
|
|
5
|
+
Project-URL: Homepage, https://www.ciralgo.com
|
|
6
|
+
Project-URL: Documentation, https://docs.ciralgo.com/sdk-python
|
|
7
|
+
Project-URL: Repository, https://github.com/Ciralgo/ciralgo-python
|
|
8
|
+
Project-URL: Changelog, https://github.com/Ciralgo/ciralgo-python/blob/main/CHANGELOG.md
|
|
9
|
+
Project-URL: Issues, https://github.com/Ciralgo/ciralgo-python/issues
|
|
10
|
+
Author-email: Ciralgo <support@ciralgo.com>
|
|
11
|
+
License-Expression: Apache-2.0
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
License-File: NOTICE
|
|
14
|
+
Keywords: ai,anthropic,ciralgo,compliance,eu-sovereignty,llm,openai
|
|
15
|
+
Classifier: Development Status :: 4 - Beta
|
|
16
|
+
Classifier: Intended Audience :: Developers
|
|
17
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
24
|
+
Requires-Python: >=3.9
|
|
25
|
+
Requires-Dist: httpx<1.0,>=0.27
|
|
26
|
+
Requires-Dist: typing-extensions>=4.5; python_version < '3.11'
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
29
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
30
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
31
|
+
Requires-Dist: respx>=0.21; extra == 'dev'
|
|
32
|
+
Requires-Dist: ruff>=0.5; extra == 'dev'
|
|
33
|
+
Description-Content-Type: text/markdown
|
|
34
|
+
|
|
35
|
+
# Ciralgo Python SDK
|
|
36
|
+
|
|
37
|
+
Official Python SDK for the [Ciralgo Platform API](https://www.ciralgo.com).
|
|
38
|
+
|
|
39
|
+
Version: `1.1.0` (mirrors `openapi.json info.version`).
|
|
40
|
+
|
|
41
|
+
## Install
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pip install ciralgo
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Requires Python 3.9+.
|
|
48
|
+
|
|
49
|
+
## Quickstart
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from ciralgo import Client
|
|
53
|
+
|
|
54
|
+
client = Client(api_key="sk-cg-...") # or set CIRALGO_API_KEY in your env
|
|
55
|
+
|
|
56
|
+
response = client.chat.completions.create(
|
|
57
|
+
model="openai/gpt-4o-mini",
|
|
58
|
+
messages=[{"role": "user", "content": "Say hello."}],
|
|
59
|
+
)
|
|
60
|
+
print(response["choices"][0]["message"]["content"])
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Authentication
|
|
64
|
+
|
|
65
|
+
The SDK reads the API key from (in order):
|
|
66
|
+
|
|
67
|
+
1. The `api_key=` argument to `Client(...)` or `AsyncClient(...)`.
|
|
68
|
+
2. The `CIRALGO_API_KEY` environment variable.
|
|
69
|
+
|
|
70
|
+
If neither is set, `AuthenticationError` is raised at construction time.
|
|
71
|
+
|
|
72
|
+
The optional `CIRALGO_BASE_URL` env var overrides the default `https://api.ciralgo.com`. This is useful for staging or for self-hosted Ciralgo deployments.
|
|
73
|
+
|
|
74
|
+
## Endpoints
|
|
75
|
+
|
|
76
|
+
The four public proxy operations from the Ciralgo OpenAPI spec (v1.1.0):
|
|
77
|
+
|
|
78
|
+
### Chat completions
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
response = client.chat.completions.create(
|
|
82
|
+
model="anthropic/claude-sonnet-4-6",
|
|
83
|
+
messages=[
|
|
84
|
+
{"role": "system", "content": "You are a finance compliance assistant."},
|
|
85
|
+
{"role": "user", "content": "Summarise EU AI Act Article 15."},
|
|
86
|
+
],
|
|
87
|
+
temperature=0.2,
|
|
88
|
+
max_tokens=400,
|
|
89
|
+
tags={"project": "ai-act-summariser", "env": "prod"},
|
|
90
|
+
)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Streaming chat completions
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
for chunk in client.chat.completions.create(
|
|
97
|
+
model="openai/gpt-4o-mini",
|
|
98
|
+
messages=[{"role": "user", "content": "Stream me a haiku."}],
|
|
99
|
+
stream=True,
|
|
100
|
+
):
|
|
101
|
+
delta = chunk["choices"][0]["delta"].get("content", "")
|
|
102
|
+
print(delta, end="", flush=True)
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Embeddings
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
embedding = client.embeddings.create(
|
|
109
|
+
model="openai/text-embedding-3-small",
|
|
110
|
+
input="EU AI Act Article 15 covers accuracy, robustness and cybersecurity.",
|
|
111
|
+
)
|
|
112
|
+
print(len(embedding["data"][0]["embedding"])) # → vector dimension
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Usage
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
usage = client.usage.get(from_date="2026-06-01", to_date="2026-06-30")
|
|
119
|
+
print(usage["total_cost_usd"], usage["calls"])
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Anthropic Messages
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
response = client.anthropic.messages_create(
|
|
126
|
+
model="anthropic/claude-sonnet-4-6",
|
|
127
|
+
max_tokens=400,
|
|
128
|
+
system="You are an EU compliance assistant.",
|
|
129
|
+
messages=[{"role": "user", "content": "What is GDPR Article 32?"}],
|
|
130
|
+
)
|
|
131
|
+
print(response["content"][0]["text"])
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Error handling
|
|
135
|
+
|
|
136
|
+
The SDK maps HTTP status codes to typed exceptions. Catch the specific class:
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
from ciralgo import Client
|
|
140
|
+
from ciralgo.errors import RateLimitError, UpstreamError, AuthenticationError
|
|
141
|
+
|
|
142
|
+
try:
|
|
143
|
+
response = client.chat.completions.create(...)
|
|
144
|
+
except RateLimitError as e:
|
|
145
|
+
time.sleep(e.retry_after or 5)
|
|
146
|
+
# retry
|
|
147
|
+
except UpstreamError as e:
|
|
148
|
+
# upstream LLM provider 5xx, pick a different model
|
|
149
|
+
...
|
|
150
|
+
except AuthenticationError:
|
|
151
|
+
# rotate / re-issue the key
|
|
152
|
+
raise
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Every exception carries:
|
|
156
|
+
|
|
157
|
+
- `code`: stable string error code from the API envelope (e.g. `rate_limit_exceeded`)
|
|
158
|
+
- `message`: human-readable
|
|
159
|
+
- `trace_id`: pass this to Ciralgo support to look up the request server-side
|
|
160
|
+
- `status_code`: HTTP status
|
|
161
|
+
- `retry_after`: only set on 429
|
|
162
|
+
|
|
163
|
+
## Async
|
|
164
|
+
|
|
165
|
+
```python
|
|
166
|
+
import asyncio
|
|
167
|
+
from ciralgo import AsyncClient
|
|
168
|
+
|
|
169
|
+
async def main():
|
|
170
|
+
async with AsyncClient() as client:
|
|
171
|
+
# NOTE: async surface in v1.1.0 is the client construction +
|
|
172
|
+
# close lifecycle. Full async parity for chat / embeddings ships
|
|
173
|
+
# in v1.2.0.
|
|
174
|
+
pass
|
|
175
|
+
|
|
176
|
+
asyncio.run(main())
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## Migration to a codegen client
|
|
180
|
+
|
|
181
|
+
The current client is hand-written. A codegen-based replacement is
|
|
182
|
+
viable using [openapi-python-client](https://github.com/openapi-generators/openapi-python-client)
|
|
183
|
+
against the published Ciralgo OpenAPI spec. The hand-written client
|
|
184
|
+
gives us control over:
|
|
185
|
+
|
|
186
|
+
- Streaming semantics (`stream=True` returning an iterator).
|
|
187
|
+
- The `X-Ciralgo-Tags` header marshalling.
|
|
188
|
+
- The typed exception hierarchy (codegen produces a single error class).
|
|
189
|
+
|
|
190
|
+
A future major bump (`v2.0.0`) can switch to codegen if the trade-offs
|
|
191
|
+
change.
|
|
192
|
+
|
|
193
|
+
## Development
|
|
194
|
+
|
|
195
|
+
From the SDK repo root:
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
pip install -e ".[dev]"
|
|
199
|
+
pytest
|
|
200
|
+
ruff check src
|
|
201
|
+
mypy src
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## Publishing
|
|
205
|
+
|
|
206
|
+
Publishing to PyPI is handled by a GitHub Actions workflow triggered on
|
|
207
|
+
tags of the form `sdk-py-v*`. The workflow uses PyPI Trusted Publishing
|
|
208
|
+
(OIDC). No long-lived API token is stored in CI secrets.
|
|
209
|
+
|
|
210
|
+
The engineer-facing release runbook (version bump, tag push, environment
|
|
211
|
+
approval, troubleshooting) is at [docs/publish-runbook.md](docs/publish-runbook.md).
|
|
212
|
+
|
|
213
|
+
## License
|
|
214
|
+
|
|
215
|
+
Apache-2.0
|
ciralgo-1.1.0/README.md
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# Ciralgo Python SDK
|
|
2
|
+
|
|
3
|
+
Official Python SDK for the [Ciralgo Platform API](https://www.ciralgo.com).
|
|
4
|
+
|
|
5
|
+
Version: `1.1.0` (mirrors `openapi.json info.version`).
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install ciralgo
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Requires Python 3.9+.
|
|
14
|
+
|
|
15
|
+
## Quickstart
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
from ciralgo import Client
|
|
19
|
+
|
|
20
|
+
client = Client(api_key="sk-cg-...") # or set CIRALGO_API_KEY in your env
|
|
21
|
+
|
|
22
|
+
response = client.chat.completions.create(
|
|
23
|
+
model="openai/gpt-4o-mini",
|
|
24
|
+
messages=[{"role": "user", "content": "Say hello."}],
|
|
25
|
+
)
|
|
26
|
+
print(response["choices"][0]["message"]["content"])
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Authentication
|
|
30
|
+
|
|
31
|
+
The SDK reads the API key from (in order):
|
|
32
|
+
|
|
33
|
+
1. The `api_key=` argument to `Client(...)` or `AsyncClient(...)`.
|
|
34
|
+
2. The `CIRALGO_API_KEY` environment variable.
|
|
35
|
+
|
|
36
|
+
If neither is set, `AuthenticationError` is raised at construction time.
|
|
37
|
+
|
|
38
|
+
The optional `CIRALGO_BASE_URL` env var overrides the default `https://api.ciralgo.com`. This is useful for staging or for self-hosted Ciralgo deployments.
|
|
39
|
+
|
|
40
|
+
## Endpoints
|
|
41
|
+
|
|
42
|
+
The four public proxy operations from the Ciralgo OpenAPI spec (v1.1.0):
|
|
43
|
+
|
|
44
|
+
### Chat completions
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
response = client.chat.completions.create(
|
|
48
|
+
model="anthropic/claude-sonnet-4-6",
|
|
49
|
+
messages=[
|
|
50
|
+
{"role": "system", "content": "You are a finance compliance assistant."},
|
|
51
|
+
{"role": "user", "content": "Summarise EU AI Act Article 15."},
|
|
52
|
+
],
|
|
53
|
+
temperature=0.2,
|
|
54
|
+
max_tokens=400,
|
|
55
|
+
tags={"project": "ai-act-summariser", "env": "prod"},
|
|
56
|
+
)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Streaming chat completions
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
for chunk in client.chat.completions.create(
|
|
63
|
+
model="openai/gpt-4o-mini",
|
|
64
|
+
messages=[{"role": "user", "content": "Stream me a haiku."}],
|
|
65
|
+
stream=True,
|
|
66
|
+
):
|
|
67
|
+
delta = chunk["choices"][0]["delta"].get("content", "")
|
|
68
|
+
print(delta, end="", flush=True)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Embeddings
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
embedding = client.embeddings.create(
|
|
75
|
+
model="openai/text-embedding-3-small",
|
|
76
|
+
input="EU AI Act Article 15 covers accuracy, robustness and cybersecurity.",
|
|
77
|
+
)
|
|
78
|
+
print(len(embedding["data"][0]["embedding"])) # → vector dimension
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Usage
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
usage = client.usage.get(from_date="2026-06-01", to_date="2026-06-30")
|
|
85
|
+
print(usage["total_cost_usd"], usage["calls"])
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Anthropic Messages
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
response = client.anthropic.messages_create(
|
|
92
|
+
model="anthropic/claude-sonnet-4-6",
|
|
93
|
+
max_tokens=400,
|
|
94
|
+
system="You are an EU compliance assistant.",
|
|
95
|
+
messages=[{"role": "user", "content": "What is GDPR Article 32?"}],
|
|
96
|
+
)
|
|
97
|
+
print(response["content"][0]["text"])
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Error handling
|
|
101
|
+
|
|
102
|
+
The SDK maps HTTP status codes to typed exceptions. Catch the specific class:
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
from ciralgo import Client
|
|
106
|
+
from ciralgo.errors import RateLimitError, UpstreamError, AuthenticationError
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
response = client.chat.completions.create(...)
|
|
110
|
+
except RateLimitError as e:
|
|
111
|
+
time.sleep(e.retry_after or 5)
|
|
112
|
+
# retry
|
|
113
|
+
except UpstreamError as e:
|
|
114
|
+
# upstream LLM provider 5xx, pick a different model
|
|
115
|
+
...
|
|
116
|
+
except AuthenticationError:
|
|
117
|
+
# rotate / re-issue the key
|
|
118
|
+
raise
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Every exception carries:
|
|
122
|
+
|
|
123
|
+
- `code`: stable string error code from the API envelope (e.g. `rate_limit_exceeded`)
|
|
124
|
+
- `message`: human-readable
|
|
125
|
+
- `trace_id`: pass this to Ciralgo support to look up the request server-side
|
|
126
|
+
- `status_code`: HTTP status
|
|
127
|
+
- `retry_after`: only set on 429
|
|
128
|
+
|
|
129
|
+
## Async
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
import asyncio
|
|
133
|
+
from ciralgo import AsyncClient
|
|
134
|
+
|
|
135
|
+
async def main():
|
|
136
|
+
async with AsyncClient() as client:
|
|
137
|
+
# NOTE: async surface in v1.1.0 is the client construction +
|
|
138
|
+
# close lifecycle. Full async parity for chat / embeddings ships
|
|
139
|
+
# in v1.2.0.
|
|
140
|
+
pass
|
|
141
|
+
|
|
142
|
+
asyncio.run(main())
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Migration to a codegen client
|
|
146
|
+
|
|
147
|
+
The current client is hand-written. A codegen-based replacement is
|
|
148
|
+
viable using [openapi-python-client](https://github.com/openapi-generators/openapi-python-client)
|
|
149
|
+
against the published Ciralgo OpenAPI spec. The hand-written client
|
|
150
|
+
gives us control over:
|
|
151
|
+
|
|
152
|
+
- Streaming semantics (`stream=True` returning an iterator).
|
|
153
|
+
- The `X-Ciralgo-Tags` header marshalling.
|
|
154
|
+
- The typed exception hierarchy (codegen produces a single error class).
|
|
155
|
+
|
|
156
|
+
A future major bump (`v2.0.0`) can switch to codegen if the trade-offs
|
|
157
|
+
change.
|
|
158
|
+
|
|
159
|
+
## Development
|
|
160
|
+
|
|
161
|
+
From the SDK repo root:
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
pip install -e ".[dev]"
|
|
165
|
+
pytest
|
|
166
|
+
ruff check src
|
|
167
|
+
mypy src
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## Publishing
|
|
171
|
+
|
|
172
|
+
Publishing to PyPI is handled by a GitHub Actions workflow triggered on
|
|
173
|
+
tags of the form `sdk-py-v*`. The workflow uses PyPI Trusted Publishing
|
|
174
|
+
(OIDC). No long-lived API token is stored in CI secrets.
|
|
175
|
+
|
|
176
|
+
The engineer-facing release runbook (version bump, tag push, environment
|
|
177
|
+
approval, troubleshooting) is at [docs/publish-runbook.md](docs/publish-runbook.md).
|
|
178
|
+
|
|
179
|
+
## License
|
|
180
|
+
|
|
181
|
+
Apache-2.0
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "ciralgo"
|
|
7
|
+
version = "1.1.0" # mirrors openapi.json info.version
|
|
8
|
+
description = "Official Python SDK for the Ciralgo Platform API"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = "Apache-2.0"
|
|
12
|
+
license-files = ["LICENSE", "NOTICE"]
|
|
13
|
+
authors = [
|
|
14
|
+
{ name = "Ciralgo", email = "support@ciralgo.com" }
|
|
15
|
+
]
|
|
16
|
+
keywords = [
|
|
17
|
+
"ciralgo",
|
|
18
|
+
"openai",
|
|
19
|
+
"anthropic",
|
|
20
|
+
"llm",
|
|
21
|
+
"ai",
|
|
22
|
+
"compliance",
|
|
23
|
+
"eu-sovereignty"
|
|
24
|
+
]
|
|
25
|
+
classifiers = [
|
|
26
|
+
"Development Status :: 4 - Beta",
|
|
27
|
+
"Intended Audience :: Developers",
|
|
28
|
+
"License :: OSI Approved :: Apache Software License",
|
|
29
|
+
"Programming Language :: Python :: 3",
|
|
30
|
+
"Programming Language :: Python :: 3.9",
|
|
31
|
+
"Programming Language :: Python :: 3.10",
|
|
32
|
+
"Programming Language :: Python :: 3.11",
|
|
33
|
+
"Programming Language :: Python :: 3.12",
|
|
34
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
35
|
+
]
|
|
36
|
+
dependencies = [
|
|
37
|
+
# httpx is the standard async-first HTTP client for Python SDKs in
|
|
38
|
+
# 2026. Mature, sync + async surfaces, types built in. We pin a wide
|
|
39
|
+
# range to be a polite dep. Customers may already have httpx pinned
|
|
40
|
+
# tighter in their own apps.
|
|
41
|
+
"httpx>=0.27,<1.0",
|
|
42
|
+
# typing-extensions for Python 3.9 / 3.10 to handle older TypedDict,
|
|
43
|
+
# Required, NotRequired that 3.11+ ships natively.
|
|
44
|
+
"typing-extensions>=4.5; python_version < '3.11'"
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
[project.urls]
|
|
48
|
+
Homepage = "https://www.ciralgo.com"
|
|
49
|
+
Documentation = "https://docs.ciralgo.com/sdk-python"
|
|
50
|
+
Repository = "https://github.com/Ciralgo/ciralgo-python"
|
|
51
|
+
Changelog = "https://github.com/Ciralgo/ciralgo-python/blob/main/CHANGELOG.md"
|
|
52
|
+
Issues = "https://github.com/Ciralgo/ciralgo-python/issues"
|
|
53
|
+
|
|
54
|
+
[project.optional-dependencies]
|
|
55
|
+
dev = [
|
|
56
|
+
"pytest>=8.0",
|
|
57
|
+
"pytest-asyncio>=0.23",
|
|
58
|
+
"respx>=0.21", # httpx mocking, no live network in unit tests
|
|
59
|
+
"mypy>=1.10",
|
|
60
|
+
"ruff>=0.5",
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
[tool.hatch.build.targets.wheel]
|
|
64
|
+
packages = ["src/ciralgo"]
|
|
65
|
+
|
|
66
|
+
[tool.hatch.build.targets.sdist]
|
|
67
|
+
include = [
|
|
68
|
+
"src/ciralgo",
|
|
69
|
+
"README.md",
|
|
70
|
+
"LICENSE",
|
|
71
|
+
"NOTICE",
|
|
72
|
+
"CHANGELOG.md",
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
[tool.pytest.ini_options]
|
|
76
|
+
testpaths = ["tests"]
|
|
77
|
+
asyncio_mode = "auto"
|
|
78
|
+
|
|
79
|
+
[tool.mypy]
|
|
80
|
+
python_version = "3.9"
|
|
81
|
+
strict = true
|
|
82
|
+
|
|
83
|
+
[tool.ruff]
|
|
84
|
+
line-length = 100
|
|
85
|
+
target-version = "py39"
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Official Python SDK for the Ciralgo Platform API.
|
|
2
|
+
|
|
3
|
+
Quick start:
|
|
4
|
+
|
|
5
|
+
from ciralgo import Client
|
|
6
|
+
|
|
7
|
+
client = Client(api_key="sk-cg-...")
|
|
8
|
+
response = client.chat.completions.create(
|
|
9
|
+
model="openai/gpt-4o-mini",
|
|
10
|
+
messages=[{"role": "user", "content": "Hello!"}],
|
|
11
|
+
)
|
|
12
|
+
print(response["choices"][0]["message"]["content"])
|
|
13
|
+
|
|
14
|
+
The SDK is a thin, typed wrapper over the public proxy surface documented
|
|
15
|
+
at https://docs.ciralgo.com. Same authentication, same error envelope.
|
|
16
|
+
See the examples/ directory for chat, embeddings, usage, and Anthropic
|
|
17
|
+
Messages examples.
|
|
18
|
+
|
|
19
|
+
The package version mirrors `openapi.json info.version`. Bumping the API
|
|
20
|
+
spec triggers a coordinated SDK release.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from ciralgo.client import Client, AsyncClient
|
|
24
|
+
from ciralgo.errors import (
|
|
25
|
+
CiralgoError,
|
|
26
|
+
AuthenticationError,
|
|
27
|
+
PermissionError,
|
|
28
|
+
NotFoundError,
|
|
29
|
+
RateLimitError,
|
|
30
|
+
ValidationError,
|
|
31
|
+
UpstreamError,
|
|
32
|
+
InternalError,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
__version__ = "1.1.0"
|
|
36
|
+
|
|
37
|
+
__all__ = [
|
|
38
|
+
"Client",
|
|
39
|
+
"AsyncClient",
|
|
40
|
+
"CiralgoError",
|
|
41
|
+
"AuthenticationError",
|
|
42
|
+
"PermissionError",
|
|
43
|
+
"NotFoundError",
|
|
44
|
+
"RateLimitError",
|
|
45
|
+
"ValidationError",
|
|
46
|
+
"UpstreamError",
|
|
47
|
+
"InternalError",
|
|
48
|
+
"__version__",
|
|
49
|
+
]
|
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
"""Client implementation for the Ciralgo Platform API.
|
|
2
|
+
|
|
3
|
+
This file ships hand-written wrappers over the four public proxy
|
|
4
|
+
operations documented in the Ciralgo OpenAPI spec v1.1.0:
|
|
5
|
+
|
|
6
|
+
POST /v1/chat/completions
|
|
7
|
+
POST /v1/embeddings
|
|
8
|
+
GET /v1/usage
|
|
9
|
+
POST /anthropic/v1/messages
|
|
10
|
+
|
|
11
|
+
A codegen approach using `openapi-python-client` is a viable replacement
|
|
12
|
+
that produces fully typed dataclasses from the spec; the README documents
|
|
13
|
+
the migration path. The hand-written client gives us full control over
|
|
14
|
+
streaming semantics, error mapping, and retries without taking the
|
|
15
|
+
codegen toolchain as a hard dependency for every release.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import os
|
|
21
|
+
from typing import Any, Dict, Iterator, List, Optional, Union
|
|
22
|
+
|
|
23
|
+
import httpx
|
|
24
|
+
|
|
25
|
+
from ciralgo.errors import (
|
|
26
|
+
AuthenticationError,
|
|
27
|
+
CiralgoError,
|
|
28
|
+
InternalError,
|
|
29
|
+
NotFoundError,
|
|
30
|
+
PermissionError,
|
|
31
|
+
RateLimitError,
|
|
32
|
+
UpstreamError,
|
|
33
|
+
ValidationError,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
DEFAULT_BASE_URL = "https://api.ciralgo.com"
|
|
37
|
+
DEFAULT_TIMEOUT_SEC = 120
|
|
38
|
+
USER_AGENT = "ciralgo-python/1.1.0"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class _ChatCompletions:
|
|
42
|
+
"""Sub-resource for /v1/chat/completions.
|
|
43
|
+
|
|
44
|
+
Accessed via `client.chat.completions.create(...)`.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __init__(self, client: "Client") -> None:
|
|
48
|
+
self._client = client
|
|
49
|
+
|
|
50
|
+
def create(
|
|
51
|
+
self,
|
|
52
|
+
*,
|
|
53
|
+
model: str,
|
|
54
|
+
messages: List[Dict[str, Any]],
|
|
55
|
+
temperature: Optional[float] = None,
|
|
56
|
+
max_tokens: Optional[int] = None,
|
|
57
|
+
stream: bool = False,
|
|
58
|
+
stream_options: Optional[Dict[str, Any]] = None,
|
|
59
|
+
tools: Optional[List[Dict[str, Any]]] = None,
|
|
60
|
+
tool_choice: Optional[Union[str, Dict[str, Any]]] = None,
|
|
61
|
+
response_format: Optional[Dict[str, Any]] = None,
|
|
62
|
+
idempotency_key: Optional[str] = None,
|
|
63
|
+
tags: Optional[Dict[str, str]] = None,
|
|
64
|
+
) -> Union[Dict[str, Any], Iterator[Dict[str, Any]]]:
|
|
65
|
+
"""Create a chat completion.
|
|
66
|
+
|
|
67
|
+
Returns the JSON dict when stream=False. When stream=True, returns
|
|
68
|
+
an iterator yielding the SSE chunks as decoded dicts. The final
|
|
69
|
+
`data: [DONE]` sentinel is consumed by the iterator and ends it.
|
|
70
|
+
"""
|
|
71
|
+
body: Dict[str, Any] = {"model": model, "messages": messages}
|
|
72
|
+
if temperature is not None:
|
|
73
|
+
body["temperature"] = temperature
|
|
74
|
+
if max_tokens is not None:
|
|
75
|
+
body["max_tokens"] = max_tokens
|
|
76
|
+
if stream:
|
|
77
|
+
body["stream"] = True
|
|
78
|
+
if stream_options is not None:
|
|
79
|
+
body["stream_options"] = stream_options
|
|
80
|
+
if tools is not None:
|
|
81
|
+
body["tools"] = tools
|
|
82
|
+
if tool_choice is not None:
|
|
83
|
+
body["tool_choice"] = tool_choice
|
|
84
|
+
if response_format is not None:
|
|
85
|
+
body["response_format"] = response_format
|
|
86
|
+
|
|
87
|
+
headers: Dict[str, str] = {}
|
|
88
|
+
if idempotency_key:
|
|
89
|
+
headers["Idempotency-Key"] = idempotency_key
|
|
90
|
+
if tags:
|
|
91
|
+
headers["X-Ciralgo-Tags"] = ",".join(f"{k}={v}" for k, v in tags.items())
|
|
92
|
+
|
|
93
|
+
if stream:
|
|
94
|
+
return self._client._stream("POST", "/v1/chat/completions", body, headers)
|
|
95
|
+
return self._client._request("POST", "/v1/chat/completions", body, headers)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class _Embeddings:
|
|
99
|
+
"""Sub-resource for /v1/embeddings."""
|
|
100
|
+
|
|
101
|
+
def __init__(self, client: "Client") -> None:
|
|
102
|
+
self._client = client
|
|
103
|
+
|
|
104
|
+
def create(
|
|
105
|
+
self,
|
|
106
|
+
*,
|
|
107
|
+
model: str,
|
|
108
|
+
input: Union[str, List[str]],
|
|
109
|
+
encoding_format: Optional[str] = None,
|
|
110
|
+
) -> Dict[str, Any]:
|
|
111
|
+
body: Dict[str, Any] = {"model": model, "input": input}
|
|
112
|
+
if encoding_format is not None:
|
|
113
|
+
body["encoding_format"] = encoding_format
|
|
114
|
+
return self._client._request("POST", "/v1/embeddings", body)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class _Usage:
|
|
118
|
+
"""Sub-resource for /v1/usage."""
|
|
119
|
+
|
|
120
|
+
def __init__(self, client: "Client") -> None:
|
|
121
|
+
self._client = client
|
|
122
|
+
|
|
123
|
+
def get(
|
|
124
|
+
self,
|
|
125
|
+
*,
|
|
126
|
+
from_date: Optional[str] = None,
|
|
127
|
+
to_date: Optional[str] = None,
|
|
128
|
+
group_by: Optional[str] = None,
|
|
129
|
+
) -> Dict[str, Any]:
|
|
130
|
+
params: Dict[str, str] = {}
|
|
131
|
+
if from_date:
|
|
132
|
+
params["from"] = from_date
|
|
133
|
+
if to_date:
|
|
134
|
+
params["to"] = to_date
|
|
135
|
+
if group_by:
|
|
136
|
+
params["group_by"] = group_by
|
|
137
|
+
return self._client._request("GET", "/v1/usage", params=params)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class _Anthropic:
|
|
141
|
+
"""Sub-resource for /anthropic/v1/messages."""
|
|
142
|
+
|
|
143
|
+
def __init__(self, client: "Client") -> None:
|
|
144
|
+
self._client = client
|
|
145
|
+
|
|
146
|
+
def messages_create(
|
|
147
|
+
self,
|
|
148
|
+
*,
|
|
149
|
+
model: str,
|
|
150
|
+
messages: List[Dict[str, Any]],
|
|
151
|
+
max_tokens: int,
|
|
152
|
+
system: Optional[str] = None,
|
|
153
|
+
tools: Optional[List[Dict[str, Any]]] = None,
|
|
154
|
+
stream: bool = False,
|
|
155
|
+
) -> Union[Dict[str, Any], Iterator[Dict[str, Any]]]:
|
|
156
|
+
body: Dict[str, Any] = {
|
|
157
|
+
"model": model,
|
|
158
|
+
"messages": messages,
|
|
159
|
+
"max_tokens": max_tokens,
|
|
160
|
+
}
|
|
161
|
+
if system is not None:
|
|
162
|
+
body["system"] = system
|
|
163
|
+
if tools is not None:
|
|
164
|
+
body["tools"] = tools
|
|
165
|
+
if stream:
|
|
166
|
+
body["stream"] = True
|
|
167
|
+
return self._client._stream("POST", "/anthropic/v1/messages", body)
|
|
168
|
+
return self._client._request("POST", "/anthropic/v1/messages", body)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class _ChatNamespace:
|
|
172
|
+
"""`client.chat.completions.create(...)` plumbing."""
|
|
173
|
+
|
|
174
|
+
def __init__(self, client: "Client") -> None:
|
|
175
|
+
self.completions = _ChatCompletions(client)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
class Client:
|
|
179
|
+
"""Synchronous Ciralgo client.
|
|
180
|
+
|
|
181
|
+
Usage:
|
|
182
|
+
|
|
183
|
+
from ciralgo import Client
|
|
184
|
+
client = Client(api_key="sk-cg-...")
|
|
185
|
+
r = client.chat.completions.create(
|
|
186
|
+
model="openai/gpt-4o-mini",
|
|
187
|
+
messages=[{"role": "user", "content": "Hello"}],
|
|
188
|
+
)
|
|
189
|
+
"""
|
|
190
|
+
|
|
191
|
+
def __init__(
|
|
192
|
+
self,
|
|
193
|
+
*,
|
|
194
|
+
api_key: Optional[str] = None,
|
|
195
|
+
base_url: Optional[str] = None,
|
|
196
|
+
timeout: float = DEFAULT_TIMEOUT_SEC,
|
|
197
|
+
) -> None:
|
|
198
|
+
self.api_key = api_key or os.environ.get("CIRALGO_API_KEY")
|
|
199
|
+
if not self.api_key:
|
|
200
|
+
raise AuthenticationError(
|
|
201
|
+
code="missing_api_key",
|
|
202
|
+
message=(
|
|
203
|
+
"No API key found. Pass api_key=... or set CIRALGO_API_KEY in the environment."
|
|
204
|
+
),
|
|
205
|
+
)
|
|
206
|
+
self.base_url = (base_url or os.environ.get("CIRALGO_BASE_URL") or DEFAULT_BASE_URL).rstrip("/")
|
|
207
|
+
self._http = httpx.Client(timeout=timeout, headers=self._default_headers())
|
|
208
|
+
self.chat = _ChatNamespace(self)
|
|
209
|
+
self.embeddings = _Embeddings(self)
|
|
210
|
+
self.usage = _Usage(self)
|
|
211
|
+
self.anthropic = _Anthropic(self)
|
|
212
|
+
|
|
213
|
+
# Sentinel close method for `with Client(...) as client:` patterns.
|
|
214
|
+
def __enter__(self) -> "Client":
|
|
215
|
+
return self
|
|
216
|
+
|
|
217
|
+
def __exit__(self, *_: Any) -> None:
|
|
218
|
+
self.close()
|
|
219
|
+
|
|
220
|
+
def close(self) -> None:
|
|
221
|
+
self._http.close()
|
|
222
|
+
|
|
223
|
+
def _default_headers(self) -> Dict[str, str]:
|
|
224
|
+
return {
|
|
225
|
+
"Authorization": f"Bearer {self.api_key}",
|
|
226
|
+
"User-Agent": USER_AGENT,
|
|
227
|
+
"Accept": "application/json",
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
def _request(
|
|
231
|
+
self,
|
|
232
|
+
method: str,
|
|
233
|
+
path: str,
|
|
234
|
+
json_body: Optional[Dict[str, Any]] = None,
|
|
235
|
+
extra_headers: Optional[Dict[str, str]] = None,
|
|
236
|
+
params: Optional[Dict[str, str]] = None,
|
|
237
|
+
) -> Dict[str, Any]:
|
|
238
|
+
url = f"{self.base_url}{path}"
|
|
239
|
+
try:
|
|
240
|
+
resp = self._http.request(
|
|
241
|
+
method,
|
|
242
|
+
url,
|
|
243
|
+
json=json_body,
|
|
244
|
+
params=params,
|
|
245
|
+
headers=extra_headers or None,
|
|
246
|
+
)
|
|
247
|
+
except httpx.TimeoutException as e:
|
|
248
|
+
raise UpstreamError(
|
|
249
|
+
code="upstream_timeout",
|
|
250
|
+
message=f"Request timed out: {e}",
|
|
251
|
+
) from e
|
|
252
|
+
return self._handle(resp)
|
|
253
|
+
|
|
254
|
+
def _stream(
|
|
255
|
+
self,
|
|
256
|
+
method: str,
|
|
257
|
+
path: str,
|
|
258
|
+
json_body: Dict[str, Any],
|
|
259
|
+
extra_headers: Optional[Dict[str, str]] = None,
|
|
260
|
+
) -> Iterator[Dict[str, Any]]:
|
|
261
|
+
"""Stream SSE chunks from /v1/chat/completions or /anthropic/v1/messages.
|
|
262
|
+
|
|
263
|
+
Yields decoded JSON chunks. Consumes and discards the `[DONE]`
|
|
264
|
+
sentinel so the iterator ends naturally.
|
|
265
|
+
"""
|
|
266
|
+
import json as _json
|
|
267
|
+
|
|
268
|
+
url = f"{self.base_url}{path}"
|
|
269
|
+
headers = dict(self._default_headers())
|
|
270
|
+
headers["Accept"] = "text/event-stream"
|
|
271
|
+
if extra_headers:
|
|
272
|
+
headers.update(extra_headers)
|
|
273
|
+
|
|
274
|
+
with self._http.stream(method, url, json=json_body, headers=headers) as resp:
|
|
275
|
+
if resp.status_code >= 400:
|
|
276
|
+
# Read the body, route through _handle for a typed error.
|
|
277
|
+
body = resp.read()
|
|
278
|
+
resp_for_handle = httpx.Response(
|
|
279
|
+
status_code=resp.status_code,
|
|
280
|
+
headers=resp.headers,
|
|
281
|
+
content=body,
|
|
282
|
+
)
|
|
283
|
+
self._handle(resp_for_handle)
|
|
284
|
+
return # _handle always raises
|
|
285
|
+
|
|
286
|
+
for line in resp.iter_lines():
|
|
287
|
+
if not line.startswith("data:"):
|
|
288
|
+
continue
|
|
289
|
+
payload = line[len("data:"):].strip()
|
|
290
|
+
if payload == "[DONE]":
|
|
291
|
+
return
|
|
292
|
+
if not payload:
|
|
293
|
+
continue
|
|
294
|
+
try:
|
|
295
|
+
yield _json.loads(payload)
|
|
296
|
+
except Exception:
|
|
297
|
+
# Skip malformed chunks rather than crash the stream.
|
|
298
|
+
continue
|
|
299
|
+
|
|
300
|
+
def _handle(self, resp: httpx.Response) -> Dict[str, Any]:
|
|
301
|
+
"""Map a finished httpx.Response to a body dict or a typed exception."""
|
|
302
|
+
if 200 <= resp.status_code < 300:
|
|
303
|
+
if not resp.content:
|
|
304
|
+
return {}
|
|
305
|
+
return resp.json()
|
|
306
|
+
|
|
307
|
+
# Error path. Pull what we can from the standard envelope.
|
|
308
|
+
try:
|
|
309
|
+
body = resp.json()
|
|
310
|
+
except Exception:
|
|
311
|
+
body = {}
|
|
312
|
+
|
|
313
|
+
err_obj = body.get("error") if isinstance(body, dict) else None
|
|
314
|
+
code = (err_obj or {}).get("code", "unknown_error")
|
|
315
|
+
message = (err_obj or {}).get("message", f"HTTP {resp.status_code}")
|
|
316
|
+
trace_id = (err_obj or {}).get("trace_id") or resp.headers.get("X-Trace-Id")
|
|
317
|
+
retry_after_raw = (err_obj or {}).get("retry_after") or resp.headers.get("Retry-After")
|
|
318
|
+
try:
|
|
319
|
+
retry_after = int(retry_after_raw) if retry_after_raw is not None else None
|
|
320
|
+
except (TypeError, ValueError):
|
|
321
|
+
retry_after = None
|
|
322
|
+
|
|
323
|
+
kwargs = dict(code=code, message=message, trace_id=trace_id, status_code=resp.status_code, retry_after=retry_after)
|
|
324
|
+
if resp.status_code == 400:
|
|
325
|
+
raise ValidationError(**kwargs)
|
|
326
|
+
if resp.status_code == 401:
|
|
327
|
+
raise AuthenticationError(**kwargs)
|
|
328
|
+
if resp.status_code == 403:
|
|
329
|
+
raise PermissionError(**kwargs)
|
|
330
|
+
if resp.status_code == 404:
|
|
331
|
+
raise NotFoundError(**kwargs)
|
|
332
|
+
if resp.status_code == 429:
|
|
333
|
+
raise RateLimitError(**kwargs)
|
|
334
|
+
if resp.status_code == 502:
|
|
335
|
+
raise UpstreamError(**kwargs)
|
|
336
|
+
if resp.status_code >= 500:
|
|
337
|
+
raise InternalError(**kwargs)
|
|
338
|
+
raise CiralgoError(**kwargs)
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
class AsyncClient:
|
|
342
|
+
"""Async sibling of Client. Same surface, async methods.
|
|
343
|
+
|
|
344
|
+
Intentionally minimal in v1.1.0. Covers the same four operations. A
|
|
345
|
+
follow-up release adds parity for streaming + retries.
|
|
346
|
+
"""
|
|
347
|
+
|
|
348
|
+
def __init__(
|
|
349
|
+
self,
|
|
350
|
+
*,
|
|
351
|
+
api_key: Optional[str] = None,
|
|
352
|
+
base_url: Optional[str] = None,
|
|
353
|
+
timeout: float = DEFAULT_TIMEOUT_SEC,
|
|
354
|
+
) -> None:
|
|
355
|
+
# Reuse the sync client's auth + URL resolution by constructing a
|
|
356
|
+
# bare Client and copying its attributes. Avoids duplicating the
|
|
357
|
+
# config logic until the async surface diverges.
|
|
358
|
+
sync = Client(api_key=api_key, base_url=base_url, timeout=timeout)
|
|
359
|
+
sync.close()
|
|
360
|
+
self.api_key = sync.api_key
|
|
361
|
+
self.base_url = sync.base_url
|
|
362
|
+
self._http = httpx.AsyncClient(
|
|
363
|
+
timeout=timeout,
|
|
364
|
+
headers=sync._default_headers(),
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
async def close(self) -> None:
|
|
368
|
+
await self._http.aclose()
|
|
369
|
+
|
|
370
|
+
async def __aenter__(self) -> "AsyncClient":
|
|
371
|
+
return self
|
|
372
|
+
|
|
373
|
+
async def __aexit__(self, *_: Any) -> None:
|
|
374
|
+
await self.close()
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Typed error hierarchy mirroring the OpenAPI ErrorResponse envelope.
|
|
2
|
+
|
|
3
|
+
Every API error from Ciralgo carries the shape:
|
|
4
|
+
|
|
5
|
+
{
|
|
6
|
+
"ok": false,
|
|
7
|
+
"error": {
|
|
8
|
+
"code": "<stable_code>",
|
|
9
|
+
"message": "<human readable>",
|
|
10
|
+
"trace_id": "<request id>",
|
|
11
|
+
"retry_after": <seconds, only on 429>
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
The client maps HTTP status codes to one of these exception classes so
|
|
16
|
+
customer code can catch the specific class rather than parsing the
|
|
17
|
+
envelope by hand:
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
client.chat.completions.create(...)
|
|
21
|
+
except RateLimitError as e:
|
|
22
|
+
time.sleep(e.retry_after or 5)
|
|
23
|
+
# retry
|
|
24
|
+
except UpstreamError as e:
|
|
25
|
+
# upstream LLM provider returned 5xx, try a different model
|
|
26
|
+
...
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
from typing import Optional
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class CiralgoError(Exception):
|
|
35
|
+
"""Base class for every Ciralgo SDK error.
|
|
36
|
+
|
|
37
|
+
Carries the API-level error code, the human message, the trace_id for
|
|
38
|
+
support correlation, and the raw HTTP status code.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
*,
|
|
44
|
+
code: str,
|
|
45
|
+
message: str,
|
|
46
|
+
trace_id: Optional[str] = None,
|
|
47
|
+
status_code: Optional[int] = None,
|
|
48
|
+
retry_after: Optional[int] = None,
|
|
49
|
+
) -> None:
|
|
50
|
+
super().__init__(message)
|
|
51
|
+
self.code = code
|
|
52
|
+
self.message = message
|
|
53
|
+
self.trace_id = trace_id
|
|
54
|
+
self.status_code = status_code
|
|
55
|
+
self.retry_after = retry_after
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class ValidationError(CiralgoError):
|
|
59
|
+
"""4xx: request shape rejected by the server (400)."""
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class AuthenticationError(CiralgoError):
|
|
63
|
+
"""401: invalid or missing API key."""
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class PermissionError(CiralgoError):
|
|
67
|
+
"""403: caller does not have permission (tenant policy, admin MFA, etc.)."""
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class NotFoundError(CiralgoError):
|
|
71
|
+
"""404: resource does not exist or is not visible to this caller."""
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class RateLimitError(CiralgoError):
|
|
75
|
+
"""429: per-key / per-org RPM or TPM limit exceeded.
|
|
76
|
+
|
|
77
|
+
The `retry_after` attribute is set when the server provided one.
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class UpstreamError(CiralgoError):
|
|
82
|
+
"""502: upstream LLM provider returned an error or timed out."""
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class InternalError(CiralgoError):
|
|
86
|
+
"""5xx: unexpected server-side error. Retry with exponential backoff."""
|