@bitkyc08/opencodex 2.10.0 → 2.10.1
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/AGENTS_INSTALL.md +77 -0
- package/README.md +4 -10
- package/bin/ocx.mjs +71 -18
- package/gui/dist/assets/index-Cd6_PBKn.css +1 -0
- package/gui/dist/assets/index-ChZQsmBY.js +70 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/alibaba-color.svg +1 -1
- package/gui/dist/provider-icons/antigravity-color.svg +1 -1
- package/gui/dist/provider-icons/claude-color.svg +1 -1
- package/gui/dist/provider-icons/cline-color.svg +16 -0
- package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -1
- package/gui/dist/provider-icons/copilot-color.svg +1 -1
- package/gui/dist/provider-icons/cursor-color.svg +1 -1
- package/gui/dist/provider-icons/deepseek-color.svg +1 -1
- package/gui/dist/provider-icons/firepass-color.svg +1 -1
- package/gui/dist/provider-icons/fireworks-color.svg +1 -1
- package/gui/dist/provider-icons/gemini-color.svg +1 -1
- package/gui/dist/provider-icons/github-copilot-color.svg +1 -1
- package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -1
- package/gui/dist/provider-icons/grok.svg +1 -1
- package/gui/dist/provider-icons/groq-color.svg +1 -1
- package/gui/dist/provider-icons/huggingface-color.svg +1 -1
- package/gui/dist/provider-icons/kimi-color.svg +1 -1
- package/gui/dist/provider-icons/kiro-color.svg +2 -2
- package/gui/dist/provider-icons/lm-studio-color.svg +1 -1
- package/gui/dist/provider-icons/mistral-color.svg +1 -1
- package/gui/dist/provider-icons/moonshot-color.svg +1 -1
- package/gui/dist/provider-icons/nvidia-color.svg +1 -1
- package/gui/dist/provider-icons/ollama-color.svg +1 -1
- package/gui/dist/provider-icons/openai.svg +1 -1
- package/gui/dist/provider-icons/opencode.svg +2 -1
- package/gui/dist/provider-icons/openrouter-color.svg +1 -1
- package/gui/dist/provider-icons/pi.svg +2 -2
- package/gui/dist/provider-icons/qianfan-color.svg +1 -1
- package/gui/dist/provider-icons/qwen-portal-color.svg +1 -1
- package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -1
- package/gui/dist/provider-icons/vllm-color.svg +1 -1
- package/gui/dist/provider-icons/xiaomi-color.svg +1 -1
- package/package.json +8 -4
- package/src/adapters/anthropic.ts +208 -14
- package/src/adapters/base.ts +16 -5
- package/src/adapters/cursor/effort-map.ts +3 -2
- package/src/adapters/cursor/framing.ts +39 -0
- package/src/adapters/cursor/live-transport.ts +105 -95
- package/src/adapters/cursor/native-exec.ts +32 -6
- package/src/adapters/cursor/protobuf-request.ts +20 -15
- package/src/adapters/cursor/request-builder.ts +21 -7
- package/src/adapters/cursor/types.ts +7 -0
- package/src/adapters/google-antigravity-replay.ts +237 -21
- package/src/adapters/google-truncation.ts +11 -0
- package/src/adapters/google.ts +50 -9
- package/src/adapters/identity.ts +39 -6
- package/src/adapters/kiro-errors.ts +11 -0
- package/src/adapters/kiro-events.ts +19 -1
- package/src/adapters/kiro-thinking.ts +10 -2
- package/src/adapters/kiro-tools.ts +10 -1
- package/src/adapters/kiro.ts +37 -11
- package/src/adapters/openai-chat.ts +284 -83
- package/src/adapters/openai-responses.ts +182 -24
- package/src/bridge.ts +177 -7
- package/src/chat/outbound.ts +78 -23
- package/src/claude/agents-inject.ts +27 -5
- package/src/claude/inbound.ts +11 -1
- package/src/claude/model-info.ts +13 -10
- package/src/claude/outbound.ts +17 -0
- package/src/cli/account-api.ts +24 -0
- package/src/cli/account-auth.ts +31 -6
- package/src/cli/account-main.ts +317 -0
- package/src/cli/account.ts +5 -0
- package/src/cli/claude.ts +2 -1
- package/src/cli/doctor.ts +93 -22
- package/src/cli/export-command.ts +26 -12
- package/src/cli/help.ts +8 -6
- package/src/cli/index.ts +56 -22
- package/src/cli/integrations.ts +84 -1
- package/src/cli/observe.ts +54 -1
- package/src/cli/opencode.ts +2 -1
- package/src/cli/provider-runtime.ts +18 -1
- package/src/cli/route-policy.ts +92 -0
- package/src/cli/runtime-api.ts +6 -3
- package/src/cli/star-prompt.ts +71 -15
- package/src/cli/status.ts +1 -1
- package/src/cli/v2.ts +36 -9
- package/src/clients/config-export.ts +687 -10
- package/src/codex/account-lifecycle.ts +30 -5
- package/src/codex/account-usability.ts +22 -2
- package/src/codex/admission.ts +255 -0
- package/src/codex/auth-api.ts +427 -140
- package/src/codex/auth-context.ts +155 -30
- package/src/codex/autostart-health.ts +8 -1
- package/src/codex/catalog/account-models.ts +62 -0
- package/src/codex/catalog/aggregation.ts +14 -1
- package/src/codex/catalog/bundled.ts +282 -32
- package/src/codex/catalog/filesystem-evidence.ts +302 -0
- package/src/codex/catalog/metadata.ts +51 -6
- package/src/codex/catalog/parsing.ts +6 -3
- package/src/codex/catalog/provider-fetch.ts +576 -41
- package/src/codex/catalog/sync.ts +505 -66
- package/src/codex/catalog-admission.ts +197 -0
- package/src/codex/catalog-write-serialization.ts +241 -0
- package/src/codex/catalog.ts +2 -1
- package/src/codex/codex-write-lock.ts +372 -0
- package/src/codex/convergence-types.ts +593 -0
- package/src/codex/convergence.ts +441 -0
- package/src/codex/desired-state.ts +177 -0
- package/src/codex/features.ts +52 -8
- package/src/codex/generation.ts +202 -0
- package/src/codex/history-job.ts +257 -0
- package/src/codex/history-lock.ts +241 -0
- package/src/codex/history-migration-guardian.ts +18 -5
- package/src/codex/history-provider.ts +9 -2
- package/src/codex/history-transition.ts +105 -0
- package/src/codex/history-worker.ts +176 -0
- package/src/codex/inject-coordination.ts +245 -0
- package/src/codex/inject.ts +605 -124
- package/src/codex/integration-record.ts +266 -0
- package/src/codex/internal/catalog-writer.ts +203 -0
- package/src/codex/internal/history-writer.ts +80 -0
- package/src/codex/journal.ts +10 -1
- package/src/codex/main-account-cache.ts +24 -0
- package/src/codex/management-convergence.ts +114 -0
- package/src/codex/native-main-admission.ts +47 -0
- package/src/codex/native-main-auth-temp.ts +187 -0
- package/src/codex/native-main-claim.ts +167 -0
- package/src/codex/native-main-lock-file.ts +158 -0
- package/src/codex/native-main-owner.ts +315 -0
- package/src/codex/native-profile-api.ts +247 -0
- package/src/codex/native-profile-manager.ts +1512 -0
- package/src/codex/native-profile-processes.ts +121 -0
- package/src/codex/native-profile-recovery.ts +99 -0
- package/src/codex/native-profile-stage-store.ts +387 -0
- package/src/codex/native-profile-startup.ts +340 -0
- package/src/codex/native-profile-store.ts +855 -0
- package/src/codex/native-profile-types.ts +120 -0
- package/src/codex/native-residue.ts +557 -0
- package/src/codex/project-config-warnings.ts +18 -4
- package/src/codex/prompt-journal.ts +311 -0
- package/src/codex/prompt-layers.ts +967 -0
- package/src/codex/prompt-lock.ts +143 -0
- package/src/codex/quota-rejection.ts +224 -0
- package/src/codex/quota.ts +86 -3
- package/src/codex/routing.ts +299 -62
- package/src/codex/runtime.ts +159 -38
- package/src/codex/shim.ts +39 -13
- package/src/codex/subagent-model-fallback.ts +73 -12
- package/src/codex/transition-state.ts +604 -0
- package/src/codex/upstream-host-health.ts +70 -0
- package/src/codex/user-identity.ts +266 -0
- package/src/codex/write-coordination.ts +114 -0
- package/src/config.ts +562 -26
- package/src/generated/jawcode-model-metadata.ts +2 -2
- package/src/grok/inject.ts +15 -4
- package/src/grok/inspect.ts +45 -0
- package/src/images/loop.ts +113 -20
- package/src/integrations/config-io.ts +151 -0
- package/src/integrations/journal.ts +315 -0
- package/src/integrations/merge.ts +135 -0
- package/src/integrations/native/ownership-preflight.ts +165 -0
- package/src/integrations/ownership.ts +111 -0
- package/src/integrations/registry.ts +101 -0
- package/src/integrations/serialize.ts +235 -0
- package/src/integrations/state.ts +290 -0
- package/src/integrations/store.ts +103 -0
- package/src/integrations/writer.ts +492 -0
- package/src/lib/bounded-body.ts +46 -8
- package/src/lib/bun-runtime.ts +110 -1
- package/src/lib/bun-stream-caps.ts +2 -1
- package/src/lib/redact.ts +407 -2
- package/src/lib/shadow-call.ts +24 -0
- package/src/lib/translator-budget.ts +10 -0
- package/src/lib/upstream-reachability.ts +91 -0
- package/src/lib/upstream-retry.ts +154 -2
- package/src/lib/windows-secret-acl.ts +212 -11
- package/src/lib/winsw.ts +9 -3
- package/src/oauth/index.ts +61 -3
- package/src/oauth/key-providers.ts +4 -0
- package/src/oauth/kiro.ts +50 -6
- package/src/oauth/store.ts +31 -6
- package/src/oauth/token-guardian.ts +9 -3
- package/src/providers/codex-capacity.ts +288 -0
- package/src/providers/derive.ts +33 -1
- package/src/providers/free-directory.ts +3 -1
- package/src/providers/key-failover.ts +71 -3
- package/src/providers/openai-sidecar.ts +64 -4
- package/src/providers/openai-virtual-models.ts +1 -0
- package/src/providers/quota.ts +334 -26
- package/src/providers/registry.ts +284 -16
- package/src/providers/xai-transport.ts +11 -4
- package/src/responses/compaction.ts +8 -1
- package/src/responses/hosted-tool-policy.ts +9 -0
- package/src/responses/parser.ts +44 -2
- package/src/responses/reasoning-envelope.ts +9 -1
- package/src/responses/reasoning-replay-cache.ts +105 -0
- package/src/responses/spill-store.ts +45 -8
- package/src/responses/state.ts +161 -17
- package/src/router.ts +302 -16
- package/src/routing/analytics.ts +377 -0
- package/src/routing/capability.ts +204 -0
- package/src/routing/cost.ts +77 -0
- package/src/routing/evaluator.ts +444 -0
- package/src/routing/health.ts +401 -0
- package/src/routing/history/cursor.ts +43 -0
- package/src/routing/history/indexer.ts +590 -0
- package/src/routing/history/schema.ts +72 -0
- package/src/routing/profile.ts +423 -0
- package/src/routing/quota.ts +124 -0
- package/src/routing/request-evidence.ts +45 -0
- package/src/routing/trace.ts +686 -0
- package/src/server/auth-cors.ts +46 -6
- package/src/server/chat-completions.ts +28 -13
- package/src/server/claude-messages.ts +23 -15
- package/src/server/gui-static.ts +39 -10
- package/src/server/images.ts +10 -1
- package/src/server/index.ts +238 -52
- package/src/server/lifecycle.ts +228 -9
- package/src/server/live.ts +14 -3
- package/src/server/management/agent-settings-routes.ts +64 -14
- package/src/server/management/combo-routes.ts +5 -5
- package/src/server/management/config-routes.ts +1 -1
- package/src/server/management/context.ts +42 -2
- package/src/server/management/integration-routes.ts +538 -0
- package/src/server/management/logs-usage-routes.ts +1 -1
- package/src/server/management/model-routes.ts +32 -113
- package/src/server/management/model-rows.ts +117 -0
- package/src/server/management/native-integration-routes.ts +587 -0
- package/src/server/management/oauth-account-routes.ts +1 -1
- package/src/server/management/provider-routes.ts +218 -117
- package/src/server/management/request-history-routes.ts +191 -0
- package/src/server/management/routing-analytics-routes.ts +74 -0
- package/src/server/management/routing-profile-routes.ts +177 -0
- package/src/server/management/shared.ts +2 -2
- package/src/server/management/sidebar-routes.ts +47 -31
- package/src/server/management/sync-response.ts +69 -0
- package/src/server/management/system-restart.ts +276 -24
- package/src/server/management/system-routes.ts +4 -0
- package/src/server/management-api.ts +84 -9
- package/src/server/management-auth.ts +43 -5
- package/src/server/relay-eager.ts +82 -42
- package/src/server/relay.ts +120 -6
- package/src/server/request-log.ts +26 -6
- package/src/server/responses/collaboration.ts +63 -8
- package/src/server/responses/compact.ts +272 -41
- package/src/server/responses/core.ts +730 -132
- package/src/server/responses/fetch-helpers.ts +15 -1
- package/src/server/responses-item-id-repair.ts +32 -3
- package/src/server/responses-json-events.ts +52 -0
- package/src/server/responses-snapshot-repair.ts +621 -0
- package/src/server/search.ts +51 -6
- package/src/server/sse-payload-rewrite.ts +89 -12
- package/src/server/startup-health-cache.ts +7 -1
- package/src/server/ws-bridge.ts +11 -17
- package/src/service-manager-probe.ts +297 -0
- package/src/service.ts +222 -32
- package/src/tray/windows-tray.ps1 +9 -0
- package/src/tray/windows.ts +15 -7
- package/src/types.ts +194 -14
- package/src/update/index.ts +13 -13
- package/src/update/job.ts +24 -21
- package/src/update/notify.ts +7 -3
- package/src/usage/cost.ts +0 -0
- package/src/usage/expected-prices.ts +129 -10
- package/src/usage/log.ts +50 -15
- package/src/usage/summary.ts +4 -4
- package/src/vision/index.ts +6 -1
- package/src/web-search/loop.ts +161 -34
- package/gui/dist/assets/index-OY43ubAq.css +0 -1
- package/gui/dist/assets/index-YwNnKZcL.js +0 -67
- package/gui/dist/provider-icons/antigravity.svg +0 -1
- package/gui/dist/provider-icons/claude.svg +0 -1
- package/gui/dist/provider-icons/copilot.svg +0 -1
- package/gui/dist/provider-icons/cursor.svg +0 -2
- package/gui/dist/provider-icons/gemini.svg +0 -1
- package/gui/dist/provider-icons/grok-color.svg +0 -1
- package/gui/dist/provider-icons/kiro.svg +0 -14
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic policy-profile evaluator (RI-04 core; extended by RI-05..08).
|
|
3
|
+
*
|
|
4
|
+
* RI-04 evaluates hard capability requirements against supplied evidence and
|
|
5
|
+
* scores by deterministic configured priority only. It never dispatches an
|
|
6
|
+
* upstream request; execution wiring arrives with RI-05.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { OcxConfig } from "../types";
|
|
10
|
+
import {
|
|
11
|
+
buildRouteDecisionTrace,
|
|
12
|
+
MAX_REQUIREMENTS,
|
|
13
|
+
type RouteCapabilityEvidence,
|
|
14
|
+
type RouteCostEvidence,
|
|
15
|
+
type RouteDecisionTraceV1,
|
|
16
|
+
type RouteExclusionReason,
|
|
17
|
+
type RouteHealthEvidence,
|
|
18
|
+
type RouteQuotaEvidence,
|
|
19
|
+
type RouteRequirementEvidence,
|
|
20
|
+
type RouteScoreEvidence,
|
|
21
|
+
type Unknownable,
|
|
22
|
+
} from "./trace";
|
|
23
|
+
import { getRoutingProfile, policyModelId, type NormalizedRoutingProfile } from "./profile";
|
|
24
|
+
import { healthScore } from "./health";
|
|
25
|
+
import { quotaScore } from "./quota";
|
|
26
|
+
import { costScore } from "./cost";
|
|
27
|
+
|
|
28
|
+
/** Unknown health under "penalize": a low-but-not-zero deterministic floor. */
|
|
29
|
+
export const HEALTH_UNKNOWN_PENALTY_SCORE = 0.3;
|
|
30
|
+
/** Unknown health under "allow": neutral midpoint of the [0,1] health scale. */
|
|
31
|
+
export const HEALTH_UNKNOWN_NEUTRAL_SCORE = 0.5;
|
|
32
|
+
/** Unknown quota under "penalize": deterministic low floor. */
|
|
33
|
+
export const QUOTA_UNKNOWN_PENALTY_SCORE = 0.3;
|
|
34
|
+
/** Unknown cost under "penalize": deterministic low floor. */
|
|
35
|
+
export const COST_UNKNOWN_PENALTY_SCORE = 0.3;
|
|
36
|
+
|
|
37
|
+
export interface PolicyRequestEvidence {
|
|
38
|
+
/** Required context window for this request (tokens). */
|
|
39
|
+
contextWindow?: number;
|
|
40
|
+
toolsRequired?: boolean;
|
|
41
|
+
imageInputRequired?: boolean;
|
|
42
|
+
structuredOutputRequired?: boolean;
|
|
43
|
+
reasoningEffort?: string;
|
|
44
|
+
serviceTier?: string;
|
|
45
|
+
encryptedCodexTask?: boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface PolicyCandidateEvidence {
|
|
49
|
+
provider: string;
|
|
50
|
+
model: string;
|
|
51
|
+
accountRef?: string;
|
|
52
|
+
/** Codex pool account id (provider "openai"); used to derive account-scoped quota evidence. */
|
|
53
|
+
codexAccountId?: string;
|
|
54
|
+
capability?: RouteCapabilityEvidence;
|
|
55
|
+
health?: RouteHealthEvidence;
|
|
56
|
+
quota?: RouteQuotaEvidence;
|
|
57
|
+
cost?: RouteCostEvidence;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface PolicyEvaluationCandidate {
|
|
61
|
+
provider: string;
|
|
62
|
+
model: string;
|
|
63
|
+
accountRef?: string;
|
|
64
|
+
eligible: boolean;
|
|
65
|
+
exclusions: RouteExclusionReason[];
|
|
66
|
+
requirements: RouteRequirementEvidence[];
|
|
67
|
+
capability?: RouteCapabilityEvidence;
|
|
68
|
+
health?: RouteHealthEvidence;
|
|
69
|
+
quota?: RouteQuotaEvidence;
|
|
70
|
+
cost?: RouteCostEvidence;
|
|
71
|
+
score?: RouteScoreEvidence;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface PolicyEvaluationResult {
|
|
75
|
+
profileId: string;
|
|
76
|
+
profileRevision: string;
|
|
77
|
+
candidates: PolicyEvaluationCandidate[];
|
|
78
|
+
selectedIndex: number | null;
|
|
79
|
+
trace: RouteDecisionTraceV1;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function booleanRequirement(
|
|
83
|
+
id: string,
|
|
84
|
+
required: boolean | undefined,
|
|
85
|
+
actual: Unknownable | undefined,
|
|
86
|
+
): RouteRequirementEvidence | null {
|
|
87
|
+
if (required === undefined) return null;
|
|
88
|
+
// Note: `required === false` is a negative assertion ("must NOT have X"),
|
|
89
|
+
// not "don't care"; an absent field is the only "no requirement" form.
|
|
90
|
+
if (actual === undefined || actual === "unknown") {
|
|
91
|
+
return { id, expected: required, outcome: "unknown" };
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
id,
|
|
95
|
+
expected: required,
|
|
96
|
+
actual: typeof actual === "boolean" ? actual : String(actual),
|
|
97
|
+
outcome: typeof actual === "boolean" && actual === required ? "satisfied" : "unsatisfied",
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function requirementFor(
|
|
102
|
+
require: NormalizedRoutingProfile["require"],
|
|
103
|
+
capability: RouteCapabilityEvidence | undefined,
|
|
104
|
+
quota: RouteQuotaEvidence | undefined,
|
|
105
|
+
): RouteRequirementEvidence[] {
|
|
106
|
+
const requirements: RouteRequirementEvidence[] = [];
|
|
107
|
+
if (require.minContextWindow !== undefined) {
|
|
108
|
+
const actual = capability?.contextWindow;
|
|
109
|
+
if (typeof actual === "number") {
|
|
110
|
+
requirements.push({
|
|
111
|
+
id: "min-context-window",
|
|
112
|
+
expected: require.minContextWindow,
|
|
113
|
+
actual,
|
|
114
|
+
outcome: actual >= require.minContextWindow ? "satisfied" : "unsatisfied",
|
|
115
|
+
});
|
|
116
|
+
} else {
|
|
117
|
+
requirements.push({ id: "min-context-window", expected: require.minContextWindow, outcome: "unknown" });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
// minQuotaHeadroom gates only KNOWN quota via the normalized score, which
|
|
121
|
+
// maps exhaustion to zero and unknown/incomplete evidence to null. Unknown
|
|
122
|
+
// quota is governed by the profile's `unknownEvidence.quota` policy
|
|
123
|
+
// (exclude / penalize / allow) via the quota score path - never by the
|
|
124
|
+
// capability unknown policy.
|
|
125
|
+
if (require.minQuotaHeadroom !== undefined) {
|
|
126
|
+
const quotaHeadroom = quotaScore(quota);
|
|
127
|
+
if (quotaHeadroom !== null) {
|
|
128
|
+
requirements.push({
|
|
129
|
+
id: "min-quota-headroom",
|
|
130
|
+
expected: require.minQuotaHeadroom,
|
|
131
|
+
actual: quotaHeadroom,
|
|
132
|
+
outcome: quotaHeadroom >= require.minQuotaHeadroom ? "satisfied" : "unsatisfied",
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
const tools = booleanRequirement("tools", require.tools, capability?.tools);
|
|
137
|
+
if (tools) requirements.push(tools);
|
|
138
|
+
const image = booleanRequirement("image-input", require.imageInput, capability?.image);
|
|
139
|
+
if (image) requirements.push(image);
|
|
140
|
+
const structured = booleanRequirement("structured-output", require.structuredOutput, capability?.structuredOutput);
|
|
141
|
+
if (structured) requirements.push(structured);
|
|
142
|
+
if (require.reasoningEffort !== undefined) {
|
|
143
|
+
const ladder = capability?.reasoningEfforts;
|
|
144
|
+
if (Array.isArray(ladder)) {
|
|
145
|
+
requirements.push({
|
|
146
|
+
id: "reasoning-effort",
|
|
147
|
+
expected: require.reasoningEffort,
|
|
148
|
+
actual: ladder.join(","),
|
|
149
|
+
outcome: ladder.includes(require.reasoningEffort) ? "satisfied" : "unsatisfied",
|
|
150
|
+
});
|
|
151
|
+
} else {
|
|
152
|
+
requirements.push({ id: "reasoning-effort", expected: require.reasoningEffort, outcome: "unknown" });
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (require.serviceTier !== undefined) {
|
|
156
|
+
const actual = capability?.serviceTier;
|
|
157
|
+
if (actual === undefined || actual === "unknown") {
|
|
158
|
+
requirements.push({ id: "service-tier", expected: require.serviceTier, outcome: "unknown" });
|
|
159
|
+
} else {
|
|
160
|
+
requirements.push({
|
|
161
|
+
id: "service-tier",
|
|
162
|
+
expected: require.serviceTier,
|
|
163
|
+
actual: String(actual),
|
|
164
|
+
outcome: actual === require.serviceTier ? "satisfied" : "unsatisfied",
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
const local = booleanRequirement("local-only", require.localOnly, capability?.localOnly);
|
|
169
|
+
if (local) requirements.push(local);
|
|
170
|
+
const remote = booleanRequirement("remote-allowed", require.remoteAllowed, capability?.remoteAllowed);
|
|
171
|
+
if (remote) requirements.push(remote);
|
|
172
|
+
const encrypted = booleanRequirement(
|
|
173
|
+
"encrypted-codex-tasks",
|
|
174
|
+
require.encryptedCodexTasks,
|
|
175
|
+
capability?.encryptedCodexTasks,
|
|
176
|
+
);
|
|
177
|
+
if (encrypted) requirements.push(encrypted);
|
|
178
|
+
return requirements;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function unsatisfiedOrUnknown(requirements: RouteRequirementEvidence[]): RouteRequirementEvidence[] {
|
|
182
|
+
return requirements.filter(requirement => requirement.outcome !== "satisfied");
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function configuredPriorityScore(index: number, total: number): number {
|
|
186
|
+
return total > 1 ? (total - index) / total : 1;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Requirements derived from the request evidence supplied to a dry-run. A
|
|
191
|
+
* candidate must satisfy both the profile `require` block and the request
|
|
192
|
+
* needs (context window, tools, image input, structured output, reasoning
|
|
193
|
+
* effort, service tier, encrypted Codex tasks) to be eligible.
|
|
194
|
+
*/
|
|
195
|
+
function requestRequirementFor(
|
|
196
|
+
request: PolicyRequestEvidence,
|
|
197
|
+
capability: RouteCapabilityEvidence | undefined,
|
|
198
|
+
): RouteRequirementEvidence[] {
|
|
199
|
+
const requirements: RouteRequirementEvidence[] = [];
|
|
200
|
+
if (request.contextWindow !== undefined) {
|
|
201
|
+
const actual = capability?.contextWindow;
|
|
202
|
+
if (typeof actual === "number") {
|
|
203
|
+
requirements.push({
|
|
204
|
+
id: "request-context-window",
|
|
205
|
+
expected: request.contextWindow,
|
|
206
|
+
actual,
|
|
207
|
+
outcome: actual >= request.contextWindow ? "satisfied" : "unsatisfied",
|
|
208
|
+
});
|
|
209
|
+
} else {
|
|
210
|
+
requirements.push({ id: "request-context-window", expected: request.contextWindow, outcome: "unknown" });
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
// Absent flags mean "no requirement": only a positive request need adds a row.
|
|
214
|
+
if (request.toolsRequired === true) {
|
|
215
|
+
const tools = booleanRequirement("request-tools", true, capability?.tools);
|
|
216
|
+
if (tools) requirements.push(tools);
|
|
217
|
+
}
|
|
218
|
+
if (request.imageInputRequired === true) {
|
|
219
|
+
const image = booleanRequirement("request-image-input", true, capability?.image);
|
|
220
|
+
if (image) requirements.push(image);
|
|
221
|
+
}
|
|
222
|
+
if (request.structuredOutputRequired === true) {
|
|
223
|
+
const structured = booleanRequirement("request-structured-output", true, capability?.structuredOutput);
|
|
224
|
+
if (structured) requirements.push(structured);
|
|
225
|
+
}
|
|
226
|
+
if (request.reasoningEffort !== undefined) {
|
|
227
|
+
const ladder = capability?.reasoningEfforts;
|
|
228
|
+
if (Array.isArray(ladder)) {
|
|
229
|
+
requirements.push({
|
|
230
|
+
id: "request-reasoning-effort",
|
|
231
|
+
expected: request.reasoningEffort,
|
|
232
|
+
actual: ladder.join(","),
|
|
233
|
+
outcome: ladder.includes(request.reasoningEffort) ? "satisfied" : "unsatisfied",
|
|
234
|
+
});
|
|
235
|
+
} else {
|
|
236
|
+
requirements.push({ id: "request-reasoning-effort", expected: request.reasoningEffort, outcome: "unknown" });
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
if (request.serviceTier !== undefined) {
|
|
240
|
+
const actual = capability?.serviceTier;
|
|
241
|
+
if (actual === undefined || actual === "unknown") {
|
|
242
|
+
requirements.push({ id: "request-service-tier", expected: request.serviceTier, outcome: "unknown" });
|
|
243
|
+
} else {
|
|
244
|
+
requirements.push({
|
|
245
|
+
id: "request-service-tier",
|
|
246
|
+
expected: request.serviceTier,
|
|
247
|
+
actual: String(actual),
|
|
248
|
+
outcome: actual === request.serviceTier ? "satisfied" : "unsatisfied",
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
if (request.encryptedCodexTask === true) {
|
|
253
|
+
const encrypted = booleanRequirement(
|
|
254
|
+
"request-encrypted-codex-tasks",
|
|
255
|
+
true,
|
|
256
|
+
capability?.encryptedCodexTasks,
|
|
257
|
+
);
|
|
258
|
+
if (encrypted) requirements.push(encrypted);
|
|
259
|
+
}
|
|
260
|
+
return requirements;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Evaluate a profile against request + candidate evidence. Deterministic:
|
|
265
|
+
* candidates are scored in declaration order, ties break by earlier index.
|
|
266
|
+
*/
|
|
267
|
+
export function evaluatePolicyProfile(
|
|
268
|
+
config: OcxConfig,
|
|
269
|
+
profileId: string,
|
|
270
|
+
requestEvidence: PolicyRequestEvidence,
|
|
271
|
+
candidateEvidence: PolicyCandidateEvidence[],
|
|
272
|
+
now = Date.now(),
|
|
273
|
+
): PolicyEvaluationResult {
|
|
274
|
+
const profile = getRoutingProfile(config, profileId);
|
|
275
|
+
if (!profile) throw new Error(`Unknown routing profile: ${profileId}`);
|
|
276
|
+
|
|
277
|
+
const candidates: PolicyEvaluationCandidate[] = [];
|
|
278
|
+
let selectedIndex: number | null = null;
|
|
279
|
+
let bestScore = Number.NEGATIVE_INFINITY;
|
|
280
|
+
|
|
281
|
+
profile.candidates.forEach((declared, index) => {
|
|
282
|
+
const evidence = candidateEvidence.find(
|
|
283
|
+
candidate => candidate.provider === declared.provider && candidate.model === declared.model,
|
|
284
|
+
) ?? { provider: declared.provider, model: declared.model };
|
|
285
|
+
const requirements = [
|
|
286
|
+
...requirementFor(profile.require, evidence.capability, evidence.quota),
|
|
287
|
+
...requestRequirementFor(requestEvidence, evidence.capability),
|
|
288
|
+
];
|
|
289
|
+
const exclusions: RouteExclusionReason[] = [];
|
|
290
|
+
const bad = unsatisfiedOrUnknown(requirements);
|
|
291
|
+
for (const requirement of bad) {
|
|
292
|
+
if (requirement.outcome === "unsatisfied") {
|
|
293
|
+
exclusions.push({ code: "capability-unsatisfied", detail: requirement.id });
|
|
294
|
+
} else {
|
|
295
|
+
exclusions.push({ code: "unknown-capability", detail: requirement.id });
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
const unsatisfied = bad.some(requirement => requirement.outcome === "unsatisfied");
|
|
299
|
+
const unknown = bad.some(requirement => requirement.outcome === "unknown");
|
|
300
|
+
// Unknown capability handling per profile: exclude (default), penalize,
|
|
301
|
+
// or allow. "penalize" cannot move the score while configuredPriorityScore
|
|
302
|
+
// is the only component; the capability score dimension is still future
|
|
303
|
+
// (RI-06+), so "penalize" behaves identically to "allow" in this release.
|
|
304
|
+
const excludedByUnknown = unknown && profile.unknownEvidence.capability === "exclude";
|
|
305
|
+
const costLimit = profile.limits.maxEstimatedCostUsd;
|
|
306
|
+
const estimatedCost = evidence.cost?.estimatedUsd;
|
|
307
|
+
const overCostLimit = costLimit !== undefined
|
|
308
|
+
&& typeof estimatedCost === "number"
|
|
309
|
+
&& Number.isFinite(estimatedCost)
|
|
310
|
+
&& estimatedCost > costLimit;
|
|
311
|
+
if (overCostLimit) {
|
|
312
|
+
exclusions.push({ code: "cost-limit", detail: "maxEstimatedCostUsd" });
|
|
313
|
+
}
|
|
314
|
+
let eligible = !unsatisfied && !excludedByUnknown && !overCostLimit;
|
|
315
|
+
|
|
316
|
+
// Health scoring (RI-06): live hard cooldown is authoritative and
|
|
317
|
+
// excludes; unknown health follows the profile's unknownEvidence policy;
|
|
318
|
+
// historical health never overrides explicit ineligibility.
|
|
319
|
+
const health = evidence.health;
|
|
320
|
+
let healthValue = health ? healthScore(health, now) : null;
|
|
321
|
+
if (health?.cooldownUntilMs !== undefined && health.cooldownUntilMs > now) {
|
|
322
|
+
exclusions.push({ code: "cooldown" });
|
|
323
|
+
eligible = false;
|
|
324
|
+
} else if (healthValue === null && profile.unknownEvidence.health === "exclude") {
|
|
325
|
+
exclusions.push({ code: "unknown-health" });
|
|
326
|
+
eligible = false;
|
|
327
|
+
} else if (healthValue === null && profile.unknownEvidence.health === "penalize") {
|
|
328
|
+
healthValue = HEALTH_UNKNOWN_PENALTY_SCORE;
|
|
329
|
+
} else if (healthValue === null && profile.unknownEvidence.health === "allow") {
|
|
330
|
+
// Neutral midpoint: blending keeps an unknown candidate from outranking
|
|
331
|
+
// a measured one with the same configured priority.
|
|
332
|
+
healthValue = HEALTH_UNKNOWN_NEUTRAL_SCORE;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Quota scoring (RI-07): unknown quota follows the profile policy;
|
|
336
|
+
// exhausted or low-headroom evidence lowers the score.
|
|
337
|
+
const quota = evidence.quota;
|
|
338
|
+
let quotaValue = quota ? quotaScore(quota) : null;
|
|
339
|
+
if (quotaValue === null) {
|
|
340
|
+
if (profile.unknownEvidence.quota === "exclude") {
|
|
341
|
+
exclusions.push({ code: "unknown-quota" });
|
|
342
|
+
eligible = false;
|
|
343
|
+
} else if (profile.unknownEvidence.quota === "penalize") {
|
|
344
|
+
quotaValue = QUOTA_UNKNOWN_PENALTY_SCORE;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Cost scoring (RI-08): the hard per-request ceiling was already checked
|
|
349
|
+
// above; unknown cost follows the profile's unknownEvidence policy.
|
|
350
|
+
const cost = evidence.cost;
|
|
351
|
+
let costValue = cost ? costScore(cost) : null;
|
|
352
|
+
if (costValue === null && profile.unknownEvidence.cost === "exclude") {
|
|
353
|
+
exclusions.push({ code: "unknown-price" });
|
|
354
|
+
eligible = false;
|
|
355
|
+
} else if (costValue === null && profile.unknownEvidence.cost === "penalize") {
|
|
356
|
+
costValue = COST_UNKNOWN_PENALTY_SCORE;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const priorityScore = configuredPriorityScore(index, profile.candidates.length);
|
|
360
|
+
const healthWeight = profile.optimize.health;
|
|
361
|
+
const quotaWeight = profile.optimize.quota;
|
|
362
|
+
const costWeight = profile.optimize.cost;
|
|
363
|
+
// Only spend a dimension's weight when a value is actually present:
|
|
364
|
+
// "allow" leaves missing health/quota/cost components null, so subtracting
|
|
365
|
+
// their weights would shrink the priority share for evidence the profile
|
|
366
|
+
// explicitly permits to be absent. Renormalize those weights back into
|
|
367
|
+
// priority instead of silently changing the ranking semantics.
|
|
368
|
+
const spentHealth = healthValue !== null ? healthWeight : 0;
|
|
369
|
+
const spentQuota = quotaValue !== null ? quotaWeight : 0;
|
|
370
|
+
const spentCost = costValue !== null ? costWeight : 0;
|
|
371
|
+
const priorityWeight = Math.max(0, 1 - spentHealth - spentQuota - spentCost);
|
|
372
|
+
const components: RouteScoreEvidence["components"] = { configuredPriority: priorityScore };
|
|
373
|
+
let total = priorityWeight * priorityScore;
|
|
374
|
+
if (healthWeight > 0 && healthValue !== null) {
|
|
375
|
+
total += healthWeight * healthValue;
|
|
376
|
+
components.health = healthValue;
|
|
377
|
+
}
|
|
378
|
+
if (quotaWeight > 0 && quotaValue !== null) {
|
|
379
|
+
total += quotaWeight * quotaValue;
|
|
380
|
+
components.quota = quotaValue;
|
|
381
|
+
}
|
|
382
|
+
if (costWeight > 0 && costValue !== null) {
|
|
383
|
+
total += costWeight * costValue;
|
|
384
|
+
components.cost = costValue;
|
|
385
|
+
}
|
|
386
|
+
const score: RouteScoreEvidence = { total, components };
|
|
387
|
+
const evaluated: PolicyEvaluationCandidate = {
|
|
388
|
+
provider: evidence.provider,
|
|
389
|
+
model: evidence.model,
|
|
390
|
+
...(evidence.accountRef ? { accountRef: evidence.accountRef } : {}),
|
|
391
|
+
eligible,
|
|
392
|
+
exclusions,
|
|
393
|
+
requirements,
|
|
394
|
+
...(evidence.capability ? { capability: evidence.capability } : {}),
|
|
395
|
+
...(evidence.health ? { health: evidence.health } : {}),
|
|
396
|
+
...(evidence.quota ? { quota: evidence.quota } : {}),
|
|
397
|
+
...(evidence.cost ? { cost: evidence.cost } : {}),
|
|
398
|
+
score,
|
|
399
|
+
};
|
|
400
|
+
candidates.push(evaluated);
|
|
401
|
+
|
|
402
|
+
if (evaluated.eligible && score.total > bestScore) {
|
|
403
|
+
bestScore = score.total;
|
|
404
|
+
selectedIndex = index;
|
|
405
|
+
}
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
const trace = buildRouteDecisionTrace({
|
|
409
|
+
requestedModel: policyModelId(profileId),
|
|
410
|
+
routeKind: "policy",
|
|
411
|
+
profile: { id: profile.id, revision: profile.revision },
|
|
412
|
+
// Flat, capped summary (truncation is flagged by the builder); per-candidate
|
|
413
|
+
// attribution lives in each candidate's `requirements`/`exclusions`.
|
|
414
|
+
requirements: candidates.flatMap(candidate => candidate.requirements).slice(0, MAX_REQUIREMENTS),
|
|
415
|
+
candidates: candidates.map(candidate => ({
|
|
416
|
+
provider: candidate.provider,
|
|
417
|
+
model: candidate.model,
|
|
418
|
+
...(candidate.accountRef ? { accountRef: candidate.accountRef } : {}),
|
|
419
|
+
eligible: candidate.eligible,
|
|
420
|
+
exclusions: candidate.exclusions,
|
|
421
|
+
...(candidate.score ? { score: candidate.score } : {}),
|
|
422
|
+
...(candidate.capability ? { capability: candidate.capability } : {}),
|
|
423
|
+
...(candidate.health ? { health: candidate.health } : {}),
|
|
424
|
+
...(candidate.quota ? { quota: candidate.quota } : {}),
|
|
425
|
+
...(candidate.cost ? { cost: candidate.cost } : {}),
|
|
426
|
+
})),
|
|
427
|
+
selected: selectedIndex === null
|
|
428
|
+
? { provider: candidates[0]?.provider ?? "", model: candidates[0]?.model ?? "", reason: "no-eligible-candidate" }
|
|
429
|
+
: {
|
|
430
|
+
candidateIndex: selectedIndex,
|
|
431
|
+
provider: candidates[selectedIndex]!.provider,
|
|
432
|
+
model: candidates[selectedIndex]!.model,
|
|
433
|
+
reason: "policy-selected",
|
|
434
|
+
},
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
return {
|
|
438
|
+
profileId: profile.id,
|
|
439
|
+
profileRevision: profile.revision,
|
|
440
|
+
candidates,
|
|
441
|
+
selectedIndex,
|
|
442
|
+
trace,
|
|
443
|
+
};
|
|
444
|
+
}
|