Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions apps/memos-local-plugin/core/config/defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export const DEFAULT_CONFIG: ResolvedConfig = {
apiKey: "",
timeoutMs: 45_000,
maxRetries: 3,
maxTokens: 4_000,
providerIgnore: [],
providerOrder: [],
openRouter: false,
Expand All @@ -60,6 +61,7 @@ export const DEFAULT_CONFIG: ResolvedConfig = {
apiKey: "",
temperature: 0,
timeoutMs: 60_000,
maxTokens: 4_000,
providerIgnore: [],
providerOrder: [],
openRouter: false,
Expand All @@ -74,6 +76,7 @@ export const DEFAULT_CONFIG: ResolvedConfig = {
apiKey: "",
temperature: 0,
timeoutMs: 60_000,
maxTokens: 4_000,
providerIgnore: [],
providerOrder: [],
openRouter: false,
Expand Down
10 changes: 10 additions & 0 deletions apps/memos-local-plugin/core/config/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,11 @@ const LlmSchema = Type.Object({
timeoutMs: NumberInRange(45_000, 1_000),
/** Max retries on transient errors. */
maxRetries: NumberInRange(3, 0, 10),
/**
* Default output-token budget for LLM calls. A larger default prevents
* reasoning models from truncating structured JSON responses.
*/
maxTokens: NumberInRange(4_000, 1_024, 32_768),
/** OpenRouter provider routing — providers to skip. */
providerIgnore: Type.Optional(Type.Array(Type.String(), { default: [] })),
/** OpenRouter provider routing — preferred order. */
Expand Down Expand Up @@ -123,6 +128,11 @@ const SkillEvolverSchema = Type.Object({
apiKey: StringWithDefault(""),
temperature: NumberInRange(0, 0, 2),
timeoutMs: NumberInRange(60_000, 1_000),
/**
* Default output-token budget for dedicated skill-evolver and L3 calls.
* These calls commonly return larger structured JSON documents.
*/
maxTokens: NumberInRange(4_000, 1_024, 32_768),
/** OpenRouter provider routing — providers to skip. */
providerIgnore: Type.Optional(Type.Array(Type.String(), { default: [] })),
/** OpenRouter provider routing — preferred order. */
Expand Down
3 changes: 3 additions & 0 deletions apps/memos-local-plugin/core/pipeline/memory-core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ type DedicatedLlmConfig = {
apiKey?: string;
temperature?: number;
timeoutMs?: number;
maxTokens?: number;
providerIgnore?: string[];
providerOrder?: string[];
openRouter?: boolean;
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
19 changes: 19 additions & 0 deletions apps/memos-local-plugin/tests/unit/config/load.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
11 changes: 11 additions & 0 deletions apps/memos-local-plugin/tests/unit/llm/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -102,6 +104,7 @@ l3Llm:
openRouter: true
model: l3-model
apiKey: sk-test
maxTokens: 7000
providerIgnore:
- novita
providerOrder:
Expand All @@ -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 () => {
Expand Down