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.
@@ -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 {{ signal?: AbortSignal, maxResponseBytes?: number }} [options]
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.read();
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 output = Array.isArray(response.output)
640
- ? response.output.map((item, terminalIndex) => isObject(item)
641
- ? normalizedTerminalItem(item, terminalIndex)
642
- : item)
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" ? undefined : replayState(message);
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
- const response = await fetchSseWithRetry(`${String(baseURL).replace(/\/+$/u, "")}/responses`, body, headers, signal, timeoutMs, { maxAttempts, maxResponseBytes });
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, { signal, maxResponseBytes }), wireMeta)), output, piEvents, piModel, { grammarToolInputProperties });
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
- yield* toDshChunks(piEvents, signal);
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
- const configuredModels = Array.isArray(profile?.models) ? profile.models : [];
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 = DEFAULT_TIMEOUT_MS, options = {}) {
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, timeoutMs);
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 {};
@@ -11,6 +11,7 @@ type PiResponsesModel = {
11
11
  provider: string;
12
12
  reasoning?: boolean;
13
13
  thinkingLevelMap?: Record<string, string | null>;
14
+ includeEncryptedReasoning?: boolean;
14
15
  compat?: ResponsesCompat;
15
16
  };
16
17
  type GenerationControls = {