@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
@@ -50,32 +50,39 @@ export const CLAUDE_CODE_2_1_195_PROFILE: ClaudeCodeProtocolProfile =
50
50
  "claude-3-5-haiku": {
51
51
  family: "haiku",
52
52
  capabilities: [],
53
+ maxOutputTokens: { default: 8192, upper: 8192 },
53
54
  },
54
55
  "claude-haiku-4-5": {
55
56
  family: "haiku",
56
57
  capabilities: ["context_management"],
58
+ maxOutputTokens: { default: 32000, upper: 64000 },
57
59
  },
58
60
  "claude-3-5-sonnet": {
59
61
  family: "sonnet",
60
62
  capabilities: [],
63
+ maxOutputTokens: { default: 8192, upper: 8192 },
61
64
  },
62
65
  "claude-3-7-sonnet": {
63
66
  family: "sonnet",
64
67
  capabilities: [],
68
+ maxOutputTokens: { default: 32000, upper: 64000 },
65
69
  },
66
70
  "claude-sonnet-4-0": {
67
71
  family: "sonnet",
68
72
  context: { window: 200000, supports1mBeta: true },
69
73
  capabilities: ["context_management"],
74
+ maxOutputTokens: { default: 32000, upper: 64000 },
70
75
  },
71
76
  "claude-sonnet-4-5": {
72
77
  family: "sonnet",
73
78
  context: { window: 200000, supports1mBeta: true },
74
79
  capabilities: ["context_management"],
80
+ maxOutputTokens: { default: 32000, upper: 64000 },
75
81
  },
