feat: finalize freeride cooldown management and fix broken tool calls
This commit is contained in:
parent
a8186acdb2
commit
0200000ff0
5 changed files with 89 additions and 36 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue