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:
can1357
2026-06-30 06:42:46 +02:00
parent bebdd22e64
commit 093660f8b7
4 changed files with 75 additions and 12 deletions
@@ -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", () => {
+17
View File
@@ -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