v2.3.2: add Google Gemini provider with OAuth support

- Two presets: API Key and OAuth modes
- OAuth Login button: full Google OAuth2 flow with auto-refresh
- Auto-refreshes expired access tokens using refresh_token
- Gemini OpenAI-compatible endpoint works with existing proxy
- Models: gemini-2.5-flash, gemini-2.5-pro, gemini-2.0-flash, etc.
This commit is contained in:
Roman
2026-05-20 14:45:43 +04:00
Unverified
parent 27b22f4fd8
commit ea60d74527
5 changed files with 193 additions and 4 deletions

View File

@@ -79,11 +79,47 @@ PORT = CONFIG["port"]
BACKEND = CONFIG["backend_type"]
TARGET_URL = CONFIG["target_url"].rstrip("/")
API_KEY = CONFIG["api_key"]
OAUTH_PROVIDER = CONFIG.get("oauth_provider", "")
MODELS = CONFIG["models"]
CC_VERSION = CONFIG.get("cc_version", "")
REASONING_ENABLED = CONFIG.get("reasoning_enabled", True)
REASONING_EFFORT = CONFIG.get("reasoning_effort", "medium")
def _refresh_oauth_token():
if OAUTH_PROVIDER != "google":
return API_KEY
token_path = os.path.join(os.path.expanduser("~"), ".cache", "codex-proxy", "google-oauth-token.json")
if not os.path.exists(token_path):
return API_KEY
try:
with open(token_path) as f:
tokens = json.load(f)
if tokens.get("expires_at", 0) > time.time() + 60:
return tokens.get("access_token", API_KEY)
client_id = tokens.get("client_id", "")
client_secret = tokens.get("client_secret", "")
refresh_token = tokens.get("refresh_token", "")
if not all([client_id, client_secret, refresh_token]):
return tokens.get("access_token", API_KEY)
print("[oauth] refreshing Google access token...", file=sys.stderr)
data = urllib.parse.urlencode({
"client_id": client_id, "client_secret": client_secret,
"refresh_token": refresh_token, "grant_type": "refresh_token",
}).encode()
req = urllib.request.Request("https://oauth2.googleapis.com/token", data=data,
headers={"Content-Type": "application/x-www-form-urlencoded"})
resp = urllib.request.urlopen(req, timeout=30)
new_tokens = json.loads(resp.read())
tokens["access_token"] = new_tokens.get("access_token", tokens.get("access_token"))
tokens["expires_at"] = time.time() + new_tokens.get("expires_in", 3600)
with open(token_path, "w") as f:
json.dump(tokens, f, indent=2)
print("[oauth] token refreshed OK", file=sys.stderr)
return tokens["access_token"]
except Exception as e:
print(f"[oauth] refresh failed: {e}", file=sys.stderr)
return API_KEY
# ═══════════════════════════════════════════════════════════════════
# Shared helpers
# ═══════════════════════════════════════════════════════════════════
@@ -1000,9 +1036,10 @@ class Handler(http.server.BaseHTTPRequestHandler):
chat_body["reasoning_effort"] = REASONING_EFFORT
target = upstream_target(TARGET_URL, "/chat/completions")
effective_key = _refresh_oauth_token()
fwd = forwarded_headers(self.headers, {
"Content-Type": "application/json",
"Authorization": f"Bearer {API_KEY}",
"Authorization": f"Bearer {effective_key}",
}, browser_ua=True)
print(f"[translate-proxy] POST {target} model={model} stream={stream} items={len(input_data) if isinstance(input_data,list) else 1} ua={fwd.get('User-Agent','')[:50]}", file=sys.stderr)