openclaw - ✅(Solved) Fix [Bug] Telegram /model picker shows stale catalog: loadModelCatalog module-level cache never invalidates on config change [2 pull requests, 1 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
openclaw/openclaw#69750Fetched 2026-04-22 07:48:43
View on GitHub
Comments
0
Participants
1
Timeline
2
Reactions
0
Author
Participants
Timeline (top)
cross-referenced ×2

loadModelCatalog() caches the resolved catalog at the module level for the lifetime of the gateway process. The Telegram /model picker calls loadModelCatalog({ config: cfg }) without useCache: false, so newly added models under models.providers.* (or agents.defaults.models) never appear in the Telegram inline keyboard until the gateway restarts — even though the same models are present in the active config, in the per-agent agents/<id>/agent/models.json cache, and in openclaw models list.

Only a gateway restart clears the in-memory modelCatalogPromise. There is no config-change hook, no TTL, and no fallthrough for the Telegram picker path.

Root Cause

Root cause (from dist source, 2026.4.15)

Fix Action

Fix / Workaround

Why agents.defaults.models workaround does NOT save you

Workaround for affected users

PR fix notes

PR #69761: fix(telegram): bypass model catalog cache in /model picker (#69750)

Description (problem / solution / changelog)

fix(telegram): bypass model catalog cache in /model picker (#69750)

The Telegram /model picker was calling loadModelCatalog without useCache: false, which caused it to serve stale model lists after config changes. Other picker UI paths (model-picker.js, auth-choice.js) already passed useCache: false.

  • Pass useCache: false in buildModelsProviderData
  • Add regression test to ensure cache is bypassed

Fixes #69750

Changed files

  • extensions/ollama/src/stream.test.ts (modified, +36/-0)
  • extensions/ollama/src/stream.ts (modified, +1/-1)
  • src/auto-reply/reply/commands-models.test.ts (modified, +8/-0)
  • src/auto-reply/reply/commands-models.ts (modified, +1/-1)

PR #69917: fix(models): bypass catalog cache when building /models picker data

Description (problem / solution / changelog)

Summary

Fixes #69750. `loadModelCatalog` caches the resolved catalog at module scope for the lifetime of the gateway process with no TTL and no config-change hook. `buildModelsProviderData` — called by Telegram, Discord, and Mattermost `/model` picker paths via `openclaw/plugin-sdk/models-provider-runtime` — passed the config but not `useCache: false`, so newly-added models under `models.providers.*` or `agents.defaults.models` never appeared in the inline keyboard until a gateway restart.

The web UI path in `src/flows/model-picker.ts:432,617` already passes `useCache: false` for the same reason, so this aligns channel pickers with the established pattern.

Fix

One-line change in `src/auto-reply/reply/commands-models.ts:buildModelsProviderData` — pass `useCache: false` when loading the catalog.

Scope note (shared-helper rule)

`buildModelsProviderData` is imported by 3 channel adapters (Telegram, Discord, Mattermost) — all of them are `/model` pickers, all affected by the same bug, all benefit from the fix. Single-point fix is the right shape vs. three separate per-adapter wraps. Verified via `grep -rn buildModelsProviderData` — no non-picker callers.

Test

Added a regression test in `commands-models.test.ts` asserting that `loadModelCatalog` is invoked with `useCache: false` when `/models` runs through the telegram surface. Existing 18 tests in the file pass locally (oxlint clean).

Closes #69750.

Changed files

  • src/auto-reply/reply/commands-models.test.ts (modified, +13/-0)
  • src/auto-reply/reply/commands-models.ts (modified, +5/-1)

Code Example

let modelCatalogPromise = null;  // module-level, lives for the process

async function loadModelCatalog(params) {
  if (params?.useCache === false) modelCatalogPromise = null;
  if (modelCatalogPromise) return modelCatalogPromise;
  modelCatalogPromise = (async () => { /* build catalog */ })();
  return modelCatalogPromise;
}

---

systemctl --user restart openclaw-gateway
# or
openclaw gateway restart
RAW_BUFFERClick to expand / collapse

Summary

loadModelCatalog() caches the resolved catalog at the module level for the lifetime of the gateway process. The Telegram /model picker calls loadModelCatalog({ config: cfg }) without useCache: false, so newly added models under models.providers.* (or agents.defaults.models) never appear in the Telegram inline keyboard until the gateway restarts — even though the same models are present in the active config, in the per-agent agents/<id>/agent/models.json cache, and in openclaw models list.

Only a gateway restart clears the in-memory modelCatalogPromise. There is no config-change hook, no TTL, and no fallthrough for the Telegram picker path.

Environment

  • OpenClaw: 2026.4.15 (041266a)
  • Runtime: Node 22.22.2
  • OS: Linux (Ubuntu aarch64), systemd user service openclaw-gateway.service
  • Channel: Telegram
  • Config mode: models.mode: "merge" with a custom OpenAI-compatible proxy under models.providers.litellm (private deployment — irrelevant to the bug)

Reproduction

  1. Start the gateway with a config that includes provider foo with models [A, B], and agents.defaults.models containing foo/A, foo/B.
  2. In Telegram, /model → inline keyboard shows foo/A, foo/B. ✅
  3. Stop editing in Telegram. Edit openclaw.json: add model C under models.providers.foo.models[] and add foo/C to agents.defaults.models.
  4. Do not restart the gateway. Confirm the config is loaded (new file mtime, gateway process start time earlier than edit).
  5. CLI: openclaw models list --json → shows foo/C with available: true, missing: false. ✅ (CLI spawns a fresh process → fresh cache)
  6. In Telegram, /model → keyboard still shows foo/A, foo/B only. ❌ foo/C never appears.
  7. systemctl --user restart openclaw-gateway/model now shows foo/C. ✅

Root cause (from dist source, 2026.4.15)

dist/model-catalog-CdCqmHkW.js:11-38

let modelCatalogPromise = null;  // module-level, lives for the process

async function loadModelCatalog(params) {
  if (params?.useCache === false) modelCatalogPromise = null;
  if (modelCatalogPromise) return modelCatalogPromise;
  modelCatalogPromise = (async () => { /* build catalog */ })();
  return modelCatalogPromise;
}

The cache is a single module-level promise with no invalidation tied to config reload, file watchers, meta.lastTouchedAt diffs, or config hashes. Only explicit useCache: false or a test helper clears it.

Callers diverge

Grep results in dist/:

CallerPasses useCache: false?Behavior
commands-models-*.js::buildModelsProviderData (Telegram /model)❌ NoServes stale cache
model-picker-*.js (other picker UI paths)✅ YesAlways fresh
auth-choice-*.js✅ YesAlways fresh

The Telegram picker is the only consumer hitting the unbounded cache. Other UI paths already opt out by passing useCache: false, confirming the intent was "Telegram picker is the odd one out" rather than "caching is safe here."

Call chain (verified in dist/)

  1. Telegram /model callback → bot-*.jstelegramDeps.buildModelsProviderData(runtimeCfg, sessionState.agentId)
  2. commands-models-*.jsawait loadModelCatalog({ config: cfg })no useCache: false
  3. model-catalog-*.js returns the cached promise from process start

Why agents.defaults.models workaround does NOT save you

It's tempting to say "just add the raw ref to agents.defaults.models, because addRawModelRef(raw) in buildAllowedModelSet adds those keys even if they aren't in the catalog." That works for dynamically added aliases, but only at first load — once modelCatalogPromise is filled, loadModelCatalog ignores the new config entirely on subsequent calls from the Telegram picker. The allowlist key and the catalog are merged after the cache is consulted.

Proposed fixes (any one is enough)

  1. Config-hash invalidation in loadModelCatalog. Keep the promise keyed by hash(cfg.models) + hash(cfg.agents?.defaults?.models). When it changes, rebuild.
  2. Pass useCache: false from buildModelsProviderData. One-line fix that matches how model-picker-*.js and auth-choice-*.js already behave. Catalog build is not that hot on interactive /model presses.
  3. Invalidate on config reload events. The gateway already re-reads config in some paths (e.g. clobber recovery) — wire that to resetModelCatalogCacheForTest-equivalent.

Option 2 is the smallest and most consistent with existing callers. Option 1 is the most robust.

Impact

Any user who edits models.providers.* or agents.defaults.models while the gateway is running sees a confusing divergence:

  • openclaw models list --json → new model present
  • ~/.openclaw/agents/<id>/agent/models.json → new model present
  • ✅ Other model picker UIs → new model present
  • ❌ Telegram /model → new model missing

The standard debugging reflex ("maybe the config didn't save / maybe the cache file didn't write") doesn't find anything, because the stale data is purely in-memory.

The only fix is openclaw gateway restart, which isn't documented as a required step for model changes.

Workaround for affected users

systemctl --user restart openclaw-gateway
# or
openclaw gateway restart

Related

  • #42854 "Model picker shows stale Gemini preview alias" — similar shape (stale picker), likely different root cause (alias registry) but worth cross-linking.

Happy to PR option 2 if maintainers confirm direction.

extent analysis

TL;DR

Passing useCache: false to loadModelCatalog from the Telegram /model callback is the most likely fix to prevent the model catalog cache from becoming stale.

Guidance

  • Identify the loadModelCatalog function call in the Telegram /model callback and pass useCache: false as a parameter to prevent caching.
  • Verify that the loadModelCatalog function is being called with the updated configuration after making changes to models.providers.* or agents.defaults.models.
  • Check the model-catalog-*.js file to ensure that the cache is being properly invalidated when the configuration changes.
  • Consider implementing a config-hash invalidation mechanism in loadModelCatalog to keep the promise keyed by a hash of the configuration, which would rebuild the catalog when the configuration changes.

Example

// In commands-models-*.js
await loadModelCatalog({ config: cfg, useCache: false });

Notes

The provided code snippet and analysis suggest that the issue is specific to the Telegram /model callback and can be resolved by passing useCache: false to loadModelCatalog. However, a more robust solution might involve implementing a config-hash invalidation mechanism to handle configuration changes.

Recommendation

Apply the workaround by passing useCache: false to loadModelCatalog from the Telegram /model callback, as it is a simple and effective solution that aligns with the existing behavior of other model picker UI paths.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING