Skip to content

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

Description

@kiwipaulrob

Bug Description

llm.maxTokens and llm.headers are read at runtime by the plugin (the LLM client resolves config.maxTokens with a 1024 fallback, and every provider spreads config.headers into outgoing requests) but they are not declared in DEFAULT_CONFIG or in LlmSchema/SkillEvolverSchema.

Root Cause

core/llm/client.ts reads opts?.maxTokens ?? config.maxTokens ?? DEFAULT_MAX_TOKENS and core/llm/providers/openai.ts (plus anthropic/gemini/bedrock) do Object.assign(headers, config.headers). However:

  • core/config/defaults.ts — the llm / skillEvolver / l3Llm trees have no maxTokens or headers keys.
  • core/config/schema.tsLlmSchema/SkillEvolverSchema only declare reasoning.maxTokens (a different, nested key); top-level maxTokens and headers are absent.
  • core/config/index.ts pruneUnknown() — an unknown key is kept "as-is for forward compatibility" but logs a warning; worse, when the default slot is an empty object (headers: {} after the fix), the recursion warns for every user-supplied header key (unknown config key 'llm.headers.User-Agent').

Impact

  • Every boot logs unknown config key 'llm.maxTokens' (and 'llm.headers.*' once headers are configured) — cosmetic noise that looks like a misconfiguration.
  • The keys are unvalidated: out-of-range maxTokens values or non-string header values pass silently until the provider rejects the request.

Environment

  • Plugin version: 2.0.15 (npm train) / monorepo main (b4cc9bc)
  • Trigger: any config with llm.maxTokens or llm.headers set

Suggested Fix

  1. Add maxTokens: 1024 + headers: {} to the llm defaults tree (and maxTokens: 1024 to skillEvolver/l3Llm, which share SkillEvolverSchema).
  2. Declare maxTokens: NumberInRange(1024, 16, 131072) and headers: Type.Optional(Type.Record(Type.String(), Type.String(), { default: {} })) in LlmSchema; declare maxTokens in SkillEvolverSchema.
  3. In pruneUnknown(), treat an empty-object default slot as a free-form map (Record<string, string>) and keep the user's object as-is instead of recursing per key.

See PR (linked below) which implements exactly this with a regression test.

Metadata

Metadata

Labels

area:pluginOpenClaw & Hermesstatus:needs-triageNeeds initial triage | 需要初步判断 & 问题复现

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions