Skip to content

Commit bcc1b46

Browse files
authored
enhancement: list model status tracking (#1537)
## Summary Add key-level model discovery status tracking to improve visibility into API key health and model availability. ## Changes - Added model discovery status tracking for individual API keys - Enhanced `ListAllModels` to collect and return key status information - Added database columns to store model discovery status and error messages - Updated UI to display key status with visual indicators and tooltips - Improved error handling in model discovery process - Added automatic model discovery when adding or updating providers ## Type of change - [x] Feature - [x] Refactor ## Affected areas - [x] Core (Go) - [x] Transports (HTTP) - [x] UI (Next.js) ## How to test 1. Add a new provider with valid and invalid API keys 2. Observe the status indicators in the UI for each key 3. Hover over status icons to see detailed error messages for failed keys ```sh # Core/Transports go version go test ./... # UI cd ui pnpm i pnpm build ``` ## Screenshots/Recordings N/A ## Breaking changes - [x] No ## Related issues N/A ## Security considerations This change only exposes error information about API keys to authenticated users with appropriate permissions. ## Checklist - [x] I read `docs/contributing/README.md` and followed the guidelines - [x] I added/updated tests where appropriate - [x] I updated documentation where needed - [x] I verified builds succeed (Go and UI) - [x] I verified the CI pipeline passes locally if applicable
1 parent 77cee39 commit bcc1b46

File tree

26 files changed

+655
-144
lines changed

26 files changed

+655
-144
lines changed

core/bifrost.go

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -437,11 +437,10 @@ func (bifrost *Bifrost) ListModelsRequest(ctx *schemas.BifrostContext, req *sche
437437
return provider.ListModels(ctx, keys, request)
438438
}, schemas.ListModelsRequest, req.Provider, "", nil, bifrost.logger)
439439
if bifrostErr != nil {
440-
bifrostErr.ExtraFields = schemas.BifrostErrorExtraFields{
441-
RequestType: schemas.ListModelsRequest,
442-
Provider: req.Provider,
443-
}
444-
return nil, bifrostErr
440+
bifrostErr.ExtraFields.RequestType = schemas.ListModelsRequest
441+
bifrostErr.ExtraFields.Provider = req.Provider
442+
// Return response even on error if it contains KeyStatuses for tracking
443+
return response, bifrostErr
445444
}
446445
return response, nil
447446
}
@@ -471,8 +470,10 @@ func (bifrost *Bifrost) ListAllModels(ctx *schemas.BifrostContext, request *sche
471470

472471
// Result structure for collecting provider responses
473472
type providerResult struct {
474-
models []schemas.Model
475-
err *schemas.BifrostError
473+
provider schemas.ModelProvider
474+
models []schemas.Model
475+
keyStatuses []schemas.KeyStatus
476+
err *schemas.BifrostError
476477
}
477478

478479
results := make(chan providerResult, len(providerKeys))
@@ -489,6 +490,7 @@ func (bifrost *Bifrost) ListAllModels(ctx *schemas.BifrostContext, request *sche
489490
defer wg.Done()
490491

491492
providerModels := make([]schemas.Model, 0)
493+
var providerKeyStatuses []schemas.KeyStatus
492494
var providerErr *schemas.BifrostError
493495

494496
// Create request for this provider with limit of 1000
@@ -521,6 +523,10 @@ func (bifrost *Bifrost) ListAllModels(ctx *schemas.BifrostContext, request *sche
521523
providerErr = bifrostErr
522524
bifrost.logger.Warn("failed to list models for provider %s: %s", providerKey, GetErrorMessage(bifrostErr))
523525
}
526+
// Collect key statuses from error (failure case)
527+
if len(bifrostErr.ExtraFields.KeyStatuses) > 0 {
528+
providerKeyStatuses = append(providerKeyStatuses, bifrostErr.ExtraFields.KeyStatuses...)
529+
}
524530
break
525531
}
526532

@@ -530,6 +536,10 @@ func (bifrost *Bifrost) ListAllModels(ctx *schemas.BifrostContext, request *sche
530536

531537
providerModels = append(providerModels, response.Data...)
532538

539+
if len(response.KeyStatuses) > 0 {
540+
providerKeyStatuses = append(providerKeyStatuses, response.KeyStatuses...)
541+
}
542+
533543
// Check if there are more pages
534544
if response.NextPageToken == "" {
535545
break
@@ -539,29 +549,40 @@ func (bifrost *Bifrost) ListAllModels(ctx *schemas.BifrostContext, request *sche
539549
providerRequest.PageToken = response.NextPageToken
540550
}
541551

542-
results <- providerResult{models: providerModels, err: providerErr}
552+
results <- providerResult{
553+
provider: providerKey,
554+
models: providerModels,
555+
keyStatuses: providerKeyStatuses,
556+
err: providerErr,
557+
}
543558
}(providerKey)
544559
}
545560

546561
// Wait for all goroutines to complete
547562
wg.Wait()
548563
close(results)
549564

550-
// Accumulate all models from all providers
565+
// Accumulate all models and key statuses from all providers
551566
allModels := make([]schemas.Model, 0)
567+
allKeyStatuses := make([]schemas.KeyStatus, 0)
552568
var firstError *schemas.BifrostError
553569

554570
for result := range results {
555571
if len(result.models) > 0 {
556572
allModels = append(allModels, result.models...)
557573
}
574+
if len(result.keyStatuses) > 0 {
575+
allKeyStatuses = append(allKeyStatuses, result.keyStatuses...)
576+
}
558577
if result.err != nil && firstError == nil {
559578
firstError = result.err
560579
}
561580
}
562581

563582
// If we couldn't get any models from any provider, return the first error
564583
if len(allModels) == 0 && firstError != nil {
584+
// Attach all key statuses to the error
585+
firstError.ExtraFields.KeyStatuses = allKeyStatuses
565586
return nil, firstError
566587
}
567588

@@ -570,9 +591,10 @@ func (bifrost *Bifrost) ListAllModels(ctx *schemas.BifrostContext, request *sche
570591
return allModels[i].ID < allModels[j].ID
571592
})
572593

