@tormentalabs/claude-code-wire-compat 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/CHANGELOG.md +217 -0
  2. package/README.md +51 -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 +44 -15
  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 +10 -3
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +6 -0
  23. package/dist/index.js.map +1 -1
  24. package/dist/model-capabilities.d.ts +3 -3
  25. package/dist/model-capabilities.d.ts.map +1 -1
  26. package/dist/model-capabilities.js +55 -19
  27. package/dist/model-capabilities.js.map +1 -1
  28. package/dist/model-identity.d.ts +9 -1
  29. package/dist/model-identity.d.ts.map +1 -1
  30. package/dist/model-identity.js +33 -2
  31. package/dist/model-identity.js.map +1 -1
  32. package/dist/model-queries.d.ts +89 -0
  33. package/dist/model-queries.d.ts.map +1 -0
  34. package/dist/model-queries.js +232 -0
  35. package/dist/model-queries.js.map +1 -0
  36. package/dist/models.js +1 -1
  37. package/dist/models.js.map +1 -1
  38. package/dist/profiles/beta-registry-2.1.280.d.ts +211 -0
  39. package/dist/profiles/beta-registry-2.1.280.d.ts.map +1 -0
  40. package/dist/profiles/beta-registry-2.1.280.js +292 -0
  41. package/dist/profiles/beta-registry-2.1.280.js.map +1 -0
  42. package/dist/profiles/claude-code-2.1.280.d.ts +3 -0
  43. package/dist/profiles/claude-code-2.1.280.d.ts.map +1 -0
  44. package/dist/profiles/claude-code-2.1.280.js +359 -0
  45. package/dist/profiles/claude-code-2.1.280.js.map +1 -0
  46. package/dist/redaction.d.ts.map +1 -1
  47. package/dist/redaction.js +3 -0
  48. package/dist/redaction.js.map +1 -1
  49. package/dist/request-body.d.ts +1 -1
  50. package/dist/request-body.d.ts.map +1 -1
  51. package/dist/request-body.js +16 -5
  52. package/dist/request-body.js.map +1 -1
  53. package/dist/thinking.d.ts +53 -2
  54. package/dist/thinking.d.ts.map +1 -1
  55. package/dist/thinking.js +124 -26
  56. package/dist/thinking.js.map +1 -1
  57. package/package.json +6 -3
  58. package/src/betas.ts +208 -9
  59. package/src/build-request.ts +54 -21
  60. package/src/contracts.ts +7 -2
  61. package/src/fingerprint.ts +5 -0
  62. package/src/headers.ts +4 -2
  63. package/src/index.ts +28 -3
  64. package/src/model-capabilities.ts +55 -19
  65. package/src/model-identity.ts +29 -2
  66. package/src/model-queries.ts +273 -0
  67. package/src/models.ts +1 -1
  68. package/src/profiles/beta-registry-2.1.280.ts +309 -0
  69. package/src/profiles/claude-code-2.1.280.ts +364 -0
  70. package/src/redaction.ts +3 -0
  71. package/src/request-body.ts +25 -3
  72. package/src/thinking.ts +148 -26
