From 524985f82816f91440c1bc0b6912e7eaecea7cad Mon Sep 17 00:00:00 2001 From: wangyiqin Date: Thu, 3 Sep 2026 14:40:57 +0800 Subject: [PATCH 1/6] feat(router): add per-model pinned reasoning effort overrides [skip ci] Allows users to configure an explicit reasoning effort tier (none..max) per model or provider-wide that is forcefully enforced on inbound requests, overriding caller choices or filling missing effort parameters. - types: add pinnedReasoningEffort and modelPinnedReasoningEfforts to OcxProviderConfig and modelPinnedEfforts to OcxConfig - policy: add resolvePinnedEffort and applyPinnedEffort in effort-policy - server: apply pinned reasoning effort override in handleResponses - api: expose and validate pinned reasoning efforts in PATCH /api/providers and PUT /api/effort-caps - tests: comprehensive regression coverage for policy, rewrites, and API (cherry picked from commit 6399c8eb05f9f5b2e16af1f499f3bd73a2983f04) Co-authored-by: Liang-Psych <19504206+Liang-Psych@users.noreply.github.com> --- src/providers/derive.ts | 21 ++- src/router.ts | 4 + src/server/effort-policy.ts | 60 ++++++ .../management/agent-settings-routes.ts | 34 +++- src/server/management/provider-routes.ts | 47 +++++ src/server/responses/core.ts | 11 ++ src/types/config.ts | 6 + src/types/provider.ts | 11 ++ tests/model-pinned-effort.test.ts | 177 ++++++++++++++++++ 9 files changed, 368 insertions(+), 3 deletions(-) create mode 100644 tests/model-pinned-effort.test.ts diff --git a/src/providers/derive.ts b/src/providers/derive.ts index 02852fce39..8beac62f16 100644 --- a/src/providers/derive.ts +++ b/src/providers/derive.ts @@ -32,6 +32,8 @@ export interface DerivedKeyLoginProvider { reasoningEfforts?: string[]; modelReasoningEfforts?: Record; modelDefaultReasoningEfforts?: Record; + pinnedReasoningEffort?: string; + modelPinnedReasoningEfforts?: Record; reasoningEffortMap?: Record; modelReasoningEffortMap?: Record>; reasoningWireFormat?: OcxProviderConfig["reasoningWireFormat"]; @@ -124,7 +126,7 @@ function sameStringArray(left: readonly string[] | undefined, right: readonly st type DirectReasoningEffortOverrides = Pick< OcxProviderConfig, - "thinkingBudgetModels" | "modelReasoningEfforts" | "modelDefaultReasoningEfforts" | "modelReasoningEffortMap" + "thinkingBudgetModels" | "modelReasoningEfforts" | "modelDefaultReasoningEfforts" | "pinnedReasoningEffort" | "modelPinnedReasoningEfforts" | "modelReasoningEffortMap" >; function fillFoldedModelDefault( @@ -194,6 +196,15 @@ export function applyDirectReasoningEffortContracts( value => value, ); + const pinnedEffort = entry.modelPinnedReasoningEfforts?.[model]; + prov.modelPinnedReasoningEfforts = fillFoldedModelDefault( + prov.modelPinnedReasoningEfforts, + explicit.modelPinnedReasoningEfforts, + model, + pinnedEffort, + value => value, + ); + // An explicit empty model map masks any provider-wide aliases. Without it, a stale global // mapping such as xhigh -> max would win before the verified direct ladder can clamp it. prov.modelReasoningEffortMap = fillFoldedModelDefault( @@ -240,6 +251,8 @@ export function providerConfigSeed(entry: ProviderRegistryEntry): OcxProviderCon ...(entry.reasoningEfforts ? { reasoningEfforts: [...entry.reasoningEfforts] } : {}), ...(entry.modelReasoningEfforts ? { modelReasoningEfforts: cloneRecordOfArrays(entry.modelReasoningEfforts) } : {}), ...(entry.modelDefaultReasoningEfforts ? { modelDefaultReasoningEfforts: { ...entry.modelDefaultReasoningEfforts } } : {}), + ...(entry.pinnedReasoningEffort !== undefined ? { pinnedReasoningEffort: entry.pinnedReasoningEffort } : {}), + ...(entry.modelPinnedReasoningEfforts ? { modelPinnedReasoningEfforts: { ...entry.modelPinnedReasoningEfforts } } : {}), ...(entry.reasoningEffortMap ? { reasoningEffortMap: { ...entry.reasoningEffortMap } } : {}), ...(entry.modelReasoningEffortMap ? { modelReasoningEffortMap: cloneNestedRecord(entry.modelReasoningEffortMap) } : {}), ...(entry.reasoningWireFormat ? { reasoningWireFormat: entry.reasoningWireFormat } : {}), @@ -300,6 +313,8 @@ export function deriveKeyLoginMap(): Record { ...(entry.reasoningEfforts ? { reasoningEfforts: [...entry.reasoningEfforts] } : {}), ...(entry.modelReasoningEfforts ? { modelReasoningEfforts: cloneRecordOfArrays(entry.modelReasoningEfforts) } : {}), ...(entry.modelDefaultReasoningEfforts ? { modelDefaultReasoningEfforts: { ...entry.modelDefaultReasoningEfforts } } : {}), + ...(entry.pinnedReasoningEffort !== undefined ? { pinnedReasoningEffort: entry.pinnedReasoningEffort } : {}), + ...(entry.modelPinnedReasoningEfforts ? { modelPinnedReasoningEfforts: { ...entry.modelPinnedReasoningEfforts } } : {}), ...(entry.reasoningEffortMap ? { reasoningEffortMap: { ...entry.reasoningEffortMap } } : {}), ...(entry.modelReasoningEffortMap ? { modelReasoningEffortMap: cloneNestedRecord(entry.modelReasoningEffortMap) } : {}), ...(entry.reasoningWireFormat ? { reasoningWireFormat: entry.reasoningWireFormat } : {}), @@ -467,6 +482,8 @@ export function enrichProviderFromRegistry(name: string, prov: OcxProviderConfig thinkingBudgetModels: prov.thinkingBudgetModels, modelReasoningEfforts: prov.modelReasoningEfforts, modelDefaultReasoningEfforts: prov.modelDefaultReasoningEfforts, + pinnedReasoningEffort: prov.pinnedReasoningEffort, + modelPinnedReasoningEfforts: prov.modelPinnedReasoningEfforts, modelReasoningEffortMap: prov.modelReasoningEffortMap, }; const seed = providerConfigSeed(entry); @@ -500,6 +517,8 @@ export function enrichProviderFromRegistry(name: string, prov: OcxProviderConfig prov.modelReasoningEfforts = fillRecordOfArrays(seed.modelReasoningEfforts, prov.modelReasoningEfforts); } if (!prov.modelDefaultReasoningEfforts && seed.modelDefaultReasoningEfforts) prov.modelDefaultReasoningEfforts = { ...seed.modelDefaultReasoningEfforts }; + if (!prov.pinnedReasoningEffort && seed.pinnedReasoningEffort) prov.pinnedReasoningEffort = seed.pinnedReasoningEffort; + if (!prov.modelPinnedReasoningEfforts && seed.modelPinnedReasoningEfforts) prov.modelPinnedReasoningEfforts = { ...seed.modelPinnedReasoningEfforts }; if (!prov.reasoningEffortMap && seed.reasoningEffortMap) prov.reasoningEffortMap = { ...seed.reasoningEffortMap }; if (!prov.modelReasoningEffortMap && seed.modelReasoningEffortMap) prov.modelReasoningEffortMap = cloneNestedRecord(seed.modelReasoningEffortMap); if (prov.reasoningWireFormat === undefined && seed.reasoningWireFormat !== undefined) prov.reasoningWireFormat = seed.reasoningWireFormat; diff --git a/src/router.ts b/src/router.ts index b2f887f0a9..cfe0506224 100644 --- a/src/router.ts +++ b/src/router.ts @@ -321,6 +321,8 @@ export function routedProviderConfig(providerName: string, provider: OcxProvider const modelReasoningEffortMap = mergeNestedRecord(registryEntry.modelReasoningEffortMap, provider.modelReasoningEffortMap); const modelReasoningEfforts = mergeStringArrayRecord(registryEntry.modelReasoningEfforts, provider.modelReasoningEfforts); const modelDefaultReasoningEfforts = mergeRecordFill(registryEntry.modelDefaultReasoningEfforts, provider.modelDefaultReasoningEfforts); + const modelPinnedReasoningEfforts = mergeRecordFill(registryEntry.modelPinnedReasoningEfforts, provider.modelPinnedReasoningEfforts); + const pinnedReasoningEffort = provider.pinnedReasoningEffort ?? registryEntry.pinnedReasoningEffort; const modelContextWindows = providerName === OPENAI_API_PROVIDER_ID ? mergePositiveNumberCaps(registryEntry.modelContextWindows, provider.modelContextWindows) : mergeRecordFill(registryEntry.modelContextWindows, provider.modelContextWindows); @@ -466,6 +468,8 @@ export function routedProviderConfig(providerName: string, provider: OcxProvider ...(modelSupportsVerbosity ? { modelSupportsVerbosity } : {}), ...(modelReasoningEfforts ? { modelReasoningEfforts } : {}), ...(modelDefaultReasoningEfforts ? { modelDefaultReasoningEfforts } : {}), + ...(pinnedReasoningEffort ? { pinnedReasoningEffort } : {}), + ...(modelPinnedReasoningEfforts ? { modelPinnedReasoningEfforts } : {}), ...(reasoningEffortMap ? { reasoningEffortMap } : {}), ...(modelReasoningEffortMap ? { modelReasoningEffortMap } : {}), ...(noVisionModels ? { noVisionModels } : {}), diff --git a/src/server/effort-policy.ts b/src/server/effort-policy.ts index 2686b73460..df9cc0e0ce 100644 --- a/src/server/effort-policy.ts +++ b/src/server/effort-policy.ts @@ -188,3 +188,63 @@ export function applyEffortCap( if (raw?.reasoning && typeof raw.reasoning === "object") raw.reasoning.effort = resolved; return { from: requested, to: resolved, subagent }; } + +/** + * Resolve any pinned reasoning effort configured for this model or provider. + * Priority order: + * 1. Provider model-specific pinned effort (`provider.modelPinnedReasoningEfforts[modelId]`) + * 2. Provider-wide pinned effort (`provider.pinnedReasoningEffort`) + * 3. Global config model-specific pinned effort (`config.modelPinnedEfforts[modelId]`) + * + * Returns undefined when no valid pinned effort tier is configured. + */ +export function resolvePinnedEffort( + route: { provider: OcxProviderConfig; modelId: string }, + parsedModelId?: string, + config?: OcxConfig, +): string | undefined { + const prov = route.provider; + const rawProvModel = modelRecordValue(prov.modelPinnedReasoningEfforts, route.modelId) + ?? (parsedModelId ? modelRecordValue(prov.modelPinnedReasoningEfforts, parsedModelId) : undefined); + if (rawProvModel && (isCodexReasoningEffort(rawProvModel) || rawProvModel === "none" || rawProvModel === "minimal")) { + return rawProvModel; + } + if (prov.pinnedReasoningEffort && (isCodexReasoningEffort(prov.pinnedReasoningEffort) || prov.pinnedReasoningEffort === "none" || prov.pinnedReasoningEffort === "minimal")) { + return prov.pinnedReasoningEffort; + } + if (config?.modelPinnedEfforts) { + const rawGlobal = modelRecordValue(config.modelPinnedEfforts, route.modelId) + ?? (parsedModelId ? modelRecordValue(config.modelPinnedEfforts, parsedModelId) : undefined); + if (rawGlobal && (isCodexReasoningEffort(rawGlobal) || rawGlobal === "none" || rawGlobal === "minimal")) { + return rawGlobal; + } + } + return undefined; +} + +/** + * Apply any pinned reasoning effort to the parsed request and raw body in BOTH shapes. + * Forces the reasoning effort regardless of what the caller sent, or when the caller sent none. + * Returns the rewrite transition { from, to } for logging, or null if no pinned effort applied. + */ +export function applyPinnedEffort( + parsed: OcxParsedRequest, + route: { provider: OcxProviderConfig; modelId: string }, + config?: OcxConfig, +): { from: string | undefined; to: string } | null { + const pinned = resolvePinnedEffort(route, parsed.modelId, config); + if (!pinned) return null; + const requested = parsed.options.reasoning; + const raw = parsed._rawBody as { reasoning?: { effort?: string } } | undefined; + const targetEffort = pinned === "none" ? undefined : pinned; + parsed.options.reasoning = targetEffort; + if (targetEffort) { + if (raw && typeof raw === "object") { + if (!raw.reasoning || typeof raw.reasoning !== "object") raw.reasoning = {}; + raw.reasoning.effort = targetEffort; + } + } else if (raw?.reasoning && typeof raw.reasoning === "object") { + delete raw.reasoning.effort; + } + return { from: requested, to: pinned }; +} diff --git a/src/server/management/agent-settings-routes.ts b/src/server/management/agent-settings-routes.ts index a7cf017f3a..a971b7fbfc 100644 --- a/src/server/management/agent-settings-routes.ts +++ b/src/server/management/agent-settings-routes.ts @@ -603,11 +603,12 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise return jsonResponse({ effortCap: config.effortCap ?? null, subagentEffortCap: config.subagentEffortCap ?? null, + modelPinnedEfforts: config.modelPinnedEfforts ?? {}, efforts: CODEX_REASONING_LEVELS.map(l => l.effort), }); } if (url.pathname === "/api/effort-caps" && req.method === "PUT") { - let body: { effortCap?: unknown; subagentEffortCap?: unknown }; + let body: { effortCap?: unknown; subagentEffortCap?: unknown; modelPinnedEfforts?: unknown }; try { body = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); } const { isCodexReasoningEffort } = await import("../../reasoning-effort"); for (const key of ["effortCap", "subagentEffortCap"] as const) { @@ -619,8 +620,37 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise } config[key] = value; } + if ("modelPinnedEfforts" in body) { + const val = body.modelPinnedEfforts; + if (val === null || val === undefined) { + deleteConfigTopLevelKey(config, "modelPinnedEfforts"); + } else if (typeof val === "object" && !Array.isArray(val)) { + const cleaned: Record = {}; + for (const [m, eff] of Object.entries(val as Record)) { + if (!m.trim()) return jsonResponse({ error: "modelPinnedEfforts keys must be nonblank model ids" }, 400); + if (eff === null || eff === "" || eff === undefined) continue; + if (typeof eff === "string" && (isCodexReasoningEffort(eff) || eff === "none" || eff === "minimal")) { + cleaned[m.trim()] = eff; + } else { + return jsonResponse({ error: `unknown reasoning effort "${String(eff)}" for model "${m}"` }, 400); + } + } + if (Object.keys(cleaned).length > 0) { + config.modelPinnedEfforts = cleaned; + } else { + deleteConfigTopLevelKey(config, "modelPinnedEfforts"); + } + } else { + return jsonResponse({ error: "modelPinnedEfforts must be a plain object or null" }, 400); + } + } saveConfigPreservingClaudeCode(config); - return jsonResponse({ ok: true, effortCap: config.effortCap ?? null, subagentEffortCap: config.subagentEffortCap ?? null }); + return jsonResponse({ + ok: true, + effortCap: config.effortCap ?? null, + subagentEffortCap: config.subagentEffortCap ?? null, + ...(config.modelPinnedEfforts ? { modelPinnedEfforts: config.modelPinnedEfforts } : {}), + }); } // Featured roster and saved picker order are separate settings. Native Codex advertises diff --git a/src/server/management/provider-routes.ts b/src/server/management/provider-routes.ts index 37785ac17b..fc33313b60 100644 --- a/src/server/management/provider-routes.ts +++ b/src/server/management/provider-routes.ts @@ -33,6 +33,7 @@ import { submitManualLoginCode, upsertOAuthProvider, } from "../../oauth"; +import { isCodexReasoningEffort } from "../../reasoning-effort"; import { replaceProviderAccountSet } from "../../oauth/store"; import { providerDestinationResolvedError } from "../../lib/destination-policy"; import { reconcileLiveStateStores } from "../../lib/state-store-registrations"; @@ -478,6 +479,40 @@ function applyProviderPatchFields( } touched = true; } + if (Object.hasOwn(rawBody, "pinnedReasoningEffort")) { + const value = rawBody.pinnedReasoningEffort; + if (value === null || value === "") { + delete next.pinnedReasoningEffort; + } else if (typeof value === "string" && (isCodexReasoningEffort(value) || value === "none" || value === "minimal")) { + next.pinnedReasoningEffort = value; + } else { + return { error: "pinnedReasoningEffort must be a valid reasoning effort or null" }; + } + touched = true; + } + if (Object.hasOwn(rawBody, "modelPinnedReasoningEfforts")) { + const value = rawBody.modelPinnedReasoningEfforts; + if (value === null) { + delete next.modelPinnedReasoningEfforts; + } else { + if (!isPlainRecord(value)) return { error: "modelPinnedReasoningEfforts must be a plain object or null" }; + const efforts: Record = { ...(next.modelPinnedReasoningEfforts ?? {}) }; + for (const [model, effort] of Object.entries(value)) { + if (!model.trim()) return { error: "modelPinnedReasoningEfforts keys must be nonblank model ids" }; + if (effort === null || effort === "") { + delete efforts[model]; + continue; + } + if (typeof effort !== "string" || (!isCodexReasoningEffort(effort) && effort !== "none" && effort !== "minimal")) { + return { error: `invalid reasoning effort "${String(effort)}" for model "${model}"` }; + } + efforts[model] = effort; + } + if (Object.keys(efforts).length > 0) next.modelPinnedReasoningEfforts = efforts; + else delete next.modelPinnedReasoningEfforts; + } + touched = true; + } if (Object.hasOwn(rawBody, "modelAutoCompactTokenLimits")) { const value = rawBody.modelAutoCompactTokenLimits; const error = modelAutoCompactTokenLimitsConfigError(value, { @@ -689,6 +724,8 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise${pinned.to}` : pinned.to; + if (isInjectionDebugEnabled()) { + injectionDebugLog(`[opencodex] ${route.modelId}: pinned reasoning effort applied (${pinned.from ?? "none"} -> ${pinned.to})`); + } + } + } + { const { applyEffortCap, effortCapAppliesTo, supportedLadderFor } = await import("../effort-policy"); const surface = collabSurface(parsed); diff --git a/src/types/config.ts b/src/types/config.ts index 31b5a52ceb..b3dd8f3de0 100644 --- a/src/types/config.ts +++ b/src/types/config.ts @@ -544,6 +544,12 @@ export interface OcxConfig { * set, the lower one wins for sub-agents. See src/server/effort-policy.ts. */ subagentEffortCap?: string; + /** + * Optional map of model IDs (or model family / bare slugs) to an enforced reasoning effort tier + * ("none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max"). + * Overrides incoming caller choices globally across all providers. + */ + modelPinnedEfforts?: Record; /** * Models hidden from Codex discovery without blocking direct proxy calls. Routed provider ids * are excluded from the catalog + /v1/models entirely. Account-qualified native ids hide only diff --git a/src/types/provider.ts b/src/types/provider.ts index 97a359506a..860133a443 100644 --- a/src/types/provider.ts +++ b/src/types/provider.ts @@ -493,6 +493,17 @@ export interface OcxProviderConfig { modelReasoningEfforts?: Record; /** Model-specific default Codex reasoning tier; must also be present in the visible tier list. */ modelDefaultReasoningEfforts?: Record; + /** + * Provider-wide pinned reasoning effort tier ("none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max"). + * When configured, models under this provider default to being forcefully pinned to this tier. + */ + pinnedReasoningEffort?: string; + /** + * Model-specific pinned reasoning effort overrides ("none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max"). + * When set, any incoming request targeting that model has its reasoning effort forcefully pinned + * to this tier, overriding caller choices. + */ + modelPinnedReasoningEfforts?: Record; /** * Model-specific Codex reasoning-summary capability. Set false when an OpenAI-compatible * Responses backend rejects Codex summary-delivery fields for that model. diff --git a/tests/model-pinned-effort.test.ts b/tests/model-pinned-effort.test.ts new file mode 100644 index 0000000000..5d0d1b2fe3 --- /dev/null +++ b/tests/model-pinned-effort.test.ts @@ -0,0 +1,177 @@ +import { describe, expect, test } from "bun:test"; +import { mkdtempSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { resolvePinnedEffort, applyPinnedEffort } from "../src/server/effort-policy"; +import { handleManagementAPI } from "../src/server/management-api"; +import type { OcxConfig, OcxParsedRequest, OcxProviderConfig } from "../src/types"; + +describe("model pinned reasoning effort policy", () => { + const providerWithPinned: OcxProviderConfig = { + adapter: "openai-responses", + baseUrl: "https://api.openai.com/v1", + pinnedReasoningEffort: "high", + modelPinnedReasoningEfforts: { + "special-model": "max", + "disabled-effort-model": "none", + }, + }; + + test("resolves model-specific pinned effort over provider-wide pinned effort", () => { + const route = { provider: providerWithPinned, modelId: "special-model" }; + expect(resolvePinnedEffort(route)).toBe("max"); + }); + + test("resolves provider-wide pinned effort when model is not specifically pinned", () => { + const route = { provider: providerWithPinned, modelId: "other-model" }; + expect(resolvePinnedEffort(route)).toBe("high"); + }); + + test("resolves global config modelPinnedEfforts fallback when provider has none", () => { + const emptyProvider: OcxProviderConfig = { + adapter: "openai-responses", + baseUrl: "https://api.openai.com/v1", + }; + const config = { + modelPinnedEfforts: { "global-pinned": "max" }, + } as unknown as OcxConfig; + const route = { provider: emptyProvider, modelId: "global-pinned" }; + expect(resolvePinnedEffort(route, undefined, config)).toBe("max"); + }); + + test("applyPinnedEffort overrides caller effort in both parsed options and raw body", () => { + const route = { provider: providerWithPinned, modelId: "special-model" }; + const parsed: OcxParsedRequest = { + modelId: "special-model", + context: { messages: [] }, + stream: true, + options: { reasoning: "low" }, + _rawBody: { reasoning: { effort: "low" } }, + }; + + const rewrite = applyPinnedEffort(parsed, route); + expect(rewrite).toEqual({ from: "low", to: "max" }); + expect(parsed.options.reasoning).toBe("max"); + expect((parsed._rawBody as any).reasoning.effort).toBe("max"); + }); + + test("applyPinnedEffort applies pinned effort when caller sent none", () => { + const route = { provider: providerWithPinned, modelId: "other-model" }; + const parsed: OcxParsedRequest = { + modelId: "other-model", + context: { messages: [] }, + stream: true, + options: {}, + _rawBody: {}, + }; + + const rewrite = applyPinnedEffort(parsed, route); + expect(rewrite).toEqual({ from: undefined, to: "high" }); + expect(parsed.options.reasoning).toBe("high"); + expect((parsed._rawBody as any).reasoning.effort).toBe("high"); + }); + + test("applyPinnedEffort with none strips effort from both shapes", () => { + const route = { provider: providerWithPinned, modelId: "disabled-effort-model" }; + const parsed: OcxParsedRequest = { + modelId: "disabled-effort-model", + context: { messages: [] }, + stream: true, + options: { reasoning: "high" }, + _rawBody: { reasoning: { effort: "high", summary: "auto" } }, + }; + + const rewrite = applyPinnedEffort(parsed, route); + expect(rewrite).toEqual({ from: "high", to: "none" }); + expect(parsed.options.reasoning).toBeUndefined(); + expect((parsed._rawBody as any).reasoning.effort).toBeUndefined(); + expect((parsed._rawBody as any).reasoning.summary).toBe("auto"); + }); +}); + +describe("management API pinned reasoning effort configuration", () => { + let tempHome: string; + function isolatedHome(): void { + tempHome = mkdtempSync(join(tmpdir(), "ocx-pinned-effort-")); + process.env.OPENCODEX_HOME = tempHome; + } + + function makeConfig(overrides: Partial = {}): OcxConfig { + return { + version: 1, + providers: { + custom: { + adapter: "openai-responses", + baseUrl: "https://api.custom.com", + allowPrivateNetwork: true, + }, + }, + ...overrides, + } as unknown as OcxConfig; + } + + test("PATCH /api/providers sets and updates pinned reasoning efforts", async () => { + isolatedHome(); + const config = makeConfig(); + const patchReq = new Request("http://localhost/api/providers?name=custom", { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + pinnedReasoningEffort: "high", + modelPinnedReasoningEfforts: { "model-a": "max", "model-b": "low" }, + }), + }); + const patchRes = await handleManagementAPI(patchReq, new URL(patchReq.url), config); + expect(patchRes?.status).toBe(200); + const provider = config.providers.custom; + expect(provider.pinnedReasoningEffort).toBe("high"); + expect(provider.modelPinnedReasoningEfforts).toEqual({ "model-a": "max", "model-b": "low" }); + + // Clearing a model pinned effort + const clearReq = new Request("http://localhost/api/providers?name=custom", { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + modelPinnedReasoningEfforts: { "model-a": null }, + }), + }); + const clearRes = await handleManagementAPI(clearReq, new URL(clearReq.url), config); + expect(clearRes?.status).toBe(200); + expect(config.providers.custom.modelPinnedReasoningEfforts).toEqual({ "model-b": "low" }); + }); + + test("PATCH /api/providers rejects invalid reasoning effort values", async () => { + isolatedHome(); + const config = makeConfig(); + const badReq = new Request("http://localhost/api/providers?name=custom", { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + pinnedReasoningEffort: "invalid-tier", + }), + }); + const badRes = await handleManagementAPI(badReq, new URL(badReq.url), config); + expect(badRes?.status).toBe(400); + }); + + test("PUT /api/effort-caps supports modelPinnedEfforts roundtrip", async () => { + isolatedHome(); + const config = makeConfig(); + const putReq = new Request("http://localhost/api/effort-caps", { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + modelPinnedEfforts: { "gpt-5.5": "max", "claude-sonnet-4-6": "high" }, + }), + }); + const putRes = await handleManagementAPI(putReq, new URL(putReq.url), config); + expect(putRes?.status).toBe(200); + expect(config.modelPinnedEfforts).toEqual({ "gpt-5.5": "max", "claude-sonnet-4-6": "high" }); + + const getReq = new Request("http://localhost/api/effort-caps"); + const getRes = await handleManagementAPI(getReq, new URL(getReq.url), config); + const data = await getRes?.json() as { modelPinnedEfforts: Record }; + expect(data.modelPinnedEfforts).toEqual({ "gpt-5.5": "max", "claude-sonnet-4-6": "high" }); + }); +}); +import { ManagementRequest as Request } from "./helpers/management-auth"; From 331e68f3542def76f7bfd355c98021fc3bd02b55 Mon Sep 17 00:00:00 2001 From: wangyiqin Date: Thu, 3 Sep 2026 14:49:35 +0800 Subject: [PATCH 2/6] fix(router): unify effort validation with isDeclaredReasoningEffort, support native chat path, and key-level merge in PUT\n\n- unify effort validation across policy and routes to use isDeclaredReasoningEffort\n- apply resolvePinnedEffort to handleNativeChatCompletions in chat-native.ts\n- support key-level partial merge and single-key deletions in PUT /api/effort-caps\n- add regression tests covering partial merge in PUT /api/effort-caps [skip ci] (cherry picked from commit db65ff9b3d5245f1efdea88109ae5be737317aa5) Co-authored-by: Liang-Psych <19504206+Liang-Psych@users.noreply.github.com> --- src/server/chat-native.ts | 18 +++++++++++++++--- src/server/effort-policy.ts | 8 ++++---- src/server/management/agent-settings-routes.ts | 17 ++++++++++------- src/server/management/provider-routes.ts | 6 +++--- tests/model-pinned-effort.test.ts | 12 ++++++++++++ 5 files changed, 44 insertions(+), 17 deletions(-) diff --git a/src/server/chat-native.ts b/src/server/chat-native.ts index 49e4beb61a..bdf30925cb 100644 --- a/src/server/chat-native.ts +++ b/src/server/chat-native.ts @@ -6,6 +6,7 @@ import { collectChatCompletion, isChatCompletionsStreamError, } from "../chat/outbound"; +import { resolvePinnedEffort } from "./effort-policy"; import { classifyError, cyberPolicyErrorType, @@ -147,9 +148,20 @@ export async function handleNativeChatCompletions(options: HandleNativeChatOptio return chatCompletionsErrorResponse(status, safeMessage, type, code); }; - logCtx.requestedEffort = typeof options.chatBody.reasoning_effort === "string" - ? options.chatBody.reasoning_effort - : undefined; + const pinnedEffort = resolvePinnedEffort(route, requestedModel, config); + if (pinnedEffort) { + const from = typeof options.chatBody.reasoning_effort === "string" ? options.chatBody.reasoning_effort : undefined; + logCtx.requestedEffort = from ? `${from}->${pinnedEffort}` : pinnedEffort; + if (pinnedEffort === "none") { + delete options.chatBody.reasoning_effort; + } else { + options.chatBody.reasoning_effort = pinnedEffort; + } + } else { + logCtx.requestedEffort = typeof options.chatBody.reasoning_effort === "string" + ? options.chatBody.reasoning_effort + : undefined; + } logCtx.requestedServiceTier = typeof options.chatBody.service_tier === "string" ? options.chatBody.service_tier : undefined; diff --git a/src/server/effort-policy.ts b/src/server/effort-policy.ts index df9cc0e0ce..d6c478d4c5 100644 --- a/src/server/effort-policy.ts +++ b/src/server/effort-policy.ts @@ -14,7 +14,7 @@ */ import type { OcxConfig, OcxParsedRequest, OcxProviderConfig } from "../types"; import { modelInList } from "../types"; -import { codexEffortRank, configuredReasoningEfforts, isCodexReasoningEffort, modelRecordValue } from "../reasoning-effort"; +import { codexEffortRank, configuredReasoningEfforts, isCodexReasoningEffort, isDeclaredReasoningEffort, modelRecordValue } from "../reasoning-effort"; import { catalogModelEfforts } from "../codex/catalog"; /** @@ -206,16 +206,16 @@ export function resolvePinnedEffort( const prov = route.provider; const rawProvModel = modelRecordValue(prov.modelPinnedReasoningEfforts, route.modelId) ?? (parsedModelId ? modelRecordValue(prov.modelPinnedReasoningEfforts, parsedModelId) : undefined); - if (rawProvModel && (isCodexReasoningEffort(rawProvModel) || rawProvModel === "none" || rawProvModel === "minimal")) { + if (rawProvModel && isDeclaredReasoningEffort(rawProvModel)) { return rawProvModel; } - if (prov.pinnedReasoningEffort && (isCodexReasoningEffort(prov.pinnedReasoningEffort) || prov.pinnedReasoningEffort === "none" || prov.pinnedReasoningEffort === "minimal")) { + if (prov.pinnedReasoningEffort && isDeclaredReasoningEffort(prov.pinnedReasoningEffort)) { return prov.pinnedReasoningEffort; } if (config?.modelPinnedEfforts) { const rawGlobal = modelRecordValue(config.modelPinnedEfforts, route.modelId) ?? (parsedModelId ? modelRecordValue(config.modelPinnedEfforts, parsedModelId) : undefined); - if (rawGlobal && (isCodexReasoningEffort(rawGlobal) || rawGlobal === "none" || rawGlobal === "minimal")) { + if (rawGlobal && isDeclaredReasoningEffort(rawGlobal)) { return rawGlobal; } } diff --git a/src/server/management/agent-settings-routes.ts b/src/server/management/agent-settings-routes.ts index a971b7fbfc..f26caa164f 100644 --- a/src/server/management/agent-settings-routes.ts +++ b/src/server/management/agent-settings-routes.ts @@ -610,7 +610,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise if (url.pathname === "/api/effort-caps" && req.method === "PUT") { let body: { effortCap?: unknown; subagentEffortCap?: unknown; modelPinnedEfforts?: unknown }; try { body = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); } - const { isCodexReasoningEffort } = await import("../../reasoning-effort"); + const { isCodexReasoningEffort, isDeclaredReasoningEffort } = await import("../../reasoning-effort"); for (const key of ["effortCap", "subagentEffortCap"] as const) { if (!(key in body)) continue; const value = body[key]; @@ -625,18 +625,21 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise if (val === null || val === undefined) { deleteConfigTopLevelKey(config, "modelPinnedEfforts"); } else if (typeof val === "object" && !Array.isArray(val)) { - const cleaned: Record = {}; + const efforts: Record = { ...(config.modelPinnedEfforts ?? {}) }; for (const [m, eff] of Object.entries(val as Record)) { if (!m.trim()) return jsonResponse({ error: "modelPinnedEfforts keys must be nonblank model ids" }, 400); - if (eff === null || eff === "" || eff === undefined) continue; - if (typeof eff === "string" && (isCodexReasoningEffort(eff) || eff === "none" || eff === "minimal")) { - cleaned[m.trim()] = eff; + if (eff === null || eff === "" || eff === undefined) { + delete efforts[m.trim()]; + continue; + } + if (typeof eff === "string" && isDeclaredReasoningEffort(eff)) { + efforts[m.trim()] = eff; } else { return jsonResponse({ error: `unknown reasoning effort "${String(eff)}" for model "${m}"` }, 400); } } - if (Object.keys(cleaned).length > 0) { - config.modelPinnedEfforts = cleaned; + if (Object.keys(efforts).length > 0) { + config.modelPinnedEfforts = efforts; } else { deleteConfigTopLevelKey(config, "modelPinnedEfforts"); } diff --git a/src/server/management/provider-routes.ts b/src/server/management/provider-routes.ts index fc33313b60..55226bde44 100644 --- a/src/server/management/provider-routes.ts +++ b/src/server/management/provider-routes.ts @@ -33,7 +33,7 @@ import { submitManualLoginCode, upsertOAuthProvider, } from "../../oauth"; -import { isCodexReasoningEffort } from "../../reasoning-effort"; +import { isDeclaredReasoningEffort } from "../../reasoning-effort"; import { replaceProviderAccountSet } from "../../oauth/store"; import { providerDestinationResolvedError } from "../../lib/destination-policy"; import { reconcileLiveStateStores } from "../../lib/state-store-registrations"; @@ -483,7 +483,7 @@ function applyProviderPatchFields( const value = rawBody.pinnedReasoningEffort; if (value === null || value === "") { delete next.pinnedReasoningEffort; - } else if (typeof value === "string" && (isCodexReasoningEffort(value) || value === "none" || value === "minimal")) { + } else if (typeof value === "string" && isDeclaredReasoningEffort(value)) { next.pinnedReasoningEffort = value; } else { return { error: "pinnedReasoningEffort must be a valid reasoning effort or null" }; @@ -503,7 +503,7 @@ function applyProviderPatchFields( delete efforts[model]; continue; } - if (typeof effort !== "string" || (!isCodexReasoningEffort(effort) && effort !== "none" && effort !== "minimal")) { + if (typeof effort !== "string" || !isDeclaredReasoningEffort(effort)) { return { error: `invalid reasoning effort "${String(effort)}" for model "${model}"` }; } efforts[model] = effort; diff --git a/tests/model-pinned-effort.test.ts b/tests/model-pinned-effort.test.ts index 5d0d1b2fe3..5f7d60f0dc 100644 --- a/tests/model-pinned-effort.test.ts +++ b/tests/model-pinned-effort.test.ts @@ -172,6 +172,18 @@ describe("management API pinned reasoning effort configuration", () => { const getRes = await handleManagementAPI(getReq, new URL(getReq.url), config); const data = await getRes?.json() as { modelPinnedEfforts: Record }; expect(data.modelPinnedEfforts).toEqual({ "gpt-5.5": "max", "claude-sonnet-4-6": "high" }); + + // Partial merge: add one model, clear another + const updateReq = new Request("http://localhost/api/effort-caps", { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + modelPinnedEfforts: { "gemini-3.7-flash": "high", "gpt-5.5": null }, + }), + }); + const updateRes = await handleManagementAPI(updateReq, new URL(updateReq.url), config); + expect(updateRes?.status).toBe(200); + expect(config.modelPinnedEfforts).toEqual({ "claude-sonnet-4-6": "high", "gemini-3.7-flash": "high" }); }); }); import { ManagementRequest as Request } from "./helpers/management-auth"; From 24149f38f5480b05f995a0f0b763cd85f5d46391 Mon Sep 17 00:00:00 2001 From: Liang Xu Date: Thu, 3 Sep 2026 19:56:43 +0800 Subject: [PATCH 3/6] fix(router): enforce effort cap after pin in native chat and normalize whitespace in model keys [skip ci] (cherry picked from commit a53597edda1598c5d536a970c6fa724b231fba97) Co-authored-by: Liang-Psych <19504206+Liang-Psych@users.noreply.github.com> --- src/server/chat-native.ts | 10 +++- src/server/effort-policy.ts | 67 ++++++++++++++++++++++++ src/server/management/provider-routes.ts | 9 ++-- tests/model-pinned-effort.test.ts | 50 ++++++++++++++++++ 4 files changed, 131 insertions(+), 5 deletions(-) diff --git a/src/server/chat-native.ts b/src/server/chat-native.ts index bdf30925cb..576e8b6633 100644 --- a/src/server/chat-native.ts +++ b/src/server/chat-native.ts @@ -6,7 +6,7 @@ import { collectChatCompletion, isChatCompletionsStreamError, } from "../chat/outbound"; -import { resolvePinnedEffort } from "./effort-policy"; +import { applyChatEffortCap, chatCollabSurface, effortCapAppliesTo, resolvePinnedEffort, supportedLadderFor } from "./effort-policy"; import { classifyError, cyberPolicyErrorType, @@ -162,6 +162,14 @@ export async function handleNativeChatCompletions(options: HandleNativeChatOptio ? options.chatBody.reasoning_effort : undefined; } + + const surface = chatCollabSurface(options.chatBody); + if (effortCapAppliesTo(surface, req.headers, config)) { + const capped = applyChatEffortCap(options.chatBody, req.headers, config, supportedLadderFor(route)); + if (capped) { + logCtx.requestedEffort = `${logCtx.requestedEffort ?? capped.from}->${capped.to}`; + } + } logCtx.requestedServiceTier = typeof options.chatBody.service_tier === "string" ? options.chatBody.service_tier : undefined; diff --git a/src/server/effort-policy.ts b/src/server/effort-policy.ts index d6c478d4c5..a6f0ac92a9 100644 --- a/src/server/effort-policy.ts +++ b/src/server/effort-policy.ts @@ -227,6 +227,73 @@ export function resolvePinnedEffort( * Forces the reasoning effort regardless of what the caller sent, or when the caller sent none. * Returns the rewrite transition { from, to } for logging, or null if no pinned effort applied. */ +/** + * Detect collaboration surface for a native chat request body. + * Mirrors Responses collabSurface behavior across function and custom tool representations. + */ +export function chatCollabSurface(chatBody: Record): "v1" | "v2" | null { + if (!Array.isArray(chatBody.tools)) return null; + let namespacedSpawn = false; + let flatSpawn = false; + let v1Only = false; + let v2Only = false; + for (const raw of chatBody.tools) { + if (!raw || typeof raw !== "object") continue; + const tool = raw as Record; + let name = ""; + let namespace: string | undefined = undefined; + if (tool.type === "function" && tool.function && typeof tool.function === "object") { + const fn = tool.function as Record; + name = typeof fn.name === "string" ? fn.name : ""; + } else if (tool.type === "custom" && tool.custom && typeof tool.custom === "object") { + const cust = tool.custom as Record; + name = typeof cust.name === "string" ? cust.name : ""; + } else if (typeof tool.name === "string") { + name = tool.name; + } + if (typeof tool.namespace === "string") namespace = tool.namespace; + if (name === "spawn_agent") { + if (namespace) namespacedSpawn = true; + else flatSpawn = true; + } else if (name === "send_input" || name === "resume_agent" || name === "close_agent") { + v1Only = true; + } else if (name === "send_message" || name === "followup_task" || name === "interrupt_agent" || name === "list_agents") { + v2Only = true; + } + } + if (!namespacedSpawn && !flatSpawn) return null; + if (namespacedSpawn && flatSpawn) return null; + if (v1Only && v2Only) return null; + if (v1Only) return "v1"; + if (v2Only) return "v2"; + return namespacedSpawn ? "v1" : "v2"; +} + +/** + * Apply effortCap to a native chat completions body when admitted by the collaboration gate. + */ +export function applyChatEffortCap( + chatBody: Record, + headers: Headers, + config: OcxConfig, + supported?: readonly string[] | undefined, +): { from: string; to: string; subagent: boolean } | null { + const subagent = isThreadSpawnRequest(headers); + const cap = effortCapFor(config, subagent); + if (!cap) return null; + const resolved = resolveCappedEffort(cap, supported); + const requested = typeof chatBody.reasoning_effort === "string" ? chatBody.reasoning_effort : undefined; + if (resolved === null) { + if (!requested) return null; + delete chatBody.reasoning_effort; + return { from: requested, to: "none", subagent }; + } + if (!requested || !isCodexReasoningEffort(requested)) return null; + if (codexEffortRank(requested) <= codexEffortRank(resolved)) return null; + chatBody.reasoning_effort = resolved; + return { from: requested, to: resolved, subagent }; +} + export function applyPinnedEffort( parsed: OcxParsedRequest, route: { provider: OcxProviderConfig; modelId: string }, diff --git a/src/server/management/provider-routes.ts b/src/server/management/provider-routes.ts index 55226bde44..1fc319987e 100644 --- a/src/server/management/provider-routes.ts +++ b/src/server/management/provider-routes.ts @@ -498,15 +498,16 @@ function applyProviderPatchFields( if (!isPlainRecord(value)) return { error: "modelPinnedReasoningEfforts must be a plain object or null" }; const efforts: Record = { ...(next.modelPinnedReasoningEfforts ?? {}) }; for (const [model, effort] of Object.entries(value)) { - if (!model.trim()) return { error: "modelPinnedReasoningEfforts keys must be nonblank model ids" }; + const modelId = model.trim(); + if (!modelId) return { error: "modelPinnedReasoningEfforts keys must be nonblank model ids" }; if (effort === null || effort === "") { - delete efforts[model]; + delete efforts[modelId]; continue; } if (typeof effort !== "string" || !isDeclaredReasoningEffort(effort)) { - return { error: `invalid reasoning effort "${String(effort)}" for model "${model}"` }; + return { error: `invalid reasoning effort "${String(effort)}" for model "${modelId}"` }; } - efforts[model] = effort; + efforts[modelId] = effort; } if (Object.keys(efforts).length > 0) next.modelPinnedReasoningEfforts = efforts; else delete next.modelPinnedReasoningEfforts; diff --git a/tests/model-pinned-effort.test.ts b/tests/model-pinned-effort.test.ts index 5f7d60f0dc..c647c21b12 100644 --- a/tests/model-pinned-effort.test.ts +++ b/tests/model-pinned-effort.test.ts @@ -127,6 +127,30 @@ describe("management API pinned reasoning effort configuration", () => { expect(provider.pinnedReasoningEffort).toBe("high"); expect(provider.modelPinnedReasoningEfforts).toEqual({ "model-a": "max", "model-b": "low" }); + // Updating with whitespace key normalizes to trimmed model id + const wsReq = new Request("http://localhost/api/providers?name=custom", { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + modelPinnedReasoningEfforts: { " model-c ": "medium" }, + }), + }); + const wsRes = await handleManagementAPI(wsReq, new URL(wsReq.url), config); + expect(wsRes?.status).toBe(200); + expect(config.providers.custom.modelPinnedReasoningEfforts).toEqual({ "model-a": "max", "model-b": "low", "model-c": "medium" }); + + // Clearing a model pinned effort with whitespace key + const wsClearReq = new Request("http://localhost/api/providers?name=custom", { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + modelPinnedReasoningEfforts: { " model-c ": null }, + }), + }); + const wsClearRes = await handleManagementAPI(wsClearReq, new URL(wsClearReq.url), config); + expect(wsClearRes?.status).toBe(200); + expect(config.providers.custom.modelPinnedReasoningEfforts).toEqual({ "model-a": "max", "model-b": "low" }); + // Clearing a model pinned effort const clearReq = new Request("http://localhost/api/providers?name=custom", { method: "PATCH", @@ -187,3 +211,29 @@ describe("management API pinned reasoning effort configuration", () => { }); }); import { ManagementRequest as Request } from "./helpers/management-auth"; + +describe("native chat completions effort policy", () => { + const { chatCollabSurface, applyChatEffortCap } = require("../src/server/effort-policy"); + + test("detects v2 collab surface in native chat tools", () => { + const chatBody = { + tools: [ + { type: "function", function: { name: "spawn_agent" } }, + { type: "function", function: { name: "send_message" } }, + ], + }; + expect(chatCollabSurface(chatBody)).toBe("v2"); + }); + + test("applyChatEffortCap respects effortCap ceiling over pinned effort", () => { + const config = { + effortCap: "low", + }; + const chatBody = { + reasoning_effort: "max", + }; + const rewrite = applyChatEffortCap(chatBody, new Headers(), config, ["low", "medium", "high", "max"]); + expect(rewrite).toEqual({ from: "max", to: "low", subagent: false }); + expect(chatBody.reasoning_effort).toBe("low"); + }); +}); From 5865367751df480e6d41493888acebf7868f77e1 Mon Sep 17 00:00:00 2001 From: t Date: Mon, 7 Sep 2026 20:20:11 +0900 Subject: [PATCH 4/6] fix: adapt pinned effort carry to current config and wire contracts [skip ci] Keep pins operator-owned, validate configuration and management writes, preserve omission/default semantics, and apply caps before wire mapping. Carry synthetic/combo effective effort into final destination normalization. Add persistence, DTO, wire and retry regression coverage in domain tests. Source review and centrally scheduled remote CI remain required; no local suite, typecheck, build or installation was run. Co-authored-by: Liang-Psych <19504206+Liang-Psych@users.noreply.github.com> --- .../docs/reference/configuration/agents.md | 26 + .../docs/reference/configuration/providers.md | 29 + scripts/test-layout/layout.json | 4 +- src/config.ts | 72 ++- src/config/provider-validation.ts | 71 ++- src/providers/derive.ts | 21 +- src/router.ts | 4 - src/server/auth-cors.ts | 8 + src/server/chat-native.ts | 86 ++- src/server/effort-policy.ts | 73 ++- .../management/agent-settings-routes.ts | 80 +-- src/server/management/provider-routes.ts | 145 +++-- src/server/responses/core.ts | 5 +- src/types/config.ts | 6 +- src/types/provider.ts | 11 +- structure/03_catalog-and-subagents.md | 9 + .../model-pinned-effort.test.ts | 568 ++++++++++++++++++ .../config/model-pinned-effort-config.test.ts | 391 ++++++++++++ tests/fixtures/test-layout-expected.json | 4 +- tests/model-pinned-effort.test.ts | 239 -------- 20 files changed, 1452 insertions(+), 400 deletions(-) create mode 100644 tests/codex-integration/model-pinned-effort.test.ts create mode 100644 tests/config/model-pinned-effort-config.test.ts delete mode 100644 tests/model-pinned-effort.test.ts diff --git a/docs-site/src/content/docs/reference/configuration/agents.md b/docs-site/src/content/docs/reference/configuration/agents.md index 54affc94ad..f3caea02f2 100644 --- a/docs-site/src/content/docs/reference/configuration/agents.md +++ b/docs-site/src/content/docs/reference/configuration/agents.md @@ -226,3 +226,29 @@ apply. `max` and `ultra` are accepted, while the dashboard offers `low` through For a beginner-oriented explanation of v1, default, and v2 behavior, see [Sub-agent surfaces](/guides/sub-agent-surface/). + +## Global model effort pins + +The optional root `modelPinnedEfforts` map fills or overrides incoming effort choices when +neither a provider model pin nor a provider-wide pin is configured. For example: + +```json +{ + "modelPinnedEfforts": { + "example-provider/example-model": "high" + } +} +``` + +Lookup checks the final selector before provider-prefix normalization, then the qualified +`provider/model` destination, then its bare upstream model ID. Original combo aliases and +synthetic effort-row selector IDs are not global pin keys; configure the concrete destination. +Synthetic-row effort and combo defaults are preserved as the effective input before pinning. +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. + +`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 +entry set to `null` or `""` deletes only that key. Invalid combined updates leave both caps +and pins unchanged. Saving a pin does not alter the featured subagent roster. diff --git a/docs-site/src/content/docs/reference/configuration/providers.md b/docs-site/src/content/docs/reference/configuration/providers.md index 0deb12616f..3c72c4b302 100644 --- a/docs-site/src/content/docs/reference/configuration/providers.md +++ b/docs-site/src/content/docs/reference/configuration/providers.md @@ -209,6 +209,35 @@ to the native default as a single choice. Defaults must belong to the final list the catalog projection, not stored configuration or arbitrary gateway models sharing a GPT name. See [custom native catalog examples](/guides/codex-app-models/). +### Operator-pinned reasoning effort + +Set `pinnedReasoningEffort` on an existing provider to override incoming effort choices, or +use `modelPinnedReasoningEfforts` for individual upstream model IDs. Per-model provider pins +win over the provider-wide pin; the root `modelPinnedEfforts` map is the fallback. These are +operator settings, not provider-registry defaults. They do not change model discovery or the +advertised effort ladder. + +```json +{ + "pinnedReasoningEffort": "high", + "modelPinnedReasoningEfforts": { + "example-model": "max" + } +} +``` + +Merge these fields into the existing provider row. Accepted values are `none`, `minimal`, +`low`, `medium`, `high`, `xhigh`, `max`, and `ultra`. **`none` removes the explicit effort field**; +it uses the provider's default behavior and does not guarantee that reasoning is disabled. +Applicable effort caps still run after the pin, and provider wire mapping/normalization can +lower or omit an unsupported value. `ultra` is normalized before it reaches an upstream wire. +Compaction maintenance requests are exempt from pins. + +`PATCH /api/providers?name=` accepts these fields. Omit a field to preserve it; +use `null` to clear a scalar or the whole map. A map entry set to `null` or `""` removes that +entry while preserving other entries. Malformed writes are rejected before saving. A malformed +optional pin in a hand-edited file is ignored on load without discarding the rest of the config. + ### Discovered model display names Use `modelDisplayNames` when a provider returns machine friendly ids but the Codex model picker diff --git a/scripts/test-layout/layout.json b/scripts/test-layout/layout.json index d46f547867..a1b6e1d46e 100644 --- a/scripts/test-layout/layout.json +++ b/scripts/test-layout/layout.json @@ -1301,7 +1301,9 @@ "zz-pr-coderabbit-readiness-revalidation.test.ts": "ci-workflows", "cli-models-price.test.ts": "cli", "model-costs-management-api.test.ts": "server", - "usage-time-range.test.ts": "usage" + "usage-time-range.test.ts": "usage", + "model-pinned-effort.test.ts": "codex-integration", + "model-pinned-effort-config.test.ts": "config" }, "migrated": [ "adapters", diff --git a/src/config.ts b/src/config.ts index d25aa07ad2..8da89cbfdf 100644 --- a/src/config.ts +++ b/src/config.ts @@ -9,6 +9,9 @@ export { DEFAULT_SUBAGENT_MODELS } from "./config/subagent-models"; import { apiKeyTransportConfigError, booleanRecordConfigError, + configReasoningPinsConfigError, + modelPinnedEffortsConfigError, + pinnedReasoningEffortConfigError, modelAdapterRecordConfigError, modelDisplayNamesConfigError, nonBlankStringArrayConfigError, @@ -515,11 +518,25 @@ const modelDisplayNamesSchema = z.unknown().superRefine((value, ctx) => { return labels; }); +const pinnedReasoningEffortSchema = z.unknown().superRefine((value, ctx) => { + const error = pinnedReasoningEffortConfigError(value); + if (error) ctx.addIssue({ code: "custom", message: error }); +}).transform(value => value as string); + +const modelPinnedEffortsSchema = z.unknown().superRefine((value, ctx) => { + const error = modelPinnedEffortsConfigError(value); + if (error) ctx.addIssue({ code: "custom", message: error }); +}).transform(value => Object.fromEntries( + Object.entries(value as Record).map(([key, effort]) => [key.trim(), effort]), +)); + /** * Zod schema for one provider entry: known fields are validated strictly while unknown * fields pass through (preserved for runtime extensions). */ const providerConfigSchema = z.object({ + pinnedReasoningEffort: pinnedReasoningEffortSchema.optional(), + modelPinnedReasoningEfforts: modelPinnedEffortsSchema.optional(), adapter: z.string().min(1), baseUrl: z.string().min(1), alias: z.string().optional(), @@ -1122,6 +1139,7 @@ const configSchema = z.object({ z.object({ enabled: z.literal(true), port: z.number().int().min(1).max(65535) }), ]).optional().catch(undefined), providers: z.record(z.string(), providerConfigSchema), + modelPinnedEfforts: modelPinnedEffortsSchema.optional(), defaultProvider: z.string().min(1).default("openai"), defaultModelAliases: z.boolean().optional(), // Malformed hand edits disable this opt-in projection without rejecting providers. @@ -1612,6 +1630,49 @@ export function hardenExistingSecret(path: string): void { } } } +/** Load only: discard invalid optional pins without rewriting the file or losing providers. */ +function sanitizeReasoningPinsForLoad(parsed: unknown): void { + if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return; + const root = parsed as Record; + let degraded = false; + const sanitizeMap = (owner: Record, field: string) => { + const value = owner[field]; + if (value === undefined) return; + if (!value || typeof value !== "object" || Array.isArray(value) + || ![Object.prototype, null].includes(Object.getPrototypeOf(value))) { + delete owner[field]; + degraded = true; + return; + } + const counts = new Map(); + for (const key of Object.keys(value)) counts.set(key.trim(), (counts.get(key.trim()) ?? 0) + 1); + const valid: Record = Object.create(null); + for (const [key, effort] of Object.entries(value)) { + if (counts.get(key.trim()) !== 1 || modelPinnedEffortsConfigError({ [key]: effort }) !== null) { + degraded = true; + continue; + } + valid[key.trim()] = effort as string; + } + if (Object.keys(valid).length) owner[field] = valid; + else delete owner[field]; + }; + sanitizeMap(root, "modelPinnedEfforts"); + if (root.providers && typeof root.providers === "object" && !Array.isArray(root.providers)) { + for (const value of Object.values(root.providers)) { + if (!value || typeof value !== "object" || Array.isArray(value)) continue; + const provider = value as Record; + if (pinnedReasoningEffortConfigError(provider.pinnedReasoningEffort)) { + delete provider.pinnedReasoningEffort; + degraded = true; + } + sanitizeMap(provider, "modelPinnedReasoningEfforts"); + } + } + // Never include a provider/model name or value: malformed pins can contain secrets. + if (degraded) console.warn("config.json contains invalid optional reasoning pins — ignoring invalid fields or entries"); +} + /** * The schema's `.catch(undefined)` silently degrades an invalid persisted * `streamMode` to "auto"; surface that once so a hand-edited typo (e.g. @@ -2190,6 +2251,7 @@ export function loadConfig(): OcxConfig { const raw = readFileSync(configPath, "utf-8").replace(/^\uFEFF/, ""); const parsed = JSON.parse(raw); sanitizeAliasesForLoad(parsed); + sanitizeReasoningPinsForLoad(parsed); sanitizeModelDisplayNamesForLoad(parsed); sanitizeRetryOn429ForLoad(parsed); sanitizeModelCostsForLoad(parsed); @@ -2722,7 +2784,8 @@ function managementIngressConfigError(value: unknown): string | null { } export function validateConfigCandidate(value: unknown): { ok: true; config: OcxConfig } | { ok: false; error: string } { - const boundaryError = blankHostnameError(value) + const boundaryError = configReasoningPinsConfigError(value) + ?? blankHostnameError(value) ?? claudeSubagentEffortError(value) ?? appOwnedMemoryBudgetError(value) ?? upstreamHostCircuitThresholdError(value) @@ -2752,6 +2815,7 @@ export function validateConfigCandidate(value: unknown): { ok: true; config: Ocx function configDiagnosticsFromRaw(raw: string): ConfigDiagnostics { try { const parsed = JSON.parse(raw.replace(/^\uFEFF/, "")); + sanitizeReasoningPinsForLoad(parsed); // Same degradation as loadConfig: a hand-edited invalid retryOn429 must not trip the // schema and send the caller a default-config fallback (the config command could then // persist that fallback over the user's providers/keys). @@ -3101,6 +3165,8 @@ export const withExpectedConfigGenerationSync: WithExpectedConfigGenerationSync * every save path. */ function persistConfigUnlocked(config: OcxConfig): boolean { + const pinError = configReasoningPinsConfigError(config); + if (pinError) throw new Error(pinError); const configPath = getConfigPath(); const rawBeforeWrite = readRawConfigJson(); const clientPersistenceError = failClosedClientPersistenceError(rawBeforeWrite, config); @@ -3176,6 +3242,8 @@ export function initializePersistedConfigIfMissing( /** Persist `config` to config.json under the config-mutation lock. */ export function saveConfig(config: OcxConfig): void { + const pinError = configReasoningPinsConfigError(config); + if (pinError) throw new Error(pinError); // Keep the real-home assertion ahead of even lock-directory preparation. assertNotRealHomeUnderTest(getConfigDir()); withConfigMutationLockSync(() => { @@ -3633,6 +3701,8 @@ function readPersistedServerBinding( * edits and deletions across stale whole-config saves. */ export function saveConfigPreservingClaudeCode(config: OcxConfig): void { + const pinError = configReasoningPinsConfigError(config); + if (pinError) throw new Error(pinError); withConfigMutationLockSync(() => { const bindingBaseline = persistedLiveServerBinding.get(config); // One authoritative pre-write read feeds both the live-config reconciliation and diff --git a/src/config/provider-validation.ts b/src/config/provider-validation.ts index 326914a758..c1e60033e8 100644 --- a/src/config/provider-validation.ts +++ b/src/config/provider-validation.ts @@ -4,7 +4,7 @@ import { isValidModelDiscoveryModelId, MODEL_DISCOVERY_MAX_MODELS, } from "../providers/model-discovery-limits"; -import { modelRecordValue } from "../reasoning-effort"; +import { isDeclaredReasoningEffort, modelRecordValue } from "../reasoning-effort"; import { isWirePinnedModel, MODEL_ADAPTER_OVERRIDE_ALLOWED, @@ -27,6 +27,75 @@ const REASONING_SUMMARY_DELIVERY_SET = new Set(REASONING_SUMMARY_DELIVER const DISPLAY_NAME_CONTROL_CHARS = /[\u0000-\u001f\u007f-\u009f\u2028\u2029]/; const MAX_MODEL_DISPLAY_NAME_LENGTH = 128; +/** Operator pins share one strict boundary across config and management writes. */ +export function pinnedReasoningEffortConfigError(value: unknown, allowClear = false): string | null { + if (value === undefined || (allowClear && (value === null || value === ""))) return null; + return typeof value === "string" && isDeclaredReasoningEffort(value) + ? null : "pinnedReasoningEffort must be a declared reasoning effort"; +} + +export function modelPinnedEffortsConfigError( + value: unknown, + field = "modelPinnedEfforts", + allowTombstones = false, +): string | null { + if (value === undefined || (allowTombstones && value === null)) return null; + if (!value || typeof value !== "object" || Array.isArray(value) + || ![Object.prototype, null].includes(Object.getPrototypeOf(value))) { + return `${field} must be a plain object`; + } + const keys = new Set(); + for (const [key, effort] of Object.entries(value)) { + const normalized = key.trim(); + if (!normalized || ["__proto__", "prototype", "constructor"].includes(normalized)) { + return `${field} keys must be nonblank model ids and must not be reserved object keys`; + } + if (keys.has(normalized)) return `${field} keys must be unique after trimming`; + keys.add(normalized); + if (allowTombstones && (effort === null || effort === "")) continue; + if (typeof effort !== "string" || !isDeclaredReasoningEffort(effort)) { + return `${field} values must be declared reasoning efforts`; + } + } + return null; +} + +/** Apply a validated map patch; null clears the field, entry tombstones remove one key. */ +export function mergeModelPinnedEfforts( + current: Record | undefined, + patch: unknown, +): Record | undefined { + if (patch === undefined) return current === undefined ? undefined : { ...current }; + if (patch === null) return undefined; + const next = Object.fromEntries(Object.entries(current ?? {}).map(([key, value]) => [key.trim(), value])); + for (const [key, effort] of Object.entries(patch as Record)) { + if (effort === null || effort === "") delete next[key.trim()]; + else next[key.trim()] = effort; + } + return Object.keys(next).length ? next : undefined; +} + +export function providerReasoningPinsConfigError(provider: Record): string | null { + return pinnedReasoningEffortConfigError(provider.pinnedReasoningEffort) + ?? modelPinnedEffortsConfigError(provider.modelPinnedReasoningEfforts, "modelPinnedReasoningEfforts"); +} + +/** Validate only pin fields, including callers that bypass the whole-config schema. */ +export function configReasoningPinsConfigError(value: unknown): string | null { + if (!value || typeof value !== "object") return null; + const raw = value as Record; + const globalError = modelPinnedEffortsConfigError(raw.modelPinnedEfforts); + if (globalError) return globalError; + if (raw.providers && typeof raw.providers === "object") { + for (const provider of Object.values(raw.providers)) { + if (!provider || typeof provider !== "object") continue; + const error = providerReasoningPinsConfigError(provider as Record); + if (error) return error; + } + } + return null; +} + /** Validate a provider destination without coupling DTO callers to config persistence. */ export function providerBaseUrlConfigError(baseUrl: string): string | null { try { diff --git a/src/providers/derive.ts b/src/providers/derive.ts index 8beac62f16..02852fce39 100644 --- a/src/providers/derive.ts +++ b/src/providers/derive.ts @@ -32,8 +32,6 @@ export interface DerivedKeyLoginProvider { reasoningEfforts?: string[]; modelReasoningEfforts?: Record; modelDefaultReasoningEfforts?: Record; - pinnedReasoningEffort?: string; - modelPinnedReasoningEfforts?: Record; reasoningEffortMap?: Record; modelReasoningEffortMap?: Record>; reasoningWireFormat?: OcxProviderConfig["reasoningWireFormat"]; @@ -126,7 +124,7 @@ function sameStringArray(left: readonly string[] | undefined, right: readonly st type DirectReasoningEffortOverrides = Pick< OcxProviderConfig, - "thinkingBudgetModels" | "modelReasoningEfforts" | "modelDefaultReasoningEfforts" | "pinnedReasoningEffort" | "modelPinnedReasoningEfforts" | "modelReasoningEffortMap" + "thinkingBudgetModels" | "modelReasoningEfforts" | "modelDefaultReasoningEfforts" | "modelReasoningEffortMap" >; function fillFoldedModelDefault( @@ -196,15 +194,6 @@ export function applyDirectReasoningEffortContracts( value => value, ); - const pinnedEffort = entry.modelPinnedReasoningEfforts?.[model]; - prov.modelPinnedReasoningEfforts = fillFoldedModelDefault( - prov.modelPinnedReasoningEfforts, - explicit.modelPinnedReasoningEfforts, - model, - pinnedEffort, - value => value, - ); - // An explicit empty model map masks any provider-wide aliases. Without it, a stale global // mapping such as xhigh -> max would win before the verified direct ladder can clamp it. prov.modelReasoningEffortMap = fillFoldedModelDefault( @@ -251,8 +240,6 @@ export function providerConfigSeed(entry: ProviderRegistryEntry): OcxProviderCon ...(entry.reasoningEfforts ? { reasoningEfforts: [...entry.reasoningEfforts] } : {}), ...(entry.modelReasoningEfforts ? { modelReasoningEfforts: cloneRecordOfArrays(entry.modelReasoningEfforts) } : {}), ...(entry.modelDefaultReasoningEfforts ? { modelDefaultReasoningEfforts: { ...entry.modelDefaultReasoningEfforts } } : {}), - ...(entry.pinnedReasoningEffort !== undefined ? { pinnedReasoningEffort: entry.pinnedReasoningEffort } : {}), - ...(entry.modelPinnedReasoningEfforts ? { modelPinnedReasoningEfforts: { ...entry.modelPinnedReasoningEfforts } } : {}), ...(entry.reasoningEffortMap ? { reasoningEffortMap: { ...entry.reasoningEffortMap } } : {}), ...(entry.modelReasoningEffortMap ? { modelReasoningEffortMap: cloneNestedRecord(entry.modelReasoningEffortMap) } : {}), ...(entry.reasoningWireFormat ? { reasoningWireFormat: entry.reasoningWireFormat } : {}), @@ -313,8 +300,6 @@ export function deriveKeyLoginMap(): Record { ...(entry.reasoningEfforts ? { reasoningEfforts: [...entry.reasoningEfforts] } : {}), ...(entry.modelReasoningEfforts ? { modelReasoningEfforts: cloneRecordOfArrays(entry.modelReasoningEfforts) } : {}), ...(entry.modelDefaultReasoningEfforts ? { modelDefaultReasoningEfforts: { ...entry.modelDefaultReasoningEfforts } } : {}), - ...(entry.pinnedReasoningEffort !== undefined ? { pinnedReasoningEffort: entry.pinnedReasoningEffort } : {}), - ...(entry.modelPinnedReasoningEfforts ? { modelPinnedReasoningEfforts: { ...entry.modelPinnedReasoningEfforts } } : {}), ...(entry.reasoningEffortMap ? { reasoningEffortMap: { ...entry.reasoningEffortMap } } : {}), ...(entry.modelReasoningEffortMap ? { modelReasoningEffortMap: cloneNestedRecord(entry.modelReasoningEffortMap) } : {}), ...(entry.reasoningWireFormat ? { reasoningWireFormat: entry.reasoningWireFormat } : {}), @@ -482,8 +467,6 @@ export function enrichProviderFromRegistry(name: string, prov: OcxProviderConfig thinkingBudgetModels: prov.thinkingBudgetModels, modelReasoningEfforts: prov.modelReasoningEfforts, modelDefaultReasoningEfforts: prov.modelDefaultReasoningEfforts, - pinnedReasoningEffort: prov.pinnedReasoningEffort, - modelPinnedReasoningEfforts: prov.modelPinnedReasoningEfforts, modelReasoningEffortMap: prov.modelReasoningEffortMap, }; const seed = providerConfigSeed(entry); @@ -517,8 +500,6 @@ export function enrichProviderFromRegistry(name: string, prov: OcxProviderConfig prov.modelReasoningEfforts = fillRecordOfArrays(seed.modelReasoningEfforts, prov.modelReasoningEfforts); } if (!prov.modelDefaultReasoningEfforts && seed.modelDefaultReasoningEfforts) prov.modelDefaultReasoningEfforts = { ...seed.modelDefaultReasoningEfforts }; - if (!prov.pinnedReasoningEffort && seed.pinnedReasoningEffort) prov.pinnedReasoningEffort = seed.pinnedReasoningEffort; - if (!prov.modelPinnedReasoningEfforts && seed.modelPinnedReasoningEfforts) prov.modelPinnedReasoningEfforts = { ...seed.modelPinnedReasoningEfforts }; if (!prov.reasoningEffortMap && seed.reasoningEffortMap) prov.reasoningEffortMap = { ...seed.reasoningEffortMap }; if (!prov.modelReasoningEffortMap && seed.modelReasoningEffortMap) prov.modelReasoningEffortMap = cloneNestedRecord(seed.modelReasoningEffortMap); if (prov.reasoningWireFormat === undefined && seed.reasoningWireFormat !== undefined) prov.reasoningWireFormat = seed.reasoningWireFormat; diff --git a/src/router.ts b/src/router.ts index cfe0506224..b2f887f0a9 100644 --- a/src/router.ts +++ b/src/router.ts @@ -321,8 +321,6 @@ export function routedProviderConfig(providerName: string, provider: OcxProvider const modelReasoningEffortMap = mergeNestedRecord(registryEntry.modelReasoningEffortMap, provider.modelReasoningEffortMap); const modelReasoningEfforts = mergeStringArrayRecord(registryEntry.modelReasoningEfforts, provider.modelReasoningEfforts); const modelDefaultReasoningEfforts = mergeRecordFill(registryEntry.modelDefaultReasoningEfforts, provider.modelDefaultReasoningEfforts); - const modelPinnedReasoningEfforts = mergeRecordFill(registryEntry.modelPinnedReasoningEfforts, provider.modelPinnedReasoningEfforts); - const pinnedReasoningEffort = provider.pinnedReasoningEffort ?? registryEntry.pinnedReasoningEffort; const modelContextWindows = providerName === OPENAI_API_PROVIDER_ID ? mergePositiveNumberCaps(registryEntry.modelContextWindows, provider.modelContextWindows) : mergeRecordFill(registryEntry.modelContextWindows, provider.modelContextWindows); @@ -468,8 +466,6 @@ export function routedProviderConfig(providerName: string, provider: OcxProvider ...(modelSupportsVerbosity ? { modelSupportsVerbosity } : {}), ...(modelReasoningEfforts ? { modelReasoningEfforts } : {}), ...(modelDefaultReasoningEfforts ? { modelDefaultReasoningEfforts } : {}), - ...(pinnedReasoningEffort ? { pinnedReasoningEffort } : {}), - ...(modelPinnedReasoningEfforts ? { modelPinnedReasoningEfforts } : {}), ...(reasoningEffortMap ? { reasoningEffortMap } : {}), ...(modelReasoningEffortMap ? { modelReasoningEffortMap } : {}), ...(noVisionModels ? { noVisionModels } : {}), diff --git a/src/server/auth-cors.ts b/src/server/auth-cors.ts index 0dd49910fb..476fd3a4ae 100644 --- a/src/server/auth-cors.ts +++ b/src/server/auth-cors.ts @@ -13,6 +13,7 @@ import { import { apiKeyTransportConfigError, booleanRecordConfigError, + providerReasoningPinsConfigError, modelAdapterRecordConfigError, nonBlankStringArrayConfigError, positiveIntegerConfigError, @@ -581,6 +582,8 @@ export function providerManagementConfigError(name: unknown, provider: unknown): return "provider must be a plain object"; } const raw = provider as Record; + const pinsError = providerReasoningPinsConfigError(raw); + if (pinsError) return pinsError; for (const field of FORBIDDEN_PROVIDER_RUNTIME_FIELDS) { if (Object.hasOwn(raw, field)) return `provider ${name} must not include runtime field "${field}"`; } @@ -594,6 +597,9 @@ export function providerManagementConfigError(name: unknown, provider: unknown): } if (seed) seed.codexAccountMode = raw.codexAccountMode; const canonicalCandidate = { ...raw }; + // Validated operator overlays do not change the canonical auth/transport seed. + delete canonicalCandidate.pinnedReasoningEffort; + delete canonicalCandidate.modelPinnedReasoningEfforts; delete canonicalCandidate.responsesSnapshotRepair; // modelCosts is a user-owned display overlay, not part of the canonical // forward seed; it is validated separately below (providerModelCostsConfigError). @@ -829,6 +835,8 @@ const PROVIDER_CONFIG_FIELD_POLICY = { reasoningEfforts: "editor", modelReasoningEfforts: "editor", modelDefaultReasoningEfforts: "editor", + pinnedReasoningEffort: "editor", + modelPinnedReasoningEfforts: "editor", modelSupportsReasoningSummaries: "editor", modelSupportsVerbosity: "editor", supportsVerbosity: "editor", diff --git a/src/server/chat-native.ts b/src/server/chat-native.ts index 576e8b6633..9abb99683e 100644 --- a/src/server/chat-native.ts +++ b/src/server/chat-native.ts @@ -7,6 +7,7 @@ import { isChatCompletionsStreamError, } from "../chat/outbound"; import { applyChatEffortCap, chatCollabSurface, effortCapAppliesTo, resolvePinnedEffort, supportedLadderFor } from "./effort-policy"; +import { mapReasoningEffort } from "../reasoning-effort"; import { classifyError, cyberPolicyErrorType, @@ -61,6 +62,68 @@ type Rec = Record; const MAX_NATIVE_CHAT_JSON_BYTES = 32 * 1024 * 1024; const MAX_NATIVE_CHAT_ERROR_BYTES = 64 * 1024; +const chatEffortSnapshots = new WeakMap(); + +function normalizePinnedChatEffort(options: HandleNativeChatOptions): void { + const { chatBody, route, config, req, logCtx, requestedModel } = options; + let snapshot = chatEffortSnapshots.get(chatBody); + const inputModel = typeof chatBody.model === "string" ? chatBody.model : requestedModel; + let selector = inputModel; + if (snapshot) { + if (snapshot.providerName === route.providerName && snapshot.modelId === route.modelId) { + logCtx.requestedEffort = snapshot.annotation; + return; + } + if (snapshot.present) chatBody.reasoning_effort = snapshot.value; + else delete chatBody.reasoning_effort; + if (selector === snapshot.inputModel || selector === snapshot.modelId) { + selector = `${route.providerName}/${route.modelId}`; + } + } else { + snapshot = { + inputModel, + providerName: route.providerName, + modelId: route.modelId, + present: Object.hasOwn(chatBody, "reasoning_effort"), + value: chatBody.reasoning_effort, + annotation: undefined, + }; + chatEffortSnapshots.set(chatBody, snapshot); + } + snapshot.inputModel = inputModel; + snapshot.providerName = route.providerName; + snapshot.modelId = route.modelId; + const from = typeof chatBody.reasoning_effort === "string" ? chatBody.reasoning_effort : undefined; + logCtx.requestedEffort = from; + // Compaction is normally excluded by native-route eligibility; preserve that boundary here too. + const pinned = chatBody.compaction_trigger === undefined + ? resolvePinnedEffort(route, selector, config) + : undefined; + if (pinned !== undefined) { + logCtx.requestedEffort = from ? `${from}->${pinned}` : pinned; + if (pinned === "none") delete chatBody.reasoning_effort; + else chatBody.reasoning_effort = pinned; + // The native lane historically passes caller effort through, including with caps set. + // Only a newly operator-pinned value enters the cap and provider-mapping pipeline. + if (effortCapAppliesTo(chatCollabSurface(chatBody), req.headers, config)) { + const capped = applyChatEffortCap(chatBody, req.headers, config, supportedLadderFor(route)); + if (capped) logCtx.requestedEffort = `${logCtx.requestedEffort}->${capped.to}`; + } + const effort = typeof chatBody.reasoning_effort === "string" ? chatBody.reasoning_effort : undefined; + const wireEffort = mapReasoningEffort(route.provider, route.modelId, effort); + if (wireEffort === undefined) delete chatBody.reasoning_effort; + else chatBody.reasoning_effort = wireEffort; + } + snapshot.annotation = logCtx.requestedEffort; +} + function isRec(value: unknown): value is Rec { return value !== null && typeof value === "object" && !Array.isArray(value); } @@ -148,28 +211,7 @@ export async function handleNativeChatCompletions(options: HandleNativeChatOptio return chatCompletionsErrorResponse(status, safeMessage, type, code); }; - const pinnedEffort = resolvePinnedEffort(route, requestedModel, config); - if (pinnedEffort) { - const from = typeof options.chatBody.reasoning_effort === "string" ? options.chatBody.reasoning_effort : undefined; - logCtx.requestedEffort = from ? `${from}->${pinnedEffort}` : pinnedEffort; - if (pinnedEffort === "none") { - delete options.chatBody.reasoning_effort; - } else { - options.chatBody.reasoning_effort = pinnedEffort; - } - } else { - logCtx.requestedEffort = typeof options.chatBody.reasoning_effort === "string" - ? options.chatBody.reasoning_effort - : undefined; - } - - const surface = chatCollabSurface(options.chatBody); - if (effortCapAppliesTo(surface, req.headers, config)) { - const capped = applyChatEffortCap(options.chatBody, req.headers, config, supportedLadderFor(route)); - if (capped) { - logCtx.requestedEffort = `${logCtx.requestedEffort ?? capped.from}->${capped.to}`; - } - } + normalizePinnedChatEffort(options); logCtx.requestedServiceTier = typeof options.chatBody.service_tier === "string" ? options.chatBody.service_tier : undefined; diff --git a/src/server/effort-policy.ts b/src/server/effort-policy.ts index a6f0ac92a9..5a8b63af7a 100644 --- a/src/server/effort-policy.ts +++ b/src/server/effort-policy.ts @@ -195,11 +195,14 @@ export function applyEffortCap( * 1. Provider model-specific pinned effort (`provider.modelPinnedReasoningEfforts[modelId]`) * 2. Provider-wide pinned effort (`provider.pinnedReasoningEffort`) * 3. Global config model-specific pinned effort (`config.modelPinnedEfforts[modelId]`) + * Global keys try the final pre-namespace selector, provider-qualified destination, + * then bare destination, using modelRecordValue's exact/family/case-fold semantics. + * The caller removes synthetic effort rows and combo selectors before this boundary. * * Returns undefined when no valid pinned effort tier is configured. */ export function resolvePinnedEffort( - route: { provider: OcxProviderConfig; modelId: string }, + route: { provider: OcxProviderConfig; modelId: string; providerName?: string }, parsedModelId?: string, config?: OcxConfig, ): string | undefined { @@ -213,8 +216,9 @@ export function resolvePinnedEffort( return prov.pinnedReasoningEffort; } if (config?.modelPinnedEfforts) { - const rawGlobal = modelRecordValue(config.modelPinnedEfforts, route.modelId) - ?? (parsedModelId ? modelRecordValue(config.modelPinnedEfforts, parsedModelId) : undefined); + const rawGlobal = (parsedModelId ? modelRecordValue(config.modelPinnedEfforts, parsedModelId) : undefined) + ?? (route.providerName ? modelRecordValue(config.modelPinnedEfforts, `${route.providerName}/${route.modelId}`) : undefined) + ?? modelRecordValue(config.modelPinnedEfforts, route.modelId); if (rawGlobal && isDeclaredReasoningEffort(rawGlobal)) { return rawGlobal; } @@ -222,11 +226,60 @@ export function resolvePinnedEffort( return undefined; } -/** - * Apply any pinned reasoning effort to the parsed request and raw body in BOTH shapes. - * Forces the reasoning effort regardless of what the caller sent, or when the caller sent none. - * Returns the rewrite transition { from, to } for logging, or null if no pinned effort applied. +interface EffortSnapshot { + selector: string; + providerName: string; + modelId: string; + reasoningPresent: boolean; + reasoning: OcxParsedRequest["options"]["reasoning"]; + rawEffortPresent: boolean; + rawEffort: unknown; +} + +const effortSnapshots = new WeakMap(); + +/** Capture effective synthetic/combo defaults before final model namespace rewriting. + * A different destination restores effort alone; intervening summary/options edits survive. + * Credential retries do not change the destination and retain their existing decision. */ +export function prepareEffortNormalization( + parsed: OcxParsedRequest, + route: { providerName: string; modelId: string }, +): string { + const raw = parsed._rawBody as { reasoning?: Record } | undefined; + const previous = effortSnapshots.get(parsed); + if (!previous) { + effortSnapshots.set(parsed, { + selector: parsed.modelId, + providerName: route.providerName, + modelId: route.modelId, + reasoningPresent: Object.hasOwn(parsed.options, "reasoning"), + reasoning: parsed.options.reasoning, + rawEffortPresent: !!raw?.reasoning && Object.hasOwn(raw.reasoning, "effort"), + rawEffort: raw?.reasoning?.effort, + }); + return parsed.modelId; + } + if (previous.providerName === route.providerName && previous.modelId === route.modelId) { + return previous.selector; + } + if (previous.reasoningPresent) parsed.options.reasoning = previous.reasoning; + else delete parsed.options.reasoning; + if (raw && previous.rawEffortPresent) { + if (!raw.reasoning || typeof raw.reasoning !== "object") raw.reasoning = {}; + raw.reasoning.effort = previous.rawEffort; + } else if (raw?.reasoning && typeof raw.reasoning === "object") { + delete raw.reasoning.effort; + } + // An unchanged wire model is the previous destination, not a new requested alias. + previous.selector = parsed.modelId === previous.modelId || parsed.modelId === previous.selector + ? `${route.providerName}/${route.modelId}` + : parsed.modelId; + previous.providerName = route.providerName; + previous.modelId = route.modelId; + return previous.selector; +} + /** * Detect collaboration surface for a native chat request body. * Mirrors Responses collabSurface behavior across function and custom tool representations. @@ -296,10 +349,12 @@ export function applyChatEffortCap( export function applyPinnedEffort( parsed: OcxParsedRequest, - route: { provider: OcxProviderConfig; modelId: string }, + route: { provider: OcxProviderConfig; modelId: string; providerName?: string }, config?: OcxConfig, + selector = effortSnapshots.get(parsed)?.selector ?? parsed.modelId, ): { from: string | undefined; to: string } | null { - const pinned = resolvePinnedEffort(route, parsed.modelId, config); + if (parsed._compactionRequest === true) return null; + const pinned = resolvePinnedEffort(route, selector, config); if (!pinned) return null; const requested = parsed.options.reasoning; const raw = parsed._rawBody as { reasoning?: { effort?: string } } | undefined; diff --git a/src/server/management/agent-settings-routes.ts b/src/server/management/agent-settings-routes.ts index f26caa164f..561d00a080 100644 --- a/src/server/management/agent-settings-routes.ts +++ b/src/server/management/agent-settings-routes.ts @@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto"; import { readFileSync } from "node:fs"; import type { CatalogModel } from "../../codex/catalog"; import { catalogModelSlug, filterCatalogVisibleModels, invalidateCodexModelsCache, nativeContextLimits, nativeModelRows, uniqueCatalogModelsForPublicList } from "../../codex/catalog"; +import { mergeModelPinnedEfforts, modelPinnedEffortsConfigError } from "../../config/provider-validation"; import { captureConfigTopLevelRollback, parsedConfigRebaseDeletionKeys, projectConfigRebaseProvenance } from "../../config/rebase-provenance"; import { DEFAULT_SUBAGENT_MODELS, @@ -16,6 +17,7 @@ import { providerHeadersConfigError, saveConfigPreservingClaudeCode, subagentDefaultSyncEffective, + validateConfigCandidate, } from "../../config"; import { clearLoginState, @@ -608,46 +610,52 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise }); } if (url.pathname === "/api/effort-caps" && req.method === "PUT") { - let body: { effortCap?: unknown; subagentEffortCap?: unknown; modelPinnedEfforts?: unknown }; + let body: unknown; try { body = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); } - const { isCodexReasoningEffort, isDeclaredReasoningEffort } = await import("../../reasoning-effort"); + if (!body || typeof body !== "object" || Array.isArray(body)) { + return jsonResponse({ error: "effort caps body must be a plain object" }, 400); + } + const patch = body as Record; + const { isCodexReasoningEffort } = await import("../../reasoning-effort"); + const draft = { ...projectConfigRebaseProvenance(config) }; + const touched: (keyof OcxConfig)[] = []; for (const key of ["effortCap", "subagentEffortCap"] as const) { - if (!(key in body)) continue; - const value = body[key]; - if (value === null || value === "") { deleteConfigTopLevelKey(config, key); continue; } - if (typeof value !== "string" || !isCodexReasoningEffort(value)) { - return jsonResponse({ error: `unknown reasoning effort "${String(value)}"` }, 400); - } - config[key] = value; - } - if ("modelPinnedEfforts" in body) { - const val = body.modelPinnedEfforts; - if (val === null || val === undefined) { - deleteConfigTopLevelKey(config, "modelPinnedEfforts"); - } else if (typeof val === "object" && !Array.isArray(val)) { - const efforts: Record = { ...(config.modelPinnedEfforts ?? {}) }; - for (const [m, eff] of Object.entries(val as Record)) { - if (!m.trim()) return jsonResponse({ error: "modelPinnedEfforts keys must be nonblank model ids" }, 400); - if (eff === null || eff === "" || eff === undefined) { - delete efforts[m.trim()]; - continue; - } - if (typeof eff === "string" && isDeclaredReasoningEffort(eff)) { - efforts[m.trim()] = eff; - } else { - return jsonResponse({ error: `unknown reasoning effort "${String(eff)}" for model "${m}"` }, 400); - } - } - if (Object.keys(efforts).length > 0) { - config.modelPinnedEfforts = efforts; - } else { - deleteConfigTopLevelKey(config, "modelPinnedEfforts"); - } - } else { - return jsonResponse({ error: "modelPinnedEfforts must be a plain object or null" }, 400); + if (!Object.hasOwn(patch, key)) continue; + const value = patch[key]; + if (value === null || value === "") deleteConfigTopLevelKey(draft, key); + else if (typeof value === "string" && isCodexReasoningEffort(value)) draft[key] = value; + else return jsonResponse({ error: "caps must be valid reasoning efforts or null" }, 400); + touched.push(key); + } + if (Object.hasOwn(patch, "modelPinnedEfforts")) { + const error = modelPinnedEffortsConfigError(patch.modelPinnedEfforts, "modelPinnedEfforts", true); + if (error) return jsonResponse({ error }, 400); + const pins = mergeModelPinnedEfforts(config.modelPinnedEfforts, patch.modelPinnedEfforts); + if (pins) draft.modelPinnedEfforts = pins; + else deleteConfigTopLevelKey(draft, "modelPinnedEfforts"); + touched.push("modelPinnedEfforts"); + } + const validation = validateConfigCandidate(draft); + if (!validation.ok) return jsonResponse({ error: validation.error }, 400); + if (touched.some(key => !Object.hasOwn(draft, key)) && config.configRebaseProvenance !== undefined + && parsedConfigRebaseDeletionKeys(config) === null) { + return jsonResponse({ error: "unsupported config deletion provenance" }, 409); + } + const projected = projectConfigRebaseProvenance(draft); + touched.push("configRebaseProvenance"); + const rollback = captureConfigTopLevelRollback(config, touched); + try { + for (const key of touched) { + if (Object.hasOwn(projected, key)) Object.defineProperty(config, key, { + value: projected[key], writable: true, enumerable: true, configurable: true, + }); + else deleteConfigTopLevelKey(config, key); } + (deps.saveConfigPreservingClaudeCode ?? saveConfigPreservingClaudeCode)(config); + } catch (error) { + rollback(); + throw error; } - saveConfigPreservingClaudeCode(config); return jsonResponse({ ok: true, effortCap: config.effortCap ?? null, diff --git a/src/server/management/provider-routes.ts b/src/server/management/provider-routes.ts index 1fc319987e..92b3c21381 100644 --- a/src/server/management/provider-routes.ts +++ b/src/server/management/provider-routes.ts @@ -33,7 +33,8 @@ import { submitManualLoginCode, upsertOAuthProvider, } from "../../oauth"; -import { isDeclaredReasoningEffort } from "../../reasoning-effort"; +import { captureConfigTopLevelRollback } from "../../config/rebase-provenance"; +import { mergeModelPinnedEfforts, modelPinnedEffortsConfigError, pinnedReasoningEffortConfigError } from "../../config/provider-validation"; import { replaceProviderAccountSet } from "../../oauth/store"; import { providerDestinationResolvedError } from "../../lib/destination-policy"; import { reconcileLiveStateStores } from "../../lib/state-store-registrations"; @@ -275,6 +276,11 @@ function providerEditorCandidate( if (!validated.ok) { return { ok: false, status: 400, error: validated.error, code: "invalid_provider_editor_config" }; } + for (const [name, provider] of Object.entries(candidate.providers)) { + if (provider.modelPinnedReasoningEfforts !== undefined) { + provider.modelPinnedReasoningEfforts = validated.config.providers[name]!.modelPinnedReasoningEfforts; + } + } return { ok: true, config: candidate, removedProviders }; } @@ -298,6 +304,27 @@ function adoptProviderEditorCandidate(live: OcxConfig, persisted: OcxConfig): vo else live.modelDiscovery = structuredClone(persisted.modelDiscovery); } +/** Share pin merge/clear semantics between POST and the PATCH mask. */ +function applyProviderPinFields( + next: OcxProviderConfig, + patch: Record, + current: OcxProviderConfig | undefined, +): string | null { + const scalarError = pinnedReasoningEffortConfigError(patch.pinnedReasoningEffort, true); + const mapError = modelPinnedEffortsConfigError(patch.modelPinnedReasoningEfforts, "modelPinnedReasoningEfforts", true); + if (scalarError || mapError) return scalarError ?? mapError; + const scalar = Object.hasOwn(patch, "pinnedReasoningEffort") + ? patch.pinnedReasoningEffort : current?.pinnedReasoningEffort; + const map = Object.hasOwn(patch, "modelPinnedReasoningEfforts") + ? mergeModelPinnedEfforts(current?.modelPinnedReasoningEfforts, patch.modelPinnedReasoningEfforts) + : current?.modelPinnedReasoningEfforts; + if (scalar === undefined || scalar === null || scalar === "") delete next.pinnedReasoningEffort; + else next.pinnedReasoningEffort = scalar as string; + if (map === undefined) delete next.modelPinnedReasoningEfforts; + else next.modelPinnedReasoningEfforts = { ...map }; + return null; +} + /** * Apply the recognized PATCH field mask onto a provider copy. The caller runs this once * for validation and again inside the config mutation lock against the newest provider, @@ -479,39 +506,9 @@ function applyProviderPatchFields( } touched = true; } - if (Object.hasOwn(rawBody, "pinnedReasoningEffort")) { - const value = rawBody.pinnedReasoningEffort; - if (value === null || value === "") { - delete next.pinnedReasoningEffort; - } else if (typeof value === "string" && isDeclaredReasoningEffort(value)) { - next.pinnedReasoningEffort = value; - } else { - return { error: "pinnedReasoningEffort must be a valid reasoning effort or null" }; - } - touched = true; - } - if (Object.hasOwn(rawBody, "modelPinnedReasoningEfforts")) { - const value = rawBody.modelPinnedReasoningEfforts; - if (value === null) { - delete next.modelPinnedReasoningEfforts; - } else { - if (!isPlainRecord(value)) return { error: "modelPinnedReasoningEfforts must be a plain object or null" }; - const efforts: Record = { ...(next.modelPinnedReasoningEfforts ?? {}) }; - for (const [model, effort] of Object.entries(value)) { - const modelId = model.trim(); - if (!modelId) return { error: "modelPinnedReasoningEfforts keys must be nonblank model ids" }; - if (effort === null || effort === "") { - delete efforts[modelId]; - continue; - } - if (typeof effort !== "string" || !isDeclaredReasoningEffort(effort)) { - return { error: `invalid reasoning effort "${String(effort)}" for model "${modelId}"` }; - } - efforts[modelId] = effort; - } - if (Object.keys(efforts).length > 0) next.modelPinnedReasoningEfforts = efforts; - else delete next.modelPinnedReasoningEfforts; - } + if (Object.hasOwn(rawBody, "pinnedReasoningEffort") || Object.hasOwn(rawBody, "modelPinnedReasoningEfforts")) { + const error = applyProviderPinFields(next, rawBody, provider); + if (error) return { error }; touched = true; } if (Object.hasOwn(rawBody, "modelAutoCompactTokenLimits")) { @@ -933,6 +930,8 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise { const { parsed, route, config, req, logCtx, inboundWire, inboundTransport } = args; + const effortSelector = prepareEffortNormalization(parsed, route); // Only Anthropic message routes retain the Codex-facing selector. Other providers must keep // their existing response.model contract even when their public and wire model ids differ. @@ -2405,7 +2406,7 @@ async function applyFinalRouteRequestNormalization(args: { { const { applyPinnedEffort } = await import("../effort-policy"); - const pinned = applyPinnedEffort(parsed, route, config); + const pinned = applyPinnedEffort(parsed, route, config, effortSelector); if (pinned) { logCtx.requestedEffort = pinned.from ? `${pinned.from}->${pinned.to}` : pinned.to; if (isInjectionDebugEnabled()) { diff --git a/src/types/config.ts b/src/types/config.ts index b3dd8f3de0..017d01a94a 100644 --- a/src/types/config.ts +++ b/src/types/config.ts @@ -544,11 +544,7 @@ export interface OcxConfig { * set, the lower one wins for sub-agents. See src/server/effort-policy.ts. */ subagentEffortCap?: string; - /** - * Optional map of model IDs (or model family / bare slugs) to an enforced reasoning effort tier - * ("none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max"). - * Overrides incoming caller choices globally across all providers. - */ + /** Global model effort overrides, after provider model/wide pins; none means omission. */ modelPinnedEfforts?: Record; /** * Models hidden from Codex discovery without blocking direct proxy calls. Routed provider ids diff --git a/src/types/provider.ts b/src/types/provider.ts index 860133a443..b51230d6d1 100644 --- a/src/types/provider.ts +++ b/src/types/provider.ts @@ -493,16 +493,9 @@ export interface OcxProviderConfig { modelReasoningEfforts?: Record; /** Model-specific default Codex reasoning tier; must also be present in the visible tier list. */ modelDefaultReasoningEfforts?: Record; - /** - * Provider-wide pinned reasoning effort tier ("none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max"). - * When configured, models under this provider default to being forcefully pinned to this tier. - */ + /** Operator-owned effort override; none omits effort and uses the provider default. */ pinnedReasoningEffort?: string; - /** - * Model-specific pinned reasoning effort overrides ("none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max"). - * When set, any incoming request targeting that model has its reasoning effort forcefully pinned - * to this tier, overriding caller choices. - */ + /** Per-model operator override, ahead of provider-wide and global pins; caps still apply. */ modelPinnedReasoningEfforts?: Record; /** * Model-specific Codex reasoning-summary capability. Set false when an OpenAI-compatible diff --git a/structure/03_catalog-and-subagents.md b/structure/03_catalog-and-subagents.md index 86aa4a81a7..b64cb4bce1 100644 --- a/structure/03_catalog-and-subagents.md +++ b/structure/03_catalog-and-subagents.md @@ -346,6 +346,15 @@ wire-clamps ultra/max to each model's real top rung (e.g. gpt-5.5 ultra → xhig (`src/server/effort-policy.ts`): they lower or preserve the requested effort rather than rejecting the request, and they never raise it. +Operator-owned `pinnedReasoningEffort`, `modelPinnedReasoningEfforts`, and root +`modelPinnedEfforts` resolve before applicable effort caps at the final destination. +Provider model pins precede provider-wide pins, then global selector/destination pins. +A pin can raise the effective caller effort; the later cap can still lower or omit it. +`none` means explicit-effort omission (provider default), not guaranteed reasoning disablement. +Compaction maintenance is exempt. Pins are user overlays and do not alter registry seeds, +model discovery or advertised ladders. Native Chat normalizes newly pinned values through +provider wire mapping; unpinned native requests retain their existing pass-through contract. + [Decision Log] - 목적과 의도: Xiaomi MiMo의 공식 OpenAI Chat endpoint가 실제로 받지 않는 `max`/ `ultra` reasoning tier를 catalog에 노출하지 않도록 한다. diff --git a/tests/codex-integration/model-pinned-effort.test.ts b/tests/codex-integration/model-pinned-effort.test.ts new file mode 100644 index 0000000000..7c7b15401c --- /dev/null +++ b/tests/codex-integration/model-pinned-effort.test.ts @@ -0,0 +1,568 @@ +import { afterEach, beforeEach, describe, expect, test } from "bun:test"; +import { mkdtempSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { resolvePinnedEffort, applyPinnedEffort, prepareEffortNormalization, chatCollabSurface, applyChatEffortCap } from "../../src/server/effort-policy"; +import { handleManagementAPI } from "../../src/server/management-api"; +import { handleResponses } from "../../src/server/responses/core"; +import { handleChatCompletions } from "../../src/server/chat-completions"; +import { handleNativeChatCompletions } from "../../src/server/chat-native"; +import { createOpenAIChatAdapter } from "../../src/adapters/openai-chat"; +import { parseRequest } from "../../src/responses/parser"; +import { routeModel } from "../../src/router"; +import { createTestTranslatorBudget, withTestTranslatorBudget } from "../helpers/translator-budget"; +import { installIsolatedCodexHome, type IsolatedCodexHome } from "../helpers/isolated-codex-home"; +import { removeTreeWithRetry } from "../helpers/remove-tree"; +import type { OcxConfig, OcxParsedRequest, OcxProviderConfig } from "../../src/types"; + +describe("model pinned reasoning effort policy", () => { + const providerWithPinned: OcxProviderConfig = { + adapter: "openai-responses", + baseUrl: "https://api.openai.com/v1", + pinnedReasoningEffort: "high", + modelPinnedReasoningEfforts: { + "special-model": "max", + "disabled-effort-model": "none", + }, + }; + + test("resolves model-specific pinned effort over provider-wide pinned effort", () => { + const route = { provider: providerWithPinned, modelId: "special-model" }; + expect(resolvePinnedEffort(route)).toBe("max"); + }); + + test("resolves provider-wide pinned effort when model is not specifically pinned", () => { + const route = { provider: providerWithPinned, modelId: "other-model" }; + expect(resolvePinnedEffort(route)).toBe("high"); + }); + + test("resolves global config modelPinnedEfforts fallback when provider has none", () => { + const emptyProvider: OcxProviderConfig = { + adapter: "openai-responses", + baseUrl: "https://api.openai.com/v1", + }; + const config = { + modelPinnedEfforts: { "global-pinned": "max" }, + } as unknown as OcxConfig; + const route = { provider: emptyProvider, modelId: "global-pinned" }; + expect(resolvePinnedEffort(route, undefined, config)).toBe("max"); + }); + + test("applyPinnedEffort overrides caller effort in both parsed options and raw body", () => { + const route = { provider: providerWithPinned, modelId: "special-model" }; + const parsed: OcxParsedRequest = { + modelId: "special-model", + context: { messages: [] }, + stream: true, + options: { reasoning: "low" }, + _rawBody: { reasoning: { effort: "low" } }, + }; + + const rewrite = applyPinnedEffort(parsed, route); + expect(rewrite).toEqual({ from: "low", to: "max" }); + expect(parsed.options.reasoning).toBe("max"); + expect((parsed._rawBody as any).reasoning.effort).toBe("max"); + }); + + test("applyPinnedEffort applies pinned effort when caller sent none", () => { + const route = { provider: providerWithPinned, modelId: "other-model" }; + const parsed: OcxParsedRequest = { + modelId: "other-model", + context: { messages: [] }, + stream: true, + options: {}, + _rawBody: {}, + }; + + const rewrite = applyPinnedEffort(parsed, route); + expect(rewrite).toEqual({ from: undefined, to: "high" }); + expect(parsed.options.reasoning).toBe("high"); + expect((parsed._rawBody as any).reasoning.effort).toBe("high"); + }); + + test("applyPinnedEffort with none strips effort from both shapes", () => { + const route = { provider: providerWithPinned, modelId: "disabled-effort-model" }; + const parsed: OcxParsedRequest = { + modelId: "disabled-effort-model", + context: { messages: [] }, + stream: true, + options: { reasoning: "high" }, + _rawBody: { reasoning: { effort: "high", summary: "auto" } }, + }; + + const rewrite = applyPinnedEffort(parsed, route); + expect(rewrite).toEqual({ from: "high", to: "none" }); + expect(parsed.options.reasoning).toBeUndefined(); + expect((parsed._rawBody as any).reasoning.effort).toBeUndefined(); + expect((parsed._rawBody as any).reasoning.summary).toBe("auto"); + }); +}); + +describe("management API pinned reasoning effort configuration", () => { + let tempHome: string | undefined; + const savedHome = process.env.OPENCODEX_HOME; + afterEach(() => { + if (savedHome === undefined) delete process.env.OPENCODEX_HOME; + else process.env.OPENCODEX_HOME = savedHome; + if (tempHome) removeTreeWithRetry(tempHome); + tempHome = undefined; + }); + function isolatedHome(): void { + tempHome = mkdtempSync(join(tmpdir(), "ocx-pinned-effort-")); + process.env.OPENCODEX_HOME = tempHome; + } + + function makeConfig(overrides: Partial = {}): OcxConfig { + return { + version: 1, + defaultProvider: "custom", + providers: { + custom: { + adapter: "openai-responses", + baseUrl: "https://api.custom.com", + allowPrivateNetwork: true, + }, + }, + ...overrides, + } as unknown as OcxConfig; + } + + test("PATCH /api/providers sets and updates pinned reasoning efforts", async () => { + isolatedHome(); + const config = makeConfig(); + const patchReq = new Request("http://localhost/api/providers?name=custom", { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + pinnedReasoningEffort: "high", + modelPinnedReasoningEfforts: { "model-a": "max", "model-b": "low" }, + }), + }); + const patchRes = await handleManagementAPI(patchReq, new URL(patchReq.url), config); + expect(patchRes?.status).toBe(200); + const provider = config.providers.custom; + expect(provider.pinnedReasoningEffort).toBe("high"); + expect(provider.modelPinnedReasoningEfforts).toEqual({ "model-a": "max", "model-b": "low" }); + + // Updating with whitespace key normalizes to trimmed model id + const wsReq = new Request("http://localhost/api/providers?name=custom", { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + modelPinnedReasoningEfforts: { " model-c ": "medium" }, + }), + }); + const wsRes = await handleManagementAPI(wsReq, new URL(wsReq.url), config); + expect(wsRes?.status).toBe(200); + expect(config.providers.custom.modelPinnedReasoningEfforts).toEqual({ "model-a": "max", "model-b": "low", "model-c": "medium" }); + + // Clearing a model pinned effort with whitespace key + const wsClearReq = new Request("http://localhost/api/providers?name=custom", { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + modelPinnedReasoningEfforts: { " model-c ": null }, + }), + }); + const wsClearRes = await handleManagementAPI(wsClearReq, new URL(wsClearReq.url), config); + expect(wsClearRes?.status).toBe(200); + expect(config.providers.custom.modelPinnedReasoningEfforts).toEqual({ "model-a": "max", "model-b": "low" }); + + // Clearing a model pinned effort + const clearReq = new Request("http://localhost/api/providers?name=custom", { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + modelPinnedReasoningEfforts: { "model-a": null }, + }), + }); + const clearRes = await handleManagementAPI(clearReq, new URL(clearReq.url), config); + expect(clearRes?.status).toBe(200); + expect(config.providers.custom.modelPinnedReasoningEfforts).toEqual({ "model-b": "low" }); + }); + + test("PATCH /api/providers rejects invalid reasoning effort values", async () => { + isolatedHome(); + const config = makeConfig(); + const badReq = new Request("http://localhost/api/providers?name=custom", { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + pinnedReasoningEffort: "invalid-tier", + }), + }); + const badRes = await handleManagementAPI(badReq, new URL(badReq.url), config); + expect(badRes?.status).toBe(400); + }); + + test("PUT /api/effort-caps supports modelPinnedEfforts roundtrip", async () => { + isolatedHome(); + const config = makeConfig(); + const putReq = new Request("http://localhost/api/effort-caps", { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + modelPinnedEfforts: { "gpt-5.5": "max", "claude-sonnet-4-6": "high" }, + }), + }); + const putRes = await handleManagementAPI(putReq, new URL(putReq.url), config); + expect(putRes?.status).toBe(200); + expect(config.modelPinnedEfforts).toEqual({ "gpt-5.5": "max", "claude-sonnet-4-6": "high" }); + + const getReq = new Request("http://localhost/api/effort-caps"); + const getRes = await handleManagementAPI(getReq, new URL(getReq.url), config); + const data = await getRes?.json() as { modelPinnedEfforts: Record }; + expect(data.modelPinnedEfforts).toEqual({ "gpt-5.5": "max", "claude-sonnet-4-6": "high" }); + + // Partial merge: add one model, clear another + const updateReq = new Request("http://localhost/api/effort-caps", { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + modelPinnedEfforts: { "gemini-3.7-flash": "high", "gpt-5.5": null }, + }), + }); + const updateRes = await handleManagementAPI(updateReq, new URL(updateReq.url), config); + expect(updateRes?.status).toBe(200); + expect(config.modelPinnedEfforts).toEqual({ "claude-sonnet-4-6": "high", "gemini-3.7-flash": "high" }); + }); +}); +import { ManagementRequest as Request } from "../helpers/management-auth"; + +describe("native chat completions effort policy", () => { + + test("detects v2 collab surface in native chat tools", () => { + const chatBody = { + tools: [ + { type: "function", function: { name: "spawn_agent" } }, + { type: "function", function: { name: "send_message" } }, + ], + }; + expect(chatCollabSurface(chatBody)).toBe("v2"); + }); + + test("applyChatEffortCap respects effortCap ceiling over pinned effort", () => { + const config = { + effortCap: "low", + }; + const chatBody = { + reasoning_effort: "max", + }; + const rewrite = applyChatEffortCap(chatBody, new Headers(), config, ["low", "medium", "high", "max"]); + expect(rewrite).toEqual({ from: "max", to: "low", subagent: false }); + expect(chatBody.reasoning_effort).toBe("low"); + }); +}); + +// Exercise the real ingress/adapter serializers. Only the upstream fetch is replaced; +// unexpected destinations fail closed instead of reaching a live provider. +describe("operator pins on the actual request wire", () => { + const originalFetch = globalThis.fetch; + let savedHome: string | undefined; + let home: string; + let codexHome: IsolatedCodexHome; + let captured: Array<{ url: string; body: Record }>; + let failFirst: boolean; + let failureStatus: number; + let onFirstSend: (() => void) | undefined; + + beforeEach(() => { + savedHome = process.env.OPENCODEX_HOME; + home = mkdtempSync(join(tmpdir(), "ocx-pin-wire-")); + process.env.OPENCODEX_HOME = home; + codexHome = installIsolatedCodexHome("ocx-pin-wire-codex-"); + captured = []; + failFirst = false; + failureStatus = 503; + onFirstSend = undefined; + globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input instanceof globalThis.Request ? input.url : String(input); + if (!url.startsWith("http://127.0.0.1:65534/")) throw new Error("unexpected pin-test destination"); + const body = JSON.parse(String(init?.body)) as Record; + captured.push({ url, body }); + if (captured.length === 1) onFirstSend?.(); + if (failFirst && captured.length === 1) { + return Response.json({ error: { message: "fixture unavailable", type: "server_error" } }, + { status: failureStatus, headers: { "retry-after": "0" } }); + } + if (url.endsWith("/chat/completions")) { + if (body.stream === true) { + return new Response([ + 'data: {"choices":[{"index":0,"delta":{"role":"assistant","content":"ok"}}]}\n\n', + 'data: {"choices":[{"index":0,"delta":{},"finish_reason":"stop"}],"usage":{"prompt_tokens":1,"completion_tokens":1}}\n\n', + 'data: [DONE]\n\n', + ].join(""), { headers: { "content-type": "text/event-stream" } }); + } + return Response.json({ + id: "chatcmpl_pin", object: "chat.completion", model: body.model, + choices: [{ index: 0, message: { role: "assistant", content: "ok" }, finish_reason: "stop" }], + usage: { prompt_tokens: 1, completion_tokens: 1 }, + }); + } + return Response.json({ + id: "resp_pin", object: "response", model: body.model, status: "completed", + output: [{ type: "message", id: "msg_pin", role: "assistant", status: "completed", + content: [{ type: "output_text", text: "ok", annotations: [] }] }], + usage: { input_tokens: 1, output_tokens: 1, total_tokens: 2 }, + }); + }) as typeof fetch; + }); + + afterEach(() => { + globalThis.fetch = originalFetch; + if (savedHome === undefined) delete process.env.OPENCODEX_HOME; + else process.env.OPENCODEX_HOME = savedHome; + codexHome.restore(); + removeTreeWithRetry(home); + }); + + function provider(overrides: Partial = {}): OcxProviderConfig { + return { + adapter: "openai-chat", authMode: "key", apiKey: "fixture-pin-key", + baseUrl: "http://127.0.0.1:65534/v1", allowPrivateNetwork: true, + liveModels: false, models: ["pin-model"], + reasoningEfforts: ["low", "medium", "high", "xhigh", "max", "ultra"], + ...overrides, + }; + } + + function config(p: Partial = {}, overrides: Partial = {}): OcxConfig { + return { port: 0, defaultProvider: "fixture", providers: { fixture: provider(p) }, + multiAgentGuidanceEnabled: false, ...overrides }; + } + + async function request(c: OcxConfig, inbound: "chat" | "responses", extra: Record = {}, headers: HeadersInit = {}) { + const body = inbound === "chat" + ? { model: "fixture/pin-model", messages: [{ role: "user", content: "hello" }], stream: false, reasoning_effort: "low", ...extra } + : { model: "fixture/pin-model", input: "hello", stream: false, reasoning: { effort: "low", summary: "auto" }, ...extra }; + const req = new Request(`http://localhost/v1/${inbound === "chat" ? "chat/completions" : "responses"}`, { + method: "POST", headers: { "content-type": "application/json", ...Object.fromEntries(new Headers(headers)) }, + body: JSON.stringify(body), + }); + const response = inbound === "chat" + ? await handleChatCompletions(req, c, { model: "", provider: "" }) + : await handleResponses(req, c, { model: "", provider: "" }, { abortSignal: AbortSignal.timeout(5_000) }); + const text = await response.text(); + expect(response.status, text).toBe(200); + expect(captured.length).toBeGreaterThan(0); + return captured.at(-1)!.body; + } + + for (const inbound of ["chat", "responses"] as const) { + test(`${inbound}: ultra pin maps to max on the Chat wire`, async () => { + const wire = await request(config({ pinnedReasoningEffort: "ultra" }), inbound); + expect(wire.reasoning_effort).toBe("max"); + }); + + test(`${inbound}: none omits effort instead of sending none`, async () => { + const wire = await request(config({ pinnedReasoningEffort: "none" }), inbound); + expect(Object.hasOwn(wire, "reasoning_effort")).toBe(false); + }); + + test(`${inbound}: minimal pin uses the existing low wire mapping`, async () => { + expect((await request(config({ pinnedReasoningEffort: "minimal" }), inbound)).reasoning_effort).toBe("low"); + }); + + test(`${inbound}: provider-model > provider-wide > global`, async () => { + const c = config({ pinnedReasoningEffort: "high", modelPinnedReasoningEfforts: { "pin-model": "xhigh" } }, + { modelPinnedEfforts: { "fixture/pin-model": "medium" } }); + expect((await request(c, inbound)).reasoning_effort).toBe("xhigh"); + delete c.providers.fixture!.modelPinnedReasoningEfforts; + expect((await request(c, inbound)).reasoning_effort).toBe("high"); + delete c.providers.fixture!.pinnedReasoningEffort; + expect((await request(c, inbound)).reasoning_effort).toBe("medium"); + }); + + test(`${inbound}: exact selector > qualified destination > bare destination`, async () => { + const c = config({ modelAliases: { "pin-model": "friendly" } }, { + modelPinnedEfforts: { "fixture/friendly": "xhigh", "fixture/pin-model": "high", "pin-model": "medium" }, + }); + expect((await request(c, inbound, { model: "fixture/friendly" })).reasoning_effort).toBe("xhigh"); + delete c.modelPinnedEfforts!["fixture/friendly"]; + expect((await request(c, inbound, { model: "fixture/friendly" })).reasoning_effort).toBe("high"); + delete c.modelPinnedEfforts!["fixture/pin-model"]; + expect((await request(c, inbound, { model: "fixture/friendly" })).reasoning_effort).toBe("medium"); + }); + + test(`${inbound}: qualified global lookup retains case-fold semantics`, async () => { + const c = config({}, { modelPinnedEfforts: { "FIXTURE/PIN-MODEL": "high", "pin-model": "medium" } }); + expect((await request(c, inbound)).reasoning_effort).toBe("high"); + }); + + test(`${inbound}: provider model selector fallback precedes provider-wide pin`, async () => { + const c = config({ modelAliases: { "pin-model": "friendly" }, pinnedReasoningEffort: "high", + modelPinnedReasoningEfforts: { "fixture/friendly": "medium" } }); + expect((await request(c, inbound, { model: "fixture/friendly" })).reasoning_effort).toBe("medium"); + }); + + test(`${inbound}: applicable child cap follows pin, before wire alias`, async () => { + const c = config({ pinnedReasoningEffort: "ultra", reasoningEffortMap: { medium: "enabled" } }, + { effortCap: "high", subagentEffortCap: "medium" }); + const wire = await request(c, inbound, {}, { "x-openai-subagent": "collab_spawn" }); + expect(wire.reasoning_effort).toBe("enabled"); + }); + + test(`${inbound}: v2 main cap follows pin; v1 main leaves it alone`, async () => { + const c = config({ pinnedReasoningEffort: "max" }, { effortCap: "medium" }); + const tools = inbound === "chat" + ? [{ type: "function", function: { name: "spawn_agent", parameters: { type: "object", properties: {} } } }] + : [{ type: "function", name: "spawn_agent", parameters: { type: "object", properties: {} } }]; + expect((await request(c, inbound, { tools })).reasoning_effort).toBe("medium"); + c.multiAgentMode = "v1"; + expect((await request(c, inbound, { tools })).reasoning_effort).toBe("max"); + }); + + test(`${inbound}: cap below all supported rungs omits pinned effort`, async () => { + const c = config({ pinnedReasoningEffort: "max", reasoningEfforts: ["high", "max"] }, { subagentEffortCap: "low" }); + expect(Object.hasOwn(await request(c, inbound, {}, { "x-openai-subagent": "collab_spawn" }), "reasoning_effort")).toBe(false); + }); + } + + test("Responses passthrough none preserves reasoning.summary", async () => { + const wire = await request(config({ adapter: "openai-responses", pinnedReasoningEffort: "none" }), "responses"); + expect(wire.reasoning).toEqual({ summary: "auto" }); + }); + + test("Responses passthrough maps a pinned ultra through its declared ladder", async () => { + const wire = await request(config({ adapter: "openai-responses", pinnedReasoningEffort: "ultra" }), "responses"); + expect(wire.reasoning).toEqual({ effort: "max", summary: "auto" }); + }); + + test("native Chat without pins preserves caller wire spelling and existing cap behavior", async () => { + const c = config({ reasoningEfforts: ["low"], reasoningEffortMap: { max: "enabled" } }, { effortCap: "low", subagentEffortCap: "low" }); + expect((await request(c, "chat", { reasoning_effort: "ultra" }, { "x-openai-subagent": "collab_spawn" })).reasoning_effort).toBe("ultra"); + expect(Object.hasOwn(await request(c, "chat", { reasoning_effort: undefined }), "reasoning_effort")).toBe(false); + }); + + test("unpinned Responses keeps its existing applicable cap", async () => { + expect((await request(config({}, { subagentEffortCap: "medium" }), "responses", + { reasoning: { effort: "max", summary: "auto" } }, { "x-openai-subagent": "collab_spawn" })).reasoning_effort).toBe("medium"); + }); + + test("routed compaction skips pins and caps", async () => { + const wire = await request(config({ pinnedReasoningEffort: "max" }, { subagentEffortCap: "low" }), "responses", { + input: [{ role: "user", content: "summarize this" }, { type: "compaction_trigger" }], + reasoning: { effort: "medium", summary: "auto" }, + }, { "x-openai-subagent": "collab_spawn" }); + expect(wire.reasoning_effort).toBe("medium"); + }); + + test("synthetic rows retain effective effort and exclude synthetic global pin keys", async () => { + const c = config({}, { cursorEffortRows: true, modelPinnedEfforts: { "fixture/pin-model--high": "max" } }); + expect((await request(c, "responses", { model: "fixture/pin-model--high" })).reasoning_effort).toBe("high"); + c.modelPinnedEfforts!["fixture/pin-model"] = "medium"; + expect((await request(c, "responses", { model: "fixture/pin-model--high" })).reasoning_effort).toBe("medium"); + }); + + test("combo failover recomputes each destination's default without leaking the first pin", async () => { + failFirst = true; + const c = config({}, { + providers: { + first: provider({ pinnedReasoningEffort: "max", reasoningEfforts: ["low", "high", "max"] }), + second: provider({ reasoningEfforts: ["low", "medium"] }), + }, + defaultProvider: "first", + modelPinnedEfforts: { "combo/pin-default": "low" }, + combos: { "pin-default": { strategy: "failover", defaultEffort: "high", targets: [ + { provider: "first", model: "pin-model" }, { provider: "second", model: "pin-model" }, + ] } }, + }); + const wire = await request(c, "responses", { model: "combo/pin-default", reasoning: { summary: "auto" } }); + expect(captured.map(({ body }) => body.reasoning_effort)).toEqual(["max", "medium"]); + expect(wire.reasoning_effort).toBe("medium"); + }); + + test("native repeated destinations restore only original effort and keep credential-retry decisions", async () => { + const c = config({}, { providers: { + first: provider({ pinnedReasoningEffort: "high" }), + second: provider(), + omit: provider({ pinnedReasoningEffort: "none" }), + last: provider(), + }, modelPinnedEfforts: { "first/pin-model": "xhigh", "last/pin-model": "medium" } }); + const body: Record = { model: "first/pin-model", messages: [{ role: "user", content: "hello" }], reasoning_effort: "low", reasoning: { summary: "auto" } }; + const req = new Request("http://localhost/v1/chat/completions", { method: "POST" }); + async function send(name: string) { + const response = await handleNativeChatCompletions({ req, config: c, logCtx: { model: "", provider: "" }, + route: routeModel(c, `${name}/pin-model`), chatBody: body, requestedModel: `${name}/pin-model`, + requestedStream: false, translatorBudget: createTestTranslatorBudget() }); + expect(response.status, await response.text()).toBe(200); + } + await send("first"); + c.providers.first!.pinnedReasoningEffort = "max"; + await send("first"); + body.reasoning = { summary: "detailed" }; + body.temperature = 0.2; + await send("second"); + await send("omit"); + await send("last"); + expect(captured.map(({ body }) => body.reasoning_effort)).toEqual(["high", "high", "low", undefined, "medium"]); + expect(body.reasoning).toEqual({ summary: "detailed" }); + expect(body.temperature).toBe(0.2); + }); + + test("native same-target retry keeps the already normalized pin decision", async () => { + failFirst = true; + failureStatus = 429; + const c = config({ pinnedReasoningEffort: "ultra", + retryOn429: { attempts: 1, intervalMs: 100, maxIntervalMs: 100, respectRetryAfter: false } }); + onFirstSend = () => { c.providers.fixture!.pinnedReasoningEffort = "low"; }; + await request(c, "chat"); + expect(captured.map(({ body }) => body.reasoning_effort)).toEqual(["max", "max"]); + }); +}); + +// The normalization entry is request-owned and shared with the real Responses path. +// Use the parser and adapter serializer to observe repeated destination normalization. +describe("repeated Responses effort normalization", () => { + test("restores pre-pin effective effort and raw presence while preserving unrelated edits", () => { + for (const reasoning of [{ effort: "medium", summary: "auto" }, { summary: "auto" }]) { + const parsed = parseRequest({ model: "first/pin-model", input: "hello", stream: false, reasoning }); + const first = { providerName: "first", modelId: "pin-model", provider: { adapter: "openai-chat" as const, + baseUrl: "http://127.0.0.1:65534/v1", pinnedReasoningEffort: "high" } }; + const second = { providerName: "second", modelId: "pin-model", provider: { ...first.provider, pinnedReasoningEffort: undefined } }; + prepareEffortNormalization(parsed, first); + parsed.modelId = first.modelId; + applyPinnedEffort(parsed, first); + const raw = parsed._rawBody as { reasoning: Record }; + raw.reasoning.summary = "detailed"; + parsed.options.temperature = 0.2; + prepareEffortNormalization(parsed, second); + applyPinnedEffort(parsed, second); + const wire = JSON.parse(withTestTranslatorBudget(createOpenAIChatAdapter(second.provider)).buildRequest(parsed).body); + expect(wire.reasoning_effort).toBe("effort" in reasoning ? "medium" : undefined); + expect(Object.hasOwn(raw.reasoning, "effort")).toBe("effort" in reasoning); + expect(raw.reasoning.summary).toBe("detailed"); + expect(parsed.options.temperature).toBe(0.2); + const omit = { ...second, providerName: "omit", provider: { ...second.provider, pinnedReasoningEffort: "none" } }; + prepareEffortNormalization(parsed, omit); + applyPinnedEffort(parsed, omit); + expect(raw.reasoning).toEqual({ summary: "detailed" }); + prepareEffortNormalization(parsed, second); + applyPinnedEffort(parsed, second); + expect(parsed.options.reasoning).toBe("effort" in reasoning ? "medium" : undefined); + } + }); + + test("pre-namespace selectors are destination-scoped and restore parser-normalized effort independently of raw effort", () => { + const parsed = parseRequest({ model: "first/pin-model", input: "hello", reasoning: { effort: "ultra", summary: "auto" } }); + const provider: OcxProviderConfig = { adapter: "openai-chat", baseUrl: "http://127.0.0.1:65534/v1" }; + const first = { providerName: "first", modelId: "pin-model", provider }; + const second = { ...first, providerName: "second" }; + const config = { port: 0, providers: { first: provider, second: provider }, + modelPinnedEfforts: { "first/pin-model": "high", "second/pin-model": "none" } }; + prepareEffortNormalization(parsed, first); + parsed.modelId = first.modelId; + applyPinnedEffort(parsed, first, config); + expect(parsed.options.reasoning).toBe("high"); + prepareEffortNormalization(parsed, second); + applyPinnedEffort(parsed, second, config); + expect(parsed.options.reasoning).toBeUndefined(); + const third = { ...first, providerName: "third" }; + prepareEffortNormalization(parsed, third); + applyPinnedEffort(parsed, third, config); + expect(parsed.options.reasoning).toBe("max"); + expect(parsed._rawBody).toMatchObject({ reasoning: { effort: "ultra", summary: "auto" } }); + const wire = JSON.parse(withTestTranslatorBudget(createOpenAIChatAdapter(provider)).buildRequest(parsed).body); + expect(wire.reasoning_effort).toBe("max"); + }); +}); diff --git a/tests/config/model-pinned-effort-config.test.ts b/tests/config/model-pinned-effort-config.test.ts new file mode 100644 index 0000000000..29c1d505fb --- /dev/null +++ b/tests/config/model-pinned-effort-config.test.ts @@ -0,0 +1,391 @@ +import { afterEach, beforeEach, describe, expect, mock, spyOn, test } from "bun:test"; +import { mkdtempSync, readFileSync, readdirSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { + deleteConfigTopLevelKey, getConfigPath, getDefaultConfig, loadConfig, readConfigDiagnostics, + saveConfig, saveConfigPreservingClaudeCode, validateConfigCandidate, +} from "../../src/config"; +import { modelPinnedEffortsConfigError, pinnedReasoningEffortConfigError } from "../../src/config/provider-validation"; +import { configRebaseDeletionKeys, projectConfigRebaseProvenance } from "../../src/config/rebase-provenance"; +import * as destinationPolicy from "../../src/lib/destination-policy"; +import { providerConfigSeed } from "../../src/providers/derive"; +import { getProviderRegistryEntry } from "../../src/providers/registry"; +import { providerEditorConfigDTO, providerManagementConfigError, safeConfigDTO } from "../../src/server/auth-cors"; +import { handleAgentSettingsRoutes } from "../../src/server/management/agent-settings-routes"; +import { handleProviderRoutes } from "../../src/server/management/provider-routes"; +import type { ManagementContext } from "../../src/server/management/context"; +import type { OcxConfig } from "../../src/types"; +import { installIsolatedCodexHome, type IsolatedCodexHome } from "../helpers/isolated-codex-home"; +import { ManagementRequest } from "../helpers/management-auth"; +import { removeTreeWithRetry } from "../helpers/remove-tree"; + +let directory: string; +let previousHome: string | undefined; +let codexHome: IsolatedCodexHome; + +function fixture(): OcxConfig { + return { + ...getDefaultConfig(), defaultProvider: "alpha", + providers: { alpha: { + adapter: "openai-chat", baseUrl: "https://alpha.example.test/v1", apiKey: "fixture-private-key", + pinnedReasoningEffort: "high", modelPinnedReasoningEfforts: { one: "low", two: "none" }, + } }, + effortCap: "max", subagentEffortCap: "medium", modelPinnedEfforts: { "alpha/one": "ultra", two: "minimal" }, + }; +} + +function context(config: OcxConfig, path: string, method: string, body?: unknown): ManagementContext { + const url = new URL(`http://localhost${path}`); + return { + url, config, version: "fixture", + req: new ManagementRequest(url, { method, ...(body === undefined ? {} : { body: JSON.stringify(body) }) }), + deps: { saveConfigPreservingClaudeCode, clearThreadAccountMap: () => {}, clearProviderQuotaCache: () => {} }, + convergeCodexCatalog: mock(async () => ({ status: "committed", changed: true, degraded: false, notices: [] } as const)), + syncClaudeAgentDefsBestEffort: mock(async () => {}), + }; +} + +beforeEach(() => { + previousHome = process.env.OPENCODEX_HOME; + directory = mkdtempSync(join(tmpdir(), "ocx-pinned-config-")); + process.env.OPENCODEX_HOME = directory; + codexHome = installIsolatedCodexHome("ocx-pinned-codex-"); + saveConfig(fixture()); +}); + +afterEach(() => { + codexHome.restore(); + if (previousHome === undefined) delete process.env.OPENCODEX_HOME; + else process.env.OPENCODEX_HOME = previousHome; + removeTreeWithRetry(directory); +}); + +describe("reasoning pin config boundaries", () => { + test("accepts declared efforts and rejects malformed maps, reserved keys and trim collisions", () => { + for (const effort of ["none", "minimal", "low", "medium", "high", "xhigh", "max", "ultra"]) { + expect(pinnedReasoningEffortConfigError(effort)).toBeNull(); + expect(modelPinnedEffortsConfigError({ model: effort })).toBeNull(); + } + for (const value of [null, [], "high", new Date(), Object.create({ inherited: "high" }), + { " ": "high" }, { constructor: "low" }, { prototype: "low" }, + JSON.parse('{"__proto__":"high"}'), { " model ": "low", model: "high" }, { model: undefined }, + { model: "invented" }, { model: null }, { model: "" }]) { + expect(modelPinnedEffortsConfigError(value)).not.toBeNull(); + } + expect(modelPinnedEffortsConfigError({ model: null, other: "" }, "pins", true)).toBeNull(); + expect(modelPinnedEffortsConfigError({ " model ": null, model: "high" }, "pins", true)).not.toBeNull(); + }); + + test("load and diagnostics salvage the same entries without fallback, secret warnings or disk rewrite", () => { + const raw = fixture(); + const provider = raw.providers.alpha! as unknown as Record; + provider.pinnedReasoningEffort = { secret: "do-not-log-pin-value" }; + provider.modelPinnedReasoningEfforts = { keep: "none", bad: "do-not-log-pin-value", " clash ": "low", clash: "high" }; + raw.modelPinnedEfforts = JSON.parse('{"keep":"minimal","__proto__":"high"," ":"high","bad":12}'); + writeFileSync(getConfigPath(), JSON.stringify(raw)); + const before = readFileSync(getConfigPath(), "utf8"); + const filesBefore = readdirSync(directory).sort(); + const warnings: string[] = []; + const warn = spyOn(console, "warn").mockImplementation((...args) => { warnings.push(args.join(" ")); }); + try { + const loaded = loadConfig(); + const diagnostics = readConfigDiagnostics(); + expect(diagnostics.source).toBe("file"); + expect(diagnostics.error).toBeNull(); + for (const config of [loaded, diagnostics.config]) { + expect(config.providers.alpha!.apiKey).toBe("fixture-private-key"); + expect(config.providers.alpha!.pinnedReasoningEffort).toBeUndefined(); + expect(config.providers.alpha!.modelPinnedReasoningEfforts).toEqual({ keep: "none" }); + expect(config.modelPinnedEfforts).toEqual({ keep: "minimal" }); + expect(config.defaultProvider).toBe("alpha"); + } + expect(warnings.length).toBeGreaterThan(0); + expect(warnings.join("\n")).not.toContain("do-not-log-pin-value"); + expect(warnings.join("\n")).not.toContain("clash"); + expect(readFileSync(getConfigPath(), "utf8")).toBe(before); + expect(readdirSync(directory).sort()).toEqual(filesBefore); + } finally { warn.mockRestore(); } + }); + + test("candidate and direct writers reject invalid pins before live or disk mutation", () => { + for (const mutation of [ + (config: OcxConfig) => { config.modelPinnedEfforts = { model: "invalid" }; }, + (config: OcxConfig) => { config.providers.alpha!.pinnedReasoningEffort = "invalid"; }, + (config: OcxConfig) => { config.providers.alpha!.modelPinnedReasoningEfforts = { " ": "high" }; }, + (config: OcxConfig) => { Reflect.set(config, "modelPinnedEfforts", null); }, + ]) { + const config = loadConfig(); + mutation(config); + const beforeLive = structuredClone(config); + const beforeDisk = readFileSync(getConfigPath(), "utf8"); + expect(validateConfigCandidate(config).ok).toBe(false); + expect(() => saveConfig(config)).toThrow(); + expect(() => saveConfigPreservingClaudeCode(config)).toThrow(); + expect(config).toEqual(beforeLive); + expect(readFileSync(getConfigPath(), "utf8")).toBe(beforeDisk); + } + }); + + test("malformed whole maps degrade only their own optional fields in both read paths", () => { + const raw = fixture(); + Reflect.set(raw, "modelPinnedEfforts", []); + Reflect.set(raw.providers.alpha!, "modelPinnedReasoningEfforts", null); + writeFileSync(getConfigPath(), JSON.stringify(raw)); + const disk = readFileSync(getConfigPath(), "utf8"); + const warn = spyOn(console, "warn").mockImplementation(() => {}); + try { + for (const config of [loadConfig(), readConfigDiagnostics().config]) { + expect(config.modelPinnedEfforts).toBeUndefined(); + expect(config.providers.alpha!.modelPinnedReasoningEfforts).toBeUndefined(); + expect(config.providers.alpha!.pinnedReasoningEffort).toBe("high"); + expect(config.providers.alpha!.apiKey).toBe("fixture-private-key"); + } + expect(readFileSync(getConfigPath(), "utf8")).toBe(disk); + } finally { warn.mockRestore(); } + }); + + test("strict candidate parsing normalizes pin keys without changing input", () => { + const config = fixture(); + config.modelPinnedEfforts = { " alpha/one ": "none" }; + config.providers.alpha!.modelPinnedReasoningEfforts = { " one ": "minimal" }; + const result = validateConfigCandidate(config); + expect(result.ok).toBe(true); + if (!result.ok) throw new Error(result.error); + expect(result.config.modelPinnedEfforts).toEqual({ "alpha/one": "none" }); + expect(result.config.providers.alpha!.modelPinnedReasoningEfforts).toEqual({ one: "minimal" }); + expect(config.modelPinnedEfforts).toEqual({ " alpha/one ": "none" }); + }); + + test("canonical OpenAI admits validated pin overlays while retaining transport and credential checks", () => { + const seed = providerConfigSeed(getProviderRegistryEntry("openai")!); + const pins = { pinnedReasoningEffort: "none", modelPinnedReasoningEfforts: { "gpt-test": "ultra" } }; + const provider = { ...seed, ...pins }; + expect(providerManagementConfigError("openai", provider)).toBeNull(); + for (const patch of [ + { pinnedReasoningEffort: "invalid" }, { modelPinnedReasoningEfforts: [] }, + { modelPinnedReasoningEfforts: { constructor: "high" } }, + { baseUrl: "https://elsewhere.example.test/v1" }, { authMode: "local" }, { apiKey: "do-not-admit" }, + ]) expect(providerManagementConfigError("openai", { ...provider, ...patch })).not.toBeNull(); + const config = { ...getDefaultConfig(), providers: { openai: provider } }; + expect(providerEditorConfigDTO(config).providers.openai).toMatchObject(pins); + const privateConfig = fixture(); + expect(providerEditorConfigDTO(privateConfig).providers.alpha).not.toHaveProperty("apiKey"); + expect(JSON.stringify(safeConfigDTO(privateConfig))).not.toContain("fixture-private-key"); + }); +}); + +describe("provider pin management", () => { + test("GET returns provider pins and canonical OpenAI PATCH/POST round-trip them", async () => { + const dns = spyOn(destinationPolicy, "providerDestinationResolvedError").mockResolvedValue(null); + try { + const config = getDefaultConfig(); + config.providers.openai = providerConfigSeed(getProviderRegistryEntry("openai")!); + saveConfig(config); + expect((await handleProviderRoutes(context(config, "/api/providers?name=openai", "PATCH", { + pinnedReasoningEffort: "none", modelPinnedReasoningEfforts: { "gpt-test": "ultra" }, + })))?.status).toBe(200); + const response = await handleProviderRoutes(context(config, "/api/providers", "GET")); + const providers = await response!.json() as Array<{ name: string; pinnedReasoningEffort?: string; modelPinnedReasoningEfforts?: Record }>; + expect(providers.find(provider => provider.name === "openai")).toMatchObject({ + pinnedReasoningEffort: "none", modelPinnedReasoningEfforts: { "gpt-test": "ultra" }, + }); + const seed = providerConfigSeed(getProviderRegistryEntry("openai")!); + expect((await handleProviderRoutes(context(config, "/api/providers", "POST", { name: "openai", provider: seed })))?.status).toBe(200); + expect(loadConfig().providers.openai).toMatchObject({ pinnedReasoningEffort: "none", modelPinnedReasoningEfforts: { "gpt-test": "ultra" } }); + } finally { dns.mockRestore(); } + }); + + test("PATCH merges normalized keys, clears entries and persists whole-field clears", async () => { + const dns = spyOn(destinationPolicy, "providerDestinationResolvedError").mockResolvedValue(null); + try { + const config = loadConfig(); + let ctx = context(config, "/api/providers?name=alpha", "PATCH", { + modelPinnedReasoningEfforts: { " one ": null, " three ": "ultra" }, + }); + expect((await handleProviderRoutes(ctx))?.status).toBe(200); + expect(config.providers.alpha!.modelPinnedReasoningEfforts).toEqual({ two: "none", three: "ultra" }); + expect(config.providers.alpha!.pinnedReasoningEffort).toBe("high"); + ctx = context(config, "/api/providers?name=alpha", "PATCH", { pinnedReasoningEffort: null, modelPinnedReasoningEfforts: null }); + expect((await handleProviderRoutes(ctx))?.status).toBe(200); + const reloaded = loadConfig(); + expect(reloaded.providers.alpha).not.toHaveProperty("pinnedReasoningEffort"); + expect(reloaded.providers.alpha).not.toHaveProperty("modelPinnedReasoningEfforts"); + } finally { dns.mockRestore(); } + }); + + test("POST omission preserves pins; entry tombstones and explicit null do not remerge old pins", async () => { + const dns = spyOn(destinationPolicy, "providerDestinationResolvedError").mockResolvedValue(null); + try { + const config = loadConfig(); + const base = { adapter: "openai-chat", baseUrl: "https://alpha.example.test/v1" }; + for (const [patch, expectedScalar, expectedMap] of [ + [{}, "high", { one: "low", two: "none" }], + [{ modelPinnedReasoningEfforts: { one: "", " three ": "minimal" } }, "high", { two: "none", three: "minimal" }], + [{ pinnedReasoningEffort: null, modelPinnedReasoningEfforts: null }, undefined, undefined], + ] as const) { + const ctx = context(config, "/api/providers", "POST", { name: "alpha", provider: { ...base, ...patch } }); + expect((await handleProviderRoutes(ctx))?.status).toBe(200); + const reloaded = loadConfig().providers.alpha!; + expect(reloaded.pinnedReasoningEffort).toBe(expectedScalar); + expect(reloaded.modelPinnedReasoningEfforts).toEqual(expectedMap); + } + } finally { dns.mockRestore(); } + }); + + test("invalid pin PATCH/POST leaves live and disk unchanged and never calls save", async () => { + const config = loadConfig(); + const beforeLive = structuredClone(config); + const beforeDisk = readFileSync(getConfigPath(), "utf8"); + for (const method of ["PATCH", "POST"]) { + const pins = { pinnedReasoningEffort: "low", modelPinnedReasoningEfforts: { " same ": "none", same: "high" } }; + const ctx = context(config, "/api/providers?name=alpha", method, method === "POST" + ? { name: "alpha", provider: { ...config.providers.alpha, ...pins } } : pins); + ctx.deps.saveConfigPreservingClaudeCode = mock(() => {}); + expect((await handleProviderRoutes(ctx))?.status).toBe(400); + expect(ctx.deps.saveConfigPreservingClaudeCode).not.toHaveBeenCalled(); + expect(config).toEqual(beforeLive); + expect(readFileSync(getConfigPath(), "utf8")).toBe(beforeDisk); + } + }); + + test("PATCH and POST save failures restore exact provider ownership and pending deletion metadata", async () => { + const dns = spyOn(destinationPolicy, "providerDestinationResolvedError").mockResolvedValue(null); + try { + for (const method of ["PATCH", "POST"]) { + const config = loadConfig(); + deleteConfigTopLevelKey(config, "modelPickerOrder"); + const row = config.providers.alpha; + const beforeLive = structuredClone(config); + const beforeProjection = projectConfigRebaseProvenance(config); + const beforeDisk = readFileSync(getConfigPath(), "utf8"); + const patch = { pinnedReasoningEffort: null, modelPinnedReasoningEfforts: null }; + const ctx = context(config, "/api/providers?name=alpha", method, method === "POST" + ? { name: "alpha", provider: { ...row, ...patch }, setDefault: true } : patch); + ctx.deps.saveConfigPreservingClaudeCode = () => { + deleteConfigTopLevelKey(config, "modelPinnedEfforts"); + // Restore the value but leave the injected deletion intent pending. + config.modelPinnedEfforts = beforeLive.modelPinnedEfforts; + config.configRebaseProvenance = { version: 1, deletedTopLevelKeys: ["effortCap"] }; + throw new Error("fixture pin save failure"); + }; + await expect(handleProviderRoutes(ctx)).rejects.toThrow("fixture pin save failure"); + expect(config.providers.alpha).toBe(row); + expect(config).toEqual(beforeLive); + expect(projectConfigRebaseProvenance(config)).toEqual(beforeProjection); + expect(readFileSync(getConfigPath(), "utf8")).toBe(beforeDisk); + } + } finally { dns.mockRestore(); } + }); + + test("raw editor omission deletes both pin fields while preserving provider credentials", async () => { + const dns = spyOn(destinationPolicy, "providerDestinationResolvedError").mockResolvedValue(null); + try { + const config = loadConfig(); + const baseline = providerEditorConfigDTO(config); + const next = structuredClone(baseline); + delete next.providers.alpha!.pinnedReasoningEffort; + delete next.providers.alpha!.modelPinnedReasoningEfforts; + expect((await handleProviderRoutes(context(config, "/api/providers", "PUT", { baseline, next })))?.status).toBe(200); + const provider = loadConfig().providers.alpha!; + expect(provider).not.toHaveProperty("pinnedReasoningEffort"); + expect(provider).not.toHaveProperty("modelPinnedReasoningEfforts"); + expect(provider.apiKey).toBe("fixture-private-key"); + } finally { dns.mockRestore(); } + }); + + test("new provider POST save failure restores registration state, default and absent row", async () => { + const dns = spyOn(destinationPolicy, "providerDestinationResolvedError").mockResolvedValue(null); + try { + const config = loadConfig(); + config.disabledModels = ["beta/stale", "alpha/keep"]; + config.modelDiscovery = { + knownModels: { beta: { ids: ["stale"], removed: [], updatedAt: "2026-01-01T00:00:00Z" } }, + recentArrivals: { beta: [{ id: "stale", at: "2026-01-01T00:00:00Z" }] }, + }; + const before = structuredClone(config); + const disk = readFileSync(getConfigPath(), "utf8"); + const ctx = context(config, "/api/providers", "POST", { name: "beta", setDefault: true, provider: { + adapter: "openai-chat", baseUrl: "https://beta.example.test/v1", pinnedReasoningEffort: "minimal", + } }); + ctx.deps.saveConfigPreservingClaudeCode = candidate => { + expect(candidate.defaultProvider).toBe("beta"); + expect(candidate.disabledModels).toEqual(["alpha/keep"]); + expect(candidate.modelDiscovery!.knownModels).not.toHaveProperty("beta"); + expect(candidate.modelDiscovery!.recentArrivals).not.toHaveProperty("beta"); + throw new Error("fixture registration save failure"); + }; + await expect(handleProviderRoutes(ctx)).rejects.toThrow("fixture registration save failure"); + expect(config).toEqual(before); + expect(config.providers).not.toHaveProperty("beta"); + expect(readFileSync(getConfigPath(), "utf8")).toBe(disk); + } finally { dns.mockRestore(); } + }); +}); + +describe("effort caps pin transaction", () => { + test("GET exposes pins; mixed invalid PUT requests leave live and disk unchanged", async () => { + const config = loadConfig(); + const get = await handleAgentSettingsRoutes(context(config, "/api/effort-caps", "GET")); + expect(await get!.json()).toMatchObject({ modelPinnedEfforts: { "alpha/one": "ultra", two: "minimal" } }); + const before = structuredClone(config); + const disk = readFileSync(getConfigPath(), "utf8"); + for (const patch of [ + { effortCap: "low", modelPinnedEfforts: { bad: "invalid" } }, + { effortCap: null, subagentEffortCap: "invalid", modelPinnedEfforts: null }, + { effortCap: "low", modelPinnedEfforts: { " two ": null, two: "high" } }, + { effortCap: "low", modelPinnedEfforts: JSON.parse('{"__proto__":"high"}') }, + null, [], + ]) { + const ctx = context(config, "/api/effort-caps", "PUT", patch); + ctx.deps.saveConfigPreservingClaudeCode = mock(() => {}); + expect((await handleAgentSettingsRoutes(ctx))?.status).toBe(400); + expect(ctx.deps.saveConfigPreservingClaudeCode).not.toHaveBeenCalled(); + expect(config).toEqual(before); + expect(configRebaseDeletionKeys(config).size).toBe(0); + expect(readFileSync(getConfigPath(), "utf8")).toBe(disk); + } + }); + + test("PUT merges pin keys and persists null clears with deletion provenance", async () => { + const config = loadConfig(); + let ctx = context(config, "/api/effort-caps", "PUT", { effortCap: "high", modelPinnedEfforts: { two: "", " third ": "none" } }); + expect((await handleAgentSettingsRoutes(ctx))?.status).toBe(200); + expect(loadConfig().modelPinnedEfforts).toEqual({ "alpha/one": "ultra", third: "none" }); + ctx = context(config, "/api/effort-caps", "PUT", { effortCap: null, subagentEffortCap: null, modelPinnedEfforts: null }); + expect((await handleAgentSettingsRoutes(ctx))?.status).toBe(200); + const reloaded = loadConfig(); + for (const key of ["effortCap", "subagentEffortCap", "modelPinnedEfforts"] as const) { + expect(reloaded).not.toHaveProperty(key); + expect(configRebaseDeletionKeys(reloaded).has(key)).toBe(true); + } + }); + + test("save failure rolls back caps, pins, provenance and preexisting pending deletion intent", async () => { + const config = loadConfig(); + deleteConfigTopLevelKey(config, "modelPickerOrder"); + const before = structuredClone(config); + const projection = projectConfigRebaseProvenance(config); + const disk = readFileSync(getConfigPath(), "utf8"); + const ctx = context(config, "/api/effort-caps", "PUT", { effortCap: null, subagentEffortCap: "low", modelPinnedEfforts: null }); + ctx.deps.saveConfigPreservingClaudeCode = () => { throw new Error("fixture disk full"); }; + await expect(handleAgentSettingsRoutes(ctx)).rejects.toThrow("fixture disk full"); + expect(config).toEqual(before); + expect(projectConfigRebaseProvenance(config)).toEqual(projection); + expect(readFileSync(getConfigPath(), "utf8")).toBe(disk); + saveConfigPreservingClaudeCode(config); + expect(loadConfig().modelPinnedEfforts).toEqual(before.modelPinnedEfforts); + expect(configRebaseDeletionKeys(loadConfig()).has("modelPickerOrder")).toBe(true); + }); + + test("unknown future deletion provenance rejects a clear before mutation", async () => { + const config = loadConfig(); + config.configRebaseProvenance = { version: 2, future: true }; + const before = structuredClone(config); + const ctx = context(config, "/api/effort-caps", "PUT", { modelPinnedEfforts: null }); + ctx.deps.saveConfigPreservingClaudeCode = mock(() => {}); + expect((await handleAgentSettingsRoutes(ctx))?.status).toBe(409); + expect(config).toEqual(before); + expect(ctx.deps.saveConfigPreservingClaudeCode).not.toHaveBeenCalled(); + }); +}); diff --git a/tests/fixtures/test-layout-expected.json b/tests/fixtures/test-layout-expected.json index 05ce7e87ff..fbd8836dea 100644 --- a/tests/fixtures/test-layout-expected.json +++ b/tests/fixtures/test-layout-expected.json @@ -1136,5 +1136,7 @@ "zz-pr-coderabbit-readiness-revalidation.test.ts": "ci-workflows", "cli-models-price.test.ts": "cli", "model-costs-management-api.test.ts": "server", - "usage-time-range.test.ts": "usage" + "usage-time-range.test.ts": "usage", + "model-pinned-effort.test.ts": "codex-integration", + "model-pinned-effort-config.test.ts": "config" } diff --git a/tests/model-pinned-effort.test.ts b/tests/model-pinned-effort.test.ts deleted file mode 100644 index c647c21b12..0000000000 --- a/tests/model-pinned-effort.test.ts +++ /dev/null @@ -1,239 +0,0 @@ -import { describe, expect, test } from "bun:test"; -import { mkdtempSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { resolvePinnedEffort, applyPinnedEffort } from "../src/server/effort-policy"; -import { handleManagementAPI } from "../src/server/management-api"; -import type { OcxConfig, OcxParsedRequest, OcxProviderConfig } from "../src/types"; - -describe("model pinned reasoning effort policy", () => { - const providerWithPinned: OcxProviderConfig = { - adapter: "openai-responses", - baseUrl: "https://api.openai.com/v1", - pinnedReasoningEffort: "high", - modelPinnedReasoningEfforts: { - "special-model": "max", - "disabled-effort-model": "none", - }, - }; - - test("resolves model-specific pinned effort over provider-wide pinned effort", () => { - const route = { provider: providerWithPinned, modelId: "special-model" }; - expect(resolvePinnedEffort(route)).toBe("max"); - }); - - test("resolves provider-wide pinned effort when model is not specifically pinned", () => { - const route = { provider: providerWithPinned, modelId: "other-model" }; - expect(resolvePinnedEffort(route)).toBe("high"); - }); - - test("resolves global config modelPinnedEfforts fallback when provider has none", () => { - const emptyProvider: OcxProviderConfig = { - adapter: "openai-responses", - baseUrl: "https://api.openai.com/v1", - }; - const config = { - modelPinnedEfforts: { "global-pinned": "max" }, - } as unknown as OcxConfig; - const route = { provider: emptyProvider, modelId: "global-pinned" }; - expect(resolvePinnedEffort(route, undefined, config)).toBe("max"); - }); - - test("applyPinnedEffort overrides caller effort in both parsed options and raw body", () => { - const route = { provider: providerWithPinned, modelId: "special-model" }; - const parsed: OcxParsedRequest = { - modelId: "special-model", - context: { messages: [] }, - stream: true, - options: { reasoning: "low" }, - _rawBody: { reasoning: { effort: "low" } }, - }; - - const rewrite = applyPinnedEffort(parsed, route); - expect(rewrite).toEqual({ from: "low", to: "max" }); - expect(parsed.options.reasoning).toBe("max"); - expect((parsed._rawBody as any).reasoning.effort).toBe("max"); - }); - - test("applyPinnedEffort applies pinned effort when caller sent none", () => { - const route = { provider: providerWithPinned, modelId: "other-model" }; - const parsed: OcxParsedRequest = { - modelId: "other-model", - context: { messages: [] }, - stream: true, - options: {}, - _rawBody: {}, - }; - - const rewrite = applyPinnedEffort(parsed, route); - expect(rewrite).toEqual({ from: undefined, to: "high" }); - expect(parsed.options.reasoning).toBe("high"); - expect((parsed._rawBody as any).reasoning.effort).toBe("high"); - }); - - test("applyPinnedEffort with none strips effort from both shapes", () => { - const route = { provider: providerWithPinned, modelId: "disabled-effort-model" }; - const parsed: OcxParsedRequest = { - modelId: "disabled-effort-model", - context: { messages: [] }, - stream: true, - options: { reasoning: "high" }, - _rawBody: { reasoning: { effort: "high", summary: "auto" } }, - }; - - const rewrite = applyPinnedEffort(parsed, route); - expect(rewrite).toEqual({ from: "high", to: "none" }); - expect(parsed.options.reasoning).toBeUndefined(); - expect((parsed._rawBody as any).reasoning.effort).toBeUndefined(); - expect((parsed._rawBody as any).reasoning.summary).toBe("auto"); - }); -}); - -describe("management API pinned reasoning effort configuration", () => { - let tempHome: string; - function isolatedHome(): void { - tempHome = mkdtempSync(join(tmpdir(), "ocx-pinned-effort-")); - process.env.OPENCODEX_HOME = tempHome; - } - - function makeConfig(overrides: Partial = {}): OcxConfig { - return { - version: 1, - providers: { - custom: { - adapter: "openai-responses", - baseUrl: "https://api.custom.com", - allowPrivateNetwork: true, - }, - }, - ...overrides, - } as unknown as OcxConfig; - } - - test("PATCH /api/providers sets and updates pinned reasoning efforts", async () => { - isolatedHome(); - const config = makeConfig(); - const patchReq = new Request("http://localhost/api/providers?name=custom", { - method: "PATCH", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - pinnedReasoningEffort: "high", - modelPinnedReasoningEfforts: { "model-a": "max", "model-b": "low" }, - }), - }); - const patchRes = await handleManagementAPI(patchReq, new URL(patchReq.url), config); - expect(patchRes?.status).toBe(200); - const provider = config.providers.custom; - expect(provider.pinnedReasoningEffort).toBe("high"); - expect(provider.modelPinnedReasoningEfforts).toEqual({ "model-a": "max", "model-b": "low" }); - - // Updating with whitespace key normalizes to trimmed model id - const wsReq = new Request("http://localhost/api/providers?name=custom", { - method: "PATCH", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - modelPinnedReasoningEfforts: { " model-c ": "medium" }, - }), - }); - const wsRes = await handleManagementAPI(wsReq, new URL(wsReq.url), config); - expect(wsRes?.status).toBe(200); - expect(config.providers.custom.modelPinnedReasoningEfforts).toEqual({ "model-a": "max", "model-b": "low", "model-c": "medium" }); - - // Clearing a model pinned effort with whitespace key - const wsClearReq = new Request("http://localhost/api/providers?name=custom", { - method: "PATCH", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - modelPinnedReasoningEfforts: { " model-c ": null }, - }), - }); - const wsClearRes = await handleManagementAPI(wsClearReq, new URL(wsClearReq.url), config); - expect(wsClearRes?.status).toBe(200); - expect(config.providers.custom.modelPinnedReasoningEfforts).toEqual({ "model-a": "max", "model-b": "low" }); - - // Clearing a model pinned effort - const clearReq = new Request("http://localhost/api/providers?name=custom", { - method: "PATCH", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - modelPinnedReasoningEfforts: { "model-a": null }, - }), - }); - const clearRes = await handleManagementAPI(clearReq, new URL(clearReq.url), config); - expect(clearRes?.status).toBe(200); - expect(config.providers.custom.modelPinnedReasoningEfforts).toEqual({ "model-b": "low" }); - }); - - test("PATCH /api/providers rejects invalid reasoning effort values", async () => { - isolatedHome(); - const config = makeConfig(); - const badReq = new Request("http://localhost/api/providers?name=custom", { - method: "PATCH", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - pinnedReasoningEffort: "invalid-tier", - }), - }); - const badRes = await handleManagementAPI(badReq, new URL(badReq.url), config); - expect(badRes?.status).toBe(400); - }); - - test("PUT /api/effort-caps supports modelPinnedEfforts roundtrip", async () => { - isolatedHome(); - const config = makeConfig(); - const putReq = new Request("http://localhost/api/effort-caps", { - method: "PUT", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - modelPinnedEfforts: { "gpt-5.5": "max", "claude-sonnet-4-6": "high" }, - }), - }); - const putRes = await handleManagementAPI(putReq, new URL(putReq.url), config); - expect(putRes?.status).toBe(200); - expect(config.modelPinnedEfforts).toEqual({ "gpt-5.5": "max", "claude-sonnet-4-6": "high" }); - - const getReq = new Request("http://localhost/api/effort-caps"); - const getRes = await handleManagementAPI(getReq, new URL(getReq.url), config); - const data = await getRes?.json() as { modelPinnedEfforts: Record }; - expect(data.modelPinnedEfforts).toEqual({ "gpt-5.5": "max", "claude-sonnet-4-6": "high" }); - - // Partial merge: add one model, clear another - const updateReq = new Request("http://localhost/api/effort-caps", { - method: "PUT", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - modelPinnedEfforts: { "gemini-3.7-flash": "high", "gpt-5.5": null }, - }), - }); - const updateRes = await handleManagementAPI(updateReq, new URL(updateReq.url), config); - expect(updateRes?.status).toBe(200); - expect(config.modelPinnedEfforts).toEqual({ "claude-sonnet-4-6": "high", "gemini-3.7-flash": "high" }); - }); -}); -import { ManagementRequest as Request } from "./helpers/management-auth"; - -describe("native chat completions effort policy", () => { - const { chatCollabSurface, applyChatEffortCap } = require("../src/server/effort-policy"); - - test("detects v2 collab surface in native chat tools", () => { - const chatBody = { - tools: [ - { type: "function", function: { name: "spawn_agent" } }, - { type: "function", function: { name: "send_message" } }, - ], - }; - expect(chatCollabSurface(chatBody)).toBe("v2"); - }); - - test("applyChatEffortCap respects effortCap ceiling over pinned effort", () => { - const config = { - effortCap: "low", - }; - const chatBody = { - reasoning_effort: "max", - }; - const rewrite = applyChatEffortCap(chatBody, new Headers(), config, ["low", "medium", "high", "max"]); - expect(rewrite).toEqual({ from: "max", to: "low", subagent: false }); - expect(chatBody.reasoning_effort).toBe("low"); - }); -}); From c145a7007c7789efb1f3182b76b410621c9a4a1a Mon Sep 17 00:00:00 2001 From: t Date: Mon, 7 Sep 2026 22:10:43 +0900 Subject: [PATCH 5/6] fix: preserve price writes across concurrent provider replacement [skip ci] found by #3336 rebase security audit Re-read provider ownership after parsing the price request body, then merge and save against the current row. Cover replacement and deletion during a deferred body read without weakening existing pricing validation or tests. Co-authored-by: Liang-Psych <19504206+Liang-Psych@users.noreply.github.com> --- src/server/management/model-routes.ts | 8 +- .../server/model-costs-management-api.test.ts | 82 ++++++++++++++++++- 2 files changed, 88 insertions(+), 2 deletions(-) diff --git a/src/server/management/model-routes.ts b/src/server/management/model-routes.ts index cc7a732a88..b5fca15abb 100644 --- a/src/server/management/model-routes.ts +++ b/src/server/management/model-routes.ts @@ -379,8 +379,8 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise { function harness(config = fixture(), persist?: (saved: OcxConfig) => void) { const persisted: OcxConfig[] = []; let convergeCalls = 0; - async function call(method: "GET" | "PUT", body?: unknown, provider = PROVIDER, rawBody?: string, rawProvider?: string) { + async function call(method: "GET" | "PUT", body?: unknown, provider = PROVIDER, rawBody?: string | ReadableStream, rawProvider?: string) { const url = new URL(`http://127.0.0.1:10100/api/providers/${rawProvider ?? encodeURIComponent(provider)}/model-costs`); const response = await handleModelRoutes({ version: "test", @@ -89,6 +89,23 @@ function harness(config = fixture(), persist?: (saved: OcxConfig) => void) { return { call, config, persisted, get convergeCalls() { return convergeCalls; } }; } +/** No eager buffering: requested resolves only when the request parser pulls the body. */ +function deferredJsonBody(value: unknown) { + let requestPull!: () => void; + let release!: () => void; + const requested = new Promise(resolve => { requestPull = resolve; }); + const released = new Promise(resolve => { release = resolve; }); + const body = new ReadableStream({ + async pull(controller) { + requestPull(); + await released; + controller.enqueue(new TextEncoder().encode(JSON.stringify(value))); + controller.close(); + }, + }, { highWaterMark: 0 }); + return { body, requested, release }; +} + describe("provider model costs API", () => { test("GET returns the exact configured provider's sanitized map or an empty map", async () => { const h = harness(); @@ -140,6 +157,69 @@ describe("provider model costs API", () => { expect(activeUserCostOverlays().some(row => row.provider === PROVIDER && row.modelId === "org/model")).toBe(false); }); + test("price PUT follows a provider row replaced by a pin edit while parsing its body", async () => { + const config = fixture({ "org/model": ZERO, sibling: SIBLING }); + writeFileSync(join(home, "config.json"), JSON.stringify(config)); + const h = harness(config, saveConfigPreservingClaudeCode); + const oldRow = config.providers[PROVIDER]!; + const oldCosts = oldRow.modelCosts; + const oldSnapshot = structuredClone(oldRow); + const deferred = deferredJsonBody({ modelId: "org/model", cost: COST }); + const pending = h.call("PUT", undefined, PROVIDER, deferred.body); + await deferred.requested; + + // Reproduce the provider PATCH ownership boundary without DNS or catalog side effects. + // This exercises row replacement during body parsing, not the pin PATCH route itself. + const newerSibling: ProviderCostOverlay = { input: 9, output: 11, cacheRead: 1, cacheWrite: 6 }; + const replacement = { + ...oldRow, + pinnedReasoningEffort: "high", + modelCosts: { ...oldRow.modelCosts, sibling: newerSibling, "newer/sibling": SIBLING }, + }; + config.providers[PROVIDER] = replacement; + deferred.release(); + + const response = await pending; + expect(response.status).toBe(200); + expect(await response.json()).toEqual({ ok: true, provider: PROVIDER, modelId: "org/model", cost: COST }); + expect(config.providers[PROVIDER]).toBe(replacement); + const expected = { "org/model": COST, sibling: newerSibling, "newer/sibling": SIBLING }; + expect(replacement.pinnedReasoningEffort).toBe("high"); + expect(replacement.modelCosts).toEqual(expected); + expect(oldRow).toEqual(oldSnapshot); + expect(oldRow.modelCosts).toBe(oldCosts); + expect(h.persisted).toHaveLength(1); + expect(h.persisted[0]!.providers[PROVIDER]!.pinnedReasoningEffort).toBe("high"); + expect(h.persisted[0]!.providers[PROVIDER]!.modelCosts).toEqual(expected); + const disk = JSON.parse(readFileSync(join(home, "config.json"), "utf8")) as OcxConfig; + expect(disk.providers[PROVIDER]!.pinnedReasoningEffort).toBe("high"); + expect(disk.providers[PROVIDER]!.modelCosts).toEqual(expected); + expect(h.convergeCalls).toBe(0); + }); + + test("price PUT returns 404 without persisting if the provider is removed during body parsing", async () => { + const config = fixture({ "org/model": ZERO, sibling: SIBLING }); + writeFileSync(join(home, "config.json"), JSON.stringify(config)); + const diskBefore = readFileSync(join(home, "config.json"), "utf8"); + const h = harness(config, saveConfigPreservingClaudeCode); + const oldRow = config.providers[PROVIDER]!; + const oldSnapshot = structuredClone(oldRow); + const deferred = deferredJsonBody({ modelId: "org/model", cost: COST }); + const pending = h.call("PUT", undefined, PROVIDER, deferred.body); + await deferred.requested; + delete config.providers[PROVIDER]; + deferred.release(); + + const response = await pending; + expect(response.status).toBe(404); + expect(await response.json()).toEqual({ error: "provider not found" }); + expect(Object.hasOwn(config.providers, PROVIDER)).toBe(false); + expect(oldRow).toEqual(oldSnapshot); + expect(h.persisted).toHaveLength(0); + expect(readFileSync(join(home, "config.json"), "utf8")).toBe(diskBefore); + expect(h.convergeCalls).toBe(0); + }); + test("persist failure restores map identity and own-property absence for set and reset", async () => { for (const costs of [undefined, {}, { "org/model": COST, sibling: SIBLING }]) { for (const cost of [SIBLING, null]) { From eeb550181ea5167078b11c4f0445bc02d0ae6dac Mon Sep 17 00:00:00 2001 From: t Date: Mon, 7 Sep 2026 22:19:20 +0900 Subject: [PATCH 6/6] test: use a valid provider config for effort-cap API fixtures [skip ci] Keep the pure policy factory unchanged. The two management cases now use a provider that matches defaultProvider under full candidate validation; all success, persistence, deletion and rejection assertions remain intact. Co-authored-by: Liang-Psych <19504206+Liang-Psych@users.noreply.github.com> --- tests/codex-integration/effort-policy.test.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/tests/codex-integration/effort-policy.test.ts b/tests/codex-integration/effort-policy.test.ts index 3f2262ade6..2f1e65c10c 100644 --- a/tests/codex-integration/effort-policy.test.ts +++ b/tests/codex-integration/effort-policy.test.ts @@ -441,6 +441,18 @@ describe("cap composition with downstream clamps", () => { }); describe("/api/effort-caps", () => { + // Management writes validate the entire config, unlike the pure policy helpers above. + function makeApiConfig(overrides: Partial = {}): OcxConfig { + return makeConfig({ + defaultProvider: "effort-fixture", + providers: { "effort-fixture": { + adapter: "openai-chat", + baseUrl: "https://effort.example.invalid/v1", + } }, + ...overrides, + }); + } + function isolatedHome(): void { tempHome = mkdtempSync(join(tmpdir(), "ocx-effort-caps-")); process.env.OPENCODEX_HOME = tempHome; @@ -459,7 +471,7 @@ describe("/api/effort-caps", () => { test("PUT sets both caps; GET surfaces them with the ladder", async () => { isolatedHome(); - const config = makeConfig(); + const config = makeApiConfig(); const putRes = await put(config, { effortCap: "high", subagentEffortCap: "medium" }); expect(await putRes.json()).toEqual({ ok: true, effortCap: "high", subagentEffortCap: "medium" }); expect(config.effortCap).toBe("high"); @@ -476,7 +488,7 @@ describe("/api/effort-caps", () => { test("absent key unchanged; null clears; invalid ladder value -> 400", async () => { isolatedHome(); - const config = makeConfig({ effortCap: "high", subagentEffortCap: "medium" }); + const config = makeApiConfig({ effortCap: "high", subagentEffortCap: "medium" }); const keep = await put(config, { subagentEffortCap: "low" }); expect(keep.status).toBe(200); expect(config.effortCap).toBe("high");