Add /models fetch timeout and health-derived default fallback for model dropdowns.
Docker Release / build-and-push (push) Successful in 57s
Docker Release / release (push) Skipped

This commit is contained in:
2026-08-05 15:59:50 -05:00
parent 4b3b62b3fa
commit 32544bc2af
+25 -9
View File
@@ -171,6 +171,7 @@ const drop=document.getElementById('drop'), fileInput=document.getElementById('f
logBox=document.getElementById('logBox'), logBox=document.getElementById('logBox'),
logHint=document.getElementById('logHint'); logHint=document.getElementById('logHint');
let chosen=null, polling=null, currentJobId=null, sheetPage={}, viewerZoom=1, reviewDirty=false; let chosen=null, polling=null, currentJobId=null, sheetPage={}, viewerZoom=1, reviewDirty=false;
let defaultVisionModel='', defaultTextModel='';
function modelLabel(m){ function modelLabel(m){
// Include per-1M-token pricing when the catalog provides it. // Include per-1M-token pricing when the catalog provides it.
@@ -212,6 +213,12 @@ function saveModelsCache(data){
try{ localStorage.setItem(MODELS_CACHE_KEY, JSON.stringify({ts:Date.now(), data})); } try{ localStorage.setItem(MODELS_CACHE_KEY, JSON.stringify({ts:Date.now(), data})); }
catch(e){} catch(e){}
} }
function fetchWithTimeout(url, ms){
return Promise.race([
fetch(url, {cache:'no-store'}),
new Promise((_,reject)=>setTimeout(()=>reject(new Error('timeout')), ms))
]);
}
async function loadModels(){ async function loadModels(){
const note=document.getElementById('modelNote'); const note=document.getElementById('modelNote');
@@ -225,7 +232,8 @@ async function loadModels(){
return; return;
} }
try{ try{
const res=await fetch('/models'); note.textContent='fetching models...';
const res=await fetchWithTimeout('/models', 5000);
if(!res.ok) throw new Error('models HTTP '+res.status); if(!res.ok) throw new Error('models HTTP '+res.status);
const data=await res.json(); const data=await res.json();
saveModelsCache(data); saveModelsCache(data);
@@ -235,10 +243,12 @@ async function loadModels(){
modelsLoaded=true; modelsLoaded=true;
note.textContent='('+(data.text||[]).length+' text / '+(data.vision||[]).length+' vision available)'; note.textContent='('+(data.text||[]).length+' text / '+(data.vision||[]).length+' vision available)';
}catch(err){ }catch(err){
visionSel.innerHTML='<option value="">(default)</option>'; const fallback=[{id:defaultVisionModel||'', name:defaultVisionModel||'(default)', prompt_usd_per_mtok:null, completion_usd_per_mtok:null}];
textSel.innerHTML='<option value="">(default)</option>'; fillSelect(visionSel, fallback.filter(m=>m.id), defaultVisionModel);
const fallbackText=[{id:defaultTextModel||'', name:defaultTextModel||'(default)', prompt_usd_per_mtok:null, completion_usd_per_mtok:null}];
fillSelect(textSel, fallbackText.filter(m=>m.id), defaultTextModel);
visionSel.disabled=false; textSel.disabled=false; visionSel.disabled=false; textSel.disabled=false;
note.textContent='using configured defaults (list unavailable)'; note.textContent='using configured defaults ('+(err.message==='timeout'?'fetch timed out':'list unavailable')+')';
console.warn('Could not load models:', err); console.warn('Could not load models:', err);
} }
} }
@@ -674,11 +684,17 @@ async function finalizeReview(){
// If opened from an email link (/?job=<id>), load that job's results directly. // If opened from an email link (/?job=<id>), load that job's results directly.
(function init(){ (function init(){
// Show the deployed build in the header so it's obvious which version is up. // Fetch /health first: build tag for the header and default models as a
fetch('/health').then(r=>r.ok?r.json():null).then(h=>{ // fallback if the larger /models catalog fails or times out.
if(h&&h.build) document.getElementById('buildTag').textContent=' · build '+h.build; fetch('/health', {cache:'no-store'}).then(r=>r.ok?r.json():null).then(h=>{
}).catch(()=>{}); if(h){
loadModels(); if(h.build) document.getElementById('buildTag').textContent=' · build '+h.build;
if(h.model) defaultVisionModel=h.model;
if(h.text_model) defaultTextModel=h.text_model;
}
}).catch(()=>{}).finally(()=>{
loadModels();
});
const jobId=new URLSearchParams(location.search).get('job'); const jobId=new URLSearchParams(location.search).get('job');
if(jobId){ statusEl.innerHTML='<span class="spinner"></span>Loading job '+esc(jobId)+'...'; poll(jobId); } if(jobId){ statusEl.innerHTML='<span class="spinner"></span>Loading job '+esc(jobId)+'...'; poll(jobId); }
})(); })();