@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.
Files changed (91) hide show
  1. package/CHANGELOG.md +226 -0
  2. package/README.md +19 -7
  3. package/dist/betas.d.ts +65 -7
  4. package/dist/betas.d.ts.map +1 -1
  5. package/dist/betas.js +143 -2
  6. package/dist/betas.js.map +1 -1
  7. package/dist/build-request.d.ts +12 -6
  8. package/dist/build-request.d.ts.map +1 -1
  9. package/dist/build-request.js +93 -50
  10. package/dist/build-request.js.map +1 -1
  11. package/dist/contracts.d.ts +7 -2
  12. package/dist/contracts.d.ts.map +1 -1
  13. package/dist/contracts.js.map +1 -1
  14. package/dist/fingerprint.d.ts.map +1 -1
  15. package/dist/fingerprint.js +5 -0
  16. package/dist/fingerprint.js.map +1 -1
  17. package/dist/headers.d.ts.map +1 -1
  18. package/dist/headers.js +5 -2
  19. package/dist/headers.js.map +1 -1
  20. package/dist/index.d.ts +2 -0
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +2 -0
  23. package/dist/index.js.map +1 -1
  24. package/dist/limits.d.ts +3 -0
  25. package/dist/limits.d.ts.map +1 -0
  26. package/dist/limits.js +4 -0
  27. package/dist/limits.js.map +1 -0
  28. package/dist/model-capabilities.d.ts +3 -3
  29. package/dist/model-capabilities.d.ts.map +1 -1
  30. package/dist/model-capabilities.js +55 -19
  31. package/dist/model-capabilities.js.map +1 -1
  32. package/dist/model-identity.d.ts +9 -1
  33. package/dist/model-identity.d.ts.map +1 -1
  34. package/dist/model-identity.js +19 -1
  35. package/dist/model-identity.js.map +1 -1
  36. package/dist/model-queries.d.ts +17 -5
  37. package/dist/model-queries.d.ts.map +1 -1
  38. package/dist/model-queries.js +33 -6
  39. package/dist/model-queries.js.map +1 -1
  40. package/dist/models.js +1 -1
  41. package/dist/models.js.map +1 -1
  42. package/dist/profiles/beta-registry-2.1.280.d.ts +211 -0
  43. package/dist/profiles/beta-registry-2.1.280.d.ts.map +1 -0
  44. package/dist/profiles/beta-registry-2.1.280.js +292 -0
  45. package/dist/profiles/beta-registry-2.1.280.js.map +1 -0
  46. package/dist/profiles/claude-code-2.1.280.d.ts +3 -0
  47. package/dist/profiles/claude-code-2.1.280.d.ts.map +1 -0
  48. package/dist/profiles/claude-code-2.1.280.js +359 -0
  49. package/dist/profiles/claude-code-2.1.280.js.map +1 -0
  50. package/dist/redaction.d.ts.map +1 -1
  51. package/dist/redaction.js +48 -2
  52. package/dist/redaction.js.map +1 -1
  53. package/dist/request-body.d.ts +4 -2
  54. package/dist/request-body.d.ts.map +1 -1
  55. package/dist/request-body.js +77 -48
  56. package/dist/request-body.js.map +1 -1
  57. package/dist/system-prompt.d.ts.map +1 -1
  58. package/dist/system-prompt.js +23 -22
  59. package/dist/system-prompt.js.map +1 -1
  60. package/dist/thinking.d.ts +53 -2
  61. package/dist/thinking.d.ts.map +1 -1
  62. package/dist/thinking.js +124 -26
  63. package/dist/thinking.js.map +1 -1
  64. package/dist/unicode.d.ts +41 -0
  65. package/dist/unicode.d.ts.map +1 -1
  66. package/dist/unicode.js +37 -0
  67. package/dist/unicode.js.map +1 -1
  68. package/dist/violation.d.ts +23 -0
  69. package/dist/violation.d.ts.map +1 -0
  70. package/dist/violation.js +210 -0
  71. package/dist/violation.js.map +1 -0
  72. package/package.json +6 -2
  73. package/src/betas.ts +208 -9
  74. package/src/build-request.ts +120 -56
  75. package/src/contracts.ts +7 -2
  76. package/src/fingerprint.ts +5 -0
  77. package/src/headers.ts +4 -2
  78. package/src/index.ts +2 -0
  79. package/src/limits.ts +4 -0
  80. package/src/model-capabilities.ts +55 -19
  81. package/src/model-identity.ts +14 -1
  82. package/src/model-queries.ts +38 -6
  83. package/src/models.ts +1 -1
  84. package/src/profiles/beta-registry-2.1.280.ts +309 -0
  85. package/src/profiles/claude-code-2.1.280.ts +364 -0
  86. package/src/redaction.ts +65 -2
  87. package/src/request-body.ts +107 -49
  88. package/src/system-prompt.ts +32 -24
  89. package/src/thinking.ts +148 -26
  90. package/src/unicode.ts +71 -0
  91. package/src/violation.ts +226 -0
