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