mirror of
https://github.com/sipeed/picoclaw.git
synced 2026-06-12 18:08:54 +00:00
Merge branch 'upstream-main' into feat/subturn-poc
This commit is contained in:
+18
-5
@@ -312,6 +312,7 @@ type TelegramConfig struct {
|
||||
Typing TypingConfig `json:"typing,omitempty"`
|
||||
Placeholder PlaceholderConfig `json:"placeholder,omitempty"`
|
||||
ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_TELEGRAM_REASONING_CHANNEL_ID"`
|
||||
UseMarkdownV2 bool `json:"use_markdown_v2" env:"PICOCLAW_CHANNELS_TELEGRAM_USE_MARKDOWN_V2"`
|
||||
}
|
||||
|
||||
type FeishuConfig struct {
|
||||
@@ -532,6 +533,7 @@ type ProvidersConfig struct {
|
||||
Minimax ProviderConfig `json:"minimax"`
|
||||
LongCat ProviderConfig `json:"longcat"`
|
||||
ModelScope ProviderConfig `json:"modelscope"`
|
||||
Novita ProviderConfig `json:"novita"`
|
||||
}
|
||||
|
||||
// IsEmpty checks if all provider configs are empty (no API keys or API bases set)
|
||||
@@ -560,7 +562,8 @@ func (p ProvidersConfig) IsEmpty() bool {
|
||||
p.Avian.APIKey == "" && p.Avian.APIBase == "" &&
|
||||
p.Minimax.APIKey == "" && p.Minimax.APIBase == "" &&
|
||||
p.LongCat.APIKey == "" && p.LongCat.APIBase == "" &&
|
||||
p.ModelScope.APIKey == "" && p.ModelScope.APIBase == ""
|
||||
p.ModelScope.APIKey == "" && p.ModelScope.APIBase == "" &&
|
||||
p.Novita.APIKey == "" && p.Novita.APIBase == ""
|
||||
}
|
||||
|
||||
// MarshalJSON implements custom JSON marshaling for ProvidersConfig
|
||||
@@ -590,7 +593,9 @@ type OpenAIProviderConfig struct {
|
||||
// ModelConfig represents a model-centric provider configuration.
|
||||
// It allows adding new providers (especially OpenAI-compatible ones) via configuration only.
|
||||
// The model field uses protocol prefix format: [protocol/]model-identifier
|
||||
// Supported protocols: openai, anthropic, antigravity, claude-cli, codex-cli, github-copilot
|
||||
// Supported protocols include openai, anthropic, antigravity, claude-cli,
|
||||
// codex-cli, github-copilot, and named OpenAI-compatible protocols such as
|
||||
// groq, deepseek, modelscope, and novita.
|
||||
// Default protocol is "openai" if no prefix is specified.
|
||||
type ModelConfig struct {
|
||||
// Required fields
|
||||
@@ -694,10 +699,18 @@ type WebToolsConfig struct {
|
||||
Perplexity PerplexityConfig ` json:"perplexity"`
|
||||
SearXNG SearXNGConfig ` json:"searxng"`
|
||||
GLMSearch GLMSearchConfig ` json:"glm_search"`
|
||||
// PreferNative controls whether to use provider-native web search when
|
||||
// the active LLM supports it (e.g. OpenAI web_search_preview). When true,
|
||||
// the client-side web_search tool is hidden to avoid duplicate search surfaces,
|
||||
// and the provider's built-in search is used instead. Falls back to client-side
|
||||
// search when the provider does not support native search.
|
||||
PreferNative bool `json:"prefer_native" env:"PICOCLAW_TOOLS_WEB_PREFER_NATIVE"`
|
||||
// Proxy is an optional proxy URL for web tools (http/https/socks5/socks5h).
|
||||
// For authenticated proxies, prefer HTTP_PROXY/HTTPS_PROXY env vars instead of embedding credentials in config.
|
||||
Proxy string `json:"proxy,omitempty" env:"PICOCLAW_TOOLS_WEB_PROXY"`
|
||||
FetchLimitBytes int64 `json:"fetch_limit_bytes,omitempty" env:"PICOCLAW_TOOLS_WEB_FETCH_LIMIT_BYTES"`
|
||||
Proxy string `json:"proxy,omitempty" env:"PICOCLAW_TOOLS_WEB_PROXY"`
|
||||
FetchLimitBytes int64 `json:"fetch_limit_bytes,omitempty" env:"PICOCLAW_TOOLS_WEB_FETCH_LIMIT_BYTES"`
|
||||
Format string `json:"format,omitempty" env:"PICOCLAW_TOOLS_WEB_FORMAT"`
|
||||
PrivateHostWhitelist FlexibleStringSlice `json:"private_host_whitelist,omitempty" env:"PICOCLAW_TOOLS_WEB_PRIVATE_HOST_WHITELIST"`
|
||||
}
|
||||
|
||||
type CronToolsConfig struct {
|
||||
@@ -1030,7 +1043,7 @@ func (c *Config) GetModelConfig(modelName string) (*ModelConfig, error) {
|
||||
}
|
||||
|
||||
// Multiple configs - use round-robin for load balancing
|
||||
idx := rrCounter.Add(1) % uint64(len(matches))
|
||||
idx := (rrCounter.Add(1) - 1) % uint64(len(matches))
|
||||
return &matches[idx], nil
|
||||
}
|
||||
|
||||
|
||||
@@ -77,6 +77,22 @@ func TestAgentModelConfig_MarshalObject(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestProvidersConfig_IsEmpty(t *testing.T) {
|
||||
var empty ProvidersConfig
|
||||
if !empty.IsEmpty() {
|
||||
t.Fatal("empty ProvidersConfig should report empty")
|
||||
}
|
||||
|
||||
novita := ProvidersConfig{
|
||||
Novita: ProviderConfig{
|
||||
APIKey: "test-key",
|
||||
},
|
||||
}
|
||||
if novita.IsEmpty() {
|
||||
t.Fatal("ProvidersConfig with novita settings should not report empty")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentConfig_FullParse(t *testing.T) {
|
||||
jsonData := `{
|
||||
"agents": {
|
||||
@@ -401,6 +417,45 @@ func TestDefaultConfig_OpenAIWebSearchEnabled(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultConfig_WebPreferNativeEnabled(t *testing.T) {
|
||||
cfg := DefaultConfig()
|
||||
if !cfg.Tools.Web.PreferNative {
|
||||
t.Fatal("DefaultConfig().Tools.Web.PreferNative should be true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadConfig_WebPreferNativeDefaultsTrueWhenUnset(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
configPath := filepath.Join(dir, "config.json")
|
||||
if err := os.WriteFile(configPath, []byte(`{"tools":{"web":{"enabled":true}}}`), 0o600); err != nil {
|
||||
t.Fatalf("WriteFile() error: %v", err)
|
||||
}
|
||||
|
||||
cfg, err := LoadConfig(configPath)
|
||||
if err != nil {
|
||||
t.Fatalf("LoadConfig() error: %v", err)
|
||||
}
|
||||
if !cfg.Tools.Web.PreferNative {
|
||||
t.Fatal("PreferNative should remain true when unset in config file")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadConfig_WebPreferNativeCanBeDisabled(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
configPath := filepath.Join(dir, "config.json")
|
||||
if err := os.WriteFile(configPath, []byte(`{"tools":{"web":{"prefer_native":false}}}`), 0o600); err != nil {
|
||||
t.Fatalf("WriteFile() error: %v", err)
|
||||
}
|
||||
|
||||
cfg, err := LoadConfig(configPath)
|
||||
if err != nil {
|
||||
t.Fatalf("LoadConfig() error: %v", err)
|
||||
}
|
||||
if cfg.Tools.Web.PreferNative {
|
||||
t.Fatal("PreferNative should be false when disabled in config file")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultConfig_ExecAllowRemoteEnabled(t *testing.T) {
|
||||
cfg := DefaultConfig()
|
||||
if !cfg.Tools.Exec.AllowRemote {
|
||||
|
||||
@@ -15,7 +15,7 @@ func DefaultConfig() *Config {
|
||||
// Determine the base path for the workspace.
|
||||
// Priority: $PICOCLAW_HOME > ~/.picoclaw
|
||||
var homePath string
|
||||
if picoclawHome := os.Getenv("PICOCLAW_HOME"); picoclawHome != "" {
|
||||
if picoclawHome := os.Getenv(EnvHome); picoclawHome != "" {
|
||||
homePath = picoclawHome
|
||||
} else {
|
||||
userHome, _ := os.UserHomeDir()
|
||||
@@ -59,6 +59,7 @@ func DefaultConfig() *Config {
|
||||
Enabled: true,
|
||||
Text: "Thinking... 💭",
|
||||
},
|
||||
UseMarkdownV2: false,
|
||||
},
|
||||
Feishu: FeishuConfig{
|
||||
Enabled: false,
|
||||
@@ -412,8 +413,10 @@ func DefaultConfig() *Config {
|
||||
ToolConfig: ToolConfig{
|
||||
Enabled: true,
|
||||
},
|
||||
PreferNative: true,
|
||||
Proxy: "",
|
||||
FetchLimitBytes: 10 * 1024 * 1024, // 10MB by default
|
||||
Format: "plaintext",
|
||||
Brave: BraveConfig{
|
||||
Enabled: false,
|
||||
APIKey: "",
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
// PicoClaw - Ultra-lightweight personal AI agent
|
||||
// License: MIT
|
||||
//
|
||||
// Copyright (c) 2026 PicoClaw contributors
|
||||
|
||||
package config
|
||||
|
||||
// Runtime environment variable keys for the picoclaw process.
|
||||
// These control the location of files and binaries at runtime and are read
|
||||
// directly via os.Getenv / os.LookupEnv. All picoclaw-specific keys use the
|
||||
// PICOCLAW_ prefix. Reference these constants instead of inline string
|
||||
// literals to keep all supported knobs visible in one place and to prevent
|
||||
// typos.
|
||||
const (
|
||||
// EnvHome overrides the base directory for all picoclaw data
|
||||
// (config, workspace, skills, auth store, …).
|
||||
// Default: ~/.picoclaw
|
||||
EnvHome = "PICOCLAW_HOME"
|
||||
|
||||
// EnvConfig overrides the full path to the JSON config file.
|
||||
// Default: $PICOCLAW_HOME/config.json
|
||||
EnvConfig = "PICOCLAW_CONFIG"
|
||||
|
||||
// EnvBuiltinSkills overrides the directory from which built-in
|
||||
// skills are loaded.
|
||||
// Default: <cwd>/skills
|
||||
EnvBuiltinSkills = "PICOCLAW_BUILTIN_SKILLS"
|
||||
|
||||
// EnvBinary overrides the path to the picoclaw executable.
|
||||
// Used by the web launcher when spawning the gateway subprocess.
|
||||
// Default: resolved from the same directory as the current executable.
|
||||
EnvBinary = "PICOCLAW_BINARY"
|
||||
|
||||
// EnvGatewayHost overrides the host address for the gateway server.
|
||||
// Default: "127.0.0.1"
|
||||
EnvGatewayHost = "PICOCLAW_GATEWAY_HOST"
|
||||
)
|
||||
@@ -80,6 +80,36 @@ func TestGetModelConfig_RoundRobin(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetModelConfig_RoundRobinStartsFromFirstMatch(t *testing.T) {
|
||||
rrCounter.Store(0)
|
||||
|
||||
cfg := &Config{
|
||||
ModelList: []ModelConfig{
|
||||
{ModelName: "lb-model", Model: "openai/gpt-4o-1", APIKey: "key1"},
|
||||
{ModelName: "lb-model", Model: "openai/gpt-4o-2", APIKey: "key2"},
|
||||
{ModelName: "lb-model", Model: "openai/gpt-4o-3", APIKey: "key3"},
|
||||
},
|
||||
}
|
||||
|
||||
wantOrder := []string{
|
||||
"openai/gpt-4o-1",
|
||||
"openai/gpt-4o-2",
|
||||
"openai/gpt-4o-3",
|
||||
"openai/gpt-4o-1",
|
||||
"openai/gpt-4o-2",
|
||||
}
|
||||
|
||||
for i, want := range wantOrder {
|
||||
result, err := cfg.GetModelConfig("lb-model")
|
||||
if err != nil {
|
||||
t.Fatalf("GetModelConfig() call %d error = %v", i, err)
|
||||
}
|
||||
if result.Model != want {
|
||||
t.Fatalf("GetModelConfig() call %d model = %q, want %q", i, result.Model, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetModelConfig_Concurrent(t *testing.T) {
|
||||
cfg := &Config{
|
||||
ModelList: []ModelConfig{
|
||||
|
||||
Reference in New Issue
Block a user