From 151a9c98dc1d9958afa14e468f160e146d417524 Mon Sep 17 00:00:00 2001 From: ghstrider Date: Fri, 20 Feb 2026 01:43:27 +0530 Subject: [PATCH] feat: add PicoLM local subprocess provider --- pkg/config/config.go | 9 ++ pkg/providers/factory.go | 8 ++ pkg/providers/picolm_provider.go | 168 +++++++++++++++++++++++++++++++ 3 files changed, 185 insertions(+) create mode 100644 pkg/providers/picolm_provider.go diff --git a/pkg/config/config.go b/pkg/config/config.go index 3bdb6f030..7d2b90d32 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -263,6 +263,15 @@ type ProvidersConfig struct { ShengSuanYun ProviderConfig `json:"shengsuanyun"` DeepSeek ProviderConfig `json:"deepseek"` GitHubCopilot ProviderConfig `json:"github_copilot"` + PicoLM PicoLMProviderConfig `json:"picolm"` +} + +type PicoLMProviderConfig struct { + Binary string `json:"binary" env:"PICOCLAW_PROVIDERS_PICOLM_BINARY"` + Model string `json:"model" env:"PICOCLAW_PROVIDERS_PICOLM_MODEL"` + MaxTokens int `json:"max_tokens" env:"PICOCLAW_PROVIDERS_PICOLM_MAX_TOKENS"` + Threads int `json:"threads" env:"PICOCLAW_PROVIDERS_PICOLM_THREADS"` + Template string `json:"template" env:"PICOCLAW_PROVIDERS_PICOLM_TEMPLATE"` } type ProviderConfig struct { diff --git a/pkg/providers/factory.go b/pkg/providers/factory.go index e39cfe32b..bb1695479 100644 --- a/pkg/providers/factory.go +++ b/pkg/providers/factory.go @@ -22,6 +22,7 @@ const ( providerTypeClaudeCLI providerTypeCodexCLI providerTypeGitHubCopilot + providerTypePicoLM ) type providerSelection struct { @@ -187,6 +188,11 @@ func resolveProviderSelection(cfg *config.Config) (providerSelection, error) { sel.providerType = providerTypeCodexCLI sel.workspace = workspace return sel, nil + case "picolm": + if cfg.Providers.PicoLM.Binary != "" || cfg.Providers.PicoLM.Model != "" { + sel.providerType = providerTypePicoLM + return sel, nil + } case "deepseek": if cfg.Providers.DeepSeek.APIKey != "" { sel.apiKey = cfg.Providers.DeepSeek.APIKey @@ -354,6 +360,8 @@ func CreateProvider(cfg *config.Config) (LLMProvider, error) { return NewCodexCliProvider(sel.workspace), nil case providerTypeGitHubCopilot: return NewGitHubCopilotProvider(sel.apiBase, sel.connectMode, sel.model) + case providerTypePicoLM: + return NewPicoLMProvider(cfg.Providers.PicoLM), nil default: return NewHTTPProvider(sel.apiKey, sel.apiBase, sel.proxy), nil } diff --git a/pkg/providers/picolm_provider.go b/pkg/providers/picolm_provider.go new file mode 100644 index 000000000..45ca2cbda --- /dev/null +++ b/pkg/providers/picolm_provider.go @@ -0,0 +1,168 @@ +package providers + +import ( + "bytes" + "context" + "fmt" + "os" + "os/exec" + "strings" + + "github.com/sipeed/picoclaw/pkg/config" +) + +// PicoLMProvider implements LLMProvider by spawning the picolm binary as a subprocess. +type PicoLMProvider struct { + binary string + model string + maxTokens int + threads int + template string +} + +// NewPicoLMProvider creates a new PicoLM provider from config. +func NewPicoLMProvider(cfg config.PicoLMProviderConfig) *PicoLMProvider { + binary := expandHome(cfg.Binary) + model := expandHome(cfg.Model) + maxTokens := cfg.MaxTokens + if maxTokens <= 0 { + maxTokens = 256 + } + threads := cfg.Threads + if threads <= 0 { + threads = 4 + } + template := cfg.Template + if template == "" { + template = "chatml" + } + return &PicoLMProvider{ + binary: binary, + model: model, + maxTokens: maxTokens, + threads: threads, + template: template, + } +} + +// Chat implements LLMProvider.Chat by executing the picolm binary. +func (p *PicoLMProvider) Chat(ctx context.Context, messages []Message, tools []ToolDefinition, model string, options map[string]interface{}) (*LLMResponse, error) { + if p.binary == "" { + return nil, fmt.Errorf("picolm binary path not configured") + } + if p.model == "" { + return nil, fmt.Errorf("picolm model path not configured") + } + + prompt := p.buildPrompt(messages, tools) + + args := []string{ + p.model, + "-n", fmt.Sprintf("%d", p.maxTokens), + "-j", fmt.Sprintf("%d", p.threads), + "-t", "0.7", + } + + if len(tools) > 0 { + args = append(args, "--json") + } + + cmd := exec.CommandContext(ctx, p.binary, args...) + cmd.Stdin = bytes.NewReader([]byte(prompt)) + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + + err := cmd.Run() + + // Try to parse stdout even on non-zero exit, as picolm may print diagnostics to stderr. + if output := strings.TrimSpace(stdout.String()); output != "" { + toolCalls := extractToolCallsFromText(output) + finishReason := "stop" + content := output + if len(toolCalls) > 0 { + finishReason = "tool_calls" + content = stripToolCallsFromText(output) + } + return &LLMResponse{ + Content: strings.TrimSpace(content), + ToolCalls: toolCalls, + FinishReason: finishReason, + }, nil + } + + if err != nil { + if ctx.Err() == context.Canceled { + return nil, ctx.Err() + } + if stderrStr := stderr.String(); stderrStr != "" { + return nil, fmt.Errorf("picolm error: %s", stderrStr) + } + return nil, fmt.Errorf("picolm error: %w", err) + } + + return &LLMResponse{ + Content: "", + FinishReason: "stop", + }, nil +} + +// GetDefaultModel returns the default model identifier. +func (p *PicoLMProvider) GetDefaultModel() string { + return "picolm-local" +} + +// buildPrompt formats messages into a ChatML template for picolm. +func (p *PicoLMProvider) buildPrompt(messages []Message, tools []ToolDefinition) string { + var sb strings.Builder + + var systemParts []string + for _, msg := range messages { + if msg.Role == "system" { + systemParts = append(systemParts, msg.Content) + } + } + + if len(systemParts) > 0 { + sb.WriteString("<|system|>\n") + sb.WriteString(strings.Join(systemParts, "\n\n")) + sb.WriteString("\n") + } + + for _, msg := range messages { + switch msg.Role { + case "user": + sb.WriteString("<|user|>\n") + sb.WriteString(msg.Content) + sb.WriteString("\n") + case "assistant": + sb.WriteString("<|assistant|>\n") + sb.WriteString(msg.Content) + sb.WriteString("\n") + case "tool": + sb.WriteString("<|user|>\n") + sb.WriteString(fmt.Sprintf("[Tool Result for %s]: %s", msg.ToolCallID, msg.Content)) + sb.WriteString("\n") + } + } + + // Open assistant turn for the model to complete + sb.WriteString("<|assistant|>\n") + + return sb.String() +} + +// expandHome expands ~ to the user's home directory. +func expandHome(path string) string { + if path == "" { + return path + } + if path[0] == '~' { + home, _ := os.UserHomeDir() + if len(path) > 1 && path[1] == '/' { + return home + path[1:] + } + return home + } + return path +}