Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,9 @@ Synthetic-row effort and combo defaults are preserved as the effective input bef
Each selected destination resolves its own pin, then applicable caps and wire normalization.
Compaction requests are exempt. `none` means effort omission and provider-default behavior,
not guaranteed reasoning disablement.
For canonical OpenAI forwarding, the final model's supported effort list takes precedence:
`gpt-6.1-sol` starts at `low`, so an incoming `none` or `minimal` is sent as `low`, including
after a compaction model switch or combo failover.

`GET /api/effort-caps` includes the map. `PUT /api/effort-caps` accepts `modelPinnedEfforts`
alongside the existing caps: omitted fields stay unchanged, `null` clears the map, and a map
Expand Down
16 changes: 8 additions & 8 deletions src/codex/catalog/effort.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,19 +53,20 @@ import {
} from "../runtime";

export function nativeEffortClamp(slug: string, effort: string | undefined): string | null {
if (!effort || (effort !== "max" && effort !== "ultra")) return null;
if (!effort) return null;
if (slug.includes("/")) return null; // routed models map efforts in their adapters
// `ultra` is a client-facing catalog choice (max reasoning plus proactive delegation) that
// codex-rs converts to `max` before any request (core/src/client.rs
// `reasoning_effort_for_request`). The ChatGPT backend rejects it with HTTP 400 even for a
// model whose pinned ladder lists it (#7590), so it is never a native wire value: fold it to
// `max`, then clamp `max` like a direct selection.
const wire = "max";
const wire = effort === "ultra" ? "max" : effort;
const entry = UPSTREAM_NATIVE_ENTRIES.get(slug);
const levels = Array.isArray(entry?.supported_reasoning_levels)
? entry.supported_reasoning_levels as Array<{ effort?: string }>
: [];
if (levels.length === 0) {
if (wire !== "max") return null;
// Not snapshot-covered. gpt-5.6 natives have a REAL max rung (ensureGpt56ReasoningLevels
// restores it even off-snapshot) -> never clamp. Every other bare native (gpt-5.5,
// a retired slug a client still asks for, and future old-ladder slugs)
Expand All @@ -76,12 +77,11 @@ export function nativeEffortClamp(slug: string, effort: string | undefined): str
}
const supported = levels.flatMap(l => typeof l.effort === "string" ? [l.effort] : []);
if (supported.includes(wire)) return effort === wire ? null : wire;
const rank = ["minimal", "low", "medium", "high", "xhigh", "max"];
const highest = supported
.filter(e => rank.includes(e))
.sort((a, b) => rank.indexOf(a) - rank.indexOf(b))
.at(-1);
return highest ?? null;
const rank = ["none", "minimal", "low", "medium", "high", "xhigh", "max", "ultra"];
const requestedRank = rank.indexOf(wire);
if (requestedRank < 0) return null;
const available = supported.filter(e => rank.includes(e)).sort((a, b) => rank.indexOf(a) - rank.indexOf(b));
return available.filter(e => rank.indexOf(e) <= requestedRank).at(-1) ?? available[0] ?? null;
}

