@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.
package/src/sha256.ts ADDED
@@ -0,0 +1,114 @@
1
+ // SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+ const SHA256_INITIAL = new Uint32Array([
4
+ 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c,
5
+ 0x1f83d9ab, 0x5be0cd19,
6
+ ]);
7
+ const SHA256_ROUNDS = new Uint32Array([
8
+ 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1,
9
+ 0x923f82a4, 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3,
10
+ 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786,
11
+ 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
12
+ 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147,
13
+ 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13,
14
+ 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b,
15
+ 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
16
+ 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a,
17
+ 0x5b9cca4f, 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208,
18
+ 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
19
+ ]);
20
+
21
+ function rotateRight(value: number, count: number): number {
22
+ return (value >>> count) | (value << (32 - count));
23
+ }
24
+
25
+ function wordAt(words: Uint32Array, index: number): number {
26
+ // The fallback is unreachable at runtime because every call site indexes
27
+ // within the allocated array. It exists solely to satisfy
28
+ // noUncheckedIndexedAccess and is therefore this file's sole uncovered
29
+ // branch, intentionally not covered by a test.
30
+ return words[index] ?? 0;
31
+ }
32
+
33
+ /**
34
+ * Computes a digest only so the synchronous parser can verify the integrity of
35
+ * evidence produced by this package. The asynchronous builder path uses the
36
+ * injected Web Crypto implementations in `src/fingerprint.ts` and
37
+ * `src/redaction.ts` and MUST continue to do so. This checks non-secret,
38
+ * self-produced data; it is not a MAC and must not inform security decisions.
39
+ */
40
+ export function sha256Hex(value: string): string {
41
+ const source = new TextEncoder().encode(value);
42
+ const paddedLength = Math.ceil((source.length + 9) / 64) * 64;
43
+ const padded = new Uint8Array(paddedLength);
44
+ padded.set(source);
45
+ padded[source.length] = 0x80;
46
+ const bitLength = source.length * 8;
47
+ const view = new DataView(padded.buffer);
48
+ view.setUint32(paddedLength - 8, Math.floor(bitLength / 0x1_0000_0000));
49
+ view.setUint32(paddedLength - 4, bitLength >>> 0);
50
+ const state = new Uint32Array(SHA256_INITIAL);
51
+ const words = new Uint32Array(64);
52
+
53
+ for (let offset = 0; offset < paddedLength; offset += 64) {
54
+ for (let index = 0; index < 16; index += 1) {
55
+ words[index] = view.getUint32(offset + index * 4);
56
+ }
57
+ for (let index = 16; index < 64; index += 1) {
58
+ const previous15 = wordAt(words, index - 15);
59
+ const previous2 = wordAt(words, index - 2);
60
+ const sigma0 =
61
+ rotateRight(previous15, 7) ^
62
+ rotateRight(previous15, 18) ^
63
+ (previous15 >>> 3);
64
+ const sigma1 =
65
+ rotateRight(previous2, 17) ^
66
+ rotateRight(previous2, 19) ^
67
+ (previous2 >>> 10);
68
+ words[index] =
69
+ wordAt(words, index - 16) + sigma0 + wordAt(words, index - 7) + sigma1;
70
+ }
71
+
72
+ let a = wordAt(state, 0);
73
+ let b = wordAt(state, 1);
74
+ let c = wordAt(state, 2);
75
+ let d = wordAt(state, 3);
76
+ let e = wordAt(state, 4);
77
+ let f = wordAt(state, 5);
78
+ let g = wordAt(state, 6);
79
+ let h = wordAt(state, 7);
80
+ for (let index = 0; index < 64; index += 1) {
81
+ const sum1 = rotateRight(e, 6) ^ rotateRight(e, 11) ^ rotateRight(e, 25);
82
+ const choice = (e & f) ^ (~e & g);
83
+ const temporary1 =
84
+ (h +
85
+ sum1 +
86
+ choice +
87
+ wordAt(SHA256_ROUNDS, index) +
88
+ wordAt(words, index)) >>>
89
+ 0;
90
+ const sum0 = rotateRight(a, 2) ^ rotateRight(a, 13) ^ rotateRight(a, 22);
91
+ const majority = (a & b) ^ (a & c) ^ (b & c);
92
+ const temporary2 = (sum0 + majority) >>> 0;
93
+ h = g;
94
+ g = f;
95
+ f = e;
96
+ e = (d + temporary1) >>> 0;
97
+ d = c;
98
+ c = b;
99
+ b = a;
100
+ a = (temporary1 + temporary2) >>> 0;
101
+ }
102
+ state[0] = wordAt(state, 0) + a;
103
+ state[1] = wordAt(state, 1) + b;
104
+ state[2] = wordAt(state, 2) + c;
105
+ state[3] = wordAt(state, 3) + d;
106
+ state[4] = wordAt(state, 4) + e;
107
+ state[5] = wordAt(state, 5) + f;
108
+ state[6] = wordAt(state, 6) + g;
109
+ state[7] = wordAt(state, 7) + h;
110
+ }
111
+ return Array.from(state, (word) => word.toString(16).padStart(8, "0")).join(
112
+ "",
113
+ );
114
+ }
@@ -0,0 +1,222 @@
1
+ // SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+ import type {
4
+ ClaudeCodeRuntimeIdentity,
5
+ SystemInput,
6
+ TextBlock,
7
+ } from "./contracts.js";
8
+ import { ClaudeCodeWireError } from "./contracts.js";
9
+ import { classifySurrogateAt } from "./unicode.js";
10
+
11
+ /**
12
+ * The pinned identity text, byte-exact.
13
+ *
14
+ * It is exported because it is the byte-exact probe the parser uses to CONFIRM
15
+ * the canonical system prefix. A caller block equal to it is dropped by
16
+ * `buildCanonicalSystem` — unconditionally, even when `suppressIdentityBlock`
17
+ * removed the canonical one — so it appears at most once in a built body.
18
+ *
19
+ * The parser no longer INFERS the prefix length from this text's position: the
20
+ * root seams `suppressBillingBlock` and `suppressIdentityBlock` are recorded in
21
+ * `evidence.billingBlockSuppressed` / `evidence.identityBlockSuppressed`, which
22
+ * state which canonical blocks were emitted. This text is what the parser then
23
+ * checks the identity slot against, so evidence is verified structurally rather
24
+ * than trusted.
25
+ */
26
+ export const IDENTITY_TEXT =
27
+ "You are Claude Code, Anthropic's official CLI for Claude.";
28
+ const MAX_INPUT_DEPTH = 64;
29
+ const MAX_INPUT_SIZE = 1_000_000;
30
+ type UnknownRecord = Readonly<Record<PropertyKey, unknown>>;
31
+
32
+ function fail(
33
+ code:
34
+ | "INVALID_INPUT"
35
+ | "INVALID_UNICODE"
36
+ | "INPUT_TOO_DEEP"
37
+ | "INPUT_TOO_LARGE"
38
+ | "CYCLIC_INPUT",
39
+ ): never {
40
+ throw new ClaudeCodeWireError(code);
41
+ }
42
+
43
+ function validateText(text: string): void {
44
+ for (let index = 0; index < text.length; index += 1) {
45
+ const codeUnit = text.charCodeAt(index);
46
+
47
+ if (
48
+ codeUnit === 0 ||
49
+ (codeUnit < 0x20 &&
50
+ codeUnit !== 0x09 &&
51
+ codeUnit !== 0x0a &&
52
+ codeUnit !== 0x0d) ||
53
+ (codeUnit >= 0x7f && codeUnit <= 0x9f)
54
+ ) {
55
+ fail("INVALID_UNICODE");
56
+ }
57
+
58
+ const classification = classifySurrogateAt(text, index);
59
+ if (classification === "loneSurrogate") fail("INVALID_UNICODE");
60
+ if (classification === "surrogatePair") index += 1;
61
+ }
62
+ }
63
+
64
+ function isUnknownRecord(value: unknown): value is UnknownRecord {
65
+ return value !== null && typeof value === "object";
66
+ }
67
+
68
+ function validateStructure(value: unknown): void {
69
+ const ancestors = new WeakSet();
70
+ let size = 0;
71
+
72
+ function visit(current: unknown, depth: number): void {
73
+ if (depth > MAX_INPUT_DEPTH) fail("INPUT_TOO_DEEP");
74
+
75
+ if (typeof current === "string") {
76
+ size += current.length;
77
+ if (size > MAX_INPUT_SIZE) fail("INPUT_TOO_LARGE");
78
+ validateText(current);
79
+ return;
80
+ }
81
+
82
+ if (!isUnknownRecord(current)) return;
83
+ if (ancestors.has(current)) fail("CYCLIC_INPUT");
84
+
85
+ ancestors.add(current);
86
+ for (const key of Reflect.ownKeys(current)) {
87
+ if (typeof key === "string") {
88
+ size += key.length;
89
+ if (size > MAX_INPUT_SIZE) fail("INPUT_TOO_LARGE");
90
+ validateText(key);
91
+ }
92
+ visit(current[key], depth + 1);
93
+ }
94
+ ancestors.delete(current);
95
+ }
96
+
97
+ visit(value, 0);
98
+ }
99
+
100
+ function cloneTextBlock(value: unknown): TextBlock {
101
+ if (!isUnknownRecord(value)) fail("INVALID_INPUT");
102
+
103
+ const type = value["type"];
104
+ const text = value["text"];
105
+ if (type !== "text" || typeof text !== "string") fail("INVALID_INPUT");
106
+
107
+ const cacheControl = value["cache_control"];
108
+ if (cacheControl === undefined) return Object.freeze({ type: "text", text });
109
+ if (!isUnknownRecord(cacheControl)) fail("INVALID_INPUT");
110
+
111
+ const cacheType = cacheControl["type"];
112
+ const ttl = cacheControl["ttl"];
113
+ const scope = cacheControl["scope"];
114
+ if (
115
+ cacheType !== "ephemeral" ||
116
+ (ttl !== undefined && ttl !== "5m" && ttl !== "1h") ||
117
+ (scope !== undefined && scope !== "global")
118
+ ) {
119
+ fail("INVALID_INPUT");
120
+ }
121
+
122
+ const clonedCacheControl: {
123
+ type: "ephemeral";
124
+ ttl?: "5m" | "1h";
125
+ scope?: "global";
126
+ } = { type: "ephemeral" };
127
+ if (ttl !== undefined) clonedCacheControl.ttl = ttl;
128
+ if (scope !== undefined) clonedCacheControl.scope = scope;
129
+
130
+ return Object.freeze({
131
+ type: "text",
132
+ text,
133
+ cache_control: Object.freeze(clonedCacheControl),
134
+ });
135
+ }
136
+
137
+ function equalCacheControl(
138
+ left: TextBlock["cache_control"],
139
+ right: TextBlock["cache_control"],
140
+ ): boolean {
141
+ if (left === undefined || right === undefined) return left === right;
142
+ if (left === null || right === null) return left === right;
143
+ return left.ttl === right.ttl && left.scope === right.scope;
144
+ }
145
+
146
+ function joinTextBlocks(left: TextBlock, right: TextBlock): TextBlock {
147
+ return Object.freeze({
148
+ type: "text",
149
+ text: `${left.text}\n${right.text}`,
150
+ ...(left.cache_control === undefined
151
+ ? {}
152
+ : { cache_control: left.cache_control }),
153
+ });
154
+ }
155
+
156
+ /** Builds the pinned Claude Code system block sequence without changing caller data. */
157
+ export function buildCanonicalSystem(
158
+ input: readonly SystemInput[] | undefined,
159
+ billingBlock: TextBlock,
160
+ identity: ClaudeCodeRuntimeIdentity,
161
+ suppressBillingBlock = false,
162
+ suppressIdentityBlock = false,
163
+ ): readonly TextBlock[] {
164
+ validateStructure(input);
165
+ if (input !== undefined && !Array.isArray(input)) fail("INVALID_INPUT");
166
+ if (billingBlock.cache_control !== undefined) fail("INVALID_INPUT");
167
+
168
+ const clonedBilling = cloneTextBlock(billingBlock);
169
+ const canonicalBilling = Object.isFrozen(billingBlock)
170
+ ? billingBlock
171
+ : clonedBilling;
172
+
173
+ // The runtime identity is accepted for parity with the request builder. The
174
+ // pinned identity system text itself intentionally contains no identifiers.
175
+ void identity;
176
+
177
+ // Package extension: `suppressBillingBlock` and `suppressIdentityBlock` are
178
+ // the only ways to omit a canonical block. Both default to `false`, which
179
+ // keeps the two-block canonical prefix the genuine client always emits. With
180
+ // both active the canonical prefix is empty and the emitted `system` array
181
+ // holds caller blocks only.
182
+ const blocks: TextBlock[] = suppressBillingBlock ? [] : [canonicalBilling];
183
+ if (!suppressIdentityBlock) {
184
+ blocks.push(
185
+ Object.freeze({
186
+ type: "text",
187
+ text: IDENTITY_TEXT,
188
+ cache_control: Object.freeze({ type: "ephemeral", ttl: "1h" }),
189
+ }),
190
+ );
191
+ }
192
+
193
+ if (input !== undefined) {
194
+ let run: TextBlock | undefined;
195
+ for (const entry of input) {
196
+ const block: TextBlock =
197
+ typeof entry === "string"
198
+ ? Object.freeze({ type: "text" as const, text: entry })
199
+ : cloneTextBlock(entry);
200
+
201
+ // Upstream recognizes only the byte-for-byte identity constant. Similar
202
+ // caller text remains ordinary prompt content.
203
+ //
204
+ // The drop stays UNCONDITIONAL under `suppressIdentityBlock`: the genuine
205
+ // client drops it too, and a caller block equal to the identity text
206
+ // landing at the front of a suppressed prefix would defeat the parser's
207
+ // structural check of the canonical prefix.
208
+ if (block.text === IDENTITY_TEXT) continue;
209
+ if (run === undefined) {
210
+ run = block;
211
+ } else if (equalCacheControl(run.cache_control, block.cache_control)) {
212
+ run = joinTextBlocks(run, block);
213
+ } else {
214
+ blocks.push(run);
215
+ run = block;
216
+ }
217
+ }
218
+ if (run !== undefined) blocks.push(run);
219
+ }
220
+
221
+ return Object.freeze(blocks);
222
+ }
@@ -0,0 +1,266 @@
1
+ // SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+ import type {
4
+ ClaudeCodeBetaPolicy,
5
+ ClaudeCodeCapabilities,
6
+ } from "./contracts.js";
7
+
8
+ /**
9
+ * Extended-thinking resolution, ported from the genuine client's request
10
+ * builder at byte offset 238154330.
11
+ *
12
+ * The single most surprising thing in here, and the reason this module exists
13
+ * rather than a handful of inline branches: **the caller does not choose
14
+ * between adaptive and enabled thinking — the model does.**
15
+ *
16
+ * Upstream the choice is
17
+ *
18
+ * ```
19
+ * cn = aSr(s.model);
20
+ * if (cn !== void 0 ? cn === "adaptive" : Uot(u) && !zt) { adaptive } else { enabled }
21
+ * ```
22
+ *
23
+ * `aSr` is `Bt.thinkingTypeOverrides.get(e)`, a host-side override map that is
24
+ * empty on a default install, so `cn` is undefined and the ternary falls through
25
+ * to `Uot(u)` — the adaptive-thinking capability predicate. `zt` additionally
26
+ * requires an environment variable that is unset by default.
27
+ *
28
+ * So a caller asking for `type: "enabled"` against an adaptive-capable model
29
+ * gets `{type:"adaptive"}` on the wire and their `budgetTokens` is discarded,
30
+ * and a caller asking for `type: "adaptive"` against a model without the
31
+ * capability gets `{type:"enabled",budget_tokens:…}`. The caller's `type` is
32
+ * load-bearing in exactly one way: whether or not it is `"disabled"`.
33
+ *
34
+ * This package reproduces that. Rejecting the mismatch instead — which is what
35
+ * it used to do — would make its traffic distinguishable from the real client's,
36
+ * which is the one thing it exists to avoid.
37
+ *
38
+ * This module also owns `modelOutputTokenLimits` and `clampMaxTokens`, which
39
+ * bound `max_tokens` rather than anything thinking-specific. They live here
40
+ * because upstream derives both from one table (`Xxe`) and feeds one clamped
41
+ * value (`Fi`) into both the emitted `max_tokens` and the thinking budget, so
42
+ * splitting them would separate two things that must not drift apart. If a
43
+ * third consumer of the limit table ever appears, extract all three into their
44
+ * own module at that point.
45
+ */
46
+
47
+ /** Permitted values of the `display` property, from the schema at 241453966. */
48
+ export type ThinkingDisplay = "summarized" | "omitted";
49
+
50
+ export interface ThinkingRequest {
51
+ readonly type: "enabled" | "adaptive" | "disabled";
52
+ readonly budgetTokens?: number;
53
+ readonly display?: ThinkingDisplay;
54
+ }
55
+
56
+ export interface ModelOutputTokenLimits {
57
+ readonly default: number;
58
+ readonly upperLimit: number;
59
+ }
60
+
61
+ export interface ResolvedThinking {
62
+ /** The object to place at `body.thinking`, or undefined to omit the field. */
63
+ readonly emitted: Readonly<Record<string, unknown>> | undefined;
64
+ /**
65
+ * Whether the caller asked for thinking at all, regardless of whether any
66
+ * `thinking` object survived resolution. This — not `emitted` — is what
67
+ * suppresses `temperature`, matching upstream `nr`.
68
+ */
69
+ readonly requestActive: boolean;
70
+ /** Whether `tool_choice` of type `tool` must be demoted to `auto`. */
71
+ readonly extendedThinkingActive: boolean;
72
+ }
73
+
74
+ /**
75
+ * 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
+ *
80
+ * Both fields are load-bearing. `upperLimit` seeds the thinking budget when the
81
+ * caller supplies none (upstream `wvi = Xxe(e).upperLimit - 1`); `default` caps
82
+ * the emitted `max_tokens` (upstream `qct`, see `clampMaxTokens`).
83
+ *
84
+ * Upstream additionally consults `Vkd` and `bvi`, neither of which is modelled:
85
+ *
86
+ * - `Vkd(e)` lowers `default` from the host config object `heather_vale`.
87
+ * That object is absent on a default install, so it returns null and makes
88
+ * no adjustment. Same class as `W9` in `model-capabilities.ts`: a host-side
89
+ * override this package cannot observe.
90
+ * - `bvi(e)` adjusts BOTH fields, but sits behind `_vi()`, which returns a
91
+ * hard `false`. Dead code upstream.
92
+ */
93
+ export function modelOutputTokenLimits(
94
+ normalizedId: string,
95
+ ): 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
+ }
111
+ 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"
116
+ ) {
117
+ return { default: 32000, upperLimit: 64000 };
118
+ }
119
+ if (
120
+ normalizedId === "claude-opus-4-1" ||
121
+ normalizedId === "claude-opus-4-0"
122
+ ) {
123
+ return { default: 32000, upperLimit: 32000 };
124
+ }
125
+ if (normalizedId === "claude-3-opus") {
126
+ return { default: 4096, upperLimit: 4096 };
127
+ }
128
+ if (normalizedId === "claude-3-sonnet") {
129
+ return { default: 8192, upperLimit: 8192 };
130
+ }
131
+ if (normalizedId === "claude-3-haiku") {
132
+ return { default: 4096, upperLimit: 4096 };
133
+ }
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
+ return { default: 32000, upperLimit: 128000 };
144
+ }
145
+
146
+ /**
147
+ * Caps the caller's `max_tokens` at the model's default output limit, porting
148
+ * upstream `Fi = Math.min(En?.maxTokensOverride || s.maxOutputTokensOverride
149
+ * || la, la)` where `la = qct(u)`.
150
+ *
151
+ * `qct` is `Fue("CLAUDE_CODE_MAX_OUTPUT_TOKENS", <env>, t.default,
152
+ * t.upperLimit).effective` over `t = Xxe(model)`. `Fue` returns `t.default`
153
+ * untouched whenever the environment variable is unset, and only ever clamps
154
+ * the ENVIRONMENT value against `t.upperLimit` — never the default. This
155
+ * package reads no environment, so `qct` reduces to `Xxe(model).default` and
156
+ * `upperLimit` plays no part in this bound.
157
+ *
158
+ * The result is load-bearing twice over: it is the emitted `max_tokens`, and it
159
+ * is the `Fi` that `resolveThinking` clamps the thinking budget against via
160
+ * `Tr = Math.min(Fi - 1, Tr)`. Both call sites must receive the CLAMPED value.
161
+ *
162
+ * Upstream uses `||`, not `??`, so a zero override would fall back to the
163
+ * default. Unreachable here: `max_tokens` is validated as a positive integer
164
+ * before this runs.
165
+ */
166
+ export function clampMaxTokens(
167
+ requested: number,
168
+ normalizedId: string,
169
+ ): number {
170
+ return Math.min(requested, modelOutputTokenLimits(normalizedId).default);
171
+ }
172
+
173
+ /**
174
+ * Upstream `Yn = nr && CM() && QOt(u) ? n.display : void 0`, combined with the
175
+ * `if (Xn && Yn)` guard on the beta splice.
176
+ *
177
+ * This answers the splice question on its own, without consulting whether a
178
+ * `thinking` object was actually emitted, because the two are equivalent: a true
179
+ * result requires `type !== "disabled"` and `capabilities.thinking`, and those
180
+ * two conditions are exactly what drives `resolveThinking` into one of its two
181
+ * emitting branches. Upstream's `Xn` is therefore always set whenever `Yn` is.
182
+ *
183
+ * Deliberately tolerant of unvalidated input so that request assembly can ask
184
+ * this question before the body validator has run. Anything malformed answers
185
+ * false here and is rejected later by `buildCanonicalBody`.
186
+ */
187
+ export function isThinkingDisplayActive(
188
+ request: unknown,
189
+ capabilities: ClaudeCodeCapabilities,
190
+ betaPolicy: ClaudeCodeBetaPolicy,
191
+ ): boolean {
192
+ if (request === null || typeof request !== "object") return false;
193
+ const record = request as Record<string, unknown>;
194
+ if (record["type"] === "disabled") return false;
195
+ const display = record["display"];
196
+ if (display !== "summarized" && display !== "omitted") return false;
197
+ return (
198
+ capabilities.thinking &&
199
+ capabilities.interleavedThinking &&
200
+ betaPolicy.experimentalBetasEnabled
201
+ );
202
+ }
203
+
204
+ /**
205
+ * Resolves the caller's thinking request into the object the genuine client
206
+ * would put on the wire.
207
+ *
208
+ * Key order is load-bearing. Upstream emits `{budget_tokens, type, display}`
209
+ * for the enabled branch — `budget_tokens` FIRST — and `{type, display}` for
210
+ * adaptive. Serialised bodies are compared byte for byte, so the insertion
211
+ * order below must not be rearranged.
212
+ */
213
+ export function resolveThinking(
214
+ request: ThinkingRequest | undefined,
215
+ normalizedId: string,
216
+ capabilities: ClaudeCodeCapabilities,
217
+ betaPolicy: ClaudeCodeBetaPolicy,
218
+ maxTokens: number,
219
+ ): ResolvedThinking {
220
+ // Upstream `nr = n.type !== "disabled" && !CLAUDE_CODE_DISABLE_THINKING`.
221
+ const requestActive = request !== undefined && request.type !== "disabled";
222
+ const displayActive = isThinkingDisplayActive(
223
+ request,
224
+ capabilities,
225
+ betaPolicy,
226
+ );
227
+ const display = displayActive ? request?.display : undefined;
228
+
229
+ let emitted: Record<string, unknown> | undefined;
230
+
231
+ if (requestActive && capabilities.thinking) {
232
+ if (capabilities.adaptiveThinking) {
233
+ emitted = { type: "adaptive" };
234
+ if (display !== undefined) emitted["display"] = display;
235
+ } else {
236
+ // Upstream: `let Tr = wvi(u)` — the model's upper limit minus one —
237
+ // overridden by the caller's budget when supplied, then clamped by
238
+ // `Tr = Math.min(Fi - 1, Tr)` where `Fi` is the emitted `max_tokens`.
239
+ const requested =
240
+ request.budgetTokens ??
241
+ modelOutputTokenLimits(normalizedId).upperLimit - 1;
242
+ emitted = { budget_tokens: Math.min(maxTokens - 1, requested) };
243
+ emitted["type"] = "enabled";
244
+ if (display !== undefined) emitted["display"] = display;
245
+ }
246
+ } else if (
247
+ request?.type === "disabled" &&
248
+ capabilities.thinking &&
249
+ !capabilities.rejectsDisabledThinking
250
+ ) {
251
+ emitted = { type: "disabled" };
252
+ }
253
+
254
+ // Upstream `Jr = Xn?.type === "enabled" || Xn?.type === "adaptive"
255
+ // || Xn === void 0 && U4e(u)`.
256
+ const extendedThinkingActive =
257
+ emitted?.["type"] === "enabled" ||
258
+ emitted?.["type"] === "adaptive" ||
259
+ (emitted === undefined && capabilities.rejectsDisabledThinking);
260
+
261
+ return Object.freeze({
262
+ emitted: emitted === undefined ? undefined : Object.freeze(emitted),
263
+ requestActive,
264
+ extendedThinkingActive,
265
+ });
266
+ }
package/src/unicode.ts ADDED
@@ -0,0 +1,24 @@
1
+ // SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+ export type SurrogateClassification =
4
+ "notSurrogate" | "surrogatePair" | "loneSurrogate";
5
+
6
+ /**
7
+ * Classifies the UTF-16 code unit at `index`.
8
+ *
9
+ * A trailing high surrogate makes charCodeAt(index + 1) return NaN, and every
10
+ * relational comparison against NaN is false, so the low-surrogate test must be
11
+ * a negated in-range test rather than an out-of-range test.
12
+ */
13
+ export function classifySurrogateAt(
14
+ value: string,
15
+ index: number,
16
+ ): SurrogateClassification {
17
+ const unit = value.charCodeAt(index);
18
+ if (unit >= 0xd800 && unit <= 0xdbff) {
19
+ const next = value.charCodeAt(index + 1);
20
+ return next >= 0xdc00 && next <= 0xdfff ? "surrogatePair" : "loneSurrogate";
21
+ }
22
+ if (unit >= 0xdc00 && unit <= 0xdfff) return "loneSurrogate";
23
+ return "notSurrogate";
24
+ }