Skip to content

Commit b047586

Browse files
committed
Add support for using CC via VertexAI
1 parent 34ac6f2 commit b047586

File tree

4 files changed

+297
-62
lines changed

4 files changed

+297
-62
lines changed

src/claude.rs

Lines changed: 51 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,21 @@ pub async fn query_with_options(prompt: &str, options: QueryOptions) -> Result<(
4444
let config = Config::load()?;
4545
let paths = config::paths()?;
4646

47-
// Get credential
48-
let credential = config
49-
.claude
50-
.api_key
51-
.ok_or_else(|| anyhow!("No credential configured. Run `cica init` to set up Claude."))?;
47+
// Resolve credential or Vertex config
48+
let use_vertex = config.claude.use_vertex;
49+
let vertex_project_id = config.claude.vertex_project_id.as_deref();
50+
let credential = config.claude.api_key.as_deref();
51+
52+
if use_vertex {
53+
let project_id = vertex_project_id
54+
.filter(|s| !s.is_empty())
55+
.ok_or_else(|| anyhow!("Vertex AI is enabled but no project ID is set. Run `cica init` to configure Vertex AI."))?;
56+
debug!("Using Vertex AI project: {}", project_id);
57+
} else {
58+
credential.ok_or_else(|| {
59+
anyhow!("No credential configured. Run `cica init` to set up Claude.")
60+
})?;
61+
}
5262

5363
// Get Bun path
5464
let bun = setup::find_bun()
@@ -100,14 +110,43 @@ pub async fn query_with_options(prompt: &str, options: QueryOptions) -> Result<(
100110
// Add the prompt
101111
cmd.arg(prompt);
102112

103-
// Set auth env var based on credential type
104-
match setup::detect_credential_type(&credential) {
105-
setup::CredentialType::ApiKey => {
106-
cmd.env("ANTHROPIC_API_KEY", &credential);
113+
// Set auth env vars: either Vertex AI (GCP) or Anthropic API key / OAuth
114+
if use_vertex {
115+
cmd.env("CLAUDE_CODE_USE_VERTEX", "1");
116+
cmd.env(
117+
"ANTHROPIC_VERTEX_PROJECT_ID",
118+
vertex_project_id.unwrap_or(""),
119+
);
120+
cmd.env(
121+
"CLOUD_ML_REGION",
122+
config
123+
.claude
124+
.vertex_region
125+
.as_deref()
126+
.unwrap_or("europe-west1"),
127+
);
128+
// Long-lived auth: service account key file (recommended for servers; no gcloud expiry)
129+
if let Some(ref cred_path) = config.claude.vertex_credentials_path {
130+
let path = std::path::Path::new(cred_path);
131+
let abs = if path.is_relative() {
132+
paths.base.join(cred_path)
133+
} else {
134+
path.to_path_buf()
135+
};
136+
if abs.exists() {
137+
cmd.env("GOOGLE_APPLICATION_CREDENTIALS", &abs);
138+
}
107139
}
108-
setup::CredentialType::OAuthToken => {
109-
cmd.env("CLAUDE_CODE_OAUTH_TOKEN", &credential);
110-
cmd.env("ANTHROPIC_OAUTH_TOKEN", &credential);
140+
// Otherwise Vertex uses gcloud ADC or existing GOOGLE_APPLICATION_CREDENTIALS env
141+
} else if let Some(cred) = credential {
142+
match setup::detect_credential_type(cred) {
143+
setup::CredentialType::ApiKey => {
144+
cmd.env("ANTHROPIC_API_KEY", cred);
145+
}
146+
setup::CredentialType::OAuthToken => {
147+
cmd.env("CLAUDE_CODE_OAUTH_TOKEN", cred);
148+
cmd.env("ANTHROPIC_OAUTH_TOKEN", cred);
149+
}
111150
}
112151
}
113152

src/cmd/init.rs

Lines changed: 155 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -840,71 +840,179 @@ async fn setup_claude(existing_config: Option<Config>) -> Result<()> {
840840
}
841841
}
842842

843-
// Get authentication
843+
// Choose provider: Anthropic vs Google Vertex AI
844844
println!();
845-
println!("Cica uses Claude Code, which can be billed through your Claude");
846-
println!("subscription or based on API usage through your Console account.");
845+
println!("Claude Code can use Anthropic directly or Google Vertex AI (GCP).");
847846
println!();
848847

849-
let auth_choices = vec![
850-
"Claude subscription Pro, Max, Team, or Enterprise",
851-
"Anthropic Console API usage billing",
848+
let provider_choices = vec![
849+
"Anthropic Subscription (Pro/Max/Team) or API key",
850+
"Google Vertex AI GCP project (billing via Google Cloud)",
852851
];
853852

854-
let auth_selection = Select::with_theme(&ColorfulTheme::default())
855-
.with_prompt("Select login method")
856-
.items(&auth_choices)
853+
let provider_selection = Select::with_theme(&ColorfulTheme::default())
854+
.with_prompt("Select Claude Code provider")
855+
.items(&provider_choices)
857856
.default(0)
858857
.interact()?;
859858

860-
let credential = match auth_selection {
861-
0 => {
862-
// OAuth / setup-token flow
863-
println!();
864-
println!("Run this command in any terminal:");
865-
println!();
866-
println!(" claude setup-token");
867-
println!();
868-
println!("Note: The token may display across two lines, but it's one");
869-
println!("continuous string. Copy and paste it as a single line.");
870-
println!();
859+
let mut config = existing_config.unwrap_or_default();
860+
let was_using_cursor = config.backend == AiBackend::Cursor && config.is_cursor_configured();
871861

872-
Password::with_theme(&ColorfulTheme::default())
873-
.with_prompt("Paste the setup token")
874-
.interact()?
875-
}
876-
1 => {
877-
// API Key flow
862+
if provider_selection == 1 {
863+
// Vertex AI setup
864+
let paths = config::paths()?;
865+
println!();
866+
println!("Vertex AI Setup");
867+
println!("───────────────");
868+
println!();
869+
println!("You need a GCP project with Vertex AI enabled and Claude models");
870+
println!("enabled in Model Garden.");
871+
println!();
872+
873+
let project_id: String = Input::with_theme(&ColorfulTheme::default())
874+
.with_prompt("GCP project ID")
875+
.interact_text()?;
876+
877+
let region: String = Input::with_theme(&ColorfulTheme::default())
878+
.with_prompt(
879+
"Region (e.g. europe-west1 or us-east5; see code.claude.com/docs/google-vertex-ai)",
880+
)
881+
.default("europe-west1".to_string())
882+
.interact_text()?;
883+
884+
let auth_choices = vec![
885+
"Service account key file (JSON) Long-lived; recommended for servers",
886+
"gcloud application-default login Uses your user credentials (may expire)",
887+
];
888+
let auth_selection = Select::with_theme(&ColorfulTheme::default())
889+
.with_prompt("GCP auth (for servers use a service account key so auth does not expire)")
890+
.items(&auth_choices)
891+
.default(0)
892+
.interact()?;
893+
894+
let vertex_credentials_path: Option<String> = if auth_selection == 0 {
878895
println!();
879-
println!("1. Go to https://console.anthropic.com/settings/keys");
880-
println!("2. Create a new API key");
896+
println!("Create a service account in GCP with Vertex AI User (or similar),");
897+
println!("download its JSON key, and enter the path below.");
898+
println!("Path can be absolute or relative to your Cica config directory.");
881899
println!();
900+
let path: String = Input::with_theme(&ColorfulTheme::default())
901+
.with_prompt("Path to service account JSON key file")
902+
.interact_text()?;
903+
let path = path.trim().to_string();
904+
if path.is_empty() {
905+
None
906+
} else {
907+
print!("Validating key file... ");
908+
std::io::Write::flush(&mut std::io::stdout())?;
909+
match setup::validate_vertex_credentials_path(&path, &paths.base) {
910+
Ok(()) => {
911+
println!("OK");
912+
Some(path)
913+
}
914+
Err(e) => {
915+
println!("FAILED");
916+
bail!("Invalid credentials file: {}", e);
917+
}
918+
}
919+
}
920+
} else {
921+
None
922+
};
923+
924+
print!("Validating Vertex config... ");
925+
std::io::Write::flush(&mut std::io::stdout())?;
882926

883-
Password::with_theme(&ColorfulTheme::default())
884-
.with_prompt("Paste your API key")
885-
.interact()?
927+
match setup::validate_vertex_config(
928+
project_id.trim(),
929+
Some(region.trim()),
930+
vertex_credentials_path.as_deref(),
931+
&paths.base,
932+
)
933+
.await
934+
{
935+
Ok(()) => println!("OK"),
936+
Err(e) => {
937+
println!("FAILED");
938+
bail!("Vertex AI setup failed: {}", e);
939+
}
886940
}
887-
_ => unreachable!(),
888-
};
889941

890-
// Trim whitespace and normalize
891-
let credential = credential.trim().to_string();
942+
config.claude.api_key = None;
943+
config.claude.use_vertex = true;
944+
config.claude.vertex_project_id = Some(project_id.trim().to_string());
945+
config.claude.vertex_region = if region.trim().is_empty() {
946+
None
947+
} else {
948+
Some(region.trim().to_string())
949+
};
950+
config.claude.vertex_credentials_path = vertex_credentials_path;
951+
} else {
952+
// Anthropic setup
953+
println!();
954+
println!("Cica uses Claude Code, which can be billed through your Claude");
955+
println!("subscription or based on API usage through your Console account.");
956+
println!();
957+
958+
let auth_choices = vec![
959+
"Claude subscription Pro, Max, Team, or Enterprise",
960+
"Anthropic Console API usage billing",
961+
];
892962

893-
print!("Validating... ");
894-
std::io::Write::flush(&mut std::io::stdout())?;
963+
let auth_selection = Select::with_theme(&ColorfulTheme::default())
964+
.with_prompt("Select login method")
965+
.items(&auth_choices)
966+
.default(0)
967+
.interact()?;
895968

896-
match setup::validate_credential(&credential).await {
897-
Ok(()) => println!("OK"),
898-
Err(e) => {
899-
println!("FAILED");
900-
bail!("Authentication failed: {}", e);
969+
let credential = match auth_selection {
970+
0 => {
971+
println!();
972+
println!("Run this command in any terminal:");
973+
println!();
974+
println!(" claude setup-token");
975+
println!();
976+
println!("Note: The token may display across two lines, but it's one");
977+
println!("continuous string. Copy and paste it as a single line.");
978+
println!();
979+
980+
Password::with_theme(&ColorfulTheme::default())
981+
.with_prompt("Paste the setup token")
982+
.interact()?
983+
}
984+
1 => {
985+
println!();
986+
println!("1. Go to https://console.anthropic.com/settings/keys");
987+
println!("2. Create a new API key");
988+
println!();
989+
990+
Password::with_theme(&ColorfulTheme::default())
991+
.with_prompt("Paste your API key")
992+
.interact()?
993+
}
994+
_ => unreachable!(),
995+
};
996+
997+
let credential = credential.trim().to_string();
998+
999+
print!("Validating... ");
1000+
std::io::Write::flush(&mut std::io::stdout())?;
1001+
1002+
match setup::validate_credential(&credential).await {
1003+
Ok(()) => println!("OK"),
1004+
Err(e) => {
1005+
println!("FAILED");
1006+
bail!("Authentication failed: {}", e);
1007+
}
9011008
}
902-
}
9031009

904-
// Save config
905-
let mut config = existing_config.unwrap_or_default();
906-
let was_using_cursor = config.backend == AiBackend::Cursor && config.is_cursor_configured();
907-
config.claude.api_key = Some(credential);
1010+
config.claude.api_key = Some(credential);
1011+
config.claude.use_vertex = false;
1012+
config.claude.vertex_project_id = None;
1013+
config.claude.vertex_region = None;
1014+
config.claude.vertex_credentials_path = None;
1015+
}
9081016

9091017
// Ask whether to switch if another backend was active
9101018
if was_using_cursor {

src/config.rs

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -260,8 +260,18 @@ impl Config {
260260
/// Claude configuration
261261
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
262262
pub struct ClaudeConfig {
263-
/// Anthropic API key or OAuth token
263+
/// Anthropic API key or OAuth token (used when not using Vertex AI)
264264
pub api_key: Option<String>,
265+
/// Use Google Vertex AI instead of Anthropic API
266+
#[serde(default)]
267+
pub use_vertex: bool,
268+
/// GCP project ID for Vertex AI (required when use_vertex is true)
269+
pub vertex_project_id: Option<String>,
270+
/// GCP region for Vertex AI (e.g. "europe-west1", "us-east5"). Defaults to "europe-west1" if unset.
271+
pub vertex_region: Option<String>,
272+
/// Path to GCP service account JSON key file (long-lived auth; recommended for servers).
273+
/// When set, GOOGLE_APPLICATION_CREDENTIALS is set for Claude so gcloud login is not needed.
274+
pub vertex_credentials_path: Option<String>,
265275
}
266276

267277
/// Cursor CLI configuration
@@ -324,9 +334,16 @@ impl Config {
324334
channels
325335
}
326336

327-
/// Check if Claude is configured
337+
/// Check if Claude is configured (Anthropic API key or Vertex AI)
328338
pub fn is_claude_configured(&self) -> bool {
329-
self.claude.api_key.is_some()
339+
if self.claude.use_vertex {
340+
self.claude
341+
.vertex_project_id
342+
.as_ref()
343+
.is_some_and(|s| !s.is_empty())
344+
} else {
345+
self.claude.api_key.is_some()
346+
}
330347
}
331348

332349
/// Check if Cursor is configured

0 commit comments

Comments
 (0)