dsh-lcx-codex 0.4.2 → 0.4.3-pre.13

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.
Files changed (39) hide show
  1. package/README.md +75 -224
  2. package/THIRD_PARTY_NOTICES.md +64 -0
  3. package/cordis.patch.yml +3 -20
  4. package/lib/auxiliary-usage.js +63 -0
  5. package/lib/client.js +1398 -167
  6. package/lib/compact-v2.js +218 -199
  7. package/lib/dsh-compat.js +294 -100
  8. package/lib/dsh-responses.js +512 -277
  9. package/lib/grok-native-search.js +391 -0
  10. package/lib/index.js +1066 -758
  11. package/lib/invocation-policy-scope.js +261 -0
  12. package/lib/json-store.js +57 -31
  13. package/lib/native-checkpoint.js +520 -194
  14. package/lib/pi-responses-runtime.js +1571 -0
  15. package/lib/responses-request.js +109 -121
  16. package/lib/responses-stream.js +1280 -447
  17. package/lib/route.js +425 -369
  18. package/lib/search-accounting.js +86 -0
  19. package/lib/search-usage.js +86 -0
  20. package/lib/service-mutex.js +73 -64
  21. package/lib/token-budget.js +176 -108
  22. package/lib/transport.js +308 -68
  23. package/lib/types/client/index.d.ts +18 -0
  24. package/lib/types/client/search-media.d.ts +16 -0
  25. package/lib/types/index.d.ts +83 -0
  26. package/lib/web-run-output.js +189 -18
  27. package/lib/web-search-alpha.js +1067 -163
  28. package/lib/web-search-capability.js +80 -65
  29. package/lib/web-search-hosted.js +321 -33
  30. package/lib/web-search-ref-store.js +145 -60
  31. package/package.json +112 -32
  32. package/ARCHITECTURE.md +0 -117
  33. package/CHANGELOG.md +0 -224
  34. package/README_EN.md +0 -277
  35. package/assets/dsh-lcx-codex-banner.jpg +0 -0
  36. package/lib/legacy-v3.js +0 -20
  37. package/lib/responses-replay.js +0 -68
  38. package/scripts/probe-alpha.mjs +0 -43
  39. package/scripts/validate-dsh-schema.mjs +0 -31
package/lib/route.js CHANGED
@@ -1,399 +1,455 @@
1
- // @ts-check
2
-
3
- import { createHash, randomUUID } from 'node:crypto'
4
- import { attributionHeaders, resolveRetryPolicy } from '@deepseek-ai/dsh-llm'
5
- import { settingsNamespace } from '@deepseek-ai/dsh-settings'
6
- import { getBuiltinModels } from '@earendil-works/pi-ai/providers/all'
7
-
8
- /** @typedef {Record<string, string>} HeaderMap */
9
- /** @typedef {Parameters<typeof resolveRetryPolicy>[0]} RetryPolicyConfig */
10
- /** @typedef {'none' | 'short' | 'long'} CacheRetention */
11
- /** @typedef {{ supportsDeveloperRole?: boolean, sessionAffinityFormat?: 'openai' | 'openai-nosession' | 'openrouter', supportsStrictMode?: boolean, supportsLongCacheRetention?: boolean, supportsOpenAIGrammarTools?: boolean, supportsAdditionalTools?: boolean, supportsToolSearch?: boolean, supportsExplicitPromptCacheMode?: boolean }} ResponsesCompat */
12
- /** @typedef {{ provider: string, model: string, baseURL: string, sessionId: string }} RouteIdentity */
13
- /** @typedef {{ provider?: unknown, model?: unknown, sessionId?: unknown }} RouteOptions */
14
- /** @typedef {{ id?: unknown, compat?: unknown }} ProviderModelProfile */
15
- /**
16
- * @typedef {object} ProviderProfile
17
- * @property {string} [api]
18
- * @property {string} [baseURL]
19
- * @property {string} [apiKeyEnv]
20
- * @property {HeaderMap} [headers]
21
- * @property {unknown} [cacheRetention]
22
- * @property {number} [timeoutMs]
23
- * @property {number} [maxRequestImageBytes]
24
- * @property {number} [requestImagePixelBudget]
25
- * @property {number} [requestImageMaxBytes]
26
- * @property {RetryPolicyConfig} [retryPolicy]
27
- * @property {unknown} [compat]
28
- * @property {ProviderModelProfile[]} [models]
29
- * @property {Record<string, ProviderModelProfile>} [modelOverrides]
30
- */
31
- /** @typedef {{ providers?: Record<string, ProviderProfile> }} LlmSettingsSection */
32
- /** @typedef {{ get?: (namespace: unknown) => LlmSettingsSection | undefined }} SettingsService */
33
- /** @typedef {{ resolve?: (name: string) => Promise<{ value?: unknown } | undefined> }} CredentialsService */
34
- /** @typedef {{ id: string, header?: { parentSession?: string }, requestHeader?: () => RequestHeader | undefined }} RouteSession */
35
- /** @typedef {{ get?: (id: string) => RouteSession | undefined }} SessionsService */
36
- /**
37
- * @typedef {object} RouteContext
38
- * @property {((name: string) => unknown)} [get]
39
- * @property {SettingsService} [settings]
40
- * @property {CredentialsService} [credentials]
41
- * @property {SessionsService} [sessions]
42
- */
43
- /**
44
- * Raw fallback values are not a resolved Responses route. cacheRetention is
45
- * intentionally unknown until resolveResponsesRouteConfig normalizes it.
46
- * @typedef {object} UnresolvedRouteConfig
47
- * @property {string} provider
48
- * @property {string} model
49
- * @property {string} baseURL
50
- * @property {string} apiKeyEnv
51
- * @property {HeaderMap} [headers]
52
- * @property {unknown} [cacheRetention]
53
- * @property {unknown} [supportsLongCacheRetention]
54
- * @property {unknown} [supportsExplicitPromptCacheMode]
55
- * @property {unknown} [responsesCompat]
56
- * @property {number} [timeoutMs]
57
- * @property {number} [maxAttempts]
58
- * @property {number} [maxRequestImageBytes]
59
- * @property {number} [requestImagePixelBudget]
60
- * @property {number} [requestImageMaxBytes]
61
- */
62
- /**
63
- * @typedef {UnresolvedRouteConfig & {
64
- * api: 'openai-responses',
65
- * cacheRetention: CacheRetention,
66
- * supportsLongCacheRetention: boolean,
67
- * responsesCompat?: ResponsesCompat
68
- * }} ResolvedResponsesRoute
69
- */
70
- /** @typedef {{ provider?: unknown, model?: unknown, reasoningEffort?: unknown, temperature?: unknown, maxTokens?: unknown }} RequestHeaderConfig */
71
- /** @typedef {{ config?: RequestHeaderConfig }} RequestHeader */
72
- /** @typedef {{ reasoningEffort?: unknown, temperature?: unknown, maxTokens?: unknown }} GenerationControls */
73
- /** @typedef {{ version: number, provider: unknown, model: unknown, baseURLFingerprint: unknown, sourceSessionId: unknown }} CheckpointRouteRecord */
74
- /** @typedef {Error & { code?: string }} LcxError */
75
-
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { credentialRef } from "@deepseek-ai/dsh-credentials";
3
+ import { SessionId } from "@deepseek-ai/dsh-session";
4
+ import "@deepseek-ai/dsh-settings";
5
+ import { attributionHeaders, resolveRetryPolicy } from "@deepseek-ai/dsh-llm";
6
+ import { getBuiltinModels, getBuiltinProviders, } from "./pi-responses-runtime.js";
7
+ /** @param {unknown} value */
8
+ function isRecord(value) {
9
+ return value !== null && typeof value === "object" && !Array.isArray(value);
10
+ }
11
+ /** @param {unknown} value */
12
+ function isPositiveInteger(value) {
13
+ return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
14
+ }
15
+ function isPositiveFinite(value) {
16
+ return typeof value === "number" && Number.isFinite(value) && value > 0;
17
+ }
18
+ function asLlmSettingsSection(value) {
19
+ if (!isRecord(value))
20
+ return undefined;
21
+ const providers = value.providers;
22
+ if (providers === undefined)
23
+ return {};
24
+ if (!isRecord(providers))
25
+ return undefined;
26
+ return { providers: providers };
27
+ }
76
28
  /** @param {unknown} value */
