@bitkyc08/opencodex 2.52.0 → 2.53.0-preview.20260913
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-BBOZWGB6.css +1 -0
- package/gui/dist/assets/index-D7ynYo2K.js +128 -0
- package/gui/dist/index.html +2 -2
- package/native/remote-workspace-helper/Cargo.lock +130 -0
- package/native/remote-workspace-helper/Cargo.toml +24 -0
- package/native/remote-workspace-helper/src/main.rs +49 -0
- package/native/remote-workspace-helper/src/protocol.rs +246 -0
- package/native/remote-workspace-helper/src/sandbox/macos.rs +19 -0
- package/native/remote-workspace-helper/src/sandbox/mod.rs +77 -0
- package/native/remote-workspace-helper/src/sandbox/windows.rs +15 -0
- package/package.json +6 -1
- package/src/adapters/anthropic-image-normalize.ts +30 -2
- package/src/adapters/anthropic.ts +1 -1
- package/src/adapters/base.ts +8 -2
- package/src/adapters/cursor/cursor-errors.ts +12 -0
- package/src/adapters/cursor/thread-continuity.ts +93 -0
- package/src/adapters/cursor.ts +104 -73
- package/src/adapters/devin/cloud-direct/chat.ts +312 -23
- package/src/adapters/devin/cloud-direct/metadata.ts +31 -2
- package/src/adapters/devin/live-models.ts +70 -3
- package/src/adapters/devin.ts +281 -21
- package/src/adapters/google-wire-compiler.ts +14 -6
- package/src/adapters/google.ts +22 -8
- package/src/adapters/kiro/adapter.ts +316 -0
- package/src/adapters/kiro/conversation.ts +136 -0
- package/src/adapters/kiro/payload.ts +432 -0
- package/src/adapters/kiro/reasoning.ts +56 -0
- package/src/adapters/kiro/stream.ts +1153 -0
- package/src/adapters/kiro/usage.ts +223 -0
- package/src/adapters/kiro/wire.ts +76 -0
- package/src/adapters/kiro.ts +8 -2319
- package/src/adapters/mimo-free.ts +1 -1
- package/src/adapters/openai-chat-images.ts +101 -0
- package/src/adapters/openai-chat.ts +201 -181
- package/src/adapters/openai-responses.ts +92 -224
- package/src/adapters/registry.ts +0 -7
- package/src/adapters/run-turn-queue.ts +13 -6
- package/src/bridge.ts +14 -15
- package/src/chat/inbound.ts +29 -4
- package/src/chat/outbound.ts +145 -107
- package/src/claude/desktop-profile.ts +4 -6
- package/src/cli/account-api.ts +14 -0
- package/src/cli/account-extended.ts +1 -1
- package/src/cli/account-history.ts +60 -0
- package/src/cli/account-main.ts +80 -0
- package/src/cli/account.ts +11 -3
- package/src/cli/capabilities.ts +113 -0
- package/src/cli/catalog.ts +109 -0
- package/src/cli/dispatch.ts +9 -0
- package/src/cli/help.ts +2 -0
- package/src/cli/index.ts +2 -2
- package/src/cli/observe.ts +28 -1
- package/src/cli/opencode.ts +42 -8
- package/src/cli/provider-runtime.ts +11 -1
- package/src/cli/provider.ts +22 -2
- package/src/cli/registry.ts +21 -0
- package/src/cli/remote-workspace.ts +154 -0
- package/src/cli/status.ts +39 -7
- package/src/cli/usage-report.ts +14 -2
- package/src/client/hub-client.ts +34 -0
- package/src/client/hub-state.ts +9 -1
- package/src/codex/account-store.ts +78 -0
- package/src/codex/auth-api.ts +81 -54
- package/src/codex/auth-context.ts +45 -16
- package/src/codex/catalog/effort.ts +1 -1
- package/src/codex/catalog/metadata.ts +3 -6
- package/src/codex/catalog/native-models.ts +4 -4
- package/src/codex/catalog/parsing.ts +2 -20
- package/src/codex/catalog/provider-fetch.ts +10 -1
- package/src/codex/catalog/remote.ts +233 -0
- package/src/codex/catalog/sync.ts +403 -35
- package/src/codex/convergence.ts +1 -1
- package/src/codex/history-manifest.ts +36 -0
- package/src/codex/history-provider.ts +32 -5
- package/src/codex/inject.ts +9 -0
- package/src/codex/main-account.ts +113 -0
- package/src/codex/main-device-reauth-api.ts +89 -0
- package/src/codex/main-device-reauth.ts +217 -0
- package/src/codex/native-residue.ts +9 -2
- package/src/codex/quota-auto-refresh.ts +3 -2
- package/src/codex/quota-capacity.ts +98 -0
- package/src/codex/quota-history.ts +160 -0
- package/src/codex/quota-types.ts +8 -0
- package/src/codex/quota.ts +118 -91
- package/src/codex/refresh.ts +2 -1
- package/src/codex/routing.ts +90 -17
- package/src/codex/sync.ts +33 -4
- package/src/combos/request.ts +19 -1
- package/src/config/multi-agent-surface.ts +61 -0
- package/src/config/provider-validation.ts +176 -0
- package/src/config.ts +213 -11
- package/src/generated/compatibility-version.json +436 -168
- package/src/images/loop.ts +119 -36
- package/src/lib/admission.ts +12 -6
- package/src/lib/redact.ts +7 -0
- package/src/lib/translator-budget.ts +4 -3
- package/src/lib/windows-atomic-replace.ts +1 -0
- package/src/lib/windows-elevation.ts +1 -1
- package/src/oauth/chatgpt-device.ts +62 -5
- package/src/oauth/devin/cli-import.ts +130 -0
- package/src/oauth/devin.ts +63 -8
- package/src/oauth/index.ts +29 -14
- package/src/oauth/kiro.ts +18 -6
- package/src/oauth/login-cli.ts +9 -1
- package/src/oauth/meta-muse-device.ts +464 -0
- package/src/oauth/meta-muse.ts +123 -32
- package/src/oauth/pool-kernel.ts +9 -0
- package/src/oauth/pool-settings-capability.ts +2 -2
- package/src/oauth/store.ts +57 -0
- package/src/oauth/types.ts +31 -0
- package/src/providers/derive.ts +13 -3
- package/src/providers/devin-cli-authmode-migration.ts +57 -35
- package/src/providers/devin-provider-merge-migration.ts +240 -0
- package/src/providers/muse-key-quota.ts +117 -0
- package/src/providers/muse-subscription-usage.ts +14 -2
- package/src/providers/openai-sidecar.ts +25 -3
- package/src/providers/opencode-zen-rate-limit.ts +58 -0
- package/src/providers/provider-id-rewrite.ts +20 -5
- package/src/providers/quota-types.ts +12 -0
- package/src/providers/quota.ts +143 -102
- package/src/providers/reasoning-metadata.ts +543 -0
- package/src/providers/registry.ts +80 -49
- package/src/reasoning-effort.ts +26 -2
- package/src/remote/hub-usage.ts +32 -0
- package/src/remote-control/index.ts +192 -41
- package/src/remote-control/workspace-activation.ts +9 -0
- package/src/remote-control/workspace-agent-connection.ts +366 -0
- package/src/remote-control/workspace-claude-runtime.ts +243 -0
- package/src/remote-control/workspace-codex-runtime.ts +531 -0
- package/src/remote-control/workspace-codex-sandbox.ts +115 -0
- package/src/remote-control/workspace-command-runner.ts +748 -0
- package/src/remote-control/workspace-coordinator.ts +231 -0
- package/src/remote-control/workspace-device.ts +585 -0
- package/src/remote-control/workspace-executable.ts +43 -0
- package/src/remote-control/workspace-executor.ts +397 -0
- package/src/remote-control/workspace-hub.ts +519 -0
- package/src/remote-control/workspace-pi-runtime.ts +382 -0
- package/src/remote-control/workspace-process.ts +129 -0
- package/src/remote-control/workspace-rpc.ts +304 -0
- package/src/remote-control/workspace-runtime.ts +60 -0
- package/src/remote-control/workspace-secret-store.ts +39 -0
- package/src/remote-control/workspace-sessions.ts +799 -0
- package/src/remote-control/workspace-tool-bridge.ts +192 -0
- package/src/responses/code-mode-helper-compat.ts +22 -3
- package/src/responses/hosted-tool-policy.ts +0 -1
- package/src/responses/muse-tool-name-alias.ts +379 -0
- package/src/responses/plaintext-v2-agent-messages.ts +902 -0
- package/src/router.ts +7 -0
- package/src/routing/compatibility/behavior.ts +0 -1
- package/src/server/audio-client.ts +64 -0
- package/src/server/audio-dictation.ts +91 -0
- package/src/server/audio-live.ts +185 -0
- package/src/server/audio-transcriptions.ts +183 -0
- package/src/server/audio-upstream.ts +153 -0
- package/src/server/auth-cors.ts +61 -2
- package/src/server/chat-completions.ts +1 -1
- package/src/server/chat-native-sse.ts +92 -48
- package/src/server/chat-native.ts +37 -15
- package/src/server/hub-usage.ts +57 -0
- package/src/server/images.ts +4 -0
- package/src/server/index.ts +722 -57
- package/src/server/lifecycle.ts +5 -6
- package/src/server/live-call-bindings.ts +60 -0
- package/src/server/live.ts +12 -1
- package/src/server/management/agent-settings-routes.ts +25 -4
- package/src/server/management/api-access.ts +37 -0
- package/src/server/management/api-key-usage.ts +7 -2
- package/src/server/management/config-routes.ts +1 -18
- package/src/server/management/context.ts +15 -0
- package/src/server/management/logs-usage-routes.ts +2 -0
- package/src/server/management/oauth-account-routes.ts +39 -12
- package/src/server/management/provider-routes.ts +125 -2
- package/src/server/management/remote-workspace-routes.ts +140 -0
- package/src/server/management/route-registry.ts +15 -0
- package/src/server/management/usage-aggregate-cache.ts +14 -15
- package/src/server/management/usage-summary-cache.ts +2 -0
- package/src/server/management-api.ts +23 -0
- package/src/server/ports.ts +17 -0
- package/src/server/relay-eager.ts +4 -1
- package/src/server/relay.ts +70 -10
- package/src/server/request-decompress.ts +6 -3
- package/src/server/responses/agent-task-recovery.ts +25 -32
- package/src/server/responses/codex-auth-error.ts +11 -0
- package/src/server/responses/codex-ws-exchange.ts +52 -3
- package/src/server/responses/codex-ws-wire.ts +55 -0
- package/src/server/responses/compact.ts +9 -1
- package/src/server/responses/core.ts +337 -73
- package/src/server/responses/encrypted-payload.ts +45 -2
- package/src/server/responses/ws-upstream.ts +4 -1
- package/src/server/responses-self-named-namespace-scrub.ts +1 -3
- package/src/server/responses-undeclared-tool-guard.ts +1 -1
- package/src/server/search.ts +3 -0
- package/src/server/sse-payload-rewrite.ts +136 -51
- package/src/server/ws-bridge.ts +35 -1
- package/src/service/cli.ts +372 -0
- package/src/service/diagnostics.ts +340 -0
- package/src/service/guards.ts +303 -0
- package/src/service/health.ts +222 -0
- package/src/service/launchd.ts +853 -0
- package/src/service/orchestration.ts +617 -0
- package/src/service/repair.ts +334 -0
- package/src/service/state.ts +363 -0
- package/src/service/systemd.ts +229 -0
- package/src/service/windows-ops.ts +690 -0
- package/src/service/windows-scheduler.ts +769 -0
- package/src/service/windows-taskxml.ts +613 -0
- package/src/service.ts +22 -5550
- package/src/storage/cleanup/db.ts +258 -0
- package/src/storage/cleanup/execute.ts +358 -0
- package/src/storage/cleanup/paths.ts +189 -0
- package/src/storage/cleanup/pending.ts +140 -0
- package/src/storage/cleanup/preview.ts +292 -0
- package/src/storage/cleanup/reconcile.ts +347 -0
- package/src/storage/cleanup/restore.ts +932 -0
- package/src/storage/cleanup/satellite.ts +474 -0
- package/src/storage/cleanup/staging.ts +129 -0
- package/src/storage/cleanup/types.ts +98 -0
- package/src/storage/cleanup.ts +49 -3127
- package/src/types/accounts.ts +2 -0
- package/src/types/config.ts +13 -12
- package/src/types/provider.ts +37 -0
- package/src/types/request.ts +2 -0
- package/src/types/tools.ts +17 -5
- package/src/types.ts +1 -0
- package/src/usage/expected-prices.ts +127 -0
- package/src/usage/log.ts +58 -1
- package/src/vision/eligibility.ts +13 -2
- package/src/web-search/loop.ts +56 -3
- package/gui/dist/assets/index-CWXut3rG.js +0 -115
- package/gui/dist/assets/index-EdoPnm9_.css +0 -1
- package/src/adapters/devin-cli/acp.ts +0 -204
- package/src/adapters/devin-cli/adapter.ts +0 -345
- package/src/adapters/devin-cli/binary.ts +0 -69
- package/src/adapters/devin-cli/models.ts +0 -57
- package/src/oauth/devin-cli.ts +0 -149
- package/src/server/responses-reasoning-summary-rewrite.ts +0 -178
|
@@ -16,7 +16,8 @@ import { CODEX_REASONING_LEVELS, codexEffortRank, configuredReasoningEfforts, mo
|
|
|
16
16
|
import { getModelMetadata, getModelMetadataCaseInsensitive, listModelMetadata, resolveMetadataProvider } from "../../generated/model-metadata";
|
|
17
17
|
import { enrichProviderFromRegistry, shouldCaseFoldMetadataModelId } from "../../providers/derive";
|
|
18
18
|
import { applyProviderContextCap, providerContextCap } from "../../providers/context-cap";
|
|
19
|
-
import { routedSlug, slugEquals, slugEquivalenceKey, slugsEquivalent } from "../../providers/slug-codec";
|
|
19
|
+
import { encodeRoutedModelId, routedSlug, slugEquals, slugEquivalenceKey, slugsEquivalent } from "../../providers/slug-codec";
|
|
20
|
+
import { canonicalAutoReviewModelKey, isValidAutoReviewModel as isValidAutoReviewTarget } from "../../config/provider-validation";
|
|
20
21
|
import { identifyRoutedModel } from "../../adapters/identity";
|
|
21
22
|
import { filterCursorConfiguredModelsByLiveDiscovery } from "../../adapters/cursor/discovery";
|
|
22
23
|
import { fetchCursorUsableModels } from "../../adapters/cursor/live-models";
|
|
@@ -48,7 +49,7 @@ import { codexAccountLogLabel, fallbackCodexAccountLogLabel } from "../account-l
|
|
|
48
49
|
|
|
49
50
|
import { CODEX_CUSTOM_MODEL_CATALOG_KIND, CODEX_PROVIDER_MODEL_CATALOG_KIND, activeCodexModelsCachePath, applyCatalogMetadata, applyMultiAgentMode, applyNativeOpenAiContextOverride, applyRoutedCodexToolMode, catalogBackupPathFor, catalogHasRoutedEntries, catalogModelSlug, ensureStrictCatalogFields, findNativeTemplate, findSupportedNativeTemplate, isDefaultCatalogPath, isRoutedModelCompatibilityExcluded, legacyCatalogBackupPath, normalizeRoutedCatalogEntry, normalizeServiceTiers, readCatalog, readCatalogBackup, readCodexCatalogPath, readCodexCatalogPathForHome, readConfiguredAutoReviewModel, readNativeBaseline } from "./parsing";
|
|
50
51
|
import type { CatalogModel, MultiAgentMode, RawCatalog, RawEntry } from "./parsing";
|
|
51
|
-
import { accountBoundNativeOpenAiSlugs, accountBoundNativeOpenAiSlugsBySelector, applyNativeVisibility, CODEX_NATIVE_ALIAS_CATALOG_KIND, desktopAllowlistSuppressedNativeSlugs, disabledNativeSlugs, isNativeAliasCatalogEntry, isUnsupportedOpenAiNativeSlug, NATIVE_OPENAI_MODELS, nativeContextLimits, observedAccountBoundNativeEntries, shouldIncludeAccountBoundNativeOpenAi, shouldIncludeNativeOpenAi, shouldUpgradeToUpstreamEntry, SUPPORTED_NATIVE_OPENAI_SLUGS, upstreamNativeEntry, type NativeContextLimitsInput } from "./metadata";
|
|
52
|
+
import { accountBoundNativeOpenAiSlugs, accountBoundNativeOpenAiSlugsBySelector, applyNativeVisibility, CODEX_NATIVE_ALIAS_CATALOG_KIND, desktopAllowlistSuppressedNativeSlugs, disabledNativeSlugs, isNativeAliasCatalogEntry, isUnsupportedOpenAiNativeSlug, NATIVE_OPENAI_MODELS, RETIRED_NATIVE_OPENAI_MODELS, nativeContextLimits, observedAccountBoundNativeEntries, shouldIncludeAccountBoundNativeOpenAi, shouldIncludeNativeOpenAi, shouldUpgradeToUpstreamEntry, SUPPORTED_NATIVE_OPENAI_SLUGS, upstreamNativeEntry, type NativeContextLimitsInput } from "./metadata";
|
|
52
53
|
import {
|
|
53
54
|
bundledCatalogCacheState,
|
|
54
55
|
loadBundledCodexCatalog,
|
|
@@ -260,7 +261,7 @@ export function finishUpstreamNativeEntry(clone: RawEntry, priority: number, con
|
|
|
260
261
|
if (priority !== 9) clone.priority = priority;
|
|
261
262
|
applyNativeOpenAiContextOverride(clone, contextCap);
|
|
262
263
|
// GPT-5.6 natives keep their exact upstream ladders (e.g. luna has max but no ultra).
|
|
263
|
-
// Older natives (gpt-5.5
|
|
264
|
+
// Older natives (gpt-5.5) get mock max + ultra
|
|
264
265
|
// (wire-clamped to xhigh). Ultra is always advertised regardless of v2 toggle.
|
|
265
266
|
if (!isGpt56NativeSlug(String(clone.slug ?? ""))) ensureUltraReasoningLevel(clone);
|
|
266
267
|
return ensureStrictCatalogFields(normalizeServiceTiers(clone));
|
|
@@ -399,15 +400,10 @@ export function deriveEntry(
|
|
|
399
400
|
applyNativeOpenAiContextOverride(e, contextCap);
|
|
400
401
|
if (isGpt56NativeSlug(slug)) ensureGpt56ReasoningLevels(e);
|
|
401
402
|
else ensureUltraReasoningLevel(e);
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
// Spark NEEDS use_responses_lite: true — it controls the tool delivery format
|
|
407
|
-
// (AdditionalTools in input vs top-level tools). The reasoning params that
|
|
408
|
-
// use_responses_lite triggers (context: "all_turns", summary) are stripped
|
|
409
|
-
// separately in the passthrough adapter (stripUnsupportedReasoningParams).
|
|
410
|
-
if (!slug.includes("codex-spark")) delete e.use_responses_lite;
|
|
403
|
+
// Older natives do not support Responses Lite. A newer template must not enable
|
|
404
|
+
// reasoning.context or WebSockets on those models.
|
|
405
|
+
if (!isGpt56NativeSlug(slug)) {
|
|
406
|
+
delete e.use_responses_lite;
|
|
411
407
|
delete e.supports_websockets;
|
|
412
408
|
}
|
|
413
409
|
}
|
|
@@ -1404,6 +1400,8 @@ interface RetainedCatalogSyncResult {
|
|
|
1404
1400
|
path: string;
|
|
1405
1401
|
catalogWritten: boolean;
|
|
1406
1402
|
comboOmissions: ComboCatalogOmission[];
|
|
1403
|
+
/** Validated catalog commit (including identical bytes), or a refused refresh. */
|
|
1404
|
+
refreshOutcome?: "committed" | "refused";
|
|
1407
1405
|
/** `desired_disabled` observed under K after the provider await; nothing was written. */
|
|
1408
1406
|
skippedReason?: "desired_disabled";
|
|
1409
1407
|
}
|
|
@@ -1586,29 +1584,58 @@ function catalogModelsForMergeWithNativeRecovery(
|
|
|
1586
1584
|
]);
|
|
1587
1585
|
}
|
|
1588
1586
|
|
|
1589
|
-
const
|
|
1587
|
+
const AUTO_REVIEW_ROOT_MARKER = "opencodex_auto_review_root";
|
|
1590
1588
|
|
|
1589
|
+
interface RootAutoReviewStamp {
|
|
1590
|
+
slug: string;
|
|
1591
|
+
original: string | null;
|
|
1592
|
+
applied: string;
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
function rootAutoReviewStamp(entry: RawEntry): RootAutoReviewStamp | undefined {
|
|
1596
|
+
const value = entry[AUTO_REVIEW_ROOT_MARKER];
|
|
1597
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
1598
|
+
const stamp = value as Record<string, unknown>;
|
|
1599
|
+
if (stamp.slug !== entry.slug || typeof stamp.slug !== "string"
|
|
1600
|
+
|| typeof stamp.applied !== "string"
|
|
1601
|
+
|| (stamp.original !== null && typeof stamp.original !== "string")) return undefined;
|
|
1602
|
+
return stamp as unknown as RootAutoReviewStamp;
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
|
|
1606
|
+
/** True when the value is a valid Codex catalog auto-review selector. */
|
|
1591
1607
|
export function isValidAutoReviewModel(value: unknown): value is string {
|
|
1592
|
-
|
|
1593
|
-
const trimmed = value.trim();
|
|
1594
|
-
return Boolean(trimmed)
|
|
1595
|
-
&& trimmed.length <= 1024
|
|
1596
|
-
&& !AUTO_REVIEW_MODEL_CONTROL_CHARS.test(trimmed);
|
|
1608
|
+
return isValidAutoReviewTarget(value);
|
|
1597
1609
|
}
|
|
1598
1610
|
|
|
1599
1611
|
export type AutoReviewModelOverrideResult = "absent" | "applied" | "invalid" | "unresolved";
|
|
1600
1612
|
|
|
1613
|
+
/** True when a catalog row was synthesized by opencodex instead of coming from upstream. */
|
|
1601
1614
|
function isRoutedCatalogEntry(entry: RawEntry): boolean {
|
|
1602
1615
|
const slug = typeof entry.slug === "string" ? entry.slug : "";
|
|
1603
1616
|
return slug.includes("/")
|
|
1604
1617
|
|| (typeof entry.description === "string" && entry.description.startsWith("Routed via opencodex → "));
|
|
1605
1618
|
}
|
|
1606
1619
|
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
)
|
|
1611
|
-
|
|
1620
|
+
/** Restore an owned native value, retaining provenance to avoid legacy reclassification. */
|
|
1621
|
+
function clearAutoReviewOverrideValue(entry: RawEntry): void {
|
|
1622
|
+
const stamp = rootAutoReviewStamp(entry);
|
|
1623
|
+
if (stamp) {
|
|
1624
|
+
if (entry.auto_review_model_override === stamp.applied) entry.auto_review_model_override = stamp.original;
|
|
1625
|
+
} else {
|
|
1626
|
+
entry.auto_review_model_override = null;
|
|
1627
|
+
delete entry[AUTO_REVIEW_ROOT_MARKER];
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
/**
|
|
1632
|
+
* Legacy whole-catalog root stamp: releases before AUTO_REVIEW_ROOT_MARKER wrote root stamps that
|
|
1633
|
+
* are textually identical to an upstream value, so the only way to recognize one is the uniform
|
|
1634
|
+
* signature the no-provider path relies on — a single value that a routed row also carries.
|
|
1635
|
+
* Returns the stamped values when the observed rows match that shape.
|
|
1636
|
+
*/
|
|
1637
|
+
function legacyRootStampValues(observedModels: readonly RawEntry[]): ReadonlySet<string> | undefined {
|
|
1638
|
+
if (observedModels.some(entry => entry?.[AUTO_REVIEW_ROOT_MARKER] !== undefined)) return undefined;
|
|
1612
1639
|
const configuredValues = new Set(observedModels.flatMap(entry => {
|
|
1613
1640
|
const value = entry?.auto_review_model_override;
|
|
1614
1641
|
return typeof value === "string" && value.trim() ? [value] : [];
|
|
@@ -1627,16 +1654,48 @@ function clearAutoReviewModelOverride(
|
|
|
1627
1654
|
|| value === undefined
|
|
1628
1655
|
|| (typeof value === "string" && configuredValues.has(value));
|
|
1629
1656
|
});
|
|
1657
|
+
return globalStamp ? configuredValues : undefined;
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
/**
|
|
1661
|
+
* Sweep legacy root stamps off the rows a root removal owns, before provider plans land.
|
|
1662
|
+
*
|
|
1663
|
+
* Root removal reaches marker-tagged native rows on its own, but a catalog written before the
|
|
1664
|
+
* marker only carries the legacy signature — and provider stamping rewrites that signature before
|
|
1665
|
+
* the root pass could read it, so the sweep has to run first.
|
|
1666
|
+
*/
|
|
1667
|
+
function clearLegacyRootStamps(models: readonly RawEntry[], sourceModels: readonly RawEntry[] = []): void {
|
|
1668
|
+
const legacyStamp = legacyRootStampValues([...models, ...sourceModels]);
|
|
1669
|
+
if (legacyStamp === undefined) return;
|
|
1670
|
+
for (const entry of models) {
|
|
1671
|
+
if (!entry || typeof entry !== "object") continue;
|
|
1672
|
+
const current = entry.auto_review_model_override;
|
|
1673
|
+
if (entry[AUTO_REVIEW_ROOT_MARKER] === undefined
|
|
1674
|
+
&& typeof current === "string" && legacyStamp.has(current)) clearAutoReviewOverrideValue(entry);
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
/**
|
|
1679
|
+
* Clear the root selector from every row this path owns: routed rows, rows stamped by a release
|
|
1680
|
+
* that writes the provenance marker, and the legacy whole-catalog stamp that predates it.
|
|
1681
|
+
*/
|
|
1682
|
+
function clearAutoReviewModelOverride(
|
|
1683
|
+
models: readonly RawEntry[],
|
|
1684
|
+
sourceModels: readonly RawEntry[] = [],
|
|
1685
|
+
): void {
|
|
1686
|
+
const legacyStamp = legacyRootStampValues([...models, ...sourceModels]);
|
|
1630
1687
|
for (const entry of models) {
|
|
1631
1688
|
if (!entry || typeof entry !== "object") continue;
|
|
1632
1689
|
const current = entry.auto_review_model_override;
|
|
1633
1690
|
if (isRoutedCatalogEntry(entry)
|
|
1634
|
-
|| (
|
|
1635
|
-
|
|
1691
|
+
|| (entry[AUTO_REVIEW_ROOT_MARKER] === true || rootAutoReviewStamp(entry) !== undefined)
|
|
1692
|
+
|| (legacyStamp !== undefined && typeof current === "string" && legacyStamp.has(current))) {
|
|
1693
|
+
clearAutoReviewOverrideValue(entry);
|
|
1636
1694
|
}
|
|
1637
1695
|
}
|
|
1638
1696
|
}
|
|
1639
1697
|
|
|
1698
|
+
/** Warn once about a malformed or unresolvable root auto-review selector. */
|
|
1640
1699
|
function warnAutoReviewModelDiagnostic(
|
|
1641
1700
|
reason: "invalid" | "unresolved",
|
|
1642
1701
|
configured: string,
|
|
@@ -1650,24 +1709,88 @@ function warnAutoReviewModelDiagnostic(
|
|
|
1650
1709
|
);
|
|
1651
1710
|
}
|
|
1652
1711
|
|
|
1712
|
+
/** Warn once about a malformed or unresolvable provider-scoped auto-review selector. */
|
|
1713
|
+
function warnProviderAutoReviewModelDiagnostic(
|
|
1714
|
+
reason: "invalid" | "unresolved",
|
|
1715
|
+
provider: string,
|
|
1716
|
+
configured: string,
|
|
1717
|
+
): void {
|
|
1718
|
+
const safeProvider = JSON.stringify(redactSecretString(provider));
|
|
1719
|
+
const safeConfigured = JSON.stringify(redactSecretString(configured));
|
|
1720
|
+
const detail = reason === "unresolved"
|
|
1721
|
+
? "the selector was not found in the final catalog"
|
|
1722
|
+
: "the selector format is invalid";
|
|
1723
|
+
console.warn(
|
|
1724
|
+
`[opencodex] auto_review_model for provider ${safeProvider} ${detail} (${safeConfigured}); using the next valid provider/root selector or upstream behavior.`,
|
|
1725
|
+
);
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
/**
|
|
1729
|
+
* Note once when a bare selector resolves to a row outside the provider it was configured on.
|
|
1730
|
+
*
|
|
1731
|
+
* That is how a native model is named as a reviewer, so it stays usable, but a mistyped target must
|
|
1732
|
+
* not be silent: the operator sees which catalog row actually supplies the reviewer.
|
|
1733
|
+
*/
|
|
1734
|
+
function warnProviderAutoReviewForeignTarget(provider: string, configured: string, target: string): void {
|
|
1735
|
+
const safeProvider = JSON.stringify(redactSecretString(provider));
|
|
1736
|
+
const safeConfigured = JSON.stringify(redactSecretString(configured));
|
|
1737
|
+
const safeTarget = JSON.stringify(redactSecretString(target));
|
|
1738
|
+
console.warn(
|
|
1739
|
+
`[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.`,
|
|
1740
|
+
);
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
/** Preserve native upstream overrides and the root-derived provenance marker from source rows. */
|
|
1653
1744
|
function preserveNativeAutoReviewModelOverrides(
|
|
1654
1745
|
models: readonly RawEntry[],
|
|
1655
1746
|
sourceModels: readonly RawEntry[],
|
|
1656
1747
|
): void {
|
|
1657
|
-
const existing = new Map<string, string | null>();
|
|
1748
|
+
const existing = new Map<string, { value: string | null; root: true | RootAutoReviewStamp | undefined }>();
|
|
1658
1749
|
for (const entry of sourceModels) {
|
|
1659
1750
|
const slug = typeof entry.slug === "string" ? entry.slug : undefined;
|
|
1660
1751
|
const value = entry.auto_review_model_override;
|
|
1661
1752
|
if (!slug || isRoutedCatalogEntry(entry)) continue;
|
|
1662
|
-
if (typeof value === "string" || value === null)
|
|
1753
|
+
if (typeof value === "string" || value === null) {
|
|
1754
|
+
existing.set(slug, { value, root: rootAutoReviewStamp(entry) ?? (entry[AUTO_REVIEW_ROOT_MARKER] === true ? true : undefined) });
|
|
1755
|
+
}
|
|
1663
1756
|
}
|
|
1664
1757
|
for (const entry of models) {
|
|
1665
1758
|
const slug = typeof entry.slug === "string" ? entry.slug : undefined;
|
|
1666
1759
|
if (!slug || isRoutedCatalogEntry(entry) || !existing.has(slug)) continue;
|
|
1667
|
-
|
|
1760
|
+
const saved = existing.get(slug)!;
|
|
1761
|
+
entry.auto_review_model_override = saved.value;
|
|
1762
|
+
if (saved.root) entry[AUTO_REVIEW_ROOT_MARKER] = structuredClone(saved.root);
|
|
1763
|
+
else delete entry[AUTO_REVIEW_ROOT_MARKER];
|
|
1668
1764
|
}
|
|
1669
1765
|
}
|
|
1670
1766
|
|
|
1767
|
+
/** Stamp a root-derived override and mark native rows so later root removal is durable. */
|
|
1768
|
+
function stampRootAutoReviewOverride(entry: RawEntry, target: string): void {
|
|
1769
|
+
if (!isRoutedCatalogEntry(entry)) {
|
|
1770
|
+
const previous = rootAutoReviewStamp(entry);
|
|
1771
|
+
const current = entry.auto_review_model_override;
|
|
1772
|
+
entry[AUTO_REVIEW_ROOT_MARKER] = {
|
|
1773
|
+
slug: typeof entry.slug === "string" ? entry.slug : "",
|
|
1774
|
+
original: previous && current === previous.applied
|
|
1775
|
+
? previous.original : typeof current === "string" ? current : null,
|
|
1776
|
+
applied: target,
|
|
1777
|
+
} satisfies RootAutoReviewStamp;
|
|
1778
|
+
} else {
|
|
1779
|
+
delete entry[AUTO_REVIEW_ROOT_MARKER];
|
|
1780
|
+
}
|
|
1781
|
+
entry.auto_review_model_override = target;
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
/** Stamp a provider-derived override; provider stamps never fall under root removal. */
|
|
1785
|
+
function stampProviderAutoReviewOverride(entry: RawEntry, target: string): void {
|
|
1786
|
+
entry.auto_review_model_override = target;
|
|
1787
|
+
delete entry[AUTO_REVIEW_ROOT_MARKER];
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1790
|
+
/**
|
|
1791
|
+
* Apply the root Codex auto-review selector to every catalog row, or clear it when the value is
|
|
1792
|
+
* absent, blank, malformed, or does not resolve against the assembled catalog.
|
|
1793
|
+
*/
|
|
1671
1794
|
export function applyAutoReviewModelOverride(
|
|
1672
1795
|
models: RawEntry[] | undefined,
|
|
1673
1796
|
autoReviewModel: string | null | undefined,
|
|
@@ -1695,21 +1818,254 @@ export function applyAutoReviewModelOverride(
|
|
|
1695
1818
|
}
|
|
1696
1819
|
for (const entry of models) {
|
|
1697
1820
|
if (entry && typeof entry === "object") {
|
|
1698
|
-
entry
|
|
1821
|
+
stampRootAutoReviewOverride(entry, trimmed);
|
|
1699
1822
|
}
|
|
1700
1823
|
}
|
|
1701
1824
|
return "applied";
|
|
1702
1825
|
}
|
|
1703
1826
|
|
|
1827
|
+
/** Validated provider-scoped target with both the configured spelling and catalog slug. */
|
|
1828
|
+
interface ValidProviderReviewTarget {
|
|
1829
|
+
configured: string;
|
|
1830
|
+
target: string;
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1833
|
+
/** One provider's resolved provider-wide and per-model auto-review targets. */
|
|
1834
|
+
interface ProviderReviewPlan {
|
|
1835
|
+
wide?: ValidProviderReviewTarget;
|
|
1836
|
+
perModel: Map<string, ValidProviderReviewTarget>;
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
/** Public provider namespace of a routed catalog row, when it has one. */
|
|
1840
|
+
function catalogEntryProviderName(entry: RawEntry): string | undefined {
|
|
1841
|
+
const slug = typeof entry.slug === "string" ? entry.slug : "";
|
|
1842
|
+
const slash = slug.indexOf("/");
|
|
1843
|
+
return slash > 0 && isRoutedCatalogEntry(entry) ? slug.slice(0, slash) : undefined;
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1846
|
+
/** Encoded model-id segment of a routed catalog row, when it has one. */
|
|
1847
|
+
function catalogEntryModelSegment(entry: RawEntry): string | undefined {
|
|
1848
|
+
const slug = typeof entry.slug === "string" ? entry.slug : "";
|
|
1849
|
+
const slash = slug.indexOf("/");
|
|
1850
|
+
return slash > 0 ? slug.slice(slash + 1) : undefined;
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1853
|
+
/** Case-preserving encoded key used to match per-model override maps. */
|
|
1854
|
+
function providerModelKey(modelId: string): string {
|
|
1855
|
+
return canonicalAutoReviewModelKey(modelId);
|
|
1856
|
+
}
|
|
1857
|
+
|
|
1858
|
+
/**
|
|
1859
|
+
* True when another routed row of this provider already carries `alias` as its own model id.
|
|
1860
|
+
*
|
|
1861
|
+
* The alias API validates against whatever ids discovery has reported so far, so on a cold start an
|
|
1862
|
+
* alias can be persisted that later turns out to name a different row. A key using it is then not
|
|
1863
|
+
* an alternate spelling of the aliased model — it is that row's id — and must not be propagated.
|
|
1864
|
+
*/
|
|
1865
|
+
function aliasNamesAnotherRoutedRow(models: readonly RawEntry[], provider: string, alias: string): boolean {
|
|
1866
|
+
const encoded = encodeRoutedModelId(alias);
|
|
1867
|
+
return models.some(entry => isRoutedCatalogEntry(entry)
|
|
1868
|
+
&& catalogEntryProviderName(entry) === provider
|
|
1869
|
+
&& catalogEntryModelSegment(entry) === encoded);
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
/** Resolve one configured target against the assembled catalog; bare values name a model of the same provider. */
|
|
1873
|
+
function resolveProviderReviewTarget(
|
|
1874
|
+
models: readonly RawEntry[],
|
|
1875
|
+
provider: string,
|
|
1876
|
+
configuredRaw: unknown,
|
|
1877
|
+
): { kind: "valid"; value: ValidProviderReviewTarget; foreign?: boolean } | { kind: "invalid"; configured: string } | { kind: "unresolved"; configured: string } | { kind: "absent" } {
|
|
1878
|
+
if (typeof configuredRaw !== "string") return { kind: "absent" };
|
|
1879
|
+
const configured = configuredRaw.trim();
|
|
1880
|
+
if (!configured) return { kind: "absent" };
|
|
1881
|
+
if (!isValidAutoReviewModel(configured)) return { kind: "invalid", configured };
|
|
1882
|
+
const prefix = `${provider}/`;
|
|
1883
|
+
let match: RawEntry | undefined;
|
|
1884
|
+
const sameProviderCandidate = (rawModelId: string): RawEntry | undefined => models.find(entry => {
|
|
1885
|
+
if (!isRoutedCatalogEntry(entry) || typeof entry.slug !== "string" || !entry.slug.startsWith(prefix)) return false;
|
|
1886
|
+
const segment = catalogEntryModelSegment(entry);
|
|
1887
|
+
return segment !== undefined && segment === encodeRoutedModelId(rawModelId);
|
|
1888
|
+
});
|
|
1889
|
+
// A bare selector names a model of this provider. A full selector that resolves in the
|
|
1890
|
+
// assembled catalog already names the exact row, including a same-provider encoded slug.
|
|
1891
|
+
if (!configured.includes("/")) {
|
|
1892
|
+
match = sameProviderCandidate(configured);
|
|
1893
|
+
}
|
|
1894
|
+
match ??= configuredCatalogEntry(models, configured);
|
|
1895
|
+
if (!match && configured.startsWith(prefix)) {
|
|
1896
|
+
match = sameProviderCandidate(configured.slice(prefix.length));
|
|
1897
|
+
}
|
|
1898
|
+
if (!match) {
|
|
1899
|
+
// A raw model id may itself contain "/" (for example zenmux moonshotai/kimi-k3).
|
|
1900
|
+
// After the full-selector lookup misses, try that spelling as a same-provider id.
|
|
1901
|
+
match = sameProviderCandidate(configured);
|
|
1902
|
+
}
|
|
1903
|
+
if (!match) return { kind: "unresolved", configured };
|
|
1904
|
+
const target = typeof match.slug === "string" ? match.slug : configured;
|
|
1905
|
+
// A qualified selector may name another provider's row on purpose; only a bare value that lands
|
|
1906
|
+
// outside this provider is worth reporting.
|
|
1907
|
+
const foreign = !configured.includes("/") && catalogEntryProviderName(match) !== provider;
|
|
1908
|
+
return { kind: "valid", value: { configured, target }, ...(foreign ? { foreign: true } : {}) };
|
|
1909
|
+
}
|
|
1910
|
+
|
|
1911
|
+
/** Build resolved per-provider plans and emit one diagnostic per bad selector. */
|
|
1912
|
+
function buildProviderReviewPlans(
|
|
1913
|
+
models: readonly RawEntry[],
|
|
1914
|
+
config: Pick<OcxConfig, "providers">,
|
|
1915
|
+
): { plans: Map<string, ProviderReviewPlan>; failure?: "invalid" | "unresolved" } {
|
|
1916
|
+
const plans = new Map<string, ProviderReviewPlan>();
|
|
1917
|
+
let failure: "invalid" | "unresolved" | undefined;
|
|
1918
|
+
const warned = new Set<string>();
|
|
1919
|
+
const recordFailure = (kind: "invalid" | "unresolved", provider: string, configured: string): void => {
|
|
1920
|
+
const signature = `${provider}\u0000${configured}`;
|
|
1921
|
+
if (warned.has(signature)) return;
|
|
1922
|
+
warned.add(signature);
|
|
1923
|
+
warnProviderAutoReviewModelDiagnostic(kind, provider, configured);
|
|
1924
|
+
failure ??= kind;
|
|
1925
|
+
};
|
|
1926
|
+
const recordForeignTarget = (provider: string, configured: string, target: string): void => {
|
|
1927
|
+
const signature = `${provider}\u0000foreign\u0000${configured}`;
|
|
1928
|
+
if (warned.has(signature)) return;
|
|
1929
|
+
warned.add(signature);
|
|
1930
|
+
warnProviderAutoReviewForeignTarget(provider, configured, target);
|
|
1931
|
+
};
|
|
1932
|
+
for (const [name, provider] of Object.entries(config.providers ?? {})) {
|
|
1933
|
+
if (provider.autoReviewModel === undefined && provider.autoReviewModelOverrides === undefined) continue;
|
|
1934
|
+
const plan: ProviderReviewPlan = { perModel: new Map() };
|
|
1935
|
+
if (provider.autoReviewModel !== undefined) {
|
|
1936
|
+
const resolved = resolveProviderReviewTarget(models, name, provider.autoReviewModel);
|
|
1937
|
+
if (resolved.kind === "valid") {
|
|
1938
|
+
plan.wide = resolved.value;
|
|
1939
|
+
if (resolved.foreign) recordForeignTarget(name, resolved.value.configured, resolved.value.target);
|
|
1940
|
+
}
|
|
1941
|
+
else if (resolved.kind !== "absent") recordFailure(resolved.kind, name, resolved.configured);
|
|
1942
|
+
}
|
|
1943
|
+
if (provider.autoReviewModelOverrides !== undefined) {
|
|
1944
|
+
for (const [modelId, rawTarget] of Object.entries(provider.autoReviewModelOverrides)) {
|
|
1945
|
+
const resolved = resolveProviderReviewTarget(models, name, rawTarget);
|
|
1946
|
+
if (resolved.kind === "valid") {
|
|
1947
|
+
plan.perModel.set(providerModelKey(modelId), resolved.value);
|
|
1948
|
+
if (resolved.foreign) recordForeignTarget(name, resolved.value.configured, resolved.value.target);
|
|
1949
|
+
} else if (resolved.kind !== "absent") {
|
|
1950
|
+
recordFailure(resolved.kind, name, resolved.configured);
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1953
|
+
}
|
|
1954
|
+
// `modelAliases` publishes a second public name for a model id, and a routed row's slug always
|
|
1955
|
+
// carries the upstream id — so accept an override key written in either spelling.
|
|
1956
|
+
for (const [modelId, alias] of Object.entries(provider.modelAliases ?? {})) {
|
|
1957
|
+
if (typeof alias !== "string" || !alias.trim()) continue;
|
|
1958
|
+
if (aliasNamesAnotherRoutedRow(models, name, alias)) continue;
|
|
1959
|
+
const idKey = providerModelKey(modelId);
|
|
1960
|
+
const aliasKey = providerModelKey(alias);
|
|
1961
|
+
if (idKey === aliasKey) continue;
|
|
1962
|
+
const fromId = plan.perModel.get(idKey);
|
|
1963
|
+
const fromAlias = plan.perModel.get(aliasKey);
|
|
1964
|
+
if (fromId !== undefined && fromAlias === undefined) plan.perModel.set(aliasKey, fromId);
|
|
1965
|
+
else if (fromAlias !== undefined && fromId === undefined) plan.perModel.set(idKey, fromAlias);
|
|
1966
|
+
}
|
|
1967
|
+
if (plan.wide !== undefined || plan.perModel.size > 0) plans.set(name, plan);
|
|
1968
|
+
}
|
|
1969
|
+
return { plans, failure };
|
|
1970
|
+
}
|
|
1971
|
+
|
|
1972
|
+
/** Apply or clear the root selector only on rows without a provider stamp. */
|
|
1973
|
+
function applyRootSelectorToRemaining(
|
|
1974
|
+
models: readonly RawEntry[],
|
|
1975
|
+
rootValue: string | null | undefined,
|
|
1976
|
+
providerStamped: ReadonlySet<RawEntry>,
|
|
1977
|
+
): AutoReviewModelOverrideResult {
|
|
1978
|
+
const clearRemaining = (): void => {
|
|
1979
|
+
for (const entry of models) {
|
|
1980
|
+
if (!entry || providerStamped.has(entry)) continue;
|
|
1981
|
+
// Native rows written by releases before the root marker cannot be told apart from upstream
|
|
1982
|
+
// values once provider stamps diverge. clearLegacyRootStamps sweeps the ones the legacy
|
|
1983
|
+
// uniform signature still recognizes before provider plans land, because provider stamping
|
|
1984
|
+
// destroys that signature; a catalog that no longer matches it needs a one-off manual sync.
|
|
1985
|
+
if (isRoutedCatalogEntry(entry) || entry[AUTO_REVIEW_ROOT_MARKER] === true || rootAutoReviewStamp(entry)) clearAutoReviewOverrideValue(entry);
|
|
1986
|
+
}
|
|
1987
|
+
};
|
|
1988
|
+
if (rootValue === null || rootValue === undefined) {
|
|
1989
|
+
clearRemaining();
|
|
1990
|
+
return "absent";
|
|
1991
|
+
}
|
|
1992
|
+
const trimmed = rootValue.trim();
|
|
1993
|
+
if (!trimmed) {
|
|
1994
|
+
clearRemaining();
|
|
1995
|
+
return "absent";
|
|
1996
|
+
}
|
|
1997
|
+
if (!isValidAutoReviewModel(trimmed)) {
|
|
1998
|
+
clearRemaining();
|
|
1999
|
+
warnAutoReviewModelDiagnostic("invalid", trimmed);
|
|
2000
|
+
return "invalid";
|
|
2001
|
+
}
|
|
2002
|
+
if (!configuredCatalogEntry(models, trimmed)) {
|
|
2003
|
+
clearRemaining();
|
|
2004
|
+
warnAutoReviewModelDiagnostic("unresolved", trimmed);
|
|
2005
|
+
return "unresolved";
|
|
2006
|
+
}
|
|
2007
|
+
for (const entry of models) {
|
|
2008
|
+
if (!entry || providerStamped.has(entry)) continue;
|
|
2009
|
+
stampRootAutoReviewOverride(entry, trimmed);
|
|
2010
|
+
}
|
|
2011
|
+
return "applied";
|
|
2012
|
+
}
|
|
2013
|
+
|
|
2014
|
+
/** Provider-aware variant: provider rows win and the root selector is the fallback. */
|
|
2015
|
+
export function applyConfiguredAutoReviewModelOverride(
|
|
2016
|
+
models: RawEntry[] | undefined,
|
|
2017
|
+
rootAutoReviewModel: string | null | undefined,
|
|
2018
|
+
config: Pick<OcxConfig, "providers">,
|
|
2019
|
+
sourceModels: readonly RawEntry[] = [],
|
|
2020
|
+
): AutoReviewModelOverrideResult {
|
|
2021
|
+
if (!models || !Array.isArray(models)) return "absent";
|
|
2022
|
+
// Runs unconditionally because the sweep only fires on the uniform legacy signature. A resolved
|
|
2023
|
+
// root selector restamps every row it touches below, so the call is behavior-preserving there;
|
|
2024
|
+
// with the root absent, invalid, or unresolved those clears are final — which is the point, and
|
|
2025
|
+
// also the limit: the legacy heuristic cannot tell a root stamp from an identical upstream value.
|
|
2026
|
+
clearLegacyRootStamps(models, sourceModels);
|
|
2027
|
+
const { plans, failure } = buildProviderReviewPlans(models, config);
|
|
2028
|
+
const providerStamped = new Set<RawEntry>();
|
|
2029
|
+
for (const entry of models) {
|
|
2030
|
+
if (!entry || typeof entry !== "object") continue;
|
|
2031
|
+
const provider = catalogEntryProviderName(entry);
|
|
2032
|
+
if (!provider) continue;
|
|
2033
|
+
const plan = plans.get(provider);
|
|
2034
|
+
if (!plan) continue;
|
|
2035
|
+
const modelSegment = catalogEntryModelSegment(entry);
|
|
2036
|
+
const perModel = modelSegment === undefined ? undefined : plan.perModel.get(providerModelKey(modelSegment));
|
|
2037
|
+
const selected = perModel ?? plan.wide;
|
|
2038
|
+
if (!selected) continue;
|
|
2039
|
+
stampProviderAutoReviewOverride(entry, selected.target);
|
|
2040
|
+
providerStamped.add(entry);
|
|
2041
|
+
}
|
|
2042
|
+
const rootResult = applyRootSelectorToRemaining(models, rootAutoReviewModel, providerStamped);
|
|
2043
|
+
const providerApplied = [...providerStamped].some(entry => typeof entry.auto_review_model_override === "string");
|
|
2044
|
+
if (providerApplied) {
|
|
2045
|
+
if (rootResult === "invalid" || rootResult === "unresolved") return rootResult;
|
|
2046
|
+
return failure ?? "applied";
|
|
2047
|
+
}
|
|
2048
|
+
return failure ?? rootResult;
|
|
2049
|
+
}
|
|
2050
|
+
|
|
2051
|
+
/** True when any provider row configures a provider-scoped auto-review selector. */
|
|
2052
|
+
function configHasProviderAutoReview(config: Pick<OcxConfig, "providers">): boolean {
|
|
2053
|
+
return Object.values(config.providers ?? {}).some(provider =>
|
|
2054
|
+
provider.autoReviewModel !== undefined || provider.autoReviewModelOverrides !== undefined);
|
|
2055
|
+
}
|
|
2056
|
+
|
|
1704
2057
|
/** Apply the root Codex auto-review selector after the final catalog merge. */
|
|
1705
2058
|
export function finalizeAutoReviewModelOverride(
|
|
1706
2059
|
models: RawEntry[] | undefined,
|
|
1707
2060
|
sourceModels: readonly RawEntry[] = [],
|
|
2061
|
+
config?: Pick<OcxConfig, "providers">,
|
|
1708
2062
|
): AutoReviewModelOverrideResult {
|
|
1709
2063
|
if (models && sourceModels.length > 0) preserveNativeAutoReviewModelOverrides(models, sourceModels);
|
|
2064
|
+
if (config && configHasProviderAutoReview(config)) {
|
|
2065
|
+
return applyConfiguredAutoReviewModelOverride(models, readConfiguredAutoReviewModel(), config, sourceModels);
|
|
2066
|
+
}
|
|
1710
2067
|
return applyAutoReviewModelOverride(models, readConfiguredAutoReviewModel(), sourceModels);
|
|
1711
2068
|
}
|
|
1712
|
-
|
|
1713
2069
|
/**
|
|
1714
2070
|
* Why an account-gated native model stopped being offered, but only when the answer is one the
|
|
1715
2071
|
* operator can act on.
|
|
@@ -2028,7 +2384,7 @@ function writeRetainedCatalogSync({
|
|
|
2028
2384
|
},
|
|
2029
2385
|
});
|
|
2030
2386
|
clampCatalogModelsToCodexSupport(catalog.models);
|
|
2031
|
-
finalizeAutoReviewModelOverride(catalog.models, catalogModelsForMerge);
|
|
2387
|
+
finalizeAutoReviewModelOverride(catalog.models, catalogModelsForMerge, config);
|
|
2032
2388
|
|
|
2033
2389
|
const added = goEntries.length + accountBoundEntries.length;
|
|
2034
2390
|
const content = `${JSON.stringify(catalog, null, 2)}\n`;
|
|
@@ -2124,6 +2480,7 @@ export async function syncCatalogModels(
|
|
|
2124
2480
|
path: readCodexCatalogPath(),
|
|
2125
2481
|
catalogWritten: false,
|
|
2126
2482
|
comboOmissions: [],
|
|
2483
|
+
refreshOutcome: "refused",
|
|
2127
2484
|
};
|
|
2128
2485
|
}
|
|
2129
2486
|
|
|
@@ -2180,12 +2537,18 @@ export async function syncCatalogModels(
|
|
|
2180
2537
|
modelEntitlements,
|
|
2181
2538
|
});
|
|
2182
2539
|
});
|
|
2183
|
-
if (committed.kind === "completed" && committed.value !== null)
|
|
2540
|
+
if (committed.kind === "completed" && committed.value !== null) {
|
|
2541
|
+
return {
|
|
2542
|
+
...committed.value,
|
|
2543
|
+
refreshOutcome: committed.value.skippedReason ? "refused" : "committed",
|
|
2544
|
+
};
|
|
2545
|
+
}
|
|
2184
2546
|
return {
|
|
2185
2547
|
added: 0,
|
|
2186
2548
|
path: prepared.catalogPath,
|
|
2187
2549
|
catalogWritten: false,
|
|
2188
2550
|
comboOmissions,
|
|
2551
|
+
refreshOutcome: "refused",
|
|
2189
2552
|
};
|
|
2190
2553
|
}
|
|
2191
2554
|
|
|
@@ -2208,18 +2571,22 @@ export function restoreCodexCatalogWithPermit(
|
|
|
2208
2571
|
const replacementVisibility = visibleAccountReplacementNatives(catalog.models, disabledModels);
|
|
2209
2572
|
const backup = readCatalogBackup(catalogPath);
|
|
2210
2573
|
if (backup && Array.isArray(backup.models)) {
|
|
2211
|
-
const removed = (catalog.models ?? []).filter(m => typeof m.slug === "string"
|
|
2574
|
+
const removed = (catalog.models ?? []).filter(m => typeof m.slug === "string"
|
|
2575
|
+
&& (m.slug.includes("/") || RETIRED_NATIVE_OPENAI_MODELS.has(m.slug))).length;
|
|
2212
2576
|
const backupSlugs = new Set(backup.models.flatMap(m => typeof m.slug === "string" ? [m.slug] : []));
|
|
2213
2577
|
const userNativeAdditions = restoreAccountHiddenBareNatives(
|
|
2214
2578
|
(catalog.models ?? []).filter(m =>
|
|
2215
2579
|
typeof m.slug === "string" && !m.slug.includes("/") && !backupSlugs.has(m.slug)
|
|
2580
|
+
&& !RETIRED_NATIVE_OPENAI_MODELS.has(m.slug)
|
|
2216
2581
|
),
|
|
2217
2582
|
replacementVisibility,
|
|
2218
2583
|
disabledModels,
|
|
2219
2584
|
);
|
|
2220
2585
|
const restored = {
|
|
2221
2586
|
...backup,
|
|
2222
|
-
|
|
2587
|
+
// A pristine backup predates retirement; it must not revive withdrawn native rows.
|
|
2588
|
+
models: [...backup.models.filter(m => typeof m.slug !== "string"
|
|
2589
|
+
|| !RETIRED_NATIVE_OPENAI_MODELS.has(trustedAccountBoundNativeCatalogSlug(m) ?? m.slug)), ...userNativeAdditions],
|
|
2223
2590
|
};
|
|
2224
2591
|
replaceActiveCodexCatalog(permit, owningCodexHome, {
|
|
2225
2592
|
path: catalogPath,
|
|
@@ -2229,7 +2596,8 @@ export function restoreCodexCatalogWithPermit(
|
|
|
2229
2596
|
}
|
|
2230
2597
|
const before = catalog.models.length;
|
|
2231
2598
|
const native = restoreAccountHiddenBareNatives(
|
|
2232
|
-
catalog.models.filter(m => !(typeof m.slug === "string"
|
|
2599
|
+
catalog.models.filter(m => !(typeof m.slug === "string"
|
|
2600
|
+
&& (m.slug.includes("/") || RETIRED_NATIVE_OPENAI_MODELS.has(m.slug)))),
|
|
2233
2601
|
replacementVisibility,
|
|
2234
2602
|
disabledModels,
|
|
2235
2603
|
);
|
package/src/codex/convergence.ts
CHANGED
|
@@ -385,7 +385,7 @@ function prepareCatalog(
|
|
|
385
385
|
? supportedCodexReasoningEffortsFromObservedCatalog(source.runtimeSupport.catalog)
|
|
386
386
|
: null,
|
|
387
387
|
);
|
|
388
|
-
finalizeAutoReviewModelOverride(mergedModels, catalogModels);
|
|
388
|
+
finalizeAutoReviewModelOverride(mergedModels, catalogModels, config);
|
|
389
389
|
catalog.models = mergedModels;
|
|
390
390
|
return catalog;
|
|
391
391
|
}
|
|
@@ -54,7 +54,32 @@ export type CodexHistoryManifestValidation =
|
|
|
54
54
|
readonly scope: "manifest" | "entry-shape" | "entry-provenance";
|
|
55
55
|
};
|
|
56
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Strip the Win32 extended-length prefix: `\\?\C:\...` becomes `C:\...` and
|
|
59
|
+
* `\\?\UNC\server\share\...` becomes `\\server\share\...` (forward-slash spellings
|
|
60
|
+
* included). Codex records some rollout paths with the prefix and others without, and
|
|
61
|
+
* both spellings name the same file — comparing them literally failed the history
|
|
62
|
+
* integrity check for intact sessions (#4442). Stripping happens before resolve() so
|
|
63
|
+
* the identity converges regardless of host path semantics.
|
|
64
|
+
*/
|
|
65
|
+
function withoutWindowsExtendedLengthPrefix(path: string): string {
|
|
66
|
+
const match = /^(?:[\\/]{2}\?[\\/])(unc[\\/])?/i.exec(path);
|
|
67
|
+
if (!match) return path;
|
|
68
|
+
return match[1] ? `\\\\${path.slice(match[0].length)}` : path.slice(match[0].length);
|
|
69
|
+
}
|
|
70
|
+
|
|
57
71
|
function codexHistoryPathIdentity(path: string): string {
|
|
72
|
+
if (process.platform !== "win32") return resolve(path);
|
|
73
|
+
return resolve(withoutWindowsExtendedLengthPrefix(path)).toLowerCase();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Identity as computed before the extended-length prefix was normalized (#4442). A
|
|
78
|
+
* state database path spelled `\\?\C:\...` hashed to a DIFFERENT backup filename
|
|
79
|
+
* before the fix; readers still check that name so an existing manifest keeps
|
|
80
|
+
* shadowing its database instead of reading as absent.
|
|
81
|
+
*/
|
|
82
|
+
function legacyCodexHistoryPathIdentity(path: string): string {
|
|
58
83
|
const canonical = resolve(path);
|
|
59
84
|
return process.platform === "win32" ? canonical.toLowerCase() : canonical;
|
|
60
85
|
}
|
|
@@ -76,6 +101,17 @@ export function codexHistoryBackupId(stateDbPath: string): string {
|
|
|
76
101
|
.slice(0, 16);
|
|
77
102
|
}
|
|
78
103
|
|
|
104
|
+
/**
|
|
105
|
+
* Backup filename id under the pre-#4442 identity. Differs from codexHistoryBackupId
|
|
106
|
+
* only for extended-length-prefixed database paths; identical everywhere else.
|
|
107
|
+
*/
|
|
108
|
+
export function legacyCodexHistoryBackupId(stateDbPath: string): string {
|
|
109
|
+
return createHash("sha256")
|
|
110
|
+
.update(legacyCodexHistoryPathIdentity(stateDbPath))
|
|
111
|
+
.digest("hex")
|
|
112
|
+
.slice(0, 16);
|
|
113
|
+
}
|
|
114
|
+
|
|
79
115
|
/** Platform-aware identity comparison shared by database and rollout checks. */
|
|
80
116
|
export function sameCodexHistoryPath(left: string, right: string): boolean {
|
|
81
117
|
return codexHistoryPathIdentity(left) === codexHistoryPathIdentity(right);
|