mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-11 09:42:15 +03:00
Merge-train validated (tip 6ce4effef8). Vitest failures confirmed as base-red (#9679).
This commit is contained in:
@@ -22,6 +22,7 @@ import {
|
||||
getReasoningCacheStats,
|
||||
setReasoningCache,
|
||||
} from "../../src/lib/db/reasoningCache.ts";
|
||||
import { isInternalReasoningPlaceholder } from "../utils/reasoningPlaceholder.ts";
|
||||
|
||||
// ──────────────── Provider/Model Detection ────────────────
|
||||
|
||||
@@ -194,6 +195,9 @@ export function cacheReasoningByKey(
|
||||
reasoning: string
|
||||
): void {
|
||||
if (!key || !reasoning) return;
|
||||
// ponytail: never store the internal replay placeholder — models echo it
|
||||
// and it poisons the cache (upstream echo loop, OmniRoute #9573).
|
||||
if (isInternalReasoningPlaceholder(reasoning)) return;
|
||||
|
||||
if (reasoning.length > MAX_ENTRY_BYTES) {
|
||||
reasoning = reasoning.slice(0, MAX_ENTRY_BYTES);
|
||||
@@ -259,6 +263,8 @@ export function cacheReasoningFromAssistantMessage(
|
||||
? message.reasoning
|
||||
: "";
|
||||
if (!reasoning) return 0;
|
||||
// ponytail: don't capture the echoed placeholder into the cache.
|
||||
if (isInternalReasoningPlaceholder(reasoning)) return 0;
|
||||
|
||||
const toolCallIds = Array.isArray(message.tool_calls)
|
||||
? (message.tool_calls as ToolCallLike[])
|
||||
@@ -299,6 +305,12 @@ export function lookupReasoning(toolCallId: string): string | null {
|
||||
const mem = memoryCache.get(toolCallId);
|
||||
if (mem) {
|
||||
if (Date.now() < mem.expiresAt) {
|
||||
// ponytail: never replay the internal placeholder from memory.
|
||||
if (isInternalReasoningPlaceholder(mem.reasoning)) {
|
||||
memoryCache.delete(toolCallId);
|
||||
misses++;
|
||||
return null;
|
||||
}
|
||||
hits++;
|
||||
return mem.reasoning;
|
||||
}
|
||||
@@ -314,6 +326,11 @@ export function lookupReasoning(toolCallId: string): string | null {
|
||||
// DB lookup failure is non-fatal; treat it as a cache miss.
|
||||
}
|
||||
if (dbResult) {
|
||||
// ponytail: never promote/replay the internal placeholder from DB.
|
||||
if (isInternalReasoningPlaceholder(dbResult.reasoning)) {
|
||||
misses++;
|
||||
return null;
|
||||
}
|
||||
hits++;
|
||||
let promotedReasoning = dbResult.reasoning;
|
||||
if (promotedReasoning.length > MAX_ENTRY_BYTES) {
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
resolveConnectionCacheOverride,
|
||||
} from "../utils/cacheControlPolicy.ts";
|
||||
import { requiresAuthenticReasoningContent } from "../utils/reasoningContentInjector.ts";
|
||||
import { isInternalReasoningPlaceholder } from "../utils/reasoningPlaceholder.ts";
|
||||
import {
|
||||
coerceToolSchemas,
|
||||
injectEmptyReasoningContentForToolCalls,
|
||||
@@ -483,10 +484,11 @@ export function translateRequest(
|
||||
!hasNonEmptyReasoningContent(msg);
|
||||
|
||||
if (!hasToolCalls && !hasToolUseBlocks && !shouldReplayReasoningOnly) {
|
||||
// Strip empty reasoning_content on non-tool-call messages we are NOT
|
||||
// replaying (e.g. non-DeepSeek targets); an empty string has no meaningful
|
||||
// value to send and may confuse some upstreams.
|
||||
if (msg.reasoning_content === "") {
|
||||
// Strip empty or placeholder reasoning_content on non-tool-call messages
|
||||
// we are NOT replaying. The placeholder is request scaffolding, never
|
||||
// real reasoning — forwarding it makes the model continue its chain of
|
||||
// thought FROM that text (echo → empty stop, #9573).
|
||||
if (msg.reasoning_content === "" || isInternalReasoningPlaceholder(msg.reasoning_content)) {
|
||||
delete msg.reasoning_content;
|
||||
}
|
||||
continue;
|
||||
@@ -528,9 +530,17 @@ export function translateRequest(
|
||||
}
|
||||
|
||||
// ── OpenAI-format message ──
|
||||
// Skip if client already provided real reasoning_content
|
||||
// Skip if client already provided real reasoning_content. The internal
|
||||
// replay placeholder is NOT real reasoning: drop it and fall through to
|
||||
// the cache lookup so it can be replaced with genuine cached reasoning.
|
||||
// Forwarding it makes the model continue its chain of thought from that
|
||||
// text (echo → empty stop), and the echo re-poisons cache + client
|
||||
// history (#9573).
|
||||
if (hasNonEmptyReasoningContent(msg)) {
|
||||
continue;
|
||||
if (!isInternalReasoningPlaceholder(msg.reasoning_content)) {
|
||||
continue;
|
||||
}
|
||||
delete msg.reasoning_content;
|
||||
}
|
||||
|
||||
const cacheKey = hasToolCalls
|
||||
@@ -553,19 +563,17 @@ export function translateRequest(
|
||||
continue;
|
||||
}
|
||||
|
||||
// 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.
|
||||
//
|
||||
// Applies to tool-call messages AND to plain (non-tool-call) assistant turns
|
||||
// on DeepSeek replay targets (#1682). Without the placeholder on plain turns,
|
||||
// a multi-turn text conversation whose reasoning_content the client stripped
|
||||
// is forwarded to DeepSeek without the field and rejected with 400.
|
||||
// Cache miss fallback — previously injected a non-empty placeholder
|
||||
// (NON_ANTHROPIC_THINKING_PLACEHOLDER) to dodge an alleged DeepSeek V4 400
|
||||
// on missing reasoning_content. The placeholder is the root cause of this
|
||||
// bug: the model echoes it as its own reasoning and stops (empty turns),
|
||||
// and the echo re-poisons the cache + client history (#9573). Empirically,
|
||||
// deepseek-v4-flash accepts an ABSENT reasoning_content field (the 400 is
|
||||
// specific to empty-string, and even that is endpoint-dependent). Omit
|
||||
// the field instead; providers that genuinely enforce the contract
|
||||
// (kimi-coding, moonshot authentic-reasoning) have their own paths above.
|
||||
if ((hasToolCalls || shouldReplayReasoningOnly) && !msg.reasoning_content) {
|
||||
msg.reasoning_content = NON_ANTHROPIC_THINKING_PLACEHOLDER;
|
||||
delete msg.reasoning_content;
|
||||
}
|
||||
}
|
||||
} else if (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { stripInternalReasoningPlaceholder } from "./reasoningPlaceholder.ts";
|
||||
|
||||
type JsonRecord = Record<string, unknown>;
|
||||
|
||||
export function asReasoningRecord(value: unknown): JsonRecord {
|
||||
@@ -69,4 +71,17 @@ export function copyOpenAICompatibleReasoningFields(source: JsonRecord, target:
|
||||
const mirrored = getUnsupportedReasoningValue(source);
|
||||
if (mirrored) target.reasoning_content = mirrored;
|
||||
}
|
||||
// ponytail: the internal replay placeholder is request scaffolding, never
|
||||
// real reasoning — models echo it and it poisons client history + the cache
|
||||
// (#8081 echo). Strip it from anything we forward to the client.
|
||||
if (typeof target.reasoning_content === "string") {
|
||||
const stripped = stripInternalReasoningPlaceholder(target.reasoning_content);
|
||||
if (stripped === "") delete target.reasoning_content;
|
||||
else if (stripped !== target.reasoning_content) target.reasoning_content = stripped;
|
||||
}
|
||||
if (typeof target.reasoning === "string") {
|
||||
const stripped = stripInternalReasoningPlaceholder(target.reasoning);
|
||||
if (stripped === "") delete target.reasoning;
|
||||
else if (stripped !== target.reasoning) target.reasoning = stripped;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,6 +81,8 @@ export function setReasoningCache(
|
||||
reasoning: string,
|
||||
ttlMs: number = DEFAULT_TTL_MS
|
||||
): void {
|
||||
const PLACEHOLDER = "(prior reasoning summary unavailable)";
|
||||
if (!reasoning || reasoning.trim() === PLACEHOLDER) return; // ponytail: never store the internal placeholder
|
||||
if (reasoning.length > MAX_ENTRY_BYTES) {
|
||||
reasoning = reasoning.slice(0, MAX_ENTRY_BYTES);
|
||||
}
|
||||
@@ -110,6 +112,8 @@ export function getReasoningCache(
|
||||
)
|
||||
.get(toolCallId) as { reasoning: string; provider: string; model: string } | undefined;
|
||||
|
||||
const PLACEHOLDER = "(prior reasoning summary unavailable)";
|
||||
if (row && row.reasoning && row.reasoning.trim() === PLACEHOLDER) return null; // ponytail: never replay the placeholder
|
||||
return row ?? null;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user