clark-platform-client 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- clark_platform_client-0.1.0/.gitignore +7 -0
- clark_platform_client-0.1.0/PKG-INFO +241 -0
- clark_platform_client-0.1.0/README.md +219 -0
- clark_platform_client-0.1.0/examples/live_smoke.py +68 -0
- clark_platform_client-0.1.0/pyproject.toml +39 -0
- clark_platform_client-0.1.0/src/clark_platform/__init__.py +61 -0
- clark_platform_client-0.1.0/src/clark_platform/_payloads.py +123 -0
- clark_platform_client-0.1.0/src/clark_platform/async_client.py +380 -0
- clark_platform_client-0.1.0/src/clark_platform/client.py +425 -0
- clark_platform_client-0.1.0/src/clark_platform/errors.py +61 -0
- clark_platform_client-0.1.0/src/clark_platform/models.py +588 -0
- clark_platform_client-0.1.0/src/clark_platform/streaming.py +69 -0
- clark_platform_client-0.1.0/tests/conftest.py +339 -0
- clark_platform_client-0.1.0/tests/test_auth_and_errors.py +121 -0
- clark_platform_client-0.1.0/tests/test_models_parsing.py +152 -0
- clark_platform_client-0.1.0/tests/test_passthrough.py +144 -0
- clark_platform_client-0.1.0/tests/test_streaming.py +153 -0
- clark_platform_client-0.1.0/uv.lock +355 -0
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: clark-platform-client
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Typed sync/async Python client for the Clark Platform API
|
|
5
|
+
Project-URL: Homepage, https://www.clarkchat.com
|
|
6
|
+
Author: Clark Labs
|
|
7
|
+
License: MIT
|
|
8
|
+
Keywords: agent,api-client,clark,clark-labs,httpx
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Typing :: Typed
|
|
16
|
+
Requires-Python: >=3.9
|
|
17
|
+
Requires-Dist: httpx>=0.24
|
|
18
|
+
Provides-Extra: dev
|
|
19
|
+
Requires-Dist: pytest-asyncio>=0.21; extra == 'dev'
|
|
20
|
+
Requires-Dist: pytest>=7.4; extra == 'dev'
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# clark-platform-client
|
|
24
|
+
|
|
25
|
+
Typed, standalone Python client (sync + async) for the [Clark Platform
|
|
26
|
+
API](https://www.clarkchat.com) — the OpenAI-compatible-ish public HTTP API
|
|
27
|
+
for creating Clark agent runs, streaming their progress, and reading back
|
|
28
|
+
chat-completion/response objects, usage, artifacts, and durable memory.
|
|
29
|
+
|
|
30
|
+
This client implements the wire contract documented in
|
|
31
|
+
[`platform/clients/API_CONTRACT.md`](../API_CONTRACT.md) in the Clark
|
|
32
|
+
monorepo. If something here disagrees with that file, the contract file is
|
|
33
|
+
the source of truth.
|
|
34
|
+
|
|
35
|
+
- Only required dependency: [`httpx`](https://www.python-httpx.org/).
|
|
36
|
+
- No `pydantic` — models are plain stdlib `dataclasses`.
|
|
37
|
+
- Both a sync client (`ClarkClient`) and an async client (`AsyncClarkClient`);
|
|
38
|
+
pick one without pulling in `asyncio` machinery you don't need.
|
|
39
|
+
- Python 3.9+.
|
|
40
|
+
|
|
41
|
+
## Install
|
|
42
|
+
|
|
43
|
+
Not published to PyPI yet. Install directly from a local checkout of this
|
|
44
|
+
directory:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
# with uv, inside another project
|
|
48
|
+
uv add /path/to/clark/platform/clients/python
|
|
49
|
+
|
|
50
|
+
# or with pip, editable install for local development
|
|
51
|
+
pip install -e /path/to/clark/platform/clients/python
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Authentication
|
|
55
|
+
|
|
56
|
+
Platform API keys (`clk_live_...`) are minted from the signed-in `/platform`
|
|
57
|
+
page in the Clark web app — there is no public self-serve key-creation
|
|
58
|
+
endpoint. Every key carries a fixed scope set
|
|
59
|
+
(`responses:create`, `responses:read`, `models:read`, `artifacts:read`,
|
|
60
|
+
`memories:read`).
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
from clark_platform import ClarkClient
|
|
64
|
+
|
|
65
|
+
client = ClarkClient(api_key="clk_live_xxxxxxxxxxxxxxxxxxxx")
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
By default the client talks to production (`https://www.clarkchat.com`).
|
|
69
|
+
|
|
70
|
+
## Quickstart — sync
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
from clark_platform import ClarkClient
|
|
74
|
+
|
|
75
|
+
with ClarkClient(api_key="clk_live_...") as client:
|
|
76
|
+
models = client.models.list()
|
|
77
|
+
print([m.id for m in models.data])
|
|
78
|
+
|
|
79
|
+
response = client.responses.create(model="clark", input="Summarize this repo's README.")
|
|
80
|
+
print(response.status, response.output_text)
|
|
81
|
+
|
|
82
|
+
chat = client.chat.completions.create(
|
|
83
|
+
model="openrouter:qwen35_flash",
|
|
84
|
+
messages=[{"role": "user", "content": "hello"}],
|
|
85
|
+
)
|
|
86
|
+
print(chat.output_text)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Quickstart — async
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
import asyncio
|
|
93
|
+
from clark_platform import AsyncClarkClient
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
async def main() -> None:
|
|
97
|
+
async with AsyncClarkClient(api_key="clk_live_...") as client:
|
|
98
|
+
response = await client.responses.create(model="clark", input="hello")
|
|
99
|
+
print(response.output_text)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
asyncio.run(main())
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Two families of tier: agentic vs. `clark-code` passthrough
|
|
106
|
+
|
|
107
|
+
The API has two entirely different behaviors depending on `model`:
|
|
108
|
+
|
|
109
|
+
1. **Agentic tiers** (`clark`, `clark_max`, `openrouter:*`) — Clark runs its
|
|
110
|
+
own internal agent loop and returns only the final projected answer.
|
|
111
|
+
`tools`/`tool_choice` are not accepted by these methods (the server
|
|
112
|
+
rejects them with `400 unsupported_parameter`, so the SDK doesn't even
|
|
113
|
+
expose those parameters on `responses.create`/`chat.completions.create`).
|
|
114
|
+
2. **The `clark-code` passthrough tier** — your `messages` (including prior
|
|
115
|
+
`tool_calls`/`tool` messages) are forwarded verbatim to the underlying
|
|
116
|
+
OpenRouter-compatible model, and native OpenAI-format `tool_calls` come
|
|
117
|
+
back for you to execute yourself. Clark does not run tools for this tier.
|
|
118
|
+
This is **only** available via `/v1/chat/completions`, never
|
|
119
|
+
`/v1/responses`.
|
|
120
|
+
|
|
121
|
+
Because these are different response *shapes*, the passthrough tier is
|
|
122
|
+
exposed via clearly separate methods that return the raw upstream JSON dict
|
|
123
|
+
instead of a typed Clark object:
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
# Agentic — typed ChatCompletionObject, no tools allowed.
|
|
127
|
+
chat = client.chat.completions.create(model="clark", messages=[...])
|
|
128
|
+
|
|
129
|
+
# Passthrough — raw OpenAI-compatible dict, tools/tool_choice forwarded verbatim.
|
|
130
|
+
raw = client.chat.completions.create_passthrough(
|
|
131
|
+
model="clark-code",
|
|
132
|
+
messages=[...],
|
|
133
|
+
tools=[{"type": "function", "function": {...}}],
|
|
134
|
+
)
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Streaming
|
|
138
|
+
|
|
139
|
+
### `responses.stream(...)`
|
|
140
|
+
|
|
141
|
+
Named SSE events (`response.created`, `response.output_text.delta`,
|
|
142
|
+
`response.artifact.completed`, `response.usage.updated`,
|
|
143
|
+
`response.completed`/`response.failed`, ...). Per the contract, the full
|
|
144
|
+
answer arrives as a **single** `response.output_text.delta` event, not
|
|
145
|
+
token-by-token — the iterator API is for symmetry with chat-completions
|
|
146
|
+
streaming and future finer-grained deltas.
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
for event in client.responses.stream(model="clark", input="hello"):
|
|
150
|
+
if event.type == "response.output_text.delta":
|
|
151
|
+
print(event.delta, end="")
|
|
152
|
+
elif event.type == "response.completed":
|
|
153
|
+
print("\n--- done ---", event.response.status)
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Async:
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
async for event in async_client.responses.stream(model="clark", input="hello"):
|
|
160
|
+
...
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### `chat.completions.stream(...)`
|
|
164
|
+
|
|
165
|
+
Plain `data:`-only SSE (no `event:` name), `chat.completion.chunk` objects,
|
|
166
|
+
terminated by the server's literal `data: [DONE]` (already consumed for you
|
|
167
|
+
— it never appears as a yielded item):
|
|
168
|
+
|
|
169
|
+
```python
|
|
170
|
+
for chunk in client.chat.completions.stream(
|
|
171
|
+
model="clark",
|
|
172
|
+
messages=[{"role": "user", "content": "hello"}],
|
|
173
|
+
stream_options={"include_usage": True},
|
|
174
|
+
):
|
|
175
|
+
for choice in chunk.choices:
|
|
176
|
+
if choice.delta.content:
|
|
177
|
+
print(choice.delta.content, end="")
|
|
178
|
+
if chunk.usage:
|
|
179
|
+
print("\nusage:", chunk.usage)
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### `chat.completions.stream_passthrough(...)`
|
|
183
|
+
|
|
184
|
+
Same framing, but yields raw upstream JSON dicts (native `tool_calls`
|
|
185
|
+
deltas and all) instead of a typed `ChatCompletionChunk`, since the
|
|
186
|
+
passthrough tier's chunk shape is whatever the upstream OpenAI-compatible
|
|
187
|
+
provider sends.
|
|
188
|
+
|
|
189
|
+
## Other endpoints
|
|
190
|
+
|
|
191
|
+
```python
|
|
192
|
+
# Poll a response (e.g. after background=True, or to recover from a chat
|
|
193
|
+
# completions timeout by replacing the "chatcmpl_" prefix with "resp_").
|
|
194
|
+
response = client.responses.get("resp_01jz4n8h2f7g9k4q2m6s")
|
|
195
|
+
|
|
196
|
+
# Progress events for a response.
|
|
197
|
+
events = client.responses.list_events("resp_01jz4n8h2f7g9k4q2m6s", after_seq=0, limit=200)
|
|
198
|
+
|
|
199
|
+
# Durable memory.
|
|
200
|
+
memories = client.memories.list(q="deploy checklist")
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## Errors
|
|
204
|
+
|
|
205
|
+
All non-2xx responses raise `ClarkApiError`, parsed from the API's error
|
|
206
|
+
envelope:
|
|
207
|
+
|
|
208
|
+
```python
|
|
209
|
+
from clark_platform import ClarkApiError
|
|
210
|
+
|
|
211
|
+
try:
|
|
212
|
+
client.responses.create(model="clark", input="hello")
|
|
213
|
+
except ClarkApiError as err:
|
|
214
|
+
print(err.status_code, err.type, err.code, err.param, err.message)
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Network-level failures (connection errors, timeouts, DNS failures) are
|
|
218
|
+
**not** wrapped — they surface as the underlying `httpx` exception (e.g.
|
|
219
|
+
`httpx.ConnectError`, `httpx.TimeoutException`), so you can rely on
|
|
220
|
+
`httpx`'s own exception hierarchy for those.
|
|
221
|
+
|
|
222
|
+
## Timeouts
|
|
223
|
+
|
|
224
|
+
Non-background `responses`/`chat.completions` calls can legitimately take up
|
|
225
|
+
to ~120s server-side (`CLARK_PLATFORM_RESPONSE_WAIT_MS`, default 120000ms).
|
|
226
|
+
The client defaults its `httpx` timeout to 150s to comfortably clear that;
|
|
227
|
+
pass your own `timeout=` (or a pre-configured `http_client=`) to
|
|
228
|
+
`ClarkClient`/`AsyncClarkClient` to change it.
|
|
229
|
+
|
|
230
|
+
## Development
|
|
231
|
+
|
|
232
|
+
```bash
|
|
233
|
+
cd platform/clients/python
|
|
234
|
+
uv sync --python 3.12
|
|
235
|
+
uv run --python 3.12 pytest -q
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
`examples/live_smoke.py` is an opt-in, real-network smoke test — it reads
|
|
239
|
+
`CLARK_API_BASE_URL`, `CLARK_API_KEY`, and `CLARK_TEST_MODEL` from the
|
|
240
|
+
environment and exits early with a clear message if `CLARK_API_KEY` is
|
|
241
|
+
unset. It is not part of the test suite and is never run automatically.
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
# clark-platform-client
|
|
2
|
+
|
|
3
|
+
Typed, standalone Python client (sync + async) for the [Clark Platform
|
|
4
|
+
API](https://www.clarkchat.com) — the OpenAI-compatible-ish public HTTP API
|
|
5
|
+
for creating Clark agent runs, streaming their progress, and reading back
|
|
6
|
+
chat-completion/response objects, usage, artifacts, and durable memory.
|
|
7
|
+
|
|
8
|
+
This client implements the wire contract documented in
|
|
9
|
+
[`platform/clients/API_CONTRACT.md`](../API_CONTRACT.md) in the Clark
|
|
10
|
+
monorepo. If something here disagrees with that file, the contract file is
|
|
11
|
+
the source of truth.
|
|
12
|
+
|
|
13
|
+
- Only required dependency: [`httpx`](https://www.python-httpx.org/).
|
|
14
|
+
- No `pydantic` — models are plain stdlib `dataclasses`.
|
|
15
|
+
- Both a sync client (`ClarkClient`) and an async client (`AsyncClarkClient`);
|
|
16
|
+
pick one without pulling in `asyncio` machinery you don't need.
|
|
17
|
+
- Python 3.9+.
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
Not published to PyPI yet. Install directly from a local checkout of this
|
|
22
|
+
directory:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
# with uv, inside another project
|
|
26
|
+
uv add /path/to/clark/platform/clients/python
|
|
27
|
+
|
|
28
|
+
# or with pip, editable install for local development
|
|
29
|
+
pip install -e /path/to/clark/platform/clients/python
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Authentication
|
|
33
|
+
|
|
34
|
+
Platform API keys (`clk_live_...`) are minted from the signed-in `/platform`
|
|
35
|
+
page in the Clark web app — there is no public self-serve key-creation
|
|
36
|
+
endpoint. Every key carries a fixed scope set
|
|
37
|
+
(`responses:create`, `responses:read`, `models:read`, `artifacts:read`,
|
|
38
|
+
`memories:read`).
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
from clark_platform import ClarkClient
|
|
42
|
+
|
|
43
|
+
client = ClarkClient(api_key="clk_live_xxxxxxxxxxxxxxxxxxxx")
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
By default the client talks to production (`https://www.clarkchat.com`).
|
|
47
|
+
|
|
48
|
+
## Quickstart — sync
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
from clark_platform import ClarkClient
|
|
52
|
+
|
|
53
|
+
with ClarkClient(api_key="clk_live_...") as client:
|
|
54
|
+
models = client.models.list()
|
|
55
|
+
print([m.id for m in models.data])
|
|
56
|
+
|
|
57
|
+
response = client.responses.create(model="clark", input="Summarize this repo's README.")
|
|
58
|
+
print(response.status, response.output_text)
|
|
59
|
+
|
|
60
|
+
chat = client.chat.completions.create(
|
|
61
|
+
model="openrouter:qwen35_flash",
|
|
62
|
+
messages=[{"role": "user", "content": "hello"}],
|
|
63
|
+
)
|
|
64
|
+
print(chat.output_text)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Quickstart — async
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
import asyncio
|
|
71
|
+
from clark_platform import AsyncClarkClient
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
async def main() -> None:
|
|
75
|
+
async with AsyncClarkClient(api_key="clk_live_...") as client:
|
|
76
|
+
response = await client.responses.create(model="clark", input="hello")
|
|
77
|
+
print(response.output_text)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
asyncio.run(main())
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Two families of tier: agentic vs. `clark-code` passthrough
|
|
84
|
+
|
|
85
|
+
The API has two entirely different behaviors depending on `model`:
|
|
86
|
+
|
|
87
|
+
1. **Agentic tiers** (`clark`, `clark_max`, `openrouter:*`) — Clark runs its
|
|
88
|
+
own internal agent loop and returns only the final projected answer.
|
|
89
|
+
`tools`/`tool_choice` are not accepted by these methods (the server
|
|
90
|
+
rejects them with `400 unsupported_parameter`, so the SDK doesn't even
|
|
91
|
+
expose those parameters on `responses.create`/`chat.completions.create`).
|
|
92
|
+
2. **The `clark-code` passthrough tier** — your `messages` (including prior
|
|
93
|
+
`tool_calls`/`tool` messages) are forwarded verbatim to the underlying
|
|
94
|
+
OpenRouter-compatible model, and native OpenAI-format `tool_calls` come
|
|
95
|
+
back for you to execute yourself. Clark does not run tools for this tier.
|
|
96
|
+
This is **only** available via `/v1/chat/completions`, never
|
|
97
|
+
`/v1/responses`.
|
|
98
|
+
|
|
99
|
+
Because these are different response *shapes*, the passthrough tier is
|
|
100
|
+
exposed via clearly separate methods that return the raw upstream JSON dict
|
|
101
|
+
instead of a typed Clark object:
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
# Agentic — typed ChatCompletionObject, no tools allowed.
|
|
105
|
+
chat = client.chat.completions.create(model="clark", messages=[...])
|
|
106
|
+
|
|
107
|
+
# Passthrough — raw OpenAI-compatible dict, tools/tool_choice forwarded verbatim.
|
|
108
|
+
raw = client.chat.completions.create_passthrough(
|
|
109
|
+
model="clark-code",
|
|
110
|
+
messages=[...],
|
|
111
|
+
tools=[{"type": "function", "function": {...}}],
|
|
112
|
+
)
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Streaming
|
|
116
|
+
|
|
117
|
+
### `responses.stream(...)`
|
|
118
|
+
|
|
119
|
+
Named SSE events (`response.created`, `response.output_text.delta`,
|
|
120
|
+
`response.artifact.completed`, `response.usage.updated`,
|
|
121
|
+
`response.completed`/`response.failed`, ...). Per the contract, the full
|
|
122
|
+
answer arrives as a **single** `response.output_text.delta` event, not
|
|
123
|
+
token-by-token — the iterator API is for symmetry with chat-completions
|
|
124
|
+
streaming and future finer-grained deltas.
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
for event in client.responses.stream(model="clark", input="hello"):
|
|
128
|
+
if event.type == "response.output_text.delta":
|
|
129
|
+
print(event.delta, end="")
|
|
130
|
+
elif event.type == "response.completed":
|
|
131
|
+
print("\n--- done ---", event.response.status)
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Async:
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
async for event in async_client.responses.stream(model="clark", input="hello"):
|
|
138
|
+
...
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### `chat.completions.stream(...)`
|
|
142
|
+
|
|
143
|
+
Plain `data:`-only SSE (no `event:` name), `chat.completion.chunk` objects,
|
|
144
|
+
terminated by the server's literal `data: [DONE]` (already consumed for you
|
|
145
|
+
— it never appears as a yielded item):
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
for chunk in client.chat.completions.stream(
|
|
149
|
+
model="clark",
|
|
150
|
+
messages=[{"role": "user", "content": "hello"}],
|
|
151
|
+
stream_options={"include_usage": True},
|
|
152
|
+
):
|
|
153
|
+
for choice in chunk.choices:
|
|
154
|
+
if choice.delta.content:
|
|
155
|
+
print(choice.delta.content, end="")
|
|
156
|
+
if chunk.usage:
|
|
157
|
+
print("\nusage:", chunk.usage)
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### `chat.completions.stream_passthrough(...)`
|
|
161
|
+
|
|
162
|
+
Same framing, but yields raw upstream JSON dicts (native `tool_calls`
|
|
163
|
+
deltas and all) instead of a typed `ChatCompletionChunk`, since the
|
|
164
|
+
passthrough tier's chunk shape is whatever the upstream OpenAI-compatible
|
|
165
|
+
provider sends.
|
|
166
|
+
|
|
167
|
+
## Other endpoints
|
|
168
|
+
|
|
169
|
+
```python
|
|
170
|
+
# Poll a response (e.g. after background=True, or to recover from a chat
|
|
171
|
+
# completions timeout by replacing the "chatcmpl_" prefix with "resp_").
|
|
172
|
+
response = client.responses.get("resp_01jz4n8h2f7g9k4q2m6s")
|
|
173
|
+
|
|
174
|
+
# Progress events for a response.
|
|
175
|
+
events = client.responses.list_events("resp_01jz4n8h2f7g9k4q2m6s", after_seq=0, limit=200)
|
|
176
|
+
|
|
177
|
+
# Durable memory.
|
|
178
|
+
memories = client.memories.list(q="deploy checklist")
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## Errors
|
|
182
|
+
|
|
183
|
+
All non-2xx responses raise `ClarkApiError`, parsed from the API's error
|
|
184
|
+
envelope:
|
|
185
|
+
|
|
186
|
+
```python
|
|
187
|
+
from clark_platform import ClarkApiError
|
|
188
|
+
|
|
189
|
+
try:
|
|
190
|
+
client.responses.create(model="clark", input="hello")
|
|
191
|
+
except ClarkApiError as err:
|
|
192
|
+
print(err.status_code, err.type, err.code, err.param, err.message)
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Network-level failures (connection errors, timeouts, DNS failures) are
|
|
196
|
+
**not** wrapped — they surface as the underlying `httpx` exception (e.g.
|
|
197
|
+
`httpx.ConnectError`, `httpx.TimeoutException`), so you can rely on
|
|
198
|
+
`httpx`'s own exception hierarchy for those.
|
|
199
|
+
|
|
200
|
+
## Timeouts
|
|
201
|
+
|
|
202
|
+
Non-background `responses`/`chat.completions` calls can legitimately take up
|
|
203
|
+
to ~120s server-side (`CLARK_PLATFORM_RESPONSE_WAIT_MS`, default 120000ms).
|
|
204
|
+
The client defaults its `httpx` timeout to 150s to comfortably clear that;
|
|
205
|
+
pass your own `timeout=` (or a pre-configured `http_client=`) to
|
|
206
|
+
`ClarkClient`/`AsyncClarkClient` to change it.
|
|
207
|
+
|
|
208
|
+
## Development
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
cd platform/clients/python
|
|
212
|
+
uv sync --python 3.12
|
|
213
|
+
uv run --python 3.12 pytest -q
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
`examples/live_smoke.py` is an opt-in, real-network smoke test — it reads
|
|
217
|
+
`CLARK_API_BASE_URL`, `CLARK_API_KEY`, and `CLARK_TEST_MODEL` from the
|
|
218
|
+
environment and exits early with a clear message if `CLARK_API_KEY` is
|
|
219
|
+
unset. It is not part of the test suite and is never run automatically.
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Opt-in real-network smoke test for the Clark Platform API client.
|
|
3
|
+
|
|
4
|
+
This is NOT part of the automated test suite (it is never imported or run by
|
|
5
|
+
pytest) and is never invoked automatically. It exists so a human can sanity
|
|
6
|
+
check the client against a live Clark deployment.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
|
|
10
|
+
export CLARK_API_BASE_URL="https://www.clarkchat.com" # or prod default
|
|
11
|
+
export CLARK_API_KEY="clk_live_xxxxxxxxxxxxxxxxxxxx"
|
|
12
|
+
export CLARK_TEST_MODEL="openrouter:qwen35_flash" # optional
|
|
13
|
+
python examples/live_smoke.py
|
|
14
|
+
|
|
15
|
+
If ``CLARK_API_KEY`` is unset, this script prints a clear message and exits
|
|
16
|
+
without making any network calls.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import os
|
|
22
|
+
import sys
|
|
23
|
+
|
|
24
|
+
from clark_platform import ClarkApiError, ClarkClient
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def main() -> int:
|
|
28
|
+
api_key = os.environ.get("CLARK_API_KEY")
|
|
29
|
+
if not api_key:
|
|
30
|
+
print(
|
|
31
|
+
"CLARK_API_KEY is not set -- skipping live smoke test.\n"
|
|
32
|
+
"Set CLARK_API_KEY (and optionally CLARK_API_BASE_URL, CLARK_TEST_MODEL) "
|
|
33
|
+
"to run this against a real Clark deployment.",
|
|
34
|
+
file=sys.stderr,
|
|
35
|
+
)
|
|
36
|
+
return 0
|
|
37
|
+
|
|
38
|
+
base_url = os.environ.get("CLARK_API_BASE_URL", "https://www.clarkchat.com")
|
|
39
|
+
model = os.environ.get("CLARK_TEST_MODEL", "openrouter:qwen35_flash")
|
|
40
|
+
|
|
41
|
+
with ClarkClient(api_key=api_key, base_url=base_url) as client:
|
|
42
|
+
print(f"Listing models from {base_url} ...")
|
|
43
|
+
models = client.models.list()
|
|
44
|
+
for m in models.data:
|
|
45
|
+
print(f" - {m.id} ({m.label})")
|
|
46
|
+
|
|
47
|
+
print(f"\nRunning a trivial non-streaming chat completion with model={model!r} ...")
|
|
48
|
+
try:
|
|
49
|
+
completion = client.chat.completions.create(
|
|
50
|
+
model=model,
|
|
51
|
+
messages=[{"role": "user", "content": "Reply with exactly the word: pong"}],
|
|
52
|
+
)
|
|
53
|
+
except ClarkApiError as err:
|
|
54
|
+
print(f"Chat completion failed: {err.status_code} {err.type}: {err.message}", file=sys.stderr)
|
|
55
|
+
return 1
|
|
56
|
+
|
|
57
|
+
print("Response:")
|
|
58
|
+
print(f" id: {completion.id}")
|
|
59
|
+
print(f" finish_reason: {completion.choices[0].finish_reason if completion.choices else None}")
|
|
60
|
+
print(f" content: {completion.output_text!r}")
|
|
61
|
+
if completion.usage:
|
|
62
|
+
print(f" usage: {completion.usage}")
|
|
63
|
+
|
|
64
|
+
return 0
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
if __name__ == "__main__":
|
|
68
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "clark-platform-client"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Typed sync/async Python client for the Clark Platform API"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.9"
|
|
7
|
+
license = { text = "MIT" }
|
|
8
|
+
authors = [{ name = "Clark Labs" }]
|
|
9
|
+
keywords = ["clark", "clark-labs", "api-client", "agent", "httpx"]
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Programming Language :: Python :: 3",
|
|
12
|
+
"Programming Language :: Python :: 3.9",
|
|
13
|
+
"Programming Language :: Python :: 3.10",
|
|
14
|
+
"Programming Language :: Python :: 3.11",
|
|
15
|
+
"Programming Language :: Python :: 3.12",
|
|
16
|
+
"Operating System :: OS Independent",
|
|
17
|
+
"Typing :: Typed",
|
|
18
|
+
]
|
|
19
|
+
dependencies = ["httpx>=0.24"]
|
|
20
|
+
|
|
21
|
+
[project.optional-dependencies]
|
|
22
|
+
dev = ["pytest>=7.4", "pytest-asyncio>=0.21"]
|
|
23
|
+
|
|
24
|
+
[project.urls]
|
|
25
|
+
Homepage = "https://www.clarkchat.com"
|
|
26
|
+
|
|
27
|
+
[build-system]
|
|
28
|
+
requires = ["hatchling"]
|
|
29
|
+
build-backend = "hatchling.build"
|
|
30
|
+
|
|
31
|
+
[tool.hatch.build.targets.wheel]
|
|
32
|
+
packages = ["src/clark_platform"]
|
|
33
|
+
|
|
34
|
+
[tool.pytest.ini_options]
|
|
35
|
+
asyncio_mode = "auto"
|
|
36
|
+
testpaths = ["tests"]
|
|
37
|
+
|
|
38
|
+
[dependency-groups]
|
|
39
|
+
dev = ["pytest>=7.4", "pytest-asyncio>=0.21"]
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Typed sync/async Python client for the Clark Platform API.
|
|
2
|
+
|
|
3
|
+
See https://www.clarkchat.com and ``platform/clients/API_CONTRACT.md`` in the
|
|
4
|
+
Clark monorepo for the full wire contract this client implements.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .async_client import AsyncClarkClient
|
|
8
|
+
from .client import ClarkClient
|
|
9
|
+
from .errors import ClarkApiError
|
|
10
|
+
from .models import (
|
|
11
|
+
Artifact,
|
|
12
|
+
Capabilities,
|
|
13
|
+
ChatChoice,
|
|
14
|
+
ChatCompletionChunk,
|
|
15
|
+
ChatCompletionObject,
|
|
16
|
+
ChatMessage,
|
|
17
|
+
ClarkMetadata,
|
|
18
|
+
Cost,
|
|
19
|
+
MemoryList,
|
|
20
|
+
MemoryRecord,
|
|
21
|
+
ModelInfo,
|
|
22
|
+
ModelList,
|
|
23
|
+
ModelOption,
|
|
24
|
+
OutputMessage,
|
|
25
|
+
Pricing,
|
|
26
|
+
ResponseError,
|
|
27
|
+
ResponseEvent,
|
|
28
|
+
ResponseEventList,
|
|
29
|
+
ResponseObject,
|
|
30
|
+
ResponseStreamEvent,
|
|
31
|
+
Usage,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
__version__ = "0.1.0"
|
|
35
|
+
|
|
36
|
+
__all__ = [
|
|
37
|
+
"ClarkClient",
|
|
38
|
+
"AsyncClarkClient",
|
|
39
|
+
"ClarkApiError",
|
|
40
|
+
"ModelInfo",
|
|
41
|
+
"ModelOption",
|
|
42
|
+
"ModelList",
|
|
43
|
+
"Pricing",
|
|
44
|
+
"Capabilities",
|
|
45
|
+
"ResponseObject",
|
|
46
|
+
"ResponseStreamEvent",
|
|
47
|
+
"ResponseEvent",
|
|
48
|
+
"ResponseEventList",
|
|
49
|
+
"OutputMessage",
|
|
50
|
+
"Artifact",
|
|
51
|
+
"ClarkMetadata",
|
|
52
|
+
"ResponseError",
|
|
53
|
+
"Cost",
|
|
54
|
+
"Usage",
|
|
55
|
+
"ChatCompletionObject",
|
|
56
|
+
"ChatCompletionChunk",
|
|
57
|
+
"ChatChoice",
|
|
58
|
+
"ChatMessage",
|
|
59
|
+
"MemoryRecord",
|
|
60
|
+
"MemoryList",
|
|
61
|
+
]
|