mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-04 06:12:10 +03:00
fix(reasoning): preserve chat effort and protocol labels (#1550)
This commit is contained in:
@@ -2,6 +2,10 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- **fix(providers):** Preserve OpenAI Chat Completions `reasoning_effort` through assistant-prefill requests and label OpenAI request protocols explicitly as `OpenAI-Chat` or `OpenAI-Responses`.
|
||||
|
||||
---
|
||||
|
||||
## [3.7.0] — 2026-04-23
|
||||
|
||||
@@ -86,17 +86,6 @@ function buildAnthropicCompatibleUrl(baseUrl) {
|
||||
// contain max_tokens or Claude model names.
|
||||
export function detectFormatFromEndpoint(body, endpointPath = "") {
|
||||
const path = String(endpointPath || "");
|
||||
const hasInputField =
|
||||
body &&
|
||||
typeof body === "object" &&
|
||||
Object.prototype.hasOwnProperty.call(body, "input") &&
|
||||
body.input !== undefined;
|
||||
const hasResponsesSpecificFields =
|
||||
body &&
|
||||
typeof body === "object" &&
|
||||
(body.max_output_tokens !== undefined ||
|
||||
body.previous_response_id !== undefined ||
|
||||
body.reasoning !== undefined);
|
||||
|
||||
if (/\/responses(?=\/|$)/i.test(path) || /^responses(?=\/|$)/i.test(path)) {
|
||||
return "openai-responses";
|
||||
@@ -110,9 +99,6 @@ export function detectFormatFromEndpoint(body, endpointPath = "") {
|
||||
/\/(?:chat\/completions|completions)(?=\/|$)/i.test(path) ||
|
||||
/^(?:chat\/completions|completions)(?=\/|$)/i.test(path)
|
||||
) {
|
||||
if (hasInputField || hasResponsesSpecificFields) {
|
||||
return "openai-responses";
|
||||
}
|
||||
return "openai";
|
||||
}
|
||||
|
||||
@@ -400,11 +386,10 @@ export function hasThinkingConfig(body) {
|
||||
}
|
||||
|
||||
// Normalize thinking config based on last message role
|
||||
// - If lastMessage is not user → remove thinking config
|
||||
// - If lastMessage is user AND has thinking config → keep it (force enable)
|
||||
// - If lastMessage is not user → remove Claude/Gemini-style thinking config
|
||||
// - Keep OpenAI Chat Completions reasoning_effort as a request-level option.
|
||||
export function normalizeThinkingConfig(body) {
|
||||
if (!isLastMessageFromUser(body)) {
|
||||
delete body.reasoning_effort;
|
||||
delete body.thinking;
|
||||
}
|
||||
return body;
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import {
|
||||
PROTOCOL_COLORS,
|
||||
PROVIDER_COLORS,
|
||||
getHttpStatusStyle as getStatusStyle,
|
||||
getProtocolColor,
|
||||
} from "@/shared/constants/colors";
|
||||
import { formatDuration, formatApiKeyLabel } from "@/shared/utils/formatting";
|
||||
|
||||
@@ -57,12 +57,7 @@ export default function RequestLoggerDetail({ log, detail, loading, onClose, onC
|
||||
|
||||
const statusStyle = getStatusStyle(log.status);
|
||||
const protocolKey = log.sourceFormat || log.provider;
|
||||
const protocol = PROTOCOL_COLORS[protocolKey] ||
|
||||
PROTOCOL_COLORS[log.provider] || {
|
||||
bg: "#6B7280",
|
||||
text: "#fff",
|
||||
label: (protocolKey || log.provider || "-").toUpperCase(),
|
||||
};
|
||||
const protocol = getProtocolColor(protocolKey, log.provider);
|
||||
const providerColor = PROVIDER_COLORS[log.provider] || {
|
||||
bg: "#374151",
|
||||
text: "#fff",
|
||||
|
||||
@@ -6,9 +6,9 @@ import Card from "./Card";
|
||||
import RequestLoggerDetail from "./RequestLoggerDetail";
|
||||
import { copyToClipboard } from "@/shared/utils/clipboard";
|
||||
import {
|
||||
PROTOCOL_COLORS,
|
||||
PROVIDER_COLORS,
|
||||
getHttpStatusStyle as getStatusStyle,
|
||||
getProtocolColor,
|
||||
} from "@/shared/constants/colors";
|
||||
import {
|
||||
formatTime,
|
||||
@@ -706,12 +706,7 @@ export default function RequestLoggerV2() {
|
||||
{sortedLogs.map((log) => {
|
||||
const statusStyle = getStatusStyle(log.status);
|
||||
const protocolKey = log.sourceFormat || log.provider;
|
||||
const protocol = PROTOCOL_COLORS[protocolKey] ||
|
||||
PROTOCOL_COLORS[log.provider] || {
|
||||
bg: "#6B7280",
|
||||
text: "#fff",
|
||||
label: (protocolKey || log.provider || "-").toUpperCase(),
|
||||
};
|
||||
const protocol = getProtocolColor(protocolKey, log.provider);
|
||||
const compatLabel = getProviderDisplayLabel(log.provider, providerNodes);
|
||||
const providerColor = PROVIDER_COLORS[log.provider] || {
|
||||
bg: "#374151",
|
||||
|
||||
@@ -38,6 +38,15 @@ export const PROTOCOL_COLORS = {
|
||||
bypass: { bg: "#6B7280", text: "#fff", label: "Bypass" },
|
||||
};
|
||||
|
||||
const PROTOCOL_KEY_ALIASES = {
|
||||
"openai-chat": "openai",
|
||||
"openai-response": "openai-responses",
|
||||
};
|
||||
|
||||
function normalizeProtocolKey(protocol) {
|
||||
return PROTOCOL_KEY_ALIASES[protocol] || protocol;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════
|
||||
// Proxy Type Colors (ProxyLogger)
|
||||
// ═══════════════════════════════════════════
|
||||
@@ -134,3 +143,21 @@ export function getProviderColor(provider) {
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default fallback for a protocol color lookup.
|
||||
* @param {string} protocol - Protocol key
|
||||
* @param {string} fallbackProvider - Provider key to use as a secondary protocol key
|
||||
* @returns {{ bg: string, text: string, label: string }}
|
||||
*/
|
||||
export function getProtocolColor(protocol, fallbackProvider) {
|
||||
const normalized = normalizeProtocolKey(protocol);
|
||||
return (
|
||||
PROTOCOL_COLORS[normalized] ||
|
||||
PROTOCOL_COLORS[fallbackProvider] || {
|
||||
bg: "#6B7280",
|
||||
text: "#fff",
|
||||
label: (protocol || fallbackProvider || "-").toUpperCase(),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -797,6 +797,55 @@ test("chatCore strips unsupported reasoning params and caps provider token field
|
||||
assert.equal(call.body.max_completion_tokens, 16384);
|
||||
});
|
||||
|
||||
test("chatCore preserves reasoning_effort for assistant-prefill OpenAI-compatible requests", async () => {
|
||||
const { call, result } = await invokeChatCore({
|
||||
provider: "openai-compatible-aio",
|
||||
model: "glm-5.1",
|
||||
endpoint: "/v1/chat/completions",
|
||||
body: {
|
||||
model: "aio/glm-5.1",
|
||||
messages: [
|
||||
{ role: "user", content: "draft the answer" },
|
||||
{ role: "assistant", content: "<thinking>" },
|
||||
],
|
||||
reasoning_effort: "xhigh",
|
||||
stream: true,
|
||||
},
|
||||
responseFormat: "openai",
|
||||
});
|
||||
|
||||
assert.equal(result.success, true);
|
||||
assert.equal(call.body.model, "glm-5.1");
|
||||
assert.equal(call.body.reasoning_effort, "xhigh");
|
||||
});
|
||||
|
||||
test("chatCore logs chat completions endpoint as OpenAI protocol", async () => {
|
||||
const { call, result } = await invokeChatCore({
|
||||
provider: "openrouter",
|
||||
model: "deepseek/deepseek-v4-pro",
|
||||
endpoint: "/v1/chat/completions",
|
||||
body: {
|
||||
model: "openrouter/deepseek/deepseek-v4-pro",
|
||||
messages: [{ role: "user", content: "Human: Hi" }],
|
||||
temperature: 1,
|
||||
max_tokens: 64000,
|
||||
stream: false,
|
||||
presence_penalty: 0,
|
||||
frequency_penalty: 0,
|
||||
top_p: 0.9,
|
||||
},
|
||||
responseFormat: "openai",
|
||||
});
|
||||
|
||||
assert.equal(result.success, true);
|
||||
assert.equal(call.body.model, "deepseek/deepseek-v4-pro");
|
||||
|
||||
const logEntry = await waitFor(getLatestCallLog);
|
||||
assert.ok(logEntry, "expected call log to be persisted");
|
||||
assert.equal(logEntry.path, "/v1/chat/completions");
|
||||
assert.equal(logEntry.sourceFormat, FORMATS.OPENAI);
|
||||
});
|
||||
|
||||
test("chatCore surfaces translation errors with explicit status codes", async () => {
|
||||
register(
|
||||
FORMATS.OPENAI_RESPONSES,
|
||||
|
||||
@@ -402,12 +402,13 @@ test("detectFormat identifies OpenAI Responses by max_output_tokens without inpu
|
||||
assert.equal(format, FORMATS.OPENAI_RESPONSES);
|
||||
});
|
||||
|
||||
test("detectFormatFromEndpoint forces OpenAI for /v1/chat/completions", () => {
|
||||
test("detectFormatFromEndpoint uses chat completions endpoint for OpenAI chat protocol", () => {
|
||||
const format = detectFormatFromEndpoint(
|
||||
{
|
||||
model: "cc/claude-opus-4-6",
|
||||
model: "test-model",
|
||||
messages: [{ role: "user", content: "hi" }],
|
||||
max_tokens: 16,
|
||||
input: "ignored for endpoint protocol detection",
|
||||
max_output_tokens: 16,
|
||||
stream: false,
|
||||
},
|
||||
"/v1/chat/completions"
|
||||
|
||||
@@ -114,7 +114,7 @@ test("Unknown providers fall back to bearer auth and OpenAI format", () => {
|
||||
assert.equal(getTargetFormat("custom-provider"), "openai");
|
||||
});
|
||||
|
||||
test("thinking config is removed when the last message is not from the user", () => {
|
||||
test("native thinking config is removed when the last message is not from the user", () => {
|
||||
const assistantLast = {
|
||||
messages: [
|
||||
{ role: "user", content: "hi" },
|
||||
@@ -134,7 +134,7 @@ test("thinking config is removed when the last message is not from the user", ()
|
||||
assert.equal(isLastMessageFromUser({ messages: [] }), true);
|
||||
assert.equal(isLastMessageFromUser(assistantLast), false);
|
||||
assert.equal(hasThinkingConfig(userLast), true);
|
||||
assert.equal("reasoning_effort" in normalized, false);
|
||||
assert.equal(normalized.reasoning_effort, "high");
|
||||
assert.equal("thinking" in normalized, false);
|
||||
assert.equal(normalizeThinkingConfig(userLast).reasoning_effort, "medium");
|
||||
});
|
||||
|
||||
@@ -6,6 +6,46 @@ import { renderToStaticMarkup } from "react-dom/server";
|
||||
const { default: RequestLoggerDetail } =
|
||||
await import("../../src/shared/components/RequestLoggerDetail.tsx");
|
||||
|
||||
function renderDetailWithSourceFormat(sourceFormat: string) {
|
||||
return renderToStaticMarkup(
|
||||
React.createElement(RequestLoggerDetail, {
|
||||
log: {
|
||||
status: 200,
|
||||
method: "POST",
|
||||
path: "/v1/chat/completions",
|
||||
timestamp: "2026-04-09T21:27:08.000Z",
|
||||
duration: 2500,
|
||||
provider: "openrouter",
|
||||
sourceFormat,
|
||||
model: "deepseek/deepseek-v4-pro",
|
||||
requestedModel: "openrouter/deepseek/deepseek-v4-pro",
|
||||
cacheSource: "upstream",
|
||||
tokens: {
|
||||
in: 10,
|
||||
out: 2,
|
||||
cacheRead: null,
|
||||
cacheWrite: null,
|
||||
reasoning: null,
|
||||
},
|
||||
},
|
||||
detail: {
|
||||
requestedModel: "openrouter/deepseek/deepseek-v4-pro",
|
||||
cacheSource: "upstream",
|
||||
tokens: {
|
||||
in: 10,
|
||||
out: 2,
|
||||
cacheRead: null,
|
||||
cacheWrite: null,
|
||||
reasoning: null,
|
||||
},
|
||||
},
|
||||
loading: false,
|
||||
onClose: () => {},
|
||||
onCopy: async () => true,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
test("request log detail splits token badges into input and output groups", () => {
|
||||
const html = renderToStaticMarkup(
|
||||
React.createElement(RequestLoggerDetail, {
|
||||
@@ -70,6 +110,7 @@ test("request log detail splits token badges into input and output groups", () =
|
||||
assert.equal(modelLabelIndex < requestedModelLabelIndex, true);
|
||||
assert.notEqual(html.indexOf(">Cache Source<"), -1);
|
||||
assert.notEqual(html.indexOf(">Semantic (OmniRoute)<"), -1);
|
||||
assert.notEqual(html.indexOf(">OpenAI-Chat<"), -1);
|
||||
|
||||
assert.match(
|
||||
html,
|
||||
@@ -77,3 +118,11 @@ test("request log detail splits token badges into input and output groups", () =
|
||||
);
|
||||
assert.match(html, /data-testid="token-group-output"[\s\S]*Total Out: 42[\s\S]*Reasoning: N\/A/);
|
||||
});
|
||||
|
||||
test("request log detail labels OpenAI protocol variants explicitly", () => {
|
||||
const chatHtml = renderDetailWithSourceFormat("openai");
|
||||
const responsesHtml = renderDetailWithSourceFormat("openai-responses");
|
||||
|
||||
assert.notEqual(chatHtml.indexOf(">OpenAI-Chat<"), -1);
|
||||
assert.notEqual(responsesHtml.indexOf(">OpenAI-Responses<"), -1);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user