mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-09-20 08:57:57 +00:00
feat: AUTO chain rewrite — silent failover+busy skip+pref round-robin+503 tier summary; chain edits reset slot cooldowns (P0/P1); stats by_status + audit jsonl rotation; UI priority-page health badges & status-code card; ctx-menu capture-phase close (outside-press guard); main.go ops warnings; local bundled-Lua verified tests (3 latent bugs fixed); plan.md
This commit is contained in:
@ -13,6 +13,7 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
@ -100,7 +101,15 @@ func buildBinary(t *testing.T) string {
|
||||
t.Helper()
|
||||
dir := t.TempDir()
|
||||
bin := filepath.Join(dir, "llmsproxy")
|
||||
if runtime.GOOS == "windows" {
|
||||
bin += ".exe" // go build writes the exact name; Windows needs the suffix to exec
|
||||
}
|
||||
out, err := exec.Command("go", "build", "-tags", "luajit", "-o", bin, "llmsproxy/cmd/llmsproxy").CombinedOutput()
|
||||
if err != nil && runtime.GOOS == "windows" {
|
||||
// local Windows dev box may lack LuaJIT: fall back to the bundled
|
||||
// Lua runtime (the e2e adapters used here are passthrough-only)
|
||||
out, err = exec.Command("go", "build", "-o", bin, "llmsproxy/cmd/llmsproxy").CombinedOutput()
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("build: %v\n%s", err, out)
|
||||
}
|
||||
@ -166,7 +175,8 @@ func (g *gatewayUnderTest) do(method, path string, body string, authed bool) (*h
|
||||
}
|
||||
|
||||
// writeConfig writes a temp gateway config pointing at the mock upstreams,
|
||||
// listening on the given address.
|
||||
// listening on the given address. Every non-image upstream becomes a chat
|
||||
// source; the first named one ("good") is the highest-priority AUTO slot.
|
||||
func writeConfig(t *testing.T, dir, listenAddr string, upstreams map[string]*mockUpstream) string {
|
||||
t.Helper()
|
||||
var sb strings.Builder
|
||||
@ -176,7 +186,8 @@ func writeConfig(t *testing.T, dir, listenAddr string, upstreams map[string]*moc
|
||||
sb.WriteString("adapter_dir: " + filepath.Join(dir, "adapters") + "\n")
|
||||
sb.WriteString("runtime_file: " + filepath.Join(dir, "runtime.json") + "\n")
|
||||
sb.WriteString("sources:\n")
|
||||
order := []string{"good", "image"}
|
||||
order := []string{"good", "fallback", "image"}
|
||||
prio := map[string]int{"good": 100, "fallback": 50}
|
||||
for _, name := range order {
|
||||
u, ok := upstreams[name]
|
||||
if !ok {
|
||||
@ -185,7 +196,7 @@ func writeConfig(t *testing.T, dir, listenAddr string, upstreams map[string]*moc
|
||||
if name == "image" {
|
||||
sb.WriteString(" - name: imagegen\n base_url: " + u.baseURL + "\n adapter: openai\n models:\n - id: flux-1\n kind: image\n priority: 80\n")
|
||||
} else {
|
||||
sb.WriteString(" - name: " + name + "\n base_url: " + u.baseURL + "\n adapter: openai\n models:\n - id: " + name + "-m\n priority: 100\n")
|
||||
sb.WriteString(" - name: " + name + "\n base_url: " + u.baseURL + "\n adapter: openai\n models:\n - id: " + name + "-m\n priority: " + fmt.Sprint(prio[name]) + "\n")
|
||||
}
|
||||
}
|
||||
path := filepath.Join(dir, "config.yaml")
|
||||
@ -197,8 +208,9 @@ func writeConfig(t *testing.T, dir, listenAddr string, upstreams map[string]*moc
|
||||
|
||||
func TestEndToEnd(t *testing.T) {
|
||||
upstreams := map[string]*mockUpstream{
|
||||
"good": newMockUpstream(t),
|
||||
"image": newMockUpstream(t),
|
||||
"good": newMockUpstream(t),
|
||||
"fallback": newMockUpstream(t),
|
||||
"image": newMockUpstream(t),
|
||||
}
|
||||
|
||||
dir := t.TempDir()
|
||||
@ -279,6 +291,35 @@ func TestEndToEnd(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestEndToEndAuto503: with every AUTO slot failing, the gateway must answer
|
||||
// 503 whose message summarizes each failed tier/source/model instead of a
|
||||
// bare "no provider available" (P8).
|
||||
func TestEndToEndAuto503(t *testing.T) {
|
||||
upstreams := map[string]*mockUpstream{
|
||||
"good": newMockUpstream(t),
|
||||
}
|
||||
dir := t.TempDir()
|
||||
l, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("pick port: %v", err)
|
||||
}
|
||||
addr := l.Addr().String()
|
||||
l.Close()
|
||||
cfg := writeConfig(t, dir, addr, upstreams)
|
||||
bin := buildBinary(t)
|
||||
g := startGateway(t, bin, addr, cfg)
|
||||
|
||||
upstreams["good"].SetFail(true)
|
||||
resp, body := g.do("POST", "/v1/chat/completions",
|
||||
`{"model":"AUTO","messages":[{"role":"user","content":"hi"}]}`, true)
|
||||
if resp == nil || resp.StatusCode != http.StatusServiceUnavailable {
|
||||
t.Fatalf("status=%v body=%q", statusOf(resp), body)
|
||||
}
|
||||
if !strings.Contains(body, "all auto tiers failed") || !strings.Contains(body, "good/good-m") {
|
||||
t.Fatalf("503 must summarize the failed slot, body=%q", body)
|
||||
}
|
||||
}
|
||||
|
||||
func statusOf(resp *http.Response) int {
|
||||
if resp == nil {
|
||||
return -1
|
||||
|
||||
Reference in New Issue
Block a user