pi-background-tasks 2.4.2 → 2.6.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/BACKGROUND-TASKS-INSTRUCTIONS.md +1 -1
- package/README.md +70 -25
- package/TESTING.md +42 -2
- package/TEST_PLAN.md +7 -4
- package/dist/extensions/anthropic-attribution-child.js +4 -0
- package/dist/extensions/anthropic-attribution-child.js.map +1 -0
- package/dist/extensions/anthropic-attribution.js +107 -0
- package/dist/extensions/anthropic-attribution.js.map +1 -0
- package/dist/extensions/background-tasks.js +2 -0
- package/dist/extensions/background-tasks.js.map +1 -0
- package/dist/extensions/delegate-child.js +2 -0
- package/dist/extensions/delegate-child.js.map +1 -0
- package/dist/extensions/fusion-child.js +2 -0
- package/dist/extensions/fusion-child.js.map +1 -0
- package/dist/package.json +5 -0
- package/dist/src/core/anthropic-attribution-path.js +24 -0
- package/dist/src/core/anthropic-attribution-path.js.map +1 -0
- package/dist/src/core/anthropic-attribution.js +2247 -0
- package/dist/src/core/anthropic-attribution.js.map +1 -0
- package/dist/src/core/attested-pi-contract.js +5 -0
- package/dist/src/core/attested-pi-contract.js.map +1 -0
- package/dist/src/core/attested-pi-run.js +749 -0
- package/dist/src/core/attested-pi-run.js.map +1 -0
- package/dist/src/core/canonical-json.js +19 -0
- package/dist/src/core/canonical-json.js.map +1 -0
- package/dist/src/core/common.js +787 -0
- package/dist/src/core/common.js.map +1 -0
- package/dist/src/core/config.js +78 -0
- package/dist/src/core/config.js.map +1 -0
- package/dist/src/core/context/parent-snapshot.js +75 -0
- package/dist/src/core/context/parent-snapshot.js.map +1 -0
- package/dist/src/core/context/token-budget.js +631 -0
- package/dist/src/core/context/token-budget.js.map +1 -0
- package/dist/src/core/context/visible-conversation-v2.js +390 -0
- package/dist/src/core/context/visible-conversation-v2.js.map +1 -0
- package/dist/src/core/delegate/artifacts.js +357 -0
- package/dist/src/core/delegate/artifacts.js.map +1 -0
- package/dist/src/core/delegate/budget.js +266 -0
- package/dist/src/core/delegate/budget.js.map +1 -0
- package/dist/src/core/delegate/facade-contract.js +7 -0
- package/dist/src/core/delegate/facade-contract.js.map +1 -0
- package/dist/src/core/delegate/hook-contract-evidence.json +18 -0
- package/dist/src/core/delegate/hook-contract.js +124 -0
- package/dist/src/core/delegate/hook-contract.js.map +1 -0
- package/dist/src/core/delegate/launch.js +366 -0
- package/dist/src/core/delegate/launch.js.map +1 -0
- package/dist/src/core/delegate/result-package.js +293 -0
- package/dist/src/core/delegate/result-package.js.map +1 -0
- package/dist/src/core/delegate/runner.js +398 -0
- package/dist/src/core/delegate/runner.js.map +1 -0
- package/dist/src/core/delegate/seed.js +334 -0
- package/dist/src/core/delegate/seed.js.map +1 -0
- package/dist/src/core/delegate/types.js +103 -0
- package/dist/src/core/delegate/types.js.map +1 -0
- package/dist/src/core/durable-fs.js +372 -0
- package/dist/src/core/durable-fs.js.map +1 -0
- package/dist/src/core/extension-api.js +382 -0
- package/dist/src/core/extension-api.js.map +1 -0
- package/dist/src/core/fusion/artifacts.js +654 -0
- package/dist/src/core/fusion/artifacts.js.map +1 -0
- package/dist/src/core/fusion/budget.js +949 -0
- package/dist/src/core/fusion/budget.js.map +1 -0
- package/dist/src/core/fusion/child-protocol.js +176 -0
- package/dist/src/core/fusion/child-protocol.js.map +1 -0
- package/dist/src/core/fusion/claude-cache.js +156 -0
- package/dist/src/core/fusion/claude-cache.js.map +1 -0
- package/dist/src/core/fusion/clean-context.js +64 -0
- package/dist/src/core/fusion/clean-context.js.map +1 -0
- package/dist/src/core/fusion/config.js +349 -0
- package/dist/src/core/fusion/config.js.map +1 -0
- package/dist/src/core/fusion/context.js +184 -0
- package/dist/src/core/fusion/context.js.map +1 -0
- package/dist/src/core/fusion/evaluation.js +696 -0
- package/dist/src/core/fusion/evaluation.js.map +1 -0
- package/dist/src/core/fusion/facade-contract.js +3 -0
- package/dist/src/core/fusion/facade-contract.js.map +1 -0
- package/dist/src/core/fusion/orchestrator.js +867 -0
- package/dist/src/core/fusion/orchestrator.js.map +1 -0
- package/dist/src/core/fusion/output-contract.js +27 -0
- package/dist/src/core/fusion/output-contract.js.map +1 -0
- package/dist/src/core/fusion/pi-child.js +1741 -0
- package/dist/src/core/fusion/pi-child.js.map +1 -0
- package/dist/src/core/fusion/prompts.js +260 -0
- package/dist/src/core/fusion/prompts.js.map +1 -0
- package/dist/src/core/fusion/result-package.js +857 -0
- package/dist/src/core/fusion/result-package.js.map +1 -0
- package/dist/src/core/fusion/source-policy.js +259 -0
- package/dist/src/core/fusion/source-policy.js.map +1 -0
- package/dist/src/core/fusion/types.js +207 -0
- package/dist/src/core/fusion/types.js.map +1 -0
- package/dist/src/core/fusion/web-fetch.js +777 -0
- package/dist/src/core/fusion/web-fetch.js.map +1 -0
- package/dist/src/core/fusion/workflows.js +126 -0
- package/dist/src/core/fusion/workflows.js.map +1 -0
- package/dist/src/core/lazy-module.js +181 -0
- package/dist/src/core/lazy-module.js.map +1 -0
- package/dist/src/core/pi-launch.js +431 -0
- package/dist/src/core/pi-launch.js.map +1 -0
- package/dist/src/core/registry.js +3331 -0
- package/dist/src/core/registry.js.map +1 -0
- package/dist/src/core/reload-shell-owner.js +1396 -0
- package/dist/src/core/reload-shell-owner.js.map +1 -0
- package/dist/src/core/shell-policy.js +80 -0
- package/dist/src/core/shell-policy.js.map +1 -0
- package/dist/src/core/task-durable.js +51 -0
- package/dist/src/core/task-durable.js.map +1 -0
- package/dist/src/core/update-check.js +92 -0
- package/dist/src/core/update-check.js.map +1 -0
- package/dist/src/core/windows-taskkill.js +185 -0
- package/dist/src/core/windows-taskkill.js.map +1 -0
- package/dist/src/delegate-child-extension.js +776 -0
- package/dist/src/delegate-child-extension.js.map +1 -0
- package/dist/src/delegate-extension.js +746 -0
- package/dist/src/delegate-extension.js.map +1 -0
- package/dist/src/extension.js +953 -0
- package/dist/src/extension.js.map +1 -0
- package/dist/src/fusion-child-extension.js +760 -0
- package/dist/src/fusion-child-extension.js.map +1 -0
- package/dist/src/fusion-extension.js +1030 -0
- package/dist/src/fusion-extension.js.map +1 -0
- package/dist/src/ui/background-tasks-manager.js +689 -0
- package/dist/src/ui/background-tasks-manager.js.map +1 -0
- package/dist/src/ui/fusion-model-selector.js +277 -0
- package/dist/src/ui/fusion-model-selector.js.map +1 -0
- package/docs/INDEX.md +35 -33
- package/docs/api/eventbus-v1.md +21 -5
- package/docs/choose-a-workflow.md +2 -0
- package/docs/commands/bg-clear.md +3 -3
- package/docs/commands/bg-update.md +3 -3
- package/docs/commands/bg.md +18 -6
- package/docs/commands/claude-cache.md +6 -6
- package/docs/commands/fusion-models.md +49 -4
- package/docs/commands/fusion.md +3 -3
- package/docs/commands/jobs.md +3 -3
- package/docs/commands/kill.md +3 -3
- package/docs/commands/logs.md +3 -3
- package/docs/commands/task-manager.md +5 -5
- package/docs/concepts/completion-delivery.md +6 -2
- package/docs/getting-started.md +7 -1
- package/docs/manifest.json +228 -42
- package/docs/operations/configuration.md +58 -5
- package/docs/operations/releasing.md +4 -2
- package/docs/operations/testing.md +22 -0
- package/docs/operations/troubleshooting.md +6 -2
- package/docs/read-before-edit.md +44 -31
- package/docs/reference/runtime-contracts.md +87 -51
- package/docs/reference/shortcuts-and-dock.md +28 -14
- package/docs/subsystems/anthropic-attribution.md +35 -13
- package/docs/subsystems/attested-pi-runs.md +12 -7
- package/docs/subsystems/background-task-runtime.md +53 -11
- package/docs/subsystems/child-launch-durability-and-safety.md +28 -9
- package/docs/subsystems/delegation.md +16 -3
- package/docs/subsystems/docs-freshness-gate.md +14 -7
- package/docs/subsystems/fusion.md +36 -11
- package/docs/subsystems/host-ui-and-telemetry.md +22 -9
- package/docs/tools/bg_delegate.md +3 -1
- package/docs/tools/bg_kill.md +3 -1
- package/docs/tools/bg_logs.md +3 -1
- package/docs/tools/bg_result.md +5 -3
- package/docs/tools/bg_run.md +31 -5
- package/docs/tools/bg_run_pi_attested.md +3 -1
- package/docs/tools/bg_status.md +3 -1
- package/docs/tools/fusion_investigate.md +3 -1
- package/docs/tools/fusion_reason.md +3 -1
- package/docs/tools/fusion_research.md +3 -1
- package/docs/tools/fusion_validate.md +3 -1
- package/extensions/anthropic-attribution-child.ts +3 -0
- package/extensions/anthropic-attribution.ts +166 -1
- package/package.json +12 -10
- package/src/core/anthropic-attribution-path.ts +5 -4
- package/src/core/anthropic-attribution.ts +1505 -237
- package/src/core/attested-pi-contract.ts +4 -0
- package/src/core/attested-pi-run.ts +468 -81
- package/src/core/canonical-json.ts +21 -0
- package/src/core/common.ts +450 -40
- package/src/core/config.ts +121 -0
- package/src/core/context/visible-conversation-v2.ts +3 -6
- package/src/core/delegate/artifacts.ts +13 -9
- package/src/core/delegate/budget.ts +13 -6
- package/src/core/delegate/facade-contract.ts +6 -0
- package/src/core/delegate/result-package.ts +32 -14
- package/src/core/delegate/runner.ts +129 -58
- package/src/core/delegate/seed.ts +5 -7
- package/src/core/durable-fs.ts +178 -30
- package/src/core/extension-api.ts +36 -8
- package/src/core/fusion/artifacts.ts +17 -16
- package/src/core/fusion/clean-context.ts +8 -5
- package/src/core/fusion/config.ts +2 -1
- package/src/core/fusion/context.ts +2 -6
- package/src/core/fusion/facade-contract.ts +2 -0
- package/src/core/fusion/orchestrator.ts +3 -3
- package/src/core/fusion/prompts.ts +1 -1
- package/src/core/fusion/result-package.ts +259 -72
- package/src/core/fusion/source-policy.ts +43 -17
- package/src/core/lazy-module.ts +215 -0
- package/src/core/pi-launch.ts +390 -65
- package/src/core/registry.ts +2010 -381
- package/src/core/reload-shell-owner.ts +1662 -0
- package/src/core/shell-policy.ts +134 -0
- package/src/core/task-durable.ts +67 -0
- package/src/delegate-extension.ts +404 -144
- package/src/extension.ts +387 -114
- package/src/fusion-extension.ts +202 -96
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { appendFileSync, readFileSync } from 'node:fs';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
|
-
import { join } from 'node:path';
|
|
4
|
+
import { isAbsolute, join } from 'node:path';
|
|
5
|
+
import { anthropicMessagesApi } from '@earendil-works/pi-ai/compat';
|
|
6
|
+
import type {
|
|
7
|
+
AssistantMessageEventStream as HostAssistantMessageEventStream,
|
|
8
|
+
Context as HostContext,
|
|
9
|
+
Model as HostModel,
|
|
10
|
+
SimpleStreamOptions as HostSimpleStreamOptions,
|
|
11
|
+
} from '@earendil-works/pi-ai';
|
|
5
12
|
|
|
6
13
|
export const CLAUDE_CODE_SESSION_HEADER = 'X-Claude-Code-Session-Id';
|
|
7
14
|
|
|
8
|
-
const CLAUDE_CODE_VERSION = '2.1.
|
|
15
|
+
const CLAUDE_CODE_VERSION = '2.1.251';
|
|
9
16
|
const CLAUDE_CODE_ENTRYPOINT = 'sdk-cli';
|
|
10
|
-
const CLAUDE_CODE_USER_AGENT = 'claude-cli/2.1.
|
|
17
|
+
const CLAUDE_CODE_USER_AGENT = 'claude-cli/2.1.251 (external, sdk-cli)';
|
|
11
18
|
export const ANTHROPIC_1M_CONTEXT_BETA = 'context-1m-2025-08-07' as const;
|
|
12
19
|
export const CLAUDE_CODE_200K_SUBSCRIPTION_CONTEXT_WINDOW = 200_000 as const;
|
|
13
20
|
|
|
@@ -20,7 +27,9 @@ type ClaudeCode200KSubscriptionBetaValue =
|
|
|
20
27
|
| 'prompt-caching-scope-2026-01-05'
|
|
21
28
|
| 'advisor-tool-2026-03-01'
|
|
22
29
|
| 'structured-outputs-2025-12-15'
|
|
23
|
-
| 'mid-conversation-system-2026-04-07'
|
|
30
|
+
| 'mid-conversation-system-2026-04-07'
|
|
31
|
+
| 'thinking-binding-controls-2026-08-01'
|
|
32
|
+
| 'cache-diagnosis-2026-04-07';
|
|
24
33
|
|
|
25
34
|
const CLAUDE_CODE_LEGACY_BETA_VALUES = [
|
|
26
35
|
'claude-code-20250219',
|
|
@@ -41,6 +50,11 @@ const CLAUDE_CODE_ADAPTIVE_200K_BETA_VALUES = [
|
|
|
41
50
|
'prompt-caching-scope-2026-01-05',
|
|
42
51
|
'mid-conversation-system-2026-04-07',
|
|
43
52
|
] as const satisfies readonly ClaudeCode200KSubscriptionBetaValue[];
|
|
53
|
+
const CLAUDE_CODE_FABLE_5_1_200K_BETA_VALUES = [
|
|
54
|
+
...CLAUDE_CODE_ADAPTIVE_200K_BETA_VALUES,
|
|
55
|
+
'thinking-binding-controls-2026-08-01',
|
|
56
|
+
'cache-diagnosis-2026-04-07',
|
|
57
|
+
] as const satisfies readonly ClaudeCode200KSubscriptionBetaValue[];
|
|
44
58
|
|
|
45
59
|
function build200KSubscriptionBetaHeader(
|
|
46
60
|
values: readonly ClaudeCode200KSubscriptionBetaValue[],
|
|
@@ -57,17 +71,32 @@ export const CLAUDE_CODE_BETA = build200KSubscriptionBetaHeader(CLAUDE_CODE_LEGA
|
|
|
57
71
|
const CLAUDE_CODE_ADAPTIVE_200K_BETA = build200KSubscriptionBetaHeader(
|
|
58
72
|
CLAUDE_CODE_ADAPTIVE_200K_BETA_VALUES,
|
|
59
73
|
);
|
|
74
|
+
const CLAUDE_CODE_FABLE_5_1_200K_BETA = build200KSubscriptionBetaHeader(
|
|
75
|
+
CLAUDE_CODE_FABLE_5_1_200K_BETA_VALUES,
|
|
76
|
+
);
|
|
60
77
|
const CLAUDE_AGENT_SDK_SYSTEM_TEXT =
|
|
61
78
|
"You are a Claude agent, built on Anthropic's Claude Agent SDK.";
|
|
62
79
|
const FINGERPRINT_SALT = '59cf53e54c78';
|
|
63
80
|
const AUDIT_ENV = 'PIPELINE_ANTHROPIC_ATTRIBUTION_AUDIT_PATH';
|
|
64
81
|
const CACHE_RETENTION_ENV = 'PI_CACHE_RETENTION';
|
|
82
|
+
export const ANTHROPIC_ACCOUNT_CONFIG_PATH_ENV = 'PI_ANTHROPIC_ACCOUNT_CONFIG_PATH';
|
|
65
83
|
export const ANTHROPIC_CACHE_RETENTION_ENTRY = 'pipeline-anthropic-cache-retention';
|
|
66
84
|
const ANTHROPIC_CACHE_RETENTION_SCHEMA = 'pipeline.anthropic_cache_retention.v1';
|
|
67
85
|
export const ANTHROPIC_ATTRIBUTION_CLAIM_CHANNEL = 'pi-anthropic-attribution:claim:v1';
|
|
68
86
|
const ANTHROPIC_ATTRIBUTION_CLAIM_SCHEMA = 'pi-anthropic-attribution.claim.v1';
|
|
69
87
|
const NATIVE_ATTESTATION_PLACEHOLDER = '00000';
|
|
70
88
|
const ANTHROPIC_CACHE_CONTROL_BREAKPOINT_LIMIT = 4;
|
|
89
|
+
const ANTHROPIC_LINEAGE_DIAGNOSTIC_TYPE = 'anthropic-cache-lineage';
|
|
90
|
+
const ANTHROPIC_LINEAGE_SCHEMA = 'pi-anthropic-attribution.lineage.v1';
|
|
91
|
+
// Bump whenever any system/tool/message wire projection changes. Old receipts then
|
|
92
|
+
// become legacy and cannot authorize signature replay under a rewritten prefix.
|
|
93
|
+
const ANTHROPIC_PROJECTION_VERSION = 3;
|
|
94
|
+
const ANTHROPIC_OFFICIAL_ORIGIN = 'https://api.anthropic.com';
|
|
95
|
+
const ANTHROPIC_BETA_MESSAGES_URL = `${ANTHROPIC_OFFICIAL_ORIGIN}/v1/messages?beta=true`;
|
|
96
|
+
const ANTHROPIC_CACHE_DIAGNOSTICS_BETA = 'cache-diagnosis-2026-04-07';
|
|
97
|
+
const ANTHROPIC_THINKING_BINDING_BETA = 'thinking-binding-controls-2026-08-01';
|
|
98
|
+
const COMPACTION_SUMMARY_PREFIX =
|
|
99
|
+
'The conversation history before this point was compacted into the following summary:';
|
|
71
100
|
|
|
72
101
|
// Sanitization behavior derived from the MIT-licensed ravshansbox/pi-anthropic-sps
|
|
73
102
|
// extension at commit 17409b5615f0ec0625776bc5434f92f2c55e3fd0. Keep exact-match
|
|
@@ -125,8 +154,11 @@ interface PiModelCostLike extends PiCostRatesLike {
|
|
|
125
154
|
export interface PiModelLike {
|
|
126
155
|
readonly provider?: string;
|
|
127
156
|
readonly id?: string;
|
|
157
|
+
readonly name?: string;
|
|
128
158
|
readonly api?: string;
|
|
129
159
|
readonly baseUrl?: string;
|
|
160
|
+
readonly input?: readonly ('text' | 'image')[];
|
|
161
|
+
readonly contextWindow?: number;
|
|
130
162
|
readonly maxTokens?: number;
|
|
131
163
|
readonly reasoning?: boolean;
|
|
132
164
|
readonly compat?: {
|
|
@@ -143,12 +175,18 @@ export interface ClaudeCodeModelPolicy {
|
|
|
143
175
|
readonly beta: string;
|
|
144
176
|
readonly thinkingPolicy: ClaudeCodeThinkingPolicy;
|
|
145
177
|
readonly contextWindow: typeof CLAUDE_CODE_200K_SUBSCRIPTION_CONTEXT_WINDOW;
|
|
178
|
+
readonly enforcesThinkingPrefixBinding: boolean;
|
|
179
|
+
readonly supportsCacheDiagnostics: boolean;
|
|
146
180
|
}
|
|
147
181
|
|
|
148
182
|
function claudeCode200KSubscriptionPolicy(
|
|
149
183
|
modelId: string,
|
|
150
184
|
beta: string,
|
|
151
185
|
thinkingPolicy: ClaudeCodeThinkingPolicy,
|
|
186
|
+
features: {
|
|
187
|
+
readonly enforcesThinkingPrefixBinding?: boolean;
|
|
188
|
+
readonly supportsCacheDiagnostics?: boolean;
|
|
189
|
+
} = {},
|
|
152
190
|
): ClaudeCodeModelPolicy {
|
|
153
191
|
if (beta.split(',').includes(ANTHROPIC_1M_CONTEXT_BETA)) {
|
|
154
192
|
throw new Error(
|
|
@@ -160,6 +198,8 @@ function claudeCode200KSubscriptionPolicy(
|
|
|
160
198
|
beta,
|
|
161
199
|
thinkingPolicy,
|
|
162
200
|
contextWindow: CLAUDE_CODE_200K_SUBSCRIPTION_CONTEXT_WINDOW,
|
|
201
|
+
enforcesThinkingPrefixBinding: features.enforcesThinkingPrefixBinding === true,
|
|
202
|
+
supportsCacheDiagnostics: features.supportsCacheDiagnostics === true,
|
|
163
203
|
};
|
|
164
204
|
}
|
|
165
205
|
|
|
@@ -209,6 +249,12 @@ const CLAUDE_CODE_MODEL_POLICIES: Record<string, ClaudeCodeModelPolicy> = Object
|
|
|
209
249
|
CLAUDE_CODE_ADAPTIVE_200K_BETA,
|
|
210
250
|
'adaptive-effort',
|
|
211
251
|
),
|
|
252
|
+
'claude-fable-5-1': claudeCode200KSubscriptionPolicy(
|
|
253
|
+
'claude-fable-5-1',
|
|
254
|
+
CLAUDE_CODE_FABLE_5_1_200K_BETA,
|
|
255
|
+
'adaptive-effort',
|
|
256
|
+
{ enforcesThinkingPrefixBinding: true, supportsCacheDiagnostics: true },
|
|
257
|
+
),
|
|
212
258
|
'claude-haiku-4-5': claudeCode200KSubscriptionPolicy(
|
|
213
259
|
'claude-haiku-4-5',
|
|
214
260
|
CLAUDE_CODE_BETA,
|
|
@@ -344,10 +390,6 @@ export interface PiExtensionHost extends PiProviderRegistrationHost {
|
|
|
344
390
|
eventName: 'session_start' | 'session_shutdown' | 'session_tree' | 'before_agent_start',
|
|
345
391
|
handler: (event: unknown, ctx: PiContextLike) => void,
|
|
346
392
|
): void;
|
|
347
|
-
on(
|
|
348
|
-
eventName: 'before_provider_request',
|
|
349
|
-
handler: (event: { readonly payload: unknown }, ctx: PiContextLike) => unknown,
|
|
350
|
-
): void;
|
|
351
393
|
registerCommand(name: string, config: PiCommandConfigLike): void;
|
|
352
394
|
appendEntry(customType: string, data?: unknown): void;
|
|
353
395
|
}
|
|
@@ -356,14 +398,36 @@ type PiContentBlock =
|
|
|
356
398
|
| { readonly type: 'text'; readonly text: string }
|
|
357
399
|
| { readonly type: 'image'; readonly mimeType: string; readonly data: string };
|
|
358
400
|
|
|
401
|
+
interface PiAssistantDiagnosticLike {
|
|
402
|
+
readonly type: string;
|
|
403
|
+
readonly timestamp: number;
|
|
404
|
+
readonly details?: JsonObject;
|
|
405
|
+
}
|
|
406
|
+
|
|
359
407
|
type PiMessage =
|
|
360
|
-
| {
|
|
361
|
-
|
|
408
|
+
| {
|
|
409
|
+
readonly role: 'user';
|
|
410
|
+
readonly content: string | readonly PiContentBlock[];
|
|
411
|
+
readonly timestamp?: number;
|
|
412
|
+
}
|
|
413
|
+
| {
|
|
414
|
+
readonly role: 'assistant';
|
|
415
|
+
readonly content: readonly JsonObject[];
|
|
416
|
+
readonly provider?: string;
|
|
417
|
+
readonly api?: string;
|
|
418
|
+
readonly model?: string;
|
|
419
|
+
readonly responseId?: string;
|
|
420
|
+
readonly stopReason?: string;
|
|
421
|
+
readonly diagnostics?: readonly PiAssistantDiagnosticLike[];
|
|
422
|
+
readonly timestamp?: number;
|
|
423
|
+
}
|
|
362
424
|
| {
|
|
363
425
|
readonly role: 'toolResult';
|
|
364
426
|
readonly toolCallId: string;
|
|
427
|
+
readonly toolName?: string;
|
|
365
428
|
readonly content: readonly PiContentBlock[];
|
|
366
429
|
readonly isError?: boolean;
|
|
430
|
+
readonly timestamp?: number;
|
|
367
431
|
};
|
|
368
432
|
|
|
369
433
|
export interface PiStreamContext {
|
|
@@ -402,6 +466,10 @@ export interface PiSimpleStreamOptions {
|
|
|
402
466
|
) => Promise<void> | void;
|
|
403
467
|
}
|
|
404
468
|
|
|
469
|
+
export interface AnthropicTransportDependencies {
|
|
470
|
+
readonly loadAccount?: () => ClaudeAttributionAccount;
|
|
471
|
+
}
|
|
472
|
+
|
|
405
473
|
export interface AssistantMessageLike {
|
|
406
474
|
role: 'assistant';
|
|
407
475
|
content: JsonObject[];
|
|
@@ -414,13 +482,20 @@ export interface AssistantMessageLike {
|
|
|
414
482
|
cacheRead: number;
|
|
415
483
|
cacheWrite: number;
|
|
416
484
|
cacheWrite1h?: number;
|
|
485
|
+
reasoning?: number;
|
|
417
486
|
totalTokens: number;
|
|
418
487
|
cost: { input: number; output: number; cacheRead: number; cacheWrite: number; total: number };
|
|
419
488
|
};
|
|
420
|
-
stopReason: 'stop' | 'length' | 'toolUse' | 'aborted' | 'error';
|
|
489
|
+
stopReason: 'pending' | 'stop' | 'length' | 'toolUse' | 'aborted' | 'error' | 'deferred';
|
|
421
490
|
timestamp: number;
|
|
491
|
+
responseModel?: string;
|
|
422
492
|
responseId?: string;
|
|
493
|
+
providerThinkingLevel?: string;
|
|
494
|
+
endTurn?: boolean;
|
|
495
|
+
diagnostics?: PiAssistantDiagnosticLike[];
|
|
496
|
+
deferred?: unknown;
|
|
423
497
|
errorMessage?: string;
|
|
498
|
+
rawStopReason?: string;
|
|
424
499
|
}
|
|
425
500
|
|
|
426
501
|
type AssistantMessageEvent =
|
|
@@ -569,12 +644,8 @@ function parseCacheRetention(value: string, source: string): CacheRetention {
|
|
|
569
644
|
);
|
|
570
645
|
}
|
|
571
646
|
|
|
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
|
-
*/
|
|
647
|
+
/** Resolve an explicit request posture. Registered Pi sessions apply the stronger
|
|
648
|
+
* one-hour policy through resolveRegisteredCacheRetention below. */
|
|
578
649
|
export function resolveCacheRetentionPreference(
|
|
579
650
|
options?: {
|
|
580
651
|
readonly cacheRetention?: CacheRetention;
|
|
@@ -589,6 +660,20 @@ export function resolveCacheRetentionPreference(
|
|
|
589
660
|
return 'long';
|
|
590
661
|
}
|
|
591
662
|
|
|
663
|
+
function resolveRegisteredCacheRetention(
|
|
664
|
+
options: PiSimpleStreamOptions | undefined,
|
|
665
|
+
sessionOverride: Exclude<CacheRetention, 'none'> | undefined,
|
|
666
|
+
): CacheRetention {
|
|
667
|
+
if (options?.cacheRetention === 'none') return 'none';
|
|
668
|
+
if (sessionOverride !== undefined) return sessionOverride;
|
|
669
|
+
const configured = providerEnvValue(CACHE_RETENTION_ENV, options?.env);
|
|
670
|
+
if (configured !== undefined) return parseCacheRetention(configured, CACHE_RETENTION_ENV);
|
|
671
|
+
if (options?.cacheRetention === 'long') return 'long';
|
|
672
|
+
// Pi's generic provider default is five minutes. Subscription coding sessions
|
|
673
|
+
// routinely have turns longer than that, so the attributed route pins one hour.
|
|
674
|
+
return 'long';
|
|
675
|
+
}
|
|
676
|
+
|
|
592
677
|
/** Restore the latest branch-local command decision; custom entries stay out of LLM context. */
|
|
593
678
|
export function restoreAnthropicSessionCacheRetention(
|
|
594
679
|
entries: readonly unknown[],
|
|
@@ -715,8 +800,48 @@ function inspectCacheControls(payload: JsonObject): CacheControlInspection {
|
|
|
715
800
|
return { count, retention: count === 0 ? undefined : hasLong ? 'long' : 'short' };
|
|
716
801
|
}
|
|
717
802
|
|
|
803
|
+
interface CacheControlOccurrence {
|
|
804
|
+
readonly path: string;
|
|
805
|
+
readonly control: AnthropicCacheControl;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
function cacheControlOccurrences(payload: JsonObject): CacheControlOccurrence[] {
|
|
809
|
+
const occurrences: CacheControlOccurrence[] = [];
|
|
810
|
+
const inspectBlocks = (value: unknown, path: string): void => {
|
|
811
|
+
if (!Array.isArray(value)) return;
|
|
812
|
+
value.forEach((block, index) => {
|
|
813
|
+
if (!isPlainObject(block)) return;
|
|
814
|
+
const blockPath = `${path}[${String(index)}]`;
|
|
815
|
+
if (block['cache_control'] !== undefined) {
|
|
816
|
+
const control = cloneAnthropicCacheControl(block['cache_control']);
|
|
817
|
+
if (control !== undefined) occurrences.push({ path: blockPath, control });
|
|
818
|
+
}
|
|
819
|
+
if (block['type'] === 'tool_result') inspectBlocks(block['content'], `${blockPath}.content`);
|
|
820
|
+
});
|
|
821
|
+
};
|
|
822
|
+
|
|
823
|
+
if (payload['cache_control'] !== undefined) {
|
|
824
|
+
const control = cloneAnthropicCacheControl(payload['cache_control']);
|
|
825
|
+
if (control !== undefined) occurrences.push({ path: '$', control });
|
|
826
|
+
}
|
|
827
|
+
inspectBlocks(payload['system'], '$.system');
|
|
828
|
+
inspectBlocks(payload['tools'], '$.tools');
|
|
829
|
+
const messages = payload['messages'];
|
|
830
|
+
if (Array.isArray(messages)) {
|
|
831
|
+
messages.forEach((message, index) => {
|
|
832
|
+
if (isPlainObject(message))
|
|
833
|
+
inspectBlocks(message['content'], `$.messages[${String(index)}].content`);
|
|
834
|
+
});
|
|
835
|
+
}
|
|
836
|
+
return occurrences;
|
|
837
|
+
}
|
|
838
|
+
|
|
718
839
|
function countCacheControlBreakpoints(payload: JsonObject): number {
|
|
719
|
-
return
|
|
840
|
+
return cacheControlOccurrences(payload).length;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
function cacheControlTopologySha256(payload: JsonObject): string {
|
|
844
|
+
return sha256Canonical(cacheControlOccurrences(payload));
|
|
720
845
|
}
|
|
721
846
|
|
|
722
847
|
function assertCacheControlBreakpointLimit(payload: JsonObject): void {
|
|
@@ -760,20 +885,43 @@ export function extractClaudeAttributionAccount(
|
|
|
760
885
|
};
|
|
761
886
|
}
|
|
762
887
|
|
|
888
|
+
export function resolveClaudeAttributionConfigPath(
|
|
889
|
+
explicitConfigPath?: string,
|
|
890
|
+
env?: ProviderEnv,
|
|
891
|
+
homeDirectory = homedir(),
|
|
892
|
+
): string {
|
|
893
|
+
const configuredPath = providerEnvValue(ANTHROPIC_ACCOUNT_CONFIG_PATH_ENV, env);
|
|
894
|
+
const selectedPath = explicitConfigPath ?? configuredPath ?? join(homeDirectory, '.claude.json');
|
|
895
|
+
const source =
|
|
896
|
+
explicitConfigPath !== undefined
|
|
897
|
+
? 'explicit configPath'
|
|
898
|
+
: configuredPath !== undefined
|
|
899
|
+
? ANTHROPIC_ACCOUNT_CONFIG_PATH_ENV
|
|
900
|
+
: 'default ~/.claude.json path';
|
|
901
|
+
if (selectedPath.trim().length === 0 || !isAbsolute(selectedPath)) {
|
|
902
|
+
throw new Error(
|
|
903
|
+
`Anthropic attribution ${source} must be a non-empty absolute file path; got ${JSON.stringify(selectedPath)}`,
|
|
904
|
+
);
|
|
905
|
+
}
|
|
906
|
+
return selectedPath;
|
|
907
|
+
}
|
|
908
|
+
|
|
763
909
|
export function loadClaudeAttributionAccount(
|
|
764
|
-
configPath
|
|
910
|
+
configPath?: string,
|
|
911
|
+
env?: ProviderEnv,
|
|
765
912
|
): ClaudeAttributionAccount {
|
|
913
|
+
const selectedPath = resolveClaudeAttributionConfigPath(configPath, env);
|
|
766
914
|
let configText: string;
|
|
767
915
|
try {
|
|
768
|
-
configText = readFileSync(
|
|
916
|
+
configText = readFileSync(selectedPath, 'utf8');
|
|
769
917
|
} catch (error) {
|
|
770
918
|
throw new Error(
|
|
771
|
-
`Anthropic attribution config ${
|
|
919
|
+
`Anthropic attribution config ${selectedPath} could not be read: ${error instanceof Error ? error.message : String(error)}`,
|
|
772
920
|
);
|
|
773
921
|
}
|
|
774
922
|
return extractClaudeAttributionAccount(
|
|
775
|
-
parseJsonValue(configText, `Anthropic attribution config ${
|
|
776
|
-
|
|
923
|
+
parseJsonValue(configText, `Anthropic attribution config ${selectedPath}`),
|
|
924
|
+
selectedPath,
|
|
777
925
|
);
|
|
778
926
|
}
|
|
779
927
|
|
|
@@ -781,12 +929,15 @@ export function isAnthropicContext(ctx: PiContextLike): boolean {
|
|
|
781
929
|
return ctx.model?.provider === 'anthropic';
|
|
782
930
|
}
|
|
783
931
|
|
|
784
|
-
function
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
throw new Error('Anthropic attribution requires a non-empty Pi session id');
|
|
932
|
+
function requireSessionId(value: unknown, source: string): string {
|
|
933
|
+
if (typeof value !== 'string' || value.trim().length === 0) {
|
|
934
|
+
throw new Error(`Anthropic attribution requires a non-empty ${source}`);
|
|
788
935
|
}
|
|
789
|
-
return
|
|
936
|
+
return value;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
function getSessionId(ctx: PiContextLike): string {
|
|
940
|
+
return requireSessionId(ctx.sessionManager.getSessionId(), 'Pi session id');
|
|
790
941
|
}
|
|
791
942
|
|
|
792
943
|
function normalizedAnthropicModelId(model: PiModelLike): string {
|
|
@@ -867,16 +1018,22 @@ export function registerAnthropicAttributionProvider(
|
|
|
867
1018
|
pi: PiProviderRegistrationHost,
|
|
868
1019
|
ctx: PiContextLike,
|
|
869
1020
|
getSessionOverride: () => Exclude<CacheRetention, 'none'> | undefined = () => undefined,
|
|
1021
|
+
dependencies: AnthropicTransportDependencies = {},
|
|
870
1022
|
): void {
|
|
871
1023
|
if (!isAnthropicContext(ctx)) return;
|
|
872
1024
|
pi.registerProvider('anthropic', {
|
|
873
1025
|
api: 'anthropic-messages',
|
|
874
1026
|
headers: buildAnthropicAttributionHeaders(getSessionId(ctx), ctx.model),
|
|
875
1027
|
streamSimple: (model, context, options) =>
|
|
876
|
-
streamAnthropicViaBetaMessages(
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
1028
|
+
streamAnthropicViaBetaMessages(
|
|
1029
|
+
model,
|
|
1030
|
+
context,
|
|
1031
|
+
{
|
|
1032
|
+
...(options ?? {}),
|
|
1033
|
+
cacheRetention: resolveRegisteredCacheRetention(options, getSessionOverride()),
|
|
1034
|
+
},
|
|
1035
|
+
dependencies,
|
|
1036
|
+
),
|
|
880
1037
|
});
|
|
881
1038
|
}
|
|
882
1039
|
|
|
@@ -1031,26 +1188,50 @@ function appendAuditRecord(args: {
|
|
|
1031
1188
|
appendFileSync(auditPath, `${JSON.stringify(record)}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
1032
1189
|
}
|
|
1033
1190
|
|
|
1034
|
-
|
|
1191
|
+
function requireAttributionAccount(value: unknown): ClaudeAttributionAccount {
|
|
1192
|
+
if (
|
|
1193
|
+
!isPlainObject(value) ||
|
|
1194
|
+
typeof value['deviceId'] !== 'string' ||
|
|
1195
|
+
value['deviceId'].trim().length === 0 ||
|
|
1196
|
+
typeof value['accountUuid'] !== 'string' ||
|
|
1197
|
+
value['accountUuid'].trim().length === 0
|
|
1198
|
+
) {
|
|
1199
|
+
throw new Error('Anthropic attribution account loader returned malformed account identity');
|
|
1200
|
+
}
|
|
1201
|
+
return { deviceId: value['deviceId'], accountUuid: value['accountUuid'] };
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
function anthropicMetadataUserId(account: ClaudeAttributionAccount, sessionId: string): string {
|
|
1205
|
+
return JSON.stringify({
|
|
1206
|
+
account_uuid: account.accountUuid,
|
|
1207
|
+
device_id: account.deviceId,
|
|
1208
|
+
session_id: sessionId,
|
|
1209
|
+
});
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
interface AnthropicAttributionForSessionArgs {
|
|
1035
1213
|
readonly payload: unknown;
|
|
1036
|
-
readonly
|
|
1214
|
+
readonly model: PiModelLike;
|
|
1215
|
+
readonly sessionId: string;
|
|
1037
1216
|
readonly account: ClaudeAttributionAccount;
|
|
1038
1217
|
readonly headerRegistered?: boolean;
|
|
1039
1218
|
readonly cacheRetention?: CacheRetention;
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
function rewriteAnthropicRequestPayloadForSession(
|
|
1222
|
+
args: AnthropicAttributionForSessionArgs,
|
|
1223
|
+
): JsonObject {
|
|
1043
1224
|
if (!isPlainObject(args.payload)) {
|
|
1044
1225
|
throw new Error('Anthropic attribution expected provider payload to be a JSON object');
|
|
1045
1226
|
}
|
|
1046
1227
|
|
|
1047
|
-
const sessionId =
|
|
1228
|
+
const sessionId = requireSessionId(args.sessionId, 'provider options.sessionId');
|
|
1048
1229
|
const metadata = args.payload['metadata'] === undefined ? {} : args.payload['metadata'];
|
|
1049
1230
|
if (!isPlainObject(metadata)) {
|
|
1050
1231
|
throw new Error('Anthropic attribution expected payload.metadata to be an object when present');
|
|
1051
1232
|
}
|
|
1052
1233
|
|
|
1053
|
-
const policy = resolveClaudeCodeModelPolicy(args.
|
|
1234
|
+
const policy = resolveClaudeCodeModelPolicy(args.model);
|
|
1054
1235
|
const maxTokens =
|
|
1055
1236
|
args.payload['max_tokens'] === undefined
|
|
1056
1237
|
? undefined
|
|
@@ -1068,7 +1249,7 @@ export function rewriteAnthropicRequestPayload(args: {
|
|
|
1068
1249
|
const cacheControl =
|
|
1069
1250
|
configuredCacheRetention === undefined
|
|
1070
1251
|
? undefined
|
|
1071
|
-
: resolveAnthropicCacheControl(args.
|
|
1252
|
+
: resolveAnthropicCacheControl(args.model, {
|
|
1072
1253
|
cacheRetention: configuredCacheRetention,
|
|
1073
1254
|
});
|
|
1074
1255
|
|
|
@@ -1076,11 +1257,7 @@ export function rewriteAnthropicRequestPayload(args: {
|
|
|
1076
1257
|
...args.payload,
|
|
1077
1258
|
metadata: {
|
|
1078
1259
|
...metadata,
|
|
1079
|
-
user_id:
|
|
1080
|
-
account_uuid: args.account.accountUuid,
|
|
1081
|
-
device_id: args.account.deviceId,
|
|
1082
|
-
session_id: sessionId,
|
|
1083
|
-
}),
|
|
1260
|
+
user_id: anthropicMetadataUserId(args.account, sessionId),
|
|
1084
1261
|
},
|
|
1085
1262
|
system: withClaudeCodeSystemIdentity(args.payload['system'], billingSystemText, cacheControl),
|
|
1086
1263
|
};
|
|
@@ -1101,24 +1278,111 @@ export function rewriteAnthropicRequestPayload(args: {
|
|
|
1101
1278
|
return rewritten;
|
|
1102
1279
|
}
|
|
1103
1280
|
|
|
1281
|
+
function assertProtectedAnthropicAttribution(args: {
|
|
1282
|
+
readonly payload: JsonObject;
|
|
1283
|
+
readonly account: ClaudeAttributionAccount;
|
|
1284
|
+
readonly sessionId: string;
|
|
1285
|
+
readonly billingSystemText: string;
|
|
1286
|
+
readonly modelId: string;
|
|
1287
|
+
readonly cacheControlTopologySha256: string;
|
|
1288
|
+
}): void {
|
|
1289
|
+
if (args.payload['model'] !== args.modelId || args.payload['stream'] !== true) {
|
|
1290
|
+
throw new Error(
|
|
1291
|
+
'Anthropic protected attribution model/stream route changed during payload middleware',
|
|
1292
|
+
);
|
|
1293
|
+
}
|
|
1294
|
+
const metadata = args.payload['metadata'];
|
|
1295
|
+
if (
|
|
1296
|
+
!isPlainObject(metadata) ||
|
|
1297
|
+
metadata['user_id'] !== anthropicMetadataUserId(args.account, args.sessionId)
|
|
1298
|
+
) {
|
|
1299
|
+
throw new Error(
|
|
1300
|
+
'Anthropic protected attribution metadata.user_id/account/device/session_id changed during payload middleware',
|
|
1301
|
+
);
|
|
1302
|
+
}
|
|
1303
|
+
const system = args.payload['system'];
|
|
1304
|
+
if (!Array.isArray(system)) {
|
|
1305
|
+
throw new Error(
|
|
1306
|
+
'Anthropic protected attribution system identity was removed during payload middleware',
|
|
1307
|
+
);
|
|
1308
|
+
}
|
|
1309
|
+
const billingBlocks = system.filter(
|
|
1310
|
+
(block) =>
|
|
1311
|
+
isPlainObject(block) &&
|
|
1312
|
+
typeof block['text'] === 'string' &&
|
|
1313
|
+
block['text'].startsWith('x-anthropic-billing-header:'),
|
|
1314
|
+
);
|
|
1315
|
+
const sdkBlocks = system.filter(
|
|
1316
|
+
(block) => isPlainObject(block) && block['text'] === CLAUDE_AGENT_SDK_SYSTEM_TEXT,
|
|
1317
|
+
);
|
|
1318
|
+
if (
|
|
1319
|
+
billingBlocks.length !== 1 ||
|
|
1320
|
+
billingBlocks[0]?.['text'] !== args.billingSystemText ||
|
|
1321
|
+
sdkBlocks.length !== 1 ||
|
|
1322
|
+
system[0] !== billingBlocks[0] ||
|
|
1323
|
+
system[1] !== sdkBlocks[0]
|
|
1324
|
+
) {
|
|
1325
|
+
throw new Error(
|
|
1326
|
+
'Anthropic protected attribution system identity changed during payload middleware',
|
|
1327
|
+
);
|
|
1328
|
+
}
|
|
1329
|
+
assertCacheControlBreakpointLimit(args.payload);
|
|
1330
|
+
if (cacheControlTopologySha256(args.payload) !== args.cacheControlTopologySha256) {
|
|
1331
|
+
throw new Error('Anthropic protected cache-control topology changed during payload middleware');
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
export function rewriteAnthropicRequestPayload(args: {
|
|
1336
|
+
readonly payload: unknown;
|
|
1337
|
+
readonly ctx: PiContextLike;
|
|
1338
|
+
readonly account: ClaudeAttributionAccount;
|
|
1339
|
+
readonly headerRegistered?: boolean;
|
|
1340
|
+
readonly cacheRetention?: CacheRetention;
|
|
1341
|
+
readonly env?: ProviderEnv;
|
|
1342
|
+
}): unknown {
|
|
1343
|
+
if (!isAnthropicContext(args.ctx)) return undefined;
|
|
1344
|
+
return rewriteAnthropicRequestPayloadForSession({
|
|
1345
|
+
payload: args.payload,
|
|
1346
|
+
model: args.ctx.model ?? {},
|
|
1347
|
+
sessionId: getSessionId(args.ctx),
|
|
1348
|
+
account: args.account,
|
|
1349
|
+
...(args.headerRegistered === undefined ? {} : { headerRegistered: args.headerRegistered }),
|
|
1350
|
+
...(args.cacheRetention === undefined ? {} : { cacheRetention: args.cacheRetention }),
|
|
1351
|
+
});
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1104
1354
|
function sanitizeSurrogates(text: string): string {
|
|
1105
|
-
|
|
1355
|
+
let sanitized = '';
|
|
1356
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
1357
|
+
const code = text.charCodeAt(index);
|
|
1358
|
+
if (code >= 0xd800 && code <= 0xdbff) {
|
|
1359
|
+
const next = index + 1 < text.length ? text.charCodeAt(index + 1) : -1;
|
|
1360
|
+
if (next >= 0xdc00 && next <= 0xdfff) {
|
|
1361
|
+
sanitized += text[index] ?? '';
|
|
1362
|
+
sanitized += text[index + 1] ?? '';
|
|
1363
|
+
index += 1;
|
|
1364
|
+
} else {
|
|
1365
|
+
sanitized += '\uFFFD';
|
|
1366
|
+
}
|
|
1367
|
+
} else if (code >= 0xdc00 && code <= 0xdfff) {
|
|
1368
|
+
sanitized += '\uFFFD';
|
|
1369
|
+
} else {
|
|
1370
|
+
sanitized += text[index] ?? '';
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
return sanitized;
|
|
1106
1374
|
}
|
|
1107
1375
|
|
|
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) => {
|
|
1376
|
+
function convertContentBlocks(content: readonly PiContentBlock[]): JsonObject[] {
|
|
1377
|
+
const blocks: JsonObject[] = content.map((block) => {
|
|
1115
1378
|
if (block.type === 'text') return { type: 'text', text: sanitizeSurrogates(block.text) };
|
|
1116
1379
|
return {
|
|
1117
1380
|
type: 'image',
|
|
1118
1381
|
source: { type: 'base64', media_type: block.mimeType, data: block.data },
|
|
1119
1382
|
};
|
|
1120
1383
|
});
|
|
1121
|
-
if (
|
|
1384
|
+
if (blocks.length === 0) blocks.push({ type: 'text', text: '' });
|
|
1385
|
+
if (!blocks.some((block) => block['type'] === 'text'))
|
|
1122
1386
|
blocks.unshift({ type: 'text', text: '(see attached image)' });
|
|
1123
1387
|
return blocks;
|
|
1124
1388
|
}
|
|
@@ -1146,9 +1410,9 @@ function markMessageContentCacheSurface(
|
|
|
1146
1410
|
if (role !== 'user' && role !== 'assistant') return false;
|
|
1147
1411
|
const content = message['content'];
|
|
1148
1412
|
if (typeof content === 'string') {
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1413
|
+
throw new Error(
|
|
1414
|
+
'Anthropic attribution cache projection encountered non-canonical string message content',
|
|
1415
|
+
);
|
|
1152
1416
|
}
|
|
1153
1417
|
if (!Array.isArray(content)) return false;
|
|
1154
1418
|
for (let index = content.length - 1; index >= 0; index -= 1) {
|
|
@@ -1173,92 +1437,448 @@ function markLastConversationCacheSurface(
|
|
|
1173
1437
|
return output;
|
|
1174
1438
|
}
|
|
1175
1439
|
|
|
1440
|
+
function canonicalizeJson(value: unknown): unknown {
|
|
1441
|
+
if (Array.isArray(value)) return value.map(canonicalizeJson);
|
|
1442
|
+
if (!isPlainObject(value)) return value;
|
|
1443
|
+
const output: JsonObject = {};
|
|
1444
|
+
for (const key of Object.keys(value).sort()) {
|
|
1445
|
+
const child = value[key];
|
|
1446
|
+
if (child !== undefined) output[key] = canonicalizeJson(child);
|
|
1447
|
+
}
|
|
1448
|
+
return output;
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
function canonicalJson(value: unknown): string {
|
|
1452
|
+
return JSON.stringify(canonicalizeJson(value));
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
function sha256Canonical(value: unknown): string {
|
|
1456
|
+
return createHash('sha256').update(canonicalJson(value), 'utf8').digest('hex');
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
function isSha256(value: unknown): value is string {
|
|
1460
|
+
return typeof value === 'string' && /^[a-f0-9]{64}$/u.test(value);
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
function isNonEmptyString(value: unknown): value is string {
|
|
1464
|
+
return typeof value === 'string' && value.trim().length > 0;
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
function blockWithoutCacheControl(block: unknown): unknown {
|
|
1468
|
+
if (!isPlainObject(block)) return block;
|
|
1469
|
+
const output = { ...block };
|
|
1470
|
+
delete output['cache_control'];
|
|
1471
|
+
return output;
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
function promptMessagesWithoutCacheControls(messages: readonly JsonObject[]): JsonObject[] {
|
|
1475
|
+
return messages.map((message) => {
|
|
1476
|
+
const content = message['content'];
|
|
1477
|
+
return {
|
|
1478
|
+
...message,
|
|
1479
|
+
...(Array.isArray(content) ? { content: content.map(blockWithoutCacheControl) } : {}),
|
|
1480
|
+
};
|
|
1481
|
+
});
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
function promptMessagesHash(messages: readonly JsonObject[]): string {
|
|
1485
|
+
return sha256Canonical(promptMessagesWithoutCacheControls(messages));
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
function assistantContentHash(content: readonly JsonObject[]): string {
|
|
1489
|
+
return sha256Canonical(content);
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
interface AnthropicLineageDetails {
|
|
1493
|
+
readonly schema_version: typeof ANTHROPIC_LINEAGE_SCHEMA;
|
|
1494
|
+
readonly projection_version: typeof ANTHROPIC_PROJECTION_VERSION;
|
|
1495
|
+
readonly source_provider: 'anthropic';
|
|
1496
|
+
readonly source_api: 'anthropic-messages';
|
|
1497
|
+
readonly source_model: string;
|
|
1498
|
+
readonly response_id: string;
|
|
1499
|
+
readonly assistant_content_sha256: string;
|
|
1500
|
+
readonly conversation_static_sha256: string;
|
|
1501
|
+
readonly request_message_count: number;
|
|
1502
|
+
readonly request_messages_sha256: string;
|
|
1503
|
+
readonly cache_profile_sha256: string;
|
|
1504
|
+
readonly cache_retention: CacheRetention;
|
|
1505
|
+
readonly compaction_boundary_sha256: string | null;
|
|
1506
|
+
readonly signature_epoch_sha256: string;
|
|
1507
|
+
readonly signature_epoch_inherits_prior: boolean;
|
|
1508
|
+
readonly previous_message_id: string | null;
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
function parseAnthropicLineageDetails(
|
|
1512
|
+
message: Extract<PiMessage, { role: 'assistant' }>,
|
|
1513
|
+
): AnthropicLineageDetails | undefined {
|
|
1514
|
+
const diagnostic = [...(message.diagnostics ?? [])]
|
|
1515
|
+
.reverse()
|
|
1516
|
+
.find((candidate) => candidate.type === ANTHROPIC_LINEAGE_DIAGNOSTIC_TYPE);
|
|
1517
|
+
const details = diagnostic?.details;
|
|
1518
|
+
if (!isPlainObject(details)) return undefined;
|
|
1519
|
+
|
|
1520
|
+
const sourceModel = details['source_model'];
|
|
1521
|
+
const responseId = details['response_id'];
|
|
1522
|
+
const assistantContentSha256 = details['assistant_content_sha256'];
|
|
1523
|
+
const conversationStaticSha256 = details['conversation_static_sha256'];
|
|
1524
|
+
const requestMessageCount = details['request_message_count'];
|
|
1525
|
+
const requestMessagesSha256 = details['request_messages_sha256'];
|
|
1526
|
+
const cacheProfileSha256 = details['cache_profile_sha256'];
|
|
1527
|
+
const cacheRetention = details['cache_retention'];
|
|
1528
|
+
const compactionBoundarySha256 = details['compaction_boundary_sha256'];
|
|
1529
|
+
const signatureEpochSha256 = details['signature_epoch_sha256'];
|
|
1530
|
+
const signatureEpochInheritsPrior = details['signature_epoch_inherits_prior'];
|
|
1531
|
+
const previousMessageId = details['previous_message_id'];
|
|
1532
|
+
if (
|
|
1533
|
+
details['schema_version'] !== ANTHROPIC_LINEAGE_SCHEMA ||
|
|
1534
|
+
details['projection_version'] !== ANTHROPIC_PROJECTION_VERSION ||
|
|
1535
|
+
details['source_provider'] !== 'anthropic' ||
|
|
1536
|
+
details['source_api'] !== 'anthropic-messages' ||
|
|
1537
|
+
!isNonEmptyString(sourceModel) ||
|
|
1538
|
+
!isNonEmptyString(responseId) ||
|
|
1539
|
+
!isSha256(assistantContentSha256) ||
|
|
1540
|
+
!isSha256(conversationStaticSha256) ||
|
|
1541
|
+
typeof requestMessageCount !== 'number' ||
|
|
1542
|
+
!Number.isSafeInteger(requestMessageCount) ||
|
|
1543
|
+
requestMessageCount < 0 ||
|
|
1544
|
+
!isSha256(requestMessagesSha256) ||
|
|
1545
|
+
!isSha256(cacheProfileSha256) ||
|
|
1546
|
+
(cacheRetention !== 'none' && cacheRetention !== 'short' && cacheRetention !== 'long') ||
|
|
1547
|
+
(compactionBoundarySha256 !== null && !isSha256(compactionBoundarySha256)) ||
|
|
1548
|
+
!isSha256(signatureEpochSha256) ||
|
|
1549
|
+
typeof signatureEpochInheritsPrior !== 'boolean' ||
|
|
1550
|
+
(previousMessageId !== null && !isNonEmptyString(previousMessageId))
|
|
1551
|
+
) {
|
|
1552
|
+
return undefined;
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
return {
|
|
1556
|
+
schema_version: ANTHROPIC_LINEAGE_SCHEMA,
|
|
1557
|
+
projection_version: ANTHROPIC_PROJECTION_VERSION,
|
|
1558
|
+
source_provider: 'anthropic',
|
|
1559
|
+
source_api: 'anthropic-messages',
|
|
1560
|
+
source_model: sourceModel,
|
|
1561
|
+
response_id: responseId,
|
|
1562
|
+
assistant_content_sha256: assistantContentSha256,
|
|
1563
|
+
conversation_static_sha256: conversationStaticSha256,
|
|
1564
|
+
request_message_count: requestMessageCount,
|
|
1565
|
+
request_messages_sha256: requestMessagesSha256,
|
|
1566
|
+
cache_profile_sha256: cacheProfileSha256,
|
|
1567
|
+
cache_retention: cacheRetention,
|
|
1568
|
+
compaction_boundary_sha256: compactionBoundarySha256,
|
|
1569
|
+
signature_epoch_sha256: signatureEpochSha256,
|
|
1570
|
+
signature_epoch_inherits_prior: signatureEpochInheritsPrior,
|
|
1571
|
+
previous_message_id: previousMessageId,
|
|
1572
|
+
};
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
function isSuccessfulLineageStopReason(
|
|
1576
|
+
stopReason: string | undefined,
|
|
1577
|
+
): stopReason is 'stop' | 'length' | 'toolUse' {
|
|
1578
|
+
return stopReason === 'stop' || stopReason === 'length' || stopReason === 'toolUse';
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
function lineageBindsContainingAssistant(
|
|
1582
|
+
message: Extract<PiMessage, { role: 'assistant' }>,
|
|
1583
|
+
lineage: AnthropicLineageDetails,
|
|
1584
|
+
): boolean {
|
|
1585
|
+
return (
|
|
1586
|
+
message.provider === lineage.source_provider &&
|
|
1587
|
+
message.api === lineage.source_api &&
|
|
1588
|
+
isNonEmptyString(message.model) &&
|
|
1589
|
+
message.model === lineage.source_model &&
|
|
1590
|
+
isSuccessfulLineageStopReason(message.stopReason) &&
|
|
1591
|
+
isNonEmptyString(message.responseId) &&
|
|
1592
|
+
message.responseId === lineage.response_id &&
|
|
1593
|
+
lineage.assistant_content_sha256 === assistantContentHash(message.content)
|
|
1594
|
+
);
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
function canTargetReadAnthropicThinking(sourceModel: string, targetModel: string): boolean {
|
|
1598
|
+
if (sourceModel === targetModel) return true;
|
|
1599
|
+
return (
|
|
1600
|
+
targetModel === 'claude-fable-5-1' && CLAUDE_CODE_MODEL_POLICIES[sourceModel] !== undefined
|
|
1601
|
+
);
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
interface SignatureEpochPolicy {
|
|
1605
|
+
readonly sha256: string;
|
|
1606
|
+
readonly inheritsPrior: boolean;
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
function initialSignatureEpochPolicy(
|
|
1610
|
+
targetModelId: string,
|
|
1611
|
+
cacheRetention: CacheRetention,
|
|
1612
|
+
compactionBoundarySha256: string | null,
|
|
1613
|
+
): SignatureEpochPolicy {
|
|
1614
|
+
return {
|
|
1615
|
+
sha256: sha256Canonical({
|
|
1616
|
+
projection_version: ANTHROPIC_PROJECTION_VERSION,
|
|
1617
|
+
kind: 'initial',
|
|
1618
|
+
target_model: targetModelId,
|
|
1619
|
+
cache_retention: cacheRetention,
|
|
1620
|
+
compaction_boundary_sha256: compactionBoundarySha256,
|
|
1621
|
+
}),
|
|
1622
|
+
inheritsPrior: compactionBoundarySha256 === null,
|
|
1623
|
+
};
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
function resolveSignatureEpochPolicy(
|
|
1627
|
+
model: PiModelLike,
|
|
1628
|
+
messages: readonly PiMessage[],
|
|
1629
|
+
cacheRetention: CacheRetention,
|
|
1630
|
+
compactionBoundarySha256: string | null,
|
|
1631
|
+
): SignatureEpochPolicy {
|
|
1632
|
+
const targetModelId = normalizedAnthropicModelId(model);
|
|
1633
|
+
const latestState = targetLineageState({ messages }, targetModelId);
|
|
1634
|
+
if (latestState.kind === 'none') {
|
|
1635
|
+
return initialSignatureEpochPolicy(targetModelId, cacheRetention, compactionBoundarySha256);
|
|
1636
|
+
}
|
|
1637
|
+
if (latestState.kind === 'untrusted') {
|
|
1638
|
+
return {
|
|
1639
|
+
sha256: sha256Canonical({
|
|
1640
|
+
projection_version: ANTHROPIC_PROJECTION_VERSION,
|
|
1641
|
+
kind: 'untrusted-lineage-reset',
|
|
1642
|
+
target_model: targetModelId,
|
|
1643
|
+
cache_retention: cacheRetention,
|
|
1644
|
+
compaction_boundary_sha256: compactionBoundarySha256,
|
|
1645
|
+
latest_assistant_sha256: latestState.assistantSha256,
|
|
1646
|
+
}),
|
|
1647
|
+
inheritsPrior: false,
|
|
1648
|
+
};
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
const latest = latestState.lineage;
|
|
1652
|
+
const retentionChanged = latest.cache_retention !== cacheRetention;
|
|
1653
|
+
const declaredNewCompaction =
|
|
1654
|
+
compactionBoundarySha256 !== null &&
|
|
1655
|
+
compactionBoundarySha256 !== latest.compaction_boundary_sha256;
|
|
1656
|
+
if (retentionChanged || declaredNewCompaction) {
|
|
1657
|
+
return {
|
|
1658
|
+
sha256: sha256Canonical({
|
|
1659
|
+
projection_version: ANTHROPIC_PROJECTION_VERSION,
|
|
1660
|
+
kind: 'transition',
|
|
1661
|
+
previous_signature_epoch_sha256: latest.signature_epoch_sha256,
|
|
1662
|
+
target_model: targetModelId,
|
|
1663
|
+
cache_retention: cacheRetention,
|
|
1664
|
+
compaction_boundary_sha256: compactionBoundarySha256,
|
|
1665
|
+
reason: retentionChanged ? 'cache-retention' : 'compaction',
|
|
1666
|
+
}),
|
|
1667
|
+
inheritsPrior: false,
|
|
1668
|
+
};
|
|
1669
|
+
}
|
|
1670
|
+
return {
|
|
1671
|
+
sha256: latest.signature_epoch_sha256,
|
|
1672
|
+
inheritsPrior: latest.signature_epoch_inherits_prior,
|
|
1673
|
+
};
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
function isTrustedReplayableAnthropicAssistant(
|
|
1677
|
+
message: Extract<PiMessage, { role: 'assistant' }>,
|
|
1678
|
+
targetModel: PiModelLike,
|
|
1679
|
+
targetCacheRetention: CacheRetention,
|
|
1680
|
+
conversationStaticSha256: string,
|
|
1681
|
+
wireMessagesBeforeAssistant: readonly JsonObject[],
|
|
1682
|
+
): boolean {
|
|
1683
|
+
if (typeof message.model !== 'string') return false;
|
|
1684
|
+
const lineage = parseAnthropicLineageDetails(message);
|
|
1685
|
+
if (
|
|
1686
|
+
lineage === undefined ||
|
|
1687
|
+
!lineageBindsContainingAssistant(message, lineage) ||
|
|
1688
|
+
!canTargetReadAnthropicThinking(message.model, normalizedAnthropicModelId(targetModel)) ||
|
|
1689
|
+
lineage.cache_retention !== targetCacheRetention ||
|
|
1690
|
+
lineage.conversation_static_sha256 !== conversationStaticSha256 ||
|
|
1691
|
+
lineage.request_message_count !== wireMessagesBeforeAssistant.length ||
|
|
1692
|
+
lineage.request_messages_sha256 !== promptMessagesHash(wireMessagesBeforeAssistant)
|
|
1693
|
+
) {
|
|
1694
|
+
return false;
|
|
1695
|
+
}
|
|
1696
|
+
return true;
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
function normalizeAnthropicToolCallId(id: string): string {
|
|
1700
|
+
if (/^[a-zA-Z0-9_-]{1,64}$/u.test(id)) return id;
|
|
1701
|
+
const safe = id.replace(/[^a-zA-Z0-9_-]/gu, '_');
|
|
1702
|
+
const digest = createHash('sha256').update(id, 'utf8').digest('hex').slice(0, 12);
|
|
1703
|
+
const prefix = safe.slice(0, 64 - digest.length - 1) || 'tool';
|
|
1704
|
+
return `${prefix}_${digest}`.slice(0, 64);
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
function projectedUserContent(content: string | readonly PiContentBlock[]): JsonObject[] {
|
|
1708
|
+
if (typeof content === 'string') {
|
|
1709
|
+
return content.trim().length > 0 ? [{ type: 'text', text: sanitizeSurrogates(content) }] : [];
|
|
1710
|
+
}
|
|
1711
|
+
const blocks: JsonObject[] = [];
|
|
1712
|
+
for (const block of content) {
|
|
1713
|
+
if (block.type === 'text') {
|
|
1714
|
+
if (block.text.trim().length > 0)
|
|
1715
|
+
blocks.push({ type: 'text', text: sanitizeSurrogates(block.text) });
|
|
1716
|
+
continue;
|
|
1717
|
+
}
|
|
1718
|
+
if (block.type === 'image') {
|
|
1719
|
+
blocks.push({
|
|
1720
|
+
type: 'image',
|
|
1721
|
+
source: { type: 'base64', media_type: block.mimeType, data: block.data },
|
|
1722
|
+
});
|
|
1723
|
+
continue;
|
|
1724
|
+
}
|
|
1725
|
+
throw new Error('Anthropic attribution encountered an unsupported user content block');
|
|
1726
|
+
}
|
|
1727
|
+
return blocks;
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
function compactionBoundarySha256FromPiMessages(messages: readonly PiMessage[]): string | null {
|
|
1731
|
+
const first = messages[0];
|
|
1732
|
+
if (first?.role !== 'user') return null;
|
|
1733
|
+
const content = projectedUserContent(first.content);
|
|
1734
|
+
return declaredCompactionBoundarySha256([{ role: 'user', content }]);
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1176
1737
|
function convertMessages(
|
|
1738
|
+
model: PiModelLike,
|
|
1177
1739
|
messages: readonly PiMessage[],
|
|
1740
|
+
conversationStaticSha256: string,
|
|
1741
|
+
cacheRetention: CacheRetention,
|
|
1742
|
+
signatureEpoch: SignatureEpochPolicy,
|
|
1178
1743
|
cacheControl?: AnthropicCacheControl,
|
|
1179
1744
|
): JsonObject[] {
|
|
1180
1745
|
const params: JsonObject[] = [];
|
|
1746
|
+
const toolCallIdMap = new Map<string, string>();
|
|
1747
|
+
const normalizedToolCallOwners = new Map<string, string>();
|
|
1748
|
+
let pendingToolCalls: Array<{ readonly id: string; readonly name: string }> = [];
|
|
1749
|
+
let completedPendingToolCallIds = new Set<string>();
|
|
1750
|
+
|
|
1751
|
+
const normalizeToolCallId = (id: string): string => {
|
|
1752
|
+
const existing = toolCallIdMap.get(id);
|
|
1753
|
+
if (existing !== undefined) return existing;
|
|
1754
|
+
const normalized = normalizeAnthropicToolCallId(id);
|
|
1755
|
+
const owner = normalizedToolCallOwners.get(normalized);
|
|
1756
|
+
if (owner !== undefined && owner !== id) {
|
|
1757
|
+
throw new Error('Anthropic attribution tool-call ID normalization collision');
|
|
1758
|
+
}
|
|
1759
|
+
normalizedToolCallOwners.set(normalized, id);
|
|
1760
|
+
toolCallIdMap.set(id, normalized);
|
|
1761
|
+
return normalized;
|
|
1762
|
+
};
|
|
1763
|
+
|
|
1764
|
+
const flushMissingToolResults = (): void => {
|
|
1765
|
+
const missing = pendingToolCalls.filter((call) => !completedPendingToolCallIds.has(call.id));
|
|
1766
|
+
if (missing.length > 0) {
|
|
1767
|
+
params.push({
|
|
1768
|
+
role: 'user',
|
|
1769
|
+
content: missing.map((call) => ({
|
|
1770
|
+
type: 'tool_result',
|
|
1771
|
+
tool_use_id: call.id,
|
|
1772
|
+
content: 'No result provided',
|
|
1773
|
+
is_error: true,
|
|
1774
|
+
})),
|
|
1775
|
+
});
|
|
1776
|
+
}
|
|
1777
|
+
pendingToolCalls = [];
|
|
1778
|
+
completedPendingToolCallIds = new Set<string>();
|
|
1779
|
+
};
|
|
1780
|
+
|
|
1181
1781
|
for (let index = 0; index < messages.length; index += 1) {
|
|
1182
1782
|
const message = messages[index];
|
|
1183
|
-
if (message === undefined) {
|
|
1184
|
-
|
|
1185
|
-
}
|
|
1783
|
+
if (message === undefined) throw new TypeError(`Anthropic message ${index} is missing`);
|
|
1784
|
+
|
|
1186
1785
|
if (message.role === 'user') {
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1786
|
+
flushMissingToolResults();
|
|
1787
|
+
const content = projectedUserContent(message.content);
|
|
1788
|
+
if (content.length > 0) params.push({ role: 'user', content });
|
|
1789
|
+
continue;
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
if (message.role === 'assistant') {
|
|
1793
|
+
flushMissingToolResults();
|
|
1794
|
+
if (message.stopReason === 'error' || message.stopReason === 'aborted') continue;
|
|
1795
|
+
const lineage = parseAnthropicLineageDetails(message);
|
|
1796
|
+
const epochAllowsReplay =
|
|
1797
|
+
signatureEpoch.inheritsPrior || lineage?.signature_epoch_sha256 === signatureEpoch.sha256;
|
|
1798
|
+
const preserveThinking =
|
|
1799
|
+
epochAllowsReplay &&
|
|
1800
|
+
isTrustedReplayableAnthropicAssistant(
|
|
1801
|
+
message,
|
|
1802
|
+
model,
|
|
1803
|
+
cacheRetention,
|
|
1804
|
+
conversationStaticSha256,
|
|
1805
|
+
params,
|
|
1806
|
+
);
|
|
1204
1807
|
const content: JsonObject[] = [];
|
|
1205
1808
|
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
|
-
) {
|
|
1809
|
+
if (block['type'] === 'text') {
|
|
1810
|
+
if (typeof block['text'] !== 'string')
|
|
1811
|
+
throw new Error('Anthropic attribution encountered malformed assistant text');
|
|
1812
|
+
if (block['text'].trim().length > 0)
|
|
1813
|
+
content.push({ type: 'text', text: sanitizeSurrogates(block['text']) });
|
|
1814
|
+
continue;
|
|
1815
|
+
}
|
|
1816
|
+
if (block['type'] === 'thinking') {
|
|
1817
|
+
if (typeof block['thinking'] !== 'string')
|
|
1818
|
+
throw new Error('Anthropic attribution encountered malformed assistant thinking');
|
|
1217
1819
|
const signature =
|
|
1218
1820
|
typeof block['thinkingSignature'] === 'string' ? block['thinkingSignature'] : '';
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1821
|
+
if (preserveThinking && signature.trim().length > 0) {
|
|
1822
|
+
content.push(
|
|
1823
|
+
block['redacted'] === true
|
|
1824
|
+
? { type: 'redacted_thinking', data: signature }
|
|
1825
|
+
: {
|
|
1826
|
+
type: 'thinking',
|
|
1827
|
+
// A signature authenticates the exact provider-returned reasoning bytes.
|
|
1828
|
+
// Provenance and lineage checks above make this raw replay safe; changing
|
|
1829
|
+
// even valid non-BMP Unicode here would invalidate the opaque signature.
|
|
1830
|
+
thinking: block['thinking'],
|
|
1831
|
+
signature,
|
|
1832
|
+
},
|
|
1833
|
+
);
|
|
1834
|
+
} else if (block['redacted'] !== true && block['thinking'].trim().length > 0) {
|
|
1835
|
+
content.push({ type: 'text', text: sanitizeSurrogates(block['thinking']) });
|
|
1836
|
+
}
|
|
1837
|
+
continue;
|
|
1838
|
+
}
|
|
1839
|
+
if (block['type'] === 'toolCall') {
|
|
1840
|
+
if (typeof block['id'] !== 'string' || typeof block['name'] !== 'string')
|
|
1841
|
+
throw new Error('Anthropic attribution encountered malformed assistant tool call');
|
|
1842
|
+
const id = normalizeToolCallId(block['id']);
|
|
1229
1843
|
content.push({
|
|
1230
1844
|
type: 'tool_use',
|
|
1231
|
-
id
|
|
1845
|
+
id,
|
|
1232
1846
|
name: block['name'],
|
|
1233
|
-
input: block['arguments'] ?? {},
|
|
1847
|
+
input: canonicalizeJson(block['arguments'] ?? {}),
|
|
1234
1848
|
});
|
|
1849
|
+
continue;
|
|
1235
1850
|
}
|
|
1851
|
+
throw new Error(
|
|
1852
|
+
`Anthropic attribution encountered unsupported assistant block type ${JSON.stringify(block['type'])}`,
|
|
1853
|
+
);
|
|
1236
1854
|
}
|
|
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;
|
|
1855
|
+
if (content.length > 0) {
|
|
1856
|
+
params.push({ role: 'assistant', content });
|
|
1857
|
+
pendingToolCalls = content
|
|
1858
|
+
.filter((block) => block['type'] === 'tool_use')
|
|
1859
|
+
.map((block) => ({ id: String(block['id']), name: String(block['name']) }));
|
|
1257
1860
|
}
|
|
1258
|
-
|
|
1259
|
-
|
|
1861
|
+
continue;
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1864
|
+
const toolResults: JsonObject[] = [];
|
|
1865
|
+
let lookahead = index;
|
|
1866
|
+
while (lookahead < messages.length && messages[lookahead]?.role === 'toolResult') {
|
|
1867
|
+
const result = messages[lookahead] as Extract<PiMessage, { role: 'toolResult' }>;
|
|
1868
|
+
const toolUseId = normalizeToolCallId(result.toolCallId);
|
|
1869
|
+
completedPendingToolCallIds.add(toolUseId);
|
|
1870
|
+
toolResults.push({
|
|
1871
|
+
type: 'tool_result',
|
|
1872
|
+
tool_use_id: toolUseId,
|
|
1873
|
+
content: convertContentBlocks(result.content),
|
|
1874
|
+
is_error: result.isError === true,
|
|
1875
|
+
});
|
|
1876
|
+
lookahead += 1;
|
|
1260
1877
|
}
|
|
1878
|
+
index = lookahead - 1;
|
|
1879
|
+
if (toolResults.length > 0) params.push({ role: 'user', content: toolResults });
|
|
1261
1880
|
}
|
|
1881
|
+
flushMissingToolResults();
|
|
1262
1882
|
return markLastConversationCacheSurface(params, cacheControl);
|
|
1263
1883
|
}
|
|
1264
1884
|
|
|
@@ -1274,8 +1894,10 @@ function convertTools(
|
|
|
1274
1894
|
description: tool.description ?? '',
|
|
1275
1895
|
input_schema: {
|
|
1276
1896
|
type: 'object',
|
|
1277
|
-
properties:
|
|
1278
|
-
|
|
1897
|
+
properties: canonicalizeJson(
|
|
1898
|
+
isPlainObject(parameters['properties']) ? parameters['properties'] : {},
|
|
1899
|
+
),
|
|
1900
|
+
required: Array.isArray(parameters['required']) ? [...parameters['required']] : [],
|
|
1279
1901
|
},
|
|
1280
1902
|
};
|
|
1281
1903
|
return cacheControl !== undefined && index === tools.length - 1
|
|
@@ -1317,45 +1939,98 @@ function adaptiveEffortFor(
|
|
|
1317
1939
|
}
|
|
1318
1940
|
}
|
|
1319
1941
|
|
|
1942
|
+
function conversationStaticHash(system: readonly unknown[], tools: readonly JsonObject[]): string {
|
|
1943
|
+
const normalizedSystem = system
|
|
1944
|
+
.filter(
|
|
1945
|
+
(block) =>
|
|
1946
|
+
!isPlainObject(block) ||
|
|
1947
|
+
typeof block['text'] !== 'string' ||
|
|
1948
|
+
!isClaudeCodeIdentityText(block['text']),
|
|
1949
|
+
)
|
|
1950
|
+
.map(blockWithoutCacheControl);
|
|
1951
|
+
return sha256Canonical({
|
|
1952
|
+
attribution_profile: {
|
|
1953
|
+
claude_code_version: CLAUDE_CODE_VERSION,
|
|
1954
|
+
entrypoint: CLAUDE_CODE_ENTRYPOINT,
|
|
1955
|
+
},
|
|
1956
|
+
system: normalizedSystem,
|
|
1957
|
+
tools: tools.map(blockWithoutCacheControl),
|
|
1958
|
+
});
|
|
1959
|
+
}
|
|
1960
|
+
|
|
1320
1961
|
export function buildAnthropicRequestParams(
|
|
1321
1962
|
model: PiModelLike,
|
|
1322
1963
|
context: PiStreamContext,
|
|
1323
1964
|
options?: PiSimpleStreamOptions,
|
|
1324
1965
|
): JsonObject {
|
|
1966
|
+
return buildAnthropicRequest(model, context, options).params;
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1969
|
+
function buildAnthropicRequest(
|
|
1970
|
+
model: PiModelLike,
|
|
1971
|
+
context: PiStreamContext,
|
|
1972
|
+
options?: PiSimpleStreamOptions,
|
|
1973
|
+
): { readonly params: JsonObject; readonly signatureEpoch: SignatureEpochPolicy } {
|
|
1325
1974
|
const policy = resolveClaudeCodeModelPolicy(model);
|
|
1326
1975
|
const maxTokens = resolveAnthropicMaxTokens(model);
|
|
1327
1976
|
const cacheControl = resolveAnthropicCacheControl(model, options);
|
|
1977
|
+
const cacheRetention: CacheRetention =
|
|
1978
|
+
cacheControl === undefined ? 'none' : cacheControl.ttl === '1h' ? 'long' : 'short';
|
|
1979
|
+
let signatureEpoch = resolveSignatureEpochPolicy(
|
|
1980
|
+
model,
|
|
1981
|
+
context.messages,
|
|
1982
|
+
cacheRetention,
|
|
1983
|
+
compactionBoundarySha256FromPiMessages(context.messages),
|
|
1984
|
+
);
|
|
1985
|
+
const system =
|
|
1986
|
+
context.systemPrompt && context.systemPrompt.trim().length > 0
|
|
1987
|
+
? markSystemCacheSurface(
|
|
1988
|
+
[
|
|
1989
|
+
{
|
|
1990
|
+
type: 'text',
|
|
1991
|
+
text: sanitizeSurrogates(stripAnthropicSystemPromptBadLines(context.systemPrompt)),
|
|
1992
|
+
},
|
|
1993
|
+
],
|
|
1994
|
+
cacheControl,
|
|
1995
|
+
)
|
|
1996
|
+
: [];
|
|
1997
|
+
const tools = convertTools(
|
|
1998
|
+
context.tools,
|
|
1999
|
+
model.compat?.supportsCacheControlOnTools === false ? undefined : cacheControl,
|
|
2000
|
+
);
|
|
2001
|
+
const staticSha256 = conversationStaticHash(system, tools);
|
|
1328
2002
|
const params: JsonObject = {
|
|
1329
2003
|
model: policy.modelId,
|
|
1330
|
-
messages: convertMessages(
|
|
2004
|
+
messages: convertMessages(
|
|
2005
|
+
model,
|
|
2006
|
+
context.messages,
|
|
2007
|
+
staticSha256,
|
|
2008
|
+
cacheRetention,
|
|
2009
|
+
signatureEpoch,
|
|
2010
|
+
cacheControl,
|
|
2011
|
+
),
|
|
1331
2012
|
max_tokens: maxTokens,
|
|
1332
2013
|
stream: true,
|
|
2014
|
+
tools,
|
|
1333
2015
|
};
|
|
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;
|
|
2016
|
+
if (system.length > 0) params['system'] = system;
|
|
2017
|
+
if (options?.toolChoice !== undefined)
|
|
2018
|
+
params['tool_choice'] = canonicalizeJson(options.toolChoice);
|
|
1352
2019
|
const reasoning = options?.reasoning;
|
|
1353
2020
|
if (model.reasoning && reasoning !== undefined) {
|
|
1354
2021
|
if (reasoning === 'off') {
|
|
2022
|
+
if (policy.enforcesThinkingPrefixBinding) {
|
|
2023
|
+
throw new Error('Anthropic attribution cannot disable thinking for Claude Fable 5.1');
|
|
2024
|
+
}
|
|
1355
2025
|
params['thinking'] = { type: 'disabled' };
|
|
1356
2026
|
params['temperature'] = options?.temperature ?? 1;
|
|
1357
2027
|
} else if (policy.thinkingPolicy === 'adaptive-effort') {
|
|
1358
|
-
params['thinking'] = {
|
|
2028
|
+
params['thinking'] = {
|
|
2029
|
+
type: 'adaptive',
|
|
2030
|
+
...(policy.enforcesThinkingPrefixBinding
|
|
2031
|
+
? { block_binding: { prefix_mismatch_behavior: 'error' } }
|
|
2032
|
+
: {}),
|
|
2033
|
+
};
|
|
1359
2034
|
params['output_config'] = { effort: adaptiveEffortFor(reasoning) };
|
|
1360
2035
|
} else {
|
|
1361
2036
|
params['thinking'] = {
|
|
@@ -1363,12 +2038,364 @@ export function buildAnthropicRequestParams(
|
|
|
1363
2038
|
budget_tokens: thinkingBudgetFor(reasoning, maxTokens, options?.thinkingBudgets),
|
|
1364
2039
|
};
|
|
1365
2040
|
}
|
|
2041
|
+
} else if (policy.enforcesThinkingPrefixBinding) {
|
|
2042
|
+
params['thinking'] = {
|
|
2043
|
+
type: 'adaptive',
|
|
2044
|
+
block_binding: { prefix_mismatch_behavior: 'error' },
|
|
2045
|
+
};
|
|
1366
2046
|
} else {
|
|
1367
2047
|
params['thinking'] = { type: 'disabled' };
|
|
1368
2048
|
params['temperature'] = options?.temperature ?? 1;
|
|
1369
2049
|
}
|
|
2050
|
+
const previous = latestLineageForTarget(context, normalizedAnthropicModelId(model));
|
|
2051
|
+
if (
|
|
2052
|
+
previous !== undefined &&
|
|
2053
|
+
signatureEpoch.sha256 === previous.signature_epoch_sha256 &&
|
|
2054
|
+
(previous.cache_profile_sha256 !== cacheProfileHash(model, policy, params, staticSha256) ||
|
|
2055
|
+
!hasLineageRequestPrefix(requestMessagesFromPayload(params), previous))
|
|
2056
|
+
) {
|
|
2057
|
+
// Resume/reload and profile edits can invalidate a lane. Re-project before transport,
|
|
2058
|
+
// rather than just clearing the response ID while retaining prefix-bound signatures.
|
|
2059
|
+
signatureEpoch = {
|
|
2060
|
+
sha256: sha256Canonical({
|
|
2061
|
+
projection_version: ANTHROPIC_PROJECTION_VERSION,
|
|
2062
|
+
kind: 'lineage-reset',
|
|
2063
|
+
previous_signature_epoch_sha256: previous.signature_epoch_sha256,
|
|
2064
|
+
previous_response_id: previous.response_id,
|
|
2065
|
+
cache_profile_sha256: cacheProfileHash(model, policy, params, staticSha256),
|
|
2066
|
+
request_messages_sha256: promptMessagesHash(requestMessagesFromPayload(params)),
|
|
2067
|
+
}),
|
|
2068
|
+
inheritsPrior: false,
|
|
2069
|
+
};
|
|
2070
|
+
params['messages'] = convertMessages(
|
|
2071
|
+
model,
|
|
2072
|
+
context.messages,
|
|
2073
|
+
staticSha256,
|
|
2074
|
+
cacheRetention,
|
|
2075
|
+
signatureEpoch,
|
|
2076
|
+
cacheControl,
|
|
2077
|
+
);
|
|
2078
|
+
}
|
|
1370
2079
|
assertCacheControlBreakpointLimit(params);
|
|
1371
|
-
return params;
|
|
2080
|
+
return { params, signatureEpoch };
|
|
2081
|
+
}
|
|
2082
|
+
|
|
2083
|
+
interface PreparedAnthropicLineage {
|
|
2084
|
+
readonly key: string;
|
|
2085
|
+
readonly details: Omit<AnthropicLineageDetails, 'response_id' | 'assistant_content_sha256'>;
|
|
2086
|
+
}
|
|
2087
|
+
|
|
2088
|
+
function requestMessagesFromPayload(payload: JsonObject): JsonObject[] {
|
|
2089
|
+
const messages = payload['messages'];
|
|
2090
|
+
if (!Array.isArray(messages) || messages.some((message) => !isPlainObject(message))) {
|
|
2091
|
+
throw new Error('Anthropic attribution final payload.messages must be an object array');
|
|
2092
|
+
}
|
|
2093
|
+
for (const message of messages as JsonObject[]) {
|
|
2094
|
+
if (message['role'] !== 'user' && message['role'] !== 'assistant') {
|
|
2095
|
+
throw new Error('Anthropic attribution final payload message has an unsupported role');
|
|
2096
|
+
}
|
|
2097
|
+
const content = message['content'];
|
|
2098
|
+
if (!Array.isArray(content) || content.some((block) => !isPlainObject(block))) {
|
|
2099
|
+
throw new Error(
|
|
2100
|
+
'Anthropic attribution final payload message content must use canonical block arrays',
|
|
2101
|
+
);
|
|
2102
|
+
}
|
|
2103
|
+
}
|
|
2104
|
+
return messages as JsonObject[];
|
|
2105
|
+
}
|
|
2106
|
+
|
|
2107
|
+
function systemBlocksFromPayload(payload: JsonObject): unknown[] {
|
|
2108
|
+
const system = payload['system'];
|
|
2109
|
+
if (system === undefined) return [];
|
|
2110
|
+
if (typeof system === 'string') return [{ type: 'text', text: system }];
|
|
2111
|
+
if (!Array.isArray(system))
|
|
2112
|
+
throw new Error('Anthropic attribution final payload.system must be a string or block array');
|
|
2113
|
+
return system;
|
|
2114
|
+
}
|
|
2115
|
+
|
|
2116
|
+
function toolsFromPayload(payload: JsonObject): JsonObject[] {
|
|
2117
|
+
const tools = payload['tools'];
|
|
2118
|
+
if (tools === undefined) return [];
|
|
2119
|
+
if (!Array.isArray(tools) || tools.some((tool) => !isPlainObject(tool))) {
|
|
2120
|
+
throw new Error('Anthropic attribution final payload.tools must be an object array');
|
|
2121
|
+
}
|
|
2122
|
+
return tools as JsonObject[];
|
|
2123
|
+
}
|
|
2124
|
+
|
|
2125
|
+
function cacheProfileHash(
|
|
2126
|
+
model: PiModelLike,
|
|
2127
|
+
policy: ClaudeCodeModelPolicy,
|
|
2128
|
+
payload: JsonObject,
|
|
2129
|
+
staticSha256: string,
|
|
2130
|
+
): string {
|
|
2131
|
+
return sha256Canonical({
|
|
2132
|
+
projection_version: ANTHROPIC_PROJECTION_VERSION,
|
|
2133
|
+
model: normalizedAnthropicModelId(model),
|
|
2134
|
+
beta: policy.beta,
|
|
2135
|
+
conversation_static_sha256: staticSha256,
|
|
2136
|
+
thinking: payload['thinking'],
|
|
2137
|
+
output_config: payload['output_config'],
|
|
2138
|
+
tool_choice: payload['tool_choice'],
|
|
2139
|
+
});
|
|
2140
|
+
}
|
|
2141
|
+
|
|
2142
|
+
function cacheRetentionFromPayload(payload: JsonObject): CacheRetention {
|
|
2143
|
+
return inspectCacheControls(payload).retention ?? 'none';
|
|
2144
|
+
}
|
|
2145
|
+
|
|
2146
|
+
type TargetLineageState =
|
|
2147
|
+
| { readonly kind: 'none' }
|
|
2148
|
+
| { readonly kind: 'trusted'; readonly lineage: AnthropicLineageDetails }
|
|
2149
|
+
| { readonly kind: 'untrusted'; readonly assistantSha256: string };
|
|
2150
|
+
|
|
2151
|
+
function untrustedAssistantSha256(
|
|
2152
|
+
message: Extract<PiMessage, { role: 'assistant' }>,
|
|
2153
|
+
): string {
|
|
2154
|
+
return sha256Canonical({
|
|
2155
|
+
provider: message.provider ?? null,
|
|
2156
|
+
api: message.api ?? null,
|
|
2157
|
+
model: message.model ?? null,
|
|
2158
|
+
response_id: message.responseId ?? null,
|
|
2159
|
+
stop_reason: message.stopReason ?? null,
|
|
2160
|
+
assistant_content_sha256: assistantContentHash(message.content),
|
|
2161
|
+
lineage_diagnostics: (message.diagnostics ?? []).filter(
|
|
2162
|
+
(diagnostic) => diagnostic.type === ANTHROPIC_LINEAGE_DIAGNOSTIC_TYPE,
|
|
2163
|
+
),
|
|
2164
|
+
});
|
|
2165
|
+
}
|
|
2166
|
+
|
|
2167
|
+
function targetLineageState(
|
|
2168
|
+
context: PiStreamContext,
|
|
2169
|
+
targetModelId: string,
|
|
2170
|
+
): TargetLineageState {
|
|
2171
|
+
for (let index = context.messages.length - 1; index >= 0; index -= 1) {
|
|
2172
|
+
const message = context.messages[index];
|
|
2173
|
+
if (message?.role !== 'assistant') continue;
|
|
2174
|
+
|
|
2175
|
+
const lineageDiagnostics = (message.diagnostics ?? []).filter(
|
|
2176
|
+
(diagnostic) => diagnostic.type === ANTHROPIC_LINEAGE_DIAGNOSTIC_TYPE,
|
|
2177
|
+
);
|
|
2178
|
+
const latestRawDetails = lineageDiagnostics.at(-1)?.details;
|
|
2179
|
+
const receiptClaimsTarget = latestRawDetails?.['source_model'] === targetModelId;
|
|
2180
|
+
const assistantClaimsTarget = message.model === targetModelId;
|
|
2181
|
+
if (!assistantClaimsTarget && !receiptClaimsTarget) continue;
|
|
2182
|
+
|
|
2183
|
+
if (
|
|
2184
|
+
(message.stopReason === 'error' || message.stopReason === 'aborted') &&
|
|
2185
|
+
lineageDiagnostics.length === 0
|
|
2186
|
+
) {
|
|
2187
|
+
continue;
|
|
2188
|
+
}
|
|
2189
|
+
|
|
2190
|
+
const lineage = parseAnthropicLineageDetails(message);
|
|
2191
|
+
if (
|
|
2192
|
+
lineage?.source_model === targetModelId &&
|
|
2193
|
+
lineageBindsContainingAssistant(message, lineage)
|
|
2194
|
+
) {
|
|
2195
|
+
return { kind: 'trusted', lineage };
|
|
2196
|
+
}
|
|
2197
|
+
return { kind: 'untrusted', assistantSha256: untrustedAssistantSha256(message) };
|
|
2198
|
+
}
|
|
2199
|
+
return { kind: 'none' };
|
|
2200
|
+
}
|
|
2201
|
+
|
|
2202
|
+
function latestLineageForTarget(
|
|
2203
|
+
context: PiStreamContext,
|
|
2204
|
+
targetModelId: string,
|
|
2205
|
+
): AnthropicLineageDetails | undefined {
|
|
2206
|
+
const state = targetLineageState(context, targetModelId);
|
|
2207
|
+
return state.kind === 'trusted' ? state.lineage : undefined;
|
|
2208
|
+
}
|
|
2209
|
+
|
|
2210
|
+
function declaredCompactionBoundarySha256(messages: readonly JsonObject[]): string | null {
|
|
2211
|
+
const firstMessage = messages[0];
|
|
2212
|
+
if (firstMessage?.['role'] !== 'user' || !Array.isArray(firstMessage['content'])) return null;
|
|
2213
|
+
const declaresCompaction = firstMessage['content'].some(
|
|
2214
|
+
(block) =>
|
|
2215
|
+
isPlainObject(block) &&
|
|
2216
|
+
block['type'] === 'text' &&
|
|
2217
|
+
typeof block['text'] === 'string' &&
|
|
2218
|
+
block['text'].startsWith(COMPACTION_SUMMARY_PREFIX),
|
|
2219
|
+
);
|
|
2220
|
+
if (!declaresCompaction) return null;
|
|
2221
|
+
return sha256Canonical(promptMessagesWithoutCacheControls([firstMessage])[0]);
|
|
2222
|
+
}
|
|
2223
|
+
|
|
2224
|
+
function hasLineageRequestPrefix(
|
|
2225
|
+
messages: readonly JsonObject[],
|
|
2226
|
+
previous: AnthropicLineageDetails,
|
|
2227
|
+
): boolean {
|
|
2228
|
+
return (
|
|
2229
|
+
messages.length >= previous.request_message_count &&
|
|
2230
|
+
promptMessagesHash(messages.slice(0, previous.request_message_count)) ===
|
|
2231
|
+
previous.request_messages_sha256
|
|
2232
|
+
);
|
|
2233
|
+
}
|
|
2234
|
+
|
|
2235
|
+
function prepareAnthropicLineageDetails(args: {
|
|
2236
|
+
readonly model: PiModelLike;
|
|
2237
|
+
readonly policy: ClaudeCodeModelPolicy;
|
|
2238
|
+
readonly context: PiStreamContext;
|
|
2239
|
+
readonly payload: JsonObject;
|
|
2240
|
+
readonly signatureEpoch: SignatureEpochPolicy;
|
|
2241
|
+
}): PreparedAnthropicLineage['details'] {
|
|
2242
|
+
const targetModelId = normalizedAnthropicModelId(args.model);
|
|
2243
|
+
const messages = requestMessagesFromPayload(args.payload);
|
|
2244
|
+
const staticSha256 = conversationStaticHash(
|
|
2245
|
+
systemBlocksFromPayload(args.payload),
|
|
2246
|
+
toolsFromPayload(args.payload),
|
|
2247
|
+
);
|
|
2248
|
+
const profileSha256 = cacheProfileHash(args.model, args.policy, args.payload, staticSha256);
|
|
2249
|
+
const cacheRetention = cacheRetentionFromPayload(args.payload);
|
|
2250
|
+
const compactionBoundarySha256 = declaredCompactionBoundarySha256(messages);
|
|
2251
|
+
const signatureEpoch = args.signatureEpoch;
|
|
2252
|
+
let previous = latestLineageForTarget(args.context, targetModelId);
|
|
2253
|
+
if (
|
|
2254
|
+
previous !== undefined &&
|
|
2255
|
+
(signatureEpoch.sha256 !== previous.signature_epoch_sha256 ||
|
|
2256
|
+
!hasLineageRequestPrefix(messages, previous) ||
|
|
2257
|
+
previous.cache_profile_sha256 !== profileSha256 ||
|
|
2258
|
+
previous.cache_retention !== cacheRetention)
|
|
2259
|
+
) {
|
|
2260
|
+
previous = undefined;
|
|
2261
|
+
}
|
|
2262
|
+
return {
|
|
2263
|
+
schema_version: ANTHROPIC_LINEAGE_SCHEMA,
|
|
2264
|
+
projection_version: ANTHROPIC_PROJECTION_VERSION,
|
|
2265
|
+
source_provider: 'anthropic',
|
|
2266
|
+
source_api: 'anthropic-messages',
|
|
2267
|
+
source_model: targetModelId,
|
|
2268
|
+
conversation_static_sha256: staticSha256,
|
|
2269
|
+
request_message_count: messages.length,
|
|
2270
|
+
request_messages_sha256: promptMessagesHash(messages),
|
|
2271
|
+
cache_profile_sha256: profileSha256,
|
|
2272
|
+
cache_retention: cacheRetention,
|
|
2273
|
+
compaction_boundary_sha256: compactionBoundarySha256,
|
|
2274
|
+
signature_epoch_sha256: signatureEpoch.sha256,
|
|
2275
|
+
signature_epoch_inherits_prior: signatureEpoch.inheritsPrior,
|
|
2276
|
+
previous_message_id: previous?.response_id ?? null,
|
|
2277
|
+
};
|
|
2278
|
+
}
|
|
2279
|
+
|
|
2280
|
+
class AnthropicLineageCoordinator {
|
|
2281
|
+
private readonly inFlight = new Set<string>();
|
|
2282
|
+
|
|
2283
|
+
prepare(args: {
|
|
2284
|
+
readonly sessionId: string;
|
|
2285
|
+
readonly model: PiModelLike;
|
|
2286
|
+
readonly policy: ClaudeCodeModelPolicy;
|
|
2287
|
+
readonly context: PiStreamContext;
|
|
2288
|
+
readonly payload: JsonObject;
|
|
2289
|
+
readonly signatureEpoch: SignatureEpochPolicy;
|
|
2290
|
+
}): PreparedAnthropicLineage {
|
|
2291
|
+
const targetModelId = normalizedAnthropicModelId(args.model);
|
|
2292
|
+
const key = `${args.sessionId}\u0000${targetModelId}`;
|
|
2293
|
+
if (this.inFlight.has(key)) {
|
|
2294
|
+
throw new Error(
|
|
2295
|
+
`Anthropic cache lineage already has an in-flight request for ${targetModelId}; concurrent continuations must fork`,
|
|
2296
|
+
);
|
|
2297
|
+
}
|
|
2298
|
+
const details = prepareAnthropicLineageDetails(args);
|
|
2299
|
+
this.inFlight.add(key);
|
|
2300
|
+
return { key, details };
|
|
2301
|
+
}
|
|
2302
|
+
|
|
2303
|
+
release(prepared: PreparedAnthropicLineage): void {
|
|
2304
|
+
this.inFlight.delete(prepared.key);
|
|
2305
|
+
}
|
|
2306
|
+
}
|
|
2307
|
+
|
|
2308
|
+
const anthropicLineageCoordinator = new AnthropicLineageCoordinator();
|
|
2309
|
+
|
|
2310
|
+
export function createAnthropicLineageDiagnostic(args: {
|
|
2311
|
+
readonly model: PiModelLike;
|
|
2312
|
+
readonly responseId: string;
|
|
2313
|
+
readonly assistantContent: readonly JsonObject[];
|
|
2314
|
+
readonly requestPayload: JsonObject;
|
|
2315
|
+
readonly previousMessageId?: string | null;
|
|
2316
|
+
}): PiAssistantDiagnosticLike {
|
|
2317
|
+
if (!isNonEmptyString(args.responseId)) {
|
|
2318
|
+
throw new Error('Anthropic lineage diagnostic requires a non-empty response ID');
|
|
2319
|
+
}
|
|
2320
|
+
if (args.previousMessageId !== undefined && args.previousMessageId !== null) {
|
|
2321
|
+
if (!isNonEmptyString(args.previousMessageId)) {
|
|
2322
|
+
throw new Error('Anthropic lineage diagnostic requires a non-empty previous response ID');
|
|
2323
|
+
}
|
|
2324
|
+
}
|
|
2325
|
+
const policy = resolveClaudeCodeModelPolicy(args.model);
|
|
2326
|
+
const messages = requestMessagesFromPayload(args.requestPayload);
|
|
2327
|
+
const staticSha256 = conversationStaticHash(
|
|
2328
|
+
systemBlocksFromPayload(args.requestPayload),
|
|
2329
|
+
toolsFromPayload(args.requestPayload),
|
|
2330
|
+
);
|
|
2331
|
+
const cacheRetention = cacheRetentionFromPayload(args.requestPayload);
|
|
2332
|
+
const compactionBoundarySha256 = declaredCompactionBoundarySha256(messages);
|
|
2333
|
+
const signatureEpoch = initialSignatureEpochPolicy(
|
|
2334
|
+
normalizedAnthropicModelId(args.model),
|
|
2335
|
+
cacheRetention,
|
|
2336
|
+
compactionBoundarySha256,
|
|
2337
|
+
);
|
|
2338
|
+
return {
|
|
2339
|
+
type: ANTHROPIC_LINEAGE_DIAGNOSTIC_TYPE,
|
|
2340
|
+
timestamp: Date.now(),
|
|
2341
|
+
details: {
|
|
2342
|
+
schema_version: ANTHROPIC_LINEAGE_SCHEMA,
|
|
2343
|
+
projection_version: ANTHROPIC_PROJECTION_VERSION,
|
|
2344
|
+
source_provider: 'anthropic',
|
|
2345
|
+
source_api: 'anthropic-messages',
|
|
2346
|
+
source_model: normalizedAnthropicModelId(args.model),
|
|
2347
|
+
response_id: args.responseId,
|
|
2348
|
+
assistant_content_sha256: assistantContentHash(args.assistantContent),
|
|
2349
|
+
conversation_static_sha256: staticSha256,
|
|
2350
|
+
request_message_count: messages.length,
|
|
2351
|
+
request_messages_sha256: promptMessagesHash(messages),
|
|
2352
|
+
cache_profile_sha256: cacheProfileHash(args.model, policy, args.requestPayload, staticSha256),
|
|
2353
|
+
cache_retention: cacheRetention,
|
|
2354
|
+
compaction_boundary_sha256: compactionBoundarySha256,
|
|
2355
|
+
signature_epoch_sha256: signatureEpoch.sha256,
|
|
2356
|
+
signature_epoch_inherits_prior: signatureEpoch.inheritsPrior,
|
|
2357
|
+
previous_message_id: args.previousMessageId ?? null,
|
|
2358
|
+
},
|
|
2359
|
+
};
|
|
2360
|
+
}
|
|
2361
|
+
|
|
2362
|
+
function appendLineageDiagnostic(
|
|
2363
|
+
output: AssistantMessageLike,
|
|
2364
|
+
prepared: PreparedAnthropicLineage,
|
|
2365
|
+
): void {
|
|
2366
|
+
if (!isNonEmptyString(output.responseId)) {
|
|
2367
|
+
throw new Error(
|
|
2368
|
+
'Anthropic attribution successful response is missing responseId lineage proof',
|
|
2369
|
+
);
|
|
2370
|
+
}
|
|
2371
|
+
output.diagnostics ??= [];
|
|
2372
|
+
output.diagnostics.push({
|
|
2373
|
+
type: ANTHROPIC_LINEAGE_DIAGNOSTIC_TYPE,
|
|
2374
|
+
timestamp: Date.now(),
|
|
2375
|
+
details: {
|
|
2376
|
+
...prepared.details,
|
|
2377
|
+
response_id: output.responseId,
|
|
2378
|
+
assistant_content_sha256: assistantContentHash(output.content),
|
|
2379
|
+
},
|
|
2380
|
+
});
|
|
2381
|
+
}
|
|
2382
|
+
|
|
2383
|
+
function appendProviderCacheDiagnostic(
|
|
2384
|
+
output: AssistantMessageLike,
|
|
2385
|
+
messageStart: JsonObject,
|
|
2386
|
+
): void {
|
|
2387
|
+
const diagnostics = messageStart['diagnostics'];
|
|
2388
|
+
const inputTransformations = messageStart['input_transformations'];
|
|
2389
|
+
if (diagnostics === undefined && inputTransformations === undefined) return;
|
|
2390
|
+
output.diagnostics ??= [];
|
|
2391
|
+
output.diagnostics.push({
|
|
2392
|
+
type: 'anthropic-provider-cache',
|
|
2393
|
+
timestamp: Date.now(),
|
|
2394
|
+
details: {
|
|
2395
|
+
cache_diagnostics: canonicalizeJson(diagnostics ?? null),
|
|
2396
|
+
input_transformations: canonicalizeJson(inputTransformations ?? []),
|
|
2397
|
+
},
|
|
2398
|
+
});
|
|
1372
2399
|
}
|
|
1373
2400
|
|
|
1374
2401
|
function headersToRecord(headers: Headers): Record<string, string> {
|
|
@@ -1381,6 +2408,35 @@ function lowerHeaderMap(headers: Record<string, string> | undefined): Record<str
|
|
|
1381
2408
|
return output;
|
|
1382
2409
|
}
|
|
1383
2410
|
|
|
2411
|
+
function resolveAnthropicBetaMessagesUrl(model: PiModelLike): string {
|
|
2412
|
+
const configured =
|
|
2413
|
+
typeof model.baseUrl === 'string' && model.baseUrl.trim().length > 0
|
|
2414
|
+
? model.baseUrl.trim()
|
|
2415
|
+
: ANTHROPIC_OFFICIAL_ORIGIN;
|
|
2416
|
+
let parsed: URL;
|
|
2417
|
+
try {
|
|
2418
|
+
parsed = new URL(configured);
|
|
2419
|
+
} catch (error) {
|
|
2420
|
+
throw new Error(
|
|
2421
|
+
`Anthropic attribution requires the official Anthropic HTTPS endpoint; invalid model.baseUrl: ${error instanceof Error ? error.message : String(error)}`,
|
|
2422
|
+
);
|
|
2423
|
+
}
|
|
2424
|
+
if (
|
|
2425
|
+
parsed.origin !== ANTHROPIC_OFFICIAL_ORIGIN ||
|
|
2426
|
+
parsed.protocol !== 'https:' ||
|
|
2427
|
+
parsed.username.length > 0 ||
|
|
2428
|
+
parsed.password.length > 0 ||
|
|
2429
|
+
(parsed.pathname !== '' && parsed.pathname !== '/') ||
|
|
2430
|
+
parsed.search.length > 0 ||
|
|
2431
|
+
parsed.hash.length > 0
|
|
2432
|
+
) {
|
|
2433
|
+
throw new Error(
|
|
2434
|
+
`Anthropic attribution requires the official Anthropic HTTPS endpoint ${ANTHROPIC_OFFICIAL_ORIGIN}; refusing model.baseUrl ${JSON.stringify(configured)}`,
|
|
2435
|
+
);
|
|
2436
|
+
}
|
|
2437
|
+
return ANTHROPIC_BETA_MESSAGES_URL;
|
|
2438
|
+
}
|
|
2439
|
+
|
|
1384
2440
|
function buildFetchHeaders(
|
|
1385
2441
|
options: PiSimpleStreamOptions | undefined,
|
|
1386
2442
|
apiKey: string,
|
|
@@ -1424,6 +2480,23 @@ function parseStreamingJsonFragment(text: string): unknown {
|
|
|
1424
2480
|
}
|
|
1425
2481
|
}
|
|
1426
2482
|
|
|
2483
|
+
function parseCompletedToolInput(text: string): JsonObject {
|
|
2484
|
+
const parsed = parseJsonValue(text, 'Anthropic streamed tool input');
|
|
2485
|
+
if (!isPlainObject(parsed)) {
|
|
2486
|
+
throw new Error('Anthropic streamed tool input must complete as a JSON object');
|
|
2487
|
+
}
|
|
2488
|
+
return parsed;
|
|
2489
|
+
}
|
|
2490
|
+
|
|
2491
|
+
function anthropicStreamErrorMessage(event: JsonObject): string {
|
|
2492
|
+
const error = event['error'];
|
|
2493
|
+
if (isPlainObject(error) && typeof error['message'] === 'string') {
|
|
2494
|
+
const type = typeof error['type'] === 'string' ? `${error['type']}: ` : '';
|
|
2495
|
+
return `Anthropic beta messages stream error: ${type}${error['message']}`;
|
|
2496
|
+
}
|
|
2497
|
+
return 'Anthropic beta messages stream emitted an error event';
|
|
2498
|
+
}
|
|
2499
|
+
|
|
1427
2500
|
function validCostRate(value: unknown, fallback: number, field: string): number {
|
|
1428
2501
|
if (value === undefined) return fallback;
|
|
1429
2502
|
if (typeof value !== 'number' || !Number.isFinite(value) || value < 0) {
|
|
@@ -1523,12 +2596,25 @@ async function* iterateSseEvents(
|
|
|
1523
2596
|
let eventName = '';
|
|
1524
2597
|
let dataLines: string[] = [];
|
|
1525
2598
|
function flush(): JsonObject | undefined {
|
|
1526
|
-
if (dataLines.length === 0)
|
|
2599
|
+
if (dataLines.length === 0) {
|
|
2600
|
+
eventName = '';
|
|
2601
|
+
return undefined;
|
|
2602
|
+
}
|
|
1527
2603
|
const data = dataLines.join('\n');
|
|
2604
|
+
const declaredEventName = eventName;
|
|
1528
2605
|
eventName = '';
|
|
1529
2606
|
dataLines = [];
|
|
1530
2607
|
if (data === '[DONE]') return undefined;
|
|
1531
|
-
|
|
2608
|
+
const event = parseJsonObject(data, 'Anthropic beta messages SSE event');
|
|
2609
|
+
if (
|
|
2610
|
+
declaredEventName.length > 0 &&
|
|
2611
|
+
(typeof event['type'] !== 'string' || event['type'] !== declaredEventName)
|
|
2612
|
+
) {
|
|
2613
|
+
throw new Error(
|
|
2614
|
+
`Anthropic beta messages SSE event name ${JSON.stringify(declaredEventName)} did not match data.type`,
|
|
2615
|
+
);
|
|
2616
|
+
}
|
|
2617
|
+
return event;
|
|
1532
2618
|
}
|
|
1533
2619
|
function consumeLine(line: string): JsonObject | undefined {
|
|
1534
2620
|
if (line.length === 0) return flush();
|
|
@@ -1539,7 +2625,6 @@ async function* iterateSseEvents(
|
|
|
1539
2625
|
if (value.startsWith(' ')) value = value.slice(1);
|
|
1540
2626
|
if (field === 'event') eventName = value;
|
|
1541
2627
|
if (field === 'data') dataLines.push(value);
|
|
1542
|
-
void eventName;
|
|
1543
2628
|
return undefined;
|
|
1544
2629
|
}
|
|
1545
2630
|
try {
|
|
@@ -1589,49 +2674,42 @@ function createOutput(model: PiModelLike): AssistantMessageLike {
|
|
|
1589
2674
|
};
|
|
1590
2675
|
}
|
|
1591
2676
|
|
|
1592
|
-
|
|
2677
|
+
type HostForwardingModel = PiModelLike & HostModel<'anthropic-messages'>;
|
|
2678
|
+
type HostForwardingContext = PiStreamContext & HostContext;
|
|
2679
|
+
type HostForwardingOptions = PiSimpleStreamOptions & HostSimpleStreamOptions;
|
|
2680
|
+
type HostForwardingStream = AssistantMessageEventStreamLike & HostAssistantMessageEventStream;
|
|
2681
|
+
|
|
2682
|
+
function forwardToBuiltInAnthropic(
|
|
1593
2683
|
model: PiModelLike,
|
|
1594
2684
|
context: PiStreamContext,
|
|
1595
2685
|
options: PiSimpleStreamOptions | undefined,
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
) => AssistantMessageEventStreamLike;
|
|
1608
|
-
}>;
|
|
1609
|
-
const mod = await dynamicImport('@earendil-works/pi-ai/anthropic');
|
|
1610
|
-
const delegated = mod.streamSimpleAnthropic(model, context, options);
|
|
1611
|
-
for await (const event of delegated) stream.push(event);
|
|
1612
|
-
stream.end(await delegated.result());
|
|
1613
|
-
} catch (error) {
|
|
1614
|
-
output.stopReason = 'error';
|
|
1615
|
-
output.errorMessage = `Anthropic attribution could not delegate non-target provider ${JSON.stringify(model.provider)}: ${error instanceof Error ? error.message : String(error)}`;
|
|
1616
|
-
stream.push({ type: 'error', reason: 'error', error: output });
|
|
1617
|
-
stream.end();
|
|
1618
|
-
}
|
|
2686
|
+
): AssistantMessageEventStreamLike {
|
|
2687
|
+
// Pi aliases these SDK imports to its own version while loading extensions.
|
|
2688
|
+
// The host therefore owns both its legacy Context or normalized TranscriptContext
|
|
2689
|
+
// input and the complete stream/event/result shape. These intersections only mark
|
|
2690
|
+
// that host-owned boundary; no request, callback, event, or result is reconstructed.
|
|
2691
|
+
const delegated = anthropicMessagesApi().streamSimple(
|
|
2692
|
+
model as HostForwardingModel,
|
|
2693
|
+
context as HostForwardingContext,
|
|
2694
|
+
options as HostForwardingOptions | undefined,
|
|
2695
|
+
);
|
|
2696
|
+
return delegated as HostForwardingStream;
|
|
1619
2697
|
}
|
|
1620
2698
|
|
|
1621
2699
|
export function streamAnthropicViaBetaMessages(
|
|
1622
2700
|
model: PiModelLike,
|
|
1623
2701
|
context: PiStreamContext,
|
|
1624
2702
|
options?: PiSimpleStreamOptions,
|
|
2703
|
+
dependencies: AnthropicTransportDependencies = {},
|
|
1625
2704
|
): AssistantMessageEventStreamLike {
|
|
1626
|
-
const stream = createAssistantMessageEventStream();
|
|
1627
|
-
const output = createOutput(model);
|
|
1628
|
-
|
|
1629
2705
|
if (model.provider !== 'anthropic') {
|
|
1630
|
-
|
|
1631
|
-
return stream;
|
|
2706
|
+
return forwardToBuiltInAnthropic(model, context, options);
|
|
1632
2707
|
}
|
|
1633
2708
|
|
|
2709
|
+
const stream = createAssistantMessageEventStream();
|
|
2710
|
+
const output = createOutput(model);
|
|
1634
2711
|
void (async () => {
|
|
2712
|
+
let preparedLineage: PreparedAnthropicLineage | undefined;
|
|
1635
2713
|
try {
|
|
1636
2714
|
const apiKey = options?.apiKey;
|
|
1637
2715
|
if (typeof apiKey !== 'string' || apiKey.length === 0) {
|
|
@@ -1645,37 +2723,111 @@ export function streamAnthropicViaBetaMessages(
|
|
|
1645
2723
|
);
|
|
1646
2724
|
}
|
|
1647
2725
|
|
|
2726
|
+
const sessionId = requireSessionId(options?.sessionId, 'options.sessionId');
|
|
2727
|
+
const account = requireAttributionAccount(
|
|
2728
|
+
dependencies.loadAccount?.() ?? loadClaudeAttributionAccount(undefined, options?.env),
|
|
2729
|
+
);
|
|
2730
|
+
const url = resolveAnthropicBetaMessagesUrl(model);
|
|
1648
2731
|
const policy = resolveClaudeCodeModelPolicy(model);
|
|
1649
|
-
|
|
2732
|
+
const request = buildAnthropicRequest(model, context, options);
|
|
2733
|
+
let params = request.params;
|
|
2734
|
+
const billingSystemText = buildClaudeCodeBillingSystemText(
|
|
2735
|
+
firstUserMessageTextFromPayload(params),
|
|
2736
|
+
);
|
|
2737
|
+
const provisionalLineage = prepareAnthropicLineageDetails({
|
|
2738
|
+
model,
|
|
2739
|
+
policy,
|
|
2740
|
+
context,
|
|
2741
|
+
payload: params,
|
|
2742
|
+
signatureEpoch: request.signatureEpoch,
|
|
2743
|
+
});
|
|
2744
|
+
if (policy.supportsCacheDiagnostics) {
|
|
2745
|
+
if (!policy.beta.split(',').includes(ANTHROPIC_CACHE_DIAGNOSTICS_BETA)) {
|
|
2746
|
+
throw new Error('Anthropic cache diagnostics policy is missing its required beta header');
|
|
2747
|
+
}
|
|
2748
|
+
params['diagnostics'] = {
|
|
2749
|
+
previous_message_id: provisionalLineage.previous_message_id,
|
|
2750
|
+
};
|
|
2751
|
+
}
|
|
2752
|
+
if (
|
|
2753
|
+
policy.enforcesThinkingPrefixBinding &&
|
|
2754
|
+
!policy.beta.split(',').includes(ANTHROPIC_THINKING_BINDING_BETA)
|
|
2755
|
+
) {
|
|
2756
|
+
throw new Error('Anthropic thinking-binding policy is missing its required beta header');
|
|
2757
|
+
}
|
|
2758
|
+
params = rewriteAnthropicRequestPayloadForSession({
|
|
2759
|
+
payload: params,
|
|
2760
|
+
model,
|
|
2761
|
+
sessionId,
|
|
2762
|
+
account,
|
|
2763
|
+
headerRegistered: true,
|
|
2764
|
+
...(options?.cacheRetention === undefined
|
|
2765
|
+
? {}
|
|
2766
|
+
: { cacheRetention: options.cacheRetention }),
|
|
2767
|
+
});
|
|
2768
|
+
const protectedCacheControlTopologySha256 = cacheControlTopologySha256(params);
|
|
1650
2769
|
const nextParams = await options?.onPayload?.(params, model);
|
|
1651
2770
|
if (nextParams !== undefined) {
|
|
1652
2771
|
if (!isPlainObject(nextParams))
|
|
1653
2772
|
throw new Error('Anthropic attribution onPayload returned a non-object payload');
|
|
1654
2773
|
params = nextParams;
|
|
1655
2774
|
}
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
}
|
|
1664
|
-
|
|
2775
|
+
assertProtectedAnthropicAttribution({
|
|
2776
|
+
payload: params,
|
|
2777
|
+
account,
|
|
2778
|
+
sessionId,
|
|
2779
|
+
billingSystemText,
|
|
2780
|
+
modelId: policy.modelId,
|
|
2781
|
+
cacheControlTopologySha256: protectedCacheControlTopologySha256,
|
|
2782
|
+
});
|
|
2783
|
+
|
|
2784
|
+
preparedLineage = anthropicLineageCoordinator.prepare({
|
|
2785
|
+
sessionId,
|
|
2786
|
+
model,
|
|
2787
|
+
policy,
|
|
2788
|
+
context,
|
|
2789
|
+
payload: params,
|
|
2790
|
+
signatureEpoch: request.signatureEpoch,
|
|
2791
|
+
});
|
|
2792
|
+
if (
|
|
2793
|
+
preparedLineage.details.previous_message_id !== provisionalLineage.previous_message_id ||
|
|
2794
|
+
preparedLineage.details.conversation_static_sha256 !==
|
|
2795
|
+
provisionalLineage.conversation_static_sha256 ||
|
|
2796
|
+
preparedLineage.details.request_message_count !==
|
|
2797
|
+
provisionalLineage.request_message_count ||
|
|
2798
|
+
preparedLineage.details.request_messages_sha256 !==
|
|
2799
|
+
provisionalLineage.request_messages_sha256 ||
|
|
2800
|
+
preparedLineage.details.cache_profile_sha256 !== provisionalLineage.cache_profile_sha256 ||
|
|
2801
|
+
preparedLineage.details.cache_retention !== provisionalLineage.cache_retention ||
|
|
2802
|
+
preparedLineage.details.compaction_boundary_sha256 !==
|
|
2803
|
+
provisionalLineage.compaction_boundary_sha256 ||
|
|
2804
|
+
preparedLineage.details.signature_epoch_sha256 !==
|
|
2805
|
+
provisionalLineage.signature_epoch_sha256 ||
|
|
2806
|
+
preparedLineage.details.signature_epoch_inherits_prior !==
|
|
2807
|
+
provisionalLineage.signature_epoch_inherits_prior
|
|
2808
|
+
) {
|
|
1665
2809
|
throw new Error(
|
|
1666
|
-
'Anthropic
|
|
2810
|
+
'Anthropic request/cache lineage changed during before_provider_request transforms',
|
|
1667
2811
|
);
|
|
2812
|
+
}
|
|
2813
|
+
if (policy.supportsCacheDiagnostics) {
|
|
2814
|
+
const diagnostics = params['diagnostics'];
|
|
2815
|
+
if (
|
|
2816
|
+
!isPlainObject(diagnostics) ||
|
|
2817
|
+
diagnostics['previous_message_id'] !== preparedLineage.details.previous_message_id
|
|
2818
|
+
) {
|
|
2819
|
+
throw new Error(
|
|
2820
|
+
'Anthropic cache diagnostics were removed or changed during before_provider_request transforms',
|
|
2821
|
+
);
|
|
2822
|
+
}
|
|
2823
|
+
}
|
|
1668
2824
|
|
|
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
2825
|
const headers = buildFetchHeaders(options, apiKey, sessionId, policy.beta);
|
|
1675
2826
|
const requestInit: RequestInit = {
|
|
1676
2827
|
method: 'POST',
|
|
1677
2828
|
headers,
|
|
1678
2829
|
body: JSON.stringify(params),
|
|
2830
|
+
redirect: 'error',
|
|
1679
2831
|
};
|
|
1680
2832
|
if (options?.signal) requestInit.signal = options.signal;
|
|
1681
2833
|
const response = await fetch(url, requestInit);
|
|
@@ -1688,26 +2840,67 @@ export function streamAnthropicViaBetaMessages(
|
|
|
1688
2840
|
`Anthropic beta messages request failed: HTTP ${response.status} ${response.statusText}: ${await response.text()}`,
|
|
1689
2841
|
);
|
|
1690
2842
|
}
|
|
2843
|
+
const contentType = response.headers.get('content-type')?.toLowerCase() ?? '';
|
|
2844
|
+
if (!contentType.includes('text/event-stream')) {
|
|
2845
|
+
throw new Error(
|
|
2846
|
+
`Anthropic beta messages response must be text/event-stream; got ${JSON.stringify(contentType || '(missing)')}`,
|
|
2847
|
+
);
|
|
2848
|
+
}
|
|
1691
2849
|
|
|
1692
2850
|
stream.push({ type: 'start', partial: output });
|
|
1693
2851
|
const blocks = output.content as Array<JsonObject & { index?: number; partialJson?: string }>;
|
|
2852
|
+
const activeContentIndexes = new Set<number>();
|
|
2853
|
+
let sawMessageStart = false;
|
|
2854
|
+
let sawMessageStop = false;
|
|
2855
|
+
let sawTerminalStopReason = false;
|
|
1694
2856
|
for await (const event of iterateSseEvents(response, options?.signal)) {
|
|
1695
|
-
|
|
2857
|
+
const eventType = event['type'];
|
|
2858
|
+
if (typeof eventType !== 'string') {
|
|
2859
|
+
throw new Error('Anthropic beta messages SSE event is missing string data.type');
|
|
2860
|
+
}
|
|
2861
|
+
if (eventType === 'error') throw new Error(anthropicStreamErrorMessage(event));
|
|
2862
|
+
if (eventType === 'ping') continue;
|
|
2863
|
+
if (sawMessageStop) {
|
|
2864
|
+
throw new Error('Anthropic beta messages stream emitted data after message_stop');
|
|
2865
|
+
}
|
|
2866
|
+
if (eventType !== 'message_start' && !sawMessageStart) {
|
|
2867
|
+
throw new Error('Anthropic beta messages stream emitted content before message_start');
|
|
2868
|
+
}
|
|
2869
|
+
|
|
2870
|
+
if (eventType === 'message_start') {
|
|
2871
|
+
if (sawMessageStart || !isPlainObject(event['message'])) {
|
|
2872
|
+
throw new Error(
|
|
2873
|
+
'Anthropic beta messages stream emitted malformed/duplicate message_start',
|
|
2874
|
+
);
|
|
2875
|
+
}
|
|
2876
|
+
if (!isNonEmptyString(event['message']['id'])) {
|
|
2877
|
+
throw new Error('Anthropic beta messages message_start is missing a response id');
|
|
2878
|
+
}
|
|
2879
|
+
sawMessageStart = true;
|
|
1696
2880
|
if (typeof event['message']['id'] === 'string')
|
|
1697
2881
|
output.responseId = event['message']['id'];
|
|
2882
|
+
appendProviderCacheDiagnostic(output, event['message']);
|
|
1698
2883
|
updateAnthropicUsage(
|
|
1699
2884
|
output,
|
|
1700
2885
|
isPlainObject(event['message']['usage']) ? event['message']['usage'] : undefined,
|
|
1701
2886
|
model,
|
|
1702
2887
|
);
|
|
1703
|
-
} else if (
|
|
1704
|
-
event['
|
|
1705
|
-
typeof event['index'] === 'number' &&
|
|
1706
|
-
isPlainObject(event['content_block'])
|
|
1707
|
-
) {
|
|
2888
|
+
} else if (eventType === 'content_block_start') {
|
|
2889
|
+
const contentIndex = event['index'];
|
|
1708
2890
|
const contentBlock = event['content_block'];
|
|
2891
|
+
if (
|
|
2892
|
+
!Number.isSafeInteger(contentIndex) ||
|
|
2893
|
+
(contentIndex as number) < 0 ||
|
|
2894
|
+
!isPlainObject(contentBlock)
|
|
2895
|
+
) {
|
|
2896
|
+
throw new Error('Anthropic beta messages stream emitted malformed content_block_start');
|
|
2897
|
+
}
|
|
2898
|
+
if (activeContentIndexes.has(contentIndex as number)) {
|
|
2899
|
+
throw new Error('Anthropic beta messages stream duplicated an active content block');
|
|
2900
|
+
}
|
|
2901
|
+
activeContentIndexes.add(contentIndex as number);
|
|
1709
2902
|
if (contentBlock['type'] === 'text') {
|
|
1710
|
-
output.content.push({ type: 'text', text: '', index:
|
|
2903
|
+
output.content.push({ type: 'text', text: '', index: contentIndex });
|
|
1711
2904
|
stream.push({
|
|
1712
2905
|
type: 'text_start',
|
|
1713
2906
|
contentIndex: output.content.length - 1,
|
|
@@ -1718,7 +2911,7 @@ export function streamAnthropicViaBetaMessages(
|
|
|
1718
2911
|
type: 'thinking',
|
|
1719
2912
|
thinking: '',
|
|
1720
2913
|
thinkingSignature: '',
|
|
1721
|
-
index:
|
|
2914
|
+
index: contentIndex,
|
|
1722
2915
|
});
|
|
1723
2916
|
stream.push({
|
|
1724
2917
|
type: 'thinking_start',
|
|
@@ -1726,12 +2919,15 @@ export function streamAnthropicViaBetaMessages(
|
|
|
1726
2919
|
partial: output,
|
|
1727
2920
|
});
|
|
1728
2921
|
} else if (contentBlock['type'] === 'redacted_thinking') {
|
|
2922
|
+
if (typeof contentBlock['data'] !== 'string') {
|
|
2923
|
+
throw new Error('Anthropic redacted_thinking block is missing string data');
|
|
2924
|
+
}
|
|
1729
2925
|
output.content.push({
|
|
1730
2926
|
type: 'thinking',
|
|
1731
2927
|
thinking: '[Reasoning redacted]',
|
|
1732
2928
|
thinkingSignature: contentBlock['data'],
|
|
1733
2929
|
redacted: true,
|
|
1734
|
-
index:
|
|
2930
|
+
index: contentIndex,
|
|
1735
2931
|
});
|
|
1736
2932
|
stream.push({
|
|
1737
2933
|
type: 'thinking_start',
|
|
@@ -1739,29 +2935,48 @@ export function streamAnthropicViaBetaMessages(
|
|
|
1739
2935
|
partial: output,
|
|
1740
2936
|
});
|
|
1741
2937
|
} else if (contentBlock['type'] === 'tool_use') {
|
|
2938
|
+
if (
|
|
2939
|
+
typeof contentBlock['id'] !== 'string' ||
|
|
2940
|
+
typeof contentBlock['name'] !== 'string' ||
|
|
2941
|
+
!isPlainObject(contentBlock['input'] ?? {})
|
|
2942
|
+
) {
|
|
2943
|
+
throw new Error('Anthropic tool_use block is malformed');
|
|
2944
|
+
}
|
|
1742
2945
|
output.content.push({
|
|
1743
2946
|
type: 'toolCall',
|
|
1744
2947
|
id: contentBlock['id'],
|
|
1745
2948
|
name: contentBlock['name'],
|
|
1746
2949
|
arguments: contentBlock['input'] ?? {},
|
|
1747
2950
|
partialJson: '',
|
|
1748
|
-
index:
|
|
2951
|
+
index: contentIndex,
|
|
1749
2952
|
});
|
|
1750
2953
|
stream.push({
|
|
1751
2954
|
type: 'toolcall_start',
|
|
1752
2955
|
contentIndex: output.content.length - 1,
|
|
1753
2956
|
partial: output,
|
|
1754
2957
|
});
|
|
2958
|
+
} else {
|
|
2959
|
+
throw new Error(
|
|
2960
|
+
`Anthropic beta messages stream emitted unsupported content block ${JSON.stringify(contentBlock['type'])}`,
|
|
2961
|
+
);
|
|
1755
2962
|
}
|
|
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;
|
|
2963
|
+
} else if (eventType === 'content_block_delta') {
|
|
2964
|
+
const contentIndex = event['index'];
|
|
1764
2965
|
const delta = event['delta'];
|
|
2966
|
+
if (
|
|
2967
|
+
!Number.isSafeInteger(contentIndex) ||
|
|
2968
|
+
!activeContentIndexes.has(contentIndex as number) ||
|
|
2969
|
+
!isPlainObject(delta)
|
|
2970
|
+
) {
|
|
2971
|
+
throw new Error(
|
|
2972
|
+
'Anthropic beta messages stream emitted malformed/orphan content_block_delta',
|
|
2973
|
+
);
|
|
2974
|
+
}
|
|
2975
|
+
const blockIndex = blocks.findIndex((block) => block.index === contentIndex);
|
|
2976
|
+
const block = blocks[blockIndex];
|
|
2977
|
+
if (!block) {
|
|
2978
|
+
throw new Error('Anthropic beta messages stream delta has no projected content block');
|
|
2979
|
+
}
|
|
1765
2980
|
if (
|
|
1766
2981
|
delta['type'] === 'text_delta' &&
|
|
1767
2982
|
block['type'] === 'text' &&
|
|
@@ -1806,11 +3021,26 @@ export function streamAnthropicViaBetaMessages(
|
|
|
1806
3021
|
) {
|
|
1807
3022
|
block['thinkingSignature'] =
|
|
1808
3023
|
`${String(block['thinkingSignature'] ?? '')}${delta['signature']}`;
|
|
3024
|
+
} else {
|
|
3025
|
+
throw new Error(
|
|
3026
|
+
`Anthropic beta messages stream emitted unsupported/mismatched delta ${JSON.stringify(delta['type'])}`,
|
|
3027
|
+
);
|
|
1809
3028
|
}
|
|
1810
|
-
} else if (
|
|
1811
|
-
const
|
|
3029
|
+
} else if (eventType === 'content_block_stop') {
|
|
3030
|
+
const contentIndex = event['index'];
|
|
3031
|
+
if (
|
|
3032
|
+
!Number.isSafeInteger(contentIndex) ||
|
|
3033
|
+
!activeContentIndexes.delete(contentIndex as number)
|
|
3034
|
+
) {
|
|
3035
|
+
throw new Error(
|
|
3036
|
+
'Anthropic beta messages stream emitted malformed/orphan content_block_stop',
|
|
3037
|
+
);
|
|
3038
|
+
}
|
|
3039
|
+
const blockIndex = blocks.findIndex((block) => block.index === contentIndex);
|
|
1812
3040
|
const block = blocks[blockIndex];
|
|
1813
|
-
if (!block)
|
|
3041
|
+
if (!block) {
|
|
3042
|
+
throw new Error('Anthropic beta messages stream stop has no projected content block');
|
|
3043
|
+
}
|
|
1814
3044
|
delete block.index;
|
|
1815
3045
|
if (block['type'] === 'text') {
|
|
1816
3046
|
stream.push({
|
|
@@ -1827,7 +3057,10 @@ export function streamAnthropicViaBetaMessages(
|
|
|
1827
3057
|
partial: output,
|
|
1828
3058
|
});
|
|
1829
3059
|
} else if (block['type'] === 'toolCall') {
|
|
1830
|
-
block['arguments'] =
|
|
3060
|
+
block['arguments'] =
|
|
3061
|
+
block.partialJson && block.partialJson.length > 0
|
|
3062
|
+
? parseCompletedToolInput(block.partialJson)
|
|
3063
|
+
: canonicalizeJson(block['arguments'] ?? {});
|
|
1831
3064
|
delete block.partialJson;
|
|
1832
3065
|
stream.push({
|
|
1833
3066
|
type: 'toolcall_end',
|
|
@@ -1836,19 +3069,54 @@ export function streamAnthropicViaBetaMessages(
|
|
|
1836
3069
|
partial: output,
|
|
1837
3070
|
});
|
|
1838
3071
|
}
|
|
1839
|
-
} else if (
|
|
1840
|
-
if (isPlainObject(event['delta'])
|
|
1841
|
-
|
|
3072
|
+
} else if (eventType === 'message_delta') {
|
|
3073
|
+
if (!isPlainObject(event['delta'])) {
|
|
3074
|
+
throw new Error('Anthropic beta messages stream emitted malformed message_delta');
|
|
3075
|
+
}
|
|
3076
|
+
const stopReason = event['delta']['stop_reason'];
|
|
3077
|
+
if (typeof stopReason === 'string') {
|
|
3078
|
+
const mapped = mapStopReason(stopReason);
|
|
3079
|
+
if (mapped === 'error') {
|
|
3080
|
+
throw new Error(
|
|
3081
|
+
`Anthropic beta messages stream emitted unsupported stop reason ${JSON.stringify(stopReason)}`,
|
|
3082
|
+
);
|
|
3083
|
+
}
|
|
3084
|
+
output.stopReason = mapped;
|
|
3085
|
+
sawTerminalStopReason = true;
|
|
3086
|
+
}
|
|
1842
3087
|
updateAnthropicUsage(
|
|
1843
3088
|
output,
|
|
1844
3089
|
isPlainObject(event['usage']) ? event['usage'] : undefined,
|
|
1845
3090
|
model,
|
|
1846
3091
|
);
|
|
3092
|
+
} else if (eventType === 'message_stop') {
|
|
3093
|
+
if (activeContentIndexes.size > 0) {
|
|
3094
|
+
throw new Error(
|
|
3095
|
+
'Anthropic beta messages message_stop arrived with open content blocks',
|
|
3096
|
+
);
|
|
3097
|
+
}
|
|
3098
|
+
sawMessageStop = true;
|
|
3099
|
+
} else {
|
|
3100
|
+
throw new Error(
|
|
3101
|
+
`Anthropic beta messages stream emitted unsupported event ${JSON.stringify(eventType)}`,
|
|
3102
|
+
);
|
|
1847
3103
|
}
|
|
1848
3104
|
}
|
|
1849
3105
|
if (options?.signal?.aborted) throw new Error('Request was aborted');
|
|
1850
|
-
if (
|
|
1851
|
-
throw new Error(
|
|
3106
|
+
if (!sawMessageStart) {
|
|
3107
|
+
throw new Error('Anthropic beta messages stream ended without message_start');
|
|
3108
|
+
}
|
|
3109
|
+
if (!sawMessageStop) {
|
|
3110
|
+
throw new Error('Anthropic beta messages stream ended without message_stop');
|
|
3111
|
+
}
|
|
3112
|
+
if (!sawTerminalStopReason || output.stopReason === 'error') {
|
|
3113
|
+
throw new Error(
|
|
3114
|
+
'Anthropic beta messages stream ended without a valid terminal stop reason',
|
|
3115
|
+
);
|
|
3116
|
+
}
|
|
3117
|
+
if (preparedLineage === undefined)
|
|
3118
|
+
throw new Error('Anthropic attribution completed without prepared cache lineage');
|
|
3119
|
+
appendLineageDiagnostic(output, preparedLineage);
|
|
1852
3120
|
stream.push({ type: 'done', reason: output.stopReason, message: output });
|
|
1853
3121
|
stream.end();
|
|
1854
3122
|
} catch (error) {
|
|
@@ -1860,6 +3128,8 @@ export function streamAnthropicViaBetaMessages(
|
|
|
1860
3128
|
output.errorMessage = error instanceof Error ? error.message : String(error);
|
|
1861
3129
|
stream.push({ type: 'error', reason: output.stopReason, error: output });
|
|
1862
3130
|
stream.end();
|
|
3131
|
+
} finally {
|
|
3132
|
+
if (preparedLineage !== undefined) anthropicLineageCoordinator.release(preparedLineage);
|
|
1863
3133
|
}
|
|
1864
3134
|
})();
|
|
1865
3135
|
|
|
@@ -1898,7 +3168,10 @@ function isAnthropicAttributionClaimProbe(value: unknown): value is AnthropicAtt
|
|
|
1898
3168
|
* publishes ownership after every registration below succeeds; a factory that throws
|
|
1899
3169
|
* cannot strand a false claim that suppresses a healthy later copy.
|
|
1900
3170
|
*/
|
|
1901
|
-
export default function spawnAnthropicAttribution(
|
|
3171
|
+
export default function spawnAnthropicAttribution(
|
|
3172
|
+
pi: PiExtensionHost,
|
|
3173
|
+
dependencies: AnthropicTransportDependencies = {},
|
|
3174
|
+
): void {
|
|
1902
3175
|
const acknowledgements: true[] = [];
|
|
1903
3176
|
const probe: AnthropicAttributionClaimProbe = {
|
|
1904
3177
|
schema_version: ANTHROPIC_ATTRIBUTION_CLAIM_SCHEMA,
|
|
@@ -1914,15 +3187,20 @@ export default function spawnAnthropicAttribution(pi: PiExtensionHost): void {
|
|
|
1914
3187
|
sessionCacheRetention;
|
|
1915
3188
|
|
|
1916
3189
|
// Registration is global but route-scoped by provider name. Keeping it at
|
|
1917
|
-
// factory scope avoids lifecycle-dependent provider availability;
|
|
1918
|
-
// transport
|
|
3190
|
+
// factory scope avoids lifecycle-dependent provider availability; every custom
|
|
3191
|
+
// transport request owns attribution from its request-scoped session options.
|
|
1919
3192
|
pi.registerProvider('anthropic', {
|
|
1920
3193
|
api: 'anthropic-messages',
|
|
1921
3194
|
streamSimple: (model, context, options) =>
|
|
1922
|
-
streamAnthropicViaBetaMessages(
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
3195
|
+
streamAnthropicViaBetaMessages(
|
|
3196
|
+
model,
|
|
3197
|
+
context,
|
|
3198
|
+
{
|
|
3199
|
+
...(options ?? {}),
|
|
3200
|
+
cacheRetention: resolveRegisteredCacheRetention(options, getSessionOverride()),
|
|
3201
|
+
},
|
|
3202
|
+
dependencies,
|
|
3203
|
+
),
|
|
1926
3204
|
});
|
|
1927
3205
|
|
|
1928
3206
|
pi.registerCommand('claude-cache', {
|
|
@@ -1965,16 +3243,6 @@ export default function spawnAnthropicAttribution(pi: PiExtensionHost): void {
|
|
|
1965
3243
|
sessionCacheRetention = restoreAnthropicSessionCacheRetention(ctx.sessionManager.getBranch());
|
|
1966
3244
|
});
|
|
1967
3245
|
|
|
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
3246
|
// Publish ownership last. Extension loading is sequential, so later independent
|
|
1979
3247
|
// copies probe this responder and become inert instead of registering duplicates.
|
|
1980
3248
|
pi.events.on(ANTHROPIC_ATTRIBUTION_CLAIM_CHANNEL, (value) => {
|