From 0200000ff06a7f5910ba2aca9da1f98c8cdded00 Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 30 Apr 2026 13:04:53 +0200 Subject: [PATCH] feat: finalize freeride cooldown management and fix broken tool calls --- k3s/configmap.yaml | 25 +++---------------- pkg/agent/instance.go | 3 ++- pkg/agent/loop_init.go | 2 +- pkg/tools/freeride.go | 45 ++++++++++++++++++++++++++++------ pkg/tools/freeride_test.go | 50 ++++++++++++++++++++++++++++++++++---- 5 files changed, 89 insertions(+), 36 deletions(-) diff --git a/k3s/configmap.yaml b/k3s/configmap.yaml index e392400e1..ece57c80e 100644 --- a/k3s/configmap.yaml +++ b/k3s/configmap.yaml @@ -18,8 +18,7 @@ data: "model_name": "nemotron-120b", "model_fallbacks": [ "meta-llama-llama-3.3-70b-instruct:free", - "qwen-qwen3-coder:free", - "openrouter-elephant-alpha" + "qwen-qwen3-coder:free" ], "max_tokens": 32768, "max_tool_iterations": 50, @@ -305,16 +304,7 @@ data: ], "request_timeout": 45 }, - { - "model_name": "openrouter-elephant", - "model": "openrouter/elephant-alpha", - "protocol": "openrouter", - "api_base": "https://openrouter.ai/api/v1", - "api_keys": [ - "env://OPENROUTER_API_KEY" - ], - "request_timeout": 45 - }, + { "model_name": "openrouter-free", "model": "arcee-ai/trinity-large-preview:free", @@ -587,16 +577,7 @@ data: ], "request_timeout": 45 }, - { - "model_name": "openrouter-elephant-alpha", - "model": "openrouter/elephant-alpha", - "protocol": "openrouter", - "enabled": true, - "api_keys": [ - "env://OPENROUTER_API_KEY" - ], - "request_timeout": 45 - }, + { "model_name": "minimax-minimax-m2.5:free", "model": "minimax/minimax-m2.5:free", diff --git a/pkg/agent/instance.go b/pkg/agent/instance.go index 7e874b92e..41cdb094d 100644 --- a/pkg/agent/instance.go +++ b/pkg/agent/instance.go @@ -118,7 +118,8 @@ func NewAgentInstance( toolsRegistry.Register(tools.NewAppendFileTool(workspace, restrict, allowWritePaths)) } if cfg.Tools.IsToolEnabled("freeride") { - toolsRegistry.Register(tools.NewFreeRideTool(config.GetDefaultConfigPath(), nil)) + cdPath := filepath.Join(filepath.Dir(filepath.Clean(workspace)), "cooldowns.json") + toolsRegistry.Register(tools.NewFreeRideTool(config.GetDefaultConfigPath(), cdPath, nil)) } sessionsDir := filepath.Join(workspace, "sessions") diff --git a/pkg/agent/loop_init.go b/pkg/agent/loop_init.go index 4175f3764..326ae0aeb 100644 --- a/pkg/agent/loop_init.go +++ b/pkg/agent/loop_init.go @@ -249,7 +249,7 @@ func registerSharedTools( // Skill discovery and installation tools skills_enabled := cfg.Tools.IsToolEnabled("skills") if skills_enabled { - agent.Tools.Register(tools.NewFreeRideTool(al.GetConfigPath(), al.GetReloadFunc())) + agent.Tools.Register(tools.NewFreeRideTool(al.GetConfigPath(), al.cooldownPath, al.GetReloadFunc())) } find_skills_enable := cfg.Tools.IsToolEnabled("find_skills") diff --git a/pkg/tools/freeride.go b/pkg/tools/freeride.go index ba92cc71c..e83cc4c74 100644 --- a/pkg/tools/freeride.go +++ b/pkg/tools/freeride.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "net/http" + "os" "sort" "strings" "time" @@ -15,14 +16,16 @@ import ( // FreeRideTool adapts the FreeRide logic (from clawhub/free-ride) for PicoClaw. // It manages OpenRouter's free models and configures them as fallbacks. type FreeRideTool struct { - configPath string - reloadFunc func() error + configPath string + cooldownPath string + reloadFunc func() error } -func NewFreeRideTool(configPath string, reloadFunc func() error) *FreeRideTool { +func NewFreeRideTool(configPath, cooldownPath string, reloadFunc func() error) *FreeRideTool { return &FreeRideTool{ - configPath: configPath, - reloadFunc: reloadFunc, + configPath: configPath, + cooldownPath: cooldownPath, + reloadFunc: reloadFunc, } } @@ -41,8 +44,8 @@ func (t *FreeRideTool) Parameters() map[string]any { "properties": map[string]any{ "command": map[string]any{ "type": "string", - "enum": []string{"auto", "list", "status", "settimeout"}, - "description": "The command to run: 'auto' (configures models), 'list' (shows free models), 'status' (checks current setup), 'settimeout' (sets request timeout)", + "enum": []string{"auto", "list", "status", "settimeout", "clear"}, + "description": "The command to run: 'auto' (configures models), 'list' (shows free models), 'status' (checks current setup), 'settimeout' (sets request timeout), 'clear' (resets model cooldowns)", }, "limit": map[string]any{ "type": "integer", @@ -94,11 +97,39 @@ func (t *FreeRideTool) Execute(ctx context.Context, args map[string]any) *ToolRe return t.handleStatus() case "settimeout": return t.handleSetTimeout(timeout) + case "clear": + return t.handleClear() default: return ErrorResult(fmt.Sprintf("unknown command: %s", cmd)) } } +func (t *FreeRideTool) handleClear() *ToolResult { + if t.cooldownPath == "" { + return ErrorResult("cooldown path not configured") + } + + // Double-check: does the file exist? + if _, err := os.Stat(t.cooldownPath); os.IsNotExist(err) { + return UserResult("Cooldowns are already empty (no file found).") + } + + if err := os.Remove(t.cooldownPath); err != nil { + return ErrorResult(fmt.Sprintf("failed to delete cooldown file: %v", err)) + } + + msg := "Success! Cooldown state cleared from disk.\n" + msg += "Re-loading configuration to reset in-memory state..." + + if t.reloadFunc != nil { + if err := t.reloadFunc(); err != nil { + return ErrorResult(fmt.Sprintf("%s\nFailed to reload: %v", msg, err)) + } + } + + return UserResult(msg) +} + func (t *FreeRideTool) fetchFreeModels(ctx context.Context) ([]openRouterModel, error) { req, err := http.NewRequestWithContext(ctx, "GET", "https://openrouter.ai/api/v1/models", nil) if err != nil { diff --git a/pkg/tools/freeride_test.go b/pkg/tools/freeride_test.go index b82597ab6..8b08757e8 100644 --- a/pkg/tools/freeride_test.go +++ b/pkg/tools/freeride_test.go @@ -51,7 +51,7 @@ func TestFreeRideTool_List(t *testing.T) { http.DefaultClient.Transport = &mockTransport{server.URL} defer func() { http.DefaultClient.Transport = oldTransport }() - tool := NewFreeRideTool("config.json", nil) + tool := NewFreeRideTool("config.json", "", nil) result := tool.Execute(context.Background(), map[string]any{ "command": "list", }) @@ -124,7 +124,7 @@ func TestFreeRideTool_Auto(t *testing.T) { return nil } - tool := NewFreeRideTool(configPath, reloadFunc) + tool := NewFreeRideTool(configPath, "", reloadFunc) result := tool.Execute(context.Background(), map[string]any{ "command": "auto", }) @@ -195,7 +195,7 @@ func TestFreeRideTool_SetTimeout(t *testing.T) { return nil } - tool := NewFreeRideTool(configPath, reloadFunc) + tool := NewFreeRideTool(configPath, "", reloadFunc) result := tool.Execute(context.Background(), map[string]any{ "command": "settimeout", "timeout": 180, @@ -250,7 +250,7 @@ func TestFreeRideTool_SetTimeout_NoOpenRouterModels(t *testing.T) { t.Fatalf("failed to save initial config: %v", err) } - tool := NewFreeRideTool(configPath, nil) + tool := NewFreeRideTool(configPath, "", nil) result := tool.Execute(context.Background(), map[string]any{ "command": "settimeout", "timeout": 180, @@ -287,7 +287,7 @@ func TestFreeRideTool_SetTimeout_MinimumTooLow(t *testing.T) { t.Fatalf("failed to save initial config: %v", err) } - tool := NewFreeRideTool(configPath, nil) + tool := NewFreeRideTool(configPath, "", nil) result := tool.Execute(context.Background(), map[string]any{ "command": "settimeout", "timeout": 20, // too low @@ -302,6 +302,46 @@ func TestFreeRideTool_SetTimeout_MinimumTooLow(t *testing.T) { } } +func TestFreeRideTool_Clear(t *testing.T) { + tempDir, err := os.MkdirTemp("", "freeride-clear-test") + if err != nil { + t.Fatalf("failed to create temp dir: %v", err) + } + defer os.RemoveAll(tempDir) + + configPath := filepath.Join(tempDir, "config.json") + cooldownPath := filepath.Join(tempDir, "cooldowns.json") + + // Create a dummy cooldown file + if err := os.WriteFile(cooldownPath, []byte("{}"), 0644); err != nil { + t.Fatalf("failed to create dummy cooldown file: %v", err) + } + + var reloadCalled bool + reloadFunc := func() error { + reloadCalled = true + return nil + } + + tool := NewFreeRideTool(configPath, cooldownPath, reloadFunc) + result := tool.Execute(context.Background(), map[string]any{ + "command": "clear", + }) + + if result.IsError { + t.Fatalf("Expected no error, got %s", result.ForLLM) + } + + if !reloadCalled { + t.Errorf("Expected reloadFunc to be called") + } + + // Verify file is gone + if _, err := os.Stat(cooldownPath); !os.IsNotExist(err) { + t.Errorf("Expected cooldown file to be deleted, but it still exists") + } +} + type mockTransport struct { url string }