77
29
  export function normalizeBaseURL(value) {
78
- return String(value ?? '').trim().replace(/\/+$/u, '')
30
+ return String(value ?? "").trim().replace(/\/+$/u, "");
79
31
  }
80
-
81
32
  /** @param {unknown} baseURL */
82
33
  export function baseURLFingerprint(baseURL) {
83
- return createHash('sha256').update(normalizeBaseURL(baseURL), 'utf8').digest('hex')
34
+ return createHash("sha256").update(normalizeBaseURL(baseURL), "utf8").digest("hex");
84
35
  }
85
-
86
- /**
87
- * @param {Partial<RouteIdentity> | null | undefined} route
88
- * @param {{ includeSession?: boolean }} [options]
89
- */
36
+ /** @param {Partial<RouteIdentity> | null | undefined} route @param {{ includeSession?: boolean }} [options] */
90
37
  export function routeFingerprint(route, options = {}) {
91
- const includeSession = options.includeSession !== false
92
- const fields = [route?.provider ?? '', route?.model ?? '', normalizeBaseURL(route?.baseURL), includeSession ? route?.sessionId ?? '' : '']
93
- return createHash('sha256').update(fields.join('\u001f'), 'utf8').digest('hex')
38
+ const includeSession = options.includeSession !== false;
39
+ return createHash("sha256").update([
40
+ route?.provider ?? "", route?.model ?? "", normalizeBaseURL(route?.baseURL), includeSession ? (route?.sessionId ?? "") : "",
41
+ ].join("\u001f"), "utf8").digest("hex");
94
42
  }
95
-
96
43
  /** @param {unknown} value */
97
44
  function clampPromptCacheKey(value) {
98
- if (value === undefined) return undefined
99
- const chars = Array.from(String(value))
100
- return chars.length <= 64 ? String(value) : chars.slice(0, 64).join('')
101
- }
102
-
103
- /**
104
- * @param {Partial<RouteIdentity> | null | undefined} route
105
- * @param {Partial<Pick<ResolvedResponsesRoute, 'cacheRetention'>>} [config]
106
- */
107
- export function promptCacheSessionId(route, config = {}) {
108
- if (config?.cacheRetention === 'none') return undefined
109
- return route?.sessionId ? String(route.sessionId) : undefined
110
- }
111
-
112
- /**
113
- * @param {Partial<RouteIdentity> | null | undefined} route
114
- * @param {Partial<Pick<ResolvedResponsesRoute, 'cacheRetention'>>} [config]
115
- */
116
- export function promptCacheKey(route, config = {}) {
117
- return clampPromptCacheKey(promptCacheSessionId(route, config))
118
- }
119
-
120
- /** @param {Partial<Pick<ResolvedResponsesRoute, 'cacheRetention' | 'supportsLongCacheRetention'>>} [config] */
45
+ if (value === undefined)
46
+ return undefined;
47
+ const chars = Array.from(String(value));
48
+ return chars.length <= 64 ? String(value) : chars.slice(0, 64).join("");
49
+ }
50
+ /** @param {Partial<RouteIdentity> | null | undefined} route @param {Partial<Pick<ResolvedResponsesRoute, 'cacheRetention'>>} [config] @param {RouteContext | null | undefined} [ctx] */
51
+ export function promptCacheSessionId(route, config = {}, ctx = undefined) {
52
+ if (config.cacheRetention === "none")
53
+ return undefined;
54
+ const sessionId = route?.sessionId ? String(route.sessionId) : undefined;
55
+ if (!sessionId)
56
+ return undefined;
57
+ const sessions = ctx?.sessions;
58
+ let current = sessions?.get(SessionId(sessionId));
59
+ if (current?.header?.origin !== "subagent")
60
+ return sessionId;
61
+ const seen = new Set([sessionId]);
62
+ while (current?.header?.origin === "subagent" && current.header.parentSession) {
63
+ const parentId = current.header.parentSession;
64
+ if (seen.has(parentId))
65
+ return sessionId;
66
+ const parent = sessions?.get(SessionId(parentId));
67
+ if (!parent)
68
+ return sessionId;
69
+ seen.add(parentId);
70
+ current = parent;
71
+ }
72
+ return current?.id ?? sessionId;
73
+ }
74
+ /** @param {Partial<RouteIdentity> | null | undefined} route @param {Partial<Pick<ResolvedResponsesRoute, 'cacheRetention'>>} [config] @param {RouteContext | null | undefined} [ctx] */
75
+ export function promptCacheKey(route, config = {}, ctx = undefined) {
76
+ return clampPromptCacheKey(promptCacheSessionId(route, config, ctx));
77
+ }
78
+ /** Grok follows Pi directly: cache/affinity belongs to the selected child session. */
79
+ export function grokPromptCacheSessionId(route, config = {}) {
80
+ if (config.cacheRetention === "none")
81
+ return undefined;
82
+ const sessionId = String(route?.sessionId ?? "");
83
+ return sessionId || undefined;
84
+ }
85
+ /** @param {Partial<Pick<ResolvedResponsesRoute, 'cacheRetention' | 'supportsLongCacheRetention' | 'responsesCompat'>>} [config] */
121
86
  export function promptCacheRetention(config = {}) {
122
- return config?.cacheRetention === 'long' && config?.supportsLongCacheRetention !== false ? '24h' : undefined
87
+ return config.cacheRetention === "long" && config.supportsLongCacheRetention === true && config.responsesCompat?.supportsExplicitPromptCacheMode !== true ? "24h" : undefined;
123
88
  }
