opencode-anthropic-fix 0.2.1 → 0.3.0-beta.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
package/README.md CHANGED
@@ -12,6 +12,22 @@ This fork continues that work and extends it with deeper research into how the o
12
12
 
13
13
  > **Account risk.** There is a real risk of account suspension if Anthropic detects requests are not coming from Claude Code. The mimicry tries hard to make every request look as close as possible to a genuine Claude Code request, but that reduces rather than eliminates the risk. Use at your own risk.
14
14
 
15
+ <a id="provider-scope"></a>
16
+
17
+ ## Provider Scope: Anthropic First-Party Only
18
+
19
+ This plugin targets the Anthropic first-party API and nothing else. **Amazon Bedrock, Anthropic on AWS (`anthropicAws`), Google Vertex AI, Azure/Microsoft Foundry and Mantle are permanently out of scope.** They are not a backlog item, not a "not yet" — support for them will not be added.
20
+
21
+ What that means concretely:
22
+
23
+ - The signature-emulation adapter **declines** any provider that is not `anthropic` (`lib/adapter-input.mjs`). Those requests fall through to the legacy request path, which serves them without any provider-specific branching.
24
+ - **Nothing is suppressed based on the provider.** There is no provider-gated logic for the `x-anthropic-billing-header` segments (`cch`, `cc_workload`), no provider filter over the `anthropic-beta` header, and no provider-conditional request-body segment. `cch=00000` and the beta list are emitted unconditionally.
25
+ - Constructs such as a `BEDROCK_UNSUPPORTED_BETAS` list or an `anthropic_beta` field inside the request body do not exist in this codebase. Their absence is pinned by tests.
26
+
27
+ Provider names still appear in three legitimate places, and none of them imply multi-provider support: reverse-engineering notes describing the **official upstream Claude Code binary** (which does branch by provider), Bedrock model-ARN parsing kept for identifier normalization, and the provider environment variables listed by `anthropic doctor`.
28
+
29
+ If a document or comment claims this plugin branches on the provider, that claim is stale — this section is the canonical answer.
30
+
15
31
  ## Quick Start
16
32
 
