@tormentalabs/claude-code-wire-compat 0.1.0 → 0.2.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 +65 -0
  2. package/README.md +14 -1
  3. package/dist/betas.d.ts +37 -0
  4. package/dist/betas.d.ts.map +1 -1
  5. package/dist/betas.js +55 -23
  6. package/dist/betas.js.map +1 -1
  7. package/dist/build-request.d.ts +11 -0
  8. package/dist/build-request.d.ts.map +1 -1
  9. package/dist/build-request.js +133 -11
  10. package/dist/build-request.js.map +1 -1
  11. package/dist/contracts.d.ts +53 -0
  12. package/dist/contracts.d.ts.map +1 -1
  13. package/dist/contracts.js.map +1 -1
  14. package/dist/fingerprint.d.ts +29 -2
  15. package/dist/fingerprint.d.ts.map +1 -1
  16. package/dist/fingerprint.js +60 -7
  17. package/dist/fingerprint.js.map +1 -1
  18. package/dist/headers.d.ts.map +1 -1
  19. package/dist/headers.js +12 -4
  20. package/dist/headers.js.map +1 -1
  21. package/dist/index.d.ts +1 -0
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +1 -0
  24. package/dist/index.js.map +1 -1
  25. package/dist/model-capabilities.d.ts +31 -3
  26. package/dist/model-capabilities.d.ts.map +1 -1
  27. package/dist/model-capabilities.js +145 -12
  28. package/dist/model-capabilities.js.map +1 -1
  29. package/dist/models.d.ts.map +1 -1
  30. package/dist/models.js +8 -4
  31. package/dist/models.js.map +1 -1
  32. package/dist/profile-behaviors.d.ts +61 -0
  33. package/dist/profile-behaviors.d.ts.map +1 -0
  34. package/dist/profile-behaviors.js +53 -0
  35. package/dist/profile-behaviors.js.map +1 -0
  36. package/dist/profiles/beta-registry-2.1.233.d.ts +140 -0
  37. package/dist/profiles/beta-registry-2.1.233.d.ts.map +1 -0
  38. package/dist/profiles/beta-registry-2.1.233.js +183 -0
  39. package/dist/profiles/beta-registry-2.1.233.js.map +1 -0
  40. package/dist/profiles/claude-code-2.1.195.d.ts.map +1 -1
  41. package/dist/profiles/claude-code-2.1.195.js +14 -0
  42. package/dist/profiles/claude-code-2.1.195.js.map +1 -1
  43. package/dist/profiles/claude-code-2.1.233.d.ts +3 -0
  44. package/dist/profiles/claude-code-2.1.233.d.ts.map +1 -0
  45. package/dist/profiles/claude-code-2.1.233.js +235 -0
  46. package/dist/profiles/claude-code-2.1.233.js.map +1 -0
  47. package/dist/redaction.d.ts.map +1 -1
  48. package/dist/redaction.js +14 -1
  49. package/dist/redaction.js.map +1 -1
  50. package/dist/request-body.d.ts.map +1 -1
  51. package/dist/request-body.js +12 -10
  52. package/dist/request-body.js.map +1 -1
  53. package/dist/thinking.d.ts +33 -7
  54. package/dist/thinking.d.ts.map +1 -1
  55. package/dist/thinking.js +105 -36
  56. package/dist/thinking.js.map +1 -1
  57. package/package.json +9 -2
  58. package/src/betas.ts +106 -23
  59. package/src/build-request.ts +155 -11
  60. package/src/contracts.ts +53 -0
  61. package/src/fingerprint.ts +78 -8
  62. package/src/headers.ts +10 -4
  63. package/src/index.ts +1 -0
  64. package/src/model-capabilities.ts +171 -13
  65. package/src/models.ts +8 -4
  66. package/src/profile-behaviors.ts +114 -0
  67. package/src/profiles/beta-registry-2.1.233.ts +200 -0
  68. package/src/profiles/claude-code-2.1.195.ts +14 -0
  69. package/src/profiles/claude-code-2.1.233.ts +240 -0
  70. package/src/redaction.ts +16 -1
  71. package/src/request-body.ts +16 -9
  72. package/src/thinking.ts +119 -39
package/src/headers.ts CHANGED
@@ -7,6 +7,7 @@ import type {
7
7
  } from "./contracts.js";
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
+ import { CLAUDE_CODE_2_1_233_PROFILE } from "./profiles/claude-code-2.1.233.js";
10
11
 
