@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
package/src/config.ts CHANGED
@@ -5,122 +5,122 @@ import { dirname, join } from "node:path";
5
5
  export type AccountSelectionStrategy = "sticky" | "round-robin" | "hybrid";
6
6
 
7
7
  export interface HealthScoreConfig {
8
- initial: number;
9
- success_reward: number;
10
- rate_limit_penalty: number;
11
- failure_penalty: number;
12
- recovery_rate_per_hour: number;
13
- min_usable: number;
14
- max_score: number;
8
+ initial: number;
9
+ success_reward: number;
10
+ rate_limit_penalty: number;
11
+ failure_penalty: number;
12
+ recovery_rate_per_hour: number;
13
+ min_usable: number;
14
+ max_score: number;
15
15
  }
16
16
 
17
17
  export interface TokenBucketConfig {
18
- max_tokens: number;
19
- regeneration_rate_per_minute: number;
20
- initial_tokens: number;
18
+ max_tokens: number;
19
+ regeneration_rate_per_minute: number;
20
+ initial_tokens: number;
21
21
  }
22
22
 
23
23
  export interface ToastConfig {
24
- /** Suppress non-error toasts */
25
- quiet: boolean;
26
- /** Minimum seconds between account-switch toasts */
27
- debounce_seconds: number;
24
+ /** Suppress non-error toasts */
25
+ quiet: boolean;
26
+ /** Minimum seconds between account-switch toasts */
27
+ debounce_seconds: number;
28
28
  }
29
29
 
30
30
  export interface OverrideModelLimitsConfig {
31
- /** When true, overrides model context limits for 1M-window models */
32
- enabled: boolean;
33
- /** Context window size to inject (tokens). Default: 1_000_000 */
34
- context: number;
35
- /** Max output tokens to inject. 0 = leave model default unchanged */
36
- output: number;
31
+ /** When true, overrides model context limits for 1M-window models */
32
+ enabled: boolean;
33
+ /** Context window size to inject (tokens). Default: 1_000_000 */
34
+ context: number;
35
+ /** Max output tokens to inject. 0 = leave model default unchanged */
36
+ output: number;
37
37
  }
38
38
 
39
39
  export interface IdleRefreshConfig {
40
- /** Opportunistically refresh near-expiry idle accounts */
41
- enabled: boolean;
42
- /** Refresh idle accounts within this many minutes of expiry */
43
- window_minutes: number;
44
- /** Minimum minutes between idle refresh attempts per account */
45
- min_interval_minutes: number;
40
+ /** Opportunistically refresh near-expiry idle accounts */
41
+ enabled: boolean;
42
+ /** Refresh idle accounts within this many minutes of expiry */
43
+ window_minutes: number;
44
+ /** Minimum minutes between idle refresh attempts per account */
45
+ min_interval_minutes: number;
46
46
  }
47
47
 
48
48
  export interface HeaderConfig {
49
- emulation_profile?: string;
50
- overrides?: Record<string, string>;
51
- disable?: string[];
52
- billing_header?: boolean;
49
+ emulation_profile?: string;
50
+ overrides?: Record<string, string>;
51
+ disable?: string[];
52
+ billing_header?: boolean;
53
53
  }
54
54
 
55
55
  export interface AnthropicAuthConfig {
56
- account_selection_strategy: AccountSelectionStrategy;
57
- failure_ttl_seconds: number;
58
- debug: boolean;
59
- signature_emulation: {
60
- enabled: boolean;
61
- fetch_claude_code_version_on_startup: boolean;
62
- prompt_compaction: "minimal" | "off";
63
- sanitize_system_prompt: boolean;
64
- };
65
- override_model_limits: OverrideModelLimitsConfig;
66
- custom_betas: string[];
67
- health_score: HealthScoreConfig;
68
- token_bucket: TokenBucketConfig;
69
- toasts: ToastConfig;
70
- headers: HeaderConfig;
71
- idle_refresh: IdleRefreshConfig;
72
- cc_credential_reuse: {
73
- enabled: boolean;
74
- auto_detect: boolean;
75
- prefer_over_oauth: boolean;
76
- };
56
+ account_selection_strategy: AccountSelectionStrategy;
57
+ failure_ttl_seconds: number;
58
+ debug: boolean;
59
+ signature_emulation: {
60
+ enabled: boolean;
61
+ fetch_claude_code_version_on_startup: boolean;
62
+ prompt_compaction: "minimal" | "off";
63
+ sanitize_system_prompt: boolean;
64
+ };
65
+ override_model_limits: OverrideModelLimitsConfig;
66
+ custom_betas: string[];
67
+ health_score: HealthScoreConfig;
68
+ token_bucket: TokenBucketConfig;
69
+ toasts: ToastConfig;
70
+ headers: HeaderConfig;
71
+ idle_refresh: IdleRefreshConfig;
72
+ cc_credential_reuse: {
73
+ enabled: boolean;
74
+ auto_detect: boolean;
75
+ prefer_over_oauth: boolean;
76
+ };
77
77
  }
78
78
 