17
33
  **Prerequisites:** [OpenCode](https://github.com/anomalyco/opencode) installed, a Claude Pro or Max subscription, Node.js 18+.
@@ -716,6 +732,14 @@ export PATH="$HOME/.local/bin:$PATH"
716
732
 
717
733
  - [AnomalyCo](https://github.com/anomalyco/opencode-anthropic-auth) — Original OpenCode Anthropic auth plugin
718
734
 
735
+ ## Third-Party Dependencies
736
+
737
+ The outgoing Anthropic Messages request is built by
738
+ [`@tormentalabs/claude-code-wire-compat`](https://github.com/marco-jardim/claude-code-wire-compat),
739
+ licensed under GPL-3.0-or-later. Attribution for every redistributed dependency is in
740
+ [NOTICE](NOTICE); the pinned artifact, its integrity hash, and the rollback procedure are in
741
+ [docs/shared-package-provenance.md](docs/shared-package-provenance.md).
742
+
719
743
  ## License
720
744
 
721
745
  This project is licensed under the [GNU General Public License v3.0](LICENSE) (GPLv3).
package/index.mjs CHANGED
@@ -3,7 +3,6 @@ import { stdin, stdout } from "node:process";
3
3
  import { randomBytes, randomUUID, createHash as createHashCrypto } from "node:crypto";
4
4
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
5
5
  import { join, resolve, basename } from "node:path";
6
- import xxhashInit from "xxhash-wasm";
7
6
  import { AccountManager, RATE_LIMIT_KEY_FAST } from "./lib/accounts.mjs";
8
7
  import {
9
8
  authorize as oauthAuthorize,
@@ -18,7 +17,6 @@ import {
18
17
  resolveBetaShortcut,
19
18
  } from "./lib/request-headers.mjs";
20
19
  import { loadConfig, loadConfigFresh, saveConfig, getConfigDir } from "./lib/config.mjs";
21
- import { loadContextHintDisabledFlag, saveContextHintDisabledFlag } from "./lib/context-hint-persist.mjs";
22
20
  import { loadAccounts, saveAccounts, clearAccounts, createDefaultStats } from "./lib/storage.mjs";
23
21
  import { acquireRefreshLock, releaseRefreshLock } from "./lib/refresh-lock.mjs";
24
22
  import {
@@ -62,8 +60,8 @@ import {
62
60
  buildRequestHeaders,
63
61
  buildAnthropicBetaHeader,
64
62
  parseRequestBodyMetadata,
65
- detectProvider,
66
63
  extractFileIds,
64
+ stripStainlessHelperMarkers,
67
65
  } from "./lib/mimicry/headers.mjs";
68
66
  import {
69
67
  buildSystemPromptBlocks,
@@ -78,6 +76,8 @@ import {
78
76
  CLAUDE_CODE_IDENTITY_STRING,
79
77
  SUBAGENT_CC_ANCHOR,
80
78
  } from "./lib/mimicry/system-prompt.mjs";
79
+ import { buildAdapterTransport, resolveAdapterEnv } from "./lib/mimicry/adapter-input.mjs";
80
+ import { buildWireCompatibleRequest } from "./lib/mimicry/wire-compat.mjs";
81
81
  import {
82
82
  hasOneMillionContext,
83
83
  isEligibleFor1MContext,
@@ -319,34 +319,6 @@ export async function AnthropicAuthPlugin({ client }) {
319
319
  /** @type {Map<string, number>} canonical-beta to rejected-at epoch ms */
320
320
  const sessionRejectedBetas = new Map();
321
321
 
322
- // Context-hint controller (CC v2.1.110+). Mirrors real CC's `createContextHintController`:
323
- // sticky on across requests until the server responds with a specific error family.
324
- // - 422/424 → apply hint compaction (clear thinking + microcompact) and retry
325
- // - 400 "Unexpected value" + "anthropic-beta" → disable for session (beta unsupported)
326
- // - 409 → disable for session (conflict)
327
- // - 529 / overloaded → disable for session (temporary overload)
328
- // When disabled, we strip context-hint from betas + body on subsequent requests so
329
- // we don't keep triggering the same rejection and churning the cache.
330
- // Persisted disable: if a prior session saw a 400 rejecting the context-hint
331
- // beta (account lacks access), skip the beta from turn 1 of every subsequent
332
- // session. Delete ~/.config/opencode/context-hint-disabled.flag (or the
333
- // %APPDATA%\opencode equivalent on Windows) to re-enable once access is
334
- // granted. See lib/context-hint-persist.mjs.
335
- const _persistedCtxHint = loadContextHintDisabledFlag();
336
- const contextHintState = {
337
- /** Permanently disabled for this session after a server rejection. */
338
- disabled: _persistedCtxHint.disabled === true,
339
- /** Number of 422/424 compactions applied this session (for telemetry). */
340
- compactionsApplied: 0,
341
- };
342
- if (contextHintState.disabled) {
343
- debugLog(
344
- "context-hint: loaded persisted disable flag",
345
- _persistedCtxHint.status ? `status=${_persistedCtxHint.status}` : "",
346
- _persistedCtxHint.timestamp ? `ts=${new Date(_persistedCtxHint.timestamp).toISOString()}` : "",
347
- );
348
- }
349
-
350
322
  // Token economy — session state for layered compaction strategies.
351
323
  const tokenEconomySession = {
352
324
  /** When thinking was last stripped (TTL-based strategy). 0 = never. */
@@ -836,7 +808,7 @@ export async function AnthropicAuthPlugin({ client }) {
836
808
  `telemetry-emulation: ${fresh.telemetry?.emulate_minimal ? "on (silent observer)" : "off"}`,
837
809
  `usage-toast: ${fresh.usage_toast ? "on" : "off"}`,
838
810
  `adaptive-context: ${fresh.adaptive_context?.enabled ? `on (↑${Math.round((fresh.adaptive_context.escalation_threshold || 150000) / 1000)}K ↓${Math.round((fresh.adaptive_context.deescalation_threshold || 100000) / 1000)}K)${adaptiveContextState.active ? " [ACTIVE]" : ""}` : "off"}`,
839
- `anti-verbosity: ${fresh.anti_verbosity?.enabled !== false ? "on" : "off"} (length-anchors: ${fresh.anti_verbosity?.length_anchors !== false ? "on" : "off"})`,
811
+ `anti-verbosity: ${fresh.anti_verbosity?.enabled !== false ? "on" : "off"}`,
840
812
  ];
841
813
  await sendCommandMessage(input.sessionID, lines.join("\n"));
842
814
  return;
@@ -2875,7 +2847,6 @@ export async function AnthropicAuthPlugin({ client }) {
2875
2847
  requestInit.body,
2876
2848
  _parsedBodyOnce,
2877
2849
  );
2878
- const _reqProvider = detectProvider(requestUrl);
2879
2850
 
2880
2851
  // --- Adaptive 1M context decision (once per logical request, not per retry) ---
2881
2852
  if (!_adaptiveDecisionMade) {
@@ -2910,12 +2881,7 @@ export async function AnthropicAuthPlugin({ client }) {
2910
2881
  const _adaptiveOverride = _adaptiveOverrideForRequest;
2911
2882
 
2912
2883
  // Token economy config (resolved once, passed to beta builder).
2913
- // If the server has rejected context-hint this session, reflect it here
2914
- // so buildAnthropicBetaHeader (called again inside buildRequestHeaders)
2915
- // drops the beta on subsequent requests.
2916
- // Also classify the request role (CC's querySource analog) and
2917
- // suppress context-hint for non-main-thread requests so we don't
2918
- // opt subagent/title/tool one-shots into a server retry loop.
2884
+ // Also classify the request role (CC's querySource analog).
2919
2885
  const _requestRole = classifyRequestRole(_parsedBodyOnce);
2920
2886
  // opencode tags subagent requests with the x-parent-session-id header
2921
2887
  // (set in its request builder only when a parent session exists).
@@ -2924,10 +2890,7 @@ export async function AnthropicAuthPlugin({ client }) {
2924
2890
  // only to pick the cheaper 5m cache tier for one-shot subagents.
2925
2891
  const _isSubagent = getIncomingHeader(input, requestInit, "x-parent-session-id") != null;
2926
2892
  const _baseTE = config.token_economy || {};
2927
- const _disableCtxHint = contextHintState.disabled || _requestRole !== "main";
2928
- const _tokenEconomy = _disableCtxHint
2929
- ? { ..._baseTE, context_hint: false, __requestRole: _requestRole }
2930
- : { ..._baseTE, __requestRole: _requestRole };
2893
+ const _tokenEconomy = { ..._baseTE, __requestRole: _requestRole };
2931
2894
 
2932
2895
  // Microcompact: inject clear betas at high context utilization
2933
2896
  let _microcompactBetas = null;
@@ -2969,7 +2932,6 @@ export async function AnthropicAuthPlugin({ client }) {
2969
2932
  "",
2970
2933
  getSignatureEmulationEnabled(),
2971
2934
  _reqModel,
2972
- _reqProvider,
2973
2935
  _sessionFilteredCustomBetas,
2974
2936
  getEffectiveStrategy(),
2975
2937
  requestUrl?.pathname,
@@ -2998,12 +2960,6 @@ export async function AnthropicAuthPlugin({ client }) {
2998
2960
  // Merge latched betas that aren't in the current set
2999
2961
  const merged = new Set(currentBetas);
3000
2962
  for (const b of betaLatchState.sent) merged.add(b);
3001
- // Context-hint kill switch: once server rejected it this session,
3002
- // stop sending the beta (body field is gated on header, so it drops too).
3003
- if (contextHintState.disabled) {
3004
- merged.delete("context-hint-2026-04-09");
3005
- betaLatchState.sent.delete("context-hint-2026-04-09");
3006
- }
3007
2963
  // Custom-beta strip kill switch: when the server rejected our custom
3008
2964
  // betas (customBetasStripped latch fired), evict them from the latch so
3009
2965
  // they do not re-appear on the retry attempt.
@@ -3046,13 +3002,38 @@ export async function AnthropicAuthPlugin({ client }) {
3046
3002
  const effectiveCachePolicy = latchedCachePolicy ||
3047
3003
  config.cache_policy || { ttl: "1h", ttl_supported: true };
3048
3004
 
3005
+ // Single decision point for the whole request: does this turn go
3006
+ // through the shared wire package, or through the legacy
3007
+ // buildRequestHeaders path?
3008
+ //
3009
+ // Two conditions remain now that multi-provider support is gone:
3010
+ // 1. signature emulation on — with it off the plugin emits only
3011
+ // 3 headers and a minimal beta set, while the package always
3012
+ // emits the full Claude Code set;
3013
+ // 2. the /v1/messages endpoint — the package pins
3014
+ // `https://api.anthropic.com/v1/messages?beta=true`, so a
3015
+ // /v1/messages/count_tokens turn sent through it would be
3016
+ // silently rewritten to the wrong endpoint.
3017
+ //
3018
+ // The former first-party check is now structural: the plugin only
3019
+ // speaks to first-party Anthropic, so it is always satisfied.
3020
+ // 3. a JSON body is actually present — the package requires
3021
+ // `model` and `max_tokens`, so a bodiless request must keep
3022
+ // using the legacy path rather than throwing INVALID_INPUT.
3023
+ const _adapterPathname = requestUrl?.pathname;
3024
+ const _useAdapter =
3025
+ getSignatureEmulationEnabled() &&
3026
+ (_adapterPathname === "/v1/messages" || _adapterPathname === "/messages") &&
3027
+ typeof requestInit.body === "string" &&
3028
+ requestInit.body.length > 0;
3029
+
3049
3030
  const body = transformRequestBody(
3050
3031
  requestInit.body,
3051
3032
  {
3052
3033
  enabled: getSignatureEmulationEnabled(),
3053
3034
  claudeCliVersion,
3054
3035
  promptCompactionMode: getPromptCompactionMode(),
3055
- provider: _reqProvider,
3036
+ useAdapter: _useAdapter,
3056
3037
  cachePolicy: effectiveCachePolicy,
3057
3038
  fastMode: config.fast_mode || false,
3058
3039
  // When the account's fast pool is cooling down, suppress
@@ -3121,29 +3102,107 @@ export async function AnthropicAuthPlugin({ client }) {
3121
3102
  }
3122
3103
  }
3123
3104
 
3124
- // Build headers with the selected account's token
3125
- const requestHeaders = buildRequestHeaders(
3126
- input,
3127
- requestInit,
3128
- accessToken,
3129
- body,
3130
- requestUrl,
3131
- {
3132
- enabled: getSignatureEmulationEnabled(),
3133
- claudeCliVersion,
3134
- customBetas: _sessionFilteredCustomBetas,
3135
- strategy: getEffectiveStrategy(),
3136
- sessionId: signatureSessionId,
3137
- },
3138
- _adaptiveOverride,
3139
- _tokenEconomy,
3140
- );
3105
+ // Build headers with the selected account's token.
3106
+ //
3107
+ // Adapter path: the shared package composes the headers AND
3108
+ // rebuilds the body (canonical system prefix, metadata, beta
3109
+ // header). Legacy path: unchanged, byte for byte.
3110
+ //
3111
+ // The URL deliberately stays whatever transformRequestUrl already
3112
+ // produced. `built.url` is the package's pinned
3113
+ // `https://api.anthropic.com/v1/messages?beta=true`, which would
3114
+ // override a custom ANTHROPIC_BASE_URL or proxy endpoint.
3115
+ const _adapterSignature = {
3116
+ enabled: getSignatureEmulationEnabled(),
3117
+ claudeCliVersion,
3118
+ customBetas: _sessionFilteredCustomBetas,
3119
+ strategy: getEffectiveStrategy(),
3120
+ sessionId: signatureSessionId,
3121
+ };
3122
+
3123
+ let requestHeaders;
3124
+ let adapterBody;
3125
+ let legacyStrippedBody;
3126
+ if (_useAdapter) {
3127
+ const _adapterResult = buildAdapterTransport({
3128
+ input,
3129
+ requestInit,
3130
+ accessToken,
3131
+ requestUrl,
3132
+ provider: "anthropic",
3133
+ clientRequestId: randomUUID(),
3134
+ signature: _adapterSignature,
3135
+ identity: {
3136
+ persistentUserId: signatureUserId,
3137
+ accountId: getAccountIdentifier(account),
3138
+ },
3139
+ adaptiveOverride: _adaptiveOverride,
3140
+ tokenEconomy: _tokenEconomy,
3141
+ // Role inputs for the identity-block cache ttl: the adapter
3142
+ // re-runs the same resolveCacheTtl the body transform used,
3143
+ // so system[1] cannot carry a ttl the rest of the request
3144
+ // does not.
3145
+ cachePolicy: effectiveCachePolicy,
3146
+ requestRole: _requestRole,
3147
+ isSubagent: _isSubagent,
3148
+ // Derived from the PRE-transform body on purpose: the lean
3149
+ // gate in buildSystemPromptBlocks tests the incoming system
3150
+ // blocks, and by now the title-generator swap has already
3151
+ // rewritten them in `body`.
3152
+ isTitleGenerator: _parsedBodyOnce
3153
+ ? isTitleGeneratorSystemBlocks(normalizeSystemTextBlocks(_parsedBodyOnce.system))
3154
+ : false,
3155
+ body,
3156
+ env: resolveAdapterEnv(process.env),
3157
+ platform: process.platform,
3158
+ arch: process.arch,
3159
+ nodeVersion: process.version,
3160
+ });
3161
+ if (_adapterResult.applicable) {
3162
+ const built = await buildWireCompatibleRequest(body, _adapterResult.transport);
3163
+ requestHeaders = built.headers;
3164
+ adapterBody = built.body;
3165
+ } else {
3166
+ debugLog(`adapter not applicable (${_adapterResult.reason}); using the legacy request path`);
3167
+ }
3168
+ }
3169
+
3170
+ if (!requestHeaders) {
3171
+ // Legacy path, and the fallback whenever the adapter declined:
3172
+ // a bodiless request, or a transport the translation refused.
3173
+ requestHeaders = buildRequestHeaders(
3174
+ input,
3175
+ requestInit,
3176
+ accessToken,
3177
+ body,
3178
+ requestUrl,
3179
+ _adapterSignature,
3180
+ _adaptiveOverride,
3181
+ _tokenEconomy,
3182
+ );
3183
+ // buildRequestHeaders just derived x-stainless-helper from the
3184
+ // markers. They are an internal signal — the Anthropic API has
3185
+ // never known those keys — so they are dropped from the body now
3186
+ // that the header exists. The adapter path does the same strip in
3187
+ // buildWireCompatibleRequest; this is the legacy half of it.
3188
+ if (typeof body === "string" && body.length > 0) {
3189
+ try {
3190
+ const _strippable = JSON.parse(body);
3191
+ if (stripStainlessHelperMarkers(_strippable?.tools, _strippable?.messages) > 0) {
3192
+ legacyStrippedBody = JSON.stringify(_strippable);
3193
+ }
3194
+ } catch (error) {
3195
+ // A non-JSON body carries no markers to strip; forward it untouched.
3196
+ debugLog(`stainless-helper strip skipped (unparsable body): ${error.message}`);
3197
+ }
3198
+ }
3199
+ }
3141
3200
  // cch stays as the static "00000" placeholder — cc-107 and cc-108
3142
3201
  // JS bundles both emit `cch=00000;` unconditionally in the billing
3143
3202
  // header. The Bun-binary Attestation.zig xxHash64 mechanism lives in
3144
3203
  // a SEPARATE header path, not in this body field. Re-hashing here
3145
3204
  // mutates system[0] each turn, invalidating the prompt cache.
3146
- const finalBody = body;
3205
+ const finalBody = adapterBody ?? legacyStrippedBody ?? body;
3147
3206
 
3148
3207
  const correlationId = createDebugCorrelationId();
3149
3208
 
@@ -3514,76 +3573,35 @@ export async function AnthropicAuthPlugin({ client }) {
3514
3573
  // Ignore read errors in debug logging path.
3515
3574
  }
3516
3575
 
3517
- // Context-hint protocol (CC v2.1.110+): detect server rejections and
3518
- // apply the same disable/compact semantics as real Claude Code.
3519
- // - 400 w/ "Unexpected value" + "anthropic-beta" → beta unsupported, disable
3520
- // - 409 / 529 / overloaded → temporary, disable
3521
- // - 422 / 424 → compact messages (strip
3522
- // thinking blocks + old tool_result content) and retry ONCE.
3523
- // Disable is permanent for the session; the beta latch strips it from future
3524
- // requests so we don't keep triggering the same rejection.
3525
- if (!contextHintState.disabled) {
3526
- if (
3527
- response.status === 400 &&
3528
- errorBody &&
3529
- errorBody.includes("Unexpected value") &&
3530
- errorBody.includes("anthropic-beta") &&
3531
- errorBody.includes("context-hint")
3532
- ) {
3533
- contextHintState.disabled = true;
3534
- betaLatchState.dirty = true;
3535
- // Persist across sessions: a 400 "Unexpected value" on the
3536
- // context-hint beta means the account lacks access to the
3537
- // beta entirely. Re-attempting next session would burn
3538
- // another turn to the same rejection.
3539
- saveContextHintDisabledFlag({
3540
- reason: "beta_unsupported_400",
3541
- status: 400,
3542
- });
3543
- debugLog("context-hint: beta rejected by server (400), disabling + persisting");
3544
- // Retry without the beta. The latch above drops the header
3545
- // and the body field on the rebuilt request.
3546
- attempt--;
3547
- continue;
3548
- } else if (response.status === 409) {
3549
- contextHintState.disabled = true;
3550
- betaLatchState.dirty = true;
3551
- debugLog("context-hint: 409 conflict, disabling for session");
3552
- attempt--;
3553
- continue;
3554
- } else if (response.status === 529 && errorBody && errorBody.includes("context_hint")) {
3555
- contextHintState.disabled = true;
3556
- betaLatchState.dirty = true;
3557
- debugLog("context-hint: 529 overloaded referencing hint, disabling for session");
3558
- attempt--;
3559
- continue;
3560
- } else if (
3561
- (response.status === 422 || response.status === 424) &&
3562
- !requestInit._contextHintCompactAttempted
3563
- ) {
3564
- try {
3565
- const hintBody = JSON.parse(requestInit.body);
3566
- if (Array.isArray(hintBody.messages)) {
3567
- const compacted = applyContextHintCompaction(hintBody.messages);
3568
- if (compacted.changed) {
3569
- hintBody.messages = compacted.messages;
3570
- requestInit.body = JSON.stringify(hintBody);
3571
- _parsedBodyOnce = null;
3572
- requestInit._contextHintCompactAttempted = true;
3573
- contextHintState.compactionsApplied += 1;
3574
- attempt--;
3575
- toast(
3576
- `⚙ Context hint compaction (${response.status}) — cleared ${compacted.stats.thinkingCleared} thinking / ${compacted.stats.toolResultsCleared} tool results`,
3577
- "info",
3578
- { debounceKey: "context-hint-compact" },
3579
- ).catch(() => {});
3580
- debugLog("context-hint: applied compaction on status", response.status, compacted.stats);
3581
- continue;
3582
- }
3576
+ // Compact-and-retry on 422/424: strip thinking blocks + old tool_result
3577
+ // content and retry ONCE. This reacts to the response status alone, so it
3578
+ // stays live even though the plugin no longer announces the
3579
+ // context-hint-2026-04-09 beta nor the paired body field.
3580
+ if (
3581
+ (response.status === 422 || response.status === 424) &&
3582
+ !requestInit._contextHintCompactAttempted
3583
+ ) {
3584
+ try {
3585
+ const hintBody = JSON.parse(requestInit.body);
3586
+ if (Array.isArray(hintBody.messages)) {
3587
+ const compacted = applyContextHintCompaction(hintBody.messages);
3588
+ if (compacted.changed) {
3589
+ hintBody.messages = compacted.messages;
3590
+ requestInit.body = JSON.stringify(hintBody);
3591
+ _parsedBodyOnce = null;
3592
+ requestInit._contextHintCompactAttempted = true;
3593
+ attempt--;
3594
+ toast(
3595
+ `⚙ Context hint compaction (${response.status}) — cleared ${compacted.stats.thinkingCleared} thinking / ${compacted.stats.toolResultsCleared} tool results`,
3596
+ "info",
3597
+ { debounceKey: "context-hint-compact" },
3598
+ ).catch(() => {});
3599
+ debugLog("context-hint: applied compaction on status", response.status, compacted.stats);
3600
+ continue;
3583
3601
  }
3584
- } catch {
3585
- // fall through to normal error handling
3586
3602
  }
3603
+ } catch {
3604
+ // fall through to normal error handling
3587
3605
  }
3588
3606
  }
3589
3607
 
@@ -5369,7 +5387,6 @@ function writeCacheStatsFile(usage, model, hitRate) {
5369
5387
  cache_ttl: _pluginConfig?.cache_policy?.ttl ?? "1h",
5370
5388
  boundary_marker: _pluginConfig?.cache_policy?.boundary_marker ?? false,
5371
5389
  anti_verbosity: _pluginConfig?.anti_verbosity?.enabled !== false,
5372
- length_anchors: _pluginConfig?.anti_verbosity?.length_anchors !== false,
5373
5390
  },
5374
5391
  timestamp: new Date().toISOString(),
5375
5392
  };
@@ -5566,17 +5583,6 @@ process.once("beforeExit", _beforeExitHandler);
5566
5583
  // Request building helpers (extracted from original fetch interceptor)
5567
5584
  // ---------------------------------------------------------------------------
5568
5585
 
5569
- // cch attestation: RE-ENABLED with xxHash64 (matching Bun binary's Attestation.zig).
5570
- // The compiled Bun binary computes cch dynamically: xxHash64(body, seed) & 0xFFFFF.
5571
- // Captured real CC v2.1.107 request shows cch=6d00f (5-hex-char, 20-bit masked hash).
5572
- // Seed extracted from binary: 0x6E52736AC806831E (unchanged since v2.1.96).
5573
-
5574
- /** @type {null | ((buf: Uint8Array, seed: bigint) => bigint)} */
5575
- let _xxh64Raw = null;
5576
- const _xxhashReady = xxhashInit().then((h) => {
5577
- _xxh64Raw = h.h64Raw;
5578
- });
5579
-
5580
5586
  /**
5581
5587
  * Extract the text content of the first user message for billing hash computation.
5582
5588
  * @param {any[] | undefined} messages
package/lib/config.mjs CHANGED
@@ -103,7 +103,7 @@ import { randomBytes } from "node:crypto";
103
103
  * @property {{ enabled: boolean, threshold_percent: number }} microcompact
104
104
  * @property {{ enabled: boolean, default_cooldown_ms: number, poll_quota_on_overload: boolean }} overload_recovery
105
105
  * @property {{ proactive_disabled: boolean }} account_management
106
- * @property {{ enabled: boolean, length_anchors: boolean }} anti_verbosity
106
+ * @property {{ enabled: boolean }} anti_verbosity
107
107
  * @property {{ sdk_token_useragent: boolean }} oauth
108
108
  */
109
109
 
@@ -119,8 +119,9 @@ export const DEFAULT_CONFIG = {
119
119
  /** Workload tag emitted in the `x-anthropic-billing-header` as
120
120
  * `cc_workload=<tag>;`. Mirrors real CC's `--workload <tag>` flag — used
121
121
  * by SDK daemon callers that spawn subprocesses for cron work, to tag
122
- * billing attribution. Empty string => omit (default). Provider-gated:
123
- * bedrock/anthropicAws/mantle skip this segment (same gate as `cch`).
122
+ * billing attribution. Empty string => omit (default). Not provider-gated:
123
+ * the plugin is Anthropic first-party only, so this segment is emitted
124
+ * whenever a tag is set (see "Provider Scope" in README.md).
124
125
  * Env var `CLAUDE_CODE_WORKLOAD` is a fallback when this is empty. */
125
126
  workload: "",
126
127
  },
@@ -211,15 +212,17 @@ export const DEFAULT_CONFIG = {
211
212
  * server-side for lower bandwidth). On by default for non-Claude-3 models
212
213
  * (CC v2.1.150+). Opt out via `/anthropic set redact-thinking off`. */
213
214
  redact_thinking: true,
214
- /** Enable context-hint-2026-04-09 beta (CC v2.1.110+). When on, the server
215
- * MAY reject 422/424 and trigger client-side compaction retries.
216
- * Default ON (Phase C2). Server-side gating + contextHintState.disabled
217
- * latching means compatible servers use it and incompatible ones fall
218
- * back cleanly via the 400/409/529 error paths. Explicit opt-out via
219
- * `token_economy.context_hint: false` is respected. Even when on, the
220
- * beta is only sent for requests classified as "main-thread" (see
221
- * classifyRequestRole). */
222
- context_hint: true,
215
+ /** [DEPRECATED — has no effect] context-hint-2026-04-09.
216
+ * The profile derived from the real Claude Code 2.1.195 binary sends
217
+ * neither the `context-hint-2026-04-09` beta nor the paired
218
+ * `context_hint: { enabled: true }` body field. Emitting either one made
219
+ * the request distinguishable from the genuine client, so both emissions
220
+ * were removed from every path (adapter and legacy alike). The effect
221
+ * cannot come back without breaking the mimicry, so this knob is not
222
+ * silently ignored: validateConfig emits a one-time deprecation warning
223
+ * when a user explicitly sets it to `true`. Retained only so an existing
224
+ * config file keeps loading. */
225
+ context_hint: false,
223
226
 
224
227
  /** Dump outgoing request bodies to
225
228
  * `~/.opencode/opencode-anthropic-fix/request-dumps/` (rotating, last 10).
@@ -300,17 +303,17 @@ export const DEFAULT_CONFIG = {
300
303
  * thread is untouched. */
301
304
  lean_system_non_main: true,
302
305
 
303
- /** Simple system prompt mode (real CC v2.1.133+ `tengu_vellum_lantern`
306
+ /** Simple system prompt mode (real CC v2.1.133+ `tengu_velvet_cascade`
304
307
  * equivalent). When enabled AND the request model is eligible
305
308
  * (claude-opus-4-7, -eap variants — see `isSimpleSystemPromptEligible`),
306
- * skip the anti-verbosity boilerplate (`ANTI_VERBOSITY_SYSTEM_PROMPT` +
307
- * `NUMERIC_LENGTH_ANCHORS_PROMPT`) that the plugin injects on Opus 4.x.
309
+ * skip the anti-verbosity boilerplate (`ANTI_VERBOSITY_SYSTEM_PROMPT`)
310
+ * that the plugin injects on Opus 4.x.
308
311
  * Conservative implementation: gates ONLY anti-verbosity. Identity
309
312
  * preamble, billing block, user instructions, and `<system-reminder>`
310
313
  * blocks are all preserved so the CC fingerprint match still holds.
311
314
  * Saves ~600-1500 tokens per request on eligible models. Default ON:
312
315
  * gates ONLY the anti-verbosity boilerplate (which real CC v2.1.133+ also
313
- * drops for eligible Opus 4.x via tengu_vellum_lantern); identity, billing
316
+ * drops for eligible Opus 4.x via tengu_velvet_cascade); identity, billing
314
317
  * and system-reminders are preserved, so the fingerprint match holds. */
315
318
  simple_system_prompt: true,
316
319
 
@@ -426,14 +429,12 @@ export const DEFAULT_CONFIG = {
426
429
  proactive_disabled: true,
427
430
  },
428
431
  /** Anti-verbosity: inject conciseness instructions into system prompt for Opus 4.6.
429
- * Mirrors CC v2.1.100 anti_verbosity + numeric_length_anchors sections (gated on
432
+ * Mirrors the CC anti_verbosity section (gated on
430
433
  * quiet_salted_ember A/B test in CC; unconditional here since we always want savings).
431
434
  * Only activates when model is opus-4-6. */
432
435
  anti_verbosity: {
433
436
  /** Master switch: inject anti-verbosity communication style instructions. */
434
437
  enabled: true,
435
- /** Also inject numeric length anchors (≤25 words between tool calls, ≤100 words final). */
436
- length_anchors: true,
437
438
  },
438
439
  /** OAuth token-endpoint request fingerprint. */
439
440
  oauth: {
@@ -529,6 +530,13 @@ function clampNumber(value, min, max, fallback) {
529
530
  * @param {Record<string, unknown>} raw
530
531
  * @returns {AnthropicAuthConfig}
531
532
  */
533
+ /**
534
+ * One-shot latch for the `token_economy.context_hint` deprecation warning, so a
535
+ * long-running session does not repeat it on every config reload.
536
+ * @type {boolean}
537
+ */
538
+ let contextHintDeprecationWarned = false;
539
+
532
540
  function validateConfig(raw) {
533
541
  const config = createDefaultConfig();
534
542
 
@@ -757,6 +765,21 @@ function validateConfig(raw) {
757
765
  // Token economy sub-config
758
766
  if (raw.token_economy && typeof raw.token_economy === "object") {
759
767
  const te = /** @type {Record<string, unknown>} */ (raw.token_economy);
768
+
769
+ // Non-silent deprecation. Only an explicit `true` warns: the `false` default
770
+ // costs the user nothing and must stay quiet. See the DEFAULT_CONFIG comment
771
+ // on token_economy.context_hint for why the effect cannot be restored.
772
+ if (te.context_hint === true && !contextHintDeprecationWarned) {
773
+ contextHintDeprecationWarned = true;
774
+
775
+ console.warn(
776
+ "[anthropic-auth] token_economy.context_hint is deprecated and has no effect: " +
777
+ "the context-hint-2026-04-09 beta and the context_hint body field are no longer sent on any " +
778
+ "request path, because the genuine Claude Code client does not send them and emitting them " +
779
+ "would make requests fingerprintable. Remove the setting from your config to silence this warning.",
780
+ );
781
+ }
782
+
760
783
  config.token_economy = {
761
784
  token_efficient_tools:
762
785
  typeof te.token_efficient_tools === "boolean"
@@ -764,6 +787,11 @@ function validateConfig(raw) {
764
787
  : DEFAULT_CONFIG.token_economy.token_efficient_tools,
765
788
  redact_thinking:
766
789
  typeof te.redact_thinking === "boolean" ? te.redact_thinking : DEFAULT_CONFIG.token_economy.redact_thinking,
790
+ // DEPRECATED: the value is still normalized so an existing config file
791
+ // keeps loading, but nothing reads it any more — the context-hint beta and
792
+ // body field were removed from every request path because the genuine
793
+ // Claude Code 2.1.195 client does not send them. A user switch that turns
794
+ // into a silent no-op is not acceptable, so an explicit opt-in warns once.
767
795
  context_hint: typeof te.context_hint === "boolean" ? te.context_hint : DEFAULT_CONFIG.token_economy.context_hint,
768
796
  conservative: typeof te.conservative === "boolean" ? te.conservative : DEFAULT_CONFIG.token_economy.conservative,
769
797
  debug_dump_bodies:
@@ -1011,8 +1039,6 @@ function validateConfig(raw) {
1011
1039
  const av = /** @type {Record<string, unknown>} */ (raw.anti_verbosity);
1012
1040
  config.anti_verbosity = {
1013
1041
  enabled: typeof av.enabled === "boolean" ? av.enabled : DEFAULT_CONFIG.anti_verbosity.enabled,
1014
- length_anchors:
1015
- typeof av.length_anchors === "boolean" ? av.length_anchors : DEFAULT_CONFIG.anti_verbosity.length_anchors,
1016
1042
  };
1017
1043
  }
1018
1044
 
@@ -1128,10 +1154,6 @@ function applyEnvOverrides(config) {
1128
1154
  if (env.OPENCODE_ANTHROPIC_ANTI_VERBOSITY === "0" || env.OPENCODE_ANTHROPIC_ANTI_VERBOSITY === "false") {
1129
1155
  config.anti_verbosity.enabled = false;
1130
1156
  }
1131
- if (env.OPENCODE_ANTHROPIC_LENGTH_ANCHORS === "0" || env.OPENCODE_ANTHROPIC_LENGTH_ANCHORS === "false") {
1132
- config.anti_verbosity.length_anchors = false;
1133
- }
1134
-
1135
1157
  return config;
1136
1158
  }
1137
1159