opencode-anthropic-fix 0.6.0 → 2.0.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 +30 -3
- package/cli.mjs +77 -24
- package/index.mjs +297 -200
- package/lib/account-state.mjs +48 -0
- package/lib/accounts.mjs +211 -12
- package/lib/betas.mjs +131 -0
- package/lib/config.mjs +90 -12
- package/lib/diagnose.mjs +4 -2
- package/lib/mimicry/adapter-input.mjs +137 -33
- package/lib/mimicry/headers.mjs +143 -15
- package/lib/mimicry/request-body.mjs +51 -14
- package/lib/mimicry/system-prompt.mjs +72 -293
- package/lib/mimicry/wire-compat.mjs +133 -11
- package/lib/oauth-constants.mjs +109 -0
- package/lib/oauth-env.mjs +187 -0
- package/lib/oauth-scopes.mjs +119 -0
- package/lib/oauth.mjs +270 -90
- package/lib/passthrough-headers.mjs +152 -0
- package/lib/token-economy/microcompact.mjs +3 -12
- package/package.json +2 -2
- package/lib/mimicry/models.mjs +0 -146
- package/lib/request-headers.mjs +0 -303
package/index.mjs
CHANGED
|
@@ -3,19 +3,14 @@ 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 { AccountManager, RATE_LIMIT_KEY_FAST } from "./lib/accounts.mjs";
|
|
6
|
+
import { AccountManager, RATE_LIMIT_KEY_FAST, warnOnEphemeralTokenRotation } from "./lib/accounts.mjs";
|
|
7
7
|
import {
|
|
8
8
|
authorize as oauthAuthorize,
|
|
9
9
|
exchange as oauthExchange,
|
|
10
10
|
parseOAuthCallback,
|
|
11
11
|
refreshToken,
|
|
12
12
|
} from "./lib/oauth.mjs";
|
|
13
|
-
import {
|
|
14
|
-
FALLBACK_CLAUDE_CLI_VERSION,
|
|
15
|
-
CLAUDE_CODE_NPM_LATEST_URL,
|
|
16
|
-
CLAUDE_CODE_BUILD_TIME,
|
|
17
|
-
resolveBetaShortcut,
|
|
18
|
-
} from "./lib/request-headers.mjs";
|
|
13
|
+
import { resolveBetaShortcut } from "./lib/betas.mjs";
|
|
19
14
|
import { loadConfig, loadConfigFresh, saveConfig, getConfigDir } from "./lib/config.mjs";
|
|
20
15
|
import { loadAccounts, saveAccounts, clearAccounts, createDefaultStats } from "./lib/storage.mjs";
|
|
21
16
|
import { acquireRefreshLock, releaseRefreshLock } from "./lib/refresh-lock.mjs";
|
|
@@ -37,7 +32,7 @@ import {
|
|
|
37
32
|
maybeApplySessionToolResultDedupe,
|
|
38
33
|
applyContextHintCompaction,
|
|
39
34
|
} from "./lib/token-economy/transforms.mjs";
|
|
40
|
-
import { shouldMicrocompact
|
|
35
|
+
import { shouldMicrocompact } from "./lib/token-economy/microcompact.mjs";
|
|
41
36
|
import { redactSecrets, redactString } from "./lib/redact.mjs";
|
|
42
37
|
import {
|
|
43
38
|
createTransformedSSEStream,
|
|
@@ -49,7 +44,12 @@ import { isTruthyEnv } from "./lib/env.mjs";
|
|
|
49
44
|
import { sessionMetrics, createInitialSessionMetrics, getAverageCacheHitRate } from "./lib/session-metrics.mjs";
|
|
50
45
|
import { repairOrphanedToolUseBlocks } from "./lib/mimicry/request-helpers.mjs";
|
|
51
46
|
import { resolveCacheTtl, shouldPlaceToolBreakpoint, updateBoundaryStability } from "./lib/mimicry/cache.mjs";
|
|
52
|
-
import {
|
|
47
|
+
import {
|
|
48
|
+
SERVICE_WIDE_MAX_RETRIES,
|
|
49
|
+
CONSECUTIVE_529_FALLBACK_THRESHOLD,
|
|
50
|
+
FOREGROUND_REFRESH_EXPIRY_BUFFER_MS,
|
|
51
|
+
} from "./lib/tuning.mjs";
|
|
52
|
+
import { isTokenExpired, isTokenUsable } from "./lib/account-state.mjs";
|
|
53
53
|
import {
|
|
54
54
|
computeServiceRetrySleepMs,
|
|
55
55
|
selectFallbackModel,
|
|
@@ -58,7 +58,6 @@ import {
|
|
|
58
58
|
} from "./lib/retry/overload-loop.mjs";
|
|
59
59
|
import {
|
|
60
60
|
buildRequestHeaders,
|
|
61
|
-
buildAnthropicBetaHeader,
|
|
62
61
|
parseRequestBodyMetadata,
|
|
63
62
|
extractFileIds,
|
|
64
63
|
stripStainlessHelperMarkers,
|
|
@@ -68,25 +67,31 @@ import {
|
|
|
68
67
|
compactSystemText,
|
|
69
68
|
compactToolDescription,
|
|
70
69
|
dedupeSystemBlocks,
|
|
71
|
-
getCachedCCPrompt,
|
|
72
70
|
isTitleGeneratorSystemBlocks,
|
|
73
71
|
normalizeSystemTextBlocks,
|
|
74
|
-
resetCachedCCPrompt,
|
|
75
72
|
sanitizeSystemText,
|
|
76
73
|
CLAUDE_CODE_IDENTITY_STRING,
|
|
77
|
-
SUBAGENT_CC_ANCHOR,
|
|
78
74
|
} from "./lib/mimicry/system-prompt.mjs";
|
|
79
|
-
import { buildAdapterTransport, resolveAdapterEnv } from "./lib/mimicry/adapter-input.mjs";
|
|
80
|
-
import { buildWireCompatibleRequest, buildWireCompatibleCountTokensRequest } from "./lib/mimicry/wire-compat.mjs";
|
|
81
75
|
import {
|
|
76
|
+
ADAPTER_COUNT_TOKENS_PATHNAMES,
|
|
77
|
+
ADAPTER_MESSAGES_PATHNAMES,
|
|
78
|
+
assertAdapterBodyUsable,
|
|
79
|
+
buildAdapterTransport,
|
|
80
|
+
resolveAdapterEnv,
|
|
81
|
+
} from "./lib/mimicry/adapter-input.mjs";
|
|
82
|
+
import { buildPassthroughHeaders, stripNonApiBodyFields } from "./lib/passthrough-headers.mjs";
|
|
83
|
+
import {
|
|
84
|
+
buildWireCompatibleRequest,
|
|
85
|
+
buildWireCompatibleCountTokensRequest,
|
|
86
|
+
WIRE_PROFILE,
|
|
82
87
|
hasOneMillionContext,
|
|
83
|
-
|
|
88
|
+
isEligibleFor1MContextWire,
|
|
84
89
|
isOpus46Model,
|
|
85
90
|
isOpus47Model,
|
|
86
91
|
isOpus48Model,
|
|
87
|
-
} from "./lib/mimicry/
|
|
92
|
+
} from "./lib/mimicry/wire-compat.mjs";
|
|
88
93
|
|
|
89
|
-
export { isFable5Model, isMythos5Model, isAdaptiveThinkingModel } from "./lib/mimicry/
|
|
94
|
+
export { isFable5Model, isMythos5Model, isAdaptiveThinkingModel } from "./lib/mimicry/wire-compat.mjs";
|
|
90
95
|
|
|
91
96
|
// Max times a single logical request may fall back from fast->standard speed on
|
|
92
97
|
// the same account before giving up the fast attempt entirely. 1 is enough: one
|
|
@@ -298,18 +303,6 @@ export async function AnthropicAuthPlugin({ client }) {
|
|
|
298
303
|
let willowLastSuggestionTime = 0;
|
|
299
304
|
let _lastOAuthPruneTime = 0; // QA fix L-oauthPrune: throttle for periodic prune
|
|
300
305
|
|
|
301
|
-
// Beta header latching: once a beta is sent in a session, it stays on for
|
|
302
|
-
// the rest of the session to prevent cache key churn (~50-70K tokens per flip).
|
|
303
|
-
// Cleared on /clear or /compact if needed.
|
|
304
|
-
const betaLatchState = {
|
|
305
|
-
/** @type {Set<string>} betas that have been sent at least once this session */
|
|
306
|
-
sent: new Set(),
|
|
307
|
-
/** When true, a config change invalidated the latch and next request rebuilds. */
|
|
308
|
-
dirty: false,
|
|
309
|
-
/** @type {string | null} The last computed beta header string (for latching). */
|
|
310
|
-
lastHeader: null,
|
|
311
|
-
};
|
|
312
|
-
|
|
313
306
|
// F4: Session-level latch for rejected custom betas.
|
|
314
307
|
// When a custom beta triggers a 400/anthropic-beta or 413-with-signal rejection,
|
|
315
308
|
// its canonical name is stored here so subsequent requests within
|
|
@@ -1240,8 +1233,6 @@ export async function AnthropicAuthPlugin({ client }) {
|
|
|
1240
1233
|
te.token_efficient_tools = enabled;
|
|
1241
1234
|
saveConfig({ token_economy: te });
|
|
1242
1235
|
config.token_economy = te;
|
|
1243
|
-
// Invalidate latched betas so the change takes effect next request
|
|
1244
|
-
betaLatchState.dirty = true;
|
|
1245
1236
|
},
|
|
1246
1237
|
"redact-thinking": () => {
|
|
1247
1238
|
const enabled = value === "on" || value === "true" || value === "1";
|
|
@@ -1251,7 +1242,6 @@ export async function AnthropicAuthPlugin({ client }) {
|
|
|
1251
1242
|
te.redact_thinking = enabled;
|
|
1252
1243
|
saveConfig({ token_economy: te });
|
|
1253
1244
|
config.token_economy = te;
|
|
1254
|
-
betaLatchState.dirty = true;
|
|
1255
1245
|
},
|
|
1256
1246
|
"tool-deferral": () => {
|
|
1257
1247
|
const enabled = value === "on" || value === "true" || value === "1";
|
|
@@ -1439,7 +1429,7 @@ export async function AnthropicAuthPlugin({ client }) {
|
|
|
1439
1429
|
*/
|
|
1440
1430
|
async function getFilesAuth(acct) {
|
|
1441
1431
|
let tok = acct.access;
|
|
1442
|
-
if (!tok ||
|
|
1432
|
+
if (!tok || isTokenExpired(acct.expires)) {
|
|
1443
1433
|
tok = await refreshAccountTokenSingleFlight(acct);
|
|
1444
1434
|
}
|
|
1445
1435
|
return {
|
|
@@ -2215,7 +2205,9 @@ export async function AnthropicAuthPlugin({ client }) {
|
|
|
2215
2205
|
return config.account_selection_strategy;
|
|
2216
2206
|
}
|
|
2217
2207
|
|
|
2218
|
-
|
|
2208
|
+
// Baseline CLI version, read from the wire package's profile through the
|
|
2209
|
+
// adapter seam. `fetchLatestClaudeCodeVersion` may raise it at runtime.
|
|
2210
|
+
let claudeCliVersion = WIRE_PROFILE.cliVersion;
|
|
2219
2211
|
const signatureSessionId = randomUUID();
|
|
2220
2212
|
const signatureUserId = getOrCreateDeviceId();
|
|
2221
2213
|
if (shouldFetchClaudeCodeVersion) {
|
|
@@ -2297,7 +2289,7 @@ export async function AnthropicAuthPlugin({ client }) {
|
|
|
2297
2289
|
// Ignore idle failure here; foreground path handles refresh decisions.
|
|
2298
2290
|
}
|
|
2299
2291
|
|
|
2300
|
-
if (account.access && account.expires
|
|
2292
|
+
if (account.access && isTokenUsable(account.expires)) {
|
|
2301
2293
|
return account.access;
|
|
2302
2294
|
}
|
|
2303
2295
|
} else {
|
|
@@ -2410,7 +2402,7 @@ export async function AnthropicAuthPlugin({ client }) {
|
|
|
2410
2402
|
const excluded = new Set([activeAccount.index]);
|
|
2411
2403
|
const candidates = accountManager
|
|
2412
2404
|
.getEnabledAccounts(excluded)
|
|
2413
|
-
.filter((acc) =>
|
|
2405
|
+
.filter((acc) => isTokenExpired(acc.expires, getIdleRefreshWindowMs(), now))
|
|
2414
2406
|
.filter((acc) => {
|
|
2415
2407
|
const last = idleRefreshLastAttempt.get(acc.id) ?? 0;
|
|
2416
2408
|
return now - last >= getIdleRefreshMinIntervalMs();
|
|
@@ -2577,7 +2569,26 @@ export async function AnthropicAuthPlugin({ client }) {
|
|
|
2577
2569
|
|
|
2578
2570
|
// Transform URL once (shared across retries)
|
|
2579
2571
|
const requestInit = init ?? {};
|
|
2580
|
-
|
|
2572
|
+
// The emulation flag decides whether the URL may be reshaped at
|
|
2573
|
+
// all (`?beta=true`, `/messages` -> `/v1/messages`). Read here,
|
|
2574
|
+
// live, because `/anthropic set` can flip it between requests.
|
|
2575
|
+
const { requestInput, requestUrl } = transformRequestUrl(input, getSignatureEmulationEnabled());
|
|
2576
|
+
|
|
2577
|
+
// A host may hand us a `Request` carrying the body, with the init
|
|
2578
|
+
// empty — `fetchFn(new Request(url, {body}), {})`. Every body-aware
|
|
2579
|
+
// stage downstream (`_parsedBodyOnce`, `transformRequestBody`, the
|
|
2580
|
+
// adapter, the retry-loop rewrites) reads `requestInit.body` and
|
|
2581
|
+
// would silently see nothing, so the body is lifted onto the init
|
|
2582
|
+
// once, here. `clone()` keeps the original Request readable, which
|
|
2583
|
+
// matters because `requestInput` is still what gets sent when no
|
|
2584
|
+
// adapter URL is adopted.
|
|
2585
|
+
if (requestInit.body == null && input instanceof Request && input.body != null) {
|
|
2586
|
+
try {
|
|
2587
|
+
requestInit.body = await input.clone().text();
|
|
2588
|
+
} catch (error) {
|
|
2589
|
+
debugLog(`could not read the body off the host Request: ${error.message}`);
|
|
2590
|
+
}
|
|
2591
|
+
}
|
|
2581
2592
|
const requestMethod = String(
|
|
2582
2593
|
requestInit.method || (requestInput instanceof Request ? requestInput.method : "POST"),
|
|
2583
2594
|
).toUpperCase();
|
|
@@ -2732,7 +2743,7 @@ export async function AnthropicAuthPlugin({ client }) {
|
|
|
2732
2743
|
let accessToken;
|
|
2733
2744
|
// Per-account token refresh
|
|
2734
2745
|
// Refresh 5 minutes before expiry to avoid mid-request token expiration (RE doc §1.10)
|
|
2735
|
-
if (!account.access ||
|
|
2746
|
+
if (!account.access || isTokenExpired(account.expires, FOREGROUND_REFRESH_EXPIRY_BUFFER_MS)) {
|
|
2736
2747
|
const attemptedRefreshToken = account.refreshToken;
|
|
2737
2748
|
try {
|
|
2738
2749
|
accessToken = await refreshAccountTokenSingleFlight(account);
|
|
@@ -2892,14 +2903,12 @@ export async function AnthropicAuthPlugin({ client }) {
|
|
|
2892
2903
|
const _baseTE = config.token_economy || {};
|
|
2893
2904
|
const _tokenEconomy = { ..._baseTE, __requestRole: _requestRole };
|
|
2894
2905
|
|
|
2895
|
-
// Microcompact:
|
|
2896
|
-
let _microcompactBetas = null;
|
|
2906
|
+
// Microcompact: track high context utilization (state + toast only)
|
|
2897
2907
|
if (requestInit.body) {
|
|
2898
2908
|
const estimatedTokens = _parsedBodyOnce
|
|
2899
2909
|
? estimatePromptTokensFromParsed(_parsedBodyOnce)
|
|
2900
2910
|
: estimatePromptTokens(requestInit.body);
|
|
2901
2911
|
if (shouldMicrocompact(estimatedTokens, config)) {
|
|
2902
|
-
_microcompactBetas = buildMicrocompactBetas();
|
|
2903
2912
|
if (!microcompactState.active) {
|
|
2904
2913
|
microcompactState.active = true;
|
|
2905
2914
|
microcompactState.lastActivatedTurn = sessionMetrics.turns;
|
|
@@ -2928,68 +2937,53 @@ export async function AnthropicAuthPlugin({ client }) {
|
|
|
2928
2937
|
}
|
|
2929
2938
|
return false;
|
|
2930
2939
|
});
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
//
|
|
2945
|
-
//
|
|
2946
|
-
//
|
|
2947
|
-
//
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
// they cannot re-enter the sent set from a prior latched header.
|
|
2979
|
-
if (sessionRejectedBetas.size > 0) {
|
|
2980
|
-
const _now = Date.now();
|
|
2981
|
-
for (const [_canonical, _rejectedAt] of sessionRejectedBetas) {
|
|
2982
|
-
if (_now - _rejectedAt <= SESSION_REJECTED_BETA_TTL_MS) {
|
|
2983
|
-
merged.delete(_canonical);
|
|
2984
|
-
betaLatchState.sent.delete(_canonical);
|
|
2985
|
-
} else {
|
|
2986
|
-
sessionRejectedBetas.delete(_canonical);
|
|
2987
|
-
}
|
|
2988
|
-
}
|
|
2989
|
-
}
|
|
2990
|
-
computedBetaHeader = [...merged].join(",");
|
|
2991
|
-
betaLatchState.lastHeader = computedBetaHeader;
|
|
2992
|
-
}
|
|
2940
|
+
// PHASE 2.2.3 — THE BETA LATCH IS GONE. It used to merge every
|
|
2941
|
+
// beta ever sent this session back into this value, to avoid
|
|
2942
|
+
// server-side cache-key churn from a beta flipping mid-session.
|
|
2943
|
+
// It was INERT on both live paths: `buildRequestHeaders`
|
|
2944
|
+
// recomputes its own `mergedBetas` from the incoming header, and
|
|
2945
|
+
// the adapter has the package compose the list from
|
|
2946
|
+
// `customBetas`. Neither ever read the latched value, so the only
|
|
2947
|
+
// thing it reached was the `task-budgets-2026-03-13` check in
|
|
2948
|
+
// `transformRequestBody` below.
|
|
2949
|
+
//
|
|
2950
|
+
// The two evictions the latch carried are not lost: both act on
|
|
2951
|
+
// `_sessionFilteredCustomBetas` above, BEFORE the header is
|
|
2952
|
+
// composed — `customBetasStripped` empties the custom set, and
|
|
2953
|
+
// `sessionRejectedBetas` filters the individual betas the API
|
|
2954
|
+
// rejected. That is the mechanism that actually reaches the wire.
|
|
2955
|
+
//
|
|
2956
|
+
// PHASE 4.1 — THE LEGACY BETA FORGE IS GONE FROM THIS PATH. The
|
|
2957
|
+
// whole ~12-param `buildAnthropicBetaHeader` call used to live
|
|
2958
|
+
// here for ONE consumer: `transformRequestBody` tested the
|
|
2959
|
+
// resulting header string for `task-budgets-2026-03-13` to decide
|
|
2960
|
+
// whether to preserve or inject `output_config`
|
|
2961
|
+
// (lib/mimicry/request-body.mjs). Nothing else read it — the
|
|
2962
|
+
// outgoing `anthropic-beta` header is composed by the shared
|
|
2963
|
+
// package on every adapter turn and recomputed from scratch by
|
|
2964
|
+
// `buildRequestHeaders` on every legacy one.
|
|
2965
|
+
//
|
|
2966
|
+
// It is now a BOOLEAN, derived here from the same gates the
|
|
2967
|
+
// adapter path applies to a custom beta. This MIRRORS
|
|
2968
|
+
// `buildAdditionalBetas` in lib/mimicry/adapter-input.mjs — keep
|
|
2969
|
+
// the two in step:
|
|
2970
|
+
// (a) signature emulation off ⇒ the plugin forges no Claude Code
|
|
2971
|
+
// fingerprint at all, so no custom beta reaches the wire;
|
|
2972
|
+
// (b) `_sessionFilteredCustomBetas` is the same list the adapter
|
|
2973
|
+
// is handed as `customBetas` (already `customBetasStripped`-
|
|
2974
|
+
// emptied and `sessionRejectedBetas`-filtered above), and the
|
|
2975
|
+
// adapter shortcut-expands each entry through
|
|
2976
|
+
// `BETA_SHORTCUTS` — `resolveBetaShortcut` is that expansion;
|
|
2977
|
+
// (c) `CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS` drops every member
|
|
2978
|
+
// of `EXPERIMENTAL_BETA_FLAGS` from the composed list
|
|
2979
|
+
// (adapter-input.mjs:490), and `task-budgets-2026-03-13` is a
|
|
2980
|
+
// member (lib/betas.mjs) — so the flag suppresses it.
|
|
2981
|
+
// There is no pathname gate to mirror: `transformRequestBody` only
|
|
2982
|
+
// runs on the adapter path, i.e. a messages endpoint.
|
|
2983
|
+
const _taskBudgetsActive =
|
|
2984
|
+
getSignatureEmulationEnabled() &&
|
|
2985
|
+
!isTruthyEnv(process.env.CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS) &&
|
|
2986
|
+
_sessionFilteredCustomBetas.some((b) => resolveBetaShortcut(b) === "task-budgets-2026-03-13");
|
|
2993
2987
|
|
|
2994
2988
|
// Cache TTL session latching: latch the cache policy at session start
|
|
2995
2989
|
// so mid-session toggles don't bust the server-side prompt cache.
|
|
@@ -3003,81 +2997,98 @@ export async function AnthropicAuthPlugin({ client }) {
|
|
|
3003
2997
|
config.cache_policy || { ttl: "1h", ttl_supported: true };
|
|
3004
2998
|
|
|
3005
2999
|
// Single decision point for the whole request: does this turn go
|
|
3006
|
-
// through the shared wire package
|
|
3007
|
-
// buildRequestHeaders path?
|
|
3000
|
+
// through the shared wire package?
|
|
3008
3001
|
//
|
|
3009
|
-
//
|
|
3010
|
-
//
|
|
3011
|
-
//
|
|
3012
|
-
//
|
|
3013
|
-
//
|
|
3014
|
-
//
|
|
3002
|
+
// PHASE 2.2 — THE ADAPTER IS UNCONDITIONAL. Two conditions, and
|
|
3003
|
+
// neither of them is about the body any more:
|
|
3004
|
+
// 1. signature emulation on — with it off the plugin does not
|
|
3005
|
+
// forge a Claude Code fingerprint at all, so there is
|
|
3006
|
+
// nothing for the package to build;
|
|
3007
|
+
// 2. a messages endpoint the package has a surface for:
|
|
3008
|
+
// `buildClaudeCodeRequest` for /v1/messages and
|
|
3015
3009
|
// `buildClaudeCodeCountTokensRequest` for
|
|
3016
3010
|
// /v1/messages/count_tokens, each pinning its OWN endpoint.
|
|
3017
|
-
// The endpoint pin is inert either way — `built.url` is never
|
|
3018
|
-
// adopted (see the consumption comment below), so the URL on
|
|
3019
|
-
// the wire stays the one transformRequestUrl produced and a
|
|
3020
|
-
// custom ANTHROPIC_BASE_URL survives.
|
|
3021
3011
|
//
|
|
3022
3012
|
// The former first-party check is now structural: the plugin only
|
|
3023
3013
|
// speaks to first-party Anthropic, so it is always satisfied.
|
|
3024
|
-
//
|
|
3025
|
-
//
|
|
3026
|
-
//
|
|
3027
|
-
//
|
|
3028
|
-
//
|
|
3029
|
-
//
|
|
3014
|
+
//
|
|
3015
|
+
// THE BODY IS NO LONGER A ROUTING INPUT. It used to be: a
|
|
3016
|
+
// bodiless or unparsable body silently fell back to the legacy
|
|
3017
|
+
// forge, which put a REQUEST WITH A DIFFERENT FINGERPRINT on the
|
|
3018
|
+
// wire mid-session. That dual path is what this phase removes.
|
|
3019
|
+
// A body the package cannot consume is now a hard error
|
|
3020
|
+
// (`assertAdapterBodyUsable`), never a quiet downgrade.
|
|
3021
|
+
// Endpoints outside the set above still pass through untouched.
|
|
3030
3022
|
const _adapterPathname = requestUrl?.pathname;
|
|
3031
|
-
const _isCountTokens =
|
|
3032
|
-
|
|
3023
|
+
const _isCountTokens = ADAPTER_COUNT_TOKENS_PATHNAMES.has(_adapterPathname);
|
|
3024
|
+
// Read ONCE per attempt. The config is runtime-mutable
|
|
3025
|
+
// (`/anthropic set`), and a flip between the routing decision,
|
|
3026
|
+
// the body transform and the header construction would emit a
|
|
3027
|
+
// half-emulated request.
|
|
3028
|
+
const _emulationEnabled = getSignatureEmulationEnabled();
|
|
3033
3029
|
const _useAdapter =
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3030
|
+
_emulationEnabled && (ADAPTER_MESSAGES_PATHNAMES.has(_adapterPathname) || _isCountTokens);
|
|
3031
|
+
if (_useAdapter) {
|
|
3032
|
+
// Validated against the INCOMING body, before transformRequestBody
|
|
3033
|
+
// gets a chance to normalize it: the diagnostic has to name what
|
|
3034
|
+
// the host actually sent. `_parsedBodyOnce` is not usable as the
|
|
3035
|
+
// signal — the retry paths below null it out after rewriting a
|
|
3036
|
+
// perfectly valid body.
|
|
3037
|
+
assertAdapterBodyUsable(requestInit.body, _adapterPathname);
|
|
3038
|
+
}
|
|
3038
3039
|
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3040
|
+
// PHASE 2.2 — EMULATION OFF DOES NOT TRANSFORM THE BODY AT ALL.
|
|
3041
|
+
// `transformRequestBody` used to run on every request and apply
|
|
3042
|
+
// its non-gated structural normalizations (output cap, thinking,
|
|
3043
|
+
// effort -> output_config, system sanitize/compact) even with
|
|
3044
|
+
// emulation off. That is policy the host never asked for. The
|
|
3045
|
+
// only exceptions are the fields the first-party API rejects
|
|
3046
|
+
// outright (`betas`, the stainless-helper markers) — see
|
|
3047
|
+
// stripNonApiBodyFields.
|
|
3048
|
+
const body = !_emulationEnabled
|
|
3049
|
+
? stripNonApiBodyFields(requestInit.body)
|
|
3050
|
+
: transformRequestBody(
|
|
3051
|
+
requestInit.body,
|
|
3052
|
+
{
|
|
3053
|
+
enabled: _emulationEnabled,
|
|
3054
|
+
claudeCliVersion,
|
|
3055
|
+
promptCompactionMode: getPromptCompactionMode(),
|
|
3056
|
+
useAdapter: _useAdapter,
|
|
3057
|
+
cachePolicy: effectiveCachePolicy,
|
|
3058
|
+
fastMode: config.fast_mode || false,
|
|
3059
|
+
// When the account's fast pool is cooling down, suppress
|
|
3060
|
+
// speed:"fast" so this turn runs at standard speed on the same
|
|
3061
|
+
// account instead of re-hitting the exhausted fast pool.
|
|
3062
|
+
fastRateLimited: accountManager.isFastRateLimited(account),
|
|
3063
|
+
strategy: getEffectiveStrategy(),
|
|
3064
|
+
toolDeferral: config.token_economy_strategies?.tool_deferral,
|
|
3065
|
+
toolDescriptionCompaction: config.token_economy_strategies?.tool_description_compaction,
|
|
3066
|
+
adaptiveToolSet: config.token_economy_strategies?.adaptive_tool_set,
|
|
3067
|
+
systemPromptTailing: config.token_economy_strategies?.system_prompt_tailing,
|
|
3068
|
+
systemPromptTailTurns: config.token_economy_strategies?.system_prompt_tail_turns,
|
|
3069
|
+
systemPromptTailMaxChars: config.token_economy_strategies?.system_prompt_tail_max_chars,
|
|
3070
|
+
},
|
|
3071
|
+
{
|
|
3072
|
+
persistentUserId: signatureUserId,
|
|
3073
|
+
sessionId: signatureSessionId,
|
|
3074
|
+
accountId: getAccountIdentifier(account),
|
|
3075
|
+
turns: sessionMetrics.turns,
|
|
3076
|
+
usedTools: sessionMetrics.usedTools,
|
|
3077
|
+
tokenEconomySession,
|
|
3078
|
+
requestRole: _requestRole,
|
|
3079
|
+
isSubagent: _isSubagent,
|
|
3080
|
+
// Adaptive cache-breakpoint placement: pass the per-boundary
|
|
3081
|
+
// stability snapshot so transformRequestBody can anchor the
|
|
3082
|
+
// cache_control marker on the most-stable boundary instead of
|
|
3083
|
+
// the (possibly thrashing) last tool. Only populated once the
|
|
3084
|
+
// detector is enabled and has a baseline.
|
|
3085
|
+
cacheBoundaryStability: config.cache_break_detection?.adaptive_breakpoint
|
|
3086
|
+
? cacheBreakState.boundaryStability
|
|
3087
|
+
: null,
|
|
3088
|
+
},
|
|
3089
|
+
_taskBudgetsActive,
|
|
3090
|
+
config,
|
|
3091
|
+
);
|
|
3081
3092
|
logTransformedSystemPrompt(body);
|
|
3082
3093
|
|
|
3083
3094
|
// Toast on first fast-mode application in session (reset on toggle)
|
|
@@ -3119,13 +3130,27 @@ export async function AnthropicAuthPlugin({ client }) {
|
|
|
3119
3130
|
// body that deliberately carries NO system, metadata or
|
|
3120
3131
|
// max_tokens. Legacy path: unchanged, byte for byte.
|
|
3121
3132
|
//
|
|
3122
|
-
// The URL
|
|
3123
|
-
//
|
|
3133
|
+
// The URL is assembled from BOTH sources: origin from the host,
|
|
3134
|
+
// path and query from the package. `built.url` is the pinned
|
|
3135
|
+
// endpoint the headers and body were composed for
|
|
3124
3136
|
// (`https://api.anthropic.com/v1/messages?beta=true`, or
|
|
3125
3137
|
// `.../v1/messages/count_tokens?beta=true` on the count surface),
|
|
3126
|
-
//
|
|
3127
|
-
//
|
|
3128
|
-
//
|
|
3138
|
+
// so the package stays the owner of the envelope — canonical path
|
|
3139
|
+
// plus `?beta=true` — and cannot drift from the body it built.
|
|
3140
|
+
// The ORIGIN, though, belongs to whoever the host addressed: a
|
|
3141
|
+
// custom provider baseURL (gateway, LiteLLM, corporate proxy)
|
|
3142
|
+
// arrives on `requestUrl`, and so does OPENCODE_MITM_BASE_URL,
|
|
3143
|
+
// which transformRequestUrl already applied. Taking the package's
|
|
3144
|
+
// origin as well would silently redirect those deployments to
|
|
3145
|
+
// api.anthropic.com.
|
|
3146
|
+
//
|
|
3147
|
+
// No gateway path PREFIX can be lost this way: the `_useAdapter`
|
|
3148
|
+
// gate only admits pathnames in {/v1/messages, /messages,
|
|
3149
|
+
// /v1/messages/count_tokens, /messages/count_tokens}, so a
|
|
3150
|
+
// prefixed endpoint never reaches the adapter in the first place.
|
|
3151
|
+
//
|
|
3152
|
+
// transformRequestUrl still owns the URL outright on the legacy /
|
|
3153
|
+
// emulation-off path.
|
|
3129
3154
|
const _adapterSignature = {
|
|
3130
3155
|
enabled: getSignatureEmulationEnabled(),
|
|
3131
3156
|
claudeCliVersion,
|
|
@@ -3136,6 +3161,7 @@ export async function AnthropicAuthPlugin({ client }) {
|
|
|
3136
3161
|
|
|
3137
3162
|
let requestHeaders;
|
|
3138
3163
|
let adapterBody;
|
|
3164
|
+
let adapterRequestInput;
|
|
3139
3165
|
let legacyStrippedBody;
|
|
3140
3166
|
if (_useAdapter) {
|
|
3141
3167
|
const _adapterResult = buildAdapterTransport({
|
|
@@ -3181,14 +3207,61 @@ export async function AnthropicAuthPlugin({ client }) {
|
|
|
3181
3207
|
: await buildWireCompatibleRequest(body, _adapterResult.transport);
|
|
3182
3208
|
requestHeaders = built.headers;
|
|
3183
3209
|
adapterBody = built.body;
|
|
3210
|
+
try {
|
|
3211
|
+
// Origin from the HOST, path+query from the PACKAGE.
|
|
3212
|
+
// `requestUrl` already carries the provider's configured
|
|
3213
|
+
// baseURL and already had OPENCODE_MITM_BASE_URL applied by
|
|
3214
|
+
// transformRequestUrl, so copying its origin is what keeps a
|
|
3215
|
+
// gateway / LiteLLM / proxy deployment reachable. With no
|
|
3216
|
+
// usable `requestUrl` the package's own origin stands.
|
|
3217
|
+
const _adapterUrl = new URL(built.url);
|
|
3218
|
+
if (requestUrl) {
|
|
3219
|
+
_adapterUrl.protocol = requestUrl.protocol;
|
|
3220
|
+
_adapterUrl.hostname = requestUrl.hostname;
|
|
3221
|
+
_adapterUrl.port = requestUrl.port;
|
|
3222
|
+
}
|
|
3223
|
+
adapterRequestInput =
|
|
3224
|
+
input instanceof Request ? new Request(_adapterUrl.toString(), input) : _adapterUrl;
|
|
3225
|
+
} catch (error) {
|
|
3226
|
+
// Unparsable package URL — fall back to the transformed URL.
|
|
3227
|
+
debugLog(`adapter url adoption skipped (${error.message}); keeping the transformed url`);
|
|
3228
|
+
}
|
|
3184
3229
|
} else {
|
|
3185
|
-
|
|
3230
|
+
// Unreachable by construction, and it must STAY unreachable:
|
|
3231
|
+
// `buildAdapterTransport` declines on exactly two conditions,
|
|
3232
|
+
// a non-anthropic provider and signature emulation off, and
|
|
3233
|
+
// the call site above pins `provider: "anthropic"` while
|
|
3234
|
+
// `_useAdapter` already required emulation on. Falling back to
|
|
3235
|
+
// the legacy forge here would silently restore the dual path
|
|
3236
|
+
// Phase 2.2 removed, so a decline is an error instead.
|
|
3237
|
+
throw new Error(
|
|
3238
|
+
`opencode-anthropic-fix: the Claude Code wire adapter declined a request it must handle ` +
|
|
3239
|
+
`(${_adapterResult.reason}) on ${_adapterPathname}. This is a plugin bug — ` +
|
|
3240
|
+
`the legacy request path is no longer reachable with signature emulation on.`,
|
|
3241
|
+
);
|
|
3186
3242
|
}
|
|
3187
3243
|
}
|
|
3188
3244
|
|
|
3245
|
+
if (!requestHeaders && !_emulationEnabled) {
|
|
3246
|
+
// PHASE 2.2 — EMULATION OFF IS PURE PASSTHROUGH PLUS THE AUTH
|
|
3247
|
+
// ENVELOPE. No mimicry function composes these headers: the
|
|
3248
|
+
// host's set goes out as it arrived, minus the two credentials
|
|
3249
|
+
// that must not travel with our bearer, plus `authorization`
|
|
3250
|
+
// and an ADDITIVE `oauth-2025-04-20`. The forged claude-cli
|
|
3251
|
+
// user-agent and the substituted beta list — the two mimicry
|
|
3252
|
+
// vectors that used to survive with emulation off — are gone.
|
|
3253
|
+
// See lib/passthrough-headers.mjs for why the envelope is not
|
|
3254
|
+
// mimicry.
|
|
3255
|
+
requestHeaders = buildPassthroughHeaders(input, requestInit, accessToken);
|
|
3256
|
+
}
|
|
3257
|
+
|
|
3189
3258
|
if (!requestHeaders) {
|
|
3190
|
-
//
|
|
3191
|
-
//
|
|
3259
|
+
// Emulation ON, on an endpoint the package has no surface for
|
|
3260
|
+
// (files, models, a gateway-prefixed route). Unchanged, and the
|
|
3261
|
+
// only caller of the legacy forge left: on a messages or
|
|
3262
|
+
// count_tokens turn the adapter above always produced headers,
|
|
3263
|
+
// which `test/conformance/adapter-unconditional.test.mjs`
|
|
3264
|
+
// observes directly.
|
|
3192
3265
|
requestHeaders = buildRequestHeaders(
|
|
3193
3266
|
input,
|
|
3194
3267
|
requestInit,
|
|
@@ -3273,7 +3346,7 @@ export async function AnthropicAuthPlugin({ client }) {
|
|
|
3273
3346
|
debugLog("failed to write outgoing request headers to debug-headers.log", e);
|
|
3274
3347
|
}
|
|
3275
3348
|
}
|
|
3276
|
-
response = await fetch(requestInput, {
|
|
3349
|
+
response = await fetch(adapterRequestInput ?? requestInput, {
|
|
3277
3350
|
...requestInit,
|
|
3278
3351
|
body: finalBody,
|
|
3279
3352
|
headers: requestHeaders,
|
|
@@ -4457,7 +4530,7 @@ export async function AnthropicAuthPlugin({ client }) {
|
|
|
4457
4530
|
|
|
4458
4531
|
const getAccessToken = async () => {
|
|
4459
4532
|
let tok = account.access;
|
|
4460
|
-
if (!tok ||
|
|
4533
|
+
if (!tok || isTokenExpired(account.expires)) {
|
|
4461
4534
|
tok = await refreshAccountTokenSingleFlight(account);
|
|
4462
4535
|
}
|
|
4463
4536
|
if (!tok) throw new Error("no access token available for Haiku call");
|
|
@@ -5172,7 +5245,7 @@ function resolveAdaptiveContext(bodyString, model, adaptiveConfig, parsedBody) {
|
|
|
5172
5245
|
}
|
|
5173
5246
|
|
|
5174
5247
|
// Model must be eligible for 1M context at all (includes Opus 4.6)
|
|
5175
|
-
if (!
|
|
5248
|
+
if (!isEligibleFor1MContextWire(model)) {
|
|
5176
5249
|
return false;
|
|
5177
5250
|
}
|
|
5178
5251
|
|
|
@@ -5423,6 +5496,12 @@ function writeCacheStatsFile(usage, model, hitRate) {
|
|
|
5423
5496
|
|
|
5424
5497
|
// --- Phase 5: Minimal telemetry emulation ("Silent Observer") ---
|
|
5425
5498
|
|
|
5499
|
+
// Host telemetry payload only (ClaudeCodeInternalEvent env block) — never sent
|
|
5500
|
+
// on /v1/messages, so it is not part of the wire-compat surface.
|
|
5501
|
+
// Real build markers extracted from the 2.1.195 native binary (Bun-embedded JS):
|
|
5502
|
+
// `BUILD_TIME:"2026-06-26T01:00:56Z"`.
|
|
5503
|
+
const CLAUDE_CODE_BUILD_TIME = "2026-06-26T01:00:56Z";
|
|
5504
|
+
|
|
5426
5505
|
class TelemetryEmitter {
|
|
5427
5506
|
#enabled = false;
|
|
5428
5507
|
#sent = false;
|
|
@@ -5828,9 +5907,10 @@ function getAccountIdentifier(account) {
|
|
|
5828
5907
|
* @param {{ use1MContext?: boolean }} [adaptiveOverride] - When set, overrides the static hasOneMillionContext() check.
|
|
5829
5908
|
* @param {boolean} [fastModeActive] - When true, emits FAST_MODE_BETA_FLAG (fast-mode-2026-02-01).
|
|
5830
5909
|
* Must be derived structurally from the already-transformed outgoing body (body.includes('"speed":"fast"')).
|
|
5831
|
-
* Only passed from the buildRequestHeaders call site (after body transform)
|
|
5832
|
-
*
|
|
5833
|
-
*
|
|
5910
|
+
* Only passed from the buildRequestHeaders call site (after body transform), which is now the
|
|
5911
|
+
* ONLY call site: the pre-transform `computedBetaHeader` caller was deleted in Phase 4.1. Its
|
|
5912
|
+
* sole consumer, the `task-budgets-2026-03-13` check in transformRequestBody, takes a derived
|
|
5913
|
+
* boolean instead (see `_taskBudgetsActive` in the interceptor).
|
|
5834
5914
|
* @returns {string}
|
|
5835
5915
|
*/
|
|
5836
5916
|
// Mirrors CC's Kw(model) effort eligibility: returns false for claude-3-* and the
|
|
@@ -5865,6 +5945,9 @@ function getAccountIdentifier(account) {
|
|
|
5865
5945
|
* @returns {string}
|
|
5866
5946
|
*/
|
|
5867
5947
|
|
|
5948
|
+
// Host update polling only (npm registry lookup), not mimicry.
|
|
5949
|
+
const CLAUDE_CODE_NPM_LATEST_URL = "https://registry.npmjs.org/@anthropic-ai/claude-code/latest";
|
|
5950
|
+
|
|
5868
5951
|
/**
|
|
5869
5952
|
* Resolve latest claude-code package version from npm registry.
|
|
5870
5953
|
* Returns null on timeout/network/parse failures.
|
|
@@ -5915,13 +5998,20 @@ async function fetchLatestClaudeCodeVersion(timeoutMs = 1200) {
|
|
|
5915
5998
|
*/
|
|
5916
5999
|
|
|
5917
6000
|
/**
|
|
5918
|
-
* Transform the request URL: add ?beta=true to /v1/messages and
|
|
5919
|
-
*
|
|
6001
|
+
* Transform the request URL: add ?beta=true to /v1/messages and
|
|
6002
|
+
* /v1/messages/count_tokens, normalizing a `/messages` path to `/v1/messages`
|
|
6003
|
+
* on the way. Preserves behaviors F1-F3.
|
|
6004
|
+
*
|
|
6005
|
+
* Both of those are CLAUDE CODE SHAPE, so both are gated on `emulateSignature`.
|
|
6006
|
+
* The `OPENCODE_MITM_BASE_URL` rewrite is not, and applies either way.
|
|
5920
6007
|
*
|
|
5921
6008
|
* @param {any} input
|
|
6009
|
+
* @param {boolean} [emulateSignature] Whether signature emulation is on for this
|
|
6010
|
+
* request. Read at the call site rather than here: the config is
|
|
6011
|
+
* runtime-mutable and this function lives outside the plugin closure.
|
|
5922
6012
|
* @returns {{requestInput: any, requestUrl: URL | null}}
|
|
5923
6013
|
*/
|
|
5924
|
-
function transformRequestUrl(input) {
|
|
6014
|
+
function transformRequestUrl(input, emulateSignature = true) {
|
|
5925
6015
|
let requestInput = input;
|
|
5926
6016
|
let requestUrl = null;
|
|
5927
6017
|
try {
|
|
@@ -5934,7 +6024,14 @@ function transformRequestUrl(input) {
|
|
|
5934
6024
|
requestUrl = null;
|
|
5935
6025
|
}
|
|
5936
6026
|
|
|
5937
|
-
|
|
6027
|
+
// PHASE 2.2 (QA finding 1) — THE URL REWRITE IS MIMICRY, SO IT IS GATED.
|
|
6028
|
+
// `?beta=true` is the endpoint the genuine Claude Code client pins, and
|
|
6029
|
+
// normalizing `/messages` to `/v1/messages` is the same kind of client-shape
|
|
6030
|
+
// assumption. With signature emulation off the plugin forges nothing, and
|
|
6031
|
+
// that has to include the URL: the host's URL goes out exactly as the host
|
|
6032
|
+
// wrote it. The MITM rewrite below stays unconditional — it is a debug and
|
|
6033
|
+
// conformance knob the operator asked for, not a disguise.
|
|
6034
|
+
if (emulateSignature && requestUrl && !requestUrl.searchParams.has("beta")) {
|
|
5938
6035
|
const p = requestUrl.pathname;
|
|
5939
6036
|
// SDK may send to /messages (base URL includes /v1) or /v1/messages (base URL is root)
|
|
5940
6037
|
const isMessages =
|
|
@@ -6076,7 +6173,7 @@ function applyDiskAuthIfFresher(account, diskAuth, options = {}) {
|
|
|
6076
6173
|
const diskTokenUpdatedAt = diskAuth.tokenUpdatedAt || 0;
|
|
6077
6174
|
const memTokenUpdatedAt = account.tokenUpdatedAt || 0;
|
|
6078
6175
|
const diskHasDifferentAuth = diskAuth.refreshToken !== account.refreshToken || diskAuth.access !== account.access;
|
|
6079
|
-
const memAuthExpired =
|
|
6176
|
+
const memAuthExpired = isTokenExpired(account.expires);
|
|
6080
6177
|
const allowExpiredFallback = options.allowExpiredFallback === true;
|
|
6081
6178
|
if (diskTokenUpdatedAt <= memTokenUpdatedAt && !(allowExpiredFallback && diskHasDifferentAuth && memAuthExpired)) {
|
|
6082
6179
|
return false;
|
|
@@ -6112,7 +6209,7 @@ async function refreshAccountToken(account, client, _source = "foreground", { on
|
|
|
6112
6209
|
// Accept CC credential if:
|
|
6113
6210
|
// - expiresAt is in the future (normal case), OR
|
|
6114
6211
|
// - expiresAt is 0/missing (CC didn't provide expiry — trust the token, let API 401 if stale)
|
|
6115
|
-
if (match && (match.expiresAt === 0 || match.expiresAt
|
|
6212
|
+
if (match && (match.expiresAt === 0 || isTokenUsable(match.expiresAt))) {
|
|
6116
6213
|
account.access = match.accessToken;
|
|
6117
6214
|
account.expires = match.expiresAt || Date.now() + 3600_000; // default 1h if unknown
|
|
6118
6215
|
markTokenStateUpdated(account);
|
|
@@ -6148,7 +6245,7 @@ async function refreshAccountToken(account, client, _source = "foreground", { on
|
|
|
6148
6245
|
if (!lock.acquired) {
|
|
6149
6246
|
const diskAuth = await readDiskAccountAuth(account.id);
|
|
6150
6247
|
const adopted = applyDiskAuthIfFresher(account, diskAuth, { allowExpiredFallback: true });
|
|
6151
|
-
if (adopted && account.access && account.expires
|
|
6248
|
+
if (adopted && account.access && isTokenUsable(account.expires)) {
|
|
6152
6249
|
return account.access;
|
|
6153
6250
|
}
|
|
6154
6251
|
throw new Error("Refresh lock busy");
|
|
@@ -6159,14 +6256,21 @@ async function refreshAccountToken(account, client, _source = "foreground", { on
|
|
|
6159
6256
|
const adopted = applyDiskAuthIfFresher(account, diskAuthBeforeRefresh);
|
|
6160
6257
|
// Apply fresher disk tokens for both foreground and idle paths — prevents an
|
|
6161
6258
|
// unnecessary HTTP refresh when another process already rotated the token.
|
|
6162
|
-
if (adopted && account.access && account.expires
|
|
6259
|
+
if (adopted && account.access && isTokenUsable(account.expires)) {
|
|
6163
6260
|
return account.access;
|
|
6164
6261
|
}
|
|
6165
6262
|
|
|
6166
|
-
|
|
6263
|
+
// `clientId` is undefined for every normal account, which means "use the
|
|
6264
|
+
// default". Only a CLAUDE_CODE_OAUTH_CLIENT_ID-seeded env account carries one
|
|
6265
|
+
// (contract §5.2, divergence D10).
|
|
6266
|
+
const json = await refreshToken(account.refreshToken, {
|
|
6267
|
+
signal: AbortSignal.timeout(15_000),
|
|
6268
|
+
clientId: account.clientId,
|
|
6269
|
+
});
|
|
6167
6270
|
|
|
6168
6271
|
account.access = json.access_token;
|
|
6169
6272
|
account.expires = Date.now() + json.expires_in * 1000;
|
|
6273
|
+
warnOnEphemeralTokenRotation(account, json.refresh_token);
|
|
6170
6274
|
if (json.refresh_token) {
|
|
6171
6275
|
account.refreshToken = json.refresh_token;
|
|
6172
6276
|
}
|
|
@@ -6294,13 +6398,6 @@ AnthropicAuthPlugin.__testing__ = {
|
|
|
6294
6398
|
maybeApplySessionToolResultDedupe,
|
|
6295
6399
|
// exposed for experimental.session.summarize integration tests
|
|
6296
6400
|
runHaikuSessionSummarize,
|
|
6297
|
-
get cachedCCPrompt() {
|
|
6298
|
-
return getCachedCCPrompt();
|
|
6299
|
-
},
|
|
6300
|
-
resetCachedCCPrompt() {
|
|
6301
|
-
resetCachedCCPrompt();
|
|
6302
|
-
},
|
|
6303
|
-
SUBAGENT_CC_ANCHOR,
|
|
6304
6401
|
CLAUDE_CODE_IDENTITY_STRING,
|
|
6305
6402
|
/** Test-only: drive the session turn counter so code paths gated on
|
|
6306
6403
|
* `sessionMetrics.turns >= N` can be exercised without a real SSE stream. */
|