124
-
125
- /**
126
- * @param {RetryPolicyConfig | null | undefined} policy
127
- * @param {number} [fallback]
128
- */
89
+ /** @param {RetryPolicyConfig | null | undefined} policy @param {number} [fallback] */
129
90
  function retryAttempts(policy, fallback = 3) {
130
- if (!policy || typeof policy !== 'object') return fallback
131
- try {
132
- const resolved = resolveRetryPolicy(policy, 'llm-pi-ai provider retryPolicy')
133
- if (resolved.mode === 'normal' && Number.isSafeInteger(resolved.maxRetries)) return Math.min(resolved.maxRetries + 1, 6)
134
- } catch { /* keep fallback */ }
135
- return fallback
136
- }
137
-
138
- /**
139
- * @param {RouteContext | null | undefined} ctx
140
- * @param {string} namespace
141
- */
91
+ if (!policy)
92
+ return fallback;
93
+ try {
94
+ const resolved = resolveRetryPolicy(policy, "llm-pi-ai provider retryPolicy");
95
+ if (resolved.mode === "normal" && isPositiveInteger(resolved.maxRetries))
96
+ return Math.min(resolved.maxRetries + 1, 6);
97
+ }
98
+ catch { }
99
+ return fallback;
100
+ }
101
+ /** @param {RouteContext | null | undefined} ctx @param {string} namespace */
142
102
  export function settingsValue(ctx, namespace) {
143
- const settings = /** @type {SettingsService | undefined} */ (ctx?.get?.('settings') ?? ctx?.settings)
144
- return settings?.get?.(settingsNamespace(namespace))
103
+ return asLlmSettingsSection(ctx?.settings?.get(namespace));
104
+ }
105
+ /** The DSH 0.1.5 contract exposes deferred provider diagnostics separately from saved settings. */
106
+ function providerDirectoryUsable(ctx, provider) {
107
+ const llm = ctx?.llm;
108
+ const list = llm?.listConfigurableProviders;
109
+ if (typeof list !== "function")
110
+ return true;
111
+ try {
112
+ const entry = list.call(llm).find((candidate) => candidate.provider === provider && candidate.settingsNs === "llm-pi-ai");
113
+ return typeof entry?.error !== "string" || entry.error.trim() === "";
114
+ }
115
+ catch {
116
+ return false;
117
+ }
145
118
  }
146
-
147
119
  /** @type {Set<keyof ResponsesCompat>} */
148
- const RESPONSES_COMPAT_FIELDS = new Set(['supportsDeveloperRole', 'sessionAffinityFormat', 'supportsStrictMode', 'supportsLongCacheRetention', 'supportsOpenAIGrammarTools', 'supportsAdditionalTools', 'supportsToolSearch', 'supportsExplicitPromptCacheMode'])
149
-
150
- /**
151
- * @param {ResponsesCompat} target
152
- * @param {unknown} source
153
- */
120
+ const RESPONSES_COMPAT_FIELDS = new Set(["supportsDeveloperRole", "sessionAffinityFormat", "supportsStrictMode", "supportsLongCacheRetention", "supportsOpenAIGrammarTools", "supportsAdditionalTools", "supportsToolSearch", "supportsExplicitPromptCacheMode", "supportsMaxOutputTokens"]);
121
+ /** @param {ResponsesCompat} target @param {unknown} source */
154
122
  function copyResponsesCompat(target, source) {
155
- if (!source || typeof source !== 'object') return
156
- const values = /** @type {Record<string, unknown>} */ (source)
157
- const output = /** @type {Record<string, unknown>} */ (/** @type {unknown} */ (target))
158
- for (const field of RESPONSES_COMPAT_FIELDS) {
159
- const value = values[field]
160
- if (field === 'sessionAffinityFormat') {
161
- if (['openai', 'openai-nosession', 'openrouter'].includes(String(value))) output[field] = value
162
- } else if (typeof value === 'boolean') output[field] = value
163
- }
164
- }
165
-
123
+ if (!isRecord(source))
124
+ return;
125
+ for (const field of RESPONSES_COMPAT_FIELDS) {
126
+ const value = source[field];
127
+ if (field === "sessionAffinityFormat") {
128
+ if (value === "openai" || value === "openai-nosession" || value === "openrouter")
129
+ target.sessionAffinityFormat = value;
130
+ }
131
+ else if (typeof value === "boolean")
132
+ Object.assign(target, { [field]: value });
133
+ }
134
+ }
166
135
  /** @param {unknown} provider @param {unknown} modelId */
