@bitkyc08/opencodex 2.10.0 → 2.10.1
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/AGENTS_INSTALL.md +77 -0
- package/README.md +4 -10
- package/bin/ocx.mjs +71 -18
- package/gui/dist/assets/index-Cd6_PBKn.css +1 -0
- package/gui/dist/assets/index-ChZQsmBY.js +70 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/alibaba-color.svg +1 -1
- package/gui/dist/provider-icons/antigravity-color.svg +1 -1
- package/gui/dist/provider-icons/claude-color.svg +1 -1
- package/gui/dist/provider-icons/cline-color.svg +16 -0
- package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -1
- package/gui/dist/provider-icons/copilot-color.svg +1 -1
- package/gui/dist/provider-icons/cursor-color.svg +1 -1
- package/gui/dist/provider-icons/deepseek-color.svg +1 -1
- package/gui/dist/provider-icons/firepass-color.svg +1 -1
- package/gui/dist/provider-icons/fireworks-color.svg +1 -1
- package/gui/dist/provider-icons/gemini-color.svg +1 -1
- package/gui/dist/provider-icons/github-copilot-color.svg +1 -1
- package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -1
- package/gui/dist/provider-icons/grok.svg +1 -1
- package/gui/dist/provider-icons/groq-color.svg +1 -1
- package/gui/dist/provider-icons/huggingface-color.svg +1 -1
- package/gui/dist/provider-icons/kimi-color.svg +1 -1
- package/gui/dist/provider-icons/kiro-color.svg +2 -2
- package/gui/dist/provider-icons/lm-studio-color.svg +1 -1
- package/gui/dist/provider-icons/mistral-color.svg +1 -1
- package/gui/dist/provider-icons/moonshot-color.svg +1 -1
- package/gui/dist/provider-icons/nvidia-color.svg +1 -1
- package/gui/dist/provider-icons/ollama-color.svg +1 -1
- package/gui/dist/provider-icons/openai.svg +1 -1
- package/gui/dist/provider-icons/opencode.svg +2 -1
- package/gui/dist/provider-icons/openrouter-color.svg +1 -1
- package/gui/dist/provider-icons/pi.svg +2 -2
- package/gui/dist/provider-icons/qianfan-color.svg +1 -1
- package/gui/dist/provider-icons/qwen-portal-color.svg +1 -1
- package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -1
- package/gui/dist/provider-icons/vllm-color.svg +1 -1
- package/gui/dist/provider-icons/xiaomi-color.svg +1 -1
- package/package.json +8 -4
- package/src/adapters/anthropic.ts +208 -14
- package/src/adapters/base.ts +16 -5
- package/src/adapters/cursor/effort-map.ts +3 -2
- package/src/adapters/cursor/framing.ts +39 -0
- package/src/adapters/cursor/live-transport.ts +105 -95
- package/src/adapters/cursor/native-exec.ts +32 -6
- package/src/adapters/cursor/protobuf-request.ts +20 -15
- package/src/adapters/cursor/request-builder.ts +21 -7
- package/src/adapters/cursor/types.ts +7 -0
- package/src/adapters/google-antigravity-replay.ts +237 -21
- package/src/adapters/google-truncation.ts +11 -0
- package/src/adapters/google.ts +50 -9
- package/src/adapters/identity.ts +39 -6
- package/src/adapters/kiro-errors.ts +11 -0
- package/src/adapters/kiro-events.ts +19 -1
- package/src/adapters/kiro-thinking.ts +10 -2
- package/src/adapters/kiro-tools.ts +10 -1
- package/src/adapters/kiro.ts +37 -11
- package/src/adapters/openai-chat.ts +284 -83
- package/src/adapters/openai-responses.ts +182 -24
- package/src/bridge.ts +177 -7
- package/src/chat/outbound.ts +78 -23
- package/src/claude/agents-inject.ts +27 -5
- package/src/claude/inbound.ts +11 -1
- package/src/claude/model-info.ts +13 -10
- package/src/claude/outbound.ts +17 -0
- package/src/cli/account-api.ts +24 -0
- package/src/cli/account-auth.ts +31 -6
- package/src/cli/account-main.ts +317 -0
- package/src/cli/account.ts +5 -0
- package/src/cli/claude.ts +2 -1
- package/src/cli/doctor.ts +93 -22
- package/src/cli/export-command.ts +26 -12
- package/src/cli/help.ts +8 -6
- package/src/cli/index.ts +56 -22
- package/src/cli/integrations.ts +84 -1
- package/src/cli/observe.ts +54 -1
- package/src/cli/opencode.ts +2 -1
- package/src/cli/provider-runtime.ts +18 -1
- package/src/cli/route-policy.ts +92 -0
- package/src/cli/runtime-api.ts +6 -3
- package/src/cli/star-prompt.ts +71 -15
- package/src/cli/status.ts +1 -1
- package/src/cli/v2.ts +36 -9
- package/src/clients/config-export.ts +687 -10
- package/src/codex/account-lifecycle.ts +30 -5
- package/src/codex/account-usability.ts +22 -2
- package/src/codex/admission.ts +255 -0
- package/src/codex/auth-api.ts +427 -140
- package/src/codex/auth-context.ts +155 -30
- package/src/codex/autostart-health.ts +8 -1
- package/src/codex/catalog/account-models.ts +62 -0
- package/src/codex/catalog/aggregation.ts +14 -1
- package/src/codex/catalog/bundled.ts +282 -32
- package/src/codex/catalog/filesystem-evidence.ts +302 -0
- package/src/codex/catalog/metadata.ts +51 -6
- package/src/codex/catalog/parsing.ts +6 -3
- package/src/codex/catalog/provider-fetch.ts +576 -41
- package/src/codex/catalog/sync.ts +505 -66
- package/src/codex/catalog-admission.ts +197 -0
- package/src/codex/catalog-write-serialization.ts +241 -0
- package/src/codex/catalog.ts +2 -1
- package/src/codex/codex-write-lock.ts +372 -0
- package/src/codex/convergence-types.ts +593 -0
- package/src/codex/convergence.ts +441 -0
- package/src/codex/desired-state.ts +177 -0
- package/src/codex/features.ts +52 -8
- package/src/codex/generation.ts +202 -0
- package/src/codex/history-job.ts +257 -0
- package/src/codex/history-lock.ts +241 -0
- package/src/codex/history-migration-guardian.ts +18 -5
- package/src/codex/history-provider.ts +9 -2
- package/src/codex/history-transition.ts +105 -0
- package/src/codex/history-worker.ts +176 -0
- package/src/codex/inject-coordination.ts +245 -0
- package/src/codex/inject.ts +605 -124
- package/src/codex/integration-record.ts +266 -0
- package/src/codex/internal/catalog-writer.ts +203 -0
- package/src/codex/internal/history-writer.ts +80 -0
- package/src/codex/journal.ts +10 -1
- package/src/codex/main-account-cache.ts +24 -0
- package/src/codex/management-convergence.ts +114 -0
- package/src/codex/native-main-admission.ts +47 -0
- package/src/codex/native-main-auth-temp.ts +187 -0
- package/src/codex/native-main-claim.ts +167 -0
- package/src/codex/native-main-lock-file.ts +158 -0
- package/src/codex/native-main-owner.ts +315 -0
- package/src/codex/native-profile-api.ts +247 -0
- package/src/codex/native-profile-manager.ts +1512 -0
- package/src/codex/native-profile-processes.ts +121 -0
- package/src/codex/native-profile-recovery.ts +99 -0
- package/src/codex/native-profile-stage-store.ts +387 -0
- package/src/codex/native-profile-startup.ts +340 -0
- package/src/codex/native-profile-store.ts +855 -0
- package/src/codex/native-profile-types.ts +120 -0
- package/src/codex/native-residue.ts +557 -0
- package/src/codex/project-config-warnings.ts +18 -4
- package/src/codex/prompt-journal.ts +311 -0
- package/src/codex/prompt-layers.ts +967 -0
- package/src/codex/prompt-lock.ts +143 -0
- package/src/codex/quota-rejection.ts +224 -0
- package/src/codex/quota.ts +86 -3
- package/src/codex/routing.ts +299 -62
- package/src/codex/runtime.ts +159 -38
- package/src/codex/shim.ts +39 -13
- package/src/codex/subagent-model-fallback.ts +73 -12
- package/src/codex/transition-state.ts +604 -0
- package/src/codex/upstream-host-health.ts +70 -0
- package/src/codex/user-identity.ts +266 -0
- package/src/codex/write-coordination.ts +114 -0
- package/src/config.ts +562 -26
- package/src/generated/jawcode-model-metadata.ts +2 -2
- package/src/grok/inject.ts +15 -4
- package/src/grok/inspect.ts +45 -0
- package/src/images/loop.ts +113 -20
- package/src/integrations/config-io.ts +151 -0
- package/src/integrations/journal.ts +315 -0
- package/src/integrations/merge.ts +135 -0
- package/src/integrations/native/ownership-preflight.ts +165 -0
- package/src/integrations/ownership.ts +111 -0
- package/src/integrations/registry.ts +101 -0
- package/src/integrations/serialize.ts +235 -0
- package/src/integrations/state.ts +290 -0
- package/src/integrations/store.ts +103 -0
- package/src/integrations/writer.ts +492 -0
- package/src/lib/bounded-body.ts +46 -8
- package/src/lib/bun-runtime.ts +110 -1
- package/src/lib/bun-stream-caps.ts +2 -1
- package/src/lib/redact.ts +407 -2
- package/src/lib/shadow-call.ts +24 -0
- package/src/lib/translator-budget.ts +10 -0
- package/src/lib/upstream-reachability.ts +91 -0
- package/src/lib/upstream-retry.ts +154 -2
- package/src/lib/windows-secret-acl.ts +212 -11
- package/src/lib/winsw.ts +9 -3
- package/src/oauth/index.ts +61 -3
- package/src/oauth/key-providers.ts +4 -0
- package/src/oauth/kiro.ts +50 -6
- package/src/oauth/store.ts +31 -6
- package/src/oauth/token-guardian.ts +9 -3
- package/src/providers/codex-capacity.ts +288 -0
- package/src/providers/derive.ts +33 -1
- package/src/providers/free-directory.ts +3 -1
- package/src/providers/key-failover.ts +71 -3
- package/src/providers/openai-sidecar.ts +64 -4
- package/src/providers/openai-virtual-models.ts +1 -0
- package/src/providers/quota.ts +334 -26
- package/src/providers/registry.ts +284 -16
- package/src/providers/xai-transport.ts +11 -4
- package/src/responses/compaction.ts +8 -1
- package/src/responses/hosted-tool-policy.ts +9 -0
- package/src/responses/parser.ts +44 -2
- package/src/responses/reasoning-envelope.ts +9 -1
- package/src/responses/reasoning-replay-cache.ts +105 -0
- package/src/responses/spill-store.ts +45 -8
- package/src/responses/state.ts +161 -17
- package/src/router.ts +302 -16
- package/src/routing/analytics.ts +377 -0
- package/src/routing/capability.ts +204 -0
- package/src/routing/cost.ts +77 -0
- package/src/routing/evaluator.ts +444 -0
- package/src/routing/health.ts +401 -0
- package/src/routing/history/cursor.ts +43 -0
- package/src/routing/history/indexer.ts +590 -0
- package/src/routing/history/schema.ts +72 -0
- package/src/routing/profile.ts +423 -0
- package/src/routing/quota.ts +124 -0
- package/src/routing/request-evidence.ts +45 -0
- package/src/routing/trace.ts +686 -0
- package/src/server/auth-cors.ts +46 -6
- package/src/server/chat-completions.ts +28 -13
- package/src/server/claude-messages.ts +23 -15
- package/src/server/gui-static.ts +39 -10
- package/src/server/images.ts +10 -1
- package/src/server/index.ts +238 -52
- package/src/server/lifecycle.ts +228 -9
- package/src/server/live.ts +14 -3
- package/src/server/management/agent-settings-routes.ts +64 -14
- package/src/server/management/combo-routes.ts +5 -5
- package/src/server/management/config-routes.ts +1 -1
- package/src/server/management/context.ts +42 -2
- package/src/server/management/integration-routes.ts +538 -0
- package/src/server/management/logs-usage-routes.ts +1 -1
- package/src/server/management/model-routes.ts +32 -113
- package/src/server/management/model-rows.ts +117 -0
- package/src/server/management/native-integration-routes.ts +587 -0
- package/src/server/management/oauth-account-routes.ts +1 -1
- package/src/server/management/provider-routes.ts +218 -117
- package/src/server/management/request-history-routes.ts +191 -0
- package/src/server/management/routing-analytics-routes.ts +74 -0
- package/src/server/management/routing-profile-routes.ts +177 -0
- package/src/server/management/shared.ts +2 -2
- package/src/server/management/sidebar-routes.ts +47 -31
- package/src/server/management/sync-response.ts +69 -0
- package/src/server/management/system-restart.ts +276 -24
- package/src/server/management/system-routes.ts +4 -0
- package/src/server/management-api.ts +84 -9
- package/src/server/management-auth.ts +43 -5
- package/src/server/relay-eager.ts +82 -42
- package/src/server/relay.ts +120 -6
- package/src/server/request-log.ts +26 -6
- package/src/server/responses/collaboration.ts +63 -8
- package/src/server/responses/compact.ts +272 -41
- package/src/server/responses/core.ts +730 -132
- package/src/server/responses/fetch-helpers.ts +15 -1
- package/src/server/responses-item-id-repair.ts +32 -3
- package/src/server/responses-json-events.ts +52 -0
- package/src/server/responses-snapshot-repair.ts +621 -0
- package/src/server/search.ts +51 -6
- package/src/server/sse-payload-rewrite.ts +89 -12
- package/src/server/startup-health-cache.ts +7 -1
- package/src/server/ws-bridge.ts +11 -17
- package/src/service-manager-probe.ts +297 -0
- package/src/service.ts +222 -32
- package/src/tray/windows-tray.ps1 +9 -0
- package/src/tray/windows.ts +15 -7
- package/src/types.ts +194 -14
- package/src/update/index.ts +13 -13
- package/src/update/job.ts +24 -21
- package/src/update/notify.ts +7 -3
- package/src/usage/cost.ts +0 -0
- package/src/usage/expected-prices.ts +129 -10
- package/src/usage/log.ts +50 -15
- package/src/usage/summary.ts +4 -4
- package/src/vision/index.ts +6 -1
- package/src/web-search/loop.ts +161 -34
- package/gui/dist/assets/index-OY43ubAq.css +0 -1
- package/gui/dist/assets/index-YwNnKZcL.js +0 -67
- package/gui/dist/provider-icons/antigravity.svg +0 -1
- package/gui/dist/provider-icons/claude.svg +0 -1
- package/gui/dist/provider-icons/copilot.svg +0 -1
- package/gui/dist/provider-icons/cursor.svg +0 -2
- package/gui/dist/provider-icons/gemini.svg +0 -1
- package/gui/dist/provider-icons/grok-color.svg +0 -1
- package/gui/dist/provider-icons/kiro.svg +0 -14
|
@@ -9,7 +9,8 @@ import { parseRequest } from "../../responses/parser";
|
|
|
9
9
|
import { buildCompactV1Output, COMPACT_PROMPT, decodeCompactionSummary, extractCompactUserMessages } from "../../responses/compaction";
|
|
10
10
|
import { FORWARD_HEADERS, sanitizeReasoningInputContent } from "../../adapters/openai-responses";
|
|
11
11
|
import { expandPreviousResponseInput, previousResponseProviderState, rememberResponseState } from "../../responses/state";
|
|
12
|
-
import { routeModel } from "../../router";
|
|
12
|
+
import { NoEligiblePolicyCandidateError, routeModel } from "../../router";
|
|
13
|
+
import { evidenceFromBody } from "../../routing/request-evidence";
|
|
13
14
|
import {
|
|
14
15
|
advanceComboAfterFailure,
|
|
15
16
|
comboDefaultEffort,
|
|
@@ -27,7 +28,7 @@ import {
|
|
|
27
28
|
import { isInjectionDebugEnabled } from "../../lib/debug-settings";
|
|
28
29
|
import { injectionDebugLog } from "../../lib/injection-debug-log";
|
|
29
30
|
import { modelInList, namespacedToolName } from "../../types";
|
|
30
|
-
import type { AdapterEvent, OcxConfig, OcxParsedRequest, OcxProviderConfig, OcxProviderContinuationState, OcxUsage } from "../../types";
|
|
31
|
+
import type { AdapterEvent, CodexAccountMode, OcxConfig, OcxParsedRequest, OcxProviderConfig, OcxProviderContinuationState, OcxUsage } from "../../types";
|
|
31
32
|
import {
|
|
32
33
|
forceRefreshOAuthAccessSnapshot,
|
|
33
34
|
getOAuthCredentialApiBaseUrl,
|
|
@@ -42,9 +43,11 @@ import { createAdapterEventQueue, preflightAdapterEvents } from "../../adapters/
|
|
|
42
43
|
import {
|
|
43
44
|
applyCodexAuthContextToProvider,
|
|
44
45
|
CodexAccountCooldownError,
|
|
46
|
+
codexMainProfileDrainingResponse,
|
|
45
47
|
cooldownErrorResponse,
|
|
46
48
|
CodexAuthContextError,
|
|
47
49
|
CodexDirectAuthenticationError,
|
|
50
|
+
CodexMainProfileDrainingError,
|
|
48
51
|
CodexPoolAuthenticationError,
|
|
49
52
|
CodexThreadAffinityExpiredError,
|
|
50
53
|
headersForCodexAuthContext,
|
|
@@ -52,6 +55,7 @@ import {
|
|
|
52
55
|
resolveCodexAuthContext,
|
|
53
56
|
codexProbeLeaseId,
|
|
54
57
|
codexProbeQuotaScope,
|
|
58
|
+
releaseCodexAuthContextProbeLease,
|
|
55
59
|
type CodexAuthContext,
|
|
56
60
|
} from "../../codex/auth-context";
|
|
57
61
|
import {
|
|
@@ -59,7 +63,14 @@ import {
|
|
|
59
63
|
recordCodexUpstreamOutcome,
|
|
60
64
|
type CodexUpstreamOutcome,
|
|
61
65
|
} from "../../codex/routing";
|
|
62
|
-
import {
|
|
66
|
+
import {
|
|
67
|
+
fetchWithResetRetry,
|
|
68
|
+
fetchWithTransientRetry,
|
|
69
|
+
applyUpstreamRecoveryInit,
|
|
70
|
+
type UpstreamSendRecovery,
|
|
71
|
+
} from "../../lib/upstream-retry";
|
|
72
|
+
import { classifyTransportFailureKind, transportErrorCode } from "../../lib/upstream-reachability";
|
|
73
|
+
import { recordUpstreamHostFailure, resetUpstreamHostHealth, upstreamHostHealthKey } from "../../codex/upstream-host-health";
|
|
63
74
|
import { ForwardAdmissionCredentialError, validateForwardAdmissionCredential } from "../auth-cors";
|
|
64
75
|
import { listOpenAiForwardSidecarCandidates, resolveFirstUsableOpenAiSidecar, type ResolvedOpenAiForwardSidecar } from "../../providers/openai-sidecar";
|
|
65
76
|
import { isCanonicalOpenAiForwardProvider, supportsNativeResponsesCompactEndpoint } from "../../providers/openai-tiers";
|
|
@@ -72,7 +83,8 @@ import { hasKeyPoolFailover, rotateProviderTransportOn429 } from "../../provider
|
|
|
72
83
|
import { shouldAttemptImageTierRetry } from "../image-retry";
|
|
73
84
|
import { resolveProviderTransport } from "../../providers/xai-transport";
|
|
74
85
|
import type { WsData } from "../ws-bridge";
|
|
75
|
-
import { registerTurn, trackStreamLifetime, unregisterTurn } from "../lifecycle";
|
|
86
|
+
import { codexAccountSelectionForTurn, registerTurn, trackStreamLifetime, unregisterTurn } from "../lifecycle";
|
|
87
|
+
import type { AdmissionLease } from "../../lib/admission";
|
|
76
88
|
import { redactSecretString } from "../../lib/redact";
|
|
77
89
|
import { readBoundedResponseBody } from "../../lib/bounded-body";
|
|
78
90
|
import { supportedLadderFor } from "../effort-policy";
|
|
@@ -101,7 +113,7 @@ import { hasResponsesItemIdRepair, relaySseWithResponsesItemIdRepair } from "../
|
|
|
101
113
|
import type { EffectiveSubagentRoster, SpawnAgentSurface } from "../../codex/catalog";
|
|
102
114
|
|
|
103
115
|
import { decodeRequestErrorResponse, handleResponses, usesCodexForwardPoolAuth } from "./core";
|
|
104
|
-
import { fetchWithHeaderTimeout, providerFetch, safeHostLabel } from "./fetch-helpers";
|
|
116
|
+
import { fetchWithHeaderTimeout, providerFetch, safeHostLabel, safeOriginLabel } from "./fetch-helpers";
|
|
105
117
|
|
|
106
118
|
export const COMPACT_RESPONSE_MAX_BYTES = 32 * 1024 * 1024;
|
|
107
119
|
|
|
@@ -117,10 +129,88 @@ export function compactResponseTooLargeError(): Response {
|
|
|
117
129
|
|
|
118
130
|
|
|
119
131
|
|
|
132
|
+
/**
|
|
133
|
+
* Resolve one eligible pool account other than `excludeAccountId`, and build everything
|
|
134
|
+
* the alternate send needs. Returns null when no alternate exists or construction fails,
|
|
135
|
+
* in which case the caller keeps the first account's rejection intact.
|
|
136
|
+
*
|
|
137
|
+
* Mirrors the auth resolution the native compact branch already does for the first
|
|
138
|
+
* account, so the alternate is built the same way rather than through a second,
|
|
139
|
+
* divergent path.
|
|
140
|
+
*/
|
|
141
|
+
async function resolveAlternateCompactContext(args: {
|
|
142
|
+
req: Request;
|
|
143
|
+
config: OcxConfig;
|
|
144
|
+
route: { provider: OcxProviderConfig; codexAccountMode?: CodexAccountMode };
|
|
145
|
+
selectedModelId: string | undefined;
|
|
146
|
+
excludeAccountId: string | null;
|
|
147
|
+
turnAdmissionLease?: AdmissionLease;
|
|
148
|
+
}): Promise<{ authCtx: CodexAuthContext; provider: OcxProviderConfig; headers: Headers } | null> {
|
|
149
|
+
const { req, config, route, selectedModelId, excludeAccountId, turnAdmissionLease } = args;
|
|
150
|
+
if (!route.codexAccountMode || !excludeAccountId) return null;
|
|
151
|
+
try {
|
|
152
|
+
const authCtx = await resolveCodexAuthContext(req.headers, config, route.codexAccountMode, {
|
|
153
|
+
...(selectedModelId ? { modelId: selectedModelId } : {}),
|
|
154
|
+
excludeAccountId,
|
|
155
|
+
beginCodexAccountSelection: codexAccountSelectionForTurn(turnAdmissionLease),
|
|
156
|
+
});
|
|
157
|
+
if (!authCtx.accountId || authCtx.accountId === excludeAccountId) return null;
|
|
158
|
+
const provider = applyCodexAuthContextToProvider(route.provider, authCtx, route.codexAccountMode);
|
|
159
|
+
const headers = new Headers({ "content-type": "application/json" });
|
|
160
|
+
const selected = headersForCodexAuthContext(req.headers, authCtx);
|
|
161
|
+
for (const name of FORWARD_HEADERS) {
|
|
162
|
+
const value = selected.get(name);
|
|
163
|
+
if (value) headers.set(name, value);
|
|
164
|
+
}
|
|
165
|
+
const override = (provider as { _codexAccountOverride?: { accessToken: string; chatgptAccountId: string } })._codexAccountOverride;
|
|
166
|
+
if (override) {
|
|
167
|
+
headers.set("authorization", `Bearer ${override.accessToken}`);
|
|
168
|
+
headers.set("chatgpt-account-id", override.chatgptAccountId);
|
|
169
|
+
}
|
|
170
|
+
if (provider.apiKey) headers.set("authorization", `Bearer ${resolveEnvValue(provider.apiKey)}`);
|
|
171
|
+
return { authCtx, provider, headers };
|
|
172
|
+
} catch (err) {
|
|
173
|
+
if (err instanceof CodexMainProfileDrainingError) {
|
|
174
|
+
// The native-main fence can start after account A has already rejected the
|
|
175
|
+
// request. Treat the now-fenced main profile as no alternate and preserve A's
|
|
176
|
+
// real rejection instead of replacing it with a synthetic drain response.
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
// No eligible alternate (all cooled, affinity expired, reauth needed) — the caller
|
|
180
|
+
// returns the first account's rejection unchanged, which is today's behavior.
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Headers a client needs to back off correctly after a pool rejection. The buffered
|
|
187
|
+
* response is rebuilt from scratch, so anything not listed here is dropped — which is
|
|
188
|
+
* what silently discarded `Retry-After` and the reset hints from a 429 before.
|
|
189
|
+
* Deliberately narrow: no hop-by-hop headers, no content-length (the buffered body sets
|
|
190
|
+
* its own), no cookies.
|
|
191
|
+
*/
|
|
192
|
+
const COMPACT_PASSTHROUGH_HEADERS = [
|
|
193
|
+
"retry-after",
|
|
194
|
+
"x-codex-primary-reset-at",
|
|
195
|
+
"x-codex-secondary-reset-at",
|
|
196
|
+
"x-codex-tertiary-reset-at",
|
|
197
|
+
// A relayed 3xx keeps its Location so the client can follow it (#914).
|
|
198
|
+
"location",
|
|
199
|
+
];
|
|
200
|
+
|
|
201
|
+
function compactResponseHeaders(upstream: Response): Headers {
|
|
202
|
+
const headers = new Headers({ "Content-Type": upstream.headers.get("content-type") ?? "application/json" });
|
|
203
|
+
for (const name of COMPACT_PASSTHROUGH_HEADERS) {
|
|
204
|
+
const value = upstream.headers.get(name);
|
|
205
|
+
if (value) headers.set(name, value);
|
|
206
|
+
}
|
|
207
|
+
return headers;
|
|
208
|
+
}
|
|
209
|
+
|
|
120
210
|
export async function bufferCompactResponse(upstream: Response, signal: AbortSignal): Promise<Response> {
|
|
121
211
|
const reader = upstream.body?.getReader();
|
|
122
|
-
const
|
|
123
|
-
if (!reader) return new Response(null, { status: upstream.status,
|
|
212
|
+
const headers = compactResponseHeaders(upstream);
|
|
213
|
+
if (!reader) return new Response(null, { status: upstream.status, statusText: upstream.statusText, headers });
|
|
124
214
|
const declaredLength = Number(upstream.headers.get("content-length"));
|
|
125
215
|
if (Number.isFinite(declaredLength) && declaredLength > COMPACT_RESPONSE_MAX_BYTES) {
|
|
126
216
|
await reader.cancel("compact_response_too_large").catch(() => undefined);
|
|
@@ -153,7 +243,7 @@ export async function bufferCompactResponse(upstream: Response, signal: AbortSig
|
|
|
153
243
|
body.set(chunk, offset);
|
|
154
244
|
offset += chunk.byteLength;
|
|
155
245
|
}
|
|
156
|
-
return new Response(body, { status: upstream.status,
|
|
246
|
+
return new Response(body, { status: upstream.status, statusText: upstream.statusText, headers });
|
|
157
247
|
}
|
|
158
248
|
|
|
159
249
|
|
|
@@ -162,6 +252,7 @@ export async function handleResponsesCompact(
|
|
|
162
252
|
req: Request,
|
|
163
253
|
config: OcxConfig,
|
|
164
254
|
logCtx: RequestLogContext,
|
|
255
|
+
turnAdmissionLease?: AdmissionLease,
|
|
165
256
|
): Promise<Response> {
|
|
166
257
|
let body: unknown;
|
|
167
258
|
try {
|
|
@@ -179,14 +270,26 @@ export async function handleResponsesCompact(
|
|
|
179
270
|
|
|
180
271
|
let route;
|
|
181
272
|
try {
|
|
182
|
-
route
|
|
273
|
+
// Compact requests route through the same policy evaluation as normal
|
|
274
|
+
// turns, so body-derived evidence (tools/image) must reach the first
|
|
275
|
+
// evaluation too - not only the later handleResponses dispatch.
|
|
276
|
+
route = routeModel(config, raw.model, evidenceFromBody(raw));
|
|
183
277
|
} catch (err) {
|
|
278
|
+
if (err instanceof NoEligiblePolicyCandidateError) {
|
|
279
|
+
// Persist the evaluation trace (per-candidate exclusions + the
|
|
280
|
+
// no-eligible reason) so a failed compact policy request stays
|
|
281
|
+
// auditable, matching the other request handlers.
|
|
282
|
+
logCtx.routeDecision = err.trace;
|
|
283
|
+
}
|
|
184
284
|
return formatErrorResponse(404, "invalid_request_error", err instanceof Error ? err.message : String(err));
|
|
185
285
|
}
|
|
186
286
|
const selectedModelId = route.modelId;
|
|
187
287
|
logCtx.requestedModel = raw.model;
|
|
188
288
|
logCtx.model = selectedModelId;
|
|
189
|
-
logCtx.
|
|
289
|
+
logCtx.routeDecision = route.routeDecision;
|
|
290
|
+
logCtx.provider = route.codexAccountNamespace
|
|
291
|
+
? `${route.providerName}-${route.codexAccountNamespace}`
|
|
292
|
+
: route.providerName;
|
|
190
293
|
logCtx.providerAdapter = route.provider.adapter;
|
|
191
294
|
const virtual = resolveOpenAiCompactModel(route.providerName, selectedModelId);
|
|
192
295
|
if (virtual) {
|
|
@@ -218,7 +321,11 @@ export async function handleResponsesCompact(
|
|
|
218
321
|
const headers = new Headers({ "content-type": "application/json" });
|
|
219
322
|
try {
|
|
220
323
|
if (route.codexAccountMode) {
|
|
221
|
-
authCtx = await resolveCodexAuthContext(req.headers, config, route.codexAccountMode, {
|
|
324
|
+
authCtx = await resolveCodexAuthContext(req.headers, config, route.codexAccountMode, {
|
|
325
|
+
accountId: route.codexAccountId,
|
|
326
|
+
modelId: selectedModelId,
|
|
327
|
+
beginCodexAccountSelection: codexAccountSelectionForTurn(turnAdmissionLease),
|
|
328
|
+
});
|
|
222
329
|
const selected = headersForCodexAuthContext(req.headers, authCtx);
|
|
223
330
|
compactProvider = applyCodexAuthContextToProvider(route.provider, authCtx, route.codexAccountMode);
|
|
224
331
|
for (const name of FORWARD_HEADERS) {
|
|
@@ -233,8 +340,9 @@ export async function handleResponsesCompact(
|
|
|
233
340
|
}
|
|
234
341
|
} catch (err) {
|
|
235
342
|
if (err instanceof CodexAccountCooldownError) {
|
|
236
|
-
return cooldownErrorResponse(err);
|
|
343
|
+
return cooldownErrorResponse(err, Date.now(), route.codexAccountNamespace);
|
|
237
344
|
}
|
|
345
|
+
if (err instanceof CodexMainProfileDrainingError) return codexMainProfileDrainingResponse();
|
|
238
346
|
if (err instanceof CodexThreadAffinityExpiredError) {
|
|
239
347
|
return formatErrorResponse(409, "invalid_request_error", "Codex thread account affinity expired; start a new session");
|
|
240
348
|
}
|
|
@@ -247,7 +355,9 @@ export async function handleResponsesCompact(
|
|
|
247
355
|
throw err;
|
|
248
356
|
}
|
|
249
357
|
const base = (compactProvider.baseUrl ?? "").replace(/\/$/, "");
|
|
250
|
-
if (compactProvider.
|
|
358
|
+
if (compactProvider.authMode !== "forward" && compactProvider.apiKey) {
|
|
359
|
+
headers.set("authorization", `Bearer ${resolveEnvValue(compactProvider.apiKey)}`);
|
|
360
|
+
}
|
|
251
361
|
const { reasoning: _reasoning, ...compactBodyRaw } = raw as typeof raw & { reasoning?: unknown };
|
|
252
362
|
// The regular /v1/responses path applies sanitizeReasoningInputContent via the adapter's
|
|
253
363
|
// buildRequest, but the compact endpoint forwards directly. Apply the same sanitizer here
|
|
@@ -256,50 +366,167 @@ export async function handleResponsesCompact(
|
|
|
256
366
|
const compactUrl = `${base}/responses/compact`;
|
|
257
367
|
const compactThreadId = req.headers.get("x-codex-parent-thread-id");
|
|
258
368
|
const connectMs = config.connectTimeoutMs ?? 200_000;
|
|
369
|
+
// Takes its context explicitly: the alternate-account flow below records a rejection
|
|
370
|
+
// against A while promoting B, then records B's own outcome. A closure over a single
|
|
371
|
+
// `authCtx` cannot express either.
|
|
259
372
|
const recordCompactPoolOutcome = (
|
|
373
|
+
ctx: CodexAuthContext,
|
|
260
374
|
outcome: CodexUpstreamOutcome,
|
|
261
|
-
meta: {
|
|
375
|
+
meta: {
|
|
376
|
+
retryAfter?: string | null;
|
|
377
|
+
resetAt?: unknown | unknown[];
|
|
378
|
+
promoteAccountId?: string;
|
|
379
|
+
} = {},
|
|
262
380
|
) => {
|
|
263
|
-
if (!usesCodexForwardPoolAuth(
|
|
264
|
-
recordCodexUpstreamOutcome(config,
|
|
381
|
+
if (!usesCodexForwardPoolAuth(ctx, route.provider)) return;
|
|
382
|
+
recordCodexUpstreamOutcome(config, ctx.accountId, outcome, {
|
|
265
383
|
...meta,
|
|
266
384
|
threadId: compactThreadId,
|
|
385
|
+
fixedAccount: ctx.fixedAccount,
|
|
267
386
|
modelId: selectedModelId,
|
|
268
|
-
probeLeaseId: codexProbeLeaseId(
|
|
269
|
-
probeQuotaScope: codexProbeQuotaScope(
|
|
270
|
-
writerGeneration:
|
|
271
|
-
?
|
|
387
|
+
probeLeaseId: codexProbeLeaseId(ctx),
|
|
388
|
+
probeQuotaScope: codexProbeQuotaScope(ctx),
|
|
389
|
+
writerGeneration: ctx.kind === "pool" || ctx.kind === "main-pool"
|
|
390
|
+
? ctx.writerGeneration
|
|
272
391
|
: undefined,
|
|
273
392
|
});
|
|
274
393
|
};
|
|
394
|
+
// Two recovery modes, mirroring retryCodexPoolOnAlternateAccount() on the regular
|
|
395
|
+
// path (core.ts:396). The first account keeps the full ladder — transient-5xx retry
|
|
396
|
+
// wrapping reset retry — because those retries happen before any alternate is even
|
|
397
|
+
// considered. The alternate is one bounded send: a second ladder would multiply the
|
|
398
|
+
// work an already-rejecting pool is doing.
|
|
399
|
+
const sendCompactAttempt = (
|
|
400
|
+
sendProvider: OcxProviderConfig,
|
|
401
|
+
sendHeaders: Headers,
|
|
402
|
+
recovery: "normal" | "single",
|
|
403
|
+
): Promise<Response> => {
|
|
404
|
+
const doFetch = (upstreamRecovery?: UpstreamSendRecovery) => fetchWithHeaderTimeout(
|
|
405
|
+
compactUrl,
|
|
406
|
+
applyUpstreamRecoveryInit({
|
|
407
|
+
method: "POST",
|
|
408
|
+
headers: sendHeaders,
|
|
409
|
+
body: JSON.stringify({ ...compactBody, model: route.modelId }),
|
|
410
|
+
}, upstreamRecovery),
|
|
411
|
+
req.signal,
|
|
412
|
+
connectMs,
|
|
413
|
+
false,
|
|
414
|
+
providerFetch(sendProvider),
|
|
415
|
+
// Every credential-bearing forward send gets manual redirects, not only
|
|
416
|
+
// pool sends: direct mode carries the caller's credential too (#914).
|
|
417
|
+
sendProvider.authMode === "forward",
|
|
418
|
+
).then(res => {
|
|
419
|
+
// Every real attempt response — including an intermediate 5xx the retry
|
|
420
|
+
// wrapper replaces — proves the host was reached (#914 review).
|
|
421
|
+
resetUpstreamHostHealth(upstreamHostHealthKey(route.providerName, safeOriginLabel(compactUrl)));
|
|
422
|
+
return res;
|
|
423
|
+
});
|
|
424
|
+
return recovery === "single"
|
|
425
|
+
? doFetch()
|
|
426
|
+
: fetchWithTransientRetry(doFetch, { abortSignal: req.signal, label: safeHostLabel(compactUrl) });
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
// The account each outcome belongs to. Reassigned only when the alternate send below
|
|
430
|
+
// actually happens, so every recorder call names the context that produced it.
|
|
431
|
+
let outcomeCtx = authCtx;
|
|
275
432
|
let upstream: Response;
|
|
276
433
|
try {
|
|
277
434
|
// Same connect timeout + keep-alive reset + transient-5xx recovery as /v1/responses —
|
|
278
435
|
// compact hits the same ChatGPT host and must soft-avoid / clear affinity (#186).
|
|
279
|
-
upstream = await
|
|
280
|
-
recovery => fetchWithHeaderTimeout(
|
|
281
|
-
compactUrl,
|
|
282
|
-
applyUpstreamRecoveryInit({
|
|
283
|
-
method: "POST",
|
|
284
|
-
headers,
|
|
285
|
-
body: JSON.stringify({ ...compactBody, model: route.modelId }),
|
|
286
|
-
}, recovery),
|
|
287
|
-
req.signal,
|
|
288
|
-
connectMs,
|
|
289
|
-
false,
|
|
290
|
-
providerFetch(compactProvider),
|
|
291
|
-
),
|
|
292
|
-
{ abortSignal: req.signal, label: safeHostLabel(compactUrl) },
|
|
293
|
-
);
|
|
436
|
+
upstream = await sendCompactAttempt(compactProvider, headers, "normal");
|
|
294
437
|
} catch (err) {
|
|
295
438
|
if (req.signal.aborted) {
|
|
296
|
-
recordCompactPoolOutcome(499);
|
|
439
|
+
recordCompactPoolOutcome(outcomeCtx, 499);
|
|
297
440
|
return formatErrorResponse(499, "client_cancelled", "Client cancelled compact request");
|
|
298
441
|
}
|
|
299
|
-
const outcome = err
|
|
300
|
-
|
|
442
|
+
const outcome = classifyTransportFailureKind(err);
|
|
443
|
+
// Host-level evidence stands regardless of pool membership (#914 review).
|
|
444
|
+
if (outcome === "connect_neutral") {
|
|
445
|
+
recordUpstreamHostFailure(
|
|
446
|
+
upstreamHostHealthKey(route.providerName, safeOriginLabel(compactUrl)),
|
|
447
|
+
{ code: transportErrorCode(err) },
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
recordCompactPoolOutcome(outcomeCtx, outcome);
|
|
301
451
|
return formatErrorResponse(502, "upstream_error", "Failed to connect to compact upstream");
|
|
302
452
|
}
|
|
453
|
+
|
|
454
|
+
// Bounded same-request alternate: the regular /v1/responses path already does this
|
|
455
|
+
// (core.ts:319-423) and recognizes exactly 429/402. Without it a pool rejection
|
|
456
|
+
// surfaces to the client, which retries the compact task OUTSIDE the logical request
|
|
457
|
+
// — reporting exhausted retries while another pool account sat idle (#913).
|
|
458
|
+
if (
|
|
459
|
+
(upstream.status === 429 || upstream.status === 402)
|
|
460
|
+
&& usesCodexForwardPoolAuth(authCtx, route.provider)
|
|
461
|
+
&& !authCtx.fixedAccount
|
|
462
|
+
&& route.codexAccountMode
|
|
463
|
+
&& !req.signal.aborted
|
|
464
|
+
) {
|
|
465
|
+
const firstRetryAfter = upstream.headers.get("retry-after");
|
|
466
|
+
const firstResetAt = [
|
|
467
|
+
upstream.headers.get("x-codex-primary-reset-at"),
|
|
468
|
+
upstream.headers.get("x-codex-secondary-reset-at"),
|
|
469
|
+
upstream.headers.get("x-codex-tertiary-reset-at"),
|
|
470
|
+
].filter(Boolean);
|
|
471
|
+
// Build the alternate COMPLETELY before cancelling the first body: if construction
|
|
472
|
+
// throws, the first rejection is still intact and can be returned to the client.
|
|
473
|
+
const alternate = await resolveAlternateCompactContext({
|
|
474
|
+
req,
|
|
475
|
+
config,
|
|
476
|
+
route,
|
|
477
|
+
selectedModelId,
|
|
478
|
+
excludeAccountId: authCtx.accountId,
|
|
479
|
+
turnAdmissionLease,
|
|
480
|
+
});
|
|
481
|
+
// Resolution can await a credential refresh, so the client may have gone away
|
|
482
|
+
// while we were choosing B. Re-check before spending anything: recording A,
|
|
483
|
+
// cancelling its body, and sending B are all observable side effects, and B's
|
|
484
|
+
// quota is not ours to spend on a request nobody is waiting for.
|
|
485
|
+
if (alternate && req.signal.aborted) {
|
|
486
|
+
releaseCodexAuthContextProbeLease(alternate.authCtx);
|
|
487
|
+
recordCompactPoolOutcome(outcomeCtx, 499);
|
|
488
|
+
return formatErrorResponse(499, "client_cancelled", "Client cancelled compact request");
|
|
489
|
+
}
|
|
490
|
+
if (alternate) {
|
|
491
|
+
// Same order the regular path uses (core.ts:349-357): a 429/402 carries the
|
|
492
|
+
// quota snapshot that produced it, so refresh A's cache before recording its
|
|
493
|
+
// rejection. Skipping this leaves quota-strategy routing and the dashboard
|
|
494
|
+
// reading numbers from before the account ran out.
|
|
495
|
+
if (authCtx.kind === "pool" || authCtx.kind === "main-pool") {
|
|
496
|
+
const { applyAccountQuotaFromUpstreamHeaders } = await import("../../codex/auth-api");
|
|
497
|
+
applyAccountQuotaFromUpstreamHeaders(
|
|
498
|
+
authCtx.accountId,
|
|
499
|
+
upstream.headers,
|
|
500
|
+
authCtx.writerGeneration,
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
recordCompactPoolOutcome(authCtx, upstream.status, {
|
|
504
|
+
retryAfter: firstRetryAfter,
|
|
505
|
+
resetAt: firstResetAt,
|
|
506
|
+
...(alternate.authCtx.accountId ? { promoteAccountId: alternate.authCtx.accountId } : {}),
|
|
507
|
+
});
|
|
508
|
+
await upstream.body?.cancel().catch(() => undefined);
|
|
509
|
+
outcomeCtx = alternate.authCtx;
|
|
510
|
+
try {
|
|
511
|
+
upstream = await sendCompactAttempt(alternate.provider, alternate.headers, "single");
|
|
512
|
+
} catch (err) {
|
|
513
|
+
if (req.signal.aborted) {
|
|
514
|
+
recordCompactPoolOutcome(outcomeCtx, 499);
|
|
515
|
+
return formatErrorResponse(499, "client_cancelled", "Client cancelled compact request");
|
|
516
|
+
}
|
|
517
|
+
const outcome = classifyTransportFailureKind(err);
|
|
518
|
+
// Host-level evidence stands regardless of pool membership (#914 review).
|
|
519
|
+
if (outcome === "connect_neutral") {
|
|
520
|
+
recordUpstreamHostFailure(
|
|
521
|
+
upstreamHostHealthKey(route.providerName, safeOriginLabel(compactUrl)),
|
|
522
|
+
{ code: transportErrorCode(err) },
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
recordCompactPoolOutcome(outcomeCtx, outcome);
|
|
526
|
+
return formatErrorResponse(502, "upstream_error", "Failed to connect to compact upstream");
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
303
530
|
const retryAfter = upstream.headers.get("retry-after");
|
|
304
531
|
const resetAt = [
|
|
305
532
|
upstream.headers.get("x-codex-primary-reset-at"),
|
|
@@ -310,12 +537,16 @@ export async function handleResponsesCompact(
|
|
|
310
537
|
// Record pool health only after the body is fully delivered (or definitively failed).
|
|
311
538
|
// A premature 200 would clear soft-avoid while the client still sees a buffer 502.
|
|
312
539
|
if (buffered.status === 499) {
|
|
313
|
-
recordCompactPoolOutcome(499);
|
|
540
|
+
recordCompactPoolOutcome(outcomeCtx, 499);
|
|
314
541
|
return buffered;
|
|
315
542
|
}
|
|
316
543
|
// Always record the real upstream status: a local buffering failure after a
|
|
317
544
|
// 200 upstream response must not soft-avoid a healthy account or rotate a thread.
|
|
318
|
-
recordCompactPoolOutcome(upstream.status, { retryAfter, resetAt });
|
|
545
|
+
recordCompactPoolOutcome(outcomeCtx, upstream.status, { retryAfter, resetAt });
|
|
546
|
+
// Lift usage and response metadata from the buffered upstream JSON into the
|
|
547
|
+
// request log; the routed branch gets the same through handleResponses. The
|
|
548
|
+
// synthetic buffer errors are not upstream bodies and stay uninspected.
|
|
549
|
+
if (buffered.ok) inspectResponseLogJson(logCtx, await buffered.clone().text());
|
|
319
550
|
return buffered;
|
|
320
551
|
}
|
|
321
552
|
|
|
@@ -337,7 +568,7 @@ export async function handleResponsesCompact(
|
|
|
337
568
|
headers: internalHeaders,
|
|
338
569
|
body: JSON.stringify(internalBody),
|
|
339
570
|
});
|
|
340
|
-
const response = await handleResponses(internalReq, config, logCtx, { abortSignal: req.signal });
|
|
571
|
+
const response = await handleResponses(internalReq, config, logCtx, { abortSignal: req.signal, turnAdmissionLease });
|
|
341
572
|
if (!response.ok) return response;
|
|
342
573
|
let json: { output?: unknown[]; status?: unknown; error?: unknown };
|
|
343
574
|
try {
|