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:
yinwm
2026-02-20 11:34:52 +08:00
parent b7c906fe18
commit 5cd1597674
14 changed files with 134 additions and 100 deletions
+2 -2
View File
@@ -838,8 +838,8 @@ Cette conception permet également le **support multi-agent** avec une sélectio
"api_key": "sk-your-openai-key"
},
{
"model_name": "claude-3-sonnet",
"model": "anthropic/claude-3-5-sonnet-20241022",
"model_name": "claude-sonnet-4",
"model": "anthropic/claude-sonnet-4-20250514",
"api_key": "sk-ant-your-key"
},
{
+2 -2
View File
@@ -774,8 +774,8 @@ HEARTBEAT_OK 応答 ユーザーが直接結果を受け取る
"api_key": "sk-your-openai-key"
},
{
"model_name": "claude-3-sonnet",
"model": "anthropic/claude-3-5-sonnet-20241022",
"model_name": "claude-sonnet-4",
"model": "anthropic/claude-sonnet-4-20250514",
"api_key": "sk-ant-your-key"
},
{
+4 -4
View File
@@ -222,8 +222,8 @@ picoclaw onboard
"api_key": "your-api-key"
},
{
"model_name": "claude3",
"model": "anthropic/claude-3-sonnet",
"model_name": "claude-sonnet-4",
"model": "anthropic/claude-sonnet-4",
"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"
},
{
"model_name": "claude-3-sonnet",
"model": "anthropic/claude-3-5-sonnet-20241022",
"model_name": "claude-sonnet-4",
"model": "anthropic/claude-sonnet-4-20250514",
"api_key": "sk-ant-your-key"
},
{
+2 -2
View File
@@ -839,8 +839,8 @@ Este design também possibilita o **suporte multi-agent** com seleção flexíve
"api_key": "sk-your-openai-key"
},
{
"model_name": "claude-3-sonnet",
"model": "anthropic/claude-3-5-sonnet-20241022",
"model_name": "claude-sonnet-4",
"model": "anthropic/claude-sonnet-4-20250514",
"api_key": "sk-ant-your-key"
},
{
+2 -2
View File
@@ -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"
},
{
"model_name": "claude-3-sonnet",
"model": "anthropic/claude-3-5-sonnet-20241022",
"model_name": "claude-sonnet-4",
"model": "anthropic/claude-sonnet-4-20250514",
"api_key": "sk-ant-your-key"
},
{
+4 -4
View File
@@ -231,8 +231,8 @@ picoclaw onboard
"api_key": "your-api-key"
},
{
"model_name": "claude3",
"model": "anthropic/claude-3-sonnet",
"model_name": "claude-sonnet-4",
"model": "anthropic/claude-sonnet-4",
"api_key": "your-anthropic-key"
}
],
@@ -610,8 +610,8 @@ Agent 读取 HEARTBEAT.md
"api_key": "sk-your-openai-key"
},
{
"model_name": "claude-3-sonnet",
"model": "anthropic/claude-3-5-sonnet-20241022",
"model_name": "claude-sonnet-4",
"model": "anthropic/claude-sonnet-4-20250514",
"api_key": "sk-ant-your-key"
},
{
+2 -2
View File
@@ -17,8 +17,8 @@
"api_base": "https://api.openai.com/v1"
},
{
"model_name": "claude3",
"model": "anthropic/claude-3-sonnet",
"model_name": "claude-sonnet-4",
"model": "anthropic/claude-sonnet-4",
"api_key": "sk-ant-your-key",
"api_base": "https://api.anthropic.com/v1"
},
+4 -4
View File
@@ -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:
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
### 2.2 New Configuration Structure
@@ -86,8 +86,8 @@ Inspired by [LiteLLM](https://docs.litellm.ai/docs/proxy/configs) design:
"api_key": "sk-xxx"
},
{
"model_name": "claude-3-sonnet",
"model": "anthropic/claude-3-5-sonnet-20241022",
"model_name": "claude-sonnet-4",
"model": "anthropic/claude-sonnet-4-20250514",
"api_key": "sk-xxx"
},
{
@@ -184,7 +184,7 @@ Identify protocol via prefix in `model` field:
"system_prompt": "You are a coding assistant..."
},
"translator": {
"model": "claude-3-sonnet"
"model": "claude-sonnet-4"
}
}
}
+5 -5
View File
@@ -58,8 +58,8 @@ The new `model_list` configuration offers several advantages:
"api_base": "https://api.openai.com/v1"
},
{
"model_name": "claude3",
"model": "anthropic/claude-3-sonnet",
"model_name": "claude-sonnet-4",
"model": "anthropic/claude-sonnet-4",
"api_key": "sk-ant-your-key"
},
{
@@ -83,12 +83,12 @@ The `model` field uses a protocol prefix format: `[protocol/]model-identifier`
| Prefix | Description | Example |
|--------|-------------|---------|
| `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` |
| `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` |
| `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` |
| `deepseek/` | DeepSeek API | `deepseek/deepseek-chat` |
| `cerebras/` | Cerebras API | `cerebras/llama-3.3-70b` |
+19 -63
View File
@@ -5,12 +5,14 @@ import (
"fmt"
"os"
"path/filepath"
"sync"
"sync/atomic"
"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,
// so allow_from can contain both "123" and 123.
type FlexibleStringSlice []string
@@ -45,18 +47,16 @@ func (f *FlexibleStringSlice) UnmarshalJSON(data []byte) error {
}
type Config struct {
Agents AgentsConfig `json:"agents"`
Bindings []AgentBinding `json:"bindings,omitempty"`
Session SessionConfig `json:"session,omitempty"`
Channels ChannelsConfig `json:"channels"`
Providers ProvidersConfig `json:"providers,omitempty"`
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
Agents AgentsConfig `json:"agents"`
Bindings []AgentBinding `json:"bindings,omitempty"`
Session SessionConfig `json:"session,omitempty"`
Channels ChannelsConfig `json:"channels"`
Providers ProvidersConfig `json:"providers,omitempty"`
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"`
}
// MarshalJSON implements custom JSON marshaling for Config
@@ -350,7 +350,7 @@ type OpenAIProviderConfig struct {
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")
Model string `json:"model"` // Protocol/model-identifier (e.g., "openai/gpt-4o", "anthropic/claude-sonnet-4")
// HTTP-based providers
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 {
cfg.mu.RLock()
defer cfg.mu.RUnlock()
data, err := json.MarshalIndent(cfg, "", " ")
if err != nil {
return err
@@ -471,14 +468,10 @@ func SaveConfig(path string, cfg *Config) error {
}
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
}
@@ -510,8 +503,6 @@ func (c *Config) GetAPIKey() string {
}
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
@@ -544,54 +535,22 @@ func expandHome(path string) string {
// 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.
// Uses double-check locking for optimal read performance.
func (c *Config) GetModelConfig(modelName string) (*ModelConfig, error) {
// First pass: use read lock to find matches
c.mu.RLock()
matches := c.findMatchesLocked(modelName)
matches := c.findMatches(modelName)
if len(matches) == 0 {
c.mu.RUnlock()
return nil, fmt.Errorf("model %q not found in model_list or providers", modelName)
}
if len(matches) == 1 {
c.mu.RUnlock()
return &matches[0], nil
}
// Multiple configs - check if counter exists
counter, ok := c.rrCounters[modelName]
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))
// Multiple configs - use round-robin for load balancing
idx := rrCounter.Add(1) % uint64(len(matches))
return &matches[idx], nil
}
// findMatchesLocked finds all ModelConfig entries with the given model_name.
// Must be called with c.mu locked (read or write).
func (c *Config) findMatchesLocked(modelName string) []ModelConfig {
// findMatches finds all ModelConfig entries with the given model_name.
func (c *Config) findMatches(modelName string) []ModelConfig {
var matches []ModelConfig
for i := range c.ModelList {
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.
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 != "" ||
+6
View File
@@ -160,6 +160,12 @@ func DefaultConfig() *Config {
},
// 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",
Model: "openrouter/openai/gpt-5.2",
+14 -3
View File
@@ -10,6 +10,17 @@ import (
"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.
type providerMigrationConfig struct {
// providerNames are the possible names used in agents.defaults.provider
@@ -67,7 +78,7 @@ func ConvertProvidersToModelList(cfg *Config) []ModelConfig {
}
return ModelConfig{
ModelName: "anthropic",
Model: "anthropic/claude-3-sonnet",
Model: "anthropic/claude-sonnet-4",
APIKey: p.Anthropic.APIKey,
APIBase: p.Anthropic.APIBase,
Proxy: p.Anthropic.Proxy,
@@ -325,13 +336,13 @@ func ConvertProvidersToModelList(cfg *Config) []ModelConfig {
// Check if this is the user's configured provider
if slices.Contains(m.providerNames, userProvider) && userModel != "" {
// 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 {
// Legacy config: no explicit provider field but model is specified
// 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
mc.ModelName = userModel
mc.Model = m.protocol + "/" + userModel
mc.Model = buildModelWithProtocol(m.protocol, userModel)
legacyModelNameApplied = true
}
+59 -5
View File
@@ -58,8 +58,8 @@ func TestConvertProvidersToModelList_Anthropic(t *testing.T) {
if result[0].ModelName != "anthropic" {
t.Errorf("ModelName = %q, want %q", result[0].ModelName, "anthropic")
}
if result[0].Model != "anthropic/claude-3-sonnet" {
t.Errorf("Model = %q, want %q", 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-sonnet-4")
}
}
@@ -239,7 +239,7 @@ func TestConvertProvidersToModelList_PreservesUserModel_Anthropic(t *testing.T)
Agents: AgentsConfig{
Defaults: AgentDefaults{
Provider: "claude", // alternative name
Model: "claude-3-opus-20240229",
Model: "claude-opus-4-20250514",
},
},
Providers: ProvidersConfig{
@@ -253,8 +253,8 @@ func TestConvertProvidersToModelList_PreservesUserModel_Anthropic(t *testing.T)
t.Fatalf("len(result) = %d, want 1", len(result))
}
if result[0].Model != "anthropic/claude-3-opus-20240229" {
t.Errorf("Model = %q, want %q", 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-opus-4-20250514")
}
}
@@ -495,3 +495,57 @@ func TestConvertProvidersToModelList_NoProviderField_NoModel(t *testing.T) {
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")
}
}
+9 -2
View File
@@ -196,8 +196,15 @@ func TestResolveProviderSelection(t *testing.T) {
func TestCreateProviderReturnsHTTPProviderForOpenRouter(t *testing.T) {
cfg := config.DefaultConfig()
cfg.Agents.Defaults.Model = "openrouter/auto"
cfg.Providers.OpenRouter.APIKey = "sk-or-test"
cfg.Agents.Defaults.Model = "test-openrouter"
cfg.ModelList = []config.ModelConfig{
{
ModelName: "test-openrouter",
Model: "openrouter/auto",
APIKey: "sk-or-test",
APIBase: "https://openrouter.ai/api/v1",
},
}
provider, _, err := CreateProvider(cfg)
if err != nil {