clark-platform-client 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.
@@ -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
+ ]
@@ -0,0 +1,123 @@
1
+ """Internal helpers for building request payloads.
2
+
3
+ Shared by both the sync and async clients so the two stay in lockstep.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from typing import Any, Dict, List, Optional, Union
9
+
10
+ DEFAULT_BASE_URL = "https://www.clarkchat.com"
11
+
12
+ # Non-background responses/chat-completions calls can legitimately take up to
13
+ # ~120s server-side (CLARK_PLATFORM_RESPONSE_WAIT_MS default), so the client
14
+ # default timeout must comfortably exceed that.
15
+ DEFAULT_TIMEOUT = 150.0
16
+
17
+ ResponseInput = Union[str, List[Dict[str, Any]]]
18
+
19
+
20
+ def omit_none(d: Dict[str, Any]) -> Dict[str, Any]:
21
+ return {k: v for k, v in d.items() if v is not None}
22
+
23
+
24
+ def build_responses_payload(
25
+ *,
26
+ model: str,
27
+ input: ResponseInput, # noqa: A002 - mirrors wire field name
28
+ tier_model_id: Optional[str] = None,
29
+ conversation_id: Optional[str] = None,
30
+ memory_scope: Optional[str] = None,
31
+ previous_response_id: Optional[str] = None,
32
+ background: Optional[bool] = None,
33
+ metadata: Optional[Dict[str, Any]] = None,
34
+ stream: Optional[bool] = None,
35
+ ) -> Dict[str, Any]:
36
+ return omit_none(
37
+ {
38
+ "model": model,
39
+ "tier_model_id": tier_model_id,
40
+ "input": input,
41
+ "conversation_id": conversation_id,
42
+ "memory_scope": memory_scope,
43
+ "previous_response_id": previous_response_id,
44
+ "background": background,
45
+ "metadata": metadata,
46
+ "stream": stream,
47
+ }
48
+ )
49
+
50
+
51
+ def build_chat_completions_payload(
52
+ *,
53
+ model: str,
54
+ messages: List[Dict[str, Any]],
55
+ tier_model_id: Optional[str] = None,
56
+ conversation_id: Optional[str] = None,
57
+ memory_scope: Optional[str] = None,
58
+ previous_response_id: Optional[str] = None,
59
+ stream: Optional[bool] = None,
60
+ stream_options: Optional[Dict[str, Any]] = None,
61
+ metadata: Optional[Dict[str, Any]] = None,
62
+ tools: Optional[List[Dict[str, Any]]] = None,
63
+ tool_choice: Optional[Any] = None,
64
+ temperature: Optional[float] = None,
65
+ top_p: Optional[float] = None,
66
+ max_tokens: Optional[int] = None,
67
+ stop: Optional[Union[str, List[str]]] = None,
68
+ parallel_tool_calls: Optional[bool] = None,
69
+ reasoning_effort: Optional[str] = None,
70
+ ) -> Dict[str, Any]:
71
+ return omit_none(
72
+ {
73
+ "model": model,
74
+ "tier_model_id": tier_model_id,
75
+ "messages": messages,
76
+ "conversation_id": conversation_id,
77
+ "memory_scope": memory_scope,
78
+ "previous_response_id": previous_response_id,
79
+ "stream": stream,
80
+ "stream_options": stream_options,
81
+ "metadata": metadata,
82
+ "tools": tools,
83
+ "tool_choice": tool_choice,
84
+ "temperature": temperature,
85
+ "top_p": top_p,
86
+ "max_tokens": max_tokens,
87
+ "stop": stop,
88
+ "parallel_tool_calls": parallel_tool_calls,
89
+ "reasoning_effort": reasoning_effort,
90
+ }
91
+ )
92
+
93
+
94
+ def build_events_params(
95
+ *,
96
+ after_seq: Optional[int] = None,
97
+ limit: Optional[int] = None,
98
+ types: Optional[List[str]] = None,
99
+ ) -> Dict[str, Any]:
100
+ params: Dict[str, Any] = {}
101
+ if after_seq is not None:
102
+ params["after_seq"] = after_seq
103
+ if limit is not None:
104
+ params["limit"] = limit
105
+ if types:
106
+ params["types"] = ",".join(types)
107
+ return params
108
+
109
+
110
+ def build_memories_params(
111
+ *,
112
+ q: Optional[str] = None,
113
+ tags: Optional[List[str]] = None,
114
+ conversation_id: Optional[str] = None,
115
+ ) -> Dict[str, Any]:
116
+ params: Dict[str, Any] = {}
117
+ if q is not None:
118
+ params["q"] = q
119
+ if tags:
120
+ params["tags"] = ",".join(tags)
121
+ if conversation_id is not None:
122
+ params["conversation_id"] = conversation_id
123
+ return params
@@ -0,0 +1,380 @@
1
+ """Asynchronous client for the Clark Platform API.
2
+
3
+ Mirrors :mod:`clark_platform.client` method-for-method; see its docstrings
4
+ for behavioral details not repeated here.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ from typing import Any, AsyncIterator, Dict, List, Optional, Union
11
+
12
+ import httpx
13
+
14
+ from ._payloads import (
15
+ DEFAULT_BASE_URL,
16
+ DEFAULT_TIMEOUT,
17
+ ResponseInput,
18
+ build_chat_completions_payload,
19
+ build_events_params,
20
+ build_memories_params,
21
+ build_responses_payload,
22
+ )
23
+ from .errors import ClarkApiError
24
+ from .models import (
25
+ ChatCompletionChunk,
26
+ ChatCompletionObject,
27
+ MemoryList,
28
+ ModelList,
29
+ ResponseEventList,
30
+ ResponseObject,
31
+ ResponseStreamEvent,
32
+ )
33
+ from .streaming import DONE_SENTINEL, aiter_sse_events
34
+
35
+
36
+ class AsyncClarkClient:
37
+ """Async client for the Clark Platform API.
38
+
39
+ Example::
40
+
41
+ from clark_platform import AsyncClarkClient
42
+
43
+ async with AsyncClarkClient(api_key="clk_live_...") as client:
44
+ response = await client.responses.create(model="clark", input="hello")
45
+ print(response.output_text)
46
+ """
47
+
48
+ def __init__(
49
+ self,
50
+ api_key: Optional[str] = None,
51
+ *,
52
+ base_url: str = DEFAULT_BASE_URL,
53
+ timeout: float = DEFAULT_TIMEOUT,
54
+ http_client: Optional[httpx.AsyncClient] = None,
55
+ ) -> None:
56
+ self.api_key = api_key
57
+ self.base_url = base_url.rstrip("/")
58
+ self._owns_http_client = http_client is None
59
+ self._http = http_client or httpx.AsyncClient(timeout=timeout)
60
+
61
+ self.models = AsyncModelsResource(self)
62
+ self.responses = AsyncResponsesResource(self)
63
+ self.chat = AsyncChatResource(self)
64
+ self.memories = AsyncMemoriesResource(self)
65
+
66
+ async def close(self) -> None:
67
+ if self._owns_http_client:
68
+ await self._http.aclose()
69
+
70
+ async def __aenter__(self) -> "AsyncClarkClient":
71
+ return self
72
+
73
+ async def __aexit__(self, *exc_info: Any) -> None:
74
+ await self.close()
75
+
76
+ # -- internal helpers ---------------------------------------------------
77
+
78
+ def _headers(self) -> Dict[str, str]:
79
+ headers = {"Content-Type": "application/json", "Accept": "application/json"}
80
+ if self.api_key:
81
+ headers["Authorization"] = f"Bearer {self.api_key}"
82
+ return headers
83
+
84
+ def _url(self, path: str) -> str:
85
+ return f"{self.base_url}{path}"
86
+
87
+ @staticmethod
88
+ def _error_from_response(response: httpx.Response) -> ClarkApiError:
89
+ try:
90
+ body = response.json()
91
+ except ValueError:
92
+ return ClarkApiError.from_unparsable_body(response.status_code, response.text)
93
+ return ClarkApiError.from_error_body(response.status_code, body)
94
+
95
+ async def request_json(
96
+ self,
97
+ method: str,
98
+ path: str,
99
+ *,
100
+ params: Optional[Dict[str, Any]] = None,
101
+ json_body: Optional[Dict[str, Any]] = None,
102
+ ) -> Dict[str, Any]:
103
+ response = await self._http.request(
104
+ method, self._url(path), params=params, json=json_body, headers=self._headers()
105
+ )
106
+ if response.status_code >= 400:
107
+ raise self._error_from_response(response)
108
+ try:
109
+ return response.json()
110
+ except ValueError as exc:
111
+ raise ClarkApiError(
112
+ f"Failed to parse JSON response body: {exc}",
113
+ status_code=response.status_code,
114
+ ) from exc
115
+
116
+ async def stream_lines(
117
+ self,
118
+ method: str,
119
+ path: str,
120
+ *,
121
+ json_body: Optional[Dict[str, Any]] = None,
122
+ ) -> AsyncIterator[str]:
123
+ async with self._http.stream(
124
+ method, self._url(path), json=json_body, headers=self._headers()
125
+ ) as response:
126
+ if response.status_code >= 400:
127
+ await response.aread()
128
+ raise self._error_from_response(response)
129
+ async for line in response.aiter_lines():
130
+ yield line
131
+
132
+
133
+ class AsyncModelsResource:
134
+ def __init__(self, client: AsyncClarkClient) -> None:
135
+ self._client = client
136
+
137
+ async def list(self) -> ModelList:
138
+ body = await self._client.request_json("GET", "/v1/models")
139
+ return ModelList.from_dict(body)
140
+
141
+
142
+ class AsyncResponsesResource:
143
+ def __init__(self, client: AsyncClarkClient) -> None:
144
+ self._client = client
145
+
146
+ async def create(
147
+ self,
148
+ *,
149
+ model: str,
150
+ input: ResponseInput, # noqa: A002
151
+ tier_model_id: Optional[str] = None,
152
+ conversation_id: Optional[str] = None,
153
+ memory_scope: Optional[str] = None,
154
+ previous_response_id: Optional[str] = None,
155
+ background: Optional[bool] = None,
156
+ metadata: Optional[Dict[str, Any]] = None,
157
+ ) -> ResponseObject:
158
+ payload = build_responses_payload(
159
+ model=model,
160
+ input=input,
161
+ tier_model_id=tier_model_id,
162
+ conversation_id=conversation_id,
163
+ memory_scope=memory_scope,
164
+ previous_response_id=previous_response_id,
165
+ background=background,
166
+ metadata=metadata,
167
+ stream=False,
168
+ )
169
+ body = await self._client.request_json("POST", "/v1/responses", json_body=payload)
170
+ return ResponseObject.from_dict(body)
171
+
172
+ async def stream(
173
+ self,
174
+ *,
175
+ model: str,
176
+ input: ResponseInput, # noqa: A002
177
+ tier_model_id: Optional[str] = None,
178
+ conversation_id: Optional[str] = None,
179
+ memory_scope: Optional[str] = None,
180
+ previous_response_id: Optional[str] = None,
181
+ metadata: Optional[Dict[str, Any]] = None,
182
+ ) -> AsyncIterator[ResponseStreamEvent]:
183
+ payload = build_responses_payload(
184
+ model=model,
185
+ input=input,
186
+ tier_model_id=tier_model_id,
187
+ conversation_id=conversation_id,
188
+ memory_scope=memory_scope,
189
+ previous_response_id=previous_response_id,
190
+ metadata=metadata,
191
+ stream=True,
192
+ )
193
+ lines = self._client.stream_lines("POST", "/v1/responses", json_body=payload)
194
+ async for event_name, data in aiter_sse_events(lines):
195
+ if data == DONE_SENTINEL:
196
+ return
197
+ obj = json.loads(data)
198
+ yield ResponseStreamEvent.from_dict(obj, event_name)
199
+
200
+ async def get(self, response_id: str) -> ResponseObject:
201
+ body = await self._client.request_json("GET", f"/v1/responses/{response_id}")
202
+ return ResponseObject.from_dict(body)
203
+
204
+ async def list_events(
205
+ self,
206
+ response_id: str,
207
+ *,
208
+ after_seq: Optional[int] = None,
209
+ limit: Optional[int] = None,
210
+ types: Optional[List[str]] = None,
211
+ ) -> ResponseEventList:
212
+ params = build_events_params(after_seq=after_seq, limit=limit, types=types)
213
+ body = await self._client.request_json(
214
+ "GET", f"/v1/responses/{response_id}/events", params=params
215
+ )
216
+ return ResponseEventList.from_dict(body)
217
+
218
+
219
+ class AsyncChatCompletionsResource:
220
+ def __init__(self, client: AsyncClarkClient) -> None:
221
+ self._client = client
222
+
223
+ async def create(
224
+ self,
225
+ *,
226
+ model: str,
227
+ messages: List[Dict[str, Any]],
228
+ tier_model_id: Optional[str] = None,
229
+ conversation_id: Optional[str] = None,
230
+ memory_scope: Optional[str] = None,
231
+ previous_response_id: Optional[str] = None,
232
+ stream_options: Optional[Dict[str, Any]] = None,
233
+ metadata: Optional[Dict[str, Any]] = None,
234
+ temperature: Optional[float] = None,
235
+ top_p: Optional[float] = None,
236
+ max_tokens: Optional[int] = None,
237
+ stop: Optional[Union[str, List[str]]] = None,
238
+ ) -> ChatCompletionObject:
239
+ payload = build_chat_completions_payload(
240
+ model=model,
241
+ messages=messages,
242
+ tier_model_id=tier_model_id,
243
+ conversation_id=conversation_id,
244
+ memory_scope=memory_scope,
245
+ previous_response_id=previous_response_id,
246
+ stream=False,
247
+ stream_options=stream_options,
248
+ metadata=metadata,
249
+ temperature=temperature,
250
+ top_p=top_p,
251
+ max_tokens=max_tokens,
252
+ stop=stop,
253
+ )
254
+ body = await self._client.request_json("POST", "/v1/chat/completions", json_body=payload)
255
+ return ChatCompletionObject.from_dict(body)
256
+
257
+ async def stream(
258
+ self,
259
+ *,
260
+ model: str,
261
+ messages: List[Dict[str, Any]],
262
+ tier_model_id: Optional[str] = None,
263
+ conversation_id: Optional[str] = None,
264
+ memory_scope: Optional[str] = None,
265
+ previous_response_id: Optional[str] = None,
266
+ stream_options: Optional[Dict[str, Any]] = None,
267
+ metadata: Optional[Dict[str, Any]] = None,
268
+ temperature: Optional[float] = None,
269
+ top_p: Optional[float] = None,
270
+ max_tokens: Optional[int] = None,
271
+ stop: Optional[Union[str, List[str]]] = None,
272
+ ) -> AsyncIterator[ChatCompletionChunk]:
273
+ payload = build_chat_completions_payload(
274
+ model=model,
275
+ messages=messages,
276
+ tier_model_id=tier_model_id,
277
+ conversation_id=conversation_id,
278
+ memory_scope=memory_scope,
279
+ previous_response_id=previous_response_id,
280
+ stream=True,
281
+ stream_options=stream_options,
282
+ metadata=metadata,
283
+ temperature=temperature,
284
+ top_p=top_p,
285
+ max_tokens=max_tokens,
286
+ stop=stop,
287
+ )
288
+ lines = self._client.stream_lines("POST", "/v1/chat/completions", json_body=payload)
289
+ async for _event_name, data in aiter_sse_events(lines):
290
+ if data == DONE_SENTINEL:
291
+ return
292
+ obj = json.loads(data)
293
+ yield ChatCompletionChunk.from_dict(obj)
294
+
295
+ async def create_passthrough(
296
+ self,
297
+ *,
298
+ model: str,
299
+ messages: List[Dict[str, Any]],
300
+ tools: Optional[List[Dict[str, Any]]] = None,
301
+ tool_choice: Optional[Any] = None,
302
+ temperature: Optional[float] = None,
303
+ top_p: Optional[float] = None,
304
+ max_tokens: Optional[int] = None,
305
+ stop: Optional[Union[str, List[str]]] = None,
306
+ parallel_tool_calls: Optional[bool] = None,
307
+ reasoning_effort: Optional[str] = None,
308
+ metadata: Optional[Dict[str, Any]] = None,
309
+ ) -> Dict[str, Any]:
310
+ payload = build_chat_completions_payload(
311
+ model=model,
312
+ messages=messages,
313
+ stream=False,
314
+ tools=tools,
315
+ tool_choice=tool_choice,
316
+ temperature=temperature,
317
+ top_p=top_p,
318
+ max_tokens=max_tokens,
319
+ stop=stop,
320
+ parallel_tool_calls=parallel_tool_calls,
321
+ reasoning_effort=reasoning_effort,
322
+ metadata=metadata,
323
+ )
324
+ return await self._client.request_json("POST", "/v1/chat/completions", json_body=payload)
325
+
326
+ async def stream_passthrough(
327
+ self,
328
+ *,
329
+ model: str,
330
+ messages: List[Dict[str, Any]],
331
+ tools: Optional[List[Dict[str, Any]]] = None,
332
+ tool_choice: Optional[Any] = None,
333
+ temperature: Optional[float] = None,
334
+ top_p: Optional[float] = None,
335
+ max_tokens: Optional[int] = None,
336
+ stop: Optional[Union[str, List[str]]] = None,
337
+ parallel_tool_calls: Optional[bool] = None,
338
+ reasoning_effort: Optional[str] = None,
339
+ metadata: Optional[Dict[str, Any]] = None,
340
+ ) -> AsyncIterator[Dict[str, Any]]:
341
+ payload = build_chat_completions_payload(
342
+ model=model,
343
+ messages=messages,
344
+ stream=True,
345
+ tools=tools,
346
+ tool_choice=tool_choice,
347
+ temperature=temperature,
348
+ top_p=top_p,
349
+ max_tokens=max_tokens,
350
+ stop=stop,
351
+ parallel_tool_calls=parallel_tool_calls,
352
+ reasoning_effort=reasoning_effort,
353
+ metadata=metadata,
354
+ )
355
+ lines = self._client.stream_lines("POST", "/v1/chat/completions", json_body=payload)
356
+ async for _event_name, data in aiter_sse_events(lines):
357
+ if data == DONE_SENTINEL:
358
+ return
359
+ yield json.loads(data)
360
+
361
+
362
+ class AsyncChatResource:
363
+ def __init__(self, client: AsyncClarkClient) -> None:
364
+ self.completions = AsyncChatCompletionsResource(client)
365
+
366
+
367
+ class AsyncMemoriesResource:
368
+ def __init__(self, client: AsyncClarkClient) -> None:
369
+ self._client = client
370
+
371
+ async def list(
372
+ self,
373
+ *,
374
+ q: Optional[str] = None,
375
+ tags: Optional[List[str]] = None,
376
+ conversation_id: Optional[str] = None,
377
+ ) -> MemoryList:
378
+ params = build_memories_params(q=q, tags=tags, conversation_id=conversation_id)
379
+ body = await self._client.request_json("GET", "/v1/memories", params=params)
380
+ return MemoryList.from_dict(body)