@bitkyc08/opencodex 2.55.0 → 2.56.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/gui/dist/assets/{index-VuoiWj9J.js → index-D4zuyIxQ.js} +1 -1
- package/gui/dist/index.html +1 -1
- package/package.json +2 -1
- package/src/adapters/base.ts +21 -0
- package/src/adapters/cursor/transport-retry.ts +46 -1
- package/src/adapters/cursor.ts +4 -0
- package/src/adapters/kiro/adapter.ts +42 -1
- package/src/adapters/kiro-retry.ts +23 -4
- package/src/adapters/openai-chat/errors.ts +116 -0
- package/src/adapters/openai-chat/messages.ts +346 -0
- package/src/adapters/openai-chat/passthrough.ts +146 -0
- package/src/adapters/openai-chat/response-events.ts +117 -0
- package/src/adapters/openai-chat/tool-call-validation.ts +200 -0
- package/src/adapters/openai-chat/tool-schema.ts +477 -0
- package/src/adapters/openai-chat/wire.ts +50 -0
- package/src/adapters/openai-chat.ts +33 -1445
- package/src/adapters/openai-responses/canonical-forward.ts +202 -0
- package/src/adapters/openai-responses/image-gen.ts +406 -0
- package/src/adapters/openai-responses/internal.ts +3 -0
- package/src/adapters/openai-responses/passthrough.ts +611 -0
- package/src/adapters/openai-responses/prompt-cache.ts +83 -0
- package/src/adapters/openai-responses/reasoning.ts +220 -0
- package/src/adapters/openai-responses/request-strips.ts +185 -0
- package/src/adapters/openai-responses/tool-output-recovery.ts +509 -0
- package/src/adapters/openai-responses/tool-schema.ts +293 -0
- package/src/adapters/openai-responses/web-search.ts +156 -0
- package/src/adapters/openai-responses.ts +4 -2625
- package/src/bridge/errors.ts +34 -0
- package/src/bridge/internal.ts +174 -0
- package/src/bridge/response-json.ts +624 -0
- package/src/bridge/sse.ts +1444 -0
- package/src/bridge.ts +5 -2204
- package/src/chat/inbound.ts +12 -1
- package/src/codex/account-lifecycle.ts +3 -0
- package/src/codex/account-store.ts +71 -9
- package/src/codex/auth-api/account-list.ts +507 -0
- package/src/codex/auth-api/http.ts +32 -0
- package/src/codex/auth-api/login-flow.ts +554 -0
- package/src/codex/auth-api/login-state.ts +64 -0
- package/src/codex/auth-api/main-account-probe.ts +331 -0
- package/src/codex/auth-api/pool-mode-gate.ts +274 -0
- package/src/codex/auth-api/pool-quota-probe.ts +512 -0
- package/src/codex/auth-api/reset-credit-service.ts +422 -0
- package/src/codex/auth-api/routes.ts +425 -0
- package/src/codex/auth-api/runtime-config.ts +48 -0
- package/src/codex/auth-api.ts +27 -3118
- package/src/codex/auth-context.ts +95 -28
- package/src/codex/catalog/auto-review.ts +507 -0
- package/src/codex/catalog/build-entries.ts +981 -0
- package/src/codex/catalog/combo-member.ts +375 -0
- package/src/codex/catalog/derive-entry.ts +229 -0
- package/src/codex/catalog/effort.ts +0 -1
- package/src/codex/catalog/gated-native-warn.ts +63 -0
- package/src/codex/catalog/gather-capture.ts +533 -0
- package/src/codex/catalog/model-hints.ts +691 -0
- package/src/codex/catalog/model-visibility.ts +304 -0
- package/src/codex/catalog/provider-fetch.ts +52 -2942
- package/src/codex/catalog/provider-models.ts +685 -0
- package/src/codex/catalog/restore.ts +132 -0
- package/src/codex/catalog/retained-sync.ts +706 -0
- package/src/codex/catalog/routed-gather.ts +858 -0
- package/src/codex/catalog/subagent-roster.ts +176 -0
- package/src/codex/catalog/sync.ts +52 -2698
- package/src/codex/inject/config-toml.ts +563 -0
- package/src/codex/inject/remove.ts +192 -0
- package/src/codex/inject/restore.ts +540 -0
- package/src/codex/inject/routing-classify.ts +109 -0
- package/src/codex/inject/routing-target.ts +125 -0
- package/src/codex/inject.ts +81 -1436
- package/src/codex/lineage.ts +458 -0
- package/src/codex/pool-refresh-backoff.ts +152 -0
- package/src/codex/routing/active-account.ts +194 -0
- package/src/codex/routing/cooldown-math.ts +275 -0
- package/src/codex/routing/health-store.ts +402 -0
- package/src/codex/routing/probe-lease.ts +358 -0
- package/src/codex/routing/selection.ts +703 -0
- package/src/codex/routing/thread-affinity.ts +538 -0
- package/src/codex/routing.ts +353 -2234
- package/src/codex/shim-fingerprint.ts +223 -0
- package/src/codex/shim-inspect.ts +175 -0
- package/src/codex/shim-probe.ts +367 -0
- package/src/codex/shim-restore-lock.ts +169 -0
- package/src/codex/shim-state-file.ts +151 -0
- package/src/codex/shim-templates.ts +265 -0
- package/src/codex/shim.ts +48 -1268
- package/src/config/diagnostics.ts +705 -0
- package/src/config/feature-flags.ts +55 -0
- package/src/config/live-reconcile.ts +403 -0
- package/src/config/load-degrade.ts +880 -0
- package/src/config/mutation-lock.ts +244 -0
- package/src/config/openai-tier-backup.ts +268 -0
- package/src/config/persist-unlocked.ts +92 -0
- package/src/config/proxy-env.ts +188 -0
- package/src/config/salvage.ts +244 -0
- package/src/config/schema/config-schema.ts +640 -0
- package/src/config/schema/leaf-validators.ts +855 -0
- package/src/config/warn-memo.ts +28 -0
- package/src/config.ts +234 -4481
- package/src/generated/compatibility-version.json +539 -39
- package/src/lib/request-execution-budget.ts +69 -20
- package/src/lib/spend-reservation-ledger.ts +940 -0
- package/src/lib/upstream-retry.ts +55 -11
- package/src/lib/workflow-budget.ts +553 -30
- package/src/providers/quota/account-cache.ts +441 -0
- package/src/providers/quota/antigravity.ts +295 -0
- package/src/providers/quota/report-cache.ts +320 -0
- package/src/providers/quota/vendor-probes-key.ts +1243 -0
- package/src/providers/quota/vendor-probes-oauth.ts +590 -0
- package/src/providers/quota.ts +324 -3079
- package/src/providers/registry/entries-core.ts +1221 -0
- package/src/providers/registry/entries-extended.ts +1204 -0
- package/src/providers/registry/model-seeds.ts +908 -0
- package/src/providers/registry/types.ts +352 -0
- package/src/providers/registry.ts +24 -3536
- package/src/responses/continuation-ownership.ts +29 -0
- package/src/responses/state/replay-fingerprint.ts +80 -0
- package/src/responses/state/snapshot-codec.ts +104 -0
- package/src/responses/state/spill-failure.ts +118 -0
- package/src/responses/state/spill-queue.ts +665 -0
- package/src/responses/state/temp-recovery.ts +257 -0
- package/src/responses/state.ts +82 -1143
- package/src/routing/identity-domains.ts +449 -0
- package/src/routing/probe-lease.ts +511 -0
- package/src/server/index/bounded-request.ts +88 -0
- package/src/server/index/live-sideband.ts +565 -0
- package/src/server/index/serve-options.ts +1766 -0
- package/src/server/index/startup-warnings.ts +213 -0
- package/src/server/index/websocket-handler.ts +335 -0
- package/src/server/index.ts +40 -2547
- package/src/server/management/route-registry.ts +26 -23
- package/src/server/management/shared.ts +8 -5
- package/src/server/management/workflow-budget-routes.ts +133 -0
- package/src/server/management-api.ts +12 -0
- package/src/server/request-log-conversation.ts +9 -7
- package/src/server/request-log.ts +245 -1
- package/src/server/responses/account-change-state.ts +233 -0
- package/src/server/responses/adapter-continuation.ts +514 -0
- package/src/server/responses/adapter-delivery.ts +214 -0
- package/src/server/responses/adapter-dispatch.ts +971 -0
- package/src/server/responses/compact.ts +59 -4
- package/src/server/responses/completion-policy.ts +33 -0
- package/src/server/responses/core-auth.ts +527 -0
- package/src/server/responses/core-codex-account.ts +859 -0
- package/src/server/responses/core-combo-failure.ts +210 -0
- package/src/server/responses/core-combo.ts +707 -0
- package/src/server/responses/core-errors.ts +152 -0
- package/src/server/responses/core-lifetime.ts +95 -0
- package/src/server/responses/core-normalize.ts +350 -0
- package/src/server/responses/core-opaque-recovery.ts +380 -0
- package/src/server/responses/core-options.ts +159 -0
- package/src/server/responses/core-replay.ts +225 -0
- package/src/server/responses/core.ts +192 -8893
- package/src/server/responses/passthrough-delivery.ts +856 -0
- package/src/server/responses/passthrough-dispatch.ts +1476 -0
- package/src/server/responses/passthrough-execution.ts +54 -0
- package/src/server/responses/request-prepare.ts +970 -0
- package/src/server/responses/request-send-budget.ts +164 -0
- package/src/server/responses/request-sidecar-auth.ts +149 -0
- package/src/server/responses/request-transport.ts +744 -0
- package/src/server/responses/response-effects.ts +157 -0
- package/src/server/responses/run-turn-execution.ts +448 -0
- package/src/server/responses/sidecar-execution.ts +469 -0
- package/src/server/responses-image-gen-repair.ts +1 -1
- package/src/server/workflow-refusal.ts +84 -0
- package/src/types/config.ts +30 -0
- package/src/usage/log.ts +146 -0
- package/src/usage/summary.ts +171 -21
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { PoolQuotaWriter } from "./quota-types";
|
|
2
|
-
import { createHash,
|
|
2
|
+
import { createHash, timingSafeEqual } from "node:crypto";
|
|
3
3
|
import {
|
|
4
4
|
CodexCredentialGenerationConflictError,
|
|
5
5
|
CodexCredentialRefreshLockTimeoutError,
|
|
@@ -40,6 +40,12 @@ import {
|
|
|
40
40
|
resolveCodexAccountForThreadDetailed,
|
|
41
41
|
type CodexAffinityDecision,
|
|
42
42
|
} from "./routing";
|
|
43
|
+
import {
|
|
44
|
+
codexConversationIdentity,
|
|
45
|
+
recordCodexThreadLineage,
|
|
46
|
+
resolveCodexThreadLineage,
|
|
47
|
+
type CodexThreadLineage,
|
|
48
|
+
} from "./lineage";
|
|
43
49
|
import {
|
|
44
50
|
entitledCodexAccountIdsForModel,
|
|
45
51
|
isDirectCallerEntitledToCodexModel,
|
|
@@ -53,7 +59,6 @@ import { CODEX_UNKNOWN_USAGE_SCORE, getAccountQuota, parseUsageQuota, parseMainP
|
|
|
53
59
|
import type { CodexAccountMode, OcxConfig, OcxProviderConfig } from "../types";
|
|
54
60
|
import { FORWARD_HEADERS } from "../adapters/openai-responses";
|
|
55
61
|
import { captureConfigGeneration } from "../lib/state-store-sweeper";
|
|
56
|
-
import { retainedUtf8Bytes } from "../lib/admission";
|
|
57
62
|
import { extractAccountId, extractEmail } from "../oauth/chatgpt";
|
|
58
63
|
import { getMainAccountHardLockStatus, isMainAccountHardLocked } from "./main-account-hard-lock";
|
|
59
64
|
import {
|
|
@@ -70,9 +75,6 @@ import type { DataPlaneAdmission } from "../server/auth-cors";
|
|
|
70
75
|
import { getMainReserveAuthorization, isMainReserveAuthorizationLive, nativeUserIdClaims, type MainReserveAuthorization } from "./reserve-availability";
|
|
71
76
|
import { UpstreamRetryEvidenceError } from "../lib/upstream-retry";
|
|
72
77
|
|
|
73
|
-
const CODEX_AFFINITY_COMPONENT_MAX_BYTES = 512;
|
|
74
|
-
const CODEX_APP_AFFINITY_KEY = randomBytes(32);
|
|
75
|
-
|
|
76
78
|
/**
|
|
77
79
|
* A request-owned bearer cannot inspect the physical main credential for its plan, but cached
|
|
78
80
|
* WHAM usage is still valid routing evidence for the same logical main account. Score it with
|
|
@@ -88,32 +90,90 @@ function requestOwnedMainPinHasQuotaHeadroom(config: OcxConfig): boolean {
|
|
|
88
90
|
return usage >= CODEX_UNKNOWN_USAGE_SCORE || usage < threshold;
|
|
89
91
|
}
|
|
90
92
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
93
|
+
/**
|
|
94
|
+
* Every thread keys as ITSELF, never as its parent (#4546, wp8).
|
|
95
|
+
*
|
|
96
|
+
* The old rule preferred `x-codex-parent-thread-id`, so every child of one parent bound under
|
|
97
|
+
* the RAW parent id -- one shared entry, unrelated to the root's own `app:HMAC(session, thread)`
|
|
98
|
+
* binding -- and a grandchild keyed on its own parent landed on a key nobody had ever bound.
|
|
99
|
+
* A child therefore started cold while its parent was being served warm somewhere, and no
|
|
100
|
+
* child could hold a binding of its own.
|
|
101
|
+
*
|
|
102
|
+
* Now a request with a `thread-id` keys as HMAC(session ?? parent, thread). A root is
|
|
103
|
+
* unchanged, a child gets an independent key, and a request naming only a parent rides the
|
|
104
|
+
* parent's lane under HMAC(parent, parent) -- the same one-to-one lane it always had, minus
|
|
105
|
+
* the caller-supplied identifier that used to sit in Pool state. Which requests produce no
|
|
106
|
+
* key at all is unchanged. First placement for a child is what consults the family, through
|
|
107
|
+
* `recordCodexThreadLineage` below and the placement hook in ./routing.
|
|
108
|
+
*
|
|
109
|
+
* The derivation itself lives in ./lineage so a lineage record's conversation key and the key
|
|
110
|
+
* the thread actually binds under can never drift apart.
|
|
111
|
+
*/
|
|
112
|
+
export function codexPoolAffinityKey(headers: Headers, now = Date.now()): string | undefined {
|
|
113
|
+
// `now` is threaded rather than read inside because a parent-only turn resolves its key
|
|
114
|
+
// through the recorded lineage, and that record is TTL-bounded: a caller working against a
|
|
115
|
+
// fixed clock would otherwise see a live record as expired and fall back to a key the parent
|
|
116
|
+
// never bound under.
|
|
117
|
+
return codexConversationIdentity(headers, now)?.conversationKey;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** What a caller needs to know to answer the Pool-state question below before auth has run. */
|
|
121
|
+
export interface CodexPoolStateEligibility {
|
|
122
|
+
/** An exact account selector from the route, i.e. `options.accountId` here. */
|
|
123
|
+
readonly accountId?: string;
|
|
124
|
+
readonly modelId?: string;
|
|
125
|
+
readonly admission?: Pick<DataPlaneAdmission, "source">;
|
|
126
|
+
/** The caller presented its own forwardable ChatGPT credential for this route. */
|
|
127
|
+
readonly requestScopedMainCredential?: boolean;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** The one expression both the resolution below and any preview must agree on. */
|
|
131
|
+
function poolStateEligible(
|
|
132
|
+
fixedAccountId: string | undefined,
|
|
133
|
+
requestScopedMainCredential: boolean,
|
|
134
|
+
): boolean {
|
|
135
|
+
return fixedAccountId === undefined && !requestScopedMainCredential;
|
|
96
136
|
}
|
|
97
137
|
|
|
98
138
|
/**
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
139
|
+
* May this request own Pool affinity state at all?
|
|
140
|
+
*
|
|
141
|
+
* Two credentials authenticate outside the Pool: an exact account selector (including the
|
|
142
|
+
* Reserve pin) and a request-owned main bearer, which exists for one request and must never
|
|
143
|
+
* fold into durable account state. Neither may read or write a binding, so neither may read
|
|
144
|
+
* or write LINEAGE either.
|
|
145
|
+
*
|
|
146
|
+
* Exported so that a preview asks the question with the code that answers it, instead of a
|
|
147
|
+
* restatement that can drift. It drifted once already: preview read a family relation from raw
|
|
148
|
+
* request headers before this function had decided anything, so it could follow a Pool family
|
|
149
|
+
* binding while the resolution below deliberately created no affinity -- and model fallback then
|
|
150
|
+
* evaluated eligibility against an account the request would never be authenticated as.
|
|
151
|
+
*/
|
|
152
|
+
export function codexPoolStateEligible(
|
|
153
|
+
headers: Headers,
|
|
154
|
+
policy: CodexAuthPolicyConfig | undefined,
|
|
155
|
+
options: CodexPoolStateEligibility = {},
|
|
156
|
+
): boolean {
|
|
157
|
+
const reserve = requiresReserveAuthorization(policy, options.modelId, options.admission);
|
|
158
|
+
return poolStateEligible(
|
|
159
|
+
reserve ? MAIN_CODEX_ACCOUNT_ID : options.accountId,
|
|
160
|
+
options.requestScopedMainCredential === true && hasCallerCodexBearer(headers),
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* The lineage a PREVIEW is allowed to see: read-only, and only for a request that may hold Pool
|
|
166
|
+
* state. Recording is left to the resolution that actually binds, so a preview can never leave a
|
|
167
|
+
* record behind for a request that turned out to own no Pool state at all.
|
|
102
168
|
*/
|
|
103
|
-
export function
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
return `app:${createHmac("sha256", CODEX_APP_AFFINITY_KEY)
|
|
112
|
-
.update("opencodex-app-pool-affinity-v1\0")
|
|
113
|
-
.update(sessionId)
|
|
114
|
-
.update("\0")
|
|
115
|
-
.update(threadId)
|
|
116
|
-
.digest("base64url")}`;
|
|
169
|
+
export function previewCodexPoolLineage(
|
|
170
|
+
headers: Headers,
|
|
171
|
+
policy: CodexAuthPolicyConfig | undefined,
|
|
172
|
+
options: CodexPoolStateEligibility = {},
|
|
173
|
+
): CodexThreadLineage | undefined {
|
|
174
|
+
return codexPoolStateEligible(headers, policy, options)
|
|
175
|
+
? resolveCodexThreadLineage(headers)
|
|
176
|
+
: undefined;
|
|
117
177
|
}
|
|
118
178
|
|
|
119
179
|
export type CodexAuthContext =
|
|
@@ -798,9 +858,15 @@ export async function resolveCodexAuthContext(
|
|
|
798
858
|
// A caller bearer can still accompany a request that selects a configured Pool account. Do not
|
|
799
859
|
// let that request read, delete, or create a file-main affinity binding while deciding whether a
|
|
800
860
|
// stored account is available; only the stored credential selected below may own Pool state.
|
|
801
|
-
const affinityKey = fixedAccountId
|
|
861
|
+
const affinityKey = poolStateEligible(fixedAccountId, requestScopedMainCredential)
|
|
802
862
|
? codexPoolAffinityKey(headers)
|
|
803
863
|
: undefined;
|
|
864
|
+
// The thread's family relation, recorded under the same condition as the key itself. A
|
|
865
|
+
// first-placing child consults it; a request-owned or fixed credential never enters Pool
|
|
866
|
+
// state, so it never enters lineage either.
|
|
867
|
+
const lineage = affinityKey !== undefined
|
|
868
|
+
? recordCodexThreadLineage(headers)
|
|
869
|
+
: undefined;
|
|
804
870
|
// Why this request is on this account, carried to the request log so a move reads as an event
|
|
805
871
|
// instead of something inferred from account labels across lines (#4546).
|
|
806
872
|
let affinityDecision: CodexAffinityDecision | undefined;
|
|
@@ -873,6 +939,7 @@ export async function resolveCodexAuthContext(
|
|
|
873
939
|
quotaScope,
|
|
874
940
|
selectionOptions,
|
|
875
941
|
options.modelId,
|
|
942
|
+
lineage,
|
|
876
943
|
);
|
|
877
944
|
if (resolution.status === "expired") throw new CodexThreadAffinityExpiredError(resolution.accountId);
|
|
878
945
|
const selected = resolution.status === "selected" ? resolution.accountId : null;
|
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
import { redactSecretString } from "../../lib/redact";
|
|
2
|
+
import type { OcxConfig } from "../../types";
|
|
3
|
+
import { encodeRoutedModelId } from "../../providers/slug-codec";
|
|
4
|
+
import { canonicalAutoReviewModelKey, isValidAutoReviewModel as isValidAutoReviewTarget } from "../../config/provider-validation";
|
|
5
|
+
import { readConfiguredAutoReviewModel } from "./parsing";
|
|
6
|
+
import type { RawEntry } from "./parsing";
|
|
7
|
+
import { configuredCatalogEntry } from "./subagent-roster";
|
|
8
|
+
|
|
9
|
+
const AUTO_REVIEW_ROOT_MARKER = "opencodex_auto_review_root";
|
|
10
|
+
|
|
11
|
+
interface RootAutoReviewStamp {
|
|
12
|
+
slug: string;
|
|
13
|
+
original: string | null;
|
|
14
|
+
applied: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function rootAutoReviewStamp(entry: RawEntry): RootAutoReviewStamp | undefined {
|
|
18
|
+
const value = entry[AUTO_REVIEW_ROOT_MARKER];
|
|
19
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
20
|
+
const stamp = value as Record<string, unknown>;
|
|
21
|
+
if (stamp.slug !== entry.slug || typeof stamp.slug !== "string"
|
|
22
|
+
|| typeof stamp.applied !== "string"
|
|
23
|
+
|| (stamp.original !== null && typeof stamp.original !== "string")) return undefined;
|
|
24
|
+
return stamp as unknown as RootAutoReviewStamp;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
/** True when the value is a valid Codex catalog auto-review selector. */
|
|
29
|
+
export function isValidAutoReviewModel(value: unknown): value is string {
|
|
30
|
+
return isValidAutoReviewTarget(value);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type AutoReviewModelOverrideResult = "absent" | "applied" | "invalid" | "unresolved";
|
|
34
|
+
|
|
35
|
+
/** True when a catalog row was synthesized by opencodex instead of coming from upstream. */
|
|
36
|
+
function isRoutedCatalogEntry(entry: RawEntry): boolean {
|
|
37
|
+
const slug = typeof entry.slug === "string" ? entry.slug : "";
|
|
38
|
+
return slug.includes("/")
|
|
39
|
+
|| (typeof entry.description === "string" && entry.description.startsWith("Routed via opencodex → "));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Restore an owned native value, retaining provenance to avoid legacy reclassification. */
|
|
43
|
+
function clearAutoReviewOverrideValue(entry: RawEntry): void {
|
|
44
|
+
const stamp = rootAutoReviewStamp(entry);
|
|
45
|
+
if (stamp) {
|
|
46
|
+
if (entry.auto_review_model_override === stamp.applied) entry.auto_review_model_override = stamp.original;
|
|
47
|
+
} else {
|
|
48
|
+
entry.auto_review_model_override = null;
|
|
49
|
+
delete entry[AUTO_REVIEW_ROOT_MARKER];
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Legacy whole-catalog root stamp: releases before AUTO_REVIEW_ROOT_MARKER wrote root stamps that
|
|
55
|
+
* are textually identical to an upstream value, so the only way to recognize one is the uniform
|
|
56
|
+
* signature the no-provider path relies on — a single value that a routed row also carries.
|
|
57
|
+
* Returns the stamped values when the observed rows match that shape.
|
|
58
|
+
*/
|
|
59
|
+
function legacyRootStampValues(observedModels: readonly RawEntry[]): ReadonlySet<string> | undefined {
|
|
60
|
+
if (observedModels.some(entry => entry?.[AUTO_REVIEW_ROOT_MARKER] !== undefined)) return undefined;
|
|
61
|
+
const configuredValues = new Set(observedModels.flatMap(entry => {
|
|
62
|
+
const value = entry?.auto_review_model_override;
|
|
63
|
+
return typeof value === "string" && value.trim() ? [value] : [];
|
|
64
|
+
}));
|
|
65
|
+
const globalStamp = configuredValues.size === 1
|
|
66
|
+
&& observedModels.some(entry => {
|
|
67
|
+
const value = entry.auto_review_model_override;
|
|
68
|
+
return isRoutedCatalogEntry(entry)
|
|
69
|
+
&& typeof value === "string"
|
|
70
|
+
&& value.trim().length > 0
|
|
71
|
+
&& configuredValues.has(value);
|
|
72
|
+
})
|
|
73
|
+
&& observedModels.every(entry => {
|
|
74
|
+
const value = entry?.auto_review_model_override;
|
|
75
|
+
return value === null
|
|
76
|
+
|| value === undefined
|
|
77
|
+
|| (typeof value === "string" && configuredValues.has(value));
|
|
78
|
+
});
|
|
79
|
+
return globalStamp ? configuredValues : undefined;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Sweep legacy root stamps off the rows a root removal owns, before provider plans land.
|
|
84
|
+
*
|
|
85
|
+
* Root removal reaches marker-tagged native rows on its own, but a catalog written before the
|
|
86
|
+
* marker only carries the legacy signature — and provider stamping rewrites that signature before
|
|
87
|
+
* the root pass could read it, so the sweep has to run first.
|
|
88
|
+
*/
|
|
89
|
+
function clearLegacyRootStamps(models: readonly RawEntry[], sourceModels: readonly RawEntry[] = []): void {
|
|
90
|
+
const legacyStamp = legacyRootStampValues([...models, ...sourceModels]);
|
|
91
|
+
if (legacyStamp === undefined) return;
|
|
92
|
+
for (const entry of models) {
|
|
93
|
+
if (!entry || typeof entry !== "object") continue;
|
|
94
|
+
const current = entry.auto_review_model_override;
|
|
95
|
+
if (entry[AUTO_REVIEW_ROOT_MARKER] === undefined
|
|
96
|
+
&& typeof current === "string" && legacyStamp.has(current)) clearAutoReviewOverrideValue(entry);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Clear the root selector from every row this path owns: routed rows, rows stamped by a release
|
|
102
|
+
* that writes the provenance marker, and the legacy whole-catalog stamp that predates it.
|
|
103
|
+
*/
|
|
104
|
+
function clearAutoReviewModelOverride(
|
|
105
|
+
models: readonly RawEntry[],
|
|
106
|
+
sourceModels: readonly RawEntry[] = [],
|
|
107
|
+
): void {
|
|
108
|
+
const legacyStamp = legacyRootStampValues([...models, ...sourceModels]);
|
|
109
|
+
for (const entry of models) {
|
|
110
|
+
if (!entry || typeof entry !== "object") continue;
|
|
111
|
+
const current = entry.auto_review_model_override;
|
|
112
|
+
if (isRoutedCatalogEntry(entry)
|
|
113
|
+
|| (entry[AUTO_REVIEW_ROOT_MARKER] === true || rootAutoReviewStamp(entry) !== undefined)
|
|
114
|
+
|| (legacyStamp !== undefined && typeof current === "string" && legacyStamp.has(current))) {
|
|
115
|
+
clearAutoReviewOverrideValue(entry);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Warn once about a malformed or unresolvable root auto-review selector. */
|
|
121
|
+
function warnAutoReviewModelDiagnostic(
|
|
122
|
+
reason: "invalid" | "unresolved",
|
|
123
|
+
configured: string,
|
|
124
|
+
): void {
|
|
125
|
+
const safeConfigured = JSON.stringify(redactSecretString(configured));
|
|
126
|
+
const detail = reason === "unresolved"
|
|
127
|
+
? "the selector was not found in the final catalog"
|
|
128
|
+
: "the selector format is invalid";
|
|
129
|
+
console.warn(
|
|
130
|
+
`[opencodex] auto_review_model ${detail} (${safeConfigured}); preserving normal upstream auto-review behavior.`,
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Warn once about a malformed or unresolvable provider-scoped auto-review selector. */
|
|
135
|
+
function warnProviderAutoReviewModelDiagnostic(
|
|
136
|
+
reason: "invalid" | "unresolved",
|
|
137
|
+
provider: string,
|
|
138
|
+
configured: string,
|
|
139
|
+
): void {
|
|
140
|
+
const safeProvider = JSON.stringify(redactSecretString(provider));
|
|
141
|
+
const safeConfigured = JSON.stringify(redactSecretString(configured));
|
|
142
|
+
const detail = reason === "unresolved"
|
|
143
|
+
? "the selector was not found in the final catalog"
|
|
144
|
+
: "the selector format is invalid";
|
|
145
|
+
console.warn(
|
|
146
|
+
`[opencodex] auto_review_model for provider ${safeProvider} ${detail} (${safeConfigured}); using the next valid provider/root selector or upstream behavior.`,
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Note once when a bare selector resolves to a row outside the provider it was configured on.
|
|
152
|
+
*
|
|
153
|
+
* That is how a native model is named as a reviewer, so it stays usable, but a mistyped target must
|
|
154
|
+
* not be silent: the operator sees which catalog row actually supplies the reviewer.
|
|
155
|
+
*/
|
|
156
|
+
function warnProviderAutoReviewForeignTarget(provider: string, configured: string, target: string): void {
|
|
157
|
+
const safeProvider = JSON.stringify(redactSecretString(provider));
|
|
158
|
+
const safeConfigured = JSON.stringify(redactSecretString(configured));
|
|
159
|
+
const safeTarget = JSON.stringify(redactSecretString(target));
|
|
160
|
+
console.warn(
|
|
161
|
+
`[opencodex] auto_review_model for provider ${safeProvider} (${safeConfigured}) resolved to ${safeTarget}, which is not a row of that provider; that catalog row supplies the reviewer.`,
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Preserve native upstream overrides and the root-derived provenance marker from source rows. */
|
|
166
|
+
function preserveNativeAutoReviewModelOverrides(
|
|
167
|
+
models: readonly RawEntry[],
|
|
168
|
+
sourceModels: readonly RawEntry[],
|
|
169
|
+
): void {
|
|
170
|
+
const existing = new Map<string, { value: string | null; root: true | RootAutoReviewStamp | undefined }>();
|
|
171
|
+
for (const entry of sourceModels) {
|
|
172
|
+
const slug = typeof entry.slug === "string" ? entry.slug : undefined;
|
|
173
|
+
const value = entry.auto_review_model_override;
|
|
174
|
+
if (!slug || isRoutedCatalogEntry(entry)) continue;
|
|
175
|
+
if (typeof value === "string" || value === null) {
|
|
176
|
+
existing.set(slug, { value, root: rootAutoReviewStamp(entry) ?? (entry[AUTO_REVIEW_ROOT_MARKER] === true ? true : undefined) });
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
for (const entry of models) {
|
|
180
|
+
const slug = typeof entry.slug === "string" ? entry.slug : undefined;
|
|
181
|
+
if (!slug || isRoutedCatalogEntry(entry) || !existing.has(slug)) continue;
|
|
182
|
+
const saved = existing.get(slug)!;
|
|
183
|
+
entry.auto_review_model_override = saved.value;
|
|
184
|
+
if (saved.root) entry[AUTO_REVIEW_ROOT_MARKER] = structuredClone(saved.root);
|
|
185
|
+
else delete entry[AUTO_REVIEW_ROOT_MARKER];
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Stamp a root-derived override and mark native rows so later root removal is durable. */
|
|
190
|
+
function stampRootAutoReviewOverride(entry: RawEntry, target: string): void {
|
|
191
|
+
if (!isRoutedCatalogEntry(entry)) {
|
|
192
|
+
const previous = rootAutoReviewStamp(entry);
|
|
193
|
+
const current = entry.auto_review_model_override;
|
|
194
|
+
entry[AUTO_REVIEW_ROOT_MARKER] = {
|
|
195
|
+
slug: typeof entry.slug === "string" ? entry.slug : "",
|
|
196
|
+
original: previous && current === previous.applied
|
|
197
|
+
? previous.original : typeof current === "string" ? current : null,
|
|
198
|
+
applied: target,
|
|
199
|
+
} satisfies RootAutoReviewStamp;
|
|
200
|
+
} else {
|
|
201
|
+
delete entry[AUTO_REVIEW_ROOT_MARKER];
|
|
202
|
+
}
|
|
203
|
+
entry.auto_review_model_override = target;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Stamp a provider-derived override; provider stamps never fall under root removal. */
|
|
207
|
+
function stampProviderAutoReviewOverride(entry: RawEntry, target: string): void {
|
|
208
|
+
entry.auto_review_model_override = target;
|
|
209
|
+
delete entry[AUTO_REVIEW_ROOT_MARKER];
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Apply the root Codex auto-review selector to every catalog row, or clear it when the value is
|
|
214
|
+
* absent, blank, malformed, or does not resolve against the assembled catalog.
|
|
215
|
+
*/
|
|
216
|
+
export function applyAutoReviewModelOverride(
|
|
217
|
+
models: RawEntry[] | undefined,
|
|
218
|
+
autoReviewModel: string | null | undefined,
|
|
219
|
+
sourceModels: readonly RawEntry[] = [],
|
|
220
|
+
): AutoReviewModelOverrideResult {
|
|
221
|
+
if (!models || !Array.isArray(models)) return "absent";
|
|
222
|
+
if (autoReviewModel === null || autoReviewModel === undefined) {
|
|
223
|
+
clearAutoReviewModelOverride(models, sourceModels);
|
|
224
|
+
return "absent";
|
|
225
|
+
}
|
|
226
|
+
const trimmed = autoReviewModel.trim();
|
|
227
|
+
if (!trimmed) {
|
|
228
|
+
clearAutoReviewModelOverride(models, sourceModels);
|
|
229
|
+
return "absent";
|
|
230
|
+
}
|
|
231
|
+
if (!isValidAutoReviewModel(trimmed)) {
|
|
232
|
+
clearAutoReviewModelOverride(models, sourceModels);
|
|
233
|
+
warnAutoReviewModelDiagnostic("invalid", trimmed);
|
|
234
|
+
return "invalid";
|
|
235
|
+
}
|
|
236
|
+
if (!configuredCatalogEntry(models, trimmed)) {
|
|
237
|
+
clearAutoReviewModelOverride(models, sourceModels);
|
|
238
|
+
warnAutoReviewModelDiagnostic("unresolved", trimmed);
|
|
239
|
+
return "unresolved";
|
|
240
|
+
}
|
|
241
|
+
for (const entry of models) {
|
|
242
|
+
if (entry && typeof entry === "object") {
|
|
243
|
+
stampRootAutoReviewOverride(entry, trimmed);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return "applied";
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** Validated provider-scoped target with both the configured spelling and catalog slug. */
|
|
250
|
+
interface ValidProviderReviewTarget {
|
|
251
|
+
configured: string;
|
|
252
|
+
target: string;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** One provider's resolved provider-wide and per-model auto-review targets. */
|
|
256
|
+
interface ProviderReviewPlan {
|
|
257
|
+
wide?: ValidProviderReviewTarget;
|
|
258
|
+
perModel: Map<string, ValidProviderReviewTarget>;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** Public provider namespace of a routed catalog row, when it has one. */
|
|
262
|
+
function catalogEntryProviderName(entry: RawEntry): string | undefined {
|
|
263
|
+
const slug = typeof entry.slug === "string" ? entry.slug : "";
|
|
264
|
+
const slash = slug.indexOf("/");
|
|
265
|
+
return slash > 0 && isRoutedCatalogEntry(entry) ? slug.slice(0, slash) : undefined;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** Encoded model-id segment of a routed catalog row, when it has one. */
|
|
269
|
+
function catalogEntryModelSegment(entry: RawEntry): string | undefined {
|
|
270
|
+
const slug = typeof entry.slug === "string" ? entry.slug : "";
|
|
271
|
+
const slash = slug.indexOf("/");
|
|
272
|
+
return slash > 0 ? slug.slice(slash + 1) : undefined;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** Case-preserving encoded key used to match per-model override maps. */
|
|
276
|
+
function providerModelKey(modelId: string): string {
|
|
277
|
+
return canonicalAutoReviewModelKey(modelId);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* True when another routed row of this provider already carries `alias` as its own model id.
|
|
282
|
+
*
|
|
283
|
+
* The alias API validates against whatever ids discovery has reported so far, so on a cold start an
|
|
284
|
+
* alias can be persisted that later turns out to name a different row. A key using it is then not
|
|
285
|
+
* an alternate spelling of the aliased model — it is that row's id — and must not be propagated.
|
|
286
|
+
*/
|
|
287
|
+
function aliasNamesAnotherRoutedRow(models: readonly RawEntry[], provider: string, alias: string): boolean {
|
|
288
|
+
const encoded = encodeRoutedModelId(alias);
|
|
289
|
+
return models.some(entry => isRoutedCatalogEntry(entry)
|
|
290
|
+
&& catalogEntryProviderName(entry) === provider
|
|
291
|
+
&& catalogEntryModelSegment(entry) === encoded);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** Resolve one configured target against the assembled catalog; bare values name a model of the same provider. */
|
|
295
|
+
function resolveProviderReviewTarget(
|
|
296
|
+
models: readonly RawEntry[],
|
|
297
|
+
provider: string,
|
|
298
|
+
configuredRaw: unknown,
|
|
299
|
+
): { kind: "valid"; value: ValidProviderReviewTarget; foreign?: boolean } | { kind: "invalid"; configured: string } | { kind: "unresolved"; configured: string } | { kind: "absent" } {
|
|
300
|
+
if (typeof configuredRaw !== "string") return { kind: "absent" };
|
|
301
|
+
const configured = configuredRaw.trim();
|
|
302
|
+
if (!configured) return { kind: "absent" };
|
|
303
|
+
if (!isValidAutoReviewModel(configured)) return { kind: "invalid", configured };
|
|
304
|
+
const prefix = `${provider}/`;
|
|
305
|
+
let match: RawEntry | undefined;
|
|
306
|
+
const sameProviderCandidate = (rawModelId: string): RawEntry | undefined => models.find(entry => {
|
|
307
|
+
if (!isRoutedCatalogEntry(entry) || typeof entry.slug !== "string" || !entry.slug.startsWith(prefix)) return false;
|
|
308
|
+
const segment = catalogEntryModelSegment(entry);
|
|
309
|
+
return segment !== undefined && segment === encodeRoutedModelId(rawModelId);
|
|
310
|
+
});
|
|
311
|
+
// A bare selector names a model of this provider. A full selector that resolves in the
|
|
312
|
+
// assembled catalog already names the exact row, including a same-provider encoded slug.
|
|
313
|
+
if (!configured.includes("/")) {
|
|
314
|
+
match = sameProviderCandidate(configured);
|
|
315
|
+
}
|
|
316
|
+
match ??= configuredCatalogEntry(models, configured);
|
|
317
|
+
if (!match && configured.startsWith(prefix)) {
|
|
318
|
+
match = sameProviderCandidate(configured.slice(prefix.length));
|
|
319
|
+
}
|
|
320
|
+
if (!match) {
|
|
321
|
+
// A raw model id may itself contain "/" (for example zenmux moonshotai/kimi-k3).
|
|
322
|
+
// After the full-selector lookup misses, try that spelling as a same-provider id.
|
|
323
|
+
match = sameProviderCandidate(configured);
|
|
324
|
+
}
|
|
325
|
+
if (!match) return { kind: "unresolved", configured };
|
|
326
|
+
const target = typeof match.slug === "string" ? match.slug : configured;
|
|
327
|
+
// A qualified selector may name another provider's row on purpose; only a bare value that lands
|
|
328
|
+
// outside this provider is worth reporting.
|
|
329
|
+
const foreign = !configured.includes("/") && catalogEntryProviderName(match) !== provider;
|
|
330
|
+
return { kind: "valid", value: { configured, target }, ...(foreign ? { foreign: true } : {}) };
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/** Build resolved per-provider plans and emit one diagnostic per bad selector. */
|
|
334
|
+
function buildProviderReviewPlans(
|
|
335
|
+
models: readonly RawEntry[],
|
|
336
|
+
config: Pick<OcxConfig, "providers">,
|
|
337
|
+
): { plans: Map<string, ProviderReviewPlan>; failure?: "invalid" | "unresolved" } {
|
|
338
|
+
const plans = new Map<string, ProviderReviewPlan>();
|
|
339
|
+
let failure: "invalid" | "unresolved" | undefined;
|
|
340
|
+
const warned = new Set<string>();
|
|
341
|
+
const recordFailure = (kind: "invalid" | "unresolved", provider: string, configured: string): void => {
|
|
342
|
+
const signature = `${provider}\u0000${configured}`;
|
|
343
|
+
if (warned.has(signature)) return;
|
|
344
|
+
warned.add(signature);
|
|
345
|
+
warnProviderAutoReviewModelDiagnostic(kind, provider, configured);
|
|
346
|
+
failure ??= kind;
|
|
347
|
+
};
|
|
348
|
+
const recordForeignTarget = (provider: string, configured: string, target: string): void => {
|
|
349
|
+
const signature = `${provider}\u0000foreign\u0000${configured}`;
|
|
350
|
+
if (warned.has(signature)) return;
|
|
351
|
+
warned.add(signature);
|
|
352
|
+
warnProviderAutoReviewForeignTarget(provider, configured, target);
|
|
353
|
+
};
|
|
354
|
+
for (const [name, provider] of Object.entries(config.providers ?? {})) {
|
|
355
|
+
if (provider.autoReviewModel === undefined && provider.autoReviewModelOverrides === undefined) continue;
|
|
356
|
+
const plan: ProviderReviewPlan = { perModel: new Map() };
|
|
357
|
+
if (provider.autoReviewModel !== undefined) {
|
|
358
|
+
const resolved = resolveProviderReviewTarget(models, name, provider.autoReviewModel);
|
|
359
|
+
if (resolved.kind === "valid") {
|
|
360
|
+
plan.wide = resolved.value;
|
|
361
|
+
if (resolved.foreign) recordForeignTarget(name, resolved.value.configured, resolved.value.target);
|
|
362
|
+
}
|
|
363
|
+
else if (resolved.kind !== "absent") recordFailure(resolved.kind, name, resolved.configured);
|
|
364
|
+
}
|
|
365
|
+
if (provider.autoReviewModelOverrides !== undefined) {
|
|
366
|
+
for (const [modelId, rawTarget] of Object.entries(provider.autoReviewModelOverrides)) {
|
|
367
|
+
const resolved = resolveProviderReviewTarget(models, name, rawTarget);
|
|
368
|
+
if (resolved.kind === "valid") {
|
|
369
|
+
plan.perModel.set(providerModelKey(modelId), resolved.value);
|
|
370
|
+
if (resolved.foreign) recordForeignTarget(name, resolved.value.configured, resolved.value.target);
|
|
371
|
+
} else if (resolved.kind !== "absent") {
|
|
372
|
+
recordFailure(resolved.kind, name, resolved.configured);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
// `modelAliases` publishes a second public name for a model id, and a routed row's slug always
|
|
377
|
+
// carries the upstream id — so accept an override key written in either spelling.
|
|
378
|
+
for (const [modelId, alias] of Object.entries(provider.modelAliases ?? {})) {
|
|
379
|
+
if (typeof alias !== "string" || !alias.trim()) continue;
|
|
380
|
+
if (aliasNamesAnotherRoutedRow(models, name, alias)) continue;
|
|
381
|
+
const idKey = providerModelKey(modelId);
|
|
382
|
+
const aliasKey = providerModelKey(alias);
|
|
383
|
+
if (idKey === aliasKey) continue;
|
|
384
|
+
const fromId = plan.perModel.get(idKey);
|
|
385
|
+
const fromAlias = plan.perModel.get(aliasKey);
|
|
386
|
+
if (fromId !== undefined && fromAlias === undefined) plan.perModel.set(aliasKey, fromId);
|
|
387
|
+
else if (fromAlias !== undefined && fromId === undefined) plan.perModel.set(idKey, fromAlias);
|
|
388
|
+
}
|
|
389
|
+
if (plan.wide !== undefined || plan.perModel.size > 0) plans.set(name, plan);
|
|
390
|
+
}
|
|
391
|
+
return { plans, failure };
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/** Apply or clear the root selector only on rows without a provider stamp. */
|
|
395
|
+
function applyRootSelectorToRemaining(
|
|
396
|
+
models: readonly RawEntry[],
|
|
397
|
+
rootValue: string | null | undefined,
|
|
398
|
+
providerStamped: ReadonlySet<RawEntry>,
|
|
399
|
+
): AutoReviewModelOverrideResult {
|
|
400
|
+
const clearRemaining = (): void => {
|
|
401
|
+
for (const entry of models) {
|
|
402
|
+
if (!entry || providerStamped.has(entry)) continue;
|
|
403
|
+
// Native rows written by releases before the root marker cannot be told apart from upstream
|
|
404
|
+
// values once provider stamps diverge. clearLegacyRootStamps sweeps the ones the legacy
|
|
405
|
+
// uniform signature still recognizes before provider plans land, because provider stamping
|
|
406
|
+
// destroys that signature; a catalog that no longer matches it needs a one-off manual sync.
|
|
407
|
+
if (isRoutedCatalogEntry(entry) || entry[AUTO_REVIEW_ROOT_MARKER] === true || rootAutoReviewStamp(entry)) clearAutoReviewOverrideValue(entry);
|
|
408
|
+
}
|
|
409
|
+
};
|
|
410
|
+
if (rootValue === null || rootValue === undefined) {
|
|
411
|
+
clearRemaining();
|
|
412
|
+
return "absent";
|
|
413
|
+
}
|
|
414
|
+
const trimmed = rootValue.trim();
|
|
415
|
+
if (!trimmed) {
|
|
416
|
+
clearRemaining();
|
|
417
|
+
return "absent";
|
|
418
|
+
}
|
|
419
|
+
if (!isValidAutoReviewModel(trimmed)) {
|
|
420
|
+
clearRemaining();
|
|
421
|
+
warnAutoReviewModelDiagnostic("invalid", trimmed);
|
|
422
|
+
return "invalid";
|
|
423
|
+
}
|
|
424
|
+
if (!configuredCatalogEntry(models, trimmed)) {
|
|
425
|
+
clearRemaining();
|
|
426
|
+
warnAutoReviewModelDiagnostic("unresolved", trimmed);
|
|
427
|
+
return "unresolved";
|
|
428
|
+
}
|
|
429
|
+
for (const entry of models) {
|
|
430
|
+
if (!entry || providerStamped.has(entry)) continue;
|
|
431
|
+
stampRootAutoReviewOverride(entry, trimmed);
|
|
432
|
+
}
|
|
433
|
+
return "applied";
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/** Provider-aware variant: provider rows win and the root selector is the fallback. */
|
|
437
|
+
export function applyConfiguredAutoReviewModelOverride(
|
|
438
|
+
models: RawEntry[] | undefined,
|
|
439
|
+
rootAutoReviewModel: string | null | undefined,
|
|
440
|
+
config: Pick<OcxConfig, "providers">,
|
|
441
|
+
sourceModels: readonly RawEntry[] = [],
|
|
442
|
+
): AutoReviewModelOverrideResult {
|
|
443
|
+
if (!models || !Array.isArray(models)) return "absent";
|
|
444
|
+
// Runs unconditionally because the sweep only fires on the uniform legacy signature. A resolved
|
|
445
|
+
// root selector restamps every row it touches below, so the call is behavior-preserving there;
|
|
446
|
+
// with the root absent, invalid, or unresolved those clears are final — which is the point, and
|
|
447
|
+
// also the limit: the legacy heuristic cannot tell a root stamp from an identical upstream value.
|
|
448
|
+
clearLegacyRootStamps(models, sourceModels);
|
|
449
|
+
const { plans, failure } = buildProviderReviewPlans(models, config);
|
|
450
|
+
const providerStamped = new Set<RawEntry>();
|
|
451
|
+
for (const entry of models) {
|
|
452
|
+
if (!entry || typeof entry !== "object") continue;
|
|
453
|
+
const provider = catalogEntryProviderName(entry);
|
|
454
|
+
if (!provider) continue;
|
|
455
|
+
const plan = plans.get(provider);
|
|
456
|
+
if (!plan) continue;
|
|
457
|
+
const modelSegment = catalogEntryModelSegment(entry);
|
|
458
|
+
const perModel = modelSegment === undefined ? undefined : plan.perModel.get(providerModelKey(modelSegment));
|
|
459
|
+
const selected = perModel ?? plan.wide;
|
|
460
|
+
if (!selected) continue;
|
|
461
|
+
stampProviderAutoReviewOverride(entry, selected.target);
|
|
462
|
+
providerStamped.add(entry);
|
|
463
|
+
}
|
|
464
|
+
const rootResult = applyRootSelectorToRemaining(models, rootAutoReviewModel, providerStamped);
|
|
465
|
+
const providerApplied = [...providerStamped].some(entry => typeof entry.auto_review_model_override === "string");
|
|
466
|
+
if (providerApplied) {
|
|
467
|
+
if (rootResult === "invalid" || rootResult === "unresolved") return rootResult;
|
|
468
|
+
return failure ?? "applied";
|
|
469
|
+
}
|
|
470
|
+
return failure ?? rootResult;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/** True when any provider row configures a provider-scoped auto-review selector. */
|
|
474
|
+
function configHasProviderAutoReview(config: Pick<OcxConfig, "providers">): boolean {
|
|
475
|
+
return Object.values(config.providers ?? {}).some(provider =>
|
|
476
|
+
provider.autoReviewModel !== undefined || provider.autoReviewModelOverrides !== undefined);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/** Apply the root Codex auto-review selector after the final catalog merge. */
|
|
480
|
+
export function finalizeAutoReviewModelOverride(
|
|
481
|
+
models: RawEntry[] | undefined,
|
|
482
|
+
sourceModels: readonly RawEntry[] = [],
|
|
483
|
+
config?: Pick<OcxConfig, "providers">,
|
|
484
|
+
): AutoReviewModelOverrideResult {
|
|
485
|
+
if (models && sourceModels.length > 0) preserveNativeAutoReviewModelOverrides(models, sourceModels);
|
|
486
|
+
if (config && configHasProviderAutoReview(config)) {
|
|
487
|
+
return applyConfiguredAutoReviewModelOverride(models, readConfiguredAutoReviewModel(), config, sourceModels);
|
|
488
|
+
}
|
|
489
|
+
return applyAutoReviewModelOverride(models, readConfiguredAutoReviewModel(), sourceModels);
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* Why an account-gated native model stopped being offered, but only when the answer is one the
|
|
493
|
+
* operator can act on.
|
|
494
|
+
*
|
|
495
|
+
* Suppression is an omission: the row is never built, so there is no catalog entry for a reason
|
|
496
|
+
* to ride on and no downstream consumer that could explain it later. #4212's reporter watched
|
|
497
|
+
* their models disappear and reasonably concluded the proxy was broken, because every surface
|
|
498
|
+
* that changed said nothing about the account that caused it.
|
|
499
|
+
*
|
|
500
|
+
* Returns `undefined` for the ordinary case — an account that is simply not entitled to a gated
|
|
501
|
+
* model. That is the default state for most installations, it is not news, and warning about it
|
|
502
|
+
* on every sync would bury the one case that matters. A credential the operator must repair is
|
|
503
|
+
* the case that matters, so that is the only one this speaks up about.
|
|
504
|
+
*
|
|
505
|
+
* Accounts are named with the durable `p`-prefixed log label, the same identifier the dashboard
|
|
506
|
+
* shows, never the raw pool id or the email.
|
|
507
|
+
*/
|