pi-background-tasks 2.4.0 → 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.
- package/README.md +1 -1
- package/TESTING.md +2 -2
- package/TEST_PLAN.md +6 -6
- package/docs/INDEX.md +1 -1
- package/docs/commands/claude-cache.md +3 -3
- package/docs/manifest.json +4 -4
- package/docs/operations/configuration.md +8 -8
- package/docs/operations/troubleshooting.md +2 -2
- package/docs/reference/runtime-contracts.md +30 -30
- package/docs/subsystems/anthropic-attribution.md +14 -4
- package/docs/subsystems/docs-freshness-gate.md +1 -1
- package/docs/subsystems/fusion.md +6 -6
- package/docs/tools/fusion_research.md +2 -2
- package/package.json +1 -1
- package/src/core/anthropic-attribution.ts +1286 -198
- package/src/core/fusion/child-protocol.ts +3 -3
- package/src/core/fusion/pi-child.ts +12 -5
- package/src/core/fusion/web-fetch.ts +72 -5
- package/src/core/update-check.ts +13 -1
|
@@ -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.
|
|
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.
|
|
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
|
-
| {
|
|
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
|
-
*
|
|
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
|
|
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
|
|
785
|
-
|
|
786
|
-
|
|
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
|
|
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(
|
|
877
|
-
|
|
878
|
-
|
|
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
|
-
|
|
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
|
|
1168
|
+
readonly model: PiModelLike;
|
|
1169
|
+
readonly sessionId: string;
|
|
1037
1170
|
readonly account: ClaudeAttributionAccount;
|
|
1038
1171
|
readonly headerRegistered?: boolean;
|
|
1039
1172
|
readonly cacheRetention?: CacheRetention;
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
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 =
|
|
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.
|
|
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.
|
|
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:
|
|
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
|
-
|
|
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[]):
|
|
1109
|
-
const
|
|
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 (
|
|
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
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
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
|
-
|
|
1185
|
-
}
|
|
1678
|
+
if (message === undefined) throw new TypeError(`Anthropic message ${index} is missing`);
|
|
1679
|
+
|
|
1186
1680
|
if (message.role === 'user') {
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
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['
|
|
1208
|
-
|
|
1209
|
-
block['text'].trim().length > 0
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
}
|
|
1213
|
-
|
|
1214
|
-
typeof block['thinking']
|
|
1215
|
-
|
|
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
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
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
|
|
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)
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
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
|
-
|
|
1259
|
-
|
|
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:
|
|
1278
|
-
|
|
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(
|
|
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 (
|
|
1335
|
-
|
|
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'] = {
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
}
|
|
1664
|
-
|
|
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
|
|
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
|
-
|
|
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['
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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['
|
|
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
|
-
|
|
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)
|
|
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'] =
|
|
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 (
|
|
1840
|
-
if (isPlainObject(event['delta'])
|
|
1841
|
-
|
|
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 (
|
|
1851
|
-
throw new Error(
|
|
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(
|
|
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;
|
|
1918
|
-
// transport
|
|
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(
|
|
1923
|
-
|
|
1924
|
-
|
|
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) => {
|