first commit

This commit is contained in:
Beyhan Oğur
2026-04-26 21:52:23 +03:00
commit 880f412e2c
2662 changed files with 866266 additions and 0 deletions

View File

@@ -0,0 +1,550 @@
package handlers
import (
"context"
"encoding/json"
"testing"
"github.com/bytedance/sonic"
"github.com/maximhq/bifrost/core/schemas"
"github.com/maximhq/bifrost/framework/configstore"
configstoreTables "github.com/maximhq/bifrost/framework/configstore/tables"
"github.com/maximhq/bifrost/framework/modelcatalog"
"github.com/maximhq/bifrost/transports/bifrost-http/lib"
"github.com/valyala/fasthttp"
)
// mockModelsManager returns stable filtered and unfiltered model lists for handler tests.
type mockModelsManager struct {
filtered map[schemas.ModelProvider][]string
unfiltered map[schemas.ModelProvider][]string
reloadCalls []schemas.ModelProvider
reloadErr error
}
func (m *mockModelsManager) ReloadProvider(_ context.Context, provider schemas.ModelProvider) (*configstoreTables.TableProvider, error) {
m.reloadCalls = append(m.reloadCalls, provider)
if m.reloadErr != nil {
return nil, m.reloadErr
}
return nil, nil
}
func (m *mockModelsManager) RemoveProvider(_ context.Context, _ schemas.ModelProvider) error {
return nil
}
func (m *mockModelsManager) GetModelsForProvider(provider schemas.ModelProvider) []string {
models := m.filtered[provider]
result := make([]string, len(models))
copy(result, models)
return result
}
func (m *mockModelsManager) GetUnfilteredModelsForProvider(provider schemas.ModelProvider) []string {
models := m.unfiltered[provider]
result := make([]string, len(models))
copy(result, models)
return result
}
// providerHandlerForTest builds a handler with fixed provider config and model sets.
func providerHandlerForTest(provider schemas.ModelProvider, keys []schemas.Key, filtered, unfiltered []string) *ProviderHandler {
return &ProviderHandler{
inMemoryStore: &lib.Config{
Providers: map[schemas.ModelProvider]configstore.ProviderConfig{
provider: {
Keys: keys,
},
},
},
modelsManager: &mockModelsManager{
filtered: map[schemas.ModelProvider][]string{
provider: filtered,
},
unfiltered: map[schemas.ModelProvider][]string{
provider: unfiltered,
},
},
}
}
func TestAddProvider_ReloadsRuntimeEvenWhenModelDiscoveryIsSkipped(t *testing.T) {
SetLogger(&mockLogger{})
lib.SetLogger(&mockLogger{})
modelsManager := &mockModelsManager{}
h := &ProviderHandler{
inMemoryStore: &lib.Config{Providers: map[schemas.ModelProvider]configstore.ProviderConfig{}},
modelsManager: modelsManager,
}
body, err := sonic.Marshal(providerCreatePayload{
Provider: "mock-openai",
CustomProviderConfig: &schemas.CustomProviderConfig{
BaseProviderType: schemas.OpenAI,
IsKeyLess: true,
},
})
if err != nil {
t.Fatalf("failed to marshal request body: %v", err)
}
ctx := &fasthttp.RequestCtx{}
ctx.Request.Header.SetMethod(fasthttp.MethodPost)
ctx.Request.SetRequestURI("/api/providers")
ctx.Request.SetBody(body)
h.addProvider(ctx)
if ctx.Response.StatusCode() != fasthttp.StatusOK {
t.Fatalf("expected 200, got %d: %s", ctx.Response.StatusCode(), string(ctx.Response.Body()))
}
if len(modelsManager.reloadCalls) != 1 || modelsManager.reloadCalls[0] != "mock-openai" {
t.Fatalf("expected provider reload for mock-openai, got %#v", modelsManager.reloadCalls)
}
if _, exists := h.inMemoryStore.Providers["mock-openai"]; !exists {
t.Fatalf("expected provider to be added to in-memory store")
}
}
func TestAddProvider_ReturnsErrorWhenRuntimeReloadFails(t *testing.T) {
SetLogger(&mockLogger{})
lib.SetLogger(&mockLogger{})
modelsManager := &mockModelsManager{reloadErr: context.DeadlineExceeded}
h := &ProviderHandler{
inMemoryStore: &lib.Config{Providers: map[schemas.ModelProvider]configstore.ProviderConfig{}},
modelsManager: modelsManager,
}
body, err := sonic.Marshal(providerCreatePayload{
Provider: "mock-openai",
CustomProviderConfig: &schemas.CustomProviderConfig{
BaseProviderType: schemas.OpenAI,
IsKeyLess: true,
},
})
if err != nil {
t.Fatalf("failed to marshal request body: %v", err)
}
ctx := &fasthttp.RequestCtx{}
ctx.Request.Header.SetMethod(fasthttp.MethodPost)
ctx.Request.SetRequestURI("/api/providers")
ctx.Request.SetBody(body)
h.addProvider(ctx)
if ctx.Response.StatusCode() != fasthttp.StatusInternalServerError {
t.Fatalf("expected 500, got %d: %s", ctx.Response.StatusCode(), string(ctx.Response.Body()))
}
if len(modelsManager.reloadCalls) != 1 || modelsManager.reloadCalls[0] != "mock-openai" {
t.Fatalf("expected single provider reload for mock-openai, got %#v", modelsManager.reloadCalls)
}
var bifrostErr schemas.BifrostError
if err := json.Unmarshal(ctx.Response.Body(), &bifrostErr); err != nil {
t.Fatalf("failed to unmarshal error response: %v", err)
}
if bifrostErr.Error == nil || bifrostErr.Error.Message == "" {
t.Fatalf("expected error message in response, got %#v", bifrostErr)
}
if bifrostErr.Error.Message != "Failed to initialize provider after add: context deadline exceeded" {
t.Fatalf("unexpected error message: %q", bifrostErr.Error.Message)
}
if _, exists := h.inMemoryStore.Providers["mock-openai"]; exists {
t.Fatalf("expected provider rollback after reload failure")
}
}
// boolPtr keeps pointer-valued key fixtures inline without pulling in pointer helpers.
func boolPtr(v bool) *bool {
return &v
}
func TestListModels_UnknownKeysDoNotFilter(t *testing.T) {
SetLogger(&mockLogger{})
h := providerHandlerForTest(
schemas.OpenAI,
[]schemas.Key{{ID: "key-a"}},
[]string{"gpt-4o", "gpt-4o-mini"},
[]string{"gpt-4o", "gpt-4o-mini"},
)
ctx := &fasthttp.RequestCtx{}
ctx.Request.Header.SetMethod("GET")
ctx.Request.SetRequestURI("/api/models?provider=openai&keys=missing")
h.listModels(ctx)
if ctx.Response.StatusCode() != fasthttp.StatusOK {
t.Fatalf("expected 200, got %d: %s", ctx.Response.StatusCode(), string(ctx.Response.Body()))
}
var resp ListModelsResponse
if err := json.Unmarshal(ctx.Response.Body(), &resp); err != nil {
t.Fatalf("failed to unmarshal response: %v", err)
}
if resp.Total != 2 {
t.Fatalf("expected total=2, got %d", resp.Total)
}
if len(resp.Models) != 2 {
t.Fatalf("expected all models to be returned, got %#v", resp.Models)
}
for _, model := range resp.Models {
if len(model.AccessibleByKeys) != 0 {
t.Fatalf("expected no accessible_by_keys annotations, got %#v", resp.Models)
}
}
}
func TestListModels_ReturnsExactAccessibleByKeysAndSkipsDisabledKeys(t *testing.T) {
SetLogger(&mockLogger{})
h := providerHandlerForTest(
schemas.OpenAI,
[]schemas.Key{
{ID: "key-a", Models: []string{"gpt-4o"}},
{ID: "key-b", Models: []string{"gpt-4o", "gpt-4o-mini"}},
{ID: "key-disabled", Enabled: boolPtr(false)},
},
[]string{"gpt-4o", "gpt-4o-mini"},
[]string{"gpt-4o", "gpt-4o-mini"},
)
ctx := &fasthttp.RequestCtx{}
ctx.Request.Header.SetMethod("GET")
ctx.Request.SetRequestURI("/api/models?provider=openai&keys=key-a,key-b,key-disabled")
h.listModels(ctx)
if ctx.Response.StatusCode() != fasthttp.StatusOK {
t.Fatalf("expected 200, got %d: %s", ctx.Response.StatusCode(), string(ctx.Response.Body()))
}
var resp ListModelsResponse
if err := json.Unmarshal(ctx.Response.Body(), &resp); err != nil {
t.Fatalf("failed to unmarshal response: %v", err)
}
if resp.Total != 2 {
t.Fatalf("expected total=2, got %d", resp.Total)
}
got := map[string][]string{}
for _, model := range resp.Models {
got[model.Name] = model.AccessibleByKeys
}
if len(got["gpt-4o"]) != 2 || got["gpt-4o"][0] != "key-a" || got["gpt-4o"][1] != "key-b" {
t.Fatalf("expected gpt-4o to be accessible by [key-a key-b], got %#v", got["gpt-4o"])
}
if len(got["gpt-4o-mini"]) != 1 || got["gpt-4o-mini"][0] != "key-b" {
t.Fatalf("expected gpt-4o-mini to be accessible by [key-b], got %#v", got["gpt-4o-mini"])
}
}
func TestListModels_AppliesQueryAndLimitAfterFiltering(t *testing.T) {
SetLogger(&mockLogger{})
h := providerHandlerForTest(
schemas.OpenAI,
[]schemas.Key{{ID: "key-a"}},
[]string{"gpt-4o", "gpt-4o-mini", "claude-3-5-sonnet"},
[]string{"gpt-4o", "gpt-4o-mini", "claude-3-5-sonnet"},
)
ctx := &fasthttp.RequestCtx{}
ctx.Request.Header.SetMethod("GET")
ctx.Request.SetRequestURI("/api/models?provider=openai&query=gpt&limit=1")
h.listModels(ctx)
if ctx.Response.StatusCode() != fasthttp.StatusOK {
t.Fatalf("expected 200, got %d: %s", ctx.Response.StatusCode(), string(ctx.Response.Body()))
}
var resp ListModelsResponse
if err := json.Unmarshal(ctx.Response.Body(), &resp); err != nil {
t.Fatalf("failed to unmarshal response: %v", err)
}
if resp.Total != 2 {
t.Fatalf("expected total=2 after query filtering, got %d", resp.Total)
}
if len(resp.Models) != 1 {
t.Fatalf("expected limit to truncate response to 1 model, got %#v", resp.Models)
}
if resp.Models[0].Name != "gpt-4o" {
t.Fatalf("expected first filtered model to be gpt-4o, got %#v", resp.Models[0])
}
}
func TestListModels_UnfilteredIgnoresKeys(t *testing.T) {
SetLogger(&mockLogger{})
h := providerHandlerForTest(
schemas.OpenAI,
[]schemas.Key{
{ID: "key-b", Models: []string{"gpt-4o-mini"}},
},
[]string{"gpt-4o"},
[]string{"gpt-4o", "gpt-4o-mini"},
)
ctx := &fasthttp.RequestCtx{}
ctx.Request.Header.SetMethod("GET")
ctx.Request.SetRequestURI("/api/models?provider=openai&keys=key-b&unfiltered=true")
h.listModels(ctx)
if ctx.Response.StatusCode() != fasthttp.StatusOK {
t.Fatalf("expected 200, got %d: %s", ctx.Response.StatusCode(), string(ctx.Response.Body()))
}
var resp ListModelsResponse
if err := json.Unmarshal(ctx.Response.Body(), &resp); err != nil {
t.Fatalf("failed to unmarshal response: %v", err)
}
if resp.Total != 2 || len(resp.Models) != 2 {
t.Fatalf("expected both unfiltered models, got %#v", resp.Models)
}
for _, model := range resp.Models {
if len(model.AccessibleByKeys) != 0 {
t.Fatalf("expected no accessible_by_keys when unfiltered bypasses key filtering, got %#v", resp.Models)
}
}
}
func TestListModels_UnfilteredWithoutKeysReturnsAllUnfilteredModels(t *testing.T) {
SetLogger(&mockLogger{})
h := providerHandlerForTest(
schemas.OpenAI,
[]schemas.Key{
{ID: "key-b", Models: []string{"gpt-4o-mini"}},
},
[]string{"gpt-4o"},
[]string{"gpt-4o", "gpt-4o-mini"},
)
ctx := &fasthttp.RequestCtx{}
ctx.Request.Header.SetMethod("GET")
ctx.Request.SetRequestURI("/api/models?provider=openai&unfiltered=true")
h.listModels(ctx)
if ctx.Response.StatusCode() != fasthttp.StatusOK {
t.Fatalf("expected 200, got %d: %s", ctx.Response.StatusCode(), string(ctx.Response.Body()))
}
var resp ListModelsResponse
if err := json.Unmarshal(ctx.Response.Body(), &resp); err != nil {
t.Fatalf("failed to unmarshal response: %v", err)
}
if resp.Total != 2 || len(resp.Models) != 2 {
t.Fatalf("expected both unfiltered models, got %#v", resp.Models)
}
for _, model := range resp.Models {
if len(model.AccessibleByKeys) != 0 {
t.Fatalf("expected no accessible_by_keys when no key filter is requested, got %#v", resp.Models)
}
}
}
func TestListModelDetails_ErrorsWhenModelCatalogUnavailable(t *testing.T) {
SetLogger(&mockLogger{})
h := providerHandlerForTest(
schemas.OpenAI,
[]schemas.Key{{ID: "key-a"}},
[]string{"gpt-4o"},
[]string{"gpt-4o"},
)
ctx := &fasthttp.RequestCtx{}
ctx.Request.Header.SetMethod("GET")
ctx.Request.SetRequestURI("/api/models/details?provider=openai")
h.listModelDetails(ctx)
if ctx.Response.StatusCode() != fasthttp.StatusInternalServerError {
t.Fatalf("expected 500, got %d: %s", ctx.Response.StatusCode(), string(ctx.Response.Body()))
}
}
func TestListModelDetails_UnknownKeysDoNotFilter(t *testing.T) {
SetLogger(&mockLogger{})
h := providerHandlerForTest(
schemas.OpenAI,
[]schemas.Key{{ID: "key-a"}},
[]string{"gpt-4o", "gpt-4o-mini"},
[]string{"gpt-4o", "gpt-4o-mini"},
)
h.inMemoryStore.ModelCatalog = &modelcatalog.ModelCatalog{}
ctx := &fasthttp.RequestCtx{}
ctx.Request.Header.SetMethod("GET")
ctx.Request.SetRequestURI("/api/models/details?provider=openai&keys=missing")
h.listModelDetails(ctx)
if ctx.Response.StatusCode() != fasthttp.StatusOK {
t.Fatalf("expected 200, got %d: %s", ctx.Response.StatusCode(), string(ctx.Response.Body()))
}
var resp ListModelDetailsResponse
if err := json.Unmarshal(ctx.Response.Body(), &resp); err != nil {
t.Fatalf("failed to unmarshal response: %v", err)
}
if resp.Total != 2 || len(resp.Models) != 2 {
t.Fatalf("expected all models when keys are unknown, got %#v", resp.Models)
}
}
func TestListModelDetails_SkipsUnknownKeysAndFiltersWithValid(t *testing.T) {
SetLogger(&mockLogger{})
h := providerHandlerForTest(
schemas.OpenAI,
[]schemas.Key{{ID: "key-a", Models: []string{"gpt-4o"}}},
[]string{"gpt-4o", "gpt-4o-mini"},
[]string{"gpt-4o", "gpt-4o-mini"},
)
h.inMemoryStore.ModelCatalog = &modelcatalog.ModelCatalog{}
ctx := &fasthttp.RequestCtx{}
ctx.Request.Header.SetMethod("GET")
ctx.Request.SetRequestURI("/api/models/details?provider=openai&keys=key-a,missing")
h.listModelDetails(ctx)
if ctx.Response.StatusCode() != fasthttp.StatusOK {
t.Fatalf("expected 200, got %d: %s", ctx.Response.StatusCode(), string(ctx.Response.Body()))
}
var resp ListModelDetailsResponse
if err := json.Unmarshal(ctx.Response.Body(), &resp); err != nil {
t.Fatalf("failed to unmarshal response: %v", err)
}
if resp.Total != 1 || len(resp.Models) != 1 {
t.Fatalf("expected 1 model filtered by valid key, got %#v", resp.Models)
}
if resp.Models[0].Name != "gpt-4o" {
t.Fatalf("expected gpt-4o, got %s", resp.Models[0].Name)
}
}
func TestListModelDetails_SkipsDisabledKeysAndFiltersWithValid(t *testing.T) {
SetLogger(&mockLogger{})
h := providerHandlerForTest(
schemas.OpenAI,
[]schemas.Key{
{ID: "key-a", Models: []string{"gpt-4o"}},
{ID: "key-disabled", Enabled: boolPtr(false)},
},
[]string{"gpt-4o", "gpt-4o-mini"},
[]string{"gpt-4o", "gpt-4o-mini"},
)
h.inMemoryStore.ModelCatalog = &modelcatalog.ModelCatalog{}
ctx := &fasthttp.RequestCtx{}
ctx.Request.Header.SetMethod("GET")
ctx.Request.SetRequestURI("/api/models/details?provider=openai&keys=key-a,key-disabled")
h.listModelDetails(ctx)
if ctx.Response.StatusCode() != fasthttp.StatusOK {
t.Fatalf("expected 200, got %d: %s", ctx.Response.StatusCode(), string(ctx.Response.Body()))
}
var resp ListModelDetailsResponse
if err := json.Unmarshal(ctx.Response.Body(), &resp); err != nil {
t.Fatalf("failed to unmarshal response: %v", err)
}
if resp.Total != 1 || len(resp.Models) != 1 {
t.Fatalf("expected 1 model filtered by valid key, got %#v", resp.Models)
}
if resp.Models[0].Name != "gpt-4o" {
t.Fatalf("expected gpt-4o, got %s", resp.Models[0].Name)
}
}
func TestListModelDetails_UnfilteredIgnoresKeys(t *testing.T) {
SetLogger(&mockLogger{})
h := providerHandlerForTest(
schemas.OpenAI,
[]schemas.Key{
{ID: "key-b", Models: []string{"gpt-4o-mini"}},
},
[]string{"gpt-4o"},
[]string{"gpt-4o", "gpt-4o-mini"},
)
h.inMemoryStore.ModelCatalog = &modelcatalog.ModelCatalog{}
ctx := &fasthttp.RequestCtx{}
ctx.Request.Header.SetMethod("GET")
ctx.Request.SetRequestURI("/api/models/details?provider=openai&keys=key-b&unfiltered=true")
h.listModelDetails(ctx)
if ctx.Response.StatusCode() != fasthttp.StatusOK {
t.Fatalf("expected 200, got %d: %s", ctx.Response.StatusCode(), string(ctx.Response.Body()))
}
var resp ListModelDetailsResponse
if err := json.Unmarshal(ctx.Response.Body(), &resp); err != nil {
t.Fatalf("failed to unmarshal response: %v", err)
}
if resp.Total != 2 || len(resp.Models) != 2 {
t.Fatalf("expected all unfiltered models when unfiltered=true, got %#v", resp.Models)
}
}
func TestListModels_UsesCatalogAwareAliasMatchingForKeyAllowlist(t *testing.T) {
SetLogger(&mockLogger{})
h := providerHandlerForTest(
schemas.OpenAI,
[]schemas.Key{
{ID: "key-a", Models: []string{"gpt-4o-2024-08-06"}},
},
[]string{"gpt-4o"},
[]string{"gpt-4o"},
)
h.inMemoryStore.ModelCatalog = modelcatalog.NewTestCatalog(map[string]string{
"gpt-4o-2024-08-06": "gpt-4o",
})
ctx := &fasthttp.RequestCtx{}
ctx.Request.Header.SetMethod("GET")
ctx.Request.SetRequestURI("/api/models?provider=openai&keys=key-a")
h.listModels(ctx)
if ctx.Response.StatusCode() != fasthttp.StatusOK {
t.Fatalf("expected 200, got %d: %s", ctx.Response.StatusCode(), string(ctx.Response.Body()))
}
var resp ListModelsResponse
if err := json.Unmarshal(ctx.Response.Body(), &resp); err != nil {
t.Fatalf("failed to unmarshal response: %v", err)
}
if resp.Total != 1 || len(resp.Models) != 1 || resp.Models[0].Name != "gpt-4o" {
t.Fatalf("expected gpt-4o to be matched through alias allowlist, got %#v", resp.Models)
}
}