@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.
@@ -15,5 +15,5 @@ export { ReplayProvider } from "../runtime/replay-provider.js";
15
15
  export type { ReplayProviderOpts } from "../runtime/replay-provider.js";
16
16
  export { extractRecordedMessages } from "../runtime/replay-fixture.js";
17
17
  export { ProviderReplayValidationError, DEGRADED_REASONING_PLACEHOLDER } from "../providers/replay-validator.js";
18
- export { assessProviderReplayability, peekProviderReplay, seedProviderReplayFromEvents, isReplayCompatibleWithProvider, } from "../runtime/provider-replay.js";
18
+ export { ProviderReplayProtocolMismatchError, assessProviderReplayability, peekProviderReplay, seedProviderReplayFromEvents, isReplayCompatibleWithProvider, } from "../runtime/provider-replay.js";
19
19
  export type { ReplayabilityAssessment } from "../types.js";
package/dist/os/public.js CHANGED
@@ -12,4 +12,4 @@ export { PermissionManager, PermissionMode } from "../safety/permissions.js";
12
12
  export { ReplayProvider } from "../runtime/replay-provider.js";
13
13
  export { extractRecordedMessages } from "../runtime/replay-fixture.js";
14
14
  export { ProviderReplayValidationError, DEGRADED_REASONING_PLACEHOLDER } from "../providers/replay-validator.js";
15
- export { assessProviderReplayability, peekProviderReplay, seedProviderReplayFromEvents, isReplayCompatibleWithProvider, } from "../runtime/provider-replay.js";
15
+ export { ProviderReplayProtocolMismatchError, assessProviderReplayability, peekProviderReplay, seedProviderReplayFromEvents, isReplayCompatibleWithProvider, } from "../runtime/provider-replay.js";
@@ -1,6 +1,15 @@
1
1
  import type { Message, ProviderReplay, ProviderUsage, ToolCall } from "../types.js";
2
2
  import type { CanonicalAdapterInput } from "./content-normalization.js";
3
3
  import { type AdapterDecodeInput, type AdapterOutput, type AdapterStreamInput, type CanonicalStopReason, type ProtocolAdapter } from "./protocol-adapter.js";
4
+ export declare const ANTHROPIC_TEXTUAL_TOOL_CALL_START_MARKER = "<\uFF5C\uFF5CDSML\uFF5C\uFF5Ctool_calls>";
5
+ type TextualToolCallPolicy = "off" | "reject";
6
+ interface TextualToolCallGuardState {
7
+ policy: TextualToolCallPolicy;
8
+ mode: "passthrough" | "candidate";
9
+ tail: string;
10
+ capturedLength: number;
11
+ sawNativeToolCall: boolean;
12
+ }
4
13
  export interface AnthropicRequestPlan {
5
14
  transport: "stable" | "beta";
6
15
  params: Record<string, unknown>;
@@ -25,17 +34,19 @@ export interface AnthropicStreamState {
25
34
  }>;
26
35
  readonly nativeBlocks: Record<number, Record<string, unknown>>;
27
36
  readonly finalToolCalls: ToolCall[];
37
+ readonly textualToolCallGuard: TextualToolCallGuardState;
28
38
  finalText: string;
29
39
  uncachedInput: number;
30
40
  cacheReadTokens: number;
31
41
  cacheCreationTokens: number;
42
+ cacheTelemetryMeasured: boolean;
32
43
  outputTokens: number;
33
44
  }
