@bitkyc08/opencodex 2.65.0 → 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-D4bA1vff.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-BZXhB3qz.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-DGLte6IR.js +0 -50
- package/gui/dist/assets/index--EWgGQvZ.css +0 -1
- package/gui/dist/assets/index-Bi1K37Wl.js +0 -86
|
@@ -15,6 +15,10 @@ import {
|
|
|
15
15
|
} from "../../usage/summary";
|
|
16
16
|
import { userCostOverlayVersion } from "../../usage/user-cost-overlays";
|
|
17
17
|
import type { UsageTimeWindow } from "../../usage/time-range";
|
|
18
|
+
import {
|
|
19
|
+
createJevStatsAccumulator,
|
|
20
|
+
type JevStatsAccumulator,
|
|
21
|
+
} from "../../usage/jev-stats";
|
|
18
22
|
|
|
19
23
|
import {
|
|
20
24
|
cacheApiKeyUsageFromRollup,
|
|
@@ -34,6 +38,19 @@ interface RetainedUsageAggregate {
|
|
|
34
38
|
retainedAt: number;
|
|
35
39
|
}
|
|
36
40
|
|
|
41
|
+
interface RetainedJevStatsAggregate {
|
|
42
|
+
accumulator: JevStatsAccumulator;
|
|
43
|
+
usageIncomplete: boolean;
|
|
44
|
+
revision: UsageLogRevision | null;
|
|
45
|
+
identityKey: string;
|
|
46
|
+
revisionKey: string;
|
|
47
|
+
processedThroughBytes: number;
|
|
48
|
+
processedThroughDigest: string;
|
|
49
|
+
retainedAt: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
type RetainedAggregateState = RetainedUsageAggregate | RetainedJevStatsAggregate;
|
|
53
|
+
|
|
37
54
|
export interface UsageAggregateResult {
|
|
38
55
|
accumulator: UsageSummaryAccumulator;
|
|
39
56
|
usageIncomplete: boolean;
|
|
@@ -50,6 +67,14 @@ export interface UsageAggregateOptions {
|
|
|
50
67
|
managementUsageMaxReadBytes?: number;
|
|
51
68
|
}
|
|
52
69
|
|
|
70
|
+
export interface JevStatsAggregateResult {
|
|
71
|
+
accumulator: JevStatsAccumulator;
|
|
72
|
+
usageIncomplete: boolean;
|
|
73
|
+
revision: UsageLogRevision | null;
|
|
74
|
+
processedThroughBytes: number;
|
|
75
|
+
update: "unchanged" | "append" | "rebuild";
|
|
76
|
+
}
|
|
77
|
+
|
|
53
78
|
export interface UsageAggregateRetainedStats {
|
|
54
79
|
count: number;
|
|
55
80
|
bytes: number;
|
|
@@ -62,11 +87,15 @@ const MAX_REBUILD_ATTEMPTS = 2;
|
|
|
62
87
|
const MAX_RETAINED_FILTERED_AGGREGATES = 4;
|
|
63
88
|
|
|
64
89
|
let retainedAggregate: RetainedUsageAggregate | null = null;
|
|
65
|
-
const pinnedAggregates = new Set<
|
|
90
|
+
const pinnedAggregates = new Set<RetainedAggregateState>();
|
|
66
91
|
let baseFlight: Promise<UsageAggregateResult> | null = null;
|
|
67
92
|
const filteredFlights = new Map<string, Promise<UsageAggregateResult>>();
|
|
68
93
|
const retainedFilteredAggregates = new Map<string, RetainedUsageAggregate>();
|
|
69
94
|
const MAX_CONCURRENT_FILTERED_AGGREGATES = 4;
|
|
95
|
+
const retainedJevStatsAggregates = new Map<string, RetainedJevStatsAggregate>();
|
|
96
|
+
const jevStatsFlights = new Map<string, Promise<JevStatsAggregateResult>>();
|
|
97
|
+
const MAX_RETAINED_JEV_STATS_AGGREGATES = 4;
|
|
98
|
+
const MAX_CONCURRENT_JEV_STATS_AGGREGATES = 4;
|
|
70
99
|
|
|
71
100
|
function currentTimeZone(): string {
|
|
72
101
|
return Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
@@ -418,10 +447,171 @@ async function refreshFilteredAggregate(
|
|
|
418
447
|
return appendFilteredAggregate(key, state, filter, window);
|
|
419
448
|
}
|
|
420
449
|
|
|
450
|
+
function jevStatsResultFrom(
|
|
451
|
+
state: RetainedJevStatsAggregate,
|
|
452
|
+
update: JevStatsAggregateResult["update"],
|
|
453
|
+
): JevStatsAggregateResult {
|
|
454
|
+
return {
|
|
455
|
+
accumulator: state.accumulator,
|
|
456
|
+
usageIncomplete: state.usageIncomplete,
|
|
457
|
+
revision: state.revision,
|
|
458
|
+
processedThroughBytes: state.processedThroughBytes,
|
|
459
|
+
update,
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function trimRetainedJevStatsAggregates(): void {
|
|
464
|
+
while (retainedJevStatsAggregates.size > MAX_RETAINED_JEV_STATS_AGGREGATES) {
|
|
465
|
+
const oldest = [...retainedJevStatsAggregates]
|
|
466
|
+
.filter(([, state]) => !pinnedAggregates.has(state))
|
|
467
|
+
.sort(([, left], [, right]) => left.retainedAt - right.retainedAt)[0];
|
|
468
|
+
if (!oldest) return;
|
|
469
|
+
retainedJevStatsAggregates.delete(oldest[0]);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function publishJevStatsAggregate(
|
|
474
|
+
key: string,
|
|
475
|
+
state: RetainedJevStatsAggregate,
|
|
476
|
+
update: JevStatsAggregateResult["update"],
|
|
477
|
+
): JevStatsAggregateResult {
|
|
478
|
+
retainedJevStatsAggregates.set(key, state);
|
|
479
|
+
trimRetainedJevStatsAggregates();
|
|
480
|
+
enforceAppOwnedMemoryBudget();
|
|
481
|
+
return jevStatsResultFrom(state, update);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function retainedJevStatsState(
|
|
485
|
+
accumulator: JevStatsAccumulator,
|
|
486
|
+
scan: Awaited<ReturnType<typeof scanUsageLedgerCooperatively>>,
|
|
487
|
+
priorIncomplete = false,
|
|
488
|
+
): RetainedJevStatsAggregate {
|
|
489
|
+
return {
|
|
490
|
+
accumulator,
|
|
491
|
+
usageIncomplete: priorIncomplete || scan.oversizedRows > 0,
|
|
492
|
+
revision: scan.revision,
|
|
493
|
+
identityKey: usageLogIdentityKey(scan.revision),
|
|
494
|
+
revisionKey: usageLogRevisionKey(scan.revision),
|
|
495
|
+
processedThroughBytes: scan.processedThroughBytes,
|
|
496
|
+
processedThroughDigest: scan.processedThroughDigest,
|
|
497
|
+
retainedAt: Date.now(),
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
async function rebuildJevStatsAggregate(
|
|
502
|
+
key: string,
|
|
503
|
+
comboId: string | null,
|
|
504
|
+
since: number | null,
|
|
505
|
+
): Promise<JevStatsAggregateResult> {
|
|
506
|
+
let lastError: unknown;
|
|
507
|
+
for (let attempt = 0; attempt < MAX_REBUILD_ATTEMPTS; attempt += 1) {
|
|
508
|
+
// A rebuild-required result means the opened ledger changed underneath the
|
|
509
|
+
// scan. Start from a fresh accumulator so no partially observed row can be
|
|
510
|
+
// returned or retained.
|
|
511
|
+
const accumulator = createJevStatsAccumulator({ comboId, since });
|
|
512
|
+
try {
|
|
513
|
+
const scan = await scanUsageLedgerCooperatively({ onEntry: entry => accumulator.add(entry) });
|
|
514
|
+
return publishJevStatsAggregate(key, retainedJevStatsState(accumulator, scan), "rebuild");
|
|
515
|
+
} catch (error) {
|
|
516
|
+
lastError = error;
|
|
517
|
+
if (!(error instanceof UsageLedgerRebuildRequiredError) || attempt + 1 >= MAX_REBUILD_ATTEMPTS) {
|
|
518
|
+
throw error;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
throw lastError ?? new Error("JEV stats aggregate rebuild did not settle");
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function jevStatsRequiresRebuild(
|
|
526
|
+
state: RetainedJevStatsAggregate,
|
|
527
|
+
observed: UsageLogRevision | null,
|
|
528
|
+
): boolean {
|
|
529
|
+
if (state.identityKey !== usageLogIdentityKey(observed)) return true;
|
|
530
|
+
if (!state.revision || !observed) return state.revision !== observed;
|
|
531
|
+
if (observed.size < state.revision.size) return true;
|
|
532
|
+
return observed.size === state.revision.size && usageLogRevisionKey(observed) !== state.revisionKey;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
async function appendJevStatsAggregate(
|
|
536
|
+
key: string,
|
|
537
|
+
state: RetainedJevStatsAggregate,
|
|
538
|
+
comboId: string | null,
|
|
539
|
+
since: number | null,
|
|
540
|
+
): Promise<JevStatsAggregateResult> {
|
|
541
|
+
pinnedAggregates.add(state);
|
|
542
|
+
let rebuildAfterUnpin = false;
|
|
543
|
+
try {
|
|
544
|
+
const candidate = state.accumulator.clone();
|
|
545
|
+
const scan = await scanUsageLedgerCooperatively({
|
|
546
|
+
startAtBytes: state.processedThroughBytes,
|
|
547
|
+
expectedIdentityKey: state.identityKey,
|
|
548
|
+
expectedProcessedThroughDigest: state.processedThroughDigest,
|
|
549
|
+
onEntry: entry => candidate.add(entry),
|
|
550
|
+
});
|
|
551
|
+
const next = retainedJevStatsState(candidate, scan, state.usageIncomplete);
|
|
552
|
+
return publishJevStatsAggregate(key, next, "append");
|
|
553
|
+
} catch (error) {
|
|
554
|
+
if (retainedJevStatsAggregates.get(key) === state) retainedJevStatsAggregates.delete(key);
|
|
555
|
+
if (error instanceof UsageLedgerRebuildRequiredError) rebuildAfterUnpin = true;
|
|
556
|
+
else throw error;
|
|
557
|
+
} finally {
|
|
558
|
+
pinnedAggregates.delete(state);
|
|
559
|
+
trimRetainedJevStatsAggregates();
|
|
560
|
+
}
|
|
561
|
+
if (rebuildAfterUnpin) return rebuildJevStatsAggregate(key, comboId, since);
|
|
562
|
+
throw new Error("JEV stats aggregate append did not settle");
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
async function refreshJevStatsAggregate(
|
|
566
|
+
key: string,
|
|
567
|
+
comboId: string | null,
|
|
568
|
+
since: number | null,
|
|
569
|
+
): Promise<JevStatsAggregateResult> {
|
|
570
|
+
const state = retainedJevStatsAggregates.get(key);
|
|
571
|
+
if (!state) return rebuildJevStatsAggregate(key, comboId, since);
|
|
572
|
+
const observed = currentUsageLogRevision();
|
|
573
|
+
if (jevStatsRequiresRebuild(state, observed)) {
|
|
574
|
+
retainedJevStatsAggregates.delete(key);
|
|
575
|
+
return rebuildJevStatsAggregate(key, comboId, since);
|
|
576
|
+
}
|
|
577
|
+
if (state.revisionKey === usageLogRevisionKey(observed)) {
|
|
578
|
+
state.retainedAt = Date.now();
|
|
579
|
+
return jevStatsResultFrom(state, "unchanged");
|
|
580
|
+
}
|
|
581
|
+
return appendJevStatsAggregate(key, state, comboId, since);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* Return one checkpointed JEV projection for a combo and fixed calendar range.
|
|
586
|
+
* Concurrent readers share a flight; later polls scan only a verified append
|
|
587
|
+
* suffix, and unchanged ledgers perform no file read.
|
|
588
|
+
*/
|
|
589
|
+
export async function getJevStatsAggregate(options: {
|
|
590
|
+
comboId?: string | null;
|
|
591
|
+
since?: number | null;
|
|
592
|
+
} = {}): Promise<JevStatsAggregateResult> {
|
|
593
|
+
const comboId = options.comboId ?? null;
|
|
594
|
+
const since = options.since ?? null;
|
|
595
|
+
const key = JSON.stringify([comboId, since]);
|
|
596
|
+
const existing = jevStatsFlights.get(key);
|
|
597
|
+
if (existing) return existing;
|
|
598
|
+
if (jevStatsFlights.size >= MAX_CONCURRENT_JEV_STATS_AGGREGATES) {
|
|
599
|
+
throw new Error("too many concurrent JEV stats aggregates");
|
|
600
|
+
}
|
|
601
|
+
const flight = refreshJevStatsAggregate(key, comboId, since);
|
|
602
|
+
jevStatsFlights.set(key, flight);
|
|
603
|
+
try {
|
|
604
|
+
return await flight;
|
|
605
|
+
} finally {
|
|
606
|
+
if (jevStatsFlights.get(key) === flight) jevStatsFlights.delete(key);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
421
610
|
export function usageAggregateRetainedStats(): UsageAggregateRetainedStats {
|
|
422
611
|
const states = [
|
|
423
612
|
...(retainedAggregate ? [retainedAggregate] : []),
|
|
424
613
|
...retainedFilteredAggregates.values(),
|
|
614
|
+
...retainedJevStatsAggregates.values(),
|
|
425
615
|
];
|
|
426
616
|
if (states.length === 0) {
|
|
427
617
|
return { count: 0, bytes: 0, evictableBytes: 0, pinnedBytes: 0, oldestAt: null };
|
|
@@ -449,19 +639,27 @@ export function usageAggregateRetainedStats(): UsageAggregateRetainedStats {
|
|
|
449
639
|
}
|
|
450
640
|
|
|
451
641
|
export function discardRetainedUsageAggregate(): number {
|
|
452
|
-
const candidates: Array<{
|
|
642
|
+
const candidates: Array<{
|
|
643
|
+
kind: "base" | "filtered" | "jev";
|
|
644
|
+
key: string | null;
|
|
645
|
+
state: RetainedAggregateState;
|
|
646
|
+
}> = [
|
|
453
647
|
...(retainedAggregate && !pinnedAggregates.has(retainedAggregate)
|
|
454
|
-
? [{ key: null, state: retainedAggregate }]
|
|
648
|
+
? [{ kind: "base" as const, key: null, state: retainedAggregate }]
|
|
455
649
|
: []),
|
|
456
650
|
...[...retainedFilteredAggregates]
|
|
457
651
|
.filter(([, state]) => !pinnedAggregates.has(state))
|
|
458
|
-
.map(([key, state]) => ({ key, state })),
|
|
652
|
+
.map(([key, state]) => ({ kind: "filtered" as const, key, state })),
|
|
653
|
+
...[...retainedJevStatsAggregates]
|
|
654
|
+
.filter(([, state]) => !pinnedAggregates.has(state))
|
|
655
|
+
.map(([key, state]) => ({ kind: "jev" as const, key, state })),
|
|
459
656
|
];
|
|
460
657
|
const oldest = candidates.sort((left, right) => left.state.retainedAt - right.state.retainedAt)[0];
|
|
461
658
|
if (!oldest) return 0;
|
|
462
659
|
const released = oldest.state.accumulator.estimatedBytes;
|
|
463
|
-
if (oldest.
|
|
464
|
-
else retainedFilteredAggregates.delete(oldest.key);
|
|
660
|
+
if (oldest.kind === "base") retainedAggregate = null;
|
|
661
|
+
else if (oldest.kind === "filtered") retainedFilteredAggregates.delete(oldest.key!);
|
|
662
|
+
else retainedJevStatsAggregates.delete(oldest.key!);
|
|
465
663
|
return released;
|
|
466
664
|
}
|
|
467
665
|
|
|
@@ -471,4 +669,6 @@ export function resetUsageAggregateCacheForTests(): void {
|
|
|
471
669
|
baseFlight = null;
|
|
472
670
|
filteredFlights.clear();
|
|
473
671
|
retainedFilteredAggregates.clear();
|
|
672
|
+
jevStatsFlights.clear();
|
|
673
|
+
retainedJevStatsAggregates.clear();
|
|
474
674
|
}
|
|
@@ -80,6 +80,7 @@ import { handleClaudeDesktopPickerRoutes } from "./management/claude-desktop-pic
|
|
|
80
80
|
import { handleCursorIntegrationRoutes } from "./management/cursor-integration-routes";
|
|
81
81
|
import type { ManagementContext } from "./management/context";
|
|
82
82
|
import type { ManagementPrincipal, ManagementSessionControl } from "./management-auth";
|
|
83
|
+
import type { ManagementRequestIngress } from "./management/context";
|
|
83
84
|
export type { ManagementApiDeps } from "./management/context";
|
|
84
85
|
import { fetchAllModels } from "./management/shared";
|
|
85
86
|
import { CatalogGatherBusyError } from "../codex/catalog/provider-fetch";
|
|
@@ -155,6 +156,16 @@ async function handleWorkflowBudgetRoutesOnDemand(ctx: ManagementContext): Promi
|
|
|
155
156
|
return handleWorkflowBudgetRoutes(ctx);
|
|
156
157
|
}
|
|
157
158
|
|
|
159
|
+
/**
|
|
160
|
+
* Lazy like the Lab and routing-profile handlers: the protocol planner reaches the router and
|
|
161
|
+
* the ingress eligibility rules, which no other dashboard request needs.
|
|
162
|
+
*/
|
|
163
|
+
async function handleProtocolRoutesOnDemand(ctx: ManagementContext): Promise<Response | null> {
|
|
164
|
+
if (!pathInManagementNamespace(ctx.url.pathname, "/api/protocols")) return null;
|
|
165
|
+
const { handleProtocolRoutes } = await import("./management/protocol-routes");
|
|
166
|
+
return handleProtocolRoutes(ctx);
|
|
167
|
+
}
|
|
168
|
+
|
|
158
169
|
async function handleGrokCouponRoutesOnDemand(ctx: ManagementContext): Promise<Response | null> {
|
|
159
170
|
if (!pathInManagementNamespace(ctx.url.pathname, "/api/grok/reset-coupons", true)) return null;
|
|
160
171
|
const { handleGrokCouponRoutes } = await import("./management/grok-coupon-routes");
|
|
@@ -184,6 +195,12 @@ async function handleRemoteWorkspaceRoutesOnDemand(ctx: ManagementContext): Prom
|
|
|
184
195
|
return handleRemoteWorkspaceRoutes(ctx);
|
|
185
196
|
}
|
|
186
197
|
|
|
198
|
+
async function handleLinkRoutesOnDemand(ctx: ManagementContext): Promise<Response | null> {
|
|
199
|
+
if (!pathInManagementNamespace(ctx.url.pathname, "/api/link")) return null;
|
|
200
|
+
const { handleLinkRoutes } = await import("./management/link-routes");
|
|
201
|
+
return handleLinkRoutes(ctx);
|
|
202
|
+
}
|
|
203
|
+
|
|
187
204
|
export async function handleManagementAPI(
|
|
188
205
|
req: Request,
|
|
189
206
|
url: URL,
|
|
@@ -191,6 +208,7 @@ export async function handleManagementAPI(
|
|
|
191
208
|
deps: ManagementApiDeps = {},
|
|
192
209
|
principal?: ManagementPrincipal,
|
|
193
210
|
sessionControl?: ManagementSessionControl,
|
|
211
|
+
requestIngress: ManagementRequestIngress = { trustedLoopback: false },
|
|
194
212
|
): Promise<Response | null> {
|
|
195
213
|
if (!isAllowedManagementOrigin(req, config)) {
|
|
196
214
|
return jsonResponse({ error: "cross-origin request blocked" }, 403, req, config);
|
|
@@ -273,10 +291,16 @@ export async function handleManagementAPI(
|
|
|
273
291
|
}
|
|
274
292
|
} catch { /* best-effort */ }
|
|
275
293
|
}
|
|
276
|
-
const ctx: ManagementContext = {
|
|
294
|
+
const ctx: ManagementContext = {
|
|
295
|
+
req, url, config, deps, version: VERSION, principal, sessionControl,
|
|
296
|
+
trustedLoopbackIngress: requestIngress.trustedLoopback,
|
|
297
|
+
guiSessionIssuance: requestIngress.guiSessionIssuance ?? null,
|
|
298
|
+
convergeCodexCatalog, syncClaudeAgentDefsBestEffort,
|
|
299
|
+
};
|
|
277
300
|
let routed: Response | null | undefined;
|
|
278
301
|
try {
|
|
279
302
|
routed = handleSessionRoutes(ctx)
|
|
303
|
+
?? (await handleLinkRoutesOnDemand(ctx))
|
|
280
304
|
?? (await handleRemoteWorkspaceRoutesOnDemand(ctx))
|
|
281
305
|
?? (await handleConfigRoutes(ctx))
|
|
282
306
|
?? (await handleStorageLogGuardRoutes(ctx))
|
|
@@ -284,6 +308,7 @@ export async function handleManagementAPI(
|
|
|
284
308
|
?? (await handleRequestHistoryRoutes(ctx))
|
|
285
309
|
?? (await handleQuotaResetRoutesOnDemand(ctx))
|
|
286
310
|
?? (await handleWorkflowBudgetRoutesOnDemand(ctx))
|
|
311
|
+
?? (await handleProtocolRoutesOnDemand(ctx))
|
|
287
312
|
?? (await handleGrokCouponRoutesOnDemand(ctx))
|
|
288
313
|
?? (await handleAnthropicResetGrantRoutesOnDemand(ctx))
|
|
289
314
|
?? handleMetricsRoutes(ctx)
|
|
@@ -71,6 +71,7 @@ import {
|
|
|
71
71
|
type GuiSessionRecord,
|
|
72
72
|
type GuiSessionRequestContext,
|
|
73
73
|
} from "./gui-session";
|
|
74
|
+
import { hasLocalDesktopSnapshotCapability } from "./local-desktop-snapshot-auth";
|
|
74
75
|
export type { GuiSessionBootstrap, GuiSessionRequestContext } from "./gui-session";
|
|
75
76
|
|
|
76
77
|
const LOCAL_READ_REPLAY_LIMIT = 256;
|
|
@@ -260,6 +261,17 @@ export function issueGuiSession(
|
|
|
260
261
|
return issueGuiSessionFromState(req, config, state, context);
|
|
261
262
|
}
|
|
262
263
|
|
|
264
|
+
/** Returns the recorded issuance for the session that authorized this request. */
|
|
265
|
+
export function managementSessionIssuance(
|
|
266
|
+
req: Request,
|
|
267
|
+
state: ManagementAuthState,
|
|
268
|
+
): import("./gui-session").GuiSessionIssuance | null {
|
|
269
|
+
if (!state.available) return null;
|
|
270
|
+
const credential = requestManagementCredential(req);
|
|
271
|
+
if (!credential || equalSecret(credential, state.token)) return null;
|
|
272
|
+
return state.sessions.get(credential)?.issuance ?? null;
|
|
273
|
+
}
|
|
274
|
+
|
|
263
275
|
export interface ManagementSessionControl {
|
|
264
276
|
revokeCurrent(req: Request): boolean;
|
|
265
277
|
/** Revalidate a long-lived request against current authority, without cached admission or renewal. */
|
|
@@ -315,8 +327,8 @@ export function createManagementSessionControl(state: ManagementAuthState): Mana
|
|
|
315
327
|
* per-session CSRF token match. Consent-bearing routes must key off this value
|
|
316
328
|
* rather than off request headers, which the token holder can forge freely.
|
|
317
329
|
* The capability principals are process-scoped HMACs bound to the current process
|
|
318
|
-
* PID and listening port. Local reads are accepted only for
|
|
319
|
-
* restart and provider reload
|
|
330
|
+
* PID and listening port. Local reads are accepted only for allowlisted GET paths;
|
|
331
|
+
* snapshot, restart and provider reload use separate contracts for their exact POSTs.
|
|
320
332
|
*/
|
|
321
333
|
export type ManagementPrincipal =
|
|
322
334
|
| "admin-token"
|
|
@@ -324,6 +336,7 @@ export type ManagementPrincipal =
|
|
|
324
336
|
| "gui-session"
|
|
325
337
|
| "gui-pair-capability"
|
|
326
338
|
| "local-read-capability"
|
|
339
|
+
| "local-desktop-snapshot-capability"
|
|
327
340
|
| "local-provider-reload-capability"
|
|
328
341
|
| "local-aside-sync-capability"
|
|
329
342
|
| "system-restart-capability";
|
|
@@ -375,8 +388,6 @@ function hasLocalReadCapability(
|
|
|
375
388
|
} catch {
|
|
376
389
|
return false;
|
|
377
390
|
}
|
|
378
|
-
// Do not let a future query-bearing variant silently inherit this narrow grant.
|
|
379
|
-
if (url.search !== "") return false;
|
|
380
391
|
const expectedPid = parseExpectedLocalManagementPid(
|
|
381
392
|
req.headers.get(LOCAL_MANAGEMENT_EXPECTED_PID_HEADER),
|
|
382
393
|
);
|
|
@@ -391,7 +402,9 @@ function hasLocalReadCapability(
|
|
|
391
402
|
local.attestationSecret,
|
|
392
403
|
req.headers.get(LOCAL_MANAGEMENT_NONCE_HEADER),
|
|
393
404
|
req.method,
|
|
394
|
-
|
|
405
|
+
// The query is signed into the capability, so a grant for one range cannot be replayed
|
|
406
|
+
// against another — the grant stays exactly as narrow as the request it was minted for.
|
|
407
|
+
url.pathname + url.search,
|
|
395
408
|
local.pid,
|
|
396
409
|
local.port,
|
|
397
410
|
expiresAt,
|
|
@@ -550,6 +563,7 @@ function resolveManagementAdmission(
|
|
|
550
563
|
if (hasSystemRestartCapability(req, local)) principal = "system-restart-capability";
|
|
551
564
|
else if (hasLocalAsideSyncCapability(req, local)) principal = "local-aside-sync-capability";
|
|
552
565
|
else if (hasLocalProviderReloadCapability(req, local)) principal = "local-provider-reload-capability";
|
|
566
|
+
else if (hasLocalDesktopSnapshotCapability(req, local)) principal = "local-desktop-snapshot-capability";
|
|
553
567
|
else if (hasLocalReadCapability(req, local)) principal = "local-read-capability";
|
|
554
568
|
else if (hasGuiPairCapability(req, local)) principal = "gui-pair-capability";
|
|
555
569
|
else if (state.available) {
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether a settled Messages route may take the managed native Messages lane (PF-08).
|
|
3
|
+
*
|
|
4
|
+
* PURE: reads the route, the body's structure and config, and nothing else. The Messages ingress,
|
|
5
|
+
* `count_tokens` and the protocol planner (`src/protocols/plan-snapshot.ts`) all ask here, so a
|
|
6
|
+
* preview, a count and the real send cannot disagree about the lane.
|
|
7
|
+
*/
|
|
8
|
+
import { anthropicBodyElidesBlockedSkill } from "../claude/inbound";
|
|
9
|
+
import { isClaudeWebSearchToolName } from "../claude/outbound";
|
|
10
|
+
import { isAnthropicAccountPoolEnabled } from "../oauth/anthropic-routing";
|
|
11
|
+
import type { ProtocolReasonCode } from "../protocols/contract";
|
|
12
|
+
import { featuresFromMessagesBody } from "../protocols/features";
|
|
13
|
+
import { credentialDomainFor } from "../protocols/opaque-state";
|
|
14
|
+
import { resolveProtocolSettings } from "../protocols/settings";
|
|
15
|
+
import type { RouteResult } from "../router";
|
|
16
|
+
import type { OcxConfig } from "../types";
|
|
17
|
+
import { requiresVisionPreprocessing } from "../vision";
|
|
18
|
+
import { resolvePinnedEffort } from "./effort-policy";
|
|
19
|
+
|
|
20
|
+
/** Why the managed native Messages lane declines a route, as a protocol reason code. */
|
|
21
|
+
export type NativeMessagesDeclineReason = Extract<
|
|
22
|
+
ProtocolReasonCode,
|
|
23
|
+
| "rollout-disabled"
|
|
24
|
+
| "cross-wire-ir"
|
|
25
|
+
| "auth-mode-not-native"
|
|
26
|
+
| "oauth-account-pool"
|
|
27
|
+
| "combo-or-policy-route"
|
|
28
|
+
| "effort-row"
|
|
29
|
+
| "fast-row"
|
|
30
|
+
| "vision-preprocessing"
|
|
31
|
+
| "bridge-only-policy"
|
|
32
|
+
>;
|
|
33
|
+
|
|
34
|
+
/** Request facts the body cannot carry. */
|
|
35
|
+
export interface NativeMessagesSelector {
|
|
36
|
+
/** A synthetic effort row was requested. */
|
|
37
|
+
effortRow?: boolean;
|
|
38
|
+
/** A synthetic fast row was requested. */
|
|
39
|
+
fastRow?: boolean;
|
|
40
|
+
/** The model id the bridge would parse (the translated body's `model`), for the effort pin. */
|
|
41
|
+
routeSelector?: string;
|
|
42
|
+
/** The Claude settings this ingress reads (intercept bindings applied); defaults to config's. */
|
|
43
|
+
claudeCode?: OcxConfig["claudeCode"];
|
|
44
|
+
/**
|
|
45
|
+
* Runtime fact, supplied by a caller that is about to send: two or more usable Anthropic OAuth
|
|
46
|
+
* accounts are stored, so the bridge would rotate accounts on a 429
|
|
47
|
+
* (`hasAnthropicFailoverQuorum`). It reads the account store, so the planner never supplies it
|
|
48
|
+
* and judges OAuth from config alone.
|
|
49
|
+
*/
|
|
50
|
+
oauthFailoverQuorum?: boolean;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
type Rec = Record<string, unknown>;
|
|
54
|
+
function isRec(value: unknown): value is Rec {
|
|
55
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Whether the bridge could hand this request to the web-search sidecar: a `web_search*` server
|
|
60
|
+
* tool the tool choice does not exclude, with the sidecar not disabled in the Claude replay config
|
|
61
|
+
* (`buildClaudeReplayConfig` merges `claudeCode.webSearchSidecar` over the global block). Whether
|
|
62
|
+
* a backend credential then exists is decided at dispatch and is not read here, so this errs
|
|
63
|
+
* toward the bridge, which is today's behavior for every such request.
|
|
64
|
+
*/
|
|
65
|
+
function webSearchSidecarMayEngage(body: Readonly<Rec>, config: OcxConfig): boolean {
|
|
66
|
+
const sidecar = { ...config.webSearchSidecar, ...config.claudeCode?.webSearchSidecar };
|
|
67
|
+
if (sidecar.enabled === false) return false;
|
|
68
|
+
if (!Array.isArray(body.tools)) return false;
|
|
69
|
+
const declared = body.tools.some(tool => isRec(tool) && typeof tool.type === "string" && tool.type.startsWith("web_search"));
|
|
70
|
+
if (!declared) return false;
|
|
71
|
+
const choice = body.tool_choice;
|
|
72
|
+
if (isRec(choice)) {
|
|
73
|
+
if (choice.type === "none") return false;
|
|
74
|
+
if (choice.type === "tool") return typeof choice.name === "string" && isClaudeWebSearchToolName(choice.name);
|
|
75
|
+
}
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Operator policy the translated path applies and the native lane would skip: a pinned
|
|
81
|
+
* reasoning effort for the route, a blocked-skill bundle the translator would elide, or the
|
|
82
|
+
* web-search sidecar. Any of them keeps the request on the bridge.
|
|
83
|
+
*/
|
|
84
|
+
function bridgeOnlyPolicyApplies(
|
|
85
|
+
route: RouteResult,
|
|
86
|
+
body: Readonly<Rec>,
|
|
87
|
+
config: OcxConfig,
|
|
88
|
+
selector: NativeMessagesSelector,
|
|
89
|
+
): boolean {
|
|
90
|
+
if (resolvePinnedEffort(route, selector.routeSelector, config) !== undefined) return true;
|
|
91
|
+
if (anthropicBodyElidesBlockedSkill(body, selector.claudeCode ?? config.claudeCode)) return true;
|
|
92
|
+
return webSearchSidecarMayEngage(body, config);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* The credential rule. A proxy-managed key is native since PF-08. An Anthropic OAuth account
|
|
97
|
+
* is native only with `managedMessagesNativeOAuth` on (itself effective only with
|
|
98
|
+
* `managedMessagesNative`), only for the `anthropic` provider the OAuth store serves, and only
|
|
99
|
+
* to `api.anthropic.com`. A pooled account set declines: rotation, session affinity and quota
|
|
100
|
+
* ranking live in the Responses pipeline's transport and are not replicated here. `forward`
|
|
101
|
+
* belongs to the caller.
|
|
102
|
+
*/
|
|
103
|
+
function credentialDeclineReason(
|
|
104
|
+
route: RouteResult,
|
|
105
|
+
config: OcxConfig,
|
|
106
|
+
selector: NativeMessagesSelector,
|
|
107
|
+
): NativeMessagesDeclineReason | undefined {
|
|
108
|
+
const provider = route.provider;
|
|
109
|
+
if (provider.authMode === undefined || provider.authMode === "key") return undefined;
|
|
110
|
+
if (provider.authMode !== "oauth") return "auth-mode-not-native";
|
|
111
|
+
if (!resolveProtocolSettings(config).rollout.managedMessagesNativeOAuth) return "auth-mode-not-native";
|
|
112
|
+
if (route.providerName !== "anthropic") return "auth-mode-not-native";
|
|
113
|
+
if (!credentialDomainFor(provider)?.firstPartyAnthropic) return "auth-mode-not-native";
|
|
114
|
+
if (isAnthropicAccountPoolEnabled(config) || selector.oauthFailoverQuorum === true) return "oauth-account-pool";
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* The first rule that keeps a Messages request off the managed native lane, or `undefined` when
|
|
120
|
+
* the route is eligible.
|
|
121
|
+
*
|
|
122
|
+
* - the `protocols.rollout.managedMessagesNative` switch is off;
|
|
123
|
+
* - the final adapter is not `anthropic`;
|
|
124
|
+
* - the credential is neither a proxy-managed key nor an eligible Anthropic OAuth account
|
|
125
|
+
* (`credentialDeclineReason`); a pooled OAuth account set is `oauth-account-pool`;
|
|
126
|
+
* - a combo or policy route owns multi-candidate execution in the Responses pipeline;
|
|
127
|
+
* - a synthetic effort or fast row needs the adapter that owns its wire rewrite;
|
|
128
|
+
* - an image would reach a model the operator declared unable to read it;
|
|
129
|
+
* - operator policy only the bridge applies would engage (pinned effort, blocked-skill elision,
|
|
130
|
+
* the web-search sidecar).
|
|
131
|
+
*/
|
|
132
|
+
export function nativeMessagesDeclineReason(
|
|
133
|
+
route: RouteResult,
|
|
134
|
+
body: Readonly<Record<string, unknown>>,
|
|
135
|
+
config: OcxConfig,
|
|
136
|
+
selector: NativeMessagesSelector = {},
|
|
137
|
+
): NativeMessagesDeclineReason | undefined {
|
|
138
|
+
if (!resolveProtocolSettings(config).rollout.managedMessagesNative) return "rollout-disabled";
|
|
139
|
+
const provider = route.provider;
|
|
140
|
+
if (provider.adapter !== "anthropic") return "cross-wire-ir";
|
|
141
|
+
const credentialDecline = credentialDeclineReason(route, config, selector);
|
|
142
|
+
if (credentialDecline) return credentialDecline;
|
|
143
|
+
if (route.combo || route.routeKind === "combo" || route.routeKind === "policy") return "combo-or-policy-route";
|
|
144
|
+
if (selector.effortRow) return "effort-row";
|
|
145
|
+
if (selector.fastRow) return "fast-row";
|
|
146
|
+
if (featuresFromMessagesBody(body).has("request.images")
|
|
147
|
+
&& requiresVisionPreprocessing(config, provider, route.modelId, route.providerName)) {
|
|
148
|
+
return "vision-preprocessing";
|
|
149
|
+
}
|
|
150
|
+
if (bridgeOnlyPolicyApplies(route, body, config, selector)) return "bridge-only-policy";
|
|
151
|
+
return undefined;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function isNativeMessagesRouteEligible(
|
|
155
|
+
route: RouteResult,
|
|
156
|
+
body: Readonly<Record<string, unknown>>,
|
|
157
|
+
config: OcxConfig,
|
|
158
|
+
selector?: NativeMessagesSelector,
|
|
159
|
+
): boolean {
|
|
160
|
+
return nativeMessagesDeclineReason(route, body, config, selector) === undefined;
|
|
161
|
+
}
|