diff --git a/pkg/agent/loop.go b/pkg/agent/loop.go index b806c9fb7..4561a1999 100644 --- a/pkg/agent/loop.go +++ b/pkg/agent/loop.go @@ -2762,8 +2762,8 @@ func (al *AgentLoop) selectCandidates( if targetModelName == agent.Model { logger.DebugCF("agent", "Model routing: primary model selected", map[string]any{ - "agent_id": agent.ID, - "score": score, + "agent_id": agent.ID, + "score": score, }) return agent.Candidates, resolvedCandidateModel(agent.Candidates, agent.Model) } diff --git a/pkg/config/config.go b/pkg/config/config.go index 5dd0236db..3a4c0f6a6 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -870,9 +870,9 @@ type ModelConfig struct { Region string `json:"region,omitempty"` // Region (e.g. for Google Vertex AI) // Optional optimizations - RPM int `json:"rpm,omitempty"` // Requests per minute limit - MaxTokensField string `json:"max_tokens_field,omitempty"` // Field name for max tokens (e.g., "max_completion_tokens") - RequestTimeout int `json:"request_timeout,omitempty"` + RPM int `json:"rpm,omitempty"` // Requests per minute limit + MaxTokensField string `json:"max_tokens_field,omitempty"` // Field name for max tokens (e.g., "max_completion_tokens") + RequestTimeout int `json:"request_timeout,omitempty"` ThinkingLevel string `json:"thinking_level,omitempty"` // Extended thinking: off|low|medium|high|xhigh|adaptive ExtraBody map[string]any `json:"extra_body,omitempty"` // Additional fields to inject into request body ExtraHeaders map[string]string `json:"extra_headers,omitempty"` // Additional headers to inject into request @@ -1048,7 +1048,7 @@ type SearXNGConfig struct { } type GLMSearchConfig struct { - Enabled bool `json:"enabled" env:"PICOCLAW_TOOLS_WEB_GLM_ENABLED"` + Enabled bool `json:"enabled" env:"PICOCLAW_TOOLS_WEB_GLM_ENABLED"` apiKey string secDirty bool BaseURL string `json:"base_url" env:"PICOCLAW_TOOLS_WEB_GLM_BASE_URL"` @@ -1088,7 +1088,7 @@ func (c *BaiduSearchConfig) SetAPIKey(key string) { } type WebToolsConfig struct { - ToolConfig ` envPrefix:"PICOCLAW_TOOLS_WEB_"` + ToolConfig ` envPrefix:"PICOCLAW_TOOLS_WEB_"` Brave BraveConfig ` json:"brave"` Tavily TavilyConfig ` json:"tavily"` DuckDuckGo DuckDuckGoConfig ` json:"duckduckgo"` @@ -1101,13 +1101,13 @@ type WebToolsConfig struct { // the client-side web_search tool is hidden to avoid duplicate search surfaces, // and the provider's built-in search is used instead. Falls back to client-side // search when the provider does not support native search. - PreferNative bool `json:"prefer_native" env:"PICOCLAW_TOOLS_WEB_PREFER_NATIVE"` + PreferNative bool ` json:"prefer_native" env:"PICOCLAW_TOOLS_WEB_PREFER_NATIVE"` // Proxy is an optional proxy URL for web tools (http/https/socks5/socks5h). // For authenticated proxies, prefer HTTP_PROXY/HTTPS_PROXY env vars instead of embedding credentials in config. - Proxy string `json:"proxy,omitempty" env:"PICOCLAW_TOOLS_WEB_PROXY"` - FetchLimitBytes int64 `json:"fetch_limit_bytes,omitempty" env:"PICOCLAW_TOOLS_WEB_FETCH_LIMIT_BYTES"` - Format string `json:"format,omitempty" env:"PICOCLAW_TOOLS_WEB_FORMAT"` - PrivateHostWhitelist FlexibleStringSlice `json:"private_host_whitelist,omitempty" env:"PICOCLAW_TOOLS_WEB_PRIVATE_HOST_WHITELIST"` + Proxy string ` json:"proxy,omitempty" env:"PICOCLAW_TOOLS_WEB_PROXY"` + FetchLimitBytes int64 ` json:"fetch_limit_bytes,omitempty" env:"PICOCLAW_TOOLS_WEB_FETCH_LIMIT_BYTES"` + Format string ` json:"format,omitempty" env:"PICOCLAW_TOOLS_WEB_FORMAT"` + PrivateHostWhitelist FlexibleStringSlice ` json:"private_host_whitelist,omitempty" env:"PICOCLAW_TOOLS_WEB_PRIVATE_HOST_WHITELIST"` } type CronToolsConfig struct { @@ -1145,8 +1145,8 @@ type ReadFileToolConfig struct { } type ToolsConfig struct { - AllowReadPaths []string `json:"allow_read_paths" env:"PICOCLAW_TOOLS_ALLOW_READ_PATHS"` - AllowWritePaths []string `json:"allow_write_paths" env:"PICOCLAW_TOOLS_ALLOW_WRITE_PATHS"` + AllowReadPaths []string `json:"allow_read_paths" env:"PICOCLAW_TOOLS_ALLOW_READ_PATHS"` + AllowWritePaths []string `json:"allow_write_paths" env:"PICOCLAW_TOOLS_ALLOW_WRITE_PATHS"` // FilterSensitiveData controls whether to filter sensitive values (API keys, // tokens, secrets) from tool results before sending to the LLM. // Default: true (enabled) @@ -1154,29 +1154,29 @@ type ToolsConfig struct { // FilterMinLength is the minimum content length required for filtering. // Content shorter than this will be returned unchanged for performance. // Default: 8 - FilterMinLength int `json:"filter_min_length" env:"PICOCLAW_TOOLS_FILTER_MIN_LENGTH"` + FilterMinLength int `json:"filter_min_length" env:"PICOCLAW_TOOLS_FILTER_MIN_LENGTH"` Web WebToolsConfig `json:"web"` Cron CronToolsConfig `json:"cron"` Exec ExecConfig `json:"exec"` Skills SkillsToolsConfig `json:"skills"` MediaCleanup MediaCleanupConfig `json:"media_cleanup"` MCP MCPConfig `json:"mcp"` - AppendFile ToolConfig `json:"append_file" envPrefix:"PICOCLAW_TOOLS_APPEND_FILE_"` - EditFile ToolConfig `json:"edit_file" envPrefix:"PICOCLAW_TOOLS_EDIT_FILE_"` - FindSkills ToolConfig `json:"find_skills" envPrefix:"PICOCLAW_TOOLS_FIND_SKILLS_"` - I2C ToolConfig `json:"i2c" envPrefix:"PICOCLAW_TOOLS_I2C_"` - InstallSkill ToolConfig `json:"install_skill" envPrefix:"PICOCLAW_TOOLS_INSTALL_SKILL_"` - UpdateSkill ToolConfig `json:"update_skill" envPrefix:"PICOCLAW_TOOLS_UPDATE_SKILL_"` - ListDir ToolConfig `json:"list_dir" envPrefix:"PICOCLAW_TOOLS_LIST_DIR_"` - Message ToolConfig `json:"message" envPrefix:"PICOCLAW_TOOLS_MESSAGE_"` - ReadFile ReadFileToolConfig `json:"read_file" envPrefix:"PICOCLAW_TOOLS_READ_FILE_"` - SendFile ToolConfig `json:"send_file" envPrefix:"PICOCLAW_TOOLS_SEND_FILE_"` - Spawn ToolConfig `json:"spawn" envPrefix:"PICOCLAW_TOOLS_SPAWN_"` - SpawnStatus ToolConfig `json:"spawn_status" envPrefix:"PICOCLAW_TOOLS_SPAWN_STATUS_"` - SPI ToolConfig `json:"spi" envPrefix:"PICOCLAW_TOOLS_SPI_"` - Subagent ToolConfig `json:"subagent" envPrefix:"PICOCLAW_TOOLS_SUBAGENT_"` - WebFetch ToolConfig `json:"web_fetch" envPrefix:"PICOCLAW_TOOLS_WEB_FETCH_"` - WriteFile ToolConfig `json:"write_file" envPrefix:"PICOCLAW_TOOLS_WRITE_FILE_"` + AppendFile ToolConfig `json:"append_file" envPrefix:"PICOCLAW_TOOLS_APPEND_FILE_"` + EditFile ToolConfig `json:"edit_file" envPrefix:"PICOCLAW_TOOLS_EDIT_FILE_"` + FindSkills ToolConfig `json:"find_skills" envPrefix:"PICOCLAW_TOOLS_FIND_SKILLS_"` + I2C ToolConfig `json:"i2c" envPrefix:"PICOCLAW_TOOLS_I2C_"` + InstallSkill ToolConfig `json:"install_skill" envPrefix:"PICOCLAW_TOOLS_INSTALL_SKILL_"` + UpdateSkill ToolConfig `json:"update_skill" envPrefix:"PICOCLAW_TOOLS_UPDATE_SKILL_"` + ListDir ToolConfig `json:"list_dir" envPrefix:"PICOCLAW_TOOLS_LIST_DIR_"` + Message ToolConfig `json:"message" envPrefix:"PICOCLAW_TOOLS_MESSAGE_"` + ReadFile ReadFileToolConfig `json:"read_file" envPrefix:"PICOCLAW_TOOLS_READ_FILE_"` + SendFile ToolConfig `json:"send_file" envPrefix:"PICOCLAW_TOOLS_SEND_FILE_"` + Spawn ToolConfig `json:"spawn" envPrefix:"PICOCLAW_TOOLS_SPAWN_"` + SpawnStatus ToolConfig `json:"spawn_status" envPrefix:"PICOCLAW_TOOLS_SPAWN_STATUS_"` + SPI ToolConfig `json:"spi" envPrefix:"PICOCLAW_TOOLS_SPI_"` + Subagent ToolConfig `json:"subagent" envPrefix:"PICOCLAW_TOOLS_SUBAGENT_"` + WebFetch ToolConfig `json:"web_fetch" envPrefix:"PICOCLAW_TOOLS_WEB_FETCH_"` + WriteFile ToolConfig `json:"write_file" envPrefix:"PICOCLAW_TOOLS_WRITE_FILE_"` } // IsFilterSensitiveDataEnabled returns true if sensitive data filtering is enabled @@ -1268,10 +1268,10 @@ type MCPServerConfig struct { // MCPConfig defines configuration for all MCP servers type MCPConfig struct { - ToolConfig ` envPrefix:"PICOCLAW_TOOLS_MCP_"` + ToolConfig ` envPrefix:"PICOCLAW_TOOLS_MCP_"` Discovery ToolDiscoveryConfig ` json:"discovery"` // Servers is a map of server name to server configuration - Servers map[string]MCPServerConfig `json:"servers,omitempty"` + Servers map[string]MCPServerConfig ` json:"servers,omitempty"` } func LoadConfig(path string) (*Config, error) { diff --git a/pkg/providers/vertex/provider.go b/pkg/providers/vertex/provider.go index 59a3060b2..9c1ea41dc 100644 --- a/pkg/providers/vertex/provider.go +++ b/pkg/providers/vertex/provider.go @@ -28,10 +28,6 @@ type ( FunctionCall = protocoltypes.FunctionCall ) -const ( - defaultRequestTimeout = common.DefaultRequestTimeout -) - // Provider implements the LLM provider interface for Google Vertex AI. // It uses the standard Vertex AI REST API for Gemini models. type Provider struct { @@ -114,7 +110,6 @@ func (p *Provider) buildURL(model string, action string) string { return baseURL } - // parseMediaData converts base64 media data into the Vertex AI inlineData format. // It tries to detect mime type from the data URI scheme if present. func parseMediaData(mediaData string) map[string]any { @@ -138,7 +133,11 @@ func parseMediaData(mediaData string) map[string]any { } // buildRequestBody formats the standard messages and tools into the Vertex AI (Gemini) REST payload format. -func (p *Provider) buildRequestBody(messages []Message, tools []ToolDefinition, options map[string]any) (map[string]any, error) { +func (p *Provider) buildRequestBody( + messages []Message, + tools []ToolDefinition, + options map[string]any, +) (map[string]any, error) { req := make(map[string]any) var contents []map[string]any @@ -234,7 +233,7 @@ func (p *Provider) buildRequestBody(messages []Message, tools []ToolDefinition, parts = append(parts, map[string]any{ "functionResponse": map[string]any{ - "name": msg.ToolCallID, + "name": msg.ToolCallID, "response": responseObj, }, }) @@ -291,7 +290,6 @@ func (p *Provider) buildRequestBody(messages []Message, tools []ToolDefinition, return req, nil } - func (p *Provider) Chat( ctx context.Context, messages []Message, diff --git a/pkg/providers/vertex/provider_test.go b/pkg/providers/vertex/provider_test.go index 65555083b..63b2a1efd 100644 --- a/pkg/providers/vertex/provider_test.go +++ b/pkg/providers/vertex/provider_test.go @@ -8,10 +8,10 @@ import ( "net/url" "testing" - - "github.com/sipeed/picoclaw/pkg/providers/protocoltypes" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/sipeed/picoclaw/pkg/providers/protocoltypes" ) func TestProvider_buildURL(t *testing.T) { @@ -37,16 +37,16 @@ func TestProvider_buildURL(t *testing.T) { expected: "https://us-central1-aiplatform.googleapis.com/v1/projects/my-project/locations/us-central1/publishers/google/models/gemini-1.5-flash:generateContent", }, { - name: "Override with base URL without method", - apiBase: "http://localhost:8080/v1/models", - model: "gemini-1.0-pro", - expected: "http://localhost:8080/v1/models/gemini-1.0-pro:generateContent?key=key", + name: "Override with base URL without method", + apiBase: "http://localhost:8080/v1/models", + model: "gemini-1.0-pro", + expected: "http://localhost:8080/v1/models/gemini-1.0-pro:generateContent?key=key", }, { - name: "Override with full endpoint URL", - apiBase: "https://my-custom-proxy.com/my-endpoint:generateContent", - model: "gemini-1.5-pro", - expected: "https://my-custom-proxy.com/my-endpoint:generateContent?key=key", + name: "Override with full endpoint URL", + apiBase: "https://my-custom-proxy.com/my-endpoint:generateContent", + model: "gemini-1.5-pro", + expected: "https://my-custom-proxy.com/my-endpoint:generateContent?key=key", }, } @@ -59,16 +59,21 @@ func TestProvider_buildURL(t *testing.T) { } } - func TestProvider_buildRequestBody(t *testing.T) { p := NewProvider("key", "", "", "proj", "us-central1") messages := []protocoltypes.Message{ {Role: "system", Content: "You are a helpful assistant."}, {Role: "user", Content: "Hello!", Media: []string{"data:image/png;base64,iVBORw0KGgo"}}, - {Role: "assistant", ToolCalls: []protocoltypes.ToolCall{{Name: "get_weather", Arguments: map[string]any{"location": "Tokyo"}}}}, + { + Role: "assistant", + ToolCalls: []protocoltypes.ToolCall{{Name: "get_weather", Arguments: map[string]any{"location": "Tokyo"}}}, + }, {Role: "tool", ToolCallID: "get_weather", Content: "Sunny"}, - {Role: "assistant", ToolCalls: []protocoltypes.ToolCall{{Name: "get_time", Arguments: map[string]any{"location": "Tokyo"}}}}, + { + Role: "assistant", + ToolCalls: []protocoltypes.ToolCall{{Name: "get_time", Arguments: map[string]any{"location": "Tokyo"}}}, + }, {Role: "tool", ToolCallID: "get_time", Content: "12:00 PM"}, } @@ -128,7 +133,6 @@ func TestProvider_buildRequestBody(t *testing.T) { assert.Equal(t, "model", contents[3]["role"]) } - func TestProvider_Chat(t *testing.T) { // Create a mock server ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -194,7 +198,11 @@ func TestProvider_ChatStream(t *testing.T) { w.Header().Set("Content-Type", "text/event-stream") // Write mock chunks w.Write([]byte(`data: {"candidates":[{"content":{"parts":[{"text":"Hello"}]}}]}` + "\n\n")) - w.Write([]byte(`data: {"candidates":[{"content":{"parts":[{"text":", world!"}]},"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":10,"candidatesTokenCount":5,"totalTokenCount":15}}` + "\n\n")) + w.Write( + []byte( + `data: {"candidates":[{"content":{"parts":[{"text":", world!"}]},"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":10,"candidatesTokenCount":5,"totalTokenCount":15}}` + "\n\n", + ), + ) })) defer ts.Close() diff --git a/pkg/routing/router.go b/pkg/routing/router.go index a5d489233..ccba4e777 100644 --- a/pkg/routing/router.go +++ b/pkg/routing/router.go @@ -4,11 +4,6 @@ import ( "github.com/sipeed/picoclaw/pkg/providers" ) -// defaultThreshold is used when the config threshold is zero or negative. -// At 0.35 a message needs at least one strong signal (code block, long text, -// or an attachment) before the heavy model is chosen. -const defaultThreshold = 0.35 - // RoutingTier defines a single tier for model routing. type RoutingTier struct { Model string diff --git a/pkg/routing/router_test.go b/pkg/routing/router_test.go index 59d719608..7ef01a549 100644 --- a/pkg/routing/router_test.go +++ b/pkg/routing/router_test.go @@ -241,9 +241,15 @@ func TestRuleClassifier_ScoreDoesNotExceedOne(t *testing.T) { // ── Router ─────────────────────────────────────────────────────────────────── - func TestRouter_SelectModel_SimpleMessageUsesLight(t *testing.T) { - r := New(RouterConfig{Tiers: []RoutingTier{{Model: "gemini-flash", Threshold: 0.0}, {Model: "claude-sonnet-4-6", Threshold: 0.35}}}) + r := New( + RouterConfig{ + Tiers: []RoutingTier{ + {Model: "gemini-flash", Threshold: 0.0}, + {Model: "claude-sonnet-4-6", Threshold: 0.35}, + }, + }, + ) msg := "hi" model, usedLight, _ := r.SelectModel(msg, nil, "claude-sonnet-4-6") if !usedLight { @@ -255,7 +261,14 @@ func TestRouter_SelectModel_SimpleMessageUsesLight(t *testing.T) { } func TestRouter_SelectModel_CodeBlockUsesPrimary(t *testing.T) { - r := New(RouterConfig{Tiers: []RoutingTier{{Model: "gemini-flash", Threshold: 0.0}, {Model: "claude-sonnet-4-6", Threshold: 0.35}}}) + r := New( + RouterConfig{ + Tiers: []RoutingTier{ + {Model: "gemini-flash", Threshold: 0.0}, + {Model: "claude-sonnet-4-6", Threshold: 0.35}, + }, + }, + ) msg := "```go\nfmt.Println(\"hello\")\n```" model, usedLight, _ := r.SelectModel(msg, nil, "claude-sonnet-4-6") if usedLight { @@ -267,7 +280,14 @@ func TestRouter_SelectModel_CodeBlockUsesPrimary(t *testing.T) { } func TestRouter_SelectModel_AttachmentUsesPrimary(t *testing.T) { - r := New(RouterConfig{Tiers: []RoutingTier{{Model: "gemini-flash", Threshold: 0.0}, {Model: "claude-sonnet-4-6", Threshold: 0.35}}}) + r := New( + RouterConfig{ + Tiers: []RoutingTier{ + {Model: "gemini-flash", Threshold: 0.0}, + {Model: "claude-sonnet-4-6", Threshold: 0.35}, + }, + }, + ) msg := "can you analyze this? data:image/png;base64,abc123" model, usedLight, _ := r.SelectModel(msg, nil, "claude-sonnet-4-6") if usedLight { @@ -279,7 +299,14 @@ func TestRouter_SelectModel_AttachmentUsesPrimary(t *testing.T) { } func TestRouter_SelectModel_LongMessageUsesPrimary(t *testing.T) { - r := New(RouterConfig{Tiers: []RoutingTier{{Model: "gemini-flash", Threshold: 0.0}, {Model: "claude-sonnet-4-6", Threshold: 0.35}}}) + r := New( + RouterConfig{ + Tiers: []RoutingTier{ + {Model: "gemini-flash", Threshold: 0.0}, + {Model: "claude-sonnet-4-6", Threshold: 0.35}, + }, + }, + ) // >200 token estimate: 210 * 3 = 630 chars msg := strings.Repeat("word ", 210) model, usedLight, _ := r.SelectModel(msg, nil, "claude-sonnet-4-6") @@ -294,7 +321,14 @@ func TestRouter_SelectModel_LongMessageUsesPrimary(t *testing.T) { func TestRouter_SelectModel_DeepToolChainUsesLight(t *testing.T) { // Tool calls alone (0.25) don't cross the 0.35 threshold — acceptable behavior. // Routing is conservative: only promote to heavy when the signal is unambiguous. - r := New(RouterConfig{Tiers: []RoutingTier{{Model: "gemini-flash", Threshold: 0.0}, {Model: "claude-sonnet-4-6", Threshold: 0.35}}}) + r := New( + RouterConfig{ + Tiers: []RoutingTier{ + {Model: "gemini-flash", Threshold: 0.0}, + {Model: "claude-sonnet-4-6", Threshold: 0.35}, + }, + }, + ) history := []providers.Message{ {Role: "assistant", ToolCalls: []providers.ToolCall{{Name: "read_file"}, {Name: "write_file"}}}, {Role: "assistant", ToolCalls: []providers.ToolCall{{Name: "exec"}, {Name: "search"}}}, @@ -308,7 +342,14 @@ func TestRouter_SelectModel_DeepToolChainUsesLight(t *testing.T) { func TestRouter_SelectModel_ToolChainPlusMediumUsesHeavy(t *testing.T) { // Tool calls (0.25) + medium message (0.15) = 0.40 >= 0.35 → heavy - r := New(RouterConfig{Tiers: []RoutingTier{{Model: "gemini-flash", Threshold: 0.0}, {Model: "claude-sonnet-4-6", Threshold: 0.35}}}) + r := New( + RouterConfig{ + Tiers: []RoutingTier{ + {Model: "gemini-flash", Threshold: 0.0}, + {Model: "claude-sonnet-4-6", Threshold: 0.35}, + }, + }, + ) history := []providers.Message{ {Role: "assistant", ToolCalls: []providers.ToolCall{ {Name: "a"}, {Name: "b"}, {Name: "c"}, {Name: "d"}, @@ -324,7 +365,14 @@ func TestRouter_SelectModel_ToolChainPlusMediumUsesHeavy(t *testing.T) { func TestRouter_SelectModel_CustomThreshold(t *testing.T) { // Very low threshold: even a short message triggers heavy model - r := New(RouterConfig{Tiers: []RoutingTier{{Model: "gemini-flash", Threshold: 0.0}, {Model: "claude-sonnet-4-6", Threshold: 0.05}}}) + r := New( + RouterConfig{ + Tiers: []RoutingTier{ + {Model: "gemini-flash", Threshold: 0.0}, + {Model: "claude-sonnet-4-6", Threshold: 0.05}, + }, + }, + ) msg := strings.Repeat("word ", 55) // medium message → 0.15 >= 0.05 _, usedLight, _ := r.SelectModel(msg, nil, "claude-sonnet-4-6") if usedLight { @@ -334,7 +382,14 @@ func TestRouter_SelectModel_CustomThreshold(t *testing.T) { func TestRouter_SelectModel_HighThreshold(t *testing.T) { // Very high threshold: even code blocks route to light - r := New(RouterConfig{Tiers: []RoutingTier{{Model: "gemini-flash", Threshold: 0.0}, {Model: "claude-sonnet-4-6", Threshold: 0.99}}}) + r := New( + RouterConfig{ + Tiers: []RoutingTier{ + {Model: "gemini-flash", Threshold: 0.0}, + {Model: "claude-sonnet-4-6", Threshold: 0.99}, + }, + }, + ) msg := "```go\nfmt.Println()\n```" _, usedLight, _ := r.SelectModel(msg, nil, "claude-sonnet-4-6") if !usedLight { @@ -343,7 +398,11 @@ func TestRouter_SelectModel_HighThreshold(t *testing.T) { } func TestRouter_Tiers(t *testing.T) { - r := New(RouterConfig{Tiers: []RoutingTier{{Model: "my-fast-model", Threshold: 0.0}, {Model: "heavy-model", Threshold: 0.35}}}) + r := New( + RouterConfig{ + Tiers: []RoutingTier{{Model: "my-fast-model", Threshold: 0.0}, {Model: "heavy-model", Threshold: 0.35}}, + }, + ) if r.Tiers()[0].Model != "my-fast-model" { t.Errorf("LightModel: got %q, want %q", "my-fast-model", "my-fast-model") } diff --git a/pkg/tools/skills_update.go b/pkg/tools/skills_update.go index f544a5fae..9edb72574 100644 --- a/pkg/tools/skills_update.go +++ b/pkg/tools/skills_update.go @@ -92,11 +92,19 @@ func (t *UpdateSkillTool) Execute(ctx context.Context, args map[string]any) *Too return ErrorResult(fmt.Sprintf("failed to append to SKILL.md: %v", err)) } - output := fmt.Sprintf("Successfully learned and updated SKILL.md.\n\nAnalysis: %s\nSkills Improved: %s\n", analysis, skillsToImprove) + output := fmt.Sprintf( + "Successfully learned and updated SKILL.md.\n\nAnalysis: %s\nSkills Improved: %s\n", + analysis, + skillsToImprove, + ) // The response is passed back to the LLM. // We also populate the ForUser field to notify the user. res := SilentResult(output) - res.ForUser = fmt.Sprintf("I have analyzed our conversation and improved my skills.\n\n**My Analysis**:\n%s\n\n**Skills I've Improved/Added**:\n%s\n\nI have saved these learnings to `SKILL.md`.", analysis, skillsToImprove) + res.ForUser = fmt.Sprintf( + "I have analyzed our conversation and improved my skills.\n\n**My Analysis**:\n%s\n\n**Skills I've Improved/Added**:\n%s\n\nI have saved these learnings to `SKILL.md`.", + analysis, + skillsToImprove, + ) return res } diff --git a/pkg/tools/skills_update_test.go b/pkg/tools/skills_update_test.go index 004898404..562e59427 100644 --- a/pkg/tools/skills_update_test.go +++ b/pkg/tools/skills_update_test.go @@ -65,5 +65,5 @@ func TestUpdateSkillTool_Execute(t *testing.T) { contentStr2 := string(content2) assert.Contains(t, contentStr2, "I was slow to find the file.") // Old content still there - assert.Contains(t, contentStr2, "Another analysis.") // New content added + assert.Contains(t, contentStr2, "Another analysis.") // New content added } diff --git a/web/backend/api/models.go b/web/backend/api/models.go index 6cf9c03c7..908a26929 100644 --- a/web/backend/api/models.go +++ b/web/backend/api/models.go @@ -32,11 +32,11 @@ type modelResponse struct { Proxy string `json:"proxy,omitempty"` AuthMethod string `json:"auth_method,omitempty"` // Advanced fields - ConnectMode string `json:"connect_mode,omitempty"` - Workspace string `json:"workspace,omitempty"` - RPM int `json:"rpm,omitempty"` - MaxTokensField string `json:"max_tokens_field,omitempty"` - RequestTimeout int `json:"request_timeout,omitempty"` + ConnectMode string `json:"connect_mode,omitempty"` + Workspace string `json:"workspace,omitempty"` + RPM int `json:"rpm,omitempty"` + MaxTokensField string `json:"max_tokens_field,omitempty"` + RequestTimeout int `json:"request_timeout,omitempty"` ThinkingLevel string `json:"thinking_level,omitempty"` ExtraBody map[string]any `json:"extra_body,omitempty"` ExtraHeaders map[string]string `json:"extra_headers,omitempty"` diff --git a/web/frontend/src/components/channels/channel-config-page.tsx b/web/frontend/src/components/channels/channel-config-page.tsx index 6af821ac9..45c32d83f 100644 --- a/web/frontend/src/components/channels/channel-config-page.tsx +++ b/web/frontend/src/components/channels/channel-config-page.tsx @@ -95,16 +95,18 @@ function buildSavePayload( if (key.startsWith("_")) continue if (key === "enabled") continue - if (key in SECRET_FIELD_MAP) { - const editKey = SECRET_FIELD_MAP[key] - const incoming = asString(editConfig[editKey]) - payload[key] = incoming !== "" ? incoming : value - continue - } - payload[key] = value } + for (const [key, editKey] of Object.entries(SECRET_FIELD_MAP)) { + if (editKey in editConfig) { + const incoming = asString(editConfig[editKey]) + if (incoming !== "") { + payload[key] = incoming + } + } + } + if (channel.name === "whatsapp_native") { payload.use_native = true }