dsh-lcx-codex 0.4.3 → 0.5.0-pre.1
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 +95 -60
- package/lib/abort-error.js +47 -0
- package/lib/client.js +597 -505
- package/lib/dsh-compat.js +37 -0
- package/lib/dsh-responses.js +92 -48
- package/lib/index.js +174 -105
- package/lib/invocation-policy-scope.js +4 -8
- package/lib/native-checkpoint.js +172 -80
- package/lib/pi-responses-runtime.js +252 -98
- package/lib/pi086-responses-allowlist.js +131 -0
- package/lib/responses-request.js +2 -2
- package/lib/responses-stream.js +62 -11
- package/lib/route.js +23 -32
- package/lib/search-accounting.js +22 -6
- package/lib/search-usage.js +32 -18
- package/lib/service-mutex.js +4 -8
- package/lib/token-budget.js +15 -21
- package/lib/transport.js +6 -6
- package/lib/types/client/search-media.d.ts +11 -2
- package/lib/types/index.d.ts +71 -43
- package/lib/web-search-alpha-history.js +62 -0
- package/lib/web-search-alpha.js +141 -124
- package/lib/web-search-capability.js +1 -1
- package/lib/web-search-hosted.js +29 -17
- package/lib/web-search-ref-store.js +27 -5
- package/package.json +41 -27
package/lib/dsh-compat.js
CHANGED
|
@@ -19,6 +19,27 @@ export function sessionFromAgent(agent) {
|
|
|
19
19
|
const session = agentLike(agent)?.session;
|
|
20
20
|
return session instanceof Session ? session : undefined;
|
|
21
21
|
}
|
|
22
|
+
// Event-driven next-route cache for live Agents; no Session log reads or polling.
|
|
23
|
+
const agentRouteStates = new WeakMap();
|
|
24
|
+
export function recordAgentRouteEvent(session, event) {
|
|
25
|
+
if (!(session instanceof Session))
|
|
26
|
+
return;
|
|
27
|
+
if (event.type !== "model/selection" && event.type !== "request/header")
|
|
28
|
+
return;
|
|
29
|
+
const previous = agentRouteStates.get(session);
|
|
30
|
+
let config = previous?.config;
|
|
31
|
+
let pending = previous?.pending;
|
|
32
|
+
if (event.type === "model/selection")
|
|
33
|
+
pending = event.data;
|
|
34
|
+
else {
|
|
35
|
+
config = event.data.header.config;
|
|
36
|
+
// An in-flight old-route request must not consume a newer selection.
|
|
37
|
+
if (pending?.provider === event.data.header.config.provider &&
|
|
38
|
+
pending.model === event.data.header.config.model)
|
|
39
|
+
pending = undefined;
|
|
40
|
+
}
|
|
41
|
+
agentRouteStates.set(session, { config, pending });
|
|
42
|
+
}
|
|
22
43
|
export function readAgentRouteState(agent) {
|
|
23
44
|
const value = agentLike(agent);
|
|
24
45
|
return {
|
|
@@ -27,6 +48,22 @@ export function readAgentRouteState(agent) {
|
|
|
27
48
|
sessionId: agentSessionId(value),
|
|
28
49
|
};
|
|
29
50
|
}
|
|
51
|
+
/** Selected next-request route, for tool advertisement before header persistence. */
|
|
52
|
+
export function readSelectedAgentRouteState(agent) {
|
|
53
|
+
const route = readAgentRouteState(agent);
|
|
54
|
+
const session = sessionFromAgent(agent);
|
|
55
|
+
if (!session)
|
|
56
|
+
return route;
|
|
57
|
+
// Cold/resumed Agents recover pending selection from DSH's public projection.
|
|
58
|
+
const projections = resolveScopedService(agent, "sessionProjections");
|
|
59
|
+
if (mutableService(projections) && typeof projections.stateOf === "function") {
|
|
60
|
+
const selection = Reflect.apply(projections.stateOf, projections, [session, "modelSelection"]);
|
|
61
|
+
if (mutableService(selection))
|
|
62
|
+
return { ...route, requestConfig: selection.pending ?? route.requestConfig };
|
|
63
|
+
}
|
|
64
|
+
const cached = agentRouteStates.get(session);
|
|
65
|
+
return cached ? { ...route, requestConfig: cached.pending ?? cached.config } : route;
|
|
66
|
+
}
|
|
30
67
|
export function scopedToolRuntime(agent) {
|
|
31
68
|
const tools = resolveScopedService(agent, "tools");
|
|
32
69
|
if (!mutableService(tools) || typeof tools.register !== "function")
|
package/lib/dsh-responses.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { offloadedImageText,
|
|
2
|
-
import {
|
|
1
|
+
import { IMAGE_OFFLOAD_REQUIRED_CODE, LlmError, offloadedImageText, projectOffloadedImages, requiredImageOffload, requestImageHandleText, resolveImageAttachmentAccess, } from "@deepseek-ai/dsh-llm";
|
|
2
|
+
import { requestImageDimensions } from "@deepseek-ai/dsh-attachment";
|
|
3
|
+
import { convertResponsesMessages, convertResponsesTools, createGrammarToolInputProperties, getBuiltinModels, normalizeContext, } from "./pi-responses-runtime.js";
|
|
4
|
+
import { wasPi086ResponsesModel } from "./pi086-responses-allowlist.js";
|
|
3
5
|
const DEFAULT_MAX_REQUEST_IMAGE_BYTES = 20 * 1024 * 1024;
|
|
4
6
|
const DEFAULT_REQUEST_IMAGE_PIXEL_BUDGET = 2048 * 2048;
|
|
5
7
|
const DEFAULT_REQUEST_IMAGE_MAX_BYTES = 1024 * 1024;
|
|
@@ -87,42 +89,45 @@ export async function resolveModelImageSupport(ctx, route, signal) {
|
|
|
87
89
|
return "unknown";
|
|
88
90
|
}
|
|
89
91
|
}
|
|
90
|
-
function attachmentResolver(ctx, options) {
|
|
92
|
+
function attachmentResolver(ctx, options, requestImages) {
|
|
91
93
|
const attachments = attachmentServices(ctx?.attachments);
|
|
92
94
|
return async (block, signal) => {
|
|
93
95
|
if (attachments === undefined)
|
|
94
96
|
throw error("LCX requires the DSH request-image attachment API", "LCX_COMPACT_IMAGE_API_UNAVAILABLE");
|
|
95
|
-
const request =
|
|
97
|
+
const request = requestImages === undefined
|
|
98
|
+
? await attachments.store.readImageRequest(block.attachment, { ...requestImageDimensions(block.attachment.width, block.attachment.height, options.requestImagePixelBudget), maxBytes: options.requestImageMaxBytes }, signal)
|
|
99
|
+
: requestImages.get(String(block.attachment.attachmentId));
|
|
100
|
+
if (request === undefined)
|
|
101
|
+
throw error("DSH attachment returned no request-image bytes", "LCX_COMPACT_IMAGE_UNAVAILABLE");
|
|
96
102
|
const data = request.data;
|
|
97
103
|
const mediaType = request.mediaType ?? block.attachment.mediaType;
|
|
98
104
|
if (!(data instanceof Uint8Array) && !Buffer.isBuffer(data))
|
|
99
105
|
throw error("DSH attachment returned no request-image bytes", "LCX_COMPACT_IMAGE_UNAVAILABLE");
|
|
100
106
|
if (typeof mediaType !== "string" || !mediaType.startsWith("image/"))
|
|
101
107
|
throw error("DSH attachment returned an invalid request-image media type", "LCX_COMPACT_IMAGE_UNAVAILABLE");
|
|
102
|
-
const ref = request.attachment ?? block.attachment;
|
|
103
108
|
return {
|
|
104
109
|
data: Buffer.from(data),
|
|
105
110
|
mediaType,
|
|
106
|
-
ref,
|
|
111
|
+
ref: block.attachment,
|
|
107
112
|
request,
|
|
108
|
-
access: imageAccess(ctx, attachments,
|
|
113
|
+
access: imageAccess(ctx, attachments, block.attachment),
|
|
109
114
|
};
|
|
110
115
|
};
|
|
111
116
|
}
|
|
112
|
-
async function imagePart(block, ctx, options, imageMap) {
|
|
117
|
+
async function imagePart(block, ctx, options, imageMap, requestImages) {
|
|
113
118
|
if (options.imageSupport === "unsupported")
|
|
114
119
|
return { type: "input_text", text: "[image omitted because the target model does not support image input]" };
|
|
115
|
-
const image = await attachmentResolver(ctx, options)(block, options.signal);
|
|
120
|
+
const image = await attachmentResolver(ctx, options, requestImages)(block, options.signal);
|
|
116
121
|
if (Math.ceil(image.data.byteLength / 3) * 4 > options.maxRequestImageBytes)
|
|
117
122
|
throw error("one image exceeds the configured LCX request image bound", "LCX_COMPACT_IMAGE_TOO_LARGE");
|
|
118
123
|
const imageUrl = `data:${image.mediaType};base64,${image.data.toString("base64")}`;
|
|
119
124
|
imageMap.set(imageUrl, structuredClone(image.ref));
|
|
120
125
|
return { type: "input_image", detail: "auto", image_url: imageUrl };
|
|
121
126
|
}
|
|
122
|
-
async function piImageParts(block, ctx, options, imageMap) {
|
|
127
|
+
async function piImageParts(block, ctx, options, imageMap, requestImages) {
|
|
123
128
|
if (options.imageSupport === "unsupported")
|
|
124
129
|
return [{ type: "text", text: "[image omitted because the target model does not support image input]" }];
|
|
125
|
-
const image = await attachmentResolver(ctx, options)(block, options.signal);
|
|
130
|
+
const image = await attachmentResolver(ctx, options, requestImages)(block, options.signal);
|
|
126
131
|
if (Math.ceil(image.data.byteLength / 3) * 4 > options.maxRequestImageBytes)
|
|
127
132
|
throw error("one image exceeds the configured LCX request image bound", "LCX_COMPACT_IMAGE_TOO_LARGE");
|
|
128
133
|
const data = image.data.toString("base64");
|
|
@@ -292,17 +297,15 @@ function toPiAssistant(message, onReplayDegrade) {
|
|
|
292
297
|
return foreignAssistant(message);
|
|
293
298
|
}
|
|
294
299
|
}
|
|
295
|
-
async function piToolContent(blocks, ctx, options, imageMap) {
|
|
300
|
+
async function piToolContent(blocks, ctx, options, imageMap, requestImages) {
|
|
296
301
|
const content = [];
|
|
297
302
|
for (const block of blocks) {
|
|
298
303
|
if (block.type === "text")
|
|
299
304
|
content.push({ type: "text", text: block.text });
|
|
300
305
|
else if (block.type === "image")
|
|
301
|
-
content.push(...(await piImageParts(block, ctx, options, imageMap)));
|
|
306
|
+
content.push(...(await piImageParts(block, ctx, options, imageMap, requestImages)));
|
|
302
307
|
else if (block.type === "reasoning")
|
|
303
308
|
continue;
|
|
304
|
-
else if (block.type === "tool-result")
|
|
305
|
-
content.push(...(await piToolContent(block.content, ctx, options, imageMap)));
|
|
306
309
|
else
|
|
307
310
|
throw unsupportedContent(block.type);
|
|
308
311
|
}
|
|
@@ -312,8 +315,6 @@ function projectFileContent(blocks, fileRequestText) {
|
|
|
312
315
|
return blocks.map((block) => {
|
|
313
316
|
if (block.type === "file")
|
|
314
317
|
return { type: "text", text: fileRequestText(block.attachment) };
|
|
315
|
-
if (block.type === "tool-result")
|
|
316
|
-
return { ...block, content: projectFileContent(block.content, fileRequestText) };
|
|
317
318
|
return block;
|
|
318
319
|
});
|
|
319
320
|
}
|
|
@@ -331,12 +332,38 @@ function projectFiles(messages, ctx) {
|
|
|
331
332
|
content: projectFileContent(message.content, fileRequestText),
|
|
332
333
|
}));
|
|
333
334
|
}
|
|
335
|
+
function collectRequestImageRefs(blocks, refs) {
|
|
336
|
+
for (const block of blocks) {
|
|
337
|
+
if (block.type === "image") {
|
|
338
|
+
if (block.offloaded !== true)
|
|
339
|
+
refs.set(String(block.attachment.attachmentId), block.attachment);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
async function prepareRequestImageVersions(messages, ctx, options) {
|
|
344
|
+
const attachments = attachmentServices(ctx?.attachments);
|
|
345
|
+
if (attachments === undefined)
|
|
346
|
+
return new Map();
|
|
347
|
+
const refs = new Map();
|
|
348
|
+
for (const message of messages)
|
|
349
|
+
collectRequestImageRefs(message.content, refs);
|
|
350
|
+
const versions = await Promise.all([...refs.values()].map(async (ref) => {
|
|
351
|
+
const target = {
|
|
352
|
+
...requestImageDimensions(ref.width, ref.height, options.requestImagePixelBudget),
|
|
353
|
+
maxBytes: options.requestImageMaxBytes,
|
|
354
|
+
};
|
|
355
|
+
return [String(ref.attachmentId), await attachments.store.readImageRequest(ref, target, options.signal)];
|
|
356
|
+
}));
|
|
357
|
+
return new Map(versions);
|
|
358
|
+
}
|
|
334
359
|
function flattenMessageText(message) {
|
|
335
|
-
return message.content
|
|
336
|
-
|
|
337
|
-
|
|
360
|
+
return message.content.map((block) => {
|
|
361
|
+
if (block.type !== "text")
|
|
362
|
+
throw unsupportedContent(block.type);
|
|
363
|
+
return block.text;
|
|
364
|
+
}).join("");
|
|
338
365
|
}
|
|
339
|
-
async function dshToPiMessages(messages, ctx, options, imageMap) {
|
|
366
|
+
async function dshToPiMessages(messages, ctx, options, imageMap, requestImages) {
|
|
340
367
|
const result = [];
|
|
341
368
|
const toolNames = new Map();
|
|
342
369
|
for (const message of messages) {
|
|
@@ -344,6 +371,8 @@ async function dshToPiMessages(messages, ctx, options, imageMap) {
|
|
|
344
371
|
result.push({ role: "user", content: flattenMessageText(message), timestamp: 0 });
|
|
345
372
|
continue;
|
|
346
373
|
}
|
|
374
|
+
if (message.role === "developer")
|
|
375
|
+
throw unsupportedContent("developer message");
|
|
347
376
|
if (message.role === "assistant") {
|
|
348
377
|
const assistant = toPiAssistant(message, options.onReplayDegrade);
|
|
349
378
|
for (const block of assistant.content)
|
|
@@ -352,15 +381,17 @@ async function dshToPiMessages(messages, ctx, options, imageMap) {
|
|
|
352
381
|
result.push(assistant);
|
|
353
382
|
continue;
|
|
354
383
|
}
|
|
384
|
+
if (message.role === "tool") {
|
|
385
|
+
const toolCallId = String(message.toolCallId);
|
|
386
|
+
result.push({ role: "toolResult", toolCallId, toolName: toolNames.get(toolCallId) ?? "unknown", content: await piToolContent(message.content, ctx, options, imageMap, requestImages), isError: message.isError === true, timestamp: 0 });
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
355
389
|
const ordinary = [];
|
|
356
|
-
const toolResults = [];
|
|
357
390
|
for (const block of message.content) {
|
|
358
391
|
if (block.type === "text" || block.type === "image")
|
|
359
392
|
ordinary.push(block);
|
|
360
393
|
else if (block.type === "reasoning")
|
|
361
394
|
continue;
|
|
362
|
-
else if (block.type === "tool-result")
|
|
363
|
-
toolResults.push(block);
|
|
364
395
|
else
|
|
365
396
|
throw unsupportedContent(block.type);
|
|
366
397
|
}
|
|
@@ -370,21 +401,18 @@ async function dshToPiMessages(messages, ctx, options, imageMap) {
|
|
|
370
401
|
if (block.type === "text")
|
|
371
402
|
content.push({ type: "text", text: block.text });
|
|
372
403
|
else if (block.type === "image")
|
|
373
|
-
content.push(...(await piImageParts(block, ctx, options, imageMap)));
|
|
404
|
+
content.push(...(await piImageParts(block, ctx, options, imageMap, requestImages)));
|
|
374
405
|
}
|
|
375
406
|
if (content.length > 0)
|
|
376
407
|
result.push({ role: "user", content, timestamp: 0 });
|
|
377
408
|
}
|
|
378
|
-
for (const block of toolResults) {
|
|
379
|
-
if (block.type !== "tool-result")
|
|
380
|
-
continue;
|
|
381
|
-
const toolCallId = String(block.toolCallId);
|
|
382
|
-
result.push({ role: "toolResult", toolCallId, toolName: toolNames.get(toolCallId) ?? "unknown", content: await piToolContent(block.content, ctx, options, imageMap), addedToolNames: [], isError: block.isError === true, timestamp: 0 });
|
|
383
|
-
}
|
|
384
409
|
}
|
|
385
410
|
return result;
|
|
386
411
|
}
|
|
387
412
|
function builtinResponsesModel(provider, modelId) {
|
|
413
|
+
// Keep descriptor and wire capabilities within the same frozen catalog scope as route ownership.
|
|
414
|
+
if (!wasPi086ResponsesModel(provider, modelId))
|
|
415
|
+
return undefined;
|
|
388
416
|
try {
|
|
389
417
|
return getBuiltinModels(provider).find((model) => model.id === modelId && model.api === "openai-responses");
|
|
390
418
|
}
|
|
@@ -405,6 +433,16 @@ export function resolvePiResponsesModel(options) {
|
|
|
405
433
|
return { ...builtinRecord, ...explicit, id, name: stringValue(explicit.name, stringValue(builtinRecord.name, id)), api: "openai-responses", provider, baseUrl: stringValue(explicit.baseUrl, stringValue(route.baseURL, stringValue(builtinRecord.baseUrl))), reasoning: typeof explicit.reasoning === "boolean" ? explicit.reasoning : typeof builtinRecord.reasoning === "boolean" ? builtinRecord.reasoning : true, input, cost: explicit.cost ?? builtinRecord.cost ?? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: positiveSafeInteger(explicit.contextWindow ?? builtinRecord.contextWindow, 262144), maxTokens: positiveSafeInteger(explicit.maxTokens ?? builtinRecord.maxTokens, 32768), ...(Object.keys(compat).length > 0 ? { compat } : {}) };
|
|
406
434
|
}
|
|
407
435
|
export async function serializeDshMessages(messages, ctx, options = {}) {
|
|
436
|
+
for (const message of messages) {
|
|
437
|
+
if (message.role === "developer")
|
|
438
|
+
throw unsupportedContent("developer message");
|
|
439
|
+
for (const block of message.content)
|
|
440
|
+
if (block.type === "tool-addition" || block.type === "tool-removal")
|
|
441
|
+
throw unsupportedContent(block.type);
|
|
442
|
+
}
|
|
443
|
+
for (const tool of options.tools ?? [])
|
|
444
|
+
if ("deferLoading" in tool && tool.deferLoading === true)
|
|
445
|
+
throw unsupportedContent("deferLoading tool declaration");
|
|
408
446
|
const normalized = {
|
|
409
447
|
imageSupport: options.imageSupport === "supported" || options.imageSupport === "unsupported" ? options.imageSupport : "unknown",
|
|
410
448
|
signal: options.signal instanceof AbortSignal ? options.signal : undefined,
|
|
@@ -419,34 +457,40 @@ export async function serializeDshMessages(messages, ctx, options = {}) {
|
|
|
419
457
|
const imageMap = new Map();
|
|
420
458
|
const fileProjected = projectFiles(messages, ctx);
|
|
421
459
|
const attachments = attachmentServices(ctx?.attachments);
|
|
422
|
-
const
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
460
|
+
const requestImages = normalized.imageSupport === "unsupported"
|
|
461
|
+
? new Map()
|
|
462
|
+
: await prepareRequestImageVersions(fileProjected, ctx, normalized);
|
|
463
|
+
const offloadImages = attachments === undefined || normalized.imageSupport === "unsupported"
|
|
464
|
+
? 0
|
|
465
|
+
: requiredImageOffload(fileProjected, { representation: "base64", maxBytes: normalized.maxRequestImageBytes }, (block) => {
|
|
466
|
+
// `requiredImageOffload` applies the base64 expansion itself for a
|
|
467
|
+
// base64 representation, so this reports raw request-version bytes.
|
|
468
|
+
const request = requestImages.get(String(block.attachment.attachmentId));
|
|
469
|
+
return request?.bytes ?? block.attachment.bytes;
|
|
470
|
+
});
|
|
471
|
+
if (offloadImages > 0) {
|
|
472
|
+
throw new LlmError(`LCX request images exceed the configured base64 bound; ${offloadImages} more oldest occurrence(s) must be offloaded.`, IMAGE_OFFLOAD_REQUIRED_CODE, { offloadImages });
|
|
473
|
+
}
|
|
474
|
+
const projected = projectOffloadedImages(fileProjected, (ref) => offloadedImageText(ref, attachments === undefined ? undefined : imageAccess(ctx, attachments, ref)));
|
|
427
475
|
const model = resolvePiResponsesModel(normalized);
|
|
428
|
-
const
|
|
476
|
+
const dshContext = { systemPrompt: normalized.systemPrompt, messages: await dshToPiMessages(projected, ctx, normalized, imageMap, requestImages), tools: [...normalized.tools] };
|
|
429
477
|
const compat = isObject(model.compat) ? model.compat : {};
|
|
430
478
|
const supportsStrictMode = compat.supportsStrictMode === true;
|
|
431
479
|
const supportsOpenAIGrammarTools = compat.supportsOpenAIGrammarTools === true;
|
|
432
480
|
const supportsAdditionalTools = compat.supportsAdditionalTools === true;
|
|
433
481
|
const supportsToolSearch = compat.supportsToolSearch === true;
|
|
434
482
|
const deferredToolsMode = supportsAdditionalTools ? "additional-tools" : supportsToolSearch ? "tool-search" : undefined;
|
|
435
|
-
const grammarToolInputProperties = createGrammarToolInputProperties(
|
|
436
|
-
//
|
|
483
|
+
const grammarToolInputProperties = createGrammarToolInputProperties(dshContext.tools, supportsOpenAIGrammarTools);
|
|
484
|
+
// These routes consume no tool updates; projectToolUpdates supplies the current immediate declarations.
|
|
437
485
|
const immediateTools = new Map();
|
|
438
|
-
for (const tool of
|
|
486
|
+
for (const tool of dshContext.tools)
|
|
439
487
|
if (tool.name)
|
|
440
488
|
immediateTools.set(tool.name, tool);
|
|
441
|
-
const toolOptions = { supportsStrictMode, supportsOpenAIGrammarTools };
|
|
442
|
-
const piContext =
|
|
443
|
-
systemPrompt: context.systemPrompt,
|
|
444
|
-
messages: context.messages,
|
|
445
|
-
tools: context.tools,
|
|
446
|
-
};
|
|
489
|
+
const toolOptions = { supportsStrictMode, supportsOpenAIGrammarTools, toolSearchResult: false };
|
|
490
|
+
const piContext = normalizeContext(dshContext);
|
|
447
491
|
const piModel = model;
|
|
448
|
-
const convertedInput = convertResponsesMessages(piModel, piContext, new Set(["openai", "openai-codex", "opencode"]), { includeSystemPrompt: normalized.includeSystemPrompt, grammarToolInputProperties,
|
|
449
|
-
const input = restorePortableAssistantPhases(convertedInput,
|
|
492
|
+
const convertedInput = convertResponsesMessages(piModel, piContext, new Set(["openai", "openai-codex", "opencode"]), { includeSystemPrompt: normalized.includeSystemPrompt, grammarToolInputProperties, supportsAdditionalTools, supportsToolSearch, toolOptions });
|
|
493
|
+
const input = restorePortableAssistantPhases(convertedInput, dshContext.messages, { provider: model.provider, id: model.id, api: model.api });
|
|
450
494
|
const tools = options.tools === undefined ? undefined : convertResponsesTools([...immediateTools.values()], toolOptions);
|
|
451
495
|
return { input, imageMap, tools, model: piModel, grammarToolInputProperties, deferredToolsMode };
|
|
452
496
|
}
|