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
package/index.mjs
CHANGED
|
@@ -3,7 +3,12 @@ import { stdin, stdout } from "node:process";
|
|
|
3
3
|
import { randomBytes, randomUUID, createHash as createHashCrypto } from "node:crypto";
|
|
4
4
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { join, resolve, basename } from "node:path";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
AccountManager,
|
|
8
|
+
RATE_LIMIT_KEY_FAST,
|
|
9
|
+
isBetaRetryAccountUsable,
|
|
10
|
+
warnOnEphemeralTokenRotation,
|
|
11
|
+
} from "./lib/accounts.mjs";
|
|
7
12
|
import {
|
|
8
13
|
authorize as oauthAuthorize,
|
|
9
14
|
exchange as oauthExchange,
|
|
@@ -37,6 +42,7 @@ import { redactSecrets, redactString } from "./lib/redact.mjs";
|
|
|
37
42
|
import {
|
|
38
43
|
createTransformedSSEStream,
|
|
39
44
|
resolveStreamIdleTimeoutMs,
|
|
45
|
+
shouldApplyHostCompatShim,
|
|
40
46
|
stripMcpPrefixFromParsedEvent,
|
|
41
47
|
} from "./lib/mimicry/response-stream.mjs";
|
|
42
48
|
import { transformRequestBody, CORE_TOOL_NAMES } from "./lib/mimicry/request-body.mjs";
|
|
@@ -78,6 +84,7 @@ import {
|
|
|
78
84
|
assertAdapterBodyUsable,
|
|
79
85
|
buildAdapterTransport,
|
|
80
86
|
resolveAdapterEnv,
|
|
87
|
+
selectLatchableRejectedBetas,
|
|
81
88
|
} from "./lib/mimicry/adapter-input.mjs";
|
|
82
89
|
import { buildPassthroughHeaders, stripNonApiBodyFields } from "./lib/passthrough-headers.mjs";
|
|
83
90
|
import {
|
|
@@ -311,6 +318,21 @@ export async function AnthropicAuthPlugin({ client }) {
|
|
|
311
318
|
const SESSION_REJECTED_BETA_TTL_MS = 5 * 60 * 1000; // 5 minutes
|
|
312
319
|
/** @type {Map<string, number>} canonical-beta to rejected-at epoch ms */
|
|
313
320
|
const sessionRejectedBetas = new Map();
|
|
321
|
+
// Sent-beta suppression latch, keyed by ACCOUNT. Holds only betas that were on
|
|
322
|
+
// the failing request's header AND named by the API's invalid-beta message
|
|
323
|
+
// (selectLatchableRejectedBetas); they reach the adapter as `rejectedBetas` ->
|
|
324
|
+
// `suppressBetas` for SESSION_REJECTED_BETA_TTL_MS. Unlike
|
|
325
|
+
// `sessionRejectedBetas` (a custom_betas filter only), this one removes
|
|
326
|
+
// package-composed betas, so it must not leak to accounts that never
|
|
327
|
+
// received the rejection. Memory only.
|
|
328
|
+
/** @type {Map<string, Map<string, number>>} account key to (beta to rejected-at epoch ms) */
|
|
329
|
+
const sentBetaSuppressionsByAccount = new Map();
|
|
330
|
+
/**
|
|
331
|
+
* @param {{id?: unknown, index?: unknown} | null | undefined} acct
|
|
332
|
+
* @returns {string}
|
|
333
|
+
*/
|
|
334
|
+
const sentBetaLatchKey = (acct) =>
|
|
335
|
+
typeof acct?.id === "string" && acct.id ? acct.id : `index:${String(acct?.index ?? "")}`;
|
|
314
336
|
|
|
315
337
|
// Token economy — session state for layered compaction strategies.
|
|
316
338
|
const tokenEconomySession = {
|
|
@@ -2592,6 +2614,18 @@ export async function AnthropicAuthPlugin({ client }) {
|
|
|
2592
2614
|
const requestMethod = String(
|
|
2593
2615
|
requestInit.method || (requestInput instanceof Request ? requestInput.method : "POST"),
|
|
2594
2616
|
).toUpperCase();
|
|
2617
|
+
// The HOST's own User-Agent (not the one sent to Anthropic): it
|
|
2618
|
+
// names the host's @ai-sdk/anthropic version, which decides
|
|
2619
|
+
// whether the L5 host-compat stream shim must run.
|
|
2620
|
+
const hostCompatShim = shouldApplyHostCompatShim(getIncomingHeader(input, requestInit, "user-agent"));
|
|
2621
|
+
/** @param {{blockTypeFallbacks: number, deltasDropped: number, eventsDropped: number, dryRun?: boolean}} shimStats */
|
|
2622
|
+
const onHostCompatShim = (shimStats) =>
|
|
2623
|
+
shimStats.dryRun
|
|
2624
|
+
? debugLog(
|
|
2625
|
+
"host-compat shim disabled for this host SDK version, but would have rewritten the response stream (dry run) — allowlists may need re-extracting",
|
|
2626
|
+
shimStats,
|
|
2627
|
+
)
|
|
2628
|
+
: debugLog("host-compat shim rewrote the response stream", shimStats);
|
|
2595
2629
|
let showUsageToast;
|
|
2596
2630
|
try {
|
|
2597
2631
|
showUsageToast = new URL(requestUrl).pathname === "/v1/messages" && requestMethod === "POST";
|
|
@@ -2699,10 +2733,25 @@ export async function AnthropicAuthPlugin({ client }) {
|
|
|
2699
2733
|
let _overloadRecoveryAttempted = false; // Guard: only one quota-aware switch per request
|
|
2700
2734
|
let _connectionResetRetries = 0; // Cap ECONNRESET/EPIPE retries to prevent infinite loop
|
|
2701
2735
|
let customBetasStripped = false; // One-shot latch: strip config.custom_betas once per logical request
|
|
2736
|
+
let sentBetaRejectionRetried = false; // One-shot latch: retry once after latching a rejected SENT beta
|
|
2737
|
+
/** @type {{key: string, betas: string[], recordedAt: number} | null} */
|
|
2738
|
+
let pendingSentBetaLatch = null; // Entries latched for the retry in flight; dropped if it fails the same way
|
|
2739
|
+
// One-shot: the account that just received a beta rejection. The
|
|
2740
|
+
// beta-rejection retry (`attempt--; continue`) must land on it: the
|
|
2741
|
+
// sent-beta latch is keyed per account, so a rotating strategy
|
|
2742
|
+
// (round-robin, hybrid) moving the retry to another account would
|
|
2743
|
+
// resend the rejected beta and burn the single retry.
|
|
2744
|
+
let betaRetryAccount = null;
|
|
2702
2745
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
2703
|
-
// Select account —
|
|
2704
|
-
|
|
2705
|
-
|
|
2746
|
+
// Select account — a beta-rejection retry stays on the rejecting
|
|
2747
|
+
// account (QA fix L5: only if it's still enabled and still
|
|
2748
|
+
// tracked by the account manager — see isBetaRetryAccountUsable);
|
|
2749
|
+
// otherwise use the pinned account on the first attempt.
|
|
2750
|
+
const _betaRetryAccount = betaRetryAccount;
|
|
2751
|
+
betaRetryAccount = null;
|
|
2752
|
+
const account = isBetaRetryAccountUsable(_betaRetryAccount, accountManager, transientRefreshSkips)
|
|
2753
|
+
? _betaRetryAccount
|
|
2754
|
+
: attempt === 0 && pinnedAccount && !transientRefreshSkips.has(pinnedAccount.index)
|
|
2706
2755
|
? pinnedAccount
|
|
2707
2756
|
: accountManager.getCurrentAccount(transientRefreshSkips);
|
|
2708
2757
|
|
|
@@ -2937,6 +2986,29 @@ export async function AnthropicAuthPlugin({ client }) {
|
|
|
2937
2986
|
}
|
|
2938
2987
|
return false;
|
|
2939
2988
|
});
|
|
2989
|
+
// Evicting a rejected beta from `customBetas` is not enough on the
|
|
2990
|
+
// adapter path: the package composes some betas on its own (e.g.
|
|
2991
|
+
// `cache-diagnosis-2026-04-07` under 2.1.280), and only
|
|
2992
|
+
// `suppressBetas` (its terminal filter) can take those off the
|
|
2993
|
+
// wire. But suppression also removes the package's OWN copy of a
|
|
2994
|
+
// beta (e.g. context-1m under adaptive 1M), so it is reserved for
|
|
2995
|
+
// betas the API explicitly named as invalid for THIS account: the
|
|
2996
|
+
// unexpired entries of `sentBetaSuppressionsByAccount`. The
|
|
2997
|
+
// `customBetasStripped` one-shot and `sessionRejectedBetas` only
|
|
2998
|
+
// filter `customBetas` above, exactly as before 2.1.280. The
|
|
2999
|
+
// adapter never suppresses UNSUPPRESSIBLE_BETAS or
|
|
3000
|
+
// BODY_COUPLED_BETAS from this list.
|
|
3001
|
+
const _sessionRejectedBetaNames = new Set();
|
|
3002
|
+
const _accountSentBetaLatch = sentBetaSuppressionsByAccount.get(sentBetaLatchKey(account));
|
|
3003
|
+
if (_accountSentBetaLatch) {
|
|
3004
|
+
for (const [beta, rejectedAt] of _accountSentBetaLatch) {
|
|
3005
|
+
if (Date.now() - rejectedAt > SESSION_REJECTED_BETA_TTL_MS) {
|
|
3006
|
+
_accountSentBetaLatch.delete(beta);
|
|
3007
|
+
} else {
|
|
3008
|
+
_sessionRejectedBetaNames.add(beta);
|
|
3009
|
+
}
|
|
3010
|
+
}
|
|
3011
|
+
}
|
|
2940
3012
|
// PHASE 2.2.3 — THE BETA LATCH IS GONE. It used to merge every
|
|
2941
3013
|
// beta ever sent this session back into this value, to avoid
|
|
2942
3014
|
// server-side cache-key churn from a beta flipping mid-session.
|
|
@@ -3155,6 +3227,7 @@ export async function AnthropicAuthPlugin({ client }) {
|
|
|
3155
3227
|
enabled: getSignatureEmulationEnabled(),
|
|
3156
3228
|
claudeCliVersion,
|
|
3157
3229
|
customBetas: _sessionFilteredCustomBetas,
|
|
3230
|
+
rejectedBetas: [..._sessionRejectedBetaNames],
|
|
3158
3231
|
strategy: getEffectiveStrategy(),
|
|
3159
3232
|
sessionId: signatureSessionId,
|
|
3160
3233
|
};
|
|
@@ -3705,43 +3778,121 @@ export async function AnthropicAuthPlugin({ client }) {
|
|
|
3705
3778
|
// context_window, long context, 1m/million context) to avoid false positives
|
|
3706
3779
|
// on generic 413s (e.g. plain upload-size limits with an empty body).
|
|
3707
3780
|
// One retry per logical request (latch prevents loop).
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
(config.custom_betas?.length ?? 0) > 0 &&
|
|
3711
|
-
((response.status === 400 &&
|
|
3781
|
+
const _betaRejectionSignal =
|
|
3782
|
+
(response.status === 400 &&
|
|
3712
3783
|
errorBody &&
|
|
3713
3784
|
errorBody.includes("anthropic-beta") &&
|
|
3714
3785
|
!errorBody.includes("context-hint")) ||
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
3786
|
+
(response.status === 413 &&
|
|
3787
|
+
errorBody &&
|
|
3788
|
+
/anthropic-beta|beta header|unsupported beta|unknown beta|invalid beta|context_window|long context|1m context|million context/i.test(
|
|
3789
|
+
errorBody,
|
|
3790
|
+
));
|
|
3791
|
+
// The retry that followed a sent-beta latch failed the same way on
|
|
3792
|
+
// the SAME account (so the suppression was actually applied): it
|
|
3793
|
+
// did not help, so drop the entries just latched instead of
|
|
3794
|
+
// carrying a useless 5-minute suppression.
|
|
3795
|
+
const _latchUnderTest = pendingSentBetaLatch;
|
|
3796
|
+
pendingSentBetaLatch = null;
|
|
3797
|
+
if (_betaRejectionSignal && _latchUnderTest && _latchUnderTest.key === sentBetaLatchKey(account)) {
|
|
3798
|
+
const _latched = sentBetaSuppressionsByAccount.get(_latchUnderTest.key);
|
|
3799
|
+
for (const _beta of _latchUnderTest.betas) {
|
|
3800
|
+
if (_latched?.get(_beta) === _latchUnderTest.recordedAt) _latched.delete(_beta);
|
|
3801
|
+
}
|
|
3802
|
+
debugLog("sent beta latch did not clear the rejection - dropped", _latchUnderTest.betas);
|
|
3803
|
+
}
|
|
3804
|
+
// Betas the API's invalid-beta message ("Unexpected value(s) `X`
|
|
3805
|
+
// for the `anthropic-beta` header") names that were actually on
|
|
3806
|
+
// the failing request's header. Under 2.1.280 the package
|
|
3807
|
+
// composes some betas by default (e.g. cache-diagnosis), so a
|
|
3808
|
+
// rejection can name a beta the user never configured; only the
|
|
3809
|
+
// adapter path can suppress those (via `rejectedBetas` ->
|
|
3810
|
+
// `suppressBetas`). `_sentLatchableBetas` excludes OAuth / Claude
|
|
3811
|
+
// Code (UNSUPPRESSIBLE_BETAS) and BODY_COUPLED_BETAS, and both
|
|
3812
|
+
// lists are empty when the error echoes the header (too many
|
|
3813
|
+
// matches). count_tokens is excluded: its input has no
|
|
3814
|
+
// suppressBetas, so a latch would not change its retry and would
|
|
3815
|
+
// only degrade the following /v1/messages requests.
|
|
3816
|
+
let _namedSentBetas = [];
|
|
3817
|
+
let _sentLatchableBetas = [];
|
|
3818
|
+
if (_betaRejectionSignal && _useAdapter && !_isCountTokens) {
|
|
3819
|
+
const _sentHeader =
|
|
3820
|
+
typeof requestHeaders?.get === "function"
|
|
3821
|
+
? requestHeaders.get("anthropic-beta")
|
|
3822
|
+
: requestHeaders?.["anthropic-beta"];
|
|
3823
|
+
({ namedSent: _namedSentBetas, latchable: _sentLatchableBetas } = selectLatchableRejectedBetas(
|
|
3824
|
+
_sentHeader,
|
|
3825
|
+
errorBody,
|
|
3826
|
+
));
|
|
3827
|
+
}
|
|
3828
|
+
/**
|
|
3829
|
+
* Latch `_sentLatchableBetas` for the account that received the
|
|
3830
|
+
* rejection, and remember them so a failed retry can undo it.
|
|
3831
|
+
* @param {number} recordedAt
|
|
3832
|
+
*/
|
|
3833
|
+
const _latchSentBetas = (recordedAt) => {
|
|
3834
|
+
const _key = sentBetaLatchKey(account);
|
|
3835
|
+
let _latched = sentBetaSuppressionsByAccount.get(_key);
|
|
3836
|
+
if (!_latched) {
|
|
3837
|
+
_latched = new Map();
|
|
3838
|
+
sentBetaSuppressionsByAccount.set(_key, _latched);
|
|
3839
|
+
}
|
|
3840
|
+
for (const _beta of _sentLatchableBetas) _latched.set(_beta, recordedAt);
|
|
3841
|
+
pendingSentBetaLatch = { key: _key, betas: [..._sentLatchableBetas], recordedAt };
|
|
3842
|
+
sentBetaRejectionRetried = true;
|
|
3843
|
+
};
|
|
3844
|
+
if (!customBetasStripped && (config.custom_betas?.length ?? 0) > 0 && _betaRejectionSignal) {
|
|
3721
3845
|
customBetasStripped = true;
|
|
3722
3846
|
// F4: record rejection in session latch so next logical request already
|
|
3723
|
-
// omits the rejected beta without needing a first-fail.
|
|
3724
|
-
//
|
|
3725
|
-
//
|
|
3847
|
+
// omits the rejected beta from `customBetas` without needing a first-fail.
|
|
3848
|
+
// When the invalid-beta message names betas that were sent, record only
|
|
3849
|
+
// the custom betas among them. Otherwise (no parsable names) fall back to
|
|
3850
|
+
// custom betas mentioned in the body, then to all custom betas. This latch
|
|
3851
|
+
// only filters `customBetas`; it never suppresses a package-composed beta.
|
|
3726
3852
|
{
|
|
3727
3853
|
const _allCustom = config.custom_betas ?? [];
|
|
3728
|
-
|
|
3729
|
-
|
|
3730
|
-
const
|
|
3854
|
+
let _toRecord;
|
|
3855
|
+
if (_namedSentBetas.length > 0) {
|
|
3856
|
+
const _namedLc = new Set(_namedSentBetas.map((_b) => _b.toLowerCase()));
|
|
3857
|
+
_toRecord = _allCustom.filter((_sb) => _namedLc.has(resolveBetaShortcut(_sb).toLowerCase()));
|
|
3858
|
+
} else {
|
|
3731
3859
|
const _bodyLc = (errorBody || "").toLowerCase();
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
|
|
3860
|
+
const _mentioned = _allCustom.filter((_sb) => {
|
|
3861
|
+
const _rawLc = _sb.toLowerCase();
|
|
3862
|
+
const _canLc = resolveBetaShortcut(_sb).toLowerCase();
|
|
3863
|
+
return _bodyLc.includes(_rawLc) || _bodyLc.includes(_canLc);
|
|
3864
|
+
});
|
|
3865
|
+
_toRecord = _mentioned.length > 0 ? _mentioned : _allCustom;
|
|
3866
|
+
}
|
|
3735
3867
|
const _recordedAt = Date.now();
|
|
3736
3868
|
for (const _sb of _toRecord) {
|
|
3737
3869
|
sessionRejectedBetas.set(resolveBetaShortcut(_sb), _recordedAt);
|
|
3738
3870
|
}
|
|
3871
|
+
// Same single retry also suppresses any named SENT beta, so it does
|
|
3872
|
+
// not take a second retry to clear a package-composed one.
|
|
3873
|
+
if (_sentLatchableBetas.length > 0) _latchSentBetas(_recordedAt);
|
|
3739
3874
|
}
|
|
3740
3875
|
attempt--;
|
|
3876
|
+
betaRetryAccount = account;
|
|
3741
3877
|
debugLog("custom beta/context rejection - retrying without custom betas");
|
|
3742
3878
|
continue;
|
|
3743
3879
|
}
|
|
3744
3880
|
|
|
3881
|
+
// Sent-beta rejection retry: the error names a beta that was on the
|
|
3882
|
+
// wire but is not (only) a custom beta, e.g. the package-default
|
|
3883
|
+
// cache-diagnosis with empty custom_betas. Latch it for this account
|
|
3884
|
+
// (TTL'd, as above) and retry once; `sentBetaRejectionRetried`
|
|
3885
|
+
// prevents a loop. Like the custom-beta path, this runs before any
|
|
3886
|
+
// account-failure bookkeeping and on a non-ok response, so no stream
|
|
3887
|
+
// was delivered.
|
|
3888
|
+
if (!sentBetaRejectionRetried && _sentLatchableBetas.length > 0) {
|
|
3889
|
+
_latchSentBetas(Date.now());
|
|
3890
|
+
attempt--;
|
|
3891
|
+
betaRetryAccount = account;
|
|
3892
|
+
debugLog("sent beta rejection - retrying without", _sentLatchableBetas);
|
|
3893
|
+
continue;
|
|
3894
|
+
}
|
|
3895
|
+
|
|
3745
3896
|
// Reactive compaction: on "prompt too long" error, trim oldest messages and retry once
|
|
3746
3897
|
if (
|
|
3747
3898
|
response.status === 400 &&
|
|
@@ -3909,7 +4060,10 @@ export async function AnthropicAuthPlugin({ client }) {
|
|
|
3909
4060
|
status: response.status,
|
|
3910
4061
|
errorBody: typeof errorBody === "string" ? errorBody.slice(0, 600) : errorBody,
|
|
3911
4062
|
});
|
|
3912
|
-
return transformResponse(response, undefined, undefined, correlationId
|
|
4063
|
+
return transformResponse(response, undefined, undefined, correlationId, {
|
|
4064
|
+
hostCompatShim,
|
|
4065
|
+
onHostCompatShim,
|
|
4066
|
+
});
|
|
3913
4067
|
}
|
|
3914
4068
|
|
|
3915
4069
|
const accountSpecific = isAccountSpecificError(response.status, errorBody);
|
|
@@ -4135,7 +4289,10 @@ export async function AnthropicAuthPlugin({ client }) {
|
|
|
4135
4289
|
status: response.status,
|
|
4136
4290
|
});
|
|
4137
4291
|
}
|
|
4138
|
-
return transformResponse(response, undefined, undefined, correlationId
|
|
4292
|
+
return transformResponse(response, undefined, undefined, correlationId, {
|
|
4293
|
+
hostCompatShim,
|
|
4294
|
+
onHostCompatShim,
|
|
4295
|
+
});
|
|
4139
4296
|
}
|
|
4140
4297
|
|
|
4141
4298
|
// Success
|
|
@@ -4331,6 +4488,7 @@ export async function AnthropicAuthPlugin({ client }) {
|
|
|
4331
4488
|
: null,
|
|
4332
4489
|
accountErrorCallback,
|
|
4333
4490
|
correlationId,
|
|
4491
|
+
{ hostCompatShim, onHostCompatShim },
|
|
4334
4492
|
);
|
|
4335
4493
|
}
|
|
4336
4494
|
|
|
@@ -6086,15 +6244,27 @@ function transformRequestUrl(input, emulateSignature = true) {
|
|
|
6086
6244
|
* @param {((details: {reason: import('./lib/backoff.mjs').RateLimitReason, invalidateToken: boolean}) => void) | null} [onAccountError]
|
|
6087
6245
|
* Called if a mid-stream error looks account-specific
|
|
6088
6246
|
* @param {string} [correlationId]
|
|
6247
|
+
* @param {object} [hostCompat] - L5 host-compat shim wiring.
|
|
6248
|
+
* @param {boolean} [hostCompat.hostCompatShim] - Run the shim (see
|
|
6249
|
+
* `shouldApplyHostCompatShim`); defaults to on.
|
|
6250
|
+
* @param {((stats: {blockTypeFallbacks: number, deltasDropped: number, eventsDropped: number, dryRun?: boolean}) => void) | null} [hostCompat.onHostCompatShim]
|
|
6089
6251
|
* @returns {Response}
|
|
6090
6252
|
*/
|
|
6091
|
-
function transformResponse(
|
|
6253
|
+
function transformResponse(
|
|
6254
|
+
response,
|
|
6255
|
+
onUsage,
|
|
6256
|
+
onAccountError,
|
|
6257
|
+
correlationId,
|
|
6258
|
+
{ hostCompatShim = true, onHostCompatShim = null } = {},
|
|
6259
|
+
) {
|
|
6092
6260
|
if (!response.body) return response;
|
|
6093
6261
|
const idleTimeoutMs = resolveStreamIdleTimeoutMs(_pluginConfig);
|
|
6094
6262
|
const captureEnabled = _pluginConfig?.token_economy?.debug_dump_bodies === true && Boolean(correlationId);
|
|
6095
6263
|
const stream = createTransformedSSEStream(response, {
|
|
6096
6264
|
onUsage,
|
|
6097
6265
|
onAccountError,
|
|
6266
|
+
hostCompatShim,
|
|
6267
|
+
onHostCompatShim,
|
|
6098
6268
|
correlationId,
|
|
6099
6269
|
idleTimeoutMs,
|
|
6100
6270
|
captureEnabled,
|
|
@@ -6103,6 +6273,13 @@ function transformResponse(response, onUsage, onAccountError, correlationId) {
|
|
|
6103
6273
|
|
|
6104
6274
|
// Inject cache transparency headers (session-level, available before stream completes).
|
|
6105
6275
|
const responseHeaders = new Headers(response.headers);
|
|
6276
|
+
// QA fix L3: undici already decoded `response.body` (e.g. gzip was
|
|
6277
|
+
// transparently inflated) before it ever reached createTransformedSSEStream,
|
|
6278
|
+
// so a Content-Length/Content-Encoding copied from the upstream response
|
|
6279
|
+
// describes the ORIGINAL (possibly compressed) wire bytes, not the stream
|
|
6280
|
+
// this Response actually serves — forwarding either would lie to the host.
|
|
6281
|
+
responseHeaders.delete("content-length");
|
|
6282
|
+
responseHeaders.delete("content-encoding");
|
|
6106
6283
|
responseHeaders.set("x-opencode-cache-hit-rate", String(Math.round(getAverageCacheHitRate() * 1000) / 1000));
|
|
6107
6284
|
responseHeaders.set("x-opencode-cache-read-total", String(sessionMetrics.totalCacheRead));
|
|
6108
6285
|
responseHeaders.set("x-opencode-session-cost", String(Math.round(sessionMetrics.sessionCostUsd * 10000) / 10000));
|
package/lib/accounts.mjs
CHANGED
|
@@ -1193,3 +1193,33 @@ export class AccountManager {
|
|
|
1193
1193
|
};
|
|
1194
1194
|
}
|
|
1195
1195
|
}
|
|
1196
|
+
|
|
1197
|
+
/**
|
|
1198
|
+
* Whether a one-shot beta-rejection retry pin (`betaRetryAccount` in the
|
|
1199
|
+
* fetch interceptor's retry loop) should still be honored for the retry
|
|
1200
|
+
* attempt it was set for (QA fix L5). Extracted from the loop closure so it
|
|
1201
|
+
* can be unit-tested without bootstrapping the full plugin.
|
|
1202
|
+
*
|
|
1203
|
+
* The pin is a live object reference captured when the *previous* attempt's
|
|
1204
|
+
* response named a rejected beta; by the time the retry lands, that same
|
|
1205
|
+
* account could have been disabled (CLI toggle, a terminal token-refresh
|
|
1206
|
+
* failure) or removed outright (account manager reload/edit) — either way it
|
|
1207
|
+
* must not be force-selected, or the retry could land on an account the rest
|
|
1208
|
+
* of the plugin no longer considers usable. `enabled` is read straight off
|
|
1209
|
+
* `betaRetryAccount` (accounts are mutated in place, so a live reference
|
|
1210
|
+
* reflects the current state), and "present in the account manager" is
|
|
1211
|
+
* checked by identity — not just index — since `removeAccount` re-indexes
|
|
1212
|
+
* the accounts that remain, so a stale index could otherwise coincidentally
|
|
1213
|
+
* match a different, unrelated account.
|
|
1214
|
+
*
|
|
1215
|
+
* @param {{index: number, enabled: boolean} | null} betaRetryAccount
|
|
1216
|
+
* @param {AccountManager} accountManager
|
|
1217
|
+
* @param {Set<number>} transientRefreshSkips - Indices excluded for the rest of this request.
|
|
1218
|
+
* @returns {boolean}
|
|
1219
|
+
*/
|
|
1220
|
+
export function isBetaRetryAccountUsable(betaRetryAccount, accountManager, transientRefreshSkips) {
|
|
1221
|
+
if (!betaRetryAccount) return false;
|
|
1222
|
+
if (transientRefreshSkips.has(betaRetryAccount.index)) return false;
|
|
1223
|
+
if (!betaRetryAccount.enabled) return false;
|
|
1224
|
+
return accountManager.getEnabledAccounts().includes(betaRetryAccount);
|
|
1225
|
+
}
|
package/lib/betas.mjs
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* ADDS or SUPPRESSES on top of them.
|
|
14
14
|
*
|
|
15
15
|
* RECONCILIATION IS ENFORCED BY TEST, not by convention. Every literal header
|
|
16
|
-
* below is checked against the package's `
|
|
16
|
+
* below is checked against the package's `BETA_REGISTRY_2_1_280` — the genuine
|
|
17
17
|
* client's own registry — by `lib/betas.test.mjs`, which fails on a stale date
|
|
18
18
|
* stamp and requires the host-only entries (the ones upstream has no registry
|
|
19
19
|
* slot for) to be enumerated and justified rather than merely accumulated. Add
|
package/lib/diagnose.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import { fileURLToPath } from "node:url";
|
|
|
5
5
|
|
|
6
6
|
import { getConfigDir, loadConfig } from "./config.mjs";
|
|
7
7
|
import { redactSecrets } from "./redact.mjs";
|
|
8
|
-
import { WIRE_PROFILE } from "./mimicry/wire-compat.mjs";
|
|
8
|
+
import { getWireCompatPackageVersion, WIRE_PROFILE } from "./mimicry/wire-compat.mjs";
|
|
9
9
|
import { loadAccounts } from "./storage.mjs";
|
|
10
10
|
|
|
11
11
|
const ENV_KEYS = [
|
|
@@ -175,6 +175,13 @@ export async function buildDiagnosticBundle() {
|
|
|
175
175
|
// Static baseline on purpose: the CLI version the wire package's profile
|
|
176
176
|
// declares, NOT the (possibly npm-updated) version of the running session.
|
|
177
177
|
mimicryBaseline: WIRE_PROFILE.cliVersion,
|
|
178
|
+
// The full profile id (e.g. "claude-code-2.1.280-sdk-0.112.1"), same
|
|
179
|
+
// baseline caveat as mimicryBaseline above.
|
|
180
|
+
mimicryProfileId: WIRE_PROFILE.id,
|
|
181
|
+
// What's ACTUALLY loaded in node_modules right now -- see
|
|
182
|
+
// getWireCompatPackageVersion's own doc comment for why this can differ
|
|
183
|
+
// from what a future `npm ci` would install.
|
|
184
|
+
wireCompatPackageVersion: getWireCompatPackageVersion(),
|
|
178
185
|
},
|
|
179
186
|
config: redactSecrets(loadConfig()),
|
|
180
187
|
env: getRuntimeEnv(),
|