Skip to content

Commit 6fb966a

Browse files
authored
Merge pull request #87 from ndycode/fix/model-mapping
fix: correct model alias mapping, add gpt-5.4-nano, validate Pro reasoning
2 parents 3b251e5 + fa29a36 commit 6fb966a

7 files changed

Lines changed: 171 additions & 63 deletions

File tree

lib/request/helpers/model-map.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -89,8 +89,6 @@ export const MODEL_MAP: Record<string, string> = {
8989
// GPT-5.4 Pro Models (optional/manual config)
9090
// ============================================================================
9191
"gpt-5.4-pro": "gpt-5.4-pro",
92-
"gpt-5.4-pro-none": "gpt-5.4-pro",
93-
"gpt-5.4-pro-low": "gpt-5.4-pro",
9492
"gpt-5.4-pro-medium": "gpt-5.4-pro",
9593
"gpt-5.4-pro-high": "gpt-5.4-pro",
9694
"gpt-5.4-pro-xhigh": "gpt-5.4-pro",
@@ -107,6 +105,17 @@ export const MODEL_MAP: Record<string, string> = {
107105
"gpt-5.4-mini-xhigh": "gpt-5.4-mini",
108106
...expandDatedAliases(`gpt-5.4-mini-${GPT_54_SNAPSHOT_DATE}`, "gpt-5.4-mini"),
109107

108+
// ============================================================================
109+
// GPT-5.4 Nano Models (lightweight efficient family)
110+
// ============================================================================
111+
"gpt-5.4-nano": "gpt-5.4-nano",
112+
"gpt-5.4-nano-none": "gpt-5.4-nano",
113+
"gpt-5.4-nano-low": "gpt-5.4-nano",
114+
"gpt-5.4-nano-medium": "gpt-5.4-nano",
115+
"gpt-5.4-nano-high": "gpt-5.4-nano",
116+
"gpt-5.4-nano-xhigh": "gpt-5.4-nano",
117+
...expandDatedAliases(`gpt-5.4-nano-${GPT_54_SNAPSHOT_DATE}`, "gpt-5.4-nano"),
118+
110119
// ============================================================================
111120
// GPT-5.2 Models (supports none/low/medium/high/xhigh per OpenAI API docs)
112121
// ============================================================================
@@ -160,8 +169,8 @@ export const MODEL_MAP: Record<string, string> = {
160169
// GPT-5 General Purpose Models (LEGACY - maps to gpt-5.4 latest)
161170
// ============================================================================
162171
"gpt-5": "gpt-5.4",
163-
"gpt-5-mini": "gpt-5.4",
164-
"gpt-5-nano": "gpt-5.4",
172+
"gpt-5-mini": "gpt-5.4-mini",
173+
"gpt-5-nano": "gpt-5.4-nano",
165174
};
166175

167176
/**

lib/request/request-transformer.ts

Lines changed: 41 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -89,33 +89,38 @@ export function normalizeModel(model: string | undefined): string {
8989
return "gpt-5.4-mini";
9090
}
9191

92-
// 6. GPT-5.4 (general purpose)
92+
// 6. GPT-5.4 Nano (first-class model)
93+
if (/\bgpt(?:-| )5\.4(?:-| )nano(?:\b|[- ])/.test(normalized)) {
94+
return "gpt-5.4-nano";
95+
}
96+
97+
// 7. GPT-5.4 (general purpose)
9398
if (/\bgpt(?:-| )5\.4(?:\b|[- ])/.test(normalized)) {
9499
return "gpt-5.4";
95100
}
96101

97-
// 7. GPT-5.2 (general purpose)
102+
// 8. GPT-5.2 (general purpose)
98103
if (normalized.includes("gpt-5.2") || normalized.includes("gpt 5.2")) {
99104
return "gpt-5.2";
100105
}
101106

102-
// 8. GPT-5.1 Codex Max
107+
// 9. GPT-5.1 Codex Max
103108
if (
104109
normalized.includes("gpt-5.1-codex-max") ||
105110
normalized.includes("gpt 5.1 codex max")
106111
) {
107112
return "gpt-5.1-codex-max";
108113
}
109114

110-
// 9. GPT-5.1 Codex Mini
115+
// 10. GPT-5.1 Codex Mini
111116
if (
112117
normalized.includes("gpt-5.1-codex-mini") ||
113118
normalized.includes("gpt 5.1 codex mini")
114119
) {
115120
return "gpt-5.1-codex-mini";
116121
}
117122

118-
// 10. Legacy Codex Mini
123+
// 11. Legacy Codex Mini
119124
if (
120125
normalized.includes("codex-mini-latest") ||
121126
normalized.includes("gpt-5-codex-mini") ||
@@ -124,33 +129,33 @@ export function normalizeModel(model: string | undefined): string {
124129
return "gpt-5.1-codex-mini";
125130
}
126131

127-
// 11. GPT-5 Codex canonical + GPT-5.1 Codex legacy alias
132+
// 12. GPT-5 Codex canonical + GPT-5.1 Codex legacy alias
128133
if (
129134
normalized.includes("gpt-5-codex") ||
130135
normalized.includes("gpt 5 codex")
131136
) {
132137
return "gpt-5-codex";
133138
}
134139

135-
// 12. GPT-5.1 Codex (legacy alias)
140+
// 13. GPT-5.1 Codex (legacy alias)
136141
if (
137142
normalized.includes("gpt-5.1-codex") ||
138143
normalized.includes("gpt 5.1 codex")
139144
) {
140145
return "gpt-5-codex";
141146
}
142147

143-
// 13. GPT-5.1 (general-purpose)
148+
// 14. GPT-5.1 (general-purpose)
144149
if (normalized.includes("gpt-5.1") || normalized.includes("gpt 5.1")) {
145150
return "gpt-5.1";
146151
}
147152

148-
// 14. GPT-5 Codex family (any other variant with "codex")
153+
// 15. GPT-5 Codex family (any other variant with "codex")
149154
if (normalized.includes("codex")) {
150155
return "gpt-5-codex";
151156
}
152157

153-
// 15. GPT-5 family (any variant) - default to 5.4 latest general model
158+
// 16. GPT-5 family (any variant) - default to 5.4 latest general model
154159
if (normalized.includes("gpt-5") || normalized.includes("gpt 5")) {
155160
return "gpt-5.4";
156161
}
@@ -480,11 +485,15 @@ export function getReasoningConfig(
480485
// GPT-5.4 Mini is a first-class explicit model.
481486
const isGpt54Mini = canonicalModelName === "gpt-5.4-mini";
482487

488+
// GPT-5.4 Nano is a first-class explicit model.
489+
const isGpt54Nano = canonicalModelName === "gpt-5.4-nano";
490+
483491
// GPT-5.4 general purpose (latest default family)
484492
const isGpt54General =
485493
(normalizedName.includes("gpt-5.4") || normalizedName.includes("gpt 5.4")) &&
486494
!isGpt54Pro &&
487-
!isGpt54Mini;
495+
!isGpt54Mini &&
496+
!isGpt54Nano;
488497

489498
// GPT-5.2 general purpose (not codex variant)
490499
const isGpt52General =
@@ -493,6 +502,7 @@ export function getReasoningConfig(
493502
const canonicalSupportsXhigh =
494503
canonicalModelName === "gpt-5.4" ||
495504
canonicalModelName === "gpt-5.4-mini" ||
505+
canonicalModelName === "gpt-5.4-nano" ||
496506
canonicalModelName === "gpt-5.4-pro" ||
497507
canonicalModelName === "gpt-5.2";
498508
const isCodexMax =
@@ -502,6 +512,7 @@ export function getReasoningConfig(
502512
const isCodex = normalizedName.includes("codex") && !isCodexMini;
503513
const isLightweight =
504514
!isGpt54Mini &&
515+
!isGpt54Nano &&
505516
!isCodexMini &&
506517
/\bgpt(?:-| )5(?:-| )(?:mini|nano)(?:\b|[- ])/.test(normalizedName);
507518

@@ -525,6 +536,7 @@ export function getReasoningConfig(
525536
const supportsXhigh =
526537
isGpt54General ||
527538
isGpt54Mini ||
539+
isGpt54Nano ||
528540
isGpt54Pro ||
529541
isGpt52General ||
530542
isGpt53Codex ||
@@ -535,15 +547,16 @@ export function getReasoningConfig(
535547
// - OpenAI API docs: "gpt-5.1 defaults to none, supports: none, low, medium, high"
536548
// - GPT-5.4 latest model docs list reasoning controls for the base model family
537549
// - GPT-5.4 Mini should stay aligned with GPT-5.4 reasoning support as a first-class model
538-
// - Legacy lightweight aliases like gpt-5-mini/gpt-5-nano stay distinct and do not inherit
539-
// full "none" support from their gpt-5.4 normalization target
550+
// - Legacy aliases like gpt-5-mini/gpt-5-nano now resolve to first-class
551+
// GPT-5.4 Mini / GPT-5.4 Nano models, so they inherit the same "none" support
540552
// - Codex CLI: ReasoningEffort enum includes None variant (codex-rs/protocol/src/openai_models.rs)
541553
// - Codex CLI: docs/config.md lists "none" as valid for model_reasoning_effort
542554
// - gpt-5.2 and gpt-5.4 general models support: none, low, medium, high, xhigh
543555
// - Codex/Pro models (including GPT-5 Codex, GPT-5.4 Pro, and legacy GPT-5.3/5.2 Codex aliases) do NOT support "none"
544556
const supportsNone =
545557
isGpt54General ||
546558
isGpt54Mini ||
559+
isGpt54Nano ||
547560
isGpt52General ||
548561
(isGpt51General && !isLightweight);
549562

@@ -552,10 +565,8 @@ export function getReasoningConfig(
552565
// for better coding assistance unless user explicitly requests "none".
553566
// - Canonical GPT-5 Codex defaults to high in stable Codex.
554567
// - Legacy GPT-5.3/5.2 Codex aliases default to xhigh for backward compatibility.
555-
// - Legacy lightweight aliases (gpt-5-mini / gpt-5-nano) intentionally keep a
556-
// minimal default based on the original alias, even though normalization maps
557-
// them to gpt-5.4 which supports higher efforts. Explicit xhigh requests are
558-
// still honored below via supportsRequestedXhigh.
568+
// - Legacy gpt-5-mini / gpt-5-nano aliases now resolve to GPT-5.4 Mini / Nano,
569+
// so they inherit the same default "high" effort and direct xhigh support.
559570
const defaultEffort: ReasoningConfig["effort"] = isCodexMini
560571
? "medium"
561572
: isGpt5Codex
@@ -570,6 +581,7 @@ export function getReasoningConfig(
570581

571582
// Get user-requested effort
572583
let effort = userConfig.reasoningEffort || defaultEffort;
584+
const originalRequestedEffort = userConfig.reasoningEffort ?? defaultEffort;
573585

574586
if (isCodexMini) {
575587
if (effort === "minimal" || effort === "low" || effort === "none") {
@@ -584,7 +596,8 @@ export function getReasoningConfig(
584596
}
585597

586598
// For models that don't support xhigh, downgrade to high
587-
// Legacy aliases like gpt-5-mini/gpt-5-nano normalize to gpt-5.4, which supports xhigh.
599+
// Legacy gpt-5-mini/gpt-5-nano aliases now normalize to GPT-5.4 Mini / Nano,
600+
// both of which support xhigh directly.
588601
const supportsRequestedXhigh = supportsXhigh || canonicalSupportsXhigh;
589602
if (!supportsRequestedXhigh && effort === "xhigh") {
590603
effort = "high";
@@ -596,6 +609,16 @@ export function getReasoningConfig(
596609
effort = "low";
597610
}
598611

612+
// GPT-5.4 Pro only supports medium/high/xhigh reasoning.
613+
// originalRequestedEffort is a non-sensitive model setting string, not a token.
614+
// Logging this coercion does not introduce new redaction or filesystem-race risk.
615+
if (isGpt54Pro && (effort === "low" || effort === "minimal")) {
616+
logWarn(
617+
`GPT-5.4 Pro supports medium/high/xhigh only; coercing '${originalRequestedEffort}' to 'medium'`,
618+
);
619+
effort = "medium";
620+
}
621+
599622
// Normalize "minimal" to "low" for Codex families
600623
// Codex CLI presets are low/medium/high (or xhigh for Codex Max / GPT-5.3/5.2 Codex)
601624
if (isCodex && effort === "minimal") {

test/config.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -90,10 +90,10 @@ describe('Configuration Parsing', () => {
9090
expect(defaultReasoning.summary).toBe('auto');
9191
});
9292

93-
it('should use minimal effort for lightweight models (nano/mini)', () => {
93+
it('should use high effort for first-class nano/mini models (resolved via alias)', () => {
9494
const nanoReasoning = getReasoningConfig('gpt-5-nano', {});
9595

96-
expect(nanoReasoning.effort).toBe('minimal');
96+
expect(nanoReasoning.effort).toBe('high');
9797
expect(nanoReasoning.summary).toBe('auto');
9898
});
9999

@@ -167,9 +167,9 @@ describe('Configuration Parsing', () => {
167167
});
168168

169169
describe('Model-specific behavior', () => {
170-
it('should detect lightweight models correctly', () => {
170+
it('should detect first-class mini model correctly (resolved via alias)', () => {
171171
const miniReasoning = getReasoningConfig('gpt-5-mini', {});
172-
expect(miniReasoning.effort).toBe('minimal');
172+
expect(miniReasoning.effort).toBe('high');
173173
});
174174

175175
it('should detect codex models correctly', () => {

test/gpt54-models.test.ts

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -71,10 +71,8 @@ describe("GPT-5.4 Model Support", () => {
7171
expect(getNormalizedModel("gpt-5.4-pro")).toBe("gpt-5.4-pro");
7272
});
7373

74-
it("should normalize all gpt-5.4-pro reasoning effort variants", () => {
74+
it("should normalize supported gpt-5.4-pro reasoning effort variants", () => {
7575
const variants = [
76-
"gpt-5.4-pro-none",
77-
"gpt-5.4-pro-low",
7876
"gpt-5.4-pro-medium",
7977
"gpt-5.4-pro-high",
8078
"gpt-5.4-pro-xhigh",
@@ -86,6 +84,11 @@ describe("GPT-5.4 Model Support", () => {
8684
}
8785
});
8886

87+
it("should not have MODEL_MAP entries for unsupported pro-none and pro-low", () => {
88+
expect(getNormalizedModel("gpt-5.4-pro-none")).toBeUndefined();
89+
expect(getNormalizedModel("gpt-5.4-pro-low")).toBeUndefined();
90+
});
91+
8992
it("should handle gpt-5.4-pro with provider prefix", () => {
9093
expect(normalizeModel("openai/gpt-5.4-pro")).toBe("gpt-5.4-pro");
9194
expect(normalizeModel("openai/gpt-5.4-pro-high")).toBe("gpt-5.4-pro");
@@ -104,8 +107,8 @@ describe("GPT-5.4 Model Support", () => {
104107

105108
it("should handle gpt-5.4-pro in MODEL_MAP", () => {
106109
expect(MODEL_MAP["gpt-5.4-pro"]).toBe("gpt-5.4-pro");
107-
expect(MODEL_MAP["gpt-5.4-pro-none"]).toBe("gpt-5.4-pro");
108-
expect(MODEL_MAP["gpt-5.4-pro-low"]).toBe("gpt-5.4-pro");
110+
expect(MODEL_MAP["gpt-5.4-pro-none"]).toBeUndefined();
111+
expect(MODEL_MAP["gpt-5.4-pro-low"]).toBeUndefined();
109112
expect(MODEL_MAP["gpt-5.4-pro-medium"]).toBe("gpt-5.4-pro");
110113
expect(MODEL_MAP["gpt-5.4-pro-high"]).toBe("gpt-5.4-pro");
111114
expect(MODEL_MAP["gpt-5.4-pro-xhigh"]).toBe("gpt-5.4-pro");
@@ -270,10 +273,10 @@ describe("GPT-5.4 Model Support", () => {
270273
expect(config.effort).toBe("high");
271274
});
272275

273-
it("should not support 'none' for gpt-5.4-pro (codex/pro models)", () => {
276+
it("should coerce 'none' to 'medium' for gpt-5.4-pro (none→low→medium chain)", () => {
274277
const config = getReasoningConfig("gpt-5.4-pro", { reasoningEffort: "none" });
275278
expect(config.effort).not.toBe("none");
276-
expect(config.effort).toBe("low");
279+
expect(config.effort).toBe("medium");
277280
});
278281

279282
it("should support xhigh reasoning for gpt-5.4-pro", () => {
@@ -387,10 +390,10 @@ describe("GPT-5.4 Model Support", () => {
387390
});
388391

389392
describe("GPT-5.4 Integration with Existing Models", () => {
390-
it("should map legacy gpt-5 aliases to gpt-5.4", () => {
393+
it("should map legacy gpt-5 aliases to correct gpt-5.4 variants", () => {
391394
expect(normalizeModel("gpt-5")).toBe("gpt-5.4");
392-
expect(normalizeModel("gpt-5-mini")).toBe("gpt-5.4");
393-
expect(normalizeModel("gpt-5-nano")).toBe("gpt-5.4");
395+
expect(normalizeModel("gpt-5-mini")).toBe("gpt-5.4-mini");
396+
expect(normalizeModel("gpt-5-nano")).toBe("gpt-5.4-nano");
394397
expect(getNormalizedModel("gpt-5")).toBe("gpt-5.4");
395398
});
396399

@@ -480,7 +483,7 @@ describe("GPT-5.4 Model Support", () => {
480483

481484
for (const key of gpt54Keys) {
482485
const normalized = MODEL_MAP[key];
483-
expect(normalized).toMatch(/^gpt-5\.4(-pro|-mini)?$/);
486+
expect(normalized).toMatch(/^gpt-5\.4(-pro|-mini|-nano)?$/);
484487
}
485488
});
486489
});

test/model-map.test.ts

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -38,15 +38,16 @@ describe("Model Map Module", () => {
3838
expect(MODEL_MAP["gpt-5.4-2026-03-05-high"]).toBe("gpt-5.4");
3939
});
4040

41-
it("contains GPT-5.4 Pro models", () => {
41+
it("contains GPT-5.4 Pro models (medium/high/xhigh only)", () => {
4242
expect(MODEL_MAP["gpt-5.4-pro"]).toBe("gpt-5.4-pro");
43-
expect(MODEL_MAP["gpt-5.4-pro-none"]).toBe("gpt-5.4-pro");
44-
expect(MODEL_MAP["gpt-5.4-pro-low"]).toBe("gpt-5.4-pro");
4543
expect(MODEL_MAP["gpt-5.4-pro-medium"]).toBe("gpt-5.4-pro");
4644
expect(MODEL_MAP["gpt-5.4-pro-high"]).toBe("gpt-5.4-pro");
4745
expect(MODEL_MAP["gpt-5.4-pro-xhigh"]).toBe("gpt-5.4-pro");
4846
expect(MODEL_MAP["gpt-5.4-pro-2026-03-05"]).toBe("gpt-5.4-pro");
4947
expect(MODEL_MAP["gpt-5.4-pro-2026-03-05-xhigh"]).toBe("gpt-5.4-pro");
48+
// none and low are not supported for Pro
49+
expect(MODEL_MAP["gpt-5.4-pro-none"]).toBeUndefined();
50+
expect(MODEL_MAP["gpt-5.4-pro-low"]).toBeUndefined();
5051
});
5152

5253
it("contains GPT-5.4 Mini models", () => {
@@ -110,10 +111,19 @@ describe("Model Map Module", () => {
110111
expect(MODEL_MAP["gpt-5-codex-mini-high"]).toBe("gpt-5.1-codex-mini");
111112
});
112113

113-
it("maps legacy GPT-5 general purpose models to GPT-5.4", () => {
114+
it("maps legacy GPT-5 aliases to proper canonical models", () => {
114115
expect(MODEL_MAP["gpt-5"]).toBe("gpt-5.4");
115-
expect(MODEL_MAP["gpt-5-mini"]).toBe("gpt-5.4");
116-
expect(MODEL_MAP["gpt-5-nano"]).toBe("gpt-5.4");
116+
expect(MODEL_MAP["gpt-5-mini"]).toBe("gpt-5.4-mini");
117+
expect(MODEL_MAP["gpt-5-nano"]).toBe("gpt-5.4-nano");
118+
});
119+
120+
it("should map GPT-5.4 Nano model entries", () => {
121+
expect(MODEL_MAP["gpt-5.4-nano"]).toBe("gpt-5.4-nano");
122+
expect(MODEL_MAP["gpt-5.4-nano-none"]).toBe("gpt-5.4-nano");
123+
expect(MODEL_MAP["gpt-5.4-nano-low"]).toBe("gpt-5.4-nano");
124+
expect(MODEL_MAP["gpt-5.4-nano-medium"]).toBe("gpt-5.4-nano");
125+
expect(MODEL_MAP["gpt-5.4-nano-high"]).toBe("gpt-5.4-nano");
126+
expect(MODEL_MAP["gpt-5.4-nano-xhigh"]).toBe("gpt-5.4-nano");
117127
});
118128
});
119129

@@ -125,7 +135,7 @@ describe("Model Map Module", () => {
125135
expect(getNormalizedModel("gpt-5.3-codex-high")).toBe("gpt-5-codex");
126136
expect(getNormalizedModel("gpt-5.3-codex-spark-high")).toBe("gpt-5-codex");
127137
expect(getNormalizedModel("gpt-5.4-high")).toBe("gpt-5.4");
128-
expect(getNormalizedModel("gpt-5.4-pro-none")).toBe("gpt-5.4-pro");
138+
expect(getNormalizedModel("gpt-5.4-pro-none")).toBeUndefined();
129139
expect(getNormalizedModel("gpt-5.4-pro-high")).toBe("gpt-5.4-pro");
130140
expect(getNormalizedModel("gpt-5.4-2026-03-05-medium")).toBe("gpt-5.4");
131141
expect(getNormalizedModel("gpt-5.4-pro-2026-03-05-medium")).toBe("gpt-5.4-pro");
@@ -170,7 +180,7 @@ describe("Model Map Module", () => {
170180
expect(isKnownModel("gpt-5.3-codex")).toBe(true);
171181
expect(isKnownModel("gpt-5.3-codex-spark")).toBe(true);
172182
expect(isKnownModel("gpt-5.4")).toBe(true);
173-
expect(isKnownModel("gpt-5.4-pro-none")).toBe(true);
183+
expect(isKnownModel("gpt-5.4-pro-none")).toBe(false);
174184
expect(isKnownModel("gpt-5.4-pro")).toBe(true);
175185
expect(isKnownModel("gpt-5.4-mini")).toBe(true);
176186
expect(isKnownModel("gpt-5.4-mini-high")).toBe(true);
@@ -213,6 +223,7 @@ describe("Model Map Module", () => {
213223
"gpt-5.4",
214224
"gpt-5.4-pro",
215225
"gpt-5.4-mini",
226+
"gpt-5.4-nano",
216227
]);
217228

218229
for (const [key, value] of Object.entries(MODEL_MAP)) {

0 commit comments

Comments
 (0)