diff --git a/config/config.example.json b/config/config.example.json index 910c4fbd3..c92399940 100644 --- a/config/config.example.json +++ b/config/config.example.json @@ -427,6 +427,10 @@ "i2c": { "enabled": false }, + "image_generate": { + "enabled": false, + "model": "openai-codex/gpt-image-2" + }, "install_skill": { "enabled": true }, diff --git a/docs/reference/tools_configuration.md b/docs/reference/tools_configuration.md index 810d91ef2..58ebeb887 100644 --- a/docs/reference/tools_configuration.md +++ b/docs/reference/tools_configuration.md @@ -156,6 +156,30 @@ If `range` is omitted, PicoClaw performs an unrestricted search. } ``` +## Image Generation Tool + +The `image_generate` tool creates image files through a provider that supports +image generation. + +| Config | Type | Default | Description | +|--------|------|---------|-------------| +| `enabled` | bool | false | Enable the image generation tool | +| `model` | string | `gpt-image-2` | Image generation model. Values may include a provider prefix, for example `openai-codex/gpt-image-2` | + +If `tools.image_generate.model` is not set, PicoClaw falls back to the legacy +`agents.defaults.image_model` setting, then to `gpt-image-2`. + +```json +{ + "tools": { + "image_generate": { + "enabled": true, + "model": "openai-codex/gpt-image-2" + } + } +} +``` + ## Exec Tool The exec tool is used to execute shell commands. diff --git a/pkg/agent/agent_init.go b/pkg/agent/agent_init.go index 2a0e24e83..13ac8bc48 100644 --- a/pkg/agent/agent_init.go +++ b/pkg/agent/agent_init.go @@ -216,10 +216,7 @@ func registerSharedTools( } if cfg.Tools.IsToolEnabled("image_generate") { - imageModel := cfg.Agents.Defaults.ImageModel - if imageModel == "" { - imageModel = "gpt-image-2" - } + imageModel := cfg.Tools.ImageGenerate.EffectiveModel(cfg.Agents.Defaults) agent.Tools.Register(tools.NewImageGenerateTool(agent.Workspace, imageModel, nil)) } diff --git a/pkg/config/config.go b/pkg/config/config.go index 083463222..d446795c7 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -626,6 +626,21 @@ type ToolConfig struct { Enabled bool `json:"enabled" yaml:"-" env:"ENABLED"` } +type ImageGenerateToolsConfig struct { + ToolConfig `yaml:"-" envPrefix:"PICOCLAW_TOOLS_IMAGE_GENERATE_"` + Model string `json:"model,omitempty" yaml:"-" env:"PICOCLAW_TOOLS_IMAGE_GENERATE_MODEL"` +} + +func (c ImageGenerateToolsConfig) EffectiveModel(defaults AgentDefaults) string { + if model := strings.TrimSpace(c.Model); model != "" { + return model + } + if model := strings.TrimSpace(defaults.ImageModel); model != "" { + return model + } + return "gpt-image-2" +} + type BraveConfig struct { Enabled bool `json:"enabled" yaml:"-" env:"PICOCLAW_TOOLS_WEB_BRAVE_ENABLED"` APIKeys SecureStrings `json:"api_keys,omitzero" yaml:"api_keys,omitempty" env:"PICOCLAW_TOOLS_WEB_BRAVE_API_KEYS"` @@ -816,31 +831,31 @@ 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" yaml:"-" env:"PICOCLAW_TOOLS_FILTER_MIN_LENGTH"` - Web WebToolsConfig `json:"web" yaml:"web,omitempty"` - Cron CronToolsConfig `json:"cron" yaml:"-"` - Exec ExecConfig `json:"exec" yaml:"-"` - Skills SkillsToolsConfig `json:"skills" yaml:"skills,omitempty"` - MediaCleanup MediaCleanupConfig `json:"media_cleanup" yaml:"-"` - MCP MCPConfig `json:"mcp" yaml:"-"` - AppendFile ToolConfig `json:"append_file" yaml:"-" envPrefix:"PICOCLAW_TOOLS_APPEND_FILE_"` - EditFile ToolConfig `json:"edit_file" yaml:"-" envPrefix:"PICOCLAW_TOOLS_EDIT_FILE_"` - FindSkills ToolConfig `json:"find_skills" yaml:"-" envPrefix:"PICOCLAW_TOOLS_FIND_SKILLS_"` - I2C ToolConfig `json:"i2c" yaml:"-" envPrefix:"PICOCLAW_TOOLS_I2C_"` - ImageGenerate ToolConfig `json:"image_generate" yaml:"-" envPrefix:"PICOCLAW_TOOLS_IMAGE_GENERATE_"` - InstallSkill ToolConfig `json:"install_skill" yaml:"-" envPrefix:"PICOCLAW_TOOLS_INSTALL_SKILL_"` - ListDir ToolConfig `json:"list_dir" yaml:"-" envPrefix:"PICOCLAW_TOOLS_LIST_DIR_"` - Message ToolConfig `json:"message" yaml:"-" envPrefix:"PICOCLAW_TOOLS_MESSAGE_"` - ReadFile ReadFileToolConfig `json:"read_file" yaml:"-" envPrefix:"PICOCLAW_TOOLS_READ_FILE_"` - Serial ToolConfig `json:"serial" yaml:"-" envPrefix:"PICOCLAW_TOOLS_SERIAL_"` - SendFile ToolConfig `json:"send_file" yaml:"-" envPrefix:"PICOCLAW_TOOLS_SEND_FILE_"` - SendTTS ToolConfig `json:"send_tts" yaml:"-" envPrefix:"PICOCLAW_TOOLS_SEND_TTS_"` - Spawn ToolConfig `json:"spawn" yaml:"-" envPrefix:"PICOCLAW_TOOLS_SPAWN_"` - SpawnStatus ToolConfig `json:"spawn_status" yaml:"-" envPrefix:"PICOCLAW_TOOLS_SPAWN_STATUS_"` - SPI ToolConfig `json:"spi" yaml:"-" envPrefix:"PICOCLAW_TOOLS_SPI_"` - Subagent ToolConfig `json:"subagent" yaml:"-" envPrefix:"PICOCLAW_TOOLS_SUBAGENT_"` - WebFetch ToolConfig `json:"web_fetch" yaml:"-" envPrefix:"PICOCLAW_TOOLS_WEB_FETCH_"` - WriteFile ToolConfig `json:"write_file" yaml:"-" envPrefix:"PICOCLAW_TOOLS_WRITE_FILE_"` + FilterMinLength int `json:"filter_min_length" yaml:"-" env:"PICOCLAW_TOOLS_FILTER_MIN_LENGTH"` + Web WebToolsConfig `json:"web" yaml:"web,omitempty"` + Cron CronToolsConfig `json:"cron" yaml:"-"` + Exec ExecConfig `json:"exec" yaml:"-"` + Skills SkillsToolsConfig `json:"skills" yaml:"skills,omitempty"` + MediaCleanup MediaCleanupConfig `json:"media_cleanup" yaml:"-"` + MCP MCPConfig `json:"mcp" yaml:"-"` + AppendFile ToolConfig `json:"append_file" yaml:"-" envPrefix:"PICOCLAW_TOOLS_APPEND_FILE_"` + EditFile ToolConfig `json:"edit_file" yaml:"-" envPrefix:"PICOCLAW_TOOLS_EDIT_FILE_"` + FindSkills ToolConfig `json:"find_skills" yaml:"-" envPrefix:"PICOCLAW_TOOLS_FIND_SKILLS_"` + I2C ToolConfig `json:"i2c" yaml:"-" envPrefix:"PICOCLAW_TOOLS_I2C_"` + ImageGenerate ImageGenerateToolsConfig `json:"image_generate" yaml:"-"` + InstallSkill ToolConfig `json:"install_skill" yaml:"-" envPrefix:"PICOCLAW_TOOLS_INSTALL_SKILL_"` + ListDir ToolConfig `json:"list_dir" yaml:"-" envPrefix:"PICOCLAW_TOOLS_LIST_DIR_"` + Message ToolConfig `json:"message" yaml:"-" envPrefix:"PICOCLAW_TOOLS_MESSAGE_"` + ReadFile ReadFileToolConfig `json:"read_file" yaml:"-" envPrefix:"PICOCLAW_TOOLS_READ_FILE_"` + Serial ToolConfig `json:"serial" yaml:"-" envPrefix:"PICOCLAW_TOOLS_SERIAL_"` + SendFile ToolConfig `json:"send_file" yaml:"-" envPrefix:"PICOCLAW_TOOLS_SEND_FILE_"` + SendTTS ToolConfig `json:"send_tts" yaml:"-" envPrefix:"PICOCLAW_TOOLS_SEND_TTS_"` + Spawn ToolConfig `json:"spawn" yaml:"-" envPrefix:"PICOCLAW_TOOLS_SPAWN_"` + SpawnStatus ToolConfig `json:"spawn_status" yaml:"-" envPrefix:"PICOCLAW_TOOLS_SPAWN_STATUS_"` + SPI ToolConfig `json:"spi" yaml:"-" envPrefix:"PICOCLAW_TOOLS_SPI_"` + Subagent ToolConfig `json:"subagent" yaml:"-" envPrefix:"PICOCLAW_TOOLS_SUBAGENT_"` + WebFetch ToolConfig `json:"web_fetch" yaml:"-" envPrefix:"PICOCLAW_TOOLS_WEB_FETCH_"` + WriteFile ToolConfig `json:"write_file" yaml:"-" envPrefix:"PICOCLAW_TOOLS_WRITE_FILE_"` } // IsFilterSensitiveDataEnabled returns true if sensitive data filtering is enabled diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index 4f1c5c5e8..47466c3b6 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -195,6 +195,50 @@ func TestLoadConfig_MCPMaxInlineTextChars(t *testing.T) { } } +func TestImageGenerateToolsConfig_EffectiveModel(t *testing.T) { + defaults := AgentDefaults{ImageModel: "legacy-image-model"} + + if got := (ImageGenerateToolsConfig{}).EffectiveModel(defaults); got != "legacy-image-model" { + t.Fatalf("legacy fallback model = %q, want legacy-image-model", got) + } + + cfg := ImageGenerateToolsConfig{Model: "openai-codex/gpt-image-2"} + if got := cfg.EffectiveModel(defaults); got != "openai-codex/gpt-image-2" { + t.Fatalf("tool model = %q, want openai-codex/gpt-image-2", got) + } + + if got := (ImageGenerateToolsConfig{}).EffectiveModel(AgentDefaults{}); got != "gpt-image-2" { + t.Fatalf("default model = %q, want gpt-image-2", got) + } +} + +func TestLoadConfig_ImageGenerateModel(t *testing.T) { + dir := t.TempDir() + configPath := filepath.Join(dir, "config.json") + raw := `{ + "tools": { + "image_generate": { + "enabled": true, + "model": "openai-codex/gpt-image-2" + } + } + }` + if err := os.WriteFile(configPath, []byte(raw), 0o644); err != nil { + t.Fatalf("WriteFile(configPath): %v", err) + } + + cfg, err := LoadConfig(configPath) + if err != nil { + t.Fatalf("LoadConfig() error: %v", err) + } + if !cfg.Tools.ImageGenerate.Enabled { + t.Fatal("cfg.Tools.ImageGenerate.Enabled should be true") + } + if got := cfg.Tools.ImageGenerate.Model; got != "openai-codex/gpt-image-2" { + t.Fatalf("cfg.Tools.ImageGenerate.Model = %q, want openai-codex/gpt-image-2", got) + } +} + func TestConfig_BackwardCompat_NoAgentsList(t *testing.T) { jsonData := `{ "agents": { diff --git a/pkg/config/defaults.go b/pkg/config/defaults.go index 2031892a8..225db5be7 100644 --- a/pkg/config/defaults.go +++ b/pkg/config/defaults.go @@ -397,8 +397,10 @@ func DefaultConfig() *Config { SendFile: ToolConfig{ Enabled: true, }, - ImageGenerate: ToolConfig{ - Enabled: false, + ImageGenerate: ImageGenerateToolsConfig{ + ToolConfig: ToolConfig{ + Enabled: false, + }, }, SendTTS: ToolConfig{ Enabled: false,