@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/rotation.ts CHANGED
@@ -2,151 +2,151 @@ import type { AccountSelectionStrategy, HealthScoreConfig, TokenBucketConfig } f
2
2
  import { DEFAULT_CONFIG } from "./config.js";
3
3
 
4
4
  export interface AccountCandidate {
5
- index: number;
6
- lastUsed: number;
7
- healthScore: number;
8
- isRateLimited: boolean;
9
- enabled: boolean;
5
+ index: number;
6
+ lastUsed: number;
7
+ healthScore: number;
8
+ isRateLimited: boolean;
9
+ enabled: boolean;
10
10
  }
11
11
 
12
12
  // --- Health Score Tracker ---
13
13
 
14
14
  export class HealthScoreTracker {
15
- #scores = new Map<number, { score: number; lastUpdated: number; consecutiveFailures: number }>();
16
- #config: HealthScoreConfig;
17
-
18
- constructor(config: Partial<HealthScoreConfig> = {}) {
19
- this.#config = { ...DEFAULT_CONFIG.health_score, ...config };
20
- }
21
-
22
- /**
23
- * Get the current health score for an account, including passive recovery.
24
- */
25
- getScore(accountIndex: number): number {
26
- const state = this.#scores.get(accountIndex);
27
- if (!state) return this.#config.initial;
28
-
29
- const hoursSinceUpdate = (Date.now() - state.lastUpdated) / (1000 * 60 * 60);
30
- const recoveredPoints = Math.floor(hoursSinceUpdate * this.#config.recovery_rate_per_hour);
31
-
32
- return Math.min(this.#config.max_score, state.score + recoveredPoints);
33
- }
34
-
35
- /**
36
- * Record a successful request.
37
- */
38
- recordSuccess(accountIndex: number): void {
39
- const current = this.getScore(accountIndex);
40
- this.#scores.set(accountIndex, {
41
- score: Math.min(this.#config.max_score, current + this.#config.success_reward),
42
- lastUpdated: Date.now(),
43
- consecutiveFailures: 0,
44
- });
45
- }
46
-
47
- /**
48
- * Record a rate limit event.
49
- */
50
- recordRateLimit(accountIndex: number): void {
51
- const current = this.getScore(accountIndex);
52
- const state = this.#scores.get(accountIndex);
53
- this.#scores.set(accountIndex, {
54
- score: Math.max(0, current + this.#config.rate_limit_penalty),
55
- lastUpdated: Date.now(),
56
- consecutiveFailures: (state?.consecutiveFailures ?? 0) + 1,
57
- });
58
- }
59
-
60
- /**
61
- * Record a general failure.
62
- */
63
- recordFailure(accountIndex: number): void {
64
- const current = this.getScore(accountIndex);
65
- const state = this.#scores.get(accountIndex);
66
- this.#scores.set(accountIndex, {
67
- score: Math.max(0, current + this.#config.failure_penalty),
68
- lastUpdated: Date.now(),
69
- consecutiveFailures: (state?.consecutiveFailures ?? 0) + 1,
70
- });
71
- }
72
-
73
- /**
74
- * Check if an account is usable (score above minimum).
75
- */
76
- isUsable(accountIndex: number): boolean {
77
- return this.getScore(accountIndex) >= this.#config.min_usable;
78
- }
79
-
80
- /**
81
- * Reset tracking for an account.
82
- */
83
- reset(accountIndex: number): void {
84
- this.#scores.delete(accountIndex);
85
- }
15
+ #scores = new Map<number, { score: number; lastUpdated: number; consecutiveFailures: number }>();
16
+ #config: HealthScoreConfig;
17
+
18
+ constructor(config: Partial<HealthScoreConfig> = {}) {
19
+ this.#config = { ...DEFAULT_CONFIG.health_score, ...config };
20
+ }
21
+
22
+ /**
23
+ * Get the current health score for an account, including passive recovery.
24
+ */
25
+ getScore(accountIndex: number): number {
26
+ const state = this.#scores.get(accountIndex);
27
+ if (!state) return this.#config.initial;
28
+
29
+ const hoursSinceUpdate = (Date.now() - state.lastUpdated) / (1000 * 60 * 60);
30
+ const recoveredPoints = Math.floor(hoursSinceUpdate * this.#config.recovery_rate_per_hour);
31
+
32
+ return Math.min(this.#config.max_score, state.score + recoveredPoints);
33
+ }
34
+
35
+ /**
36
+ * Record a successful request.
37
+ */
38
+ recordSuccess(accountIndex: number): void {
39
+ const current = this.getScore(accountIndex);
40
+ this.#scores.set(accountIndex, {
41
+ score: Math.min(this.#config.max_score, current + this.#config.success_reward),
42
+ lastUpdated: Date.now(),
43
+ consecutiveFailures: 0,
44
+ });
45
+ }
46
+
47
+ /**
48
+ * Record a rate limit event.
49
+ */
50
+ recordRateLimit(accountIndex: number): void {
51
+ const current = this.getScore(accountIndex);
52
+ const state = this.#scores.get(accountIndex);
53
+ this.#scores.set(accountIndex, {
54
+ score: Math.max(0, current + this.#config.rate_limit_penalty),
55
+ lastUpdated: Date.now(),
56
+ consecutiveFailures: (state?.consecutiveFailures ?? 0) + 1,
57
+ });
58
+ }
59
+
60
+ /**
61
+ * Record a general failure.
62
+ */
63
+ recordFailure(accountIndex: number): void {
64
+ const current = this.getScore(accountIndex);
65
+ const state = this.#scores.get(accountIndex);
66
+ this.#scores.set(accountIndex, {
67
+ score: Math.max(0, current + this.#config.failure_penalty),
68
+ lastUpdated: Date.now(),
69
+ consecutiveFailures: (state?.consecutiveFailures ?? 0) + 1,
70
+ });
71
+ }
72
+
73
+ /**
74
+ * Check if an account is usable (score above minimum).
75
+ */
76
+ isUsable(accountIndex: number): boolean {
77
+ return this.getScore(accountIndex) >= this.#config.min_usable;
78
+ }
79
+
80
+ /**
81
+ * Reset tracking for an account.
82
+ */
83
+ reset(accountIndex: number): void {
84
+ this.#scores.delete(accountIndex);
85
+ }
86
86
  }
