fix(#4737): retry model catalog fetch once after cold-cache fallback

This commit is contained in:
Rod Boev 2026-07-01 15:56:04 -04:00
parent b84bf1c38e
commit d73bea19cd
2 changed files with 356 additions and 4 deletions

View file

@ -2390,6 +2390,7 @@ function _persistSessionModelCorrection(model, provider, opts){
return opts&&opts.propagateErrors ? request : request.catch(()=>{});
}
let _modelDropdownRequestSeq=0;
let _modelCatalogFallbackRetried=false;
function _applySessionModelFallback(sel){
if(!sel) return null;
@ -2414,10 +2415,12 @@ async function populateModelDropdown(opts={}){
const sel=$('modelSelect');
if(!sel) return;
if(typeof _modelDropdownRequestSeq!=='number') _modelDropdownRequestSeq=0;
if(typeof _modelCatalogFallbackRetried!=='boolean') _modelCatalogFallbackRetried=false;
const requestSeq=++_modelDropdownRequestSeq;
try{
const modelsUrl=new URL('api/models',document.baseURI||location.href);
if(opts&&opts.freshness) modelsUrl.searchParams.set('freshness',opts.freshness);
const requestedFreshness=opts&&opts.freshness?String(opts.freshness):'';
if(requestedFreshness) modelsUrl.searchParams.set('freshness',requestedFreshness);
const _modelsRes=await fetch(modelsUrl.href,{credentials:'include'});
if(requestSeq!==_modelDropdownRequestSeq) return;
const customRedirectIfUnauth=opts&&typeof opts.redirectIfUnauth==='function'?opts.redirectIfUnauth:null;
@ -2469,9 +2472,10 @@ async function populateModelDropdown(opts={}){
return groups;
};
const groups=(Array.isArray(data.groups)&&data.groups.length)
? data.groups
: _synthGroupsFromConfigured();
const usedConfiguredFallback=!(Array.isArray(data.groups)&&data.groups.length);
const groups=usedConfiguredFallback
? _synthGroupsFromConfigured()
: data.groups;
if(!groups.length) return; // no server groups and no configured fallback
const previousSelection=_captureModelDropdownSelection(sel);
@ -2518,6 +2522,10 @@ async function populateModelDropdown(opts={}){
// Kick off a background live-model fetch for the active provider.
// This runs after the static list is already shown (no blocking flicker).
if(data.active_provider) _fetchLiveModels(data.active_provider, sel, requestSeq);
if(usedConfiguredFallback && requestedFreshness!=='session_visit' && !_modelCatalogFallbackRetried){
_modelCatalogFallbackRetried=true;
populateModelDropdown({freshness:'session_visit'}).catch(()=>{});
}
}catch(e){
if(requestSeq!==_modelDropdownRequestSeq) return;
// API unavailable -- keep the hardcoded HTML options as fallback

View file

@ -0,0 +1,344 @@
"""Browserless regression coverage for first-tab model catalog refresh (#4737)."""
import json
import shutil
import subprocess
import tempfile
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[1]
UI_JS = (ROOT / "static" / "ui.js").read_text(encoding="utf-8")
NODE = shutil.which("node")
pytestmark = pytest.mark.skipif(
NODE is None,
reason="node is required to execute the model catalog refresh harness",
)
_DRIVER = r"""
const fs = require('fs');
const scenario = JSON.parse(process.argv[2] || '{}');
const fnSource = fs.readFileSync(process.argv[3], 'utf8');
class FakeStorage {
constructor(seed = {}) {
this.store = { ...seed };
}
getItem(key) {
return Object.prototype.hasOwnProperty.call(this.store, key)
? this.store[key]
: null;
}
setItem(key, value) {
this.store[key] = String(value);
}
removeItem(key) {
delete this.store[key];
}
}
class FakeNode {
constructor(tagName) {
this.tagName = tagName.toUpperCase();
this.children = [];
this.dataset = {};
this.classList = { contains: () => false };
this.style = {};
this.textContent = '';
this.label = '';
this.value = '';
this.parentNode = null;
}
appendChild(child) {
this.children.push(child);
child.parentNode = this;
return child;
}
}
class FakeSelect extends FakeNode {
constructor() {
super('select');
this.id = 'modelSelect';
this._innerHTML = '';
this._value = '';
}
set innerHTML(value) {
this._innerHTML = value;
this.children = [];
}
get innerHTML() {
return this._innerHTML;
}
get options() {
const options = [];
for (const child of this.children) {
if (child.tagName === 'OPTGROUP') {
options.push(...child.children);
} else if (child.tagName === 'OPTION') {
options.push(child);
}
}
return options;
}
set value(value) {
this._value = value;
}
get value() {
return this._value;
}
querySelector(selector) {
if (selector === 'optgroup > option, option') {
return this.options[0] || null;
}
return null;
}
}
function buildFetchResponse(payload, status = 200) {
return {
status,
ok: status >= 200 && status < 300,
json: async () => payload,
text: async () => JSON.stringify(payload),
};
}
function buildHarness(currentScenario) {
const select = new FakeSelect();
const dropdown = new FakeNode('div');
const fetchCalls = [];
const fetchQueue = (currentScenario.fetchResponses || []).map((payload) => buildFetchResponse(payload));
globalThis.window = globalThis;
globalThis.document = {
baseURI: 'http://localhost/session/abc',
createElement(tag) {
return new FakeNode(tag);
},
};
globalThis.location = { href: 'http://localhost/session/abc' };
globalThis.sessionStorage = new FakeStorage();
globalThis.localStorage = new FakeStorage();
globalThis.S = { pendingFiles: [] };
globalThis._dynamicModelLabels = {};
globalThis._modelEndpointErrors = {};
globalThis._defaultModel = null;
globalThis._activeProvider = null;
globalThis._configuredModelBadges = {};
globalThis._modelDropdownRequestSeq = 0;
globalThis._modelCatalogFallbackRetried = false;
globalThis.$ = (id) => {
if (id === 'modelSelect') return select;
if (id === 'composerModelDropdown') return dropdown;
return null;
};
globalThis.getModelLabel = (id) => `label:${id}`;
globalThis._captureModelDropdownSelection = () => null;
globalThis._reconcileModelDropdownSelection = (_sel, data) => {
const firstGroup = Array.isArray(data.groups) && data.groups.length ? data.groups[0] : null;
const firstModel = firstGroup && Array.isArray(firstGroup.models) && firstGroup.models.length
? firstGroup.models[0]
: null;
if (firstModel) _sel.value = firstModel.id;
};
globalThis.syncModelChip = () => {};
globalThis.renderModelDropdown = () => {};
globalThis._positionModelDropdown = () => {};
globalThis._redirectIfUnauth = () => false;
globalThis.console = { warn() {}, debug() {}, log() {} };
globalThis._fetchLiveModels = () => {};
globalThis.fetch = async (url) => {
fetchCalls.push(String(url));
if (!fetchQueue.length) throw new Error(`unexpected fetch: ${url}`);
return fetchQueue.shift();
};
return { select, fetchCalls };
}
async function runScenario(currentScenario) {
const { select, fetchCalls } = buildHarness(currentScenario);
let _modelDropdownRequestSeq = 0;
let _modelCatalogFallbackRetried = false;
eval(fnSource);
await populateModelDropdown(currentScenario.opts || {});
await Promise.resolve();
await Promise.resolve();
await new Promise((resolve) => setTimeout(resolve, 0));
return {
fetchCalls,
optionValues: select.options.map((opt) => opt.value),
};
}
runScenario(scenario).then((result) => {
process.stdout.write(JSON.stringify(result));
}).catch((error) => {
process.stderr.write(String(error && error.stack || error));
process.exit(1);
});
"""
@pytest.fixture(scope="module")
def driver_path(tmp_path_factory):
path = tmp_path_factory.mktemp("model-catalog-refresh-driver") / "driver.js"
path.write_text(_DRIVER, encoding="utf-8")
return str(path)
def _run(driver_path, scenario):
marker = "async function populateModelDropdown("
start = UI_JS.index(marker)
paren_depth = 1
idx = start + len(marker)
while idx < len(UI_JS) and paren_depth > 0:
char = UI_JS[idx]
if char == "(":
paren_depth += 1
elif char == ")":
paren_depth -= 1
idx += 1
if paren_depth != 0:
raise AssertionError("could not locate populateModelDropdown signature")
brace_start = UI_JS.index("{", idx)
depth = 0
for idx in range(brace_start, len(UI_JS)):
char = UI_JS[idx]
if char == "{":
depth += 1
elif char == "}":
depth -= 1
if depth == 0:
fn_source = UI_JS[start : idx + 1]
break
else:
raise AssertionError("could not extract populateModelDropdown")
with tempfile.NamedTemporaryFile("w", encoding="utf-8", suffix=".js", delete=False) as handle:
handle.write(fn_source)
fn_source_path = handle.name
try:
process = subprocess.run(
[NODE, driver_path, json.dumps(scenario), fn_source_path],
capture_output=True,
text=True,
timeout=60,
check=False,
)
finally:
Path(fn_source_path).unlink(missing_ok=True)
if process.returncode != 0:
raise RuntimeError(process.stderr.strip() or process.stdout.strip())
return json.loads(process.stdout)
def test_populate_model_dropdown_refetches_once_when_server_returns_empty_groups(driver_path):
payload = _run(
driver_path,
{
"fetchResponses": [
{
"active_provider": "anthropic",
"default_model": "claude-sonnet-4",
"configured_model_badges": {
"claude-sonnet-4": {"provider": "anthropic"},
},
"groups": [],
},
{
"active_provider": "anthropic",
"default_model": "claude-sonnet-4",
"configured_model_badges": {
"claude-sonnet-4": {"provider": "anthropic"},
},
"groups": [
{
"provider": "Anthropic",
"provider_id": "anthropic",
"models": [
{"id": "claude-sonnet-4", "label": "Claude Sonnet 4"},
{"id": "claude-opus-4", "label": "Claude Opus 4"},
],
}
],
},
],
},
)
assert len(payload["fetchCalls"]) == 2
assert "freshness=session_visit" in payload["fetchCalls"][1]
assert "claude-opus-4" in payload["optionValues"]
def test_populate_model_dropdown_does_not_refetch_when_server_groups_already_populated(driver_path):
payload = _run(
driver_path,
{
"fetchResponses": [
{
"active_provider": "anthropic",
"default_model": "claude-sonnet-4",
"configured_model_badges": {
"claude-sonnet-4": {"provider": "anthropic"},
},
"groups": [
{
"provider": "Anthropic",
"provider_id": "anthropic",
"models": [
{"id": "claude-sonnet-4", "label": "Claude Sonnet 4"},
],
}
],
}
],
},
)
assert len(payload["fetchCalls"]) == 1
assert payload["optionValues"] == ["claude-sonnet-4"]
def test_populate_model_dropdown_retries_at_most_once_even_if_refetch_is_still_empty(driver_path):
payload = _run(
driver_path,
{
"fetchResponses": [
{
"active_provider": "anthropic",
"default_model": "claude-sonnet-4",
"configured_model_badges": {
"claude-sonnet-4": {"provider": "anthropic"},
},
"groups": [],
},
{
"active_provider": "anthropic",
"default_model": "claude-sonnet-4",
"configured_model_badges": {
"claude-sonnet-4": {"provider": "anthropic"},
},
"groups": [],
},
],
},
)
assert len(payload["fetchCalls"]) == 2
assert payload["fetchCalls"][1].endswith("freshness=session_visit")
assert payload["optionValues"] == ["claude-sonnet-4"]