@bitkyc08/opencodex 2.7.21 → 2.7.23
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/gui/dist/assets/{index-CILVKWmx.css → index-Bk_GgFrh.css} +1 -1
- package/gui/dist/assets/index-DQjt6Hly.js +40 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/claude/outbound.ts +15 -4
- package/src/cli/index.ts +1 -1
- package/src/codex/auth-api.ts +7 -6
- package/src/codex/quota.ts +13 -27
- package/src/codex/routing.ts +1 -2
- package/src/lib/abort.ts +55 -0
- package/src/lib/upstream-retry.ts +62 -0
- package/src/oauth/index.ts +82 -24
- package/src/oauth/local-token-detect.ts +23 -1
- package/src/oauth/store.ts +51 -31
- package/src/oauth/xai.ts +17 -10
- package/src/providers/quota.ts +2 -6
- package/src/providers/registry.ts +38 -13
- package/src/providers/xai-transport.ts +107 -60
- package/src/responses/parser.ts +59 -17
- package/src/server/claude-messages.ts +240 -14
- package/src/server/management-api.ts +3 -3
- package/src/server/request-log.ts +13 -1
- package/src/server/responses.ts +67 -18
- package/src/types.ts +13 -0
- package/src/usage/log.ts +11 -0
- package/gui/dist/assets/index-DYIS0tTL.js +0 -40
package/src/server/responses.ts
CHANGED
|
@@ -14,8 +14,10 @@ import { injectionDebugLog } from "../lib/injection-debug-log";
|
|
|
14
14
|
import { modelInList, namespacedToolName } from "../types";
|
|
15
15
|
import type { AdapterEvent, OcxConfig, OcxParsedRequest, OcxProviderConfig } from "../types";
|
|
16
16
|
import {
|
|
17
|
+
forceRefreshOAuthAccessSnapshot,
|
|
17
18
|
getOAuthCredentialProjectId,
|
|
18
|
-
|
|
19
|
+
getValidAccessTokenSnapshot,
|
|
20
|
+
type OAuthAccessSnapshot,
|
|
19
21
|
UnsupportedOAuthProviderError,
|
|
20
22
|
} from "../oauth";
|
|
21
23
|
import { buildWebSearchTool, planWebSearch, runWithWebSearch } from "../web-search";
|
|
@@ -36,7 +38,7 @@ import {
|
|
|
36
38
|
recordCodexUpstreamOutcome,
|
|
37
39
|
type CodexUpstreamOutcome,
|
|
38
40
|
} from "../codex/routing";
|
|
39
|
-
import { fetchWithResetRetry } from "../lib/upstream-retry";
|
|
41
|
+
import { fetchWithResetRetry, fetchWithTransientRetry } from "../lib/upstream-retry";
|
|
40
42
|
import { isUsageDebugEnabled } from "../usage/debug";
|
|
41
43
|
import { readJsonRequestBody, DecompressedBodyTooLargeError, UnsupportedContentEncodingError } from "./request-decompress";
|
|
42
44
|
import { resolveAdapter, resolveWireProtocolOverride } from "./adapter-resolve";
|
|
@@ -611,9 +613,13 @@ export async function handleResponses(
|
|
|
611
613
|
|
|
612
614
|
// OAuth providers: swap in a fresh access token (auto-refreshed) as the Bearer key, so the
|
|
613
615
|
// existing openai-chat / anthropic adapters authenticate with no change.
|
|
616
|
+
const isXaiOAuthRequest = route.providerName === "xai" && route.provider.authMode === "oauth";
|
|
617
|
+
let sentOAuthSnapshot: OAuthAccessSnapshot | undefined;
|
|
614
618
|
if (route.provider.authMode === "oauth") {
|
|
615
619
|
try {
|
|
616
|
-
|
|
620
|
+
const resolved = await getValidAccessTokenSnapshot(route.providerName);
|
|
621
|
+
if (isXaiOAuthRequest) sentOAuthSnapshot = resolved;
|
|
622
|
+
route.provider = { ...route.provider, apiKey: resolved.accessToken };
|
|
617
623
|
// Antigravity (cloud-code-assist) needs the discovered Cloud Code Assist project id in the
|
|
618
624
|
// CCA envelope; the server injects only the bare token, so pull project from the credential.
|
|
619
625
|
if (route.provider.googleMode === "cloud-code-assist" && !route.provider.project) {
|
|
@@ -694,12 +700,15 @@ export async function handleResponses(
|
|
|
694
700
|
const connectMs = config.connectTimeoutMs ?? 200_000;
|
|
695
701
|
let upstreamResponse: Response;
|
|
696
702
|
try {
|
|
697
|
-
|
|
703
|
+
// Transient-5xx pre-stream retry (devlog/_plan/260716_claudecode_hardening/010):
|
|
704
|
+
// the ChatGPT backend emits transient 502/520s that an immediate retry absorbs.
|
|
705
|
+
// Body is a replayable string; nothing has streamed to the client yet.
|
|
706
|
+
upstreamResponse = await fetchWithTransientRetry(
|
|
698
707
|
() => fetchWithHeaderTimeout(request.url, {
|
|
699
708
|
method: request.method,
|
|
700
709
|
headers: request.headers,
|
|
701
710
|
body: request.body,
|
|
702
|
-
}, upstream.signal, connectMs, parsed.stream),
|
|
711
|
+
}, upstream.signal, connectMs, parsed.stream, providerFetch(route.provider)),
|
|
703
712
|
{ abortSignal: upstream.signal, label: safeHostLabel(request.url) },
|
|
704
713
|
);
|
|
705
714
|
} catch (err) {
|
|
@@ -726,22 +735,24 @@ export async function handleResponses(
|
|
|
726
735
|
const terminalRecorder = codexForwardTerminalOutcomeRecorder(config, authCtx, route.provider);
|
|
727
736
|
const terminalBodyWillRecord = !!terminalRecorder && upstreamResponse.ok && isEventStream;
|
|
728
737
|
// Capture quota from upstream response for multi-account tracking
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
738
|
+
if (usesCodexForwardPoolAuth(authCtx, route.provider)) {
|
|
739
|
+
// primary was the 5h window; it now carries weekly data for GPT plans.
|
|
740
|
+
// Prefer primary when present, fall back to secondary for compatibility.
|
|
741
|
+
const primaryRaw = upstreamResponse.headers.get("x-codex-primary-used-percent");
|
|
742
|
+
const secondaryRaw = upstreamResponse.headers.get("x-codex-secondary-used-percent");
|
|
743
|
+
const weeklyRaw = primaryRaw ?? secondaryRaw;
|
|
732
744
|
const monthlyRaw = upstreamResponse.headers.get("x-codex-tertiary-used-percent");
|
|
733
|
-
const
|
|
734
|
-
const
|
|
745
|
+
const primaryResetRaw = upstreamResponse.headers.get("x-codex-primary-reset-at");
|
|
746
|
+
const secondaryResetRaw = upstreamResponse.headers.get("x-codex-secondary-reset-at");
|
|
747
|
+
const weeklyResetRaw = primaryRaw ? primaryResetRaw : secondaryResetRaw;
|
|
735
748
|
const monthlyResetRaw = upstreamResponse.headers.get("x-codex-tertiary-reset-at");
|
|
736
749
|
const retryAfterRaw = upstreamResponse.headers.get("retry-after");
|
|
737
|
-
if (weeklyRaw ||
|
|
750
|
+
if (weeklyRaw || monthlyRaw) {
|
|
738
751
|
const { updateAccountQuota } = await import("../codex/auth-api");
|
|
739
752
|
updateAccountQuota(
|
|
740
753
|
authCtx.accountId,
|
|
741
754
|
weeklyRaw,
|
|
742
|
-
fiveHourRaw,
|
|
743
755
|
weeklyResetRaw,
|
|
744
|
-
fiveHourResetRaw,
|
|
745
756
|
monthlyRaw,
|
|
746
757
|
monthlyResetRaw,
|
|
747
758
|
);
|
|
@@ -753,8 +764,8 @@ export async function handleResponses(
|
|
|
753
764
|
});
|
|
754
765
|
} else {
|
|
755
766
|
recordCodexUpstreamOutcome(config, authCtx.accountId, upstreamResponse.status, {
|
|
756
|
-
|
|
757
|
-
|
|
767
|
+
retryAfter: retryAfterRaw,
|
|
768
|
+
resetAt: [primaryResetRaw, secondaryResetRaw, monthlyResetRaw].filter(Boolean),
|
|
758
769
|
});
|
|
759
770
|
}
|
|
760
771
|
}
|
|
@@ -945,7 +956,7 @@ export async function handleResponses(
|
|
|
945
956
|
: await fetchWithResetRetry(
|
|
946
957
|
() => fetchWithHeaderTimeout(request.url, {
|
|
947
958
|
method: request.method, headers: request.headers, body: request.body,
|
|
948
|
-
}, upstream.signal, connectMs, parsed.stream),
|
|
959
|
+
}, upstream.signal, connectMs, parsed.stream, providerFetch(route.provider)),
|
|
949
960
|
{ abortSignal: upstream.signal, label: safeHostLabel(request.url) },
|
|
950
961
|
);
|
|
951
962
|
} catch (err) {
|
|
@@ -966,6 +977,7 @@ export async function handleResponses(
|
|
|
966
977
|
let activeAdapter = adapter;
|
|
967
978
|
let imageTierBias = 0;
|
|
968
979
|
let imageRetryAttempted = false;
|
|
980
|
+
let oauth401ReplayAttempted = false;
|
|
969
981
|
const rebuildAndRefetch = async (): Promise<Response | { failed: Response }> => {
|
|
970
982
|
const retryRequest = await activeAdapter.buildRequest(parsed, {
|
|
971
983
|
headers: selectedForwardHeaders,
|
|
@@ -976,7 +988,7 @@ export async function handleResponses(
|
|
|
976
988
|
? await activeAdapter.fetchResponse(retryRequest, { abortSignal: upstream.signal, timeoutMs: connectMs, stream: parsed.stream })
|
|
977
989
|
: await fetchWithHeaderTimeout(retryRequest.url, {
|
|
978
990
|
method: retryRequest.method, headers: retryRequest.headers, body: retryRequest.body,
|
|
979
|
-
}, upstream.signal, connectMs, parsed.stream);
|
|
991
|
+
}, upstream.signal, connectMs, parsed.stream, providerFetch(route.provider));
|
|
980
992
|
} catch (err) {
|
|
981
993
|
cleanupUpstreamAbort();
|
|
982
994
|
upstream.abort();
|
|
@@ -987,6 +999,38 @@ export async function handleResponses(
|
|
|
987
999
|
}
|
|
988
1000
|
};
|
|
989
1001
|
recovery: for (;;) {
|
|
1002
|
+
if (
|
|
1003
|
+
upstreamResponse.status === 401
|
|
1004
|
+
&& isXaiOAuthRequest
|
|
1005
|
+
&& sentOAuthSnapshot
|
|
1006
|
+
&& !oauth401ReplayAttempted
|
|
1007
|
+
) {
|
|
1008
|
+
oauth401ReplayAttempted = true;
|
|
1009
|
+
try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already consumed/closed */ }
|
|
1010
|
+
let refreshed: OAuthAccessSnapshot;
|
|
1011
|
+
try {
|
|
1012
|
+
refreshed = await forceRefreshOAuthAccessSnapshot(sentOAuthSnapshot);
|
|
1013
|
+
} catch (err) {
|
|
1014
|
+
cleanupUpstreamAbort();
|
|
1015
|
+
return formatErrorResponse(401, "authentication_error", err instanceof Error ? err.message : String(err));
|
|
1016
|
+
}
|
|
1017
|
+
sentOAuthSnapshot = refreshed;
|
|
1018
|
+
const refreshedProvider = resolveProviderTransport(
|
|
1019
|
+
route.providerName,
|
|
1020
|
+
{ ...route.provider, apiKey: refreshed.accessToken },
|
|
1021
|
+
parsed.options.promptCacheKey,
|
|
1022
|
+
);
|
|
1023
|
+
route.provider = refreshedProvider;
|
|
1024
|
+
activeAdapter = resolveAdapter(
|
|
1025
|
+
resolveWireProtocolOverride(route.providerName, route.modelId, refreshedProvider),
|
|
1026
|
+
config.cacheRetention,
|
|
1027
|
+
);
|
|
1028
|
+
const result = await rebuildAndRefetch();
|
|
1029
|
+
if ("failed" in result) return result.failed;
|
|
1030
|
+
upstreamResponse = result;
|
|
1031
|
+
continue recovery;
|
|
1032
|
+
}
|
|
1033
|
+
|
|
990
1034
|
// Multi-key 429 failover: rotate to the next pool key (cooldown-aware) and retry the
|
|
991
1035
|
// SAME request once per remaining key. OAuth/forward providers and single-key pools
|
|
992
1036
|
// return null immediately, so this stays a no-op for them (src/providers/key-failover.ts).
|
|
@@ -1222,12 +1266,17 @@ export function safeHostLabel(url: string): string {
|
|
|
1222
1266
|
}
|
|
1223
1267
|
}
|
|
1224
1268
|
|
|
1269
|
+
function providerFetch(provider: OcxProviderConfig): typeof globalThis.fetch {
|
|
1270
|
+
return (provider as OcxProviderConfig & { fetch?: typeof globalThis.fetch }).fetch ?? globalThis.fetch;
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1225
1273
|
export async function fetchWithHeaderTimeout(
|
|
1226
1274
|
url: string,
|
|
1227
1275
|
init: Omit<RequestInit, "signal">,
|
|
1228
1276
|
abortSignal: AbortSignal,
|
|
1229
1277
|
timeoutMs: number,
|
|
1230
1278
|
preferIdentityEncoding = false,
|
|
1279
|
+
executor: typeof globalThis.fetch = globalThis.fetch,
|
|
1231
1280
|
): Promise<Response> {
|
|
1232
1281
|
const timeout = new AbortController();
|
|
1233
1282
|
const timer = setTimeout(() => {
|
|
@@ -1240,7 +1289,7 @@ export async function fetchWithHeaderTimeout(
|
|
|
1240
1289
|
headers.set("accept-encoding", "identity");
|
|
1241
1290
|
}
|
|
1242
1291
|
try {
|
|
1243
|
-
return await
|
|
1292
|
+
return await executor(url, {
|
|
1244
1293
|
...init,
|
|
1245
1294
|
headers,
|
|
1246
1295
|
signal: AbortSignal.any([abortSignal, timeout.signal]),
|
package/src/types.ts
CHANGED
|
@@ -258,6 +258,19 @@ export interface OcxClaudeCodeConfig {
|
|
|
258
258
|
nativePassthrough?: boolean;
|
|
259
259
|
/** Upstream for the native passthrough (tests/enterprise gateways). Default: https://api.anthropic.com */
|
|
260
260
|
anthropicBaseUrl?: string;
|
|
261
|
+
/**
|
|
262
|
+
* Native passthrough body inactivity budget in SECONDS — raw upstream-byte silence
|
|
263
|
+
* while a read is pending, NOT total duration (slow-but-alive streams never trip it;
|
|
264
|
+
* devlog 260716_passthrough_followups/010). Default 90. Min 1. Exactly 0 disables;
|
|
265
|
+
* negative/non-finite values fall back to the default.
|
|
266
|
+
*/
|
|
267
|
+
bodyStallSec?: number;
|
|
268
|
+
/**
|
|
269
|
+
* Native passthrough cumulative body byte cap (streamed SSE and buffered non-stream
|
|
270
|
+
* alike) — an OOM/occupancy guard, not a correctness limit. Default 67108864 (64 MiB).
|
|
271
|
+
* Exactly 0 disables; negative/non-finite values fall back to the default.
|
|
272
|
+
*/
|
|
273
|
+
bodyMaxBytes?: number;
|
|
261
274
|
/** Default model slot injected as ANTHROPIC_MODEL by `ocx claude`. */
|
|
262
275
|
model?: string;
|
|
263
276
|
/** Haiku/small-fast slot injected as ANTHROPIC_DEFAULT_HAIKU_MODEL (+ legacy SMALL_FAST). */
|
package/src/usage/log.ts
CHANGED
|
@@ -18,6 +18,13 @@ export interface PersistedUsageEntry {
|
|
|
18
18
|
usageStatus: UsageStatus;
|
|
19
19
|
usage?: OcxUsage;
|
|
20
20
|
totalTokens?: number;
|
|
21
|
+
// Failure diagnostics (devlog/_plan/260716_claudecode_hardening/030): persisted for
|
|
22
|
+
// status>=400 or non-completed terminals so incidents survive the in-memory ring buffer.
|
|
23
|
+
errorCode?: string;
|
|
24
|
+
terminalStatus?: string;
|
|
25
|
+
closeReason?: "terminal" | "client_cancel" | "non_stream" | "body_stall" | "body_overflow";
|
|
26
|
+
/** Already redacted + capped at capture (request-log.ts redactSecretString().slice(0,500)). */
|
|
27
|
+
upstreamError?: string;
|
|
21
28
|
}
|
|
22
29
|
|
|
23
30
|
export function usageLogPath(): string {
|
|
@@ -76,6 +83,10 @@ function normalizeUsageEntry(entry: PersistedUsageEntry): PersistedUsageEntry {
|
|
|
76
83
|
usageStatus: entry.usageStatus,
|
|
77
84
|
...(entry.usage ? { usage: normalizeUsageValue(entry.usage) } : {}),
|
|
78
85
|
...(typeof entry.totalTokens === "number" ? { totalTokens: entry.totalTokens } : {}),
|
|
86
|
+
...(entry.errorCode ? { errorCode: entry.errorCode } : {}),
|
|
87
|
+
...(entry.terminalStatus ? { terminalStatus: entry.terminalStatus } : {}),
|
|
88
|
+
...(entry.closeReason ? { closeReason: entry.closeReason } : {}),
|
|
89
|
+
...(entry.upstreamError ? { upstreamError: entry.upstreamError } : {}),
|
|
79
90
|
};
|
|
80
91
|
}
|
|
81
92
|
|