dsh-lcx-codex 0.4.3 → 0.5.0-pre.1
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 +95 -60
- package/lib/abort-error.js +47 -0
- package/lib/client.js +597 -505
- package/lib/dsh-compat.js +37 -0
- package/lib/dsh-responses.js +92 -48
- package/lib/index.js +174 -105
- package/lib/invocation-policy-scope.js +4 -8
- package/lib/native-checkpoint.js +172 -80
- package/lib/pi-responses-runtime.js +252 -98
- package/lib/pi086-responses-allowlist.js +131 -0
- package/lib/responses-request.js +2 -2
- package/lib/responses-stream.js +62 -11
- package/lib/route.js +23 -32
- package/lib/search-accounting.js +22 -6
- package/lib/search-usage.js +32 -18
- package/lib/service-mutex.js +4 -8
- package/lib/token-budget.js +15 -21
- package/lib/transport.js +6 -6
- package/lib/types/client/search-media.d.ts +11 -2
- package/lib/types/index.d.ts +71 -43
- package/lib/web-search-alpha-history.js +62 -0
- package/lib/web-search-alpha.js +141 -124
- package/lib/web-search-capability.js +1 -1
- package/lib/web-search-hosted.js +29 -17
- package/lib/web-search-ref-store.js +27 -5
- package/package.json +41 -27
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// Generated by scripts/generate-pi086-allowlist.mjs from the Pi 0.86.0
|
|
2
|
+
// openai-responses catalog snapshot in data/pi086-responses-models.json.
|
|
3
|
+
// Source: Issue #90 Step 0(c) probe/dump.mjs, runtime/pi-catalog/catalog-v0.86.0.json.
|
|
4
|
+
// Keep provider and model paired: an id on another provider has separate authority.
|
|
5
|
+
const responsesModels = {
|
|
6
|
+
"cloudflare-ai-gateway": [
|
|
7
|
+
"gpt-4.1",
|
|
8
|
+
"gpt-4.1-mini",
|
|
9
|
+
"gpt-4.1-nano",
|
|
10
|
+
"gpt-4o",
|
|
11
|
+
"gpt-4o-mini",
|
|
12
|
+
"gpt-5",
|
|
13
|
+
"gpt-5-mini",
|
|
14
|
+
"gpt-5-nano",
|
|
15
|
+
"gpt-5.1",
|
|
16
|
+
"gpt-5.4",
|
|
17
|
+
"gpt-5.4-mini",
|
|
18
|
+
"gpt-5.4-nano",
|
|
19
|
+
"gpt-5.4-pro",
|
|
20
|
+
"gpt-5.5",
|
|
21
|
+
"gpt-5.5-pro",
|
|
22
|
+
"gpt-5.6-luna",
|
|
23
|
+
"gpt-5.6-sol",
|
|
24
|
+
"gpt-5.6-terra",
|
|
25
|
+
"gpt-6-astra",
|
|
26
|
+
"o3",
|
|
27
|
+
"o3-mini",
|
|
28
|
+
"o4-mini"
|
|
29
|
+
],
|
|
30
|
+
"github-copilot": [
|
|
31
|
+
"gpt-5-mini",
|
|
32
|
+
"gpt-5.3-codex",
|
|
33
|
+
"gpt-5.4",
|
|
34
|
+
"gpt-5.4-mini",
|
|
35
|
+
"gpt-5.4-nano",
|
|
36
|
+
"gpt-5.5",
|
|
37
|
+
"gpt-5.6-luna",
|
|
38
|
+
"gpt-5.6-sol",
|
|
39
|
+
"gpt-5.6-terra",
|
|
40
|
+
"gpt-6-astra",
|
|
41
|
+
"grok-4.5",
|
|
42
|
+
"grok-4.6",
|
|
43
|
+
"mai-code-1-flash-picker",
|
|
44
|
+
"mai-code-1.1-flash"
|
|
45
|
+
],
|
|
46
|
+
"openai": [
|
|
47
|
+
"gpt-4",
|
|
48
|
+
"gpt-4-turbo",
|
|
49
|
+
"gpt-4.1",
|
|
50
|
+
"gpt-4.1-mini",
|
|
51
|
+
"gpt-4.1-nano",
|
|
52
|
+
"gpt-4o",
|
|
53
|
+
"gpt-4o-2024-05-13",
|
|
54
|
+
"gpt-4o-2024-08-06",
|
|
55
|
+
"gpt-4o-2024-11-20",
|
|
56
|
+
"gpt-4o-mini",
|
|
57
|
+
"gpt-5",
|
|
58
|
+
"gpt-5-chat-latest",
|
|
59
|
+
"gpt-5-mini",
|
|
60
|
+
"gpt-5-nano",
|
|
61
|
+
"gpt-5-pro",
|
|
62
|
+
"gpt-5.1",
|
|
63
|
+
"gpt-5.2",
|
|
64
|
+
"gpt-5.2-chat-latest",
|
|
65
|
+
"gpt-5.2-pro",
|
|
66
|
+
"gpt-5.3-chat-latest",
|
|
67
|
+
"gpt-5.3-codex",
|
|
68
|
+
"gpt-5.3-codex-spark",
|
|
69
|
+
"gpt-5.4",
|
|
70
|
+
"gpt-5.4-mini",
|
|
71
|
+
"gpt-5.4-nano",
|
|
72
|
+
"gpt-5.4-pro",
|
|
73
|
+
"gpt-5.5",
|
|
74
|
+
"gpt-5.5-pro",
|
|
75
|
+
"gpt-5.6-luna",
|
|
76
|
+
"gpt-5.6-sol",
|
|
77
|
+
"gpt-5.6-terra",
|
|
78
|
+
"gpt-6-astra",
|
|
79
|
+
"gpt-realtime-2.1",
|
|
80
|
+
"o1",
|
|
81
|
+
"o1-pro",
|
|
82
|
+
"o3",
|
|
83
|
+
"o3-mini",
|
|
84
|
+
"o3-pro",
|
|
85
|
+
"o4-mini"
|
|
86
|
+
],
|
|
87
|
+
"opencode": [
|
|
88
|
+
"gpt-5",
|
|
89
|
+
"gpt-5-codex",
|
|
90
|
+
"gpt-5-nano",
|
|
91
|
+
"gpt-5.1",
|
|
92
|
+
"gpt-5.1-codex",
|
|
93
|
+
"gpt-5.1-codex-max",
|
|
94
|
+
"gpt-5.1-codex-mini",
|
|
95
|
+
"gpt-5.2",
|
|
96
|
+
"gpt-5.2-codex",
|
|
97
|
+
"gpt-5.3-codex",
|
|
98
|
+
"gpt-5.4",
|
|
99
|
+
"gpt-5.4-mini",
|
|
100
|
+
"gpt-5.4-nano",
|
|
101
|
+
"gpt-5.4-pro",
|
|
102
|
+
"gpt-5.5",
|
|
103
|
+
"gpt-5.5-pro",
|
|
104
|
+
"gpt-5.6-luna",
|
|
105
|
+
"gpt-5.6-sol",
|
|
106
|
+
"gpt-5.6-terra",
|
|
107
|
+
"gpt-6-astra",
|
|
108
|
+
"grok-4.5",
|
|
109
|
+
"grok-4.6",
|
|
110
|
+
"grok-build-0.1",
|
|
111
|
+
"muse-spark-1.2",
|
|
112
|
+
"muse-spark-1.2-contributor-free",
|
|
113
|
+
"muse-spark-1.3",
|
|
114
|
+
"muse-spark-1.3-contributor-free"
|
|
115
|
+
],
|
|
116
|
+
"opencode-go": [
|
|
117
|
+
"gpt-5.6-luna",
|
|
118
|
+
"grok-4.6",
|
|
119
|
+
"muse-spark-1.2-contributor",
|
|
120
|
+
"muse-spark-1.3-contributor"
|
|
121
|
+
],
|
|
122
|
+
"xai": [
|
|
123
|
+
"grok-4.3",
|
|
124
|
+
"grok-4.5",
|
|
125
|
+
"grok-4.6"
|
|
126
|
+
]
|
|
127
|
+
};
|
|
128
|
+
export function wasPi086ResponsesModel(provider, model) {
|
|
129
|
+
const ids = responsesModels[provider];
|
|
130
|
+
return ids?.includes(model) ?? false;
|
|
131
|
+
}
|
package/lib/responses-request.js
CHANGED
|
@@ -66,7 +66,7 @@ export function responsesGenerationEnvelope({ model = "unknown", reasoningEffort
|
|
|
66
66
|
return result;
|
|
67
67
|
}
|
|
68
68
|
/**
|
|
69
|
-
* Build the shared LCX-owned request envelope while retaining Pi 0.
|
|
69
|
+
* Build the shared LCX-owned request envelope while retaining Pi 0.86.0 Responses semantics.
|
|
70
70
|
* Production LCX ordinary/compact/replay construction places the DSH system prompt in canonical input.
|
|
71
71
|
* @param {BuildResponsesBodyOptions} options
|
|
72
72
|
*/
|
|
@@ -84,7 +84,7 @@ export function buildResponsesBody({ model, input, tools, sessionId, promptCache
|
|
|
84
84
|
const longRetention = retention === "long" && compat.supportsExplicitPromptCacheMode !== true
|
|
85
85
|
? promptCacheRetention ?? (compat.supportsLongCacheRetention === true ? "24h" : undefined)
|
|
86
86
|
: undefined;
|
|
87
|
-
// Pi 0.
|
|
87
|
+
// Pi 0.86.0 uses explicit 30m only for long retention; short is implicit.
|
|
88
88
|
const currentCache = retention === "long" && compat.supportsLongCacheRetention === true && compat.supportsExplicitPromptCacheMode === true;
|
|
89
89
|
const explicitCache = retention === "none" && compat.supportsExplicitPromptCacheMode === true;
|
|
90
90
|
const promptCacheOptions = currentCache
|
package/lib/responses-stream.js
CHANGED
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
import { createAssistantMessageEventStream, processResponsesStream, } from "./pi-responses-runtime.js";
|
|
3
|
-
import {
|
|
3
|
+
import { abortError } from "./abort-error.js";
|
|
4
|
+
import { IMAGE_OFFLOAD_REQUIRED_CODE, ToolCallId } from "@deepseek-ai/dsh-llm";
|
|
4
5
|
import { fetchSseWithRetry } from "./transport.js";
|
|
6
|
+
import { sanitizeContextDetails } from "./search-accounting.js";
|
|
5
7
|
import { createGrokNativeReplayEnvelope, grokPendingSourcesStart, sanitizeGrokVisibleText, } from "./grok-native-search.js";
|
|
8
|
+
/** Durable LCX-only pressure evidence; canonical usage stays on Pi/DSH billing. */
|
|
9
|
+
function responseUsageMarker(meta) {
|
|
10
|
+
return { version: 1, inputTokenScope: meta.inputTokenScope,
|
|
11
|
+
...(meta.inputTokenScope === 'aggregate' && meta.contextDetails
|
|
12
|
+
? { contextDetails: meta.contextDetails, contextProvenance: 'provider-context-details' } : {}) };
|
|
13
|
+
}
|
|
6
14
|
function emptyUsage() {
|
|
7
15
|
return {
|
|
8
16
|
input: 0,
|
|
@@ -159,7 +167,35 @@ export function managedFailure(error, signal) {
|
|
|
159
167
|
};
|
|
160
168
|
}
|
|
161
169
|
/** @param {unknown} error @param {AbortSignal} [signal] */
|
|
170
|
+
/**
|
|
171
|
+
* The host image-offload contract, when this failure carries it.
|
|
172
|
+
*
|
|
173
|
+
* `IMAGE_OFFLOAD_REQUIRED` is not an LCX failure class: it is a request the
|
|
174
|
+
* route makes of DSH's durable image-offload owner, which matches on the exact
|
|
175
|
+
* code and `offloadImages` count before recording an `image/offload` decision
|
|
176
|
+
* and retrying. Normalizing it into the closed LCX taxonomy would drop both
|
|
177
|
+
* fields and turn a recoverable step into a terminal turn failure.
|
|
178
|
+
* @param {unknown} error
|
|
179
|
+
* @returns {LlmFailure | undefined}
|
|
180
|
+
*/
|
|
181
|
+
function imageOffloadFailure(error) {
|
|
182
|
+
if (!isObject(error))
|
|
183
|
+
return undefined;
|
|
184
|
+
const source = isObject(error.failure) ? error.failure : error;
|
|
185
|
+
if (source.code !== IMAGE_OFFLOAD_REQUIRED_CODE)
|
|
186
|
+
return undefined;
|
|
187
|
+
const offloadImages = source.offloadImages;
|
|
188
|
+
if (!Number.isSafeInteger(offloadImages) || offloadImages <= 0)
|
|
189
|
+
return undefined;
|
|
190
|
+
const message = typeof source.message === "string" && source.message.length > 0
|
|
191
|
+
? source.message
|
|
192
|
+
: "Request images exceed the route budget";
|
|
193
|
+
return { message, code: IMAGE_OFFLOAD_REQUIRED_CODE, offloadImages: offloadImages };
|
|
194
|
+
}
|
|
162
195
|
export function managedFailureChunk(error, signal) {
|
|
196
|
+
const offload = imageOffloadFailure(error);
|
|
197
|
+
if (offload !== undefined)
|
|
198
|
+
return { type: "finish", reason: { kind: "error", failure: offload } };
|
|
163
199
|
const managed = managedFailure(error, signal);
|
|
164
200
|
const failure = {
|
|
165
201
|
message: managed.message,
|
|
@@ -180,9 +216,9 @@ async function readWithSignal(reader, signal) {
|
|
|
180
216
|
if (!signal)
|
|
181
217
|
return reader.read();
|
|
182
218
|
if (signal.aborted)
|
|
183
|
-
throw signal
|
|
219
|
+
throw abortError(signal);
|
|
184
220
|
return new Promise((resolve, reject) => {
|
|
185
|
-
const onAbort = () => reject(signal
|
|
221
|
+
const onAbort = () => reject(abortError(signal));
|
|
186
222
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
187
223
|
reader.read().then(resolve, reject).finally(() => {
|
|
188
224
|
signal.removeEventListener("abort", onAbort);
|
|
@@ -285,8 +321,7 @@ async function* responseEvents(response, options = {}) {
|
|
|
285
321
|
try {
|
|
286
322
|
while (true) {
|
|
287
323
|
if (options.signal?.aborted)
|
|
288
|
-
throw (options.signal
|
|
289
|
-
Object.assign(new Error("request aborted"), { code: "LCX_ABORTED" }));
|
|
324
|
+
throw abortError(options.signal);
|
|
290
325
|
const { done, value } = await readWithSignal(reader, options.signal);
|
|
291
326
|
if (done)
|
|
292
327
|
break;
|
|
@@ -654,6 +689,7 @@ async function* normalizedResponseEvents(source, meta = {}, options = {}) {
|
|
|
654
689
|
const isServerToolItem = (value) => isObject(value) && (options.serverToolTypes?.has(String(value.type ?? "")) === true ||
|
|
655
690
|
options.isServerToolItem?.(value) === true);
|
|
656
691
|
const observeServerTool = (item) => {
|
|
692
|
+
meta.inputTokenScope = "aggregate";
|
|
657
693
|
const type = String(item.type);
|
|
658
694
|
if (type === "custom_tool_call" && typeof item.call_id === "string")
|
|
659
695
|
nativeServerSearchEchoCallIds.add(item.call_id);
|
|
@@ -683,6 +719,22 @@ async function* normalizedResponseEvents(source, meta = {}, options = {}) {
|
|
|
683
719
|
if (!isObject(raw))
|
|
684
720
|
continue;
|
|
685
721
|
const event = /** @type {UnknownRecord} */ raw;
|
|
722
|
+
if (options.serverToolTypes && ['response.completed', 'response.failed', 'response.incomplete'].includes(String(event.type))) {
|
|
723
|
+
meta.contextDetails = sanitizeContextDetails(isObject(event.response) && isObject(event.response.usage)
|
|
724
|
+
? event.response.usage.context_details : undefined);
|
|
725
|
+
}
|
|
726
|
+
// Scope is response-local evidence, including failed/incomplete terminal samples.
|
|
727
|
+
if (options.serverToolTypes && isObject(event.response)) {
|
|
728
|
+
const response = event.response;
|
|
729
|
+
const usage = isObject(response.usage) ? response.usage : {};
|
|
730
|
+
const details = isObject(usage.server_side_tool_usage_details) ? usage.server_side_tool_usage_details : {};
|
|
731
|
+
if (Number(usage.num_server_side_tools_used ?? 0) > 0 ||
|
|
732
|
+
Number(details.web_search_calls ?? 0) > 0 || Number(details.x_search_calls ?? 0) > 0)
|
|
733
|
+
meta.inputTokenScope = "aggregate";
|
|
734
|
+
if (Array.isArray(response.output))
|
|
735
|
+
if (response.output.some(isServerToolItem))
|
|
736
|
+
meta.inputTokenScope = "aggregate";
|
|
737
|
+
}
|
|
686
738
|
const index = Number.isInteger(event.output_index)
|
|
687
739
|
? Number(event.output_index)
|
|
688
740
|
: 0;
|
|
@@ -872,7 +924,7 @@ async function* normalizedResponseEvents(source, meta = {}, options = {}) {
|
|
|
872
924
|
meta.nativeServerSearchEchoCallIds = [...nativeServerSearchEchoCallIds];
|
|
873
925
|
const usage = isObject(response.usage) ? response.usage : {};
|
|
874
926
|
const details = isObject(usage.server_side_tool_usage_details) ? usage.server_side_tool_usage_details : {};
|
|
875
|
-
meta.inputTokenScope = serverToolIds.size > 0 || nativeOutput.some(isServerToolItem)
|
|
927
|
+
meta.inputTokenScope = meta.inputTokenScope === "aggregate" || serverToolIds.size > 0 || nativeOutput.some(isServerToolItem)
|
|
876
928
|
|| Number(usage.num_server_side_tools_used ?? 0) > 0
|
|
877
929
|
|| Number(details.web_search_calls ?? 0) > 0 || Number(details.x_search_calls ?? 0) > 0
|
|
878
930
|
? "aggregate" : "request";
|
|
@@ -1222,7 +1274,7 @@ async function* toDshChunks(events, signal, wireMeta, nativeReplayRoute) {
|
|
|
1222
1274
|
reason,
|
|
1223
1275
|
...(replay === undefined && !nativeReplayRoute ? {} : { replayState: {
|
|
1224
1276
|
...replay,
|
|
1225
|
-
response: { ...replay?.response, ...(nativeReplayRoute ? { lcxUsage:
|
|
1277
|
+
response: { ...replay?.response, ...(nativeReplayRoute && wireMeta?.inputTokenScope ? { lcxUsage: responseUsageMarker(wireMeta) } : {}) },
|
|
1226
1278
|
} }),
|
|
1227
1279
|
};
|
|
1228
1280
|
return;
|
|
@@ -1252,7 +1304,7 @@ async function* toDshChunks(events, signal, wireMeta, nativeReplayRoute) {
|
|
|
1252
1304
|
reason: normalizedFailure.code === "ABORTED"
|
|
1253
1305
|
? { kind: "aborted", failure: normalizedFailure }
|
|
1254
1306
|
: { kind: "error", failure: normalizedFailure },
|
|
1255
|
-
...(nativeReplayRoute ? { replayState: { response: { lcxUsage:
|
|
1307
|
+
...(nativeReplayRoute && wireMeta?.inputTokenScope ? { replayState: { response: { lcxUsage: responseUsageMarker(wireMeta) } } } : {}),
|
|
1256
1308
|
};
|
|
1257
1309
|
return;
|
|
1258
1310
|
}
|
|
@@ -1312,8 +1364,7 @@ export async function* streamResponsesRequest({ baseURL, provider, model, piMode
|
|
|
1312
1364
|
wireMeta.responseModel.length > 0)
|
|
1313
1365
|
output.responseModel = wireMeta.responseModel;
|
|
1314
1366
|
if (signal?.aborted)
|
|
1315
|
-
throw (signal
|
|
1316
|
-
Object.assign(new Error("request aborted"), { code: "LCX_ABORTED" }));
|
|
1367
|
+
throw abortError(signal);
|
|
1317
1368
|
if (output.stopReason === "pending")
|
|
1318
1369
|
throw Object.assign(new Error("Responses stream ended without a stop reason"), { code: "LCX_INVALID_SSE" });
|
|
1319
1370
|
if (output.stopReason === "aborted" || output.stopReason === "error")
|
|
@@ -1344,7 +1395,7 @@ export async function* streamResponsesRequest({ baseURL, provider, model, piMode
|
|
|
1344
1395
|
while (true) {
|
|
1345
1396
|
const result = await chunks.next();
|
|
1346
1397
|
if (watchdog.signal.aborted)
|
|
1347
|
-
throw watchdog.signal
|
|
1398
|
+
throw abortError(watchdog.signal);
|
|
1348
1399
|
if (result.done) {
|
|
1349
1400
|
exhausted = true;
|
|
1350
1401
|
watchdog.disarm();
|
package/lib/route.js
CHANGED
|
@@ -4,6 +4,7 @@ import { SessionId } from "@deepseek-ai/dsh-session";
|
|
|
4
4
|
import "@deepseek-ai/dsh-settings";
|
|
5
5
|
import { attributionHeaders, resolveRetryPolicy } from "@deepseek-ai/dsh-llm";
|
|
6
6
|
import { getBuiltinModels, getBuiltinProviders, } from "./pi-responses-runtime.js";
|
|
7
|
+
import { wasPi086ResponsesModel } from "./pi086-responses-allowlist.js";
|
|
7
8
|
/** @param {unknown} value */
|
|
8
9
|
function isRecord(value) {
|
|
9
10
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
@@ -15,16 +16,6 @@ function isPositiveInteger(value) {
|
|
|
15
16
|
function isPositiveFinite(value) {
|
|
16
17
|
return typeof value === "number" && Number.isFinite(value) && value > 0;
|
|
17
18
|
}
|
|
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
|
-
}
|
|
28
19
|
/** @param {unknown} value */
|
|
29
20
|
export function normalizeBaseURL(value) {
|
|
30
21
|
return String(value ?? "").trim().replace(/\/+$/u, "");
|
|
@@ -98,22 +89,24 @@ function retryAttempts(policy, fallback = 3) {
|
|
|
98
89
|
catch { }
|
|
99
90
|
return fallback;
|
|
100
91
|
}
|
|
101
|
-
/**
|
|
102
|
-
|
|
103
|
-
|
|
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;
|
|
92
|
+
/** Follow the live directory entry to its SettingsForms profile. */
|
|
93
|
+
function configuredProviderProfile(ctx, provider) {
|
|
94
|
+
// Only llm-pi-ai profiles: other provider plugins' profiles are not LCX routes (Decision 8).
|
|
111
95
|
try {
|
|
112
|
-
const entry =
|
|
113
|
-
|
|
96
|
+
const entry = ctx?.llm.listConfigurableProviders().find((candidate) => candidate.provider === provider && candidate.settingsNs === "llm-pi-ai");
|
|
97
|
+
if (!entry || (typeof entry.error === "string" && entry.error.trim()))
|
|
98
|
+
return undefined;
|
|
99
|
+
const descriptor = ctx?.settings.describe().find((candidate) => candidate.ns === entry.settingsNs);
|
|
100
|
+
let value = descriptor?.value;
|
|
101
|
+
for (const segment of entry.settingsPath) {
|
|
102
|
+
if (!isRecord(value))
|
|
103
|
+
return undefined;
|
|
104
|
+
value = value[segment];
|
|
105
|
+
}
|
|
106
|
+
return isRecord(value) ? value : undefined;
|
|
114
107
|
}
|
|
115
108
|
catch {
|
|
116
|
-
return
|
|
109
|
+
return undefined;
|
|
117
110
|
}
|
|
118
111
|
}
|
|
119
112
|
/** @type {Set<keyof ResponsesCompat>} */
|
|
@@ -135,6 +128,9 @@ function copyResponsesCompat(target, source) {
|
|
|
135
128
|
/** @param {unknown} provider @param {unknown} modelId */
|
|
136
129
|
function builtinResponsesModel(provider, modelId) {
|
|
137
130
|
const providerName = String(provider ?? "");
|
|
131
|
+
// A newer plugin-Pi catalog must not grant LCX a route that Pi 0.86 lacked.
|
|
132
|
+
if (!wasPi086ResponsesModel(providerName, String(modelId ?? "")))
|
|
133
|
+
return undefined;
|
|
138
134
|
const builtinProvider = getBuiltinProviders().find((candidate) => candidate === providerName);
|
|
139
135
|
if (builtinProvider === undefined)
|
|
140
136
|
return undefined;
|
|
@@ -210,10 +206,9 @@ function isLcxCapabilityRoute(provider, model) {
|
|
|
210
206
|
export function resolveResponsesRouteConfig(ctx, options, policy) {
|
|
211
207
|
const provider = String(options?.provider ?? "");
|
|
212
208
|
const model = String(options?.model ?? "");
|
|
213
|
-
if (!provider.trim() || !/^gpt-/iu.test(model)
|
|
209
|
+
if (!provider.trim() || !/^gpt-/iu.test(model))
|
|
214
210
|
return undefined;
|
|
215
|
-
const
|
|
216
|
-
const profile = section?.providers?.[provider];
|
|
211
|
+
const profile = configuredProviderProfile(ctx, provider);
|
|
217
212
|
const lcxCapabilityRoute = isLcxCapabilityRoute(provider, model);
|
|
218
213
|
if (!isRecord(profile))
|
|
219
214
|
return undefined;
|
|
@@ -268,10 +263,9 @@ export function resolveResponsesRouteConfig(ctx, options, policy) {
|
|
|
268
263
|
export function resolveGrokResponsesRouteConfig(ctx, options, policy) {
|
|
269
264
|
const provider = String(options?.provider ?? "");
|
|
270
265
|
const model = String(options?.model ?? "");
|
|
271
|
-
if (!provider.trim() || !/^grok/iu.test(model)
|
|
266
|
+
if (!provider.trim() || !/^grok/iu.test(model))
|
|
272
267
|
return undefined;
|
|
273
|
-
const
|
|
274
|
-
const configured = section?.providers?.[provider];
|
|
268
|
+
const configured = configuredProviderProfile(ctx, provider);
|
|
275
269
|
if (!isRecord(configured))
|
|
276
270
|
return undefined;
|
|
277
271
|
const selectedBuiltin = builtinResponsesModel(provider, model);
|
|
@@ -337,9 +331,6 @@ export async function resolveApiKey(ctx, config) {
|
|
|
337
331
|
const resolved = await ctx?.credentials.resolve(credentialRef(config.apiKeyEnv));
|
|
338
332
|
if (resolved?.value.trim())
|
|
339
333
|
return resolved.value.trim();
|
|
340
|
-
const ambient = String(process.env[config.apiKeyEnv] ?? "").trim();
|
|
341
|
-
if (ambient)
|
|
342
|
-
return ambient;
|
|
343
334
|
const error = new Error(`DSH provider credential is unavailable: ${config.apiKeyEnv}`);
|
|
344
335
|
error.code = "LCX_CREDENTIAL_UNAVAILABLE";
|
|
345
336
|
throw error;
|
package/lib/search-accounting.js
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
export const object = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
2
2
|
const count = (v) => typeof v === 'number' && Number.isSafeInteger(v) && v >= 0;
|
|
3
|
+
/** Copy only complete, safe counters; provider extensions never enter durable metadata. */
|
|
4
|
+
export function sanitizeContextDetails(value) {
|
|
5
|
+
if (!object(value) || !count(value.input_tokens) || !count(value.output_tokens))
|
|
6
|
+
return;
|
|
7
|
+
return { input_tokens: value.input_tokens, output_tokens: value.output_tokens };
|
|
8
|
+
}
|
|
3
9
|
export const zeroBuckets = () => ({ uncachedInputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 });
|
|
4
10
|
export const isSearchTool = (name) => name === 'web_search' || name === 'websearch_gpt_advanced';
|
|
5
11
|
export function auxiliaryUsageOf(event, toolName) {
|
|
@@ -69,6 +75,12 @@ export function addTurnUsage(base, records) {
|
|
|
69
75
|
}
|
|
70
76
|
/** The metadata is presentation/accounting state; it never changes request messages. */
|
|
71
77
|
export function aggregateContextOf(event) {
|
|
78
|
+
const mark = contextMarkerOf(event);
|
|
79
|
+
if (mark === undefined)
|
|
80
|
+
return;
|
|
81
|
+
return mark?.version === 1 && mark.inputTokenScope === 'aggregate';
|
|
82
|
+
}
|
|
83
|
+
function contextMarkerOf(event) {
|
|
72
84
|
if (!object(event) || !['assistant/message', 'assistant/attempt'].includes(event.type) || !object(event.data))
|
|
73
85
|
return;
|
|
74
86
|
const chunks = Array.isArray(event.data.stream) ? event.data.stream.filter((e) => e.type === 'chunk').map((e) => e.chunk) : [];
|
|
@@ -77,10 +89,14 @@ export function aggregateContextOf(event) {
|
|
|
77
89
|
return;
|
|
78
90
|
const replay = chunks.findLast((c) => c?.type === 'finish')?.replayState;
|
|
79
91
|
const mark = replay?.response?.lcxUsage;
|
|
80
|
-
if (mark
|
|
81
|
-
return mark
|
|
82
|
-
//
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
92
|
+
if (object(mark) && mark.version === 1 && ['request', 'aggregate'].includes(mark.inputTokenScope))
|
|
93
|
+
return mark;
|
|
94
|
+
// Without a valid durable scope marker, preserve DSH's ordinary usage baseline.
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
export function providerContextOf(event) {
|
|
98
|
+
const mark = contextMarkerOf(event);
|
|
99
|
+
if (mark?.inputTokenScope !== 'aggregate' || mark.contextProvenance !== 'provider-context-details')
|
|
100
|
+
return;
|
|
101
|
+
return sanitizeContextDetails(mark.contextDetails);
|
|
86
102
|
}
|
package/lib/search-usage.js
CHANGED
|
@@ -1,13 +1,25 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import {
|
|
3
|
-
import { addUsage, aggregateContextOf, auxiliaryUsageOf, isSearchTool, zeroBuckets } from './search-accounting.js';
|
|
2
|
+
import { canonicalHeader, headerEquals } from '@deepseek-ai/dsh-session';
|
|
3
|
+
import { addUsage, aggregateContextOf, auxiliaryUsageOf, isSearchTool, providerContextOf, zeroBuckets } from './search-accounting.js';
|
|
4
4
|
const n = z.number().int().nonnegative();
|
|
5
|
-
const schema = z.object({ auxiliary: z.object({ uncachedInputTokens: n, outputTokens: n, cacheReadTokens: n, cacheWriteTokens: n }).strict(), aggregateContext: z.boolean()
|
|
5
|
+
const schema = z.object({ auxiliary: z.object({ uncachedInputTokens: n, outputTokens: n, cacheReadTokens: n, cacheWriteTokens: n }).strict(), aggregateContext: z.boolean(),
|
|
6
|
+
contextDetails: z.object({ input_tokens: n, output_tokens: n }).strict().optional(), contextProvenance: z.literal('provider-context-details').optional() }).strict();
|
|
7
|
+
const headerKey = (header) => header === undefined ? undefined : JSON.stringify(canonicalHeader(header));
|
|
6
8
|
/** Separate, durable extension: the host's own usage projection remains primary-call billing. */
|
|
7
9
|
export const searchUsageProjection = {
|
|
8
|
-
key: 'lcxSearchUsage', stateVersion:
|
|
10
|
+
key: 'lcxSearchUsage', stateVersion: 3, stateSchema: schema.extend({ pending: z.record(z.string(), z.string()), headerKey: z.string().optional(), sampleHeaderKey: z.string().optional() }),
|
|
9
11
|
init: () => ({ auxiliary: zeroBuckets(), aggregateContext: false, pending: {} }),
|
|
10
12
|
apply(state, event) {
|
|
13
|
+
if (event.type === 'request/header')
|
|
14
|
+
return { ...state, headerKey: headerKey(event.data.header) };
|
|
15
|
+
// DSH measure anchors assistant/message only. Attempts must not transplant a
|
|
16
|
+
// newer context sample onto an older successful measurement anchor.
|
|
17
|
+
if (event.type === 'assistant/message' || event.type === 'assistant/attempt') {
|
|
18
|
+
const contextDetails = event.type === 'assistant/message' ? providerContextOf(event) : undefined;
|
|
19
|
+
return { ...state, aggregateContext: aggregateContextOf(event) ?? state.aggregateContext, contextDetails,
|
|
20
|
+
contextProvenance: contextDetails ? 'provider-context-details' : undefined,
|
|
21
|
+
sampleHeaderKey: contextDetails ? state.headerKey : undefined };
|
|
22
|
+
}
|
|
11
23
|
if (event.type === 'tool/call' && isSearchTool(event.data.name))
|
|
12
24
|
return { ...state, pending: { ...state.pending, [event.data.callId]: event.data.name } };
|
|
13
25
|
const callId = event.type === 'tool/result' ? event.data.message.source.callId : undefined;
|
|
@@ -16,17 +28,17 @@ export const searchUsageProjection = {
|
|
|
16
28
|
if (callId && Object.hasOwn(state.pending, callId)) {
|
|
17
29
|
const pending = { ...state.pending };
|
|
18
30
|
delete pending[callId];
|
|
19
|
-
return { auxiliary, aggregateContext, pending };
|
|
31
|
+
return { ...state, auxiliary, aggregateContext, pending };
|
|
20
32
|
}
|
|
21
33
|
if (event.type === 'turn/end' && Object.keys(state.pending).length)
|
|
22
|
-
return { auxiliary, aggregateContext, pending: {} };
|
|
34
|
+
return { ...state, auxiliary, aggregateContext, pending: {} };
|
|
23
35
|
return auxiliary === state.auxiliary && aggregateContext === state.aggregateContext ? state : { ...state, auxiliary, aggregateContext };
|
|
24
36
|
},
|
|
25
|
-
wire: { viewSchema: schema, view: ({ auxiliary, aggregateContext }) => ({ auxiliary, aggregateContext }) },
|
|
37
|
+
wire: { viewSchema: schema, view: ({ auxiliary, aggregateContext, contextDetails, contextProvenance }) => ({ auxiliary, aggregateContext, ...(contextDetails ? { contextDetails, contextProvenance } : {}) }) },
|
|
26
38
|
};
|
|
27
39
|
const installed = new WeakMap();
|
|
28
40
|
/** Own a reversible adapter on the public measure method, never a DSH file or private fold. */
|
|
29
|
-
export function installSearchMeasurement(meter) {
|
|
41
|
+
export function installSearchMeasurement(meter, projections) {
|
|
30
42
|
const existing = installed.get(meter);
|
|
31
43
|
if (existing) {
|
|
32
44
|
existing.refs++;
|
|
@@ -37,17 +49,19 @@ export function installSearchMeasurement(meter) {
|
|
|
37
49
|
} };
|
|
38
50
|
}
|
|
39
51
|
const original = meter.measure, descriptor = Object.getOwnPropertyDescriptor(meter, 'measure');
|
|
40
|
-
const cursors = new WeakMap();
|
|
41
52
|
function measure(session, header) {
|
|
42
53
|
const value = original.call(this, session, header);
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
54
|
+
const state = projections.stateOf(session, 'lcxSearchUsage');
|
|
55
|
+
if (!state?.aggregateContext)
|
|
56
|
+
return value;
|
|
57
|
+
const effectiveHeader = header ?? session.requestHeader();
|
|
58
|
+
if (state.contextDetails && state.sampleHeaderKey !== undefined && effectiveHeader !== undefined
|
|
59
|
+
&& headerEquals(JSON.parse(state.sampleHeaderKey), canonicalHeader(effectiveHeader))) {
|
|
60
|
+
const tokens = state.contextDetails.input_tokens;
|
|
61
|
+
return Object.freeze({ ...value, baseline: Object.freeze({ kind: 'estimated', tokens }),
|
|
62
|
+
totalTokens: Math.max(0, tokens + value.surfaceDeltaTokens) });
|
|
48
63
|
}
|
|
49
|
-
|
|
50
|
-
if (!state.aggregate || value.baseline.kind !== 'usage')
|
|
64
|
+
if (value.baseline.kind !== 'usage')
|
|
51
65
|
return value;
|
|
52
66
|
// The official measure already prices retained images/files and surface replacements.
|
|
53
67
|
// Only discard its unsuitable aggregate anchor; keep its current surface and node prices.
|
|
@@ -80,7 +94,7 @@ function release(meter) {
|
|
|
80
94
|
}
|
|
81
95
|
export function installSearchUsage(ctx) {
|
|
82
96
|
ctx.inject(['sessionProjections'], c => { c.sessionProjections.register(searchUsageProjection); });
|
|
83
|
-
ctx.inject(['tokenMeter'], c => {
|
|
84
|
-
c.effect(() => installSearchMeasurement(c.tokenMeter), 'lcx search context measurement');
|
|
97
|
+
ctx.inject(['tokenMeter', 'sessionProjections'], c => {
|
|
98
|
+
c.effect(() => installSearchMeasurement(c.tokenMeter, c.sessionProjections), 'lcx search context measurement');
|
|
85
99
|
});
|
|
86
100
|
}
|
package/lib/service-mutex.js
CHANGED
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
if (signal?.reason instanceof Error)
|
|
3
|
-
return signal.reason;
|
|
4
|
-
return new DOMException("The operation was aborted", "AbortError");
|
|
5
|
-
}
|
|
1
|
+
import { abortError } from "./abort-error.js";
|
|
6
2
|
export class ServiceMutex {
|
|
7
3
|
locked = false;
|
|
8
4
|
closed = false;
|
|
@@ -14,7 +10,7 @@ export class ServiceMutex {
|
|
|
14
10
|
if (this.closed)
|
|
15
11
|
return Promise.reject(this.closeReason ?? new Error("service mutex closed"));
|
|
16
12
|
if (signal?.aborted)
|
|
17
|
-
return Promise.reject(
|
|
13
|
+
return Promise.reject(abortError(signal));
|
|
18
14
|
if (!this.locked) {
|
|
19
15
|
this.locked = true;
|
|
20
16
|
return Promise.resolve(() => this.release());
|
|
@@ -27,7 +23,7 @@ export class ServiceMutex {
|
|
|
27
23
|
if (index >= 0)
|
|
28
24
|
this.queue.splice(index, 1);
|
|
29
25
|
signal.removeEventListener("abort", onAbort);
|
|
30
|
-
reject(
|
|
26
|
+
reject(abortError(signal));
|
|
31
27
|
};
|
|
32
28
|
waiter.onAbort = onAbort;
|
|
33
29
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
@@ -43,7 +39,7 @@ export class ServiceMutex {
|
|
|
43
39
|
if (waiter.signal && waiter.onAbort)
|
|
44
40
|
waiter.signal.removeEventListener("abort", waiter.onAbort);
|
|
45
41
|
if (waiter.signal?.aborted) {
|
|
46
|
-
waiter.reject(
|
|
42
|
+
waiter.reject(abortError(waiter.signal));
|
|
47
43
|
continue;
|
|
48
44
|
}
|
|
49
45
|
waiter.resolve(() => this.release());
|
package/lib/token-budget.js
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
export const PORTABLE_BUDGET_ERROR_CODE = "LCX_PORTABLE_BUDGET_EXCEEDED";
|
|
2
|
+
export const PORTABLE_UNSUPPORTED_CONTENT_CODE = "LCX_CHECKPOINT_PORTABLE_UNSUPPORTED_CONTENT";
|
|
3
|
+
function unsupportedContent(kind) {
|
|
4
|
+
return Object.assign(new Error(`LCX cannot budget unsupported message content: ${String(kind)}`), {
|
|
5
|
+
code: PORTABLE_UNSUPPORTED_CONTENT_CODE,
|
|
6
|
+
});
|
|
7
|
+
}
|
|
2
8
|
const MAX_BUDGET_INPUT_CHARS = 2_000_000;
|
|
3
9
|
const CONSERVATIVE_IMAGE_TOKEN_COST = 2_048;
|
|
4
10
|
const CJK = /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}]/u;
|
|
@@ -98,7 +104,7 @@ function visibleContent(content) {
|
|
|
98
104
|
const parts = [];
|
|
99
105
|
for (const part of content) {
|
|
100
106
|
if (!isObject(part) || typeof part.type !== "string")
|
|
101
|
-
|
|
107
|
+
throw unsupportedContent("missing block type");
|
|
102
108
|
if (["text", "input_text", "output_text"].includes(part.type)) {
|
|
103
109
|
if (typeof part.text !== "string")
|
|
104
110
|
return undefined;
|
|
@@ -120,27 +126,11 @@ function visibleContent(content) {
|
|
|
120
126
|
parts.push({ type: "tool-call", id, name, arguments: argumentsText });
|
|
121
127
|
continue;
|
|
122
128
|
}
|
|
123
|
-
if (part.type === "tool-result") {
|
|
124
|
-
const toolCallId = safeScalar(part.toolCallId);
|
|
125
|
-
const toolName = safeScalar(part.toolName ?? part.name ?? "unknown");
|
|
126
|
-
const nested = visibleContent(part.content);
|
|
127
|
-
if (toolCallId === undefined ||
|
|
128
|
-
toolName === undefined ||
|
|
129
|
-
nested === undefined)
|
|
130
|
-
return undefined;
|
|
131
|
-
parts.push({
|
|
132
|
-
type: "tool-result",
|
|
133
|
-
toolCallId,
|
|
134
|
-
toolName,
|
|
135
|
-
content: nested,
|
|
136
|
-
});
|
|
137
|
-
continue;
|
|
138
|
-
}
|
|
139
129
|
if (["image", "input_image", "output_image", "dsh_image_attachment"].includes(part.type)) {
|
|
140
130
|
parts.push({ type: part.type, image: true });
|
|
141
131
|
continue;
|
|
142
132
|
}
|
|
143
|
-
|
|
133
|
+
throw unsupportedContent(part.type);
|
|
144
134
|
}
|
|
145
135
|
return parts;
|
|
146
136
|
}
|
|
@@ -177,12 +167,16 @@ export function modelVisibleBudgetView(item) {
|
|
|
177
167
|
}
|
|
178
168
|
if (item.type !== undefined && item.type !== "message")
|
|
179
169
|
return undefined;
|
|
180
|
-
if (!["developer", "system", "user", "assistant"].includes(String(item.role ?? "")))
|
|
181
|
-
|
|
170
|
+
if (!["developer", "system", "user", "assistant", "tool"].includes(String(item.role ?? "")))
|
|
171
|
+
throw unsupportedContent(item.role);
|
|
182
172
|
const content = visibleContent(item.content);
|
|
173
|
+
if (item.role === "tool" && safeScalar(item.toolCallId) === undefined)
|
|
174
|
+
throw unsupportedContent("tool message without toolCallId");
|
|
183
175
|
return content === undefined
|
|
184
176
|
? undefined
|
|
185
|
-
:
|
|
177
|
+
: item.role === "tool"
|
|
178
|
+
? { role: "tool", toolCallId: safeScalar(item.toolCallId), content }
|
|
179
|
+
: { role: String(item.role), content };
|
|
186
180
|
}
|
|
187
181
|
/**
|
|
188
182
|
* Images have provider/model-dependent costs. Count each known image block with
|