diff --git a/messages/en/dashboard.json b/messages/en/dashboard.json index 752ed7c37..f9741421e 100644 --- a/messages/en/dashboard.json +++ b/messages/en/dashboard.json @@ -551,6 +551,11 @@ "overridden": "Overridden by provider", "tooltip": "Thinking effort in the Codex request (reasoning.effort), shown verbatim." }, + "reasoningEffortOpenai": { + "label": "Reasoning effort", + "overridden": "Overridden by provider", + "tooltip": "Thinking effort in the OpenAI chat/completions request (reasoning_effort / reasoning.effort), shown verbatim." + }, "logicTrace": { "title": "Decision Chain", "singleRouteSelectionTitle": "Provider selection under single-route protection", diff --git a/messages/ja/dashboard.json b/messages/ja/dashboard.json index 0f49373bb..79b101a14 100644 --- a/messages/ja/dashboard.json +++ b/messages/ja/dashboard.json @@ -551,6 +551,11 @@ "overridden": "プロバイダーにより上書き", "tooltip": "Codex リクエストの推論強度 (reasoning.effort) をそのまま表示します。" }, + "reasoningEffortOpenai": { + "label": "推論強度", + "overridden": "プロバイダーにより上書き", + "tooltip": "OpenAI chat/completions リクエストの推論強度 (reasoning_effort / reasoning.effort) をそのまま表示します。" + }, "logicTrace": { "title": "決定チェーン", "singleRouteSelectionTitle": "単一経路保護での Provider 選択", diff --git a/messages/ru/dashboard.json b/messages/ru/dashboard.json index 5b630c199..22de6672d 100644 --- a/messages/ru/dashboard.json +++ b/messages/ru/dashboard.json @@ -551,6 +551,11 @@ "overridden": "Переопределено провайдером", "tooltip": "Интенсивность рассуждений в запросе Codex (reasoning.effort), показанная без изменений." }, + "reasoningEffortOpenai": { + "label": "Интенсивность рассуждений", + "overridden": "Переопределено провайдером", + "tooltip": "Интенсивность рассуждений в запросе OpenAI chat/completions (reasoning_effort / reasoning.effort), показанная без изменений." + }, "logicTrace": { "title": "Цепочка решений", "singleRouteSelectionTitle": "Выбор провайдера в режиме защиты одним маршрутом", diff --git a/messages/zh-CN/dashboard.json b/messages/zh-CN/dashboard.json index 5b07a9346..5fa41c03d 100644 --- a/messages/zh-CN/dashboard.json +++ b/messages/zh-CN/dashboard.json @@ -551,6 +551,11 @@ "overridden": "已被供应商覆写", "tooltip": "Codex 请求中的思考强度(reasoning.effort),按原值显示。" }, + "reasoningEffortOpenai": { + "label": "思考强度", + "overridden": "已被供应商覆写", + "tooltip": "OpenAI chat/completions 请求中的思考强度(reasoning_effort / reasoning.effort),按原值显示。" + }, "logicTrace": { "title": "决策链", "singleRouteSelectionTitle": "单路保护下的供应商选择", diff --git a/messages/zh-TW/dashboard.json b/messages/zh-TW/dashboard.json index b69873d4b..8caa52816 100644 --- a/messages/zh-TW/dashboard.json +++ b/messages/zh-TW/dashboard.json @@ -551,6 +551,11 @@ "overridden": "已被供應商覆寫", "tooltip": "Codex 請求中的思考強度(reasoning.effort),按原值顯示。" }, + "reasoningEffortOpenai": { + "label": "思考強度", + "overridden": "已被供應商覆寫", + "tooltip": "OpenAI chat/completions 請求中的思考強度(reasoning_effort / reasoning.effort),按原值顯示。" + }, "logicTrace": { "title": "決策鏈", "singleRouteSelectionTitle": "單路保護下的供應商選擇", diff --git a/package.json b/package.json index 24465dab9..b64a31e0a 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,7 @@ "@langfuse/otel": "^5.10.0", "@langfuse/tracing": "^5.10.0", "@lobehub/icons": "^5.15.0", + "@lobehub/ui": "^5.0.0", "@opentelemetry/sdk-node": "^0.221.0", "@radix-ui/react-alert-dialog": "^1.1.23", "@radix-ui/react-avatar": "^1.2.6", diff --git a/src/app/[locale]/dashboard/logs/_components/error-details-dialog/components/SummaryTab.tsx b/src/app/[locale]/dashboard/logs/_components/error-details-dialog/components/SummaryTab.tsx index ad967e6e1..61d381c87 100644 --- a/src/app/[locale]/dashboard/logs/_components/error-details-dialog/components/SummaryTab.tsx +++ b/src/app/[locale]/dashboard/logs/_components/error-details-dialog/components/SummaryTab.tsx @@ -142,7 +142,12 @@ export function SummaryTab({ const showNoSignatureBadge = thinkingSignatureDetection?.source === "fallback_no_signature_with_thinking"; const thinkingEffortInfo = extractThinkingEffortInfo(specialSettings); - const effortMessageKey = thinkingEffortInfo?.source === "codex" ? "reasoningEffort" : "effort"; + const effortMessageKey = + thinkingEffortInfo?.source === "codex" + ? "reasoningEffort" + : thinkingEffortInfo?.source === "openai" + ? "reasoningEffortOpenai" + : "effort"; const effortDisplay = thinkingEffortInfo ? { requestedEffort: thinkingEffortInfo.requestedEffort, diff --git a/src/app/[locale]/dashboard/logs/_components/thinking-effort-display.test.tsx b/src/app/[locale]/dashboard/logs/_components/thinking-effort-display.test.tsx index 5a1367fc6..b71f64215 100644 --- a/src/app/[locale]/dashboard/logs/_components/thinking-effort-display.test.tsx +++ b/src/app/[locale]/dashboard/logs/_components/thinking-effort-display.test.tsx @@ -180,4 +180,51 @@ describe("ThinkingEffortDisplay", () => { expect(html).toContain("reasoningEffort.tooltip"); expect(html).not.toContain(">medium<"); }); + + test("显示 OpenAI chat/completions 请求中的思考强度", () => { + const html = renderToStaticMarkup( + + ); + + expect(html).toContain('data-slot="thinking-effort"'); + expect(html).toContain("max"); + expect(html).toContain("reasoningEffortOpenai.tooltip"); + expect(html).not.toContain("overridden"); + }); + + test("OpenAI 与 Codex 审计并存时优先展示 Codex 强度", () => { + const html = renderToStaticMarkup( + + ); + + expect(html).toContain("high"); + expect(html).toContain("reasoningEffort.tooltip"); + expect(html).not.toContain(">max<"); + }); }); diff --git a/src/app/[locale]/dashboard/logs/_components/thinking-effort-display.tsx b/src/app/[locale]/dashboard/logs/_components/thinking-effort-display.tsx index 624b8cae2..4dce53430 100644 --- a/src/app/[locale]/dashboard/logs/_components/thinking-effort-display.tsx +++ b/src/app/[locale]/dashboard/logs/_components/thinking-effort-display.tsx @@ -14,7 +14,8 @@ interface ThinkingEffortDisplayProps { } /** - * 在使用记录中展示任意模型的思考强度(Codex reasoning.effort 或 Anthropic effort)。 + * 在使用记录中展示任意模型的思考强度(Codex / OpenAI chat/completions 的 + * reasoning.effort,或 Anthropic 的 output_config.effort)。 * * 供应商改变强度时同时展示请求值和实际转发值,避免只看到客户端参数而误判上游行为。 */ @@ -26,7 +27,12 @@ export function ThinkingEffortDisplay({ specialSettings }: ThinkingEffortDisplay return -; } - const messageNamespace = effortInfo.source === "codex" ? "reasoningEffort" : "effort"; + const messageNamespace = + effortInfo.source === "anthropic" + ? "effort" + : effortInfo.source === "openai" + ? "reasoningEffortOpenai" + : "reasoningEffort"; const showEffectiveBadge = effortInfo.isOverridden && effortInfo.effectiveEffort != null; return ( diff --git a/src/app/v1/_lib/proxy/message-service.test.ts b/src/app/v1/_lib/proxy/message-service.test.ts index d46e26cb0..fc1f49412 100644 --- a/src/app/v1/_lib/proxy/message-service.test.ts +++ b/src/app/v1/_lib/proxy/message-service.test.ts @@ -9,7 +9,11 @@ vi.mock("@/repository/message", () => ({ import { ProxyMessageService } from "./message-service"; -function createSession(providerType: string, message: Record) { +function createSession( + providerType: string, + message: Record, + endpoint: string = "/v1/responses" +) { const specialSettings: NonNullable> = []; const setMessageContext = vi.fn(); const session = { @@ -28,8 +32,8 @@ function createSession(providerType: string, message: Record) { sessionId: "session-1", userAgent: "codex_cli_rs/1.0.0", clientIp: "127.0.0.1", - getEndpoint: () => "/v1/responses", - getManagedEndpoint: () => "/v1/responses", + getEndpoint: () => endpoint, + getManagedEndpoint: () => endpoint, getOriginalModel: () => "gpt-5", setOriginalModel: vi.fn(), getSpecialSettings: () => (specialSettings.length > 0 ? specialSettings : null), @@ -139,4 +143,123 @@ describe("ProxyMessageService Codex reasoning effort audit", () => { expect.objectContaining({ endpoint: "/v1/responses/compact" }) ); }); + + test("openai-compatible chat/completions 顶层 reasoning_effort 保存审计", async () => { + const { session, specialSettings } = createSession( + "openai-compatible", + { model: "gpt-5.5", messages: [], reasoning_effort: "high" }, + "/v1/chat/completions" + ); + + await ProxyMessageService.ensureContext(session); + + expect(specialSettings).toContainEqual({ + type: "openai_reasoning_effort", + scope: "request", + hit: true, + effort: "high", + source: "reasoning_effort", + }); + expect(createMessageRequestMock).toHaveBeenCalledWith( + expect.objectContaining({ special_settings: specialSettings }) + ); + }); + + test("openai-compatible chat/completions 嵌套 reasoning.effort 保存审计", async () => { + const { session, specialSettings } = createSession( + "openai-compatible", + { model: "gpt-5.5", messages: [], reasoning: { effort: "low" } }, + "/v1/chat/completions" + ); + + await ProxyMessageService.ensureContext(session); + + expect(specialSettings).toContainEqual({ + type: "openai_reasoning_effort", + scope: "request", + hit: true, + effort: "low", + source: "reasoning.effort", + }); + }); + + test("openai-compatible 顶层与嵌套不一致时以顶层为准", async () => { + const { session, specialSettings } = createSession( + "openai-compatible", + { + model: "gpt-5.5", + messages: [], + reasoning_effort: "high", + reasoning: { effort: "low" }, + }, + "/v1/chat/completions" + ); + + await ProxyMessageService.ensureContext(session); + + expect(specialSettings).toContainEqual( + expect.objectContaining({ effort: "high", source: "reasoning_effort" }) + ); + }); + + test("openai-compatible 非 chat/completions 端点不写入思考强度审计", async () => { + const { session, specialSettings } = createSession("openai-compatible", { + model: "gpt-5.5", + messages: [], + reasoning_effort: "high", + }); + + await ProxyMessageService.ensureContext(session); + + expect(specialSettings).toEqual([]); + }); + + test("openai-compatible chat/completions 尾斜杠变体仍保存审计", async () => { + const { session, specialSettings } = createSession( + "openai-compatible", + { model: "gpt-5.5", messages: [], reasoning_effort: "high" }, + "/v1/chat/completions/" + ); + + await ProxyMessageService.ensureContext(session); + + expect(specialSettings).toContainEqual({ + type: "openai_reasoning_effort", + scope: "request", + hit: true, + effort: "high", + source: "reasoning_effort", + }); + }); + + test("openai-compatible 请求缺少 effort 时不写入空审计", async () => { + const { session, specialSettings } = createSession( + "openai-compatible", + { model: "gpt-5.5", messages: [] }, + "/v1/chat/completions" + ); + + await ProxyMessageService.ensureContext(session); + + expect(specialSettings).toEqual([]); + }); + + test("复用已有 openai 思考强度审计,避免重复记录", async () => { + const { session, specialSettings } = createSession( + "openai-compatible", + { model: "gpt-5.5", messages: [], reasoning_effort: "high" }, + "/v1/chat/completions" + ); + specialSettings.push({ + type: "openai_reasoning_effort", + scope: "request", + hit: true, + effort: "high", + source: "reasoning_effort", + }); + + await ProxyMessageService.ensureContext(session); + + expect(specialSettings).toHaveLength(1); + }); }); diff --git a/src/app/v1/_lib/proxy/message-service.ts b/src/app/v1/_lib/proxy/message-service.ts index b891f902a..fb6907e38 100644 --- a/src/app/v1/_lib/proxy/message-service.ts +++ b/src/app/v1/_lib/proxy/message-service.ts @@ -1,5 +1,7 @@ +import { normalizeEndpointPath, V1_ENDPOINT_PATHS } from "@/app/v1/_lib/proxy/endpoint-paths"; import { extractAnthropicEffortFromRequestBody } from "@/lib/utils/anthropic-effort"; import { extractCodexReasoningEffortFromRequestBody } from "@/lib/utils/codex-reasoning-effort"; +import { extractOpenAIReasoningEffortFromRequestBody } from "@/lib/utils/openai-reasoning-effort"; import { createMessageRequest } from "@/repository/message"; import type { ProxySession } from "./session"; @@ -75,6 +77,30 @@ export class ProxyMessageService { } } + // openai-compatible 供应商的 chat/completions 请求:解析并记录思考强度审计。 + // 兼容顶层 reasoning_effort(OpenAI 官方及多数 openai-compatible 供应商)与嵌套 + // reasoning.effort(OpenRouter / Ollama / Vercel AI Gateway 等),见 openai-reasoning-effort。 + const hasOpenAIReasoningEffortAudit = session + .getSpecialSettings() + ?.some((setting) => setting.type === "openai_reasoning_effort"); + + if ( + provider.providerType === "openai-compatible" && + normalizeEndpointPath(endpoint ?? "") === V1_ENDPOINT_PATHS.CHAT_COMPLETIONS && + !hasOpenAIReasoningEffortAudit + ) { + const extraction = extractOpenAIReasoningEffortFromRequestBody(session.request.message); + if (extraction) { + session.addSpecialSetting({ + type: "openai_reasoning_effort", + scope: "request", + hit: true, + effort: extraction.effort, + source: extraction.source, + }); + } + } + const messageRequest = await createMessageRequest({ provider_id: provider.id, user_id: authState.user.id, diff --git a/src/lib/utils/openai-reasoning-effort.ts b/src/lib/utils/openai-reasoning-effort.ts new file mode 100644 index 000000000..82e83cbdd --- /dev/null +++ b/src/lib/utils/openai-reasoning-effort.ts @@ -0,0 +1,84 @@ +import type { OpenAIReasoningEffortFieldSource, SpecialSetting } from "@/types/special-settings"; + +/** 从 OpenAI Chat Completions 请求体解析出的思考强度信息。 */ +export interface OpenAIReasoningEffortExtraction { + effort: string; + source: OpenAIReasoningEffortFieldSource; +} + +/** + * 过滤非字符串及空白值,避免把无效参数写入审计记录。 + * 校验用裁剪后值,但返回原始字符串,保证审计记录如实反映客户端发送的字段值。 + */ +function normalizeOpenAIReasoningEffort(value: unknown): string | null { + if (typeof value !== "string") { + return null; + } + + return value.trim().length > 0 ? value : null; +} + +/** + * 从 OpenAI Chat Completions 请求体解析思考强度。 + * + * 兼容两种载体(外部兼容性调研结论): + * - 顶层标量 `reasoning_effort`:OpenAI 官方 Chat Completions 参数,且是 + * DeepSeek / xAI / Mistral / Groq / Gemini(OAI-compat) / DeepInfra / OpenRouter / + * LiteLLM 等大多数 openai-compatible 供应商接受的 de-facto 标准 → 优先读取。 + * - 嵌套对象 `reasoning.effort`:OpenRouter / Ollama / Vercel AI Gateway 等 + * 在 chat/completions 端点也接受该 Responses 风格载体 → 兜底读取。 + * + * 两者同时存在且不一致时以顶层为准:与 OpenRouter 官方声明「reasoning_effort 是 + * reasoning.effort 的简写、二者不可冲突」的语义一致,避免嵌套值覆盖顶层声明。 + * 审计层仅做非空校验、原样记录,不做取值白名单(各供应商取值域不同,白名单会丢真实值)。 + */ +export function extractOpenAIReasoningEffortFromRequestBody( + requestBody: unknown +): OpenAIReasoningEffortExtraction | null { + if (!requestBody || typeof requestBody !== "object" || Array.isArray(requestBody)) { + return null; + } + + const record = requestBody as Record; + + const topLevel = normalizeOpenAIReasoningEffort(record.reasoning_effort); + if (topLevel) { + return { effort: topLevel, source: "reasoning_effort" }; + } + + const reasoning = record.reasoning; + if (!reasoning || typeof reasoning !== "object" || Array.isArray(reasoning)) { + return null; + } + + const nested = normalizeOpenAIReasoningEffort("effort" in reasoning ? reasoning.effort : null); + if (!nested) { + return null; + } + + return { effort: nested, source: "reasoning.effort" }; +} + +/** 从使用记录审计中读取 OpenAI Chat Completions 的思考强度。 */ +export function extractOpenAIReasoningEffortFromSpecialSettings( + specialSettings: SpecialSetting[] | null | undefined +): OpenAIReasoningEffortExtraction | null { + if (!Array.isArray(specialSettings)) { + return null; + } + + for (const setting of specialSettings) { + if (setting.type !== "openai_reasoning_effort") { + continue; + } + if (typeof setting.effort !== "string" || setting.effort.trim().length === 0) { + continue; + } + return { + effort: setting.effort, + source: setting.source, + }; + } + + return null; +} diff --git a/src/lib/utils/special-settings.ts b/src/lib/utils/special-settings.ts index 61a5e659f..8aef096c9 100644 --- a/src/lib/utils/special-settings.ts +++ b/src/lib/utils/special-settings.ts @@ -54,6 +54,8 @@ function buildSettingKey(setting: SpecialSetting): string { return JSON.stringify([setting.type, setting.hit, setting.effort]); case "codex_reasoning_effort": return JSON.stringify([setting.type, setting.hit, setting.effort]); + case "openai_reasoning_effort": + return JSON.stringify([setting.type, setting.hit, setting.effort, setting.source]); case "anthropic_cache_ttl_header_override": return JSON.stringify([setting.type, setting.ttl]); case "anthropic_context_1m_header_override": diff --git a/src/lib/utils/thinking-effort.ts b/src/lib/utils/thinking-effort.ts index aad4d3279..68e2b998e 100644 --- a/src/lib/utils/thinking-effort.ts +++ b/src/lib/utils/thinking-effort.ts @@ -1,9 +1,10 @@ import { extractAnthropicEffortInfo } from "@/lib/utils/anthropic-effort"; import { extractCodexReasoningEffortInfo } from "@/lib/utils/codex-reasoning-effort"; +import { extractOpenAIReasoningEffortFromSpecialSettings } from "@/lib/utils/openai-reasoning-effort"; import type { SpecialSetting } from "@/types/special-settings"; -/** 思考强度审计来源:Codex 的 reasoning.effort 或 Anthropic 的 output_config.effort。 */ -export type ThinkingEffortSource = "codex" | "anthropic"; +/** 思考强度审计来源:Codex 的 reasoning.effort、OpenAI chat/completions 或 Anthropic 的 output_config.effort。 */ +export type ThinkingEffortSource = "codex" | "openai" | "anthropic"; /** 任意模型统一后的思考强度展示信息,供列表列与请求详情共用。 */ export interface ThinkingEffortInfo { @@ -18,8 +19,9 @@ export interface ThinkingEffortInfo { /** * 从 specialSettings 中提取任意模型的思考强度。 * - * 复用 Codex 与 Anthropic 两个提取器并统一返回结构:Codex 审计优先, - * 其次回退到 Anthropic effort,两者都无则返回 null。 + * 复用 Codex、OpenAI chat/completions 与 Anthropic 三个提取器并统一返回结构: + * Codex 审计优先,其次 OpenAI,最后回退到 Anthropic effort,三者都无则返回 null。 + * OpenAI chat/completions 目前无供应商级覆写机制,isOverridden 恒为 false。 */ export function extractThinkingEffortInfo( specialSettings: SpecialSetting[] | null | undefined @@ -34,6 +36,16 @@ export function extractThinkingEffortInfo( }; } + const openaiInfo = extractOpenAIReasoningEffortFromSpecialSettings(specialSettings); + if (openaiInfo) { + return { + source: "openai", + requestedEffort: openaiInfo.effort, + effectiveEffort: openaiInfo.effort, + isOverridden: false, + }; + } + const anthropicInfo = extractAnthropicEffortInfo(specialSettings); if (anthropicInfo) { return { diff --git a/src/types/special-settings.ts b/src/types/special-settings.ts index 1b14f417f..0ad2b5940 100644 --- a/src/types/special-settings.ts +++ b/src/types/special-settings.ts @@ -17,6 +17,7 @@ export type SpecialSetting = | ClaudeMetadataUserIdInjectionSpecialSetting | AnthropicEffortSpecialSetting | CodexReasoningEffortSpecialSetting + | OpenAIReasoningEffortSpecialSetting | AnthropicCacheTtlHeaderOverrideSpecialSetting | AnthropicContext1mHeaderOverrideSpecialSetting | LongContextPricingSpecialSetting @@ -103,6 +104,25 @@ export type CodexReasoningEffortSpecialSetting = { effort: string; }; +/** OpenAI Chat Completions 请求体中思考强度(effort)的载体字段。 */ +export type OpenAIReasoningEffortFieldSource = "reasoning_effort" | "reasoning.effort"; + +/** + * OpenAI Chat Completions reasoning effort 请求参数审计 + * + * 记录 openai-compatible 供应商的 /v1/chat/completions 请求中客户端声明的思考强度; + * 兼容顶层 reasoning_effort 与嵌套 reasoning.effort 两种载体(source 标注来源字段), + * 便于排查客户端实际用哪种字段表达思考等级。 + */ +export type OpenAIReasoningEffortSpecialSetting = { + type: "openai_reasoning_effort"; + scope: "request"; + hit: boolean; + effort: string; + /** 请求体中的载体字段:顶层 reasoning_effort 或嵌套 reasoning.effort。 */ + source: OpenAIReasoningEffortFieldSource; +}; + /** * Anthropic 缓存 TTL 相关标头覆写审计 * diff --git a/tests/unit/lib/utils/openai-reasoning-effort.test.ts b/tests/unit/lib/utils/openai-reasoning-effort.test.ts new file mode 100644 index 000000000..8a1d46d04 --- /dev/null +++ b/tests/unit/lib/utils/openai-reasoning-effort.test.ts @@ -0,0 +1,132 @@ +import { describe, expect, test } from "vitest"; +import { + extractOpenAIReasoningEffortFromRequestBody, + extractOpenAIReasoningEffortFromSpecialSettings, +} from "@/lib/utils/openai-reasoning-effort"; +import type { SpecialSetting } from "@/types/special-settings"; + +describe("extractOpenAIReasoningEffortFromRequestBody", () => { + test("优先读取顶层 reasoning_effort", () => { + const result = extractOpenAIReasoningEffortFromRequestBody({ + model: "gpt-5.5", + messages: [], + reasoning_effort: "high", + }); + + expect(result).toEqual({ effort: "high", source: "reasoning_effort" }); + }); + + test("顶层缺失时兜底读取嵌套 reasoning.effort", () => { + const result = extractOpenAIReasoningEffortFromRequestBody({ + model: "gpt-5.5", + messages: [], + reasoning: { effort: "low" }, + }); + + expect(result).toEqual({ effort: "low", source: "reasoning.effort" }); + }); + + test("顶层与嵌套不一致时以顶层为准", () => { + const result = extractOpenAIReasoningEffortFromRequestBody({ + reasoning_effort: "max", + reasoning: { effort: "minimal" }, + }); + + expect(result).toEqual({ effort: "max", source: "reasoning_effort" }); + }); + + test("顶层与嵌套一致时读取顶层", () => { + const result = extractOpenAIReasoningEffortFromRequestBody({ + reasoning_effort: "medium", + reasoning: { effort: "medium" }, + }); + + expect(result).toEqual({ effort: "medium", source: "reasoning_effort" }); + }); + + test("顶层为空白时回退嵌套值", () => { + const result = extractOpenAIReasoningEffortFromRequestBody({ + reasoning_effort: " ", + reasoning: { effort: "xhigh" }, + }); + + expect(result).toEqual({ effort: "xhigh", source: "reasoning.effort" }); + }); + + test("保留非空值的前后空白(原样记录审计值)", () => { + const result = extractOpenAIReasoningEffortFromRequestBody({ + reasoning_effort: " high ", + }); + + expect(result).toEqual({ effort: " high ", source: "reasoning_effort" }); + }); + + test("顶层为非法类型时回退嵌套值", () => { + const result = extractOpenAIReasoningEffortFromRequestBody({ + reasoning_effort: 42, + reasoning: { effort: "high" }, + }); + + expect(result).toEqual({ effort: "high", source: "reasoning.effort" }); + }); + + test("两者均缺失时返回 null", () => { + expect( + extractOpenAIReasoningEffortFromRequestBody({ model: "gpt-5.5", messages: [] }) + ).toBeNull(); + }); + + test("reasoning 非对象时返回 null", () => { + expect(extractOpenAIReasoningEffortFromRequestBody({ reasoning: "not-an-object" })).toBeNull(); + }); + + test("非对象请求体返回 null", () => { + expect(extractOpenAIReasoningEffortFromRequestBody(null)).toBeNull(); + expect(extractOpenAIReasoningEffortFromRequestBody("string")).toBeNull(); + expect(extractOpenAIReasoningEffortFromRequestBody([1, 2])).toBeNull(); + }); + + test("保留实际取值域(不做白名单过滤)", () => { + const result = extractOpenAIReasoningEffortFromRequestBody({ reasoning_effort: "max" }); + + expect(result?.effort).toBe("max"); + }); +}); + +describe("extractOpenAIReasoningEffortFromSpecialSettings", () => { + test("读取首个有效的 openai 思考强度审计", () => { + const settings: SpecialSetting[] = [ + { + type: "openai_reasoning_effort", + scope: "request", + hit: true, + effort: "high", + source: "reasoning_effort", + }, + ]; + + expect(extractOpenAIReasoningEffortFromSpecialSettings(settings)).toEqual({ + effort: "high", + source: "reasoning_effort", + }); + }); + + test("忽略其它类型审计", () => { + const settings: SpecialSetting[] = [ + { + type: "codex_reasoning_effort", + scope: "request", + hit: true, + effort: "high", + }, + ]; + + expect(extractOpenAIReasoningEffortFromSpecialSettings(settings)).toBeNull(); + }); + + test("缺少审计数组时返回 null", () => { + expect(extractOpenAIReasoningEffortFromSpecialSettings(undefined)).toBeNull(); + expect(extractOpenAIReasoningEffortFromSpecialSettings(null)).toBeNull(); + expect(extractOpenAIReasoningEffortFromSpecialSettings([])).toBeNull(); + }); +});