mirror of
https://github.com/aiclientproxy/proxycast.git
synced 2026-09-24 23:10:56 +08:00
feat: add Gemini CLI OAuth provider
- Add GeminiProvider with OAuth token refresh support - Add Gemini credential management commands - Update Providers page with tabs for Kiro/Gemini - Auto-detect credential file changes for both providers - Add Gemini models to /v1/models endpoint - OAuth credentials must be set via GEMINI_OAUTH_CLIENT_ID/SECRET env vars
This commit is contained in:
@@ -240,6 +240,149 @@ struct CheckResult {
|
||||
reloaded: bool,
|
||||
}
|
||||
|
||||
// ============ Gemini Provider Commands ============
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
struct GeminiCredentialStatus {
|
||||
loaded: bool,
|
||||
has_access_token: bool,
|
||||
has_refresh_token: bool,
|
||||
expiry_date: Option<i64>,
|
||||
is_valid: bool,
|
||||
creds_path: String,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn get_gemini_credentials(state: tauri::State<'_, AppState>) -> Result<GeminiCredentialStatus, String> {
|
||||
let s = state.read().await;
|
||||
let creds = &s.gemini_provider.credentials;
|
||||
let path = providers::gemini::GeminiProvider::default_creds_path();
|
||||
|
||||
Ok(GeminiCredentialStatus {
|
||||
loaded: creds.access_token.is_some() || creds.refresh_token.is_some(),
|
||||
has_access_token: creds.access_token.is_some(),
|
||||
has_refresh_token: creds.refresh_token.is_some(),
|
||||
expiry_date: creds.expiry_date,
|
||||
is_valid: s.gemini_provider.is_token_valid(),
|
||||
creds_path: path.to_string_lossy().to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn reload_gemini_credentials(state: tauri::State<'_, AppState>, logs: tauri::State<'_, LogState>) -> Result<String, String> {
|
||||
let mut s = state.write().await;
|
||||
logs.write().await.add("info", "[Gemini] 正在加载凭证...");
|
||||
s.gemini_provider.load_credentials().map_err(|e| e.to_string())?;
|
||||
logs.write().await.add("info", "[Gemini] 凭证加载成功");
|
||||
Ok("Gemini credentials reloaded".to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn refresh_gemini_token(state: tauri::State<'_, AppState>, logs: tauri::State<'_, LogState>) -> Result<String, String> {
|
||||
let mut s = state.write().await;
|
||||
logs.write().await.add("info", "[Gemini] 正在刷新 Token...");
|
||||
let result = s.gemini_provider.refresh_token().await.map_err(|e| e.to_string());
|
||||
match &result {
|
||||
Ok(_) => logs.write().await.add("info", "[Gemini] Token 刷新成功"),
|
||||
Err(e) => logs.write().await.add("error", &format!("[Gemini] Token 刷新失败: {}", e)),
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn get_gemini_env_variables(state: tauri::State<'_, AppState>) -> Result<Vec<EnvVariable>, String> {
|
||||
let s = state.read().await;
|
||||
let creds = &s.gemini_provider.credentials;
|
||||
let mut vars = Vec::new();
|
||||
|
||||
if let Some(token) = &creds.access_token {
|
||||
vars.push(EnvVariable {
|
||||
key: "GEMINI_ACCESS_TOKEN".to_string(),
|
||||
value: token.clone(),
|
||||
masked: mask_token(token),
|
||||
});
|
||||
}
|
||||
if let Some(token) = &creds.refresh_token {
|
||||
vars.push(EnvVariable {
|
||||
key: "GEMINI_REFRESH_TOKEN".to_string(),
|
||||
value: token.clone(),
|
||||
masked: mask_token(token),
|
||||
});
|
||||
}
|
||||
if let Some(expiry) = creds.expiry_date {
|
||||
let expiry_str = expiry.to_string();
|
||||
vars.push(EnvVariable {
|
||||
key: "GEMINI_EXPIRY_DATE".to_string(),
|
||||
value: expiry_str.clone(),
|
||||
masked: expiry_str,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(vars)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn get_gemini_token_file_hash() -> Result<String, String> {
|
||||
let path = providers::gemini::GeminiProvider::default_creds_path();
|
||||
if !path.exists() {
|
||||
return Ok("".to_string());
|
||||
}
|
||||
|
||||
let content = std::fs::read(&path).map_err(|e| e.to_string())?;
|
||||
let hash = format!("{:x}", md5::compute(&content));
|
||||
Ok(hash)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn check_and_reload_gemini_credentials(
|
||||
state: tauri::State<'_, AppState>,
|
||||
logs: tauri::State<'_, LogState>,
|
||||
last_hash: String,
|
||||
) -> Result<CheckResult, String> {
|
||||
let path = providers::gemini::GeminiProvider::default_creds_path();
|
||||
|
||||
if !path.exists() {
|
||||
return Ok(CheckResult {
|
||||
changed: false,
|
||||
new_hash: "".to_string(),
|
||||
reloaded: false,
|
||||
});
|
||||
}
|
||||
|
||||
let content = std::fs::read(&path).map_err(|e| e.to_string())?;
|
||||
let new_hash = format!("{:x}", md5::compute(&content));
|
||||
|
||||
if !last_hash.is_empty() && new_hash != last_hash {
|
||||
logs.write().await.add("info", "[Gemini][自动检测] 凭证文件已变化,正在重新加载...");
|
||||
|
||||
let mut s = state.write().await;
|
||||
match s.gemini_provider.load_credentials() {
|
||||
Ok(_) => {
|
||||
logs.write().await.add("info", "[Gemini][自动检测] 凭证重新加载成功");
|
||||
Ok(CheckResult {
|
||||
changed: true,
|
||||
new_hash,
|
||||
reloaded: true,
|
||||
})
|
||||
}
|
||||
Err(e) => {
|
||||
logs.write().await.add("error", &format!("[Gemini][自动检测] 凭证重新加载失败: {}", e));
|
||||
Ok(CheckResult {
|
||||
changed: true,
|
||||
new_hash,
|
||||
reloaded: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Ok(CheckResult {
|
||||
changed: false,
|
||||
new_hash,
|
||||
reloaded: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn get_logs(logs: tauri::State<'_, LogState>) -> Result<Vec<logger::LogEntry>, String> {
|
||||
Ok(logs.read().await.get_logs())
|
||||
@@ -324,12 +467,21 @@ pub fn run() {
|
||||
get_server_status,
|
||||
get_config,
|
||||
save_config,
|
||||
// Kiro commands
|
||||
refresh_kiro_token,
|
||||
reload_credentials,
|
||||
get_kiro_credentials,
|
||||
get_env_variables,
|
||||
get_token_file_hash,
|
||||
check_and_reload_credentials,
|
||||
// Gemini commands
|
||||
get_gemini_credentials,
|
||||
reload_gemini_credentials,
|
||||
refresh_gemini_token,
|
||||
get_gemini_env_variables,
|
||||
get_gemini_token_file_hash,
|
||||
check_and_reload_gemini_credentials,
|
||||
// Common
|
||||
get_logs,
|
||||
clear_logs,
|
||||
test_api,
|
||||
|
||||
@@ -0,0 +1,314 @@
|
||||
//! Gemini CLI OAuth Provider
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::error::Error;
|
||||
use std::path::PathBuf;
|
||||
use reqwest::Client;
|
||||
|
||||
// Constants
|
||||
const CODE_ASSIST_ENDPOINT: &str = "https://cloudcode-pa.googleapis.com";
|
||||
const CODE_ASSIST_API_VERSION: &str = "v1internal";
|
||||
const CREDENTIALS_DIR: &str = ".gemini";
|
||||
const CREDENTIALS_FILE: &str = "oauth_creds.json";
|
||||
|
||||
// OAuth credentials - loaded from environment variables
|
||||
// Set GEMINI_OAUTH_CLIENT_ID and GEMINI_OAUTH_CLIENT_SECRET
|
||||
// These are the same as Gemini CLI uses (public OAuth app credentials)
|
||||
fn get_oauth_client_id() -> Option<String> {
|
||||
std::env::var("GEMINI_OAUTH_CLIENT_ID").ok()
|
||||
}
|
||||
|
||||
fn get_oauth_client_secret() -> Option<String> {
|
||||
std::env::var("GEMINI_OAUTH_CLIENT_SECRET").ok()
|
||||
}
|
||||
|
||||
pub const GEMINI_MODELS: &[&str] = &[
|
||||
"gemini-2.5-flash",
|
||||
"gemini-2.5-flash-lite",
|
||||
"gemini-2.5-pro",
|
||||
"gemini-2.5-pro-preview-06-05",
|
||||
"gemini-2.5-flash-preview-09-2025",
|
||||
"gemini-3-pro-preview",
|
||||
];
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct GeminiCredentials {
|
||||
pub access_token: Option<String>,
|
||||
pub refresh_token: Option<String>,
|
||||
pub token_type: Option<String>,
|
||||
pub expiry_date: Option<i64>,
|
||||
pub scope: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for GeminiCredentials {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
access_token: None,
|
||||
refresh_token: None,
|
||||
token_type: Some("Bearer".to_string()),
|
||||
expiry_date: None,
|
||||
scope: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct GeminiContent {
|
||||
pub role: String,
|
||||
pub parts: Vec<GeminiPart>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct GeminiPart {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub text: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct GeminiRequest {
|
||||
pub model: String,
|
||||
pub project: String,
|
||||
pub request: GeminiRequestBody,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct GeminiRequestBody {
|
||||
pub contents: Vec<GeminiContent>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub system_instruction: Option<GeminiContent>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub generation_config: Option<GeminiGenerationConfig>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct GeminiGenerationConfig {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub temperature: Option<f32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub max_output_tokens: Option<i32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub top_p: Option<f32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub top_k: Option<i32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct GeminiResponse {
|
||||
pub candidates: Option<Vec<GeminiCandidate>>,
|
||||
#[serde(rename = "usageMetadata")]
|
||||
pub usage_metadata: Option<GeminiUsageMetadata>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct GeminiCandidate {
|
||||
pub content: Option<GeminiContent>,
|
||||
pub finish_reason: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct GeminiUsageMetadata {
|
||||
pub prompt_token_count: Option<i32>,
|
||||
pub candidates_token_count: Option<i32>,
|
||||
pub total_token_count: Option<i32>,
|
||||
}
|
||||
|
||||
pub struct GeminiProvider {
|
||||
pub credentials: GeminiCredentials,
|
||||
pub project_id: Option<String>,
|
||||
pub client: Client,
|
||||
}
|
||||
|
||||
impl GeminiProvider {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
credentials: GeminiCredentials::default(),
|
||||
project_id: None,
|
||||
client: Client::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn default_creds_path() -> PathBuf {
|
||||
dirs::home_dir()
|
||||
.unwrap_or_else(|| PathBuf::from("."))
|
||||
.join(CREDENTIALS_DIR)
|
||||
.join(CREDENTIALS_FILE)
|
||||
}
|
||||
|
||||
pub fn load_credentials(&mut self) -> Result<(), Box<dyn Error + Send + Sync>> {
|
||||
let path = Self::default_creds_path();
|
||||
|
||||
if path.exists() {
|
||||
let content = std::fs::read_to_string(&path)?;
|
||||
let creds: GeminiCredentials = serde_json::from_str(&content)?;
|
||||
self.credentials = creds;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn save_credentials(&self) -> Result<(), Box<dyn Error + Send + Sync>> {
|
||||
let path = Self::default_creds_path();
|
||||
if let Some(parent) = path.parent() {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
}
|
||||
let content = serde_json::to_string_pretty(&self.credentials)?;
|
||||
std::fs::write(&path, content)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn is_token_valid(&self) -> bool {
|
||||
if self.credentials.access_token.is_none() {
|
||||
return false;
|
||||
}
|
||||
if let Some(expiry) = self.credentials.expiry_date {
|
||||
let now = chrono::Utc::now().timestamp_millis();
|
||||
// Token valid if more than 5 minutes until expiry
|
||||
return expiry > now + 300_000;
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
pub async fn refresh_token(&mut self) -> Result<String, Box<dyn Error + Send + Sync>> {
|
||||
let refresh_token = self.credentials.refresh_token.as_ref()
|
||||
.ok_or("No refresh token available")?;
|
||||
|
||||
let client_id = get_oauth_client_id()
|
||||
.ok_or("GEMINI_OAUTH_CLIENT_ID not set")?;
|
||||
let client_secret = get_oauth_client_secret()
|
||||
.ok_or("GEMINI_OAUTH_CLIENT_SECRET not set")?;
|
||||
|
||||
let params = [
|
||||
("client_id", client_id.as_str()),
|
||||
("client_secret", client_secret.as_str()),
|
||||
("refresh_token", refresh_token.as_str()),
|
||||
("grant_type", "refresh_token"),
|
||||
];
|
||||
|
||||
let resp = self.client
|
||||
.post("https://oauth2.googleapis.com/token")
|
||||
.form(¶ms)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if !resp.status().is_success() {
|
||||
let status = resp.status();
|
||||
let body = resp.text().await.unwrap_or_default();
|
||||
return Err(format!("Token refresh failed: {} - {}", status, body).into());
|
||||
}
|
||||
|
||||
let data: serde_json::Value = resp.json().await?;
|
||||
|
||||
let new_token = data["access_token"]
|
||||
.as_str()
|
||||
.ok_or("No access token in response")?;
|
||||
|
||||
self.credentials.access_token = Some(new_token.to_string());
|
||||
|
||||
if let Some(expires_in) = data["expires_in"].as_i64() {
|
||||
self.credentials.expiry_date = Some(
|
||||
chrono::Utc::now().timestamp_millis() + expires_in * 1000
|
||||
);
|
||||
}
|
||||
|
||||
// Save refreshed credentials
|
||||
self.save_credentials()?;
|
||||
|
||||
Ok(new_token.to_string())
|
||||
}
|
||||
|
||||
pub fn get_api_url(&self, action: &str) -> String {
|
||||
format!("{}/{}:{}", CODE_ASSIST_ENDPOINT, CODE_ASSIST_API_VERSION, action)
|
||||
}
|
||||
|
||||
pub async fn call_api(
|
||||
&self,
|
||||
action: &str,
|
||||
body: &serde_json::Value,
|
||||
) -> Result<serde_json::Value, Box<dyn Error + Send + Sync>> {
|
||||
let token = self.credentials.access_token.as_ref()
|
||||
.ok_or("No access token")?;
|
||||
|
||||
let url = self.get_api_url(action);
|
||||
|
||||
let resp = self.client
|
||||
.post(&url)
|
||||
.header("Authorization", format!("Bearer {}", token))
|
||||
.header("Content-Type", "application/json")
|
||||
.json(body)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if !resp.status().is_success() {
|
||||
let status = resp.status();
|
||||
let body = resp.text().await.unwrap_or_default();
|
||||
return Err(format!("API call failed: {} - {}", status, body).into());
|
||||
}
|
||||
|
||||
let data: serde_json::Value = resp.json().await?;
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
pub async fn discover_project(&mut self) -> Result<String, Box<dyn Error + Send + Sync>> {
|
||||
if let Some(ref project_id) = self.project_id {
|
||||
return Ok(project_id.clone());
|
||||
}
|
||||
|
||||
let body = serde_json::json!({
|
||||
"cloudaicompanionProject": "",
|
||||
"metadata": {
|
||||
"ideType": "IDE_UNSPECIFIED",
|
||||
"platform": "PLATFORM_UNSPECIFIED",
|
||||
"pluginType": "GEMINI",
|
||||
"duetProject": ""
|
||||
}
|
||||
});
|
||||
|
||||
let resp = self.call_api("loadCodeAssist", &body).await?;
|
||||
|
||||
if let Some(project) = resp["cloudaicompanionProject"].as_str() {
|
||||
if !project.is_empty() {
|
||||
self.project_id = Some(project.to_string());
|
||||
return Ok(project.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
// Need to onboard
|
||||
let onboard_body = serde_json::json!({
|
||||
"tierId": "free-tier",
|
||||
"cloudaicompanionProject": "",
|
||||
"metadata": {
|
||||
"ideType": "IDE_UNSPECIFIED",
|
||||
"platform": "PLATFORM_UNSPECIFIED",
|
||||
"pluginType": "GEMINI",
|
||||
"duetProject": ""
|
||||
}
|
||||
});
|
||||
|
||||
let mut lro_resp = self.call_api("onboardUser", &onboard_body).await?;
|
||||
|
||||
// Poll until done
|
||||
for _ in 0..30 {
|
||||
if lro_resp["done"].as_bool().unwrap_or(false) {
|
||||
break;
|
||||
}
|
||||
tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
|
||||
lro_resp = self.call_api("onboardUser", &onboard_body).await?;
|
||||
}
|
||||
|
||||
let project_id = lro_resp["response"]["cloudaicompanionProject"]["id"]
|
||||
.as_str()
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
|
||||
if project_id.is_empty() {
|
||||
return Err("Failed to discover project ID".into());
|
||||
}
|
||||
|
||||
self.project_id = Some(project_id.clone());
|
||||
Ok(project_id)
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
pub mod kiro;
|
||||
pub mod gemini;
|
||||
|
||||
pub use kiro::KiroProvider;
|
||||
pub use gemini::GeminiProvider;
|
||||
|
||||
+14
-1
@@ -4,6 +4,7 @@ use crate::models::openai::*;
|
||||
use crate::models::anthropic::*;
|
||||
use crate::converter::anthropic_to_openai::convert_anthropic_to_openai;
|
||||
use crate::providers::kiro::KiroProvider;
|
||||
use crate::providers::gemini::GeminiProvider;
|
||||
use crate::logger::LogStore;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
@@ -31,6 +32,7 @@ pub struct ServerState {
|
||||
pub requests: u64,
|
||||
pub start_time: Option<std::time::Instant>,
|
||||
pub kiro_provider: KiroProvider,
|
||||
pub gemini_provider: GeminiProvider,
|
||||
shutdown_tx: Option<oneshot::Sender<()>>,
|
||||
}
|
||||
|
||||
@@ -39,12 +41,16 @@ impl ServerState {
|
||||
let mut kiro = KiroProvider::new();
|
||||
let _ = kiro.load_credentials();
|
||||
|
||||
let mut gemini = GeminiProvider::new();
|
||||
let _ = gemini.load_credentials();
|
||||
|
||||
Self {
|
||||
config,
|
||||
running: false,
|
||||
requests: 0,
|
||||
start_time: None,
|
||||
kiro_provider: kiro,
|
||||
gemini_provider: gemini,
|
||||
shutdown_tx: None,
|
||||
}
|
||||
}
|
||||
@@ -158,10 +164,17 @@ async fn models() -> impl IntoResponse {
|
||||
Json(serde_json::json!({
|
||||
"object": "list",
|
||||
"data": [
|
||||
// Kiro/Claude models
|
||||
{"id": "claude-sonnet-4-5", "object": "model", "owned_by": "anthropic"},
|
||||
{"id": "claude-sonnet-4-5-20250929", "object": "model", "owned_by": "anthropic"},
|
||||
{"id": "claude-3-7-sonnet-20250219", "object": "model", "owned_by": "anthropic"},
|
||||
{"id": "claude-3-5-sonnet-latest", "object": "model", "owned_by": "anthropic"}
|
||||
{"id": "claude-3-5-sonnet-latest", "object": "model", "owned_by": "anthropic"},
|
||||
// Gemini models
|
||||
{"id": "gemini-2.5-flash", "object": "model", "owned_by": "google"},
|
||||
{"id": "gemini-2.5-flash-lite", "object": "model", "owned_by": "google"},
|
||||
{"id": "gemini-2.5-pro", "object": "model", "owned_by": "google"},
|
||||
{"id": "gemini-2.5-pro-preview-06-05", "object": "model", "owned_by": "google"},
|
||||
{"id": "gemini-3-pro-preview", "object": "model", "owned_by": "google"}
|
||||
]
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -145,3 +145,39 @@ export interface CheckResult {
|
||||
export async function checkAndReloadCredentials(lastHash: string): Promise<CheckResult> {
|
||||
return invoke("check_and_reload_credentials", { last_hash: lastHash });
|
||||
}
|
||||
|
||||
|
||||
// ============ Gemini Provider ============
|
||||
|
||||
export interface GeminiCredentialStatus {
|
||||
loaded: boolean;
|
||||
has_access_token: boolean;
|
||||
has_refresh_token: boolean;
|
||||
expiry_date: number | null;
|
||||
is_valid: boolean;
|
||||
creds_path: string;
|
||||
}
|
||||
|
||||
export async function getGeminiCredentials(): Promise<GeminiCredentialStatus> {
|
||||
return invoke("get_gemini_credentials");
|
||||
}
|
||||
|
||||
export async function reloadGeminiCredentials(): Promise<string> {
|
||||
return invoke("reload_gemini_credentials");
|
||||
}
|
||||
|
||||
export async function refreshGeminiToken(): Promise<string> {
|
||||
return invoke("refresh_gemini_token");
|
||||
}
|
||||
|
||||
export async function getGeminiEnvVariables(): Promise<EnvVariable[]> {
|
||||
return invoke("get_gemini_env_variables");
|
||||
}
|
||||
|
||||
export async function getGeminiTokenFileHash(): Promise<string> {
|
||||
return invoke("get_gemini_token_file_hash");
|
||||
}
|
||||
|
||||
export async function checkAndReloadGeminiCredentials(lastHash: string): Promise<CheckResult> {
|
||||
return invoke("check_and_reload_gemini_credentials", { last_hash: lastHash });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user