opencode-anthropic-fix 0.5.0 → 1.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/index.mjs +271 -184
- package/lib/betas.mjs +131 -0
- package/lib/diagnose.mjs +4 -2
- package/lib/mimicry/adapter-input.mjs +131 -29
- 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 +148 -96
- package/lib/oauth.mjs +3 -2
- package/lib/passthrough-headers.mjs +152 -0
- package/package.json +1 -1
- package/lib/mimicry/models.mjs +0 -146
- package/lib/request-headers.mjs +0 -303
package/index.mjs
CHANGED
|
@@ -10,12 +10,7 @@ import {
|
|
|
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";
|
|
@@ -58,7 +53,6 @@ import {
|
|
|
58
53
|
} from "./lib/retry/overload-loop.mjs";
|
|
59
54
|
import {
|
|
60
55
|
buildRequestHeaders,
|
|
61
|
-
buildAnthropicBetaHeader,
|
|
62
56
|
parseRequestBodyMetadata,
|
|
63
57
|
extractFileIds,
|
|
64
58
|
stripStainlessHelperMarkers,
|
|
@@ -68,25 +62,31 @@ import {
|
|
|
68
62
|
compactSystemText,
|
|
69
63
|
compactToolDescription,
|
|
70
64
|
dedupeSystemBlocks,
|
|
71
|
-
getCachedCCPrompt,
|
|
72
65
|
isTitleGeneratorSystemBlocks,
|
|
73
66
|
normalizeSystemTextBlocks,
|
|
74
|
-
resetCachedCCPrompt,
|
|
75
67
|
sanitizeSystemText,
|
|
76
68
|
CLAUDE_CODE_IDENTITY_STRING,
|
|
77
|
-
SUBAGENT_CC_ANCHOR,
|
|
78
69
|
} 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
70
|
import {
|
|
71
|
+
ADAPTER_COUNT_TOKENS_PATHNAMES,
|
|
72
|
+
ADAPTER_MESSAGES_PATHNAMES,
|
|
73
|
+
assertAdapterBodyUsable,
|
|
74
|
+
buildAdapterTransport,
|
|
75
|
+
resolveAdapterEnv,
|
|
76
|
+
} from "./lib/mimicry/adapter-input.mjs";
|
|
77
|
+
import { buildPassthroughHeaders, stripNonApiBodyFields } from "./lib/passthrough-headers.mjs";
|
|
78
|
+
import {
|
|
79
|
+
buildWireCompatibleRequest,
|
|
80
|
+
buildWireCompatibleCountTokensRequest,
|
|
81
|
+
WIRE_PROFILE,
|
|
82
82
|
hasOneMillionContext,
|
|
83
|
-
|
|
83
|
+
isEligibleFor1MContextWire,
|
|
84
84
|
isOpus46Model,
|
|
85
85
|
isOpus47Model,
|
|
86
86
|
isOpus48Model,
|
|
87
|
-
} from "./lib/mimicry/
|
|
87
|
+
} from "./lib/mimicry/wire-compat.mjs";
|
|
88
88
|
|
|
89
|
-
export { isFable5Model, isMythos5Model, isAdaptiveThinkingModel } from "./lib/mimicry/
|
|
89
|
+
export { isFable5Model, isMythos5Model, isAdaptiveThinkingModel } from "./lib/mimicry/wire-compat.mjs";
|
|
90
90
|
|
|
91
91
|
// Max times a single logical request may fall back from fast->standard speed on
|
|
92
92
|
// the same account before giving up the fast attempt entirely. 1 is enough: one
|
|
@@ -298,18 +298,6 @@ export async function AnthropicAuthPlugin({ client }) {
|
|
|
298
298
|
let willowLastSuggestionTime = 0;
|
|
299
299
|
let _lastOAuthPruneTime = 0; // QA fix L-oauthPrune: throttle for periodic prune
|
|
300
300
|
|
|
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
301
|
// F4: Session-level latch for rejected custom betas.
|
|
314
302
|
// When a custom beta triggers a 400/anthropic-beta or 413-with-signal rejection,
|
|
315
303
|
// its canonical name is stored here so subsequent requests within
|
|
@@ -1240,8 +1228,6 @@ export async function AnthropicAuthPlugin({ client }) {
|
|
|
1240
1228
|
te.token_efficient_tools = enabled;
|
|
1241
1229
|
saveConfig({ token_economy: te });
|
|
1242
1230
|
config.token_economy = te;
|
|
1243
|
-
// Invalidate latched betas so the change takes effect next request
|
|
1244
|
-
betaLatchState.dirty = true;
|
|
1245
1231
|
},
|
|
1246
1232
|
"redact-thinking": () => {
|
|
1247
1233
|
const enabled = value === "on" || value === "true" || value === "1";
|
|
@@ -1251,7 +1237,6 @@ export async function AnthropicAuthPlugin({ client }) {
|
|
|
1251
1237
|
te.redact_thinking = enabled;
|
|
1252
1238
|
saveConfig({ token_economy: te });
|
|
1253
1239
|
config.token_economy = te;
|
|
1254
|
-
betaLatchState.dirty = true;
|
|
1255
1240
|
},
|
|
1256
1241
|
"tool-deferral": () => {
|
|
1257
1242
|
const enabled = value === "on" || value === "true" || value === "1";
|
|
@@ -2215,7 +2200,9 @@ export async function AnthropicAuthPlugin({ client }) {
|
|
|
2215
2200
|
return config.account_selection_strategy;
|
|
2216
2201
|
}
|
|
2217
2202
|
|
|
2218
|
-
|
|
2203
|
+
// Baseline CLI version, read from the wire package's profile through the
|
|
2204
|
+
// adapter seam. `fetchLatestClaudeCodeVersion` may raise it at runtime.
|
|
2205
|
+
let claudeCliVersion = WIRE_PROFILE.cliVersion;
|
|
2219
2206
|
const signatureSessionId = randomUUID();
|
|
2220
2207
|
const signatureUserId = getOrCreateDeviceId();
|
|
2221
2208
|
if (shouldFetchClaudeCodeVersion) {
|
|
@@ -2577,7 +2564,26 @@ export async function AnthropicAuthPlugin({ client }) {
|
|
|
2577
2564
|
|
|
2578
2565
|
// Transform URL once (shared across retries)
|
|
2579
2566
|
const requestInit = init ?? {};
|
|
2580
|
-
|
|
2567
|
+
// The emulation flag decides whether the URL may be reshaped at
|
|
2568
|
+
// all (`?beta=true`, `/messages` -> `/v1/messages`). Read here,
|
|
2569
|
+
// live, because `/anthropic set` can flip it between requests.
|
|
2570
|
+
const { requestInput, requestUrl } = transformRequestUrl(input, getSignatureEmulationEnabled());
|
|
2571
|
+
|
|
2572
|
+
// A host may hand us a `Request` carrying the body, with the init
|
|
2573
|
+
// empty — `fetchFn(new Request(url, {body}), {})`. Every body-aware
|
|
2574
|
+
// stage downstream (`_parsedBodyOnce`, `transformRequestBody`, the
|
|
2575
|
+
// adapter, the retry-loop rewrites) reads `requestInit.body` and
|
|
2576
|
+
// would silently see nothing, so the body is lifted onto the init
|
|
2577
|
+
// once, here. `clone()` keeps the original Request readable, which
|
|
2578
|
+
// matters because `requestInput` is still what gets sent when no
|
|
2579
|
+
// adapter URL is adopted.
|
|
2580
|
+
if (requestInit.body == null && input instanceof Request && input.body != null) {
|
|
2581
|
+
try {
|
|
2582
|
+
requestInit.body = await input.clone().text();
|
|
2583
|
+
} catch (error) {
|
|
2584
|
+
debugLog(`could not read the body off the host Request: ${error.message}`);
|
|
2585
|
+
}
|
|
2586
|
+
}
|
|
2581
2587
|
const requestMethod = String(
|
|
2582
2588
|
requestInit.method || (requestInput instanceof Request ? requestInput.method : "POST"),
|
|
2583
2589
|
).toUpperCase();
|
|
@@ -2928,68 +2934,53 @@ export async function AnthropicAuthPlugin({ client }) {
|
|
|
2928
2934
|
}
|
|
2929
2935
|
return false;
|
|
2930
2936
|
});
|
|
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
|
-
}
|
|
2937
|
+
// PHASE 2.2.3 — THE BETA LATCH IS GONE. It used to merge every
|
|
2938
|
+
// beta ever sent this session back into this value, to avoid
|
|
2939
|
+
// server-side cache-key churn from a beta flipping mid-session.
|
|
2940
|
+
// It was INERT on both live paths: `buildRequestHeaders`
|
|
2941
|
+
// recomputes its own `mergedBetas` from the incoming header, and
|
|
2942
|
+
// the adapter has the package compose the list from
|
|
2943
|
+
// `customBetas`. Neither ever read the latched value, so the only
|
|
2944
|
+
// thing it reached was the `task-budgets-2026-03-13` check in
|
|
2945
|
+
// `transformRequestBody` below.
|
|
2946
|
+
//
|
|
2947
|
+
// The two evictions the latch carried are not lost: both act on
|
|
2948
|
+
// `_sessionFilteredCustomBetas` above, BEFORE the header is
|
|
2949
|
+
// composed — `customBetasStripped` empties the custom set, and
|
|
2950
|
+
// `sessionRejectedBetas` filters the individual betas the API
|
|
2951
|
+
// rejected. That is the mechanism that actually reaches the wire.
|
|
2952
|
+
//
|
|
2953
|
+
// PHASE 4.1 — THE LEGACY BETA FORGE IS GONE FROM THIS PATH. The
|
|
2954
|
+
// whole ~12-param `buildAnthropicBetaHeader` call used to live
|
|
2955
|
+
// here for ONE consumer: `transformRequestBody` tested the
|
|
2956
|
+
// resulting header string for `task-budgets-2026-03-13` to decide
|
|
2957
|
+
// whether to preserve or inject `output_config`
|
|
2958
|
+
// (lib/mimicry/request-body.mjs). Nothing else read it — the
|
|
2959
|
+
// outgoing `anthropic-beta` header is composed by the shared
|
|
2960
|
+
// package on every adapter turn and recomputed from scratch by
|
|
2961
|
+
// `buildRequestHeaders` on every legacy one.
|
|
2962
|
+
//
|
|
2963
|
+
// It is now a BOOLEAN, derived here from the same gates the
|
|
2964
|
+
// adapter path applies to a custom beta. This MIRRORS
|
|
2965
|
+
// `buildAdditionalBetas` in lib/mimicry/adapter-input.mjs — keep
|
|
2966
|
+
// the two in step:
|
|
2967
|
+
// (a) signature emulation off ⇒ the plugin forges no Claude Code
|
|
2968
|
+
// fingerprint at all, so no custom beta reaches the wire;
|
|
2969
|
+
// (b) `_sessionFilteredCustomBetas` is the same list the adapter
|
|
2970
|
+
// is handed as `customBetas` (already `customBetasStripped`-
|
|
2971
|
+
// emptied and `sessionRejectedBetas`-filtered above), and the
|
|
2972
|
+
// adapter shortcut-expands each entry through
|
|
2973
|
+
// `BETA_SHORTCUTS` — `resolveBetaShortcut` is that expansion;
|
|
2974
|
+
// (c) `CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS` drops every member
|
|
2975
|
+
// of `EXPERIMENTAL_BETA_FLAGS` from the composed list
|
|
2976
|
+
// (adapter-input.mjs:490), and `task-budgets-2026-03-13` is a
|
|
2977
|
+
// member (lib/betas.mjs) — so the flag suppresses it.
|
|
2978
|
+
// There is no pathname gate to mirror: `transformRequestBody` only
|
|
2979
|
+
// runs on the adapter path, i.e. a messages endpoint.
|
|
2980
|
+
const _taskBudgetsActive =
|
|
2981
|
+
getSignatureEmulationEnabled() &&
|
|
2982
|
+
!isTruthyEnv(process.env.CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS) &&
|
|
2983
|
+
_sessionFilteredCustomBetas.some((b) => resolveBetaShortcut(b) === "task-budgets-2026-03-13");
|
|
2993
2984
|
|
|
2994
2985
|
// Cache TTL session latching: latch the cache policy at session start
|
|
2995
2986
|
// so mid-session toggles don't bust the server-side prompt cache.
|
|
@@ -3003,81 +2994,98 @@ export async function AnthropicAuthPlugin({ client }) {
|
|
|
3003
2994
|
config.cache_policy || { ttl: "1h", ttl_supported: true };
|
|
3004
2995
|
|
|
3005
2996
|
// Single decision point for the whole request: does this turn go
|
|
3006
|
-
// through the shared wire package
|
|
3007
|
-
// buildRequestHeaders path?
|
|
2997
|
+
// through the shared wire package?
|
|
3008
2998
|
//
|
|
3009
|
-
//
|
|
3010
|
-
//
|
|
3011
|
-
//
|
|
3012
|
-
//
|
|
3013
|
-
//
|
|
3014
|
-
//
|
|
2999
|
+
// PHASE 2.2 — THE ADAPTER IS UNCONDITIONAL. Two conditions, and
|
|
3000
|
+
// neither of them is about the body any more:
|
|
3001
|
+
// 1. signature emulation on — with it off the plugin does not
|
|
3002
|
+
// forge a Claude Code fingerprint at all, so there is
|
|
3003
|
+
// nothing for the package to build;
|
|
3004
|
+
// 2. a messages endpoint the package has a surface for:
|
|
3005
|
+
// `buildClaudeCodeRequest` for /v1/messages and
|
|
3015
3006
|
// `buildClaudeCodeCountTokensRequest` for
|
|
3016
3007
|
// /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
3008
|
//
|
|
3022
3009
|
// The former first-party check is now structural: the plugin only
|
|
3023
3010
|
// speaks to first-party Anthropic, so it is always satisfied.
|
|
3024
|
-
//
|
|
3025
|
-
//
|
|
3026
|
-
//
|
|
3027
|
-
//
|
|
3028
|
-
//
|
|
3029
|
-
//
|
|
3011
|
+
//
|
|
3012
|
+
// THE BODY IS NO LONGER A ROUTING INPUT. It used to be: a
|
|
3013
|
+
// bodiless or unparsable body silently fell back to the legacy
|
|
3014
|
+
// forge, which put a REQUEST WITH A DIFFERENT FINGERPRINT on the
|
|
3015
|
+
// wire mid-session. That dual path is what this phase removes.
|
|
3016
|
+
// A body the package cannot consume is now a hard error
|
|
3017
|
+
// (`assertAdapterBodyUsable`), never a quiet downgrade.
|
|
3018
|
+
// Endpoints outside the set above still pass through untouched.
|
|
3030
3019
|
const _adapterPathname = requestUrl?.pathname;
|
|
3031
|
-
const _isCountTokens =
|
|
3032
|
-
|
|
3020
|
+
const _isCountTokens = ADAPTER_COUNT_TOKENS_PATHNAMES.has(_adapterPathname);
|
|
3021
|
+
// Read ONCE per attempt. The config is runtime-mutable
|
|
3022
|
+
// (`/anthropic set`), and a flip between the routing decision,
|
|
3023
|
+
// the body transform and the header construction would emit a
|
|
3024
|
+
// half-emulated request.
|
|
3025
|
+
const _emulationEnabled = getSignatureEmulationEnabled();
|
|
3033
3026
|
const _useAdapter =
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3027
|
+
_emulationEnabled && (ADAPTER_MESSAGES_PATHNAMES.has(_adapterPathname) || _isCountTokens);
|
|
3028
|
+
if (_useAdapter) {
|
|
3029
|
+
// Validated against the INCOMING body, before transformRequestBody
|
|
3030
|
+
// gets a chance to normalize it: the diagnostic has to name what
|
|
3031
|
+
// the host actually sent. `_parsedBodyOnce` is not usable as the
|
|
3032
|
+
// signal — the retry paths below null it out after rewriting a
|
|
3033
|
+
// perfectly valid body.
|
|
3034
|
+
assertAdapterBodyUsable(requestInit.body, _adapterPathname);
|
|
3035
|
+
}
|
|
3038
3036
|
|
|
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
|
-
|
|
3037
|
+
// PHASE 2.2 — EMULATION OFF DOES NOT TRANSFORM THE BODY AT ALL.
|
|
3038
|
+
// `transformRequestBody` used to run on every request and apply
|
|
3039
|
+
// its non-gated structural normalizations (output cap, thinking,
|
|
3040
|
+
// effort -> output_config, system sanitize/compact) even with
|
|
3041
|
+
// emulation off. That is policy the host never asked for. The
|
|
3042
|
+
// only exceptions are the fields the first-party API rejects
|
|
3043
|
+
// outright (`betas`, the stainless-helper markers) — see
|
|
3044
|
+
// stripNonApiBodyFields.
|
|
3045
|
+
const body = !_emulationEnabled
|
|
3046
|
+
? stripNonApiBodyFields(requestInit.body)
|
|
3047
|
+
: transformRequestBody(
|
|
3048
|
+
requestInit.body,
|
|
3049
|
+
{
|
|
3050
|
+
enabled: _emulationEnabled,
|
|
3051
|
+
claudeCliVersion,
|
|
3052
|
+
promptCompactionMode: getPromptCompactionMode(),
|
|
3053
|
+
useAdapter: _useAdapter,
|
|
3054
|
+
cachePolicy: effectiveCachePolicy,
|
|
3055
|
+
fastMode: config.fast_mode || false,
|
|
3056
|
+
// When the account's fast pool is cooling down, suppress
|
|
3057
|
+
// speed:"fast" so this turn runs at standard speed on the same
|
|
3058
|
+
// account instead of re-hitting the exhausted fast pool.
|
|
3059
|
+
fastRateLimited: accountManager.isFastRateLimited(account),
|
|
3060
|
+
strategy: getEffectiveStrategy(),
|
|
3061
|
+
toolDeferral: config.token_economy_strategies?.tool_deferral,
|
|
3062
|
+
toolDescriptionCompaction: config.token_economy_strategies?.tool_description_compaction,
|
|
3063
|
+
adaptiveToolSet: config.token_economy_strategies?.adaptive_tool_set,
|
|
3064
|
+
systemPromptTailing: config.token_economy_strategies?.system_prompt_tailing,
|
|
3065
|
+
systemPromptTailTurns: config.token_economy_strategies?.system_prompt_tail_turns,
|
|
3066
|
+
systemPromptTailMaxChars: config.token_economy_strategies?.system_prompt_tail_max_chars,
|
|
3067
|
+
},
|
|
3068
|
+
{
|
|
3069
|
+
persistentUserId: signatureUserId,
|
|
3070
|
+
sessionId: signatureSessionId,
|
|
3071
|
+
accountId: getAccountIdentifier(account),
|
|
3072
|
+
turns: sessionMetrics.turns,
|
|
3073
|
+
usedTools: sessionMetrics.usedTools,
|
|
3074
|
+
tokenEconomySession,
|
|
3075
|
+
requestRole: _requestRole,
|
|
3076
|
+
isSubagent: _isSubagent,
|
|
3077
|
+
// Adaptive cache-breakpoint placement: pass the per-boundary
|
|
3078
|
+
// stability snapshot so transformRequestBody can anchor the
|
|
3079
|
+
// cache_control marker on the most-stable boundary instead of
|
|
3080
|
+
// the (possibly thrashing) last tool. Only populated once the
|
|
3081
|
+
// detector is enabled and has a baseline.
|
|
3082
|
+
cacheBoundaryStability: config.cache_break_detection?.adaptive_breakpoint
|
|
3083
|
+
? cacheBreakState.boundaryStability
|
|
3084
|
+
: null,
|
|
3085
|
+
},
|
|
3086
|
+
_taskBudgetsActive,
|
|
3087
|
+
config,
|
|
3088
|
+
);
|
|
3081
3089
|
logTransformedSystemPrompt(body);
|
|
3082
3090
|
|
|
3083
3091
|
// Toast on first fast-mode application in session (reset on toggle)
|
|
@@ -3119,13 +3127,27 @@ export async function AnthropicAuthPlugin({ client }) {
|
|
|
3119
3127
|
// body that deliberately carries NO system, metadata or
|
|
3120
3128
|
// max_tokens. Legacy path: unchanged, byte for byte.
|
|
3121
3129
|
//
|
|
3122
|
-
// The URL
|
|
3123
|
-
//
|
|
3130
|
+
// The URL is assembled from BOTH sources: origin from the host,
|
|
3131
|
+
// path and query from the package. `built.url` is the pinned
|
|
3132
|
+
// endpoint the headers and body were composed for
|
|
3124
3133
|
// (`https://api.anthropic.com/v1/messages?beta=true`, or
|
|
3125
3134
|
// `.../v1/messages/count_tokens?beta=true` on the count surface),
|
|
3126
|
-
//
|
|
3127
|
-
//
|
|
3128
|
-
//
|
|
3135
|
+
// so the package stays the owner of the envelope — canonical path
|
|
3136
|
+
// plus `?beta=true` — and cannot drift from the body it built.
|
|
3137
|
+
// The ORIGIN, though, belongs to whoever the host addressed: a
|
|
3138
|
+
// custom provider baseURL (gateway, LiteLLM, corporate proxy)
|
|
3139
|
+
// arrives on `requestUrl`, and so does OPENCODE_MITM_BASE_URL,
|
|
3140
|
+
// which transformRequestUrl already applied. Taking the package's
|
|
3141
|
+
// origin as well would silently redirect those deployments to
|
|
3142
|
+
// api.anthropic.com.
|
|
3143
|
+
//
|
|
3144
|
+
// No gateway path PREFIX can be lost this way: the `_useAdapter`
|
|
3145
|
+
// gate only admits pathnames in {/v1/messages, /messages,
|
|
3146
|
+
// /v1/messages/count_tokens, /messages/count_tokens}, so a
|
|
3147
|
+
// prefixed endpoint never reaches the adapter in the first place.
|
|
3148
|
+
//
|
|
3149
|
+
// transformRequestUrl still owns the URL outright on the legacy /
|
|
3150
|
+
// emulation-off path.
|
|
3129
3151
|
const _adapterSignature = {
|
|
3130
3152
|
enabled: getSignatureEmulationEnabled(),
|
|
3131
3153
|
claudeCliVersion,
|
|
@@ -3136,6 +3158,7 @@ export async function AnthropicAuthPlugin({ client }) {
|
|
|
3136
3158
|
|
|
3137
3159
|
let requestHeaders;
|
|
3138
3160
|
let adapterBody;
|
|
3161
|
+
let adapterRequestInput;
|
|
3139
3162
|
let legacyStrippedBody;
|
|
3140
3163
|
if (_useAdapter) {
|
|
3141
3164
|
const _adapterResult = buildAdapterTransport({
|
|
@@ -3181,14 +3204,61 @@ export async function AnthropicAuthPlugin({ client }) {
|
|
|
3181
3204
|
: await buildWireCompatibleRequest(body, _adapterResult.transport);
|
|
3182
3205
|
requestHeaders = built.headers;
|
|
3183
3206
|
adapterBody = built.body;
|
|
3207
|
+
try {
|
|
3208
|
+
// Origin from the HOST, path+query from the PACKAGE.
|
|
3209
|
+
// `requestUrl` already carries the provider's configured
|
|
3210
|
+
// baseURL and already had OPENCODE_MITM_BASE_URL applied by
|
|
3211
|
+
// transformRequestUrl, so copying its origin is what keeps a
|
|
3212
|
+
// gateway / LiteLLM / proxy deployment reachable. With no
|
|
3213
|
+
// usable `requestUrl` the package's own origin stands.
|
|
3214
|
+
const _adapterUrl = new URL(built.url);
|
|
3215
|
+
if (requestUrl) {
|
|
3216
|
+
_adapterUrl.protocol = requestUrl.protocol;
|
|
3217
|
+
_adapterUrl.hostname = requestUrl.hostname;
|
|
3218
|
+
_adapterUrl.port = requestUrl.port;
|
|
3219
|
+
}
|
|
3220
|
+
adapterRequestInput =
|
|
3221
|
+
input instanceof Request ? new Request(_adapterUrl.toString(), input) : _adapterUrl;
|
|
3222
|
+
} catch (error) {
|
|
3223
|
+
// Unparsable package URL — fall back to the transformed URL.
|
|
3224
|
+
debugLog(`adapter url adoption skipped (${error.message}); keeping the transformed url`);
|
|
3225
|
+
}
|
|
3184
3226
|
} else {
|
|
3185
|
-
|
|
3227
|
+
// Unreachable by construction, and it must STAY unreachable:
|
|
3228
|
+
// `buildAdapterTransport` declines on exactly two conditions,
|
|
3229
|
+
// a non-anthropic provider and signature emulation off, and
|
|
3230
|
+
// the call site above pins `provider: "anthropic"` while
|
|
3231
|
+
// `_useAdapter` already required emulation on. Falling back to
|
|
3232
|
+
// the legacy forge here would silently restore the dual path
|
|
3233
|
+
// Phase 2.2 removed, so a decline is an error instead.
|
|
3234
|
+
throw new Error(
|
|
3235
|
+
`opencode-anthropic-fix: the Claude Code wire adapter declined a request it must handle ` +
|
|
3236
|
+
`(${_adapterResult.reason}) on ${_adapterPathname}. This is a plugin bug — ` +
|
|
3237
|
+
`the legacy request path is no longer reachable with signature emulation on.`,
|
|
3238
|
+
);
|
|
3186
3239
|
}
|
|
3187
3240
|
}
|
|
3188
3241
|
|
|
3242
|
+
if (!requestHeaders && !_emulationEnabled) {
|
|
3243
|
+
// PHASE 2.2 — EMULATION OFF IS PURE PASSTHROUGH PLUS THE AUTH
|
|
3244
|
+
// ENVELOPE. No mimicry function composes these headers: the
|
|
3245
|
+
// host's set goes out as it arrived, minus the two credentials
|
|
3246
|
+
// that must not travel with our bearer, plus `authorization`
|
|
3247
|
+
// and an ADDITIVE `oauth-2025-04-20`. The forged claude-cli
|
|
3248
|
+
// user-agent and the substituted beta list — the two mimicry
|
|
3249
|
+
// vectors that used to survive with emulation off — are gone.
|
|
3250
|
+
// See lib/passthrough-headers.mjs for why the envelope is not
|
|
3251
|
+
// mimicry.
|
|
3252
|
+
requestHeaders = buildPassthroughHeaders(input, requestInit, accessToken);
|
|
3253
|
+
}
|
|
3254
|
+
|
|
3189
3255
|
if (!requestHeaders) {
|
|
3190
|
-
//
|
|
3191
|
-
//
|
|
3256
|
+
// Emulation ON, on an endpoint the package has no surface for
|
|
3257
|
+
// (files, models, a gateway-prefixed route). Unchanged, and the
|
|
3258
|
+
// only caller of the legacy forge left: on a messages or
|
|
3259
|
+
// count_tokens turn the adapter above always produced headers,
|
|
3260
|
+
// which `test/conformance/adapter-unconditional.test.mjs`
|
|
3261
|
+
// observes directly.
|
|
3192
3262
|
requestHeaders = buildRequestHeaders(
|
|
3193
3263
|
input,
|
|
3194
3264
|
requestInit,
|
|
@@ -3273,7 +3343,7 @@ export async function AnthropicAuthPlugin({ client }) {
|
|
|
3273
3343
|
debugLog("failed to write outgoing request headers to debug-headers.log", e);
|
|
3274
3344
|
}
|
|
3275
3345
|
}
|
|
3276
|
-
response = await fetch(requestInput, {
|
|
3346
|
+
response = await fetch(adapterRequestInput ?? requestInput, {
|
|
3277
3347
|
...requestInit,
|
|
3278
3348
|
body: finalBody,
|
|
3279
3349
|
headers: requestHeaders,
|
|
@@ -5172,7 +5242,7 @@ function resolveAdaptiveContext(bodyString, model, adaptiveConfig, parsedBody) {
|
|
|
5172
5242
|
}
|
|
5173
5243
|
|
|
5174
5244
|
// Model must be eligible for 1M context at all (includes Opus 4.6)
|
|
5175
|
-
if (!
|
|
5245
|
+
if (!isEligibleFor1MContextWire(model)) {
|
|
5176
5246
|
return false;
|
|
5177
5247
|
}
|
|
5178
5248
|
|
|
@@ -5423,6 +5493,12 @@ function writeCacheStatsFile(usage, model, hitRate) {
|
|
|
5423
5493
|
|
|
5424
5494
|
// --- Phase 5: Minimal telemetry emulation ("Silent Observer") ---
|
|
5425
5495
|
|
|
5496
|
+
// Host telemetry payload only (ClaudeCodeInternalEvent env block) — never sent
|
|
5497
|
+
// on /v1/messages, so it is not part of the wire-compat surface.
|
|
5498
|
+
// Real build markers extracted from the 2.1.195 native binary (Bun-embedded JS):
|
|
5499
|
+
// `BUILD_TIME:"2026-06-26T01:00:56Z"`.
|
|
5500
|
+
const CLAUDE_CODE_BUILD_TIME = "2026-06-26T01:00:56Z";
|
|
5501
|
+
|
|
5426
5502
|
class TelemetryEmitter {
|
|
5427
5503
|
#enabled = false;
|
|
5428
5504
|
#sent = false;
|
|
@@ -5828,9 +5904,10 @@ function getAccountIdentifier(account) {
|
|
|
5828
5904
|
* @param {{ use1MContext?: boolean }} [adaptiveOverride] - When set, overrides the static hasOneMillionContext() check.
|
|
5829
5905
|
* @param {boolean} [fastModeActive] - When true, emits FAST_MODE_BETA_FLAG (fast-mode-2026-02-01).
|
|
5830
5906
|
* 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
|
-
*
|
|
5907
|
+
* Only passed from the buildRequestHeaders call site (after body transform), which is now the
|
|
5908
|
+
* ONLY call site: the pre-transform `computedBetaHeader` caller was deleted in Phase 4.1. Its
|
|
5909
|
+
* sole consumer, the `task-budgets-2026-03-13` check in transformRequestBody, takes a derived
|
|
5910
|
+
* boolean instead (see `_taskBudgetsActive` in the interceptor).
|
|
5834
5911
|
* @returns {string}
|
|
5835
5912
|
*/
|
|
5836
5913
|
// Mirrors CC's Kw(model) effort eligibility: returns false for claude-3-* and the
|
|
@@ -5865,6 +5942,9 @@ function getAccountIdentifier(account) {
|
|
|
5865
5942
|
* @returns {string}
|
|
5866
5943
|
*/
|
|
5867
5944
|
|
|
5945
|
+
// Host update polling only (npm registry lookup), not mimicry.
|
|
5946
|
+
const CLAUDE_CODE_NPM_LATEST_URL = "https://registry.npmjs.org/@anthropic-ai/claude-code/latest";
|
|
5947
|
+
|
|
5868
5948
|
/**
|
|
5869
5949
|
* Resolve latest claude-code package version from npm registry.
|
|
5870
5950
|
* Returns null on timeout/network/parse failures.
|
|
@@ -5915,13 +5995,20 @@ async function fetchLatestClaudeCodeVersion(timeoutMs = 1200) {
|
|
|
5915
5995
|
*/
|
|
5916
5996
|
|
|
5917
5997
|
/**
|
|
5918
|
-
* Transform the request URL: add ?beta=true to /v1/messages and
|
|
5919
|
-
*
|
|
5998
|
+
* Transform the request URL: add ?beta=true to /v1/messages and
|
|
5999
|
+
* /v1/messages/count_tokens, normalizing a `/messages` path to `/v1/messages`
|
|
6000
|
+
* on the way. Preserves behaviors F1-F3.
|
|
6001
|
+
*
|
|
6002
|
+
* Both of those are CLAUDE CODE SHAPE, so both are gated on `emulateSignature`.
|
|
6003
|
+
* The `OPENCODE_MITM_BASE_URL` rewrite is not, and applies either way.
|
|
5920
6004
|
*
|
|
5921
6005
|
* @param {any} input
|
|
6006
|
+
* @param {boolean} [emulateSignature] Whether signature emulation is on for this
|
|
6007
|
+
* request. Read at the call site rather than here: the config is
|
|
6008
|
+
* runtime-mutable and this function lives outside the plugin closure.
|
|
5922
6009
|
* @returns {{requestInput: any, requestUrl: URL | null}}
|
|
5923
6010
|
*/
|
|
5924
|
-
function transformRequestUrl(input) {
|
|
6011
|
+
function transformRequestUrl(input, emulateSignature = true) {
|
|
5925
6012
|
let requestInput = input;
|
|
5926
6013
|
let requestUrl = null;
|
|
5927
6014
|
try {
|
|
@@ -5934,7 +6021,14 @@ function transformRequestUrl(input) {
|
|
|
5934
6021
|
requestUrl = null;
|
|
5935
6022
|
}
|
|
5936
6023
|
|
|
5937
|
-
|
|
6024
|
+
// PHASE 2.2 (QA finding 1) — THE URL REWRITE IS MIMICRY, SO IT IS GATED.
|
|
6025
|
+
// `?beta=true` is the endpoint the genuine Claude Code client pins, and
|
|
6026
|
+
// normalizing `/messages` to `/v1/messages` is the same kind of client-shape
|
|
6027
|
+
// assumption. With signature emulation off the plugin forges nothing, and
|
|
6028
|
+
// that has to include the URL: the host's URL goes out exactly as the host
|
|
6029
|
+
// wrote it. The MITM rewrite below stays unconditional — it is a debug and
|
|
6030
|
+
// conformance knob the operator asked for, not a disguise.
|
|
6031
|
+
if (emulateSignature && requestUrl && !requestUrl.searchParams.has("beta")) {
|
|
5938
6032
|
const p = requestUrl.pathname;
|
|
5939
6033
|
// SDK may send to /messages (base URL includes /v1) or /v1/messages (base URL is root)
|
|
5940
6034
|
const isMessages =
|
|
@@ -6294,13 +6388,6 @@ AnthropicAuthPlugin.__testing__ = {
|
|
|
6294
6388
|
maybeApplySessionToolResultDedupe,
|
|
6295
6389
|
// exposed for experimental.session.summarize integration tests
|
|
6296
6390
|
runHaikuSessionSummarize,
|
|
6297
|
-
get cachedCCPrompt() {
|
|
6298
|
-
return getCachedCCPrompt();
|
|
6299
|
-
},
|
|
6300
|
-
resetCachedCCPrompt() {
|
|
6301
|
-
resetCachedCCPrompt();
|
|
6302
|
-
},
|
|
6303
|
-
SUBAGENT_CC_ANCHOR,
|
|
6304
6391
|
CLAUDE_CODE_IDENTITY_STRING,
|
|
6305
6392
|
/** Test-only: drive the session turn counter so code paths gated on
|
|
6306
6393
|
* `sessionMetrics.turns >= N` can be exercised without a real SSE stream. */
|