fix(ai): fixed filtering of empty user text blocks and reasoning content normalization

- Fixed filtering of empty user text blocks for OpenAI-compatible completions.
- Fixed normalization of Kimi reasoning_content for OpenRouter tool-call messages.
- Added compatibility flags for reasoning content field handling and tool-call requirements.
- Updated model registry with new models (kimi-k2.5, solar-pro-3, qwen3-max-thinking) and adjusted pricing/token limits.
This commit is contained in:
can1357
2026-01-27 18:22:37 +01:00
parent 91da560cc7
commit 3cec31c57d
4 changed files with 135 additions and 46 deletions
+3
View File
@@ -2,6 +2,9 @@
## [Unreleased]
### Fixed
- Filtered empty user text blocks for OpenAI-compatible completions and normalized Kimi reasoning_content for OpenRouter tool-call messages
## [8.4.0] - 2026-01-25
### Added
+72 -38
View File
@@ -5367,7 +5367,7 @@ export const MODELS = {
cacheWrite: 0.08333333333333334,
},
contextWindow: 1048576,
maxTokens: 65535,
maxTokens: 65536,
} satisfies Model<"openai-completions">,
"google/gemini-2.5-pro": {
id: "google/gemini-2.5-pro",
@@ -5760,23 +5760,6 @@ export const MODELS = {
contextWindow: 262144,
maxTokens: 65536,
} satisfies Model<"openai-completions">,
"mistralai/devstral-2512:free": {
id: "mistralai/devstral-2512:free",
name: "Mistral: Devstral 2 2512 (free)",
api: "openai-completions",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
reasoning: false,
input: ["text"],
cost: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 262144,
maxTokens: 4096,
} satisfies Model<"openai-completions">,
"mistralai/devstral-medium": {
id: "mistralai/devstral-medium",
name: "Mistral: Devstral Medium",
@@ -6287,6 +6270,23 @@ export const MODELS = {
contextWindow: 262144,
maxTokens: 65535,
} satisfies Model<"openai-completions">,
"moonshotai/kimi-k2.5": {
id: "moonshotai/kimi-k2.5",
name: "MoonshotAI: Kimi K2.5",
api: "openai-completions",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
reasoning: true,
input: ["text", "image"],
cost: {
input: 0.6,
output: 3,
cacheRead: 0.09999999999999999,
cacheWrite: 0,
},
contextWindow: 262144,
maxTokens: 4096,
} satisfies Model<"openai-completions">,
"nex-agi/deepseek-v3.1-nex-n1": {
id: "nex-agi/deepseek-v3.1-nex-n1",
name: "Nex AGI: DeepSeek V3.1 Nex N1",
@@ -7726,7 +7726,7 @@ export const MODELS = {
cost: {
input: 0.22,
output: 1.7999999999999998,
cacheRead: 0,
cacheRead: 0.022,
cacheWrite: 0,
},
contextWindow: 262144,
@@ -7862,7 +7862,7 @@ export const MODELS = {
cost: {
input: 0.15,
output: 0.6,
cacheRead: 0,
cacheRead: 0.075,
cacheWrite: 0,
},
contextWindow: 262144,
@@ -8089,6 +8089,23 @@ export const MODELS = {
contextWindow: 163840,
maxTokens: 65536,
} satisfies Model<"openai-completions">,
"upstage/solar-pro-3:free": {
id: "upstage/solar-pro-3:free",
name: "Upstage: Solar Pro 3 (free)",
api: "openai-completions",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
reasoning: true,
input: ["text"],
cost: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 128000,
maxTokens: 4096,
} satisfies Model<"openai-completions">,
"x-ai/grok-3": {
id: "x-ai/grok-3",
name: "xAI: Grok 3",
@@ -8242,23 +8259,6 @@ export const MODELS = {
contextWindow: 262144,
maxTokens: 4096,
} satisfies Model<"openai-completions">,
"xiaomi/mimo-v2-flash:free": {
id: "xiaomi/mimo-v2-flash:free",
name: "Xiaomi: MiMo-V2-Flash (free)",
api: "openai-completions",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
reasoning: true,
input: ["text"],
cost: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 262144,
maxTokens: 65536,
} satisfies Model<"openai-completions">,
"z-ai/glm-4-32b": {
id: "z-ai/glm-4-32b",
name: "Z.AI: GLM 4 32B ",
@@ -8372,7 +8372,7 @@ export const MODELS = {
cost: {
input: 0.44,
output: 1.76,
cacheRead: 0,
cacheRead: 0.11,
cacheWrite: 0,
},
contextWindow: 204800,
@@ -8601,6 +8601,23 @@ export const MODELS = {
contextWindow: 262144,
maxTokens: 32768,
} satisfies Model<"anthropic-messages">,
"alibaba/qwen3-max-thinking": {
id: "alibaba/qwen3-max-thinking",
name: "Qwen 3 Max Thinking",
api: "anthropic-messages",
provider: "vercel-ai-gateway",
baseUrl: "https://ai-gateway.vercel.sh",
reasoning: true,
input: ["text"],
cost: {
input: 1.2,
output: 6,
cacheRead: 0.24,
cacheWrite: 0,
},
contextWindow: 256000,
maxTokens: 256000,
} satisfies Model<"anthropic-messages">,
"anthropic/claude-3-haiku": {
id: "anthropic/claude-3-haiku",
name: "Claude 3 Haiku",
@@ -9485,6 +9502,23 @@ export const MODELS = {
contextWindow: 256000,
maxTokens: 16384,
} satisfies Model<"anthropic-messages">,
"moonshotai/kimi-k2.5": {
id: "moonshotai/kimi-k2.5",
name: "Kimi K2.5",
api: "anthropic-messages",
provider: "vercel-ai-gateway",
baseUrl: "https://ai-gateway.vercel.sh",
reasoning: true,
input: ["text", "image"],
cost: {
input: 1.2,
output: 1.2,
cacheRead: 0.6,
cacheWrite: 0,
},
contextWindow: 256000,
maxTokens: 256000,
} satisfies Model<"anthropic-messages">,
"nvidia/nemotron-nano-12b-v2-vl": {
id: "nvidia/nemotron-nano-12b-v2-vl",
name: "Nvidia Nemotron Nano 12B V2 VL",
@@ -504,26 +504,31 @@ export function convertMessages(
if (msg.role === "user") {
if (typeof msg.content === "string") {
const text = sanitizeSurrogates(msg.content);
if (text.trim().length === 0) continue;
params.push({
role: "user",
content: sanitizeSurrogates(msg.content),
content: text,
});
} else {
const content: ChatCompletionContentPart[] = msg.content.map((item): ChatCompletionContentPart => {
const content: ChatCompletionContentPart[] = [];
for (const item of msg.content) {
if (item.type === "text") {
return {
const text = sanitizeSurrogates(item.text);
if (text.trim().length === 0) continue;
content.push({
type: "text",
text: sanitizeSurrogates(item.text),
} satisfies ChatCompletionContentPartText;
text,
} satisfies ChatCompletionContentPartText);
} else {
return {
content.push({
type: "image_url",
image_url: {
url: `data:${item.mimeType};base64,${item.data}`,
},
} satisfies ChatCompletionContentPartImage;
} satisfies ChatCompletionContentPartImage);
}
});
}
const filteredContent = !model.input.includes("image")
? content.filter(c => c.type !== "image_url")
: content;
@@ -578,7 +583,36 @@ export function convertMessages(
}
}
if (compat.thinkingFormat === "openai") {
const reasoningField = compat.reasoningContentField ?? "reasoning_content";
const reasoningContent = (assistantMsg as any)[reasoningField];
if (!reasoningContent) {
const reasoning = (assistantMsg as any).reasoning;
const reasoningText = (assistantMsg as any).reasoning_text;
if (reasoning && reasoningField !== "reasoning") {
(assistantMsg as any)[reasoningField] = reasoning;
} else if (reasoningText && reasoningField !== "reasoning_text") {
(assistantMsg as any)[reasoningField] = reasoningText;
} else if (nonEmptyThinkingBlocks.length > 0) {
(assistantMsg as any)[reasoningField] = nonEmptyThinkingBlocks.map(b => b.thinking).join("\n");
}
}
}
const toolCalls = msg.content.filter(b => b.type === "toolCall") as ToolCall[];
const hasReasoningField =
(assistantMsg as any).reasoning_content !== undefined ||
(assistantMsg as any).reasoning !== undefined ||
(assistantMsg as any).reasoning_text !== undefined;
if (
toolCalls.length > 0 &&
compat.requiresReasoningContentForToolCalls &&
compat.thinkingFormat === "openai" &&
!hasReasoningField
) {
const reasoningField = compat.reasoningContentField ?? "reasoning_content";
(assistantMsg as any)[reasoningField] = ".";
}
if (toolCalls.length > 0) {
assistantMsg.tool_calls = toolCalls.map(tc => ({
id: normalizeMistralToolId(tc.id, compat.requiresMistralToolIds),
@@ -611,6 +645,9 @@ export function convertMessages(
content !== null &&
content !== undefined &&
(typeof content === "string" ? content.length > 0 : content.length > 0);
if (!hasContent && assistantMsg.tool_calls && compat.requiresAssistantContentForToolCalls) {
assistantMsg.content = ".";
}
if (!hasContent && !assistantMsg.tool_calls) {
continue;
}
@@ -732,6 +769,7 @@ function detectCompat(model: Model<"openai-completions">): ResolvedOpenAICompat
const baseUrl = model.baseUrl;
const isZai = provider === "zai" || baseUrl.includes("api.z.ai");
const isOpenRouterKimi = provider === "openrouter" && model.id.includes("moonshotai/kimi");
const isNonStandard =
provider === "cerebras" ||
@@ -762,6 +800,9 @@ function detectCompat(model: Model<"openai-completions">): ResolvedOpenAICompat
requiresThinkingAsText: isMistral,
requiresMistralToolIds: isMistral,
thinkingFormat: isZai ? "zai" : "openai",
reasoningContentField: "reasoning_content",
requiresReasoningContentForToolCalls: isOpenRouterKimi,
requiresAssistantContentForToolCalls: isOpenRouterKimi,
openRouterRouting: undefined,
};
}
@@ -786,6 +827,11 @@ function getCompat(model: Model<"openai-completions">): ResolvedOpenAICompat {
requiresThinkingAsText: model.compat.requiresThinkingAsText ?? detected.requiresThinkingAsText,
requiresMistralToolIds: model.compat.requiresMistralToolIds ?? detected.requiresMistralToolIds,
thinkingFormat: model.compat.thinkingFormat ?? detected.thinkingFormat,
reasoningContentField: model.compat.reasoningContentField ?? detected.reasoningContentField,
requiresReasoningContentForToolCalls:
model.compat.requiresReasoningContentForToolCalls ?? detected.requiresReasoningContentForToolCalls,
requiresAssistantContentForToolCalls:
model.compat.requiresAssistantContentForToolCalls ?? detected.requiresAssistantContentForToolCalls,
openRouterRouting: model.compat.openRouterRouting ?? detected.openRouterRouting,
};
}
+6
View File
@@ -292,6 +292,12 @@ export interface OpenAICompat {
requiresMistralToolIds?: boolean;
/** Format for reasoning/thinking parameter. "openai" uses reasoning_effort, "zai" uses thinking: { type: "enabled" }. Default: "openai". */
thinkingFormat?: "openai" | "zai";
/** Which reasoning content field to emit on assistant messages. Default: auto-detected. */
reasoningContentField?: "reasoning_content" | "reasoning" | "reasoning_text";
/** Whether assistant tool-call messages must include reasoning content. Default: false. */
requiresReasoningContentForToolCalls?: boolean;
/** Whether assistant tool-call messages must include non-empty content. Default: false. */
requiresAssistantContentForToolCalls?: boolean;
/** OpenRouter-specific routing preferences. Only used when baseUrl points to OpenRouter. */
openRouterRouting?: OpenRouterRouting;
}