@bitkyc08/opencodex 2.57.0 → 2.59.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 +28 -10
- package/gui/dist/assets/index-C5IebErG.js +136 -0
- package/gui/dist/assets/{index-C5-RdDmD.css → index-OESInAjC.css} +1 -1
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/crusoe.svg +1 -0
- package/gui/dist/provider-icons/opper.svg +3 -0
- package/package.json +2 -2
- package/src/adapters/base.ts +11 -1
- package/src/adapters/codebuddy/scaffold-guard.ts +5 -4
- package/src/adapters/command-code.ts +13 -4
- package/src/adapters/cursor/catalog.ts +11 -0
- package/src/adapters/cursor/cursor-errors.ts +15 -0
- package/src/adapters/cursor/discovery.ts +65 -1
- package/src/adapters/cursor/effort-map.ts +16 -2
- package/src/adapters/cursor/envelope-echo.ts +55 -2
- package/src/adapters/cursor/live-transport.ts +5 -1
- package/src/adapters/cursor/message-mapper.ts +3 -2
- package/src/adapters/cursor/protobuf-events.ts +110 -11
- package/src/adapters/cursor/protobuf-request.ts +27 -6
- package/src/adapters/cursor/request-builder.ts +14 -3
- package/src/adapters/cursor/text-toolcall.ts +230 -0
- package/src/adapters/cursor/thread-continuity.ts +141 -0
- package/src/adapters/cursor/tool-guidance.ts +5 -4
- package/src/adapters/cursor/types.ts +5 -0
- package/src/adapters/cursor.ts +97 -6
- package/src/adapters/devin/cloud-direct/chat.ts +11 -2
- package/src/adapters/devin/cloud-direct/index.ts +7 -0
- package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +103 -0
- package/src/adapters/devin.ts +75 -13
- package/src/adapters/google-antigravity-wire.ts +29 -2
- package/src/adapters/google-http.ts +45 -13
- package/src/adapters/google.ts +23 -4
- package/src/adapters/mimo-free.ts +32 -17
- package/src/adapters/ollama-native.ts +42 -8
- package/src/adapters/openai-chat/response-events.ts +61 -0
- package/src/adapters/openai-chat.ts +5 -10
- package/src/adapters/openai-responses/passthrough.ts +40 -5
- package/src/adapters/openai-responses/request-strips.ts +43 -0
- package/src/adapters/openai-responses/tool-output-recovery.ts +75 -0
- package/src/adapters/openai-responses/tool-schema.ts +19 -7
- package/src/adapters/physical-send.ts +50 -0
- package/src/adapters/responses-tool-schema.ts +76 -46
- package/src/adapters/run-turn-queue.ts +17 -4
- package/src/bridge/response-json.ts +2 -2
- package/src/bridge/sse.ts +166 -25
- package/src/claude/context-windows.ts +22 -0
- package/src/claude/outbound.ts +46 -5
- package/src/cli/account-api.ts +4 -3
- package/src/cli/account-extended.ts +22 -2
- package/src/cli/account-orca-import.ts +63 -0
- package/src/cli/account.ts +32 -4
- package/src/cli/capabilities.ts +40 -0
- package/src/cli/claude.ts +29 -1
- package/src/cli/codex-cli-update.ts +97 -2
- package/src/cli/config-command.ts +35 -18
- package/src/cli/dispatch.ts +71 -4
- package/src/cli/doctor.ts +197 -2
- package/src/cli/help.ts +4 -1
- package/src/cli/index.ts +132 -22
- package/src/cli/models-runtime.ts +33 -4
- package/src/cli/registry.ts +11 -1
- package/src/cli/runtime-api.ts +44 -0
- package/src/cli/start-args.ts +94 -0
- package/src/cli/system-command.ts +72 -1
- package/src/cli/uninstall-client-state.ts +12 -0
- package/src/client/machine-api.ts +4 -3
- package/src/client/machine-listener.ts +14 -1
- package/src/clients/config-export/constants.ts +2 -3
- package/src/clients/config-export.ts +5 -5
- package/src/codex/account-store.ts +81 -5
- package/src/codex/auth-api/pool-quota-probe.ts +14 -3
- package/src/codex/auth-api/routes.ts +17 -2
- package/src/codex/auth-context.ts +58 -20
- package/src/codex/catalog/build-entries.ts +25 -4
- package/src/codex/catalog/derive-entry.ts +8 -1
- package/src/codex/catalog/effort.ts +10 -6
- package/src/codex/catalog/gather-capture.ts +1 -0
- package/src/codex/catalog/model-hints.ts +37 -5
- package/src/codex/catalog/parsing.ts +83 -5
- package/src/codex/catalog/reserve-warn.ts +96 -0
- package/src/codex/catalog/retained-sync.ts +19 -0
- package/src/codex/catalog/routed-gather.ts +42 -3
- package/src/codex/cli-installation-identity.ts +210 -0
- package/src/codex/cli-installation-targets.ts +158 -0
- package/src/codex/convergence.ts +5 -0
- package/src/codex/desktop-switches.ts +145 -0
- package/src/codex/history-job.ts +5 -1
- package/src/codex/history-provider.ts +37 -5
- package/src/codex/history-state-open.ts +105 -0
- package/src/codex/history-worker.ts +14 -1
- package/src/codex/inject/config-toml.ts +44 -2
- package/src/codex/inject/remove.ts +145 -7
- package/src/codex/inject/restore.ts +204 -32
- package/src/codex/inject.ts +6 -9
- package/src/codex/lineage.ts +83 -32
- package/src/codex/loopback-target.ts +40 -0
- package/src/codex/main-account-hard-lock.ts +2 -1
- package/src/codex/main-account.ts +10 -3
- package/src/codex/main-device-reauth.ts +17 -9
- package/src/codex/model-entitlements.ts +60 -1
- package/src/codex/native-profile-startup.ts +64 -20
- package/src/codex/observed-model-denials.ts +137 -0
- package/src/codex/orca-auth-source.ts +94 -0
- package/src/codex/orca-import.ts +219 -0
- package/src/codex/prompt-text-probe.ts +282 -12
- package/src/codex/quota-401-recovery.ts +12 -0
- package/src/codex/quota-types.ts +65 -0
- package/src/codex/quota.ts +24 -19
- package/src/codex/routing/cooldown-math.ts +8 -47
- package/src/codex/routing/pin-drain.ts +57 -0
- package/src/codex/routing.ts +13 -15
- package/src/codex/subagent-model-fallback.ts +94 -0
- package/src/codex/windows-installation-files.ts +224 -0
- package/src/combos/failover.ts +122 -5
- package/src/config/atomic-write.ts +83 -8
- package/src/config/diagnostics.ts +21 -0
- package/src/config/load-degrade.ts +15 -0
- package/src/config/pending-teardown.ts +8 -0
- package/src/config/process-state.ts +36 -3
- package/src/config/provider-relative-send-path.ts +16 -0
- package/src/config/proxy-env.ts +23 -5
- package/src/config/schema/config-schema.ts +23 -0
- package/src/config/schema/leaf-validators.ts +65 -17
- package/src/generated/compatibility-version.json +337 -201
- package/src/generated/model-metadata.ts +1 -1
- package/src/lib/bounded-body.ts +4 -2
- package/src/lib/bounded-subprocess.ts +62 -10
- package/src/lib/destination-policy.ts +48 -6
- package/src/lib/errors.ts +3 -15
- package/src/lib/local-destinations.ts +32 -5
- package/src/lib/provider-outbound.ts +3 -3
- package/src/lib/proxy-env.ts +70 -3
- package/src/lib/request-execution-budget.ts +11 -3
- package/src/lib/response-body-inactivity.ts +193 -0
- package/src/lib/retry-delay.ts +69 -0
- package/src/lib/socks5-fetch.ts +631 -0
- package/src/lib/spend-reservation-ledger.ts +115 -9
- package/src/lib/windows-secret-acl.ts +151 -15
- package/src/lib/windows-user-principal.ts +5 -1
- package/src/lib/workflow-budget.ts +145 -8
- package/src/oauth/account-quota-rank.ts +72 -15
- package/src/oauth/generic-account-failover.ts +40 -27
- package/src/oauth/orcarouter.ts +15 -2
- package/src/oauth/store.ts +8 -0
- package/src/providers/codex-capacity.ts +9 -0
- package/src/providers/derive.ts +6 -0
- package/src/providers/devin-provider-merge-migration.ts +33 -12
- package/src/providers/free-directory.ts +20 -2
- package/src/providers/key-failover.ts +261 -7
- package/src/providers/model-discovery.ts +19 -7
- package/src/providers/model-rename-migration.ts +1 -0
- package/src/providers/openai-sidecar.ts +4 -0
- package/src/providers/opencode-go-transport.ts +14 -5
- package/src/providers/quota/report-cache.ts +3 -0
- package/src/providers/registry/entries-core.ts +11 -0
- package/src/providers/registry/entries-extended.ts +146 -28
- package/src/providers/registry/model-seeds.ts +136 -29
- package/src/providers/registry/types.ts +9 -0
- package/src/responses/apply-patch-envelope.ts +44 -11
- package/src/responses/bridge-search-replay-cache.ts +152 -0
- package/src/responses/code-mode-helper-compat.ts +26 -16
- package/src/responses/custom-tool-compat.ts +1 -1
- package/src/responses/hosted-tool-policy.ts +85 -2
- package/src/responses/schema.ts +9 -2
- package/src/responses/spill-store.ts +17 -0
- package/src/responses/state/body-policy.ts +25 -0
- package/src/responses/state/spill-queue.ts +8 -6
- package/src/responses/state.ts +3 -22
- package/src/router.ts +4 -0
- package/src/server/auth-cors.ts +27 -0
- package/src/server/chat-completions.ts +9 -4
- package/src/server/chat-native-sse.ts +26 -9
- package/src/server/chat-native.ts +10 -4
- package/src/server/claude-messages.ts +24 -2
- package/src/server/gui-static.ts +36 -2
- package/src/server/inbound-body-admission.ts +187 -0
- package/src/server/index/websocket-handler.ts +48 -1
- package/src/server/index.ts +15 -19
- package/src/server/management/api-access.ts +3 -4
- package/src/server/management/config-routes.ts +57 -10
- package/src/server/management/provider-capability-config.ts +35 -7
- package/src/server/management/provider-routes.ts +70 -18
- package/src/server/models-capabilities.ts +24 -3
- package/src/server/proxy-liveness.ts +97 -2
- package/src/server/relay.ts +17 -24
- package/src/server/request-log.ts +25 -1
- package/src/server/responses/adapter-continuation.ts +71 -27
- package/src/server/responses/adapter-delivery.ts +39 -8
- package/src/server/responses/adapter-dispatch.ts +52 -24
- package/src/server/responses/codex-ws-exchange.ts +65 -4
- package/src/server/responses/combo-stream-preflight.ts +68 -5
- package/src/server/responses/compact.ts +60 -11
- package/src/server/responses/core-codex-account.ts +83 -22
- package/src/server/responses/core-combo.ts +26 -0
- package/src/server/responses/core-normalize.ts +12 -5
- package/src/server/responses/core-options.ts +3 -0
- package/src/server/responses/fetch-helpers.ts +72 -3
- package/src/server/responses/native-injection-protocol.ts +42 -0
- package/src/server/responses/native-injection-replay.ts +105 -0
- package/src/server/responses/native-injection.ts +242 -0
- package/src/server/responses/native-response-control.ts +56 -0
- package/src/server/responses/native-response-json.ts +14 -0
- package/src/server/responses/native-response-output.ts +37 -0
- package/src/server/responses/native-steering-log.ts +44 -0
- package/src/server/responses/native-steering-policy.ts +49 -0
- package/src/server/responses/native-steering-replay.ts +126 -0
- package/src/server/responses/native-steering-settings.ts +76 -0
- package/src/server/responses/native-steering.ts +400 -0
- package/src/server/responses/native-tool-results.ts +130 -0
- package/src/server/responses/passthrough-delivery.ts +21 -1
- package/src/server/responses/passthrough-dispatch.ts +146 -49
- package/src/server/responses/passthrough-execution.ts +11 -1
- package/src/server/responses/request-prepare.ts +70 -0
- package/src/server/responses/request-send-budget.ts +84 -7
- package/src/server/responses/request-sidecar-auth.ts +16 -8
- package/src/server/responses/request-spend.ts +38 -9
- package/src/server/responses/request-transport.ts +13 -10
- package/src/server/responses/run-turn-execution.ts +20 -5
- package/src/server/responses/sidecar-execution.ts +2 -0
- package/src/server/responses/ws-upstream.ts +23 -2
- package/src/server/responses-custom-tool-repair.ts +2 -2
- package/src/server/sse-frame-buffer.ts +12 -10
- package/src/server/sse-payload-rewrite.ts +36 -9
- package/src/server/stop-teardown.ts +8 -1
- package/src/server/system-env-shell.ts +5 -1
- package/src/server/system-env.ts +7 -1
- package/src/server/workflow-refusal.ts +56 -2
- package/src/server/ws-bridge.ts +16 -1
- package/src/service/cli.ts +29 -7
- package/src/service/guards.ts +10 -0
- package/src/service/health.ts +43 -0
- package/src/service/state.ts +7 -2
- package/src/types/accounts.ts +4 -0
- package/src/types/config.ts +104 -3
- package/src/types/provider.ts +32 -0
- package/src/types/request.ts +7 -1
- package/src/types/wire.ts +9 -1
- package/src/usage/expected-prices.ts +28 -0
- package/src/usage/log.ts +87 -4
- package/src/web-search/passthrough-bridge.ts +39 -5
- package/gui/dist/assets/index-Cz7CLdif.js +0 -128
|
@@ -152,6 +152,8 @@ export function nativeChatSse(
|
|
|
152
152
|
let sawFinish = false;
|
|
153
153
|
let sawDone = false;
|
|
154
154
|
let firstOutput = false;
|
|
155
|
+
let sawTextOutput = false;
|
|
156
|
+
let sawToolCalls = false;
|
|
155
157
|
let settled = false;
|
|
156
158
|
let cancelled = false;
|
|
157
159
|
let cancelledBySignal = false;
|
|
@@ -264,7 +266,7 @@ export function nativeChatSse(
|
|
|
264
266
|
const choices = Array.isArray(parsed.choices) ? parsed.choices : [];
|
|
265
267
|
const finished = choices.some(choice => isRec(choice) && typeof choice.finish_reason === "string" && choice.finish_reason.length > 0);
|
|
266
268
|
if (finished) sawFinish = true;
|
|
267
|
-
const
|
|
269
|
+
const hasText = choices.some(choice => {
|
|
268
270
|
if (!isRec(choice) || !isRec(choice.delta)) return false;
|
|
269
271
|
const delta = choice.delta;
|
|
270
272
|
return (typeof delta.content === "string" && delta.content.length > 0)
|
|
@@ -272,15 +274,22 @@ export function nativeChatSse(
|
|
|
272
274
|
|| (typeof delta.reasoning === "string" && delta.reasoning.length > 0)
|
|
273
275
|
|| (Array.isArray(delta.reasoning_details) && delta.reasoning_details.some(detail =>
|
|
274
276
|
isRec(detail) && typeof detail.text === "string" && detail.text.length > 0))
|
|
275
|
-
|| (typeof delta.refusal === "string" && delta.refusal.length > 0)
|
|
276
|
-
|| (Array.isArray(delta.tool_calls) && delta.tool_calls.some(tool => {
|
|
277
|
-
if (!isRec(tool)) return false;
|
|
278
|
-
const fn = isRec(tool.function) ? tool.function : {};
|
|
279
|
-
return (typeof tool.id === "string" && tool.id.length > 0)
|
|
280
|
-
|| (typeof fn.name === "string" && fn.name.length > 0)
|
|
281
|
-
|| (typeof fn.arguments === "string" && fn.arguments.length > 0);
|
|
282
|
-
}));
|
|
277
|
+
|| (typeof delta.refusal === "string" && delta.refusal.length > 0);
|
|
283
278
|
});
|
|
279
|
+
const hasTool = choices.some(choice => {
|
|
280
|
+
if (!isRec(choice) || !isRec(choice.delta)) return false;
|
|
281
|
+
const delta = choice.delta;
|
|
282
|
+
return Array.isArray(delta.tool_calls) && delta.tool_calls.some(tool => {
|
|
283
|
+
if (!isRec(tool)) return false;
|
|
284
|
+
const fn = isRec(tool.function) ? tool.function : {};
|
|
285
|
+
return (typeof tool.id === "string" && tool.id.length > 0)
|
|
286
|
+
|| (typeof fn.name === "string" && fn.name.length > 0)
|
|
287
|
+
|| (typeof fn.arguments === "string" && fn.arguments.length > 0);
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
if (hasText) sawTextOutput = true;
|
|
291
|
+
if (hasTool) sawToolCalls = true;
|
|
292
|
+
const hasProgress = hasText || hasTool;
|
|
284
293
|
if (hasProgress || finished) pullDeadline = performance.now() + stallMs;
|
|
285
294
|
else if (performance.now() >= pullDeadline) throw stalled;
|
|
286
295
|
if (!firstOutput && hasProgress) {
|
|
@@ -358,6 +367,14 @@ export function nativeChatSse(
|
|
|
358
367
|
if (!sawDone) enqueue(controller, "data: [DONE]\n\n");
|
|
359
368
|
settle(200);
|
|
360
369
|
controller.close();
|
|
370
|
+
} else if (sawToolCalls) {
|
|
371
|
+
// Mid tool-call truncation must fail closed to prevent downstream executing partial arguments.
|
|
372
|
+
fail(controller, "upstream SSE ended mid tool call without a terminal signal — possible truncation", "upstream_sse_truncated");
|
|
373
|
+
} else if (sawTextOutput) {
|
|
374
|
+
// Upstream produced text or reasoning output but missed the terminal frame; recover cleanly with [DONE].
|
|
375
|
+
enqueue(controller, "data: [DONE]\n\n");
|
|
376
|
+
settle(200);
|
|
377
|
+
controller.close();
|
|
361
378
|
} else {
|
|
362
379
|
fail(controller, "upstream SSE ended before a terminal event", "upstream_sse_truncated");
|
|
363
380
|
}
|
|
@@ -50,7 +50,7 @@ import { enrichOpenCodeZenFreeTierMessage } from "../providers/opencode-zen-rate
|
|
|
50
50
|
import type { OcxProviderTransport } from "../providers/xai-transport";
|
|
51
51
|
import type { RouteResult } from "../router";
|
|
52
52
|
import type { OcxConfig, OcxProviderConfig } from "../types";
|
|
53
|
-
import { fetchWithHeaderTimeout, providerFetch, safeHostLabel } from "./responses/fetch-helpers";
|
|
53
|
+
import { fetchWithHeaderTimeout, providerFetch, safeHostLabel, sendWithConnectionPolicy } from "./responses/fetch-helpers";
|
|
54
54
|
import { linkAbortSignal } from "./responses";
|
|
55
55
|
import {
|
|
56
56
|
addFinalRequestLog,
|
|
@@ -351,9 +351,15 @@ export async function handleNativeChatCompletions(options: HandleNativeChatOptio
|
|
|
351
351
|
if (!headers.has("accept-encoding") && encoding) headers.set("accept-encoding", encoding);
|
|
352
352
|
if (init.signal?.aborted) throw init.signal.reason;
|
|
353
353
|
noteProviderAttemptSend(logCtx, route.providerName, activeProvider, logCtx.usageLogInputTokens, transportRecovery ?? recovery);
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
354
|
+
// A reselected provider transport is still a physical send: the connection policy
|
|
355
|
+
// and manual-redirect ownership wrap the selected implementation (#4992).
|
|
356
|
+
const dispatched = await sendWithConnectionPolicy(
|
|
357
|
+
(activeProvider as OcxProviderTransport).fetch ?? execute,
|
|
358
|
+
request.url,
|
|
359
|
+
applyUpstreamRecoveryInit({
|
|
360
|
+
...init, method: request.method, headers, body: request.body,
|
|
361
|
+
}, transportRecovery),
|
|
362
|
+
);
|
|
357
363
|
if (!dispatched.ok) await recordKeyAttemptFailure(logCtx, dispatched, init.signal ?? upstream.signal);
|
|
358
364
|
return dispatched;
|
|
359
365
|
},
|
|
@@ -790,6 +790,22 @@ async function handleClaudeMessagesWithBudget(
|
|
|
790
790
|
|
|
791
791
|
if (!requestedModel) requestedModel = (anthropicBody as Rec).model as string;
|
|
792
792
|
const stream = internalBody.stream === true;
|
|
793
|
+
/**
|
|
794
|
+
* This proxy's count of the prompt it is about to forward, computed at most once.
|
|
795
|
+
*
|
|
796
|
+
* Two readers want it and they want it under different rules. The usage log takes it as a
|
|
797
|
+
* floor only for estimated-usage adapters, because its merge is `max(reported, estimate)` and
|
|
798
|
+
* would otherwise overwrite real usage. `message_start` takes it whenever the upstream sent
|
|
799
|
+
* no confirmed usage before the first frame, where nothing is merged and the terminal
|
|
800
|
+
* `message_delta` still corrects it (#4857).
|
|
801
|
+
*/
|
|
802
|
+
let requestTokenFloor: number | undefined;
|
|
803
|
+
const claudeRequestTokenFloor = (): number => {
|
|
804
|
+
if (requestTokenFloor === undefined) {
|
|
805
|
+
requestTokenFloor = estimateClaudeRequestTokens(anthropicBody as Rec, requestedModel);
|
|
806
|
+
}
|
|
807
|
+
return requestTokenFloor;
|
|
808
|
+
};
|
|
793
809
|
// Routed adapters only support streamed turns; always stream internally and fold
|
|
794
810
|
// the translated Anthropic SSE into a message JSON for non-streaming clients.
|
|
795
811
|
internalBody.stream = true;
|
|
@@ -815,7 +831,7 @@ async function handleClaudeMessagesWithBudget(
|
|
|
815
831
|
// accurate-usage adapters — the request-log merge is max(reported, estimate) and
|
|
816
832
|
// would overwrite real usage (audit 133 R1#7).
|
|
817
833
|
if (route.provider.adapter === "cursor" || route.provider.adapter === "kiro") {
|
|
818
|
-
logCtx.usageLogInputTokens =
|
|
834
|
+
logCtx.usageLogInputTokens = claudeRequestTokenFloor();
|
|
819
835
|
}
|
|
820
836
|
// Effort safety valve (devlog 136 B6, audit 139 R2#2): opus-shaped aliases make
|
|
821
837
|
// every routed model look like a reasoning model to Claude clients, so a forced
|
|
@@ -988,7 +1004,13 @@ async function handleClaudeMessagesWithBudget(
|
|
|
988
1004
|
|
|
989
1005
|
const contentType = response.headers.get("content-type") ?? "";
|
|
990
1006
|
if (contentType.includes("text/event-stream") && response.body) {
|
|
991
|
-
const anthropicSse = responsesSseToAnthropicSse(response.body, requestedModel, {
|
|
1007
|
+
const anthropicSse = responsesSseToAnthropicSse(response.body, requestedModel, {
|
|
1008
|
+
translatorBudget,
|
|
1009
|
+
// Only a floor, and only for the first frame: an upstream that reports usage early wins
|
|
1010
|
+
// over it inside the translator, and the terminal `message_delta` carries the
|
|
1011
|
+
// authoritative count either way (#4857).
|
|
1012
|
+
inputTokenFloor: claudeRequestTokenFloor(),
|
|
1013
|
+
});
|
|
992
1014
|
if (stream) {
|
|
993
1015
|
return new Response(anthropicSse, {
|
|
994
1016
|
status: 200,
|
package/src/server/gui-static.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
2
|
-
import { extname, isAbsolute, join, relative, resolve } from "node:path";
|
|
2
|
+
import { basename, extname, isAbsolute, join, relative, resolve } from "node:path";
|
|
3
3
|
import { browserSecurityHeaders } from "./auth-cors";
|
|
4
4
|
import type { GuiSessionBootstrap } from "./gui-session";
|
|
5
5
|
|
|
@@ -18,6 +18,12 @@ const MIME_TYPES: Record<string, string> = {
|
|
|
18
18
|
".ico": "image/x-icon",
|
|
19
19
|
};
|
|
20
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Matches Vite's content-hashed bundle filenames (e.g. `index-B5r7LNHN.js` or `style-D5SiRo8X.css`).
|
|
23
|
+
* Unhashed static assets (e.g. `runtime-config.js` or unversioned icons) must not be cached immutably.
|
|
24
|
+
*/
|
|
25
|
+
const HASHED_ASSET_PATTERN = /-[a-zA-Z0-9_-]{8,}\.[a-zA-Z0-9]+$/;
|
|
26
|
+
|
|
21
27
|
function findGuiDist(): string | null {
|
|
22
28
|
const candidates = [
|
|
23
29
|
join(import.meta.dir, "..", "..", "gui", "dist"),
|
|
@@ -146,6 +152,14 @@ export function serveSessionBootstrap(session: GuiSessionBootstrap): Response {
|
|
|
146
152
|
return htmlDocumentResponse(html);
|
|
147
153
|
}
|
|
148
154
|
|
|
155
|
+
/**
|
|
156
|
+
* Serves a GUI static file from the distribution directory.
|
|
157
|
+
* Returns a Response with MIME type and appropriate Cache-Control headers:
|
|
158
|
+
* - HTML files receive `no-store` to guarantee immediate bootstrap updates.
|
|
159
|
+
* - Content-hashed bundles under `assets/` receive 1-year `immutable` caching.
|
|
160
|
+
* - Non-hashed static files (e.g. favicon, unhashed assets) receive `no-cache` for prompt revalidation.
|
|
161
|
+
* Returns null if the file cannot be found or resolved.
|
|
162
|
+
*/
|
|
149
163
|
export function serveGuiFile(
|
|
150
164
|
pathname: string,
|
|
151
165
|
guiDist = findGuiDist(),
|
|
@@ -170,11 +184,31 @@ export function serveGuiFile(
|
|
|
170
184
|
const ext = extname(filePath);
|
|
171
185
|
const contentType = MIME_TYPES[ext] || "application/octet-stream";
|
|
172
186
|
if (ext === ".html") return htmlResponse(filePath, session, runtimeRole, managementAuthRequired);
|
|
187
|
+
|
|
188
|
+
const root = resolve(guiDist);
|
|
189
|
+
// "assets/" matches Vite's default assetsDir (gui/vite.config.ts). If that value is
|
|
190
|
+
// ever changed, update this prefix to match. filePath is already resolve()-normalized
|
|
191
|
+
// by resolveGuiFilePath, so rel cannot contain ".." fragments.
|
|
192
|
+
//
|
|
193
|
+
// Immutable 1-year caching is only applied when the file is under assets/ AND its basename
|
|
194
|
+
// matches Vite's content-hash pattern (e.g. index-B5r7LNHN.js). Any unhashed asset under
|
|
195
|
+
// assets/ (e.g. runtime-config.js) or elsewhere falls back to no-cache so clients revalidate,
|
|
196
|
+
// whereas index.html uses no-store above to avoid storing any bootstrap state.
|
|
197
|
+
const rel = relative(root, filePath).replace(/\\/g, "/");
|
|
198
|
+
const isHashedAsset = rel.startsWith("assets/") && HASHED_ASSET_PATTERN.test(basename(filePath));
|
|
199
|
+
const cacheControl = isHashedAsset
|
|
200
|
+
? "public, max-age=31536000, immutable"
|
|
201
|
+
: "no-cache";
|
|
202
|
+
|
|
173
203
|
// Snapshot bytes before returning the response. Bun.file is lazy: if gui/dist is replaced
|
|
174
204
|
// after Bun frames the response but before the stream finishes, its Content-Length can
|
|
175
205
|
// describe the old file while the body comes from the new one (#2792).
|
|
176
206
|
return new Response(readFileSync(filePath), {
|
|
177
|
-
headers: {
|
|
207
|
+
headers: {
|
|
208
|
+
"Content-Type": contentType,
|
|
209
|
+
"Cache-Control": cacheControl,
|
|
210
|
+
...browserSecurityHeaders(),
|
|
211
|
+
},
|
|
178
212
|
});
|
|
179
213
|
}
|
|
180
214
|
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import {
|
|
2
|
+
MAX_CONFIGURABLE_INBOUND_BODY_BYTES,
|
|
3
|
+
MAX_DECOMPRESSED_BODY_BYTES,
|
|
4
|
+
resolveInboundBodyLimitBytes,
|
|
5
|
+
} from "./request-decompress";
|
|
6
|
+
import { withCors, type RequestPolicyView } from "./auth-cors";
|
|
7
|
+
import { codexCompatibleUrl } from "../codex/context-compat";
|
|
8
|
+
import { addFinalRequestLog, markLocalRequestLogRefusal } from "./request-log";
|
|
9
|
+
import type { WorkflowRefusalLog } from "./workflow-refusal";
|
|
10
|
+
|
|
11
|
+
/** Admission units, not a bound on process RSS or the size of parsed object graphs. */
|
|
12
|
+
export const MAX_CONCURRENT_INBOUND_BODY_BYTES = MAX_CONFIGURABLE_INBOUND_BODY_BYTES;
|
|
13
|
+
let reservedInboundBodyBytes = 0;
|
|
14
|
+
|
|
15
|
+
// Match the HTTP routes whose JSON readers use maxInboundBodyBytes. Keep the
|
|
16
|
+
// current image/search/count_tokens support; management, audio, context relay,
|
|
17
|
+
// and WebSocket frames have separate limits and are not part of this gate.
|
|
18
|
+
// Exported so the contract tests classify the dispatcher's own route list
|
|
19
|
+
// instead of copying it: a new loopback route has to land in one of these two
|
|
20
|
+
// sets, or `tests/server/server-request-body-size.test.ts` fails.
|
|
21
|
+
export const CONFIGURABLE_JSON_BODY_ROUTES: ReadonlySet<string> = new Set([
|
|
22
|
+
"/v1/responses",
|
|
23
|
+
"/v1/responses/compact",
|
|
24
|
+
"/v1/chat/completions",
|
|
25
|
+
"/v1/messages",
|
|
26
|
+
"/v1/messages/count_tokens",
|
|
27
|
+
"/v1/images/generations",
|
|
28
|
+
"/v1/images/edits",
|
|
29
|
+
"/v1/alpha/search",
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Loopback routes deliberately outside this gate. Audio uploads, realtime/live
|
|
34
|
+
* control and the model list do not read JSON under `maxInboundBodyBytes`, so
|
|
35
|
+
* reserving that allowance for them would refuse traffic the limit never covered.
|
|
36
|
+
*/
|
|
37
|
+
export const UNGATED_LOOPBACK_ROUTES: ReadonlySet<string> = new Set([
|
|
38
|
+
"/v1/audio/transcriptions",
|
|
39
|
+
"/v1/audio/transcriptions/stream",
|
|
40
|
+
"/v1/models",
|
|
41
|
+
"/v1/realtime",
|
|
42
|
+
"/v1/realtime/calls",
|
|
43
|
+
"/v1/live",
|
|
44
|
+
]);
|
|
45
|
+
|
|
46
|
+
export class InboundBodyCapacityError extends Error {
|
|
47
|
+
constructor() {
|
|
48
|
+
super("OpenCodex inbound request body capacity is temporarily exhausted. Retry after the current request completes.");
|
|
49
|
+
this.name = "InboundBodyCapacityError";
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function cancelUnreadBody(req: Request, reason: unknown): void {
|
|
54
|
+
if (!req.body || req.body.locked) return;
|
|
55
|
+
try {
|
|
56
|
+
// A tee's cancellation can wait for the other branch. Refusal must not wait
|
|
57
|
+
// for a peer that has not finished uploading, or for an unconsumed clone.
|
|
58
|
+
void req.body.cancel(reason).catch(() => undefined);
|
|
59
|
+
} catch { /* Non-standard streams may throw synchronously. */ }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function capacityResponse(pathname: string, error: InboundBodyCapacityError): Response {
|
|
63
|
+
const anthropic = pathname === "/v1/messages" || pathname === "/v1/messages/count_tokens";
|
|
64
|
+
return Response.json({
|
|
65
|
+
...(anthropic ? { type: "error" } : {}),
|
|
66
|
+
error: {
|
|
67
|
+
type: anthropic ? "overloaded_error" : "server_error",
|
|
68
|
+
code: "server_busy",
|
|
69
|
+
message: error.message,
|
|
70
|
+
},
|
|
71
|
+
}, { status: 503, headers: { "Retry-After": "1" } });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function retainUntilResponseSettles(response: Response, release: () => void): Response {
|
|
75
|
+
if (!response.body) {
|
|
76
|
+
release();
|
|
77
|
+
return response;
|
|
78
|
+
}
|
|
79
|
+
const reader = response.body.getReader();
|
|
80
|
+
let cancelling = false;
|
|
81
|
+
let finalized = false;
|
|
82
|
+
const finalize = (): void => {
|
|
83
|
+
if (finalized) return;
|
|
84
|
+
finalized = true;
|
|
85
|
+
try { reader.releaseLock(); } finally { release(); }
|
|
86
|
+
};
|
|
87
|
+
const body = new ReadableStream<Uint8Array>({
|
|
88
|
+
async pull(controller) {
|
|
89
|
+
try {
|
|
90
|
+
const result = await reader.read();
|
|
91
|
+
// cancel() can resolve a pending read as EOF before the producer's
|
|
92
|
+
// asynchronous cancellation completes. Only cancel owns release then.
|
|
93
|
+
if (cancelling) return;
|
|
94
|
+
if (result.done) {
|
|
95
|
+
finalize();
|
|
96
|
+
controller.close();
|
|
97
|
+
} else {
|
|
98
|
+
controller.enqueue(result.value);
|
|
99
|
+
}
|
|
100
|
+
} catch (error) {
|
|
101
|
+
if (cancelling) return;
|
|
102
|
+
finalize();
|
|
103
|
+
controller.error(error);
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
async cancel(reason) {
|
|
107
|
+
cancelling = true;
|
|
108
|
+
try { await reader.cancel(reason); } finally { finalize(); }
|
|
109
|
+
},
|
|
110
|
+
}, { highWaterMark: 0 });
|
|
111
|
+
// This is the outermost HTTP wrapper, after protocol conversion, relay
|
|
112
|
+
// preflight and request logging; no internal relay marker is consumed later.
|
|
113
|
+
return new Response(body, {
|
|
114
|
+
status: response.status,
|
|
115
|
+
statusText: response.statusText,
|
|
116
|
+
headers: response.headers,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Own one raised-limit HTTP request from before body reading through response
|
|
122
|
+
* EOF/error or settled cancellation. Call only at the admitted HTTP boundary,
|
|
123
|
+
* after auth/origin checks, not from readers or internal combo/translation hops.
|
|
124
|
+
* pathname is the same canonical pathname used by the server's dispatcher.
|
|
125
|
+
*/
|
|
126
|
+
export async function withRaisedInboundBodyAdmission(
|
|
127
|
+
req: Request,
|
|
128
|
+
pathname: string,
|
|
129
|
+
configuredLimit: number | undefined,
|
|
130
|
+
work: () => Promise<Response>,
|
|
131
|
+
onRefusal: (response: Response) => Response = response => response,
|
|
132
|
+
): Promise<Response> {
|
|
133
|
+
const maxBytes = resolveInboundBodyLimitBytes(configuredLimit);
|
|
134
|
+
if (req.method !== "POST" || !CONFIGURABLE_JSON_BODY_ROUTES.has(pathname)
|
|
135
|
+
|| maxBytes <= MAX_DECOMPRESSED_BODY_BYTES || req.signal.aborted) {
|
|
136
|
+
return work();
|
|
137
|
+
}
|
|
138
|
+
const declared = req.headers.get("content-length");
|
|
139
|
+
const declaredBytes = declared === null || declared.trim() === "" ? NaN : Number(declared);
|
|
140
|
+
if (Number.isFinite(declaredBytes) && declaredBytes > maxBytes) {
|
|
141
|
+
// The existing bounded reader rejects this before allocating. Let its
|
|
142
|
+
// protocol-specific 413 win over temporary capacity refusal, unchanged.
|
|
143
|
+
return work();
|
|
144
|
+
}
|
|
145
|
+
if (maxBytes > MAX_CONCURRENT_INBOUND_BODY_BYTES - reservedInboundBodyBytes) {
|
|
146
|
+
const error = new InboundBodyCapacityError();
|
|
147
|
+
cancelUnreadBody(req, error);
|
|
148
|
+
return onRefusal(capacityResponse(pathname, error));
|
|
149
|
+
}
|
|
150
|
+
reservedInboundBodyBytes += maxBytes;
|
|
151
|
+
let released = false;
|
|
152
|
+
const release = (): void => {
|
|
153
|
+
if (released) return;
|
|
154
|
+
released = true;
|
|
155
|
+
reservedInboundBodyBytes -= maxBytes;
|
|
156
|
+
};
|
|
157
|
+
try {
|
|
158
|
+
return retainUntilResponseSettles(await work(), release);
|
|
159
|
+
} catch (error) {
|
|
160
|
+
release();
|
|
161
|
+
throw error;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* The admitted HTTP call site. Refusal is recorded as a local terminal and
|
|
167
|
+
* carries the receiving listener's CORS headers, so a browser dashboard can
|
|
168
|
+
* read it. No request content is read or logged on this path.
|
|
169
|
+
*/
|
|
170
|
+
export function runAdmittedBodyWork(
|
|
171
|
+
req: Request,
|
|
172
|
+
policy: RequestPolicyView,
|
|
173
|
+
configuredLimit: number | undefined,
|
|
174
|
+
work: () => Promise<Response>,
|
|
175
|
+
refusalLog?: WorkflowRefusalLog,
|
|
176
|
+
): Promise<Response> {
|
|
177
|
+
return withRaisedInboundBodyAdmission(req, codexCompatibleUrl(req.url).pathname, configuredLimit, work, refused => {
|
|
178
|
+
if (refusalLog) {
|
|
179
|
+
markLocalRequestLogRefusal(refusalLog.logCtx, "server_busy");
|
|
180
|
+
refusalLog.logCtx.errorCode = "server_busy";
|
|
181
|
+
addFinalRequestLog(refusalLog.requestId, refusalLog.start, refusalLog.logCtx, refused.status, {
|
|
182
|
+
closeReason: "terminal",
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
return withCors(refused, req, policy);
|
|
186
|
+
});
|
|
187
|
+
}
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { nativeSteeringUnavailableReason, nativeResponseControlMode, type NativeResponseControl } from "../responses/native-response-control";
|
|
2
|
+
import { NativeInjectionChannel } from "../responses/native-injection";
|
|
3
|
+
import { NativeSteeringChannel, NativeSteeringError } from "../responses/native-steering";
|
|
4
|
+
import { createNativeSteeringLogObserver } from "../responses/native-steering-log";
|
|
1
5
|
import type { Server, ServerWebSocket } from "bun";
|
|
2
6
|
import {
|
|
3
7
|
LIVE_SIDEBAND_UPSTREAM_OPEN_TIMEOUT_MS,
|
|
@@ -188,11 +192,47 @@ export function createWebsocketHandler(ctx: ServeOptionsContext) {
|
|
|
188
192
|
} catch {
|
|
189
193
|
return; // text-only contract; ignore unparseable frames
|
|
190
194
|
}
|
|
195
|
+
if (frame.type === "response.inject" || frame.type === "response.steer" || (frame.type === "response.create" && ws.data.nativeControl)) {
|
|
196
|
+
try {
|
|
197
|
+
if (frame.type === "response.inject") {
|
|
198
|
+
if (!ws.data.nativeControl?.inject) throw new NativeSteeringError("injection_not_supported", "Native injection is disabled or unavailable on this route.");
|
|
199
|
+
ws.data.nativeControl.inject(frame);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if (frame.type === "response.steer") {
|
|
203
|
+
if (!ws.data.nativeControl) throw new NativeSteeringError("steering_not_supported", ws.data.nativeSteeringUnavailable ?? "Native steering transport is unavailable; the route may be unsupported or using HTTP fallback.");
|
|
204
|
+
ws.data.nativeControl.steer(frame);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
if (ws.data.nativeControl?.continue(frame)) return;
|
|
208
|
+
} catch (error) {
|
|
209
|
+
sendJsonFrame(ws, buildWsErrorFrame(400, {
|
|
210
|
+
type: "invalid_request_error",
|
|
211
|
+
code: error instanceof NativeSteeringError ? error.code : "native_steering_error",
|
|
212
|
+
message: error instanceof NativeSteeringError ? error.message : "Native steering transport failed; delivery may be unknown. Do not automatically replay input.",
|
|
213
|
+
}));
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
191
217
|
if (frame.type === "response.processed") return; // ack — no-op
|
|
192
218
|
if (frame.type !== "response.create") return;
|
|
193
219
|
markActivity("ws response.create");
|
|
194
220
|
|
|
221
|
+
let nativeControl: NativeResponseControl | undefined;
|
|
222
|
+
try {
|
|
223
|
+
const idleMs = typeof config.stallTimeoutSec === "number" && Number.isFinite(config.stallTimeoutSec)
|
|
224
|
+
? Math.max(1, config.stallTimeoutSec) * 1000 : 300_000;
|
|
225
|
+
const mode = nativeResponseControlMode(frame, config);
|
|
226
|
+
nativeControl = mode === "injection" ? new NativeInjectionChannel(frame, idleMs)
|
|
227
|
+
: mode === "steering" ? new NativeSteeringChannel(frame, idleMs) : undefined;
|
|
228
|
+
} catch {
|
|
229
|
+
sendJsonFrame(ws, buildWsErrorFrame(400, { type: "invalid_request_error", message: "Invalid native steering request settings" }));
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
195
232
|
ws.data.cancel?.();
|
|
233
|
+
// A superseded turn must not keep ownership during warmup or refusal.
|
|
234
|
+
ws.data.nativeControl = undefined;
|
|
235
|
+
ws.data.nativeSteeringUnavailable = nativeSteeringUnavailableReason(frame, config.codexNativeSteering);
|
|
196
236
|
const turnId = (ws.data.turnId ?? 0) + 1;
|
|
197
237
|
ws.data.turnId = turnId;
|
|
198
238
|
const isCurrent = () => ws.data.turnId === turnId;
|
|
@@ -227,6 +267,8 @@ export function createWebsocketHandler(ctx: ServeOptionsContext) {
|
|
|
227
267
|
return;
|
|
228
268
|
}
|
|
229
269
|
|
|
270
|
+
// Only a genuinely admitted turn may receive steering or continuations.
|
|
271
|
+
ws.data.nativeControl = nativeControl;
|
|
230
272
|
const payload: Record<string, unknown> = { ...frame };
|
|
231
273
|
delete payload.type;
|
|
232
274
|
turnAdmissionLease.bindAbortController(turnAbort);
|
|
@@ -267,6 +309,7 @@ export function createWebsocketHandler(ctx: ServeOptionsContext) {
|
|
|
267
309
|
...(wsAdmission ? { admission: wsAdmission } : {}),
|
|
268
310
|
forceEmptyResponseId: true,
|
|
269
311
|
inboundTransport: "websocket",
|
|
312
|
+
nativeControl,
|
|
270
313
|
abortSignal: turnAbort.signal,
|
|
271
314
|
turnAdmissionLease,
|
|
272
315
|
onFirstOutput: () => recordFirstOutput(logCtx, start),
|
|
@@ -277,7 +320,10 @@ export function createWebsocketHandler(ctx: ServeOptionsContext) {
|
|
|
277
320
|
},
|
|
278
321
|
});
|
|
279
322
|
await sendResponseToWebSocket(ws, response, isCurrent, {
|
|
280
|
-
|
|
323
|
+
untilEof: nativeControl?.relayActive === true,
|
|
324
|
+
onSsePayload: nativeControl?.relayActive
|
|
325
|
+
? createNativeSteeringLogObserver(logCtx, () => recordFirstOutput(logCtx, start))
|
|
326
|
+
: payload => inspectResponseLogSsePayload(logCtx, payload),
|
|
281
327
|
onTerminal: status => {
|
|
282
328
|
terminalRecorder?.(status, logCtx.terminalHttpStatus);
|
|
283
329
|
finalizeLog(httpStatusForRequestLogTerminal(status, logCtx), {
|
|
@@ -313,6 +359,7 @@ export function createWebsocketHandler(ctx: ServeOptionsContext) {
|
|
|
313
359
|
}
|
|
314
360
|
} finally {
|
|
315
361
|
turnAdmissionLease.release();
|
|
362
|
+
if (ws.data.nativeControl === nativeControl) ws.data.nativeControl = undefined;
|
|
316
363
|
if (!logged && turnAbort.signal.aborted) finalizeLog(499);
|
|
317
364
|
if (ws.data.cancel === cancelTurn) ws.data.cancel = undefined;
|
|
318
365
|
}
|
package/src/server/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { remoteWorkspaceEnabled } from "../remote-control/workspace-activation";
|
|
2
2
|
import { AuxiliaryListenerBindError } from "./ports";
|
|
3
|
+
import { runAdmittedBodyWork } from "./inbound-body-admission";
|
|
3
4
|
import {
|
|
4
5
|
buildWarmupCompletionFrames,
|
|
5
6
|
buildWsErrorFrame,
|
|
@@ -106,7 +107,6 @@ export {
|
|
|
106
107
|
unregisterTurn,
|
|
107
108
|
} from "./lifecycle";
|
|
108
109
|
import {
|
|
109
|
-
addFinalRequestLog,
|
|
110
110
|
hydrateRequestLogsFromDisk,
|
|
111
111
|
httpStatusForRequestLogTerminal,
|
|
112
112
|
inspectResponseLogSsePayload,
|
|
@@ -116,8 +116,8 @@ import {
|
|
|
116
116
|
type RequestLogEntry,
|
|
117
117
|
} from "./request-log";
|
|
118
118
|
import { sessionLaneIdFromRequest } from "./request-log-conversation";
|
|
119
|
-
import {
|
|
120
|
-
import {
|
|
119
|
+
import { configureSharedSpendLedger, spendPolicyFromConfig } from "../lib/spend-reservation-ledger";
|
|
120
|
+
import { admitHttpWorkflowTurn, workflowDecisionRefusalResponse, type WorkflowRefusalLog } from "./workflow-refusal";
|
|
121
121
|
export {
|
|
122
122
|
addFinalRequestLog,
|
|
123
123
|
filterRequestLogs,
|
|
@@ -181,7 +181,7 @@ import {
|
|
|
181
181
|
type NativeMainStartupLifecycle,
|
|
182
182
|
} from "../codex/native-profile-startup";
|
|
183
183
|
import { EXTERNAL_CALL_PREFIX, LiveCallBindings } from "./live-call-bindings";
|
|
184
|
-
import {
|
|
184
|
+
import { contextEndpoint, contextRelayActivated } from "../codex/context-compat";
|
|
185
185
|
import { fetchAllModels, handleManagementAPI, VERSION, type ManagementApiDeps } from "./management-api";
|
|
186
186
|
import {
|
|
187
187
|
createManagementSessionControl,
|
|
@@ -225,7 +225,7 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
225
225
|
warnPlaintextV2AgentMessagesStartup(config);
|
|
226
226
|
warnAgentTaskRecoveryStartup(config);
|
|
227
227
|
setLiveStateStoreConfig(config);
|
|
228
|
-
applyProxyEnv(config);
|
|
228
|
+
applyProxyEnv(config, true);
|
|
229
229
|
assertServerAuthConfig(config);
|
|
230
230
|
const managementAuth = deps.managementAuthState ?? initializeManagementAuthState(config);
|
|
231
231
|
const managementSessionControl = createManagementSessionControl(managementAuth);
|
|
@@ -299,6 +299,12 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
299
299
|
registerAppOwnedMemorySweepFallback();
|
|
300
300
|
configureAppOwnedMemoryBudget(resolveAppOwnedMemoryBudgetBytes(config.appOwnedMemoryBudgetMb));
|
|
301
301
|
enforceAppOwnedMemoryBudget();
|
|
302
|
+
// Operator token ceilings (#4546). Applied before the listener binds: the shared ledger is
|
|
303
|
+
// built on the first reservation, and a request arriving before this ran would build it with
|
|
304
|
+
// no policy and enforce nothing. An absent `spend` section resolves to the unconfigured
|
|
305
|
+
// default, which refuses nothing and opens no journal -- so on an install that never wrote
|
|
306
|
+
// the key this line changes no behaviour at all.
|
|
307
|
+
configureSharedSpendLedger(spendPolicyFromConfig(config.spend));
|
|
302
308
|
registerCodexCooldownRecoveryProbeWorker(config);
|
|
303
309
|
// Issue #42 Phase 3: opt-in archived auto-cleanup (default OFF). Unref'd hourly
|
|
304
310
|
// tick for daily/weekly; startup evaluation is fire-and-forget after listen.
|
|
@@ -498,28 +504,18 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
498
504
|
): Promise<Response> {
|
|
499
505
|
const lease = tryAdmitTurn(sessionLaneIdFromRequest(req.headers));
|
|
500
506
|
if (!lease) return serverBusyResponse(req, "active turns", policy);
|
|
501
|
-
//
|
|
502
|
-
|
|
503
|
-
// children. A request that names a parent is treated as that fan-out; a top-level request is
|
|
504
|
-
// the conversation and may use the reserved slots.
|
|
505
|
-
const workflowRootId = req.headers.get("x-codex-parent-thread-id")?.trim() || undefined;
|
|
506
|
-
const workflowThreadId = req.headers.get("thread-id")?.trim() || undefined;
|
|
507
|
-
const workflowLane: WorkflowLane = workflowRootId !== undefined
|
|
508
|
-
&& workflowThreadId !== undefined
|
|
509
|
-
&& workflowThreadId !== workflowRootId
|
|
510
|
-
? "worker"
|
|
511
|
-
: "interactive";
|
|
512
|
-
const workflow = admitWorkflowTurn(workflowRootId, workflowLane, undefined, workflowThreadId);
|
|
507
|
+
// Root, lane, and the refusal that follows from them, all live in ./workflow-refusal.
|
|
508
|
+
const workflow = admitHttpWorkflowTurn(req.headers);
|
|
513
509
|
if (workflow && !workflow.admitted) {
|
|
514
510
|
lease.release();
|
|
515
511
|
// withCors, because without Access-Control-Allow-Origin the exposed refusal header is
|
|
516
512
|
// still unreadable to a browser dashboard -- which made exposing it pointless.
|
|
517
|
-
return withCors(
|
|
513
|
+
return withCors(workflowDecisionRefusalResponse(workflow, undefined, refusalLog), req, policy);
|
|
518
514
|
}
|
|
519
515
|
const releaseWorkflow = (): void => { if (workflow?.admitted) workflow.lease.release(); };
|
|
520
516
|
let response: Response;
|
|
521
517
|
try {
|
|
522
|
-
response = await work(lease);
|
|
518
|
+
response = await runAdmittedBodyWork(req, policy, config.maxInboundBodyBytes, () => work(lease), refusalLog);
|
|
523
519
|
} catch (error) {
|
|
524
520
|
releaseWorkflow();
|
|
525
521
|
lease.release();
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { OcxConfig } from "../../types";
|
|
2
2
|
import { isWildcardHostname } from "../../codex/loopback-target";
|
|
3
|
-
import { localInferenceDestination } from "../../lib/local-destinations";
|
|
4
|
-
import { probeHostname } from "../proxy-liveness";
|
|
3
|
+
import { localCredentialDestinationHostname, localInferenceDestination } from "../../lib/local-destinations";
|
|
5
4
|
import { isCanonicalOpenAiForwardProvider, OPENAI_API_PROVIDER_ID, OPENAI_CODEX_PROVIDER_ID } from "../../providers/openai-tiers-destination";
|
|
6
5
|
import { LIVE_AUDIO_MODEL, TRANSCRIPTION_MODEL } from "../audio-upstream";
|
|
7
6
|
|
|
@@ -95,7 +94,7 @@ export function resolveApiAccessBaseUrl(
|
|
|
95
94
|
const port = config.port ?? 10100;
|
|
96
95
|
|
|
97
96
|
if (!isWildcardBindHost(config.hostname)) {
|
|
98
|
-
return `http://${
|
|
97
|
+
return `http://${localCredentialDestinationHostname(config.hostname)}:${port}/v1`;
|
|
99
98
|
}
|
|
100
99
|
|
|
101
100
|
const fromOrigin = opts.requestOrigin ? originBaseUrl(opts.requestOrigin) : null;
|
|
@@ -140,7 +139,7 @@ export function resolveApiAccessDisplayHost(
|
|
|
140
139
|
opts: BuildApiAccessEndpointsOptions = {},
|
|
141
140
|
): string {
|
|
142
141
|
if (!isWildcardBindHost(configHostname)) {
|
|
143
|
-
return
|
|
142
|
+
return localCredentialDestinationHostname(configHostname);
|
|
144
143
|
}
|
|
145
144
|
try {
|
|
146
145
|
return new URL(resolveApiAccessBaseUrl({ hostname: configHostname, port: 10100 }, opts)).hostname
|