Skip to content

Commit 41e75ba

Browse files
author
Codex
committed
Fix strict Codex catalog regeneration
1 parent ed90261 commit 41e75ba

3 files changed

Lines changed: 52 additions & 21 deletions

File tree

src/codex-catalog.ts

Lines changed: 38 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,12 @@ function readCatalog(path: string): { models?: RawEntry[]; [k: string]: unknown
5757
} catch { return null; }
5858
}
5959

60+
function findNativeTemplate(catalog: { models?: RawEntry[] } | null): RawEntry | null {
61+
return catalog?.models?.find(
62+
m => typeof m.slug === "string" && !m.slug.includes("/") && "base_instructions" in m,
63+
) ?? null;
64+
}
65+
6066
function normalizeServiceTiers(entry: RawEntry): RawEntry {
6167
// Codex stores the user-facing config spelling as "fast", but the catalog/request
6268
// service tier id is "priority" in current codex-rs. Keep legacy catalogs working.
@@ -83,6 +89,28 @@ function ensureAutoCompactTokenLimit(entry: RawEntry): RawEntry {
8389
return entry;
8490
}
8591

92+
function ensureStrictCatalogFields(entry: RawEntry): RawEntry {
93+
if (typeof entry.supports_reasoning_summaries !== "boolean") entry.supports_reasoning_summaries = true;
94+
if (typeof entry.default_reasoning_summary !== "string") entry.default_reasoning_summary = "none";
95+
if (typeof entry.support_verbosity !== "boolean") entry.support_verbosity = true;
96+
if (typeof entry.default_verbosity !== "string") entry.default_verbosity = "low";
97+
if (typeof entry.apply_patch_tool_type !== "string") entry.apply_patch_tool_type = "freeform";
98+
if (!entry.truncation_policy || typeof entry.truncation_policy !== "object" || Array.isArray(entry.truncation_policy)) {
99+
entry.truncation_policy = { mode: "tokens", limit: 10000 };
100+
}
101+
if (typeof entry.supports_parallel_tool_calls !== "boolean") entry.supports_parallel_tool_calls = true;
102+
if (typeof entry.supports_image_detail_original !== "boolean") entry.supports_image_detail_original = false;
103+
if (!Array.isArray(entry.experimental_supported_tools)) entry.experimental_supported_tools = [];
104+
if (!Array.isArray(entry.input_modalities)) entry.input_modalities = ["text"];
105+
if (typeof entry.context_window !== "number" || entry.context_window <= 0) entry.context_window = 128000;
106+
if (typeof entry.max_context_window !== "number" || entry.max_context_window <= 0) {
107+
entry.max_context_window = entry.context_window;
108+
}
109+
if (typeof entry.effective_context_window_percent !== "number") entry.effective_context_window_percent = 95;
110+
if (typeof entry.comp_hash !== "string") entry.comp_hash = "opencodex";
111+
return ensureAutoCompactTokenLimit(entry);
112+
}
113+
86114
export function normalizeRoutedCatalogEntry(entry: RawEntry): RawEntry {
87115
delete entry.model_messages;
88116
delete entry.tool_mode;
@@ -97,7 +125,7 @@ export function normalizeRoutedCatalogEntry(entry: RawEntry): RawEntry {
97125
// runs through native gpt-5.4-mini, so image search is available and verbalized for text-only models.
98126
entry.web_search_tool_type = "text_and_image";
99127
entry.supports_search_tool = true;
100-
return ensureAutoCompactTokenLimit(entry);
128+
return ensureStrictCatalogFields(entry);
101129
}
102130

103131
function applyJawcodeCatalogMetadata(entry: RawEntry, slug: string): void {
@@ -122,8 +150,8 @@ function applyJawcodeCatalogMetadata(entry: RawEntry, slug: string): void {
122150

123151
function loadCatalogForSync(path: string): { models?: RawEntry[]; [k: string]: unknown } | null {
124152
const catalog = readCatalog(path);
125-
if (catalog) return catalog;
126-
return readCatalog(CODEX_MODELS_CACHE_PATH);
153+
if (catalog && findNativeTemplate(catalog)) return catalog;
154+
return readCatalog(CATALOG_BACKUP_PATH) ?? readCatalog(CODEX_MODELS_CACHE_PATH) ?? catalog;
127155
}
128156

129157
function readCurrentCatalogOrCache(): { models?: RawEntry[]; [k: string]: unknown } | null {
@@ -136,10 +164,9 @@ function readCurrentCatalogOrCache(): { models?: RawEntry[]; [k: string]: unknow
136164
* Returns a deep copy, or null if no catalog/native entry exists.
137165
*/
138166
export function loadCatalogTemplate(): RawEntry | null {
139-
const cat = readCurrentCatalogOrCache();
140-
const native = cat?.models?.find(
141-
m => typeof m.slug === "string" && !m.slug.includes("/") && "base_instructions" in m,
142-
);
167+
const native = findNativeTemplate(readCatalog(readCodexCatalogPath()))
168+
?? findNativeTemplate(readCatalog(CATALOG_BACKUP_PATH))
169+
?? findNativeTemplate(readCatalog(CODEX_MODELS_CACHE_PATH));
143170
return native ? JSON.parse(JSON.stringify(native)) : null;
144171
}
145172

@@ -186,7 +213,7 @@ function deriveEntry(template: RawEntry | null, slug: string, desc: string, prio
186213
normalizeRoutedCatalogEntry(e);
187214
applyJawcodeCatalogMetadata(e, slug);
188215
}
189-
return ensureAutoCompactTokenLimit(normalizeServiceTiers(e));
216+
return ensureStrictCatalogFields(normalizeServiceTiers(e));
190217
}
191218
// Fallback when no template is available (best-effort; strict parser may need more).
192219
const entry: RawEntry = {
@@ -198,7 +225,7 @@ function deriveEntry(template: RawEntry | null, slug: string, desc: string, prio
198225
...(slug.includes("/") ? { web_search_tool_type: "text_and_image", supports_search_tool: true } : {}),
199226
};
200227
applyJawcodeCatalogMetadata(entry, slug);
201-
return ensureAutoCompactTokenLimit(normalizeServiceTiers(entry));
228+
return ensureStrictCatalogFields(normalizeServiceTiers(entry));
202229
}
203230

204231
/**
@@ -351,9 +378,7 @@ export async function syncCatalogModels(config: OcxConfig): Promise<{ added: num
351378
const catalog = loadCatalogForSync(catalogPath);
352379
if (!catalog) return { added: 0, path: catalogPath };
353380

354-
const template = (catalog.models ?? []).find(
355-
m => typeof m.slug === "string" && !m.slug.includes("/") && "base_instructions" in m,
356-
) ?? null;
381+
const template = findNativeTemplate(catalog);
357382

358383
const goModels = await gatherRoutedModels(config);
359384
if (goModels.length === 0) return { added: 0, path: catalogPath };
@@ -383,7 +408,7 @@ export async function syncCatalogModels(config: OcxConfig): Promise<{ added: num
383408
// native template can never leak supports_websockets while the flag is off.
384409
const wsEnabled = websocketsEnabled(config);
385410
catalog.models = [...native, ...goEntries].map(m => {
386-
const e = ensureAutoCompactTokenLimit(normalizeServiceTiers(m));
411+
const e = ensureStrictCatalogFields(normalizeServiceTiers(m));
387412
if (wsEnabled) e.supports_websockets = true;
388413
else delete e.supports_websockets;
389414
return e;

src/codex-inject.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -143,12 +143,16 @@ function ensureFastModeFeature(content: string): string {
143143
return lines.join("\n");
144144
}
145145

146-
function stripDefaultCatalogPath(content: string): string {
146+
function isOpencodexCatalogPath(path: string): boolean {
147+
return path.replace(/\\/g, "/").split("/").pop() === "opencodex-catalog.json";
148+
}
149+
150+
function stripOpencodexCatalogPath(content: string): string {
147151
return content
148152
.split("\n")
149153
.filter(line => {
150154
const m = line.match(/^\s*model_catalog_json\s*=\s*("(?:\\.|[^"])*"|'[^']*')\s*$/);
151-
return !m || parseTomlString(m[1]) !== DEFAULT_CATALOG_PATH;
155+
return !m || !isOpencodexCatalogPath(parseTomlString(m[1]));
152156
})
153157
.join("\n");
154158
}
@@ -240,7 +244,7 @@ export function stripOpencodexConfig(content: string): string {
240244
// must match the detection regex above, or a detected line could survive un-removed.
241245
out = out.split("\n").filter(l => !/^\s*model_provider\s*=\s*"opencodex"\s*$/.test(l)).join("\n");
242246
out = stripRootContextWindowOverrides(out);
243-
out = stripDefaultCatalogPath(out);
247+
out = stripOpencodexCatalogPath(out);
244248
return out.replace(/\n{3,}/g, "\n\n").trimEnd() + "\n";
245249
}
246250

tests/codex-catalog.test.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -203,16 +203,18 @@ describe("Codex catalog routed normalization", () => {
203203
expect(routed?.input_modalities).toEqual(["text", "image"]);
204204
});
205205

206-
test("unknown routed entries do not guess jawcode metadata", () => {
206+
test("unknown routed entries receive conservative strict catalog defaults", () => {
207207
const entries = buildCatalogEntries(null, [], [
208208
{ provider: "local", id: "qwen3-coder" },
209209
]);
210210
const routed = entries.find(e => e.slug === "local/qwen3-coder");
211211

212-
expect(routed).not.toHaveProperty("context_window");
213-
expect(routed).not.toHaveProperty("max_context_window");
214-
expect(routed).not.toHaveProperty("auto_compact_token_limit");
215-
expect(routed).not.toHaveProperty("input_modalities");
212+
expect(routed?.context_window).toBe(128_000);
213+
expect(routed?.max_context_window).toBe(128_000);
214+
expect(routed?.auto_compact_token_limit).toBe(115_200);
215+
expect(routed?.input_modalities).toEqual(["text"]);
216+
expect(routed?.supports_reasoning_summaries).toBe(true);
217+
expect(routed?.default_reasoning_summary).toBe("none");
216218
});
217219

218220
test("generated jawcode snapshot is restricted to mapped providers", () => {

0 commit comments

Comments
 (0)