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