mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-11 09:42:15 +03:00
refactor(reasoning): use replay policy for K3
This commit is contained in:
@@ -64,6 +64,8 @@ const REASONING_REPLAY_MODEL_PATTERNS = [
|
||||
];
|
||||
|
||||
const DEEPSEEK_V4_MODEL_PATTERN = /deepseek[-/]v4[-.](flash|pro)/i;
|
||||
const K3_REASONING_REPLAY_MODEL_PATTERN = /(?:^|\/)(?:kimi-)?k3(?:$|-)/i;
|
||||
const NATIVE_K27_REASONING_REPLAY_MODEL_PATTERN = /(?:^|\/)kimi-k2\.7-code(?:$|-)/i;
|
||||
|
||||
export function isDeepSeekReasoningModel(params: {
|
||||
provider: string;
|
||||
@@ -94,6 +96,14 @@ export function requiresReasoningReplay(params: {
|
||||
if (normalizedInterleavedField === "reasoning_content") return true;
|
||||
if (normalizedInterleavedField === "reasoning_details") return false;
|
||||
|
||||
if (K3_REASONING_REPLAY_MODEL_PATTERN.test(normalizedModel)) return true;
|
||||
if (
|
||||
(normalizedProvider === "moonshot" || normalizedProvider === "kimi") &&
|
||||
NATIVE_K27_REASONING_REPLAY_MODEL_PATTERN.test(normalizedModel)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// DeepSeek legacy reasoner family has an inverse contract: do not replay.
|
||||
if (/deepseek-reasoner/i.test(normalizedModel) || /deepseek-r1/i.test(normalizedModel)) {
|
||||
return false;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Convert OpenAI Responses API format to standard chat completions format.
|
||||
* Delegates to the canonical translator to avoid logic duplication.
|
||||
*/
|
||||
import { requiresAuthenticReasoningContent } from "../../utils/reasoningContentInjector.ts";
|
||||
import { requiresReasoningReplay } from "../../services/reasoningCache.ts";
|
||||
import { openaiResponsesToOpenAIRequest } from "../request/openai-responses.ts";
|
||||
import { toRecord } from "../request/openai-responses/helpers.ts";
|
||||
|
||||
@@ -23,7 +23,11 @@ export function convertResponsesApiFormat(
|
||||
credentials && typeof credentials === "object" && !Array.isArray(credentials)
|
||||
? (credentials as Record<string, unknown>)
|
||||
: {};
|
||||
const translationCredentials = requiresAuthenticReasoningContent(provider, model)
|
||||
const translationCredentials = requiresReasoningReplay({
|
||||
provider: String(provider ?? ""),
|
||||
model: String(model ?? ""),
|
||||
allowLegacyFallback: false,
|
||||
})
|
||||
? { ...credentialRecord, _preserveReasoningContent: true }
|
||||
: credentials;
|
||||
const converted = openaiResponsesToOpenAIRequest(
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
providerHonorsOpenAIFormatCacheControl,
|
||||
resolveConnectionCacheOverride,
|
||||
} from "../utils/cacheControlPolicy.ts";
|
||||
import { requiresAuthenticReasoningContent } from "../utils/reasoningContentInjector.ts";
|
||||
import { isInternalReasoningPlaceholder } from "../utils/reasoningPlaceholder.ts";
|
||||
import {
|
||||
coerceToolSchemas,
|
||||
@@ -162,7 +161,11 @@ function isReasoningOnlyReplayTarget(provider: unknown, model: unknown): boolean
|
||||
/(^|\/)deepseek/i.test(normalizedModel) ||
|
||||
normalizedProvider === "xiaomi-mimo" ||
|
||||
/(^|\/)mimo/i.test(normalizedModel) ||
|
||||
requiresAuthenticReasoningContent(normalizedProvider, normalizedModel)
|
||||
requiresReasoningReplay({
|
||||
provider: normalizedProvider,
|
||||
model: normalizedModel,
|
||||
allowLegacyFallback: false,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -237,12 +240,13 @@ export function translateRequest(
|
||||
const normalizedModel = String(model ?? "");
|
||||
const isKimiCoding =
|
||||
normalizedProvider === "kimi-coding" || normalizedProvider === "kimi-coding-apikey";
|
||||
const requiresAuthenticReasoning = requiresAuthenticReasoningContent(
|
||||
normalizedProvider,
|
||||
normalizedModel
|
||||
);
|
||||
const requiresExplicitReasoningReplay = requiresReasoningReplay({
|
||||
provider: normalizedProvider,
|
||||
model: normalizedModel,
|
||||
allowLegacyFallback: false,
|
||||
});
|
||||
const preserveResponsesReasoning =
|
||||
sourceFormat === FORMATS.OPENAI_RESPONSES && requiresAuthenticReasoning;
|
||||
sourceFormat === FORMATS.OPENAI_RESPONSES && requiresExplicitReasoningReplay;
|
||||
|
||||
// Phase 2: Apply thinking budget control before normalization
|
||||
result = applyThinkingBudget(result);
|
||||
@@ -379,18 +383,16 @@ export function translateRequest(
|
||||
provider: normalizedProvider,
|
||||
model: normalizedModel,
|
||||
});
|
||||
const isReasoner =
|
||||
requiresAuthenticReasoning ||
|
||||
requiresReasoningReplay({
|
||||
const isReasoner = requiresReasoningReplay({
|
||||
provider: normalizedProvider,
|
||||
model: normalizedModel,
|
||||
thinkingEnabled: hasThinkingConfig(result),
|
||||
supportsReasoning: supportsReasoning({
|
||||
provider: normalizedProvider,
|
||||
model: normalizedModel,
|
||||
thinkingEnabled: hasThinkingConfig(result),
|
||||
supportsReasoning: supportsReasoning({
|
||||
provider: normalizedProvider,
|
||||
model: normalizedModel,
|
||||
}),
|
||||
interleavedField: resolvedCapabilities?.interleavedField ?? null,
|
||||
});
|
||||
}),
|
||||
interleavedField: resolvedCapabilities?.interleavedField ?? null,
|
||||
});
|
||||
|
||||
// Always normalize to clean OpenAI format when target is OpenAI
|
||||
// This handles hybrid requests (e.g., OpenAI messages + Claude tools)
|
||||
@@ -461,7 +463,7 @@ export function translateRequest(
|
||||
|
||||
if (
|
||||
targetFormat === FORMATS.OPENAI &&
|
||||
!requiresAuthenticReasoning &&
|
||||
!requiresExplicitReasoningReplay &&
|
||||
result.messages &&
|
||||
Array.isArray(result.messages)
|
||||
) {
|
||||
@@ -579,7 +581,7 @@ export function translateRequest(
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (requiresAuthenticReasoning) continue;
|
||||
if (requiresExplicitReasoningReplay) continue;
|
||||
msg.content.splice(firstToolUseIdx, 0, {
|
||||
type: "thinking",
|
||||
thinking: NON_ANTHROPIC_THINKING_PLACEHOLDER,
|
||||
@@ -616,7 +618,7 @@ export function translateRequest(
|
||||
// Native Moonshot K3/K2.7 accepts only the real prior reasoning. If it
|
||||
// was not supplied and the cache missed, leave it absent so upstream can
|
||||
// enforce its contract instead of corrupting history with a placeholder.
|
||||
if (requiresAuthenticReasoning) {
|
||||
if (requiresExplicitReasoningReplay) {
|
||||
if (msg.reasoning_content === "") delete msg.reasoning_content;
|
||||
continue;
|
||||
}
|
||||
@@ -629,7 +631,7 @@ export function translateRequest(
|
||||
// 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.
|
||||
// (kimi-coding, moonshot reasoning replay) have their own paths above.
|
||||
if ((hasToolCalls || shouldReplayReasoningOnly) && !msg.reasoning_content) {
|
||||
if (requiresReasoningContentPresence(normalizedProvider, normalizedModel)) {
|
||||
msg.reasoning_content = NON_ANTHROPIC_THINKING_PLACEHOLDER;
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
* that proxy to thinking-mode models.
|
||||
*/
|
||||
|
||||
import { requiresReasoningReplay } from "../services/reasoningCache.ts";
|
||||
|
||||
const PLACEHOLDER = " ";
|
||||
|
||||
type JsonRecord = Record<string, unknown>;
|
||||
@@ -30,27 +32,6 @@ const THINKING_MODEL_PATTERNS: RegExp[] = [
|
||||
/\bmimo\b/i, // xiaomi-tokenplan mimo family (e.g. xiaomi-tokenplan/mimo-v2.5-pro)
|
||||
];
|
||||
|
||||
const K3_AUTHENTIC_REASONING_PATTERN = /(?:^|\/)(?:kimi-)?k3(?:$|-)/i;
|
||||
const NATIVE_K27_AUTHENTIC_REASONING_PATTERN = /(?:^|\/)kimi-k2\.7-code(?:$|-)/i;
|
||||
|
||||
/**
|
||||
* K3 requires authentic reasoning regardless of which provider serves it.
|
||||
* Native Moonshot K2.7 retains the same preserved-thinking contract. Empty
|
||||
* protocol markers remain valid only after client content and replay miss.
|
||||
*/
|
||||
export function requiresAuthenticReasoningContent(provider: unknown, model: unknown): boolean {
|
||||
const normalizedModel = String(model ?? "").trim();
|
||||
if (K3_AUTHENTIC_REASONING_PATTERN.test(normalizedModel)) return true;
|
||||
|
||||
const normalizedProvider = String(provider ?? "")
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
return (
|
||||
(normalizedProvider === "moonshot" || normalizedProvider === "kimi") &&
|
||||
NATIVE_K27_AUTHENTIC_REASONING_PATTERN.test(normalizedModel)
|
||||
);
|
||||
}
|
||||
|
||||
export function isThinkingMessageModel(model: string | undefined | null): boolean {
|
||||
if (!model || typeof model !== "string") return false;
|
||||
return THINKING_MODEL_PATTERNS.some((re) => re.test(model));
|
||||
@@ -65,7 +46,11 @@ export function shouldInjectReasoningContentPlaceholder(
|
||||
.toLowerCase();
|
||||
return (
|
||||
(normalizedProvider === "moonshot" || normalizedProvider === "kimi") &&
|
||||
!requiresAuthenticReasoningContent(normalizedProvider, model) &&
|
||||
!requiresReasoningReplay({
|
||||
provider: normalizedProvider,
|
||||
model: String(model ?? ""),
|
||||
allowLegacyFallback: false,
|
||||
}) &&
|
||||
isThinkingMessageModel(model)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -66,8 +66,8 @@ describe("reasoningCache helpers", () => {
|
||||
it("returns false for reasoning_details interleaved field", () => {
|
||||
assert.equal(
|
||||
mod.requiresReasoningReplay({
|
||||
provider: "any",
|
||||
model: "any",
|
||||
provider: "moonshot",
|
||||
model: "k3",
|
||||
interleavedField: "reasoning_details",
|
||||
}),
|
||||
false
|
||||
@@ -116,6 +116,9 @@ describe("reasoningCache helpers", () => {
|
||||
|
||||
it("detects native Kimi thinking model IDs without matching unrelated aliases", () => {
|
||||
for (const model of [
|
||||
"k3",
|
||||
"k3-256k",
|
||||
"kimi-k3",
|
||||
"kimi-k2",
|
||||
"kimi-k2.6",
|
||||
"kimi-k2.6-thinking",
|
||||
@@ -126,11 +129,25 @@ describe("reasoningCache helpers", () => {
|
||||
assert.equal(mod.requiresReasoningReplay({ provider: "some-other", model }), true, model);
|
||||
}
|
||||
|
||||
for (const model of ["k3", "moonshot-v1-8k", "kimi-latest"]) {
|
||||
for (const model of ["moonshot-v1-8k", "kimi-latest"]) {
|
||||
assert.equal(mod.requiresReasoningReplay({ provider: "some-other", model }), false, model);
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps K3 and native Moonshot K2.7 replay explicit", () => {
|
||||
for (const model of ["k3", "k3-256k", "kimi-k3", "kimi-k2.7-code"]) {
|
||||
assert.equal(
|
||||
mod.requiresReasoningReplay({
|
||||
provider: "moonshot",
|
||||
model,
|
||||
allowLegacyFallback: false,
|
||||
}),
|
||||
true,
|
||||
model
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("returns false when allowLegacyFallback is false and no explicit signal", () => {
|
||||
assert.equal(
|
||||
mod.requiresReasoningReplay({
|
||||
|
||||
Reference in New Issue
Block a user