feat: finalize freeride cooldown management and fix broken tool calls

This commit is contained in:
Your Name 2026-04-30 13:04:53 +02:00
parent a8186acdb2
commit 0200000ff0
5 changed files with 89 additions and 36 deletions

View file

@ -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",

View file

@ -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")

View file

@ -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")

View file

@ -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 {

View file

@ -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
}