fix: remove unnecessary lock mechanism and upgrade Claude 3 to Claude 4
- Remove sync.RWMutex and rrCounters from Config struct - Simplify GetModelConfig to use global atomic counter for load balancing - Remove unnecessary locks from HasProvidersConfig, SaveConfig, etc. - Add buildModelWithProtocol helper to handle models with existing prefix - Fix TestCreateProviderReturnsHTTPProviderForOpenRouter to use model_list - Upgrade all Claude 3 references to Claude 4 across documentation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
b7c906fe18
commit
5cd1597674
14 changed files with 134 additions and 100 deletions
|
|
@ -838,8 +838,8 @@ Cette conception permet également le **support multi-agent** avec une sélectio
|
||||||
"api_key": "sk-your-openai-key"
|
"api_key": "sk-your-openai-key"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"model_name": "claude-3-sonnet",
|
"model_name": "claude-sonnet-4",
|
||||||
"model": "anthropic/claude-3-5-sonnet-20241022",
|
"model": "anthropic/claude-sonnet-4-20250514",
|
||||||
"api_key": "sk-ant-your-key"
|
"api_key": "sk-ant-your-key"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -774,8 +774,8 @@ HEARTBEAT_OK 応答 ユーザーが直接結果を受け取る
|
||||||
"api_key": "sk-your-openai-key"
|
"api_key": "sk-your-openai-key"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"model_name": "claude-3-sonnet",
|
"model_name": "claude-sonnet-4",
|
||||||
"model": "anthropic/claude-3-5-sonnet-20241022",
|
"model": "anthropic/claude-sonnet-4-20250514",
|
||||||
"api_key": "sk-ant-your-key"
|
"api_key": "sk-ant-your-key"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -222,8 +222,8 @@ picoclaw onboard
|
||||||
"api_key": "your-api-key"
|
"api_key": "your-api-key"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"model_name": "claude3",
|
"model_name": "claude-sonnet-4",
|
||||||
"model": "anthropic/claude-3-sonnet",
|
"model": "anthropic/claude-sonnet-4",
|
||||||
"api_key": "your-anthropic-key"
|
"api_key": "your-anthropic-key"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
@ -733,8 +733,8 @@ This design also enables **multi-agent support** with flexible provider selectio
|
||||||
"api_key": "sk-your-openai-key"
|
"api_key": "sk-your-openai-key"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"model_name": "claude-3-sonnet",
|
"model_name": "claude-sonnet-4",
|
||||||
"model": "anthropic/claude-3-5-sonnet-20241022",
|
"model": "anthropic/claude-sonnet-4-20250514",
|
||||||
"api_key": "sk-ant-your-key"
|
"api_key": "sk-ant-your-key"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -839,8 +839,8 @@ Este design também possibilita o **suporte multi-agent** com seleção flexíve
|
||||||
"api_key": "sk-your-openai-key"
|
"api_key": "sk-your-openai-key"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"model_name": "claude-3-sonnet",
|
"model_name": "claude-sonnet-4",
|
||||||
"model": "anthropic/claude-3-5-sonnet-20241022",
|
"model": "anthropic/claude-sonnet-4-20250514",
|
||||||
"api_key": "sk-ant-your-key"
|
"api_key": "sk-ant-your-key"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -816,8 +816,8 @@ Thiết kế này cũng cho phép **hỗ trợ đa tác nhân** với lựa ch
|
||||||
"api_key": "sk-your-openai-key"
|
"api_key": "sk-your-openai-key"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"model_name": "claude-3-sonnet",
|
"model_name": "claude-sonnet-4",
|
||||||
"model": "anthropic/claude-3-5-sonnet-20241022",
|
"model": "anthropic/claude-sonnet-4-20250514",
|
||||||
"api_key": "sk-ant-your-key"
|
"api_key": "sk-ant-your-key"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -231,8 +231,8 @@ picoclaw onboard
|
||||||
"api_key": "your-api-key"
|
"api_key": "your-api-key"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"model_name": "claude3",
|
"model_name": "claude-sonnet-4",
|
||||||
"model": "anthropic/claude-3-sonnet",
|
"model": "anthropic/claude-sonnet-4",
|
||||||
"api_key": "your-anthropic-key"
|
"api_key": "your-anthropic-key"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
@ -610,8 +610,8 @@ Agent 读取 HEARTBEAT.md
|
||||||
"api_key": "sk-your-openai-key"
|
"api_key": "sk-your-openai-key"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"model_name": "claude-3-sonnet",
|
"model_name": "claude-sonnet-4",
|
||||||
"model": "anthropic/claude-3-5-sonnet-20241022",
|
"model": "anthropic/claude-sonnet-4-20250514",
|
||||||
"api_key": "sk-ant-your-key"
|
"api_key": "sk-ant-your-key"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,8 @@
|
||||||
"api_base": "https://api.openai.com/v1"
|
"api_base": "https://api.openai.com/v1"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"model_name": "claude3",
|
"model_name": "claude-sonnet-4",
|
||||||
"model": "anthropic/claude-3-sonnet",
|
"model": "anthropic/claude-sonnet-4",
|
||||||
"api_key": "sk-ant-your-key",
|
"api_key": "sk-ant-your-key",
|
||||||
"api_base": "https://api.anthropic.com/v1"
|
"api_base": "https://api.anthropic.com/v1"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,7 @@ Problem: Agent needs to know both `provider` and `model`, adding complexity.
|
||||||
Inspired by [LiteLLM](https://docs.litellm.ai/docs/proxy/configs) design:
|
Inspired by [LiteLLM](https://docs.litellm.ai/docs/proxy/configs) design:
|
||||||
|
|
||||||
1. **Model-centric**: Users care about models, not providers
|
1. **Model-centric**: Users care about models, not providers
|
||||||
2. **Protocol prefix**: Use `protocol/model_name` format, e.g., `openai/gpt-5.2`, `anthropic/claude-3-sonnet`
|
2. **Protocol prefix**: Use `protocol/model_name` format, e.g., `openai/gpt-5.2`, `anthropic/claude-sonnet-4`
|
||||||
3. **Configuration-driven**: Adding new Providers only requires config changes, no code changes
|
3. **Configuration-driven**: Adding new Providers only requires config changes, no code changes
|
||||||
|
|
||||||
### 2.2 New Configuration Structure
|
### 2.2 New Configuration Structure
|
||||||
|
|
@ -86,8 +86,8 @@ Inspired by [LiteLLM](https://docs.litellm.ai/docs/proxy/configs) design:
|
||||||
"api_key": "sk-xxx"
|
"api_key": "sk-xxx"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"model_name": "claude-3-sonnet",
|
"model_name": "claude-sonnet-4",
|
||||||
"model": "anthropic/claude-3-5-sonnet-20241022",
|
"model": "anthropic/claude-sonnet-4-20250514",
|
||||||
"api_key": "sk-xxx"
|
"api_key": "sk-xxx"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -184,7 +184,7 @@ Identify protocol via prefix in `model` field:
|
||||||
"system_prompt": "You are a coding assistant..."
|
"system_prompt": "You are a coding assistant..."
|
||||||
},
|
},
|
||||||
"translator": {
|
"translator": {
|
||||||
"model": "claude-3-sonnet"
|
"model": "claude-sonnet-4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -58,8 +58,8 @@ The new `model_list` configuration offers several advantages:
|
||||||
"api_base": "https://api.openai.com/v1"
|
"api_base": "https://api.openai.com/v1"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"model_name": "claude3",
|
"model_name": "claude-sonnet-4",
|
||||||
"model": "anthropic/claude-3-sonnet",
|
"model": "anthropic/claude-sonnet-4",
|
||||||
"api_key": "sk-ant-your-key"
|
"api_key": "sk-ant-your-key"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -83,12 +83,12 @@ The `model` field uses a protocol prefix format: `[protocol/]model-identifier`
|
||||||
| Prefix | Description | Example |
|
| Prefix | Description | Example |
|
||||||
|--------|-------------|---------|
|
|--------|-------------|---------|
|
||||||
| `openai/` | OpenAI API (default) | `openai/gpt-5.2` |
|
| `openai/` | OpenAI API (default) | `openai/gpt-5.2` |
|
||||||
| `anthropic/` | Anthropic API | `anthropic/claude-3-opus` |
|
| `anthropic/` | Anthropic API | `anthropic/claude-opus-4` |
|
||||||
| `antigravity/` | Google via Antigravity OAuth | `antigravity/gemini-2.0-flash` |
|
| `antigravity/` | Google via Antigravity OAuth | `antigravity/gemini-2.0-flash` |
|
||||||
| `claude-cli/` | Claude CLI (local) | `claude-cli/claude-3-sonnet` |
|
| `claude-cli/` | Claude CLI (local) | `claude-cli/claude-sonnet-4` |
|
||||||
| `codex-cli/` | Codex CLI (local) | `codex-cli/codex-4` |
|
| `codex-cli/` | Codex CLI (local) | `codex-cli/codex-4` |
|
||||||
| `github-copilot/` | GitHub Copilot | `github-copilot/gpt-4o` |
|
| `github-copilot/` | GitHub Copilot | `github-copilot/gpt-4o` |
|
||||||
| `openrouter/` | OpenRouter | `openrouter/anthropic/claude-3` |
|
| `openrouter/` | OpenRouter | `openrouter/anthropic/claude-sonnet-4` |
|
||||||
| `groq/` | Groq API | `groq/llama-3.1-70b` |
|
| `groq/` | Groq API | `groq/llama-3.1-70b` |
|
||||||
| `deepseek/` | DeepSeek API | `deepseek/deepseek-chat` |
|
| `deepseek/` | DeepSeek API | `deepseek/deepseek-chat` |
|
||||||
| `cerebras/` | Cerebras API | `cerebras/llama-3.3-70b` |
|
| `cerebras/` | Cerebras API | `cerebras/llama-3.3-70b` |
|
||||||
|
|
|
||||||
|
|
@ -5,12 +5,14 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sync"
|
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
|
||||||
"github.com/caarlos0/env/v11"
|
"github.com/caarlos0/env/v11"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// rrCounter is a global counter for round-robin load balancing across models.
|
||||||
|
var rrCounter atomic.Uint64
|
||||||
|
|
||||||
// FlexibleStringSlice is a []string that also accepts JSON numbers,
|
// FlexibleStringSlice is a []string that also accepts JSON numbers,
|
||||||
// so allow_from can contain both "123" and 123.
|
// so allow_from can contain both "123" and 123.
|
||||||
type FlexibleStringSlice []string
|
type FlexibleStringSlice []string
|
||||||
|
|
@ -45,18 +47,16 @@ func (f *FlexibleStringSlice) UnmarshalJSON(data []byte) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Agents AgentsConfig `json:"agents"`
|
Agents AgentsConfig `json:"agents"`
|
||||||
Bindings []AgentBinding `json:"bindings,omitempty"`
|
Bindings []AgentBinding `json:"bindings,omitempty"`
|
||||||
Session SessionConfig `json:"session,omitempty"`
|
Session SessionConfig `json:"session,omitempty"`
|
||||||
Channels ChannelsConfig `json:"channels"`
|
Channels ChannelsConfig `json:"channels"`
|
||||||
Providers ProvidersConfig `json:"providers,omitempty"`
|
Providers ProvidersConfig `json:"providers,omitempty"`
|
||||||
ModelList []ModelConfig `json:"model_list"` // New model-centric provider configuration
|
ModelList []ModelConfig `json:"model_list"` // New model-centric provider configuration
|
||||||
Gateway GatewayConfig `json:"gateway"`
|
Gateway GatewayConfig `json:"gateway"`
|
||||||
Tools ToolsConfig `json:"tools"`
|
Tools ToolsConfig `json:"tools"`
|
||||||
Heartbeat HeartbeatConfig `json:"heartbeat"`
|
Heartbeat HeartbeatConfig `json:"heartbeat"`
|
||||||
Devices DevicesConfig `json:"devices"`
|
Devices DevicesConfig `json:"devices"`
|
||||||
mu sync.RWMutex
|
|
||||||
rrCounters map[string]*atomic.Uint64 // Round-robin counters for load balancing
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MarshalJSON implements custom JSON marshaling for Config
|
// MarshalJSON implements custom JSON marshaling for Config
|
||||||
|
|
@ -350,7 +350,7 @@ type OpenAIProviderConfig struct {
|
||||||
type ModelConfig struct {
|
type ModelConfig struct {
|
||||||
// Required fields
|
// Required fields
|
||||||
ModelName string `json:"model_name"` // User-facing alias for the model
|
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")
|
Model string `json:"model"` // Protocol/model-identifier (e.g., "openai/gpt-4o", "anthropic/claude-sonnet-4")
|
||||||
|
|
||||||
// HTTP-based providers
|
// HTTP-based providers
|
||||||
APIBase string `json:"api_base,omitempty"` // API endpoint URL
|
APIBase string `json:"api_base,omitempty"` // API endpoint URL
|
||||||
|
|
@ -454,9 +454,6 @@ func LoadConfig(path string) (*Config, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func SaveConfig(path string, cfg *Config) error {
|
func SaveConfig(path string, cfg *Config) error {
|
||||||
cfg.mu.RLock()
|
|
||||||
defer cfg.mu.RUnlock()
|
|
||||||
|
|
||||||
data, err := json.MarshalIndent(cfg, "", " ")
|
data, err := json.MarshalIndent(cfg, "", " ")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -471,14 +468,10 @@ func SaveConfig(path string, cfg *Config) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Config) WorkspacePath() string {
|
func (c *Config) WorkspacePath() string {
|
||||||
c.mu.RLock()
|
|
||||||
defer c.mu.RUnlock()
|
|
||||||
return expandHome(c.Agents.Defaults.Workspace)
|
return expandHome(c.Agents.Defaults.Workspace)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Config) GetAPIKey() string {
|
func (c *Config) GetAPIKey() string {
|
||||||
c.mu.RLock()
|
|
||||||
defer c.mu.RUnlock()
|
|
||||||
if c.Providers.OpenRouter.APIKey != "" {
|
if c.Providers.OpenRouter.APIKey != "" {
|
||||||
return c.Providers.OpenRouter.APIKey
|
return c.Providers.OpenRouter.APIKey
|
||||||
}
|
}
|
||||||
|
|
@ -510,8 +503,6 @@ func (c *Config) GetAPIKey() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Config) GetAPIBase() string {
|
func (c *Config) GetAPIBase() string {
|
||||||
c.mu.RLock()
|
|
||||||
defer c.mu.RUnlock()
|
|
||||||
if c.Providers.OpenRouter.APIKey != "" {
|
if c.Providers.OpenRouter.APIKey != "" {
|
||||||
if c.Providers.OpenRouter.APIBase != "" {
|
if c.Providers.OpenRouter.APIBase != "" {
|
||||||
return c.Providers.OpenRouter.APIBase
|
return c.Providers.OpenRouter.APIBase
|
||||||
|
|
@ -544,54 +535,22 @@ func expandHome(path string) string {
|
||||||
// GetModelConfig returns the ModelConfig for the given model name.
|
// GetModelConfig returns the ModelConfig for the given model name.
|
||||||
// If multiple configs exist with the same model_name, it uses round-robin
|
// 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.
|
// selection for load balancing. Returns an error if the model is not found.
|
||||||
// Uses double-check locking for optimal read performance.
|
|
||||||
func (c *Config) GetModelConfig(modelName string) (*ModelConfig, error) {
|
func (c *Config) GetModelConfig(modelName string) (*ModelConfig, error) {
|
||||||
// First pass: use read lock to find matches
|
matches := c.findMatches(modelName)
|
||||||
c.mu.RLock()
|
|
||||||
matches := c.findMatchesLocked(modelName)
|
|
||||||
if len(matches) == 0 {
|
if len(matches) == 0 {
|
||||||
c.mu.RUnlock()
|
|
||||||
return nil, fmt.Errorf("model %q not found in model_list or providers", modelName)
|
return nil, fmt.Errorf("model %q not found in model_list or providers", modelName)
|
||||||
}
|
}
|
||||||
if len(matches) == 1 {
|
if len(matches) == 1 {
|
||||||
c.mu.RUnlock()
|
|
||||||
return &matches[0], nil
|
return &matches[0], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Multiple configs - check if counter exists
|
// Multiple configs - use round-robin for load balancing
|
||||||
counter, ok := c.rrCounters[modelName]
|
idx := rrCounter.Add(1) % uint64(len(matches))
|
||||||
c.mu.RUnlock()
|
|
||||||
|
|
||||||
// Double-check locking: only acquire write lock if counter needs initialization
|
|
||||||
if !ok {
|
|
||||||
c.mu.Lock()
|
|
||||||
// Re-check after acquiring write lock
|
|
||||||
if c.rrCounters == nil {
|
|
||||||
c.rrCounters = make(map[string]*atomic.Uint64)
|
|
||||||
}
|
|
||||||
if c.rrCounters[modelName] == nil {
|
|
||||||
c.rrCounters[modelName] = &atomic.Uint64{}
|
|
||||||
}
|
|
||||||
counter = c.rrCounters[modelName]
|
|
||||||
c.mu.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Re-fetch matches to ensure consistency (ModelList could have changed)
|
|
||||||
c.mu.RLock()
|
|
||||||
matches = c.findMatchesLocked(modelName)
|
|
||||||
c.mu.RUnlock()
|
|
||||||
|
|
||||||
if len(matches) == 0 {
|
|
||||||
return nil, fmt.Errorf("model %q not found in model_list or providers", modelName)
|
|
||||||
}
|
|
||||||
|
|
||||||
idx := counter.Add(1) % uint64(len(matches))
|
|
||||||
return &matches[idx], nil
|
return &matches[idx], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// findMatchesLocked finds all ModelConfig entries with the given model_name.
|
// findMatches finds all ModelConfig entries with the given model_name.
|
||||||
// Must be called with c.mu locked (read or write).
|
func (c *Config) findMatches(modelName string) []ModelConfig {
|
||||||
func (c *Config) findMatchesLocked(modelName string) []ModelConfig {
|
|
||||||
var matches []ModelConfig
|
var matches []ModelConfig
|
||||||
for i := range c.ModelList {
|
for i := range c.ModelList {
|
||||||
if c.ModelList[i].ModelName == modelName {
|
if c.ModelList[i].ModelName == modelName {
|
||||||
|
|
@ -603,9 +562,6 @@ func (c *Config) findMatchesLocked(modelName string) []ModelConfig {
|
||||||
|
|
||||||
// HasProvidersConfig checks if any provider in the old providers config has configuration.
|
// HasProvidersConfig checks if any provider in the old providers config has configuration.
|
||||||
func (c *Config) HasProvidersConfig() bool {
|
func (c *Config) HasProvidersConfig() bool {
|
||||||
c.mu.RLock()
|
|
||||||
defer c.mu.RUnlock()
|
|
||||||
|
|
||||||
v := c.Providers
|
v := c.Providers
|
||||||
return v.Anthropic.APIKey != "" || v.Anthropic.APIBase != "" ||
|
return v.Anthropic.APIKey != "" || v.Anthropic.APIBase != "" ||
|
||||||
v.OpenAI.APIKey != "" || v.OpenAI.APIBase != "" ||
|
v.OpenAI.APIKey != "" || v.OpenAI.APIBase != "" ||
|
||||||
|
|
|
||||||
|
|
@ -160,6 +160,12 @@ func DefaultConfig() *Config {
|
||||||
},
|
},
|
||||||
|
|
||||||
// OpenRouter (100+ models) - https://openrouter.ai/keys
|
// OpenRouter (100+ models) - https://openrouter.ai/keys
|
||||||
|
{
|
||||||
|
ModelName: "openrouter-auto",
|
||||||
|
Model: "openrouter/auto",
|
||||||
|
APIBase: "https://openrouter.ai/api/v1",
|
||||||
|
APIKey: "",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
ModelName: "openrouter-gpt-5.2",
|
ModelName: "openrouter-gpt-5.2",
|
||||||
Model: "openrouter/openai/gpt-5.2",
|
Model: "openrouter/openai/gpt-5.2",
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,17 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// buildModelWithProtocol constructs a model string with protocol prefix.
|
||||||
|
// If the model already contains a "/" (indicating it has a protocol prefix), it is returned as-is.
|
||||||
|
// Otherwise, the protocol prefix is added.
|
||||||
|
func buildModelWithProtocol(protocol, model string) string {
|
||||||
|
if strings.Contains(model, "/") {
|
||||||
|
// Model already has a protocol prefix, return as-is
|
||||||
|
return model
|
||||||
|
}
|
||||||
|
return protocol + "/" + model
|
||||||
|
}
|
||||||
|
|
||||||
// providerMigrationConfig defines how to migrate a provider from old config to new format.
|
// providerMigrationConfig defines how to migrate a provider from old config to new format.
|
||||||
type providerMigrationConfig struct {
|
type providerMigrationConfig struct {
|
||||||
// providerNames are the possible names used in agents.defaults.provider
|
// providerNames are the possible names used in agents.defaults.provider
|
||||||
|
|
@ -67,7 +78,7 @@ func ConvertProvidersToModelList(cfg *Config) []ModelConfig {
|
||||||
}
|
}
|
||||||
return ModelConfig{
|
return ModelConfig{
|
||||||
ModelName: "anthropic",
|
ModelName: "anthropic",
|
||||||
Model: "anthropic/claude-3-sonnet",
|
Model: "anthropic/claude-sonnet-4",
|
||||||
APIKey: p.Anthropic.APIKey,
|
APIKey: p.Anthropic.APIKey,
|
||||||
APIBase: p.Anthropic.APIBase,
|
APIBase: p.Anthropic.APIBase,
|
||||||
Proxy: p.Anthropic.Proxy,
|
Proxy: p.Anthropic.Proxy,
|
||||||
|
|
@ -325,13 +336,13 @@ func ConvertProvidersToModelList(cfg *Config) []ModelConfig {
|
||||||
// Check if this is the user's configured provider
|
// Check if this is the user's configured provider
|
||||||
if slices.Contains(m.providerNames, userProvider) && userModel != "" {
|
if slices.Contains(m.providerNames, userProvider) && userModel != "" {
|
||||||
// Use the user's configured model instead of default
|
// Use the user's configured model instead of default
|
||||||
mc.Model = m.protocol + "/" + userModel
|
mc.Model = buildModelWithProtocol(m.protocol, userModel)
|
||||||
} else if userProvider == "" && userModel != "" && !legacyModelNameApplied {
|
} else if userProvider == "" && userModel != "" && !legacyModelNameApplied {
|
||||||
// Legacy config: no explicit provider field but model is specified
|
// Legacy config: no explicit provider field but model is specified
|
||||||
// Use userModel as ModelName for the FIRST provider so GetModelConfig(model) can find it
|
// Use userModel as ModelName for the FIRST provider so GetModelConfig(model) can find it
|
||||||
// This maintains backward compatibility with old configs that relied on implicit provider selection
|
// This maintains backward compatibility with old configs that relied on implicit provider selection
|
||||||
mc.ModelName = userModel
|
mc.ModelName = userModel
|
||||||
mc.Model = m.protocol + "/" + userModel
|
mc.Model = buildModelWithProtocol(m.protocol, userModel)
|
||||||
legacyModelNameApplied = true
|
legacyModelNameApplied = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -58,8 +58,8 @@ func TestConvertProvidersToModelList_Anthropic(t *testing.T) {
|
||||||
if result[0].ModelName != "anthropic" {
|
if result[0].ModelName != "anthropic" {
|
||||||
t.Errorf("ModelName = %q, want %q", result[0].ModelName, "anthropic")
|
t.Errorf("ModelName = %q, want %q", result[0].ModelName, "anthropic")
|
||||||
}
|
}
|
||||||
if result[0].Model != "anthropic/claude-3-sonnet" {
|
if result[0].Model != "anthropic/claude-sonnet-4" {
|
||||||
t.Errorf("Model = %q, want %q", result[0].Model, "anthropic/claude-3-sonnet")
|
t.Errorf("Model = %q, want %q", result[0].Model, "anthropic/claude-sonnet-4")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -239,7 +239,7 @@ func TestConvertProvidersToModelList_PreservesUserModel_Anthropic(t *testing.T)
|
||||||
Agents: AgentsConfig{
|
Agents: AgentsConfig{
|
||||||
Defaults: AgentDefaults{
|
Defaults: AgentDefaults{
|
||||||
Provider: "claude", // alternative name
|
Provider: "claude", // alternative name
|
||||||
Model: "claude-3-opus-20240229",
|
Model: "claude-opus-4-20250514",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Providers: ProvidersConfig{
|
Providers: ProvidersConfig{
|
||||||
|
|
@ -253,8 +253,8 @@ func TestConvertProvidersToModelList_PreservesUserModel_Anthropic(t *testing.T)
|
||||||
t.Fatalf("len(result) = %d, want 1", len(result))
|
t.Fatalf("len(result) = %d, want 1", len(result))
|
||||||
}
|
}
|
||||||
|
|
||||||
if result[0].Model != "anthropic/claude-3-opus-20240229" {
|
if result[0].Model != "anthropic/claude-opus-4-20250514" {
|
||||||
t.Errorf("Model = %q, want %q", result[0].Model, "anthropic/claude-3-opus-20240229")
|
t.Errorf("Model = %q, want %q", result[0].Model, "anthropic/claude-opus-4-20250514")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -495,3 +495,57 @@ func TestConvertProvidersToModelList_NoProviderField_NoModel(t *testing.T) {
|
||||||
t.Errorf("ModelName = %q, want %q", result[0].ModelName, "zhipu")
|
t.Errorf("ModelName = %q, want %q", result[0].ModelName, "zhipu")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tests for buildModelWithProtocol helper function
|
||||||
|
|
||||||
|
func TestBuildModelWithProtocol_NoPrefix(t *testing.T) {
|
||||||
|
result := buildModelWithProtocol("openai", "gpt-5.2")
|
||||||
|
if result != "openai/gpt-5.2" {
|
||||||
|
t.Errorf("buildModelWithProtocol(openai, gpt-5.2) = %q, want %q", result, "openai/gpt-5.2")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildModelWithProtocol_AlreadyHasPrefix(t *testing.T) {
|
||||||
|
result := buildModelWithProtocol("openrouter", "openrouter/auto")
|
||||||
|
if result != "openrouter/auto" {
|
||||||
|
t.Errorf("buildModelWithProtocol(openrouter, openrouter/auto) = %q, want %q", result, "openrouter/auto")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildModelWithProtocol_DifferentPrefix(t *testing.T) {
|
||||||
|
result := buildModelWithProtocol("anthropic", "openrouter/claude-sonnet-4")
|
||||||
|
if result != "openrouter/claude-sonnet-4" {
|
||||||
|
t.Errorf("buildModelWithProtocol(anthropic, openrouter/claude-sonnet-4) = %q, want %q", result, "openrouter/claude-sonnet-4")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test for legacy config with protocol prefix in model name
|
||||||
|
func TestConvertProvidersToModelList_LegacyModelWithProtocolPrefix(t *testing.T) {
|
||||||
|
cfg := &Config{
|
||||||
|
Agents: AgentsConfig{
|
||||||
|
Defaults: AgentDefaults{
|
||||||
|
Provider: "", // No explicit provider
|
||||||
|
Model: "openrouter/auto", // Model already has protocol prefix
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Providers: ProvidersConfig{
|
||||||
|
OpenRouter: ProviderConfig{APIKey: "sk-or-test"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
result := ConvertProvidersToModelList(cfg)
|
||||||
|
|
||||||
|
if len(result) < 1 {
|
||||||
|
t.Fatalf("len(result) = %d, want at least 1", len(result))
|
||||||
|
}
|
||||||
|
|
||||||
|
// First provider should use userModel as ModelName for backward compatibility
|
||||||
|
if result[0].ModelName != "openrouter/auto" {
|
||||||
|
t.Errorf("ModelName = %q, want %q", result[0].ModelName, "openrouter/auto")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Model should NOT have duplicated prefix
|
||||||
|
if result[0].Model != "openrouter/auto" {
|
||||||
|
t.Errorf("Model = %q, want %q (should not duplicate prefix)", result[0].Model, "openrouter/auto")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -196,8 +196,15 @@ func TestResolveProviderSelection(t *testing.T) {
|
||||||
|
|
||||||
func TestCreateProviderReturnsHTTPProviderForOpenRouter(t *testing.T) {
|
func TestCreateProviderReturnsHTTPProviderForOpenRouter(t *testing.T) {
|
||||||
cfg := config.DefaultConfig()
|
cfg := config.DefaultConfig()
|
||||||
cfg.Agents.Defaults.Model = "openrouter/auto"
|
cfg.Agents.Defaults.Model = "test-openrouter"
|
||||||
cfg.Providers.OpenRouter.APIKey = "sk-or-test"
|
cfg.ModelList = []config.ModelConfig{
|
||||||
|
{
|
||||||
|
ModelName: "test-openrouter",
|
||||||
|
Model: "openrouter/auto",
|
||||||
|
APIKey: "sk-or-test",
|
||||||
|
APIBase: "https://openrouter.ai/api/v1",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
provider, _, err := CreateProvider(cfg)
|
provider, _, err := CreateProvider(cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue