opencode-anthropic-fix 0.2.1 → 0.3.0

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 (39) hide show
  1. package/README.md +24 -0
  2. package/index.mjs +155 -149
  3. package/lib/config.mjs +47 -25
  4. package/lib/mimicry/adapter-input.mjs +671 -0
  5. package/lib/mimicry/context-hint-threshold.mjs +127 -0
  6. package/lib/mimicry/headers.mjs +71 -64
  7. package/lib/mimicry/request-body.mjs +23 -22
  8. package/lib/mimicry/system-prompt.mjs +54 -23
  9. package/lib/mimicry/wire-compat.mjs +305 -0
  10. package/package.json +3 -3
  11. package/lib/account-state.test.mjs +0 -109
  12. package/lib/accounts.test.mjs +0 -912
  13. package/lib/adaptive-thinking-models.test.mjs +0 -30
  14. package/lib/backoff.test.mjs +0 -411
  15. package/lib/cc-credentials.test.mjs +0 -391
  16. package/lib/config.test.mjs +0 -764
  17. package/lib/context-hint-persist.mjs +0 -68
  18. package/lib/diagnose.test.mjs +0 -122
  19. package/lib/env.test.mjs +0 -25
  20. package/lib/haiku-call.test.mjs +0 -72
  21. package/lib/message-transform.test.mjs +0 -173
  22. package/lib/mimicry/cache.test.mjs +0 -112
  23. package/lib/mimicry/headers.test.mjs +0 -50
  24. package/lib/mimicry/models.test.mjs +0 -132
  25. package/lib/mimicry/request-body.test.mjs +0 -34
  26. package/lib/mimicry/request-helpers.test.mjs +0 -155
  27. package/lib/mimicry/response-stream.test.mjs +0 -127
  28. package/lib/mimicry/system-prompt.test.mjs +0 -73
  29. package/lib/oauth.test.mjs +0 -343
  30. package/lib/redact.test.mjs +0 -154
  31. package/lib/refresh-lock.test.mjs +0 -131
  32. package/lib/request-headers.test.mjs +0 -164
  33. package/lib/retry/overload-loop.test.mjs +0 -107
  34. package/lib/rotation.test.mjs +0 -392
  35. package/lib/session-metrics.test.mjs +0 -67
  36. package/lib/storage.test.mjs +0 -696
  37. package/lib/token-economy/microcompact.test.mjs +0 -28
  38. package/lib/token-economy/transforms.test.mjs +0 -192
  39. package/lib/tuning.test.mjs +0 -89