573-
// Return aggregated response with accumulated latency
594+
// Return aggregated response with accumulated latency and key statuses
574595
response := &schemas.BifrostListModelsResponse{
575-
Data: allModels,
596+
Data: allModels,
597+
KeyStatuses: allKeyStatuses,
576598
ExtraFields: schemas.BifrostResponseExtraFields{
577599
RequestType: schemas.ListModelsRequest,
578600
Latency: time.Since(startTime).Milliseconds(),

core/providers/anthropic/anthropic.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,9 @@ func (provider *AnthropicProvider) ListModels(ctx *schemas.BifrostContext, keys
245245
return nil, err
246246
}
247247
if provider.customProviderConfig != nil && provider.customProviderConfig.IsKeyLess {
248-
return provider.listModelsByKey(ctx, schemas.Key{}, request)
248+
return providerUtils.HandleKeylessListModelsRequest(schemas.Anthropic, func() (*schemas.BifrostListModelsResponse, *schemas.BifrostError) {
249+
return provider.listModelsByKey(ctx, schemas.Key{}, request)
250+
})
249251
}
250252
return providerUtils.HandleMultipleListModelsRequests(
251253
ctx,

core/providers/anthropic/utils.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1265,6 +1265,13 @@ func convertAnthropicOutputFormatToResponsesTextConfig(outputFormat interface{})
12651265
Type: formatType,
12661266
}
12671267

1268+
// Extract name if present
1269+
if name, ok := formatMap["name"].(string); ok && strings.TrimSpace(name) != "" {
1270+
format.Name = schemas.Ptr(strings.TrimSpace(name))
1271+
} else {
1272+
format.Name = schemas.Ptr("output_format")
1273+
}
1274+
12681275
// Extract schema if present
12691276
if schemaMap, ok := formatMap["schema"].(map[string]interface{}); ok {
12701277
jsonSchema := &schemas.ResponsesTextConfigFormatJSONSchema{}

core/providers/cohere/cohere.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,9 @@ func (provider *CohereProvider) ListModels(ctx *schemas.BifrostContext, keys []s
265265
return nil, err
266266
}
267267
if provider.customProviderConfig != nil && provider.customProviderConfig.IsKeyLess {
268-
return provider.listModelsByKey(ctx, schemas.Key{}, request)
268+
return providerUtils.HandleKeylessListModelsRequest(provider.GetProviderKey(), func() (*schemas.BifrostListModelsResponse, *schemas.BifrostError) {
269+
return provider.listModelsByKey(ctx, schemas.Key{}, request)
270+
})
269271
}
270272
return providerUtils.HandleMultipleListModelsRequests(
271273
ctx,

core/providers/gemini/gemini.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,9 @@ func (provider *GeminiProvider) ListModels(ctx *schemas.BifrostContext, keys []s
196196
return nil, err
197197
}
198198
if provider.customProviderConfig != nil && provider.customProviderConfig.IsKeyLess {
199-
return provider.listModelsByKey(ctx, schemas.Key{}, request)
199+
return providerUtils.HandleKeylessListModelsRequest(provider.GetProviderKey(), func() (*schemas.BifrostListModelsResponse, *schemas.BifrostError) {
200+
return provider.listModelsByKey(ctx, schemas.Key{}, request)
201+
})
200202
}
201203
return providerUtils.HandleMultipleListModelsRequests(
202204
ctx,

core/providers/huggingface/huggingface.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -423,7 +423,9 @@ func (provider *HuggingFaceProvider) ListModels(ctx *schemas.BifrostContext, key
423423
return nil, err
424424
}
425425
if provider.customProviderConfig != nil && provider.customProviderConfig.IsKeyLess {
426-
return provider.listModelsByKey(ctx, schemas.Key{}, request)
426+
return providerUtils.HandleKeylessListModelsRequest(provider.GetProviderKey(), func() (*schemas.BifrostListModelsResponse, *schemas.BifrostError) {
427+
return provider.listModelsByKey(ctx, schemas.Key{}, request)
428+
})
427429
}
428430
return providerUtils.HandleMultipleListModelsRequests(
429431
ctx,

core/providers/openai/openai.go

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -90,16 +90,18 @@ func (provider *OpenAIProvider) ListModels(ctx *schemas.BifrostContext, keys []s
9090
providerName := provider.GetProviderKey()
9191

9292
if provider.customProviderConfig != nil && provider.customProviderConfig.IsKeyLess {
93-
return listModelsByKey(
94-
ctx,
95-
provider.client,
96-
provider.buildRequestURL(ctx, "/v1/models", schemas.ListModelsRequest),
97-
schemas.Key{},
98-
provider.networkConfig.ExtraHeaders,
99-
providerName,
100-
providerUtils.ShouldSendBackRawRequest(ctx, provider.sendBackRawRequest),
101-
providerUtils.ShouldSendBackRawResponse(ctx, provider.sendBackRawResponse),
102-
)
93+
return providerUtils.HandleKeylessListModelsRequest(providerName, func() (*schemas.BifrostListModelsResponse, *schemas.BifrostError) {
94+
return listModelsByKey(
95+
ctx,
96+
provider.client,
97+
provider.buildRequestURL(ctx, "/v1/models", schemas.ListModelsRequest),
98+
schemas.Key{},
99+
provider.networkConfig.ExtraHeaders,
100+
providerName,
101+
providerUtils.ShouldSendBackRawRequest(ctx, provider.sendBackRawRequest),
102+
providerUtils.ShouldSendBackRawResponse(ctx, provider.sendBackRawResponse),
103+
)
104+
})
103105
}
104106

105107
return HandleOpenAIListModelsRequest(ctx,

core/providers/utils/utils.go

Lines changed: 65 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1599,30 +1599,50 @@ func aggregateListModelsResponses(responses []*schemas.BifrostListModelsResponse
15991599
}
16001600

16011601
// extractSuccessfulListModelsResponses extracts successful responses from a results channel
1602-
// and tracks the last error encountered. This utility reduces code duplication across providers
1602+
// and tracks per-key status information. This utility reduces code duplication across providers
16031603
// for handling multi-key ListModels requests.
16041604
func extractSuccessfulListModelsResponses(
16051605
results chan schemas.ListModelsByKeyResult,
16061606
providerName schemas.ModelProvider,
1607-
) ([]*schemas.BifrostListModelsResponse, *schemas.BifrostError) {
1607+
) ([]*schemas.BifrostListModelsResponse, []schemas.KeyStatus, *schemas.BifrostError) {
16081608
var successfulResponses []*schemas.BifrostListModelsResponse
1609+
var keyStatuses []schemas.KeyStatus
16091610
var lastError *schemas.BifrostError
16101611

16111612
for result := range results {
16121613
if result.Err != nil {
1613-
getLogger().Debug(fmt.Sprintf("failed to list models with key %s: %s", result.KeyID, result.Err.Error.Message))
1614+
errMsg := "unknown error"
1615+
if errorField := result.Err.Error; errorField != nil {
1616+
if errorField.Message != "" {
1617+
errMsg = errorField.Message
1618+
} else if errorField.Error != nil {
1619+
errMsg = errorField.Error.Error()
1620+
}
1621+
}
1622+
getLogger().Warn(fmt.Sprintf("failed to list models with key %s: %s", result.KeyID, errMsg))
1623+
keyStatuses = append(keyStatuses, schemas.KeyStatus{
1624+
KeyID: result.KeyID,
1625+
Provider: providerName,
1626+
Status: schemas.KeyStatusListModelsFailed,
1627+
Error: result.Err,
1628+
})
16141629
lastError = result.Err
16151630
continue
16161631
}
16171632

1633+
keyStatuses = append(keyStatuses, schemas.KeyStatus{
1634+
KeyID: result.KeyID,
1635+
Provider: providerName,
1636+
Status: schemas.KeyStatusSuccess,
1637+
})
16181638
successfulResponses = append(successfulResponses, result.Response)
16191639
}
16201640

16211641
if len(successfulResponses) == 0 {
16221642
if lastError != nil {
1623-
return nil, lastError
1643+
return nil, keyStatuses, lastError
16241644
}
1625-
return nil, &schemas.BifrostError{
1645+
return nil, keyStatuses, &schemas.BifrostError{
16261646
IsBifrostError: false,
16271647
Error: &schemas.ErrorField{
16281648
Message: "all keys failed to list models",
@@ -1634,12 +1654,44 @@ func extractSuccessfulListModelsResponses(
16341654
}
16351655
}
16361656

1637-
return successfulResponses, nil
1657+
return successfulResponses, keyStatuses, nil
1658+
}
1659+
1660+
// HandleKeylessListModelsRequest wraps a list models request for keyless providers
1661+
// and automatically populates the KeyStatuses field with provider-level status tracking.
1662+
// This centralizes the status tracking logic for keyless providers.
1663+
func HandleKeylessListModelsRequest(
1664+
provider schemas.ModelProvider,
1665+
listFunc func() (*schemas.BifrostListModelsResponse, *schemas.BifrostError),
1666+
) (*schemas.BifrostListModelsResponse, *schemas.BifrostError) {
1667+
resp, bifrostErr := listFunc()
1668+
1669+
keyStatus := schemas.KeyStatus{
1670+
KeyID: "", // Empty for keyless providers
1671+
Provider: provider,
1672+
}
1673+
1674+
// If request failed, attach status to error
1675+
if bifrostErr != nil {
1676+
keyStatus.Status = schemas.KeyStatusListModelsFailed
1677+
keyStatus.Error = bifrostErr
1678+
bifrostErr.ExtraFields.KeyStatuses = []schemas.KeyStatus{keyStatus}
1679+
return nil, bifrostErr
1680+
}
1681+
1682+
// Success case
1683+
if resp != nil {
1684+
keyStatus.Status = schemas.KeyStatusSuccess
1685+
resp.KeyStatuses = []schemas.KeyStatus{keyStatus}
1686+
return resp, nil
1687+
}
1688+
1689+
return resp, bifrostErr
16381690
}
16391691

16401692
// HandleMultipleListModelsRequests handles multiple list models requests concurrently for different keys.
16411693
// It launches concurrent requests for all keys and waits for all goroutines to complete.
1642-
// It returns the aggregated response or an error if the request fails.
1694+
// It returns the aggregated response with per-key status information or an error if the request fails.
16431695
func HandleMultipleListModelsRequests(
16441696
ctx *schemas.BifrostContext,
16451697
keys []schemas.Key,
@@ -1665,15 +1717,20 @@ func HandleMultipleListModelsRequests(
16651717
wg.Wait()
16661718
close(results)
16671719

1668-
successfulResponses, err := extractSuccessfulListModelsResponses(results, request.Provider)
1720+
successfulResponses, keyStatuses, err := extractSuccessfulListModelsResponses(results, request.Provider)
16691721
if err != nil {
1722+
// Attach key statuses to error's ExtraFields
1723+
err.ExtraFields.KeyStatuses = keyStatuses
16701724
return nil, err
16711725
}
16721726

16731727
// Aggregate all successful responses
16741728
response := aggregateListModelsResponses(successfulResponses)
16751729
response = response.ApplyPagination(request.PageSize, request.PageToken)
16761730

1731+
// Attach key statuses to response
1732+
response.KeyStatuses = keyStatuses
1733+
16771734
// Set ExtraFields
16781735
latency := time.Since(startTime)
16791736
response.ExtraFields.Provider = request.Provider

core/schemas/account.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,13 @@ package schemas
33

44
import "context"
55

6+
type KeyStatusType string
7+
8+
const (
9+
KeyStatusSuccess KeyStatusType = "success"
10+
KeyStatusListModelsFailed KeyStatusType = "list_models_failed"
11+
)
12+
613
// Key represents an API key and its associated configuration for a provider.
714
// It contains the key value, supported models, and a weight for load balancing.
815
type Key struct {
@@ -19,6 +26,8 @@ type Key struct {
1926
Enabled *bool `json:"enabled,omitempty"` // Whether the key is active (default:true)
2027
UseForBatchAPI *bool `json:"use_for_batch_api,omitempty"` // Whether this key can be used for batch API operations (default:false for new keys, migrated keys default to true)
2128
ConfigHash string `json:"config_hash,omitempty"` // Hash of config.json version, used for change detection
29+
Status KeyStatusType `json:"status,omitempty"` // Status of key
30+
Description string `json:"description,omitempty"` // Description of key
2231
}
2332

2433
// AzureKeyConfig represents the Azure-specific configuration.

core/schemas/bifrost.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -758,4 +758,5 @@ type BifrostErrorExtraFields struct {
758758
RawRequest interface{} `json:"raw_request,omitempty"`
759759
RawResponse interface{} `json:"raw_response,omitempty"`
760760
LiteLLMCompat bool `json:"litellm_compat,omitempty"`
761+
KeyStatuses []KeyStatus `json:"-"`
761762
}

0 commit comments

Comments
 (0)