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_name": "nemotron-120b",
|
||||||
"model_fallbacks": [
|
"model_fallbacks": [
|
||||||
"meta-llama-llama-3.3-70b-instruct:free",
|
"meta-llama-llama-3.3-70b-instruct:free",
|
||||||
"qwen-qwen3-coder:free",
|
"qwen-qwen3-coder:free"
|
||||||
"openrouter-elephant-alpha"
|
|
||||||
],
|
],
|
||||||
"max_tokens": 32768,
|
"max_tokens": 32768,
|
||||||
"max_tool_iterations": 50,
|
"max_tool_iterations": 50,
|
||||||
|
|
@ -305,16 +304,7 @@ data:
|
||||||
],
|
],
|
||||||
"request_timeout": 45
|
"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_name": "openrouter-free",
|
||||||
"model": "arcee-ai/trinity-large-preview:free",
|
"model": "arcee-ai/trinity-large-preview:free",
|
||||||
|
|
@ -587,16 +577,7 @@ data:
|
||||||
],
|
],
|
||||||
"request_timeout": 45
|
"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_name": "minimax-minimax-m2.5:free",
|
||||||
"model": "minimax/minimax-m2.5:free",
|
"model": "minimax/minimax-m2.5:free",
|
||||||
|
|
|
||||||
|
|
@ -118,7 +118,8 @@ func NewAgentInstance(
|
||||||
toolsRegistry.Register(tools.NewAppendFileTool(workspace, restrict, allowWritePaths))
|
toolsRegistry.Register(tools.NewAppendFileTool(workspace, restrict, allowWritePaths))
|
||||||
}
|
}
|
||||||
if cfg.Tools.IsToolEnabled("freeride") {
|
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")
|
sessionsDir := filepath.Join(workspace, "sessions")
|
||||||
|
|
|
||||||
|
|
@ -249,7 +249,7 @@ func registerSharedTools(
|
||||||
// Skill discovery and installation tools
|
// Skill discovery and installation tools
|
||||||
skills_enabled := cfg.Tools.IsToolEnabled("skills")
|
skills_enabled := cfg.Tools.IsToolEnabled("skills")
|
||||||
if skills_enabled {
|
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")
|
find_skills_enable := cfg.Tools.IsToolEnabled("find_skills")
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
@ -15,14 +16,16 @@ import (
|
||||||
// FreeRideTool adapts the FreeRide logic (from clawhub/free-ride) for PicoClaw.
|
// FreeRideTool adapts the FreeRide logic (from clawhub/free-ride) for PicoClaw.
|
||||||
// It manages OpenRouter's free models and configures them as fallbacks.
|
// It manages OpenRouter's free models and configures them as fallbacks.
|
||||||
type FreeRideTool struct {
|
type FreeRideTool struct {
|
||||||
configPath string
|
configPath string
|
||||||
reloadFunc func() error
|
cooldownPath string
|
||||||
|
reloadFunc func() error
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewFreeRideTool(configPath string, reloadFunc func() error) *FreeRideTool {
|
func NewFreeRideTool(configPath, cooldownPath string, reloadFunc func() error) *FreeRideTool {
|
||||||
return &FreeRideTool{
|
return &FreeRideTool{
|
||||||
configPath: configPath,
|
configPath: configPath,
|
||||||
reloadFunc: reloadFunc,
|
cooldownPath: cooldownPath,
|
||||||
|
reloadFunc: reloadFunc,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -41,8 +44,8 @@ func (t *FreeRideTool) Parameters() map[string]any {
|
||||||
"properties": map[string]any{
|
"properties": map[string]any{
|
||||||
"command": map[string]any{
|
"command": map[string]any{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": []string{"auto", "list", "status", "settimeout"},
|
"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)",
|
"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{
|
"limit": map[string]any{
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
|
|
@ -94,11 +97,39 @@ func (t *FreeRideTool) Execute(ctx context.Context, args map[string]any) *ToolRe
|
||||||
return t.handleStatus()
|
return t.handleStatus()
|
||||||
case "settimeout":
|
case "settimeout":
|
||||||
return t.handleSetTimeout(timeout)
|
return t.handleSetTimeout(timeout)
|
||||||
|
case "clear":
|
||||||
|
return t.handleClear()
|
||||||
default:
|
default:
|
||||||
return ErrorResult(fmt.Sprintf("unknown command: %s", cmd))
|
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) {
|
func (t *FreeRideTool) fetchFreeModels(ctx context.Context) ([]openRouterModel, error) {
|
||||||
req, err := http.NewRequestWithContext(ctx, "GET", "https://openrouter.ai/api/v1/models", nil)
|
req, err := http.NewRequestWithContext(ctx, "GET", "https://openrouter.ai/api/v1/models", nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@ func TestFreeRideTool_List(t *testing.T) {
|
||||||
http.DefaultClient.Transport = &mockTransport{server.URL}
|
http.DefaultClient.Transport = &mockTransport{server.URL}
|
||||||
defer func() { http.DefaultClient.Transport = oldTransport }()
|
defer func() { http.DefaultClient.Transport = oldTransport }()
|
||||||
|
|
||||||
tool := NewFreeRideTool("config.json", nil)
|
tool := NewFreeRideTool("config.json", "", nil)
|
||||||
result := tool.Execute(context.Background(), map[string]any{
|
result := tool.Execute(context.Background(), map[string]any{
|
||||||
"command": "list",
|
"command": "list",
|
||||||
})
|
})
|
||||||
|
|
@ -124,7 +124,7 @@ func TestFreeRideTool_Auto(t *testing.T) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
tool := NewFreeRideTool(configPath, reloadFunc)
|
tool := NewFreeRideTool(configPath, "", reloadFunc)
|
||||||
result := tool.Execute(context.Background(), map[string]any{
|
result := tool.Execute(context.Background(), map[string]any{
|
||||||
"command": "auto",
|
"command": "auto",
|
||||||
})
|
})
|
||||||
|
|
@ -195,7 +195,7 @@ func TestFreeRideTool_SetTimeout(t *testing.T) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
tool := NewFreeRideTool(configPath, reloadFunc)
|
tool := NewFreeRideTool(configPath, "", reloadFunc)
|
||||||
result := tool.Execute(context.Background(), map[string]any{
|
result := tool.Execute(context.Background(), map[string]any{
|
||||||
"command": "settimeout",
|
"command": "settimeout",
|
||||||
"timeout": 180,
|
"timeout": 180,
|
||||||
|
|
@ -250,7 +250,7 @@ func TestFreeRideTool_SetTimeout_NoOpenRouterModels(t *testing.T) {
|
||||||
t.Fatalf("failed to save initial config: %v", err)
|
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{
|
result := tool.Execute(context.Background(), map[string]any{
|
||||||
"command": "settimeout",
|
"command": "settimeout",
|
||||||
"timeout": 180,
|
"timeout": 180,
|
||||||
|
|
@ -287,7 +287,7 @@ func TestFreeRideTool_SetTimeout_MinimumTooLow(t *testing.T) {
|
||||||
t.Fatalf("failed to save initial config: %v", err)
|
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{
|
result := tool.Execute(context.Background(), map[string]any{
|
||||||
"command": "settimeout",
|
"command": "settimeout",
|
||||||
"timeout": 20, // too low
|
"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 {
|
type mockTransport struct {
|
||||||
url string
|
url string
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue