@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/CHANGELOG.md +36 -2
- package/README.md +53 -1
- package/package.json +2 -1
- package/src/anti-verbosity.ts +219 -0
- package/src/beta-registry.ts +140 -0
- package/src/betas.ts +219 -0
- package/src/build-request.ts +1655 -0
- package/src/contracts.ts +1233 -0
- package/src/count-tokens.ts +84 -0
- package/src/fingerprint.ts +85 -0
- package/src/headers.ts +442 -0
- package/src/index.ts +62 -0
- package/src/metadata.ts +331 -0
- package/src/model-capabilities.ts +295 -0
- package/src/model-identity.ts +45 -0
- package/src/models.ts +46 -0
- package/src/profiles/claude-code-2.1.195.ts +154 -0
- package/src/redaction.ts +521 -0
- package/src/request-body.ts +1924 -0
- package/src/sha256.ts +114 -0
- package/src/system-prompt.ts +222 -0
- package/src/thinking.ts +266 -0
- package/src/unicode.ts +24 -0
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
|
+
}
|
package/src/thinking.ts
ADDED
|
@@ -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
|
+
}
|