feat: integrate Gemini CLI OAuth with Cloud Code API

- Add gemini_oauth.rs: full OAuth flow with PKCE, token refresh,
  and Cloud Code project discovery (loadCodeAssist + onboardUser)
- Route preview/gemini-3 models through cloudcode-pa.googleapis.com
  with proper project ID injection in request payload
- Trigger OAuth login during onboarding wizard (not first chat message)
- Support manual redirect URL paste as fallback (tokio::select race)
- Parse 429 rate-limit errors with retry_after from Google response
- Add static model list: gemini-1.5/2.0/2.5/3.0/3.1 variants
- Add GeminiOauthConfig with default credentials path (~/.gemini/)
This commit is contained in:
Artem
2026-03-02 23:50:45 +03:00
parent 906d618681
commit 727283afe3
5 changed files with 1029 additions and 3 deletions
+44 -1
View File
@@ -26,6 +26,8 @@ pub enum LlmBackend {
OpenAiCompatible,
/// Tinfoil private inference
Tinfoil,
/// Official Gemini OAuth integrated provider
GeminiOauth,
}
impl std::str::FromStr for LlmBackend {
@@ -39,8 +41,9 @@ impl std::str::FromStr for LlmBackend {
"ollama" => Ok(Self::Ollama),
"openai_compatible" | "openai-compatible" | "compatible" => Ok(Self::OpenAiCompatible),
"tinfoil" => Ok(Self::Tinfoil),
"gemini_oauth" | "gemini-oauth" => Ok(Self::GeminiOauth),
_ => Err(format!(
"invalid LLM backend '{}', expected one of: nearai, openai, anthropic, ollama, openai_compatible, tinfoil",
"invalid LLM backend '{}', expected one of: nearai, openai, anthropic, ollama, openai_compatible, tinfoil, gemini_oauth",
s
)),
}
@@ -56,6 +59,7 @@ impl std::fmt::Display for LlmBackend {
Self::Ollama => write!(f, "ollama"),
Self::OpenAiCompatible => write!(f, "openai_compatible"),
Self::Tinfoil => write!(f, "tinfoil"),
Self::GeminiOauth => write!(f, "gemini_oauth"),
}
}
}
@@ -73,6 +77,7 @@ impl LlmBackend {
Self::Ollama => "OLLAMA_MODEL",
Self::OpenAiCompatible => "LLM_MODEL",
Self::Tinfoil => "TINFOIL_MODEL",
Self::GeminiOauth => "GEMINI_MODEL",
}
}
}
@@ -140,6 +145,24 @@ pub struct LlmConfig {
pub openai_compatible: Option<OpenAiCompatibleConfig>,
/// Tinfoil config (populated when backend=tinfoil)
pub tinfoil: Option<TinfoilConfig>,
/// Gemini OAuth config (populated when backend=gemini_oauth)
pub gemini_oauth: Option<GeminiOauthConfig>,
}
/// Configuration for Gemini OAuth integration.
#[derive(Debug, Clone)]
pub struct GeminiOauthConfig {
pub model: String,
pub credentials_path: PathBuf,
}
impl GeminiOauthConfig {
pub fn default_credentials_path() -> PathBuf {
dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join(".gemini")
.join("oauth_creds.json")
}
}
/// NEAR AI configuration.
@@ -350,6 +373,25 @@ impl LlmConfig {
None
};
let gemini_oauth = if backend == LlmBackend::GeminiOauth {
let model = Self::resolve_model("GEMINI_MODEL", settings, "gemini-2.5-flash")?;
let credentials_path = optional_env("GEMINI_CREDENTIALS_PATH")?
.map(PathBuf::from)
.unwrap_or_else(|| {
crate::bootstrap::ironclaw_base_dir()
.parent() // ~/.ironclaw -> ~/
.expect("ironclaw_base_dir has no parent")
.join(".gemini")
.join("oauth_creds.json")
});
Some(GeminiOauthConfig {
model,
credentials_path,
})
} else {
None
};
Ok(Self {
backend,
nearai,
@@ -358,6 +400,7 @@ impl LlmConfig {
ollama,
openai_compatible,
tinfoil,
gemini_oauth,
})
}
}
+1 -1
View File
@@ -37,7 +37,7 @@ pub use self::embeddings::EmbeddingsConfig;
pub use self::heartbeat::HeartbeatConfig;
pub use self::hygiene::HygieneConfig;
pub use self::llm::{
AnthropicDirectConfig, LlmBackend, LlmConfig, NearAiConfig, OllamaConfig,
AnthropicDirectConfig, GeminiOauthConfig, LlmBackend, LlmConfig, NearAiConfig, OllamaConfig,
OpenAiCompatibleConfig, OpenAiDirectConfig, TinfoilConfig,
};
pub use self::routines::RoutineConfig;