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