167
136
  function builtinResponsesModel(provider, modelId) {
168
- try {
169
- return getBuiltinModels(/** @type {any} */ (String(provider ?? '')))
170
- .find((model) => model?.id === String(modelId ?? '') && model?.api === 'openai-responses')
171
- } catch { return undefined }
172
- }
173
-
174
- /**
175
- * @param {ProviderProfile | null | undefined} profile
176
- * @param {unknown} modelId
177
- * @returns {ResponsesCompat | undefined}
178
- */
137
+ const providerName = String(provider ?? "");
138
+ const builtinProvider = getBuiltinProviders().find((candidate) => candidate === providerName);
139
+ if (builtinProvider === undefined)
140
+ return undefined;
141
+ return getBuiltinModels(builtinProvider).find((model) => model.id === String(modelId ?? "") && model.api === "openai-responses");
142
+ }
143
+ function modelDescriptorDefaults(model) {
144
+ if (!model)
145
+ return undefined;
146
+ return {
147
+ reasoning: model.reasoning,
148
+ thinkingLevelMap: model.thinkingLevelMap,
149
+ };
150
+ }
151
+ function configuredModelProfile(profile, modelId) {
152
+ const configuredModels = Array.isArray(profile?.models) ? profile.models : [];
153
+ return configuredModels.length > 0
154
+ ? configuredModels.find((entry) => String(entry?.id ?? "") === String(modelId))
155
+ : profile?.modelOverrides?.[String(modelId)];
156
+ }
157
+ /** @param {ProviderProfile | null | undefined} profile @param {unknown} modelId @returns {ResponsesCompat | undefined} */
179
158
  function configuredResponsesCompat(profile, modelId) {
180
- /** @type {ResponsesCompat} */
181
- const compat = {}
182
- copyResponsesCompat(compat, profile?.compat)
183
- const configuredModels = Array.isArray(profile?.models) ? profile.models : []
184
- const modelEntry = configuredModels.length > 0
185
- ? configuredModels.find((entry) => String(entry?.id ?? '') === String(modelId))
186
- : profile?.modelOverrides?.[String(modelId)]
187
- copyResponsesCompat(compat, modelEntry?.compat)
188
- return Object.keys(compat).length > 0 ? compat : undefined
189
- }
190
-
191
- /**
192
- * @param {RouteContext | null | undefined} ctx
193
- * @param {RouteOptions} options
194
- * @param {UnresolvedRouteConfig} fallbackConfig
195
- * @returns {ResolvedResponsesRoute | undefined}
196
- */
197
- export function resolveResponsesRouteConfig(ctx, options, fallbackConfig) {
198
- const provider = String(options?.provider ?? '')
199
- const model = String(options?.model ?? '')
200
- const section = settingsValue(ctx, 'llm-pi-ai')
201
- const profile = section?.providers?.[provider]
202
- const fallbackOwned = provider === fallbackConfig.provider
203
- if (profile === undefined && !fallbackOwned) return undefined
204
- const configured = /** @type {ProviderProfile} */ (profile ?? {})
205
-
206
- const builtin = builtinResponsesModel(provider, model)
207
- const api = configured.api ?? builtin?.api ?? (fallbackOwned ? 'openai-responses' : undefined)
208
- if (api !== 'openai-responses') return undefined
209
-
210
- const baseURL = configured.baseURL ?? builtin?.baseUrl ?? (fallbackOwned ? fallbackConfig.baseURL : undefined)
211
- const apiKeyEnv = configured.apiKeyEnv ?? (fallbackOwned ? fallbackConfig.apiKeyEnv : undefined)
212
- if (!baseURL || !apiKeyEnv) return undefined
213
-
214
- /** @type {ResponsesCompat} */
215
- const responsesCompat = {}
216
- copyResponsesCompat(responsesCompat, builtin?.compat)
217
- copyResponsesCompat(responsesCompat, fallbackOwned ? fallbackConfig.responsesCompat : undefined)
218
- copyResponsesCompat(responsesCompat, configuredResponsesCompat(configured, model))
219
- // Host Pi rc.2 withholds this field; the plugin opt-in still requires Pi's exact model capability.
220
- const promptCacheModel = fallbackOwned && fallbackConfig.supportsExplicitPromptCacheMode === true
221
- ? builtinResponsesModel('openai', model)
222
- : undefined
223
- const promptCacheCompat = /** @type {ResponsesCompat | undefined} */ (promptCacheModel?.compat)
224
- if (promptCacheCompat?.supportsExplicitPromptCacheMode === true) responsesCompat.supportsExplicitPromptCacheMode = true
225
- const resolvedCompat = Object.keys(responsesCompat).length > 0 ? responsesCompat : undefined
226
-
227
- return {
228
- ...fallbackConfig,
229
- provider,
230
- model,
231
- api: 'openai-responses',
232
- baseURL: normalizeBaseURL(baseURL),
233
- apiKeyEnv,
234
- headers: configured.headers && typeof configured.headers === 'object' ? { ...configured.headers } : { ...(fallbackConfig.headers ?? {}) },
235
- cacheRetention: /** @type {CacheRetention} */ (['none', 'short', 'long'].includes(/** @type {string} */ (configured.cacheRetention)) ? configured.cacheRetention : (['none', 'short', 'long'].includes(/** @type {string} */ (fallbackConfig.cacheRetention)) ? fallbackConfig.cacheRetention : 'short')),
236
- supportsLongCacheRetention: resolvedCompat?.supportsLongCacheRetention ?? /** @type {boolean | undefined} */ (fallbackConfig.supportsLongCacheRetention) ?? true,
237
- responsesCompat: resolvedCompat,
238
- timeoutMs: Number.isInteger(configured.timeoutMs) && /** @type {number} */ (configured.timeoutMs) > 0 ? /** @type {number} */ (configured.timeoutMs) : fallbackConfig.timeoutMs,
239
- maxAttempts: retryAttempts(configured.retryPolicy, fallbackConfig.maxAttempts),
240
- maxRequestImageBytes: Number.isSafeInteger(configured.maxRequestImageBytes) && /** @type {number} */ (configured.maxRequestImageBytes) > 0 ? /** @type {number} */ (configured.maxRequestImageBytes) : fallbackConfig.maxRequestImageBytes,
241
- requestImagePixelBudget: Number.isSafeInteger(configured.requestImagePixelBudget) && /** @type {number} */ (configured.requestImagePixelBudget) > 0 ? /** @type {number} */ (configured.requestImagePixelBudget) : fallbackConfig.requestImagePixelBudget,
242
- requestImageMaxBytes: Number.isSafeInteger(configured.requestImageMaxBytes) && /** @type {number} */ (configured.requestImageMaxBytes) > 0 ? /** @type {number} */ (configured.requestImageMaxBytes) : fallbackConfig.requestImageMaxBytes,
243
- }
244
- }
245
-
246
- /**
247
- * @param {RouteContext | null | undefined} ctx
248
- * @param {Pick<ResolvedResponsesRoute, 'apiKeyEnv'>} config
249
- */
159
+ const compat = {};
160
+ copyResponsesCompat(compat, profile?.compat);
161
+ copyResponsesCompat(compat, configuredModelProfile(profile, modelId)?.compat);
162
+ return Object.keys(compat).length ? compat : undefined;
163
+ }
164
+ const MODEL_THINKING_LEVELS = [
165
+ "off",
166
+ "minimal",
167
+ "low",
168
+ "medium",
169
+ "high",
170
+ "xhigh",
171
+ "max",
172
+ ];
173
+ function profileReasoning(value) {
174
+ return MODEL_THINKING_LEVELS.find((level) => level === value);
175
+ }
176
+ function configuredModelControls(profile, modelId, base) {
177
+ const configured = configuredModelProfile(profile, modelId);
178
+ const efforts = configured?.reasoningEfforts;
179
+ if (efforts === undefined)
180
+ return {
181
+ reasoning: base?.reasoning ?? false,
182
+ ...(base?.thinkingLevelMap === undefined
183
+ ? {}
184
+ : { thinkingLevelMap: base.thinkingLevelMap }),
185
+ };
186
+ if (efforts === false)
187
+ return { reasoning: false };
188
+ if (!isRecord(efforts))
189
+ return { reasoning: false };
190
+ const thinkingLevelMap = {};
191
+ for (const level of MODEL_THINKING_LEVELS) {
192
+ const value = efforts[level];
193
+ if (value === undefined)
194
+ thinkingLevelMap[level] = null;
195
+ else if (value === null) {
196
+ // DSH permits only off:null: it means supported-off by omitting the wire option.
197
+ if (level !== "off")
198
+ thinkingLevelMap[level] = null;
199
+ }
200
+ else if (typeof value === "string")
201
+ thinkingLevelMap[level] = value;
202
+ }
203
+ return { reasoning: true, thinkingLevelMap };
204
+ }
205
+ function isLcxCapabilityRoute(provider, model) {
206
+ return provider === "lcx" &&
207
+ /^gpt-5\.6-(?:sol|luna|terra)$/iu.test(model);
208
+ }
209
+ /** Resolve only the selected DSH profile; policy cannot supply route identity or credentials. */
210
+ export function resolveResponsesRouteConfig(ctx, options, policy) {
211
+ const provider = String(options?.provider ?? "");
212
+ const model = String(options?.model ?? "");
213
+ if (!provider.trim() || !/^gpt-/iu.test(model) || !providerDirectoryUsable(ctx, provider))
214
+ return undefined;
215
+ const section = settingsValue(ctx, "llm-pi-ai");
216
+ const profile = section?.providers?.[provider];
217
+ const lcxCapabilityRoute = isLcxCapabilityRoute(provider, model);
218
+ if (!isRecord(profile))
219
+ return undefined;
220
+ const configured = profile;
221
+ const builtin = builtinResponsesModel(provider, model);
222
+ const api = configured.api ?? builtin?.api;
223
+ if (api !== "openai-responses")
224
+ return undefined;
225
+ const baseURL = configured.baseURL ?? builtin?.baseUrl;
226
+ const apiKeyEnv = configured.apiKeyEnv;
227
+ if (typeof baseURL !== "string" || !normalizeBaseURL(baseURL) || typeof apiKeyEnv !== "string" || !apiKeyEnv.trim())
228
+ return undefined;
229
+ const responsesCompat = {};
230
+ copyResponsesCompat(responsesCompat, builtin?.compat);
231
+ copyResponsesCompat(responsesCompat, lcxCapabilityRoute ? policy.responsesCompat : undefined);
232
+ copyResponsesCompat(responsesCompat, configuredResponsesCompat(configured, model));
233
+ const promptCacheModel = lcxCapabilityRoute && policy.supportsExplicitPromptCacheMode === true ? builtinResponsesModel("openai", model) : undefined;
234
+ const promptCacheRaw = promptCacheModel;
235
+ const promptCacheCompat = isRecord(promptCacheRaw)
236
+ ? promptCacheRaw.compat
237
+ : undefined;
238
+ if (isRecord(promptCacheCompat) &&
239
+ promptCacheCompat.supportsExplicitPromptCacheMode === true &&
240
+ responsesCompat.supportsExplicitPromptCacheMode === undefined)
241
+ responsesCompat.supportsExplicitPromptCacheMode = true;
242
+ const resolvedCompat = Object.keys(responsesCompat).length ? responsesCompat : undefined;
243
+ const supportsLongCacheRetention = resolvedCompat?.supportsLongCacheRetention ?? (lcxCapabilityRoute && policy.supportsLongCacheRetention === true);
244
+ responsesCompat.supportsLongCacheRetention = supportsLongCacheRetention;
245
+ const configuredRetention = configured.cacheRetention;
246
+ const fallbackRetention = lcxCapabilityRoute ? policy.cacheRetention : undefined;
247
+ const requestedRetention = configuredRetention === "none" || configuredRetention === "short" || configuredRetention === "long" ? configuredRetention : fallbackRetention === "none" || fallbackRetention === "short" || fallbackRetention === "long" ? fallbackRetention : undefined;
248
+ const cacheRetention = requestedRetention === "long" && !supportsLongCacheRetention ? "short" : requestedRetention ?? (supportsLongCacheRetention ? "long" : "short");
249
+ return {
250
+ provider,
251
+ model,
252
+ api: "openai-responses",
253
+ baseURL: normalizeBaseURL(baseURL),
254
+ apiKeyEnv,
255
+ headers: { ...(configured.headers ?? {}) },
256
+ cacheRetention,
257
+ supportsLongCacheRetention,
258
+ responsesCompat,
259
+ // These are explicitly LCX-owned transport and request-capability policies.
260
+ timeoutMs: isPositiveInteger(configured.timeoutMs) ? configured.timeoutMs : policy.timeoutMs,
261
+ maxAttempts: retryAttempts(configured.retryPolicy, policy.maxAttempts),
262
+ maxRequestImageBytes: isPositiveInteger(configured.maxRequestImageBytes) ? configured.maxRequestImageBytes : policy.maxRequestImageBytes,
263
+ requestImagePixelBudget: isPositiveInteger(configured.requestImagePixelBudget) ? configured.requestImagePixelBudget : policy.requestImagePixelBudget,
264
+ requestImageMaxBytes: isPositiveInteger(configured.requestImageMaxBytes) ? configured.requestImageMaxBytes : policy.requestImageMaxBytes,
265
+ };
266
+ }
267
+ /** Resolve a Grok-prefixed model through its selected DSH Responses profile. */
268
+ export function resolveGrokResponsesRouteConfig(ctx, options, policy) {
269
+ const provider = String(options?.provider ?? "");
270
+ const model = String(options?.model ?? "");
271
+ if (!provider.trim() || !/^grok/iu.test(model) || !providerDirectoryUsable(ctx, provider))
272
+ return undefined;
273
+ const section = settingsValue(ctx, "llm-pi-ai");
274
+ const configured = section?.providers?.[provider];
275
+ if (!isRecord(configured))
276
+ return undefined;
277
+ const selectedBuiltin = builtinResponsesModel(provider, model);
278
+ const api = configured.api ?? selectedBuiltin?.api;
279
+ if (api !== "openai-responses")
280
+ return undefined;
281
+ const baseURL = configured.baseURL ?? selectedBuiltin?.baseUrl;
282
+ const apiKeyEnv = configured.apiKeyEnv;
283
+ if (typeof baseURL !== "string" ||
284
+ !normalizeBaseURL(baseURL) ||
285
+ typeof apiKeyEnv !== "string" ||
286
+ !apiKeyEnv.trim())
287
+ return undefined;
288
+ const responsesCompat = {};
289
+ copyResponsesCompat(responsesCompat, selectedBuiltin?.compat);
290
+ copyResponsesCompat(responsesCompat, configuredResponsesCompat(configured, model));
291
+ const supportsLongCacheRetention = responsesCompat.supportsLongCacheRetention === true;
292
+ responsesCompat.supportsLongCacheRetention = supportsLongCacheRetention;
293
+ const requestedRetention = configured.cacheRetention;
294
+ const cacheRetention = requestedRetention === "none"
295
+ ? "none"
296
+ : requestedRetention === "long" && supportsLongCacheRetention
297
+ ? "long"
298
+ : "short";
299
+ return {
300
+ provider,
301
+ model,
302
+ api: "openai-responses",
303
+ baseURL: normalizeBaseURL(baseURL),
304
+ apiKeyEnv,
305
+ headers: effectiveGrokProfileHeaders(configured.headers),
306
+ cacheRetention,
307
+ supportsLongCacheRetention,
308
+ responsesCompat,
309
+ modelDefaults: modelDescriptorDefaults(selectedBuiltin),
310
+ modelControls: {
311
+ ...configuredModelControls(configured, model, selectedBuiltin),
312
+ includeEncryptedReasoning: true,
313
+ },
314
+ profileReasoning: profileReasoning(configured.reasoning),
315
+ // Pi applies only an explicitly configured request deadline; stream liveness
316
+ // is owned separately by the provider idle watchdog.
317
+ timeoutMs: isPositiveInteger(configured.timeoutMs)
318
+ ? configured.timeoutMs
319
+ : undefined,
320
+ streamIdleTimeoutMs: isPositiveFinite(configured.streamIdleTimeoutMs)
321
+ ? configured.streamIdleTimeoutMs
322
+ : 300_000,
323
+ maxAttempts: retryAttempts(configured.retryPolicy, policy.maxAttempts),
324
+ maxRequestImageBytes: isPositiveInteger(configured.maxRequestImageBytes)
325
+ ? configured.maxRequestImageBytes
326
+ : policy.maxRequestImageBytes,
327
+ requestImagePixelBudget: isPositiveInteger(configured.requestImagePixelBudget)
328
+ ? configured.requestImagePixelBudget
329
+ : policy.requestImagePixelBudget,
330
+ requestImageMaxBytes: isPositiveInteger(configured.requestImageMaxBytes)
331
+ ? configured.requestImageMaxBytes
332
+ : policy.requestImageMaxBytes,
333
+ };
334
+ }
335
+ /** @param {RouteContext | null | undefined} ctx @param {Pick<ResolvedResponsesRoute, 'apiKeyEnv'>} config */
250
336
  export async function resolveApiKey(ctx, config) {
251
- const credentials = /** @type {CredentialsService | undefined} */ (ctx?.get?.('credentials') ?? ctx?.credentials)
252
- if (credentials?.resolve) {
253
- const resolved = await credentials.resolve(config.apiKeyEnv)
254
- if (typeof resolved?.value === 'string' && resolved.value.trim()) return resolved.value.trim()
255
- }
256
- const ambient = String(process.env[config.apiKeyEnv] ?? '').trim()
257
- if (ambient) return ambient
258
- /** @type {LcxError} */
259
- const error = new Error(`DSH provider credential is unavailable: ${config.apiKeyEnv}`)
260
- error.code = 'LCX_CREDENTIAL_UNAVAILABLE'
261
- throw error
262
- }
263
-
264
- /**
265
- * @param {HeaderMap | null | undefined} headers
266
- * @param {string} name
267
- */
268
- function hasHeader(headers, name) { return Object.keys(headers ?? {}).some((key) => key.toLowerCase() === name.toLowerCase()) }
337
+ const resolved = await ctx?.credentials.resolve(credentialRef(config.apiKeyEnv));
338
+ if (resolved?.value.trim())
339
+ return resolved.value.trim();
340
+ const ambient = String(process.env[config.apiKeyEnv] ?? "").trim();
341
+ if (ambient)
342
+ return ambient;
343
+ const error = new Error(`DSH provider credential is unavailable: ${config.apiKeyEnv}`);
344
+ error.code = "LCX_CREDENTIAL_UNAVAILABLE";
345
+ throw error;
346
+ }
347
+ function setHeaderCaseInsensitive(headers, name, value) {
348
+ const normalized = name.toLowerCase();
349
+ for (const existing of Object.keys(headers))
350
+ if (existing.toLowerCase() === normalized)
351
+ delete headers[existing];
352
+ headers[name] = value;
353
+ }
354
+ /** Apply DSH's case-insensitive attribution reservation at profile resolution. */
355
+ function effectiveGrokProfileHeaders(configured) {
356
+ const attribution = attributionHeaders();
357
+ const reserved = new Set(Object.keys(attribution).map((name) => name.toLowerCase()));
358
+ const effective = {};
359
+ for (const [name, value] of Object.entries(configured ?? {}))
360
+ if (!reserved.has(name.toLowerCase()))
361
+ setHeaderCaseInsensitive(effective, name, value);
362
+ return effective;
363
+ }
364
+ /** @param {HeaderMap | null | undefined} headers @param {string} name */
365
+ function hasHeader(headers, name) { return Object.keys(headers ?? {}).some((key) => key.toLowerCase() === name.toLowerCase()); }
269
366
  /** @param {HeaderMap | null | undefined} headers */
