mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-09-21 09:28:00 +00:00
feat: opencode zen adapter + first-run config generation, fix stats/stream bugs
- adapters/opencode.lua: opencode.ai zen free pool adapter — sends the opencode client User-Agent (zen fingerprints clients by UA; non-official clients hit FreeUsageLimitError); pairs with api_key: public - config: no config file ships in the repo; first run generates a default config at the -config path with a random admin key, loopback listen and a keyless zen source (config.EnsureDefault); remove config.example.yaml - lua: seed bundled adapters from the embedded FS instead of a hardcoded name list - ui: widen model kind select (chat was clipped to 'cha') - phase 5 bugfixes: stats ms/s bucket mixing, cleanScopes nil, ctx.Err guards, direct-path ModelAvailable, empty stream body failure, bestImageModel rewrite, transform failure recording, Core.mu, timer, effective model for tool-calls
This commit is contained in:
@ -6,6 +6,7 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestStatsByStatus(t *testing.T) {
|
||||
@ -91,18 +92,21 @@ func TestAuditRotationRecords(t *testing.T) {
|
||||
func TestLoadAuditFullReplay(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "audit.jsonl")
|
||||
// timestamps relative to now: buckets are whole unix hours, so window
|
||||
// assertions must not depend on the minute-of-hour of the test run.
|
||||
now := time.Now()
|
||||
lines := []string{
|
||||
`{"obj":"access","time":1699999999000,"key":"k","method":"GET","path":"/api/stats","status":200}`,
|
||||
`{"time":1700000000000,"key":"k","type":"chat","model":"m","source":"s","prompt_tokens":100,"completion_tokens":50,"latency_ms":10,"ok":true,"status":200}`,
|
||||
fmt.Sprintf(`{"time":%d,"key":"k","type":"chat","model":"m","source":"s","prompt_tokens":100,"completion_tokens":50,"latency_ms":10,"ok":true,"status":200}`, now.Add(-65*time.Minute).UnixMilli()),
|
||||
`{this is not valid json`,
|
||||
`{"time":1700003600000,"key":"k","type":"stream","model":"m","source":"s","prompt_tokens":200,"completion_tokens":20,"latency_ms":20,"ok":false,"status":503}`,
|
||||
fmt.Sprintf(`{"time":%d,"key":"k","type":"stream","model":"m","source":"s","prompt_tokens":200,"completion_tokens":20,"latency_ms":20,"ok":false,"status":503}`, now.Add(-30*time.Minute).UnixMilli()),
|
||||
"garbage-not-json\n",
|
||||
`{"time":1700007200000,"key":"k","type":"chat","model":"m2","source":"s","prompt_tokens":7,"completion_tokens":3,"latency_ms":5,"ok":true,"status":200}`,
|
||||
fmt.Sprintf(`{"time":%d,"key":"k","type":"chat","model":"m2","source":"s","prompt_tokens":7,"completion_tokens":3,"latency_ms":5,"ok":true,"status":200}`, now.Add(-30*time.Minute).UnixMilli()),
|
||||
}
|
||||
loaded := strings.Join(lines, "\n") + "\n" + strings.Repeat("x", 1<<18) + "\n"
|
||||
// oversized row at the END proves the scanner tolerates >64KB lines and
|
||||
// still finishes the replay instead of truncating silently.
|
||||
loaded += `{"time":1700010800000,"key":"k","type":"chat","model":"m","source":"s","prompt_tokens":1,"completion_tokens":1,"latency_ms":1,"ok":true,"status":200}` + "\n"
|
||||
loaded += fmt.Sprintf(`{"time":%d,"key":"k","type":"chat","model":"m","source":"s","prompt_tokens":1,"completion_tokens":1,"latency_ms":1,"ok":true,"status":200}`, now.Add(-time.Minute).UnixMilli()) + "\n"
|
||||
if err := os.WriteFile(path, []byte(loaded), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@ -121,9 +125,24 @@ func TestLoadAuditFullReplay(t *testing.T) {
|
||||
if len(s.recs) != 4 {
|
||||
t.Fatalf("ring must hold only real requests, got %d rows: %#v", len(s.recs), s.recs)
|
||||
}
|
||||
// quota window rebuilt from full history
|
||||
if w := s.WindowTokens("m", "s", hourSec); w != 372 {
|
||||
t.Fatalf("window tokens want 372, got %d", w)
|
||||
// quota window rebuilt from full history. All-time and multi-hour windows
|
||||
// must see everything; a 1h window must NOT return all records (the old
|
||||
// ms/seconds unit bug made any sec>0 window return everything), and must
|
||||
// always include the row written one minute ago (current hour bucket).
|
||||
if w := s.WindowTokens("m", "s", 0); w != 372 {
|
||||
t.Fatalf("all-time window want 372, got %d", w)
|
||||
}
|
||||
if w := s.WindowTokens("m", "s", 3*hourSec); w != 372 {
|
||||
t.Fatalf("3h window want 372, got %d", w)
|
||||
}
|
||||
if w := s.WindowTokens("m", "s", 24*hourSec); w != 372 {
|
||||
t.Fatalf("24h window want 372, got %d", w)
|
||||
}
|
||||
if w := s.WindowTokens("m", "s", hourSec); w < 2 || w >= 372 {
|
||||
t.Fatalf("1h window want [2,372), got %d", w)
|
||||
}
|
||||
if w := s.WindowTokens("m2", "s", 24*hourSec); w != 10 {
|
||||
t.Fatalf("m2 24h window want 10, got %d", w)
|
||||
}
|
||||
// by_status only from requests (200 x3, 503 x1) — access line must not count
|
||||
if st := s.byStatus[200]; st == nil || st.Reqs != 3 {
|
||||
|
||||
Reference in New Issue
Block a user