pi-background-tasks 2.4.2 → 2.5.0

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.
@@ -5,9 +5,9 @@ import { join } from 'node:path';
5
5
 
6
6
  export const CLAUDE_CODE_SESSION_HEADER = 'X-Claude-Code-Session-Id';
7
7
 
8
- const CLAUDE_CODE_VERSION = '2.1.173';
8
+ const CLAUDE_CODE_VERSION = '2.1.251';
9
9
  const CLAUDE_CODE_ENTRYPOINT = 'sdk-cli';
10
- const CLAUDE_CODE_USER_AGENT = 'claude-cli/2.1.173 (external, sdk-cli)';
10
+ const CLAUDE_CODE_USER_AGENT = 'claude-cli/2.1.251 (external, sdk-cli)';
11
11
  export const ANTHROPIC_1M_CONTEXT_BETA = 'context-1m-2025-08-07' as const;
12
12
  export const CLAUDE_CODE_200K_SUBSCRIPTION_CONTEXT_WINDOW = 200_000 as const;
13
13
 
@@ -20,7 +20,9 @@ type ClaudeCode200KSubscriptionBetaValue =
20
20
  | 'prompt-caching-scope-2026-01-05'
21
21
  | 'advisor-tool-2026-03-01'
22
22
  | 'structured-outputs-2025-12-15'
23
- | 'mid-conversation-system-2026-04-07';
23
+ | 'mid-conversation-system-2026-04-07'
24
+ | 'thinking-binding-controls-2026-08-01'
25
+ | 'cache-diagnosis-2026-04-07';
24
26
 
