package app
import (
"context"
"fmt"
"maps"
"slices"
"strings"
"albedo/cli/internal/config"
"albedo/cli/internal/daemon"
)
// providerModels is one configured provider and the model ids it offers.
type providerModels struct {
provider string
models []string
}
// modelChoice is a model id and the provider that serves it.
type modelChoice struct {
provider string
model string
}
// configuredModels lists every provider's models, the active provider first.
// A provider lists its default model first, even when its catalog cannot be
// read, as the daemon does when it resolves an agent's model.
func configuredModels(ctx context.Context, conn *daemon.Connection, profiles config.Profiles) []providerModels {
names := slices.Sorted(maps.Keys(profiles.Providers))
if i := slices.Index(names, profiles.Active); i > 0 {
names = append([]string{profiles.Active}, slices.Delete(names, i, i+1)...)
}
listed := make([]providerModels, 0, len(names))
for _, name := range names {
settings := profiles.Providers[name]
models := []string{settings.Model}
catalog, _ := daemon.ListProfileModels(ctx, conn, settings)
for _, model := range catalog {
if !slices.Contains(models, model.ID) {
models = append(models, model.ID)
}
}
listed = append(listed, providerModels{name, slices.DeleteFunc(models, func(id string) bool { return id == "" })})
}
return listed
}
// chooseModel resolves a model the way the daemon resolves an agent's model:
// provider/model picks the provider, and a bare id comes from the active
// provider, else from the one provider that offers it. Only exact ids match.
func chooseModel(listed []providerModels, active, requested string) (modelChoice, error) {
for _, p := range listed {
if model, ok := strings.CutPrefix(requested, p.provider+"/"); ok {
if !slices.Contains(p.models, model) {
return modelChoice{}, fmt.Errorf("provider %s does not offer model %s; run albedo models to see the models you can use", p.provider, model)
}
return modelChoice{p.provider, model}, nil
}
}
var offering []string
for _, p := range listed {
if slices.Contains(p.models, requested) {
offering = append(offering, p.provider)
}
}
switch {
case len(offering) == 0:
return modelChoice{}, fmt.Errorf("no configured provider offers model %s; run albedo models to see the models you can use", requested)
case slices.Contains(offering, active):
return modelChoice{active, requested}, nil
case len(offering) == 1:
return modelChoice{offering[0], requested}, nil
}
return modelChoice{}, fmt.Errorf("model %s is offered by %s; choose one with provider/model", requested, strings.Join(offering, ", "))
}
// switchModel moves an idle session to the chosen model without making it the
// default for new sessions.
func switchModel(ctx context.Context, conn *daemon.Connection, sessionID string, choice modelChoice) error {
_, err := daemon.SelectModel(ctx, conn, sessionID, daemon.ModelSelectionRequest{Model: choice.model, Provider: choice.provider})
return err
}
func (s *Service) Models(ctx context.Context) ([]string, error) {
conn, err := s.Connect(ctx)
if err != nil {
return nil, err
}
profiles, err := daemon.ProviderProfiles(ctx, conn)
if err != nil {
return nil, err
}
var models []string
for _, p := range configuredModels(ctx, conn, profiles) {
for _, model := range p.models {
models = append(models, p.provider+"/"+model)
}
}
return models, nil
}