@tormentalabs/claude-code-wire-compat 0.1.0-rc.17 → 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 (80) hide show
  1. package/CHANGELOG.md +101 -2
  2. package/README.md +67 -2
  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 +10 -2
  58. package/src/anti-verbosity.ts +219 -0
  59. package/src/beta-registry.ts +140 -0
  60. package/src/betas.ts +302 -0
  61. package/src/build-request.ts +1799 -0
  62. package/src/contracts.ts +1286 -0
  63. package/src/count-tokens.ts +84 -0
  64. package/src/fingerprint.ts +155 -0
  65. package/src/headers.ts +448 -0
  66. package/src/index.ts +63 -0
  67. package/src/metadata.ts +331 -0
  68. package/src/model-capabilities.ts +453 -0
  69. package/src/model-identity.ts +45 -0
  70. package/src/models.ts +50 -0
  71. package/src/profile-behaviors.ts +114 -0
  72. package/src/profiles/beta-registry-2.1.233.ts +200 -0
  73. package/src/profiles/claude-code-2.1.195.ts +168 -0
  74. package/src/profiles/claude-code-2.1.233.ts +240 -0
  75. package/src/redaction.ts +536 -0
  76. package/src/request-body.ts +1931 -0
  77. package/src/sha256.ts +114 -0
  78. package/src/system-prompt.ts +222 -0
  79. package/src/thinking.ts +346 -0
  80. package/src/unicode.ts +24 -0
