feat: add PicoLM local subprocess provider
This commit is contained in:
parent
394d1d1197
commit
151a9c98dc
3 changed files with 185 additions and 0 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
168
pkg/providers/picolm_provider.go
Normal file
168
pkg/providers/picolm_provider.go
Normal file
|
|
@ -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("</s>\n")
|
||||
}
|
||||
|
||||
for _, msg := range messages {
|
||||
switch msg.Role {
|
||||
case "user":
|
||||
sb.WriteString("<|user|>\n")
|
||||
sb.WriteString(msg.Content)
|
||||
sb.WriteString("</s>\n")
|
||||
case "assistant":
|
||||
sb.WriteString("<|assistant|>\n")
|
||||
sb.WriteString(msg.Content)
|
||||
sb.WriteString("</s>\n")
|
||||
case "tool":
|
||||
sb.WriteString("<|user|>\n")
|
||||
sb.WriteString(fmt.Sprintf("[Tool Result for %s]: %s", msg.ToolCallID, msg.Content))
|
||||
sb.WriteString("</s>\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
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue