@bitkyc08/opencodex 2.58.0 → 2.60.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-BTuCbqQd.css +1 -0
- package/gui/dist/assets/index-DoBVdPHP.js +134 -0
- 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 +4 -1
- package/src/adapters/anthropic-image-codec.ts +16 -2
- package/src/adapters/anthropic-image-normalize.ts +49 -2
- package/src/adapters/anthropic.ts +4 -1
- package/src/adapters/base.ts +34 -1
- package/src/adapters/coding-agent/turn.ts +22 -2
- package/src/adapters/command-code.ts +50 -3
- package/src/adapters/cursor/catalog.ts +11 -0
- package/src/adapters/cursor/checkpoint-store.ts +3 -0
- package/src/adapters/cursor/discovery.ts +11 -8
- 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 +26 -9
- package/src/adapters/cursor/message-mapper.ts +3 -2
- package/src/adapters/cursor/protobuf-request.ts +8 -5
- package/src/adapters/cursor/request-builder.ts +21 -4
- package/src/adapters/cursor/thread-continuity.ts +105 -31
- package/src/adapters/cursor/tool-guidance.ts +5 -4
- package/src/adapters/cursor/transport.ts +19 -0
- package/src/adapters/cursor.ts +45 -2
- package/src/adapters/devin/cloud-direct/chat.ts +14 -3
- package/src/adapters/devin/cloud-direct/index.ts +7 -0
- package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +140 -0
- package/src/adapters/devin.ts +125 -26
- package/src/adapters/google-antigravity-replay.ts +1 -1
- package/src/adapters/google-antigravity-wire.ts +55 -4
- package/src/adapters/google-http.ts +57 -11
- package/src/adapters/google-tool-schema.ts +595 -31
- package/src/adapters/google-wire-compiler.ts +93 -10
- package/src/adapters/google-wire-shape.ts +461 -0
- package/src/adapters/google.ts +60 -10
- package/src/adapters/openai-chat/response-events.ts +61 -0
- package/src/adapters/openai-chat-images.ts +3 -1
- package/src/adapters/openai-chat.ts +10 -11
- package/src/adapters/openai-responses/image-gen.ts +8 -6
- package/src/adapters/openai-responses/passthrough.ts +25 -4
- package/src/adapters/openai-responses/reasoning.ts +7 -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/opencode-go-additional-tools.ts +12 -2
- package/src/adapters/responses-tool-schema.ts +76 -46
- package/src/adapters/run-turn-queue.ts +17 -4
- package/src/bridge/response-json.ts +1 -1
- package/src/bridge/sse.ts +28 -35
- package/src/claude/context-windows.ts +22 -0
- package/src/claude/outbound.ts +35 -4
- package/src/cli/account-api.ts +4 -3
- package/src/cli/account-extended.ts +26 -6
- 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/dispatch.ts +57 -3
- package/src/cli/doctor.ts +218 -4
- package/src/cli/help.ts +4 -1
- package/src/cli/hub.ts +3 -2
- package/src/cli/index.ts +95 -22
- package/src/cli/models-runtime.ts +33 -4
- package/src/cli/opencode.ts +2 -2
- package/src/cli/provider.ts +13 -1
- 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 +2 -0
- package/src/client/machine-api.ts +6 -5
- package/src/client/machine-listener.ts +16 -3
- package/src/client/runtime.ts +26 -2
- package/src/clients/config-export/constants.ts +2 -3
- package/src/clients/config-export.ts +5 -5
- package/src/codex/account-store.ts +146 -5
- package/src/codex/auth-api/account-list.ts +19 -11
- package/src/codex/auth-api/pool-quota-probe.ts +44 -10
- package/src/codex/auth-api/routes.ts +17 -2
- package/src/codex/auth-context.ts +16 -12
- 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 +22 -2
- package/src/codex/catalog/model-hints.ts +66 -33
- package/src/codex/catalog/parsing.ts +90 -5
- package/src/codex/catalog/provider-models.ts +19 -2
- package/src/codex/catalog/reserve-warn.ts +96 -0
- package/src/codex/catalog/retained-sync.ts +41 -26
- package/src/codex/catalog/routed-gather.ts +61 -3
- package/src/codex/cli-installation-identity.ts +210 -0
- package/src/codex/cli-installation-targets.ts +158 -0
- package/src/codex/context-compat.ts +5 -2
- package/src/codex/convergence.ts +5 -0
- package/src/codex/desired-state.ts +4 -1
- package/src/codex/history-job.ts +6 -6
- package/src/codex/history-provider.ts +24 -167
- package/src/codex/history-rollout-read.ts +174 -0
- package/src/codex/history-state-open.ts +105 -0
- package/src/codex/inject/config-toml.ts +44 -2
- package/src/codex/inject.ts +3 -2
- package/src/codex/internal/catalog-writer.ts +33 -1
- package/src/codex/lineage.ts +83 -32
- package/src/codex/loopback-target.ts +31 -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-cache.ts +47 -0
- package/src/codex/model-entitlements.ts +80 -2
- package/src/codex/observed-model-denials.ts +230 -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 +289 -16
- 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 +20 -16
- package/src/codex/shim.ts +1 -1
- package/src/codex/subagent-model-fallback.ts +114 -2
- package/src/codex/windows-installation-files.ts +224 -0
- package/src/combos/failover.ts +125 -5
- package/src/config/admitted-identity.ts +222 -0
- package/src/config/diagnostics.ts +43 -1
- package/src/config/feature-flags.ts +5 -0
- package/src/config/load-degrade.ts +33 -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 +31 -7
- package/src/config/schema/compaction-triggers.ts +11 -0
- package/src/config/schema/config-schema.ts +25 -0
- package/src/config/schema/leaf-validators.ts +76 -17
- package/src/config.ts +2 -2
- package/src/generated/compatibility-version.json +410 -258
- package/src/generated/model-metadata.ts +1 -1
- package/src/grok/reset-coupons.ts +38 -19
- package/src/images/loop.ts +6 -1
- package/src/integrations/aside-profile-context.ts +37 -3
- package/src/integrations/aside-profile-journal.ts +68 -3
- package/src/integrations/aside-profiles.ts +128 -3
- package/src/integrations/mutation-plan.ts +815 -0
- package/src/integrations/writer.ts +85 -99
- package/src/lab/live/transport.ts +4 -0
- package/src/lab/live/types.ts +5 -0
- package/src/lab/subject/behavior-fingerprint.ts +1 -1
- package/src/lib/admin-secrets.ts +9 -1
- package/src/lib/bounded-body.ts +4 -2
- package/src/lib/debug-log-buffer.ts +6 -1
- package/src/lib/debug.ts +23 -0
- package/src/lib/destination-policy.ts +48 -6
- package/src/lib/errors.ts +82 -15
- package/src/lib/http-response-semantics.ts +57 -0
- package/src/lib/lab-live-pinned-sender.ts +26 -12
- package/src/lib/local-destinations.ts +32 -5
- package/src/lib/pinned-http.ts +142 -2
- package/src/lib/plain-data.ts +103 -0
- package/src/lib/process-control.ts +13 -5
- package/src/lib/provider-outbound.ts +53 -5
- 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 +741 -0
- package/src/lib/spend-ledger-owner.ts +364 -0
- package/src/lib/spend-reservation-ledger.ts +332 -35
- package/src/lib/windows-system-proxy.ts +16 -11
- package/src/lib/workflow-budget.ts +145 -8
- package/src/oauth/account-quota-rank.ts +72 -15
- package/src/oauth/callback-server.ts +4 -3
- package/src/oauth/generic-account-failover.ts +41 -27
- package/src/oauth/health.ts +12 -1
- package/src/oauth/index.ts +3 -107
- package/src/oauth/login-flow-state.ts +127 -0
- 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 +34 -17
- package/src/providers/devin-cli-authmode-migration.ts +14 -10
- package/src/providers/devin-provider-merge-migration.ts +33 -12
- package/src/providers/free-directory.ts +20 -2
- package/src/providers/key-failover.ts +327 -25
- package/src/providers/model-rename-migration.ts +56 -1
- package/src/providers/model-rename-startup.ts +7 -5
- package/src/providers/openai-sidecar.ts +4 -0
- package/src/providers/openai-virtual-models.ts +42 -2
- package/src/providers/opencode-go-transport.ts +14 -5
- package/src/providers/quota/antigravity.ts +22 -2
- package/src/providers/quota/report-cache.ts +3 -0
- package/src/providers/quota/vendor-probes-key.ts +1 -1
- package/src/providers/registry/entries-core.ts +39 -17
- package/src/providers/registry/entries-extended.ts +131 -1
- package/src/providers/registry/model-ids.ts +168 -0
- package/src/providers/registry/model-seeds.ts +87 -21
- package/src/providers/registry/types.ts +2 -0
- package/src/providers/resolved-model-policy-merge.ts +167 -0
- package/src/providers/resolved-model-policy.ts +406 -0
- package/src/providers/stale-vision-classification-migration.ts +137 -0
- package/src/responses/apply-patch-envelope.ts +32 -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/freeform-wrapper-scan.ts +279 -0
- package/src/responses/hosted-tool-policy.ts +85 -2
- package/src/responses/legacy-dotted-tool-name-repair.ts +134 -0
- package/src/responses/progressive-freeform-input.ts +130 -0
- package/src/responses/reasoning-envelope.ts +30 -0
- package/src/responses/schema.ts +9 -2
- package/src/responses/state.ts +5 -12
- package/src/responses/tool-name-aliases.ts +15 -1
- package/src/router.ts +91 -115
- package/src/routing/compatibility/behavior.ts +9 -0
- package/src/routing/compatibility/subject.ts +16 -1
- package/src/server/adapter-resolve.ts +9 -0
- package/src/server/auth-cors.ts +29 -0
- package/src/server/chat-completions.ts +13 -5
- package/src/server/chat-native-sse.ts +26 -9
- package/src/server/chat-native.ts +10 -4
- package/src/server/claude-messages.ts +30 -5
- package/src/server/effort-row.ts +11 -3
- package/src/server/grok-responses-control-frame.ts +160 -1
- package/src/server/gui-static.ts +36 -2
- package/src/server/inbound-body-admission.ts +187 -0
- package/src/server/index/serve-options.ts +86 -29
- package/src/server/index/spend-ledger-lifecycle.ts +66 -0
- package/src/server/index/websocket-handler.ts +6 -1
- package/src/server/index.ts +24 -25
- package/src/server/management/api-access.ts +3 -4
- package/src/server/management/aside-profile-routes.ts +266 -7
- package/src/server/management/config-routes.ts +48 -7
- package/src/server/management/context.ts +3 -0
- package/src/server/management/integration-routes.ts +287 -5
- package/src/server/management/metrics-routes.ts +20 -0
- package/src/server/management/model-rows.ts +224 -12
- package/src/server/management/provider-capability-config.ts +35 -7
- package/src/server/management/provider-routes.ts +70 -18
- package/src/server/management/route-registry.ts +14 -0
- package/src/server/management/shared.ts +10 -3
- package/src/server/management/system-restart.ts +7 -2
- package/src/server/management/system-routes.ts +2 -0
- package/src/server/management/usage-aggregate-cache.ts +4 -0
- package/src/server/management-api.ts +2 -0
- package/src/server/management-auth.ts +15 -1
- package/src/server/proxy-liveness.ts +97 -2
- package/src/server/readiness.ts +29 -10
- package/src/server/relay-eager.ts +24 -2
- package/src/server/relay.ts +136 -34
- package/src/server/request-log.ts +80 -3
- package/src/server/request-metrics.ts +236 -0
- package/src/server/responses/adapter-continuation.ts +74 -30
- package/src/server/responses/adapter-delivery.ts +39 -8
- package/src/server/responses/adapter-dispatch.ts +63 -30
- package/src/server/responses/compact.ts +89 -18
- package/src/server/responses/compaction-routing.ts +111 -0
- package/src/server/responses/core-codex-account.ts +90 -24
- package/src/server/responses/core-combo.ts +7 -7
- package/src/server/responses/core-normalize.ts +16 -15
- package/src/server/responses/core-opaque-recovery.ts +1 -0
- package/src/server/responses/core-options.ts +4 -0
- package/src/server/responses/encrypted-payload.ts +20 -2
- package/src/server/responses/fetch-helpers.ts +68 -2
- package/src/server/responses/passthrough-delivery.ts +41 -15
- package/src/server/responses/passthrough-dispatch.ts +145 -53
- package/src/server/responses/passthrough-execution.ts +11 -1
- package/src/server/responses/policy-fallback.ts +5 -13
- package/src/server/responses/request-prepare.ts +96 -17
- package/src/server/responses/request-send-budget.ts +89 -8
- package/src/server/responses/request-sidecar-auth.ts +17 -9
- package/src/server/responses/request-spend.ts +38 -9
- package/src/server/responses/request-transport.ts +15 -12
- package/src/server/responses/run-turn-execution.ts +45 -8
- package/src/server/responses/sidecar-execution.ts +19 -2
- package/src/server/responses/ws-upstream.ts +16 -28
- package/src/server/responses-custom-tool-repair.ts +29 -56
- package/src/server/responses-undeclared-tool-guard.ts +31 -1
- package/src/server/sse-frame-buffer.ts +12 -10
- package/src/server/sse-payload-rewrite.ts +37 -10
- 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/service/cli.ts +16 -6
- package/src/service/guards.ts +10 -0
- package/src/service/health.ts +43 -0
- package/src/service/state.ts +7 -2
- package/src/tray/windows-tray.ps1 +155 -3
- package/src/types/accounts.ts +4 -0
- package/src/types/config.ts +111 -6
- package/src/types/provider.ts +37 -0
- package/src/types/request.ts +9 -1
- package/src/types/tools.ts +14 -0
- package/src/types/wire.ts +9 -1
- package/src/types.ts +1 -0
- package/src/usage/expected-prices.ts +28 -0
- package/src/usage/log.ts +87 -4
- package/src/vision/eligibility.ts +88 -9
- package/src/vision/plan.ts +34 -10
- package/src/web-search/executor.ts +41 -2
- package/src/web-search/loop.ts +6 -1
- package/src/web-search/passthrough-bridge.ts +39 -5
- package/gui/dist/assets/index-BbrHOIY0.js +0 -128
- package/gui/dist/assets/index-C5-RdDmD.css +0 -1
|
@@ -29,7 +29,7 @@ import {
|
|
|
29
29
|
} from "../claude/outbound";
|
|
30
30
|
import { clearableDeadline, idleDeadline } from "../lib/abort";
|
|
31
31
|
import { estimateTokens } from "../lib/token-estimate";
|
|
32
|
-
import { NoEligiblePolicyCandidateError, UnknownRoutingPolicyError, routeModel } from "../router";
|
|
32
|
+
import { captureRouteStaticPolicy, NoEligiblePolicyCandidateError, UnknownRoutingPolicyError, routeModel } from "../router";
|
|
33
33
|
import { evidenceFromBody } from "../routing/request-evidence";
|
|
34
34
|
import { resolveWireProtocolOverride } from "./adapter-resolve";
|
|
35
35
|
import type { OcxConfig } from "../types";
|
|
@@ -420,7 +420,7 @@ async function anthropicNativePassthrough(
|
|
|
420
420
|
// count_tokens too — counts must match what the real send will contain, and the 32MB
|
|
421
421
|
// body cap applies to it equally. Non-message bodies pass through untouched.
|
|
422
422
|
if (Array.isArray(body.messages)) {
|
|
423
|
-
await normalizeAnthropicImages(body.messages);
|
|
423
|
+
await normalizeAnthropicImages(body.messages, { abortSignal: req.signal });
|
|
424
424
|
enforceAnthropicImageLimits(body.messages);
|
|
425
425
|
}
|
|
426
426
|
const headers = new Headers();
|
|
@@ -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;
|
|
@@ -801,7 +817,10 @@ async function handleClaudeMessagesWithBudget(
|
|
|
801
817
|
const route = routeModel(config, internalBody.model as string, evidenceFromBody(internalBody));
|
|
802
818
|
// Settle the wire once so the sampling decision below reads the effective
|
|
803
819
|
// adapter rather than the provider-wide default (#404).
|
|
804
|
-
route.
|
|
820
|
+
route.staticPolicy = captureRouteStaticPolicy(
|
|
821
|
+
route.providerName, route.modelId, route.provider, route.staticPolicy.effectiveAlias, "anthropic",
|
|
822
|
+
);
|
|
823
|
+
route.provider = resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, "anthropic", route.staticPolicy);
|
|
805
824
|
logCtx.routeDecision = route.routeDecision;
|
|
806
825
|
if (route.provider.adapter === "openai-responses") {
|
|
807
826
|
delete internalBody.max_output_tokens;
|
|
@@ -815,7 +834,7 @@ async function handleClaudeMessagesWithBudget(
|
|
|
815
834
|
// accurate-usage adapters — the request-log merge is max(reported, estimate) and
|
|
816
835
|
// would overwrite real usage (audit 133 R1#7).
|
|
817
836
|
if (route.provider.adapter === "cursor" || route.provider.adapter === "kiro") {
|
|
818
|
-
logCtx.usageLogInputTokens =
|
|
837
|
+
logCtx.usageLogInputTokens = claudeRequestTokenFloor();
|
|
819
838
|
}
|
|
820
839
|
// Effort safety valve (devlog 136 B6, audit 139 R2#2): opus-shaped aliases make
|
|
821
840
|
// every routed model look like a reasoning model to Claude clients, so a forced
|
|
@@ -988,7 +1007,13 @@ async function handleClaudeMessagesWithBudget(
|
|
|
988
1007
|
|
|
989
1008
|
const contentType = response.headers.get("content-type") ?? "";
|
|
990
1009
|
if (contentType.includes("text/event-stream") && response.body) {
|
|
991
|
-
const anthropicSse = responsesSseToAnthropicSse(response.body, requestedModel, {
|
|
1010
|
+
const anthropicSse = responsesSseToAnthropicSse(response.body, requestedModel, {
|
|
1011
|
+
translatorBudget,
|
|
1012
|
+
// Only a floor, and only for the first frame: an upstream that reports usage early wins
|
|
1013
|
+
// over it inside the translator, and the terminal `message_delta` carries the
|
|
1014
|
+
// authoritative count either way (#4857).
|
|
1015
|
+
inputTokenFloor: claudeRequestTokenFloor(),
|
|
1016
|
+
});
|
|
992
1017
|
if (stream) {
|
|
993
1018
|
return new Response(anthropicSse, {
|
|
994
1019
|
status: 200,
|
package/src/server/effort-row.ts
CHANGED
|
@@ -30,7 +30,9 @@ export interface EffortRowOptions {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
export function isKnownId(knownIds: EffortRowKnownIds | undefined, id: string): boolean {
|
|
33
|
-
return typeof knownIds === "function"
|
|
33
|
+
return typeof knownIds === "function"
|
|
34
|
+
? knownIds(id)
|
|
35
|
+
: knownIds?.has(id) === true || knownIds?.has(id.toLowerCase()) === true;
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
export function effortRowId(baseId: string, effort: string): string {
|
|
@@ -43,6 +45,12 @@ export function effortRowId(baseId: string, effort: string): string {
|
|
|
43
45
|
*/
|
|
44
46
|
export function knownEffortRowIds(config: OcxConfig): Set<string> {
|
|
45
47
|
const ids = new Set<string>();
|
|
48
|
+
const addAlias = (id: string): void => {
|
|
49
|
+
ids.add(id);
|
|
50
|
+
// Model aliases are resolved case-insensitively. Preserve that contract here so an
|
|
51
|
+
// alias cannot be mistaken for a synthetic effort row before ordinary routing runs.
|
|
52
|
+
ids.add(id.toLowerCase());
|
|
53
|
+
};
|
|
46
54
|
for (const [providerName, provider] of Object.entries(config.providers)) {
|
|
47
55
|
const known = knownModelIdsForProvider(providerName, provider, config);
|
|
48
56
|
const namespaces = [providerName, provider.alias].filter((value): value is string => (
|
|
@@ -54,8 +62,8 @@ export function knownEffortRowIds(config: OcxConfig): Set<string> {
|
|
|
54
62
|
for (const namespace of namespaces) ids.add(`${namespace}/${id}`);
|
|
55
63
|
}
|
|
56
64
|
for (const alias of Object.values(provider.modelAliases ?? {})) {
|
|
57
|
-
|
|
58
|
-
for (const namespace of namespaces)
|
|
65
|
+
addAlias(alias);
|
|
66
|
+
for (const namespace of namespaces) addAlias(`${namespace}/${alias}`);
|
|
59
67
|
}
|
|
60
68
|
}
|
|
61
69
|
for (const [id, combo] of Object.entries(config.combos ?? {})) {
|
|
@@ -1,10 +1,137 @@
|
|
|
1
|
-
import { sseDataPayload, type SseBlockRewrite } from "./sse-payload-rewrite";
|
|
1
|
+
import { replaceSseDataPayload, sseDataPayload, type SseBlockRewrite } from "./sse-payload-rewrite";
|
|
2
2
|
|
|
3
3
|
const GROK_CONTROL_FRAME_TYPES: Record<string, true> = {
|
|
4
4
|
"codex.rate_limits": true,
|
|
5
5
|
"codex.response.metadata": true,
|
|
6
6
|
};
|
|
7
7
|
|
|
8
|
+
const GROK_RESPONSE_INTEGER_TIMESTAMP_FIELDS = ["created_at", "completed_at"] as const;
|
|
9
|
+
|
|
10
|
+
type JsonRange = { start: number; end: number };
|
|
11
|
+
|
|
12
|
+
function skipJsonWhitespace(payload: string, index: number): number {
|
|
13
|
+
while (index < payload.length && /[\t\n\r ]/.test(payload[index] ?? "")) index += 1;
|
|
14
|
+
return index;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function findJsonStringEnd(payload: string, start: number): number | undefined {
|
|
18
|
+
for (let index = start + 1; index < payload.length; index += 1) {
|
|
19
|
+
if (payload[index] === "\\") index += 1;
|
|
20
|
+
else if (payload[index] === '"') return index + 1;
|
|
21
|
+
}
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function findJsonValueEnd(payload: string, start: number): number | undefined {
|
|
26
|
+
if (payload[start] === '"') return findJsonStringEnd(payload, start);
|
|
27
|
+
if (payload[start] !== "{" && payload[start] !== "[") {
|
|
28
|
+
let index = start;
|
|
29
|
+
while (index < payload.length && !",}]".includes(payload[index] ?? "")) index += 1;
|
|
30
|
+
return index;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let depth = 0;
|
|
34
|
+
let inString = false;
|
|
35
|
+
for (let index = start; index < payload.length; index += 1) {
|
|
36
|
+
const char = payload[index];
|
|
37
|
+
if (inString) {
|
|
38
|
+
if (char === "\\") index += 1;
|
|
39
|
+
else if (char === '"') inString = false;
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (char === '"') inString = true;
|
|
43
|
+
else if (char === "{" || char === "[") depth += 1;
|
|
44
|
+
else if (char === "}" || char === "]") {
|
|
45
|
+
depth -= 1;
|
|
46
|
+
if (depth === 0) return index + 1;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function findResponseObjectRange(payload: string): JsonRange | null {
|
|
53
|
+
let cursor = skipJsonWhitespace(payload, 0);
|
|
54
|
+
if (payload[cursor] !== "{") return null;
|
|
55
|
+
cursor += 1;
|
|
56
|
+
let responseRange: JsonRange | null = null;
|
|
57
|
+
|
|
58
|
+
while (cursor < payload.length) {
|
|
59
|
+
cursor = skipJsonWhitespace(payload, cursor);
|
|
60
|
+
if (payload[cursor] === "}") return responseRange;
|
|
61
|
+
if (payload[cursor] !== '"') return null;
|
|
62
|
+
const keyEnd = findJsonStringEnd(payload, cursor);
|
|
63
|
+
if (keyEnd === undefined) return null;
|
|
64
|
+
let key: unknown;
|
|
65
|
+
try {
|
|
66
|
+
key = JSON.parse(payload.slice(cursor, keyEnd));
|
|
67
|
+
} catch {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
cursor = skipJsonWhitespace(payload, keyEnd);
|
|
72
|
+
if (payload[cursor] !== ":") return null;
|
|
73
|
+
const valueStart = skipJsonWhitespace(payload, cursor + 1);
|
|
74
|
+
const valueEnd = findJsonValueEnd(payload, valueStart);
|
|
75
|
+
if (valueEnd === undefined) return null;
|
|
76
|
+
if (key === "response" && payload[valueStart] === "{") {
|
|
77
|
+
responseRange = { start: valueStart, end: valueEnd };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
cursor = skipJsonWhitespace(payload, valueEnd);
|
|
81
|
+
if (payload[cursor] === "}") return responseRange;
|
|
82
|
+
if (payload[cursor] !== ",") return null;
|
|
83
|
+
cursor += 1;
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function rewriteIntegralTimestamp(payload: string, field: string, value: number, event: unknown): string | null {
|
|
89
|
+
const responseRange = findResponseObjectRange(payload);
|
|
90
|
+
if (responseRange === null) return null;
|
|
91
|
+
let cursor = responseRange.start + 1;
|
|
92
|
+
let candidate: { start: number; length: number } | null = null;
|
|
93
|
+
while (cursor < responseRange.end) {
|
|
94
|
+
cursor = skipJsonWhitespace(payload, cursor);
|
|
95
|
+
if (payload[cursor] === "}") break;
|
|
96
|
+
if (payload[cursor] !== '"') return null;
|
|
97
|
+
const keyEnd = findJsonStringEnd(payload, cursor);
|
|
98
|
+
if (keyEnd === undefined) return null;
|
|
99
|
+
let key: unknown;
|
|
100
|
+
try {
|
|
101
|
+
key = JSON.parse(payload.slice(cursor, keyEnd));
|
|
102
|
+
} catch {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
cursor = skipJsonWhitespace(payload, keyEnd);
|
|
106
|
+
if (payload[cursor] !== ":") return null;
|
|
107
|
+
const valueStart = skipJsonWhitespace(payload, cursor + 1);
|
|
108
|
+
const valueEnd = findJsonValueEnd(payload, valueStart);
|
|
109
|
+
if (valueEnd === undefined || valueEnd > responseRange.end) return null;
|
|
110
|
+
const valueToken = payload.slice(valueStart, valueEnd).trimEnd();
|
|
111
|
+
if (
|
|
112
|
+
key === field &&
|
|
113
|
+
/^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?$/.test(valueToken) &&
|
|
114
|
+
valueToken !== String(value) &&
|
|
115
|
+
Number(valueToken) === value
|
|
116
|
+
) {
|
|
117
|
+
if (candidate !== null) return null;
|
|
118
|
+
candidate = { start: valueStart, length: valueToken.length };
|
|
119
|
+
}
|
|
120
|
+
cursor = skipJsonWhitespace(payload, valueEnd);
|
|
121
|
+
if (payload[cursor] === "}") break;
|
|
122
|
+
if (payload[cursor] !== ",") return null;
|
|
123
|
+
cursor += 1;
|
|
124
|
+
}
|
|
125
|
+
if (candidate === null) return payload;
|
|
126
|
+
|
|
127
|
+
const rewritten = `${payload.slice(0, candidate.start)}${value}${payload.slice(candidate.start + candidate.length)}`;
|
|
128
|
+
try {
|
|
129
|
+
return JSON.stringify(JSON.parse(rewritten)) === JSON.stringify(event) ? rewritten : null;
|
|
130
|
+
} catch {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
8
135
|
/**
|
|
9
136
|
* Hide Codex-only control frames from Grok's strict Responses decoder.
|
|
10
137
|
*
|
|
@@ -41,3 +168,35 @@ export function createGrokResponsesControlFrameBlockRewrite(): SseBlockRewrite {
|
|
|
41
168
|
: [block];
|
|
42
169
|
};
|
|
43
170
|
}
|
|
171
|
+
|
|
172
|
+
/** Normalize safe integer response timestamps for Grok's strict Responses decoder. */
|
|
173
|
+
export function createGrokResponsesTimestampBlockRewrite(): SseBlockRewrite {
|
|
174
|
+
return (block) => {
|
|
175
|
+
const payload = sseDataPayload(block);
|
|
176
|
+
if (payload === null || payload === "[DONE]") return [block];
|
|
177
|
+
|
|
178
|
+
let event: unknown;
|
|
179
|
+
try {
|
|
180
|
+
event = JSON.parse(payload);
|
|
181
|
+
} catch {
|
|
182
|
+
return [block];
|
|
183
|
+
}
|
|
184
|
+
if (!event || typeof event !== "object" || Array.isArray(event)) return [block];
|
|
185
|
+
const eventRecord = event as Record<string, unknown>;
|
|
186
|
+
if (typeof eventRecord.type !== "string" || !eventRecord.type.startsWith("response.")) return [block];
|
|
187
|
+
if (!eventRecord.response || typeof eventRecord.response !== "object" || Array.isArray(eventRecord.response)) {
|
|
188
|
+
return [block];
|
|
189
|
+
}
|
|
190
|
+
const response = eventRecord.response as Record<string, unknown>;
|
|
191
|
+
let rewrittenPayload = payload;
|
|
192
|
+
for (const field of GROK_RESPONSE_INTEGER_TIMESTAMP_FIELDS) {
|
|
193
|
+
const value = response[field];
|
|
194
|
+
if (value === undefined) continue;
|
|
195
|
+
if (typeof value !== "number" || value < 0 || !Number.isSafeInteger(value)) return [block];
|
|
196
|
+
const rewritten = rewriteIntegralTimestamp(rewrittenPayload, field, value, event);
|
|
197
|
+
if (rewritten === null) return [block];
|
|
198
|
+
rewrittenPayload = rewritten;
|
|
199
|
+
}
|
|
200
|
+
return rewrittenPayload === payload ? [block] : [replaceSseDataPayload(block, rewrittenPayload)];
|
|
201
|
+
};
|
|
202
|
+
}
|
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
|
+
}
|