fix(ai): restricted reasoning context all_turns option to openai models v5.4 and newer
- Guarded the `all_turns` reasoning context value to OpenAI models version 5.4 or greater. - Suppressed `reasoning.context` defaults and explicit overrides when `all_turns` is requested on unsupported models to prevent server rejection. - Introduced `supportsAllTurnsReasoningContext` helper in `@oh-my-pi/pi-catalog/identity` using semver classification. - Updated request-transformer and response-options logic to conditionalize the request payload shaping. - Expanded test coverage to verify correct fallback behavior and explicit overrides across gpt-5.x versions.
This commit is contained in:
@@ -104,7 +104,7 @@ import { transformMessages } from "./transform-messages";
|
||||
export interface OpenAICodexResponsesOptions extends StreamOptions {
|
||||
reasoning?: "none" | "minimal" | "low" | "medium" | "high" | "xhigh";
|
||||
reasoningSummary?: "auto" | "concise" | "detailed" | null;
|
||||
/** `reasoning.context` replay scope; defaults to `all_turns` for every Codex request when unset. */
|
||||
/** `reasoning.context` replay scope; defaults to `all_turns` when unset. The `all_turns` value is gated to gpt-5.4+ Codex models — older ids reject it, so it is suppressed and `context` omitted. */
|
||||
reasoningContext?: CodexReasoningContext;
|
||||
textVerbosity?: "low" | "medium" | "high";
|
||||
include?: string[];
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { Effort } from "@oh-my-pi/pi-catalog/effort";
|
||||
import { supportsAllTurnsReasoningContext } from "@oh-my-pi/pi-catalog/identity";
|
||||
import { requireSupportedEffort } from "@oh-my-pi/pi-catalog/model-thinking";
|
||||
import type { Api, Model } from "../../types";
|
||||
|
||||
@@ -14,7 +15,7 @@ export interface ReasoningConfig {
|
||||
export interface CodexRequestOptions {
|
||||
reasoningEffort?: ReasoningConfig["effort"];
|
||||
reasoningSummary?: ReasoningConfig["summary"] | null;
|
||||
/** Explicit `reasoning.context` override; defaults to `all_turns` for every Codex request when unset. */
|
||||
/** Explicit `reasoning.context` override; defaults to `all_turns` when unset. The `all_turns` value is gated to gpt-5.4+ Codex models — older ids reject it, so it is suppressed and `context` omitted. */
|
||||
reasoningContext?: CodexReasoningContext;
|
||||
textVerbosity?: "low" | "medium" | "high";
|
||||
include?: string[];
|
||||
@@ -254,9 +255,21 @@ export async function transformRequestBody(
|
||||
...body.reasoning,
|
||||
...reasoningConfig,
|
||||
};
|
||||
// Default reasoning replay to `all_turns` for every Codex request,
|
||||
// mirroring codex-rs; an explicit `reasoningContext` overrides it.
|
||||
body.reasoning.context = options.reasoningContext ?? "all_turns";
|
||||
// Default reasoning replay to `all_turns`, mirroring codex-rs; an
|
||||
// explicit `reasoningContext` overrides the default. The `all_turns`
|
||||
// value is only accepted from gpt-5.4 onward — earlier Codex ids
|
||||
// (gpt-5.1-codex, gpt-5.3-codex, gpt-5.3-codex-spark) reject it with
|
||||
// "Unsupported value: 'all_turns' is not supported with this model".
|
||||
// For those, drop `context` so the server applies its `current_turn`
|
||||
// default. The version gate is authoritative: even an explicit
|
||||
// `all_turns` override is suppressed on unsupported models, while
|
||||
// `current_turn`/`auto` (universally supported) always pass through.
|
||||
const context = options.reasoningContext ?? "all_turns";
|
||||
if (context === "all_turns" && !supportsAllTurnsReasoningContext(model.id)) {
|
||||
delete body.reasoning.context;
|
||||
} else {
|
||||
body.reasoning.context = context;
|
||||
}
|
||||
} else {
|
||||
delete body.reasoning;
|
||||
}
|
||||
|
||||
@@ -83,21 +83,21 @@ function createCodexFetchMock(sse: string, onRequest: (captured: CapturedCodexRe
|
||||
}
|
||||
|
||||
describe("openai-codex reasoning.context", () => {
|
||||
it("forwards an explicit reasoning.context and defaults to all_turns", async () => {
|
||||
const model = createCodexModel("gpt-5.1-codex");
|
||||
it("defaults to all_turns on gpt-5.4+ models and forwards explicit overrides", async () => {
|
||||
const model = createCodexModel("gpt-5.4");
|
||||
|
||||
const defaulted = await transformRequestBody({ model: model.id }, model, { reasoningEffort: "medium" });
|
||||
expect(defaulted.reasoning?.context).toBe("all_turns");
|
||||
|
||||
const explicit = await transformRequestBody({ model: model.id }, model, {
|
||||
reasoningEffort: "medium",
|
||||
reasoningContext: "current_turn",
|
||||
});
|
||||
expect(explicit.reasoning?.context).toBe("current_turn");
|
||||
|
||||
const defaulted = await transformRequestBody({ model: model.id }, model, { reasoningEffort: "medium" });
|
||||
expect(defaulted.reasoning?.context).toBe("all_turns");
|
||||
});
|
||||
|
||||
it("defaults reasoning.context to all_turns under Responses Lite unless overridden", async () => {
|
||||
const model = createCodexModel("gpt-5.1-codex");
|
||||
it("keeps the all_turns default for the lite transport on supported models", async () => {
|
||||
const model = createCodexModel("gpt-5.5");
|
||||
|
||||
const lite = await transformRequestBody({ model: model.id }, model, {
|
||||
reasoningEffort: "medium",
|
||||
@@ -112,6 +112,39 @@ describe("openai-codex reasoning.context", () => {
|
||||
});
|
||||
expect(overridden.reasoning?.context).toBe("auto");
|
||||
});
|
||||
|
||||
// gpt-5.1-codex / gpt-5.3-codex / gpt-5.3-codex-spark reject `all_turns`
|
||||
// ("Unsupported value: 'all_turns' is not supported with this model").
|
||||
it.each([
|
||||
"gpt-5.1-codex",
|
||||
"gpt-5.3-codex",
|
||||
"gpt-5.3-codex-spark",
|
||||
])("omits the all_turns default for pre-5.4 model %s", async modelId => {
|
||||
const model = createCodexModel(modelId);
|
||||
|
||||
const defaulted = await transformRequestBody({ model: model.id }, model, { reasoningEffort: "medium" });
|
||||
expect(defaulted.reasoning).toBeDefined();
|
||||
expect(defaulted.reasoning?.context).toBeUndefined();
|
||||
expect("context" in (defaulted.reasoning ?? {})).toBe(false);
|
||||
|
||||
// A supported override (current_turn/auto) is still honored.
|
||||
const overridden = await transformRequestBody({ model: model.id }, model, {
|
||||
reasoningEffort: "medium",
|
||||
reasoningContext: "current_turn",
|
||||
});
|
||||
expect(overridden.reasoning?.context).toBe("current_turn");
|
||||
});
|
||||
|
||||
it("suppresses an explicit all_turns override on a pre-5.4 model", async () => {
|
||||
const model = createCodexModel("gpt-5.3-codex-spark");
|
||||
|
||||
const forced = await transformRequestBody({ model: model.id }, model, {
|
||||
reasoningEffort: "medium",
|
||||
reasoningContext: "all_turns",
|
||||
});
|
||||
expect(forced.reasoning).toBeDefined();
|
||||
expect(forced.reasoning?.context).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("openai-codex Responses Lite input shaping", () => {
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
parseAnthropicModel,
|
||||
parseGlmModel,
|
||||
parseKnownModel,
|
||||
parseOpenAIModel,
|
||||
semverGte,
|
||||
} from "./classify";
|
||||
|
||||
@@ -121,6 +122,22 @@ export const isOpenAIModelId = memo((modelId: string): boolean => {
|
||||
return /(^|\/)(gpt|o1|o3|o4)[-.]/i.test(modelId) || modelId.toLowerCase().includes("openai/");
|
||||
});
|
||||
|
||||
/**
|
||||
* OpenAI Codex models that honor `reasoning.context: "all_turns"` (full
|
||||
* cross-turn reasoning replay). The `reasoning.context` field itself exists for
|
||||
* the whole gpt-5/o-series family, but the `all_turns` value is only accepted
|
||||
* from gpt-5.4 onward; earlier ids (`gpt-5.1-codex`, `gpt-5.3-codex`, and
|
||||
* `gpt-5.3-codex-spark`) reject it with
|
||||
* `Unsupported value: 'all_turns' is not supported with this model`. Version
|
||||
* floor (not an allowlist) so 5.6/6.x inherit support automatically. Callers
|
||||
* fall back to omitting `context`, letting the server default to `current_turn`.
|
||||
*/
|
||||
export const supportsAllTurnsReasoningContext = memo((modelId: string): boolean => {
|
||||
const parsed = parseOpenAIModel(bareModelId(modelId));
|
||||
if (!parsed) return false;
|
||||
return semverGte(parsed.version, "5.4");
|
||||
});
|
||||
|
||||
/**
|
||||
* Reasoning-capable GLM coding SKUs: glm-4.5 and up on the base / `-air` /
|
||||
* `-turbo` lines. Excludes the vision (`…v`) shape, the non-reasoning
|
||||
|
||||
Reference in New Issue
Block a user