opencode-anthropic-fix 2.0.0 → 2.1.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 +203 -26
- package/lib/accounts.mjs +30 -0
- package/lib/betas.mjs +1 -1
- package/lib/diagnose.mjs +8 -1
- package/lib/mimicry/adapter-input.mjs +177 -15
- package/lib/mimicry/headers.mjs +1 -1
- package/lib/mimicry/response-stream.mjs +816 -112
- package/lib/mimicry/system-prompt.mjs +20 -10
- package/lib/mimicry/wire-compat.mjs +132 -15
- package/lib/rolling-summarizer.mjs +20 -5
- package/lib/unicode-text.mjs +111 -0
- package/package.json +4 -2
|
@@ -30,7 +30,7 @@ import {
|
|
|
30
30
|
supportsStructuredOutputs,
|
|
31
31
|
supportsWebSearch,
|
|
32
32
|
WIRE_PROFILE,
|
|
33
|
-
|
|
33
|
+
BETA_REGISTRY_2_1_280,
|
|
34
34
|
} from "./wire-compat.mjs";
|
|
35
35
|
import { BETA_SHORTCUTS, EXPERIMENTAL_BETA_FLAGS } from "../betas.mjs";
|
|
36
36
|
import { resolveCacheTtl } from "./cache.mjs";
|
|
@@ -149,7 +149,145 @@ export const MAX_SUPPRESS_BETAS = 32;
|
|
|
149
149
|
* registry through the seam rather than re-typed here: if the package ever
|
|
150
150
|
* re-dates the beta, the suppression keeps matching what the package composes.
|
|
151
151
|
*/
|
|
152
|
-
export const PROMPT_CACHING_SCOPE_BETA =
|
|
152
|
+
export const PROMPT_CACHING_SCOPE_BETA = BETA_REGISTRY_2_1_280.PROMPT_CACHING_SCOPE.header;
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Betas the session-rejected-beta latch may NEVER suppress, whatever an error
|
|
156
|
+
* body names. `suppressBetas` is unguarded package-side ("Suppressing
|
|
157
|
+
* `oauth-2025-04-20` produces a request the API rejects with 401"), so the guard
|
|
158
|
+
* lives here: OAuth first, and the Claude Code identity beta stays on the wire.
|
|
159
|
+
* Applies only to the rejected-beta input, never to the policy suppressions.
|
|
160
|
+
*/
|
|
161
|
+
export const UNSUPPRESSIBLE_BETAS = new Set([BETA_REGISTRY_2_1_280.OAUTH_AUTH.header, CLAUDE_CODE_BETA_FLAG]);
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Betas whose presence is COUPLED to a request-body field, so the rejected-beta
|
|
165
|
+
* latch must never suppress them: `suppressBetas` only filters the header, and
|
|
166
|
+
* the package (or the plugin's own body transform) still writes the paired body
|
|
167
|
+
* field, producing a request that is neither genuine nor valid. For latching
|
|
168
|
+
* purposes they are treated like `UNSUPPRESSIBLE_BETAS`: a rejection naming one
|
|
169
|
+
* surfaces as before. Headers are read from the package registry through the
|
|
170
|
+
* seam. The pairings, verified against wire-compat 0.7.0 `dist/betas.js` and
|
|
171
|
+
* `dist/request-body.js` (plus lib/mimicry/request-body.mjs for task budgets):
|
|
172
|
+
*
|
|
173
|
+
* - thinking-display-updates -> `thinking.display: "updates"` (site 12b pair)
|
|
174
|
+
* - context-management -> `context_management`
|
|
175
|
+
* - effort -> `output_config.effort`
|
|
176
|
+
* - structured-outputs -> `output_format`
|
|
177
|
+
* - fast-mode (SPEED) -> `speed`
|
|
178
|
+
* - task-budgets -> `output_config` (plugin body transform)
|
|
179
|
+
* - context-hint -> `context_hint`
|
|
180
|
+
* - extended-cache-ttl -> `cache_control.ttl: "1h"` (package `cacheTtl`)
|
|
181
|
+
* - files-api -> file references in `messages`
|
|
182
|
+
* - mcp-servers -> `mcp_servers` (experimental body passthrough)
|
|
183
|
+
*
|
|
184
|
+
* `thinking-binding-controls` is NOT listed: the package emits it header-only
|
|
185
|
+
* (its `thinking.block_binding` field is never written).
|
|
186
|
+
*/
|
|
187
|
+
export const BODY_COUPLED_BETAS = new Set([
|
|
188
|
+
BETA_REGISTRY_2_1_280.THINKING_DISPLAY_UPDATES.header,
|
|
189
|
+
BETA_REGISTRY_2_1_280.CONTEXT_MANAGEMENT.header,
|
|
190
|
+
BETA_REGISTRY_2_1_280.EFFORT.header,
|
|
191
|
+
BETA_REGISTRY_2_1_280.STRUCTURED_OUTPUTS.header,
|
|
192
|
+
BETA_REGISTRY_2_1_280.SPEED.header,
|
|
193
|
+
BETA_REGISTRY_2_1_280.TASK_BUDGETS.header,
|
|
194
|
+
BETA_REGISTRY_2_1_280.CONTEXT_HINT.header,
|
|
195
|
+
BETA_REGISTRY_2_1_280.EXTENDED_CACHE_TTL.header,
|
|
196
|
+
BETA_REGISTRY_2_1_280.FILES_API.header,
|
|
197
|
+
BETA_REGISTRY_2_1_280.MCP_SERVERS.header,
|
|
198
|
+
]);
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Upper bound on sent betas one rejection may latch. The API names the one or
|
|
202
|
+
* two identifiers it refused; more matches than this means the error echoed the
|
|
203
|
+
* whole header back, and latching from it would strip betas that were fine.
|
|
204
|
+
*/
|
|
205
|
+
export const MAX_LATCHABLE_REJECTED_BETAS = 2;
|
|
206
|
+
|
|
207
|
+
/** The package's `suppressBetas` entry grammar; anything else is `INVALID_INPUT`. */
|
|
208
|
+
const BETA_IDENTIFIER_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
209
|
+
const BETA_IDENTIFIER_MAX_LENGTH = 128;
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* The API's invalid-beta message shape:
|
|
213
|
+
* "Unexpected value(s) `a`, `b` for the `anthropic-beta` header. ...".
|
|
214
|
+
* Group 1 is the value list; quoting varies (backtick / single / double).
|
|
215
|
+
* The separators are literal single spaces, not `\s+`: two `\s+` runs around a
|
|
216
|
+
* lazy group backtrack catastrophically on long whitespace (ReDoS).
|
|
217
|
+
*/
|
|
218
|
+
const INVALID_BETA_MESSAGE_PATTERN = /Unexpected value\(s\) (.*?) for the [`'"]?anthropic-beta[`'"]? header/i;
|
|
219
|
+
/**
|
|
220
|
+
* Only this much of the message is matched. The API's invalid-beta sentence
|
|
221
|
+
* sits at the start and is far shorter; the cap bounds the lazy scan above on
|
|
222
|
+
* a hostile or runaway body (the pattern is linear per start position, and
|
|
223
|
+
* the cap keeps start positions x scan length small).
|
|
224
|
+
*/
|
|
225
|
+
const INVALID_BETA_MESSAGE_SCAN_LIMIT = 2048;
|
|
226
|
+
const QUOTED_BETA_PATTERN = /[`'"]([A-Za-z0-9][A-Za-z0-9._-]*)[`'"]/g;
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Extract the beta identifiers an Anthropic invalid-beta error names. Only the
|
|
230
|
+
* specific "Unexpected value(s) ... for the `anthropic-beta` header" shape is
|
|
231
|
+
* recognised, read from `error.message` when the body is the JSON error
|
|
232
|
+
* envelope (the raw text otherwise). Any other wording yields `[]`, so a body
|
|
233
|
+
* that merely mentions a beta name elsewhere never drives the latch.
|
|
234
|
+
*
|
|
235
|
+
* @param {string | null | undefined} errorBody Raw response body text.
|
|
236
|
+
* @returns {string[]} Named identifiers, in message order, deduplicated.
|
|
237
|
+
*/
|
|
238
|
+
export function parseRejectedBetaNames(errorBody) {
|
|
239
|
+
if (typeof errorBody !== "string" || !errorBody) return [];
|
|
240
|
+
let message = errorBody;
|
|
241
|
+
try {
|
|
242
|
+
const parsed = JSON.parse(errorBody);
|
|
243
|
+
if (typeof parsed?.error?.message === "string") message = parsed.error.message;
|
|
244
|
+
} catch {
|
|
245
|
+
// Not JSON: match against the raw text.
|
|
246
|
+
}
|
|
247
|
+
const match = INVALID_BETA_MESSAGE_PATTERN.exec(message.slice(0, INVALID_BETA_MESSAGE_SCAN_LIMIT));
|
|
248
|
+
if (!match) return [];
|
|
249
|
+
const list = match[1];
|
|
250
|
+
const names = [...list.matchAll(QUOTED_BETA_PATTERN)].map((m) => m[1]);
|
|
251
|
+
const candidates = names.length > 0 ? names : list.split(/[\s,]+/);
|
|
252
|
+
return [
|
|
253
|
+
...new Set(
|
|
254
|
+
candidates.filter(
|
|
255
|
+
(name) => name && name.length <= BETA_IDENTIFIER_MAX_LENGTH && BETA_IDENTIFIER_PATTERN.test(name),
|
|
256
|
+
),
|
|
257
|
+
),
|
|
258
|
+
];
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Decide which betas of a rejected request the latch may suppress.
|
|
263
|
+
*
|
|
264
|
+
* `namedSent` is every identifier on the failing request's `anthropic-beta`
|
|
265
|
+
* header that the invalid-beta message names (see `parseRejectedBetaNames`),
|
|
266
|
+
* UNSUPPRESSIBLE ones included; it is empty when the message is not that
|
|
267
|
+
* shape or when it looks like an echoed header (more than
|
|
268
|
+
* `MAX_LATCHABLE_REJECTED_BETAS` suppressible matches). `latchable` is
|
|
269
|
+
* `namedSent` minus `UNSUPPRESSIBLE_BETAS` and `BODY_COUPLED_BETAS`.
|
|
270
|
+
*
|
|
271
|
+
* @param {string | null | undefined} sentHeader The failing request's `anthropic-beta` value.
|
|
272
|
+
* @param {string | null | undefined} errorBody Raw response body text.
|
|
273
|
+
* @returns {{namedSent: string[], latchable: string[]}}
|
|
274
|
+
*/
|
|
275
|
+
export function selectLatchableRejectedBetas(sentHeader, errorBody) {
|
|
276
|
+
const named = new Set(parseRejectedBetaNames(errorBody).map((name) => name.toLowerCase()));
|
|
277
|
+
if (named.size === 0) return { namedSent: [], latchable: [] };
|
|
278
|
+
const sent = [
|
|
279
|
+
...new Set(
|
|
280
|
+
String(sentHeader ?? "")
|
|
281
|
+
.split(",")
|
|
282
|
+
.map((b) => b.trim())
|
|
283
|
+
.filter(Boolean),
|
|
284
|
+
),
|
|
285
|
+
];
|
|
286
|
+
const namedSent = sent.filter((b) => named.has(b.toLowerCase()));
|
|
287
|
+
const suppressible = namedSent.filter((b) => !UNSUPPRESSIBLE_BETAS.has(b.toLowerCase()));
|
|
288
|
+
if (suppressible.length > MAX_LATCHABLE_REJECTED_BETAS) return { namedSent: [], latchable: [] };
|
|
289
|
+
return { namedSent, latchable: suppressible.filter((b) => !BODY_COUPLED_BETAS.has(b.toLowerCase())) };
|
|
290
|
+
}
|
|
153
291
|
|
|
154
292
|
/**
|
|
155
293
|
* Betas the plugin opts INTO on its own (see `buildAdditionalBetas`): whether
|
|
@@ -161,11 +299,11 @@ export const PROMPT_CACHING_SCOPE_BETA = BETA_REGISTRY_2_1_233.PROMPT_CACHING_SC
|
|
|
161
299
|
* rename or date bump surfaces as a changed emission instead of drifting
|
|
162
300
|
* silently against a literal re-typed in this file.
|
|
163
301
|
*/
|
|
164
|
-
export const WEB_SEARCH_BETA =
|
|
165
|
-
export const ADVISOR_TOOL_BETA =
|
|
166
|
-
export const FILES_API_BETA =
|
|
167
|
-
export const EXTENDED_CACHE_TTL_BETA =
|
|
168
|
-
export const STRUCTURED_OUTPUTS_BETA =
|
|
302
|
+
export const WEB_SEARCH_BETA = BETA_REGISTRY_2_1_280.WEB_SEARCH.header;
|
|
303
|
+
export const ADVISOR_TOOL_BETA = BETA_REGISTRY_2_1_280.ADVISOR_TOOL.header;
|
|
304
|
+
export const FILES_API_BETA = BETA_REGISTRY_2_1_280.FILES_API.header;
|
|
305
|
+
export const EXTENDED_CACHE_TTL_BETA = BETA_REGISTRY_2_1_280.EXTENDED_CACHE_TTL.header;
|
|
306
|
+
export const STRUCTURED_OUTPUTS_BETA = BETA_REGISTRY_2_1_280.STRUCTURED_OUTPUTS.header;
|
|
169
307
|
|
|
170
308
|
/**
|
|
171
309
|
* `runtime.sessionId` is REQUIRED by the package (`requiredString`), while the
|
|
@@ -233,7 +371,7 @@ export const ADAPTER_ENV_KEYS = Object.freeze([
|
|
|
233
371
|
|
|
234
372
|
/**
|
|
235
373
|
* Protocol identity of the profile this adapter composes against
|
|
236
|
-
* (`claude-code-2.1.
|
|
374
|
+
* (`claude-code-2.1.280-sdk-0.112.1`). Used to decide whether a
|
|
237
375
|
* `profileOverride` is needed at all — see `resolveProfileOverride` for the
|
|
238
376
|
* user-agent convergence argument.
|
|
239
377
|
*
|
|
@@ -298,11 +436,11 @@ export function buildAdapterUserAgent(version, env) {
|
|
|
298
436
|
|
|
299
437
|
/**
|
|
300
438
|
* The inherited default profile carries a FIXED `userAgent`
|
|
301
|
-
* (`"claude-cli/2.1.
|
|
439
|
+
* (`"claude-cli/2.1.280 (external, cli)"`) while the plugin builds one
|
|
302
440
|
* dynamically from entrypoint / agent-sdk version / client-app. In the common
|
|
303
441
|
* case — no `CLAUDE_CODE_ENTRYPOINT`, no `CLAUDE_AGENT_SDK_VERSION`, no
|
|
304
|
-
* `CLAUDE_AGENT_SDK_CLIENT_APP`, detected CLI version `2.1.
|
|
305
|
-
* produce the byte-identical `"claude-cli/2.1.
|
|
442
|
+
* `CLAUDE_AGENT_SDK_CLIENT_APP`, detected CLI version `2.1.280` — both sides
|
|
443
|
+
* produce the byte-identical `"claude-cli/2.1.280 (external, cli)"`, so no
|
|
306
444
|
* override is emitted and the request stays byte-identical to the profile.
|
|
307
445
|
*
|
|
308
446
|
* When they diverge, `userAgent` and `cliVersion` are validated TOGETHER by the
|
|
@@ -521,16 +659,35 @@ function buildAdditionalBetas(args) {
|
|
|
521
659
|
* `EXPERIMENTAL_BETA_FLAGS` set is safe and avoids depending on which of them the
|
|
522
660
|
* package happens to compose for a given model.
|
|
523
661
|
*
|
|
524
|
-
*
|
|
662
|
+
* `rejectedBetas` carries the sent betas the API explicitly named as invalid
|
|
663
|
+
* for the current account (the TTL'd per-account sent-beta latch in index.mjs;
|
|
664
|
+
* see `selectLatchableRejectedBetas`). Dropping them from `customBetas` alone is
|
|
665
|
+
* not enough once the package composes the same beta itself (e.g.
|
|
666
|
+
* `cache-diagnosis-2026-04-07` under 2.1.280), so they are suppressed here.
|
|
667
|
+
* `UNSUPPRESSIBLE_BETAS` and `BODY_COUPLED_BETAS` are never taken from this
|
|
668
|
+
* input, entries outside the package grammar are skipped (they cannot be on the
|
|
669
|
+
* wire anyway), and the list is capped at `MAX_SUPPRESS_BETAS` with the policy
|
|
670
|
+
* suppressions taking precedence.
|
|
671
|
+
*
|
|
672
|
+
* @param {{isRoundRobin: boolean, disableExperimentalBetas: boolean, rejectedBetas?: readonly string[]}} args
|
|
525
673
|
* @returns {readonly string[] | undefined}
|
|
526
674
|
*/
|
|
527
|
-
function buildSuppressBetas({ isRoundRobin, disableExperimentalBetas }) {
|
|
675
|
+
function buildSuppressBetas({ isRoundRobin, disableExperimentalBetas, rejectedBetas }) {
|
|
528
676
|
/** @type {Set<string>} */
|
|
529
677
|
const suppressed = new Set();
|
|
530
678
|
|
|
531
679
|
if (isRoundRobin) suppressed.add(PROMPT_CACHING_SCOPE_BETA);
|
|
532
680
|
if (disableExperimentalBetas) for (const beta of EXPERIMENTAL_BETA_FLAGS) suppressed.add(beta);
|
|
533
681
|
|
|
682
|
+
for (const rejected of rejectedBetas ?? []) {
|
|
683
|
+
if (suppressed.size >= MAX_SUPPRESS_BETAS) break;
|
|
684
|
+
if (typeof rejected !== "string") continue;
|
|
685
|
+
const beta = rejected.trim();
|
|
686
|
+
if (!beta || beta.length > BETA_IDENTIFIER_MAX_LENGTH || !BETA_IDENTIFIER_PATTERN.test(beta)) continue;
|
|
687
|
+
if (UNSUPPRESSIBLE_BETAS.has(beta.toLowerCase()) || BODY_COUPLED_BETAS.has(beta.toLowerCase())) continue;
|
|
688
|
+
suppressed.add(beta);
|
|
689
|
+
}
|
|
690
|
+
|
|
534
691
|
if (suppressed.size === 0) return undefined;
|
|
535
692
|
return [...suppressed];
|
|
536
693
|
}
|
|
@@ -592,7 +749,8 @@ function buildMetadataOverrides(env) {
|
|
|
592
749
|
* @property {URL | undefined} [requestUrl] Parsed outgoing URL.
|
|
593
750
|
* @property {string} provider Result of `detectProvider(requestUrl)`.
|
|
594
751
|
* @property {string} clientRequestId Caller-generated `randomUUID()`.
|
|
595
|
-
* @property {{enabled: boolean, claudeCliVersion: string, customBetas?: readonly string[], strategy?: string, sessionId?: string}} signature
|
|
752
|
+
* @property {{enabled: boolean, claudeCliVersion: string, customBetas?: readonly string[], rejectedBetas?: readonly string[], strategy?: string, sessionId?: string}} signature
|
|
753
|
+
* `rejectedBetas`: sent betas the API named as invalid for this account (TTL'd); routed to `suppressBetas`.
|
|
596
754
|
* @property {{persistentUserId: string, accountId: string}} identity Correlation triple sources.
|
|
597
755
|
* @property {{use1MContext?: unknown} | undefined} [adaptiveOverride]
|
|
598
756
|
* @property {Record<string, unknown> | undefined} [tokenEconomy]
|
|
@@ -662,7 +820,11 @@ export function buildAdapterTransport(state) {
|
|
|
662
820
|
disableExperimentalBetas,
|
|
663
821
|
incomingBetas,
|
|
664
822
|
});
|
|
665
|
-
const suppressBetas = buildSuppressBetas({
|
|
823
|
+
const suppressBetas = buildSuppressBetas({
|
|
824
|
+
isRoundRobin,
|
|
825
|
+
disableExperimentalBetas,
|
|
826
|
+
rejectedBetas: signature.rejectedBetas,
|
|
827
|
+
});
|
|
666
828
|
|
|
667
829
|
// 1M context is ALWAYS passed explicitly. The plugin's default is
|
|
668
830
|
// `hasOneMillionContext(model)` (`/(^|[-_ ])1m($|[-_ ])|context[-_]?1m/i`)
|
package/lib/mimicry/headers.mjs
CHANGED
|
@@ -73,7 +73,7 @@ export const TOKEN_COUNTING_BETA_FLAG = "token-counting-2024-11-01";
|
|
|
73
73
|
*
|
|
74
74
|
* These are betas the HOST SDK injects into its outgoing request that the
|
|
75
75
|
* genuine Claude Code client NEVER sends. They are not stale registry entries
|
|
76
|
-
* and they are not the package's business — `
|
|
76
|
+
* and they are not the package's business — `BETA_REGISTRY_2_1_280` has no slot
|
|
77
77
|
* for either, because upstream has no reason to model a header it does not
|
|
78
78
|
* emit. The plugin drops them so a host's own SDK cannot leak a foreign
|
|
79
79
|
* fingerprint into a request that is otherwise byte-identical to the real
|