opencode-anthropic-fix 0.1.32 → 0.2.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 +9 -6
- package/cli.mjs +43 -35
- package/index.mjs +377 -3646
- package/lib/backoff.mjs +1 -1
- package/lib/cc-credentials.mjs +1 -1
- package/lib/config.mjs +2 -2
- package/lib/config.test.mjs +305 -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 +528 -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 +682 -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/rotation.mjs +16 -0
- package/lib/rotation.test.mjs +46 -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
package/lib/backoff.mjs
CHANGED
|
@@ -134,7 +134,7 @@ function extractErrorSignals(body) {
|
|
|
134
134
|
* @returns {boolean}
|
|
135
135
|
*/
|
|
136
136
|
function bodyHasAccountError(body) {
|
|
137
|
-
const { errorType, message
|
|
137
|
+
const { errorType, message } = extractErrorSignals(body);
|
|
138
138
|
|
|
139
139
|
const typeSignals = [
|
|
140
140
|
"rate_limit",
|
package/lib/cc-credentials.mjs
CHANGED
package/lib/config.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import { homedir } from "node:os";
|
|
|
4
4
|
import { randomBytes } from "node:crypto";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* @typedef {'sticky' | 'round-robin' | 'hybrid'} AccountSelectionStrategy
|
|
7
|
+
* @typedef {'sticky' | 'round-robin' | 'hybrid' | 'single'} AccountSelectionStrategy
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
/**
|
|
@@ -449,7 +449,7 @@ export const DEFAULT_CONFIG = {
|
|
|
449
449
|
},
|
|
450
450
|
};
|
|
451
451
|
|
|
452
|
-
export const VALID_STRATEGIES = ["sticky", "round-robin", "hybrid"];
|
|
452
|
+
export const VALID_STRATEGIES = ["sticky", "round-robin", "hybrid", "single"];
|
|
453
453
|
|
|
454
454
|
/** OpenCode's OAuth client ID for Anthropic console auth flows. */
|
|
455
455
|
export const CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
|
package/lib/config.test.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
-
import { loadConfig, DEFAULT_CONFIG, getConfigDir, getConfigPath } from "./config.mjs";
|
|
2
|
+
import { loadConfig, loadRawConfig, DEFAULT_CONFIG, getConfigDir, getConfigPath } from "./config.mjs";
|
|
3
3
|
import { existsSync, readFileSync } from "node:fs";
|
|
4
4
|
|
|
5
5
|
// Mock fs module
|
|
@@ -49,6 +49,28 @@ describe("getConfigDir", () => {
|
|
|
49
49
|
const dir = getConfigDir();
|
|
50
50
|
expect(dir.endsWith("opencode")).toBe(true);
|
|
51
51
|
});
|
|
52
|
+
|
|
53
|
+
it("uses APPDATA on Windows", () => {
|
|
54
|
+
const originalAppData = process.env.APPDATA;
|
|
55
|
+
process.env.APPDATA = "C:\\Users\\test\\AppData\\Roaming";
|
|
56
|
+
|
|
57
|
+
expect(getConfigDir()).toBe("C:\\Users\\test\\AppData\\Roaming\\opencode");
|
|
58
|
+
|
|
59
|
+
if (originalAppData === undefined) delete process.env.APPDATA;
|
|
60
|
+
else process.env.APPDATA = originalAppData;
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("uses XDG_CONFIG_HOME on non-Windows platforms", () => {
|
|
64
|
+
const platform = vi.spyOn(process, "platform", "get").mockReturnValue("linux");
|
|
65
|
+
const originalXdgConfigHome = process.env.XDG_CONFIG_HOME;
|
|
66
|
+
process.env.XDG_CONFIG_HOME = "/tmp/custom-config";
|
|
67
|
+
|
|
68
|
+
expect(getConfigDir()).toBe("\\tmp\\custom-config\\opencode");
|
|
69
|
+
|
|
70
|
+
platform.mockRestore();
|
|
71
|
+
if (originalXdgConfigHome === undefined) delete process.env.XDG_CONFIG_HOME;
|
|
72
|
+
else process.env.XDG_CONFIG_HOME = originalXdgConfigHome;
|
|
73
|
+
});
|
|
52
74
|
});
|
|
53
75
|
|
|
54
76
|
describe("getConfigPath", () => {
|
|
@@ -70,7 +92,12 @@ describe("loadConfig", () => {
|
|
|
70
92
|
delete process.env.OPENCODE_ANTHROPIC_EMULATE_CLAUDE_CODE_SIGNATURE;
|
|
71
93
|
delete process.env.OPENCODE_ANTHROPIC_FETCH_CLAUDE_CODE_VERSION;
|
|
72
94
|
delete process.env.OPENCODE_ANTHROPIC_PROMPT_COMPACTION;
|
|
95
|
+
delete process.env.OPENCODE_ANTHROPIC_OVERRIDE_MODEL_LIMITS;
|
|
96
|
+
delete process.env.OPENCODE_ANTHROPIC_CC_CREDENTIALS;
|
|
73
97
|
delete process.env.OPENCODE_ANTHROPIC_ADAPTIVE_CONTEXT;
|
|
98
|
+
delete process.env.OPENCODE_ANTHROPIC_PROACTIVE_DISABLED;
|
|
99
|
+
delete process.env.OPENCODE_ANTHROPIC_ANTI_VERBOSITY;
|
|
100
|
+
delete process.env.OPENCODE_ANTHROPIC_LENGTH_ANCHORS;
|
|
74
101
|
});
|
|
75
102
|
|
|
76
103
|
afterEach(() => {
|
|
@@ -111,6 +138,13 @@ describe("loadConfig", () => {
|
|
|
111
138
|
expect(config.account_selection_strategy).toBe("sticky");
|
|
112
139
|
});
|
|
113
140
|
|
|
141
|
+
it("accepts 'single' strategy from config file", () => {
|
|
142
|
+
existsSync.mockReturnValue(true);
|
|
143
|
+
readFileSync.mockReturnValue(JSON.stringify({ account_selection_strategy: "single" }));
|
|
144
|
+
const config = loadConfig();
|
|
145
|
+
expect(config.account_selection_strategy).toBe("single");
|
|
146
|
+
});
|
|
147
|
+
|
|
114
148
|
it("ignores invalid strategy", () => {
|
|
115
149
|
existsSync.mockReturnValue(true);
|
|
116
150
|
readFileSync.mockReturnValue(JSON.stringify({ account_selection_strategy: "invalid" }));
|
|
@@ -188,6 +222,206 @@ describe("loadConfig", () => {
|
|
|
188
222
|
expect(config.token_bucket.regeneration_rate_per_minute).toBe(6);
|
|
189
223
|
});
|
|
190
224
|
|
|
225
|
+
it("validates and merges every supported nested config section", () => {
|
|
226
|
+
existsSync.mockReturnValue(true);
|
|
227
|
+
readFileSync.mockReturnValue(
|
|
228
|
+
JSON.stringify({
|
|
229
|
+
failure_ttl_seconds: 9_000,
|
|
230
|
+
signature_emulation: {
|
|
231
|
+
enabled: false,
|
|
232
|
+
fetch_claude_code_version_on_startup: false,
|
|
233
|
+
prompt_compaction: "off",
|
|
234
|
+
workload: " nightly-agent ",
|
|
235
|
+
},
|
|
236
|
+
override_model_limits: { enabled: true, context: 3_000_000, output: -1 },
|
|
237
|
+
custom_betas: [" beta-one ", "", 42, "beta-two"],
|
|
238
|
+
health_score: {
|
|
239
|
+
initial: 80,
|
|
240
|
+
success_reward: 5,
|
|
241
|
+
rate_limit_penalty: -15,
|
|
242
|
+
failure_penalty: -30,
|
|
243
|
+
recovery_rate_per_hour: 4,
|
|
244
|
+
min_usable: 60,
|
|
245
|
+
max_score: 90,
|
|
246
|
+
},
|
|
247
|
+
token_bucket: { max_tokens: 75, regeneration_rate_per_minute: 8, initial_tokens: 70 },
|
|
248
|
+
toasts: { quiet: true, debounce_seconds: 12 },
|
|
249
|
+
headers: {
|
|
250
|
+
emulation_profile: " 2.1.0 ",
|
|
251
|
+
overrides: { "x-test": "value", ignored: 7, "": "empty-key" },
|
|
252
|
+
disable: [" X-REMOVE ", 10, "", "X-OTHER"],
|
|
253
|
+
billing_header: false,
|
|
254
|
+
},
|
|
255
|
+
idle_refresh: { enabled: false, window_minutes: 90, min_interval_minutes: 45 },
|
|
256
|
+
cache_policy: {
|
|
257
|
+
ttl: "5m",
|
|
258
|
+
ttl_supported: false,
|
|
259
|
+
boundary_marker: true,
|
|
260
|
+
hit_rate_warning_threshold: 2,
|
|
261
|
+
},
|
|
262
|
+
fast_mode: true,
|
|
263
|
+
telemetry: { emulate_minimal: true },
|
|
264
|
+
usage_toast: true,
|
|
265
|
+
cc_credential_reuse: { enabled: true, auto_detect: false, prefer_over_oauth: true },
|
|
266
|
+
adaptive_context: { enabled: false, escalation_threshold: 200_000, deescalation_threshold: 220_000 },
|
|
267
|
+
willow_mode: { enabled: false, idle_threshold_minutes: 45, cooldown_minutes: 90, min_turns_before_suggest: 5 },
|
|
268
|
+
token_economy: {
|
|
269
|
+
token_efficient_tools: true,
|
|
270
|
+
redact_thinking: false,
|
|
271
|
+
context_hint: false,
|
|
272
|
+
conservative: false,
|
|
273
|
+
debug_dump_bodies: true,
|
|
274
|
+
ttl_thinking_strip: false,
|
|
275
|
+
proactive_microcompact: false,
|
|
276
|
+
microcompact_percent: 20,
|
|
277
|
+
microcompact_keep_recent: 100,
|
|
278
|
+
stable_tool_ordering: false,
|
|
279
|
+
deferred_tool_names: ["read", 12, "grep"],
|
|
280
|
+
adaptive_thinking_zero_simple: false,
|
|
281
|
+
tool_result_dedupe: true,
|
|
282
|
+
fast_mode_auto: true,
|
|
283
|
+
trailing_summary_trim: true,
|
|
284
|
+
role_scoped_cache_ttl: false,
|
|
285
|
+
lean_system_non_main: false,
|
|
286
|
+
simple_system_prompt: false,
|
|
287
|
+
},
|
|
288
|
+
token_economy_strategies: {
|
|
289
|
+
system_prompt_tailing: true,
|
|
290
|
+
system_prompt_tail_turns: 0,
|
|
291
|
+
system_prompt_tail_max_chars: 100_000,
|
|
292
|
+
tool_deferral: true,
|
|
293
|
+
tool_description_compaction: true,
|
|
294
|
+
adaptive_tool_set: true,
|
|
295
|
+
tool_result_dedupe_session_wide: true,
|
|
296
|
+
haiku_rolling_summary: true,
|
|
297
|
+
stale_read_eviction: true,
|
|
298
|
+
per_tool_class_prune: true,
|
|
299
|
+
},
|
|
300
|
+
output_cap: { enabled: false, default_max_tokens: 100, escalated_max_tokens: 300_000 },
|
|
301
|
+
preconnect: { enabled: false, timeout_ms: 50 },
|
|
302
|
+
overflow_recovery: { enabled: false, safety_margin: 20_000 },
|
|
303
|
+
cache_break_detection: { enabled: false, alert_threshold: 1_500, adaptive_breakpoint: false },
|
|
304
|
+
request_classification: {
|
|
305
|
+
enabled: false,
|
|
306
|
+
background_max_service_retries: 3,
|
|
307
|
+
background_max_should_retries: 4,
|
|
308
|
+
},
|
|
309
|
+
token_budget: { enabled: true, default: 25_000, completion_threshold: 2 },
|
|
310
|
+
microcompact: { enabled: false, threshold_percent: 120 },
|
|
311
|
+
overload_recovery: { enabled: false, default_cooldown_ms: 500, poll_quota_on_overload: false },
|
|
312
|
+
account_management: { proactive_disabled: false },
|
|
313
|
+
anti_verbosity: { enabled: false, length_anchors: false },
|
|
314
|
+
oauth: { sdk_token_useragent: false },
|
|
315
|
+
}),
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
const config = loadConfig();
|
|
319
|
+
|
|
320
|
+
expect(config.failure_ttl_seconds).toBe(7_200);
|
|
321
|
+
expect(config.signature_emulation).toEqual({
|
|
322
|
+
enabled: false,
|
|
323
|
+
fetch_claude_code_version_on_startup: false,
|
|
324
|
+
prompt_compaction: "off",
|
|
325
|
+
workload: "nightly-agent",
|
|
326
|
+
});
|
|
327
|
+
expect(config.override_model_limits).toEqual({ enabled: true, context: 2_000_000, output: 0 });
|
|
328
|
+
expect(config.custom_betas).toEqual(["beta-one", "beta-two"]);
|
|
329
|
+
expect(config.health_score).toEqual({
|
|
330
|
+
initial: 80,
|
|
331
|
+
success_reward: 5,
|
|
332
|
+
rate_limit_penalty: -15,
|
|
333
|
+
failure_penalty: -30,
|
|
334
|
+
recovery_rate_per_hour: 4,
|
|
335
|
+
min_usable: 60,
|
|
336
|
+
max_score: 90,
|
|
337
|
+
});
|
|
338
|
+
expect(config.token_bucket).toEqual({ max_tokens: 75, regeneration_rate_per_minute: 8, initial_tokens: 70 });
|
|
339
|
+
expect(config.toasts).toEqual({ quiet: true, debounce_seconds: 12 });
|
|
340
|
+
expect(config.headers).toEqual({
|
|
341
|
+
emulation_profile: "2.1.0",
|
|
342
|
+
overrides: { "x-test": "value" },
|
|
343
|
+
disable: ["x-remove", "x-other"],
|
|
344
|
+
billing_header: false,
|
|
345
|
+
});
|
|
346
|
+
expect(config.idle_refresh).toEqual({ enabled: false, window_minutes: 90, min_interval_minutes: 45 });
|
|
347
|
+
expect(config.cache_policy).toEqual({
|
|
348
|
+
ttl: "5m",
|
|
349
|
+
ttl_supported: false,
|
|
350
|
+
boundary_marker: true,
|
|
351
|
+
hit_rate_warning_threshold: 1,
|
|
352
|
+
});
|
|
353
|
+
expect(config.fast_mode).toBe(true);
|
|
354
|
+
expect(config.telemetry.emulate_minimal).toBe(true);
|
|
355
|
+
expect(config.usage_toast).toBe(true);
|
|
356
|
+
expect(config.cc_credential_reuse).toEqual({ enabled: true, auto_detect: false, prefer_over_oauth: true });
|
|
357
|
+
expect(config.adaptive_context).toEqual({
|
|
358
|
+
enabled: false,
|
|
359
|
+
escalation_threshold: 200_000,
|
|
360
|
+
deescalation_threshold: 150_000,
|
|
361
|
+
});
|
|
362
|
+
expect(config.willow_mode).toEqual({
|
|
363
|
+
enabled: false,
|
|
364
|
+
idle_threshold_minutes: 45,
|
|
365
|
+
cooldown_minutes: 90,
|
|
366
|
+
min_turns_before_suggest: 5,
|
|
367
|
+
});
|
|
368
|
+
expect(config.token_economy).toEqual({
|
|
369
|
+
token_efficient_tools: true,
|
|
370
|
+
redact_thinking: false,
|
|
371
|
+
context_hint: false,
|
|
372
|
+
conservative: false,
|
|
373
|
+
debug_dump_bodies: true,
|
|
374
|
+
ttl_thinking_strip: false,
|
|
375
|
+
proactive_microcompact: false,
|
|
376
|
+
microcompact_percent: 30,
|
|
377
|
+
microcompact_keep_recent: 64,
|
|
378
|
+
stable_tool_ordering: false,
|
|
379
|
+
deferred_tool_names: ["read", "grep"],
|
|
380
|
+
adaptive_thinking_zero_simple: false,
|
|
381
|
+
tool_result_dedupe: true,
|
|
382
|
+
fast_mode_auto: true,
|
|
383
|
+
trailing_summary_trim: true,
|
|
384
|
+
role_scoped_cache_ttl: false,
|
|
385
|
+
lean_system_non_main: false,
|
|
386
|
+
simple_system_prompt: false,
|
|
387
|
+
});
|
|
388
|
+
expect(config.token_economy_strategies).toEqual({
|
|
389
|
+
system_prompt_tailing: true,
|
|
390
|
+
system_prompt_tail_turns: 1,
|
|
391
|
+
system_prompt_tail_max_chars: 50_000,
|
|
392
|
+
tool_deferral: true,
|
|
393
|
+
tool_description_compaction: true,
|
|
394
|
+
adaptive_tool_set: true,
|
|
395
|
+
tool_result_dedupe_session_wide: true,
|
|
396
|
+
haiku_rolling_summary: true,
|
|
397
|
+
stale_read_eviction: true,
|
|
398
|
+
per_tool_class_prune: true,
|
|
399
|
+
});
|
|
400
|
+
expect(config.output_cap).toEqual({ enabled: false, default_max_tokens: 256, escalated_max_tokens: 200_000 });
|
|
401
|
+
expect(config.preconnect).toEqual({ enabled: false, timeout_ms: 100 });
|
|
402
|
+
expect(config.overflow_recovery).toEqual({ enabled: false, safety_margin: 10_000 });
|
|
403
|
+
expect(config.cache_break_detection).toEqual({
|
|
404
|
+
enabled: false,
|
|
405
|
+
alert_threshold: 1_500,
|
|
406
|
+
adaptive_breakpoint: false,
|
|
407
|
+
});
|
|
408
|
+
expect(config.request_classification).toEqual({
|
|
409
|
+
enabled: false,
|
|
410
|
+
background_max_service_retries: 3,
|
|
411
|
+
background_max_should_retries: 4,
|
|
412
|
+
});
|
|
413
|
+
expect(config.token_budget).toEqual({ enabled: true, default: 25_000, completion_threshold: 1 });
|
|
414
|
+
expect(config.microcompact).toEqual({ enabled: false, threshold_percent: 100 });
|
|
415
|
+
expect(config.overload_recovery).toEqual({
|
|
416
|
+
enabled: false,
|
|
417
|
+
default_cooldown_ms: 1_000,
|
|
418
|
+
poll_quota_on_overload: false,
|
|
419
|
+
});
|
|
420
|
+
expect(config.account_management.proactive_disabled).toBe(false);
|
|
421
|
+
expect(config.anti_verbosity).toEqual({ enabled: false, length_anchors: false });
|
|
422
|
+
expect(config.oauth.sdk_token_useragent).toBe(false);
|
|
423
|
+
});
|
|
424
|
+
|
|
191
425
|
// Environment variable overrides
|
|
192
426
|
it("overrides strategy from OPENCODE_ANTHROPIC_STRATEGY", () => {
|
|
193
427
|
existsSync.mockReturnValue(false);
|
|
@@ -378,6 +612,76 @@ describe("loadConfig", () => {
|
|
|
378
612
|
const config = loadConfig();
|
|
379
613
|
expect(config.adaptive_context.enabled).toBe(false);
|
|
380
614
|
});
|
|
615
|
+
|
|
616
|
+
it("applies true-form environment overrides for feature switches", () => {
|
|
617
|
+
existsSync.mockReturnValue(true);
|
|
618
|
+
readFileSync.mockReturnValue(
|
|
619
|
+
JSON.stringify({
|
|
620
|
+
signature_emulation: { enabled: false, fetch_claude_code_version_on_startup: false, prompt_compaction: "off" },
|
|
621
|
+
override_model_limits: { enabled: false },
|
|
622
|
+
cc_credential_reuse: { enabled: false },
|
|
623
|
+
account_management: { proactive_disabled: false },
|
|
624
|
+
anti_verbosity: { enabled: false },
|
|
625
|
+
}),
|
|
626
|
+
);
|
|
627
|
+
process.env.OPENCODE_ANTHROPIC_EMULATE_CLAUDE_CODE_SIGNATURE = "true";
|
|
628
|
+
process.env.OPENCODE_ANTHROPIC_FETCH_CLAUDE_CODE_VERSION = "true";
|
|
629
|
+
process.env.OPENCODE_ANTHROPIC_PROMPT_COMPACTION = "minimal";
|
|
630
|
+
process.env.OPENCODE_ANTHROPIC_OVERRIDE_MODEL_LIMITS = "true";
|
|
631
|
+
process.env.OPENCODE_ANTHROPIC_CC_CREDENTIALS = "true";
|
|
632
|
+
process.env.OPENCODE_ANTHROPIC_PROACTIVE_DISABLED = "true";
|
|
633
|
+
process.env.OPENCODE_ANTHROPIC_ANTI_VERBOSITY = "true";
|
|
634
|
+
|
|
635
|
+
const config = loadConfig();
|
|
636
|
+
|
|
637
|
+
expect(config.signature_emulation).toMatchObject({
|
|
638
|
+
enabled: true,
|
|
639
|
+
fetch_claude_code_version_on_startup: true,
|
|
640
|
+
prompt_compaction: "minimal",
|
|
641
|
+
});
|
|
642
|
+
expect(config.override_model_limits.enabled).toBe(true);
|
|
643
|
+
expect(config.cc_credential_reuse.enabled).toBe(true);
|
|
644
|
+
expect(config.account_management.proactive_disabled).toBe(true);
|
|
645
|
+
expect(config.anti_verbosity.enabled).toBe(true);
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
it("applies false-form environment overrides for feature switches", () => {
|
|
649
|
+
existsSync.mockReturnValue(false);
|
|
650
|
+
process.env.OPENCODE_ANTHROPIC_OVERRIDE_MODEL_LIMITS = "false";
|
|
651
|
+
process.env.OPENCODE_ANTHROPIC_CC_CREDENTIALS = "false";
|
|
652
|
+
process.env.OPENCODE_ANTHROPIC_PROACTIVE_DISABLED = "false";
|
|
653
|
+
process.env.OPENCODE_ANTHROPIC_ANTI_VERBOSITY = "false";
|
|
654
|
+
process.env.OPENCODE_ANTHROPIC_LENGTH_ANCHORS = "false";
|
|
655
|
+
|
|
656
|
+
const config = loadConfig();
|
|
657
|
+
|
|
658
|
+
expect(config.override_model_limits.enabled).toBe(false);
|
|
659
|
+
expect(config.cc_credential_reuse.enabled).toBe(false);
|
|
660
|
+
expect(config.account_management.proactive_disabled).toBe(false);
|
|
661
|
+
expect(config.anti_verbosity).toEqual({ enabled: false, length_anchors: false });
|
|
662
|
+
});
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
describe("loadRawConfig", () => {
|
|
666
|
+
beforeEach(() => {
|
|
667
|
+
vi.resetAllMocks();
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
it("returns parsed values without applying validation", () => {
|
|
671
|
+
existsSync.mockReturnValue(true);
|
|
672
|
+
readFileSync.mockReturnValue(JSON.stringify({ failure_ttl_seconds: -10, custom_key: "preserved" }));
|
|
673
|
+
|
|
674
|
+
expect(loadRawConfig()).toEqual({ failure_ttl_seconds: -10, custom_key: "preserved" });
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
it("returns an empty object for missing, malformed, or non-object config", () => {
|
|
678
|
+
existsSync.mockReturnValueOnce(false).mockReturnValue(true);
|
|
679
|
+
readFileSync.mockReturnValueOnce("invalid json").mockReturnValueOnce("[]");
|
|
680
|
+
|
|
681
|
+
expect(loadRawConfig()).toEqual({});
|
|
682
|
+
expect(loadRawConfig()).toEqual({});
|
|
683
|
+
expect(loadRawConfig()).toEqual({});
|
|
684
|
+
});
|
|
381
685
|
});
|
|
382
686
|
|
|
383
687
|
describe("token_economy_strategies.haiku_rolling_summary", () => {
|
package/lib/diagnose.mjs
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
import { getConfigDir, loadConfig } from "./config.mjs";
|
|
7
|
+
import { redactSecrets } from "./redact.mjs";
|
|
8
|
+
import { FALLBACK_CLAUDE_CLI_VERSION } from "./request-headers.mjs";
|
|
9
|
+
import { loadAccounts } from "./storage.mjs";
|
|
10
|
+
|
|
11
|
+
const ENV_KEYS = [
|
|
12
|
+
"OPENCODE_ANTHROPIC_STRATEGY",
|
|
13
|
+
"OPENCODE_ANTHROPIC_DEBUG",
|
|
14
|
+
"OPENCODE_ANTHROPIC_MAX_BUDGET_USD",
|
|
15
|
+
"INITIAL_ACCOUNT",
|
|
16
|
+
"TELEMETRY_EMULATE",
|
|
17
|
+
"IGNORE_BUDGET",
|
|
18
|
+
"SIGNATURE_USER_ID",
|
|
19
|
+
"DISABLE_ADAPTIVE_THINKING",
|
|
20
|
+
"DISABLE_FAST_MODE",
|
|
21
|
+
"STREAM_IDLE_TIMEOUT_MS",
|
|
22
|
+
"CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS",
|
|
23
|
+
"DISABLE_1M_CONTEXT",
|
|
24
|
+
"ORGANIZATION_UUID",
|
|
25
|
+
"ENTRYPOINT",
|
|
26
|
+
"USE_BEDROCK",
|
|
27
|
+
"USE_FOUNDRY",
|
|
28
|
+
"USE_ANTHROPIC_AWS",
|
|
29
|
+
"USE_MANTLE",
|
|
30
|
+
"USE_VERTEX",
|
|
31
|
+
"ACCOUNT_UUID",
|
|
32
|
+
"EXTRA_METADATA",
|
|
33
|
+
"ATTRIBUTION_HEADER",
|
|
34
|
+
"WORKLOAD",
|
|
35
|
+
"FORCE_GLOBAL_CACHE",
|
|
36
|
+
"BACKGROUND",
|
|
37
|
+
"CONTAINER_ID",
|
|
38
|
+
"REMOTE_SESSION_ID",
|
|
39
|
+
"ADDITIONAL_PROTECTION",
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
function getPluginVersion() {
|
|
43
|
+
try {
|
|
44
|
+
const packagePath = fileURLToPath(new URL("../package.json", import.meta.url));
|
|
45
|
+
const packageJson = JSON.parse(readFileSync(packagePath, "utf8"));
|
|
46
|
+
return typeof packageJson.version === "string" ? packageJson.version : "unknown";
|
|
47
|
+
} catch {
|
|
48
|
+
return "unknown";
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function getRuntimeEnv() {
|
|
53
|
+
const env = {};
|
|
54
|
+
for (const key of ENV_KEYS) {
|
|
55
|
+
if (process.env[key] !== undefined) env[key] = process.env[key];
|
|
56
|
+
}
|
|
57
|
+
return redactSecrets(env);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function getAccountSummary() {
|
|
61
|
+
try {
|
|
62
|
+
const stored = await loadAccounts();
|
|
63
|
+
if (!stored || !Array.isArray(stored.accounts)) return { count: 0, items: [] };
|
|
64
|
+
|
|
65
|
+
const items = stored.accounts.map((account, index) => {
|
|
66
|
+
const item = { index };
|
|
67
|
+
for (const key of ["email", "disabled", "enabled", "consecutiveFailures", "rateLimitResetTimes"]) {
|
|
68
|
+
if (Object.hasOwn(account, key)) item[key] = account[key];
|
|
69
|
+
}
|
|
70
|
+
return item;
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
return redactSecrets({ count: items.length, items });
|
|
74
|
+
} catch {
|
|
75
|
+
return { count: 0, items: [] };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function sortedArtifactFiles(directory, prefix, extension) {
|
|
80
|
+
try {
|
|
81
|
+
if (!existsSync(directory)) return [];
|
|
82
|
+
return readdirSync(directory)
|
|
83
|
+
.filter((file) => file.startsWith(prefix) && file.endsWith(extension))
|
|
84
|
+
.sort()
|
|
85
|
+
.slice(-10);
|
|
86
|
+
} catch {
|
|
87
|
+
return [];
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function requestDumpMetadata(directory) {
|
|
92
|
+
return sortedArtifactFiles(directory, "req-", ".json").map((file) => {
|
|
93
|
+
try {
|
|
94
|
+
const filePath = path.join(directory, file);
|
|
95
|
+
const envelope = JSON.parse(readFileSync(filePath, "utf8"));
|
|
96
|
+
return {
|
|
97
|
+
file,
|
|
98
|
+
correlationId: envelope.correlationId,
|
|
99
|
+
timestamp: envelope.timestamp,
|
|
100
|
+
bytes: statSync(filePath).size,
|
|
101
|
+
};
|
|
102
|
+
} catch {
|
|
103
|
+
return { file, error: "unparseable" };
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function correlationIdFromResponseFile(file) {
|
|
109
|
+
const stem = file.slice("res-".length, -".sse".length);
|
|
110
|
+
const isoTimestamp = /^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-\d{3}Z-(.+)$/i.exec(stem);
|
|
111
|
+
if (isoTimestamp) return isoTimestamp[1];
|
|
112
|
+
|
|
113
|
+
const numericTimestamp = /^\d{10,}-(.+)$/.exec(stem);
|
|
114
|
+
if (numericTimestamp) return numericTimestamp[1];
|
|
115
|
+
|
|
116
|
+
const uuid = /([0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12})$/i.exec(stem);
|
|
117
|
+
return uuid?.[1] ?? stem;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function responseDumpMetadata(directory) {
|
|
121
|
+
const items = [];
|
|
122
|
+
for (const file of sortedArtifactFiles(directory, "res-", ".sse")) {
|
|
123
|
+
try {
|
|
124
|
+
items.push({
|
|
125
|
+
file,
|
|
126
|
+
correlationId: correlationIdFromResponseFile(file),
|
|
127
|
+
bytes: statSync(path.join(directory, file)).size,
|
|
128
|
+
});
|
|
129
|
+
} catch {
|
|
130
|
+
// A file can disappear between directory enumeration and stat; omit it rather than abort diagnostics.
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return items;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function debugHeadersMetadata() {
|
|
137
|
+
try {
|
|
138
|
+
const filePath = path.join(getConfigDir(), "debug-headers.log");
|
|
139
|
+
if (!existsSync(filePath)) return { exists: false, sizeBytes: 0 };
|
|
140
|
+
return { exists: true, sizeBytes: statSync(filePath).size };
|
|
141
|
+
} catch {
|
|
142
|
+
return { exists: false, sizeBytes: 0 };
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function getArtifactSummary() {
|
|
147
|
+
try {
|
|
148
|
+
const dumpsDirectory = path.join(os.homedir(), ".opencode", "opencode-anthropic-fix", "request-dumps");
|
|
149
|
+
return {
|
|
150
|
+
requestDumps: requestDumpMetadata(dumpsDirectory),
|
|
151
|
+
responseDumps: responseDumpMetadata(dumpsDirectory),
|
|
152
|
+
debugHeadersLog: debugHeadersMetadata(),
|
|
153
|
+
};
|
|
154
|
+
} catch {
|
|
155
|
+
return {
|
|
156
|
+
requestDumps: [],
|
|
157
|
+
responseDumps: [],
|
|
158
|
+
debugHeadersLog: { exists: false, sizeBytes: 0 },
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Build a shareable diagnostic snapshot without exposing credentials.
|
|
165
|
+
*
|
|
166
|
+
* @returns {Promise<object>}
|
|
167
|
+
*/
|
|
168
|
+
export async function buildDiagnosticBundle() {
|
|
169
|
+
const bundle = {
|
|
170
|
+
meta: {
|
|
171
|
+
generatedAt: new Date().toISOString(),
|
|
172
|
+
pluginVersion: getPluginVersion(),
|
|
173
|
+
nodeVersion: process.version,
|
|
174
|
+
platform: process.platform,
|
|
175
|
+
mimicryBaseline: FALLBACK_CLAUDE_CLI_VERSION,
|
|
176
|
+
},
|
|
177
|
+
config: redactSecrets(loadConfig()),
|
|
178
|
+
env: getRuntimeEnv(),
|
|
179
|
+
accounts: await getAccountSummary(),
|
|
180
|
+
artifacts: getArtifactSummary(),
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
return redactSecrets(bundle);
|
|
184
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
const state = vi.hoisted(() => ({
|
|
4
|
+
accounts: null,
|
|
5
|
+
config: {},
|
|
6
|
+
configDir: "/diagnose/config",
|
|
7
|
+
debugHeadersExists: false,
|
|
8
|
+
directoryExists: false,
|
|
9
|
+
entries: [],
|
|
10
|
+
fileContents: new Map(),
|
|
11
|
+
home: "/diagnose/home",
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
vi.mock("node:fs", () => ({
|
|
15
|
+
existsSync: vi.fn((filePath) => {
|
|
16
|
+
const value = String(filePath);
|
|
17
|
+
if (value.endsWith("debug-headers.log")) return state.debugHeadersExists;
|
|
18
|
+
if (value.endsWith("request-dumps")) return state.directoryExists;
|
|
19
|
+
return false;
|
|
20
|
+
}),
|
|
21
|
+
readFileSync: vi.fn((filePath) => {
|
|
22
|
+
const value = String(filePath);
|
|
23
|
+
if (value.endsWith("package.json")) return JSON.stringify({ version: "1.2.3-test" });
|
|
24
|
+
const file = value.split(/[\\/]/).at(-1);
|
|
25
|
+
if (!state.fileContents.has(file)) throw new Error(`Missing fixture: ${file}`);
|
|
26
|
+
return state.fileContents.get(file);
|
|
27
|
+
}),
|
|
28
|
+
readdirSync: vi.fn(() => [...state.entries]),
|
|
29
|
+
statSync: vi.fn((filePath) => {
|
|
30
|
+
const file = String(filePath).split(/[\\/]/).at(-1);
|
|
31
|
+
const contents = state.fileContents.get(file) ?? "";
|
|
32
|
+
return { size: Buffer.byteLength(contents) };
|
|
33
|
+
}),
|
|
34
|
+
}));
|
|
35
|
+
|
|
36
|
+
vi.mock("node:os", () => ({
|
|
37
|
+
default: { homedir: vi.fn(() => state.home) },
|
|
38
|
+
}));
|
|
39
|
+
|
|
40
|
+
vi.mock("./config.mjs", () => ({
|
|
41
|
+
getConfigDir: vi.fn(() => state.configDir),
|
|
42
|
+
loadConfig: vi.fn(() => state.config),
|
|
43
|
+
}));
|
|
44
|
+
|
|
45
|
+
vi.mock("./storage.mjs", () => ({
|
|
46
|
+
loadAccounts: vi.fn(async () => state.accounts),
|
|
47
|
+
}));
|
|
48
|
+
|
|
49
|
+
import { buildDiagnosticBundle } from "./diagnose.mjs";
|
|
50
|
+
|
|
51
|
+
describe("buildDiagnosticBundle", () => {
|
|
52
|
+
beforeEach(() => {
|
|
53
|
+
state.accounts = null;
|
|
54
|
+
state.config = {};
|
|
55
|
+
state.debugHeadersExists = false;
|
|
56
|
+
state.directoryExists = false;
|
|
57
|
+
state.entries = [];
|
|
58
|
+
state.fileContents.clear();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("returns all sections without leaking seeded secrets", async () => {
|
|
62
|
+
const bearer = "Bearer diagnoseBearerToken123";
|
|
63
|
+
const apiKey = "sk-ant-diagnoseSecret123";
|
|
64
|
+
const oauthToken = "oat01DiagnoseSecret123";
|
|
65
|
+
const refreshToken = "literal-refresh-secret";
|
|
66
|
+
const access = "literal-access-secret";
|
|
67
|
+
|
|
68
|
+
state.config = {
|
|
69
|
+
authorization: bearer,
|
|
70
|
+
apiKey,
|
|
71
|
+
metadata: oauthToken,
|
|
72
|
+
refreshToken,
|
|
73
|
+
};
|
|
74
|
+
state.accounts = {
|
|
75
|
+
accounts: [
|
|
76
|
+
{
|
|
77
|
+
email: "alice@example.com",
|
|
78
|
+
enabled: true,
|
|
79
|
+
consecutiveFailures: 2,
|
|
80
|
+
rateLimitResetTimes: { primary: 1234 },
|
|
81
|
+
access,
|
|
82
|
+
refreshToken,
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const bundle = await buildDiagnosticBundle();
|
|
88
|
+
const serialized = JSON.stringify(bundle);
|
|
89
|
+
|
|
90
|
+
expect(Object.keys(bundle)).toEqual(["meta", "config", "env", "accounts", "artifacts"]);
|
|
91
|
+
expect(serialized).not.toMatch(/Bearer\s+[A-Za-z0-9]/);
|
|
92
|
+
expect(serialized).not.toMatch(/sk-ant-[A-Za-z0-9]{4,}/);
|
|
93
|
+
expect(serialized).not.toMatch(/\boat01[A-Za-z0-9]{8,}/);
|
|
94
|
+
for (const secret of [bearer, apiKey, oauthToken, refreshToken, access]) {
|
|
95
|
+
expect(serialized).not.toContain(secret);
|
|
96
|
+
}
|
|
97
|
+
expect(bundle.accounts.items[0].email).toBe("a***@example.com");
|
|
98
|
+
expect(bundle.accounts.items[0]).not.toHaveProperty("access");
|
|
99
|
+
expect(bundle.accounts.items[0]).not.toHaveProperty("refreshToken");
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("degrades gracefully when accounts and artifact directories are absent", async () => {
|
|
103
|
+
const bundle = await buildDiagnosticBundle();
|
|
104
|
+
|
|
105
|
+
expect(bundle.accounts).toEqual({ count: 0, items: [] });
|
|
106
|
+
expect(bundle.artifacts.requestDumps).toEqual([]);
|
|
107
|
+
expect(bundle.artifacts.responseDumps).toEqual([]);
|
|
108
|
+
expect(bundle.artifacts.debugHeadersLog).toEqual({ exists: false, sizeBytes: 0 });
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("records a corrupted request dump and continues", async () => {
|
|
112
|
+
const corruptFile = "req-2026-04-10T12-00-00-000Z-correlation.json";
|
|
113
|
+
state.directoryExists = true;
|
|
114
|
+
state.entries = [corruptFile];
|
|
115
|
+
state.fileContents.set(corruptFile, "{not-json");
|
|
116
|
+
|
|
117
|
+
const bundle = await buildDiagnosticBundle();
|
|
118
|
+
|
|
119
|
+
expect(bundle.artifacts.requestDumps).toEqual([{ file: corruptFile, error: "unparseable" }]);
|
|
120
|
+
expect(bundle.artifacts.responseDumps).toEqual([]);
|
|
121
|
+
});
|
|
122
|
+
});
|