feat: first-byte timeout with same-endpoint retry
Some checks failed
Build Docker Image / build (push) Has been cancelled
Some checks failed
Build Docker Image / build (push) Has been cancelled
Upstream sometimes accepts a request (HTTP 200 headers) but stalls without sending any event-stream packet. Add a configurable timeout that counts from request dispatch until the first AWS event-stream prelude is read, and retry on the same endpoint before falling back. - Config: FirstByteTimeoutSec (default 10s, 0=disabled, range 0-300), FirstByteRetries (default 1, range 0-10), with Get/Update helpers. - kiro.go: parseEventStream signature gains onFirstByte callback, fired once when the first 12-byte prelude reads successfully. CallKiroAPI wraps each attempt in a context.WithCancel + time.AfterFunc timer that cancels the HTTP request if no event arrives before the deadline. Separate retry budgets for INVALID_MODEL_ID and first-byte timeout, tracked on the same attempt loop; maxAttempts = max(both)+1. - handler.go: /admin/api/general extended to read/write the two new fields with validation (timeout 0-300, retries 0-10). - web/index.html: General Settings card gains two numeric inputs plus CN/EN i18n and the corresponding load/save JS.
This commit is contained in:
@@ -977,6 +977,18 @@
|
||||
<small style="color:#64748b;font-size:12px;margin-top:4px;display:block"
|
||||
data-i18n="settings.invalidModelRetriesHint"></small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label data-i18n="settings.firstByteTimeoutSec"></label>
|
||||
<input type="number" id="firstByteTimeoutSec" min="0" max="300" step="1" placeholder="10">
|
||||
<small style="color:#64748b;font-size:12px;margin-top:4px;display:block"
|
||||
data-i18n="settings.firstByteTimeoutHint"></small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label data-i18n="settings.firstByteRetries"></label>
|
||||
<input type="number" id="firstByteRetries" min="0" max="10" step="1" placeholder="1">
|
||||
<small style="color:#64748b;font-size:12px;margin-top:4px;display:block"
|
||||
data-i18n="settings.firstByteRetriesHint"></small>
|
||||
</div>
|
||||
<button class="btn btn-primary" onclick="saveGeneralConfig()"
|
||||
data-i18n="settings.saveGeneral"></button>
|
||||
</div>
|
||||
@@ -1165,6 +1177,10 @@
|
||||
'settings.generalSettings': '通用设置',
|
||||
'settings.invalidModelRetries': 'INVALID_MODEL_ID 同端点重试次数',
|
||||
'settings.invalidModelRetriesHint': '当上游返回 INVALID_MODEL_ID(HTTP 400)时,先在当前端点重试 N 次后再 fallback 到下一个端点。默认 3,范围 0-20',
|
||||
'settings.firstByteTimeoutSec': '首字节超时(秒)',
|
||||
'settings.firstByteTimeoutHint': '请求发出后,若在 N 秒内未收到上游任何 event-stream 数据包,则判定首字节超时并在同端点重试。默认 10,设为 0 禁用。范围 0-300',
|
||||
'settings.firstByteRetries': '首字节超时同端点重试次数',
|
||||
'settings.firstByteRetriesHint': '首字节超时发生时,在当前端点重试 N 次后再 fallback 到下一个端点。默认 1,范围 0-10',
|
||||
'settings.saveGeneral': '保存通用设置',
|
||||
'settings.generalSaved': '通用设置已保存',
|
||||
'settings.thinkingSettings': 'Thinking 模式设置',
|
||||
@@ -1386,6 +1402,10 @@
|
||||
'settings.generalSettings': 'General Settings',
|
||||
'settings.invalidModelRetries': 'INVALID_MODEL_ID same-endpoint retries',
|
||||
'settings.invalidModelRetriesHint': 'When upstream returns INVALID_MODEL_ID (HTTP 400), retry the current endpoint N times before falling back. Default 3, range 0-20',
|
||||
'settings.firstByteTimeoutSec': 'First-byte timeout (seconds)',
|
||||
'settings.firstByteTimeoutHint': 'After sending a request, if no upstream event-stream packet arrives within N seconds, treat as first-byte timeout and retry on the same endpoint. Default 10, set 0 to disable. Range 0-300',
|
||||
'settings.firstByteRetries': 'First-byte timeout same-endpoint retries',
|
||||
'settings.firstByteRetriesHint': 'On first-byte timeout, retry the current endpoint N times before falling back. Default 1, range 0-10',
|
||||
'settings.saveGeneral': 'Save General Settings',
|
||||
'settings.generalSaved': 'General settings saved',
|
||||
'settings.thinkingSettings': 'Thinking Mode Settings',
|
||||
@@ -2094,19 +2114,32 @@
|
||||
async function loadGeneralConfig() {
|
||||
const res = await fetch('/admin/api/general', { headers: { 'X-Admin-Password': password } });
|
||||
const d = await res.json();
|
||||
const v = (d && typeof d.invalidModelRetries === 'number') ? d.invalidModelRetries : 3;
|
||||
document.getElementById('invalidModelRetries').value = v;
|
||||
document.getElementById('invalidModelRetries').value =
|
||||
(d && typeof d.invalidModelRetries === 'number') ? d.invalidModelRetries : 3;
|
||||
document.getElementById('firstByteTimeoutSec').value =
|
||||
(d && typeof d.firstByteTimeoutSec === 'number') ? d.firstByteTimeoutSec : 10;
|
||||
document.getElementById('firstByteRetries').value =
|
||||
(d && typeof d.firstByteRetries === 'number') ? d.firstByteRetries : 1;
|
||||
}
|
||||
async function saveGeneralConfig() {
|
||||
const raw = document.getElementById('invalidModelRetries').value;
|
||||
const n = parseInt(raw, 10);
|
||||
const n = parseInt(document.getElementById('invalidModelRetries').value, 10);
|
||||
const t1 = parseInt(document.getElementById('firstByteTimeoutSec').value, 10);
|
||||
const r1 = parseInt(document.getElementById('firstByteRetries').value, 10);
|
||||
if (isNaN(n) || n < 0 || n > 20) {
|
||||
alert(t('common.saveFailed') + ': 0-20');
|
||||
alert(t('common.saveFailed') + ': invalidModelRetries 0-20');
|
||||
return;
|
||||
}
|
||||
if (isNaN(t1) || t1 < 0 || t1 > 300) {
|
||||
alert(t('common.saveFailed') + ': firstByteTimeoutSec 0-300');
|
||||
return;
|
||||
}
|
||||
if (isNaN(r1) || r1 < 0 || r1 > 10) {
|
||||
alert(t('common.saveFailed') + ': firstByteRetries 0-10');
|
||||
return;
|
||||
}
|
||||
const res = await fetch('/admin/api/general', {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Admin-Password': password },
|
||||
body: JSON.stringify({ invalidModelRetries: n })
|
||||
body: JSON.stringify({ invalidModelRetries: n, firstByteTimeoutSec: t1, firstByteRetries: r1 })
|
||||
});
|
||||
const d = await res.json();
|
||||
if (d.success) { alert(t('settings.generalSaved')); } else { alert(t('common.saveFailed') + ': ' + d.error); }
|
||||
|
||||
Reference in New Issue
Block a user