dsh-lcx-codex 0.3.0
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 +42 -0
- package/LICENSE +21 -0
- package/README.md +120 -0
- package/cordis.patch.yml +18 -0
- package/lib/checkpoint-store-v3.js +459 -0
- package/lib/client.js +248 -0
- package/lib/compact-v2.js +305 -0
- package/lib/compact.js +653 -0
- package/lib/dsh-pi-responses.js +339 -0
- package/lib/index.js +1757 -0
- package/lib/private-file.js +18 -0
- package/lib/session-lease.js +70 -0
- package/lib/transport.js +396 -0
- package/lib/web-run-output.js +157 -0
- package/lib/web-search-alpha.js +451 -0
- package/lib/web-search-capability.js +78 -0
- package/lib/web-search-hosted.js +383 -0
- package/lib/web-search-ref-store.js +70 -0
- package/lib/web-search-store.js +181 -0
- package/package.json +99 -0
- package/scripts/probe-alpha.mjs +91 -0
package/lib/index.js
ADDED
|
@@ -0,0 +1,1757 @@
|
|
|
1
|
+
import z from '@deepseek-ai/schemastery'
|
|
2
|
+
import { attributionHeaders, resolveRetryPolicy } from '@deepseek-ai/dsh-llm'
|
|
3
|
+
import { settingsNamespace } from '@deepseek-ai/dsh-settings'
|
|
4
|
+
import { randomUUID } from 'node:crypto'
|
|
5
|
+
import { homedir } from 'node:os'
|
|
6
|
+
import { join } from 'node:path'
|
|
7
|
+
import { CheckpointV3Store, CHECKPOINT_V3_VERSION } from './checkpoint-store-v3.js'
|
|
8
|
+
import {
|
|
9
|
+
baseURLFingerprint,
|
|
10
|
+
buildPortableHistory,
|
|
11
|
+
assertCheckpointRoute,
|
|
12
|
+
buildPortableResponsesInput,
|
|
13
|
+
buildPortableResponsesInputWithImages,
|
|
14
|
+
hasPortableCheckpoint,
|
|
15
|
+
hydrateNativeImageReferences,
|
|
16
|
+
inputImageCount,
|
|
17
|
+
latestPortableMarker,
|
|
18
|
+
normalizeCompactionResponse,
|
|
19
|
+
persistNativeImageReferences,
|
|
20
|
+
routeFingerprint,
|
|
21
|
+
textOfMessage,
|
|
22
|
+
} from './compact.js'
|
|
23
|
+
import { DEFAULT_MAX_REQUEST_IMAGE_BYTES, resolveModelImageSupport } from './dsh-pi-responses.js'
|
|
24
|
+
import { abortIfNeeded, fetchJsonWithRetry, fetchSse } from './transport.js'
|
|
25
|
+
import { mergeFeatureHeader, requestNativeCompaction, responsesTools } from './compact-v2.js'
|
|
26
|
+
import {
|
|
27
|
+
HOSTED_SEARCH_OUTPUT,
|
|
28
|
+
HOSTED_SEARCH_PARAMETERS,
|
|
29
|
+
buildHostedSearchBody,
|
|
30
|
+
normalizeHostedSearchArgs,
|
|
31
|
+
parseHostedSearchResponse,
|
|
32
|
+
renderHostedSearchResult,
|
|
33
|
+
} from './web-search-hosted.js'
|
|
34
|
+
import {
|
|
35
|
+
ALPHA_SCHEMA_FINGERPRINT,
|
|
36
|
+
ALPHA_SEARCH_OUTPUT,
|
|
37
|
+
ALPHA_SEARCH_PARAMETERS,
|
|
38
|
+
buildAlphaSearchBody,
|
|
39
|
+
normalizeAlphaSearchArgs,
|
|
40
|
+
parseAlphaSearchResponse,
|
|
41
|
+
renderAlphaSearchResult,
|
|
42
|
+
} from './web-search-alpha.js'
|
|
43
|
+
import { AlphaCapabilityStore, alphaCapabilityFingerprint, alphaCapabilityUsable } from './web-search-capability.js'
|
|
44
|
+
import { AlphaRefStore } from './web-search-ref-store.js'
|
|
45
|
+
import { createSessionGenerationTracker } from './session-lease.js'
|
|
46
|
+
|
|
47
|
+
const name = 'lcx-codex'
|
|
48
|
+
const inject = ['llm', 'web', 'credentials', 'settings', 'tools']
|
|
49
|
+
const WEB_SEARCH_TOOL_NAME = 'websearch_gpt'
|
|
50
|
+
const ALPHA_SEARCH_TOOL_NAME = 'websearch_alpha'
|
|
51
|
+
const COMPACTION_DIRECTIVE = 'You are now acting as a compaction engine'
|
|
52
|
+
const SETTINGS_NAMESPACE = 'lcx-codex'
|
|
53
|
+
const checkpointReplayOptions = new WeakSet()
|
|
54
|
+
const PORTABLE_REPLAY_TEXT_MAX_CHARS = 32_000
|
|
55
|
+
const PORTABLE_REPLAY_TOTAL_MAX_CHARS = 80_000
|
|
56
|
+
const PORTABLE_REPLAY_TOTAL_MAX_BYTES = 2 * 1024 * 1024
|
|
57
|
+
const V3_CHECKPOINT_MARKER_PATTERN = /\[dsh-lcx-codex-v3-checkpoint:[0-9a-f-]{36}\]/giu
|
|
58
|
+
const CHECKPOINT_REPLAY_UNAVAILABLE_CODE = 'LCX_CHECKPOINT_REPLAY_UNAVAILABLE'
|
|
59
|
+
const UNSUPPORTED_IMAGE_PLACEHOLDER = '[image omitted because the target model does not support image input]'
|
|
60
|
+
|
|
61
|
+
const Config = z.object({
|
|
62
|
+
provider: z.string().default('lcx'),
|
|
63
|
+
baseURL: z.string().default('https://api.lcxbot.com/v1'),
|
|
64
|
+
apiKeyEnv: z.string().default('LCX_API_KEY'),
|
|
65
|
+
model: z.string().default('gpt-5.6-sol'),
|
|
66
|
+
compactTransport: z.string().default('native-v2'),
|
|
67
|
+
checkpointPath: z.string().default(''),
|
|
68
|
+
alphaCapabilityPath: z.string().default(''),
|
|
69
|
+
alphaRefPath: z.string().default(''),
|
|
70
|
+
alphaProfile: z.string().default(''),
|
|
71
|
+
alphaGroup: z.string().default(''),
|
|
72
|
+
alphaMaxOutputTokens: z.number().default(2500),
|
|
73
|
+
webSearchProvider: z.string().default('lcx-responses'),
|
|
74
|
+
webMaxResults: z.number().default(8),
|
|
75
|
+
timeoutMs: z.number().default(300000),
|
|
76
|
+
maxResponseBytes: z.number().default(4 * 1024 * 1024),
|
|
77
|
+
maxAttempts: z.number().default(3),
|
|
78
|
+
maxRequestImageBytes: z.number().default(DEFAULT_MAX_REQUEST_IMAGE_BYTES),
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
const SettingsSchema = z.object({
|
|
82
|
+
enabled: z.boolean().default(false),
|
|
83
|
+
webSearch: z.boolean().default(false),
|
|
84
|
+
alphaSearch: z.boolean().default(false),
|
|
85
|
+
remoteCompaction: z.boolean().default(false),
|
|
86
|
+
fallbackToBasicCompaction: z.boolean().default(true),
|
|
87
|
+
provider: z.string().default('lcx'),
|
|
88
|
+
baseURL: z.string().default('https://api.lcxbot.com/v1'),
|
|
89
|
+
apiKeyEnv: z.string().default('LCX_API_KEY'),
|
|
90
|
+
model: z.string().default('gpt-5.6-sol'),
|
|
91
|
+
compactTransport: z.string().default('native-v2'),
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
function webError(message, code, cause) {
|
|
95
|
+
const error = new Error(message, cause === undefined ? undefined : { cause })
|
|
96
|
+
error.name = 'WebError'
|
|
97
|
+
error.code = code
|
|
98
|
+
return error
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function defaultCheckpointPath() {
|
|
102
|
+
return join(process.env.DSH_HOME ?? join(homedir(), '.dsh'), 'storages', 'lcx-codex', 'checkpoints-v3.json')
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function defaultAlphaCapabilityPath() {
|
|
106
|
+
return join(process.env.DSH_HOME ?? join(homedir(), '.dsh'), 'storages', 'lcx-codex', 'web-alpha-capabilities.json')
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function defaultAlphaRefPath() {
|
|
110
|
+
return join(process.env.DSH_HOME ?? join(homedir(), '.dsh'), 'storages', 'lcx-codex', 'web-alpha-refs.json')
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function portableCheckpointPath(checkpointPath) {
|
|
114
|
+
const value = String(checkpointPath)
|
|
115
|
+
if (/-v3\.json$/iu.test(value)) return value
|
|
116
|
+
return /\.json$/iu.test(value) ? value.replace(/\.json$/iu, '-v3.json') : `${value}-v3.json`
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const COMPACT_TRANSPORT_UNSUPPORTED_CODE = 'LCX_COMPACT_TRANSPORT_UNSUPPORTED'
|
|
120
|
+
const COMPACT_TRANSPORT_INVALID_CODE = 'LCX_COMPACT_TRANSPORT_INVALID'
|
|
121
|
+
|
|
122
|
+
function normalizeCompactTransport(value) {
|
|
123
|
+
const normalized = String(value ?? 'native-v2').trim().toLowerCase()
|
|
124
|
+
if (normalized === 'legacy') {
|
|
125
|
+
const error = new Error('Legacy compact transport is unsupported; only native-v2 is available')
|
|
126
|
+
error.code = COMPACT_TRANSPORT_UNSUPPORTED_CODE
|
|
127
|
+
throw error
|
|
128
|
+
}
|
|
129
|
+
if (normalized !== 'native-v2') {
|
|
130
|
+
const error = new Error(`Unsupported compact transport: ${String(value)}`)
|
|
131
|
+
error.code = COMPACT_TRANSPORT_INVALID_CODE
|
|
132
|
+
throw error
|
|
133
|
+
}
|
|
134
|
+
return normalized
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function normalizeConfig(input = {}) {
|
|
138
|
+
return {
|
|
139
|
+
provider: input.provider || 'lcx',
|
|
140
|
+
baseURL: String(input.baseURL || 'https://api.lcxbot.com/v1').replace(/\/+$/u, ''),
|
|
141
|
+
apiKeyEnv: input.apiKeyEnv || 'LCX_API_KEY',
|
|
142
|
+
model: input.model || 'gpt-5.6-sol',
|
|
143
|
+
compactTransport: normalizeCompactTransport(input.compactTransport),
|
|
144
|
+
headers: input.headers && typeof input.headers === 'object' ? { ...input.headers } : {},
|
|
145
|
+
checkpointPath: input.checkpointPath || defaultCheckpointPath(),
|
|
146
|
+
portableCheckpointPath: input.portableCheckpointPath || portableCheckpointPath(input.checkpointPath || defaultCheckpointPath()),
|
|
147
|
+
alphaCapabilityPath: input.alphaCapabilityPath || defaultAlphaCapabilityPath(),
|
|
148
|
+
alphaRefPath: input.alphaRefPath || defaultAlphaRefPath(),
|
|
149
|
+
alphaProfile: String(input.alphaProfile ?? ''),
|
|
150
|
+
alphaGroup: String(input.alphaGroup ?? ''),
|
|
151
|
+
alphaMaxOutputTokens: Number.isInteger(input.alphaMaxOutputTokens) && input.alphaMaxOutputTokens > 0 ? Math.min(input.alphaMaxOutputTokens, 32_000) : 2500,
|
|
152
|
+
webSearchProvider: input.webSearchProvider || 'lcx-responses',
|
|
153
|
+
webMaxResults: Number.isInteger(input.webMaxResults) && input.webMaxResults > 0 ? input.webMaxResults : 8,
|
|
154
|
+
timeoutMs: Number.isInteger(input.timeoutMs) && input.timeoutMs > 0 ? input.timeoutMs : 300000,
|
|
155
|
+
maxResponseBytes: Number.isInteger(input.maxResponseBytes) && input.maxResponseBytes > 0 ? input.maxResponseBytes : 4 * 1024 * 1024,
|
|
156
|
+
maxAttempts: Number.isInteger(input.maxAttempts) && input.maxAttempts > 0 ? Math.min(input.maxAttempts, 6) : 3,
|
|
157
|
+
maxRequestImageBytes: Number.isSafeInteger(input.maxRequestImageBytes) && input.maxRequestImageBytes > 0 ? input.maxRequestImageBytes : DEFAULT_MAX_REQUEST_IMAGE_BYTES,
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function isGptModel(model) {
|
|
162
|
+
return /(^|[^a-z])gpt(?:[^a-z]|$)/iu.test(String(model ?? ''))
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function settingsValue(ctx, namespace) {
|
|
166
|
+
const settings = ctx?.get?.('settings') ?? ctx?.settings
|
|
167
|
+
return settings?.get?.(settingsNamespace(namespace))
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function resolveResponsesRouteConfig(ctx, options, fallbackConfig) {
|
|
171
|
+
if (!isGptModel(options.model)) return undefined
|
|
172
|
+
const provider = String(options.provider ?? '')
|
|
173
|
+
const section = settingsValue(ctx, 'llm-pi-ai')
|
|
174
|
+
const profile = section?.providers?.[provider]
|
|
175
|
+
if (profile === undefined) {
|
|
176
|
+
if (provider !== fallbackConfig.provider) return undefined
|
|
177
|
+
return normalizeConfig({ ...fallbackConfig, provider, model: options.model })
|
|
178
|
+
}
|
|
179
|
+
if (profile.api !== 'openai-responses') return undefined
|
|
180
|
+
const baseURL = profile.baseURL ?? (provider === fallbackConfig.provider ? fallbackConfig.baseURL : undefined)
|
|
181
|
+
const apiKeyEnv = profile.apiKeyEnv ?? (provider === fallbackConfig.provider ? fallbackConfig.apiKeyEnv : undefined)
|
|
182
|
+
if (!baseURL || !apiKeyEnv) return undefined
|
|
183
|
+
return normalizeConfig({
|
|
184
|
+
...fallbackConfig,
|
|
185
|
+
provider,
|
|
186
|
+
baseURL,
|
|
187
|
+
apiKeyEnv,
|
|
188
|
+
headers: profile.headers ?? fallbackConfig.headers,
|
|
189
|
+
model: options.model,
|
|
190
|
+
compactTransport: profile.compactTransport ?? fallbackConfig.compactTransport,
|
|
191
|
+
timeoutMs: profile.timeoutMs ?? fallbackConfig.timeoutMs,
|
|
192
|
+
maxRequestImageBytes: profile.maxRequestImageBytes ?? fallbackConfig.maxRequestImageBytes,
|
|
193
|
+
maxAttempts: retryAttempts(profile.retryPolicy, fallbackConfig.maxAttempts),
|
|
194
|
+
})
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function retryAttempts(policy, fallback) {
|
|
198
|
+
if (!policy || typeof policy !== 'object') return fallback
|
|
199
|
+
try {
|
|
200
|
+
const resolved = resolveRetryPolicy(policy, 'llm-pi-ai provider retryPolicy')
|
|
201
|
+
if (resolved.mode === 'normal' && Number.isSafeInteger(resolved.maxRetries)) return Math.min(resolved.maxRetries + 1, 6)
|
|
202
|
+
} catch {
|
|
203
|
+
return fallback
|
|
204
|
+
}
|
|
205
|
+
return fallback
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function nativeRouteError(options) {
|
|
209
|
+
const error = new Error(`【GPT 专属原生远程压缩】当前路由不满足 GPT + openai-responses 条件:${String(options.provider)}/${String(options.model)}`)
|
|
210
|
+
error.code = 'LCX_CHECKPOINT_ROUTE_MISMATCH'
|
|
211
|
+
return error
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function ambientApiKey(config) {
|
|
215
|
+
const key = String(process.env[config.apiKeyEnv] ?? '').trim()
|
|
216
|
+
if (!key) throw new Error(`DSH provider credential is unavailable: ${config.apiKeyEnv}`)
|
|
217
|
+
return key
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async function resolveApiKey(ctx, config) {
|
|
221
|
+
const credentials = ctx?.get?.('credentials') ?? ctx?.credentials
|
|
222
|
+
if (credentials?.resolve) {
|
|
223
|
+
const resolved = await credentials.resolve(config.apiKeyEnv)
|
|
224
|
+
if (typeof resolved?.value === 'string' && resolved.value.length > 0) return resolved.value
|
|
225
|
+
}
|
|
226
|
+
return ambientApiKey(config)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function attachmentImageResolver(ctx) {
|
|
230
|
+
const attachments = ctx?.get?.('attachments') ?? ctx?.attachments
|
|
231
|
+
return async (block, signal) => {
|
|
232
|
+
if (!attachments?.readImage) {
|
|
233
|
+
const error = new Error('LCX Compact image input requires the DSH attachment service')
|
|
234
|
+
error.code = 'LCX_COMPACT_IMAGE_UNAVAILABLE'
|
|
235
|
+
throw error
|
|
236
|
+
}
|
|
237
|
+
try {
|
|
238
|
+
const stored = await attachments.readImage(block?.attachment, signal)
|
|
239
|
+
return { data: stored?.data, mediaType: stored?.ref?.mediaType ?? block?.attachment?.mediaType }
|
|
240
|
+
} catch (error) {
|
|
241
|
+
const wrapped = new Error('LCX Compact failed to read image attachment', { cause: error })
|
|
242
|
+
wrapped.code = 'LCX_COMPACT_IMAGE_UNAVAILABLE'
|
|
243
|
+
throw wrapped
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function usageFrom(value, seen = new Set()) {
|
|
249
|
+
if (!value || typeof value !== 'object' || seen.has(value)) return undefined
|
|
250
|
+
seen.add(value)
|
|
251
|
+
if (Number.isFinite(value.input_tokens) || Number.isFinite(value.output_tokens)) {
|
|
252
|
+
const totalInputTokens = Number(value.input_tokens ?? 0)
|
|
253
|
+
const outputTokens = Number(value.output_tokens ?? 0)
|
|
254
|
+
const cachedValue = Number(value.input_tokens_details?.cached_tokens ?? value.input_token_details?.cached_tokens ?? value.prompt_tokens_details?.cached_tokens ?? 0)
|
|
255
|
+
const cacheWriteValue = Number(value.input_tokens_details?.cache_write_tokens ?? value.input_token_details?.cache_write_tokens ?? value.prompt_tokens_details?.cache_write_tokens ?? 0)
|
|
256
|
+
const cacheReadTokens = Number.isFinite(cachedValue) && cachedValue > 0 ? cachedValue : 0
|
|
257
|
+
const cacheWriteTokens = Number.isFinite(cacheWriteValue) && cacheWriteValue > 0 ? cacheWriteValue : 0
|
|
258
|
+
const inputTokens = Number.isFinite(totalInputTokens)
|
|
259
|
+
? Math.max(0, totalInputTokens - cacheReadTokens - cacheWriteTokens)
|
|
260
|
+
: 0
|
|
261
|
+
const normalizedOutputTokens = Number.isFinite(outputTokens) && outputTokens > 0 ? outputTokens : 0
|
|
262
|
+
const reasoningValue = Number(value.output_token_details?.reasoning_tokens ?? 0)
|
|
263
|
+
if (inputTokens <= 0 && cacheReadTokens <= 0 && cacheWriteTokens <= 0 && normalizedOutputTokens <= 0) return undefined
|
|
264
|
+
const usage = { inputTokens, outputTokens: normalizedOutputTokens }
|
|
265
|
+
if (cacheReadTokens > 0) usage.cacheReadTokens = cacheReadTokens
|
|
266
|
+
if (cacheWriteTokens > 0) usage.cacheWriteTokens = cacheWriteTokens
|
|
267
|
+
if (Number.isFinite(reasoningValue) && reasoningValue > 0) usage.reasoningTokens = reasoningValue
|
|
268
|
+
return usage
|
|
269
|
+
}
|
|
270
|
+
if (Array.isArray(value)) {
|
|
271
|
+
for (const item of value) {
|
|
272
|
+
const found = usageFrom(item, seen)
|
|
273
|
+
if (found) return found
|
|
274
|
+
}
|
|
275
|
+
} else {
|
|
276
|
+
for (const item of Object.values(value)) {
|
|
277
|
+
const found = usageFrom(item, seen)
|
|
278
|
+
if (found) return found
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return undefined
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function responseTextParts(item) {
|
|
285
|
+
return (Array.isArray(item?.content) ? item.content : [])
|
|
286
|
+
.filter((part) => part?.type === 'output_text' && typeof part.text === 'string')
|
|
287
|
+
.map((part) => part.text)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function responseReasoningText(item) {
|
|
291
|
+
const textOf = (parts) => (Array.isArray(parts) ? parts : [])
|
|
292
|
+
.filter((part) => typeof part?.text === 'string')
|
|
293
|
+
.map((part) => part.text)
|
|
294
|
+
.filter((text) => text.length > 0)
|
|
295
|
+
.join('\n\n')
|
|
296
|
+
return textOf(item?.summary) || textOf(item?.content)
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
async function authenticatedHeaders(ctx, config, sessionId, clientRequestId) {
|
|
300
|
+
return {
|
|
301
|
+
...config.headers,
|
|
302
|
+
...attributionHeaders(),
|
|
303
|
+
authorization: `Bearer ${await resolveApiKey(ctx, config)}`,
|
|
304
|
+
'x-client-request-id': clientRequestId ?? randomUUID(),
|
|
305
|
+
...(sessionId ? { 'session-id': String(sessionId) } : {}),
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const REMOTE_COMPACTION_ERROR_CODES = new Set([
|
|
310
|
+
'LCX_HTTP_RETRYABLE',
|
|
311
|
+
'LCX_TIMEOUT',
|
|
312
|
+
'LCX_RETRY_EXHAUSTED',
|
|
313
|
+
])
|
|
314
|
+
const TRANSIENT_NETWORK_ERROR_CODES = new Set(['ECONNRESET', 'ECONNREFUSED', 'ETIMEDOUT', 'EAI_AGAIN', 'UND_ERR_CONNECT_TIMEOUT'])
|
|
315
|
+
|
|
316
|
+
function hasRemoteCompactionErrorCode(error) {
|
|
317
|
+
const seen = new Set()
|
|
318
|
+
let current = error
|
|
319
|
+
while (current && !seen.has(current)) {
|
|
320
|
+
seen.add(current)
|
|
321
|
+
const statusRetryable = Number.isInteger(current.status) && (current.status === 429 || (current.status >= 500 && current.status <= 599))
|
|
322
|
+
if (current instanceof TypeError || current.name === 'TypeError' || statusRetryable || current.retryable === true || REMOTE_COMPACTION_ERROR_CODES.has(current.code) || TRANSIENT_NETWORK_ERROR_CODES.has(current.code)) return true
|
|
323
|
+
current = current.cause
|
|
324
|
+
}
|
|
325
|
+
return false
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function markRemoteCompactionRequestError(error) {
|
|
329
|
+
const failure = error instanceof Error ? error : new Error(String(error))
|
|
330
|
+
failure.remoteCompactionRequest = hasRemoteCompactionErrorCode(failure)
|
|
331
|
+
return failure
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function currentRoute(options, config) {
|
|
335
|
+
return {
|
|
336
|
+
provider: String(options.provider ?? config.provider ?? ''),
|
|
337
|
+
model: String(options.model ?? config.model ?? ''),
|
|
338
|
+
baseURL: config.baseURL,
|
|
339
|
+
sessionId: options.sessionId ? String(options.sessionId) : '',
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function sessionAncestry(ctx, sessionId) {
|
|
344
|
+
const id = typeof sessionId === 'string' ? sessionId : ''
|
|
345
|
+
const sessions = ctx?.get?.('sessions') ?? ctx?.sessions
|
|
346
|
+
if (!id || !sessions?.get) return []
|
|
347
|
+
const ancestors = []
|
|
348
|
+
const seen = new Set([id])
|
|
349
|
+
let current = sessions.get(id)
|
|
350
|
+
while (current && ancestors.length < 32) {
|
|
351
|
+
const parent = current.header?.parentSession
|
|
352
|
+
if (typeof parent !== 'string' || parent.length === 0 || seen.has(parent)) break
|
|
353
|
+
ancestors.push(parent)
|
|
354
|
+
seen.add(parent)
|
|
355
|
+
current = sessions.get(parent)
|
|
356
|
+
}
|
|
357
|
+
return ancestors
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function routeWithSessionAncestry(ctx, route) {
|
|
361
|
+
return { ...route, ancestorSessionIds: sessionAncestry(ctx, route.sessionId) }
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function promptCacheKey(route) {
|
|
365
|
+
return route?.sessionId ? routeFingerprint(route) : undefined
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
async function requestRemoteCompaction(options, input, config, signal, ctx, route, preflightHeaders) {
|
|
369
|
+
try {
|
|
370
|
+
const headers = preflightHeaders ?? await authenticatedHeaders(ctx, config, options.sessionId)
|
|
371
|
+
return await requestNativeCompaction({
|
|
372
|
+
baseURL: config.baseURL,
|
|
373
|
+
model: options.model,
|
|
374
|
+
input,
|
|
375
|
+
instructions: options.system,
|
|
376
|
+
promptCacheKey: promptCacheKey(route),
|
|
377
|
+
idempotencyKey: randomUUID(),
|
|
378
|
+
tools: options.tools,
|
|
379
|
+
headers,
|
|
380
|
+
signal,
|
|
381
|
+
timeoutMs: config.timeoutMs,
|
|
382
|
+
maxAttempts: config.maxAttempts,
|
|
383
|
+
maxResponseBytes: config.maxResponseBytes,
|
|
384
|
+
})
|
|
385
|
+
} catch (error) {
|
|
386
|
+
throw markRemoteCompactionRequestError(error)
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function portableSummaryText(model, usage) {
|
|
391
|
+
const input = Number.isFinite(usage?.inputTokens) ? usage.inputTokens : undefined
|
|
392
|
+
const output = Number.isFinite(usage?.outputTokens) ? usage.outputTokens : undefined
|
|
393
|
+
const usageText = input === undefined || output === undefined ? 'usage:未知' : `usage:${input}/${output} tokens`
|
|
394
|
+
return `LCX 压缩完成 · Native V2 · v3 已保存 · 模型:${String(model)} · ${usageText}`
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function checkpointReplaySource(checkpointId) {
|
|
398
|
+
return {
|
|
399
|
+
kind: 'plugin',
|
|
400
|
+
plugin: 'dsh-lcx-codex',
|
|
401
|
+
purpose: 'checkpoint-recall',
|
|
402
|
+
...(checkpointId ? { checkpointId } : {}),
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function stripCheckpointMarker(text) {
|
|
407
|
+
return String(text).replace(V3_CHECKPOINT_MARKER_PATTERN, '').trim()
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function replayBudget() {
|
|
411
|
+
return {
|
|
412
|
+
chars: PORTABLE_REPLAY_TOTAL_MAX_CHARS,
|
|
413
|
+
bytes: PORTABLE_REPLAY_TOTAL_MAX_BYTES,
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function boundedReplayText(value, budget) {
|
|
418
|
+
if (typeof value !== 'string') return undefined
|
|
419
|
+
const normalized = stripCheckpointMarker(value)
|
|
420
|
+
if (!normalized || budget.chars <= 0 || budget.bytes <= 0) return undefined
|
|
421
|
+
let text = normalized.slice(0, Math.min(PORTABLE_REPLAY_TEXT_MAX_CHARS, budget.chars))
|
|
422
|
+
while (text.length > 0 && Buffer.byteLength(text, 'utf8') > budget.bytes) text = text.slice(0, -1)
|
|
423
|
+
if (!text) return undefined
|
|
424
|
+
budget.chars -= text.length
|
|
425
|
+
budget.bytes -= Buffer.byteLength(text, 'utf8')
|
|
426
|
+
return text
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function exactReplayText(value, budget) {
|
|
430
|
+
if (typeof value !== 'string' || value.length === 0 || value.length > PORTABLE_REPLAY_TEXT_MAX_CHARS) return undefined
|
|
431
|
+
const byteLength = Buffer.byteLength(value, 'utf8')
|
|
432
|
+
if (value.length > budget.chars || byteLength > budget.bytes) return undefined
|
|
433
|
+
budget.chars -= value.length
|
|
434
|
+
budget.bytes -= byteLength
|
|
435
|
+
return value
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function isPortableReplayItem(item) {
|
|
439
|
+
return item !== null && typeof item === 'object' && !Array.isArray(item) &&
|
|
440
|
+
typeof item.type === 'string' && !item.type.startsWith('response.') &&
|
|
441
|
+
item.type !== 'compaction' && item.type !== 'context_compaction' && item.type !== 'compaction_trigger' &&
|
|
442
|
+
!Object.prototype.hasOwnProperty.call(item, 'encrypted_content')
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function portableReplayUnsupportedContentError(type) {
|
|
446
|
+
const error = new Error(`LCX checkpoint portable replay cannot represent Responses item type: ${String(type)}`)
|
|
447
|
+
error.code = 'LCX_CHECKPOINT_PORTABLE_UNSUPPORTED_CONTENT'
|
|
448
|
+
return error
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function safePortableCall(item) {
|
|
452
|
+
if (!isPortableReplayItem(item) || item.type !== 'function_call') return undefined
|
|
453
|
+
if (typeof item.call_id !== 'string' || item.call_id.length === 0 || item.call_id.length > 256) return undefined
|
|
454
|
+
if (typeof item.name !== 'string' || item.name.trim().length === 0 || item.name.length > 256) return undefined
|
|
455
|
+
if (typeof item.arguments !== 'string' || item.arguments.length === 0 || item.arguments.length > PORTABLE_REPLAY_TEXT_MAX_CHARS) return undefined
|
|
456
|
+
try {
|
|
457
|
+
JSON.parse(item.arguments)
|
|
458
|
+
} catch {
|
|
459
|
+
return undefined
|
|
460
|
+
}
|
|
461
|
+
return { callId: item.call_id, name: item.name, arguments: item.arguments }
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function safePortableOutput(item) {
|
|
465
|
+
if (!isPortableReplayItem(item) || item.type !== 'function_call_output') return undefined
|
|
466
|
+
if (typeof item.call_id !== 'string' || item.call_id.length === 0 || item.call_id.length > 256) return undefined
|
|
467
|
+
let outputParts
|
|
468
|
+
if (typeof item.output === 'string') {
|
|
469
|
+
outputParts = [{ type: 'input_text', text: item.output }]
|
|
470
|
+
} else if (Array.isArray(item.output)) {
|
|
471
|
+
for (const part of item.output) {
|
|
472
|
+
if (part === null || typeof part !== 'object' || Array.isArray(part) ||
|
|
473
|
+
(part.type !== 'dsh_image_attachment' &&
|
|
474
|
+
((part.type !== 'input_text' && part.type !== 'output_text') || typeof part.text !== 'string'))) {
|
|
475
|
+
throw portableReplayUnsupportedContentError(part?.type)
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
outputParts = item.output
|
|
479
|
+
} else {
|
|
480
|
+
return undefined
|
|
481
|
+
}
|
|
482
|
+
const textLength = outputParts.reduce((sum, part) => sum + (typeof part.text === 'string' ? part.text.length : 0), 0)
|
|
483
|
+
if (textLength > PORTABLE_REPLAY_TEXT_MAX_CHARS) return undefined
|
|
484
|
+
return { callId: item.call_id, outputParts }
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function portableImageCapabilityError() {
|
|
488
|
+
const error = new Error('LCX Compact cannot determine whether the target model accepts checkpoint images')
|
|
489
|
+
error.code = 'LCX_COMPACT_IMAGE_CAPABILITY_UNKNOWN'
|
|
490
|
+
return error
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function portableImageBlock(part, imageSupport) {
|
|
494
|
+
if (!part?.attachment || typeof part.attachment !== 'object' || Array.isArray(part.attachment)) {
|
|
495
|
+
throw portableReplayUnsupportedContentError('image')
|
|
496
|
+
}
|
|
497
|
+
if (imageSupport === 'unknown') throw portableImageCapabilityError()
|
|
498
|
+
return imageSupport === 'supported'
|
|
499
|
+
? { type: 'image', attachment: structuredClone(part.attachment) }
|
|
500
|
+
: { type: 'text', text: UNSUPPORTED_IMAGE_PLACEHOLDER }
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function dshBlocksContainImage(blocks) {
|
|
504
|
+
return (blocks ?? []).some((block) => block?.type === 'image' ||
|
|
505
|
+
(block?.type === 'tool-result' && dshBlocksContainImage(block.content)))
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function dshMessagesContainImage(messages) {
|
|
509
|
+
return (messages ?? []).some((message) => dshBlocksContainImage(message?.content))
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function portableContentBlocks(parts, expectedTypes, budget, imageSupport) {
|
|
513
|
+
const content = []
|
|
514
|
+
let pendingText = ''
|
|
515
|
+
const flushText = () => {
|
|
516
|
+
if (!pendingText) return
|
|
517
|
+
const text = boundedReplayText(pendingText, budget)
|
|
518
|
+
if (text !== undefined) content.push({ type: 'text', text })
|
|
519
|
+
pendingText = ''
|
|
520
|
+
}
|
|
521
|
+
for (const part of parts ?? []) {
|
|
522
|
+
if (expectedTypes.has(part?.type) && typeof part.text === 'string') {
|
|
523
|
+
pendingText += part.text
|
|
524
|
+
continue
|
|
525
|
+
}
|
|
526
|
+
if (part?.type === 'dsh_image_attachment' && part.attachment) {
|
|
527
|
+
flushText()
|
|
528
|
+
content.push(portableImageBlock(part, imageSupport))
|
|
529
|
+
continue
|
|
530
|
+
}
|
|
531
|
+
throw portableReplayUnsupportedContentError(part?.type)
|
|
532
|
+
}
|
|
533
|
+
flushText()
|
|
534
|
+
return content
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function portableTextMessage(item, budget, checkpointId, imageSupport) {
|
|
538
|
+
if (!isPortableReplayItem(item) || item.type !== 'message' || (item.role !== 'user' && item.role !== 'assistant')) return undefined
|
|
539
|
+
const expectedType = item.role === 'assistant' ? 'output_text' : 'input_text'
|
|
540
|
+
const parts = Array.isArray(item.content) ? item.content : []
|
|
541
|
+
const content = portableContentBlocks(parts, new Set([expectedType]), budget, imageSupport)
|
|
542
|
+
if (content.length === 0) return undefined
|
|
543
|
+
return {
|
|
544
|
+
id: randomUUID(),
|
|
545
|
+
role: item.role,
|
|
546
|
+
content,
|
|
547
|
+
source: checkpointReplaySource(checkpointId),
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function cloneReplayBlock(block, imageSupport) {
|
|
552
|
+
const supported = new Set(['text', 'tool-call', 'tool-result', 'reasoning', 'image'])
|
|
553
|
+
if (!supported.has(block?.type)) throw portableReplayUnsupportedContentError(block?.type)
|
|
554
|
+
if (block.type === 'reasoning') return undefined
|
|
555
|
+
if (block.type === 'image') return portableImageBlock({ attachment: block.attachment }, imageSupport)
|
|
556
|
+
if (block.type === 'tool-result') {
|
|
557
|
+
return {
|
|
558
|
+
...structuredClone(block),
|
|
559
|
+
content: (block.content ?? []).map((part) => cloneReplayBlock(part, imageSupport)).filter(Boolean),
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
return structuredClone(block)
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function cloneReplayTail(messages, imageSupport = 'unknown') {
|
|
566
|
+
const projected = []
|
|
567
|
+
for (const message of messages ?? []) {
|
|
568
|
+
if (message === null || typeof message !== 'object' || Array.isArray(message) ||
|
|
569
|
+
typeof message.role !== 'string' || !Array.isArray(message.content)) continue
|
|
570
|
+
const content = []
|
|
571
|
+
for (const block of message.content) {
|
|
572
|
+
const cloned = cloneReplayBlock(block, imageSupport)
|
|
573
|
+
if (cloned) content.push(cloned)
|
|
574
|
+
}
|
|
575
|
+
if (content.length === 0) continue
|
|
576
|
+
projected.push({ ...structuredClone(message), content })
|
|
577
|
+
}
|
|
578
|
+
return projected
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function portableResponsesToMessages(input, options = {}) {
|
|
582
|
+
const budget = options.budget ?? replayBudget()
|
|
583
|
+
const checkpointId = options.checkpointId
|
|
584
|
+
const imageSupport = options.imageSupport ?? 'unknown'
|
|
585
|
+
const items = Array.isArray(input) ? input : []
|
|
586
|
+
for (const item of items) {
|
|
587
|
+
if (!isPortableReplayItem(item)) continue
|
|
588
|
+
if (item.type === 'message') {
|
|
589
|
+
const expectedType = item.role === 'assistant' ? 'output_text' : item.role === 'user' ? 'input_text' : undefined
|
|
590
|
+
if (!expectedType || !Array.isArray(item.content) || item.content.some((part) =>
|
|
591
|
+
part?.type !== expectedType && part?.type !== 'dsh_image_attachment')) {
|
|
592
|
+
throw portableReplayUnsupportedContentError(item.type)
|
|
593
|
+
}
|
|
594
|
+
} else if (item.type !== 'function_call' && item.type !== 'function_call_output') {
|
|
595
|
+
throw portableReplayUnsupportedContentError(item.type)
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
const calls = new Map()
|
|
599
|
+
const outputs = new Map()
|
|
600
|
+
for (let index = 0; index < items.length; index += 1) {
|
|
601
|
+
const call = safePortableCall(items[index])
|
|
602
|
+
const output = safePortableOutput(items[index])
|
|
603
|
+
if (call) calls.set(call.callId, [...(calls.get(call.callId) ?? []), { ...call, index }])
|
|
604
|
+
if (output) outputs.set(output.callId, [...(outputs.get(output.callId) ?? []), { ...output, index }])
|
|
605
|
+
}
|
|
606
|
+
const pairs = new Map()
|
|
607
|
+
for (const [callId, callItems] of calls) {
|
|
608
|
+
const outputItems = outputs.get(callId)
|
|
609
|
+
if (callItems.length === 1 && outputItems?.length === 1 && callItems[0].index < outputItems[0].index) {
|
|
610
|
+
pairs.set(callId, { call: callItems[0], output: outputItems[0] })
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
const messages = []
|
|
615
|
+
const reservedPairs = new Map()
|
|
616
|
+
for (const item of items) {
|
|
617
|
+
const textMessage = portableTextMessage(item, budget, checkpointId, imageSupport)
|
|
618
|
+
if (textMessage) {
|
|
619
|
+
messages.push(textMessage)
|
|
620
|
+
continue
|
|
621
|
+
}
|
|
622
|
+
const call = safePortableCall(item)
|
|
623
|
+
const pair = call ? pairs.get(call.callId) : undefined
|
|
624
|
+
if (pair) {
|
|
625
|
+
const before = { ...budget }
|
|
626
|
+
const argumentsText = exactReplayText(pair.call.arguments, budget)
|
|
627
|
+
let outputContent = portableContentBlocks(
|
|
628
|
+
pair.output.outputParts,
|
|
629
|
+
new Set(['input_text', 'output_text']),
|
|
630
|
+
budget,
|
|
631
|
+
imageSupport,
|
|
632
|
+
)
|
|
633
|
+
if (outputContent.length === 0) {
|
|
634
|
+
const emptyOutput = exactReplayText('(no output)', budget)
|
|
635
|
+
outputContent = emptyOutput ? [{ type: 'text', text: emptyOutput }] : []
|
|
636
|
+
}
|
|
637
|
+
if (!argumentsText || outputContent.length === 0) {
|
|
638
|
+
budget.chars = before.chars
|
|
639
|
+
budget.bytes = before.bytes
|
|
640
|
+
continue
|
|
641
|
+
}
|
|
642
|
+
reservedPairs.set(call.callId, outputContent)
|
|
643
|
+
messages.push({
|
|
644
|
+
id: randomUUID(),
|
|
645
|
+
role: 'assistant',
|
|
646
|
+
content: [{ type: 'tool-call', id: call.callId, name: call.name, arguments: argumentsText }],
|
|
647
|
+
source: checkpointReplaySource(checkpointId),
|
|
648
|
+
})
|
|
649
|
+
continue
|
|
650
|
+
}
|
|
651
|
+
const output = safePortableOutput(item)
|
|
652
|
+
const outputContent = output ? reservedPairs.get(output.callId) : undefined
|
|
653
|
+
if (output && outputContent !== undefined) {
|
|
654
|
+
messages.push({
|
|
655
|
+
id: randomUUID(),
|
|
656
|
+
role: 'user',
|
|
657
|
+
content: [{ type: 'tool-result', toolCallId: output.callId, content: outputContent }],
|
|
658
|
+
source: checkpointReplaySource(checkpointId),
|
|
659
|
+
})
|
|
660
|
+
reservedPairs.delete(output.callId)
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
return messages
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
function portableReplayRouteError(record, route) {
|
|
667
|
+
const dimensions = []
|
|
668
|
+
if (record.provider !== String(route.provider ?? '')) dimensions.push('provider')
|
|
669
|
+
if (record.baseURLFingerprint !== baseURLFingerprint(route.baseURL)) dimensions.push('endpoint')
|
|
670
|
+
const sessionId = String(route.sessionId ?? '')
|
|
671
|
+
const sameSessionLineage = !sessionId || record.lineageId === sessionId ||
|
|
672
|
+
(Array.isArray(route.ancestorSessionIds) && route.ancestorSessionIds.includes(record.lineageId))
|
|
673
|
+
if (!sameSessionLineage) dimensions.push('session')
|
|
674
|
+
const error = new Error(`LCX v3 checkpoint cannot migrate; route mismatch dimensions: ${(dimensions.length > 0 ? dimensions : ['route']).join(', ')}`)
|
|
675
|
+
error.code = 'LCX_CHECKPOINT_ROUTE_MISMATCH'
|
|
676
|
+
return error
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
function buildPortableReplayMessages(messages, store, route, options = {}) {
|
|
680
|
+
const marker = latestPortableMarker(messages)
|
|
681
|
+
if (!marker) return cloneReplayTail(messages)
|
|
682
|
+
const record = store?.get?.(marker.id)
|
|
683
|
+
if (!record) {
|
|
684
|
+
const error = new Error(`LCX v3 checkpoint ${marker.id} is missing from ${store?.file ?? 'checkpoint store'}`)
|
|
685
|
+
error.code = 'LCX_CHECKPOINT_V3_CORRUPT'
|
|
686
|
+
throw error
|
|
687
|
+
}
|
|
688
|
+
const exactRoute = Boolean(route.sessionId) && record.routeFingerprint === routeFingerprint(route)
|
|
689
|
+
const sessionId = String(route.sessionId ?? '')
|
|
690
|
+
const sameSessionLineage = !sessionId || record.lineageId === sessionId ||
|
|
691
|
+
(Array.isArray(route.ancestorSessionIds) && route.ancestorSessionIds.includes(record.lineageId))
|
|
692
|
+
const portableRoute = record.provider === String(route.provider ?? '') &&
|
|
693
|
+
record.baseURLFingerprint === baseURLFingerprint(route.baseURL) &&
|
|
694
|
+
sameSessionLineage
|
|
695
|
+
if (!exactRoute && !portableRoute) throw portableReplayRouteError(record, route)
|
|
696
|
+
const portableImages = Number(record.portableImageCount ?? 0)
|
|
697
|
+
const durableImages = inputImageCount(record.portableHistory)
|
|
698
|
+
if (portableImages > durableImages) {
|
|
699
|
+
throw portableReplayUnsupportedContentError('image')
|
|
700
|
+
}
|
|
701
|
+
const imageSupport = options.imageSupport ?? 'unknown'
|
|
702
|
+
const budget = replayBudget()
|
|
703
|
+
const summary = boundedReplayText(record.portableSummary, budget)
|
|
704
|
+
const recalled = []
|
|
705
|
+
if (summary) recalled.push({
|
|
706
|
+
id: randomUUID(),
|
|
707
|
+
role: 'assistant',
|
|
708
|
+
content: [{ type: 'text', text: summary }],
|
|
709
|
+
source: checkpointReplaySource(marker.id),
|
|
710
|
+
})
|
|
711
|
+
recalled.push(...portableResponsesToMessages(record.portableHistory, { budget, checkpointId: marker.id, imageSupport }))
|
|
712
|
+
recalled.push(...cloneReplayTail((messages ?? []).slice(marker.index + 1), imageSupport))
|
|
713
|
+
return recalled
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
function checkpointReplayUnavailableError() {
|
|
717
|
+
const error = new Error('LCX checkpoint replay requires the public DSH ctx.llm.stream API')
|
|
718
|
+
error.code = CHECKPOINT_REPLAY_UNAVAILABLE_CODE
|
|
719
|
+
return error
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
function checkpointReplayRecord(messages, store) {
|
|
723
|
+
const marker = latestPortableMarker(messages)
|
|
724
|
+
if (!marker) return undefined
|
|
725
|
+
const record = store?.get?.(marker.id)
|
|
726
|
+
if (!record) {
|
|
727
|
+
const error = new Error(`LCX v3 checkpoint ${marker.id} is missing from ${store?.file ?? 'checkpoint store'}`)
|
|
728
|
+
error.code = 'LCX_CHECKPOINT_V3_CORRUPT'
|
|
729
|
+
throw error
|
|
730
|
+
}
|
|
731
|
+
return { marker, record }
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
function nativeReplayBody(options, route, nativeOutput, tailInput) {
|
|
735
|
+
return {
|
|
736
|
+
model: options.model,
|
|
737
|
+
input: [...structuredClone(nativeOutput), ...structuredClone(tailInput)],
|
|
738
|
+
stream: true,
|
|
739
|
+
store: false,
|
|
740
|
+
...(options.tools !== undefined ? { tools: responsesTools(options.tools) } : {}),
|
|
741
|
+
...(options.system !== undefined ? { instructions: options.system } : {}),
|
|
742
|
+
...(promptCacheKey(route) ? { prompt_cache_key: promptCacheKey(route) } : {}),
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
async function* nativeCheckpointReplayStream(options, config, portableStore, ctx, record, marker) {
|
|
747
|
+
const route = routeWithSessionAncestry(ctx, currentRoute(options, config))
|
|
748
|
+
assertCheckpointRoute(record, route)
|
|
749
|
+
const llm = ctx?.llm ?? ctx?.get?.('llm')
|
|
750
|
+
const imageSupport = typeof llm?.resolveModelInfo === 'function'
|
|
751
|
+
? await resolveModelImageSupport(llm, route, options.signal)
|
|
752
|
+
: 'supported'
|
|
753
|
+
const imageOptions = { resolveImage: attachmentImageResolver(ctx), imageSupport, signal: options.signal, maxRequestImageBytes: config.maxRequestImageBytes }
|
|
754
|
+
const nativeOutput = await hydrateNativeImageReferences(record.nativeOutput, imageOptions)
|
|
755
|
+
const tailInput = await buildPortableResponsesInputWithImages(
|
|
756
|
+
(options.messages ?? []).slice(marker.index + 1),
|
|
757
|
+
portableStore,
|
|
758
|
+
route,
|
|
759
|
+
imageOptions,
|
|
760
|
+
)
|
|
761
|
+
const replaySignals = replayEffectiveSignal(options.signal, config.timeoutMs)
|
|
762
|
+
try {
|
|
763
|
+
const response = await fetchSse(
|
|
764
|
+
`${config.baseURL}/responses`,
|
|
765
|
+
nativeReplayBody(options, route, nativeOutput, tailInput),
|
|
766
|
+
mergeFeatureHeader(await authenticatedHeaders(ctx, config, options.sessionId)),
|
|
767
|
+
replaySignals.signal,
|
|
768
|
+
config.timeoutMs + 1000,
|
|
769
|
+
{ maxResponseBytes: config.maxResponseBytes },
|
|
770
|
+
)
|
|
771
|
+
yield* responsesSseChunks(response, {
|
|
772
|
+
signal: replaySignals.signal,
|
|
773
|
+
requestSignal: options.signal,
|
|
774
|
+
timeoutSignal: replaySignals.timeoutSignal,
|
|
775
|
+
timeoutMs: config.timeoutMs,
|
|
776
|
+
maxResponseBytes: config.maxResponseBytes,
|
|
777
|
+
})
|
|
778
|
+
} catch (error) {
|
|
779
|
+
if (replaySignals.timeoutSignal.aborted && !options.signal?.aborted) {
|
|
780
|
+
throw replayTimeoutError(config.timeoutMs, error)
|
|
781
|
+
}
|
|
782
|
+
throw error
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
async function* textChunks(text, usage) {
|
|
787
|
+
yield { type: 'block-start', index: 0, blockType: 'text' }
|
|
788
|
+
yield { type: 'text-delta', index: 0, text }
|
|
789
|
+
yield { type: 'block-end', index: 0, block: { type: 'text', text } }
|
|
790
|
+
if (usage) yield { type: 'usage', usage }
|
|
791
|
+
yield { type: 'finish', reason: { kind: 'stop' } }
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
function isCompactionDirective(message) {
|
|
795
|
+
return textOfMessage(message).includes(COMPACTION_DIRECTIVE)
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
function diagnosticEndpoint(baseURL) {
|
|
799
|
+
try {
|
|
800
|
+
const url = new URL(`${String(baseURL).replace(/\/+$/u, '')}/responses`)
|
|
801
|
+
return `${url.origin}${url.pathname}`
|
|
802
|
+
} catch {
|
|
803
|
+
return 'invalid'
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
function logCompactionDiagnostic(ctx, options, config, error, phase) {
|
|
808
|
+
const failure = failureOf(error)
|
|
809
|
+
const status = failure.status === undefined ? '' : ` status=${failure.status}`
|
|
810
|
+
const detail = `dsh-lcx-codex compaction ${phase}: code=${failure.code}${status} provider=${String(options.provider ?? config.provider)} model=${String(options.model ?? config.model)} transport=${config.compactTransport} endpoint=${diagnosticEndpoint(config.baseURL)} inputItems=${Array.isArray(options.messages) ? options.messages.length : 0}`
|
|
811
|
+
try {
|
|
812
|
+
if (typeof ctx?.logger?.error === 'function') ctx.logger.error(detail)
|
|
813
|
+
else if (typeof console?.error === 'function') console.error(detail)
|
|
814
|
+
} catch {
|
|
815
|
+
// Diagnostics must never change compaction behavior.
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
async function prepareRemoteCompaction(options, config, portableStore, ctx, sessionTracker) {
|
|
820
|
+
const last = options.messages?.[options.messages.length - 1]
|
|
821
|
+
const history = last && isCompactionDirective(last) ? options.messages.slice(0, -1) : options.messages
|
|
822
|
+
const route = currentRoute(options, config)
|
|
823
|
+
const lease = sessionTracker?.capture(route.sessionId)
|
|
824
|
+
lease?.assert('request-start')
|
|
825
|
+
const headers = await authenticatedHeaders(ctx, config, options.sessionId)
|
|
826
|
+
responsesTools(options.tools)
|
|
827
|
+
const llm = ctx?.llm ?? ctx?.get?.('llm')
|
|
828
|
+
const imageSupport = typeof llm?.resolveModelInfo === 'function'
|
|
829
|
+
? await resolveModelImageSupport(llm, route, options.signal)
|
|
830
|
+
: 'supported'
|
|
831
|
+
const imageReferences = new Map()
|
|
832
|
+
const input = await buildPortableResponsesInputWithImages(history, portableStore, route, {
|
|
833
|
+
resolveImage: attachmentImageResolver(ctx),
|
|
834
|
+
imageSupport,
|
|
835
|
+
signal: options.signal,
|
|
836
|
+
maxRequestImageBytes: config.maxRequestImageBytes,
|
|
837
|
+
onImageResolved: ({ imageUrl, attachment }) => imageReferences.set(imageUrl, attachment),
|
|
838
|
+
})
|
|
839
|
+
lease?.assert('preflight')
|
|
840
|
+
return { history, route, lease, input, imageReferences, headers }
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
function mergeUsage(...values) {
|
|
844
|
+
const keys = ['inputTokens', 'outputTokens', 'cacheReadTokens', 'cacheWriteTokens', 'reasoningTokens']
|
|
845
|
+
const merged = {}
|
|
846
|
+
for (const key of keys) {
|
|
847
|
+
const total = values.reduce((sum, value) => sum + (Number.isFinite(value?.[key]) ? value[key] : 0), 0)
|
|
848
|
+
if (total > 0) merged[key] = total
|
|
849
|
+
}
|
|
850
|
+
return Object.keys(merged).length > 0 ? merged : undefined
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
function readableSummaryText(text, model, usage) {
|
|
854
|
+
const summary = stripCheckpointMarker(String(text ?? '')).trim().slice(0, 16 * 1024)
|
|
855
|
+
return summary || portableSummaryText(model, usage)
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
function portableMarkerTextWithSummary(id, model, usage, summary) {
|
|
859
|
+
const summaryText = stripCheckpointMarker(String(summary ?? '')).trim().slice(0, 16 * 1024)
|
|
860
|
+
const status = portableSummaryText(model, usage)
|
|
861
|
+
return `${summaryText ? `${summaryText}\n` : ''}${status}\n[dsh-lcx-codex-v3-checkpoint:${id}]`
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
async function commitRemoteCompaction(prepared, result, portableStore, config, summary, usage) {
|
|
865
|
+
const { history, route, lease, input, imageReferences } = prepared
|
|
866
|
+
lease?.assert('commit')
|
|
867
|
+
const persistedNativeOutput = persistNativeImageReferences(result.output, imageReferences)
|
|
868
|
+
const persistedPortableInput = persistNativeImageReferences(input, imageReferences)
|
|
869
|
+
const id = randomUUID()
|
|
870
|
+
const parent = latestPortableMarker(history)
|
|
871
|
+
const portableImageCount = inputImageCount(input)
|
|
872
|
+
const portableSummary = readableSummaryText(summary, route.model, usage)
|
|
873
|
+
portableStore.put(id, {
|
|
874
|
+
version: CHECKPOINT_V3_VERSION,
|
|
875
|
+
checkpointId: id,
|
|
876
|
+
...(parent ? { parentCheckpointId: parent.id } : {}),
|
|
877
|
+
lineageId: route.sessionId || `${route.provider}:${route.baseURL}`,
|
|
878
|
+
sourceSessionId: route.sessionId || `${route.provider}:${route.baseURL}`,
|
|
879
|
+
provider: route.provider,
|
|
880
|
+
model: route.model,
|
|
881
|
+
modelKey: `${route.provider}:${route.model}`,
|
|
882
|
+
transport: config.compactTransport,
|
|
883
|
+
baseURLFingerprint: baseURLFingerprint(route.baseURL),
|
|
884
|
+
routeFingerprint: routeFingerprint(route),
|
|
885
|
+
nativeOutput: persistedNativeOutput,
|
|
886
|
+
nativeCompaction: result.compaction,
|
|
887
|
+
portableHistory: buildPortableHistory(persistedPortableInput),
|
|
888
|
+
...(portableImageCount > 0 ? { portableImageCount } : {}),
|
|
889
|
+
portableSummary,
|
|
890
|
+
createdAt: Date.now(),
|
|
891
|
+
usage,
|
|
892
|
+
})
|
|
893
|
+
return { id, route, usage, text: portableMarkerTextWithSummary(id, route.model, usage, summary) }
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
async function* remoteCompactionStream(options, config, portableStore, ctx, sessionTracker) {
|
|
897
|
+
try {
|
|
898
|
+
const prepared = await prepareRemoteCompaction(options, config, portableStore, ctx, sessionTracker)
|
|
899
|
+
const result = await requestRemoteCompaction(options, prepared.input, config, options.signal, ctx, prepared.route, prepared.headers)
|
|
900
|
+
const committed = await commitRemoteCompaction(prepared, result, portableStore, config, undefined, result.usage)
|
|
901
|
+
yield* textChunks(committed.text, committed.usage)
|
|
902
|
+
} catch (error) {
|
|
903
|
+
logCompactionDiagnostic(ctx, options, config, error, 'failed')
|
|
904
|
+
throw error
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
async function collectLocalCompaction(next) {
|
|
909
|
+
const chunks = []
|
|
910
|
+
const stream = await next()
|
|
911
|
+
for await (const chunk of stream) chunks.push(chunk)
|
|
912
|
+
const text = chunks.filter((chunk) => chunk?.type === 'text-delta' && typeof chunk.text === 'string').map((chunk) => chunk.text).join('')
|
|
913
|
+
const usage = chunks.find((chunk) => chunk?.type === 'usage')?.usage
|
|
914
|
+
return { chunks, text, usage }
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
async function* parallelCompactionStream(options, config, portableStore, ctx, sessionTracker, next, diagnostic) {
|
|
918
|
+
let prepared
|
|
919
|
+
try {
|
|
920
|
+
prepared = await prepareRemoteCompaction(options, config, portableStore, ctx, sessionTracker)
|
|
921
|
+
} catch (error) {
|
|
922
|
+
logCompactionDiagnostic(ctx, options, config, error, 'failed')
|
|
923
|
+
throw error
|
|
924
|
+
}
|
|
925
|
+
const localPromise = collectLocalCompaction(next)
|
|
926
|
+
const remotePromise = requestRemoteCompaction(options, prepared.input, config, options.signal, ctx, prepared.route, prepared.headers)
|
|
927
|
+
.then((result) => ({ result }), (error) => ({ error }))
|
|
928
|
+
const [localOutcome, remoteOutcome] = await Promise.allSettled([localPromise, remotePromise])
|
|
929
|
+
if (remoteOutcome.status === 'fulfilled' && remoteOutcome.value.result) {
|
|
930
|
+
const local = localOutcome.status === 'fulfilled' ? localOutcome.value : { text: '', usage: undefined }
|
|
931
|
+
const usage = mergeUsage(remoteOutcome.value.result.usage, local.usage)
|
|
932
|
+
try {
|
|
933
|
+
const committed = await commitRemoteCompaction(prepared, remoteOutcome.value.result, portableStore, config, local.text, usage)
|
|
934
|
+
yield* textChunks(committed.text, committed.usage)
|
|
935
|
+
return
|
|
936
|
+
} catch (error) {
|
|
937
|
+
logCompactionDiagnostic(ctx, options, config, error, 'failed')
|
|
938
|
+
throw error
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
const remoteError = remoteOutcome.status === 'fulfilled' ? remoteOutcome.value.error : remoteOutcome.reason
|
|
942
|
+
if (options.signal?.aborted || remoteError?.remoteCompactionRequest !== true) throw remoteError
|
|
943
|
+
diagnostic?.(remoteError, 'failed')
|
|
944
|
+
diagnostic?.(remoteError, 'fallback')
|
|
945
|
+
if (localOutcome.status !== 'fulfilled') throw localOutcome.reason
|
|
946
|
+
for (const chunk of localOutcome.value.chunks) yield chunk
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
function replaySseError(message, code = 'LCX_RESPONSES_UPSTREAM_ERROR') {
|
|
950
|
+
const error = new Error(message)
|
|
951
|
+
error.code = code
|
|
952
|
+
return error
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
function replayTimeoutError(timeoutMs, cause) {
|
|
956
|
+
const error = replaySseError(`LCX Responses replay timed out after ${timeoutMs} ms`, 'LCX_TIMEOUT')
|
|
957
|
+
if (cause !== undefined) error.cause = cause
|
|
958
|
+
return error
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
function replayAbortError(options, cause) {
|
|
962
|
+
if (options.timeoutSignal?.aborted && !options.requestSignal?.aborted) {
|
|
963
|
+
return replayTimeoutError(options.timeoutMs, cause)
|
|
964
|
+
}
|
|
965
|
+
return cause ?? options.signal?.reason ?? new Error('request aborted')
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
function replayEffectiveSignal(signal, timeoutMs) {
|
|
969
|
+
const timeoutSignal = AbortSignal.timeout(timeoutMs)
|
|
970
|
+
return {
|
|
971
|
+
timeoutSignal,
|
|
972
|
+
signal: signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal,
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
function replaySseEvent(dataLines) {
|
|
977
|
+
if (dataLines.length === 0) return undefined
|
|
978
|
+
const data = dataLines.join('\n')
|
|
979
|
+
if (data === '[DONE]') return undefined
|
|
980
|
+
try {
|
|
981
|
+
const parsed = JSON.parse(data)
|
|
982
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) throw new Error('event is not an object')
|
|
983
|
+
return parsed
|
|
984
|
+
} catch (error) {
|
|
985
|
+
throw replaySseError(`LCX Responses replay returned malformed SSE JSON: ${String(error)}`, 'LCX_INVALID_SSE')
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
function replayOutputIndex(event, item) {
|
|
990
|
+
const value = event?.output_index ?? item?.output_index
|
|
991
|
+
return Number.isInteger(value) ? value : undefined
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
function replayItemId(event, item) {
|
|
995
|
+
const value = event?.item_id ?? item?.id
|
|
996
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
function replayItemKey(event, item) {
|
|
1000
|
+
const outputIndex = replayOutputIndex(event, item)
|
|
1001
|
+
if (outputIndex !== undefined) return `output:${outputIndex}`
|
|
1002
|
+
const itemId = replayItemId(event, item)
|
|
1003
|
+
return itemId ? `item:${itemId}` : undefined
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
function replayKind(type) {
|
|
1007
|
+
if (type === 'function_call' || type === 'response.function_call' || type === 'tool-call') return 'tool-call'
|
|
1008
|
+
if (type === 'reasoning') return 'reasoning'
|
|
1009
|
+
return 'text'
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
function replayItemText(item, kind) {
|
|
1013
|
+
if (kind === 'reasoning') return responseReasoningText(item)
|
|
1014
|
+
return responseTextParts(item).join('')
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
function replayEnsureOpen(state) {
|
|
1018
|
+
if (state.open) return []
|
|
1019
|
+
state.open = true
|
|
1020
|
+
state.started = true
|
|
1021
|
+
return [{ type: 'block-start', index: state.index, blockType: state.kind }]
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
function replayClose(state, finalText) {
|
|
1025
|
+
const chunks = []
|
|
1026
|
+
if (!state.open) {
|
|
1027
|
+
if (!state.started && typeof finalText === 'string' && finalText.length > 0) chunks.push(...replayEnsureOpen(state))
|
|
1028
|
+
else return []
|
|
1029
|
+
}
|
|
1030
|
+
if (typeof finalText === 'string' && (state.text.length === 0 || finalText.length >= state.text.length)) state.text = finalText
|
|
1031
|
+
state.open = false
|
|
1032
|
+
state.closed = true
|
|
1033
|
+
return [...chunks, {
|
|
1034
|
+
type: 'block-end',
|
|
1035
|
+
index: state.index,
|
|
1036
|
+
block: state.kind === 'tool-call'
|
|
1037
|
+
? { type: 'tool-call', id: state.id, name: state.name, arguments: state.arguments }
|
|
1038
|
+
: { type: state.kind, text: state.text },
|
|
1039
|
+
}]
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
function replayCloseItem(state, item) {
|
|
1043
|
+
const kind = replayKind(item?.type ?? state.kind)
|
|
1044
|
+
state.kind = kind
|
|
1045
|
+
if (kind === 'tool-call') {
|
|
1046
|
+
if (typeof item?.call_id === 'string' && item.call_id.length > 0) state.id = item.call_id
|
|
1047
|
+
if (!state.id) state.id = randomUUID()
|
|
1048
|
+
state.name = String(item?.name ?? state.name ?? '')
|
|
1049
|
+
const finalArguments = typeof item?.arguments === 'string' ? item.arguments : undefined
|
|
1050
|
+
const chunks = []
|
|
1051
|
+
if (!state.started && (state.name || finalArguments)) {
|
|
1052
|
+
chunks.push(...replayEnsureOpen(state))
|
|
1053
|
+
if (finalArguments) {
|
|
1054
|
+
state.arguments = finalArguments
|
|
1055
|
+
chunks.push({ type: 'tool-call-delta', index: state.index, id: state.id, name: state.name, argumentsDelta: finalArguments })
|
|
1056
|
+
}
|
|
1057
|
+
} else if (finalArguments && state.arguments.length === 0) {
|
|
1058
|
+
state.arguments = finalArguments
|
|
1059
|
+
}
|
|
1060
|
+
chunks.push(...replayClose(state))
|
|
1061
|
+
return chunks
|
|
1062
|
+
}
|
|
1063
|
+
const finalText = replayItemText(item, kind)
|
|
1064
|
+
const chunks = []
|
|
1065
|
+
if (!state.started && finalText) {
|
|
1066
|
+
chunks.push(...replayEnsureOpen(state))
|
|
1067
|
+
state.text = finalText
|
|
1068
|
+
chunks.push({ type: kind === 'reasoning' ? 'reasoning-delta' : 'text-delta', index: state.index, text: finalText })
|
|
1069
|
+
}
|
|
1070
|
+
chunks.push(...replayClose(state, finalText))
|
|
1071
|
+
return chunks
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
async function* responsesSseChunks(response, options = {}) {
|
|
1075
|
+
if (!response?.body) throw replaySseError('LCX Responses replay response did not include an SSE body', 'LCX_INVALID_SSE')
|
|
1076
|
+
const maxBytes = options.maxResponseBytes ?? 8 * 1024 * 1024
|
|
1077
|
+
const reader = response.body.getReader()
|
|
1078
|
+
const decoder = new TextDecoder()
|
|
1079
|
+
const states = new Map()
|
|
1080
|
+
let nextIndex = 0
|
|
1081
|
+
let pending = ''
|
|
1082
|
+
let dataLines = []
|
|
1083
|
+
let bytes = 0
|
|
1084
|
+
let sawCompleted = false
|
|
1085
|
+
let hasToolCall = false
|
|
1086
|
+
let completedResponse
|
|
1087
|
+
const cancel = () => { reader.cancel(options.signal?.reason).catch(() => undefined) }
|
|
1088
|
+
options.signal?.addEventListener('abort', cancel, { once: true })
|
|
1089
|
+
|
|
1090
|
+
const stateFor = (event, item, kindHint) => {
|
|
1091
|
+
const outputIndex = replayOutputIndex(event, item)
|
|
1092
|
+
const itemId = replayItemId(event, item)
|
|
1093
|
+
const kind = kindHint ?? replayKind(item?.type)
|
|
1094
|
+
const key = replayItemKey(event, item)
|
|
1095
|
+
let state = key ? states.get(key) : undefined
|
|
1096
|
+
if (!state) {
|
|
1097
|
+
state = [...states.values()].find((candidate) =>
|
|
1098
|
+
!candidate.closed &&
|
|
1099
|
+
((outputIndex !== undefined && candidate.outputIndex === outputIndex) ||
|
|
1100
|
+
(itemId !== undefined && candidate.itemId === itemId) ||
|
|
1101
|
+
(outputIndex === undefined && itemId === undefined && candidate.kind === kind)))
|
|
1102
|
+
}
|
|
1103
|
+
if (!state) {
|
|
1104
|
+
state = {
|
|
1105
|
+
index: nextIndex++,
|
|
1106
|
+
kind,
|
|
1107
|
+
open: false,
|
|
1108
|
+
closed: false,
|
|
1109
|
+
started: false,
|
|
1110
|
+
text: '',
|
|
1111
|
+
arguments: '',
|
|
1112
|
+
id: undefined,
|
|
1113
|
+
itemId: undefined,
|
|
1114
|
+
outputIndex: undefined,
|
|
1115
|
+
name: '',
|
|
1116
|
+
}
|
|
1117
|
+
states.set(key ?? `anonymous:${state.index}`, state)
|
|
1118
|
+
}
|
|
1119
|
+
if (outputIndex !== undefined) state.outputIndex = outputIndex
|
|
1120
|
+
if (itemId !== undefined) state.itemId = itemId
|
|
1121
|
+
if (item?.type) state.kind = replayKind(item.type)
|
|
1122
|
+
if (typeof event?.call_id === 'string' && event.call_id.length > 0) state.id = event.call_id
|
|
1123
|
+
if (typeof item?.call_id === 'string' && item.call_id.length > 0) state.id = item.call_id
|
|
1124
|
+
if (item?.name) state.name = String(item.name)
|
|
1125
|
+
return state
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
const stateForCompleted = (item, outputIndex, consumed) => {
|
|
1129
|
+
const kind = replayKind(item?.type)
|
|
1130
|
+
const itemId = replayItemId(undefined, item)
|
|
1131
|
+
let state = [...states.values()].find((candidate) =>
|
|
1132
|
+
!consumed.has(candidate) &&
|
|
1133
|
+
((outputIndex !== undefined && candidate.outputIndex === outputIndex) ||
|
|
1134
|
+
(itemId !== undefined && candidate.itemId === itemId)))
|
|
1135
|
+
if (!state && itemId === undefined) {
|
|
1136
|
+
state = [...states.values()].find((candidate) => !consumed.has(candidate) && candidate.kind === kind)
|
|
1137
|
+
}
|
|
1138
|
+
if (!state) state = stateFor({ output_index: outputIndex }, item, kind)
|
|
1139
|
+
consumed.add(state)
|
|
1140
|
+
return state
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
const eventChunks = (event) => {
|
|
1144
|
+
if (event?.type === 'error' || event?.type === 'response.failed' || event?.type === 'response.incomplete' || event?.type === 'response.error') {
|
|
1145
|
+
const message = event.error?.message ?? event.response?.error?.message ?? `LCX Responses replay upstream error (${String(event.type)})`
|
|
1146
|
+
throw replaySseError(message)
|
|
1147
|
+
}
|
|
1148
|
+
if (event?.type === 'response.output_item.added') {
|
|
1149
|
+
const item = event.item
|
|
1150
|
+
const state = stateFor(event, item)
|
|
1151
|
+
if (item?.type === 'function_call') hasToolCall = true
|
|
1152
|
+
return []
|
|
1153
|
+
}
|
|
1154
|
+
if (event?.type === 'response.output_text.delta') {
|
|
1155
|
+
const state = stateFor(event, undefined, 'text')
|
|
1156
|
+
const delta = typeof event.delta === 'string' ? event.delta : ''
|
|
1157
|
+
if (!delta) return []
|
|
1158
|
+
const chunks = replayEnsureOpen(state)
|
|
1159
|
+
state.text += delta
|
|
1160
|
+
chunks.push({ type: 'text-delta', index: state.index, text: delta })
|
|
1161
|
+
return chunks
|
|
1162
|
+
}
|
|
1163
|
+
if (event?.type === 'response.output_text.done') {
|
|
1164
|
+
const state = stateFor(event, undefined, 'text')
|
|
1165
|
+
return replayClose(state, typeof event.text === 'string' ? event.text : undefined)
|
|
1166
|
+
}
|
|
1167
|
+
if (event?.type === 'response.function_call_arguments.delta') {
|
|
1168
|
+
const state = stateFor(event, undefined, 'tool-call')
|
|
1169
|
+
state.kind = 'tool-call'
|
|
1170
|
+
if (typeof event.call_id === 'string' && event.call_id.length > 0) state.id = event.call_id
|
|
1171
|
+
if (!state.id) state.id = randomUUID()
|
|
1172
|
+
if (typeof event.name === 'string') state.name = event.name
|
|
1173
|
+
const delta = typeof event.delta === 'string' ? event.delta : ''
|
|
1174
|
+
if (!delta) return []
|
|
1175
|
+
hasToolCall = true
|
|
1176
|
+
const chunks = replayEnsureOpen(state)
|
|
1177
|
+
state.arguments += delta
|
|
1178
|
+
chunks.push({ type: 'tool-call-delta', index: state.index, id: state.id, name: state.name, argumentsDelta: delta })
|
|
1179
|
+
return chunks
|
|
1180
|
+
}
|
|
1181
|
+
if (event?.type === 'response.function_call_arguments.done') {
|
|
1182
|
+
const state = stateFor(event, undefined, 'tool-call')
|
|
1183
|
+
state.kind = 'tool-call'
|
|
1184
|
+
hasToolCall = true
|
|
1185
|
+
if (typeof event.call_id === 'string' && event.call_id.length > 0) state.id = event.call_id
|
|
1186
|
+
if (!state.id) state.id = randomUUID()
|
|
1187
|
+
if (typeof event.name === 'string') state.name = event.name
|
|
1188
|
+
const chunks = []
|
|
1189
|
+
const finalArguments = typeof event.arguments === 'string' ? event.arguments : ''
|
|
1190
|
+
if (!state.started && (state.name || finalArguments)) {
|
|
1191
|
+
chunks.push(...replayEnsureOpen(state))
|
|
1192
|
+
if (finalArguments) {
|
|
1193
|
+
state.arguments = finalArguments
|
|
1194
|
+
chunks.push({ type: 'tool-call-delta', index: state.index, id: state.id, name: state.name, argumentsDelta: finalArguments })
|
|
1195
|
+
}
|
|
1196
|
+
} else if (finalArguments && state.arguments.length === 0) {
|
|
1197
|
+
state.arguments = finalArguments
|
|
1198
|
+
}
|
|
1199
|
+
chunks.push(...replayClose(state))
|
|
1200
|
+
return chunks
|
|
1201
|
+
}
|
|
1202
|
+
if (event?.type === 'response.output_item.done') {
|
|
1203
|
+
const state = stateFor(event, event.item)
|
|
1204
|
+
if (event.item?.type === 'function_call' || state.kind === 'tool-call') hasToolCall = true
|
|
1205
|
+
if (state.kind === 'text') {
|
|
1206
|
+
const finalText = replayItemText(event.item, 'text')
|
|
1207
|
+
if (finalText && [...states.values()].some((candidate) =>
|
|
1208
|
+
candidate !== state && candidate.closed && candidate.kind === 'text' && candidate.text === finalText)) return []
|
|
1209
|
+
}
|
|
1210
|
+
return replayCloseItem(state, event.item)
|
|
1211
|
+
}
|
|
1212
|
+
if (event?.type === 'response.completed') {
|
|
1213
|
+
sawCompleted = true
|
|
1214
|
+
completedResponse = event.response && typeof event.response === 'object' ? event.response : {}
|
|
1215
|
+
const chunks = []
|
|
1216
|
+
const completedStates = new Set()
|
|
1217
|
+
if (Array.isArray(completedResponse.output)) {
|
|
1218
|
+
for (let outputIndex = 0; outputIndex < completedResponse.output.length; outputIndex += 1) {
|
|
1219
|
+
const item = completedResponse.output[outputIndex]
|
|
1220
|
+
if (item?.type === 'function_call') hasToolCall = true
|
|
1221
|
+
const completedText = replayItemText(item, 'text')
|
|
1222
|
+
if (completedText && [...states.values()].some((candidate) =>
|
|
1223
|
+
candidate.closed && candidate.kind === 'text' && candidate.text === completedText)) {
|
|
1224
|
+
continue
|
|
1225
|
+
}
|
|
1226
|
+
const state = stateForCompleted(item, outputIndex, completedStates)
|
|
1227
|
+
// A provider may emit output_text.done/output_item.done before the
|
|
1228
|
+
// authoritative response.completed event. The completed event closes
|
|
1229
|
+
// the response, but must not create a second visible DSH block for an
|
|
1230
|
+
// item that has already been closed on the stream.
|
|
1231
|
+
if (state.closed) continue
|
|
1232
|
+
// Some gateways change output indexes or omit item ids between
|
|
1233
|
+
// streaming events and response.completed. If the completed text
|
|
1234
|
+
// exactly matches an already closed text state, it is the same
|
|
1235
|
+
// visible item and must not be projected a second time.
|
|
1236
|
+
chunks.push(...replayCloseItem(state, item))
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
for (const state of states.values()) {
|
|
1240
|
+
if (!completedStates.has(state)) chunks.push(...replayClose(state))
|
|
1241
|
+
}
|
|
1242
|
+
const usage = usageFrom(completedResponse)
|
|
1243
|
+
if (usage) chunks.push({ type: 'usage', usage })
|
|
1244
|
+
chunks.push({ type: 'finish', reason: { kind: hasToolCall ? 'tool-calls' : 'stop' } })
|
|
1245
|
+
return chunks
|
|
1246
|
+
}
|
|
1247
|
+
if (event?.type === 'response.reasoning_summary_text.delta' || event?.type === 'response.reasoning_text.delta' || event?.type === 'response.reasoning.delta') {
|
|
1248
|
+
const state = stateFor(event, undefined, 'reasoning')
|
|
1249
|
+
state.kind = 'reasoning'
|
|
1250
|
+
const delta = typeof event.delta === 'string' ? event.delta : ''
|
|
1251
|
+
if (!delta) return []
|
|
1252
|
+
const chunks = replayEnsureOpen(state)
|
|
1253
|
+
state.text += delta
|
|
1254
|
+
chunks.push({ type: 'reasoning-delta', index: state.index, text: delta })
|
|
1255
|
+
return chunks
|
|
1256
|
+
}
|
|
1257
|
+
if (event?.type === 'response.reasoning_summary_text.done' || event?.type === 'response.reasoning_text.done' || event?.type === 'response.reasoning.done') {
|
|
1258
|
+
const state = stateFor(event, undefined, 'reasoning')
|
|
1259
|
+
state.kind = 'reasoning'
|
|
1260
|
+
return replayClose(state, typeof event.text === 'string' ? event.text : undefined)
|
|
1261
|
+
}
|
|
1262
|
+
return []
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
const dispatch = () => {
|
|
1266
|
+
const event = replaySseEvent(dataLines)
|
|
1267
|
+
dataLines = []
|
|
1268
|
+
return event ? eventChunks(event) : []
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
try {
|
|
1272
|
+
while (true) {
|
|
1273
|
+
if (options.signal?.aborted) throw replayAbortError(options)
|
|
1274
|
+
const result = await reader.read()
|
|
1275
|
+
if (result.done) break
|
|
1276
|
+
bytes += result.value.byteLength
|
|
1277
|
+
if (bytes > maxBytes) throw replaySseError(`LCX Responses replay SSE response exceeds ${maxBytes} bytes`, 'LCX_RESPONSE_TOO_LARGE')
|
|
1278
|
+
pending += decoder.decode(result.value, { stream: true })
|
|
1279
|
+
let newline
|
|
1280
|
+
while ((newline = pending.indexOf('\n')) >= 0) {
|
|
1281
|
+
const line = pending.slice(0, newline).replace(/\r$/u, '')
|
|
1282
|
+
pending = pending.slice(newline + 1)
|
|
1283
|
+
if (line === '') {
|
|
1284
|
+
for (const chunk of dispatch()) yield chunk
|
|
1285
|
+
} else if (!line.startsWith(':')) {
|
|
1286
|
+
if (line.startsWith('data:')) dataLines.push(line.slice(5).replace(/^ /u, ''))
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
pending += decoder.decode()
|
|
1291
|
+
if (pending.length > 0) {
|
|
1292
|
+
const line = pending.replace(/\r$/u, '')
|
|
1293
|
+
if (line.startsWith('data:')) dataLines.push(line.slice(5).replace(/^ /u, ''))
|
|
1294
|
+
}
|
|
1295
|
+
for (const chunk of dispatch()) yield chunk
|
|
1296
|
+
} catch (error) {
|
|
1297
|
+
throw replayAbortError(options, error)
|
|
1298
|
+
} finally {
|
|
1299
|
+
options.signal?.removeEventListener('abort', cancel)
|
|
1300
|
+
await reader.cancel().catch(() => undefined)
|
|
1301
|
+
}
|
|
1302
|
+
if (!sawCompleted && options.timeoutSignal?.aborted && !options.requestSignal?.aborted) {
|
|
1303
|
+
throw replayTimeoutError(options.timeoutMs)
|
|
1304
|
+
}
|
|
1305
|
+
if (!sawCompleted) throw replaySseError('LCX Responses replay SSE ended without response.completed', 'LCX_INCOMPLETE_SSE')
|
|
1306
|
+
if (!completedResponse) throw replaySseError('LCX Responses replay did not return a completed response', 'LCX_INCOMPLETE_SSE')
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
function failureOf(error) {
|
|
1310
|
+
return {
|
|
1311
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1312
|
+
code: typeof error?.code === 'string' ? error.code : 'LCX_CODEX_ERROR',
|
|
1313
|
+
...(Number.isInteger(error?.status) ? { status: error.status } : {}),
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
function guardedStream(factory, signal) {
|
|
1318
|
+
return (async function* () {
|
|
1319
|
+
try {
|
|
1320
|
+
yield* factory()
|
|
1321
|
+
} catch (error) {
|
|
1322
|
+
const failure = failureOf(error)
|
|
1323
|
+
yield {
|
|
1324
|
+
type: 'finish',
|
|
1325
|
+
reason: signal?.aborted ? { kind: 'aborted', failure } : { kind: 'error', failure },
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
})()
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
async function executeHostedSearch(ctx, config, args, signal) {
|
|
1332
|
+
abortIfNeeded(signal)
|
|
1333
|
+
const normalized = normalizeHostedSearchArgs(args)
|
|
1334
|
+
const requestId = randomUUID()
|
|
1335
|
+
try {
|
|
1336
|
+
const response = await fetchJsonWithRetry(
|
|
1337
|
+
`${config.baseURL}/responses`,
|
|
1338
|
+
buildHostedSearchBody(normalized, config.model),
|
|
1339
|
+
await authenticatedHeaders(ctx, config, undefined, requestId),
|
|
1340
|
+
signal,
|
|
1341
|
+
config.timeoutMs,
|
|
1342
|
+
{ maxAttempts: config.maxAttempts, maxResponseBytes: config.maxResponseBytes },
|
|
1343
|
+
)
|
|
1344
|
+
return parseHostedSearchResponse(response, requestId, config.webMaxResults)
|
|
1345
|
+
} catch (error) {
|
|
1346
|
+
if (signal?.aborted) throw webError('LCX hosted Web Search aborted', 'LCX_WEB_ABORTED', error)
|
|
1347
|
+
if (error?.code === 'LCX_TIMEOUT') throw webError(String(error), 'LCX_WEB_TIMEOUT', error)
|
|
1348
|
+
if (error?.code?.startsWith?.('WEB_')) throw error
|
|
1349
|
+
throw webError(String(error), 'LCX_WEB_PROVIDER_ERROR', error)
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
function configuredResponsesRoute(ctx, config) {
|
|
1354
|
+
const normalized = normalizeConfig(config)
|
|
1355
|
+
return resolveResponsesRouteConfig(ctx, { provider: normalized.provider, model: normalized.model }, normalized)
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
function currentAlphaCapabilityFingerprint(config, model = config.model) {
|
|
1359
|
+
return alphaCapabilityFingerprint({
|
|
1360
|
+
baseURL: config.baseURL,
|
|
1361
|
+
provider: config.provider,
|
|
1362
|
+
model,
|
|
1363
|
+
profile: config.alphaProfile,
|
|
1364
|
+
group: config.alphaGroup,
|
|
1365
|
+
schemaFingerprint: ALPHA_SCHEMA_FINGERPRINT,
|
|
1366
|
+
})
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
function isHttpReference(value) {
|
|
1370
|
+
try {
|
|
1371
|
+
return ['http:', 'https:'].includes(new URL(value).protocol)
|
|
1372
|
+
} catch {
|
|
1373
|
+
return false
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
function alphaUnavailable(message = 'Alpha Web Search action is unavailable for the verified deployment capability') {
|
|
1378
|
+
return webError(message, 'LCX_ALPHA_ACTION_UNAVAILABLE')
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
function activeAgentRoute(agent) {
|
|
1382
|
+
const requestContext = agent?.session?.requestContext?.()
|
|
1383
|
+
const requestConfig = agent?.session?.requestHeader?.()?.config
|
|
1384
|
+
return {
|
|
1385
|
+
provider: requestContext?.provider ?? requestConfig?.provider ?? agent?.options?.provider,
|
|
1386
|
+
model: requestContext?.model ?? requestConfig?.model ?? agent?.options?.model,
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
async function executeAlphaSearch(ctx, config, capabilityStore, refStore, args, exec) {
|
|
1391
|
+
abortIfNeeded(exec?.signal)
|
|
1392
|
+
const normalized = normalizeAlphaSearchArgs(args)
|
|
1393
|
+
const sessionId = typeof exec?.agent?.id === 'string' ? exec.agent.id : undefined
|
|
1394
|
+
if (!sessionId) throw webError('Alpha Web Search requires a live DSH agent session', 'LCX_ALPHA_SESSION_REQUIRED')
|
|
1395
|
+
const { provider: agentProvider, model: agentModel } = activeAgentRoute(exec?.agent)
|
|
1396
|
+
const routeConfig = resolveResponsesRouteConfig(ctx, {
|
|
1397
|
+
provider: agentProvider ?? config.provider,
|
|
1398
|
+
model: agentModel ?? config.model,
|
|
1399
|
+
}, config)
|
|
1400
|
+
if (!routeConfig) throw alphaUnavailable('Alpha Web Search requires an active DSH GPT openai-responses model route')
|
|
1401
|
+
const fingerprint = currentAlphaCapabilityFingerprint(routeConfig)
|
|
1402
|
+
const capability = capabilityStore.get(fingerprint)
|
|
1403
|
+
if (!alphaCapabilityUsable(capability) || capability.schemaFingerprint !== ALPHA_SCHEMA_FINGERPRINT || capability.actions?.[normalized.action] !== 'supported') {
|
|
1404
|
+
throw alphaUnavailable()
|
|
1405
|
+
}
|
|
1406
|
+
if (normalized.refId && !isHttpReference(normalized.refId)) refStore.assertUsable(sessionId, fingerprint, normalized.refId)
|
|
1407
|
+
const requestId = randomUUID()
|
|
1408
|
+
try {
|
|
1409
|
+
const response = await fetchJsonWithRetry(
|
|
1410
|
+
`${routeConfig.baseURL}/alpha/search`,
|
|
1411
|
+
buildAlphaSearchBody(normalized, routeConfig.model, sessionId, true, routeConfig.alphaMaxOutputTokens),
|
|
1412
|
+
await authenticatedHeaders(ctx, routeConfig, sessionId, requestId),
|
|
1413
|
+
exec?.signal,
|
|
1414
|
+
routeConfig.timeoutMs,
|
|
1415
|
+
{ maxAttempts: routeConfig.maxAttempts, maxResponseBytes: routeConfig.maxResponseBytes },
|
|
1416
|
+
)
|
|
1417
|
+
const result = parseAlphaSearchResponse(response, {
|
|
1418
|
+
action: normalized.action,
|
|
1419
|
+
capability: capability.classification,
|
|
1420
|
+
requestId,
|
|
1421
|
+
})
|
|
1422
|
+
if (result.refs.length > 0) {
|
|
1423
|
+
refStore.record(sessionId, fingerprint, result.refs.map((refId) => ({
|
|
1424
|
+
refId,
|
|
1425
|
+
...(result.sources.find((source) => source.refId === refId)?.url ? { url: result.sources.find((source) => source.refId === refId).url } : {}),
|
|
1426
|
+
})))
|
|
1427
|
+
}
|
|
1428
|
+
return result
|
|
1429
|
+
} catch (error) {
|
|
1430
|
+
if (exec?.signal?.aborted) throw webError('LCX Alpha Web Search aborted', 'LCX_ALPHA_ABORTED', error)
|
|
1431
|
+
if (error?.code === 'LCX_TIMEOUT') throw webError('LCX Alpha Web Search timed out', 'LCX_ALPHA_TIMEOUT', error)
|
|
1432
|
+
if (error?.code?.startsWith?.('WEB_') || error?.code?.startsWith?.('LCX_ALPHA_')) throw error
|
|
1433
|
+
if ([404, 405].includes(error?.status) || /channel does not support/iu.test(String(error?.message ?? ''))) throw alphaUnavailable()
|
|
1434
|
+
throw webError('LCX Alpha Web Search provider request failed', 'LCX_ALPHA_PROVIDER_ERROR', error)
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
class LcxResponsesSearchProvider {
|
|
1439
|
+
constructor(ctxOrConfig, maybeConfig, enabled = () => true) {
|
|
1440
|
+
this.ctx = maybeConfig === undefined ? undefined : ctxOrConfig
|
|
1441
|
+
this.config = maybeConfig ?? ctxOrConfig
|
|
1442
|
+
this.enabled = typeof enabled === 'function' ? enabled : () => true
|
|
1443
|
+
this.id = this.config.webSearchProvider
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
available() {
|
|
1447
|
+
const routeConfig = configuredResponsesRoute(this.ctx, this.config)
|
|
1448
|
+
return this.enabled() && Boolean(routeConfig) && URL.canParse(`${routeConfig.baseURL}/responses`)
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
async search(request, signal) {
|
|
1452
|
+
abortIfNeeded(signal)
|
|
1453
|
+
const routeConfig = configuredResponsesRoute(this.ctx, this.config)
|
|
1454
|
+
if (!routeConfig) throw webError('Hosted Web Search requires a configured DSH GPT openai-responses model route', 'LCX_WEB_ROUTE_UNAVAILABLE')
|
|
1455
|
+
const result = await executeHostedSearch(this.ctx, routeConfig, { query: request.query }, signal)
|
|
1456
|
+
const maxResults = Number.isInteger(request.maxResults) && request.maxResults > 0 ? request.maxResults : routeConfig.webMaxResults
|
|
1457
|
+
return {
|
|
1458
|
+
content: `【Responses Hosted 搜索 · LCX】${result.content ? `\n${result.content}` : ''}`,
|
|
1459
|
+
sources: result.sources.slice(0, maxResults),
|
|
1460
|
+
truncated: result.sources.length > maxResults || result.truncated,
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
function createWebSearchGptTool(ctx, state, getConfig) {
|
|
1466
|
+
return {
|
|
1467
|
+
name: WEB_SEARCH_TOOL_NAME,
|
|
1468
|
+
description: 'GPT 专属一次性 Responses Hosted Web Search。支持 query、域名过滤、用户位置、搜索上下文、外部 Web 访问、返回预算和图片搜索控制,并返回来源、引用和检索时间;不提供基于当前会话 ref_id 的 open、find、click 或 screenshot 页面操作。',
|
|
1469
|
+
parameters: HOSTED_SEARCH_PARAMETERS,
|
|
1470
|
+
output: {
|
|
1471
|
+
schema: HOSTED_SEARCH_OUTPUT,
|
|
1472
|
+
render: (_args, value) => renderHostedSearchResult(value),
|
|
1473
|
+
presentationMeta: (_args, value) => ({
|
|
1474
|
+
mode: value.mode,
|
|
1475
|
+
action: value.action,
|
|
1476
|
+
emulation: value.emulation,
|
|
1477
|
+
answer: value.content,
|
|
1478
|
+
sources: value.sources,
|
|
1479
|
+
citations: value.citations,
|
|
1480
|
+
images: value.images,
|
|
1481
|
+
warnings: value.warnings,
|
|
1482
|
+
...(value.outputBlocks ? { outputBlocks: value.outputBlocks } : {}),
|
|
1483
|
+
...(value.domains ? { domains: value.domains } : {}),
|
|
1484
|
+
...(value.lineRange ? { lineRange: value.lineRange } : {}),
|
|
1485
|
+
...(value.requestId ? { requestId: value.requestId } : {}),
|
|
1486
|
+
...(value.responseId ? { responseId: value.responseId } : {}),
|
|
1487
|
+
retrievedAt: value.retrievedAt,
|
|
1488
|
+
truncated: value.truncated,
|
|
1489
|
+
}),
|
|
1490
|
+
},
|
|
1491
|
+
presentCall: (args) => ({
|
|
1492
|
+
card: 'generic',
|
|
1493
|
+
title: `Responses Hosted 搜索:${typeof args?.query === 'string' ? args.query : ''}`,
|
|
1494
|
+
kind: 'search',
|
|
1495
|
+
rawInput: JSON.stringify(args ?? {}),
|
|
1496
|
+
}),
|
|
1497
|
+
async execute(args, exec) {
|
|
1498
|
+
if (!state.enabled || !state.webSearch) throw new Error(`${WEB_SEARCH_TOOL_NAME} 未启用,请先在设置中开启 GPT 原生 Web Search`)
|
|
1499
|
+
const config = getConfig()
|
|
1500
|
+
const { provider, model } = activeAgentRoute(exec?.agent)
|
|
1501
|
+
const routeConfig = resolveResponsesRouteConfig(ctx, {
|
|
1502
|
+
provider: provider ?? config.provider,
|
|
1503
|
+
model: model ?? config.model,
|
|
1504
|
+
}, config)
|
|
1505
|
+
if (!routeConfig) throw webError('Hosted Web Search requires an active DSH GPT openai-responses model route', 'LCX_WEB_ROUTE_UNAVAILABLE')
|
|
1506
|
+
return executeHostedSearch(ctx, routeConfig, args, exec?.signal)
|
|
1507
|
+
},
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
function createWebSearchAlphaTool(ctx, getConfig, capabilityStore, refStore) {
|
|
1512
|
+
return {
|
|
1513
|
+
name: ALPHA_SEARCH_TOOL_NAME,
|
|
1514
|
+
description: '独立且有状态的 Alpha Web Search command 工具。一次只执行一个 search_query、image_query、open、find、click、screenshot、finance、weather、sports 或 time action;open、find、click 和 screenshot 使用当前会话已返回的 ref_id。仅在匹配的 capability probe 已验证时注册。',
|
|
1515
|
+
parameters: ALPHA_SEARCH_PARAMETERS,
|
|
1516
|
+
output: {
|
|
1517
|
+
schema: ALPHA_SEARCH_OUTPUT,
|
|
1518
|
+
render: (_args, value) => renderAlphaSearchResult(value),
|
|
1519
|
+
presentationMeta: (_args, value) => ({
|
|
1520
|
+
mode: value.mode,
|
|
1521
|
+
action: value.action,
|
|
1522
|
+
capability: value.capability,
|
|
1523
|
+
emulation: value.emulation,
|
|
1524
|
+
answer: value.content,
|
|
1525
|
+
sources: value.sources,
|
|
1526
|
+
citations: value.citations,
|
|
1527
|
+
refs: value.refs,
|
|
1528
|
+
links: value.links,
|
|
1529
|
+
pdfRefs: value.pdfRefs,
|
|
1530
|
+
...(value.requestId ? { requestId: value.requestId } : {}),
|
|
1531
|
+
...(value.responseId ? { responseId: value.responseId } : {}),
|
|
1532
|
+
retrievedAt: value.retrievedAt,
|
|
1533
|
+
warnings: value.warnings,
|
|
1534
|
+
}),
|
|
1535
|
+
},
|
|
1536
|
+
presentCall: (args) => ({
|
|
1537
|
+
card: 'generic',
|
|
1538
|
+
title: `Alpha Web Search:${typeof args?.action === 'string' ? args.action : ''}`,
|
|
1539
|
+
kind: 'search',
|
|
1540
|
+
rawInput: JSON.stringify(args ?? {}),
|
|
1541
|
+
}),
|
|
1542
|
+
execute(args, exec) {
|
|
1543
|
+
return executeAlphaSearch(ctx, getConfig(), capabilityStore, refStore, args, exec)
|
|
1544
|
+
},
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
function apply(ctx, rawConfig) {
|
|
1549
|
+
const config = normalizeConfig(rawConfig)
|
|
1550
|
+
const portableStore = new CheckpointV3Store(config.portableCheckpointPath)
|
|
1551
|
+
const originalSearchProvider = ctx.web.searchProviderId
|
|
1552
|
+
const state = {
|
|
1553
|
+
enabled: false,
|
|
1554
|
+
webSearch: false,
|
|
1555
|
+
alphaSearch: false,
|
|
1556
|
+
remoteCompaction: false,
|
|
1557
|
+
fallbackToBasicCompaction: true,
|
|
1558
|
+
}
|
|
1559
|
+
let runtimeConfig = config
|
|
1560
|
+
const provider = new LcxResponsesSearchProvider(ctx, runtimeConfig, () => state.enabled && state.webSearch)
|
|
1561
|
+
ctx.web.registerSearchProvider(provider)
|
|
1562
|
+
const tools = ctx.get?.('tools') ?? ctx.tools
|
|
1563
|
+
const systemPrompt = ctx.get?.('systemPrompt') ?? ctx.systemPrompt
|
|
1564
|
+
const sessionTracker = createSessionGenerationTracker(ctx)
|
|
1565
|
+
let disposeHostedTool
|
|
1566
|
+
let disposeAlphaTool
|
|
1567
|
+
let disposeHostedPrompt
|
|
1568
|
+
let disposeAlphaPrompt
|
|
1569
|
+
let alphaCapabilityStore
|
|
1570
|
+
let alphaRefStore
|
|
1571
|
+
let lastAlphaStoreErrorCode
|
|
1572
|
+
|
|
1573
|
+
const alphaStores = () => {
|
|
1574
|
+
try {
|
|
1575
|
+
alphaCapabilityStore ??= new AlphaCapabilityStore(config.alphaCapabilityPath)
|
|
1576
|
+
alphaRefStore ??= new AlphaRefStore(config.alphaRefPath)
|
|
1577
|
+
return { capability: alphaCapabilityStore, refs: alphaRefStore }
|
|
1578
|
+
} catch (error) {
|
|
1579
|
+
const code = String(error?.code ?? 'LCX_ALPHA_STORE_UNAVAILABLE')
|
|
1580
|
+
if (code !== lastAlphaStoreErrorCode) ctx.logger?.error?.(`[lcx-codex] alpha-store unavailable code=${code}`)
|
|
1581
|
+
lastAlphaStoreErrorCode = code
|
|
1582
|
+
alphaCapabilityStore = undefined
|
|
1583
|
+
alphaRefStore = undefined
|
|
1584
|
+
return undefined
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
const syncToolRegistration = () => {
|
|
1589
|
+
const hostedEnabled = Boolean(state.enabled && state.webSearch)
|
|
1590
|
+
const configuredRoute = configuredResponsesRoute(ctx, runtimeConfig)
|
|
1591
|
+
const fingerprint = configuredRoute ? currentAlphaCapabilityFingerprint(configuredRoute) : undefined
|
|
1592
|
+
const stores = state.enabled && state.alphaSearch ? alphaStores() : undefined
|
|
1593
|
+
let alphaCapability
|
|
1594
|
+
try {
|
|
1595
|
+
alphaCapability = fingerprint ? stores?.capability.get(fingerprint) : undefined
|
|
1596
|
+
if (stores) lastAlphaStoreErrorCode = undefined
|
|
1597
|
+
} catch (error) {
|
|
1598
|
+
const code = String(error?.code ?? 'LCX_ALPHA_STORE_UNAVAILABLE')
|
|
1599
|
+
if (code !== lastAlphaStoreErrorCode) ctx.logger?.error?.(`[lcx-codex] alpha-store unavailable code=${code}`)
|
|
1600
|
+
lastAlphaStoreErrorCode = code
|
|
1601
|
+
}
|
|
1602
|
+
const alphaEnabled = Boolean(stores && alphaCapabilityUsable(alphaCapability) && alphaCapability?.schemaFingerprint === ALPHA_SCHEMA_FINGERPRINT)
|
|
1603
|
+
if (hostedEnabled && !disposeHostedTool && tools?.register) disposeHostedTool = tools.register(createWebSearchGptTool(ctx, state, () => runtimeConfig))
|
|
1604
|
+
if (!hostedEnabled && disposeHostedTool) {
|
|
1605
|
+
disposeHostedTool()
|
|
1606
|
+
disposeHostedTool = undefined
|
|
1607
|
+
}
|
|
1608
|
+
if (alphaEnabled && !disposeAlphaTool && tools?.register) disposeAlphaTool = tools.register(createWebSearchAlphaTool(ctx, () => runtimeConfig, stores.capability, stores.refs))
|
|
1609
|
+
if (!alphaEnabled && disposeAlphaTool) {
|
|
1610
|
+
disposeAlphaTool()
|
|
1611
|
+
disposeAlphaTool = undefined
|
|
1612
|
+
}
|
|
1613
|
+
if (hostedEnabled && !disposeHostedPrompt && systemPrompt?.section) {
|
|
1614
|
+
disposeHostedPrompt = systemPrompt.section({
|
|
1615
|
+
name: 'tool:websearch_gpt',
|
|
1616
|
+
order: 111,
|
|
1617
|
+
text: '使用 websearch_gpt 查询当前或需要来源的问题。该工具走 Responses hosted web_search;最终回答必须引用直接 URL,并区分来源发布时间与检索时间。',
|
|
1618
|
+
}) ?? (() => {})
|
|
1619
|
+
}
|
|
1620
|
+
if (!hostedEnabled && disposeHostedPrompt) {
|
|
1621
|
+
disposeHostedPrompt()
|
|
1622
|
+
disposeHostedPrompt = undefined
|
|
1623
|
+
}
|
|
1624
|
+
if (alphaEnabled && !disposeAlphaPrompt && systemPrompt?.section) {
|
|
1625
|
+
disposeAlphaPrompt = systemPrompt.section({
|
|
1626
|
+
name: 'tool:websearch_alpha',
|
|
1627
|
+
order: 112,
|
|
1628
|
+
text: 'websearch_alpha 是独立 Alpha command API。一次只调用一个 action;只能对当前 session 已返回的 ref_id 执行 open/find/click/screenshot,最终回答引用直接 URL,不暴露内部 ref_id。',
|
|
1629
|
+
}) ?? (() => {})
|
|
1630
|
+
}
|
|
1631
|
+
if (!alphaEnabled && disposeAlphaPrompt) {
|
|
1632
|
+
disposeAlphaPrompt()
|
|
1633
|
+
disposeAlphaPrompt = undefined
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
const sync = (next) => {
|
|
1638
|
+
Object.assign(state, next ?? {})
|
|
1639
|
+
runtimeConfig = normalizeConfig({ ...config, ...next })
|
|
1640
|
+
provider.config = runtimeConfig
|
|
1641
|
+
ctx.web.searchProviderId = state.enabled && state.webSearch ? runtimeConfig.webSearchProvider : originalSearchProvider
|
|
1642
|
+
syncToolRegistration()
|
|
1643
|
+
}
|
|
1644
|
+
const settingsBase = {
|
|
1645
|
+
enabled: false,
|
|
1646
|
+
webSearch: false,
|
|
1647
|
+
alphaSearch: false,
|
|
1648
|
+
remoteCompaction: false,
|
|
1649
|
+
fallbackToBasicCompaction: true,
|
|
1650
|
+
provider: config.provider,
|
|
1651
|
+
baseURL: config.baseURL,
|
|
1652
|
+
apiKeyEnv: config.apiKeyEnv,
|
|
1653
|
+
model: config.model,
|
|
1654
|
+
compactTransport: config.compactTransport,
|
|
1655
|
+
}
|
|
1656
|
+
const attachSettings = (settingsContext) => {
|
|
1657
|
+
const settings = settingsContext.settings ?? settingsContext
|
|
1658
|
+
if (!settings?.register) return
|
|
1659
|
+
const scope = settings.register(settingsNamespace(SETTINGS_NAMESPACE), SettingsSchema, { base: settingsBase })
|
|
1660
|
+
sync(scope.get())
|
|
1661
|
+
scope.watch(() => sync(scope.get()))
|
|
1662
|
+
}
|
|
1663
|
+
if (typeof ctx.inject === 'function') ctx.inject(['settings'], attachSettings)
|
|
1664
|
+
else attachSettings(ctx.get?.('settings') ?? {})
|
|
1665
|
+
|
|
1666
|
+
ctx.effect?.(() => () => {
|
|
1667
|
+
disposeHostedTool?.()
|
|
1668
|
+
disposeAlphaTool?.()
|
|
1669
|
+
disposeHostedPrompt?.()
|
|
1670
|
+
disposeAlphaPrompt?.()
|
|
1671
|
+
sessionTracker.dispose()
|
|
1672
|
+
ctx.web.searchProviderId = originalSearchProvider
|
|
1673
|
+
}, 'lcx-codex: restore web provider selection')
|
|
1674
|
+
ctx.on('llm/stream', (options, next) => {
|
|
1675
|
+
if (checkpointReplayOptions.delete(options)) return next()
|
|
1676
|
+
if (!state.enabled || !state.remoteCompaction) return next()
|
|
1677
|
+
if (options.purpose === 'session-title') return next()
|
|
1678
|
+
const routeConfig = resolveResponsesRouteConfig(ctx, options, runtimeConfig)
|
|
1679
|
+
let portableCheckpoint
|
|
1680
|
+
try {
|
|
1681
|
+
portableCheckpoint = hasPortableCheckpoint(options.messages)
|
|
1682
|
+
} catch (error) {
|
|
1683
|
+
return guardedStream(() => { throw error }, options.signal)
|
|
1684
|
+
}
|
|
1685
|
+
if (portableCheckpoint) {
|
|
1686
|
+
if (!routeConfig) return guardedStream(() => { throw nativeRouteError(options) }, options.signal)
|
|
1687
|
+
if (options.purpose === 'compaction') {
|
|
1688
|
+
const factory = () => remoteCompactionStream(options, routeConfig, portableStore, ctx, sessionTracker)
|
|
1689
|
+
return guardedStream(factory, options.signal)
|
|
1690
|
+
}
|
|
1691
|
+
let replay
|
|
1692
|
+
try {
|
|
1693
|
+
replay = checkpointReplayRecord(options.messages, portableStore)
|
|
1694
|
+
} catch (error) {
|
|
1695
|
+
return guardedStream(() => { throw error }, options.signal)
|
|
1696
|
+
}
|
|
1697
|
+
const route = routeWithSessionAncestry(ctx, currentRoute(options, routeConfig))
|
|
1698
|
+
if (options.sessionId && replay?.record.routeFingerprint === routeFingerprint(route)) {
|
|
1699
|
+
return guardedStream(
|
|
1700
|
+
() => nativeCheckpointReplayStream(options, routeConfig, portableStore, ctx, replay.record, replay.marker),
|
|
1701
|
+
options.signal,
|
|
1702
|
+
)
|
|
1703
|
+
}
|
|
1704
|
+
const llm = ctx?.llm ?? ctx?.get?.('llm')
|
|
1705
|
+
if (!llm || typeof llm.stream !== 'function') {
|
|
1706
|
+
return guardedStream(() => { throw checkpointReplayUnavailableError() }, options.signal)
|
|
1707
|
+
}
|
|
1708
|
+
return guardedStream(async function* () {
|
|
1709
|
+
const markerTail = replay?.marker ? options.messages.slice(replay.marker.index + 1) : []
|
|
1710
|
+
const needsImageCapability = Number(replay?.record.portableImageCount ?? 0) > 0 || dshMessagesContainImage(markerTail)
|
|
1711
|
+
const imageSupport = needsImageCapability
|
|
1712
|
+
? await resolveModelImageSupport(llm, route, options.signal)
|
|
1713
|
+
: 'unknown'
|
|
1714
|
+
const rewrittenOptions = {
|
|
1715
|
+
...options,
|
|
1716
|
+
messages: buildPortableReplayMessages(options.messages, portableStore, route, { imageSupport }),
|
|
1717
|
+
}
|
|
1718
|
+
checkpointReplayOptions.add(rewrittenOptions)
|
|
1719
|
+
const stream = await llm.stream(rewrittenOptions)
|
|
1720
|
+
for await (const chunk of stream) yield chunk
|
|
1721
|
+
}, options.signal)
|
|
1722
|
+
}
|
|
1723
|
+
if (options.purpose === 'compaction') {
|
|
1724
|
+
if (!routeConfig) return next()
|
|
1725
|
+
const factory = () => remoteCompactionStream(options, routeConfig, portableStore, ctx, sessionTracker)
|
|
1726
|
+
const diagnostic = (error, phase) => logCompactionDiagnostic(ctx, options, routeConfig, error, phase)
|
|
1727
|
+
return state.fallbackToBasicCompaction
|
|
1728
|
+
? guardedStream(
|
|
1729
|
+
() => parallelCompactionStream(options, routeConfig, portableStore, ctx, sessionTracker, next, diagnostic),
|
|
1730
|
+
options.signal,
|
|
1731
|
+
)
|
|
1732
|
+
: guardedStream(factory, options.signal)
|
|
1733
|
+
}
|
|
1734
|
+
return next()
|
|
1735
|
+
}, { global: true, prepend: true })
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
export {
|
|
1739
|
+
Config,
|
|
1740
|
+
SettingsSchema,
|
|
1741
|
+
CheckpointV3Store,
|
|
1742
|
+
LcxResponsesSearchProvider,
|
|
1743
|
+
apply,
|
|
1744
|
+
baseURLFingerprint,
|
|
1745
|
+
buildPortableHistory,
|
|
1746
|
+
buildPortableReplayMessages,
|
|
1747
|
+
buildPortableResponsesInput,
|
|
1748
|
+
hasPortableCheckpoint,
|
|
1749
|
+
inject,
|
|
1750
|
+
name,
|
|
1751
|
+
normalizeConfig,
|
|
1752
|
+
normalizeCompactionResponse,
|
|
1753
|
+
normalizeHostedSearchArgs,
|
|
1754
|
+
normalizeAlphaSearchArgs,
|
|
1755
|
+
portableResponsesToMessages,
|
|
1756
|
+
routeFingerprint,
|
|
1757
|
+
}
|