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/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")
@@ -1,5 +1,7 @@
1
- import { offloadedImageText, offloadRequestImagesWithPolicy, requestImageHandleText, resolveImageAttachmentAccess, } from "@deepseek-ai/dsh-llm";
2
- import { convertResponsesMessages, convertResponsesTools, createGrammarToolInputProperties, getBuiltinModels, } from "./pi-responses-runtime.js";
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 = await attachments.store.readImageRequest(block.attachment, { maxPixels: options.requestImagePixelBudget, maxBytes: options.requestImageMaxBytes }, signal);
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, ref),
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
- .map((block) => (block.type === "text" ? block.text : ""))
337
- .join("");
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 projected = offloadRequestImagesWithPolicy(fileProjected, {
423
- representation: "base64", maxBytes: normalized.maxRequestImageBytes, byteQuantum: 1,
424
- byteLength: (ref) => Math.min(ref.bytes, normalized.requestImageMaxBytes),
425
- placeholder: (ref) => offloadedImageText(ref, attachments === undefined ? undefined : imageAccess(ctx, attachments, ref)),
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 context = { systemPrompt: normalized.systemPrompt, messages: await dshToPiMessages(projected, ctx, normalized, imageMap), tools: [...normalized.tools] };
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(context.tools, supportsOpenAIGrammarTools);
436
- // DSH has no authoritative added-tool provenance; keep its full catalog immediate.
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 context.tools)
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, deferredTools: new Map(), deferredToolsMode, toolOptions });
449
- const input = restorePortableAssistantPhases(convertedInput, context.messages, { provider: model.provider, id: model.id, api: model.api });
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
  }