From 6ffa47a9268ed0e4bc99842b4558f3befa56fc63 Mon Sep 17 00:00:00 2001 From: Elginux Agent Date: Tue, 8 Sep 2026 19:39:27 +0200 Subject: [PATCH 1/4] feat(combos): allow forced default reasoning effort --- docs-site/src/content/docs/guides/combos.md | 21 +++++++------- .../docs/reference/configuration/routing.md | 3 +- gui/src/combo-workspace-data.ts | 11 +++++++ src/cli/combo.ts | 9 +++++- src/combos/request.ts | 22 ++++++++------ src/combos/types.ts | 20 ++++++++++++- src/server/management/combo-routes.ts | 6 +++- src/server/responses/core.ts | 16 ++++++++++ src/types.ts | 1 + src/types/config.ts | 3 ++ tests/cli/cli-headless-parity.test.ts | 11 +++++++ tests/codex-integration/combos.test.ts | 23 +++++++++++++++ tests/gui/combo-workspace-data.test.ts | 15 ++++++++++ tests/routing/combo-management-api.test.ts | 29 +++++++++++++++++++ .../server/server-combo-failover-e2e.test.ts | 22 ++++++++++++++ 15 files changed, 188 insertions(+), 24 deletions(-) diff --git a/docs-site/src/content/docs/guides/combos.md b/docs-site/src/content/docs/guides/combos.md index db94da045f..3f96e682d3 100644 --- a/docs-site/src/content/docs/guides/combos.md +++ b/docs-site/src/content/docs/guides/combos.md @@ -254,20 +254,18 @@ instead of growing memory without a bound. ## Default reasoning effort -`defaultEffort` supplies `reasoning.effort` only when all of these are true: +`defaultEffort` supplies `reasoning.effort` when the combo has a non-null default and the selected target advertises a compatible effort. By default, `defaultEffortMode: "fallback"` preserves an explicit caller effort. Set `defaultEffortMode: "force"` to make the operator-configured default override a valid caller effort (for example, caller `medium` becomes configured `max`). Force mode can increase cost and latency, is available only through combo configuration/management, and is rejected without `defaultEffort`. -1. the combo has a non-null default; -2. the caller did not set an effort; and -3. the selected target's catalog advertises that exact effort. +Resolution remains capability-safe: the configured effort is lowered to the target's highest compatible rung, an explicitly unsupported target receives no effort control, and an unknown capability never causes an override or injection. Malformed caller effort is not repaired into a valid expensive request. If the request has no `reasoning` object, opencodex creates one. If `reasoning` exists without an -`effort` property, it preserves the other fields and adds the default. A caller-provided effort is -never overwritten. +`effort` property, it preserves the other fields and adds the default. A valid caller-provided effort +is overwritten only in explicit `force` mode. -When target capability is unknown or does not include the configured effort, opencodex omits the -default and leaves the target's own behavior unchanged. Supported values are `low`, `medium`, -`high`, `xhigh`, `max`, and `ultra`; omit the field or set it to `null` to leave effort entirely to -the caller and target. +When target capability is unknown, opencodex leaves the request unchanged. When the target +explicitly advertises no effort control, opencodex omits the effort. Supported values are `low`, +`medium`, `high`, `xhigh`, `max`, and `ultra`; omit `defaultEffort` or set it to `null` to leave effort +entirely to the caller and target. ### Mixed-capability groups (`reasoningEffortMode`) @@ -414,7 +412,8 @@ Combos are stored in the top-level `combos` object, keyed by combo id: | `stickyLimit` | No | `1` | Integer from 1 to 100 successful requests per round-robin selection. Applies only to round-robin. | | `cooldownMs` | No | unset → upstream fallback (5 s for request-rate 429 codes `1302`/`1305`, otherwise 60 s) | Integer from 1 to 600000. When set, applies as the per-target cooldown whenever no usable upstream `Retry-After` or Codex reset signal exists, including request-rate 429s; when unset, uses the upstream fallback. | | `waitForCooldownMs` | No | `0` | Integer from 0 to 600000. Maximum time to wait for the earliest eligible cooling target before returning `combo_unavailable`; abort cancels the wait. | -| `defaultEffort` | No | `null` | `low`, `medium`, `high`, `xhigh`, `max`, or `ultra`; applied only when the caller omits effort and the target advertises support. | +| `defaultEffort` | No | `null` | `low`, `medium`, `high`, `xhigh`, `max`, or `ultra`; resolved against each target's advertised ladder. | +| `defaultEffortMode` | No | `"fallback"` | `"fallback"` preserves an explicit caller effort. `"force"` overrides valid caller effort with `defaultEffort` and requires a non-null default; it can increase cost and latency. | | `reasoningEffortMode` | No | `"strict"` | `"strict"` intersects every known target ladder, so one target advertising no effort control empties the combo's picker. `"adaptive"` excludes those empty ladders from the published intersection. Metadata only; dispatch is unchanged. | | `imageInput` | No | `"auto"` | `"auto"` or `"disabled"`. `"auto"` publishes image support only when every target supports images; `"disabled"` forces text-only (drops image from published modalities and rejects image-bearing requests before dispatch). | | `alias` | No | none | Optional trimmed public model id; use the alias rules above. An empty value is stored as no alias. | diff --git a/docs-site/src/content/docs/reference/configuration/routing.md b/docs-site/src/content/docs/reference/configuration/routing.md index 25bd32f64e..2ee1ae2b71 100644 --- a/docs-site/src/content/docs/reference/configuration/routing.md +++ b/docs-site/src/content/docs/reference/configuration/routing.md @@ -90,7 +90,8 @@ namespace, and cannot use reserved bare native families such as `gpt-*`, `o1-*`, | `stickyLimit?` | `number` | `1` | Successful requests retained in one round-robin batch. Range 1–100. Applies only to round-robin. | | `cooldownMs?` | `number` | unset → upstream fallback (5 s for request-rate 429 codes `1302`/`1305`, otherwise 60 s) | Range 1–600000. When set, applies whenever no usable upstream `Retry-After` or Codex reset signal exists, including request-rate 429s; when unset, uses the upstream fallback. Upstream signals take precedence and all cooldowns are capped at 10 minutes. | | `waitForCooldownMs?` | `number` | `0` | Maximum wait for the earliest eligible cooling target on each selection attempt before returning `combo_unavailable`. Range 0–600000; an abort cancels the wait. | -| `defaultEffort?` | `"low" \| "medium" \| "high" \| "xhigh" \| "max" \| "ultra" \| null` | unset | Applied only when the caller omits effort and the selected target advertises the requested rung. | +| `defaultEffort?` | `"low" \| "medium" \| "high" \| "xhigh" \| "max" \| "ultra" \| null` | unset | Resolved against each selected target's advertised effort ladder. | +| `defaultEffortMode?` | `"fallback" \| "force"` | `"fallback"` | `"fallback"` preserves explicit caller effort. `"force"` overrides valid caller effort with `defaultEffort`, requires a non-null default, and can increase cost and latency. Unknown target capability fails closed. | | `reasoningEffortMode?` | `"strict" \| "adaptive"` | `"strict"` | `"strict"` intersects every known target effort ladder, so a target advertising no effort control empties the combo's picker. `"adaptive"` excludes those empty ladders from the published intersection. Picker metadata only; target selection and dispatch are unchanged. | | `imageInput?` | `"auto" \| "disabled"` | `"auto"` | `"auto"` publishes image only when every target supports images; `"disabled"` forces text-only (drops image from published modalities and rejects image-bearing requests before dispatch). | | `alias?` | `string` | — | Optional public model id in place of the canonical picker slug. | diff --git a/gui/src/combo-workspace-data.ts b/gui/src/combo-workspace-data.ts index bf8b881c55..719c0661f8 100644 --- a/gui/src/combo-workspace-data.ts +++ b/gui/src/combo-workspace-data.ts @@ -114,6 +114,10 @@ function normalizeReasoningEffortMode(value: unknown): "strict" | "adaptive" { return value === "adaptive" ? "adaptive" : "strict"; } +function normalizeDefaultEffortMode(value: unknown): "fallback" | "force" { + return value === "force" ? "force" : "fallback"; +} + export interface ComboItem { id: string; /** Wire id shown to clients, e.g. combo/free */ @@ -127,6 +131,8 @@ export interface ComboItem { strategy: ComboStrategy; stickyLimit: number; defaultEffort: ComboEffort | null; + /** Operator policy; force overrides valid client effort. */ + defaultEffortMode?: "fallback" | "force"; imageInput?: "auto" | "disabled"; /** * Picker-ladder policy. `adaptive` lets targets that advertise no effort control drop @@ -240,6 +246,7 @@ export function parseComboList(payload: unknown): ComboItem[] { strategy: normalizeStrategy(r.strategy), stickyLimit: normalizeStickyLimit(r.stickyLimit), defaultEffort: normalizeDefaultEffort(r.defaultEffort), + defaultEffortMode: normalizeDefaultEffortMode(r.defaultEffortMode), imageInput: normalizeImageInput(r.imageInput), reasoningEffortMode: normalizeReasoningEffortMode(r.reasoningEffortMode), targets, @@ -498,6 +505,7 @@ export function draftEquals(a: ComboItem, b: ComboItem): boolean { || a.strategy !== b.strategy || a.stickyLimit !== b.stickyLimit || a.defaultEffort !== b.defaultEffort + || (a.defaultEffortMode ?? "fallback") !== (b.defaultEffortMode ?? "fallback") || (a.imageInput ?? "auto") !== (b.imageInput ?? "auto") || (a.reasoningEffortMode ?? "strict") !== (b.reasoningEffortMode ?? "strict") ) return false; @@ -516,6 +524,7 @@ export function toPutBody(item: ComboItem, options: { renameFrom?: string } = {} strategy: ComboStrategy; stickyLimit?: number; defaultEffort: ComboEffort | null; + defaultEffortMode?: "force"; imageInput?: "disabled"; reasoningEffortMode?: "adaptive"; alias?: string; @@ -533,6 +542,7 @@ export function toPutBody(item: ComboItem, options: { renameFrom?: string } = {} : { provider: target.provider.trim(), model: target.model.trim() }), strategy: item.strategy, defaultEffort: item.defaultEffort, + ...(item.defaultEffortMode === "force" ? { defaultEffortMode: "force" as const } : {}), ...(item.imageInput === "disabled" ? { imageInput: "disabled" as const } : {}), ...(item.reasoningEffortMode === "adaptive" ? { reasoningEffortMode: "adaptive" as const } : {}), ...(item.strategy === "round-robin" ? { stickyLimit: item.stickyLimit } : {}), @@ -643,6 +653,7 @@ export function emptyDraft(id = ""): ComboItem { strategy: "failover", stickyLimit: 1, defaultEffort: null, + defaultEffortMode: "fallback", imageInput: "auto", reasoningEffortMode: "strict", targets: [newComboTarget()], diff --git a/src/cli/combo.ts b/src/cli/combo.ts index 3e0aa0d0bf..c6a1771946 100644 --- a/src/cli/combo.ts +++ b/src/cli/combo.ts @@ -15,7 +15,8 @@ const USAGE = `Usage: ocx combo show [--json] ocx combo set --targets [--strategy ] [--sticky <1-100>] - [--effort ] [--alias ] + [--effort ] [--effort-mode ] + (force overrides valid client effort and can increase cost/latency) [--alias ] [--native-alias] [--display-name ] [--rename-from ] [--json] ocx combo remove --yes [--json]`; @@ -80,6 +81,10 @@ async function set(argv: string[], deps: RuntimeApiDeps): Promise { if (strategy !== "round-robin") throw new CliUsageError("--sticky applies only to round-robin", USAGE); } const effort = takeOption(args, "--effort"); + const effortMode = takeOption(args, "--effort-mode"); + if (effortMode !== undefined && effortMode !== "fallback" && effortMode !== "force") { + throw new CliUsageError("--effort-mode must be fallback or force", USAGE); + } const alias = takeOption(args, "--alias"); const nativeAlias = takeFlag(args, "--native-alias"); const displayName = takeOption(args, "--display-name"); @@ -91,12 +96,14 @@ async function set(argv: string[], deps: RuntimeApiDeps): Promise { targets: parseTargets(targetsRaw), }; if (effort !== undefined) combo.defaultEffort = effort === "-" ? null : effort; + if (effortMode !== undefined) combo.defaultEffortMode = effortMode; if (alias !== undefined) combo.alias = alias === "-" ? "" : alias; if (nativeAlias) combo.nativeAlias = true; if (displayName !== undefined) combo.displayName = displayName === "-" ? "" : displayName; const current = await runtimeRequest<{ combos?: ComboRow[] }>("/api/combos", {}, deps); const existing = (current.combos ?? []).find(row => row.id === (renameFrom ?? id)); if (existing?.imageInput === "disabled") combo.imageInput = "disabled"; + if (effortMode === undefined && existing?.defaultEffortMode === "force") combo.defaultEffortMode = "force"; const result = await runtimeRequest("/api/combos", { method: "PUT", body: JSON.stringify({ id, combo, ...(renameFrom ? { renameFrom } : {}) }), diff --git a/src/combos/request.ts b/src/combos/request.ts index abafccc525..ab2ca0b076 100644 --- a/src/combos/request.ts +++ b/src/combos/request.ts @@ -1,5 +1,5 @@ -import type { OcxComboDefaultEffort, OcxComboTarget, OcxConfig } from "../types"; -import { resolveEffortAtOrBelow } from "../reasoning-effort"; +import type { OcxComboDefaultEffort, OcxComboDefaultEffortMode, OcxComboTarget, OcxConfig } from "../types"; +import { isCodexReasoningEffort, resolveEffortAtOrBelow } from "../reasoning-effort"; import { resolveComboId } from "./types"; const warnedUnsupportedDefaults = new Set(); @@ -59,18 +59,22 @@ export function concreteComboRequestBody( target: Pick, defaultEffort: OcxComboDefaultEffort | null, targetReasoningEfforts: readonly string[] | undefined, + defaultEffortMode: OcxComboDefaultEffortMode = "fallback", ): Record { const clone = structuredClone(body) as Record; clone.model = `${target.provider}/${target.model}`; if (!defaultEffort) return clone; const reasoning = clone.reasoning; - const needsDefault = reasoning === undefined || ( - reasoning - && typeof reasoning === "object" - && !Array.isArray(reasoning) - && !Object.prototype.hasOwnProperty.call(reasoning, "effort") - ); - if (!needsDefault) return clone; + const reasoningRecord = reasoning && typeof reasoning === "object" && !Array.isArray(reasoning) + ? reasoning as Record + : undefined; + const hasEffort = reasoningRecord !== undefined + && Object.prototype.hasOwnProperty.call(reasoningRecord, "effort"); + const callerEffort = reasoningRecord?.effort; + const validCallerEffort = typeof callerEffort === "string" && isCodexReasoningEffort(callerEffort); + const needsDefault = reasoning === undefined || (reasoningRecord !== undefined && !hasEffort); + const shouldForce = defaultEffortMode === "force" && validCallerEffort; + if (!needsDefault && !shouldForce) return clone; // Picker availability treats an unknown ladder as a wildcard, but runtime // injection stays fail-closed until this concrete target advertises support. // diff --git a/src/combos/types.ts b/src/combos/types.ts index b5c5bf697c..5bd2b8553e 100644 --- a/src/combos/types.ts +++ b/src/combos/types.ts @@ -1,6 +1,6 @@ import { isCodexReasoningEffort } from "../reasoning-effort"; import { SUPPORTED_NATIVE_OPENAI_SLUGS } from "../codex/catalog/native-models"; -import type { OcxComboConfig, OcxComboDefaultEffort, OcxComboReasoningEffortMode, OcxComboStrategy, OcxComboTarget, OcxProviderConfig } from "../types"; +import type { OcxComboConfig, OcxComboDefaultEffort, OcxComboDefaultEffortMode, OcxComboReasoningEffortMode, OcxComboStrategy, OcxComboTarget, OcxProviderConfig } from "../types"; import { COMBO_NAMESPACE, isValidComboId, targetKey } from "./identifiers"; export const COMBO_DEFAULT_WAIT_FOR_COOLDOWN_MS = 0; @@ -26,6 +26,8 @@ export interface NormalizedComboConfig { cooldownMs?: number; waitForCooldownMs: number; defaultEffort: OcxComboDefaultEffort | null; + /** Client-precedence policy; `fallback` preserves legacy behavior. */ + defaultEffortMode: OcxComboDefaultEffortMode; /** Picker-ladder derivation policy; `strict` preserves the legacy intersection rule. */ reasoningEffortMode: OcxComboReasoningEffortMode; /** Disable image input; `auto` preserves the intersection derived from all targets. */ @@ -167,6 +169,21 @@ export function comboConfigIssues( message: "defaultEffort must be one of: low, medium, high, xhigh, max, ultra", }); } + if (body.defaultEffortMode !== undefined + && body.defaultEffortMode !== "fallback" + && body.defaultEffortMode !== "force") { + issues.push({ + path: ["defaultEffortMode"], + message: 'defaultEffortMode must be "fallback" or "force"', + }); + } + if (body.defaultEffortMode === "force" + && (typeof body.defaultEffort !== "string" || !isCodexReasoningEffort(body.defaultEffort))) { + issues.push({ + path: ["defaultEffort"], + message: "defaultEffort is required when defaultEffortMode is force", + }); + } if (body.imageInput !== undefined && body.imageInput !== "auto" && body.imageInput !== "disabled") { issues.push({ path: ["imageInput"], message: 'imageInput must be "auto" or "disabled"' }); } @@ -299,6 +316,7 @@ export function normalizeComboConfig(raw: OcxComboConfig): NormalizedComboConfig cooldownMs: raw.cooldownMs, waitForCooldownMs: raw.waitForCooldownMs ?? COMBO_DEFAULT_WAIT_FOR_COOLDOWN_MS, defaultEffort: raw.defaultEffort ?? null, + defaultEffortMode: raw.defaultEffortMode === "force" ? "force" : "fallback", reasoningEffortMode: raw.reasoningEffortMode === "adaptive" ? "adaptive" : "strict", imageInput: raw.imageInput === "disabled" ? "disabled" : "auto", alias: alias || null, diff --git a/src/server/management/combo-routes.ts b/src/server/management/combo-routes.ts index 475e72db41..d61e7faf5c 100644 --- a/src/server/management/combo-routes.ts +++ b/src/server/management/combo-routes.ts @@ -80,17 +80,20 @@ function sparseComboConfig(combo: T): Omit & { + defaultEffortMode?: "fallback" | "force"; +}>(combo: T): Omit & { cooldownMs?: number; waitForCooldownMs?: number; imageInput?: "disabled"; reasoningEffortMode?: "adaptive"; + defaultEffortMode?: "force"; } { const { cooldownMs, waitForCooldownMs, imageInput, reasoningEffortMode, + defaultEffortMode, ...rest } = combo; return { @@ -101,6 +104,7 @@ function sparseComboConfig { if (attemptRetained) return; @@ -2781,6 +2793,10 @@ export async function handleComboResponses( onNativePassthroughCancel: callbackGate.onCancel, onResponseComplete: callbackGate.onResponseComplete, }); + if (originalRequestedEffort !== undefined) { + childLog.requestedEffort = originalRequestedEffort; + attempt.requestedEffort = originalRequestedEffort; + } } catch (error) { callbackGate.discard(); if (options.abortSignal?.aborted) { diff --git a/src/types.ts b/src/types.ts index f759406fe0..d13861e011 100644 --- a/src/types.ts +++ b/src/types.ts @@ -74,6 +74,7 @@ export type { OcxAccountPoolQuotaWindow, OcxComboStrategy, OcxComboDefaultEffort, + OcxComboDefaultEffortMode, OcxComboReasoningEffortMode, OcxComboTarget, OcxComboConfig, diff --git a/src/types/config.ts b/src/types/config.ts index fc9a55a8fa..a06ffb6fcd 100644 --- a/src/types/config.ts +++ b/src/types/config.ts @@ -853,6 +853,7 @@ export type OcxAccountPoolQuotaWindow = "five-hour" | "weekly" | "max-utilizatio export type OcxComboStrategy = "failover" | "round-robin" | "random" | "least-used" | "reset-window"; export type OcxComboDefaultEffort = "low" | "medium" | "high" | "xhigh" | "max" | "ultra"; +export type OcxComboDefaultEffortMode = "fallback" | "force"; /** * How a combo derives the reasoning ladder it publishes to the picker. @@ -889,6 +890,8 @@ export interface OcxComboConfig { waitForCooldownMs?: number; /** Used when the client omits reasoning.effort. null/omitted leaves the target default unchanged. */ defaultEffort?: OcxComboDefaultEffort | null; + /** `force` makes the combo default override a valid client effort. Omitted / `fallback` preserves client precedence. */ + defaultEffortMode?: OcxComboDefaultEffortMode; /** * Picker-ladder derivation policy. Omitted / `"strict"` keeps the legacy rule where an * explicitly empty target ladder suppresses the whole combo's effort control. diff --git a/tests/cli/cli-headless-parity.test.ts b/tests/cli/cli-headless-parity.test.ts index 78f0cc04a1..9334bbc945 100644 --- a/tests/cli/cli-headless-parity.test.ts +++ b/tests/cli/cli-headless-parity.test.ts @@ -497,6 +497,17 @@ describe("headless GUI parity CLI", () => { }); }); + test("combo set exposes the opt-in force-default policy", async () => { + const runtime = fakeRuntime(); + expect(await handleComboCommand([ + "set", "deep", "--targets", "ark/model-a", "--effort", "max", "--effort-mode", "force", "--json", + ], runtime.deps)).toBe(0); + expect(runtime.requests.find(request => request.method === "PUT")?.body).toMatchObject({ + id: "deep", + combo: { defaultEffort: "max", defaultEffortMode: "force" }, + }); + }); + test("combo set rejects --sticky outside round-robin instead of dropping it", async () => { const runtime = fakeRuntime(); const errorSpy = spyOn(console, "error").mockImplementation(() => {}); diff --git a/tests/codex-integration/combos.test.ts b/tests/codex-integration/combos.test.ts index 98174c3848..87c86a5a8c 100644 --- a/tests/codex-integration/combos.test.ts +++ b/tests/codex-integration/combos.test.ts @@ -321,6 +321,27 @@ describe("combo request cloning", () => { expect(concreteComboRequestBody({ model: "combo/x" }, target, "high", undefined).reasoning).toBeUndefined(); }); + test("force mode overrides only valid caller effort and resolves independently per target", () => { + const raw = { model: "combo/x", reasoning: { effort: "medium", summary: "concise" } }; + expect(concreteComboRequestBody(raw, target, "max", ["low", "high", "max"], "force").reasoning) + .toEqual({ effort: "max", summary: "concise" }); + expect(concreteComboRequestBody(raw, target, "max", ["low", "high"], "force").reasoning) + .toEqual({ effort: "high", summary: "concise" }); + expect(raw.reasoning).toEqual({ effort: "medium", summary: "concise" }); + }); + + test("force mode fails closed for malformed and unknown capabilities and strips unsupported effort", () => { + expect(concreteComboRequestBody( + { model: "combo/x", reasoning: { effort: "banana" } }, target, "max", ["max"], "force", + ).reasoning).toEqual({ effort: "banana" }); + expect(concreteComboRequestBody( + { model: "combo/x", reasoning: { effort: "medium" } }, target, "max", undefined, "force", + ).reasoning).toEqual({ effort: "medium" }); + expect(concreteComboRequestBody( + { model: "combo/x" }, target, "max", [], "force", + ).reasoning).toBeUndefined(); + }); + /** * #3108: a combo configured for `max` routed to a target whose ladder tops out lower * sent NO effort at all, so the provider default applied and the turn ran at `none` — @@ -1409,8 +1430,10 @@ describe("combo validation and normalization", () => { })).toEqual({ strategy: "failover", stickyLimit: 1, + cooldownMs: undefined, waitForCooldownMs: 0, defaultEffort: "high", + defaultEffortMode: "fallback", reasoningEffortMode: "strict", imageInput: "auto", alias: null, diff --git a/tests/gui/combo-workspace-data.test.ts b/tests/gui/combo-workspace-data.test.ts index e3d340f2f8..67ac1882c5 100644 --- a/tests/gui/combo-workspace-data.test.ts +++ b/tests/gui/combo-workspace-data.test.ts @@ -113,6 +113,7 @@ describe("combo-workspace-data", () => { strategy: "failover", stickyLimit: 1, defaultEffort: null, + defaultEffortMode: "fallback", imageInput: "auto", reasoningEffortMode: "strict", targets: [{ provider: "a", model: "m1", weight: 1, clientKey: expect.stringMatching(/^ct-\d+$/) }], @@ -126,6 +127,7 @@ describe("combo-workspace-data", () => { strategy: "round-robin", stickyLimit: 4, defaultEffort: "high", + defaultEffortMode: "fallback", imageInput: "auto", reasoningEffortMode: "strict", targets: [ @@ -269,6 +271,19 @@ describe("combo-workspace-data", () => { expect(draftEquals(combo({ reasoningEffortMode: "strict" }), combo())).toBe(true); }); + test("defaultEffortMode survives parse and serialize while fallback stays sparse", () => { + const forced = parseComboList({ combos: [{ + id: "forced", + defaultEffort: "max", + defaultEffortMode: "force", + targets: [{ provider: "a", model: "m1" }], + }] })[0]!; + expect(forced.defaultEffortMode).toBe("force"); + expect(toPutBody(forced).combo.defaultEffortMode).toBe("force"); + expect(toPutBody(combo()).combo).not.toHaveProperty("defaultEffortMode"); + expect(draftEquals(combo(), combo({ defaultEffortMode: "force" }))).toBe(false); + }); + test("attention flags zero-target and one-target defensive rows", () => { const attention = buildComboAttention([ combo({ id: "empty", model: "combo/empty", targets: [] }), diff --git a/tests/routing/combo-management-api.test.ts b/tests/routing/combo-management-api.test.ts index 85f6be0ff6..8877bf5dcf 100644 --- a/tests/routing/combo-management-api.test.ts +++ b/tests/routing/combo-management-api.test.ts @@ -488,6 +488,35 @@ describe("combo management API", () => { }); }); + test("defaultEffortMode force round-trips sparsely and invalid policy never mutates config", async () => { + await withTempHome(async () => { + const config = baseConfig({ combos: undefined }); + saveConfig(config); + const forced = await comboApi(config, "PUT", "/api/combos", { + id: "forced", + combo: { ...VALID_COMBO, defaultEffort: "max", defaultEffortMode: "force" }, + }); + expect(forced?.status).toBe(200); + expect(await responseJson(forced)).toMatchObject({ + combo: { defaultEffort: "max", defaultEffortMode: "force" }, + }); + expect(config.combos?.forced).toMatchObject({ defaultEffort: "max", defaultEffortMode: "force" }); + + const missingDefault = await comboApi(config, "PUT", "/api/combos", { + id: "bad", combo: { ...VALID_COMBO, defaultEffortMode: "force" }, + }); + expect(missingDefault?.status).toBe(400); + expect(config.combos?.bad).toBeUndefined(); + + const fallback = await comboApi(config, "PUT", "/api/combos", { + id: "forced", + combo: { ...VALID_COMBO, defaultEffort: "max", defaultEffortMode: "fallback" }, + }); + expect(fallback?.status).toBe(200); + expect(config.combos?.forced).not.toHaveProperty("defaultEffortMode"); + }); + }); + test("PUT stores aliases and GET exposes the public model", async () => { await withTempHome(async () => { const config = baseConfig({ combos: undefined }); diff --git a/tests/server/server-combo-failover-e2e.test.ts b/tests/server/server-combo-failover-e2e.test.ts index c08c706bac..765b01020b 100644 --- a/tests/server/server-combo-failover-e2e.test.ts +++ b/tests/server/server-combo-failover-e2e.test.ts @@ -2874,6 +2874,28 @@ describe("server combo failover 030 activation matrix", () => { expect(bodies.map(row => row.body.reasoning_effort)).toEqual(["low", "low"]); }); + test("force-default raises Hermes-like medium to max while fallback keeps medium", async () => { + const efforts: unknown[] = []; + const upstream = serve(async request => { + const body = await request.json() as Record; + efforts.push(body.reasoning_effort); + return chatSuccess("forced", "m1"); + }); + const providers = { + a: provider("openai-chat", baseUrl(upstream), "key-a", { + reasoningEfforts: ["low", "medium", "high", "max"], + }), + }; + const forced = comboConfig(providers, undefined, { + defaultEffort: "max", + defaultEffortMode: "force", + }); + expect((await post(forced, { reasoning: { effort: "medium" } })).status).toBe(200); + const fallback = comboConfig(providers, undefined, { defaultEffort: "max" }); + expect((await post(fallback, { reasoning: { effort: "medium" } })).status).toBe(200); + expect(efforts).toEqual(["max", "medium"]); + }); + test("backup noReasoningModels removes the fresh combo default", async () => { const a = serve(() => Response.json({ error: { message: "retry" } }, { status: 503 })); let backupBody: Record | undefined; From 9e34da464824b466517a80a7da3384313bcbe0b9 Mon Sep 17 00:00:00 2001 From: Elginux Agent Date: Tue, 8 Sep 2026 21:58:42 +0200 Subject: [PATCH 2/4] fix(combos): fail closed on invalid forced effort --- src/cli/combo.ts | 4 +++- src/combos/request.ts | 5 ++++- src/combos/types.ts | 7 ++++-- src/types/config.ts | 2 +- tests/cli/cli-headless-parity.test.ts | 31 ++++++++++++++++++++++++++ tests/codex-integration/combos.test.ts | 24 +++++++++++++++++++- 6 files changed, 67 insertions(+), 6 deletions(-) diff --git a/src/cli/combo.ts b/src/cli/combo.ts index c6a1771946..380bcd36ff 100644 --- a/src/cli/combo.ts +++ b/src/cli/combo.ts @@ -103,7 +103,9 @@ async function set(argv: string[], deps: RuntimeApiDeps): Promise { const current = await runtimeRequest<{ combos?: ComboRow[] }>("/api/combos", {}, deps); const existing = (current.combos ?? []).find(row => row.id === (renameFrom ?? id)); if (existing?.imageInput === "disabled") combo.imageInput = "disabled"; - if (effortMode === undefined && existing?.defaultEffortMode === "force") combo.defaultEffortMode = "force"; + if (effortMode === undefined && existing?.defaultEffortMode === "force") { + combo.defaultEffortMode = effort === "-" ? "fallback" : "force"; + } const result = await runtimeRequest("/api/combos", { method: "PUT", body: JSON.stringify({ id, combo, ...(renameFrom ? { renameFrom } : {}) }), diff --git a/src/combos/request.ts b/src/combos/request.ts index ab2ca0b076..abc423d43e 100644 --- a/src/combos/request.ts +++ b/src/combos/request.ts @@ -63,7 +63,10 @@ export function concreteComboRequestBody( ): Record { const clone = structuredClone(body) as Record; clone.model = `${target.provider}/${target.model}`; - if (!defaultEffort) return clone; + if (defaultEffortMode === "force" && (!defaultEffort || !isCodexReasoningEffort(defaultEffort))) { + throw new Error("force combo default effort requires a valid defaultEffort"); + } + if (!defaultEffort || !isCodexReasoningEffort(defaultEffort)) return clone; const reasoning = clone.reasoning; const reasoningRecord = reasoning && typeof reasoning === "object" && !Array.isArray(reasoning) ? reasoning as Record diff --git a/src/combos/types.ts b/src/combos/types.ts index 5bd2b8553e..f4b3e26b2f 100644 --- a/src/combos/types.ts +++ b/src/combos/types.ts @@ -310,13 +310,16 @@ export function comboConfigError( export function normalizeComboConfig(raw: OcxComboConfig): NormalizedComboConfig { const alias = typeof raw.alias === "string" ? raw.alias.trim() : ""; const displayName = typeof raw.displayName === "string" ? raw.displayName.trim() : ""; + const defaultEffort = typeof raw.defaultEffort === "string" && isCodexReasoningEffort(raw.defaultEffort) + ? raw.defaultEffort + : null; return { strategy: raw.strategy ?? "failover", stickyLimit: raw.stickyLimit ?? 1, cooldownMs: raw.cooldownMs, waitForCooldownMs: raw.waitForCooldownMs ?? COMBO_DEFAULT_WAIT_FOR_COOLDOWN_MS, - defaultEffort: raw.defaultEffort ?? null, - defaultEffortMode: raw.defaultEffortMode === "force" ? "force" : "fallback", + defaultEffort, + defaultEffortMode: raw.defaultEffortMode === "force" && defaultEffort !== null ? "force" : "fallback", reasoningEffortMode: raw.reasoningEffortMode === "adaptive" ? "adaptive" : "strict", imageInput: raw.imageInput === "disabled" ? "disabled" : "auto", alias: alias || null, diff --git a/src/types/config.ts b/src/types/config.ts index a06ffb6fcd..05b8999d9d 100644 --- a/src/types/config.ts +++ b/src/types/config.ts @@ -888,7 +888,7 @@ export interface OcxComboConfig { cooldownMs?: number; /** Maximum wait for an eligible target cooldown to expire before failing closed. Default 0; range 0..600000, per selection attempt. */ waitForCooldownMs?: number; - /** Used when the client omits reasoning.effort. null/omitted leaves the target default unchanged. */ + /** Used as a fallback when the client omits reasoning.effort, or as an override in `force` mode. null/omitted leaves the target default unchanged. */ defaultEffort?: OcxComboDefaultEffort | null; /** `force` makes the combo default override a valid client effort. Omitted / `fallback` preserves client precedence. */ defaultEffortMode?: OcxComboDefaultEffortMode; diff --git a/tests/cli/cli-headless-parity.test.ts b/tests/cli/cli-headless-parity.test.ts index 9334bbc945..b270f7b9e0 100644 --- a/tests/cli/cli-headless-parity.test.ts +++ b/tests/cli/cli-headless-parity.test.ts @@ -508,6 +508,37 @@ describe("headless GUI parity CLI", () => { }); }); + test("combo set sends fallback when clearing an existing forced default effort", async () => { + const runtime = fakeRuntime(req => req.method === "GET" ? { + combos: [{ + id: "deep", + defaultEffort: "max", + defaultEffortMode: "force", + targets: [{ provider: "ark", model: "old-model" }], + }], + } : undefined); + expect(await handleComboCommand([ + "set", "deep", "--targets", "ark/model-a", "--effort", "-", "--json", + ], runtime.deps)).toBe(0); + expect(runtime.requests).toEqual([ + { path: "/api/combos", method: "GET", body: null }, + { + path: "/api/combos", + method: "PUT", + body: { + id: "deep", + combo: { + strategy: "failover", + stickyLimit: 1, + targets: [{ provider: "ark", model: "model-a" }], + defaultEffort: null, + defaultEffortMode: "fallback", + }, + }, + }, + ]); + }); + test("combo set rejects --sticky outside round-robin instead of dropping it", async () => { const runtime = fakeRuntime(); const errorSpy = spyOn(console, "error").mockImplementation(() => {}); diff --git a/tests/codex-integration/combos.test.ts b/tests/codex-integration/combos.test.ts index 87c86a5a8c..2486209fb7 100644 --- a/tests/codex-integration/combos.test.ts +++ b/tests/codex-integration/combos.test.ts @@ -52,7 +52,7 @@ import { getConfigPath, readConfigDiagnostics, saveConfig } from "../../src/conf import { routeModel } from "../../src/router"; import { handleManagementAPI } from "../../src/server/management-api"; import { handleResponses } from "../../src/server/responses"; -import type { OcxConfig } from "../../src/types"; +import type { OcxComboConfig, OcxComboDefaultEffort, OcxConfig } from "../../src/types"; import { syncCatalogModels } from "../../src/codex/catalog"; import { injectClaudeAgentDefs } from "../../src/claude/agents-inject"; import { reconcileComboRotationState } from "../../src/combos/resolve"; @@ -330,6 +330,14 @@ describe("combo request cloning", () => { expect(raw.reasoning).toEqual({ effort: "medium", summary: "concise" }); }); + test("force mode rejects missing or invalid direct default efforts", () => { + const raw = { model: "combo/x", reasoning: { effort: "medium", summary: "concise" } }; + for (const defaultEffort of [null, "turbo" as OcxComboDefaultEffort]) { + expect(() => concreteComboRequestBody(raw, target, defaultEffort, ["low", "high"], "force")) + .toThrow("force combo default effort requires a valid defaultEffort"); + } + }); + test("force mode fails closed for malformed and unknown capabilities and strips unsupported effort", () => { expect(concreteComboRequestBody( { model: "combo/x", reasoning: { effort: "banana" } }, target, "max", ["max"], "force", @@ -1467,6 +1475,20 @@ describe("combo validation and normalization", () => { expect(comboDefaultEffort(corrupt, "free")).toBeNull(); }); + test("direct normalization rejects force mode without a valid default effort", () => { + const corruptConfigs = [ + { defaultEffortMode: "force", targets: [{ provider: "a", model: "m1" }] }, + { defaultEffort: null, defaultEffortMode: "force", targets: [{ provider: "a", model: "m1" }] }, + { defaultEffort: "turbo", defaultEffortMode: "force", targets: [{ provider: "a", model: "m1" }] }, + ] as unknown as OcxComboConfig[]; + for (const corrupt of corruptConfigs) { + expect(normalizeComboConfig(corrupt)).toMatchObject({ + defaultEffort: null, + defaultEffortMode: "fallback", + }); + } + }); + test("inherited combo names are unknown across getters, effort, and routing", () => { const config = baseConfig(); for (const id of ["constructor", "toString"]) { From 3b4bfedee654b6fea4381992d40c142ab03ab63f Mon Sep 17 00:00:00 2001 From: Elginux Agent Date: Tue, 8 Sep 2026 22:34:37 +0200 Subject: [PATCH 3/4] fix(gui): preserve advanced combo effort policy --- gui/src/combo-workspace-data.ts | 11 ----------- src/server/management/combo-routes.ts | 5 +++++ tests/gui/combo-workspace-data.test.ts | 15 --------------- tests/routing/combo-management-api.test.ts | 12 ++++++++++++ 4 files changed, 17 insertions(+), 26 deletions(-) diff --git a/gui/src/combo-workspace-data.ts b/gui/src/combo-workspace-data.ts index 719c0661f8..bf8b881c55 100644 --- a/gui/src/combo-workspace-data.ts +++ b/gui/src/combo-workspace-data.ts @@ -114,10 +114,6 @@ function normalizeReasoningEffortMode(value: unknown): "strict" | "adaptive" { return value === "adaptive" ? "adaptive" : "strict"; } -function normalizeDefaultEffortMode(value: unknown): "fallback" | "force" { - return value === "force" ? "force" : "fallback"; -} - export interface ComboItem { id: string; /** Wire id shown to clients, e.g. combo/free */ @@ -131,8 +127,6 @@ export interface ComboItem { strategy: ComboStrategy; stickyLimit: number; defaultEffort: ComboEffort | null; - /** Operator policy; force overrides valid client effort. */ - defaultEffortMode?: "fallback" | "force"; imageInput?: "auto" | "disabled"; /** * Picker-ladder policy. `adaptive` lets targets that advertise no effort control drop @@ -246,7 +240,6 @@ export function parseComboList(payload: unknown): ComboItem[] { strategy: normalizeStrategy(r.strategy), stickyLimit: normalizeStickyLimit(r.stickyLimit), defaultEffort: normalizeDefaultEffort(r.defaultEffort), - defaultEffortMode: normalizeDefaultEffortMode(r.defaultEffortMode), imageInput: normalizeImageInput(r.imageInput), reasoningEffortMode: normalizeReasoningEffortMode(r.reasoningEffortMode), targets, @@ -505,7 +498,6 @@ export function draftEquals(a: ComboItem, b: ComboItem): boolean { || a.strategy !== b.strategy || a.stickyLimit !== b.stickyLimit || a.defaultEffort !== b.defaultEffort - || (a.defaultEffortMode ?? "fallback") !== (b.defaultEffortMode ?? "fallback") || (a.imageInput ?? "auto") !== (b.imageInput ?? "auto") || (a.reasoningEffortMode ?? "strict") !== (b.reasoningEffortMode ?? "strict") ) return false; @@ -524,7 +516,6 @@ export function toPutBody(item: ComboItem, options: { renameFrom?: string } = {} strategy: ComboStrategy; stickyLimit?: number; defaultEffort: ComboEffort | null; - defaultEffortMode?: "force"; imageInput?: "disabled"; reasoningEffortMode?: "adaptive"; alias?: string; @@ -542,7 +533,6 @@ export function toPutBody(item: ComboItem, options: { renameFrom?: string } = {} : { provider: target.provider.trim(), model: target.model.trim() }), strategy: item.strategy, defaultEffort: item.defaultEffort, - ...(item.defaultEffortMode === "force" ? { defaultEffortMode: "force" as const } : {}), ...(item.imageInput === "disabled" ? { imageInput: "disabled" as const } : {}), ...(item.reasoningEffortMode === "adaptive" ? { reasoningEffortMode: "adaptive" as const } : {}), ...(item.strategy === "round-robin" ? { stickyLimit: item.stickyLimit } : {}), @@ -653,7 +643,6 @@ export function emptyDraft(id = ""): ComboItem { strategy: "failover", stickyLimit: 1, defaultEffort: null, - defaultEffortMode: "fallback", imageInput: "auto", reasoningEffortMode: "strict", targets: [newComboTarget()], diff --git a/src/server/management/combo-routes.ts b/src/server/management/combo-routes.ts index d61e7faf5c..cdc567a6eb 100644 --- a/src/server/management/combo-routes.ts +++ b/src/server/management/combo-routes.ts @@ -174,6 +174,11 @@ export async function handleComboRoutes(ctx: ManagementContext): Promise { strategy: "failover", stickyLimit: 1, defaultEffort: null, - defaultEffortMode: "fallback", imageInput: "auto", reasoningEffortMode: "strict", targets: [{ provider: "a", model: "m1", weight: 1, clientKey: expect.stringMatching(/^ct-\d+$/) }], @@ -127,7 +126,6 @@ describe("combo-workspace-data", () => { strategy: "round-robin", stickyLimit: 4, defaultEffort: "high", - defaultEffortMode: "fallback", imageInput: "auto", reasoningEffortMode: "strict", targets: [ @@ -271,19 +269,6 @@ describe("combo-workspace-data", () => { expect(draftEquals(combo({ reasoningEffortMode: "strict" }), combo())).toBe(true); }); - test("defaultEffortMode survives parse and serialize while fallback stays sparse", () => { - const forced = parseComboList({ combos: [{ - id: "forced", - defaultEffort: "max", - defaultEffortMode: "force", - targets: [{ provider: "a", model: "m1" }], - }] })[0]!; - expect(forced.defaultEffortMode).toBe("force"); - expect(toPutBody(forced).combo.defaultEffortMode).toBe("force"); - expect(toPutBody(combo()).combo).not.toHaveProperty("defaultEffortMode"); - expect(draftEquals(combo(), combo({ defaultEffortMode: "force" }))).toBe(false); - }); - test("attention flags zero-target and one-target defensive rows", () => { const attention = buildComboAttention([ combo({ id: "empty", model: "combo/empty", targets: [] }), diff --git a/tests/routing/combo-management-api.test.ts b/tests/routing/combo-management-api.test.ts index 8877bf5dcf..ba6bd7e2d0 100644 --- a/tests/routing/combo-management-api.test.ts +++ b/tests/routing/combo-management-api.test.ts @@ -514,6 +514,18 @@ describe("combo management API", () => { }); expect(fallback?.status).toBe(200); expect(config.combos?.forced).not.toHaveProperty("defaultEffortMode"); + + const restoreForce = await comboApi(config, "PUT", "/api/combos", { + id: "forced", + combo: { ...VALID_COMBO, defaultEffort: "max", defaultEffortMode: "force" }, + }); + expect(restoreForce?.status).toBe(200); + const guiRoundTrip = await comboApi(config, "PUT", "/api/combos", { + id: "forced", + combo: { ...VALID_COMBO, defaultEffort: "high" }, + }); + expect(guiRoundTrip?.status).toBe(200); + expect(config.combos?.forced).toMatchObject({ defaultEffort: "high", defaultEffortMode: "force" }); }); }); From 9fae9c154068a44fb023b47b9ba331e8e4dafb46 Mon Sep 17 00:00:00 2001 From: Elginux Agent Date: Tue, 8 Sep 2026 22:34:46 +0200 Subject: [PATCH 4/4] fix(combos): enforce effort on chat routes --- src/server/chat-completions.ts | 4 +- src/server/responses/core.ts | 26 +++++--- .../server/server-combo-failover-e2e.test.ts | 63 +++++++++++++++++++ 3 files changed, 84 insertions(+), 9 deletions(-) diff --git a/src/server/chat-completions.ts b/src/server/chat-completions.ts index 7e69010636..e2c8852046 100644 --- a/src/server/chat-completions.ts +++ b/src/server/chat-completions.ts @@ -159,7 +159,9 @@ async function handleChatCompletionsWithBudget( if (chatBody.tools !== undefined) parts.push(JSON.stringify(chatBody.tools)); logCtx.usageLogInputTokens = Math.max(1, estimateTokens(parts.join("\n"), requestedModel)); } - if (!effortRow && isNativeChatRouteEligible(route, chatBody)) chatNativeRoute = route; + // Combos must enter the Responses routing path so child selection, forced default + // effort, failover, and per-attempt telemetry are applied before any native Chat send. + if (!route.combo && !effortRow && isNativeChatRouteEligible(route, chatBody)) chatNativeRoute = route; } catch (err) { if (err instanceof UnknownRoutingPolicyError) { logCtx.requestedModel = requestedModel; diff --git a/src/server/responses/core.ts b/src/server/responses/core.ts index 1e439c48d6..72474d4894 100644 --- a/src/server/responses/core.ts +++ b/src/server/responses/core.ts @@ -12,6 +12,7 @@ import { describeOutboundBodyRefusal, } from "./outbound-body-guard"; import { nativeContextLimits } from "../../codex/catalog"; +import { isDeclaredReasoningEffort } from "../../reasoning-effort"; import { describeUpstreamConnectFailure } from "./upstream-error"; import type { CodexWsQuotaObserver } from "./codex-ws-metadata"; import { applyAccountQuotaFromUpstreamHeaders as applyCapturedCodexQuota } from "../../codex/quota"; @@ -2681,10 +2682,22 @@ export async function handleComboResponses( const originalReasoning = body && typeof body === "object" && !Array.isArray(body) ? (body as { reasoning?: unknown }).reasoning : undefined; - const originalRequestedEffort = originalReasoning && typeof originalReasoning === "object" && !Array.isArray(originalReasoning) - && typeof (originalReasoning as { effort?: unknown }).effort === "string" - ? (originalReasoning as { effort: string }).effort + const originalRequestedEffortValue = originalReasoning && typeof originalReasoning === "object" && !Array.isArray(originalReasoning) + ? (originalReasoning as { effort?: unknown }).effort : undefined; + const originalRequestedEffort = typeof originalRequestedEffortValue === "string" + && isDeclaredReasoningEffort(originalRequestedEffortValue) + ? originalRequestedEffortValue + : undefined; + const restoreOriginalRequestedEffort = (childLog: RequestLogContext): void => { + if (originalRequestedEffort === undefined) return; + const normalizedRequestedEffort = childLog.requestedEffort; + const transitionIndex = normalizedRequestedEffort?.indexOf("->") ?? -1; + childLog.requestedEffort = transitionIndex >= 0 + ? `${originalRequestedEffort}${normalizedRequestedEffort!.slice(transitionIndex)}` + : originalRequestedEffort; + recordAttemptRequestedEffort(childLog); + }; let lastFailure: Response | null = null; while (pick) { if (options.abortSignal?.aborted) return clientCancelledResponse(); @@ -2720,7 +2733,7 @@ export async function handleComboResponses( childLog.activeAttempt = attempt; if (originalRequestedEffort !== undefined) { childLog.requestedEffort = originalRequestedEffort; - attempt.requestedEffort = originalRequestedEffort; + recordAttemptRequestedEffort(childLog); } let attemptRetained = false; const retainCancelledAttempt = (): void => { @@ -2793,10 +2806,7 @@ export async function handleComboResponses( onNativePassthroughCancel: callbackGate.onCancel, onResponseComplete: callbackGate.onResponseComplete, }); - if (originalRequestedEffort !== undefined) { - childLog.requestedEffort = originalRequestedEffort; - attempt.requestedEffort = originalRequestedEffort; - } + restoreOriginalRequestedEffort(childLog); } catch (error) { callbackGate.discard(); if (options.abortSignal?.aborted) { diff --git a/tests/server/server-combo-failover-e2e.test.ts b/tests/server/server-combo-failover-e2e.test.ts index 765b01020b..724252e677 100644 --- a/tests/server/server-combo-failover-e2e.test.ts +++ b/tests/server/server-combo-failover-e2e.test.ts @@ -2896,6 +2896,69 @@ describe("server combo failover 030 activation matrix", () => { expect(efforts).toEqual(["max", "medium"]); }); + for (const chatEffort of [ + { name: "reasoning_effort", body: { reasoning_effort: "medium" } }, + { name: "reasoning.effort", body: { reasoning: { effort: "medium" } } }, + ] as const) { + test(`Chat ${chatEffort.name} force-default routes through the combo and records normalized wire telemetry`, async () => { + const upstreamBodies: Array<{ provider: string; body: Record }> = []; + const a = serve(async request => { + upstreamBodies.push({ provider: "a", body: await request.json() as Record }); + return chatStream("forced chat"); + }); + const config = comboConfig({ + a: provider("openai-chat", baseUrl(a), "key-a", { + reasoningEfforts: ["low", "medium", "high", "max"], + }), + }, undefined, { + defaultEffort: "max", + defaultEffortMode: "force", + }); + saveConfig(config); + const server = startServer(0); + try { + const response = await fetch(new URL("/v1/chat/completions", server.url), { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + model: "combo/free", + messages: [{ role: "user", content: "hello" }], + stream: false, + ...chatEffort.body, + }), + }); + expect(response.status).toBe(200); + expect(await response.text()).toContain("forced chat"); + expect(upstreamBodies).toEqual([ + { provider: "a", body: expect.objectContaining({ model: "m1", reasoning_effort: "max" }) }, + ]); + + const { log, usage } = await latestAttemptReceipts(config); + for (const receipt of [log, usage]) { + expect(receipt).toMatchObject({ + provider: "combo", + model: "combo/free", + requestedEffort: "medium", + effectiveEffort: "max", + reasoningWireField: "reasoning_effort", + reasoningWireValue: "max", + routeDecision: { routeKind: "combo" }, + attempts: [{ + provider: "a", + model: "m1", + requestedEffort: "medium", + effectiveEffort: "max", + reasoningWireField: "reasoning_effort", + reasoningWireValue: "max", + }], + }); + } + } finally { + await server.stop(true); + } + }); + } + test("backup noReasoningModels removes the fresh combo default", async () => { const a = serve(() => Response.json({ error: { message: "retry" } }, { status: 503 })); let backupBody: Record | undefined;