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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions messages/en/dashboard.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
5 changes: 5 additions & 0 deletions messages/ja/dashboard.json
Original file line number Diff line number Diff line change
Expand Up @@ -551,6 +551,11 @@
"overridden": "プロバイダーにより上書き",
"tooltip": "Codex リクエストの推論強度 (reasoning.effort) をそのまま表示します。"
},
"reasoningEffortOpenai": {
"label": "推論強度",
"overridden": "プロバイダーにより上書き",
"tooltip": "OpenAI chat/completions リクエストの推論強度 (reasoning_effort / reasoning.effort) をそのまま表示します。"
},
"logicTrace": {
"title": "決定チェーン",
"singleRouteSelectionTitle": "単一経路保護での Provider 選択",
Expand Down
5 changes: 5 additions & 0 deletions messages/ru/dashboard.json
Original file line number Diff line number Diff line change
Expand Up @@ -551,6 +551,11 @@
"overridden": "Переопределено провайдером",
"tooltip": "Интенсивность рассуждений в запросе Codex (reasoning.effort), показанная без изменений."
},
"reasoningEffortOpenai": {
"label": "Интенсивность рассуждений",
"overridden": "Переопределено провайдером",
"tooltip": "Интенсивность рассуждений в запросе OpenAI chat/completions (reasoning_effort / reasoning.effort), показанная без изменений."
},
"logicTrace": {
"title": "Цепочка решений",
"singleRouteSelectionTitle": "Выбор провайдера в режиме защиты одним маршрутом",
Expand Down
5 changes: 5 additions & 0 deletions messages/zh-CN/dashboard.json
Original file line number Diff line number Diff line change
Expand Up @@ -551,6 +551,11 @@
"overridden": "已被供应商覆写",
"tooltip": "Codex 请求中的思考强度(reasoning.effort),按原值显示。"
},
"reasoningEffortOpenai": {
"label": "思考强度",
"overridden": "已被供应商覆写",
"tooltip": "OpenAI chat/completions 请求中的思考强度(reasoning_effort / reasoning.effort),按原值显示。"
},
"logicTrace": {
"title": "决策链",
"singleRouteSelectionTitle": "单路保护下的供应商选择",
Expand Down
5 changes: 5 additions & 0 deletions messages/zh-TW/dashboard.json
Original file line number Diff line number Diff line change
Expand Up @@ -551,6 +551,11 @@
"overridden": "已被供應商覆寫",
"tooltip": "Codex 請求中的思考強度(reasoning.effort),按原值顯示。"
},
"reasoningEffortOpenai": {
"label": "思考強度",
"overridden": "已被供應商覆寫",
"tooltip": "OpenAI chat/completions 請求中的思考強度(reasoning_effort / reasoning.effort),按原值顯示。"
},
"logicTrace": {
"title": "決策鏈",
"singleRouteSelectionTitle": "單路保護下的供應商選擇",
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -180,4 +180,51 @@ describe("ThinkingEffortDisplay", () => {
expect(html).toContain("reasoningEffort.tooltip");
expect(html).not.toContain(">medium<");
});

test("显示 OpenAI chat/completions 请求中的思考强度", () => {
const html = renderToStaticMarkup(
<ThinkingEffortDisplay
specialSettings={[
{
type: "openai_reasoning_effort",
scope: "request",
hit: true,
effort: "max",
source: "reasoning_effort",
},
]}
/>
);

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(
<ThinkingEffortDisplay
specialSettings={[
{
type: "openai_reasoning_effort",
scope: "request",
hit: true,
effort: "max",
source: "reasoning_effort",
},
{
type: "codex_reasoning_effort",
scope: "request",
hit: true,
effort: "high",
},
]}
/>
);

expect(html).toContain("high");
expect(html).toContain("reasoningEffort.tooltip");
expect(html).not.toContain(">max<");
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ interface ThinkingEffortDisplayProps {
}

/**
* 在使用记录中展示任意模型的思考强度(Codex reasoning.effort 或 Anthropic effort)。
* 在使用记录中展示任意模型的思考强度(Codex / OpenAI chat/completions 的
* reasoning.effort,或 Anthropic 的 output_config.effort)。
*
* 供应商改变强度时同时展示请求值和实际转发值,避免只看到客户端参数而误判上游行为。
*/
Expand All @@ -26,7 +27,12 @@ export function ThinkingEffortDisplay({ specialSettings }: ThinkingEffortDisplay
return <span className="text-muted-foreground">-</span>;
}

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 (
Expand Down
129 changes: 126 additions & 3 deletions src/app/v1/_lib/proxy/message-service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@ vi.mock("@/repository/message", () => ({

import { ProxyMessageService } from "./message-service";

function createSession(providerType: string, message: Record<string, unknown>) {
function createSession(
providerType: string,
message: Record<string, unknown>,
endpoint: string = "/v1/responses"
) {
const specialSettings: NonNullable<ReturnType<ProxySession["getSpecialSettings"]>> = [];
const setMessageContext = vi.fn();
const session = {
Expand All @@ -28,8 +32,8 @@ function createSession(providerType: string, message: Record<string, unknown>) {
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),
Expand Down Expand Up @@ -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);
});
});
26 changes: 26 additions & 0 deletions src/app/v1/_lib/proxy/message-service.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand Down Expand Up @@ -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
Comment on lines +88 to +90

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Raw path skips effort audit

When a valid Chat Completions request uses a normalized route variant such as /v1/chat/completions/, the raw managed pathname fails this exact comparison even though endpoint classification recognizes the route, causing the reasoning-effort audit and dashboard value to be omitted.

Knowledge Base Used: Proxy request pipeline

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/v1/_lib/proxy/message-service.ts
Line: 87-89

Comment:
**Raw path skips effort audit**

When a valid Chat Completions request uses a normalized route variant such as `/v1/chat/completions/`, the raw managed pathname fails this exact comparison even though endpoint classification recognizes the route, causing the reasoning-effort audit and dashboard value to be omitted.

**Knowledge Base Used:** [Proxy request pipeline](https://app.greptile.com/ygxz/-/custom-context/knowledge-base/ding113/claude-code-hub/-/docs/proxy-pipeline.md)

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

) {
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,
Expand Down
Loading