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:
@@ -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
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user