79
79
  export const DEFAULT_CONFIG: AnthropicAuthConfig = {
80
- account_selection_strategy: "sticky",
81
- failure_ttl_seconds: 3600,
82
- debug: false,
83
- signature_emulation: {
84
- enabled: true,
85
- fetch_claude_code_version_on_startup: true,
86
- prompt_compaction: "minimal",
87
- sanitize_system_prompt: false,
88
- },
89
- override_model_limits: {
90
- enabled: true,
91
- context: 1_000_000,
92
- output: 0,
93
- },
94
- custom_betas: [],
95
- health_score: {
96
- initial: 70,
97
- success_reward: 1,
98
- rate_limit_penalty: -10,
99
- failure_penalty: -20,
100
- recovery_rate_per_hour: 2,
101
- min_usable: 50,
102
- max_score: 100,
103
- },
104
- token_bucket: {
105
- max_tokens: 50,
106
- regeneration_rate_per_minute: 6,
107
- initial_tokens: 50,
108
- },
109
- toasts: {
110
- quiet: false,
111
- debounce_seconds: 30,
112
- },
113
- headers: {},
114
- idle_refresh: {
115
- enabled: true,
116
- window_minutes: 60,
117
- min_interval_minutes: 30,
118
- },
119
- cc_credential_reuse: {
120
- enabled: true,
121
- auto_detect: true,
122
- prefer_over_oauth: true,
123
- },
80
+ account_selection_strategy: "sticky",
81
+ failure_ttl_seconds: 3600,
82
+ debug: false,
83
+ signature_emulation: {
84
+ enabled: true,
85
+ fetch_claude_code_version_on_startup: true,
86
+ prompt_compaction: "minimal",
87
+ sanitize_system_prompt: false,
88
+ },
89
+ override_model_limits: {
90
+ enabled: true,
91
+ context: 1_000_000,
92
+ output: 0,
93
+ },
94
+ custom_betas: [],
95
+ health_score: {
96
+ initial: 70,
97
+ success_reward: 1,
98
+ rate_limit_penalty: -10,
99
+ failure_penalty: -20,
100
+ recovery_rate_per_hour: 2,
101
+ min_usable: 50,
102
+ max_score: 100,
103
+ },
104
+ token_bucket: {
105
+ max_tokens: 50,
106
+ regeneration_rate_per_minute: 6,
107
+ initial_tokens: 50,
108
+ },
109
+ toasts: {
110
+ quiet: false,
111
+ debounce_seconds: 30,
112
+ },
113
+ headers: {},
114
+ idle_refresh: {
115
+ enabled: true,
116
+ window_minutes: 60,
117
+ min_interval_minutes: 30,
118
+ },
119
+ cc_credential_reuse: {
120
+ enabled: true,
121
+ auto_detect: true,
122
+ prefer_over_oauth: true,
123
+ },
124
124
  };
125
125
 
126
126
  export const VALID_STRATEGIES: AccountSelectionStrategy[] = ["sticky", "round-robin", "hybrid"];
