mirror of
https://github.com/docker/docker-agent.git
synced 2026-11-04 10:02:15 +00:00
169 lines
4.9 KiB
Go
169 lines
4.9 KiB
Go
package config
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"maps"
|
|
"os"
|
|
"slices"
|
|
"strings"
|
|
|
|
"github.com/docker/docker-agent/pkg/config/latest"
|
|
"github.com/docker/docker-agent/pkg/environment"
|
|
"github.com/docker/docker-agent/pkg/gateway"
|
|
"github.com/docker/docker-agent/pkg/model/provider"
|
|
)
|
|
|
|
// gatherMissingEnvVars finds out which environment variables are required by the models and tools.
|
|
// It returns the missing variables and any non-fatal error encountered during tool discovery.
|
|
func gatherMissingEnvVars(ctx context.Context, cfg *latest.Config, modelsGateway string, env environment.Provider) (missing []string, toolErr error) {
|
|
requiredEnv := map[string]bool{}
|
|
|
|
// Models
|
|
if modelsGateway == "" {
|
|
names := GatherEnvVarsForModels(cfg)
|
|
for _, e := range names {
|
|
requiredEnv[e] = true
|
|
}
|
|
}
|
|
|
|
// Tools
|
|
names, err := GatherEnvVarsForTools(ctx, cfg)
|
|
if err != nil {
|
|
// Store tool preflight error but continue checking models
|
|
toolErr = err
|
|
} else {
|
|
for _, e := range names {
|
|
requiredEnv[e] = true
|
|
}
|
|
}
|
|
|
|
for _, e := range sortedKeys(requiredEnv) {
|
|
if v, _ := env.Get(ctx, e); v == "" {
|
|
missing = append(missing, e)
|
|
}
|
|
}
|
|
|
|
return missing, toolErr
|
|
}
|
|
|
|
func GatherEnvVarsForModels(cfg *latest.Config) []string {
|
|
requiredEnv := map[string]bool{}
|
|
|
|
// Inspect only the models that are actually used by agents
|
|
for _, agent := range cfg.Agents {
|
|
modelNames := strings.SplitSeq(agent.Model, ",")
|
|
for modelName := range modelNames {
|
|
modelName = strings.TrimSpace(modelName)
|
|
gatherEnvVarsForModel(cfg, modelName, requiredEnv)
|
|
}
|
|
}
|
|
|
|
return sortedKeys(requiredEnv)
|
|
}
|
|
|
|
// gatherEnvVarsForModel collects required environment variables for a single model,
|
|
// including any models referenced in its routing rules.
|
|
func gatherEnvVarsForModel(cfg *latest.Config, modelName string, requiredEnv map[string]bool) {
|
|
model := cfg.Models[modelName]
|
|
|
|
// Add env vars for the model itself
|
|
addEnvVarsForModelConfig(&model, cfg.Providers, requiredEnv)
|
|
|
|
// If the model has routing rules, also check all referenced models
|
|
for _, rule := range model.Routing {
|
|
ruleModelName := rule.Model
|
|
if ruleModel, exists := cfg.Models[ruleModelName]; exists {
|
|
// Model reference - add its env vars
|
|
addEnvVarsForModelConfig(&ruleModel, cfg.Providers, requiredEnv)
|
|
} else if providerName, _, ok := strings.Cut(ruleModelName, "/"); ok {
|
|
// Inline spec (e.g., "openai/gpt-4o") - infer env vars from provider
|
|
inlineModel := latest.ModelConfig{Provider: providerName}
|
|
addEnvVarsForModelConfig(&inlineModel, cfg.Providers, requiredEnv)
|
|
}
|
|
}
|
|
}
|
|
|
|
// addEnvVarsForModelConfig adds required environment variables for a model config.
|
|
// It checks custom providers first, then built-in aliases, then hardcoded fallbacks.
|
|
func addEnvVarsForModelConfig(model *latest.ModelConfig, customProviders map[string]latest.ProviderConfig, requiredEnv map[string]bool) {
|
|
if model.TokenKey != "" {
|
|
requiredEnv[model.TokenKey] = true
|
|
} else if customProviders != nil {
|
|
// Check custom providers from config
|
|
if provCfg, exists := customProviders[model.Provider]; exists {
|
|
if provCfg.TokenKey != "" {
|
|
requiredEnv[provCfg.TokenKey] = true
|
|
}
|
|
}
|
|
} else if alias, exists := provider.Aliases[model.Provider]; exists {
|
|
// Check built-in aliases
|
|
if alias.TokenEnvVar != "" {
|
|
requiredEnv[alias.TokenEnvVar] = true
|
|
}
|
|
} else {
|
|
// Fallback to hardcoded mappings for core providers
|
|
switch model.Provider {
|
|
case "openai":
|
|
requiredEnv["OPENAI_API_KEY"] = true
|
|
case "anthropic":
|
|
requiredEnv["ANTHROPIC_API_KEY"] = true
|
|
case "google":
|
|
if model.ProviderOpts["project"] == nil && model.ProviderOpts["location"] == nil {
|
|
if os.Getenv("GOOGLE_GENAI_USE_VERTEXAI") != "" {
|
|
requiredEnv["GOOGLE_CLOUD_PROJECT"] = true
|
|
requiredEnv["GOOGLE_CLOUD_LOCATION"] = true
|
|
} else if _, exist := os.LookupEnv("GEMINI_API_KEY"); !exist {
|
|
requiredEnv["GOOGLE_API_KEY"] = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func GatherEnvVarsForTools(ctx context.Context, cfg *latest.Config) ([]string, error) {
|
|
requiredEnv := map[string]bool{}
|
|
var errs []error
|
|
|
|
for i := range cfg.Agents {
|
|
agent := cfg.Agents[i]
|
|
|
|
for j := range agent.Toolsets {
|
|
toolSet := agent.Toolsets[j]
|
|
ref := toolSet.Ref
|
|
if toolSet.Type != "mcp" || ref == "" {
|
|
continue
|
|
}
|
|
|
|
mcpServerName := gateway.ParseServerRef(ref)
|
|
secrets, err := gateway.RequiredEnvVars(ctx, mcpServerName)
|
|
if err != nil {
|
|
errs = append(errs, fmt.Errorf("reading which secrets the MCP server needs for %s: %w", ref, err))
|
|
continue
|
|
}
|
|
|
|
for _, secret := range secrets {
|
|
value, ok := toolSet.Env[secret.Env]
|
|
if !ok {
|
|
requiredEnv[secret.Env] = true
|
|
} else {
|
|
os.Expand(value, func(name string) string {
|
|
requiredEnv[name] = true
|
|
return ""
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(errs) > 0 {
|
|
return sortedKeys(requiredEnv), fmt.Errorf("tool env preflight: %w", errors.Join(errs...))
|
|
}
|
|
return sortedKeys(requiredEnv), nil
|
|
}
|
|
|
|
func sortedKeys(requiredEnv map[string]bool) []string {
|
|
return slices.Sorted(maps.Keys(requiredEnv))
|
|
}
|