export function shouldApplyNativeEffortClamp(
Expand Down
2 changes: 1 addition & 1 deletion structure/catalog.md
Original file line number Diff line number Diff line change
Expand Up @@ -481,7 +481,7 @@ The shared Responses path follows the [bounded multipart recovery contract](suba

Ultra is always advertised in the catalog regardless of the `multi_agent_v2` toggle. The v2 toggle
controls only the multi-agent collab surface, not ultra visibility. The `nativeEffortClamp` function
wire-clamps ultra/max to each model's real top rung (e.g. gpt-5.5 ultra → xhigh on the wire). `ultra` itself is never a native wire value: the ChatGPT backend accepts `none` through `max`, so the clamp folds it to `max` even on rows whose pinned ladder lists it (`gpt-6-astra`, `gpt-6.1-sol`), and `src/server/responses/core-normalize.ts` applies that to the raw body the passthrough serializes, which the parser's options-only fold does not reach (#7590; `tests/responses/responses-native-ultra-effort-wire.test.ts`).
wire-clamps ultra/max to each model's real top rung (e.g. gpt-5.5 ultra → xhigh on the wire). `ultra` itself is never a native wire value: the ChatGPT backend accepts `none` through `max`, so the clamp folds it to `max` even on rows whose pinned ladder lists it (`gpt-6-astra`, `gpt-6.1-sol`), and `src/server/responses/core-normalize.ts` applies that to the raw body the passthrough serializes, which the parser's options-only fold does not reach (#7590; `tests/responses/responses-native-ultra-effort-wire.test.ts`). For canonical OpenAI forwarding, other unsupported efforts map to the nearest published rung at or below the request, or the lowest rung when below the model minimum (`gpt-6.1-sol` `none` → `low`), after final model selection, including combo failover.

For routed models, `modelSuppressSyntheticMax` is a catalog-only per-model setting. A true value
prevents `src/codex/catalog/effort.ts` from adding a missing synthetic `max` and prevents
Expand Down
5 changes: 4 additions & 1 deletion structure/transports/responses-failover.md
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,10 @@ canonical `openai` provider exists (#2901), not when its quota is exhausted. A h
`triggers` the schema would reject disables the whole block instead of widening it, so a
malformed edit can never route more than it names.

The override changes only the model and optional reasoning effort; native forwarding, routed summaries, capability handling, and retry
The override changes only the model and optional reasoning effort. Final-route normalization
checks that effort against the selected canonical native model's published ladder before each
Responses send, including a combo fallback (`gpt-6.1-sol` `none` → `low`). Native forwarding,
routed summaries, capability handling, and retry
budgets stay authoritative, and native compact still removes reasoning before sending. Handoffs carry the override record (with the source
model) as a recursion guard so combo children and fallbacks keep their targets. Overrides bypass shadow interception and conversation
combo recall, and do not publish replacement combo/handoff recall. They never change the conversation's model or compactions outside the
Expand Down
4 changes: 4 additions & 0 deletions tests/codex-integration/codex-catalog-ladders.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,10 @@ describe("catalog ultra (always-on)", () => {
});

describe("mock-max wire clamp (nativeEffortClamp)", () => {
test("native model rejects none and selects its lowest supported effort", () => {
expect(nativeEffortClamp("gpt-6.1-sol", "none")).toBe("low");
});

test("gpt-5.5 max/ultra clamp to its real top rung (xhigh)", () => {
expect(nativeEffortClamp("gpt-5.5", "max")).toBe("xhigh");
expect(nativeEffortClamp("gpt-5.5", "ultra")).toBe("xhigh");
Expand Down
53 changes: 53 additions & 0 deletions tests/responses/responses-compaction-override.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,59 @@ describe("manual compaction config", () => {
});

describe("manual compaction reuses existing handlers", () => {
test.each(["direct", "compaction switch"])("%s sends gpt-6.1-sol's supported low effort", async mode => {
const settings = config();
settings.providers.openai = { adapter: "openai-responses", authMode: "forward", codexAccountMode: "direct", baseUrl: "https://chatgpt.com/backend-api/codex" };
if (mode === "direct") delete settings.compactionRouting;
else {
settings.compactionRouting!.model = "gpt-6.1-sol";
delete settings.compactionRouting!.reasoningEffort;
}
const calls: Array<Record<string, any>> = [];
globalThis.fetch = (async (_url: unknown, init?: RequestInit) => {
const outbound = JSON.parse(String(init?.body));
calls.push(outbound);
return outbound.reasoning?.effort === "none"
? Response.json({ error: { code: "unsupported_value", param: "reasoning.effort" } }, { status: 400 })
: upstreamCompletion(outbound);
}) as typeof fetch;
const input = { ...body(mode !== "direct"), model: mode === "direct" ? "gpt-6.1-sol" : "gateway/normal", reasoning: { effort: "none", summary: "auto" } };
const req = request(input, mode === "direct" ? undefined : "manual");
req.headers.set("authorization", `Bearer ${fakeChatGptJwt({ chatgpt_account_id: "fixture-account" })}`);
const response = await handleResponses(req, settings, { model: "", provider: "" });
await response.text();
expect(response.status).toBe(200);
expect(calls).toHaveLength(1);
expect(calls[0]!.model).toBe("gpt-6.1-sol");
expect(calls[0]!.reasoning).toMatchObject({ effort: "low", summary: "auto" });
});

test("combo failover normalizes the second native target before its first send", async () => {
const settings = config();
delete settings.compactionRouting;
settings.providers.openai = { adapter: "openai-responses", authMode: "forward", codexAccountMode: "direct", baseUrl: "https://chatgpt.com/backend-api/codex" };
settings.combos = { fail: { strategy: "failover", targets: [
{ provider: "gateway", model: "normal" }, { provider: "openai", model: "gpt-6.1-sol" },
] } };
const calls: Array<Record<string, any>> = [];
globalThis.fetch = (async (_url: unknown, init?: RequestInit) => {
const outbound = JSON.parse(String(init?.body));
calls.push(outbound);
if (outbound.model === "normal") return Response.json({ error: { message: "quota", type: "rate_limit_error" } }, { status: 429 });
return outbound.reasoning?.effort === "none"
? Response.json({ error: { code: "unsupported_value", param: "reasoning.effort" } }, { status: 400 })
: upstreamCompletion(outbound);
}) as typeof fetch;
const req = request({ ...body(false), model: "combo/fail", reasoning: { effort: "none", summary: "auto" } });
req.headers.set("authorization", `Bearer ${fakeChatGptJwt({ chatgpt_account_id: "fixture-account" })}`);
const response = await handleResponses(req, settings, { model: "", provider: "" });
await response.text();
expect(response.status).toBe(200);
expect(calls.map(call => [call.model, call.reasoning?.effort])).toEqual([
["normal", "none"], ["gpt-6.1-sol", "low"],
]);
});

test("an ordinary turn carrying manual metadata stays on the conversation model", async () => {
const settings = config();
const calls: string[] = [];
Expand Down
Loading