@tormentalabs/claude-code-wire-compat 0.1.0-rc.17 → 0.1.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/CHANGELOG.md +36 -2
- package/README.md +53 -1
- package/package.json +2 -1
- package/src/anti-verbosity.ts +219 -0
- package/src/beta-registry.ts +140 -0
- package/src/betas.ts +219 -0
- package/src/build-request.ts +1655 -0
- package/src/contracts.ts +1233 -0
- package/src/count-tokens.ts +84 -0
- package/src/fingerprint.ts +85 -0
- package/src/headers.ts +442 -0
- package/src/index.ts +62 -0
- package/src/metadata.ts +331 -0
- package/src/model-capabilities.ts +295 -0
- package/src/model-identity.ts +45 -0
- package/src/models.ts +46 -0
- package/src/profiles/claude-code-2.1.195.ts +154 -0
- package/src/redaction.ts +521 -0
- package/src/request-body.ts +1924 -0
- package/src/sha256.ts +114 -0
- package/src/system-prompt.ts +222 -0
- package/src/thinking.ts +266 -0
- package/src/unicode.ts +24 -0
|
@@ -0,0 +1,1655 @@
|
|
|
1
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
2
|
+
|
|
3
|
+
import { composeBetas, composeBetasWithAudit } from "./betas.js";
|
|
4
|
+
import type {
|
|
5
|
+
BuiltClaudeCodeCountTokensRequest,
|
|
6
|
+
BuiltClaudeCodeRequest,
|
|
7
|
+
ClaudeCodeBetaOverrides,
|
|
8
|
+
ClaudeCodeCapabilities,
|
|
9
|
+
ClaudeCodeCapabilityDecisions,
|
|
10
|
+
ClaudeCodeExtraHeaderPolicy,
|
|
11
|
+
ClaudeCodeProfileOverride,
|
|
12
|
+
ClaudeCodeProtocolProfile,
|
|
13
|
+
ClaudeCodeCountTokensInput,
|
|
14
|
+
ClaudeCodeRequestInput,
|
|
15
|
+
HeaderPair,
|
|
16
|
+
RedactedRequestEvidence,
|
|
17
|
+
} from "./contracts.js";
|
|
18
|
+
import { ClaudeCodeWireError } from "./contracts.js";
|
|
19
|
+
import {
|
|
20
|
+
buildCountTokensBody,
|
|
21
|
+
filterCountTokensBetas,
|
|
22
|
+
TOKEN_COUNTING_BETA,
|
|
23
|
+
} from "./count-tokens.js";
|
|
24
|
+
import { createBillingBlock } from "./fingerprint.js";
|
|
25
|
+
import { buildOrderedHeaderPlan, buildOrderedHeaders } from "./headers.js";
|
|
26
|
+
import {
|
|
27
|
+
buildCorrelatedMetadata,
|
|
28
|
+
validateRuntimeIdentity,
|
|
29
|
+
} from "./metadata.js";
|
|
30
|
+
import { resolveModel } from "./models.js";
|
|
31
|
+
import { CLAUDE_CODE_2_1_195_PROFILE } from "./profiles/claude-code-2.1.195.js";
|
|
32
|
+
import type { NormalizedRequestInput } from "./redaction.js";
|
|
33
|
+
import { buildRedactedEvidence, toSafeErrorDetails } from "./redaction.js";
|
|
34
|
+
import {
|
|
35
|
+
buildCanonicalBody,
|
|
36
|
+
canonicalCountTokensLists,
|
|
37
|
+
} from "./request-body.js";
|
|
38
|
+
import { sha256Hex } from "./sha256.js";
|
|
39
|
+
import { buildCanonicalSystem, IDENTITY_TEXT } from "./system-prompt.js";
|
|
40
|
+
import { isThinkingDisplayActive } from "./thinking.js";
|
|
41
|
+
import { classifySurrogateAt } from "./unicode.js";
|
|
42
|
+
|
|
43
|
+
const METHOD = "POST";
|
|
44
|
+
const MAX_INPUT_DEPTH = 100;
|
|
45
|
+
const MAX_INPUT_SIZE = 1_000_000;
|
|
46
|
+
const FORBIDDEN_KEYS = new Set(["__proto__", "prototype", "constructor"]);
|
|
47
|
+
const INPUT_KEYS = new Set([
|
|
48
|
+
"accessToken",
|
|
49
|
+
"model",
|
|
50
|
+
"maxTokens",
|
|
51
|
+
"messages",
|
|
52
|
+
"system",
|
|
53
|
+
"tools",
|
|
54
|
+
"cacheControl",
|
|
55
|
+
"runtime",
|
|
56
|
+
"capabilities",
|
|
57
|
+
"profileOverride",
|
|
58
|
+
"thinking",
|
|
59
|
+
"effort",
|
|
60
|
+
"metadata",
|
|
61
|
+
"experimentalBodyFields",
|
|
62
|
+
"contextManagement",
|
|
63
|
+
"outputConfig",
|
|
64
|
+
"speed",
|
|
65
|
+
"serviceTier",
|
|
66
|
+
"outputFormat",
|
|
67
|
+
"toolChoice",
|
|
68
|
+
"topP",
|
|
69
|
+
"topK",
|
|
70
|
+
"stopSequences",
|
|
71
|
+
"stream",
|
|
72
|
+
"temperature",
|
|
73
|
+
"clientRequestId",
|
|
74
|
+
"app",
|
|
75
|
+
"stainlessRetryCount",
|
|
76
|
+
"stainlessHelper",
|
|
77
|
+
"claudeRemoteContainerId",
|
|
78
|
+
"claudeRemoteSessionId",
|
|
79
|
+
"clientApp",
|
|
80
|
+
"anthropicAdditionalProtection",
|
|
81
|
+
"additionalBetas",
|
|
82
|
+
"suppressBetas",
|
|
83
|
+
"suppressBillingBlock",
|
|
84
|
+
"suppressIdentityBlock",
|
|
85
|
+
"preserveThinkingBlockCacheControl",
|
|
86
|
+
"betaOverrides",
|
|
87
|
+
"metadataOverrides",
|
|
88
|
+
"extraHeaders",
|
|
89
|
+
"extraHeaderPolicy",
|
|
90
|
+
"crypto",
|
|
91
|
+
]);
|
|
92
|
+
const BETA_OVERRIDE_KEYS = new Set(["use1MContext"]);
|
|
93
|
+
const METADATA_OVERRIDE_KEYS = new Set(["userId", "userIdFields"]);
|
|
94
|
+
const COUNT_TOKENS_INPUT_KEYS = new Set([
|
|
95
|
+
"accessToken",
|
|
96
|
+
"model",
|
|
97
|
+
"messages",
|
|
98
|
+
"tools",
|
|
99
|
+
"runtime",
|
|
100
|
+
"clientRequestId",
|
|
101
|
+
"profileOverride",
|
|
102
|
+
"crypto",
|
|
103
|
+
"app",
|
|
104
|
+
"stainlessRetryCount",
|
|
105
|
+
"stainlessHelper",
|
|
106
|
+
"claudeRemoteContainerId",
|
|
107
|
+
"claudeRemoteSessionId",
|
|
108
|
+
"clientApp",
|
|
109
|
+
"anthropicAdditionalProtection",
|
|
110
|
+
"extraHeaders",
|
|
111
|
+
]);
|
|
112
|
+
const BUILT_KEYS = new Set(["url", "method", "headers", "body", "evidence"]);
|
|
113
|
+
/**
|
|
114
|
+
* The self-describing prefix of the canonical billing block's text.
|
|
115
|
+
*
|
|
116
|
+
* `buildBillingBlock` emits
|
|
117
|
+
* `x-anthropic-billing-header: cc_version=<version>.<fingerprint>; ...`, whose
|
|
118
|
+
* tail varies per request, so only this fixed head can anchor a structural
|
|
119
|
+
* check. It is what lets the parser CONFIRM that a request which claims to
|
|
120
|
+
* carry the billing block actually carries it.
|
|
121
|
+
*/
|
|
122
|
+
const BILLING_BLOCK_TEXT_PREFIX = "x-anthropic-billing-header: cc_version=";
|
|
123
|
+
const EVIDENCE_KEYS = new Set([
|
|
124
|
+
"profileId",
|
|
125
|
+
"url",
|
|
126
|
+
"method",
|
|
127
|
+
"modelFamily",
|
|
128
|
+
"logicalHeaderNames",
|
|
129
|
+
"betaFeatures",
|
|
130
|
+
"bodySha256",
|
|
131
|
+
"bodyByteLength",
|
|
132
|
+
"messageCount",
|
|
133
|
+
"systemBlockCount",
|
|
134
|
+
"capabilityDecisions",
|
|
135
|
+
"droppedExtraHeaderNames",
|
|
136
|
+
"suppressedBetaNames",
|
|
137
|
+
"billingBlockSuppressed",
|
|
138
|
+
"identityBlockSuppressed",
|
|
139
|
+
"thinkingBlockCacheControlPreserved",
|
|
140
|
+
]);
|
|
141
|
+
const CAPABILITY_KEYS = [
|
|
142
|
+
"thinking",
|
|
143
|
+
"adaptiveThinking",
|
|
144
|
+
"interleavedThinking",
|
|
145
|
+
"effort",
|
|
146
|
+
"maxEffort",
|
|
147
|
+
"xhighEffort",
|
|
148
|
+
"contextManagement",
|
|
149
|
+
"temperature",
|
|
150
|
+
"rejectsDisabledThinking",
|
|
151
|
+
] as const;
|
|
152
|
+
const CAPABILITY_KEY_SET = new Set(CAPABILITY_KEYS);
|
|
153
|
+
/** Adds the optional package-extension override keys carried by evidence. */
|
|
154
|
+
const CAPABILITY_DECISION_KEY_SET = new Set([
|
|
155
|
+
...CAPABILITY_KEYS,
|
|
156
|
+
"use1MContext",
|
|
157
|
+
]);
|
|
158
|
+
const OVERRIDE_KEYS = new Set([
|
|
159
|
+
"id",
|
|
160
|
+
"cliVersion",
|
|
161
|
+
"sdkVersion",
|
|
162
|
+
"entrypoint",
|
|
163
|
+
"userAgent",
|
|
164
|
+
"buildTime",
|
|
165
|
+
"gitSha",
|
|
166
|
+
"attributionHeaderEnabled",
|
|
167
|
+
"contextHintEnabled",
|
|
168
|
+
"betaPolicy",
|
|
169
|
+
"supportedModels",
|
|
170
|
+
]);
|
|
171
|
+
const MODEL_KEYS = new Set([
|
|
172
|
+
"family",
|
|
173
|
+
"context",
|
|
174
|
+
"capabilities",
|
|
175
|
+
"defaultEffort",
|
|
176
|
+
]);
|
|
177
|
+
const BETA_POLICY_KEYS = new Set([
|
|
178
|
+
"oauthAuthenticated",
|
|
179
|
+
"experimentalBetasEnabled",
|
|
180
|
+
"oneMillionContextEnabled",
|
|
181
|
+
"interleavedThinkingEnabled",
|
|
182
|
+
"interactive",
|
|
183
|
+
"thinkingSummariesShown",
|
|
184
|
+
"thinkingTokenCountEnabled",
|
|
185
|
+
"narrationSummariesEnabled",
|
|
186
|
+
"structuredOutputsEnabled",
|
|
187
|
+
"afkModeEnabled",
|
|
188
|
+
"cacheDiagnosisEnabled",
|
|
189
|
+
]);
|
|
190
|
+
|
|
191
|
+
type UnknownRecord = Readonly<Record<string, unknown>>;
|
|
192
|
+
|
|
193
|
+
function fail(
|
|
194
|
+
code: ConstructorParameters<typeof ClaudeCodeWireError>[0] = "INVALID_INPUT",
|
|
195
|
+
): never {
|
|
196
|
+
throw new ClaudeCodeWireError(code);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function isRecord(value: unknown): value is UnknownRecord {
|
|
200
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function ownValue(value: object, key: string): unknown {
|
|
204
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
205
|
+
if (descriptor === undefined || !("value" in descriptor)) fail();
|
|
206
|
+
return descriptor.value;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function present<T>(value: T | undefined): T {
|
|
210
|
+
if (value === undefined) fail();
|
|
211
|
+
return value;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function assertExactKeys(
|
|
215
|
+
value: Readonly<Record<string, unknown>>,
|
|
216
|
+
allowed: ReadonlySet<string>,
|
|
217
|
+
): void {
|
|
218
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
219
|
+
if (typeof key !== "string" || !allowed.has(key)) fail("INVALID_INPUT");
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Screens one string from the caller's input graph.
|
|
225
|
+
*
|
|
226
|
+
* TAB (0x09), LF (0x0A) and CR (0x0D) are ALLOWED. This function walks the
|
|
227
|
+
* whole input graph, which is overwhelmingly BODY content — message text,
|
|
228
|
+
* system blocks, tool descriptions — where a line break is ordinary prose that
|
|
229
|
+
* `JSON.stringify` escapes on the way out. Rejecting them here made the package
|
|
230
|
+
* unusable for real traffic: no genuine prompt is a single line.
|
|
231
|
+
*
|
|
232
|
+
* The strict rule those three characters used to be caught by is a HEADER rule,
|
|
233
|
+
* and it still lives where it belongs and still applies in full:
|
|
234
|
+
* `assertHeaderText` in `src/headers.ts` rejects every control character,
|
|
235
|
+
* including these three, because a bare LF in a header is request smuggling.
|
|
236
|
+
* `src/metadata.ts` is likewise unchanged: `user_id` and metadata keys are
|
|
237
|
+
* identifiers that travel as JSON inside a header, not prose.
|
|
238
|
+
*
|
|
239
|
+
* Every other C0 control (0x00-0x08, 0x0B, 0x0C, 0x0E-0x1F) and DEL (0x7F)
|
|
240
|
+
* stay rejected: they have no meaning in prompt text and are a reliable signal
|
|
241
|
+
* of a corrupted or hostile input.
|
|
242
|
+
*
|
|
243
|
+
* LONE SURROGATES stay rejected in every context, deliberately. `TextEncoder`
|
|
244
|
+
* silently replaces them with U+FFFD, so an unpaired surrogate would corrupt
|
|
245
|
+
* the body — and the body hash recorded in evidence — with no error anywhere.
|
|
246
|
+
*/
|
|
247
|
+
function inspectString(value: string): number {
|
|
248
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
249
|
+
const unit = value.charCodeAt(index);
|
|
250
|
+
if (
|
|
251
|
+
(unit <= 0x1f && unit !== 0x09 && unit !== 0x0a && unit !== 0x0d) ||
|
|
252
|
+
unit === 0x7f
|
|
253
|
+
) {
|
|
254
|
+
fail("INVALID_UNICODE");
|
|
255
|
+
}
|
|
256
|
+
const classification = classifySurrogateAt(value, index);
|
|
257
|
+
if (classification === "loneSurrogate") fail("INVALID_UNICODE");
|
|
258
|
+
if (classification === "surrogatePair") index += 1;
|
|
259
|
+
}
|
|
260
|
+
return new TextEncoder().encode(value).byteLength;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function inspectGraph(value: unknown): void {
|
|
264
|
+
const active = new WeakSet();
|
|
265
|
+
let size = 0;
|
|
266
|
+
|
|
267
|
+
function visit(current: unknown, depth: number): void {
|
|
268
|
+
if (depth > MAX_INPUT_DEPTH) fail("INPUT_TOO_DEEP");
|
|
269
|
+
if (typeof current === "string") {
|
|
270
|
+
size += inspectString(current);
|
|
271
|
+
} else if (
|
|
272
|
+
current === null ||
|
|
273
|
+
typeof current === "boolean" ||
|
|
274
|
+
typeof current === "number"
|
|
275
|
+
) {
|
|
276
|
+
if (typeof current === "number" && !Number.isFinite(current)) fail();
|
|
277
|
+
size += 1;
|
|
278
|
+
} else if (typeof current !== "object") {
|
|
279
|
+
fail();
|
|
280
|
+
} else {
|
|
281
|
+
const prototype = Reflect.getPrototypeOf(current);
|
|
282
|
+
if (
|
|
283
|
+
prototype !== null &&
|
|
284
|
+
prototype !== Object.prototype &&
|
|
285
|
+
prototype !== Array.prototype
|
|
286
|
+
) {
|
|
287
|
+
fail();
|
|
288
|
+
}
|
|
289
|
+
if (active.has(current)) fail("CYCLIC_INPUT");
|
|
290
|
+
active.add(current);
|
|
291
|
+
const keys = Reflect.ownKeys(current);
|
|
292
|
+
size += keys.length;
|
|
293
|
+
for (const key of keys) {
|
|
294
|
+
if (typeof key !== "string" || FORBIDDEN_KEYS.has(key)) fail();
|
|
295
|
+
size += inspectString(key);
|
|
296
|
+
visit(ownValue(current, key), depth + 1);
|
|
297
|
+
}
|
|
298
|
+
active.delete(current);
|
|
299
|
+
}
|
|
300
|
+
if (size > MAX_INPUT_SIZE) fail("INPUT_TOO_LARGE");
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
visit(value, 0);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function containsString(value: unknown, target: string): boolean {
|
|
307
|
+
if (typeof value === "string") return value === target;
|
|
308
|
+
if (value === null || typeof value !== "object") return false;
|
|
309
|
+
return Reflect.ownKeys(value).some((key) =>
|
|
310
|
+
typeof key === "string"
|
|
311
|
+
? containsString(ownValue(value, key), target)
|
|
312
|
+
: false,
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function validateProfile(
|
|
317
|
+
profile: ClaudeCodeProtocolProfile,
|
|
318
|
+
): ClaudeCodeProtocolProfile {
|
|
319
|
+
if (profile !== CLAUDE_CODE_2_1_195_PROFILE) fail();
|
|
320
|
+
return profile;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function isCryptoProvider(value: unknown): value is Pick<Crypto, "subtle"> {
|
|
324
|
+
if (!isRecord(value)) return false;
|
|
325
|
+
const subtleDescriptor = Object.getOwnPropertyDescriptor(value, "subtle");
|
|
326
|
+
if (
|
|
327
|
+
subtleDescriptor === undefined ||
|
|
328
|
+
!("value" in subtleDescriptor) ||
|
|
329
|
+
!isRecord(subtleDescriptor.value)
|
|
330
|
+
) {
|
|
331
|
+
return false;
|
|
332
|
+
}
|
|
333
|
+
const digestDescriptor = Object.getOwnPropertyDescriptor(
|
|
334
|
+
subtleDescriptor.value,
|
|
335
|
+
"digest",
|
|
336
|
+
);
|
|
337
|
+
return (
|
|
338
|
+
digestDescriptor !== undefined &&
|
|
339
|
+
"value" in digestDescriptor &&
|
|
340
|
+
typeof digestDescriptor.value === "function"
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function validateCrypto(value: unknown): Pick<Crypto, "subtle"> | undefined {
|
|
345
|
+
if (value === undefined) return undefined;
|
|
346
|
+
if (!isCryptoProvider(value)) fail("CRYPTO_UNAVAILABLE");
|
|
347
|
+
return value;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function requireNonEmptyString(value: unknown): string {
|
|
351
|
+
if (typeof value !== "string" || value.length === 0) fail();
|
|
352
|
+
return value;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function parseCatalogueCapabilities(value: unknown): readonly string[] {
|
|
356
|
+
if (
|
|
357
|
+
!Array.isArray(value) ||
|
|
358
|
+
!value.every((item) => typeof item === "string")
|
|
359
|
+
) {
|
|
360
|
+
throw new ClaudeCodeWireError("INVALID_INPUT");
|
|
361
|
+
}
|
|
362
|
+
return Object.freeze([...value]);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function parseCatalogueContext(value: unknown): Readonly<{
|
|
366
|
+
readonly window: number;
|
|
367
|
+
readonly native1m?: boolean;
|
|
368
|
+
readonly supports1mBeta?: boolean;
|
|
369
|
+
}> {
|
|
370
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
371
|
+
throw new ClaudeCodeWireError("INVALID_INPUT");
|
|
372
|
+
}
|
|
373
|
+
const keys = Reflect.ownKeys(value);
|
|
374
|
+
if (
|
|
375
|
+
keys.some(
|
|
376
|
+
(key) =>
|
|
377
|
+
typeof key !== "string" ||
|
|
378
|
+
(key !== "window" && key !== "native1m" && key !== "supports1mBeta"),
|
|
379
|
+
)
|
|
380
|
+
) {
|
|
381
|
+
throw new ClaudeCodeWireError("INVALID_INPUT");
|
|
382
|
+
}
|
|
383
|
+
const window: unknown = Reflect.get(value, "window");
|
|
384
|
+
const native1m: unknown = Reflect.get(value, "native1m");
|
|
385
|
+
const supports1mBeta: unknown = Reflect.get(value, "supports1mBeta");
|
|
386
|
+
if (
|
|
387
|
+
typeof window !== "number" ||
|
|
388
|
+
!Number.isFinite(window) ||
|
|
389
|
+
window <= 0 ||
|
|
390
|
+
(native1m !== undefined && typeof native1m !== "boolean") ||
|
|
391
|
+
(supports1mBeta !== undefined && typeof supports1mBeta !== "boolean")
|
|
392
|
+
) {
|
|
393
|
+
throw new ClaudeCodeWireError("INVALID_INPUT");
|
|
394
|
+
}
|
|
395
|
+
return Object.freeze({
|
|
396
|
+
window,
|
|
397
|
+
...(native1m === undefined ? {} : { native1m }),
|
|
398
|
+
...(supports1mBeta === undefined ? {} : { supports1mBeta }),
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function parseDefaultEffort(
|
|
403
|
+
value: unknown,
|
|
404
|
+
): "low" | "medium" | "high" | "xhigh" | "max" {
|
|
405
|
+
if (
|
|
406
|
+
value !== "low" &&
|
|
407
|
+
value !== "medium" &&
|
|
408
|
+
value !== "high" &&
|
|
409
|
+
value !== "xhigh" &&
|
|
410
|
+
value !== "max"
|
|
411
|
+
) {
|
|
412
|
+
throw new ClaudeCodeWireError("INVALID_INPUT");
|
|
413
|
+
}
|
|
414
|
+
return value;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function parseBoolean(value: unknown): boolean {
|
|
418
|
+
if (typeof value !== "boolean") {
|
|
419
|
+
throw new ClaudeCodeWireError("INVALID_INPUT");
|
|
420
|
+
}
|
|
421
|
+
return value;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function parseBetaPolicy(
|
|
425
|
+
value: unknown,
|
|
426
|
+
): ClaudeCodeProtocolProfile["betaPolicy"] {
|
|
427
|
+
if (!isRecord(value)) fail();
|
|
428
|
+
assertExactKeys(value, BETA_POLICY_KEYS);
|
|
429
|
+
if (Reflect.ownKeys(value).length !== BETA_POLICY_KEYS.size) fail();
|
|
430
|
+
return Object.freeze({
|
|
431
|
+
oauthAuthenticated: parseBoolean(ownValue(value, "oauthAuthenticated")),
|
|
432
|
+
experimentalBetasEnabled: parseBoolean(
|
|
433
|
+
ownValue(value, "experimentalBetasEnabled"),
|
|
434
|
+
),
|
|
435
|
+
oneMillionContextEnabled: parseBoolean(
|
|
436
|
+
ownValue(value, "oneMillionContextEnabled"),
|
|
437
|
+
),
|
|
438
|
+
interleavedThinkingEnabled: parseBoolean(
|
|
439
|
+
ownValue(value, "interleavedThinkingEnabled"),
|
|
440
|
+
),
|
|
441
|
+
interactive: parseBoolean(ownValue(value, "interactive")),
|
|
442
|
+
thinkingSummariesShown: parseBoolean(
|
|
443
|
+
ownValue(value, "thinkingSummariesShown"),
|
|
444
|
+
),
|
|
445
|
+
thinkingTokenCountEnabled: parseBoolean(
|
|
446
|
+
ownValue(value, "thinkingTokenCountEnabled"),
|
|
447
|
+
),
|
|
448
|
+
narrationSummariesEnabled: parseBoolean(
|
|
449
|
+
ownValue(value, "narrationSummariesEnabled"),
|
|
450
|
+
),
|
|
451
|
+
structuredOutputsEnabled: parseBoolean(
|
|
452
|
+
ownValue(value, "structuredOutputsEnabled"),
|
|
453
|
+
),
|
|
454
|
+
afkModeEnabled: parseBoolean(ownValue(value, "afkModeEnabled")),
|
|
455
|
+
cacheDiagnosisEnabled: parseBoolean(
|
|
456
|
+
ownValue(value, "cacheDiagnosisEnabled"),
|
|
457
|
+
),
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function parseSupportedModels(
|
|
462
|
+
value: unknown,
|
|
463
|
+
): ClaudeCodeProtocolProfile["supportedModels"] {
|
|
464
|
+
if (!isRecord(value) || Reflect.ownKeys(value).length === 0) fail();
|
|
465
|
+
const result: Record<
|
|
466
|
+
string,
|
|
467
|
+
ClaudeCodeProtocolProfile["supportedModels"][string]
|
|
468
|
+
> = {};
|
|
469
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
470
|
+
if (typeof key !== "string" || key.length === 0) fail();
|
|
471
|
+
const model = ownValue(value, key);
|
|
472
|
+
if (!isRecord(model)) fail();
|
|
473
|
+
assertExactKeys(model, MODEL_KEYS);
|
|
474
|
+
const family = ownValue(model, "family");
|
|
475
|
+
if (
|
|
476
|
+
family !== "haiku" &&
|
|
477
|
+
family !== "sonnet" &&
|
|
478
|
+
family !== "opus" &&
|
|
479
|
+
family !== "fable" &&
|
|
480
|
+
family !== "mythos" &&
|
|
481
|
+
family !== "unknown"
|
|
482
|
+
)
|
|
483
|
+
fail();
|
|
484
|
+
result[key] = Object.freeze({
|
|
485
|
+
family,
|
|
486
|
+
...(Object.hasOwn(model, "context")
|
|
487
|
+
? { context: parseCatalogueContext(ownValue(model, "context")) }
|
|
488
|
+
: {}),
|
|
489
|
+
capabilities: parseCatalogueCapabilities(ownValue(model, "capabilities")),
|
|
490
|
+
...(Object.hasOwn(model, "defaultEffort")
|
|
491
|
+
? {
|
|
492
|
+
defaultEffort: parseDefaultEffort(ownValue(model, "defaultEffort")),
|
|
493
|
+
}
|
|
494
|
+
: {}),
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
return Object.freeze(result);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function validateProfileOverride(value: unknown): ClaudeCodeProfileOverride {
|
|
501
|
+
if (!isRecord(value)) fail();
|
|
502
|
+
assertExactKeys(value, OVERRIDE_KEYS);
|
|
503
|
+
if (Reflect.ownKeys(value).length === 0) fail();
|
|
504
|
+
|
|
505
|
+
const cliVersion = Object.hasOwn(value, "cliVersion")
|
|
506
|
+
? requireNonEmptyString(ownValue(value, "cliVersion"))
|
|
507
|
+
: undefined;
|
|
508
|
+
const userAgent = Object.hasOwn(value, "userAgent")
|
|
509
|
+
? requireNonEmptyString(ownValue(value, "userAgent"))
|
|
510
|
+
: undefined;
|
|
511
|
+
// `cc_version` in the billing header derives from `cliVersion` while the
|
|
512
|
+
// user agent is separate. Overriding one alone would emit a self-inconsistent
|
|
513
|
+
// client signature, precisely the fingerprint mismatch this package prevents.
|
|
514
|
+
if (cliVersion !== undefined && !userAgent?.includes(cliVersion)) {
|
|
515
|
+
fail();
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
const attributionHeaderEnabled = Object.hasOwn(
|
|
519
|
+
value,
|
|
520
|
+
"attributionHeaderEnabled",
|
|
521
|
+
)
|
|
522
|
+
? ownValue(value, "attributionHeaderEnabled")
|
|
523
|
+
: undefined;
|
|
524
|
+
if (
|
|
525
|
+
attributionHeaderEnabled !== undefined &&
|
|
526
|
+
typeof attributionHeaderEnabled !== "boolean"
|
|
527
|
+
) {
|
|
528
|
+
fail();
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
return Object.freeze({
|
|
532
|
+
...(Object.hasOwn(value, "id")
|
|
533
|
+
? { id: requireNonEmptyString(ownValue(value, "id")) }
|
|
534
|
+
: {}),
|
|
535
|
+
...(cliVersion === undefined ? {} : { cliVersion }),
|
|
536
|
+
...(Object.hasOwn(value, "sdkVersion")
|
|
537
|
+
? { sdkVersion: requireNonEmptyString(ownValue(value, "sdkVersion")) }
|
|
538
|
+
: {}),
|
|
539
|
+
...(Object.hasOwn(value, "entrypoint")
|
|
540
|
+
? { entrypoint: requireNonEmptyString(ownValue(value, "entrypoint")) }
|
|
541
|
+
: {}),
|
|
542
|
+
...(userAgent === undefined ? {} : { userAgent }),
|
|
543
|
+
...(Object.hasOwn(value, "buildTime")
|
|
544
|
+
? { buildTime: requireNonEmptyString(ownValue(value, "buildTime")) }
|
|
545
|
+
: {}),
|
|
546
|
+
...(Object.hasOwn(value, "gitSha")
|
|
547
|
+
? { gitSha: requireNonEmptyString(ownValue(value, "gitSha")) }
|
|
548
|
+
: {}),
|
|
549
|
+
...(attributionHeaderEnabled === undefined
|
|
550
|
+
? {}
|
|
551
|
+
: { attributionHeaderEnabled }),
|
|
552
|
+
...(Object.hasOwn(value, "contextHintEnabled")
|
|
553
|
+
? {
|
|
554
|
+
contextHintEnabled: parseBoolean(
|
|
555
|
+
ownValue(value, "contextHintEnabled"),
|
|
556
|
+
),
|
|
557
|
+
}
|
|
558
|
+
: {}),
|
|
559
|
+
...(Object.hasOwn(value, "betaPolicy")
|
|
560
|
+
? { betaPolicy: parseBetaPolicy(ownValue(value, "betaPolicy")) }
|
|
561
|
+
: {}),
|
|
562
|
+
...(Object.hasOwn(value, "supportedModels")
|
|
563
|
+
? {
|
|
564
|
+
supportedModels: parseSupportedModels(
|
|
565
|
+
ownValue(value, "supportedModels"),
|
|
566
|
+
),
|
|
567
|
+
}
|
|
568
|
+
: {}),
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Validates the package-extension beta overrides.
|
|
574
|
+
*
|
|
575
|
+
* An explicitly present key with an `undefined` value is rejected rather than
|
|
576
|
+
* silently treated as absent, so the tri-state stays observable: the caller
|
|
577
|
+
* either states a decision or omits the key.
|
|
578
|
+
*/
|
|
579
|
+
function validateBetaOverrides(value: unknown): ClaudeCodeBetaOverrides {
|
|
580
|
+
if (!isRecord(value)) fail();
|
|
581
|
+
assertExactKeys(value, BETA_OVERRIDE_KEYS);
|
|
582
|
+
return Object.freeze({
|
|
583
|
+
...(Object.hasOwn(value, "use1MContext")
|
|
584
|
+
? { use1MContext: parseBoolean(ownValue(value, "use1MContext")) }
|
|
585
|
+
: {}),
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* Validates the shape of the package-extension metadata overrides.
|
|
591
|
+
*
|
|
592
|
+
* Only the key set is decided here, exactly as `validateBetaOverrides` does.
|
|
593
|
+
* Member values, and the mutual exclusion between them, are decided by
|
|
594
|
+
* `buildCorrelatedMetadata`, which owns the correlation rules.
|
|
595
|
+
*/
|
|
596
|
+
function validateMetadataOverrides(value: unknown): void {
|
|
597
|
+
if (!isRecord(value)) fail();
|
|
598
|
+
assertExactKeys(value, METADATA_OVERRIDE_KEYS);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* Validates the package-extension extra-header policy.
|
|
603
|
+
*
|
|
604
|
+
* The two literals are the whole domain; anything else, including a casing
|
|
605
|
+
* variant or a padded string, is a caller mistake rather than a silent fallback
|
|
606
|
+
* to `strict`, because falling back would emit a request the caller did not ask
|
|
607
|
+
* for.
|
|
608
|
+
*/
|
|
609
|
+
function validateExtraHeaderPolicy(
|
|
610
|
+
value: unknown,
|
|
611
|
+
): ClaudeCodeExtraHeaderPolicy {
|
|
612
|
+
if (value !== "strict" && value !== "dropConflicting") fail();
|
|
613
|
+
return value;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* Validates the package-extension billing-block suppression flag.
|
|
618
|
+
*
|
|
619
|
+
* Only a boolean states a decision. A truthy string or `0` is a caller mistake
|
|
620
|
+
* rather than a coercion target, because coercing would silently change what
|
|
621
|
+
* Anthropic receives for attribution purposes.
|
|
622
|
+
*/
|
|
623
|
+
function validateSuppressBillingBlock(value: unknown): boolean {
|
|
624
|
+
return parseBoolean(value);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/**
|
|
628
|
+
* Validates the package-extension identity-block suppression flag.
|
|
629
|
+
*
|
|
630
|
+
* Same contract as `validateSuppressBillingBlock`: only a boolean states a
|
|
631
|
+
* decision, because coercing a truthy string would silently drop a canonical
|
|
632
|
+
* block the genuine client always sends.
|
|
633
|
+
*/
|
|
634
|
+
function validateSuppressIdentityBlock(value: unknown): boolean {
|
|
635
|
+
return parseBoolean(value);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* Validates the package-extension thinking-block `cache_control` seam flag.
|
|
640
|
+
*
|
|
641
|
+
* Same contract as the suppression flags: only a boolean states a decision,
|
|
642
|
+
* because coercing a truthy string would silently widen the thinking-block
|
|
643
|
+
* allowlist this package pins against the genuine client.
|
|
644
|
+
*/
|
|
645
|
+
function validatePreserveThinkingBlockCacheControl(value: unknown): boolean {
|
|
646
|
+
return parseBoolean(value);
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
function createEffectiveProfile(
|
|
650
|
+
pinnedProfile: ClaudeCodeProtocolProfile,
|
|
651
|
+
override: ClaudeCodeProfileOverride | undefined,
|
|
652
|
+
): ClaudeCodeProtocolProfile {
|
|
653
|
+
if (override === undefined) return pinnedProfile;
|
|
654
|
+
return deepFreeze({ ...pinnedProfile, ...override });
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function applyEffectiveProfileHeaders(
|
|
658
|
+
headers: readonly HeaderPair[],
|
|
659
|
+
profile: ClaudeCodeProtocolProfile,
|
|
660
|
+
): readonly HeaderPair[] {
|
|
661
|
+
return Object.freeze(
|
|
662
|
+
headers.map(([name, value]): HeaderPair => {
|
|
663
|
+
if (name === "user-agent") {
|
|
664
|
+
return Object.freeze([name, profile.userAgent]);
|
|
665
|
+
}
|
|
666
|
+
if (name === "x-stainless-package-version") {
|
|
667
|
+
return Object.freeze([name, profile.sdkVersion]);
|
|
668
|
+
}
|
|
669
|
+
return Object.freeze([name, value]);
|
|
670
|
+
}),
|
|
671
|
+
);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
function validateInput(input: ClaudeCodeRequestInput): {
|
|
675
|
+
readonly source: ClaudeCodeRequestInput;
|
|
676
|
+
readonly clientRequestId: string;
|
|
677
|
+
readonly crypto: Pick<Crypto, "subtle"> | undefined;
|
|
678
|
+
readonly profileOverride: ClaudeCodeProfileOverride | undefined;
|
|
679
|
+
readonly betaOverrides: ClaudeCodeBetaOverrides | undefined;
|
|
680
|
+
readonly extraHeaderPolicy: ClaudeCodeExtraHeaderPolicy | undefined;
|
|
681
|
+
readonly suppressBillingBlock: boolean;
|
|
682
|
+
readonly suppressIdentityBlock: boolean;
|
|
683
|
+
readonly preserveThinkingBlockCacheControl: boolean;
|
|
684
|
+
} {
|
|
685
|
+
if (!isRecord(input)) fail();
|
|
686
|
+
assertExactKeys(input, INPUT_KEYS);
|
|
687
|
+
const graph: Record<string, unknown> = {};
|
|
688
|
+
for (const key of Reflect.ownKeys(input)) {
|
|
689
|
+
if (key !== "crypto" && typeof key === "string") {
|
|
690
|
+
graph[key] = ownValue(input, key);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
inspectGraph(graph);
|
|
694
|
+
const accessToken = ownValue(input, "accessToken");
|
|
695
|
+
if (
|
|
696
|
+
typeof accessToken !== "string" ||
|
|
697
|
+
typeof ownValue(input, "model") !== "string" ||
|
|
698
|
+
!Array.isArray(ownValue(input, "messages"))
|
|
699
|
+
) {
|
|
700
|
+
fail();
|
|
701
|
+
}
|
|
702
|
+
for (const key of Reflect.ownKeys(input)) {
|
|
703
|
+
if (
|
|
704
|
+
typeof key === "string" &&
|
|
705
|
+
key !== "accessToken" &&
|
|
706
|
+
key !== "crypto" &&
|
|
707
|
+
containsString(ownValue(input, key), accessToken)
|
|
708
|
+
) {
|
|
709
|
+
fail();
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
const clientRequestId = ownValue(input, "clientRequestId");
|
|
713
|
+
if (typeof clientRequestId !== "string" || clientRequestId.length === 0)
|
|
714
|
+
fail();
|
|
715
|
+
const cryptoValue = Object.hasOwn(input, "crypto")
|
|
716
|
+
? validateCrypto(ownValue(input, "crypto"))
|
|
717
|
+
: undefined;
|
|
718
|
+
const profileOverride = Object.hasOwn(input, "profileOverride")
|
|
719
|
+
? validateProfileOverride(ownValue(input, "profileOverride"))
|
|
720
|
+
: undefined;
|
|
721
|
+
const betaOverrides = Object.hasOwn(input, "betaOverrides")
|
|
722
|
+
? validateBetaOverrides(ownValue(input, "betaOverrides"))
|
|
723
|
+
: undefined;
|
|
724
|
+
if (Object.hasOwn(input, "metadataOverrides")) {
|
|
725
|
+
validateMetadataOverrides(ownValue(input, "metadataOverrides"));
|
|
726
|
+
}
|
|
727
|
+
const extraHeaderPolicy = Object.hasOwn(input, "extraHeaderPolicy")
|
|
728
|
+
? validateExtraHeaderPolicy(ownValue(input, "extraHeaderPolicy"))
|
|
729
|
+
: undefined;
|
|
730
|
+
const suppressBillingBlock = Object.hasOwn(input, "suppressBillingBlock")
|
|
731
|
+
? validateSuppressBillingBlock(ownValue(input, "suppressBillingBlock"))
|
|
732
|
+
: false;
|
|
733
|
+
const suppressIdentityBlock = Object.hasOwn(input, "suppressIdentityBlock")
|
|
734
|
+
? validateSuppressIdentityBlock(ownValue(input, "suppressIdentityBlock"))
|
|
735
|
+
: false;
|
|
736
|
+
const preserveThinkingBlockCacheControl = Object.hasOwn(
|
|
737
|
+
input,
|
|
738
|
+
"preserveThinkingBlockCacheControl",
|
|
739
|
+
)
|
|
740
|
+
? validatePreserveThinkingBlockCacheControl(
|
|
741
|
+
ownValue(input, "preserveThinkingBlockCacheControl"),
|
|
742
|
+
)
|
|
743
|
+
: false;
|
|
744
|
+
return {
|
|
745
|
+
source: input,
|
|
746
|
+
clientRequestId,
|
|
747
|
+
crypto: cryptoValue,
|
|
748
|
+
profileOverride,
|
|
749
|
+
betaOverrides,
|
|
750
|
+
extraHeaderPolicy,
|
|
751
|
+
suppressBillingBlock,
|
|
752
|
+
suppressIdentityBlock,
|
|
753
|
+
preserveThinkingBlockCacheControl,
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
function validateCountTokensInput(input: ClaudeCodeCountTokensInput): {
|
|
758
|
+
readonly source: ClaudeCodeCountTokensInput;
|
|
759
|
+
readonly clientRequestId: string;
|
|
760
|
+
readonly crypto: Pick<Crypto, "subtle"> | undefined;
|
|
761
|
+
readonly profileOverride: ClaudeCodeProfileOverride | undefined;
|
|
762
|
+
} {
|
|
763
|
+
if (!isRecord(input)) fail();
|
|
764
|
+
assertExactKeys(input, COUNT_TOKENS_INPUT_KEYS);
|
|
765
|
+
const graph: Record<string, unknown> = {};
|
|
766
|
+
for (const key of Reflect.ownKeys(input)) {
|
|
767
|
+
if (key !== "crypto" && typeof key === "string") {
|
|
768
|
+
graph[key] = ownValue(input, key);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
inspectGraph(graph);
|
|
772
|
+
const accessToken = ownValue(input, "accessToken");
|
|
773
|
+
const tools = Object.hasOwn(input, "tools")
|
|
774
|
+
? ownValue(input, "tools")
|
|
775
|
+
: undefined;
|
|
776
|
+
if (
|
|
777
|
+
typeof accessToken !== "string" ||
|
|
778
|
+
typeof ownValue(input, "model") !== "string" ||
|
|
779
|
+
!Array.isArray(ownValue(input, "messages")) ||
|
|
780
|
+
(tools !== undefined && !Array.isArray(tools))
|
|
781
|
+
) {
|
|
782
|
+
fail();
|
|
783
|
+
}
|
|
784
|
+
for (const key of Reflect.ownKeys(input)) {
|
|
785
|
+
if (
|
|
786
|
+
typeof key === "string" &&
|
|
787
|
+
key !== "accessToken" &&
|
|
788
|
+
key !== "crypto" &&
|
|
789
|
+
containsString(ownValue(input, key), accessToken)
|
|
790
|
+
) {
|
|
791
|
+
fail();
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
const clientRequestId = ownValue(input, "clientRequestId");
|
|
795
|
+
if (typeof clientRequestId !== "string" || clientRequestId.length === 0) {
|
|
796
|
+
fail();
|
|
797
|
+
}
|
|
798
|
+
const cryptoValue = Object.hasOwn(input, "crypto")
|
|
799
|
+
? validateCrypto(ownValue(input, "crypto"))
|
|
800
|
+
: undefined;
|
|
801
|
+
const profileOverride = Object.hasOwn(input, "profileOverride")
|
|
802
|
+
? validateProfileOverride(ownValue(input, "profileOverride"))
|
|
803
|
+
: undefined;
|
|
804
|
+
return {
|
|
805
|
+
source: input,
|
|
806
|
+
clientRequestId,
|
|
807
|
+
crypto: cryptoValue,
|
|
808
|
+
profileOverride,
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
function requestedCapabilities(
|
|
813
|
+
input: ClaudeCodeRequestInput,
|
|
814
|
+
supported: ClaudeCodeCapabilities,
|
|
815
|
+
): ClaudeCodeCapabilities {
|
|
816
|
+
const raw = input.capabilities;
|
|
817
|
+
if (raw !== undefined) {
|
|
818
|
+
if (!isRecord(raw)) fail("UNSUPPORTED_CAPABILITY");
|
|
819
|
+
assertExactKeys(raw, CAPABILITY_KEY_SET);
|
|
820
|
+
}
|
|
821
|
+
const result: ClaudeCodeCapabilities = {
|
|
822
|
+
thinking: raw?.thinking ?? supported.thinking,
|
|
823
|
+
adaptiveThinking: raw?.adaptiveThinking ?? supported.adaptiveThinking,
|
|
824
|
+
interleavedThinking:
|
|
825
|
+
raw?.interleavedThinking ?? supported.interleavedThinking,
|
|
826
|
+
effort: raw?.effort ?? supported.effort,
|
|
827
|
+
maxEffort: raw?.maxEffort ?? supported.maxEffort,
|
|
828
|
+
xhighEffort: raw?.xhighEffort ?? supported.xhighEffort,
|
|
829
|
+
contextManagement: raw?.contextManagement ?? supported.contextManagement,
|
|
830
|
+
temperature: raw?.temperature ?? supported.temperature,
|
|
831
|
+
rejectsDisabledThinking:
|
|
832
|
+
raw?.rejectsDisabledThinking ?? supported.rejectsDisabledThinking,
|
|
833
|
+
};
|
|
834
|
+
for (const key of CAPABILITY_KEYS) {
|
|
835
|
+
if (typeof result[key] !== "boolean") fail("UNSUPPORTED_CAPABILITY");
|
|
836
|
+
if (result[key] && !supported[key]) {
|
|
837
|
+
throw new ClaudeCodeWireError("UNSUPPORTED_CAPABILITY", {
|
|
838
|
+
capability: key,
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
return Object.freeze(result);
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
/**
|
|
846
|
+
* Selects the text that seeds the billing fingerprint.
|
|
847
|
+
*
|
|
848
|
+
* The source is the FIRST USER MESSAGE, and nothing else. Upstream
|
|
849
|
+
* `lib/mimicry/system-prompt.mjs:134,143` calls
|
|
850
|
+
* `buildAnthropicBillingHeader(version, firstUserMessage, ...)`, which forwards
|
|
851
|
+
* that message to `computeBillingCacheHash(firstUserMessage || "", version)`.
|
|
852
|
+
* The system prompt never contributes.
|
|
853
|
+
*
|
|
854
|
+
* This is protocol-critical: seeding the fingerprint from system text instead
|
|
855
|
+
* would emit a `cc_version` suffix that does not match the pinned wire profile,
|
|
856
|
+
* silently breaking parity while every local test still looked plausible.
|
|
857
|
+
* The committed goldens are the ground truth here — `outgoing-foreground.json`
|
|
858
|
+
* pairs first user text `hello wire compat` with `cc_version=2.1.195.0f6`.
|
|
859
|
+
*/
|
|
860
|
+
function fingerprintText(input: ClaudeCodeRequestInput): string {
|
|
861
|
+
for (const message of input.messages) {
|
|
862
|
+
if (message.role !== "user") continue;
|
|
863
|
+
if (typeof message.content === "string") return message.content;
|
|
864
|
+
for (const block of message.content) {
|
|
865
|
+
if (block.type === "text") return block.text;
|
|
866
|
+
}
|
|
867
|
+
// The first user message exists but carries no text block. Upstream's
|
|
868
|
+
// `firstUserMessage || ""` fallback applies; later messages never substitute.
|
|
869
|
+
return "";
|
|
870
|
+
}
|
|
871
|
+
return "";
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
function sanitizeError(error: unknown): never {
|
|
875
|
+
if (error instanceof ClaudeCodeWireError) {
|
|
876
|
+
const details = toSafeErrorDetails(error);
|
|
877
|
+
const safeDetails = Object.fromEntries(
|
|
878
|
+
Object.entries(details).filter(([key]) => key !== "code"),
|
|
879
|
+
);
|
|
880
|
+
throw new ClaudeCodeWireError(error.code, safeDetails);
|
|
881
|
+
}
|
|
882
|
+
throw new ClaudeCodeWireError("INVALID_INPUT");
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
function deepFreeze<T>(value: T): T {
|
|
886
|
+
if (value !== null && typeof value === "object" && !Object.isFrozen(value)) {
|
|
887
|
+
for (const key of Reflect.ownKeys(value))
|
|
888
|
+
deepFreeze(Reflect.get(value, key));
|
|
889
|
+
Object.freeze(value);
|
|
890
|
+
}
|
|
891
|
+
return value;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
function parseStringArray(value: unknown): readonly string[] {
|
|
895
|
+
if (!Array.isArray(value)) fail();
|
|
896
|
+
return value.map((entry) => {
|
|
897
|
+
if (typeof entry !== "string") fail();
|
|
898
|
+
return entry;
|
|
899
|
+
});
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
function parseHeaders(value: unknown): readonly HeaderPair[] {
|
|
903
|
+
if (!Array.isArray(value)) fail();
|
|
904
|
+
return value.map((entry): HeaderPair => {
|
|
905
|
+
if (
|
|
906
|
+
!Array.isArray(entry) ||
|
|
907
|
+
entry.length !== 2 ||
|
|
908
|
+
typeof entry[0] !== "string" ||
|
|
909
|
+
typeof entry[1] !== "string"
|
|
910
|
+
) {
|
|
911
|
+
fail();
|
|
912
|
+
}
|
|
913
|
+
return [entry[0], entry[1]];
|
|
914
|
+
});
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
function parseCapabilityDecisions(
|
|
918
|
+
value: unknown,
|
|
919
|
+
): ClaudeCodeCapabilityDecisions {
|
|
920
|
+
if (!isRecord(value)) fail();
|
|
921
|
+
// The nine capability keys are mandatory; the package-extension override keys
|
|
922
|
+
// are optional and must survive the round-trip untouched, so they are allowed
|
|
923
|
+
// here but never synthesized.
|
|
924
|
+
assertExactKeys(value, CAPABILITY_DECISION_KEY_SET);
|
|
925
|
+
// Read each key through a narrowing helper rather than building a record and
|
|
926
|
+
// re-checking it afterwards. The re-check was unreachable -- every value was
|
|
927
|
+
// already proven boolean -- which cost coverage and produced mutants that
|
|
928
|
+
// could not be killed.
|
|
929
|
+
const readBoolean = (key: string): boolean => {
|
|
930
|
+
const entry = ownValue(value, key);
|
|
931
|
+
if (typeof entry !== "boolean") fail();
|
|
932
|
+
return entry;
|
|
933
|
+
};
|
|
934
|
+
return {
|
|
935
|
+
...(Object.hasOwn(value, "use1MContext")
|
|
936
|
+
? { use1MContext: readBoolean("use1MContext") }
|
|
937
|
+
: {}),
|
|
938
|
+
thinking: readBoolean("thinking"),
|
|
939
|
+
adaptiveThinking: readBoolean("adaptiveThinking"),
|
|
940
|
+
interleavedThinking: readBoolean("interleavedThinking"),
|
|
941
|
+
effort: readBoolean("effort"),
|
|
942
|
+
maxEffort: readBoolean("maxEffort"),
|
|
943
|
+
xhighEffort: readBoolean("xhighEffort"),
|
|
944
|
+
contextManagement: readBoolean("contextManagement"),
|
|
945
|
+
temperature: readBoolean("temperature"),
|
|
946
|
+
rejectsDisabledThinking: readBoolean("rejectsDisabledThinking"),
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
function parseEvidence(value: unknown): RedactedRequestEvidence {
|
|
951
|
+
if (!isRecord(value)) fail();
|
|
952
|
+
assertExactKeys(value, EVIDENCE_KEYS);
|
|
953
|
+
const modelFamily = ownValue(value, "modelFamily");
|
|
954
|
+
if (
|
|
955
|
+
modelFamily !== "haiku" &&
|
|
956
|
+
modelFamily !== "sonnet" &&
|
|
957
|
+
modelFamily !== "opus" &&
|
|
958
|
+
modelFamily !== "fable" &&
|
|
959
|
+
modelFamily !== "mythos" &&
|
|
960
|
+
modelFamily !== "unknown"
|
|
961
|
+
) {
|
|
962
|
+
fail();
|
|
963
|
+
}
|
|
964
|
+
const bodySha256 = ownValue(value, "bodySha256");
|
|
965
|
+
const bodyByteLength = ownValue(value, "bodyByteLength");
|
|
966
|
+
const messageCount = ownValue(value, "messageCount");
|
|
967
|
+
const systemBlockCount = ownValue(value, "systemBlockCount");
|
|
968
|
+
if (
|
|
969
|
+
ownValue(value, "profileId") !== CLAUDE_CODE_2_1_195_PROFILE.id ||
|
|
970
|
+
ownValue(value, "url") !== CLAUDE_CODE_2_1_195_PROFILE.endpoint ||
|
|
971
|
+
ownValue(value, "method") !== METHOD ||
|
|
972
|
+
typeof bodySha256 !== "string" ||
|
|
973
|
+
!/^[0-9a-f]{64}$/u.test(bodySha256) ||
|
|
974
|
+
typeof bodyByteLength !== "number" ||
|
|
975
|
+
!Number.isSafeInteger(bodyByteLength) ||
|
|
976
|
+
typeof messageCount !== "number" ||
|
|
977
|
+
!Number.isSafeInteger(messageCount) ||
|
|
978
|
+
typeof systemBlockCount !== "number" ||
|
|
979
|
+
!Number.isSafeInteger(systemBlockCount)
|
|
980
|
+
) {
|
|
981
|
+
fail();
|
|
982
|
+
}
|
|
983
|
+
return {
|
|
984
|
+
profileId: CLAUDE_CODE_2_1_195_PROFILE.id,
|
|
985
|
+
url: CLAUDE_CODE_2_1_195_PROFILE.endpoint,
|
|
986
|
+
method: METHOD,
|
|
987
|
+
modelFamily,
|
|
988
|
+
logicalHeaderNames: parseStringArray(ownValue(value, "logicalHeaderNames")),
|
|
989
|
+
betaFeatures: parseStringArray(ownValue(value, "betaFeatures")),
|
|
990
|
+
bodySha256,
|
|
991
|
+
bodyByteLength,
|
|
992
|
+
messageCount,
|
|
993
|
+
systemBlockCount,
|
|
994
|
+
capabilityDecisions: parseCapabilityDecisions(
|
|
995
|
+
ownValue(value, "capabilityDecisions"),
|
|
996
|
+
),
|
|
997
|
+
// Optional package-extension audit: preserved verbatim when present and
|
|
998
|
+
// never synthesized, exactly like the `use1MContext` decision key.
|
|
999
|
+
...(Object.hasOwn(value, "droppedExtraHeaderNames")
|
|
1000
|
+
? {
|
|
1001
|
+
droppedExtraHeaderNames: parseStringArray(
|
|
1002
|
+
ownValue(value, "droppedExtraHeaderNames"),
|
|
1003
|
+
),
|
|
1004
|
+
}
|
|
1005
|
+
: {}),
|
|
1006
|
+
...(Object.hasOwn(value, "suppressedBetaNames")
|
|
1007
|
+
? {
|
|
1008
|
+
suppressedBetaNames: parseStringArray(
|
|
1009
|
+
ownValue(value, "suppressedBetaNames"),
|
|
1010
|
+
),
|
|
1011
|
+
}
|
|
1012
|
+
: {}),
|
|
1013
|
+
...(Object.hasOwn(value, "billingBlockSuppressed")
|
|
1014
|
+
? {
|
|
1015
|
+
billingBlockSuppressed: parseBoolean(
|
|
1016
|
+
ownValue(value, "billingBlockSuppressed"),
|
|
1017
|
+
),
|
|
1018
|
+
}
|
|
1019
|
+
: {}),
|
|
1020
|
+
...(Object.hasOwn(value, "identityBlockSuppressed")
|
|
1021
|
+
? {
|
|
1022
|
+
identityBlockSuppressed: parseBoolean(
|
|
1023
|
+
ownValue(value, "identityBlockSuppressed"),
|
|
1024
|
+
),
|
|
1025
|
+
}
|
|
1026
|
+
: {}),
|
|
1027
|
+
// Rehydrated here because `assertExactKeys` above already accepts the key:
|
|
1028
|
+
// omitting this branch would silently DROP it, and the round-trip equality
|
|
1029
|
+
// every seam test asserts would fail on an envelope that is entirely legal.
|
|
1030
|
+
...(Object.hasOwn(value, "thinkingBlockCacheControlPreserved")
|
|
1031
|
+
? {
|
|
1032
|
+
thinkingBlockCacheControlPreserved: parseBoolean(
|
|
1033
|
+
ownValue(value, "thinkingBlockCacheControlPreserved"),
|
|
1034
|
+
),
|
|
1035
|
+
}
|
|
1036
|
+
: {}),
|
|
1037
|
+
};
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
/** Reads an own property of a value that is not asserted to be a record. */
|
|
1041
|
+
function ownProperty(value: unknown, key: string): unknown {
|
|
1042
|
+
return isRecord(value) && Object.hasOwn(value, key)
|
|
1043
|
+
? ownValue(value, key)
|
|
1044
|
+
: undefined;
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
/**
|
|
1048
|
+
* Reports whether the emitted `messages` carry a reasoning block that actually
|
|
1049
|
+
* kept a `cache_control` key.
|
|
1050
|
+
*
|
|
1051
|
+
* This is what makes `evidence.thinkingBlockCacheControlPreserved` a record of
|
|
1052
|
+
* what the seam DID rather than of what it was allowed to do, and it is the
|
|
1053
|
+
* check that refutes an envelope claiming the seam over a body carrying no such
|
|
1054
|
+
* block. Presence of the KEY is the test, not truthiness: `cache_control: null`
|
|
1055
|
+
* is a preserved marker too, exactly as it is on every other block type.
|
|
1056
|
+
*/
|
|
1057
|
+
function hasThinkingBlockCacheControl(messages: unknown): boolean {
|
|
1058
|
+
if (!Array.isArray(messages)) return false;
|
|
1059
|
+
return messages.some((message) => {
|
|
1060
|
+
const content = ownProperty(message, "content");
|
|
1061
|
+
return (
|
|
1062
|
+
Array.isArray(content) &&
|
|
1063
|
+
content.some((block) => {
|
|
1064
|
+
const type = ownProperty(block, "type");
|
|
1065
|
+
return (
|
|
1066
|
+
(type === "thinking" || type === "redacted_thinking") &&
|
|
1067
|
+
ownProperty(block, "cache_control") !== undefined
|
|
1068
|
+
);
|
|
1069
|
+
})
|
|
1070
|
+
);
|
|
1071
|
+
});
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
/** Reads a system block's `text` without asserting the block's shape. */
|
|
1075
|
+
function systemBlockText(block: unknown): unknown {
|
|
1076
|
+
return ownProperty(block, "text");
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
/** Recognises the canonical billing block by its fixed, self-describing head. */
|
|
1080
|
+
function isBillingBlockText(text: unknown): boolean {
|
|
1081
|
+
return typeof text === "string" && text.startsWith(BILLING_BLOCK_TEXT_PREFIX);
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
/**
|
|
1085
|
+
* VERIFIES how many canonical blocks the emitted `system` array carries.
|
|
1086
|
+
*
|
|
1087
|
+
* The root seams `suppressBillingBlock` and `suppressIdentityBlock` make four
|
|
1088
|
+
* prefixes legitimate — `[billing, identity]`, `[identity]`, `[billing]` and
|
|
1089
|
+
* `[]` — so no probe over the array alone can tell them apart: an empty prefix
|
|
1090
|
+
* is indistinguishable from a caller-only array. The prefix length is therefore
|
|
1091
|
+
* READ from the evidence flags, which the builder emits only when suppression
|
|
1092
|
+
* actually removed a block.
|
|
1093
|
+
*
|
|
1094
|
+
* Evidence is not trusted blindly. Each block the flags claim is present is
|
|
1095
|
+
* confirmed in place: billing by its `x-anthropic-billing-header: cc_version=`
|
|
1096
|
+
* head (its tail is per-request), identity by the byte-exact `IDENTITY_TEXT`;
|
|
1097
|
+
* a claim that identity was suppressed is confirmed by that text being absent
|
|
1098
|
+
* from the whole array. This is strictly stronger than the position probe it
|
|
1099
|
+
* replaces, which never checked the billing slot at all. Never `cache_control`:
|
|
1100
|
+
* the
|
|
1101
|
+
* `cacheControl.suppressIdentityBlock` seam can emit the identity block with no
|
|
1102
|
+
* marker, so a marker probe would misread a legitimate request.
|
|
1103
|
+
*/
|
|
1104
|
+
function canonicalSystemPrefixLength(
|
|
1105
|
+
system: readonly unknown[],
|
|
1106
|
+
billingSuppressed: boolean,
|
|
1107
|
+
identitySuppressed: boolean,
|
|
1108
|
+
): number {
|
|
1109
|
+
let index = 0;
|
|
1110
|
+
if (!billingSuppressed) {
|
|
1111
|
+
if (!isBillingBlockText(systemBlockText(system[index]))) fail();
|
|
1112
|
+
index += 1;
|
|
1113
|
+
}
|
|
1114
|
+
if (identitySuppressed) {
|
|
1115
|
+
// The identity text cannot appear ANYWHERE in a body built with the seam
|
|
1116
|
+
// active: `buildCanonicalSystem` drops a caller block equal to it
|
|
1117
|
+
// unconditionally, and merging joins with `\n`, so no merged run can equal
|
|
1118
|
+
// it either. Absence is therefore checkable, which is what refutes a claim
|
|
1119
|
+
// of suppression made over a body that still carries the block.
|
|
1120
|
+
//
|
|
1121
|
+
// No mirror check exists for billing: a caller block may legitimately begin
|
|
1122
|
+
// with the billing header text, so its presence proves nothing.
|
|
1123
|
+
if (system.some((block) => systemBlockText(block) === IDENTITY_TEXT)) {
|
|
1124
|
+
fail();
|
|
1125
|
+
}
|
|
1126
|
+
return index;
|
|
1127
|
+
}
|
|
1128
|
+
if (systemBlockText(system[index]) !== IDENTITY_TEXT) fail();
|
|
1129
|
+
return index + 1;
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
function parseBody(value: string): UnknownRecord {
|
|
1133
|
+
let parsed: unknown;
|
|
1134
|
+
try {
|
|
1135
|
+
parsed = JSON.parse(value);
|
|
1136
|
+
} catch {
|
|
1137
|
+
fail();
|
|
1138
|
+
}
|
|
1139
|
+
if (!isRecord(parsed)) fail();
|
|
1140
|
+
return parsed;
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
function parsedBodySessionId(body: UnknownRecord): string {
|
|
1144
|
+
const metadata = body["metadata"];
|
|
1145
|
+
if (!isRecord(metadata) || typeof metadata["user_id"] !== "string") fail();
|
|
1146
|
+
const identity = parseBody(metadata["user_id"]);
|
|
1147
|
+
const sessionId = identity["session_id"];
|
|
1148
|
+
if (typeof sessionId !== "string") fail();
|
|
1149
|
+
return sessionId;
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
function headerValue(headers: readonly HeaderPair[], name: string): string {
|
|
1153
|
+
const matches = headers.filter(([candidate]) => candidate === name);
|
|
1154
|
+
if (matches.length !== 1) fail();
|
|
1155
|
+
const match = matches[0];
|
|
1156
|
+
if (match === undefined) fail();
|
|
1157
|
+
return match[1];
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
function splitDynamicAndExtraHeaders(headers: readonly HeaderPair[]): {
|
|
1161
|
+
readonly stainlessHelper: string | undefined;
|
|
1162
|
+
readonly claudeRemoteContainerId: string | undefined;
|
|
1163
|
+
readonly claudeRemoteSessionId: string | undefined;
|
|
1164
|
+
readonly clientApp: string | undefined;
|
|
1165
|
+
readonly anthropicAdditionalProtection: string | undefined;
|
|
1166
|
+
readonly extraHeaders: readonly HeaderPair[];
|
|
1167
|
+
} {
|
|
1168
|
+
const timeoutIndex = headers.findIndex(
|
|
1169
|
+
([name]) => name === "x-stainless-timeout",
|
|
1170
|
+
);
|
|
1171
|
+
if (timeoutIndex < 0) fail();
|
|
1172
|
+
let cursor = timeoutIndex + 1;
|
|
1173
|
+
function consume(name: string): string | undefined {
|
|
1174
|
+
const pair = headers[cursor];
|
|
1175
|
+
if (pair?.[0] !== name) return undefined;
|
|
1176
|
+
cursor += 1;
|
|
1177
|
+
return pair[1];
|
|
1178
|
+
}
|
|
1179
|
+
return {
|
|
1180
|
+
stainlessHelper: consume("x-stainless-helper"),
|
|
1181
|
+
claudeRemoteContainerId: consume("x-claude-remote-container-id"),
|
|
1182
|
+
claudeRemoteSessionId: consume("x-claude-remote-session-id"),
|
|
1183
|
+
clientApp: consume("x-client-app"),
|
|
1184
|
+
anthropicAdditionalProtection: consume("x-anthropic-additional-protection"),
|
|
1185
|
+
extraHeaders: headers.slice(cursor),
|
|
1186
|
+
};
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
function evidenceRequest(
|
|
1190
|
+
input: ClaudeCodeRequestInput,
|
|
1191
|
+
callerModel: string,
|
|
1192
|
+
): NormalizedRequestInput {
|
|
1193
|
+
const request: {
|
|
1194
|
+
accessToken: string;
|
|
1195
|
+
model: string;
|
|
1196
|
+
maxTokens: number;
|
|
1197
|
+
messages: ClaudeCodeRequestInput["messages"];
|
|
1198
|
+
runtime: ClaudeCodeRequestInput["runtime"];
|
|
1199
|
+
system?: NonNullable<ClaudeCodeRequestInput["system"]>;
|
|
1200
|
+
tools?: NonNullable<ClaudeCodeRequestInput["tools"]>;
|
|
1201
|
+
cacheControl?: Exclude<ClaudeCodeRequestInput["cacheControl"], undefined>;
|
|
1202
|
+
capabilities?: NonNullable<ClaudeCodeRequestInput["capabilities"]>;
|
|
1203
|
+
betaOverrides?: NonNullable<ClaudeCodeRequestInput["betaOverrides"]>;
|
|
1204
|
+
preserveThinkingBlockCacheControl?: NonNullable<
|
|
1205
|
+
ClaudeCodeRequestInput["preserveThinkingBlockCacheControl"]
|
|
1206
|
+
>;
|
|
1207
|
+
thinking?: NonNullable<ClaudeCodeRequestInput["thinking"]>;
|
|
1208
|
+
effort?: NonNullable<ClaudeCodeRequestInput["effort"]>;
|
|
1209
|
+
metadata?: NonNullable<ClaudeCodeRequestInput["metadata"]>;
|
|
1210
|
+
experimentalBodyFields?: NonNullable<
|
|
1211
|
+
ClaudeCodeRequestInput["experimentalBodyFields"]
|
|
1212
|
+
>;
|
|
1213
|
+
contextManagement?: Exclude<
|
|
1214
|
+
ClaudeCodeRequestInput["contextManagement"],
|
|
1215
|
+
undefined
|
|
1216
|
+
>;
|
|
1217
|
+
outputConfig?: Exclude<ClaudeCodeRequestInput["outputConfig"], undefined>;
|
|
1218
|
+
speed?: Exclude<ClaudeCodeRequestInput["speed"], undefined>;
|
|
1219
|
+
serviceTier?: Exclude<ClaudeCodeRequestInput["serviceTier"], undefined>;
|
|
1220
|
+
outputFormat?: Exclude<ClaudeCodeRequestInput["outputFormat"], undefined>;
|
|
1221
|
+
toolChoice?: Exclude<ClaudeCodeRequestInput["toolChoice"], undefined>;
|
|
1222
|
+
topP?: Exclude<ClaudeCodeRequestInput["topP"], undefined>;
|
|
1223
|
+
topK?: Exclude<ClaudeCodeRequestInput["topK"], undefined>;
|
|
1224
|
+
stopSequences?: Exclude<ClaudeCodeRequestInput["stopSequences"], undefined>;
|
|
1225
|
+
stream?: Exclude<ClaudeCodeRequestInput["stream"], undefined>;
|
|
1226
|
+
temperature?: Exclude<ClaudeCodeRequestInput["temperature"], undefined>;
|
|
1227
|
+
} = {
|
|
1228
|
+
accessToken: input.accessToken,
|
|
1229
|
+
model: callerModel,
|
|
1230
|
+
maxTokens: input.maxTokens,
|
|
1231
|
+
messages: input.messages,
|
|
1232
|
+
runtime: input.runtime,
|
|
1233
|
+
};
|
|
1234
|
+
if (input.system !== undefined) request.system = input.system;
|
|
1235
|
+
if (input.tools !== undefined) request.tools = input.tools;
|
|
1236
|
+
if (Object.hasOwn(input, "cacheControl"))
|
|
1237
|
+
request.cacheControl = present(input.cacheControl);
|
|
1238
|
+
if (input.capabilities !== undefined)
|
|
1239
|
+
request.capabilities = input.capabilities;
|
|
1240
|
+
if (input.betaOverrides !== undefined)
|
|
1241
|
+
request.betaOverrides = input.betaOverrides;
|
|
1242
|
+
// The body builder owns the thinking-block allowlist, so the seam flag has to
|
|
1243
|
+
// reach it. It is forwarded only when the caller stated it, keeping the
|
|
1244
|
+
// normalized request shape identical for every request that ignores the seam.
|
|
1245
|
+
if (input.preserveThinkingBlockCacheControl !== undefined)
|
|
1246
|
+
request.preserveThinkingBlockCacheControl =
|
|
1247
|
+
input.preserveThinkingBlockCacheControl;
|
|
1248
|
+
if (input.thinking !== undefined) request.thinking = input.thinking;
|
|
1249
|
+
if (input.effort !== undefined) request.effort = input.effort;
|
|
1250
|
+
if (input.metadata !== undefined) request.metadata = input.metadata;
|
|
1251
|
+
if (input.experimentalBodyFields !== undefined)
|
|
1252
|
+
request.experimentalBodyFields = input.experimentalBodyFields;
|
|
1253
|
+
if (Object.hasOwn(input, "contextManagement"))
|
|
1254
|
+
request.contextManagement = present(input.contextManagement);
|
|
1255
|
+
if (Object.hasOwn(input, "outputConfig"))
|
|
1256
|
+
request.outputConfig = present(input.outputConfig);
|
|
1257
|
+
if (Object.hasOwn(input, "speed")) request.speed = present(input.speed);
|
|
1258
|
+
if (Object.hasOwn(input, "serviceTier"))
|
|
1259
|
+
request.serviceTier = present(input.serviceTier);
|
|
1260
|
+
if (Object.hasOwn(input, "outputFormat"))
|
|
1261
|
+
request.outputFormat = present(input.outputFormat);
|
|
1262
|
+
if (Object.hasOwn(input, "toolChoice"))
|
|
1263
|
+
request.toolChoice = present(input.toolChoice);
|
|
1264
|
+
if (Object.hasOwn(input, "topP")) request.topP = present(input.topP);
|
|
1265
|
+
if (Object.hasOwn(input, "topK")) request.topK = present(input.topK);
|
|
1266
|
+
if (Object.hasOwn(input, "stopSequences"))
|
|
1267
|
+
request.stopSequences = present(input.stopSequences);
|
|
1268
|
+
if (Object.hasOwn(input, "stream")) request.stream = present(input.stream);
|
|
1269
|
+
if (Object.hasOwn(input, "temperature"))
|
|
1270
|
+
request.temperature = present(input.temperature);
|
|
1271
|
+
return request;
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
function countTokensEvidenceRequest(
|
|
1275
|
+
input: ClaudeCodeCountTokensInput,
|
|
1276
|
+
capabilities: ClaudeCodeCapabilities,
|
|
1277
|
+
): NormalizedRequestInput {
|
|
1278
|
+
return {
|
|
1279
|
+
accessToken: input.accessToken,
|
|
1280
|
+
model: input.model,
|
|
1281
|
+
maxTokens: 1,
|
|
1282
|
+
messages: input.messages,
|
|
1283
|
+
...(input.tools === undefined ? {} : { tools: input.tools }),
|
|
1284
|
+
runtime: input.runtime,
|
|
1285
|
+
capabilities,
|
|
1286
|
+
};
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
/** Builds a canonical Claude Code count-tokens request. */
|
|
1290
|
+
export async function buildClaudeCodeCountTokensRequest(
|
|
1291
|
+
input: ClaudeCodeCountTokensInput,
|
|
1292
|
+
profile: ClaudeCodeProtocolProfile = CLAUDE_CODE_2_1_195_PROFILE,
|
|
1293
|
+
): Promise<BuiltClaudeCodeCountTokensRequest> {
|
|
1294
|
+
try {
|
|
1295
|
+
const pinnedProfile = validateProfile(profile);
|
|
1296
|
+
const validated = validateCountTokensInput(input);
|
|
1297
|
+
const effectiveProfile = createEffectiveProfile(
|
|
1298
|
+
pinnedProfile,
|
|
1299
|
+
validated.profileOverride,
|
|
1300
|
+
);
|
|
1301
|
+
const identity = validateRuntimeIdentity(validated.source.runtime);
|
|
1302
|
+
const resolvedModel = resolveModel(
|
|
1303
|
+
validated.source.model,
|
|
1304
|
+
effectiveProfile,
|
|
1305
|
+
);
|
|
1306
|
+
const countTokensBetas = filterCountTokensBetas(
|
|
1307
|
+
composeBetas(
|
|
1308
|
+
{
|
|
1309
|
+
rawModel: validated.source.model,
|
|
1310
|
+
normalizedId: resolvedModel.id,
|
|
1311
|
+
capabilities: resolvedModel.capabilities,
|
|
1312
|
+
thinkingDisplayActive: false,
|
|
1313
|
+
},
|
|
1314
|
+
effectiveProfile,
|
|
1315
|
+
),
|
|
1316
|
+
);
|
|
1317
|
+
const betas = Object.freeze([...countTokensBetas, TOKEN_COUNTING_BETA]);
|
|
1318
|
+
const headers = applyEffectiveProfileHeaders(
|
|
1319
|
+
buildOrderedHeaders({
|
|
1320
|
+
accessToken: validated.source.accessToken,
|
|
1321
|
+
runtime: identity,
|
|
1322
|
+
clientRequestId: validated.clientRequestId,
|
|
1323
|
+
betaFeatures: betas,
|
|
1324
|
+
app: validated.source.app ?? effectiveProfile.entrypoint,
|
|
1325
|
+
stainlessRetryCount: validated.source.stainlessRetryCount ?? 0,
|
|
1326
|
+
stainlessHelper: validated.source.stainlessHelper,
|
|
1327
|
+
claudeRemoteContainerId: validated.source.claudeRemoteContainerId,
|
|
1328
|
+
claudeRemoteSessionId: validated.source.claudeRemoteSessionId,
|
|
1329
|
+
clientApp: validated.source.clientApp,
|
|
1330
|
+
anthropicAdditionalProtection:
|
|
1331
|
+
validated.source.anthropicAdditionalProtection,
|
|
1332
|
+
extraHeaders: validated.source.extraHeaders ?? [],
|
|
1333
|
+
profile: pinnedProfile,
|
|
1334
|
+
}),
|
|
1335
|
+
effectiveProfile,
|
|
1336
|
+
);
|
|
1337
|
+
const canonical = canonicalCountTokensLists(
|
|
1338
|
+
validated.source.messages,
|
|
1339
|
+
validated.source.tools,
|
|
1340
|
+
);
|
|
1341
|
+
const body = JSON.stringify(
|
|
1342
|
+
buildCountTokensBody(
|
|
1343
|
+
resolvedModel.wireId,
|
|
1344
|
+
canonical.messages,
|
|
1345
|
+
canonical.tools,
|
|
1346
|
+
),
|
|
1347
|
+
);
|
|
1348
|
+
const evidenceRequestInput = countTokensEvidenceRequest(
|
|
1349
|
+
validated.source,
|
|
1350
|
+
resolvedModel.capabilities,
|
|
1351
|
+
);
|
|
1352
|
+
const evidence = await buildRedactedEvidence(
|
|
1353
|
+
{
|
|
1354
|
+
profile: pinnedProfile,
|
|
1355
|
+
effectiveProfile,
|
|
1356
|
+
request: evidenceRequestInput,
|
|
1357
|
+
modelFamily: resolvedModel.family,
|
|
1358
|
+
logicalHeaders: headers,
|
|
1359
|
+
betaFeatures: betas,
|
|
1360
|
+
body,
|
|
1361
|
+
},
|
|
1362
|
+
validated.crypto,
|
|
1363
|
+
);
|
|
1364
|
+
return deepFreeze({
|
|
1365
|
+
url: effectiveProfile.countTokensEndpoint,
|
|
1366
|
+
method: METHOD,
|
|
1367
|
+
headers,
|
|
1368
|
+
body,
|
|
1369
|
+
evidence,
|
|
1370
|
+
});
|
|
1371
|
+
} catch (error: unknown) {
|
|
1372
|
+
return sanitizeError(error);
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
/**
|
|
1377
|
+
* Builds one canonical request for the pinned Claude Code wire profile.
|
|
1378
|
+
*
|
|
1379
|
+
* @param profile - The only accepted value is the exported
|
|
1380
|
+
* `CLAUDE_CODE_2_1_195_PROFILE` singleton. Any other object, even a
|
|
1381
|
+
* structurally identical clone, is rejected with `ClaudeCodeWireError` code
|
|
1382
|
+
* `INVALID_INPUT`. This deliberate fail-closed behaviour prevents callers from
|
|
1383
|
+
* substituting an unpinned protocol profile.
|
|
1384
|
+
*/
|
|
1385
|
+
export async function buildClaudeCodeRequest(
|
|
1386
|
+
input: ClaudeCodeRequestInput,
|
|
1387
|
+
profile: ClaudeCodeProtocolProfile = CLAUDE_CODE_2_1_195_PROFILE,
|
|
1388
|
+
): Promise<BuiltClaudeCodeRequest> {
|
|
1389
|
+
try {
|
|
1390
|
+
const pinnedProfile = validateProfile(profile);
|
|
1391
|
+
const validated = validateInput(input);
|
|
1392
|
+
const effectiveProfile = createEffectiveProfile(
|
|
1393
|
+
pinnedProfile,
|
|
1394
|
+
validated.profileOverride,
|
|
1395
|
+
);
|
|
1396
|
+
const identity = validateRuntimeIdentity(validated.source.runtime);
|
|
1397
|
+
const resolvedModel = resolveModel(
|
|
1398
|
+
validated.source.model,
|
|
1399
|
+
effectiveProfile,
|
|
1400
|
+
);
|
|
1401
|
+
const capabilities = requestedCapabilities(
|
|
1402
|
+
validated.source,
|
|
1403
|
+
resolvedModel.capabilities,
|
|
1404
|
+
);
|
|
1405
|
+
const effectiveModel = Object.freeze({
|
|
1406
|
+
...resolvedModel,
|
|
1407
|
+
capabilities,
|
|
1408
|
+
});
|
|
1409
|
+
const billing = await createBillingBlock(
|
|
1410
|
+
fingerprintText(validated.source),
|
|
1411
|
+
effectiveProfile.cliVersion,
|
|
1412
|
+
validated.crypto,
|
|
1413
|
+
);
|
|
1414
|
+
const metadata = buildCorrelatedMetadata(
|
|
1415
|
+
identity,
|
|
1416
|
+
validated.source.metadata,
|
|
1417
|
+
validated.source.metadataOverrides,
|
|
1418
|
+
);
|
|
1419
|
+
const system = buildCanonicalSystem(
|
|
1420
|
+
validated.source.system,
|
|
1421
|
+
billing,
|
|
1422
|
+
identity,
|
|
1423
|
+
validated.suppressBillingBlock,
|
|
1424
|
+
validated.suppressIdentityBlock,
|
|
1425
|
+
);
|
|
1426
|
+
const canonicalBody = buildCanonicalBody(
|
|
1427
|
+
evidenceRequest(validated.source, validated.source.model),
|
|
1428
|
+
effectiveModel,
|
|
1429
|
+
system,
|
|
1430
|
+
metadata,
|
|
1431
|
+
effectiveProfile,
|
|
1432
|
+
);
|
|
1433
|
+
const composedBetas = composeBetasWithAudit(
|
|
1434
|
+
{
|
|
1435
|
+
rawModel: validated.source.model,
|
|
1436
|
+
normalizedId: resolvedModel.id,
|
|
1437
|
+
capabilities,
|
|
1438
|
+
thinkingDisplayActive: isThinkingDisplayActive(
|
|
1439
|
+
validated.source.thinking,
|
|
1440
|
+
capabilities,
|
|
1441
|
+
effectiveProfile.betaPolicy,
|
|
1442
|
+
),
|
|
1443
|
+
...(validated.source.cacheControl?.ttl === undefined
|
|
1444
|
+
? {}
|
|
1445
|
+
: { cacheTtl: validated.source.cacheControl.ttl }),
|
|
1446
|
+
...(validated.source.speed === undefined
|
|
1447
|
+
? {}
|
|
1448
|
+
: { speed: validated.source.speed }),
|
|
1449
|
+
...(validated.source.additionalBetas === undefined
|
|
1450
|
+
? {}
|
|
1451
|
+
: { additionalBetas: validated.source.additionalBetas }),
|
|
1452
|
+
...(validated.source.suppressBetas === undefined
|
|
1453
|
+
? {}
|
|
1454
|
+
: { suppressBetas: validated.source.suppressBetas }),
|
|
1455
|
+
...(validated.betaOverrides?.use1MContext === undefined
|
|
1456
|
+
? {}
|
|
1457
|
+
: { use1MContextOverride: validated.betaOverrides.use1MContext }),
|
|
1458
|
+
},
|
|
1459
|
+
effectiveProfile,
|
|
1460
|
+
);
|
|
1461
|
+
const betas = composedBetas.betas;
|
|
1462
|
+
const headerPlan = buildOrderedHeaderPlan({
|
|
1463
|
+
accessToken: validated.source.accessToken,
|
|
1464
|
+
runtime: identity,
|
|
1465
|
+
clientRequestId: validated.clientRequestId,
|
|
1466
|
+
betaFeatures: betas,
|
|
1467
|
+
app: validated.source.app ?? effectiveProfile.entrypoint,
|
|
1468
|
+
stainlessRetryCount: validated.source.stainlessRetryCount ?? 0,
|
|
1469
|
+
stainlessHelper: validated.source.stainlessHelper,
|
|
1470
|
+
claudeRemoteContainerId: validated.source.claudeRemoteContainerId,
|
|
1471
|
+
claudeRemoteSessionId: validated.source.claudeRemoteSessionId,
|
|
1472
|
+
clientApp: validated.source.clientApp,
|
|
1473
|
+
anthropicAdditionalProtection:
|
|
1474
|
+
validated.source.anthropicAdditionalProtection,
|
|
1475
|
+
extraHeaders: validated.source.extraHeaders ?? [],
|
|
1476
|
+
extraHeaderPolicy: validated.extraHeaderPolicy ?? "strict",
|
|
1477
|
+
profile: pinnedProfile,
|
|
1478
|
+
});
|
|
1479
|
+
const headers = applyEffectiveProfileHeaders(
|
|
1480
|
+
headerPlan.headers,
|
|
1481
|
+
effectiveProfile,
|
|
1482
|
+
);
|
|
1483
|
+
const body = JSON.stringify(canonicalBody);
|
|
1484
|
+
const evidence = await buildRedactedEvidence(
|
|
1485
|
+
{
|
|
1486
|
+
profile: pinnedProfile,
|
|
1487
|
+
effectiveProfile,
|
|
1488
|
+
request: evidenceRequest(validated.source, validated.source.model),
|
|
1489
|
+
modelFamily: resolvedModel.family,
|
|
1490
|
+
logicalHeaders: headers,
|
|
1491
|
+
betaFeatures: betas,
|
|
1492
|
+
body,
|
|
1493
|
+
// The canonical system merges adjacent caller blocks and drops any
|
|
1494
|
+
// block equal to the identity text, so only the emitted count keeps
|
|
1495
|
+
// `systemBlockCount === body.system.length - <canonical>` true.
|
|
1496
|
+
// Symmetric arithmetic over the two suppression seams: each canonical
|
|
1497
|
+
// block that survived costs one slot. A single constant cannot express
|
|
1498
|
+
// the empty prefix both seams together produce.
|
|
1499
|
+
emittedSystemBlockCount:
|
|
1500
|
+
system.length -
|
|
1501
|
+
(validated.suppressBillingBlock ? 0 : 1) -
|
|
1502
|
+
(validated.suppressIdentityBlock ? 0 : 1),
|
|
1503
|
+
// Emitted only for the opted-in policy, so evidence for every other
|
|
1504
|
+
// request keeps the shape it had before the seam existed.
|
|
1505
|
+
...(validated.extraHeaderPolicy === "dropConflicting"
|
|
1506
|
+
? { droppedExtraHeaderNames: headerPlan.droppedExtraHeaderNames }
|
|
1507
|
+
: {}),
|
|
1508
|
+
// Emitted only when suppression actually removed something, so
|
|
1509
|
+
// evidence for every request that ignores the seam is unchanged.
|
|
1510
|
+
...(composedBetas.suppressedBetaNames.length === 0
|
|
1511
|
+
? {}
|
|
1512
|
+
: { suppressedBetaNames: composedBetas.suppressedBetaNames }),
|
|
1513
|
+
// Emitted only when the billing block was actually removed, so the
|
|
1514
|
+
// audit records the attribution decision without inventing a key for
|
|
1515
|
+
// every request that leaves the canonical prefix intact.
|
|
1516
|
+
...(validated.suppressBillingBlock
|
|
1517
|
+
? { billingBlockSuppressed: true }
|
|
1518
|
+
: {}),
|
|
1519
|
+
// Emitted only when the identity block was actually removed, on the
|
|
1520
|
+
// same terms as `billingBlockSuppressed`: the parser reads both flags
|
|
1521
|
+
// to know the length of the canonical prefix it must verify.
|
|
1522
|
+
...(validated.suppressIdentityBlock
|
|
1523
|
+
? { identityBlockSuppressed: true }
|
|
1524
|
+
: {}),
|
|
1525
|
+
// Emitted only when the seam was active AND a reasoning block actually
|
|
1526
|
+
// carried the marker. Opting in without using it records nothing, so
|
|
1527
|
+
// the audit states what happened on the wire rather than what the
|
|
1528
|
+
// caller was permitted to do.
|
|
1529
|
+
...(validated.preserveThinkingBlockCacheControl &&
|
|
1530
|
+
hasThinkingBlockCacheControl(canonicalBody["messages"])
|
|
1531
|
+
? { thinkingBlockCacheControlPreserved: true }
|
|
1532
|
+
: {}),
|
|
1533
|
+
},
|
|
1534
|
+
validated.crypto,
|
|
1535
|
+
);
|
|
1536
|
+
return deepFreeze({
|
|
1537
|
+
url: effectiveProfile.endpoint,
|
|
1538
|
+
method: METHOD,
|
|
1539
|
+
headers,
|
|
1540
|
+
body,
|
|
1541
|
+
evidence,
|
|
1542
|
+
});
|
|
1543
|
+
} catch (error: unknown) {
|
|
1544
|
+
return sanitizeError(error);
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
/**
|
|
1549
|
+
* Validates and clones a previously built request into a deeply frozen value.
|
|
1550
|
+
*
|
|
1551
|
+
* @param profile - The only accepted value is the exported
|
|
1552
|
+
* `CLAUDE_CODE_2_1_195_PROFILE` singleton. Any other object, even a
|
|
1553
|
+
* structurally identical clone, is rejected with `ClaudeCodeWireError` code
|
|
1554
|
+
* `INVALID_INPUT`. This deliberate fail-closed behaviour prevents callers from
|
|
1555
|
+
* substituting an unpinned protocol profile.
|
|
1556
|
+
*/
|
|
1557
|
+
export function parseBuiltClaudeCodeRequest(
|
|
1558
|
+
value: unknown,
|
|
1559
|
+
profile: ClaudeCodeProtocolProfile = CLAUDE_CODE_2_1_195_PROFILE,
|
|
1560
|
+
): BuiltClaudeCodeRequest {
|
|
1561
|
+
try {
|
|
1562
|
+
const pinnedProfile = validateProfile(profile);
|
|
1563
|
+
inspectGraph(value);
|
|
1564
|
+
if (!isRecord(value)) fail();
|
|
1565
|
+
assertExactKeys(value, BUILT_KEYS);
|
|
1566
|
+
if (
|
|
1567
|
+
ownValue(value, "url") !== pinnedProfile.endpoint ||
|
|
1568
|
+
ownValue(value, "method") !== METHOD
|
|
1569
|
+
) {
|
|
1570
|
+
fail();
|
|
1571
|
+
}
|
|
1572
|
+
const body = ownValue(value, "body");
|
|
1573
|
+
if (typeof body !== "string") fail();
|
|
1574
|
+
const parsedBody = parseBody(body);
|
|
1575
|
+
const headers = parseHeaders(ownValue(value, "headers"));
|
|
1576
|
+
const evidence = parseEvidence(ownValue(value, "evidence"));
|
|
1577
|
+
// Reading evidence is not trusting evidence. A claim that the seam
|
|
1578
|
+
// preserved a marker is confirmed against the body, and it is confirmed
|
|
1579
|
+
// HERE — before the byte-length and digest checks — so that a forgery which
|
|
1580
|
+
// is byte-length preserving and evidence-self-consistent is refused by the
|
|
1581
|
+
// structural check rather than incidentally by arithmetic.
|
|
1582
|
+
if (
|
|
1583
|
+
evidence.thinkingBlockCacheControlPreserved === true &&
|
|
1584
|
+
!hasThinkingBlockCacheControl(parsedBody["messages"])
|
|
1585
|
+
) {
|
|
1586
|
+
fail();
|
|
1587
|
+
}
|
|
1588
|
+
const sessionId = headerValue(headers, "x-claude-code-session-id");
|
|
1589
|
+
const additionalHeaders = splitDynamicAndExtraHeaders(headers);
|
|
1590
|
+
const expectedHeaders = buildOrderedHeaders({
|
|
1591
|
+
accessToken: headerValue(headers, "authorization").replace(
|
|
1592
|
+
/^Bearer /u,
|
|
1593
|
+
"",
|
|
1594
|
+
),
|
|
1595
|
+
runtime: {
|
|
1596
|
+
sessionId,
|
|
1597
|
+
runtime: headerValue(headers, "x-stainless-runtime"),
|
|
1598
|
+
runtimeVersion: headerValue(headers, "x-stainless-runtime-version"),
|
|
1599
|
+
os: headerValue(headers, "x-stainless-os"),
|
|
1600
|
+
arch: headerValue(headers, "x-stainless-arch"),
|
|
1601
|
+
},
|
|
1602
|
+
clientRequestId: headerValue(headers, "x-client-request-id"),
|
|
1603
|
+
betaFeatures: evidence.betaFeatures,
|
|
1604
|
+
app: headerValue(headers, "x-app"),
|
|
1605
|
+
stainlessRetryCount: Number(
|
|
1606
|
+
headerValue(headers, "x-stainless-retry-count"),
|
|
1607
|
+
),
|
|
1608
|
+
stainlessHelper: additionalHeaders.stainlessHelper,
|
|
1609
|
+
claudeRemoteContainerId: additionalHeaders.claudeRemoteContainerId,
|
|
1610
|
+
claudeRemoteSessionId: additionalHeaders.claudeRemoteSessionId,
|
|
1611
|
+
clientApp: additionalHeaders.clientApp,
|
|
1612
|
+
anthropicAdditionalProtection:
|
|
1613
|
+
additionalHeaders.anthropicAdditionalProtection,
|
|
1614
|
+
extraHeaders: additionalHeaders.extraHeaders,
|
|
1615
|
+
profile: pinnedProfile,
|
|
1616
|
+
});
|
|
1617
|
+
if (JSON.stringify(headers) !== JSON.stringify(expectedHeaders)) fail();
|
|
1618
|
+
if (
|
|
1619
|
+
evidence.logicalHeaderNames.length !== headers.length ||
|
|
1620
|
+
evidence.logicalHeaderNames.some(
|
|
1621
|
+
(name, index) => name !== headers[index]?.[0],
|
|
1622
|
+
) ||
|
|
1623
|
+
parsedBodySessionId(parsedBody) !== sessionId ||
|
|
1624
|
+
headerValue(headers, "anthropic-beta") !==
|
|
1625
|
+
evidence.betaFeatures.join(",") ||
|
|
1626
|
+
evidence.bodyByteLength !== new TextEncoder().encode(body).byteLength ||
|
|
1627
|
+
evidence.messageCount !==
|
|
1628
|
+
(Array.isArray(parsedBody["messages"])
|
|
1629
|
+
? parsedBody["messages"].length
|
|
1630
|
+
: -1) ||
|
|
1631
|
+
evidence.systemBlockCount !==
|
|
1632
|
+
(Array.isArray(parsedBody["system"])
|
|
1633
|
+
? parsedBody["system"].length -
|
|
1634
|
+
canonicalSystemPrefixLength(
|
|
1635
|
+
parsedBody["system"],
|
|
1636
|
+
evidence.billingBlockSuppressed === true,
|
|
1637
|
+
evidence.identityBlockSuppressed === true,
|
|
1638
|
+
)
|
|
1639
|
+
: -1)
|
|
1640
|
+
) {
|
|
1641
|
+
fail();
|
|
1642
|
+
}
|
|
1643
|
+
const result: BuiltClaudeCodeRequest = {
|
|
1644
|
+
url: pinnedProfile.endpoint,
|
|
1645
|
+
method: METHOD,
|
|
1646
|
+
headers,
|
|
1647
|
+
body,
|
|
1648
|
+
evidence,
|
|
1649
|
+
};
|
|
1650
|
+
if (sha256Hex(body) !== evidence.bodySha256) fail();
|
|
1651
|
+
return deepFreeze(result);
|
|
1652
|
+
} catch (error: unknown) {
|
|
1653
|
+
return sanitizeError(error);
|
|
1654
|
+
}
|
|
1655
|
+
}
|