mirror of
https://github.com/sipeed/picoclaw.git
synced 2026-06-12 18:08:54 +00:00
ec86b21d3f
- Preserve user's configured model during config migration (issue #5) - Simplify ExtractProtocol using strings.Cut - Extract NormalizeToolCall to shared utility, removing ~70 lines of duplicate code - Clean up unused fields in providerMigrationConfig struct Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
394 lines
9.9 KiB
Go
394 lines
9.9 KiB
Go
// PicoClaw - Ultra-lightweight personal AI agent
|
|
// License: MIT
|
|
//
|
|
// Copyright (c) 2026 PicoClaw contributors
|
|
|
|
package config
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestConvertProvidersToModelList_OpenAI(t *testing.T) {
|
|
cfg := &Config{
|
|
Providers: ProvidersConfig{
|
|
OpenAI: ProviderConfig{
|
|
APIKey: "sk-test-key",
|
|
APIBase: "https://custom.api.com/v1",
|
|
},
|
|
},
|
|
}
|
|
|
|
result := ConvertProvidersToModelList(cfg)
|
|
|
|
if len(result) != 1 {
|
|
t.Fatalf("len(result) = %d, want 1", len(result))
|
|
}
|
|
|
|
if result[0].ModelName != "openai" {
|
|
t.Errorf("ModelName = %q, want %q", result[0].ModelName, "openai")
|
|
}
|
|
if result[0].Model != "openai/gpt-4o" {
|
|
t.Errorf("Model = %q, want %q", result[0].Model, "openai/gpt-4o")
|
|
}
|
|
if result[0].APIKey != "sk-test-key" {
|
|
t.Errorf("APIKey = %q, want %q", result[0].APIKey, "sk-test-key")
|
|
}
|
|
}
|
|
|
|
func TestConvertProvidersToModelList_Anthropic(t *testing.T) {
|
|
cfg := &Config{
|
|
Providers: ProvidersConfig{
|
|
Anthropic: ProviderConfig{
|
|
APIKey: "ant-key",
|
|
APIBase: "https://custom.anthropic.com",
|
|
},
|
|
},
|
|
}
|
|
|
|
result := ConvertProvidersToModelList(cfg)
|
|
|
|
if len(result) != 1 {
|
|
t.Fatalf("len(result) = %d, want 1", len(result))
|
|
}
|
|
|
|
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")
|
|
}
|
|
}
|
|
|
|
func TestConvertProvidersToModelList_Multiple(t *testing.T) {
|
|
cfg := &Config{
|
|
Providers: ProvidersConfig{
|
|
OpenAI: ProviderConfig{APIKey: "openai-key"},
|
|
Groq: ProviderConfig{APIKey: "groq-key"},
|
|
Zhipu: ProviderConfig{APIKey: "zhipu-key"},
|
|
},
|
|
}
|
|
|
|
result := ConvertProvidersToModelList(cfg)
|
|
|
|
if len(result) != 3 {
|
|
t.Fatalf("len(result) = %d, want 3", len(result))
|
|
}
|
|
|
|
// Check that all providers are present
|
|
found := make(map[string]bool)
|
|
for _, mc := range result {
|
|
found[mc.ModelName] = true
|
|
}
|
|
|
|
for _, name := range []string{"openai", "groq", "zhipu"} {
|
|
if !found[name] {
|
|
t.Errorf("Missing provider %q in result", name)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestConvertProvidersToModelList_Empty(t *testing.T) {
|
|
cfg := &Config{
|
|
Providers: ProvidersConfig{},
|
|
}
|
|
|
|
result := ConvertProvidersToModelList(cfg)
|
|
|
|
if len(result) != 0 {
|
|
t.Errorf("len(result) = %d, want 0", len(result))
|
|
}
|
|
}
|
|
|
|
func TestConvertProvidersToModelList_Nil(t *testing.T) {
|
|
result := ConvertProvidersToModelList(nil)
|
|
|
|
if result != nil {
|
|
t.Errorf("result = %v, want nil", result)
|
|
}
|
|
}
|
|
|
|
func TestConvertProvidersToModelList_AllProviders(t *testing.T) {
|
|
cfg := &Config{
|
|
Providers: ProvidersConfig{
|
|
OpenAI: ProviderConfig{APIKey: "key1"},
|
|
Anthropic: ProviderConfig{APIKey: "key2"},
|
|
OpenRouter: ProviderConfig{APIKey: "key3"},
|
|
Groq: ProviderConfig{APIKey: "key4"},
|
|
Zhipu: ProviderConfig{APIKey: "key5"},
|
|
VLLM: ProviderConfig{APIKey: "key6"},
|
|
Gemini: ProviderConfig{APIKey: "key7"},
|
|
Nvidia: ProviderConfig{APIKey: "key8"},
|
|
Ollama: ProviderConfig{APIKey: "key9"},
|
|
Moonshot: ProviderConfig{APIKey: "key10"},
|
|
ShengSuanYun: ProviderConfig{APIKey: "key11"},
|
|
DeepSeek: ProviderConfig{APIKey: "key12"},
|
|
Cerebras: ProviderConfig{APIKey: "key13"},
|
|
VolcEngine: ProviderConfig{APIKey: "key14"},
|
|
GitHubCopilot: ProviderConfig{ConnectMode: "grpc"},
|
|
Antigravity: ProviderConfig{AuthMethod: "oauth"},
|
|
Qwen: ProviderConfig{APIKey: "key17"},
|
|
},
|
|
}
|
|
|
|
result := ConvertProvidersToModelList(cfg)
|
|
|
|
// All 17 providers should be converted
|
|
if len(result) != 17 {
|
|
t.Errorf("len(result) = %d, want 17", len(result))
|
|
}
|
|
}
|
|
|
|
func TestConvertProvidersToModelList_Proxy(t *testing.T) {
|
|
cfg := &Config{
|
|
Providers: ProvidersConfig{
|
|
OpenAI: ProviderConfig{
|
|
APIKey: "key",
|
|
Proxy: "http://proxy:8080",
|
|
},
|
|
},
|
|
}
|
|
|
|
result := ConvertProvidersToModelList(cfg)
|
|
|
|
if len(result) != 1 {
|
|
t.Fatalf("len(result) = %d, want 1", len(result))
|
|
}
|
|
|
|
if result[0].Proxy != "http://proxy:8080" {
|
|
t.Errorf("Proxy = %q, want %q", result[0].Proxy, "http://proxy:8080")
|
|
}
|
|
}
|
|
|
|
func TestConvertProvidersToModelList_AuthMethod(t *testing.T) {
|
|
cfg := &Config{
|
|
Providers: ProvidersConfig{
|
|
OpenAI: ProviderConfig{
|
|
AuthMethod: "oauth",
|
|
},
|
|
},
|
|
}
|
|
|
|
result := ConvertProvidersToModelList(cfg)
|
|
|
|
if len(result) != 0 {
|
|
t.Errorf("len(result) = %d, want 0 (AuthMethod alone should not create entry)", len(result))
|
|
}
|
|
}
|
|
|
|
// Tests for preserving user's configured model during migration
|
|
|
|
func TestConvertProvidersToModelList_PreservesUserModel_DeepSeek(t *testing.T) {
|
|
cfg := &Config{
|
|
Agents: AgentsConfig{
|
|
Defaults: AgentDefaults{
|
|
Provider: "deepseek",
|
|
Model: "deepseek-reasoner",
|
|
},
|
|
},
|
|
Providers: ProvidersConfig{
|
|
DeepSeek: ProviderConfig{APIKey: "sk-deepseek"},
|
|
},
|
|
}
|
|
|
|
result := ConvertProvidersToModelList(cfg)
|
|
|
|
if len(result) != 1 {
|
|
t.Fatalf("len(result) = %d, want 1", len(result))
|
|
}
|
|
|
|
// Should use user's model, not default
|
|
if result[0].Model != "openai/deepseek-reasoner" {
|
|
t.Errorf("Model = %q, want %q (user's configured model)", result[0].Model, "openai/deepseek-reasoner")
|
|
}
|
|
}
|
|
|
|
func TestConvertProvidersToModelList_PreservesUserModel_OpenAI(t *testing.T) {
|
|
cfg := &Config{
|
|
Agents: AgentsConfig{
|
|
Defaults: AgentDefaults{
|
|
Provider: "openai",
|
|
Model: "gpt-4-turbo",
|
|
},
|
|
},
|
|
Providers: ProvidersConfig{
|
|
OpenAI: ProviderConfig{APIKey: "sk-openai"},
|
|
},
|
|
}
|
|
|
|
result := ConvertProvidersToModelList(cfg)
|
|
|
|
if len(result) != 1 {
|
|
t.Fatalf("len(result) = %d, want 1", len(result))
|
|
}
|
|
|
|
if result[0].Model != "openai/gpt-4-turbo" {
|
|
t.Errorf("Model = %q, want %q", result[0].Model, "openai/gpt-4-turbo")
|
|
}
|
|
}
|
|
|
|
func TestConvertProvidersToModelList_PreservesUserModel_Anthropic(t *testing.T) {
|
|
cfg := &Config{
|
|
Agents: AgentsConfig{
|
|
Defaults: AgentDefaults{
|
|
Provider: "claude", // alternative name
|
|
Model: "claude-3-opus-20240229",
|
|
},
|
|
},
|
|
Providers: ProvidersConfig{
|
|
Anthropic: ProviderConfig{APIKey: "sk-ant"},
|
|
},
|
|
}
|
|
|
|
result := ConvertProvidersToModelList(cfg)
|
|
|
|
if len(result) != 1 {
|
|
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")
|
|
}
|
|
}
|
|
|
|
func TestConvertProvidersToModelList_PreservesUserModel_Qwen(t *testing.T) {
|
|
cfg := &Config{
|
|
Agents: AgentsConfig{
|
|
Defaults: AgentDefaults{
|
|
Provider: "qwen",
|
|
Model: "qwen-plus",
|
|
},
|
|
},
|
|
Providers: ProvidersConfig{
|
|
Qwen: ProviderConfig{APIKey: "sk-qwen"},
|
|
},
|
|
}
|
|
|
|
result := ConvertProvidersToModelList(cfg)
|
|
|
|
if len(result) != 1 {
|
|
t.Fatalf("len(result) = %d, want 1", len(result))
|
|
}
|
|
|
|
if result[0].Model != "qwen/qwen-plus" {
|
|
t.Errorf("Model = %q, want %q", result[0].Model, "qwen/qwen-plus")
|
|
}
|
|
}
|
|
|
|
func TestConvertProvidersToModelList_UsesDefaultWhenNoUserModel(t *testing.T) {
|
|
cfg := &Config{
|
|
Agents: AgentsConfig{
|
|
Defaults: AgentDefaults{
|
|
Provider: "deepseek",
|
|
Model: "", // no model specified
|
|
},
|
|
},
|
|
Providers: ProvidersConfig{
|
|
DeepSeek: ProviderConfig{APIKey: "sk-deepseek"},
|
|
},
|
|
}
|
|
|
|
result := ConvertProvidersToModelList(cfg)
|
|
|
|
if len(result) != 1 {
|
|
t.Fatalf("len(result) = %d, want 1", len(result))
|
|
}
|
|
|
|
// Should use default model
|
|
if result[0].Model != "openai/deepseek-chat" {
|
|
t.Errorf("Model = %q, want %q (default)", result[0].Model, "openai/deepseek-chat")
|
|
}
|
|
}
|
|
|
|
func TestConvertProvidersToModelList_MultipleProviders_PreservesUserModel(t *testing.T) {
|
|
cfg := &Config{
|
|
Agents: AgentsConfig{
|
|
Defaults: AgentDefaults{
|
|
Provider: "deepseek",
|
|
Model: "deepseek-reasoner",
|
|
},
|
|
},
|
|
Providers: ProvidersConfig{
|
|
OpenAI: ProviderConfig{APIKey: "sk-openai"},
|
|
DeepSeek: ProviderConfig{APIKey: "sk-deepseek"},
|
|
},
|
|
}
|
|
|
|
result := ConvertProvidersToModelList(cfg)
|
|
|
|
if len(result) != 2 {
|
|
t.Fatalf("len(result) = %d, want 2", len(result))
|
|
}
|
|
|
|
// Find each provider and verify model
|
|
for _, mc := range result {
|
|
switch mc.ModelName {
|
|
case "openai":
|
|
if mc.Model != "openai/gpt-4o" {
|
|
t.Errorf("OpenAI Model = %q, want %q (default)", mc.Model, "openai/gpt-4o")
|
|
}
|
|
case "deepseek":
|
|
if mc.Model != "openai/deepseek-reasoner" {
|
|
t.Errorf("DeepSeek Model = %q, want %q (user's)", mc.Model, "openai/deepseek-reasoner")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestConvertProvidersToModelList_ProviderNameAliases(t *testing.T) {
|
|
tests := []struct {
|
|
providerAlias string
|
|
expectedModel string
|
|
provider ProviderConfig
|
|
}{
|
|
{"gpt", "openai/gpt-4-custom", ProviderConfig{APIKey: "key"}},
|
|
{"claude", "anthropic/claude-custom", ProviderConfig{APIKey: "key"}},
|
|
{"doubao", "openai/doubao-custom", ProviderConfig{APIKey: "key"}},
|
|
{"tongyi", "qwen/qwen-custom", ProviderConfig{APIKey: "key"}},
|
|
{"kimi", "moonshot/kimi-custom", ProviderConfig{APIKey: "key"}},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.providerAlias, func(t *testing.T) {
|
|
cfg := &Config{
|
|
Agents: AgentsConfig{
|
|
Defaults: AgentDefaults{
|
|
Provider: tt.providerAlias,
|
|
Model: strings.TrimPrefix(tt.expectedModel, tt.expectedModel[:strings.Index(tt.expectedModel, "/")+1]),
|
|
},
|
|
},
|
|
Providers: ProvidersConfig{},
|
|
}
|
|
|
|
// Set the appropriate provider config
|
|
switch tt.providerAlias {
|
|
case "gpt":
|
|
cfg.Providers.OpenAI = tt.provider
|
|
case "claude":
|
|
cfg.Providers.Anthropic = tt.provider
|
|
case "doubao":
|
|
cfg.Providers.VolcEngine = tt.provider
|
|
case "tongyi":
|
|
cfg.Providers.Qwen = tt.provider
|
|
case "kimi":
|
|
cfg.Providers.Moonshot = tt.provider
|
|
}
|
|
|
|
// Need to fix the model name in config
|
|
cfg.Agents.Defaults.Model = strings.TrimPrefix(tt.expectedModel, tt.expectedModel[:strings.Index(tt.expectedModel, "/")+1])
|
|
|
|
result := ConvertProvidersToModelList(cfg)
|
|
if len(result) != 1 {
|
|
t.Fatalf("len(result) = %d, want 1", len(result))
|
|
}
|
|
|
|
// Extract just the model ID part (after the first /)
|
|
expectedModelID := tt.expectedModel
|
|
if result[0].Model != expectedModelID {
|
|
t.Errorf("Model = %q, want %q", result[0].Model, expectedModelID)
|
|
}
|
|
})
|
|
}
|
|
}
|