fix(config): inherit provider settings for model_list entries
This commit is contained in:
parent
cff85cfe5c
commit
5ada8c71ba
2 changed files with 293 additions and 8 deletions
|
|
@ -920,14 +920,7 @@ func LoadConfig(path string) (*Config, error) {
|
|||
cfg.ModelList = ConvertProvidersToModelList(cfg)
|
||||
}
|
||||
|
||||
// Inherit credentials from providers to model_list entries (#1635).
|
||||
// When both providers and model_list are present, model_list entries
|
||||
// whose api_key/api_base are empty will inherit from the matching
|
||||
// provider (matched by protocol prefix). Explicit model_list values
|
||||
// always take precedence.
|
||||
if cfg.HasProvidersConfig() {
|
||||
InheritProviderCredentials(cfg.ModelList, cfg.Providers)
|
||||
}
|
||||
cfg.applyModelListProviderInheritance()
|
||||
|
||||
// Validate model_list for uniqueness and required fields
|
||||
if err := cfg.ValidateModelList(); err != nil {
|
||||
|
|
@ -990,6 +983,128 @@ func resolveAPIKeys(models []ModelConfig, configDir string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// applyModelListProviderInheritance fills missing model_list fields from the
|
||||
// matching legacy providers entry while keeping any explicit model_list values.
|
||||
func (c *Config) applyModelListProviderInheritance() {
|
||||
if len(c.ModelList) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
for i := range c.ModelList {
|
||||
protocol := canonicalModelProtocol(c.ModelList[i].Model)
|
||||
if protocol == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
providerCfg, ok := providerConfigForProtocol(c.Providers, protocol)
|
||||
if !ok || !hasInheritableProviderFields(providerCfg) {
|
||||
continue
|
||||
}
|
||||
|
||||
inheritModelConfig(&c.ModelList[i], providerCfg)
|
||||
}
|
||||
}
|
||||
|
||||
func canonicalModelProtocol(model string) string {
|
||||
model = strings.TrimSpace(model)
|
||||
if model == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
protocol, _, found := strings.Cut(model, "/")
|
||||
if !found {
|
||||
return "openai"
|
||||
}
|
||||
|
||||
switch strings.ToLower(protocol) {
|
||||
case "copilot":
|
||||
return "github-copilot"
|
||||
default:
|
||||
return strings.ToLower(protocol)
|
||||
}
|
||||
}
|
||||
|
||||
func providerConfigForProtocol(providers ProvidersConfig, protocol string) (ProviderConfig, bool) {
|
||||
switch protocol {
|
||||
case "anthropic":
|
||||
return providers.Anthropic, true
|
||||
case "openai":
|
||||
return providers.OpenAI.ProviderConfig, true
|
||||
case "litellm":
|
||||
return providers.LiteLLM, true
|
||||
case "openrouter":
|
||||
return providers.OpenRouter, true
|
||||
case "groq":
|
||||
return providers.Groq, true
|
||||
case "zhipu":
|
||||
return providers.Zhipu, true
|
||||
case "vllm":
|
||||
return providers.VLLM, true
|
||||
case "gemini":
|
||||
return providers.Gemini, true
|
||||
case "nvidia":
|
||||
return providers.Nvidia, true
|
||||
case "ollama":
|
||||
return providers.Ollama, true
|
||||
case "moonshot":
|
||||
return providers.Moonshot, true
|
||||
case "shengsuanyun":
|
||||
return providers.ShengSuanYun, true
|
||||
case "deepseek":
|
||||
return providers.DeepSeek, true
|
||||
case "cerebras":
|
||||
return providers.Cerebras, true
|
||||
case "vivgrid":
|
||||
return providers.Vivgrid, true
|
||||
case "volcengine":
|
||||
return providers.VolcEngine, true
|
||||
case "github-copilot":
|
||||
return providers.GitHubCopilot, true
|
||||
case "antigravity":
|
||||
return providers.Antigravity, true
|
||||
case "qwen":
|
||||
return providers.Qwen, true
|
||||
case "mistral":
|
||||
return providers.Mistral, true
|
||||
case "avian":
|
||||
return providers.Avian, true
|
||||
case "minimax":
|
||||
return providers.Minimax, true
|
||||
default:
|
||||
return ProviderConfig{}, false
|
||||
}
|
||||
}
|
||||
|
||||
func hasInheritableProviderFields(provider ProviderConfig) bool {
|
||||
return provider.APIKey != "" ||
|
||||
provider.APIBase != "" ||
|
||||
provider.Proxy != "" ||
|
||||
provider.RequestTimeout != 0 ||
|
||||
provider.AuthMethod != "" ||
|
||||
provider.ConnectMode != ""
|
||||
}
|
||||
|
||||
func inheritModelConfig(model *ModelConfig, provider ProviderConfig) {
|
||||
if model.APIKey == "" {
|
||||
model.APIKey = provider.APIKey
|
||||
}
|
||||
if model.APIBase == "" {
|
||||
model.APIBase = provider.APIBase
|
||||
}
|
||||
if model.Proxy == "" {
|
||||
model.Proxy = provider.Proxy
|
||||
}
|
||||
if model.RequestTimeout == 0 {
|
||||
model.RequestTimeout = provider.RequestTimeout
|
||||
}
|
||||
if model.AuthMethod == "" {
|
||||
model.AuthMethod = provider.AuthMethod
|
||||
}
|
||||
if model.ConnectMode == "" {
|
||||
model.ConnectMode = provider.ConnectMode
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Config) migrateChannelConfigs() {
|
||||
// Discord: mention_only -> group_trigger.mention_only
|
||||
if c.Channels.Discord.MentionOnly && !c.Channels.Discord.GroupTrigger.MentionOnly {
|
||||
|
|
|
|||
|
|
@ -555,6 +555,176 @@ func TestLoadConfig_WebToolsProxy(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestLoadConfig_ModelListInheritsProviderFields(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
configPath := filepath.Join(tmpDir, "config.json")
|
||||
configJSON := `{
|
||||
"providers": {
|
||||
"litellm": {
|
||||
"api_key": "shared-key",
|
||||
"api_base": "http://host:4000/v1",
|
||||
"proxy": "http://proxy:8080",
|
||||
"request_timeout": 45
|
||||
}
|
||||
},
|
||||
"model_list": [
|
||||
{
|
||||
"model_name": "kimi1",
|
||||
"model": "litellm/kimi-1"
|
||||
}
|
||||
]
|
||||
}`
|
||||
if err := os.WriteFile(configPath, []byte(configJSON), 0o600); err != nil {
|
||||
t.Fatalf("os.WriteFile() error: %v", err)
|
||||
}
|
||||
|
||||
cfg, err := LoadConfig(configPath)
|
||||
if err != nil {
|
||||
t.Fatalf("LoadConfig() error: %v", err)
|
||||
}
|
||||
|
||||
modelCfg, err := cfg.GetModelConfig("kimi1")
|
||||
if err != nil {
|
||||
t.Fatalf("GetModelConfig() error: %v", err)
|
||||
}
|
||||
if modelCfg.APIKey != "shared-key" {
|
||||
t.Fatalf("APIKey = %q, want %q", modelCfg.APIKey, "shared-key")
|
||||
}
|
||||
if modelCfg.APIBase != "http://host:4000/v1" {
|
||||
t.Fatalf("APIBase = %q, want %q", modelCfg.APIBase, "http://host:4000/v1")
|
||||
}
|
||||
if modelCfg.Proxy != "http://proxy:8080" {
|
||||
t.Fatalf("Proxy = %q, want %q", modelCfg.Proxy, "http://proxy:8080")
|
||||
}
|
||||
if modelCfg.RequestTimeout != 45 {
|
||||
t.Fatalf("RequestTimeout = %d, want %d", modelCfg.RequestTimeout, 45)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadConfig_ModelListKeepsExplicitOverrides(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
configPath := filepath.Join(tmpDir, "config.json")
|
||||
configJSON := `{
|
||||
"providers": {
|
||||
"litellm": {
|
||||
"api_key": "shared-key",
|
||||
"api_base": "http://host:4000/v1",
|
||||
"proxy": "http://proxy:8080",
|
||||
"request_timeout": 45
|
||||
}
|
||||
},
|
||||
"model_list": [
|
||||
{
|
||||
"model_name": "kimi1",
|
||||
"model": "litellm/kimi-1",
|
||||
"api_key": "model-key",
|
||||
"request_timeout": 90
|
||||
}
|
||||
]
|
||||
}`
|
||||
if err := os.WriteFile(configPath, []byte(configJSON), 0o600); err != nil {
|
||||
t.Fatalf("os.WriteFile() error: %v", err)
|
||||
}
|
||||
|
||||
cfg, err := LoadConfig(configPath)
|
||||
if err != nil {
|
||||
t.Fatalf("LoadConfig() error: %v", err)
|
||||
}
|
||||
|
||||
modelCfg, err := cfg.GetModelConfig("kimi1")
|
||||
if err != nil {
|
||||
t.Fatalf("GetModelConfig() error: %v", err)
|
||||
}
|
||||
if modelCfg.APIKey != "model-key" {
|
||||
t.Fatalf("APIKey = %q, want %q", modelCfg.APIKey, "model-key")
|
||||
}
|
||||
if modelCfg.APIBase != "http://host:4000/v1" {
|
||||
t.Fatalf("APIBase = %q, want %q", modelCfg.APIBase, "http://host:4000/v1")
|
||||
}
|
||||
if modelCfg.Proxy != "http://proxy:8080" {
|
||||
t.Fatalf("Proxy = %q, want %q", modelCfg.Proxy, "http://proxy:8080")
|
||||
}
|
||||
if modelCfg.RequestTimeout != 90 {
|
||||
t.Fatalf("RequestTimeout = %d, want %d", modelCfg.RequestTimeout, 90)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadConfig_ModelListInheritsOpenAIProviderForBareModel(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
configPath := filepath.Join(tmpDir, "config.json")
|
||||
configJSON := `{
|
||||
"providers": {
|
||||
"openai": {
|
||||
"api_key": "openai-key",
|
||||
"api_base": "https://api.example.com/v1"
|
||||
}
|
||||
},
|
||||
"model_list": [
|
||||
{
|
||||
"model_name": "gpt4",
|
||||
"model": "gpt-4.1"
|
||||
}
|
||||
]
|
||||
}`
|
||||
if err := os.WriteFile(configPath, []byte(configJSON), 0o600); err != nil {
|
||||
t.Fatalf("os.WriteFile() error: %v", err)
|
||||
}
|
||||
|
||||
cfg, err := LoadConfig(configPath)
|
||||
if err != nil {
|
||||
t.Fatalf("LoadConfig() error: %v", err)
|
||||
}
|
||||
|
||||
modelCfg, err := cfg.GetModelConfig("gpt4")
|
||||
if err != nil {
|
||||
t.Fatalf("GetModelConfig() error: %v", err)
|
||||
}
|
||||
if modelCfg.APIKey != "openai-key" {
|
||||
t.Fatalf("APIKey = %q, want %q", modelCfg.APIKey, "openai-key")
|
||||
}
|
||||
if modelCfg.APIBase != "https://api.example.com/v1" {
|
||||
t.Fatalf("APIBase = %q, want %q", modelCfg.APIBase, "https://api.example.com/v1")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadConfig_ModelListInheritsCopilotProviderAlias(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
configPath := filepath.Join(tmpDir, "config.json")
|
||||
configJSON := `{
|
||||
"providers": {
|
||||
"github_copilot": {
|
||||
"api_base": "localhost:5000",
|
||||
"connect_mode": "stdio"
|
||||
}
|
||||
},
|
||||
"model_list": [
|
||||
{
|
||||
"model_name": "copilot",
|
||||
"model": "copilot/gpt-5"
|
||||
}
|
||||
]
|
||||
}`
|
||||
if err := os.WriteFile(configPath, []byte(configJSON), 0o600); err != nil {
|
||||
t.Fatalf("os.WriteFile() error: %v", err)
|
||||
}
|
||||
|
||||
cfg, err := LoadConfig(configPath)
|
||||
if err != nil {
|
||||
t.Fatalf("LoadConfig() error: %v", err)
|
||||
}
|
||||
|
||||
modelCfg, err := cfg.GetModelConfig("copilot")
|
||||
if err != nil {
|
||||
t.Fatalf("GetModelConfig() error: %v", err)
|
||||
}
|
||||
if modelCfg.APIBase != "localhost:5000" {
|
||||
t.Fatalf("APIBase = %q, want %q", modelCfg.APIBase, "localhost:5000")
|
||||
}
|
||||
if modelCfg.ConnectMode != "stdio" {
|
||||
t.Fatalf("ConnectMode = %q, want %q", modelCfg.ConnectMode, "stdio")
|
||||
}
|
||||
}
|
||||
|
||||
// TestDefaultConfig_DMScope verifies the default dm_scope value
|
||||
// TestDefaultConfig_SummarizationThresholds verifies summarization defaults
|
||||
func TestDefaultConfig_SummarizationThresholds(t *testing.T) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue