diff --git a/CHANGELOG.md b/CHANGELOG.md index 17ffab3..c3f5086 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## v2.3.2 (2026-05-20) + +- **Added Google Gemini provider with OAuth support** + - Two presets: "Google Gemini (API Key)" and "Google Gemini (OAuth)" + - OAuth Login button in endpoint editor — full Google OAuth2 flow + - Starts local HTTP server (port 8085), opens browser for Google consent + - Captures auth code, exchanges for access + refresh tokens + - Stores tokens in `~/.cache/codex-proxy/google-oauth-token.json` + - Auto-refreshes access tokens when expired (no manual re-login) + - Uses Gemini's OpenAI-compatible endpoint: `generativelanguage.googleapis.com/v1beta/openai` + - Models: gemini-2.5-flash, gemini-2.5-pro, gemini-2.0-flash, gemini-2.0-flash-lite, and more + - Setup instructions shown if `client_secret.json` not found + ## v2.3.0 (2026-05-20) - **Adaptive Crof self-healing system** diff --git a/codex-launcher_2.3.0_all.deb b/codex-launcher_2.3.0_all.deb deleted file mode 100644 index f633419..0000000 Binary files a/codex-launcher_2.3.0_all.deb and /dev/null differ diff --git a/codex-launcher_2.3.2_all.deb b/codex-launcher_2.3.2_all.deb new file mode 100644 index 0000000..2385cf9 Binary files /dev/null and b/codex-launcher_2.3.2_all.deb differ diff --git a/src/codex-launcher-gui b/src/codex-launcher-gui index a11a5f1..18190ef 100755 --- a/src/codex-launcher-gui +++ b/src/codex-launcher-gui @@ -24,6 +24,14 @@ model_catalog_json = "" """ CHANGELOG = [ + ("2.3.2", "2026-05-20", [ + "Added Google Gemini provider with OAuth support", + "Two presets: 'Google Gemini (API Key)' and 'Google Gemini (OAuth)'", + "OAuth Login button in endpoint editor — full Google OAuth2 flow with auto-refresh", + "Auto-refreshes OAuth access tokens when expired (no manual re-login needed)", + "Supports gemini-2.5-flash, gemini-2.5-pro, gemini-2.0-flash, and more", + "Uses Gemini's OpenAI-compatible endpoint — works with existing proxy", + ]), ("2.3.0", "2026-05-20", [ "Adaptive Crof self-healing system — auto-adjusts to Crof model limits", "Tracks per-model success/failure history, learns item count limits dynamically", @@ -174,6 +182,25 @@ PROVIDER_PRESETS = { "base_url": "https://openrouter.ai/api/v1", "models": [], }, + "Google Gemini (API Key)": { + "backend_type": "openai-compat", + "base_url": "https://generativelanguage.googleapis.com/v1beta/openai", + "models": [ + "gemini-2.5-flash", "gemini-2.5-pro", + "gemini-2.0-flash", "gemini-2.0-flash-lite", + "gemini-2.5-flash-preview-native-audio-dialog", + ], + }, + "Google Gemini (OAuth)": { + "backend_type": "openai-compat", + "base_url": "https://generativelanguage.googleapis.com/v1beta/openai", + "oauth_provider": "google", + "models": [ + "gemini-2.5-flash", "gemini-2.5-pro", + "gemini-2.0-flash", "gemini-2.0-flash-lite", + "gemini-2.5-flash-preview-native-audio-dialog", + ], + }, } def safe_name(name): @@ -455,6 +482,7 @@ def _start_proxy_for(endpoint, logfn): "target_url": normalize_base_url(endpoint["base_url"]), "api_key": endpoint["api_key"], "cc_version": endpoint.get("cc_version", ""), + "oauth_provider": endpoint.get("oauth_provider", ""), "reasoning_enabled": endpoint.get("reasoning_enabled", True), "reasoning_effort": endpoint.get("reasoning_effort", "medium"), "models": [{"id": m, "object": "model", "created": 1700000000, "owned_by": endpoint["name"]} @@ -555,7 +583,7 @@ class LauncherWin(Gtk.Window): # header row hdr = Gtk.Box(spacing=8) vbox.pack_start(hdr, False, False, 0) - lbl = Gtk.Label(label="Codex Launcher v2.3.0") + lbl = Gtk.Label(label="Codex Launcher v2.3.2") lbl.set_use_markup(True) hdr.pack_start(lbl, False, False, 0) changelog_btn = Gtk.Button(label="Changelog") @@ -1451,7 +1479,13 @@ class EditEndpointDialog(Gtk.Dialog): self._entry_key = Gtk.Entry(text=self._data.get("api_key", "")) self._entry_key.set_visibility(False) - add_row(4, "API Key:", self._entry_key) + key_box = Gtk.Box(spacing=6) + key_box.pack_start(self._entry_key, True, True, 0) + self._oauth_btn = Gtk.Button(label="OAuth Login") + self._oauth_btn.connect("clicked", lambda b: self._do_oauth_login()) + key_box.pack_start(self._oauth_btn, False, False, 0) + add_row(4, "API Key:", key_box) + self._oauth_btn.set_visible(False) self._entry_cc_ver = Gtk.Entry(text=self._data.get("cc_version", "")) self._entry_cc_ver.set_placeholder_text("e.g. 0.26.8 (Command Code only)") @@ -1590,6 +1624,12 @@ class EditEndpointDialog(Gtk.Dialog): def _apply_selected_preset(self, initial=False): preset_name = self._combo_preset.get_active_text() or "Custom" preset = PROVIDER_PRESETS.get(preset_name, PROVIDER_PRESETS["Custom"]) + is_oauth = bool(preset.get("oauth_provider")) + self._oauth_btn.set_visible(is_oauth) + if is_oauth: + self._entry_key.set_placeholder_text("Auto-filled by OAuth") + else: + self._entry_key.set_placeholder_text("") if not initial or self._existing_name is None: self._combo_type.set_active_id(preset.get("backend_type", "openai-compat")) self._entry_url.set_text(preset.get("base_url", "")) @@ -1605,7 +1645,102 @@ class EditEndpointDialog(Gtk.Dialog): if initial and self._data.get("models"): self._refresh_default_combo(self._data.get("default_model", "")) - def _on_reasoning_toggled(self, *_): + def _do_oauth_login(self): + preset_name = self._combo_preset.get_active_text() or "Custom" + preset = PROVIDER_PRESETS.get(preset_name, {}) + provider = preset.get("oauth_provider", "") + if provider == "google": + self._google_oauth_flow() + + def _google_oauth_flow(self): + token_path = os.path.expanduser("~/.cache/codex-proxy/google-oauth-token.json") + client_secret_path = os.path.expanduser("~/.cache/codex-proxy/client_secret.json") + + if not os.path.exists(client_secret_path): + d = Gtk.MessageDialog(self, 0, Gtk.MessageType.INFO, Gtk.ButtonsType.OK, + "Google OAuth Setup\n\n" + "1. Go to https://console.cloud.google.com/\n" + "2. Create a project → Enable 'Generative Language API'\n" + "3. Go to APIs & Services → Credentials → Create OAuth 2.0 Client ID\n" + "4. Application type: Desktop app\n" + "5. Add redirect URI: http://localhost:8085\n" + "6. Download client_secret.json\n" + f"7. Save it to: {client_secret_path}\n\n" + "Then click OAuth Login again.") + d.run(); d.destroy() + return + + import http.server, threading, urllib.parse, json, urllib.request + + with open(client_secret_path) as f: + cs = json.load(f) + installed = cs.get("installed", cs.get("web", {})) + client_id = installed["client_id"] + client_secret = installed["client_secret"] + redirect_uri = "http://localhost:8085" + scope = "https://www.googleapis.com/auth/generative-language.retriever" + + auth_url = ( + f"https://accounts.google.com/o/oauth2/v2/auth?" + f"client_id={client_id}&redirect_uri={urllib.parse.quote(redirect_uri)}" + f"&response_type=code&scope={urllib.parse.quote(scope)}&access_type=offline&prompt=consent" + ) + + code_holder = [None] + class OAuthHandler(http.server.BaseHTTPRequestHandler): + def do_GET(self2): + qs = urllib.parse.urlparse(self2.path).query + params = urllib.parse.parse_qs(qs) + if "code" in params: + code_holder[0] = params["code"][0] + self2.send_response(200) + self2.send_header("Content-Type", "text/html") + self2.end_headers() + self2.wfile.write(b"