Add LLM busy modes: wait (hang) or reject with Retry-After
LLM_BUSY_MODE=reject answers blocked LLM requests immediately with LLM_BUSY_STATUS (default 503, 429 works) and Retry-After, so routers like LiteLLM can cool down and retry instead of holding a hung connection. The default wait mode now also sends Retry-After when LLM_WAIT_TIMEOUT expires. Document the service account (LocalSystem default, NT SERVICE virtual-account hardening) and the Program Files / ProgramData install layout.
This commit is contained in:
@@ -2,6 +2,7 @@ package proxy
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
@@ -345,3 +346,108 @@ func TestPassThroughNoLock(t *testing.T) {
|
||||
t.Fatalf("pass-through = %d %s", resp.StatusCode, body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLLMBusyReject(t *testing.T) {
|
||||
f := newFakes(t)
|
||||
|
||||
lk := lock.New(nil)
|
||||
if err := lk.AcquireImage(context.Background()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ollamaClient, err := ollama.New(f.ollama.URL, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
comfyClient, err := comfy.New(f.comfy.URL, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
srv, err := New(Config{
|
||||
OllamaURL: f.ollama.URL,
|
||||
ComfyURL: f.comfy.URL,
|
||||
Lock: lk,
|
||||
Ollama: ollamaClient,
|
||||
Comfy: comfyClient,
|
||||
Metrics: metrics.New(),
|
||||
LLMWaitTimeout: 2 * time.Second,
|
||||
LLMBusyMode: "reject",
|
||||
BusyRetryAfter: 17,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
front := httptest.NewServer(srv.OllamaHandler())
|
||||
defer front.Close()
|
||||
|
||||
start := time.Now()
|
||||
resp, err := http.Post(front.URL+"/api/chat", "application/json", strings.NewReader(`{}`))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusServiceUnavailable {
|
||||
t.Fatalf("busy chat status = %d %s", resp.StatusCode, body)
|
||||
}
|
||||
if got := resp.Header.Get("Retry-After"); got != "17" {
|
||||
t.Fatalf("Retry-After = %q", got)
|
||||
}
|
||||
if elapsed := time.Since(start); elapsed > time.Second {
|
||||
t.Fatalf("reject was not immediate: %v", elapsed)
|
||||
}
|
||||
|
||||
// After the image lock is released the next LLM request goes through.
|
||||
lk.ReleaseImage()
|
||||
resp, err = http.Post(front.URL+"/api/chat", "application/json", strings.NewReader(`{}`))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode != 200 {
|
||||
t.Fatalf("chat after release status = %d", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLLMBusyWaitTimeoutRetryAfter(t *testing.T) {
|
||||
f := newFakes(t)
|
||||
|
||||
lk := lock.New(nil)
|
||||
if err := lk.AcquireImage(context.Background()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer lk.ReleaseImage()
|
||||
ollamaClient, err := ollama.New(f.ollama.URL, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
comfyClient, err := comfy.New(f.comfy.URL, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
srv, err := New(Config{
|
||||
OllamaURL: f.ollama.URL,
|
||||
ComfyURL: f.comfy.URL,
|
||||
Lock: lk,
|
||||
Ollama: ollamaClient,
|
||||
Comfy: comfyClient,
|
||||
Metrics: metrics.New(),
|
||||
LLMWaitTimeout: 50 * time.Millisecond,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
front := httptest.NewServer(srv.OllamaHandler())
|
||||
defer front.Close()
|
||||
|
||||
resp, err := http.Post(front.URL+"/api/chat", "application/json", strings.NewReader(`{}`))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusServiceUnavailable {
|
||||
t.Fatalf("timed-out chat status = %d", resp.StatusCode)
|
||||
}
|
||||
if got := resp.Header.Get("Retry-After"); got != "30" {
|
||||
t.Fatalf("Retry-After = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user