@@ -4,12 +4,14 @@ import (
44 "context"
55 "fmt"
66 "log/slog"
7+ "slices"
78 "strings"
89
910 "github.com/docker/cagent/pkg/config/latest"
1011 "github.com/docker/cagent/pkg/environment"
1112 "github.com/docker/cagent/pkg/model/provider"
1213 "github.com/docker/cagent/pkg/model/provider/options"
14+ "github.com/docker/cagent/pkg/modelsdev"
1315)
1416
1517// ModelChoice represents a model available for selection in the TUI picker.
@@ -28,6 +30,8 @@ type ModelChoice struct {
2830 IsCurrent bool
2931 // IsCustom indicates this is a custom model from the session history (not from config)
3032 IsCustom bool
33+ // IsCatalog indicates this is a model from the models.dev catalog
34+ IsCatalog bool
3135}
3236
3337// ModelSwitcher is an optional interface for runtimes that support changing the model
@@ -249,7 +253,7 @@ func (r *LocalRuntime) createProvidersFromAlloyConfig(ctx context.Context, alloy
249253}
250254
251255// AvailableModels implements ModelSwitcher for LocalRuntime.
252- func (r * LocalRuntime ) AvailableModels (_ context.Context ) []ModelChoice {
256+ func (r * LocalRuntime ) AvailableModels (ctx context.Context ) []ModelChoice {
253257 var choices []ModelChoice
254258
255259 if r .modelSwitcherCfg == nil {
@@ -273,9 +277,178 @@ func (r *LocalRuntime) AvailableModels(_ context.Context) []ModelChoice {
273277 })
274278 }
275279
280+ // Append models.dev catalog entries filtered by available credentials
281+ catalogChoices := r .buildCatalogChoices (ctx )
282+ choices = append (choices , catalogChoices ... )
283+
276284 return choices
277285}
278286
287+ // CatalogStore is an extended interface for model stores that support fetching the full database.
288+ type CatalogStore interface {
289+ ModelStore
290+ GetDatabase (ctx context.Context ) (* modelsdev.Database , error )
291+ }
292+
293+ // buildCatalogChoices builds ModelChoice entries from the models.dev catalog,
294+ // filtered by supported providers and available credentials.
295+ func (r * LocalRuntime ) buildCatalogChoices (ctx context.Context ) []ModelChoice {
296+ // Check if modelsStore supports GetDatabase
297+ catalogStore , ok := r .modelsStore .(CatalogStore )
298+ if ! ok {
299+ slog .Debug ("Models store does not support GetDatabase, skipping catalog" )
300+ return nil
301+ }
302+
303+ db , err := catalogStore .GetDatabase (ctx )
304+ if err != nil {
305+ slog .Debug ("Failed to get models.dev database for catalog" , "error" , err )
306+ return nil
307+ }
308+
309+ // Build set of existing model refs to avoid duplicates
310+ existingRefs := make (map [string ]bool )
311+ for name , cfg := range r .modelSwitcherCfg .Models {
312+ existingRefs [name ] = true
313+ if cfg .Provider != "" && cfg .Model != "" {
314+ existingRefs [cfg .Provider + "/" + cfg .Model ] = true
315+ }
316+ }
317+
318+ // Check which providers the user has credentials for
319+ availableProviders := r .getAvailableProviders (ctx )
320+ if len (availableProviders ) == 0 {
321+ slog .Debug ("No provider credentials available, skipping catalog" )
322+ return nil
323+ }
324+
325+ var choices []ModelChoice
326+ for providerID , prov := range db .Providers {
327+ // Check if this provider is supported and user has credentials
328+ cagentProvider , supported := mapModelsDevProvider (providerID )
329+ if ! supported {
330+ continue
331+ }
332+ if ! availableProviders [cagentProvider ] {
333+ continue
334+ }
335+
336+ for modelID , model := range prov .Models {
337+ // Skip models that don't output text (not suitable for chat)
338+ if ! slices .Contains (model .Modalities .Output , "text" ) {
339+ continue
340+ }
341+ // Skip embedding models (not suitable for chat)
342+ if isEmbeddingModel (model .Family , model .Name ) {
343+ continue
344+ }
345+
346+ ref := cagentProvider + "/" + modelID
347+ if existingRefs [ref ] {
348+ continue
349+ }
350+ existingRefs [ref ] = true
351+
352+ choices = append (choices , ModelChoice {
353+ Name : model .Name ,
354+ Ref : ref ,
355+ Provider : cagentProvider ,
356+ Model : modelID ,
357+ IsCatalog : true ,
358+ })
359+ }
360+ }
361+
362+ slog .Debug ("Built catalog choices" , "count" , len (choices ), "available_providers" , len (availableProviders ))
363+ return choices
364+ }
365+
366+ // mapModelsDevProvider maps a models.dev provider ID to a cagent provider name.
367+ // Returns the cagent provider name and whether it's supported.
368+ // Uses provider.IsCatalogProvider to dynamically include all core providers
369+ // and aliases with defined base URLs.
370+ func mapModelsDevProvider (providerID string ) (string , bool ) {
371+ if provider .IsCatalogProvider (providerID ) {
372+ return providerID , true
373+ }
374+ return "" , false
375+ }
376+
377+ // isEmbeddingModel returns true if the model is an embedding model
378+ // based on its family or name fields from models.dev.
379+ func isEmbeddingModel (family , name string ) bool {
380+ familyLower := strings .ToLower (family )
381+ nameLower := strings .ToLower (name )
382+ return strings .Contains (familyLower , "embed" ) || strings .Contains (nameLower , "embed" )
383+ }
384+
385+ // getAvailableProviders returns a map of provider names that the user has credentials for.
386+ func (r * LocalRuntime ) getAvailableProviders (ctx context.Context ) map [string ]bool {
387+ available := make (map [string ]bool )
388+ env := r .modelSwitcherCfg .EnvProvider
389+
390+ // If using a models gateway, check for Docker token
391+ if r .modelSwitcherCfg .ModelsGateway != "" {
392+ if token , _ := env .Get (ctx , environment .DockerDesktopTokenEnv ); token != "" {
393+ // Gateway supports all providers
394+ available ["openai" ] = true
395+ available ["anthropic" ] = true
396+ available ["google" ] = true
397+ available ["mistral" ] = true
398+ available ["xai" ] = true
399+ }
400+ return available
401+ }
402+
403+ // Check credentials for each provider
404+ providerEnvVars := map [string ]string {
405+ "openai" : "OPENAI_API_KEY" ,
406+ "anthropic" : "ANTHROPIC_API_KEY" ,
407+ "google" : "GOOGLE_API_KEY" ,
408+ "mistral" : "MISTRAL_API_KEY" ,
409+ "xai" : "XAI_API_KEY" ,
410+ "nebius" : "NEBIUS_API_KEY" ,
411+ "requesty" : "REQUESTY_API_KEY" ,
412+ "azure" : "AZURE_API_KEY" ,
413+ }
414+
415+ for providerName , envVar := range providerEnvVars {
416+ if key , _ := env .Get (ctx , envVar ); key != "" {
417+ available [providerName ] = true
418+ }
419+ }
420+
421+ // DMR and ollama don't require credentials (local models)
422+ available ["dmr" ] = true
423+ available ["ollama" ] = true
424+
425+ // Amazon Bedrock uses AWS credentials which can come from many sources.
426+ // We do a quick heuristic check for common indicators without blocking:
427+ // - AWS_ACCESS_KEY_ID: explicit access key
428+ // - AWS_PROFILE / AWS_DEFAULT_PROFILE: named profile (credentials in ~/.aws/)
429+ // - AWS_WEB_IDENTITY_TOKEN_FILE: EKS/IRSA web identity
430+ // - AWS_CONTAINER_CREDENTIALS_RELATIVE_URI: ECS task role
431+ // - AWS_ROLE_ARN: assumed role
432+ // Note: This won't catch all cases (e.g., EC2 instance profiles, SSO) but
433+ // those require network calls which would block the UI.
434+ awsCredentialIndicators := []string {
435+ "AWS_ACCESS_KEY_ID" ,
436+ "AWS_PROFILE" ,
437+ "AWS_DEFAULT_PROFILE" ,
438+ "AWS_WEB_IDENTITY_TOKEN_FILE" ,
439+ "AWS_CONTAINER_CREDENTIALS_RELATIVE_URI" ,
440+ "AWS_ROLE_ARN" ,
441+ }
442+ for _ , indicator := range awsCredentialIndicators {
443+ if val , _ := env .Get (ctx , indicator ); val != "" {
444+ available ["amazon-bedrock" ] = true
445+ break
446+ }
447+ }
448+
449+ return available
450+ }
451+
279452// createProviderFromConfig creates a provider from a ModelConfig using the runtime's configuration.
280453func (r * LocalRuntime ) createProviderFromConfig (ctx context.Context , cfg * latest.ModelConfig ) (provider.Provider , error ) {
281454 opts := []options.Opt {
0 commit comments