@tormentalabs/claude-code-wire-compat 0.1.0-rc.17 → 0.1.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.
@@ -0,0 +1,331 @@
1
+ // SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+ import { ClaudeCodeWireError } from "./contracts.js";
4
+ import type {
5
+ ClaudeCodeMetadataOverrides,
6
+ ClaudeCodeRuntimeIdentity,
7
+ JsonValue,
8
+ } from "./contracts.js";
9
+ import { inspectJsonInputs, validatedJson } from "./request-body.js";
10
+ import { classifySurrogateAt } from "./unicode.js";
11
+
12
+ const MAX_TEXT_LENGTH = 8_192;
13
+ const FORBIDDEN_KEYS = new Set(["__proto__", "prototype", "constructor"]);
14
+ const CORRELATION_KEYS = new Set([
15
+ "user_id",
16
+ "device_id",
17
+ "account_uuid",
18
+ "session_id",
19
+ ]);
20
+ /**
21
+ * The members of the derived `user_id` JSON object.
22
+ *
23
+ * `metadataOverrides.userIdFields` may not carry these keys: the package writes
24
+ * them last so correlation always wins, and a silently overwritten caller value
25
+ * would look honoured while being discarded.
26
+ */
27
+ const IDENTITY_KEYS = new Set(["device_id", "account_uuid", "session_id"]);
28
+
29
+ function hasInvalidUtf16(value: string): boolean {
30
+ for (let index = 0; index < value.length; index += 1) {
31
+ const classification = classifySurrogateAt(value, index);
32
+ if (classification === "loneSurrogate") return true;
33
+ if (classification === "surrogatePair") index += 1;
34
+ }
35
+ return false;
36
+ }
37
+
38
+ function hasControlCharacter(value: string): boolean {
39
+ for (let index = 0; index < value.length; index += 1) {
40
+ const codeUnit = value.charCodeAt(index);
41
+ if (codeUnit <= 0x1f || codeUnit === 0x7f) return true;
42
+ }
43
+ return false;
44
+ }
45
+
46
+ function ownDataValue(value: object, key: string): unknown {
47
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
48
+ return descriptor === undefined || !("value" in descriptor)
49
+ ? undefined
50
+ : descriptor.value;
51
+ }
52
+
53
+ function validateIdentityText(
54
+ value: unknown,
55
+ field: keyof ClaudeCodeRuntimeIdentity,
56
+ ): string {
57
+ if (typeof value !== "string") {
58
+ throw new ClaudeCodeWireError("INVALID_IDENTITY", { field });
59
+ }
60
+ if (hasInvalidUtf16(value)) {
61
+ throw new ClaudeCodeWireError("INVALID_UNICODE", { field });
62
+ }
63
+ if (
64
+ value.length === 0 ||
65
+ value.length > MAX_TEXT_LENGTH ||
66
+ value.trim().length === 0 ||
67
+ hasControlCharacter(value)
68
+ ) {
69
+ throw new ClaudeCodeWireError("INVALID_IDENTITY", { field });
70
+ }
71
+ return value;
72
+ }
73
+
74
+ function validateIdentityObject(identity: unknown): object {
75
+ if (
76
+ typeof identity !== "object" ||
77
+ identity === null ||
78
+ Array.isArray(identity)
79
+ ) {
80
+ throw new ClaudeCodeWireError("INVALID_IDENTITY");
81
+ }
82
+ for (const key of Reflect.ownKeys(identity)) {
83
+ if (typeof key !== "string" || FORBIDDEN_KEYS.has(key)) {
84
+ throw new ClaudeCodeWireError("INVALID_IDENTITY");
85
+ }
86
+ }
87
+ return identity;
88
+ }
89
+
90
+ export function validateRuntimeIdentity(
91
+ identity: unknown,
92
+ ): ClaudeCodeRuntimeIdentity {
93
+ const source = validateIdentityObject(identity);
94
+ const sessionId = validateIdentityText(
95
+ ownDataValue(source, "sessionId"),
96
+ "sessionId",
97
+ );
98
+ const deviceId = validateIdentityText(
99
+ ownDataValue(source, "deviceId"),
100
+ "deviceId",
101
+ );
102
+ const accountUuid = validateIdentityText(
103
+ ownDataValue(source, "accountUuid"),
104
+ "accountUuid",
105
+ );
106
+ const runtime = validateIdentityText(
107
+ ownDataValue(source, "runtime"),
108
+ "runtime",
109
+ );
110
+ const runtimeVersion = validateIdentityText(
111
+ ownDataValue(source, "runtimeVersion"),
112
+ "runtimeVersion",
113
+ );
114
+ const os = validateIdentityText(ownDataValue(source, "os"), "os");
115
+ const arch = validateIdentityText(ownDataValue(source, "arch"), "arch");
116
+
117
+ if (runtime !== "node" && runtime !== "bun" && runtime !== "workerd") {
118
+ throw new ClaudeCodeWireError("INVALID_IDENTITY", { field: "runtime" });
119
+ }
120
+ if (os !== "Windows" && os !== "Linux" && os !== "macOS") {
121
+ throw new ClaudeCodeWireError("INVALID_IDENTITY", { field: "os" });
122
+ }
123
+
124
+ return Object.freeze({
125
+ sessionId,
126
+ deviceId,
127
+ accountUuid,
128
+ runtime,
129
+ runtimeVersion,
130
+ os,
131
+ arch,
132
+ });
133
+ }
134
+
135
+ function validateMetadataKey(key: PropertyKey): string {
136
+ if (typeof key !== "string" || FORBIDDEN_KEYS.has(key)) {
137
+ throw new ClaudeCodeWireError("INVALID_INPUT");
138
+ }
139
+ if (hasInvalidUtf16(key)) {
140
+ throw new ClaudeCodeWireError("INVALID_UNICODE", { field: "metadataKey" });
141
+ }
142
+ if (key.length > MAX_TEXT_LENGTH) {
143
+ throw new ClaudeCodeWireError("INPUT_TOO_LARGE", {
144
+ field: "metadataKey",
145
+ });
146
+ }
147
+ if (key.length === 0 || hasControlCharacter(key)) {
148
+ throw new ClaudeCodeWireError("INVALID_INPUT", { field: "metadataKey" });
149
+ }
150
+ return key;
151
+ }
152
+
153
+ function validateMetadataValue(value: unknown, key: string): JsonValue {
154
+ if (typeof value === "string") {
155
+ if (hasInvalidUtf16(value)) {
156
+ throw new ClaudeCodeWireError("INVALID_UNICODE", { field: key });
157
+ }
158
+ if (value.length > MAX_TEXT_LENGTH) {
159
+ throw new ClaudeCodeWireError("INPUT_TOO_LARGE", { field: key });
160
+ }
161
+ if (hasControlCharacter(value)) {
162
+ throw new ClaudeCodeWireError("INVALID_UNICODE", { field: key });
163
+ }
164
+ return value;
165
+ }
166
+ if (typeof value === "number") {
167
+ if (!Number.isFinite(value)) {
168
+ throw new ClaudeCodeWireError("INVALID_INPUT", { field: key });
169
+ }
170
+ return value;
171
+ }
172
+ if (typeof value === "boolean" || value === null) return value;
173
+ if (typeof value !== "object") {
174
+ throw new ClaudeCodeWireError("INVALID_INPUT", { field: key });
175
+ }
176
+ inspectJsonInputs([value], (nestedString) => {
177
+ if (hasInvalidUtf16(nestedString)) {
178
+ throw new ClaudeCodeWireError("INVALID_UNICODE", { field: key });
179
+ }
180
+ if (nestedString.length > MAX_TEXT_LENGTH) {
181
+ throw new ClaudeCodeWireError("INPUT_TOO_LARGE", { field: key });
182
+ }
183
+ if (hasControlCharacter(nestedString)) {
184
+ throw new ClaudeCodeWireError("INVALID_UNICODE", { field: key });
185
+ }
186
+ });
187
+ return validatedJson(value);
188
+ }
189
+
190
+ function expectedCorrelationValue(
191
+ key: string,
192
+ identity: ClaudeCodeRuntimeIdentity,
193
+ userId: string,
194
+ ): string | undefined {
195
+ switch (key) {
196
+ case "user_id":
197
+ return userId;
198
+ case "device_id":
199
+ return identity.deviceId;
200
+ case "account_uuid":
201
+ return identity.accountUuid;
202
+ case "session_id":
203
+ return identity.sessionId;
204
+ default:
205
+ return undefined;
206
+ }
207
+ }
208
+
209
+ function validateMetadataObject(metadata: unknown): object {
210
+ if (
211
+ typeof metadata !== "object" ||
212
+ metadata === null ||
213
+ Array.isArray(metadata)
214
+ ) {
215
+ throw new ClaudeCodeWireError("INVALID_INPUT", { field: "metadata" });
216
+ }
217
+ return metadata;
218
+ }
219
+
220
+ /**
221
+ * Validates a package-extension `user_id` replacement.
222
+ *
223
+ * PACKAGE EXTENSION, not observed Claude Code behaviour. The value is opaque to
224
+ * this package, so it is checked only for wire-safety, never for correlation.
225
+ */
226
+ function validateOverrideUserId(value: unknown): string {
227
+ if (typeof value !== "string") {
228
+ throw new ClaudeCodeWireError("INVALID_INPUT", { field: "userId" });
229
+ }
230
+ if (hasInvalidUtf16(value)) {
231
+ throw new ClaudeCodeWireError("INVALID_UNICODE", { field: "userId" });
232
+ }
233
+ if (value.length > MAX_TEXT_LENGTH) {
234
+ throw new ClaudeCodeWireError("INPUT_TOO_LARGE", { field: "userId" });
235
+ }
236
+ if (hasControlCharacter(value)) {
237
+ throw new ClaudeCodeWireError("INVALID_UNICODE", { field: "userId" });
238
+ }
239
+ if (value.length === 0 || value.trim().length === 0) {
240
+ throw new ClaudeCodeWireError("INVALID_INPUT", { field: "userId" });
241
+ }
242
+ return value;
243
+ }
244
+
245
+ /** Validates the package-extension members merged into the `user_id` object. */
246
+ function validateUserIdFields(value: unknown): [string, JsonValue][] {
247
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
248
+ throw new ClaudeCodeWireError("INVALID_INPUT", { field: "userIdFields" });
249
+ }
250
+ const entries: [string, JsonValue][] = [];
251
+ for (const rawKey of Reflect.ownKeys(value)) {
252
+ const key = validateMetadataKey(rawKey);
253
+ if (IDENTITY_KEYS.has(key)) {
254
+ throw new ClaudeCodeWireError("INVALID_INPUT", { field: key });
255
+ }
256
+ entries.push([key, validateMetadataValue(ownDataValue(value, key), key)]);
257
+ }
258
+ return entries;
259
+ }
260
+
261
+ /**
262
+ * Resolves the emitted `user_id`, applying the package-extension seam.
263
+ *
264
+ * With no overrides this is the derived correlation triple, byte for byte as
265
+ * before the seam existed. The two override members are mutually exclusive
266
+ * because they express opposite intents: one abandons the derived value, the
267
+ * other extends it.
268
+ */
269
+ function resolveUserId(
270
+ identity: ClaudeCodeRuntimeIdentity,
271
+ overrides: unknown,
272
+ ): string {
273
+ const correlation = {
274
+ device_id: identity.deviceId,
275
+ account_uuid: identity.accountUuid,
276
+ session_id: identity.sessionId,
277
+ };
278
+ if (overrides === undefined) return JSON.stringify(correlation);
279
+ if (
280
+ typeof overrides !== "object" ||
281
+ overrides === null ||
282
+ Array.isArray(overrides)
283
+ ) {
284
+ throw new ClaudeCodeWireError("INVALID_INPUT", {
285
+ field: "metadataOverrides",
286
+ });
287
+ }
288
+ const hasUserId = Object.hasOwn(overrides, "userId");
289
+ const hasUserIdFields = Object.hasOwn(overrides, "userIdFields");
290
+ if (hasUserId && hasUserIdFields) {
291
+ throw new ClaudeCodeWireError("INVALID_INPUT", {
292
+ field: "metadataOverrides",
293
+ });
294
+ }
295
+ if (hasUserId) {
296
+ return validateOverrideUserId(ownDataValue(overrides, "userId"));
297
+ }
298
+ if (!hasUserIdFields) return JSON.stringify(correlation);
299
+ const fields = validateUserIdFields(ownDataValue(overrides, "userIdFields"));
300
+ // Caller members first, correlation last: the triple always wins, which
301
+ // mirrors how the consumer composes the same object.
302
+ return JSON.stringify({ ...Object.fromEntries(fields), ...correlation });
303
+ }
304
+
305
+ export function buildCorrelatedMetadata(
306
+ identity: ClaudeCodeRuntimeIdentity,
307
+ suppliedMetadata?: Readonly<Record<string, JsonValue>>,
308
+ overrides?: ClaudeCodeMetadataOverrides,
309
+ ): Readonly<Record<string, JsonValue>> {
310
+ const validatedIdentity = validateRuntimeIdentity(identity);
311
+ const userId = resolveUserId(validatedIdentity, overrides);
312
+ const entries: [string, JsonValue][] = [["user_id", userId]];
313
+
314
+ if (suppliedMetadata === undefined) return Object.freeze({ user_id: userId });
315
+ const metadataObject = validateMetadataObject(suppliedMetadata);
316
+
317
+ for (const rawKey of Reflect.ownKeys(metadataObject)) {
318
+ const key = validateMetadataKey(rawKey);
319
+ const value = validateMetadataValue(ownDataValue(metadataObject, key), key);
320
+ if (CORRELATION_KEYS.has(key)) {
321
+ const expected = expectedCorrelationValue(key, validatedIdentity, userId);
322
+ if (value !== expected) {
323
+ throw new ClaudeCodeWireError("INVALID_INPUT", { field: key });
324
+ }
325
+ if (key === "user_id") continue;
326
+ }
327
+ entries.push([key, value]);
328
+ }
329
+
330
+ return Object.freeze(Object.fromEntries(entries));
331
+ }
@@ -0,0 +1,295 @@
1
+ // SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+ import type { ClaudeCodeCapabilities } from "./contracts.js";
4
+
5
+ /*
6
+ * Capability derivation, ported from the genuine client's nine capability
7
+ * predicates.
8
+ *
9
+ * THE LOAD-BEARING FACT, stated up front because it is surprising:
10
+ *
11
+ * On the first-party provider -- the only provider this package targets --
12
+ * 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.
15
+ *
16
+ * Why. Upstream, each predicate has the shape
17
+ *
18
+ * let override = W9(model, cap); // env-var capability override
19
+ * if (override !== undefined) return override;
20
+ * let id = mo(model); // normalize
21
+ * if (<exclusion list>) return false;
22
+ * if (JB(id, cap) || id === "claude-mythos-5") return true;
23
+ * return ZO(l_(model)); // provider fallback
24
+ *
25
+ * and in this package:
26
+ *
27
+ * - `W9` opens with `if (td()) return;` and `td()` is true for first party,
28
+ * so the override always yields `undefined`. It also reads environment
29
+ * variables, which this package is designed never to do. Guard dropped.
30
+ * - `ZO(...)` is `provider is firstParty|anthropicAws|foundry|mantle`. The
31
+ * profile pins `provider: "anthropic"`, so `ZO(...)` is unconditionally
32
+ * true and `return ZO(l_(e))` becomes `return true`.
33
+ * - Therefore the `JB(id, cap)` catalogue-membership test and the
34
+ * `claude-mythos-5` special case can only return `true` from a position
35
+ * where the fallback already returns `true`. They are unobservable.
36
+ * - `l_(e) === "foundry"` branches are unreachable for the same reason.
37
+ * - `ut(CLAUDE_CODE_ALWAYS_ENABLE_EFFORT)` reads an environment variable;
38
+ * dropped by the same design rule as `W9`.
39
+ *
40
+ * Only the leading exclusion list is observable, so that is all these
41
+ * functions contain.
42
+ *
43
+ * WARNING TO FUTURE READERS. Two things follow that look like bugs and are not:
44
+ *
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.
52
+ *
53
+ * `claude-mythos-5` has no catalogue entry by product decision D-1. Upstream
54
+ * special-cases it by name in `Kw`, `Hke`, `Yte` and `Uot`; this port subsumes
55
+ * those clauses into the first-party fallback, which yields an identical
56
+ * result. The explicit D-1 test asserting its full nine-boolean row is the
57
+ * guard for that equivalence.
58
+ *
59
+ * Model ids reaching these functions have already been normalized by
60
+ * `resolveModel` via `normalizeModelId`.
61
+ */
62
+
63
+ /**
64
+ * Upstream `Kw` at byte offset 227719902.
65
+ *
66
+ * Elided: the `W9` override, `CLAUDE_CODE_ALWAYS_ENABLE_EFFORT`, the
67
+ * `JB(id, "effort") || id === "claude-mythos-5"` test, and the `ZO` fallback.
68
+ */
69
+ export function supportsEffort(normalizedId: string): boolean {
70
+ if (
71
+ normalizedId.includes("claude-3-") ||
72
+ normalizedId === "claude-opus-4-0" ||
73
+ normalizedId === "claude-opus-4-1" ||
74
+ normalizedId === "claude-sonnet-4-0" ||
75
+ normalizedId === "claude-sonnet-4-5" ||
76
+ normalizedId === "claude-haiku-4-5"
77
+ ) {
78
+ return false;
79
+ }
80
+ return true;
81
+ }
82
+
83
+ /**
84
+ * Upstream `Hke` at byte offset 227720257.
85
+ *
86
+ * Its exclusion list is `Kw`'s plus `claude-opus-4-5`. Elided: the `W9`
87
+ * override, the `JB(id, "max_effort") || id === "claude-mythos-5"` test, and
88
+ * the `ZO` fallback.
89
+ */
90
+ export function supportsMaxEffort(normalizedId: string): boolean {
91
+ if (
92
+ normalizedId.includes("claude-3-") ||
93
+ normalizedId === "claude-opus-4-0" ||
94
+ normalizedId === "claude-opus-4-1" ||
95
+ normalizedId === "claude-opus-4-5" ||
96
+ normalizedId === "claude-sonnet-4-0" ||
97
+ normalizedId === "claude-sonnet-4-5" ||
98
+ normalizedId === "claude-haiku-4-5"
99
+ ) {
100
+ return false;
101
+ }
102
+ return true;
103
+ }
104
+
105
+ /**
106
+ * Upstream `Yte` at byte offset 227720583.
107
+ *
108
+ * Its exclusion list is `Hke`'s plus `claude-opus-4-6` and
109
+ * `claude-sonnet-4-6`. Elided: the `W9` override, the
110
+ * `JB(id, "xhigh_effort") || id === "claude-mythos-5"` test, and the `ZO`
111
+ * fallback.
112
+ */
113
+ export function supportsXhighEffort(normalizedId: string): boolean {
114
+ if (
115
+ normalizedId.includes("claude-3-") ||
116
+ normalizedId === "claude-opus-4-0" ||
117
+ normalizedId === "claude-opus-4-1" ||
118
+ normalizedId === "claude-opus-4-5" ||
119
+ normalizedId === "claude-opus-4-6" ||
120
+ normalizedId === "claude-sonnet-4-0" ||
121
+ normalizedId === "claude-sonnet-4-5" ||
122
+ normalizedId === "claude-sonnet-4-6" ||
123
+ normalizedId === "claude-haiku-4-5"
124
+ ) {
125
+ return false;
126
+ }
127
+ return true;
128
+ }
129
+
130
+ /**
131
+ * Upstream `Uot` at byte offset 227383245.
132
+ *
133
+ * Its exclusion list is `Kw`'s plus `claude-opus-4-5`, matching `Hke`'s today.
134
+ * The two are transcribed separately on purpose: they are independent upstream
135
+ * predicates that happen to agree at this client version. Elided: the `W9`
136
+ * override, the `JB(id, "adaptive_thinking") || id === "claude-mythos-5"`
137
+ * test, and the `ZO` fallback.
138
+ */
139
+ export function supportsAdaptiveThinking(normalizedId: string): boolean {
140
+ if (
141
+ normalizedId.includes("claude-3-") ||
142
+ normalizedId === "claude-opus-4-0" ||
143
+ normalizedId === "claude-opus-4-1" ||
144
+ normalizedId === "claude-opus-4-5" ||
145
+ normalizedId === "claude-sonnet-4-0" ||
146
+ normalizedId === "claude-sonnet-4-5" ||
147
+ normalizedId === "claude-haiku-4-5"
148
+ ) {
149
+ return false;
150
+ }
151
+ return true;
152
+ }
153
+
154
+ /**
155
+ * Upstream `D9r` at byte offset 227382784.
156
+ *
157
+ * Reduces to the same body as `supportsInterleavedThinking` and
158
+ * `supportsContextManagement` at this client version. Kept separate on
159
+ * purpose: three independent upstream predicates with different provenance
160
+ * that are versioned independently upstream. Do not merge them.
161
+ *
162
+ * Elided: the `W9` override.
163
+ */
164
+ export function supportsThinking(normalizedId: string): boolean {
165
+ return !normalizedId.includes("claude-3-");
166
+ }
167
+
168
+ /**
169
+ * Upstream `QOt` at byte offset 227384610.
170
+ *
171
+ * See the note on `supportsThinking` about the three identical bodies.
172
+ * Elided: the `W9` override, the unreachable `foundry` branch, and the
173
+ * non-`ZO` provider tail (`claude-haiku-4-5` is excluded only for providers
174
+ * outside `ZO`, which cannot occur here -- note this is why haiku 4.5 has
175
+ * interleaved thinking on first party but not on, say, vertex).
176
+ */
177
+ export function supportsInterleavedThinking(normalizedId: string): boolean {
178
+ return !normalizedId.includes("claude-3-");
179
+ }
180
+
181
+ /**
182
+ * Upstream `n0d` at byte offset 227385143.
183
+ *
184
+ * See the note on `supportsThinking` about the three identical bodies.
185
+ * Elided: the unreachable `foundry` branch and the non-`ZO` tail
186
+ * `JB(id, "context_management") || id === "claude-mythos-5"`. `n0d` has no
187
+ * `W9` override upstream.
188
+ */
189
+ export function supportsContextManagement(normalizedId: string): boolean {
190
+ return !normalizedId.includes("claude-3-");
191
+ }
192
+
193
+ /**
194
+ * Upstream `j4e` at byte offset 227385302.
195
+ *
196
+ * Elided: the unconditionally true `ZO` provider gate.
197
+ * This beta-only gate is intentionally absent from `ClaudeCodeCapabilities`.
198
+ */
199
+ export function supportsStructuredOutputs(normalizedId: string): boolean {
200
+ return !(
201
+ normalizedId.includes("claude-3-") ||
202
+ normalizedId === "claude-opus-4-0" ||
203
+ normalizedId === "claude-sonnet-4-0"
204
+ );
205
+ }
206
+
207
+ /**
208
+ * Upstream `RCn` at byte offset 227387413.
209
+ *
210
+ * Elided: host-state gates, the `W9` override, the unobservable `JB`/mythos
211
+ * clause, and the unconditionally true `ZO` fallback. This exclusion list
212
+ * differs from `rejectsDisabledThinking` by one member: that predicate also
213
+ * excludes `claude-opus-4-8`. Do not merge them.
214
+ * This beta-only gate is intentionally absent from `ClaudeCodeCapabilities`.
215
+ */
216
+ export function supportsMidConversationSystem(normalizedId: string): boolean {
217
+ return !(
218
+ normalizedId.includes("claude-3-") ||
219
+ normalizedId === "claude-opus-4-0" ||
220
+ normalizedId === "claude-opus-4-1" ||
221
+ normalizedId === "claude-opus-4-5" ||
222
+ normalizedId === "claude-opus-4-6" ||
223
+ normalizedId === "claude-opus-4-7" ||
224
+ normalizedId === "claude-sonnet-4-0" ||
225
+ normalizedId === "claude-sonnet-4-5" ||
226
+ normalizedId === "claude-sonnet-4-6" ||
227
+ normalizedId === "claude-haiku-4-5"
228
+ );
229
+ }
230
+
231
+ /**
232
+ * Upstream `LCn` at byte offset 227385451.
233
+ *
234
+ * INVERTED POLARITY relative to every other predicate in this module: this
235
+ * list is an ALLOWLIST. Membership means temperature IS supported. Do not
236
+ * refactor it into the shared exclusion-list shape.
237
+ *
238
+ * Elided: the `W9` override.
239
+ */
240
+ export function supportsTemperature(normalizedId: string): boolean {
241
+ return (
242
+ normalizedId.includes("claude-3-") ||
243
+ normalizedId === "claude-opus-4-0" ||
244
+ normalizedId === "claude-opus-4-1" ||
245
+ normalizedId === "claude-opus-4-5" ||
246
+ normalizedId === "claude-opus-4-6" ||
247
+ normalizedId === "claude-sonnet-4-0" ||
248
+ normalizedId === "claude-sonnet-4-5" ||
249
+ normalizedId === "claude-sonnet-4-6" ||
250
+ normalizedId === "claude-haiku-4-5"
251
+ );
252
+ }
253
+
254
+ /**
255
+ * Upstream `U4e` at byte offset 227382881.
256
+ *
257
+ * Its exclusion list is the widest of the five: every catalogue model except
258
+ * `claude-fable-5`. It has no `W9` override and no `claude-mythos-5` clause
259
+ * upstream. Elided: the `JB(id, "rejects_disabled_thinking")` test and the
260
+ * `ZO` fallback.
261
+ */
262
+ export function rejectsDisabledThinking(normalizedId: string): boolean {
263
+ if (
264
+ normalizedId.includes("claude-3-") ||
265
+ normalizedId === "claude-opus-4-0" ||
266
+ normalizedId === "claude-opus-4-1" ||
267
+ normalizedId === "claude-opus-4-5" ||
268
+ normalizedId === "claude-opus-4-6" ||
269
+ normalizedId === "claude-opus-4-7" ||
270
+ normalizedId === "claude-opus-4-8" ||
271
+ normalizedId === "claude-sonnet-4-0" ||
272
+ normalizedId === "claude-sonnet-4-5" ||
273
+ normalizedId === "claude-sonnet-4-6" ||
274
+ normalizedId === "claude-haiku-4-5"
275
+ ) {
276
+ return false;
277
+ }
278
+ return true;
279
+ }
280
+
281
+ export function deriveCapabilities(
282
+ normalizedId: string,
283
+ ): ClaudeCodeCapabilities {
284
+ return Object.freeze({
285
+ thinking: supportsThinking(normalizedId),
286
+ adaptiveThinking: supportsAdaptiveThinking(normalizedId),
287
+ interleavedThinking: supportsInterleavedThinking(normalizedId),
288
+ effort: supportsEffort(normalizedId),
289
+ maxEffort: supportsMaxEffort(normalizedId),
290
+ xhighEffort: supportsXhighEffort(normalizedId),
291
+ contextManagement: supportsContextManagement(normalizedId),
292
+ temperature: supportsTemperature(normalizedId),
293
+ rejectsDisabledThinking: rejectsDisabledThinking(normalizedId),
294
+ });
295
+ }
@@ -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
+ }