diff --git a/.gitignore b/.gitignore index 0447fdfd..699fc754 100644 --- a/.gitignore +++ b/.gitignore @@ -49,3 +49,9 @@ _bmad-output/* # macOS .DS_Store ._* + +# Opencode +.beads/ +.opencode/ +.cli-proxy-api/ +.venv/ diff --git a/cmd/server/main.go b/cmd/server/main.go index e12e5261..8bc41c78 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -70,6 +70,7 @@ func main() { var kimiLogin bool var projectID string var vertexImport string + var vertexImportPrefix string var configPath string var password string var tuiMode bool @@ -91,6 +92,7 @@ func main() { flag.StringVar(&projectID, "project_id", "", "Project ID (Gemini only, not required)") flag.StringVar(&configPath, "config", DefaultConfigPath, "Configure File Path") flag.StringVar(&vertexImport, "vertex-import", "", "Import Vertex service account key JSON file") + flag.StringVar(&vertexImportPrefix, "vertex-import-prefix", "", "Prefix for Vertex model namespacing (use with -vertex-import)") flag.StringVar(&password, "password", "", "") flag.BoolVar(&tuiMode, "tui", false, "Start with terminal management UI") flag.BoolVar(&standalone, "standalone", false, "In TUI mode, start an embedded local server") @@ -462,7 +464,7 @@ func main() { if vertexImport != "" { // Handle Vertex service account import - cmd.DoVertexImport(cfg, vertexImport) + cmd.DoVertexImport(cfg, vertexImport, vertexImportPrefix) } else if login { // Handle Google/Gemini login cmd.DoLogin(cfg, projectID, options) diff --git a/internal/auth/vertex/vertex_credentials.go b/internal/auth/vertex/vertex_credentials.go index 4853d340..9f830994 100644 --- a/internal/auth/vertex/vertex_credentials.go +++ b/internal/auth/vertex/vertex_credentials.go @@ -30,6 +30,10 @@ type VertexCredentialStorage struct { // Type is the provider identifier stored alongside credentials. Always "vertex". Type string `json:"type"` + + // Prefix optionally namespaces models for this credential (e.g., "teamA"). + // This results in model names like "teamA/gemini-2.0-flash". + Prefix string `json:"prefix,omitempty"` } // SaveTokenToFile writes the credential payload to the given file path in JSON format. diff --git a/internal/cmd/vertex_import.go b/internal/cmd/vertex_import.go index 32d782d8..4aa0d74b 100644 --- a/internal/cmd/vertex_import.go +++ b/internal/cmd/vertex_import.go @@ -20,7 +20,7 @@ import ( // DoVertexImport imports a Google Cloud service account key JSON and persists // it as a "vertex" provider credential. The file content is embedded in the auth // file to allow portable deployment across stores. -func DoVertexImport(cfg *config.Config, keyPath string) { +func DoVertexImport(cfg *config.Config, keyPath string, prefix string) { if cfg == nil { cfg = &config.Config{} } @@ -62,13 +62,28 @@ func DoVertexImport(cfg *config.Config, keyPath string) { // Default location if not provided by user. Can be edited in the saved file later. location := "us-central1" - fileName := fmt.Sprintf("vertex-%s.json", sanitizeFilePart(projectID)) + // Normalize and validate prefix: must be a single segment (no "/" allowed). + prefix = strings.TrimSpace(prefix) + prefix = strings.Trim(prefix, "/") + if prefix != "" && strings.Contains(prefix, "/") { + log.Errorf("vertex-import: prefix must be a single segment (no '/' allowed): %q", prefix) + return + } + + // Include prefix in filename so importing the same project with different + // prefixes creates separate credential files instead of overwriting. + baseName := sanitizeFilePart(projectID) + if prefix != "" { + baseName = sanitizeFilePart(prefix) + "-" + baseName + } + fileName := fmt.Sprintf("vertex-%s.json", baseName) // Build auth record storage := &vertex.VertexCredentialStorage{ ServiceAccount: sa, ProjectID: projectID, Email: email, Location: location, + Prefix: prefix, } metadata := map[string]any{ "service_account": sa, @@ -76,6 +91,7 @@ func DoVertexImport(cfg *config.Config, keyPath string) { "email": email, "location": location, "type": "vertex", + "prefix": prefix, "label": labelForVertex(projectID, email), } record := &coreauth.Auth{