87
87
 
88
88
  // --- Token Bucket Tracker ---
89
89
 
90
90
  export class TokenBucketTracker {
91
- #buckets = new Map<number, { tokens: number; lastUpdated: number }>();
92
- #config: TokenBucketConfig;
93
-
94
- constructor(config: Partial<TokenBucketConfig> = {}) {
95
- this.#config = { ...DEFAULT_CONFIG.token_bucket, ...config };
96
- }
97
-
98
- /**
99
- * Get current token count for an account, including regeneration.
100
- */
101
- getTokens(accountIndex: number): number {
102
- const state = this.#buckets.get(accountIndex);
103
- if (!state) return this.#config.initial_tokens;
104
-
105
- const minutesSinceUpdate = (Date.now() - state.lastUpdated) / (1000 * 60);
106
- const recoveredTokens = minutesSinceUpdate * this.#config.regeneration_rate_per_minute;
107
-
108
- return Math.min(this.#config.max_tokens, state.tokens + recoveredTokens);
109
- }
110
-
111
- /**
112
- * Check if an account has enough tokens.
113
- */
114
- hasTokens(accountIndex: number, cost = 1): boolean {
115
- return this.getTokens(accountIndex) >= cost;
116
- }
117
-
118
- /**
119
- * Consume tokens for a request.
120
- * @returns Whether tokens were available and consumed
121
- */
122
- consume(accountIndex: number, cost = 1): boolean {
123
- const current = this.getTokens(accountIndex);
124
- if (current < cost) return false;
125
-
126
- this.#buckets.set(accountIndex, {
127
- tokens: current - cost,
128
- lastUpdated: Date.now(),
129
- });
130
- return true;
131
- }
132
-
133
- /**
134
- * Refund tokens (e.g., on non-rate-limit failure).
135
- */
136
- refund(accountIndex: number, amount = 1): void {
137
- const current = this.getTokens(accountIndex);
138
- this.#buckets.set(accountIndex, {
139
- tokens: Math.min(this.#config.max_tokens, current + amount),
140
- lastUpdated: Date.now(),
141
- });
142
- }
143
-
144
- /**
145
- * Get the max tokens value (for scoring calculations).
146
- */
147
- getMaxTokens(): number {
148
- return this.#config.max_tokens;
149
- }
91
+ #buckets = new Map<number, { tokens: number; lastUpdated: number }>();
92
+ #config: TokenBucketConfig;
93
+
94
+ constructor(config: Partial<TokenBucketConfig> = {}) {
95
+ this.#config = { ...DEFAULT_CONFIG.token_bucket, ...config };
96
+ }
97
+
98
+ /**
99
+ * Get current token count for an account, including regeneration.
100
+ */
101
+ getTokens(accountIndex: number): number {
102
+ const state = this.#buckets.get(accountIndex);
103
+ if (!state) return this.#config.initial_tokens;
104
+
105
+ const minutesSinceUpdate = (Date.now() - state.lastUpdated) / (1000 * 60);
106
+ const recoveredTokens = minutesSinceUpdate * this.#config.regeneration_rate_per_minute;
107
+
108
+ return Math.min(this.#config.max_tokens, state.tokens + recoveredTokens);
109
+ }
110
+
111
+ /**
112
+ * Check if an account has enough tokens.
113
+ */
114
+ hasTokens(accountIndex: number, cost = 1): boolean {
115
+ return this.getTokens(accountIndex) >= cost;
116
+ }
117
+
118
+ /**
119
+ * Consume tokens for a request.
120
+ * @returns Whether tokens were available and consumed
121
+ */
122
+ consume(accountIndex: number, cost = 1): boolean {
123
+ const current = this.getTokens(accountIndex);
124
+ if (current < cost) return false;
125
+
126
+ this.#buckets.set(accountIndex, {
127
+ tokens: current - cost,
128
+ lastUpdated: Date.now(),
129
+ });
130
+ return true;
131
+ }
132
+
133
+ /**
134
+ * Refund tokens (e.g., on non-rate-limit failure).
135
+ */
136
+ refund(accountIndex: number, amount = 1): void {
137
+ const current = this.getTokens(accountIndex);
138
+ this.#buckets.set(accountIndex, {
139
+ tokens: Math.min(this.#config.max_tokens, current + amount),
140
+ lastUpdated: Date.now(),
141
+ });
142
+ }
143
+
144
+ /**
145
+ * Get the max tokens value (for scoring calculations).
146
+ */
147
+ getMaxTokens(): number {
148
+ return this.#config.max_tokens;
149
+ }
150
150
  }