@@ -30,6 +30,7 @@ import {
30
30
  import { resolveModel } from "./models.js";
31
31
  import { CLAUDE_CODE_2_1_195_PROFILE } from "./profiles/claude-code-2.1.195.js";
32
32
  import { CLAUDE_CODE_2_1_233_PROFILE } from "./profiles/claude-code-2.1.233.js";
33
+ import { CLAUDE_CODE_2_1_280_PROFILE } from "./profiles/claude-code-2.1.280.js";
33
34
  import type { NormalizedRequestInput } from "./redaction.js";
34
35
  import { buildRedactedEvidence, toSafeErrorDetails } from "./redaction.js";
35
36
  import {
@@ -38,7 +39,7 @@ import {
38
39
  } from "./request-body.js";
39
40
  import { sha256Hex } from "./sha256.js";
40
41
  import { buildCanonicalSystem, IDENTITY_TEXT } from "./system-prompt.js";
41
- import { isThinkingDisplayActive } from "./thinking.js";
42
+ import { isThinkingActive, isThinkingDisplayActive } from "./thinking.js";
42
43
  import { classifySurrogateAt } from "./unicode.js";
43
44
 
44
45
  const METHOD = "POST";
@@ -152,6 +153,8 @@ const CAPABILITY_KEYS = [
152
153
  "contextManagement",
153
154
  "temperature",
154
155
  "rejectsDisabledThinking",
156
+ "midConvToolChange",
157
+ "perTurnEffort",
155
158
  ] as const;
156
159
  const CAPABILITY_KEY_SET = new Set(CAPABILITY_KEYS);
157
160
  /** Adds the optional package-extension override keys carried by evidence. */
@@ -319,10 +322,14 @@ function containsString(value: unknown, target: string): boolean {
319
322
  }
320
323
 
321
324
  /**
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.
325
+ * Every pinned profile singleton, and nothing else. The set is not enumerated
326
+ * in prose here: the literal below IS the enumeration and cannot go stale,
327
+ * whereas a count in a comment goes stale on the next port -- which is exactly
328
+ * what happened to the text this replaced. Which profile a caller gets when it
329
+ * passes none is `DEFAULT_PROFILE` below and is deliberately not restated
330
+ * here, so that a default switch touches one line rather than two. Admitting a
331
+ * profile is exactly this list -- `validateProfile` never changed to accept a
332
+ * new one.
326
333
  *
327
334
  * Membership is by REFERENCE, deliberately. A structural check would accept a
328
335
  * caller-built object that merely looks like a pinned profile, and every wire
@@ -339,11 +346,16 @@ function containsString(value: unknown, target: string): boolean {
339
346
  const ACCEPTED_PROFILES: ReadonlySet<ClaudeCodeProtocolProfile> = new Set([
340
347
  CLAUDE_CODE_2_1_195_PROFILE,
341
348
  CLAUDE_CODE_2_1_233_PROFILE,
349
+ CLAUDE_CODE_2_1_280_PROFILE,
342
350
  ]);
343
351
 
344
352
  /**
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
353
+ * The profile the request-building entry points (`buildClaudeCodeRequest`,
354
+ * `buildClaudeCodeCountTokensRequest` and `parseBuiltClaudeCodeRequest`)
355
+ * resolve to when the caller supplies none. It is not a global default: the
356
+ * model-query and anti-verbosity helpers deliberately keep their own,
357
+ * separately declared default and are NOT governed by this constant. Declared
358
+ * once so that the default is a single, greppable seam: a test
347
359
  * that means "whatever the default is" reads THIS instead of naming a
348
360
  * version, which keeps a default switch to a one-line diff and keeps tests
349
361
  * that genuinely mean 2.1.195 honest about saying so.
@@ -352,7 +364,7 @@ const ACCEPTED_PROFILES: ReadonlySet<ClaudeCodeProtocolProfile> = new Set([
352
364
  * from `src/index.ts`: the public runtime surface stays closed.
353
365
  */
354
366
  export const DEFAULT_PROFILE: ClaudeCodeProtocolProfile =
355
- CLAUDE_CODE_2_1_233_PROFILE;
367
+ CLAUDE_CODE_2_1_280_PROFILE;
356
368
 
357
369
  function validateProfile(
358
370
  profile: ClaudeCodeProtocolProfile,
@@ -950,6 +962,8 @@ function requestedCapabilities(
950
962
  temperature: raw?.temperature ?? supported.temperature,
951
963
  rejectsDisabledThinking:
952
964
  raw?.rejectsDisabledThinking ?? supported.rejectsDisabledThinking,
965
+ midConvToolChange: raw?.midConvToolChange ?? supported.midConvToolChange,
966
+ perTurnEffort: raw?.perTurnEffort ?? supported.perTurnEffort,
953
967
  };
954
968
  for (const key of CAPABILITY_KEYS) {
955
969
  if (typeof result[key] !== "boolean") fail("UNSUPPORTED_CAPABILITY");
@@ -1038,7 +1052,7 @@ function parseCapabilityDecisions(
1038
1052
  value: unknown,
1039
1053
  ): ClaudeCodeCapabilityDecisions {
1040
1054
  if (!isRecord(value)) fail();
1041
- // The nine capability keys are mandatory; the package-extension override keys
1055
+ // Every capability key is mandatory; the package-extension override keys
1042
1056
  // are optional and must survive the round-trip untouched, so they are allowed
1043
1057
  // here but never synthesized.
1044
1058
  assertExactKeys(value, CAPABILITY_DECISION_KEY_SET);
@@ -1064,6 +1078,8 @@ function parseCapabilityDecisions(
1064
1078
  contextManagement: readBoolean("contextManagement"),
1065
1079
  temperature: readBoolean("temperature"),
1066
1080
  rejectsDisabledThinking: readBoolean("rejectsDisabledThinking"),
1081
+ midConvToolChange: readBoolean("midConvToolChange"),
1082
+ perTurnEffort: readBoolean("perTurnEffort"),
1067
1083
  };
1068
1084
  }
1069
1085
 
@@ -1071,7 +1087,7 @@ function parseCapabilityDecisions(
1071
1087
  * Validates evidence against the profile the request was parsed under, not
1072
1088
  * against a hardcoded singleton. `parseBuiltClaudeCodeRequest` already
1073
1089
  * 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
1090
+ * remaining field where pinned profiles differ, so it has to follow
1075
1091
  * the same source or a request built with a non-default profile could never
1076
1092
  * be re-parsed. Still fail-closed: the profile reaching here has already
1077
1093
  * passed `validateProfile`.
@@ -1442,6 +1458,10 @@ export async function buildClaudeCodeCountTokensRequest(
1442
1458
  normalizedId: resolvedModel.id,
1443
1459
  capabilities: resolvedModel.capabilities,
1444
1460
  thinkingDisplayActive: false,
1461
+ // A constant, not a re-derivation: this path carries no thinking
1462
+ // request, so `isThinkingActive(undefined, capabilities)` is false
1463
+ // by construction.
1464
+ thinkingActive: false,
1445
1465
  },
1446
1466
  effectiveProfile,
1447
1467
  ),
@@ -1516,8 +1536,9 @@ export async function buildClaudeCodeCountTokensRequest(
1516
1536
  /**
1517
1537
  * Builds one canonical request for the pinned Claude Code wire profile.
1518
1538
  *
1519
- * @param profile - The only accepted value is the exported
1520
- * `CLAUDE_CODE_2_1_195_PROFILE` singleton. Any other object, even a
1539
+ * @param profile - The accepted values are the exported pinned profile
1540
+ * singletons held in `ACCEPTED_PROFILES`; omitting the argument resolves to
1541
+ * `DEFAULT_PROFILE`. Any other object, even a
1521
1542
  * structurally identical clone, is rejected with `ClaudeCodeWireError` code
1522
1543
  * `INVALID_INPUT`. This deliberate fail-closed behaviour prevents callers from
1523
1544
  * substituting an unpinned protocol profile.
@@ -1581,13 +1602,12 @@ export async function buildClaudeCodeRequest(
1581
1602
  validated.suppressBillingBlock,
1582
1603
  validated.suppressIdentityBlock,
1583
1604
  );
1584
- const canonicalBody = buildCanonicalBody(
1585
- evidenceRequest(validated.source, validated.source.model),
1586
- effectiveModel,
1587
- system,
1588
- metadata,
1589
- effectiveProfile,
1590
- );
1605
+ /*
1606
+ * Composition runs BEFORE body construction so that a beta site can hand a
1607
+ * decision to the body emitter. Both calls are pure and neither feeds the
1608
+ * other's arguments today, so the order itself changes no serialised byte;
1609
+ * the swap lands on its own so the packed-consumer digests prove that.
1610
+ */
1591
1611
  const composedBetas = composeBetasWithAudit(
1592
1612
  {
1593
1613
  rawModel: validated.source.model,
@@ -1598,6 +1618,10 @@ export async function buildClaudeCodeRequest(
1598
1618
  capabilities,
1599
1619
  effectiveProfile.betaPolicy,
1600
1620
  ),
1621
+ thinkingActive: isThinkingActive(
1622
+ validated.source.thinking,
1623
+ capabilities,
1624
+ ),
1601
1625
  ...(validated.source.cacheControl?.ttl === undefined
1602
1626
  ? {}
1603
1627
  : { cacheTtl: validated.source.cacheControl.ttl }),
@@ -1616,6 +1640,14 @@ export async function buildClaudeCodeRequest(
1616
1640
  },
1617
1641
  effectiveProfile,
1618
1642
  );
1643
+ const canonicalBody = buildCanonicalBody(
1644
+ evidenceRequest(validated.source, validated.source.model),
1645
+ effectiveModel,
1646
+ system,
1647
+ metadata,
1648
+ effectiveProfile,
1649
+ composedBetas.thinkingDisplayOverride,
1650
+ );
1619
1651
  const betas = composedBetas.betas;
1620
1652
  const headerPlan = buildOrderedHeaderPlan({
1621
1653
  accessToken: validated.source.accessToken,
@@ -1706,8 +1738,9 @@ export async function buildClaudeCodeRequest(
1706
1738
  /**
1707
1739
  * Validates and clones a previously built request into a deeply frozen value.
1708
1740
  *
1709
- * @param profile - The only accepted value is the exported
1710
- * `CLAUDE_CODE_2_1_195_PROFILE` singleton. Any other object, even a
1741
+ * @param profile - The accepted values are the exported pinned profile
1742
+ * singletons held in `ACCEPTED_PROFILES`; omitting the argument resolves to
1743
+ * `DEFAULT_PROFILE`. Any other object, even a
1711
1744
  * structurally identical clone, is rejected with `ClaudeCodeWireError` code
1712
1745
  * `INVALID_INPUT`. This deliberate fail-closed behaviour prevents callers from
1713
1746
  * substituting an unpinned protocol profile.
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
@@ -3,9 +3,10 @@
3
3
  /**
4
4
  * Public entry point for the Claude Code wire compatibility package.
5
5
  *
6
- * Only the surfaces listed below are public. The Wave 2 implementation
7
- * Internal protocol modules remain private; only the documented builder and
8
- * parser are exported here.
6
+ * Only the surfaces listed below are public. Internal protocol modules remain
7
+ * private; the documented builder, parser, anti-verbosity helpers, read-only
8
+ * model queries and the transcribed beta registries are exported here, and
9
+ * nothing else.
9
10
  *
10
11
  * Importing this module has no side effects. It reads no environment, opens
11
12
  * no network connection, touches no clock or random source, and holds no
@@ -53,11 +54,35 @@ export {
53
54
  selectAntiVerbositySection,
54
55
  } from "./anti-verbosity.js";
55
56
 
57
+ export { BETA_REGISTRY } from "./beta-registry.js";
58
+
56
59
  export {
57
60
  buildClaudeCodeCountTokensRequest,
58
61
  buildClaudeCodeRequest,
59
62
  parseBuiltClaudeCodeRequest,
60
63
  } from "./build-request.js";
61
64
 
65
+ export { TOKEN_COUNTING_BETA } from "./count-tokens.js";
66
+
67
+ export {
68
+ hasOneMillionContext,
69
+ isAdaptiveThinkingModel,
70
+ isClaude3Model,
71
+ isEligibleFor1MContext,
72
+ isFable5Model,
73
+ isHaikuModel,
74
+ isMythos5Model,
75
+ isOpus46Model,
76
+ isOpus47Model,
77
+ isOpus48Model,
78
+ isSonnet46Model,
79
+ modelCapability,
80
+ supportsStructuredOutputs,
81
+ supportsWebSearch,
82
+ } from "./model-queries.js";
83
+
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";
62
86
  export { CLAUDE_CODE_2_1_195_PROFILE } from "./profiles/claude-code-2.1.195.js";
63
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";
@@ -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
 
@@ -7,17 +7,44 @@ export function stripModelMarkers(model: string): string {
7
7
  return model.replace(/\[(1|2)m\]/gi, "");
8
8
  }
9
9
 
10
- /** Ports upstream `$_` (binary offset 226639025). */
10
+ /**
11
+ * Rewrites dotted version separators to the hyphenated form upstream uses on
12
+ * the wire (`claude-opus-4.7` -> `claude-opus-4-7`). Only a digit-dot-digit
13
+ * run is rewritten, so ids with no dotted version (`gpt-4o`, `""`) and
14
+ * unrelated dots are left byte-identical.
15
+ *
16
+ * The plugin performs the same rewrite at its wire seam
17
+ * (`lib/mimicry/wire-compat.mjs`); the plugin's own family predicates accept
18
+ * `[._-]` interchangeably, so this keeps package classification in parity.
19
+ */
20
+ function dottedToDashedVersion(model: string): string {
21
+ return model.replace(/(\d)\.(\d)/g, "$1-$2");
22
+ }
23
+
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
+ */
11
33
  export function normalizeModelId(model: string): string {
12
- model = model.toLowerCase();
34
+ model = dottedToDashedVersion(model.toLowerCase());
35
+ if (model.includes("claude-fable-5-1")) return "claude-fable-5-1";
13
36
  if (model.includes("claude-fable-5")) return "claude-fable-5";
37
+ if (model.includes("claude-mythos-5-1")) return "claude-mythos-5-1";
14
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";
15
41
  if (model.includes("claude-opus-4-8")) return "claude-opus-4-8";
16
42
  if (model.includes("claude-opus-4-7")) return "claude-opus-4-7";
17
43
  if (model.includes("claude-opus-4-6")) return "claude-opus-4-6";
18
44
  if (model.includes("claude-opus-4-5")) return "claude-opus-4-5";
19
45
  if (model.includes("claude-opus-4-1")) return "claude-opus-4-1";
20
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";
21
48
  if (model.includes("claude-sonnet-4-6")) return "claude-sonnet-4-6";
22
49
  if (model.includes("claude-sonnet-4-5")) return "claude-sonnet-4-5";
23
50
  if (/claude-sonnet-4(?!-\d(?!\d))/.test(model)) return "claude-sonnet-4-0";