From 8f1ac43e983d7119b0f2df748e91f4e1b973ba64 Mon Sep 17 00:00:00 2001 From: Dave Date: Wed, 16 Sep 2026 15:51:37 +0200 Subject: [PATCH] feat(ai): strip MiniMax inline reasoning blocks --- src/lib/ai/think-strip.test.ts | 44 ++++++++++++++++++++++++++++++++++ src/lib/ai/think-strip.ts | 23 ++++++++++++++++++ 2 files changed, 67 insertions(+) create mode 100644 src/lib/ai/think-strip.test.ts create mode 100644 src/lib/ai/think-strip.ts diff --git a/src/lib/ai/think-strip.test.ts b/src/lib/ai/think-strip.test.ts new file mode 100644 index 0000000..473f8cf --- /dev/null +++ b/src/lib/ai/think-strip.test.ts @@ -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("Let me consider this.\nFixed the quote form.") + ).toBe("Fixed the quote form."); + }); + + it("removes several blocks", () => { + expect(stripThinking("aOne.bTwo.")).toBe( + "One.Two." + ); + }); + + it("handles a block spanning newlines", () => { + expect(stripThinking("\nline one\nline two\nResult.")).toBe( + "Result." + ); + }); + + it("drops an unclosed block rather than showing it", () => { + // A truncated response can leave open. Showing the reasoning is + // worse than showing nothing — the client sees the model talking to itself. + expect(stripThinking("Good.Now let me reconsider")).toBe("Good."); + }); + + it("is case-insensitive about the tag", () => { + expect(stripThinking("xDone.")).toBe("Done."); + }); + + it("returns an empty string for content that is only thinking", () => { + expect(stripThinking("all of it")).toBe(""); + }); + + it("handles empty input", () => { + expect(stripThinking("")).toBe(""); + }); +}); \ No newline at end of file diff --git a/src/lib/ai/think-strip.ts b/src/lib/ai/think-strip.ts new file mode 100644 index 0000000..dc220b1 --- /dev/null +++ b/src/lib/ai/think-strip.ts @@ -0,0 +1,23 @@ +/** + * Removes ... 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 ("") 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 = /[\s\S]*?<\/think>/gi; +/** A response truncated mid-reasoning leaves an unclosed tag. */ +const UNCLOSED_THINK = /[\s\S]*$/i; + +export function stripThinking(text: string): string { + return (text ?? "") + .replace(THINK_BLOCK, "") + .replace(UNCLOSED_THINK, "") + .trim(); +} \ No newline at end of file