dsh-lcx-codex 0.3.2 → 0.3.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/CHANGELOG.md CHANGED
@@ -4,6 +4,17 @@
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## 0.3.3 - 2026-08-21
8
+
9
+ ### Fixed
10
+
11
+ - Native V2 checkpoint 现在由客户端按 64K token 预算保留最近真实 user messages,再追加 exactly-one compaction item;不再错误假设上游 `response.output` 会返回 retained messages。旧的 compaction-only v3 checkpoint 会从 portable history 安全补全,运行时上下文注入不会进入 replacement history。
12
+
13
+ ### Documentation
14
+
15
+ - 记录 installed `0.3.2` 的 compact + 三次 replay 缓存实测,并说明 Sub2API Codex OAuth 路径会删除上游不支持的 `prompt_cache_retention`;短缓存过期后的单次冷请求不等于插件改变了稳定前缀。
16
+ - 增加连续 replay 的前缀稳定性回归断言,区分缓存生命周期与 replacement-history 保真问题。
17
+
7
18
  ## 0.3.2 - 2026-08-21
8
19
 
9
20
  ### Fixed
package/README.md CHANGED
@@ -130,6 +130,8 @@ node (Join-Path $dshHome 'profiles\web\node_modules\dsh-lcx-codex\scripts\probe-
130
130
  - Alpha capability:`$DSH_HOME/storages/lcx-codex/web-alpha-capabilities.json`
131
131
  - Alpha refs:`$DSH_HOME/storages/lcx-codex/web-alpha-refs.json`
132
132
  - 只支持 Native remote-compaction V2,不调用 `/responses/compact`
133
+ - 同路由 replay 会复用 DSH session 的 `prompt_cache_key` 并保持已有请求前缀稳定;短期缓存过期后仍可能出现单次冷请求,不能用会话累计命中率判断插件是否破坏缓存
134
+ - Sub2API 的 Codex OAuth 转换层会删除上游不支持的 `prompt_cache_retention`,因此经该路径设置 `24h` 不会延长缓存;以实际连续请求的 `cached_tokens` 为准
133
135
  - Checkpoint 不保存图片原始字节或 data URL
134
136
  - Opaque checkpoint 不跨不兼容 provider、model、base URL、session 或 lineage 回放
135
137
  - 不包含图片生成功能
package/README_EN.md CHANGED
@@ -130,6 +130,8 @@ The probe does not print the key or full response bodies. Restart DSH, or disabl
130
130
  - Alpha capabilities: `$DSH_HOME/storages/lcx-codex/web-alpha-capabilities.json`
131
131
  - Alpha references: `$DSH_HOME/storages/lcx-codex/web-alpha-refs.json`
132
132
  - Only Native remote-compaction V2 is supported; `/responses/compact` is never called.
133
+ - Same-route replay reuses the DSH session `prompt_cache_key` and preserves the existing request prefix. A single cold request can still occur after the short-lived upstream cache expires, so the cumulative session hit rate is not evidence that the plugin changed the prefix.
134
+ - Sub2API strips the unsupported `prompt_cache_retention` field on its Codex OAuth path, so requesting `24h` does not extend cache lifetime on that route. Use the `cached_tokens` reported by consecutive real requests as the authoritative signal.
133
135
  - Checkpoints never store raw image bytes or data URLs.
134
136
  - Opaque checkpoints are not replayed across an incompatible provider, model, base URL, session, or lineage.
135
137
  - Image generation is not included.
package/lib/compact.js CHANGED
@@ -4,8 +4,107 @@ import { offloadDshRequestImages, resolveDshImage, serializeDshResponsesInput }
4
4
  const UNSUPPORTED_CHECKPOINT_PATTERN = /\[dsh-lcx-codex-checkpoint:[0-9a-f-]{36}\]/iu
5
5
  const PORTABLE_CHECKPOINT_PATTERN = /\[dsh-lcx-codex-v3-checkpoint:([0-9a-f-]{36})\]/giu
6
6
  const PORTABLE_HISTORY_TOKEN_BUDGET = 20_000
7
+ export const NATIVE_RETAINED_MESSAGE_TOKEN_BUDGET = 64_000
7
8
  export const PORTABLE_HISTORY_BYTE_BUDGET = 2 * 1024 * 1024
8
9
  const UNSUPPORTED_IMAGE_PLACEHOLDER = '[image omitted because the target model does not support image input]'
10
+ const NATIVE_IMAGE_TOKEN_ESTIMATE = 765
11
+ const CONTEXTUAL_USER_PREFIXES = [
12
+ '<environment_context>',
13
+ '<user_instructions>',
14
+ '<additional_context>',
15
+ '<skills',
16
+ '<token_budget>',
17
+ '<model_switch>',
18
+ ]
19
+
20
+ function nativeContentPartTokens(part) {
21
+ if (part?.type === 'input_image' || part?.type === 'dsh_image_attachment') return NATIVE_IMAGE_TOKEN_ESTIMATE
22
+ if (typeof part?.text === 'string') return Math.ceil(part.text.length / 4)
23
+ return Math.ceil(JSON.stringify(part ?? null).length / 4)
24
+ }
25
+
26
+ function nativeMessageTokens(item) {
27
+ return Math.max(1, (item?.content ?? []).reduce((total, part) => total + nativeContentPartTokens(part), 0))
28
+ }
29
+
30
+ function truncatedNativeText(text, maxTokens) {
31
+ const maxChars = maxTokens * 4
32
+ if (maxChars <= 0) return ''
33
+ if (text.length <= maxChars) return text
34
+ const omittedTokens = Math.max(1, Math.ceil((text.length - maxChars) / 4))
35
+ const marker = `[...${omittedTokens} tokens truncated...]`
36
+ if (maxChars <= marker.length + 2) return text.slice(-maxChars)
37
+ const sideChars = Math.max(1, Math.floor((maxChars - marker.length) / 2))
38
+ return `${text.slice(0, sideChars)}${marker}${text.slice(-sideChars)}`
39
+ }
40
+
41
+ function truncateNativeUserMessage(item, maxTokens) {
42
+ let remaining = maxTokens
43
+ const content = []
44
+ for (const part of item?.content ?? []) {
45
+ if (remaining <= 0) break
46
+ const tokens = Math.max(1, nativeContentPartTokens(part))
47
+ if (tokens <= remaining) {
48
+ content.push(structuredClone(part))
49
+ remaining -= tokens
50
+ continue
51
+ }
52
+ if (typeof part?.text === 'string') {
53
+ const text = truncatedNativeText(part.text, remaining)
54
+ if (text) content.push({ ...structuredClone(part), text })
55
+ }
56
+ remaining = 0
57
+ }
58
+ return content.length > 0 ? { ...structuredClone(item), content } : undefined
59
+ }
60
+
61
+ function isNativeRetainedUserMessage(item) {
62
+ if (item?.type !== 'message' || item.role !== 'user' || !Array.isArray(item.content)) return false
63
+ return !item.content.some((part) => {
64
+ if (part?.type !== 'input_text' || typeof part.text !== 'string') return false
65
+ const text = part.text.trimStart().toLowerCase()
66
+ return CONTEXTUAL_USER_PREFIXES.some((prefix) => text.startsWith(prefix))
67
+ })
68
+ }
69
+
70
+ export function buildNativeReplacementHistory(input, compaction, options = {}) {
71
+ if (compaction?.type !== 'compaction' || typeof compaction.encrypted_content !== 'string' || compaction.encrypted_content.length === 0) {
72
+ const error = new Error('LCX Native replacement history requires one non-empty compaction item')
73
+ error.code = 'LCX_COMPACT_INVALID_RESPONSE'
74
+ throw error
75
+ }
76
+ let remaining = options.tokenBudget ?? NATIVE_RETAINED_MESSAGE_TOKEN_BUDGET
77
+ if (!Number.isSafeInteger(remaining) || remaining <= 0) {
78
+ const error = new Error('LCX Native retained message token budget must be a positive integer')
79
+ error.code = 'LCX_COMPACT_INVALID_INPUT'
80
+ throw error
81
+ }
82
+ const retained = []
83
+ const candidates = (input ?? []).filter(isNativeRetainedUserMessage)
84
+ for (let index = candidates.length - 1; index >= 0 && remaining > 0; index -= 1) {
85
+ const item = candidates[index]
86
+ const tokens = nativeMessageTokens(item)
87
+ if (tokens <= remaining) {
88
+ retained.push(structuredClone(item))
89
+ remaining -= tokens
90
+ continue
91
+ }
92
+ const truncated = truncateNativeUserMessage(item, remaining)
93
+ if (truncated) retained.push(truncated)
94
+ remaining = 0
95
+ }
96
+ retained.reverse()
97
+ retained.push(structuredClone(compaction))
98
+ return retained
99
+ }
100
+
101
+ export function checkpointNativeReplacementHistory(record) {
102
+ const nativeOutput = Array.isArray(record?.nativeOutput) ? record.nativeOutput : []
103
+ if (nativeOutput.some(isNativeRetainedUserMessage)) return structuredClone(nativeOutput)
104
+ const compaction = nativeOutput.find((item) => item?.type === 'compaction') ?? record?.nativeCompaction
105
+ if (!compaction) return structuredClone(nativeOutput)
106
+ return buildNativeReplacementHistory(record?.portableHistory ?? [], compaction)
107
+ }
9
108
 
10
109
  export function portableCheckpointIds(text) {
11
110
  return [...String(text ?? '').matchAll(PORTABLE_CHECKPOINT_PATTERN)].map((match) => match[1].toLowerCase())
@@ -461,7 +560,7 @@ export function buildPortableResponsesInput(messages, store, route) {
461
560
  throw error
462
561
  }
463
562
  const prefix = state === 'native-compatible'
464
- ? record.nativeOutput
563
+ ? checkpointNativeReplacementHistory(record)
465
564
  : [...portableSummaryItem(record.portableSummary), ...buildPortableHistory(record.portableHistory)]
466
565
  const tail = normalInput((messages ?? []).slice(marker.index + 1), { strict: true })
467
566
  return [...structuredClone(prefix), ...tail]
@@ -513,7 +612,7 @@ export async function buildPortableResponsesInputWithImages(messages, store, rou
513
612
  }
514
613
  }
515
614
  const prefix = state === 'native-compatible'
516
- ? await hydrateNativeImageReferences(record.nativeOutput, options)
615
+ ? await hydrateNativeImageReferences(checkpointNativeReplacementHistory(record), options)
517
616
  : [...portableSummaryItem(record.portableSummary), ...portableHistory]
518
617
  const tail = state === 'native-compatible'
519
618
  ? await serializeDshResponsesInput(requestMessages.slice(marker.index + 1), { ...options, imageSupport, route })
package/lib/index.js CHANGED
@@ -8,6 +8,7 @@ import { join } from 'node:path'
8
8
  import { CheckpointV3Store, CHECKPOINT_V3_VERSION } from './checkpoint-store-v3.js'
9
9
  import {
10
10
  baseURLFingerprint,
11
+ buildNativeReplacementHistory,
11
12
  buildPortableHistory,
12
13
  assertCheckpointRoute,
13
14
  buildPortableResponsesInput,
@@ -15,6 +16,7 @@ import {
15
16
  hasPortableCheckpoint,
16
17
  hydrateNativeImageReferences,
17
18
  inputImageCount,
19
+ checkpointNativeReplacementHistory,
18
20
  latestPortableMarker,
19
21
  normalizeCompactionResponse,
20
22
  persistNativeImageReferences,
@@ -774,7 +776,7 @@ async function* nativeCheckpointReplayStream(options, config, portableStore, ctx
774
776
  ? await resolveModelImageSupport(llm, route, options.signal)
775
777
  : 'supported'
776
778
  const imageOptions = { resolveImage: attachmentImageResolver(ctx), imageSupport, signal: options.signal, maxRequestImageBytes: config.maxRequestImageBytes }
777
- const nativeOutput = await hydrateNativeImageReferences(record.nativeOutput, imageOptions)
779
+ const nativeOutput = await hydrateNativeImageReferences(checkpointNativeReplacementHistory(record), imageOptions)
778
780
  const tailInput = await buildPortableResponsesInputWithImages(
779
781
  (options.messages ?? []).slice(marker.index + 1),
780
782
  portableStore,
@@ -887,7 +889,8 @@ function portableMarkerTextWithSummary(id, model, usage, summary) {
887
889
  async function commitRemoteCompaction(prepared, result, portableStore, config, summary, usage) {
888
890
  const { history, route, lease, input, imageReferences } = prepared
889
891
  lease?.assert('commit')
890
- const persistedNativeOutput = persistNativeImageReferences(result.output, imageReferences)
892
+ const replacementHistory = buildNativeReplacementHistory(input, result.compaction)
893
+ const persistedNativeOutput = persistNativeImageReferences(replacementHistory, imageReferences)
891
894
  const persistedPortableInput = persistNativeImageReferences(input, imageReferences)
892
895
  const id = randomUUID()
893
896
  const parent = latestPortableMarker(history)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-lcx-codex",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
4
4
  "description": "DSH web search and Responses native V2 compaction for Sub2API-proxied or NewAPI-relayed GPT models",
5
5
  "keywords": [
6
6
  "deepseek-harness",