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.
@@ -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
+ }
@@ -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.85.1 Responses semantics.
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.85.1 uses explicit 30m only for long retention; short is implicit.
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
@@ -1,8 +1,16 @@
1
1
  // @ts-check
2
2
  import { createAssistantMessageEventStream, processResponsesStream, } from "./pi-responses-runtime.js";
3
- import { ToolCallId } from "@deepseek-ai/dsh-llm";
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.reason;
219
+ throw abortError(signal);
184
220
  return new Promise((resolve, reject) => {
185
- const onAbort = () => reject(signal.reason);
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.reason ??
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: { version: 1, inputTokenScope: wireMeta?.inputTokenScope ?? "aggregate" } } : {}) },
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: { version: 1, inputTokenScope: "aggregate" } } } } : {}),
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.reason ??
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.reason;
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
- /** @param {RouteContext | null | undefined} ctx @param {string} namespace */
102
- export function settingsValue(ctx, 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;
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 = list.call(llm).find((candidate) => candidate.provider === provider && candidate.settingsNs === "llm-pi-ai");
113
- return typeof entry?.error !== "string" || entry.error.trim() === "";
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 false;
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) || !providerDirectoryUsable(ctx, provider))
209
+ if (!provider.trim() || !/^gpt-/iu.test(model))
214
210
  return undefined;
215
- const section = settingsValue(ctx, "llm-pi-ai");
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) || !providerDirectoryUsable(ctx, provider))
266
+ if (!provider.trim() || !/^grok/iu.test(model))
272
267
  return undefined;
273
- const section = settingsValue(ctx, "llm-pi-ai");
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;
@@ -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?.version === 1 && ['request', 'aggregate'].includes(mark.inputTokenScope))
81
- return mark.inputTokenScope === 'aggregate';
82
- // Read the already-installed local candidate without rewriting its logs.
83
- if (sample.inputTokenScope === 'aggregate' || sample.inputTokenScope === 'request')
84
- return sample.inputTokenScope === 'aggregate';
85
- return replay?.grokNative?.kind === 'xai-responses-native-search' && replay.grokNative.version === 3;
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
  }
@@ -1,13 +1,25 @@
1
1
  import { z } from 'zod';
2
- import { SessionSeq } from '@deepseek-ai/dsh-session';
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() }).strict();
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: 2, stateSchema: schema.extend({ pending: z.record(z.string(), z.string()) }),
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
- let state = cursors.get(session) ?? { seq: 0, aggregate: false };
44
- while (state.seq < session.seq) {
45
- const e = session.eventAt(SessionSeq(state.seq++));
46
- if (e?.type === 'assistant/message')
47
- state.aggregate = aggregateContextOf(e) ?? false;
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
- cursors.set(session, state);
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
  }
@@ -1,8 +1,4 @@
1
- function abortReason(signal) {
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(abortReason(signal));
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(abortReason(signal));
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(abortReason(waiter.signal));
42
+ waiter.reject(abortError(waiter.signal));
47
43
  continue;
48
44
  }
49
45
  waiter.resolve(() => this.release());
@@ -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
- return undefined;
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
- return undefined;
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
- return undefined;
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
- : { role: String(item.role), content };
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