Skip to content

Commit 4e5ca32

Browse files
committed
fix: resolve second-round PR review findings
Harden storage fallback lock/error handling, preserve xhigh for GPT-5 legacy aliases mapped to GPT-5.4, and address review nits in tests/model map.
1 parent 610440b commit 4e5ca32

File tree

6 files changed

+120
-9
lines changed

6 files changed

+120
-9
lines changed

lib/request/helpers/model-map.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ const DATED_ALIAS_EFFORT_SUFFIXES = [
1919
"-high",
2020
"-xhigh",
2121
] as const;
22+
const GPT_54_SNAPSHOT_DATE = "2026-03-05" as const;
2223

2324
function expandDatedAliases(prefix: string, target: string): Record<string, string> {
2425
return Object.fromEntries(
@@ -82,7 +83,7 @@ export const MODEL_MAP: Record<string, string> = {
8283
"gpt-5.4-medium": "gpt-5.4",
8384
"gpt-5.4-high": "gpt-5.4",
8485
"gpt-5.4-xhigh": "gpt-5.4",
85-
...expandDatedAliases("gpt-5.4-2026-03-05", "gpt-5.4"),
86+
...expandDatedAliases(`gpt-5.4-${GPT_54_SNAPSHOT_DATE}`, "gpt-5.4"),
8687

8788
// ============================================================================
8889
// GPT-5.4 Pro Models (optional/manual config)
@@ -93,7 +94,7 @@ export const MODEL_MAP: Record<string, string> = {
9394
"gpt-5.4-pro-medium": "gpt-5.4-pro",
9495
"gpt-5.4-pro-high": "gpt-5.4-pro",
9596
"gpt-5.4-pro-xhigh": "gpt-5.4-pro",
96-
...expandDatedAliases("gpt-5.4-pro-2026-03-05", "gpt-5.4-pro"),
97+
...expandDatedAliases(`gpt-5.4-pro-${GPT_54_SNAPSHOT_DATE}`, "gpt-5.4-pro"),
9798

9899
// ============================================================================
99100
// GPT-5.2 Models (supports none/low/medium/high/xhigh per OpenAI API docs)

lib/request/request-transformer.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -426,6 +426,7 @@ export function getReasoningConfig(
426426
userConfig: ConfigOptions = {},
427427
): ReasoningConfig {
428428
const normalizedName = modelName?.toLowerCase() ?? "";
429+
const canonicalModelName = normalizeModel(modelName);
429430

430431
// Canonical GPT-5 Codex (stable) defaults to high and does not support "none".
431432
const isGpt5Codex =
@@ -456,6 +457,10 @@ export function getReasoningConfig(
456457
const isGpt52General =
457458
(normalizedName.includes("gpt-5.2") || normalizedName.includes("gpt 5.2")) &&
458459
!isGpt52Codex;
460+
const canonicalSupportsXhigh =
461+
canonicalModelName === "gpt-5.4" ||
462+
canonicalModelName === "gpt-5.4-pro" ||
463+
canonicalModelName === "gpt-5.2";
459464
const isCodexMax =
460465
normalizedName.includes("codex-max") ||
461466
normalizedName.includes("codex max");
@@ -487,7 +492,12 @@ export function getReasoningConfig(
487492

488493
// GPT-5.4/5.2 general, GPT-5.4 Pro, legacy GPT-5.2/5.3 Codex aliases, and Codex Max support xhigh reasoning
489494
const supportsXhigh =
490-
isGpt54General || isGpt54Pro || isGpt52General || isGpt53Codex || isGpt52Codex || isCodexMax;
495+
isGpt54General ||
496+
isGpt54Pro ||
497+
isGpt52General ||
498+
isGpt53Codex ||
499+
isGpt52Codex ||
500+
isCodexMax;
491501

492502
// GPT 5.1/5.2/5.4 general support "none" reasoning per:
493503
// - OpenAI API docs: "gpt-5.1 defaults to none, supports: none, low, medium, high"
@@ -496,7 +506,10 @@ export function getReasoningConfig(
496506
// - Codex CLI: docs/config.md lists "none" as valid for model_reasoning_effort
497507
// - gpt-5.2 and gpt-5.4 general models support: none, low, medium, high, xhigh
498508
// - Codex/Pro models (including GPT-5 Codex, GPT-5.4 Pro, and legacy GPT-5.3/5.2 Codex aliases) do NOT support "none"
499-
const supportsNone = isGpt54General || isGpt52General || isGpt51General;
509+
const supportsNone =
510+
isGpt54General ||
511+
isGpt52General ||
512+
isGpt51General;
500513

501514
// Default based on model type (Codex CLI defaults + plugin opinionated tuning)
502515
// Note: OpenAI docs say gpt-5.1 defaults to "none", but we default to "medium"
@@ -531,7 +544,9 @@ export function getReasoningConfig(
531544
}
532545

533546
// For models that don't support xhigh, downgrade to high
534-
if (!supportsXhigh && effort === "xhigh") {
547+
// Legacy aliases like gpt-5-mini/gpt-5-nano normalize to gpt-5.4, which supports xhigh.
548+
const supportsRequestedXhigh = supportsXhigh || canonicalSupportsXhigh;
549+
if (!supportsRequestedXhigh && effort === "xhigh") {
535550
effort = "high";
536551
}
537552

lib/storage.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -676,7 +676,7 @@ export function normalizeAccountStorage(data: unknown): AccountStorageV3 | null
676676
* @returns AccountStorageV3 if file exists and is valid, null otherwise
677677
*/
678678
export async function loadAccounts(): Promise<AccountStorageV3 | null> {
679-
return loadAccountsInternal(saveAccounts);
679+
return withStorageLock(async () => loadAccountsInternal(saveAccountsUnlocked));
680680
}
681681

682682
function getGlobalAccountsStoragePath(): string {
@@ -775,8 +775,16 @@ async function loadAccountsInternal(
775775
try {
776776
await fs.access(seedPath);
777777
return;
778-
} catch {
779-
// Another concurrent caller has not seeded the project path yet.
778+
} catch (accessError) {
779+
const accessCode = (accessError as NodeJS.ErrnoException).code;
780+
if (accessCode !== "ENOENT") {
781+
log.warn("Failed to inspect project seed path before fallback seeding", {
782+
path: seedPath,
783+
error: String(accessError),
784+
});
785+
return;
786+
}
787+
// File is missing; proceed with seed write.
780788
}
781789

782790
try {

test/codex-prompts.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@ describe("Codex Prompts Module", () => {
4444

4545
it("should be a readonly array", () => {
4646
expect(Array.isArray(MODEL_FAMILIES)).toBe(true);
47-
expect(MODEL_FAMILIES.length).toBe(7);
47+
expect(MODEL_FAMILIES.length).toBeGreaterThanOrEqual(7);
48+
expect(new Set(MODEL_FAMILIES).size).toBe(MODEL_FAMILIES.length);
4849
});
4950
});
5051

test/request-transformer.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2102,6 +2102,32 @@ describe('Request Transformer Module', () => {
21022102
expect(result.model).toBe('gpt-5.4'); // legacy gpt-5 aliases now map to gpt-5.4
21032103
expect(result.reasoning?.effort).toBe('minimal'); // Lightweight gpt-5-mini defaults to minimal
21042104
});
2105+
2106+
it('should preserve xhigh effort for gpt-5-mini (normalized to gpt-5.4)', async () => {
2107+
const body: RequestBody = {
2108+
model: 'gpt-5-mini',
2109+
input: [],
2110+
reasoning: { effort: 'xhigh' },
2111+
};
2112+
2113+
const result = await transformRequestBody(body, codexInstructions);
2114+
2115+
expect(result.model).toBe('gpt-5.4');
2116+
expect(result.reasoning?.effort).toBe('xhigh');
2117+
});
2118+
2119+
it('should preserve xhigh effort for gpt-5-nano (normalized to gpt-5.4)', async () => {
2120+
const body: RequestBody = {
2121+
model: 'gpt-5-nano',
2122+
input: [],
2123+
reasoning: { effort: 'xhigh' },
2124+
};
2125+
2126+
const result = await transformRequestBody(body, codexInstructions);
2127+
2128+
expect(result.model).toBe('gpt-5.4');
2129+
expect(result.reasoning?.effort).toBe('xhigh');
2130+
});
21052131
});
21062132

21072133
describe('Scenario 2: Custom preset names (new style)', () => {

test/storage.test.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1921,6 +1921,66 @@ describe("storage", () => {
19211921
}
19221922
});
19231923

1924+
it("skips seed write when project path access fails with non-ENOENT error", async () => {
1925+
const fakeHome = join(testWorkDir, "home-fallback-access-error");
1926+
const projectDir = join(testWorkDir, "project-fallback-access-error");
1927+
const projectGitDir = join(projectDir, ".git");
1928+
const globalConfigDir = join(fakeHome, ".opencode");
1929+
const globalStoragePath = join(globalConfigDir, "openai-codex-accounts.json");
1930+
1931+
await fs.mkdir(fakeHome, { recursive: true });
1932+
await fs.mkdir(projectGitDir, { recursive: true });
1933+
await fs.mkdir(globalConfigDir, { recursive: true });
1934+
process.env.HOME = fakeHome;
1935+
process.env.USERPROFILE = fakeHome;
1936+
setStoragePath(projectDir);
1937+
1938+
const globalStorage = {
1939+
version: 3,
1940+
activeIndex: 0,
1941+
accounts: [
1942+
{
1943+
refreshToken: "global-refresh-access-error",
1944+
accountId: "global-account-access-error",
1945+
addedAt: 1,
1946+
lastUsed: 1,
1947+
},
1948+
],
1949+
};
1950+
await fs.writeFile(globalStoragePath, JSON.stringify(globalStorage), "utf-8");
1951+
1952+
const projectScopedPath = getStoragePath();
1953+
const originalAccess = fs.access.bind(fs);
1954+
const originalRename = fs.rename.bind(fs);
1955+
let projectSeedWriteCount = 0;
1956+
1957+
const accessSpy = vi.spyOn(fs, "access").mockImplementation(async (path, mode) => {
1958+
if (String(path) === projectScopedPath) {
1959+
const err = new Error("EACCES access failure") as NodeJS.ErrnoException;
1960+
err.code = "EACCES";
1961+
throw err;
1962+
}
1963+
return originalAccess(path as string, mode);
1964+
});
1965+
1966+
const renameSpy = vi.spyOn(fs, "rename").mockImplementation(async (sourcePath, destinationPath) => {
1967+
if (String(destinationPath) === projectScopedPath) {
1968+
projectSeedWriteCount += 1;
1969+
}
1970+
return originalRename(sourcePath, destinationPath);
1971+
});
1972+
1973+
try {
1974+
const loaded = await loadAccounts();
1975+
expect(loaded?.accounts[0]?.accountId).toBe("global-account-access-error");
1976+
expect(projectSeedWriteCount).toBe(0);
1977+
expect(existsSync(projectScopedPath)).toBe(false);
1978+
} finally {
1979+
accessSpy.mockRestore();
1980+
renameSpy.mockRestore();
1981+
}
1982+
});
1983+
19241984
it("returns null when global fallback storage is corrupted", async () => {
19251985
const fakeHome = join(testWorkDir, "home-fallback-corrupted");
19261986
const projectDir = join(testWorkDir, "project-fallback-corrupted");

0 commit comments

Comments
 (0)