opencode-anthropic-fix 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -0
- package/index.mjs +155 -149
- package/lib/config.mjs +47 -25
- package/lib/mimicry/adapter-input.mjs +671 -0
- package/lib/mimicry/context-hint-threshold.mjs +127 -0
- package/lib/mimicry/headers.mjs +71 -64
- package/lib/mimicry/request-body.mjs +23 -22
- package/lib/mimicry/system-prompt.mjs +54 -23
- package/lib/mimicry/wire-compat.mjs +305 -0
- package/package.json +3 -3
- package/lib/account-state.test.mjs +0 -109
- package/lib/accounts.test.mjs +0 -912
- package/lib/adaptive-thinking-models.test.mjs +0 -30
- package/lib/backoff.test.mjs +0 -411
- package/lib/cc-credentials.test.mjs +0 -391
- package/lib/config.test.mjs +0 -764
- package/lib/context-hint-persist.mjs +0 -68
- package/lib/diagnose.test.mjs +0 -122
- package/lib/env.test.mjs +0 -25
- package/lib/haiku-call.test.mjs +0 -72
- package/lib/message-transform.test.mjs +0 -173
- package/lib/mimicry/cache.test.mjs +0 -112
- package/lib/mimicry/headers.test.mjs +0 -50
- package/lib/mimicry/models.test.mjs +0 -132
- package/lib/mimicry/request-body.test.mjs +0 -34
- package/lib/mimicry/request-helpers.test.mjs +0 -155
- package/lib/mimicry/response-stream.test.mjs +0 -127
- package/lib/mimicry/system-prompt.test.mjs +0 -73
- package/lib/oauth.test.mjs +0 -343
- package/lib/redact.test.mjs +0 -154
- package/lib/refresh-lock.test.mjs +0 -131
- package/lib/request-headers.test.mjs +0 -164
- package/lib/retry/overload-loop.test.mjs +0 -107
- package/lib/rotation.test.mjs +0 -392
- package/lib/session-metrics.test.mjs +0 -67
- package/lib/storage.test.mjs +0 -696
- package/lib/token-economy/microcompact.test.mjs +0 -28
- package/lib/token-economy/transforms.test.mjs +0 -192
- 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"}
|
|
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
|
-
//
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
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
|
-
//
|
|
3518
|
-
//
|
|
3519
|
-
//
|
|
3520
|
-
//
|
|
3521
|
-
|
|
3522
|
-
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
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
|
|
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).
|
|
123
|
-
*
|
|
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
|
-
/**
|
|
215
|
-
*
|
|
216
|
-
*
|
|
217
|
-
*
|
|
218
|
-
*
|
|
219
|
-
*
|
|
220
|
-
*
|
|
221
|
-
*
|
|
222
|
-
|
|
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+ `
|
|
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
|
-
*
|
|
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
|
|
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
|
|
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
|
|