-
Notifications
You must be signed in to change notification settings - Fork 760
fix(codex): preserve account-scoped native model ids #1515
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,6 +4,7 @@ import { copyFileSync, existsSync, mkdirSync, readFileSync, realpathSync } from | |
| import { delimiter, dirname, join, resolve } from "node:path"; | ||
| import { atomicWriteFile, expandUserPath, getConfigDir, websocketsEnabled } from "../../config"; | ||
| import { CODEX_CONFIG_PATH, CODEX_MODELS_CACHE_PATH, DEFAULT_CATALOG_PATH, readRootTomlString, resolveCodexConfigPath } from "../paths"; | ||
| import { codexAccountNamespaceEntries, isMainCodexAccountTarget } from "../account-namespaces"; | ||
| import { clearModelCache, DEFAULT_MODEL_CACHE_TTL_MS, getFreshCached, getStaleCached, isModelsFetchCoolingDown, markModelsFetchFailure, setCached } from "../model-cache"; | ||
| import { buildModelsRequest, resolveModelsAuthToken } from "../../oauth"; | ||
| import type { OcxConfig, OcxProviderConfig } from "../../types"; | ||
|
|
@@ -33,8 +34,8 @@ import upstreamModelsSnapshot from "../data/upstream-models.json"; | |
|
|
||
|
|
||
| import type { RawEntry } from "./parsing"; | ||
| import { readCurrentCatalogOrCache, unique } from "./bundled"; | ||
| import { trustedAccountBoundNativeCatalogSlug } from "./account-models"; | ||
| import { readCurrentCatalogOrCache, readCurrentCodexCatalog, readCurrentCodexModelsCache, unique } from "./bundled"; | ||
| import { trustedAccountBoundNativeCatalogSlug, visibleCodexAccountSelectors } from "./account-models"; | ||
| import { CODEX_NATIVE_ALIAS_CATALOG_KIND } from "./kinds"; | ||
| import { NATIVE_OPENAI_MODELS, SUPPORTED_NATIVE_OPENAI_SLUGS } from "./native-models"; | ||
| export { CODEX_NATIVE_ALIAS_CATALOG_KIND } from "./kinds"; | ||
|
|
@@ -194,10 +195,35 @@ export function shouldIncludeNativeOpenAi(config: Pick<OcxConfig, "providers">): | |
| return !hasEnabledProvider || shouldIncludeAccountBoundNativeOpenAi(config); | ||
| } | ||
|
|
||
| type AccountSelectorConfig = Pick< | ||
| OcxConfig, | ||
| "codexAccounts" | "codexAccountNamespaces" | "codexAccountPickerEnabled" | ||
| >; | ||
|
|
||
| function mainAccountSelectors(config: AccountSelectorConfig): string[] { | ||
| const targets = new Map(codexAccountNamespaceEntries(config)); | ||
| return visibleCodexAccountSelectors(config).filter(selector => | ||
| isMainCodexAccountTarget(targets.get(selector) ?? "")); | ||
| } | ||
|
|
||
| /** Native slugs exposed to Claude Desktop show/export/apply (opt-out via claudeCode.desktopNativeModels). */ | ||
| export function desktopVisibleNativeSlugs(config: Pick<OcxConfig, "claudeCode" | "disabledModels" | "combos">): string[] { | ||
| export function desktopVisibleNativeSlugs( | ||
| config: Pick<OcxConfig, "claudeCode" | "disabledModels" | "combos" | "providers" | ||
| | "codexAccounts" | "codexAccountNamespaces" | "codexAccountPickerEnabled">, | ||
| ): string[] { | ||
| if (config.claudeCode?.desktopNativeModels === false) return []; | ||
| return visibleNativeSlugs(config); | ||
| const visible = visibleNativeSlugs(config); | ||
| if (!shouldIncludeAccountBoundNativeOpenAi(config)) return visible; | ||
| const qualified = [...accountBoundNativeOpenAiSlugsBySelector(config).entries()].flatMap(([selector, slugs]) => | ||
| slugs | ||
| .filter(slug => !SUPPORTED_NATIVE_OPENAI_SLUGS.has(slug)) | ||
| .map(slug => `${selector}/${slug}`), | ||
| ); | ||
| const disabled = new Set(config.disabledModels ?? []); | ||
| return unique([ | ||
| ...visible, | ||
| ...qualified.filter(slug => !disabled.has(slug) && !disabled.has(slug.slice(slug.indexOf("/") + 1))), | ||
| ]); | ||
| } | ||
|
|
||
| export function nativeModelRows(config: Pick<OcxConfig, "disabledModels" | "combos" | "providerContextCaps">): Array<{ slug: string; disabled: boolean; contextWindow?: number }> { | ||
|
|
@@ -214,6 +240,7 @@ export function applyNativeVisibility( | |
| entries: RawEntry[], | ||
| disabledModels: ReadonlySet<string>, | ||
| hideBareNative = false, | ||
| observedNativeSlugs: ReadonlySet<string> = new Set(), | ||
| ): RawEntry[] { | ||
| for (const entry of entries) { | ||
| if (isNativeAliasCatalogEntry(entry)) continue; | ||
|
|
@@ -222,7 +249,7 @@ export function applyNativeVisibility( | |
| const nativeSlug = accountBoundSlug ?? slug; | ||
| if (!nativeSlug | ||
| || (!accountBoundSlug && slug.includes("/")) | ||
| || !SUPPORTED_NATIVE_OPENAI_SLUGS.has(nativeSlug)) continue; | ||
| || (!SUPPORTED_NATIVE_OPENAI_SLUGS.has(nativeSlug) && !observedNativeSlugs.has(nativeSlug))) continue; | ||
| const disabled = disabledModels.has(nativeSlug) | ||
| || (accountBoundSlug !== undefined && disabledModels.has(slug)); | ||
| entry.visibility = disabled || (!accountBoundSlug && hideBareNative) | ||
|
|
@@ -259,6 +286,154 @@ export function nativeOpenAiSlugs(): string[] { | |
| return live.length > 0 ? unique([...live, ...DOCUMENTED_NATIVE_OPENAI_ADDITIONS]) : NATIVE_OPENAI_MODELS; | ||
| } | ||
|
|
||
| const ACCOUNT_BOUND_OPENAI_NATIVE_PREFIX = /^(?:gpt-|o1-|o3-|o4-)/; | ||
| const ACCOUNT_BOUND_OBSERVED_NATIVE_MARKER = "opencodex_account_observed_native"; | ||
| const ACCOUNT_BOUND_OBSERVED_SELECTORS_MARKER = "opencodex_account_observed_selectors"; | ||
|
|
||
| function isAccountBoundOpenAiNativeSlug(slug: string): boolean { | ||
| return !slug.includes("/") && ACCOUNT_BOUND_OPENAI_NATIVE_PREFIX.test(slug); | ||
| } | ||
|
|
||
| /** | ||
| * Shape/plausibility filter for a candidate account-native row. **This is not a trust control.** | ||
| * | ||
| * It checks that a row carries the field shape a real Codex catalog row has, which rejects | ||
| * malformed and minimal hand-written rows. It cannot distinguish a genuine upstream observation | ||
| * from a complete row typed by hand into `$CODEX_HOME/models_cache.json`: there is no signature, | ||
| * source identity, or server attestation to check. A full-shape forged row is accepted, and | ||
| * `observedFullShapeRowIsAccepted` in tests/native-model-toggle.test.ts pins that so nobody | ||
| * later mistakes this predicate for a security boundary. | ||
| * | ||
| * That is acceptable here because the file is user-owned and written by Codex itself: anyone | ||
| * able to rewrite it can already edit `config.json` or run `ocx` directly, and `router.ts` | ||
| * accepts any bare `gpt-*` id under an account namespace regardless of this catalog. What the | ||
| * filter buys is that garbage rows do not get advertised through discovery — not that an | ||
| * advertised row is proven genuine. | ||
| */ | ||
| function hasNativeCatalogRowShape(entry: RawEntry): boolean { | ||
| const levels = entry.supported_reasoning_levels; | ||
| const messages = entry.model_messages; | ||
| return typeof entry.base_instructions === "string" | ||
| && entry.base_instructions.length > 0 | ||
| && (typeof entry.comp_hash === "string" || entry.comp_hash === null) | ||
| && entry.shell_type === "shell_command" | ||
| && Array.isArray(levels) | ||
| && levels.length > 0 | ||
| && levels.every(level => typeof level === "object" && level !== null | ||
| && typeof (level as { effort?: unknown }).effort === "string") | ||
| && typeof messages === "object" | ||
| && messages !== null | ||
| && !Array.isArray(messages); | ||
| } | ||
|
|
||
| function observedAccountBoundNativeSlug(entry: RawEntry): string | undefined { | ||
| const accountBound = trustedAccountBoundNativeCatalogSlug(entry); | ||
| const slug = accountBound ?? (typeof entry.slug === "string" ? entry.slug : ""); | ||
| if (!isAccountBoundOpenAiNativeSlug(slug) | ||
| || entry.supported_in_api !== true | ||
| || !hasNativeCatalogRowShape(entry) | ||
| || (entry.visibility !== "list" && entry[ACCOUNT_BOUND_OBSERVED_NATIVE_MARKER] !== true)) { | ||
| return undefined; | ||
| } | ||
| return slug; | ||
| } | ||
|
|
||
| /** | ||
| * Return exact, previously observed account-native rows that are not in the static release set. | ||
| * The result is used only to carry a hidden observation across startup cache invalidation. | ||
| */ | ||
| export function observedAccountBoundNativeEntries( | ||
| observedEntries: readonly RawEntry[], | ||
| ): RawEntry[] { | ||
| const seen = new Set<string>(); | ||
| return observedEntries.flatMap(entry => { | ||
| const slug = observedAccountBoundNativeSlug(entry); | ||
| // Only carry bare upstream observations across cache replacement. Account-qualified rows are | ||
| // already a projection of the current selector map and must not preserve private/stale labels. | ||
| if (!slug | ||
| || typeof entry.slug !== "string" | ||
| || entry.slug.includes("/") | ||
| || SUPPORTED_NATIVE_OPENAI_SLUGS.has(slug) | ||
| || seen.has(slug)) return []; | ||
| seen.add(slug); | ||
| return [structuredClone(entry)]; | ||
| }); | ||
| } | ||
|
|
||
| /** | ||
| * Native ids observed in the user's Codex catalog/cache for account-qualified discovery. | ||
| * | ||
| * Unknown ids are deliberately returned only to callers that build selector-qualified rows. The | ||
| * static bare set remains the source of truth for global/API-key discovery, while this preserves | ||
| * exact account-scoped ids such as `gpt-daybreak-blue-latest` until the static set catches up. | ||
| */ | ||
| export function accountBoundNativeOpenAiSlugs( | ||
| observedEntries: readonly RawEntry[] = [ | ||
| ...(readCurrentCodexModelsCache()?.models ?? []), | ||
| // Existing generated rows are also safe to reuse after a process starts without a cache | ||
| // invalidation pass; bare user-authored catalog rows are intentionally not trusted here. | ||
| ...(readCurrentCodexCatalog()?.models ?? []).filter(entry => | ||
| trustedAccountBoundNativeCatalogSlug(entry) !== undefined), | ||
| ], | ||
| ): string[] { | ||
| const observed = observedEntries.flatMap(entry => { | ||
| const slug = observedAccountBoundNativeSlug(entry); | ||
| return slug === undefined ? [] : [slug]; | ||
| }); | ||
| return unique([...NATIVE_OPENAI_MODELS, ...observed]); | ||
| } | ||
|
|
||
| /** | ||
| * Resolve account-native ids per public selector. Bare observations come from Codex's main | ||
| * catalog/cache, so they are eligible only for selectors that target the main account. A | ||
| * generated qualified row carries its own selector and never gets copied to an unrelated pool | ||
| * account. An explicit observation marker is public selector metadata only; private account ids | ||
| * never enter the catalog or cache. | ||
| */ | ||
| export function accountBoundNativeOpenAiSlugsBySelector( | ||
| config: AccountSelectorConfig, | ||
| observedEntries: readonly RawEntry[] = [ | ||
| ...(readCurrentCodexModelsCache()?.models ?? []), | ||
| ...(readCurrentCodexCatalog()?.models ?? []).filter(entry => | ||
| trustedAccountBoundNativeCatalogSlug(entry) !== undefined), | ||
| ], | ||
|
Comment on lines
+370
to
+399
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Description: Find callers that rely on the disk-reading default parameters.
set -euo pipefail
echo "== accountBoundNativeOpenAiSlugsBySelector call sites =="
rg -nP --type=ts -C4 '\baccountBoundNativeOpenAiSlugsBySelector\s*\('
echo "== accountBoundNativeOpenAiSlugs call sites =="
rg -nP --type=ts -C4 '\baccountBoundNativeOpenAiSlugs\s*\('
echo "== observedAccountBoundNativeOpenAiSlugs call sites =="
rg -nP --type=ts -C4 '\bobservedAccountBoundNativeOpenAiSlugs\s*\('
echo "== desktopVisibleNativeSlugs call sites =="
rg -nP --type=ts -C4 '\bdesktopVisibleNativeSlugs\s*\('
echo "== confirm the /v1/models handler encloses the src/server/index.ts calls =="
rg -nP --type=ts -C25 "url\.pathname === \"/v1/models\"" src/server/index.tsRepository: lidge-jun/opencodex Length of output: 214 🏁 Script executed: set -euo pipefail
echo "== matching files =="
fd -t f -e ts . | rg '(^|/)(metadata|parsing|sync|convergence|index|model-rows|model-routes)\.ts$'
echo "== symbol definitions and call sites =="
rg -n -F -e 'accountBoundNativeOpenAiSlugsBySelector' \
-e 'accountBoundNativeOpenAiSlugs' \
-e 'observedAccountBoundNativeOpenAiSlugs' \
-e 'desktopVisibleNativeSlugs' \
-e 'readCurrentCodexModelsCache' \
-e 'readCurrentCodexCatalog' \
--glob '*.ts' .
echo "== metadata structure =="
metadata="$(fd -t f -a 'metadata.ts' . | head -n1)"
test -n "$metadata"
wc -l "$metadata"
sed -n '330,455p' "$metadata"
echo "== server model-list context =="
index="$(fd -t f -a 'index.ts' . | rg '(^|/)src/server/index\.ts$' | head -n1)"
test -n "$index"
rg -n -C35 'v1/models|accountBoundNativeOpenAiSlugsBySelector|desktopVisibleNativeSlugs|observedAccountBoundNativeOpenAiSlugs' "$index"
echo "== management callers =="
for name in model-rows.ts model-routes.ts; do
file="$(fd -t f -a "$name" . | head -n1)"
if test -n "$file"; then
echo "--- $file"
rg -n -C12 'accountBoundNativeOpenAiSlugs|accountBoundNativeOpenAiSlugsBySelector|observedAccountBoundNativeOpenAiSlugs' "$file" || true
fi
doneRepository: lidge-jun/opencodex Length of output: 28406 🏁 Script executed: set -euo pipefail
echo "== metadata implementation =="
sed -n '190,240p' src/codex/catalog/metadata.ts
sed -n '350,445p' src/codex/catalog/metadata.ts
echo "== bundled readers and parsing =="
sed -n '500,555p' src/codex/catalog/bundled.ts
rg -n -C12 'function readCatalog|export .*readCatalog|readCatalog\(' src/codex/catalog/parsing.ts src/codex/catalog/bundled.ts
echo "== complete /v1/models response branches =="
sed -n '869,1015p' src/server/index.ts
echo "== all relevant direct callers with function context =="
for file in src/codex/catalog/metadata.ts src/server/index.ts src/server/management/model-rows.ts src/server/management/model-routes.ts src/cli/claude-desktop.ts src/server/management/native-integration-routes.ts src/server/management/agent-settings-routes.ts src/server/management/shared.ts; do
echo "--- $file"
rg -n -C8 'accountBoundNativeOpenAiSlugsBySelector|accountBoundNativeOpenAiSlugs\(|observedAccountBoundNativeOpenAiSlugs|desktopVisibleNativeSlugs\(' "$file" || true
done
echo "== catalog/cache file size and reader caching =="
rg -n -C8 'CODEX.*CATALOG|MODELS.*CACHE|catalogPath|modelsCache|readFileSync|existsSync|JSON.parse' src/codex/catalog/bundled.ts src/codex/catalog/parsing.ts
find . -type f \( -name 'model_catalog.json' -o -name '*models*cache*.json' \) -printf '%s %p\n' 2>/dev/null | head -20Repository: lidge-jun/opencodex Length of output: 50375 🏁 Script executed: set -euo pipefail
echo "== account-bound inclusion condition =="
rg -n -C12 'function shouldIncludeAccountBoundNativeOpenAi|export function shouldIncludeAccountBoundNativeOpenAi|shouldIncludeAccountBoundNativeOpenAi' src/codex/catalog/metadata.ts
echo "== concise line-numbered target regions =="
nl -ba src/codex/catalog/metadata.ts | sed -n '208,228p;366,438p'
nl -ba src/server/index.ts | sed -n '869,947p'
nl -ba src/server/management/model-rows.ts | sed -n '45,68p'
nl -ba src/server/management/model-routes.ts | sed -n '232,245p'
echo "== read path resolution =="
rg -n -C10 'function readCodexCatalogPath|export function readCodexCatalogPath|function activeCodexModelsCachePath|export function activeCodexModelsCachePath' src/codex
echo "== deterministic static verifier for default-reader call paths =="
python3 - <<'PY'
from pathlib import Path
import re
metadata = Path("src/codex/catalog/metadata.ts").read_text()
for name in ("accountBoundNativeOpenAiSlugs", "accountBoundNativeOpenAiSlugsBySelector"):
m = re.search(rf"export function {name}\s*\\([^)]*\\)\\s*(?::[^{{]+)?\\{{", metadata, re.S)
assert m, name
body = metadata[m.start():]
body = body[:body.find("\nexport function ", 1)] if "\nexport function " in body[1:] else body
print(name, "default disk-reader expressions:", body.count("readCurrentCodexModelsCache()"), body.count("readCurrentCodexCatalog()"))
index = Path("src/server/index.ts").read_text()
for pattern in (
r"accountBoundNativeOpenAiSlugsBySelector\\(config\\)",
r"desktopVisibleNativeSlugs\\(config\\)",
):
print(pattern, "matches:", len(re.findall(pattern, index)))
print("desktop call is inside Anthropic branch:",
index.index("desktopVisibleNativeSlugs(config)") > index.index("if (wantsAnthropicList")
and index.index("desktopVisibleNativeSlugs(config)") < index.index("if (url.searchParams.has(\"client_version\""))
)
PYRepository: lidge-jun/opencodex Length of output: 3512 🏁 Script executed: set -euo pipefail
echo "== concise line-numbered target regions =="
sed -n '208,228p' src/codex/catalog/metadata.ts | awk '{printf "%d %s\n", NR+207, $0}'
sed -n '366,438p' src/codex/catalog/metadata.ts | awk '{printf "%d %s\n", NR+365, $0}'
sed -n '869,947p' src/server/index.ts | awk '{printf "%d %s\n", NR+868, $0}'
sed -n '45,68p' src/server/management/model-rows.ts | awk '{printf "%d %s\n", NR+44, $0}'
sed -n '232,245p' src/server/management/model-routes.ts | awk '{printf "%d %s\n", NR+231, $0}'
echo "== read path resolution =="
rg -n -C10 'function readCodexCatalogPath|export function readCodexCatalogPath|function activeCodexModelsCachePath|export function activeCodexModelsCachePath' src/codex || true
echo "== deterministic static verifier for default-reader call paths =="
python3 - <<'PY'
from pathlib import Path
import re
metadata = Path("src/codex/catalog/metadata.ts").read_text()
for name in ("accountBoundNativeOpenAiSlugs", "accountBoundNativeOpenAiSlugsBySelector"):
m = re.search(rf"export function {name}\s*\([^)]*\)\s*(?::[^{]+)?\{{", metadata, re.S)
assert m, name
tail = metadata[m.start():]
next_export = tail.find("\nexport function ", 1)
body = tail if next_export < 0 else tail[:next_export]
print(name, "readCurrentCodexModelsCache calls:", body.count("readCurrentCodexModelsCache()"),
"readCurrentCodexCatalog calls:", body.count("readCurrentCodexCatalog()"))
index = Path("src/server/index.ts").read_text()
for expression in (
"accountBoundNativeOpenAiSlugsBySelector(config)",
"desktopVisibleNativeSlugs(config)",
):
print(expression, "matches:", index.count(expression))
start = index.index("if (wantsAnthropicList")
end = index.index('if (url.searchParams.has("client_version")', start)
print("desktop call is inside Anthropic branch:", "desktopVisibleNativeSlugs(config)" in index[start:end])
PYRepository: lidge-jun/opencodex Length of output: 16779 🏁 Script executed: set -euo pipefail
python3 - <<'PY'
from pathlib import Path
import re
metadata = Path("src/codex/catalog/metadata.ts").read_text()
for name in ("accountBoundNativeOpenAiSlugs", "accountBoundNativeOpenAiSlugsBySelector"):
pattern = r"export function " + name + r"\s*\([^)]*\)\s*(?::[^{]+)?\{"
m = re.search(pattern, metadata, re.S)
assert m, name
tail = metadata[m.start():]
next_export = tail.find("\nexport function ", 1)
body = tail if next_export < 0 else tail[:next_export]
print(name, "readCurrentCodexModelsCache calls:", body.count("readCurrentCodexModelsCache()"),
"readCurrentCodexCatalog calls:", body.count("readCurrentCodexCatalog()"))
index = Path("src/server/index.ts").read_text()
print("accountBoundNativeOpenAiSlugsBySelector(config) matches:",
index.count("accountBoundNativeOpenAiSlugsBySelector(config)"))
print("desktopVisibleNativeSlugs(config) matches:",
index.count("desktopVisibleNativeSlugs(config)"))
start = index.index("if (wantsAnthropicList")
end = index.index('if (url.searchParams.has("client_version")', start)
print("desktop call is inside Anthropic branch:",
"desktopVisibleNativeSlugs(config)" in index[start:end])
PYRepository: lidge-jun/opencodex Length of output: 276 Reuse one observed-entry snapshot per discovery request. 🧰 Tools🪛 ast-grep (0.45.1)[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec. (detect-child-process-typescript) 🤖 Prompt for AI AgentsSource: Path instructions |
||
| ): ReadonlyMap<string, readonly string[]> { | ||
| const selectors = visibleCodexAccountSelectors(config); | ||
| const mainSelectors = new Set(mainAccountSelectors(config)); | ||
| const result = new Map<string, Set<string>>( | ||
| selectors.map(selector => [selector, new Set(NATIVE_OPENAI_MODELS)]), | ||
| ); | ||
| for (const entry of observedEntries) { | ||
| const slug = observedAccountBoundNativeSlug(entry); | ||
| if (slug === undefined || SUPPORTED_NATIVE_OPENAI_SLUGS.has(slug)) continue; | ||
| const generated = trustedAccountBoundNativeCatalogSlug(entry); | ||
| const generatedSelector = generated === undefined || typeof entry.slug !== "string" | ||
| ? undefined | ||
| : entry.slug.slice(0, entry.slug.indexOf("/")); | ||
| const markedSelectors = Array.isArray(entry[ACCOUNT_BOUND_OBSERVED_SELECTORS_MARKER]) | ||
| ? entry[ACCOUNT_BOUND_OBSERVED_SELECTORS_MARKER].filter((value): value is string => typeof value === "string") | ||
| : []; | ||
| const eligible = generatedSelector !== undefined | ||
| ? (mainSelectors.has(generatedSelector) ? [generatedSelector] : []) | ||
| : markedSelectors.length > 0 | ||
| ? markedSelectors.filter(selector => mainSelectors.has(selector)) | ||
| : [...mainSelectors]; | ||
|
Comment on lines
+416
to
+420
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a user renames a selector that maps to Useful? React with 👍 / 👎. |
||
| for (const selector of eligible) { | ||
| const rows = result.get(selector); | ||
| if (rows) rows.add(slug); | ||
| } | ||
| } | ||
| return new Map([...result.entries()].map(([selector, slugs]) => [selector, [...slugs]])); | ||
| } | ||
|
|
||
| /** Unknown native ids observed from Codex, excluding the static release set. */ | ||
| export function observedAccountBoundNativeOpenAiSlugs( | ||
| observedEntries?: readonly RawEntry[], | ||
| ): string[] { | ||
| const all = accountBoundNativeOpenAiSlugs(observedEntries); | ||
| return all.filter(slug => !SUPPORTED_NATIVE_OPENAI_SLUGS.has(slug)); | ||
| } | ||
|
|
||
| function catalogNativeSlugs(): string[] { | ||
| const cat = readCurrentCatalogOrCache(); | ||
| const models = cat?.models ?? []; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When a user disables an unknown account-scoped model, convergence writes its generated selector-qualified row with
visibility: "hide"but withoutopencodex_account_observed_native. This condition then rejects that row on the next catalog read, soaccountBoundNativeOpenAiSlugsBySelector()forgets the model and/api/model-visibilityno longer includes it insupportedNative; an attempt to re-enable the same model therefore returns 400 until Codex happens to observe it upstream again. Treat trusted generated account-bound rows as observations regardless of their current visibility, while still applying the current selector filter, and add a disable/re-enable regression test.AGENTS.md reference: src/AGENTS.md:L24-L26
Useful? React with 👍 / 👎.