270
- function hasExplicitSessionAffinity(headers) { return ['session-id', 'session_id', 'x-session-id'].some((name) => hasHeader(headers, name)) }
367
+ function hasExplicitSessionAffinity(headers) { return ["session-id", "session_id", "x-session-id"].some((name) => hasHeader(headers, name)); }
271
368
  /** @param {Partial<ResolvedResponsesRoute> | null | undefined} config */
272
369
  function sessionAffinityFormat(config) {
273
- if (config?.api && config.api !== 'openai-responses') return 'none'
274
- const explicit = config?.responsesCompat?.sessionAffinityFormat
275
- if (['openai', 'openai-nosession', 'openrouter'].includes(String(explicit))) return explicit
276
- const provider = String(config?.provider ?? '').toLowerCase()
277
- const baseURL = String(config?.baseURL ?? '').toLowerCase()
278
- return provider === 'openrouter' || baseURL.includes('openrouter.ai') ? 'openrouter' : 'openai'
279
- }
280
-
281
- /**
282
- * @param {RouteContext | null | undefined} ctx
283
- * @param {ResolvedResponsesRoute} config
284
- * @param {unknown} sessionId
285
- * @param {string | null | undefined} requestId
286
- * @returns {Promise<HeaderMap>}
287
- */
370
+ if (config?.api && config.api !== "openai-responses")
371
+ return "none";
372
+ const explicit = config?.responsesCompat?.sessionAffinityFormat;
373
+ if (explicit === "openai" || explicit === "openai-nosession" || explicit === "openrouter")
374
+ return explicit;
375
+ return String(config?.provider ?? "").toLowerCase() === "openrouter" ||
376
+ String(config?.baseURL ?? "").toLowerCase().includes("openrouter.ai")
377
+ ? "openrouter"
378
+ : "openai";
379
+ }
380
+ /** @param {RouteContext | null | undefined} ctx @param {ResolvedResponsesRoute} config @param {unknown} sessionId @param {string | null | undefined} requestId @returns {Promise<HeaderMap>} */
288
381
  export async function authenticatedHeaders(ctx, config, sessionId, requestId) {
289
- const explicit = { ...(config.headers ?? {}) }
290
- /** @type {HeaderMap} */
291
- const headers = {
292
- ...attributionHeaders(),
293
- authorization: `Bearer ${await resolveApiKey(ctx, config)}`,
294
- }
295
- const sid = sessionId ? String(sessionId) : undefined
296
- const format = sessionAffinityFormat(config)
297
- if (sid && !hasExplicitSessionAffinity(explicit)) {
298
- if (format === 'openai') headers.session_id = sid
299
- else if (format === 'openrouter') headers['x-session-id'] = sid
300
- }
301
- if (requestId !== null && !hasHeader(explicit, 'x-client-request-id')) {
302
- const correlation = requestId ?? (sid && (format === 'openai' || format === 'openai-nosession') ? sid : (!sid ? randomUUID() : undefined))
303
- if (correlation) headers['x-client-request-id'] = correlation
304
- }
305
- return { ...headers, ...explicit }
306
- }
307
-
308
- /**
309
- * @param {RouteOptions | null | undefined} options
310
- * @param {Pick<UnresolvedRouteConfig, 'provider' | 'model' | 'baseURL'>} config
311
- * @returns {RouteIdentity}
312
- */
382
+ const explicit = { ...(config.headers ?? {}) };
383
+ const headers = {
384
+ ...attributionHeaders(),
385
+ authorization: `Bearer ${await resolveApiKey(ctx, config)}`,
386
+ };
387
+ const sid = sessionId ? String(sessionId) : undefined;
388
+ const format = sessionAffinityFormat(config);
389
+ if (sid && !hasExplicitSessionAffinity(explicit)) {
390
+ if (format === "openai")
391
+ headers.session_id = sid;
392
+ else if (format === "openrouter")
393
+ headers["x-session-id"] = sid;
394
+ }
395
+ if (requestId !== null && !hasHeader(explicit, "x-client-request-id")) {
396
+ const correlation = requestId ?? (sid && (format === "openai" || format === "openai-nosession") ? sid : !sid ? randomUUID() : undefined);
397
+ if (correlation)
398
+ headers["x-client-request-id"] = correlation;
399
+ }
400
+ return { ...headers, ...explicit };
401
+ }
402
+ /** Grok mirrors Pi createClient ordering without changing accepted GPT headers. */
403
+ export async function authenticatedGrokHeaders(ctx, config, sessionId) {
404
+ const explicit = effectiveGrokProfileHeaders(config.headers);
405
+ const headers = {
406
+ authorization: `Bearer ${await resolveApiKey(ctx, config)}`,
407
+ };
408
+ const sid = sessionId ? String(sessionId) : undefined;
409
+ const format = sessionAffinityFormat(config);
410
+ if (sid) {
411
+ if (format === "openai")
412
+ headers.session_id = sid;
413
+ else if (format === "openrouter")
414
+ headers["x-session-id"] = sid;
415
+ if (format === "openai" || format === "openai-nosession")
416
+ headers["x-client-request-id"] = sid;
417
+ }
418
+ for (const [name, value] of Object.entries(explicit))
419
+ setHeaderCaseInsensitive(headers, name, value);
420
+ for (const [name, value] of Object.entries(attributionHeaders()))
421
+ setHeaderCaseInsensitive(headers, name, value);
422
+ return headers;
423
+ }
424
+ /** @param {RouteOptions | null | undefined} options @param {Pick<UnresolvedRouteConfig, 'provider' | 'model' | 'baseURL'>} config @returns {RouteIdentity} */
313
425
  export function currentRoute(options, config) {
314
- return {
315
- provider: String(options?.provider ?? config.provider ?? ''),
316
- model: String(options?.model ?? config.model ?? ''),
317
- baseURL: normalizeBaseURL(config.baseURL),
318
- sessionId: String(options?.sessionId ?? ''),
319
- }
320
- }
321
-
322
- /**
323
- * @param {RequestHeader | null | undefined} header
324
- * @param {Partial<RouteIdentity> | null | undefined} route
325
- * @returns {GenerationControls}
326
- */
426
+ return { provider: String(options?.provider ?? config.provider ?? ""), model: String(options?.model ?? config.model ?? ""), baseURL: normalizeBaseURL(config.baseURL), sessionId: String(options?.sessionId ?? "") };
427
+ }
428
+ /** @param {RequestHeader | null | undefined} header @param {Partial<RouteIdentity> | null | undefined} route @returns {GenerationControls} */
327
429
  export function generationControlsFromHeader(header, route) {
328
- const config = header?.config
329
- if (!config || String(config.provider ?? '') !== String(route?.provider ?? '') || String(config.model ?? '') !== String(route?.model ?? '')) return {}
330
- /** @type {GenerationControls} */
331
- const controls = {}
332
- if (config.reasoningEffort !== undefined) controls.reasoningEffort = config.reasoningEffort
333
- if (config.temperature !== undefined) controls.temperature = config.temperature
334
- if (config.maxTokens !== undefined) controls.maxTokens = config.maxTokens
335
- return controls
336
- }
337
-
338
- /**
339
- * @param {RouteSession | null | undefined} session
340
- * @param {Partial<RouteIdentity> | null | undefined} route
341
- * @returns {GenerationControls}
342
- */
430
+ const config = header?.config;
431
+ if (!config || String(config.provider ?? "") !== String(route?.provider ?? "") || String(config.model ?? "") !== String(route?.model ?? ""))
432
+ return {};
433
+ const controls = {};
434
+ if (config.reasoningEffort !== undefined)
435
+ controls.reasoningEffort = config.reasoningEffort;
436
+ if (config.temperature !== undefined)
437
+ controls.temperature = config.temperature;
438
+ if (config.maxTokens !== undefined)
439
+ controls.maxTokens = config.maxTokens;
440
+ return controls;
441
+ }
442
+ /** @param {RouteSession | null | undefined} session @param {Partial<RouteIdentity> | null | undefined} route @returns {GenerationControls} */
343
443
  export function generationControlsFromSession(session, route) {
344
- try { return generationControlsFromHeader(session?.requestHeader?.(), route) }
345
- catch { return {} }
346
- }
347
-
348
- /**
349
- * @param {{ set?: (key: string, value: RequestHeader) => unknown } | null | undefined} cache
350
- * @param {RouteSession | null | undefined} session
351
- * @param {{ type?: string, data?: { header?: RequestHeader } } | null | undefined} event
352
- */
353
- export function updateRequestHeaderCache(cache, session, event) {
354
- if (!cache?.set || !session?.id) return false
355
- let header
356
- if (event?.type === 'request/header') header = event?.data?.header
357
- else if (event?.type === 'compaction/start') {
358
- try { header = session.requestHeader?.() } catch { return false }
359
- }
360
- if (!header) return false
361
- cache.set(String(session.id), header)
362
- return true
363
- }
364
-
365
- /**
366
- * @param {RouteContext | null | undefined} ctx
367
- * @param {string | null | undefined} sessionId
368
- * @returns {string[]}
369
- */
370
- export function sessionAncestry(ctx, sessionId) {
371
- if (!sessionId) return []
372
- const sessions = /** @type {SessionsService | undefined} */ (ctx?.get?.('sessions') ?? ctx?.sessions)
373
- const result = []
374
- /** @type {Set<string>} */
375
- const seen = new Set()
376
- let current = sessions?.get?.(sessionId)
377
- while (current && !seen.has(current.id)) {
378
- seen.add(current.id)
379
- result.push(String(current.id))
380
- const parent = current.header?.parentSession
381
- if (!parent) break
382
- current = sessions?.get?.(parent)
383
- }
384
- return result
385
- }
386
-
387
- /**
388
- * @param {CheckpointRouteRecord | null | undefined} record
389
- * @param {RouteIdentity} route
390
- * @param {unknown} ctx
391
- */
444
+ try {
445
+ return generationControlsFromHeader(session?.requestHeader?.(), route);
446
+ }
447
+ catch {
448
+ return {};
449
+ }
450
+ }
451
+ /** @param {CheckpointRouteRecord | null | undefined} record @param {RouteIdentity} route @param {unknown} ctx */
392
452
  export function routeCompatible(record, route, ctx) {
393
- if (!record || ![4, 5].includes(record.version)) return false
394
- if (record.provider !== route.provider || record.model !== route.model) return false
395
- if (record.baseURLFingerprint !== baseURLFingerprint(route.baseURL)) return false
396
- // Ancestry authorizes portable migration only. Opaque native output is
397
- // replayable exclusively by the session that created the checkpoint.
398
- return record.sourceSessionId === route.sessionId
453
+ void ctx;
454
+ return !!record && record.version === 5 && record.provider === route.provider && record.model === route.model && record.baseURLFingerprint === baseURLFingerprint(route.baseURL) && record.sourceSessionId === route.sessionId;
399
455
  }