From cbc170818f39630671a1e4405850bbc87edd70ad Mon Sep 17 00:00:00 2001 From: chouti Date: Mon, 8 Jun 2026 19:06:03 +0800 Subject: [PATCH 1/2] fix(config): add maxTokens to LlmSchema & SkillEvolverSchema Reasoning models (deepseek-reasoner, o1*, gpt-5-thinking) consume hundreds of tokens on chain-of-thought. The hard-coded 1024 cap on LLM JSON reflection caused 73+ 'llm.json malformed' errors per episode, blocking episode closure and disconnecting the bridge. - Add maxTokens: NumberInRange(4000, 1024, 32768) to LlmSchema - Add maxTokens: NumberInRange(4000, 1024, 32768) to SkillEvolverSchema - Add corresponding defaults in defaults.ts (backward compat) TypeBox's Value.Default preserves user-supplied fields, so old configs without maxTokens get the 4000 default; new configs can override. --- apps/memos-local-plugin/core/config/defaults.ts | 2 ++ apps/memos-local-plugin/core/config/schema.ts | 15 +++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/apps/memos-local-plugin/core/config/defaults.ts b/apps/memos-local-plugin/core/config/defaults.ts index 1cf2d2cf6..fe43147a2 100644 --- a/apps/memos-local-plugin/core/config/defaults.ts +++ b/apps/memos-local-plugin/core/config/defaults.ts @@ -41,6 +41,7 @@ export const DEFAULT_CONFIG: ResolvedConfig = { apiKey: "", timeoutMs: 45_000, maxRetries: 3, + maxTokens: 4_000, }, skillEvolver: { // Empty by default — falls back to the shared `llm` settings. @@ -52,6 +53,7 @@ export const DEFAULT_CONFIG: ResolvedConfig = { apiKey: "", temperature: 0, timeoutMs: 60_000, + maxTokens: 4_000, }, algorithm: { lightweightMemory: { diff --git a/apps/memos-local-plugin/core/config/schema.ts b/apps/memos-local-plugin/core/config/schema.ts index 7c9ff193b..daffa0f75 100644 --- a/apps/memos-local-plugin/core/config/schema.ts +++ b/apps/memos-local-plugin/core/config/schema.ts @@ -65,6 +65,14 @@ const LlmSchema = Type.Object({ timeoutMs: NumberInRange(45_000, 1_000), /** Max retries on transient errors. */ maxRetries: NumberInRange(3, 0, 10), + /** + * Max output tokens per LLM call. Default 4_000 — sufficient budget for + * reasoning models (deepseek-reasoner, o1*, gpt-5-thinking) that burn + * hundreds of tokens on chain-of-thought before content. Below ~2_000 + * the JSON reflection / synth prompts get truncated mid-string and the + * bridge logs `llm.json malformed`. + */ + maxTokens: NumberInRange(4_000, 1_024, 32_768), }, { default: {} }); /** @@ -89,6 +97,13 @@ const SkillEvolverSchema = Type.Object({ apiKey: StringWithDefault(""), temperature: NumberInRange(0, 0, 2), timeoutMs: NumberInRange(60_000, 1_000), + /** + * Max output tokens per skill-evolver LLM call. Same reasoning budget + * as `llm.maxTokens` — 4_000 covers typical crystallisation JSON + * (multi-policy evidence rollup + decision rationale) without hitting + * the 32k OpenAI ceiling. + */ + maxTokens: NumberInRange(4_000, 1_024, 32_768), }, { default: {} }); const AlgorithmSchema = Type.Object({ From 6e1f4a1b8f64cc81360a6e1e64b089eb8dfb3def Mon Sep 17 00:00:00 2001 From: jiachengzhen Date: Thu, 6 Aug 2026 19:49:03 +0800 Subject: [PATCH 2/2] fix(plugin): wire configurable LLM output budgets --- .../core/pipeline/memory-core.ts | 3 +++ .../tests/unit/config/load.test.ts | 19 +++++++++++++++++++ .../tests/unit/llm/client.test.ts | 11 +++++++++++ .../pipeline/bootstrap-llm-config.test.ts | 8 ++++++++ 4 files changed, 41 insertions(+) diff --git a/apps/memos-local-plugin/core/pipeline/memory-core.ts b/apps/memos-local-plugin/core/pipeline/memory-core.ts index a0354bf64..b14c2e132 100644 --- a/apps/memos-local-plugin/core/pipeline/memory-core.ts +++ b/apps/memos-local-plugin/core/pipeline/memory-core.ts @@ -133,6 +133,7 @@ type DedicatedLlmConfig = { apiKey?: string; temperature?: number; timeoutMs?: number; + maxTokens?: number; providerIgnore?: string[]; providerOrder?: string[]; openRouter?: boolean; @@ -433,6 +434,7 @@ export async function bootstrapMemoryCoreFull( apiKey: evolver?.apiKey ?? "", temperature: evolver?.temperature ?? 0, timeoutMs: evolver?.timeoutMs ?? 60_000, + maxTokens: evolver?.maxTokens, providerIgnore: evolver?.providerIgnore, providerOrder: evolver?.providerOrder, openRouter: evolver?.openRouter ?? false, @@ -495,6 +497,7 @@ export async function bootstrapMemoryCoreFull( apiKey: l3c?.apiKey ?? "", temperature: l3c?.temperature ?? 0, timeoutMs: l3c?.timeoutMs ?? 60_000, + maxTokens: l3c?.maxTokens, providerIgnore: l3c?.providerIgnore, providerOrder: l3c?.providerOrder, openRouter: l3c?.openRouter ?? false, diff --git a/apps/memos-local-plugin/tests/unit/config/load.test.ts b/apps/memos-local-plugin/tests/unit/config/load.test.ts index 5d85fd6e0..69b9d9478 100644 --- a/apps/memos-local-plugin/tests/unit/config/load.test.ts +++ b/apps/memos-local-plugin/tests/unit/config/load.test.ts @@ -56,6 +56,25 @@ describe("config/loadConfig", () => { expect(cfg.embedding.openRouter).toBe(false); }); + it("defaults and validates LLM output-token budgets", () => { + const defaults = resolveConfig({}); + expect(defaults.llm.maxTokens).toBe(4_000); + expect(defaults.skillEvolver.maxTokens).toBe(4_000); + expect(defaults.l3Llm.maxTokens).toBe(4_000); + + const configured = resolveConfig({ + llm: { maxTokens: 2_048 }, + skillEvolver: { maxTokens: 8_192 }, + l3Llm: { maxTokens: 16_384 }, + }); + expect(configured.llm.maxTokens).toBe(2_048); + expect(configured.skillEvolver.maxTokens).toBe(8_192); + expect(configured.l3Llm.maxTokens).toBe(16_384); + + expect(() => resolveConfig({ llm: { maxTokens: 1_023 } })).toThrow(/schema validation/); + expect(() => resolveConfig({ skillEvolver: { maxTokens: 32_769 } })).toThrow(/schema validation/); + }); + it("merges YAML over defaults and preserves unspecified branches", async () => { const yaml = ` viewer: diff --git a/apps/memos-local-plugin/tests/unit/llm/client.test.ts b/apps/memos-local-plugin/tests/unit/llm/client.test.ts index cd125ecd8..b12999e31 100644 --- a/apps/memos-local-plugin/tests/unit/llm/client.test.ts +++ b/apps/memos-local-plugin/tests/unit/llm/client.test.ts @@ -96,6 +96,17 @@ describe("llm/client", () => { expect(fake.lastMessages).toEqual([{ role: "user", content: "hi there" }]); }); + it("uses the configured maxTokens unless the call overrides it", async () => { + const fake = new FakeProvider("openai_compatible", () => ({ text: "ok", durationMs: 1 })); + const client = createLlmClientWithProvider(cfg({ maxTokens: 4_000 }), fake); + + await client.complete("configured budget"); + expect(fake.lastInput?.maxTokens).toBe(4_000); + + await client.complete("call override", { maxTokens: 6_000 }); + expect(fake.lastInput?.maxTokens).toBe(6_000); + }); + it("injects json hints into system and user messages when jsonMode=true", async () => { const fake = new FakeProvider("openai_compatible", () => ({ text: '{"ok":1}', durationMs: 1 })); const client = createLlmClientWithProvider(cfg(), fake); diff --git a/apps/memos-local-plugin/tests/unit/pipeline/bootstrap-llm-config.test.ts b/apps/memos-local-plugin/tests/unit/pipeline/bootstrap-llm-config.test.ts index 512c4cdde..71829cd9a 100644 --- a/apps/memos-local-plugin/tests/unit/pipeline/bootstrap-llm-config.test.ts +++ b/apps/memos-local-plugin/tests/unit/pipeline/bootstrap-llm-config.test.ts @@ -86,12 +86,14 @@ describe("bootstrapMemoryCore dedicated LLM config", () => { llm: provider: local_only model: main + maxTokens: 5000 skillEvolver: provider: openai_compatible endpoint: https://openrouter.ai/api/v1 openRouter: true model: skill-model apiKey: sk-test + maxTokens: 6000 providerIgnore: - together providerOrder: @@ -102,6 +104,7 @@ l3Llm: openRouter: true model: l3-model apiKey: sk-test + maxTokens: 7000 providerIgnore: - novita providerOrder: @@ -120,16 +123,21 @@ l3Llm: }); expect(capturedLlmConfigs.find((cfg) => cfg.model === "skill-model")).toMatchObject({ + maxTokens: 6_000, providerIgnore: ["together"], providerOrder: ["anthropic"], openRouter: true, }); expect(capturedLlmConfigs.find((cfg) => cfg.model === "l3-model")).toMatchObject({ + maxTokens: 7_000, providerIgnore: ["novita"], providerOrder: ["openai"], openRouter: true, reasoning: { enabled: true, maxTokens: 4_000 }, }); + expect(capturedLlmConfigs.find((cfg) => cfg.model === "main")).toMatchObject({ + maxTokens: 5_000, + }); }); it("normalizes missing dedicated OpenRouter flags to false", async () => {