Skip to content

fix(plugin): declare llm.maxTokens and llm.headers as first-class config keys - #2248

Open
kiwipaulrob wants to merge 4 commits into
MemTensor:mainfrom
kiwipaulrob:fix/config-llm-maxtokens-headers
Open

fix(plugin): declare llm.maxTokens and llm.headers as first-class config keys#2248
kiwipaulrob wants to merge 4 commits into
MemTensor:mainfrom
kiwipaulrob:fix/config-llm-maxtokens-headers

Conversation

@kiwipaulrob

Copy link
Copy Markdown

Summary

llm.maxTokens and llm.headers are read at runtime (LLM client resolves config.maxTokens with a 1024 fallback; every provider spreads config.headers into requests) but are absent from DEFAULT_CONFIG and the config schema. Every boot logs unknown config key 'llm.maxTokens', and once headers is present, pruneUnknown() recurses into the empty default slot and warns for every user header key (unknown config key 'llm.headers.User-Agent').

This PR declares both keys as first-class config, adds defaults, and fixes pruneUnknown() so empty-object default slots are treated as free-form maps.

Change

  • core/config/defaults.ts — add maxTokens: 1024 + headers: {} to the llm tree; add maxTokens: 1024 to skillEvolver and l3Llm (both share SkillEvolverSchema).
  • core/config/schema.ts — declare maxTokens (range 16–131072, default 1024) + headers (Record<string, string>) in LlmSchema; declare maxTokens in SkillEvolverSchema.
  • core/config/index.tspruneUnknown(): an empty-object default slot is a free-form map (Record<string, string>), so keep the whole user object as-is instead of recursing and warning per key.
  • tests/unit/config/llm-max-tokens-headers.test.ts — new regression suite: acceptance without warnings, defaults, range validation, non-string header rejection, unrelated-field preservation.

Tests

  • npx vitest run tests/unit/config tests/unit/llm148 passed (10 files)
  • npx tsc -p tsconfig.json --noEmit → clean (exit 0)
  • New file: 7 tests (acceptance / default maxTokens / default headers / skillEvolver maxTokens / range rejection / header type rejection / unrelated fields)

Related

Fixes #2247

Environment

  • Plugin version: monorepo main (b4cc9bc)
  • Runtime: Node 22, npm 10

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)

How Tested

  1. npx vitest run tests/unit/config tests/unit/llm — 148 passed
  2. npx tsc -p tsconfig.json --noEmit — 0 errors
  3. Manual: resolveConfig({ llm: { maxTokens: 2048, headers: { "User-Agent": "test" } } }) returns both values with zero warnings; resolveConfig({}) yields maxTokens: 1024, headers: {}

Checklist

  • I have read the CONTRIBUTING guidelines
  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation (config template)
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective
  • New and existing unit tests pass locally

@Memtensor-AI Memtensor-AI added area:plugin OpenClaw & Hermes status:in-progress Someone or AI is working on it | 人工或 AI 正在处理 labels Aug 14, 2026
@Memtensor-AI

Copy link
Copy Markdown
Collaborator

✅ Automated Test Results: PASSED

All tests passed (7/7 executed). memos_local_plugin/unit: 7/7. Duration: 3s

Branch: fix/config-llm-maxtokens-headers

…fig keys

llm.maxTokens and llm.headers are read at runtime (client.ts reads
config.maxTokens, providers spread config.headers) but were absent from
DEFAULT_CONFIG and LlmSchema/SkillEvolverSchema, so every boot logged
"unknown config key 'llm.maxTokens'" and "unknown config key
'llm.headers.<key>'" (pruneUnknown recursed into the empty headers slot
and warned per user key). Add both to defaults + schema, and teach
pruneUnknown that an empty-object default slot is a free-form map that
must be kept as-is, eliminating the per-key warnings.

Adds a regression test covering acceptance, defaults, range validation
and the free-form-map warning suppression.
@kiwipaulrob
kiwipaulrob force-pushed the fix/config-llm-maxtokens-headers branch from 4b539bb to d59fe83 Compare August 14, 2026 19:15
@Memtensor-AI

Memtensor-AI commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator

🤖 Open Code Review

Target: PR #2248
Task: e2abe61b8ebd8925
Base: main
Head: fix/config-llm-maxtokens-headers

🔍 OpenCodeReview found 1 issue(s) in this PR.

