@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.
Files changed (61) hide show
  1. package/README.md +19 -0
  2. package/dist/bun-proxy.mjs +282 -55
  3. package/dist/opencode-anthropic-auth-cli.mjs +194 -55
  4. package/dist/opencode-anthropic-auth-plugin.js +1816 -594
  5. package/package.json +1 -1
  6. package/src/__tests__/billing-edge-cases.test.ts +84 -0
  7. package/src/__tests__/bun-proxy.parallel.test.ts +460 -0
  8. package/src/__tests__/debug-gating.test.ts +76 -0
  9. package/src/__tests__/decomposition-smoke.test.ts +92 -0
  10. package/src/__tests__/fingerprint-regression.test.ts +1 -1
  11. package/src/__tests__/helpers/conversation-history.smoke.test.ts +338 -0
  12. package/src/__tests__/helpers/conversation-history.ts +376 -0
  13. package/src/__tests__/helpers/deferred.smoke.test.ts +161 -0
  14. package/src/__tests__/helpers/deferred.ts +122 -0
  15. package/src/__tests__/helpers/in-memory-storage.smoke.test.ts +166 -0
  16. package/src/__tests__/helpers/in-memory-storage.ts +152 -0
  17. package/src/__tests__/helpers/mock-bun-proxy.smoke.test.ts +92 -0
  18. package/src/__tests__/helpers/mock-bun-proxy.ts +229 -0
  19. package/src/__tests__/helpers/plugin-fetch-harness.smoke.test.ts +337 -0
  20. package/src/__tests__/helpers/plugin-fetch-harness.ts +401 -0
  21. package/src/__tests__/helpers/sse.smoke.test.ts +243 -0
  22. package/src/__tests__/helpers/sse.ts +288 -0
  23. package/src/__tests__/index.parallel.test.ts +711 -0
  24. package/src/__tests__/sanitization-regex.test.ts +65 -0
  25. package/src/__tests__/state-bounds.test.ts +110 -0
  26. package/src/account-identity.test.ts +213 -0
  27. package/src/account-identity.ts +108 -0
  28. package/src/accounts.dedup.test.ts +696 -0
  29. package/src/accounts.test.ts +2 -1
  30. package/src/accounts.ts +485 -191
  31. package/src/bun-fetch.test.ts +379 -0
  32. package/src/bun-fetch.ts +447 -174
  33. package/src/bun-proxy.ts +289 -57
  34. package/src/circuit-breaker.test.ts +274 -0
  35. package/src/circuit-breaker.ts +235 -0
  36. package/src/cli.test.ts +1 -0
  37. package/src/cli.ts +37 -18
  38. package/src/commands/router.ts +25 -5
  39. package/src/env.ts +1 -0
  40. package/src/headers/billing.ts +31 -13
  41. package/src/index.ts +224 -247
  42. package/src/oauth.ts +7 -1
  43. package/src/parent-pid-watcher.test.ts +219 -0
  44. package/src/parent-pid-watcher.ts +99 -0
  45. package/src/plugin-helpers.ts +112 -0
  46. package/src/refresh-helpers.ts +169 -0
  47. package/src/refresh-lock.test.ts +36 -9
  48. package/src/refresh-lock.ts +2 -2
  49. package/src/request/body.history.test.ts +398 -0
  50. package/src/request/body.ts +200 -13
  51. package/src/request/metadata.ts +6 -2
  52. package/src/response/index.ts +1 -1
  53. package/src/response/mcp.ts +60 -31
  54. package/src/response/streaming.test.ts +382 -0
  55. package/src/response/streaming.ts +403 -76
  56. package/src/storage.test.ts +127 -104
  57. package/src/storage.ts +152 -62
  58. package/src/system-prompt/builder.ts +33 -3
  59. package/src/system-prompt/sanitize.ts +12 -2
  60. package/src/token-refresh.test.ts +84 -1
  61. package/src/token-refresh.ts +14 -8
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Decomposition smoke tests (Tasks 6-7 from quality-refactor plan)
3
+ *
4
+ * Verifies that extracted refresh-helpers and plugin-helpers modules
5
+ * export their factory functions and produce the expected API surface.
6
+ */
7
+
8
+ import { describe, it, expect, vi } from "vitest";
9
+
10
+ import { DEFAULT_CONFIG } from "../config.js";
11
+ import { createRefreshHelpers } from "../refresh-helpers.js";
12
+ import { createPluginHelpers } from "../plugin-helpers.js";
13
+
14
+ describe("refresh-helpers module", () => {
15
+ it("exports createRefreshHelpers as a function", () => {
16
+ expect(typeof createRefreshHelpers).toBe("function");
17
+ });
18
+
19
+ it("factory returns an object when called with valid deps", () => {
20
+ const stubClient = {
21
+ tui: { showToast: vi.fn() },
22
+ command: { prompt: vi.fn() },
23
+ session: { prompt: vi.fn() },
24
+ };
25
+ const helpers = createRefreshHelpers({
26
+ client: stubClient as any,
27
+ config: { ...DEFAULT_CONFIG } as any,
28
+ getAccountManager: () => null,
29
+ debugLog: vi.fn(),
30
+ });
31
+
32
+ expect(helpers).toBeDefined();
33
+ expect(typeof helpers).toBe("object");
34
+ expect(helpers).not.toBeNull();
35
+ });
36
+
37
+ it("factory reads idle_refresh config fields at construction", () => {
38
+ const config = {
39
+ ...DEFAULT_CONFIG,
40
+ idle_refresh: { enabled: false, window_minutes: 10, min_interval_minutes: 5 },
41
+ };
42
+ expect(() =>
43
+ createRefreshHelpers({
44
+ client: {} as any,
45
+ config: config as any,
46
+ getAccountManager: () => null,
47
+ debugLog: vi.fn(),
48
+ }),
49
+ ).not.toThrow();
50
+ });
51
+ });
52
+
53
+ describe("plugin-helpers module", () => {
54
+ it("exports createPluginHelpers as a function", () => {
55
+ expect(typeof createPluginHelpers).toBe("function");
56
+ });
57
+
58
+ it("factory returns an object when called with valid deps", () => {
59
+ const stubClient = {
60
+ tui: { showToast: vi.fn() },
61
+ command: { prompt: vi.fn() },
62
+ session: { prompt: vi.fn() },
63
+ };
64
+ const helpers = createPluginHelpers({
65
+ client: stubClient as any,
66
+ config: { ...DEFAULT_CONFIG } as any,
67
+ debugLog: vi.fn(),
68
+ getAccountManager: () => null,
69
+ setAccountManager: vi.fn(),
70
+ });
71
+
72
+ expect(helpers).toBeDefined();
73
+ expect(typeof helpers).toBe("object");
74
+ expect(helpers).not.toBeNull();
75
+ });
76
+
77
+ it("factory accepts quiet toast config without throwing", () => {
78
+ const config = {
79
+ ...DEFAULT_CONFIG,
80
+ toasts: { quiet: true, debounce_seconds: 60 },
81
+ };
82
+ expect(() =>
83
+ createPluginHelpers({
84
+ client: {} as any,
85
+ config: config as any,
86
+ debugLog: vi.fn(),
87
+ getAccountManager: () => null,
88
+ setAccountManager: vi.fn(),
89
+ }),
90
+ ).not.toThrow();
91
+ });
92
+ });
@@ -207,7 +207,7 @@ describe("CC 2.1.98 — Billing header", () => {
207
207
  const messages = [{ role: "user", content: "Hello world from a test" }];
208
208
  const header = buildAnthropicBillingHeader(CC_VERSION, messages);
209
209
 
210
- // Replicate the documented algorithm: SHA-256(salt + chars[4,7,20] + version)
210
+ // Replicate the current billing-header algorithm: SHA-256(salt + chars[4,7,20] + version)
211
211
  const text = "Hello world from a test";
212
212
  const salt = "59cf53e54c78";
213
213
  const picked = [4, 7, 20].map((i) => (i < text.length ? text[i] : "0")).join("");
@@ -0,0 +1,338 @@
1
+ /**
2
+ * Smoke tests for conversation-history helper.
3
+ *
4
+ * Validates factory functions create valid Anthropic Messages API structures
5
+ * and that tool_use/tool_result pairing works correctly.
6
+ */
7
+
8
+ import { describe, expect, it, beforeEach } from "vitest";
9
+ import {
10
+ makeConversation,
11
+ makeMessage,
12
+ makeToolUse,
13
+ makeToolResult,
14
+ makeTextBlock,
15
+ makeToolExchange,
16
+ makeToolConversation,
17
+ validateToolPair,
18
+ findToolResult,
19
+ validateConversationTools,
20
+ generateToolUseId,
21
+ resetIdCounter,
22
+ type Conversation,
23
+ type Message,
24
+ type ToolUseBlock,
25
+ type ToolResultBlock,
26
+ } from "./conversation-history.js";
27
+
28
+ describe("conversation-history factories", () => {
29
+ beforeEach(() => {
30
+ resetIdCounter();
31
+ });
32
+
33
+ describe("makeConversation", () => {
34
+ it("creates empty conversation by default", () => {
35
+ const conv = makeConversation();
36
+
37
+ expect(conv.messages).toEqual([]);
38
+ expect(conv.metadata).toBeUndefined();
39
+ });
40
+
41
+ it("creates conversation with messages", () => {
42
+ const messages = [makeMessage({ role: "user", content: "Hello" })];
43
+ const conv = makeConversation({ messages });
44
+
45
+ expect(conv.messages).toHaveLength(1);
46
+ expect(conv.messages[0].role).toBe("user");
47
+ });
48
+
49
+ it("creates conversation with metadata", () => {
50
+ const conv = makeConversation({
51
+ metadata: { sessionId: "test-123", model: "claude-sonnet" },
52
+ });
53
+
54
+ expect(conv.metadata).toEqual({ sessionId: "test-123", model: "claude-sonnet" });
55
+ });
56
+ });
57
+
58
+ describe("makeMessage", () => {
59
+ it("creates user message by default", () => {
60
+ const msg = makeMessage();
61
+
62
+ expect(msg.role).toBe("user");
63
+ expect(msg.content).toBe("");
64
+ });
65
+
66
+ it("creates message with string content", () => {
67
+ const msg = makeMessage({ role: "user", content: "Hello Claude" });
68
+
69
+ expect(msg.role).toBe("user");
70
+ expect(msg.content).toBe("Hello Claude");
71
+ });
72
+
73
+ it("creates assistant message with content blocks", () => {
74
+ const msg = makeMessage({
75
+ role: "assistant",
76
+ content: [makeTextBlock("Hello!")],
77
+ });
78
+
79
+ expect(msg.role).toBe("assistant");
80
+ expect(Array.isArray(msg.content)).toBe(true);
81
+ expect((msg.content as Array<{ type: string }>)[0].type).toBe("text");
82
+ });
83
+ });
84
+
85
+ describe("makeTextBlock", () => {
86
+ it("creates text block with correct type", () => {
87
+ const block = makeTextBlock("Hello world");
88
+
89
+ expect(block.type).toBe("text");
90
+ expect(block.text).toBe("Hello world");
91
+ });
92
+ });
93
+
94
+ describe("makeToolUse", () => {
95
+ it("creates tool_use with auto-generated ID", () => {
96
+ const tool = makeToolUse();
97
+
98
+ expect(tool.type).toBe("tool_use");
99
+ expect(tool.id).toMatch(/^tu_[a-f0-9]+_\d+$/);
100
+ expect(tool.name).toBe("unnamed_tool");
101
+ expect(tool.input).toEqual({});
102
+ });
103
+
104
+ it("creates tool_use with custom properties", () => {
105
+ const tool = makeToolUse({
106
+ id: "custom_id",
107
+ name: "read_file",
108
+ input: { path: "test.txt", offset: 0 },
109
+ });
110
+
111
+ expect(tool.id).toBe("custom_id");
112
+ expect(tool.name).toBe("read_file");
113
+ expect(tool.input).toEqual({ path: "test.txt", offset: 0 });
114
+ });
115
+
116
+ it("generates unique IDs for multiple tools", () => {
117
+ const tool1 = makeToolUse();
118
+ const tool2 = makeToolUse();
119
+
120
+ expect(tool1.id).not.toBe(tool2.id);
121
+ });
122
+ });
123
+
124
+ describe("makeToolResult", () => {
125
+ it("creates tool_result with auto-generated tool_use_id", () => {
126
+ const result = makeToolResult();
127
+
128
+ expect(result.type).toBe("tool_result");
129
+ expect(result.tool_use_id).toMatch(/^tr_[a-f0-9]+_\d+$/);
130
+ expect(result.content).toBe("");
131
+ expect(result.is_error).toBe(false);
132
+ });
133
+
134
+ it("creates tool_result with custom properties", () => {
135
+ const result = makeToolResult({
136
+ toolUseId: "tu_abc123",
137
+ content: "File contents here",
138
+ isError: true,
139
+ });
140
+
141
+ expect(result.tool_use_id).toBe("tu_abc123");
142
+ expect(result.content).toBe("File contents here");
143
+ expect(result.is_error).toBe(true);
144
+ });
145
+
146
+ it("creates tool_result with content blocks", () => {
147
+ const result = makeToolResult({
148
+ toolUseId: "tu_123",
149
+ content: [makeTextBlock("Result text")],
150
+ });
151
+
152
+ expect(Array.isArray(result.content)).toBe(true);
153
+ expect((result.content as Array<{ type: string }>)[0].type).toBe("text");
154
+ });
155
+ });
156
+
157
+ describe("tool pairing validation", () => {
158
+ it("validates matching tool_use and tool_result pair", () => {
159
+ const toolUse = makeToolUse({ id: "tu_test123", name: "read_file" });
160
+ const toolResult = makeToolResult({ toolUseId: "tu_test123", content: "data" });
161
+
162
+ expect(validateToolPair(toolUse, toolResult)).toBe(true);
163
+ });
164
+
165
+ it("rejects mismatched tool pair", () => {
166
+ const toolUse = makeToolUse({ id: "tu_abc" });
167
+ const toolResult = makeToolResult({ toolUseId: "tu_xyz" });
168
+
169
+ expect(validateToolPair(toolUse, toolResult)).toBe(false);
170
+ });
171
+
172
+ it("finds tool_result by ID in message array", () => {
173
+ const toolUse = makeToolUse({ id: "tu_findme" });
174
+ const toolResult = makeToolResult({ toolUseId: "tu_findme", content: "found" });
175
+
176
+ const messages: Message[] = [
177
+ makeMessage({ role: "assistant", content: [toolUse] }),
178
+ makeMessage({ role: "user", content: [toolResult] }),
179
+ ];
180
+
181
+ const found = findToolResult(messages, "tu_findme");
182
+ expect(found).toBeDefined();
183
+ expect(found?.content).toBe("found");
184
+ });
185
+
186
+ it("returns undefined when tool_result not found", () => {
187
+ const messages: Message[] = [makeMessage({ role: "user", content: "Hello" })];
188
+
189
+ const found = findToolResult(messages, "tu_missing");
190
+ expect(found).toBeUndefined();
191
+ });
192
+ });
193
+
194
+ describe("conversation tool validation", () => {
195
+ it("validates conversation with complete tool pairs", () => {
196
+ const toolUse = makeToolUse({ id: "tu_complete" });
197
+ const toolResult = makeToolResult({ toolUseId: "tu_complete" });
198
+
199
+ const conv = makeConversation({
200
+ messages: [
201
+ makeMessage({ role: "user", content: "Use tool" }),
202
+ makeMessage({ role: "assistant", content: [toolUse] }),
203
+ makeMessage({ role: "user", content: [toolResult] }),
204
+ ],
205
+ });
206
+
207
+ const validation = validateConversationTools(conv);
208
+ expect(validation.valid).toBe(true);
209
+ expect(validation.unmatchedToolUses).toHaveLength(0);
210
+ expect(validation.unmatchedToolResults).toHaveLength(0);
211
+ });
212
+
213
+ it("detects unmatched tool_use blocks", () => {
214
+ const toolUse = makeToolUse({ id: "tu_unmatched" });
215
+
216
+ const conv = makeConversation({
217
+ messages: [makeMessage({ role: "assistant", content: [toolUse] })],
218
+ });
219
+
220
+ const validation = validateConversationTools(conv);
221
+ expect(validation.valid).toBe(false);
222
+ expect(validation.unmatchedToolUses).toHaveLength(1);
223
+ expect(validation.unmatchedToolResults).toHaveLength(0);
224
+ });
225
+
226
+ it("detects unmatched tool_result blocks", () => {
227
+ const toolResult = makeToolResult({ toolUseId: "tu_missing" });
228
+
229
+ const conv = makeConversation({
230
+ messages: [makeMessage({ role: "user", content: [toolResult] })],
231
+ });
232
+
233
+ const validation = validateConversationTools(conv);
234
+ expect(validation.valid).toBe(false);
235
+ expect(validation.unmatchedToolUses).toHaveLength(0);
236
+ expect(validation.unmatchedToolResults).toHaveLength(1);
237
+ });
238
+ });
239
+
240
+ describe("makeToolExchange", () => {
241
+ it("creates paired tool_use and tool_result", () => {
242
+ const [toolUse, toolResult] = makeToolExchange("read_file", { path: "test.txt" }, "file contents");
243
+
244
+ expect(toolUse.type).toBe("tool_use");
245
+ expect(toolUse.name).toBe("read_file");
246
+ expect(toolUse.input).toEqual({ path: "test.txt" });
247
+
248
+ expect(toolResult.type).toBe("tool_result");
249
+ expect(toolResult.tool_use_id).toBe(toolUse.id);
250
+ expect(toolResult.content).toBe("file contents");
251
+
252
+ expect(validateToolPair(toolUse, toolResult)).toBe(true);
253
+ });
254
+ });
255
+
256
+ describe("makeToolConversation", () => {
257
+ it("creates complete tool conversation flow", () => {
258
+ const conv = makeToolConversation(
259
+ "Read the config file",
260
+ "read_file",
261
+ { path: ".config" },
262
+ '{ "setting": true }',
263
+ );
264
+
265
+ expect(conv.messages).toHaveLength(3);
266
+
267
+ // User request
268
+ expect(conv.messages[0].role).toBe("user");
269
+ expect(conv.messages[0].content).toBe("Read the config file");
270
+
271
+ // Assistant tool use
272
+ expect(conv.messages[1].role).toBe("assistant");
273
+ const assistantContent = conv.messages[1].content as Array<{ type: string; name?: string }>;
274
+ expect(assistantContent[0].type).toBe("tool_use");
275
+ expect(assistantContent[0].name).toBe("read_file");
276
+
277
+ // User tool result
278
+ expect(conv.messages[2].role).toBe("user");
279
+ const userContent = conv.messages[2].content as Array<{ type: string }>;
280
+ expect(userContent[0].type).toBe("tool_result");
281
+
282
+ // Validate pairing
283
+ const validation = validateConversationTools(conv);
284
+ expect(validation.valid).toBe(true);
285
+ });
286
+ });
287
+
288
+ describe("ID generation", () => {
289
+ it("generates unique IDs with different prefixes", () => {
290
+ const id1 = generateToolUseId("tu");
291
+ const id2 = generateToolUseId("tr");
292
+
293
+ expect(id1.startsWith("tu_")).toBe(true);
294
+ expect(id2.startsWith("tr_")).toBe(true);
295
+ expect(id1).not.toBe(id2);
296
+ });
297
+
298
+ it("resets counter for deterministic tests", () => {
299
+ const tool1 = makeToolUse();
300
+ const counter1 = parseInt(tool1.id.split("_").pop() || "0", 10);
301
+
302
+ resetIdCounter();
303
+
304
+ const tool2 = makeToolUse();
305
+ const counter2 = parseInt(tool2.id.split("_").pop() || "0", 10);
306
+
307
+ expect(counter2).toBe(1);
308
+ });
309
+ });
310
+
311
+ describe("complex conversation scenarios", () => {
312
+ it("handles mixed content types", () => {
313
+ const imageBlock = {
314
+ type: "image" as const,
315
+ source: { type: "base64" as const, media_type: "image/png", data: "abc123" },
316
+ };
317
+
318
+ const conv = makeConversation({
319
+ messages: [
320
+ makeMessage({
321
+ role: "user",
322
+ content: [makeTextBlock("Please analyze this:"), imageBlock],
323
+ }),
324
+ makeMessage({
325
+ role: "assistant",
326
+ content: [makeToolUse({ name: "analyze_image" })],
327
+ }),
328
+ ],
329
+ });
330
+
331
+ expect(conv.messages[0].role).toBe("user");
332
+ const userContent = conv.messages[0].content as Array<{ type: string }>;
333
+ expect(userContent).toHaveLength(2);
334
+ expect(userContent[0].type).toBe("text");
335
+ expect(userContent[1].type).toBe("image");
336
+ });
337
+ });
338
+ });