@bitkyc08/opencodex 2.8.0 → 2.9.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/README.md +24 -0
- package/bin/ocx.mjs +32 -4
- package/gui/dist/assets/index-CHwf3tTD.css +1 -0
- package/gui/dist/assets/index-u5eFOv2y.js +67 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic-image-normalize.ts +114 -20
- package/src/adapters/anthropic.ts +126 -10
- package/src/adapters/azure.ts +3 -3
- package/src/adapters/base.ts +7 -3
- package/src/adapters/cursor/discovery.ts +14 -4
- package/src/adapters/cursor/effort-map.ts +18 -6
- package/src/adapters/cursor/framing.ts +102 -27
- package/src/adapters/cursor/kv-store.ts +30 -3
- package/src/adapters/cursor/live-models.ts +22 -2
- package/src/adapters/cursor/live-transport.ts +245 -49
- package/src/adapters/cursor/mcp-manager.ts +105 -8
- package/src/adapters/cursor/native-exec-mcp.ts +5 -3
- package/src/adapters/cursor/native-exec-shell.ts +296 -14
- package/src/adapters/cursor/native-exec.ts +381 -33
- package/src/adapters/cursor/protobuf-events.ts +28 -1
- package/src/adapters/cursor/protobuf-request.ts +71 -39
- package/src/adapters/cursor/request-builder.ts +2 -2
- package/src/adapters/cursor/transport.ts +2 -0
- package/src/adapters/cursor.ts +13 -2
- package/src/adapters/google-antigravity-replay.ts +184 -17
- package/src/adapters/google.ts +58 -8
- package/src/adapters/kiro-thinking.ts +23 -9
- package/src/adapters/kiro-tools.ts +49 -18
- package/src/adapters/kiro.ts +377 -133
- package/src/adapters/mimo-free.ts +36 -4
- package/src/adapters/openai-chat.ts +143 -17
- package/src/adapters/openai-responses.ts +130 -14
- package/src/adapters/run-turn-queue.ts +7 -1
- package/src/bridge.ts +466 -69
- package/src/chat/outbound.ts +144 -38
- package/src/claude/inbound-debug.ts +53 -8
- package/src/claude/outbound.ts +224 -38
- package/src/cli/agent-driven.ts +34 -1
- package/src/cli/catalog-prewarm.ts +5 -2
- package/src/cli/claude-desktop.ts +2 -2
- package/src/cli/doctor.ts +12 -0
- package/src/cli/export-command.ts +187 -0
- package/src/cli/help.ts +11 -0
- package/src/cli/index.ts +13 -3
- package/src/cli/init.ts +129 -102
- package/src/cli/opencode.ts +36 -151
- package/src/cli/star-prompt.ts +13 -4
- package/src/cli/status-oauth.ts +12 -2
- package/src/clients/config-export.ts +377 -0
- package/src/codex/account-runtime-state.ts +19 -1
- package/src/codex/account-store.ts +162 -82
- package/src/codex/auth-api.ts +467 -159
- package/src/codex/auth-context.ts +15 -2
- package/src/codex/catalog/aggregation.ts +15 -0
- package/src/codex/catalog/effort.ts +16 -6
- package/src/codex/catalog/metadata.ts +6 -0
- package/src/codex/catalog/parsing.ts +3 -1
- package/src/codex/catalog/provider-fetch.ts +29 -0
- package/src/codex/catalog/sync.ts +64 -7
- package/src/codex/catalog.ts +2 -2
- package/src/codex/inject.ts +5 -5
- package/src/codex/main-account-cache.ts +8 -1
- package/src/codex/model-cache.ts +81 -2
- package/src/codex/pool-rotation.ts +39 -0
- package/src/codex/project-config-warnings.ts +12 -1
- package/src/codex/quota.ts +35 -3
- package/src/codex/routing.ts +46 -1
- package/src/codex/shim.ts +10 -4
- package/src/codex/subagent-model-fallback.ts +12 -0
- package/src/codex/websocket-registry.ts +27 -0
- package/src/combos/failover.ts +31 -1
- package/src/combos/request.ts +9 -0
- package/src/combos/resolve.ts +60 -4
- package/src/combos/types.ts +12 -0
- package/src/config.ts +510 -55
- package/src/github/star-state.ts +13 -1
- package/src/images/fulfill.ts +39 -1
- package/src/images/loop.ts +52 -12
- package/src/lib/admission.ts +83 -0
- package/src/lib/app-owned-memory-stores.ts +173 -0
- package/src/lib/app-owned-memory.ts +265 -0
- package/src/lib/bun-stream-caps.ts +31 -7
- package/src/lib/config-ownership.ts +33 -0
- package/src/lib/crash-guard.ts +65 -5
- package/src/lib/debug-log-buffer.ts +47 -6
- package/src/lib/destination-policy.ts +12 -1
- package/src/lib/errors.ts +3 -0
- package/src/lib/gcp-adc.ts +40 -2
- package/src/lib/injection-debug-log.ts +26 -2
- package/src/lib/provider-outbound.ts +3 -0
- package/src/lib/sidecar-tracker.ts +5 -2
- package/src/lib/sse-decoder.ts +257 -37
- package/src/lib/state-store-registrations.ts +109 -0
- package/src/lib/state-store-sweeper.ts +184 -0
- package/src/lib/translator-budget.ts +356 -0
- package/src/lib/windows-secret-acl.ts +33 -12
- package/src/lib/winsw.ts +14 -1
- package/src/oauth/anthropic-routing.ts +31 -7
- package/src/oauth/google-antigravity.ts +2 -1
- package/src/oauth/health.ts +30 -12
- package/src/oauth/index.ts +127 -23
- package/src/oauth/kiro-credentials.ts +72 -1
- package/src/oauth/kiro.ts +23 -4
- package/src/oauth/store.ts +165 -18
- package/src/oauth/token-guardian.ts +43 -4
- package/src/oauth/types.ts +2 -1
- package/src/providers/base-url-choices.ts +10 -0
- package/src/providers/derive.ts +12 -0
- package/src/providers/free-directory.ts +4 -1
- package/src/providers/key-failover.ts +12 -0
- package/src/providers/openai-sidecar.ts +4 -1
- package/src/providers/quota.ts +68 -7
- package/src/providers/registry.ts +279 -3
- package/src/responses/parser.ts +5 -1
- package/src/responses/spill-store.ts +394 -0
- package/src/responses/state.ts +520 -102
- package/src/router.ts +18 -1
- package/src/server/adapter-resolve.ts +20 -3
- package/src/server/auth-cors.ts +121 -28
- package/src/server/chat-completions.ts +57 -12
- package/src/server/claude-messages.ts +85 -13
- package/src/server/index.ts +242 -100
- package/src/server/lifecycle.ts +155 -25
- package/src/server/management/agent-settings-routes.ts +79 -36
- package/src/server/management/api-key-usage.ts +167 -0
- package/src/server/management/body.ts +35 -0
- package/src/server/management/combo-routes.ts +5 -1
- package/src/server/management/config-routes.ts +42 -12
- package/src/server/management/logs-usage-routes.ts +41 -21
- package/src/server/management/model-routes.ts +188 -54
- package/src/server/management/oauth-account-routes.ts +115 -26
- package/src/server/management/provider-routes.ts +56 -6
- package/src/server/management/shared.ts +16 -3
- package/src/server/management/sidebar-routes.ts +50 -1
- package/src/server/management/system-restart.ts +13 -6
- package/src/server/management/system-routes.ts +15 -3
- package/src/server/management/usage-summary-cache.ts +86 -0
- package/src/server/management-api.ts +39 -5
- package/src/server/management-auth.ts +65 -14
- package/src/server/port-reclaim.ts +58 -12
- package/src/server/ports.ts +2 -0
- package/src/server/proxy-liveness.ts +60 -14
- package/src/server/relay-eager.ts +20 -4
- package/src/server/relay.ts +548 -154
- package/src/server/request-decompress.ts +51 -4
- package/src/server/request-log.ts +134 -15
- package/src/server/responses/collaboration.ts +15 -4
- package/src/server/responses/compact.ts +3 -0
- package/src/server/responses/core.ts +241 -66
- package/src/server/responses-image-gen-repair.ts +19 -5
- package/src/server/responses-item-id-repair.ts +23 -5
- package/src/server/sse-payload-rewrite.ts +71 -12
- package/src/server/startup-health-cache.ts +14 -1
- package/src/server/system-env.ts +8 -1
- package/src/server/windows-tcp-drop.ts +15 -5
- package/src/server/ws-bridge.ts +25 -0
- package/src/service.ts +179 -13
- package/src/storage/policy-job.ts +93 -23
- package/src/storage/policy-worker.ts +6 -0
- package/src/storage/restore-job.ts +62 -16
- package/src/storage/restore-worker.ts +6 -0
- package/src/storage/storage-mutation-coordinator.ts +36 -6
- package/src/storage/worker-lifecycle.ts +181 -47
- package/src/tray/windows.ts +97 -25
- package/src/types.ts +39 -6
- package/src/update/index.ts +24 -5
- package/src/update/job.ts +598 -73
- package/src/usage/log.ts +115 -17
- package/src/usage/summary.ts +67 -2
- package/src/vision/index.ts +112 -22
- package/src/web-search/loop.ts +38 -6
- package/src/web-search/progress-stream.ts +14 -3
- package/gui/dist/assets/index-BDjpkcRN.js +0 -67
- package/gui/dist/assets/index-BHsKRFh9.css +0 -1
package/src/server/index.ts
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
buildWsErrorFrame,
|
|
5
5
|
selectForwardHeaders,
|
|
6
6
|
sendJsonFrame,
|
|
7
|
+
buildResponsesWsData,
|
|
7
8
|
sendResponseToWebSocket,
|
|
8
9
|
sendTextFrame,
|
|
9
10
|
type WsData,
|
|
@@ -20,6 +21,21 @@ import {
|
|
|
20
21
|
import { reconcileOAuthProviders } from "../oauth";
|
|
21
22
|
import { invalidateCodexModelsCache } from "../codex/catalog";
|
|
22
23
|
import { startMemoryWatchdog } from "./memory-watchdog";
|
|
24
|
+
import {
|
|
25
|
+
reconcileLiveStateStores,
|
|
26
|
+
setLiveStateStoreConfig,
|
|
27
|
+
} from "../lib/state-store-registrations";
|
|
28
|
+
import { startStateStoreSweeper } from "../lib/state-store-sweeper";
|
|
29
|
+
import {
|
|
30
|
+
configureAppOwnedMemoryBudget,
|
|
31
|
+
enforceAppOwnedMemoryBudget,
|
|
32
|
+
resolveAppOwnedMemoryBudgetBytes,
|
|
33
|
+
} from "../lib/app-owned-memory";
|
|
34
|
+
import {
|
|
35
|
+
registerAppOwnedMemorySweepFallback,
|
|
36
|
+
registerDefaultAppOwnedMemoryStores,
|
|
37
|
+
registerDefaultAppOwnedObservedBuffers,
|
|
38
|
+
} from "../lib/app-owned-memory-stores";
|
|
23
39
|
import { setStorageCleanupPolicyLiveSink } from "../storage/policy";
|
|
24
40
|
import { setStorageCleanupPolicyJobLiveApply } from "../storage/policy-job";
|
|
25
41
|
import { scheduleStorageCleanupStartupRun, startStorageCleanupScheduler } from "../storage/policy-scheduler";
|
|
@@ -38,7 +54,8 @@ export {
|
|
|
38
54
|
resolveCodexAccountForThread,
|
|
39
55
|
} from "../codex/routing";
|
|
40
56
|
import { formatCodexProviderForLog } from "../codex/routing";
|
|
41
|
-
import {
|
|
57
|
+
import { CatalogGatherBusyError } from "../codex/catalog/provider-fetch";
|
|
58
|
+
import { registerCodexWebSocket, tryReserveCodexWebSocket, unregisterCodexWebSocket, updateCodexWebSocketAuthContext } from "../codex/websocket-registry";
|
|
42
59
|
import { resolveGuiFilePath, rootFallbackPayload, serveGuiFile } from "./gui-static";
|
|
43
60
|
export { resolveGuiFilePath, rootFallbackPayload } from "./gui-static";
|
|
44
61
|
export { resolveAdapter } from "./adapter-resolve";
|
|
@@ -50,7 +67,9 @@ import {
|
|
|
50
67
|
registerTurn,
|
|
51
68
|
setServerRef,
|
|
52
69
|
trackStreamLifetime,
|
|
70
|
+
tryAdmitTurn,
|
|
53
71
|
unregisterTurn,
|
|
72
|
+
type ActiveTurnLease,
|
|
54
73
|
} from "./lifecycle";
|
|
55
74
|
export {
|
|
56
75
|
drainAndShutdown,
|
|
@@ -105,14 +124,14 @@ import {
|
|
|
105
124
|
assertServerAuthConfig,
|
|
106
125
|
corsHeaders,
|
|
107
126
|
managementCorsHeaders,
|
|
108
|
-
hasValidApiAuth,
|
|
109
127
|
isAllowedRequestOrigin,
|
|
110
128
|
isAllowedManagementOrigin,
|
|
111
129
|
isApiAuthRequired,
|
|
112
130
|
isLoopbackHostname,
|
|
113
131
|
jsonResponse,
|
|
114
|
-
|
|
115
|
-
|
|
132
|
+
admissionFields,
|
|
133
|
+
resolveApiAuth,
|
|
134
|
+
resolveResponsesApiAuth,
|
|
116
135
|
safeConfigDTO,
|
|
117
136
|
setCorsOrigin,
|
|
118
137
|
withCors,
|
|
@@ -231,8 +250,10 @@ function attachLiveSidebandUpstream(ws: ServerWebSocket<WsData>): void {
|
|
|
231
250
|
// if (isEventStream && upstreamResponse.body) {
|
|
232
251
|
// const repairConfig = route.provider.responsesItemIdRepair;
|
|
233
252
|
// const needsClientRewrite = imageGenCallAliases.size > 0
|
|
234
|
-
// #314 gated shape
|
|
235
|
-
//
|
|
253
|
+
// #314 gated shape: win32 no-rewrite traffic follows runtime/config policy; darwin no-rewrite
|
|
254
|
+
// traffic requires explicit config-eager opt-in (`auto` always stays tee on darwin). Default OFF
|
|
255
|
+
// on the bundled known-bad runtime; policy lives in 260731_macos_rss_retention phase 100.
|
|
256
|
+
// selectEagerPath(process.platform, needsClientRewrite, config.streamMode ?? "auto")
|
|
236
257
|
// relaySseEagerBounded(upstreamResponse.body, turnAc,
|
|
237
258
|
// new Response(eagerBody,
|
|
238
259
|
// Default shape (tee + background inspection):
|
|
@@ -252,12 +273,14 @@ function attachLiveSidebandUpstream(ws: ServerWebSocket<WsData>): void {
|
|
|
252
273
|
|
|
253
274
|
export function startServer(port?: number) {
|
|
254
275
|
const config = runAlibabaRegionStartupMigration(runOpenAiTierStartupMigration(loadConfig()));
|
|
276
|
+
setLiveStateStoreConfig(config);
|
|
255
277
|
applyProxyEnv(config);
|
|
256
278
|
assertServerAuthConfig(config);
|
|
257
279
|
const managementAuth = initializeManagementAuthState(config);
|
|
258
280
|
// Refresh OAuth provider presets (models/noReasoningModels) from the registry so a proxy update
|
|
259
281
|
// adding/dropping models reaches existing configs on start — not just fresh installs.
|
|
260
282
|
reconcileOAuthProviders(config);
|
|
283
|
+
reconcileLiveStateStores();
|
|
261
284
|
// Seed default featured subagent models on first run only (UNSET → defaults). A user-set list,
|
|
262
285
|
// even [], is left alone so GUI removals persist.
|
|
263
286
|
if (config.subagentModels === undefined) {
|
|
@@ -301,6 +324,12 @@ export function startServer(port?: number) {
|
|
|
301
324
|
// #314: warn-only RSS observability (unref'd, idempotent — safe under repeated
|
|
302
325
|
// startServer(0) in tests). Snapshot surfaces via GET /api/system/memory.
|
|
303
326
|
startMemoryWatchdog();
|
|
327
|
+
registerDefaultAppOwnedMemoryStores();
|
|
328
|
+
registerDefaultAppOwnedObservedBuffers();
|
|
329
|
+
registerAppOwnedMemorySweepFallback();
|
|
330
|
+
configureAppOwnedMemoryBudget(resolveAppOwnedMemoryBudgetBytes(config.appOwnedMemoryBudgetMb));
|
|
331
|
+
enforceAppOwnedMemoryBudget();
|
|
332
|
+
startStateStoreSweeper();
|
|
304
333
|
// Issue #42 Phase 3: opt-in archived auto-cleanup (default OFF). Unref'd hourly
|
|
305
334
|
// tick for daily/weekly; startup evaluation is fire-and-forget after listen.
|
|
306
335
|
// Heavy work runs in a Worker via the single-flight job controller.
|
|
@@ -334,6 +363,31 @@ export function startServer(port?: number) {
|
|
|
334
363
|
return new Response(response.body, { status: 503, headers });
|
|
335
364
|
}
|
|
336
365
|
|
|
366
|
+
function serverBusyResponse(req: Request, resource: string): Response {
|
|
367
|
+
return withCors(new Response(JSON.stringify({
|
|
368
|
+
error: { type: "server_error", code: "server_busy", message: `${resource} capacity reached` },
|
|
369
|
+
}), {
|
|
370
|
+
status: 503,
|
|
371
|
+
headers: { "Content-Type": "application/json", "Retry-After": "1" },
|
|
372
|
+
}), req, config);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
async function runAdmittedHttpTurn(req: Request, work: (lease: ActiveTurnLease) => Promise<Response>): Promise<Response> {
|
|
376
|
+
const lease = tryAdmitTurn();
|
|
377
|
+
if (!lease) return serverBusyResponse(req, "active turns");
|
|
378
|
+
let response: Response;
|
|
379
|
+
try {
|
|
380
|
+
response = await work(lease);
|
|
381
|
+
} catch (error) {
|
|
382
|
+
lease.release();
|
|
383
|
+
throw error;
|
|
384
|
+
}
|
|
385
|
+
if (!lease.isTransferred()) {
|
|
386
|
+
lease.release();
|
|
387
|
+
}
|
|
388
|
+
return response;
|
|
389
|
+
}
|
|
390
|
+
|
|
337
391
|
const server: Server<WsData> = Bun.serve<WsData>({
|
|
338
392
|
port: listenPort,
|
|
339
393
|
hostname: bindHost,
|
|
@@ -362,8 +416,10 @@ export function startServer(port?: number) {
|
|
|
362
416
|
if (isDraining()) {
|
|
363
417
|
return drainingResponse(req);
|
|
364
418
|
}
|
|
365
|
-
const
|
|
366
|
-
if (
|
|
419
|
+
const admission = resolveResponsesApiAuth(req, config);
|
|
420
|
+
if (!admission) {
|
|
421
|
+
return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req, config);
|
|
422
|
+
}
|
|
367
423
|
if (!isAllowedRequestOrigin(req, config)) {
|
|
368
424
|
return withCors(formatErrorResponse(403, "origin_rejected", "WebSocket upgrade blocked: non-local Origin"), req, config);
|
|
369
425
|
}
|
|
@@ -375,11 +431,12 @@ export function startServer(port?: number) {
|
|
|
375
431
|
if (!websocketsEnabled(config)) {
|
|
376
432
|
return withCors(formatErrorResponse(426, "upgrade_required", "Responses WebSocket transport is disabled; use HTTP"), req, config);
|
|
377
433
|
}
|
|
434
|
+
const websocketLease = tryReserveCodexWebSocket();
|
|
435
|
+
if (!websocketLease) return serverBusyResponse(req, "Codex WebSockets");
|
|
378
436
|
if (server.upgrade(req, {
|
|
379
|
-
data:
|
|
380
|
-
headers: selectForwardHeaders(req.headers),
|
|
381
|
-
},
|
|
437
|
+
data: buildResponsesWsData(selectForwardHeaders(req.headers), admission, websocketLease),
|
|
382
438
|
})) return undefined as unknown as Response;
|
|
439
|
+
websocketLease.release();
|
|
383
440
|
return withCors(formatErrorResponse(426, "upgrade_required", "WebSocket upgrade failed"), req, config);
|
|
384
441
|
}
|
|
385
442
|
|
|
@@ -400,13 +457,24 @@ export function startServer(port?: number) {
|
|
|
400
457
|
// Model discovery never forwards Authorization upstream, so the broader admission
|
|
401
458
|
// set (Authorization / x-api-key / x-opencodex-api-key) is safe here and required by
|
|
402
459
|
// remote OpenAI-style bearer clients and Claude gateway discovery (anthropic-version).
|
|
403
|
-
const
|
|
404
|
-
if (
|
|
460
|
+
const admission = resolveApiAuth(req, config);
|
|
461
|
+
if (!admission) return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req, config);
|
|
405
462
|
if (!isAllowedRequestOrigin(req, config)) {
|
|
406
463
|
return withCors(formatErrorResponse(403, "origin_rejected", "cross-origin data-plane request blocked"), req, config);
|
|
407
464
|
}
|
|
408
|
-
|
|
409
|
-
|
|
465
|
+
let goModels;
|
|
466
|
+
try {
|
|
467
|
+
goModels = await fetchAllModels(config);
|
|
468
|
+
} catch (error) {
|
|
469
|
+
if (error instanceof CatalogGatherBusyError) {
|
|
470
|
+
return withCors(new Response(JSON.stringify({ error: { type: "server_error", code: "catalog_busy", message: error.message } }), {
|
|
471
|
+
status: 503,
|
|
472
|
+
headers: { "content-type": "application/json", "Retry-After": "1" },
|
|
473
|
+
}), req, config);
|
|
474
|
+
}
|
|
475
|
+
throw error;
|
|
476
|
+
}
|
|
477
|
+
const { applyNativeVisibility, buildCatalogEntries, disabledNativeSlugs, exactComboCatalogSlugs, loadCatalogTemplate, nativeOpenAiSlugs, orderForSubagents, filterCatalogVisibleModels, uniqueCatalogModelsForRawPublicList, visibleNativeSlugs, desktopVisibleNativeSlugs } = await import("../codex/catalog");
|
|
410
478
|
const nativeSlugs = nativeOpenAiSlugs();
|
|
411
479
|
const goEnabled = filterCatalogVisibleModels(goModels, config);
|
|
412
480
|
const goOrdered = orderForSubagents(goEnabled, config.subagentModels);
|
|
@@ -424,7 +492,7 @@ export function startServer(port?: number) {
|
|
|
424
492
|
if (config.claudeCode?.enabled === false) return jsonResponse({ data: [] }, 200, req, config);
|
|
425
493
|
// Build Desktop 3P registry so inbound alias resolution works for subsequent requests.
|
|
426
494
|
buildDesktop3pRegistry(
|
|
427
|
-
[...
|
|
495
|
+
[...desktopVisibleNativeSlugs(config)],
|
|
428
496
|
goOrdered.map(m => ({ provider: m.provider, id: m.id, contextWindow: m.contextWindow })),
|
|
429
497
|
config.claudeCode?.desktopProfile,
|
|
430
498
|
);
|
|
@@ -441,7 +509,7 @@ export function startServer(port?: number) {
|
|
|
441
509
|
: idsParam === "desktop"
|
|
442
510
|
? "desktop3p" as const
|
|
443
511
|
: (/^claude-code\//i.test(req.headers.get("user-agent") ?? "") ? "readable" as const : "desktop3p" as const);
|
|
444
|
-
const data = buildAnthropicModelInfos([...
|
|
512
|
+
const data = buildAnthropicModelInfos([...desktopVisibleNativeSlugs(config)], goOrdered, resolveAutoContext(config.claudeCode), idStyle, activeDesktop3pAlias);
|
|
445
513
|
return jsonResponse({ data }, 200, req, config);
|
|
446
514
|
}
|
|
447
515
|
if (url.searchParams.has("client_version")) {
|
|
@@ -470,28 +538,30 @@ export function startServer(port?: number) {
|
|
|
470
538
|
if (isDraining()) {
|
|
471
539
|
return drainingResponse(req);
|
|
472
540
|
}
|
|
473
|
-
const
|
|
474
|
-
if (
|
|
541
|
+
const admission = resolveResponsesApiAuth(req, config);
|
|
542
|
+
if (!admission) return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req, config);
|
|
475
543
|
if (!isAllowedRequestOrigin(req, config)) {
|
|
476
544
|
return withCors(formatErrorResponse(403, "origin_rejected", "cross-origin data-plane request blocked"), req, config);
|
|
477
545
|
}
|
|
478
546
|
const start = Date.now();
|
|
479
547
|
const requestId = nextRequestLogId(start);
|
|
480
|
-
const logCtx: RequestLogContext = {
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
548
|
+
const logCtx: RequestLogContext = {
|
|
549
|
+
model: "unknown",
|
|
550
|
+
provider: "unknown",
|
|
551
|
+
...admissionFields(admission),
|
|
552
|
+
inboundProtocol: "responses",
|
|
553
|
+
};
|
|
554
|
+
return runAdmittedHttpTurn(req, async () => {
|
|
555
|
+
let response: Response;
|
|
556
|
+
try {
|
|
557
|
+
response = await handleResponsesCompact(req, config, logCtx);
|
|
558
|
+
} catch {
|
|
559
|
+
response = formatErrorResponse(500, "server_error", "Unexpected compact request failure");
|
|
560
|
+
}
|
|
561
|
+
addFinalRequestLog(requestId, start, logCtx, response.status,
|
|
562
|
+
response.status === 499 ? { closeReason: "client_cancel" } : undefined);
|
|
563
|
+
return withCors(response, req, config);
|
|
564
|
+
});
|
|
495
565
|
}
|
|
496
566
|
|
|
497
567
|
if (
|
|
@@ -502,23 +572,29 @@ export function startServer(port?: number) {
|
|
|
502
572
|
if (isDraining()) {
|
|
503
573
|
return drainingResponse(req);
|
|
504
574
|
}
|
|
505
|
-
const
|
|
506
|
-
if (
|
|
575
|
+
const admission = resolveApiAuth(req, config);
|
|
576
|
+
if (!admission) return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req, config);
|
|
507
577
|
if (!isAllowedRequestOrigin(req, config)) {
|
|
508
578
|
return withCors(formatErrorResponse(403, "origin_rejected", "cross-origin data-plane request blocked"), req, config);
|
|
509
579
|
}
|
|
510
580
|
const start = Date.now();
|
|
511
581
|
const requestId = nextRequestLogId(start);
|
|
512
|
-
const logCtx: RequestLogContext = {
|
|
582
|
+
const logCtx: RequestLogContext = {
|
|
583
|
+
model: "image_gen",
|
|
584
|
+
provider: "unknown",
|
|
585
|
+
...admissionFields(admission),
|
|
586
|
+
};
|
|
513
587
|
const endpoint = url.pathname.endsWith("/edits") ? "edits" as const : "generations" as const;
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
588
|
+
return runAdmittedHttpTurn(req, async () => {
|
|
589
|
+
const response = await handleImages(req, config, endpoint, logCtx);
|
|
590
|
+
addFinalRequestLog(requestId, start, logCtx, response.status, response.status === 499 ? { closeReason: "client_cancel" } : undefined);
|
|
591
|
+
return withCors(response, req, config);
|
|
592
|
+
});
|
|
517
593
|
}
|
|
518
594
|
|
|
519
595
|
if (req.method === "GET" && url.pathname.startsWith("/v1/opencodex/artifacts/")) {
|
|
520
|
-
const
|
|
521
|
-
if (
|
|
596
|
+
const admission = resolveApiAuth(req, config);
|
|
597
|
+
if (!admission) return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req, config);
|
|
522
598
|
if (!isAllowedRequestOrigin(req, config)) {
|
|
523
599
|
return withCors(formatErrorResponse(403, "origin_rejected", "cross-origin data-plane request blocked"), req, config);
|
|
524
600
|
}
|
|
@@ -551,23 +627,24 @@ export function startServer(port?: number) {
|
|
|
551
627
|
if (isDraining()) {
|
|
552
628
|
return drainingResponse(req);
|
|
553
629
|
}
|
|
554
|
-
const
|
|
555
|
-
if (
|
|
630
|
+
const admission = resolveApiAuth(req, config);
|
|
631
|
+
if (!admission) return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req, config);
|
|
556
632
|
if (!isAllowedRequestOrigin(req, config)) {
|
|
557
633
|
return withCors(formatErrorResponse(403, "origin_rejected", "cross-origin data-plane request blocked"), req, config);
|
|
558
634
|
}
|
|
559
635
|
const start = Date.now();
|
|
560
636
|
const requestId = nextRequestLogId(start);
|
|
561
|
-
const logCtx: RequestLogContext = {
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
response
|
|
568
|
-
response.status
|
|
569
|
-
|
|
570
|
-
|
|
637
|
+
const logCtx: RequestLogContext = {
|
|
638
|
+
model: "web_search",
|
|
639
|
+
provider: "unknown",
|
|
640
|
+
...admissionFields(admission),
|
|
641
|
+
};
|
|
642
|
+
return runAdmittedHttpTurn(req, async () => {
|
|
643
|
+
const response = await handleSearch(req, config, logCtx);
|
|
644
|
+
addFinalRequestLog(requestId, start, logCtx, response.status,
|
|
645
|
+
response.status === 499 ? { closeReason: "client_cancel" } : undefined);
|
|
646
|
+
return withCors(response, req, config);
|
|
647
|
+
});
|
|
571
648
|
}
|
|
572
649
|
|
|
573
650
|
if (url.pathname === "/v1/responses" && req.method === "POST") {
|
|
@@ -575,14 +652,19 @@ export function startServer(port?: number) {
|
|
|
575
652
|
if (isDraining()) {
|
|
576
653
|
return drainingResponse(req);
|
|
577
654
|
}
|
|
578
|
-
const
|
|
579
|
-
if (
|
|
655
|
+
const admission = resolveResponsesApiAuth(req, config);
|
|
656
|
+
if (!admission) return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req, config);
|
|
580
657
|
if (!isAllowedRequestOrigin(req, config)) {
|
|
581
658
|
return withCors(formatErrorResponse(403, "origin_rejected", "cross-origin data-plane request blocked"), req, config);
|
|
582
659
|
}
|
|
583
660
|
const start = Date.now();
|
|
584
661
|
const requestId = nextRequestLogId(start);
|
|
585
|
-
const logCtx = {
|
|
662
|
+
const logCtx: RequestLogContext = {
|
|
663
|
+
model: "unknown",
|
|
664
|
+
provider: "unknown",
|
|
665
|
+
...admissionFields(admission),
|
|
666
|
+
inboundProtocol: "responses",
|
|
667
|
+
};
|
|
586
668
|
let logged = false;
|
|
587
669
|
const finalizeNativePassthroughLog = (
|
|
588
670
|
status: number,
|
|
@@ -592,20 +674,23 @@ export function startServer(port?: number) {
|
|
|
592
674
|
logged = true;
|
|
593
675
|
addFinalRequestLog(requestId, start, logCtx, status, meta);
|
|
594
676
|
};
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
677
|
+
return runAdmittedHttpTurn(req, async turnAdmissionLease => {
|
|
678
|
+
const response = await handleResponses(req, config, logCtx, {
|
|
679
|
+
turnAdmissionLease,
|
|
680
|
+
abortSignal: req.signal,
|
|
681
|
+
onFirstOutput: () => recordFirstOutput(logCtx, start),
|
|
682
|
+
onNativePassthroughTerminal: status => {
|
|
683
|
+
finalizeNativePassthroughLog(httpStatusForTerminalStatus(status), {
|
|
684
|
+
terminalStatus: status,
|
|
685
|
+
closeReason: "terminal",
|
|
686
|
+
});
|
|
687
|
+
},
|
|
688
|
+
onNativePassthroughCancel: () => {
|
|
689
|
+
finalizeNativePassthroughLog(499, { closeReason: "client_cancel" });
|
|
690
|
+
},
|
|
691
|
+
});
|
|
692
|
+
return withCors(responseWithDeferredRequestLog(response, requestId, start, logCtx), req, config);
|
|
607
693
|
});
|
|
608
|
-
return withCors(responseWithDeferredRequestLog(response, requestId, start, logCtx), req, config);
|
|
609
694
|
}
|
|
610
695
|
|
|
611
696
|
// Anthropic Messages inbound (Claude Code). count_tokens FIRST (longer path).
|
|
@@ -614,14 +699,14 @@ export function startServer(port?: number) {
|
|
|
614
699
|
if (isDraining()) {
|
|
615
700
|
return drainingResponse(req);
|
|
616
701
|
}
|
|
617
|
-
|
|
702
|
+
const admission = resolveApiAuth(req, config);
|
|
703
|
+
if (!admission) {
|
|
618
704
|
return withCors(anthropicErrorResponse(401, "opencodex API key required", "authentication_error"), req, config);
|
|
619
705
|
}
|
|
620
706
|
if (!isAllowedRequestOrigin(req, config)) {
|
|
621
707
|
return withCors(anthropicErrorResponse(403, "cross-origin data-plane request blocked", "permission_error"), req, config);
|
|
622
708
|
}
|
|
623
|
-
|
|
624
|
-
return withCors(response, req, config);
|
|
709
|
+
return runAdmittedHttpTurn(req, async () => withCors(await handleClaudeCountTokens(req, config), req, config));
|
|
625
710
|
}
|
|
626
711
|
|
|
627
712
|
if (url.pathname === "/v1/messages" && req.method === "POST") {
|
|
@@ -629,7 +714,8 @@ export function startServer(port?: number) {
|
|
|
629
714
|
if (isDraining()) {
|
|
630
715
|
return drainingResponse(req);
|
|
631
716
|
}
|
|
632
|
-
|
|
717
|
+
const admission = resolveApiAuth(req, config);
|
|
718
|
+
if (!admission) {
|
|
633
719
|
return withCors(anthropicErrorResponse(401, "opencodex API key required", "authentication_error"), req, config);
|
|
634
720
|
}
|
|
635
721
|
if (!isAllowedRequestOrigin(req, config)) {
|
|
@@ -637,12 +723,20 @@ export function startServer(port?: number) {
|
|
|
637
723
|
}
|
|
638
724
|
const start = Date.now();
|
|
639
725
|
const requestId = nextRequestLogId(start);
|
|
640
|
-
const logCtx: RequestLogContext = {
|
|
726
|
+
const logCtx: RequestLogContext = {
|
|
727
|
+
model: "unknown",
|
|
728
|
+
provider: "unknown",
|
|
729
|
+
...admissionFields(admission),
|
|
730
|
+
inboundProtocol: "messages",
|
|
731
|
+
};
|
|
641
732
|
// Logging is finalized inside handleClaudeMessages (Responses-vocab tap on the
|
|
642
733
|
// pre-translation stream + native passthrough callbacks) — do not re-wrap the
|
|
643
734
|
// translated Anthropic stream here.
|
|
644
|
-
|
|
645
|
-
|
|
735
|
+
return runAdmittedHttpTurn(req, async turnAdmissionLease => withCors(
|
|
736
|
+
await handleClaudeMessages(req, config, logCtx, { requestId, start, turnAdmissionLease }),
|
|
737
|
+
req,
|
|
738
|
+
config,
|
|
739
|
+
));
|
|
646
740
|
}
|
|
647
741
|
|
|
648
742
|
|
|
@@ -652,16 +746,24 @@ export function startServer(port?: number) {
|
|
|
652
746
|
if (isDraining()) {
|
|
653
747
|
return drainingResponse(req);
|
|
654
748
|
}
|
|
655
|
-
const
|
|
656
|
-
if (
|
|
749
|
+
const admission = resolveResponsesApiAuth(req, config);
|
|
750
|
+
if (!admission) return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req, config);
|
|
657
751
|
if (!isAllowedRequestOrigin(req, config)) {
|
|
658
752
|
return withCors(formatErrorResponse(403, "origin_rejected", "cross-origin data-plane request blocked"), req, config);
|
|
659
753
|
}
|
|
660
754
|
const start = Date.now();
|
|
661
755
|
const requestId = nextRequestLogId(start);
|
|
662
|
-
const logCtx: RequestLogContext = {
|
|
663
|
-
|
|
664
|
-
|
|
756
|
+
const logCtx: RequestLogContext = {
|
|
757
|
+
model: "unknown",
|
|
758
|
+
provider: "unknown",
|
|
759
|
+
...admissionFields(admission),
|
|
760
|
+
inboundProtocol: "chat",
|
|
761
|
+
};
|
|
762
|
+
return runAdmittedHttpTurn(req, async turnAdmissionLease => withCors(
|
|
763
|
+
await handleChatCompletions(req, config, logCtx, { requestId, start, turnAdmissionLease }),
|
|
764
|
+
req,
|
|
765
|
+
config,
|
|
766
|
+
));
|
|
665
767
|
}
|
|
666
768
|
|
|
667
769
|
// ChatGPT / Codex App voice (GPT‑Live / Frameless Bidi) + OpenAI Realtime call-create.
|
|
@@ -675,23 +777,29 @@ export function startServer(port?: number) {
|
|
|
675
777
|
if (isDraining()) {
|
|
676
778
|
return drainingResponse(req);
|
|
677
779
|
}
|
|
678
|
-
const
|
|
679
|
-
if (
|
|
780
|
+
const admission = resolveApiAuth(req, config);
|
|
781
|
+
if (!admission) return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req, config);
|
|
680
782
|
if (!isAllowedRequestOrigin(req, config)) {
|
|
681
783
|
return withCors(formatErrorResponse(403, "origin_rejected", "cross-origin data-plane request blocked"), req, config);
|
|
682
784
|
}
|
|
683
785
|
const start = Date.now();
|
|
684
786
|
const requestId = nextRequestLogId(start);
|
|
685
|
-
const logCtx: RequestLogContext = {
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
response
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
787
|
+
const logCtx: RequestLogContext = {
|
|
788
|
+
model: "gpt-live",
|
|
789
|
+
provider: "unknown",
|
|
790
|
+
...admissionFields(admission),
|
|
791
|
+
};
|
|
792
|
+
return runAdmittedHttpTurn(req, async () => {
|
|
793
|
+
const response = await handleLive(req, config, logCtx);
|
|
794
|
+
addFinalRequestLog(
|
|
795
|
+
requestId,
|
|
796
|
+
start,
|
|
797
|
+
logCtx,
|
|
798
|
+
response.status,
|
|
799
|
+
response.status === 499 ? { closeReason: "client_cancel" } : undefined,
|
|
800
|
+
);
|
|
801
|
+
return withCors(response, req, config);
|
|
802
|
+
});
|
|
695
803
|
}
|
|
696
804
|
|
|
697
805
|
// Voice / Realtime sideband WebSocket: Frameless joins /v1/live/{callId}; Realtime v1 joins
|
|
@@ -703,14 +811,18 @@ export function startServer(port?: number) {
|
|
|
703
811
|
if (isDraining()) {
|
|
704
812
|
return drainingResponse(req);
|
|
705
813
|
}
|
|
706
|
-
const
|
|
707
|
-
if (
|
|
814
|
+
const admission = resolveApiAuth(req, config);
|
|
815
|
+
if (!admission) return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req, config);
|
|
708
816
|
if (!isAllowedRequestOrigin(req, config)) {
|
|
709
817
|
return withCors(formatErrorResponse(403, "origin_rejected", "WebSocket upgrade blocked: non-local Origin"), req, config);
|
|
710
818
|
}
|
|
711
819
|
const start = Date.now();
|
|
712
820
|
const requestId = nextRequestLogId(start);
|
|
713
|
-
const logCtx: RequestLogContext = {
|
|
821
|
+
const logCtx: RequestLogContext = {
|
|
822
|
+
model: "gpt-live",
|
|
823
|
+
provider: "unknown",
|
|
824
|
+
...admissionFields(admission),
|
|
825
|
+
};
|
|
714
826
|
const resolved = await resolveLiveSidebandUpgrade(req, config, logCtx, liveSidebandTarget);
|
|
715
827
|
if (resolved instanceof Response) {
|
|
716
828
|
addFinalRequestLog(requestId, start, logCtx, resolved.status);
|
|
@@ -759,6 +871,11 @@ export function startServer(port?: number) {
|
|
|
759
871
|
attachLiveSidebandUpstream(ws);
|
|
760
872
|
return;
|
|
761
873
|
}
|
|
874
|
+
if (!ws.data.admissionLease) {
|
|
875
|
+
ws.close(1013, "server busy");
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
878
|
+
ws.data.admissionLease.bind(ws);
|
|
762
879
|
registerCodexWebSocket(ws);
|
|
763
880
|
},
|
|
764
881
|
message(ws: ServerWebSocket<WsData>, raw: string | Buffer) {
|
|
@@ -827,13 +944,35 @@ export function startServer(port?: number) {
|
|
|
827
944
|
return;
|
|
828
945
|
}
|
|
829
946
|
|
|
947
|
+
const turnAdmissionLease = tryAdmitTurn();
|
|
948
|
+
if (!turnAdmissionLease) {
|
|
949
|
+
sendJsonFrame(ws, buildWsErrorFrame(503, {
|
|
950
|
+
type: "server_error",
|
|
951
|
+
code: "server_busy",
|
|
952
|
+
message: "active turns capacity reached",
|
|
953
|
+
retryable: true,
|
|
954
|
+
}, new Headers({ "Retry-After": "1" })));
|
|
955
|
+
if (ws.data.cancel === cancelTurn) ws.data.cancel = undefined;
|
|
956
|
+
return;
|
|
957
|
+
}
|
|
958
|
+
|
|
830
959
|
const payload: Record<string, unknown> = { ...frame };
|
|
831
960
|
delete payload.type;
|
|
832
|
-
|
|
961
|
+
turnAdmissionLease.bindAbortController(turnAbort);
|
|
833
962
|
void (async () => {
|
|
834
963
|
const start = Date.now();
|
|
835
964
|
const requestId = nextRequestLogId(start);
|
|
836
|
-
|
|
965
|
+
// Resolved once at the handshake — a frame has no request headers left
|
|
966
|
+
// to re-resolve from. Optional on WsData like every other member, so
|
|
967
|
+
// narrow rather than assume: an unattributed frame is preferable to a
|
|
968
|
+
// fabricated attribution.
|
|
969
|
+
const wsAdmission = ws.data.admission;
|
|
970
|
+
const logCtx: RequestLogContext = {
|
|
971
|
+
model: "unknown",
|
|
972
|
+
provider: "unknown",
|
|
973
|
+
...(wsAdmission ? admissionFields(wsAdmission) : {}),
|
|
974
|
+
inboundProtocol: "responses",
|
|
975
|
+
};
|
|
837
976
|
let logged = false;
|
|
838
977
|
const finalizeLog = (
|
|
839
978
|
status: number,
|
|
@@ -856,6 +995,7 @@ export function startServer(port?: number) {
|
|
|
856
995
|
const response = await handleResponses(req, config, logCtx, {
|
|
857
996
|
forceEmptyResponseId: true,
|
|
858
997
|
abortSignal: turnAbort.signal,
|
|
998
|
+
turnAdmissionLease,
|
|
859
999
|
onFirstOutput: () => recordFirstOutput(logCtx, start),
|
|
860
1000
|
onCodexAuthContextResolved: context => updateCodexWebSocketAuthContext(ws, context),
|
|
861
1001
|
recordTerminalOutcomes: false,
|
|
@@ -896,7 +1036,7 @@ export function startServer(port?: number) {
|
|
|
896
1036
|
/* socket already gone or send dropped */
|
|
897
1037
|
}
|
|
898
1038
|
} finally {
|
|
899
|
-
|
|
1039
|
+
turnAdmissionLease.release();
|
|
900
1040
|
if (!logged && turnAbort.signal.aborted) finalizeLog(499);
|
|
901
1041
|
if (ws.data.cancel === cancelTurn) ws.data.cancel = undefined;
|
|
902
1042
|
}
|
|
@@ -909,6 +1049,8 @@ export function startServer(port?: number) {
|
|
|
909
1049
|
return;
|
|
910
1050
|
}
|
|
911
1051
|
unregisterCodexWebSocket(ws);
|
|
1052
|
+
ws.data.admissionLease?.release();
|
|
1053
|
+
ws.data.admissionLease = undefined;
|
|
912
1054
|
ws.data.cancel?.(); // RC2: abort the upstream when the client disconnects
|
|
913
1055
|
},
|
|
914
1056
|
},
|