mirror of
https://github.com/cexll/myclaude.git
synced 2026-02-05 02:30:26 +08:00
refactor!: remove hardcoded default models, require explicit config
REMOVED all hardcoded default backend/model values from defaultModelsConfig. Now ~/.codeagent/models.json is REQUIRED - missing config returns clear error with example configuration. BREAKING CHANGE: Users must configure ~/.codeagent/models.json before using --agent or parallel tasks with agent: field. Generated with SWE-Agent.ai Co-Authored-By: SWE-Agent.ai <noreply@swe-agent.ai>
This commit is contained in:
@@ -3,6 +3,7 @@ package wrapper
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
config "codeagent-wrapper/internal/config"
|
config "codeagent-wrapper/internal/config"
|
||||||
@@ -29,6 +30,18 @@ func BenchmarkConfigParse_ParseArgs(b *testing.B) {
|
|||||||
b.Setenv("HOME", home)
|
b.Setenv("HOME", home)
|
||||||
b.Setenv("USERPROFILE", home)
|
b.Setenv("USERPROFILE", home)
|
||||||
|
|
||||||
|
configDir := filepath.Join(home, ".codeagent")
|
||||||
|
if err := os.MkdirAll(configDir, 0o755); err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(filepath.Join(configDir, "models.json"), []byte(`{
|
||||||
|
"agents": {
|
||||||
|
"develop": { "backend": "codex", "model": "gpt-test" }
|
||||||
|
}
|
||||||
|
}`), 0o644); err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
config.ResetModelsConfigCacheForTest()
|
config.ResetModelsConfigCacheForTest()
|
||||||
b.Cleanup(config.ResetModelsConfigCacheForTest)
|
b.Cleanup(config.ResetModelsConfigCacheForTest)
|
||||||
|
|
||||||
|
|||||||
@@ -254,7 +254,11 @@ func buildSingleConfig(cmd *cobra.Command, args []string, rawArgv []string, opts
|
|||||||
var resolvedBackend, resolvedModel, resolvedPromptFile, resolvedReasoning string
|
var resolvedBackend, resolvedModel, resolvedPromptFile, resolvedReasoning string
|
||||||
if agentName != "" {
|
if agentName != "" {
|
||||||
var resolvedYolo bool
|
var resolvedYolo bool
|
||||||
resolvedBackend, resolvedModel, resolvedPromptFile, resolvedReasoning, _, _, resolvedYolo = config.ResolveAgentConfig(agentName)
|
var err error
|
||||||
|
resolvedBackend, resolvedModel, resolvedPromptFile, resolvedReasoning, _, _, resolvedYolo, err = config.ResolveAgentConfig(agentName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to resolve agent %q: %w", agentName, err)
|
||||||
|
}
|
||||||
yolo = resolvedYolo
|
yolo = resolvedYolo
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1392,6 +1392,24 @@ func TestBackendParseArgs_PromptFileFlag(t *testing.T) {
|
|||||||
func TestBackendParseArgs_PromptFileOverridesAgent(t *testing.T) {
|
func TestBackendParseArgs_PromptFileOverridesAgent(t *testing.T) {
|
||||||
defer resetTestHooks()
|
defer resetTestHooks()
|
||||||
|
|
||||||
|
home := t.TempDir()
|
||||||
|
t.Setenv("HOME", home)
|
||||||
|
t.Setenv("USERPROFILE", home)
|
||||||
|
t.Cleanup(config.ResetModelsConfigCacheForTest)
|
||||||
|
config.ResetModelsConfigCacheForTest()
|
||||||
|
|
||||||
|
configDir := filepath.Join(home, ".codeagent")
|
||||||
|
if err := os.MkdirAll(configDir, 0o755); err != nil {
|
||||||
|
t.Fatalf("MkdirAll: %v", err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(filepath.Join(configDir, "models.json"), []byte(`{
|
||||||
|
"agents": {
|
||||||
|
"develop": { "backend": "codex", "model": "gpt-test" }
|
||||||
|
}
|
||||||
|
}`), 0o644); err != nil {
|
||||||
|
t.Fatalf("WriteFile: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
os.Args = []string{"codeagent-wrapper", "--prompt-file", "/tmp/custom.md", "--agent", "develop", "task"}
|
os.Args = []string{"codeagent-wrapper", "--prompt-file", "/tmp/custom.md", "--agent", "develop", "task"}
|
||||||
cfg, err := parseArgs()
|
cfg, err := parseArgs()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -7,8 +7,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
ilogger "codeagent-wrapper/internal/logger"
|
|
||||||
|
|
||||||
"github.com/goccy/go-json"
|
"github.com/goccy/go-json"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -35,80 +33,85 @@ type ModelsConfig struct {
|
|||||||
Backends map[string]BackendConfig `json:"backends,omitempty"`
|
Backends map[string]BackendConfig `json:"backends,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var defaultModelsConfig = ModelsConfig{
|
var defaultModelsConfig = ModelsConfig{}
|
||||||
DefaultBackend: "opencode",
|
|
||||||
DefaultModel: "opencode/grok-code",
|
const modelsConfigTildePath = "~/.codeagent/models.json"
|
||||||
Agents: map[string]AgentModelConfig{
|
|
||||||
"oracle": {Backend: "claude", Model: "claude-opus-4-5-20251101", PromptFile: "~/.claude/skills/omo/references/oracle.md", Description: "Technical advisor"},
|
const modelsConfigExample = `{
|
||||||
"librarian": {Backend: "claude", Model: "claude-sonnet-4-5-20250929", PromptFile: "~/.claude/skills/omo/references/librarian.md", Description: "Researcher"},
|
"default_backend": "codex",
|
||||||
"explore": {Backend: "opencode", Model: "opencode/grok-code", PromptFile: "~/.claude/skills/omo/references/explore.md", Description: "Code search"},
|
"default_model": "gpt-4.1",
|
||||||
"develop": {Backend: "codex", Model: "", PromptFile: "~/.claude/skills/omo/references/develop.md", Description: "Code development"},
|
"backends": {
|
||||||
"frontend-ui-ux-engineer": {Backend: "gemini", Model: "", PromptFile: "~/.claude/skills/omo/references/frontend-ui-ux-engineer.md", Description: "Frontend engineer"},
|
"codex": { "api_key": "..." },
|
||||||
"document-writer": {Backend: "gemini", Model: "", PromptFile: "~/.claude/skills/omo/references/document-writer.md", Description: "Documentation"},
|
"claude": { "api_key": "..." }
|
||||||
},
|
},
|
||||||
}
|
"agents": {
|
||||||
|
"develop": {
|
||||||
|
"backend": "codex",
|
||||||
|
"model": "gpt-4.1",
|
||||||
|
"prompt_file": "~/.codeagent/prompts/develop.md",
|
||||||
|
"reasoning": "high",
|
||||||
|
"yolo": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
|
||||||
var (
|
var (
|
||||||
modelsConfigOnce sync.Once
|
modelsConfigOnce sync.Once
|
||||||
modelsConfigCached *ModelsConfig
|
modelsConfigCached *ModelsConfig
|
||||||
|
modelsConfigErr error
|
||||||
)
|
)
|
||||||
|
|
||||||
func modelsConfig() *ModelsConfig {
|
func modelsConfig() (*ModelsConfig, error) {
|
||||||
modelsConfigOnce.Do(func() {
|
modelsConfigOnce.Do(func() {
|
||||||
modelsConfigCached = loadModelsConfig()
|
modelsConfigCached, modelsConfigErr = loadModelsConfig()
|
||||||
})
|
})
|
||||||
if modelsConfigCached == nil {
|
return modelsConfigCached, modelsConfigErr
|
||||||
return &defaultModelsConfig
|
|
||||||
}
|
|
||||||
return modelsConfigCached
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadModelsConfig() *ModelsConfig {
|
func modelsConfigPath() (string, error) {
|
||||||
home, err := os.UserHomeDir()
|
home, err := os.UserHomeDir()
|
||||||
if err != nil {
|
if err != nil || strings.TrimSpace(home) == "" {
|
||||||
ilogger.LogWarn(fmt.Sprintf("Failed to resolve home directory for models config: %v; using defaults", err))
|
return "", fmt.Errorf("failed to resolve user home directory: %w", err)
|
||||||
return &defaultModelsConfig
|
|
||||||
}
|
}
|
||||||
|
|
||||||
configDir := filepath.Clean(filepath.Join(home, ".codeagent"))
|
configDir := filepath.Clean(filepath.Join(home, ".codeagent"))
|
||||||
configPath := filepath.Clean(filepath.Join(configDir, "models.json"))
|
configPath := filepath.Clean(filepath.Join(configDir, "models.json"))
|
||||||
rel, err := filepath.Rel(configDir, configPath)
|
rel, err := filepath.Rel(configDir, configPath)
|
||||||
if err != nil || rel == ".." || strings.HasPrefix(rel, ".."+string(os.PathSeparator)) {
|
if err != nil || rel == ".." || strings.HasPrefix(rel, ".."+string(os.PathSeparator)) {
|
||||||
return &defaultModelsConfig
|
return "", fmt.Errorf("refusing to read models config outside %s: %s", configDir, configPath)
|
||||||
|
}
|
||||||
|
return configPath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func modelsConfigHint(configPath string) string {
|
||||||
|
configPath = strings.TrimSpace(configPath)
|
||||||
|
if configPath == "" {
|
||||||
|
return fmt.Sprintf("Create %s with e.g.:\n%s", modelsConfigTildePath, modelsConfigExample)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("Create %s (resolved to %s) with e.g.:\n%s", modelsConfigTildePath, configPath, modelsConfigExample)
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadModelsConfig() (*ModelsConfig, error) {
|
||||||
|
configPath, err := modelsConfigPath()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%w\n\n%s", err, modelsConfigHint(""))
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := os.ReadFile(configPath) // #nosec G304 -- path is fixed under user home and validated to stay within configDir
|
data, err := os.ReadFile(configPath) // #nosec G304 -- path is fixed under user home and validated to stay within configDir
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
ilogger.LogWarn(fmt.Sprintf("Failed to read models config %s: %v; using defaults", configPath, err))
|
return nil, fmt.Errorf("models config not found: %s\n\n%s", configPath, modelsConfigHint(configPath))
|
||||||
}
|
}
|
||||||
return &defaultModelsConfig
|
return nil, fmt.Errorf("failed to read models config %s: %w\n\n%s", configPath, err, modelsConfigHint(configPath))
|
||||||
}
|
}
|
||||||
|
|
||||||
var cfg ModelsConfig
|
var cfg ModelsConfig
|
||||||
if err := json.Unmarshal(data, &cfg); err != nil {
|
if err := json.Unmarshal(data, &cfg); err != nil {
|
||||||
ilogger.LogWarn(fmt.Sprintf("Failed to parse models config %s: %v; using defaults", configPath, err))
|
return nil, fmt.Errorf("failed to parse models config %s: %w\n\n%s", configPath, err, modelsConfigHint(configPath))
|
||||||
return &defaultModelsConfig
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg.DefaultBackend = strings.TrimSpace(cfg.DefaultBackend)
|
cfg.DefaultBackend = strings.TrimSpace(cfg.DefaultBackend)
|
||||||
if cfg.DefaultBackend == "" {
|
|
||||||
cfg.DefaultBackend = defaultModelsConfig.DefaultBackend
|
|
||||||
}
|
|
||||||
cfg.DefaultModel = strings.TrimSpace(cfg.DefaultModel)
|
cfg.DefaultModel = strings.TrimSpace(cfg.DefaultModel)
|
||||||
if cfg.DefaultModel == "" {
|
|
||||||
cfg.DefaultModel = defaultModelsConfig.DefaultModel
|
|
||||||
}
|
|
||||||
|
|
||||||
// Merge with defaults
|
|
||||||
for name, agent := range defaultModelsConfig.Agents {
|
|
||||||
if _, exists := cfg.Agents[name]; !exists {
|
|
||||||
if cfg.Agents == nil {
|
|
||||||
cfg.Agents = make(map[string]AgentModelConfig)
|
|
||||||
}
|
|
||||||
cfg.Agents[name] = agent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Normalize backend keys so lookups can be case-insensitive.
|
// Normalize backend keys so lookups can be case-insensitive.
|
||||||
if len(cfg.Backends) > 0 {
|
if len(cfg.Backends) > 0 {
|
||||||
@@ -127,7 +130,7 @@ func loadModelsConfig() *ModelsConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return &cfg
|
return &cfg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func LoadDynamicAgent(name string) (AgentModelConfig, bool) {
|
func LoadDynamicAgent(name string) (AgentModelConfig, bool) {
|
||||||
@@ -150,7 +153,10 @@ func LoadDynamicAgent(name string) (AgentModelConfig, bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ResolveBackendConfig(backendName string) (baseURL, apiKey string) {
|
func ResolveBackendConfig(backendName string) (baseURL, apiKey string) {
|
||||||
cfg := modelsConfig()
|
cfg, err := modelsConfig()
|
||||||
|
if err != nil || cfg == nil {
|
||||||
|
return "", ""
|
||||||
|
}
|
||||||
resolved := resolveBackendConfig(cfg, backendName)
|
resolved := resolveBackendConfig(cfg, backendName)
|
||||||
return strings.TrimSpace(resolved.BaseURL), strings.TrimSpace(resolved.APIKey)
|
return strings.TrimSpace(resolved.BaseURL), strings.TrimSpace(resolved.APIKey)
|
||||||
}
|
}
|
||||||
@@ -172,12 +178,30 @@ func resolveBackendConfig(cfg *ModelsConfig, backendName string) BackendConfig {
|
|||||||
return BackendConfig{}
|
return BackendConfig{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func resolveAgentConfig(agentName string) (backend, model, promptFile, reasoning, baseURL, apiKey string, yolo bool) {
|
func resolveAgentConfig(agentName string) (backend, model, promptFile, reasoning, baseURL, apiKey string, yolo bool, err error) {
|
||||||
cfg := modelsConfig()
|
if err := ValidateAgentName(agentName); err != nil {
|
||||||
|
return "", "", "", "", "", "", false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg, err := modelsConfig()
|
||||||
|
if err != nil {
|
||||||
|
return "", "", "", "", "", "", false, err
|
||||||
|
}
|
||||||
|
if cfg == nil {
|
||||||
|
return "", "", "", "", "", "", false, fmt.Errorf("models config is nil\n\n%s", modelsConfigHint(""))
|
||||||
|
}
|
||||||
|
|
||||||
if agent, ok := cfg.Agents[agentName]; ok {
|
if agent, ok := cfg.Agents[agentName]; ok {
|
||||||
backend = strings.TrimSpace(agent.Backend)
|
backend = strings.TrimSpace(agent.Backend)
|
||||||
if backend == "" {
|
if backend == "" {
|
||||||
backend = cfg.DefaultBackend
|
backend = strings.TrimSpace(cfg.DefaultBackend)
|
||||||
|
if backend == "" {
|
||||||
|
configPath, pathErr := modelsConfigPath()
|
||||||
|
if pathErr != nil {
|
||||||
|
return "", "", "", "", "", "", false, fmt.Errorf("agent %q has empty backend and default_backend is not set\n\n%s", agentName, modelsConfigHint(""))
|
||||||
|
}
|
||||||
|
return "", "", "", "", "", "", false, fmt.Errorf("agent %q has empty backend and default_backend is not set\n\n%s", agentName, modelsConfigHint(configPath))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
backendCfg := resolveBackendConfig(cfg, backend)
|
backendCfg := resolveBackendConfig(cfg, backend)
|
||||||
|
|
||||||
@@ -190,31 +214,46 @@ func resolveAgentConfig(agentName string) (backend, model, promptFile, reasoning
|
|||||||
apiKey = strings.TrimSpace(backendCfg.APIKey)
|
apiKey = strings.TrimSpace(backendCfg.APIKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
return backend, strings.TrimSpace(agent.Model), agent.PromptFile, agent.Reasoning, baseURL, apiKey, agent.Yolo
|
model = strings.TrimSpace(agent.Model)
|
||||||
|
if model == "" {
|
||||||
|
configPath, pathErr := modelsConfigPath()
|
||||||
|
if pathErr != nil {
|
||||||
|
return "", "", "", "", "", "", false, fmt.Errorf("agent %q has empty model; set agents.%s.model in %s\n\n%s", agentName, agentName, modelsConfigTildePath, modelsConfigHint(""))
|
||||||
|
}
|
||||||
|
return "", "", "", "", "", "", false, fmt.Errorf("agent %q has empty model; set agents.%s.model in %s\n\n%s", agentName, agentName, modelsConfigTildePath, modelsConfigHint(configPath))
|
||||||
|
}
|
||||||
|
return backend, model, agent.PromptFile, agent.Reasoning, baseURL, apiKey, agent.Yolo, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if dynamic, ok := LoadDynamicAgent(agentName); ok {
|
if dynamic, ok := LoadDynamicAgent(agentName); ok {
|
||||||
backend = cfg.DefaultBackend
|
backend = strings.TrimSpace(cfg.DefaultBackend)
|
||||||
model = cfg.DefaultModel
|
model = strings.TrimSpace(cfg.DefaultModel)
|
||||||
|
configPath, pathErr := modelsConfigPath()
|
||||||
|
if backend == "" || model == "" {
|
||||||
|
if pathErr != nil {
|
||||||
|
return "", "", "", "", "", "", false, fmt.Errorf("dynamic agent %q requires default_backend and default_model to be set in %s\n\n%s", agentName, modelsConfigTildePath, modelsConfigHint(""))
|
||||||
|
}
|
||||||
|
return "", "", "", "", "", "", false, fmt.Errorf("dynamic agent %q requires default_backend and default_model to be set in %s\n\n%s", agentName, modelsConfigTildePath, modelsConfigHint(configPath))
|
||||||
|
}
|
||||||
backendCfg := resolveBackendConfig(cfg, backend)
|
backendCfg := resolveBackendConfig(cfg, backend)
|
||||||
baseURL = strings.TrimSpace(backendCfg.BaseURL)
|
baseURL = strings.TrimSpace(backendCfg.BaseURL)
|
||||||
apiKey = strings.TrimSpace(backendCfg.APIKey)
|
apiKey = strings.TrimSpace(backendCfg.APIKey)
|
||||||
return backend, model, dynamic.PromptFile, "", baseURL, apiKey, false
|
return backend, model, dynamic.PromptFile, "", baseURL, apiKey, false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
backend = cfg.DefaultBackend
|
configPath, pathErr := modelsConfigPath()
|
||||||
model = cfg.DefaultModel
|
if pathErr != nil {
|
||||||
backendCfg := resolveBackendConfig(cfg, backend)
|
return "", "", "", "", "", "", false, fmt.Errorf("agent %q not found in %s\n\n%s", agentName, modelsConfigTildePath, modelsConfigHint(""))
|
||||||
baseURL = strings.TrimSpace(backendCfg.BaseURL)
|
}
|
||||||
apiKey = strings.TrimSpace(backendCfg.APIKey)
|
return "", "", "", "", "", "", false, fmt.Errorf("agent %q not found in %s\n\n%s", agentName, modelsConfigTildePath, modelsConfigHint(configPath))
|
||||||
return backend, model, "", "", baseURL, apiKey, false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func ResolveAgentConfig(agentName string) (backend, model, promptFile, reasoning, baseURL, apiKey string, yolo bool) {
|
func ResolveAgentConfig(agentName string) (backend, model, promptFile, reasoning, baseURL, apiKey string, yolo bool, err error) {
|
||||||
return resolveAgentConfig(agentName)
|
return resolveAgentConfig(agentName)
|
||||||
}
|
}
|
||||||
|
|
||||||
func ResetModelsConfigCacheForTest() {
|
func ResetModelsConfigCacheForTest() {
|
||||||
modelsConfigCached = nil
|
modelsConfigCached = nil
|
||||||
|
modelsConfigErr = nil
|
||||||
modelsConfigOnce = sync.Once{}
|
modelsConfigOnce = sync.Once{}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,78 +3,43 @@ package config
|
|||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestResolveAgentConfig_Defaults(t *testing.T) {
|
func TestResolveAgentConfig_NoConfig_ReturnsHelpfulError(t *testing.T) {
|
||||||
home := t.TempDir()
|
home := t.TempDir()
|
||||||
t.Setenv("HOME", home)
|
t.Setenv("HOME", home)
|
||||||
t.Setenv("USERPROFILE", home)
|
t.Setenv("USERPROFILE", home)
|
||||||
t.Cleanup(ResetModelsConfigCacheForTest)
|
t.Cleanup(ResetModelsConfigCacheForTest)
|
||||||
ResetModelsConfigCacheForTest()
|
ResetModelsConfigCacheForTest()
|
||||||
|
|
||||||
// Test that default agents resolve correctly without config file
|
_, _, _, _, _, _, _, err := ResolveAgentConfig("develop")
|
||||||
tests := []struct {
|
if err == nil {
|
||||||
agent string
|
t.Fatalf("expected error, got nil")
|
||||||
wantBackend string
|
|
||||||
wantModel string
|
|
||||||
wantPromptFile string
|
|
||||||
}{
|
|
||||||
{"oracle", "claude", "claude-opus-4-5-20251101", "~/.claude/skills/omo/references/oracle.md"},
|
|
||||||
{"librarian", "claude", "claude-sonnet-4-5-20250929", "~/.claude/skills/omo/references/librarian.md"},
|
|
||||||
{"explore", "opencode", "opencode/grok-code", "~/.claude/skills/omo/references/explore.md"},
|
|
||||||
{"frontend-ui-ux-engineer", "gemini", "", "~/.claude/skills/omo/references/frontend-ui-ux-engineer.md"},
|
|
||||||
{"document-writer", "gemini", "", "~/.claude/skills/omo/references/document-writer.md"},
|
|
||||||
}
|
}
|
||||||
|
msg := err.Error()
|
||||||
for _, tt := range tests {
|
if !strings.Contains(msg, modelsConfigTildePath) {
|
||||||
t.Run(tt.agent, func(t *testing.T) {
|
t.Fatalf("error should mention %s, got: %s", modelsConfigTildePath, msg)
|
||||||
backend, model, promptFile, _, _, _, _ := resolveAgentConfig(tt.agent)
|
|
||||||
if backend != tt.wantBackend {
|
|
||||||
t.Errorf("backend = %q, want %q", backend, tt.wantBackend)
|
|
||||||
}
|
}
|
||||||
if model != tt.wantModel {
|
if !strings.Contains(msg, filepath.Join(home, ".codeagent", "models.json")) {
|
||||||
t.Errorf("model = %q, want %q", model, tt.wantModel)
|
t.Fatalf("error should mention resolved config path, got: %s", msg)
|
||||||
}
|
}
|
||||||
if promptFile != tt.wantPromptFile {
|
if !strings.Contains(msg, "\"agents\"") {
|
||||||
t.Errorf("promptFile = %q, want %q", promptFile, tt.wantPromptFile)
|
t.Fatalf("error should include example config, got: %s", msg)
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestResolveAgentConfig_UnknownAgent(t *testing.T) {
|
|
||||||
home := t.TempDir()
|
|
||||||
t.Setenv("HOME", home)
|
|
||||||
t.Setenv("USERPROFILE", home)
|
|
||||||
t.Cleanup(ResetModelsConfigCacheForTest)
|
|
||||||
ResetModelsConfigCacheForTest()
|
|
||||||
|
|
||||||
backend, model, promptFile, _, _, _, _ := resolveAgentConfig("unknown-agent")
|
|
||||||
if backend != "opencode" {
|
|
||||||
t.Errorf("unknown agent backend = %q, want %q", backend, "opencode")
|
|
||||||
}
|
|
||||||
if model != "opencode/grok-code" {
|
|
||||||
t.Errorf("unknown agent model = %q, want %q", model, "opencode/grok-code")
|
|
||||||
}
|
|
||||||
if promptFile != "" {
|
|
||||||
t.Errorf("unknown agent promptFile = %q, want empty", promptFile)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLoadModelsConfig_NoFile(t *testing.T) {
|
func TestLoadModelsConfig_NoFile(t *testing.T) {
|
||||||
home := "/nonexistent/path/that/does/not/exist"
|
home := t.TempDir()
|
||||||
t.Setenv("HOME", home)
|
t.Setenv("HOME", home)
|
||||||
t.Setenv("USERPROFILE", home)
|
t.Setenv("USERPROFILE", home)
|
||||||
t.Cleanup(ResetModelsConfigCacheForTest)
|
t.Cleanup(ResetModelsConfigCacheForTest)
|
||||||
ResetModelsConfigCacheForTest()
|
ResetModelsConfigCacheForTest()
|
||||||
|
|
||||||
cfg := loadModelsConfig()
|
_, err := loadModelsConfig()
|
||||||
if cfg.DefaultBackend != "opencode" {
|
if err == nil {
|
||||||
t.Errorf("DefaultBackend = %q, want %q", cfg.DefaultBackend, "opencode")
|
t.Fatalf("expected error, got nil")
|
||||||
}
|
|
||||||
if len(cfg.Agents) != 6 {
|
|
||||||
t.Errorf("len(Agents) = %d, want 6", len(cfg.Agents))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,7 +84,10 @@ func TestLoadModelsConfig_WithFile(t *testing.T) {
|
|||||||
t.Cleanup(ResetModelsConfigCacheForTest)
|
t.Cleanup(ResetModelsConfigCacheForTest)
|
||||||
ResetModelsConfigCacheForTest()
|
ResetModelsConfigCacheForTest()
|
||||||
|
|
||||||
cfg := loadModelsConfig()
|
cfg, err := loadModelsConfig()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("loadModelsConfig: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
if cfg.DefaultBackend != "claude" {
|
if cfg.DefaultBackend != "claude" {
|
||||||
t.Errorf("DefaultBackend = %q, want %q", cfg.DefaultBackend, "claude")
|
t.Errorf("DefaultBackend = %q, want %q", cfg.DefaultBackend, "claude")
|
||||||
@@ -140,9 +108,8 @@ func TestLoadModelsConfig_WithFile(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check that defaults are merged
|
if _, ok := cfg.Agents["oracle"]; ok {
|
||||||
if _, ok := cfg.Agents["oracle"]; !ok {
|
t.Error("oracle should not be present without explicit config")
|
||||||
t.Error("default agent oracle should be merged")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
baseURL, apiKey := ResolveBackendConfig("claude")
|
baseURL, apiKey := ResolveBackendConfig("claude")
|
||||||
@@ -153,7 +120,10 @@ func TestLoadModelsConfig_WithFile(t *testing.T) {
|
|||||||
t.Errorf("ResolveBackendConfig(apiKey) = %q, want %q", apiKey, "backend-key")
|
t.Errorf("ResolveBackendConfig(apiKey) = %q, want %q", apiKey, "backend-key")
|
||||||
}
|
}
|
||||||
|
|
||||||
backend, model, _, _, agentBaseURL, agentAPIKey, _ := ResolveAgentConfig("custom-agent")
|
backend, model, _, _, agentBaseURL, agentAPIKey, _, err := ResolveAgentConfig("custom-agent")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ResolveAgentConfig(custom-agent): %v", err)
|
||||||
|
}
|
||||||
if backend != "codex" {
|
if backend != "codex" {
|
||||||
t.Errorf("ResolveAgentConfig(backend) = %q, want %q", backend, "codex")
|
t.Errorf("ResolveAgentConfig(backend) = %q, want %q", backend, "codex")
|
||||||
}
|
}
|
||||||
@@ -183,12 +153,26 @@ func TestResolveAgentConfig_DynamicAgent(t *testing.T) {
|
|||||||
t.Fatalf("WriteFile: %v", err)
|
t.Fatalf("WriteFile: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
backend, model, promptFile, _, _, _, _ := resolveAgentConfig("sarsh")
|
configDir := filepath.Join(home, ".codeagent")
|
||||||
if backend != "opencode" {
|
if err := os.MkdirAll(configDir, 0o755); err != nil {
|
||||||
t.Errorf("backend = %q, want %q", backend, "opencode")
|
t.Fatalf("MkdirAll: %v", err)
|
||||||
}
|
}
|
||||||
if model != "opencode/grok-code" {
|
if err := os.WriteFile(filepath.Join(configDir, "models.json"), []byte(`{
|
||||||
t.Errorf("model = %q, want %q", model, "opencode/grok-code")
|
"default_backend": "codex",
|
||||||
|
"default_model": "gpt-test"
|
||||||
|
}`), 0o644); err != nil {
|
||||||
|
t.Fatalf("WriteFile: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
backend, model, promptFile, _, _, _, _, err := ResolveAgentConfig("sarsh")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ResolveAgentConfig(sarsh): %v", err)
|
||||||
|
}
|
||||||
|
if backend != "codex" {
|
||||||
|
t.Errorf("backend = %q, want %q", backend, "codex")
|
||||||
|
}
|
||||||
|
if model != "gpt-test" {
|
||||||
|
t.Errorf("model = %q, want %q", model, "gpt-test")
|
||||||
}
|
}
|
||||||
if promptFile != "~/.codeagent/agents/sarsh.md" {
|
if promptFile != "~/.codeagent/agents/sarsh.md" {
|
||||||
t.Errorf("promptFile = %q, want %q", promptFile, "~/.codeagent/agents/sarsh.md")
|
t.Errorf("promptFile = %q, want %q", promptFile, "~/.codeagent/agents/sarsh.md")
|
||||||
@@ -213,9 +197,66 @@ func TestLoadModelsConfig_InvalidJSON(t *testing.T) {
|
|||||||
t.Cleanup(ResetModelsConfigCacheForTest)
|
t.Cleanup(ResetModelsConfigCacheForTest)
|
||||||
ResetModelsConfigCacheForTest()
|
ResetModelsConfigCacheForTest()
|
||||||
|
|
||||||
cfg := loadModelsConfig()
|
_, err := loadModelsConfig()
|
||||||
// Should fall back to defaults
|
if err == nil {
|
||||||
if cfg.DefaultBackend != "opencode" {
|
t.Fatalf("expected error, got nil")
|
||||||
t.Errorf("invalid JSON should fallback, got DefaultBackend = %q", cfg.DefaultBackend)
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveAgentConfig_UnknownAgent_ReturnsError(t *testing.T) {
|
||||||
|
home := t.TempDir()
|
||||||
|
t.Setenv("HOME", home)
|
||||||
|
t.Setenv("USERPROFILE", home)
|
||||||
|
t.Cleanup(ResetModelsConfigCacheForTest)
|
||||||
|
ResetModelsConfigCacheForTest()
|
||||||
|
|
||||||
|
configDir := filepath.Join(home, ".codeagent")
|
||||||
|
if err := os.MkdirAll(configDir, 0o755); err != nil {
|
||||||
|
t.Fatalf("MkdirAll: %v", err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(filepath.Join(configDir, "models.json"), []byte(`{
|
||||||
|
"default_backend": "codex",
|
||||||
|
"default_model": "gpt-test",
|
||||||
|
"agents": {
|
||||||
|
"develop": { "backend": "codex", "model": "gpt-test" }
|
||||||
|
}
|
||||||
|
}`), 0o644); err != nil {
|
||||||
|
t.Fatalf("WriteFile: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _, _, _, _, _, _, err := ResolveAgentConfig("unknown-agent")
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("expected error, got nil")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "unknown-agent") {
|
||||||
|
t.Fatalf("error should mention agent name, got: %s", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveAgentConfig_EmptyModel_ReturnsError(t *testing.T) {
|
||||||
|
home := t.TempDir()
|
||||||
|
t.Setenv("HOME", home)
|
||||||
|
t.Setenv("USERPROFILE", home)
|
||||||
|
t.Cleanup(ResetModelsConfigCacheForTest)
|
||||||
|
ResetModelsConfigCacheForTest()
|
||||||
|
|
||||||
|
configDir := filepath.Join(home, ".codeagent")
|
||||||
|
if err := os.MkdirAll(configDir, 0o755); err != nil {
|
||||||
|
t.Fatalf("MkdirAll: %v", err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(filepath.Join(configDir, "models.json"), []byte(`{
|
||||||
|
"agents": {
|
||||||
|
"bad-agent": { "backend": "codex", "model": " " }
|
||||||
|
}
|
||||||
|
}`), 0o644); err != nil {
|
||||||
|
t.Fatalf("WriteFile: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _, _, _, _, _, _, err := ResolveAgentConfig("bad-agent")
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("expected error, got nil")
|
||||||
|
}
|
||||||
|
if !strings.Contains(strings.ToLower(err.Error()), "empty model") {
|
||||||
|
t.Fatalf("error should mention empty model, got: %s", err.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,7 +46,10 @@ func TestEnvInjectionWithAgent(t *testing.T) {
|
|||||||
defer config.ResetModelsConfigCacheForTest()
|
defer config.ResetModelsConfigCacheForTest()
|
||||||
|
|
||||||
// Test ResolveAgentConfig
|
// Test ResolveAgentConfig
|
||||||
agentBackend, model, _, _, baseURL, apiKey, _ := config.ResolveAgentConfig("test-agent")
|
agentBackend, model, _, _, baseURL, apiKey, _, err := config.ResolveAgentConfig("test-agent")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ResolveAgentConfig: %v", err)
|
||||||
|
}
|
||||||
t.Logf("ResolveAgentConfig: backend=%q, model=%q, baseURL=%q, apiKey=%q",
|
t.Logf("ResolveAgentConfig: backend=%q, model=%q, baseURL=%q, apiKey=%q",
|
||||||
agentBackend, model, baseURL, apiKey)
|
agentBackend, model, baseURL, apiKey)
|
||||||
|
|
||||||
@@ -118,7 +121,10 @@ func TestEnvInjectionLogic(t *testing.T) {
|
|||||||
|
|
||||||
// Step 2: If agent specified, get agent config
|
// Step 2: If agent specified, get agent config
|
||||||
if agentName != "" {
|
if agentName != "" {
|
||||||
agentBackend, _, _, _, agentBaseURL, agentAPIKey, _ := config.ResolveAgentConfig(agentName)
|
agentBackend, _, _, _, agentBaseURL, agentAPIKey, _, err := config.ResolveAgentConfig(agentName)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ResolveAgentConfig(%q): %v", agentName, err)
|
||||||
|
}
|
||||||
t.Logf("Step 2 - ResolveAgentConfig(%q): backend=%q, baseURL=%q, apiKey=%q",
|
t.Logf("Step 2 - ResolveAgentConfig(%q): backend=%q, baseURL=%q, apiKey=%q",
|
||||||
agentName, agentBackend, agentBaseURL, agentAPIKey)
|
agentName, agentBackend, agentBaseURL, agentAPIKey)
|
||||||
|
|
||||||
|
|||||||
@@ -1060,11 +1060,13 @@ func RunCodexTaskWithContext(parentCtx context.Context, taskSpec TaskSpec, backe
|
|||||||
if envBackend != nil {
|
if envBackend != nil {
|
||||||
baseURL, apiKey := config.ResolveBackendConfig(cfg.Backend)
|
baseURL, apiKey := config.ResolveBackendConfig(cfg.Backend)
|
||||||
if agentName := strings.TrimSpace(taskSpec.Agent); agentName != "" {
|
if agentName := strings.TrimSpace(taskSpec.Agent); agentName != "" {
|
||||||
agentBackend, _, _, _, agentBaseURL, agentAPIKey, _ := config.ResolveAgentConfig(agentName)
|
agentBackend, _, _, _, agentBaseURL, agentAPIKey, _, err := config.ResolveAgentConfig(agentName)
|
||||||
|
if err == nil {
|
||||||
if strings.EqualFold(strings.TrimSpace(agentBackend), strings.TrimSpace(cfg.Backend)) {
|
if strings.EqualFold(strings.TrimSpace(agentBackend), strings.TrimSpace(cfg.Backend)) {
|
||||||
baseURL, apiKey = agentBaseURL, agentAPIKey
|
baseURL, apiKey = agentBaseURL, agentAPIKey
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if injected := envBackend.Env(baseURL, apiKey); len(injected) > 0 {
|
if injected := envBackend.Env(baseURL, apiKey); len(injected) > 0 {
|
||||||
cmd.SetEnv(injected)
|
cmd.SetEnv(injected)
|
||||||
// Log injected env vars with masked API keys (to file and stderr)
|
// Log injected env vars with masked API keys (to file and stderr)
|
||||||
|
|||||||
@@ -96,7 +96,10 @@ func ParseParallelConfig(data []byte) (*ParallelConfig, error) {
|
|||||||
if err := config.ValidateAgentName(task.Agent); err != nil {
|
if err := config.ValidateAgentName(task.Agent); err != nil {
|
||||||
return nil, fmt.Errorf("task block #%d invalid agent name: %w", taskIndex, err)
|
return nil, fmt.Errorf("task block #%d invalid agent name: %w", taskIndex, err)
|
||||||
}
|
}
|
||||||
backend, model, promptFile, reasoning, _, _, _ := config.ResolveAgentConfig(task.Agent)
|
backend, model, promptFile, reasoning, _, _, _, err := config.ResolveAgentConfig(task.Agent)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("task block #%d failed to resolve agent %q: %w", taskIndex, task.Agent, err)
|
||||||
|
}
|
||||||
if task.Backend == "" {
|
if task.Backend == "" {
|
||||||
task.Backend = backend
|
task.Backend = backend
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ EOF
|
|||||||
|
|
||||||
## ~/.codeagent/models.json Configuration
|
## ~/.codeagent/models.json Configuration
|
||||||
|
|
||||||
Optional. Uses codeagent-wrapper built-in config by default. To customize:
|
Required when using `agent:` in parallel tasks or `--agent`. Create `~/.codeagent/models.json` to configure agent → backend/model mappings:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user