opencode-anthropic-fix 0.1.32 → 0.2.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.
- package/README.md +2 -0
- package/cli.mjs +42 -35
- package/index.mjs +399 -3591
- package/lib/config.test.mjs +298 -1
- package/lib/diagnose.mjs +184 -0
- package/lib/diagnose.test.mjs +122 -0
- package/lib/env.mjs +19 -0
- package/lib/env.test.mjs +25 -0
- package/lib/mimicry/cache.mjs +62 -0
- package/lib/mimicry/cache.test.mjs +112 -0
- package/lib/mimicry/headers.mjs +530 -0
- package/lib/mimicry/headers.test.mjs +50 -0
- package/lib/mimicry/models.mjs +146 -0
- package/lib/mimicry/models.test.mjs +132 -0
- package/lib/mimicry/request-body.mjs +684 -0
- package/lib/mimicry/request-body.test.mjs +34 -0
- package/lib/mimicry/request-helpers.mjs +243 -0
- package/lib/mimicry/request-helpers.test.mjs +155 -0
- package/lib/mimicry/response-stream.mjs +429 -0
- package/lib/mimicry/response-stream.test.mjs +127 -0
- package/lib/mimicry/system-prompt.mjs +638 -0
- package/lib/mimicry/system-prompt.test.mjs +73 -0
- package/lib/redact.mjs +147 -0
- package/lib/redact.test.mjs +154 -0
- package/lib/refresh-lock.test.mjs +52 -0
- package/lib/retry/overload-loop.mjs +83 -0
- package/lib/retry/overload-loop.test.mjs +107 -0
- package/lib/session-metrics.mjs +75 -0
- package/lib/session-metrics.test.mjs +67 -0
- package/lib/storage.test.mjs +82 -0
- package/lib/token-economy/microcompact.mjs +35 -0
- package/lib/token-economy/microcompact.test.mjs +28 -0
- package/lib/token-economy/transforms.mjs +637 -0
- package/lib/token-economy/transforms.test.mjs +192 -0
- package/lib/tuning.mjs +70 -0
- package/lib/tuning.test.mjs +89 -0
- package/package.json +4 -1
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { isTruthyEnv } from "../env.mjs";
|
|
2
|
+
|
|
3
|
+
// Perf: module-scope regex reused across per-request hot paths. It does not use
|
|
4
|
+
// the `/g` flag and is consumed via `.test()`, so the shared instance is
|
|
5
|
+
// stateless and safe (no `lastIndex` to reset between calls).
|
|
6
|
+
export const CLAUDE_3_MODEL_RE = /claude-3-/i;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Detects claude-opus-4.6 / claude-opus-4-6 model IDs.
|
|
10
|
+
* These models use adaptive thinking (effort parameter) instead of
|
|
11
|
+
* manual budgetTokens.
|
|
12
|
+
* @param {string | undefined} model
|
|
13
|
+
* @returns {boolean}
|
|
14
|
+
*/
|
|
15
|
+
export function isOpus46Model(model) {
|
|
16
|
+
if (!model) return false;
|
|
17
|
+
// Match standard IDs (claude-opus-4-6, claude-opus-4.6) and Bedrock ARNs
|
|
18
|
+
// (arn:aws:bedrock:...anthropic.claude-opus-4-6-...).
|
|
19
|
+
// Also match bare "opus-4-6" / "opus-4.6" fragments for non-standard strings.
|
|
20
|
+
return /claude-opus-4[._-]6|opus[._-]4[._-]6/i.test(model);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Detects claude-opus-4.7 / claude-opus-4-7 model IDs.
|
|
25
|
+
* @param {string | undefined} model
|
|
26
|
+
* @returns {boolean}
|
|
27
|
+
*/
|
|
28
|
+
export function isOpus47Model(model) {
|
|
29
|
+
if (!model) return false;
|
|
30
|
+
return /claude-opus-4[._-]7|opus[._-]4[._-]7/i.test(model);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Detects claude-opus-4.8 / claude-opus-4-8 model IDs.
|
|
35
|
+
* Opus 4.8 (launched 2026-05-28) is an adaptive-thinking model: manual
|
|
36
|
+
* `thinking: {type: "enabled", budget_tokens}` returns a 400 — it MUST use
|
|
37
|
+
* `thinking: {type: "adaptive"}` + the effort parameter. It also supports
|
|
38
|
+
* `speed: "fast"` (fast-mode research preview) and 1M context by default.
|
|
39
|
+
* @param {string | undefined} model
|
|
40
|
+
* @returns {boolean}
|
|
41
|
+
*/
|
|
42
|
+
export function isOpus48Model(model) {
|
|
43
|
+
if (!model) return false;
|
|
44
|
+
return /claude-opus-4[._-]8|opus[._-]4[._-]8/i.test(model);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Check if a model is eligible for 1M context (can receive context-1m beta).
|
|
49
|
+
* Real CC v2.1.97 U01(): claude-sonnet-4* || opus-4-6 are eligible.
|
|
50
|
+
* Also matches explicit "1m" in the name (e.g. "claude-opus-4-6[1m]").
|
|
51
|
+
* @param {string} model
|
|
52
|
+
* @returns {boolean}
|
|
53
|
+
*/
|
|
54
|
+
export function isEligibleFor1MContext(model) {
|
|
55
|
+
if (!model) return false;
|
|
56
|
+
if (/(^|[-_ ])1m($|[-_ ])|context[-_]?1m|\[1m\]/i.test(model)) return true;
|
|
57
|
+
return (
|
|
58
|
+
/claude-sonnet-4|sonnet[._-]4/i.test(model) || isOpus46Model(model) || isOpus47Model(model) || isOpus48Model(model)
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Check if a model should ALWAYS use 1M context (static mode, no adaptive gating).
|
|
64
|
+
* Only models with explicit "1m" in the name — NOT bare Opus 4.6.
|
|
65
|
+
* When adaptive_context is enabled, Opus 4.6 uses the adaptive decision instead.
|
|
66
|
+
* @param {string} model
|
|
67
|
+
* @returns {boolean}
|
|
68
|
+
*/
|
|
69
|
+
export function hasOneMillionContext(model) {
|
|
70
|
+
return /(^|[-_ ])1m($|[-_ ])|context[-_]?1m/i.test(model);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Detects claude-sonnet-4.6 / claude-sonnet-4-6 model IDs.
|
|
75
|
+
* @param {string | undefined} model
|
|
76
|
+
* @returns {boolean}
|
|
77
|
+
*/
|
|
78
|
+
export function isSonnet46Model(model) {
|
|
79
|
+
if (!model) return false;
|
|
80
|
+
return /claude-sonnet-4[._-]6|sonnet[._-]4[._-]6/i.test(model);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* @param {string | undefined} model
|
|
85
|
+
* @returns {boolean}
|
|
86
|
+
*/
|
|
87
|
+
export function isFable5Model(model) {
|
|
88
|
+
if (!model) return false;
|
|
89
|
+
return /claude-fable-5|fable[._-]5/i.test(model);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* @param {string | undefined} model
|
|
94
|
+
* @returns {boolean}
|
|
95
|
+
*/
|
|
96
|
+
export function isMythos5Model(model) {
|
|
97
|
+
if (!model) return false;
|
|
98
|
+
return /claude-mythos-5|mythos[._-]5/i.test(model);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Detects models that support adaptive thinking ({type: "adaptive"}).
|
|
103
|
+
* Currently: Opus 4.6, Opus 4.7, Opus 4.8, Sonnet 4.6, Fable 5, and Mythos 5.
|
|
104
|
+
* @param {string | undefined} model
|
|
105
|
+
* @returns {boolean}
|
|
106
|
+
*/
|
|
107
|
+
export function isAdaptiveThinkingModel(model) {
|
|
108
|
+
return (
|
|
109
|
+
isOpus46Model(model) ||
|
|
110
|
+
isOpus47Model(model) ||
|
|
111
|
+
isOpus48Model(model) ||
|
|
112
|
+
isSonnet46Model(model) ||
|
|
113
|
+
isFable5Model(model) ||
|
|
114
|
+
isMythos5Model(model)
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* @param {any} thinking
|
|
120
|
+
* @param {string} model
|
|
121
|
+
* @returns {any}
|
|
122
|
+
*/
|
|
123
|
+
export function normalizeThinkingBlock(thinking, model) {
|
|
124
|
+
// If thinking is absent or not an object, pass through
|
|
125
|
+
if (!thinking || typeof thinking !== "object") {
|
|
126
|
+
return thinking;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Adaptive thinking models always get { type: "adaptive" }
|
|
130
|
+
// regardless of what format the incoming thinking block has
|
|
131
|
+
if (isAdaptiveThinkingModel(model)) {
|
|
132
|
+
// Check for env-var override to force budget_tokens fallback
|
|
133
|
+
if (isTruthyEnv(process.env.OPENCODE_ANTHROPIC_DISABLE_ADAPTIVE_THINKING)) {
|
|
134
|
+
// Fallback: return as-is if already budget_tokens shape, otherwise default
|
|
135
|
+
if (thinking.type === "enabled" && typeof thinking.budget_tokens === "number") {
|
|
136
|
+
return thinking;
|
|
137
|
+
}
|
|
138
|
+
const parsedBudget = parseInt(process.env.MAX_THINKING_TOKENS, 10);
|
|
139
|
+
return { type: "enabled", budget_tokens: Number.isNaN(parsedBudget) ? 16000 : parsedBudget };
|
|
140
|
+
}
|
|
141
|
+
return { type: "adaptive" };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Non-adaptive models: pass through unchanged
|
|
145
|
+
return thinking;
|
|
146
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
CLAUDE_3_MODEL_RE,
|
|
5
|
+
hasOneMillionContext,
|
|
6
|
+
isAdaptiveThinkingModel,
|
|
7
|
+
isEligibleFor1MContext,
|
|
8
|
+
isFable5Model,
|
|
9
|
+
isMythos5Model,
|
|
10
|
+
isOpus46Model,
|
|
11
|
+
isOpus47Model,
|
|
12
|
+
isOpus48Model,
|
|
13
|
+
isSonnet46Model,
|
|
14
|
+
normalizeThinkingBlock,
|
|
15
|
+
} from "./models.mjs";
|
|
16
|
+
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
vi.unstubAllEnvs();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
describe("model detection", () => {
|
|
22
|
+
it("detects Claude 3 model IDs", () => {
|
|
23
|
+
expect(CLAUDE_3_MODEL_RE.test("claude-3-5-sonnet-20241022")).toBe(true);
|
|
24
|
+
expect(CLAUDE_3_MODEL_RE.test("claude-sonnet-4-6")).toBe(false);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("detects Opus 4.6 model IDs", () => {
|
|
28
|
+
expect(isOpus46Model("claude-opus-4-6-20260205")).toBe(true);
|
|
29
|
+
expect(isOpus46Model("arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-opus-4.6-v1")).toBe(true);
|
|
30
|
+
expect(isOpus46Model("claude-opus-4-7")).toBe(false);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("detects Opus 4.7 model IDs", () => {
|
|
34
|
+
expect(isOpus47Model("claude-opus-4.7")).toBe(true);
|
|
35
|
+
expect(isOpus47Model("opus_4_7-eap")).toBe(true);
|
|
36
|
+
expect(isOpus47Model("claude-opus-4-8")).toBe(false);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("detects Opus 4.8 model IDs", () => {
|
|
40
|
+
expect(isOpus48Model("claude-opus-4-8")).toBe(true);
|
|
41
|
+
expect(isOpus48Model("opus.4.8-fast")).toBe(true);
|
|
42
|
+
expect(isOpus48Model("claude-opus-4-7")).toBe(false);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("detects Sonnet 4.6 model IDs", () => {
|
|
46
|
+
expect(isSonnet46Model("claude-sonnet-4-6")).toBe(true);
|
|
47
|
+
expect(isSonnet46Model("sonnet.4.6")).toBe(true);
|
|
48
|
+
expect(isSonnet46Model("claude-sonnet-4-5")).toBe(false);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("detects 1M-context eligibility and explicit context markers", () => {
|
|
52
|
+
expect(isEligibleFor1MContext("claude-sonnet-4-5")).toBe(true);
|
|
53
|
+
expect(isEligibleFor1MContext("claude-opus-4-6[1m]")).toBe(true);
|
|
54
|
+
expect(hasOneMillionContext("claude-opus-4-6-1m")).toBe(true);
|
|
55
|
+
expect(hasOneMillionContext("claude-opus-4-6[1m]")).toBe(false);
|
|
56
|
+
expect(hasOneMillionContext("claude-opus-4-6")).toBe(false);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("detects Fable 5 model IDs", () => {
|
|
60
|
+
expect(isFable5Model("claude-fable-5")).toBe(true);
|
|
61
|
+
expect(isFable5Model("fable_5-preview")).toBe(true);
|
|
62
|
+
expect(isFable5Model("claude-mythos-5")).toBe(false);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("detects Mythos 5 model IDs", () => {
|
|
66
|
+
expect(isMythos5Model("claude-mythos-5")).toBe(true);
|
|
67
|
+
expect(isMythos5Model("mythos.5-preview")).toBe(true);
|
|
68
|
+
expect(isMythos5Model("claude-fable-5")).toBe(false);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it.each([
|
|
72
|
+
"claude-opus-4-6",
|
|
73
|
+
"claude-opus-4-7",
|
|
74
|
+
"claude-opus-4-8",
|
|
75
|
+
"claude-sonnet-4-6",
|
|
76
|
+
"claude-fable-5",
|
|
77
|
+
"claude-mythos-5",
|
|
78
|
+
])("recognizes %s as an adaptive-thinking model", (model) => {
|
|
79
|
+
expect(isAdaptiveThinkingModel(model)).toBe(true);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("rejects non-adaptive models", () => {
|
|
83
|
+
expect(isAdaptiveThinkingModel("claude-sonnet-4-5")).toBe(false);
|
|
84
|
+
expect(isAdaptiveThinkingModel(undefined)).toBe(false);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe("normalizeThinkingBlock", () => {
|
|
89
|
+
it("passes absent and non-object values through", () => {
|
|
90
|
+
expect(normalizeThinkingBlock(undefined, "claude-opus-4-8")).toBeUndefined();
|
|
91
|
+
expect(normalizeThinkingBlock("enabled", "claude-opus-4-8")).toBe("enabled");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("passes a non-adaptive model's block through unchanged", () => {
|
|
95
|
+
const thinking = { type: "enabled", budget_tokens: 8000 };
|
|
96
|
+
|
|
97
|
+
expect(normalizeThinkingBlock(thinking, "claude-sonnet-4-5")).toBe(thinking);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("normalizes adaptive models to adaptive thinking", () => {
|
|
101
|
+
expect(normalizeThinkingBlock({ type: "enabled", budget_tokens: 8000 }, "claude-opus-4-8")).toEqual({
|
|
102
|
+
type: "adaptive",
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("preserves an enabled block when adaptive thinking is disabled", () => {
|
|
107
|
+
vi.stubEnv("OPENCODE_ANTHROPIC_DISABLE_ADAPTIVE_THINKING", "1");
|
|
108
|
+
const thinking = { type: "enabled", budget_tokens: 12000 };
|
|
109
|
+
|
|
110
|
+
expect(normalizeThinkingBlock(thinking, "claude-opus-4-8")).toBe(thinking);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("uses the configured fallback budget when adaptive thinking is disabled", () => {
|
|
114
|
+
vi.stubEnv("OPENCODE_ANTHROPIC_DISABLE_ADAPTIVE_THINKING", "true");
|
|
115
|
+
vi.stubEnv("MAX_THINKING_TOKENS", "24000");
|
|
116
|
+
|
|
117
|
+
expect(normalizeThinkingBlock({ type: "adaptive" }, "claude-sonnet-4-6")).toEqual({
|
|
118
|
+
type: "enabled",
|
|
119
|
+
budget_tokens: 24000,
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("uses the default fallback budget when the configured value is invalid", () => {
|
|
124
|
+
vi.stubEnv("OPENCODE_ANTHROPIC_DISABLE_ADAPTIVE_THINKING", "yes");
|
|
125
|
+
vi.stubEnv("MAX_THINKING_TOKENS", "invalid");
|
|
126
|
+
|
|
127
|
+
expect(normalizeThinkingBlock({}, "claude-fable-5")).toEqual({
|
|
128
|
+
type: "enabled",
|
|
129
|
+
budget_tokens: 16000,
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
});
|