diff --git a/src/providers/derive.ts b/src/providers/derive.ts index 2a224476a3..b0c69090ad 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( @@ -239,6 +250,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 } : {}), @@ -299,6 +312,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 } : {}), @@ -466,6 +481,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); @@ -492,6 +509,8 @@ export function enrichProviderFromRegistry(name: string, prov: OcxProviderConfig } if (!prov.modelReasoningEfforts && seed.modelReasoningEfforts) prov.modelReasoningEfforts = cloneRecordOfArrays(seed.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 874af4633c..2379a8460a 100644 --- a/src/router.ts +++ b/src/router.ts @@ -315,6 +315,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); @@ -460,6 +462,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/chat-native.ts b/src/server/chat-native.ts index 32f3daea8a..6b85d81bc6 100644 --- a/src/server/chat-native.ts +++ b/src/server/chat-native.ts @@ -6,6 +6,7 @@ import { collectChatCompletion, isChatCompletionsStreamError, } from "../chat/outbound"; +import { applyChatEffortCap, chatCollabSurface, effortCapAppliesTo, resolvePinnedEffort, supportedLadderFor } from "./effort-policy"; import { classifyError, cyberPolicyErrorType, @@ -144,9 +145,28 @@ 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; + } + + 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 2686b73460..a6f0ac92a9 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"; /** @@ -188,3 +188,130 @@ 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 && isDeclaredReasoningEffort(rawProvModel)) { + return rawProvModel; + } + 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 && isDeclaredReasoningEffort(rawGlobal)) { + 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. + */ +/** + * 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 }, + 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 193f9841fa..62c6abc82b 100644 --- a/src/server/management/agent-settings-routes.ts +++ b/src/server/management/agent-settings-routes.ts @@ -602,13 +602,14 @@ 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"); + const { isCodexReasoningEffort, isDeclaredReasoningEffort } = await import("../../reasoning-effort"); for (const key of ["effortCap", "subagentEffortCap"] as const) { if (!(key in body)) continue; const value = body[key]; @@ -618,8 +619,40 @@ 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 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); + } + } 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 } : {}), + }); } // Subagent model picker: which ≤5 routed models Codex's spawn_agent advertises (it shows the diff --git a/src/server/management/provider-routes.ts b/src/server/management/provider-routes.ts index 8b9f8d0dd4..fcd2a471f1 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 { isDeclaredReasoningEffort } from "../../reasoning-effort"; import { replaceProviderAccountSet } from "../../oauth/store"; import { providerDestinationResolvedError } from "../../lib/destination-policy"; import { reconcileLiveStateStores } from "../../lib/state-store-registrations"; @@ -468,6 +469,41 @@ 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; + } + touched = true; + } if (Object.hasOwn(rawBody, "modelAutoCompactTokenLimits")) { const value = rawBody.modelAutoCompactTokenLimits; const error = modelAutoCompactTokenLimitsConfigError(value, { @@ -679,6 +715,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 06270fc172..4fe47ea6e5 100644 --- a/src/types/config.ts +++ b/src/types/config.ts @@ -512,6 +512,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 a3a4dd4c10..72a559f3db 100644 --- a/src/types/provider.ts +++ b/src/types/provider.ts @@ -467,6 +467,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..c647c21b12 --- /dev/null +++ b/tests/model-pinned-effort.test.ts @@ -0,0 +1,239 @@ +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"); + }); +});