@@ -1,28 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import { shouldMicrocompact, buildMicrocompactBetas } from "./microcompact.mjs";
3
-
4
- describe("shouldMicrocompact", () => {
5
- it("returns false when microcompact is disabled or unconfigured", () => {
6
- expect(shouldMicrocompact(1_000_000, {})).toBe(false);
7
- expect(shouldMicrocompact(1_000_000, { microcompact: { enabled: false } })).toBe(false);
8
- });
9
-
10
- it("uses the default 80% threshold of a 200K window (=160K)", () => {
11
- const config = { microcompact: { enabled: true } };
12
- expect(shouldMicrocompact(159_999, config)).toBe(false);
13
- expect(shouldMicrocompact(160_000, config)).toBe(true);
14
- expect(shouldMicrocompact(200_000, config)).toBe(true);
15
- });
16
-
17
- it("honors a custom threshold_percent", () => {
18
- const config = { microcompact: { enabled: true, threshold_percent: 50 } };
19
- expect(shouldMicrocompact(99_999, config)).toBe(false);
20
- expect(shouldMicrocompact(100_000, config)).toBe(true);
21
- });
22
- });
23
-
24
- describe("buildMicrocompactBetas", () => {
25
- it("returns the fixed microcompact beta flag list", () => {
26
- expect(buildMicrocompactBetas()).toEqual(["clear_tool_uses_20250919", "clear_thinking_20251015"]);
27
- });
28
- });
@@ -1,192 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import {
3
- applyAdaptiveThinkingZero,
4
- applyContextHintCompaction,
5
- applySessionToolResultDedupe,
6
- applyStableToolOrdering,
7
- applyToolResultDedupe,
8
- applyToolSchemaDeferral,
9
- applyTrailingSummaryTrim,
10
- estimatePromptTokensFromParsed,
11
- injectTokenBudgetBlock,
12
- parseNaturalLanguageBudget,
13
- } from "./transforms.mjs";
14
-
15
- describe("token-economy transforms", () => {
16
- it.each([
17
- ["Use 1 million tokens", 1_000_000],
18
- ["budget: 500k", 500_000],
19
- ["spend 1200", 1_200],
20
- ])("parses natural-language budget %s", (content, expected) => {
21
- expect(parseNaturalLanguageBudget([{ role: "user", content }])).toBe(expected);
22
- });
23
-
24
- it("injects a token budget system block", () => {
25
- const existing = { type: "text", text: "Existing prompt" };
26
- const result = injectTokenBudgetBlock([existing], { limit: 10_000, used: 2_500, continuations: 1 }, 0.9);
27
-
28
- expect(result).toHaveLength(2);
29
- expect(result[0]).toEqual({
30
- type: "text",
31
- text: `Token budget: ${(2_500).toLocaleString()}/${(10_000).toLocaleString()} tokens used (25%). Stop generating at ${(9_000).toLocaleString()} tokens. Remaining: ${(7_500).toLocaleString()} tokens.`,
32
- });
33
- expect(result[1]).toBe(existing);
34
- });
35
-
36
- it("estimates a plausible prompt token count from a parsed body", () => {
37
- const result = estimatePromptTokensFromParsed({
38
- system: [{ type: "text", text: "12345678" }],
39
- messages: [{ role: "user", content: "12345678" }],
40
- });
41
-
42
- expect(result).toBe(4);
43
- });
44
-
45
- it("dedupes repeated reproducible results while preserving stateful results", () => {
46
- const messages = [
47
- {
48
- role: "assistant",
49
- content: [
50
- { type: "tool_use", id: "read-1", name: "read", input: { path: "a.mjs" } },
51
- { type: "tool_use", id: "bash-1", name: "bash", input: { command: "date" } },
52
- ],
53
- },
54
- {
55
- role: "user",
56
- content: [
57
- { type: "tool_result", tool_use_id: "read-1", content: "old read" },
58
- { type: "tool_result", tool_use_id: "bash-1", content: "old bash" },
59
- ],
60
- },
61
- {
62
- role: "assistant",
63
- content: [
64
- { type: "tool_use", id: "read-2", name: "read", input: { path: "a.mjs" } },
65
- { type: "tool_use", id: "bash-2", name: "bash", input: { command: "date" } },
66
- ],
67
- },
68
- {
69
- role: "user",
70
- content: [
71
- { type: "tool_result", tool_use_id: "read-2", content: "latest read" },
72
- { type: "tool_result", tool_use_id: "bash-2", content: "latest bash" },
73
- ],
74
- },
75
- ];
76
-
77
- const result = applySessionToolResultDedupe(messages);
78
-
79
- expect(result.changed).toBe(true);
80
- expect(result.stats.deduped).toBe(1);
81
- expect(result.messages[1].content[0].content).toBe('[Read of {"path":"a.mjs"} superseded by later read at msg #3]');
82
- expect(result.messages[3].content[0].content).toBe("latest read");
83
- expect(result.messages[1].content[1].content).toBe("old bash");
84
- expect(result.messages[3].content[1].content).toBe("latest bash");
85
- });
86
-
87
- it("clears thinking and old tool results while keeping the latest eight", () => {
88
- const messages = [
89
- {
90
- role: "assistant",
91
- content: [
92
- { type: "thinking", thinking: "secret" },
93
- { type: "text", text: "answer" },
94
- ],
95
- },
96
- ...Array.from({ length: 10 }, (_, i) => ({
97
- role: "user",
98
- content: [{ type: "tool_result", tool_use_id: `tool-${i}`, content: `result-${i}` }],
99
- })),
100
- ];
101
-
102
- const result = applyContextHintCompaction(messages);
103
-
104
- expect(result.stats).toEqual({ thinkingCleared: 1, toolResultsCleared: 2 });
105
- expect(result.messages[0].content).toEqual([{ type: "text", text: "answer" }]);
106
- expect(result.messages[1].content[0].content).toBe("[Old tool result content cleared]");
107
- expect(result.messages[2].content[0].content).toBe("[Old tool result content cleared]");
108
- expect(result.messages[3].content[0].content).toBe("result-2");
109
- expect(result.messages[10].content[0].content).toBe("result-9");
110
- });
111
-
112
- it("trims a trailing summary block from a past assistant message", () => {
113
- const summary = `Summary: ${"completed the requested implementation. ".repeat(3)}`;
114
- const messages = [
115
- {
116
- role: "assistant",
117
- content: [
118
- { type: "text", text: "Useful detail" },
119
- { type: "text", text: summary },
120
- ],
121
- },
122
- { role: "user", content: "Continue" },
123
- { role: "assistant", content: [{ type: "text", text: "Current response" }] },
124
- ];
125
-
126
- const result = applyTrailingSummaryTrim(messages);
127
-
128
- expect(result).toMatchObject({ changed: true, trimmed: 1 });
129
- expect(result.messages[0].content).toEqual([{ type: "text", text: "Useful detail" }]);
130
- });
131
-
132
- it("dedupes a repeated safe tool result", () => {
133
- const messages = [
134
- {
135
- role: "assistant",
136
- content: [
137
- { type: "tool_use", id: "first", name: "Read", input: { file: "a" } },
138
- { type: "tool_use", id: "second", name: "Read", input: { file: "a" } },
139
- ],
140
- },
141
- {
142
- role: "user",
143
- content: [
144
- { type: "tool_result", tool_use_id: "first", content: "one" },
145
- { type: "tool_result", tool_use_id: "second", content: "two" },
146
- ],
147
- },
148
- ];
149
-
150
- const result = applyToolResultDedupe(messages, { seen: new Map(), safeTools: new Set(["Read"]) });
151
-
152
- expect(result).toMatchObject({ changed: true, deduped: 1 });
153
- expect(result.messages[1].content[1].content).toBe("[Identical to tool_use_id=first]");
154
- });
155
-
156
- it("orders tools stably by name", () => {
157
- const tools = [{ name: "write" }, { name: "grep" }, { name: "read" }];
158
- expect(applyStableToolOrdering(tools).map((tool) => tool.name)).toEqual(["grep", "read", "write"]);
159
- expect(tools.map((tool) => tool.name)).toEqual(["write", "grep", "read"]);
160
- });
161
-
162
- it("defers schemas only for uninvoked configured tools", () => {
163
- const tools = [
164
- { name: "read", input_schema: { type: "object", required: ["path"] } },
165
- { name: "grep", input_schema: { type: "object", required: ["pattern"] } },
166
- ];
167
-
168
- const result = applyToolSchemaDeferral(tools, {
169
- deferred: new Set(["read", "grep"]),
170
- invoked: new Set(["grep"]),
171
- });
172
-
173
- expect(result.deferredCount).toBe(1);
174
- expect(result.tools[0].input_schema).toEqual({ type: "object", properties: {}, additionalProperties: true });
175
- expect(result.tools[1]).toBe(tools[1]);
176
- });
177
-
178
- it("disables thinking for a simple follow-up", () => {
179
- const parsed = {
180
- thinking: { type: "enabled", budget_tokens: 4_096 },
181
- messages: [
182
- { role: "user", content: "Initial request" },
183
- { role: "assistant", content: "Done" },
184
- { role: "user", content: "Thanks" },
185
- ],
186
- };
187
-
188
- expect(applyAdaptiveThinkingZero(parsed)).toEqual({ applied: true, previousBudget: 4_096 });
189
- expect(parsed).not.toHaveProperty("thinking");
190
- expect(parsed.temperature).toBe(1);
191
- });
192
- });
@@ -1,89 +0,0 @@
1
- import { readFileSync } from "node:fs";
2
- import { describe, it, expect } from "vitest";
3
-
4
- import * as tuning from "./tuning.mjs";
5
-
6
- const indexSource = readFileSync(new URL("../index.mjs", import.meta.url), "utf8");
7
-
8
- const expectedValues = {
9
- SERVICE_WIDE_MAX_RETRIES: 2,
10
- CONSECUTIVE_529_FALLBACK_THRESHOLD: 3,
11
- SERVICE_RETRY_BASE_DELAY_SEC: 0.5,
12
- SERVICE_RETRY_BACKOFF_MULTIPLIER: 2,
13
- SERVICE_RETRY_MAX_DELAY_SEC: 3,
14
- SERVICE_RETRY_JITTER_FRACTION: 0.25,
15
- TOKEN_REFRESH_TIMEOUT_MS: 15000,
16
- FOREGROUND_REFRESH_EXPIRY_BUFFER_MS: 300000,
17
- };
18
-
19
- /**
20
- * @param {RegExp} pattern
21
- * @param {number} [group]
22
- * @returns {number}
23
- */
24
- function extractNumber(pattern, group = 1) {
25
- const match = indexSource.match(pattern);
26
- expect(match, `Expected index.mjs to match ${pattern}`).not.toBeNull();
27
- return Number(match[group].replaceAll("_", ""));
28
- }
29
-
30
- describe("tuning constants", () => {
31
- it("exports every documented numeric value", () => {
32
- for (const [name, expected] of Object.entries(expectedValues)) {
33
- expect(tuning).toHaveProperty(name);
34
- expect(typeof tuning[name]).toBe("number");
35
- expect(tuning[name]).toBe(expected);
36
- }
37
- });
38
-
39
- it("wires the service retry budget and fallback threshold to tuning constants", () => {
40
- // Wave 3 P3.2 replaced the inline literals with imported constant references.
41
- expect(indexSource).toMatch(
42
- /const\s+maxServiceRetries\s*=\s*requestClass\s*===\s*"background"[\s\S]*?:\s*SERVICE_WIDE_MAX_RETRIES\s*;/,
43
- );
44
- expect(indexSource).toMatch(/consecutive529Count\s*>=\s*CONSECUTIVE_529_FALLBACK_THRESHOLD/);
45
- expect(indexSource).toMatch(
46
- /import\s*\{[^}]*\bSERVICE_WIDE_MAX_RETRIES\b[^}]*\bCONSECUTIVE_529_FALLBACK_THRESHOLD\b[^}]*\}\s*from\s*"\.\/lib\/tuning\.mjs"/,
47
- );
48
- });
49
-
50
- it("consumes the service retry delay constants in the overload-loop helper", () => {
51
- // The Math.min/jitter delay formula moved out of index.mjs into this helper (P3.2).
52
- const overloadSource = readFileSync(new URL("./retry/overload-loop.mjs", import.meta.url), "utf8");
53
- for (const name of [
54
- "SERVICE_RETRY_BASE_DELAY_SEC",
55
- "SERVICE_RETRY_BACKOFF_MULTIPLIER",
56
- "SERVICE_RETRY_MAX_DELAY_SEC",
57
- "SERVICE_RETRY_JITTER_FRACTION",
58
- ]) {
59
- expect(overloadSource, `overload-loop.mjs must consume ${name}`).toContain(name);
60
- }
61
- expect(overloadSource).toMatch(/from\s*"\.\.\/tuning\.mjs"/);
62
- // The inline delay formula must no longer live in index.mjs.
63
- expect(indexSource).not.toMatch(/1\s*-\s*Math\.random\(\)\s*\*\s*0\.25/);
64
- });
65
-
66
- it("matches the inline token refresh timeout and foreground expiry buffer", () => {
67
- const tokenRefreshTimeout = extractNumber(
68
- /refreshToken\(\s*account\.refreshToken\s*,\s*\{\s*signal:\s*AbortSignal\.timeout\(\s*(\d[\d_]*)\s*\)\s*\}\s*\)/,
69
- );
70
- const foregroundExpiryBuffer = extractNumber(/account\.expires\s*<\s*Date\.now\(\)\s*\+\s*(\d[\d_]*)/);
71
-
72
- expect(tokenRefreshTimeout).toBe(tuning.TOKEN_REFRESH_TIMEOUT_MS);
73
- expect(foregroundExpiryBuffer).toBe(tuning.FOREGROUND_REFRESH_EXPIRY_BUFFER_MS);
74
- });
75
-
76
- it("gives every exported constant an adjacent JSDoc @see tag", () => {
77
- const tuningSource = readFileSync(new URL("./tuning.mjs", import.meta.url), "utf8");
78
- const exports = [...tuningSource.matchAll(/export const\s+([A-Z0-9_]+)\s*=/g)];
79
-
80
- expect(exports.map((match) => match[1])).toEqual(Object.keys(expectedValues));
81
- for (const exported of exports) {
82
- const prefix = tuningSource.slice(0, exported.index);
83
- const adjacentJSDoc = prefix.slice(prefix.lastIndexOf("/**"));
84
- expect(adjacentJSDoc, `${exported[1]} must have an adjacent JSDoc @see tag`).toMatch(
85
- /^\/\*\*[\s\S]*?@see\s+\S[^\n]*[\s\S]*?\*\/\s*$/,
86
- );
87
- }
88
- });
89
- });