mirror of
https://github.com/sipeed/picoclaw.git
synced 2026-06-12 18:08:54 +00:00
a73d8e1a16
- Add ModelConfig struct with protocol prefix support (openai/, anthropic/, etc.) - Implement GetModelConfig with round-robin load balancing - Add CreateProviderFromConfig factory for protocol-based routing - Add ModelRegistry for thread-safe endpoint selection - Maintain full backward compatibility with legacy providers config - Update README.md and README.zh.md with model_list documentation - Add migration guide at docs/migration/model-list-migration.md Supported protocols: openai, anthropic, antigravity, claude-cli, codex-cli, github-copilot, openrouter, groq, deepseek, cerebras, qwen, zhipu, gemini Closes #283 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
802 lines
25 KiB
Go
802 lines
25 KiB
Go
package config
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"sync"
|
|
"sync/atomic"
|
|
|
|
"github.com/caarlos0/env/v11"
|
|
)
|
|
|
|
// FlexibleStringSlice is a []string that also accepts JSON numbers,
|
|
// so allow_from can contain both "123" and 123.
|
|
type FlexibleStringSlice []string
|
|
|
|
func (f *FlexibleStringSlice) UnmarshalJSON(data []byte) error {
|
|
// Try []string first
|
|
var ss []string
|
|
if err := json.Unmarshal(data, &ss); err == nil {
|
|
*f = ss
|
|
return nil
|
|
}
|
|
|
|
// Try []interface{} to handle mixed types
|
|
var raw []interface{}
|
|
if err := json.Unmarshal(data, &raw); err != nil {
|
|
return err
|
|
}
|
|
|
|
result := make([]string, 0, len(raw))
|
|
for _, v := range raw {
|
|
switch val := v.(type) {
|
|
case string:
|
|
result = append(result, val)
|
|
case float64:
|
|
result = append(result, fmt.Sprintf("%.0f", val))
|
|
default:
|
|
result = append(result, fmt.Sprintf("%v", val))
|
|
}
|
|
}
|
|
*f = result
|
|
return nil
|
|
}
|
|
|
|
type Config struct {
|
|
Agents AgentsConfig `json:"agents"`
|
|
Channels ChannelsConfig `json:"channels"`
|
|
Providers ProvidersConfig `json:"providers"`
|
|
ModelList []ModelConfig `json:"model_list"` // New model-centric provider configuration
|
|
Gateway GatewayConfig `json:"gateway"`
|
|
Tools ToolsConfig `json:"tools"`
|
|
Heartbeat HeartbeatConfig `json:"heartbeat"`
|
|
Devices DevicesConfig `json:"devices"`
|
|
mu sync.RWMutex
|
|
rrCounters map[string]*atomic.Uint64 // Round-robin counters for load balancing
|
|
}
|
|
|
|
type AgentsConfig struct {
|
|
Defaults AgentDefaults `json:"defaults"`
|
|
}
|
|
|
|
type AgentDefaults struct {
|
|
Workspace string `json:"workspace" env:"PICOCLAW_AGENTS_DEFAULTS_WORKSPACE"`
|
|
RestrictToWorkspace bool `json:"restrict_to_workspace" env:"PICOCLAW_AGENTS_DEFAULTS_RESTRICT_TO_WORKSPACE"`
|
|
Provider string `json:"provider" env:"PICOCLAW_AGENTS_DEFAULTS_PROVIDER"`
|
|
Model string `json:"model" env:"PICOCLAW_AGENTS_DEFAULTS_MODEL"`
|
|
MaxTokens int `json:"max_tokens" env:"PICOCLAW_AGENTS_DEFAULTS_MAX_TOKENS"`
|
|
Temperature float64 `json:"temperature" env:"PICOCLAW_AGENTS_DEFAULTS_TEMPERATURE"`
|
|
MaxToolIterations int `json:"max_tool_iterations" env:"PICOCLAW_AGENTS_DEFAULTS_MAX_TOOL_ITERATIONS"`
|
|
}
|
|
|
|
type ChannelsConfig struct {
|
|
WhatsApp WhatsAppConfig `json:"whatsapp"`
|
|
Telegram TelegramConfig `json:"telegram"`
|
|
Feishu FeishuConfig `json:"feishu"`
|
|
Discord DiscordConfig `json:"discord"`
|
|
MaixCam MaixCamConfig `json:"maixcam"`
|
|
QQ QQConfig `json:"qq"`
|
|
DingTalk DingTalkConfig `json:"dingtalk"`
|
|
Slack SlackConfig `json:"slack"`
|
|
LINE LINEConfig `json:"line"`
|
|
OneBot OneBotConfig `json:"onebot"`
|
|
}
|
|
|
|
type WhatsAppConfig struct {
|
|
Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_WHATSAPP_ENABLED"`
|
|
BridgeURL string `json:"bridge_url" env:"PICOCLAW_CHANNELS_WHATSAPP_BRIDGE_URL"`
|
|
AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_WHATSAPP_ALLOW_FROM"`
|
|
}
|
|
|
|
type TelegramConfig struct {
|
|
Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_TELEGRAM_ENABLED"`
|
|
Token string `json:"token" env:"PICOCLAW_CHANNELS_TELEGRAM_TOKEN"`
|
|
Proxy string `json:"proxy" env:"PICOCLAW_CHANNELS_TELEGRAM_PROXY"`
|
|
AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_TELEGRAM_ALLOW_FROM"`
|
|
}
|
|
|
|
type FeishuConfig struct {
|
|
Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_FEISHU_ENABLED"`
|
|
AppID string `json:"app_id" env:"PICOCLAW_CHANNELS_FEISHU_APP_ID"`
|
|
AppSecret string `json:"app_secret" env:"PICOCLAW_CHANNELS_FEISHU_APP_SECRET"`
|
|
EncryptKey string `json:"encrypt_key" env:"PICOCLAW_CHANNELS_FEISHU_ENCRYPT_KEY"`
|
|
VerificationToken string `json:"verification_token" env:"PICOCLAW_CHANNELS_FEISHU_VERIFICATION_TOKEN"`
|
|
AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_FEISHU_ALLOW_FROM"`
|
|
}
|
|
|
|
type DiscordConfig struct {
|
|
Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_DISCORD_ENABLED"`
|
|
Token string `json:"token" env:"PICOCLAW_CHANNELS_DISCORD_TOKEN"`
|
|
AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_DISCORD_ALLOW_FROM"`
|
|
}
|
|
|
|
type MaixCamConfig struct {
|
|
Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_MAIXCAM_ENABLED"`
|
|
Host string `json:"host" env:"PICOCLAW_CHANNELS_MAIXCAM_HOST"`
|
|
Port int `json:"port" env:"PICOCLAW_CHANNELS_MAIXCAM_PORT"`
|
|
AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_MAIXCAM_ALLOW_FROM"`
|
|
}
|
|
|
|
type QQConfig struct {
|
|
Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_QQ_ENABLED"`
|
|
AppID string `json:"app_id" env:"PICOCLAW_CHANNELS_QQ_APP_ID"`
|
|
AppSecret string `json:"app_secret" env:"PICOCLAW_CHANNELS_QQ_APP_SECRET"`
|
|
AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_QQ_ALLOW_FROM"`
|
|
}
|
|
|
|
type DingTalkConfig struct {
|
|
Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_DINGTALK_ENABLED"`
|
|
ClientID string `json:"client_id" env:"PICOCLAW_CHANNELS_DINGTALK_CLIENT_ID"`
|
|
ClientSecret string `json:"client_secret" env:"PICOCLAW_CHANNELS_DINGTALK_CLIENT_SECRET"`
|
|
AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_DINGTALK_ALLOW_FROM"`
|
|
}
|
|
|
|
type SlackConfig struct {
|
|
Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_SLACK_ENABLED"`
|
|
BotToken string `json:"bot_token" env:"PICOCLAW_CHANNELS_SLACK_BOT_TOKEN"`
|
|
AppToken string `json:"app_token" env:"PICOCLAW_CHANNELS_SLACK_APP_TOKEN"`
|
|
AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_SLACK_ALLOW_FROM"`
|
|
}
|
|
|
|
type LINEConfig struct {
|
|
Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_LINE_ENABLED"`
|
|
ChannelSecret string `json:"channel_secret" env:"PICOCLAW_CHANNELS_LINE_CHANNEL_SECRET"`
|
|
ChannelAccessToken string `json:"channel_access_token" env:"PICOCLAW_CHANNELS_LINE_CHANNEL_ACCESS_TOKEN"`
|
|
WebhookHost string `json:"webhook_host" env:"PICOCLAW_CHANNELS_LINE_WEBHOOK_HOST"`
|
|
WebhookPort int `json:"webhook_port" env:"PICOCLAW_CHANNELS_LINE_WEBHOOK_PORT"`
|
|
WebhookPath string `json:"webhook_path" env:"PICOCLAW_CHANNELS_LINE_WEBHOOK_PATH"`
|
|
AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_LINE_ALLOW_FROM"`
|
|
}
|
|
|
|
type OneBotConfig struct {
|
|
Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_ONEBOT_ENABLED"`
|
|
WSUrl string `json:"ws_url" env:"PICOCLAW_CHANNELS_ONEBOT_WS_URL"`
|
|
AccessToken string `json:"access_token" env:"PICOCLAW_CHANNELS_ONEBOT_ACCESS_TOKEN"`
|
|
ReconnectInterval int `json:"reconnect_interval" env:"PICOCLAW_CHANNELS_ONEBOT_RECONNECT_INTERVAL"`
|
|
GroupTriggerPrefix []string `json:"group_trigger_prefix" env:"PICOCLAW_CHANNELS_ONEBOT_GROUP_TRIGGER_PREFIX"`
|
|
AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_ONEBOT_ALLOW_FROM"`
|
|
}
|
|
|
|
type HeartbeatConfig struct {
|
|
Enabled bool `json:"enabled" env:"PICOCLAW_HEARTBEAT_ENABLED"`
|
|
Interval int `json:"interval" env:"PICOCLAW_HEARTBEAT_INTERVAL"` // minutes, min 5
|
|
}
|
|
|
|
type DevicesConfig struct {
|
|
Enabled bool `json:"enabled" env:"PICOCLAW_DEVICES_ENABLED"`
|
|
MonitorUSB bool `json:"monitor_usb" env:"PICOCLAW_DEVICES_MONITOR_USB"`
|
|
}
|
|
|
|
type ProvidersConfig struct {
|
|
Anthropic ProviderConfig `json:"anthropic"`
|
|
OpenAI ProviderConfig `json:"openai"`
|
|
OpenRouter ProviderConfig `json:"openrouter"`
|
|
Groq ProviderConfig `json:"groq"`
|
|
Zhipu ProviderConfig `json:"zhipu"`
|
|
VLLM ProviderConfig `json:"vllm"`
|
|
Gemini ProviderConfig `json:"gemini"`
|
|
Nvidia ProviderConfig `json:"nvidia"`
|
|
Ollama ProviderConfig `json:"ollama"`
|
|
Moonshot ProviderConfig `json:"moonshot"`
|
|
ShengSuanYun ProviderConfig `json:"shengsuanyun"`
|
|
DeepSeek ProviderConfig `json:"deepseek"`
|
|
Cerebras ProviderConfig `json:"cerebras"`
|
|
VolcEngine ProviderConfig `json:"volcengine"`
|
|
GitHubCopilot ProviderConfig `json:"github_copilot"`
|
|
Antigravity ProviderConfig `json:"antigravity"`
|
|
Qwen ProviderConfig `json:"qwen"`
|
|
}
|
|
|
|
type ProviderConfig struct {
|
|
APIKey string `json:"api_key" env:"PICOCLAW_PROVIDERS_{{.Name}}_API_KEY"`
|
|
APIBase string `json:"api_base" env:"PICOCLAW_PROVIDERS_{{.Name}}_API_BASE"`
|
|
Proxy string `json:"proxy,omitempty" env:"PICOCLAW_PROVIDERS_{{.Name}}_PROXY"`
|
|
AuthMethod string `json:"auth_method,omitempty" env:"PICOCLAW_PROVIDERS_{{.Name}}_AUTH_METHOD"`
|
|
ConnectMode string `json:"connect_mode,omitempty" env:"PICOCLAW_PROVIDERS_{{.Name}}_CONNECT_MODE"` //only for Github Copilot, `stdio` or `grpc`
|
|
}
|
|
|
|
// ModelConfig represents a model-centric provider configuration.
|
|
// It allows adding new providers (especially OpenAI-compatible ones) via configuration only.
|
|
// The model field uses protocol prefix format: [protocol/]model-identifier
|
|
// Supported protocols: openai, anthropic, antigravity, claude-cli, codex-cli, github-copilot
|
|
// Default protocol is "openai" if no prefix is specified.
|
|
type ModelConfig struct {
|
|
// Required fields
|
|
ModelName string `json:"model_name"` // User-facing alias for the model
|
|
Model string `json:"model"` // Protocol/model-identifier (e.g., "openai/gpt-4o", "anthropic/claude-3")
|
|
|
|
// HTTP-based providers
|
|
APIBase string `json:"api_base,omitempty"` // API endpoint URL
|
|
APIKey string `json:"api_key,omitempty"` // API authentication key
|
|
Proxy string `json:"proxy,omitempty"` // HTTP proxy URL
|
|
|
|
// Special providers (CLI-based, OAuth, etc.)
|
|
AuthMethod string `json:"auth_method,omitempty"` // Authentication method: oauth, token
|
|
ConnectMode string `json:"connect_mode,omitempty"` // Connection mode: stdio, grpc
|
|
|
|
// Optional optimizations
|
|
RPM int `json:"rpm,omitempty"` // Requests per minute limit
|
|
MaxTokensField string `json:"max_tokens_field,omitempty"` // Field name for max tokens (e.g., "max_completion_tokens")
|
|
}
|
|
|
|
// Validate checks if the ModelConfig has all required fields.
|
|
func (c *ModelConfig) Validate() error {
|
|
if c.ModelName == "" {
|
|
return fmt.Errorf("model_name is required")
|
|
}
|
|
if c.Model == "" {
|
|
return fmt.Errorf("model is required")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ParseProtocol extracts the protocol prefix and model identifier from the Model field.
|
|
// If no prefix is specified, it defaults to "openai".
|
|
// Examples:
|
|
// - "openai/gpt-4o" -> ("openai", "gpt-4o")
|
|
// - "anthropic/claude-3" -> ("anthropic", "claude-3")
|
|
// - "gpt-4o" -> ("openai", "gpt-4o") // default protocol
|
|
func (c *ModelConfig) ParseProtocol() (protocol, modelID string) {
|
|
model := c.Model
|
|
for i := 0; i < len(model); i++ {
|
|
if model[i] == '/' {
|
|
return model[:i], model[i+1:]
|
|
}
|
|
}
|
|
// No prefix found, default to openai
|
|
return "openai", model
|
|
}
|
|
|
|
type GatewayConfig struct {
|
|
Host string `json:"host" env:"PICOCLAW_GATEWAY_HOST"`
|
|
Port int `json:"port" env:"PICOCLAW_GATEWAY_PORT"`
|
|
}
|
|
|
|
type BraveConfig struct {
|
|
Enabled bool `json:"enabled" env:"PICOCLAW_TOOLS_WEB_BRAVE_ENABLED"`
|
|
APIKey string `json:"api_key" env:"PICOCLAW_TOOLS_WEB_BRAVE_API_KEY"`
|
|
MaxResults int `json:"max_results" env:"PICOCLAW_TOOLS_WEB_BRAVE_MAX_RESULTS"`
|
|
}
|
|
|
|
type DuckDuckGoConfig struct {
|
|
Enabled bool `json:"enabled" env:"PICOCLAW_TOOLS_WEB_DUCKDUCKGO_ENABLED"`
|
|
MaxResults int `json:"max_results" env:"PICOCLAW_TOOLS_WEB_DUCKDUCKGO_MAX_RESULTS"`
|
|
}
|
|
|
|
type PerplexityConfig struct {
|
|
Enabled bool `json:"enabled" env:"PICOCLAW_TOOLS_WEB_PERPLEXITY_ENABLED"`
|
|
APIKey string `json:"api_key" env:"PICOCLAW_TOOLS_WEB_PERPLEXITY_API_KEY"`
|
|
MaxResults int `json:"max_results" env:"PICOCLAW_TOOLS_WEB_PERPLEXITY_MAX_RESULTS"`
|
|
}
|
|
|
|
type WebToolsConfig struct {
|
|
Brave BraveConfig `json:"brave"`
|
|
DuckDuckGo DuckDuckGoConfig `json:"duckduckgo"`
|
|
Perplexity PerplexityConfig `json:"perplexity"`
|
|
}
|
|
|
|
type CronToolsConfig struct {
|
|
ExecTimeoutMinutes int `json:"exec_timeout_minutes" env:"PICOCLAW_TOOLS_CRON_EXEC_TIMEOUT_MINUTES"` // 0 means no timeout
|
|
}
|
|
|
|
type ToolsConfig struct {
|
|
Web WebToolsConfig `json:"web"`
|
|
Cron CronToolsConfig `json:"cron"`
|
|
}
|
|
|
|
func DefaultConfig() *Config {
|
|
return &Config{
|
|
Agents: AgentsConfig{
|
|
Defaults: AgentDefaults{
|
|
Workspace: "~/.picoclaw/workspace",
|
|
RestrictToWorkspace: true,
|
|
Provider: "",
|
|
Model: "glm-4.7",
|
|
MaxTokens: 8192,
|
|
Temperature: 0.7,
|
|
MaxToolIterations: 20,
|
|
},
|
|
},
|
|
Channels: ChannelsConfig{
|
|
WhatsApp: WhatsAppConfig{
|
|
Enabled: false,
|
|
BridgeURL: "ws://localhost:3001",
|
|
AllowFrom: FlexibleStringSlice{},
|
|
},
|
|
Telegram: TelegramConfig{
|
|
Enabled: false,
|
|
Token: "",
|
|
AllowFrom: FlexibleStringSlice{},
|
|
},
|
|
Feishu: FeishuConfig{
|
|
Enabled: false,
|
|
AppID: "",
|
|
AppSecret: "",
|
|
EncryptKey: "",
|
|
VerificationToken: "",
|
|
AllowFrom: FlexibleStringSlice{},
|
|
},
|
|
Discord: DiscordConfig{
|
|
Enabled: false,
|
|
Token: "",
|
|
AllowFrom: FlexibleStringSlice{},
|
|
},
|
|
MaixCam: MaixCamConfig{
|
|
Enabled: false,
|
|
Host: "0.0.0.0",
|
|
Port: 18790,
|
|
AllowFrom: FlexibleStringSlice{},
|
|
},
|
|
QQ: QQConfig{
|
|
Enabled: false,
|
|
AppID: "",
|
|
AppSecret: "",
|
|
AllowFrom: FlexibleStringSlice{},
|
|
},
|
|
DingTalk: DingTalkConfig{
|
|
Enabled: false,
|
|
ClientID: "",
|
|
ClientSecret: "",
|
|
AllowFrom: FlexibleStringSlice{},
|
|
},
|
|
Slack: SlackConfig{
|
|
Enabled: false,
|
|
BotToken: "",
|
|
AppToken: "",
|
|
AllowFrom: FlexibleStringSlice{},
|
|
},
|
|
LINE: LINEConfig{
|
|
Enabled: false,
|
|
ChannelSecret: "",
|
|
ChannelAccessToken: "",
|
|
WebhookHost: "0.0.0.0",
|
|
WebhookPort: 18791,
|
|
WebhookPath: "/webhook/line",
|
|
AllowFrom: FlexibleStringSlice{},
|
|
},
|
|
OneBot: OneBotConfig{
|
|
Enabled: false,
|
|
WSUrl: "ws://127.0.0.1:3001",
|
|
AccessToken: "",
|
|
ReconnectInterval: 5,
|
|
GroupTriggerPrefix: []string{},
|
|
AllowFrom: FlexibleStringSlice{},
|
|
},
|
|
},
|
|
Providers: ProvidersConfig{
|
|
Anthropic: ProviderConfig{},
|
|
OpenAI: ProviderConfig{},
|
|
OpenRouter: ProviderConfig{},
|
|
Groq: ProviderConfig{},
|
|
Zhipu: ProviderConfig{},
|
|
VLLM: ProviderConfig{},
|
|
Gemini: ProviderConfig{},
|
|
Nvidia: ProviderConfig{},
|
|
Moonshot: ProviderConfig{},
|
|
ShengSuanYun: ProviderConfig{},
|
|
Cerebras: ProviderConfig{},
|
|
VolcEngine: ProviderConfig{},
|
|
},
|
|
Gateway: GatewayConfig{
|
|
Host: "0.0.0.0",
|
|
Port: 18790,
|
|
},
|
|
Tools: ToolsConfig{
|
|
Web: WebToolsConfig{
|
|
Brave: BraveConfig{
|
|
Enabled: false,
|
|
APIKey: "",
|
|
MaxResults: 5,
|
|
},
|
|
DuckDuckGo: DuckDuckGoConfig{
|
|
Enabled: true,
|
|
MaxResults: 5,
|
|
},
|
|
Perplexity: PerplexityConfig{
|
|
Enabled: false,
|
|
APIKey: "",
|
|
MaxResults: 5,
|
|
},
|
|
},
|
|
Cron: CronToolsConfig{
|
|
ExecTimeoutMinutes: 5, // default 5 minutes for LLM operations
|
|
},
|
|
},
|
|
Heartbeat: HeartbeatConfig{
|
|
Enabled: true,
|
|
Interval: 30, // default 30 minutes
|
|
},
|
|
Devices: DevicesConfig{
|
|
Enabled: false,
|
|
MonitorUSB: true,
|
|
},
|
|
}
|
|
}
|
|
|
|
func LoadConfig(path string) (*Config, error) {
|
|
cfg := DefaultConfig()
|
|
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return cfg, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
if err := json.Unmarshal(data, cfg); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := env.Parse(cfg); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return cfg, nil
|
|
}
|
|
|
|
func SaveConfig(path string, cfg *Config) error {
|
|
cfg.mu.RLock()
|
|
defer cfg.mu.RUnlock()
|
|
|
|
data, err := json.MarshalIndent(cfg, "", " ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
dir := filepath.Dir(path)
|
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
|
return err
|
|
}
|
|
|
|
return os.WriteFile(path, data, 0600)
|
|
}
|
|
|
|
func (c *Config) WorkspacePath() string {
|
|
c.mu.RLock()
|
|
defer c.mu.RUnlock()
|
|
return expandHome(c.Agents.Defaults.Workspace)
|
|
}
|
|
|
|
func (c *Config) GetAPIKey() string {
|
|
c.mu.RLock()
|
|
defer c.mu.RUnlock()
|
|
if c.Providers.OpenRouter.APIKey != "" {
|
|
return c.Providers.OpenRouter.APIKey
|
|
}
|
|
if c.Providers.Anthropic.APIKey != "" {
|
|
return c.Providers.Anthropic.APIKey
|
|
}
|
|
if c.Providers.OpenAI.APIKey != "" {
|
|
return c.Providers.OpenAI.APIKey
|
|
}
|
|
if c.Providers.Gemini.APIKey != "" {
|
|
return c.Providers.Gemini.APIKey
|
|
}
|
|
if c.Providers.Zhipu.APIKey != "" {
|
|
return c.Providers.Zhipu.APIKey
|
|
}
|
|
if c.Providers.Groq.APIKey != "" {
|
|
return c.Providers.Groq.APIKey
|
|
}
|
|
if c.Providers.VLLM.APIKey != "" {
|
|
return c.Providers.VLLM.APIKey
|
|
}
|
|
if c.Providers.ShengSuanYun.APIKey != "" {
|
|
return c.Providers.ShengSuanYun.APIKey
|
|
}
|
|
if c.Providers.Cerebras.APIKey != "" {
|
|
return c.Providers.Cerebras.APIKey
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (c *Config) GetAPIBase() string {
|
|
c.mu.RLock()
|
|
defer c.mu.RUnlock()
|
|
if c.Providers.OpenRouter.APIKey != "" {
|
|
if c.Providers.OpenRouter.APIBase != "" {
|
|
return c.Providers.OpenRouter.APIBase
|
|
}
|
|
return "https://openrouter.ai/api/v1"
|
|
}
|
|
if c.Providers.Zhipu.APIKey != "" {
|
|
return c.Providers.Zhipu.APIBase
|
|
}
|
|
if c.Providers.VLLM.APIKey != "" && c.Providers.VLLM.APIBase != "" {
|
|
return c.Providers.VLLM.APIBase
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func expandHome(path string) string {
|
|
if path == "" {
|
|
return path
|
|
}
|
|
if path[0] == '~' {
|
|
home, _ := os.UserHomeDir()
|
|
if len(path) > 1 && path[1] == '/' {
|
|
return home + path[1:]
|
|
}
|
|
return home
|
|
}
|
|
return path
|
|
}
|
|
|
|
// GetModelConfig returns the ModelConfig for the given model name.
|
|
// If multiple configs exist with the same model_name, it uses round-robin
|
|
// selection for load balancing. Returns an error if the model is not found.
|
|
func (c *Config) GetModelConfig(modelName string) (*ModelConfig, error) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
|
|
// Find all configs with matching model_name
|
|
var matches []ModelConfig
|
|
for i := range c.ModelList {
|
|
if c.ModelList[i].ModelName == modelName {
|
|
matches = append(matches, c.ModelList[i])
|
|
}
|
|
}
|
|
|
|
if len(matches) == 0 {
|
|
return nil, fmt.Errorf("model %q not found in model_list or providers", modelName)
|
|
}
|
|
|
|
// Single config - return directly
|
|
if len(matches) == 1 {
|
|
return &matches[0], nil
|
|
}
|
|
|
|
// Multiple configs - use round-robin for load balancing
|
|
if c.rrCounters == nil {
|
|
c.rrCounters = make(map[string]*atomic.Uint64)
|
|
}
|
|
|
|
counter, ok := c.rrCounters[modelName]
|
|
if !ok {
|
|
counter = &atomic.Uint64{}
|
|
c.rrCounters[modelName] = counter
|
|
}
|
|
|
|
idx := counter.Add(1) % uint64(len(matches))
|
|
return &matches[idx], nil
|
|
}
|
|
|
|
// HasProvidersConfig checks if any provider in the old providers config has configuration.
|
|
func (c *Config) HasProvidersConfig() bool {
|
|
c.mu.RLock()
|
|
defer c.mu.RUnlock()
|
|
|
|
v := c.Providers
|
|
return v.Anthropic.APIKey != "" || v.Anthropic.APIBase != "" ||
|
|
v.OpenAI.APIKey != "" || v.OpenAI.APIBase != "" ||
|
|
v.OpenRouter.APIKey != "" || v.OpenRouter.APIBase != "" ||
|
|
v.Groq.APIKey != "" || v.Groq.APIBase != "" ||
|
|
v.Zhipu.APIKey != "" || v.Zhipu.APIBase != "" ||
|
|
v.VLLM.APIKey != "" || v.VLLM.APIBase != "" ||
|
|
v.Gemini.APIKey != "" || v.Gemini.APIBase != "" ||
|
|
v.Nvidia.APIKey != "" || v.Nvidia.APIBase != "" ||
|
|
v.Ollama.APIKey != "" || v.Ollama.APIBase != "" ||
|
|
v.Moonshot.APIKey != "" || v.Moonshot.APIBase != "" ||
|
|
v.ShengSuanYun.APIKey != "" || v.ShengSuanYun.APIBase != "" ||
|
|
v.DeepSeek.APIKey != "" || v.DeepSeek.APIBase != "" ||
|
|
v.Cerebras.APIKey != "" || v.Cerebras.APIBase != "" ||
|
|
v.VolcEngine.APIKey != "" || v.VolcEngine.APIBase != "" ||
|
|
v.GitHubCopilot.APIKey != "" || v.GitHubCopilot.APIBase != "" ||
|
|
v.Antigravity.APIKey != "" || v.Antigravity.APIBase != "" ||
|
|
v.Qwen.APIKey != "" || v.Qwen.APIBase != ""
|
|
}
|
|
|
|
// ValidateModelList validates all ModelConfig entries in the model_list.
|
|
// It checks that each model_name/model combination is valid.
|
|
func (c *Config) ValidateModelList() error {
|
|
for i := range c.ModelList {
|
|
if err := c.ModelList[i].Validate(); err != nil {
|
|
return fmt.Errorf("model_list[%d]: %w", i, err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ConvertProvidersToModelList converts the old ProvidersConfig to a slice of ModelConfig.
|
|
// This enables backward compatibility with existing configurations.
|
|
func ConvertProvidersToModelList(cfg *Config) []ModelConfig {
|
|
if cfg == nil {
|
|
return nil
|
|
}
|
|
|
|
var result []ModelConfig
|
|
p := cfg.Providers
|
|
|
|
// OpenAI
|
|
if p.OpenAI.APIKey != "" || p.OpenAI.APIBase != "" {
|
|
result = append(result, ModelConfig{
|
|
ModelName: "openai",
|
|
Model: "openai/gpt-4o",
|
|
APIKey: p.OpenAI.APIKey,
|
|
APIBase: p.OpenAI.APIBase,
|
|
Proxy: p.OpenAI.Proxy,
|
|
AuthMethod: p.OpenAI.AuthMethod,
|
|
})
|
|
}
|
|
|
|
// Anthropic
|
|
if p.Anthropic.APIKey != "" || p.Anthropic.APIBase != "" {
|
|
result = append(result, ModelConfig{
|
|
ModelName: "anthropic",
|
|
Model: "anthropic/claude-3-sonnet",
|
|
APIKey: p.Anthropic.APIKey,
|
|
APIBase: p.Anthropic.APIBase,
|
|
Proxy: p.Anthropic.Proxy,
|
|
AuthMethod: p.Anthropic.AuthMethod,
|
|
})
|
|
}
|
|
|
|
// OpenRouter
|
|
if p.OpenRouter.APIKey != "" || p.OpenRouter.APIBase != "" {
|
|
result = append(result, ModelConfig{
|
|
ModelName: "openrouter",
|
|
Model: "openrouter/auto",
|
|
APIKey: p.OpenRouter.APIKey,
|
|
APIBase: p.OpenRouter.APIBase,
|
|
Proxy: p.OpenRouter.Proxy,
|
|
})
|
|
}
|
|
|
|
// Groq
|
|
if p.Groq.APIKey != "" || p.Groq.APIBase != "" {
|
|
result = append(result, ModelConfig{
|
|
ModelName: "groq",
|
|
Model: "groq/llama-3.1-70b-versatile",
|
|
APIKey: p.Groq.APIKey,
|
|
APIBase: p.Groq.APIBase,
|
|
Proxy: p.Groq.Proxy,
|
|
})
|
|
}
|
|
|
|
// Zhipu
|
|
if p.Zhipu.APIKey != "" || p.Zhipu.APIBase != "" {
|
|
result = append(result, ModelConfig{
|
|
ModelName: "zhipu",
|
|
Model: "openai/glm-4",
|
|
APIKey: p.Zhipu.APIKey,
|
|
APIBase: p.Zhipu.APIBase,
|
|
Proxy: p.Zhipu.Proxy,
|
|
})
|
|
}
|
|
|
|
// VLLM
|
|
if p.VLLM.APIKey != "" || p.VLLM.APIBase != "" {
|
|
result = append(result, ModelConfig{
|
|
ModelName: "vllm",
|
|
Model: "openai/auto",
|
|
APIKey: p.VLLM.APIKey,
|
|
APIBase: p.VLLM.APIBase,
|
|
Proxy: p.VLLM.Proxy,
|
|
})
|
|
}
|
|
|
|
// Gemini
|
|
if p.Gemini.APIKey != "" || p.Gemini.APIBase != "" {
|
|
result = append(result, ModelConfig{
|
|
ModelName: "gemini",
|
|
Model: "openai/gemini-pro",
|
|
APIKey: p.Gemini.APIKey,
|
|
APIBase: p.Gemini.APIBase,
|
|
Proxy: p.Gemini.Proxy,
|
|
})
|
|
}
|
|
|
|
// Nvidia
|
|
if p.Nvidia.APIKey != "" || p.Nvidia.APIBase != "" {
|
|
result = append(result, ModelConfig{
|
|
ModelName: "nvidia",
|
|
Model: "nvidia/meta/llama-3.1-8b-instruct",
|
|
APIKey: p.Nvidia.APIKey,
|
|
APIBase: p.Nvidia.APIBase,
|
|
Proxy: p.Nvidia.Proxy,
|
|
})
|
|
}
|
|
|
|
// Ollama
|
|
if p.Ollama.APIKey != "" || p.Ollama.APIBase != "" {
|
|
result = append(result, ModelConfig{
|
|
ModelName: "ollama",
|
|
Model: "ollama/llama3",
|
|
APIKey: p.Ollama.APIKey,
|
|
APIBase: p.Ollama.APIBase,
|
|
Proxy: p.Ollama.Proxy,
|
|
})
|
|
}
|
|
|
|
// Moonshot
|
|
if p.Moonshot.APIKey != "" || p.Moonshot.APIBase != "" {
|
|
result = append(result, ModelConfig{
|
|
ModelName: "moonshot",
|
|
Model: "moonshot/kimi",
|
|
APIKey: p.Moonshot.APIKey,
|
|
APIBase: p.Moonshot.APIBase,
|
|
Proxy: p.Moonshot.Proxy,
|
|
})
|
|
}
|
|
|
|
// ShengSuanYun
|
|
if p.ShengSuanYun.APIKey != "" || p.ShengSuanYun.APIBase != "" {
|
|
result = append(result, ModelConfig{
|
|
ModelName: "shengsuanyun",
|
|
Model: "openai/auto",
|
|
APIKey: p.ShengSuanYun.APIKey,
|
|
APIBase: p.ShengSuanYun.APIBase,
|
|
Proxy: p.ShengSuanYun.Proxy,
|
|
})
|
|
}
|
|
|
|
// DeepSeek
|
|
if p.DeepSeek.APIKey != "" || p.DeepSeek.APIBase != "" {
|
|
result = append(result, ModelConfig{
|
|
ModelName: "deepseek",
|
|
Model: "openai/deepseek-chat",
|
|
APIKey: p.DeepSeek.APIKey,
|
|
APIBase: p.DeepSeek.APIBase,
|
|
Proxy: p.DeepSeek.Proxy,
|
|
})
|
|
}
|
|
|
|
// Cerebras
|
|
if p.Cerebras.APIKey != "" || p.Cerebras.APIBase != "" {
|
|
result = append(result, ModelConfig{
|
|
ModelName: "cerebras",
|
|
Model: "cerebras/llama-3.3-70b",
|
|
APIKey: p.Cerebras.APIKey,
|
|
APIBase: p.Cerebras.APIBase,
|
|
Proxy: p.Cerebras.Proxy,
|
|
})
|
|
}
|
|
|
|
// VolcEngine (Doubao)
|
|
if p.VolcEngine.APIKey != "" || p.VolcEngine.APIBase != "" {
|
|
result = append(result, ModelConfig{
|
|
ModelName: "volcengine",
|
|
Model: "openai/doubao-pro",
|
|
APIKey: p.VolcEngine.APIKey,
|
|
APIBase: p.VolcEngine.APIBase,
|
|
Proxy: p.VolcEngine.Proxy,
|
|
})
|
|
}
|
|
|
|
// GitHub Copilot
|
|
if p.GitHubCopilot.APIKey != "" || p.GitHubCopilot.APIBase != "" || p.GitHubCopilot.ConnectMode != "" {
|
|
result = append(result, ModelConfig{
|
|
ModelName: "github-copilot",
|
|
Model: "github-copilot/gpt-4o",
|
|
APIBase: p.GitHubCopilot.APIBase,
|
|
ConnectMode: p.GitHubCopilot.ConnectMode,
|
|
})
|
|
}
|
|
|
|
// Antigravity
|
|
if p.Antigravity.APIKey != "" || p.Antigravity.AuthMethod != "" {
|
|
result = append(result, ModelConfig{
|
|
ModelName: "antigravity",
|
|
Model: "antigravity/gemini-2.0-flash",
|
|
APIKey: p.Antigravity.APIKey,
|
|
AuthMethod: p.Antigravity.AuthMethod,
|
|
})
|
|
}
|
|
|
|
// Qwen
|
|
if p.Qwen.APIKey != "" || p.Qwen.APIBase != "" {
|
|
result = append(result, ModelConfig{
|
|
ModelName: "qwen",
|
|
Model: "qwen/qwen-max",
|
|
APIKey: p.Qwen.APIKey,
|
|
APIBase: p.Qwen.APIBase,
|
|
Proxy: p.Qwen.Proxy,
|
|
})
|
|
}
|
|
|
|
return result
|
|
}
|