opencode-anthropic-fix 0.1.29 → 0.1.31
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 +127 -36
- package/lib/adaptive-thinking-models.test.mjs +30 -0
- package/lib/backoff.mjs +23 -0
- package/lib/backoff.test.mjs +67 -1
- package/lib/config.mjs +24 -0
- package/lib/oauth.mjs +60 -13
- package/lib/oauth.test.mjs +54 -0
- package/lib/request-headers.mjs +59 -8
- package/lib/request-headers.test.mjs +41 -2
- package/package.json +1 -1
package/index.mjs
CHANGED
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
parseRateLimitReason,
|
|
31
31
|
parseRetryAfterHeader,
|
|
32
32
|
parseRetryAfterMsHeader,
|
|
33
|
+
parseUnifiedResetMsHeader,
|
|
33
34
|
parseShouldRetryHeader,
|
|
34
35
|
TRANSIENT_RETRY_THRESHOLD_MS,
|
|
35
36
|
} from "./lib/backoff.mjs";
|
|
@@ -3745,7 +3746,10 @@ export async function AnthropicAuthPlugin({ client, project, directory, worktree
|
|
|
3745
3746
|
// Account-specific errors (429/401/billing/permission)
|
|
3746
3747
|
if (accountSpecific) {
|
|
3747
3748
|
const reason = parseRateLimitReason(response.status, errorBody);
|
|
3748
|
-
const retryAfterMs =
|
|
3749
|
+
const retryAfterMs =
|
|
3750
|
+
parseRetryAfterMsHeader(response) ??
|
|
3751
|
+
parseRetryAfterHeader(response) ??
|
|
3752
|
+
parseUnifiedResetMsHeader(response);
|
|
3749
3753
|
|
|
3750
3754
|
// Transient 429: short retry-after (<=10s) is a burst throttle.
|
|
3751
3755
|
// Retry on the SAME account instead of rotating — avoids wasting
|
|
@@ -6661,7 +6665,7 @@ function getClaudeEntrypoint() {
|
|
|
6661
6665
|
}
|
|
6662
6666
|
|
|
6663
6667
|
/**
|
|
6664
|
-
* @param {string | undefined}
|
|
6668
|
+
* @param {string | undefined} model
|
|
6665
6669
|
* @returns {string | undefined}
|
|
6666
6670
|
*/
|
|
6667
6671
|
function logTransformedSystemPrompt(body) {
|
|
@@ -6773,8 +6777,33 @@ function isSonnet46Model(model) {
|
|
|
6773
6777
|
* @param {string | undefined} body
|
|
6774
6778
|
* @returns {boolean}
|
|
6775
6779
|
*/
|
|
6776
|
-
function
|
|
6777
|
-
|
|
6780
|
+
export function isFable5Model(model) {
|
|
6781
|
+
if (!model) return false;
|
|
6782
|
+
return /claude-fable-5|fable[._-]5/i.test(model);
|
|
6783
|
+
}
|
|
6784
|
+
|
|
6785
|
+
/**
|
|
6786
|
+
* @param {string | undefined} model
|
|
6787
|
+
* @returns {boolean}
|
|
6788
|
+
*/
|
|
6789
|
+
export function isMythos5Model(model) {
|
|
6790
|
+
if (!model) return false;
|
|
6791
|
+
return /claude-mythos-5|mythos[._-]5/i.test(model);
|
|
6792
|
+
}
|
|
6793
|
+
|
|
6794
|
+
/**
|
|
6795
|
+
* @param {string | undefined} model
|
|
6796
|
+
* @returns {boolean}
|
|
6797
|
+
*/
|
|
6798
|
+
export function isAdaptiveThinkingModel(model) {
|
|
6799
|
+
return (
|
|
6800
|
+
isOpus46Model(model) ||
|
|
6801
|
+
isOpus47Model(model) ||
|
|
6802
|
+
isOpus48Model(model) ||
|
|
6803
|
+
isSonnet46Model(model) ||
|
|
6804
|
+
isFable5Model(model) ||
|
|
6805
|
+
isMythos5Model(model)
|
|
6806
|
+
);
|
|
6778
6807
|
}
|
|
6779
6808
|
|
|
6780
6809
|
/**
|
|
@@ -7062,6 +7091,14 @@ const MAX_SUBAGENT_CC_PREFIX = MAX_SAFE_SYSTEM_TEXT_LENGTH;
|
|
|
7062
7091
|
const SUBAGENT_CC_ANCHOR = "You are an interactive";
|
|
7063
7092
|
let cachedCCPrompt = null;
|
|
7064
7093
|
|
|
7094
|
+
// Perf: module-scope regexes reused across per-request hot paths. None use the
|
|
7095
|
+
// `/g` flag and all are consumed via `.test()`, so a single shared instance is
|
|
7096
|
+
// stateless and safe (no `lastIndex` to reset between calls).
|
|
7097
|
+
const CLAUDE_3_MODEL_RE = /claude-3-/i;
|
|
7098
|
+
const TAIL_IMPORTANT_RE = /\b(MUST|NEVER|CRITICAL|IMPORTANT|REQUIRED|DO NOT|ALWAYS|FORBIDDEN)\b/i;
|
|
7099
|
+
const TAIL_HEADER_RE = /^#{1,4}\s/;
|
|
7100
|
+
const TAIL_LIST_ITEM_RE = /^\s*[-*]\s/;
|
|
7101
|
+
|
|
7065
7102
|
function sanitizeSystemText(text) {
|
|
7066
7103
|
// QA fix M4: use word boundaries to avoid mangling URLs and code identifiers
|
|
7067
7104
|
let sanitized = text.replace(/\bOpenCode\b/g, "Claude Code").replace(/\bopencode\b/gi, "Claude");
|
|
@@ -7084,9 +7121,9 @@ function tailSystemBlock(text, maxChars, turnThreshold) {
|
|
|
7084
7121
|
const lines = text.split("\n");
|
|
7085
7122
|
const kept = [];
|
|
7086
7123
|
let charCount = 0;
|
|
7087
|
-
const importantRe =
|
|
7088
|
-
const headerRe =
|
|
7089
|
-
const listItemRe =
|
|
7124
|
+
const importantRe = TAIL_IMPORTANT_RE;
|
|
7125
|
+
const headerRe = TAIL_HEADER_RE;
|
|
7126
|
+
const listItemRe = TAIL_LIST_ITEM_RE;
|
|
7090
7127
|
// Always keep the first paragraph (identity/role definition)
|
|
7091
7128
|
let firstParaEnd = 0;
|
|
7092
7129
|
for (let j = 0; j < lines.length; j++) {
|
|
@@ -7575,6 +7612,22 @@ function buildSystemPromptBlocks(system, signature) {
|
|
|
7575
7612
|
* accumulates fast-mode independently of actual body content.
|
|
7576
7613
|
* @returns {string}
|
|
7577
7614
|
*/
|
|
7615
|
+
// Mirrors CC's Kw(model) effort eligibility: returns false for claude-3-* and the
|
|
7616
|
+
// explicit older 4.x exclusion set (opus-4-0/4-1, sonnet-4-0/4-5, haiku-4-5), true
|
|
7617
|
+
// for every other model (effort-capable: Opus 4.5/4.6/4.7/4.8, Sonnet 4.6, etc.).
|
|
7618
|
+
const _EFFORT_EXCLUDED_MODELS = [
|
|
7619
|
+
/claude-opus-4-0/i,
|
|
7620
|
+
/claude-opus-4-1/i,
|
|
7621
|
+
/claude-sonnet-4-0/i,
|
|
7622
|
+
/claude-sonnet-4-5/i,
|
|
7623
|
+
/claude-haiku-4-5/i,
|
|
7624
|
+
];
|
|
7625
|
+
function isEffortCapableModel(model) {
|
|
7626
|
+
if (!model) return false;
|
|
7627
|
+
if (CLAUDE_3_MODEL_RE.test(model)) return false;
|
|
7628
|
+
return !_EFFORT_EXCLUDED_MODELS.some((re) => re.test(model));
|
|
7629
|
+
}
|
|
7630
|
+
|
|
7578
7631
|
function buildAnthropicBetaHeader(
|
|
7579
7632
|
incomingBeta,
|
|
7580
7633
|
signatureEnabled,
|
|
@@ -7628,13 +7681,15 @@ function buildAnthropicBetaHeader(
|
|
|
7628
7681
|
|
|
7629
7682
|
// v2.1.150: fast-mode-2026-02-01 removed from always-on (CC sends only when speed feature active).
|
|
7630
7683
|
|
|
7631
|
-
// v2.1.
|
|
7684
|
+
// v2.1.195: effort-2025-11-24 is no longer always-on; it is now a model-gated
|
|
7685
|
+
// default emitted in the conditional section below for effort-capable models
|
|
7686
|
+
// (mirrors CC's Kw(model)).
|
|
7632
7687
|
|
|
7633
7688
|
// Interleaved thinking — real CC's i01 pushes via hv4(model), which is
|
|
7634
7689
|
// (firstParty && non-Claude-3). Claude 3.x models don't support interleaved
|
|
7635
7690
|
// thinking and real CC never sends this flag for them, so emitting it
|
|
7636
7691
|
// diverges the fingerprint for legacy Haiku/Sonnet 3.x requests.
|
|
7637
|
-
if (!isTruthyEnv(process.env.DISABLE_INTERLEAVED_THINKING) &&
|
|
7692
|
+
if (!isTruthyEnv(process.env.DISABLE_INTERLEAVED_THINKING) && !CLAUDE_3_MODEL_RE.test(model)) {
|
|
7638
7693
|
betas.push("interleaved-thinking-2025-05-14");
|
|
7639
7694
|
}
|
|
7640
7695
|
|
|
@@ -7660,11 +7715,28 @@ function buildAnthropicBetaHeader(
|
|
|
7660
7715
|
|
|
7661
7716
|
// === CONDITIONAL BETAS (model/context-dependent) ===
|
|
7662
7717
|
|
|
7663
|
-
//
|
|
7664
|
-
|
|
7718
|
+
// First-party provider predicate (OAuth path). Mirrors CC's ZO(provider) which
|
|
7719
|
+
// excludes bedrock/vertex/mantle. context-management and effort are first-party
|
|
7720
|
+
// only here, so they must never be pushed on bedrock/vertex.
|
|
7721
|
+
const isFirstPartyProvider = provider !== "vertex" && provider !== "bedrock" && provider !== "mantle";
|
|
7722
|
+
|
|
7723
|
+
// v2.1.195: context-management-2025-06-27 is default-ON for first-party non-claude-3
|
|
7724
|
+
// models (incl. Haiku 4.5), mirroring CC's n0d(model) eligibility path. Earlier
|
|
7725
|
+
// analyses read only the separate USE_API_CONTEXT_MANAGEMENT env term (hardcoded
|
|
7726
|
+
// && false) and recorded it as off; 2.1.195 confirms it ships by default for modern
|
|
7727
|
+
// first-party models. Opt out via token_economy.context_management = false.
|
|
7728
|
+
if (te.context_management !== false && isFirstPartyProvider && !CLAUDE_3_MODEL_RE.test(model)) {
|
|
7665
7729
|
betas.push("context-management-2025-06-27");
|
|
7666
7730
|
}
|
|
7667
7731
|
|
|
7732
|
+
// v2.1.195: effort-2025-11-24 is a model-gated default for effort-capable models
|
|
7733
|
+
// (Opus 4.5/4.6/4.7/4.8, Sonnet 4.6), mirroring CC's Kw(model). Excluded for
|
|
7734
|
+
// claude-3-*, opus-4-0, opus-4-1, sonnet-4-0, sonnet-4-5, haiku-4-5. First-party
|
|
7735
|
+
// only; opt out via token_economy.effort = false.
|
|
7736
|
+
if (te.effort !== false && isFirstPartyProvider && isEffortCapableModel(model)) {
|
|
7737
|
+
betas.push(_EFFORT_BETA_FLAG);
|
|
7738
|
+
}
|
|
7739
|
+
|
|
7668
7740
|
// v2.1.150: structured-outputs gated behind opt-in (CC has behind tengu_tool_pear flag).
|
|
7669
7741
|
if (te.structured_outputs && supportsStructuredOutputs(model)) {
|
|
7670
7742
|
betas.push("structured-outputs-2025-12-15");
|
|
@@ -7679,7 +7751,7 @@ function buildAnthropicBetaHeader(
|
|
|
7679
7751
|
// (tengu_sage_compass2) and firstParty+isLoggedIn. Since we can't check
|
|
7680
7752
|
// CC's feature flags, include it unconditionally for Claude 4+ models.
|
|
7681
7753
|
// CC v108 sends it in MITM captures for Max/Pro users.
|
|
7682
|
-
if (
|
|
7754
|
+
if (!CLAUDE_3_MODEL_RE.test(model)) {
|
|
7683
7755
|
betas.push("advisor-tool-2026-03-01");
|
|
7684
7756
|
}
|
|
7685
7757
|
|
|
@@ -7691,12 +7763,11 @@ function buildAnthropicBetaHeader(
|
|
|
7691
7763
|
// token-efficient context compaction. Sticky: disabled permanently on
|
|
7692
7764
|
// 400/409/529 errors referencing the hint. Users can opt out via
|
|
7693
7765
|
// token_economy.context_hint = false.
|
|
7694
|
-
const isFirstPartyProvider = provider !== "vertex" && provider !== "bedrock" && provider !== "mantle";
|
|
7695
7766
|
// Mimicry note: real CC gates context-hint on querySource.startsWith("repl_main_thread").
|
|
7696
7767
|
// We infer main-thread from body shape via classifyRequestRole and pass it via
|
|
7697
7768
|
// tokenEconomy.__requestRole. Treat absent marker as "main" for backward compat.
|
|
7698
7769
|
const _isMainThread = te.__requestRole == null || te.__requestRole === "main";
|
|
7699
|
-
if (isFirstPartyProvider &&
|
|
7770
|
+
if (isFirstPartyProvider && !CLAUDE_3_MODEL_RE.test(model) && te.context_hint !== false && _isMainThread) {
|
|
7700
7771
|
betas.push("context-hint-2026-04-09");
|
|
7701
7772
|
}
|
|
7702
7773
|
|
|
@@ -7714,13 +7785,13 @@ function buildAnthropicBetaHeader(
|
|
|
7714
7785
|
|
|
7715
7786
|
// v2.1.150: redact-thinking is default-ON in CC (first-party, non-SDK, thinking models).
|
|
7716
7787
|
// Opt out via `/anthropic set redact-thinking off` to see thinking content.
|
|
7717
|
-
if (te.redact_thinking !== false && !disableExperimentalBetas &&
|
|
7788
|
+
if (te.redact_thinking !== false && !disableExperimentalBetas && !CLAUDE_3_MODEL_RE.test(model)) {
|
|
7718
7789
|
betas.push("redact-thinking-2026-02-12");
|
|
7719
7790
|
}
|
|
7720
7791
|
|
|
7721
7792
|
// v2.1.150: thinking-token-count for token budget tracking (behind tengu_chert_bezel in CC).
|
|
7722
7793
|
// Default ON for token economy visibility. Opt out via token_economy.thinking_token_count = false.
|
|
7723
|
-
if (te.thinking_token_count !== false && !disableExperimentalBetas &&
|
|
7794
|
+
if (te.thinking_token_count !== false && !disableExperimentalBetas && !CLAUDE_3_MODEL_RE.test(model)) {
|
|
7724
7795
|
betas.push("thinking-token-count-2026-05-13");
|
|
7725
7796
|
}
|
|
7726
7797
|
|
|
@@ -8014,7 +8085,9 @@ function buildRequestHeaders(
|
|
|
8014
8085
|
requestHeaders.set("x-anthropic-additional-protection", "true");
|
|
8015
8086
|
}
|
|
8016
8087
|
|
|
8017
|
-
// x-client-request-id:
|
|
8088
|
+
// x-client-request-id: real CC 2.1.195's first-party fetch middleware (Ukd) sets
|
|
8089
|
+
// this to crypto.randomUUID() on every first-party request. Re-emit a random uuid.
|
|
8090
|
+
requestHeaders.set("x-client-request-id", randomUUID());
|
|
8018
8091
|
}
|
|
8019
8092
|
requestHeaders.delete("x-api-key");
|
|
8020
8093
|
// x-session-affinity: set by opencode SDK but NOT in real CC. Strip it.
|
|
@@ -8155,16 +8228,16 @@ function transformRequestBody(body, signature, runtime, betaHeader, config) {
|
|
|
8155
8228
|
delete parsed.temperature;
|
|
8156
8229
|
|
|
8157
8230
|
// Claude Code v2.1.84: inject the context_management body field ONLY when the
|
|
8158
|
-
//
|
|
8159
|
-
// the
|
|
8160
|
-
//
|
|
8161
|
-
//
|
|
8162
|
-
//
|
|
8163
|
-
// field
|
|
8164
|
-
//
|
|
8231
|
+
// user has explicitly opted in via token_economy.context_management. Note: as of
|
|
8232
|
+
// v2.1.195 the context-management *beta header* is default-ON for first-party
|
|
8233
|
+
// non-claude-3 models (see buildAnthropicBetaHeader), but the body field stays
|
|
8234
|
+
// opt-in. A top-level context_management field WITHOUT the beta is rejected with
|
|
8235
|
+
// "context_management: Extra inputs are not permitted"; the beta being present
|
|
8236
|
+
// without the field is fine. Gating the field on explicit opt-in keeps the field
|
|
8237
|
+
// ⊆ beta invariant, so the 400 never fires.
|
|
8165
8238
|
if (
|
|
8166
8239
|
config?.token_economy?.context_management &&
|
|
8167
|
-
|
|
8240
|
+
!CLAUDE_3_MODEL_RE.test(parsed.model || "") &&
|
|
8168
8241
|
!parsed.context_management
|
|
8169
8242
|
) {
|
|
8170
8243
|
parsed.context_management = {
|
|
@@ -8293,9 +8366,34 @@ function transformRequestBody(body, signature, runtime, betaHeader, config) {
|
|
|
8293
8366
|
const modelId = parsed.model || "";
|
|
8294
8367
|
// Extract first user message text for billing hash computation (cch)
|
|
8295
8368
|
const firstUserMessage = extractFirstUserMessageText(parsed.messages);
|
|
8369
|
+
// Resolve the prompt-cache TTL ONCE so system blocks, tools, and messages
|
|
8370
|
+
// all share the SAME ttl. Anthropic processes cache_control blocks in the
|
|
8371
|
+
// order tools → system → messages and rejects any ttl='1h' block that comes
|
|
8372
|
+
// AFTER a ttl='5m' block. Before this fix the role-scoped 5m downgrade was
|
|
8373
|
+
// applied only to tools/messages (resolveCacheTtl in the breakpoint loop)
|
|
8374
|
+
// while the system blocks kept the configured 1h — so subagent requests
|
|
8375
|
+
// (5m tools, then 1h system) tripped:
|
|
8376
|
+
// "system.1.cache_control.ttl: a ttl='1h' cache_control block must not
|
|
8377
|
+
// come after a ttl='5m' cache_control block".
|
|
8378
|
+
// Threading it through buildSystemPromptBlocks keeps every cache_control ttl
|
|
8379
|
+
// consistent (real CC derives the ttl uniformly from querySource).
|
|
8380
|
+
const baseCachePolicy = signature.cachePolicy || { ttl: "1h", ttl_supported: true };
|
|
8381
|
+
const cachingEnabledForTtl = baseCachePolicy.ttl !== "off" && baseCachePolicy.ttl_supported !== false;
|
|
8382
|
+
const resolvedCacheTtl = cachingEnabledForTtl
|
|
8383
|
+
? resolveCacheTtl({
|
|
8384
|
+
configuredTtl: baseCachePolicy.ttl || "1h",
|
|
8385
|
+
roleScopedTtl: config?.token_economy?.role_scoped_cache_ttl !== false,
|
|
8386
|
+
isMainForCache: runtime?.requestRole === "main" || runtime?.requestRole == null,
|
|
8387
|
+
isSubagent: runtime?.isSubagent === true,
|
|
8388
|
+
env: process.env,
|
|
8389
|
+
})
|
|
8390
|
+
: baseCachePolicy.ttl;
|
|
8296
8391
|
const signatureWithModel = {
|
|
8297
8392
|
...signature,
|
|
8298
8393
|
modelId,
|
|
8394
|
+
// Override cachePolicy.ttl with the role/subagent-resolved ttl so the
|
|
8395
|
+
// system blocks match the tool/message breakpoint ttl (see comment above).
|
|
8396
|
+
cachePolicy: { ...baseCachePolicy, ttl: resolvedCacheTtl },
|
|
8299
8397
|
firstUserMessage,
|
|
8300
8398
|
antiVerbosity: config?.anti_verbosity,
|
|
8301
8399
|
// Role-aware system-prompt leaning: for non-main-thread requests (title,
|
|
@@ -8393,17 +8491,10 @@ function transformRequestBody(body, signature, runtime, betaHeader, config) {
|
|
|
8393
8491
|
// We map step 4 via classifyRequestRole: main (interactive/auto_mode thread)
|
|
8394
8492
|
// → 1h; side-queries (title-gen, etc.) → 5m. The env vars are honored
|
|
8395
8493
|
// exactly as CC does, giving the user a manual override + mimicry fidelity.
|
|
8396
|
-
|
|
8397
|
-
|
|
8398
|
-
|
|
8399
|
-
const
|
|
8400
|
-
const ccTtl = resolveCacheTtl({
|
|
8401
|
-
configuredTtl,
|
|
8402
|
-
roleScopedTtl,
|
|
8403
|
-
isMainForCache,
|
|
8404
|
-
isSubagent: isSubagentForCache,
|
|
8405
|
-
env: process.env,
|
|
8406
|
-
});
|
|
8494
|
+
// Reuse the request-wide resolved ttl (computed above where
|
|
8495
|
+
// signatureWithModel is built) so tools/messages match the system blocks
|
|
8496
|
+
// and never trip the 1h-after-5m ordering rule on subagent requests.
|
|
8497
|
+
const ccTtl = resolvedCacheTtl;
|
|
8407
8498
|
if (Array.isArray(parsed.tools) && parsed.tools.length > 0) {
|
|
8408
8499
|
for (const tool of parsed.tools) {
|
|
8409
8500
|
if (tool.cache_control) delete tool.cache_control;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { isAdaptiveThinkingModel, isFable5Model, isMythos5Model } from "../index.mjs";
|
|
4
|
+
|
|
5
|
+
describe("Claude 5 adaptive thinking model predicates", () => {
|
|
6
|
+
test("isFable5Model matches version-anchored fable 5 model names", () => {
|
|
7
|
+
expect(isFable5Model("claude-fable-5")).toBe(true);
|
|
8
|
+
expect(isFable5Model("anthropic/claude-fable-5")).toBe(true);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test("isFable5Model rejects non-fable-5 model names", () => {
|
|
12
|
+
expect(isFable5Model("claude-opus-4-8")).toBe(false);
|
|
13
|
+
expect(isFable5Model("claude-haiku-4-5")).toBe(false);
|
|
14
|
+
expect(isFable5Model("")).toBe(false);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test("isMythos5Model matches version-anchored mythos 5 model names", () => {
|
|
18
|
+
expect(isMythos5Model("claude-mythos-5")).toBe(true);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("isMythos5Model rejects non-mythos-5 model names", () => {
|
|
22
|
+
expect(isMythos5Model("claude-opus-4-8")).toBe(false);
|
|
23
|
+
expect(isMythos5Model("claude-haiku-4-5")).toBe(false);
|
|
24
|
+
expect(isMythos5Model("")).toBe(false);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("isAdaptiveThinkingModel includes fable 5 models", () => {
|
|
28
|
+
expect(isAdaptiveThinkingModel("claude-fable-5")).toBe(true);
|
|
29
|
+
});
|
|
30
|
+
});
|
package/lib/backoff.mjs
CHANGED
|
@@ -64,6 +64,29 @@ export function parseRetryAfterHeader(response) {
|
|
|
64
64
|
return null;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
/**
|
|
68
|
+
* Parse the unified rate-limit reset header from a response (Claude Code 2.1.195+).
|
|
69
|
+
* Reads `anthropic-ratelimit-unified-reset`, falling back to
|
|
70
|
+
* `anthropic-ratelimit-unified-overage-reset` when the primary header is absent.
|
|
71
|
+
* Both are UNIX epochs in SECONDS (parsed via `Number()`, mirroring upstream).
|
|
72
|
+
* Returns milliseconds-until-reset, or null when the header is missing, not a
|
|
73
|
+
* number, or already in the past.
|
|
74
|
+
* @param {Response} response
|
|
75
|
+
* @returns {number | null} Milliseconds until reset, or null
|
|
76
|
+
*/
|
|
77
|
+
export function parseUnifiedResetMsHeader(response) {
|
|
78
|
+
const header =
|
|
79
|
+
response.headers.get("anthropic-ratelimit-unified-reset") ??
|
|
80
|
+
response.headers.get("anthropic-ratelimit-unified-overage-reset");
|
|
81
|
+
if (!header) return null;
|
|
82
|
+
|
|
83
|
+
const epochSeconds = Number(header);
|
|
84
|
+
if (isNaN(epochSeconds)) return null;
|
|
85
|
+
|
|
86
|
+
const ms = epochSeconds * 1000 - Date.now();
|
|
87
|
+
return ms > 0 ? ms : null;
|
|
88
|
+
}
|
|
89
|
+
|
|
67
90
|
/**
|
|
68
91
|
* Parse normalized error signals from a response body.
|
|
69
92
|
* Accepts raw strings, parsed objects, or null.
|
package/lib/backoff.test.mjs
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
parseRateLimitReason,
|
|
4
|
+
calculateBackoffMs,
|
|
5
|
+
parseRetryAfterHeader,
|
|
6
|
+
parseUnifiedResetMsHeader,
|
|
7
|
+
isAccountSpecificError,
|
|
8
|
+
} from "./backoff.mjs";
|
|
3
9
|
|
|
4
10
|
// ---------------------------------------------------------------------------
|
|
5
11
|
// isAccountSpecificError
|
|
@@ -343,3 +349,63 @@ describe("parseRetryAfterHeader", () => {
|
|
|
343
349
|
expect(parseRetryAfterHeader(response)).toBeNull();
|
|
344
350
|
});
|
|
345
351
|
});
|
|
352
|
+
|
|
353
|
+
// ---------------------------------------------------------------------------
|
|
354
|
+
// parseUnifiedResetMsHeader
|
|
355
|
+
// ---------------------------------------------------------------------------
|
|
356
|
+
|
|
357
|
+
describe("parseUnifiedResetMsHeader", () => {
|
|
358
|
+
it("returns null when no unified-reset header present", () => {
|
|
359
|
+
const response = new Response(null, { headers: {} });
|
|
360
|
+
expect(parseUnifiedResetMsHeader(response)).toBeNull();
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it("returns ~ms-until-reset for a future epoch (seconds)", () => {
|
|
364
|
+
const futureEpochSeconds = Math.floor(Date.now() / 1000) + 120;
|
|
365
|
+
const response = new Response(null, {
|
|
366
|
+
headers: { "anthropic-ratelimit-unified-reset": String(futureEpochSeconds) },
|
|
367
|
+
});
|
|
368
|
+
const result = parseUnifiedResetMsHeader(response);
|
|
369
|
+
expect(result).toBeGreaterThan(110_000);
|
|
370
|
+
expect(result).toBeLessThanOrEqual(120_000);
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it("returns null for a non-numeric value (NaN)", () => {
|
|
374
|
+
const response = new Response(null, {
|
|
375
|
+
headers: { "anthropic-ratelimit-unified-reset": "not-a-number" },
|
|
376
|
+
});
|
|
377
|
+
expect(parseUnifiedResetMsHeader(response)).toBeNull();
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
it("returns null for a past epoch (<= now)", () => {
|
|
381
|
+
const pastEpochSeconds = Math.floor(Date.now() / 1000) - 60;
|
|
382
|
+
const response = new Response(null, {
|
|
383
|
+
headers: { "anthropic-ratelimit-unified-reset": String(pastEpochSeconds) },
|
|
384
|
+
});
|
|
385
|
+
expect(parseUnifiedResetMsHeader(response)).toBeNull();
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
it("falls back to overage-reset when unified-reset is absent", () => {
|
|
389
|
+
const futureEpochSeconds = Math.floor(Date.now() / 1000) + 90;
|
|
390
|
+
const response = new Response(null, {
|
|
391
|
+
headers: { "anthropic-ratelimit-unified-overage-reset": String(futureEpochSeconds) },
|
|
392
|
+
});
|
|
393
|
+
const result = parseUnifiedResetMsHeader(response);
|
|
394
|
+
expect(result).toBeGreaterThan(80_000);
|
|
395
|
+
expect(result).toBeLessThanOrEqual(90_000);
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
it("prefers unified-reset over overage-reset when both present", () => {
|
|
399
|
+
const resetEpoch = Math.floor(Date.now() / 1000) + 120;
|
|
400
|
+
const overageEpoch = Math.floor(Date.now() / 1000) + 300;
|
|
401
|
+
const response = new Response(null, {
|
|
402
|
+
headers: {
|
|
403
|
+
"anthropic-ratelimit-unified-reset": String(resetEpoch),
|
|
404
|
+
"anthropic-ratelimit-unified-overage-reset": String(overageEpoch),
|
|
405
|
+
},
|
|
406
|
+
});
|
|
407
|
+
const result = parseUnifiedResetMsHeader(response);
|
|
408
|
+
expect(result).toBeGreaterThan(110_000);
|
|
409
|
+
expect(result).toBeLessThanOrEqual(120_000);
|
|
410
|
+
});
|
|
411
|
+
});
|
package/lib/config.mjs
CHANGED
|
@@ -104,6 +104,7 @@ import { randomBytes } from "node:crypto";
|
|
|
104
104
|
* @property {{ enabled: boolean, default_cooldown_ms: number, poll_quota_on_overload: boolean }} overload_recovery
|
|
105
105
|
* @property {{ proactive_disabled: boolean }} account_management
|
|
106
106
|
* @property {{ enabled: boolean, length_anchors: boolean }} anti_verbosity
|
|
107
|
+
* @property {{ sdk_token_useragent: boolean }} oauth
|
|
107
108
|
*/
|
|
108
109
|
|
|
109
110
|
/** @type {AnthropicAuthConfig} */
|
|
@@ -434,6 +435,19 @@ export const DEFAULT_CONFIG = {
|
|
|
434
435
|
/** Also inject numeric length anchors (≤25 words between tool calls, ≤100 words final). */
|
|
435
436
|
length_anchors: true,
|
|
436
437
|
},
|
|
438
|
+
/** OAuth token-endpoint request fingerprint. HIGH-RISK knob — leave OFF unless
|
|
439
|
+
* validated against a live refresh. */
|
|
440
|
+
oauth: {
|
|
441
|
+
/** When true, the OAuth token EXCHANGE/REFRESH POSTs use Claude Code
|
|
442
|
+
* 2.1.195's SDK-native-fetch fingerprint (`User-Agent:
|
|
443
|
+
* anthropic-sdk-typescript/0.94.0 userOAuthProvider`, `anthropic-beta:
|
|
444
|
+
* oauth-2025-04-20`, no axios `Accept`). When false (DEFAULT), the legacy
|
|
445
|
+
* axios fingerprint (`User-Agent: axios/1.13.6`, axios `Accept` header) is
|
|
446
|
+
* sent byte-for-byte as before.
|
|
447
|
+
* Default OFF: the token endpoint historically 429'd requests lacking the
|
|
448
|
+
* axios UA, so this stays opt-in until proven against live refresh. */
|
|
449
|
+
sdk_token_useragent: false,
|
|
450
|
+
},
|
|
437
451
|
};
|
|
438
452
|
|
|
439
453
|
export const VALID_STRATEGIES = ["sticky", "round-robin", "hybrid"];
|
|
@@ -473,6 +487,7 @@ function createDefaultConfig() {
|
|
|
473
487
|
microcompact: { ...DEFAULT_CONFIG.microcompact },
|
|
474
488
|
overload_recovery: { ...DEFAULT_CONFIG.overload_recovery },
|
|
475
489
|
account_management: { ...DEFAULT_CONFIG.account_management },
|
|
490
|
+
oauth: { ...DEFAULT_CONFIG.oauth },
|
|
476
491
|
};
|
|
477
492
|
}
|
|
478
493
|
|
|
@@ -1002,6 +1017,15 @@ function validateConfig(raw) {
|
|
|
1002
1017
|
};
|
|
1003
1018
|
}
|
|
1004
1019
|
|
|
1020
|
+
// OAuth token-endpoint fingerprint sub-config
|
|
1021
|
+
if (raw.oauth && typeof raw.oauth === "object") {
|
|
1022
|
+
const o = /** @type {Record<string, unknown>} */ (raw.oauth);
|
|
1023
|
+
config.oauth = {
|
|
1024
|
+
sdk_token_useragent:
|
|
1025
|
+
typeof o.sdk_token_useragent === "boolean" ? o.sdk_token_useragent : DEFAULT_CONFIG.oauth.sdk_token_useragent,
|
|
1026
|
+
};
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1005
1029
|
return config;
|
|
1006
1030
|
}
|
|
1007
1031
|
|
package/lib/oauth.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
-
import { CLIENT_ID } from "./config.mjs";
|
|
2
|
+
import { CLIENT_ID, loadConfig } from "./config.mjs";
|
|
3
3
|
import { parseRetryAfterHeader, parseRetryAfterMsHeader } from "./backoff.mjs";
|
|
4
4
|
|
|
5
5
|
// ---------------------------------------------------------------------------
|
|
@@ -17,6 +17,17 @@ const OAUTH_REVOKE_URL = `https://${OAUTH_CONSOLE_HOST}/v1/oauth/revoke`;
|
|
|
17
17
|
const OAUTH_AXIOS_VERSION = "1.13.6";
|
|
18
18
|
const OAUTH_USER_AGENT = `axios/${OAUTH_AXIOS_VERSION}`;
|
|
19
19
|
const OAUTH_ACCEPT = "application/json, text/plain, */*";
|
|
20
|
+
// Claude Code 2.1.195 migrated the OAuth token client from axios to the
|
|
21
|
+
// Anthropic TS SDK's native-fetch `userOAuthProvider`. When the
|
|
22
|
+
// `oauth.sdk_token_useragent` config flag is ON, the token POST switches to
|
|
23
|
+
// this fingerprint: `User-Agent: anthropic-sdk-typescript/0.94.0
|
|
24
|
+
// userOAuthProvider`, `anthropic-beta: oauth-2025-04-20`, JSON body, and NO
|
|
25
|
+
// axios `Accept` header (native fetch sends no default Accept). The SDK version
|
|
26
|
+
// mirrors request-headers.mjs `ANTHROPIC_SDK_VERSION` ("0.94.0"); hardcoded here
|
|
27
|
+
// to avoid coupling the OAuth path to the messages-header module.
|
|
28
|
+
const OAUTH_SDK_VERSION = "0.94.0";
|
|
29
|
+
const OAUTH_SDK_USER_AGENT = `anthropic-sdk-typescript/${OAUTH_SDK_VERSION} userOAuthProvider`;
|
|
30
|
+
const OAUTH_SDK_BETA = "oauth-2025-04-20";
|
|
20
31
|
const OAUTH_MAX_RETRIES = 2;
|
|
21
32
|
const OAUTH_MAX_RETRY_DELAY_MS = 30_000;
|
|
22
33
|
const OAUTH_RATE_LIMIT_COOLDOWN_MS = 30_000;
|
|
@@ -44,6 +55,44 @@ function generatePKCE() {
|
|
|
44
55
|
return { verifier, challenge, state };
|
|
45
56
|
}
|
|
46
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Resolve whether the SDK-native-fetch token fingerprint should be used.
|
|
60
|
+
* An explicit boolean override (used by callers/tests) wins; otherwise the
|
|
61
|
+
* live `oauth.sdk_token_useragent` config flag is read (default false).
|
|
62
|
+
* @param {boolean | undefined} override
|
|
63
|
+
* @returns {boolean}
|
|
64
|
+
*/
|
|
65
|
+
function useSdkTokenFingerprint(override) {
|
|
66
|
+
if (typeof override === "boolean") return override;
|
|
67
|
+
try {
|
|
68
|
+
return loadConfig()?.oauth?.sdk_token_useragent === true;
|
|
69
|
+
} catch {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Build the headers for an OAuth token POST (exchange/refresh).
|
|
76
|
+
* @param {boolean} useSdk - When true, emit the CC 2.1.195 SDK-native-fetch
|
|
77
|
+
* fingerprint; when false, emit the legacy axios fingerprint (default).
|
|
78
|
+
* @returns {Record<string, string>}
|
|
79
|
+
*/
|
|
80
|
+
function buildTokenHeaders(useSdk) {
|
|
81
|
+
if (useSdk) {
|
|
82
|
+
// Native fetch sends no default Accept header — omit it deliberately.
|
|
83
|
+
return {
|
|
84
|
+
"Content-Type": "application/json",
|
|
85
|
+
"anthropic-beta": OAUTH_SDK_BETA,
|
|
86
|
+
"User-Agent": OAUTH_SDK_USER_AGENT,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
Accept: OAUTH_ACCEPT,
|
|
91
|
+
"Content-Type": "application/json",
|
|
92
|
+
"User-Agent": OAUTH_USER_AGENT,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
47
96
|
/**
|
|
48
97
|
* @param {number} ms
|
|
49
98
|
* @returns {Promise<void>}
|
|
@@ -261,6 +310,8 @@ export function parseOAuthCallback(input) {
|
|
|
261
310
|
* Exchange an authorization code for tokens.
|
|
262
311
|
* @param {string} code
|
|
263
312
|
* @param {string} verifier
|
|
313
|
+
* @param {{ sdkTokenUserAgent?: boolean }} [options] - `sdkTokenUserAgent`
|
|
314
|
+
* overrides the `oauth.sdk_token_useragent` config flag (mainly for tests).
|
|
264
315
|
* @returns {Promise<
|
|
265
316
|
* | { type: "success", refresh: string, access: string, expires: number, email?: string }
|
|
266
317
|
* | {
|
|
@@ -274,7 +325,8 @@ export function parseOAuthCallback(input) {
|
|
|
274
325
|
* }
|
|
275
326
|
* >}
|
|
276
327
|
*/
|
|
277
|
-
export async function exchange(code, verifier) {
|
|
328
|
+
export async function exchange(code, verifier, options = {}) {
|
|
329
|
+
const tokenHeaders = buildTokenHeaders(useSdkTokenFingerprint(options.sdkTokenUserAgent));
|
|
278
330
|
const fail = (status, rawText = "", cooldownHint = { retryAfterMs: null }) => {
|
|
279
331
|
const { errorCode, reason } = parseOAuthErrorBody(rawText);
|
|
280
332
|
const retryAfterMs =
|
|
@@ -312,11 +364,7 @@ export async function exchange(code, verifier) {
|
|
|
312
364
|
};
|
|
313
365
|
result = await fetch(OAUTH_TOKEN_URL, {
|
|
314
366
|
method: "POST",
|
|
315
|
-
headers:
|
|
316
|
-
Accept: OAUTH_ACCEPT,
|
|
317
|
-
"Content-Type": "application/json",
|
|
318
|
-
"User-Agent": OAUTH_USER_AGENT,
|
|
319
|
-
},
|
|
367
|
+
headers: tokenHeaders,
|
|
320
368
|
body: JSON.stringify(_exchangeBody),
|
|
321
369
|
signal: AbortSignal.timeout(15_000),
|
|
322
370
|
});
|
|
@@ -413,24 +461,23 @@ export async function revoke(refreshToken) {
|
|
|
413
461
|
/**
|
|
414
462
|
* Refresh an OAuth access token.
|
|
415
463
|
* @param {string} refreshTokenValue - The refresh token to use
|
|
416
|
-
* @param {{ signal?: AbortSignal }} [options]
|
|
464
|
+
* @param {{ signal?: AbortSignal, scopes?: string[], sdkTokenUserAgent?: boolean }} [options]
|
|
465
|
+
* `sdkTokenUserAgent` overrides the `oauth.sdk_token_useragent` config flag
|
|
466
|
+
* (mainly for tests).
|
|
417
467
|
* @returns {Promise<{access_token: string, refresh_token: string, expires_in: number}>}
|
|
418
468
|
* @throws {Error} On HTTP errors or network failures
|
|
419
469
|
*/
|
|
420
470
|
export async function refreshToken(refreshTokenValue, options = {}) {
|
|
421
471
|
// QA fix M13: default 30s timeout to prevent hanging on unresponsive server
|
|
422
472
|
const signal = options.signal ?? AbortSignal.timeout(30_000);
|
|
473
|
+
const tokenHeaders = buildTokenHeaders(useSdkTokenFingerprint(options.sdkTokenUserAgent));
|
|
423
474
|
|
|
424
475
|
for (let attempt = 0; attempt <= OAUTH_MAX_RETRIES; attempt++) {
|
|
425
476
|
let resp;
|
|
426
477
|
try {
|
|
427
478
|
resp = await fetch(OAUTH_TOKEN_URL, {
|
|
428
479
|
method: "POST",
|
|
429
|
-
headers:
|
|
430
|
-
Accept: OAUTH_ACCEPT,
|
|
431
|
-
"Content-Type": "application/json",
|
|
432
|
-
"User-Agent": OAUTH_USER_AGENT,
|
|
433
|
-
},
|
|
480
|
+
headers: tokenHeaders,
|
|
434
481
|
body: JSON.stringify({
|
|
435
482
|
grant_type: "refresh_token",
|
|
436
483
|
refresh_token: refreshTokenValue,
|
package/lib/oauth.test.mjs
CHANGED
|
@@ -47,6 +47,60 @@ describe("oauth headers", () => {
|
|
|
47
47
|
expect(init.headers["User-Agent"]).toBe("axios/1.13.6");
|
|
48
48
|
});
|
|
49
49
|
|
|
50
|
+
it("flag OFF (default) keeps axios fingerprint on exchange", async () => {
|
|
51
|
+
mockFetch.mockResolvedValueOnce({ ok: false, status: 400, text: async () => "" });
|
|
52
|
+
|
|
53
|
+
await exchange("code#state", "verifier", { sdkTokenUserAgent: false });
|
|
54
|
+
|
|
55
|
+
const [, init] = mockFetch.mock.calls[0];
|
|
56
|
+
expect(init.headers["User-Agent"]).toBe("axios/1.13.6");
|
|
57
|
+
expect(init.headers["Accept"]).toBe("application/json, text/plain, */*");
|
|
58
|
+
expect(init.headers["Content-Type"]).toBe("application/json");
|
|
59
|
+
expect(init.headers["anthropic-beta"]).toBeUndefined();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("flag ON sends SDK native-fetch fingerprint on exchange", async () => {
|
|
63
|
+
mockFetch.mockResolvedValueOnce({ ok: false, status: 400, text: async () => "" });
|
|
64
|
+
|
|
65
|
+
await exchange("code#state", "verifier", { sdkTokenUserAgent: true });
|
|
66
|
+
|
|
67
|
+
const [, init] = mockFetch.mock.calls[0];
|
|
68
|
+
expect(init.headers["User-Agent"]).toBe("anthropic-sdk-typescript/0.94.0 userOAuthProvider");
|
|
69
|
+
expect(init.headers["anthropic-beta"]).toBe("oauth-2025-04-20");
|
|
70
|
+
expect(init.headers["Content-Type"]).toBe("application/json");
|
|
71
|
+
expect("Accept" in init.headers).toBe(false);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("flag OFF (default) keeps axios fingerprint on refresh", async () => {
|
|
75
|
+
mockFetch.mockResolvedValueOnce({
|
|
76
|
+
ok: true,
|
|
77
|
+
json: async () => ({ access_token: "a", refresh_token: "r", expires_in: 3600 }),
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
await refreshToken("refresh-token", { sdkTokenUserAgent: false });
|
|
81
|
+
|
|
82
|
+
const [, init] = mockFetch.mock.calls[0];
|
|
83
|
+
expect(init.headers["User-Agent"]).toBe("axios/1.13.6");
|
|
84
|
+
expect(init.headers["Accept"]).toBe("application/json, text/plain, */*");
|
|
85
|
+
expect(init.headers["Content-Type"]).toBe("application/json");
|
|
86
|
+
expect(init.headers["anthropic-beta"]).toBeUndefined();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("flag ON sends SDK native-fetch fingerprint on refresh", async () => {
|
|
90
|
+
mockFetch.mockResolvedValueOnce({
|
|
91
|
+
ok: true,
|
|
92
|
+
json: async () => ({ access_token: "a", refresh_token: "r", expires_in: 3600 }),
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
await refreshToken("refresh-token", { sdkTokenUserAgent: true });
|
|
96
|
+
|
|
97
|
+
const [, init] = mockFetch.mock.calls[0];
|
|
98
|
+
expect(init.headers["User-Agent"]).toBe("anthropic-sdk-typescript/0.94.0 userOAuthProvider");
|
|
99
|
+
expect(init.headers["anthropic-beta"]).toBe("oauth-2025-04-20");
|
|
100
|
+
expect(init.headers["Content-Type"]).toBe("application/json");
|
|
101
|
+
expect("Accept" in init.headers).toBe(false);
|
|
102
|
+
});
|
|
103
|
+
|
|
50
104
|
it("parses Anthropic nested error payloads on exchange failure", async () => {
|
|
51
105
|
mockFetch.mockResolvedValueOnce({
|
|
52
106
|
ok: false,
|
package/lib/request-headers.mjs
CHANGED
|
@@ -16,15 +16,15 @@
|
|
|
16
16
|
// Version / build / SDK constants
|
|
17
17
|
// ---------------------------------------------------------------------------
|
|
18
18
|
|
|
19
|
-
export const FALLBACK_CLAUDE_CLI_VERSION = "2.1.
|
|
19
|
+
export const FALLBACK_CLAUDE_CLI_VERSION = "2.1.195";
|
|
20
20
|
export const CLAUDE_CODE_NPM_LATEST_URL = "https://registry.npmjs.org/@anthropic-ai/claude-code/latest";
|
|
21
|
-
// Real build markers extracted from the 2.1.
|
|
22
|
-
// (Bun-embedded JS): `BUILD_TIME:"2026-
|
|
23
|
-
// `GIT_SHA:"
|
|
24
|
-
// (Prior 2.1.
|
|
25
|
-
// GIT_SHA
|
|
26
|
-
export const CLAUDE_CODE_BUILD_TIME = "2026-
|
|
27
|
-
export const CLAUDE_CODE_GIT_SHA = "
|
|
21
|
+
// Real build markers extracted from the 2.1.195 native binary
|
|
22
|
+
// (Bun-embedded JS): `BUILD_TIME:"2026-06-26T01:00:56Z"`,
|
|
23
|
+
// `GIT_SHA:"4603aa3f2ea164bd0974f82eb413ae7acc99a7ee"`.
|
|
24
|
+
// (Prior 2.1.159 linux-x64: BUILD_TIME 2026-05-31T16:22:50Z,
|
|
25
|
+
// GIT_SHA dd8c11fc8d05cea0b2b9fc8f5a99a5c5c5dffc9b.)
|
|
26
|
+
export const CLAUDE_CODE_BUILD_TIME = "2026-06-26T01:00:56Z";
|
|
27
|
+
export const CLAUDE_CODE_GIT_SHA = "4603aa3f2ea164bd0974f82eb413ae7acc99a7ee";
|
|
28
28
|
|
|
29
29
|
// The @anthropic-ai/sdk version bundled with Claude Code.
|
|
30
30
|
// This is distinct from the CLI version and goes in X-Stainless-Package-Version.
|
|
@@ -38,6 +38,46 @@ export const ANTHROPIC_SDK_VERSION = "0.94.0";
|
|
|
38
38
|
|
|
39
39
|
// Map of CLI version to bundled SDK version (update when CLI version changes)
|
|
40
40
|
export const CLI_TO_SDK_VERSION = new Map([
|
|
41
|
+
// 2.1.160-2.1.195: refusal-fallback feature (server-side-fallback /
|
|
42
|
+
// fallback-credit betas) is opt-in/GrowthBook-gated and never emitted on a
|
|
43
|
+
// default /v1/messages turn; no wire SDK bump (binary-verified:
|
|
44
|
+
// x-stainless-package-version still "0.94.0" in 2.1.195).
|
|
45
|
+
["2.1.195", "0.94.0"],
|
|
46
|
+
["2.1.194", "0.94.0"],
|
|
47
|
+
["2.1.193", "0.94.0"],
|
|
48
|
+
["2.1.192", "0.94.0"],
|
|
49
|
+
["2.1.191", "0.94.0"],
|
|
50
|
+
["2.1.190", "0.94.0"],
|
|
51
|
+
["2.1.189", "0.94.0"],
|
|
52
|
+
["2.1.188", "0.94.0"],
|
|
53
|
+
["2.1.187", "0.94.0"],
|
|
54
|
+
["2.1.186", "0.94.0"],
|
|
55
|
+
["2.1.185", "0.94.0"],
|
|
56
|
+
["2.1.184", "0.94.0"],
|
|
57
|
+
["2.1.183", "0.94.0"],
|
|
58
|
+
["2.1.182", "0.94.0"],
|
|
59
|
+
["2.1.181", "0.94.0"],
|
|
60
|
+
["2.1.180", "0.94.0"],
|
|
61
|
+
["2.1.179", "0.94.0"],
|
|
62
|
+
["2.1.178", "0.94.0"],
|
|
63
|
+
["2.1.177", "0.94.0"],
|
|
64
|
+
["2.1.176", "0.94.0"],
|
|
65
|
+
["2.1.175", "0.94.0"],
|
|
66
|
+
["2.1.174", "0.94.0"],
|
|
67
|
+
["2.1.173", "0.94.0"],
|
|
68
|
+
["2.1.172", "0.94.0"],
|
|
69
|
+
["2.1.171", "0.94.0"],
|
|
70
|
+
["2.1.170", "0.94.0"],
|
|
71
|
+
["2.1.169", "0.94.0"],
|
|
72
|
+
["2.1.168", "0.94.0"],
|
|
73
|
+
["2.1.167", "0.94.0"],
|
|
74
|
+
["2.1.166", "0.94.0"],
|
|
75
|
+
["2.1.165", "0.94.0"],
|
|
76
|
+
["2.1.164", "0.94.0"],
|
|
77
|
+
["2.1.163", "0.94.0"],
|
|
78
|
+
["2.1.162", "0.94.0"],
|
|
79
|
+
["2.1.161", "0.94.0"],
|
|
80
|
+
["2.1.160", "0.94.0"],
|
|
41
81
|
// 2.1.155-2.1.159: only wire change is the revival of
|
|
42
82
|
// summarize-connector-text-2026-03-13 (label narration_summaries), gated by
|
|
43
83
|
// GrowthBook pewter_owl_header (default-off) — not always-on. SDK still 0.94.0
|
|
@@ -146,6 +186,10 @@ export const EXPERIMENTAL_BETA_FLAGS = new Set([
|
|
|
146
186
|
"context-management-2025-06-27",
|
|
147
187
|
"environments-2025-11-01",
|
|
148
188
|
"extended-cache-ttl-2025-04-11",
|
|
189
|
+
// Refusal-fallback credit beta from the upstream Udd registry (CC 2.1.195).
|
|
190
|
+
// Opt-in/GrowthBook-gated; never auto-emitted on a default /v1/messages turn.
|
|
191
|
+
// Listed here for the disable-experimental guard + manual opt-in ONLY.
|
|
192
|
+
"fallback-credit-2026-06-01",
|
|
149
193
|
"fast-mode-2026-02-01",
|
|
150
194
|
"files-api-2025-04-14",
|
|
151
195
|
"interleaved-thinking-2025-05-14",
|
|
@@ -158,6 +202,10 @@ export const EXPERIMENTAL_BETA_FLAGS = new Set([
|
|
|
158
202
|
"mid-conversation-system-2026-04-07",
|
|
159
203
|
"prompt-caching-scope-2026-01-05",
|
|
160
204
|
"redact-thinking-2026-02-12",
|
|
205
|
+
// Server-side refusal-fallback beta from the upstream Udd registry (CC 2.1.195).
|
|
206
|
+
// Opt-in/GrowthBook-gated; never auto-emitted on a default /v1/messages turn.
|
|
207
|
+
// Listed here for the disable-experimental guard + manual opt-in ONLY.
|
|
208
|
+
"server-side-fallback-2026-06-01",
|
|
161
209
|
"structured-outputs-2025-12-15",
|
|
162
210
|
// Revived in CC 2.1.159 as registry label `narration_summaries` (it was a dead
|
|
163
211
|
// slot from v2.1.90-2.1.154). Real CC emits it only when GrowthBook flag
|
|
@@ -208,6 +256,9 @@ export const BETA_SHORTCUTS = new Map([
|
|
|
208
256
|
["connector-text", "summarize-connector-text-2026-03-13"],
|
|
209
257
|
["narration-summaries", "summarize-connector-text-2026-03-13"],
|
|
210
258
|
["summarize-connector-text", "summarize-connector-text-2026-03-13"],
|
|
259
|
+
["server-side-fallback", "server-side-fallback-2026-06-01"],
|
|
260
|
+
["fallback", "server-side-fallback-2026-06-01"],
|
|
261
|
+
["fallback-credit", "fallback-credit-2026-06-01"],
|
|
211
262
|
]);
|
|
212
263
|
|
|
213
264
|
// ---------------------------------------------------------------------------
|
|
@@ -8,18 +8,21 @@ import {
|
|
|
8
8
|
ANTHROPIC_SDK_VERSION,
|
|
9
9
|
getSdkVersion,
|
|
10
10
|
BETA_SHORTCUTS,
|
|
11
|
+
EXPERIMENTAL_BETA_FLAGS,
|
|
11
12
|
resolveBetaShortcut,
|
|
12
13
|
buildExtendedUserAgent,
|
|
13
14
|
} from "./request-headers.mjs";
|
|
14
15
|
|
|
15
16
|
describe("request-headers constants", () => {
|
|
16
|
-
it("FALLBACK_CLAUDE_CLI_VERSION is 2.1.
|
|
17
|
-
expect(FALLBACK_CLAUDE_CLI_VERSION).toBe("2.1.
|
|
17
|
+
it("FALLBACK_CLAUDE_CLI_VERSION is 2.1.195", () => {
|
|
18
|
+
expect(FALLBACK_CLAUDE_CLI_VERSION).toBe("2.1.195");
|
|
18
19
|
});
|
|
19
20
|
|
|
20
21
|
it("ANTHROPIC_SDK_VERSION matches expected value", () => {
|
|
21
22
|
expect(typeof ANTHROPIC_SDK_VERSION).toBe("string");
|
|
22
23
|
expect(ANTHROPIC_SDK_VERSION).toMatch(/^\d+\.\d+\.\d+$/);
|
|
24
|
+
// The wire SDK version did NOT bump for 2.1.195.
|
|
25
|
+
expect(ANTHROPIC_SDK_VERSION).toBe("0.94.0");
|
|
23
26
|
});
|
|
24
27
|
});
|
|
25
28
|
|
|
@@ -31,6 +34,12 @@ describe("getSdkVersion", () => {
|
|
|
31
34
|
expect(v).toMatch(/^\d+\.\d+\.\d+$/);
|
|
32
35
|
});
|
|
33
36
|
|
|
37
|
+
it("resolves 2.1.195 (and the 2.1.160-2.1.194 range) to 0.94.0", () => {
|
|
38
|
+
expect(getSdkVersion("2.1.195")).toBe("0.94.0");
|
|
39
|
+
expect(getSdkVersion("2.1.160")).toBe("0.94.0");
|
|
40
|
+
expect(getSdkVersion("2.1.180")).toBe("0.94.0");
|
|
41
|
+
});
|
|
42
|
+
|
|
34
43
|
it("falls back to ANTHROPIC_SDK_VERSION for an unknown CLI version", () => {
|
|
35
44
|
expect(getSdkVersion("9.99.999")).toBe(ANTHROPIC_SDK_VERSION);
|
|
36
45
|
});
|
|
@@ -53,6 +62,26 @@ describe("BETA_SHORTCUTS", () => {
|
|
|
53
62
|
it("has a cache-diag alias for cache-diagnosis-2026-04-07", () => {
|
|
54
63
|
expect(BETA_SHORTCUTS.get("cache-diag")).toBe("cache-diagnosis-2026-04-07");
|
|
55
64
|
});
|
|
65
|
+
|
|
66
|
+
it("has server-side-fallback / fallback aliases for server-side-fallback-2026-06-01", () => {
|
|
67
|
+
expect(BETA_SHORTCUTS.get("server-side-fallback")).toBe("server-side-fallback-2026-06-01");
|
|
68
|
+
expect(BETA_SHORTCUTS.get("fallback")).toBe("server-side-fallback-2026-06-01");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("has a fallback-credit alias for fallback-credit-2026-06-01", () => {
|
|
72
|
+
expect(BETA_SHORTCUTS.get("fallback-credit")).toBe("fallback-credit-2026-06-01");
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe("EXPERIMENTAL_BETA_FLAGS", () => {
|
|
77
|
+
it("is a Set", () => {
|
|
78
|
+
expect(EXPERIMENTAL_BETA_FLAGS).toBeInstanceOf(Set);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("includes the 2.1.195 refusal-fallback registry betas", () => {
|
|
82
|
+
expect(EXPERIMENTAL_BETA_FLAGS.has("server-side-fallback-2026-06-01")).toBe(true);
|
|
83
|
+
expect(EXPERIMENTAL_BETA_FLAGS.has("fallback-credit-2026-06-01")).toBe(true);
|
|
84
|
+
});
|
|
56
85
|
});
|
|
57
86
|
|
|
58
87
|
describe("resolveBetaShortcut", () => {
|
|
@@ -72,6 +101,15 @@ describe("resolveBetaShortcut", () => {
|
|
|
72
101
|
expect(resolveBetaShortcut("fast")).toBe("fast-mode-2026-02-01");
|
|
73
102
|
});
|
|
74
103
|
|
|
104
|
+
it("expands server-side-fallback / fallback shortcuts", () => {
|
|
105
|
+
expect(resolveBetaShortcut("server-side-fallback")).toBe("server-side-fallback-2026-06-01");
|
|
106
|
+
expect(resolveBetaShortcut("fallback")).toBe("server-side-fallback-2026-06-01");
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("expands fallback-credit shortcut", () => {
|
|
110
|
+
expect(resolveBetaShortcut("fallback-credit")).toBe("fallback-credit-2026-06-01");
|
|
111
|
+
});
|
|
112
|
+
|
|
75
113
|
it("returns the input unchanged when no alias matches", () => {
|
|
76
114
|
expect(resolveBetaShortcut("context-hint-2026-04-09")).toBe("context-hint-2026-04-09");
|
|
77
115
|
expect(resolveBetaShortcut("no-such-beta")).toBe("no-such-beta");
|
|
@@ -121,5 +159,6 @@ describe("buildExtendedUserAgent", () => {
|
|
|
121
159
|
it("uses FALLBACK_CLAUDE_CLI_VERSION when explicitly passed as the version", () => {
|
|
122
160
|
const ua = buildExtendedUserAgent(FALLBACK_CLAUDE_CLI_VERSION);
|
|
123
161
|
expect(ua).toContain(FALLBACK_CLAUDE_CLI_VERSION);
|
|
162
|
+
expect(ua).toBe("claude-cli/2.1.195 (external, cli)");
|
|
124
163
|
});
|
|
125
164
|
});
|