dsh-lcx-codex 0.4.2 → 0.4.3-pre.12

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.
Files changed (49) hide show
  1. package/README.md +89 -205
  2. package/cordis.patch.yml +3 -20
  3. package/lib/client.js +309 -146
  4. package/lib/compact-v2.js +218 -199
  5. package/lib/dsh-compat.js +232 -100
  6. package/lib/dsh-responses.js +452 -277
  7. package/lib/grok-native-search.js +254 -0
  8. package/lib/index.js +1013 -757
  9. package/lib/json-store.js +50 -31
  10. package/lib/native-checkpoint.js +520 -194
  11. package/lib/responses-request.js +109 -121
  12. package/lib/responses-stream.js +1249 -447
  13. package/lib/route.js +411 -369
  14. package/lib/service-mutex.js +73 -64
  15. package/lib/token-budget.js +176 -108
  16. package/lib/transport.js +280 -68
  17. package/lib/types/client/index.d.ts +6 -0
  18. package/lib/types/compact-v2.d.ts +104 -0
  19. package/lib/types/dsh-compat.d.ts +78 -0
  20. package/lib/types/dsh-responses.d.ts +82 -0
  21. package/lib/types/grok-native-search.d.ts +34 -0
  22. package/lib/types/index.d.ts +83 -0
  23. package/lib/types/json-store.d.ts +10 -0
  24. package/lib/types/native-checkpoint.d.ts +213 -0
  25. package/lib/types/responses-request.d.ts +59 -0
  26. package/lib/types/responses-stream.d.ts +64 -0
  27. package/lib/types/route.d.ts +153 -0
  28. package/lib/types/service-mutex.d.ts +14 -0
  29. package/lib/types/token-budget.d.ts +50 -0
  30. package/lib/types/transport.d.ts +21 -0
  31. package/lib/types/web-run-output.d.ts +29 -0
  32. package/lib/types/web-search-alpha.d.ts +286 -0
  33. package/lib/types/web-search-capability.d.ts +26 -0
  34. package/lib/types/web-search-hosted.d.ts +246 -0
  35. package/lib/types/web-search-ref-store.d.ts +22 -0
  36. package/lib/web-run-output.js +167 -18
  37. package/lib/web-search-alpha.js +865 -163
  38. package/lib/web-search-capability.js +55 -65
  39. package/lib/web-search-hosted.js +210 -32
  40. package/lib/web-search-ref-store.js +63 -59
  41. package/package.json +89 -29
  42. package/ARCHITECTURE.md +0 -117
  43. package/CHANGELOG.md +0 -224
  44. package/README_EN.md +0 -277
  45. package/assets/dsh-lcx-codex-banner.jpg +0 -0
  46. package/lib/legacy-v3.js +0 -20
  47. package/lib/responses-replay.js +0 -68
  48. package/scripts/probe-alpha.mjs +0 -43
  49. package/scripts/validate-dsh-schema.mjs +0 -31
@@ -1,485 +1,1236 @@
1
1
  // @ts-check
2
-
3
- import { processResponsesStream } from '@earendil-works/pi-ai/api/openai-responses-shared'
4
- import { fetchSseWithRetry } from './transport.js'
5
-
6
- /** @typedef {Record<string, unknown>} UnknownRecord */
7
- /** @typedef {import('openai/resources/responses/responses.js').ResponseStreamEvent} ResponseStreamEvent */
8
- /** @typedef {{ message: string, code: string, status?: number, requestId?: string, providerRetryAfterMs?: number }} ManagedFailure */
9
- /** @typedef {Error & { code?: string, status?: number, requestId?: string, providerRetryAfterMs?: number, cause?: unknown }} LcxError */
10
-
2
+ import { createAssistantMessageEventStream, } from "@earendil-works/pi-ai";
3
+ import { processResponsesStream } from "@earendil-works/pi-ai/api/openai-responses-shared";
4
+ import { ToolCallId } from "@deepseek-ai/dsh-llm";
5
+ import { fetchSseWithRetry } from "./transport.js";
6
+ import { createGrokNativeReplayEnvelope, } from "./grok-native-search.js";
11
7
  function emptyUsage() {
12
- return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } }
8
+ return {
9
+ input: 0,
10
+ output: 0,
11
+ cacheRead: 0,
12
+ cacheWrite: 0,
13
+ totalTokens: 0,
14
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
15
+ };
13
16
  }
14
-
15
17
  /** @param {unknown} value @returns {value is UnknownRecord} */