151
151
 
152
152
  // --- Selection Algorithms ---
@@ -155,88 +155,88 @@ const STICKINESS_BONUS = 150;
155
155
  const SWITCH_THRESHOLD = 100;
156
156
 
157
157
  function calculateHybridScore(account: AccountCandidate & { tokens: number }, maxTokens: number): number {
158
- const healthComponent = account.healthScore * 2;
159
- const tokenComponent = (account.tokens / maxTokens) * 100 * 5;
160
- const secondsSinceUsed = (Date.now() - account.lastUsed) / 1000;
161
- const freshnessComponent = Math.min(secondsSinceUsed, 3600) * 0.1;
158
+ const healthComponent = account.healthScore * 2;
159
+ const tokenComponent = (account.tokens / maxTokens) * 100 * 5;
160
+ const secondsSinceUsed = (Date.now() - account.lastUsed) / 1000;
161
+ const freshnessComponent = Math.min(secondsSinceUsed, 3600) * 0.1;
162
162
 
163
- return Math.max(0, healthComponent + tokenComponent + freshnessComponent);
163
+ return Math.max(0, healthComponent + tokenComponent + freshnessComponent);
164
164
  }
165
165
 
166
166
  /**
167
167
  * Select the best account based on the configured strategy.
168
168
  */
169
169
  export function selectAccount(
170
- candidates: AccountCandidate[],
171
- strategy: AccountSelectionStrategy,
172
- currentIndex: number | null,
173
- healthTracker: HealthScoreTracker,
174
- tokenTracker: TokenBucketTracker,
175
- cursor: number,
170
+ candidates: AccountCandidate[],
171
+ strategy: AccountSelectionStrategy,
172
+ currentIndex: number | null,
173
+ healthTracker: HealthScoreTracker,
174
+ tokenTracker: TokenBucketTracker,
175
+ cursor: number,
176
176
  ): { index: number; cursor: number } | null {
177
- const available = candidates.filter((acc) => acc.enabled && !acc.isRateLimited);
178
-
179
- if (available.length === 0) return null;
180
-
181
- switch (strategy) {
182
- case "sticky": {
183
- if (currentIndex !== null) {
184
- const current = available.find((acc) => acc.index === currentIndex);
185
- if (current) {
186
- return { index: current.index, cursor };
177
+ const available = candidates.filter((acc) => acc.enabled && !acc.isRateLimited);
178
+
179
+ if (available.length === 0) return null;
180
+
181
+ switch (strategy) {
182
+ case "sticky": {
183
+ if (currentIndex !== null) {
184
+ const current = available.find((acc) => acc.index === currentIndex);
185
+ if (current) {
186
+ return { index: current.index, cursor };
187
+ }
188
+ }
189
+ const next = available[cursor % available.length];
190
+ return next ? { index: next.index, cursor: cursor + 1 } : null;
187
191
  }
188
- }
189
- const next = available[cursor % available.length];
190
- return next ? { index: next.index, cursor: cursor + 1 } : null;
191
- }
192
192
 
193
- case "round-robin": {
194
- const next = available[cursor % available.length];
195
- return next ? { index: next.index, cursor: cursor + 1 } : null;
196
- }
193
+ case "round-robin": {
194
+ const next = available[cursor % available.length];
195
+ return next ? { index: next.index, cursor: cursor + 1 } : null;
196
+ }
197
197
 
198
- case "hybrid": {
199
- const scoredCandidates = available
200
- .filter((acc) => healthTracker.isUsable(acc.index) && tokenTracker.hasTokens(acc.index))
201
- .map((acc) => ({
202
- ...acc,
203
- tokens: tokenTracker.getTokens(acc.index),
204
- }));
205
-
206
- if (scoredCandidates.length === 0) {
207
- const fallback = available[0];
208
- return fallback ? { index: fallback.index, cursor } : null;
209
- }
210
-
211
- const maxTokens = tokenTracker.getMaxTokens();
212
- const scored = scoredCandidates
213
- .map((acc) => {
214
- const baseScore = calculateHybridScore(acc, maxTokens);
215
- const stickinessBonus = acc.index === currentIndex ? STICKINESS_BONUS : 0;
216
- return {
217
- index: acc.index,
218
- baseScore,
219
- score: baseScore + stickinessBonus,
220
- isCurrent: acc.index === currentIndex,
221
- };
222
- })
223
- .sort((a, b) => b.score - a.score);
224
-
225
- const best = scored[0];
226
- if (!best) return null;
227
-
228
- const currentCandidate = scored.find((s) => s.isCurrent);
229
- if (currentCandidate && !best.isCurrent) {
230
- const advantage = best.baseScore - currentCandidate.baseScore;
231
- if (advantage < SWITCH_THRESHOLD) {
232
- return { index: currentCandidate.index, cursor };
198
+ case "hybrid": {
199
+ const scoredCandidates = available
200
+ .filter((acc) => healthTracker.isUsable(acc.index) && tokenTracker.hasTokens(acc.index))
201
+ .map((acc) => ({
202
+ ...acc,
203
+ tokens: tokenTracker.getTokens(acc.index),
204
+ }));
205
+
206
+ if (scoredCandidates.length === 0) {
207
+ const fallback = available[0];
208
+ return fallback ? { index: fallback.index, cursor } : null;
209
+ }
210
+
211
+ const maxTokens = tokenTracker.getMaxTokens();
212
+ const scored = scoredCandidates
213
+ .map((acc) => {
214
+ const baseScore = calculateHybridScore(acc, maxTokens);
215
+ const stickinessBonus = acc.index === currentIndex ? STICKINESS_BONUS : 0;
216
+ return {
217
+ index: acc.index,
218
+ baseScore,
219
+ score: baseScore + stickinessBonus,
220
+ isCurrent: acc.index === currentIndex,
221
+ };
222
+ })
223
+ .sort((a, b) => b.score - a.score);
224
+
225
+ const best = scored[0];
226
+ if (!best) return null;
227
+
228
+ const currentCandidate = scored.find((s) => s.isCurrent);
229
+ if (currentCandidate && !best.isCurrent) {
230
+ const advantage = best.baseScore - currentCandidate.baseScore;
231
+ if (advantage < SWITCH_THRESHOLD) {
232
+ return { index: currentCandidate.index, cursor };
233
+ }
234
+ }
235
+
236
+ return { index: best.index, cursor };
233
237
  }
234
- }
235
238
 
236
- return { index: best.index, cursor };
239
+ default:
240
+ return available[0] ? { index: available[0].index, cursor } : null;
237
241
  }
238
-
239
- default:
240
- return available[0] ? { index: available[0].index, cursor } : null;
241
- }
242
242
  }