@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
|
@@ -9,19 +9,42 @@
|
|
|
9
9
|
* assistant message is about to serialize without thinking parts (compacted
|
|
10
10
|
* history, lost assistant turn, orphan-repaired tool results).
|
|
11
11
|
*
|
|
12
|
-
* Entries
|
|
13
|
-
*
|
|
14
|
-
* process-wide key would let one conversation's reasoning bleed into
|
|
15
|
-
* when ids collide (CodeRabbit P1 on #971).
|
|
12
|
+
* Entries require a conversation identity in addition to the call id:
|
|
13
|
+
* provider-generated ids like `call_1` are not globally unique, so an
|
|
14
|
+
* unscoped process-wide key would let one conversation's reasoning bleed into
|
|
15
|
+
* another when ids collide (CodeRabbit P1 on #971).
|
|
16
16
|
*
|
|
17
17
|
* Privacy: entries hold reasoning text in memory only — never logged,
|
|
18
18
|
* serialized, or exported. Bounded by entry count, total bytes, and TTL, so a
|
|
19
19
|
* long-lived proxy cannot grow without limit.
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
|
+
import { createHmac, randomBytes } from "node:crypto";
|
|
23
|
+
import type {
|
|
24
|
+
OcxProviderConfig,
|
|
25
|
+
OcxReasoningReplayIdentity,
|
|
26
|
+
OcxReasoningReplayScopeRef,
|
|
27
|
+
} from "../types";
|
|
28
|
+
|
|
22
29
|
const MAX_ENTRIES = 64;
|
|
23
30
|
const MAX_TOTAL_BYTES = 256 * 1024;
|
|
24
31
|
const TTL_MS = 60 * 60 * 1000;
|
|
32
|
+
const replayIdentityKey = randomBytes(32);
|
|
33
|
+
const CREDENTIAL_HEADER_NAMES = new Set([
|
|
34
|
+
"authorization",
|
|
35
|
+
"api-key",
|
|
36
|
+
"chatgpt-account-id",
|
|
37
|
+
"cookie",
|
|
38
|
+
"openai-organization",
|
|
39
|
+
"openai-project",
|
|
40
|
+
"proxy-authorization",
|
|
41
|
+
"x-api-key",
|
|
42
|
+
"x-api-token",
|
|
43
|
+
"x-auth-token",
|
|
44
|
+
"x-goog-api-key",
|
|
45
|
+
"x-openai-organization",
|
|
46
|
+
"x-openai-project",
|
|
47
|
+
]);
|
|
25
48
|
|
|
26
49
|
interface CacheEntry {
|
|
27
50
|
text: string;
|
|
@@ -34,8 +57,131 @@ let totalBytes = 0;
|
|
|
34
57
|
let clockForTests: (() => number) | null = null;
|
|
35
58
|
|
|
36
59
|
const now = (): number => clockForTests?.() ?? Date.now();
|
|
37
|
-
|
|
38
|
-
|
|
60
|
+
|
|
61
|
+
function nonEmpty(value: unknown): value is string {
|
|
62
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function keyFor(callId: string, scope: OcxReasoningReplayScopeRef | undefined): string | undefined {
|
|
66
|
+
const identity = scope?.current;
|
|
67
|
+
if (
|
|
68
|
+
!nonEmpty(callId)
|
|
69
|
+
|| !nonEmpty(scope?.clientThreadId)
|
|
70
|
+
|| !nonEmpty(identity?.providerName)
|
|
71
|
+
|| !nonEmpty(identity?.providerDestinationIdentity)
|
|
72
|
+
|| !nonEmpty(identity?.adapterName)
|
|
73
|
+
|| !nonEmpty(identity?.modelId)
|
|
74
|
+
|| !nonEmpty(identity?.credentialIdentity)
|
|
75
|
+
) return undefined;
|
|
76
|
+
return JSON.stringify([
|
|
77
|
+
scope.clientThreadId,
|
|
78
|
+
identity.providerName,
|
|
79
|
+
identity.providerDestinationIdentity,
|
|
80
|
+
identity.adapterName,
|
|
81
|
+
identity.modelId,
|
|
82
|
+
identity.credentialIdentity,
|
|
83
|
+
callId,
|
|
84
|
+
]);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function processLocalIdentity(domain: string, material: string): string {
|
|
88
|
+
return createHmac("sha256", replayIdentityKey)
|
|
89
|
+
.update(domain)
|
|
90
|
+
.update("\0")
|
|
91
|
+
.update(material)
|
|
92
|
+
.digest("hex");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function credentialHeaderOverrides(headers: Record<string, string> | undefined): [string, string][] {
|
|
96
|
+
return Object.entries(headers ?? {})
|
|
97
|
+
.filter(([name, value]) => CREDENTIAL_HEADER_NAMES.has(name.trim().toLowerCase()) && nonEmpty(value))
|
|
98
|
+
.map(([name, value]) => [name.trim().toLowerCase(), value] as [string, string])
|
|
99
|
+
.sort(([leftName, leftValue], [rightName, rightValue]) => (
|
|
100
|
+
leftName.localeCompare(rightName) || leftValue.localeCompare(rightValue)
|
|
101
|
+
));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Produce a non-reversible process-local identity for an exact upstream destination. */
|
|
105
|
+
export function reasoningReplayDestinationIdentity(baseUrl: string | undefined): string | undefined {
|
|
106
|
+
if (!nonEmpty(baseUrl)) return undefined;
|
|
107
|
+
const canonical = baseUrl.trim().replace(/\/+$/, "");
|
|
108
|
+
return `destination:${processLocalIdentity("destination", canonical)}`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Produce a non-reversible process-local identity for credential material. */
|
|
112
|
+
export function reasoningReplayCredentialIdentity(
|
|
113
|
+
kind: "key" | "oauth" | "codex",
|
|
114
|
+
material: string | undefined,
|
|
115
|
+
headers?: Record<string, string>,
|
|
116
|
+
): string | undefined {
|
|
117
|
+
if (!nonEmpty(material)) return undefined;
|
|
118
|
+
const overrides = credentialHeaderOverrides(headers);
|
|
119
|
+
return `${kind}:${processLocalIdentity(`credential:${kind}`, JSON.stringify([material, overrides]))}`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Bind Codex forwarding to the effective bearer/account and selected physical pool slot. */
|
|
123
|
+
export function reasoningReplayCodexCredentialIdentity(args: {
|
|
124
|
+
authorization?: string | null;
|
|
125
|
+
chatgptAccountId?: string | null;
|
|
126
|
+
accountId?: string | null;
|
|
127
|
+
credentialGeneration?: number | string | null;
|
|
128
|
+
writerGeneration?: number | string | null;
|
|
129
|
+
headers?: Record<string, string>;
|
|
130
|
+
}): string | undefined {
|
|
131
|
+
const configuredAuthorization = credentialHeaderOverrides(args.headers)
|
|
132
|
+
.find(([name]) => name === "authorization")?.[1];
|
|
133
|
+
const authorization = nonEmpty(args.authorization) ? args.authorization : configuredAuthorization;
|
|
134
|
+
if (!authorization) return undefined;
|
|
135
|
+
const material = JSON.stringify([
|
|
136
|
+
authorization,
|
|
137
|
+
nonEmpty(args.chatgptAccountId) ? args.chatgptAccountId : "",
|
|
138
|
+
nonEmpty(args.accountId) ? args.accountId : "",
|
|
139
|
+
args.credentialGeneration === null || args.credentialGeneration === undefined
|
|
140
|
+
? ""
|
|
141
|
+
: String(args.credentialGeneration),
|
|
142
|
+
args.writerGeneration === null || args.writerGeneration === undefined
|
|
143
|
+
? ""
|
|
144
|
+
: String(args.writerGeneration),
|
|
145
|
+
]);
|
|
146
|
+
return reasoningReplayCredentialIdentity("codex", material, args.headers);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Bind OAuth replay to one persisted credential slot and exact token generation. */
|
|
150
|
+
export function reasoningReplayOAuthCredentialIdentity(
|
|
151
|
+
snapshot: Readonly<{ accountId: string; generation: string }> | undefined,
|
|
152
|
+
headers?: Record<string, string>,
|
|
153
|
+
): string | undefined {
|
|
154
|
+
if (!snapshot || !nonEmpty(snapshot.accountId) || !nonEmpty(snapshot.generation)) return undefined;
|
|
155
|
+
return reasoningReplayCredentialIdentity(
|
|
156
|
+
"oauth",
|
|
157
|
+
`${snapshot.accountId}\0${snapshot.generation}`,
|
|
158
|
+
headers,
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Bind key-auth provider material without putting raw secrets in the replay key. */
|
|
163
|
+
export function reasoningReplayKeyCredentialIdentity(
|
|
164
|
+
provider: Pick<OcxProviderConfig, "apiKey" | "headers">,
|
|
165
|
+
): string | undefined {
|
|
166
|
+
const apiKey = nonEmpty(provider.apiKey) ? provider.apiKey : undefined;
|
|
167
|
+
// Public/static headers do not establish a physical credential boundary.
|
|
168
|
+
// Header-only/key-optional providers therefore fail closed for replay.
|
|
169
|
+
if (!apiKey) return undefined;
|
|
170
|
+
return reasoningReplayCredentialIdentity("key", apiKey, provider.headers);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Replace only the holder snapshot so parsed-request copies observe rotations. */
|
|
174
|
+
export function bindReasoningReplayScope(
|
|
175
|
+
scope: OcxReasoningReplayScopeRef | undefined,
|
|
176
|
+
identity: OcxReasoningReplayIdentity | undefined,
|
|
177
|
+
): void {
|
|
178
|
+
if (!scope) return;
|
|
179
|
+
if (!identity || !keyFor("binding-check", { ...scope, current: identity })) {
|
|
180
|
+
delete scope.current;
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
scope.current = { ...identity };
|
|
184
|
+
}
|
|
39
185
|
|
|
40
186
|
/**
|
|
41
187
|
* Record the raw reasoning text that preceded the given tool call.
|
|
@@ -43,9 +189,18 @@ const keyFor = (callId: string, scope: string | undefined): string =>
|
|
|
43
189
|
* Expired entries are swept on insert so the TTL bound holds even when a call
|
|
44
190
|
* id is never read again.
|
|
45
191
|
*/
|
|
46
|
-
export function rememberReasoningForCall(
|
|
192
|
+
export function rememberReasoningForCall(
|
|
193
|
+
callId: string,
|
|
194
|
+
text: string,
|
|
195
|
+
scope?: OcxReasoningReplayScopeRef,
|
|
196
|
+
): void {
|
|
197
|
+
// Never fall back to a process-wide namespace. Call ids are supplied by
|
|
198
|
+
// clients/providers and are therefore neither unique nor trustworthy; an
|
|
199
|
+
// unscoped entry could be recovered by an unrelated request that reuses the
|
|
200
|
+
// same id.
|
|
47
201
|
// Empty provider deltas are absence of new reasoning, not a request to erase a candidate.
|
|
48
|
-
|
|
202
|
+
const key = keyFor(callId, scope);
|
|
203
|
+
if (!key || typeof text !== "string" || text.length === 0) return;
|
|
49
204
|
const bytes = Buffer.byteLength(text, "utf8");
|
|
50
205
|
// A single entry larger than the whole budget would immediately evict itself.
|
|
51
206
|
if (bytes > MAX_TOTAL_BYTES) return;
|
|
@@ -58,7 +213,6 @@ export function rememberReasoningForCall(callId: string, text: string, scope?: s
|
|
|
58
213
|
totalBytes -= entry.bytes;
|
|
59
214
|
}
|
|
60
215
|
}
|
|
61
|
-
const key = keyFor(callId, scope);
|
|
62
216
|
const previous = entries.get(key);
|
|
63
217
|
if (previous) totalBytes -= previous.bytes;
|
|
64
218
|
entries.set(key, { text, bytes, at });
|
|
@@ -85,9 +239,12 @@ export function rememberReasoningForCall(callId: string, text: string, scope?: s
|
|
|
85
239
|
* Read the recorded reasoning for a call id without removing it: retries after
|
|
86
240
|
* a failed continuation reuse the same fallback.
|
|
87
241
|
*/
|
|
88
|
-
export function peekReasoningForCall(
|
|
89
|
-
|
|
242
|
+
export function peekReasoningForCall(
|
|
243
|
+
callId: string,
|
|
244
|
+
scope?: OcxReasoningReplayScopeRef,
|
|
245
|
+
): string | undefined {
|
|
90
246
|
const key = keyFor(callId, scope);
|
|
247
|
+
if (!key) return undefined;
|
|
91
248
|
const entry = entries.get(key);
|
|
92
249
|
if (!entry) return undefined;
|
|
93
250
|
if (now() - entry.at >= TTL_MS) {
|
package/src/responses/schema.ts
CHANGED
|
@@ -107,7 +107,7 @@ export const toolSchema = z.object({
|
|
|
107
107
|
const builtinToolSchema = z.object({ type: z.string() }).loose();
|
|
108
108
|
|
|
109
109
|
const hostedToolType = z.enum([
|
|
110
|
-
"web_search_preview", "file_search", "computer_use_preview",
|
|
110
|
+
"web_search", "web_search_preview", "file_search", "computer_use_preview",
|
|
111
111
|
"code_interpreter", "image_generation", "mcp",
|
|
112
112
|
]);
|
|
113
113
|
|
package/src/router.ts
CHANGED
|
@@ -31,10 +31,7 @@ import {
|
|
|
31
31
|
} from "./routing/trace";
|
|
32
32
|
import { getRoutingProfile, resolvePolicyProfileId } from "./routing/profile";
|
|
33
33
|
import { evaluatePolicyProfile, type PolicyRequestEvidence } from "./routing/evaluator";
|
|
34
|
-
import {
|
|
35
|
-
import { policyCandidateHealthEvidence } from "./routing/health";
|
|
36
|
-
import { quotaEvidenceForCandidate } from "./routing/quota";
|
|
37
|
-
import { costEvidenceForCandidate } from "./routing/cost";
|
|
34
|
+
import { assemblePolicyCandidateEvidence } from "./routing/compatibility/assemble";
|
|
38
35
|
|
|
39
36
|
export class NoEligiblePolicyCandidateError extends Error {
|
|
40
37
|
/** Evaluation trace (with per-candidate exclusions) when nothing qualified. */
|
|
@@ -250,7 +247,7 @@ function usableResolvedApiKey(apiKey: string | undefined): string | undefined {
|
|
|
250
247
|
return typeof resolved === "string" && resolved.trim().length > 0 ? resolved : undefined;
|
|
251
248
|
}
|
|
252
249
|
|
|
253
|
-
function routedProviderConfig(providerName: string, provider: OcxProviderConfig): OcxProviderConfig {
|
|
250
|
+
export function routedProviderConfig(providerName: string, provider: OcxProviderConfig): OcxProviderConfig {
|
|
254
251
|
const registryEntry = PROVIDER_REGISTRY.find(entry => entry.id === providerName);
|
|
255
252
|
if (!registryEntry || !providerMatchesRegistryTransport(providerName, provider)) {
|
|
256
253
|
assertProviderDestinationAllowed(providerName, provider);
|
|
@@ -512,21 +509,9 @@ function routeModelInternal(
|
|
|
512
509
|
// One clock read per decision keeps candidate evidence, exclusions, and
|
|
513
510
|
// scores mutually consistent and reproducible.
|
|
514
511
|
const now = Date.now();
|
|
515
|
-
const candidateEvidence =
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
capability: candidateCapabilityEvidence(config, candidate.provider, candidate.model),
|
|
519
|
-
health: policyCandidateHealthEvidence(config, candidate, now),
|
|
520
|
-
quota: quotaEvidenceForCandidate({
|
|
521
|
-
provider: candidate.provider,
|
|
522
|
-
model: candidate.model,
|
|
523
|
-
}),
|
|
524
|
-
cost: costEvidenceForCandidate({
|
|
525
|
-
provider: candidate.provider,
|
|
526
|
-
model: candidate.model,
|
|
527
|
-
limitUsd: profile.limits.maxEstimatedCostUsd,
|
|
528
|
-
}),
|
|
529
|
-
}));
|
|
512
|
+
const candidateEvidence = assemblePolicyCandidateEvidence(config, profile, now, {
|
|
513
|
+
routedProviderConfig,
|
|
514
|
+
});
|
|
530
515
|
const evaluation = evaluatePolicyProfile(config, policyId, policyEvidence ?? {}, candidateEvidence, now);
|
|
531
516
|
if (evaluation.selectedIndex === null) {
|
|
532
517
|
throw new NoEligiblePolicyCandidateError(policyId, evaluation.trace);
|
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import type { OcxConfig } from "../types";
|
|
14
|
-
import { isCanonicalOpenAiForwardProvider } from "../providers/openai-tiers";
|
|
14
|
+
import { isCanonicalOpenAiForwardProvider, OPENAI_CODEX_PROVIDER_ID } from "../providers/openai-tiers";
|
|
15
|
+
import { applyProviderContextCap, providerContextCap } from "../providers/context-cap";
|
|
15
16
|
import { PROVIDER_REGISTRY } from "../providers/registry";
|
|
16
17
|
import {
|
|
17
18
|
nativeInputModalities,
|
|
@@ -145,13 +146,18 @@ export function candidateCapabilityEvidence(
|
|
|
145
146
|
const provider = config.providers[providerName];
|
|
146
147
|
const registryEntry = PROVIDER_REGISTRY.find(entry => entry.id === providerName);
|
|
147
148
|
const catalogRow = cachedCatalogModels().find(model => model.provider === providerName && model.id === modelId);
|
|
148
|
-
const isNative = providerName ===
|
|
149
|
+
const isNative = providerName === OPENAI_CODEX_PROVIDER_ID && !modelId.includes("/");
|
|
149
150
|
|
|
150
|
-
const
|
|
151
|
+
const rawContextWindow = provider?.modelContextWindows?.[modelId]
|
|
151
152
|
?? provider?.contextWindow
|
|
152
153
|
?? registryEntry?.modelContextWindows?.[modelId]
|
|
153
154
|
?? catalogRow?.contextWindow
|
|
154
155
|
?? (isNative ? nativeOpenAiContextWindow(modelId) : undefined);
|
|
156
|
+
// providerContextCaps.openai also ceilings native OpenAI rows (#1430), so routing
|
|
157
|
+
// evidence never contradicts a capped catalog entry.
|
|
158
|
+
const contextWindow = isNative
|
|
159
|
+
? applyProviderContextCap(rawContextWindow, providerContextCap(config, OPENAI_CODEX_PROVIDER_ID)) ?? rawContextWindow
|
|
160
|
+
: rawContextWindow;
|
|
155
161
|
|
|
156
162
|
const modalities = provider?.modelInputModalities?.[modelId]
|
|
157
163
|
?? registryEntry?.modelInputModalities?.[modelId]
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import type { OcxConfig, OcxProviderConfig } from "../../types";
|
|
2
|
+
import { candidateCapabilityEvidence } from "../capability";
|
|
3
|
+
import { costEvidenceForCandidate } from "../cost";
|
|
4
|
+
import type { PolicyCandidateEvidence } from "../evaluator";
|
|
5
|
+
import { policyCandidateHealthEvidence } from "../health";
|
|
6
|
+
import type { NormalizedRoutingProfile } from "../profile";
|
|
7
|
+
import { quotaEvidenceForCandidate } from "../quota";
|
|
8
|
+
import {
|
|
9
|
+
compatibilitySuiteKey,
|
|
10
|
+
loadCompatibilityCatalogSnapshot,
|
|
11
|
+
type CompatibilityCatalogSnapshot,
|
|
12
|
+
} from "./catalog";
|
|
13
|
+
import { findVerdictForSuite, loadCompatibilityEvidenceSnapshot } from "./reader";
|
|
14
|
+
import {
|
|
15
|
+
resolvePolicyCompatibilitySubjects,
|
|
16
|
+
type ResolvedPolicyCompatibilitySubjects,
|
|
17
|
+
} from "./subject";
|
|
18
|
+
import type { CandidateCompatibilityEvidence } from "./types";
|
|
19
|
+
|
|
20
|
+
export type RoutedProviderResolver = (
|
|
21
|
+
providerName: string,
|
|
22
|
+
provider: OcxProviderConfig,
|
|
23
|
+
) => OcxProviderConfig;
|
|
24
|
+
|
|
25
|
+
export interface AssemblePolicyEvidenceOptions {
|
|
26
|
+
configDir?: string;
|
|
27
|
+
routedProviderConfig: RoutedProviderResolver;
|
|
28
|
+
resolveSubjects?: typeof resolvePolicyCompatibilitySubjects;
|
|
29
|
+
loadEvidenceSnapshot?: typeof loadCompatibilityEvidenceSnapshot;
|
|
30
|
+
loadCatalogSnapshot?: typeof loadCompatibilityCatalogSnapshot;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function attachCompatibilityEvidence(
|
|
34
|
+
resolved: ResolvedPolicyCompatibilitySubjects | undefined,
|
|
35
|
+
snapshot: ReturnType<typeof loadCompatibilityEvidenceSnapshot>,
|
|
36
|
+
catalog: CompatibilityCatalogSnapshot,
|
|
37
|
+
profile: NonNullable<NormalizedRoutingProfile["compatibility"]>,
|
|
38
|
+
): CandidateCompatibilityEvidence {
|
|
39
|
+
const subjectIds = resolved?.subjectIds ?? {};
|
|
40
|
+
const suites: CandidateCompatibilityEvidence["suites"] = [];
|
|
41
|
+
|
|
42
|
+
for (const requirement of profile.requiredSuites) {
|
|
43
|
+
const subjectId = subjectIds[requirement.evidenceLayer];
|
|
44
|
+
if (!subjectId) continue;
|
|
45
|
+
const metadata = catalog.get(compatibilitySuiteKey(requirement.evidenceLayer, requirement.suiteId));
|
|
46
|
+
if (!metadata) continue;
|
|
47
|
+
const row = findVerdictForSuite(
|
|
48
|
+
snapshot,
|
|
49
|
+
subjectId,
|
|
50
|
+
requirement.evidenceLayer,
|
|
51
|
+
requirement.suiteId,
|
|
52
|
+
metadata.suiteVersion,
|
|
53
|
+
metadata.suiteManifestDigest,
|
|
54
|
+
);
|
|
55
|
+
if (!row) continue;
|
|
56
|
+
suites.push({
|
|
57
|
+
subjectId,
|
|
58
|
+
suiteId: row.suiteId,
|
|
59
|
+
evidenceLayer: requirement.evidenceLayer,
|
|
60
|
+
suiteVersion: row.suiteVersion,
|
|
61
|
+
suiteManifestDigest: row.suiteManifestDigest,
|
|
62
|
+
verdict: row.verdict,
|
|
63
|
+
asOf: row.asOf,
|
|
64
|
+
maxAgeMs: metadata.maxAgeMs,
|
|
65
|
+
notes: row.notes,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
subjectIds: { ...subjectIds },
|
|
71
|
+
projectionAvailable: snapshot.projectionAvailable,
|
|
72
|
+
suites,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Assemble production policy candidate evidence including compatibility snapshots.
|
|
78
|
+
* Compatibility work is completely skipped for legacy/no-requirement profiles.
|
|
79
|
+
* Active compatibility profiles use one catalog snapshot and one bounded SQLite
|
|
80
|
+
* read for all candidate subject IDs.
|
|
81
|
+
*/
|
|
82
|
+
export function assemblePolicyCandidateEvidence(
|
|
83
|
+
config: OcxConfig,
|
|
84
|
+
profile: NormalizedRoutingProfile,
|
|
85
|
+
now: number,
|
|
86
|
+
options: AssemblePolicyEvidenceOptions,
|
|
87
|
+
): PolicyCandidateEvidence[] {
|
|
88
|
+
const compatibilityPolicy = profile.compatibility;
|
|
89
|
+
const hasCompatibilityRequirements = Boolean(
|
|
90
|
+
compatibilityPolicy && compatibilityPolicy.requiredSuites.length > 0,
|
|
91
|
+
);
|
|
92
|
+
const resolvedByCandidate = new Map<string, ResolvedPolicyCompatibilitySubjects>();
|
|
93
|
+
let catalog: CompatibilityCatalogSnapshot = new Map();
|
|
94
|
+
let snapshot: ReturnType<typeof loadCompatibilityEvidenceSnapshot> = {
|
|
95
|
+
projectionAvailable: true,
|
|
96
|
+
projectionIncompatible: false,
|
|
97
|
+
bySubject: new Map(),
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
if (hasCompatibilityRequirements && compatibilityPolicy) {
|
|
101
|
+
const resolveSubjects = options.resolveSubjects ?? resolvePolicyCompatibilitySubjects;
|
|
102
|
+
const loadCatalog = options.loadCatalogSnapshot ?? loadCompatibilityCatalogSnapshot;
|
|
103
|
+
const loadEvidence = options.loadEvidenceSnapshot ?? loadCompatibilityEvidenceSnapshot;
|
|
104
|
+
catalog = loadCatalog(compatibilityPolicy.requiredSuites);
|
|
105
|
+
const subjectIds = new Set<string>();
|
|
106
|
+
|
|
107
|
+
for (const candidate of profile.candidates) {
|
|
108
|
+
const provider = config.providers[candidate.provider];
|
|
109
|
+
if (!provider) continue;
|
|
110
|
+
try {
|
|
111
|
+
const routed = options.routedProviderConfig(candidate.provider, provider);
|
|
112
|
+
const resolved = resolveSubjects(
|
|
113
|
+
config,
|
|
114
|
+
candidate.provider,
|
|
115
|
+
candidate.model,
|
|
116
|
+
routed,
|
|
117
|
+
options.configDir,
|
|
118
|
+
);
|
|
119
|
+
resolvedByCandidate.set(`${candidate.provider}/${candidate.model}`, resolved);
|
|
120
|
+
for (const subjectId of Object.values(resolved.subjectIds)) {
|
|
121
|
+
if (subjectId) subjectIds.add(subjectId);
|
|
122
|
+
}
|
|
123
|
+
} catch {
|
|
124
|
+
// Subject construction failure is handled per required layer as unknown.
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
snapshot = loadEvidence([...subjectIds], options.configDir);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return profile.candidates.map(candidate => {
|
|
132
|
+
const key = `${candidate.provider}/${candidate.model}`;
|
|
133
|
+
const compatibility = hasCompatibilityRequirements && compatibilityPolicy
|
|
134
|
+
? attachCompatibilityEvidence(
|
|
135
|
+
resolvedByCandidate.get(key),
|
|
136
|
+
snapshot,
|
|
137
|
+
catalog,
|
|
138
|
+
compatibilityPolicy,
|
|
139
|
+
)
|
|
140
|
+
: undefined;
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
provider: candidate.provider,
|
|
144
|
+
model: candidate.model,
|
|
145
|
+
capability: candidateCapabilityEvidence(config, candidate.provider, candidate.model),
|
|
146
|
+
health: policyCandidateHealthEvidence(config, candidate, now),
|
|
147
|
+
quota: quotaEvidenceForCandidate({
|
|
148
|
+
provider: candidate.provider,
|
|
149
|
+
model: candidate.model,
|
|
150
|
+
}),
|
|
151
|
+
cost: costEvidenceForCandidate({
|
|
152
|
+
provider: candidate.provider,
|
|
153
|
+
model: candidate.model,
|
|
154
|
+
limitUsd: profile.limits.maxEstimatedCostUsd,
|
|
155
|
+
}),
|
|
156
|
+
...(compatibility ? { compatibility } : {}),
|
|
157
|
+
};
|
|
158
|
+
});
|
|
159
|
+
}
|