@tormentalabs/claude-code-wire-compat 0.5.0 → 0.7.0-rc.1
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 +226 -0
- package/README.md +19 -7
- package/dist/betas.d.ts +65 -7
- package/dist/betas.d.ts.map +1 -1
- package/dist/betas.js +143 -2
- package/dist/betas.js.map +1 -1
- package/dist/build-request.d.ts +12 -6
- package/dist/build-request.d.ts.map +1 -1
- package/dist/build-request.js +93 -50
- package/dist/build-request.js.map +1 -1
- package/dist/contracts.d.ts +7 -2
- package/dist/contracts.d.ts.map +1 -1
- package/dist/contracts.js.map +1 -1
- package/dist/fingerprint.d.ts.map +1 -1
- package/dist/fingerprint.js +5 -0
- package/dist/fingerprint.js.map +1 -1
- package/dist/headers.d.ts.map +1 -1
- package/dist/headers.js +5 -2
- package/dist/headers.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/limits.d.ts +3 -0
- package/dist/limits.d.ts.map +1 -0
- package/dist/limits.js +4 -0
- package/dist/limits.js.map +1 -0
- package/dist/model-capabilities.d.ts +3 -3
- package/dist/model-capabilities.d.ts.map +1 -1
- package/dist/model-capabilities.js +55 -19
- package/dist/model-capabilities.js.map +1 -1
- package/dist/model-identity.d.ts +9 -1
- package/dist/model-identity.d.ts.map +1 -1
- package/dist/model-identity.js +19 -1
- package/dist/model-identity.js.map +1 -1
- package/dist/model-queries.d.ts +17 -5
- package/dist/model-queries.d.ts.map +1 -1
- package/dist/model-queries.js +33 -6
- package/dist/model-queries.js.map +1 -1
- package/dist/models.js +1 -1
- package/dist/models.js.map +1 -1
- package/dist/profiles/beta-registry-2.1.280.d.ts +211 -0
- package/dist/profiles/beta-registry-2.1.280.d.ts.map +1 -0
- package/dist/profiles/beta-registry-2.1.280.js +292 -0
- package/dist/profiles/beta-registry-2.1.280.js.map +1 -0
- package/dist/profiles/claude-code-2.1.280.d.ts +3 -0
- package/dist/profiles/claude-code-2.1.280.d.ts.map +1 -0
- package/dist/profiles/claude-code-2.1.280.js +359 -0
- package/dist/profiles/claude-code-2.1.280.js.map +1 -0
- package/dist/redaction.d.ts.map +1 -1
- package/dist/redaction.js +48 -2
- package/dist/redaction.js.map +1 -1
- package/dist/request-body.d.ts +4 -2
- package/dist/request-body.d.ts.map +1 -1
- package/dist/request-body.js +77 -48
- package/dist/request-body.js.map +1 -1
- package/dist/system-prompt.d.ts.map +1 -1
- package/dist/system-prompt.js +23 -22
- package/dist/system-prompt.js.map +1 -1
- package/dist/thinking.d.ts +53 -2
- package/dist/thinking.d.ts.map +1 -1
- package/dist/thinking.js +124 -26
- package/dist/thinking.js.map +1 -1
- package/dist/unicode.d.ts +41 -0
- package/dist/unicode.d.ts.map +1 -1
- package/dist/unicode.js +37 -0
- package/dist/unicode.js.map +1 -1
- package/dist/violation.d.ts +23 -0
- package/dist/violation.d.ts.map +1 -0
- package/dist/violation.js +210 -0
- package/dist/violation.js.map +1 -0
- package/package.json +6 -2
- package/src/betas.ts +208 -9
- package/src/build-request.ts +120 -56
- package/src/contracts.ts +7 -2
- package/src/fingerprint.ts +5 -0
- package/src/headers.ts +4 -2
- package/src/index.ts +2 -0
- package/src/limits.ts +4 -0
- package/src/model-capabilities.ts +55 -19
- package/src/model-identity.ts +14 -1
- package/src/model-queries.ts +38 -6
- package/src/models.ts +1 -1
- package/src/profiles/beta-registry-2.1.280.ts +309 -0
- package/src/profiles/claude-code-2.1.280.ts +364 -0
- package/src/redaction.ts +65 -2
- package/src/request-body.ts +107 -49
- package/src/system-prompt.ts +32 -24
- package/src/thinking.ts +148 -26
- package/src/unicode.ts +71 -0
- package/src/violation.ts +226 -0
package/src/build-request.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
2
2
|
|
|
3
|
+
import { MAX_INPUT_SIZE } from "./limits.js";
|
|
4
|
+
|
|
3
5
|
import { composeBetas, composeBetasWithAudit } from "./betas.js";
|
|
4
6
|
import type {
|
|
5
7
|
BuiltClaudeCodeCountTokensRequest,
|
|
@@ -30,6 +32,7 @@ import {
|
|
|
30
32
|
import { resolveModel } from "./models.js";
|
|
31
33
|
import { CLAUDE_CODE_2_1_195_PROFILE } from "./profiles/claude-code-2.1.195.js";
|
|
32
34
|
import { CLAUDE_CODE_2_1_233_PROFILE } from "./profiles/claude-code-2.1.233.js";
|
|
35
|
+
import { CLAUDE_CODE_2_1_280_PROFILE } from "./profiles/claude-code-2.1.280.js";
|
|
33
36
|
import type { NormalizedRequestInput } from "./redaction.js";
|
|
34
37
|
import { buildRedactedEvidence, toSafeErrorDetails } from "./redaction.js";
|
|
35
38
|
import {
|
|
@@ -38,12 +41,16 @@ import {
|
|
|
38
41
|
} from "./request-body.js";
|
|
39
42
|
import { sha256Hex } from "./sha256.js";
|
|
40
43
|
import { buildCanonicalSystem, IDENTITY_TEXT } from "./system-prompt.js";
|
|
41
|
-
import { isThinkingDisplayActive } from "./thinking.js";
|
|
42
|
-
import {
|
|
44
|
+
import { isThinkingActive, isThinkingDisplayActive } from "./thinking.js";
|
|
45
|
+
import {
|
|
46
|
+
inspectText,
|
|
47
|
+
TEXT_POLICY_IDENTIFIER,
|
|
48
|
+
TEXT_POLICY_PROSE,
|
|
49
|
+
} from "./unicode.js";
|
|
50
|
+
import { violationDetails, type ViolationPathSegment } from "./violation.js";
|
|
43
51
|
|
|
44
52
|
const METHOD = "POST";
|
|
45
53
|
const MAX_INPUT_DEPTH = 100;
|
|
46
|
-
const MAX_INPUT_SIZE = 1_000_000;
|
|
47
54
|
const FORBIDDEN_KEYS = new Set(["__proto__", "prototype", "constructor"]);
|
|
48
55
|
const INPUT_KEYS = new Set([
|
|
49
56
|
"accessToken",
|
|
@@ -152,6 +159,8 @@ const CAPABILITY_KEYS = [
|
|
|
152
159
|
"contextManagement",
|
|
153
160
|
"temperature",
|
|
154
161
|
"rejectsDisabledThinking",
|
|
162
|
+
"midConvToolChange",
|
|
163
|
+
"perTurnEffort",
|
|
155
164
|
] as const;
|
|
156
165
|
const CAPABILITY_KEY_SET = new Set(CAPABILITY_KEYS);
|
|
157
166
|
/** Adds the optional package-extension override keys carried by evidence. */
|
|
@@ -197,8 +206,9 @@ type UnknownRecord = Readonly<Record<string, unknown>>;
|
|
|
197
206
|
|
|
198
207
|
function fail(
|
|
199
208
|
code: ConstructorParameters<typeof ClaudeCodeWireError>[0] = "INVALID_INPUT",
|
|
209
|
+
safeDetails: Readonly<Record<string, string | number | boolean>> = {},
|
|
200
210
|
): never {
|
|
201
|
-
throw new ClaudeCodeWireError(code);
|
|
211
|
+
throw new ClaudeCodeWireError(code, safeDetails);
|
|
202
212
|
}
|
|
203
213
|
|
|
204
214
|
function isRecord(value: unknown): value is UnknownRecord {
|
|
@@ -228,39 +238,51 @@ function assertExactKeys(
|
|
|
228
238
|
/**
|
|
229
239
|
* Screens one string from the caller's input graph.
|
|
230
240
|
*
|
|
231
|
-
*
|
|
232
|
-
*
|
|
233
|
-
*
|
|
234
|
-
* `
|
|
235
|
-
*
|
|
241
|
+
* BODY PROSE POLICY (decision P1.T1, 2026-09-25): every well-formed UTF-16
|
|
242
|
+
* string is accepted. Control characters are valid Unicode scalars;
|
|
243
|
+
* `JSON.stringify` escapes the C0 range and emits DEL/C1 raw, and
|
|
244
|
+
* `TextEncoder` encodes every scalar deterministically, so no control
|
|
245
|
+
* character can desync the body from its hash. Real tool output legitimately
|
|
246
|
+
* carries ESC (ANSI colour), NUL, FF and DEL; synthetic probes reproduced
|
|
247
|
+
* rejection before fetch, not the original incident's exact input. The old rule was a
|
|
248
|
+
* library-local defensive heuristic with no upstream provenance; whether the
|
|
249
|
+
* remote API rejects any scalar is a remote concern, surfaced as a remote
|
|
250
|
+
* error, not a local pre-flight abort.
|
|
236
251
|
*
|
|
237
|
-
*
|
|
238
|
-
* and it still lives where it belongs and still applies in full:
|
|
239
|
-
* `assertHeaderText` in `src/headers.ts` rejects every control character,
|
|
240
|
-
* including these three, because a bare LF in a header is request smuggling.
|
|
241
|
-
* `src/metadata.ts` is likewise unchanged: `user_id` and metadata keys are
|
|
242
|
-
* identifiers that travel as JSON inside a header, not prose.
|
|
252
|
+
* What did NOT move:
|
|
243
253
|
*
|
|
244
|
-
*
|
|
245
|
-
*
|
|
246
|
-
*
|
|
247
|
-
*
|
|
248
|
-
*
|
|
249
|
-
*
|
|
250
|
-
*
|
|
254
|
+
* - HEADERS: `assertHeaderText` in `src/headers.ts` still rejects every
|
|
255
|
+
* control character, TAB/LF/CR included, because a bare LF in a header is
|
|
256
|
+
* request smuggling. `src/metadata.ts` is likewise unchanged: `user_id` and
|
|
257
|
+
* metadata keys are identifiers that travel as JSON inside a header, not
|
|
258
|
+
* prose, and keep their own strict rule.
|
|
259
|
+
* - LONE SURROGATES stay rejected in every context, deliberately.
|
|
260
|
+
* Raw `TextEncoder` replaces them, whereas JSON serialization escapes them.
|
|
261
|
+
* Rejecting them keeps the text contract independent of serialization order.
|
|
251
262
|
*/
|
|
252
|
-
function inspectString(
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
263
|
+
function inspectString(
|
|
264
|
+
value: string,
|
|
265
|
+
path: readonly ViolationPathSegment[],
|
|
266
|
+
inKey: boolean,
|
|
267
|
+
): number {
|
|
268
|
+
const root = path[0];
|
|
269
|
+
const prose =
|
|
270
|
+
root === "messages" ||
|
|
271
|
+
root === "system" ||
|
|
272
|
+
root === "tools" ||
|
|
273
|
+
root === "stopSequences" ||
|
|
274
|
+
root === "stop_sequences" ||
|
|
275
|
+
root === "body";
|
|
276
|
+
const violation = inspectText(
|
|
277
|
+
value,
|
|
278
|
+
prose ? TEXT_POLICY_PROSE : TEXT_POLICY_IDENTIFIER,
|
|
279
|
+
);
|
|
280
|
+
if (violation !== null) {
|
|
281
|
+
if (violation.reason === "control-char") fail("INVALID_UNICODE");
|
|
282
|
+
fail(
|
|
283
|
+
"INVALID_UNICODE",
|
|
284
|
+
violationDetails(violation, path, value.length, inKey),
|
|
285
|
+
);
|
|
264
286
|
}
|
|
265
287
|
return new TextEncoder().encode(value).byteLength;
|
|
266
288
|
}
|
|
@@ -268,11 +290,14 @@ function inspectString(value: string): number {
|
|
|
268
290
|
function inspectGraph(value: unknown): void {
|
|
269
291
|
const active = new WeakSet();
|
|
270
292
|
let size = 0;
|
|
293
|
+
// Mutable walk stack: pushed/popped per node and only READ (synchronously)
|
|
294
|
+
// when a failure renders it, so successful requests pay no per-node copy.
|
|
295
|
+
const path: ViolationPathSegment[] = [];
|
|
271
296
|
|
|
272
297
|
function visit(current: unknown, depth: number): void {
|
|
273
298
|
if (depth > MAX_INPUT_DEPTH) fail("INPUT_TOO_DEEP");
|
|
274
299
|
if (typeof current === "string") {
|
|
275
|
-
size += inspectString(current);
|
|
300
|
+
size += inspectString(current, path, false);
|
|
276
301
|
} else if (
|
|
277
302
|
current === null ||
|
|
278
303
|
typeof current === "boolean" ||
|
|
@@ -297,8 +322,16 @@ function inspectGraph(value: unknown): void {
|
|
|
297
322
|
size += keys.length;
|
|
298
323
|
for (const key of keys) {
|
|
299
324
|
if (typeof key !== "string" || FORBIDDEN_KEYS.has(key)) fail();
|
|
300
|
-
|
|
325
|
+
// Only ARRAY indices become numeric segments; digits inside an object
|
|
326
|
+
// key are user-controlled text and must stay masked (QA F1).
|
|
327
|
+
const segment: ViolationPathSegment =
|
|
328
|
+
Array.isArray(current) && /^(?:0|[1-9]\d{0,5})$/u.test(key)
|
|
329
|
+
? Number(key)
|
|
330
|
+
: key;
|
|
331
|
+
path.push(segment);
|
|
332
|
+
size += inspectString(key, path, true);
|
|
301
333
|
visit(ownValue(current, key), depth + 1);
|
|
334
|
+
path.pop();
|
|
302
335
|
}
|
|
303
336
|
active.delete(current);
|
|
304
337
|
}
|
|
@@ -319,10 +352,14 @@ function containsString(value: unknown, target: string): boolean {
|
|
|
319
352
|
}
|
|
320
353
|
|
|
321
354
|
/**
|
|
322
|
-
*
|
|
323
|
-
*
|
|
324
|
-
*
|
|
325
|
-
*
|
|
355
|
+
* Every pinned profile singleton, and nothing else. The set is not enumerated
|
|
356
|
+
* in prose here: the literal below IS the enumeration and cannot go stale,
|
|
357
|
+
* whereas a count in a comment goes stale on the next port -- which is exactly
|
|
358
|
+
* what happened to the text this replaced. Which profile a caller gets when it
|
|
359
|
+
* passes none is `DEFAULT_PROFILE` below and is deliberately not restated
|
|
360
|
+
* here, so that a default switch touches one line rather than two. Admitting a
|
|
361
|
+
* profile is exactly this list -- `validateProfile` never changed to accept a
|
|
362
|
+
* new one.
|
|
326
363
|
*
|
|
327
364
|
* Membership is by REFERENCE, deliberately. A structural check would accept a
|
|
328
365
|
* caller-built object that merely looks like a pinned profile, and every wire
|
|
@@ -339,11 +376,16 @@ function containsString(value: unknown, target: string): boolean {
|
|
|
339
376
|
const ACCEPTED_PROFILES: ReadonlySet<ClaudeCodeProtocolProfile> = new Set([
|
|
340
377
|
CLAUDE_CODE_2_1_195_PROFILE,
|
|
341
378
|
CLAUDE_CODE_2_1_233_PROFILE,
|
|
379
|
+
CLAUDE_CODE_2_1_280_PROFILE,
|
|
342
380
|
]);
|
|
343
381
|
|
|
344
382
|
/**
|
|
345
|
-
* The profile
|
|
346
|
-
*
|
|
383
|
+
* The profile the request-building entry points (`buildClaudeCodeRequest`,
|
|
384
|
+
* `buildClaudeCodeCountTokensRequest` and `parseBuiltClaudeCodeRequest`)
|
|
385
|
+
* resolve to when the caller supplies none. It is not a global default: the
|
|
386
|
+
* model-query and anti-verbosity helpers deliberately keep their own,
|
|
387
|
+
* separately declared default and are NOT governed by this constant. Declared
|
|
388
|
+
* once so that the default is a single, greppable seam: a test
|
|
347
389
|
* that means "whatever the default is" reads THIS instead of naming a
|
|
348
390
|
* version, which keeps a default switch to a one-line diff and keeps tests
|
|
349
391
|
* that genuinely mean 2.1.195 honest about saying so.
|
|
@@ -352,7 +394,7 @@ const ACCEPTED_PROFILES: ReadonlySet<ClaudeCodeProtocolProfile> = new Set([
|
|
|
352
394
|
* from `src/index.ts`: the public runtime surface stays closed.
|
|
353
395
|
*/
|
|
354
396
|
export const DEFAULT_PROFILE: ClaudeCodeProtocolProfile =
|
|
355
|
-
|
|
397
|
+
CLAUDE_CODE_2_1_280_PROFILE;
|
|
356
398
|
|
|
357
399
|
function validateProfile(
|
|
358
400
|
profile: ClaudeCodeProtocolProfile,
|
|
@@ -950,6 +992,8 @@ function requestedCapabilities(
|
|
|
950
992
|
temperature: raw?.temperature ?? supported.temperature,
|
|
951
993
|
rejectsDisabledThinking:
|
|
952
994
|
raw?.rejectsDisabledThinking ?? supported.rejectsDisabledThinking,
|
|
995
|
+
midConvToolChange: raw?.midConvToolChange ?? supported.midConvToolChange,
|
|
996
|
+
perTurnEffort: raw?.perTurnEffort ?? supported.perTurnEffort,
|
|
953
997
|
};
|
|
954
998
|
for (const key of CAPABILITY_KEYS) {
|
|
955
999
|
if (typeof result[key] !== "boolean") fail("UNSUPPORTED_CAPABILITY");
|
|
@@ -1038,7 +1082,7 @@ function parseCapabilityDecisions(
|
|
|
1038
1082
|
value: unknown,
|
|
1039
1083
|
): ClaudeCodeCapabilityDecisions {
|
|
1040
1084
|
if (!isRecord(value)) fail();
|
|
1041
|
-
//
|
|
1085
|
+
// Every capability key is mandatory; the package-extension override keys
|
|
1042
1086
|
// are optional and must survive the round-trip untouched, so they are allowed
|
|
1043
1087
|
// here but never synthesized.
|
|
1044
1088
|
assertExactKeys(value, CAPABILITY_DECISION_KEY_SET);
|
|
@@ -1064,6 +1108,8 @@ function parseCapabilityDecisions(
|
|
|
1064
1108
|
contextManagement: readBoolean("contextManagement"),
|
|
1065
1109
|
temperature: readBoolean("temperature"),
|
|
1066
1110
|
rejectsDisabledThinking: readBoolean("rejectsDisabledThinking"),
|
|
1111
|
+
midConvToolChange: readBoolean("midConvToolChange"),
|
|
1112
|
+
perTurnEffort: readBoolean("perTurnEffort"),
|
|
1067
1113
|
};
|
|
1068
1114
|
}
|
|
1069
1115
|
|
|
@@ -1071,7 +1117,7 @@ function parseCapabilityDecisions(
|
|
|
1071
1117
|
* Validates evidence against the profile the request was parsed under, not
|
|
1072
1118
|
* against a hardcoded singleton. `parseBuiltClaudeCodeRequest` already
|
|
1073
1119
|
* validates `url` against `pinnedProfile.endpoint`; the profile id is the one
|
|
1074
|
-
* remaining field where
|
|
1120
|
+
* remaining field where pinned profiles differ, so it has to follow
|
|
1075
1121
|
* the same source or a request built with a non-default profile could never
|
|
1076
1122
|
* be re-parsed. Still fail-closed: the profile reaching here has already
|
|
1077
1123
|
* passed `validateProfile`.
|
|
@@ -1442,6 +1488,10 @@ export async function buildClaudeCodeCountTokensRequest(
|
|
|
1442
1488
|
normalizedId: resolvedModel.id,
|
|
1443
1489
|
capabilities: resolvedModel.capabilities,
|
|
1444
1490
|
thinkingDisplayActive: false,
|
|
1491
|
+
// A constant, not a re-derivation: this path carries no thinking
|
|
1492
|
+
// request, so `isThinkingActive(undefined, capabilities)` is false
|
|
1493
|
+
// by construction.
|
|
1494
|
+
thinkingActive: false,
|
|
1445
1495
|
},
|
|
1446
1496
|
effectiveProfile,
|
|
1447
1497
|
),
|
|
@@ -1516,8 +1566,9 @@ export async function buildClaudeCodeCountTokensRequest(
|
|
|
1516
1566
|
/**
|
|
1517
1567
|
* Builds one canonical request for the pinned Claude Code wire profile.
|
|
1518
1568
|
*
|
|
1519
|
-
* @param profile - The
|
|
1520
|
-
* `
|
|
1569
|
+
* @param profile - The accepted values are the exported pinned profile
|
|
1570
|
+
* singletons held in `ACCEPTED_PROFILES`; omitting the argument resolves to
|
|
1571
|
+
* `DEFAULT_PROFILE`. Any other object, even a
|
|
1521
1572
|
* structurally identical clone, is rejected with `ClaudeCodeWireError` code
|
|
1522
1573
|
* `INVALID_INPUT`. This deliberate fail-closed behaviour prevents callers from
|
|
1523
1574
|
* substituting an unpinned protocol profile.
|
|
@@ -1581,13 +1632,12 @@ export async function buildClaudeCodeRequest(
|
|
|
1581
1632
|
validated.suppressBillingBlock,
|
|
1582
1633
|
validated.suppressIdentityBlock,
|
|
1583
1634
|
);
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
);
|
|
1635
|
+
/*
|
|
1636
|
+
* Composition runs BEFORE body construction so that a beta site can hand a
|
|
1637
|
+
* decision to the body emitter. Both calls are pure and neither feeds the
|
|
1638
|
+
* other's arguments today, so the order itself changes no serialised byte;
|
|
1639
|
+
* the swap lands on its own so the packed-consumer digests prove that.
|
|
1640
|
+
*/
|
|
1591
1641
|
const composedBetas = composeBetasWithAudit(
|
|
1592
1642
|
{
|
|
1593
1643
|
rawModel: validated.source.model,
|
|
@@ -1598,6 +1648,10 @@ export async function buildClaudeCodeRequest(
|
|
|
1598
1648
|
capabilities,
|
|
1599
1649
|
effectiveProfile.betaPolicy,
|
|
1600
1650
|
),
|
|
1651
|
+
thinkingActive: isThinkingActive(
|
|
1652
|
+
validated.source.thinking,
|
|
1653
|
+
capabilities,
|
|
1654
|
+
),
|
|
1601
1655
|
...(validated.source.cacheControl?.ttl === undefined
|
|
1602
1656
|
? {}
|
|
1603
1657
|
: { cacheTtl: validated.source.cacheControl.ttl }),
|
|
@@ -1616,6 +1670,14 @@ export async function buildClaudeCodeRequest(
|
|
|
1616
1670
|
},
|
|
1617
1671
|
effectiveProfile,
|
|
1618
1672
|
);
|
|
1673
|
+
const canonicalBody = buildCanonicalBody(
|
|
1674
|
+
evidenceRequest(validated.source, validated.source.model),
|
|
1675
|
+
effectiveModel,
|
|
1676
|
+
system,
|
|
1677
|
+
metadata,
|
|
1678
|
+
effectiveProfile,
|
|
1679
|
+
composedBetas.thinkingDisplayOverride,
|
|
1680
|
+
);
|
|
1619
1681
|
const betas = composedBetas.betas;
|
|
1620
1682
|
const headerPlan = buildOrderedHeaderPlan({
|
|
1621
1683
|
accessToken: validated.source.accessToken,
|
|
@@ -1706,8 +1768,9 @@ export async function buildClaudeCodeRequest(
|
|
|
1706
1768
|
/**
|
|
1707
1769
|
* Validates and clones a previously built request into a deeply frozen value.
|
|
1708
1770
|
*
|
|
1709
|
-
* @param profile - The
|
|
1710
|
-
* `
|
|
1771
|
+
* @param profile - The accepted values are the exported pinned profile
|
|
1772
|
+
* singletons held in `ACCEPTED_PROFILES`; omitting the argument resolves to
|
|
1773
|
+
* `DEFAULT_PROFILE`. Any other object, even a
|
|
1711
1774
|
* structurally identical clone, is rejected with `ClaudeCodeWireError` code
|
|
1712
1775
|
* `INVALID_INPUT`. This deliberate fail-closed behaviour prevents callers from
|
|
1713
1776
|
* substituting an unpinned protocol profile.
|
|
@@ -1730,6 +1793,7 @@ export function parseBuiltClaudeCodeRequest(
|
|
|
1730
1793
|
const body = ownValue(value, "body");
|
|
1731
1794
|
if (typeof body !== "string") fail();
|
|
1732
1795
|
const parsedBody = parseBody(body);
|
|
1796
|
+
inspectGraph(parsedBody);
|
|
1733
1797
|
const headers = parseHeaders(ownValue(value, "headers"));
|
|
1734
1798
|
const evidence = parseEvidence(ownValue(value, "evidence"), pinnedProfile);
|
|
1735
1799
|
// Reading evidence is not trusting evidence. A claim that the seam
|
package/src/contracts.ts
CHANGED
|
@@ -669,6 +669,10 @@ export interface ClaudeCodeCapabilities {
|
|
|
669
669
|
readonly contextManagement: boolean;
|
|
670
670
|
readonly temperature: boolean;
|
|
671
671
|
readonly rejectsDisabledThinking: boolean;
|
|
672
|
+
/** Catalogue string `mid_conv_tool_change`. */
|
|
673
|
+
readonly midConvToolChange: boolean;
|
|
674
|
+
/** Catalogue string `per_turn_effort`. */
|
|
675
|
+
readonly perTurnEffort: boolean;
|
|
672
676
|
}
|
|
673
677
|
|
|
674
678
|
/** Host-state beta gates pinned for a default first-party environment. */
|
|
@@ -1221,8 +1225,9 @@ export interface RedactedRequestEvidence {
|
|
|
1221
1225
|
}
|
|
1222
1226
|
|
|
1223
1227
|
/**
|
|
1224
|
-
* Records
|
|
1225
|
-
*
|
|
1228
|
+
* Records every model capability decision -- the key set is exactly
|
|
1229
|
+
* `keyof ClaudeCodeCapabilities`, so it widens whenever that interface does --
|
|
1230
|
+
* plus any package-extension beta override the caller supplied.
|
|
1226
1231
|
*
|
|
1227
1232
|
* The override keys are OPTIONAL and are emitted only when the corresponding
|
|
1228
1233
|
* member of `betaOverrides` is present, so evidence for a request that omits
|
package/src/fingerprint.ts
CHANGED
|
@@ -56,6 +56,11 @@ export async function createBillingFingerprint(
|
|
|
56
56
|
crypto?: Pick<Crypto, "subtle">,
|
|
57
57
|
): Promise<string> {
|
|
58
58
|
const cryptoProvider = crypto ?? getDefaultCrypto();
|
|
59
|
+
// The upstream transcription in the analysis doc writes `||` here; `??` is a
|
|
60
|
+
// deliberate, safe difference, not a divergence to "fix" in either
|
|
61
|
+
// direction. A string subscript yields either a one-unit string, which is
|
|
62
|
+
// always truthy, or `undefined`, so no value makes the two operators
|
|
63
|
+
// disagree (and no test can tell them apart).
|
|
59
64
|
const material = `${FINGERPRINT_PREFIX}${firstUserText[4] ?? "0"}${firstUserText[7] ?? "0"}${firstUserText[20] ?? "0"}${cliVersion}`;
|
|
60
65
|
const bytes = new TextEncoder().encode(material);
|
|
61
66
|
|
package/src/headers.ts
CHANGED
|
@@ -8,6 +8,7 @@ import type {
|
|
|
8
8
|
import { ClaudeCodeWireError } from "./contracts.js";
|
|
9
9
|
import { CLAUDE_CODE_2_1_195_PROFILE } from "./profiles/claude-code-2.1.195.js";
|
|
10
10
|
import { CLAUDE_CODE_2_1_233_PROFILE } from "./profiles/claude-code-2.1.233.js";
|
|
11
|
+
import { CLAUDE_CODE_2_1_280_PROFILE } from "./profiles/claude-code-2.1.280.js";
|
|
11
12
|
|
|
12
13
|
const HEADER_NAMES = Object.freeze({
|
|
13
14
|
anthropicBeta: "anthropic-beta",
|
|
@@ -163,13 +164,14 @@ function parseExtraHeaderPolicy(value: unknown): ClaudeCodeExtraHeaderPolicy {
|
|
|
163
164
|
|
|
164
165
|
/**
|
|
165
166
|
* Accepts a pinned profile by REFERENCE, never by shape, and returns the
|
|
166
|
-
* singleton itself so nothing downstream can be handed a look-alike.
|
|
167
|
-
*
|
|
167
|
+
* singleton itself so nothing downstream can be handed a look-alike. Each
|
|
168
|
+
* pinned profile widens the accepted set by exactly one object;
|
|
168
169
|
* anything else, including a structural clone, still fails closed.
|
|
169
170
|
*/
|
|
170
171
|
function parseProfile(value: unknown): ClaudeCodeProtocolProfile {
|
|
171
172
|
if (value === CLAUDE_CODE_2_1_195_PROFILE) return CLAUDE_CODE_2_1_195_PROFILE;
|
|
172
173
|
if (value === CLAUDE_CODE_2_1_233_PROFILE) return CLAUDE_CODE_2_1_233_PROFILE;
|
|
174
|
+
if (value === CLAUDE_CODE_2_1_280_PROFILE) return CLAUDE_CODE_2_1_280_PROFILE;
|
|
173
175
|
throw new ClaudeCodeWireError("INVALID_INPUT");
|
|
174
176
|
}
|
|
175
177
|
|
package/src/index.ts
CHANGED
|
@@ -82,5 +82,7 @@ export {
|
|
|
82
82
|
} from "./model-queries.js";
|
|
83
83
|
|
|
84
84
|
export { BETA_REGISTRY_2_1_233 } from "./profiles/beta-registry-2.1.233.js";
|
|
85
|
+
export { BETA_REGISTRY_2_1_280 } from "./profiles/beta-registry-2.1.280.js";
|
|
85
86
|
export { CLAUDE_CODE_2_1_195_PROFILE } from "./profiles/claude-code-2.1.195.js";
|
|
86
87
|
export { CLAUDE_CODE_2_1_233_PROFILE } from "./profiles/claude-code-2.1.233.js";
|
|
88
|
+
export { CLAUDE_CODE_2_1_280_PROFILE } from "./profiles/claude-code-2.1.280.js";
|
package/src/limits.ts
ADDED
|
@@ -9,20 +9,20 @@ import { profileBehaviors } from "./profile-behaviors.js";
|
|
|
9
9
|
import { CLAUDE_CODE_2_1_195_PROFILE } from "./profiles/claude-code-2.1.195.js";
|
|
10
10
|
|
|
11
11
|
/*
|
|
12
|
-
* Capability derivation, ported from the genuine client's
|
|
13
|
-
* predicates.
|
|
12
|
+
* Capability derivation, ported from the genuine client's capability
|
|
13
|
+
* predicates and, since 2.1.280, from catalogue strings that have no predicate
|
|
14
|
+
* upstream at all.
|
|
14
15
|
*
|
|
15
16
|
* READ THIS FIRST -- there are two derivation paths and they are not
|
|
16
17
|
* interchangeable:
|
|
17
18
|
*
|
|
18
19
|
* 1. Catalogue path (`deriveCapabilitiesFromCatalogue`), taken for every id
|
|
19
|
-
* present in the
|
|
20
|
-
* verbatim upstream string in
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* predicate-derived.
|
|
20
|
+
* present in the catalogue. The fields listed in
|
|
21
|
+
* `CATALOGUE_BACKED_CAPABILITIES` have a verbatim upstream string in
|
|
22
|
+
* `ClaudeCodeCatalogueEntry.capabilities` and are read from there; that
|
|
23
|
+
* map is the enumeration, so this comment does not repeat it. The rest
|
|
24
|
+
* (`thinking`, `interleavedThinking`, `temperature`) have NO catalogue
|
|
25
|
+
* string in any client version and stay predicate-derived.
|
|
26
26
|
* 2. Predicate fallback (`deriveCapabilitiesFromPredicates`), taken for ids
|
|
27
27
|
* with no catalogue entry -- `claude-mythos-5` (absent by product
|
|
28
28
|
* decision D-1), ids from a newer client, and anything that escaped
|
|
@@ -39,7 +39,7 @@ import { CLAUDE_CODE_2_1_195_PROFILE } from "./profiles/claude-code-2.1.195.js";
|
|
|
39
39
|
* surprising:
|
|
40
40
|
*
|
|
41
41
|
* On the first-party provider -- the only provider this package targets --
|
|
42
|
-
* every one of these
|
|
42
|
+
* every one of these predicates reduces to a pure function of the
|
|
43
43
|
* normalized model id.
|
|
44
44
|
*
|
|
45
45
|
* Why. Upstream, each predicate has the shape
|
|
@@ -87,7 +87,7 @@ import { CLAUDE_CODE_2_1_195_PROFILE } from "./profiles/claude-code-2.1.195.js";
|
|
|
87
87
|
* `claude-mythos-5` has no catalogue entry by product decision D-1. Upstream
|
|
88
88
|
* special-cases it by name in `Kw`, `Hke`, `Yte` and `Uot`; this port subsumes
|
|
89
89
|
* those clauses into the first-party fallback, which yields an identical
|
|
90
|
-
* result. The explicit D-1 test asserting its full
|
|
90
|
+
* result. The explicit D-1 test asserting its full capability row is the
|
|
91
91
|
* guard for that equivalence.
|
|
92
92
|
*
|
|
93
93
|
* Model ids reaching these functions have already been normalized by
|
|
@@ -337,11 +337,19 @@ export function rejectsDisabledThinking(normalizedId: string): boolean {
|
|
|
337
337
|
}
|
|
338
338
|
|
|
339
339
|
/**
|
|
340
|
-
* The
|
|
341
|
-
*
|
|
342
|
-
* `thinking`, `interleavedThinking
|
|
343
|
-
* string in any client version and
|
|
344
|
-
* paths.
|
|
340
|
+
* The `ClaudeCodeCapabilities` fields the catalogue represents, paired with
|
|
341
|
+
* their verbatim upstream capability string. Whatever is not a key here --
|
|
342
|
+
* `thinking`, `interleavedThinking` and `temperature` today -- has no
|
|
343
|
+
* catalogue string in any client version and is derived from its predicate on
|
|
344
|
+
* both paths. The literal below is the enumeration; no count is restated in
|
|
345
|
+
* prose, because a count beside the thing it counts goes stale on the next
|
|
346
|
+
* port.
|
|
347
|
+
*
|
|
348
|
+
* `per_turn_timing` is deliberately NOT mapped. It is a real 2.1.280
|
|
349
|
+
* catalogue string carried by three models, but the beta it would gate,
|
|
350
|
+
* `timing-2026-09-09`, is environment-gated off on the default path, so a
|
|
351
|
+
* field here would be a capability nothing reads. Mapping it would invent a
|
|
352
|
+
* derived value with no emission behind it.
|
|
345
353
|
*/
|
|
346
354
|
const CATALOGUE_BACKED_CAPABILITIES = {
|
|
347
355
|
effort: "effort",
|
|
@@ -350,12 +358,14 @@ const CATALOGUE_BACKED_CAPABILITIES = {
|
|
|
350
358
|
adaptiveThinking: "adaptive_thinking",
|
|
351
359
|
contextManagement: "context_management",
|
|
352
360
|
rejectsDisabledThinking: "rejects_disabled_thinking",
|
|
361
|
+
midConvToolChange: "mid_conv_tool_change",
|
|
362
|
+
perTurnEffort: "per_turn_effort",
|
|
353
363
|
} as const;
|
|
354
364
|
|
|
355
365
|
/**
|
|
356
|
-
* Pure catalogue -> capabilities mapping. Reads nothing but `entry` for
|
|
357
|
-
*
|
|
358
|
-
*
|
|
366
|
+
* Pure catalogue -> capabilities mapping. Reads nothing but `entry` for every
|
|
367
|
+
* catalogue-backed field; `normalizedId` is used only for the fields the
|
|
368
|
+
* catalogue does not represent.
|
|
359
369
|
*
|
|
360
370
|
* This function applies no exceptions and no id special cases. The one cell
|
|
361
371
|
* where the 2.1.195 catalogue disagrees with the wire is corrected by the
|
|
@@ -381,6 +391,8 @@ export function deriveCapabilitiesFromCatalogue(
|
|
|
381
391
|
rejectsDisabledThinking: has(
|
|
382
392
|
CATALOGUE_BACKED_CAPABILITIES.rejectsDisabledThinking,
|
|
383
393
|
),
|
|
394
|
+
midConvToolChange: has(CATALOGUE_BACKED_CAPABILITIES.midConvToolChange),
|
|
395
|
+
perTurnEffort: has(CATALOGUE_BACKED_CAPABILITIES.perTurnEffort),
|
|
384
396
|
});
|
|
385
397
|
}
|
|
386
398
|
|
|
@@ -408,6 +420,30 @@ function deriveCapabilitiesFromPredicates(
|
|
|
408
420
|
contextManagement: supportsContextManagement(normalizedId),
|
|
409
421
|
temperature: supportsTemperature(normalizedId),
|
|
410
422
|
rejectsDisabledThinking: rejectsDisabledThinking(normalizedId),
|
|
423
|
+
/*
|
|
424
|
+
* Both are FALSE here, and this is the one place where the two derivation
|
|
425
|
+
* paths stop agreeing in kind: the six older catalogue-backed fields above
|
|
426
|
+
* resolve maximally permissive for an uncatalogued id, these two resolve
|
|
427
|
+
* restrictive. That asymmetry is deliberate, and the reason differs per
|
|
428
|
+
* field.
|
|
429
|
+
*
|
|
430
|
+
* `perTurnEffort: false` MATCHES upstream. Its lookup consults the static
|
|
431
|
+
* catalogue and, at default settings, the remote client-data path that
|
|
432
|
+
* could override it is empty, so an id with no entry yields false there
|
|
433
|
+
* too.
|
|
434
|
+
*
|
|
435
|
+
* `midConvToolChange: false` DIVERGES from upstream, which still sends
|
|
436
|
+
* `mid-conversation-tool-changes-2026-07-01` for a model it has no
|
|
437
|
+
* catalogue entry for. The analysis document records that as a divergence
|
|
438
|
+
* recorded but not ported, for the same reason the package declines the
|
|
439
|
+
* neighbouring permissive tail: a capability granted to an unknown id is a
|
|
440
|
+
* guess about a model this package knows nothing about.
|
|
441
|
+
*
|
|
442
|
+
* Neither field has an upstream predicate to fall back on -- both exist
|
|
443
|
+
* only as catalogue strings -- so there is no third option here.
|
|
444
|
+
*/
|
|
445
|
+
midConvToolChange: false,
|
|
446
|
+
perTurnEffort: false,
|
|
411
447
|
});
|
|
412
448
|
}
|
|
413
449
|
|
package/src/model-identity.ts
CHANGED
|
@@ -21,17 +21,30 @@ function dottedToDashedVersion(model: string): string {
|
|
|
21
21
|
return model.replace(/(\d)\.(\d)/g, "$1-$2");
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
/**
|
|
24
|
+
/**
|
|
25
|
+
* Ports upstream `$_` (binary offset 226639025); in the 2.1.280 bundle the
|
|
26
|
+
* function is `GF` at byte offset 7413751. The two offsets are in different
|
|
27
|
+
* address spaces: the first indexes the 2.1.195 platform binary, the second
|
|
28
|
+
* the carved 2.1.280 JavaScript bundle, so neither can be found in the other.
|
|
29
|
+
*
|
|
30
|
+
* Rung order is load-bearing: each test is a substring test, so a longer id
|
|
31
|
+
* must precede the shorter id it contains or the shorter rung swallows it.
|
|
32
|
+
*/
|
|
25
33
|
export function normalizeModelId(model: string): string {
|
|
26
34
|
model = dottedToDashedVersion(model.toLowerCase());
|
|
35
|
+
if (model.includes("claude-fable-5-1")) return "claude-fable-5-1";
|
|
27
36
|
if (model.includes("claude-fable-5")) return "claude-fable-5";
|
|
37
|
+
if (model.includes("claude-mythos-5-1")) return "claude-mythos-5-1";
|
|
28
38
|
if (model.includes("claude-mythos-5")) return "claude-mythos-5";
|
|
39
|
+
if (model.includes("claude-opus-5-5")) return "claude-opus-5-5";
|
|
40
|
+
if (model.includes("claude-opus-5")) return "claude-opus-5";
|
|
29
41
|
if (model.includes("claude-opus-4-8")) return "claude-opus-4-8";
|
|
30
42
|
if (model.includes("claude-opus-4-7")) return "claude-opus-4-7";
|
|
31
43
|
if (model.includes("claude-opus-4-6")) return "claude-opus-4-6";
|
|
32
44
|
if (model.includes("claude-opus-4-5")) return "claude-opus-4-5";
|
|
33
45
|
if (model.includes("claude-opus-4-1")) return "claude-opus-4-1";
|
|
34
46
|
if (/claude-opus-4(?!-\d(?!\d))/.test(model)) return "claude-opus-4-0";
|
|
47
|
+
if (model.includes("claude-sonnet-5")) return "claude-sonnet-5";
|
|
35
48
|
if (model.includes("claude-sonnet-4-6")) return "claude-sonnet-4-6";
|
|
36
49
|
if (model.includes("claude-sonnet-4-5")) return "claude-sonnet-4-5";
|
|
37
50
|
if (/claude-sonnet-4(?!-\d(?!\d))/.test(model)) return "claude-sonnet-4-0";
|