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
4 changes: 4 additions & 0 deletions apps/memos-local-plugin/core/config/defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ export const DEFAULT_CONFIG: ResolvedConfig = {
providerIgnore: [],
providerOrder: [],
openRouter: false,
maxTokens: 1024,
headers: {},
},
l3Llm: {
// Empty by default — falls back to the shared `llm` settings.
Expand All @@ -73,6 +75,7 @@ export const DEFAULT_CONFIG: ResolvedConfig = {
providerIgnore: [],
providerOrder: [],
openRouter: false,
maxTokens: 1024,
},
skillEvolver: {
// Empty by default — falls back to the shared `llm` settings.
Expand All @@ -87,6 +90,7 @@ export const DEFAULT_CONFIG: ResolvedConfig = {
providerIgnore: [],
providerOrder: [],
openRouter: false,
maxTokens: 1024,
},
storage: {
ftsTokenizer: "trigram",
Expand Down
7 changes: 7 additions & 0 deletions apps/memos-local-plugin/core/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,13 @@ function pruneUnknown(
continue;
}
if (isPlainObject(v) && isPlainObject((defaults as Record<string, unknown>)[k])) {
if (Object.keys((defaults as Record<string, unknown>)[k] as Record<string, unknown>).length === 0) {
// Empty-object default slot = free-form map (e.g. llm.headers, a
// Record<string,string>). Keep the whole user object as-is; recursing
// would warn on every user key.
out[k] = v;
continue;
}
out[k] = pruneUnknown(v, (defaults as Record<string, unknown>)[k], path, warnings);
} else {
out[k] = v;
Expand Down
6 changes: 6 additions & 0 deletions apps/memos-local-plugin/core/config/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,10 @@ const LlmSchema = Type.Object({
openRouter: Type.Optional(Bool(false)),
/** Optional reasoning control (see ReasoningSchema). Omit = model default. */
reasoning: Type.Optional(ReasoningSchema),
/** Max output tokens per completion (deepseek-v4-flash needs >= 100). */
maxTokens: NumberInRange(1024, 16, 131072),
/** Extra HTTP headers for the provider request. */
headers: Type.Optional(Type.Record(Type.String(), Type.String(), { default: {} })),
}, { default: {} });

/**
Expand Down Expand Up @@ -131,6 +135,8 @@ const SkillEvolverSchema = Type.Object({
openRouter: Type.Optional(Bool(false)),
/** Optional reasoning control (see ReasoningSchema). Omit = model default. */
reasoning: Type.Optional(ReasoningSchema),
/** Max output tokens per completion. */
maxTokens: NumberInRange(1024, 16, 131072),
}, { default: {} });

const StorageSchema = Type.Object({
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { describe, expect, it } from "vitest";

import { DEFAULT_CONFIG, resolveConfig } from "../../../core/config/index.js";

describe("resolveConfig llm.maxTokens + llm.headers", () => {
it("accepts llm.maxTokens and llm.headers without unknown-key warnings", () => {
const warnings: string[] = [];
const cfg = resolveConfig(
{
llm: {
maxTokens: 2048,
headers: { "User-Agent": "hermes-test", "X-Custom": "v1" },
},
},
warnings,
);
expect(cfg.llm.maxTokens).toBe(2048);
expect(cfg.llm.headers).toEqual({ "User-Agent": "hermes-test", "X-Custom": "v1" });
// The free-form-map special case must not warn per header key.
expect(warnings).toEqual([]);
});

it("declares llm.maxTokens with a sane default of 1024", () => {
expect(DEFAULT_CONFIG.llm.maxTokens).toBe(1024);
const cfg = resolveConfig({});
expect(cfg.llm.maxTokens).toBe(1024);
});

it("declares llm.headers defaulting to an empty map", () => {
expect(DEFAULT_CONFIG.llm.headers).toEqual({});
const cfg = resolveConfig({});
expect(cfg.llm.headers).toEqual({});
});

it("declares skillEvolver.maxTokens (default 1024) for the crystallizer LLM slot", () => {
expect(DEFAULT_CONFIG.skillEvolver.maxTokens).toBe(1024);
const cfg = resolveConfig({ skillEvolver: { maxTokens: 4096 } });
expect(cfg.skillEvolver.maxTokens).toBe(4096);
});

it("rejects out-of-range maxTokens with config_invalid", () => {
expect(() => resolveConfig({ llm: { maxTokens: 8 } })).toThrow(/config failed schema validation/);
});

it("rejects non-string header values", () => {
expect(() => resolveConfig({ llm: { headers: { "X-Bad": 42 } } })).toThrow(
/config failed schema validation/,
);
});

it("keeps unrelated llm fields untouched when maxTokens/headers are set", () => {
const cfg = resolveConfig({
llm: { provider: "openai_compatible", model: "deepseek-v4-flash", maxTokens: 2048 },
});
expect(cfg.llm.provider).toBe("openai_compatible");
expect(cfg.llm.model).toBe("deepseek-v4-flash");
expect(cfg.llm.temperature).toBe(0);
expect(cfg.llm.fallbackToHost).toBe(true);
expect(cfg.llm.timeoutMs).toBe(45_000);
expect(cfg.llm.maxRetries).toBe(3);
});
});
Loading