34
45
  export declare class AnthropicMessagesAdapter implements ProtocolAdapter<AnthropicRequestPlan, Record<string, any>, AnthropicStreamChunk, AnthropicStreamState, undefined> {
35
46
  readonly protocol: "anthropic-messages";
36
47
  readonly protocolCapabilities: import("./protocol-capabilities.js").ProtocolRuntimeCapabilities;
37
48
  buildRequest(input: CanonicalAdapterInput): AnthropicRequestPlan;
38
- decodeComplete(raw: Record<string, any>, _input: AdapterDecodeInput): {
49
+ decodeComplete(raw: Record<string, any>, decodeInput: AdapterDecodeInput): {
39
50
  message: Message;
40
51
  replay?: ProviderReplay;
41
52
  };
@@ -45,3 +56,4 @@ export declare class AnthropicMessagesAdapter implements ProtocolAdapter<Anthrop
45
56
  normalizeUsage(raw: unknown): ProviderUsage | undefined;
46
57
  normalizeStopReason(raw: string | undefined): CanonicalStopReason | undefined;
47
58
  }
59
+ export {};
@@ -1,6 +1,9 @@
1
1
  import { normalizeToolCall } from "./base.js";
2
2
  import { ProtocolResponseError, } from "./protocol-adapter.js";
3
3
  import { ANTHROPIC_PROTOCOL_CAPABILITIES } from "./protocol-capabilities.js";
4
+ import { endpointProfiles } from "./endpoints.js";
5
+ export const ANTHROPIC_TEXTUAL_TOOL_CALL_START_MARKER = "<||DSML||tool_calls>";
6
+ const TEXTUAL_TOOL_CALL_CAPTURE_LIMIT = 64 * 1024;
4
7
  const CACHE_BREAKPOINT_STRATEGIES = new Set([
5
8
  "default", "tools-only", "system-only", "frozen-prefix", "none",
6
9
  ]);
@@ -13,10 +16,63 @@ function cacheStrategy(extensions) {
13
16
  function extensionsForWire(extensions) {
14
17
  const blocked = new Set([
15
18
  "model", "messages", "system", "tools", "max_tokens", "stream",
16
- "__deepstrikeThinkingEnabled", "degradeMissingReasoningReplay",
19
+ "__deepstrikeThinkingEnabled", "degradeMissingReasoningReplay", "textualToolCallPolicy",
17
20
  ]);
18
21
  return Object.fromEntries(Object.entries(extensions).filter(([key]) => !blocked.has(key)));
19
22
  }
23
+ function textualToolCallPolicy(input) {
24
+ if (input.tools.length === 0)
25
+ return "off";
26
+ const explicit = input.extensions.textualToolCallPolicy;
27
+ if (explicit === "off" || explicit === "reject")
28
+ return explicit;
29
+ const official = endpointProfiles["anthropic.messages"];
30
+ const endpoint = input.resolved.endpoint;
31
+ const isOfficial = input.resolved.identity.endpointId === official.id
32
+ && (!endpoint || endpoint.baseURL === official.baseURL);
33
+ return isOfficial ? "off" : "reject";
34
+ }
35
+ function textualToolCallError() {
36
+ return new ProtocolResponseError("anthropic-messages", "Provider emitted a tool call as text instead of a native tool block", { providerCode: "textual_tool_call", retryable: true });
37
+ }
38
+ function hasTextualToolCall(text, input) {
39
+ return textualToolCallPolicy(input) === "reject"
40
+ && text.includes(ANTHROPIC_TEXTUAL_TOOL_CALL_START_MARKER);
41
+ }
42
+ function markerTailLength(text) {
43
+ const max = Math.min(text.length, ANTHROPIC_TEXTUAL_TOOL_CALL_START_MARKER.length - 1);
44
+ for (let length = max; length > 0; length -= 1) {
45
+ if (text.endsWith(ANTHROPIC_TEXTUAL_TOOL_CALL_START_MARKER.slice(0, length)))
46
+ return length;
47
+ }
48
+ return 0;
49
+ }
50
+ function utf8Length(text) {
51
+ return Buffer.byteLength(text, "utf8");
52
+ }
53
+ function guardTextDelta(text, guard) {
54
+ if (guard.policy === "off")
55
+ return text;
56
+ if (guard.mode === "candidate") {
57
+ guard.capturedLength += utf8Length(text);
58
+ if (guard.capturedLength > TEXTUAL_TOOL_CALL_CAPTURE_LIMIT)
59
+ throw textualToolCallError();
60
+ return "";
61
+ }
62
+ const combined = guard.tail + text;
63
+ const markerIndex = combined.indexOf(ANTHROPIC_TEXTUAL_TOOL_CALL_START_MARKER);
64
+ if (markerIndex >= 0) {
65
+ guard.mode = "candidate";
66
+ guard.tail = "";
67
+ guard.capturedLength = utf8Length(combined.slice(markerIndex));
68
+ if (guard.capturedLength > TEXTUAL_TOOL_CALL_CAPTURE_LIMIT)
69
+ throw textualToolCallError();
70
+ return combined.slice(0, markerIndex);
71
+ }
72
+ const tailLength = markerTailLength(combined);
73
+ guard.tail = tailLength > 0 ? combined.slice(-tailLength) : "";
74
+ return tailLength > 0 ? combined.slice(0, -tailLength) : combined;
75
+ }
20
76
  function systemBlocks(context, strategy) {
21
77
  if (!context.systemStable && !context.systemKnowledge) {
22
78
  return context.systemText || undefined;
@@ -206,22 +262,12 @@ function assertCacheBudget(params) {
206
262
  throw new Error(`Anthropic cache_control budget exceeded: ${count} > 4`);
207
263
  }
208
264
  }
209
- function estimateCacheRead(cacheRead, slots) {
210
- if (cacheRead <= 0)
211
- return undefined;
212
- const keys = ["system", "tools", "messages"].filter(key => slots[key]);
213
- if (!keys.length)
214
- return undefined;
215
- const share = Math.floor(cacheRead / keys.length);
216
- const remainder = cacheRead - share * keys.length;
217
- return Object.fromEntries(keys.map((key, index) => [key, share + (index === 0 ? remainder : 0)]));
218
- }
219
265
  function numeric(raw, field) {
220
266
  const value = raw[field];
221
267
  if (value === undefined || value === null)
222
268
  return undefined;
223
- if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
224
- throw new ProtocolResponseError("anthropic-messages", `usage.${field} is invalid`);
269
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
270
+ throw new ProtocolResponseError("anthropic-messages", `usage.${field} must be a non-negative safe integer`);
225
271
  }
226
272
  return value;
227
273
  }
@@ -265,7 +311,7 @@ export class AnthropicMessagesAdapter {
265
311
  cacheSlots: countSlots(system, tools, messages),
266
312
  };
267
313
  }
268
- decodeComplete(raw, _input) {
314
+ decodeComplete(raw, decodeInput) {
269
315
  let content = "";
270
316
  const toolCalls = [];
271
317
  for (const block of raw.content ?? []) {
@@ -277,6 +323,8 @@ export class AnthropicMessagesAdapter {
277
323
  toolCalls.push(call);
278
324
  }
279
325
  }
326
+ if (hasTextualToolCall(content, decodeInput.input))
327
+ throw textualToolCallError();
280
328
  const usage = this.normalizeUsage(raw.usage);
281
329
  const blocks = raw.content;
282
330
  return {
@@ -297,10 +345,18 @@ export class AnthropicMessagesAdapter {
297
345
  toolBlocks: {},
298
346
  nativeBlocks: {},
299
347
  finalToolCalls: [],
348
+ textualToolCallGuard: {
349
+ policy: textualToolCallPolicy(input.input),
350
+ mode: "passthrough",
351
+ tail: "",
352
+ capturedLength: 0,
353
+ sawNativeToolCall: false,
354
+ },
300
355
  finalText: "",
301
356
  uncachedInput: 0,
302
357
  cacheReadTokens: 0,
303
358
  cacheCreationTokens: 0,
359
+ cacheTelemetryMeasured: false,
304
360
  outputTokens: 0,
305
361
  };
306
362
  }
@@ -309,20 +365,26 @@ export class AnthropicMessagesAdapter {
309
365
  if (chunk.type === "message_start" || chunk.type === "message_delta") {
310
366
  const raw = chunk.usage ?? chunk.message?.usage;
311
367
  if (raw) {
368
+ if (Object.prototype.hasOwnProperty.call(raw, "cache_read_input_tokens")
369
+ || Object.prototype.hasOwnProperty.call(raw, "cache_creation_input_tokens")) {
370
+ state.cacheTelemetryMeasured = true;
371
+ }
312
372
  state.uncachedInput = Math.max(state.uncachedInput, numeric(raw, "input_tokens") ?? 0);
313
373
  state.cacheReadTokens = Math.max(state.cacheReadTokens, numeric(raw, "cache_read_input_tokens") ?? 0);
314
374
  state.cacheCreationTokens = Math.max(state.cacheCreationTokens, numeric(raw, "cache_creation_input_tokens") ?? 0);
315
375
  state.outputTokens = Math.max(state.outputTokens, numeric(raw, "output_tokens") ?? 0);
316
376
  const inputTokens = state.uncachedInput + state.cacheReadTokens + state.cacheCreationTokens;
377
+ const cacheTelemetryStatus = state.cacheTelemetryMeasured ? "measured" : "unavailable";
317
378
  const providerUsage = {
318
379
  inputTokens,
319
380
  outputTokens: state.outputTokens,
320
381
  ...(state.cacheReadTokens ? { cacheReadInputTokens: state.cacheReadTokens } : {}),
321
382
  ...(state.cacheCreationTokens ? { cacheCreationInputTokens: state.cacheCreationTokens } : {}),
383
+ cacheTelemetryStatus,
384
+ ...(state.cacheTelemetryMeasured ? { cacheTelemetrySource: "anthropic_usage" } : {}),
322
385
  };
323
386
  const rawStopReason = chunk.delta?.stop_reason;
324
387
  const stopReason = this.normalizeStopReason(rawStopReason);
325
- const bySlot = estimateCacheRead(state.cacheReadTokens, state.cacheSlots);
326
388
  events.push({
327
389
  type: "usage",
328
390
  totalTokens: inputTokens + state.outputTokens,
@@ -330,7 +392,8 @@ export class AnthropicMessagesAdapter {
330
392
  outputTokens: state.outputTokens,
331
393
  cacheReadInputTokens: state.cacheReadTokens,
332
394
  cacheCreationInputTokens: state.cacheCreationTokens,
333
- ...(bySlot ? { cacheReadInputTokensBySlot: bySlot } : {}),
395
+ cacheTelemetryStatus,
396
+ ...(state.cacheTelemetryMeasured ? { cacheTelemetrySource: "anthropic_usage" } : {}),
334
397
  ...(stopReason ? { stopReason } : {}),
335
398
  ...(rawStopReason ? { rawStopReason } : {}),
336
399
  providerUsage,
@@ -339,7 +402,15 @@ export class AnthropicMessagesAdapter {
339
402
  }
340
403
  else if (chunk.type === "content_block_start") {
341
404
  state.nativeBlocks[chunk.index] = { ...chunk.content_block };
342
- if (chunk.content_block.type === "tool_use") {
405
+ if (chunk.content_block.type === "text") {
406
+ const initialText = String(chunk.content_block.text ?? "");
407
+ const visibleText = guardTextDelta(initialText, state.textualToolCallGuard);
408
+ state.finalText += visibleText;
409
+ if (visibleText)
410
+ events.push({ type: "text_delta", delta: visibleText });
411
+ }
412
+ else if (chunk.content_block.type === "tool_use") {
413
+ state.textualToolCallGuard.sawNativeToolCall = true;
343
414
  state.toolBlocks[chunk.index] = {
344
415
  id: chunk.content_block.id,
345
416
  name: chunk.content_block.name,
@@ -350,12 +421,14 @@ export class AnthropicMessagesAdapter {
350
421
  else if (chunk.type === "content_block_delta") {
351
422
  const delta = chunk.delta;
352
423
  if (delta.type === "text_delta") {
353
- state.finalText += delta.text;
424
+ const visibleText = guardTextDelta(delta.text, state.textualToolCallGuard);
425
+ state.finalText += visibleText;
354
426
  state.nativeBlocks[chunk.index] = {
355
427
  ...state.nativeBlocks[chunk.index],
356
428
  text: String(state.nativeBlocks[chunk.index]?.text ?? "") + delta.text,
357
429
  };
358
- events.push({ type: "text_delta", delta: delta.text });
430
+ if (visibleText)
431
+ events.push({ type: "text_delta", delta: visibleText });
359
432
  }
360
433
  else if (delta.type === "thinking_delta") {
361
434
  state.nativeBlocks[chunk.index] = {
@@ -400,12 +473,21 @@ export class AnthropicMessagesAdapter {
400
473
  return { events };
401
474
  }
402
475
  finishStream(state) {
476
+ if (state.textualToolCallGuard.mode === "candidate")
477
+ throw textualToolCallError();
478
+ const events = [];
479
+ if (state.textualToolCallGuard.tail) {
480
+ const tail = state.textualToolCallGuard.tail;
481
+ state.textualToolCallGuard.tail = "";
482
+ state.finalText += tail;
483
+ events.push({ type: "text_delta", delta: tail });
484
+ }
403
485
  const blocks = Object.keys(state.nativeBlocks)
404
486
  .map(Number)
405
487
  .sort((left, right) => left - right)
406
488
  .map(index => state.nativeBlocks[index]);
407
489
  return {
408
- events: [],
490
+ events,
409
491
  ...(blocks.length ? { replay: { protocol: "anthropic-messages", native_blocks: blocks } } : {}),
410
492
  };
411
493
  }
@@ -428,6 +510,10 @@ export class AnthropicMessagesAdapter {
428
510
  outputTokens: output ?? 0,
429
511
  ...(cacheRead ? { cacheReadInputTokens: cacheRead } : {}),
430
512
  ...(cacheCreation ? { cacheCreationInputTokens: cacheCreation } : {}),
513
+ ...(Object.prototype.hasOwnProperty.call(record, "cache_read_input_tokens")
514
+ || Object.prototype.hasOwnProperty.call(record, "cache_creation_input_tokens")
515
+ ? { cacheTelemetryStatus: "measured", cacheTelemetrySource: "anthropic_usage" }
516
+ : { cacheTelemetryStatus: "unavailable" }),
431
517
  };
432
518
  }
433
519
  normalizeStopReason(raw) {
@@ -22,6 +22,7 @@ export declare class AnthropicProvider implements LLMProvider {
22
22
  private readonly nativeAssistantBlocks;
23
23
  private readonly resolvedRuntimePolicy;
24
24
  private readonly directNativeTokenCounting;
25
+ private readonly defaultTextualToolCallPolicy;
25
26
  private resolvedRuntime?;
26
27
  constructor(config: AnthropicProviderConfig);
27
28
  runtimePolicy(): RuntimePolicy;
@@ -16,6 +16,7 @@ export class AnthropicProvider {
16
16
  nativeAssistantBlocks = new Map();
17
17
  resolvedRuntimePolicy;
18
18
  directNativeTokenCounting;
19
+ defaultTextualToolCallPolicy;
19
20
  resolvedRuntime;
20
21
  constructor(config) {
21
22
  if (!config || typeof config !== "object" || Array.isArray(config)) {
@@ -40,8 +41,11 @@ export class AnthropicProvider {
40
41
  this.maxRetries = c.retry?.maxRetries ?? 3;
41
42
  this.baseDelay = c.retry?.baseDelay ?? 1000;
42
43
  this.resolvedRuntimePolicy = c.runtimePolicy ?? {};
43
- this.directNativeTokenCounting = c.baseURL === undefined
44
- || c.baseURL === endpointProfiles["anthropic.messages"].baseURL;
44
+ const configuredBaseURL = c.baseURL?.replace(/\/+$/, "");
45
+ const officialBaseURL = endpointProfiles["anthropic.messages"].baseURL.replace(/\/+$/, "");
46
+ this.directNativeTokenCounting = configuredBaseURL === undefined
47
+ || configuredBaseURL === officialBaseURL;
48
+ this.defaultTextualToolCallPolicy = this.directNativeTokenCounting ? "off" : "reject";
45
49
  }
46
50
  runtimePolicy() {
47
51
  return this.resolvedRuntimePolicy;
@@ -107,7 +111,10 @@ export class AnthropicProvider {
107
111
  context,
108
112
  tools,
109
113
  resolved,
110
- extensions,
114
+ extensions: {
115
+ textualToolCallPolicy: this.defaultTextualToolCallPolicy,
116
+ ...extensions,
117
+ },
111
118
  replayForMessage: message => this.peekProviderReplay(message),
112
119
  });
113
120
  }
@@ -18,8 +18,8 @@ export declare const INTERNAL_EXTENSION_KEYS: readonly string[];
18
18
  export declare function omitExtensionKeys(extensions: Record<string, unknown> | undefined, keys: readonly string[]): Record<string, unknown>;
19
19
  /**
20
20
  * Cached-prompt-token count from an OpenAI-compatible usage object. Covers the
21
- * standard `prompt_tokens_details.cached_tokens` (OpenAI, Qwen, MiniMax, GLM,
22
- * Kimi) and DeepSeek's `prompt_cache_hit_tokens`. These caches bill reads only,
21
+ * Chat `prompt_tokens_details.cached_tokens`, Responses
22
+ * `input_tokens_details.cached_tokens`, and DeepSeek's `prompt_cache_hit_tokens`. These caches bill reads only,
23
23
  * so there is no separate cache-creation count. The figure is a subset of
24
24
  * `prompt_tokens` (the full prompt), surfaced for cost visibility — it must not
25
25
  * be subtracted from the input count the kernel uses for context accounting.
@@ -44,8 +44,8 @@ export function omitExtensionKeys(extensions, keys) {
44
44
  }
45
45
  /**
46
46
  * Cached-prompt-token count from an OpenAI-compatible usage object. Covers the
47
- * standard `prompt_tokens_details.cached_tokens` (OpenAI, Qwen, MiniMax, GLM,
48
- * Kimi) and DeepSeek's `prompt_cache_hit_tokens`. These caches bill reads only,
47
+ * Chat `prompt_tokens_details.cached_tokens`, Responses
48
+ * `input_tokens_details.cached_tokens`, and DeepSeek's `prompt_cache_hit_tokens`. These caches bill reads only,
49
49
  * so there is no separate cache-creation count. The figure is a subset of
50
50
  * `prompt_tokens` (the full prompt), surfaced for cost visibility — it must not
51
51
  * be subtracted from the input count the kernel uses for context accounting.
@@ -54,10 +54,12 @@ export function openAICachedPromptTokens(usage) {
54
54
  if (!usage || typeof usage !== "object")
55
55
  return 0;
56
56
  const u = usage;
57
- const details = u.prompt_tokens_details;
58
- const standard = typeof details?.cached_tokens === "number" ? details.cached_tokens : 0;
57
+ const promptDetails = u.prompt_tokens_details;
58
+ const inputDetails = u.input_tokens_details;
59
+ const standard = typeof promptDetails?.cached_tokens === "number" ? promptDetails.cached_tokens : 0;
60
+ const responses = typeof inputDetails?.cached_tokens === "number" ? inputDetails.cached_tokens : 0;
59
61
  const deepseek = typeof u.prompt_cache_hit_tokens === "number" ? u.prompt_cache_hit_tokens : 0;
60
- return Math.max(standard, deepseek);
62
+ return Math.max(standard, responses, deepseek);
61
63
  }
62
64
  /**
63
65
  * Prompt-cache hit rate for one usage record: the fraction of the full prompt
@@ -1,5 +1,5 @@
1
1
  import { PROVIDER_REGISTRY, providerRegistryKey, supportsBearerCredential } from "./registry.js";
2
- import { endpointCapabilitiesFor, generationProtocol, modelRegistry, normalizeModelId, resolveEffectiveModelCapabilities, } from "./model-registry.js";
2
+ import { defaultEndpointForProvider, endpointCapabilitiesFor, generationProtocol, modelRegistry, normalizeModelId, resolveEffectiveModelCapabilities, } from "./model-registry.js";
3
3
  import { endpointProfiles } from "./endpoints.js";
4
4
  import { resolveCredential, resolveCredentialSync, CredentialResolutionError, } from "./credentials.js";
5
5
  export function createProvider(options) {
@@ -38,7 +38,9 @@ async function resolveRuntimeDraftAsync(options) {
38
38
  }
39
39
  function resolveRuntimeDraftWithRegistration(options, parsedProviderId, initialRegistration) {
40
40
  const providerHint = options.provider ?? parsedProviderId;
41
- const endpointId = (options.endpoint ?? initialRegistration?.defaultEndpointId ?? defaultEndpointForProvider(providerHint));
41
+ const endpointId = (options.endpoint
42
+ ?? initialRegistration?.defaultEndpointId
43
+ ?? (providerHint ? defaultEndpointForProvider(providerHint) : undefined));
42
44
  if (!endpointId) {
43
45
  throw new Error(`Unknown model profile: ${options.model}. Pass provider or endpoint for custom model names.`);
44
46
  }
@@ -85,7 +87,7 @@ function constructResolvedRuntime(draft, credential, options) {
85
87
  effectiveCapabilities: resolveEffectiveModelCapabilities({
86
88
  model: draft.registration.descriptor,
87
89
  protocol,
88
- endpointCapabilities: endpointCapabilitiesFor(draft.endpointId, options.baseURL === undefined || options.endpoint !== undefined),
90
+ endpointCapabilities: endpointCapabilitiesFor(draft.endpointId, options.baseURL === undefined || options.endpoint !== undefined, options.baseURL === undefined),
89
91
  }),
90
92
  ...(draft.registration.recommendedRuntimePolicy ? { runtimePolicy: draft.registration.recommendedRuntimePolicy } : {}),
91
93
  };
@@ -132,20 +134,3 @@ function providerPrefix(model) {
132
134
  function providerIds() {
133
135
  return Array.from(new Set(Object.values(endpointProfiles).map(endpoint => endpoint.providerId)));
134
136
  }
135
- function defaultEndpointForProvider(providerId) {
136
- if (!providerId)
137
- return undefined;
138
- const defaults = {
139
- anthropic: "anthropic.messages",
140
- openai: "openai.chat",
141
- minimax: "minimax.anthropic",
142
- deepseek: "deepseek.anthropic",
143
- kimi: "kimi.anthropic",
144
- qwen: "qwen.anthropic",
145
- gemini: "gemini.google",
146
- glm: "glm.anthropic",
147
- baai: "baai.self-hosted.embeddings",
148
- ollama: "ollama.local",
149
- };
150
- return defaults[providerId];
151
- }
@@ -1,31 +1,41 @@
1
1
  import { PROVIDER_REGISTRY } from "./registry.js";
2
2
  import { OllamaProvider } from "./ollama.js";
3
- import { defaultModelForProvider, getRuntimePolicy, isKnownProviderId } from "./model-registry.js";
4
- function build(providerId, protocol, o) {
3
+ import { defaultEndpointForProvider, defaultModelForProvider, getRuntimePolicy, isKnownProviderId } from "./model-registry.js";
4
+ import { endpointProfiles } from "./endpoints.js";
5
+ function build(providerId, o) {
5
6
  if (!isKnownProviderId(providerId))
6
7
  throw new Error(`Unknown provider: ${providerId}`);
8
+ const defaultProtocol = endpointProfiles[defaultEndpointForProvider(providerId)].protocol;
9
+ const protocol = o.protocol === "openai"
10
+ ? "openai-chat"
11
+ : o.protocol === "anthropic"
12
+ ? "anthropic-messages"
13
+ : defaultProtocol;
14
+ if (protocol !== "openai-chat" && protocol !== "anthropic-messages") {
15
+ throw new Error(`Provider ${providerId} does not use an OpenAI- or Anthropic-compatible default`);
16
+ }
7
17
  const model = o.model ?? defaultModelForProvider(providerId);
8
18
  return PROVIDER_REGISTRY[`${providerId}:${protocol}`](o.apiKey, model, o.retry, o.baseURL, getRuntimePolicy(providerId, model));
9
19
  }
10
20
  /** DeepSeek. Defaults to the OpenAI-compatible wire (richer reasoning-replay handling). */
11
21
  export function deepseek(o) {
12
- return build("deepseek", o.protocol === "anthropic" ? "anthropic-messages" : "openai-chat", o);
22
+ return build("deepseek", o);
13
23
  }
14
24
  /** Moonshot Kimi. Defaults to the OpenAI-compatible wire. */
15
25
  export function kimi(o) {
16
- return build("kimi", o.protocol === "anthropic" ? "anthropic-messages" : "openai-chat", o);
26
+ return build("kimi", o);
17
27
  }
18
28
  /** Alibaba Qwen / DashScope. Defaults to the OpenAI-compatible (DashScope) wire. */
19
29
  export function qwen(o) {
20
- return build("qwen", o.protocol === "anthropic" ? "anthropic-messages" : "openai-chat", o);
30
+ return build("qwen", o);
21
31
  }
22
32
  /** Zhipu GLM. Defaults to the OpenAI-compatible wire. */
23
33
  export function glm(o) {
24
- return build("glm", o.protocol === "anthropic" ? "anthropic-messages" : "openai-chat", o);
34
+ return build("glm", o);
25
35
  }
26
36
  /** MiniMax. Defaults to the Anthropic-compatible wire (the primary M2.x path). */
27
37
  export function minimax(o) {
28
- return build("minimax", o.protocol === "openai" ? "openai-chat" : "anthropic-messages", o);
38
+ return build("minimax", o);
29
39
  }
30
40
  /** Google Gemini (single wire). */
31
41
  export function gemini(o) {
@@ -1,5 +1,6 @@
1
1
  import { projectToolOutputToText } from "./content-normalization.js";
2
2
  import { normalizeToolCall } from "./base.js";
3
+ import { normalizeGeminiUsage } from "./usage-normalizer.js";
3
4
  import { ProtocolResponseError, GEMINI_PROTOCOL_CAPABILITIES, } from "./protocol-adapter.js";
4
5
  // Google Generate Content streams response chunks, while @google/generative-ai 0.24.1 exposes
5
6
  // a separate promise for the aggregated response. Candidate finishReason and aggregate usage are
@@ -145,8 +146,8 @@ function numberField(raw, field) {
145
146
  const value = raw[field];
146
147
  if (value === undefined)
147
148
  return undefined;
148
- if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
149
- throw new ProtocolResponseError("gemini", `usage.${field} must be a non-negative finite number`);
149
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
150
+ throw new ProtocolResponseError("gemini", `usage.${field} must be a non-negative safe integer`);
150
151
  }
151
152
  return value;
152
153
  }
@@ -222,6 +223,8 @@ export class GeminiAdapter {
222
223
  ...(usage.cacheReadInputTokens
223
224
  ? { cacheReadInputTokens: usage.cacheReadInputTokens }
224
225
  : {}),
226
+ ...(usage.cacheTelemetryStatus ? { cacheTelemetryStatus: usage.cacheTelemetryStatus } : {}),
227
+ ...(usage.cacheTelemetrySource ? { cacheTelemetrySource: usage.cacheTelemetrySource } : {}),
225
228
  providerUsage: usage,
226
229
  ...(stopReason ? { stopReason } : {}),
227
230
  ...(rawStopReason ? { rawStopReason } : {}),
@@ -236,19 +239,8 @@ export class GeminiAdapter {
236
239
  throw new ProtocolResponseError("gemini", "usage must be an object");
237
240
  }
238
241
  const usage = raw;
239
- const inputTokens = numberField(usage, "promptTokenCount");
240
- const outputTokens = numberField(usage, "candidatesTokenCount");
241
242
  numberField(usage, "totalTokenCount");
242
- const cacheReadInputTokens = numberField(usage, "cachedContentTokenCount");
243
- if (inputTokens === undefined
244
- && outputTokens === undefined
245
- && cacheReadInputTokens === undefined)
246
- return undefined;
247
- return {
248
- inputTokens: inputTokens ?? 0,
249
- outputTokens: outputTokens ?? 0,
250
- ...(cacheReadInputTokens ? { cacheReadInputTokens } : {}),
251
- };
243
+ return normalizeGeminiUsage(usage);
252
244
  }
253
245
  normalizeStopReason(raw) {
254
246
  if (raw === undefined)
@@ -29,8 +29,26 @@ export interface DynamicModelDescriptorResolver {
29
29
  }
30
30
  export interface EndpointRuntimeCapabilities {
31
31
  nativeTokenCounting?: boolean;
32
+ promptCaching?: boolean;
32
33
  protocolOverrides?: ProtocolRuntimeCapabilityOverrides;
33
34
  }
35
+ export interface CacheCapabilityEvidence {
36
+ endpointId: EndpointProfileId;
37
+ source: string;
38
+ verifiedAt: "2026-08-26";
39
+ classification: "documentation" | "live_probe";
40
+ usageFields: readonly string[];
41
+ }
42
+ export interface TokenMeasurementEvidence {
43
+ endpointId: EndpointProfileId;
44
+ source: string;
45
+ verifiedAt: "2026-08-26";
46
+ providerApiState: "supported" | "unsupported" | "unknown";
47
+ adapterState: "available" | "unavailable";
48
+ method: "provider_preflight" | "official_local_tokenizer" | "postflight" | "heuristic";
49
+ coverage: readonly string[];
50
+ sdk: string;
51
+ }
34
52
  export type CapabilityEvidenceLayer = "model" | "protocol" | "endpoint";
35
53
  export interface EffectiveCapability<T = boolean> {
36
54
  state: CapabilityState;
@@ -74,7 +92,10 @@ export interface RegistryRuleEvidence {
74
92
  verifiedAt: "2026-08-12";
75
93
  }
76
94
  export declare const registryEvidence: readonly RegistryRuleEvidence[];
95
+ export declare const cacheCapabilityEvidence: readonly CacheCapabilityEvidence[];
96
+ export declare const tokenMeasurementEvidence: readonly TokenMeasurementEvidence[];
77
97
  export declare function defaultModelForProvider(providerId: ProviderId): string;
98
+ export declare function defaultEndpointForProvider(providerId: ProviderId): EndpointProfileId;
78
99
  export declare class ModelRegistry {
79
100
  resolve(modelId: string, providerId?: ProviderId): ModelRegistration | undefined;
80
101
  }
@@ -94,5 +115,5 @@ export declare function resolveEffectiveModelCapabilities(input: {
94
115
  endpointCapabilities?: EndpointRuntimeCapabilities;
95
116
  }): EffectiveModelCapabilities;
96
117
  export declare function generationProtocol(protocol: EndpointProtocol): GenerationProtocol | undefined;
97
- export declare function endpointCapabilitiesFor(endpointId: EndpointProfileId, preserveEndpointIdentity: boolean): EndpointRuntimeCapabilities | undefined;
118
+ export declare function endpointCapabilitiesFor(endpointId: EndpointProfileId, preserveEndpointIdentity: boolean, preserveCacheEvidence?: boolean): EndpointRuntimeCapabilities | undefined;
98
119
  export declare function isKnownProviderId(value: string): value is ProviderId;