25
27
  const CLAUDE_CODE_LEGACY_BETA_VALUES = [
26
28
  'claude-code-20250219',
@@ -41,6 +43,11 @@ const CLAUDE_CODE_ADAPTIVE_200K_BETA_VALUES = [
41
43
  'prompt-caching-scope-2026-01-05',
42
44
  'mid-conversation-system-2026-04-07',
43
45
  ] as const satisfies readonly ClaudeCode200KSubscriptionBetaValue[];
46
+ const CLAUDE_CODE_FABLE_5_1_200K_BETA_VALUES = [
47
+ ...CLAUDE_CODE_ADAPTIVE_200K_BETA_VALUES,
48
+ 'thinking-binding-controls-2026-08-01',
49
+ 'cache-diagnosis-2026-04-07',
50
+ ] as const satisfies readonly ClaudeCode200KSubscriptionBetaValue[];
44
51
 
45
52
  function build200KSubscriptionBetaHeader(
46
53
  values: readonly ClaudeCode200KSubscriptionBetaValue[],
@@ -57,6 +64,9 @@ export const CLAUDE_CODE_BETA = build200KSubscriptionBetaHeader(CLAUDE_CODE_LEGA
57
64
  const CLAUDE_CODE_ADAPTIVE_200K_BETA = build200KSubscriptionBetaHeader(
58
65
  CLAUDE_CODE_ADAPTIVE_200K_BETA_VALUES,
59
66
  );
67
+ const CLAUDE_CODE_FABLE_5_1_200K_BETA = build200KSubscriptionBetaHeader(
68
+ CLAUDE_CODE_FABLE_5_1_200K_BETA_VALUES,
69
+ );
60
70
  const CLAUDE_AGENT_SDK_SYSTEM_TEXT =
61
71
  "You are a Claude agent, built on Anthropic's Claude Agent SDK.";
62
72
  const FINGERPRINT_SALT = '59cf53e54c78';
@@ -68,6 +78,17 @@ export const ANTHROPIC_ATTRIBUTION_CLAIM_CHANNEL = 'pi-anthropic-attribution:cla
68
78
  const ANTHROPIC_ATTRIBUTION_CLAIM_SCHEMA = 'pi-anthropic-attribution.claim.v1';
69
79
  const NATIVE_ATTESTATION_PLACEHOLDER = '00000';
70
80
  const ANTHROPIC_CACHE_CONTROL_BREAKPOINT_LIMIT = 4;
81
+ const ANTHROPIC_LINEAGE_DIAGNOSTIC_TYPE = 'anthropic-cache-lineage';
82
+ const ANTHROPIC_LINEAGE_SCHEMA = 'pi-anthropic-attribution.lineage.v1';
83
+ // Bump whenever any system/tool/message wire projection changes. Old receipts then
84
+ // become legacy and cannot authorize signature replay under a rewritten prefix.
85
+ const ANTHROPIC_PROJECTION_VERSION = 3;
86
+ const ANTHROPIC_OFFICIAL_ORIGIN = 'https://api.anthropic.com';
87
+ const ANTHROPIC_BETA_MESSAGES_URL = `${ANTHROPIC_OFFICIAL_ORIGIN}/v1/messages?beta=true`;
88
+ const ANTHROPIC_CACHE_DIAGNOSTICS_BETA = 'cache-diagnosis-2026-04-07';
89
+ const ANTHROPIC_THINKING_BINDING_BETA = 'thinking-binding-controls-2026-08-01';
90
+ const COMPACTION_SUMMARY_PREFIX =
91
+ 'The conversation history before this point was compacted into the following summary:';
71
92
 
72
93
  // Sanitization behavior derived from the MIT-licensed ravshansbox/pi-anthropic-sps
73
94
  // extension at commit 17409b5615f0ec0625776bc5434f92f2c55e3fd0. Keep exact-match
@@ -143,12 +164,18 @@ export interface ClaudeCodeModelPolicy {
143
164
  readonly beta: string;
144
165
  readonly thinkingPolicy: ClaudeCodeThinkingPolicy;
145
166
  readonly contextWindow: typeof CLAUDE_CODE_200K_SUBSCRIPTION_CONTEXT_WINDOW;
167
+ readonly enforcesThinkingPrefixBinding: boolean;
168
+ readonly supportsCacheDiagnostics: boolean;
146
169
  }
147
170
 
148
171
  function claudeCode200KSubscriptionPolicy(
149
172
  modelId: string,
150
173
  beta: string,
151
174
  thinkingPolicy: ClaudeCodeThinkingPolicy,
175
+ features: {
176
+ readonly enforcesThinkingPrefixBinding?: boolean;
177
+ readonly supportsCacheDiagnostics?: boolean;
178
+ } = {},
152
179
  ): ClaudeCodeModelPolicy {
153
180
  if (beta.split(',').includes(ANTHROPIC_1M_CONTEXT_BETA)) {
154
181
  throw new Error(
@@ -160,6 +187,8 @@ function claudeCode200KSubscriptionPolicy(
160
187
  beta,
161
188
  thinkingPolicy,
162
189
  contextWindow: CLAUDE_CODE_200K_SUBSCRIPTION_CONTEXT_WINDOW,
190
+ enforcesThinkingPrefixBinding: features.enforcesThinkingPrefixBinding === true,
191
+ supportsCacheDiagnostics: features.supportsCacheDiagnostics === true,
163
192
  };
164
193
  }
165
194
 
@@ -209,6 +238,12 @@ const CLAUDE_CODE_MODEL_POLICIES: Record<string, ClaudeCodeModelPolicy> = Object
209
238
  CLAUDE_CODE_ADAPTIVE_200K_BETA,
210
239
  'adaptive-effort',
211
240
  ),
241
+ 'claude-fable-5-1': claudeCode200KSubscriptionPolicy(
242
+ 'claude-fable-5-1',
243
+ CLAUDE_CODE_FABLE_5_1_200K_BETA,
244
+ 'adaptive-effort',
245
+ { enforcesThinkingPrefixBinding: true, supportsCacheDiagnostics: true },
246
+ ),
212
247
  'claude-haiku-4-5': claudeCode200KSubscriptionPolicy(
213
248
  'claude-haiku-4-5',
214
249
  CLAUDE_CODE_BETA,
@@ -344,10 +379,6 @@ export interface PiExtensionHost extends PiProviderRegistrationHost {
344
379
  eventName: 'session_start' | 'session_shutdown' | 'session_tree' | 'before_agent_start',
345
380
  handler: (event: unknown, ctx: PiContextLike) => void,
346
381
  ): void;
347
- on(
348
- eventName: 'before_provider_request',
349
- handler: (event: { readonly payload: unknown }, ctx: PiContextLike) => unknown,
350
- ): void;
351
382
  registerCommand(name: string, config: PiCommandConfigLike): void;
352
383
  appendEntry(customType: string, data?: unknown): void;
353
384
  }
@@ -356,12 +387,28 @@ type PiContentBlock =
356
387
  | { readonly type: 'text'; readonly text: string }
357
388
  | { readonly type: 'image'; readonly mimeType: string; readonly data: string };
358
389
 
390
+ interface PiAssistantDiagnosticLike {
391
+ readonly type: string;
392
+ readonly timestamp: number;
393
+ readonly details?: JsonObject;
394
+ }
395
+
359
396
  type PiMessage =
360
397
  | { readonly role: 'user'; readonly content: string | readonly PiContentBlock[] }
361
- | { readonly role: 'assistant'; readonly content: readonly JsonObject[] }
398
+ | {
399
+ readonly role: 'assistant';
400
+ readonly content: readonly JsonObject[];
401
+ readonly provider?: string;
402
+ readonly api?: string;
403
+ readonly model?: string;
404
+ readonly responseId?: string;
405
+ readonly stopReason?: string;
406
+ readonly diagnostics?: readonly PiAssistantDiagnosticLike[];
407
+ }
362
408
  | {
363
409
  readonly role: 'toolResult';
364
410
  readonly toolCallId: string;
411
+ readonly toolName?: string;
365
412
  readonly content: readonly PiContentBlock[];
366
413
  readonly isError?: boolean;
367
414
  };
@@ -402,6 +449,10 @@ export interface PiSimpleStreamOptions {
402
449
  ) => Promise<void> | void;
403
450
  }
404
451
 
452
+ export interface AnthropicTransportDependencies {
453
+ readonly loadAccount?: () => ClaudeAttributionAccount;
454
+ }
455
+
405
456
  export interface AssistantMessageLike {
406
457
  role: 'assistant';
407
458
  content: JsonObject[];
@@ -420,6 +471,7 @@ export interface AssistantMessageLike {
420
471
  stopReason: 'stop' | 'length' | 'toolUse' | 'aborted' | 'error';
421
472
  timestamp: number;
422
473
  responseId?: string;
474
+ diagnostics?: PiAssistantDiagnosticLike[];
423
475
  errorMessage?: string;
424
476
  }
425
477
 
@@ -569,12 +621,8 @@ function parseCacheRetention(value: string, source: string): CacheRetention {
569
621
  );
570
622
  }
571
623
 
572
- /**
573
- * Resolve retention without allowing the extension default to override an
574
- * explicit call-level posture (notably Pi's cacheRetention=none compaction calls).
575
- * Precedence: request option -> persisted session override -> process/provider env
576
- * -> the repo policy default of one hour.
577
- */
624
+ /** Resolve an explicit request posture. Registered Pi sessions apply the stronger
625
+ * one-hour policy through resolveRegisteredCacheRetention below. */
578
626
  export function resolveCacheRetentionPreference(
579
627
  options?: {
580
628
  readonly cacheRetention?: CacheRetention;
@@ -589,6 +637,20 @@ export function resolveCacheRetentionPreference(
589
637
  return 'long';
590
638
  }
591
639
 
640
+ function resolveRegisteredCacheRetention(
641
+ options: PiSimpleStreamOptions | undefined,
642
+ sessionOverride: Exclude<CacheRetention, 'none'> | undefined,
643
+ ): CacheRetention {
644
+ if (options?.cacheRetention === 'none') return 'none';
645
+ if (sessionOverride !== undefined) return sessionOverride;
646
+ const configured = providerEnvValue(CACHE_RETENTION_ENV, options?.env);
647
+ if (configured !== undefined) return parseCacheRetention(configured, CACHE_RETENTION_ENV);
648
+ if (options?.cacheRetention === 'long') return 'long';
649
+ // Pi's generic provider default is five minutes. Subscription coding sessions
650
+ // routinely have turns longer than that, so the attributed route pins one hour.
651
+ return 'long';
652
+ }
653
+
592
654
  /** Restore the latest branch-local command decision; custom entries stay out of LLM context. */
593
655
  export function restoreAnthropicSessionCacheRetention(
594
656
  entries: readonly unknown[],
@@ -715,8 +777,48 @@ function inspectCacheControls(payload: JsonObject): CacheControlInspection {
715
777
  return { count, retention: count === 0 ? undefined : hasLong ? 'long' : 'short' };
716
778
  }
717
779
 
780
+ interface CacheControlOccurrence {
781
+ readonly path: string;
782
+ readonly control: AnthropicCacheControl;
783
+ }
784
+
785
+ function cacheControlOccurrences(payload: JsonObject): CacheControlOccurrence[] {
786
+ const occurrences: CacheControlOccurrence[] = [];
787
+ const inspectBlocks = (value: unknown, path: string): void => {
788
+ if (!Array.isArray(value)) return;
789
+ value.forEach((block, index) => {
790
+ if (!isPlainObject(block)) return;
791
+ const blockPath = `${path}[${String(index)}]`;
792
+ if (block['cache_control'] !== undefined) {
793
+ const control = cloneAnthropicCacheControl(block['cache_control']);
794
+ if (control !== undefined) occurrences.push({ path: blockPath, control });
795
+ }
796
+ if (block['type'] === 'tool_result') inspectBlocks(block['content'], `${blockPath}.content`);
797
+ });
798
+ };
799
+
800
+ if (payload['cache_control'] !== undefined) {
801
+ const control = cloneAnthropicCacheControl(payload['cache_control']);
802
+ if (control !== undefined) occurrences.push({ path: '$', control });
803
+ }
804
+ inspectBlocks(payload['system'], '$.system');
805
+ inspectBlocks(payload['tools'], '$.tools');
806
+ const messages = payload['messages'];
807
+ if (Array.isArray(messages)) {
808
+ messages.forEach((message, index) => {
809
+ if (isPlainObject(message))
810
+ inspectBlocks(message['content'], `$.messages[${String(index)}].content`);
811
+ });
812
+ }
813
+ return occurrences;
814
+ }
815
+
718
816
  function countCacheControlBreakpoints(payload: JsonObject): number {
719
- return inspectCacheControls(payload).count;
817
+ return cacheControlOccurrences(payload).length;
818
+ }
819
+
820
+ function cacheControlTopologySha256(payload: JsonObject): string {
821
+ return sha256Canonical(cacheControlOccurrences(payload));
720
822
  }
721
823
 
722
824
  function assertCacheControlBreakpointLimit(payload: JsonObject): void {
@@ -781,12 +883,15 @@ export function isAnthropicContext(ctx: PiContextLike): boolean {
781
883
  return ctx.model?.provider === 'anthropic';
782
884
  }
783
885
 
784
- function getSessionId(ctx: PiContextLike): string {
785
- const sessionId = ctx.sessionManager.getSessionId();
786
- if (typeof sessionId !== 'string' || sessionId.trim().length === 0) {
787
- throw new Error('Anthropic attribution requires a non-empty Pi session id');
886
+ function requireSessionId(value: unknown, source: string): string {
887
+ if (typeof value !== 'string' || value.trim().length === 0) {
888
+ throw new Error(`Anthropic attribution requires a non-empty ${source}`);
788
889
  }
789
- return sessionId;
890
+ return value;
891
+ }
892
+
893
+ function getSessionId(ctx: PiContextLike): string {
894
+ return requireSessionId(ctx.sessionManager.getSessionId(), 'Pi session id');
790
895
  }
791
896
 
792
897
  function normalizedAnthropicModelId(model: PiModelLike): string {
@@ -867,16 +972,22 @@ export function registerAnthropicAttributionProvider(
867
972
  pi: PiProviderRegistrationHost,
868
973
  ctx: PiContextLike,
869
974
  getSessionOverride: () => Exclude<CacheRetention, 'none'> | undefined = () => undefined,
975
+ dependencies: AnthropicTransportDependencies = {},
870
976
  ): void {
871
977
  if (!isAnthropicContext(ctx)) return;
872
978
  pi.registerProvider('anthropic', {
873
979
  api: 'anthropic-messages',
874
980
  headers: buildAnthropicAttributionHeaders(getSessionId(ctx), ctx.model),
875
981
  streamSimple: (model, context, options) =>
876
- streamAnthropicViaBetaMessages(model, context, {
877
- ...(options ?? {}),
878
- cacheRetention: resolveCacheRetentionPreference(options, getSessionOverride()),
879
- }),
982
+ streamAnthropicViaBetaMessages(
983
+ model,
984
+ context,
985
+ {
986
+ ...(options ?? {}),
987
+ cacheRetention: resolveRegisteredCacheRetention(options, getSessionOverride()),
988
+ },
989
+ dependencies,
990
+ ),
880
991
  });
881
992
  }
882
993
 
@@ -1031,26 +1142,50 @@ function appendAuditRecord(args: {
1031
1142
  appendFileSync(auditPath, `${JSON.stringify(record)}\n`, { encoding: 'utf8', mode: 0o600 });
1032
1143
  }
1033
1144
 
1034
- export function rewriteAnthropicRequestPayload(args: {
1145
+ function requireAttributionAccount(value: unknown): ClaudeAttributionAccount {
1146
+ if (
1147
+ !isPlainObject(value) ||
1148
+ typeof value['deviceId'] !== 'string' ||
1149
+ value['deviceId'].trim().length === 0 ||
1150
+ typeof value['accountUuid'] !== 'string' ||
1151
+ value['accountUuid'].trim().length === 0
1152
+ ) {
1153
+ throw new Error('Anthropic attribution account loader returned malformed account identity');
1154
+ }
1155
+ return { deviceId: value['deviceId'], accountUuid: value['accountUuid'] };
1156
+ }
1157
+
1158
+ function anthropicMetadataUserId(account: ClaudeAttributionAccount, sessionId: string): string {
1159
+ return JSON.stringify({
1160
+ account_uuid: account.accountUuid,
1161
+ device_id: account.deviceId,
1162
+ session_id: sessionId,
1163
+ });
1164
+ }
1165
+
1166
+ interface AnthropicAttributionForSessionArgs {
1035
1167
  readonly payload: unknown;
1036
- readonly ctx: PiContextLike;
1168
+ readonly model: PiModelLike;
1169
+ readonly sessionId: string;
1037
1170
  readonly account: ClaudeAttributionAccount;
1038
1171
  readonly headerRegistered?: boolean;
1039
1172
  readonly cacheRetention?: CacheRetention;
1040
- readonly env?: ProviderEnv;
1041
- }): unknown {
1042
- if (!isAnthropicContext(args.ctx)) return undefined;
1173
+ }
1174
+
1175
+ function rewriteAnthropicRequestPayloadForSession(
1176
+ args: AnthropicAttributionForSessionArgs,
1177
+ ): JsonObject {
1043
1178
  if (!isPlainObject(args.payload)) {
1044
1179
  throw new Error('Anthropic attribution expected provider payload to be a JSON object');
1045
1180
  }
1046
1181
 
1047
- const sessionId = getSessionId(args.ctx);
1182
+ const sessionId = requireSessionId(args.sessionId, 'provider options.sessionId');
1048
1183
  const metadata = args.payload['metadata'] === undefined ? {} : args.payload['metadata'];
1049
1184
  if (!isPlainObject(metadata)) {
1050
1185
  throw new Error('Anthropic attribution expected payload.metadata to be an object when present');
1051
1186
  }
1052
1187
 
1053
- const policy = resolveClaudeCodeModelPolicy(args.ctx.model ?? {});
1188
+ const policy = resolveClaudeCodeModelPolicy(args.model);
1054
1189
  const maxTokens =
1055
1190
  args.payload['max_tokens'] === undefined
1056
1191
  ? undefined
@@ -1068,7 +1203,7 @@ export function rewriteAnthropicRequestPayload(args: {
1068
1203
  const cacheControl =
1069
1204
  configuredCacheRetention === undefined
1070
1205
  ? undefined
1071
- : resolveAnthropicCacheControl(args.ctx.model, {
1206
+ : resolveAnthropicCacheControl(args.model, {
1072
1207
  cacheRetention: configuredCacheRetention,
1073
1208
  });
1074
1209
 
@@ -1076,11 +1211,7 @@ export function rewriteAnthropicRequestPayload(args: {
1076
1211
  ...args.payload,
1077
1212
  metadata: {
1078
1213
  ...metadata,
1079
- user_id: JSON.stringify({
1080
- account_uuid: args.account.accountUuid,
1081
- device_id: args.account.deviceId,
1082
- session_id: sessionId,
1083
- }),
1214
+ user_id: anthropicMetadataUserId(args.account, sessionId),
1084
1215
  },
1085
1216
  system: withClaudeCodeSystemIdentity(args.payload['system'], billingSystemText, cacheControl),
1086
1217
  };
@@ -1101,24 +1232,111 @@ export function rewriteAnthropicRequestPayload(args: {
1101
1232
  return rewritten;
1102
1233
  }
1103
1234
 
1235
+ function assertProtectedAnthropicAttribution(args: {
1236
+ readonly payload: JsonObject;
1237
+ readonly account: ClaudeAttributionAccount;
1238
+ readonly sessionId: string;
1239
+ readonly billingSystemText: string;
1240
+ readonly modelId: string;
1241
+ readonly cacheControlTopologySha256: string;
1242
+ }): void {
1243
+ if (args.payload['model'] !== args.modelId || args.payload['stream'] !== true) {
1244
+ throw new Error(
1245
+ 'Anthropic protected attribution model/stream route changed during payload middleware',
1246
+ );
1247
+ }
1248
+ const metadata = args.payload['metadata'];
1249
+ if (
1250
+ !isPlainObject(metadata) ||
1251
+ metadata['user_id'] !== anthropicMetadataUserId(args.account, args.sessionId)
1252
+ ) {
1253
+ throw new Error(
1254
+ 'Anthropic protected attribution metadata.user_id/account/device/session_id changed during payload middleware',
1255
+ );
1256
+ }
1257
+ const system = args.payload['system'];
1258
+ if (!Array.isArray(system)) {
1259
+ throw new Error(
1260
+ 'Anthropic protected attribution system identity was removed during payload middleware',
1261
+ );
1262
+ }
1263
+ const billingBlocks = system.filter(
1264
+ (block) =>
1265
+ isPlainObject(block) &&
1266
+ typeof block['text'] === 'string' &&
1267
+ block['text'].startsWith('x-anthropic-billing-header:'),
1268
+ );
1269
+ const sdkBlocks = system.filter(
1270
+ (block) => isPlainObject(block) && block['text'] === CLAUDE_AGENT_SDK_SYSTEM_TEXT,
1271
+ );
1272
+ if (
1273
+ billingBlocks.length !== 1 ||
1274
+ billingBlocks[0]?.['text'] !== args.billingSystemText ||
1275
+ sdkBlocks.length !== 1 ||
1276
+ system[0] !== billingBlocks[0] ||
1277
+ system[1] !== sdkBlocks[0]
1278
+ ) {
1279
+ throw new Error(
1280
+ 'Anthropic protected attribution system identity changed during payload middleware',
1281
+ );
1282
+ }
1283
+ assertCacheControlBreakpointLimit(args.payload);
1284
+ if (cacheControlTopologySha256(args.payload) !== args.cacheControlTopologySha256) {
1285
+ throw new Error('Anthropic protected cache-control topology changed during payload middleware');
1286
+ }
1287
+ }
1288
+
1289
+ export function rewriteAnthropicRequestPayload(args: {
1290
+ readonly payload: unknown;
1291
+ readonly ctx: PiContextLike;
1292
+ readonly account: ClaudeAttributionAccount;
1293
+ readonly headerRegistered?: boolean;
1294
+ readonly cacheRetention?: CacheRetention;
1295
+ readonly env?: ProviderEnv;
1296
+ }): unknown {
1297
+ if (!isAnthropicContext(args.ctx)) return undefined;
1298
+ return rewriteAnthropicRequestPayloadForSession({
1299
+ payload: args.payload,
1300
+ model: args.ctx.model ?? {},
1301
+ sessionId: getSessionId(args.ctx),
1302
+ account: args.account,
1303
+ ...(args.headerRegistered === undefined ? {} : { headerRegistered: args.headerRegistered }),
1304
+ ...(args.cacheRetention === undefined ? {} : { cacheRetention: args.cacheRetention }),
1305
+ });
1306
+ }
1307
+
1104
1308
  function sanitizeSurrogates(text: string): string {
1105
- return text.replace(/[\uD800-\uDFFF]/g, '\uFFFD');
1309
+ let sanitized = '';
1310
+ for (let index = 0; index < text.length; index += 1) {
1311
+ const code = text.charCodeAt(index);
1312
+ if (code >= 0xd800 && code <= 0xdbff) {
1313
+ const next = index + 1 < text.length ? text.charCodeAt(index + 1) : -1;
1314
+ if (next >= 0xdc00 && next <= 0xdfff) {
1315
+ sanitized += text[index] ?? '';
1316
+ sanitized += text[index + 1] ?? '';
1317
+ index += 1;
1318
+ } else {
1319
+ sanitized += '\uFFFD';
1320
+ }
1321
+ } else if (code >= 0xdc00 && code <= 0xdfff) {
1322
+ sanitized += '\uFFFD';
1323
+ } else {
1324
+ sanitized += text[index] ?? '';
1325
+ }
1326
+ }
1327
+ return sanitized;
1106
1328
  }
1107
1329
 
1108
- function convertContentBlocks(content: readonly PiContentBlock[]): string | JsonObject[] {
1109
- const hasImages = content.some((block) => block.type === 'image');
1110
- if (!hasImages)
1111
- return sanitizeSurrogates(
1112
- content.map((block) => (block.type === 'text' ? block.text : '')).join('\n'),
1113
- );
1114
- const blocks = content.map((block) => {
1330
+ function convertContentBlocks(content: readonly PiContentBlock[]): JsonObject[] {
1331
+ const blocks: JsonObject[] = content.map((block) => {
1115
1332
  if (block.type === 'text') return { type: 'text', text: sanitizeSurrogates(block.text) };
1116
1333
  return {
1117
1334
  type: 'image',
1118
1335
  source: { type: 'base64', media_type: block.mimeType, data: block.data },
1119
1336
  };
1120
1337
  });
1121
- if (!blocks.some((block) => block.type === 'text'))
1338
+ if (blocks.length === 0) blocks.push({ type: 'text', text: '' });
1339
+ if (!blocks.some((block) => block['type'] === 'text'))
1122
1340
  blocks.unshift({ type: 'text', text: '(see attached image)' });
1123
1341
  return blocks;
1124
1342
  }
@@ -1146,9 +1364,9 @@ function markMessageContentCacheSurface(
1146
1364
  if (role !== 'user' && role !== 'assistant') return false;
1147
1365
  const content = message['content'];
1148
1366
  if (typeof content === 'string') {
1149
- if (content.trim().length === 0) return false;
1150
- message['content'] = [{ type: 'text', text: content, cache_control: { ...cacheControl } }];
1151
- return true;
1367
+ throw new Error(
1368
+ 'Anthropic attribution cache projection encountered non-canonical string message content',
1369
+ );
1152
1370
  }
1153
1371
  if (!Array.isArray(content)) return false;
1154
1372
  for (let index = content.length - 1; index >= 0; index -= 1) {
@@ -1173,92 +1391,389 @@ function markLastConversationCacheSurface(
1173
1391
  return output;
1174
1392
  }
1175
1393
 
1394
+ function canonicalizeJson(value: unknown): unknown {
1395
+ if (Array.isArray(value)) return value.map(canonicalizeJson);
1396
+ if (!isPlainObject(value)) return value;
1397
+ const output: JsonObject = {};
1398
+ for (const key of Object.keys(value).sort()) {
1399
+ const child = value[key];
1400
+ if (child !== undefined) output[key] = canonicalizeJson(child);
1401
+ }
1402
+ return output;
1403
+ }
1404
+
1405
+ function canonicalJson(value: unknown): string {
1406
+ return JSON.stringify(canonicalizeJson(value));
1407
+ }
1408
+
1409
+ function sha256Canonical(value: unknown): string {
1410
+ return createHash('sha256').update(canonicalJson(value), 'utf8').digest('hex');
1411
+ }
1412
+
1413
+ function isSha256(value: unknown): value is string {
1414
+ return typeof value === 'string' && /^[a-f0-9]{64}$/u.test(value);
1415
+ }
1416
+
1417
+ function blockWithoutCacheControl(block: unknown): unknown {
1418
+ if (!isPlainObject(block)) return block;
1419
+ const output = { ...block };
1420
+ delete output['cache_control'];
1421
+ return output;
1422
+ }
1423
+
1424
+ function promptMessagesWithoutCacheControls(messages: readonly JsonObject[]): JsonObject[] {
1425
+ return messages.map((message) => {
1426
+ const content = message['content'];
1427
+ return {
1428
+ ...message,
1429
+ ...(Array.isArray(content) ? { content: content.map(blockWithoutCacheControl) } : {}),
1430
+ };
1431
+ });
1432
+ }
1433
+
1434
+ function promptMessagesHash(messages: readonly JsonObject[]): string {
1435
+ return sha256Canonical(promptMessagesWithoutCacheControls(messages));
1436
+ }
1437
+
1438
+ function assistantContentHash(content: readonly JsonObject[]): string {
1439
+ return sha256Canonical(content);
1440
+ }
1441
+
1442
+ interface AnthropicLineageDetails {
1443
+ readonly schema_version: typeof ANTHROPIC_LINEAGE_SCHEMA;
1444
+ readonly projection_version: typeof ANTHROPIC_PROJECTION_VERSION;
1445
+ readonly source_provider: 'anthropic';
1446
+ readonly source_api: 'anthropic-messages';
1447
+ readonly source_model: string;
1448
+ readonly response_id: string;
1449
+ readonly assistant_content_sha256: string;
1450
+ readonly conversation_static_sha256: string;
1451
+ readonly request_message_count: number;
1452
+ readonly request_messages_sha256: string;
1453
+ readonly cache_profile_sha256: string;
1454
+ readonly cache_retention: CacheRetention;
1455
+ readonly compaction_boundary_sha256: string | null;
1456
+ readonly signature_epoch_sha256: string;
1457
+ readonly signature_epoch_inherits_prior: boolean;
1458
+ readonly previous_message_id: string | null;
1459
+ }
1460
+
1461
+ function parseAnthropicLineageDetails(
1462
+ message: Extract<PiMessage, { role: 'assistant' }>,
1463
+ ): AnthropicLineageDetails | undefined {
1464
+ const diagnostic = [...(message.diagnostics ?? [])]
1465
+ .reverse()
1466
+ .find((candidate) => candidate.type === ANTHROPIC_LINEAGE_DIAGNOSTIC_TYPE);
1467
+ const details = diagnostic?.details;
1468
+ if (!isPlainObject(details)) return undefined;
1469
+ if (
1470
+ details['schema_version'] !== ANTHROPIC_LINEAGE_SCHEMA ||
1471
+ details['projection_version'] !== ANTHROPIC_PROJECTION_VERSION ||
1472
+ details['source_provider'] !== 'anthropic' ||
1473
+ details['source_api'] !== 'anthropic-messages' ||
1474
+ typeof details['source_model'] !== 'string' ||
1475
+ typeof details['response_id'] !== 'string' ||
1476
+ !isSha256(details['assistant_content_sha256']) ||
1477
+ !isSha256(details['conversation_static_sha256']) ||
1478
+ !Number.isSafeInteger(details['request_message_count']) ||
1479
+ (details['request_message_count'] as number) < 0 ||
1480
+ !isSha256(details['request_messages_sha256']) ||
1481
+ !isSha256(details['cache_profile_sha256']) ||
1482
+ (details['cache_retention'] !== 'none' &&
1483
+ details['cache_retention'] !== 'short' &&
1484
+ details['cache_retention'] !== 'long') ||
1485
+ (details['compaction_boundary_sha256'] !== null &&
1486
+ !isSha256(details['compaction_boundary_sha256'])) ||
1487
+ !isSha256(details['signature_epoch_sha256']) ||
1488
+ typeof details['signature_epoch_inherits_prior'] !== 'boolean' ||
1489
+ (details['previous_message_id'] !== null && typeof details['previous_message_id'] !== 'string')
1490
+ ) {
1491
+ return undefined;
1492
+ }
1493
+ return details as unknown as AnthropicLineageDetails;
1494
+ }
1495
+
1496
+ function canTargetReadAnthropicThinking(sourceModel: string, targetModel: string): boolean {
1497
+ if (sourceModel === targetModel) return true;
1498
+ return (
1499
+ targetModel === 'claude-fable-5-1' && CLAUDE_CODE_MODEL_POLICIES[sourceModel] !== undefined
1500
+ );
1501
+ }
1502
+
1503
+ interface SignatureEpochPolicy {
1504
+ readonly sha256: string;
1505
+ readonly inheritsPrior: boolean;
1506
+ }
1507
+
1508
+ function initialSignatureEpochPolicy(
1509
+ targetModelId: string,
1510
+ cacheRetention: CacheRetention,
1511
+ compactionBoundarySha256: string | null,
1512
+ ): SignatureEpochPolicy {
1513
+ return {
1514
+ sha256: sha256Canonical({
1515
+ projection_version: ANTHROPIC_PROJECTION_VERSION,
1516
+ kind: 'initial',
1517
+ target_model: targetModelId,
1518
+ cache_retention: cacheRetention,
1519
+ compaction_boundary_sha256: compactionBoundarySha256,
1520
+ }),
1521
+ inheritsPrior: compactionBoundarySha256 === null,
1522
+ };
1523
+ }
1524
+
1525
+ function resolveSignatureEpochPolicy(
1526
+ model: PiModelLike,
1527
+ messages: readonly PiMessage[],
1528
+ cacheRetention: CacheRetention,
1529
+ compactionBoundarySha256: string | null,
1530
+ ): SignatureEpochPolicy {
1531
+ const targetModelId = normalizedAnthropicModelId(model);
1532
+ const latest = latestLineageForTarget({ messages }, targetModelId);
1533
+ if (latest === undefined) {
1534
+ return initialSignatureEpochPolicy(targetModelId, cacheRetention, compactionBoundarySha256);
1535
+ }
1536
+
1537
+ const retentionChanged = latest.cache_retention !== cacheRetention;
1538
+ const declaredNewCompaction =
1539
+ compactionBoundarySha256 !== null &&
1540
+ compactionBoundarySha256 !== latest.compaction_boundary_sha256;
1541
+ if (retentionChanged || declaredNewCompaction) {
1542
+ return {
1543
+ sha256: sha256Canonical({
1544
+ projection_version: ANTHROPIC_PROJECTION_VERSION,
1545
+ kind: 'transition',
1546
+ previous_signature_epoch_sha256: latest.signature_epoch_sha256,
1547
+ target_model: targetModelId,
1548
+ cache_retention: cacheRetention,
1549
+ compaction_boundary_sha256: compactionBoundarySha256,
1550
+ reason: retentionChanged ? 'cache-retention' : 'compaction',
1551
+ }),
1552
+ inheritsPrior: false,
1553
+ };
1554
+ }
1555
+ return {
1556
+ sha256: latest.signature_epoch_sha256,
1557
+ inheritsPrior: latest.signature_epoch_inherits_prior,
1558
+ };
1559
+ }
1560
+
1561
+ function isTrustedReplayableAnthropicAssistant(
1562
+ message: Extract<PiMessage, { role: 'assistant' }>,
1563
+ targetModel: PiModelLike,
1564
+ targetCacheRetention: CacheRetention,
1565
+ conversationStaticSha256: string,
1566
+ wireMessagesBeforeAssistant: readonly JsonObject[],
1567
+ ): boolean {
1568
+ if (
1569
+ message.provider !== 'anthropic' ||
1570
+ message.api !== 'anthropic-messages' ||
1571
+ typeof message.model !== 'string' ||
1572
+ message.stopReason === 'error' ||
1573
+ message.stopReason === 'aborted' ||
1574
+ !canTargetReadAnthropicThinking(message.model, normalizedAnthropicModelId(targetModel))
1575
+ ) {
1576
+ return false;
1577
+ }
1578
+ const lineage = parseAnthropicLineageDetails(message);
1579
+ if (
1580
+ lineage === undefined ||
1581
+ lineage.source_model !== message.model ||
1582
+ lineage.response_id !== message.responseId ||
1583
+ lineage.assistant_content_sha256 !== assistantContentHash(message.content) ||
1584
+ lineage.cache_retention !== targetCacheRetention ||
1585
+ lineage.conversation_static_sha256 !== conversationStaticSha256 ||
1586
+ lineage.request_message_count !== wireMessagesBeforeAssistant.length ||
1587
+ lineage.request_messages_sha256 !== promptMessagesHash(wireMessagesBeforeAssistant)
1588
+ ) {
1589
+ return false;
1590
+ }
1591
+ return true;
1592
+ }
1593
+
1594
+ function normalizeAnthropicToolCallId(id: string): string {
1595
+ if (/^[a-zA-Z0-9_-]{1,64}$/u.test(id)) return id;
1596
+ const safe = id.replace(/[^a-zA-Z0-9_-]/gu, '_');
1597
+ const digest = createHash('sha256').update(id, 'utf8').digest('hex').slice(0, 12);
1598
+ const prefix = safe.slice(0, 64 - digest.length - 1) || 'tool';
1599
+ return `${prefix}_${digest}`.slice(0, 64);
1600
+ }
1601
+
1602
+ function projectedUserContent(content: string | readonly PiContentBlock[]): JsonObject[] {
1603
+ if (typeof content === 'string') {
1604
+ return content.trim().length > 0 ? [{ type: 'text', text: sanitizeSurrogates(content) }] : [];
1605
+ }
1606
+ const blocks: JsonObject[] = [];
1607
+ for (const block of content) {
1608
+ if (block.type === 'text') {
1609
+ if (block.text.trim().length > 0)
1610
+ blocks.push({ type: 'text', text: sanitizeSurrogates(block.text) });
1611
+ continue;
1612
+ }
1613
+ if (block.type === 'image') {
1614
+ blocks.push({
1615
+ type: 'image',
1616
+ source: { type: 'base64', media_type: block.mimeType, data: block.data },
1617
+ });
1618
+ continue;
1619
+ }
1620
+ throw new Error('Anthropic attribution encountered an unsupported user content block');
1621
+ }
1622
+ return blocks;
1623
+ }
1624
+
1625
+ function compactionBoundarySha256FromPiMessages(messages: readonly PiMessage[]): string | null {
1626
+ const first = messages[0];
1627
+ if (first?.role !== 'user') return null;
1628
+ const content = projectedUserContent(first.content);
1629
+ return declaredCompactionBoundarySha256([{ role: 'user', content }]);
1630
+ }
1631
+
1176
1632
  function convertMessages(
1633
+ model: PiModelLike,
1177
1634
  messages: readonly PiMessage[],
1635
+ conversationStaticSha256: string,
1636
+ cacheRetention: CacheRetention,
1637
+ signatureEpoch: SignatureEpochPolicy,
1178
1638
  cacheControl?: AnthropicCacheControl,
1179
1639
  ): JsonObject[] {
1180
1640
  const params: JsonObject[] = [];
1641
+ const toolCallIdMap = new Map<string, string>();
1642
+ const normalizedToolCallOwners = new Map<string, string>();
1643
+ let pendingToolCalls: Array<{ readonly id: string; readonly name: string }> = [];
1644
+ let completedPendingToolCallIds = new Set<string>();
1645
+
1646
+ const normalizeToolCallId = (id: string): string => {
1647
+ const existing = toolCallIdMap.get(id);
1648
+ if (existing !== undefined) return existing;
1649
+ const normalized = normalizeAnthropicToolCallId(id);
1650
+ const owner = normalizedToolCallOwners.get(normalized);
1651
+ if (owner !== undefined && owner !== id) {
1652
+ throw new Error('Anthropic attribution tool-call ID normalization collision');
1653
+ }
1654
+ normalizedToolCallOwners.set(normalized, id);
1655
+ toolCallIdMap.set(id, normalized);
1656
+ return normalized;
1657
+ };
1658
+
1659
+ const flushMissingToolResults = (): void => {
1660
+ const missing = pendingToolCalls.filter((call) => !completedPendingToolCallIds.has(call.id));
1661
+ if (missing.length > 0) {
1662
+ params.push({
1663
+ role: 'user',
1664
+ content: missing.map((call) => ({
1665
+ type: 'tool_result',
1666
+ tool_use_id: call.id,
1667
+ content: 'No result provided',
1668
+ is_error: true,
1669
+ })),
1670
+ });
1671
+ }
1672
+ pendingToolCalls = [];
1673
+ completedPendingToolCallIds = new Set<string>();
1674
+ };
1675
+
1181
1676
  for (let index = 0; index < messages.length; index += 1) {
1182
1677
  const message = messages[index];
1183
- if (message === undefined) {
1184
- throw new TypeError(`Anthropic message ${index} is missing`);
1185
- }
1678
+ if (message === undefined) throw new TypeError(`Anthropic message ${index} is missing`);
1679
+
1186
1680
  if (message.role === 'user') {
1187
- if (typeof message.content === 'string') {
1188
- if (message.content.trim().length > 0)
1189
- params.push({ role: 'user', content: sanitizeSurrogates(message.content) });
1190
- } else {
1191
- const content = message.content
1192
- .map((block) =>
1193
- block.type === 'text'
1194
- ? { type: 'text', text: sanitizeSurrogates(block.text) }
1195
- : {
1196
- type: 'image',
1197
- source: { type: 'base64', media_type: block.mimeType, data: block.data },
1198
- },
1199
- )
1200
- .filter((block) => block.type !== 'text' || String(block.text).trim().length > 0);
1201
- if (content.length > 0) params.push({ role: 'user', content });
1202
- }
1203
- } else if (message.role === 'assistant') {
1681
+ flushMissingToolResults();
1682
+ const content = projectedUserContent(message.content);
1683
+ if (content.length > 0) params.push({ role: 'user', content });
1684
+ continue;
1685
+ }
1686
+
1687
+ if (message.role === 'assistant') {
1688
+ flushMissingToolResults();
1689
+ if (message.stopReason === 'error' || message.stopReason === 'aborted') continue;
1690
+ const lineage = parseAnthropicLineageDetails(message);
1691
+ const epochAllowsReplay =
1692
+ signatureEpoch.inheritsPrior || lineage?.signature_epoch_sha256 === signatureEpoch.sha256;
1693
+ const preserveThinking =
1694
+ epochAllowsReplay &&
1695
+ isTrustedReplayableAnthropicAssistant(
1696
+ message,
1697
+ model,
1698
+ cacheRetention,
1699
+ conversationStaticSha256,
1700
+ params,
1701
+ );
1204
1702
  const content: JsonObject[] = [];
1205
1703
  for (const block of message.content) {
1206
- if (
1207
- block['type'] === 'text' &&
1208
- typeof block['text'] === 'string' &&
1209
- block['text'].trim().length > 0
1210
- ) {
1211
- content.push({ type: 'text', text: sanitizeSurrogates(block['text']) });
1212
- } else if (
1213
- block['type'] === 'thinking' &&
1214
- typeof block['thinking'] === 'string' &&
1215
- block['thinking'].trim().length > 0
1216
- ) {
1704
+ if (block['type'] === 'text') {
1705
+ if (typeof block['text'] !== 'string')
1706
+ throw new Error('Anthropic attribution encountered malformed assistant text');
1707
+ if (block['text'].trim().length > 0)
1708
+ content.push({ type: 'text', text: sanitizeSurrogates(block['text']) });
1709
+ continue;
1710
+ }
1711
+ if (block['type'] === 'thinking') {
1712
+ if (typeof block['thinking'] !== 'string')
1713
+ throw new Error('Anthropic attribution encountered malformed assistant thinking');
1217
1714
  const signature =
1218
1715
  typeof block['thinkingSignature'] === 'string' ? block['thinkingSignature'] : '';
1219
- content.push(
1220
- signature.length > 0
1221
- ? { type: 'thinking', thinking: sanitizeSurrogates(block['thinking']), signature }
1222
- : { type: 'text', text: sanitizeSurrogates(block['thinking']) },
1223
- );
1224
- } else if (
1225
- block['type'] === 'toolCall' &&
1226
- typeof block['id'] === 'string' &&
1227
- typeof block['name'] === 'string'
1228
- ) {
1716
+ if (preserveThinking && signature.trim().length > 0) {
1717
+ content.push(
1718
+ block['redacted'] === true
1719
+ ? { type: 'redacted_thinking', data: signature }
1720
+ : {
1721
+ type: 'thinking',
1722
+ // A signature authenticates the exact provider-returned reasoning bytes.
1723
+ // Provenance and lineage checks above make this raw replay safe; changing
1724
+ // even valid non-BMP Unicode here would invalidate the opaque signature.
1725
+ thinking: block['thinking'],
1726
+ signature,
1727
+ },
1728
+ );
1729
+ } else if (block['redacted'] !== true && block['thinking'].trim().length > 0) {
1730
+ content.push({ type: 'text', text: sanitizeSurrogates(block['thinking']) });
1731
+ }
1732
+ continue;
1733
+ }
1734
+ if (block['type'] === 'toolCall') {
1735
+ if (typeof block['id'] !== 'string' || typeof block['name'] !== 'string')
1736
+ throw new Error('Anthropic attribution encountered malformed assistant tool call');
1737
+ const id = normalizeToolCallId(block['id']);
1229
1738
  content.push({
1230
1739
  type: 'tool_use',
1231
- id: block['id'],
1740
+ id,
1232
1741
  name: block['name'],
1233
- input: block['arguments'] ?? {},
1742
+ input: canonicalizeJson(block['arguments'] ?? {}),
1234
1743
  });
1744
+ continue;
1235
1745
  }
1746
+ throw new Error(
1747
+ `Anthropic attribution encountered unsupported assistant block type ${JSON.stringify(block['type'])}`,
1748
+ );
1236
1749
  }
1237
- if (content.length > 0) params.push({ role: 'assistant', content });
1238
- } else if (message.role === 'toolResult') {
1239
- const toolResults: JsonObject[] = [
1240
- {
1241
- type: 'tool_result',
1242
- tool_use_id: message.toolCallId,
1243
- content: convertContentBlocks(message.content),
1244
- is_error: message.isError === true,
1245
- },
1246
- ];
1247
- let lookahead = index + 1;
1248
- while (lookahead < messages.length && messages[lookahead]?.role === 'toolResult') {
1249
- const next = messages[lookahead] as Extract<PiMessage, { role: 'toolResult' }>;
1250
- toolResults.push({
1251
- type: 'tool_result',
1252
- tool_use_id: next.toolCallId,
1253
- content: convertContentBlocks(next.content),
1254
- is_error: next.isError === true,
1255
- });
1256
- lookahead += 1;
1750
+ if (content.length > 0) {
1751
+ params.push({ role: 'assistant', content });
1752
+ pendingToolCalls = content
1753
+ .filter((block) => block['type'] === 'tool_use')
1754
+ .map((block) => ({ id: String(block['id']), name: String(block['name']) }));
1257
1755
  }
1258
- index = lookahead - 1;
1259
- params.push({ role: 'user', content: toolResults });
1756
+ continue;
1757
+ }
1758
+
1759
+ const toolResults: JsonObject[] = [];
1760
+ let lookahead = index;
1761
+ while (lookahead < messages.length && messages[lookahead]?.role === 'toolResult') {
1762
+ const result = messages[lookahead] as Extract<PiMessage, { role: 'toolResult' }>;
1763
+ const toolUseId = normalizeToolCallId(result.toolCallId);
1764
+ completedPendingToolCallIds.add(toolUseId);
1765
+ toolResults.push({
1766
+ type: 'tool_result',
1767
+ tool_use_id: toolUseId,
1768
+ content: convertContentBlocks(result.content),
1769
+ is_error: result.isError === true,
1770
+ });
1771
+ lookahead += 1;
1260
1772
  }
1773
+ index = lookahead - 1;
1774
+ if (toolResults.length > 0) params.push({ role: 'user', content: toolResults });
1261
1775
  }
1776
+ flushMissingToolResults();
1262
1777
  return markLastConversationCacheSurface(params, cacheControl);
1263
1778
  }
1264
1779
 
@@ -1274,8 +1789,10 @@ function convertTools(
1274
1789
  description: tool.description ?? '',
1275
1790
  input_schema: {
1276
1791
  type: 'object',
1277
- properties: isPlainObject(parameters['properties']) ? parameters['properties'] : {},
1278
- required: Array.isArray(parameters['required']) ? parameters['required'] : [],
1792
+ properties: canonicalizeJson(
1793
+ isPlainObject(parameters['properties']) ? parameters['properties'] : {},
1794
+ ),
1795
+ required: Array.isArray(parameters['required']) ? [...parameters['required']] : [],
1279
1796
  },
1280
1797
  };
1281
1798
  return cacheControl !== undefined && index === tools.length - 1
@@ -1317,6 +1834,25 @@ function adaptiveEffortFor(
1317
1834
  }
1318
1835
  }
1319
1836
 
1837
+ function conversationStaticHash(system: readonly unknown[], tools: readonly JsonObject[]): string {
1838
+ const normalizedSystem = system
1839
+ .filter(
1840
+ (block) =>
1841
+ !isPlainObject(block) ||
1842
+ typeof block['text'] !== 'string' ||
1843
+ !isClaudeCodeIdentityText(block['text']),
1844
+ )
1845
+ .map(blockWithoutCacheControl);
1846
+ return sha256Canonical({
1847
+ attribution_profile: {
1848
+ claude_code_version: CLAUDE_CODE_VERSION,
1849
+ entrypoint: CLAUDE_CODE_ENTRYPOINT,
1850
+ },
1851
+ system: normalizedSystem,
1852
+ tools: tools.map(blockWithoutCacheControl),
1853
+ });
1854
+ }
1855
+
1320
1856
  export function buildAnthropicRequestParams(
1321
1857
  model: PiModelLike,
1322
1858
  context: PiStreamContext,
@@ -1325,37 +1861,63 @@ export function buildAnthropicRequestParams(
1325
1861
  const policy = resolveClaudeCodeModelPolicy(model);
1326
1862
  const maxTokens = resolveAnthropicMaxTokens(model);
1327
1863
  const cacheControl = resolveAnthropicCacheControl(model, options);
1864
+ const cacheRetention: CacheRetention =
1865
+ cacheControl === undefined ? 'none' : cacheControl.ttl === '1h' ? 'long' : 'short';
1866
+ const signatureEpoch = resolveSignatureEpochPolicy(
1867
+ model,
1868
+ context.messages,
1869
+ cacheRetention,
1870
+ compactionBoundarySha256FromPiMessages(context.messages),
1871
+ );
1872
+ const system =
1873
+ context.systemPrompt && context.systemPrompt.trim().length > 0
1874
+ ? markSystemCacheSurface(
1875
+ [
1876
+ {
1877
+ type: 'text',
1878
+ text: sanitizeSurrogates(stripAnthropicSystemPromptBadLines(context.systemPrompt)),
1879
+ },
1880
+ ],
1881
+ cacheControl,
1882
+ )
1883
+ : [];
1884
+ const tools = convertTools(
1885
+ context.tools,
1886
+ model.compat?.supportsCacheControlOnTools === false ? undefined : cacheControl,
1887
+ );
1888
+ const staticSha256 = conversationStaticHash(system, tools);
1328
1889
  const params: JsonObject = {
1329
1890
  model: policy.modelId,
1330
- messages: convertMessages(context.messages, cacheControl),
1891
+ messages: convertMessages(
1892
+ model,
1893
+ context.messages,
1894
+ staticSha256,
1895
+ cacheRetention,
1896
+ signatureEpoch,
1897
+ cacheControl,
1898
+ ),
1331
1899
  max_tokens: maxTokens,
1332
1900
  stream: true,
1901
+ tools,
1333
1902
  };
1334
- if (context.systemPrompt && context.systemPrompt.trim().length > 0) {
1335
- params['system'] = markSystemCacheSurface(
1336
- [
1337
- {
1338
- type: 'text',
1339
- text: sanitizeSurrogates(stripAnthropicSystemPromptBadLines(context.systemPrompt)),
1340
- },
1341
- ],
1342
- cacheControl,
1343
- );
1344
- }
1345
- const tools = convertTools(
1346
- context.tools,
1347
- model.compat?.supportsCacheControlOnTools === false ? undefined : cacheControl,
1348
- );
1349
- if (tools.length > 0) params['tools'] = tools;
1350
- else params['tools'] = [];
1351
- if (options?.toolChoice !== undefined) params['tool_choice'] = options.toolChoice;
1903
+ if (system.length > 0) params['system'] = system;
1904
+ if (options?.toolChoice !== undefined)
1905
+ params['tool_choice'] = canonicalizeJson(options.toolChoice);
1352
1906
  const reasoning = options?.reasoning;
1353
1907
  if (model.reasoning && reasoning !== undefined) {
1354
1908
  if (reasoning === 'off') {
1909
+ if (policy.enforcesThinkingPrefixBinding) {
1910
+ throw new Error('Anthropic attribution cannot disable thinking for Claude Fable 5.1');
1911
+ }
1355
1912
  params['thinking'] = { type: 'disabled' };
1356
1913
  params['temperature'] = options?.temperature ?? 1;
1357
1914
  } else if (policy.thinkingPolicy === 'adaptive-effort') {
1358
- params['thinking'] = { type: 'adaptive' };
1915
+ params['thinking'] = {
1916
+ type: 'adaptive',
1917
+ ...(policy.enforcesThinkingPrefixBinding
1918
+ ? { block_binding: { prefix_mismatch_behavior: 'error' } }
1919
+ : {}),
1920
+ };
1359
1921
  params['output_config'] = { effort: adaptiveEffortFor(reasoning) };
1360
1922
  } else {
1361
1923
  params['thinking'] = {
@@ -1363,6 +1925,11 @@ export function buildAnthropicRequestParams(
1363
1925
  budget_tokens: thinkingBudgetFor(reasoning, maxTokens, options?.thinkingBudgets),
1364
1926
  };
1365
1927
  }
1928
+ } else if (policy.enforcesThinkingPrefixBinding) {
1929
+ params['thinking'] = {
1930
+ type: 'adaptive',
1931
+ block_binding: { prefix_mismatch_behavior: 'error' },
1932
+ };
1366
1933
  } else {
1367
1934
  params['thinking'] = { type: 'disabled' };
1368
1935
  params['temperature'] = options?.temperature ?? 1;
@@ -1371,6 +1938,280 @@ export function buildAnthropicRequestParams(
1371
1938
  return params;
1372
1939
  }
1373
1940
 
1941
+ interface PreparedAnthropicLineage {
1942
+ readonly key: string;
1943
+ readonly details: Omit<AnthropicLineageDetails, 'response_id' | 'assistant_content_sha256'>;
1944
+ }
1945
+
1946
+ function requestMessagesFromPayload(payload: JsonObject): JsonObject[] {
1947
+ const messages = payload['messages'];
1948
+ if (!Array.isArray(messages) || messages.some((message) => !isPlainObject(message))) {
1949
+ throw new Error('Anthropic attribution final payload.messages must be an object array');
1950
+ }
1951
+ for (const message of messages as JsonObject[]) {
1952
+ if (message['role'] !== 'user' && message['role'] !== 'assistant') {
1953
+ throw new Error('Anthropic attribution final payload message has an unsupported role');
1954
+ }
1955
+ const content = message['content'];
1956
+ if (!Array.isArray(content) || content.some((block) => !isPlainObject(block))) {
1957
+ throw new Error(
1958
+ 'Anthropic attribution final payload message content must use canonical block arrays',
1959
+ );
1960
+ }
1961
+ }
1962
+ return messages as JsonObject[];
1963
+ }
1964
+
1965
+ function systemBlocksFromPayload(payload: JsonObject): unknown[] {
1966
+ const system = payload['system'];
1967
+ if (system === undefined) return [];
1968
+ if (typeof system === 'string') return [{ type: 'text', text: system }];
1969
+ if (!Array.isArray(system))
1970
+ throw new Error('Anthropic attribution final payload.system must be a string or block array');
1971
+ return system;
1972
+ }
1973
+
1974
+ function toolsFromPayload(payload: JsonObject): JsonObject[] {
1975
+ const tools = payload['tools'];
1976
+ if (tools === undefined) return [];
1977
+ if (!Array.isArray(tools) || tools.some((tool) => !isPlainObject(tool))) {
1978
+ throw new Error('Anthropic attribution final payload.tools must be an object array');
1979
+ }
1980
+ return tools as JsonObject[];
1981
+ }
1982
+
1983
+ function cacheProfileHash(
1984
+ model: PiModelLike,
1985
+ policy: ClaudeCodeModelPolicy,
1986
+ payload: JsonObject,
1987
+ staticSha256: string,
1988
+ ): string {
1989
+ return sha256Canonical({
1990
+ projection_version: ANTHROPIC_PROJECTION_VERSION,
1991
+ model: normalizedAnthropicModelId(model),
1992
+ beta: policy.beta,
1993
+ conversation_static_sha256: staticSha256,
1994
+ thinking: payload['thinking'],
1995
+ output_config: payload['output_config'],
1996
+ tool_choice: payload['tool_choice'],
1997
+ });
1998
+ }
1999
+
2000
+ function cacheRetentionFromPayload(payload: JsonObject): CacheRetention {
2001
+ return inspectCacheControls(payload).retention ?? 'none';
2002
+ }
2003
+
2004
+ function latestLineageForTarget(
2005
+ context: PiStreamContext,
2006
+ targetModelId: string,
2007
+ ): AnthropicLineageDetails | undefined {
2008
+ for (let index = context.messages.length - 1; index >= 0; index -= 1) {
2009
+ const message = context.messages[index];
2010
+ if (
2011
+ message?.role !== 'assistant' ||
2012
+ message.model !== targetModelId ||
2013
+ message.stopReason === 'error' ||
2014
+ message.stopReason === 'aborted'
2015
+ ) {
2016
+ continue;
2017
+ }
2018
+ const lineage = parseAnthropicLineageDetails(message);
2019
+ return lineage?.source_model === targetModelId ? lineage : undefined;
2020
+ }
2021
+ return undefined;
2022
+ }
2023
+
2024
+ function declaredCompactionBoundarySha256(messages: readonly JsonObject[]): string | null {
2025
+ const firstMessage = messages[0];
2026
+ if (firstMessage?.['role'] !== 'user' || !Array.isArray(firstMessage['content'])) return null;
2027
+ const declaresCompaction = firstMessage['content'].some(
2028
+ (block) =>
2029
+ isPlainObject(block) &&
2030
+ block['type'] === 'text' &&
2031
+ typeof block['text'] === 'string' &&
2032
+ block['text'].startsWith(COMPACTION_SUMMARY_PREFIX),
2033
+ );
2034
+ if (!declaresCompaction) return null;
2035
+ return sha256Canonical(promptMessagesWithoutCacheControls([firstMessage])[0]);
2036
+ }
2037
+
2038
+ function prepareAnthropicLineageDetails(args: {
2039
+ readonly model: PiModelLike;
2040
+ readonly policy: ClaudeCodeModelPolicy;
2041
+ readonly context: PiStreamContext;
2042
+ readonly payload: JsonObject;
2043
+ }): PreparedAnthropicLineage['details'] {
2044
+ const targetModelId = normalizedAnthropicModelId(args.model);
2045
+ const messages = requestMessagesFromPayload(args.payload);
2046
+ const staticSha256 = conversationStaticHash(
2047
+ systemBlocksFromPayload(args.payload),
2048
+ toolsFromPayload(args.payload),
2049
+ );
2050
+ const profileSha256 = cacheProfileHash(args.model, args.policy, args.payload, staticSha256);
2051
+ const cacheRetention = cacheRetentionFromPayload(args.payload);
2052
+ const compactionBoundarySha256 = declaredCompactionBoundarySha256(messages);
2053
+ const signatureEpoch = resolveSignatureEpochPolicy(
2054
+ args.model,
2055
+ args.context.messages,
2056
+ cacheRetention,
2057
+ compactionBoundarySha256,
2058
+ );
2059
+ let previous = latestLineageForTarget(args.context, targetModelId);
2060
+ if (previous !== undefined) {
2061
+ const prefixStillExists =
2062
+ messages.length >= previous.request_message_count &&
2063
+ promptMessagesHash(messages.slice(0, previous.request_message_count)) ===
2064
+ previous.request_messages_sha256;
2065
+ if (
2066
+ !prefixStillExists &&
2067
+ compactionBoundarySha256 !== null &&
2068
+ compactionBoundarySha256 !== previous.compaction_boundary_sha256
2069
+ ) {
2070
+ previous = undefined;
2071
+ } else {
2072
+ if (!prefixStillExists) {
2073
+ throw new Error(
2074
+ 'Anthropic cache lineage diverged before transport: message history is not append-only',
2075
+ );
2076
+ }
2077
+ if (previous.cache_profile_sha256 !== profileSha256) {
2078
+ throw new Error(
2079
+ 'Anthropic cache lineage diverged before transport: model/system/tools/thinking/beta profile changed',
2080
+ );
2081
+ }
2082
+ if (previous.cache_retention !== cacheRetention) previous = undefined;
2083
+ }
2084
+ }
2085
+ return {
2086
+ schema_version: ANTHROPIC_LINEAGE_SCHEMA,
2087
+ projection_version: ANTHROPIC_PROJECTION_VERSION,
2088
+ source_provider: 'anthropic',
2089
+ source_api: 'anthropic-messages',
2090
+ source_model: targetModelId,
2091
+ conversation_static_sha256: staticSha256,
2092
+ request_message_count: messages.length,
2093
+ request_messages_sha256: promptMessagesHash(messages),
2094
+ cache_profile_sha256: profileSha256,
2095
+ cache_retention: cacheRetention,
2096
+ compaction_boundary_sha256: compactionBoundarySha256,
2097
+ signature_epoch_sha256: signatureEpoch.sha256,
2098
+ signature_epoch_inherits_prior: signatureEpoch.inheritsPrior,
2099
+ previous_message_id: previous?.response_id ?? null,
2100
+ };
2101
+ }
2102
+
2103
+ class AnthropicLineageCoordinator {
2104
+ private readonly inFlight = new Set<string>();
2105
+
2106
+ prepare(args: {
2107
+ readonly sessionId: string;
2108
+ readonly model: PiModelLike;
2109
+ readonly policy: ClaudeCodeModelPolicy;
2110
+ readonly context: PiStreamContext;
2111
+ readonly payload: JsonObject;
2112
+ }): PreparedAnthropicLineage {
2113
+ const targetModelId = normalizedAnthropicModelId(args.model);
2114
+ const key = `${args.sessionId}\u0000${targetModelId}`;
2115
+ if (this.inFlight.has(key)) {
2116
+ throw new Error(
2117
+ `Anthropic cache lineage already has an in-flight request for ${targetModelId}; concurrent continuations must fork`,
2118
+ );
2119
+ }
2120
+ const details = prepareAnthropicLineageDetails(args);
2121
+ this.inFlight.add(key);
2122
+ return { key, details };
2123
+ }
2124
+
2125
+ release(prepared: PreparedAnthropicLineage): void {
2126
+ this.inFlight.delete(prepared.key);
2127
+ }
2128
+ }
2129
+
2130
+ const anthropicLineageCoordinator = new AnthropicLineageCoordinator();
2131
+
2132
+ export function createAnthropicLineageDiagnostic(args: {
2133
+ readonly model: PiModelLike;
2134
+ readonly responseId: string;
2135
+ readonly assistantContent: readonly JsonObject[];
2136
+ readonly requestPayload: JsonObject;
2137
+ readonly previousMessageId?: string | null;
2138
+ }): PiAssistantDiagnosticLike {
2139
+ const policy = resolveClaudeCodeModelPolicy(args.model);
2140
+ const messages = requestMessagesFromPayload(args.requestPayload);
2141
+ const staticSha256 = conversationStaticHash(
2142
+ systemBlocksFromPayload(args.requestPayload),
2143
+ toolsFromPayload(args.requestPayload),
2144
+ );
2145
+ const cacheRetention = cacheRetentionFromPayload(args.requestPayload);
2146
+ const compactionBoundarySha256 = declaredCompactionBoundarySha256(messages);
2147
+ const signatureEpoch = initialSignatureEpochPolicy(
2148
+ normalizedAnthropicModelId(args.model),
2149
+ cacheRetention,
2150
+ compactionBoundarySha256,
2151
+ );
2152
+ return {
2153
+ type: ANTHROPIC_LINEAGE_DIAGNOSTIC_TYPE,
2154
+ timestamp: Date.now(),
2155
+ details: {
2156
+ schema_version: ANTHROPIC_LINEAGE_SCHEMA,
2157
+ projection_version: ANTHROPIC_PROJECTION_VERSION,
2158
+ source_provider: 'anthropic',
2159
+ source_api: 'anthropic-messages',
2160
+ source_model: normalizedAnthropicModelId(args.model),
2161
+ response_id: args.responseId,
2162
+ assistant_content_sha256: assistantContentHash(args.assistantContent),
2163
+ conversation_static_sha256: staticSha256,
2164
+ request_message_count: messages.length,
2165
+ request_messages_sha256: promptMessagesHash(messages),
2166
+ cache_profile_sha256: cacheProfileHash(args.model, policy, args.requestPayload, staticSha256),
2167
+ cache_retention: cacheRetention,
2168
+ compaction_boundary_sha256: compactionBoundarySha256,
2169
+ signature_epoch_sha256: signatureEpoch.sha256,
2170
+ signature_epoch_inherits_prior: signatureEpoch.inheritsPrior,
2171
+ previous_message_id: args.previousMessageId ?? null,
2172
+ },
2173
+ };
2174
+ }
2175
+
2176
+ function appendLineageDiagnostic(
2177
+ output: AssistantMessageLike,
2178
+ prepared: PreparedAnthropicLineage,
2179
+ ): void {
2180
+ if (typeof output.responseId !== 'string' || output.responseId.length === 0) {
2181
+ throw new Error(
2182
+ 'Anthropic attribution successful response is missing responseId lineage proof',
2183
+ );
2184
+ }
2185
+ output.diagnostics ??= [];
2186
+ output.diagnostics.push({
2187
+ type: ANTHROPIC_LINEAGE_DIAGNOSTIC_TYPE,
2188
+ timestamp: Date.now(),
2189
+ details: {
2190
+ ...prepared.details,
2191
+ response_id: output.responseId,
2192
+ assistant_content_sha256: assistantContentHash(output.content),
2193
+ },
2194
+ });
2195
+ }
2196
+
2197
+ function appendProviderCacheDiagnostic(
2198
+ output: AssistantMessageLike,
2199
+ messageStart: JsonObject,
2200
+ ): void {
2201
+ const diagnostics = messageStart['diagnostics'];
2202
+ const inputTransformations = messageStart['input_transformations'];
2203
+ if (diagnostics === undefined && inputTransformations === undefined) return;
2204
+ output.diagnostics ??= [];
2205
+ output.diagnostics.push({
2206
+ type: 'anthropic-provider-cache',
2207
+ timestamp: Date.now(),
2208
+ details: {
2209
+ cache_diagnostics: canonicalizeJson(diagnostics ?? null),
2210
+ input_transformations: canonicalizeJson(inputTransformations ?? []),
2211
+ },
2212
+ });
2213
+ }
2214
+
1374
2215
  function headersToRecord(headers: Headers): Record<string, string> {
1375
2216
  return Object.fromEntries([...headers.entries()]);
1376
2217
  }
@@ -1381,6 +2222,35 @@ function lowerHeaderMap(headers: Record<string, string> | undefined): Record<str
1381
2222
  return output;
1382
2223
  }
1383
2224
 
2225
+ function resolveAnthropicBetaMessagesUrl(model: PiModelLike): string {
2226
+ const configured =
2227
+ typeof model.baseUrl === 'string' && model.baseUrl.trim().length > 0
2228
+ ? model.baseUrl.trim()
2229
+ : ANTHROPIC_OFFICIAL_ORIGIN;
2230
+ let parsed: URL;
2231
+ try {
2232
+ parsed = new URL(configured);
2233
+ } catch (error) {
2234
+ throw new Error(
2235
+ `Anthropic attribution requires the official Anthropic HTTPS endpoint; invalid model.baseUrl: ${error instanceof Error ? error.message : String(error)}`,
2236
+ );
2237
+ }
2238
+ if (
2239
+ parsed.origin !== ANTHROPIC_OFFICIAL_ORIGIN ||
2240
+ parsed.protocol !== 'https:' ||
2241
+ parsed.username.length > 0 ||
2242
+ parsed.password.length > 0 ||
2243
+ (parsed.pathname !== '' && parsed.pathname !== '/') ||
2244
+ parsed.search.length > 0 ||
2245
+ parsed.hash.length > 0
2246
+ ) {
2247
+ throw new Error(
2248
+ `Anthropic attribution requires the official Anthropic HTTPS endpoint ${ANTHROPIC_OFFICIAL_ORIGIN}; refusing model.baseUrl ${JSON.stringify(configured)}`,
2249
+ );
2250
+ }
2251
+ return ANTHROPIC_BETA_MESSAGES_URL;
2252
+ }
2253
+
1384
2254
  function buildFetchHeaders(
1385
2255
  options: PiSimpleStreamOptions | undefined,
1386
2256
  apiKey: string,
@@ -1424,6 +2294,23 @@ function parseStreamingJsonFragment(text: string): unknown {
1424
2294
  }
1425
2295
  }
1426
2296
 
2297
+ function parseCompletedToolInput(text: string): JsonObject {
2298
+ const parsed = parseJsonValue(text, 'Anthropic streamed tool input');
2299
+ if (!isPlainObject(parsed)) {
2300
+ throw new Error('Anthropic streamed tool input must complete as a JSON object');
2301
+ }
2302
+ return parsed;
2303
+ }
2304
+
2305
+ function anthropicStreamErrorMessage(event: JsonObject): string {
2306
+ const error = event['error'];
2307
+ if (isPlainObject(error) && typeof error['message'] === 'string') {
2308
+ const type = typeof error['type'] === 'string' ? `${error['type']}: ` : '';
2309
+ return `Anthropic beta messages stream error: ${type}${error['message']}`;
2310
+ }
2311
+ return 'Anthropic beta messages stream emitted an error event';
2312
+ }
2313
+
1427
2314
  function validCostRate(value: unknown, fallback: number, field: string): number {
1428
2315
  if (value === undefined) return fallback;
1429
2316
  if (typeof value !== 'number' || !Number.isFinite(value) || value < 0) {
@@ -1523,12 +2410,25 @@ async function* iterateSseEvents(
1523
2410
  let eventName = '';
1524
2411
  let dataLines: string[] = [];
1525
2412
  function flush(): JsonObject | undefined {
1526
- if (dataLines.length === 0) return undefined;
2413
+ if (dataLines.length === 0) {
2414
+ eventName = '';
2415
+ return undefined;
2416
+ }
1527
2417
  const data = dataLines.join('\n');
2418
+ const declaredEventName = eventName;
1528
2419
  eventName = '';
1529
2420
  dataLines = [];
1530
2421
  if (data === '[DONE]') return undefined;
1531
- return parseJsonObject(data, 'Anthropic beta messages SSE event');
2422
+ const event = parseJsonObject(data, 'Anthropic beta messages SSE event');
2423
+ if (
2424
+ declaredEventName.length > 0 &&
2425
+ (typeof event['type'] !== 'string' || event['type'] !== declaredEventName)
2426
+ ) {
2427
+ throw new Error(
2428
+ `Anthropic beta messages SSE event name ${JSON.stringify(declaredEventName)} did not match data.type`,
2429
+ );
2430
+ }
2431
+ return event;
1532
2432
  }
1533
2433
  function consumeLine(line: string): JsonObject | undefined {
1534
2434
  if (line.length === 0) return flush();
@@ -1539,7 +2439,6 @@ async function* iterateSseEvents(
1539
2439
  if (value.startsWith(' ')) value = value.slice(1);
1540
2440
  if (field === 'event') eventName = value;
1541
2441
  if (field === 'data') dataLines.push(value);
1542
- void eventName;
1543
2442
  return undefined;
1544
2443
  }
1545
2444
  try {
@@ -1622,6 +2521,7 @@ export function streamAnthropicViaBetaMessages(
1622
2521
  model: PiModelLike,
1623
2522
  context: PiStreamContext,
1624
2523
  options?: PiSimpleStreamOptions,
2524
+ dependencies: AnthropicTransportDependencies = {},
1625
2525
  ): AssistantMessageEventStreamLike {
1626
2526
  const stream = createAssistantMessageEventStream();
1627
2527
  const output = createOutput(model);
@@ -1632,6 +2532,7 @@ export function streamAnthropicViaBetaMessages(
1632
2532
  }
1633
2533
 
1634
2534
  void (async () => {
2535
+ let preparedLineage: PreparedAnthropicLineage | undefined;
1635
2536
  try {
1636
2537
  const apiKey = options?.apiKey;
1637
2538
  if (typeof apiKey !== 'string' || apiKey.length === 0) {
@@ -1645,37 +2546,108 @@ export function streamAnthropicViaBetaMessages(
1645
2546
  );
1646
2547
  }
1647
2548
 
2549
+ const sessionId = requireSessionId(options?.sessionId, 'options.sessionId');
2550
+ const account = requireAttributionAccount(
2551
+ (dependencies.loadAccount ?? loadClaudeAttributionAccount)(),
2552
+ );
2553
+ const url = resolveAnthropicBetaMessagesUrl(model);
1648
2554
  const policy = resolveClaudeCodeModelPolicy(model);
1649
2555
  let params = buildAnthropicRequestParams(model, context, options);
2556
+ const billingSystemText = buildClaudeCodeBillingSystemText(
2557
+ firstUserMessageTextFromPayload(params),
2558
+ );
2559
+ const provisionalLineage = prepareAnthropicLineageDetails({
2560
+ model,
2561
+ policy,
2562
+ context,
2563
+ payload: params,
2564
+ });
2565
+ if (policy.supportsCacheDiagnostics) {
2566
+ if (!policy.beta.split(',').includes(ANTHROPIC_CACHE_DIAGNOSTICS_BETA)) {
2567
+ throw new Error('Anthropic cache diagnostics policy is missing its required beta header');
2568
+ }
2569
+ params['diagnostics'] = {
2570
+ previous_message_id: provisionalLineage.previous_message_id,
2571
+ };
2572
+ }
2573
+ if (
2574
+ policy.enforcesThinkingPrefixBinding &&
2575
+ !policy.beta.split(',').includes(ANTHROPIC_THINKING_BINDING_BETA)
2576
+ ) {
2577
+ throw new Error('Anthropic thinking-binding policy is missing its required beta header');
2578
+ }
2579
+ params = rewriteAnthropicRequestPayloadForSession({
2580
+ payload: params,
2581
+ model,
2582
+ sessionId,
2583
+ account,
2584
+ headerRegistered: true,
2585
+ ...(options?.cacheRetention === undefined
2586
+ ? {}
2587
+ : { cacheRetention: options.cacheRetention }),
2588
+ });
2589
+ const protectedCacheControlTopologySha256 = cacheControlTopologySha256(params);
1650
2590
  const nextParams = await options?.onPayload?.(params, model);
1651
2591
  if (nextParams !== undefined) {
1652
2592
  if (!isPlainObject(nextParams))
1653
2593
  throw new Error('Anthropic attribution onPayload returned a non-object payload');
1654
2594
  params = nextParams;
1655
2595
  }
1656
- const metadataUserId = isPlainObject(params['metadata'])
1657
- ? params['metadata']['user_id']
1658
- : undefined;
1659
- let sessionId: string | undefined;
1660
- if (typeof metadataUserId === 'string') {
1661
- const parsed = parseJsonObject(metadataUserId, 'Anthropic attribution metadata.user_id');
1662
- if (typeof parsed['session_id'] === 'string') sessionId = parsed['session_id'];
1663
- }
1664
- if (!sessionId)
2596
+ assertProtectedAnthropicAttribution({
2597
+ payload: params,
2598
+ account,
2599
+ sessionId,
2600
+ billingSystemText,
2601
+ modelId: policy.modelId,
2602
+ cacheControlTopologySha256: protectedCacheControlTopologySha256,
2603
+ });
2604
+
2605
+ preparedLineage = anthropicLineageCoordinator.prepare({
2606
+ sessionId,
2607
+ model,
2608
+ policy,
2609
+ context,
2610
+ payload: params,
2611
+ });
2612
+ if (
2613
+ preparedLineage.details.previous_message_id !== provisionalLineage.previous_message_id ||
2614
+ preparedLineage.details.conversation_static_sha256 !==
2615
+ provisionalLineage.conversation_static_sha256 ||
2616
+ preparedLineage.details.request_message_count !==
2617
+ provisionalLineage.request_message_count ||
2618
+ preparedLineage.details.request_messages_sha256 !==
2619
+ provisionalLineage.request_messages_sha256 ||
2620
+ preparedLineage.details.cache_profile_sha256 !== provisionalLineage.cache_profile_sha256 ||
2621
+ preparedLineage.details.cache_retention !== provisionalLineage.cache_retention ||
2622
+ preparedLineage.details.compaction_boundary_sha256 !==
2623
+ provisionalLineage.compaction_boundary_sha256 ||
2624
+ preparedLineage.details.signature_epoch_sha256 !==
2625
+ provisionalLineage.signature_epoch_sha256 ||
2626
+ preparedLineage.details.signature_epoch_inherits_prior !==
2627
+ provisionalLineage.signature_epoch_inherits_prior
2628
+ ) {
1665
2629
  throw new Error(
1666
- 'Anthropic attribution could not derive session_id from rewritten metadata.user_id',
2630
+ 'Anthropic request/cache lineage changed during before_provider_request transforms',
1667
2631
  );
2632
+ }
2633
+ if (policy.supportsCacheDiagnostics) {
2634
+ const diagnostics = params['diagnostics'];
2635
+ if (
2636
+ !isPlainObject(diagnostics) ||
2637
+ diagnostics['previous_message_id'] !== preparedLineage.details.previous_message_id
2638
+ ) {
2639
+ throw new Error(
2640
+ 'Anthropic cache diagnostics were removed or changed during before_provider_request transforms',
2641
+ );
2642
+ }
2643
+ }
1668
2644
 
1669
- const baseUrl =
1670
- model.baseUrl && model.baseUrl.length > 0
1671
- ? model.baseUrl.replace(/\/$/, '')
1672
- : 'https://api.anthropic.com';
1673
- const url = `${baseUrl}/v1/messages?beta=true`;
1674
2645
  const headers = buildFetchHeaders(options, apiKey, sessionId, policy.beta);
1675
2646
  const requestInit: RequestInit = {
1676
2647
  method: 'POST',
1677
2648
  headers,
1678
2649
  body: JSON.stringify(params),
2650
+ redirect: 'error',
1679
2651
  };
1680
2652
  if (options?.signal) requestInit.signal = options.signal;
1681
2653
  const response = await fetch(url, requestInit);
@@ -1688,26 +2660,67 @@ export function streamAnthropicViaBetaMessages(
1688
2660
  `Anthropic beta messages request failed: HTTP ${response.status} ${response.statusText}: ${await response.text()}`,
1689
2661
  );
1690
2662
  }
2663
+ const contentType = response.headers.get('content-type')?.toLowerCase() ?? '';
2664
+ if (!contentType.includes('text/event-stream')) {
2665
+ throw new Error(
2666
+ `Anthropic beta messages response must be text/event-stream; got ${JSON.stringify(contentType || '(missing)')}`,
2667
+ );
2668
+ }
1691
2669
 
1692
2670
  stream.push({ type: 'start', partial: output });
1693
2671
  const blocks = output.content as Array<JsonObject & { index?: number; partialJson?: string }>;
2672
+ const activeContentIndexes = new Set<number>();
2673
+ let sawMessageStart = false;
2674
+ let sawMessageStop = false;
2675
+ let sawTerminalStopReason = false;
1694
2676
  for await (const event of iterateSseEvents(response, options?.signal)) {
1695
- if (event['type'] === 'message_start' && isPlainObject(event['message'])) {
2677
+ const eventType = event['type'];
2678
+ if (typeof eventType !== 'string') {
2679
+ throw new Error('Anthropic beta messages SSE event is missing string data.type');
2680
+ }
2681
+ if (eventType === 'error') throw new Error(anthropicStreamErrorMessage(event));
2682
+ if (eventType === 'ping') continue;
2683
+ if (sawMessageStop) {
2684
+ throw new Error('Anthropic beta messages stream emitted data after message_stop');
2685
+ }
2686
+ if (eventType !== 'message_start' && !sawMessageStart) {
2687
+ throw new Error('Anthropic beta messages stream emitted content before message_start');
2688
+ }
2689
+
2690
+ if (eventType === 'message_start') {
2691
+ if (sawMessageStart || !isPlainObject(event['message'])) {
2692
+ throw new Error(
2693
+ 'Anthropic beta messages stream emitted malformed/duplicate message_start',
2694
+ );
2695
+ }
2696
+ if (typeof event['message']['id'] !== 'string' || event['message']['id'].length === 0) {
2697
+ throw new Error('Anthropic beta messages message_start is missing a response id');
2698
+ }
2699
+ sawMessageStart = true;
1696
2700
  if (typeof event['message']['id'] === 'string')
1697
2701
  output.responseId = event['message']['id'];
2702
+ appendProviderCacheDiagnostic(output, event['message']);
1698
2703
  updateAnthropicUsage(
1699
2704
  output,
1700
2705
  isPlainObject(event['message']['usage']) ? event['message']['usage'] : undefined,
1701
2706
  model,
1702
2707
  );
1703
- } else if (
1704
- event['type'] === 'content_block_start' &&
1705
- typeof event['index'] === 'number' &&
1706
- isPlainObject(event['content_block'])
1707
- ) {
2708
+ } else if (eventType === 'content_block_start') {
2709
+ const contentIndex = event['index'];
1708
2710
  const contentBlock = event['content_block'];
2711
+ if (
2712
+ !Number.isSafeInteger(contentIndex) ||
2713
+ (contentIndex as number) < 0 ||
2714
+ !isPlainObject(contentBlock)
2715
+ ) {
2716
+ throw new Error('Anthropic beta messages stream emitted malformed content_block_start');
2717
+ }
2718
+ if (activeContentIndexes.has(contentIndex as number)) {
2719
+ throw new Error('Anthropic beta messages stream duplicated an active content block');
2720
+ }
2721
+ activeContentIndexes.add(contentIndex as number);
1709
2722
  if (contentBlock['type'] === 'text') {
1710
- output.content.push({ type: 'text', text: '', index: event['index'] });
2723
+ output.content.push({ type: 'text', text: '', index: contentIndex });
1711
2724
  stream.push({
1712
2725
  type: 'text_start',
1713
2726
  contentIndex: output.content.length - 1,
@@ -1718,7 +2731,7 @@ export function streamAnthropicViaBetaMessages(
1718
2731
  type: 'thinking',
1719
2732
  thinking: '',
1720
2733
  thinkingSignature: '',
1721
- index: event['index'],
2734
+ index: contentIndex,
1722
2735
  });
1723
2736
  stream.push({
1724
2737
  type: 'thinking_start',
@@ -1726,12 +2739,15 @@ export function streamAnthropicViaBetaMessages(
1726
2739
  partial: output,
1727
2740
  });
1728
2741
  } else if (contentBlock['type'] === 'redacted_thinking') {
2742
+ if (typeof contentBlock['data'] !== 'string') {
2743
+ throw new Error('Anthropic redacted_thinking block is missing string data');
2744
+ }
1729
2745
  output.content.push({
1730
2746
  type: 'thinking',
1731
2747
  thinking: '[Reasoning redacted]',
1732
2748
  thinkingSignature: contentBlock['data'],
1733
2749
  redacted: true,
1734
- index: event['index'],
2750
+ index: contentIndex,
1735
2751
  });
1736
2752
  stream.push({
1737
2753
  type: 'thinking_start',
@@ -1739,29 +2755,48 @@ export function streamAnthropicViaBetaMessages(
1739
2755
  partial: output,
1740
2756
  });
1741
2757
  } else if (contentBlock['type'] === 'tool_use') {
2758
+ if (
2759
+ typeof contentBlock['id'] !== 'string' ||
2760
+ typeof contentBlock['name'] !== 'string' ||
2761
+ !isPlainObject(contentBlock['input'] ?? {})
2762
+ ) {
2763
+ throw new Error('Anthropic tool_use block is malformed');
2764
+ }
1742
2765
  output.content.push({
1743
2766
  type: 'toolCall',
1744
2767
  id: contentBlock['id'],
1745
2768
  name: contentBlock['name'],
1746
2769
  arguments: contentBlock['input'] ?? {},
1747
2770
  partialJson: '',
1748
- index: event['index'],
2771
+ index: contentIndex,
1749
2772
  });
1750
2773
  stream.push({
1751
2774
  type: 'toolcall_start',
1752
2775
  contentIndex: output.content.length - 1,
1753
2776
  partial: output,
1754
2777
  });
2778
+ } else {
2779
+ throw new Error(
2780
+ `Anthropic beta messages stream emitted unsupported content block ${JSON.stringify(contentBlock['type'])}`,
2781
+ );
1755
2782
  }
1756
- } else if (
1757
- event['type'] === 'content_block_delta' &&
1758
- typeof event['index'] === 'number' &&
1759
- isPlainObject(event['delta'])
1760
- ) {
1761
- const blockIndex = blocks.findIndex((block) => block.index === event['index']);
1762
- const block = blocks[blockIndex];
1763
- if (!block) continue;
2783
+ } else if (eventType === 'content_block_delta') {
2784
+ const contentIndex = event['index'];
1764
2785
  const delta = event['delta'];
2786
+ if (
2787
+ !Number.isSafeInteger(contentIndex) ||
2788
+ !activeContentIndexes.has(contentIndex as number) ||
2789
+ !isPlainObject(delta)
2790
+ ) {
2791
+ throw new Error(
2792
+ 'Anthropic beta messages stream emitted malformed/orphan content_block_delta',
2793
+ );
2794
+ }
2795
+ const blockIndex = blocks.findIndex((block) => block.index === contentIndex);
2796
+ const block = blocks[blockIndex];
2797
+ if (!block) {
2798
+ throw new Error('Anthropic beta messages stream delta has no projected content block');
2799
+ }
1765
2800
  if (
1766
2801
  delta['type'] === 'text_delta' &&
1767
2802
  block['type'] === 'text' &&
@@ -1806,11 +2841,26 @@ export function streamAnthropicViaBetaMessages(
1806
2841
  ) {
1807
2842
  block['thinkingSignature'] =
1808
2843
  `${String(block['thinkingSignature'] ?? '')}${delta['signature']}`;
2844
+ } else {
2845
+ throw new Error(
2846
+ `Anthropic beta messages stream emitted unsupported/mismatched delta ${JSON.stringify(delta['type'])}`,
2847
+ );
2848
+ }
2849
+ } else if (eventType === 'content_block_stop') {
2850
+ const contentIndex = event['index'];
2851
+ if (
2852
+ !Number.isSafeInteger(contentIndex) ||
2853
+ !activeContentIndexes.delete(contentIndex as number)
2854
+ ) {
2855
+ throw new Error(
2856
+ 'Anthropic beta messages stream emitted malformed/orphan content_block_stop',
2857
+ );
1809
2858
  }
1810
- } else if (event['type'] === 'content_block_stop' && typeof event['index'] === 'number') {
1811
- const blockIndex = blocks.findIndex((block) => block.index === event['index']);
2859
+ const blockIndex = blocks.findIndex((block) => block.index === contentIndex);
1812
2860
  const block = blocks[blockIndex];
1813
- if (!block) continue;
2861
+ if (!block) {
2862
+ throw new Error('Anthropic beta messages stream stop has no projected content block');
2863
+ }
1814
2864
  delete block.index;
1815
2865
  if (block['type'] === 'text') {
1816
2866
  stream.push({
@@ -1827,7 +2877,10 @@ export function streamAnthropicViaBetaMessages(
1827
2877
  partial: output,
1828
2878
  });
1829
2879
  } else if (block['type'] === 'toolCall') {
1830
- block['arguments'] = parseStreamingJsonFragment(block.partialJson ?? '{}');
2880
+ block['arguments'] =
2881
+ block.partialJson && block.partialJson.length > 0
2882
+ ? parseCompletedToolInput(block.partialJson)
2883
+ : canonicalizeJson(block['arguments'] ?? {});
1831
2884
  delete block.partialJson;
1832
2885
  stream.push({
1833
2886
  type: 'toolcall_end',
@@ -1836,19 +2889,54 @@ export function streamAnthropicViaBetaMessages(
1836
2889
  partial: output,
1837
2890
  });
1838
2891
  }
1839
- } else if (event['type'] === 'message_delta') {
1840
- if (isPlainObject(event['delta']) && event['delta']['stop_reason'])
1841
- output.stopReason = mapStopReason(event['delta']['stop_reason']);
2892
+ } else if (eventType === 'message_delta') {
2893
+ if (!isPlainObject(event['delta'])) {
2894
+ throw new Error('Anthropic beta messages stream emitted malformed message_delta');
2895
+ }
2896
+ const stopReason = event['delta']['stop_reason'];
2897
+ if (typeof stopReason === 'string') {
2898
+ const mapped = mapStopReason(stopReason);
2899
+ if (mapped === 'error') {
2900
+ throw new Error(
2901
+ `Anthropic beta messages stream emitted unsupported stop reason ${JSON.stringify(stopReason)}`,
2902
+ );
2903
+ }
2904
+ output.stopReason = mapped;
2905
+ sawTerminalStopReason = true;
2906
+ }
1842
2907
  updateAnthropicUsage(
1843
2908
  output,
1844
2909
  isPlainObject(event['usage']) ? event['usage'] : undefined,
1845
2910
  model,
1846
2911
  );
2912
+ } else if (eventType === 'message_stop') {
2913
+ if (activeContentIndexes.size > 0) {
2914
+ throw new Error(
2915
+ 'Anthropic beta messages message_stop arrived with open content blocks',
2916
+ );
2917
+ }
2918
+ sawMessageStop = true;
2919
+ } else {
2920
+ throw new Error(
2921
+ `Anthropic beta messages stream emitted unsupported event ${JSON.stringify(eventType)}`,
2922
+ );
1847
2923
  }
1848
2924
  }
1849
2925
  if (options?.signal?.aborted) throw new Error('Request was aborted');
1850
- if (output.stopReason === 'error')
1851
- throw new Error(output.errorMessage || 'Anthropic stream ended with error stop reason');
2926
+ if (!sawMessageStart) {
2927
+ throw new Error('Anthropic beta messages stream ended without message_start');
2928
+ }
2929
+ if (!sawMessageStop) {
2930
+ throw new Error('Anthropic beta messages stream ended without message_stop');
2931
+ }
2932
+ if (!sawTerminalStopReason || output.stopReason === 'error') {
2933
+ throw new Error(
2934
+ 'Anthropic beta messages stream ended without a valid terminal stop reason',
2935
+ );
2936
+ }
2937
+ if (preparedLineage === undefined)
2938
+ throw new Error('Anthropic attribution completed without prepared cache lineage');
2939
+ appendLineageDiagnostic(output, preparedLineage);
1852
2940
  stream.push({ type: 'done', reason: output.stopReason, message: output });
1853
2941
  stream.end();
1854
2942
  } catch (error) {
@@ -1860,6 +2948,8 @@ export function streamAnthropicViaBetaMessages(
1860
2948
  output.errorMessage = error instanceof Error ? error.message : String(error);
1861
2949
  stream.push({ type: 'error', reason: output.stopReason, error: output });
1862
2950
  stream.end();
2951
+ } finally {
2952
+ if (preparedLineage !== undefined) anthropicLineageCoordinator.release(preparedLineage);
1863
2953
  }
1864
2954
  })();
1865
2955
 
@@ -1898,7 +2988,10 @@ function isAnthropicAttributionClaimProbe(value: unknown): value is AnthropicAtt
1898
2988
  * publishes ownership after every registration below succeeds; a factory that throws
1899
2989
  * cannot strand a false claim that suppresses a healthy later copy.
1900
2990
  */
1901
- export default function spawnAnthropicAttribution(pi: PiExtensionHost): void {
2991
+ export default function spawnAnthropicAttribution(
2992
+ pi: PiExtensionHost,
2993
+ dependencies: AnthropicTransportDependencies = {},
2994
+ ): void {
1902
2995
  const acknowledgements: true[] = [];
1903
2996
  const probe: AnthropicAttributionClaimProbe = {
1904
2997
  schema_version: ANTHROPIC_ATTRIBUTION_CLAIM_SCHEMA,
@@ -1914,15 +3007,20 @@ export default function spawnAnthropicAttribution(pi: PiExtensionHost): void {
1914
3007
  sessionCacheRetention;
1915
3008
 
1916
3009
  // Registration is global but route-scoped by provider name. Keeping it at
1917
- // factory scope avoids lifecycle-dependent provider availability; the custom
1918
- // transport derives session/model headers from the attributed payload.
3010
+ // factory scope avoids lifecycle-dependent provider availability; every custom
3011
+ // transport request owns attribution from its request-scoped session options.
1919
3012
  pi.registerProvider('anthropic', {
1920
3013
  api: 'anthropic-messages',
1921
3014
  streamSimple: (model, context, options) =>
1922
- streamAnthropicViaBetaMessages(model, context, {
1923
- ...(options ?? {}),
1924
- cacheRetention: resolveCacheRetentionPreference(options, getSessionOverride()),
1925
- }),
3015
+ streamAnthropicViaBetaMessages(
3016
+ model,
3017
+ context,
3018
+ {
3019
+ ...(options ?? {}),
3020
+ cacheRetention: resolveRegisteredCacheRetention(options, getSessionOverride()),
3021
+ },
3022
+ dependencies,
3023
+ ),
1926
3024
  });
1927
3025
 
1928
3026
  pi.registerCommand('claude-cache', {
@@ -1965,16 +3063,6 @@ export default function spawnAnthropicAttribution(pi: PiExtensionHost): void {
1965
3063
  sessionCacheRetention = restoreAnthropicSessionCacheRetention(ctx.sessionManager.getBranch());
1966
3064
  });
1967
3065
 
1968
- pi.on('before_provider_request', (event, ctx) => {
1969
- if (!isAnthropicContext(ctx)) return undefined;
1970
- return rewriteAnthropicRequestPayload({
1971
- payload: event.payload,
1972
- ctx,
1973
- account: loadClaudeAttributionAccount(),
1974
- headerRegistered: true,
1975
- });
1976
- });
1977
-
1978
3066
  // Publish ownership last. Extension loading is sequential, so later independent
1979
3067
  // copies probe this responder and become inert instead of registering duplicates.
1980
3068
  pi.events.on(ANTHROPIC_ATTRIBUTION_CLAIM_CHANNEL, (value) => {