feat: first-byte timeout with same-endpoint retry
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:
2026-05-12 09:04:11 +08:00
parent de4524ad19
commit 89f731cb19
4 changed files with 196 additions and 13 deletions

View File

@@ -2935,6 +2935,8 @@ func (h *Handler) apiUpdateProxy(w http.ResponseWriter, r *http.Request) {
func (h *Handler) apiGetGeneralConfig(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(map[string]interface{}{
"invalidModelRetries": config.GetInvalidModelRetries(),
"firstByteTimeoutSec": config.GetFirstByteTimeoutSec(),
"firstByteRetries": config.GetFirstByteRetries(),
})
}
@@ -2942,6 +2944,8 @@ func (h *Handler) apiGetGeneralConfig(w http.ResponseWriter, r *http.Request) {
func (h *Handler) apiUpdateGeneralConfig(w http.ResponseWriter, r *http.Request) {
var req struct {
InvalidModelRetries *int `json:"invalidModelRetries"`
FirstByteTimeoutSec *int `json:"firstByteTimeoutSec"`
FirstByteRetries *int `json:"firstByteRetries"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
w.WriteHeader(400)
@@ -2963,6 +2967,34 @@ func (h *Handler) apiUpdateGeneralConfig(w http.ResponseWriter, r *http.Request)
}
}
if req.FirstByteTimeoutSec != nil {
n := *req.FirstByteTimeoutSec
if n < 0 || n > 300 {
w.WriteHeader(400)
json.NewEncoder(w).Encode(map[string]string{"error": "firstByteTimeoutSec must be 0-300"})
return
}
if err := config.UpdateFirstByteTimeoutSec(n); err != nil {
w.WriteHeader(500)
json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
return
}
}
if req.FirstByteRetries != nil {
n := *req.FirstByteRetries
if n < 0 || n > 10 {
w.WriteHeader(400)
json.NewEncoder(w).Encode(map[string]string{"error": "firstByteRetries must be 0-10"})
return
}
if err := config.UpdateFirstByteRetries(n); err != nil {
w.WriteHeader(500)
json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
return
}
}
json.NewEncoder(w).Encode(map[string]bool{"success": true})
}