@@ -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 { classifySurrogateAt } from "./unicode.js";
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
- * TAB (0x09), LF (0x0A) and CR (0x0D) are ALLOWED. This function walks the
232
- * whole input graph, which is overwhelmingly BODY content — message text,
233
- * system blocks, tool descriptions — where a line break is ordinary prose that
234
- * `JSON.stringify` escapes on the way out. Rejecting them here made the package
235
- * unusable for real traffic: no genuine prompt is a single line.
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
- * The strict rule those three characters used to be caught by is a HEADER rule,
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
- * Every other C0 control (0x00-0x08, 0x0B, 0x0C, 0x0E-0x1F) and DEL (0x7F)
245
- * stay rejected: they have no meaning in prompt text and are a reliable signal
246
- * of a corrupted or hostile input.
247
- *
248
- * LONE SURROGATES stay rejected in every context, deliberately. `TextEncoder`
249
- * silently replaces them with U+FFFD, so an unpaired surrogate would corrupt
250
- * the body — and the body hash recorded in evidence — with no error anywhere.
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(value: string): number {
253
- for (let index = 0; index < value.length; index += 1) {
254
- const unit = value.charCodeAt(index);
255
- if (
256
- (unit <= 0x1f && unit !== 0x09 && unit !== 0x0a && unit !== 0x0d) ||
257
- unit === 0x7f
258
- ) {
259
- fail("INVALID_UNICODE");
260
- }
261
- const classification = classifySurrogateAt(value, index);
262
- if (classification === "loneSurrogate") fail("INVALID_UNICODE");
263
- if (classification === "surrogatePair") index += 1;
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
- size += inspectString(key);
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
- * The profiles this package will assemble a request for. Two entries: the
323
- * 2.1.195 default and the 2.1.233 profile, which callers must pass
324
- * explicitly. Admitting a profile is exactly this list -- `validateProfile`
325
- * did not change to accept the second one.
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 every public entry point resolves to when the caller supplies
346
- * none. Declared once so that the default is a single, greppable seam: a test
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
- CLAUDE_CODE_2_1_233_PROFILE;
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
- // The nine capability keys are mandatory; the package-extension override keys
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 the two pinned profiles differ, so it has to follow
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 only accepted value is the exported
1520
- * `CLAUDE_CODE_2_1_195_PROFILE` singleton. Any other object, even a
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
- const canonicalBody = buildCanonicalBody(
1585
- evidenceRequest(validated.source, validated.source.model),
1586
- effectiveModel,
1587
- system,
1588
- metadata,
1589
- effectiveProfile,
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 only accepted value is the exported
1710
- * `CLAUDE_CODE_2_1_195_PROFILE` singleton. Any other object, even a
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 the nine model capability decisions, plus any package-extension beta
1225
- * override the caller supplied.
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
@@ -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. Adding
167
- * the second pinned profile widens the accepted set by exactly one object;
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
@@ -0,0 +1,4 @@
1
+ // SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+ /** Aggregate graph budget, not a serialized request-size guarantee. */
4
+ export const MAX_INPUT_SIZE = 1_000_000;
@@ -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 nine capability
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 2.1.195 catalogue. Six of the nine capabilities have a
20
- * verbatim upstream string in `ClaudeCodeCatalogueEntry.capabilities`
21
- * (`effort`, `max_effort`, `xhigh_effort`, `adaptive_thinking`,
22
- * `context_management`, `rejects_disabled_thinking`) and are read from
23
- * there. The other three (`thinking`, `interleavedThinking`,
24
- * `temperature`) have NO catalogue string in any client version and stay
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 nine predicates reduces to a pure function of the
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 nine-boolean row is the
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 six `ClaudeCodeCapabilities` fields the catalogue represents, paired
341
- * with their verbatim upstream capability string. The three omitted fields --
342
- * `thinking`, `interleavedThinking`, `temperature` -- have no catalogue
343
- * string in any client version and are derived from their predicates on both
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 the
357
- * six catalogue-backed fields; `normalizedId` is used only for the three
358
- * fields the catalogue does not represent.
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
 
@@ -21,17 +21,30 @@ function dottedToDashedVersion(model: string): string {
21
21
  return model.replace(/(\d)\.(\d)/g, "$1-$2");
22
22
  }
23
23
 
24
- /** Ports upstream `$_` (binary offset 226639025). */
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";