mirror of
https://github.com/sipeed/picoclaw.git
synced 2026-06-12 18:08:54 +00:00
Merge branch 'upstream-main' into feat/subturn-poc
This commit is contained in:
@@ -221,6 +221,10 @@ func buildRequestBody(
|
||||
|
||||
// Add tool_use blocks
|
||||
for _, tc := range msg.ToolCalls {
|
||||
if strings.TrimSpace(tc.Name) == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Handle nil Arguments (GLM-4 may return null input)
|
||||
input := tc.Arguments
|
||||
if input == nil {
|
||||
|
||||
@@ -492,6 +492,20 @@ func TestBuildRequestBodyEdgeCases(t *testing.T) {
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "skip tool calls with empty names",
|
||||
messages: []Message{
|
||||
{Role: "assistant", Content: "Calling tool", ToolCalls: []ToolCall{
|
||||
{ID: "tool-empty", Name: "", Arguments: map[string]any{"ignored": true}},
|
||||
{ID: "tool-valid", Name: "test_tool", Arguments: map[string]any{"arg": "value"}},
|
||||
}},
|
||||
},
|
||||
model: "test-model",
|
||||
options: map[string]any{
|
||||
"max_tokens": 8192,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
@@ -513,6 +527,37 @@ func TestBuildRequestBodyEdgeCases(t *testing.T) {
|
||||
if got["model"] != tt.model {
|
||||
t.Errorf("model = %v, want %v", got["model"], tt.model)
|
||||
}
|
||||
|
||||
if tt.name == "skip tool calls with empty names" {
|
||||
messages, ok := got["messages"].([]any)
|
||||
if !ok || len(messages) != 1 {
|
||||
t.Fatalf("messages = %#v, want single assistant message", got["messages"])
|
||||
}
|
||||
|
||||
assistantMsg, ok := messages[0].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("assistant message = %#v, want map", messages[0])
|
||||
}
|
||||
|
||||
content, ok := assistantMsg["content"].([]any)
|
||||
if !ok {
|
||||
t.Fatalf("assistant content = %#v, want []any", assistantMsg["content"])
|
||||
}
|
||||
if len(content) != 2 {
|
||||
t.Fatalf("assistant content length = %d, want 2", len(content))
|
||||
}
|
||||
|
||||
toolUse, ok := content[1].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("tool_use block = %#v, want map", content[1])
|
||||
}
|
||||
if gotName := toolUse["name"]; gotName != "test_tool" {
|
||||
t.Fatalf("tool_use name = %v, want %q", gotName, "test_tool")
|
||||
}
|
||||
if gotID := toolUse["id"]; gotID != "tool-valid" {
|
||||
t.Fatalf("tool_use id = %v, want %q", gotID, "tool-valid")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,8 +115,9 @@ func CreateProviderFromConfig(cfg *config.ModelConfig) (LLMProvider, string, err
|
||||
|
||||
case "litellm", "openrouter", "groq", "zhipu", "gemini", "nvidia",
|
||||
"ollama", "moonshot", "shengsuanyun", "deepseek", "cerebras",
|
||||
"vivgrid", "volcengine", "vllm", "qwen", "mistral", "avian",
|
||||
"minimax", "longcat", "modelscope", "novita":
|
||||
"vivgrid", "volcengine", "vllm", "qwen", "qwen-intl", "qwen-international", "dashscope-intl",
|
||||
"qwen-us", "dashscope-us", "mistral", "avian", "minimax", "longcat", "modelscope", "novita",
|
||||
"coding-plan", "alibaba-coding", "qwen-coding":
|
||||
// All other OpenAI-compatible HTTP providers
|
||||
if cfg.APIKey == "" && cfg.APIBase == "" {
|
||||
return nil, "", fmt.Errorf("api_key or api_base is required for HTTP-based protocol %q", protocol)
|
||||
@@ -173,6 +174,21 @@ func CreateProviderFromConfig(cfg *config.ModelConfig) (LLMProvider, string, err
|
||||
cfg.RequestTimeout,
|
||||
), modelID, nil
|
||||
|
||||
case "coding-plan-anthropic", "alibaba-coding-anthropic":
|
||||
// Alibaba Coding Plan with Anthropic-compatible API
|
||||
apiBase := cfg.APIBase
|
||||
if apiBase == "" {
|
||||
apiBase = getDefaultAPIBase(protocol)
|
||||
}
|
||||
if cfg.APIKey == "" {
|
||||
return nil, "", fmt.Errorf("api_key is required for %q protocol (model: %s)", protocol, cfg.Model)
|
||||
}
|
||||
return anthropicmessages.NewProviderWithTimeout(
|
||||
cfg.APIKey,
|
||||
apiBase,
|
||||
cfg.RequestTimeout,
|
||||
), modelID, nil
|
||||
|
||||
case "antigravity":
|
||||
return NewAntigravityProvider(), modelID, nil
|
||||
|
||||
@@ -245,6 +261,14 @@ func getDefaultAPIBase(protocol string) string {
|
||||
return "https://ark.cn-beijing.volces.com/api/v3"
|
||||
case "qwen":
|
||||
return "https://dashscope.aliyuncs.com/compatible-mode/v1"
|
||||
case "qwen-intl", "qwen-international", "dashscope-intl":
|
||||
return "https://dashscope-intl.aliyuncs.com/compatible-mode/v1"
|
||||
case "qwen-us", "dashscope-us":
|
||||
return "https://dashscope-us.aliyuncs.com/compatible-mode/v1"
|
||||
case "coding-plan", "alibaba-coding", "qwen-coding":
|
||||
return "https://coding-intl.dashscope.aliyuncs.com/v1"
|
||||
case "coding-plan-anthropic", "alibaba-coding-anthropic":
|
||||
return "https://coding-intl.dashscope.aliyuncs.com/apps/anthropic"
|
||||
case "vllm":
|
||||
return "http://localhost:8000/v1"
|
||||
case "mistral":
|
||||
|
||||
@@ -472,3 +472,134 @@ func TestCreateProviderFromConfig_AzureMissingAPIBase(t *testing.T) {
|
||||
t.Fatal("CreateProviderFromConfig() expected error for missing API base")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateProviderFromConfig_QwenInternationalAlias(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
protocol string
|
||||
}{
|
||||
{"qwen-international", "qwen-international"},
|
||||
{"dashscope-intl", "dashscope-intl"},
|
||||
{"qwen-intl", "qwen-intl"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cfg := &config.ModelConfig{
|
||||
ModelName: "test-" + tt.protocol,
|
||||
Model: tt.protocol + "/qwen-max",
|
||||
APIKey: "test-key",
|
||||
}
|
||||
|
||||
provider, modelID, err := CreateProviderFromConfig(cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateProviderFromConfig() error = %v", err)
|
||||
}
|
||||
if provider == nil {
|
||||
t.Fatal("CreateProviderFromConfig() returned nil provider")
|
||||
}
|
||||
if modelID != "qwen-max" {
|
||||
t.Errorf("modelID = %q, want %q", modelID, "qwen-max")
|
||||
}
|
||||
if _, ok := provider.(*HTTPProvider); !ok {
|
||||
t.Fatalf("expected *HTTPProvider, got %T", provider)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateProviderFromConfig_QwenUSAlias(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
protocol string
|
||||
}{
|
||||
{"qwen-us", "qwen-us"},
|
||||
{"dashscope-us", "dashscope-us"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cfg := &config.ModelConfig{
|
||||
ModelName: "test-" + tt.protocol,
|
||||
Model: tt.protocol + "/qwen-max",
|
||||
APIKey: "test-key",
|
||||
}
|
||||
|
||||
provider, modelID, err := CreateProviderFromConfig(cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateProviderFromConfig() error = %v", err)
|
||||
}
|
||||
if provider == nil {
|
||||
t.Fatal("CreateProviderFromConfig() returned nil provider")
|
||||
}
|
||||
if modelID != "qwen-max" {
|
||||
t.Errorf("modelID = %q, want %q", modelID, "qwen-max")
|
||||
}
|
||||
if _, ok := provider.(*HTTPProvider); !ok {
|
||||
t.Fatalf("expected *HTTPProvider, got %T", provider)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateProviderFromConfig_CodingPlanAnthropic(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
protocol string
|
||||
}{
|
||||
{"coding-plan-anthropic", "coding-plan-anthropic"},
|
||||
{"alibaba-coding-anthropic", "alibaba-coding-anthropic"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cfg := &config.ModelConfig{
|
||||
ModelName: "test-" + tt.protocol,
|
||||
Model: tt.protocol + "/claude-sonnet-4-20250514",
|
||||
APIKey: "test-key",
|
||||
}
|
||||
|
||||
provider, modelID, err := CreateProviderFromConfig(cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateProviderFromConfig() error = %v", err)
|
||||
}
|
||||
if provider == nil {
|
||||
t.Fatal("CreateProviderFromConfig() returned nil provider")
|
||||
}
|
||||
if modelID != "claude-sonnet-4-20250514" {
|
||||
t.Errorf("modelID = %q, want %q", modelID, "claude-sonnet-4-20250514")
|
||||
}
|
||||
// coding-plan-anthropic uses Anthropic Messages provider
|
||||
// Verify it's the anthropic messages provider by checking interface
|
||||
var _ LLMProvider = provider
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetDefaultAPIBase_CodingPlanAnthropic(t *testing.T) {
|
||||
expectedURL := "https://coding-intl.dashscope.aliyuncs.com/apps/anthropic"
|
||||
if got := getDefaultAPIBase("coding-plan-anthropic"); got != expectedURL {
|
||||
t.Fatalf("getDefaultAPIBase(%q) = %q, want %q", "coding-plan-anthropic", got, expectedURL)
|
||||
}
|
||||
if got := getDefaultAPIBase("alibaba-coding-anthropic"); got != expectedURL {
|
||||
t.Fatalf("getDefaultAPIBase(%q) = %q, want %q", "alibaba-coding-anthropic", got, expectedURL)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetDefaultAPIBase_QwenIntlAliases(t *testing.T) {
|
||||
expectedURL := "https://dashscope-intl.aliyuncs.com/compatible-mode/v1"
|
||||
for _, protocol := range []string{"qwen-intl", "qwen-international", "dashscope-intl"} {
|
||||
if got := getDefaultAPIBase(protocol); got != expectedURL {
|
||||
t.Fatalf("getDefaultAPIBase(%q) = %q, want %q", protocol, got, expectedURL)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetDefaultAPIBase_QwenUSAliases(t *testing.T) {
|
||||
expectedURL := "https://dashscope-us.aliyuncs.com/compatible-mode/v1"
|
||||
for _, protocol := range []string{"qwen-us", "dashscope-us"} {
|
||||
if got := getDefaultAPIBase(protocol); got != expectedURL {
|
||||
t.Fatalf("getDefaultAPIBase(%q) = %q, want %q", protocol, got, expectedURL)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +53,14 @@ func NormalizeProvider(provider string) string {
|
||||
return "zhipu"
|
||||
case "google":
|
||||
return "gemini"
|
||||
case "alibaba-coding", "qwen-coding":
|
||||
return "coding-plan"
|
||||
case "alibaba-coding-anthropic":
|
||||
return "coding-plan-anthropic"
|
||||
case "qwen-international", "dashscope-intl":
|
||||
return "qwen-intl"
|
||||
case "dashscope-us":
|
||||
return "qwen-us"
|
||||
}
|
||||
|
||||
return p
|
||||
|
||||
@@ -73,6 +73,14 @@ func TestNormalizeProvider(t *testing.T) {
|
||||
{"glm", "zhipu"},
|
||||
{"google", "gemini"},
|
||||
{"groq", "groq"},
|
||||
// Alibaba Coding Plan aliases
|
||||
{"alibaba-coding", "coding-plan"},
|
||||
{"qwen-coding", "coding-plan"},
|
||||
{"alibaba-coding-anthropic", "coding-plan-anthropic"},
|
||||
// Qwen international aliases
|
||||
{"qwen-international", "qwen-intl"},
|
||||
{"dashscope-intl", "qwen-intl"},
|
||||
{"dashscope-us", "qwen-us"},
|
||||
{"", ""},
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user