@vacbo/opencode-anthropic-fix 0.0.44 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -0
- package/dist/bun-proxy.mjs +282 -55
- package/dist/opencode-anthropic-auth-cli.mjs +194 -55
- package/dist/opencode-anthropic-auth-plugin.js +1816 -594
- package/package.json +1 -1
- package/src/__tests__/billing-edge-cases.test.ts +84 -0
- package/src/__tests__/bun-proxy.parallel.test.ts +460 -0
- package/src/__tests__/debug-gating.test.ts +76 -0
- package/src/__tests__/decomposition-smoke.test.ts +92 -0
- package/src/__tests__/fingerprint-regression.test.ts +1 -1
- package/src/__tests__/helpers/conversation-history.smoke.test.ts +338 -0
- package/src/__tests__/helpers/conversation-history.ts +376 -0
- package/src/__tests__/helpers/deferred.smoke.test.ts +161 -0
- package/src/__tests__/helpers/deferred.ts +122 -0
- package/src/__tests__/helpers/in-memory-storage.smoke.test.ts +166 -0
- package/src/__tests__/helpers/in-memory-storage.ts +152 -0
- package/src/__tests__/helpers/mock-bun-proxy.smoke.test.ts +92 -0
- package/src/__tests__/helpers/mock-bun-proxy.ts +229 -0
- package/src/__tests__/helpers/plugin-fetch-harness.smoke.test.ts +337 -0
- package/src/__tests__/helpers/plugin-fetch-harness.ts +401 -0
- package/src/__tests__/helpers/sse.smoke.test.ts +243 -0
- package/src/__tests__/helpers/sse.ts +288 -0
- package/src/__tests__/index.parallel.test.ts +711 -0
- package/src/__tests__/sanitization-regex.test.ts +65 -0
- package/src/__tests__/state-bounds.test.ts +110 -0
- package/src/account-identity.test.ts +213 -0
- package/src/account-identity.ts +108 -0
- package/src/accounts.dedup.test.ts +696 -0
- package/src/accounts.test.ts +2 -1
- package/src/accounts.ts +485 -191
- package/src/bun-fetch.test.ts +379 -0
- package/src/bun-fetch.ts +447 -174
- package/src/bun-proxy.ts +289 -57
- package/src/circuit-breaker.test.ts +274 -0
- package/src/circuit-breaker.ts +235 -0
- package/src/cli.test.ts +1 -0
- package/src/cli.ts +37 -18
- package/src/commands/router.ts +25 -5
- package/src/env.ts +1 -0
- package/src/headers/billing.ts +31 -13
- package/src/index.ts +224 -247
- package/src/oauth.ts +7 -1
- package/src/parent-pid-watcher.test.ts +219 -0
- package/src/parent-pid-watcher.ts +99 -0
- package/src/plugin-helpers.ts +112 -0
- package/src/refresh-helpers.ts +169 -0
- package/src/refresh-lock.test.ts +36 -9
- package/src/refresh-lock.ts +2 -2
- package/src/request/body.history.test.ts +398 -0
- package/src/request/body.ts +200 -13
- package/src/request/metadata.ts +6 -2
- package/src/response/index.ts +1 -1
- package/src/response/mcp.ts +60 -31
- package/src/response/streaming.test.ts +382 -0
- package/src/response/streaming.ts +403 -76
- package/src/storage.test.ts +127 -104
- package/src/storage.ts +152 -62
- package/src/system-prompt/builder.ts +33 -3
- package/src/system-prompt/sanitize.ts +12 -2
- package/src/token-refresh.test.ts +84 -1
- package/src/token-refresh.ts +14 -8
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Smoke test for plugin-fetch-harness.ts
|
|
3
|
+
*
|
|
4
|
+
* Verifies that the harness can:
|
|
5
|
+
* 1. Create a plugin instance with mocked dependencies
|
|
6
|
+
* 2. Fire a request through the fetch interceptor
|
|
7
|
+
* 3. Assert that the request lands on the mock
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
11
|
+
import { createFetchHarness, setMockAccounts, clearMockAccounts } from "./plugin-fetch-harness.js";
|
|
12
|
+
|
|
13
|
+
// Mock dependencies (hoisted by Vitest)
|
|
14
|
+
vi.mock("node:readline/promises", () => ({
|
|
15
|
+
createInterface: vi.fn(() => ({
|
|
16
|
+
question: vi.fn().mockResolvedValue("a"),
|
|
17
|
+
close: vi.fn(),
|
|
18
|
+
})),
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
vi.mock("../../storage.js", () => ({
|
|
22
|
+
createDefaultStats: (now?: number) => ({
|
|
23
|
+
requests: 0,
|
|
24
|
+
inputTokens: 0,
|
|
25
|
+
outputTokens: 0,
|
|
26
|
+
cacheReadTokens: 0,
|
|
27
|
+
cacheWriteTokens: 0,
|
|
28
|
+
lastReset: now ?? Date.now(),
|
|
29
|
+
}),
|
|
30
|
+
loadAccounts: vi.fn().mockResolvedValue(null),
|
|
31
|
+
saveAccounts: vi.fn().mockResolvedValue(undefined),
|
|
32
|
+
clearAccounts: vi.fn().mockResolvedValue(undefined),
|
|
33
|
+
getStoragePath: vi.fn(() => "/tmp/test-accounts.json"),
|
|
34
|
+
}));
|
|
35
|
+
|
|
36
|
+
vi.mock("../../config.js", () => {
|
|
37
|
+
const DEFAULT_CONFIG = {
|
|
38
|
+
account_selection_strategy: "sticky",
|
|
39
|
+
failure_ttl_seconds: 3600,
|
|
40
|
+
debug: false,
|
|
41
|
+
signature_emulation: {
|
|
42
|
+
enabled: true,
|
|
43
|
+
fetch_claude_code_version_on_startup: false,
|
|
44
|
+
prompt_compaction: "minimal",
|
|
45
|
+
},
|
|
46
|
+
override_model_limits: {
|
|
47
|
+
enabled: false,
|
|
48
|
+
context: 1_000_000,
|
|
49
|
+
output: 0,
|
|
50
|
+
},
|
|
51
|
+
custom_betas: [],
|
|
52
|
+
health_score: {
|
|
53
|
+
initial: 70,
|
|
54
|
+
success_reward: 1,
|
|
55
|
+
rate_limit_penalty: -10,
|
|
56
|
+
failure_penalty: -20,
|
|
57
|
+
recovery_rate_per_hour: 2,
|
|
58
|
+
min_usable: 50,
|
|
59
|
+
max_score: 100,
|
|
60
|
+
},
|
|
61
|
+
token_bucket: {
|
|
62
|
+
max_tokens: 50,
|
|
63
|
+
regeneration_rate_per_minute: 6,
|
|
64
|
+
initial_tokens: 50,
|
|
65
|
+
},
|
|
66
|
+
toasts: {
|
|
67
|
+
quiet: true,
|
|
68
|
+
debounce_seconds: 30,
|
|
69
|
+
},
|
|
70
|
+
headers: {},
|
|
71
|
+
idle_refresh: {
|
|
72
|
+
enabled: false,
|
|
73
|
+
window_minutes: 60,
|
|
74
|
+
min_interval_minutes: 30,
|
|
75
|
+
},
|
|
76
|
+
cc_credential_reuse: {
|
|
77
|
+
enabled: false,
|
|
78
|
+
auto_detect: false,
|
|
79
|
+
prefer_over_oauth: false,
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const createBaseConfig = () => ({
|
|
84
|
+
...DEFAULT_CONFIG,
|
|
85
|
+
account_selection_strategy: "sticky",
|
|
86
|
+
signature_emulation: {
|
|
87
|
+
...DEFAULT_CONFIG.signature_emulation,
|
|
88
|
+
fetch_claude_code_version_on_startup: false,
|
|
89
|
+
},
|
|
90
|
+
override_model_limits: {
|
|
91
|
+
...DEFAULT_CONFIG.override_model_limits,
|
|
92
|
+
},
|
|
93
|
+
custom_betas: [...DEFAULT_CONFIG.custom_betas],
|
|
94
|
+
health_score: { ...DEFAULT_CONFIG.health_score },
|
|
95
|
+
token_bucket: { ...DEFAULT_CONFIG.token_bucket },
|
|
96
|
+
toasts: { ...DEFAULT_CONFIG.toasts },
|
|
97
|
+
headers: { ...DEFAULT_CONFIG.headers },
|
|
98
|
+
idle_refresh: { ...DEFAULT_CONFIG.idle_refresh, enabled: false },
|
|
99
|
+
cc_credential_reuse: { ...DEFAULT_CONFIG.cc_credential_reuse },
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
CLIENT_ID: "9d1c250a-e61b-44d9-88ed-5944d1962f5e",
|
|
104
|
+
DEFAULT_CONFIG,
|
|
105
|
+
VALID_STRATEGIES: ["sticky", "round-robin", "hybrid"],
|
|
106
|
+
loadConfig: vi.fn(() => createBaseConfig()),
|
|
107
|
+
loadConfigFresh: vi.fn(() => createBaseConfig()),
|
|
108
|
+
saveConfig: vi.fn(),
|
|
109
|
+
getConfigDir: vi.fn(() => "/tmp/test-config"),
|
|
110
|
+
};
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
vi.mock("../../cc-credentials.js", () => ({
|
|
114
|
+
readCCCredentials: vi.fn(() => []),
|
|
115
|
+
}));
|
|
116
|
+
|
|
117
|
+
vi.mock("../../refresh-lock.js", () => ({
|
|
118
|
+
acquireRefreshLock: vi.fn().mockResolvedValue({
|
|
119
|
+
acquired: true,
|
|
120
|
+
lockPath: "/tmp/opencode-test.lock",
|
|
121
|
+
}),
|
|
122
|
+
releaseRefreshLock: vi.fn().mockResolvedValue(undefined),
|
|
123
|
+
}));
|
|
124
|
+
|
|
125
|
+
vi.mock("@clack/prompts", () => {
|
|
126
|
+
const noop = vi.fn();
|
|
127
|
+
return {
|
|
128
|
+
text: vi.fn().mockResolvedValue(""),
|
|
129
|
+
confirm: vi.fn().mockResolvedValue(false),
|
|
130
|
+
select: vi.fn().mockResolvedValue("cancel"),
|
|
131
|
+
spinner: vi.fn(() => ({ start: noop, stop: noop, message: noop })),
|
|
132
|
+
intro: noop,
|
|
133
|
+
outro: noop,
|
|
134
|
+
isCancel: vi.fn().mockReturnValue(false),
|
|
135
|
+
log: {
|
|
136
|
+
info: noop,
|
|
137
|
+
success: noop,
|
|
138
|
+
warn: noop,
|
|
139
|
+
error: noop,
|
|
140
|
+
message: noop,
|
|
141
|
+
step: noop,
|
|
142
|
+
},
|
|
143
|
+
note: noop,
|
|
144
|
+
cancel: noop,
|
|
145
|
+
};
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
describe("plugin-fetch-harness", () => {
|
|
149
|
+
beforeEach(() => {
|
|
150
|
+
clearMockAccounts();
|
|
151
|
+
vi.clearAllMocks();
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
afterEach(() => {
|
|
155
|
+
clearMockAccounts();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("should create a harness with default options", async () => {
|
|
159
|
+
const harness = await createFetchHarness();
|
|
160
|
+
|
|
161
|
+
expect(harness.fetch).toBeDefined();
|
|
162
|
+
expect(harness.mockFetch).toBeDefined();
|
|
163
|
+
expect(harness.tearDown).toBeDefined();
|
|
164
|
+
expect(harness.waitFor).toBeDefined();
|
|
165
|
+
expect(harness.getFetchHeaders).toBeDefined();
|
|
166
|
+
expect(harness.getFetchUrl).toBeDefined();
|
|
167
|
+
|
|
168
|
+
harness.tearDown();
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("should fire a request and land on mock fetch", async () => {
|
|
172
|
+
const harness = await createFetchHarness({
|
|
173
|
+
accounts: [{ email: "test@example.com" }],
|
|
174
|
+
mockResponses: {
|
|
175
|
+
"api.anthropic.com": {
|
|
176
|
+
ok: true,
|
|
177
|
+
status: 200,
|
|
178
|
+
json: async () => ({ id: "msg_123", content: [{ type: "text", text: "Hello" }] }),
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
const response = await harness.fetch("https://api.anthropic.com/v1/messages", {
|
|
184
|
+
method: "POST",
|
|
185
|
+
headers: { "Content-Type": "application/json" },
|
|
186
|
+
body: JSON.stringify({ model: "claude-sonnet", messages: [{ role: "user", content: "Hi" }] }),
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
expect(response.ok).toBe(true);
|
|
190
|
+
expect(harness.mockFetch).toHaveBeenCalledTimes(1);
|
|
191
|
+
|
|
192
|
+
const [callUrl] = harness.mockFetch.mock.calls[0] ?? [];
|
|
193
|
+
expect(callUrl).toBeDefined();
|
|
194
|
+
|
|
195
|
+
harness.tearDown();
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("should support multiple accounts", async () => {
|
|
199
|
+
const harness = await createFetchHarness({
|
|
200
|
+
accounts: [
|
|
201
|
+
{ email: "alice@example.com", refreshToken: "refresh-alice" },
|
|
202
|
+
{ email: "bob@example.com", refreshToken: "refresh-bob" },
|
|
203
|
+
],
|
|
204
|
+
initialAccount: 1,
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
await harness.fetch("https://api.anthropic.com/v1/messages", {
|
|
208
|
+
method: "POST",
|
|
209
|
+
body: JSON.stringify({}),
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
expect(harness.mockFetch).toHaveBeenCalledTimes(1);
|
|
213
|
+
|
|
214
|
+
harness.tearDown();
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it("should use custom mock responses", async () => {
|
|
218
|
+
const harness = await createFetchHarness({
|
|
219
|
+
mockResponses: {
|
|
220
|
+
"api.anthropic.com": () => ({
|
|
221
|
+
ok: true,
|
|
222
|
+
status: 200,
|
|
223
|
+
headers: new Headers({ "x-custom-header": "test-value" }),
|
|
224
|
+
json: async () => ({ custom: true }),
|
|
225
|
+
}),
|
|
226
|
+
},
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
const response = await harness.fetch("https://api.anthropic.com/v1/messages", {
|
|
230
|
+
method: "POST",
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
const data = await response.json();
|
|
234
|
+
expect(data).toEqual({ custom: true });
|
|
235
|
+
|
|
236
|
+
harness.tearDown();
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("should provide access to request headers", async () => {
|
|
240
|
+
const harness = await createFetchHarness();
|
|
241
|
+
|
|
242
|
+
await harness.fetch("https://api.anthropic.com/v1/messages", {
|
|
243
|
+
method: "POST",
|
|
244
|
+
headers: {
|
|
245
|
+
"x-api-key": "test-key",
|
|
246
|
+
"content-type": "application/json",
|
|
247
|
+
},
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
const headers = harness.getFetchHeaders(0);
|
|
251
|
+
expect(headers).toBeDefined();
|
|
252
|
+
|
|
253
|
+
harness.tearDown();
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it("should support waitFor for async assertions", async () => {
|
|
257
|
+
const harness = await createFetchHarness();
|
|
258
|
+
|
|
259
|
+
let callCount = 0;
|
|
260
|
+
harness.mockFetch.mockImplementation(async () => {
|
|
261
|
+
callCount++;
|
|
262
|
+
return new Response("{}", {
|
|
263
|
+
status: 200,
|
|
264
|
+
headers: { "content-type": "application/json" },
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
// Fire request asynchronously
|
|
269
|
+
setTimeout(() => {
|
|
270
|
+
harness.fetch("https://api.anthropic.com/v1/messages", { method: "POST" });
|
|
271
|
+
}, 50);
|
|
272
|
+
|
|
273
|
+
// Wait for the assertion to pass
|
|
274
|
+
await harness.waitFor(() => {
|
|
275
|
+
expect(callCount).toBeGreaterThan(0);
|
|
276
|
+
}, 500);
|
|
277
|
+
|
|
278
|
+
harness.tearDown();
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it("should restore original fetch on tearDown", async () => {
|
|
282
|
+
const originalFetch = globalThis.fetch;
|
|
283
|
+
|
|
284
|
+
const harness = await createFetchHarness();
|
|
285
|
+
expect(globalThis.fetch).toBe(harness.mockFetch);
|
|
286
|
+
|
|
287
|
+
harness.tearDown();
|
|
288
|
+
expect(globalThis.fetch).toBe(originalFetch);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it("should handle config overrides", async () => {
|
|
292
|
+
const harness = await createFetchHarness({
|
|
293
|
+
config: {
|
|
294
|
+
debug: true,
|
|
295
|
+
account_selection_strategy: "round-robin",
|
|
296
|
+
},
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
expect(harness.fetch).toBeDefined();
|
|
300
|
+
|
|
301
|
+
harness.tearDown();
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
describe("setMockAccounts / clearMockAccounts", () => {
|
|
306
|
+
it("should set and clear mock accounts", () => {
|
|
307
|
+
const testAccounts = [
|
|
308
|
+
{
|
|
309
|
+
id: "test-1",
|
|
310
|
+
index: 0,
|
|
311
|
+
refreshToken: "refresh-1",
|
|
312
|
+
access: "access-1",
|
|
313
|
+
expires: Date.now() + 3600_000,
|
|
314
|
+
token_updated_at: Date.now(),
|
|
315
|
+
addedAt: Date.now(),
|
|
316
|
+
lastUsed: 0,
|
|
317
|
+
enabled: true,
|
|
318
|
+
rateLimitResetTimes: {},
|
|
319
|
+
consecutiveFailures: 0,
|
|
320
|
+
lastFailureTime: null,
|
|
321
|
+
email: undefined,
|
|
322
|
+
stats: {
|
|
323
|
+
requests: 0,
|
|
324
|
+
inputTokens: 0,
|
|
325
|
+
outputTokens: 0,
|
|
326
|
+
cacheReadTokens: 0,
|
|
327
|
+
cacheWriteTokens: 0,
|
|
328
|
+
lastReset: Date.now(),
|
|
329
|
+
},
|
|
330
|
+
source: "oauth" as const,
|
|
331
|
+
},
|
|
332
|
+
];
|
|
333
|
+
|
|
334
|
+
setMockAccounts(testAccounts, 0);
|
|
335
|
+
clearMockAccounts();
|
|
336
|
+
});
|
|
337
|
+
});
|