@vacbo/opencode-anthropic-fix 0.1.7 → 0.1.9
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/README.md +88 -88
- package/dist/opencode-anthropic-auth-cli.mjs +804 -507
- package/dist/opencode-anthropic-auth-plugin.js +4751 -4109
- package/package.json +67 -59
- package/src/__tests__/billing-edge-cases.test.ts +59 -59
- package/src/__tests__/bun-proxy.parallel.test.ts +388 -382
- package/src/__tests__/cc-comparison.test.ts +87 -87
- package/src/__tests__/cc-credentials.test.ts +254 -250
- package/src/__tests__/cch-drift-checker.test.ts +51 -51
- package/src/__tests__/cch-native-style.test.ts +56 -56
- package/src/__tests__/debug-gating.test.ts +42 -42
- package/src/__tests__/decomposition-smoke.test.ts +68 -68
- package/src/__tests__/fingerprint-regression.test.ts +575 -566
- package/src/__tests__/helpers/conversation-history.smoke.test.ts +271 -271
- package/src/__tests__/helpers/conversation-history.ts +119 -119
- package/src/__tests__/helpers/deferred.smoke.test.ts +103 -103
- package/src/__tests__/helpers/deferred.ts +69 -69
- package/src/__tests__/helpers/in-memory-storage.smoke.test.ts +155 -155
- package/src/__tests__/helpers/in-memory-storage.ts +88 -88
- package/src/__tests__/helpers/mock-bun-proxy.smoke.test.ts +68 -68
- package/src/__tests__/helpers/mock-bun-proxy.ts +189 -189
- package/src/__tests__/helpers/plugin-fetch-harness.smoke.test.ts +273 -273
- package/src/__tests__/helpers/plugin-fetch-harness.ts +288 -288
- package/src/__tests__/helpers/sse.smoke.test.ts +236 -236
- package/src/__tests__/helpers/sse.ts +209 -209
- package/src/__tests__/index.parallel.test.ts +605 -595
- package/src/__tests__/sanitization-regex.test.ts +112 -112
- package/src/__tests__/state-bounds.test.ts +90 -90
- package/src/account-identity.test.ts +197 -192
- package/src/account-identity.ts +69 -67
- package/src/account-state.test.ts +86 -86
- package/src/account-state.ts +25 -25
- package/src/accounts/matching.test.ts +335 -0
- package/src/accounts/matching.ts +167 -0
- package/src/accounts/persistence.test.ts +345 -0
- package/src/accounts/persistence.ts +432 -0
- package/src/accounts/repair.test.ts +276 -0
- package/src/accounts/repair.ts +407 -0
- package/src/accounts.dedup.test.ts +621 -621
- package/src/accounts.test.ts +933 -929
- package/src/accounts.ts +633 -989
- package/src/backoff.test.ts +345 -345
- package/src/backoff.ts +219 -219
- package/src/betas.ts +124 -124
- package/src/bun-fetch.test.ts +345 -342
- package/src/bun-fetch.ts +424 -424
- package/src/bun-proxy.test.ts +25 -25
- package/src/bun-proxy.ts +209 -209
- package/src/cc-credentials.ts +111 -111
- package/src/circuit-breaker.test.ts +184 -184
- package/src/circuit-breaker.ts +169 -169
- package/src/cli/commands/auth.ts +963 -0
- package/src/cli/commands/config.ts +547 -0
- package/src/cli/formatting.test.ts +406 -0
- package/src/cli/formatting.ts +219 -0
- package/src/cli.ts +255 -2022
- package/src/commands/handlers/betas.ts +100 -0
- package/src/commands/handlers/config.ts +99 -0
- package/src/commands/handlers/files.ts +375 -0
- package/src/commands/oauth-flow.ts +181 -166
- package/src/commands/prompts.ts +61 -61
- package/src/commands/router.test.ts +421 -0
- package/src/commands/router.ts +143 -635
- package/src/config.test.ts +482 -482
- package/src/config.ts +412 -404
- package/src/constants.ts +48 -48
- package/src/drift/cch-constants.ts +95 -95
- package/src/env.ts +111 -105
- package/src/headers/billing.ts +33 -33
- package/src/headers/builder.ts +130 -130
- package/src/headers/cch.ts +75 -75
- package/src/headers/stainless.ts +25 -25
- package/src/headers/user-agent.ts +23 -23
- package/src/index.ts +436 -828
- package/src/models.ts +27 -27
- package/src/oauth.test.ts +102 -102
- package/src/oauth.ts +178 -178
- package/src/parent-pid-watcher.test.ts +148 -148
- package/src/parent-pid-watcher.ts +69 -69
- package/src/plugin-helpers.ts +82 -82
- package/src/refresh-helpers.ts +145 -139
- package/src/refresh-lock.test.ts +94 -94
- package/src/refresh-lock.ts +93 -93
- package/src/request/body.history.test.ts +579 -571
- package/src/request/body.ts +255 -255
- package/src/request/metadata.ts +65 -65
- package/src/request/retry.test.ts +156 -156
- package/src/request/retry.ts +67 -67
- package/src/request/url.ts +21 -21
- package/src/request-orchestration-helpers.ts +648 -0
- package/src/response/index.ts +5 -5
- package/src/response/mcp.ts +58 -58
- package/src/response/streaming.test.ts +313 -311
- package/src/response/streaming.ts +412 -410
- package/src/rotation.test.ts +304 -301
- package/src/rotation.ts +205 -205
- package/src/storage.test.ts +547 -547
- package/src/storage.ts +315 -291
- package/src/system-prompt/builder.ts +38 -38
- package/src/system-prompt/index.ts +5 -5
- package/src/system-prompt/normalize.ts +60 -60
- package/src/system-prompt/sanitize.ts +30 -30
- package/src/thinking.ts +21 -20
- package/src/token-refresh.test.ts +265 -265
- package/src/token-refresh.ts +219 -214
- package/src/types.ts +30 -30
- package/dist/bun-proxy.mjs +0 -291
|
@@ -13,15 +13,15 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
|
13
13
|
|
|
14
14
|
import { buildAnthropicBetaHeader } from "../betas.js";
|
|
15
15
|
import {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
16
|
+
ADVANCED_TOOL_USE_BETA_FLAG,
|
|
17
|
+
BEDROCK_UNSUPPORTED_BETAS,
|
|
18
|
+
CLAUDE_CODE_BETA_FLAG,
|
|
19
|
+
CLAUDE_CODE_IDENTITY_STRING,
|
|
20
|
+
EFFORT_BETA_FLAG,
|
|
21
|
+
EXPERIMENTAL_BETA_FLAGS,
|
|
22
|
+
FALLBACK_CLAUDE_CLI_VERSION,
|
|
23
|
+
FAST_MODE_BETA_FLAG,
|
|
24
|
+
TOKEN_COUNTING_BETA_FLAG,
|
|
25
25
|
} from "../constants.js";
|
|
26
26
|
import { buildAnthropicBillingHeader } from "../headers/billing.js";
|
|
27
27
|
import { isAdaptiveThinkingModel, isSonnet46Model } from "../models.js";
|
|
@@ -38,649 +38,658 @@ const CC_VERSION = "2.1.98";
|
|
|
38
38
|
const STAINLESS_PACKAGE_VERSION = "0.81.0";
|
|
39
39
|
|
|
40
40
|
type EnvKey =
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
41
|
+
| "CLAUDE_AGENT_SDK_CLIENT_APP"
|
|
42
|
+
| "CLAUDE_AGENT_SDK_VERSION"
|
|
43
|
+
| "CLAUDE_CODE_ATTRIBUTION_HEADER"
|
|
44
|
+
| "CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS"
|
|
45
|
+
| "CLAUDE_CODE_ENTRYPOINT";
|
|
46
46
|
|
|
47
47
|
function snapshotEnv(...keys: readonly EnvKey[]) {
|
|
48
|
-
|
|
48
|
+
return Object.fromEntries(keys.map((key) => [key, process.env[key]])) as Partial<
|
|
49
|
+
Record<EnvKey, string | undefined>
|
|
50
|
+
>;
|
|
49
51
|
}
|
|
50
52
|
|
|
51
53
|
function restoreEnv(snapshot: Partial<Record<EnvKey, string | undefined>>) {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
for (const [key, value] of Object.entries(snapshot) as [EnvKey, string | undefined][]) {
|
|
55
|
+
if (value === undefined) {
|
|
56
|
+
delete process.env[key];
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
57
59
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
+
process.env[key] = value;
|
|
61
|
+
}
|
|
60
62
|
}
|
|
61
63
|
|
|
62
64
|
// ---------------------------------------------------------------------------
|
|
63
65
|
// User-Agent
|
|
64
66
|
// ---------------------------------------------------------------------------
|
|
65
67
|
describe("CC 2.1.98 — User-Agent format", () => {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
beforeEach(() => {
|
|
69
|
-
originalEnv = snapshotEnv("CLAUDE_CODE_ENTRYPOINT", "CLAUDE_AGENT_SDK_VERSION", "CLAUDE_AGENT_SDK_CLIENT_APP");
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
afterEach(() => {
|
|
73
|
-
restoreEnv(originalEnv);
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it("matches the claude-cli/<version> (external, cli) pattern", () => {
|
|
77
|
-
delete process.env.CLAUDE_CODE_ENTRYPOINT;
|
|
78
|
-
delete process.env.CLAUDE_AGENT_SDK_VERSION;
|
|
79
|
-
delete process.env.CLAUDE_AGENT_SDK_CLIENT_APP;
|
|
80
|
-
const ua = buildUserAgent(CC_VERSION);
|
|
81
|
-
expect(ua).toBe(`claude-cli/${CC_VERSION} (external, cli)`);
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it("embeds the correct CC version (2.1.98)", () => {
|
|
85
|
-
const ua = buildUserAgent(CC_VERSION);
|
|
86
|
-
expect(ua).toMatch(/^claude-cli\/2\.1\.98 /);
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
it("FALLBACK_CLAUDE_CLI_VERSION constant is 2.1.98", () => {
|
|
90
|
-
expect(FALLBACK_CLAUDE_CLI_VERSION).toBe("2.1.98");
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
it("appends agent-sdk suffix when CLAUDE_AGENT_SDK_VERSION is set", () => {
|
|
94
|
-
process.env.CLAUDE_AGENT_SDK_VERSION = "1.2.3";
|
|
95
|
-
process.env.CLAUDE_CODE_ENTRYPOINT = "cli";
|
|
96
|
-
const ua = buildUserAgent(CC_VERSION);
|
|
97
|
-
expect(ua).toContain(", agent-sdk/1.2.3");
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
it("appends client-app suffix when CLAUDE_AGENT_SDK_CLIENT_APP is set", () => {
|
|
101
|
-
process.env.CLAUDE_AGENT_SDK_CLIENT_APP = "myapp";
|
|
102
|
-
process.env.CLAUDE_CODE_ENTRYPOINT = "cli";
|
|
103
|
-
const ua = buildUserAgent(CC_VERSION);
|
|
104
|
-
expect(ua).toContain(", client-app/myapp");
|
|
105
|
-
});
|
|
106
|
-
});
|
|
68
|
+
let originalEnv: Partial<Record<EnvKey, string | undefined>>;
|
|
107
69
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
});
|
|
70
|
+
beforeEach(() => {
|
|
71
|
+
originalEnv = snapshotEnv("CLAUDE_CODE_ENTRYPOINT", "CLAUDE_AGENT_SDK_VERSION", "CLAUDE_AGENT_SDK_CLIENT_APP");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
afterEach(() => {
|
|
75
|
+
restoreEnv(originalEnv);
|
|
76
|
+
});
|
|
116
77
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
78
|
+
it("matches the claude-cli/<version> (external, cli) pattern", () => {
|
|
79
|
+
delete process.env.CLAUDE_CODE_ENTRYPOINT;
|
|
80
|
+
delete process.env.CLAUDE_AGENT_SDK_VERSION;
|
|
81
|
+
delete process.env.CLAUDE_AGENT_SDK_CLIENT_APP;
|
|
82
|
+
const ua = buildUserAgent(CC_VERSION);
|
|
83
|
+
expect(ua).toBe(`claude-cli/${CC_VERSION} (external, cli)`);
|
|
84
|
+
});
|
|
121
85
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
86
|
+
it("embeds the correct CC version (2.1.98)", () => {
|
|
87
|
+
const ua = buildUserAgent(CC_VERSION);
|
|
88
|
+
expect(ua).toMatch(/^claude-cli\/2\.1\.98 /);
|
|
89
|
+
});
|
|
125
90
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
91
|
+
it("FALLBACK_CLAUDE_CLI_VERSION constant is 2.1.98", () => {
|
|
92
|
+
expect(FALLBACK_CLAUDE_CLI_VERSION).toBe("2.1.98");
|
|
93
|
+
});
|
|
129
94
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
95
|
+
it("appends agent-sdk suffix when CLAUDE_AGENT_SDK_VERSION is set", () => {
|
|
96
|
+
process.env.CLAUDE_AGENT_SDK_VERSION = "1.2.3";
|
|
97
|
+
process.env.CLAUDE_CODE_ENTRYPOINT = "cli";
|
|
98
|
+
const ua = buildUserAgent(CC_VERSION);
|
|
99
|
+
expect(ua).toContain(", agent-sdk/1.2.3");
|
|
133
100
|
});
|
|
134
101
|
|
|
135
|
-
it("
|
|
136
|
-
|
|
102
|
+
it("appends client-app suffix when CLAUDE_AGENT_SDK_CLIENT_APP is set", () => {
|
|
103
|
+
process.env.CLAUDE_AGENT_SDK_CLIENT_APP = "myapp";
|
|
104
|
+
process.env.CLAUDE_CODE_ENTRYPOINT = "cli";
|
|
105
|
+
const ua = buildUserAgent(CC_VERSION);
|
|
106
|
+
expect(ua).toContain(", client-app/myapp");
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
// Stainless headers
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
113
|
+
describe("CC 2.1.98 — Stainless headers", () => {
|
|
114
|
+
it("x-stainless-package-version is 0.81.0", () => {
|
|
115
|
+
// The constant is hardcoded in builder.ts — verify the documented value
|
|
116
|
+
expect(STAINLESS_PACKAGE_VERSION).toBe("0.81.0");
|
|
137
117
|
});
|
|
138
118
|
|
|
139
|
-
it("
|
|
140
|
-
|
|
119
|
+
it("x-stainless-lang is js (documented)", () => {
|
|
120
|
+
// Verified by builder.ts line: requestHeaders.set("x-stainless-lang", "js")
|
|
121
|
+
expect("js").toBe("js");
|
|
141
122
|
});
|
|
142
123
|
|
|
143
|
-
it("
|
|
144
|
-
|
|
124
|
+
it("x-stainless-runtime is node (documented)", () => {
|
|
125
|
+
expect("node").toBe("node");
|
|
145
126
|
});
|
|
146
|
-
});
|
|
147
127
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
expect(getStainlessArch("x64")).toBe("x64");
|
|
128
|
+
it("x-stainless-timeout is 600 (documented)", () => {
|
|
129
|
+
expect("600").toBe("600");
|
|
151
130
|
});
|
|
152
131
|
|
|
153
|
-
|
|
154
|
-
|
|
132
|
+
describe("getStainlessOs", () => {
|
|
133
|
+
it("maps darwin to MacOS", () => {
|
|
134
|
+
expect(getStainlessOs("darwin")).toBe("MacOS");
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("maps win32 to Windows", () => {
|
|
138
|
+
expect(getStainlessOs("win32")).toBe("Windows");
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("maps linux to Linux", () => {
|
|
142
|
+
expect(getStainlessOs("linux")).toBe("Linux");
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("passes through unknown platforms", () => {
|
|
146
|
+
expect(getStainlessOs("freebsd" as NodeJS.Platform)).toBe("freebsd");
|
|
147
|
+
});
|
|
155
148
|
});
|
|
156
149
|
|
|
157
|
-
|
|
158
|
-
|
|
150
|
+
describe("getStainlessArch", () => {
|
|
151
|
+
it("maps x64 to x64", () => {
|
|
152
|
+
expect(getStainlessArch("x64")).toBe("x64");
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("maps arm64 to arm64", () => {
|
|
156
|
+
expect(getStainlessArch("arm64")).toBe("arm64");
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("passes through unknown arch", () => {
|
|
160
|
+
expect(getStainlessArch("riscv64")).toBe("riscv64");
|
|
161
|
+
});
|
|
159
162
|
});
|
|
160
|
-
});
|
|
161
163
|
});
|
|
162
164
|
|
|
163
165
|
// ---------------------------------------------------------------------------
|
|
164
166
|
// Billing header / cch
|
|
165
167
|
// ---------------------------------------------------------------------------
|
|
166
168
|
describe("CC 2.1.98 — Billing header", () => {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
169
|
+
let originalEnv: Partial<Record<EnvKey, string | undefined>>;
|
|
170
|
+
|
|
171
|
+
beforeEach(() => {
|
|
172
|
+
originalEnv = snapshotEnv("CLAUDE_CODE_ATTRIBUTION_HEADER", "CLAUDE_CODE_ENTRYPOINT");
|
|
173
|
+
process.env.CLAUDE_CODE_ATTRIBUTION_HEADER = "true";
|
|
174
|
+
process.env.CLAUDE_CODE_ENTRYPOINT = "cli";
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
afterEach(() => {
|
|
178
|
+
restoreEnv(originalEnv);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("contains the native placeholder cch before post-serialization replacement", () => {
|
|
182
|
+
const header = buildAnthropicBillingHeader(CC_VERSION, []);
|
|
183
|
+
expect(header).toContain("cch=00000;");
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("contains the correct cc_version", () => {
|
|
187
|
+
const header = buildAnthropicBillingHeader(CC_VERSION, []);
|
|
188
|
+
expect(header).toContain(`cc_version=${CC_VERSION}`);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("contains the correct cc_entrypoint=cli", () => {
|
|
192
|
+
const header = buildAnthropicBillingHeader(CC_VERSION, []);
|
|
193
|
+
expect(header).toContain("cc_entrypoint=cli;");
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("starts with x-anthropic-billing-header:", () => {
|
|
197
|
+
const header = buildAnthropicBillingHeader(CC_VERSION, []);
|
|
198
|
+
expect(header).toMatch(/^x-anthropic-billing-header:/);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it("returns empty string when CLAUDE_CODE_ATTRIBUTION_HEADER is not set", () => {
|
|
202
|
+
process.env.CLAUDE_CODE_ATTRIBUTION_HEADER = "false";
|
|
203
|
+
const header = buildAnthropicBillingHeader(CC_VERSION, []);
|
|
204
|
+
expect(header).toBe("");
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it("appends version hash derived from first user message", () => {
|
|
208
|
+
const messages = [{ role: "user", content: "Hello world from a test" }];
|
|
209
|
+
const header = buildAnthropicBillingHeader(CC_VERSION, messages);
|
|
210
|
+
|
|
211
|
+
// Replicate the current billing-header algorithm: SHA-256(salt + chars[4,7,20] + version)
|
|
212
|
+
const text = "Hello world from a test";
|
|
213
|
+
const salt = "59cf53e54c78";
|
|
214
|
+
const picked = [4, 7, 20].map((i) => (i < text.length ? text[i] : "0")).join("");
|
|
215
|
+
const expectedHash = createHash("sha256")
|
|
216
|
+
.update(salt + picked + CC_VERSION)
|
|
217
|
+
.digest("hex")
|
|
218
|
+
.slice(0, 3);
|
|
219
|
+
|
|
220
|
+
expect(header).toContain(`cc_version=${CC_VERSION}.${expectedHash}`);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it("omits version hash when no user message is present", () => {
|
|
224
|
+
const messages = [{ role: "assistant", content: "I am the assistant" }];
|
|
225
|
+
const header = buildAnthropicBillingHeader(CC_VERSION, messages);
|
|
226
|
+
// No dot-suffix after version
|
|
227
|
+
expect(header).toContain(`cc_version=${CC_VERSION};`);
|
|
228
|
+
});
|
|
227
229
|
});
|
|
228
230
|
|
|
229
231
|
// ---------------------------------------------------------------------------
|
|
230
232
|
// Beta flags
|
|
231
233
|
// ---------------------------------------------------------------------------
|
|
232
234
|
describe("CC 2.1.98 — Beta constants", () => {
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
235
|
+
it("CLAUDE_CODE_BETA_FLAG is claude-code-20250219", () => {
|
|
236
|
+
expect(CLAUDE_CODE_BETA_FLAG).toBe("claude-code-20250219");
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("ADVANCED_TOOL_USE_BETA_FLAG is advanced-tool-use-2025-11-20", () => {
|
|
240
|
+
expect(ADVANCED_TOOL_USE_BETA_FLAG).toBe("advanced-tool-use-2025-11-20");
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it("FAST_MODE_BETA_FLAG is fast-mode-2026-02-01", () => {
|
|
244
|
+
expect(FAST_MODE_BETA_FLAG).toBe("fast-mode-2026-02-01");
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it("TOKEN_COUNTING_BETA_FLAG is token-counting-2024-11-01", () => {
|
|
248
|
+
expect(TOKEN_COUNTING_BETA_FLAG).toBe("token-counting-2024-11-01");
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it("BEDROCK_UNSUPPORTED_BETAS contains the exact 3 documented betas", () => {
|
|
252
|
+
expect(BEDROCK_UNSUPPORTED_BETAS).toContain("interleaved-thinking-2025-05-14");
|
|
253
|
+
expect(BEDROCK_UNSUPPORTED_BETAS).toContain("context-1m-2025-08-07");
|
|
254
|
+
expect(BEDROCK_UNSUPPORTED_BETAS).toContain("tool-search-tool-2025-10-19");
|
|
255
|
+
expect(BEDROCK_UNSUPPORTED_BETAS.size).toBe(3);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it("EXPERIMENTAL_BETA_FLAGS includes fast-mode and advanced-tool-use", () => {
|
|
259
|
+
expect(EXPERIMENTAL_BETA_FLAGS).toContain("fast-mode-2026-02-01");
|
|
260
|
+
expect(EXPERIMENTAL_BETA_FLAGS).toContain("advanced-tool-use-2025-11-20");
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it("EXPERIMENTAL_BETA_FLAGS includes CC v2.1.98 new betas", () => {
|
|
264
|
+
// CC Remote and feature betas from v2.1.98
|
|
265
|
+
expect(EXPERIMENTAL_BETA_FLAGS).toContain("ccr-byoc-2025-07-29");
|
|
266
|
+
expect(EXPERIMENTAL_BETA_FLAGS).toContain("ccr-triggers-2026-01-30");
|
|
267
|
+
expect(EXPERIMENTAL_BETA_FLAGS).toContain("environments-2025-11-01");
|
|
268
|
+
expect(EXPERIMENTAL_BETA_FLAGS).toContain("mcp-client-2025-11-20");
|
|
269
|
+
expect(EXPERIMENTAL_BETA_FLAGS).toContain("skills-2025-10-02");
|
|
270
|
+
});
|
|
269
271
|
});
|
|
270
272
|
|
|
271
273
|
describe("CC 2.1.98 — Beta header composition (signature enabled)", () => {
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
274
|
+
let originalEnv: Partial<Record<EnvKey, string | undefined>>;
|
|
275
|
+
|
|
276
|
+
beforeEach(() => {
|
|
277
|
+
originalEnv = snapshotEnv("CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS");
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
afterEach(() => {
|
|
281
|
+
restoreEnv(originalEnv);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
const baseArgs = {
|
|
285
|
+
incomingBeta: "",
|
|
286
|
+
signatureEnabled: true,
|
|
287
|
+
model: "claude-3-5-sonnet-20241022",
|
|
288
|
+
provider: "anthropic" as const,
|
|
289
|
+
customBetas: undefined as string[] | undefined,
|
|
290
|
+
strategy: undefined as "round-robin" | undefined,
|
|
291
|
+
requestPath: undefined as string | undefined,
|
|
292
|
+
hasFileReferences: false,
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
function callBuildBeta(
|
|
296
|
+
overrides: Partial<Omit<typeof baseArgs, "provider">> & { provider?: "anthropic" | "bedrock" } = {},
|
|
297
|
+
) {
|
|
298
|
+
const args = { ...baseArgs, ...overrides } as typeof baseArgs & { provider?: "anthropic" | "bedrock" };
|
|
299
|
+
return buildAnthropicBetaHeader(
|
|
300
|
+
args.incomingBeta,
|
|
301
|
+
args.signatureEnabled,
|
|
302
|
+
args.model,
|
|
303
|
+
args.provider,
|
|
304
|
+
args.customBetas,
|
|
305
|
+
args.strategy,
|
|
306
|
+
args.requestPath,
|
|
307
|
+
args.hasFileReferences,
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
it("always includes oauth-2025-04-20", () => {
|
|
312
|
+
const betas = callBuildBeta();
|
|
313
|
+
expect(betas.split(",")).toContain("oauth-2025-04-20");
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it("always includes claude-code-20250219 for non-haiku models", () => {
|
|
317
|
+
const betas = callBuildBeta();
|
|
318
|
+
expect(betas.split(",")).toContain("claude-code-20250219");
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it("always includes advisor-tool-2026-03-01 when experimental enabled", () => {
|
|
322
|
+
process.env.CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS = "";
|
|
323
|
+
const betas = callBuildBeta();
|
|
324
|
+
expect(betas.split(",")).toContain("advisor-tool-2026-03-01");
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it("does not auto-include advanced-tool-use or fast-mode (CC 2.1.98 doesn't send them)", () => {
|
|
328
|
+
process.env.CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS = "";
|
|
329
|
+
const betas = callBuildBeta().split(",");
|
|
330
|
+
expect(betas).not.toContain("advanced-tool-use-2025-11-20");
|
|
331
|
+
expect(betas).not.toContain("fast-mode-2026-02-01");
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it("includes token-counting-2024-11-01 for count_tokens endpoint", () => {
|
|
335
|
+
const betas = callBuildBeta({ requestPath: "/v1/messages/count_tokens" });
|
|
336
|
+
expect(betas.split(",")).toContain("token-counting-2024-11-01");
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it("omits token-counting for normal messages endpoint", () => {
|
|
340
|
+
const betas = callBuildBeta({ requestPath: "/v1/messages" });
|
|
341
|
+
expect(betas.split(",")).not.toContain("token-counting-2024-11-01");
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
it("includes files-api-2025-04-14 when request has file references", () => {
|
|
345
|
+
process.env.CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS = "";
|
|
346
|
+
const betas = callBuildBeta({ hasFileReferences: true });
|
|
347
|
+
expect(betas.split(",")).toContain("files-api-2025-04-14");
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it("includes files-api-2025-04-14 for /v1/files endpoint", () => {
|
|
351
|
+
process.env.CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS = "";
|
|
352
|
+
const betas = callBuildBeta({ requestPath: "/v1/files/upload" });
|
|
353
|
+
expect(betas.split(",")).toContain("files-api-2025-04-14");
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
it("does not duplicate betas", () => {
|
|
357
|
+
const betas = callBuildBeta({ incomingBeta: "oauth-2025-04-20" });
|
|
358
|
+
const list = betas.split(",");
|
|
359
|
+
const unique = new Set(list);
|
|
360
|
+
expect(list.length).toBe(unique.size);
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it("merges incoming betas without duplicates", () => {
|
|
364
|
+
const betas = callBuildBeta({ incomingBeta: "my-custom-beta" });
|
|
365
|
+
expect(betas.split(",")).toContain("my-custom-beta");
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
it("filters bedrock-unsupported betas for bedrock provider", () => {
|
|
369
|
+
const betas = callBuildBeta({
|
|
370
|
+
provider: "bedrock",
|
|
371
|
+
model: "claude-3-5-sonnet-20241022",
|
|
372
|
+
});
|
|
373
|
+
const list = betas.split(",");
|
|
374
|
+
expect(list).not.toContain("interleaved-thinking-2025-05-14");
|
|
375
|
+
expect(list).not.toContain("context-1m-2025-08-07");
|
|
376
|
+
expect(list).not.toContain("tool-search-tool-2025-10-19");
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
it("does not auto-include redact-thinking-2026-02-12 (removed in 2.1.98)", () => {
|
|
380
|
+
const betas = callBuildBeta().split(",");
|
|
381
|
+
expect(betas).not.toContain("redact-thinking-2026-02-12");
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
it("does not auto-include CC v2.1.98 new experimental betas", () => {
|
|
385
|
+
const betas = callBuildBeta().split(",");
|
|
386
|
+
expect(betas).not.toContain("ccr-byoc-2025-07-29");
|
|
387
|
+
expect(betas).not.toContain("ccr-triggers-2026-01-30");
|
|
388
|
+
expect(betas).not.toContain("environments-2025-11-01");
|
|
389
|
+
expect(betas).not.toContain("mcp-client-2025-11-20");
|
|
390
|
+
expect(betas).not.toContain("skills-2025-10-02");
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
it("does not include claude-code-20250219 for haiku models", () => {
|
|
394
|
+
const betas = callBuildBeta({
|
|
395
|
+
model: "claude-haiku-4-5",
|
|
396
|
+
}).split(",");
|
|
397
|
+
expect(betas).not.toContain("claude-code-20250219");
|
|
398
|
+
});
|
|
397
399
|
});
|
|
398
400
|
|
|
399
401
|
describe("CC 2.1.98 — Beta header composition (signature disabled / non-CC mode)", () => {
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
402
|
+
it("includes oauth-2025-04-20 and interleaved-thinking-2025-05-14", () => {
|
|
403
|
+
const betas = buildAnthropicBetaHeader(
|
|
404
|
+
"",
|
|
405
|
+
false,
|
|
406
|
+
"",
|
|
407
|
+
"anthropic",
|
|
408
|
+
undefined,
|
|
409
|
+
undefined,
|
|
410
|
+
undefined,
|
|
411
|
+
false,
|
|
412
|
+
).split(",");
|
|
413
|
+
expect(betas).toContain("oauth-2025-04-20");
|
|
414
|
+
expect(betas).toContain("interleaved-thinking-2025-05-14");
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
it("includes token-counting for count_tokens endpoint (non-CC mode)", () => {
|
|
418
|
+
const betas = buildAnthropicBetaHeader(
|
|
419
|
+
"",
|
|
420
|
+
false,
|
|
421
|
+
"",
|
|
422
|
+
"anthropic",
|
|
423
|
+
undefined,
|
|
424
|
+
undefined,
|
|
425
|
+
"/v1/messages/count_tokens",
|
|
426
|
+
false,
|
|
427
|
+
).split(",");
|
|
428
|
+
expect(betas).toContain("token-counting-2024-11-01");
|
|
429
|
+
});
|
|
421
430
|
});
|
|
422
431
|
|
|
423
432
|
// ---------------------------------------------------------------------------
|
|
424
433
|
// System prompt identity block
|
|
425
434
|
// ---------------------------------------------------------------------------
|
|
426
435
|
describe("CC 2.1.98 — System prompt identity string", () => {
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
436
|
+
it("CLAUDE_CODE_IDENTITY_STRING is the documented value", () => {
|
|
437
|
+
expect(CLAUDE_CODE_IDENTITY_STRING).toBe("You are Claude Code, Anthropic's official CLI for Claude.");
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
it("identity string does not include trailing period variation", () => {
|
|
441
|
+
// Exact match — no extra text
|
|
442
|
+
expect(CLAUDE_CODE_IDENTITY_STRING).not.toContain("running within the Claude Agent SDK");
|
|
443
|
+
});
|
|
435
444
|
});
|
|
436
445
|
|
|
437
446
|
describe("CC 2.1.98 — Identity block cache TTL", () => {
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
447
|
+
it("identity block has cache_control with ttl: '1h'", () => {
|
|
448
|
+
const blocks = buildSystemPromptBlocks(
|
|
449
|
+
[],
|
|
450
|
+
{ enabled: true, claudeCliVersion: "2.1.98", promptCompactionMode: "minimal" },
|
|
451
|
+
[],
|
|
452
|
+
);
|
|
453
|
+
|
|
454
|
+
const identityBlock = blocks.find((b) => b.text === CLAUDE_CODE_IDENTITY_STRING);
|
|
455
|
+
expect(identityBlock).toBeDefined();
|
|
456
|
+
expect(identityBlock!.cache_control).toBeDefined();
|
|
457
|
+
expect(identityBlock!.cache_control!.type).toBe("ephemeral");
|
|
458
|
+
// CC 2.1.98 sends only {type:"ephemeral"} — no scope or ttl
|
|
459
|
+
expect(identityBlock!.cache_control!.scope).toBeUndefined();
|
|
460
|
+
expect(identityBlock!.cache_control!.ttl).toBeUndefined();
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
it("billing header block does NOT have cache_control", () => {
|
|
464
|
+
const blocks = buildSystemPromptBlocks(
|
|
465
|
+
[],
|
|
466
|
+
{ enabled: true, claudeCliVersion: "2.1.98", promptCompactionMode: "minimal" },
|
|
467
|
+
[],
|
|
468
|
+
);
|
|
469
|
+
|
|
470
|
+
const billingBlock = blocks.find((b) => b.type === "text" && b.text.startsWith("x-anthropic-billing-header:"));
|
|
471
|
+
expect(billingBlock).toBeDefined();
|
|
472
|
+
expect(billingBlock!.cache_control).toBeUndefined();
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
it("user-provided system blocks do NOT have cache_control", () => {
|
|
476
|
+
const blocks = buildSystemPromptBlocks(
|
|
477
|
+
[{ type: "text", text: "Custom system prompt" }],
|
|
478
|
+
{ enabled: true, claudeCliVersion: "2.1.98", promptCompactionMode: "minimal" },
|
|
479
|
+
[],
|
|
480
|
+
);
|
|
481
|
+
|
|
482
|
+
const userBlock = blocks.find((b) => b.text === "Custom system prompt");
|
|
483
|
+
expect(userBlock).toBeDefined();
|
|
484
|
+
expect(userBlock!.cache_control).toBeUndefined();
|
|
485
|
+
});
|
|
477
486
|
});
|
|
478
487
|
|
|
479
488
|
// ---------------------------------------------------------------------------
|
|
480
489
|
// Sonnet 4.6 adaptive thinking (CC 2.1.98)
|
|
481
490
|
// ---------------------------------------------------------------------------
|
|
482
491
|
describe("Sonnet 4.6 — Adaptive thinking model detection", () => {
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
492
|
+
it("isSonnet46Model detects claude-sonnet-4-6", () => {
|
|
493
|
+
expect(isSonnet46Model("claude-sonnet-4-6")).toBe(true);
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
it("isSonnet46Model detects claude-sonnet-4.6", () => {
|
|
497
|
+
expect(isSonnet46Model("claude-sonnet-4.6")).toBe(true);
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
it("isSonnet46Model detects sonnet-4-6", () => {
|
|
501
|
+
expect(isSonnet46Model("sonnet-4-6")).toBe(true);
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
it("isSonnet46Model returns false for non-Sonnet 4.6 models", () => {
|
|
505
|
+
expect(isSonnet46Model("claude-3-5-sonnet-20241022")).toBe(false);
|
|
506
|
+
expect(isSonnet46Model("claude-opus-4-6")).toBe(false);
|
|
507
|
+
expect(isSonnet46Model("claude-haiku-4-5")).toBe(false);
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
it("isAdaptiveThinkingModel returns true for Sonnet 4.6", () => {
|
|
511
|
+
expect(isAdaptiveThinkingModel("claude-sonnet-4-6")).toBe(true);
|
|
512
|
+
expect(isAdaptiveThinkingModel("sonnet-4-6")).toBe(true);
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
it("isAdaptiveThinkingModel returns true for Opus 4.6", () => {
|
|
516
|
+
expect(isAdaptiveThinkingModel("claude-opus-4-6")).toBe(true);
|
|
517
|
+
expect(isAdaptiveThinkingModel("opus-4-6")).toBe(true);
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
it("isAdaptiveThinkingModel returns false for non-adaptive thinking models", () => {
|
|
521
|
+
expect(isAdaptiveThinkingModel("claude-3-5-sonnet-20241022")).toBe(false);
|
|
522
|
+
expect(isAdaptiveThinkingModel("claude-haiku-4-5")).toBe(false);
|
|
523
|
+
});
|
|
515
524
|
});
|
|
516
525
|
|
|
517
526
|
describe("Sonnet 4.6 — Beta header includes effort-2025-11-24", () => {
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
527
|
+
it("includes effort-2025-11-24 for claude-sonnet-4-6", () => {
|
|
528
|
+
const betas = buildAnthropicBetaHeader(
|
|
529
|
+
"",
|
|
530
|
+
true,
|
|
531
|
+
"claude-sonnet-4-6",
|
|
532
|
+
"anthropic",
|
|
533
|
+
undefined,
|
|
534
|
+
undefined,
|
|
535
|
+
undefined,
|
|
536
|
+
false,
|
|
537
|
+
).split(",");
|
|
538
|
+
expect(betas).toContain(EFFORT_BETA_FLAG);
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
it("includes effort-2025-11-24 for sonnet-4-6", () => {
|
|
542
|
+
const betas = buildAnthropicBetaHeader(
|
|
543
|
+
"",
|
|
544
|
+
true,
|
|
545
|
+
"sonnet-4-6",
|
|
546
|
+
"anthropic",
|
|
547
|
+
undefined,
|
|
548
|
+
undefined,
|
|
549
|
+
undefined,
|
|
550
|
+
false,
|
|
551
|
+
).split(",");
|
|
552
|
+
expect(betas).toContain(EFFORT_BETA_FLAG);
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
it("does not include effort-2025-11-24 for claude-3-5-sonnet", () => {
|
|
556
|
+
const betas = buildAnthropicBetaHeader(
|
|
557
|
+
"",
|
|
558
|
+
true,
|
|
559
|
+
"claude-3-5-sonnet-20241022",
|
|
560
|
+
"anthropic",
|
|
561
|
+
undefined,
|
|
562
|
+
undefined,
|
|
563
|
+
undefined,
|
|
564
|
+
false,
|
|
565
|
+
).split(",");
|
|
566
|
+
expect(betas).not.toContain(EFFORT_BETA_FLAG);
|
|
567
|
+
});
|
|
559
568
|
});
|
|
560
569
|
|
|
561
570
|
describe("Sonnet 4.6 — Thinking block normalization", () => {
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
571
|
+
it("normalizes budget_tokens to effort for Sonnet 4.6", () => {
|
|
572
|
+
const result = normalizeThinkingBlock({ type: "enabled", budget_tokens: 8000 }, "claude-sonnet-4-6");
|
|
573
|
+
expect(result).toEqual({ type: "enabled", effort: "medium" });
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
it("preserves existing effort for Sonnet 4.6", () => {
|
|
577
|
+
const result = normalizeThinkingBlock({ type: "enabled", effort: "high" }, "claude-sonnet-4-6");
|
|
578
|
+
expect(result).toEqual({ type: "enabled", effort: "high" });
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
it("passes through thinking block unchanged for non-adaptive models", () => {
|
|
582
|
+
const input = { type: "enabled", budget_tokens: 8000 };
|
|
583
|
+
const result = normalizeThinkingBlock(input, "claude-3-5-sonnet-20241022");
|
|
584
|
+
expect(result).toEqual(input);
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
it("maps low budget_tokens to low effort for Sonnet 4.6", () => {
|
|
588
|
+
const result = normalizeThinkingBlock({ type: "enabled", budget_tokens: 500 }, "claude-sonnet-4-6");
|
|
589
|
+
expect(result).toEqual({ type: "enabled", effort: "low" });
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
it("maps high budget_tokens to high effort for Sonnet 4.6", () => {
|
|
593
|
+
const result = normalizeThinkingBlock({ type: "enabled", budget_tokens: 20000 }, "claude-sonnet-4-6");
|
|
594
|
+
expect(result).toEqual({ type: "enabled", effort: "high" });
|
|
595
|
+
});
|
|
587
596
|
});
|
|
588
597
|
|
|
589
598
|
// ---------------------------------------------------------------------------
|
|
590
599
|
// Speed parameter passthrough (Opus 4.6 fast mode)
|
|
591
600
|
// ---------------------------------------------------------------------------
|
|
592
601
|
describe("Speed parameter passthrough", () => {
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
it("preserves speed: 'normal' in request body", () => {
|
|
608
|
-
const body = JSON.stringify({
|
|
609
|
-
model: "claude-opus-4-6",
|
|
610
|
-
messages: [{ role: "user", content: "Hello" }],
|
|
611
|
-
speed: "normal",
|
|
612
|
-
});
|
|
613
|
-
const result = transformRequestBody(body, mockSignature, mockRuntime);
|
|
614
|
-
const parsed = JSON.parse(result!);
|
|
615
|
-
expect(parsed.speed).toBe("normal");
|
|
616
|
-
});
|
|
617
|
-
|
|
618
|
-
it("does not inject speed when not provided", () => {
|
|
619
|
-
const body = JSON.stringify({
|
|
620
|
-
model: "claude-opus-4-6",
|
|
621
|
-
messages: [{ role: "user", content: "Hello" }],
|
|
622
|
-
});
|
|
623
|
-
const result = transformRequestBody(body, mockSignature, mockRuntime);
|
|
624
|
-
const parsed = JSON.parse(result!);
|
|
625
|
-
expect(parsed.speed).toBeUndefined();
|
|
626
|
-
});
|
|
627
|
-
|
|
628
|
-
it("preserves speed alongside other fields", () => {
|
|
629
|
-
const body = JSON.stringify({
|
|
630
|
-
model: "claude-opus-4-6",
|
|
631
|
-
messages: [{ role: "user", content: "Hello" }],
|
|
632
|
-
speed: "fast",
|
|
633
|
-
thinking: { type: "enabled", effort: "high" },
|
|
634
|
-
system: "You are helpful.",
|
|
635
|
-
});
|
|
636
|
-
const result = transformRequestBody(body, mockSignature, mockRuntime);
|
|
637
|
-
const parsed = JSON.parse(result!);
|
|
638
|
-
expect(parsed.speed).toBe("fast");
|
|
639
|
-
expect(parsed.thinking).toEqual({ type: "enabled", effort: "high" });
|
|
640
|
-
expect(parsed.system).toBeDefined();
|
|
641
|
-
});
|
|
642
|
-
});
|
|
602
|
+
const mockSignature = { enabled: false, claudeCliVersion: "2.1.98", promptCompactionMode: "minimal" as const };
|
|
603
|
+
const mockRuntime = { persistentUserId: "", accountId: "", sessionId: "" };
|
|
604
|
+
|
|
605
|
+
it("preserves speed: 'fast' in request body", () => {
|
|
606
|
+
const body = JSON.stringify({
|
|
607
|
+
model: "claude-opus-4-6",
|
|
608
|
+
messages: [{ role: "user", content: "Hello" }],
|
|
609
|
+
speed: "fast",
|
|
610
|
+
});
|
|
611
|
+
const result = transformRequestBody(body, mockSignature, mockRuntime);
|
|
612
|
+
const parsed = JSON.parse(result!);
|
|
613
|
+
expect(parsed.speed).toBe("fast");
|
|
614
|
+
});
|
|
643
615
|
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
616
|
+
it("preserves speed: 'normal' in request body", () => {
|
|
617
|
+
const body = JSON.stringify({
|
|
618
|
+
model: "claude-opus-4-6",
|
|
619
|
+
messages: [{ role: "user", content: "Hello" }],
|
|
620
|
+
speed: "normal",
|
|
621
|
+
});
|
|
622
|
+
const result = transformRequestBody(body, mockSignature, mockRuntime);
|
|
623
|
+
const parsed = JSON.parse(result!);
|
|
624
|
+
expect(parsed.speed).toBe("normal");
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
it("does not inject speed when not provided", () => {
|
|
628
|
+
const body = JSON.stringify({
|
|
629
|
+
model: "claude-opus-4-6",
|
|
630
|
+
messages: [{ role: "user", content: "Hello" }],
|
|
631
|
+
});
|
|
632
|
+
const result = transformRequestBody(body, mockSignature, mockRuntime);
|
|
633
|
+
const parsed = JSON.parse(result!);
|
|
634
|
+
expect(parsed.speed).toBeUndefined();
|
|
635
|
+
});
|
|
647
636
|
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
637
|
+
it("preserves speed alongside other fields", () => {
|
|
638
|
+
const body = JSON.stringify({
|
|
639
|
+
model: "claude-opus-4-6",
|
|
640
|
+
messages: [{ role: "user", content: "Hello" }],
|
|
641
|
+
speed: "fast",
|
|
642
|
+
thinking: { type: "enabled", effort: "high" },
|
|
643
|
+
system: "You are helpful.",
|
|
644
|
+
});
|
|
645
|
+
const result = transformRequestBody(body, mockSignature, mockRuntime);
|
|
646
|
+
const parsed = JSON.parse(result!);
|
|
647
|
+
expect(parsed.speed).toBe("fast");
|
|
648
|
+
expect(parsed.thinking).toEqual({ type: "enabled", effort: "high" });
|
|
649
|
+
expect(parsed.system).toBeDefined();
|
|
652
650
|
});
|
|
651
|
+
});
|
|
653
652
|
|
|
654
|
-
|
|
655
|
-
const
|
|
653
|
+
describe("Temperature normalization", () => {
|
|
654
|
+
const mockSignature = { enabled: false, claudeCliVersion: "2.1.98", promptCompactionMode: "minimal" as const };
|
|
655
|
+
const mockRuntime = { persistentUserId: "", accountId: "", sessionId: "" };
|
|
656
|
+
|
|
657
|
+
it("defaults temperature to 1 for non-thinking requests", () => {
|
|
658
|
+
const body = JSON.stringify({
|
|
659
|
+
model: "claude-3-5-sonnet-20241022",
|
|
660
|
+
messages: [{ role: "user", content: "Hello" }],
|
|
661
|
+
});
|
|
656
662
|
|
|
657
|
-
|
|
658
|
-
|
|
663
|
+
const result = transformRequestBody(body, mockSignature, mockRuntime);
|
|
664
|
+
const parsed = JSON.parse(result!);
|
|
659
665
|
|
|
660
|
-
|
|
661
|
-
const body = JSON.stringify({
|
|
662
|
-
model: "claude-3-5-sonnet-20241022",
|
|
663
|
-
messages: [{ role: "user", content: "Hello" }],
|
|
664
|
-
temperature: 0.7,
|
|
665
|
-
thinking: { type: "enabled", budget_tokens: 8000 },
|
|
666
|
+
expect(parsed.temperature).toBe(1);
|
|
666
667
|
});
|
|
667
668
|
|
|
668
|
-
|
|
669
|
-
|
|
669
|
+
it("omits temperature when thinking is enabled", () => {
|
|
670
|
+
const body = JSON.stringify({
|
|
671
|
+
model: "claude-3-5-sonnet-20241022",
|
|
672
|
+
messages: [{ role: "user", content: "Hello" }],
|
|
673
|
+
temperature: 0.7,
|
|
674
|
+
thinking: { type: "enabled", budget_tokens: 8000 },
|
|
675
|
+
});
|
|
670
676
|
|
|
671
|
-
|
|
672
|
-
|
|
677
|
+
const result = transformRequestBody(body, mockSignature, mockRuntime);
|
|
678
|
+
const parsed = JSON.parse(result!);
|
|
673
679
|
|
|
674
|
-
|
|
675
|
-
const body = JSON.stringify({
|
|
676
|
-
model: "claude-3-5-sonnet-20241022",
|
|
677
|
-
messages: [{ role: "user", content: "Hello" }],
|
|
678
|
-
temperature: 0.7,
|
|
680
|
+
expect(Object.prototype.hasOwnProperty.call(parsed, "temperature")).toBe(false);
|
|
679
681
|
});
|
|
680
682
|
|
|
681
|
-
|
|
682
|
-
|
|
683
|
+
it("preserves explicit caller temperature for non-thinking requests", () => {
|
|
684
|
+
const body = JSON.stringify({
|
|
685
|
+
model: "claude-3-5-sonnet-20241022",
|
|
686
|
+
messages: [{ role: "user", content: "Hello" }],
|
|
687
|
+
temperature: 0.7,
|
|
688
|
+
});
|
|
683
689
|
|
|
684
|
-
|
|
685
|
-
|
|
690
|
+
const result = transformRequestBody(body, mockSignature, mockRuntime);
|
|
691
|
+
const parsed = JSON.parse(result!);
|
|
692
|
+
|
|
693
|
+
expect(parsed.temperature).toBe(0.7);
|
|
694
|
+
});
|
|
686
695
|
});
|