16
- function isObject(value) { return value !== null && typeof value === 'object' && !Array.isArray(value) }
17
-
18
- class PiEventQueue {
19
- constructor() {
20
- /** @type {unknown[]} */
21
- this.queue = []
22
- /** @type {Array<(value: IteratorResult<unknown>) => void>} */
23
- this.waiting = []
24
- this.done = false
25
- }
26
- /** @param {unknown} event */
27
- push(event) {
28
- if (this.done) return
29
- const waiter = this.waiting.shift()
30
- if (waiter) waiter({ value: event, done: false })
31
- else this.queue.push(event)
32
- }
33
- end() {
34
- this.done = true
35
- while (this.waiting.length > 0) this.waiting.shift()?.({ value: undefined, done: true })
36
- }
37
- async *[Symbol.asyncIterator]() {
38
- while (true) {
39
- if (this.queue.length > 0) yield this.queue.shift()
40
- else if (this.done) return
41
- else {
42
- const result = await new Promise((resolve) => this.waiting.push(resolve))
43
- if (result.done) return
44
- yield result.value
45
- }
46
- }
47
- }
18
+ function isObject(value) {
19
+ return value !== null && typeof value === "object" && !Array.isArray(value);
48
20
  }
49
-
50
21
  /** @param {unknown} raw */
51
- function safeStatus(raw) { return Number.isInteger(raw) && Number(raw) >= 100 && Number(raw) <= 599 ? Number(raw) : undefined }
52
-
22
+ function safeStatus(raw) {
23
+ return Number.isInteger(raw) && Number(raw) >= 100 && Number(raw) <= 599
24
+ ? Number(raw)
25
+ : undefined;
26
+ }
53
27
  /** @param {unknown} raw */
54
28
  function safeRequestId(raw) {
55
- if (typeof raw !== 'string' || raw.length === 0 || raw.length > 160) return undefined
56
- return /^[A-Za-z0-9._:\/-]+$/u.test(raw) ? raw : undefined
29
+ if (typeof raw !== "string" || raw.length === 0 || raw.length > 160)
30
+ return undefined;
31
+ return /^[A-Za-z0-9._:\/-]+$/u.test(raw) ? raw : undefined;
57
32
  }
58
-
59
33
  /** @param {unknown} error */
60
34
  function errorFacts(error) {
61
- const seen = new Set()
62
- const codes = []
63
- const texts = []
64
- let status
65
- let requestId
66
- let providerRetryAfterMs
67
- let current = error
68
- for (let depth = 0; depth < 8 && (current instanceof Error || isObject(current)) && !seen.has(current); depth += 1) {
69
- seen.add(current)
70
- const value = /** @type {LcxError} */ (current)
71
- if (status === undefined) status = safeStatus(value.status)
72
- if (requestId === undefined) requestId = safeRequestId(value.requestId)
73
- if (providerRetryAfterMs === undefined && Number.isFinite(value.providerRetryAfterMs) && Number(value.providerRetryAfterMs) > 0) providerRetryAfterMs = Number(value.providerRetryAfterMs)
74
- if (typeof value.code === 'string' && value.code) codes.push(value.code)
75
- if (typeof value.message === 'string' && value.message) texts.push(value.message)
76
- current = value.cause
77
- }
78
- return { status, requestId, providerRetryAfterMs, codes, text: texts.join(' | ') }
35
+ const seen = new Set();
36
+ const codes = [];
37
+ const texts = [];
38
+ let status;
39
+ let requestId;
40
+ let providerRetryAfterMs;
41
+ let current = error;
42
+ for (let depth = 0; depth < 8 &&
43
+ (current instanceof Error || isObject(current)) &&
44
+ !seen.has(current); depth += 1) {
45
+ seen.add(current);
46
+ const value = current;
47
+ if (status === undefined)
48
+ status = safeStatus(value.status);
49
+ if (requestId === undefined)
50
+ requestId = safeRequestId(value.requestId);
51
+ if (providerRetryAfterMs === undefined &&
52
+ Number.isFinite(value.providerRetryAfterMs) &&
53
+ Number(value.providerRetryAfterMs) > 0)
54
+ providerRetryAfterMs = Number(value.providerRetryAfterMs);
55
+ if (typeof value.code === "string" && value.code)
56
+ codes.push(value.code);
57
+ if (typeof value.message === "string" && value.message)
58
+ texts.push(value.message);
59
+ current = value.cause;
60
+ }
61
+ return {
62
+ status,
63
+ requestId,
64
+ providerRetryAfterMs,
65
+ codes,
66
+ text: texts.join(" | "),
67
+ };
79
68
  }
80
-
81
69
  /**
82
70
  * Provider bodies/messages are deliberately not surfaced. Only a stable class and safe facts leave the wire boundary.
83
71
  * @param {unknown} error
84
72
  * @param {AbortSignal} [signal]
85
73
  */
86
74
  export function managedFailure(error, signal) {
87
- const facts = errorFacts(error)
88
- const sourceCodes = new Set(facts.codes)
89
- const text = facts.text
90
- let code = 'RESPONSES_ERROR'
91
- if (signal?.aborted || sourceCodes.has('LCX_ABORTED') || /\babort(?:ed)?\b/iu.test(text)) code = 'ABORTED'
92
- else if (sourceCodes.has('LCX_RESPONSES_UNSUPPORTED_OPTION')) code = 'UNSUPPORTED_OPTION'
93
- else if (sourceCodes.has('LCX_RESPONSES_ROUTE_UNAVAILABLE') || sourceCodes.has('LCX_RESPONSES_MODEL_UNAVAILABLE')) code = 'NO_ADAPTER'
94
- else if (facts.status === 401 || facts.status === 403 || sourceCodes.has('AUTH') || sourceCodes.has('LCX_CREDENTIAL_UNAVAILABLE')) code = 'AUTH'
95
- else if (facts.status === 408) code = 'TIMEOUT'
96
- else if (facts.status === 409 || facts.status === 425) code = 'TRANSPORT'
97
- else if (facts.status === 429 || /rate.?limit|quota exceeded/iu.test(text)) code = 'RATE_LIMIT'
98
- else if (facts.status !== undefined && facts.status >= 500) code = 'SERVER'
99
- else if (/context (?:window|length)|maximum context|too many tokens/iu.test(text) || [...sourceCodes].some((value) => /context.*(?:window|length|exceed)/iu.test(value))) code = 'CONTEXT_WINDOW_EXCEEDED'
100
- else if (facts.status === 400 || facts.status === 404 || facts.status === 413 || facts.status === 422 || sourceCodes.has('LCX_RESPONSES_INVALID_INPUT') || /invalid.?request|payload too large|length limit exceeded/iu.test(text)) code = 'INVALID_REQUEST'
101
- else if (sourceCodes.has('LCX_RESPONSE_TOO_LARGE')) code = 'INVALID_REQUEST'
102
- else if (sourceCodes.has('LCX_INVALID_SSE') || /stream ended before|without a terminal|malformed.*sse/iu.test(text)) code = 'TRANSPORT'
103
- else if (/time(?:d)?\s*out|timeout/iu.test(text) || sourceCodes.has('TimeoutError')) code = 'TIMEOUT'
104
- else if (error instanceof TypeError || /\bnetwork|connection|socket|fetch|ECONN|EAI_AGAIN|terminated|premature close\b/iu.test(text)) code = 'TRANSPORT'
105
- const messages = /** @type {Record<string, string>} */ ({
106
- ABORTED: 'Responses request was aborted',
107
- AUTH: 'Responses request was rejected by authentication',
108
- UNSUPPORTED_OPTION: 'LCX Responses does not support this request option',
109
- NO_ADAPTER: 'LCX could not resolve the selected Responses route',
110
- RATE_LIMIT: 'Responses provider rate limit was reached',
111
- SERVER: 'Responses provider returned a server failure',
112
- INVALID_REQUEST: 'Responses provider rejected the request',
113
- CONTEXT_WINDOW_EXCEEDED: 'Responses request exceeded the model context window',
114
- TIMEOUT: 'Responses request timed out',
115
- TRANSPORT: 'Responses transport failed',
116
- RESPONSES_ERROR: 'Responses request failed',
117
- })
118
- return {
119
- message: messages[code] ?? messages.RESPONSES_ERROR,
120
- code,
121
- ...(facts.status === undefined ? {} : { status: facts.status }),
122
- ...(facts.requestId === undefined ? {} : { requestId: facts.requestId }),
123
- ...(facts.providerRetryAfterMs === undefined ? {} : { providerRetryAfterMs: facts.providerRetryAfterMs }),
124
- }
75
+ const facts = errorFacts(error);
76
+ const sourceCodes = new Set(facts.codes);
77
+ const text = facts.text;
78
+ const signalReason = signal?.reason;
79
+ const signalTimedOut = signal?.aborted &&
80
+ (signalReason?.name === "TimeoutError" ||
81
+ signalReason?.code === "TimeoutError" ||
82
+ /time(?:d)?\s*out|timeout/iu.test(String(signalReason?.message ?? "")));
83
+ let code = "RESPONSES_ERROR";
84
+ if (signalTimedOut ||
85
+ facts.status === 408 ||
86
+ sourceCodes.has("TimeoutError") ||
87
+ /time(?:d)?\s*out|timeout/iu.test(text))
88
+ code = "TIMEOUT";
89
+ else if (signal?.aborted ||
90
+ sourceCodes.has("LCX_ABORTED") ||
91
+ /\babort(?:ed)?\b/iu.test(text))
92
+ code = "ABORTED";
93
+ else if (sourceCodes.has("LCX_RESPONSES_UNSUPPORTED_OPTION"))
94
+ code = "UNSUPPORTED_OPTION";
95
+ else if (sourceCodes.has("LCX_RESPONSES_ROUTE_UNAVAILABLE") ||
96
+ sourceCodes.has("LCX_RESPONSES_MODEL_UNAVAILABLE"))
97
+ code = "NO_ADAPTER";
98
+ else if (facts.status === 401 ||
99
+ facts.status === 403 ||
100
+ sourceCodes.has("AUTH") ||
101
+ sourceCodes.has("LCX_CREDENTIAL_UNAVAILABLE"))
102
+ code = "AUTH";
103
+ else if (facts.status === 409 || facts.status === 425)
104
+ code = "TRANSPORT";
105
+ else if (facts.status === 429 || /rate.?limit|quota exceeded/iu.test(text))
106
+ code = "RATE_LIMIT";
107
+ else if (facts.status !== undefined && facts.status >= 500)
108
+ code = "SERVER";
109
+ else if (/context (?:window|length)|maximum context|too many tokens/iu.test(text) ||
110
+ [...sourceCodes].some((value) => /context.*(?:window|length|exceed)/iu.test(value)))
111
+ code = "CONTEXT_WINDOW_EXCEEDED";
112
+ else if (facts.status === 400 ||
113
+ facts.status === 404 ||
114
+ facts.status === 413 ||
115
+ facts.status === 422 ||
116
+ sourceCodes.has("LCX_RESPONSES_INVALID_INPUT") ||
117
+ sourceCodes.has("LCX_CHECKPOINT_UNSUPPORTED") ||
118
+ /invalid.?request|payload too large|length limit exceeded/iu.test(text))
119
+ code = "INVALID_REQUEST";
120
+ else if (sourceCodes.has("LCX_RESPONSE_TOO_LARGE"))
121
+ code = "INVALID_REQUEST";
122
+ else if (sourceCodes.has("LCX_INVALID_SSE") ||
123
+ /stream ended before|without a terminal|malformed.*sse/iu.test(text))
124
+ code = "TRANSPORT";
125
+ else if (error instanceof TypeError ||
126
+ /\bnetwork|connection|socket|fetch|ECONN|EAI_AGAIN|terminated|premature close\b/iu.test(text))
127
+ code = "TRANSPORT";
128
+ const messages = /** @type {Record<string, string>} */ {
129
+ ABORTED: "Responses request was aborted",
130
+ AUTH: "Responses request was rejected by authentication",
131
+ UNSUPPORTED_OPTION: "LCX Responses does not support this request option",
132
+ NO_ADAPTER: "LCX could not resolve the selected Responses route",
133
+ RATE_LIMIT: "Responses provider rate limit was reached",
134
+ SERVER: "Responses provider returned a server failure",
135
+ INVALID_REQUEST: sourceCodes.has("LCX_CHECKPOINT_UNSUPPORTED")
136
+ ? "Unsupported or invalid LCX checkpoint; start a new session"
137
+ : "Responses provider rejected the request",
138
+ CONTEXT_WINDOW_EXCEEDED: "Responses request exceeded the model context window",
139
+ TIMEOUT: "Responses request timed out",
140
+ TRANSPORT: "Responses transport failed",
141
+ RESPONSES_ERROR: "Responses request failed",
142
+ };
143
+ return {
144
+ message: messages[code] ?? messages.RESPONSES_ERROR,
145
+ code,
146
+ ...(facts.status === undefined ? {} : { status: facts.status }),
147
+ ...(facts.requestId === undefined ? {} : { requestId: facts.requestId }),
148
+ ...(facts.providerRetryAfterMs === undefined
149
+ ? {}
150
+ : { providerRetryAfterMs: facts.providerRetryAfterMs }),
151
+ };
125
152
  }
126
-
127
153
  /** @param {unknown} error @param {AbortSignal} [signal] */
128
154
  export function managedFailureChunk(error, signal) {
129
- const failure = managedFailure(error, signal)
130
- return { type: 'finish', reason: failure.code === 'ABORTED' ? { kind: 'aborted', failure } : { kind: 'error', failure } }
155
+ const managed = managedFailure(error, signal);
156
+ const failure = {
157
+ message: managed.message,
158
+ code: managed.code,
159
+ ...(managed.status === undefined ? {} : { status: managed.status }),
160
+ ...(managed.providerRetryAfterMs === undefined
161
+ ? {}
162
+ : { providerRetryAfterMs: managed.providerRetryAfterMs }),
163
+ };
164
+ return {
165
+ type: "finish",
166
+ reason: managed.code === "ABORTED"
167
+ ? { kind: "aborted", failure }
168
+ : { kind: "error", failure },
169
+ };
170
+ }
171
+ async function readWithSignal(reader, signal) {
172
+ if (!signal)
173
+ return reader.read();
174
+ if (signal.aborted)
175
+ throw signal.reason;
176
+ return new Promise((resolve, reject) => {
177
+ const onAbort = () => reject(signal.reason);
178
+ signal.addEventListener("abort", onAbort, { once: true });
179
+ reader.read().then(resolve, reject).finally(() => {
180
+ signal.removeEventListener("abort", onAbort);
181
+ });
182
+ });
183
+ }
184
+ function requestDeadline(parent, timeoutMs) {
185
+ const controller = new AbortController();
186
+ let timer;
187
+ const onParentAbort = () => controller.abort(parent?.reason);
188
+ if (parent?.aborted)
189
+ onParentAbort();
190
+ else
191
+ parent?.addEventListener("abort", onParentAbort, { once: true });
192
+ if (!controller.signal.aborted &&
193
+ typeof timeoutMs === "number" &&
194
+ Number.isFinite(timeoutMs) &&
195
+ timeoutMs > 0)
196
+ timer = setTimeout(() => controller.abort(Object.assign(new Error(`Responses request timeout after ${timeoutMs}ms`), { code: "LCX_REQUEST_TIMEOUT" })), timeoutMs);
197
+ return {
198
+ signal: controller.signal,
199
+ dispose() {
200
+ if (timer !== undefined)
201
+ clearTimeout(timer);
202
+ parent?.removeEventListener("abort", onParentAbort);
203
+ if (!controller.signal.aborted)
204
+ controller.abort(Object.assign(new Error("Responses request consumer stopped"), {
205
+ code: "LCX_ABORTED",
206
+ }));
207
+ },
208
+ };
209
+ }
210
+ function streamIdleWatchdog(parent, idleTimeoutMs) {
211
+ const controller = new AbortController();
212
+ let timer;
213
+ const onParentAbort = () => controller.abort(parent?.reason);
214
+ if (parent?.aborted)
215
+ onParentAbort();
216
+ else
217
+ parent?.addEventListener("abort", onParentAbort, { once: true });
218
+ const disarm = () => {
219
+ if (timer !== undefined)
220
+ clearTimeout(timer);
221
+ timer = undefined;
222
+ };
223
+ const arm = () => {
224
+ if (controller.signal.aborted)
225
+ return;
226
+ disarm();
227
+ if (typeof idleTimeoutMs === "number" && Number.isFinite(idleTimeoutMs) && idleTimeoutMs > 0)
228
+ timer = setTimeout(() => controller.abort(Object.assign(new Error(`Responses stream idle timeout after ${idleTimeoutMs}ms`), { code: "LCX_STREAM_IDLE_TIMEOUT" })), idleTimeoutMs);
229
+ };
230
+ return {
231
+ signal: controller.signal,
232
+ arm,
233
+ disarm,
234
+ dispose() {
235
+ disarm();
236
+ parent?.removeEventListener("abort", onParentAbort);
237
+ if (!controller.signal.aborted)
238
+ controller.abort(Object.assign(new Error("Responses stream consumer stopped"), {
239
+ code: "LCX_ABORTED",
240
+ }));
241
+ },
242
+ };
131
243
  }
132
-
133
244
  /**
134
245
  * Minimal JSON SSE reader. LCX owns the exact HTTP wire; Pi owns event semantics after this boundary.
246
+ * Stream liveness is measured after Pi emits meaningful DSH progress, not here.
135
247
  * @param {Response} response
136
- * @param {{ signal?: AbortSignal, maxResponseBytes?: number }} [options]
248
+ * @param {StreamOptions} [options]
137
249
  */
138
250
  async function* responseEvents(response, options = {}) {
139
- if (!response?.body) throw Object.assign(new Error('Responses stream returned no body'), { code: 'LCX_INVALID_SSE', status: response?.status })
140
- const reader = response.body.getReader()
141
- const decoder = new TextDecoder()
142
- const maxBytes = options.maxResponseBytes ?? 8 * 1024 * 1024
143
- let bytes = 0
144
- let pending = ''
145
- /** @type {string[]} */
146
- let dataLines = []
147
- const decode = () => {
148
- if (dataLines.length === 0) return undefined
149
- const data = dataLines.join('\n')
150
- dataLines = []
151
- if (data === '[DONE]') return undefined
152
- try { return JSON.parse(data) }
153
- catch (cause) { throw Object.assign(new Error('Responses stream contained malformed SSE JSON', { cause }), { code: 'LCX_INVALID_SSE' }) }
154
- }
155
- try {
156
- while (true) {
157
- if (options.signal?.aborted) throw options.signal.reason ?? Object.assign(new Error('request aborted'), { code: 'LCX_ABORTED' })
158
- const { done, value } = await reader.read()
159
- if (done) break
160
- bytes += value.byteLength
161
- if (bytes > maxBytes) throw Object.assign(new Error(`Responses SSE exceeds ${maxBytes} bytes`), { code: 'LCX_RESPONSE_TOO_LARGE' })
162
- pending += decoder.decode(value, { stream: true })
163
- let newline
164
- while ((newline = pending.indexOf('\n')) >= 0) {
165
- const line = pending.slice(0, newline).replace(/\r$/u, '')
166
- pending = pending.slice(newline + 1)
167
- if (line === '') {
168
- const event = decode()
169
- if (event !== undefined) yield event
170
- } else if (!line.startsWith(':') && line.startsWith('data:')) {
171
- dataLines.push(line.slice(5).replace(/^ /u, ''))
251
+ if (!response?.body)
252
+ throw Object.assign(new Error("Responses stream returned no body"), {
253
+ code: "LCX_INVALID_SSE",
254
+ status: response?.status,
255
+ });
256
+ const reader = response.body.getReader();
257
+ const decoder = new TextDecoder();
258
+ const maxBytes = options.maxResponseBytes ?? 8 * 1024 * 1024;
259
+ let bytes = 0;
260
+ let pending = "";
261
+ /** @type {string[]} */
262
+ let dataLines = [];
263
+ const decode = () => {
264
+ if (dataLines.length === 0)
265
+ return undefined;
266
+ const data = dataLines.join("\n");
267
+ dataLines = [];
268
+ if (data === "[DONE]")
269
+ return undefined;
270
+ try {
271
+ return JSON.parse(data);
172
272
  }
173
- }
273
+ catch (cause) {
274
+ throw Object.assign(new Error("Responses stream contained malformed SSE JSON", { cause }), { code: "LCX_INVALID_SSE" });
275
+ }
276
+ };
277
+ try {
278
+ while (true) {
279
+ if (options.signal?.aborted)
280
+ throw (options.signal.reason ??
281
+ Object.assign(new Error("request aborted"), { code: "LCX_ABORTED" }));
282
+ const { done, value } = await readWithSignal(reader, options.signal);
283
+ if (done)
284
+ break;
285
+ bytes += value.byteLength;
286
+ if (bytes > maxBytes)
287
+ throw Object.assign(new Error(`Responses SSE exceeds ${maxBytes} bytes`), { code: "LCX_RESPONSE_TOO_LARGE" });
288
+ pending += decoder.decode(value, { stream: true });
289
+ let newline;
290
+ while ((newline = pending.indexOf("\n")) >= 0) {
291
+ const line = pending.slice(0, newline).replace(/\r$/u, "");
292
+ pending = pending.slice(newline + 1);
293
+ if (line === "") {
294
+ const event = decode();
295
+ if (event !== undefined)
296
+ yield event;
297
+ }
298
+ else if (!line.startsWith(":") && line.startsWith("data:")) {
299
+ dataLines.push(line.slice(5).replace(/^ /u, ""));
300
+ }
301
+ }
302
+ }
303
+ pending += decoder.decode();
304
+ if (pending.startsWith("data:"))
305
+ dataLines.push(pending.slice(5).replace(/^ /u, ""));
306
+ const event = decode();
307
+ if (event !== undefined)
308
+ yield event;
309
+ }
310
+ finally {
311
+ await reader.cancel().catch(() => undefined);
312
+ }
313
+ }
314
+ function isSupportedOutputItem(value) {
315
+ if (!isObject(value))
316
+ return false;
317
+ if (value.type === "message")
318
+ return (typeof value.id === "string" &&
319
+ value.role === "assistant" &&
320
+ (value.status === "in_progress" ||
321
+ value.status === "completed" ||
322
+ value.status === "incomplete") &&
323
+ Array.isArray(value.content) &&
324
+ value.content.every((part) => isObject(part) &&
325
+ ((part.type === "output_text" &&
326
+ typeof part.text === "string" &&
327
+ Array.isArray(part.annotations)) ||
328
+ (part.type === "refusal" && typeof part.refusal === "string"))));
329
+ if (value.type === "reasoning")
330
+ return (typeof value.id === "string" &&
331
+ Array.isArray(value.summary) &&
332
+ value.summary.every((part) => isObject(part) && typeof part.text === "string"));
333
+ if (value.type === "function_call")
334
+ return (typeof value.id === "string" &&
335
+ typeof value.call_id === "string" &&
336
+ typeof value.name === "string" &&
337
+ typeof value.arguments === "string");
338
+ return (value.type === "custom_tool_call" &&
339
+ typeof value.id === "string" &&
340
+ typeof value.call_id === "string" &&
341
+ typeof value.name === "string" &&
342
+ typeof value.input === "string");
343
+ }
344
+ function isTerminalResponse(value) {
345
+ if (!isObject(value) || typeof value.id !== "string")
346
+ return false;
347
+ if (!Array.isArray(value.output))
348
+ return false;
349
+ return (value.status === "completed" ||
350
+ value.status === "incomplete" ||
351
+ value.status === "failed" ||
352
+ value.status === "cancelled" ||
353
+ value.status === "in_progress" ||
354
+ value.status === "queued");
355
+ }
356
+ const PI_RESPONSE_EVENT_TYPES = new Set([
357
+ "response.created",
358
+ "response.output_item.added",
359
+ "response.output_item.done",
360
+ "response.reasoning_summary_text.delta",
361
+ "response.reasoning_summary_part.done",
362
+ "response.reasoning_text.delta",
363
+ "response.output_text.delta",
364
+ "response.refusal.delta",
365
+ "response.function_call_arguments.delta",
366
+ "response.function_call_arguments.done",
367
+ "response.custom_tool_call_input.delta",
368
+ "response.custom_tool_call_input.done",
369
+ "response.completed",
370
+ "response.incomplete",
371
+ "response.failed",
372
+ "error",
373
+ ]);
374
+ /**
375
+ * Pi only consumes this public Responses subset. Validate all fields it reads before
376
+ * narrowing provider wire data, and silently preserve Pi's existing behavior of
377
+ * ignoring unrelated Responses events.
378
+ */
379
+ function isSupportedResponseStreamEvent(value) {
380
+ if (!isObject(value))
381
+ return false;
382
+ if (!Number.isSafeInteger(value.sequence_number) ||
383
+ typeof value.type !== "string" ||
384
+ !PI_RESPONSE_EVENT_TYPES.has(value.type))
385
+ return false;
386
+ if (value.type === "response.created")
387
+ return isObject(value.response) && typeof value.response.id === "string";
388
+ if (value.type === "response.output_item.added" ||
389
+ value.type === "response.output_item.done")
390
+ return Number.isSafeInteger(value.output_index) && isSupportedOutputItem(value.item);
391
+ if (value.type === "response.output_text.delta" ||
392
+ value.type === "response.refusal.delta" ||
393
+ value.type === "response.reasoning_text.delta")
394
+ return (Number.isSafeInteger(value.output_index) &&
395
+ Number.isSafeInteger(value.content_index) &&
396
+ typeof value.item_id === "string" &&
397
+ typeof value.delta === "string");
398
+ if (value.type === "response.reasoning_summary_text.delta")
399
+ return (Number.isSafeInteger(value.output_index) &&
400
+ Number.isSafeInteger(value.summary_index) &&
401
+ typeof value.item_id === "string" &&
402
+ typeof value.delta === "string");
403
+ if (value.type === "response.reasoning_summary_part.done")
404
+ return (Number.isSafeInteger(value.output_index) &&
405
+ Number.isSafeInteger(value.summary_index) &&
406
+ typeof value.item_id === "string");
407
+ if (value.type === "response.function_call_arguments.delta")
408
+ return (Number.isSafeInteger(value.output_index) &&
409
+ typeof value.item_id === "string" &&
410
+ typeof value.delta === "string");
411
+ if (value.type === "response.function_call_arguments.done")
412
+ return (Number.isSafeInteger(value.output_index) &&
413
+ typeof value.item_id === "string" &&
414
+ typeof value.arguments === "string");
415
+ if (value.type === "response.custom_tool_call_input.delta")
416
+ return (Number.isSafeInteger(value.output_index) &&
417
+ typeof value.item_id === "string" &&
418
+ typeof value.delta === "string");
419
+ if (value.type === "response.custom_tool_call_input.done")
420
+ return (Number.isSafeInteger(value.output_index) &&
421
+ typeof value.item_id === "string" &&
422
+ typeof value.input === "string");
423
+ if (value.type === "response.completed" ||
424
+ value.type === "response.incomplete")
425
+ return isTerminalResponse(value.response);
426
+ if (value.type === "response.failed")
427
+ return isObject(value.response);
428
+ return typeof value.code === "string" && typeof value.message === "string";
429
+ }
430
+ async function* validatedResponseEvents(source) {
431
+ let sequenceNumber = 0;
432
+ for await (const raw of source) {
433
+ if (!isObject(raw))
434
+ continue;
435
+ if (!PI_RESPONSE_EVENT_TYPES.has(String(raw.type ?? "")))
436
+ continue;
437
+ const event = { ...raw, sequence_number: sequenceNumber++ };
438
+ if (!isSupportedResponseStreamEvent(event))
439
+ throw Object.assign(new Error("Responses stream contained an unsupported Pi event"), { code: "LCX_INVALID_SSE" });
440
+ yield event;
174
441
  }
175
- pending += decoder.decode()
176
- if (pending.startsWith('data:')) dataLines.push(pending.slice(5).replace(/^ /u, ''))
177
- const event = decode()
178
- if (event !== undefined) yield event
179
- } finally {
180
- await reader.cancel().catch(() => undefined)
181
- }
182
442
  }
183
-
184
443
  /** @param {UnknownRecord} item */
185
444
  function itemKind(item) {
186
- if (item.type === 'message') return 'text'
187
- if (item.type === 'reasoning') return 'reasoning'
188
- if (item.type === 'function_call' || item.type === 'custom_tool_call') return 'tool-call'
189
- return undefined
445
+ if (item.type === "message")
446
+ return "text";
447
+ if (item.type === "reasoning")
448
+ return "reasoning";
449
+ if (item.type === "function_call" || item.type === "custom_tool_call")
450
+ return "tool-call";
451
+ return undefined;
190
452
  }
191
-
192
453
  /** @param {UnknownRecord} item */
193
454
  function itemIdentity(item) {
194
- const id = typeof item.id === 'string' && item.id ? item.id : typeof item.call_id === 'string' ? item.call_id : ''
195
- return id ? `${String(item.type)}:${id}` : undefined
455
+ const id = typeof item.id === "string" && item.id
456
+ ? item.id
457
+ : typeof item.call_id === "string"
458
+ ? item.call_id
459
+ : "";
460
+ return id ? `${String(item.type)}:${id}` : undefined;
196
461
  }
197
-
198
462
  /** @param {UnknownRecord} item */
199
463
  function itemText(item) {
200
- if (item.type === 'message') return (Array.isArray(item.content) ? item.content : []).map((part) => isObject(part) && (part.type === 'output_text' || part.type === 'refusal') ? String(part.text ?? part.refusal ?? '') : '').join('')
201
- if (item.type === 'reasoning') {
202
- const parts = Array.isArray(item.summary) && item.summary.length > 0 ? item.summary : Array.isArray(item.content) ? item.content : []
203
- return parts.map((part) => isObject(part) ? String(part.text ?? '') : '').join('\n\n')
204
- }
205
- if (item.type === 'function_call') return String(item.arguments ?? '')
206
- if (item.type === 'custom_tool_call') return String(item.input ?? '')
207
- return ''
464
+ if (item.type === "message")
465
+ return (Array.isArray(item.content) ? item.content : [])
466
+ .map((part) => isObject(part) &&
467
+ (part.type === "output_text" || part.type === "refusal")
468
+ ? String(part.text ?? part.refusal ?? "")
469
+ : "")
470
+ .join("");
471
+ if (item.type === "reasoning") {
472
+ const parts = Array.isArray(item.summary) && item.summary.length > 0
473
+ ? item.summary
474
+ : Array.isArray(item.content)
475
+ ? item.content
476
+ : [];
477
+ return parts
478
+ .map((part) => isObject(part) ? String(part.text ?? "") : "")
479
+ .join("\n\n");
480
+ }
481
+ if (item.type === "function_call")
482
+ return String(item.arguments ?? "");
483
+ if (item.type === "custom_tool_call")
484
+ return String(item.input ?? "");
485
+ return "";
486
+ }
487
+ const GROK_RENDER_MARKER = /(?:\bstateless_invoke\s+)?\{?render_inline_citation(?:\s*\(\s*citation_id\s*=\s*\d+\s*\)|\s+with\s+citation_id\s+is\s+\d+)\}?/giu;
488
+ const GROK_UI_CITATION_MARKER = /render_inline_citation[^\r\n]*/giu;
489
+ const GROK_PRIVATE_RENDER_MARKER = /[\uE000-\uF8FF]render_inline_citation[^\uE000-\uF8FF\r\n]*[\uE000-\uF8FF]/giu;
490
+ const GROK_NUMBERED_CITATION = /\[\[\d+\]\]\(https?:\/\/[^)\s]+\)/giu;
491
+ const GROK_EOS_MARKER = /<\|eos\|>/giu;
492
+ const GROK_SOURCE_TAIL = /(?:^|[ \t\r\n]+)Sources:\s*(?=(?:\r?\n)?\s*[-*])/iu;
493
+ function sanitizeGrokVisibleText(value) {
494
+ let text = value
495
+ .replace(GROK_RENDER_MARKER, "")
496
+ .replace(GROK_UI_CITATION_MARKER, "")
497
+ .replace(GROK_PRIVATE_RENDER_MARKER, "")
498
+ .replace(GROK_NUMBERED_CITATION, "")
499
+ .replace(GROK_EOS_MARKER, "");
500
+ const sourceTail = text.search(GROK_SOURCE_TAIL);
501
+ if (sourceTail >= 0)
502
+ text = text.slice(0, sourceTail);
503
+ return text.replace(/[ \t]{2,}/gu, " ").replace(/[ \t]+([,.;:!?])/gu, "$1").replace(/[ \t]+\n/gu, "\n").trimEnd();
504
+ }
505
+ function sanitizeGrokTerminalItem(item, index) {
506
+ const normalized = normalizedTerminalItem(item, index);
507
+ if (normalized.type !== "message" || !Array.isArray(normalized.content))
508
+ return normalized;
509
+ return {
510
+ ...normalized,
511
+ content: normalized.content.map((part) => {
512
+ if (!isObject(part) || (part.type !== "output_text" && part.type !== "refusal"))
513
+ return structuredClone(part);
514
+ const text = sanitizeGrokVisibleText(String(part.text ?? part.refusal ?? ""));
515
+ return part.type === "output_text"
516
+ ? { ...structuredClone(part), text }
517
+ : { ...structuredClone(part), refusal: text };
518
+ }),
519
+ };
208
520
  }
209
-
210
521
  /** @param {UnknownRecord} item @param {number} index */
211
522
  function normalizedTerminalItem(item, index) {
212
- if (item.type === 'message') return { ...structuredClone(item), id: typeof item.id === 'string' && item.id ? item.id : `msg_lcx_${index}`, role: 'assistant', status: item.status ?? 'completed', content: Array.isArray(item.content) ? structuredClone(item.content) : [] }
213
- if (item.type === 'reasoning') return { ...structuredClone(item), id: typeof item.id === 'string' && item.id ? item.id : `rs_lcx_${index}`, summary: Array.isArray(item.summary) ? structuredClone(item.summary) : [] }
214
- if (item.type === 'function_call') return { ...structuredClone(item), id: typeof item.id === 'string' && item.id ? item.id : `fc_lcx_${index}`, call_id: typeof item.call_id === 'string' && item.call_id ? item.call_id : `call_lcx_${index}`, name: String(item.name ?? ''), arguments: String(item.arguments ?? '') }
215
- if (item.type === 'custom_tool_call') return { ...structuredClone(item), id: typeof item.id === 'string' && item.id ? item.id : `ctc_lcx_${index}`, call_id: typeof item.call_id === 'string' && item.call_id ? item.call_id : `call_lcx_${index}`, name: String(item.name ?? ''), input: String(item.input ?? '') }
216
- return structuredClone(item)
523
+ if (item.type === "message")
524
+ return {
525
+ ...structuredClone(item),
526
+ id: typeof item.id === "string" && item.id ? item.id : `msg_lcx_${index}`,
527
+ role: "assistant",
528
+ status: item.status ?? "completed",
529
+ content: Array.isArray(item.content) ? structuredClone(item.content) : [],
530
+ };
531
+ if (item.type === "reasoning")
532
+ return {
533
+ ...structuredClone(item),
534
+ id: typeof item.id === "string" && item.id ? item.id : `rs_lcx_${index}`,
535
+ summary: Array.isArray(item.summary) ? structuredClone(item.summary) : [],
536
+ };
537
+ if (item.type === "function_call")
538
+ return {
539
+ ...structuredClone(item),
540
+ id: typeof item.id === "string" && item.id ? item.id : `fc_lcx_${index}`,
541
+ call_id: typeof item.call_id === "string" && item.call_id
542
+ ? item.call_id
543
+ : `call_lcx_${index}`,
544
+ name: String(item.name ?? ""),
545
+ arguments: String(item.arguments ?? ""),
546
+ };
547
+ if (item.type === "custom_tool_call")
548
+ return {
549
+ ...structuredClone(item),
550
+ id: typeof item.id === "string" && item.id ? item.id : `ctc_lcx_${index}`,
551
+ call_id: typeof item.call_id === "string" && item.call_id
552
+ ? item.call_id
553
+ : `call_lcx_${index}`,
554
+ name: String(item.name ?? ""),
555
+ input: String(item.input ?? ""),
556
+ };
557
+ return structuredClone(item);
217
558
  }
218
-
219
559
  /** @param {UnknownRecord} item @param {number} index */
220
560
  function addedShell(item, index) {
221
- if (item.type === 'message') return { type: 'message', id: item.id ?? `msg_lcx_${index}`, role: 'assistant', status: 'in_progress', content: [] }
222
- if (item.type === 'reasoning') return { type: 'reasoning', id: item.id ?? `rs_lcx_${index}`, summary: [] }
223
- if (item.type === 'function_call') return { type: 'function_call', id: item.id ?? `fc_lcx_${index}`, call_id: item.call_id ?? `call_lcx_${index}`, name: String(item.name ?? ''), arguments: '' }
224
- return { type: 'custom_tool_call', id: item.id ?? `ctc_lcx_${index}`, call_id: item.call_id ?? `call_lcx_${index}`, name: String(item.name ?? ''), input: '' }
561
+ if (item.type === "message")
562
+ return {
563
+ type: "message",
564
+ id: item.id ?? `msg_lcx_${index}`,
565
+ role: "assistant",
566
+ status: "in_progress",
567
+ content: [],
568
+ };
569
+ if (item.type === "reasoning")
570
+ return { type: "reasoning", id: item.id ?? `rs_lcx_${index}`, summary: [] };
571
+ if (item.type === "function_call")
572
+ return {
573
+ type: "function_call",
574
+ id: item.id ?? `fc_lcx_${index}`,
575
+ call_id: item.call_id ?? `call_lcx_${index}`,
576
+ name: String(item.name ?? ""),
577
+ arguments: "",
578
+ };
579
+ return {
580
+ type: "custom_tool_call",
581
+ id: item.id ?? `ctc_lcx_${index}`,
582
+ call_id: item.call_id ?? `call_lcx_${index}`,
583
+ name: String(item.name ?? ""),
584
+ input: "",
585
+ };
225
586
  }
226
-
227
587
  /** @param {UnknownRecord} record @param {UnknownRecord} item */
228
588
  function recordMatches(record, item) {
229
- if (record.kind !== itemKind(item)) return false
230
- const recordItem = /** @type {UnknownRecord} */ (record.item)
231
- if (itemIdentity(recordItem) && itemIdentity(recordItem) === itemIdentity(item)) return true
232
- const streamed = String(record.text ?? '')
233
- const terminal = itemText(item)
234
- return streamed.length > 0 && terminal.length > 0 && (streamed === terminal || streamed.startsWith(terminal) || terminal.startsWith(streamed))
589
+ if (record.kind !== itemKind(item))
590
+ return false;
591
+ const recordItem = /** @type {UnknownRecord} */ record.item;
592
+ if (itemIdentity(recordItem) &&
593
+ itemIdentity(recordItem) === itemIdentity(item))
594
+ return true;
595
+ const streamed = String(record.text ?? "");
596
+ const terminal = itemText(item);
597
+ return (streamed.length > 0 &&
598
+ terminal.length > 0 &&
599
+ (streamed === terminal ||
600
+ streamed.startsWith(terminal) ||
601
+ terminal.startsWith(streamed)));
235
602
  }
236
-
237
603
  /**
238
604
  * Some compatible gateways omit `response.output_item.added/done`, or terminal output indexes drift
239
605
  * when a reasoning item is inserted. Normalize only the missing framing; Pi remains authoritative for item semantics.
240
606
  * @param {AsyncIterable<unknown>} source
241
607
  * @param {{ responseModel?: string }} [meta]
242
608
  */
243
- async function* normalizedResponseEvents(source, meta = {}) {
244
- const open = new Map()
245
- const completed = new Set()
246
- /** @param {number} index @param {UnknownRecord} item */
247
- const ensure = function* (index, item) {
248
- let record = open.get(index)
249
- if (record) return record
250
- const normalized = normalizedTerminalItem(item, index)
251
- record = { kind: itemKind(normalized), item: normalized, text: '' }
252
- open.set(index, record)
253
- yield { type: 'response.output_item.added', output_index: index, item: addedShell(normalized, index) }
254
- return record
255
- }
256
-
257
- for await (const raw of source) {
258
- if (!isObject(raw)) continue
259
- const event = /** @type {UnknownRecord} */ (raw)
260
- const index = Number.isInteger(event.output_index) ? Number(event.output_index) : 0
261
- if (event.type === 'response.output_item.added' && isObject(event.item)) {
262
- const item = normalizedTerminalItem(/** @type {UnknownRecord} */ (event.item), index)
263
- open.set(index, { kind: itemKind(item), item, text: itemText(item) })
264
- yield { ...event, item }
265
- continue
266
- }
267
- if (event.type === 'response.output_text.delta') {
268
- let record = open.get(index)
269
- if (!record) {
270
- const item = { type: 'message', id: typeof event.item_id === 'string' ? event.item_id : `msg_lcx_${index}` }
271
- const generated = ensure(index, item)
272
- let next = generated.next()
273
- while (!next.done) { yield next.value; next = generated.next() }
274
- record = next.value
275
- }
276
- record.text = String(record.text ?? '') + String(event.delta ?? '')
277
- yield event
278
- continue
279
- }
280
- if (event.type === 'response.reasoning_summary_text.delta' || event.type === 'response.reasoning_text.delta') {
281
- let record = open.get(index)
282
- if (!record) {
283
- const item = { type: 'reasoning', id: typeof event.item_id === 'string' ? event.item_id : `rs_lcx_${index}` }
284
- const generated = ensure(index, item)
285
- let next = generated.next()
286
- while (!next.done) { yield next.value; next = generated.next() }
287
- record = next.value
288
- }
289
- record.text = String(record.text ?? '') + String(event.delta ?? '')
290
- yield event
291
- continue
292
- }
293
- if (event.type === 'response.function_call_arguments.delta') {
294
- let record = open.get(index)
295
- if (!record) {
296
- const item = { type: 'function_call', id: typeof event.item_id === 'string' ? event.item_id : `fc_lcx_${index}`, call_id: typeof event.call_id === 'string' ? event.call_id : `call_lcx_${index}`, name: String(event.name ?? '') }
297
- const generated = ensure(index, item)
298
- let next = generated.next()
299
- while (!next.done) { yield next.value; next = generated.next() }
300
- record = next.value
301
- }
302
- record.text = String(record.text ?? '') + String(event.delta ?? '')
303
- yield event
304
- continue
305
- }
306
- if (event.type === 'response.custom_tool_call_input.delta') {
307
- let record = open.get(index)
308
- if (!record) {
309
- const item = { type: 'custom_tool_call', id: typeof event.item_id === 'string' ? event.item_id : `ctc_lcx_${index}`, call_id: typeof event.call_id === 'string' ? event.call_id : `call_lcx_${index}`, name: String(event.name ?? '') }
310
- const generated = ensure(index, item)
311
- let next = generated.next()
312
- while (!next.done) { yield next.value; next = generated.next() }
313
- record = next.value
314
- }
315
- record.text = String(record.text ?? '') + String(event.delta ?? '')
316
- yield event
317
- continue
318
- }
319
- if (event.type === 'response.output_item.done' && isObject(event.item)) {
320
- const item = normalizedTerminalItem(/** @type {UnknownRecord} */ (event.item), index)
321
- const identity = itemIdentity(item)
322
- if (identity) completed.add(identity)
323
- open.delete(index)
324
- yield { ...event, item }
325
- continue
326
- }
327
- if ((event.type === 'response.completed' || event.type === 'response.incomplete') && isObject(event.response)) {
328
- const response = /** @type {UnknownRecord} */ (event.response)
329
- if (typeof response.model === 'string' && response.model.length > 0) meta.responseModel = response.model
330
- const output = Array.isArray(response.output) ? response.output.map((item, terminalIndex) => isObject(item) ? normalizedTerminalItem(/** @type {UnknownRecord} */ (item), terminalIndex) : item) : []
331
- const used = new Set()
332
- for (const [streamIndex, record] of open) {
333
- let terminalIndex = output.findIndex((item, candidateIndex) => !used.has(candidateIndex) && isObject(item) && candidateIndex === streamIndex && recordMatches(record, /** @type {UnknownRecord} */ (item)))
334
- if (terminalIndex < 0) terminalIndex = output.findIndex((item, candidateIndex) => !used.has(candidateIndex) && isObject(item) && recordMatches(record, /** @type {UnknownRecord} */ (item)))
335
- const item = terminalIndex >= 0
336
- ? /** @type {UnknownRecord} */ (output[terminalIndex])
337
- : normalizedTerminalItem(/** @type {UnknownRecord} */ (record.item), streamIndex)
338
- if (terminalIndex >= 0) used.add(terminalIndex)
339
- yield { type: 'response.output_item.done', output_index: streamIndex, item }
340
- const identity = itemIdentity(item)
341
- if (identity) completed.add(identity)
342
- }
343
- open.clear()
344
- for (const [terminalIndex, candidate] of output.entries()) {
345
- if (!isObject(candidate) || !itemKind(/** @type {UnknownRecord} */ (candidate)) || used.has(terminalIndex)) continue
346
- const item = /** @type {UnknownRecord} */ (candidate)
347
- const identity = itemIdentity(item)
348
- if (identity && completed.has(identity)) continue
349
- yield { type: 'response.output_item.added', output_index: terminalIndex, item: addedShell(item, terminalIndex) }
350
- const text = itemText(item)
351
- if (text) {
352
- if (item.type === 'message') yield { type: 'response.output_text.delta', output_index: terminalIndex, content_index: 0, item_id: item.id, delta: text }
353
- else if (item.type === 'reasoning') yield { type: 'response.reasoning_summary_text.delta', output_index: terminalIndex, summary_index: 0, item_id: item.id, delta: text }
354
- else if (item.type === 'function_call') yield { type: 'response.function_call_arguments.delta', output_index: terminalIndex, item_id: item.id, call_id: item.call_id, name: item.name, delta: text }
355
- else yield { type: 'response.custom_tool_call_input.delta', output_index: terminalIndex, item_id: item.id, call_id: item.call_id, name: item.name, delta: text }
609
+ async function* normalizedResponseEvents(source, meta = {}, options = {}) {
610
+ const open = new Map();
611
+ const grokTextProjection = new Map();
612
+ const hiddenPrefixTokens = ["sources:", "render_inline_citation", "{render_inline_citation", "render_inline_citation", "stateless_invoke", "[[", "<|eos|>"];
613
+ const safeGrokRawPrefix = (raw, final) => {
614
+ if (final || raw.search(GROK_SOURCE_TAIL) >= 0)
615
+ return raw;
616
+ const lower = raw.toLowerCase();
617
+ let holdStart = raw.length;
618
+ const pendingSource = lower.lastIndexOf("sources:");
619
+ if (pendingSource >= 0 && (pendingSource === 0 || /\s/u.test(raw[pendingSource - 1] ?? "")))
620
+ holdStart = Math.min(holdStart, pendingSource);
621
+ for (const token of hiddenPrefixTokens) {
622
+ const complete = lower.lastIndexOf(token);
623
+ if (complete >= 0) {
624
+ const tail = raw.slice(complete);
625
+ if (sanitizeGrokVisibleText(tail).toLowerCase().includes(token.replace(/^\{/u, "")))
626
+ holdStart = Math.min(holdStart, complete);
627
+ }
628
+ const max = Math.min(token.length - 1, lower.length);
629
+ for (let size = max; size > 0; size -= 1) {
630
+ if (lower.endsWith(token.slice(0, size))) {
631
+ holdStart = Math.min(holdStart, raw.length - size);
632
+ break;
633
+ }
634
+ }
635
+ }
636
+ return raw.slice(0, holdStart);
637
+ };
638
+ const grokProjectionDelta = (index, rawText, final = false) => {
639
+ let state = grokTextProjection.get(index);
640
+ if (!state) {
641
+ state = { raw: "", emitted: "" };
642
+ grokTextProjection.set(index, state);
643
+ }
644
+ state.raw = rawText;
645
+ const target = sanitizeGrokVisibleText(safeGrokRawPrefix(rawText, final));
646
+ if (!target.startsWith(state.emitted))
647
+ return "";
648
+ const delta = target.slice(state.emitted.length);
649
+ state.emitted = target;
650
+ if (final)
651
+ grokTextProjection.delete(index);
652
+ return delta;
653
+ };
654
+ const completed = new Set();
655
+ const completedWireItems = new Map();
656
+ const serverToolIds = new Map();
657
+ const isServerToolItem = (value) => isObject(value) && options.serverToolTypes?.has(String(value.type ?? "")) === true;
658
+ const observeServerTool = (item) => {
659
+ const type = String(item.type);
660
+ let ids = serverToolIds.get(type);
661
+ if (!ids) {
662
+ ids = new Set();
663
+ serverToolIds.set(type, ids);
664
+ }
665
+ ids.add(typeof item.id === "string" && item.id ? item.id : `${type}:${ids.size}`);
666
+ };
667
+ /** @param {number} index @param {UnknownRecord} item */
668
+ const ensure = function* (index, item) {
669
+ let record = open.get(index);
670
+ if (record)
671
+ return record;
672
+ const normalized = normalizedTerminalItem(item, index);
673
+ record = { kind: itemKind(normalized), item: normalized, text: "" };
674
+ open.set(index, record);
675
+ yield {
676
+ type: "response.output_item.added",
677
+ output_index: index,
678
+ item: addedShell(normalized, index),
679
+ };
680
+ return record;
681
+ };
682
+ for await (const raw of source) {
683
+ if (!isObject(raw))
684
+ continue;
685
+ const event = /** @type {UnknownRecord} */ raw;
686
+ const index = Number.isInteger(event.output_index)
687
+ ? Number(event.output_index)
688
+ : 0;
689
+ if (event.type === "response.output_item.done" && isObject(event.item))
690
+ completedWireItems.set(index, structuredClone(event.item));
691
+ if ((event.type === "response.output_item.added" ||
692
+ event.type === "response.output_item.done") &&
693
+ isServerToolItem(event.item)) {
694
+ observeServerTool(event.item);
695
+ continue;
696
+ }
697
+ // Grok may send many encrypted reasoning items with no visible summary.
698
+ // Keep them in completedWireItems/nativeOutput, but do not open empty UI blocks.
699
+ if (options.serverToolTypes &&
700
+ (event.type === "response.output_item.added" || event.type === "response.output_item.done") &&
701
+ isObject(event.item) && event.item.type === "reasoning" &&
702
+ !itemText(event.item).trim() && !open.get(index)?.text.trim())
703
+ continue;
704
+ if (event.type === "response.output_item.added" && isObject(event.item)) {
705
+ const rawItem = normalizedTerminalItem(event.item, index);
706
+ const item = options.serverToolTypes ? sanitizeGrokTerminalItem(rawItem, index) : rawItem;
707
+ open.set(index, { kind: itemKind(item), item: rawItem, text: itemText(rawItem) });
708
+ yield { ...event, item: options.serverToolTypes ? addedShell(item, index) : item };
709
+ continue;
710
+ }
711
+ if (event.type === "response.output_text.delta") {
712
+ let record = open.get(index);
713
+ if (!record) {
714
+ const item = {
715
+ type: "message",
716
+ id: typeof event.item_id === "string"
717
+ ? event.item_id
718
+ : `msg_lcx_${index}`,
719
+ };
720
+ const generated = ensure(index, item);
721
+ let next = generated.next();
722
+ while (!next.done) {
723
+ yield next.value;
724
+ next = generated.next();
725
+ }
726
+ record = next.value;
727
+ }
728
+ record.text = String(record.text ?? "") + String(event.delta ?? "");
729
+ if (!options.serverToolTypes)
730
+ yield event;
731
+ else {
732
+ const delta = grokProjectionDelta(index, record.text);
733
+ if (delta)
734
+ yield { ...event, delta };
735
+ }
736
+ continue;
737
+ }
738
+ if (event.type === "response.reasoning_summary_text.delta" ||
739
+ event.type === "response.reasoning_text.delta") {
740
+ if (options.serverToolTypes && !open.has(index) && !String(event.delta ?? "").trim())
741
+ continue;
742
+ let record = open.get(index);
743
+ if (!record) {
744
+ const item = {
745
+ type: "reasoning",
746
+ id: typeof event.item_id === "string"
747
+ ? event.item_id
748
+ : `rs_lcx_${index}`,
749
+ };
750
+ const generated = ensure(index, item);
751
+ let next = generated.next();
752
+ while (!next.done) {
753
+ yield next.value;
754
+ next = generated.next();
755
+ }
756
+ record = next.value;
757
+ }
758
+ record.text = String(record.text ?? "") + String(event.delta ?? "");
759
+ yield event;
760
+ continue;
761
+ }
762
+ if (event.type === "response.function_call_arguments.delta") {
763
+ let record = open.get(index);
764
+ if (!record) {
765
+ const item = {
766
+ type: "function_call",
767
+ id: typeof event.item_id === "string"
768
+ ? event.item_id
769
+ : `fc_lcx_${index}`,
770
+ call_id: typeof event.call_id === "string"
771
+ ? event.call_id
772
+ : `call_lcx_${index}`,
773
+ name: String(event.name ?? ""),
774
+ };
775
+ const generated = ensure(index, item);
776
+ let next = generated.next();
777
+ while (!next.done) {
778
+ yield next.value;
779
+ next = generated.next();
780
+ }
781
+ record = next.value;
782
+ }
783
+ record.text = String(record.text ?? "") + String(event.delta ?? "");
784
+ yield event;
785
+ continue;
786
+ }
787
+ if (event.type === "response.custom_tool_call_input.delta") {
788
+ let record = open.get(index);
789
+ if (!record) {
790
+ const item = {
791
+ type: "custom_tool_call",
792
+ id: typeof event.item_id === "string"
793
+ ? event.item_id
794
+ : `ctc_lcx_${index}`,
795
+ call_id: typeof event.call_id === "string"
796
+ ? event.call_id
797
+ : `call_lcx_${index}`,
798
+ name: String(event.name ?? ""),
799
+ };
800
+ const generated = ensure(index, item);
801
+ let next = generated.next();
802
+ while (!next.done) {
803
+ yield next.value;
804
+ next = generated.next();
805
+ }
806
+ record = next.value;
807
+ }
808
+ record.text = String(record.text ?? "") + String(event.delta ?? "");
809
+ yield event;
810
+ continue;
356
811
  }
357
- yield { type: 'response.output_item.done', output_index: terminalIndex, item }
358
- if (identity) completed.add(identity)
359
- }
360
- yield { ...event, response: { ...response, output } }
361
- continue
812
+ if (event.type === "response.output_item.done" && isObject(event.item)) {
813
+ const rawItem = normalizedTerminalItem(event.item, index);
814
+ const item = options.serverToolTypes ? sanitizeGrokTerminalItem(rawItem, index) : rawItem;
815
+ if (options.serverToolTypes && item.type === "message") {
816
+ const delta = grokProjectionDelta(index, itemText(rawItem), true);
817
+ if (delta)
818
+ yield { type: "response.output_text.delta", output_index: index, content_index: 0, item_id: item.id, delta };
819
+ }
820
+ const identity = itemIdentity(item);
821
+ if (identity)
822
+ completed.add(identity);
823
+ open.delete(index);
824
+ yield { ...event, item };
825
+ continue;
826
+ }
827
+ if ((event.type === "response.completed" ||
828
+ event.type === "response.incomplete") &&
829
+ isObject(event.response)) {
830
+ const response = /** @type {UnknownRecord} */ event.response;
831
+ if (typeof response.model === "string" && response.model.length > 0)
832
+ meta.responseModel = response.model;
833
+ const terminalOutput = Array.isArray(response.output) && response.output.length > 0
834
+ ? response.output
835
+ : [...completedWireItems.entries()]
836
+ .sort(([left], [right]) => left - right)
837
+ .map(([, item]) => item);
838
+ const nativeOutput = terminalOutput.map((item) => structuredClone(item));
839
+ for (const item of terminalOutput) {
840
+ if (isServerToolItem(item))
841
+ observeServerTool(item);
842
+ }
843
+ const output = terminalOutput
844
+ .filter((item) => !isServerToolItem(item))
845
+ .filter((item) => !(options.serverToolTypes && isObject(item) &&
846
+ item.type === "reasoning" && !itemText(item).trim()))
847
+ .map((item, terminalIndex) => isObject(item)
848
+ ? (options.serverToolTypes
849
+ ? sanitizeGrokTerminalItem(item, terminalIndex)
850
+ : normalizedTerminalItem(item, terminalIndex))
851
+ : item);
852
+ if (event.type === "response.completed" &&
853
+ response.status === "completed" &&
854
+ options.serverToolTypes) {
855
+ meta.nativeOutput = nativeOutput;
856
+ }
857
+ const used = new Set();
858
+ for (const [streamIndex, record] of open) {
859
+ let terminalIndex = output.findIndex((item, candidateIndex) => !used.has(candidateIndex) &&
860
+ isObject(item) &&
861
+ candidateIndex === streamIndex &&
862
+ recordMatches(record, item));
863
+ if (terminalIndex < 0)
864
+ terminalIndex = output.findIndex((item, candidateIndex) => !used.has(candidateIndex) &&
865
+ isObject(item) &&
866
+ recordMatches(record, item));
867
+ const item = terminalIndex >= 0
868
+ ? output[terminalIndex]
869
+ : normalizedTerminalItem(record.item, streamIndex);
870
+ if (terminalIndex >= 0)
871
+ used.add(terminalIndex);
872
+ if (options.serverToolTypes && item.type === "message") {
873
+ const delta = grokProjectionDelta(streamIndex, itemText(item), true);
874
+ if (delta)
875
+ yield { type: "response.output_text.delta", output_index: streamIndex, content_index: 0, item_id: item.id, delta };
876
+ }
877
+ yield {
878
+ type: "response.output_item.done",
879
+ output_index: streamIndex,
880
+ item,
881
+ };
882
+ const identity = itemIdentity(item);
883
+ if (identity)
884
+ completed.add(identity);
885
+ }
886
+ open.clear();
887
+ grokTextProjection.clear();
888
+ for (const [terminalIndex, candidate] of output.entries()) {
889
+ if (!isObject(candidate) ||
890
+ !itemKind(candidate) ||
891
+ used.has(terminalIndex))
892
+ continue;
893
+ const item = candidate;
894
+ const identity = itemIdentity(item);
895
+ if (identity && completed.has(identity))
896
+ continue;
897
+ yield {
898
+ type: "response.output_item.added",
899
+ output_index: terminalIndex,
900
+ item: addedShell(item, terminalIndex),
901
+ };
902
+ const text = itemText(item);
903
+ if (text) {
904
+ if (item.type === "message")
905
+ yield {
906
+ type: "response.output_text.delta",
907
+ output_index: terminalIndex,
908
+ content_index: 0,
909
+ item_id: item.id,
910
+ delta: text,
911
+ };
912
+ else if (item.type === "reasoning")
913
+ yield {
914
+ type: "response.reasoning_summary_text.delta",
915
+ output_index: terminalIndex,
916
+ summary_index: 0,
917
+ item_id: item.id,
918
+ delta: text,
919
+ };
920
+ else if (item.type === "function_call")
921
+ yield {
922
+ type: "response.function_call_arguments.delta",
923
+ output_index: terminalIndex,
924
+ item_id: item.id,
925
+ call_id: item.call_id,
926
+ name: item.name,
927
+ delta: text,
928
+ };
929
+ else
930
+ yield {
931
+ type: "response.custom_tool_call_input.delta",
932
+ output_index: terminalIndex,
933
+ item_id: item.id,
934
+ call_id: item.call_id,
935
+ name: item.name,
936
+ delta: text,
937
+ };
938
+ }
939
+ yield {
940
+ type: "response.output_item.done",
941
+ output_index: terminalIndex,
942
+ item,
943
+ };
944
+ if (identity)
945
+ completed.add(identity);
946
+ }
947
+ if (options.onServerToolUsage) {
948
+ const usage = isObject(response.usage) ? response.usage : {};
949
+ const details = isObject(usage.server_side_tool_usage_details)
950
+ ? usage.server_side_tool_usage_details
951
+ : {};
952
+ const safeCount = (value, fallback) => Number.isSafeInteger(value) && Number(value) >= 0
953
+ ? Number(value)
954
+ : fallback;
955
+ const webSearchCalls = safeCount(details.web_search_calls, serverToolIds.get("web_search_call")?.size ?? 0);
956
+ const xSearchCalls = safeCount(details.x_search_calls, serverToolIds.get("x_search_call")?.size ?? 0);
957
+ const total = safeCount(usage.num_server_side_tools_used, webSearchCalls + xSearchCalls);
958
+ if (total > 0 || webSearchCalls > 0 || xSearchCalls > 0)
959
+ options.onServerToolUsage({ webSearchCalls, xSearchCalls, total });
960
+ }
961
+ yield { ...event, response: { ...response, output } };
962
+ continue;
963
+ }
964
+ yield event;
362
965
  }
363
- yield event
364
- }
365
966
  }
366
-
367
967
  /** @param {unknown} usage */
968
+ function isManagedFailure(value) {
969
+ return (isObject(value) &&
970
+ typeof value.message === "string" &&
971
+ typeof value.code === "string" &&
972
+ (value.status === undefined || typeof value.status === "number") &&
973
+ (value.requestId === undefined || typeof value.requestId === "string") &&
974
+ (value.providerRetryAfterMs === undefined ||
975
+ typeof value.providerRetryAfterMs === "number"));
976
+ }
368
977
  function dshUsage(usage) {
369
- const value = /** @type {UnknownRecord} */ (isObject(usage) ? usage : {})
370
- return {
371
- inputTokens: Number(value.input ?? 0),
372
- outputTokens: Number(value.output ?? 0),
373
- ...(Number(value.cacheRead ?? 0) > 0 ? { cacheReadTokens: Number(value.cacheRead) } : {}),
374
- ...(Number(value.cacheWrite ?? 0) > 0 ? { cacheWriteTokens: Number(value.cacheWrite) } : {}),
375
- ...(Number(value.reasoning ?? 0) > 0 ? { reasoningTokens: Number(value.reasoning) } : {}),
376
- }
978
+ const value = /** @type {UnknownRecord} */ isObject(usage) ? usage : {};
979
+ return {
980
+ inputTokens: Number(value.input ?? 0),
981
+ outputTokens: Number(value.output ?? 0),
982
+ ...(Number(value.cacheRead ?? 0) > 0
983
+ ? { cacheReadTokens: Number(value.cacheRead) }
984
+ : {}),
985
+ ...(Number(value.cacheWrite ?? 0) > 0
986
+ ? { cacheWriteTokens: Number(value.cacheWrite) }
987
+ : {}),
988
+ ...(Number(value.reasoning ?? 0) > 0
989
+ ? { reasoningTokens: Number(value.reasoning) }
990
+ : {}),
991
+ };
377
992
  }
378
-
379
993
  /** @param {unknown} value */
380
994
  function rawArguments(value) {
381
- try { return JSON.stringify(isObject(value) ? value : {}) }
382
- catch { return '{}' }
995
+ try {
996
+ return JSON.stringify(isObject(value) ? value : {});
997
+ }
998
+ catch {
999
+ return "{}";
1000
+ }
383
1001
  }
384
-
385
1002
  /** @param {unknown} message */
386
- function replayState(message) {
387
- if (!isObject(message)) return undefined
388
- const content = /** @type {UnknownRecord[]} */ (Array.isArray(message.content) ? message.content.filter(isObject) : [])
389
- const provider = typeof message.provider === 'string' ? message.provider : undefined
390
- const model = typeof message.model === 'string' ? message.model : undefined
391
- const api = typeof message.api === 'string' ? message.api : undefined
392
- if (!provider || !model || !api) return undefined
393
- return {
394
- response: {
395
- kind: 'pi-ai',
396
- version: 2,
397
- api,
398
- provider,
399
- model,
400
- ...(typeof message.responseModel === 'string' ? { responseModel: message.responseModel } : {}),
401
- ...(typeof message.responseId === 'string' ? { responseId: message.responseId } : {}),
402
- stopReason: message.stopReason,
403
- },
404
- blocks: content.map((block) => {
405
- if (block?.type === 'text') return { type: 'text', ...(typeof block.textSignature === 'string' ? { textSignature: block.textSignature } : {}) }
406
- if (block?.type === 'thinking') return { type: 'reasoning', ...(typeof block.thinkingSignature === 'string' ? { thinkingSignature: block.thinkingSignature } : {}), ...(typeof block.redacted === 'boolean' ? { redacted: block.redacted } : {}) }
407
- return {
408
- type: 'tool-call',
409
- ...(typeof block?.thoughtSignature === 'string' ? { thoughtSignature: block.thoughtSignature } : {}),
410
- ...(typeof block?.namespace === 'string' && block.namespace.length > 0 ? { namespace: block.namespace } : {}),
411
- }
412
- }),
413
- }
1003
+ function replayState(message, nativeOutput, nativeReplayRoute) {
1004
+ if (!isObject(message))
1005
+ return undefined;
1006
+ const content = Array.isArray(message.content)
1007
+ ? message.content.filter(isObject)
1008
+ : [];
1009
+ const provider = typeof message.provider === "string" ? message.provider : undefined;
1010
+ const model = typeof message.model === "string" ? message.model : undefined;
1011
+ const api = typeof message.api === "string" ? message.api : undefined;
1012
+ if (!provider || !model || !api)
1013
+ return undefined;
1014
+ const grokNative = createGrokNativeReplayEnvelope(nativeOutput, nativeReplayRoute);
1015
+ return {
1016
+ response: {
1017
+ kind: "pi-ai",
1018
+ version: 2,
1019
+ api,
1020
+ provider,
1021
+ model,
1022
+ ...(typeof message.responseModel === "string"
1023
+ ? { responseModel: message.responseModel }
1024
+ : {}),
1025
+ ...(typeof message.responseId === "string"
1026
+ ? { responseId: message.responseId }
1027
+ : {}),
1028
+ stopReason: message.stopReason,
1029
+ },
1030
+ blocks: content.map((block) => {
1031
+ if (block?.type === "text")
1032
+ return {
1033
+ type: "text",
1034
+ ...(typeof block.textSignature === "string"
1035
+ ? { textSignature: block.textSignature }
1036
+ : {}),
1037
+ };
1038
+ if (block?.type === "thinking")
1039
+ return {
1040
+ type: "reasoning",
1041
+ ...(typeof block.thinkingSignature === "string"
1042
+ ? { thinkingSignature: block.thinkingSignature }
1043
+ : {}),
1044
+ ...(typeof block.redacted === "boolean"
1045
+ ? { redacted: block.redacted }
1046
+ : {}),
1047
+ };
1048
+ return {
1049
+ type: "tool-call",
1050
+ ...(typeof block?.thoughtSignature === "string"
1051
+ ? { thoughtSignature: block.thoughtSignature }
1052
+ : {}),
1053
+ ...(typeof block?.namespace === "string" && block.namespace.length > 0
1054
+ ? { namespace: block.namespace }
1055
+ : {}),
1056
+ };
1057
+ }),
1058
+ ...(grokNative === undefined ? {} : { grokNative }),
1059
+ };
414
1060
  }
415
-
416
1061
  /** @param {unknown} message */
417
1062
  function successfulFinish(message) {
418
- const value = /** @type {UnknownRecord} */ (isObject(message) ? message : {})
419
- const stopReason = String(value.stopReason ?? 'stop')
420
- const content = /** @type {UnknownRecord[]} */ (Array.isArray(value.content) ? value.content.filter(isObject) : [])
421
- if (stopReason === 'length') return { kind: 'max-tokens' }
422
- if (stopReason === 'toolUse' || content.some((block) => block?.type === 'toolCall')) return { kind: 'tool-calls' }
423
- if (stopReason === 'stop' && content.length === 0) return { kind: 'error', failure: { message: 'Responses provider completed without content', code: 'EMPTY_RESPONSE' } }
424
- return { kind: 'stop' }
1063
+ const value = /** @type {UnknownRecord} */ isObject(message) ? message : {};
1064
+ const stopReason = String(value.stopReason ?? "stop");
1065
+ const content = /** @type {UnknownRecord[]} */ Array.isArray(value.content)
1066
+ ? value.content.filter(isObject)
1067
+ : [];
1068
+ if (stopReason === "length")
1069
+ return { kind: "max-tokens" };
1070
+ if (stopReason === "toolUse" ||
1071
+ content.some((block) => block.type === "toolCall"))
1072
+ return { kind: "tool-calls" };
1073
+ if (stopReason === "stop" && content.length === 0)
1074
+ return {
1075
+ kind: "error",
1076
+ failure: {
1077
+ message: "Responses provider completed without content",
1078
+ code: "EMPTY_RESPONSE",
1079
+ },
1080
+ };
1081
+ return { kind: "stop" };
425
1082
  }
426
-
427
1083
  /**
428
1084
  * Pi event vocabulary -> DSH StreamChunk. This remains thin and provider-neutral.
429
- * @param {AsyncIterable<unknown>} events
1085
+ * @param {AsyncIterable<AssistantMessageEvent>} events
430
1086
  * @param {AbortSignal} [signal]
431
1087
  */
432
- async function* toDshChunks(events, signal) {
433
- const toolIds = new Map()
434
- for await (const raw of events) {
435
- const event = /** @type {UnknownRecord} */ (raw)
436
- switch (event.type) {
437
- case 'start': break
438
- case 'text_start': yield { type: 'block-start', index: Number(event.contentIndex), blockType: 'text' }; break
439
- case 'text_delta': yield { type: 'text-delta', index: Number(event.contentIndex), text: String(event.delta ?? '') }; break
440
- case 'text_end': yield { type: 'block-end', index: Number(event.contentIndex), block: { type: 'text', text: String(event.content ?? '') } }; break
441
- case 'thinking_start': yield { type: 'block-start', index: Number(event.contentIndex), blockType: 'reasoning' }; break
442
- case 'thinking_delta': yield { type: 'reasoning-delta', index: Number(event.contentIndex), text: String(event.delta ?? '') }; break
443
- case 'thinking_end': yield { type: 'block-end', index: Number(event.contentIndex), block: { type: 'reasoning', text: String(event.content ?? '') } }; break
444
- case 'toolcall_start': {
445
- const partial = /** @type {UnknownRecord | undefined} */ (isObject(event.partial) ? event.partial : undefined)
446
- const content = Array.isArray(partial?.content) ? partial.content : []
447
- const block = /** @type {UnknownRecord | undefined} */ (content[Number(event.contentIndex)])
448
- toolIds.set(Number(event.contentIndex), { id: String(block?.id ?? ''), name: String(block?.name ?? '') })
449
- yield { type: 'block-start', index: Number(event.contentIndex), blockType: 'tool-call' }
450
- break
451
- }
452
- case 'toolcall_delta': {
453
- const known = toolIds.get(Number(event.contentIndex)) ?? { id: '', name: '' }
454
- yield { type: 'tool-call-delta', index: Number(event.contentIndex), id: known.id, ...(known.name ? { name: known.name } : {}), argumentsDelta: String(event.delta ?? '') }
455
- break
456
- }
457
- case 'toolcall_end': {
458
- const call = /** @type {UnknownRecord} */ (isObject(event.toolCall) ? event.toolCall : {})
459
- yield { type: 'block-end', index: Number(event.contentIndex), block: { type: 'tool-call', id: String(call.id ?? ''), name: String(call.name ?? ''), arguments: rawArguments(call.arguments) } }
460
- break
461
- }
462
- case 'done': {
463
- const message = /** @type {UnknownRecord} */ (isObject(event.message) ? event.message : {})
464
- yield { type: 'usage', usage: dshUsage(message.usage) }
465
- const reason = successfulFinish(message)
466
- const replay = reason.kind === 'error' ? undefined : replayState(message)
467
- yield { type: 'finish', reason, ...(replay === undefined ? {} : { replayState: replay }) }
468
- return
469
- }
470
- case 'error': {
471
- const message = /** @type {UnknownRecord} */ (isObject(event.error) ? event.error : {})
472
- yield { type: 'usage', usage: dshUsage(message.usage) }
473
- const failure = /** @type {ManagedFailure} */ (isObject(message.__lcxFailure) ? message.__lcxFailure : managedFailure(Object.assign(new Error(String(message.errorMessage ?? 'Responses stream failed')), { code: message.stopReason === 'aborted' ? 'LCX_ABORTED' : undefined }), signal))
474
- yield { type: 'finish', reason: failure.code === 'ABORTED' ? { kind: 'aborted', failure } : { kind: 'error', failure } }
475
- return
476
- }
477
- default: break
1088
+ async function* toDshChunks(events, signal, wireMeta, nativeReplayRoute) {
1089
+ const toolIds = new Map();
1090
+ for await (const event of events) {
1091
+ switch (event.type) {
1092
+ case "start":
1093
+ break;
1094
+ case "text_start":
1095
+ yield {
1096
+ type: "block-start",
1097
+ index: Number(event.contentIndex),
1098
+ blockType: "text",
1099
+ };
1100
+ break;
1101
+ case "text_delta":
1102
+ yield {
1103
+ type: "text-delta",
1104
+ index: Number(event.contentIndex),
1105
+ text: String(event.delta ?? ""),
1106
+ };
1107
+ break;
1108
+ case "text_end":
1109
+ yield {
1110
+ type: "block-end",
1111
+ index: Number(event.contentIndex),
1112
+ block: { type: "text", text: String(event.content ?? "") },
1113
+ };
1114
+ break;
1115
+ case "thinking_start":
1116
+ yield {
1117
+ type: "block-start",
1118
+ index: Number(event.contentIndex),
1119
+ blockType: "reasoning",
1120
+ };
1121
+ break;
1122
+ case "thinking_delta":
1123
+ yield {
1124
+ type: "reasoning-delta",
1125
+ index: Number(event.contentIndex),
1126
+ text: String(event.delta ?? ""),
1127
+ };
1128
+ break;
1129
+ case "thinking_end":
1130
+ yield {
1131
+ type: "block-end",
1132
+ index: Number(event.contentIndex),
1133
+ block: { type: "reasoning", text: String(event.content ?? "") },
1134
+ };
1135
+ break;
1136
+ case "toolcall_start": {
1137
+ const partial = /** @type {UnknownRecord | undefined} */ isObject(event.partial)
1138
+ ? event.partial
1139
+ : undefined;
1140
+ const content = Array.isArray(partial?.content) ? partial.content : [];
1141
+ const item = content[Number(event.contentIndex)];
1142
+ const block = isObject(item) ? item : undefined;
1143
+ toolIds.set(Number(event.contentIndex), {
1144
+ id: String(block?.id ?? ""),
1145
+ name: String(block?.name ?? ""),
1146
+ });
1147
+ yield {
1148
+ type: "block-start",
1149
+ index: Number(event.contentIndex),
1150
+ blockType: "tool-call",
1151
+ };
1152
+ break;
1153
+ }
1154
+ case "toolcall_delta": {
1155
+ const known = toolIds.get(Number(event.contentIndex)) ?? {
1156
+ id: "",
1157
+ name: "",
1158
+ };
1159
+ yield {
1160
+ type: "tool-call-delta",
1161
+ index: Number(event.contentIndex),
1162
+ id: ToolCallId(known.id),
1163
+ ...(known.name ? { name: known.name } : {}),
1164
+ argumentsDelta: String(event.delta ?? ""),
1165
+ };
1166
+ break;
1167
+ }
1168
+ case "toolcall_end": {
1169
+ const call = isObject(event.toolCall)
1170
+ ? event.toolCall
1171
+ : {};
1172
+ yield {
1173
+ type: "block-end",
1174
+ index: Number(event.contentIndex),
1175
+ block: {
1176
+ type: "tool-call",
1177
+ id: ToolCallId(String(call.id ?? "")),
1178
+ name: String(call.name ?? ""),
1179
+ arguments: rawArguments(call.arguments),
1180
+ },
1181
+ };
1182
+ break;
1183
+ }
1184
+ case "done": {
1185
+ const message = isObject(event.message)
1186
+ ? event.message
1187
+ : {};
1188
+ yield { type: "usage", usage: dshUsage(message.usage) };
1189
+ const reason = successfulFinish(message);
1190
+ const replay = reason.kind === "error"
1191
+ ? undefined
1192
+ : replayState(message, wireMeta?.nativeOutput, nativeReplayRoute);
1193
+ yield {
1194
+ type: "finish",
1195
+ reason,
1196
+ ...(replay === undefined ? {} : { replayState: replay }),
1197
+ };
1198
+ return;
1199
+ }
1200
+ case "error": {
1201
+ const message = isObject(event.error)
1202
+ ? event.error
1203
+ : {};
1204
+ yield { type: "usage", usage: dshUsage(message.usage) };
1205
+ const failure = isManagedFailure(message.__lcxFailure)
1206
+ ? message.__lcxFailure
1207
+ : managedFailure(Object.assign(new Error(String(message.errorMessage ?? "Responses stream failed")), {
1208
+ code: message.stopReason === "aborted"
1209
+ ? "LCX_ABORTED"
1210
+ : undefined,
1211
+ }), signal);
1212
+ const normalizedFailure = {
1213
+ message: String(failure.message ?? "Responses stream failed"),
1214
+ code: String(failure.code ?? "RESPONSES_ERROR"),
1215
+ ...(typeof failure.status === "number" ? { status: failure.status } : {}),
1216
+ ...(typeof failure.providerRetryAfterMs === "number"
1217
+ ? { providerRetryAfterMs: failure.providerRetryAfterMs }
1218
+ : {}),
1219
+ };
1220
+ yield {
1221
+ type: "finish",
1222
+ reason: normalizedFailure.code === "ABORTED"
1223
+ ? { kind: "aborted", failure: normalizedFailure }
1224
+ : { kind: "error", failure: normalizedFailure },
1225
+ };
1226
+ return;
1227
+ }
1228
+ default:
1229
+ break;
1230
+ }
478
1231
  }
479
- }
480
- yield managedFailureChunk(Object.assign(new Error('Responses event stream ended without done/error'), { code: 'LCX_INVALID_SSE' }), signal)
1232
+ yield managedFailureChunk(Object.assign(new Error("Responses event stream ended without done/error"), { code: "LCX_INVALID_SSE" }), signal);
481
1233
  }
482
-
483
1234
  /**
484
1235
  * Send one LCX-owned OpenAI Responses request. Ordinary and replay use one provider attempt;
485
1236
  * the DSH agent recovery layer remains the visible retry owner.
@@ -495,45 +1246,96 @@ async function* toDshChunks(events, signal) {
495
1246
  * @param {number} [options.timeoutMs]
496
1247
  * @param {number} [options.maxAttempts]
497
1248
  * @param {number} [options.maxResponseBytes]
1249
+ * @param {ReadonlySet<string>} [options.serverToolTypes]
1250
+ * @param {(usage: ServerToolUsage) => void} [options.onServerToolUsage]
498
1251
  */
499
- export async function* streamResponsesRequest({ baseURL, provider, model, piModel, body, grammarToolInputProperties, headers, signal, timeoutMs, maxAttempts = 1, maxResponseBytes }) {
500
- try {
501
- const response = await fetchSseWithRetry(`${String(baseURL).replace(/\/+$/u, '')}/responses`, body, headers, signal, timeoutMs, { maxAttempts, maxResponseBytes })
502
- /** @type {import('@earendil-works/pi-ai').AssistantMessage & { __lcxFailure?: ManagedFailure }} */
503
- const output = {
504
- role: 'assistant', content: [], api: 'openai-responses', provider, model,
505
- usage: emptyUsage(), stopReason: 'pending', timestamp: Date.now(),
1252
+ export async function* streamResponsesRequest({ baseURL, provider, model, piModel, body, grammarToolInputProperties, headers, signal, timeoutMs, applyDefaultTimeout = true, streamIdleTimeoutMs, maxAttempts = 1, maxResponseBytes, serverToolTypes, nativeReplayRoute, onServerToolUsage, }) {
1253
+ const effectiveTimeoutMs = timeoutMs ?? (applyDefaultTimeout ? 300_000 : undefined);
1254
+ const deadline = requestDeadline(signal, effectiveTimeoutMs);
1255
+ const watchdog = streamIdleWatchdog(deadline.signal, streamIdleTimeoutMs);
1256
+ try {
1257
+ // Match DSH's idleWatchdog.next(): the first outstanding next includes
1258
+ // request dispatch and headers, and later intervals cover one DSH chunk.
1259
+ watchdog.arm();
1260
+ const response = await fetchSseWithRetry(`${String(baseURL).replace(/\/+$/u, "")}/responses`, body, headers, watchdog.signal, undefined, { maxAttempts, maxResponseBytes, applyDefaultTimeout: false });
1261
+ const output = {
1262
+ role: "assistant",
1263
+ content: [],
1264
+ api: "openai-responses",
1265
+ provider,
1266
+ model,
1267
+ usage: emptyUsage(),
1268
+ stopReason: "pending",
1269
+ timestamp: Date.now(),
1270
+ };
1271
+ const piEvents = createAssistantMessageEventStream();
1272
+ const wireMeta = {};
1273
+ const parser = (async () => {
1274
+ try {
1275
+ piEvents.push({ type: "start", partial: output });
1276
+ await processResponsesStream(validatedResponseEvents(normalizedResponseEvents(responseEvents(response, {
1277
+ signal: watchdog.signal,
1278
+ maxResponseBytes,
1279
+ }), wireMeta, { serverToolTypes, onServerToolUsage })), output, piEvents, piModel, { grammarToolInputProperties });
1280
+ if (typeof wireMeta.responseModel === "string" &&
1281
+ wireMeta.responseModel.length > 0)
1282
+ output.responseModel = wireMeta.responseModel;
1283
+ if (signal?.aborted)
1284
+ throw (signal.reason ??
1285
+ Object.assign(new Error("request aborted"), { code: "LCX_ABORTED" }));
1286
+ if (output.stopReason === "pending")
1287
+ throw Object.assign(new Error("Responses stream ended without a stop reason"), { code: "LCX_INVALID_SSE" });
1288
+ if (output.stopReason === "aborted" || output.stopReason === "error")
1289
+ throw new Error("Responses stream ended in failure");
1290
+ piEvents.push({
1291
+ type: "done",
1292
+ reason: output.stopReason,
1293
+ message: output,
1294
+ });
1295
+ piEvents.end();
1296
+ }
1297
+ catch (error) {
1298
+ output.stopReason = signal?.aborted ? "aborted" : "error";
1299
+ output.errorMessage =
1300
+ error instanceof Error ? error.message : "Responses stream failed";
1301
+ output.__lcxFailure = managedFailure(error, signal);
1302
+ piEvents.push({
1303
+ type: "error",
1304
+ reason: /** @type {'aborted' | 'error'} */ output.stopReason,
1305
+ error: output,
1306
+ });
1307
+ piEvents.end();
1308
+ }
1309
+ })();
1310
+ const chunks = toDshChunks(piEvents, signal, wireMeta, nativeReplayRoute)[Symbol.asyncIterator]();
1311
+ let exhausted = false;
1312
+ try {
1313
+ while (true) {
1314
+ const result = await chunks.next();
1315
+ if (watchdog.signal.aborted)
1316
+ throw watchdog.signal.reason;
1317
+ if (result.done) {
1318
+ exhausted = true;
1319
+ watchdog.disarm();
1320
+ break;
1321
+ }
1322
+ watchdog.disarm();
1323
+ yield result.value;
1324
+ watchdog.arm();
1325
+ }
1326
+ }
1327
+ finally {
1328
+ watchdog.disarm();
1329
+ if (!exhausted)
1330
+ await chunks.return?.(undefined);
1331
+ }
1332
+ await parser;
1333
+ }
1334
+ catch (error) {
1335
+ yield managedFailureChunk(error, signal);
1336
+ }
1337
+ finally {
1338
+ watchdog.dispose();
1339
+ deadline.dispose();
506
1340
  }
507
- const piEvents = new PiEventQueue()
508
- /** @type {{ responseModel?: string }} */
509
- const wireMeta = {}
510
- const parser = (async () => {
511
- try {
512
- piEvents.push({ type: 'start', partial: output })
513
- await processResponsesStream(
514
- /** @type {AsyncIterable<ResponseStreamEvent>} */ (/** @type {unknown} */ (normalizedResponseEvents(responseEvents(response, { signal, maxResponseBytes }), wireMeta))),
515
- output,
516
- /** @type {any} */ (piEvents),
517
- /** @type {any} */ (piModel),
518
- { grammarToolInputProperties },
519
- )
520
- if (typeof wireMeta.responseModel === 'string' && wireMeta.responseModel.length > 0) output.responseModel = wireMeta.responseModel
521
- if (signal?.aborted) throw signal.reason ?? Object.assign(new Error('request aborted'), { code: 'LCX_ABORTED' })
522
- if (output.stopReason === 'pending') throw Object.assign(new Error('Responses stream ended without a stop reason'), { code: 'LCX_INVALID_SSE' })
523
- if (output.stopReason === 'aborted' || output.stopReason === 'error') throw new Error('Responses stream ended in failure')
524
- piEvents.push({ type: 'done', reason: /** @type {'stop' | 'length' | 'toolUse' | 'deferred'} */ (output.stopReason), message: output })
525
- piEvents.end()
526
- } catch (error) {
527
- output.stopReason = signal?.aborted ? 'aborted' : 'error'
528
- output.errorMessage = error instanceof Error ? error.message : 'Responses stream failed'
529
- output.__lcxFailure = managedFailure(error, signal)
530
- piEvents.push({ type: 'error', reason: /** @type {'aborted' | 'error'} */ (output.stopReason), error: output })
531
- piEvents.end()
532
- }
533
- })()
534
- yield* toDshChunks(piEvents, signal)
535
- await parser
536
- } catch (error) {
537
- yield managedFailureChunk(error, signal)
538
- }
539
1341
  }