@bitkyc08/opencodex 2.65.0-preview.20260925 → 2.66.0-preview.20260925
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 +14 -0
- package/gui/dist/assets/App-BVjA0T6O.js +51 -0
- package/gui/dist/assets/App-gkoJIEOV.css +1 -0
- package/gui/dist/assets/{Tray-DUvc_Wul.js → Tray-BCFtiWwD.js} +1 -1
- package/gui/dist/assets/index-ComVsdSk.css +1 -0
- package/gui/dist/assets/index-vO4dgfiU.js +86 -0
- package/gui/dist/assets/{tray-data-f0lTZ4sF.js → tray-data-C4FiNaxk.js} +1 -1
- package/gui/dist/index.html +2 -2
- package/package.json +1 -2
- package/src/adapters/anthropic/beta-allowlist.ts +80 -0
- package/src/adapters/anthropic/passthrough.ts +221 -0
- package/src/adapters/anthropic-image-codec.ts +56 -19
- package/src/adapters/anthropic-image-normalize.ts +20 -10
- package/src/adapters/anthropic.ts +55 -24
- package/src/adapters/coding-agent/protocol.ts +29 -19
- package/src/adapters/cursor/live-transport.ts +18 -3
- package/src/adapters/cursor/native-exec-shell.ts +18 -63
- package/src/adapters/cursor/native-exec.ts +5 -2
- package/src/adapters/cursor/native-foreground-shell.ts +44 -0
- package/src/adapters/devin/cloud-direct/chat.ts +7 -1
- package/src/adapters/exec-tool-result-normalize.ts +4 -1
- package/src/adapters/google.ts +12 -0
- package/src/adapters/ollama-native.ts +32 -1
- package/src/adapters/openai-chat/serialized-tool-call-content.ts +131 -34
- package/src/adapters/openai-chat.ts +6 -4
- package/src/adapters/openai-responses/passthrough.ts +1 -0
- package/src/adapters/openai-responses/reasoning.ts +45 -1
- package/src/bridge/response-json.ts +10 -2
- package/src/chat/outbound.ts +104 -52
- package/src/claude/claude-code-block.ts +16 -0
- package/src/claude/context-windows.ts +52 -4
- package/src/claude/desktop-first-party.ts +25 -8
- package/src/claude/inbound.ts +22 -0
- package/src/claude/intercept/connect-proxy.ts +37 -4
- package/src/claude/intercept/proxy-auth.ts +166 -0
- package/src/claude/intercept/runtime.ts +21 -0
- package/src/claude/intercept/settings.ts +33 -12
- package/src/claude/outbound.ts +45 -27
- package/src/cli/account-extended.ts +47 -15
- package/src/cli/account-target.ts +115 -0
- package/src/cli/account.ts +20 -28
- package/src/cli/agent.ts +10 -1
- package/src/cli/api-protocols.ts +206 -0
- package/src/cli/capabilities.ts +86 -0
- package/src/cli/combo.ts +2 -2
- package/src/cli/connect.ts +127 -3
- package/src/cli/dispatch.ts +8 -0
- package/src/cli/doctor.ts +33 -0
- package/src/cli/help.ts +2 -0
- package/src/cli/link.ts +288 -0
- package/src/cli/registry.ts +23 -0
- package/src/cli/runtime-api.ts +79 -0
- package/src/cli/system-command.ts +21 -2
- package/src/client/connect.ts +125 -23
- package/src/client/hub-relay.ts +12 -8
- package/src/client/link-join.ts +323 -0
- package/src/client/link-relay.ts +241 -0
- package/src/client/link-state.ts +110 -0
- package/src/client/link-teardown.ts +63 -0
- package/src/client/link-tunnel.ts +465 -0
- package/src/client/machine-listener.ts +14 -5
- package/src/client/runtime.ts +91 -42
- package/src/client/state.ts +4 -0
- package/src/codex/catalog/build-entries.ts +4 -0
- package/src/codex/catalog/derive-entry.ts +5 -0
- package/src/codex/catalog/parsing.ts +4 -0
- package/src/codex/catalog/retained-sync.ts +4 -1
- package/src/codex/desktop-switches.ts +88 -8
- package/src/codex/inject/plan.ts +16 -3
- package/src/codex/inject.ts +3 -0
- package/src/codex/internal/catalog-writer.ts +13 -2
- package/src/combos/failover.ts +3 -2
- package/src/combos/index.ts +12 -0
- package/src/combos/jev.ts +646 -0
- package/src/combos/request.ts +5 -0
- package/src/combos/resolve.ts +17 -15
- package/src/combos/types.ts +47 -3
- package/src/config/atomic-write.ts +3 -0
- package/src/config/live-reconcile.ts +20 -4
- package/src/config/persist-unlocked.ts +20 -7
- package/src/config/schema/config-schema.ts +16 -0
- package/src/config/schema/leaf-validators.ts +38 -1
- package/src/generated/compatibility-version.json +416 -128
- package/src/integrations/omp-yaml-source.ts +109 -4
- package/src/lab/automation/orchestrator.ts +60 -4
- package/src/lab/events/limits.ts +1 -1
- package/src/lib/bounded-body.ts +55 -38
- package/src/lib/config-ownership.ts +11 -22
- package/src/lib/local-desktop-snapshot-capability.ts +56 -0
- package/src/lib/local-management-capability.ts +25 -3
- package/src/lib/optional-shutdown-hooks.ts +17 -0
- package/src/lib/state-store-registrations.ts +2 -2
- package/src/lib/windows-elevation.ts +62 -25
- package/src/lib/windows-secret-acl.ts +114 -23
- package/src/link/admission-wait.ts +73 -0
- package/src/link/compensation.ts +100 -0
- package/src/link/fingerprint.ts +21 -0
- package/src/link/paths.ts +19 -0
- package/src/link/ports.ts +12 -0
- package/src/link/routes.ts +32 -0
- package/src/link/ssh-argv.ts +170 -0
- package/src/link/ssh-config.ts +152 -0
- package/src/link/ssh-runner.ts +161 -0
- package/src/link/status-projection.ts +96 -0
- package/src/link/store.ts +139 -0
- package/src/link/supervisor.ts +404 -0
- package/src/link/tunnel-state.ts +91 -0
- package/src/protocols/baseline.ts +88 -0
- package/src/protocols/codecs/chat.ts +17 -0
- package/src/protocols/codecs/messages.ts +18 -0
- package/src/protocols/codecs/responses.ts +17 -0
- package/src/protocols/contract.ts +185 -0
- package/src/protocols/dto.ts +267 -0
- package/src/protocols/encoders/adapter-events.ts +876 -0
- package/src/protocols/encoders/chat.ts +243 -0
- package/src/protocols/encoders/messages.ts +462 -0
- package/src/protocols/envelope.ts +43 -0
- package/src/protocols/features.ts +295 -0
- package/src/protocols/guard.ts +33 -0
- package/src/protocols/opaque-state.ts +132 -0
- package/src/protocols/path.ts +39 -0
- package/src/protocols/plan-snapshot.ts +226 -0
- package/src/protocols/plan.ts +192 -0
- package/src/protocols/provider-summary.ts +101 -0
- package/src/protocols/settings.ts +118 -0
- package/src/protocols/shadow-plan.ts +36 -0
- package/src/protocols/shadow.ts +62 -0
- package/src/protocols/trace.ts +335 -0
- package/src/providers/model-rename-migration.ts +3 -3
- package/src/providers/registry/entries-core.ts +6 -1
- package/src/providers/registry/entries-extended.ts +15 -0
- package/src/providers/registry/model-ids.ts +1 -0
- package/src/providers/registry/types.ts +6 -0
- package/src/providers/registry.ts +1 -1
- package/src/remote-control/workspace-hub.ts +5 -4
- package/src/responses/apply-patch-envelope.ts +6 -1
- package/src/responses/code-mode-shell-input.ts +4 -2
- package/src/responses/freeform-wrapper-scan.ts +51 -24
- package/src/responses/progressive-freeform-input.ts +14 -20
- package/src/responses/spill-store.ts +187 -33
- package/src/responses/state/snapshot-select.ts +49 -0
- package/src/responses/state/spill-inspect.ts +46 -0
- package/src/responses/state/spill-queue.ts +16 -0
- package/src/responses/state.ts +51 -27
- package/src/server/audio-client.ts +8 -3
- package/src/server/audio-upstream.ts +9 -4
- package/src/server/auth-cors.ts +25 -8
- package/src/server/chat-completions.ts +71 -11
- package/src/server/chat-native-eligibility.ts +74 -0
- package/src/server/chat-native.ts +139 -74
- package/src/server/claude-messages.ts +140 -22
- package/src/server/hub-usage.ts +4 -3
- package/src/server/index/link-listener.ts +195 -0
- package/src/server/index/optional-listeners.ts +115 -0
- package/src/server/index/serve-options.ts +44 -10
- package/src/server/index.ts +10 -9
- package/src/server/inference/attempt.ts +51 -0
- package/src/server/inference/client-encoder-delivery.ts +235 -0
- package/src/server/inference/client-wire-log.ts +49 -0
- package/src/server/inference/client-wire.ts +69 -0
- package/src/server/inference/context.ts +15 -0
- package/src/server/inference/final-log.ts +37 -0
- package/src/server/local-desktop-snapshot-auth.ts +57 -0
- package/src/server/management/agent-settings-routes.ts +15 -13
- package/src/server/management/api-access.ts +11 -1
- package/src/server/management/config-routes.ts +3 -6
- package/src/server/management/context.ts +25 -0
- package/src/server/management/link-routes.ts +535 -0
- package/src/server/management/logs-usage-routes.ts +31 -1
- package/src/server/management/native-integration-routes.ts +6 -11
- package/src/server/management/oauth-account-routes.ts +42 -16
- package/src/server/management/protocol-routes.ts +159 -0
- package/src/server/management/protocol-settings-patch.ts +145 -0
- package/src/server/management/provider-patch-transaction.ts +61 -0
- package/src/server/management/provider-routes.ts +39 -18
- package/src/server/management/route-registry.ts +12 -0
- package/src/server/management/sidebar-routes.ts +8 -3
- package/src/server/management/system-routes.ts +18 -1
- package/src/server/management/usage-aggregate-cache.ts +206 -6
- package/src/server/management-api.ts +26 -1
- package/src/server/management-auth.ts +19 -5
- package/src/server/messages-native-eligibility.ts +161 -0
- package/src/server/messages-native-oauth.ts +149 -0
- package/src/server/messages-native.ts +790 -0
- package/src/server/relay.ts +9 -0
- package/src/server/request-log-filter.ts +92 -0
- package/src/server/request-log.ts +31 -70
- package/src/server/responses/adapter-delivery.ts +43 -16
- package/src/server/responses/agent-task-recovery.ts +90 -22
- package/src/server/responses/codex-ws-exchange.ts +4 -0
- package/src/server/responses/codex-ws-wire.ts +10 -1
- package/src/server/responses/core-combo-native.ts +323 -0
- package/src/server/responses/core-combo.ts +224 -13
- package/src/server/responses/core-options.ts +22 -0
- package/src/server/responses/core.ts +14 -26
- package/src/server/responses/request-sidecar-auth.ts +8 -3
- package/src/server/responses/run-turn-execution.ts +257 -63
- package/src/server/responses/sidecar-execution.ts +7 -4
- package/src/server/system-env.ts +7 -0
- package/src/service/diagnostics.ts +7 -1
- package/src/service/launchd.ts +26 -37
- package/src/service/windows-ops.ts +24 -20
- package/src/types/config.ts +58 -1
- package/src/types.ts +2 -0
- package/src/update/transactional-install.d.mts +0 -1
- package/src/update/transactional-install.mjs +17 -17
- package/src/usage/jev-stats.ts +495 -0
- package/src/usage/log.ts +24 -3
- package/src/web-search/run-turn-loop.ts +568 -0
- package/gui/dist/assets/App-8NMiZxT0.css +0 -1
- package/gui/dist/assets/App-CsYvvpr3.js +0 -50
- package/gui/dist/assets/index--EWgGQvZ.css +0 -1
- package/gui/dist/assets/index-BB0iHG8-.js +0 -86
package/src/responses/state.ts
CHANGED
|
@@ -6,16 +6,23 @@ import { windowsSecretAclApplies } from "../lib/windows-secret-acl";
|
|
|
6
6
|
import type { OcxProviderContinuationState } from "../types";
|
|
7
7
|
import {
|
|
8
8
|
deleteResponseSpill,
|
|
9
|
+
inspectResponseSpillDir,
|
|
9
10
|
noteStubSwapForTest,
|
|
10
11
|
readResponseSpill,
|
|
11
12
|
recoverOrphanedResponseSpills,
|
|
13
|
+
resetPeriodicSpillSweepCursorForTests,
|
|
12
14
|
responseSpillDirectory,
|
|
13
15
|
responseSpillPayloadCap,
|
|
16
|
+
sweepOrphanedResponseSpillsPeriodically,
|
|
17
|
+
type ResponseSpillDirInspection,
|
|
14
18
|
type ResponseSpillRef,
|
|
15
19
|
writeResponseSpillDurably,
|
|
16
20
|
} from "./spill-store";
|
|
21
|
+
import { collectReferencedSpillFileNames, snapshotReferencedSpillFileNames } from "./state/spill-inspect";
|
|
22
|
+
import { selectSnapshotEntries } from "./state/snapshot-select";
|
|
17
23
|
import { clientCarriedPrefixLength, providerIssuedIdentity } from "./state/replay-fingerprint";
|
|
18
24
|
export type { ResponseStateTempRecoveryResult, ResponseStateTempRecoveryOptions } from "./state/temp-recovery";
|
|
25
|
+
export type { ResponseSpillDirInspection } from "./spill-store";
|
|
19
26
|
export { recoverStaleResponseStateTemps, reclaimAbandonedResponseStateTemps, inspectAbandonedResponseStateTemps, sweepAbandonedResponseStateTemps } from "./state/temp-recovery";
|
|
20
27
|
import { recoverStaleResponseStateTemps } from "./state/temp-recovery";
|
|
21
28
|
export type { ResponseSpillWriteFailureCode, ResponseSpillWriteStatus, ResponseSpillWriteFailureOrigin } from "./state/spill-failure";
|
|
@@ -36,6 +43,7 @@ import {
|
|
|
36
43
|
spillQueueAccounting,
|
|
37
44
|
spillQueueHoldsResidentCandidate,
|
|
38
45
|
spillQueuePendingBytes,
|
|
46
|
+
spillQueueReferencedSpillFileNames,
|
|
39
47
|
spillQueueResidentCandidates,
|
|
40
48
|
spillQueueSupersededSpillFor,
|
|
41
49
|
} from "./state/spill-queue";
|
|
@@ -144,6 +152,7 @@ let residentResponseBytes = 0;
|
|
|
144
152
|
let oldestResidentId: string | undefined;
|
|
145
153
|
let oldestResidentAt: number | null = null;
|
|
146
154
|
let byteCapOverride: number | null = null;
|
|
155
|
+
let snapshotTotalCapOverride: number | null = null;
|
|
147
156
|
let stateRevision = 0;
|
|
148
157
|
/** Byte length and digest of the last snapshot actually written, for the
|
|
149
158
|
* identical-payload skip and the size-scaled debounce. The payload itself is not
|
|
@@ -215,6 +224,15 @@ export function setResponseStateByteCapForTests(bytes: number | null): void {
|
|
|
215
224
|
byteCapOverride = bytes;
|
|
216
225
|
}
|
|
217
226
|
|
|
227
|
+
function snapshotTotalBytes(): number {
|
|
228
|
+
return snapshotTotalCapOverride ?? SNAPSHOT_TOTAL_MAX_BYTES;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** Test-only: lower/restore the durable snapshot byte budget (null restores the default). */
|
|
232
|
+
export function setResponseStateSnapshotByteCapForTests(bytes: number | null): void {
|
|
233
|
+
snapshotTotalCapOverride = bytes;
|
|
234
|
+
}
|
|
235
|
+
|
|
218
236
|
/** Test-only: current in-memory byte accounting (proves evictions release their bytes). */
|
|
219
237
|
export function getStoredResponseBytesForTests(): number {
|
|
220
238
|
return storedResponseBytes;
|
|
@@ -655,14 +673,27 @@ function ensureLoaded(): void {
|
|
|
655
673
|
} catch {
|
|
656
674
|
/* missing/corrupt snapshot: start empty */
|
|
657
675
|
}
|
|
658
|
-
|
|
659
|
-
for (const state of states.values()) {
|
|
660
|
-
if (state.kind === "spill") referenced.add(state.spill.fileName);
|
|
661
|
-
}
|
|
662
|
-
try { recoverOrphanedResponseSpills(referenced); } catch { /* best effort */ }
|
|
676
|
+
try { recoverOrphanedResponseSpills(ownedSpillFileNames()); } catch { /* best effort */ }
|
|
663
677
|
pruneResponses();
|
|
664
678
|
}
|
|
665
679
|
|
|
680
|
+
/** Every file name the process still needs: live stubs, deferred unlinks, and queued publications. */
|
|
681
|
+
function ownedSpillFileNames(): Set<string> {
|
|
682
|
+
const referenced = collectReferencedSpillFileNames(states.values(), pendingSpillUnlinks);
|
|
683
|
+
for (const name of spillQueueReferencedSpillFileNames()) referenced.add(name);
|
|
684
|
+
return referenced;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/** Liveness-tick counterpart of the orphan GC in ensureLoaded; a no-op until first load. */
|
|
688
|
+
export function sweepOrphanedResponseSpills(): number {
|
|
689
|
+
if (!loaded) return 0;
|
|
690
|
+
try {
|
|
691
|
+
return sweepOrphanedResponseSpillsPeriodically(ownedSpillFileNames()).removed;
|
|
692
|
+
} catch {
|
|
693
|
+
return 0;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
666
697
|
type SnapshotWriteOutcome = "stable" | "unstable" | "failed";
|
|
667
698
|
|
|
668
699
|
async function writeBoundedSnapshot(path: string, attemptLimit: number): Promise<SnapshotWriteOutcome> {
|
|
@@ -674,28 +705,7 @@ async function writeBoundedSnapshot(path: string, attemptLimit: number): Promise
|
|
|
674
705
|
try {
|
|
675
706
|
for (let attempt = 0; attempt < attemptLimit; attempt += 1) {
|
|
676
707
|
const revision = stateRevision;
|
|
677
|
-
const entries
|
|
678
|
-
let total = 0;
|
|
679
|
-
// Newest-first so the most recent chains survive both legacy snapshot caps.
|
|
680
|
-
for (const [id, state] of [...states].reverse()) {
|
|
681
|
-
let persistable: unknown;
|
|
682
|
-
if (state.kind === "resident") {
|
|
683
|
-
const { sizeBytes: _sizeBytes, kind: _kind, ...resident } = state;
|
|
684
|
-
persistable = resident;
|
|
685
|
-
} else {
|
|
686
|
-
const { sizeBytes: _sizeBytes, ...smallState } = state;
|
|
687
|
-
persistable = smallState;
|
|
688
|
-
}
|
|
689
|
-
const persistEntry: [string, unknown] = [id, persistable];
|
|
690
|
-
// UTF-8 bytes, not UTF-16 code units: multibyte items otherwise slip
|
|
691
|
-
// past both snapshot caps at up to 2x the intended size.
|
|
692
|
-
const size = Buffer.byteLength(JSON.stringify(persistEntry), "utf8");
|
|
693
|
-
if (state.kind === "resident" && size > SNAPSHOT_ENTRY_MAX_BYTES) continue;
|
|
694
|
-
if (total + size > SNAPSHOT_TOTAL_MAX_BYTES) break;
|
|
695
|
-
total += size;
|
|
696
|
-
entries.push(persistEntry);
|
|
697
|
-
}
|
|
698
|
-
entries.reverse();
|
|
708
|
+
const entries = selectSnapshotEntries(states, snapshotTotalBytes(), SNAPSHOT_ENTRY_MAX_BYTES);
|
|
699
709
|
const payload = JSON.stringify({ version: 2, states: entries });
|
|
700
710
|
const payloadBytes = Buffer.byteLength(payload, "utf8");
|
|
701
711
|
const payloadDigest = Bun.hash(payload).toString(36);
|
|
@@ -1226,6 +1236,19 @@ export function responseStateMetrics(): ResponseStateMetrics {
|
|
|
1226
1236
|
};
|
|
1227
1237
|
}
|
|
1228
1238
|
|
|
1239
|
+
/**
|
|
1240
|
+
* Read-only spill report for `ocx doctor` and /api/system/memory. Owned = live
|
|
1241
|
+
* stubs, deferred unlinks, queued publications, and snapshot references (what a
|
|
1242
|
+
* restart re-owns). Shares the reclaim's orphan predicate; never unlinks.
|
|
1243
|
+
*/
|
|
1244
|
+
export function inspectResponseSpillStorage(): ResponseSpillDirInspection {
|
|
1245
|
+
const referenced = ownedSpillFileNames();
|
|
1246
|
+
for (const name of snapshotReferencedSpillFileNames(snapshotPath(), SNAPSHOT_FILE_MAX_BYTES)) {
|
|
1247
|
+
referenced.add(name);
|
|
1248
|
+
}
|
|
1249
|
+
return inspectResponseSpillDir(referenced);
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1229
1252
|
/**
|
|
1230
1253
|
* Cache completed output and max_output_tokens partial output for previous_response_id replay.
|
|
1231
1254
|
* Content-filtered incomplete and failed output are not authoritative replay history.
|
|
@@ -1307,6 +1330,7 @@ export function clearResponseStateMemoryForTests(): void {
|
|
|
1307
1330
|
}
|
|
1308
1331
|
pendingPersistPath = null;
|
|
1309
1332
|
resetSpillQueueForTests();
|
|
1333
|
+
resetPeriodicSpillSweepCursorForTests();
|
|
1310
1334
|
states.clear();
|
|
1311
1335
|
storedResponseBytes = 0;
|
|
1312
1336
|
residentResponseBytes = 0;
|
|
@@ -2,7 +2,7 @@ import { createHash, timingSafeEqual } from "node:crypto";
|
|
|
2
2
|
import { formatErrorResponse } from "../bridge";
|
|
3
3
|
import type { OcxConfig } from "../types";
|
|
4
4
|
import { captureExplicitOpenAiCallerAuth, selectOpenAiImagesProvider } from "../providers/openai-sidecar";
|
|
5
|
-
import { isProxyAdmissionSecret, type DataPlaneAdmission } from "./auth-cors";
|
|
5
|
+
import { isProxyAdmissionSecret, type DataPlaneAdmission, type DataPlaneAdmissionOptions } from "./auth-cors";
|
|
6
6
|
import { resolveAudioAdmission } from "./audio-upstream";
|
|
7
7
|
|
|
8
8
|
export const AUDIO_WEBSOCKET_PROTOCOL = "opencodex-audio";
|
|
@@ -16,7 +16,12 @@ export interface AudioClient {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
/** Browser protocols are an audio-only credential carrier; only the public marker is echoed. */
|
|
19
|
-
export function resolveAudioClient(
|
|
19
|
+
export function resolveAudioClient(
|
|
20
|
+
req: Request,
|
|
21
|
+
config: OcxConfig,
|
|
22
|
+
required = false,
|
|
23
|
+
options: DataPlaneAdmissionOptions = {},
|
|
24
|
+
): AudioClient | Response | null {
|
|
20
25
|
const headers = new Headers(req.headers);
|
|
21
26
|
let protocol: string | undefined;
|
|
22
27
|
let carrier = false;
|
|
@@ -43,7 +48,7 @@ export function resolveAudioClient(req: Request, config: OcxConfig, required = f
|
|
|
43
48
|
}
|
|
44
49
|
}
|
|
45
50
|
}
|
|
46
|
-
const admission = resolveAudioAdmission(headers, config);
|
|
51
|
+
const admission = resolveAudioAdmission(headers, config, options);
|
|
47
52
|
if (!admission) {
|
|
48
53
|
const bearer = /^Bearer\s+([^\s,]+)$/i.exec(headers.get("authorization") ?? "")?.[1];
|
|
49
54
|
const platformKey = selectOpenAiImagesProvider(config).keyed?.apiKey;
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
resolveDataPlaneAdmissionSecret,
|
|
21
21
|
validateForwardAdmissionCredential,
|
|
22
22
|
type DataPlaneAdmission,
|
|
23
|
+
type DataPlaneAdmissionOptions,
|
|
23
24
|
} from "./auth-cors";
|
|
24
25
|
import { codexAccountSelectionForTurn } from "./lifecycle";
|
|
25
26
|
import type { RequestLogContext } from "./request-log";
|
|
@@ -28,16 +29,20 @@ export const TRANSCRIPTION_MODEL = "gpt-4o-transcribe";
|
|
|
28
29
|
export const LIVE_AUDIO_MODEL = "gpt-live-1-codex";
|
|
29
30
|
|
|
30
31
|
/** Audio keys remain identifiable even on the otherwise unauthenticated local listener. */
|
|
31
|
-
export function resolveAudioAdmission(
|
|
32
|
+
export function resolveAudioAdmission(
|
|
33
|
+
headers: Headers,
|
|
34
|
+
config: OcxConfig,
|
|
35
|
+
options: DataPlaneAdmissionOptions = {},
|
|
36
|
+
): DataPlaneAdmission | null {
|
|
32
37
|
const dedicated = headers.get("x-opencodex-api-key")?.trim();
|
|
33
|
-
if (dedicated) return resolveDataPlaneAdmissionSecret(dedicated, config, "dedicated");
|
|
38
|
+
if (dedicated) return resolveDataPlaneAdmissionSecret(dedicated, config, "dedicated", options);
|
|
34
39
|
const authorization = headers.get("authorization")?.trim();
|
|
35
40
|
if (authorization) {
|
|
36
41
|
const token = /^Bearer\s+([^\s,]+)$/i.exec(authorization)?.[1];
|
|
37
|
-
return token ? resolveDataPlaneAdmissionSecret(token, config, "bearer") : null;
|
|
42
|
+
return token ? resolveDataPlaneAdmissionSecret(token, config, "bearer", options) : null;
|
|
38
43
|
}
|
|
39
44
|
const key = headers.get("x-api-key")?.trim();
|
|
40
|
-
return key ? resolveDataPlaneAdmissionSecret(key, config, "x-api-key") : null;
|
|
45
|
+
return key ? resolveDataPlaneAdmissionSecret(key, config, "x-api-key", options) : null;
|
|
41
46
|
}
|
|
42
47
|
|
|
43
48
|
export interface AudioUpstream {
|
package/src/server/auth-cors.ts
CHANGED
|
@@ -312,14 +312,29 @@ export function isApiAuthRequired(config: Pick<OcxConfig, "hostname">): boolean
|
|
|
312
312
|
* So this type is deliberately narrow: it cannot masquerade as a business config, and a policy
|
|
313
313
|
* view that leaks into a routing path fails to typecheck rather than silently taking effect.
|
|
314
314
|
*/
|
|
315
|
-
export
|
|
315
|
+
export interface LinkIngressPolicy {
|
|
316
|
+
allowedKeyIds: ReadonlySet<string>;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export type RequestPolicyView = Pick<OcxConfig, "hostname" | "corsAllowOrigins" | "apiKeys"> & {
|
|
320
|
+
linkIngress?: LinkIngressPolicy;
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
export type DataPlaneAdmissionOptions = {
|
|
324
|
+
linkIngress?: ReadonlySet<string>;
|
|
325
|
+
};
|
|
316
326
|
|
|
317
327
|
/** Derive the per-request policy view for a listener. Cheap enough to build per request. */
|
|
318
|
-
export function requestPolicyView(
|
|
328
|
+
export function requestPolicyView(
|
|
329
|
+
config: OcxConfig,
|
|
330
|
+
bindHostname: string,
|
|
331
|
+
linkIngress?: LinkIngressPolicy,
|
|
332
|
+
): RequestPolicyView {
|
|
319
333
|
return {
|
|
320
334
|
hostname: bindHostname,
|
|
321
335
|
...(config.corsAllowOrigins ? { corsAllowOrigins: config.corsAllowOrigins } : {}),
|
|
322
336
|
...(config.apiKeys ? { apiKeys: config.apiKeys } : {}),
|
|
337
|
+
...(linkIngress ? { linkIngress } : {}),
|
|
323
338
|
};
|
|
324
339
|
}
|
|
325
340
|
|
|
@@ -398,13 +413,15 @@ export function resolveDataPlaneAdmissionSecret(
|
|
|
398
413
|
token: string,
|
|
399
414
|
config: Pick<OcxConfig, "apiKeys">,
|
|
400
415
|
source: DataPlaneAdmissionSource = "dedicated",
|
|
416
|
+
options: DataPlaneAdmissionOptions = {},
|
|
401
417
|
): DataPlaneAdmission | null {
|
|
402
418
|
const actual = token.trim();
|
|
403
419
|
if (!actual) return null;
|
|
404
|
-
if (secretEquals(actual, configuredApiAuthToken(config))) {
|
|
420
|
+
if (!options.linkIngress && secretEquals(actual, configuredApiAuthToken(config))) {
|
|
405
421
|
return { kind: "environment", source, contextPrincipalId: mintContextPrincipal("environment", "", actual) };
|
|
406
422
|
}
|
|
407
423
|
for (const k of config.apiKeys ?? []) {
|
|
424
|
+
if (options.linkIngress && !options.linkIngress.has(k.id)) continue;
|
|
408
425
|
if (secretEquals(actual, k.key)) {
|
|
409
426
|
return { kind: "configured", keyId: k.id, source, contextPrincipalId: mintContextPrincipal("configured", k.id, actual) };
|
|
410
427
|
}
|
|
@@ -555,12 +572,12 @@ export function resolveApiAuth(req: Request, config: RequestPolicyView): DataPla
|
|
|
555
572
|
// A loopback bind never reads a token at all, so there is no key to name.
|
|
556
573
|
if (!isApiAuthRequired(config)) return { kind: "loopback", source: "loopback" };
|
|
557
574
|
const dedicated = req.headers.get("x-opencodex-api-key")?.trim();
|
|
558
|
-
if (dedicated) return resolveDataPlaneAdmissionSecret(dedicated, config, "dedicated");
|
|
575
|
+
if (dedicated) return resolveDataPlaneAdmissionSecret(dedicated, config, "dedicated", { linkIngress: config.linkIngress?.allowedKeyIds });
|
|
559
576
|
const bearer = req.headers.get("authorization")?.replace(/^Bearer\s+/i, "").trim();
|
|
560
|
-
if (bearer) return resolveDataPlaneAdmissionSecret(bearer, config, "bearer");
|
|
577
|
+
if (bearer) return resolveDataPlaneAdmissionSecret(bearer, config, "bearer", { linkIngress: config.linkIngress?.allowedKeyIds });
|
|
561
578
|
// Anthropic-SDK clients (Claude Code with ANTHROPIC_API_KEY) authenticate via x-api-key.
|
|
562
579
|
const apiKey = req.headers.get("x-api-key")?.trim();
|
|
563
|
-
if (apiKey) return resolveDataPlaneAdmissionSecret(apiKey, config, "x-api-key");
|
|
580
|
+
if (apiKey) return resolveDataPlaneAdmissionSecret(apiKey, config, "x-api-key", { linkIngress: config.linkIngress?.allowedKeyIds });
|
|
564
581
|
return null;
|
|
565
582
|
}
|
|
566
583
|
|
|
@@ -582,14 +599,14 @@ export function resolveResponsesApiAuth(req: Request, config: RequestPolicyView)
|
|
|
582
599
|
if (!isApiAuthRequired(config)) return { kind: "loopback", source: "loopback" };
|
|
583
600
|
// The dedicated header still WINS, because it is unambiguous.
|
|
584
601
|
const dedicated = req.headers.get("x-opencodex-api-key")?.trim();
|
|
585
|
-
if (dedicated) return resolveDataPlaneAdmissionSecret(dedicated, config, "dedicated");
|
|
602
|
+
if (dedicated) return resolveDataPlaneAdmissionSecret(dedicated, config, "dedicated", { linkIngress: config.linkIngress?.allowedKeyIds });
|
|
586
603
|
// #1686: a bearer may also be one of OUR admission secrets. Rejecting it outright meant a
|
|
587
604
|
// Codex client configured with `env_key` could not reach Direct at all. Admitting it is only
|
|
588
605
|
// safe because the upstream credential is then SUBSTITUTED rather than forwarded -- see
|
|
589
606
|
// materializeCodexUpstreamAuth. A bearer that is NOT our secret stays unadmitted here and
|
|
590
607
|
// remains Codex Direct passthrough, so the two bearer domains still never mix.
|
|
591
608
|
const bearer = req.headers.get("authorization")?.replace(/^Bearer\s+/i, "").trim();
|
|
592
|
-
if (bearer) return resolveDataPlaneAdmissionSecret(bearer, config, "bearer");
|
|
609
|
+
if (bearer) return resolveDataPlaneAdmissionSecret(bearer, config, "bearer", { linkIngress: config.linkIngress?.allowedKeyIds });
|
|
593
610
|
// `x-api-key` is deliberately NOT accepted on this transport.
|
|
594
611
|
return null;
|
|
595
612
|
}
|
|
@@ -9,8 +9,8 @@ import { FORWARD_HEADERS } from "../adapters/openai-responses";
|
|
|
9
9
|
import {
|
|
10
10
|
assertChatCompletionsRoutingBody,
|
|
11
11
|
ChatCompletionsRequestError,
|
|
12
|
-
chatCompletionsToResponsesBody,
|
|
13
12
|
} from "../chat/inbound";
|
|
13
|
+
import { chatToResponsesBody } from "../protocols/codecs/chat";
|
|
14
14
|
import { normalizeChatImageParts } from "../chat/image-parts";
|
|
15
15
|
import {
|
|
16
16
|
chatCompletionsErrorResponse,
|
|
@@ -47,8 +47,10 @@ import {
|
|
|
47
47
|
httpStatusForRequestLogTerminal,
|
|
48
48
|
recordFirstOutput,
|
|
49
49
|
type RequestLogContext,
|
|
50
|
-
type RequestLogEntry,
|
|
51
50
|
} from "./request-log";
|
|
51
|
+
import { createFinalRequestLog } from "./inference/final-log";
|
|
52
|
+
import { clientWireLogOf, clientWireOf } from "./inference/client-wire";
|
|
53
|
+
import { directEncodersApply } from "./inference/client-encoder-delivery";
|
|
52
54
|
import { responseWithDeferredRequestLog } from "./relay";
|
|
53
55
|
import { handleResponses } from "./responses";
|
|
54
56
|
import { providerConsumesCallerAuthorization } from "../providers/caller-authorization";
|
|
@@ -69,7 +71,15 @@ import {
|
|
|
69
71
|
isTranslatorBudgetExceededError,
|
|
70
72
|
type TranslatorBudget,
|
|
71
73
|
} from "../lib/translator-budget";
|
|
72
|
-
import { handleNativeChatCompletions,
|
|
74
|
+
import { createNativeChatComboSource, handleNativeChatCompletions, nativeChatDeclineReason } from "./chat-native";
|
|
75
|
+
import { upstreamWireForAdapter, type ProtocolReasonCode } from "../protocols/contract";
|
|
76
|
+
import { createProtocolEnvelope } from "../protocols/envelope";
|
|
77
|
+
import { featuresFromChatBody } from "../protocols/features";
|
|
78
|
+
import { checkRepresentable, unrepresentableMessage } from "../protocols/guard";
|
|
79
|
+
import { requestPathForLane } from "../protocols/path";
|
|
80
|
+
import { resolveProtocolSettings } from "../protocols/settings";
|
|
81
|
+
import { markProtocolBlocked, markProtocolEntry } from "../protocols/trace";
|
|
82
|
+
import { recordProtocolShadowPlan } from "../protocols/shadow-plan";
|
|
73
83
|
import { jsonCompletionSse } from "./chat-native-sse";
|
|
74
84
|
import { parseRequestEffortRowId } from "./effort-row";
|
|
75
85
|
import { parseSyntheticRowId } from "./fast-row";
|
|
@@ -163,6 +173,8 @@ async function handleChatCompletionsWithBudget(
|
|
|
163
173
|
let routeMayChangeCredentialDomain = false;
|
|
164
174
|
let settledRoute: ReturnType<typeof routeModel> | null = null;
|
|
165
175
|
let chatNativeRoute: ReturnType<typeof routeModel> | null = null;
|
|
176
|
+
// Why the native Chat lane was not taken; stays `unknown-model` when routing threw.
|
|
177
|
+
let nativeDecline: ProtocolReasonCode | undefined = "unknown-model";
|
|
166
178
|
try {
|
|
167
179
|
const route = routeModel(config, chatBody.model as string, evidenceFromBody(chatBody));
|
|
168
180
|
// The native Chat lane sends without re-entering the Responses path, so it
|
|
@@ -199,7 +211,10 @@ async function handleChatCompletionsWithBudget(
|
|
|
199
211
|
}
|
|
200
212
|
// Combos must enter the Responses routing path so child selection, forced default
|
|
201
213
|
// effort, failover, and per-attempt telemetry run before any native Chat send.
|
|
202
|
-
|
|
214
|
+
nativeDecline = route.combo ? "combo-or-policy-route"
|
|
215
|
+
: effortRow ? "effort-row"
|
|
216
|
+
: nativeChatDeclineReason(route, chatBody, config);
|
|
217
|
+
if (nativeDecline === undefined) {
|
|
203
218
|
chatNativeRoute = route;
|
|
204
219
|
// Reserve an input estimate for spend without recording it as usage: native Chat attempts
|
|
205
220
|
// keep the provider-reported counts, as they did before the reservation existed.
|
|
@@ -232,6 +247,39 @@ async function handleChatCompletionsWithBudget(
|
|
|
232
247
|
/* unknown model: let handleResponses shape the 404 */
|
|
233
248
|
}
|
|
234
249
|
|
|
250
|
+
// Off by default: under the legacy policy with `nativeChatCombos` off nothing below is built
|
|
251
|
+
// and the request is unchanged. An effort row keeps its effort on the Responses body only, so
|
|
252
|
+
// its combo stays on the bridge.
|
|
253
|
+
const protocolSettings = resolveProtocolSettings(config);
|
|
254
|
+
const nativeChatCombos = protocolSettings.rollout.nativeChatCombos
|
|
255
|
+
&& settledRoute?.combo !== undefined && !effortRow;
|
|
256
|
+
const envelope = protocolSettings.unrepresentable === "reject" || nativeChatCombos
|
|
257
|
+
? createProtocolEnvelope({ inbound: "chat", body: chatBody, translatorBudget })
|
|
258
|
+
: undefined;
|
|
259
|
+
// Combo and policy children are judged per candidate (PF-07); an unknown model has no route.
|
|
260
|
+
if (envelope && settledRoute && !settledRoute.combo && settledRoute.routeKind !== "policy") {
|
|
261
|
+
const verdict = checkRepresentable({
|
|
262
|
+
inbound: "chat",
|
|
263
|
+
requestPath: chatNativeRoute
|
|
264
|
+
? requestPathForLane("chat", "native", "chat")
|
|
265
|
+
: requestPathForLane("chat", "bridge", upstreamWireForAdapter(settledRoute.provider.adapter)),
|
|
266
|
+
features: envelope.features(),
|
|
267
|
+
policy: "reject",
|
|
268
|
+
});
|
|
269
|
+
if (!verdict.ok) {
|
|
270
|
+
markProtocolBlocked(logCtx, { inbound: "chat", reasonCodes: verdict.reasonCodes, features: verdict.features });
|
|
271
|
+
logCtx.errorCode = "unsupported_feature";
|
|
272
|
+
if (logIds) addFinalRequestLog(logIds.requestId, logIds.start, logCtx, 400, { closeReason: "non_stream" });
|
|
273
|
+
return chatCompletionsErrorResponse(400, unrepresentableMessage(verdict.features), "invalid_request_error", "unsupported_feature");
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
markProtocolEntry(logCtx, {
|
|
277
|
+
inbound: "chat",
|
|
278
|
+
lane: chatNativeRoute ? "native" : "bridge",
|
|
279
|
+
reasonCodes: !chatNativeRoute && nativeDecline ? [nativeDecline] : [],
|
|
280
|
+
features: envelope ? () => envelope.features() : () => featuresFromChatBody(chatBody),
|
|
281
|
+
});
|
|
282
|
+
recordProtocolShadowPlan(logCtx, config, { inbound: "chat", model: requestedModel });
|
|
235
283
|
if (chatNativeRoute) {
|
|
236
284
|
return handleNativeChatCompletions({
|
|
237
285
|
req,
|
|
@@ -250,7 +298,7 @@ async function handleChatCompletionsWithBudget(
|
|
|
250
298
|
try {
|
|
251
299
|
// Validate the full Chat boundary after routing. Native Chat keeps `chatBody` as
|
|
252
300
|
// its wire source; this Responses projection is used only by the fallback path.
|
|
253
|
-
internalBody =
|
|
301
|
+
internalBody = chatToResponsesBody(chatBody);
|
|
254
302
|
if (effortRow) {
|
|
255
303
|
internalBody.reasoning = {
|
|
256
304
|
...(isRec(internalBody.reasoning) ? internalBody.reasoning : {}),
|
|
@@ -373,12 +421,7 @@ async function handleChatCompletionsWithBudget(
|
|
|
373
421
|
});
|
|
374
422
|
linkRequestSessionLane(req, internalReq);
|
|
375
423
|
|
|
376
|
-
|
|
377
|
-
const finalizeNativeLog = (status: number, meta: { terminalStatus?: RequestLogEntry["terminalStatus"]; closeReason: "terminal" | "client_cancel" | "non_stream" }) => {
|
|
378
|
-
if (!logIds || nativeLogged) return;
|
|
379
|
-
nativeLogged = true;
|
|
380
|
-
addFinalRequestLog(logIds.requestId, logIds.start, logCtx, status, meta);
|
|
381
|
-
};
|
|
424
|
+
const finalizeNativeLog = createFinalRequestLog(logIds, logCtx).finish;
|
|
382
425
|
const upstream = await handleResponses(internalReq, config, logCtx, {
|
|
383
426
|
openAiSidecarAuth,
|
|
384
427
|
allowStoredOpenAiSidecarAuth: !!(callerAuthorizationRoute && settledRoute
|
|
@@ -392,6 +435,12 @@ async function handleChatCompletionsWithBudget(
|
|
|
392
435
|
abortSignal: req.signal,
|
|
393
436
|
// Body is Responses-shaped by now, but the client spoke Chat Completions.
|
|
394
437
|
inboundWire: "chat",
|
|
438
|
+
// PF-07: the combo sends eligible candidates natively from this envelope.
|
|
439
|
+
...(envelope && nativeChatCombos ? {
|
|
440
|
+
protocolSource: createNativeChatComboSource({
|
|
441
|
+
req, config, envelope, requestedModel, requestedStream: stream, translatorBudget,
|
|
442
|
+
}),
|
|
443
|
+
} : {}),
|
|
395
444
|
// Terminal vision-describe marker (roadmap 180): the bridge rebuilds
|
|
396
445
|
// headers from the FORWARD_HEADERS allowlist, which would drop the raw
|
|
397
446
|
// header — so the fact is detected here and carried as an option flag.
|
|
@@ -400,7 +449,18 @@ async function handleChatCompletionsWithBudget(
|
|
|
400
449
|
...(logIds ? { onFirstOutput: () => recordFirstOutput(logCtx, logIds.start) } : {}),
|
|
401
450
|
onNativePassthroughTerminal: status => finalizeNativeLog(httpStatusForRequestLogTerminal(status, logCtx), { terminalStatus: status, closeReason: "terminal" }),
|
|
402
451
|
onNativePassthroughCancel: () => finalizeNativeLog(499, { closeReason: "client_cancel" }),
|
|
452
|
+
...(directEncodersApply(config, settledRoute)
|
|
453
|
+
? { clientEncoder: { protocol: "chat" as const, stream, model: requestedModel } }
|
|
454
|
+
: {}),
|
|
403
455
|
});
|
|
456
|
+
// Already in the Chat wire: no conversion. A direct-encoder body (PF-09) reports its own log
|
|
457
|
+
// facts to the deferred request log. A native combo child (PF-07) carries none: its row is
|
|
458
|
+
// written by the terminal callbacks above, and the deferred log's Responses-shaped inspector
|
|
459
|
+
// would misread a Chat stream, so of those only a refusal is wrapped.
|
|
460
|
+
if (clientWireOf(upstream) === "chat") {
|
|
461
|
+
if (!logIds || (upstream.ok && !clientWireLogOf(upstream))) return upstream;
|
|
462
|
+
return responseWithDeferredRequestLog(upstream, logIds.requestId, logIds.start, logCtx);
|
|
463
|
+
}
|
|
404
464
|
|
|
405
465
|
// Rewrite non-2xx before deferred logging so /api/logs records the client-facing status
|
|
406
466
|
// (e.g. cyber_policy remapped from a passthrough 5xx to HTTP 400).
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { chatBodyCarriesImage, chatBodyCarriesToolResultImage } from "../chat/image-parts";
|
|
2
|
+
import type { ProtocolReasonCode } from "../protocols/contract";
|
|
3
|
+
import type { RouteResult } from "../router";
|
|
4
|
+
import type { OcxConfig } from "../types";
|
|
5
|
+
import { isModelTextOnly, requiresVisionPreprocessing } from "../vision";
|
|
6
|
+
|
|
7
|
+
type Rec = Record<string, unknown>;
|
|
8
|
+
|
|
9
|
+
function isRec(value: unknown): value is Rec {
|
|
10
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Why the native Chat lane declines a route, as a protocol reason code. */
|
|
14
|
+
export type NativeChatDeclineReason = Extract<
|
|
15
|
+
ProtocolReasonCode,
|
|
16
|
+
| "cross-wire-ir"
|
|
17
|
+
| "auth-mode-not-native"
|
|
18
|
+
| "combo-or-policy-route"
|
|
19
|
+
| "responses-only-feature"
|
|
20
|
+
| "tool-result-image"
|
|
21
|
+
| "vision-preprocessing"
|
|
22
|
+
| "hosted-tool"
|
|
23
|
+
>;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The first rule that keeps a Chat request off the native Chat lane, or `undefined` when the
|
|
27
|
+
* route is eligible. The order is the order the checks always ran in, so the reason reported is
|
|
28
|
+
* the one that actually decided.
|
|
29
|
+
*/
|
|
30
|
+
export function nativeChatDeclineReason(
|
|
31
|
+
route: RouteResult,
|
|
32
|
+
rawBody: Rec,
|
|
33
|
+
config?: OcxConfig,
|
|
34
|
+
): NativeChatDeclineReason | undefined {
|
|
35
|
+
const provider = route.provider;
|
|
36
|
+
if (provider.adapter !== "openai-chat") return "cross-wire-ir";
|
|
37
|
+
if (provider.authMode !== undefined && provider.authMode !== "key" && provider.authMode !== "local") return "auth-mode-not-native";
|
|
38
|
+
// Combo and policy execution own multi-candidate retries in the Responses pipeline.
|
|
39
|
+
if (route.combo || route.routeKind === "combo" || route.routeKind === "policy") return "combo-or-policy-route";
|
|
40
|
+
if (rawBody.store === true || rawBody.background === true) return "responses-only-feature";
|
|
41
|
+
if (typeof rawBody.previous_response_id === "string" && rawBody.previous_response_id.length > 0) return "responses-only-feature";
|
|
42
|
+
if (rawBody.compaction_trigger !== undefined) return "responses-only-feature";
|
|
43
|
+
// A standard Chat tool message accepts a string or text parts, not image_url, so
|
|
44
|
+
// normalizing a Pi/Anthropic tool image into image_url is not enough on its own —
|
|
45
|
+
// the part is still inside a tool message. The translated adapter already places
|
|
46
|
+
// tool-result images in a following user carrier after the complete paired batch
|
|
47
|
+
// (flushToolResultImages), so divert these requests there. Ordinary user images and
|
|
48
|
+
// text-only tool results keep the native fast path.
|
|
49
|
+
if (chatBodyCarriesToolResultImage(rawBody)) return "tool-result-image";
|
|
50
|
+
// Vision sidecar coverage (roadmap 180): a text-only routed model with an
|
|
51
|
+
// image-bearing body must go through the Responses pipeline, whose plan
|
|
52
|
+
// site describes or strips the image. The native fast path has no vision
|
|
53
|
+
// handling, so letting it keep such a request forwards raw pixels to a
|
|
54
|
+
// model the operator declared blind.
|
|
55
|
+
if (chatBodyCarriesImage(rawBody)) {
|
|
56
|
+
const needsVision = config
|
|
57
|
+
? requiresVisionPreprocessing(config, provider, route.modelId, route.providerName)
|
|
58
|
+
: isModelTextOnly(provider, route.modelId);
|
|
59
|
+
if (needsVision) return "vision-preprocessing";
|
|
60
|
+
}
|
|
61
|
+
if (Array.isArray(rawBody.tools)) {
|
|
62
|
+
for (const tool of rawBody.tools) {
|
|
63
|
+
if (!isRec(tool)) continue;
|
|
64
|
+
if (tool.type === "web_search" || tool.type === "web_search_preview" || tool.type === "image_generation") {
|
|
65
|
+
return "hosted-tool";
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function isNativeChatRouteEligible(route: RouteResult, rawBody: Rec, config?: OcxConfig): boolean {
|
|
73
|
+
return nativeChatDeclineReason(route, rawBody, config) === undefined;
|
|
74
|
+
}
|