@@ -129,311 +129,319 @@ export const VALID_STRATEGIES: AccountSelectionStrategy[] = ["sticky", "round-ro
129
129
  export const CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
130
130
 
131
131
  function createDefaultConfig(): AnthropicAuthConfig {
132
- return {
133
- ...DEFAULT_CONFIG,
134
- signature_emulation: { ...DEFAULT_CONFIG.signature_emulation },
135
- override_model_limits: { ...DEFAULT_CONFIG.override_model_limits },
136
- custom_betas: [...DEFAULT_CONFIG.custom_betas],
137
- health_score: { ...DEFAULT_CONFIG.health_score },
138
- token_bucket: { ...DEFAULT_CONFIG.token_bucket },
139
- toasts: { ...DEFAULT_CONFIG.toasts },
140
- headers: {},
141
- idle_refresh: { ...DEFAULT_CONFIG.idle_refresh },
142
- cc_credential_reuse: { ...DEFAULT_CONFIG.cc_credential_reuse },
143
- };
132
+ return {
133
+ ...DEFAULT_CONFIG,
134
+ signature_emulation: { ...DEFAULT_CONFIG.signature_emulation },
135
+ override_model_limits: { ...DEFAULT_CONFIG.override_model_limits },
136
+ custom_betas: [...DEFAULT_CONFIG.custom_betas],
137
+ health_score: { ...DEFAULT_CONFIG.health_score },
138
+ token_bucket: { ...DEFAULT_CONFIG.token_bucket },
139
+ toasts: { ...DEFAULT_CONFIG.toasts },
140
+ headers: {},
141
+ idle_refresh: { ...DEFAULT_CONFIG.idle_refresh },
142
+ cc_credential_reuse: { ...DEFAULT_CONFIG.cc_credential_reuse },
143
+ };
144
144
  }
145
145
 
146
146
  /**
147
147
  * Get the OpenCode config directory (XDG-compliant).
148
148
  */
149
149
  export function getConfigDir(): string {
150
- const platform = process.platform;
151
- if (platform === "win32") {
152
- return join(process.env.APPDATA || join(homedir(), "AppData", "Roaming"), "opencode");
153
- }
154
- const xdgConfig = process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
155
- return join(xdgConfig, "opencode");
150
+ const platform = process.platform;
151
+ if (platform === "win32") {
152
+ return join(process.env.APPDATA || join(homedir(), "AppData", "Roaming"), "opencode");
153
+ }
154
+ const xdgConfig = process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
155
+ return join(xdgConfig, "opencode");
156
156
  }
157
157
 
158
158
  /**
159
159
  * Get the path to the config file.
160
160
  */
161
161
  export function getConfigPath(): string {
162
- return join(getConfigDir(), "anthropic-auth.json");
162
+ return join(getConfigDir(), "anthropic-auth.json");
163
163
  }
164
164
 
165
165
  function clampNumber(value: unknown, min: number, max: number, fallback: number): number {
166
- if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
167
- return Math.max(min, Math.min(max, value));
166
+ if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
167
+ return Math.max(min, Math.min(max, value));
168
168
  }
169
169
 
170
170
  function validateConfig(raw: Record<string, unknown>): AnthropicAuthConfig {
171
- const config = createDefaultConfig();
172
-
173
- if (
174
- typeof raw.account_selection_strategy === "string" &&
175
- VALID_STRATEGIES.includes(raw.account_selection_strategy as AccountSelectionStrategy)
176
- ) {
177
- config.account_selection_strategy = raw.account_selection_strategy as AccountSelectionStrategy;
178
- }
179
-
180
- config.failure_ttl_seconds = clampNumber(raw.failure_ttl_seconds, 60, 7200, DEFAULT_CONFIG.failure_ttl_seconds);
181
-
182
- if (typeof raw.debug === "boolean") {
183
- config.debug = raw.debug;
184
- }
185
-
186
- if (raw.signature_emulation && typeof raw.signature_emulation === "object") {
187
- const se = raw.signature_emulation as Record<string, unknown>;
188
- config.signature_emulation = {
189
- enabled: typeof se.enabled === "boolean" ? se.enabled : DEFAULT_CONFIG.signature_emulation.enabled,
190
- fetch_claude_code_version_on_startup:
191
- typeof se.fetch_claude_code_version_on_startup === "boolean"
192
- ? se.fetch_claude_code_version_on_startup
193
- : DEFAULT_CONFIG.signature_emulation.fetch_claude_code_version_on_startup,
194
- prompt_compaction:
195
- se.prompt_compaction === "off" || se.prompt_compaction === "minimal"
196
- ? se.prompt_compaction
197
- : DEFAULT_CONFIG.signature_emulation.prompt_compaction,
198
- sanitize_system_prompt:
199
- typeof se.sanitize_system_prompt === "boolean"
200
- ? se.sanitize_system_prompt
201
- : DEFAULT_CONFIG.signature_emulation.sanitize_system_prompt,
202
- };
203
- }
204
-
205
- // Top-level alias: `sanitize_system_prompt` is honored as a convenience so
206
- // users can flip it on/off without learning the nested signature_emulation
207
- // schema. The top-level value, when set, takes precedence over the nested
208
- // one because it's the more specific user intent.
209
- if (typeof raw.sanitize_system_prompt === "boolean") {
210
- config.signature_emulation.sanitize_system_prompt = raw.sanitize_system_prompt;
211
- }
212
-
213
- if (raw.override_model_limits && typeof raw.override_model_limits === "object") {
214
- const oml = raw.override_model_limits as Record<string, unknown>;
215
- config.override_model_limits = {
216
- enabled: typeof oml.enabled === "boolean" ? oml.enabled : DEFAULT_CONFIG.override_model_limits.enabled,
217
- context: clampNumber(oml.context, 200_000, 2_000_000, DEFAULT_CONFIG.override_model_limits.context),
218
- output: clampNumber(oml.output, 0, 128_000, DEFAULT_CONFIG.override_model_limits.output),
219
- };
220
- }
221
-
222
- if (Array.isArray(raw.custom_betas)) {
223
- config.custom_betas = (raw.custom_betas as unknown[])
224
- .filter((b): b is string => typeof b === "string" && b.trim().length > 0)
225
- .map((b) => b.trim());
226
- }
227
-
228
- if (raw.health_score && typeof raw.health_score === "object") {
229
- const hs = raw.health_score as Record<string, unknown>;
230
- config.health_score = {
231
- initial: clampNumber(hs.initial, 0, 100, DEFAULT_CONFIG.health_score.initial),
232
- success_reward: clampNumber(hs.success_reward, 0, 10, DEFAULT_CONFIG.health_score.success_reward),
233
- rate_limit_penalty: clampNumber(hs.rate_limit_penalty, -50, 0, DEFAULT_CONFIG.health_score.rate_limit_penalty),
234
- failure_penalty: clampNumber(hs.failure_penalty, -100, 0, DEFAULT_CONFIG.health_score.failure_penalty),
235
- recovery_rate_per_hour: clampNumber(
236
- hs.recovery_rate_per_hour,
237
- 0,
238
- 20,
239
- DEFAULT_CONFIG.health_score.recovery_rate_per_hour,
240
- ),
241
- min_usable: clampNumber(hs.min_usable, 0, 100, DEFAULT_CONFIG.health_score.min_usable),
242
- max_score: clampNumber(hs.max_score, 50, 100, DEFAULT_CONFIG.health_score.max_score),
243
- };
244
- }
171
+ const config = createDefaultConfig();
245
172
 
246
- if (raw.toasts && typeof raw.toasts === "object") {
247
- const t = raw.toasts as Record<string, unknown>;
248
- config.toasts = {
249
- quiet: typeof t.quiet === "boolean" ? t.quiet : DEFAULT_CONFIG.toasts.quiet,
250
- debounce_seconds: clampNumber(t.debounce_seconds, 0, 300, DEFAULT_CONFIG.toasts.debounce_seconds),
251
- };
252
- }
253
-
254
- if (raw.token_bucket && typeof raw.token_bucket === "object") {
255
- const tb = raw.token_bucket as Record<string, unknown>;
256
- config.token_bucket = {
257
- max_tokens: clampNumber(tb.max_tokens, 1, 1000, DEFAULT_CONFIG.token_bucket.max_tokens),
258
- regeneration_rate_per_minute: clampNumber(
259
- tb.regeneration_rate_per_minute,
260
- 0.1,
261
- 60,
262
- DEFAULT_CONFIG.token_bucket.regeneration_rate_per_minute,
263
- ),
264
- initial_tokens: clampNumber(tb.initial_tokens, 1, 1000, DEFAULT_CONFIG.token_bucket.initial_tokens),
265
- };
266
- }
173
+ if (
174
+ typeof raw.account_selection_strategy === "string" &&
175
+ VALID_STRATEGIES.includes(raw.account_selection_strategy as AccountSelectionStrategy)
176
+ ) {
177
+ config.account_selection_strategy = raw.account_selection_strategy as AccountSelectionStrategy;
178
+ }
267
179
 
268
- if (raw.headers && typeof raw.headers === "object") {
269
- const h = raw.headers as Record<string, unknown>;
180
+ config.failure_ttl_seconds = clampNumber(raw.failure_ttl_seconds, 60, 7200, DEFAULT_CONFIG.failure_ttl_seconds);
270
181
 
271
- if (typeof h.emulation_profile === "string" && h.emulation_profile.trim()) {
272
- config.headers.emulation_profile = h.emulation_profile.trim();
182
+ if (typeof raw.debug === "boolean") {
183
+ config.debug = raw.debug;
273
184
  }
274
185
 
275
- if (h.overrides && typeof h.overrides === "object" && !Array.isArray(h.overrides)) {
276
- const overrides: Record<string, string> = {};
277
- for (const [key, value] of Object.entries(h.overrides as Record<string, unknown>)) {
278
- if (!key) continue;
279
- if (typeof value === "string") {
280
- overrides[key] = value;
281
- }
282
- }
283
- config.headers.overrides = overrides;
186
+ if (raw.signature_emulation && typeof raw.signature_emulation === "object") {
187
+ const se = raw.signature_emulation as Record<string, unknown>;
188
+ config.signature_emulation = {
189
+ enabled: typeof se.enabled === "boolean" ? se.enabled : DEFAULT_CONFIG.signature_emulation.enabled,
190
+ fetch_claude_code_version_on_startup:
191
+ typeof se.fetch_claude_code_version_on_startup === "boolean"
192
+ ? se.fetch_claude_code_version_on_startup
193
+ : DEFAULT_CONFIG.signature_emulation.fetch_claude_code_version_on_startup,
194
+ prompt_compaction:
195
+ se.prompt_compaction === "off" || se.prompt_compaction === "minimal"
196
+ ? se.prompt_compaction
197
+ : DEFAULT_CONFIG.signature_emulation.prompt_compaction,
198
+ sanitize_system_prompt:
199
+ typeof se.sanitize_system_prompt === "boolean"
200
+ ? se.sanitize_system_prompt
201
+ : DEFAULT_CONFIG.signature_emulation.sanitize_system_prompt,
202
+ };
284
203
  }
285
204
 
286
- if (Array.isArray(h.disable)) {
287
- config.headers.disable = (h.disable as unknown[])
288
- .filter((v): v is string => typeof v === "string")
289
- .map((v) => v.trim().toLowerCase())
290
- .filter(Boolean);
205
+ // Top-level alias: `sanitize_system_prompt` is honored as a convenience so
206
+ // users can flip it on/off without learning the nested signature_emulation
207
+ // schema. The top-level value, when set, takes precedence over the nested
208
+ // one because it's the more specific user intent.
209
+ if (typeof raw.sanitize_system_prompt === "boolean") {
210
+ config.signature_emulation.sanitize_system_prompt = raw.sanitize_system_prompt;
291
211
  }
292
212
 
293
- if (typeof h.billing_header === "boolean") {
294
- config.headers.billing_header = h.billing_header;
213
+ if (raw.override_model_limits && typeof raw.override_model_limits === "object") {
214
+ const oml = raw.override_model_limits as Record<string, unknown>;
215
+ config.override_model_limits = {
216
+ enabled: typeof oml.enabled === "boolean" ? oml.enabled : DEFAULT_CONFIG.override_model_limits.enabled,
217
+ context: clampNumber(oml.context, 200_000, 2_000_000, DEFAULT_CONFIG.override_model_limits.context),
218
+ output: clampNumber(oml.output, 0, 128_000, DEFAULT_CONFIG.override_model_limits.output),
219
+ };
295
220
  }
296
- }
297
221
 
298
- if (raw.idle_refresh && typeof raw.idle_refresh === "object") {
299
- const ir = raw.idle_refresh as Record<string, unknown>;
300
- config.idle_refresh = {
301
- enabled: typeof ir.enabled === "boolean" ? ir.enabled : DEFAULT_CONFIG.idle_refresh.enabled,
302
- window_minutes: clampNumber(ir.window_minutes, 1, 24 * 60, DEFAULT_CONFIG.idle_refresh.window_minutes),
303
- min_interval_minutes: clampNumber(
304
- ir.min_interval_minutes,
305
- 1,
306
- 24 * 60,
307
- DEFAULT_CONFIG.idle_refresh.min_interval_minutes,
308
- ),
309
- };
310
- }
311
-
312
- if (raw.cc_credential_reuse && typeof raw.cc_credential_reuse === "object") {
313
- const ccr = raw.cc_credential_reuse as Record<string, unknown>;
314
- config.cc_credential_reuse = {
315
- enabled: typeof ccr.enabled === "boolean" ? ccr.enabled : DEFAULT_CONFIG.cc_credential_reuse.enabled,
316
- auto_detect:
317
- typeof ccr.auto_detect === "boolean" ? ccr.auto_detect : DEFAULT_CONFIG.cc_credential_reuse.auto_detect,
318
- prefer_over_oauth:
319
- typeof ccr.prefer_over_oauth === "boolean"
320
- ? ccr.prefer_over_oauth
321
- : DEFAULT_CONFIG.cc_credential_reuse.prefer_over_oauth,
322
- };
323
- }
222
+ if (Array.isArray(raw.custom_betas)) {
223
+ config.custom_betas = (raw.custom_betas as unknown[])
224
+ .filter((b): b is string => typeof b === "string" && b.trim().length > 0)
225
+ .map((b) => b.trim());
226
+ }
227
+
228
+ if (raw.health_score && typeof raw.health_score === "object") {
229
+ const hs = raw.health_score as Record<string, unknown>;
230
+ config.health_score = {
231
+ initial: clampNumber(hs.initial, 0, 100, DEFAULT_CONFIG.health_score.initial),
232
+ success_reward: clampNumber(hs.success_reward, 0, 10, DEFAULT_CONFIG.health_score.success_reward),
233
+ rate_limit_penalty: clampNumber(
234
+ hs.rate_limit_penalty,
235
+ -50,
236
+ 0,
237
+ DEFAULT_CONFIG.health_score.rate_limit_penalty,
238
+ ),
239
+ failure_penalty: clampNumber(hs.failure_penalty, -100, 0, DEFAULT_CONFIG.health_score.failure_penalty),
240
+ recovery_rate_per_hour: clampNumber(
241
+ hs.recovery_rate_per_hour,
242
+ 0,
243
+ 20,
244
+ DEFAULT_CONFIG.health_score.recovery_rate_per_hour,
245
+ ),
246
+ min_usable: clampNumber(hs.min_usable, 0, 100, DEFAULT_CONFIG.health_score.min_usable),
247
+ max_score: clampNumber(hs.max_score, 50, 100, DEFAULT_CONFIG.health_score.max_score),
248
+ };
249
+ }
324
250
 
325
- return config;
251
+ if (raw.toasts && typeof raw.toasts === "object") {
252
+ const t = raw.toasts as Record<string, unknown>;
253
+ config.toasts = {
254
+ quiet: typeof t.quiet === "boolean" ? t.quiet : DEFAULT_CONFIG.toasts.quiet,
255
+ debounce_seconds: clampNumber(t.debounce_seconds, 0, 300, DEFAULT_CONFIG.toasts.debounce_seconds),
256
+ };
257
+ }
258
+
259
+ if (raw.token_bucket && typeof raw.token_bucket === "object") {
260
+ const tb = raw.token_bucket as Record<string, unknown>;
261
+ config.token_bucket = {
262
+ max_tokens: clampNumber(tb.max_tokens, 1, 1000, DEFAULT_CONFIG.token_bucket.max_tokens),
263
+ regeneration_rate_per_minute: clampNumber(
264
+ tb.regeneration_rate_per_minute,
265
+ 0.1,
266
+ 60,
267
+ DEFAULT_CONFIG.token_bucket.regeneration_rate_per_minute,
268
+ ),
269
+ initial_tokens: clampNumber(tb.initial_tokens, 1, 1000, DEFAULT_CONFIG.token_bucket.initial_tokens),
270
+ };
271
+ }
272
+
273
+ if (raw.headers && typeof raw.headers === "object") {
274
+ const h = raw.headers as Record<string, unknown>;
275
+
276
+ if (typeof h.emulation_profile === "string" && h.emulation_profile.trim()) {
277
+ config.headers.emulation_profile = h.emulation_profile.trim();
278
+ }
279
+
280
+ if (h.overrides && typeof h.overrides === "object" && !Array.isArray(h.overrides)) {
281
+ const overrides: Record<string, string> = {};
282
+ for (const [key, value] of Object.entries(h.overrides as Record<string, unknown>)) {
283
+ if (!key) continue;
284
+ if (typeof value === "string") {
285
+ overrides[key] = value;
286
+ }
287
+ }
288
+ config.headers.overrides = overrides;
289
+ }
290
+
291
+ if (Array.isArray(h.disable)) {
292
+ config.headers.disable = (h.disable as unknown[])
293
+ .filter((v): v is string => typeof v === "string")
294
+ .map((v) => v.trim().toLowerCase())
295
+ .filter(Boolean);
296
+ }
297
+
298
+ if (typeof h.billing_header === "boolean") {
299
+ config.headers.billing_header = h.billing_header;
300
+ }
301
+ }
302
+
303
+ if (raw.idle_refresh && typeof raw.idle_refresh === "object") {
304
+ const ir = raw.idle_refresh as Record<string, unknown>;
305
+ config.idle_refresh = {
306
+ enabled: typeof ir.enabled === "boolean" ? ir.enabled : DEFAULT_CONFIG.idle_refresh.enabled,
307
+ window_minutes: clampNumber(ir.window_minutes, 1, 24 * 60, DEFAULT_CONFIG.idle_refresh.window_minutes),
308
+ min_interval_minutes: clampNumber(
309
+ ir.min_interval_minutes,
310
+ 1,
311
+ 24 * 60,
312
+ DEFAULT_CONFIG.idle_refresh.min_interval_minutes,
313
+ ),
314
+ };
315
+ }
316
+
317
+ if (raw.cc_credential_reuse && typeof raw.cc_credential_reuse === "object") {
318
+ const ccr = raw.cc_credential_reuse as Record<string, unknown>;
319
+ config.cc_credential_reuse = {
320
+ enabled: typeof ccr.enabled === "boolean" ? ccr.enabled : DEFAULT_CONFIG.cc_credential_reuse.enabled,
321
+ auto_detect:
322
+ typeof ccr.auto_detect === "boolean" ? ccr.auto_detect : DEFAULT_CONFIG.cc_credential_reuse.auto_detect,
323
+ prefer_over_oauth:
324
+ typeof ccr.prefer_over_oauth === "boolean"
325
+ ? ccr.prefer_over_oauth
326
+ : DEFAULT_CONFIG.cc_credential_reuse.prefer_over_oauth,
327
+ };
328
+ }
329
+
330
+ return config;
326
331
  }
327
332
 
328
333
  function applyEnvOverrides(config: AnthropicAuthConfig): AnthropicAuthConfig {
329
- const env = process.env;
330
-
331
- if (
332
- env.OPENCODE_ANTHROPIC_STRATEGY &&
333
- VALID_STRATEGIES.includes(env.OPENCODE_ANTHROPIC_STRATEGY as AccountSelectionStrategy)
334
- ) {
335
- config.account_selection_strategy = env.OPENCODE_ANTHROPIC_STRATEGY as AccountSelectionStrategy;
336
- }
337
-
338
- if (env.OPENCODE_ANTHROPIC_DEBUG === "1" || env.OPENCODE_ANTHROPIC_DEBUG === "true") {
339
- config.debug = true;
340
- }
341
- if (env.OPENCODE_ANTHROPIC_DEBUG === "0" || env.OPENCODE_ANTHROPIC_DEBUG === "false") {
342
- config.debug = false;
343
- }
344
-
345
- if (env.OPENCODE_ANTHROPIC_QUIET === "1" || env.OPENCODE_ANTHROPIC_QUIET === "true") {
346
- config.toasts.quiet = true;
347
- }
348
- if (env.OPENCODE_ANTHROPIC_QUIET === "0" || env.OPENCODE_ANTHROPIC_QUIET === "false") {
349
- config.toasts.quiet = false;
350
- }
351
-
352
- if (
353
- env.OPENCODE_ANTHROPIC_EMULATE_CLAUDE_CODE_SIGNATURE === "1" ||
354
- env.OPENCODE_ANTHROPIC_EMULATE_CLAUDE_CODE_SIGNATURE === "true"
355
- ) {
356
- config.signature_emulation.enabled = true;
357
- }
358
- if (
359
- env.OPENCODE_ANTHROPIC_EMULATE_CLAUDE_CODE_SIGNATURE === "0" ||
360
- env.OPENCODE_ANTHROPIC_EMULATE_CLAUDE_CODE_SIGNATURE === "false"
361
- ) {
362
- config.signature_emulation.enabled = false;
363
- }
364
-
365
- if (
366
- env.OPENCODE_ANTHROPIC_FETCH_CLAUDE_CODE_VERSION === "1" ||
367
- env.OPENCODE_ANTHROPIC_FETCH_CLAUDE_CODE_VERSION === "true"
368
- ) {
369
- config.signature_emulation.fetch_claude_code_version_on_startup = true;
370
- }
371
- if (
372
- env.OPENCODE_ANTHROPIC_FETCH_CLAUDE_CODE_VERSION === "0" ||
373
- env.OPENCODE_ANTHROPIC_FETCH_CLAUDE_CODE_VERSION === "false"
374
- ) {
375
- config.signature_emulation.fetch_claude_code_version_on_startup = false;
376
- }
377
-
378
- if (env.OPENCODE_ANTHROPIC_PROMPT_COMPACTION === "off") {
379
- config.signature_emulation.prompt_compaction = "off";
380
- }
381
- if (env.OPENCODE_ANTHROPIC_PROMPT_COMPACTION === "minimal") {
382
- config.signature_emulation.prompt_compaction = "minimal";
383
- }
384
-
385
- if (
386
- env.OPENCODE_ANTHROPIC_SANITIZE_SYSTEM_PROMPT === "1" ||
387
- env.OPENCODE_ANTHROPIC_SANITIZE_SYSTEM_PROMPT === "true"
388
- ) {
389
- config.signature_emulation.sanitize_system_prompt = true;
390
- }
391
- if (
392
- env.OPENCODE_ANTHROPIC_SANITIZE_SYSTEM_PROMPT === "0" ||
393
- env.OPENCODE_ANTHROPIC_SANITIZE_SYSTEM_PROMPT === "false"
394
- ) {
395
- config.signature_emulation.sanitize_system_prompt = false;
396
- }
397
-
398
- if (env.OPENCODE_ANTHROPIC_OVERRIDE_MODEL_LIMITS === "1" || env.OPENCODE_ANTHROPIC_OVERRIDE_MODEL_LIMITS === "true") {
399
- config.override_model_limits.enabled = true;
400
- }
401
- if (
402
- env.OPENCODE_ANTHROPIC_OVERRIDE_MODEL_LIMITS === "0" ||
403
- env.OPENCODE_ANTHROPIC_OVERRIDE_MODEL_LIMITS === "false"
404
- ) {
405
- config.override_model_limits.enabled = false;
406
- }
407
-
408
- if (env.OPENCODE_ANTHROPIC_CC_REUSE_ENABLED === "0" || env.OPENCODE_ANTHROPIC_CC_REUSE_ENABLED === "false") {
409
- config.cc_credential_reuse.enabled = false;
410
- config.cc_credential_reuse.auto_detect = false;
411
- }
412
-
413
- return config;
334
+ const env = process.env;
335
+
336
+ if (
337
+ env.OPENCODE_ANTHROPIC_STRATEGY &&
338
+ VALID_STRATEGIES.includes(env.OPENCODE_ANTHROPIC_STRATEGY as AccountSelectionStrategy)
339
+ ) {
340
+ config.account_selection_strategy = env.OPENCODE_ANTHROPIC_STRATEGY as AccountSelectionStrategy;
341
+ }
342
+
343
+ if (env.OPENCODE_ANTHROPIC_DEBUG === "1" || env.OPENCODE_ANTHROPIC_DEBUG === "true") {
344
+ config.debug = true;
345
+ }
346
+ if (env.OPENCODE_ANTHROPIC_DEBUG === "0" || env.OPENCODE_ANTHROPIC_DEBUG === "false") {
347
+ config.debug = false;
348
+ }
349
+
350
+ if (env.OPENCODE_ANTHROPIC_QUIET === "1" || env.OPENCODE_ANTHROPIC_QUIET === "true") {
351
+ config.toasts.quiet = true;
352
+ }
353
+ if (env.OPENCODE_ANTHROPIC_QUIET === "0" || env.OPENCODE_ANTHROPIC_QUIET === "false") {
354
+ config.toasts.quiet = false;
355
+ }
356
+
357
+ if (
358
+ env.OPENCODE_ANTHROPIC_EMULATE_CLAUDE_CODE_SIGNATURE === "1" ||
359
+ env.OPENCODE_ANTHROPIC_EMULATE_CLAUDE_CODE_SIGNATURE === "true"
360
+ ) {
361
+ config.signature_emulation.enabled = true;
362
+ }
363
+ if (
364
+ env.OPENCODE_ANTHROPIC_EMULATE_CLAUDE_CODE_SIGNATURE === "0" ||
365
+ env.OPENCODE_ANTHROPIC_EMULATE_CLAUDE_CODE_SIGNATURE === "false"
366
+ ) {
367
+ config.signature_emulation.enabled = false;
368
+ }
369
+
370
+ if (
371
+ env.OPENCODE_ANTHROPIC_FETCH_CLAUDE_CODE_VERSION === "1" ||
372
+ env.OPENCODE_ANTHROPIC_FETCH_CLAUDE_CODE_VERSION === "true"
373
+ ) {
374
+ config.signature_emulation.fetch_claude_code_version_on_startup = true;
375
+ }
376
+ if (
377
+ env.OPENCODE_ANTHROPIC_FETCH_CLAUDE_CODE_VERSION === "0" ||
378
+ env.OPENCODE_ANTHROPIC_FETCH_CLAUDE_CODE_VERSION === "false"
379
+ ) {
380
+ config.signature_emulation.fetch_claude_code_version_on_startup = false;
381
+ }
382
+
383
+ if (env.OPENCODE_ANTHROPIC_PROMPT_COMPACTION === "off") {
384
+ config.signature_emulation.prompt_compaction = "off";
385
+ }
386
+ if (env.OPENCODE_ANTHROPIC_PROMPT_COMPACTION === "minimal") {
387
+ config.signature_emulation.prompt_compaction = "minimal";
388
+ }
389
+
390
+ if (
391
+ env.OPENCODE_ANTHROPIC_SANITIZE_SYSTEM_PROMPT === "1" ||
392
+ env.OPENCODE_ANTHROPIC_SANITIZE_SYSTEM_PROMPT === "true"
393
+ ) {
394
+ config.signature_emulation.sanitize_system_prompt = true;
395
+ }
396
+ if (
397
+ env.OPENCODE_ANTHROPIC_SANITIZE_SYSTEM_PROMPT === "0" ||
398
+ env.OPENCODE_ANTHROPIC_SANITIZE_SYSTEM_PROMPT === "false"
399
+ ) {
400
+ config.signature_emulation.sanitize_system_prompt = false;
401
+ }
402
+
403
+ if (
404
+ env.OPENCODE_ANTHROPIC_OVERRIDE_MODEL_LIMITS === "1" ||
405
+ env.OPENCODE_ANTHROPIC_OVERRIDE_MODEL_LIMITS === "true"
406
+ ) {
407
+ config.override_model_limits.enabled = true;
408
+ }
409
+ if (
410
+ env.OPENCODE_ANTHROPIC_OVERRIDE_MODEL_LIMITS === "0" ||
411
+ env.OPENCODE_ANTHROPIC_OVERRIDE_MODEL_LIMITS === "false"
412
+ ) {
413
+ config.override_model_limits.enabled = false;
414
+ }
415
+
416
+ if (env.OPENCODE_ANTHROPIC_CC_REUSE_ENABLED === "0" || env.OPENCODE_ANTHROPIC_CC_REUSE_ENABLED === "false") {
417
+ config.cc_credential_reuse.enabled = false;
418
+ config.cc_credential_reuse.auto_detect = false;
419
+ }
420
+
421
+ return config;
414
422
  }
415
423
 
416
424
  /**
417
425
  * Load config from disk, validate, apply env overrides.
418
426
  */
419
427
  export function loadConfig(): AnthropicAuthConfig {
420
- const configPath = getConfigPath();
421
-
422
- if (!existsSync(configPath)) {
423
- return applyEnvOverrides(createDefaultConfig());
424
- }
425
-
426
- try {
427
- const content = readFileSync(configPath, "utf-8");
428
- const raw = JSON.parse(content);
429
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
430
- return applyEnvOverrides(createDefaultConfig());
431
- }
432
- const config = validateConfig(raw as Record<string, unknown>);
433
- return applyEnvOverrides(config);
434
- } catch {
435
- return applyEnvOverrides(createDefaultConfig());
436
- }
428
+ const configPath = getConfigPath();
429
+
430
+ if (!existsSync(configPath)) {
431
+ return applyEnvOverrides(createDefaultConfig());
432
+ }
433
+
434
+ try {
435
+ const content = readFileSync(configPath, "utf-8");
436
+ const raw = JSON.parse(content);
437
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
438
+ return applyEnvOverrides(createDefaultConfig());
439
+ }
440
+ const config = validateConfig(raw as Record<string, unknown>);
441
+ return applyEnvOverrides(config);
442
+ } catch {
443
+ return applyEnvOverrides(createDefaultConfig());
444
+ }
437
445
  }
438
446
 
439
447
  /**
@@ -441,38 +449,38 @@ export function loadConfig(): AnthropicAuthConfig {
441
449
  * Returns an empty object if the file doesn't exist or is invalid.
442
450
  */
443
451
  export function loadRawConfig(): Record<string, unknown> {
444
- const configPath = getConfigPath();
445
- if (!existsSync(configPath)) return {};
446
- try {
447
- const content = readFileSync(configPath, "utf-8");
448
- const raw = JSON.parse(content);
449
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) return {};
450
- return raw as Record<string, unknown>;
451
- } catch {
452
- return {};
453
- }
452
+ const configPath = getConfigPath();
453
+ if (!existsSync(configPath)) return {};
454
+ try {
455
+ const content = readFileSync(configPath, "utf-8");
456
+ const raw = JSON.parse(content);
457
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return {};
458
+ return raw as Record<string, unknown>;
459
+ } catch {
460
+ return {};
461
+ }
454
462
  }
455
463
 
456
464
  function deepMergeConfig(target: Record<string, unknown>, source: Record<string, unknown>): Record<string, unknown> {
457
- const result = { ...target };
458
- for (const [key, value] of Object.entries(source)) {
459
- if (
460
- value &&
461
- typeof value === "object" &&
462
- !Array.isArray(value) &&
463
- typeof result[key] === "object" &&
464
- result[key] &&
465
- !Array.isArray(result[key])
466
- ) {
467
- result[key] = {
468
- ...(result[key] as Record<string, unknown>),
469
- ...(value as Record<string, unknown>),
470
- };
471
- } else {
472
- result[key] = value;
473
- }
474
- }
475
- return result;
465
+ const result = { ...target };
466
+ for (const [key, value] of Object.entries(source)) {
467
+ if (
468
+ value &&
469
+ typeof value === "object" &&
470
+ !Array.isArray(value) &&
471
+ typeof result[key] === "object" &&
472
+ result[key] &&
473
+ !Array.isArray(result[key])
474
+ ) {
475
+ result[key] = {
476
+ ...(result[key] as Record<string, unknown>),
477
+ ...(value as Record<string, unknown>),
478
+ };
479
+ } else {
480
+ result[key] = value;
481
+ }
482
+ }
483
+ return result;
476
484
  }
477
485
 
478
486
  /**
@@ -481,24 +489,24 @@ function deepMergeConfig(target: Record<string, unknown>, source: Record<string,
481
489
  * Uses atomic write (temp + rename) for safety.
482
490
  */
483
491
  export function saveConfig(updates: Record<string, unknown>): void {
484
- const configPath = getConfigPath();
485
- const dir = dirname(configPath);
486
- mkdirSync(dir, { recursive: true });
487
-
488
- const existing = loadRawConfig();
489
- const merged = deepMergeConfig(existing, updates);
490
-
491
- const tmpPath = configPath + `.tmp.${process.pid}`;
492
- writeFileSync(tmpPath, JSON.stringify(merged, null, 2) + "\n", {
493
- encoding: "utf-8",
494
- mode: 0o600,
495
- });
496
- renameSync(tmpPath, configPath);
492
+ const configPath = getConfigPath();
493
+ const dir = dirname(configPath);
494
+ mkdirSync(dir, { recursive: true });
495
+
496
+ const existing = loadRawConfig();
497
+ const merged = deepMergeConfig(existing, updates);
498
+
499
+ const tmpPath = configPath + `.tmp.${process.pid}`;
500
+ writeFileSync(tmpPath, JSON.stringify(merged, null, 2) + "\n", {
501
+ encoding: "utf-8",
502
+ mode: 0o600,
503
+ });
504
+ renameSync(tmpPath, configPath);
497
505
  }
498
506
 
499
507
  /**
500
508
  * Load config fresh from disk (bypassing any startup cache).
501
509
  */
502
510
  export function loadConfigFresh(): AnthropicAuthConfig {
503
- return loadConfig();
511
+ return loadConfig();
504
512
  }