fix(ai): parsed minimax think tags across providers
Promoted MiniMax model/provider detection into stream markup healing so OpenCode Zen MiniMax streams use the existing thinking-tag parser instead of exposing raw tags. Added a regression test for OpenCode Zen minimax-m3 chunks split across <think> boundaries. Fixes #2049
This commit is contained in:
@@ -537,7 +537,6 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions"> = (
|
||||
}
|
||||
stream.push({ type: "start", partial: output });
|
||||
|
||||
const parseMiniMaxThinkTags = model.provider === "minimax-code" || model.provider === "minimax-code-cn";
|
||||
// Some OpenAI-compatible DeepSeek hosts (including NVIDIA NIM and DeepSeek's
|
||||
// native API) leak chat-template tool-call markers in `delta.content` even
|
||||
// though tool calls are also surfaced structurally. Strip the leaked markers
|
||||
@@ -678,9 +677,7 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions"> = (
|
||||
}
|
||||
};
|
||||
|
||||
const streamMarkupHealingPattern = getStreamMarkupHealingPattern(model.provider, model.id, {
|
||||
parseThinkingTags: parseMiniMaxThinkTags,
|
||||
});
|
||||
const streamMarkupHealingPattern = getStreamMarkupHealingPattern(model.provider, model.id);
|
||||
const streamMarkupHealing = streamMarkupHealingPattern
|
||||
? new StreamMarkupHealing({ pattern: streamMarkupHealingPattern })
|
||||
: undefined;
|
||||
|
||||
@@ -600,12 +600,17 @@ export function modelMayLeakDsmlToolCalls(provider: string, modelId: string): bo
|
||||
);
|
||||
}
|
||||
|
||||
/** Cheap model/provider gate for MiniMax plain thinking tag leaks. */
|
||||
export function modelMayLeakThinkingTags(provider: string, modelId: string): boolean {
|
||||
return /minimax/i.test(provider) || /minimax/i.test(modelId);
|
||||
}
|
||||
|
||||
export function getStreamMarkupHealingPattern(
|
||||
provider: string,
|
||||
modelId: string,
|
||||
options?: { readonly parseThinkingTags?: boolean },
|
||||
): StreamMarkupHealingPattern | undefined {
|
||||
if (options?.parseThinkingTags) return "thinking";
|
||||
if (options?.parseThinkingTags || modelMayLeakThinkingTags(provider, modelId)) return "thinking";
|
||||
if (modelMayLeakKimiToolCalls(provider, modelId)) return "kimi";
|
||||
if (modelMayLeakDsmlToolCalls(provider, modelId)) return "dsml";
|
||||
return undefined;
|
||||
|
||||
@@ -148,6 +148,7 @@ describe("StreamMarkupHealing pattern selection", () => {
|
||||
expect(getStreamMarkupHealingPattern("minimax-code", "MiniMax-M2.5", { parseThinkingTags: true })).toBe(
|
||||
"thinking",
|
||||
);
|
||||
expect(getStreamMarkupHealingPattern("opencode-zen", "minimax-m3")).toBe("thinking");
|
||||
expect(getStreamMarkupHealingPattern("nanogpt", "deepseek/deepseek-v4-pro")).toBe("dsml");
|
||||
expect(getStreamMarkupHealingPattern("ollama-cloud", "gpt-oss:120b")).toBeUndefined();
|
||||
expect(getStreamMarkupHealingPattern("openai", "deepseek-v4-pro")).toBeUndefined();
|
||||
@@ -582,6 +583,39 @@ describe("Ollama provider DSML envelope healing", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("OpenAI completions MiniMax thinking healing", () => {
|
||||
it("parses OpenCode Zen MiniMax think tags into a thinking block", async () => {
|
||||
const model: Model<"openai-completions"> = {
|
||||
id: "minimax-m3",
|
||||
name: "MiniMax M3",
|
||||
api: "openai-completions",
|
||||
provider: "opencode-zen",
|
||||
baseUrl: "https://opencode.ai/zen/v1",
|
||||
reasoning: true,
|
||||
input: ["text"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 200_000,
|
||||
maxTokens: 8_192,
|
||||
};
|
||||
global.fetch = mockFetch([
|
||||
chunk(model.id, { content: "visible <thin" }),
|
||||
chunk(model.id, { content: "k>hidden reasoning</think" }),
|
||||
chunk(model.id, { content: ">" }),
|
||||
chunk(model.id, { content: " answer" }),
|
||||
chunk(model.id, {}, "stop"),
|
||||
"[DONE]",
|
||||
]);
|
||||
|
||||
const result = await streamOpenAICompletions(model, baseContext(), { apiKey: "test-key" }).result();
|
||||
|
||||
expect(result.content).toEqual([
|
||||
{ type: "text", text: "visible " },
|
||||
{ type: "thinking", thinking: "hidden reasoning", thinkingSignature: undefined },
|
||||
{ type: "text", text: " answer" },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("OpenAI completions provider DSML envelope healing", () => {
|
||||
it("heals the envelope into a structured tool call and suppresses leaked text", async () => {
|
||||
const model: Model<"openai-completions"> = {
|
||||
|
||||
Reference in New Issue
Block a user