@@ -0,0 +1,453 @@
1
+ // SPDX-License-Identifier: GPL-3.0-or-later
2
+
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";
10
+
11
+ /*
12
+ * Capability derivation, ported from the genuine client's nine capability
13
+ * predicates.
14
+ *
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:
40
+ *
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
43
+ * normalized model id.
44
+ *
45
+ * Why. Upstream, each predicate has the shape
46
+ *
47
+ * let override = W9(model, cap); // env-var capability override
48
+ * if (override !== undefined) return override;
49
+ * let id = mo(model); // normalize
50
+ * if (<exclusion list>) return false;
51
+ * if (JB(id, cap) || id === "claude-mythos-5") return true;
52
+ * return ZO(l_(model)); // provider fallback
53
+ *
54
+ * and in this package:
55
+ *
56
+ * - `W9` opens with `if (td()) return;` and `td()` is true for first party,
57
+ * so the override always yields `undefined`. It also reads environment
58
+ * variables, which this package is designed never to do. Guard dropped.
59
+ * - `ZO(...)` is `provider is firstParty|anthropicAws|foundry|mantle`. The
60
+ * profile pins `provider: "anthropic"`, so `ZO(...)` is unconditionally
61
+ * true and `return ZO(l_(e))` becomes `return true`.
62
+ * - Therefore the `JB(id, cap)` catalogue-membership test and the
63
+ * `claude-mythos-5` special case can only return `true` from a position
64
+ * where the fallback already returns `true`. They are unobservable.
65
+ * - `l_(e) === "foundry"` branches are unreachable for the same reason.
66
+ * - `ut(CLAUDE_CODE_ALWAYS_ENABLE_EFFORT)` reads an environment variable;
67
+ * dropped by the same design rule as `W9`.
68
+ *
69
+ * Only the leading exclusion list is observable, so that is all these
70
+ * functions contain.
71
+ *
72
+ * WARNING TO FUTURE READERS. Two things follow that look like bugs and are not:
73
+ *
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.
86
+ *
87
+ * `claude-mythos-5` has no catalogue entry by product decision D-1. Upstream
88
+ * special-cases it by name in `Kw`, `Hke`, `Yte` and `Uot`; this port subsumes
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
91
+ * guard for that equivalence.
92
+ *
93
+ * Model ids reaching these functions have already been normalized by
94
+ * `resolveModel` via `normalizeModelId`.
95
+ */
96
+
97
+ /**
98
+ * Upstream `Kw` at byte offset 227719902.
99
+ *
100
+ * Elided: the `W9` override, `CLAUDE_CODE_ALWAYS_ENABLE_EFFORT`, the
101
+ * `JB(id, "effort") || id === "claude-mythos-5"` test, and the `ZO` fallback.
102
+ */
103
+ export function supportsEffort(normalizedId: string): boolean {
104
+ if (
105
+ normalizedId.includes("claude-3-") ||
106
+ normalizedId === "claude-opus-4-0" ||
107
+ normalizedId === "claude-opus-4-1" ||
108
+ normalizedId === "claude-sonnet-4-0" ||
109
+ normalizedId === "claude-sonnet-4-5" ||
110
+ normalizedId === "claude-haiku-4-5"
111
+ ) {
112
+ return false;
113
+ }
114
+ return true;
115
+ }
116
+
117
+ /**
118
+ * Upstream `Hke` at byte offset 227720257.
119
+ *
120
+ * Its exclusion list is `Kw`'s plus `claude-opus-4-5`. Elided: the `W9`
121
+ * override, the `JB(id, "max_effort") || id === "claude-mythos-5"` test, and
122
+ * the `ZO` fallback.
123
+ */
124
+ export function supportsMaxEffort(normalizedId: string): boolean {
125
+ if (
126
+ normalizedId.includes("claude-3-") ||
127
+ normalizedId === "claude-opus-4-0" ||
128
+ normalizedId === "claude-opus-4-1" ||
129
+ normalizedId === "claude-opus-4-5" ||
130
+ normalizedId === "claude-sonnet-4-0" ||
131
+ normalizedId === "claude-sonnet-4-5" ||
132
+ normalizedId === "claude-haiku-4-5"
133
+ ) {
134
+ return false;
135
+ }
136
+ return true;
137
+ }
138
+
139
+ /**
140
+ * Upstream `Yte` at byte offset 227720583.
141
+ *
142
+ * Its exclusion list is `Hke`'s plus `claude-opus-4-6` and
143
+ * `claude-sonnet-4-6`. Elided: the `W9` override, the
144
+ * `JB(id, "xhigh_effort") || id === "claude-mythos-5"` test, and the `ZO`
145
+ * fallback.
146
+ */
147
+ export function supportsXhighEffort(normalizedId: string): boolean {
148
+ if (
149
+ normalizedId.includes("claude-3-") ||
150
+ normalizedId === "claude-opus-4-0" ||
151
+ normalizedId === "claude-opus-4-1" ||
152
+ normalizedId === "claude-opus-4-5" ||
153
+ normalizedId === "claude-opus-4-6" ||
154
+ normalizedId === "claude-sonnet-4-0" ||
155
+ normalizedId === "claude-sonnet-4-5" ||
156
+ normalizedId === "claude-sonnet-4-6" ||
157
+ normalizedId === "claude-haiku-4-5"
158
+ ) {
159
+ return false;
160
+ }
161
+ return true;
162
+ }
163
+
164
+ /**
165
+ * Upstream `Uot` at byte offset 227383245.
166
+ *
167
+ * Its exclusion list is `Kw`'s plus `claude-opus-4-5`, matching `Hke`'s today.
168
+ * The two are transcribed separately on purpose: they are independent upstream
169
+ * predicates that happen to agree at this client version. Elided: the `W9`
170
+ * override, the `JB(id, "adaptive_thinking") || id === "claude-mythos-5"`
171
+ * test, and the `ZO` fallback.
172
+ */
173
+ export function supportsAdaptiveThinking(normalizedId: string): boolean {
174
+ if (
175
+ normalizedId.includes("claude-3-") ||
176
+ normalizedId === "claude-opus-4-0" ||
177
+ normalizedId === "claude-opus-4-1" ||
178
+ normalizedId === "claude-opus-4-5" ||
179
+ normalizedId === "claude-sonnet-4-0" ||
180
+ normalizedId === "claude-sonnet-4-5" ||
181
+ normalizedId === "claude-haiku-4-5"
182
+ ) {
183
+ return false;
184
+ }
185
+ return true;
186
+ }
187
+
188
+ /**
189
+ * Upstream `D9r` at byte offset 227382784.
190
+ *
191
+ * Reduces to the same body as `supportsInterleavedThinking` and
192
+ * `supportsContextManagement` at this client version. Kept separate on
193
+ * purpose: three independent upstream predicates with different provenance
194
+ * that are versioned independently upstream. Do not merge them.
195
+ *
196
+ * Elided: the `W9` override.
197
+ */
198
+ export function supportsThinking(normalizedId: string): boolean {
199
+ return !normalizedId.includes("claude-3-");
200
+ }
201
+
202
+ /**
203
+ * Upstream `QOt` at byte offset 227384610.
204
+ *
205
+ * See the note on `supportsThinking` about the three identical bodies.
206
+ * Elided: the `W9` override, the unreachable `foundry` branch, and the
207
+ * non-`ZO` provider tail (`claude-haiku-4-5` is excluded only for providers
208
+ * outside `ZO`, which cannot occur here -- note this is why haiku 4.5 has
209
+ * interleaved thinking on first party but not on, say, vertex).
210
+ */
211
+ export function supportsInterleavedThinking(normalizedId: string): boolean {
212
+ return !normalizedId.includes("claude-3-");
213
+ }
214
+
215
+ /**
216
+ * Upstream `n0d` at byte offset 227385143.
217
+ *
218
+ * See the note on `supportsThinking` about the three identical bodies.
219
+ * Elided: the unreachable `foundry` branch and the non-`ZO` tail
220
+ * `JB(id, "context_management") || id === "claude-mythos-5"`. `n0d` has no
221
+ * `W9` override upstream.
222
+ */
223
+ export function supportsContextManagement(normalizedId: string): boolean {
224
+ return !normalizedId.includes("claude-3-");
225
+ }
226
+
227
+ /**
228
+ * Upstream `j4e` at byte offset 227385302.
229
+ *
230
+ * Elided: the unconditionally true `ZO` provider gate.
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.
238
+ */
239
+ export function supportsStructuredOutputs(normalizedId: string): boolean {
240
+ return !(
241
+ normalizedId.includes("claude-3-") ||
242
+ normalizedId === "claude-opus-4-0" ||
243
+ normalizedId === "claude-sonnet-4-0"
244
+ );
245
+ }
246
+
247
+ /**
248
+ * Upstream `RCn` at byte offset 227387413.
249
+ *
250
+ * Elided: host-state gates, the `W9` override, the unobservable `JB`/mythos
251
+ * clause, and the unconditionally true `ZO` fallback. This exclusion list
252
+ * differs from `rejectsDisabledThinking` by one member: that predicate also
253
+ * excludes `claude-opus-4-8`. Do not merge them.
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.
266
+ */
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
+ }
275
+ return !(
276
+ normalizedId.includes("claude-3-") ||
277
+ normalizedId === "claude-opus-4-0" ||
278
+ normalizedId === "claude-opus-4-1" ||
279
+ normalizedId === "claude-opus-4-5" ||
280
+ normalizedId === "claude-opus-4-6" ||
281
+ normalizedId === "claude-opus-4-7" ||
282
+ normalizedId === "claude-sonnet-4-0" ||
283
+ normalizedId === "claude-sonnet-4-5" ||
284
+ normalizedId === "claude-sonnet-4-6" ||
285
+ normalizedId === "claude-haiku-4-5"
286
+ );
287
+ }
288
+
289
+ /**
290
+ * Upstream `LCn` at byte offset 227385451.
291
+ *
292
+ * INVERTED POLARITY relative to every other predicate in this module: this
293
+ * list is an ALLOWLIST. Membership means temperature IS supported. Do not
294
+ * refactor it into the shared exclusion-list shape.
295
+ *
296
+ * Elided: the `W9` override.
297
+ */
298
+ export function supportsTemperature(normalizedId: string): boolean {
299
+ return (
300
+ normalizedId.includes("claude-3-") ||
301
+ normalizedId === "claude-opus-4-0" ||
302
+ normalizedId === "claude-opus-4-1" ||
303
+ normalizedId === "claude-opus-4-5" ||
304
+ normalizedId === "claude-opus-4-6" ||
305
+ normalizedId === "claude-sonnet-4-0" ||
306
+ normalizedId === "claude-sonnet-4-5" ||
307
+ normalizedId === "claude-sonnet-4-6" ||
308
+ normalizedId === "claude-haiku-4-5"
309
+ );
310
+ }
311
+
312
+ /**
313
+ * Upstream `U4e` at byte offset 227382881.
314
+ *
315
+ * Its exclusion list is the widest of the five: every catalogue model except
316
+ * `claude-fable-5`. It has no `W9` override and no `claude-mythos-5` clause
317
+ * upstream. Elided: the `JB(id, "rejects_disabled_thinking")` test and the
318
+ * `ZO` fallback.
319
+ */
320
+ export function rejectsDisabledThinking(normalizedId: string): boolean {
321
+ if (
322
+ normalizedId.includes("claude-3-") ||
323
+ normalizedId === "claude-opus-4-0" ||
324
+ normalizedId === "claude-opus-4-1" ||
325
+ normalizedId === "claude-opus-4-5" ||
326
+ normalizedId === "claude-opus-4-6" ||
327
+ normalizedId === "claude-opus-4-7" ||
328
+ normalizedId === "claude-opus-4-8" ||
329
+ normalizedId === "claude-sonnet-4-0" ||
330
+ normalizedId === "claude-sonnet-4-5" ||
331
+ normalizedId === "claude-sonnet-4-6" ||
332
+ normalizedId === "claude-haiku-4-5"
333
+ ) {
334
+ return false;
335
+ }
336
+ return true;
337
+ }
338
+
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(
399
+ normalizedId: string,
400
+ ): ClaudeCodeCapabilities {
401
+ return Object.freeze({
402
+ thinking: supportsThinking(normalizedId),
403
+ adaptiveThinking: supportsAdaptiveThinking(normalizedId),
404
+ interleavedThinking: supportsInterleavedThinking(normalizedId),
405
+ effort: supportsEffort(normalizedId),
406
+ maxEffort: supportsMaxEffort(normalizedId),
407
+ xhighEffort: supportsXhighEffort(normalizedId),
408
+ contextManagement: supportsContextManagement(normalizedId),
409
+ temperature: supportsTemperature(normalizedId),
410
+ rejectsDisabledThinking: rejectsDisabledThinking(normalizedId),
411
+ });
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
+ }
@@ -0,0 +1,45 @@
1
+ // SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+ import type { ClaudeCodeModelFamily } from "./contracts.js";
4
+
5
+ /** Ports upstream `dp` (binary offset 226644497). */
6
+ export function stripModelMarkers(model: string): string {
7
+ return model.replace(/\[(1|2)m\]/gi, "");
8
+ }
9
+
10
+ /** Ports upstream `$_` (binary offset 226639025). */
11
+ export function normalizeModelId(model: string): string {
12
+ model = model.toLowerCase();
13
+ if (model.includes("claude-fable-5")) return "claude-fable-5";
14
+ if (model.includes("claude-mythos-5")) return "claude-mythos-5";
15
+ if (model.includes("claude-opus-4-8")) return "claude-opus-4-8";
16
+ if (model.includes("claude-opus-4-7")) return "claude-opus-4-7";
17
+ if (model.includes("claude-opus-4-6")) return "claude-opus-4-6";
18
+ if (model.includes("claude-opus-4-5")) return "claude-opus-4-5";
19
+ if (model.includes("claude-opus-4-1")) return "claude-opus-4-1";
20
+ if (/claude-opus-4(?!-\d(?!\d))/.test(model)) return "claude-opus-4-0";
21
+ if (model.includes("claude-sonnet-4-6")) return "claude-sonnet-4-6";
22
+ if (model.includes("claude-sonnet-4-5")) return "claude-sonnet-4-5";
23
+ if (/claude-sonnet-4(?!-\d(?!\d))/.test(model)) return "claude-sonnet-4-0";
24
+ if (model.includes("claude-haiku-4-5")) return "claude-haiku-4-5";
25
+ if (model.includes("claude-3-7-sonnet")) return "claude-3-7-sonnet";
26
+ if (model.includes("claude-3-5-sonnet")) return "claude-3-5-sonnet";
27
+ if (model.includes("claude-3-5-haiku")) return "claude-3-5-haiku";
28
+ if (model.includes("claude-3-opus")) return "claude-3-opus";
29
+ if (model.includes("claude-3-sonnet")) return "claude-3-sonnet";
30
+ if (model.includes("claude-3-haiku")) return "claude-3-haiku";
31
+ return model.replace(/-\d{8}$/, "");
32
+ }
33
+
34
+ /**
35
+ * Derives the package-local family classification used only by
36
+ * `RedactedRequestEvidence`; upstream has no corresponding family concept.
37
+ */
38
+ export function modelFamilyOf(normalizedId: string): ClaudeCodeModelFamily {
39
+ if (normalizedId.includes("fable")) return "fable";
40
+ if (normalizedId.includes("mythos")) return "mythos";
41
+ if (normalizedId.includes("haiku")) return "haiku";
42
+ if (normalizedId.includes("sonnet")) return "sonnet";
43
+ if (normalizedId.includes("opus")) return "opus";
44
+ return "unknown";
45
+ }
package/src/models.ts ADDED
@@ -0,0 +1,50 @@
1
+ // SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+ import type {
4
+ ClaudeCodeCapabilities,
5
+ ClaudeCodeModelFamily,
6
+ ClaudeCodeProtocolProfile,
7
+ } from "./contracts.js";
8
+ import { ClaudeCodeWireError } from "./contracts.js";
9
+ import { deriveCapabilities } from "./model-capabilities.js";
10
+ import {
11
+ modelFamilyOf,
12
+ normalizeModelId,
13
+ stripModelMarkers,
14
+ } from "./model-identity.js";
15
+ import { CLAUDE_CODE_2_1_195_PROFILE } from "./profiles/claude-code-2.1.195.js";
16
+
17
+ export interface ResolvedClaudeCodeModel {
18
+ readonly id: string;
19
+ readonly wireId: string;
20
+ readonly family: ClaudeCodeModelFamily;
21
+ readonly capabilities: ClaudeCodeCapabilities;
22
+ }
23
+
24
+ export function resolveModel(
25
+ model: string,
26
+ profile: ClaudeCodeProtocolProfile = CLAUDE_CODE_2_1_195_PROFILE,
27
+ ): ResolvedClaudeCodeModel {
28
+ if (typeof model !== "string" || model.length === 0) {
29
+ throw new ClaudeCodeWireError("INVALID_INPUT", { model });
30
+ }
31
+
32
+ const wireId = stripModelMarkers(model);
33
+ const id = normalizeModelId(model);
34
+ const entry = Object.hasOwn(profile.supportedModels, id)
35
+ ? profile.supportedModels[id]
36
+ : undefined;
37
+ return Object.freeze({
38
+ id,
39
+ wireId,
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`.
47
+ family: entry?.family ?? modelFamilyOf(id),
48
+ capabilities: deriveCapabilities(id, profile),
49
+ });
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
+ }