feat: retry button for orchestrator errors + explicit client timeout

Extract orchestrator inner loop into _doOrchestrate() so the retry button
can re-run without re-adding the user message to DOM or history — same
pattern as the existing chat retry.

Also set AsyncOpenAI(timeout=settings.timeout_local) so slow remote models
(OpenRouter/DeepSeek) get the same 300s budget as local chat calls instead
of the SDK default which varies by connection.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Scott Idem
2026-05-09 12:39:34 -04:00
parent ff154b1ec0
commit a99ebb8c30
2 changed files with 55 additions and 20 deletions

View File

@@ -405,7 +405,7 @@ def _build_client(
base_url = api_url.rstrip("/") base_url = api_url.rstrip("/")
if host_type == "openwebui": if host_type == "openwebui":
base_url = base_url + "/api" base_url = base_url + "/api"
client = AsyncOpenAI(base_url=base_url, api_key=api_key) client = AsyncOpenAI(base_url=base_url, api_key=api_key, timeout=settings.timeout_local)
if model_cfg.get("tools") is False: if model_cfg.get("tools") is False:
active_tools = [] active_tools = []
else: else:

View File

@@ -1215,24 +1215,9 @@
inputEl.focus(); inputEl.focus();
} }
async function sendOrchestrate() { // Extracted so the retry button can call it without re-adding the
const text = inputEl.value.trim(); // user message to the DOM or currentHistory.
if (!text || activeController) return; async function _doOrchestrate(text, thinkingDiv, userMsgDiv) {
inputEl.value = '';
syncHeight();
sendBtn.style.display = 'none';
stopBtn.style.display = 'flex';
headerEmoji.classList.add('processing');
activeController = new AbortController();
currentHistory.push({ role: 'user', content: text });
const userMsgDiv = addMessage('user', text);
scrollToBottom();
const thinkingDiv = addMessage('assistant thinking', '⚡ working…');
try { try {
const res = await fetch('/orchestrate', { const res = await fetch('/orchestrate', {
method: 'POST', method: 'POST',
@@ -1336,9 +1321,59 @@
thinkingDiv.textContent = 'Stopped.'; thinkingDiv.textContent = 'Stopped.';
} else { } else {
thinkingDiv.className = 'message error'; thinkingDiv.className = 'message error';
thinkingDiv.textContent = `Error: ${err.message}`; thinkingDiv.innerHTML = '';
const errSpan = document.createElement('span');
errSpan.textContent = `Error: ${err.message}`;
thinkingDiv.appendChild(errSpan);
const retryBtn = document.createElement('button');
retryBtn.className = 'retry-btn';
retryBtn.textContent = '↺ Retry';
retryBtn.addEventListener('click', async () => {
if (currentHistory.at(-1)?.role === 'user') currentHistory.pop();
currentHistory.push({ role: 'user', content: text });
thinkingDiv.className = 'message assistant thinking';
thinkingDiv.textContent = '⚡ working…';
activeController = new AbortController();
sendBtn.style.display = 'none';
stopBtn.style.display = 'flex';
headerEmoji.classList.add('processing');
await _doOrchestrate(text, thinkingDiv, userMsgDiv);
activeController = null;
headerEmoji.classList.remove('processing');
sendBtn.style.display = 'block';
stopBtn.style.display = 'none';
inputEl.focus();
});
thinkingDiv.appendChild(retryBtn);
} }
} }
}
async function sendOrchestrate() {
const text = inputEl.value.trim();
if (!text || activeController) return;
inputEl.value = '';
syncHeight();
sendBtn.style.display = 'none';
stopBtn.style.display = 'flex';
headerEmoji.classList.add('processing');
activeController = new AbortController();
currentHistory.push({ role: 'user', content: text });
const userMsgDiv = addMessage('user', text);
scrollToBottom();
const thinkingDiv = addMessage('assistant thinking', '⚡ working…');
await _doOrchestrate(text, thinkingDiv, userMsgDiv);
activeController = null; activeController = null;
headerEmoji.classList.remove('processing'); headerEmoji.classList.remove('processing');