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/lib/betas.mjs
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host beta policy.
|
|
3
|
+
*
|
|
4
|
+
* Three things live here, and none of them is wire construction:
|
|
5
|
+
* - `EXPERIMENTAL_BETA_FLAGS`: the betas the HOST treats as experimental, i.e.
|
|
6
|
+
* the set it filters out when `CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS` is on;
|
|
7
|
+
* - `BETA_SHORTCUTS`: CLI UX aliases for `/anthropic betas` and
|
|
8
|
+
* `config.custom_betas`;
|
|
9
|
+
* - `resolveBetaShortcut`: the resolver behind those aliases.
|
|
10
|
+
*
|
|
11
|
+
* The betas that actually go on the wire are the shared package's business
|
|
12
|
+
* (`lib/mimicry/wire-compat.mjs`); this module only decides what the plugin
|
|
13
|
+
* ADDS or SUPPRESSES on top of them.
|
|
14
|
+
*
|
|
15
|
+
* RECONCILIATION IS ENFORCED BY TEST, not by convention. Every literal header
|
|
16
|
+
* below is checked against the package's `BETA_REGISTRY_2_1_233` — the genuine
|
|
17
|
+
* client's own registry — by `lib/betas.test.mjs`, which fails on a stale date
|
|
18
|
+
* stamp and requires the host-only entries (the ones upstream has no registry
|
|
19
|
+
* slot for) to be enumerated and justified rather than merely accumulated. Add
|
|
20
|
+
* a beta here that upstream does not ship and the test will tell you to say why.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Set of all known experimental/optional beta flags.
|
|
25
|
+
* Used to filter betas when CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS=1.
|
|
26
|
+
* WARNING: this set intentionally overlaps with always-on betas - only use
|
|
27
|
+
* it with the disable-experimental guard, never unconditionally.
|
|
28
|
+
*/
|
|
29
|
+
export const EXPERIMENTAL_BETA_FLAGS = new Set([
|
|
30
|
+
"adaptive-thinking-2026-01-28",
|
|
31
|
+
"advanced-tool-use-2025-11-20",
|
|
32
|
+
"advisor-tool-2026-03-01",
|
|
33
|
+
"afk-mode-2026-01-31",
|
|
34
|
+
"cache-diagnosis-2026-04-07",
|
|
35
|
+
// CCR bring-your-own-cloud beta. Registered in the 2.1.154 binary's _W() table
|
|
36
|
+
// but not emitted on /v1/messages; kept here for the disable-experimental guard.
|
|
37
|
+
"ccr-byoc-2025-07-29",
|
|
38
|
+
"code-execution-2025-08-25",
|
|
39
|
+
"compact-2026-01-12",
|
|
40
|
+
"context-1m-2025-08-07",
|
|
41
|
+
"context-hint-2026-04-09",
|
|
42
|
+
"context-management-2025-06-27",
|
|
43
|
+
"environments-2025-11-01",
|
|
44
|
+
"extended-cache-ttl-2025-04-11",
|
|
45
|
+
// Refusal-fallback credit beta from the upstream Udd registry (CC 2.1.195).
|
|
46
|
+
// Opt-in/GrowthBook-gated; never auto-emitted on a default /v1/messages turn.
|
|
47
|
+
// Listed here for the disable-experimental guard + manual opt-in ONLY.
|
|
48
|
+
"fallback-credit-2026-06-01",
|
|
49
|
+
"fast-mode-2026-02-01",
|
|
50
|
+
"files-api-2025-04-14",
|
|
51
|
+
"interleaved-thinking-2025-05-14",
|
|
52
|
+
// Registered in the 2.1.154 binary's _W() table (mcp_servers label). Plugin
|
|
53
|
+
// proxies MCP tool calls inline and does not emit this on chat completions.
|
|
54
|
+
"mcp-servers-2025-12-04",
|
|
55
|
+
// Registered in CC v2.1.143 but NOT in real CC's always-on emission set
|
|
56
|
+
// (master registry only, not in the always-on subset). Likely GrowthBook-gated
|
|
57
|
+
// pending server rollout. Paired telemetry: tengu_mid_conv_system_fallback_retry.
|
|
58
|
+
"mid-conversation-system-2026-04-07",
|
|
59
|
+
"prompt-caching-scope-2026-01-05",
|
|
60
|
+
"redact-thinking-2026-02-12",
|
|
61
|
+
// Server-side refusal-fallback beta from the upstream Udd registry (CC 2.1.195).
|
|
62
|
+
// Opt-in/GrowthBook-gated; never auto-emitted on a default /v1/messages turn.
|
|
63
|
+
// Listed here for the disable-experimental guard + manual opt-in ONLY.
|
|
64
|
+
"server-side-fallback-2026-06-01",
|
|
65
|
+
"structured-outputs-2025-12-15",
|
|
66
|
+
// Revived in CC 2.1.159 as registry label `narration_summaries` (it was a dead
|
|
67
|
+
// slot from v2.1.90-2.1.154). Real CC emits it only when GrowthBook flag
|
|
68
|
+
// `pewter_owl_header` is on (default-off), first-party, and NOT in fast-mode.
|
|
69
|
+
// Listed here for the disable-guard + manual opt-in ONLY; never emitted always-on.
|
|
70
|
+
"summarize-connector-text-2026-03-13",
|
|
71
|
+
"task-budgets-2026-03-13",
|
|
72
|
+
"tool-search-tool-2025-10-19",
|
|
73
|
+
// SDK admin route beta (used for /v1/user_profiles* endpoints). Registered for
|
|
74
|
+
// forward-compat; the plugin proxies /v1/messages only and does NOT emit this
|
|
75
|
+
// on chat completions.
|
|
76
|
+
"user-profiles-2026-03-24",
|
|
77
|
+
"web-search-2025-03-05",
|
|
78
|
+
]);
|
|
79
|
+
|
|
80
|
+
/** Friendly shortcut aliases for config.custom_betas values. */
|
|
81
|
+
export const BETA_SHORTCUTS = new Map([
|
|
82
|
+
["1m", "context-1m-2025-08-07"],
|
|
83
|
+
["1m-context", "context-1m-2025-08-07"],
|
|
84
|
+
["context-1m", "context-1m-2025-08-07"],
|
|
85
|
+
["cache-diagnosis", "cache-diagnosis-2026-04-07"],
|
|
86
|
+
["cache-diag", "cache-diagnosis-2026-04-07"],
|
|
87
|
+
["cache-ttl", "extended-cache-ttl-2025-04-11"],
|
|
88
|
+
["context-hint", "context-hint-2026-04-09"],
|
|
89
|
+
["environments", "environments-2025-11-01"],
|
|
90
|
+
["extended-cache-ttl", "extended-cache-ttl-2025-04-11"],
|
|
91
|
+
["hint", "context-hint-2026-04-09"],
|
|
92
|
+
["fast", "fast-mode-2026-02-01"],
|
|
93
|
+
["fast-mode", "fast-mode-2026-02-01"],
|
|
94
|
+
["opus-fast", "fast-mode-2026-02-01"],
|
|
95
|
+
["task-budgets", "task-budgets-2026-03-13"],
|
|
96
|
+
["budgets", "task-budgets-2026-03-13"],
|
|
97
|
+
["redact-thinking", "redact-thinking-2026-02-12"],
|
|
98
|
+
["mid-conv-system", "mid-conversation-system-2026-04-07"],
|
|
99
|
+
["mid-system", "mid-conversation-system-2026-04-07"],
|
|
100
|
+
["context-management", "context-management-2025-06-27"],
|
|
101
|
+
["structured-outputs", "structured-outputs-2025-12-15"],
|
|
102
|
+
["web-search", "web-search-2025-03-05"],
|
|
103
|
+
["advanced-tool-use", "advanced-tool-use-2025-11-20"],
|
|
104
|
+
["tool-search-tool", "tool-search-tool-2025-10-19"],
|
|
105
|
+
["effort", "effort-2025-11-24"],
|
|
106
|
+
["prompt-caching-scope", "prompt-caching-scope-2026-01-05"],
|
|
107
|
+
["thinking-token-count", "thinking-token-count-2026-05-13"],
|
|
108
|
+
["afk-mode", "afk-mode-2026-01-31"],
|
|
109
|
+
["advisor-tool", "advisor-tool-2026-03-01"],
|
|
110
|
+
["mcp-servers", "mcp-servers-2025-12-04"],
|
|
111
|
+
["ccr-byoc", "ccr-byoc-2025-07-29"],
|
|
112
|
+
["connector-text", "summarize-connector-text-2026-03-13"],
|
|
113
|
+
["narration-summaries", "summarize-connector-text-2026-03-13"],
|
|
114
|
+
["summarize-connector-text", "summarize-connector-text-2026-03-13"],
|
|
115
|
+
["server-side-fallback", "server-side-fallback-2026-06-01"],
|
|
116
|
+
["fallback", "server-side-fallback-2026-06-01"],
|
|
117
|
+
["fallback-credit", "fallback-credit-2026-06-01"],
|
|
118
|
+
]);
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Resolve a beta shortcut alias to its canonical flag name.
|
|
122
|
+
* Returns the value unchanged if no alias matches.
|
|
123
|
+
* @param {string | null | undefined} value
|
|
124
|
+
* @returns {string}
|
|
125
|
+
*/
|
|
126
|
+
export function resolveBetaShortcut(value) {
|
|
127
|
+
if (!value) return "";
|
|
128
|
+
const trimmed = value.trim();
|
|
129
|
+
const mapped = BETA_SHORTCUTS.get(trimmed.toLowerCase());
|
|
130
|
+
return mapped || trimmed;
|
|
131
|
+
}
|
package/lib/diagnose.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import { fileURLToPath } from "node:url";
|
|
|
5
5
|
|
|
6
6
|
import { getConfigDir, loadConfig } from "./config.mjs";
|
|
7
7
|
import { redactSecrets } from "./redact.mjs";
|
|
8
|
-
import {
|
|
8
|
+
import { WIRE_PROFILE } from "./mimicry/wire-compat.mjs";
|
|
9
9
|
import { loadAccounts } from "./storage.mjs";
|
|
10
10
|
|
|
11
11
|
const ENV_KEYS = [
|
|
@@ -172,7 +172,9 @@ export async function buildDiagnosticBundle() {
|
|
|
172
172
|
pluginVersion: getPluginVersion(),
|
|
173
173
|
nodeVersion: process.version,
|
|
174
174
|
platform: process.platform,
|
|
175
|
-
|
|
175
|
+
// Static baseline on purpose: the CLI version the wire package's profile
|
|
176
|
+
// declares, NOT the (possibly npm-updated) version of the running session.
|
|
177
|
+
mimicryBaseline: WIRE_PROFILE.cliVersion,
|
|
176
178
|
},
|
|
177
179
|
config: redactSecrets(loadConfig()),
|
|
178
180
|
env: getRuntimeEnv(),
|
|
@@ -23,19 +23,30 @@
|
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
25
|
import { isTruthyEnv, isFalsyEnv } from "../env.mjs";
|
|
26
|
-
import {
|
|
27
|
-
|
|
26
|
+
import {
|
|
27
|
+
hasOneMillionContext,
|
|
28
|
+
isClaude3Model,
|
|
29
|
+
isHaikuModel,
|
|
30
|
+
supportsStructuredOutputs,
|
|
31
|
+
supportsWebSearch,
|
|
32
|
+
WIRE_PROFILE,
|
|
33
|
+
BETA_REGISTRY_2_1_233,
|
|
34
|
+
} from "./wire-compat.mjs";
|
|
35
|
+
import { BETA_SHORTCUTS, EXPERIMENTAL_BETA_FLAGS } from "../betas.mjs";
|
|
28
36
|
import { resolveCacheTtl } from "./cache.mjs";
|
|
37
|
+
// CLAUDE_CODE_BETA_FLAG and HOST_SDK_BETAS_BLOCKLIST are HOST POLICY, not wire
|
|
38
|
+
// capability: the first is the sentinel this module tests for when deciding
|
|
39
|
+
// whether a rescued host beta is already package-composed, the second is the
|
|
40
|
+
// plugin's own list of betas the host SDK injects that the genuine client never
|
|
41
|
+
// sends. Both stay plugin-side by design; only the model CAPABILITY predicates
|
|
42
|
+
// moved to the package seam above.
|
|
29
43
|
import {
|
|
30
44
|
CLAUDE_CODE_BETA_FLAG,
|
|
31
45
|
HOST_SDK_BETAS_BLOCKLIST,
|
|
32
46
|
buildStainlessHelperHeader,
|
|
33
47
|
getStainlessArch,
|
|
34
48
|
getStainlessOs,
|
|
35
|
-
isHaikuModel,
|
|
36
49
|
parseRequestBodyMetadata,
|
|
37
|
-
supportsStructuredOutputs,
|
|
38
|
-
supportsWebSearch,
|
|
39
50
|
} from "./headers.mjs";
|
|
40
51
|
|
|
41
52
|
// ---------------------------------------------------------------------------
|
|
@@ -48,6 +59,69 @@ export const ADAPTER_SKIP_SIGNATURE_DISABLED = "signature-disabled";
|
|
|
48
59
|
/** The shared package pins the Anthropic first-party URL and rejects any other. */
|
|
49
60
|
export const ADAPTER_SKIP_NON_ANTHROPIC_PROVIDER = "non-anthropic-provider";
|
|
50
61
|
|
|
62
|
+
/**
|
|
63
|
+
* The pathnames the shared package has a surface for. `/v1/messages` and
|
|
64
|
+
* `/messages` go to `buildClaudeCodeRequest`; the count pair goes to
|
|
65
|
+
* `buildClaudeCodeCountTokensRequest`. Every other pathname (files, models,
|
|
66
|
+
* a gateway-prefixed route) is passed through untouched by the interceptor.
|
|
67
|
+
*/
|
|
68
|
+
export const ADAPTER_MESSAGES_PATHNAMES = new Set(["/v1/messages", "/messages"]);
|
|
69
|
+
|
|
70
|
+
/** @see ADAPTER_MESSAGES_PATHNAMES */
|
|
71
|
+
export const ADAPTER_COUNT_TOKENS_PATHNAMES = new Set(["/v1/messages/count_tokens", "/messages/count_tokens"]);
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Reject a request the adapter is REQUIRED to handle but cannot.
|
|
75
|
+
*
|
|
76
|
+
* WHY THIS THROWS INSTEAD OF DEGRADING. Since Phase 2.2 the adapter is
|
|
77
|
+
* unconditional: with signature emulation on, a messages/count_tokens turn has
|
|
78
|
+
* exactly one construction path. A body the package cannot consume used to fall
|
|
79
|
+
* back to the legacy forge, which meant a malformed or bodiless request quietly
|
|
80
|
+
* went on the wire with a different fingerprint than every other turn in the
|
|
81
|
+
* session — the failure mode the migration exists to remove. Failing loudly
|
|
82
|
+
* here keeps the path single, and the message names the endpoint and the exact
|
|
83
|
+
* defect so the caller can fix its request.
|
|
84
|
+
*
|
|
85
|
+
* The check is deliberately structural (a JSON object), not semantic: `model`,
|
|
86
|
+
* `max_tokens` and `messages` validation belongs to the package, which already
|
|
87
|
+
* reports it as `INVALID_INPUT` with `safeDetails`.
|
|
88
|
+
*
|
|
89
|
+
* @param {unknown} body The body the host handed the interceptor.
|
|
90
|
+
* @param {string | undefined} pathname The outgoing pathname, for the message.
|
|
91
|
+
* @returns {void}
|
|
92
|
+
* @throws {Error} When the body is absent, unparsable, or not a JSON object.
|
|
93
|
+
*/
|
|
94
|
+
export function assertAdapterBodyUsable(body, pathname) {
|
|
95
|
+
const where = `${pathname ?? "the messages endpoint"} with signature emulation on`;
|
|
96
|
+
if (typeof body !== "string" || body.length === 0) {
|
|
97
|
+
throw new Error(
|
|
98
|
+
`opencode-anthropic-fix: ${where} requires a JSON request body, but the request carried ` +
|
|
99
|
+
`${typeof body === "string" ? "an empty string" : `no usable body (${typeof body})`}. ` +
|
|
100
|
+
`The Claude Code wire adapter cannot construct this request.`,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
let parsed;
|
|
105
|
+
try {
|
|
106
|
+
parsed = JSON.parse(body);
|
|
107
|
+
} catch (error) {
|
|
108
|
+
throw new Error(
|
|
109
|
+
`opencode-anthropic-fix: ${where} requires a JSON request body, but the body could not be ` +
|
|
110
|
+
`parsed as JSON (${error instanceof Error ? error.message : String(error)}). ` +
|
|
111
|
+
`The Claude Code wire adapter cannot construct this request.`,
|
|
112
|
+
{ cause: error },
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
117
|
+
throw new Error(
|
|
118
|
+
`opencode-anthropic-fix: ${where} requires a JSON OBJECT request body, but the body parsed ` +
|
|
119
|
+
`as ${Array.isArray(parsed) ? "an array" : String(parsed === null ? "null" : typeof parsed)}. ` +
|
|
120
|
+
`The Claude Code wire adapter cannot construct this request.`,
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
51
125
|
/**
|
|
52
126
|
* `additionalBetas` accepts at most 32 entries; overflow is `INVALID_INPUT`,
|
|
53
127
|
* a hard failure rather than a degradation. Exported so the test suite can
|
|
@@ -70,8 +144,28 @@ export const MAX_SUPPRESS_BETAS = 32;
|
|
|
70
144
|
* the round-robin strategy: rotating accounts means the cache scope marker
|
|
71
145
|
* points at a prompt cache the next account cannot read, so paying the beta's
|
|
72
146
|
* bookkeeping buys nothing.
|
|
147
|
+
*
|
|
148
|
+
* The header string itself is WIRE state, so it is read from the package's beta
|
|
149
|
+
* registry through the seam rather than re-typed here: if the package ever
|
|
150
|
+
* re-dates the beta, the suppression keeps matching what the package composes.
|
|
73
151
|
*/
|
|
74
|
-
export const PROMPT_CACHING_SCOPE_BETA =
|
|
152
|
+
export const PROMPT_CACHING_SCOPE_BETA = BETA_REGISTRY_2_1_233.PROMPT_CACHING_SCOPE.header;
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Betas the plugin opts INTO on its own (see `buildAdditionalBetas`): whether
|
|
156
|
+
* each one is emitted is HOST POLICY — model capability, request shape, the
|
|
157
|
+
* round-robin rotation, `toolEnablement` — and that policy stays here.
|
|
158
|
+
*
|
|
159
|
+
* The CANONICAL NAMES are not host policy; they are wire state owned by the
|
|
160
|
+
* package. Reading them from the registry through the seam means an upstream
|
|
161
|
+
* rename or date bump surfaces as a changed emission instead of drifting
|
|
162
|
+
* silently against a literal re-typed in this file.
|
|
163
|
+
*/
|
|
164
|
+
export const WEB_SEARCH_BETA = BETA_REGISTRY_2_1_233.WEB_SEARCH.header;
|
|
165
|
+
export const ADVISOR_TOOL_BETA = BETA_REGISTRY_2_1_233.ADVISOR_TOOL.header;
|
|
166
|
+
export const FILES_API_BETA = BETA_REGISTRY_2_1_233.FILES_API.header;
|
|
167
|
+
export const EXTENDED_CACHE_TTL_BETA = BETA_REGISTRY_2_1_233.EXTENDED_CACHE_TTL.header;
|
|
168
|
+
export const STRUCTURED_OUTPUTS_BETA = BETA_REGISTRY_2_1_233.STRUCTURED_OUTPUTS.header;
|
|
75
169
|
|
|
76
170
|
/**
|
|
77
171
|
* `runtime.sessionId` is REQUIRED by the package (`requiredString`), while the
|
|
@@ -82,6 +176,13 @@ export const PROMPT_CACHING_SCOPE_BETA = "prompt-caching-scope-2026-01-05";
|
|
|
82
176
|
* package's own correlation checks; in practice `signatureSessionId` is always
|
|
83
177
|
* populated when signature emulation is enabled, so this is a safety net rather
|
|
84
178
|
* than a routine path.
|
|
179
|
+
*
|
|
180
|
+
* BOUNDARY: host policy, NOT wire state. This is a plugin invention — genuine
|
|
181
|
+
* Claude Code always has a session id, so it has no equivalent constant, and
|
|
182
|
+
* neither the package (no occurrence in `dist/`) nor the reverse-engineering
|
|
183
|
+
* notes carry this UUID. It therefore stays a local literal by design and is
|
|
184
|
+
* exempt from the version-literal governance guard
|
|
185
|
+
* (`test/conformance/version-literals-retired.test.mjs`).
|
|
85
186
|
*/
|
|
86
187
|
export const SESSION_ID_FALLBACK = "00000000-0000-4000-8000-000000000000";
|
|
87
188
|
|
|
@@ -143,14 +244,15 @@ export const ADAPTER_ENV_KEYS = Object.freeze([
|
|
|
143
244
|
* the `latest` dist-tag, so each package release can advance the default profile
|
|
144
245
|
* and the plugin follows without a code change to the composition path.
|
|
145
246
|
*
|
|
146
|
-
* These two constants are the
|
|
147
|
-
*
|
|
148
|
-
* `
|
|
149
|
-
*
|
|
150
|
-
*
|
|
247
|
+
* These two constants are the baseline `resolveProfileOverride` compares
|
|
248
|
+
* against, and they are READ FROM the profile through the seam
|
|
249
|
+
* (`WIRE_PROFILE`, lib/mimicry/wire-compat.mjs) rather than re-typed. A package
|
|
250
|
+
* bump that advances the default profile therefore moves the plugin's baseline
|
|
251
|
+
* with it — no lockstep edit, and no way for the two to silently disagree and
|
|
252
|
+
* emit a spurious profile override on every request.
|
|
151
253
|
*/
|
|
152
|
-
export const PROFILE_CLI_VERSION =
|
|
153
|
-
export const PROFILE_USER_AGENT =
|
|
254
|
+
export const PROFILE_CLI_VERSION = WIRE_PROFILE.cliVersion;
|
|
255
|
+
export const PROFILE_USER_AGENT = WIRE_PROFILE.userAgent;
|
|
154
256
|
|
|
155
257
|
// ---------------------------------------------------------------------------
|
|
156
258
|
// Environment snapshot
|
|
@@ -177,7 +279,7 @@ export function resolveAdapterEnv(source) {
|
|
|
177
279
|
// ---------------------------------------------------------------------------
|
|
178
280
|
|
|
179
281
|
/**
|
|
180
|
-
* Pure mirror of `buildExtendedUserAgent` (lib/
|
|
282
|
+
* Pure mirror of `buildExtendedUserAgent` (lib/mimicry/headers.mjs) with the
|
|
181
283
|
* environment injected instead of read. The two are pinned to agree by
|
|
182
284
|
* `adapter-input.test.mjs`.
|
|
183
285
|
*
|
|
@@ -385,11 +487,11 @@ function buildAdditionalBetas(args) {
|
|
|
385
487
|
const isRoundRobin = strategy === "round-robin";
|
|
386
488
|
const te = tokenEconomy || {};
|
|
387
489
|
|
|
388
|
-
if (supportsWebSearch(model)) betas.push(
|
|
389
|
-
if (!
|
|
390
|
-
if (isFilesEndpoint || hasFileReferences) betas.push(
|
|
391
|
-
if (te.extended_cache_ttl !== false && !isRoundRobin) betas.push(
|
|
392
|
-
if (te.structured_outputs && supportsStructuredOutputs(model)) betas.push(
|
|
490
|
+
if (supportsWebSearch(model)) betas.push(WEB_SEARCH_BETA);
|
|
491
|
+
if (!isClaude3Model(model)) betas.push(ADVISOR_TOOL_BETA);
|
|
492
|
+
if (isFilesEndpoint || hasFileReferences) betas.push(FILES_API_BETA);
|
|
493
|
+
if (te.extended_cache_ttl !== false && !isRoundRobin) betas.push(EXTENDED_CACHE_TTL_BETA);
|
|
494
|
+
if (te.structured_outputs && supportsStructuredOutputs(model)) betas.push(STRUCTURED_OUTPUTS_BETA);
|
|
393
495
|
// The package deliberately excludes Haiku from `claude-code-20250219`; the
|
|
394
496
|
// plugin deliberately includes it so Haiku subagents reached through
|
|
395
497
|
// model-router delegation still get full mimic behaviour.
|
|
@@ -639,17 +741,17 @@ export function buildAdapterTransport(state) {
|
|
|
639
741
|
}
|
|
640
742
|
// Lean system prompt for non-main requests (`token_economy.lean_system_non_main`).
|
|
641
743
|
//
|
|
642
|
-
//
|
|
643
|
-
//
|
|
644
|
-
//
|
|
645
|
-
//
|
|
646
|
-
//
|
|
647
|
-
//
|
|
648
|
-
//
|
|
744
|
+
// This is the ONLY place the lean decision is made. `suppressCanonicalBlocks`
|
|
745
|
+
// hands the composition of the billing header and the identity prefix to the
|
|
746
|
+
// package, so the plugin can no longer withhold either block by simply not
|
|
747
|
+
// emitting it; the intent has to be expressed as the package's two root
|
|
748
|
+
// seams (`suppressBillingBlock` / `suppressIdentityBlock`), which the package
|
|
749
|
+
// honours when it assembles the canonical blocks. Without them the opt-in
|
|
750
|
+
// silently degrades to a no-op.
|
|
649
751
|
//
|
|
650
|
-
// The gate
|
|
651
|
-
//
|
|
652
|
-
//
|
|
752
|
+
// The gate: opt-in flag, a "title" or "small" role, and not a title-generator
|
|
753
|
+
// turn (that shape gets its own canonical system prompt swap and keeps the
|
|
754
|
+
// attribution).
|
|
653
755
|
if (
|
|
654
756
|
tokenEconomy.lean_system_non_main === true &&
|
|
655
757
|
(state.requestRole === "title" || state.requestRole === "small") &&
|
package/lib/mimicry/headers.mjs
CHANGED
|
@@ -1,12 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ===========================================================================
|
|
3
|
+
* BOUNDARY: THE LEGACY HEADER FORGE IS A FROZEN COMPATIBILITY SHIM.
|
|
4
|
+
* ===========================================================================
|
|
5
|
+
*
|
|
6
|
+
* WHAT IS FROZEN. The beta/header COMPOSITION in this module —
|
|
7
|
+
* `buildAnthropicBetaHeader`, `buildRequestHeaders`, and the Claude Code beta
|
|
8
|
+
* constants and model gates they consume (`CLAUDE_CODE_BETA_FLAG`,
|
|
9
|
+
* `_EFFORT_BETA_FLAG`, `FAST_MODE_BETA_FLAG`, `TOKEN_COUNTING_BETA_FLAG`,
|
|
10
|
+
* `_EFFORT_EXCLUDED_MODELS`, `isEffortCapableModel`, and the local
|
|
11
|
+
* `isHaikuModel` / `supportsStructuredOutputs` / `supportsWebSearch` copies).
|
|
12
|
+
* It is a hand-written reimplementation of what
|
|
13
|
+
* `@tormentalabs/claude-code-wire-compat` now does from the genuine client's
|
|
14
|
+
* own tables.
|
|
15
|
+
*
|
|
16
|
+
* WHY IT IS STILL HERE. It is reachable ONLY for requests the shared package
|
|
17
|
+
* has no surface for: the files and models endpoints, a gateway-prefixed route,
|
|
18
|
+
* or any request made with signature emulation switched off. On `/v1/messages`
|
|
19
|
+
* and `/v1/messages/count_tokens` with emulation on — the path that carries the
|
|
20
|
+
* fingerprint claim — the adapter never enters it. It is a fallback, not a
|
|
21
|
+
* second implementation competing for the same traffic.
|
|
22
|
+
*
|
|
23
|
+
* THE RULE. Do not extend it, do not add a beta to it, do not "improve" its
|
|
24
|
+
* gating, and do not let a new caller in. Fixes to wire behaviour belong in the
|
|
25
|
+
* package. When the package grows a files/models/gateway surface, DELETE this
|
|
26
|
+
* forge and the constants above rather than porting them forward.
|
|
27
|
+
*
|
|
28
|
+
* GOVERNANCE. The Claude Code beta constants here would otherwise trip the
|
|
29
|
+
* Phase 4.1 guard against locally-declared wire constants; they are allowlisted
|
|
30
|
+
* as an explicitly-commented compat exception, and this banner is the comment
|
|
31
|
+
* the allowlist points at. Removing it should fail that guard, by design.
|
|
32
|
+
*
|
|
33
|
+
* NOT FROZEN, and NOT part of the exception: `HOST_SDK_BETAS_BLOCKLIST` (host
|
|
34
|
+
* policy — see its own comment), `parseRequestBodyMetadata`,
|
|
35
|
+
* `stripStainlessHelperMarkers`, `getStainlessOs`/`getStainlessArch` and
|
|
36
|
+
* `buildStainlessHelperHeader`, all of which the adapter path imports and uses.
|
|
37
|
+
*/
|
|
38
|
+
|
|
1
39
|
import { isTruthyEnv, isFalsyEnv } from "../env.mjs";
|
|
2
|
-
import {
|
|
3
|
-
import { EXPERIMENTAL_BETA_FLAGS, BETA_SHORTCUTS
|
|
40
|
+
import { hasOneMillionContext, isClaude3Model } from "./wire-compat.mjs";
|
|
41
|
+
import { EXPERIMENTAL_BETA_FLAGS, BETA_SHORTCUTS } from "../betas.mjs";
|
|
4
42
|
import { randomUUID } from "node:crypto";
|
|
5
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Build the extended User-Agent for API calls.
|
|
46
|
+
* Real CC v96 sends "claude-cli/{version} (external, {entrypoint})" - confirmed via
|
|
47
|
+
* proxy capture of real CC on Windows/Node.js.
|
|
48
|
+
*
|
|
49
|
+
* SCOPE: the legacy forge below is its only production caller, i.e. the
|
|
50
|
+
* endpoints the shared wire package has no surface for. `buildAdapterUserAgent`
|
|
51
|
+
* (lib/mimicry/adapter-input.mjs) is the pure mirror the adapter path uses, and
|
|
52
|
+
* `adapter-input.test.mjs` pins the two to agree. Phase 3.3 of the wire-compat
|
|
53
|
+
* migration may replace this with the package's own user-agent template.
|
|
54
|
+
*
|
|
55
|
+
* @param {string} version
|
|
56
|
+
* @returns {string}
|
|
57
|
+
*/
|
|
58
|
+
export function buildExtendedUserAgent(version) {
|
|
59
|
+
const entrypoint = process.env.CLAUDE_CODE_ENTRYPOINT ?? "cli";
|
|
60
|
+
const sdkVersion = process.env.CLAUDE_AGENT_SDK_VERSION ? `, agent-sdk/${process.env.CLAUDE_AGENT_SDK_VERSION}` : "";
|
|
61
|
+
const clientApp = process.env.CLAUDE_AGENT_SDK_CLIENT_APP
|
|
62
|
+
? `, client-app/${process.env.CLAUDE_AGENT_SDK_CLIENT_APP}`
|
|
63
|
+
: "";
|
|
64
|
+
return `claude-cli/${version} (external, ${entrypoint}${sdkVersion}${clientApp})`;
|
|
65
|
+
}
|
|
66
|
+
|
|
6
67
|
export const CLAUDE_CODE_BETA_FLAG = "claude-code-20250219";
|
|
7
68
|
export const _EFFORT_BETA_FLAG = "effort-2025-11-24";
|
|
8
69
|
export const FAST_MODE_BETA_FLAG = "fast-mode-2026-02-01";
|
|
9
70
|
export const TOKEN_COUNTING_BETA_FLAG = "token-counting-2024-11-01";
|
|
71
|
+
/**
|
|
72
|
+
* BOUNDARY: host policy, not wire construction. Deliberately plugin-side.
|
|
73
|
+
*
|
|
74
|
+
* These are betas the HOST SDK injects into its outgoing request that the
|
|
75
|
+
* genuine Claude Code client NEVER sends. They are not stale registry entries
|
|
76
|
+
* and they are not the package's business — `BETA_REGISTRY_2_1_233` has no slot
|
|
77
|
+
* for either, because upstream has no reason to model a header it does not
|
|
78
|
+
* emit. The plugin drops them so a host's own SDK cannot leak a foreign
|
|
79
|
+
* fingerprint into a request that is otherwise byte-identical to the real
|
|
80
|
+
* client's.
|
|
81
|
+
*
|
|
82
|
+
* `structured-outputs-2025-11-13` is the sharp one: it is an OLDER date stamp of
|
|
83
|
+
* a beta upstream does ship (`structured-outputs-2025-12-15`). Blocking it is
|
|
84
|
+
* what keeps a host SDK from pinning the plugin to a superseded revision of a
|
|
85
|
+
* live beta. `lib/betas.test.mjs` reconciles the host tables against the
|
|
86
|
+
* registry; this set is intentionally exempt from that reconciliation, because
|
|
87
|
+
* its whole purpose is to name headers the registry does not contain.
|
|
88
|
+
*
|
|
89
|
+
* Consumed by BOTH paths — `lib/mimicry/adapter-input.mjs` imports it — so
|
|
90
|
+
* unlike the beta constants above it does NOT die with the legacy forge.
|
|
91
|
+
*/
|
|
10
92
|
export const HOST_SDK_BETAS_BLOCKLIST = new Set([
|
|
11
93
|
"fine-grained-tool-streaming-2025-05-14",
|
|
12
94
|
"structured-outputs-2025-11-13",
|
|
@@ -160,10 +242,35 @@ export function stripStainlessHelperMarkers(tools, messages) {
|
|
|
160
242
|
|
|
161
243
|
export function isEffortCapableModel(model) {
|
|
162
244
|
if (!model) return false;
|
|
163
|
-
if (
|
|
245
|
+
if (isClaude3Model(model)) return false;
|
|
164
246
|
return !_EFFORT_EXCLUDED_MODELS.some((re) => re.test(model));
|
|
165
247
|
}
|
|
166
248
|
|
|
249
|
+
/**
|
|
250
|
+
* ===========================================================================
|
|
251
|
+
* FROZEN COMPATIBILITY SHIM — DO NOT EXTEND. See the banner at the top of this
|
|
252
|
+
* file for the full boundary contract.
|
|
253
|
+
* ===========================================================================
|
|
254
|
+
*
|
|
255
|
+
* The legacy beta composer. On a `/v1/messages` or `/v1/messages/count_tokens`
|
|
256
|
+
* turn with signature emulation on — i.e. the overwhelming majority of traffic
|
|
257
|
+
* — the adapter path NEVER enters this function: the shared package composes the
|
|
258
|
+
* beta list and `lib/mimicry/adapter-input.mjs` only supplies `additionalBetas`
|
|
259
|
+
* and `suppressBetas` on top of it.
|
|
260
|
+
*
|
|
261
|
+
* It survives for exactly ONE caller and no more: `buildRequestHeaders` below,
|
|
262
|
+
* i.e. the endpoints the package has no surface for (files, models, a
|
|
263
|
+
* gateway-prefixed route).
|
|
264
|
+
*
|
|
265
|
+
* PHASE 4.1 — RESOLVED. The second caller, the `computedBetaHeader` call in
|
|
266
|
+
* `index.mjs`, is gone. It never reached the wire; it existed only so
|
|
267
|
+
* `transformRequestBody` could substring-test for `task-budgets-2026-03-13`,
|
|
268
|
+
* and that consumer now takes a boolean derived at the call site from the same
|
|
269
|
+
* gates `buildAdditionalBetas` applies. Do not add a caller back.
|
|
270
|
+
*
|
|
271
|
+
* @param {string} incomingBeta
|
|
272
|
+
* @param {boolean} signatureEnabled
|
|
273
|
+
*/
|
|
167
274
|
export function buildAnthropicBetaHeader(
|
|
168
275
|
incomingBeta,
|
|
169
276
|
signatureEnabled,
|
|
@@ -222,7 +329,7 @@ export function buildAnthropicBetaHeader(
|
|
|
222
329
|
// (firstParty && non-Claude-3). Claude 3.x models don't support interleaved
|
|
223
330
|
// thinking and real CC never sends this flag for them, so emitting it
|
|
224
331
|
// diverges the fingerprint for legacy Haiku/Sonnet 3.x requests.
|
|
225
|
-
if (!isTruthyEnv(process.env.DISABLE_INTERLEAVED_THINKING) && !
|
|
332
|
+
if (!isTruthyEnv(process.env.DISABLE_INTERLEAVED_THINKING) && !isClaude3Model(model)) {
|
|
226
333
|
betas.push("interleaved-thinking-2025-05-14");
|
|
227
334
|
}
|
|
228
335
|
|
|
@@ -257,7 +364,7 @@ export function buildAnthropicBetaHeader(
|
|
|
257
364
|
// analyses read only the separate USE_API_CONTEXT_MANAGEMENT env term (hardcoded
|
|
258
365
|
// && false) and recorded it as off; 2.1.195 confirms it ships by default for modern
|
|
259
366
|
// first-party models. Opt out via token_economy.context_management = false.
|
|
260
|
-
if (te.context_management !== false && !
|
|
367
|
+
if (te.context_management !== false && !isClaude3Model(model)) {
|
|
261
368
|
betas.push("context-management-2025-06-27");
|
|
262
369
|
}
|
|
263
370
|
|
|
@@ -284,7 +391,7 @@ export function buildAnthropicBetaHeader(
|
|
|
284
391
|
// (tengu_sage_compass2) and firstParty+isLoggedIn. Since we can't check
|
|
285
392
|
// CC's feature flags, include it unconditionally for Claude 4+ models.
|
|
286
393
|
// CC v108 sends it in MITM captures for Max/Pro users.
|
|
287
|
-
if (!
|
|
394
|
+
if (!isClaude3Model(model)) {
|
|
288
395
|
betas.push("advisor-tool-2026-03-01");
|
|
289
396
|
}
|
|
290
397
|
|
|
@@ -310,13 +417,13 @@ export function buildAnthropicBetaHeader(
|
|
|
310
417
|
|
|
311
418
|
// v2.1.150: redact-thinking is default-ON in CC (first-party, non-SDK, thinking models).
|
|
312
419
|
// Opt out via `/anthropic set redact-thinking off` to see thinking content.
|
|
313
|
-
if (te.redact_thinking !== false && !disableExperimentalBetas && !
|
|
420
|
+
if (te.redact_thinking !== false && !disableExperimentalBetas && !isClaude3Model(model)) {
|
|
314
421
|
betas.push("redact-thinking-2026-02-12");
|
|
315
422
|
}
|
|
316
423
|
|
|
317
424
|
// v2.1.150: thinking-token-count for token budget tracking (behind tengu_chert_bezel in CC).
|
|
318
425
|
// Default ON for token economy visibility. Opt out via token_economy.thinking_token_count = false.
|
|
319
|
-
if (te.thinking_token_count !== false && !disableExperimentalBetas && !
|
|
426
|
+
if (te.thinking_token_count !== false && !disableExperimentalBetas && !isClaude3Model(model)) {
|
|
320
427
|
betas.push("thinking-token-count-2026-05-13");
|
|
321
428
|
}
|
|
322
429
|
|
|
@@ -383,6 +490,27 @@ export function getStainlessArch(value) {
|
|
|
383
490
|
return value;
|
|
384
491
|
}
|
|
385
492
|
|
|
493
|
+
/**
|
|
494
|
+
* ===========================================================================
|
|
495
|
+
* FROZEN COMPATIBILITY SHIM — DO NOT EXTEND. See the banner at the top of this
|
|
496
|
+
* file for the full boundary contract.
|
|
497
|
+
* ===========================================================================
|
|
498
|
+
*
|
|
499
|
+
* The legacy header forge. ONE production caller (`index.mjs`), reached only
|
|
500
|
+
* when the wire adapter declines the request — signature emulation off, or a
|
|
501
|
+
* pathname the shared package has no surface for. Every `/v1/messages` and
|
|
502
|
+
* `/v1/messages/count_tokens` turn with emulation on is built by the package
|
|
503
|
+
* instead, so this function's output is not what the plugin's fingerprint claim
|
|
504
|
+
* rests on.
|
|
505
|
+
*
|
|
506
|
+
* Deleting it is a package-capability question, not a plugin one: when
|
|
507
|
+
* `@tormentalabs/claude-code-wire-compat` grows a files / models / gateway
|
|
508
|
+
* surface, this forge and the CC beta constants it consumes go with it.
|
|
509
|
+
*
|
|
510
|
+
* @param {Record<string, unknown>} input
|
|
511
|
+
* @param {RequestInit | undefined} requestInit
|
|
512
|
+
* @param {string} accessToken
|
|
513
|
+
*/
|
|
386
514
|
export function buildRequestHeaders(
|
|
387
515
|
input,
|
|
388
516
|
requestInit,
|
|
@@ -423,12 +551,12 @@ export function buildRequestHeaders(
|
|
|
423
551
|
const incomingBeta = requestHeaders.get("anthropic-beta") || "";
|
|
424
552
|
const { model, tools, messages, hasFileReferences } = parseRequestBodyMetadata(requestBody);
|
|
425
553
|
// Detect fast mode structurally from the already-transformed body string.
|
|
426
|
-
// transformRequestBody runs before buildRequestHeaders
|
|
427
|
-
//
|
|
428
|
-
//
|
|
429
|
-
//
|
|
430
|
-
//
|
|
431
|
-
//
|
|
554
|
+
// transformRequestBody runs before buildRequestHeaders in index.mjs, so speed:"fast" is
|
|
555
|
+
// already present when fast mode fired. This is now the ONLY call site of
|
|
556
|
+
// buildAnthropicBetaHeader (the pre-transform `computedBetaHeader` caller was deleted in
|
|
557
|
+
// Phase 4.1), and it is the correct one for fast-mode detection: it runs AFTER the body
|
|
558
|
+
// transform. The requestHeaders.set("anthropic-beta", mergedBetas) call below is what
|
|
559
|
+
// reaches the wire.
|
|
432
560
|
const fastModeActive = typeof requestBody === "string" && requestBody.includes('"speed":"fast"');
|
|
433
561
|
const mergedBetas = buildAnthropicBetaHeader(
|
|
434
562
|
incomingBeta,
|
|
@@ -440,7 +568,7 @@ export function buildRequestHeaders(
|
|
|
440
568
|
hasFileReferences,
|
|
441
569
|
adaptiveOverride,
|
|
442
570
|
tokenEconomy,
|
|
443
|
-
undefined, // microcompactBetas: not available at this call site (
|
|
571
|
+
undefined, // microcompactBetas: not available at this call site (the interceptor computes them)
|
|
444
572
|
fastModeActive, // NEW 12th param: emit fast-mode-2026-02-01 beta when body contains speed:"fast"
|
|
445
573
|
);
|
|
446
574
|
|