fix(core): skip models.dev refresh event when the catalog is unchanged - #44282
Merged
Conversation
The 5-minute refresh loop republished ModelsDev.Event.Refreshed on every cycle even when api.json was byte-identical, causing the models.dev plugin to reload its transforms and Catalog.finalize to emit catalog.updated unconditionally — spamming downstream listeners (TUI, event logger) and rewriting a multi-MB KV entry each time. Persist a sha256 digest of the raw body alongside the cached entry; refresh now fetches but skips the cache write, invalidation, and Refreshed publish when the body matches. Legacy entries without a digest publish once and then self-correct.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
The models.dev service repeats
refresh()every 5 minutes (Schedule.spaced(ttl)), and because the repeat interval equals the freshness TTL, every cycle fetched a new body, rewrote the multi-MB KV entry, invalidated the memoized catalog, and publishedModelsDev.Event.Refreshed— even when api.json was byte-identical.That triggered the full downstream chain on every cycle with no content change:
Refreshed→ theopencode.models.devplugin re-runs its integration/catalog transforms (ctx.catalog.reload())Catalog.finalizepublishescatalog.updatedunconditionally (no diffing)catalog.updated, and the event logger writes itFix
Persist a sha256 digest of the raw response body alongside the KV cache entry.
refresh()still fetches each cycle (same cadence as before), but when the new body is byte-identical to the cached one it now skips:Refreshedpublishrefresh(true)(explicit force) bypasses both the freshness check and the digest skip, preserving its "reload now" semantics for callers like tests/CLI.Legacy cache entries written before this change have no digest; they decode fine via the optional schema field and publish once before self-correcting.
Notes
models.opencode.ai/api.json, Cloudflare), so conditionalIf-None-Matchrequests could additionally save bandwidth. That is left out of scope here: 304 handling would need special-casing aroundHttpClient.filterStatusOk+retryTransient(304 is not an ok status and would be retried). The digest comparison achieves the behavioral goal with no protocol complexity.Refreshedis the models-dev plugin, so no other call sites are affected.Testing
refresh(false) fetches when staleto assert oneRefreshedevent is published on a real content changeRefreshedevent and cache entry untouchedpackages/core/test/models.test.tspass;bun typecheckclean inpackages/core