Skip to content

[Bug]: lookupContextTokens() ignores config contextWindow — returns API-discovered values instead #17404

@michaelbship

Description

@michaelbship

Summary

MODEL_CACHE in src/agents/context.ts is populated exclusively from modelRegistry.getAll() (API/catalog discovery) and never reads contextWindow values from models.providers[].models[] in openclaw.json. Operator overrides are silently ignored by all 16 callsites that resolve context limits through lookupContextTokens().

Environment

  • OpenClaw: 2026.2.13
  • Provider: OpenRouter (also affects any provider reporting incorrect context windows)
  • File: src/agents/context.ts

Steps to Reproduce

  1. Configure an OpenRouter model with an explicit contextWindow override:
    {
      "models": {
        "providers": {
          "openrouter": {
            "models": [{
              "id": "anthropic/claude-opus-4-6",
              "contextWindow": 200000
            }]
          }
        }
      }
    }
  2. Start a session using that model
  3. Run /status

Expected Behavior

Context window shows 200,000 — the operator-configured value. All runtime decisions (compaction, memory flush, session persistence) use this value.

Actual Behavior

Context window shows the API-discovered value (e.g., 1,000,000 or 2,000,000 from OpenRouter). The config contextWindow is never consulted by lookupContextTokens().

Root Cause

In src/agents/context.ts, the MODEL_CACHE population loop (inside the async IIFE at lines 11-27) reads exclusively from modelRegistry.getAll():

const modelRegistry = discoverModels(authStorage, agentDir);
const models = modelRegistry.getAll() as ModelEntry[];
for (const m of models) {
  if (!m?.id) continue;
  if (typeof m.contextWindow === "number" && m.contextWindow > 0) {
    MODEL_CACHE.set(m.id, m.contextWindow);
  }
}

loadConfig() is called on line 14 but only used for ensureOpenClawModelsJson(cfg). The config's models.providers[].models[].contextWindow values are never written into MODEL_CACHE.

Impact

lookupContextTokens() is called from 16 sites across 12 files. Getting the wrong value is not a display bug — it controls runtime behavior:

Subsystem File Effect of wrong value
Session defaults gateway/session-utils.ts New sessions get wrong contextTokens
Compaction threshold auto-reply/reply/model-selection.ts Compaction fires too late (inflated) or too early (deflated)
Session persistence auto-reply/reply/directive-handling.persist.ts Wrong contextTokens stored in session record
Memory flush auto-reply/reply/memory-flush.ts Flush timing miscalculated
Agent runner auto-reply/reply/agent-runner.ts Wrong context limit for agent execution
Followup runner auto-reply/reply/followup-runner.ts Wrong context limit for followup agents
Cron agents cron/isolated-agent/run.ts Wrong context limit for scheduled runs
Status display auto-reply/status.ts /status shows wrong context percentage
Session listing commands/sessions.ts Session list shows wrong limits
Status summary commands/status.summary.ts Summary shows wrong limits

When inflated (e.g., 2M instead of 200K), compaction never fires and the session grows until the API rejects it with a context-length error.

Suggested Fix

After the API discovery loop populates MODEL_CACHE, do a second pass over config models.providers and overwrite cache entries where the operator has set contextWindow:

const providers = cfg.models?.providers;
if (providers) {
  for (const provider of Object.values(providers)) {
    if (!provider?.models) continue;
    for (const model of provider.models) {
      if (model?.id && typeof model.contextWindow === "number" && model.contextWindow > 0) {
        MODEL_CACHE.set(model.id, model.contextWindow);
      }
    }
  }
}

This runs after the discovery loop, so operator config always wins. The semantics are simple: explicit config overrides implicit discovery.

Workaround

None that covers all callsites. resolveContextWindowInfo() (used by the embedded runner display path) does read config, but the 16 lookupContextTokens() callsites do not.

Related Issues

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions