ai-servers/llm-gateway/internal/provider/registry_test.go
Ray Andrew 90adf6f3a8
feat(gateway): add circuit breaker, retry, and concurrency limit support
feat(gateway): add debug logging with file storage and retention

feat(gateway): add audit logging for user actions

feat(gateway): add request ID tracking and rate limit headers

feat(gateway): add model aliases and load balancing strategies

feat(gateway): add config hot-reload via SIGHUP

feat(gateway): add CORS support

feat(gateway): add data export API and dashboard endpoints

feat(gateway): add dashboard pages for audit and debug logs

feat(gateway): add concurrent request limiting per token

feat(gateway): add streaming timeout support

feat(gateway): add migration support for new schema fields
2026-02-15 04:21:40 -06:00

282 lines
6.7 KiB
Go

package provider
import (
"context"
"io"
"testing"
"llm-gateway/internal/config"
)
// mockProvider implements the Provider interface for testing.
type mockProvider struct {
name string
}
func (m *mockProvider) Name() string { return m.name }
func (m *mockProvider) ChatCompletion(_ context.Context, _ string, _ *ChatRequest) (*ChatResponse, error) {
return nil, nil
}
func (m *mockProvider) ChatCompletionStream(_ context.Context, _ string, _ *ChatRequest) (io.ReadCloser, error) {
return nil, nil
}
// newTestRegistry builds a Registry directly without going through config parsing.
func newTestRegistry(models []testModel) *Registry {
r := &Registry{
routes: make(map[string][]Route),
balancers: make(map[string]LoadBalancer),
aliases: make(map[string]string),
}
for _, m := range models {
r.routes[m.name] = m.routes
r.balancers[m.name] = &FirstBalancer{}
r.order = append(r.order, m.name)
for _, alias := range m.aliases {
r.aliases[alias] = m.name
}
}
return r
}
type testModel struct {
name string
aliases []string
routes []Route
}
func TestRegistry_Lookup_Canonical(t *testing.T) {
reg := newTestRegistry([]testModel{
{
name: "gpt-4",
routes: []Route{
{Provider: &mockProvider{name: "openai"}, ProviderModel: "gpt-4", Priority: 1},
},
},
})
routes, ok := reg.Lookup("gpt-4")
if !ok {
t.Fatal("expected Lookup to find gpt-4")
}
if len(routes) != 1 {
t.Fatalf("expected 1 route, got %d", len(routes))
}
if routes[0].Provider.Name() != "openai" {
t.Errorf("expected provider 'openai', got %q", routes[0].Provider.Name())
}
}
func TestRegistry_Lookup_Alias(t *testing.T) {
reg := newTestRegistry([]testModel{
{
name: "gpt-4",
aliases: []string{"gpt4", "gpt-4-latest"},
routes: []Route{
{Provider: &mockProvider{name: "openai"}, ProviderModel: "gpt-4", Priority: 1},
},
},
})
tests := []struct {
name string
model string
found bool
}{
{"canonical", "gpt-4", true},
{"alias1", "gpt4", true},
{"alias2", "gpt-4-latest", true},
{"unknown", "gpt-5", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
routes, ok := reg.Lookup(tt.model)
if ok != tt.found {
t.Fatalf("Lookup(%q) found=%v, want %v", tt.model, ok, tt.found)
}
if tt.found && len(routes) != 1 {
t.Fatalf("expected 1 route, got %d", len(routes))
}
})
}
}
func TestRegistry_ModelNames_IncludesAliases(t *testing.T) {
reg := newTestRegistry([]testModel{
{
name: "gpt-4",
aliases: []string{"gpt4"},
routes: []Route{
{Provider: &mockProvider{name: "openai"}, ProviderModel: "gpt-4", Priority: 1},
},
},
{
name: "claude-3",
routes: []Route{
{Provider: &mockProvider{name: "anthropic"}, ProviderModel: "claude-3", Priority: 1},
},
},
})
names := reg.ModelNames()
want := map[string]bool{"gpt-4": true, "gpt4": true, "claude-3": true}
got := make(map[string]bool)
for _, n := range names {
got[n] = true
}
for name := range want {
if !got[name] {
t.Errorf("expected %q in ModelNames, not found", name)
}
}
if len(names) != len(want) {
t.Errorf("expected %d names, got %d: %v", len(want), len(names), names)
}
}
func TestRegistry_AllRoutes_ShowsAliases(t *testing.T) {
reg := newTestRegistry([]testModel{
{
name: "gpt-4",
aliases: []string{"gpt4", "gpt-4-latest"},
routes: []Route{
{Provider: &mockProvider{name: "openai"}, ProviderModel: "gpt-4", Priority: 1},
{Provider: &mockProvider{name: "azure"}, ProviderModel: "gpt-4", Priority: 2},
},
},
})
allRoutes := reg.AllRoutes()
if len(allRoutes) != 1 {
t.Fatalf("expected 1 model, got %d", len(allRoutes))
}
m := allRoutes[0]
if m.Name != "gpt-4" {
t.Errorf("expected name 'gpt-4', got %q", m.Name)
}
aliasSet := make(map[string]bool)
for _, a := range m.Aliases {
aliasSet[a] = true
}
if !aliasSet["gpt4"] || !aliasSet["gpt-4-latest"] {
t.Errorf("expected aliases [gpt4, gpt-4-latest], got %v", m.Aliases)
}
if len(m.Routes) != 2 {
t.Fatalf("expected 2 routes, got %d", len(m.Routes))
}
if m.Routes[0].ProviderName != "openai" {
t.Errorf("expected first route provider 'openai', got %q", m.Routes[0].ProviderName)
}
if m.Routes[1].ProviderName != "azure" {
t.Errorf("expected second route provider 'azure', got %q", m.Routes[1].ProviderName)
}
}
func TestRegistry_AllRoutes_ConfigOrder(t *testing.T) {
reg := newTestRegistry([]testModel{
{
name: "model-b",
routes: []Route{
{Provider: &mockProvider{name: "prov"}, ProviderModel: "b", Priority: 1},
},
},
{
name: "model-a",
routes: []Route{
{Provider: &mockProvider{name: "prov"}, ProviderModel: "a", Priority: 1},
},
},
})
allRoutes := reg.AllRoutes()
if len(allRoutes) != 2 {
t.Fatalf("expected 2 models, got %d", len(allRoutes))
}
if allRoutes[0].Name != "model-b" {
t.Errorf("expected first model 'model-b', got %q", allRoutes[0].Name)
}
if allRoutes[1].Name != "model-a" {
t.Errorf("expected second model 'model-a', got %q", allRoutes[1].Name)
}
}
func TestRegistry_PrioritySorting(t *testing.T) {
reg := newTestRegistry([]testModel{
{
name: "multi-provider",
routes: []Route{
{Provider: &mockProvider{name: "low-priority"}, ProviderModel: "m", Priority: 3},
{Provider: &mockProvider{name: "high-priority"}, ProviderModel: "m", Priority: 1},
{Provider: &mockProvider{name: "mid-priority"}, ProviderModel: "m", Priority: 2},
},
},
})
// Note: routes are stored as given (sorting happens during buildFromConfig).
// For this test we verify AllRoutes returns them in stored order.
allRoutes := reg.AllRoutes()
if len(allRoutes) != 1 {
t.Fatalf("expected 1 model, got %d", len(allRoutes))
}
routes := allRoutes[0].Routes
if len(routes) != 3 {
t.Fatalf("expected 3 routes, got %d", len(routes))
}
// Verify the priorities are present
priorities := make(map[int]bool)
for _, r := range routes {
priorities[r.Priority] = true
}
for _, p := range []int{1, 2, 3} {
if !priorities[p] {
t.Errorf("expected priority %d in routes", p)
}
}
}
func TestRegistry_NewRegistry_UnknownProvider(t *testing.T) {
cfg := &config.Config{
Models: []config.ModelConfig{
{
Name: "test-model",
Routes: []config.RouteConfig{
{Provider: "nonexistent", Model: "m"},
},
},
},
}
_, err := NewRegistry(cfg)
if err == nil {
t.Fatal("expected error for unknown provider, got nil")
}
}
func TestRegistry_Lookup_NotFound(t *testing.T) {
reg := newTestRegistry([]testModel{
{
name: "gpt-4",
routes: []Route{
{Provider: &mockProvider{name: "openai"}, ProviderModel: "gpt-4", Priority: 1},
},
},
})
_, ok := reg.Lookup("nonexistent")
if ok {
t.Fatal("expected Lookup to return false for nonexistent model")
}
}