From 88f7622d642a583ca2f9a6930100a371451dddd7 Mon Sep 17 00:00:00 2001 From: AutoDev Bot Date: Tue, 30 Jun 2026 01:24:34 +0800 Subject: [PATCH] fix(memos-local-openclaw): raise topic-judge max_tokens to 60 to unblock MiniMax (#1315) judgeNewTopicOpenAI and arbitrateTopicSplitOpenAI were sending max_tokens: 10. MiniMax's gateway (api.minimaxi.com) rejects requests with that small a ceiling by returning an HTML 404 page before the request ever reaches the model, producing 100% failure rate on topic boundary detection for users on MiniMax-backed openai_compatible setups. Other summarizer calls in the same file (summarize, summarizeTask, filterRelevant, judgeDedup, classifyTopic) work fine because they use larger max_tokens (or none at all). classifyTopicOpenAI already uses 60 against the same MiniMax endpoint successfully, so 60 is adopted as the new minimum for the two affected helpers. Includes regression test that stubs global fetch and asserts both helpers send max_tokens >= 60 while leaving the existing limits of the healthy callers untouched. --- .../src/ingest/providers/openai.ts | 13 +- .../tests/topic-judge-minimax-1315.test.ts | 132 ++++++++++++++++++ 2 files changed, 143 insertions(+), 2 deletions(-) create mode 100644 apps/memos-local-openclaw/tests/topic-judge-minimax-1315.test.ts diff --git a/apps/memos-local-openclaw/src/ingest/providers/openai.ts b/apps/memos-local-openclaw/src/ingest/providers/openai.ts index 825e2131d..60f878e4c 100644 --- a/apps/memos-local-openclaw/src/ingest/providers/openai.ts +++ b/apps/memos-local-openclaw/src/ingest/providers/openai.ts @@ -233,7 +233,14 @@ export async function judgeNewTopicOpenAI( body: JSON.stringify(buildRequestBody(cfg, { model, temperature: 0, - max_tokens: 10, + // NOTE: must stay >= 60. MiniMax's gateway (api.minimaxi.com) rejects + // chat-completion requests with very small max_tokens (e.g. 10) by + // returning an HTML 404 page before the request ever reaches the + // model. 60 matches classifyTopicOpenAI below — already proven to + // work against MiniMax-M2.7-highspeed — and is plenty for a one-word + // NEW/SAME reply plus any reasoning preamble the model may emit. + // See issue #1315. + max_tokens: 60, messages: [ { role: "system", content: TOPIC_JUDGE_PROMPT }, { role: "user", content: userContent }, @@ -336,7 +343,9 @@ export async function arbitrateTopicSplitOpenAI( body: JSON.stringify(buildRequestBody(cfg, { model, temperature: 0, - max_tokens: 10, + // NOTE: must stay >= 60. See note in judgeNewTopicOpenAI above — + // MiniMax's gateway returns HTML 404 for max_tokens: 10. Issue #1315. + max_tokens: 60, messages: [ { role: "system", content: TOPIC_ARBITRATION_PROMPT }, { role: "user", content: userContent }, diff --git a/apps/memos-local-openclaw/tests/topic-judge-minimax-1315.test.ts b/apps/memos-local-openclaw/tests/topic-judge-minimax-1315.test.ts new file mode 100644 index 000000000..afd4ac750 --- /dev/null +++ b/apps/memos-local-openclaw/tests/topic-judge-minimax-1315.test.ts @@ -0,0 +1,132 @@ +/** + * Regression test for issue #1315: + * Topic Judge 100% failure rate against MiniMax (api.minimaxi.com) because + * judgeNewTopicOpenAI / arbitrateTopicSplitOpenAI request max_tokens: 10, + * which MiniMax's gateway rejects with an HTML 404 page. + * + * The fix raises the minimum to 60 (matching classifyTopicOpenAI in the same + * file, which is already proven to work against MiniMax). These tests assert + * the on-the-wire request body uses at least 60 max_tokens for the two + * affected helpers; if anyone lowers them back to 10 the tests fail. + */ +import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; +import { + judgeNewTopicOpenAI, + arbitrateTopicSplitOpenAI, + classifyTopicOpenAI, + filterRelevantOpenAI, + judgeDedupOpenAI, + summarizeOpenAI, +} from "../src/ingest/providers/openai"; +import type { SummarizerConfig, Logger } from "../src/types"; + +const silentLog: Logger = { + debug: () => {}, + info: () => {}, + warn: () => {}, + error: () => {}, +}; + +const minimaxCfg: SummarizerConfig = { + provider: "openai_compatible", + model: "MiniMax-M2.7-highspeed", + endpoint: "https://api.minimaxi.com/v1", + apiKey: "test-key", +}; + +interface CapturedRequest { + url: string; + body: Record; +} + +/** + * Replace global.fetch with a recorder that returns a canned successful + * completion. Returns the captured-requests array so tests can assert on + * url / body / max_tokens. + */ +function installFetchRecorder(replyContent: string): CapturedRequest[] { + const captured: CapturedRequest[] = []; + const fakeFetch = vi.fn(async (url: string | URL, init?: RequestInit) => { + const body = init?.body ? JSON.parse(init.body as string) : {}; + captured.push({ url: String(url), body }); + return new Response( + JSON.stringify({ + choices: [{ message: { content: replyContent } }], + }), + { status: 200, headers: { "Content-Type": "application/json" } }, + ); + }); + vi.stubGlobal("fetch", fakeFetch); + return captured; +} + +describe("openai topic-judge max_tokens regression (issue #1315)", () => { + beforeEach(() => { + vi.restoreAllMocks(); + vi.unstubAllGlobals(); + }); + + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it("judgeNewTopicOpenAI sends max_tokens >= 60 (MiniMax rejects max_tokens: 10 with HTML 404)", async () => { + const captured = installFetchRecorder("SAME"); + + await judgeNewTopicOpenAI("current task context", "new user message", minimaxCfg, silentLog); + + expect(captured).toHaveLength(1); + expect(captured[0].url).toBe("https://api.minimaxi.com/v1/chat/completions"); + const maxTokens = captured[0].body.max_tokens as number; + expect(maxTokens).toBeGreaterThanOrEqual(60); + }); + + it("arbitrateTopicSplitOpenAI sends max_tokens >= 60 (same MiniMax 404 gateway behaviour)", async () => { + const captured = installFetchRecorder("NEW"); + + await arbitrateTopicSplitOpenAI("task state", "new message", minimaxCfg, silentLog); + + expect(captured).toHaveLength(1); + expect(captured[0].url).toBe("https://api.minimaxi.com/v1/chat/completions"); + const maxTokens = captured[0].body.max_tokens as number; + expect(maxTokens).toBeGreaterThanOrEqual(60); + }); + + it("judgeNewTopicOpenAI still parses single-word NEW / SAME replies after the bump", async () => { + installFetchRecorder("NEW"); + const isNew = await judgeNewTopicOpenAI("ctx", "msg", minimaxCfg, silentLog); + expect(isNew).toBe(true); + + installFetchRecorder("SAME"); + const isSame = await judgeNewTopicOpenAI("ctx", "msg", minimaxCfg, silentLog); + expect(isSame).toBe(false); + }); + + it("arbitrateTopicSplitOpenAI still normalises replies to NEW or SAME after the bump", async () => { + installFetchRecorder("NEW\n"); + expect(await arbitrateTopicSplitOpenAI("task", "msg", minimaxCfg, silentLog)).toBe("NEW"); + + installFetchRecorder("same"); + expect(await arbitrateTopicSplitOpenAI("task", "msg", minimaxCfg, silentLog)).toBe("SAME"); + }); + + it("other openai helpers keep their existing max_tokens limits (no regression on healthy callers)", async () => { + const a = installFetchRecorder("60"); + await classifyTopicOpenAI("task", "msg", minimaxCfg, silentLog); + // classifyTopic was already 60 — should remain at least 60. + expect(a[0].body.max_tokens as number).toBeGreaterThanOrEqual(60); + + const b = installFetchRecorder('{"relevant":[],"sufficient":false}'); + await filterRelevantOpenAI("q", [{ index: 1, role: "user", content: "c" }], minimaxCfg, silentLog); + expect(b[0].body.max_tokens as number).toBeGreaterThanOrEqual(200); + + const c = installFetchRecorder('{"action":"NEW","reason":""}'); + await judgeDedupOpenAI("new", [{ index: 1, summary: "s", chunkId: "x" }], minimaxCfg, silentLog); + expect(c[0].body.max_tokens as number).toBeGreaterThanOrEqual(300); + + const d = installFetchRecorder("hello world summary"); + await summarizeOpenAI("input text", minimaxCfg, silentLog); + // summarize does not set max_tokens (server default) — assert the field is absent / unset. + expect(d[0].body.max_tokens).toBeUndefined(); + }); +});