diff --git a/internal/components/sdd/read_assignments.go b/internal/components/sdd/read_assignments.go index a6f449a8..21a72203 100644 --- a/internal/components/sdd/read_assignments.go +++ b/internal/components/sdd/read_assignments.go @@ -3,7 +3,6 @@ package sdd import ( "encoding/json" "os" - "strings" "github.com/gentleman-programming/gentle-ai/internal/model" "github.com/gentleman-programming/gentle-ai/internal/opencode" @@ -79,18 +78,23 @@ func ReadCurrentModelAssignments(settingsPath string) (map[string]model.ModelAss if !ok || modelStr == "" { continue } - // Try colon first (standard: "anthropic:claude-sonnet-4"), then slash - // ("zai-coding-plan/glm-5-turbo") for custom providers (issue #152). - idx := strings.Index(modelStr, ":") - if idx <= 0 { - idx = strings.Index(modelStr, "/") + // Find the first separator (either '/' or ':') to correctly parse + // model specs like "openrouter/qwen/qwen3.6-plus:free" where the + // provider is before the first slash, not before the colon. + // Issue #802: colon-first parsing broke OpenRouter free-model specs. + sep := -1 + for i, c := range modelStr { + if c == '/' || c == ':' { + sep = i + break + } } - if idx <= 0 { + if sep <= 0 { // No separator or separator is the first character — skip malformed value. continue } - providerID := modelStr[:idx] - modelID := modelStr[idx+1:] + providerID := modelStr[:sep] + modelID := modelStr[sep+1:] if modelID == "" { continue } diff --git a/internal/components/sdd/read_assignments_test.go b/internal/components/sdd/read_assignments_test.go index 7e92e8da..deb57b63 100644 --- a/internal/components/sdd/read_assignments_test.go +++ b/internal/components/sdd/read_assignments_test.go @@ -215,6 +215,40 @@ func TestReadCurrentModelAssignmentsSlashSeparator(t *testing.T) { } } +// TestReadCurrentModelAssignmentsOpenRouterFreeModel verifies that model specs +// with multiple slashes and a colon (like "openrouter/qwen/qwen3.6-plus:free") +// are parsed correctly. The provider is everything before the FIRST separator +// (slash or colon), not before the colon. Issue #802. +func TestReadCurrentModelAssignmentsOpenRouterFreeModel(t *testing.T) { + dir := t.TempDir() + settingsPath := filepath.Join(dir, "opencode.json") + + content := `{ + "agent": { + "sdd-apply": { "model": "openrouter/qwen/qwen3.6-plus:free" } + } +}` + if err := os.WriteFile(settingsPath, []byte(content), 0o644); err != nil { + t.Fatalf("write settings: %v", err) + } + + got, err := ReadCurrentModelAssignments(settingsPath) + if err != nil { + t.Fatalf("ReadCurrentModelAssignments() error = %v", err) + } + + a, ok := got["sdd-apply"] + if !ok { + t.Fatal("sdd-apply missing from result — OpenRouter free-model format not parsed") + } + if a.ProviderID != "openrouter" { + t.Errorf("ProviderID = %q, want %q", a.ProviderID, "openrouter") + } + if a.ModelID != "qwen/qwen3.6-plus:free" { + t.Errorf("ModelID = %q, want %q", a.ModelID, "qwen/qwen3.6-plus:free") + } +} + // TestReadCurrentModelAssignmentsReadsVariant verifies that the // variant field in an agent definition is populated on the returned // ModelAssignment.Effort. diff --git a/internal/update/upgrade/executor_test.go b/internal/update/upgrade/executor_test.go index 7d366dc3..dd17622c 100644 --- a/internal/update/upgrade/executor_test.go +++ b/internal/update/upgrade/executor_test.go @@ -13,6 +13,7 @@ import ( "testing" "github.com/gentleman-programming/gentle-ai/internal/backup" + "github.com/gentleman-programming/gentle-ai/internal/components/gga" "github.com/gentleman-programming/gentle-ai/internal/model" "github.com/gentleman-programming/gentle-ai/internal/state" "github.com/gentleman-programming/gentle-ai/internal/system" @@ -856,8 +857,9 @@ func TestConfigPathsForBackup_CoversRegistryAgentsNotInOldList(t *testing.T) { func TestConfigPathsForBackup_GGAExtrasAreIncluded(t *testing.T) { homeDir := t.TempDir() - // Create GGA config file at ~/.config/gga/config - ggaConfigFile := filepath.Join(homeDir, ".config", "gga", "config") + // Use gga package functions to get platform-correct paths + // (Windows uses %APPDATA%\gga, Unix uses ~/.config/gga) + ggaConfigFile := gga.ConfigPath(homeDir) if err := os.MkdirAll(filepath.Dir(ggaConfigFile), 0o755); err != nil { t.Fatalf("MkdirAll gga config: %v", err) } @@ -865,8 +867,9 @@ func TestConfigPathsForBackup_GGAExtrasAreIncluded(t *testing.T) { t.Fatalf("WriteFile gga config: %v", err) } - // Create GGA runtime lib file at ~/.local/share/gga/lib/pr_mode.sh - ggaLibFile := filepath.Join(homeDir, ".local", "share", "gga", "lib", "pr_mode.sh") + // GGA runtime lib file at ~/.local/share/gga/lib/pr_mode.sh + // (same path on all platforms) + ggaLibFile := gga.RuntimePRModePath(homeDir) if err := os.MkdirAll(filepath.Dir(ggaLibFile), 0o755); err != nil { t.Fatalf("MkdirAll gga lib: %v", err) }