76
82
  "claude-sonnet-4-6": {
77
83
  family: "sonnet",
78
84
  context: { window: 200000, supports1mBeta: true },
85
+ maxOutputTokens: { default: 32000, upper: 128000 },
79
86
  capabilities: [
80
87
  "effort",
81
88
  "max_effort",
@@ -86,18 +93,22 @@ export const CLAUDE_CODE_2_1_195_PROFILE: ClaudeCodeProtocolProfile =
86
93
  "claude-opus-4-0": {
87
94
  family: "opus",
88
95
  capabilities: ["context_management"],
96
+ maxOutputTokens: { default: 32000, upper: 32000 },
89
97
  },
90
98
  "claude-opus-4-1": {
91
99
  family: "opus",
92
100
  capabilities: ["context_management"],
101
+ maxOutputTokens: { default: 32000, upper: 32000 },
93
102
  },
94
103
  "claude-opus-4-5": {
95
104
  family: "opus",
96
105
  capabilities: ["context_management"],
106
+ maxOutputTokens: { default: 32000, upper: 64000 },
97
107
  },
98
108
  "claude-opus-4-6": {
99
109
  family: "opus",
100
110
  context: { window: 200000, supports1mBeta: true },
111
+ maxOutputTokens: { default: 64000, upper: 128000 },
101
112
  capabilities: [
102
113
  "effort",
103
114
  "max_effort",
@@ -109,6 +120,7 @@ export const CLAUDE_CODE_2_1_195_PROFILE: ClaudeCodeProtocolProfile =
109
120
  "claude-opus-4-7": {
110
121
  family: "opus",
111
122
  context: { window: 1e6, native1m: true, supports1mBeta: true },
123
+ maxOutputTokens: { default: 64000, upper: 128000 },
112
124
  capabilities: [
113
125
  "effort",
114
126
  "max_effort",
@@ -122,6 +134,7 @@ export const CLAUDE_CODE_2_1_195_PROFILE: ClaudeCodeProtocolProfile =
122
134
  "claude-opus-4-8": {
123
135
  family: "opus",
124
136
  context: { window: 1e6, native1m: true, supports1mBeta: true },
137
+ maxOutputTokens: { default: 64000, upper: 128000 },
125
138
  capabilities: [
126
139
  "effort",
127
140
  "max_effort",
@@ -137,6 +150,7 @@ export const CLAUDE_CODE_2_1_195_PROFILE: ClaudeCodeProtocolProfile =
137
150
  "claude-fable-5": {
138
151
  family: "fable",
139
152
  context: { window: 1e6, native1m: true, supports1mBeta: true },
153
+ maxOutputTokens: { default: 64000, upper: 128000 },
140
154
  capabilities: [
141
155
  "effort",
142
156
  "max_effort",
@@ -0,0 +1,240 @@
1
+ // SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+ import type { ClaudeCodeProtocolProfile } from "../contracts.js";
4
+ import { COUNT_TOKENS_ENDPOINT } from "../count-tokens.js";
5
+
6
+ /*
7
+ * Protocol profile for genuine client 2.1.233.
8
+ *
9
+ * Provenance: extracted from the official 2.1.233 win32-x64 binary. The
10
+ * catalogue and every scalar below are transcribed from
11
+ * `docs/protocol/versions/claude-code-2.1.233-analysis.md`, which records the
12
+ * byte offsets they came from.
13
+ *
14
+ * `effort_cost_index` -- the only static-catalogue field added between 2.1.222
15
+ * and 2.1.233 -- is deliberately omitted; the decision is recorded in
16
+ * `docs/source-trace.md`.
17
+ *
18
+ * Context modelling follows 2.1.195 exactly wherever the upstream flags are
19
+ * the same. `supports_1m_suffix` is not modelled, so an entry declaring a
20
+ * 200000 window with nothing but that flag carries no `context` object here,
21
+ * as in the 2.1.195 profile.
22
+ */
23
+
24
+ function deepFreeze<T>(value: T): T {
25
+ if (value !== null && typeof value === "object") {
26
+ for (const key of Reflect.ownKeys(value)) {
27
+ deepFreeze(Reflect.get(value, key));
28
+ }
29
+ Object.freeze(value);
30
+ }
31
+ return value;
32
+ }
33
+
34
+ export const CLAUDE_CODE_2_1_233_PROFILE: ClaudeCodeProtocolProfile =
35
+ deepFreeze({
36
+ id: "claude-code-2.1.233-sdk-0.112.1",
37
+ cliVersion: "2.1.233",
38
+ sdkVersion: "0.112.1",
39
+ endpoint: "https://api.anthropic.com/v1/messages?beta=true",
40
+ countTokensEndpoint: COUNT_TOKENS_ENDPOINT,
41
+ entrypoint: "cli",
42
+ userAgent: "claude-cli/2.1.233 (external, cli)",
43
+ buildTime: "2026-08-14T17:21:48Z",
44
+ gitSha: "f8d57569aaf350fe25dc4dfa10cad59db8ea4d45",
45
+ attributionHeaderEnabled: true,
46
+ provider: "anthropic",
47
+ anthropicVersion: "2023-06-01",
48
+ contextHintEnabled: false,
49
+ betaPolicy: {
50
+ oauthAuthenticated: true,
51
+ experimentalBetasEnabled: true,
52
+ oneMillionContextEnabled: true,
53
+ interleavedThinkingEnabled: true,
54
+ interactive: true,
55
+ thinkingSummariesShown: false,
56
+ thinkingTokenCountEnabled: true,
57
+ // Inert for this profile: 2.1.233 removed `narration_summaries` from the
58
+ // beta registry, so there is no entry for this flag to gate.
59
+ narrationSummariesEnabled: false,
60
+ structuredOutputsEnabled: false,
61
+ afkModeEnabled: false,
62
+ cacheDiagnosisEnabled: false,
63
+ },
64
+ /**
65
+ * Verbatim genuine-client catalogue, 17 entries in the catalogue's own
66
+ * order. `defaultEffort` is policy exposed as catalogue data; this package
67
+ * must never apply it to a request.
68
+ */
69
+ supportedModels: {
70
+ "claude-3-5-haiku": {
71
+ family: "haiku",
72
+ capabilities: [],
73
+ maxOutputTokens: { default: 8192, upper: 8192 },
74
+ },
75
+ "claude-haiku-4-5": {
76
+ family: "haiku",
77
+ capabilities: ["context_management"],
78
+ maxOutputTokens: { default: 32000, upper: 64000 },
79
+ },
80
+ "claude-3-5-sonnet": {
81
+ family: "sonnet",
82
+ capabilities: [],
83
+ maxOutputTokens: { default: 8192, upper: 8192 },
84
+ },
85
+ "claude-3-7-sonnet": {
86
+ family: "sonnet",
87
+ capabilities: [],
88
+ maxOutputTokens: { default: 32000, upper: 64000 },
89
+ },
90
+ "claude-sonnet-4-0": {
91
+ family: "sonnet",
92
+ context: { window: 200000, supports1mBeta: true },
93
+ capabilities: ["context_management"],
94
+ maxOutputTokens: { default: 32000, upper: 64000 },
95
+ },
96
+ "claude-sonnet-4-5": {
97
+ family: "sonnet",
98
+ context: { window: 200000, supports1mBeta: true },
99
+ capabilities: ["context_management"],
100
+ maxOutputTokens: { default: 32000, upper: 64000 },
101
+ },
102
+ "claude-sonnet-4-6": {
103
+ family: "sonnet",
104
+ context: { window: 200000, supports1mBeta: true },
105
+ maxOutputTokens: { default: 32000, upper: 128000 },
106
+ capabilities: [
107
+ "effort",
108
+ "max_effort",
109
+ "adaptive_thinking",
110
+ "context_management",
111
+ ],
112
+ },
113
+ // New at 2.1.233. The entry also declares `native_1m_3p` for
114
+ // bedrock/vertex/foundry; this package is anthropic-only, so that flag
115
+ // is not modelled.
116
+ "claude-sonnet-5": {
117
+ family: "sonnet",
118
+ context: { window: 1e6, native1m: true, supports1mBeta: true },
119
+ maxOutputTokens: { default: 64000, upper: 128000 },
120
+ capabilities: [
121
+ "effort",
122
+ "max_effort",
123
+ "xhigh_effort",
124
+ "adaptive_thinking",
125
+ "mid_conv_system",
126
+ "context_management",
127
+ ],
128
+ defaultEffort: "high",
129
+ },
130
+ "claude-opus-4-0": {
131
+ family: "opus",
132
+ capabilities: ["context_management"],
133
+ maxOutputTokens: { default: 32000, upper: 32000 },
134
+ },
135
+ "claude-opus-4-1": {
136
+ family: "opus",
137
+ capabilities: ["context_management"],
138
+ maxOutputTokens: { default: 32000, upper: 32000 },
139
+ },
140
+ // No `effort`, and no exception restoring it. The C1 exception in
141
+ // `model-capabilities.ts` belongs to 2.1.195, whose derivation ran off
142
+ // predicates; 2.1.233 derives from this catalogue, so the omission is
143
+ // the genuine answer here.
144
+ "claude-opus-4-5": {
145
+ family: "opus",
146
+ capabilities: ["context_management"],
147
+ maxOutputTokens: { default: 32000, upper: 64000 },
148
+ },
149
+ // Delta D3 vs 2.1.195: `fast_mode` is gone from this entry.
150
+ "claude-opus-4-6": {
151
+ family: "opus",
152
+ context: { window: 200000, supports1mBeta: true },
153
+ maxOutputTokens: { default: 64000, upper: 128000 },
154
+ capabilities: [
155
+ "effort",
156
+ "max_effort",
157
+ "adaptive_thinking",
158
+ "context_management",
159
+ ],
160
+ },
161
+ // Delta D3 vs 2.1.195: `fast_mode` is gone from this entry too.
162
+ "claude-opus-4-7": {
163
+ family: "opus",
164
+ context: { window: 1e6, native1m: true, supports1mBeta: true },
165
+ maxOutputTokens: { default: 64000, upper: 128000 },
166
+ capabilities: [
167
+ "effort",
168
+ "max_effort",
169
+ "xhigh_effort",
170
+ "adaptive_thinking",
171
+ "context_management",
172
+ ],
173
+ defaultEffort: "xhigh",
174
+ },
175
+ "claude-opus-4-8": {
176
+ family: "opus",
177
+ context: { window: 1e6, native1m: true, supports1mBeta: true },
178
+ maxOutputTokens: { default: 64000, upper: 128000 },
179
+ capabilities: [
180
+ "effort",
181
+ "max_effort",
182
+ "xhigh_effort",
183
+ "adaptive_thinking",
184
+ "mid_conv_system",
185
+ "context_management",
186
+ "fast_mode",
187
+ "lean_prompt",
188
+ ],
189
+ defaultEffort: "high",
190
+ },
191
+ // New at 2.1.233.
192
+ "claude-opus-5": {
193
+ family: "opus",
194
+ context: { window: 1e6, native1m: true, supports1mBeta: true },
195
+ maxOutputTokens: { default: 64000, upper: 128000 },
196
+ capabilities: [
197
+ "effort",
198
+ "max_effort",
199
+ "xhigh_effort",
200
+ "adaptive_thinking",
201
+ "mid_conv_system",
202
+ "context_management",
203
+ "fast_mode",
204
+ "lean_prompt",
205
+ "refusal_fallback",
206
+ "opus_5_prompt_bundle",
207
+ ],
208
+ defaultEffort: "high",
209
+ },
210
+ "claude-fable-5": {
211
+ family: "fable",
212
+ context: { window: 1e6, native1m: true, supports1mBeta: true },
213
+ maxOutputTokens: { default: 64000, upper: 128000 },
214
+ capabilities: [
215
+ "effort",
216
+ "max_effort",
217
+ "xhigh_effort",
218
+ "adaptive_thinking",
219
+ "rejects_disabled_thinking",
220
+ "mid_conv_system",
221
+ "context_management",
222
+ "lean_prompt",
223
+ "fable_5_mitigations",
224
+ "refusal_fallback",
225
+ ],
226
+ defaultEffort: "high",
227
+ },
228
+ // New at 2.1.233, and the notable one: under 2.1.195 `claude-mythos-5`
229
+ // had no catalogue entry at all (product decision D-1) and was
230
+ // recognised only by name in individual predicates. Here it is
231
+ // catalogued with an empty capability array, which is a denial rather
232
+ // than the maximally permissive predicate fallback.
233
+ "claude-mythos-5": {
234
+ family: "mythos",
235
+ context: { window: 1e6, native1m: true, supports1mBeta: true },
236
+ capabilities: [],
237
+ maxOutputTokens: { default: 64000, upper: 128000 },
238
+ },
239
+ },
240
+ });
package/src/redaction.ts CHANGED
@@ -65,6 +65,17 @@ export interface BuildRedactedEvidenceInput {
65
65
  const MAX_INPUT_DEPTH = 100;
66
66
  const MAX_INPUT_SIZE = 1_000_000;
67
67
  const ENDPOINT = "https://api.anthropic.com/v1/messages?beta=true";
68
+ /**
69
+ * The profile ids evidence may claim, spelled out here for the same reason
70
+ * `ENDPOINT` is: this module validates UNTRUSTED evidence records, so it
71
+ * compares them against literals of its own rather than importing the
72
+ * profiles it is checking. Membership stays exact -- an id outside this set
73
+ * is rejected, as a free-form id always was.
74
+ */
75
+ const PINNED_PROFILE_IDS: ReadonlySet<string> = new Set([
76
+ "claude-code-2.1.195-sdk-0.94.0",
77
+ "claude-code-2.1.233-sdk-0.112.1",
78
+ ]);
68
79
  const FORBIDDEN_KEYS = new Set(["__proto__", "prototype", "constructor"]);
69
80
  const SAFE_ERROR_CODES = new Set([
70
81
  "INVALID_INPUT",
@@ -285,7 +296,11 @@ function assertEvidenceSources(value: unknown): void {
285
296
  const logicalHeaders = readOwnValue(value, "logicalHeaders");
286
297
  const betaFeatures = readOwnValue(value, "betaFeatures");
287
298
 
288
- if (profileId !== "claude-code-2.1.195-sdk-0.94.0" || endpoint !== ENDPOINT) {
299
+ if (
300
+ typeof profileId !== "string" ||
301
+ !PINNED_PROFILE_IDS.has(profileId) ||
302
+ endpoint !== ENDPOINT
303
+ ) {
289
304
  throw wireError("INVALID_INPUT");
290
305
  }
291
306
  if (effectiveProfile !== undefined) {
@@ -1413,22 +1413,26 @@ function capabilityBoolean(value: unknown, fallback: boolean): boolean {
1413
1413
  return value;
1414
1414
  }
1415
1415
 
1416
- function modelResolution(value: unknown): ModelResolution {
1416
+ function modelResolution(
1417
+ value: unknown,
1418
+ profile: ClaudeCodeProtocolProfile = CLAUDE_CODE_2_1_195_PROFILE,
1419
+ ): ModelResolution {
1417
1420
  const record = requireRecord(value);
1418
1421
  const capabilityValue = record["capabilities"];
1419
- // A catalogue-shaped capability array is accepted at this boundary, but it
1420
- // never affects derivation: on first party every predicate depends only on
1421
- // the normalized id. See the header of `model-capabilities.ts`. The elements
1422
- // are still validated so that malformed input fails closed.
1422
+ // A catalogue-shaped capability array is accepted at this boundary, but its
1423
+ // elements never drive derivation: the active profile's catalogue does,
1424
+ // falling back to the id predicates. See the header of
1425
+ // `model-capabilities.ts`. The elements are still validated so that
1426
+ // malformed input fails closed.
1423
1427
  if (Array.isArray(capabilityValue)) {
1424
1428
  for (const capability of capabilityValue) {
1425
1429
  if (typeof capability !== "string") fail("INVALID_INPUT");
1426
1430
  }
1427
1431
  }
1428
1432
  const capabilities = Array.isArray(capabilityValue)
1429
- ? deriveCapabilities(String(record["id"]))
1433
+ ? deriveCapabilities(String(record["id"]), profile)
1430
1434
  : requireRecord(capabilityValue);
1431
- const derived = deriveCapabilities(String(record["id"]));
1435
+ const derived = deriveCapabilities(String(record["id"]), profile);
1432
1436
  if (
1433
1437
  (capabilities.thinking !== undefined &&
1434
1438
  typeof capabilities.thinking !== "boolean") ||
@@ -1739,7 +1743,8 @@ export function buildCanonicalBody(
1739
1743
  const input = requireRecord(rawInput);
1740
1744
  assertExactKeys(input, INPUT_KEY_SET);
1741
1745
  requireKeys(input, ["maxTokens", "messages"]);
1742
- const resolvedModel = modelResolution(rawResolvedModel);
1746
+ const effectiveProfile = profile ?? CLAUDE_CODE_2_1_195_PROFILE;
1747
+ const resolvedModel = modelResolution(rawResolvedModel, effectiveProfile);
1743
1748
  if (
1744
1749
  hasOwn(input, "model") &&
1745
1750
  (typeof input["model"] !== "string" ||
@@ -1775,6 +1780,7 @@ export function buildCanonicalBody(
1775
1780
  const maxTokens = clampMaxTokens(
1776
1781
  requirePositiveInteger(input["maxTokens"]),
1777
1782
  resolvedModel.id,
1783
+ effectiveProfile,
1778
1784
  );
1779
1785
  const result: Record<string, unknown> = {
1780
1786
  model: resolvedModel.wireId,
@@ -1828,8 +1834,9 @@ export function buildCanonicalBody(
1828
1834
  thinkingRequest,
1829
1835
  resolvedModel.id,
1830
1836
  resolvedModel.capabilities,
1831
- (profile ?? CLAUDE_CODE_2_1_195_PROFILE).betaPolicy,
1837
+ effectiveProfile.betaPolicy,
1832
1838
  maxTokens,
1839
+ effectiveProfile,
1833
1840
  );
1834
1841
  if (resolved.emitted !== undefined) result["thinking"] = resolved.emitted;
1835
1842
 
package/src/thinking.ts CHANGED
@@ -3,7 +3,10 @@
3
3
  import type {
4
4
  ClaudeCodeBetaPolicy,
5
5
  ClaudeCodeCapabilities,
6
+ ClaudeCodeProtocolProfile,
6
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";
7
10
 
8
11
  /**
9
12
  * Extended-thinking resolution, ported from the genuine client's request
@@ -73,9 +76,21 @@ export interface ResolvedThinking {
73
76
 
74
77
  /**
75
78
  * Per-model output token limits, ported from upstream `Xxe` at byte offset
76
- * 227378240. Keyed on the NORMALISED model id, and deliberately independent of
77
- * the catalogue: `claude-3-opus`, `claude-3-sonnet` and `claude-3-haiku` are
78
- * reachable through the normaliser but have no catalogue entry.
79
+ * 227378240. Keyed on the NORMALISED model id.
80
+ *
81
+ * Resolution order, since Fase 1.2:
82
+ *
83
+ * 1. The pinned 2.1.195 catalogue, when the id has an entry carrying
84
+ * `maxOutputTokens`. That is the single source of truth for every model
85
+ * the profile knows, and `token-limits-equivalence.test.ts` pins the two
86
+ * sources cell by cell.
87
+ * 2. Otherwise the transcribed `Xxe` table below, preserved intact as the
88
+ * demarcated fallback. It is NOT dead code and must not be trimmed to
89
+ * "only the ids the catalogue lacks": `claude-3-opus`, `claude-3-sonnet`
90
+ * and `claude-3-haiku` are reachable through the normaliser with no
91
+ * catalogue entry, `claude-mythos-5` is absent from the catalogue by
92
+ * product decision D-1, and any id from a newer client lands on the
93
+ * final fallback row.
79
94
  *
80
95
  * Both fields are load-bearing. `upperLimit` seeds the thinking budget when the
81
96
  * caller supplies none (upstream `wvi = Xxe(e).upperLimit - 1`); `default` caps
@@ -89,38 +104,96 @@ export interface ResolvedThinking {
89
104
  * override this package cannot observe.
90
105
  * - `bvi(e)` adjusts BOTH fields, but sits behind `_vi()`, which returns a
91
106
  * hard `false`. Dead code upstream.
107
+ *
108
+ * From 2.1.222 onward upstream grew a THIRD adjustment, this one derived from
109
+ * the request rather than from host state, and therefore observable: see
110
+ * `requestedMaxTokens` below.
111
+ *
112
+ * @param requestedMaxTokens
113
+ * The caller's own `max_tokens`, when the call site has it. Modelled for
114
+ * profiles from 2.1.222 onward only; see the demarcated block below.
92
115
  */
93
116
  export function modelOutputTokenLimits(
94
117
  normalizedId: string,
118
+ profile: ClaudeCodeProtocolProfile = CLAUDE_CODE_2_1_195_PROFILE,
119
+ requestedMaxTokens?: number,
95
120
  ): ModelOutputTokenLimits {
96
- if (normalizedId === "claude-fable-5" || normalizedId === "claude-mythos-5") {
97
- return { default: 64000, upperLimit: 128000 };
98
- }
99
- if (normalizedId === "claude-opus-4-8") {
100
- return { default: 64000, upperLimit: 128000 };
101
- }
102
- if (normalizedId === "claude-opus-4-7") {
103
- return { default: 64000, upperLimit: 128000 };
104
- }
105
- if (normalizedId === "claude-sonnet-4-6") {
106
- return { default: 32000, upperLimit: 128000 };
107
- }
108
- if (normalizedId === "claude-opus-4-6") {
109
- return { default: 64000, upperLimit: 128000 };
110
- }
121
+ const resolved = resolveDeclaredLimits(normalizedId, profile);
122
+
123
+ /*
124
+ * ---- Demarcated: request-derived upper bound, upstream 2.1.222+. ----
125
+ *
126
+ * Upstream raises `upperLimit` to the caller's own `max_tokens` and lowers
127
+ * `default` to fit under it:
128
+ *
129
+ * upperLimit = requestedMaxTokens;
130
+ * default = Math.min(default, upperLimit);
131
+ *
132
+ * Verified byte-identical between upstream 2.1.222 and 2.1.233.
133
+ *
134
+ * The gate is STRUCTURAL, not a capability flag: this behaviour exists in
135
+ * upstream 2.1.222+ and 2.1.195 does not have it, so the 195 profile must
136
+ * never see it. Which profiles are on which side is `profile-behaviors.ts`'s
137
+ * question, not this module's.
138
+ *
139
+ * `Number.isSafeInteger` is deliberately stricter than upstream's truthy
140
+ * check. The upstream runtime only ever produces integers in this field, so
141
+ * the two agree on every reachable input; here a NaN or Infinity would
142
+ * propagate straight into `budget_tokens`, which must stay an integer.
143
+ */
111
144
  if (
112
- normalizedId === "claude-opus-4-5" ||
113
- normalizedId === "claude-sonnet-4-0" ||
114
- normalizedId === "claude-sonnet-4-5" ||
115
- normalizedId === "claude-haiku-4-5"
145
+ profileBehaviors(profile).requestDerivedTokenCeiling &&
146
+ requestedMaxTokens !== undefined &&
147
+ Number.isSafeInteger(requestedMaxTokens) &&
148
+ requestedMaxTokens >= 4096
116
149
  ) {
117
- return { default: 32000, upperLimit: 64000 };
150
+ const upperLimit = requestedMaxTokens;
151
+ return { default: Math.min(resolved.default, upperLimit), upperLimit };
118
152
  }
119
- if (
120
- normalizedId === "claude-opus-4-1" ||
121
- normalizedId === "claude-opus-4-0"
122
- ) {
123
- return { default: 32000, upperLimit: 32000 };
153
+
154
+ return resolved;
155
+ }
156
+
157
+ function resolveDeclaredLimits(
158
+ normalizedId: string,
159
+ profile: ClaudeCodeProtocolProfile,
160
+ ): ModelOutputTokenLimits {
161
+ const declared = profile.supportedModels[normalizedId]?.maxOutputTokens;
162
+ if (declared !== undefined) {
163
+ // `upper` is the catalogue's name for what this module calls `upperLimit`;
164
+ // the rename happens here and nowhere else.
165
+ return { default: declared.default, upperLimit: declared.upper };
166
+ }
167
+
168
+ /*
169
+ * ---- Demarcated fallback: the reachable remainder of `Xxe`. ----
170
+ *
171
+ * No catalogue id reaches this point. Every entry of the 2.1.195 catalogue
172
+ * declares `maxOutputTokens`, and `capability-equivalence.test.ts` fails if
173
+ * one stops doing so, which is what keeps the rows below to the ids the
174
+ * catalogue genuinely cannot answer for:
175
+ *
176
+ * - `claude-mythos-5` has no catalogue entry by product decision D-1.
177
+ * - `claude-3-opus`, `claude-3-sonnet` and `claude-3-haiku` are reachable
178
+ * through the normaliser and predate the catalogue.
179
+ *
180
+ * The rows for catalogued ids were deleted rather than kept "just in case":
181
+ * they were unreachable, so they could be neither covered nor
182
+ * mutation-killed, and a second copy of a limit that no longer serves any
183
+ * request is exactly the duplicated table
184
+ * `test/governance/single-source-of-truth.test.ts` exists to prevent.
185
+ *
186
+ * This mirrors upstream 2.1.222, where derivation is catalogue-first and
187
+ * the surviving legacy rows are the `claude-3-*` ones plus a generic tail.
188
+ *
189
+ * If a future profile omits `maxOutputTokens` for some id, that id lands on
190
+ * the generic tail below -- 32000/128000 -- rather than on a stale
191
+ * per-model row. That is deliberate: a wrong-but-loud generic limit is
192
+ * recoverable, a silently stale per-model limit is not. The equivalence
193
+ * guard fires first in any case.
194
+ */
195
+ if (normalizedId === "claude-mythos-5") {
196
+ return { default: 64000, upperLimit: 128000 };
124
197
  }
125
198
  if (normalizedId === "claude-3-opus") {
126
199
  return { default: 4096, upperLimit: 4096 };
@@ -131,15 +204,6 @@ export function modelOutputTokenLimits(
131
204
  if (normalizedId === "claude-3-haiku") {
132
205
  return { default: 4096, upperLimit: 4096 };
133
206
  }
134
- if (
135
- normalizedId === "claude-3-5-sonnet" ||
136
- normalizedId === "claude-3-5-haiku"
137
- ) {
138
- return { default: 8192, upperLimit: 8192 };
139
- }
140
- if (normalizedId === "claude-3-7-sonnet") {
141
- return { default: 32000, upperLimit: 64000 };
142
- }
143
207
  return { default: 32000, upperLimit: 128000 };
144
208
  }
145
209
 
@@ -162,12 +226,22 @@ export function modelOutputTokenLimits(
162
226
  * Upstream uses `||`, not `??`, so a zero override would fall back to the
163
227
  * default. Unreachable here: `max_tokens` is validated as a positive integer
164
228
  * before this runs.
229
+ *
230
+ * `requested` is forwarded as the request-derived bound so that this call site
231
+ * reads the same table upstream reads. It cannot change the result: the
232
+ * override only ever lowers `default` to `requested`, and
233
+ * `min(requested, min(default, requested)) === min(requested, default)`. It is
234
+ * passed for coherence of reading, not for effect.
165
235
  */
166
236
  export function clampMaxTokens(
167
237
  requested: number,
168
238
  normalizedId: string,
239
+ profile: ClaudeCodeProtocolProfile = CLAUDE_CODE_2_1_195_PROFILE,
169
240
  ): number {
170
- return Math.min(requested, modelOutputTokenLimits(normalizedId).default);
241
+ return Math.min(
242
+ requested,
243
+ modelOutputTokenLimits(normalizedId, profile, requested).default,
244
+ );
171
245
  }
172
246
 
173
247
  /**
@@ -216,6 +290,7 @@ export function resolveThinking(
216
290
  capabilities: ClaudeCodeCapabilities,
217
291
  betaPolicy: ClaudeCodeBetaPolicy,
218
292
  maxTokens: number,
293
+ profile: ClaudeCodeProtocolProfile = CLAUDE_CODE_2_1_195_PROFILE,
219
294
  ): ResolvedThinking {
220
295
  // Upstream `nr = n.type !== "disabled" && !CLAUDE_CODE_DISABLE_THINKING`.
221
296
  const requestActive = request !== undefined && request.type !== "disabled";
@@ -236,9 +311,14 @@ export function resolveThinking(
236
311
  // Upstream: `let Tr = wvi(u)` — the model's upper limit minus one —
237
312
  // overridden by the caller's budget when supplied, then clamped by
238
313
  // `Tr = Math.min(Fi - 1, Tr)` where `Fi` is the emitted `max_tokens`.
314
+ //
315
+ // This is the one wire-visible consumer of the request-derived bound: on
316
+ // a 2.1.222+ profile a caller asking for a `max_tokens` above the
317
+ // catalogue's upper limit seeds the default budget from THEIR number
318
+ // minus one, not from the catalogue's.
239
319
  const requested =
240
320
  request.budgetTokens ??
241
- modelOutputTokenLimits(normalizedId).upperLimit - 1;
321
+ modelOutputTokenLimits(normalizedId, profile, maxTokens).upperLimit - 1;
242
322
  emitted = { budget_tokens: Math.min(maxTokens - 1, requested) };
243
323
  emitted["type"] = "enabled";
244
324
  if (display !== undefined) emitted["display"] = display;