opencode-anthropic-fix 0.1.31 → 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.mjs +9 -10
- 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/oauth.mjs +1 -1
- package/lib/oauth.test.mjs +8 -4
- 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
package/lib/config.mjs
CHANGED
|
@@ -435,18 +435,17 @@ export const DEFAULT_CONFIG = {
|
|
|
435
435
|
/** Also inject numeric length anchors (≤25 words between tool calls, ≤100 words final). */
|
|
436
436
|
length_anchors: true,
|
|
437
437
|
},
|
|
438
|
-
/** OAuth token-endpoint request fingerprint.
|
|
439
|
-
* validated against a live refresh. */
|
|
438
|
+
/** OAuth token-endpoint request fingerprint. */
|
|
440
439
|
oauth: {
|
|
441
|
-
/** When true, the OAuth token EXCHANGE/REFRESH POSTs use Claude
|
|
442
|
-
* 2.1.195's SDK-native-fetch fingerprint (`User-Agent:
|
|
440
|
+
/** When true (DEFAULT), the OAuth token EXCHANGE/REFRESH POSTs use Claude
|
|
441
|
+
* Code 2.1.195's SDK-native-fetch fingerprint (`User-Agent:
|
|
443
442
|
* anthropic-sdk-typescript/0.94.0 userOAuthProvider`, `anthropic-beta:
|
|
444
|
-
* oauth-2025-04-20`, no axios `Accept`). When false
|
|
445
|
-
*
|
|
446
|
-
*
|
|
447
|
-
* Default
|
|
448
|
-
*
|
|
449
|
-
sdk_token_useragent:
|
|
443
|
+
* oauth-2025-04-20`, no axios `Accept`). When false, the legacy axios
|
|
444
|
+
* fingerprint (`User-Agent: axios/1.13.6`, axios `Accept` header) is sent
|
|
445
|
+
* byte-for-byte as before.
|
|
446
|
+
* Default ON to match upstream CC 2.1.195's userOAuthProvider. Set to
|
|
447
|
+
* `false` to revert to the legacy axios/1.13.6 + `Accept` fingerprint. */
|
|
448
|
+
sdk_token_useragent: true,
|
|
450
449
|
},
|
|
451
450
|
};
|
|
452
451
|
|
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(() => {
|
|
@@ -188,6 +215,206 @@ describe("loadConfig", () => {
|
|
|
188
215
|
expect(config.token_bucket.regeneration_rate_per_minute).toBe(6);
|
|
189
216
|
});
|
|
190
217
|
|
|
218
|
+
it("validates and merges every supported nested config section", () => {
|
|
219
|
+
existsSync.mockReturnValue(true);
|
|
220
|
+
readFileSync.mockReturnValue(
|
|
221
|
+
JSON.stringify({
|
|
222
|
+
failure_ttl_seconds: 9_000,
|
|
223
|
+
signature_emulation: {
|
|
224
|
+
enabled: false,
|
|
225
|
+
fetch_claude_code_version_on_startup: false,
|
|
226
|
+
prompt_compaction: "off",
|
|
227
|
+
workload: " nightly-agent ",
|
|
228
|
+
},
|
|
229
|
+
override_model_limits: { enabled: true, context: 3_000_000, output: -1 },
|
|
230
|
+
custom_betas: [" beta-one ", "", 42, "beta-two"],
|
|
231
|
+
health_score: {
|
|
232
|
+
initial: 80,
|
|
233
|
+
success_reward: 5,
|
|
234
|
+
rate_limit_penalty: -15,
|
|
235
|
+
failure_penalty: -30,
|
|
236
|
+
recovery_rate_per_hour: 4,
|
|
237
|
+
min_usable: 60,
|
|
238
|
+
max_score: 90,
|
|
239
|
+
},
|
|
240
|
+
token_bucket: { max_tokens: 75, regeneration_rate_per_minute: 8, initial_tokens: 70 },
|
|
241
|
+
toasts: { quiet: true, debounce_seconds: 12 },
|
|
242
|
+
headers: {
|
|
243
|
+
emulation_profile: " 2.1.0 ",
|
|
244
|
+
overrides: { "x-test": "value", ignored: 7, "": "empty-key" },
|
|
245
|
+
disable: [" X-REMOVE ", 10, "", "X-OTHER"],
|
|
246
|
+
billing_header: false,
|
|
247
|
+
},
|
|
248
|
+
idle_refresh: { enabled: false, window_minutes: 90, min_interval_minutes: 45 },
|
|
249
|
+
cache_policy: {
|
|
250
|
+
ttl: "5m",
|
|
251
|
+
ttl_supported: false,
|
|
252
|
+
boundary_marker: true,
|
|
253
|
+
hit_rate_warning_threshold: 2,
|
|
254
|
+
},
|
|
255
|
+
fast_mode: true,
|
|
256
|
+
telemetry: { emulate_minimal: true },
|
|
257
|
+
usage_toast: true,
|
|
258
|
+
cc_credential_reuse: { enabled: true, auto_detect: false, prefer_over_oauth: true },
|
|
259
|
+
adaptive_context: { enabled: false, escalation_threshold: 200_000, deescalation_threshold: 220_000 },
|
|
260
|
+
willow_mode: { enabled: false, idle_threshold_minutes: 45, cooldown_minutes: 90, min_turns_before_suggest: 5 },
|
|
261
|
+
token_economy: {
|
|
262
|
+
token_efficient_tools: true,
|
|
263
|
+
redact_thinking: false,
|
|
264
|
+
context_hint: false,
|
|
265
|
+
conservative: false,
|
|
266
|
+
debug_dump_bodies: true,
|
|
267
|
+
ttl_thinking_strip: false,
|
|
268
|
+
proactive_microcompact: false,
|
|
269
|
+
microcompact_percent: 20,
|
|
270
|
+
microcompact_keep_recent: 100,
|
|
271
|
+
stable_tool_ordering: false,
|
|
272
|
+
deferred_tool_names: ["read", 12, "grep"],
|
|
273
|
+
adaptive_thinking_zero_simple: false,
|
|
274
|
+
tool_result_dedupe: true,
|
|
275
|
+
fast_mode_auto: true,
|
|
276
|
+
trailing_summary_trim: true,
|
|
277
|
+
role_scoped_cache_ttl: false,
|
|
278
|
+
lean_system_non_main: false,
|
|
279
|
+
simple_system_prompt: false,
|
|
280
|
+
},
|
|
281
|
+
token_economy_strategies: {
|
|
282
|
+
system_prompt_tailing: true,
|
|
283
|
+
system_prompt_tail_turns: 0,
|
|
284
|
+
system_prompt_tail_max_chars: 100_000,
|
|
285
|
+
tool_deferral: true,
|
|
286
|
+
tool_description_compaction: true,
|
|
287
|
+
adaptive_tool_set: true,
|
|
288
|
+
tool_result_dedupe_session_wide: true,
|
|
289
|
+
haiku_rolling_summary: true,
|
|
290
|
+
stale_read_eviction: true,
|
|
291
|
+
per_tool_class_prune: true,
|
|
292
|
+
},
|
|
293
|
+
output_cap: { enabled: false, default_max_tokens: 100, escalated_max_tokens: 300_000 },
|
|
294
|
+
preconnect: { enabled: false, timeout_ms: 50 },
|
|
295
|
+
overflow_recovery: { enabled: false, safety_margin: 20_000 },
|
|
296
|
+
cache_break_detection: { enabled: false, alert_threshold: 1_500, adaptive_breakpoint: false },
|
|
297
|
+
request_classification: {
|
|
298
|
+
enabled: false,
|
|
299
|
+
background_max_service_retries: 3,
|
|
300
|
+
background_max_should_retries: 4,
|
|
301
|
+
},
|
|
302
|
+
token_budget: { enabled: true, default: 25_000, completion_threshold: 2 },
|
|
303
|
+
microcompact: { enabled: false, threshold_percent: 120 },
|
|
304
|
+
overload_recovery: { enabled: false, default_cooldown_ms: 500, poll_quota_on_overload: false },
|
|
305
|
+
account_management: { proactive_disabled: false },
|
|
306
|
+
anti_verbosity: { enabled: false, length_anchors: false },
|
|
307
|
+
oauth: { sdk_token_useragent: false },
|
|
308
|
+
}),
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
const config = loadConfig();
|
|
312
|
+
|
|
313
|
+
expect(config.failure_ttl_seconds).toBe(7_200);
|
|
314
|
+
expect(config.signature_emulation).toEqual({
|
|
315
|
+
enabled: false,
|
|
316
|
+
fetch_claude_code_version_on_startup: false,
|
|
317
|
+
prompt_compaction: "off",
|
|
318
|
+
workload: "nightly-agent",
|
|
319
|
+
});
|
|
320
|
+
expect(config.override_model_limits).toEqual({ enabled: true, context: 2_000_000, output: 0 });
|
|
321
|
+
expect(config.custom_betas).toEqual(["beta-one", "beta-two"]);
|
|
322
|
+
expect(config.health_score).toEqual({
|
|
323
|
+
initial: 80,
|
|
324
|
+
success_reward: 5,
|
|
325
|
+
rate_limit_penalty: -15,
|
|
326
|
+
failure_penalty: -30,
|
|
327
|
+
recovery_rate_per_hour: 4,
|
|
328
|
+
min_usable: 60,
|
|
329
|
+
max_score: 90,
|
|
330
|
+
});
|
|
331
|
+
expect(config.token_bucket).toEqual({ max_tokens: 75, regeneration_rate_per_minute: 8, initial_tokens: 70 });
|
|
332
|
+
expect(config.toasts).toEqual({ quiet: true, debounce_seconds: 12 });
|
|
333
|
+
expect(config.headers).toEqual({
|
|
334
|
+
emulation_profile: "2.1.0",
|
|
335
|
+
overrides: { "x-test": "value" },
|
|
336
|
+
disable: ["x-remove", "x-other"],
|
|
337
|
+
billing_header: false,
|
|
338
|
+
});
|
|
339
|
+
expect(config.idle_refresh).toEqual({ enabled: false, window_minutes: 90, min_interval_minutes: 45 });
|
|
340
|
+
expect(config.cache_policy).toEqual({
|
|
341
|
+
ttl: "5m",
|
|
342
|
+
ttl_supported: false,
|
|
343
|
+
boundary_marker: true,
|
|
344
|
+
hit_rate_warning_threshold: 1,
|
|
345
|
+
});
|
|
346
|
+
expect(config.fast_mode).toBe(true);
|
|
347
|
+
expect(config.telemetry.emulate_minimal).toBe(true);
|
|
348
|
+
expect(config.usage_toast).toBe(true);
|
|
349
|
+
expect(config.cc_credential_reuse).toEqual({ enabled: true, auto_detect: false, prefer_over_oauth: true });
|
|
350
|
+
expect(config.adaptive_context).toEqual({
|
|
351
|
+
enabled: false,
|
|
352
|
+
escalation_threshold: 200_000,
|
|
353
|
+
deescalation_threshold: 150_000,
|
|
354
|
+
});
|
|
355
|
+
expect(config.willow_mode).toEqual({
|
|
356
|
+
enabled: false,
|
|
357
|
+
idle_threshold_minutes: 45,
|
|
358
|
+
cooldown_minutes: 90,
|
|
359
|
+
min_turns_before_suggest: 5,
|
|
360
|
+
});
|
|
361
|
+
expect(config.token_economy).toEqual({
|
|
362
|
+
token_efficient_tools: true,
|
|
363
|
+
redact_thinking: false,
|
|
364
|
+
context_hint: false,
|
|
365
|
+
conservative: false,
|
|
366
|
+
debug_dump_bodies: true,
|
|
367
|
+
ttl_thinking_strip: false,
|
|
368
|
+
proactive_microcompact: false,
|
|
369
|
+
microcompact_percent: 30,
|
|
370
|
+
microcompact_keep_recent: 64,
|
|
371
|
+
stable_tool_ordering: false,
|
|
372
|
+
deferred_tool_names: ["read", "grep"],
|
|
373
|
+
adaptive_thinking_zero_simple: false,
|
|
374
|
+
tool_result_dedupe: true,
|
|
375
|
+
fast_mode_auto: true,
|
|
376
|
+
trailing_summary_trim: true,
|
|
377
|
+
role_scoped_cache_ttl: false,
|
|
378
|
+
lean_system_non_main: false,
|
|
379
|
+
simple_system_prompt: false,
|
|
380
|
+
});
|
|
381
|
+
expect(config.token_economy_strategies).toEqual({
|
|
382
|
+
system_prompt_tailing: true,
|
|
383
|
+
system_prompt_tail_turns: 1,
|
|
384
|
+
system_prompt_tail_max_chars: 50_000,
|
|
385
|
+
tool_deferral: true,
|
|
386
|
+
tool_description_compaction: true,
|
|
387
|
+
adaptive_tool_set: true,
|
|
388
|
+
tool_result_dedupe_session_wide: true,
|
|
389
|
+
haiku_rolling_summary: true,
|
|
390
|
+
stale_read_eviction: true,
|
|
391
|
+
per_tool_class_prune: true,
|
|
392
|
+
});
|
|
393
|
+
expect(config.output_cap).toEqual({ enabled: false, default_max_tokens: 256, escalated_max_tokens: 200_000 });
|
|
394
|
+
expect(config.preconnect).toEqual({ enabled: false, timeout_ms: 100 });
|
|
395
|
+
expect(config.overflow_recovery).toEqual({ enabled: false, safety_margin: 10_000 });
|
|
396
|
+
expect(config.cache_break_detection).toEqual({
|
|
397
|
+
enabled: false,
|
|
398
|
+
alert_threshold: 1_500,
|
|
399
|
+
adaptive_breakpoint: false,
|
|
400
|
+
});
|
|
401
|
+
expect(config.request_classification).toEqual({
|
|
402
|
+
enabled: false,
|
|
403
|
+
background_max_service_retries: 3,
|
|
404
|
+
background_max_should_retries: 4,
|
|
405
|
+
});
|
|
406
|
+
expect(config.token_budget).toEqual({ enabled: true, default: 25_000, completion_threshold: 1 });
|
|
407
|
+
expect(config.microcompact).toEqual({ enabled: false, threshold_percent: 100 });
|
|
408
|
+
expect(config.overload_recovery).toEqual({
|
|
409
|
+
enabled: false,
|
|
410
|
+
default_cooldown_ms: 1_000,
|
|
411
|
+
poll_quota_on_overload: false,
|
|
412
|
+
});
|
|
413
|
+
expect(config.account_management.proactive_disabled).toBe(false);
|
|
414
|
+
expect(config.anti_verbosity).toEqual({ enabled: false, length_anchors: false });
|
|
415
|
+
expect(config.oauth.sdk_token_useragent).toBe(false);
|
|
416
|
+
});
|
|
417
|
+
|
|
191
418
|
// Environment variable overrides
|
|
192
419
|
it("overrides strategy from OPENCODE_ANTHROPIC_STRATEGY", () => {
|
|
193
420
|
existsSync.mockReturnValue(false);
|
|
@@ -378,6 +605,76 @@ describe("loadConfig", () => {
|
|
|
378
605
|
const config = loadConfig();
|
|
379
606
|
expect(config.adaptive_context.enabled).toBe(false);
|
|
380
607
|
});
|
|
608
|
+
|
|
609
|
+
it("applies true-form environment overrides for feature switches", () => {
|
|
610
|
+
existsSync.mockReturnValue(true);
|
|
611
|
+
readFileSync.mockReturnValue(
|
|
612
|
+
JSON.stringify({
|
|
613
|
+
signature_emulation: { enabled: false, fetch_claude_code_version_on_startup: false, prompt_compaction: "off" },
|
|
614
|
+
override_model_limits: { enabled: false },
|
|
615
|
+
cc_credential_reuse: { enabled: false },
|
|
616
|
+
account_management: { proactive_disabled: false },
|
|
617
|
+
anti_verbosity: { enabled: false },
|
|
618
|
+
}),
|
|
619
|
+
);
|
|
620
|
+
process.env.OPENCODE_ANTHROPIC_EMULATE_CLAUDE_CODE_SIGNATURE = "true";
|
|
621
|
+
process.env.OPENCODE_ANTHROPIC_FETCH_CLAUDE_CODE_VERSION = "true";
|
|
622
|
+
process.env.OPENCODE_ANTHROPIC_PROMPT_COMPACTION = "minimal";
|
|
623
|
+
process.env.OPENCODE_ANTHROPIC_OVERRIDE_MODEL_LIMITS = "true";
|
|
624
|
+
process.env.OPENCODE_ANTHROPIC_CC_CREDENTIALS = "true";
|
|
625
|
+
process.env.OPENCODE_ANTHROPIC_PROACTIVE_DISABLED = "true";
|
|
626
|
+
process.env.OPENCODE_ANTHROPIC_ANTI_VERBOSITY = "true";
|
|
627
|
+
|
|
628
|
+
const config = loadConfig();
|
|
629
|
+
|
|
630
|
+
expect(config.signature_emulation).toMatchObject({
|
|
631
|
+
enabled: true,
|
|
632
|
+
fetch_claude_code_version_on_startup: true,
|
|
633
|
+
prompt_compaction: "minimal",
|
|
634
|
+
});
|
|
635
|
+
expect(config.override_model_limits.enabled).toBe(true);
|
|
636
|
+
expect(config.cc_credential_reuse.enabled).toBe(true);
|
|
637
|
+
expect(config.account_management.proactive_disabled).toBe(true);
|
|
638
|
+
expect(config.anti_verbosity.enabled).toBe(true);
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
it("applies false-form environment overrides for feature switches", () => {
|
|
642
|
+
existsSync.mockReturnValue(false);
|
|
643
|
+
process.env.OPENCODE_ANTHROPIC_OVERRIDE_MODEL_LIMITS = "false";
|
|
644
|
+
process.env.OPENCODE_ANTHROPIC_CC_CREDENTIALS = "false";
|
|
645
|
+
process.env.OPENCODE_ANTHROPIC_PROACTIVE_DISABLED = "false";
|
|
646
|
+
process.env.OPENCODE_ANTHROPIC_ANTI_VERBOSITY = "false";
|
|
647
|
+
process.env.OPENCODE_ANTHROPIC_LENGTH_ANCHORS = "false";
|
|
648
|
+
|
|
649
|
+
const config = loadConfig();
|
|
650
|
+
|
|
651
|
+
expect(config.override_model_limits.enabled).toBe(false);
|
|
652
|
+
expect(config.cc_credential_reuse.enabled).toBe(false);
|
|
653
|
+
expect(config.account_management.proactive_disabled).toBe(false);
|
|
654
|
+
expect(config.anti_verbosity).toEqual({ enabled: false, length_anchors: false });
|
|
655
|
+
});
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
describe("loadRawConfig", () => {
|
|
659
|
+
beforeEach(() => {
|
|
660
|
+
vi.resetAllMocks();
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
it("returns parsed values without applying validation", () => {
|
|
664
|
+
existsSync.mockReturnValue(true);
|
|
665
|
+
readFileSync.mockReturnValue(JSON.stringify({ failure_ttl_seconds: -10, custom_key: "preserved" }));
|
|
666
|
+
|
|
667
|
+
expect(loadRawConfig()).toEqual({ failure_ttl_seconds: -10, custom_key: "preserved" });
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
it("returns an empty object for missing, malformed, or non-object config", () => {
|
|
671
|
+
existsSync.mockReturnValueOnce(false).mockReturnValue(true);
|
|
672
|
+
readFileSync.mockReturnValueOnce("invalid json").mockReturnValueOnce("[]");
|
|
673
|
+
|
|
674
|
+
expect(loadRawConfig()).toEqual({});
|
|
675
|
+
expect(loadRawConfig()).toEqual({});
|
|
676
|
+
expect(loadRawConfig()).toEqual({});
|
|
677
|
+
});
|
|
381
678
|
});
|
|
382
679
|
|
|
383
680
|
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
|
+
});
|
package/lib/env.mjs
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {string | undefined} value
|
|
3
|
+
* @returns {boolean}
|
|
4
|
+
*/
|
|
5
|
+
export function isTruthyEnv(value) {
|
|
6
|
+
if (!value) return false;
|
|
7
|
+
const normalized = value.trim().toLowerCase();
|
|
8
|
+
return normalized === "1" || normalized === "true" || normalized === "yes";
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @param {string | undefined} value
|
|
13
|
+
* @returns {boolean}
|
|
14
|
+
*/
|
|
15
|
+
export function isFalsyEnv(value) {
|
|
16
|
+
if (!value) return false;
|
|
17
|
+
const normalized = value.trim().toLowerCase();
|
|
18
|
+
return normalized === "0" || normalized === "false" || normalized === "no";
|
|
19
|
+
}
|