@bitkyc08/opencodex 2.36.0-preview.20260829 → 2.36.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/gui/dist/assets/index-Cy7Z_pl0.css +1 -0
- package/gui/dist/assets/index-DO8liQVL.js +112 -0
- package/gui/dist/index.html +2 -2
- package/package.json +2 -1
- package/src/AGENTS.md +2 -1
- package/src/adapters/agentrouter.ts +50 -0
- package/src/adapters/anthropic.ts +1 -51
- package/src/adapters/cursor/call-id.ts +76 -8
- package/src/adapters/cursor/checkpoint-store.ts +6 -1
- package/src/adapters/cursor/cursor-errors.ts +44 -0
- package/src/adapters/cursor/native-exec.ts +13 -0
- package/src/adapters/cursor/protobuf-request.ts +651 -29
- package/src/adapters/cursor/tool-result-normalize.ts +3 -3
- package/src/adapters/cursor/transport-retry.ts +5 -1
- package/src/adapters/cursor.ts +15 -1
- package/src/adapters/empty-tool-output-annotation.ts +43 -0
- package/src/adapters/exec-tool-result-normalize.ts +70 -5
- package/src/adapters/google.ts +22 -2
- package/src/adapters/kiro.ts +26 -2
- package/src/adapters/ollama-native-url.ts +111 -0
- package/src/adapters/ollama-native.ts +1131 -0
- package/src/adapters/openai-chat.ts +30 -7
- package/src/adapters/openai-responses.ts +72 -4
- package/src/adapters/registry.ts +7 -0
- package/src/adapters/xai-web-search.ts +58 -0
- package/src/claude/desktop-3p.ts +21 -1
- package/src/claude/desktop-policy.ts +149 -0
- package/src/cli/account.ts +16 -2
- package/src/cli/claude-desktop.ts +13 -3
- package/src/cli/combo.ts +8 -5
- package/src/cli/doctor.ts +77 -11
- package/src/cli/help.ts +1 -1
- package/src/cli/index.ts +16 -0
- package/src/cli/models.ts +20 -3
- package/src/cli/registry.ts +2 -1
- package/src/cli/status.ts +140 -2
- package/src/cli/storage.ts +10 -1
- package/src/codex/account-runtime-state.ts +39 -5
- package/src/codex/account-store.ts +393 -13
- package/src/codex/account-usability.ts +11 -4
- package/src/codex/app-server-processes.ts +46 -5
- package/src/codex/auth-context.ts +160 -32
- package/src/codex/catalog/bundled.ts +7 -5
- package/src/codex/catalog/metadata.ts +1 -1
- package/src/codex/catalog/parsing.ts +57 -1
- package/src/codex/catalog/provider-fetch.ts +61 -4
- package/src/codex/catalog/sync.ts +4 -3
- package/src/codex/convergence.ts +3 -2
- package/src/codex/data/upstream-models.json +40 -8
- package/src/codex/inject-coordination.ts +111 -14
- package/src/codex/integration-record.ts +12 -2
- package/src/codex/main-account.ts +225 -1
- package/src/codex/model-entitlements.ts +339 -27
- package/src/codex/prompt-layers.ts +346 -7
- package/src/codex/prompt-text-probe.ts +272 -21
- package/src/codex/routing.ts +693 -132
- package/src/codex/runtime.ts +12 -0
- package/src/codex/subagent-model-fallback.ts +62 -24
- package/src/codex/user-identity.ts +33 -25
- package/src/combos/index.ts +1 -0
- package/src/combos/reset-window.ts +46 -0
- package/src/combos/resolve.ts +84 -2
- package/src/combos/types.ts +5 -2
- package/src/config/atomic-write.ts +104 -22
- package/src/config/provider-validation.ts +11 -0
- package/src/config.ts +75 -3
- package/src/generated/compatibility-version.json +207 -131
- package/src/generated/model-metadata.ts +1 -1
- package/src/grok/catalog.ts +71 -0
- package/src/grok/effort.ts +83 -0
- package/src/grok/inject.ts +952 -127
- package/src/grok/models.ts +56 -0
- package/src/grok/status.ts +21 -8
- package/src/grok/sync.ts +10 -18
- package/src/images/loop.ts +6 -3
- package/src/integrations/native/ownership-preflight.ts +4 -1
- package/src/lab/fabric/producer-isolate.ts +36 -3
- package/src/lib/destination-policy.ts +93 -7
- package/src/lib/redact.ts +6 -1
- package/src/lib/shadow-call.ts +38 -3
- package/src/lib/test-home-guard.ts +18 -3
- package/src/lib/upstream-retry.ts +43 -6
- package/src/lib/windows-secret-acl.ts +66 -0
- package/src/lib/windows-text.ts +28 -2
- package/src/lib/windows-user-principal.ts +35 -23
- package/src/oauth/account-quota-rank.ts +107 -0
- package/src/oauth/anthropic-routing.ts +125 -30
- package/src/oauth/chatgpt.ts +5 -1
- package/src/oauth/generic-account-failover.ts +114 -7
- package/src/oauth/index.ts +15 -8
- package/src/oauth/store.ts +16 -0
- package/src/providers/account-quota-disk.ts +79 -0
- package/src/providers/command-code-efforts.ts +24 -0
- package/src/providers/derive.ts +6 -0
- package/src/providers/key-failover.ts +33 -1
- package/src/providers/kiro-usage.ts +272 -0
- package/src/providers/ollama-show.ts +311 -0
- package/src/providers/openai-sidecar.ts +5 -0
- package/src/providers/quota-routing-cache.ts +32 -0
- package/src/providers/quota-types.ts +36 -0
- package/src/providers/quota-wire.ts +102 -0
- package/src/providers/quota.ts +208 -147
- package/src/providers/registry.ts +68 -8
- package/src/providers/slug-codec.ts +12 -4
- package/src/providers/vercel-gateway-routing.ts +108 -0
- package/src/router.ts +22 -12
- package/src/server/auth-cors.ts +26 -0
- package/src/server/catalog-download.ts +73 -0
- package/src/server/chat-native.ts +12 -2
- package/src/server/gui-static.ts +4 -1
- package/src/server/index.ts +132 -9
- package/src/server/management/agent-settings-routes.ts +38 -5
- package/src/server/management/codex-prompt-routes.ts +7 -1
- package/src/server/management/combo-routes.ts +10 -1
- package/src/server/management/config-routes.ts +9 -1
- package/src/server/management/context.ts +5 -0
- package/src/server/management/model-routes.ts +16 -6
- package/src/server/management/native-integration-routes.ts +12 -17
- package/src/server/management/oauth-account-routes.ts +13 -0
- package/src/server/management/provider-routes.ts +32 -5
- package/src/server/management/routing-profile-routes.ts +15 -0
- package/src/server/management/shadow-call-validation.ts +29 -0
- package/src/server/management-api.ts +7 -3
- package/src/server/request-log.ts +3 -5
- package/src/server/responses/agent-task-recovery-cache.ts +8 -0
- package/src/server/responses/agent-task-recovery.ts +52 -20
- package/src/server/responses/codex-auth-error.ts +26 -0
- package/src/server/responses/compact.ts +345 -10
- package/src/server/responses/core.ts +736 -108
- package/src/server/responses/empty-completion-guard.ts +16 -0
- package/src/server/responses/fetch-helpers.ts +42 -0
- package/src/server/responses/policy-fallback.ts +11 -6
- package/src/server/responses-undeclared-tool-guard.ts +16 -3
- package/src/server/startup-health-cache.ts +59 -13
- package/src/service-manager-probe.ts +115 -9
- package/src/service.ts +139 -40
- package/src/storage/cleanup.ts +10 -0
- package/src/storage/storage-mutation-coordinator.ts +14 -3
- package/src/tray/windows-tray.ps1 +10 -4
- package/src/tray/windows.ts +30 -2
- package/src/types/config.ts +27 -14
- package/src/types/provider.ts +54 -0
- package/src/types/tools.ts +13 -3
- package/src/types.ts +4 -0
- package/src/usage/summary.ts +421 -177
- package/src/vision/anthropic-describe.ts +3 -3
- package/src/vision/describe.ts +5 -3
- package/src/web-search/anthropic-executor.ts +9 -2
- package/src/web-search/exa-executor.ts +3 -3
- package/src/web-search/executor.ts +8 -3
- package/src/web-search/gemini-executor.ts +3 -3
- package/src/web-search/loop.ts +11 -3
- package/src/web-search/xai-executor.ts +3 -3
- package/gui/dist/assets/index-DQ-Ie18T.css +0 -1
- package/gui/dist/assets/index-DX0RGj3e.js +0 -112
|
@@ -44,19 +44,30 @@ import {
|
|
|
44
44
|
applyCodexAuthContextToProvider,
|
|
45
45
|
CodexMainProfileDrainingError,
|
|
46
46
|
headersForCodexAuthContext,
|
|
47
|
-
|
|
47
|
+
materializeCodexUpstreamAuthAsync,
|
|
48
48
|
isCodexAuthContextUsable,
|
|
49
49
|
resolveCodexAuthContext,
|
|
50
50
|
codexProbeLeaseId,
|
|
51
51
|
codexProbeQuotaScope,
|
|
52
52
|
releaseCodexAuthContextProbeLease,
|
|
53
|
+
stripCodexRuntimeProviderFields,
|
|
53
54
|
type CodexAuthContext,
|
|
54
55
|
} from "../../codex/auth-context";
|
|
56
|
+
import {
|
|
57
|
+
forceRefreshMainAccountToken,
|
|
58
|
+
type NativeMainRefreshDependencies,
|
|
59
|
+
} from "../../codex/main-account";
|
|
55
60
|
import {
|
|
56
61
|
formatCodexProviderForLog,
|
|
62
|
+
handOffThreadAffinityGeneration,
|
|
57
63
|
recordCodexUpstreamOutcome,
|
|
58
64
|
type CodexUpstreamOutcome,
|
|
59
65
|
} from "../../codex/routing";
|
|
66
|
+
import {
|
|
67
|
+
TokenRefreshError,
|
|
68
|
+
forceRefreshCodexPoolToken,
|
|
69
|
+
readCodexAccountRecord,
|
|
70
|
+
} from "../../codex/account-store";
|
|
60
71
|
import {
|
|
61
72
|
fetchWithResetRetry,
|
|
62
73
|
fetchWithTransientRetry,
|
|
@@ -74,7 +85,11 @@ import {
|
|
|
74
85
|
upstreamHostHealthKey,
|
|
75
86
|
type UpstreamHostAdmissionLease,
|
|
76
87
|
} from "../../codex/upstream-host-health";
|
|
77
|
-
import {
|
|
88
|
+
import {
|
|
89
|
+
ForwardAdmissionCredentialError,
|
|
90
|
+
hasForwardableCodexBearer,
|
|
91
|
+
validateForwardAdmissionCredential,
|
|
92
|
+
} from "../auth-cors";
|
|
78
93
|
import type { DataPlaneAdmission } from "../auth-cors";
|
|
79
94
|
import { listOpenAiForwardSidecarCandidates, resolveFirstUsableOpenAiSidecar, type ResolvedOpenAiForwardSidecar } from "../../providers/openai-sidecar";
|
|
80
95
|
import { CODEX_FORWARD_BASE_URL, isCanonicalOpenAiForwardProvider, supportsNativeResponsesCompactEndpoint } from "../../providers/openai-tiers";
|
|
@@ -91,6 +106,7 @@ import { codexAccountSelectionForTurn, registerTurn, trackStreamLifetime, unregi
|
|
|
91
106
|
import type { AdmissionLease } from "../../lib/admission";
|
|
92
107
|
import { redactSecretString } from "../../lib/redact";
|
|
93
108
|
import { readBoundedResponseBody } from "../../lib/bounded-body";
|
|
109
|
+
import { isRateLimitOrQuotaFailureMessage } from "../../lib/errors";
|
|
94
110
|
import { supportedLadderFor } from "../effort-policy";
|
|
95
111
|
import {
|
|
96
112
|
beginRequestAttempt,
|
|
@@ -126,10 +142,71 @@ import {
|
|
|
126
142
|
usesCodexForwardPoolAuth,
|
|
127
143
|
} from "./core";
|
|
128
144
|
import { fetchWithHeaderTimeout, providerFetch, safeHostLabel, safeOriginLabel } from "./fetch-helpers";
|
|
129
|
-
import { mapCodexAuthContextErrorToResponse } from "./codex-auth-error";
|
|
145
|
+
import { mapCodexAuthContextErrorToResponse, nativeMainRefreshFailureResponse } from "./codex-auth-error";
|
|
146
|
+
import { sessionLaneIdFromRequest } from "../request-log-conversation";
|
|
130
147
|
|
|
131
148
|
export const COMPACT_RESPONSE_MAX_BYTES = 32 * 1024 * 1024;
|
|
132
149
|
|
|
150
|
+
const COMPACT_HANDOFF_ROUTE_TTL_MS = 24 * 60 * 60_000;
|
|
151
|
+
const COMPACT_HANDOFF_ROUTE_MAX_ENTRIES = 2_048;
|
|
152
|
+
const COMPACT_HANDOFF_MODEL_MAX_LENGTH = 512;
|
|
153
|
+
|
|
154
|
+
interface CompactHandoffRoute {
|
|
155
|
+
model: string;
|
|
156
|
+
lastUsedAt: number;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* The Codex client does not send its newly selected model on an automatic
|
|
161
|
+
* previous-model compact request. Keep the last route that demonstrably compacted
|
|
162
|
+
* this same thread so a quota-blocked previous model has one safe fallback target.
|
|
163
|
+
*/
|
|
164
|
+
const compactHandoffRoutes = new Map<string, CompactHandoffRoute>();
|
|
165
|
+
|
|
166
|
+
export function clearCompactHandoffRoutesForTests(): void {
|
|
167
|
+
compactHandoffRoutes.clear();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function pruneCompactHandoffRoutes(now: number): void {
|
|
171
|
+
for (const [key, entry] of compactHandoffRoutes) {
|
|
172
|
+
if (now - entry.lastUsedAt > COMPACT_HANDOFF_ROUTE_TTL_MS) compactHandoffRoutes.delete(key);
|
|
173
|
+
}
|
|
174
|
+
while (compactHandoffRoutes.size > COMPACT_HANDOFF_ROUTE_MAX_ENTRIES) {
|
|
175
|
+
const oldest = compactHandoffRoutes.keys().next().value;
|
|
176
|
+
if (typeof oldest !== "string") return;
|
|
177
|
+
compactHandoffRoutes.delete(oldest);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function rememberCompactHandoffRoute(req: Request, model: string, now = Date.now()): void {
|
|
182
|
+
const key = sessionLaneIdFromRequest(req.headers);
|
|
183
|
+
if (!key || model.length > COMPACT_HANDOFF_MODEL_MAX_LENGTH) return;
|
|
184
|
+
pruneCompactHandoffRoutes(now);
|
|
185
|
+
compactHandoffRoutes.delete(key);
|
|
186
|
+
compactHandoffRoutes.set(key, { model, lastUsedAt: now });
|
|
187
|
+
pruneCompactHandoffRoutes(now);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function forgetCompactHandoffRoute(req: Request): void {
|
|
191
|
+
const key = sessionLaneIdFromRequest(req.headers);
|
|
192
|
+
if (key) compactHandoffRoutes.delete(key);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function compactHandoffRoute(req: Request, previousModel: string, now = Date.now()): string | null {
|
|
196
|
+
const key = sessionLaneIdFromRequest(req.headers);
|
|
197
|
+
if (!key) return null;
|
|
198
|
+
pruneCompactHandoffRoutes(now);
|
|
199
|
+
const entry = compactHandoffRoutes.get(key);
|
|
200
|
+
if (!entry || entry.model === previousModel) return null;
|
|
201
|
+
compactHandoffRoutes.delete(key);
|
|
202
|
+
compactHandoffRoutes.set(key, { ...entry, lastUsedAt: now });
|
|
203
|
+
return entry.model;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export interface HandleResponsesCompactOptions {
|
|
207
|
+
nativeMainRefreshDependencies?: NativeMainRefreshDependencies;
|
|
208
|
+
}
|
|
209
|
+
|
|
133
210
|
export function compactResponseTooLargeError(): Response {
|
|
134
211
|
return new Response(JSON.stringify({
|
|
135
212
|
error: {
|
|
@@ -140,6 +217,143 @@ export function compactResponseTooLargeError(): Response {
|
|
|
140
217
|
}), { status: 502, headers: { "Content-Type": "application/json" } });
|
|
141
218
|
}
|
|
142
219
|
|
|
220
|
+
async function refreshNativeMainCompactContext(args: {
|
|
221
|
+
req: Request;
|
|
222
|
+
authCtx: CodexAuthContext;
|
|
223
|
+
provider: OcxProviderConfig;
|
|
224
|
+
codexAccountMode?: CodexAccountMode;
|
|
225
|
+
substituteMainCredential: boolean;
|
|
226
|
+
options: HandleResponsesCompactOptions;
|
|
227
|
+
}): Promise<
|
|
228
|
+
| { ok: true; authCtx: CodexAuthContext; provider: OcxProviderConfig; headers: Headers }
|
|
229
|
+
| { ok: false; response: Response }
|
|
230
|
+
> {
|
|
231
|
+
const { req, authCtx, provider, codexAccountMode, substituteMainCredential, options } = args;
|
|
232
|
+
if (authCtx.kind !== "main-pool") {
|
|
233
|
+
return { ok: false, response: formatErrorResponse(401, "authentication_error", "No native main credential to refresh") };
|
|
234
|
+
}
|
|
235
|
+
try {
|
|
236
|
+
const refreshed = await forceRefreshMainAccountToken(authCtx.accessToken, {
|
|
237
|
+
signal: req.signal,
|
|
238
|
+
...(options.nativeMainRefreshDependencies ?? {}),
|
|
239
|
+
});
|
|
240
|
+
if (!refreshed) {
|
|
241
|
+
return { ok: false, response: formatErrorResponse(401, "authentication_error", "Codex main account needs reauthentication") };
|
|
242
|
+
}
|
|
243
|
+
const refreshedAuthCtx: CodexAuthContext = {
|
|
244
|
+
...authCtx,
|
|
245
|
+
accessToken: refreshed.accessToken,
|
|
246
|
+
chatgptAccountId: refreshed.chatgptAccountId,
|
|
247
|
+
};
|
|
248
|
+
const refreshedProvider = applyCodexAuthContextToProvider(
|
|
249
|
+
stripCodexRuntimeProviderFields(provider),
|
|
250
|
+
refreshedAuthCtx,
|
|
251
|
+
codexAccountMode,
|
|
252
|
+
);
|
|
253
|
+
const headers = new Headers({ "content-type": "application/json" });
|
|
254
|
+
const selected = await materializeCodexUpstreamAuthAsync(req.headers, refreshedAuthCtx, {
|
|
255
|
+
substituteMainCredential,
|
|
256
|
+
signal: req.signal,
|
|
257
|
+
nativeMainRefreshDependencies: options.nativeMainRefreshDependencies,
|
|
258
|
+
});
|
|
259
|
+
for (const name of FORWARD_HEADERS) {
|
|
260
|
+
const value = selected.get(name);
|
|
261
|
+
if (value) headers.set(name, value);
|
|
262
|
+
}
|
|
263
|
+
const override = (refreshedProvider as { _codexAccountOverride?: { accessToken: string; chatgptAccountId: string } })._codexAccountOverride;
|
|
264
|
+
if (override) {
|
|
265
|
+
headers.set("authorization", `Bearer ${override.accessToken}`);
|
|
266
|
+
headers.set("chatgpt-account-id", override.chatgptAccountId);
|
|
267
|
+
}
|
|
268
|
+
return { ok: true, authCtx: refreshedAuthCtx, provider: refreshedProvider, headers };
|
|
269
|
+
} catch (error) {
|
|
270
|
+
return { ok: false, response: nativeMainRefreshFailureResponse(error) };
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** See the core counterpart: only a dead grant is terminal (#2887). */
|
|
275
|
+
function isTerminalCompactPoolRefreshFailure(error: unknown): boolean {
|
|
276
|
+
return error instanceof TokenRefreshError && (error.reason === "revoked" || error.reason === "expired");
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Compact's forced refresh for an ordinary stored pool credential rejected with a
|
|
281
|
+
* pre-stream 401. Mirrors {@link refreshNativeMainCompactContext} so the two 401
|
|
282
|
+
* contracts on this endpoint cannot drift.
|
|
283
|
+
*/
|
|
284
|
+
async function refreshPoolCompactContext(args: {
|
|
285
|
+
req: Request;
|
|
286
|
+
authCtx: CodexAuthContext & { kind: "pool" };
|
|
287
|
+
provider: OcxProviderConfig;
|
|
288
|
+
codexAccountMode?: CodexAccountMode;
|
|
289
|
+
substituteMainCredential: boolean;
|
|
290
|
+
options: HandleResponsesCompactOptions;
|
|
291
|
+
}): Promise<
|
|
292
|
+
| { ok: true; authCtx: CodexAuthContext; provider: OcxProviderConfig; headers: Headers }
|
|
293
|
+
| { ok: false; response: Response; quarantine: boolean; quarantineGeneration?: number }
|
|
294
|
+
> {
|
|
295
|
+
const { req, authCtx, provider, codexAccountMode, substituteMainCredential, options } = args;
|
|
296
|
+
const reauthResponse = () => formatErrorResponse(
|
|
297
|
+
401,
|
|
298
|
+
"authentication_error",
|
|
299
|
+
"Selected Codex account needs reauthentication",
|
|
300
|
+
);
|
|
301
|
+
try {
|
|
302
|
+
const refreshed = await forceRefreshCodexPoolToken(authCtx.accountId, {
|
|
303
|
+
rejectedGeneration: authCtx.generation,
|
|
304
|
+
rejectedAccessToken: authCtx.accessToken,
|
|
305
|
+
signal: req.signal,
|
|
306
|
+
});
|
|
307
|
+
// See the core counterpart: a successful response can rotate only the refresh grant,
|
|
308
|
+
// so the credential may already sit at a later generation than the one we rejected.
|
|
309
|
+
if (!refreshed.rotated) {
|
|
310
|
+
return { ok: false, quarantine: true, quarantineGeneration: refreshed.generation, response: reauthResponse() };
|
|
311
|
+
}
|
|
312
|
+
if (refreshed.selfRefreshed) {
|
|
313
|
+
handOffThreadAffinityGeneration(authCtx.accountId, authCtx.generation, refreshed.generation);
|
|
314
|
+
}
|
|
315
|
+
const refreshedAuthCtx: CodexAuthContext = {
|
|
316
|
+
...authCtx,
|
|
317
|
+
accessToken: refreshed.accessToken,
|
|
318
|
+
chatgptAccountId: refreshed.chatgptAccountId,
|
|
319
|
+
generation: refreshed.generation,
|
|
320
|
+
};
|
|
321
|
+
const refreshedProvider = applyCodexAuthContextToProvider(
|
|
322
|
+
stripCodexRuntimeProviderFields(provider),
|
|
323
|
+
refreshedAuthCtx,
|
|
324
|
+
codexAccountMode,
|
|
325
|
+
);
|
|
326
|
+
const headers = new Headers({ "content-type": "application/json" });
|
|
327
|
+
const selected = await materializeCodexUpstreamAuthAsync(req.headers, refreshedAuthCtx, {
|
|
328
|
+
substituteMainCredential,
|
|
329
|
+
signal: req.signal,
|
|
330
|
+
nativeMainRefreshDependencies: options.nativeMainRefreshDependencies,
|
|
331
|
+
});
|
|
332
|
+
for (const name of FORWARD_HEADERS) {
|
|
333
|
+
const value = selected.get(name);
|
|
334
|
+
if (value) headers.set(name, value);
|
|
335
|
+
}
|
|
336
|
+
const override = (refreshedProvider as { _codexAccountOverride?: { accessToken: string; chatgptAccountId: string } })._codexAccountOverride;
|
|
337
|
+
if (override) {
|
|
338
|
+
headers.set("authorization", `Bearer ${override.accessToken}`);
|
|
339
|
+
headers.set("chatgpt-account-id", override.chatgptAccountId);
|
|
340
|
+
}
|
|
341
|
+
return { ok: true, authCtx: refreshedAuthCtx, provider: refreshedProvider, headers };
|
|
342
|
+
} catch (error) {
|
|
343
|
+
if (isTerminalCompactPoolRefreshFailure(error)) {
|
|
344
|
+
return { ok: false, quarantine: true, response: reauthResponse() };
|
|
345
|
+
}
|
|
346
|
+
const response = formatErrorResponse(
|
|
347
|
+
503,
|
|
348
|
+
"server_busy",
|
|
349
|
+
"Codex credential refresh did not complete; retry this request",
|
|
350
|
+
);
|
|
351
|
+
const headers = new Headers(response.headers);
|
|
352
|
+
headers.set("Retry-After", "1");
|
|
353
|
+
return { ok: false, quarantine: false, response: new Response(response.body, { status: response.status, headers }) };
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
143
357
|
|
|
144
358
|
|
|
145
359
|
/**
|
|
@@ -165,6 +379,7 @@ async function resolveAlternateCompactContext(args: {
|
|
|
165
379
|
const authCtx = await resolveCodexAuthContext(req.headers, config, route.codexAccountMode, {
|
|
166
380
|
...(selectedModelId ? { modelId: selectedModelId } : {}),
|
|
167
381
|
excludeAccountId,
|
|
382
|
+
requestScopedMainCredential: hasForwardableCodexBearer(req.headers, config),
|
|
168
383
|
beginCodexAccountSelection: codexAccountSelectionForTurn(turnAdmissionLease),
|
|
169
384
|
});
|
|
170
385
|
if (!authCtx.accountId || authCtx.accountId === excludeAccountId) return null;
|
|
@@ -267,6 +482,7 @@ export async function handleResponsesCompact(
|
|
|
267
482
|
logCtx: RequestLogContext,
|
|
268
483
|
turnAdmissionLease?: AdmissionLease,
|
|
269
484
|
admission?: DataPlaneAdmission,
|
|
485
|
+
options: HandleResponsesCompactOptions = {},
|
|
270
486
|
): Promise<Response> {
|
|
271
487
|
let body: unknown;
|
|
272
488
|
try {
|
|
@@ -326,6 +542,9 @@ export async function handleResponsesCompact(
|
|
|
326
542
|
// consume that credential. See the longer note in core.ts resolveResponsesCodexAuth.
|
|
327
543
|
const substituteMainCredential = admission?.source === "bearer"
|
|
328
544
|
&& route.codexAccountMode !== undefined;
|
|
545
|
+
const requestScopedMainCredential = route.codexAccountMode !== undefined
|
|
546
|
+
&& !substituteMainCredential
|
|
547
|
+
&& hasForwardableCodexBearer(req.headers, config);
|
|
329
548
|
if (route.codexAccountMode === "direct" && !substituteMainCredential) {
|
|
330
549
|
try { validateForwardAdmissionCredential(req.headers, config); }
|
|
331
550
|
catch (err) {
|
|
@@ -365,17 +584,24 @@ export async function handleResponsesCompact(
|
|
|
365
584
|
// headers would run compaction on the wrong account (or 401) whenever a pool account is
|
|
366
585
|
// active for this thread while normal turns succeed.
|
|
367
586
|
let compactProvider = route.provider;
|
|
368
|
-
|
|
587
|
+
let headers = new Headers({ "content-type": "application/json" });
|
|
369
588
|
try {
|
|
370
589
|
if (route.codexAccountMode) {
|
|
371
590
|
authCtx = await resolveCodexAuthContext(req.headers, config, route.codexAccountMode, {
|
|
372
591
|
accountId: route.codexAccountId,
|
|
373
592
|
modelId: selectedModelId,
|
|
374
593
|
substituteMainCredentialForDirect: substituteMainCredential,
|
|
594
|
+
requestScopedMainCredential,
|
|
375
595
|
beginCodexAccountSelection: codexAccountSelectionForTurn(turnAdmissionLease),
|
|
596
|
+
signal: req.signal,
|
|
597
|
+
nativeMainRefreshDependencies: options.nativeMainRefreshDependencies,
|
|
376
598
|
});
|
|
377
599
|
logCtx.accountLogLabel = codexAuthContextLogLabel(authCtx, config);
|
|
378
|
-
const selected =
|
|
600
|
+
const selected = await materializeCodexUpstreamAuthAsync(req.headers, authCtx, {
|
|
601
|
+
substituteMainCredential,
|
|
602
|
+
signal: req.signal,
|
|
603
|
+
nativeMainRefreshDependencies: options.nativeMainRefreshDependencies,
|
|
604
|
+
});
|
|
379
605
|
compactProvider = applyCodexAuthContextToProvider(route.provider, authCtx, route.codexAccountMode);
|
|
380
606
|
for (const name of FORWARD_HEADERS) {
|
|
381
607
|
const value = selected.get(name);
|
|
@@ -466,6 +692,10 @@ export async function handleResponsesCompact(
|
|
|
466
692
|
fixedAccount: ctx.fixedAccount,
|
|
467
693
|
modelId: selectedModelId,
|
|
468
694
|
probeLeaseId: codexProbeLeaseId(ctx),
|
|
695
|
+
// Fence a stored pool account's outcome on the credential the request was
|
|
696
|
+
// holding, so a 401 that lost a race with re-authentication cannot retire the
|
|
697
|
+
// replacement (#2887). Also covers the replay's own second 401.
|
|
698
|
+
...(ctx.kind === "pool" ? { credentialGeneration: ctx.generation } : {}),
|
|
469
699
|
probeQuotaScope: codexProbeQuotaScope(ctx),
|
|
470
700
|
writerGeneration: ctx.kind === "pool" || ctx.kind === "main-pool"
|
|
471
701
|
? ctx.writerGeneration
|
|
@@ -514,6 +744,7 @@ export async function handleResponsesCompact(
|
|
|
514
744
|
// actually happens, so every recorder call names the context that produced it.
|
|
515
745
|
let outcomeCtx = authCtx;
|
|
516
746
|
let upstream: Response;
|
|
747
|
+
let storedPool401ReplayAttempted = false;
|
|
517
748
|
try {
|
|
518
749
|
// Same connect timeout + keep-alive reset + transient-5xx recovery as /v1/responses —
|
|
519
750
|
// compact hits the same ChatGPT host and must soft-avoid / clear affinity (#186).
|
|
@@ -543,12 +774,76 @@ export async function handleResponsesCompact(
|
|
|
543
774
|
return formatErrorResponse(502, "upstream_error", "Failed to connect to compact upstream");
|
|
544
775
|
}
|
|
545
776
|
|
|
777
|
+
if (
|
|
778
|
+
upstream.status === 401
|
|
779
|
+
&& (authCtx.kind === "main-pool" || authCtx.kind === "pool")
|
|
780
|
+
&& usesCodexForwardPoolAuth(authCtx, compactProvider)
|
|
781
|
+
&& !req.signal.aborted
|
|
782
|
+
) {
|
|
783
|
+
await upstream.body?.cancel().catch(() => undefined);
|
|
784
|
+
const poolAuthCtx = authCtx.kind === "pool" ? authCtx : undefined;
|
|
785
|
+
storedPool401ReplayAttempted = poolAuthCtx !== undefined;
|
|
786
|
+
const poolReplay = poolAuthCtx
|
|
787
|
+
? await refreshPoolCompactContext({
|
|
788
|
+
req,
|
|
789
|
+
authCtx: poolAuthCtx,
|
|
790
|
+
provider: compactProvider,
|
|
791
|
+
codexAccountMode: route.codexAccountMode,
|
|
792
|
+
substituteMainCredential,
|
|
793
|
+
options,
|
|
794
|
+
})
|
|
795
|
+
: undefined;
|
|
796
|
+
const replay = poolReplay
|
|
797
|
+
?? await refreshNativeMainCompactContext({
|
|
798
|
+
req,
|
|
799
|
+
authCtx,
|
|
800
|
+
provider: compactProvider,
|
|
801
|
+
codexAccountMode: route.codexAccountMode,
|
|
802
|
+
substituteMainCredential,
|
|
803
|
+
options,
|
|
804
|
+
});
|
|
805
|
+
if (!replay.ok) {
|
|
806
|
+
// A transient refresh failure must not retire the account; only a dead grant
|
|
807
|
+
// does, and only while the rejected credential is still the stored one (#2887).
|
|
808
|
+
if (poolAuthCtx) {
|
|
809
|
+
if (poolReplay && !poolReplay.ok && poolReplay.quarantine) {
|
|
810
|
+
recordCodexUpstreamOutcome(config, poolAuthCtx.accountId, 401, {
|
|
811
|
+
threadId: poolAuthCtx.affinityKey,
|
|
812
|
+
fixedAccount: poolAuthCtx.fixedAccount,
|
|
813
|
+
modelId: selectedModelId,
|
|
814
|
+
writerGeneration: poolAuthCtx.writerGeneration,
|
|
815
|
+
credentialGeneration: poolReplay.quarantineGeneration ?? poolAuthCtx.generation,
|
|
816
|
+
});
|
|
817
|
+
}
|
|
818
|
+
return replay.response;
|
|
819
|
+
}
|
|
820
|
+
recordCompactPoolOutcome(outcomeCtx, replay.response.status === 401 ? 401 : "connect_neutral");
|
|
821
|
+
return replay.response;
|
|
822
|
+
}
|
|
823
|
+
authCtx = replay.authCtx;
|
|
824
|
+
outcomeCtx = replay.authCtx;
|
|
825
|
+
compactProvider = replay.provider;
|
|
826
|
+
headers = replay.headers;
|
|
827
|
+
logCtx.accountLogLabel = codexAuthContextLogLabel(replay.authCtx, config);
|
|
828
|
+
try {
|
|
829
|
+
upstream = await sendCompactAttempt(compactProvider, headers, "single");
|
|
830
|
+
} catch (err) {
|
|
831
|
+
if (req.signal.aborted) {
|
|
832
|
+
recordCompactPoolOutcome(outcomeCtx, 499);
|
|
833
|
+
return formatErrorResponse(499, "client_cancelled", "Client cancelled compact request");
|
|
834
|
+
}
|
|
835
|
+
recordCompactPoolOutcome(outcomeCtx, classifyTransportFailureKind(err));
|
|
836
|
+
return formatErrorResponse(502, "upstream_error", "Failed to connect to compact upstream");
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
|
|
546
840
|
// Bounded same-request alternate: the regular /v1/responses path already does this
|
|
547
841
|
// (core.ts:319-423) and recognizes exactly 429/402. Without it a pool rejection
|
|
548
842
|
// surfaces to the client, which retries the compact task OUTSIDE the logical request
|
|
549
843
|
// — reporting exhausted retries while another pool account sat idle (#913).
|
|
550
844
|
if (
|
|
551
845
|
(upstream.status === 429 || upstream.status === 402)
|
|
846
|
+
&& !storedPool401ReplayAttempted
|
|
552
847
|
&& usesCodexForwardPoolAuth(authCtx, route.provider)
|
|
553
848
|
&& !authCtx.fixedAccount
|
|
554
849
|
&& route.codexAccountMode
|
|
@@ -635,19 +930,56 @@ export async function handleResponsesCompact(
|
|
|
635
930
|
upstream.headers.get("x-codex-tertiary-reset-at"),
|
|
636
931
|
].filter(Boolean);
|
|
637
932
|
const buffered = await bufferCompactResponse(upstream, req.signal);
|
|
933
|
+
const bufferedErrorText = buffered.ok
|
|
934
|
+
? ""
|
|
935
|
+
: await buffered.clone().text().catch(() => "");
|
|
936
|
+
const explicitQuotaStatus = buffered.status === 429 || buffered.status === 402;
|
|
937
|
+
const bodyInferredQuota = !buffered.ok
|
|
938
|
+
&& !explicitQuotaStatus
|
|
939
|
+
&& isRateLimitOrQuotaFailureMessage(bufferedErrorText);
|
|
940
|
+
const quotaFailure = explicitQuotaStatus || bodyInferredQuota;
|
|
638
941
|
// Record pool health only after the body is fully delivered (or definitively failed).
|
|
639
942
|
// A premature 200 would clear soft-avoid while the client still sees a buffer 502.
|
|
640
943
|
if (buffered.status === 499) {
|
|
641
944
|
recordCompactPoolOutcome(outcomeCtx, 499);
|
|
642
945
|
return buffered;
|
|
643
946
|
}
|
|
644
|
-
//
|
|
645
|
-
//
|
|
646
|
-
|
|
947
|
+
// A body-confirmed quota failure can arrive behind a generic 5xx. Record it as
|
|
948
|
+
// quota evidence; otherwise preserve the real upstream status so a local buffering
|
|
949
|
+
// failure after a 200 cannot soft-avoid a healthy account or rotate a thread.
|
|
950
|
+
recordCompactPoolOutcome(outcomeCtx, bodyInferredQuota ? 429 : upstream.status, { retryAfter, resetAt });
|
|
647
951
|
// Lift usage and response metadata from the buffered upstream JSON into the
|
|
648
952
|
// request log; the routed branch gets the same through handleResponses. The
|
|
649
953
|
// synthetic buffer errors are not upstream bodies and stay uninspected.
|
|
650
|
-
if (buffered.ok)
|
|
954
|
+
if (buffered.ok) {
|
|
955
|
+
inspectResponseLogJson(logCtx, await buffered.clone().text());
|
|
956
|
+
forgetCompactHandoffRoute(req);
|
|
957
|
+
} else if (quotaFailure && !storedPool401ReplayAttempted) {
|
|
958
|
+
const fallbackModel = compactHandoffRoute(req, raw.model);
|
|
959
|
+
if (fallbackModel && !req.signal.aborted) {
|
|
960
|
+
const fallbackReq = new Request(req.url, {
|
|
961
|
+
method: "POST",
|
|
962
|
+
headers: req.headers,
|
|
963
|
+
body: JSON.stringify({ ...raw, model: fallbackModel }),
|
|
964
|
+
signal: req.signal,
|
|
965
|
+
});
|
|
966
|
+
try {
|
|
967
|
+
const fallback = await handleResponsesCompact(
|
|
968
|
+
fallbackReq,
|
|
969
|
+
config,
|
|
970
|
+
logCtx,
|
|
971
|
+
turnAdmissionLease,
|
|
972
|
+
admission,
|
|
973
|
+
options,
|
|
974
|
+
);
|
|
975
|
+
if (fallback.ok || fallback.status === 499) return fallback;
|
|
976
|
+
await fallback.body?.cancel().catch(() => undefined);
|
|
977
|
+
} catch {
|
|
978
|
+
// The previous-model rejection is the authoritative failure when the
|
|
979
|
+
// remembered handoff route can no longer compact this thread.
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
}
|
|
651
983
|
return buffered;
|
|
652
984
|
} finally {
|
|
653
985
|
releaseUpstreamHostAdmission(compactHostAdmissionLease);
|
|
@@ -740,9 +1072,11 @@ export async function handleResponsesCompact(
|
|
|
740
1072
|
// The canonical Responses stream returns a real OpenAI-encrypted compaction item. OCX cannot
|
|
741
1073
|
// and should not decrypt it; /responses/compact callers can consume that item directly.
|
|
742
1074
|
if (accountGatedCompactWireModel) {
|
|
743
|
-
|
|
1075
|
+
const result = new Response(JSON.stringify({ output: compactionItems }), {
|
|
744
1076
|
headers: { "Content-Type": "application/json" },
|
|
745
1077
|
});
|
|
1078
|
+
rememberCompactHandoffRoute(req, raw.model);
|
|
1079
|
+
return result;
|
|
746
1080
|
}
|
|
747
1081
|
const encrypted = compactionItems[0]!.encrypted_content;
|
|
748
1082
|
const decoded = typeof encrypted === "string" ? decodeCompactionSummary(encrypted) : null;
|
|
@@ -752,5 +1086,6 @@ export async function handleResponsesCompact(
|
|
|
752
1086
|
}
|
|
753
1087
|
const summary = decoded;
|
|
754
1088
|
const output = buildCompactV1Output(extractCompactUserMessages(inputItems), summary);
|
|
1089
|
+
rememberCompactHandoffRoute(req, raw.model);
|
|
755
1090
|
return new Response(JSON.stringify({ output }), { headers: { "Content-Type": "application/json" } });
|
|
756
1091
|
}
|