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
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { parseSSEStream } from "../sse.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {Object} ChatMessageTextPart
|
|
5
|
+
* @property {"text"} type
|
|
6
|
+
* @property {string} text
|
|
7
|
+
*
|
|
8
|
+
* @typedef {Object} ChatMessage
|
|
9
|
+
* @property {"system"|"user"|"assistant"|"tool"|string} role
|
|
10
|
+
* @property {string|ChatMessageTextPart[]|null} [content]
|
|
11
|
+
* @property {string} [name]
|
|
12
|
+
* @property {string} [tool_call_id]
|
|
13
|
+
* @property {Array<Object>} [tool_calls]
|
|
14
|
+
*
|
|
15
|
+
* @typedef {Object} CreateChatCompletionParams
|
|
16
|
+
* @property {string} model Bare tier id or "tier:option". Use "clark-code" (or "clark-code:<option>") only via `createPassthrough`/`streamPassthrough`.
|
|
17
|
+
* @property {string} [tierModelId]
|
|
18
|
+
* @property {ChatMessage[]} messages
|
|
19
|
+
* @property {string} [conversationId]
|
|
20
|
+
* @property {"user"|"conversation"} [memoryScope]
|
|
21
|
+
* @property {string} [previousResponseId]
|
|
22
|
+
* @property {{include_usage?: boolean}} [streamOptions]
|
|
23
|
+
* @property {Record<string, unknown>} [metadata]
|
|
24
|
+
* @property {number} [temperature]
|
|
25
|
+
* @property {number} [topP]
|
|
26
|
+
* @property {number} [maxTokens]
|
|
27
|
+
* @property {string|string[]} [stop]
|
|
28
|
+
* @property {AbortSignal} [signal]
|
|
29
|
+
*
|
|
30
|
+
* @typedef {CreateChatCompletionParams} CreatePassthroughChatCompletionParams
|
|
31
|
+
* @property {Array<Object>} [tools]
|
|
32
|
+
* @property {string|Object} [toolChoice]
|
|
33
|
+
* @property {boolean} [parallelToolCalls]
|
|
34
|
+
* @property {"minimal"|"low"|"medium"|"high"|"xhigh"} [reasoningEffort]
|
|
35
|
+
*
|
|
36
|
+
* @typedef {Object} ChatCompletionChoice
|
|
37
|
+
* @property {number} index
|
|
38
|
+
* @property {{role: "assistant", content: string}} message
|
|
39
|
+
* @property {"stop"|"error"|"timeout"|string} finish_reason
|
|
40
|
+
*
|
|
41
|
+
* @typedef {Object} ChatCompletionObject
|
|
42
|
+
* @property {string} id
|
|
43
|
+
* @property {"chat.completion"} object
|
|
44
|
+
* @property {number} created
|
|
45
|
+
* @property {string} model
|
|
46
|
+
* @property {ChatCompletionChoice[]} choices
|
|
47
|
+
* @property {import("./responses.js").PublicUsage} [usage]
|
|
48
|
+
* @property {import("./responses.js").PublicResponseMetadata} [clark]
|
|
49
|
+
*
|
|
50
|
+
* @typedef {Object} ChatCompletionChunk
|
|
51
|
+
* @property {string} id
|
|
52
|
+
* @property {"chat.completion.chunk"} object
|
|
53
|
+
* @property {number} created
|
|
54
|
+
* @property {string} model
|
|
55
|
+
* @property {Array<{index: number, delta: Object, finish_reason: string|null}>} choices
|
|
56
|
+
* @property {import("./responses.js").PublicUsage|null} [usage]
|
|
57
|
+
* @property {import("./responses.js").PublicResponseMetadata|null} [clark]
|
|
58
|
+
*/
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* @param {CreateChatCompletionParams & CreatePassthroughChatCompletionParams & Record<string, unknown>} params
|
|
62
|
+
* @param {boolean} stream
|
|
63
|
+
*/
|
|
64
|
+
function buildChatBody(params, stream) {
|
|
65
|
+
const body = {
|
|
66
|
+
model: params.model,
|
|
67
|
+
messages: params.messages,
|
|
68
|
+
stream,
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const tierModelId = params.tierModelId ?? params.tier_model_id;
|
|
72
|
+
if (tierModelId !== undefined) body.tier_model_id = tierModelId;
|
|
73
|
+
|
|
74
|
+
const conversationId = params.conversationId ?? params.conversation_id;
|
|
75
|
+
if (conversationId !== undefined) body.conversation_id = conversationId;
|
|
76
|
+
|
|
77
|
+
const memoryScope = params.memoryScope ?? params.memory_scope;
|
|
78
|
+
if (memoryScope !== undefined) body.memory_scope = memoryScope;
|
|
79
|
+
|
|
80
|
+
const previousResponseId = params.previousResponseId ?? params.previous_response_id;
|
|
81
|
+
if (previousResponseId !== undefined) body.previous_response_id = previousResponseId;
|
|
82
|
+
|
|
83
|
+
const streamOptions = params.streamOptions ?? params.stream_options;
|
|
84
|
+
if (streamOptions !== undefined) body.stream_options = streamOptions;
|
|
85
|
+
|
|
86
|
+
if (params.metadata !== undefined) body.metadata = params.metadata;
|
|
87
|
+
if (params.temperature !== undefined) body.temperature = params.temperature;
|
|
88
|
+
|
|
89
|
+
const topP = params.topP ?? params.top_p;
|
|
90
|
+
if (topP !== undefined) body.top_p = topP;
|
|
91
|
+
|
|
92
|
+
const maxTokens = params.maxTokens ?? params.max_tokens;
|
|
93
|
+
if (maxTokens !== undefined) body.max_tokens = maxTokens;
|
|
94
|
+
|
|
95
|
+
if (params.stop !== undefined) body.stop = params.stop;
|
|
96
|
+
|
|
97
|
+
if (params.tools !== undefined) body.tools = params.tools;
|
|
98
|
+
|
|
99
|
+
const toolChoice = params.toolChoice ?? params.tool_choice;
|
|
100
|
+
if (toolChoice !== undefined) body.tool_choice = toolChoice;
|
|
101
|
+
|
|
102
|
+
const parallelToolCalls = params.parallelToolCalls ?? params.parallel_tool_calls;
|
|
103
|
+
if (parallelToolCalls !== undefined) body.parallel_tool_calls = parallelToolCalls;
|
|
104
|
+
|
|
105
|
+
const reasoningEffort = params.reasoningEffort ?? params.reasoning_effort;
|
|
106
|
+
if (reasoningEffort !== undefined) body.reasoning_effort = reasoningEffort;
|
|
107
|
+
|
|
108
|
+
return body;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Iterate a `data:`-only SSE stream (no `event:` names), yielding parsed
|
|
113
|
+
* JSON chunks and stopping (without yielding) on the literal `data: [DONE]`
|
|
114
|
+
* sentinel. Shared by both the agentic and passthrough streaming paths,
|
|
115
|
+
* which use the same wire-level framing but different payload shapes.
|
|
116
|
+
*
|
|
117
|
+
* @param {Response} response
|
|
118
|
+
* @returns {AsyncGenerator<any, void, unknown>}
|
|
119
|
+
*/
|
|
120
|
+
async function* iterateDataOnlySSE(response) {
|
|
121
|
+
for await (const sseEvent of parseSSEStream(response)) {
|
|
122
|
+
if (!sseEvent.data) continue;
|
|
123
|
+
if (sseEvent.data === "[DONE]") return;
|
|
124
|
+
yield JSON.parse(sseEvent.data);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export class ChatCompletions {
|
|
129
|
+
/** @param {import("../http.js").HttpTransport} http */
|
|
130
|
+
constructor(http) {
|
|
131
|
+
this._http = http;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* `POST /v1/chat/completions` for agentic tiers (`clark`, `clark_max`,
|
|
136
|
+
* `openrouter:*`) — non-streaming. Do NOT pass `tools`/`tool_choice` here;
|
|
137
|
+
* the server rejects them with `400 unsupported_parameter` for agentic
|
|
138
|
+
* tiers. Use `createPassthrough` for `model: "clark-code"` with tools.
|
|
139
|
+
*
|
|
140
|
+
* @param {CreateChatCompletionParams} params
|
|
141
|
+
* @returns {Promise<ChatCompletionObject>}
|
|
142
|
+
*/
|
|
143
|
+
async create(params) {
|
|
144
|
+
const body = buildChatBody(params, false);
|
|
145
|
+
return this._http.requestJson("POST", "/v1/chat/completions", {
|
|
146
|
+
body,
|
|
147
|
+
signal: params.signal,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* `POST /v1/chat/completions` with `stream: true` for agentic tiers.
|
|
153
|
+
* Returns an async generator over `chat.completion.chunk` objects; the
|
|
154
|
+
* stream ends (generator returns) when the server sends `data: [DONE]`.
|
|
155
|
+
* The final usage chunk only appears if `streamOptions.include_usage` (or
|
|
156
|
+
* `stream_options.include_usage`) was `true`.
|
|
157
|
+
*
|
|
158
|
+
* @param {CreateChatCompletionParams} params
|
|
159
|
+
* @returns {AsyncGenerator<ChatCompletionChunk, void, unknown>}
|
|
160
|
+
*/
|
|
161
|
+
async *stream(params) {
|
|
162
|
+
const body = buildChatBody(params, true);
|
|
163
|
+
const response = await this._http.requestStream("POST", "/v1/chat/completions", {
|
|
164
|
+
body,
|
|
165
|
+
signal: params.signal,
|
|
166
|
+
});
|
|
167
|
+
yield* iterateDataOnlySSE(response);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* `POST /v1/chat/completions` with `model: "clark-code"` (or
|
|
172
|
+
* `"clark-code:<option>"`) — the passthrough tier. `tools`/`tool_choice`
|
|
173
|
+
* and the full `messages` array (including prior `tool_calls`/`tool`
|
|
174
|
+
* messages) are forwarded verbatim to the upstream OpenAI-compatible
|
|
175
|
+
* provider. The response is the RAW upstream OpenAI-shaped object, not a
|
|
176
|
+
* Clark `ChatCompletionObject` — do not assume the `clark`/Clark usage
|
|
177
|
+
* shape here. `conversationId`/`previousResponseId`/`memoryScope` are
|
|
178
|
+
* meaningless for this tier; omit them.
|
|
179
|
+
*
|
|
180
|
+
* @param {CreatePassthroughChatCompletionParams} params
|
|
181
|
+
* @returns {Promise<Object>} Raw upstream OpenAI-compatible chat completion object.
|
|
182
|
+
*/
|
|
183
|
+
async createPassthrough(params) {
|
|
184
|
+
const body = buildChatBody(params, false);
|
|
185
|
+
return this._http.requestJson("POST", "/v1/chat/completions", {
|
|
186
|
+
body,
|
|
187
|
+
signal: params.signal,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Streaming counterpart of `createPassthrough`. Returns an async generator
|
|
193
|
+
* over the raw upstream provider's native `chat.completion.chunk`-shaped
|
|
194
|
+
* SSE payloads (including native `tool_calls` deltas) ending on
|
|
195
|
+
* `data: [DONE]`. The caller is responsible for running its own tool loop
|
|
196
|
+
* — Clark does not execute tools for this tier.
|
|
197
|
+
*
|
|
198
|
+
* @param {CreatePassthroughChatCompletionParams} params
|
|
199
|
+
* @returns {AsyncGenerator<Object, void, unknown>}
|
|
200
|
+
*/
|
|
201
|
+
async *streamPassthrough(params) {
|
|
202
|
+
const body = buildChatBody(params, true);
|
|
203
|
+
const response = await this._http.requestStream("POST", "/v1/chat/completions", {
|
|
204
|
+
body,
|
|
205
|
+
signal: params.signal,
|
|
206
|
+
});
|
|
207
|
+
yield* iterateDataOnlySSE(response);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {Object} MemoryListParams
|
|
3
|
+
* @property {string} [q] Search string.
|
|
4
|
+
* @property {string} [tags]
|
|
5
|
+
* @property {string} [conversationId] Scope to a conversation's memory instead of the caller's user-scoped memory.
|
|
6
|
+
* @property {AbortSignal} [signal]
|
|
7
|
+
*
|
|
8
|
+
* @typedef {Object} MemoryList
|
|
9
|
+
* @property {"list"} object
|
|
10
|
+
* @property {Array<Object>} data Memory record shape is defined by clark_memory_service; treated as opaque here.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export class Memories {
|
|
14
|
+
/** @param {import("../http.js").HttpTransport} http */
|
|
15
|
+
constructor(http) {
|
|
16
|
+
this._http = http;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* `GET /v1/memories` — list durable memory records for the caller.
|
|
21
|
+
*
|
|
22
|
+
* @param {MemoryListParams} [params]
|
|
23
|
+
* @returns {Promise<MemoryList>}
|
|
24
|
+
*/
|
|
25
|
+
async list(params = {}) {
|
|
26
|
+
const conversationId = params.conversationId ?? params.conversation_id;
|
|
27
|
+
return this._http.requestJson("GET", "/v1/memories", {
|
|
28
|
+
query: {
|
|
29
|
+
q: params.q,
|
|
30
|
+
tags: params.tags,
|
|
31
|
+
conversation_id: conversationId,
|
|
32
|
+
},
|
|
33
|
+
signal: params.signal,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {Object} ClarkModelPricing
|
|
3
|
+
* @property {string} currency
|
|
4
|
+
* @property {string} unit
|
|
5
|
+
* @property {number} input
|
|
6
|
+
* @property {number} output
|
|
7
|
+
* @property {number} [cache_write]
|
|
8
|
+
* @property {number} [cache_read]
|
|
9
|
+
*
|
|
10
|
+
* @typedef {Object} ClarkModelCapabilities
|
|
11
|
+
* @property {string[]} public_input_modalities
|
|
12
|
+
* @property {string[]} model_input_modalities
|
|
13
|
+
* @property {string[]} output_modalities
|
|
14
|
+
* @property {string[]} features
|
|
15
|
+
* @property {boolean} public_file_upload
|
|
16
|
+
*
|
|
17
|
+
* @typedef {Object} ClarkModelDetail
|
|
18
|
+
* @property {string} tier_id
|
|
19
|
+
* @property {string} label
|
|
20
|
+
* @property {string} description
|
|
21
|
+
* @property {number} context_window_tokens
|
|
22
|
+
* @property {number} max_output_tokens
|
|
23
|
+
* @property {ClarkModelPricing} pricing
|
|
24
|
+
* @property {ClarkModelCapabilities} capabilities
|
|
25
|
+
* @property {ClarkModelEntry[]} [model_options]
|
|
26
|
+
*
|
|
27
|
+
* @typedef {Object} ClarkModelEntry
|
|
28
|
+
* @property {string} id
|
|
29
|
+
* @property {"model"} object
|
|
30
|
+
* @property {string} owned_by
|
|
31
|
+
* @property {ClarkModelDetail} clark
|
|
32
|
+
*
|
|
33
|
+
* @typedef {Object} ClarkModelList
|
|
34
|
+
* @property {"list"} object
|
|
35
|
+
* @property {ClarkModelEntry[]} data
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
export class Models {
|
|
39
|
+
/** @param {import("../http.js").HttpTransport} http */
|
|
40
|
+
constructor(http) {
|
|
41
|
+
this._http = http;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* `GET /v1/models` — list model tiers/options with pricing + capabilities.
|
|
46
|
+
* Treat the returned tier/option ids as dynamic; never hardcode them.
|
|
47
|
+
*
|
|
48
|
+
* @param {Object} [options]
|
|
49
|
+
* @param {AbortSignal} [options.signal]
|
|
50
|
+
* @returns {Promise<ClarkModelList>}
|
|
51
|
+
*/
|
|
52
|
+
async list(options = {}) {
|
|
53
|
+
return this._http.requestJson("GET", "/v1/models", { signal: options.signal });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { parseSSEStream } from "../sse.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {"user"|"assistant"|string} ResponseInputRole
|
|
5
|
+
*
|
|
6
|
+
* @typedef {Object} ResponseInputTextPart
|
|
7
|
+
* @property {"text"|"input_text"} type
|
|
8
|
+
* @property {string} text
|
|
9
|
+
*
|
|
10
|
+
* @typedef {Object} ResponseInputItem
|
|
11
|
+
* @property {ResponseInputRole} [role]
|
|
12
|
+
* @property {string|ResponseInputTextPart[]} content
|
|
13
|
+
*
|
|
14
|
+
* @typedef {Object} CreateResponseParams
|
|
15
|
+
* @property {string} model Bare tier id (e.g. "clark") or "tier:option" (e.g. "openrouter:qwen35_flash").
|
|
16
|
+
* @property {string} [tierModelId] Explicit tier/model id; wins over `model` if both are present.
|
|
17
|
+
* @property {string|ResponseInputItem[]} input
|
|
18
|
+
* @property {string} [conversationId]
|
|
19
|
+
* @property {"user"|"conversation"} [memoryScope]
|
|
20
|
+
* @property {string} [previousResponseId]
|
|
21
|
+
* @property {boolean} [background]
|
|
22
|
+
* @property {Record<string, unknown>} [metadata]
|
|
23
|
+
* @property {AbortSignal} [signal]
|
|
24
|
+
*
|
|
25
|
+
* @typedef {Object} PublicUsageCost
|
|
26
|
+
* @property {string|null} amount
|
|
27
|
+
* @property {string} currency
|
|
28
|
+
* @property {"estimated"|"unavailable"|string} type
|
|
29
|
+
*
|
|
30
|
+
* @typedef {Object} PublicUsage
|
|
31
|
+
* @property {number} input_tokens
|
|
32
|
+
* @property {{cached_tokens: number}} input_tokens_details
|
|
33
|
+
* @property {number} output_tokens
|
|
34
|
+
* @property {{reasoning_tokens: number}} output_tokens_details
|
|
35
|
+
* @property {number} total_tokens
|
|
36
|
+
* @property {number} [artifact_bytes]
|
|
37
|
+
* @property {PublicUsageCost} cost
|
|
38
|
+
*
|
|
39
|
+
* @typedef {Object} PublicArtifact
|
|
40
|
+
* @property {string} id
|
|
41
|
+
* @property {string} name
|
|
42
|
+
* @property {string} kind
|
|
43
|
+
* @property {string} role
|
|
44
|
+
* @property {string} mime_type
|
|
45
|
+
* @property {number} size_bytes
|
|
46
|
+
* @property {string} [summary]
|
|
47
|
+
* @property {string} download_url
|
|
48
|
+
*
|
|
49
|
+
* @typedef {Object} PublicResponseMetadata
|
|
50
|
+
* @property {string} response_id
|
|
51
|
+
* @property {string} conversation_id
|
|
52
|
+
* @property {string} run_id
|
|
53
|
+
* @property {"in_progress"|"completed"|"failed"} status
|
|
54
|
+
* @property {PublicArtifact[]} artifacts
|
|
55
|
+
*
|
|
56
|
+
* @typedef {Object} ResponseObject
|
|
57
|
+
* @property {string} id
|
|
58
|
+
* @property {"response"} object
|
|
59
|
+
* @property {"in_progress"|"completed"|"failed"} status
|
|
60
|
+
* @property {number} created_at
|
|
61
|
+
* @property {string} model
|
|
62
|
+
* @property {string|null} previous_response_id
|
|
63
|
+
* @property {boolean} background
|
|
64
|
+
* @property {Array<Object>} output
|
|
65
|
+
* @property {PublicArtifact[]} artifacts
|
|
66
|
+
* @property {PublicUsage} [usage]
|
|
67
|
+
* @property {Record<string, unknown>} [metadata]
|
|
68
|
+
* @property {PublicResponseMetadata} clark
|
|
69
|
+
* @property {{type: string, message: string}|null} error
|
|
70
|
+
*/
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Build the JSON body for POST /v1/responses from either camelCase or
|
|
74
|
+
* wire-native snake_case fields (both accepted for ergonomics).
|
|
75
|
+
*
|
|
76
|
+
* @param {CreateResponseParams & Record<string, unknown>} params
|
|
77
|
+
* @param {boolean} stream
|
|
78
|
+
*/
|
|
79
|
+
function buildResponsesBody(params, stream) {
|
|
80
|
+
const body = {
|
|
81
|
+
model: params.model,
|
|
82
|
+
input: params.input,
|
|
83
|
+
stream,
|
|
84
|
+
};
|
|
85
|
+
const tierModelId = params.tierModelId ?? params.tier_model_id;
|
|
86
|
+
if (tierModelId !== undefined) body.tier_model_id = tierModelId;
|
|
87
|
+
|
|
88
|
+
const conversationId = params.conversationId ?? params.conversation_id;
|
|
89
|
+
if (conversationId !== undefined) body.conversation_id = conversationId;
|
|
90
|
+
|
|
91
|
+
const memoryScope = params.memoryScope ?? params.memory_scope;
|
|
92
|
+
if (memoryScope !== undefined) body.memory_scope = memoryScope;
|
|
93
|
+
|
|
94
|
+
const previousResponseId = params.previousResponseId ?? params.previous_response_id;
|
|
95
|
+
if (previousResponseId !== undefined) body.previous_response_id = previousResponseId;
|
|
96
|
+
|
|
97
|
+
if (params.background !== undefined) body.background = params.background;
|
|
98
|
+
if (params.metadata !== undefined) body.metadata = params.metadata;
|
|
99
|
+
|
|
100
|
+
return body;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export class Responses {
|
|
104
|
+
/** @param {import("../http.js").HttpTransport} http */
|
|
105
|
+
constructor(http) {
|
|
106
|
+
this._http = http;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* `POST /v1/responses` (non-streaming, or `background: true` to return
|
|
111
|
+
* immediately with an `in_progress` object for polling).
|
|
112
|
+
*
|
|
113
|
+
* @param {CreateResponseParams} params
|
|
114
|
+
* @returns {Promise<ResponseObject>}
|
|
115
|
+
*/
|
|
116
|
+
async create(params) {
|
|
117
|
+
const body = buildResponsesBody(params, false);
|
|
118
|
+
return this._http.requestJson("POST", "/v1/responses", { body, signal: params.signal });
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* `POST /v1/responses` with `stream: true`. Returns an async generator over
|
|
123
|
+
* the named SSE events (`response.created`, `response.in_progress`,
|
|
124
|
+
* `response.output_text.delta`, `response.output_text.done`,
|
|
125
|
+
* `response.artifact.completed`, `response.usage.updated`,
|
|
126
|
+
* `response.completed` / `response.failed`).
|
|
127
|
+
*
|
|
128
|
+
* The full answer text arrives as a single `response.output_text.delta`
|
|
129
|
+
* event, not token-by-token — do not assume many small chunks.
|
|
130
|
+
*
|
|
131
|
+
* @param {CreateResponseParams} params
|
|
132
|
+
* @returns {AsyncGenerator<{type: string, sequence_number: number, [key: string]: unknown}, void, unknown>}
|
|
133
|
+
*/
|
|
134
|
+
async *stream(params) {
|
|
135
|
+
const body = buildResponsesBody(params, true);
|
|
136
|
+
const response = await this._http.requestStream("POST", "/v1/responses", {
|
|
137
|
+
body,
|
|
138
|
+
signal: params.signal,
|
|
139
|
+
});
|
|
140
|
+
for await (const sseEvent of parseSSEStream(response)) {
|
|
141
|
+
if (!sseEvent.data) continue;
|
|
142
|
+
yield JSON.parse(sseEvent.data);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* `GET /v1/responses/{response_id}` — poll a response by its stable id.
|
|
148
|
+
*
|
|
149
|
+
* @param {string} responseId
|
|
150
|
+
* @param {Object} [options]
|
|
151
|
+
* @param {AbortSignal} [options.signal]
|
|
152
|
+
* @returns {Promise<ResponseObject>}
|
|
153
|
+
*/
|
|
154
|
+
async get(responseId, options = {}) {
|
|
155
|
+
return this._http.requestJson("GET", `/v1/responses/${encodeURIComponent(responseId)}`, {
|
|
156
|
+
signal: options.signal,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* `GET /v1/responses/{response_id}/events` — poll public progress events.
|
|
162
|
+
*
|
|
163
|
+
* @param {string} responseId
|
|
164
|
+
* @param {Object} [options]
|
|
165
|
+
* @param {number} [options.afterSeq] Default 0.
|
|
166
|
+
* @param {number} [options.limit] Default 200, max 500.
|
|
167
|
+
* @param {string[]|string} [options.types] Comma-separated allowlist filter.
|
|
168
|
+
* @param {AbortSignal} [options.signal]
|
|
169
|
+
* @returns {Promise<{object: "list", data: Array<Object>, next_after_seq: number}>}
|
|
170
|
+
*/
|
|
171
|
+
async listEvents(responseId, options = {}) {
|
|
172
|
+
const types = Array.isArray(options.types) ? options.types.join(",") : options.types;
|
|
173
|
+
return this._http.requestJson(
|
|
174
|
+
"GET",
|
|
175
|
+
`/v1/responses/${encodeURIComponent(responseId)}/events`,
|
|
176
|
+
{
|
|
177
|
+
query: {
|
|
178
|
+
after_seq: options.afterSeq,
|
|
179
|
+
limit: options.limit,
|
|
180
|
+
types,
|
|
181
|
+
},
|
|
182
|
+
signal: options.signal,
|
|
183
|
+
}
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
}
|
package/src/sse.js
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal Server-Sent Events (SSE) line-protocol parser, built directly on
|
|
3
|
+
* the Web Streams API (`Response.body` as a `ReadableStream<Uint8Array>`),
|
|
4
|
+
* with no external dependency. Works identically in Node 18+ (native
|
|
5
|
+
* `fetch`/`ReadableStream`) and modern browsers.
|
|
6
|
+
*
|
|
7
|
+
* @typedef {Object} SSEEvent
|
|
8
|
+
* @property {string|null} event SSE `event:` field, or null if the stream
|
|
9
|
+
* only sends bare `data:` lines (as `/v1/chat/completions` streaming does).
|
|
10
|
+
* @property {string} data The (possibly multi-line, newline-joined) `data:` payload.
|
|
11
|
+
* @property {string|null} id SSE `id:` field, if present.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Parse a fetch Response body as an SSE stream, yielding one `SSEEvent` per
|
|
16
|
+
* blank-line-delimited block.
|
|
17
|
+
*
|
|
18
|
+
* @param {Response} response A fetch Response whose `.body` is a ReadableStream.
|
|
19
|
+
* @returns {AsyncGenerator<SSEEvent, void, unknown>}
|
|
20
|
+
*/
|
|
21
|
+
export async function* parseSSEStream(response) {
|
|
22
|
+
if (!response.body) {
|
|
23
|
+
throw new Error("Response has no readable body to stream from");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const decoder = new TextDecoder("utf-8");
|
|
27
|
+
const reader = response.body.getReader();
|
|
28
|
+
let buffer = "";
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
while (true) {
|
|
32
|
+
const { done, value } = await reader.read();
|
|
33
|
+
if (done) break;
|
|
34
|
+
buffer += decoder.decode(value, { stream: true });
|
|
35
|
+
|
|
36
|
+
// SSE blocks are separated by a blank line. Support both \n\n and \r\n\r\n.
|
|
37
|
+
let boundary;
|
|
38
|
+
while ((boundary = findBlockBoundary(buffer)) !== -1) {
|
|
39
|
+
const rawBlock = buffer.slice(0, boundary.start);
|
|
40
|
+
buffer = buffer.slice(boundary.end);
|
|
41
|
+
const parsed = parseBlock(rawBlock);
|
|
42
|
+
if (parsed) yield parsed;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Flush any trailing block without a final blank-line terminator.
|
|
47
|
+
buffer += decoder.decode();
|
|
48
|
+
const trimmed = buffer.trim();
|
|
49
|
+
if (trimmed.length > 0) {
|
|
50
|
+
const parsed = parseBlock(buffer);
|
|
51
|
+
if (parsed) yield parsed;
|
|
52
|
+
}
|
|
53
|
+
} finally {
|
|
54
|
+
reader.releaseLock();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* @param {string} buffer
|
|
60
|
+
* @returns {{start: number, end: number}|-1}
|
|
61
|
+
*/
|
|
62
|
+
function findBlockBoundary(buffer) {
|
|
63
|
+
const idxLfLf = buffer.indexOf("\n\n");
|
|
64
|
+
const idxCrLfCrLf = buffer.indexOf("\r\n\r\n");
|
|
65
|
+
if (idxCrLfCrLf !== -1 && (idxLfLf === -1 || idxCrLfCrLf < idxLfLf)) {
|
|
66
|
+
return { start: idxCrLfCrLf, end: idxCrLfCrLf + 4 };
|
|
67
|
+
}
|
|
68
|
+
if (idxLfLf !== -1) {
|
|
69
|
+
return { start: idxLfLf, end: idxLfLf + 2 };
|
|
70
|
+
}
|
|
71
|
+
return -1;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* @param {string} block
|
|
76
|
+
* @returns {SSEEvent|null}
|
|
77
|
+
*/
|
|
78
|
+
function parseBlock(block) {
|
|
79
|
+
const lines = block.split(/\r\n|\n/);
|
|
80
|
+
/** @type {string|null} */
|
|
81
|
+
let event = null;
|
|
82
|
+
/** @type {string|null} */
|
|
83
|
+
let id = null;
|
|
84
|
+
const dataLines = [];
|
|
85
|
+
|
|
86
|
+
for (const line of lines) {
|
|
87
|
+
if (line === "" || line.startsWith(":")) continue;
|
|
88
|
+
const colonIdx = line.indexOf(":");
|
|
89
|
+
const field = colonIdx === -1 ? line : line.slice(0, colonIdx);
|
|
90
|
+
let value = colonIdx === -1 ? "" : line.slice(colonIdx + 1);
|
|
91
|
+
if (value.startsWith(" ")) value = value.slice(1);
|
|
92
|
+
|
|
93
|
+
if (field === "event") event = value;
|
|
94
|
+
else if (field === "data") dataLines.push(value);
|
|
95
|
+
else if (field === "id") id = value;
|
|
96
|
+
// "retry" and unknown fields are ignored — not used by the Clark API.
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (dataLines.length === 0 && event === null) return null;
|
|
100
|
+
return { event, data: dataLines.join("\n"), id };
|
|
101
|
+
}
|