mirror of
https://github.com/sipeed/picoclaw.git
synced 2026-06-12 18:08:54 +00:00
9a3ca8e54d
* feat(provider): add Alibaba Coding Plan and regional Qwen endpoints - Add Alibaba Coding Plan provider with OpenAI-compatible endpoint (https://coding-intl.dashscope.aliyuncs.com/v1) - Add Coding Plan Anthropic-compatible endpoint (https://coding-intl.dashscope.aliyuncs.com/apps/anthropic) - Add regional Qwen endpoints (qwen-intl, qwen-us) - Add provider aliases: coding-plan, alibaba-coding, qwen-coding - Normalize provider names for coding-plan variants 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * fix(provider): add reviewer-requested fixes for Alibaba Coding Plan - Add qwen-international, dashscope-intl, dashscope-us aliases to switch case - Add coding-plan-anthropic case with anthropicmessages.NewProviderWithTimeout - Add alibaba-coding-anthropic -> coding-plan-anthropic normalization - Add qwen-international -> qwen-intl and dashscope-us -> qwen-us normalization 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * test(provider): add tests for Alibaba Coding Plan protocol aliases - Add tests for qwen-international, dashscope-intl, dashscope-us aliases - Add tests for coding-plan-anthropic and alibaba-coding-anthropic - Add getDefaultAPIBase tests for all new aliases - Add normalization tests for new provider aliases 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
288 lines
8.8 KiB
Go
288 lines
8.8 KiB
Go
// PicoClaw - Ultra-lightweight personal AI agent
|
|
// License: MIT
|
|
//
|
|
// Copyright (c) 2026 PicoClaw contributors
|
|
|
|
package providers
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/sipeed/picoclaw/pkg/config"
|
|
anthropicmessages "github.com/sipeed/picoclaw/pkg/providers/anthropic_messages"
|
|
"github.com/sipeed/picoclaw/pkg/providers/azure"
|
|
)
|
|
|
|
// createClaudeAuthProvider creates a Claude provider using OAuth credentials from auth store.
|
|
func createClaudeAuthProvider() (LLMProvider, error) {
|
|
cred, err := getCredential("anthropic")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("loading auth credentials: %w", err)
|
|
}
|
|
if cred == nil {
|
|
return nil, fmt.Errorf("no credentials for anthropic. Run: picoclaw auth login --provider anthropic")
|
|
}
|
|
return NewClaudeProviderWithTokenSource(cred.AccessToken, createClaudeTokenSource()), nil
|
|
}
|
|
|
|
// createCodexAuthProvider creates a Codex provider using OAuth credentials from auth store.
|
|
func createCodexAuthProvider() (LLMProvider, error) {
|
|
cred, err := getCredential("openai")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("loading auth credentials: %w", err)
|
|
}
|
|
if cred == nil {
|
|
return nil, fmt.Errorf("no credentials for openai. Run: picoclaw auth login --provider openai")
|
|
}
|
|
return NewCodexProviderWithTokenSource(cred.AccessToken, cred.AccountID, createCodexTokenSource()), nil
|
|
}
|
|
|
|
// ExtractProtocol extracts the protocol prefix and model identifier from a model string.
|
|
// If no prefix is specified, it defaults to "openai".
|
|
// Examples:
|
|
// - "openai/gpt-4o" -> ("openai", "gpt-4o")
|
|
// - "anthropic/claude-sonnet-4.6" -> ("anthropic", "claude-sonnet-4.6")
|
|
// - "gpt-4o" -> ("openai", "gpt-4o") // default protocol
|
|
func ExtractProtocol(model string) (protocol, modelID string) {
|
|
model = strings.TrimSpace(model)
|
|
protocol, modelID, found := strings.Cut(model, "/")
|
|
if !found {
|
|
return "openai", model
|
|
}
|
|
return protocol, modelID
|
|
}
|
|
|
|
// CreateProviderFromConfig creates a provider based on the ModelConfig.
|
|
// It uses the protocol prefix in the Model field to determine which provider to create.
|
|
// Supported protocols: openai, litellm, novita, anthropic, anthropic-messages,
|
|
// antigravity, claude-cli, codex-cli, github-copilot
|
|
// Returns the provider, the model ID (without protocol prefix), and any error.
|
|
func CreateProviderFromConfig(cfg *config.ModelConfig) (LLMProvider, string, error) {
|
|
if cfg == nil {
|
|
return nil, "", fmt.Errorf("config is nil")
|
|
}
|
|
|
|
if cfg.Model == "" {
|
|
return nil, "", fmt.Errorf("model is required")
|
|
}
|
|
|
|
protocol, modelID := ExtractProtocol(cfg.Model)
|
|
|
|
switch protocol {
|
|
case "openai":
|
|
// OpenAI with OAuth/token auth (Codex-style)
|
|
if cfg.AuthMethod == "oauth" || cfg.AuthMethod == "token" {
|
|
provider, err := createCodexAuthProvider()
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
return provider, modelID, nil
|
|
}
|
|
// OpenAI with API key
|
|
if cfg.APIKey == "" && cfg.APIBase == "" {
|
|
return nil, "", fmt.Errorf("api_key or api_base is required for HTTP-based protocol %q", protocol)
|
|
}
|
|
apiBase := cfg.APIBase
|
|
if apiBase == "" {
|
|
apiBase = getDefaultAPIBase(protocol)
|
|
}
|
|
return NewHTTPProviderWithMaxTokensFieldAndRequestTimeout(
|
|
cfg.APIKey,
|
|
apiBase,
|
|
cfg.Proxy,
|
|
cfg.MaxTokensField,
|
|
cfg.RequestTimeout,
|
|
), modelID, nil
|
|
|
|
case "azure", "azure-openai":
|
|
// Azure OpenAI uses deployment-based URLs, api-key header auth,
|
|
// and always sends max_completion_tokens.
|
|
if cfg.APIKey == "" {
|
|
return nil, "", fmt.Errorf("api_key is required for azure protocol")
|
|
}
|
|
if cfg.APIBase == "" {
|
|
return nil, "", fmt.Errorf(
|
|
"api_base is required for azure protocol (e.g., https://your-resource.openai.azure.com)",
|
|
)
|
|
}
|
|
return azure.NewProviderWithTimeout(
|
|
cfg.APIKey,
|
|
cfg.APIBase,
|
|
cfg.Proxy,
|
|
cfg.RequestTimeout,
|
|
), modelID, nil
|
|
|
|
case "litellm", "openrouter", "groq", "zhipu", "gemini", "nvidia",
|
|
"ollama", "moonshot", "shengsuanyun", "deepseek", "cerebras",
|
|
"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)
|
|
}
|
|
apiBase := cfg.APIBase
|
|
if apiBase == "" {
|
|
apiBase = getDefaultAPIBase(protocol)
|
|
}
|
|
return NewHTTPProviderWithMaxTokensFieldAndRequestTimeout(
|
|
cfg.APIKey,
|
|
apiBase,
|
|
cfg.Proxy,
|
|
cfg.MaxTokensField,
|
|
cfg.RequestTimeout,
|
|
), modelID, nil
|
|
|
|
case "anthropic":
|
|
if cfg.AuthMethod == "oauth" || cfg.AuthMethod == "token" {
|
|
// Use OAuth credentials from auth store
|
|
provider, err := createClaudeAuthProvider()
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
return provider, modelID, nil
|
|
}
|
|
// Use API key with HTTP API
|
|
apiBase := cfg.APIBase
|
|
if apiBase == "" {
|
|
apiBase = "https://api.anthropic.com/v1"
|
|
}
|
|
if cfg.APIKey == "" {
|
|
return nil, "", fmt.Errorf("api_key is required for anthropic protocol (model: %s)", cfg.Model)
|
|
}
|
|
return NewHTTPProviderWithMaxTokensFieldAndRequestTimeout(
|
|
cfg.APIKey,
|
|
apiBase,
|
|
cfg.Proxy,
|
|
cfg.MaxTokensField,
|
|
cfg.RequestTimeout,
|
|
), modelID, nil
|
|
|
|
case "anthropic-messages":
|
|
// Anthropic Messages API with native format (HTTP-based, no SDK)
|
|
apiBase := cfg.APIBase
|
|
if apiBase == "" {
|
|
apiBase = "https://api.anthropic.com/v1"
|
|
}
|
|
if cfg.APIKey == "" {
|
|
return nil, "", fmt.Errorf("api_key is required for anthropic-messages protocol (model: %s)", cfg.Model)
|
|
}
|
|
return anthropicmessages.NewProviderWithTimeout(
|
|
cfg.APIKey,
|
|
apiBase,
|
|
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
|
|
|
|
case "claude-cli", "claudecli":
|
|
workspace := cfg.Workspace
|
|
if workspace == "" {
|
|
workspace = "."
|
|
}
|
|
return NewClaudeCliProvider(workspace), modelID, nil
|
|
|
|
case "codex-cli", "codexcli":
|
|
workspace := cfg.Workspace
|
|
if workspace == "" {
|
|
workspace = "."
|
|
}
|
|
return NewCodexCliProvider(workspace), modelID, nil
|
|
|
|
case "github-copilot", "copilot":
|
|
apiBase := cfg.APIBase
|
|
if apiBase == "" {
|
|
apiBase = "localhost:4321"
|
|
}
|
|
connectMode := cfg.ConnectMode
|
|
if connectMode == "" {
|
|
connectMode = "grpc"
|
|
}
|
|
provider, err := NewGitHubCopilotProvider(apiBase, connectMode, modelID)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
return provider, modelID, nil
|
|
|
|
default:
|
|
return nil, "", fmt.Errorf("unknown protocol %q in model %q", protocol, cfg.Model)
|
|
}
|
|
}
|
|
|
|
// getDefaultAPIBase returns the default API base URL for a given protocol.
|
|
func getDefaultAPIBase(protocol string) string {
|
|
switch protocol {
|
|
case "openai":
|
|
return "https://api.openai.com/v1"
|
|
case "openrouter":
|
|
return "https://openrouter.ai/api/v1"
|
|
case "litellm":
|
|
return "http://localhost:4000/v1"
|
|
case "novita":
|
|
return "https://api.novita.ai/openai"
|
|
case "groq":
|
|
return "https://api.groq.com/openai/v1"
|
|
case "zhipu":
|
|
return "https://open.bigmodel.cn/api/paas/v4"
|
|
case "gemini":
|
|
return "https://generativelanguage.googleapis.com/v1beta"
|
|
case "nvidia":
|
|
return "https://integrate.api.nvidia.com/v1"
|
|
case "ollama":
|
|
return "http://localhost:11434/v1"
|
|
case "moonshot":
|
|
return "https://api.moonshot.cn/v1"
|
|
case "shengsuanyun":
|
|
return "https://router.shengsuanyun.com/api/v1"
|
|
case "deepseek":
|
|
return "https://api.deepseek.com/v1"
|
|
case "cerebras":
|
|
return "https://api.cerebras.ai/v1"
|
|
case "vivgrid":
|
|
return "https://api.vivgrid.com/v1"
|
|
case "volcengine":
|
|
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":
|
|
return "https://api.mistral.ai/v1"
|
|
case "avian":
|
|
return "https://api.avian.io/v1"
|
|
case "minimax":
|
|
return "https://api.minimaxi.com/v1"
|
|
case "longcat":
|
|
return "https://api.longcat.chat/openai"
|
|
case "modelscope":
|
|
return "https://api-inference.modelscope.cn/v1"
|
|
default:
|
|
return ""
|
|
}
|
|
}
|