@bitkyc08/opencodex 2.12.0 → 2.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/gui/dist/assets/index-BHldBl6_.js +76 -0
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/adapters/anthropic-output-schema.ts +137 -0
- package/src/adapters/anthropic.ts +15 -0
- package/src/adapters/google-antigravity-replay.ts +269 -5
- package/src/adapters/openai-chat.ts +25 -18
- package/src/adapters/openai-responses.ts +63 -17
- package/src/bridge.ts +11 -5
- package/src/claude/inbound.ts +24 -3
- package/src/cli/account-api.ts +2 -0
- package/src/cli/account-extended.ts +262 -1
- package/src/cli/account.ts +3 -1
- package/src/cli/dispatch.ts +532 -0
- package/src/cli/doctor.ts +26 -21
- package/src/cli/help.ts +9 -278
- package/src/cli/index.ts +45 -506
- package/src/cli/lab.ts +171 -65
- package/src/cli/registry.ts +384 -0
- package/src/cli/root.ts +86 -0
- package/src/cli/status.ts +9 -2
- package/src/cli/v2.ts +37 -2
- package/src/codex/account-label.ts +14 -1
- package/src/codex/account-store.ts +12 -1
- package/src/codex/admission.ts +2 -1
- package/src/codex/auth-api.ts +3 -2
- package/src/codex/catalog/metadata.ts +6 -4
- package/src/codex/catalog/parsing.ts +31 -7
- package/src/codex/catalog/provider-fetch.ts +2 -1
- package/src/codex/catalog/sync.ts +65 -11
- package/src/codex/features.ts +437 -31
- package/src/codex/history-job.ts +6 -11
- package/src/codex/history-provider.ts +10 -9
- package/src/codex/inject.ts +14 -1
- package/src/codex/native-profile-startup.ts +49 -3
- package/src/codex/native-residue.ts +14 -2
- package/src/codex/paths.ts +75 -1
- package/src/codex/reset-credit-recovery.ts +1044 -0
- package/src/codex/shim.ts +876 -16
- package/src/codex/sync.ts +27 -2
- package/src/config.ts +148 -10
- package/src/generated/compatibility-version.json +2572 -0
- package/src/images/loop.ts +1 -1
- package/src/lab/artifacts/sanitize.ts +3 -3
- package/src/lab/automation/budgets.ts +78 -0
- package/src/lab/automation/config-persistence.ts +256 -0
- package/src/lab/automation/constants.ts +39 -0
- package/src/lab/automation/cooldown.ts +103 -0
- package/src/lab/automation/dispatch.ts +211 -0
- package/src/lab/automation/index.ts +13 -0
- package/src/lab/automation/orchestrator.ts +480 -0
- package/src/lab/automation/persistence.ts +512 -0
- package/src/lab/automation/planner.ts +371 -0
- package/src/lab/automation/policy.ts +136 -0
- package/src/lab/automation/queue.ts +191 -0
- package/src/lab/automation/recovery.ts +24 -0
- package/src/lab/automation/route-context.ts +21 -0
- package/src/lab/automation/run-key.ts +44 -0
- package/src/lab/automation/runs-query.ts +34 -0
- package/src/lab/automation/types.ts +160 -0
- package/src/lab/conformance/types.ts +23 -0
- package/src/lab/events/types.ts +2 -1
- package/src/lab/events/validate.ts +2 -1
- package/src/lab/fabric/constants.ts +40 -0
- package/src/lab/fabric/executor.ts +492 -0
- package/src/lab/fabric/index.ts +80 -0
- package/src/lab/fabric/manifest.ts +222 -0
- package/src/lab/fabric/observe.ts +454 -0
- package/src/lab/fabric/patch.ts +79 -0
- package/src/lab/fabric/producer-child.ts +139 -0
- package/src/lab/fabric/producer-isolate.ts +276 -0
- package/src/lab/fabric/producer-protocol.ts +61 -0
- package/src/lab/fabric/scratch.ts +439 -0
- package/src/lab/fabric/subject.ts +106 -0
- package/src/lab/fabric/types.ts +134 -0
- package/src/lab/fabric/verifier.ts +98 -0
- package/src/lab/index.ts +2 -0
- package/src/lab/ledger/store.ts +173 -0
- package/src/lab/live/executor.ts +29 -4
- package/src/lab/live/transport.ts +3 -1
- package/src/lab/observe/from-conformance.ts +12 -57
- package/src/lab/observe/from-live.ts +9 -1
- package/src/lab/paths.ts +74 -9
- package/src/lab/projection/schema.ts +14 -1
- package/src/lab/projection/verdicts.ts +89 -53
- package/src/lab/projection/verification.ts +83 -3
- package/src/lab/query/catalog.ts +34 -5
- package/src/lab/query/freshness.ts +53 -0
- package/src/lab/query/index.ts +11 -0
- package/src/lab/query/latest-observation.ts +59 -0
- package/src/lab/query/passive-production.ts +159 -0
- package/src/lab/subject/installation-salt.ts +24 -4
- package/src/lab/subject/protocol-subject.ts +80 -0
- package/src/lab/subject/route-subject.ts +2 -1
- package/src/lib/fabric-task-execution-authority.ts +7 -0
- package/src/lib/fabric-task-host.ts +29 -0
- package/src/lib/lab-live-route-production.ts +130 -0
- package/src/lib/local-management-capability.ts +100 -0
- package/src/lib/server-resource-ownership.ts +71 -0
- package/src/lib/windows-elevation.ts +8 -1
- package/src/lib/windows-user-principal.ts +62 -2
- package/src/lib/winsw.ts +8 -2
- package/src/oauth/account-import/google-antigravity-adapter.ts +74 -0
- package/src/oauth/account-import/index.ts +15 -0
- package/src/oauth/account-import/parser.ts +83 -0
- package/src/oauth/account-import/registry.ts +18 -0
- package/src/oauth/account-import/service.ts +75 -0
- package/src/oauth/account-import/types.ts +91 -0
- package/src/oauth/anthropic.ts +12 -1
- package/src/oauth/callback-server.ts +8 -2
- package/src/oauth/chatgpt.ts +12 -1
- package/src/oauth/google-antigravity.ts +27 -0
- package/src/oauth/health.ts +12 -48
- package/src/oauth/index.ts +122 -0
- package/src/oauth/kimi.ts +16 -2
- package/src/oauth/nous.ts +749 -0
- package/src/oauth/store.ts +56 -3
- package/src/providers/codex-capacity.ts +1 -0
- package/src/providers/registry.ts +161 -16
- package/src/responses/custom-tool-compat.ts +182 -0
- package/src/responses/parser.ts +22 -0
- package/src/responses/reasoning-replay-cache.ts +168 -11
- package/src/responses/schema.ts +1 -1
- package/src/router.ts +5 -20
- package/src/routing/capability.ts +9 -3
- package/src/routing/compatibility/assemble.ts +159 -0
- package/src/routing/compatibility/behavior.ts +224 -0
- package/src/routing/compatibility/catalog.ts +99 -0
- package/src/routing/compatibility/endpoint.ts +52 -0
- package/src/routing/compatibility/policy.ts +181 -0
- package/src/routing/compatibility/reader.ts +110 -0
- package/src/routing/compatibility/subject.ts +191 -0
- package/src/routing/compatibility/types.ts +64 -0
- package/src/routing/compatibility/version.ts +104 -0
- package/src/routing/evaluator.ts +27 -32
- package/src/routing/profile.ts +118 -4
- package/src/routing/trace.ts +82 -28
- package/src/server/auth-cors.ts +7 -0
- package/src/server/background-lifecycle.ts +20 -7
- package/src/server/chat-completions.ts +2 -2
- package/src/server/claude-messages.ts +6 -3
- package/src/server/direct-local-http.ts +343 -0
- package/src/server/index.ts +80 -18
- package/src/server/lifecycle.ts +12 -4
- package/src/server/local-management-read-client.ts +90 -0
- package/src/server/management/agent-settings-routes.ts +27 -3
- package/src/server/management/lab-automation-routes.ts +206 -0
- package/src/server/management/lab-routes.ts +13 -0
- package/src/server/management/logs-usage-routes.ts +1 -0
- package/src/server/management/oauth-account-routes.ts +57 -1
- package/src/server/management/provider-routes.ts +16 -0
- package/src/server/management/routing-profile-routes.ts +12 -24
- package/src/server/management-api.ts +2 -0
- package/src/server/management-auth.ts +68 -3
- package/src/server/proxy-liveness.ts +3 -2
- package/src/server/request-decompress.ts +9 -3
- package/src/server/request-log.ts +53 -4
- package/src/server/responses/collaboration.ts +93 -17
- package/src/server/responses/compact.ts +3 -0
- package/src/server/responses/core.ts +202 -15
- package/src/server/responses/policy-fallback.ts +1 -0
- package/src/server/responses-custom-tool-repair.ts +280 -0
- package/src/server/responses-terminal-repair.ts +342 -0
- package/src/server/startup-action-control.ts +8 -1
- package/src/service.ts +278 -12
- package/src/types.ts +49 -3
- package/src/usage/expected-prices.ts +30 -0
- package/src/usage/log.ts +30 -4
- package/src/usage/summary.ts +153 -2
- package/src/web-search/loop.ts +1 -1
- package/gui/dist/assets/index-Cw1_mxO8.js +0 -70
- package/src/cli/internal-dispatch.ts +0 -20
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import type { OcxConfig, OcxProviderConfig } from "../../types";
|
|
2
|
+
import type { InboundWire } from "../../providers/registry";
|
|
3
|
+
import { subjectIdForSubject } from "../../lab/digest";
|
|
4
|
+
import { buildRouteSubjectV1 } from "../../lab/subject/route-subject";
|
|
5
|
+
import { buildProtocolSubjectV1 } from "../../lab/subject/protocol-subject";
|
|
6
|
+
import { readExistingInstallationSalt } from "../../lab/subject/installation-salt";
|
|
7
|
+
import type { LabDestinationV1, LabRouteContext } from "../../lab/live/types";
|
|
8
|
+
import { resolveWireProtocolOverride } from "../../server/adapter-resolve";
|
|
9
|
+
import { endpointFingerprintFromBaseUrl, parseEndpointFingerprintParts } from "./endpoint";
|
|
10
|
+
import {
|
|
11
|
+
providerInstanceKey,
|
|
12
|
+
resolveProductionBehaviorValues,
|
|
13
|
+
surfaceForProtocols,
|
|
14
|
+
upstreamProtocolForAdapter,
|
|
15
|
+
} from "./behavior";
|
|
16
|
+
import { readOpenCodexCompatibilityVersion } from "./version";
|
|
17
|
+
import type { RoutingCompatibilityEvidenceLayer } from "./types";
|
|
18
|
+
|
|
19
|
+
const POLICY_INBOUND_WIRE: InboundWire = "responses";
|
|
20
|
+
|
|
21
|
+
export interface ResolvedPolicyRouteSubject {
|
|
22
|
+
subjectId: string;
|
|
23
|
+
routeContext: LabRouteContext;
|
|
24
|
+
destination: LabDestinationV1;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ResolvedPolicyCompatibilitySubjects {
|
|
28
|
+
subjectIds: Partial<Record<RoutingCompatibilityEvidenceLayer, string>>;
|
|
29
|
+
route?: ResolvedPolicyRouteSubject;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Canonical Lab protocol identity for a production request wire. */
|
|
33
|
+
export function inboundProtocolForWire(inboundWire: InboundWire): string {
|
|
34
|
+
switch (inboundWire) {
|
|
35
|
+
case "responses": return "openai-responses";
|
|
36
|
+
case "chat": return "openai-chat";
|
|
37
|
+
case "anthropic": return "anthropic-messages";
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function destinationSnapshotFromBaseUrl(baseUrl: string, fingerprint: string): LabDestinationV1 {
|
|
42
|
+
const parts = parseEndpointFingerprintParts(baseUrl);
|
|
43
|
+
if (!parts) throw new Error("invalid provider baseUrl for route subject");
|
|
44
|
+
return Object.freeze({
|
|
45
|
+
scheme: parts.scheme,
|
|
46
|
+
host: parts.host,
|
|
47
|
+
port: parts.port,
|
|
48
|
+
basePath: parts.basePath,
|
|
49
|
+
sniHost: parts.host,
|
|
50
|
+
addresses: Object.freeze([]),
|
|
51
|
+
privateNetwork: false,
|
|
52
|
+
fingerprint,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Resolve subject identities for one exact inbound wire without network I/O,
|
|
58
|
+
* projection reads/rebuilds, ledger replay, or Lab-state creation.
|
|
59
|
+
*/
|
|
60
|
+
export function resolveCompatibilitySubjectsForInboundWire(
|
|
61
|
+
config: OcxConfig,
|
|
62
|
+
providerName: string,
|
|
63
|
+
modelId: string,
|
|
64
|
+
routed: OcxProviderConfig,
|
|
65
|
+
inboundWire: InboundWire,
|
|
66
|
+
configDir?: string,
|
|
67
|
+
): ResolvedPolicyCompatibilitySubjects {
|
|
68
|
+
const effective = resolveWireProtocolOverride(providerName, modelId, routed, inboundWire);
|
|
69
|
+
const baseUrl = typeof effective.baseUrl === "string" ? effective.baseUrl.trim() : "";
|
|
70
|
+
const adapter = effective.adapter ?? "openai-responses";
|
|
71
|
+
const inboundProtocol = inboundProtocolForWire(inboundWire);
|
|
72
|
+
const upstreamProtocol = upstreamProtocolForAdapter(adapter);
|
|
73
|
+
const surface = surfaceForProtocols(inboundProtocol, upstreamProtocol);
|
|
74
|
+
const subjectIds: ResolvedPolicyCompatibilitySubjects["subjectIds"] = {};
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
const protocolSubject = buildProtocolSubjectV1({
|
|
78
|
+
inboundProtocol,
|
|
79
|
+
upstreamProtocol,
|
|
80
|
+
surface,
|
|
81
|
+
}, adapter);
|
|
82
|
+
subjectIds.protocol_conformance = subjectIdForSubject(protocolSubject);
|
|
83
|
+
} catch {
|
|
84
|
+
// No CL-01 protocol subject exists for this exact adapter/wire identity.
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (!baseUrl) return { subjectIds };
|
|
88
|
+
|
|
89
|
+
let installationSalt: Uint8Array | null;
|
|
90
|
+
try {
|
|
91
|
+
installationSalt = readExistingInstallationSalt(configDir);
|
|
92
|
+
} catch {
|
|
93
|
+
installationSalt = null;
|
|
94
|
+
}
|
|
95
|
+
const compatibilityVersion = readOpenCodexCompatibilityVersion();
|
|
96
|
+
if (!installationSalt || !compatibilityVersion) return { subjectIds };
|
|
97
|
+
|
|
98
|
+
const endpointFp = endpointFingerprintFromBaseUrl(baseUrl, installationSalt);
|
|
99
|
+
if (!endpointFp) return { subjectIds };
|
|
100
|
+
const destination = destinationSnapshotFromBaseUrl(baseUrl, endpointFp);
|
|
101
|
+
const behaviorValues = resolveProductionBehaviorValues(
|
|
102
|
+
config,
|
|
103
|
+
providerName,
|
|
104
|
+
modelId,
|
|
105
|
+
effective,
|
|
106
|
+
installationSalt,
|
|
107
|
+
);
|
|
108
|
+
if (!behaviorValues) return { subjectIds };
|
|
109
|
+
|
|
110
|
+
const routeContext: LabRouteContext = {
|
|
111
|
+
providerId: providerName,
|
|
112
|
+
providerInstanceKey: providerInstanceKey(providerName, effective),
|
|
113
|
+
clientModelId: modelId,
|
|
114
|
+
upstreamModelId: modelId,
|
|
115
|
+
effectiveAdapter: adapter,
|
|
116
|
+
inboundProtocol,
|
|
117
|
+
upstreamProtocol,
|
|
118
|
+
surface,
|
|
119
|
+
baseUrl,
|
|
120
|
+
opencodexCompatibilityVersion: compatibilityVersion,
|
|
121
|
+
behaviorValues,
|
|
122
|
+
dependencies: [],
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
const subject = buildRouteSubjectV1(routeContext, destination, configDir, installationSalt);
|
|
127
|
+
const route: ResolvedPolicyRouteSubject = {
|
|
128
|
+
subjectId: subjectIdForSubject(subject),
|
|
129
|
+
routeContext,
|
|
130
|
+
destination,
|
|
131
|
+
};
|
|
132
|
+
subjectIds.live_route_compatibility = route.subjectId;
|
|
133
|
+
return { subjectIds, route };
|
|
134
|
+
} catch {
|
|
135
|
+
return { subjectIds };
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Resolve the subject identities a policy candidate may legitimately consume.
|
|
141
|
+
* CL-06 policy evaluation is a Responses-surface lookup and remains unchanged.
|
|
142
|
+
*/
|
|
143
|
+
export function resolvePolicyCompatibilitySubjects(
|
|
144
|
+
config: OcxConfig,
|
|
145
|
+
providerName: string,
|
|
146
|
+
modelId: string,
|
|
147
|
+
routed: OcxProviderConfig,
|
|
148
|
+
configDir?: string,
|
|
149
|
+
): ResolvedPolicyCompatibilitySubjects {
|
|
150
|
+
return resolveCompatibilitySubjectsForInboundWire(
|
|
151
|
+
config,
|
|
152
|
+
providerName,
|
|
153
|
+
modelId,
|
|
154
|
+
routed,
|
|
155
|
+
POLICY_INBOUND_WIRE,
|
|
156
|
+
configDir,
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Build exact RouteSubjectV1 identity for a policy candidate without network I/O. */
|
|
161
|
+
export function resolvePolicyRouteSubject(
|
|
162
|
+
config: OcxConfig,
|
|
163
|
+
providerName: string,
|
|
164
|
+
modelId: string,
|
|
165
|
+
routed: OcxProviderConfig,
|
|
166
|
+
configDir?: string,
|
|
167
|
+
): ResolvedPolicyRouteSubject | null {
|
|
168
|
+
return resolvePolicyCompatibilitySubjects(config, providerName, modelId, routed, configDir).route ?? null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Build the exact production-attempt RouteSubjectV1 identity for its actual
|
|
173
|
+
* inbound wire. Returns null when no existing Lab salt/identity can be read.
|
|
174
|
+
*/
|
|
175
|
+
export function resolveProductionRouteSubject(
|
|
176
|
+
config: OcxConfig,
|
|
177
|
+
providerName: string,
|
|
178
|
+
modelId: string,
|
|
179
|
+
routed: OcxProviderConfig,
|
|
180
|
+
inboundWire: InboundWire,
|
|
181
|
+
configDir?: string,
|
|
182
|
+
): ResolvedPolicyRouteSubject | null {
|
|
183
|
+
return resolveCompatibilitySubjectsForInboundWire(
|
|
184
|
+
config,
|
|
185
|
+
providerName,
|
|
186
|
+
modelId,
|
|
187
|
+
routed,
|
|
188
|
+
inboundWire,
|
|
189
|
+
configDir,
|
|
190
|
+
).route ?? null;
|
|
191
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { CompatibilityVerdict, EvidenceLayer } from "../../lab/constants";
|
|
2
|
+
import type { OcxRoutingUnknownEvidenceMode } from "../../types";
|
|
3
|
+
|
|
4
|
+
export type RoutingCompatibilityEvidenceLayer = Extract<
|
|
5
|
+
EvidenceLayer,
|
|
6
|
+
"protocol_conformance" | "live_route_compatibility"
|
|
7
|
+
>;
|
|
8
|
+
|
|
9
|
+
export interface CompatibilitySuiteRequirement {
|
|
10
|
+
suiteId: string;
|
|
11
|
+
evidenceLayer: RoutingCompatibilityEvidenceLayer;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface NormalizedCompatibilityPolicy {
|
|
15
|
+
requiredSuites: CompatibilitySuiteRequirement[];
|
|
16
|
+
minStatus?: "PROBED" | "VERIFIED";
|
|
17
|
+
maxEvidenceAgeMs?: number;
|
|
18
|
+
unknownEvidence: OcxRoutingUnknownEvidenceMode;
|
|
19
|
+
degradedEvidence: OcxRoutingUnknownEvidenceMode;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ObservedSuiteVerdict {
|
|
23
|
+
subjectId: string;
|
|
24
|
+
suiteId: string;
|
|
25
|
+
evidenceLayer: RoutingCompatibilityEvidenceLayer;
|
|
26
|
+
suiteVersion: string;
|
|
27
|
+
suiteManifestDigest: string;
|
|
28
|
+
verdict: CompatibilityVerdict;
|
|
29
|
+
asOf: number;
|
|
30
|
+
/** Current catalog freshness ceiling for this exact suite identity. */
|
|
31
|
+
maxAgeMs: number | null;
|
|
32
|
+
notes: string[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface CandidateCompatibilityEvidence {
|
|
36
|
+
subjectIds: Partial<Record<RoutingCompatibilityEvidenceLayer, string>>;
|
|
37
|
+
projectionAvailable: boolean;
|
|
38
|
+
suites: ObservedSuiteVerdict[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface CompatibilityEvaluationOutcome {
|
|
42
|
+
exclusions: Array<{ code: string; detail?: string }>;
|
|
43
|
+
/** Numeric floor applied only when an explicit penalize policy fires. */
|
|
44
|
+
penaltyScore: number | null;
|
|
45
|
+
suiteTraces: Array<{
|
|
46
|
+
suiteId: string;
|
|
47
|
+
evidenceLayer: RoutingCompatibilityEvidenceLayer;
|
|
48
|
+
subjectIdPrefix?: string;
|
|
49
|
+
verdict?: CompatibilityVerdict;
|
|
50
|
+
minStatus?: "PROBED" | "VERIFIED";
|
|
51
|
+
fresh?: boolean;
|
|
52
|
+
unknownPolicy?: OcxRoutingUnknownEvidenceMode;
|
|
53
|
+
degradedPolicy?: OcxRoutingUnknownEvidenceMode;
|
|
54
|
+
outcome: "satisfied" | "penalized" | "excluded" | "unknown";
|
|
55
|
+
reason?: string;
|
|
56
|
+
}>;
|
|
57
|
+
traceTruncated: boolean;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export const COMPATIBILITY_UNKNOWN_PENALTY_SCORE = 0.3;
|
|
61
|
+
/** Compatibility is a bounded penalty dimension, not a universal score. */
|
|
62
|
+
export const COMPATIBILITY_WEIGHT = 0.05;
|
|
63
|
+
export const MAX_TRACE_COMPATIBILITY_SUITES = 8;
|
|
64
|
+
export const MAX_COMPATIBILITY_REQUIRED_SUITES = MAX_TRACE_COMPATIBILITY_SUITES;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { jcsStringify } from "../../lab/digest";
|
|
5
|
+
|
|
6
|
+
const VERSION_DOMAIN = "ocx-lab:compatibility-version:v1";
|
|
7
|
+
let cachedVersion: string | null | undefined;
|
|
8
|
+
let testOverride: string | null | undefined;
|
|
9
|
+
|
|
10
|
+
interface CompatibilityVersionManifest {
|
|
11
|
+
schemaVersion: 1;
|
|
12
|
+
assertionDslVersion: string;
|
|
13
|
+
evidenceSchemaVersion: string;
|
|
14
|
+
bunRuntimeVersion: string;
|
|
15
|
+
files: Array<{ path: string; sha256: string }>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function isSha256Hex(value: string): boolean {
|
|
19
|
+
return /^[0-9a-f]{64}$/.test(value);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function hashManifest(manifest: unknown): string {
|
|
23
|
+
const hash = createHash("sha256");
|
|
24
|
+
hash.update(new TextEncoder().encode(`${VERSION_DOMAIN}\0`));
|
|
25
|
+
hash.update(new TextEncoder().encode(jcsStringify(manifest)));
|
|
26
|
+
return hash.digest("hex");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function parseEmbeddedManifest(raw: unknown): CompatibilityVersionManifest | null {
|
|
30
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
31
|
+
const record = raw as Record<string, unknown>;
|
|
32
|
+
if (record.schemaVersion !== 1
|
|
33
|
+
|| typeof record.assertionDslVersion !== "string"
|
|
34
|
+
|| typeof record.evidenceSchemaVersion !== "string"
|
|
35
|
+
|| typeof record.bunRuntimeVersion !== "string"
|
|
36
|
+
|| !Array.isArray(record.files)
|
|
37
|
+
|| record.files.length === 0) return null;
|
|
38
|
+
const files: CompatibilityVersionManifest["files"] = [];
|
|
39
|
+
const seen = new Set<string>();
|
|
40
|
+
for (const row of record.files) {
|
|
41
|
+
if (!row || typeof row !== "object" || Array.isArray(row)) return null;
|
|
42
|
+
const path = (row as Record<string, unknown>).path;
|
|
43
|
+
const sha256 = (row as Record<string, unknown>).sha256;
|
|
44
|
+
if (typeof path !== "string" || !path || path.includes("\\") || path.startsWith("/") || path.split("/").includes("..")) return null;
|
|
45
|
+
if (typeof sha256 !== "string" || !isSha256Hex(sha256) || seen.has(path)) return null;
|
|
46
|
+
seen.add(path);
|
|
47
|
+
files.push({ path, sha256 });
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
schemaVersion: 1,
|
|
51
|
+
assertionDslVersion: record.assertionDslVersion,
|
|
52
|
+
evidenceSchemaVersion: record.evidenceSchemaVersion,
|
|
53
|
+
// The runtime version is part of the canonical identity. The embedded file
|
|
54
|
+
// hashes remain valid across bundled Bun updates, while this field reflects
|
|
55
|
+
// the actual executing runtime rather than the package-build runtime.
|
|
56
|
+
bunRuntimeVersion: Bun.version,
|
|
57
|
+
files,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function readEmbeddedManifest(): CompatibilityVersionManifest | null {
|
|
62
|
+
const candidates = [
|
|
63
|
+
join(import.meta.dir, "..", "..", "generated", "compatibility-version.json"),
|
|
64
|
+
join(import.meta.dir, "..", "..", "..", "src", "generated", "compatibility-version.json"),
|
|
65
|
+
];
|
|
66
|
+
for (const path of candidates) {
|
|
67
|
+
try {
|
|
68
|
+
const parsed = JSON.parse(readFileSync(path, "utf8")) as unknown;
|
|
69
|
+
const manifest = parseEmbeddedManifest(parsed);
|
|
70
|
+
if (manifest) return manifest;
|
|
71
|
+
} catch {
|
|
72
|
+
// try next candidate
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* OpenCodex compatibility version for route subject identity.
|
|
80
|
+
*
|
|
81
|
+
* There is intentionally no runtime-only fallback: without the generated file
|
|
82
|
+
* manifest we cannot prove exact implementation identity, so live-route subject
|
|
83
|
+
* resolution fails closed and follows the profile's unknown-evidence policy.
|
|
84
|
+
*/
|
|
85
|
+
export function readOpenCodexCompatibilityVersion(): string | null {
|
|
86
|
+
if (testOverride !== undefined) return testOverride;
|
|
87
|
+
if (cachedVersion !== undefined) return cachedVersion;
|
|
88
|
+
const embedded = readEmbeddedManifest();
|
|
89
|
+
cachedVersion = embedded ? hashManifest(embedded) : null;
|
|
90
|
+
return cachedVersion;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Test-only exact identity override; never sourced from process environment. */
|
|
94
|
+
export function setCompatibilityVersionOverrideForTests(value: string | null): void {
|
|
95
|
+
if (value !== null && !isSha256Hex(value)) throw new Error("invalid test compatibility version");
|
|
96
|
+
testOverride = value;
|
|
97
|
+
cachedVersion = undefined;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Test-only reset of the compatibility version memo and override. */
|
|
101
|
+
export function resetCompatibilityVersionCacheForTests(): void {
|
|
102
|
+
cachedVersion = undefined;
|
|
103
|
+
testOverride = undefined;
|
|
104
|
+
}
|
package/src/routing/evaluator.ts
CHANGED
|
@@ -24,6 +24,9 @@ import { getRoutingProfile, policyModelId, type NormalizedRoutingProfile } from
|
|
|
24
24
|
import { healthScore } from "./health";
|
|
25
25
|
import { quotaScore } from "./quota";
|
|
26
26
|
import { costScore } from "./cost";
|
|
27
|
+
import { evaluateCompatibilityForCandidate } from "./compatibility/policy";
|
|
28
|
+
import { COMPATIBILITY_WEIGHT, type CandidateCompatibilityEvidence } from "./compatibility/types";
|
|
29
|
+
import type { RouteCompatibilityEvidence } from "./trace";
|
|
27
30
|
|
|
28
31
|
/** Unknown health under "penalize": a low-but-not-zero deterministic floor. */
|
|
29
32
|
export const HEALTH_UNKNOWN_PENALTY_SCORE = 0.3;
|
|
@@ -55,6 +58,7 @@ export interface PolicyCandidateEvidence {
|
|
|
55
58
|
health?: RouteHealthEvidence;
|
|
56
59
|
quota?: RouteQuotaEvidence;
|
|
57
60
|
cost?: RouteCostEvidence;
|
|
61
|
+
compatibility?: CandidateCompatibilityEvidence;
|
|
58
62
|
}
|
|
59
63
|
|
|
60
64
|
export interface PolicyEvaluationCandidate {
|
|
@@ -68,6 +72,7 @@ export interface PolicyEvaluationCandidate {
|
|
|
68
72
|
health?: RouteHealthEvidence;
|
|
69
73
|
quota?: RouteQuotaEvidence;
|
|
70
74
|
cost?: RouteCostEvidence;
|
|
75
|
+
compatibility?: RouteCompatibilityEvidence;
|
|
71
76
|
score?: RouteScoreEvidence;
|
|
72
77
|
}
|
|
73
78
|
|
|
@@ -85,8 +90,6 @@ function booleanRequirement(
|
|
|
85
90
|
actual: Unknownable | undefined,
|
|
86
91
|
): RouteRequirementEvidence | null {
|
|
87
92
|
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
93
|
if (actual === undefined || actual === "unknown") {
|
|
91
94
|
return { id, expected: required, outcome: "unknown" };
|
|
92
95
|
}
|
|
@@ -117,11 +120,6 @@ function requirementFor(
|
|
|
117
120
|
requirements.push({ id: "min-context-window", expected: require.minContextWindow, outcome: "unknown" });
|
|
118
121
|
}
|
|
119
122
|
}
|
|
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
123
|
if (require.minQuotaHeadroom !== undefined) {
|
|
126
124
|
const quotaHeadroom = quotaScore(quota);
|
|
127
125
|
if (quotaHeadroom !== null) {
|
|
@@ -186,12 +184,6 @@ function configuredPriorityScore(index: number, total: number): number {
|
|
|
186
184
|
return total > 1 ? (total - index) / total : 1;
|
|
187
185
|
}
|
|
188
186
|
|
|
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
187
|
function requestRequirementFor(
|
|
196
188
|
request: PolicyRequestEvidence,
|
|
197
189
|
capability: RouteCapabilityEvidence | undefined,
|
|
@@ -210,7 +202,6 @@ function requestRequirementFor(
|
|
|
210
202
|
requirements.push({ id: "request-context-window", expected: request.contextWindow, outcome: "unknown" });
|
|
211
203
|
}
|
|
212
204
|
}
|
|
213
|
-
// Absent flags mean "no requirement": only a positive request need adds a row.
|
|
214
205
|
if (request.toolsRequired === true) {
|
|
215
206
|
const tools = booleanRequirement("request-tools", true, capability?.tools);
|
|
216
207
|
if (tools) requirements.push(tools);
|
|
@@ -297,10 +288,6 @@ export function evaluatePolicyProfile(
|
|
|
297
288
|
}
|
|
298
289
|
const unsatisfied = bad.some(requirement => requirement.outcome === "unsatisfied");
|
|
299
290
|
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
291
|
const excludedByUnknown = unknown && profile.unknownEvidence.capability === "exclude";
|
|
305
292
|
const costLimit = profile.limits.maxEstimatedCostUsd;
|
|
306
293
|
const estimatedCost = evidence.cost?.estimatedUsd;
|
|
@@ -357,9 +344,18 @@ export function evaluatePolicyProfile(
|
|
|
357
344
|
}
|
|
358
345
|
}
|
|
359
346
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
347
|
+
const compatibilityEval = evaluateCompatibilityForCandidate(
|
|
348
|
+
profile.compatibility,
|
|
349
|
+
evidence.compatibility,
|
|
350
|
+
now,
|
|
351
|
+
);
|
|
352
|
+
if (compatibilityEval.exclusions.length > 0) {
|
|
353
|
+
exclusions.push(...compatibilityEval.exclusions);
|
|
354
|
+
eligible = false;
|
|
355
|
+
}
|
|
356
|
+
const compatibilityValue = compatibilityEval.penaltyScore;
|
|
357
|
+
const compatibilityTrace = compatibilityEval.trace;
|
|
358
|
+
|
|
363
359
|
const health = evidence.health;
|
|
364
360
|
let healthValue = health ? healthScore(health, now) : null;
|
|
365
361
|
if (health?.cooldownUntilMs !== undefined && health.cooldownUntilMs > now) {
|
|
@@ -371,13 +367,9 @@ export function evaluatePolicyProfile(
|
|
|
371
367
|
} else if (healthValue === null && profile.unknownEvidence.health === "penalize") {
|
|
372
368
|
healthValue = HEALTH_UNKNOWN_PENALTY_SCORE;
|
|
373
369
|
} else if (healthValue === null && profile.unknownEvidence.health === "allow") {
|
|
374
|
-
// Neutral midpoint: blending keeps an unknown candidate from outranking
|
|
375
|
-
// a measured one with the same configured priority.
|
|
376
370
|
healthValue = HEALTH_UNKNOWN_NEUTRAL_SCORE;
|
|
377
371
|
}
|
|
378
372
|
|
|
379
|
-
// Quota scoring (RI-07): unknown quota follows the profile policy;
|
|
380
|
-
// exhausted or low-headroom evidence lowers the score.
|
|
381
373
|
const quota = evidence.quota;
|
|
382
374
|
let quotaValue = quota ? quotaScore(quota) : null;
|
|
383
375
|
if (quotaValue === null) {
|
|
@@ -406,11 +398,6 @@ export function evaluatePolicyProfile(
|
|
|
406
398
|
const healthWeight = profile.optimize.health;
|
|
407
399
|
const quotaWeight = profile.optimize.quota;
|
|
408
400
|
const costWeight = profile.optimize.cost;
|
|
409
|
-
// Only spend a dimension's weight when a value is actually present:
|
|
410
|
-
// "allow" leaves missing health/quota/cost components null, so subtracting
|
|
411
|
-
// their weights would shrink the priority share for evidence the profile
|
|
412
|
-
// explicitly permits to be absent. Renormalize those weights back into
|
|
413
|
-
// priority instead of silently changing the ranking semantics.
|
|
414
401
|
const spentHealth = healthValue !== null ? healthWeight : 0;
|
|
415
402
|
const spentQuota = quotaValue !== null ? quotaWeight : 0;
|
|
416
403
|
const spentCost = costValue !== null ? costWeight : 0;
|
|
@@ -429,6 +416,13 @@ export function evaluatePolicyProfile(
|
|
|
429
416
|
total += costWeight * costValue;
|
|
430
417
|
components.cost = costValue;
|
|
431
418
|
}
|
|
419
|
+
if (compatibilityValue !== null) {
|
|
420
|
+
// Compatibility is a penalty-only dimension. A penalized candidate loses
|
|
421
|
+
// a bounded fraction of its existing score; satisfied/allowed evidence
|
|
422
|
+
// does not receive a synthetic bonus or alter configured priority.
|
|
423
|
+
total = Math.max(0, total - COMPATIBILITY_WEIGHT * (1 - compatibilityValue));
|
|
424
|
+
components.compatibility = compatibilityValue;
|
|
425
|
+
}
|
|
432
426
|
const score: RouteScoreEvidence = { total, components };
|
|
433
427
|
const evaluated: PolicyEvaluationCandidate = {
|
|
434
428
|
provider: evidence.provider,
|
|
@@ -440,6 +434,8 @@ export function evaluatePolicyProfile(
|
|
|
440
434
|
...(evidence.capability ? { capability: evidence.capability } : {}),
|
|
441
435
|
...(evidence.health ? { health: evidence.health } : {}),
|
|
442
436
|
...(evidence.quota ? { quota: evidence.quota } : {}),
|
|
437
|
+
...(evidence.cost ? { cost: evidence.cost } : {}),
|
|
438
|
+
...(compatibilityTrace ? { compatibility: compatibilityTrace } : {}),
|
|
443
439
|
...(costForCandidate ? { cost: costForCandidate } : {}),
|
|
444
440
|
score,
|
|
445
441
|
};
|
|
@@ -455,8 +451,6 @@ export function evaluatePolicyProfile(
|
|
|
455
451
|
requestedModel: policyModelId(profileId),
|
|
456
452
|
routeKind: "policy",
|
|
457
453
|
profile: { id: profile.id, revision: profile.revision },
|
|
458
|
-
// Flat, capped summary (truncation is flagged by the builder); per-candidate
|
|
459
|
-
// attribution lives in each candidate's `requirements`/`exclusions`.
|
|
460
454
|
requirements: candidates.flatMap(candidate => candidate.requirements).slice(0, MAX_REQUIREMENTS),
|
|
461
455
|
candidates: candidates.map(candidate => ({
|
|
462
456
|
provider: candidate.provider,
|
|
@@ -469,6 +463,7 @@ export function evaluatePolicyProfile(
|
|
|
469
463
|
...(candidate.health ? { health: candidate.health } : {}),
|
|
470
464
|
...(candidate.quota ? { quota: candidate.quota } : {}),
|
|
471
465
|
...(candidate.cost ? { cost: candidate.cost } : {}),
|
|
466
|
+
...(candidate.compatibility ? { compatibility: candidate.compatibility } : {}),
|
|
472
467
|
})),
|
|
473
468
|
selected: selectedIndex === null
|
|
474
469
|
? { provider: candidates[0]?.provider ?? "", model: candidates[0]?.model ?? "", reason: "no-eligible-candidate" }
|