clark-platform-client-js 0.1.0
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.
- package/README.md +234 -0
- package/package.json +33 -0
- package/src/client.js +46 -0
- package/src/errors.js +103 -0
- package/src/http.js +159 -0
- package/src/index.js +3 -0
- package/src/resources/artifacts.js +40 -0
- package/src/resources/chat_completions.js +209 -0
- package/src/resources/memories.js +36 -0
- package/src/resources/models.js +55 -0
- package/src/resources/responses.js +186 -0
- package/src/sse.js +101 -0
- package/types/index.d.ts +293 -0
package/README.md
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
# clark-platform-client-js
|
|
2
|
+
|
|
3
|
+
A zero-build-step, plain-JavaScript client for the [Clark Platform
|
|
4
|
+
API](../API_CONTRACT.md). Native ESM, no `tsc`/bundler required to consume it
|
|
5
|
+
— the files you `import` are the files that ship. Works in Node 18+ and any
|
|
6
|
+
modern browser that supports native ESM + `fetch`.
|
|
7
|
+
|
|
8
|
+
This package is **not** the TypeScript client at `platform/clients/typescript`
|
|
9
|
+
— see [How this differs from the TypeScript client](#how-this-differs-from-the-typescript-client)
|
|
10
|
+
below.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
Not published to npm yet. Use it locally:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install /Users/stan/Documents/git/clark/platform/clients/javascript
|
|
18
|
+
# or, from another package in this monorepo:
|
|
19
|
+
npm install ../../platform/clients/javascript
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Or, in a browser, import it directly with no package manager at all:
|
|
23
|
+
|
|
24
|
+
```html
|
|
25
|
+
<script type="module">
|
|
26
|
+
import { ClarkClient } from "./node_modules/clark-platform-client-js/src/index.js";
|
|
27
|
+
const client = new ClarkClient({ apiKey: "clk_live_..." });
|
|
28
|
+
</script>
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Quickstart: agentic (`clark`, `clark_max`, `openrouter:*`)
|
|
32
|
+
|
|
33
|
+
```js
|
|
34
|
+
import { ClarkClient } from "clark-platform-client-js";
|
|
35
|
+
|
|
36
|
+
const client = new ClarkClient({
|
|
37
|
+
apiKey: process.env.CLARK_API_KEY, // clk_live_...
|
|
38
|
+
// baseUrl: "https://dev.clarkslabs.com", // defaults to https://www.clarkchat.com
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// List available model tiers/options (never hardcode tier ids — they're dynamic).
|
|
42
|
+
const { data: models } = await client.models.list();
|
|
43
|
+
|
|
44
|
+
// One-shot Responses call.
|
|
45
|
+
const response = await client.responses.create({
|
|
46
|
+
model: "clark",
|
|
47
|
+
input: "Summarize the latest Clark release notes.",
|
|
48
|
+
});
|
|
49
|
+
console.log(response.output[0].content[0].text);
|
|
50
|
+
|
|
51
|
+
// Equivalent via the OpenAI-shaped chat.completions facade.
|
|
52
|
+
const completion = await client.chat.completions.create({
|
|
53
|
+
model: "openrouter:qwen35_flash",
|
|
54
|
+
messages: [{ role: "user", content: "Summarize the latest Clark release notes." }],
|
|
55
|
+
});
|
|
56
|
+
console.log(completion.choices[0].message.content);
|
|
57
|
+
|
|
58
|
+
// Multi-turn: resume a prior response's Clark conversation.
|
|
59
|
+
const followUp = await client.responses.create({
|
|
60
|
+
model: "clark",
|
|
61
|
+
input: "Now do the same for last week.",
|
|
62
|
+
previousResponseId: response.id,
|
|
63
|
+
});
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
`tools`/`tool_choice` are rejected with `400 unsupported_parameter` for these
|
|
67
|
+
agentic tiers on both endpoints — Clark runs its own internal tool loop and
|
|
68
|
+
only returns the final projected answer.
|
|
69
|
+
|
|
70
|
+
## Streaming
|
|
71
|
+
|
|
72
|
+
### `responses.stream()` — named SSE events
|
|
73
|
+
|
|
74
|
+
```js
|
|
75
|
+
for await (const event of client.responses.stream({ model: "clark", input: "hi" })) {
|
|
76
|
+
switch (event.type) {
|
|
77
|
+
case "response.output_text.delta":
|
|
78
|
+
// Note: the full answer arrives as ONE delta, not token-by-token.
|
|
79
|
+
process.stdout.write(event.delta);
|
|
80
|
+
break;
|
|
81
|
+
case "response.artifact.completed":
|
|
82
|
+
console.log("artifact:", event.artifact.name);
|
|
83
|
+
break;
|
|
84
|
+
case "response.completed":
|
|
85
|
+
console.log("done:", event.response.status);
|
|
86
|
+
break;
|
|
87
|
+
case "response.failed":
|
|
88
|
+
console.error("failed:", event.error ?? event.response?.error);
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### `chat.completions.stream()` — OpenAI-style chunks, ending in `[DONE]`
|
|
95
|
+
|
|
96
|
+
```js
|
|
97
|
+
for await (const chunk of client.chat.completions.stream({
|
|
98
|
+
model: "clark",
|
|
99
|
+
messages: [{ role: "user", content: "hi" }],
|
|
100
|
+
streamOptions: { include_usage: true },
|
|
101
|
+
})) {
|
|
102
|
+
const delta = chunk.choices[0]?.delta;
|
|
103
|
+
if (delta?.content) process.stdout.write(delta.content);
|
|
104
|
+
if (chunk.usage) console.log("\nusage:", chunk.usage);
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
The generator returns (stream ends) when the server sends the literal
|
|
109
|
+
`data: [DONE]` line — you never see `"[DONE]"` as a yielded value.
|
|
110
|
+
|
|
111
|
+
## The `clark-code` passthrough tier
|
|
112
|
+
|
|
113
|
+
`clark-code` behaves nothing like the agentic tiers: the **entire**
|
|
114
|
+
`messages` array (including prior `assistant` messages with `tool_calls` and
|
|
115
|
+
`tool` role messages) is forwarded verbatim to the upstream OpenAI-compatible
|
|
116
|
+
provider, `tools`/`tool_choice` are required rather than rejected, and the
|
|
117
|
+
response is the **raw upstream OpenAI-shaped object** — not a Clark
|
|
118
|
+
`ResponseObject`/`ChatCompletionObject`. This client exposes it via distinct
|
|
119
|
+
methods so you can't confuse the two shapes:
|
|
120
|
+
|
|
121
|
+
```js
|
|
122
|
+
const result = await client.chat.completions.createPassthrough({
|
|
123
|
+
model: "clark-code",
|
|
124
|
+
messages: [{ role: "user", content: "What's the weather in Tokyo?" }],
|
|
125
|
+
tools: [
|
|
126
|
+
{
|
|
127
|
+
type: "function",
|
|
128
|
+
function: { name: "get_weather", parameters: { type: "object", properties: { city: { type: "string" } } } },
|
|
129
|
+
},
|
|
130
|
+
],
|
|
131
|
+
toolChoice: "auto",
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const toolCall = result.choices[0].message.tool_calls?.[0];
|
|
135
|
+
if (toolCall) {
|
|
136
|
+
// Run the tool yourself — Clark does not execute tools for this tier —
|
|
137
|
+
// then append an assistant message (with tool_calls) and a `tool` message
|
|
138
|
+
// with the result, and call createPassthrough again with the full history.
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
`streamPassthrough(...)` is the streaming counterpart, yielding the
|
|
143
|
+
provider's native `chat.completion.chunk`-shaped deltas (including
|
|
144
|
+
`tool_calls` deltas) and ending on `[DONE]`, same as above.
|
|
145
|
+
|
|
146
|
+
Do not send `conversationId`/`previousResponseId`/`memoryScope` for this
|
|
147
|
+
tier — there is no Clark conversation or memory scoping involved.
|
|
148
|
+
|
|
149
|
+
## Other endpoints
|
|
150
|
+
|
|
151
|
+
```js
|
|
152
|
+
// Poll a response by id (e.g. after background: true, or to reconstruct
|
|
153
|
+
// a timed-out chat-completions call via chatcmpl_<id> -> resp_<id>).
|
|
154
|
+
const polled = await client.responses.get("resp_01jz4n8h2f7g9k4q2m6s");
|
|
155
|
+
|
|
156
|
+
// Poll public progress events.
|
|
157
|
+
const { data: events, next_after_seq } = await client.responses.listEvents(
|
|
158
|
+
"resp_01jz4n8h2f7g9k4q2m6s",
|
|
159
|
+
{ afterSeq: 0, limit: 200 }
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
// Durable memory records.
|
|
163
|
+
const { data: memories } = await client.memories.list({ q: "release notes" });
|
|
164
|
+
|
|
165
|
+
// Prefer artifacts[].download_url; this is available for completeness.
|
|
166
|
+
const fileResponse = await client.artifacts.download("conv-id", "analysis.md");
|
|
167
|
+
const text = await fileResponse.text();
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## Errors
|
|
171
|
+
|
|
172
|
+
Every non-2xx response and network/parse failure raises `ClarkApiError`:
|
|
173
|
+
|
|
174
|
+
```js
|
|
175
|
+
import { ClarkApiError } from "clark-platform-client-js";
|
|
176
|
+
|
|
177
|
+
try {
|
|
178
|
+
await client.chat.completions.create({ model: "clark", messages: [...], tools: [...] });
|
|
179
|
+
} catch (err) {
|
|
180
|
+
if (err instanceof ClarkApiError) {
|
|
181
|
+
console.error(err.status, err.type, err.code, err.param, err.message);
|
|
182
|
+
// 400 invalid_request_error invalid_request_error tools "..."
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## Configuration
|
|
188
|
+
|
|
189
|
+
```js
|
|
190
|
+
new ClarkClient({
|
|
191
|
+
apiKey: "clk_live_...", // required
|
|
192
|
+
baseUrl: "https://www.clarkchat.com", // default; also https://dev.clarkslabs.com
|
|
193
|
+
timeoutMs: 130_000, // default; server-side non-background wait budget is ~120s
|
|
194
|
+
fetchFn: myCustomFetch, // optional override, mainly for tests/non-global-fetch runtimes
|
|
195
|
+
});
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Every call also accepts a `signal: AbortSignal` for caller-driven
|
|
199
|
+
cancellation; it's merged with the client's own timeout signal.
|
|
200
|
+
|
|
201
|
+
## Testing
|
|
202
|
+
|
|
203
|
+
```bash
|
|
204
|
+
npm test # node --test, fully offline against a local mock HTTP server
|
|
205
|
+
npm run check # node --check examples/live-smoke.js (syntax only, no network)
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
`examples/live-smoke.js` is an opt-in, real-network smoke test:
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
CLARK_API_BASE_URL=https://dev.clarkslabs.com \
|
|
212
|
+
CLARK_API_KEY=clk_live_xxxx \
|
|
213
|
+
CLARK_TEST_MODEL=openrouter:qwen35_flash \
|
|
214
|
+
node examples/live-smoke.js
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
## How this differs from the TypeScript client
|
|
218
|
+
|
|
219
|
+
`platform/clients/typescript` is a compiled TypeScript package meant for
|
|
220
|
+
TypeScript projects with a build step. This package (`clark-platform-client-js`)
|
|
221
|
+
is a **separate, independent implementation** built from the same
|
|
222
|
+
[`API_CONTRACT.md`](../API_CONTRACT.md), for consumers who want:
|
|
223
|
+
|
|
224
|
+
- Plain `.js` source with JSDoc comments for editor autocomplete/type-checking
|
|
225
|
+
(via `checkJs`), but no `tsc`/bundler step — the shipped files are the
|
|
226
|
+
source files.
|
|
227
|
+
- Native ESM (`"type": "module"`) that works unmodified in both Node and
|
|
228
|
+
browsers via `<script type="module">`.
|
|
229
|
+
- Zero runtime dependencies and a test suite on Node's built-in `node --test`
|
|
230
|
+
runner (no Jest/Vitest) for a genuinely lightweight footprint.
|
|
231
|
+
|
|
232
|
+
A hand-written `types/index.d.ts` is included for TypeScript consumers who
|
|
233
|
+
want IntelliSense against this package, but it is not required to use it and
|
|
234
|
+
is not generated by a build step.
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "clark-platform-client-js",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Zero-build-step, plain-JavaScript client for the Clark Platform API (OpenAI-compatible-ish, with extras). Native ESM, no compile step required to consume.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.js"
|
|
9
|
+
},
|
|
10
|
+
"types": "./types/index.d.ts",
|
|
11
|
+
"engines": {
|
|
12
|
+
"node": ">=18"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"src",
|
|
16
|
+
"types",
|
|
17
|
+
"README.md"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"test": "node --test",
|
|
21
|
+
"check": "node --check examples/live-smoke.js"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"clark",
|
|
25
|
+
"clark-platform",
|
|
26
|
+
"openai-compatible",
|
|
27
|
+
"sse",
|
|
28
|
+
"streaming"
|
|
29
|
+
],
|
|
30
|
+
"license": "UNLICENSED",
|
|
31
|
+
"devDependencies": {},
|
|
32
|
+
"dependencies": {}
|
|
33
|
+
}
|
package/src/client.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { HttpTransport, DEFAULT_BASE_URL, DEFAULT_TIMEOUT_MS } from "./http.js";
|
|
2
|
+
import { Models } from "./resources/models.js";
|
|
3
|
+
import { Responses } from "./resources/responses.js";
|
|
4
|
+
import { ChatCompletions } from "./resources/chat_completions.js";
|
|
5
|
+
import { Memories } from "./resources/memories.js";
|
|
6
|
+
import { Artifacts } from "./resources/artifacts.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Plain-JavaScript client for the Clark Platform API. No build step: import
|
|
10
|
+
* this module directly in Node (18+) or a browser via native ESM.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* import { ClarkClient } from "clark-platform-client-js";
|
|
14
|
+
* const client = new ClarkClient({ apiKey: process.env.CLARK_API_KEY });
|
|
15
|
+
* const { data } = await client.models.list();
|
|
16
|
+
*/
|
|
17
|
+
export class ClarkClient {
|
|
18
|
+
/**
|
|
19
|
+
* @param {Object} options
|
|
20
|
+
* @param {string} options.apiKey Bearer token, e.g. `clk_live_...`.
|
|
21
|
+
* @param {string} [options.baseUrl] Defaults to `https://www.clarkchat.com`.
|
|
22
|
+
* @param {number} [options.timeoutMs] Default request timeout; defaults to 130s to
|
|
23
|
+
* comfortably exceed the server's ~120s non-background wait budget.
|
|
24
|
+
* @param {typeof fetch} [options.fetchFn] Override the fetch implementation (mainly for tests).
|
|
25
|
+
*/
|
|
26
|
+
constructor(options) {
|
|
27
|
+
const http = new HttpTransport({
|
|
28
|
+
apiKey: options.apiKey,
|
|
29
|
+
baseUrl: options.baseUrl ?? DEFAULT_BASE_URL,
|
|
30
|
+
timeoutMs: options.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
31
|
+
fetchFn: options.fetchFn,
|
|
32
|
+
});
|
|
33
|
+
this._http = http;
|
|
34
|
+
|
|
35
|
+
/** `GET /v1/models`. */
|
|
36
|
+
this.models = new Models(http);
|
|
37
|
+
/** `POST/GET /v1/responses`, `GET /v1/responses/{id}/events`. */
|
|
38
|
+
this.responses = new Responses(http);
|
|
39
|
+
/** `POST /v1/chat/completions` (agentic + `clark-code` passthrough). */
|
|
40
|
+
this.chat = { completions: new ChatCompletions(http) };
|
|
41
|
+
/** `GET /v1/memories`. */
|
|
42
|
+
this.memories = new Memories(http);
|
|
43
|
+
/** `GET /v1/artifacts/{conversation_id}/{artifact_name}`. */
|
|
44
|
+
this.artifacts = new Artifacts(http);
|
|
45
|
+
}
|
|
46
|
+
}
|
package/src/errors.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {Object} ClarkErrorEnvelope
|
|
3
|
+
* @property {string} message
|
|
4
|
+
* @property {"invalid_request_error"|"authentication_error"|"permission_error"|"not_found_error"|"upstream_error"|"server_error"|string} type
|
|
5
|
+
* @property {string|null} [param]
|
|
6
|
+
* @property {string} [code]
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Error thrown for any non-2xx response from the Clark Platform API that
|
|
11
|
+
* carries the documented `{"error": {...}}` envelope (see API_CONTRACT.md).
|
|
12
|
+
*
|
|
13
|
+
* Also used (with a synthesized envelope) for network failures and
|
|
14
|
+
* malformed/unparseable responses so callers can catch a single error type.
|
|
15
|
+
*/
|
|
16
|
+
export class ClarkApiError extends Error {
|
|
17
|
+
/**
|
|
18
|
+
* @param {string} message
|
|
19
|
+
* @param {Object} [details]
|
|
20
|
+
* @param {number} [details.status] HTTP status code, if the error came from an HTTP response.
|
|
21
|
+
* @param {string} [details.type] Error type from the envelope (or a synthesized one).
|
|
22
|
+
* @param {string|null} [details.param]
|
|
23
|
+
* @param {string} [details.code]
|
|
24
|
+
* @param {unknown} [details.cause] Underlying cause (e.g. a fetch/network error).
|
|
25
|
+
* @param {Response} [details.response] The raw Response object, if available.
|
|
26
|
+
*/
|
|
27
|
+
constructor(message, details = {}) {
|
|
28
|
+
super(message);
|
|
29
|
+
this.name = "ClarkApiError";
|
|
30
|
+
this.status = details.status ?? null;
|
|
31
|
+
this.type = details.type ?? "client_error";
|
|
32
|
+
this.param = details.param ?? null;
|
|
33
|
+
this.code = details.code ?? details.type ?? "client_error";
|
|
34
|
+
this.cause = details.cause;
|
|
35
|
+
this.response = details.response;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Build a ClarkApiError from a fetch Response whose body is (expected to
|
|
40
|
+
* be) the standard `{"error": {...}}` JSON envelope. Falls back to a
|
|
41
|
+
* generic error if the body doesn't parse as JSON or doesn't match the
|
|
42
|
+
* envelope shape.
|
|
43
|
+
*
|
|
44
|
+
* @param {Response} response
|
|
45
|
+
* @returns {Promise<ClarkApiError>}
|
|
46
|
+
*/
|
|
47
|
+
static async fromResponse(response) {
|
|
48
|
+
let bodyText = "";
|
|
49
|
+
try {
|
|
50
|
+
bodyText = await response.text();
|
|
51
|
+
} catch (err) {
|
|
52
|
+
return new ClarkApiError(
|
|
53
|
+
`Clark API request failed with status ${response.status} and an unreadable body`,
|
|
54
|
+
{ status: response.status, type: "server_error", cause: err, response }
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let parsed;
|
|
59
|
+
try {
|
|
60
|
+
parsed = bodyText ? JSON.parse(bodyText) : null;
|
|
61
|
+
} catch (err) {
|
|
62
|
+
return new ClarkApiError(
|
|
63
|
+
`Clark API request failed with status ${response.status}: ${bodyText.slice(0, 500)}`,
|
|
64
|
+
{ status: response.status, type: "server_error", cause: err, response }
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const envelope = parsed && typeof parsed === "object" ? parsed.error : null;
|
|
69
|
+
if (envelope && typeof envelope === "object" && typeof envelope.message === "string") {
|
|
70
|
+
return new ClarkApiError(envelope.message, {
|
|
71
|
+
status: response.status,
|
|
72
|
+
type: envelope.type,
|
|
73
|
+
param: envelope.param ?? null,
|
|
74
|
+
code: envelope.code ?? envelope.type,
|
|
75
|
+
response,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return new ClarkApiError(
|
|
80
|
+
`Clark API request failed with status ${response.status}`,
|
|
81
|
+
{ status: response.status, type: "server_error", response }
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Build a ClarkApiError representing a network-level failure (DNS, TLS,
|
|
87
|
+
* connection refused, abort, etc.) that never produced an HTTP response.
|
|
88
|
+
*
|
|
89
|
+
* @param {unknown} cause
|
|
90
|
+
* @returns {ClarkApiError}
|
|
91
|
+
*/
|
|
92
|
+
static fromNetworkError(cause) {
|
|
93
|
+
const isAbort = cause && typeof cause === "object" && cause.name === "AbortError";
|
|
94
|
+
const message = isAbort
|
|
95
|
+
? "Clark API request was aborted or timed out"
|
|
96
|
+
: `Clark API request failed: ${cause instanceof Error ? cause.message : String(cause)}`;
|
|
97
|
+
return new ClarkApiError(message, {
|
|
98
|
+
type: isAbort ? "timeout_error" : "network_error",
|
|
99
|
+
code: isAbort ? "timeout_error" : "network_error",
|
|
100
|
+
cause,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
package/src/http.js
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { ClarkApiError } from "./errors.js";
|
|
2
|
+
|
|
3
|
+
/** Default base URL for the Clark Platform API (production). */
|
|
4
|
+
export const DEFAULT_BASE_URL = "https://www.clarkchat.com";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Non-background `/v1/responses` and `/v1/chat/completions` calls can block
|
|
8
|
+
* server-side for up to `CLARK_PLATFORM_RESPONSE_WAIT_MS` (default 120000ms)
|
|
9
|
+
* per API_CONTRACT.md. Default client timeout is set generously above that.
|
|
10
|
+
*/
|
|
11
|
+
export const DEFAULT_TIMEOUT_MS = 130_000;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Thin fetch wrapper: builds the URL, attaches Bearer auth, applies a
|
|
15
|
+
* timeout (merged with any caller-supplied AbortSignal), and normalizes
|
|
16
|
+
* both network failures and non-2xx HTTP responses into `ClarkApiError`.
|
|
17
|
+
*/
|
|
18
|
+
export class HttpTransport {
|
|
19
|
+
/**
|
|
20
|
+
* @param {Object} options
|
|
21
|
+
* @param {string} options.apiKey
|
|
22
|
+
* @param {string} [options.baseUrl]
|
|
23
|
+
* @param {number} [options.timeoutMs]
|
|
24
|
+
* @param {typeof fetch} [options.fetchFn] Override for testing or non-global fetch environments.
|
|
25
|
+
*/
|
|
26
|
+
constructor({ apiKey, baseUrl = DEFAULT_BASE_URL, timeoutMs = DEFAULT_TIMEOUT_MS, fetchFn }) {
|
|
27
|
+
if (!apiKey) {
|
|
28
|
+
throw new Error("Clark platform client requires an apiKey");
|
|
29
|
+
}
|
|
30
|
+
this.apiKey = apiKey;
|
|
31
|
+
this.baseUrl = baseUrl.replace(/\/+$/, "");
|
|
32
|
+
this.timeoutMs = timeoutMs;
|
|
33
|
+
/** @type {typeof fetch} */
|
|
34
|
+
this.fetchFn = fetchFn ?? globalThis.fetch;
|
|
35
|
+
if (!this.fetchFn) {
|
|
36
|
+
throw new Error(
|
|
37
|
+
"No global fetch is available in this environment; pass options.fetchFn explicitly"
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @param {string} path
|
|
44
|
+
* @param {Record<string, string|number|boolean|undefined|null>} [query]
|
|
45
|
+
* @returns {string}
|
|
46
|
+
*/
|
|
47
|
+
buildUrl(path, query) {
|
|
48
|
+
const url = new URL(this.baseUrl + path);
|
|
49
|
+
if (query) {
|
|
50
|
+
for (const [key, value] of Object.entries(query)) {
|
|
51
|
+
if (value === undefined || value === null) continue;
|
|
52
|
+
url.searchParams.set(key, String(value));
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return url.toString();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Combine the client's default timeout with any caller-supplied signal.
|
|
60
|
+
* @param {AbortSignal} [callerSignal]
|
|
61
|
+
* @returns {{ signal: AbortSignal, cancel: () => void }}
|
|
62
|
+
*/
|
|
63
|
+
_resolveSignal(callerSignal) {
|
|
64
|
+
const timeoutSignal = AbortSignal.timeout(this.timeoutMs);
|
|
65
|
+
const signals = callerSignal ? [callerSignal, timeoutSignal] : [timeoutSignal];
|
|
66
|
+
const combined =
|
|
67
|
+
typeof AbortSignal.any === "function" ? AbortSignal.any(signals) : timeoutSignal;
|
|
68
|
+
return { signal: combined, cancel: () => {} };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Perform a request and return the raw fetch Response, throwing
|
|
73
|
+
* ClarkApiError on network failure. Does NOT check response.ok — callers
|
|
74
|
+
* decide whether to parse JSON, stream, or raise on non-2xx.
|
|
75
|
+
*
|
|
76
|
+
* @param {string} method
|
|
77
|
+
* @param {string} path
|
|
78
|
+
* @param {Object} [options]
|
|
79
|
+
* @param {Record<string, string|number|boolean|undefined|null>} [options.query]
|
|
80
|
+
* @param {unknown} [options.body] JSON-serializable request body.
|
|
81
|
+
* @param {AbortSignal} [options.signal]
|
|
82
|
+
* @param {Record<string, string>} [options.headers]
|
|
83
|
+
* @returns {Promise<Response>}
|
|
84
|
+
*/
|
|
85
|
+
async request(method, path, options = {}) {
|
|
86
|
+
const { query, body, signal, headers } = options;
|
|
87
|
+
const url = this.buildUrl(path, query);
|
|
88
|
+
const { signal: mergedSignal } = this._resolveSignal(signal);
|
|
89
|
+
|
|
90
|
+
/** @type {Record<string, string>} */
|
|
91
|
+
const reqHeaders = {
|
|
92
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
93
|
+
...headers,
|
|
94
|
+
};
|
|
95
|
+
let payload;
|
|
96
|
+
if (body !== undefined) {
|
|
97
|
+
reqHeaders["Content-Type"] = "application/json";
|
|
98
|
+
payload = JSON.stringify(body);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
return await this.fetchFn(url, {
|
|
103
|
+
method,
|
|
104
|
+
headers: reqHeaders,
|
|
105
|
+
body: payload,
|
|
106
|
+
signal: mergedSignal,
|
|
107
|
+
});
|
|
108
|
+
} catch (err) {
|
|
109
|
+
throw ClarkApiError.fromNetworkError(err);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Perform a request and parse the JSON body, raising `ClarkApiError` for
|
|
115
|
+
* any non-2xx response using the documented error envelope.
|
|
116
|
+
*
|
|
117
|
+
* @param {string} method
|
|
118
|
+
* @param {string} path
|
|
119
|
+
* @param {Object} [options]
|
|
120
|
+
* @returns {Promise<any>}
|
|
121
|
+
*/
|
|
122
|
+
async requestJson(method, path, options = {}) {
|
|
123
|
+
const response = await this.request(method, path, options);
|
|
124
|
+
if (!response.ok) {
|
|
125
|
+
throw await ClarkApiError.fromResponse(response);
|
|
126
|
+
}
|
|
127
|
+
const text = await response.text();
|
|
128
|
+
if (!text) return null;
|
|
129
|
+
try {
|
|
130
|
+
return JSON.parse(text);
|
|
131
|
+
} catch (err) {
|
|
132
|
+
throw new ClarkApiError(`Failed to parse Clark API response as JSON: ${err.message}`, {
|
|
133
|
+
status: response.status,
|
|
134
|
+
type: "server_error",
|
|
135
|
+
cause: err,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Perform a request expected to return an SSE stream, raising
|
|
142
|
+
* `ClarkApiError` for any non-2xx response before streaming begins.
|
|
143
|
+
*
|
|
144
|
+
* @param {string} method
|
|
145
|
+
* @param {string} path
|
|
146
|
+
* @param {Object} [options]
|
|
147
|
+
* @returns {Promise<Response>}
|
|
148
|
+
*/
|
|
149
|
+
async requestStream(method, path, options = {}) {
|
|
150
|
+
const response = await this.request(method, path, {
|
|
151
|
+
...options,
|
|
152
|
+
headers: { Accept: "text/event-stream", ...options.headers },
|
|
153
|
+
});
|
|
154
|
+
if (!response.ok) {
|
|
155
|
+
throw await ClarkApiError.fromResponse(response);
|
|
156
|
+
}
|
|
157
|
+
return response;
|
|
158
|
+
}
|
|
159
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { ClarkApiError } from "../errors.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Thin wrapper for `GET /v1/artifacts/{conversation_id}/{artifact_name}`.
|
|
5
|
+
* Prefer the `download_url` returned inline on `ResponseObject.artifacts[]`
|
|
6
|
+
* over constructing this URL manually — this method exists for completeness
|
|
7
|
+
* (e.g. resuming a partial download via `Range`).
|
|
8
|
+
*/
|
|
9
|
+
export class Artifacts {
|
|
10
|
+
/** @param {import("../http.js").HttpTransport} http */
|
|
11
|
+
constructor(http) {
|
|
12
|
+
this._http = http;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Fetch the raw fetch `Response` for an artifact file so the caller can
|
|
17
|
+
* stream/read the body as needed (`.arrayBuffer()`, `.body` as a
|
|
18
|
+
* ReadableStream, etc.). Supports `Range` via `options.headers`.
|
|
19
|
+
*
|
|
20
|
+
* @param {string} conversationId
|
|
21
|
+
* @param {string} artifactName
|
|
22
|
+
* @param {Object} [options]
|
|
23
|
+
* @param {Record<string, string>} [options.headers] e.g. `{ Range: "bytes=0-1023" }`.
|
|
24
|
+
* @param {AbortSignal} [options.signal]
|
|
25
|
+
* @returns {Promise<Response>}
|
|
26
|
+
*/
|
|
27
|
+
async download(conversationId, artifactName, options = {}) {
|
|
28
|
+
const path = `/v1/artifacts/${encodeURIComponent(conversationId)}/${encodeURIComponent(
|
|
29
|
+
artifactName
|
|
30
|
+
)}`;
|
|
31
|
+
const response = await this._http.request("GET", path, {
|
|
32
|
+
headers: options.headers,
|
|
33
|
+
signal: options.signal,
|
|
34
|
+
});
|
|
35
|
+
if (!response.ok) {
|
|
36
|
+
throw await ClarkApiError.fromResponse(response);
|
|
37
|
+
}
|
|
38
|
+
return response;
|
|
39
|
+
}
|
|
40
|
+
}
|