fix(providers): resolve protocol parsing crash and restore thinking support

This commit is contained in:
stevef 2026-04-20 08:47:13 +02:00
parent e39f85b37b
commit 71a1a2b2c4
7 changed files with 256 additions and 22 deletions

View file

@ -622,7 +622,7 @@ data:
"api_key": "picoclaw-secret-123",
"chat_enabled": true,
"hot_reload": true,
"log_level": "info"
"log_level": "debug"
},
"hooks": {
"enabled": true,

View file

@ -26,11 +26,20 @@ func NewAgentLoop(
msgBus *bus.MessageBus,
provider providers.LLMProvider,
) *AgentLoop {
logger.Debug("Initializing AgentRegistry...")
registry := NewAgentRegistry(cfg, provider)
// Set up shared fallback chain with rate limiting.
cooldownPath := filepath.Join(filepath.Dir(filepath.Clean(registry.GetDefaultAgent().Workspace)), "cooldowns.json")
// Harden: ensure we have a valid workspace for the cooldown file even if the default agent isn't ready.
baseDir := filepath.Dir(configPath)
if defaultAgent := registry.GetDefaultAgent(); defaultAgent != nil {
baseDir = filepath.Dir(filepath.Clean(defaultAgent.Workspace))
}
cooldownPath := filepath.Join(baseDir, "cooldowns.json")
logger.Debugf("Initializing CooldownTracker at %s", cooldownPath)
cooldown := providers.NewCooldownTracker(cooldownPath)
rl := providers.NewRateLimiterRegistry()
// Register rate limiters for all agents' candidates so that RPM limits
// configured in ModelConfig are enforced before each LLM call.
@ -46,6 +55,7 @@ func NewAgentLoop(
defaultAgent := registry.GetDefaultAgent()
var stateManager *state.Manager
if defaultAgent != nil {
logger.Debugf("Initializing State Manager for agent %s", defaultAgent.ID)
stateManager = state.NewManager(defaultAgent.Workspace)
}

View file

@ -114,24 +114,46 @@ func (p *startupBlockedProvider) GetDefaultModel() string {
}
// Run starts the gateway runtime using the configuration loaded from configPath.
func Run(debug bool, homePath, configPath string, allowEmptyStartup bool) (runErr error) {
panicPath := filepath.Join(homePath, logPath, panicFile)
panicFunc, err := logger.InitPanic(panicPath)
if err != nil {
return fmt.Errorf("error initializing panic log: %w", err)
}
defer panicFunc()
func Run(
debug bool,
homePath string,
configPath string,
allowEmptyStartup bool,
) (runErr error) {
fmt.Printf("📂 Home Path: %s\n", homePath)
fmt.Printf("📄 Config Path: %s\n", configPath)
if err = logger.EnableFileLogging(filepath.Join(homePath, logPath, logFile)); err != nil {
// Ensure home directory exists early
if err := os.MkdirAll(homePath, 0o755); err != nil {
return fmt.Errorf("failed to create home directory: %w", err)
}
// Initialize panic logging
panicPath := filepath.Join(homePath, logPath, panicFile)
fmt.Printf("🔧 Initializing panic log: %s\n", panicPath)
panicCleanup, err := logger.InitPanic(panicPath)
if err != nil {
return fmt.Errorf("failed to initialize panic log: %w", err)
}
defer panicCleanup()
fmt.Println("✓ Panic log initialized")
// Enable main file logging
mainLogPath := filepath.Join(homePath, logPath, logFile)
fmt.Printf("🔧 Enabling file logging: %s\n", mainLogPath)
if err = logger.EnableFileLogging(mainLogPath); err != nil {
logger.Fatal(fmt.Sprintf("error enabling file logging: %v", err))
}
defer logger.DisableFileLogging()
fmt.Println("✓ File logging enabled")
// Set initial log level from config if possible, otherwise default to INFO
if debug {
logger.SetLevel(logger.DEBUG)
} else {
logger.SetLevelFromString(config.ResolveGatewayLogLevel(configPath))
}
defer func() {
if runErr != nil {
logger.ErrorCF("gateway", "Gateway startup failed", map[string]any{
@ -144,10 +166,12 @@ func Run(debug bool, homePath, configPath string, allowEmptyStartup bool) (runEr
}
}()
fmt.Println("🔍 Loading configuration...")
cfg, err := config.LoadConfig(configPath)
if err != nil {
return fmt.Errorf("error loading config: %w", err)
}
logger.Info("✓ Configuration loaded")
if err = preCheckConfig(cfg); err != nil {
return fmt.Errorf("config pre-check failed: %w", err)
@ -156,6 +180,7 @@ func Run(debug bool, homePath, configPath string, allowEmptyStartup bool) (runEr
// Debug mode permanently overrides the config log level to DEBUG.
if debug {
fmt.Println("🔍 Debug mode enabled")
logger.SetLevel(logger.DEBUG)
} else {
effectiveLogLevel := config.EffectiveGatewayLogLevel(cfg)
logger.SetLevelFromString(effectiveLogLevel)
@ -167,7 +192,7 @@ func Run(debug bool, homePath, configPath string, allowEmptyStartup bool) (runEr
return fmt.Errorf("error opening gateway listeners: %w", err)
}
// Enforce singleton: write PID file with generated token.
// Enforce singleton and generate auth token
pidData, err := pid.WritePidFile(homePath, bindPlan.ProbeHost, cfg.Gateway.Port)
if err != nil {
logger.Warnf("write pid file failed: %v", err)
@ -177,6 +202,8 @@ func Run(debug bool, homePath, configPath string, allowEmptyStartup bool) (runEr
return fmt.Errorf("singleton check failed: %w", err)
}
defer pid.RemovePidFile(homePath)
logger.Info("✓ PID file and auth token initialized")
closeListeners := true
defer func() {
if !closeListeners {
@ -187,17 +214,26 @@ func Run(debug bool, homePath, configPath string, allowEmptyStartup bool) (runEr
}
}()
msgBus := bus.NewMessageBus()
provider, modelID, err := createStartupProvider(cfg, allowEmptyStartup)
if err != nil {
return fmt.Errorf("error creating provider: %w", err)
}
logger.Infof("✓ LLM Provider initialized (Model: %s)", modelID)
if modelID != "" {
cfg.Agents.Defaults.ModelName = modelID
}
msgBus := bus.NewMessageBus()
agentLoop := agent.NewAgentLoop(cfg, configPath, msgBus, provider)
logger.Info("✓ Agent loop initialized")
runningServices, err := setupAndStartServices(cfg, agentLoop, msgBus, pidData.Token, listenResult)
if err != nil {
return fmt.Errorf("error setting up services: %w", err)
}
logger.Info("✓ Core services started")
fmt.Println("\n📦 Agent Status:")
startupInfo := agentLoop.GetStartupInfo()
@ -212,11 +248,6 @@ func Run(debug bool, homePath, configPath string, allowEmptyStartup bool) (runEr
"skills_total": skillsInfo["total"],
"skills_available": skillsInfo["available"],
})
runningServices, err := setupAndStartServices(cfg, agentLoop, msgBus, pidData.Token, listenResult)
if err != nil {
return err
}
closeListeners = false
// Setup manual reload channel for /reload endpoint

View file

@ -58,6 +58,29 @@ var protocolMetaByName = map[string]protocolMeta{
"longcat": {defaultAPIBase: "https://api.longcat.chat/openai"},
"modelscope": {defaultAPIBase: "https://api-inference.modelscope.cn/v1"},
"mimo": {defaultAPIBase: "https://api.xiaomimimo.com/v1"},
// Specialty and Custom Protocols
"anthropic": {defaultAPIBase: "https://api.anthropic.com"},
"google": {defaultAPIBase: "https://openrouter.ai/api/v1"}, // Alias for OpenRouter/OpenAI-compatible
"elevenlabs": {},
"claude-cli": {},
"codex-cli": {},
"antigravity": {},
"cli": {},
"fs": {},
"memory": {},
"dummy": {},
"openai-tts": {},
"gpt-4-v": {defaultAPIBase: "https://api.openai.com/v1"},
"gpt-4o": {defaultAPIBase: "https://api.openai.com/v1"},
"gpt-4-turbo": {defaultAPIBase: "https://api.openai.com/v1"},
"azure": {},
"azure-openai": {},
"bedrock": {},
"github-copilot": {},
"github-copilot-chat": {},
"copilot": {},
"claude": {defaultAPIBase: "https://api.anthropic.com"},
}
// createClaudeAuthProvider creates a Claude provider using OAuth credentials from auth store.
@ -88,15 +111,15 @@ func createCodexAuthProvider() (LLMProvider, error) {
// If no prefix is specified, it defaults to "openai".
// Examples:
// - "openai/gpt-4o" -> ("openai", "gpt-4o")
// - "anthropic/claude-sonnet-4.6" -> ("anthropic", "claude-sonnet-4.6")
// - "gpt-4o" -> ("openai", "gpt-4o") // default protocol
// - "anthropic/claude-3-opus" -> ("anthropic", "claude-3-opus")
// - "gpt-4o" -> ("openai", "gpt-4o")
func ExtractProtocol(model string) (protocol, modelID string) {
model = strings.TrimSpace(model)
protocol, modelID, found := strings.Cut(model, "/")
p, m, found := strings.Cut(model, "/")
if !found {
return "openai", model
}
return protocol, modelID
return p, m
}
// ResolveAPIBase returns the configured API base, or the protocol default when
@ -133,7 +156,13 @@ func CreateProviderFromConfig(cfg *config.ModelConfig) (LLMProvider, string, err
protocol, modelID := ExtractProtocol(cfg.Model)
if cfg.Protocol != "" {
protocol = cfg.Protocol
modelID = cfg.Model
// If protocol was explicitly set, modelID should be the full model string
// unless it was already prefixed with the SAME protocol.
if p, m, found := strings.Cut(cfg.Model, "/"); found && strings.EqualFold(p, protocol) {
modelID = m
} else {
modelID = cfg.Model
}
}
userAgent := cfg.UserAgent

View file

@ -74,6 +74,10 @@ func (p *HTTPProvider) GetDefaultModel() string {
return ""
}
func (p *HTTPProvider) SupportsThinking() bool {
return p.delegate.SupportsThinking()
}
func (p *HTTPProvider) SupportsNativeSearch() bool {
return p.delegate.SupportsNativeSearch()
}

View file

@ -180,6 +180,32 @@ func (p *Provider) buildRequestBody(
}
}
// Thinking support for OpenRouter
if level, ok := options["thinking_level"].(string); ok && level != "" && level != "off" {
if u, err := url.Parse(p.apiBase); err == nil && u.Hostname() == "openrouter.ai" {
// Map level to budget tokens (using same scale as Anthropic)
budget := 0
switch level {
case "low":
budget = 4096
case "medium":
budget = 16384
case "high":
budget = 32000
case "xhigh":
budget = 64000
}
if budget > 0 {
requestBody["thinking"] = map[string]any{
"type": "enabled",
"budget_tokens": budget,
}
// Remove temperature when thinking is enabled (strict requirement for some models)
delete(requestBody, "temperature")
}
}
}
// Merge extra body fields configured per-provider/model.
// These are injected last so they take precedence over defaults.
maps.Copy(requestBody, p.extraBody)
@ -466,6 +492,16 @@ func buildToolsList(tools []ToolDefinition, nativeSearch bool) []any {
return result
}
func (p *Provider) SupportsThinking() bool {
// Enable thinking support for OpenRouter.
// OpenRouter acts as a passthrough for Anthropic and DeepSeek-R1 models that support thinking.
u, err := url.Parse(p.apiBase)
if err != nil {
return false
}
return u.Hostname() == "openrouter.ai"
}
func (p *Provider) SupportsNativeSearch() bool {
return isNativeSearchHost(p.apiBase)
}

124
test_results.txt Normal file
View file

@ -0,0 +1,124 @@
# v3.466
no required module provides package v3.466; to add it:
go get v3.466
FAIL v3.466 [setup failed]
# Balancing
package Balancing is not in std (/usr/local/go/src/Balancing)
FAIL Balancing [setup failed]
# Makefile
package Makefile is not in std (/usr/local/go/src/Makefile)
FAIL Makefile [setup failed]
# across
package across is not in std (/usr/local/go/src/across)
FAIL across [setup failed]
# components.
malformed import path "components.": trailing dot in path element
FAIL components. [setup failed]
? github.com/sipeed/picoclaw/cmd/freeride-diag [no test files]
ok github.com/sipeed/picoclaw/cmd/membench (cached)
ok github.com/sipeed/picoclaw/cmd/picoclaw (cached)
ok github.com/sipeed/picoclaw/cmd/picoclaw/internal (cached)
ok github.com/sipeed/picoclaw/cmd/picoclaw/internal/agent (cached)
ok github.com/sipeed/picoclaw/cmd/picoclaw/internal/auth (cached)
ok github.com/sipeed/picoclaw/cmd/picoclaw/internal/cliui (cached)
ok github.com/sipeed/picoclaw/cmd/picoclaw/internal/cron (cached)
? github.com/sipeed/picoclaw/cmd/picoclaw/internal/freeride [no test files]
ok github.com/sipeed/picoclaw/cmd/picoclaw/internal/gateway (cached)
ok github.com/sipeed/picoclaw/cmd/picoclaw/internal/migrate (cached)
ok github.com/sipeed/picoclaw/cmd/picoclaw/internal/model (cached)
ok github.com/sipeed/picoclaw/cmd/picoclaw/internal/onboard (cached)
ok github.com/sipeed/picoclaw/cmd/picoclaw/internal/skills (cached)
ok github.com/sipeed/picoclaw/cmd/picoclaw/internal/status (cached)
ok github.com/sipeed/picoclaw/cmd/picoclaw/internal/version (cached)
? github.com/sipeed/picoclaw/cmd/picoclaw-launcher-tui [no test files]
? github.com/sipeed/picoclaw/cmd/picoclaw-launcher-tui/config [no test files]
? github.com/sipeed/picoclaw/cmd/picoclaw-launcher-tui/ui [no test files]
? github.com/sipeed/picoclaw/examples/pico-echo-server [no test files]
? github.com/sipeed/picoclaw/pkg [no test files]
ok github.com/sipeed/picoclaw/pkg/agent (cached)
ok github.com/sipeed/picoclaw/pkg/audio (cached)
ok github.com/sipeed/picoclaw/pkg/audio/asr (cached)
ok github.com/sipeed/picoclaw/pkg/audio/tts (cached)
ok github.com/sipeed/picoclaw/pkg/auth (cached)
ok github.com/sipeed/picoclaw/pkg/bus (cached)
ok github.com/sipeed/picoclaw/pkg/channels (cached)
ok github.com/sipeed/picoclaw/pkg/channels/dingtalk (cached)
ok github.com/sipeed/picoclaw/pkg/channels/discord (cached)
ok github.com/sipeed/picoclaw/pkg/channels/feishu (cached)
ok github.com/sipeed/picoclaw/pkg/channels/irc (cached)
ok github.com/sipeed/picoclaw/pkg/channels/line (cached)
? github.com/sipeed/picoclaw/pkg/channels/maixcam [no test files]
ok github.com/sipeed/picoclaw/pkg/channels/matrix (cached)
? github.com/sipeed/picoclaw/pkg/channels/onebot [no test files]
ok github.com/sipeed/picoclaw/pkg/channels/pico (cached)
ok github.com/sipeed/picoclaw/pkg/channels/qq (cached)
ok github.com/sipeed/picoclaw/pkg/channels/slack (cached)
ok github.com/sipeed/picoclaw/pkg/channels/teams_webhook (cached)
ok github.com/sipeed/picoclaw/pkg/channels/telegram (cached)
ok github.com/sipeed/picoclaw/pkg/channels/vk (cached)
ok github.com/sipeed/picoclaw/pkg/channels/wecom (cached)
ok github.com/sipeed/picoclaw/pkg/channels/weixin (cached)
ok github.com/sipeed/picoclaw/pkg/channels/whatsapp (cached)
? github.com/sipeed/picoclaw/pkg/channels/whatsapp_native [no test files]
ok github.com/sipeed/picoclaw/pkg/commands (cached)
ok github.com/sipeed/picoclaw/pkg/config (cached)
? github.com/sipeed/picoclaw/pkg/constants [no test files]
ok github.com/sipeed/picoclaw/pkg/credential (cached)
ok github.com/sipeed/picoclaw/pkg/cron (cached)
? github.com/sipeed/picoclaw/pkg/devices [no test files]
? github.com/sipeed/picoclaw/pkg/devices/events [no test files]
? github.com/sipeed/picoclaw/pkg/devices/sources [no test files]
ok github.com/sipeed/picoclaw/pkg/fileutil (cached)
ok github.com/sipeed/picoclaw/pkg/gateway (cached)
ok github.com/sipeed/picoclaw/pkg/health (cached)
ok github.com/sipeed/picoclaw/pkg/heartbeat (cached)
ok github.com/sipeed/picoclaw/pkg/identity (cached)
ok github.com/sipeed/picoclaw/pkg/isolation (cached)
ok github.com/sipeed/picoclaw/pkg/logger (cached)
ok github.com/sipeed/picoclaw/pkg/mcp (cached)
ok github.com/sipeed/picoclaw/pkg/media (cached)
ok github.com/sipeed/picoclaw/pkg/memory (cached)
ok github.com/sipeed/picoclaw/pkg/migrate (cached)
ok github.com/sipeed/picoclaw/pkg/migrate/internal (cached)
ok github.com/sipeed/picoclaw/pkg/migrate/sources/openclaw (cached)
ok github.com/sipeed/picoclaw/pkg/netbind (cached)
ok github.com/sipeed/picoclaw/pkg/pid (cached)
ok github.com/sipeed/picoclaw/pkg/providers (cached)
ok github.com/sipeed/picoclaw/pkg/providers/anthropic (cached)
ok github.com/sipeed/picoclaw/pkg/providers/anthropic_messages (cached)
ok github.com/sipeed/picoclaw/pkg/providers/azure (cached)
ok github.com/sipeed/picoclaw/pkg/providers/bedrock (cached)
ok github.com/sipeed/picoclaw/pkg/providers/cli (cached)
ok github.com/sipeed/picoclaw/pkg/providers/common (cached)
ok github.com/sipeed/picoclaw/pkg/providers/httpapi (cached)
ok github.com/sipeed/picoclaw/pkg/providers/oauth (cached)
ok github.com/sipeed/picoclaw/pkg/providers/openai_compat (cached)
ok github.com/sipeed/picoclaw/pkg/providers/openai_responses_common (cached)
? github.com/sipeed/picoclaw/pkg/providers/protocoltypes [no test files]
ok github.com/sipeed/picoclaw/pkg/routing (cached)
ok github.com/sipeed/picoclaw/pkg/seahorse (cached)
ok github.com/sipeed/picoclaw/pkg/security (cached)
ok github.com/sipeed/picoclaw/pkg/security/behavior (cached)
ok github.com/sipeed/picoclaw/pkg/security/canary (cached)
ok github.com/sipeed/picoclaw/pkg/security/ipia (cached)
ok github.com/sipeed/picoclaw/pkg/security/pii (cached)
ok github.com/sipeed/picoclaw/pkg/security/policy (cached)
ok github.com/sipeed/picoclaw/pkg/session (cached)
ok github.com/sipeed/picoclaw/pkg/skills (cached)
ok github.com/sipeed/picoclaw/pkg/state (cached)
? github.com/sipeed/picoclaw/pkg/tokenizer [no test files]
ok github.com/sipeed/picoclaw/pkg/tools (cached)
ok github.com/sipeed/picoclaw/pkg/tools/fs (cached)
? github.com/sipeed/picoclaw/pkg/tools/hardware [no test files]
ok github.com/sipeed/picoclaw/pkg/tools/integration (cached)
? github.com/sipeed/picoclaw/pkg/tools/shared [no test files]
ok github.com/sipeed/picoclaw/pkg/updater (cached)
ok github.com/sipeed/picoclaw/pkg/utils (cached)
ok github.com/sipeed/picoclaw/web/backend (cached)
ok github.com/sipeed/picoclaw/web/backend/api (cached)
? github.com/sipeed/picoclaw/web/backend/dashboardauth [no test files]
ok github.com/sipeed/picoclaw/web/backend/launcherconfig (cached)
ok github.com/sipeed/picoclaw/web/backend/middleware (cached)
? github.com/sipeed/picoclaw/web/backend/model [no test files]
ok github.com/sipeed/picoclaw/web/backend/utils (cached)
FAIL