fix(config): inherit provider settings for model_list entries

This commit is contained in:
Alix-007 2026-03-17 12:30:41 +08:00
parent cff85cfe5c
commit 5ada8c71ba
2 changed files with 293 additions and 8 deletions

View file

@ -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 {

View file

@ -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) {