1 line
30 KiB
JSON
1 line
30 KiB
JSON
{"repo": "8u9i/picoclaw", "files": {"pkg/providers/http_provider.go": {"4d8dd5990d90bf7635347cc25cc648c0ddf9a685": {"t": 1785746096.9541023, "n": 13374, "h": "a7c2425aeb8e34a2", "c": "// PicoClaw - Ultra-lightweight personal AI agent\n// Inspired by and based on nanobot: https://github.com/HKUDS/nanobot\n// License: MIT\n//\n// Copyright (c) 2026 PicoClaw contributors\n\npackage providers\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/sipeed/picoclaw/pkg/auth\"\n\t\"github.com/sipeed/picoclaw/pkg/config\"\n)\n\ntype HTTPProvider struct {\n\tapiKey string\n\tapiBase string\n\thttpClient *http.Client\n}\n\nfunc NewHTTPProvider(apiKey, apiBase, proxy string) *HTTPProvider {\n\tclient := &http.Client{\n\t\tTimeout: 120 * time.Second,\n\t}\n\n\tif proxy != \"\" {\n\t\tproxyURL, err := url.Parse(proxy)\n\t\tif err == nil {\n\t\t\tclient.Transport = &http.Transport{\n\t\t\t\tProxy: http.ProxyURL(proxyURL),\n\t\t\t}\n\t\t}\n\t}\n\n\treturn &HTTPProvider{\n\t\tapiKey: apiKey,\n\t\tapiBase: strings.TrimRight(apiBase, \"/\"),\n\t\thttpClient: client,\n\t}\n}\n\nfunc (p *HTTPProvider) Chat(ctx context.Context, messages []Message, tools []ToolDefinition, model string, options map[string]interface{}) (*LLMResponse, error) {\n\tif p.apiBase == \"\" {\n\t\treturn nil, fmt.Errorf(\"API base not configured\")\n\t}\n\n\t// Strip provider prefix from model name (e.g., moonshot/kimi-k2.5 -> kimi-k2.5)\n\tif idx := strings.Index(model, \"/\"); idx != -1 {\n\t\tprefix := model[:idx]\n\t\tif prefix == \"moonshot\" || prefix == \"nvidia\" {\n\t\t\tmodel = model[idx+1:]\n\t\t}\n\t}\n\n\trequestBody := map[string]interface{}{\n\t\t\"model\": model,\n\t\t\"messages\": messages,\n\t}\n\n\tif len(tools) > 0 {\n\t\trequestBody[\"tools\"] = tools\n\t\trequestBody[\"tool_choice\"] = \"auto\"\n\t}\n\n\tif maxTokens, ok := options[\"max_tokens\"].(int); ok {\n\t\tlowerModel := strings.ToLower(model)\n\t\tif strings.Contains(lowerModel, \"glm\") || strings.Contains(lowerModel, \"o1\") {\n\t\t\trequestBody[\"max_completion_tokens\"] = maxTokens\n\t\t} else {\n\t\t\trequestBody[\"max_tokens\"] = maxTokens\n\t\t}\n\t}\n\n\tif temperature, ok := options[\"temperature\"].(float64); ok {\n\t\tlowerModel := strings.ToLower(model)\n\t\t// Kimi k2 models only support temperature=1\n\t\tif strings.Contains(lowerModel, \"kimi\") && strings.Contains(lowerModel, \"k2\") {\n\t\t\trequestBody[\"temperature\"] = 1.0\n\t\t} else {\n\t\t\trequestBody[\"temperature\"] = temperature\n\t\t}\n\t}\n\n\tjsonData, err := json.Marshal(requestBody)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, \"POST\", p.apiBase+\"/chat/completions\", bytes.NewReader(jsonData))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\tif p.apiKey != \"\" {\n\t\treq.Header.Set(\"Authorization\", \"Bearer \"+p.apiKey)\n\t}\n\n\tresp, err := p.httpClient.Do(req)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to send request: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\tbody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to read response: %w\", err)\n\t}\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn nil, fmt.Errorf(\"API request failed:\\n Status: %d\\n Body: %s\", resp.StatusCode, string(body))\n\t}\n\n\treturn p.parseResponse(body)\n}\n\nfunc (p *HTTPProvider) parseResponse(body []byte) (*LLMResponse, error) {\n\tvar apiResponse struct {\n\t\tChoices []struct {\n\t\t\tMessage struct {\n\t\t\t\tContent string `json:\"content\"`\n\t\t\t\tToolCalls []struct {\n\t\t\t\t\tID string `json:\"id\"`\n\t\t\t\t\tType string `json:\"type\"`\n\t\t\t\t\tFunction *struct {\n\t\t\t\t\t\tName string `json:\"name\"`\n\t\t\t\t\t\tArguments string `json:\"arguments\"`\n\t\t\t\t\t} `json:\"function\"`\n\t\t\t\t} `json:\"tool_calls\"`\n\t\t\t} `json:\"message\"`\n\t\t\tFinishReason string `json:\"finish_reason\"`\n\t\t} `json:\"choices\"`\n\t\tUsage *UsageInfo `json:\"usage\"`\n\t}\n\n\tif err := json.Unmarshal(body, &apiResponse); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tif len(apiResponse.Choices) == 0 {\n\t\treturn &LLMResponse{\n\t\t\tContent: \"\",\n\t\t\tFinishReason: \"stop\",\n\t\t}, nil\n\t}\n\n\tchoice := apiResponse.Choices[0]\n\n\ttoolCalls := make([]ToolCall, 0, len(choice.Message.ToolCalls))\n\tfor _, tc := range choice.Message.ToolCalls {\n\t\targuments := make(map[string]interface{})\n\t\tname := \"\"\n\n\t\t// Handle OpenAI format with nested function object\n\t\tif tc.Type == \"function\" && tc.Function != nil {\n\t\t\tname = tc.Function.Name\n\t\t\tif tc.Function.Arguments != \"\" {\n\t\t\t\tif err := json.Unmarshal([]byte(tc.Function.Arguments), &arguments); err != nil {\n\t\t\t\t\targuments[\"raw\"] = tc.Function.Arguments\n\t\t\t\t}\n\t\t\t}\n\t\t} else if tc.Function != nil {\n\t\t\t// Legacy format without type field\n\t\t\tname = tc.Function.Name\n\t\t\tif tc.Function.Arguments != \"\" {\n\t\t\t\tif err := json.Unmarshal([]byte(tc.Function.Arguments), &arguments); err != nil {\n\t\t\t\t\targuments[\"raw\"] = tc.Function.Arguments\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\ttoolCalls = append(toolCalls, ToolCall{\n\t\t\tID: tc.ID,\n\t\t\tName: name,\n\t\t\tArguments: arguments,\n\t\t})\n\t}\n\n\treturn &LLMResponse{\n\t\tContent: choice.Message.Content,\n\t\tToolCalls: toolCalls,\n\t\tFinishReason: choice.FinishReason,\n\t\tUsage: apiResponse.Usage,\n\t}, nil\n}\n\nfunc (p *HTTPProvider) GetDefaultModel() string {\n\treturn \"\"\n}\n\nfunc createClaudeAuthProvider() (LLMProvider, error) {\n\tcred, err := auth.GetCredential(\"anthropic\")\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"loading auth credentials: %w\", err)\n\t}\n\tif cred == nil {\n\t\treturn nil, fmt.Errorf(\"no credentials for anthropic. Run: picoclaw auth login --provider anthropic\")\n\t}\n\treturn NewClaudeProviderWithTokenSource(cred.AccessToken, createClaudeTokenSource()), nil\n}\n\nfunc createCodexAuthProvider() (LLMProvider, error) {\n\tcred, err := auth.GetCredential(\"openai\")\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"loading auth credentials: %w\", err)\n\t}\n\tif cred == nil {\n\t\treturn nil, fmt.Errorf(\"no credentials for openai. Run: picoclaw auth login --provider openai\")\n\t}\n\treturn NewCodexProviderWithTokenSource(cred.AccessToken, cred.AccountID, createCodexTokenSource()), nil\n}\n\nfunc CreateProvider(cfg *config.Config) (LLMProvider, error) {\n\tmodel := cfg.Agents.Defaults.Model\n\tproviderName := strings.ToLower(cfg.Agents.Defaults.Provider)\n\n\tvar apiKey, apiBase, proxy string\n\n\tlowerModel := strings.ToLower(model)\n\n\t// First, try to use explicitly configured provider\n\tif providerName != \"\" {\n\t\tswitch providerName {\n\t\tcase \"groq\":\n\t\t\tif cfg.Providers.Groq.APIKey != \"\" {\n\t\t\t\tapiKey = cfg.Providers.Groq.APIKey\n\t\t\t\tapiBase = cfg.Providers.Groq.APIBase\n\t\t\t\tif apiBase == \"\" {\n\t\t\t\t\tapiBase = \"https://api.groq.com/openai/v1\"\n\t\t\t\t}\n\t\t\t}\n\t\tcase \"openai\", \"gpt\":\n\t\t\tif cfg.Providers.OpenAI.APIKey != \"\" || cfg.Providers.OpenAI.AuthMethod != \"\" {\n\t\t\t\tif cfg.Providers.OpenAI.AuthMethod == \"oauth\" || cfg.Providers.OpenAI.AuthMethod == \"token\" {\n\t\t\t\t\treturn createCodexAuthProvider()\n\t\t\t\t}\n\t\t\t\tapiKey = cfg.Providers.OpenAI.APIKey\n\t\t\t\tapiBase = cfg.Providers.OpenAI.APIBase\n\t\t\t\tif apiBase == \"\" {\n\t\t\t\t\tapiBase = \"https://api.openai.com/v1\"\n\t\t\t\t}\n\t\t\t}\n\t\tcase \"anthropic\", \"claude\":\n\t\t\tif cfg.Providers.Anthropic.APIKey != \"\" || cfg.Providers.Anthropic.AuthMethod != \"\" {\n\t\t\t\tif cfg.Providers.Anthropic.AuthMethod == \"oauth\" || cfg.Providers.Anthropic.AuthMethod == \"token\" {\n\t\t\t\t\treturn createClaudeAuthProvider()\n\t\t\t\t}\n\t\t\t\tapiKey = cfg.Providers.Anthropic.APIKey\n\t\t\t\tapiBase = cfg.Providers.Anthropic.APIBase\n\t\t\t\tif apiBase == \"\" {\n\t\t\t\t\tapiBase = \"https://api.anthropic.com/v1\"\n\t\t\t\t}\n\t\t\t}\n\t\tcase \"openrouter\":\n\t\t\tif cfg.Providers.OpenRouter.APIKey != \"\" {\n\t\t\t\tapiKey = cfg.Providers.OpenRouter.APIKey\n\t\t\t\tif cfg.Providers.OpenRouter.APIBase != \"\" {\n\t\t\t\t\tapiBase = cfg.Providers.OpenRouter.APIBase\n\t\t\t\t} else {\n\t\t\t\t\tapiBase = \"https://openrouter.ai/api/v1\"\n\t\t\t\t}\n\t\t\t}\n\t\tcase \"zhipu\", \"glm\":\n\t\t\tif cfg.Providers.Zhipu.APIKey != \"\" {\n\t\t\t\tapiKey = cfg.Providers.Zhipu.APIKey\n\t\t\t\tapiBase = cfg.Providers.Zhipu.APIBase\n\t\t\t\tif apiBase == \"\" {\n\t\t\t\t\tapiBase = \"https://open.bigmodel.cn/api/paas/v4\"\n\t\t\t\t}\n\t\t\t}\n\t\tcase \"gemini\", \"google\":\n\t\t\tif cfg.Providers.Gemini.APIKey != \"\" {\n\t\t\t\tapiKey = cfg.Providers.Gemini.APIKey\n\t\t\t\tapiBase = cfg.Providers.Gemini.APIBase\n\t\t\t\tif apiBase == \"\" {\n\t\t\t\t\tapiBase = \"https://generativelanguage.googleapis.com/v1beta\"\n\t\t\t\t}\n\t\t\t}\n\t\tcase \"vllm\":\n\t\t\tif cfg.Providers.VLLM.APIBase != \"\" {\n\t\t\t\tapiKey = cfg.Providers.VLLM.APIKey\n\t\t\t\tapiBase = cfg.Providers.VLLM.APIBase\n\t\t\t}\n\t\tcase \"shengsuanyun\":\n\t\t\tif cfg.Providers.ShengSuanYun.APIKey != \"\" {\n\t\t\t\tapiKey = cfg.Providers.ShengSuanYun.APIKey\n\t\t\t\tapiBase = cfg.Providers.ShengSuanYun.APIBase\n\t\t\t\tif apiBase == \"\" {\n\t\t\t\t\tapiBase = \"https://router.shengsuanyun.com/api/v1\"\n\t\t\t\t}\n\t\t\t}\n\t\tcase \"claude-cli\", \"claudecode\", \"claude-code\":\n\t\t\tworkspace := cfg.Agents.Defaults.Workspace\n\t\t\tif workspace == \"\" {\n\t\t\t\tworkspace = \".\"\n\t\t\t}\n\t\t\treturn NewClaudeCliProvider(workspace), nil\n\t\tcase \"deepseek\":\n\t\t\tif cfg.Providers.DeepSeek.APIKey != \"\" {\n\t\t\t\tapiKey = cfg.Providers.DeepSeek.APIKey\n\t\t\t\tapiBase = cfg.Providers.DeepSeek.APIBase\n\t\t\t\tif apiBase == \"\" {\n\t\t\t\t\tapiBase = \"https://api.deepseek.com/v1\"\n\t\t\t\t}\n\t\t\t\tif model != \"deepseek-chat\" && model != \"deepseek-reasoner\" {\n\t\t\t\t\tmodel = \"deepseek-chat\"\n\t\t\t\t}\n\t\t\t}\n\t\tcase \"github_copilot\", \"copilot\":\n\t\t\tif cfg.Providers.GitHubCopilot.APIBase != \"\" {\n\t\t\t\tapiBase = cfg.Providers.GitHubCopilot.APIBase\n\t\t\t} else {\n\t\t\t\tapiBase = \"localhost:4321\"\n\t\t\t}\n\t\t\treturn NewGitHubCopilotProvider(apiBase, cfg.Providers.GitHubCopilot.ConnectMode, model)\n\n\t\t}\n\n\t}\n\n\t// Fallback: detect provider from model name\n\tif apiKey == \"\" && apiBase == \"\" {\n\t\tswitch {\n\t\tcase (strings.Contains(lowerModel, \"kimi\") || strings.Contains(lowerModel, \"moonshot\") || strings.HasPrefix(model, \"moonshot/\")) && cfg.Providers.Moonshot.APIKey != \"\":\n\t\t\tapiKey = cfg.Providers.Moonshot.APIKey\n\t\t\tapiBase = cfg.Providers.Moonshot.APIBase\n\t\t\tproxy = cfg.Providers.Moonshot.Proxy\n\t\t\tif apiBase == \"\" {\n\t\t\t\tapiBase = \"https://api.moonshot.cn/v1\"\n\t\t\t}\n\n\t\tcase strings.HasPrefix(model, \"openrouter/\") || strings.HasPrefix(model, \"anthropic/\") || strings.HasPrefix(model, \"openai/\") || strings.HasPrefix(model, \"meta-llama/\") || strings.HasPrefix(model, \"deepseek/\") || strings.HasPrefix(model, \"google/\"):\n\t\t\tapiKey = cfg.Providers.OpenRouter.APIKey\n\t\t\tproxy = cfg.Providers.OpenRouter.Proxy\n\t\t\tif cfg.Providers.OpenRouter.APIBase != \"\" {\n\t\t\t\tapiBase = cfg.Providers.OpenRouter.APIBase\n\t\t\t} else {\n\t\t\t\tapiBase = \"https://openrouter.ai/api/v1\"\n\t\t\t}\n\n\t\tcase (strings.Contains(lowerModel, \"claude\") || strings.HasPrefix(model, \"anthropic/\")) && (cfg.Providers.Anthropic.APIKey != \"\" || cfg.Providers.Anthropic.AuthMethod != \"\"):\n\t\t\tif cfg.Providers.Anthropic.AuthMethod == \"oauth\" || cfg.Providers.Anthropic.AuthMethod == \"token\" {\n\t\t\t\treturn createClaudeAuthProvider()\n\t\t\t}\n\t\t\tapiKey = cfg.Providers.Anthropic.APIKey\n\t\t\tapiBase = cfg.Providers.Anthropic.APIBase\n\t\t\tproxy = cfg.Providers.Anthropic.Proxy\n\t\t\tif apiBase == \"\" {\n\t\t\t\tapiBase = \"https://api.anthropic.com/v1\"\n\t\t\t}\n\n\t\tcase (strings.Contains(lowerModel, \"gpt\") || strings.HasPrefix(model, \"openai/\")) && (cfg.Providers.OpenAI.APIKey != \"\" || cfg.Providers.OpenAI.AuthMethod != \"\"):\n\t\t\tif cfg.Providers.OpenAI.AuthMethod == \"oauth\" || cfg.Providers.OpenAI.AuthMethod == \"token\" {\n\t\t\t\treturn createCodexAuthProvider()\n\t\t\t}\n\t\t\tapiKey = cfg.Providers.OpenAI.APIKey\n\t\t\tapiBase = cfg.Providers.OpenAI.APIBase\n\t\t\tproxy = cfg.Providers.OpenAI.Proxy\n\t\t\tif apiBase == \"\" {\n\t\t\t\tapiBase = \"https://api.openai.com/v1\"\n\t\t\t}\n\n\t\tcase (strings.Contains(lowerModel, \"gemini\") || strings.HasPrefix(model, \"google/\")) && cfg.Providers.Gemini.APIKey != \"\":\n\t\t\tapiKey = cfg.Providers.Gemini.APIKey\n\t\t\tapiBase = cfg.Providers.Gemini.APIBase\n\t\t\tproxy = cfg.Providers.Gemini.Proxy\n\t\t\tif apiBase == \"\" {\n\t\t\t\tapiBase = \"https://generativelanguage.googleapis.com/v1beta\"\n\t\t\t}\n\n\t\tcase (strings.Contains(lowerModel, \"glm\") || strings.Contains(lowerModel, \"zhipu\") || strings.Contains(lowerModel, \"zai\")) && cfg.Providers.Zhipu.APIKey != \"\":\n\t\t\tapiKey = cfg.Providers.Zhipu.APIKey\n\t\t\tapiBase = cfg.Providers.Zhipu.APIBase\n\t\t\tproxy = cfg.Providers.Zhipu.Proxy\n\t\t\tif apiBase == \"\" {\n\t\t\t\tapiBase = \"https://open.bigmodel.cn/api/paas/v4\"\n\t\t\t}\n\n\t\tcase (strings.Contains(lowerModel, \"groq\") || strings.HasPrefix(model, \"groq/\")) && cfg.Providers.Groq.APIKey != \"\":\n\t\t\tapiKey = cfg.Providers.Groq.APIKey\n\t\t\tapiBase = cfg.Providers.Groq.APIBase\n\t\t\tproxy = cfg.Providers.Groq.Proxy\n\t\t\tif apiBase == \"\" {\n\t\t\t\tapiBase = \"https://api.groq.com/openai/v1\"\n\t\t\t}\n\n\t\tcase (strings.Contains(lowerModel, \"nvidia\") || strings.HasPrefix(model, \"nvidia/\")) && cfg.Providers.Nvidia.APIKey != \"\":\n\t\t\tapiKey = cfg.Providers.Nvidia.APIKey\n\t\t\tapiBase = cfg.Providers.Nvidia.APIBase\n\t\t\tproxy = cfg.Providers.Nvidia.Proxy\n\t\t\tif apiBase == \"\" {\n\t\t\t\tapiBase = \"https://integrate.api.nvidia.com/v1\"\n\t\t\t}\n\n\t\tcase cfg.Providers.VLLM.APIBase != \"\":\n\t\t\tapiKey = cfg.Providers.VLLM.APIKey\n\t\t\tapiBase = cfg.Providers.VLLM.APIBase\n\t\t\tproxy = cfg.Providers.VLLM.Proxy\n\n\t\tdefault:\n\t\t\tif cfg.Providers.OpenRouter.APIKey != \"\" {\n\t\t\t\tapiKey = cfg.Providers.OpenRouter.APIKey\n\t\t\t\tproxy = cfg.Providers.OpenRouter.Proxy\n\t\t\t\tif cfg.Providers.OpenRouter.APIBase != \"\" {\n\t\t\t\t\tapiBase = cfg.Providers.OpenRouter.APIBase\n\t\t\t\t} else {\n\t\t\t\t\tapiBase = \"https://openrouter.ai/api/v1\"\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\treturn nil, fmt.Errorf(\"no API key configured for model: %s\", model)\n\t\t\t}\n\t\t}\n\t}\n\n\tif apiKey == \"\" && !strings.HasPrefix(model, \"bedrock/\") {\n\t\treturn nil, fmt.Errorf(\"no API key configured for provider (model: %s)\", model)\n\t}\n\n\tif apiBase == \"\" {\n\t\treturn nil, fmt.Errorf(\"no API base configured for provider (model: %s)\", model)\n\t}\n\n\treturn NewHTTPProvider(apiKey, apiBase, proxy), nil\n}\n"}, "65c75272968220f112e0232f46707d59bf349c90": {"t": 1785759832.6613982, "n": 13374, "h": "a7c2425aeb8e34a2", "c": "// PicoClaw - Ultra-lightweight personal AI agent\n// Inspired by and based on nanobot: https://github.com/HKUDS/nanobot\n// License: MIT\n//\n// Copyright (c) 2026 PicoClaw contributors\n\npackage providers\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/sipeed/picoclaw/pkg/auth\"\n\t\"github.com/sipeed/picoclaw/pkg/config\"\n)\n\ntype HTTPProvider struct {\n\tapiKey string\n\tapiBase string\n\thttpClient *http.Client\n}\n\nfunc NewHTTPProvider(apiKey, apiBase, proxy string) *HTTPProvider {\n\tclient := &http.Client{\n\t\tTimeout: 120 * time.Second,\n\t}\n\n\tif proxy != \"\" {\n\t\tproxyURL, err := url.Parse(proxy)\n\t\tif err == nil {\n\t\t\tclient.Transport = &http.Transport{\n\t\t\t\tProxy: http.ProxyURL(proxyURL),\n\t\t\t}\n\t\t}\n\t}\n\n\treturn &HTTPProvider{\n\t\tapiKey: apiKey,\n\t\tapiBase: strings.TrimRight(apiBase, \"/\"),\n\t\thttpClient: client,\n\t}\n}\n\nfunc (p *HTTPProvider) Chat(ctx context.Context, messages []Message, tools []ToolDefinition, model string, options map[string]interface{}) (*LLMResponse, error) {\n\tif p.apiBase == \"\" {\n\t\treturn nil, fmt.Errorf(\"API base not configured\")\n\t}\n\n\t// Strip provider prefix from model name (e.g., moonshot/kimi-k2.5 -> kimi-k2.5)\n\tif idx := strings.Index(model, \"/\"); idx != -1 {\n\t\tprefix := model[:idx]\n\t\tif prefix == \"moonshot\" || prefix == \"nvidia\" {\n\t\t\tmodel = model[idx+1:]\n\t\t}\n\t}\n\n\trequestBody := map[string]interface{}{\n\t\t\"model\": model,\n\t\t\"messages\": messages,\n\t}\n\n\tif len(tools) > 0 {\n\t\trequestBody[\"tools\"] = tools\n\t\trequestBody[\"tool_choice\"] = \"auto\"\n\t}\n\n\tif maxTokens, ok := options[\"max_tokens\"].(int); ok {\n\t\tlowerModel := strings.ToLower(model)\n\t\tif strings.Contains(lowerModel, \"glm\") || strings.Contains(lowerModel, \"o1\") {\n\t\t\trequestBody[\"max_completion_tokens\"] = maxTokens\n\t\t} else {\n\t\t\trequestBody[\"max_tokens\"] = maxTokens\n\t\t}\n\t}\n\n\tif temperature, ok := options[\"temperature\"].(float64); ok {\n\t\tlowerModel := strings.ToLower(model)\n\t\t// Kimi k2 models only support temperature=1\n\t\tif strings.Contains(lowerModel, \"kimi\") && strings.Contains(lowerModel, \"k2\") {\n\t\t\trequestBody[\"temperature\"] = 1.0\n\t\t} else {\n\t\t\trequestBody[\"temperature\"] = temperature\n\t\t}\n\t}\n\n\tjsonData, err := json.Marshal(requestBody)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, \"POST\", p.apiBase+\"/chat/completions\", bytes.NewReader(jsonData))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\tif p.apiKey != \"\" {\n\t\treq.Header.Set(\"Authorization\", \"Bearer \"+p.apiKey)\n\t}\n\n\tresp, err := p.httpClient.Do(req)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to send request: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\tbody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to read response: %w\", err)\n\t}\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn nil, fmt.Errorf(\"API request failed:\\n Status: %d\\n Body: %s\", resp.StatusCode, string(body))\n\t}\n\n\treturn p.parseResponse(body)\n}\n\nfunc (p *HTTPProvider) parseResponse(body []byte) (*LLMResponse, error) {\n\tvar apiResponse struct {\n\t\tChoices []struct {\n\t\t\tMessage struct {\n\t\t\t\tContent string `json:\"content\"`\n\t\t\t\tToolCalls []struct {\n\t\t\t\t\tID string `json:\"id\"`\n\t\t\t\t\tType string `json:\"type\"`\n\t\t\t\t\tFunction *struct {\n\t\t\t\t\t\tName string `json:\"name\"`\n\t\t\t\t\t\tArguments string `json:\"arguments\"`\n\t\t\t\t\t} `json:\"function\"`\n\t\t\t\t} `json:\"tool_calls\"`\n\t\t\t} `json:\"message\"`\n\t\t\tFinishReason string `json:\"finish_reason\"`\n\t\t} `json:\"choices\"`\n\t\tUsage *UsageInfo `json:\"usage\"`\n\t}\n\n\tif err := json.Unmarshal(body, &apiResponse); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tif len(apiResponse.Choices) == 0 {\n\t\treturn &LLMResponse{\n\t\t\tContent: \"\",\n\t\t\tFinishReason: \"stop\",\n\t\t}, nil\n\t}\n\n\tchoice := apiResponse.Choices[0]\n\n\ttoolCalls := make([]ToolCall, 0, len(choice.Message.ToolCalls))\n\tfor _, tc := range choice.Message.ToolCalls {\n\t\targuments := make(map[string]interface{})\n\t\tname := \"\"\n\n\t\t// Handle OpenAI format with nested function object\n\t\tif tc.Type == \"function\" && tc.Function != nil {\n\t\t\tname = tc.Function.Name\n\t\t\tif tc.Function.Arguments != \"\" {\n\t\t\t\tif err := json.Unmarshal([]byte(tc.Function.Arguments), &arguments); err != nil {\n\t\t\t\t\targuments[\"raw\"] = tc.Function.Arguments\n\t\t\t\t}\n\t\t\t}\n\t\t} else if tc.Function != nil {\n\t\t\t// Legacy format without type field\n\t\t\tname = tc.Function.Name\n\t\t\tif tc.Function.Arguments != \"\" {\n\t\t\t\tif err := json.Unmarshal([]byte(tc.Function.Arguments), &arguments); err != nil {\n\t\t\t\t\targuments[\"raw\"] = tc.Function.Arguments\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\ttoolCalls = append(toolCalls, ToolCall{\n\t\t\tID: tc.ID,\n\t\t\tName: name,\n\t\t\tArguments: arguments,\n\t\t})\n\t}\n\n\treturn &LLMResponse{\n\t\tContent: choice.Message.Content,\n\t\tToolCalls: toolCalls,\n\t\tFinishReason: choice.FinishReason,\n\t\tUsage: apiResponse.Usage,\n\t}, nil\n}\n\nfunc (p *HTTPProvider) GetDefaultModel() string {\n\treturn \"\"\n}\n\nfunc createClaudeAuthProvider() (LLMProvider, error) {\n\tcred, err := auth.GetCredential(\"anthropic\")\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"loading auth credentials: %w\", err)\n\t}\n\tif cred == nil {\n\t\treturn nil, fmt.Errorf(\"no credentials for anthropic. Run: picoclaw auth login --provider anthropic\")\n\t}\n\treturn NewClaudeProviderWithTokenSource(cred.AccessToken, createClaudeTokenSource()), nil\n}\n\nfunc createCodexAuthProvider() (LLMProvider, error) {\n\tcred, err := auth.GetCredential(\"openai\")\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"loading auth credentials: %w\", err)\n\t}\n\tif cred == nil {\n\t\treturn nil, fmt.Errorf(\"no credentials for openai. Run: picoclaw auth login --provider openai\")\n\t}\n\treturn NewCodexProviderWithTokenSource(cred.AccessToken, cred.AccountID, createCodexTokenSource()), nil\n}\n\nfunc CreateProvider(cfg *config.Config) (LLMProvider, error) {\n\tmodel := cfg.Agents.Defaults.Model\n\tproviderName := strings.ToLower(cfg.Agents.Defaults.Provider)\n\n\tvar apiKey, apiBase, proxy string\n\n\tlowerModel := strings.ToLower(model)\n\n\t// First, try to use explicitly configured provider\n\tif providerName != \"\" {\n\t\tswitch providerName {\n\t\tcase \"groq\":\n\t\t\tif cfg.Providers.Groq.APIKey != \"\" {\n\t\t\t\tapiKey = cfg.Providers.Groq.APIKey\n\t\t\t\tapiBase = cfg.Providers.Groq.APIBase\n\t\t\t\tif apiBase == \"\" {\n\t\t\t\t\tapiBase = \"https://api.groq.com/openai/v1\"\n\t\t\t\t}\n\t\t\t}\n\t\tcase \"openai\", \"gpt\":\n\t\t\tif cfg.Providers.OpenAI.APIKey != \"\" || cfg.Providers.OpenAI.AuthMethod != \"\" {\n\t\t\t\tif cfg.Providers.OpenAI.AuthMethod == \"oauth\" || cfg.Providers.OpenAI.AuthMethod == \"token\" {\n\t\t\t\t\treturn createCodexAuthProvider()\n\t\t\t\t}\n\t\t\t\tapiKey = cfg.Providers.OpenAI.APIKey\n\t\t\t\tapiBase = cfg.Providers.OpenAI.APIBase\n\t\t\t\tif apiBase == \"\" {\n\t\t\t\t\tapiBase = \"https://api.openai.com/v1\"\n\t\t\t\t}\n\t\t\t}\n\t\tcase \"anthropic\", \"claude\":\n\t\t\tif cfg.Providers.Anthropic.APIKey != \"\" || cfg.Providers.Anthropic.AuthMethod != \"\" {\n\t\t\t\tif cfg.Providers.Anthropic.AuthMethod == \"oauth\" || cfg.Providers.Anthropic.AuthMethod == \"token\" {\n\t\t\t\t\treturn createClaudeAuthProvider()\n\t\t\t\t}\n\t\t\t\tapiKey = cfg.Providers.Anthropic.APIKey\n\t\t\t\tapiBase = cfg.Providers.Anthropic.APIBase\n\t\t\t\tif apiBase == \"\" {\n\t\t\t\t\tapiBase = \"https://api.anthropic.com/v1\"\n\t\t\t\t}\n\t\t\t}\n\t\tcase \"openrouter\":\n\t\t\tif cfg.Providers.OpenRouter.APIKey != \"\" {\n\t\t\t\tapiKey = cfg.Providers.OpenRouter.APIKey\n\t\t\t\tif cfg.Providers.OpenRouter.APIBase != \"\" {\n\t\t\t\t\tapiBase = cfg.Providers.OpenRouter.APIBase\n\t\t\t\t} else {\n\t\t\t\t\tapiBase = \"https://openrouter.ai/api/v1\"\n\t\t\t\t}\n\t\t\t}\n\t\tcase \"zhipu\", \"glm\":\n\t\t\tif cfg.Providers.Zhipu.APIKey != \"\" {\n\t\t\t\tapiKey = cfg.Providers.Zhipu.APIKey\n\t\t\t\tapiBase = cfg.Providers.Zhipu.APIBase\n\t\t\t\tif apiBase == \"\" {\n\t\t\t\t\tapiBase = \"https://open.bigmodel.cn/api/paas/v4\"\n\t\t\t\t}\n\t\t\t}\n\t\tcase \"gemini\", \"google\":\n\t\t\tif cfg.Providers.Gemini.APIKey != \"\" {\n\t\t\t\tapiKey = cfg.Providers.Gemini.APIKey\n\t\t\t\tapiBase = cfg.Providers.Gemini.APIBase\n\t\t\t\tif apiBase == \"\" {\n\t\t\t\t\tapiBase = \"https://generativelanguage.googleapis.com/v1beta\"\n\t\t\t\t}\n\t\t\t}\n\t\tcase \"vllm\":\n\t\t\tif cfg.Providers.VLLM.APIBase != \"\" {\n\t\t\t\tapiKey = cfg.Providers.VLLM.APIKey\n\t\t\t\tapiBase = cfg.Providers.VLLM.APIBase\n\t\t\t}\n\t\tcase \"shengsuanyun\":\n\t\t\tif cfg.Providers.ShengSuanYun.APIKey != \"\" {\n\t\t\t\tapiKey = cfg.Providers.ShengSuanYun.APIKey\n\t\t\t\tapiBase = cfg.Providers.ShengSuanYun.APIBase\n\t\t\t\tif apiBase == \"\" {\n\t\t\t\t\tapiBase = \"https://router.shengsuanyun.com/api/v1\"\n\t\t\t\t}\n\t\t\t}\n\t\tcase \"claude-cli\", \"claudecode\", \"claude-code\":\n\t\t\tworkspace := cfg.Agents.Defaults.Workspace\n\t\t\tif workspace == \"\" {\n\t\t\t\tworkspace = \".\"\n\t\t\t}\n\t\t\treturn NewClaudeCliProvider(workspace), nil\n\t\tcase \"deepseek\":\n\t\t\tif cfg.Providers.DeepSeek.APIKey != \"\" {\n\t\t\t\tapiKey = cfg.Providers.DeepSeek.APIKey\n\t\t\t\tapiBase = cfg.Providers.DeepSeek.APIBase\n\t\t\t\tif apiBase == \"\" {\n\t\t\t\t\tapiBase = \"https://api.deepseek.com/v1\"\n\t\t\t\t}\n\t\t\t\tif model != \"deepseek-chat\" && model != \"deepseek-reasoner\" {\n\t\t\t\t\tmodel = \"deepseek-chat\"\n\t\t\t\t}\n\t\t\t}\n\t\tcase \"github_copilot\", \"copilot\":\n\t\t\tif cfg.Providers.GitHubCopilot.APIBase != \"\" {\n\t\t\t\tapiBase = cfg.Providers.GitHubCopilot.APIBase\n\t\t\t} else {\n\t\t\t\tapiBase = \"localhost:4321\"\n\t\t\t}\n\t\t\treturn NewGitHubCopilotProvider(apiBase, cfg.Providers.GitHubCopilot.ConnectMode, model)\n\n\t\t}\n\n\t}\n\n\t// Fallback: detect provider from model name\n\tif apiKey == \"\" && apiBase == \"\" {\n\t\tswitch {\n\t\tcase (strings.Contains(lowerModel, \"kimi\") || strings.Contains(lowerModel, \"moonshot\") || strings.HasPrefix(model, \"moonshot/\")) && cfg.Providers.Moonshot.APIKey != \"\":\n\t\t\tapiKey = cfg.Providers.Moonshot.APIKey\n\t\t\tapiBase = cfg.Providers.Moonshot.APIBase\n\t\t\tproxy = cfg.Providers.Moonshot.Proxy\n\t\t\tif apiBase == \"\" {\n\t\t\t\tapiBase = \"https://api.moonshot.cn/v1\"\n\t\t\t}\n\n\t\tcase strings.HasPrefix(model, \"openrouter/\") || strings.HasPrefix(model, \"anthropic/\") || strings.HasPrefix(model, \"openai/\") || strings.HasPrefix(model, \"meta-llama/\") || strings.HasPrefix(model, \"deepseek/\") || strings.HasPrefix(model, \"google/\"):\n\t\t\tapiKey = cfg.Providers.OpenRouter.APIKey\n\t\t\tproxy = cfg.Providers.OpenRouter.Proxy\n\t\t\tif cfg.Providers.OpenRouter.APIBase != \"\" {\n\t\t\t\tapiBase = cfg.Providers.OpenRouter.APIBase\n\t\t\t} else {\n\t\t\t\tapiBase = \"https://openrouter.ai/api/v1\"\n\t\t\t}\n\n\t\tcase (strings.Contains(lowerModel, \"claude\") || strings.HasPrefix(model, \"anthropic/\")) && (cfg.Providers.Anthropic.APIKey != \"\" || cfg.Providers.Anthropic.AuthMethod != \"\"):\n\t\t\tif cfg.Providers.Anthropic.AuthMethod == \"oauth\" || cfg.Providers.Anthropic.AuthMethod == \"token\" {\n\t\t\t\treturn createClaudeAuthProvider()\n\t\t\t}\n\t\t\tapiKey = cfg.Providers.Anthropic.APIKey\n\t\t\tapiBase = cfg.Providers.Anthropic.APIBase\n\t\t\tproxy = cfg.Providers.Anthropic.Proxy\n\t\t\tif apiBase == \"\" {\n\t\t\t\tapiBase = \"https://api.anthropic.com/v1\"\n\t\t\t}\n\n\t\tcase (strings.Contains(lowerModel, \"gpt\") || strings.HasPrefix(model, \"openai/\")) && (cfg.Providers.OpenAI.APIKey != \"\" || cfg.Providers.OpenAI.AuthMethod != \"\"):\n\t\t\tif cfg.Providers.OpenAI.AuthMethod == \"oauth\" || cfg.Providers.OpenAI.AuthMethod == \"token\" {\n\t\t\t\treturn createCodexAuthProvider()\n\t\t\t}\n\t\t\tapiKey = cfg.Providers.OpenAI.APIKey\n\t\t\tapiBase = cfg.Providers.OpenAI.APIBase\n\t\t\tproxy = cfg.Providers.OpenAI.Proxy\n\t\t\tif apiBase == \"\" {\n\t\t\t\tapiBase = \"https://api.openai.com/v1\"\n\t\t\t}\n\n\t\tcase (strings.Contains(lowerModel, \"gemini\") || strings.HasPrefix(model, \"google/\")) && cfg.Providers.Gemini.APIKey != \"\":\n\t\t\tapiKey = cfg.Providers.Gemini.APIKey\n\t\t\tapiBase = cfg.Providers.Gemini.APIBase\n\t\t\tproxy = cfg.Providers.Gemini.Proxy\n\t\t\tif apiBase == \"\" {\n\t\t\t\tapiBase = \"https://generativelanguage.googleapis.com/v1beta\"\n\t\t\t}\n\n\t\tcase (strings.Contains(lowerModel, \"glm\") || strings.Contains(lowerModel, \"zhipu\") || strings.Contains(lowerModel, \"zai\")) && cfg.Providers.Zhipu.APIKey != \"\":\n\t\t\tapiKey = cfg.Providers.Zhipu.APIKey\n\t\t\tapiBase = cfg.Providers.Zhipu.APIBase\n\t\t\tproxy = cfg.Providers.Zhipu.Proxy\n\t\t\tif apiBase == \"\" {\n\t\t\t\tapiBase = \"https://open.bigmodel.cn/api/paas/v4\"\n\t\t\t}\n\n\t\tcase (strings.Contains(lowerModel, \"groq\") || strings.HasPrefix(model, \"groq/\")) && cfg.Providers.Groq.APIKey != \"\":\n\t\t\tapiKey = cfg.Providers.Groq.APIKey\n\t\t\tapiBase = cfg.Providers.Groq.APIBase\n\t\t\tproxy = cfg.Providers.Groq.Proxy\n\t\t\tif apiBase == \"\" {\n\t\t\t\tapiBase = \"https://api.groq.com/openai/v1\"\n\t\t\t}\n\n\t\tcase (strings.Contains(lowerModel, \"nvidia\") || strings.HasPrefix(model, \"nvidia/\")) && cfg.Providers.Nvidia.APIKey != \"\":\n\t\t\tapiKey = cfg.Providers.Nvidia.APIKey\n\t\t\tapiBase = cfg.Providers.Nvidia.APIBase\n\t\t\tproxy = cfg.Providers.Nvidia.Proxy\n\t\t\tif apiBase == \"\" {\n\t\t\t\tapiBase = \"https://integrate.api.nvidia.com/v1\"\n\t\t\t}\n\n\t\tcase cfg.Providers.VLLM.APIBase != \"\":\n\t\t\tapiKey = cfg.Providers.VLLM.APIKey\n\t\t\tapiBase = cfg.Providers.VLLM.APIBase\n\t\t\tproxy = cfg.Providers.VLLM.Proxy\n\n\t\tdefault:\n\t\t\tif cfg.Providers.OpenRouter.APIKey != \"\" {\n\t\t\t\tapiKey = cfg.Providers.OpenRouter.APIKey\n\t\t\t\tproxy = cfg.Providers.OpenRouter.Proxy\n\t\t\t\tif cfg.Providers.OpenRouter.APIBase != \"\" {\n\t\t\t\t\tapiBase = cfg.Providers.OpenRouter.APIBase\n\t\t\t\t} else {\n\t\t\t\t\tapiBase = \"https://openrouter.ai/api/v1\"\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\treturn nil, fmt.Errorf(\"no API key configured for model: %s\", model)\n\t\t\t}\n\t\t}\n\t}\n\n\tif apiKey == \"\" && !strings.HasPrefix(model, \"bedrock/\") {\n\t\treturn nil, fmt.Errorf(\"no API key configured for provider (model: %s)\", model)\n\t}\n\n\tif apiBase == \"\" {\n\t\treturn nil, fmt.Errorf(\"no API base configured for provider (model: %s)\", model)\n\t}\n\n\treturn NewHTTPProvider(apiKey, apiBase, proxy), nil\n}\n"}}}} |