@bitkyc08/opencodex 2.7.23 → 2.7.24
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/README.ko.md +37 -7
- package/README.md +52 -11
- package/README.zh-CN.md +36 -7
- package/bin/ocx.mjs +5 -3
- package/gui/dist/assets/index-BzhyTAco.js +40 -0
- package/gui/dist/assets/index-Dq3eZ1cU.css +1 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/opencode.svg +1 -1
- package/package.json +5 -2
- package/src/adapters/anthropic-image-normalize.ts +70 -29
- package/src/adapters/cursor/transport-retry.ts +20 -1
- package/src/adapters/run-turn-queue.ts +40 -0
- package/src/codex/auth-api.ts +10 -1
- package/src/codex/auth-context.ts +33 -7
- package/src/codex/catalog.ts +357 -23
- package/src/codex/routing.ts +10 -4
- package/src/combos/failover.ts +102 -0
- package/src/combos/index.ts +37 -0
- package/src/combos/request.ts +31 -0
- package/src/combos/resolve.ts +171 -0
- package/src/combos/types.ts +203 -0
- package/src/config.ts +280 -11
- package/src/lib/errors.ts +86 -24
- package/src/lib/upstream-retry.ts +8 -4
- package/src/oauth/index.ts +7 -1
- package/src/oauth/key-providers.ts +2 -32
- package/src/oauth/login-cli.ts +4 -3
- package/src/oauth/token-guardian.ts +38 -3
- package/src/providers/derive.ts +27 -2
- package/src/providers/kiro-models.ts +8 -3
- package/src/providers/label.ts +3 -1
- package/src/providers/openai-sidecar.ts +94 -0
- package/src/providers/openai-tier-startup.ts +27 -0
- package/src/providers/openai-tiers.ts +283 -0
- package/src/providers/openai-virtual-models.ts +82 -0
- package/src/providers/quota.ts +344 -24
- package/src/providers/registry.ts +112 -20
- package/src/reasoning-effort.ts +12 -11
- package/src/router.ts +80 -36
- package/src/server/auth-cors.ts +85 -9
- package/src/server/images.ts +31 -75
- package/src/server/index.ts +45 -86
- package/src/server/management-api.ts +273 -21
- package/src/server/request-log.ts +221 -20
- package/src/server/responses.ts +594 -75
- package/src/server/search.ts +22 -37
- package/src/types.ts +49 -1
- package/src/update/index.ts +50 -6
- package/src/update/job.ts +21 -4
- package/src/usage/log.ts +124 -1
- package/src/usage/summary.ts +147 -56
- package/src/vision/index.ts +20 -19
- package/src/web-search/index.ts +15 -17
- package/gui/dist/assets/index-Bk_GgFrh.css +0 -1
- package/gui/dist/assets/index-DQjt6Hly.js +0 -40
package/src/server/images.ts
CHANGED
|
@@ -15,20 +15,18 @@ import { formatErrorResponse } from "../bridge";
|
|
|
15
15
|
import {
|
|
16
16
|
CodexAccountCooldownError,
|
|
17
17
|
CodexAuthContextError,
|
|
18
|
+
CodexPoolAuthenticationError,
|
|
18
19
|
CodexThreadAffinityExpiredError,
|
|
19
|
-
headersForCodexAuthContext,
|
|
20
|
-
isCodexAuthContextUsable,
|
|
21
|
-
resolveCodexAuthContext,
|
|
22
20
|
} from "../codex/auth-context";
|
|
23
21
|
import { formatCodexProviderForLog } from "../codex/routing";
|
|
24
|
-
import { resolveEnvValue } from "../config";
|
|
25
22
|
import { signalWithTimeout } from "../lib/abort";
|
|
26
23
|
import { sidecarEnter } from "../lib/sidecar-tracker";
|
|
27
|
-
import type { OcxConfig
|
|
28
|
-
import {
|
|
24
|
+
import type { OcxConfig } from "../types";
|
|
25
|
+
import { resolveFirstUsableOpenAiSidecar, selectOpenAiImagesProvider } from "../providers/openai-sidecar";
|
|
29
26
|
import { readJsonRequestBody } from "./request-decompress";
|
|
27
|
+
import { ForwardAdmissionCredentialError, validateForwardAdmissionCredential } from "./auth-cors";
|
|
30
28
|
import type { RequestLogContext } from "./request-log";
|
|
31
|
-
import { codexLogAccountId, decodeRequestErrorResponse
|
|
29
|
+
import { codexLogAccountId, decodeRequestErrorResponse } from "./responses";
|
|
32
30
|
|
|
33
31
|
export type ImagesEndpoint = "generations" | "edits";
|
|
34
32
|
|
|
@@ -42,45 +40,17 @@ const IMAGES_UPSTREAM_TIMEOUT_MS = 300_000;
|
|
|
42
40
|
*/
|
|
43
41
|
const IMAGES_RESPONSE_MAX_BYTES = 100 * 1024 * 1024;
|
|
44
42
|
|
|
45
|
-
interface NamedProvider {
|
|
46
|
-
name: string;
|
|
47
|
-
provider: OcxProviderConfig;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
interface ImagesUpstreamCandidates {
|
|
51
|
-
/** ChatGPT passthrough — the backend codex itself would have called absent the base_url override. */
|
|
52
|
-
forward?: NamedProvider;
|
|
53
|
-
/** Keyed openai-responses provider (e.g. api.openai.com), whose /v1/images/* is the platform Images API. */
|
|
54
|
-
keyed?: NamedProvider & { apiKey: string };
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Collect the upstreams that can serve /images/*. The forward provider is preferred (same
|
|
59
|
-
* precedence as the vision/web-search sidecars) but only usable when the request actually
|
|
60
|
-
* carries relayable ChatGPT auth — startServer auto-upserts a `chatgpt` forward entry into
|
|
61
|
-
* every config, so its mere presence proves nothing about credentials.
|
|
62
|
-
*/
|
|
63
|
-
function findImagesUpstreams(config: OcxConfig): ImagesUpstreamCandidates {
|
|
64
|
-
const candidates: ImagesUpstreamCandidates = {};
|
|
65
|
-
for (const [name, provider] of Object.entries(config.providers)) {
|
|
66
|
-
if (provider.disabled === true) continue;
|
|
67
|
-
if (provider.authMode === "forward") {
|
|
68
|
-
candidates.forward ??= { name, provider };
|
|
69
|
-
continue;
|
|
70
|
-
}
|
|
71
|
-
if (candidates.keyed || provider.adapter !== "openai-responses" || provider.authMode === "oauth") continue;
|
|
72
|
-
const apiKey = resolveEnvValue(provider.apiKey);
|
|
73
|
-
if (apiKey) candidates.keyed = { name, provider, apiKey };
|
|
74
|
-
}
|
|
75
|
-
return candidates;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
43
|
export async function handleImages(
|
|
79
44
|
req: Request,
|
|
80
45
|
config: OcxConfig,
|
|
81
46
|
endpoint: ImagesEndpoint,
|
|
82
47
|
logCtx: RequestLogContext,
|
|
83
48
|
): Promise<Response> {
|
|
49
|
+
try { validateForwardAdmissionCredential(req.headers, config); }
|
|
50
|
+
catch (err) {
|
|
51
|
+
if (err instanceof ForwardAdmissionCredentialError) return formatErrorResponse(401, "authentication_error", err.message);
|
|
52
|
+
throw err;
|
|
53
|
+
}
|
|
84
54
|
let body: unknown;
|
|
85
55
|
try {
|
|
86
56
|
body = await readJsonRequestBody(req);
|
|
@@ -90,8 +60,8 @@ export async function handleImages(
|
|
|
90
60
|
const model = (body as { model?: unknown } | null)?.model;
|
|
91
61
|
if (typeof model === "string" && model) logCtx.model = model;
|
|
92
62
|
|
|
93
|
-
const candidates =
|
|
94
|
-
if (
|
|
63
|
+
const candidates = selectOpenAiImagesProvider(config);
|
|
64
|
+
if (candidates.forwardCandidates.length === 0 && !candidates.keyed) {
|
|
95
65
|
// 400, not 5xx: codex retries every 5xx up to 5 total attempts, and this is a permanent
|
|
96
66
|
// configuration state that must surface on the first attempt.
|
|
97
67
|
return formatErrorResponse(
|
|
@@ -106,39 +76,23 @@ export async function handleImages(
|
|
|
106
76
|
// Resolve forward auth first; failures are captured, not returned, so a configured keyed
|
|
107
77
|
// provider can still serve the request (e.g. every pool account cooling down must not
|
|
108
78
|
// 429 image_gen while api.openai.com sits idle).
|
|
109
|
-
let
|
|
79
|
+
let forward: Awaited<ReturnType<typeof resolveFirstUsableOpenAiSidecar>>;
|
|
110
80
|
let forwardAuthError: Response | undefined;
|
|
111
|
-
|
|
112
|
-
if (candidates.forward) {
|
|
81
|
+
if (candidates.forwardCandidates.length > 0) {
|
|
113
82
|
try {
|
|
114
|
-
|
|
115
|
-
if (
|
|
116
|
-
forwardAuthError = formatErrorResponse(401, "authentication_error", "Selected Codex account needs reauthentication");
|
|
117
|
-
} else {
|
|
118
|
-
// Forwarded caller auth, overridden by the routed pool account's token when one is selected.
|
|
119
|
-
const authHeaders = headersForCodexAuthContext(req.headers, authCtx);
|
|
120
|
-
const bearer = authHeaders.get("authorization")?.replace(/^Bearer\s+/i, "") ?? "";
|
|
121
|
-
// A caller may authenticate to the proxy itself with `Authorization: Bearer <admission
|
|
122
|
-
// token>` (non-loopback binds); that secret must never be relayed to chatgpt.com.
|
|
123
|
-
if (bearer && isProxyAdmissionSecret(bearer, config)) authHeaders.delete("authorization");
|
|
124
|
-
// Only relay through the ChatGPT backend when there is a bearer to relay: startServer
|
|
125
|
-
// auto-upserts the `chatgpt` provider, so an unauthenticated request must not be bounced
|
|
126
|
-
// off chatgpt.com when a keyed OpenAI provider (or an honest error) serves it better.
|
|
127
|
-
if (authHeaders.get("authorization")) {
|
|
128
|
-
forwardAuthHeaders = authHeaders;
|
|
129
|
-
recordOutcome = sidecarOutcomeRecorder(config, authCtx);
|
|
130
|
-
logCtx.provider = formatCodexProviderForLog(candidates.forward.name, codexLogAccountId(authCtx), config);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
83
|
+
forward = await resolveFirstUsableOpenAiSidecar(candidates.forwardCandidates, req.headers, config);
|
|
84
|
+
if (forward) logCtx.provider = formatCodexProviderForLog(forward.providerName, codexLogAccountId(forward.authContext), config);
|
|
133
85
|
} catch (err) {
|
|
134
86
|
if (err instanceof CodexAccountCooldownError) {
|
|
135
87
|
forwardAuthError = formatErrorResponse(429, "rate_limit_error", "Selected Codex account is cooling down");
|
|
136
88
|
} else if (err instanceof CodexThreadAffinityExpiredError) {
|
|
137
89
|
forwardAuthError = formatErrorResponse(409, "invalid_request_error", "Codex thread account affinity expired; start a new session");
|
|
138
90
|
} else if (err instanceof CodexAuthContextError) {
|
|
139
|
-
const safeAccountLabel = formatCodexProviderForLog(
|
|
91
|
+
const safeAccountLabel = formatCodexProviderForLog("openai", err.accountId, config);
|
|
140
92
|
console.error(`[images] Pool account ${safeAccountLabel} token failed; reauthentication required`);
|
|
141
93
|
forwardAuthError = formatErrorResponse(401, "authentication_error", "Selected Codex account needs reauthentication");
|
|
94
|
+
} else if (err instanceof CodexPoolAuthenticationError) {
|
|
95
|
+
forwardAuthError = formatErrorResponse(401, "authentication_error", err.message);
|
|
142
96
|
} else {
|
|
143
97
|
throw err;
|
|
144
98
|
}
|
|
@@ -147,21 +101,23 @@ export async function handleImages(
|
|
|
147
101
|
|
|
148
102
|
const headers: Record<string, string> = { "content-type": "application/json" };
|
|
149
103
|
let url: string;
|
|
150
|
-
if (
|
|
151
|
-
const { provider } =
|
|
104
|
+
if (forward) {
|
|
105
|
+
const { provider } = forward;
|
|
152
106
|
if (provider.headers) Object.assign(headers, provider.headers);
|
|
153
|
-
for (const [name, value] of
|
|
107
|
+
for (const [name, value] of forward.headers) headers[name] = value;
|
|
154
108
|
// The ChatGPT codex backend takes bare paths (matches the adapter's `${baseUrl}/responses`).
|
|
155
109
|
url = `${provider.baseUrl}/images/${endpoint}`;
|
|
110
|
+
} else if (forwardAuthError) {
|
|
111
|
+
// A configured OpenAI pool mode owns its authentication failure. Do not hide a
|
|
112
|
+
// broken/expired pool behind separately billed API-key image generation.
|
|
113
|
+
return forwardAuthError;
|
|
156
114
|
} else if (candidates.keyed) {
|
|
157
|
-
const { provider, apiKey,
|
|
115
|
+
const { provider, apiKey, providerName } = candidates.keyed;
|
|
158
116
|
if (provider.headers) Object.assign(headers, provider.headers);
|
|
159
117
|
headers["authorization"] = `Bearer ${apiKey}`;
|
|
160
|
-
logCtx.provider =
|
|
118
|
+
logCtx.provider = providerName;
|
|
161
119
|
// Keyed providers tolerate baseUrl with or without /v1 (mirrors openai-responses.ts).
|
|
162
120
|
url = `${provider.baseUrl.replace(/\/v1\/?$/, "")}/v1/images/${endpoint}`;
|
|
163
|
-
} else if (forwardAuthError) {
|
|
164
|
-
return forwardAuthError;
|
|
165
121
|
} else {
|
|
166
122
|
return formatErrorResponse(
|
|
167
123
|
401,
|
|
@@ -189,7 +145,7 @@ export async function handleImages(
|
|
|
189
145
|
if (payload.byteLength > IMAGES_RESPONSE_MAX_BYTES) {
|
|
190
146
|
return formatErrorResponse(502, "upstream_error", `image ${endpoint} response too large (${payload.byteLength} bytes)`);
|
|
191
147
|
}
|
|
192
|
-
recordOutcome?.(upstreamResponse.status);
|
|
148
|
+
forward?.recordOutcome?.(upstreamResponse.status);
|
|
193
149
|
const relayHeaders: Record<string, string> = {};
|
|
194
150
|
const contentType = upstreamResponse.headers.get("content-type");
|
|
195
151
|
if (contentType) relayHeaders["content-type"] = contentType;
|
|
@@ -201,11 +157,11 @@ export async function handleImages(
|
|
|
201
157
|
return formatErrorResponse(499, "client_closed_request", `image ${endpoint} request canceled by client`);
|
|
202
158
|
}
|
|
203
159
|
if (err instanceof Error && err.name === "TimeoutError") {
|
|
204
|
-
recordOutcome?.("timeout");
|
|
160
|
+
forward?.recordOutcome?.("timeout");
|
|
205
161
|
// codex retries 5xx up to 4 more times; a retried 504 is acceptable for a transient hang.
|
|
206
162
|
return formatErrorResponse(504, "upstream_error", `image ${endpoint} upstream timed out`);
|
|
207
163
|
}
|
|
208
|
-
recordOutcome?.("connect_error");
|
|
164
|
+
forward?.recordOutcome?.("connect_error");
|
|
209
165
|
return formatErrorResponse(
|
|
210
166
|
502,
|
|
211
167
|
"upstream_error",
|
package/src/server/index.ts
CHANGED
|
@@ -16,18 +16,13 @@ import {
|
|
|
16
16
|
saveConfig,
|
|
17
17
|
websocketsEnabled,
|
|
18
18
|
} from "../config";
|
|
19
|
-
import {
|
|
20
|
-
clearLoginState, getLoginStatus, getOAuthCredentialProjectId, getValidAccessToken, isOAuthProvider,
|
|
21
|
-
listOAuthProviders, reconcileOAuthProviders, startLoginFlow, UnsupportedOAuthProviderError, upsertOAuthProvider,
|
|
22
|
-
} from "../oauth";
|
|
19
|
+
import { reconcileOAuthProviders } from "../oauth";
|
|
23
20
|
import { invalidateCodexModelsCache } from "../codex/catalog";
|
|
21
|
+
import { runOpenAiTierStartupMigration } from "../providers/openai-tier-startup";
|
|
22
|
+
import { isCanonicalOpenAiForwardProvider } from "../providers/openai-tiers";
|
|
23
|
+
import { providerCodexAccountMode } from "../providers/registry";
|
|
24
24
|
import {
|
|
25
25
|
CodexAccountCooldownError,
|
|
26
|
-
CodexAuthContextError,
|
|
27
|
-
CodexThreadAffinityExpiredError,
|
|
28
|
-
headersForCodexAuthContext,
|
|
29
|
-
resolveCodexAuthContext,
|
|
30
|
-
type CodexAuthContext,
|
|
31
26
|
} from "../codex/auth-context";
|
|
32
27
|
export {
|
|
33
28
|
clearThreadAccountMap,
|
|
@@ -102,6 +97,7 @@ import {
|
|
|
102
97
|
isLoopbackHostname,
|
|
103
98
|
jsonResponse,
|
|
104
99
|
requireApiAuth,
|
|
100
|
+
requireResponsesApiAuth,
|
|
105
101
|
safeConfigDTO,
|
|
106
102
|
setCorsOrigin,
|
|
107
103
|
withCors,
|
|
@@ -147,17 +143,12 @@ const WEBSOCKET_IDLE_TIMEOUT_SECONDS = 0;
|
|
|
147
143
|
// export function relaySseWithHeartbeat
|
|
148
144
|
|
|
149
145
|
export function startServer(port?: number) {
|
|
150
|
-
const config = loadConfig();
|
|
146
|
+
const config = runOpenAiTierStartupMigration(loadConfig());
|
|
151
147
|
applyProxyEnv(config);
|
|
152
148
|
assertServerAuthConfig(config);
|
|
153
149
|
// Refresh OAuth provider presets (models/noReasoningModels) from the registry so a proxy update
|
|
154
150
|
// adding/dropping models reaches existing configs on start — not just fresh installs.
|
|
155
151
|
reconcileOAuthProviders(config);
|
|
156
|
-
// Ensure the ChatGPT passthrough provider exists so gpt-* models route correctly.
|
|
157
|
-
if (!config.providers["chatgpt"]) {
|
|
158
|
-
upsertOAuthProvider(config, "chatgpt");
|
|
159
|
-
saveConfig(config);
|
|
160
|
-
}
|
|
161
152
|
// Seed default featured subagent models on first run only (UNSET → defaults). A user-set list,
|
|
162
153
|
// even [], is left alone so GUI removals persist.
|
|
163
154
|
if (config.subagentModels === undefined) {
|
|
@@ -214,7 +205,7 @@ export function startServer(port?: number) {
|
|
|
214
205
|
if (isDraining()) {
|
|
215
206
|
return new Response("Service shutting down", { status: 503, headers: { ...corsHeaders(req, config), "Retry-After": "5" } });
|
|
216
207
|
}
|
|
217
|
-
const apiAuthError =
|
|
208
|
+
const apiAuthError = requireResponsesApiAuth(req, config);
|
|
218
209
|
if (apiAuthError) return withCors(apiAuthError, req, config);
|
|
219
210
|
if (!isAllowedRequestOrigin(req, config)) {
|
|
220
211
|
return withCors(formatErrorResponse(403, "origin_rejected", "WebSocket upgrade blocked: non-local Origin"), req, config);
|
|
@@ -227,27 +218,9 @@ export function startServer(port?: number) {
|
|
|
227
218
|
if (!websocketsEnabled(config)) {
|
|
228
219
|
return withCors(formatErrorResponse(426, "upgrade_required", "Responses WebSocket transport is disabled; use HTTP"), req, config);
|
|
229
220
|
}
|
|
230
|
-
let authCtx: CodexAuthContext;
|
|
231
|
-
try {
|
|
232
|
-
authCtx = await resolveCodexAuthContext(req.headers, config);
|
|
233
|
-
} catch (err) {
|
|
234
|
-
if (err instanceof CodexAccountCooldownError) {
|
|
235
|
-
return withCors(formatErrorResponse(429, "rate_limit_error", "Selected Codex account is cooling down"), req, config);
|
|
236
|
-
}
|
|
237
|
-
if (err instanceof CodexThreadAffinityExpiredError) {
|
|
238
|
-
return withCors(formatErrorResponse(409, "invalid_request_error", "Codex thread account affinity expired; start a new session"), req, config);
|
|
239
|
-
}
|
|
240
|
-
if (err instanceof CodexAuthContextError) {
|
|
241
|
-
const safeAccountLabel = formatCodexProviderForLog("chatgpt", err.accountId, config);
|
|
242
|
-
console.error(`[codex-auth] Pool account ${safeAccountLabel} token failed during websocket upgrade; reauthentication required`);
|
|
243
|
-
return withCors(formatErrorResponse(401, "authentication_error", "Selected Codex account needs reauthentication"), req, config);
|
|
244
|
-
}
|
|
245
|
-
throw err;
|
|
246
|
-
}
|
|
247
221
|
if (server.upgrade(req, {
|
|
248
222
|
data: {
|
|
249
223
|
headers: selectForwardHeaders(req.headers),
|
|
250
|
-
authContext: authCtx,
|
|
251
224
|
},
|
|
252
225
|
})) return undefined as unknown as Response;
|
|
253
226
|
return withCors(formatErrorResponse(426, "upgrade_required", "WebSocket upgrade failed"), req, config);
|
|
@@ -272,7 +245,7 @@ export function startServer(port?: number) {
|
|
|
272
245
|
return withCors(formatErrorResponse(403, "origin_rejected", "cross-origin data-plane request blocked"), req, config);
|
|
273
246
|
}
|
|
274
247
|
const goModels = await fetchAllModels(config);
|
|
275
|
-
const { applyNativeVisibility, buildCatalogEntries, disabledNativeSlugs, loadCatalogTemplate, nativeOpenAiSlugs, orderForSubagents, filterCatalogVisibleModels, visibleNativeSlugs } = await import("../codex/catalog");
|
|
248
|
+
const { applyNativeVisibility, buildCatalogEntries, disabledNativeSlugs, exactComboCatalogSlugs, loadCatalogTemplate, nativeOpenAiSlugs, orderForSubagents, filterCatalogVisibleModels, visibleNativeSlugs } = await import("../codex/catalog");
|
|
276
249
|
const nativeSlugs = nativeOpenAiSlugs();
|
|
277
250
|
const goEnabled = filterCatalogVisibleModels(goModels, config);
|
|
278
251
|
const goOrdered = orderForSubagents(goEnabled, config.subagentModels);
|
|
@@ -315,7 +288,7 @@ export function startServer(port?: number) {
|
|
|
315
288
|
// Disabled natives stay in the catalog shape with visibility "hide" (mirrors the
|
|
316
289
|
// on-disk sync; codex-rs keeps them out of the picker itself).
|
|
317
290
|
const maMode = config.multiAgentMode === "v1" || config.multiAgentMode === "v2" ? config.multiAgentMode : "default";
|
|
318
|
-
const entries = buildCatalogEntries(loadCatalogTemplate(), nativeSlugs, goOrdered, config.subagentModels, websocketsEnabled(config), maMode as "v1" | "default" | "v2");
|
|
291
|
+
const entries = buildCatalogEntries(loadCatalogTemplate(), nativeSlugs, goOrdered, config.subagentModels, websocketsEnabled(config), maMode as "v1" | "default" | "v2", exactComboCatalogSlugs(config));
|
|
319
292
|
return jsonResponse({ models: applyNativeVisibility(entries, disabledNativeSlugs(config)) }, 200, req, config);
|
|
320
293
|
}
|
|
321
294
|
// OpenAI list shape: native gpt bare + routed models namespaced "<provider>/<id>"
|
|
@@ -334,12 +307,28 @@ export function startServer(port?: number) {
|
|
|
334
307
|
if (isDraining()) {
|
|
335
308
|
return new Response("Service shutting down", { status: 503, headers: { ...corsHeaders(req, config), "Retry-After": "5" } });
|
|
336
309
|
}
|
|
337
|
-
const apiAuthError =
|
|
310
|
+
const apiAuthError = requireResponsesApiAuth(req, config);
|
|
338
311
|
if (apiAuthError) return withCors(apiAuthError, req, config);
|
|
339
312
|
if (!isAllowedRequestOrigin(req, config)) {
|
|
340
313
|
return withCors(formatErrorResponse(403, "origin_rejected", "cross-origin data-plane request blocked"), req, config);
|
|
341
314
|
}
|
|
342
|
-
|
|
315
|
+
const start = Date.now();
|
|
316
|
+
const requestId = nextRequestLogId(start);
|
|
317
|
+
const logCtx: RequestLogContext = { model: "unknown", provider: "unknown" };
|
|
318
|
+
let response: Response;
|
|
319
|
+
try {
|
|
320
|
+
response = await handleResponsesCompact(req, config, logCtx);
|
|
321
|
+
} catch {
|
|
322
|
+
response = formatErrorResponse(500, "server_error", "Unexpected compact request failure");
|
|
323
|
+
}
|
|
324
|
+
addFinalRequestLog(
|
|
325
|
+
requestId,
|
|
326
|
+
start,
|
|
327
|
+
logCtx,
|
|
328
|
+
response.status,
|
|
329
|
+
response.status === 499 ? { closeReason: "client_cancel" } : undefined,
|
|
330
|
+
);
|
|
331
|
+
return withCors(response, req, config);
|
|
343
332
|
}
|
|
344
333
|
|
|
345
334
|
if (
|
|
@@ -399,7 +388,7 @@ export function startServer(port?: number) {
|
|
|
399
388
|
if (isDraining()) {
|
|
400
389
|
return new Response("Service shutting down", { status: 503, headers: { ...corsHeaders(req, config), "Retry-After": "5" } });
|
|
401
390
|
}
|
|
402
|
-
const apiAuthError =
|
|
391
|
+
const apiAuthError = requireResponsesApiAuth(req, config);
|
|
403
392
|
if (apiAuthError) return withCors(apiAuthError, req, config);
|
|
404
393
|
if (!isAllowedRequestOrigin(req, config)) {
|
|
405
394
|
return withCors(formatErrorResponse(403, "origin_rejected", "cross-origin data-plane request blocked"), req, config);
|
|
@@ -521,6 +510,10 @@ export function startServer(port?: number) {
|
|
|
521
510
|
turnAbort.abort("websocket turn superseded or closed");
|
|
522
511
|
};
|
|
523
512
|
ws.data.cancel = cancelTurn;
|
|
513
|
+
// A socket may carry several response.create frames. Clear the previous
|
|
514
|
+
// account before resolving this frame so a failed Multi resolution cannot
|
|
515
|
+
// leave stale invalidation ownership behind.
|
|
516
|
+
updateCodexWebSocketAuthContext(ws, undefined);
|
|
524
517
|
|
|
525
518
|
if (frame.generate === false) {
|
|
526
519
|
for (const payload of buildWarmupCompletionFrames(frame)) {
|
|
@@ -548,49 +541,8 @@ export function startServer(port?: number) {
|
|
|
548
541
|
addFinalRequestLog(requestId, start, logCtx, status, meta);
|
|
549
542
|
};
|
|
550
543
|
const baseHeaders = ws.data.headers ?? new Headers();
|
|
551
|
-
let authCtx: CodexAuthContext;
|
|
552
|
-
let selectedForwardHeaders: Headers;
|
|
553
|
-
try {
|
|
554
|
-
authCtx = await resolveCodexAuthContext(baseHeaders, config);
|
|
555
|
-
selectedForwardHeaders = headersForCodexAuthContext(baseHeaders, authCtx);
|
|
556
|
-
updateCodexWebSocketAuthContext(ws, authCtx);
|
|
557
|
-
} catch (err) {
|
|
558
|
-
if (!isCurrent()) return;
|
|
559
|
-
if (err instanceof CodexAccountCooldownError) {
|
|
560
|
-
finalizeLog(429);
|
|
561
|
-
sendJsonFrame(ws, buildWsErrorFrame(429, {
|
|
562
|
-
type: "rate_limit_error",
|
|
563
|
-
message: "Selected Codex account is cooling down",
|
|
564
|
-
}));
|
|
565
|
-
return;
|
|
566
|
-
}
|
|
567
|
-
if (err instanceof CodexThreadAffinityExpiredError) {
|
|
568
|
-
finalizeLog(409);
|
|
569
|
-
sendJsonFrame(ws, buildWsErrorFrame(409, {
|
|
570
|
-
type: "invalid_request_error",
|
|
571
|
-
message: "Codex thread account affinity expired; start a new session",
|
|
572
|
-
}));
|
|
573
|
-
return;
|
|
574
|
-
}
|
|
575
|
-
if (err instanceof CodexAuthContextError) {
|
|
576
|
-
const safeAccountLabel = formatCodexProviderForLog("chatgpt", err.accountId, config);
|
|
577
|
-
console.error(`[codex-auth] Pool account ${safeAccountLabel} token failed during websocket turn; reauthentication required`);
|
|
578
|
-
finalizeLog(401);
|
|
579
|
-
sendJsonFrame(ws, buildWsErrorFrame(401, {
|
|
580
|
-
type: "authentication_error",
|
|
581
|
-
message: "Selected Codex account needs reauthentication",
|
|
582
|
-
}));
|
|
583
|
-
return;
|
|
584
|
-
}
|
|
585
|
-
finalizeLog(502);
|
|
586
|
-
sendJsonFrame(ws, buildWsErrorFrame(502, {
|
|
587
|
-
type: "proxy_error",
|
|
588
|
-
message: err instanceof Error ? err.message : String(err),
|
|
589
|
-
}));
|
|
590
|
-
return;
|
|
591
|
-
}
|
|
592
544
|
const fwd = new Headers({ "content-type": "application/json" });
|
|
593
|
-
|
|
545
|
+
baseHeaders.forEach((value, key) => fwd.set(key, value));
|
|
594
546
|
const req = new Request("http://localhost/v1/responses", {
|
|
595
547
|
method: "POST",
|
|
596
548
|
headers: fwd,
|
|
@@ -601,8 +553,7 @@ export function startServer(port?: number) {
|
|
|
601
553
|
const response = await handleResponses(req, config, logCtx, {
|
|
602
554
|
forceEmptyResponseId: true,
|
|
603
555
|
abortSignal: turnAbort.signal,
|
|
604
|
-
|
|
605
|
-
selectedForwardHeaders,
|
|
556
|
+
onCodexAuthContextResolved: context => updateCodexWebSocketAuthContext(ws, context),
|
|
606
557
|
recordTerminalOutcomes: false,
|
|
607
558
|
setTerminalOutcomeRecorder: recorder => {
|
|
608
559
|
terminalRecorder = recorder;
|
|
@@ -666,9 +617,17 @@ export function startServer(port?: number) {
|
|
|
666
617
|
// usage scores from the first routing decision, even when the dashboard is
|
|
667
618
|
// never opened (the common CLI/WSL case). Fire-and-forget: never blocks the
|
|
668
619
|
// listener, and a blocked network silently no-ops (see Phase 30 diagnostics).
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
620
|
+
const openAiProvider = config.providers.openai;
|
|
621
|
+
if (
|
|
622
|
+
openAiProvider
|
|
623
|
+
&& openAiProvider.disabled !== true
|
|
624
|
+
&& isCanonicalOpenAiForwardProvider(openAiProvider)
|
|
625
|
+
&& providerCodexAccountMode("openai", openAiProvider) === "pool"
|
|
626
|
+
) {
|
|
627
|
+
import("../codex/auth-api")
|
|
628
|
+
.then(({ primeCodexPoolQuotas }) => primeCodexPoolQuotas(config, "startup"))
|
|
629
|
+
.catch(() => {});
|
|
630
|
+
}
|
|
672
631
|
|
|
673
632
|
return server;
|
|
674
633
|
}
|