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"`
|
ShengSuanYun ProviderConfig `json:"shengsuanyun"`
|
||||||
DeepSeek ProviderConfig `json:"deepseek"`
|
DeepSeek ProviderConfig `json:"deepseek"`
|
||||||
GitHubCopilot ProviderConfig `json:"github_copilot"`
|
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 {
|
type ProviderConfig struct {
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ const (
|
||||||
providerTypeClaudeCLI
|
providerTypeClaudeCLI
|
||||||
providerTypeCodexCLI
|
providerTypeCodexCLI
|
||||||
providerTypeGitHubCopilot
|
providerTypeGitHubCopilot
|
||||||
|
providerTypePicoLM
|
||||||
)
|
)
|
||||||
|
|
||||||
type providerSelection struct {
|
type providerSelection struct {
|
||||||
|
|
@ -187,6 +188,11 @@ func resolveProviderSelection(cfg *config.Config) (providerSelection, error) {
|
||||||
sel.providerType = providerTypeCodexCLI
|
sel.providerType = providerTypeCodexCLI
|
||||||
sel.workspace = workspace
|
sel.workspace = workspace
|
||||||
return sel, nil
|
return sel, nil
|
||||||
|
case "picolm":
|
||||||
|
if cfg.Providers.PicoLM.Binary != "" || cfg.Providers.PicoLM.Model != "" {
|
||||||
|
sel.providerType = providerTypePicoLM
|
||||||
|
return sel, nil
|
||||||
|
}
|
||||||
case "deepseek":
|
case "deepseek":
|
||||||
if cfg.Providers.DeepSeek.APIKey != "" {
|
if cfg.Providers.DeepSeek.APIKey != "" {
|
||||||
sel.apiKey = 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
|
return NewCodexCliProvider(sel.workspace), nil
|
||||||
case providerTypeGitHubCopilot:
|
case providerTypeGitHubCopilot:
|
||||||
return NewGitHubCopilotProvider(sel.apiBase, sel.connectMode, sel.model)
|
return NewGitHubCopilotProvider(sel.apiBase, sel.connectMode, sel.model)
|
||||||
|
case providerTypePicoLM:
|
||||||
|
return NewPicoLMProvider(cfg.Providers.PicoLM), nil
|
||||||
default:
|
default:
|
||||||
return NewHTTPProvider(sel.apiKey, sel.apiBase, sel.proxy), nil
|
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