opencode-anthropic-fix 0.6.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/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
- isEligibleFor1MContext,
83
+ isEligibleFor1MContextWire,
84
84
  isOpus46Model,
85
85
  isOpus47Model,
86
86
  isOpus48Model,
87
- } from "./lib/mimicry/models.mjs";
87
+ } from "./lib/mimicry/wire-compat.mjs";
88
88
 
89
- export { isFable5Model, isMythos5Model, isAdaptiveThinkingModel } from "./lib/mimicry/models.mjs";
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
- let claudeCliVersion = FALLBACK_CLAUDE_CLI_VERSION;
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
- const { requestInput, requestUrl } = transformRequestUrl(input);
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
- let computedBetaHeader = buildAnthropicBetaHeader(
2932
- "",
2933
- getSignatureEmulationEnabled(),
2934
- _reqModel,
2935
- _sessionFilteredCustomBetas,
2936
- getEffectiveStrategy(),
2937
- requestUrl?.pathname,
2938
- _reqHasFileRefs,
2939
- _adaptiveOverride,
2940
- _tokenEconomy,
2941
- _microcompactBetas, // NEW
2942
- );
2943
-
2944
- // Beta header latching: once a beta has been sent in this session,
2945
- // keep sending it to avoid server-side cache key churn (~50-70K tokens
2946
- // per flip). The latch is sticky-on: new betas can be added but never
2947
- // removed mid-session unless explicitly invalidated by config change.
2948
- {
2949
- const currentBetas = computedBetaHeader
2950
- .split(",")
2951
- .map((b) => b.trim())
2952
- .filter(Boolean);
2953
- // Add all current betas to the latch set
2954
- for (const b of currentBetas) betaLatchState.sent.add(b);
2955
- // If a config change dirtied the latch, rebuild from current (allow removal)
2956
- if (betaLatchState.dirty) {
2957
- betaLatchState.dirty = false;
2958
- betaLatchState.sent = new Set(currentBetas);
2959
- }
2960
- // Merge latched betas that aren't in the current set
2961
- const merged = new Set(currentBetas);
2962
- for (const b of betaLatchState.sent) merged.add(b);
2963
- // Custom-beta strip kill switch: when the server rejected our custom
2964
- // betas (customBetasStripped latch fired), evict them from the latch so
2965
- // they do not re-appear on the retry attempt.
2966
- // F1: delete both the raw alias AND the canonical form so that aliases
2967
- // like "cache-diag" (cache-diagnosis-2026-04-07) are fully evicted.
2968
- if (customBetasStripped && config.custom_betas?.length) {
2969
- for (const b of config.custom_betas) {
2970
- const canonical = resolveBetaShortcut(b);
2971
- merged.delete(b);
2972
- merged.delete(canonical);
2973
- betaLatchState.sent.delete(b);
2974
- betaLatchState.sent.delete(canonical);
2975
- }
2976
- }
2977
- // F4: Evict session-rejected betas from the latch on every request so
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, or through the legacy
3007
- // buildRequestHeaders path?
2997
+ // through the shared wire package?
3008
2998
  //
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. a messages endpoint the package has a surface for. Both
3014
- // qualify now: `buildClaudeCodeRequest` for /v1/messages and
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
- // 3. a JSON body is actually present. The main-turn surface
3025
- // requires `model` and `max_tokens`; the count surface
3026
- // requires `model` and `messages` and accepts no
3027
- // `max_tokens` at all. Either way a bodiless request must
3028
- // keep using the legacy path rather than throwing
3029
- // INVALID_INPUT.
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
- _adapterPathname === "/v1/messages/count_tokens" || _adapterPathname === "/messages/count_tokens";
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
- getSignatureEmulationEnabled() &&
3035
- (_adapterPathname === "/v1/messages" || _adapterPathname === "/messages" || _isCountTokens) &&
3036
- typeof requestInit.body === "string" &&
3037
- requestInit.body.length > 0;
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
- const body = transformRequestBody(
3040
- requestInit.body,
3041
- {
3042
- enabled: getSignatureEmulationEnabled(),
3043
- claudeCliVersion,
3044
- promptCompactionMode: getPromptCompactionMode(),
3045
- useAdapter: _useAdapter,
3046
- cachePolicy: effectiveCachePolicy,
3047
- fastMode: config.fast_mode || false,
3048
- // When the account's fast pool is cooling down, suppress
3049
- // speed:"fast" so this turn runs at standard speed on the same
3050
- // account instead of re-hitting the exhausted fast pool.
3051
- fastRateLimited: accountManager.isFastRateLimited(account),
3052
- strategy: getEffectiveStrategy(),
3053
- toolDeferral: config.token_economy_strategies?.tool_deferral,
3054
- toolDescriptionCompaction: config.token_economy_strategies?.tool_description_compaction,
3055
- adaptiveToolSet: config.token_economy_strategies?.adaptive_tool_set,
3056
- systemPromptTailing: config.token_economy_strategies?.system_prompt_tailing,
3057
- systemPromptTailTurns: config.token_economy_strategies?.system_prompt_tail_turns,
3058
- systemPromptTailMaxChars: config.token_economy_strategies?.system_prompt_tail_max_chars,
3059
- },
3060
- {
3061
- persistentUserId: signatureUserId,
3062
- sessionId: signatureSessionId,
3063
- accountId: getAccountIdentifier(account),
3064
- turns: sessionMetrics.turns,
3065
- usedTools: sessionMetrics.usedTools,
3066
- tokenEconomySession,
3067
- requestRole: _requestRole,
3068
- isSubagent: _isSubagent,
3069
- // Adaptive cache-breakpoint placement: pass the per-boundary
3070
- // stability snapshot so transformRequestBody can anchor the
3071
- // cache_control marker on the most-stable boundary instead of
3072
- // the (possibly thrashing) last tool. Only populated once the
3073
- // detector is enabled and has a baseline.
3074
- cacheBoundaryStability: config.cache_break_detection?.adaptive_breakpoint
3075
- ? cacheBreakState.boundaryStability
3076
- : null,
3077
- },
3078
- computedBetaHeader,
3079
- config,
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 deliberately stays whatever transformRequestUrl already
3123
- // produced. `built.url` is the package's pinned endpoint
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
- // which would override a custom ANTHROPIC_BASE_URL or proxy
3127
- // endpoint. transformRequestUrl already appends `?beta=true` to
3128
- // both endpoints itself, so discarding `built.url` costs nothing.
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
- debugLog(`adapter not applicable (${_adapterResult.reason}); using the legacy request path`);
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
- // Legacy path, and the fallback whenever the adapter declined:
3191
- // a bodiless request, or a transport the translation refused.
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 (!isEligibleFor1MContext(model)) {
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). The pre-transform
5832
- * computedBetaHeader call site (index.mjs:2905) leaves this undefined so the latch never
5833
- * accumulates fast-mode independently of actual body content.
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 /v1/messages/count_tokens.
5919
- * Preserves behaviors F1-F3.
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
- if (requestUrl && !requestUrl.searchParams.has("beta")) {
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. */