feat(ai): strip MiniMax inline reasoning blocks

This commit is contained in:
Dave 2026-09-16 15:51:37 +02:00
parent b5593457af
commit 8f1ac43e98
2 changed files with 67 additions and 0 deletions

View file

@ -0,0 +1,44 @@
import { describe, it, expect } from "vitest";
import { stripThinking } from "@/lib/ai/think-strip";
describe("stripThinking", () => {
it("leaves ordinary text alone", () => {
expect(stripThinking("Fixed the quote form.")).toBe("Fixed the quote form.");
});
it("removes a think block and trims what is left", () => {
expect(
stripThinking("<think>Let me consider this.</think>\nFixed the quote form.")
).toBe("Fixed the quote form.");
});
it("removes several blocks", () => {
expect(stripThinking("<think>a</think>One.<think>b</think>Two.")).toBe(
"One.Two."
);
});
it("handles a block spanning newlines", () => {
expect(stripThinking("<think>\nline one\nline two\n</think>Result.")).toBe(
"Result."
);
});
it("drops an unclosed block rather than showing it", () => {
// A truncated response can leave <think> open. Showing the reasoning is
// worse than showing nothing — the client sees the model talking to itself.
expect(stripThinking("Good.<think>Now let me reconsider")).toBe("Good.");
});
it("is case-insensitive about the tag", () => {
expect(stripThinking("<THINK>x</THINK>Done.")).toBe("Done.");
});
it("returns an empty string for content that is only thinking", () => {
expect(stripThinking("<think>all of it</think>")).toBe("");
});
it("handles empty input", () => {
expect(stripThinking("")).toBe("");
});
});

23
src/lib/ai/think-strip.ts Normal file
View file

@ -0,0 +1,23 @@
/**
* Removes <think>...</think> reasoning from a MiniMax response.
*
* MiniMax M2.x emits its chain of thought inline in the normal content field.
* Anthropic returns reasoning as separate blocks, so this is a provider quirk
* and belongs to the OpenAI-compatible adapter alone. Left in, every commit
* summary a client reads would open with the model deliberating.
*
* Mega's equivalent handles a STREAM, where a tag can split across chunk
* boundaries ("<thi" + "nk>") and text must be held back. This phase does not
* stream, so the whole string is available and the simple form is correct.
*/
const THINK_BLOCK = /<think>[\s\S]*?<\/think>/gi;
/** A response truncated mid-reasoning leaves an unclosed tag. */
const UNCLOSED_THINK = /<think>[\s\S]*$/i;
export function stripThinking(text: string): string {
return (text ?? "")
.replace(THINK_BLOCK, "")
.replace(UNCLOSED_THINK, "")
.trim();
}