@deepstrike/sdk 0.2.60 → 0.2.61

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.
@@ -31,6 +31,54 @@ export const registryEvidence = [
31
31
  verifiedAt: "2026-08-12",
32
32
  },
33
33
  ];
34
+ export const cacheCapabilityEvidence = [
35
+ {
36
+ endpointId: "anthropic.messages",
37
+ source: "https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching",
38
+ verifiedAt: "2026-08-26",
39
+ classification: "documentation",
40
+ usageFields: ["cache_read_input_tokens", "cache_creation_input_tokens"],
41
+ },
42
+ {
43
+ endpointId: "deepseek.openai",
44
+ source: "https://api-docs.deepseek.com/guides/kv_cache",
45
+ verifiedAt: "2026-08-26",
46
+ classification: "documentation",
47
+ usageFields: ["prompt_cache_hit_tokens", "prompt_cache_miss_tokens"],
48
+ },
49
+ ];
50
+ export const tokenMeasurementEvidence = [
51
+ {
52
+ endpointId: "anthropic.messages",
53
+ source: "https://github.com/anthropics/anthropic-sdk-typescript/blob/main/api.md#count-tokens",
54
+ verifiedAt: "2026-08-26",
55
+ providerApiState: "supported",
56
+ adapterState: "available",
57
+ method: "provider_preflight",
58
+ coverage: ["system", "messages", "tools"],
59
+ sdk: "@anthropic-ai/sdk ^0.99.0",
60
+ },
61
+ {
62
+ endpointId: "gemini.google",
63
+ source: "https://ai.google.dev/api/tokens#method-models.countTokens",
64
+ verifiedAt: "2026-08-26",
65
+ providerApiState: "supported",
66
+ adapterState: "available",
67
+ method: "provider_preflight",
68
+ coverage: ["contents", "system", "tools", "provider_options"],
69
+ sdk: "@google/generative-ai ^0.24.1",
70
+ },
71
+ {
72
+ endpointId: "openai.responses",
73
+ source: "https://github.com/openai/openai-node/tree/master/src/resources/responses/input-tokens.ts",
74
+ verifiedAt: "2026-08-26",
75
+ providerApiState: "supported",
76
+ adapterState: "available",
77
+ method: "provider_preflight",
78
+ coverage: ["input", "instructions", "tools", "provider_options"],
79
+ sdk: "openai ^7.5.0",
80
+ },
81
+ ];
34
82
  const POLICY = {
35
83
  "anthropic/claude-opus-4-1": { maxTurns: 50 },
36
84
  "anthropic/claude-opus-4-7": { maxTurns: 50 },
@@ -114,11 +162,11 @@ const DEFAULT_ENDPOINT = {
114
162
  anthropic: "anthropic.messages",
115
163
  openai: "openai.chat",
116
164
  minimax: "minimax.anthropic",
117
- deepseek: "deepseek.anthropic",
118
- kimi: "kimi.anthropic",
119
- qwen: "qwen.anthropic",
165
+ deepseek: "deepseek.openai",
166
+ kimi: "kimi.openai",
167
+ qwen: "qwen.dashscope",
120
168
  gemini: "gemini.google",
121
- glm: "glm.anthropic",
169
+ glm: "glm.openai",
122
170
  baai: "baai.self-hosted.embeddings",
123
171
  ollama: "ollama.local",
124
172
  };
@@ -137,6 +185,9 @@ const DEFAULT_MODEL = {
137
185
  export function defaultModelForProvider(providerId) {
138
186
  return DEFAULT_MODEL[providerId];
139
187
  }
188
+ export function defaultEndpointForProvider(providerId) {
189
+ return DEFAULT_ENDPOINT[providerId];
190
+ }
140
191
  function endpointFor(providerId, modelId) {
141
192
  if (providerId === "openai") {
142
193
  if (modelId.startsWith("text-embedding-"))
@@ -225,8 +276,10 @@ export const protocolRuntimeCapabilities = {
225
276
  "ollama-chat": OLLAMA_PROTOCOL_CAPABILITIES,
226
277
  };
227
278
  export const endpointRuntimeCapabilities = {
228
- "anthropic.messages": { nativeTokenCounting: true },
279
+ "anthropic.messages": { nativeTokenCounting: true, promptCaching: true },
280
+ "deepseek.openai": { promptCaching: true },
229
281
  "gemini.google": { nativeTokenCounting: true },
282
+ "openai.responses": { nativeTokenCounting: true },
230
283
  };
231
284
  export function resolveEffectiveCapability(layers) {
232
285
  const evidence = layers.filter(layer => layer.state !== "unknown").map(layer => layer.layer);
@@ -285,7 +338,14 @@ export function resolveEffectiveModelCapabilities(input) {
285
338
  ]),
286
339
  parallelToolCalls: protocolBoolean(protocol.parallelToolCalls, overrides?.parallelToolCalls),
287
340
  structuredOutput: protocolBoolean(protocol.structuredOutput, overrides?.structuredOutput),
288
- promptCaching: protocolBoolean(protocol.promptCaching, overrides?.promptCaching),
341
+ promptCaching: resolveEffectiveCapability([
342
+ { layer: "protocol", state: booleanState(protocol.promptCaching), value: protocol.promptCaching },
343
+ {
344
+ layer: "endpoint",
345
+ state: booleanState(input.endpointCapabilities?.promptCaching),
346
+ value: input.endpointCapabilities?.promptCaching,
347
+ },
348
+ ]),
289
349
  nativeTokenCounting: resolveEffectiveCapability([
290
350
  { layer: "endpoint", state: booleanState(input.endpointCapabilities?.nativeTokenCounting) },
291
351
  ]),
@@ -307,8 +367,12 @@ export function generationProtocol(protocol) {
307
367
  return undefined;
308
368
  }
309
369
  }
310
- export function endpointCapabilitiesFor(endpointId, preserveEndpointIdentity) {
311
- return preserveEndpointIdentity ? endpointRuntimeCapabilities[endpointId] : undefined;
370
+ export function endpointCapabilitiesFor(endpointId, preserveEndpointIdentity, preserveCacheEvidence = preserveEndpointIdentity) {
371
+ const capabilities = preserveEndpointIdentity ? endpointRuntimeCapabilities[endpointId] : undefined;
372
+ if (!capabilities || preserveCacheEvidence)
373
+ return capabilities;
374
+ const { promptCaching: _promptCaching, ...withoutCacheEvidence } = capabilities;
375
+ return withoutCacheEvidence;
312
376
  }
313
377
  export function isKnownProviderId(value) {
314
378
  return value in DEFAULT_ENDPOINT;
@@ -40,8 +40,8 @@ function validCount(raw, field) {
40
40
  const value = raw[field];
41
41
  if (value === undefined)
42
42
  return undefined;
43
- if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
44
- throw new ProtocolResponseError("ollama-chat", `${field} must be a non-negative finite number`);
43
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
44
+ throw new ProtocolResponseError("ollama-chat", `${field} must be a non-negative safe integer`);
45
45
  }
46
46
  return value;
47
47
  }
@@ -2,6 +2,7 @@ import { assistantReplayKey } from "../runtime/provider-replay.js";
2
2
  import { openAICachedPromptTokens, stablePromptCacheKey, ThinkingTagStreamExtractor, } from "./base.js";
3
3
  import { normalizeCanonicalContext, projectToolOutputToText } from "./content-normalization.js";
4
4
  import { normalizeToolCall } from "./base.js";
5
+ import { normalizeOpenAIUsage } from "./usage-normalizer.js";
5
6
  import { DEGRADED_REASONING_PLACEHOLDER, assessReasoningReplay, validateOpenAIChatReplay, } from "./replay-validator.js";
6
7
  import { ProtocolResponseError, } from "./protocol-adapter.js";
7
8
  import { OPENAI_CHAT_PROTOCOL_CAPABILITIES } from "./protocol-capabilities.js";
@@ -349,6 +350,8 @@ export class OpenAIChatAdapter {
349
350
  ...(stopReason ? { stopReason } : {}),
350
351
  ...(state.finishReason ? { rawStopReason: state.finishReason } : {}),
351
352
  ...(providerUsage ? { providerUsage } : {}),
353
+ ...(providerUsage?.cacheTelemetryStatus ? { cacheTelemetryStatus: providerUsage.cacheTelemetryStatus } : {}),
354
+ ...(providerUsage?.cacheTelemetrySource ? { cacheTelemetrySource: providerUsage.cacheTelemetrySource } : {}),
352
355
  });
353
356
  }
354
357
  const replay = streamReplay(state);
@@ -371,12 +374,7 @@ export class OpenAIChatAdapter {
371
374
  const reasoningTokens = details && typeof details === "object"
372
375
  ? numberField(details, "reasoning_tokens")
373
376
  : undefined;
374
- return {
375
- inputTokens: inputTokens ?? 0,
376
- outputTokens: outputTokens ?? 0,
377
- ...(cacheReadInputTokens > 0 ? { cacheReadInputTokens } : {}),
378
- ...(reasoningTokens !== undefined ? { reasoningTokens } : {}),
379
- };
377
+ return normalizeOpenAIUsage(usage);
380
378
  }
381
379
  normalizeStopReason(raw) {
382
380
  if (raw === undefined)
@@ -1,13 +1,14 @@
1
1
  import { normalizeCanonicalContext } from "./content-normalization.js";
2
2
  import { normalizeToolCall, UnsupportedModalityError } from "./base.js";
3
+ import { normalizeOpenAIUsage } from "./usage-normalizer.js";
3
4
  import { ProtocolResponseError, } from "./protocol-adapter.js";
4
5
  import { OPENAI_RESPONSES_PROTOCOL_CAPABILITIES } from "./protocol-capabilities.js";
5
6
  function numberField(raw, field) {
6
7
  const value = raw[field];
7
8
  if (value === undefined || value === null)
8
9
  return undefined;
9
- if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
10
- throw new ProtocolResponseError("openai-responses", `usage.${field} must be a non-negative finite number`);
10
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
11
+ throw new ProtocolResponseError("openai-responses", `usage.${field} must be a non-negative safe integer`);
11
12
  }
12
13
  return value;
13
14
  }
@@ -267,6 +268,12 @@ export class OpenAIResponsesAdapter {
267
268
  ...(inputTokens ? { inputTokens } : {}),
268
269
  ...(outputTokens ? { outputTokens } : {}),
269
270
  ...(cacheReadInputTokens ? { cacheReadInputTokens } : {}),
271
+ ...(providerUsage?.cacheTelemetryStatus
272
+ ? { cacheTelemetryStatus: providerUsage.cacheTelemetryStatus }
273
+ : {}),
274
+ ...(providerUsage?.cacheTelemetrySource
275
+ ? { cacheTelemetrySource: providerUsage.cacheTelemetrySource }
276
+ : {}),
270
277
  ...(providerUsage && (inputTokens || outputTokens) ? { providerUsage } : {}),
271
278
  ...(stopReason ? { stopReason } : {}),
272
279
  ...(rawStopReason ? { rawStopReason } : {}),
@@ -294,16 +301,11 @@ export class OpenAIResponsesAdapter {
294
301
  throw new ProtocolResponseError("openai-responses", "usage.output_tokens_details must be an object");
295
302
  }
296
303
  cacheReadTokens(usage);
297
- const reasoningTokens = outputDetails
298
- ? numberField(outputDetails, "reasoning_tokens")
299
- : undefined;
304
+ if (outputDetails)
305
+ numberField(outputDetails, "reasoning_tokens");
300
306
  if (inputTokens === undefined && outputTokens === undefined)
301
307
  return undefined;
302
- return {
303
- inputTokens: inputTokens ?? 0,
304
- outputTokens: outputTokens ?? 0,
305
- ...(reasoningTokens !== undefined ? { reasoningTokens } : {}),
306
- };
308
+ return normalizeOpenAIUsage(usage);
307
309
  }
308
310
  normalizeStopReason(raw) {
309
311
  if (raw === undefined)
@@ -1,5 +1,5 @@
1
1
  import OpenAI from "openai";
2
- import type { LLMProvider, Message, ProviderRunState, RenderedContext, RuntimePolicy, StreamEvent, ToolSchema } from "../types.js";
2
+ import type { LLMProvider, Message, PromptMeasurement, ProviderRunState, RenderedContext, RuntimePolicy, StreamEvent, ToolSchema } from "../types.js";
3
3
  import { CircuitBreaker } from "./base.js";
4
4
  import { type CanonicalAdapterInput } from "./content-normalization.js";
5
5
  import { OpenAIResponsesAdapter, type OpenAIResponsesRunState } from "./openai-responses-adapter.js";
@@ -14,6 +14,7 @@ export declare class OpenAIResponsesProvider implements LLMProvider {
14
14
  protected baseDelay: number;
15
15
  protected readonly responses: OpenAIResponsesAdapter;
16
16
  private readonly resolvedRuntimePolicy;
17
+ private readonly directNativeTokenCounting;
17
18
  private resolvedRuntime?;
18
19
  constructor(apiKey: string, model?: string, retry?: {
19
20
  maxRetries: number;
@@ -24,6 +25,10 @@ export declare class OpenAIResponsesProvider implements LLMProvider {
24
25
  createRunState(): OpenAIResponsesRunState;
25
26
  private adapterInput;
26
27
  complete(context: RenderedContext, tools: ToolSchema[], extensions?: Record<string, unknown>): Promise<Message>;
28
+ /** spc_024-05: native preflight via the official Responses input-token count endpoint. Counts
29
+ * the exact create request plan (stateful `previous_response_id` continuation included) —
30
+ * native measurement belongs to the verified official endpoint, not the wire protocol. */
31
+ countTokens(context: RenderedContext, tools: ToolSchema[], extensions?: Record<string, unknown>, state?: ProviderRunState): Promise<PromptMeasurement>;
27
32
  stream(context: RenderedContext, tools: ToolSchema[], extensions?: Record<string, unknown>, state?: ProviderRunState, signal?: AbortSignal): AsyncIterable<StreamEvent>;
28
33
  private builtinTools;
29
34
  private requestExtensions;
@@ -6,6 +6,14 @@ import { endpointProfiles } from "./endpoints.js";
6
6
  import { OpenAIResponsesAdapter, } from "./openai-responses-adapter.js";
7
7
  import { circuitOpenError, classifyProviderError } from "./provider-error.js";
8
8
  export { OpenAIResponsesAdapter } from "./openai-responses-adapter.js";
9
+ const OFFICIAL_OPENAI_RESPONSES_BASE_URL = "https://api.openai.com/v1";
10
+ /** Params the official input-token count endpoint accepts (SDK `InputTokenCountParams`). The
11
+ * create plan is projected onto this set — remaining keys (max_output_tokens, store, …) cannot
12
+ * change the input token count — rather than maintaining a second serialization. */
13
+ const INPUT_TOKEN_COUNT_PARAM_KEYS = [
14
+ "conversation", "input", "instructions", "model", "parallel_tool_calls",
15
+ "previous_response_id", "reasoning", "text", "tool_choice", "tools", "truncation",
16
+ ];
9
17
  export class OpenAIResponsesProvider {
10
18
  model;
11
19
  client;
@@ -14,6 +22,7 @@ export class OpenAIResponsesProvider {
14
22
  baseDelay;
15
23
  responses = new OpenAIResponsesAdapter();
16
24
  resolvedRuntimePolicy;
25
+ directNativeTokenCounting;
17
26
  resolvedRuntime;
18
27
  constructor(apiKey, model = "gpt-4.1", retry = { maxRetries: 3, baseDelay: 1000 }, baseURL = "https://api.openai.com/v1", runtimePolicy = {}, authMode = "api_key") {
19
28
  this.model = model;
@@ -26,6 +35,7 @@ export class OpenAIResponsesProvider {
26
35
  this.maxRetries = retry.maxRetries;
27
36
  this.baseDelay = retry.baseDelay;
28
37
  this.resolvedRuntimePolicy = runtimePolicy;
38
+ this.directNativeTokenCounting = baseURL.replace(/\/+$/, "") === OFFICIAL_OPENAI_RESPONSES_BASE_URL;
29
39
  }
30
40
  runtimePolicy() {
31
41
  return this.resolvedRuntimePolicy;
@@ -90,6 +100,29 @@ export class OpenAIResponsesProvider {
90
100
  }
91
101
  throw classifyProviderError("openai", lastError);
92
102
  }
103
+ /** spc_024-05: native preflight via the official Responses input-token count endpoint. Counts
104
+ * the exact create request plan (stateful `previous_response_id` continuation included) —
105
+ * native measurement belongs to the verified official endpoint, not the wire protocol. */
106
+ async countTokens(context, tools, extensions, state) {
107
+ const enabled = this.resolvedRuntime
108
+ ? this.resolvedRuntime.effectiveCapabilities.nativeTokenCounting.state === "supported"
109
+ : this.directNativeTokenCounting;
110
+ const inputTokens = this.client.responses.inputTokens;
111
+ if (!enabled || typeof inputTokens?.count !== "function") {
112
+ throw new Error("Native token counting is unavailable on this OpenAI-compatible endpoint");
113
+ }
114
+ const input = this.adapterInput(context, tools, extensions);
115
+ const plan = this.responses.buildRequest(input, this.asRunState(state));
116
+ const body = Object.fromEntries(INPUT_TOKEN_COUNT_PARAM_KEYS
117
+ .filter(key => key in plan.params)
118
+ .map(key => [key, plan.params[key]]));
119
+ const response = await inputTokens.count(body);
120
+ return {
121
+ inputTokens: response.input_tokens,
122
+ source: { kind: "native", provider: "openai" },
123
+ confidence: "exact",
124
+ };
125
+ }
93
126
  async *stream(context, tools, extensions, state, signal) {
94
127
  try {
95
128
  const runState = this.asRunState(state);
@@ -30,5 +30,10 @@ export interface ProtocolAdapter<TRequest, TCompleteResponse, TStreamChunk, TStr
30
30
  }
31
31
  export declare class ProtocolResponseError extends Error {
32
32
  readonly protocol: GenerationProtocol;
33
- constructor(protocol: GenerationProtocol, message: string);
33
+ readonly providerCode?: string;
34
+ readonly retryable?: boolean;
35
+ constructor(protocol: GenerationProtocol, message: string, options?: {
36
+ providerCode?: string;
37
+ retryable?: boolean;
38
+ });
34
39
  }
@@ -1,9 +1,13 @@
1
1
  export { GEMINI_PROTOCOL_CAPABILITIES, OLLAMA_PROTOCOL_CAPABILITIES, } from "./protocol-capabilities.js";
2
2
  export class ProtocolResponseError extends Error {
3
3
  protocol;
4
- constructor(protocol, message) {
5
- super(`${protocol} protocol response error: ${message}`);
4
+ providerCode;
5
+ retryable;
6
+ constructor(protocol, message, options = {}) {
7
+ super(options.providerCode ? message : `${protocol} protocol response error: ${message}`);
6
8
  this.name = "ProtocolResponseError";
7
9
  this.protocol = protocol;
10
+ this.providerCode = options.providerCode;
11
+ this.retryable = options.retryable;
8
12
  }
9
13
  }
@@ -113,10 +113,14 @@ export function classifyProviderError(provider, error) {
113
113
  const httpStatus = errorStatus(error);
114
114
  const providerCode = errorCode(error);
115
115
  const kind = classifyKind(error, httpStatus, providerCode);
116
+ const explicitProtocolRetryable = kind === "protocol"
117
+ && typeof object(error)?.retryable === "boolean"
118
+ ? object(error).retryable
119
+ : undefined;
116
120
  return new ProviderError({
117
121
  provider,
118
122
  kind,
119
- retryable: retryable(kind, httpStatus),
123
+ retryable: explicitProtocolRetryable ?? retryable(kind, httpStatus),
120
124
  message: errorMessage(error),
121
125
  ...(httpStatus !== undefined ? { httpStatus } : {}),
122
126
  ...(providerCode !== undefined ? { providerCode } : {}),
@@ -13,6 +13,7 @@ export interface ProviderRequestPlan {
13
13
  tools: ToolSchema[];
14
14
  options: Record<string, unknown>;
15
15
  fingerprint: string;
16
+ stablePrefixFingerprint: string;
16
17
  }
17
18
  export interface NormalizedProviderUsage extends ProviderUsage {
18
19
  /** Input not accounted as a cache read or write. `inputTokens` remains the full footprint. */
@@ -28,6 +29,8 @@ export interface RecordedPromptMeasurement {
28
29
  } | {
29
30
  kind: "local_exact";
30
31
  tokenizer: string;
32
+ } | {
33
+ kind: "postflight";
31
34
  } | {
32
35
  kind: "heuristic";
33
36
  };
@@ -56,7 +59,7 @@ export type CostObservation = {
56
59
  source: "unpriced";
57
60
  reason: "pricing_snapshot_not_effective" | "pricing_snapshot_expired" | "invalid_pricing_snapshot";
58
61
  };
59
- export declare function createProviderRequestPlan(input: Omit<ProviderRequestPlan, "fingerprint" | "options"> & {
62
+ export declare function createProviderRequestPlan(input: Omit<ProviderRequestPlan, "fingerprint" | "stablePrefixFingerprint" | "options"> & {
60
63
  options?: Record<string, unknown>;
61
64
  }): ProviderRequestPlan;
62
65
  /** Build the plan from a resolved provider when the runner only has the public provider object. */
@@ -13,7 +13,19 @@ export function createProviderRequestPlan(input) {
13
13
  tools: clone(input.tools),
14
14
  options,
15
15
  };
16
- return { ...plan, fingerprint: sha256(stableJson(plan)) };
16
+ const stablePrefix = {
17
+ providerId: plan.providerId,
18
+ modelId: plan.modelId,
19
+ endpoint: plan.endpoint,
20
+ context: stablePrefixContext(plan.context),
21
+ tools: plan.tools,
22
+ options: plan.options,
23
+ };
24
+ return {
25
+ ...plan,
26
+ fingerprint: sha256(stableJson(plan)),
27
+ stablePrefixFingerprint: sha256(stableJson(stablePrefix)),
28
+ };
17
29
  }
18
30
  /** Build the plan from a resolved provider when the runner only has the public provider object. */
19
31
  export function createProviderRequestPlanForProvider(provider, context, tools, options) {
@@ -59,6 +71,8 @@ export function measurementForPlan(plan, recorded) {
59
71
  return clone(recorded);
60
72
  if (source.kind === "local_exact" && typeof source.tokenizer === "string" && source.tokenizer.length > 0)
61
73
  return clone(recorded);
74
+ if (source.kind === "postflight")
75
+ return clone(recorded);
62
76
  if (source.kind === "heuristic")
63
77
  return clone(recorded);
64
78
  return undefined;
@@ -155,6 +169,16 @@ function stableJson(value) {
155
169
  const object = value;
156
170
  return `{${Object.keys(object).sort().map(key => `${JSON.stringify(key)}:${stableJson(object[key])}`).join(",")}}`;
157
171
  }
172
+ function stablePrefixContext(context) {
173
+ const frozenPrefixLen = context.frozenPrefixLen ?? 0;
174
+ return {
175
+ systemText: context.systemText,
176
+ ...(context.systemStable !== undefined ? { systemStable: context.systemStable } : {}),
177
+ ...(context.systemKnowledge !== undefined ? { systemKnowledge: context.systemKnowledge } : {}),
178
+ frozenPrefixLen,
179
+ turns: clone(context.turns.slice(0, frozenPrefixLen)),
180
+ };
181
+ }
158
182
  function sha256(value) {
159
183
  return `sha256:${createHash("sha256").update(value).digest("hex")}`;
160
184
  }
@@ -1,7 +1,27 @@
1
1
  import { openAICachedPromptTokens } from "./base.js";
2
- function readNumber(obj, key) {
2
+ import { ProtocolResponseError } from "./protocol-adapter.js";
3
+ function readNumber(obj, key, protocol) {
3
4
  const value = obj?.[key];
4
- return typeof value === "number" ? value : undefined;
5
+ if (value === undefined || value === null)
6
+ return undefined;
7
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
8
+ throw new ProtocolResponseError(protocol, `usage.${key} must be a non-negative safe integer`);
9
+ }
10
+ return value;
11
+ }
12
+ function hasOwn(obj, key) {
13
+ return obj !== undefined && Object.prototype.hasOwnProperty.call(obj, key);
14
+ }
15
+ function openAICacheTelemetry(usage) {
16
+ const promptDetails = usage?.prompt_tokens_details;
17
+ const inputDetails = usage?.input_tokens_details;
18
+ if (hasOwn(usage, "prompt_cache_hit_tokens") || hasOwn(usage, "prompt_cache_miss_tokens")) {
19
+ return { cacheTelemetryStatus: "measured", cacheTelemetrySource: "deepseek_prompt_cache" };
20
+ }
21
+ if (hasOwn(promptDetails, "cached_tokens") || hasOwn(inputDetails, "cached_tokens")) {
22
+ return { cacheTelemetryStatus: "measured", cacheTelemetrySource: "openai_prompt_details" };
23
+ }
24
+ return { cacheTelemetryStatus: "unavailable" };
5
25
  }
6
26
  /**
7
27
  * Covers both OpenAI wire shapes actually in use: Chat Completions (`prompt_tokens`/
@@ -12,20 +32,34 @@ function readNumber(obj, key) {
12
32
  */
13
33
  export function normalizeOpenAIUsage(usage) {
14
34
  const u = usage && typeof usage === "object" ? usage : undefined;
15
- const rawInput = readNumber(u, "prompt_tokens") ?? readNumber(u, "input_tokens");
16
- const rawOutput = readNumber(u, "completion_tokens") ?? readNumber(u, "output_tokens");
35
+ const protocol = hasOwn(u, "input_tokens") ? "openai-responses" : "openai-chat";
36
+ const rawInput = readNumber(u, "prompt_tokens", protocol) ?? readNumber(u, "input_tokens", protocol);
37
+ const rawOutput = readNumber(u, "completion_tokens", protocol) ?? readNumber(u, "output_tokens", protocol);
17
38
  if (rawInput === undefined && rawOutput === undefined)
18
39
  return undefined;
19
40
  const inputTokens = rawInput ?? 0;
20
41
  const outputTokens = rawOutput ?? 0;
42
+ const promptDetails = u?.prompt_tokens_details;
43
+ const inputDetails = u?.input_tokens_details;
44
+ readNumber(promptDetails, "cached_tokens", protocol);
45
+ readNumber(inputDetails, "cached_tokens", protocol);
46
+ const cacheHit = readNumber(u, "prompt_cache_hit_tokens", protocol);
47
+ const cacheMiss = readNumber(u, "prompt_cache_miss_tokens", protocol);
21
48
  const cacheReadInputTokens = openAICachedPromptTokens(usage);
22
49
  const details = (u?.completion_tokens_details ?? u?.output_tokens_details);
23
- const reasoningTokens = readNumber(details, "reasoning_tokens");
50
+ const reasoningTokens = readNumber(details, "reasoning_tokens", protocol);
51
+ if (cacheReadInputTokens > inputTokens) {
52
+ throw new ProtocolResponseError(protocol, "cache token subsets cannot exceed input tokens");
53
+ }
54
+ if (cacheHit !== undefined && cacheMiss !== undefined && cacheHit + cacheMiss !== inputTokens) {
55
+ throw new ProtocolResponseError(protocol, "DeepSeek cache hit and miss tokens must sum to prompt tokens");
56
+ }
24
57
  const providerUsage = {
25
58
  inputTokens,
26
59
  outputTokens,
27
60
  ...(cacheReadInputTokens > 0 ? { cacheReadInputTokens } : {}),
28
61
  ...(reasoningTokens !== undefined ? { reasoningTokens } : {}),
62
+ ...openAICacheTelemetry(u),
29
63
  };
30
64
  return providerUsage;
31
65
  }
@@ -42,10 +76,10 @@ export function normalizeOpenAIUsage(usage) {
42
76
  */
43
77
  export function normalizeAnthropicUsage(usage) {
44
78
  const u = usage && typeof usage === "object" ? usage : undefined;
45
- const rawUncachedInput = readNumber(u, "input_tokens");
46
- const rawCacheRead = readNumber(u, "cache_read_input_tokens");
47
- const rawCacheCreation = readNumber(u, "cache_creation_input_tokens");
48
- const rawOutput = readNumber(u, "output_tokens");
79
+ const rawUncachedInput = readNumber(u, "input_tokens", "anthropic-messages");
80
+ const rawCacheRead = readNumber(u, "cache_read_input_tokens", "anthropic-messages");
81
+ const rawCacheCreation = readNumber(u, "cache_creation_input_tokens", "anthropic-messages");
82
+ const rawOutput = readNumber(u, "output_tokens", "anthropic-messages");
49
83
  if (rawUncachedInput === undefined && rawCacheRead === undefined && rawCacheCreation === undefined && rawOutput === undefined)
50
84
  return undefined;
51
85
  const uncachedInput = rawUncachedInput ?? 0;
@@ -57,6 +91,9 @@ export function normalizeAnthropicUsage(usage) {
57
91
  outputTokens,
58
92
  ...(cacheReadInputTokens > 0 ? { cacheReadInputTokens } : {}),
59
93
  ...(cacheCreationInputTokens > 0 ? { cacheCreationInputTokens } : {}),
94
+ ...(hasOwn(u, "cache_read_input_tokens") || hasOwn(u, "cache_creation_input_tokens")
95
+ ? { cacheTelemetryStatus: "measured", cacheTelemetrySource: "anthropic_usage" }
96
+ : { cacheTelemetryStatus: "unavailable" }),
60
97
  };
61
98
  return providerUsage;
62
99
  }
@@ -67,17 +104,23 @@ export function normalizeAnthropicUsage(usage) {
67
104
  */
68
105
  export function normalizeGeminiUsage(usage) {
69
106
  const u = usage && typeof usage === "object" ? usage : undefined;
70
- const rawInput = readNumber(u, "promptTokenCount");
71
- const rawOutput = readNumber(u, "candidatesTokenCount");
107
+ const rawInput = readNumber(u, "promptTokenCount", "gemini");
108
+ const rawOutput = readNumber(u, "candidatesTokenCount", "gemini");
72
109
  if (rawInput === undefined && rawOutput === undefined)
73
110
  return undefined;
74
111
  const inputTokens = rawInput ?? 0;
75
112
  const outputTokens = rawOutput ?? 0;
76
- const cacheReadInputTokens = readNumber(u, "cachedContentTokenCount");
113
+ const cacheReadInputTokens = readNumber(u, "cachedContentTokenCount", "gemini");
114
+ if ((cacheReadInputTokens ?? 0) > inputTokens) {
115
+ throw new ProtocolResponseError("gemini", "cache token subsets cannot exceed input tokens");
116
+ }
77
117
  const providerUsage = {
78
118
  inputTokens,
79
119
  outputTokens,
80
120
  ...(cacheReadInputTokens ? { cacheReadInputTokens } : {}),
121
+ ...(hasOwn(u, "cachedContentTokenCount")
122
+ ? { cacheTelemetryStatus: "measured", cacheTelemetrySource: "gemini_usage" }
123
+ : { cacheTelemetryStatus: "unavailable" }),
81
124
  };
82
125
  return providerUsage;
83
126
  }
@@ -88,9 +131,9 @@ export function normalizeGeminiUsage(usage) {
88
131
  */
89
132
  export function normalizeOllamaUsage(chunk) {
90
133
  const u = chunk && typeof chunk === "object" ? chunk : undefined;
91
- const rawInput = readNumber(u, "prompt_eval_count");
92
- const rawOutput = readNumber(u, "eval_count");
134
+ const rawInput = readNumber(u, "prompt_eval_count", "ollama-chat");
135
+ const rawOutput = readNumber(u, "eval_count", "ollama-chat");
93
136
  if (rawInput === undefined && rawOutput === undefined)
94
137
  return undefined;
95
- return { inputTokens: rawInput ?? 0, outputTokens: rawOutput ?? 0 };
138
+ return { inputTokens: rawInput ?? 0, outputTokens: rawOutput ?? 0, cacheTelemetryStatus: "unavailable" };
96
139
  }
@@ -1,5 +1,9 @@
1
1
  import type { LLMProvider, Message, ProviderDescriptor, ProviderReplay, RenderedContext, ReplayabilityAssessment, ToolCall } from "../types.js";
2
2
  import type { SessionEvent } from "./session-log.js";
3
+ export declare class ProviderReplayProtocolMismatchError extends Error {
4
+ readonly code: "provider_replay_protocol_mismatch";
5
+ constructor(provider: string, storedProtocol: string, resolvedProtocol: string);
6
+ }
3
7
  export declare function assistantReplayKey(message: Pick<Message, "content" | "toolCalls">): string;
4
8
  /**
5
9
  * A stored replay may only be seeded into a provider speaking the same wire
@@ -1,3 +1,11 @@
1
+ export class ProviderReplayProtocolMismatchError extends Error {
2
+ code = "provider_replay_protocol_mismatch";
3
+ constructor(provider, storedProtocol, resolvedProtocol) {
4
+ super(`Stored ${storedProtocol} tool replay is incompatible with resolved ${provider}/${resolvedProtocol}; `
5
+ + `pin the previous ${storedProtocol} endpoint explicitly to resume this session`);
6
+ this.name = "ProviderReplayProtocolMismatchError";
7
+ }
8
+ }
1
9
  function sortObjectKeys(val) {
2
10
  if (val === null || typeof val !== "object") {
3
11
  return val;
@@ -60,8 +68,14 @@ export function seedProviderReplayFromEvents(provider, events) {
60
68
  continue;
61
69
  const toolCalls = event.tool_calls ?? [];
62
70
  const stored = event.provider_replay;
63
- if (!stored || !isReplayCompatibleWithProvider(stored, descriptor))
71
+ if (!stored)
64
72
  continue;
73
+ if (!isReplayCompatibleWithProvider(stored, descriptor)) {
74
+ if (toolCalls.length > 0 && descriptor) {
75
+ throw new ProviderReplayProtocolMismatchError(descriptor.provider, stored.protocol, descriptor.protocol);
76
+ }
77
+ continue;
78
+ }
65
79
  provider.seedProviderReplay({ content: event.content, toolCalls }, stored);
66
80
  }
67
81
  }
@@ -56,11 +56,7 @@ export interface TurnMetrics {
56
56
  inputTokens: number;
57
57
  /** Tokens served from the prompt cache this turn (Anthropic `cache_read_input_tokens`). */
58
58
  cacheReadTokens: number;
59
- /** I1: per-slot attribution of `cacheReadTokens`. Anthropic reports a single cache-read total,
60
- * not a per-block breakdown — this field is a pro-rata estimate over the slots that actually
61
- * carried a `cache_control` breakpoint on the request. Missing / empty when the provider does
62
- * not honor `cache_control` (OpenAI-family auto-cache) or when no breakpoints were placed.
63
- * Useful for diagnosing which slot is buying the cache hit when comparing strategies. */
59
+ /** Provider-authoritative per-slot attribution, when the endpoint reports one. */
64
60
  cacheReadTokensBySlot?: {
65
61
  system?: number;
66
62
  tools?: number;
@@ -68,6 +64,10 @@ export interface TurnMetrics {
68
64
  };
69
65
  /** Tokens written to the prompt cache this turn (Anthropic `cache_creation_input_tokens`). */
70
66
  cacheCreationTokens: number;
67
+ cacheTelemetryStatus?: "measured" | "unavailable";
68
+ cacheTelemetrySource?: "anthropic_usage" | "openai_prompt_details" | "deepseek_prompt_cache" | "gemini_usage";
69
+ requestFingerprint?: string;
70
+ stablePrefixFingerprint?: string;
71
71
  }
72
72
  /** O5: decision returned by `onToolCall` — `block: true` denies this call before it executes; the
73
73
  * `reason` is fed back to the model as a governance-denied tool result (so it can redirect). */