opencode-anthropic-fix 0.2.0 → 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.
- package/README.md +31 -6
- package/cli.mjs +1 -0
- package/index.mjs +160 -231
- package/lib/backoff.mjs +1 -1
- package/lib/cc-credentials.mjs +1 -1
- package/lib/config.mjs +49 -27
- package/lib/mimicry/adapter-input.mjs +671 -0
- package/lib/mimicry/context-hint-threshold.mjs +127 -0
- package/lib/mimicry/headers.mjs +71 -66
- package/lib/mimicry/request-body.mjs +23 -24
- package/lib/mimicry/system-prompt.mjs +54 -23
- package/lib/mimicry/wire-compat.mjs +305 -0
- package/lib/rotation.mjs +16 -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 -757
- 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 -346
- 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/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,
|
|
@@ -15,15 +14,10 @@ import {
|
|
|
15
14
|
FALLBACK_CLAUDE_CLI_VERSION,
|
|
16
15
|
CLAUDE_CODE_NPM_LATEST_URL,
|
|
17
16
|
CLAUDE_CODE_BUILD_TIME,
|
|
18
|
-
EXPERIMENTAL_BETA_FLAGS,
|
|
19
|
-
BETA_SHORTCUTS,
|
|
20
17
|
resolveBetaShortcut,
|
|
21
|
-
buildExtendedUserAgent,
|
|
22
18
|
} from "./lib/request-headers.mjs";
|
|
23
|
-
import { loadConfig, loadConfigFresh, saveConfig,
|
|
24
|
-
import { loadContextHintDisabledFlag, saveContextHintDisabledFlag } from "./lib/context-hint-persist.mjs";
|
|
19
|
+
import { loadConfig, loadConfigFresh, saveConfig, getConfigDir } from "./lib/config.mjs";
|
|
25
20
|
import { loadAccounts, saveAccounts, clearAccounts, createDefaultStats } from "./lib/storage.mjs";
|
|
26
|
-
import { applyOAuthCredentials, resetAccountTracking } from "./lib/account-state.mjs";
|
|
27
21
|
import { acquireRefreshLock, releaseRefreshLock } from "./lib/refresh-lock.mjs";
|
|
28
22
|
import {
|
|
29
23
|
isAccountSpecificError,
|
|
@@ -51,7 +45,7 @@ import {
|
|
|
51
45
|
stripMcpPrefixFromParsedEvent,
|
|
52
46
|
} from "./lib/mimicry/response-stream.mjs";
|
|
53
47
|
import { transformRequestBody, CORE_TOOL_NAMES } from "./lib/mimicry/request-body.mjs";
|
|
54
|
-
import {
|
|
48
|
+
import { isTruthyEnv } from "./lib/env.mjs";
|
|
55
49
|
import { sessionMetrics, createInitialSessionMetrics, getAverageCacheHitRate } from "./lib/session-metrics.mjs";
|
|
56
50
|
import { repairOrphanedToolUseBlocks } from "./lib/mimicry/request-helpers.mjs";
|
|
57
51
|
import { resolveCacheTtl, shouldPlaceToolBreakpoint, updateBoundaryStability } from "./lib/mimicry/cache.mjs";
|
|
@@ -66,8 +60,8 @@ import {
|
|
|
66
60
|
buildRequestHeaders,
|
|
67
61
|
buildAnthropicBetaHeader,
|
|
68
62
|
parseRequestBodyMetadata,
|
|
69
|
-
detectProvider,
|
|
70
63
|
extractFileIds,
|
|
64
|
+
stripStainlessHelperMarkers,
|
|
71
65
|
} from "./lib/mimicry/headers.mjs";
|
|
72
66
|
import {
|
|
73
67
|
buildSystemPromptBlocks,
|
|
@@ -82,14 +76,14 @@ import {
|
|
|
82
76
|
CLAUDE_CODE_IDENTITY_STRING,
|
|
83
77
|
SUBAGENT_CC_ANCHOR,
|
|
84
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";
|
|
85
81
|
import {
|
|
86
|
-
CLAUDE_3_MODEL_RE,
|
|
87
82
|
hasOneMillionContext,
|
|
88
83
|
isEligibleFor1MContext,
|
|
89
84
|
isOpus46Model,
|
|
90
85
|
isOpus47Model,
|
|
91
86
|
isOpus48Model,
|
|
92
|
-
isSonnet46Model,
|
|
93
87
|
} from "./lib/mimicry/models.mjs";
|
|
94
88
|
|
|
95
89
|
export { isFable5Model, isMythos5Model, isAdaptiveThinkingModel } from "./lib/mimicry/models.mjs";
|
|
@@ -241,7 +235,7 @@ async function runHaikuSessionSummarize(
|
|
|
241
235
|
}
|
|
242
236
|
}
|
|
243
237
|
|
|
244
|
-
export async function AnthropicAuthPlugin({ client
|
|
238
|
+
export async function AnthropicAuthPlugin({ client }) {
|
|
245
239
|
const config = loadConfig();
|
|
246
240
|
_pluginConfig = config; // expose to module-level functions (cache stats, response headers)
|
|
247
241
|
// QA fix H6: read emulation settings live from config instead of stale const capture
|
|
@@ -325,34 +319,6 @@ export async function AnthropicAuthPlugin({ client, project, directory, worktree
|
|
|
325
319
|
/** @type {Map<string, number>} canonical-beta to rejected-at epoch ms */
|
|
326
320
|
const sessionRejectedBetas = new Map();
|
|
327
321
|
|
|
328
|
-
// Context-hint controller (CC v2.1.110+). Mirrors real CC's `createContextHintController`:
|
|
329
|
-
// sticky on across requests until the server responds with a specific error family.
|
|
330
|
-
// - 422/424 → apply hint compaction (clear thinking + microcompact) and retry
|
|
331
|
-
// - 400 "Unexpected value" + "anthropic-beta" → disable for session (beta unsupported)
|
|
332
|
-
// - 409 → disable for session (conflict)
|
|
333
|
-
// - 529 / overloaded → disable for session (temporary overload)
|
|
334
|
-
// When disabled, we strip context-hint from betas + body on subsequent requests so
|
|
335
|
-
// we don't keep triggering the same rejection and churning the cache.
|
|
336
|
-
// Persisted disable: if a prior session saw a 400 rejecting the context-hint
|
|
337
|
-
// beta (account lacks access), skip the beta from turn 1 of every subsequent
|
|
338
|
-
// session. Delete ~/.config/opencode/context-hint-disabled.flag (or the
|
|
339
|
-
// %APPDATA%\opencode equivalent on Windows) to re-enable once access is
|
|
340
|
-
// granted. See lib/context-hint-persist.mjs.
|
|
341
|
-
const _persistedCtxHint = loadContextHintDisabledFlag();
|
|
342
|
-
const contextHintState = {
|
|
343
|
-
/** Permanently disabled for this session after a server rejection. */
|
|
344
|
-
disabled: _persistedCtxHint.disabled === true,
|
|
345
|
-
/** Number of 422/424 compactions applied this session (for telemetry). */
|
|
346
|
-
compactionsApplied: 0,
|
|
347
|
-
};
|
|
348
|
-
if (contextHintState.disabled) {
|
|
349
|
-
debugLog(
|
|
350
|
-
"context-hint: loaded persisted disable flag",
|
|
351
|
-
_persistedCtxHint.status ? `status=${_persistedCtxHint.status}` : "",
|
|
352
|
-
_persistedCtxHint.timestamp ? `ts=${new Date(_persistedCtxHint.timestamp).toISOString()}` : "",
|
|
353
|
-
);
|
|
354
|
-
}
|
|
355
|
-
|
|
356
322
|
// Token economy — session state for layered compaction strategies.
|
|
357
323
|
const tokenEconomySession = {
|
|
358
324
|
/** When thinking was last stripped (TTL-based strategy). 0 = never. */
|
|
@@ -842,7 +808,7 @@ export async function AnthropicAuthPlugin({ client, project, directory, worktree
|
|
|
842
808
|
`telemetry-emulation: ${fresh.telemetry?.emulate_minimal ? "on (silent observer)" : "off"}`,
|
|
843
809
|
`usage-toast: ${fresh.usage_toast ? "on" : "off"}`,
|
|
844
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"}`,
|
|
845
|
-
`anti-verbosity: ${fresh.anti_verbosity?.enabled !== false ? "on" : "off"}
|
|
811
|
+
`anti-verbosity: ${fresh.anti_verbosity?.enabled !== false ? "on" : "off"}`,
|
|
846
812
|
];
|
|
847
813
|
await sendCommandMessage(input.sessionID, lines.join("\n"));
|
|
848
814
|
return;
|
|
@@ -1191,7 +1157,7 @@ export async function AnthropicAuthPlugin({ client, project, directory, worktree
|
|
|
1191
1157
|
config.toasts.quiet = enabled;
|
|
1192
1158
|
},
|
|
1193
1159
|
strategy: () => {
|
|
1194
|
-
const valid = ["sticky", "round-robin", "hybrid"];
|
|
1160
|
+
const valid = ["sticky", "round-robin", "hybrid", "single"];
|
|
1195
1161
|
if (valid.includes(value)) {
|
|
1196
1162
|
saveConfig({ account_selection_strategy: value });
|
|
1197
1163
|
strategyState.manualOverride = true;
|
|
@@ -2881,7 +2847,6 @@ export async function AnthropicAuthPlugin({ client, project, directory, worktree
|
|
|
2881
2847
|
requestInit.body,
|
|
2882
2848
|
_parsedBodyOnce,
|
|
2883
2849
|
);
|
|
2884
|
-
const _reqProvider = detectProvider(requestUrl);
|
|
2885
2850
|
|
|
2886
2851
|
// --- Adaptive 1M context decision (once per logical request, not per retry) ---
|
|
2887
2852
|
if (!_adaptiveDecisionMade) {
|
|
@@ -2916,12 +2881,7 @@ export async function AnthropicAuthPlugin({ client, project, directory, worktree
|
|
|
2916
2881
|
const _adaptiveOverride = _adaptiveOverrideForRequest;
|
|
2917
2882
|
|
|
2918
2883
|
// Token economy config (resolved once, passed to beta builder).
|
|
2919
|
-
//
|
|
2920
|
-
// so buildAnthropicBetaHeader (called again inside buildRequestHeaders)
|
|
2921
|
-
// drops the beta on subsequent requests.
|
|
2922
|
-
// Also classify the request role (CC's querySource analog) and
|
|
2923
|
-
// suppress context-hint for non-main-thread requests so we don't
|
|
2924
|
-
// opt subagent/title/tool one-shots into a server retry loop.
|
|
2884
|
+
// Also classify the request role (CC's querySource analog).
|
|
2925
2885
|
const _requestRole = classifyRequestRole(_parsedBodyOnce);
|
|
2926
2886
|
// opencode tags subagent requests with the x-parent-session-id header
|
|
2927
2887
|
// (set in its request builder only when a parent session exists).
|
|
@@ -2930,10 +2890,7 @@ export async function AnthropicAuthPlugin({ client, project, directory, worktree
|
|
|
2930
2890
|
// only to pick the cheaper 5m cache tier for one-shot subagents.
|
|
2931
2891
|
const _isSubagent = getIncomingHeader(input, requestInit, "x-parent-session-id") != null;
|
|
2932
2892
|
const _baseTE = config.token_economy || {};
|
|
2933
|
-
const
|
|
2934
|
-
const _tokenEconomy = _disableCtxHint
|
|
2935
|
-
? { ..._baseTE, context_hint: false, __requestRole: _requestRole }
|
|
2936
|
-
: { ..._baseTE, __requestRole: _requestRole };
|
|
2893
|
+
const _tokenEconomy = { ..._baseTE, __requestRole: _requestRole };
|
|
2937
2894
|
|
|
2938
2895
|
// Microcompact: inject clear betas at high context utilization
|
|
2939
2896
|
let _microcompactBetas = null;
|
|
@@ -2975,7 +2932,6 @@ export async function AnthropicAuthPlugin({ client, project, directory, worktree
|
|
|
2975
2932
|
"",
|
|
2976
2933
|
getSignatureEmulationEnabled(),
|
|
2977
2934
|
_reqModel,
|
|
2978
|
-
_reqProvider,
|
|
2979
2935
|
_sessionFilteredCustomBetas,
|
|
2980
2936
|
getEffectiveStrategy(),
|
|
2981
2937
|
requestUrl?.pathname,
|
|
@@ -3004,12 +2960,6 @@ export async function AnthropicAuthPlugin({ client, project, directory, worktree
|
|
|
3004
2960
|
// Merge latched betas that aren't in the current set
|
|
3005
2961
|
const merged = new Set(currentBetas);
|
|
3006
2962
|
for (const b of betaLatchState.sent) merged.add(b);
|
|
3007
|
-
// Context-hint kill switch: once server rejected it this session,
|
|
3008
|
-
// stop sending the beta (body field is gated on header, so it drops too).
|
|
3009
|
-
if (contextHintState.disabled) {
|
|
3010
|
-
merged.delete("context-hint-2026-04-09");
|
|
3011
|
-
betaLatchState.sent.delete("context-hint-2026-04-09");
|
|
3012
|
-
}
|
|
3013
2963
|
// Custom-beta strip kill switch: when the server rejected our custom
|
|
3014
2964
|
// betas (customBetasStripped latch fired), evict them from the latch so
|
|
3015
2965
|
// they do not re-appear on the retry attempt.
|
|
@@ -3052,13 +3002,38 @@ export async function AnthropicAuthPlugin({ client, project, directory, worktree
|
|
|
3052
3002
|
const effectiveCachePolicy = latchedCachePolicy ||
|
|
3053
3003
|
config.cache_policy || { ttl: "1h", ttl_supported: true };
|
|
3054
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
|
+
|
|
3055
3030
|
const body = transformRequestBody(
|
|
3056
3031
|
requestInit.body,
|
|
3057
3032
|
{
|
|
3058
3033
|
enabled: getSignatureEmulationEnabled(),
|
|
3059
3034
|
claudeCliVersion,
|
|
3060
3035
|
promptCompactionMode: getPromptCompactionMode(),
|
|
3061
|
-
|
|
3036
|
+
useAdapter: _useAdapter,
|
|
3062
3037
|
cachePolicy: effectiveCachePolicy,
|
|
3063
3038
|
fastMode: config.fast_mode || false,
|
|
3064
3039
|
// When the account's fast pool is cooling down, suppress
|
|
@@ -3127,29 +3102,107 @@ export async function AnthropicAuthPlugin({ client, project, directory, worktree
|
|
|
3127
3102
|
}
|
|
3128
3103
|
}
|
|
3129
3104
|
|
|
3130
|
-
// Build headers with the selected account's token
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
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
|
+
}
|
|
3147
3200
|
// cch stays as the static "00000" placeholder — cc-107 and cc-108
|
|
3148
3201
|
// JS bundles both emit `cch=00000;` unconditionally in the billing
|
|
3149
3202
|
// header. The Bun-binary Attestation.zig xxHash64 mechanism lives in
|
|
3150
3203
|
// a SEPARATE header path, not in this body field. Re-hashing here
|
|
3151
3204
|
// mutates system[0] each turn, invalidating the prompt cache.
|
|
3152
|
-
const finalBody = body;
|
|
3205
|
+
const finalBody = adapterBody ?? legacyStrippedBody ?? body;
|
|
3153
3206
|
|
|
3154
3207
|
const correlationId = createDebugCorrelationId();
|
|
3155
3208
|
|
|
@@ -3520,76 +3573,35 @@ export async function AnthropicAuthPlugin({ client, project, directory, worktree
|
|
|
3520
3573
|
// Ignore read errors in debug logging path.
|
|
3521
3574
|
}
|
|
3522
3575
|
|
|
3523
|
-
//
|
|
3524
|
-
//
|
|
3525
|
-
//
|
|
3526
|
-
//
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
});
|
|
3549
|
-
debugLog("context-hint: beta rejected by server (400), disabling + persisting");
|
|
3550
|
-
// Retry without the beta. The latch above drops the header
|
|
3551
|
-
// and the body field on the rebuilt request.
|
|
3552
|
-
attempt--;
|
|
3553
|
-
continue;
|
|
3554
|
-
} else if (response.status === 409) {
|
|
3555
|
-
contextHintState.disabled = true;
|
|
3556
|
-
betaLatchState.dirty = true;
|
|
3557
|
-
debugLog("context-hint: 409 conflict, disabling for session");
|
|
3558
|
-
attempt--;
|
|
3559
|
-
continue;
|
|
3560
|
-
} else if (response.status === 529 && errorBody && errorBody.includes("context_hint")) {
|
|
3561
|
-
contextHintState.disabled = true;
|
|
3562
|
-
betaLatchState.dirty = true;
|
|
3563
|
-
debugLog("context-hint: 529 overloaded referencing hint, disabling for session");
|
|
3564
|
-
attempt--;
|
|
3565
|
-
continue;
|
|
3566
|
-
} else if (
|
|
3567
|
-
(response.status === 422 || response.status === 424) &&
|
|
3568
|
-
!requestInit._contextHintCompactAttempted
|
|
3569
|
-
) {
|
|
3570
|
-
try {
|
|
3571
|
-
const hintBody = JSON.parse(requestInit.body);
|
|
3572
|
-
if (Array.isArray(hintBody.messages)) {
|
|
3573
|
-
const compacted = applyContextHintCompaction(hintBody.messages);
|
|
3574
|
-
if (compacted.changed) {
|
|
3575
|
-
hintBody.messages = compacted.messages;
|
|
3576
|
-
requestInit.body = JSON.stringify(hintBody);
|
|
3577
|
-
_parsedBodyOnce = null;
|
|
3578
|
-
requestInit._contextHintCompactAttempted = true;
|
|
3579
|
-
contextHintState.compactionsApplied += 1;
|
|
3580
|
-
attempt--;
|
|
3581
|
-
toast(
|
|
3582
|
-
`⚙ Context hint compaction (${response.status}) — cleared ${compacted.stats.thinkingCleared} thinking / ${compacted.stats.toolResultsCleared} tool results`,
|
|
3583
|
-
"info",
|
|
3584
|
-
{ debounceKey: "context-hint-compact" },
|
|
3585
|
-
).catch(() => {});
|
|
3586
|
-
debugLog("context-hint: applied compaction on status", response.status, compacted.stats);
|
|
3587
|
-
continue;
|
|
3588
|
-
}
|
|
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;
|
|
3589
3601
|
}
|
|
3590
|
-
} catch {
|
|
3591
|
-
// fall through to normal error handling
|
|
3592
3602
|
}
|
|
3603
|
+
} catch {
|
|
3604
|
+
// fall through to normal error handling
|
|
3593
3605
|
}
|
|
3594
3606
|
}
|
|
3595
3607
|
|
|
@@ -5375,7 +5387,6 @@ function writeCacheStatsFile(usage, model, hitRate) {
|
|
|
5375
5387
|
cache_ttl: _pluginConfig?.cache_policy?.ttl ?? "1h",
|
|
5376
5388
|
boundary_marker: _pluginConfig?.cache_policy?.boundary_marker ?? false,
|
|
5377
5389
|
anti_verbosity: _pluginConfig?.anti_verbosity?.enabled !== false,
|
|
5378
|
-
length_anchors: _pluginConfig?.anti_verbosity?.length_anchors !== false,
|
|
5379
5390
|
},
|
|
5380
5391
|
timestamp: new Date().toISOString(),
|
|
5381
5392
|
};
|
|
@@ -5572,34 +5583,6 @@ process.once("beforeExit", _beforeExitHandler);
|
|
|
5572
5583
|
// Request building helpers (extracted from original fetch interceptor)
|
|
5573
5584
|
// ---------------------------------------------------------------------------
|
|
5574
5585
|
|
|
5575
|
-
// cch attestation: RE-ENABLED with xxHash64 (matching Bun binary's Attestation.zig).
|
|
5576
|
-
// The compiled Bun binary computes cch dynamically: xxHash64(body, seed) & 0xFFFFF.
|
|
5577
|
-
// Captured real CC v2.1.107 request shows cch=6d00f (5-hex-char, 20-bit masked hash).
|
|
5578
|
-
// Seed extracted from binary: 0x6E52736AC806831E (unchanged since v2.1.96).
|
|
5579
|
-
const CCH_SEED = 0x6e52736ac806831en; // BigInt — Attestation.zig seed
|
|
5580
|
-
|
|
5581
|
-
/** @type {null | ((buf: Uint8Array, seed: bigint) => bigint)} */
|
|
5582
|
-
let _xxh64Raw = null;
|
|
5583
|
-
const _xxhashReady = xxhashInit().then((h) => {
|
|
5584
|
-
_xxh64Raw = h.h64Raw;
|
|
5585
|
-
});
|
|
5586
|
-
|
|
5587
|
-
/**
|
|
5588
|
-
* Compute and replace the cch=00000 placeholder in the serialized body with
|
|
5589
|
-
* xxHash64(body, seed) & 0xFFFFF, matching the Bun binary's native attestation.
|
|
5590
|
-
* @param {string} body - Serialized JSON body
|
|
5591
|
-
* @returns {Promise<string>} Body with cch replaced
|
|
5592
|
-
*/
|
|
5593
|
-
async function computeAndReplaceCCH(body) {
|
|
5594
|
-
if (typeof body !== "string" || !body.includes("cch=00000")) return body;
|
|
5595
|
-
await _xxhashReady;
|
|
5596
|
-
if (!_xxh64Raw) return body; // fallback: send as-is if wasm failed to load
|
|
5597
|
-
const bodyBytes = Buffer.from(body, "utf-8");
|
|
5598
|
-
const hash = _xxh64Raw(bodyBytes, CCH_SEED);
|
|
5599
|
-
const cch = (hash & 0xfffffn).toString(16).padStart(5, "0");
|
|
5600
|
-
return body.replace("cch=00000", `cch=${cch}`);
|
|
5601
|
-
}
|
|
5602
|
-
|
|
5603
5586
|
/**
|
|
5604
5587
|
* Extract the text content of the first user message for billing hash computation.
|
|
5605
5588
|
* @param {any[] | undefined} messages
|
|
@@ -5700,13 +5683,6 @@ function shouldDebugSystemPrompt() {
|
|
|
5700
5683
|
* @returns {Record<string, string>}
|
|
5701
5684
|
*/
|
|
5702
5685
|
|
|
5703
|
-
/**
|
|
5704
|
-
* @returns {string}
|
|
5705
|
-
*/
|
|
5706
|
-
function getClaudeEntrypoint() {
|
|
5707
|
-
return process.env.CLAUDE_CODE_ENTRYPOINT || "cli";
|
|
5708
|
-
}
|
|
5709
|
-
|
|
5710
5686
|
/**
|
|
5711
5687
|
* @param {string | undefined} model
|
|
5712
5688
|
* @returns {string | undefined}
|
|
@@ -5733,15 +5709,6 @@ function logTransformedSystemPrompt(body) {
|
|
|
5733
5709
|
* @returns {boolean}
|
|
5734
5710
|
*/
|
|
5735
5711
|
|
|
5736
|
-
/**
|
|
5737
|
-
* @param {string | undefined} body
|
|
5738
|
-
* @returns {boolean}
|
|
5739
|
-
*/
|
|
5740
|
-
function supportsThinking(model) {
|
|
5741
|
-
if (!model) return true;
|
|
5742
|
-
return /claude|sonnet|opus|haiku/i.test(model);
|
|
5743
|
-
}
|
|
5744
|
-
|
|
5745
5712
|
/**
|
|
5746
5713
|
* @param {string | undefined} body
|
|
5747
5714
|
* @returns {boolean}
|
|
@@ -5830,19 +5797,6 @@ function getAccountIdentifier(account) {
|
|
|
5830
5797
|
* @returns {{user_id: string}}
|
|
5831
5798
|
*/
|
|
5832
5799
|
|
|
5833
|
-
/**
|
|
5834
|
-
* Return the cache_control object appropriate for the given cache policy.
|
|
5835
|
-
* @param {{ttl: string, ttl_supported: boolean, boundary_marker?: boolean} | undefined} cachePolicy
|
|
5836
|
-
* @returns {{type: string, ttl?: string}}
|
|
5837
|
-
*/
|
|
5838
|
-
function getCacheControlForPolicy(cachePolicy) {
|
|
5839
|
-
if (!cachePolicy) return { type: "ephemeral" };
|
|
5840
|
-
if (cachePolicy.ttl === "off" || cachePolicy.ttl_supported === false) {
|
|
5841
|
-
return { type: "ephemeral" };
|
|
5842
|
-
}
|
|
5843
|
-
return { type: "ephemeral", ttl: cachePolicy.ttl };
|
|
5844
|
-
}
|
|
5845
|
-
|
|
5846
5800
|
/**
|
|
5847
5801
|
* @param {string} incomingBeta
|
|
5848
5802
|
* @param {boolean} signatureEnabled
|
|
@@ -5866,18 +5820,6 @@ function getCacheControlForPolicy(cachePolicy) {
|
|
|
5866
5820
|
|
|
5867
5821
|
/** @typedef {'low' | 'medium' | 'high'} ThinkingEffort */
|
|
5868
5822
|
|
|
5869
|
-
/**
|
|
5870
|
-
* Map budgetTokens to an effort level.
|
|
5871
|
-
* Used when an Opus 4.6 request arrives with the legacy budgetTokens shape.
|
|
5872
|
-
* @param {number} budgetTokens
|
|
5873
|
-
* @returns {ThinkingEffort}
|
|
5874
|
-
*/
|
|
5875
|
-
function budgetTokensToEffort(budgetTokens) {
|
|
5876
|
-
if (budgetTokens <= 1024) return "low";
|
|
5877
|
-
if (budgetTokens <= 8000) return "medium";
|
|
5878
|
-
return "high";
|
|
5879
|
-
}
|
|
5880
|
-
|
|
5881
5823
|
// QA fix L3: budgetTokensToEffort() removed — dead code, never called
|
|
5882
5824
|
// QA fix L4: isValidEffort() removed — dead code, never called
|
|
5883
5825
|
|
|
@@ -6068,19 +6010,6 @@ function isEventStreamResponse(response) {
|
|
|
6068
6010
|
return contentType.toLowerCase().includes("text/event-stream");
|
|
6069
6011
|
}
|
|
6070
6012
|
|
|
6071
|
-
/**
|
|
6072
|
-
* Build user-facing switch reason text for account-specific errors.
|
|
6073
|
-
* @param {number} status
|
|
6074
|
-
* @param {import('./lib/backoff.mjs').RateLimitReason} reason
|
|
6075
|
-
* @returns {string}
|
|
6076
|
-
*/
|
|
6077
|
-
function formatSwitchReason(status, reason) {
|
|
6078
|
-
if (reason === "AUTH_FAILED") return "auth failed";
|
|
6079
|
-
if (status === 403 && reason === "QUOTA_EXHAUSTED") return "permission denied";
|
|
6080
|
-
if (reason === "QUOTA_EXHAUSTED") return "quota exhausted";
|
|
6081
|
-
return "rate-limited";
|
|
6082
|
-
}
|
|
6083
|
-
|
|
6084
6013
|
// ---------------------------------------------------------------------------
|
|
6085
6014
|
// Token refresh (per-account)
|
|
6086
6015
|
// ---------------------------------------------------------------------------
|
|
@@ -6153,7 +6082,7 @@ function applyDiskAuthIfFresher(account, diskAuth, options = {}) {
|
|
|
6153
6082
|
* @returns {Promise<string>} The new access token
|
|
6154
6083
|
* @throws {Error} If refresh fails
|
|
6155
6084
|
*/
|
|
6156
|
-
async function refreshAccountToken(account, client,
|
|
6085
|
+
async function refreshAccountToken(account, client, _source = "foreground", { onTokensUpdated } = {}) {
|
|
6157
6086
|
// CC-sourced accounts must NEVER enter the OAuth HTTP refresh flow.
|
|
6158
6087
|
// Instead, re-read credentials from the CC source. If they're still
|
|
6159
6088
|
// expired, let the caller handle it (the account will be skipped).
|
package/lib/backoff.mjs
CHANGED
|
@@ -134,7 +134,7 @@ function extractErrorSignals(body) {
|
|
|
134
134
|
* @returns {boolean}
|
|
135
135
|
*/
|
|
136
136
|
function bodyHasAccountError(body) {
|
|
137
|
-
const { errorType, message
|
|
137
|
+
const { errorType, message } = extractErrorSignals(body);
|
|
138
138
|
|
|
139
139
|
const typeSignals = [
|
|
140
140
|
"rate_limit",
|
package/lib/cc-credentials.mjs
CHANGED