⚠️ 3 warning(s) occurred during review.


1. apps/memos-local-plugin/core/config/index.ts (L168-L174)

The free-form-map contract ("an empty-object default slot means the field accepts arbitrary user keys") is implicit and relies on convention rather than explicit typing. Any future contributor who adds a new config field with a {} default will unknowingly opt that field into the same bypass, suppressing all unknown-key warnings for it — even if it is NOT intended to be a free-form map.

Consider making the contract explicit by introducing a sentinel value (e.g. a branded symbol FREE_FORM_MAP) in defaults.ts for intentionally free-form slots, and testing for that sentinel here instead of checking for an empty object. At minimum, add a JSDoc comment in defaults.ts on every field that uses {} to signal the intent.

💡 Suggested Change

Before:

      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;
      }

After:

// In defaults.ts, mark intentional free-form slots:
//   export const FREE_FORM_MAP: unique symbol = Symbol('FREE_FORM_MAP');
//   headers: FREE_FORM_MAP,
//
// Then here:
      const defaultSlot = (defaults as Record<string, unknown>)[k];
      if (defaultSlot === FREE_FORM_MAP ||
          (isPlainObject(defaultSlot) &&
           Object.keys(defaultSlot as Record<string, unknown>).length === 0)) {
        // Explicit free-form map slot — keep user object as-is.
        out[k] = v;
        continue;
      }

Generated by cloud-assistant via Open Code Review.

@Memtensor-AI

Copy link
Copy Markdown
Collaborator

✅ Automated Test Results: PASSED

All tests passed (7/7 executed). memos_local_plugin/unit: 7/7. Duration: 3s [advisory, non-gating] AI-generated tests on branch test/auto-gen-e795d6b24525860c-20260815034233: 72/72 passed — these do NOT affect the PR verdict; review the branch manually.

Branch: fix/config-llm-maxtokens-headers

@Memtensor-AI Memtensor-AI added status:ready Ready for implementation; waiting for assignee or AI dispatch | 可进入实现,等待认领或派发 and removed status:in-progress Someone or AI is working on it | 人工或 AI 正在处理 labels Aug 14, 2026
…ed-slot defaults to 4096

Addresses OpenCodeReview feedback on MemTensor#2248:
- The l3Llm and skillEvolver client builders constructed their clients with
  explicit field picks that dropped maxTokens and headers — the config keys
  declared by the previous commit were inert at runtime (effective cap was
  the hard-coded DEFAULT_MAX_TOKENS=1024 in client.ts regardless of config).
- Add maxTokens+headers to DedicatedLlmConfig and pass both through in the
  reflectLlm (skillEvolver) and l3Llm builders so configured values actually
  reach the provider request.
- Add headers to SkillEvolverSchema (l3Llm/skillEvolver slots) so custom
  HTTP headers are accepted on those slots, mirroring the llm slot.
- Raise l3Llm/skillEvolver maxTokens defaults from 1024 to 4096: L3 world-
  model bodies span multiple L2 policies/evidence traces, and crystallized
  skill bodies include invocation guides + procedure steps — 1024 tokens
  risks silent truncation on both workloads (both slots already assume
  60s timeouts, implying heavier calls).
- Update config tests: default assertions now pin 4096, plus new coverage
  for l3Llm.maxTokens and headers on both dedicated slots.
@Memtensor-AI Memtensor-AI added status:in-progress Someone or AI is working on it | 人工或 AI 正在处理 and removed status:ready Ready for implementation; waiting for assignee or AI dispatch | 可进入实现,等待认领或派发 labels Aug 14, 2026
@kiwipaulrob

Copy link
Copy Markdown
Author

Thanks for the review — all three findings are addressed in 1078640.

1. headers on SkillEvolverSchema — accepted and extended: headers added to SkillEvolverSchema (l3Llm/skillEvolver slots). More importantly, I found the deeper issue the review hinted at: the l3Llm and skillEvolver client builders in memory-core.ts were constructing their clients with explicit field picks that dropped maxTokens and headers entirely, so the keys declared in the previous commit were inert at runtime (effective cap was always the hard-coded DEFAULT_MAX_TOKENS=1024 in client.ts). Both builders now pass maxTokens and headers through to createLlmClient, so configured values actually reach the provider request.

2. l3Llm.maxTokens default 1024 → 4096 — accepted. L3 world-model bodies are generated from multiple L2 policies and evidence traces; 1024 was a real truncation risk. Now wired AND defaulted to 4096.

3. skillEvolver.maxTokens default 1024 → 4096 — accepted. Crystallized skill bodies include invocation guides + structured procedure steps; same truncation risk, same fix (wired + 4096).

Tests updated: config suite now pins the 4096 defaults and adds coverage for l3Llm.maxTokens and headers on both dedicated slots (70/70 pass in tests/unit/config). Typecheck clean. Pipeline test failures in this environment are pre-existing at the base commit (71 failed before and after), unrelated to this change.

@Memtensor-AI

Copy link
Copy Markdown
Collaborator

✅ Automated Test Results: PASSED

All tests passed (50/50 executed). memos_local_plugin/unit: 50/50. Duration: 11s [advisory, non-gating] AI-generated tests on branch test/auto-gen-4e07394653d2e46a-20260815040834: 0/98 passed, 98 failed — these do NOT affect the PR verdict; review the branch manually.

Branch: fix/config-llm-maxtokens-headers

@Memtensor-AI Memtensor-AI added status:ready Ready for implementation; waiting for assignee or AI dispatch | 可进入实现,等待认领或派发 and removed status:in-progress Someone or AI is working on it | 人工或 AI 正在处理 labels Aug 14, 2026
…s floor 100

Addresses remaining OpenCodeReview feedback on MemTensor#2248: headers was declared on SkillEvolverSchema but absent from the l3Llm/skillEvolver defaults, so setting those keys in YAML still warned unknown config key and bypassed the pruneUnknown free-form-map shortcut; maxTokens floor raised 16 to 100 to match the documented deepseek-v4-flash constraint; dedicated-slot headers now asserted warning-free, defaults pinned, out-of-range regression pinned at 50.
@Memtensor-AI Memtensor-AI added status:in-progress Someone or AI is working on it | 人工或 AI 正在处理 and removed status:ready Ready for implementation; waiting for assignee or AI dispatch | 可进入实现,等待认领或派发 labels Aug 15, 2026
@kiwipaulrob

Copy link
Copy Markdown
Author

Addressed the remaining OpenCodeReview findings on the new head (2b726985, fix/config-llm-maxtokens-headers):

1. maxTokens floor 16 → 100 — accepted. Both LlmSchema and SkillEvolverSchema now use NumberInRange(1024, 100, 131072), matching the documented deepseek-v4-flash constraint. The out-of-range regression test now pins maxTokens: 50 (valid under the old 16 floor, rejected now) so the boundary is actually exercised.

4 + 5. headers: {} missing from the l3Llm/skillEvolver defaults — accepted, this was a real gap. headers: {} is now in both dedicated-slot defaults, so setting l3Llm.headers / skillEvolver.headers in YAML no longer emits unknown config key and the pruneUnknown free-form-map shortcut fires for those slots too. The dedicated-slot headers test now asserts zero warnings (it would fail without the defaults fix), and a new test pins the default empty maps.

2 + 3. Arbitrary headers could override managed headers (Authorization/Content-Type) — acknowledged, deliberately out of scope for this PR. This is a local single-user plugin whose config file is user-owned; headers exists precisely so operators can inject provider/gateway-specific auth and proxy headers, and a hard block on Authorization would break legitimate setups (reverse proxies, gateway tokens). Happy to add a documented caveat or a schema-level note if the maintainers prefer a softer guard — no runtime block here.

Validation on the new head: tests/unit/config 71/71 pass (incl. the updated suite), tsc -p tsconfig.json --noEmit clean. The AutoTest/OCR bots should pick up the new head on their next run.

@Memtensor-AI

Copy link
Copy Markdown
Collaborator

✅ Automated Test Results: PASSED

All tests passed (51/51 executed). memos_local_plugin/unit: 51/51. Duration: 11s

Branch: fix/config-llm-maxtokens-headers

@Memtensor-AI Memtensor-AI added status:ready Ready for implementation; waiting for assignee or AI dispatch | 可进入实现,等待认领或派发 and removed status:in-progress Someone or AI is working on it | 人工或 AI 正在处理 labels Aug 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:plugin OpenClaw & Hermes status:ready Ready for implementation; waiting for assignee or AI dispatch | 可进入实现,等待认领或派发

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug: llm.maxTokens and llm.headers are read at runtime but undeclared in config schema (boot-time unknown-key warnings)

3 participants