dsh-lcx-codex 0.4.3-pre.2 → 0.4.3-pre.3
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 +77 -109
- package/lib/client.js +40 -4
- package/lib/dsh-compat.js +8 -3
- package/lib/grok-native-search.js +317 -0
- package/lib/index.js +227 -71
- package/lib/responses-request.js +3 -0
- package/lib/responses-stream.js +248 -15
- package/lib/route.js +173 -5
- package/lib/transport.js +4 -2
- package/lib/types/dsh-compat.d.ts +1 -1
- package/lib/types/grok-native-search.d.ts +39 -0
- package/lib/types/responses-request.d.ts +1 -0
- package/lib/types/responses-stream.d.ts +14 -1
- package/lib/types/route.d.ts +22 -1
- package/lib/types/transport.d.ts +2 -1
- package/package.json +5 -1
package/lib/responses-stream.js
CHANGED
|
@@ -3,6 +3,7 @@ import { createAssistantMessageEventStream, } from "@earendil-works/pi-ai";
|
|
|
3
3
|
import { processResponsesStream } from "@earendil-works/pi-ai/api/openai-responses-shared";
|
|
4
4
|
import { ToolCallId } from "@deepseek-ai/dsh-llm";
|
|
5
5
|
import { fetchSseWithRetry } from "./transport.js";
|
|
6
|
+
import { createGrokNativeReplayEnvelope, } from "./grok-native-search.js";
|
|
6
7
|
function emptyUsage() {
|
|
7
8
|
return {
|
|
8
9
|
input: 0,
|
|
@@ -167,10 +168,84 @@ export function managedFailureChunk(error, signal) {
|
|
|
167
168
|
: { kind: "error", failure },
|
|
168
169
|
};
|
|
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
|
+
};
|
|
243
|
+
}
|
|
170
244
|
/**
|
|
171
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.
|
|
172
247
|
* @param {Response} response
|
|
173
|
-
* @param {
|
|
248
|
+
* @param {StreamOptions} [options]
|
|
174
249
|
*/
|
|
175
250
|
async function* responseEvents(response, options = {}) {
|
|
176
251
|
if (!response?.body)
|
|
@@ -204,7 +279,7 @@ async function* responseEvents(response, options = {}) {
|
|
|
204
279
|
if (options.signal?.aborted)
|
|
205
280
|
throw (options.signal.reason ??
|
|
206
281
|
Object.assign(new Error("request aborted"), { code: "LCX_ABORTED" }));
|
|
207
|
-
const { done, value } = await reader.
|
|
282
|
+
const { done, value } = await readWithSignal(reader, options.signal);
|
|
208
283
|
if (done)
|
|
209
284
|
break;
|
|
210
285
|
bytes += value.byteLength;
|
|
@@ -497,9 +572,55 @@ function recordMatches(record, item) {
|
|
|
497
572
|
* @param {AsyncIterable<unknown>} source
|
|
498
573
|
* @param {{ responseModel?: string }} [meta]
|
|
499
574
|
*/
|
|
500
|
-
async function* normalizedResponseEvents(source, meta = {}) {
|
|
575
|
+
async function* normalizedResponseEvents(source, meta = {}, options = {}) {
|
|
501
576
|
const open = new Map();
|
|
502
577
|
const completed = new Set();
|
|
578
|
+
const completedWireItems = new Map();
|
|
579
|
+
const serverToolIds = new Map();
|
|
580
|
+
const citations = new Map();
|
|
581
|
+
const observeCitation = (candidate) => {
|
|
582
|
+
if (!isObject(candidate))
|
|
583
|
+
return;
|
|
584
|
+
try {
|
|
585
|
+
const url = new URL(String(candidate.url ?? ""));
|
|
586
|
+
if (url.protocol !== "http:" && url.protocol !== "https:")
|
|
587
|
+
return;
|
|
588
|
+
const title = typeof candidate.title === "string"
|
|
589
|
+
? candidate.title.replace(/[\r\n]+/gu, " ").trim()
|
|
590
|
+
: "";
|
|
591
|
+
if (!citations.has(url.href))
|
|
592
|
+
citations.set(url.href, title);
|
|
593
|
+
}
|
|
594
|
+
catch { }
|
|
595
|
+
};
|
|
596
|
+
const observeCitations = (item) => {
|
|
597
|
+
if (!isObject(item))
|
|
598
|
+
return;
|
|
599
|
+
if (item.type === "message" && Array.isArray(item.content)) {
|
|
600
|
+
for (const part of item.content) {
|
|
601
|
+
if (!isObject(part) || part.type !== "output_text" || !Array.isArray(part.annotations))
|
|
602
|
+
continue;
|
|
603
|
+
for (const annotation of part.annotations)
|
|
604
|
+
if (isObject(annotation) && annotation.type === "url_citation")
|
|
605
|
+
observeCitation(annotation);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
if (item.type === "web_search_call" &&
|
|
609
|
+
isObject(item.action) &&
|
|
610
|
+
Array.isArray(item.action.sources))
|
|
611
|
+
for (const source of item.action.sources)
|
|
612
|
+
observeCitation(source);
|
|
613
|
+
};
|
|
614
|
+
const isServerToolItem = (value) => isObject(value) && options.serverToolTypes?.has(String(value.type ?? "")) === true;
|
|
615
|
+
const observeServerTool = (item) => {
|
|
616
|
+
const type = String(item.type);
|
|
617
|
+
let ids = serverToolIds.get(type);
|
|
618
|
+
if (!ids) {
|
|
619
|
+
ids = new Set();
|
|
620
|
+
serverToolIds.set(type, ids);
|
|
621
|
+
}
|
|
622
|
+
ids.add(typeof item.id === "string" && item.id ? item.id : `${type}:${ids.size}`);
|
|
623
|
+
};
|
|
503
624
|
/** @param {number} index @param {UnknownRecord} item */
|
|
504
625
|
const ensure = function* (index, item) {
|
|
505
626
|
let record = open.get(index);
|
|
@@ -522,6 +643,18 @@ async function* normalizedResponseEvents(source, meta = {}) {
|
|
|
522
643
|
const index = Number.isInteger(event.output_index)
|
|
523
644
|
? Number(event.output_index)
|
|
524
645
|
: 0;
|
|
646
|
+
if ((event.type === "response.output_item.added" ||
|
|
647
|
+
event.type === "response.output_item.done") &&
|
|
648
|
+
isObject(event.item))
|
|
649
|
+
observeCitations(event.item);
|
|
650
|
+
if (event.type === "response.output_item.done" && isObject(event.item))
|
|
651
|
+
completedWireItems.set(index, structuredClone(event.item));
|
|
652
|
+
if ((event.type === "response.output_item.added" ||
|
|
653
|
+
event.type === "response.output_item.done") &&
|
|
654
|
+
isServerToolItem(event.item)) {
|
|
655
|
+
observeServerTool(event.item);
|
|
656
|
+
continue;
|
|
657
|
+
}
|
|
525
658
|
if (event.type === "response.output_item.added" && isObject(event.item)) {
|
|
526
659
|
const item = normalizedTerminalItem(event.item, index);
|
|
527
660
|
open.set(index, { kind: itemKind(item), item, text: itemText(item) });
|
|
@@ -636,11 +769,57 @@ async function* normalizedResponseEvents(source, meta = {}) {
|
|
|
636
769
|
const response = /** @type {UnknownRecord} */ event.response;
|
|
637
770
|
if (typeof response.model === "string" && response.model.length > 0)
|
|
638
771
|
meta.responseModel = response.model;
|
|
639
|
-
const
|
|
640
|
-
? response.output
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
772
|
+
const terminalOutput = Array.isArray(response.output) && response.output.length > 0
|
|
773
|
+
? response.output
|
|
774
|
+
: [...completedWireItems.entries()]
|
|
775
|
+
.sort(([left], [right]) => left - right)
|
|
776
|
+
.map(([, item]) => item);
|
|
777
|
+
const nativeOutput = terminalOutput.map((item) => structuredClone(item));
|
|
778
|
+
const nativeVisibleAdditions = [];
|
|
779
|
+
for (const item of terminalOutput) {
|
|
780
|
+
observeCitations(item);
|
|
781
|
+
if (isServerToolItem(item))
|
|
782
|
+
observeServerTool(item);
|
|
783
|
+
}
|
|
784
|
+
const output = terminalOutput
|
|
785
|
+
.filter((item) => !isServerToolItem(item))
|
|
786
|
+
.map((item, terminalIndex) => isObject(item)
|
|
787
|
+
? normalizedTerminalItem(item, terminalIndex)
|
|
788
|
+
: item);
|
|
789
|
+
if (options.serverToolTypes && citations.size > 0) {
|
|
790
|
+
const visibleText = output
|
|
791
|
+
.filter(isObject)
|
|
792
|
+
.map((item) => itemText(item))
|
|
793
|
+
.join("\n");
|
|
794
|
+
const missing = [...citations].filter(([url]) => !visibleText.includes(url));
|
|
795
|
+
if (missing.length > 0) {
|
|
796
|
+
const suffix = missing
|
|
797
|
+
.map(([url, title]) => `- ${title ? `${title}: ` : ""}${url}`)
|
|
798
|
+
.join("\n");
|
|
799
|
+
const responseId = String(response.id ?? "native")
|
|
800
|
+
.replace(/[^A-Za-z0-9_-]/gu, "_")
|
|
801
|
+
.slice(0, 40);
|
|
802
|
+
const fallbackItem = {
|
|
803
|
+
type: "message",
|
|
804
|
+
id: `msg_lcx_sources_${responseId}`.slice(0, 64),
|
|
805
|
+
role: "assistant",
|
|
806
|
+
status: "completed",
|
|
807
|
+
content: [{
|
|
808
|
+
type: "output_text",
|
|
809
|
+
text: `\n\nSources:\n${suffix}`,
|
|
810
|
+
annotations: [],
|
|
811
|
+
}],
|
|
812
|
+
};
|
|
813
|
+
output.push(fallbackItem);
|
|
814
|
+
nativeVisibleAdditions.push(structuredClone(fallbackItem));
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
if (event.type === "response.completed" &&
|
|
818
|
+
response.status === "completed" &&
|
|
819
|
+
options.serverToolTypes) {
|
|
820
|
+
meta.nativeOutput = nativeOutput;
|
|
821
|
+
meta.nativeVisibleAdditions = nativeVisibleAdditions;
|
|
822
|
+
}
|
|
644
823
|
const used = new Set();
|
|
645
824
|
for (const [streamIndex, record] of open) {
|
|
646
825
|
let terminalIndex = output.findIndex((item, candidateIndex) => !used.has(candidateIndex) &&
|
|
@@ -725,6 +904,20 @@ async function* normalizedResponseEvents(source, meta = {}) {
|
|
|
725
904
|
if (identity)
|
|
726
905
|
completed.add(identity);
|
|
727
906
|
}
|
|
907
|
+
if (options.onServerToolUsage) {
|
|
908
|
+
const usage = isObject(response.usage) ? response.usage : {};
|
|
909
|
+
const details = isObject(usage.server_side_tool_usage_details)
|
|
910
|
+
? usage.server_side_tool_usage_details
|
|
911
|
+
: {};
|
|
912
|
+
const safeCount = (value, fallback) => Number.isSafeInteger(value) && Number(value) >= 0
|
|
913
|
+
? Number(value)
|
|
914
|
+
: fallback;
|
|
915
|
+
const webSearchCalls = safeCount(details.web_search_calls, serverToolIds.get("web_search_call")?.size ?? 0);
|
|
916
|
+
const xSearchCalls = safeCount(details.x_search_calls, serverToolIds.get("x_search_call")?.size ?? 0);
|
|
917
|
+
const total = safeCount(usage.num_server_side_tools_used, webSearchCalls + xSearchCalls);
|
|
918
|
+
if (total > 0 || webSearchCalls > 0 || xSearchCalls > 0)
|
|
919
|
+
options.onServerToolUsage({ webSearchCalls, xSearchCalls, total });
|
|
920
|
+
}
|
|
728
921
|
yield { ...event, response: { ...response, output } };
|
|
729
922
|
continue;
|
|
730
923
|
}
|
|
@@ -767,7 +960,7 @@ function rawArguments(value) {
|
|
|
767
960
|
}
|
|
768
961
|
}
|
|
769
962
|
/** @param {unknown} message */
|
|
770
|
-
function replayState(message) {
|
|
963
|
+
function replayState(message, nativeOutput, nativeVisibleAdditions, nativeReplayRoute) {
|
|
771
964
|
if (!isObject(message))
|
|
772
965
|
return undefined;
|
|
773
966
|
const content = Array.isArray(message.content)
|
|
@@ -778,6 +971,7 @@ function replayState(message) {
|
|
|
778
971
|
const api = typeof message.api === "string" ? message.api : undefined;
|
|
779
972
|
if (!provider || !model || !api)
|
|
780
973
|
return undefined;
|
|
974
|
+
const grokNative = createGrokNativeReplayEnvelope(nativeOutput, nativeReplayRoute, nativeVisibleAdditions);
|
|
781
975
|
return {
|
|
782
976
|
response: {
|
|
783
977
|
kind: "pi-ai",
|
|
@@ -821,6 +1015,7 @@ function replayState(message) {
|
|
|
821
1015
|
: {}),
|
|
822
1016
|
};
|
|
823
1017
|
}),
|
|
1018
|
+
...(grokNative === undefined ? {} : { grokNative }),
|
|
824
1019
|
};
|
|
825
1020
|
}
|
|
826
1021
|
/** @param {unknown} message */
|
|
@@ -850,7 +1045,7 @@ function successfulFinish(message) {
|
|
|
850
1045
|
* @param {AsyncIterable<AssistantMessageEvent>} events
|
|
851
1046
|
* @param {AbortSignal} [signal]
|
|
852
1047
|
*/
|
|
853
|
-
async function* toDshChunks(events, signal) {
|
|
1048
|
+
async function* toDshChunks(events, signal, wireMeta, nativeReplayRoute) {
|
|
854
1049
|
const toolIds = new Map();
|
|
855
1050
|
for await (const event of events) {
|
|
856
1051
|
switch (event.type) {
|
|
@@ -952,7 +1147,9 @@ async function* toDshChunks(events, signal) {
|
|
|
952
1147
|
: {};
|
|
953
1148
|
yield { type: "usage", usage: dshUsage(message.usage) };
|
|
954
1149
|
const reason = successfulFinish(message);
|
|
955
|
-
const replay = reason.kind === "error"
|
|
1150
|
+
const replay = reason.kind === "error"
|
|
1151
|
+
? undefined
|
|
1152
|
+
: replayState(message, wireMeta?.nativeOutput, wireMeta?.nativeVisibleAdditions, nativeReplayRoute);
|
|
956
1153
|
yield {
|
|
957
1154
|
type: "finish",
|
|
958
1155
|
reason,
|
|
@@ -1009,10 +1206,18 @@ async function* toDshChunks(events, signal) {
|
|
|
1009
1206
|
* @param {number} [options.timeoutMs]
|
|
1010
1207
|
* @param {number} [options.maxAttempts]
|
|
1011
1208
|
* @param {number} [options.maxResponseBytes]
|
|
1209
|
+
* @param {ReadonlySet<string>} [options.serverToolTypes]
|
|
1210
|
+
* @param {(usage: ServerToolUsage) => void} [options.onServerToolUsage]
|
|
1012
1211
|
*/
|
|
1013
|
-
export async function* streamResponsesRequest({ baseURL, provider, model, piModel, body, grammarToolInputProperties, headers, signal, timeoutMs, maxAttempts = 1, maxResponseBytes, }) {
|
|
1212
|
+
export async function* streamResponsesRequest({ baseURL, provider, model, piModel, body, grammarToolInputProperties, headers, signal, timeoutMs, applyDefaultTimeout = true, streamIdleTimeoutMs, maxAttempts = 1, maxResponseBytes, serverToolTypes, nativeReplayRoute, onServerToolUsage, }) {
|
|
1213
|
+
const effectiveTimeoutMs = timeoutMs ?? (applyDefaultTimeout ? 300_000 : undefined);
|
|
1214
|
+
const deadline = requestDeadline(signal, effectiveTimeoutMs);
|
|
1215
|
+
const watchdog = streamIdleWatchdog(deadline.signal, streamIdleTimeoutMs);
|
|
1014
1216
|
try {
|
|
1015
|
-
|
|
1217
|
+
// Match DSH's idleWatchdog.next(): the first outstanding next includes
|
|
1218
|
+
// request dispatch and headers, and later intervals cover one DSH chunk.
|
|
1219
|
+
watchdog.arm();
|
|
1220
|
+
const response = await fetchSseWithRetry(`${String(baseURL).replace(/\/+$/u, "")}/responses`, body, headers, watchdog.signal, undefined, { maxAttempts, maxResponseBytes, applyDefaultTimeout: false });
|
|
1016
1221
|
const output = {
|
|
1017
1222
|
role: "assistant",
|
|
1018
1223
|
content: [],
|
|
@@ -1028,7 +1233,10 @@ export async function* streamResponsesRequest({ baseURL, provider, model, piMode
|
|
|
1028
1233
|
const parser = (async () => {
|
|
1029
1234
|
try {
|
|
1030
1235
|
piEvents.push({ type: "start", partial: output });
|
|
1031
|
-
await processResponsesStream(validatedResponseEvents(normalizedResponseEvents(responseEvents(response, {
|
|
1236
|
+
await processResponsesStream(validatedResponseEvents(normalizedResponseEvents(responseEvents(response, {
|
|
1237
|
+
signal: watchdog.signal,
|
|
1238
|
+
maxResponseBytes,
|
|
1239
|
+
}), wireMeta, { serverToolTypes, onServerToolUsage })), output, piEvents, piModel, { grammarToolInputProperties });
|
|
1032
1240
|
if (typeof wireMeta.responseModel === "string" &&
|
|
1033
1241
|
wireMeta.responseModel.length > 0)
|
|
1034
1242
|
output.responseModel = wireMeta.responseModel;
|
|
@@ -1059,10 +1267,35 @@ export async function* streamResponsesRequest({ baseURL, provider, model, piMode
|
|
|
1059
1267
|
piEvents.end();
|
|
1060
1268
|
}
|
|
1061
1269
|
})();
|
|
1062
|
-
|
|
1270
|
+
const chunks = toDshChunks(piEvents, signal, wireMeta, nativeReplayRoute)[Symbol.asyncIterator]();
|
|
1271
|
+
let exhausted = false;
|
|
1272
|
+
try {
|
|
1273
|
+
while (true) {
|
|
1274
|
+
const result = await chunks.next();
|
|
1275
|
+
if (watchdog.signal.aborted)
|
|
1276
|
+
throw watchdog.signal.reason;
|
|
1277
|
+
if (result.done) {
|
|
1278
|
+
exhausted = true;
|
|
1279
|
+
watchdog.disarm();
|
|
1280
|
+
break;
|
|
1281
|
+
}
|
|
1282
|
+
watchdog.disarm();
|
|
1283
|
+
yield result.value;
|
|
1284
|
+
watchdog.arm();
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
finally {
|
|
1288
|
+
watchdog.disarm();
|
|
1289
|
+
if (!exhausted)
|
|
1290
|
+
await chunks.return?.(undefined);
|
|
1291
|
+
}
|
|
1063
1292
|
await parser;
|
|
1064
1293
|
}
|
|
1065
1294
|
catch (error) {
|
|
1066
1295
|
yield managedFailureChunk(error, signal);
|
|
1067
1296
|
}
|
|
1297
|
+
finally {
|
|
1298
|
+
watchdog.dispose();
|
|
1299
|
+
deadline.dispose();
|
|
1300
|
+
}
|
|
1068
1301
|
}
|
package/lib/route.js
CHANGED
|
@@ -12,6 +12,9 @@ function isRecord(value) {
|
|
|
12
12
|
function isPositiveInteger(value) {
|
|
13
13
|
return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
|
|
14
14
|
}
|
|
15
|
+
function isPositiveFinite(value) {
|
|
16
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0;
|
|
17
|
+
}
|
|
15
18
|
function asLlmSettingsSection(value) {
|
|
16
19
|
if (!isRecord(value))
|
|
17
20
|
return undefined;
|
|
@@ -72,6 +75,13 @@ export function promptCacheSessionId(route, config = {}, ctx = undefined) {
|
|
|
72
75
|
export function promptCacheKey(route, config = {}, ctx = undefined) {
|
|
73
76
|
return clampPromptCacheKey(promptCacheSessionId(route, config, ctx));
|
|
74
77
|
}
|
|
78
|
+
/** Grok follows Pi directly: cache/affinity belongs to the selected child session. */
|
|
79
|
+
export function grokPromptCacheSessionId(route, config = {}) {
|
|
80
|
+
if (config.cacheRetention === "none")
|
|
81
|
+
return undefined;
|
|
82
|
+
const sessionId = String(route?.sessionId ?? "");
|
|
83
|
+
return sessionId || undefined;
|
|
84
|
+
}
|
|
75
85
|
/** @param {Partial<Pick<ResolvedResponsesRoute, 'cacheRetention' | 'supportsLongCacheRetention' | 'responsesCompat'>>} [config] */
|
|
76
86
|
export function promptCacheRetention(config = {}) {
|
|
77
87
|
return config.cacheRetention === "long" && config.supportsLongCacheRetention === true && config.responsesCompat?.supportsExplicitPromptCacheMode !== true ? "24h" : undefined;
|
|
@@ -116,17 +126,68 @@ function builtinResponsesModel(provider, modelId) {
|
|
|
116
126
|
return undefined;
|
|
117
127
|
return getBuiltinModels(builtinProvider).find((model) => model.id === String(modelId ?? "") && model.api === "openai-responses");
|
|
118
128
|
}
|
|
129
|
+
function modelDescriptorDefaults(model) {
|
|
130
|
+
if (!model)
|
|
131
|
+
return undefined;
|
|
132
|
+
return {
|
|
133
|
+
reasoning: model.reasoning,
|
|
134
|
+
thinkingLevelMap: model.thinkingLevelMap,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
function configuredModelProfile(profile, modelId) {
|
|
138
|
+
const configuredModels = Array.isArray(profile?.models) ? profile.models : [];
|
|
139
|
+
return configuredModels.length > 0
|
|
140
|
+
? configuredModels.find((entry) => String(entry?.id ?? "") === String(modelId))
|
|
141
|
+
: profile?.modelOverrides?.[String(modelId)];
|
|
142
|
+
}
|
|
119
143
|
/** @param {ProviderProfile | null | undefined} profile @param {unknown} modelId @returns {ResponsesCompat | undefined} */
|
|
120
144
|
function configuredResponsesCompat(profile, modelId) {
|
|
121
145
|
const compat = {};
|
|
122
146
|
copyResponsesCompat(compat, profile?.compat);
|
|
123
|
-
|
|
124
|
-
const modelEntry = configuredModels.length > 0
|
|
125
|
-
? configuredModels.find((entry) => String(entry?.id ?? "") === String(modelId))
|
|
126
|
-
: profile?.modelOverrides?.[String(modelId)];
|
|
127
|
-
copyResponsesCompat(compat, modelEntry?.compat);
|
|
147
|
+
copyResponsesCompat(compat, configuredModelProfile(profile, modelId)?.compat);
|
|
128
148
|
return Object.keys(compat).length ? compat : undefined;
|
|
129
149
|
}
|
|
150
|
+
const MODEL_THINKING_LEVELS = [
|
|
151
|
+
"off",
|
|
152
|
+
"minimal",
|
|
153
|
+
"low",
|
|
154
|
+
"medium",
|
|
155
|
+
"high",
|
|
156
|
+
"xhigh",
|
|
157
|
+
"max",
|
|
158
|
+
];
|
|
159
|
+
function profileReasoning(value) {
|
|
160
|
+
return MODEL_THINKING_LEVELS.find((level) => level === value);
|
|
161
|
+
}
|
|
162
|
+
function configuredModelControls(profile, modelId, base) {
|
|
163
|
+
const configured = configuredModelProfile(profile, modelId);
|
|
164
|
+
const efforts = configured?.reasoningEfforts;
|
|
165
|
+
if (efforts === undefined)
|
|
166
|
+
return {
|
|
167
|
+
reasoning: base?.reasoning ?? false,
|
|
168
|
+
...(base?.thinkingLevelMap === undefined
|
|
169
|
+
? {}
|
|
170
|
+
: { thinkingLevelMap: base.thinkingLevelMap }),
|
|
171
|
+
};
|
|
172
|
+
if (efforts === false)
|
|
173
|
+
return { reasoning: false };
|
|
174
|
+
if (!isRecord(efforts))
|
|
175
|
+
return { reasoning: false };
|
|
176
|
+
const thinkingLevelMap = {};
|
|
177
|
+
for (const level of MODEL_THINKING_LEVELS) {
|
|
178
|
+
const value = efforts[level];
|
|
179
|
+
if (value === undefined)
|
|
180
|
+
thinkingLevelMap[level] = null;
|
|
181
|
+
else if (value === null) {
|
|
182
|
+
// DSH permits only off:null: it means supported-off by omitting the wire option.
|
|
183
|
+
if (level !== "off")
|
|
184
|
+
thinkingLevelMap[level] = null;
|
|
185
|
+
}
|
|
186
|
+
else if (typeof value === "string")
|
|
187
|
+
thinkingLevelMap[level] = value;
|
|
188
|
+
}
|
|
189
|
+
return { reasoning: true, thinkingLevelMap };
|
|
190
|
+
}
|
|
130
191
|
function isLcxCapabilityRoute(provider, model) {
|
|
131
192
|
return provider === "lcx" &&
|
|
132
193
|
/^gpt-5\.6-(?:sol|luna|terra)$/iu.test(model);
|
|
@@ -189,6 +250,74 @@ export function resolveResponsesRouteConfig(ctx, options, policy) {
|
|
|
189
250
|
requestImageMaxBytes: isPositiveInteger(configured.requestImageMaxBytes) ? configured.requestImageMaxBytes : policy.requestImageMaxBytes,
|
|
190
251
|
};
|
|
191
252
|
}
|
|
253
|
+
/** Resolve a Grok-prefixed model through its selected DSH Responses profile. */
|
|
254
|
+
export function resolveGrokResponsesRouteConfig(ctx, options, policy) {
|
|
255
|
+
const provider = String(options?.provider ?? "");
|
|
256
|
+
const model = String(options?.model ?? "");
|
|
257
|
+
if (!provider.trim() || !/^grok/iu.test(model))
|
|
258
|
+
return undefined;
|
|
259
|
+
const section = settingsValue(ctx, "llm-pi-ai");
|
|
260
|
+
const configured = section?.providers?.[provider];
|
|
261
|
+
if (!isRecord(configured))
|
|
262
|
+
return undefined;
|
|
263
|
+
const selectedBuiltin = builtinResponsesModel(provider, model);
|
|
264
|
+
const api = configured.api ?? selectedBuiltin?.api;
|
|
265
|
+
if (api !== "openai-responses")
|
|
266
|
+
return undefined;
|
|
267
|
+
const baseURL = configured.baseURL ?? selectedBuiltin?.baseUrl;
|
|
268
|
+
const apiKeyEnv = configured.apiKeyEnv;
|
|
269
|
+
if (typeof baseURL !== "string" ||
|
|
270
|
+
!normalizeBaseURL(baseURL) ||
|
|
271
|
+
typeof apiKeyEnv !== "string" ||
|
|
272
|
+
!apiKeyEnv.trim())
|
|
273
|
+
return undefined;
|
|
274
|
+
const responsesCompat = {};
|
|
275
|
+
copyResponsesCompat(responsesCompat, selectedBuiltin?.compat);
|
|
276
|
+
copyResponsesCompat(responsesCompat, configuredResponsesCompat(configured, model));
|
|
277
|
+
const supportsLongCacheRetention = responsesCompat.supportsLongCacheRetention === true;
|
|
278
|
+
responsesCompat.supportsLongCacheRetention = supportsLongCacheRetention;
|
|
279
|
+
const requestedRetention = configured.cacheRetention;
|
|
280
|
+
const cacheRetention = requestedRetention === "none"
|
|
281
|
+
? "none"
|
|
282
|
+
: requestedRetention === "long" && supportsLongCacheRetention
|
|
283
|
+
? "long"
|
|
284
|
+
: "short";
|
|
285
|
+
return {
|
|
286
|
+
provider,
|
|
287
|
+
model,
|
|
288
|
+
api: "openai-responses",
|
|
289
|
+
baseURL: normalizeBaseURL(baseURL),
|
|
290
|
+
apiKeyEnv,
|
|
291
|
+
headers: effectiveGrokProfileHeaders(configured.headers),
|
|
292
|
+
cacheRetention,
|
|
293
|
+
supportsLongCacheRetention,
|
|
294
|
+
responsesCompat,
|
|
295
|
+
modelDefaults: modelDescriptorDefaults(selectedBuiltin),
|
|
296
|
+
modelControls: {
|
|
297
|
+
...configuredModelControls(configured, model, selectedBuiltin),
|
|
298
|
+
includeEncryptedReasoning: true,
|
|
299
|
+
},
|
|
300
|
+
profileReasoning: profileReasoning(configured.reasoning),
|
|
301
|
+
// Pi applies only an explicitly configured request deadline; stream liveness
|
|
302
|
+
// is owned separately by the provider idle watchdog.
|
|
303
|
+
timeoutMs: isPositiveInteger(configured.timeoutMs)
|
|
304
|
+
? configured.timeoutMs
|
|
305
|
+
: undefined,
|
|
306
|
+
streamIdleTimeoutMs: isPositiveFinite(configured.streamIdleTimeoutMs)
|
|
307
|
+
? configured.streamIdleTimeoutMs
|
|
308
|
+
: 300_000,
|
|
309
|
+
maxAttempts: retryAttempts(configured.retryPolicy, policy.maxAttempts),
|
|
310
|
+
maxRequestImageBytes: isPositiveInteger(configured.maxRequestImageBytes)
|
|
311
|
+
? configured.maxRequestImageBytes
|
|
312
|
+
: policy.maxRequestImageBytes,
|
|
313
|
+
requestImagePixelBudget: isPositiveInteger(configured.requestImagePixelBudget)
|
|
314
|
+
? configured.requestImagePixelBudget
|
|
315
|
+
: policy.requestImagePixelBudget,
|
|
316
|
+
requestImageMaxBytes: isPositiveInteger(configured.requestImageMaxBytes)
|
|
317
|
+
? configured.requestImageMaxBytes
|
|
318
|
+
: policy.requestImageMaxBytes,
|
|
319
|
+
};
|
|
320
|
+
}
|
|
192
321
|
/** @param {RouteContext | null | undefined} ctx @param {Pick<ResolvedResponsesRoute, 'apiKeyEnv'>} config */
|
|
193
322
|
export async function resolveApiKey(ctx, config) {
|
|
194
323
|
const resolved = await ctx?.credentials.resolve(credentialRef(config.apiKeyEnv));
|
|
@@ -201,6 +330,23 @@ export async function resolveApiKey(ctx, config) {
|
|
|
201
330
|
error.code = "LCX_CREDENTIAL_UNAVAILABLE";
|
|
202
331
|
throw error;
|
|
203
332
|
}
|
|
333
|
+
function setHeaderCaseInsensitive(headers, name, value) {
|
|
334
|
+
const normalized = name.toLowerCase();
|
|
335
|
+
for (const existing of Object.keys(headers))
|
|
336
|
+
if (existing.toLowerCase() === normalized)
|
|
337
|
+
delete headers[existing];
|
|
338
|
+
headers[name] = value;
|
|
339
|
+
}
|
|
340
|
+
/** Apply DSH's case-insensitive attribution reservation at profile resolution. */
|
|
341
|
+
function effectiveGrokProfileHeaders(configured) {
|
|
342
|
+
const attribution = attributionHeaders();
|
|
343
|
+
const reserved = new Set(Object.keys(attribution).map((name) => name.toLowerCase()));
|
|
344
|
+
const effective = {};
|
|
345
|
+
for (const [name, value] of Object.entries(configured ?? {}))
|
|
346
|
+
if (!reserved.has(name.toLowerCase()))
|
|
347
|
+
setHeaderCaseInsensitive(effective, name, value);
|
|
348
|
+
return effective;
|
|
349
|
+
}
|
|
204
350
|
/** @param {HeaderMap | null | undefined} headers @param {string} name */
|
|
205
351
|
function hasHeader(headers, name) { return Object.keys(headers ?? {}).some((key) => key.toLowerCase() === name.toLowerCase()); }
|
|
206
352
|
/** @param {HeaderMap | null | undefined} headers */
|
|
@@ -239,6 +385,28 @@ export async function authenticatedHeaders(ctx, config, sessionId, requestId) {
|
|
|
239
385
|
}
|
|
240
386
|
return { ...headers, ...explicit };
|
|
241
387
|
}
|
|
388
|
+
/** Grok mirrors Pi createClient ordering without changing accepted GPT headers. */
|
|
389
|
+
export async function authenticatedGrokHeaders(ctx, config, sessionId) {
|
|
390
|
+
const explicit = effectiveGrokProfileHeaders(config.headers);
|
|
391
|
+
const headers = {
|
|
392
|
+
authorization: `Bearer ${await resolveApiKey(ctx, config)}`,
|
|
393
|
+
};
|
|
394
|
+
const sid = sessionId ? String(sessionId) : undefined;
|
|
395
|
+
const format = sessionAffinityFormat(config);
|
|
396
|
+
if (sid) {
|
|
397
|
+
if (format === "openai")
|
|
398
|
+
headers.session_id = sid;
|
|
399
|
+
else if (format === "openrouter")
|
|
400
|
+
headers["x-session-id"] = sid;
|
|
401
|
+
if (format === "openai" || format === "openai-nosession")
|
|
402
|
+
headers["x-client-request-id"] = sid;
|
|
403
|
+
}
|
|
404
|
+
for (const [name, value] of Object.entries(explicit))
|
|
405
|
+
setHeaderCaseInsensitive(headers, name, value);
|
|
406
|
+
for (const [name, value] of Object.entries(attributionHeaders()))
|
|
407
|
+
setHeaderCaseInsensitive(headers, name, value);
|
|
408
|
+
return headers;
|
|
409
|
+
}
|
|
242
410
|
/** @param {RouteOptions | null | undefined} options @param {Pick<UnresolvedRouteConfig, 'provider' | 'model' | 'baseURL'>} config @returns {RouteIdentity} */
|
|
243
411
|
export function currentRoute(options, config) {
|
|
244
412
|
return { provider: String(options?.provider ?? config.provider ?? ""), model: String(options?.model ?? config.model ?? ""), baseURL: normalizeBaseURL(config.baseURL), sessionId: String(options?.sessionId ?? "") };
|
package/lib/transport.js
CHANGED
|
@@ -168,13 +168,15 @@ export async function fetchJsonWithRetry(url, body, headers = {}, signal, timeou
|
|
|
168
168
|
}
|
|
169
169
|
throw last ?? makeError("request failed", "LCX_REQUEST_FAILED");
|
|
170
170
|
}
|
|
171
|
-
export async function fetchSseWithRetry(url, body, headers = {}, signal, timeoutMs
|
|
171
|
+
export async function fetchSseWithRetry(url, body, headers = {}, signal, timeoutMs, options = {}) {
|
|
172
|
+
const requestTimeoutMs = timeoutMs ??
|
|
173
|
+
(options.applyDefaultTimeout === false ? undefined : DEFAULT_TIMEOUT_MS);
|
|
172
174
|
const maxAttempts = Math.max(1, Math.min(6, Number(options.maxAttempts ?? 3)));
|
|
173
175
|
let last;
|
|
174
176
|
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
175
177
|
abortIfNeeded(signal);
|
|
176
178
|
try {
|
|
177
|
-
const requestSignal = combinedSignal(signal,
|
|
179
|
+
const requestSignal = combinedSignal(signal, requestTimeoutMs);
|
|
178
180
|
const response = await fetch(url, {
|
|
179
181
|
method: "POST",
|
|
180
182
|
headers: {
|
|
@@ -40,7 +40,7 @@ export declare function readAgentRouteState(agent: unknown): {
|
|
|
40
40
|
options: unknown;
|
|
41
41
|
sessionId: string;
|
|
42
42
|
};
|
|
43
|
-
export declare function scopedToolRuntime(agent: unknown): Pick<ToolRuntime, "register"> | undefined;
|
|
43
|
+
export declare function scopedToolRuntime(agent: unknown): (Pick<ToolRuntime, "register"> & Partial<Pick<ToolRuntime, "get">>) | undefined;
|
|
44
44
|
export declare function tokenMeterTotal(value: unknown, session: Session): number | undefined;
|
|
45
45
|
export declare function sessionsService(ctx: unknown): Pick<SessionStore, "get"> | undefined;
|
|
46
46
|
export declare function sessionFor(ctx: unknown, sessionId: string): Session | undefined;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { Message, ToolSchema } from "@deepseek-ai/dsh-llm";
|
|
2
|
+
export declare const GROK_NATIVE_SERVER_TOOL_TYPES: Set<string>;
|
|
3
|
+
export type GrokNativeSearchState = {
|
|
4
|
+
web: boolean;
|
|
5
|
+
x: boolean;
|
|
6
|
+
};
|
|
7
|
+
export type GrokNativeReplayRoute = {
|
|
8
|
+
provider: string;
|
|
9
|
+
model: string;
|
|
10
|
+
sessionId: string;
|
|
11
|
+
baseURL: string;
|
|
12
|
+
apiKeyEnv: string;
|
|
13
|
+
headers?: Record<string, string>;
|
|
14
|
+
};
|
|
15
|
+
type WireTool = Record<string, unknown>;
|
|
16
|
+
type WireItem = Record<string, unknown> & {
|
|
17
|
+
type: string;
|
|
18
|
+
};
|
|
19
|
+
type GrokNativeVisibleAddition = {
|
|
20
|
+
kind: "citation-sources";
|
|
21
|
+
textSha256: string;
|
|
22
|
+
};
|
|
23
|
+
type GrokNativeReplayEnvelope = {
|
|
24
|
+
kind: "xai-responses-native-search";
|
|
25
|
+
version: 2;
|
|
26
|
+
provider: string;
|
|
27
|
+
model: string;
|
|
28
|
+
sourceSessionId: string;
|
|
29
|
+
routeAuthorityFingerprint: string;
|
|
30
|
+
output: WireItem[];
|
|
31
|
+
visibleAdditions: GrokNativeVisibleAddition[];
|
|
32
|
+
};
|
|
33
|
+
export declare function grokNativeReplayRouteFingerprint(route: GrokNativeReplayRoute): string;
|
|
34
|
+
export declare function grokNativeSearchEnabled(state: GrokNativeSearchState): boolean;
|
|
35
|
+
export declare function grokVisibleFunctionTools(tools: readonly ToolSchema[] | undefined, state?: GrokNativeSearchState): ToolSchema[] | undefined;
|
|
36
|
+
export declare function grokWireTools(tools: readonly unknown[] | undefined, state: GrokNativeSearchState): WireTool[];
|
|
37
|
+
export declare function createGrokNativeReplayEnvelope(output: readonly unknown[] | undefined, route: GrokNativeReplayRoute | undefined, visibleAdditions?: readonly unknown[]): GrokNativeReplayEnvelope | undefined;
|
|
38
|
+
export declare function restoreGrokNativeReplay(input: readonly unknown[], messages: readonly Message[], route: GrokNativeReplayRoute | undefined): unknown[];
|
|
39
|
+
export {};
|