11
12
  const HEADER_NAMES = Object.freeze({
12
13
  anthropicBeta: "anthropic-beta",
@@ -160,11 +161,16 @@ function parseExtraHeaderPolicy(value: unknown): ClaudeCodeExtraHeaderPolicy {
160
161
  return value;
161
162
  }
162
163
 
164
+ /**
165
+ * 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;
168
+ * anything else, including a structural clone, still fails closed.
169
+ */
163
170
  function parseProfile(value: unknown): ClaudeCodeProtocolProfile {
164
- if (value !== CLAUDE_CODE_2_1_195_PROFILE) {
165
- throw new ClaudeCodeWireError("INVALID_INPUT");
166
- }
167
- return CLAUDE_CODE_2_1_195_PROFILE;
171
+ if (value === CLAUDE_CODE_2_1_195_PROFILE) return CLAUDE_CODE_2_1_195_PROFILE;
172
+ if (value === CLAUDE_CODE_2_1_233_PROFILE) return CLAUDE_CODE_2_1_233_PROFILE;
173
+ throw new ClaudeCodeWireError("INVALID_INPUT");
168
174
  }
169
175
 
170
176
  function parseApp(value: unknown): "cli" | "cli-bg" {
package/src/index.ts CHANGED
@@ -60,3 +60,4 @@ export {
60
60
  } from "./build-request.js";
61
61
 
62
62
  export { CLAUDE_CODE_2_1_195_PROFILE } from "./profiles/claude-code-2.1.195.js";
63
+ export { CLAUDE_CODE_2_1_233_PROFILE } from "./profiles/claude-code-2.1.233.js";
@@ -1,17 +1,46 @@
1
1
  // SPDX-License-Identifier: GPL-3.0-or-later
2
2
 
3
- import type { ClaudeCodeCapabilities } from "./contracts.js";
3
+ import type {
4
+ ClaudeCodeCapabilities,
5
+ ClaudeCodeCatalogueEntry,
6
+ ClaudeCodeProtocolProfile,
7
+ } from "./contracts.js";
8
+ import { profileBehaviors } from "./profile-behaviors.js";
9
+ import { CLAUDE_CODE_2_1_195_PROFILE } from "./profiles/claude-code-2.1.195.js";
4
10
 
5
11
  /*
6
12
  * Capability derivation, ported from the genuine client's nine capability
7
13
  * predicates.
8
14
  *
9
- * THE LOAD-BEARING FACT, stated up front because it is surprising:
15
+ * READ THIS FIRST -- there are two derivation paths and they are not
16
+ * interchangeable:
17
+ *
18
+ * 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.
26
+ * 2. Predicate fallback (`deriveCapabilitiesFromPredicates`), taken for ids
27
+ * with no catalogue entry -- `claude-mythos-5` (absent by product
28
+ * decision D-1), ids from a newer client, and anything that escaped
29
+ * normalization. Those fall through every exclusion list and resolve
30
+ * maximally permissive, `temperature` excepted because its predicate is
31
+ * an allowlist.
32
+ *
33
+ * The two paths agree on every catalogue cell but one; see the demarcated
34
+ * C1 block in `deriveCapabilities`. `test/validation/capability-equivalence
35
+ * .test.ts` pins the agreement cell by cell and pins that one divergence from
36
+ * both sides, so neither path can drift silently.
37
+ *
38
+ * THE LOAD-BEARING FACT about the predicates, stated up front because it is
39
+ * surprising:
10
40
  *
11
41
  * On the first-party provider -- the only provider this package targets --
12
42
  * every one of these nine predicates reduces to a pure function of the
13
- * normalized model id. The catalogue's `capabilities` string array does not
14
- * participate in capability derivation at all.
43
+ * normalized model id.
15
44
  *
16
45
  * Why. Upstream, each predicate has the shape
17
46
  *
@@ -42,13 +71,18 @@ import type { ClaudeCodeCapabilities } from "./contracts.js";
42
71
  *
43
72
  * WARNING TO FUTURE READERS. Two things follow that look like bugs and are not:
44
73
  *
45
- * 1. No membership test was lost. Do NOT "restore" a `JB`-equivalent check
46
- * here. Adding one back cannot change any result, but it reintroduces
47
- * dead branches that cannot be covered or mutation-killed.
48
- * 2. The catalogue `capabilities` arrays are retained in the profile as
49
- * faithful transcribed evidence AND are consumed elsewhere --
50
- * `mid_conv_system`, `lean_prompt` and `fast_mode` are read by later
51
- * work packages. Do NOT delete them because this module ignores them.
74
+ * 1. The individual predicates below still carry no `JB`-equivalent
75
+ * membership test, and must not grow one. Catalogue membership is
76
+ * consulted in exactly one place -- `deriveCapabilitiesFromCatalogue` --
77
+ * so the fallback path stays a pure function of the id and the
78
+ * equivalence between the two paths stays testable. A membership check
79
+ * inside a predicate would be a dead branch on the catalogue path and an
80
+ * unreachable one on the fallback path.
81
+ * 2. The catalogue `capabilities` arrays carry strings this module does not
82
+ * map to a `ClaudeCodeCapabilities` field -- `fast_mode`, `lean_prompt`,
83
+ * `fable_5_mitigations` and `mid_conv_system`. They are faithful
84
+ * transcribed evidence and are consumed elsewhere. Do NOT delete them
85
+ * because this module ignores them.
52
86
  *
53
87
  * `claude-mythos-5` has no catalogue entry by product decision D-1. Upstream
54
88
  * special-cases it by name in `Kw`, `Hke`, `Yte` and `Uot`; this port subsumes
@@ -195,6 +229,12 @@ export function supportsContextManagement(normalizedId: string): boolean {
195
229
  *
196
230
  * Elided: the unconditionally true `ZO` provider gate.
197
231
  * This beta-only gate is intentionally absent from `ClaudeCodeCapabilities`.
232
+ *
233
+ * Stays predicate-derived and takes no part in the catalogue path: no
234
+ * `structured_outputs` string exists in any catalogue entry, so there is
235
+ * nothing to read. Deliberately takes no profile parameter -- upstream does
236
+ * not gate this beta on the catalogue in any modelled version, so making it
237
+ * profile-aware would invent behaviour rather than port it.
198
238
  */
199
239
  export function supportsStructuredOutputs(normalizedId: string): boolean {
200
240
  return !(
@@ -212,8 +252,26 @@ export function supportsStructuredOutputs(normalizedId: string): boolean {
212
252
  * differs from `rejectsDisabledThinking` by one member: that predicate also
213
253
  * excludes `claude-opus-4-8`. Do not merge them.
214
254
  * This beta-only gate is intentionally absent from `ClaudeCodeCapabilities`.
255
+ *
256
+ * Catalogue-first WHEN a profile is supplied and that profile catalogues the
257
+ * id: `mid_conv_system` exists as a catalogue string, and from 2.1.222+ the
258
+ * catalogue is what upstream reads. The switch is behaviour-preserving for
259
+ * 2.1.195, which is the point of the equivalence pinning in
260
+ * `test/validation/capability-equivalence.test.ts`: in the 2.1.195 catalogue
261
+ * exactly `claude-opus-4-8` and `claude-fable-5` carry the string, and those
262
+ * are exactly the two ids the exclusion list below admits.
263
+ *
264
+ * Without a profile -- or for an id the profile does not catalogue, such as
265
+ * `claude-mythos-5` under 2.1.195 -- the predicate remains authoritative.
215
266
  */
216
- export function supportsMidConversationSystem(normalizedId: string): boolean {
267
+ export function supportsMidConversationSystem(
268
+ normalizedId: string,
269
+ profile?: ClaudeCodeProtocolProfile,
270
+ ): boolean {
271
+ const entry = profile?.supportedModels[normalizedId];
272
+ if (entry !== undefined) {
273
+ return entry.capabilities.includes("mid_conv_system");
274
+ }
217
275
  return !(
218
276
  normalizedId.includes("claude-3-") ||
219
277
  normalizedId === "claude-opus-4-0" ||
@@ -278,7 +336,66 @@ export function rejectsDisabledThinking(normalizedId: string): boolean {
278
336
  return true;
279
337
  }
280
338
 
281
- export function deriveCapabilities(
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.
345
+ */
346
+ const CATALOGUE_BACKED_CAPABILITIES = {
347
+ effort: "effort",
348
+ maxEffort: "max_effort",
349
+ xhighEffort: "xhigh_effort",
350
+ adaptiveThinking: "adaptive_thinking",
351
+ contextManagement: "context_management",
352
+ rejectsDisabledThinking: "rejects_disabled_thinking",
353
+ } as const;
354
+
355
+ /**
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.
359
+ *
360
+ * This function applies no exceptions and no id special cases. The one cell
361
+ * where the 2.1.195 catalogue disagrees with the wire is corrected by the
362
+ * caller, so that this mapping stays a faithful reading of the data and the
363
+ * correction stays visible at exactly one site.
364
+ */
365
+ export function deriveCapabilitiesFromCatalogue(
366
+ entry: ClaudeCodeCatalogueEntry,
367
+ normalizedId: string,
368
+ ): ClaudeCodeCapabilities {
369
+ const has = (capability: string): boolean =>
370
+ entry.capabilities.includes(capability);
371
+
372
+ return Object.freeze({
373
+ thinking: supportsThinking(normalizedId),
374
+ adaptiveThinking: has(CATALOGUE_BACKED_CAPABILITIES.adaptiveThinking),
375
+ interleavedThinking: supportsInterleavedThinking(normalizedId),
376
+ effort: has(CATALOGUE_BACKED_CAPABILITIES.effort),
377
+ maxEffort: has(CATALOGUE_BACKED_CAPABILITIES.maxEffort),
378
+ xhighEffort: has(CATALOGUE_BACKED_CAPABILITIES.xhighEffort),
379
+ contextManagement: has(CATALOGUE_BACKED_CAPABILITIES.contextManagement),
380
+ temperature: supportsTemperature(normalizedId),
381
+ rejectsDisabledThinking: has(
382
+ CATALOGUE_BACKED_CAPABILITIES.rejectsDisabledThinking,
383
+ ),
384
+ });
385
+ }
386
+
387
+ /**
388
+ * Fallback for ids with no catalogue entry. Every field comes from its
389
+ * predicate, which is the pre-catalogue behaviour of this module, preserved
390
+ * byte for byte in result: unknown ids fall through every exclusion list and
391
+ * resolve maximally permissive, `temperature` excepted (allowlist polarity).
392
+ *
393
+ * `claude-mythos-5` reaches this path -- it has no catalogue entry by product
394
+ * decision D-1 -- and upstream special-cases it by name in `Kw`, `Hke`, `Yte`
395
+ * and `Uot`. Those clauses are subsumed here by the first-party fallback,
396
+ * which yields an identical result.
397
+ */
398
+ function deriveCapabilitiesFromPredicates(
282
399
  normalizedId: string,
283
400
  ): ClaudeCodeCapabilities {
284
401
  return Object.freeze({
@@ -293,3 +410,44 @@ export function deriveCapabilities(
293
410
  rejectsDisabledThinking: rejectsDisabledThinking(normalizedId),
294
411
  });
295
412
  }
413
+
414
+ export function deriveCapabilities(
415
+ normalizedId: string,
416
+ profile: ClaudeCodeProtocolProfile = CLAUDE_CODE_2_1_195_PROFILE,
417
+ ): ClaudeCodeCapabilities {
418
+ const entry = profile.supportedModels[normalizedId];
419
+ if (entry === undefined) {
420
+ return deriveCapabilitiesFromPredicates(normalizedId);
421
+ }
422
+
423
+ const capabilities = deriveCapabilitiesFromCatalogue(entry, normalizedId);
424
+
425
+ /*
426
+ * DEMARCATED EXCEPTION -- docs/plans/BLOCKERS.md, finding C1.
427
+ *
428
+ * Exactly one cell of the 2.1.195 catalogue disagrees with the binary that
429
+ * shipped it: `claude-opus-4-5` omits `effort` from its `capabilities`
430
+ * array, but 2.1.195 derives capabilities from predicate code and `Kw`
431
+ * (`supportsEffort`) does not exclude `claude-opus-4-5`. The predicate is
432
+ * therefore wire-authoritative for this profile, and the golden fixtures
433
+ * and packed-consumer digests prove `effort: true` is what 2.1.195 sends.
434
+ *
435
+ * Scope. This exception belongs to the 2.1.195 profile only, and the
436
+ * behaviour guard below enforces that mechanically. Upstream 2.1.222+
437
+ * switches derivation to the catalogue, which makes `effort: false`
438
+ * genuine there: for those profiles the catalogue IS the truth, so a
439
+ * profile ported from them does NOT inherit this block. Which profiles are
440
+ * on which side is `profile-behaviors.ts`'s question, not this module's.
441
+ */
442
+ if (
443
+ profileBehaviors(profile).opus45EffortException &&
444
+ normalizedId === "claude-opus-4-5"
445
+ ) {
446
+ return Object.freeze({
447
+ ...capabilities,
448
+ effort: supportsEffort(normalizedId),
449
+ });
450
+ }
451
+
452
+ return capabilities;
453
+ }
package/src/models.ts CHANGED
@@ -37,10 +37,14 @@ export function resolveModel(
37
37
  return Object.freeze({
38
38
  id,
39
39
  wireId,
40
- // The catalogue supplies the family. It does NOT supply capabilities:
41
- // on first party every capability is a pure function of the normalized
42
- // id. See the header of `model-capabilities.ts` for why.
40
+ // The catalogue supplies the family here, and -- since T1.1.2 -- also
41
+ // supplies the six catalogue-backed capabilities. Both now honour THIS
42
+ // profile: `deriveCapabilities` takes the active profile, so a request
43
+ // built against a non-pinned profile derives from that profile's
44
+ // catalogue rather than from 2.1.195's. Ids with no catalogue entry fall
45
+ // back to the ported predicates, which are pure functions of the
46
+ // normalized id. See the header of `model-capabilities.ts`.
43
47
  family: entry?.family ?? modelFamilyOf(id),
44
- capabilities: deriveCapabilities(id),
48
+ capabilities: deriveCapabilities(id, profile),
45
49
  });
46
50
  }
@@ -0,0 +1,114 @@
1
+ // SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+ import type { ClaudeCodeProtocolProfile } from "./contracts.js";
4
+ import { CLAUDE_CODE_2_1_195_PROFILE } from "./profiles/claude-code-2.1.195.js";
5
+
6
+ /**
7
+ * Per-profile behaviour dispatch.
8
+ *
9
+ * Three modules — `thinking.ts`, `fingerprint.ts` and `model-capabilities.ts` —
10
+ * each need to know whether the profile they were handed behaves like upstream
11
+ * 2.1.195 or like upstream 2.1.222+. Each grew its own
12
+ * `profile.id === CLAUDE_CODE_2_1_195_PROFILE.id` comparison as its feature
13
+ * landed, and three copies of one question is three places to forget when a
14
+ * fourth profile arrives. This module is the single place that asks it.
15
+ *
16
+ * The flags are named for the BEHAVIOUR they gate, never for a version. A call
17
+ * site reading `profileBehaviors(profile).billingChainSegments` says what it is
18
+ * deciding; a call site reading `profile.id !== …195….id` says only which
19
+ * client it is not, and leaves the reader to reconstruct why that matters.
20
+ * `test/governance/version-dispatch.test.ts` fails the build if a fourth copy
21
+ * of the comparison appears in `src/`.
22
+ *
23
+ * `ClaudeCodeProfileBehaviors` deliberately stays here rather than moving to
24
+ * `contracts.ts`. It is not part of the wire contract and is not exported from
25
+ * `index.ts`: it is an internal derivation over a profile, and putting it in
26
+ * the contract module would imply consumers can supply one, which they cannot.
27
+ * Behaviour is derived from the profile, never declared alongside it.
28
+ */
29
+ export interface ClaudeCodeProfileBehaviors {
30
+ /**
31
+ * Whether a caller's own `max_tokens` of 4096 or more raises the model's
32
+ * `upperLimit` and lowers its `default` to fit under it.
33
+ *
34
+ * Consumed by `modelOutputTokenLimits` in `thinking.ts`, whose one
35
+ * wire-visible effect is the seed of the default thinking budget.
36
+ */
37
+ readonly requestDerivedTokenCeiling: boolean;
38
+
39
+ /**
40
+ * Whether the billing block may carry the `cc_prev_req` and `cc_prompt_id`
41
+ * conversation-chaining segments.
42
+ *
43
+ * Consumed by `createBillingBlock` in `fingerprint.ts`. False means the
44
+ * segments are dropped silently even when the caller supplies both, which is
45
+ * what a client with no parameter for them does.
46
+ */
47
+ readonly billingChainSegments: boolean;
48
+
49
+ /**
50
+ * Whether `claude-opus-4-5` takes its `effort` capability from the predicate
51
+ * rather than from the catalogue.
52
+ *
53
+ * Consumed by `deriveCapabilities` in `model-capabilities.ts`. See
54
+ * `docs/plans/BLOCKERS.md`, finding C1: this is the one cell where the
55
+ * 2.1.195 catalogue disagrees with the binary that shipped it, and the
56
+ * predicate is wire-authoritative for that profile alone.
57
+ */
58
+ readonly opus45EffortException: boolean;
59
+ }
60
+
61
+ /**
62
+ * Upstream 2.1.195: derivation is predicate-driven, the request builder has no
63
+ * request-derived token ceiling, and the billing block has no parameter for
64
+ * conversation chaining.
65
+ */
66
+ const LEGACY_BEHAVIORS: ClaudeCodeProfileBehaviors = Object.freeze({
67
+ requestDerivedTokenCeiling: false,
68
+ billingChainSegments: false,
69
+ opus45EffortException: true,
70
+ });
71
+
72
+ /**
73
+ * Upstream 2.1.222 and later: derivation is catalogue-first, so the
74
+ * `claude-opus-4-5` exception does not apply, and both of the newer request
75
+ * behaviours are present.
76
+ */
77
+ const MODERN_BEHAVIORS: ClaudeCodeProfileBehaviors = Object.freeze({
78
+ requestDerivedTokenCeiling: true,
79
+ billingChainSegments: true,
80
+ opus45EffortException: false,
81
+ });
82
+
83
+ /**
84
+ * Resolves the behaviour set a profile follows.
85
+ *
86
+ * Pure and total: every profile answers, and the answer depends on nothing but
87
+ * the profile's identity.
88
+ */
89
+ export function profileBehaviors(
90
+ profile: ClaudeCodeProtocolProfile,
91
+ ): ClaudeCodeProfileBehaviors {
92
+ /*
93
+ * ---- Demarcated: the ONLY per-version identity comparison in `src/`. ----
94
+ *
95
+ * The split is 2.1.195 versus 2.1.222+, and it is structural rather than a
96
+ * capability flag: these are differences in what the upstream builder is
97
+ * written to do, not values it reads from a catalogue. 2.1.195 is the one
98
+ * profile ported from the older builder, so it is the one named here.
99
+ *
100
+ * The default is deliberately the MODERN side. A profile ported from a
101
+ * client newer than 2.1.233 inherits the current behaviour and needs no edit
102
+ * here; only a profile ported from a client OLDER than 2.1.222 would, and
103
+ * adding one is a decision that should require touching this module. Written
104
+ * the other way round — naming the modern profiles and defaulting to legacy
105
+ * — every new profile would silently regress to 2.1.195 semantics.
106
+ *
107
+ * `test/governance/version-dispatch.test.ts` asserts this comparison is
108
+ * here, and that it is nowhere else.
109
+ */
110
+ if (profile.id === CLAUDE_CODE_2_1_195_PROFILE.id) {
111
+ return LEGACY_BEHAVIORS;
112
+ }
113
+ return MODERN_BEHAVIORS;
114
+ }
@@ -0,0 +1,200 @@
1
+ // SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+ interface BetaRegistryEntry {
4
+ readonly featureKey: string;
5
+ readonly header: string;
6
+ }
7
+
8
+ /*
9
+ * `deepFreeze` is duplicated from `src/beta-registry.ts` rather than imported.
10
+ * A shared helper module would make the two registries import-coupled for four
11
+ * lines of code, and the 2.1.195 registry already carries its own copy; keeping
12
+ * each registry a leaf keeps a version bump from rippling.
13
+ */
14
+ function deepFreeze<T>(value: T): T {
15
+ if (value !== null && typeof value === "object") {
16
+ for (const key of Reflect.ownKeys(value)) {
17
+ deepFreeze(Reflect.get(value, key));
18
+ }
19
+ Object.freeze(value);
20
+ }
21
+ return value;
22
+ }
23
+
24
+ /**
25
+ * Genuine-client beta registry as of 2.1.233, upstream `Fb_` (byte offset
26
+ * 287505865).
27
+ *
28
+ * KEY ORDER IS LOAD-BEARING. Upstream neither sorts this object nor keeps a
29
+ * canonical list elsewhere, and `src/betas.ts` emits headers in the order its
30
+ * push sites fire, not in registry order -- but the registry order is the
31
+ * transcription of record and the only thing that makes a diff against a future
32
+ * build meaningful. Do not reorder for tidiness, alphabetisation, or grouping.
33
+ */
34
+ export const BETA_REGISTRY_2_1_233 = deepFreeze({
35
+ CLAUDE_CODE: { featureKey: "claude_code", header: "claude-code-20250219" },
36
+ OAUTH_AUTH: { featureKey: "oauth_auth", header: "oauth-2025-04-20" },
37
+ INTERLEAVED_THINKING: {
38
+ featureKey: "interleaved_thinking",
39
+ header: "interleaved-thinking-2025-05-14",
40
+ },
41
+ LONG_CONTEXT: { featureKey: "long_context", header: "context-1m-2025-08-07" },
42
+ CONTEXT_MANAGEMENT: {
43
+ featureKey: "context_management",
44
+ header: "context-management-2025-06-27",
45
+ },
46
+ STRUCTURED_OUTPUTS: {
47
+ featureKey: "structured_outputs",
48
+ header: "structured-outputs-2025-12-15",
49
+ },
50
+ WEB_SEARCH: { featureKey: "web_search", header: "web-search-2025-03-05" },
51
+ ADVANCED_TOOL_USE: {
52
+ featureKey: "tool_search",
53
+ header: "advanced-tool-use-2025-11-20",
54
+ },
55
+ TOOL_SEARCH: {
56
+ featureKey: "tool_search",
57
+ header: "tool-search-tool-2025-10-19",
58
+ },
59
+ EFFORT: { featureKey: "effort", header: "effort-2025-11-24" },
60
+ TASK_BUDGETS: {
61
+ featureKey: "task_budgets",
62
+ header: "task-budgets-2026-03-13",
63
+ },
64
+ PROMPT_CACHING_SCOPE: {
65
+ featureKey: "prompt_caching_scope",
66
+ header: "prompt-caching-scope-2026-01-05",
67
+ },
68
+ PROMPT_CACHING_EVICT: {
69
+ featureKey: "prompt_caching_evict",
70
+ header: "prompt-caching-evict-2026-05-12",
71
+ },
72
+ EXTENDED_CACHE_TTL: {
73
+ featureKey: "extended_cache_ttl",
74
+ header: "extended-cache-ttl-2025-04-11",
75
+ },
76
+ SPEED: { featureKey: "speed", header: "fast-mode-2026-02-01" },
77
+ REDACT_THINKING: {
78
+ featureKey: "redact_thinking",
79
+ header: "redact-thinking-2026-02-12",
80
+ },
81
+ THINKING_TOKEN_COUNT: {
82
+ featureKey: "thinking_token_count",
83
+ header: "thinking-token-count-2026-05-13",
84
+ },
85
+ AFK_MODE: { featureKey: "afk_mode", header: "afk-mode-2026-01-31" },
86
+ ADVISOR_TOOL: {
87
+ featureKey: "advisor_tool",
88
+ header: "advisor-tool-2026-03-01",
89
+ },
90
+ CACHE_DIAGNOSIS: {
91
+ featureKey: "cache_diagnosis",
92
+ header: "cache-diagnosis-2026-04-07",
93
+ },
94
+ CONTEXT_HINT: {
95
+ featureKey: "context_hint",
96
+ header: "context-hint-2026-04-09",
97
+ },
98
+ MCP_SERVERS: {
99
+ featureKey: "mcp_servers",
100
+ header: "mcp-servers-2025-12-04",
101
+ },
102
+ FILES_API: { featureKey: "files_api", header: "files-api-2025-04-14" },
103
+ ENVIRONMENTS: {
104
+ featureKey: "environments",
105
+ header: "environments-2025-11-01",
106
+ },
107
+ CCR_BYOC: { featureKey: "ccr_byoc", header: "ccr-byoc-2025-07-29" },
108
+ MID_CONVERSATION_SYSTEM: {
109
+ featureKey: "mid_conversation_system",
110
+ header: "mid-conversation-system-2026-04-07",
111
+ },
112
+ PER_MESSAGE_EFFORT: {
113
+ featureKey: "per_message_effort",
114
+ header: "per-turn-control-2026-07-01",
115
+ },
116
+ SERVER_SIDE_FALLBACK: {
117
+ featureKey: "server_side_fallback",
118
+ header: "server-side-fallback-2026-06-01",
119
+ },
120
+ SERVER_SIDE_FALLBACK_CATEGORY: {
121
+ featureKey: "server_side_fallback_category",
122
+ header: "server-side-fallback-2026-07-01",
123
+ },
124
+ FALLBACK_CREDIT: {
125
+ featureKey: "fallback_credit",
126
+ header: "fallback-credit-2026-06-01",
127
+ },
128
+ /*
129
+ * NARRATION_SUMMARIES BELONGS HERE AND IS DELIBERATELY ABSENT.
130
+ *
131
+ * 2.1.195 carried `NARRATION_SUMMARIES` (`narration_summaries` /
132
+ * `summarize-connector-text-2026-03-13`, upstream `RPt`) at exactly this
133
+ * position, between `FALLBACK_CREDIT` and `AUTO_MODE_CLASSIFIER`. Upstream
134
+ * removed it in 2.1.222+: the frozen array in the 2.1.233 build has a null
135
+ * slot here and the header string appears nowhere in the binary.
136
+ *
137
+ * This is a REMOVAL, not a transcription gap. Re-adding the entry would
138
+ * reintroduce a header the genuine client no longer sends and would break the
139
+ * wire-compatibility claim on the first request. `src/betas.ts` treats the
140
+ * narration push site as conditional on the entry's presence precisely so
141
+ * this absence needs no special-casing anywhere else. Do not "fix" it.
142
+ */
143
+ AUTO_MODE_CLASSIFIER: {
144
+ featureKey: "auto_mode_classifier",
145
+ header: "auto-mode-classifier-2026-07-16",
146
+ },
147
+ } satisfies Record<string, BetaRegistryEntry>);
148
+
149
+ /*
150
+ * Third-party filtering as of 2.1.233, upstream `X5p` (consumed by `J4u`).
151
+ *
152
+ * Delta against the 2.1.195 set (`Pvi`): `mid-conversation-system-2026-04-07`
153
+ * was ADDED; nothing was removed. Eleven headers, extracted from the 2.1.233
154
+ * build rather than assumed to have carried over.
155
+ */
156
+ export const THIRD_PARTY_ALLOWED_BETAS_2_1_233: ReadonlySet<string> =
157
+ Object.freeze(
158
+ new Set([
159
+ BETA_REGISTRY_2_1_233.CLAUDE_CODE.header,
160
+ BETA_REGISTRY_2_1_233.INTERLEAVED_THINKING.header,
161
+ BETA_REGISTRY_2_1_233.LONG_CONTEXT.header,
162
+ BETA_REGISTRY_2_1_233.CONTEXT_MANAGEMENT.header,
163
+ BETA_REGISTRY_2_1_233.STRUCTURED_OUTPUTS.header,
164
+ BETA_REGISTRY_2_1_233.WEB_SEARCH.header,
165
+ BETA_REGISTRY_2_1_233.EFFORT.header,
166
+ BETA_REGISTRY_2_1_233.TOOL_SEARCH.header,
167
+ BETA_REGISTRY_2_1_233.AFK_MODE.header,
168
+ BETA_REGISTRY_2_1_233.FALLBACK_CREDIT.header,
169
+ BETA_REGISTRY_2_1_233.MID_CONVERSATION_SYSTEM.header,
170
+ ]),
171
+ );
172
+
173
+ /*
174
+ * Provider filtering as of 2.1.233, upstream `pts` (consumed by `y1s`).
175
+ * Extracted, not assumed: the set is byte-identical to the 2.1.195 one (`S2r`).
176
+ *
177
+ * Static reference data with no call site in `src/`, exactly like its 2.1.195
178
+ * counterpart. See `test/governance/provider-scope.test.ts`.
179
+ */
180
+ export const BEDROCK_UNSUPPORTED_BETAS_2_1_233: ReadonlySet<string> =
181
+ Object.freeze(
182
+ new Set([
183
+ BETA_REGISTRY_2_1_233.INTERLEAVED_THINKING.header,
184
+ BETA_REGISTRY_2_1_233.LONG_CONTEXT.header,
185
+ BETA_REGISTRY_2_1_233.TOOL_SEARCH.header,
186
+ ]),
187
+ );
188
+
189
+ /*
190
+ * Count-tokens selection as of 2.1.233, upstream `fts` (consumed by `_1s`).
191
+ * Extracted, not assumed: identical to the 2.1.195 set (`E2r`).
192
+ */
193
+ export const COUNT_TOKENS_BETAS_2_1_233: ReadonlySet<string> = Object.freeze(
194
+ new Set([
195
+ BETA_REGISTRY_2_1_233.CLAUDE_CODE.header,
196
+ BETA_REGISTRY_2_1_233.INTERLEAVED_THINKING.header,
197
+ BETA_REGISTRY_2_1_233.CONTEXT_MANAGEMENT.header,
198
+ BETA_REGISTRY_2_1_233.OAUTH_AUTH.header,
199
+ ]),
200
+ );