opencode-anthropic-fix 0.2.1 → 0.3.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 +24 -0
- package/index.mjs +155 -149
- package/lib/config.mjs +47 -25
- package/lib/mimicry/adapter-input.mjs +671 -0
- package/lib/mimicry/context-hint-threshold.mjs +127 -0
- package/lib/mimicry/headers.mjs +71 -64
- package/lib/mimicry/request-body.mjs +23 -22
- package/lib/mimicry/system-prompt.mjs +54 -23
- package/lib/mimicry/wire-compat.mjs +305 -0
- package/package.json +3 -3
- package/lib/account-state.test.mjs +0 -109
- package/lib/accounts.test.mjs +0 -912
- package/lib/adaptive-thinking-models.test.mjs +0 -30
- package/lib/backoff.test.mjs +0 -411
- package/lib/cc-credentials.test.mjs +0 -391
- package/lib/config.test.mjs +0 -764
- package/lib/context-hint-persist.mjs +0 -68
- package/lib/diagnose.test.mjs +0 -122
- package/lib/env.test.mjs +0 -25
- package/lib/haiku-call.test.mjs +0 -72
- package/lib/message-transform.test.mjs +0 -173
- package/lib/mimicry/cache.test.mjs +0 -112
- package/lib/mimicry/headers.test.mjs +0 -50
- package/lib/mimicry/models.test.mjs +0 -132
- package/lib/mimicry/request-body.test.mjs +0 -34
- package/lib/mimicry/request-helpers.test.mjs +0 -155
- package/lib/mimicry/response-stream.test.mjs +0 -127
- package/lib/mimicry/system-prompt.test.mjs +0 -73
- package/lib/oauth.test.mjs +0 -343
- package/lib/redact.test.mjs +0 -154
- package/lib/refresh-lock.test.mjs +0 -131
- package/lib/request-headers.test.mjs +0 -164
- package/lib/retry/overload-loop.test.mjs +0 -107
- package/lib/rotation.test.mjs +0 -392
- package/lib/session-metrics.test.mjs +0 -67
- package/lib/storage.test.mjs +0 -696
- package/lib/token-economy/microcompact.test.mjs +0 -28
- package/lib/token-economy/transforms.test.mjs +0 -192
- package/lib/tuning.test.mjs +0 -89
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code 2.1.195 context-hint token accounting.
|
|
3
|
+
*
|
|
4
|
+
* The native bundle calls these values Pac, gao, Acp, aNn, and Ecp,
|
|
5
|
+
* respectively. Its internal transcript checks `type === "assistant"`; the
|
|
6
|
+
* plugin receives the equivalent wire shape as `role === "assistant"`.
|
|
7
|
+
*
|
|
8
|
+
* Genuine Claude Code also filters tool uses through `Hcp.has(block.name)`.
|
|
9
|
+
* In Claude Code 2.1.195, `Hcp` contains Read, Bash, PowerShell, Grep, Glob,
|
|
10
|
+
* WebSearch, WebFetch, Edit, and Write.
|
|
11
|
+
*
|
|
12
|
+
* @typedef {{ type?: string, id?: string, name?: string, tool_use_id?: string, content?: unknown, text?: unknown }} ContextBlock
|
|
13
|
+
* @typedef {{ role?: string, content?: unknown }} ContextMessage
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export const KEPT_TOOL_RESULT_GROUPS = 5;
|
|
17
|
+
export const CONTEXT_HINT_TOKEN_THRESHOLD = 20_000;
|
|
18
|
+
export const MEDIA_BLOCK_TOKENS = 2_000;
|
|
19
|
+
export const CLEARED_MARKER = "[Old tool result content cleared]";
|
|
20
|
+
export const PERSISTED_PREFIX = "<persisted-output>";
|
|
21
|
+
export const CONTEXT_HINT_TOOL_NAMES = new Set([
|
|
22
|
+
"Read",
|
|
23
|
+
"Bash",
|
|
24
|
+
"PowerShell",
|
|
25
|
+
"Grep",
|
|
26
|
+
"Glob",
|
|
27
|
+
"WebSearch",
|
|
28
|
+
"WebFetch",
|
|
29
|
+
"Edit",
|
|
30
|
+
"Write",
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Genuine `If` estimator. This intentionally uses Math.round, unlike the
|
|
35
|
+
* plugin's other token estimators.
|
|
36
|
+
*
|
|
37
|
+
* @param {unknown} value
|
|
38
|
+
* @returns {number}
|
|
39
|
+
*/
|
|
40
|
+
export function estimateTextTokens(value) {
|
|
41
|
+
if (typeof value !== "string") return 0;
|
|
42
|
+
return Math.round(value.length / 4);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Genuine `Tcp` tool-result estimator.
|
|
47
|
+
*
|
|
48
|
+
* @param {ContextBlock} block
|
|
49
|
+
* @returns {number}
|
|
50
|
+
*/
|
|
51
|
+
export function estimateToolResultTokens(block) {
|
|
52
|
+
if (!block.content) return 0;
|
|
53
|
+
if (typeof block.content === "string") return estimateTextTokens(block.content);
|
|
54
|
+
if (!Array.isArray(block.content)) return 0;
|
|
55
|
+
|
|
56
|
+
return block.content.reduce((sum, item) => {
|
|
57
|
+
if (item?.type === "text") return sum + estimateTextTokens(item.text);
|
|
58
|
+
if (item?.type === "image" || item?.type === "document") return sum + MEDIA_BLOCK_TOKENS;
|
|
59
|
+
return sum;
|
|
60
|
+
}, 0);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Genuine `wcp` cleared-result check.
|
|
65
|
+
*
|
|
66
|
+
* @param {unknown} content
|
|
67
|
+
* @returns {boolean}
|
|
68
|
+
*/
|
|
69
|
+
export function isAlreadyCleared(content) {
|
|
70
|
+
return typeof content === "string" && (content === CLEARED_MARKER || content.startsWith(PERSISTED_PREFIX));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Genuine `vcp` collection order, adapted from the internal transcript's
|
|
75
|
+
* `type` discriminator to the wire message's `role` discriminator. Tool names
|
|
76
|
+
* are matched case-sensitively against Claude Code 2.1.195's native `Hcp` set.
|
|
77
|
+
*
|
|
78
|
+
* @param {ContextMessage[]} messages
|
|
79
|
+
* @returns {string[]}
|
|
80
|
+
*/
|
|
81
|
+
export function collectClearableToolUseIds(messages) {
|
|
82
|
+
const ids = [];
|
|
83
|
+
|
|
84
|
+
for (const message of messages) {
|
|
85
|
+
if (message?.role !== "assistant" || !Array.isArray(message.content)) continue;
|
|
86
|
+
for (const block of message.content) {
|
|
87
|
+
if (block?.type === "tool_use" && CONTEXT_HINT_TOOL_NAMES.has(block.name)) ids.push(block.id);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return ids;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Genuine `hao(messages, Pac)` saved-token calculation.
|
|
96
|
+
*
|
|
97
|
+
* @param {ContextMessage[]} messages
|
|
98
|
+
* @returns {number}
|
|
99
|
+
*/
|
|
100
|
+
export function computeContextHintTokensSaved(messages) {
|
|
101
|
+
const ids = collectClearableToolUseIds(messages);
|
|
102
|
+
const keep = Math.max(1, KEPT_TOOL_RESULT_GROUPS);
|
|
103
|
+
const keepSet = new Set(ids.slice(-keep));
|
|
104
|
+
const clearSet = new Set(ids.filter((id) => !keepSet.has(id)));
|
|
105
|
+
let tokensSaved = 0;
|
|
106
|
+
|
|
107
|
+
if (clearSet.size > 0) {
|
|
108
|
+
for (const message of messages) {
|
|
109
|
+
if (message?.role !== "user" || !Array.isArray(message.content)) continue;
|
|
110
|
+
for (const block of message.content) {
|
|
111
|
+
if (block?.type === "tool_result" && clearSet.has(block.tool_use_id) && !isAlreadyCleared(block.content)) {
|
|
112
|
+
tokensSaved += estimateToolResultTokens(block);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return tokensSaved;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* @param {ContextMessage[]} messages
|
|
123
|
+
* @returns {boolean}
|
|
124
|
+
*/
|
|
125
|
+
export function shouldEmitContextHintBody(messages) {
|
|
126
|
+
return computeContextHintTokensSaved(messages) >= CONTEXT_HINT_TOKEN_THRESHOLD;
|
|
127
|
+
}
|
package/lib/mimicry/headers.mjs
CHANGED
|
@@ -7,11 +7,6 @@ export const CLAUDE_CODE_BETA_FLAG = "claude-code-20250219";
|
|
|
7
7
|
export const _EFFORT_BETA_FLAG = "effort-2025-11-24";
|
|
8
8
|
export const FAST_MODE_BETA_FLAG = "fast-mode-2026-02-01";
|
|
9
9
|
export const TOKEN_COUNTING_BETA_FLAG = "token-counting-2024-11-01";
|
|
10
|
-
export const BEDROCK_UNSUPPORTED_BETAS = new Set([
|
|
11
|
-
"interleaved-thinking-2025-05-14",
|
|
12
|
-
"context-1m-2025-08-07",
|
|
13
|
-
"tool-search-tool-2025-10-19",
|
|
14
|
-
]);
|
|
15
10
|
export const HOST_SDK_BETAS_BLOCKLIST = new Set([
|
|
16
11
|
"fine-grained-tool-streaming-2025-05-14",
|
|
17
12
|
"structured-outputs-2025-11-13",
|
|
@@ -69,24 +64,6 @@ export function supportsWebSearch(model) {
|
|
|
69
64
|
return /claude|sonnet|opus|haiku|gpt|gemini/i.test(model);
|
|
70
65
|
}
|
|
71
66
|
|
|
72
|
-
export function detectProvider(requestUrl) {
|
|
73
|
-
// Match Claude Code provider precedence first (env-driven), then URL fallback.
|
|
74
|
-
if (isTruthyEnv(process.env.CLAUDE_CODE_USE_BEDROCK)) return "bedrock";
|
|
75
|
-
if (isTruthyEnv(process.env.CLAUDE_CODE_USE_FOUNDRY)) return "foundry";
|
|
76
|
-
if (isTruthyEnv(process.env.CLAUDE_CODE_USE_ANTHROPIC_AWS)) return "anthropicAws";
|
|
77
|
-
if (isTruthyEnv(process.env.CLAUDE_CODE_USE_MANTLE)) return "mantle";
|
|
78
|
-
if (isTruthyEnv(process.env.CLAUDE_CODE_USE_VERTEX)) return "vertex";
|
|
79
|
-
|
|
80
|
-
if (!requestUrl) return "anthropic";
|
|
81
|
-
const host = requestUrl.hostname.toLowerCase();
|
|
82
|
-
if (host.includes("mantle")) return "mantle";
|
|
83
|
-
if (host.includes("anthropicaws")) return "anthropicAws";
|
|
84
|
-
if (host.includes("bedrock") || host.includes("amazonaws.com")) return "bedrock";
|
|
85
|
-
if (host.includes("aiplatform") || host.includes("vertex")) return "vertex";
|
|
86
|
-
if (host.includes("foundry") || host.includes("azure")) return "foundry";
|
|
87
|
-
return "anthropic";
|
|
88
|
-
}
|
|
89
|
-
|
|
90
67
|
export function parseRequestBodyMetadata(body, parsedBody) {
|
|
91
68
|
const parsed =
|
|
92
69
|
parsedBody ||
|
|
@@ -110,29 +87,75 @@ export function parseRequestBodyMetadata(body, parsedBody) {
|
|
|
110
87
|
return { model, tools, messages, hasFileReferences };
|
|
111
88
|
}
|
|
112
89
|
|
|
90
|
+
/**
|
|
91
|
+
* Walk every object that may carry a stainless helper marker: a tool object, a
|
|
92
|
+
* message object, and each block nested under `.content`. Shared by the header
|
|
93
|
+
* builder and the body stripper on purpose — what is READ to build
|
|
94
|
+
* `x-stainless-helper` and what is REMOVED from the body must be the exact same
|
|
95
|
+
* traversal, or the two would drift and a marker would leak to the wire.
|
|
96
|
+
*
|
|
97
|
+
* @param {unknown[]} tools
|
|
98
|
+
* @param {unknown[]} messages
|
|
99
|
+
* @param {(carrier: Record<string, unknown>) => void} visit
|
|
100
|
+
*/
|
|
101
|
+
function walkStainlessHelperCarriers(tools, messages, visit) {
|
|
102
|
+
const walk = (value) => {
|
|
103
|
+
if (!value || typeof value !== "object") return;
|
|
104
|
+
|
|
105
|
+
visit(value);
|
|
106
|
+
|
|
107
|
+
if (Array.isArray(value.content)) {
|
|
108
|
+
for (const contentBlock of value.content) {
|
|
109
|
+
walk(contentBlock);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
if (Array.isArray(tools)) for (const tool of tools) walk(tool);
|
|
115
|
+
if (Array.isArray(messages)) for (const message of messages) walk(message);
|
|
116
|
+
}
|
|
117
|
+
|
|
113
118
|
export function buildStainlessHelperHeader(tools, messages) {
|
|
114
119
|
const helpers = new Set();
|
|
115
120
|
|
|
116
|
-
|
|
117
|
-
if (!value || typeof value !== "object") return;
|
|
118
|
-
|
|
121
|
+
walkStainlessHelperCarriers(tools, messages, (value) => {
|
|
119
122
|
for (const key of STAINLESS_HELPER_KEYS) {
|
|
120
123
|
if (typeof value[key] === "string" && value[key]) {
|
|
121
124
|
helpers.add(value[key]);
|
|
122
125
|
}
|
|
123
126
|
}
|
|
127
|
+
});
|
|
124
128
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
129
|
+
return Array.from(helpers).join(", ");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Remove the stainless helper markers from the request body, in place.
|
|
134
|
+
*
|
|
135
|
+
* The marker is an INTERNAL signal whose only purpose is to compute the
|
|
136
|
+
* `x-stainless-helper` header. It is not part of the Anthropic wire contract:
|
|
137
|
+
* the API has never known these keys, and the shared wire-compat package
|
|
138
|
+
* rejects the whole request with `INVALID_INPUT` when one reaches it. Call this
|
|
139
|
+
* AFTER `buildStainlessHelperHeader` so the header is still derived from the
|
|
140
|
+
* untouched body, and before the body is handed to the package or to `fetch`.
|
|
141
|
+
*
|
|
142
|
+
* @param {unknown[]} tools
|
|
143
|
+
* @param {unknown[]} messages
|
|
144
|
+
* @returns {number} how many marker keys were removed
|
|
145
|
+
*/
|
|
146
|
+
export function stripStainlessHelperMarkers(tools, messages) {
|
|
147
|
+
let removed = 0;
|
|
148
|
+
|
|
149
|
+
walkStainlessHelperCarriers(tools, messages, (value) => {
|
|
150
|
+
for (const key of STAINLESS_HELPER_KEYS) {
|
|
151
|
+
if (key in value) {
|
|
152
|
+
delete value[key];
|
|
153
|
+
removed += 1;
|
|
128
154
|
}
|
|
129
155
|
}
|
|
130
|
-
};
|
|
156
|
+
});
|
|
131
157
|
|
|
132
|
-
|
|
133
|
-
for (const message of messages) collect(message);
|
|
134
|
-
|
|
135
|
-
return Array.from(helpers).join(", ");
|
|
158
|
+
return removed;
|
|
136
159
|
}
|
|
137
160
|
|
|
138
161
|
export function isEffortCapableModel(model) {
|
|
@@ -145,7 +168,6 @@ export function buildAnthropicBetaHeader(
|
|
|
145
168
|
incomingBeta,
|
|
146
169
|
signatureEnabled,
|
|
147
170
|
model,
|
|
148
|
-
provider,
|
|
149
171
|
customBetas,
|
|
150
172
|
strategy,
|
|
151
173
|
requestPath,
|
|
@@ -226,17 +248,16 @@ export function buildAnthropicBetaHeader(
|
|
|
226
248
|
|
|
227
249
|
// === CONDITIONAL BETAS (model/context-dependent) ===
|
|
228
250
|
|
|
229
|
-
//
|
|
230
|
-
//
|
|
231
|
-
//
|
|
232
|
-
const isFirstPartyProvider = provider !== "vertex" && provider !== "bedrock" && provider !== "mantle";
|
|
251
|
+
// The plugin is first-party OAuth only; Bedrock/Vertex/Foundry/Mantle support
|
|
252
|
+
// was removed, so the former isFirstPartyProvider predicate is always true and
|
|
253
|
+
// the gates below no longer test it.
|
|
233
254
|
|
|
234
255
|
// v2.1.195: context-management-2025-06-27 is default-ON for first-party non-claude-3
|
|
235
256
|
// models (incl. Haiku 4.5), mirroring CC's n0d(model) eligibility path. Earlier
|
|
236
257
|
// analyses read only the separate USE_API_CONTEXT_MANAGEMENT env term (hardcoded
|
|
237
258
|
// && false) and recorded it as off; 2.1.195 confirms it ships by default for modern
|
|
238
259
|
// first-party models. Opt out via token_economy.context_management = false.
|
|
239
|
-
if (te.context_management !== false &&
|
|
260
|
+
if (te.context_management !== false && !CLAUDE_3_MODEL_RE.test(model)) {
|
|
240
261
|
betas.push("context-management-2025-06-27");
|
|
241
262
|
}
|
|
242
263
|
|
|
@@ -244,11 +265,12 @@ export function buildAnthropicBetaHeader(
|
|
|
244
265
|
// (Opus 4.5/4.6/4.7/4.8, Sonnet 4.6), mirroring CC's Kw(model). Excluded for
|
|
245
266
|
// claude-3-*, opus-4-0, opus-4-1, sonnet-4-0, sonnet-4-5, haiku-4-5. First-party
|
|
246
267
|
// only; opt out via token_economy.effort = false.
|
|
247
|
-
if (te.effort !== false &&
|
|
268
|
+
if (te.effort !== false && isEffortCapableModel(model)) {
|
|
248
269
|
betas.push(_EFFORT_BETA_FLAG);
|
|
249
270
|
}
|
|
250
271
|
|
|
251
|
-
//
|
|
272
|
+
// structured-outputs is gated by the caller supplying an output format.
|
|
273
|
+
// `tengu_tool_pear` instead gates `tool.strict = true` on the tool-schema path.
|
|
252
274
|
if (te.structured_outputs && supportsStructuredOutputs(model)) {
|
|
253
275
|
betas.push("structured-outputs-2025-12-15");
|
|
254
276
|
}
|
|
@@ -266,21 +288,13 @@ export function buildAnthropicBetaHeader(
|
|
|
266
288
|
betas.push("advisor-tool-2026-03-01");
|
|
267
289
|
}
|
|
268
290
|
|
|
269
|
-
// context-hint-2026-04-09
|
|
270
|
-
//
|
|
271
|
-
//
|
|
272
|
-
//
|
|
273
|
-
//
|
|
274
|
-
//
|
|
275
|
-
//
|
|
276
|
-
// token_economy.context_hint = false.
|
|
277
|
-
// Mimicry note: real CC gates context-hint on querySource.startsWith("repl_main_thread").
|
|
278
|
-
// We infer main-thread from body shape via classifyRequestRole and pass it via
|
|
279
|
-
// tokenEconomy.__requestRole. Treat absent marker as "main" for backward compat.
|
|
280
|
-
const _isMainThread = te.__requestRole == null || te.__requestRole === "main";
|
|
281
|
-
if (isFirstPartyProvider && !CLAUDE_3_MODEL_RE.test(model) && te.context_hint !== false && _isMainThread) {
|
|
282
|
-
betas.push("context-hint-2026-04-09");
|
|
283
|
-
}
|
|
291
|
+
// context-hint-2026-04-09 is NOT emitted, on any path, under any knob.
|
|
292
|
+
// The profile derived from the real Claude Code 2.1.195 binary sends neither
|
|
293
|
+
// this beta nor its paired `context_hint` body field. Emitting either makes
|
|
294
|
+
// the request distinguishable from the genuine client — the exact opposite of
|
|
295
|
+
// what this plugin exists to do. `token_economy.context_hint` is therefore
|
|
296
|
+
// deprecated rather than honoured: validateConfig warns on explicit opt-in so
|
|
297
|
+
// the switch is not a silent no-op. See lib/config.mjs.
|
|
284
298
|
|
|
285
299
|
// Files API — scoped to file endpoints/references
|
|
286
300
|
if (isFilesEndpoint || hasFileReferences) {
|
|
@@ -353,11 +367,6 @@ export function buildAnthropicBetaHeader(
|
|
|
353
367
|
mergedBetas = mergedBetas.filter((beta) => !EXPERIMENTAL_BETA_FLAGS.has(beta));
|
|
354
368
|
}
|
|
355
369
|
|
|
356
|
-
// Remove betas unsupported by Bedrock
|
|
357
|
-
if (provider === "bedrock") {
|
|
358
|
-
mergedBetas = mergedBetas.filter((beta) => !BEDROCK_UNSUPPORTED_BETAS.has(beta));
|
|
359
|
-
}
|
|
360
|
-
|
|
361
370
|
return mergedBetas.join(",");
|
|
362
371
|
}
|
|
363
372
|
|
|
@@ -413,7 +422,6 @@ export function buildRequestHeaders(
|
|
|
413
422
|
// Preserve all incoming beta headers while ensuring OAuth requirements
|
|
414
423
|
const incomingBeta = requestHeaders.get("anthropic-beta") || "";
|
|
415
424
|
const { model, tools, messages, hasFileReferences } = parseRequestBodyMetadata(requestBody);
|
|
416
|
-
const provider = detectProvider(requestUrl);
|
|
417
425
|
// Detect fast mode structurally from the already-transformed body string.
|
|
418
426
|
// transformRequestBody runs before buildRequestHeaders (see index.mjs:2986 vs 3039),
|
|
419
427
|
// so speed:"fast" is already present when fast mode fired. Same detection idiom as the
|
|
@@ -426,7 +434,6 @@ export function buildRequestHeaders(
|
|
|
426
434
|
incomingBeta,
|
|
427
435
|
signature.enabled,
|
|
428
436
|
model,
|
|
429
|
-
provider,
|
|
430
437
|
signature.customBetas,
|
|
431
438
|
signature.strategy,
|
|
432
439
|
requestUrl?.pathname,
|
|
@@ -57,7 +57,7 @@ export const CORE_TOOL_NAMES = new Set([
|
|
|
57
57
|
* Preserves behaviors E1-E7.
|
|
58
58
|
*
|
|
59
59
|
* @param {string | undefined} body
|
|
60
|
-
* @param {{enabled: boolean, claudeCliVersion: string, promptCompactionMode: 'minimal' | 'off', provider?: string}} signature
|
|
60
|
+
* @param {{enabled: boolean, claudeCliVersion: string, promptCompactionMode: 'minimal' | 'off', provider?: string, useAdapter?: boolean}} signature
|
|
61
61
|
* @param {{persistentUserId: string, sessionId: string, accountId: string, turns?: number, usedTools?: Set<string>}} runtime
|
|
62
62
|
* @param {string} [betaHeader] - Pre-computed anthropic-beta header value to inject into the body.
|
|
63
63
|
* @param {import('../config.mjs').AnthropicAuthConfig} [config] - Plugin configuration for output cap
|
|
@@ -87,17 +87,10 @@ export function transformRequestBody(body, signature, runtime, betaHeader, confi
|
|
|
87
87
|
if (config?.output_cap?.enabled) {
|
|
88
88
|
parsed.max_tokens = resolveMaxTokens(parsed, config);
|
|
89
89
|
}
|
|
90
|
-
//
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
|
|
94
|
-
const betaArray = betaHeader
|
|
95
|
-
.split(",")
|
|
96
|
-
.map((b) => b.trim())
|
|
97
|
-
.filter(Boolean)
|
|
98
|
-
.filter((b) => b !== "oauth-2025-04-20");
|
|
99
|
-
parsed.anthropic_beta = betaArray;
|
|
100
|
-
}
|
|
90
|
+
// The body-level `anthropic_beta` array existed only for Bedrock, which does
|
|
91
|
+
// not forward custom HTTP headers. Multi-provider support was removed, and
|
|
92
|
+
// the first-party API rejects betas in the body with "Extra inputs are not
|
|
93
|
+
// permitted" — they are header-only here.
|
|
101
94
|
// Strip any incoming "betas" field — API rejects it as unknown
|
|
102
95
|
if (Object.prototype.hasOwnProperty.call(parsed, "betas")) {
|
|
103
96
|
delete parsed.betas;
|
|
@@ -330,7 +323,7 @@ export function transformRequestBody(body, signature, runtime, betaHeader, confi
|
|
|
330
323
|
requestRole: runtime?.requestRole,
|
|
331
324
|
leanNonMain: config?.token_economy?.lean_system_non_main === true,
|
|
332
325
|
// Simple-system-prompt mode for Opus 4.7+ (CC v2.1.133+ parity, gated by
|
|
333
|
-
// GrowthBook `
|
|
326
|
+
// GrowthBook `tengu_velvet_cascade` in real CC). Plugin gate: model
|
|
334
327
|
// eligibility + opt-in flag. See buildSystemPromptBlocks for what it
|
|
335
328
|
// strips (anti-verbosity boilerplate only; identity/billing untouched).
|
|
336
329
|
simpleSystemPrompt: config?.token_economy?.simple_system_prompt === true,
|
|
@@ -338,6 +331,11 @@ export function transformRequestBody(body, signature, runtime, betaHeader, confi
|
|
|
338
331
|
// Mirrors real CC's `--workload <tag>` CLI flag (process-scoped, used by
|
|
339
332
|
// SDK daemon callers spawning cron subprocesses). Empty string -> omit.
|
|
340
333
|
workload: typeof config?.signature_emulation?.workload === "string" ? config.signature_emulation.workload : "",
|
|
334
|
+
// Adapter path only: suppress the canonical billing + identity blocks
|
|
335
|
+
// because the shared package prepends its own. Every other stage of the
|
|
336
|
+
// system pipeline (anti-verbosity, subagent CC-prefix, title-generator
|
|
337
|
+
// swap, dedupe, compaction, tailing, token-budget block) still runs.
|
|
338
|
+
suppressCanonicalBlocks: signature.useAdapter === true,
|
|
341
339
|
};
|
|
342
340
|
// Sanitize system prompt and optionally inject Claude Code identity/billing blocks.
|
|
343
341
|
parsed.system = buildSystemPromptBlocks(normalizeSystemTextBlocks(parsed.system), signatureWithModel);
|
|
@@ -377,7 +375,13 @@ export function transformRequestBody(body, signature, runtime, betaHeader, confi
|
|
|
377
375
|
}
|
|
378
376
|
}
|
|
379
377
|
|
|
380
|
-
|
|
378
|
+
// Adapter path only: the shared package composes `metadata.user_id` itself
|
|
379
|
+
// from the runtime correlation triple, and the two env features become
|
|
380
|
+
// `metadataOverrides` on the transport instead (see adapter-input.mjs).
|
|
381
|
+
// Writing metadata here as well would collide with the package's own
|
|
382
|
+
// composition. CONDITIONAL, never unconditional: the legacy path still
|
|
383
|
+
// needs buildRequestMetadata.
|
|
384
|
+
if (signature.enabled && signature.useAdapter !== true) {
|
|
381
385
|
const currentMetadata =
|
|
382
386
|
parsed.metadata && typeof parsed.metadata === "object" && !Array.isArray(parsed.metadata)
|
|
383
387
|
? parsed.metadata
|
|
@@ -585,14 +589,11 @@ export function transformRequestBody(body, signature, runtime, betaHeader, confi
|
|
|
585
589
|
}
|
|
586
590
|
}
|
|
587
591
|
|
|
588
|
-
//
|
|
589
|
-
// the
|
|
590
|
-
//
|
|
591
|
-
//
|
|
592
|
-
//
|
|
593
|
-
if (betaHeader && betaHeader.includes("context-hint-2026-04-09") && !parsed.context_hint) {
|
|
594
|
-
parsed.context_hint = { enabled: true };
|
|
595
|
-
}
|
|
592
|
+
// The `context_hint` body field is never injected, on either path. The
|
|
593
|
+
// profile derived from the real Claude Code 2.1.195 binary does not send it
|
|
594
|
+
// (nor the paired context-hint-2026-04-09 beta), so emitting it would be a
|
|
595
|
+
// fingerprint. `token_economy.context_hint` is deprecated in lib/config.mjs,
|
|
596
|
+
// which warns on explicit opt-in rather than silently doing nothing.
|
|
596
597
|
|
|
597
598
|
// Fast mode: inject speed parameter for fast-mode-eligible models.
|
|
598
599
|
// Per Anthropic fast-mode docs, `speed: "fast"` (beta fast-mode-2026-02-01) is
|
|
@@ -6,6 +6,8 @@ const BILLING_HASH_SALT = "59cf53e54c78";
|
|
|
6
6
|
const BILLING_HASH_INDICES = [4, 7, 20];
|
|
7
7
|
|
|
8
8
|
export const CLAUDE_CODE_IDENTITY_STRING = "You are Claude Code, Anthropic's official CLI for Claude.";
|
|
9
|
+
/** Marker that identifies the canonical billing block inside a system array. */
|
|
10
|
+
export const BILLING_HEADER_PREFIX = "x-anthropic-billing-header:";
|
|
9
11
|
const KNOWN_IDENTITY_STRINGS = new Set([
|
|
10
12
|
CLAUDE_CODE_IDENTITY_STRING,
|
|
11
13
|
"You are Claude Code, Anthropic's official CLI for Claude, running within the Claude Agent SDK.",
|
|
@@ -25,11 +27,12 @@ const COMPACT_TITLE_GENERATOR_SYSTEM_PROMPT = [
|
|
|
25
27
|
|
|
26
28
|
/**
|
|
27
29
|
* Anti-verbosity system prompt text.
|
|
28
|
-
*
|
|
30
|
+
* Text verified against the CC v2.1.195 binary; the heading corrects a stale
|
|
31
|
+
* v2.1.100 value.
|
|
29
32
|
* Significantly reduces output token count by instructing the model to be concise.
|
|
30
33
|
*/
|
|
31
34
|
const ANTI_VERBOSITY_SYSTEM_PROMPT = [
|
|
32
|
-
"#
|
|
35
|
+
"# Text output (does not apply to tool calls)",
|
|
33
36
|
"Assume users can't see most tool calls or thinking — only your text output. Before your first tool call, state in one sentence what you're about to do. While working, give short updates at key moments: when you find something, when you change direction, or when you hit a blocker. Brief is good — silent is not. One sentence per update is almost always enough.",
|
|
34
37
|
"",
|
|
35
38
|
"Don't narrate your internal deliberation. User-facing text should be relevant communication to the user, not a running commentary on your thought process. State results and decisions directly, and focus user-facing text on relevant updates for the user.",
|
|
@@ -43,13 +46,6 @@ const ANTI_VERBOSITY_SYSTEM_PROMPT = [
|
|
|
43
46
|
"In code: default to writing no comments. Never write multi-paragraph docstrings or multi-line comment blocks — one short line max. Don't create planning, decision, or analysis documents unless the user asks for them — work from conversation context, not intermediate files.",
|
|
44
47
|
].join("\n");
|
|
45
48
|
|
|
46
|
-
/**
|
|
47
|
-
* Numeric length anchors text.
|
|
48
|
-
* Extracted from CC v2.1.100. Hard word-count limits for output.
|
|
49
|
-
*/
|
|
50
|
-
const NUMERIC_LENGTH_ANCHORS_PROMPT =
|
|
51
|
-
"Length limits: keep text between tool calls to ≤25 words. Keep final responses to ≤100 words unless the task requires more detail.";
|
|
52
|
-
|
|
53
49
|
// Max system prompt length that passes CC billing validation.
|
|
54
50
|
// The server pattern-matches the system prompt against the real CC prompt.
|
|
55
51
|
// Opencode's customizations after ~5800 chars diverge and trigger extra usage billing.
|
|
@@ -98,7 +94,7 @@ export function computeBillingCacheHash(firstUserMessage, version) {
|
|
|
98
94
|
|
|
99
95
|
/**
|
|
100
96
|
* Models eligible for the "simple system prompt" mode that real CC ships in
|
|
101
|
-
* v2.1.133+ under GrowthBook flag `
|
|
97
|
+
* v2.1.133+ under GrowthBook flag `tengu_velvet_cascade` (or forced via
|
|
102
98
|
* `CLAUDE_CODE_SIMPLE_SYSTEM_PROMPT=1`).
|
|
103
99
|
*
|
|
104
100
|
* Real CC's `sR9` matches: `claude-opus-4-7`, any model with `-eap` (early
|
|
@@ -112,7 +108,7 @@ export function isSimpleSystemPromptEligible(model) {
|
|
|
112
108
|
if (!model) return false;
|
|
113
109
|
if (isOpus47Model(model) || isOpus48Model(model)) return true;
|
|
114
110
|
// -eap suffix variant, e.g. "claude-opus-4-7-eap" or "claude-opus-4-7-eap[1m]"
|
|
115
|
-
if (/-eap(
|
|
111
|
+
if (/-eap($|\[)/i.test(model)) return true;
|
|
116
112
|
return false;
|
|
117
113
|
}
|
|
118
114
|
|
|
@@ -126,12 +122,11 @@ export function isSimpleSystemPromptEligible(model) {
|
|
|
126
122
|
*
|
|
127
123
|
* @param {string} version - CLI version (e.g., "2.1.97")
|
|
128
124
|
* @param {string} [firstUserMessage] - First user message text for fingerprint computation
|
|
129
|
-
* @param {string} [provider] - API provider ("anthropic" | "bedrock" | "vertex" | "foundry" | "anthropicAws" | "mantle")
|
|
130
125
|
* @param {string} [workloadOverride] - Explicit workload tag from config (`signature_emulation.workload`).
|
|
131
126
|
* Takes precedence over `CLAUDE_CODE_WORKLOAD` env var. Mirrors real CC's `--workload <tag>` flag.
|
|
132
127
|
* @returns {string}
|
|
133
128
|
*/
|
|
134
|
-
export function buildAnthropicBillingHeader(version, firstUserMessage,
|
|
129
|
+
export function buildAnthropicBillingHeader(version, firstUserMessage, workloadOverride) {
|
|
135
130
|
if (isFalsyEnv(process.env.CLAUDE_CODE_ATTRIBUTION_HEADER)) return "";
|
|
136
131
|
// Real CC sends cc_entrypoint=cli (confirmed via proxy capture).
|
|
137
132
|
const entrypoint = process.env.CLAUDE_CODE_ENTRYPOINT || "cli";
|
|
@@ -144,15 +139,15 @@ export function buildAnthropicBillingHeader(version, firstUserMessage, provider,
|
|
|
144
139
|
const ccVersion = `${version}.${fingerprint}`;
|
|
145
140
|
// cch: v2.1.97 sends static "cch=00000" — xxHash64 attestation was removed.
|
|
146
141
|
// The server uses the PRESENCE of cch=00000 as a CC identification signal.
|
|
147
|
-
|
|
148
|
-
|
|
142
|
+
// The plugin is first-party only, so cch is always emitted; the former
|
|
143
|
+
// bedrock/anthropicAws/mantle suppression went away with multi-provider support.
|
|
144
|
+
const cchPart = " cch=00000;";
|
|
149
145
|
// Build workload part (upstream concatenates directly, no regex replace).
|
|
150
146
|
// Config wins over env so `signature_emulation.workload` is portable across hosts.
|
|
151
147
|
let workloadPart = "";
|
|
152
148
|
const workload = workloadOverride || process.env.CLAUDE_CODE_WORKLOAD;
|
|
153
|
-
if (workload
|
|
149
|
+
if (workload) {
|
|
154
150
|
// QA fix M5: sanitize workload value to prevent header injection.
|
|
155
|
-
// Real CC skips cc_workload on bedrock/anthropicAws/mantle (same gate as cch).
|
|
156
151
|
const safeWorkload = String(workload).replace(/[;\s\r\n]/g, "_");
|
|
157
152
|
if (safeWorkload) workloadPart = ` cc_workload=${safeWorkload};`;
|
|
158
153
|
}
|
|
@@ -550,7 +545,7 @@ export function buildSystemPromptBlocks(system, signature) {
|
|
|
550
545
|
// Anti-verbosity injection (CC v2.1.100 quiet_salted_ember equivalent).
|
|
551
546
|
// Applies to Opus 4.6 / 4.7 for non-title-generator requests.
|
|
552
547
|
//
|
|
553
|
-
// Simple-system-prompt gate (CC v2.1.133+ `
|
|
548
|
+
// Simple-system-prompt gate (CC v2.1.133+ `tengu_velvet_cascade` equivalent):
|
|
554
549
|
// when `signature.simpleSystemPrompt` is true AND the model is eligible
|
|
555
550
|
// (Opus 4.7, -eap variants), skip the anti-verbosity boilerplate. Real CC's
|
|
556
551
|
// simple-prompt mode strips this kind of post-hoc behavioral hand-holding on
|
|
@@ -570,9 +565,6 @@ export function buildSystemPromptBlocks(system, signature) {
|
|
|
570
565
|
if (avConfig?.enabled !== false) {
|
|
571
566
|
sanitized.push({ type: "text", text: ANTI_VERBOSITY_SYSTEM_PROMPT });
|
|
572
567
|
}
|
|
573
|
-
if (avConfig?.length_anchors !== false) {
|
|
574
|
-
sanitized.push({ type: "text", text: NUMERIC_LENGTH_ANCHORS_PROMPT });
|
|
575
|
-
}
|
|
576
568
|
}
|
|
577
569
|
|
|
578
570
|
if (!signature.enabled) {
|
|
@@ -598,7 +590,6 @@ export function buildSystemPromptBlocks(system, signature) {
|
|
|
598
590
|
const billingHeader = buildAnthropicBillingHeader(
|
|
599
591
|
signature.claudeCliVersion,
|
|
600
592
|
signature.firstUserMessage,
|
|
601
|
-
signature.provider,
|
|
602
593
|
signature.workload,
|
|
603
594
|
);
|
|
604
595
|
|
|
@@ -617,9 +608,21 @@ export function buildSystemPromptBlocks(system, signature) {
|
|
|
617
608
|
// Run the real CC splitSysPromptPrefix algorithm to get blocks with cacheScope
|
|
618
609
|
const scopedBlocks = splitSysPromptPrefix(sanitized, billingHeader || undefined, identityString, useBoundaryMode);
|
|
619
610
|
|
|
611
|
+
// Adapter path only: the shared package prepends its OWN billing block
|
|
612
|
+
// (index 0) and identity block (index 1), each with its own cache_control
|
|
613
|
+
// marker. Emitting ours too would duplicate both blocks on the wire.
|
|
614
|
+
//
|
|
615
|
+
// This is CONDITIONAL, never unconditional: the legacy path (any request the
|
|
616
|
+
// signature-emulation adapter declines — non-Anthropic provider, or emulation
|
|
617
|
+
// off) still builds the request through buildRequestHeaders and depends on
|
|
618
|
+
// these two blocks being present. Dropping them for every caller would break
|
|
619
|
+
// that path silently — no error, just a wrong request on the wire.
|
|
620
|
+
const emittedBlocks =
|
|
621
|
+
signature.suppressCanonicalBlocks === true ? dropCanonicalPrefixBlocks(scopedBlocks, identityString) : scopedBlocks;
|
|
622
|
+
|
|
620
623
|
// Convert scoped blocks to wire format using getCacheControlForScope
|
|
621
624
|
// (mirrors real CC buildSystemPromptBlocks → map + getCacheControl)
|
|
622
|
-
return
|
|
625
|
+
return emittedBlocks.map((block) => {
|
|
623
626
|
const cc = getCacheControlForScope(block.cacheScope, effectiveCachePolicy);
|
|
624
627
|
return {
|
|
625
628
|
type: "text",
|
|
@@ -629,6 +632,34 @@ export function buildSystemPromptBlocks(system, signature) {
|
|
|
629
632
|
});
|
|
630
633
|
}
|
|
631
634
|
|
|
635
|
+
/**
|
|
636
|
+
* Drop the two canonical prefix blocks the shared package prepends itself.
|
|
637
|
+
*
|
|
638
|
+
* Both are matched with the SAME predicates `splitSysPromptPrefix` uses to
|
|
639
|
+
* recognise them on input, and only the FIRST occurrence of each is removed, so
|
|
640
|
+
* a caller-supplied block that happens to repeat the identity string survives.
|
|
641
|
+
*
|
|
642
|
+
* @param {{text: string, cacheScope: string | null}[]} blocks
|
|
643
|
+
* @param {string} identityString
|
|
644
|
+
* @returns {{text: string, cacheScope: string | null}[]}
|
|
645
|
+
*/
|
|
646
|
+
function dropCanonicalPrefixBlocks(blocks, identityString) {
|
|
647
|
+
let billingDropped = false;
|
|
648
|
+
let identityDropped = false;
|
|
649
|
+
return blocks.filter((block) => {
|
|
650
|
+
const text = typeof block.text === "string" ? block.text : "";
|
|
651
|
+
if (!billingDropped && text.startsWith(BILLING_HEADER_PREFIX)) {
|
|
652
|
+
billingDropped = true;
|
|
653
|
+
return false;
|
|
654
|
+
}
|
|
655
|
+
if (!identityDropped && text === identityString) {
|
|
656
|
+
identityDropped = true;
|
|
657
|
+
return false;
|
|
658
|
+
}
|
|
659
|
+
return true;
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
|
|
632
663
|
export function getCachedCCPrompt() {
|
|
633
664
|
return cachedCCPrompt;
|
|
634
665
|
}
|