picoclaw/pkg/providers/anyllm_adapter.go
KoheiYamashita 01353f6b00 refactor: migrate LLM provider layer to any-llm-go adapter
Replace custom per-provider implementations (Anthropic, OpenAI, Codex,
GitHub Copilot, etc.) with a single AnyLLMAdapter wrapping any-llm-go.
Unify config from per-provider ProvidersConfig to a flat LLM section
(model, api_key, base_url). Remove OAuth/auth layer, migration tooling,
and hardcoded temperature defaults.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 13:54:14 +09:00

260 lines
6.8 KiB
Go

package providers
import (
"context"
"encoding/json"
"fmt"
"strings"
anyllm "github.com/mozilla-ai/any-llm-go"
"github.com/mozilla-ai/any-llm-go/providers/anthropic"
"github.com/mozilla-ai/any-llm-go/providers/deepseek"
"github.com/mozilla-ai/any-llm-go/providers/gemini"
"github.com/mozilla-ai/any-llm-go/providers/groq"
"github.com/mozilla-ai/any-llm-go/providers/llamacpp"
"github.com/mozilla-ai/any-llm-go/providers/llamafile"
"github.com/mozilla-ai/any-llm-go/providers/mistral"
"github.com/mozilla-ai/any-llm-go/providers/ollama"
"github.com/mozilla-ai/any-llm-go/providers/openai"
)
// AnyLLMAdapter wraps an any-llm-go provider to implement LLMProvider.
type AnyLLMAdapter struct {
provider anyllm.Provider // any-llm-go Provider interface
defaultModel string // e.g. "openai/gpt-5.2-chat-latest"
modelName string // e.g. "gpt-5.2-chat-latest" (passed per-request)
}
// parseModel splits "provider/model_name" at the first "/".
func parseModel(model string) (providerName, modelName string) {
idx := strings.Index(model, "/")
if idx == -1 {
return "", model
}
return model[:idx], model[idx+1:]
}
// providerAliases maps convenience names to canonical provider names.
var providerAliases = map[string]string{
"claude": "anthropic",
"google": "gemini",
}
// NewAnyLLMAdapter creates an AnyLLMAdapter from a model string (provider/model_name),
// an API key, and an optional base URL override.
func NewAnyLLMAdapter(model, apiKey, baseURL string) (*AnyLLMAdapter, error) {
providerName, modelName := parseModel(model)
// Apply aliases
if canonical, ok := providerAliases[providerName]; ok {
providerName = canonical
}
if providerName == "" {
return nil, fmt.Errorf("model must be in provider/model_name format (e.g. openai/gpt-4): %s", model)
}
p, err := createAnyLLMProvider(providerName, apiKey, baseURL)
if err != nil {
return nil, fmt.Errorf("creating provider %q: %w", providerName, err)
}
return &AnyLLMAdapter{
provider: p,
defaultModel: model,
modelName: modelName,
}, nil
}
// createAnyLLMProvider creates the appropriate any-llm-go provider by name.
func createAnyLLMProvider(name, apiKey, baseURL string) (anyllm.Provider, error) {
var opts []anyllm.Option
if apiKey != "" {
opts = append(opts, anyllm.WithAPIKey(apiKey))
}
if baseURL != "" {
opts = append(opts, anyllm.WithBaseURL(baseURL))
}
switch name {
case "anthropic":
return anthropic.New(opts...)
case "deepseek":
return deepseek.New(opts...)
case "gemini":
return gemini.New(opts...)
case "groq":
return groq.New(opts...)
case "llamacpp":
return llamacpp.New(opts...)
case "llamafile":
return llamafile.New(opts...)
case "mistral":
return mistral.New(opts...)
case "ollama":
return ollama.New(opts...)
case "openai":
return openai.New(opts...)
default:
return nil, fmt.Errorf("unsupported provider %q", name)
}
}
// Chat implements LLMProvider.
func (a *AnyLLMAdapter) Chat(ctx context.Context, messages []Message, tools []ToolDefinition, model string, options map[string]interface{}) (*LLMResponse, error) {
params := anyllm.CompletionParams{
Model: a.modelName,
Messages: convertMessagesToAnyLLM(messages),
Tools: convertToolsToAnyLLM(tools),
}
switch v := options["max_tokens"].(type) {
case int:
params.MaxTokens = &v
case float64:
mt := int(v)
params.MaxTokens = &mt
}
if temperature, ok := options["temperature"].(float64); ok {
params.Temperature = &temperature
}
result, err := a.provider.Completion(ctx, params)
if err != nil {
return nil, err
}
return convertAnyLLMResult(result), nil
}
// GetDefaultModel implements LLMProvider.
func (a *AnyLLMAdapter) GetDefaultModel() string {
return a.defaultModel
}
// convertMessagesToAnyLLM converts internal messages to any-llm-go messages.
func convertMessagesToAnyLLM(messages []Message) []anyllm.Message {
result := make([]anyllm.Message, 0, len(messages))
for _, msg := range messages {
m := anyllm.Message{
Role: msg.Role,
}
// Tool result messages
if msg.ToolCallID != "" {
m.Role = anyllm.RoleTool
m.Content = msg.Content
m.ToolCallID = msg.ToolCallID
result = append(result, m)
continue
}
// Build content: plain text or multimodal
if len(msg.Media) > 0 {
// Multimodal: text + images as ContentPart slice
var parts []anyllm.ContentPart
if msg.Content != "" {
parts = append(parts, anyllm.ContentPart{
Type: "text",
Text: msg.Content,
})
}
for _, mediaURL := range msg.Media {
parts = append(parts, anyllm.ContentPart{
Type: "image_url",
ImageURL: &anyllm.ImageURL{URL: mediaURL},
})
}
m.Content = parts
} else {
m.Content = msg.Content
}
// Assistant messages with tool calls
if len(msg.ToolCalls) > 0 {
m.ToolCalls = make([]anyllm.ToolCall, 0, len(msg.ToolCalls))
for _, tc := range msg.ToolCalls {
name := ""
args := ""
if tc.Function != nil {
name = tc.Function.Name
args = tc.Function.Arguments
}
if name == "" {
name = tc.Name
}
if args == "" && tc.Arguments != nil {
argsJSON, _ := json.Marshal(tc.Arguments)
args = string(argsJSON)
}
m.ToolCalls = append(m.ToolCalls, anyllm.ToolCall{
ID: tc.ID,
Type: "function",
Function: anyllm.FunctionCall{
Name: name,
Arguments: args,
},
})
}
}
result = append(result, m)
}
return result
}
// convertToolsToAnyLLM converts internal tool definitions to any-llm-go tools.
func convertToolsToAnyLLM(tools []ToolDefinition) []anyllm.Tool {
result := make([]anyllm.Tool, 0, len(tools))
for _, t := range tools {
result = append(result, anyllm.Tool{
Type: "function",
Function: anyllm.Function{
Name: t.Function.Name,
Description: t.Function.Description,
Parameters: t.Function.Parameters,
},
})
}
return result
}
// convertAnyLLMResult converts an any-llm-go ChatCompletion to our LLMResponse.
func convertAnyLLMResult(result *anyllm.ChatCompletion) *LLMResponse {
if len(result.Choices) == 0 {
return &LLMResponse{}
}
choice := result.Choices[0]
resp := &LLMResponse{
Content: choice.Message.ContentString(),
FinishReason: choice.FinishReason,
}
// Convert tool calls
for _, tc := range choice.Message.ToolCalls {
var args map[string]interface{}
_ = json.Unmarshal([]byte(tc.Function.Arguments), &args)
resp.ToolCalls = append(resp.ToolCalls, ToolCall{
ID: tc.ID,
Name: tc.Function.Name,
Arguments: args,
Function: &FunctionCall{
Name: tc.Function.Name,
Arguments: tc.Function.Arguments,
},
})
}
// Convert usage
if result.Usage != nil {
resp.Usage = &UsageInfo{
PromptTokens: result.Usage.PromptTokens,
CompletionTokens: result.Usage.CompletionTokens,
TotalTokens: result.Usage.TotalTokens,
}
}
return resp
}