@bitkyc08/opencodex 2.58.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 +1 -1
- package/src/adapters/base.ts +11 -1
- package/src/adapters/cursor/catalog.ts +11 -0
- package/src/adapters/cursor/effort-map.ts +16 -2
- package/src/adapters/cursor/envelope-echo.ts +55 -2
- 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 +14 -3
- package/src/adapters/cursor/thread-continuity.ts +105 -31
- package/src/adapters/cursor/tool-guidance.ts +5 -4
- package/src/adapters/cursor.ts +42 -1
- 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 +8 -1
- package/src/adapters/google.ts +23 -4
- 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 +10 -1
- package/src/adapters/openai-responses/tool-output-recovery.ts +75 -0
- package/src/adapters/openai-responses/tool-schema.ts +19 -7
- 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 +165 -24
- 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 +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/dispatch.ts +54 -0
- package/src/cli/doctor.ts +197 -2
- package/src/cli/help.ts +4 -1
- package/src/cli/index.ts +88 -20
- 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 +2 -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 +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 +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/history-provider.ts +4 -1
- 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/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-entitlements.ts +60 -1
- 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/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 +21 -0
- package/src/config/schema/leaf-validators.ts +64 -17
- package/src/generated/compatibility-version.json +235 -163
- package/src/generated/model-metadata.ts +1 -1
- package/src/lib/bounded-body.ts +4 -2
- 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/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/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-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-extended.ts +96 -0
- package/src/providers/registry/model-seeds.ts +78 -21
- 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/server/auth-cors.ts +26 -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.ts +15 -19
- package/src/server/management/api-access.ts +3 -4
- package/src/server/management/config-routes.ts +31 -6
- package/src/server/management/provider-capability-config.ts +35 -7
- package/src/server/management/provider-routes.ts +70 -18
- 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/compact.ts +60 -11
- package/src/server/responses/core-codex-account.ts +83 -22
- package/src/server/responses/core-normalize.ts +12 -5
- package/src/server/responses/fetch-helpers.ts +68 -2
- package/src/server/responses/passthrough-delivery.ts +10 -1
- package/src/server/responses/passthrough-dispatch.ts +113 -48
- package/src/server/responses/passthrough-execution.ts +11 -1
- package/src/server/responses/request-prepare.ts +29 -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 +2 -1
- 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/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/types/accounts.ts +4 -0
- package/src/types/config.ts +100 -3
- package/src/types/provider.ts +19 -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-BbrHOIY0.js +0 -128
package/src/server/auth-cors.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { providerRelativeSendPathConfigError } from "../config/provider-relative-send-path";
|
|
1
2
|
import { modelCapabilitiesConfigError } from "../config/provider-validation";
|
|
2
3
|
import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
|
|
3
4
|
import { initialModelSelection } from "../providers/initial-model-selection";
|
|
@@ -29,6 +30,7 @@ import {
|
|
|
29
30
|
} from "../config/provider-validation";
|
|
30
31
|
import { providerDestinationConfigError } from "../lib/destination-policy";
|
|
31
32
|
import { redactSecretString } from "../lib/redact";
|
|
33
|
+
import { DECLARABLE_HOSTED_TOOL_TYPES } from "../responses/hosted-tool-policy";
|
|
32
34
|
import { effectiveGoogleMode, getProviderRegistryEntry, providerCodexAccountMode, providerMatchesRegistryTransport, registryEntryForProviderDestination } from "../providers/registry";
|
|
33
35
|
import { providerConfigSeed } from "../providers/derive";
|
|
34
36
|
import type { OcxConfig, OcxProviderConfig } from "../types";
|
|
@@ -754,6 +756,10 @@ export function providerManagementConfigError(
|
|
|
754
756
|
}
|
|
755
757
|
const destinationError = providerDestinationConfigError(name, typed);
|
|
756
758
|
if (destinationError) return `provider ${name} ${destinationError}`;
|
|
759
|
+
for (const field of ["responsesPath", "chatCompletionsPath"] as const) {
|
|
760
|
+
const sendPathError = providerRelativeSendPathConfigError(field, raw[field]);
|
|
761
|
+
if (sendPathError) return `provider ${JSON.stringify(redactSecretString(name))} ${sendPathError}`;
|
|
762
|
+
}
|
|
757
763
|
const headersError = providerHeadersConfigError(typed.headers);
|
|
758
764
|
if (headersError) return `provider ${name} ${headersError}`;
|
|
759
765
|
const retryOn429Error = retryOn429PolicyConfigError(raw.retryOn429);
|
|
@@ -793,6 +799,8 @@ export function providerManagementConfigError(
|
|
|
793
799
|
}
|
|
794
800
|
const reasoningSummariesError = booleanRecordConfigError(raw.modelSupportsReasoningSummaries, "modelSupportsReasoningSummaries");
|
|
795
801
|
if (reasoningSummariesError) return `provider ${name} ${reasoningSummariesError}`;
|
|
802
|
+
const suppressSyntheticMaxError = booleanRecordConfigError(raw.modelSuppressSyntheticMax, "modelSuppressSyntheticMax");
|
|
803
|
+
if (suppressSyntheticMaxError) return `provider ${name} ${suppressSyntheticMaxError}`;
|
|
796
804
|
const reasoningSummaryDeliveryError = reasoningSummaryDeliveryRecordConfigError(
|
|
797
805
|
raw.modelReasoningSummaryDelivery,
|
|
798
806
|
raw.modelSupportsReasoningSummaries,
|
|
@@ -834,6 +842,22 @@ export function providerManagementConfigError(
|
|
|
834
842
|
"omitReasoningEffortWithToolsModels",
|
|
835
843
|
);
|
|
836
844
|
if (toolReasoningOptOutError) return `provider ${name} ${toolReasoningOptOutError}`;
|
|
845
|
+
const unsupportedHostedToolsError = nonBlankStringArrayConfigError(
|
|
846
|
+
raw.unsupportedHostedTools,
|
|
847
|
+
"unsupportedHostedTools",
|
|
848
|
+
);
|
|
849
|
+
if (unsupportedHostedToolsError) return `provider ${name} ${unsupportedHostedToolsError}`;
|
|
850
|
+
if (Array.isArray(raw.unsupportedHostedTools)) {
|
|
851
|
+
// Closed vocabulary, same reason as the config schema: an unrecognized name would be
|
|
852
|
+
// stored and then strip nothing, so the operator would keep getting the upstream 400
|
|
853
|
+
// this field exists to prevent.
|
|
854
|
+
const unknownTool = (raw.unsupportedHostedTools as unknown[])
|
|
855
|
+
.find(tool => typeof tool === "string" && !DECLARABLE_HOSTED_TOOL_TYPES.has(tool.trim()));
|
|
856
|
+
if (unknownTool !== undefined) {
|
|
857
|
+
return `provider ${name} unsupportedHostedTools must name only hosted tool types: `
|
|
858
|
+
+ `${[...DECLARABLE_HOSTED_TOOL_TYPES].join(", ")}`;
|
|
859
|
+
}
|
|
860
|
+
}
|
|
837
861
|
const openRouterError = openRouterRoutingConfigError(typed);
|
|
838
862
|
if (openRouterError) return `provider ${name} ${openRouterError}`;
|
|
839
863
|
const vercelError = vercelGatewayRoutingConfigError(typed);
|
|
@@ -964,6 +988,7 @@ const PROVIDER_CONFIG_FIELD_POLICY = {
|
|
|
964
988
|
refreshPolicy: "editor",
|
|
965
989
|
reasoningEfforts: "editor",
|
|
966
990
|
modelReasoningEfforts: "editor",
|
|
991
|
+
modelSuppressSyntheticMax: "editor",
|
|
967
992
|
modelDefaultReasoningEfforts: "editor",
|
|
968
993
|
pinnedReasoningEffort: "editor",
|
|
969
994
|
modelPinnedReasoningEfforts: "editor",
|
|
@@ -979,6 +1004,7 @@ const PROVIDER_CONFIG_FIELD_POLICY = {
|
|
|
979
1004
|
xaiResponsesDefaultVersion: "runtime",
|
|
980
1005
|
zaiResponsesDefaultVersion: "runtime",
|
|
981
1006
|
supportsResponsesCustomTools: "editor",
|
|
1007
|
+
unsupportedHostedTools: "editor",
|
|
982
1008
|
responsesSnapshotRepair: "editor",
|
|
983
1009
|
webSearchBridge: "editor",
|
|
984
1010
|
reasoningEffortMap: "editor",
|
|
@@ -150,10 +150,15 @@ async function handleChatCompletionsWithBudget(
|
|
|
150
150
|
let chatNativeRoute: ReturnType<typeof routeModel> | null = null;
|
|
151
151
|
try {
|
|
152
152
|
const route = routeModel(config, chatBody.model as string, evidenceFromBody(chatBody));
|
|
153
|
-
|
|
154
|
-
//
|
|
155
|
-
|
|
156
|
-
|
|
153
|
+
// Preserve the routed destination for Go recognition, then settle the wire before
|
|
154
|
+
// deriving protocol-scoped affinity. Recognition must not inspect the flipped adapter.
|
|
155
|
+
const routedProvider = route.provider;
|
|
156
|
+
const wireProvider = resolveWireProtocolOverride(route.providerName, route.modelId, routedProvider, "chat");
|
|
157
|
+
route.provider = resolveOpenCodeGoTransport(
|
|
158
|
+
wireProvider,
|
|
159
|
+
getOrAllocateRequestSessionLane(req),
|
|
160
|
+
routedProvider,
|
|
161
|
+
);
|
|
157
162
|
logCtx.model = route.modelId;
|
|
158
163
|
logCtx.providerAdapter = route.provider.adapter;
|
|
159
164
|
logCtx.requestedModel = requestedModel;
|
|
@@ -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
|
+
}
|
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
|