dsh-lcx-codex 0.4.2 → 0.4.3-pre.2
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/README.md +116 -202
- package/cordis.patch.yml +3 -20
- package/lib/client.js +273 -146
- package/lib/compact-v2.js +218 -199
- package/lib/dsh-compat.js +227 -100
- package/lib/dsh-responses.js +445 -277
- package/lib/index.js +851 -757
- package/lib/json-store.js +50 -31
- package/lib/native-checkpoint.js +520 -194
- package/lib/responses-request.js +106 -121
- package/lib/responses-stream.js +972 -443
- package/lib/route.js +229 -355
- package/lib/service-mutex.js +73 -64
- package/lib/token-budget.js +176 -108
- package/lib/transport.js +277 -67
- package/lib/types/client/index.d.ts +6 -0
- package/lib/types/compact-v2.d.ts +104 -0
- package/lib/types/dsh-compat.d.ts +78 -0
- package/lib/types/dsh-responses.d.ts +82 -0
- package/lib/types/index.d.ts +83 -0
- package/lib/types/json-store.d.ts +10 -0
- package/lib/types/native-checkpoint.d.ts +213 -0
- package/lib/types/responses-request.d.ts +58 -0
- package/lib/types/responses-stream.d.ts +51 -0
- package/lib/types/route.d.ts +132 -0
- package/lib/types/service-mutex.d.ts +14 -0
- package/lib/types/token-budget.d.ts +50 -0
- package/lib/types/transport.d.ts +20 -0
- package/lib/types/web-run-output.d.ts +29 -0
- package/lib/types/web-search-alpha.d.ts +286 -0
- package/lib/types/web-search-capability.d.ts +26 -0
- package/lib/types/web-search-hosted.d.ts +246 -0
- package/lib/types/web-search-ref-store.d.ts +22 -0
- package/lib/web-run-output.js +167 -18
- package/lib/web-search-alpha.js +865 -163
- package/lib/web-search-capability.js +55 -65
- package/lib/web-search-hosted.js +210 -32
- package/lib/web-search-ref-store.js +63 -59
- package/package.json +79 -27
- package/ARCHITECTURE.md +0 -117
- package/CHANGELOG.md +0 -224
- package/README_EN.md +0 -277
- package/assets/dsh-lcx-codex-banner.jpg +0 -0
- package/lib/legacy-v3.js +0 -20
- package/lib/responses-replay.js +0 -68
- package/scripts/probe-alpha.mjs +0 -43
- package/scripts/validate-dsh-schema.mjs +0 -31
package/lib/route.js
CHANGED
|
@@ -1,399 +1,273 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
6
|
-
import { getBuiltinModels } from
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
/** @
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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 "@earendil-works/pi-ai/providers/all";
|
|
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 asLlmSettingsSection(value) {
|
|
16
|
+
if (!isRecord(value))
|
|
17
|
+
return undefined;
|
|
18
|
+
const providers = value.providers;
|
|
19
|
+
if (providers === undefined)
|
|
20
|
+
return {};
|
|
21
|
+
if (!isRecord(providers))
|
|
22
|
+
return undefined;
|
|
23
|
+
return { providers: providers };
|
|
24
|
+
}
|
|
76
25
|
/** @param {unknown} value */
|
|
77
26
|
export function normalizeBaseURL(value) {
|
|
78
|
-
|
|
27
|
+
return String(value ?? "").trim().replace(/\/+$/u, "");
|
|
79
28
|
}
|
|
80
|
-
|
|
81
29
|
/** @param {unknown} baseURL */
|
|
82
30
|
export function baseURLFingerprint(baseURL) {
|
|
83
|
-
|
|
31
|
+
return createHash("sha256").update(normalizeBaseURL(baseURL), "utf8").digest("hex");
|
|
84
32
|
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* @param {Partial<RouteIdentity> | null | undefined} route
|
|
88
|
-
* @param {{ includeSession?: boolean }} [options]
|
|
89
|
-
*/
|
|
33
|
+
/** @param {Partial<RouteIdentity> | null | undefined} route @param {{ includeSession?: boolean }} [options] */
|
|
90
34
|
export function routeFingerprint(route, options = {}) {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
35
|
+
const includeSession = options.includeSession !== false;
|
|
36
|
+
return createHash("sha256").update([
|
|
37
|
+
route?.provider ?? "", route?.model ?? "", normalizeBaseURL(route?.baseURL), includeSession ? (route?.sessionId ?? "") : "",
|
|
38
|
+
].join("\u001f"), "utf8").digest("hex");
|
|
94
39
|
}
|
|
95
|
-
|
|
96
40
|
/** @param {unknown} value */
|
|
97
41
|
function clampPromptCacheKey(value) {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
42
|
+
if (value === undefined)
|
|
43
|
+
return undefined;
|
|
44
|
+
const chars = Array.from(String(value));
|
|
45
|
+
return chars.length <= 64 ? String(value) : chars.slice(0, 64).join("");
|
|
101
46
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
47
|
+
/** @param {Partial<RouteIdentity> | null | undefined} route @param {Partial<Pick<ResolvedResponsesRoute, 'cacheRetention'>>} [config] @param {RouteContext | null | undefined} [ctx] */
|
|
48
|
+
export function promptCacheSessionId(route, config = {}, ctx = undefined) {
|
|
49
|
+
if (config.cacheRetention === "none")
|
|
50
|
+
return undefined;
|
|
51
|
+
const sessionId = route?.sessionId ? String(route.sessionId) : undefined;
|
|
52
|
+
if (!sessionId)
|
|
53
|
+
return undefined;
|
|
54
|
+
const sessions = ctx?.sessions;
|
|
55
|
+
let current = sessions?.get(SessionId(sessionId));
|
|
56
|
+
if (current?.header?.origin !== "subagent")
|
|
57
|
+
return sessionId;
|
|
58
|
+
const seen = new Set([sessionId]);
|
|
59
|
+
while (current?.header?.origin === "subagent" && current.header.parentSession) {
|
|
60
|
+
const parentId = current.header.parentSession;
|
|
61
|
+
if (seen.has(parentId))
|
|
62
|
+
return sessionId;
|
|
63
|
+
const parent = sessions?.get(SessionId(parentId));
|
|
64
|
+
if (!parent)
|
|
65
|
+
return sessionId;
|
|
66
|
+
seen.add(parentId);
|
|
67
|
+
current = parent;
|
|
68
|
+
}
|
|
69
|
+
return current?.id ?? sessionId;
|
|
110
70
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
* @param {Partial<Pick<ResolvedResponsesRoute, 'cacheRetention'>>} [config]
|
|
115
|
-
*/
|
|
116
|
-
export function promptCacheKey(route, config = {}) {
|
|
117
|
-
return clampPromptCacheKey(promptCacheSessionId(route, config))
|
|
71
|
+
/** @param {Partial<RouteIdentity> | null | undefined} route @param {Partial<Pick<ResolvedResponsesRoute, 'cacheRetention'>>} [config] @param {RouteContext | null | undefined} [ctx] */
|
|
72
|
+
export function promptCacheKey(route, config = {}, ctx = undefined) {
|
|
73
|
+
return clampPromptCacheKey(promptCacheSessionId(route, config, ctx));
|
|
118
74
|
}
|
|
119
|
-
|
|
120
|
-
/** @param {Partial<Pick<ResolvedResponsesRoute, 'cacheRetention' | 'supportsLongCacheRetention'>>} [config] */
|
|
75
|
+
/** @param {Partial<Pick<ResolvedResponsesRoute, 'cacheRetention' | 'supportsLongCacheRetention' | 'responsesCompat'>>} [config] */
|
|
121
76
|
export function promptCacheRetention(config = {}) {
|
|
122
|
-
|
|
77
|
+
return config.cacheRetention === "long" && config.supportsLongCacheRetention === true && config.responsesCompat?.supportsExplicitPromptCacheMode !== true ? "24h" : undefined;
|
|
123
78
|
}
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* @param {RetryPolicyConfig | null | undefined} policy
|
|
127
|
-
* @param {number} [fallback]
|
|
128
|
-
*/
|
|
79
|
+
/** @param {RetryPolicyConfig | null | undefined} policy @param {number} [fallback] */
|
|
129
80
|
function retryAttempts(policy, fallback = 3) {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
81
|
+
if (!policy)
|
|
82
|
+
return fallback;
|
|
83
|
+
try {
|
|
84
|
+
const resolved = resolveRetryPolicy(policy, "llm-pi-ai provider retryPolicy");
|
|
85
|
+
if (resolved.mode === "normal" && isPositiveInteger(resolved.maxRetries))
|
|
86
|
+
return Math.min(resolved.maxRetries + 1, 6);
|
|
87
|
+
}
|
|
88
|
+
catch { }
|
|
89
|
+
return fallback;
|
|
136
90
|
}
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* @param {RouteContext | null | undefined} ctx
|
|
140
|
-
* @param {string} namespace
|
|
141
|
-
*/
|
|
91
|
+
/** @param {RouteContext | null | undefined} ctx @param {string} namespace */
|
|
142
92
|
export function settingsValue(ctx, namespace) {
|
|
143
|
-
|
|
144
|
-
return settings?.get?.(settingsNamespace(namespace))
|
|
93
|
+
return asLlmSettingsSection(ctx?.settings?.get(namespace));
|
|
145
94
|
}
|
|
146
|
-
|
|
147
95
|
/** @type {Set<keyof ResponsesCompat>} */
|
|
148
|
-
const RESPONSES_COMPAT_FIELDS = new Set([
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* @param {ResponsesCompat} target
|
|
152
|
-
* @param {unknown} source
|
|
153
|
-
*/
|
|
96
|
+
const RESPONSES_COMPAT_FIELDS = new Set(["supportsDeveloperRole", "sessionAffinityFormat", "supportsStrictMode", "supportsLongCacheRetention", "supportsOpenAIGrammarTools", "supportsAdditionalTools", "supportsToolSearch", "supportsExplicitPromptCacheMode", "supportsMaxOutputTokens"]);
|
|
97
|
+
/** @param {ResponsesCompat} target @param {unknown} source */
|
|
154
98
|
function copyResponsesCompat(target, source) {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
99
|
+
if (!isRecord(source))
|
|
100
|
+
return;
|
|
101
|
+
for (const field of RESPONSES_COMPAT_FIELDS) {
|
|
102
|
+
const value = source[field];
|
|
103
|
+
if (field === "sessionAffinityFormat") {
|
|
104
|
+
if (value === "openai" || value === "openai-nosession" || value === "openrouter")
|
|
105
|
+
target.sessionAffinityFormat = value;
|
|
106
|
+
}
|
|
107
|
+
else if (typeof value === "boolean")
|
|
108
|
+
Object.assign(target, { [field]: value });
|
|
109
|
+
}
|
|
164
110
|
}
|
|
165
|
-
|
|
166
111
|
/** @param {unknown} provider @param {unknown} modelId */
|
|
167
112
|
function builtinResponsesModel(provider, modelId) {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
113
|
+
const providerName = String(provider ?? "");
|
|
114
|
+
const builtinProvider = getBuiltinProviders().find((candidate) => candidate === providerName);
|
|
115
|
+
if (builtinProvider === undefined)
|
|
116
|
+
return undefined;
|
|
117
|
+
return getBuiltinModels(builtinProvider).find((model) => model.id === String(modelId ?? "") && model.api === "openai-responses");
|
|
172
118
|
}
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* @param {ProviderProfile | null | undefined} profile
|
|
176
|
-
* @param {unknown} modelId
|
|
177
|
-
* @returns {ResponsesCompat | undefined}
|
|
178
|
-
*/
|
|
119
|
+
/** @param {ProviderProfile | null | undefined} profile @param {unknown} modelId @returns {ResponsesCompat | undefined} */
|
|
179
120
|
function configuredResponsesCompat(profile, modelId) {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
121
|
+
const compat = {};
|
|
122
|
+
copyResponsesCompat(compat, profile?.compat);
|
|
123
|
+
const configuredModels = Array.isArray(profile?.models) ? profile.models : [];
|
|
124
|
+
const modelEntry = configuredModels.length > 0
|
|
125
|
+
? configuredModels.find((entry) => String(entry?.id ?? "") === String(modelId))
|
|
126
|
+
: profile?.modelOverrides?.[String(modelId)];
|
|
127
|
+
copyResponsesCompat(compat, modelEntry?.compat);
|
|
128
|
+
return Object.keys(compat).length ? compat : undefined;
|
|
129
|
+
}
|
|
130
|
+
function isLcxCapabilityRoute(provider, model) {
|
|
131
|
+
return provider === "lcx" &&
|
|
132
|
+
/^gpt-5\.6-(?:sol|luna|terra)$/iu.test(model);
|
|
189
133
|
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
134
|
+
/** Resolve only the selected DSH profile; policy cannot supply route identity or credentials. */
|
|
135
|
+
export function resolveResponsesRouteConfig(ctx, options, policy) {
|
|
136
|
+
const provider = String(options?.provider ?? "");
|
|
137
|
+
const model = String(options?.model ?? "");
|
|
138
|
+
if (!provider.trim() || !/^gpt-/iu.test(model))
|
|
139
|
+
return undefined;
|
|
140
|
+
const section = settingsValue(ctx, "llm-pi-ai");
|
|
141
|
+
const profile = section?.providers?.[provider];
|
|
142
|
+
const lcxCapabilityRoute = isLcxCapabilityRoute(provider, model);
|
|
143
|
+
if (!isRecord(profile))
|
|
144
|
+
return undefined;
|
|
145
|
+
const configured = profile;
|
|
146
|
+
const builtin = builtinResponsesModel(provider, model);
|
|
147
|
+
const api = configured.api ?? builtin?.api;
|
|
148
|
+
if (api !== "openai-responses")
|
|
149
|
+
return undefined;
|
|
150
|
+
const baseURL = configured.baseURL ?? builtin?.baseUrl;
|
|
151
|
+
const apiKeyEnv = configured.apiKeyEnv;
|
|
152
|
+
if (typeof baseURL !== "string" || !normalizeBaseURL(baseURL) || typeof apiKeyEnv !== "string" || !apiKeyEnv.trim())
|
|
153
|
+
return undefined;
|
|
154
|
+
const responsesCompat = {};
|
|
155
|
+
copyResponsesCompat(responsesCompat, builtin?.compat);
|
|
156
|
+
copyResponsesCompat(responsesCompat, lcxCapabilityRoute ? policy.responsesCompat : undefined);
|
|
157
|
+
copyResponsesCompat(responsesCompat, configuredResponsesCompat(configured, model));
|
|
158
|
+
const promptCacheModel = lcxCapabilityRoute && policy.supportsExplicitPromptCacheMode === true ? builtinResponsesModel("openai", model) : undefined;
|
|
159
|
+
const promptCacheRaw = promptCacheModel;
|
|
160
|
+
const promptCacheCompat = isRecord(promptCacheRaw)
|
|
161
|
+
? promptCacheRaw.compat
|
|
162
|
+
: undefined;
|
|
163
|
+
if (isRecord(promptCacheCompat) &&
|
|
164
|
+
promptCacheCompat.supportsExplicitPromptCacheMode === true &&
|
|
165
|
+
responsesCompat.supportsExplicitPromptCacheMode === undefined)
|
|
166
|
+
responsesCompat.supportsExplicitPromptCacheMode = true;
|
|
167
|
+
const resolvedCompat = Object.keys(responsesCompat).length ? responsesCompat : undefined;
|
|
168
|
+
const supportsLongCacheRetention = resolvedCompat?.supportsLongCacheRetention ?? (lcxCapabilityRoute && policy.supportsLongCacheRetention === true);
|
|
169
|
+
responsesCompat.supportsLongCacheRetention = supportsLongCacheRetention;
|
|
170
|
+
const configuredRetention = configured.cacheRetention;
|
|
171
|
+
const fallbackRetention = lcxCapabilityRoute ? policy.cacheRetention : undefined;
|
|
172
|
+
const requestedRetention = configuredRetention === "none" || configuredRetention === "short" || configuredRetention === "long" ? configuredRetention : fallbackRetention === "none" || fallbackRetention === "short" || fallbackRetention === "long" ? fallbackRetention : undefined;
|
|
173
|
+
const cacheRetention = requestedRetention === "long" && !supportsLongCacheRetention ? "short" : requestedRetention ?? (supportsLongCacheRetention ? "long" : "short");
|
|
174
|
+
return {
|
|
175
|
+
provider,
|
|
176
|
+
model,
|
|
177
|
+
api: "openai-responses",
|
|
178
|
+
baseURL: normalizeBaseURL(baseURL),
|
|
179
|
+
apiKeyEnv,
|
|
180
|
+
headers: { ...(configured.headers ?? {}) },
|
|
181
|
+
cacheRetention,
|
|
182
|
+
supportsLongCacheRetention,
|
|
183
|
+
responsesCompat,
|
|
184
|
+
// These are explicitly LCX-owned transport and request-capability policies.
|
|
185
|
+
timeoutMs: isPositiveInteger(configured.timeoutMs) ? configured.timeoutMs : policy.timeoutMs,
|
|
186
|
+
maxAttempts: retryAttempts(configured.retryPolicy, policy.maxAttempts),
|
|
187
|
+
maxRequestImageBytes: isPositiveInteger(configured.maxRequestImageBytes) ? configured.maxRequestImageBytes : policy.maxRequestImageBytes,
|
|
188
|
+
requestImagePixelBudget: isPositiveInteger(configured.requestImagePixelBudget) ? configured.requestImagePixelBudget : policy.requestImagePixelBudget,
|
|
189
|
+
requestImageMaxBytes: isPositiveInteger(configured.requestImageMaxBytes) ? configured.requestImageMaxBytes : policy.requestImageMaxBytes,
|
|
190
|
+
};
|
|
244
191
|
}
|
|
245
|
-
|
|
246
|
-
/**
|
|
247
|
-
* @param {RouteContext | null | undefined} ctx
|
|
248
|
-
* @param {Pick<ResolvedResponsesRoute, 'apiKeyEnv'>} config
|
|
249
|
-
*/
|
|
192
|
+
/** @param {RouteContext | null | undefined} ctx @param {Pick<ResolvedResponsesRoute, 'apiKeyEnv'>} config */
|
|
250
193
|
export async function resolveApiKey(ctx, config) {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
error.code = 'LCX_CREDENTIAL_UNAVAILABLE'
|
|
261
|
-
throw error
|
|
194
|
+
const resolved = await ctx?.credentials.resolve(credentialRef(config.apiKeyEnv));
|
|
195
|
+
if (resolved?.value.trim())
|
|
196
|
+
return resolved.value.trim();
|
|
197
|
+
const ambient = String(process.env[config.apiKeyEnv] ?? "").trim();
|
|
198
|
+
if (ambient)
|
|
199
|
+
return ambient;
|
|
200
|
+
const error = new Error(`DSH provider credential is unavailable: ${config.apiKeyEnv}`);
|
|
201
|
+
error.code = "LCX_CREDENTIAL_UNAVAILABLE";
|
|
202
|
+
throw error;
|
|
262
203
|
}
|
|
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()) }
|
|
204
|
+
/** @param {HeaderMap | null | undefined} headers @param {string} name */
|
|
205
|
+
function hasHeader(headers, name) { return Object.keys(headers ?? {}).some((key) => key.toLowerCase() === name.toLowerCase()); }
|
|
269
206
|
/** @param {HeaderMap | null | undefined} headers */
|
|
270
|
-
function hasExplicitSessionAffinity(headers) { return [
|
|
207
|
+
function hasExplicitSessionAffinity(headers) { return ["session-id", "session_id", "x-session-id"].some((name) => hasHeader(headers, name)); }
|
|
271
208
|
/** @param {Partial<ResolvedResponsesRoute> | null | undefined} config */
|
|
272
209
|
function sessionAffinityFormat(config) {
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
210
|
+
if (config?.api && config.api !== "openai-responses")
|
|
211
|
+
return "none";
|
|
212
|
+
const explicit = config?.responsesCompat?.sessionAffinityFormat;
|
|
213
|
+
if (explicit === "openai" || explicit === "openai-nosession" || explicit === "openrouter")
|
|
214
|
+
return explicit;
|
|
215
|
+
return String(config?.provider ?? "").toLowerCase() === "openrouter" ||
|
|
216
|
+
String(config?.baseURL ?? "").toLowerCase().includes("openrouter.ai")
|
|
217
|
+
? "openrouter"
|
|
218
|
+
: "openai";
|
|
279
219
|
}
|
|
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
|
-
*/
|
|
220
|
+
/** @param {RouteContext | null | undefined} ctx @param {ResolvedResponsesRoute} config @param {unknown} sessionId @param {string | null | undefined} requestId @returns {Promise<HeaderMap>} */
|
|
288
221
|
export async function authenticatedHeaders(ctx, config, sessionId, requestId) {
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
222
|
+
const explicit = { ...(config.headers ?? {}) };
|
|
223
|
+
const headers = {
|
|
224
|
+
...attributionHeaders(),
|
|
225
|
+
authorization: `Bearer ${await resolveApiKey(ctx, config)}`,
|
|
226
|
+
};
|
|
227
|
+
const sid = sessionId ? String(sessionId) : undefined;
|
|
228
|
+
const format = sessionAffinityFormat(config);
|
|
229
|
+
if (sid && !hasExplicitSessionAffinity(explicit)) {
|
|
230
|
+
if (format === "openai")
|
|
231
|
+
headers.session_id = sid;
|
|
232
|
+
else if (format === "openrouter")
|
|
233
|
+
headers["x-session-id"] = sid;
|
|
234
|
+
}
|
|
235
|
+
if (requestId !== null && !hasHeader(explicit, "x-client-request-id")) {
|
|
236
|
+
const correlation = requestId ?? (sid && (format === "openai" || format === "openai-nosession") ? sid : !sid ? randomUUID() : undefined);
|
|
237
|
+
if (correlation)
|
|
238
|
+
headers["x-client-request-id"] = correlation;
|
|
239
|
+
}
|
|
240
|
+
return { ...headers, ...explicit };
|
|
306
241
|
}
|
|
307
|
-
|
|
308
|
-
/**
|
|
309
|
-
* @param {RouteOptions | null | undefined} options
|
|
310
|
-
* @param {Pick<UnresolvedRouteConfig, 'provider' | 'model' | 'baseURL'>} config
|
|
311
|
-
* @returns {RouteIdentity}
|
|
312
|
-
*/
|
|
242
|
+
/** @param {RouteOptions | null | undefined} options @param {Pick<UnresolvedRouteConfig, 'provider' | 'model' | 'baseURL'>} config @returns {RouteIdentity} */
|
|
313
243
|
export function currentRoute(options, config) {
|
|
314
|
-
|
|
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
|
-
}
|
|
244
|
+
return { provider: String(options?.provider ?? config.provider ?? ""), model: String(options?.model ?? config.model ?? ""), baseURL: normalizeBaseURL(config.baseURL), sessionId: String(options?.sessionId ?? "") };
|
|
320
245
|
}
|
|
321
|
-
|
|
322
|
-
/**
|
|
323
|
-
* @param {RequestHeader | null | undefined} header
|
|
324
|
-
* @param {Partial<RouteIdentity> | null | undefined} route
|
|
325
|
-
* @returns {GenerationControls}
|
|
326
|
-
*/
|
|
246
|
+
/** @param {RequestHeader | null | undefined} header @param {Partial<RouteIdentity> | null | undefined} route @returns {GenerationControls} */
|
|
327
247
|
export function generationControlsFromHeader(header, route) {
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
248
|
+
const config = header?.config;
|
|
249
|
+
if (!config || String(config.provider ?? "") !== String(route?.provider ?? "") || String(config.model ?? "") !== String(route?.model ?? ""))
|
|
250
|
+
return {};
|
|
251
|
+
const controls = {};
|
|
252
|
+
if (config.reasoningEffort !== undefined)
|
|
253
|
+
controls.reasoningEffort = config.reasoningEffort;
|
|
254
|
+
if (config.temperature !== undefined)
|
|
255
|
+
controls.temperature = config.temperature;
|
|
256
|
+
if (config.maxTokens !== undefined)
|
|
257
|
+
controls.maxTokens = config.maxTokens;
|
|
258
|
+
return controls;
|
|
336
259
|
}
|
|
337
|
-
|
|
338
|
-
/**
|
|
339
|
-
* @param {RouteSession | null | undefined} session
|
|
340
|
-
* @param {Partial<RouteIdentity> | null | undefined} route
|
|
341
|
-
* @returns {GenerationControls}
|
|
342
|
-
*/
|
|
260
|
+
/** @param {RouteSession | null | undefined} session @param {Partial<RouteIdentity> | null | undefined} route @returns {GenerationControls} */
|
|
343
261
|
export function generationControlsFromSession(session, route) {
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
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
|
|
262
|
+
try {
|
|
263
|
+
return generationControlsFromHeader(session?.requestHeader?.(), route);
|
|
264
|
+
}
|
|
265
|
+
catch {
|
|
266
|
+
return {};
|
|
267
|
+
}
|
|
385
268
|
}
|
|
386
|
-
|
|
387
|
-
/**
|
|
388
|
-
* @param {CheckpointRouteRecord | null | undefined} record
|
|
389
|
-
* @param {RouteIdentity} route
|
|
390
|
-
* @param {unknown} ctx
|
|
391
|
-
*/
|
|
269
|
+
/** @param {CheckpointRouteRecord | null | undefined} record @param {RouteIdentity} route @param {unknown} ctx */
|
|
392
270
|
export function routeCompatible(record, route, ctx) {
|
|
393
|
-
|
|
394
|
-
|
|
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
|
|
271
|
+
void ctx;
|
|
272
|
+
return !!record && record.version === 5 && record.provider === route.provider && record.model === route.model && record.baseURLFingerprint === baseURLFingerprint(route.baseURL) && record.sourceSessionId === route.sessionId;
|
|
399
273
|
}
|