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 +11 -0
- package/README.md +2 -0
- package/README_EN.md +2 -0
- package/lib/compact.js +101 -2
- package/lib/index.js +5 -2
- package/package.json +1 -1
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
|
|
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
|
|
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
|
|
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
|
|
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)
|