mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-09-20 08:57:57 +00:00
- Lua adapters per upstream (transform_request/response/stream_chunk, build_headers signing hooks) - AUTO priority routing with per-model kind (chat/image), explicit source/model routing - Per-source concurrency caps with queueing, exponential backoff, AUTO failover - OpenAI-compatible API: chat completions, SSE streaming, image generations, models - Gateway key auth, web UI for adapter/source management, runtime persistence - e2e test running the real binary against mocked upstreams
122 lines
3.0 KiB
Go
122 lines
3.0 KiB
Go
package config
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
)
|
|
|
|
func TestLoadAndApplyDefaults(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "cfg.yaml")
|
|
content := `
|
|
listen: 127.0.0.1:9999
|
|
gateway_keys: [sk-1]
|
|
default_model: AUTO
|
|
adapter_dir: adapters
|
|
runtime_file: runtime.json
|
|
sources:
|
|
- name: deepseek
|
|
base_url: https://api.deepseek.com
|
|
api_key: sk-d
|
|
adapter: deepseek
|
|
models:
|
|
- id: deepseek-v4-flash
|
|
priority: 100
|
|
- name: ollama
|
|
base_url: http://127.0.0.1:11434
|
|
adapter: ollama
|
|
endpoint: /api/chat
|
|
models:
|
|
- id: llama3
|
|
priority: 50
|
|
`
|
|
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cfg, err := Load(path)
|
|
if err != nil {
|
|
t.Fatalf("load: %v", err)
|
|
}
|
|
if len(cfg.Sources) != 2 {
|
|
t.Fatalf("sources = %d", len(cfg.Sources))
|
|
}
|
|
if cfg.Sources[1].Endpoint != "/api/chat" {
|
|
t.Fatalf("endpoint = %q", cfg.Sources[1].Endpoint)
|
|
}
|
|
if cfg.Sources[1].Timeout == 0 {
|
|
t.Fatal("default timeout not applied")
|
|
}
|
|
if cfg.Sources[1].MaxConcurrent == 0 {
|
|
t.Fatal("default max_concurrent not applied")
|
|
}
|
|
if cfg.Sources[0].Models[0].Priority != 100 {
|
|
t.Fatalf("priority = %d", cfg.Sources[0].Models[0].Priority)
|
|
}
|
|
if cfg.DefaultModel != "AUTO" {
|
|
t.Fatalf("default model = %q", cfg.DefaultModel)
|
|
}
|
|
}
|
|
|
|
func TestApplyDefaultsDuplicateSource(t *testing.T) {
|
|
cfg := Config{Sources: []Source{
|
|
{Name: "a", BaseURL: "http://x", Models: []Model{{ID: "m1"}}},
|
|
{Name: "a", BaseURL: "http://y", Models: []Model{{ID: "m2"}}},
|
|
}}
|
|
if err := cfg.ApplyDefaults(); err == nil {
|
|
t.Fatal("expected duplicate source error")
|
|
}
|
|
}
|
|
|
|
func TestApplyDefaultsNoSources(t *testing.T) {
|
|
// empty source list is valid (sources may be added later via the Web UI)
|
|
cfg := Config{}
|
|
if err := cfg.ApplyDefaults(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if cfg.Listen != ":8080" || cfg.DefaultModel != "AUTO" {
|
|
t.Fatalf("defaults not applied: %+v", cfg)
|
|
}
|
|
}
|
|
|
|
func TestApplyDefaultsDuplicateModel(t *testing.T) {
|
|
cfg := Config{Sources: []Source{
|
|
{Name: "a", BaseURL: "http://x", Models: []Model{{ID: "m1"}}},
|
|
{Name: "b", BaseURL: "http://y", Models: []Model{{ID: "m1"}}},
|
|
}}
|
|
if err := cfg.ApplyDefaults(); err == nil {
|
|
t.Fatal("expected duplicate model error")
|
|
}
|
|
}
|
|
|
|
func TestStoreUpsertRemove(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "runtime.json")
|
|
s := NewStore(path)
|
|
if err := s.Load(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := s.Upsert(Source{Name: "a", BaseURL: "http://a", Models: []Model{{ID: "m"}}}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := s.Upsert(Source{Name: "b", BaseURL: "http://b", Models: []Model{{ID: "m2"}}}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(s.List()) != 2 {
|
|
t.Fatalf("list = %d", len(s.List()))
|
|
}
|
|
removed, err := s.Remove("a")
|
|
if err != nil || !removed {
|
|
t.Fatalf("remove: %v %v", removed, err)
|
|
}
|
|
if len(s.List()) != 1 {
|
|
t.Fatalf("after remove list = %d", len(s.List()))
|
|
}
|
|
// reload from disk
|
|
s2 := NewStore(path)
|
|
if err := s2.Load(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(s2.List()) != 1 {
|
|
t.Fatalf("reloaded list = %d", len(s2.List()))
|
|
}
|
|
} |