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)
|
cfg.ModelList = ConvertProvidersToModelList(cfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Inherit credentials from providers to model_list entries (#1635).
|
cfg.applyModelListProviderInheritance()
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate model_list for uniqueness and required fields
|
// Validate model_list for uniqueness and required fields
|
||||||
if err := cfg.ValidateModelList(); err != nil {
|
if err := cfg.ValidateModelList(); err != nil {
|
||||||
|
|
@ -990,6 +983,128 @@ func resolveAPIKeys(models []ModelConfig, configDir string) error {
|
||||||
return nil
|
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() {
|
func (c *Config) migrateChannelConfigs() {
|
||||||
// Discord: mention_only -> group_trigger.mention_only
|
// Discord: mention_only -> group_trigger.mention_only
|
||||||
if c.Channels.Discord.MentionOnly && !c.Channels.Discord.GroupTrigger.MentionOnly {
|
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_DMScope verifies the default dm_scope value
|
||||||
// TestDefaultConfig_SummarizationThresholds verifies summarization defaults
|
// TestDefaultConfig_SummarizationThresholds verifies summarization defaults
|
||||||
func TestDefaultConfig_SummarizationThresholds(t *testing.T) {
|
func TestDefaultConfig_SummarizationThresholds(t *testing.T) {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue