mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
fix(translator): use non-empty reasoning_content placeholder on cache miss instead of empty string (#3433)
Integrated into release/v3.8.17
This commit is contained in:
@@ -294,7 +294,6 @@ export function translateRequest(
|
||||
thinkingEnabled: hasThinkingConfig(result),
|
||||
supportsReasoning: supportsReasoning({ provider: normalizedProvider, model: normalizedModel }),
|
||||
interleavedField: resolvedCapabilities?.interleavedField ?? null,
|
||||
allowLegacyFallback: false,
|
||||
});
|
||||
if (isReasoner && result.messages && Array.isArray(result.messages)) {
|
||||
const canReplayReasoningOnly = isDeepSeekReplayTarget(normalizedProvider, normalizedModel);
|
||||
@@ -370,9 +369,14 @@ export function translateRequest(
|
||||
}
|
||||
}
|
||||
|
||||
// Legacy fallback — empty string (works for older DeepSeek versions)
|
||||
if (hasToolCalls && msg.reasoning_content === undefined) {
|
||||
msg.reasoning_content = "";
|
||||
// Cache miss fallback — use a non-empty placeholder.
|
||||
// Empty string causes DeepSeek V4+ to reject with 400:
|
||||
// "reasoning_content in the thinking mode must be passed back to the API."
|
||||
// Note: injectEmptyReasoningContentForToolCalls may have pre-set
|
||||
// reasoning_content="" before the cache lookup, so we check for
|
||||
// both undefined AND empty string here.
|
||||
if (hasToolCalls && !msg.reasoning_content) {
|
||||
msg.reasoning_content = NON_ANTHROPIC_THINKING_PLACEHOLDER;
|
||||
}
|
||||
}
|
||||
} else if (
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
getAutoUpdateConfig,
|
||||
launchAutoUpdate,
|
||||
validateAutoUpdateRuntime,
|
||||
PROJECT_ROOT,
|
||||
} from "@/lib/system/autoUpdate";
|
||||
import { NEWS_JSON_URL, parseActiveNewsPayload } from "@/shared/utils/releaseNotes";
|
||||
|
||||
@@ -169,7 +170,7 @@ export async function POST(req: NextRequest) {
|
||||
});
|
||||
await execFileAsync("git", ["fetch", "--tags", config.gitRemote], {
|
||||
timeout: 60_000,
|
||||
cwd: process.cwd(),
|
||||
cwd: PROJECT_ROOT,
|
||||
});
|
||||
send({ step: "install", status: "done", message: "Tags fetched" });
|
||||
|
||||
@@ -178,7 +179,7 @@ export async function POST(req: NextRequest) {
|
||||
status: "running",
|
||||
message: `Validating ${resolvedTargetTag}...`,
|
||||
});
|
||||
await ensureGitTagExists(resolvedTargetTag, execFileAsync, process.cwd());
|
||||
await ensureGitTagExists(resolvedTargetTag, execFileAsync, PROJECT_ROOT);
|
||||
send({
|
||||
step: "install",
|
||||
status: "done",
|
||||
@@ -193,7 +194,7 @@ export async function POST(req: NextRequest) {
|
||||
try {
|
||||
await execFileAsync("git", ["stash", "--include-untracked"], {
|
||||
timeout: 30_000,
|
||||
cwd: process.cwd(),
|
||||
cwd: PROJECT_ROOT,
|
||||
});
|
||||
} catch {
|
||||
// No local changes to stash.
|
||||
@@ -202,7 +203,7 @@ export async function POST(req: NextRequest) {
|
||||
const shortHead = (
|
||||
await execFileAsync("git", ["rev-parse", "--short", "HEAD"], {
|
||||
timeout: 10_000,
|
||||
cwd: process.cwd(),
|
||||
cwd: PROJECT_ROOT,
|
||||
})
|
||||
).stdout.trim();
|
||||
const backupBranch = `pre-update/${shortHead}-${new Date().toISOString().replace(/[:.]/g, "-")}`;
|
||||
@@ -210,7 +211,7 @@ export async function POST(req: NextRequest) {
|
||||
try {
|
||||
await execFileAsync("git", ["branch", backupBranch], {
|
||||
timeout: 10_000,
|
||||
cwd: process.cwd(),
|
||||
cwd: PROJECT_ROOT,
|
||||
});
|
||||
} catch {
|
||||
// Backup branch is best-effort only.
|
||||
@@ -218,7 +219,7 @@ export async function POST(req: NextRequest) {
|
||||
|
||||
await execFileAsync("git", ["checkout", resolvedTargetTag], {
|
||||
timeout: 30_000,
|
||||
cwd: process.cwd(),
|
||||
cwd: PROJECT_ROOT,
|
||||
});
|
||||
send({ step: "install", status: "done", message: `Checked out ${resolvedTargetTag}` });
|
||||
|
||||
@@ -229,14 +230,14 @@ export async function POST(req: NextRequest) {
|
||||
});
|
||||
await execFileAsync("npm", ["install", "--legacy-peer-deps"], {
|
||||
timeout: 300_000,
|
||||
cwd: process.cwd(),
|
||||
cwd: PROJECT_ROOT,
|
||||
});
|
||||
send({ step: "rebuild", status: "done", message: "Dependencies installed" });
|
||||
|
||||
try {
|
||||
await execFileAsync("node", ["scripts/dev/sync-env.mjs"], {
|
||||
timeout: 15_000,
|
||||
cwd: process.cwd(),
|
||||
cwd: PROJECT_ROOT,
|
||||
});
|
||||
} catch {
|
||||
// .env sync is non-fatal during update.
|
||||
@@ -249,7 +250,7 @@ export async function POST(req: NextRequest) {
|
||||
});
|
||||
await execFileAsync("npm", ["run", "build"], {
|
||||
timeout: 600_000,
|
||||
cwd: process.cwd(),
|
||||
cwd: PROJECT_ROOT,
|
||||
});
|
||||
send({ step: "rebuild", status: "done", message: "Build complete" });
|
||||
|
||||
@@ -257,7 +258,7 @@ export async function POST(req: NextRequest) {
|
||||
try {
|
||||
await execFileAsync("pm2", ["restart", "omniroute", "--update-env"], {
|
||||
timeout: 30_000,
|
||||
cwd: process.cwd(),
|
||||
cwd: PROJECT_ROOT,
|
||||
});
|
||||
send({ step: "restart", status: "done", message: "Service restarted" });
|
||||
} catch {
|
||||
@@ -306,13 +307,14 @@ export async function POST(req: NextRequest) {
|
||||
try {
|
||||
// Step 1: Install
|
||||
send({ step: "install", status: "running", message: `Installing omniroute@${latest}...` });
|
||||
await execFileAsync(
|
||||
"npm",
|
||||
["install", "-g", `omniroute@${latest}`, "--ignore-scripts", "--legacy-peer-deps"],
|
||||
{
|
||||
timeout: 300000,
|
||||
}
|
||||
);
|
||||
await execFileAsync(
|
||||
"npm",
|
||||
["install", "-g", `omniroute@${latest}`, "--ignore-scripts", "--legacy-peer-deps"],
|
||||
{
|
||||
timeout: 300000,
|
||||
cwd: PROJECT_ROOT,
|
||||
}
|
||||
);
|
||||
send({ step: "install", status: "done", message: `Installed omniroute@${latest}` });
|
||||
|
||||
// Step 2: Rebuild native modules (critical for better-sqlite3)
|
||||
@@ -321,29 +323,36 @@ export async function POST(req: NextRequest) {
|
||||
status: "running",
|
||||
message: "Rebuilding native modules (better-sqlite3)...",
|
||||
});
|
||||
const globalRoot = (
|
||||
await execFileAsync("npm", ["root", "-g"], { timeout: 10000 })
|
||||
).stdout.trim();
|
||||
const globalRoot = (
|
||||
await execFileAsync("npm", ["root", "-g"], { timeout: 10000, cwd: PROJECT_ROOT })
|
||||
).stdout.trim();
|
||||
const omniPath = `${globalRoot}/omniroute/app`;
|
||||
await execFileAsync("npm", ["rebuild", "better-sqlite3"], {
|
||||
cwd: omniPath,
|
||||
timeout: 120000,
|
||||
});
|
||||
await execFileAsync(
|
||||
"npm",
|
||||
["rebuild", "better-sqlite3"],
|
||||
{
|
||||
cwd: omniPath,
|
||||
timeout: 120000,
|
||||
}
|
||||
);
|
||||
send({ step: "rebuild", status: "done", message: "Native modules rebuilt" });
|
||||
|
||||
// Step 3: Restart PM2
|
||||
send({ step: "restart", status: "running", message: "Restarting service via PM2..." });
|
||||
try {
|
||||
await execFileAsync("pm2", ["restart", "omniroute", "--update-env"], { timeout: 30000 });
|
||||
send({ step: "restart", status: "done", message: "Service restarted" });
|
||||
} catch {
|
||||
// PM2 may not be available (Docker/manual setups)
|
||||
send({
|
||||
step: "restart",
|
||||
status: "skipped",
|
||||
message: "PM2 not available — manual restart needed",
|
||||
});
|
||||
}
|
||||
try {
|
||||
await execFileAsync("pm2", ["restart", "omniroute", "--update-env"], {
|
||||
timeout: 30000,
|
||||
cwd: PROJECT_ROOT,
|
||||
});
|
||||
send({ step: "restart", status: "done", message: "Service restarted" });
|
||||
} catch {
|
||||
// PM2 may not be available (Docker/manual setups)
|
||||
send({
|
||||
step: "restart",
|
||||
status: "skipped",
|
||||
message: "PM2 not available — manual restart needed",
|
||||
});
|
||||
}
|
||||
|
||||
send({
|
||||
step: "complete",
|
||||
|
||||
@@ -3,9 +3,24 @@ import { closeSync, mkdirSync, openSync, existsSync } from "node:fs";
|
||||
import { access } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
import { homedir } from "node:os";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
function resolveProjectRoot(fallback: string): string {
|
||||
try {
|
||||
const cwd = process.cwd();
|
||||
if (existsSync(path.join(cwd, "package.json"))) return cwd;
|
||||
if (existsSync(path.join(cwd, ".git"))) return cwd;
|
||||
return cwd;
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
const FALLBACK_CWD = process.env.HOME || homedir() || "/tmp";
|
||||
export const PROJECT_ROOT: string = resolveProjectRoot(FALLBACK_CWD);
|
||||
|
||||
type ComposeCommand = "docker compose" | "docker-compose";
|
||||
export type AutoUpdateMode = "npm" | "docker-compose" | "source";
|
||||
|
||||
@@ -70,8 +85,8 @@ export function getAutoUpdateConfig(env: NodeJS.ProcessEnv = process.env): AutoU
|
||||
|
||||
let mode = normalizeMode(env.AUTO_UPDATE_MODE);
|
||||
if (mode === "npm") {
|
||||
const isGitRepo = existsSync(path.join(process.cwd(), ".git"));
|
||||
const currentDir = typeof __dirname !== "undefined" ? __dirname : process.cwd();
|
||||
const isGitRepo = existsSync(path.join(PROJECT_ROOT, ".git"));
|
||||
const currentDir = typeof __dirname !== "undefined" ? __dirname : PROJECT_ROOT;
|
||||
const isGlobalNodeModules = currentDir.includes("node_modules");
|
||||
|
||||
// If we are not in a global node_modules directory, we are likely a local source install/build.
|
||||
@@ -117,7 +132,7 @@ export async function validateAutoUpdateRuntime(
|
||||
existsImpl: (targetPath: string) => Promise<boolean> = pathExists
|
||||
): Promise<AutoUpdateValidation> {
|
||||
if (config.mode === "source") {
|
||||
const gitDir = path.join(process.cwd(), ".git");
|
||||
const gitDir = path.join(PROJECT_ROOT, ".git");
|
||||
if (!(await existsImpl(gitDir))) {
|
||||
return {
|
||||
supported: false,
|
||||
@@ -197,7 +212,7 @@ export async function validateAutoUpdateRuntime(
|
||||
export async function ensureGitTagExists(
|
||||
targetTag: string,
|
||||
execFileImpl: ExecFileLike = execFileAsync,
|
||||
cwd = process.cwd()
|
||||
cwd = PROJECT_ROOT
|
||||
): Promise<void> {
|
||||
try {
|
||||
await execFileImpl("git", ["rev-parse", "-q", "--verify", `refs/tags/${targetTag}`], {
|
||||
|
||||
@@ -754,6 +754,62 @@ describe("Reasoning Replay Cache — Translator Replay", () => {
|
||||
|
||||
assert.equal(translated.messages[1].reasoning_content, undefined);
|
||||
});
|
||||
|
||||
it("should replace empty-string reasoning_content with NON_ANTHROPIC_THINKING_PLACEHOLDER on cache miss", async () => {
|
||||
// Regression: injectEmptyReasoningContentForToolCalls (schemaCoercion.ts) pre-sets
|
||||
// reasoning_content="" before the cache lookup. The old condition
|
||||
// `msg.reasoning_content === undefined` never fired on cache miss, leaving the
|
||||
// empty string in place. DeepSeek V4+ rejects "" with a 400.
|
||||
clearReasoningCacheAll();
|
||||
clearModelsDevCapabilities();
|
||||
saveModelsDevCapabilities({
|
||||
deepseek: {
|
||||
"deepseek-v4-flash": buildCapability({
|
||||
interleaved_field: "reasoning_content",
|
||||
reasoning: true,
|
||||
tool_call: true,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
const { NON_ANTHROPIC_THINKING_PLACEHOLDER } = await import(
|
||||
"../../open-sse/translator/helpers/claudeHelper.ts"
|
||||
);
|
||||
|
||||
// No cache entry → cache miss
|
||||
const translated = translateRequest(
|
||||
FORMATS.OPENAI,
|
||||
FORMATS.OPENAI,
|
||||
"deepseek-v4-flash",
|
||||
{
|
||||
messages: [
|
||||
{ role: "user", content: "use a tool" },
|
||||
{
|
||||
role: "assistant",
|
||||
content: null,
|
||||
reasoning_content: "",
|
||||
tool_calls: [
|
||||
{
|
||||
id: "call_empty_rc",
|
||||
type: "function",
|
||||
function: { name: "read_file", arguments: "{}" },
|
||||
},
|
||||
],
|
||||
},
|
||||
{ role: "tool", tool_call_id: "call_empty_rc", content: "file contents" },
|
||||
],
|
||||
},
|
||||
false,
|
||||
null,
|
||||
"deepseek"
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
translated.messages[1].reasoning_content,
|
||||
NON_ANTHROPIC_THINKING_PLACEHOLDER,
|
||||
"empty reasoning_content should be replaced with placeholder on cache miss"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Reasoning Replay Cache — API Route", () => {
|
||||
|
||||
Reference in New Issue
Block a user