48 Commits

26 changed files with 425 additions and 14395 deletions

4
.gitignore vendored
View File

@@ -11,7 +11,3 @@ config.toml
.DS_Store
DEBIAN/
usr/
oauth-secrets.json
secrets/
*.secret
.env

View File

@@ -1,229 +1,5 @@
# Changelog
## v3.10.10 (2026-05-25)
**Context Normalizer Fix — Compaction Summary Preservation**
### Bug Fixes
- Fixed normalizer stripping ALL context on resumed sessions after compaction
- Normalizer no longer auto-resets when compaction summary is present
- Compaction summaries ("Auto-compacted: N earlier turns") are always preserved
- Deduplicates consecutive identical `<goal_context>` messages (10→1)
- Emergency reset now preserves compaction summaries
- Previous behavior: after compaction reduced 1925→185 items, normalizer saw `n_tool_outputs == 0` and stripped to just `system + latest_user`, losing all context — model responded with "I don't have context"
### hashlib Fix (v3.10.9 hotfix)
- `_antigravity_normalize_context` crashed with `NameError: hashlib` on resumed sessions
- Replaced SHA256 duplicate detection with string comparison
## v3.10.9 (2026-05-25)
**Antigravity Overhaul — Context Normalizer, Claude Thinking Fix, Endpoint Lockdown**
### Antigravity Endpoint Lockdown
- Production-only: `cloudcode-pa.googleapis.com` by default
- Sandbox/staging blocked unless `ALLOW_ANTIGRAVITY_STAGING=1`
- 403 SERVICE_DISABLED falls through, 429 returns to client
### AntigravityContextNormalizer
- Bounded context — no more 136-item polluted requests for "hi"
- Simple message detector, auto-reset polluted context
- Duplicate removal, tool output budget, hard char limits
### Claude Thinking Fix (Antigravity-only)
- Fixed 400 error: `maxOutputTokens=64000` when thinking enabled
- Snake_case config, VALIDATED toolConfig, proper budgets
### z.ai / OpenRouter (cobra91 PR #4)
- Full OpenClaw attribution headers, OpenRouter caching
## v3.10.8 (2026-05-25)
**OAuth & Antigravity Endpoint Fixes**
### Re-OAuth Buttons Fixed
- Linux GUI: `load_oauth_secrets()` was undefined — buttons crashed silently on click
- Now loads OAuth secrets inline from `~/.config/codex-launcher/oauth-secrets.json`
- Both Linux and Windows Re-OAuth use PKCE + localhost callback (was deprecated OOB paste)
### Antigravity Staging/Sandbox Blocked by Default
- Proxy: production `cloudcode-pa.googleapis.com` tried FIRST, sandbox/daily/autopush as fallback only
- Proxy: 403 SERVICE_DISABLED now falls through to next endpoint instead of returning error immediately
- Project discovery: validates against production endpoint, not staging-cloudaicompanion.sandbox
- Antigravity preset `base_url` changed to production (was `daily-cloudcode-pa.sandbox.googleapis.com`)
- `[antigravity-endpoint]` log line shows which endpoints are being tried
### Other Fixes
- GLib.idle_add lambda returning truthy tuple fixed (caused repeated callbacks)
- Windows GUI project discovery also uses production endpoint
## v3.10.7 (2026-05-25)
**Prompt Enhancer — Fix Lost Context After Compaction**
### Prompt Enhancer (Per-Provider Toggle)
- **Offline mode**: Injects structured XML instructions before every user prompt to keep the model focused, decisive, and context-aware after compaction strips conversation history
- **AI-powered mode**: Optionally calls an external LLM (configurable model/URL/key) to rewrite vague prompts into clear, actionable instructions
- Prevents the "had to resend and reword" problem in long sessions where compaction summarizes hundreds of turns
- **Per-endpoint setting** — enable/disable for each provider independently
- Configurable in both Linux and Windows GUI: toggle switch, mode selector, enhancer model, URL, API key fields
### How It Works
- **Offline**: Prepends a `<prompt-enhancer>` block with rules like "never ask for clarification, infer from compacted context, execute decisively"
- **AI-powered**: Sends the user's prompt + compaction summary to a separate model (e.g. DeepSeek V4 Flash via Freebuff) which rewrites it for clarity, then prepends the offline instructions too
- Both modes run after compaction but before the request is sent upstream
## v3.10.6 (2026-05-25)
**Freebuff Integration + Codebuff OAuth Fix + Windows Consolidation**
### Freebuff (Free DeepSeek/Kimi)
- **Freebuff integration**: Free DeepSeek/Kimi models via codebuff.com API
- Fixed User-Agent to match official SDK: `ai-sdk/openai-compatible/1.0.25/codebuff`
- Fixed metadata fields: `freebuff_instance_id` + `client_id` (base36 random) + `cost_mode: "free"`
- Fixed session endpoint: POST empty `{}` body (not `{"model": model}`)
- GUI preset aliases: "Freebuff (Free DeepSeek/Kimi)", "FreeBuff", "Codebuff (Free DeepSeek/Kimi)" all map to same backend
### Codebuff Fix
- Fixed Codebuff OAuth: use `www.codebuff.com` (bare `codebuff.com` returns 307 redirect)
### OAuth Secrets & Credentials (All Providers)
- **OAuth Secrets dialog now shows ALL providers**: Google (Antigravity + Gemini CLI) AND Freebuff/Codebuff
- **Re-OAuth buttons** for each provider: instantly re-authenticate Google or GitHub/Codebuff
- Token status indicators (valid/missing) for each Google provider
- Shows logged-in email and auth status for Freebuff/Codebuff
- Editable auth token and fingerprint fields for Freebuff/Codebuff
### Windows
- Windows GUI files consolidated into `src/` (merged by cobra91 via PR #1 and PR #2)
### Proxy & GUI Improvements (cobra91 PR #3)
- CROF adaptive logic gated to `crof.ai` only — no more log pollution for other providers
- Data directory consolidation: all data now in `codex-proxy/` (was split across `codex-desktop/`, `codex-launcher/`, `codex-proxy/`)
- Sticky proxy port: persists in `.last-proxy-port`, reused on restart so Codex Desktop keeps connection
- Adaptive compact budget raised from 60% to 80% — avoids premature compaction on large-context models (DeepSeek v4 Pro 1M)
- Config cleanup fix: stale `proxy-*.json` cleanup moved after `_init_runtime()` to avoid deleting active config
- Windows GUI: added Clear Log, Restart Proxy, View Log buttons
- **Linux/Windows feature parity**: both GUIs now have identical features
- Windows GUI: ported OAuth Secrets all-providers dialog (Google + Freebuff/Codebuff with Re-OAuth buttons, token status)
- Windows GUI: added Codebuff/Freebuff OAuth login flow (GitHub browser-based)
- Windows GUI: added Sync from Preset button in endpoint editor
- Linux GUI: added Clear Log + Restart Proxy buttons (matching Windows)
## v3.10.5 (2026-05-25)
**Windows GUI + Context Compaction for Antigravity/Gemini OAuth**
### Windows Native GUI (tkinter)
- **Windows GUI** in `windows/` folder — full tkinter port by cobra91
- OAuth Secrets editor, Import JSON, Antigravity model list
- Shared backend with Linux (same translate-proxy.py)
- See README for Windows installation and usage
**Context Compaction for Antigravity/Gemini OAuth**
### Fix
- **Prevent `input token count exceeds maximum` errors** during long conversations
- Added aggressive compaction policies for Antigravity (`cloudcode-pa`) and Gemini CLI (`googleapis`)
- Auto-trims old turns when approaching 60% of model context limit (1M tokens for Gemini, 200K for Claude, 128K for GPT-OSS)
- Added REST model IDs to context size map (`gemini-3-flash`, `gemini-3.1-pro-low`, `claude-sonnet-4-6`, etc.)
## v3.10.4 (2026-05-25)
**Security: OAuth Secrets Editor + Import JSON**
### Security
- **All hardcoded OAuth secrets removed from source code and git history**
- OAuth client IDs and secrets now stored locally in `~/.config/codex-launcher/oauth-secrets.json`
- Git history rewritten to scrub all leaked credentials (0 matches verified)
- Pre-push hook blocks any future commit containing secrets
- All old Gitea releases deleted (contained leaked secrets in .deb files)
### New Features
- **OAuth Secrets editor** in GUI — "OAuth Secrets" button in header bar
- **Import JSON** button — import `client_secret_*.json` downloaded from Google Cloud Console
- Supports both `"installed"` and `"web"` JSON formats from Google
### Antigravity Fix (from v3.10.3)
- Antigravity REST API uses slug IDs, not display names
- Verified all model IDs with live API testing
## v3.10.3 (2026-05-25)
**Fix Antigravity 404 Errors — Verified REST Model IDs**
### Critical Fix
- Antigravity REST API (`v1internal:generateContent`) uses slug IDs, not display names
- Verified all model IDs with live API testing against `daily-cloudcode-pa.sandbox.googleapis.com`
- Display names map to closest working REST model (e.g. `Gemini 3.5 Flash (High)``gemini-3-flash`)
- Model list now matches agy CLI: Gemini 3.5 Flash (H/M/L), Gemini 3.1 Pro (H/L), Claude Sonnet/Opus 4.6, GPT-OSS 120B
### Working REST Model IDs
| Display Name | REST ID |
|---|---|
| Gemini 3.5 Flash (High) | gemini-3-flash |
| Gemini 3.5 Flash (Medium) | gemini-3-flash |
| Gemini 3.5 Flash (Low) | gemini-3.5-flash-low |
| Gemini 3.1 Pro (High) | gemini-3.1-pro-low |
| Gemini 3.1 Pro (Low) | gemini-3.1-pro-low |
| Claude Sonnet 4.6 (Thinking) | claude-sonnet-4-6 |
| Claude Opus 4.6 (Thinking) | claude-opus-4-6-thinking |
| GPT-OSS 120B (Medium) | gpt-oss-120b-medium |
## v3.10.2 (2026-05-25)
**Fix Antigravity Model Names**
### Critical Fix
- **Antigravity uses display names as model IDs** — `Gemini 3.5 Flash (High)` not `gemini-3.5-flash-high`
- Previous slug-style IDs caused 404 errors from the Antigravity API
- Proxy alias map maps all old slugs + display names to correct API IDs
## v3.10.0 (2026-05-25)
**Provider Model Editor + Antigravity Model Refresh**
### Provider Editor
- **Remove Selected** button to remove highlighted model(s) from provider
- **Clear All** button to empty model list
- **Sync from Preset** button to refresh model list from current preset definition
- Preset sync now replaces (not appends) models — fixes stale saved model lists
### Antigravity Models Updated
- **Gemini 3.5 Flash** (High / Medium)
- **Gemini 3.1 Pro** (High / Low)
- **Claude Sonnet 4.6 Thinking**
- **Claude Opus 4.6 Thinking**
- **GPT-OSS 120B Medium**
## v3.9.9 (2026-05-25)
**Antigravity Model Refresh**
### Updated Models
- **Gemini 3.5 Flash** (High / Medium) — new flagship flash model
- **Gemini 3.1 Pro** (High / Low) — tiered reasoning control
- **Claude Sonnet 4.6 Thinking** — Anthropic partner model via Antigravity
- **Claude Opus 4.6 Thinking** — Anthropic partner model via Antigravity
- **GPT-OSS 120B Medium** — open-weight GPT model via Antigravity
- Removed stale `antigravity-*` prefixed IDs and old preview models
### Proxy Updates
- Alias map updated for tiered model IDs (high/medium/low/thinking)
- Context sizes added for all new Antigravity models
## v3.9.8 (2026-05-25)
**Codex Desktop Model Fix & Global BrokenPipeError Protection**
### Desktop Model Fix
- **Codex Desktop sending wrong model** (gpt-5.4-mini) instead of user-selected model — now remapped via `CODEX_LAUNCHER_MODEL` env var
- **Config.toml** now writes `review_model`, `wire_api`, `request_max_retries`, `stream_max_retries`, `stream_idle_timeout_ms` for Desktop compatibility
- **Proxy model remap** intercepts Desktop forced models (`gpt-5.4-mini`, `gpt-5.5`, etc.) and routes to the user's selected model
### Global Crash Fix
- **`send_json()` globally catches BrokenPipeError** — no more crashes on client disconnect across all backends
## v3.9.7 (2026-05-25)
**Codebuff Error Forwarding & Crash Fixes**

View File

@@ -9,28 +9,13 @@
<a href="https://z.ai/subscribe?ic=ROK78RJKNW">z.ai/subscribe</a>
</p>
<p align="center">
---
If you want fork it, use the Github copy, here it is:
<a href="https://github.com/roman-ryzenadvanced/Codex-Launcher-Any-AI-Provider">Codex-Any-AI-Provider on Github (Official)</a>
---
</p>
<h1 align="center">Codex Launcher — Any AI Provider</h1>
<p align="center">
<strong>Run OpenAI Codex CLI &amp; Desktop with <em>any</em> AI provider.</strong><br/>
Google Antigravity &bull; Gemini CLI &bull; OpenCode &bull; Z.AI &bull; Anthropic &bull; Command Code &bull; Freebuff &bull; OpenRouter &bull; Crof.ai &bull; NVIDIA NIM &bull; OpenAdapter &bull; Kilo.ai &bull; DeepSeek &bull; and more
</p>
<p align="center">
<sub>
Windows version by <a href="https://github.com/cobra91">cobra91</a> &bull;
Original Linux development by <a href="https://github.com/roman-ryzenadvanced">roman-ryzenadvanced</a>
</sub>
Google Antigravity &bull; Gemini CLI &bull; OpenCode &bull; Z.AI &bull; Anthropic &bull; Command Code &bull; Codebuff &bull; OpenRouter &bull; Crof.ai &bull; NVIDIA NIM &bull; OpenAdapter &bull; Kilo.ai &bull; DeepSeek &bull; and more
</p>
<p align="center">
@@ -561,16 +546,6 @@ Codex Launcher includes special handling for Gemini 3 / Antigravity OAuth:
- **User instruction enforcement**: The latest user message is guaranteed to be the
final content turn sent to Gemini, even after compaction.
- **Smart compaction**: Old tool outputs capped at 3000 chars, recent 6 at 20000 chars.
- **Context compaction**: Aggressive auto-trimming when approaching 60% of model context
limit (1M tokens Gemini, 200K Claude, 128K GPT-OSS). Prevents token limit errors.
- **Model ID mapping**: Display names (e.g. `Gemini 3.5 Flash (High)`) mapped to REST API
slugs (e.g. `gemini-3-flash`). See `docs/ANTIGRAVITY.md` for details.
### OAuth Secrets
Google OAuth credentials are stored locally in `~/.config/codex-launcher/oauth-secrets.json`
and never committed to the repository. Use the **OAuth Secrets** button in the launcher
header to edit or import `client_secret_*.json` files from Google Cloud Console.
---
@@ -630,7 +605,7 @@ curl http://127.0.0.1:PORT/v1/accounts
| OpenCode Zen | OpenAI-compat | `https://opencode.ai/zen/v1` |
| OpenCode Go | OpenAI-compat | `https://opencode.ai/zen/go/v1` |
| Command Code | Command Code | `https://api.commandcode.ai` |
| **Codebuff / Freebuff** | **Codebuff** | `https://www.codebuff.com` *(free DeepSeek/Kimi — OAuth login built-in)* |
| **Codebuff** | **Codebuff** | `https://codebuff.com` *(free DeepSeek/Kimi — OAuth login built-in)* |
| Crof.ai | OpenAI-compat | `https://crof.ai/v1` |
| OpenAdapter | OpenAI-compat | `https://api.openadapter.in/v1` |
| Z.ai Coding | OpenAI-compat | `https://api.z.ai/api/coding/paas/v4` |
@@ -643,14 +618,14 @@ curl http://127.0.0.1:PORT/v1/accounts
| Google Antigravity (OAuth) | Antigravity OAuth | `daily-cloudcode-pa.sandbox.googleapis.com` |
| Custom | Any | User-defined |
### Free Models (via Codebuff/Freebuff)
Codebuff/Freebuff provides free access to these models — no API key needed:
### Free Models (via Codebuff)
Codebuff provides free access to these models — no API key needed:
- **DeepSeek V4 Pro** — Smartest model
- **DeepSeek V4 Flash** — Most efficient
- **Kimi K2.6** — Balanced
- **MiniMax M2.7** — Fastest
*Requires: `freebuff login` via GUI OAuth button, or `npm install -g freebuff && freebuff login` (GitHub OAuth)*
*Requires: `codebuff login` via GUI OAuth button, or `npm install -g codebuff && codebuff login` (GitHub OAuth)*
---
@@ -787,70 +762,15 @@ codex --profile my-profile -c model=my-model
---
## Windows Version
A native **Windows GUI** (tkinter) is available in the `src/` folder alongside the Linux version. Both GUIs have **full feature parity**.
<p align="center">
<sub>
Windows version by <a href="https://github.com/cobra91">cobra91</a> &bull;
Original Linux development by <a href="https://github.com/roman-ryzenadvanced">roman-ryzenadvanced</a>
</sub>
</p>
### Files
| File | Purpose |
|---|---|
| `src/codex-launcher-gui.py` | tkinter GUI (Windows) — manage endpoints, launch Codex CLI/Desktop |
| `src/codex-launcher-gui` | GTK GUI (Linux) — same features, native GTK look |
| `src/codex_launcher_lib.py` | Shared library — proxy lifecycle, config, OAuth, diagnostics |
| `src/translate-proxy.py` | Proxy — translates Responses API for any provider |
### How to Run (Windows)
Python ≥ 3.8 with tkinter is required (comes with the official Python installer).
```powershell
# From repo root
cd src
python codex-launcher-gui.py
```
The GUI will:
1. Auto-create default endpoints on first run
2. Show a toolbar with Endpoints, OAuth Secrets, AI Monitor, and more
3. Launch Codex CLI/Desktop with your chosen provider
### OAuth Credentials
Google OAuth (Antigravity / Gemini CLI) requires a `client_secret_*.json` from [Google Cloud Console](https://console.cloud.google.com/apis/credentials). Use the **OAuth Secrets** button in the GUI to import it — credentials are stored locally in `~/.config/codex-launcher/oauth-secrets.json`, never in the repo.
The **OAuth Secrets** dialog shows all providers (Google + Freebuff/Codebuff) with **Re-OAuth buttons** to instantly re-authenticate any provider.
### Feature Parity
Both Linux (GTK) and Windows (tkinter) GUIs have identical features:
- All provider presets, endpoint management, BGP routing
- OAuth Secrets with all providers + Re-OAuth buttons
- AI Monitor, Usage Dashboard, Request History, Benchmark
- Clear Log, Restart Proxy, View Log
- Doctor, Diagnostic Agent, Profile Backup/Import
- Antigravity model mapping, context compaction (80% budget)
- Multi-account rotation, rate limit handling
---
## Requirements
- Python ≥ 3.8
- python3-gi (`sudo apt install python3-gi`) — Linux only
- tkinter (`python3-tk`) — Windows / Linux GUI
- python3-gi (`sudo apt install python3-gi`)
- Codex CLI ≥ 2.0
- Codex Desktop (optional, for Desktop mode)
- bash, curl, lsof — Linux only
- bash, curl, lsof
**No pip dependencies.** Zero. Pure stdlib.
**No pip dependencies.** Zero. Pure stdlib + system GTK.
---

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@@ -1,335 +0,0 @@
# Antigravity (Google CloudCode) — Technical Reference
Everything needed to understand, maintain, and debug the Antigravity OAuth provider integration in Codex Launcher.
---
## 1. What Is Antigravity?
Antigravity is Google's internal codename for **Google CloudCode** — a cloud-based AI coding agent powered by Gemini and other models. The CLI tool (`agy`) is a native Go binary that uses gRPC to communicate with Google's CloudCode backend.
- **Official CLI binary**: `~/.local/bin/agy-core` (ELF x86-64 Go binary, ~183MB)
- **Wrapper script**: `~/.local/bin/agy` (Python, manages provider switching)
- **CLI settings**: `~/.gemini/antigravity-cli/settings.json`
- **Provider state**: `~/.gemini/antigravity-cli/agy_provider.json`
---
## 2. Two API Protocols — REST vs gRPC
### 2.1 What the agy CLI uses (gRPC)
The native `agy-core` binary uses **gRPC** to communicate with the CloudCode backend:
- **Service**: `google.internal.cloud.code.v1internal.PredictionService`
- **Methods**:
- `GenerateContent` — main inference
- `FetchAvailableModels` — list available models
- `CountTokens` — token counting
- `RetrieveUserQuota` — quota check
- **Other services**: `CloudCode`, `JetskiService` (settings, plugins, etc.)
- **Proto files**: `google/internal/cloud/code/v1internal/prediction_service.proto`, `cloudcode.proto`
- **Model IDs in gRPC**: Display names like `"Gemini 3.5 Flash (High)"` — verified from `settings.json`
### 2.2 What our proxy uses (REST)
Our Codex Launcher proxy does NOT use gRPC. It uses the **REST API** that the CloudCode backend also exposes:
- **Endpoint path**: `v1internal:generateContent` (non-streaming) / `v1internal:streamGenerateContent?alt=sse` (streaming SSE)
- **This is NOT the standard Gemini REST API** — it's the CloudCode-internal REST gateway
- **Model IDs in REST**: Slug-style IDs like `gemini-3-flash` — NOT display names
- **The REST API is more limited** — fewer model variants available than gRPC
### 2.3 Why not gRPC?
The agy binary uses gRPC with protobuf serialization. Using gRPC from the proxy would require:
- Maintaining proto definitions (compiled from the binary)
- More complex streaming
- The `grpcio` Python library (not installed by default)
The REST API works well enough for our use case.
---
## 3. Endpoints
The proxy tries these endpoints in order for Antigravity:
```
1. https://daily-cloudcode-pa.sandbox.googleapis.com (primary)
2. https://autopush-cloudcode-pa.sandbox.googleapis.com (fallback)
3. https://cloudcode-pa.googleapis.com (production fallback)
```
For regular Gemini CLI OAuth, only `cloudcode-pa.googleapis.com` is used.
---
## 4. Authentication
### 4.1 OAuth Flow
- **Client IDs**: Stored locally in `~/.config/codex-launcher/oauth-secrets.json` (not in repo)
- **OAuth callback**: `https://antigravity.google/oauth-callback`
- **Token storage**: `~/.cache/codex-proxy/google-antigravity-oauth-token.json`
- **Token refresh**: via `https://oauth2.googleapis.com/token`
- **Scopes**: `email profile openid cloud-platform cclog experimentsandconfigs userinfo.email userinfo.profile`
- **Note**: The token does NOT have `auth/aicode` scope — it uses `cloud-platform` instead
### 4.2 Multi-Account Support
- `GoogleAccountPool("antigravity")` manages multiple Google accounts
- Token files: `google-antigravity-oauth-token.json`, `google-antigravity-oauth-token-2.json`, etc.
- Round-robin rotation across accounts
---
## 5. Request Format
### 5.1 REST Request Wrapper
The proxy wraps the Gemini-format request body in an outer envelope:
```json
{
"project": "<gcp-project-id>",
"model": "<rest-model-id>",
"requestType": "agent",
"userAgent": "antigravity",
"requestId": "agent-<uuid>",
"request": {
"contents": [...],
"systemInstruction": {...},
"generationConfig": {...},
"tools": [...]
}
}
```
### 5.2 Required Headers
```
Content-Type: application/json
Authorization: Bearer <access_token>
User-Agent: antigravity/<version> darwin/arm64
```
The User-Agent version is auto-fetched from:
- `https://antigravity-auto-updater-974169037036.us-central1.run.app`
- Fallback: `https://antigravity.google/changelog`
- Cached in `~/.cache/codex-proxy/antigravity-version.json`
- Default: `1.18.3`
---
## 6. Model ID Mapping (CRITICAL)
### 6.1 The Problem
The agy CLI shows models with display names:
- `Gemini 3.5 Flash (High)`
- `Claude Sonnet 4.6 (Thinking)`
But the **REST API only accepts slug IDs**:
- `gemini-3-flash`
- `claude-sonnet-4-6`
Sending display names to the REST API returns **HTTP 404 "Requested entity was not found"**.
### 6.2 Verified Working Model IDs
All tested with live API calls to `daily-cloudcode-pa.sandbox.googleapis.com/v1internal:generateContent` on 2026-05-25:
| Display Name (agy CLI / GUI) | REST API Model ID | Status |
|---|---|---|
| Gemini 3.5 Flash (High) | `gemini-3-flash` | OK |
| Gemini 3.5 Flash (Medium) | `gemini-3-flash` | OK |
| Gemini 3.5 Flash (Low) | `gemini-3.5-flash-low` | OK |
| Gemini 3.1 Pro (High) | `gemini-3.1-pro-low` | OK (only low tier works via REST) |
| Gemini 3.1 Pro (Low) | `gemini-3.1-pro-low` | OK |
| Claude Sonnet 4.6 (Thinking) | `claude-sonnet-4-6` | OK |
| Claude Opus 4.6 (Thinking) | `claude-opus-4-6-thinking` | OK |
| GPT-OSS 120B (Medium) | `gpt-oss-120b-medium` | OK |
| Gemini 2.5 Flash | `gemini-2.5-flash` | OK |
| Gemini 2.5 Flash Lite | `gemini-2.5-flash-lite` | OK |
| Gemini 2.5 Pro | `gemini-2.5-pro` | 503 (exists, no capacity) |
### 6.3 Models That Return 404 via REST
These exist in gRPC but NOT in the REST API:
```
gemini-3-flash-high, gemini-3-flash-medium, gemini-3-flash-low
gemini-3.5-flash, gemini-3.5-flash-high, gemini-3.5-flash-medium
gemini-3.1-pro-high (400, not 404, but doesn't work)
gemini-3-pro, gemini-3-pro-high, gemini-3-pro-low (500)
gemini-3.1-flash, gemini-3.1-flash-high
claude-sonnet-4, claude-sonnet-4-5, claude-sonnet-4-6-thinking
claude-opus-4, claude-opus-4-5
claude-haiku-4-5
gpt-oss-120b, gpt-oss-120b-maas, gpt-oss-20b-maas
```
### 6.4 How the Mapping Works
1. GUI shows display names (matching agy CLI): `Gemini 3.5 Flash (High)`
2. Codex CLI sends whatever model ID the user selected
3. Proxy `alias_map` translates: `"Gemini 3.5 Flash (High)" → "gemini-3-flash"`
4. Proxy sends REST request with `"model": "gemini-3-flash"`
The alias map is in `_handle_gemini_oauth()` around line 4316 of `translate-proxy.py`.
---
## 7. Response Format
### 7.1 Non-Streaming
```json
{
"response": {
"candidates": [{
"content": {
"role": "model",
"parts": [{"text": "..."}]
},
"finishReason": "STOP"
}]
}
}
```
### 7.2 Streaming (SSE)
Content-Type: `text/event-stream`
Each SSE event contains a JSON chunk with the same structure. The proxy converts these to OpenAI Responses API format for Codex CLI.
---
## 8. Context Sizes
```python
"Gemini 3.5 Flash": 1000000, "Gemini 3.1 Pro": 2000000,
"gemini-3-flash": 1000000, "gemini-3.1-pro-low": 2000000,
"gemini-3.5-flash-low": 1000000,
"Claude Sonnet 4.6": 200000, "Claude Opus 4.6": 200000,
"claude-sonnet-4-6": 200000, "claude-opus-4-6-thinking": 200000,
"GPT-OSS 120B": 128000, "gpt-oss-120b-medium": 128000,
"gemini-2.5-flash": 1000000, "gemini-2.5-pro": 2000000,
```
---
## 9. Key Proxy Code Locations
| Component | File | Line (approx) |
|---|---|---|
| Antigravity version | translate-proxy.py | 287-288 |
| Version fetcher | translate-proxy.py | 705-748 |
| Model alias map | translate-proxy.py | ~4316 |
| REST request building | translate-proxy.py | ~4563-4602 |
| Endpoint fallback loop | translate-proxy.py | ~4610 |
| SSE streaming handler | translate-proxy.py | `_forward_gemini_sse()` |
| Auto-continue for MAX_TOKENS | translate-proxy.py | `_auto_continue_gemini()` |
| OAuth token refresh | translate-proxy.py | `_refresh_oauth_token_for()` |
| Google account pool | translate-proxy.py | `_google_antigravity_pool` |
| GUI preset models | codex-launcher-gui | ~358 |
| GUI static model list | codex-launcher-gui | ~760 `_ANTIGRAVITY_MODELS` |
| GUI fetch_models shortcut | codex-launcher-gui | ~770 `fetch_models_for_endpoint()` |
---
## 10. Debugging
### 10.1 Debug Logs
- **Proxy stderr**: Shows model mapping, request details, errors
- **400 error dump**: `~/.cache/codex-proxy/gemini-last-400-request.json`
- **Long context dump**: `~/.cache/codex-proxy/gemini-long-ctx-<session>.json`
### 10.2 Quick API Test
```bash
TOKEN=$(python3 -c "import json; print(json.load(open('$HOME/.cache/codex-proxy/google-antigravity-oauth-token.json'))['access_token'])")
curl -s "https://daily-cloudcode-pa.sandbox.googleapis.com/v1internal:generateContent" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-H "User-Agent: antigravity/2.0.1 darwin/arm64" \
-d '{
"project": "voltaic-hangout-z1qhf",
"model": "gemini-3-flash",
"requestType": "agent",
"userAgent": "antigravity",
"requestId": "test-123",
"request": {
"contents": [{"role": "user", "parts": [{"text": "say hi"}]}]
}
}'
```
### 10.3 Token Info
```bash
TOKEN=$(python3 -c "import json; print(json.load(open('$HOME/.cache/codex-proxy/google-antigravity-oauth-token.json'))['access_token'])")
curl -s "https://www.googleapis.com/oauth2/v3/tokeninfo?access_token=$TOKEN" | python3 -m json.tool
```
### 10.4 Common Errors
| Error | Cause | Fix |
|---|---|---|
| 404 "Requested entity was not found" | Wrong model ID (display name instead of slug) | Check alias_map |
| 404 on `/v1/models` | Antigravity has no models REST endpoint | Proxy returns static list |
| 404 on POST /responses | Codex CLI routing issue, not Antigravity | Check proxy is running |
| 503 "No capacity" | Model exists but overloaded | Try another model or endpoint |
| 500 "Unknown Error" | Model ID exists but broken on server | Known for gemini-3-pro-low |
| PERMISSION_DENIED (gRPC) | Token lacks scope or empty request body | Use REST API instead |
---
## 11. Version History (Antigravity-specific)
| Version | Date | Change |
|---|---|---|
| v3.10.3 | 2026-05-25 | **Fix 404**: Verified REST model IDs, display→slug mapping |
| v3.10.2 | 2026-05-25 | Wrong fix: tried display names (didn't work) |
| v3.10.0 | 2026-05-25 | Provider model editor, static Antigravity model list |
| v3.9.9 | 2026-05-25 | Refreshed Antigravity models (slugs were wrong) |
| v3.3.0 | Earlier | Initial Antigravity OAuth + tool calls + SSE streaming |
---
## 12. Testing a New Model ID
If new models appear in the agy CLI, verify them against the REST API before adding:
```python
# Test a candidate model ID
import urllib.request, json, os
token = json.load(open(os.path.expanduser("~/.cache/codex-proxy/google-antigravity-oauth-token.json")))["access_token"]
wrapped = {
"project": "voltaic-hangout-z1qhf", "model": "NEW-MODEL-ID",
"requestType": "agent", "userAgent": "antigravity",
"requestId": "test-123",
"request": {"contents": [{"role": "user", "parts": [{"text": "say hi"}]}]},
}
req = urllib.request.Request(
"https://daily-cloudcode-pa.sandbox.googleapis.com/v1internal:generateContent",
data=json.dumps(wrapped).encode(),
headers={"Content-Type": "application/json", "Authorization": f"Bearer {token}", "User-Agent": "antigravity/2.0.1 darwin/arm64"},
)
try:
resp = urllib.request.urlopen(req, timeout=15)
print("OK:", resp.read().decode()[:200])
except urllib.error.HTTPError as e:
print(f"{e.code}:", e.read().decode()[:200])
```
Then update:
1. `alias_map` in `translate-proxy.py` — add display name → REST slug mapping
2. `_ANTIGRAVITY_MODELS` in `codex-launcher-gui` — add display name to list
3. Preset in `codex-launcher-gui` — add display name to `"Google Antigravity (OAuth)"` models
4. Context sizes in `translate-proxy.py` — add model ID to `_MODEL_CTX` dict

View File

@@ -3,11 +3,11 @@ set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
if [ -f "$SCRIPT_DIR/codex-launcher_3.10.10_all.deb" ]; then
echo "Installing codex-launcher_3.10.10_all.deb ..."
sudo dpkg -i "$SCRIPT_DIR/codex-launcher_3.10.10_all.deb"
if [ -f "$SCRIPT_DIR/codex-launcher_3.9.7_all.deb" ]; then
echo "Installing codex-launcher_3.9.7_all.deb ..."
sudo dpkg -i "$SCRIPT_DIR/codex-launcher_3.9.7_all.deb"
echo ""
echo "Installed v3.10.10 via .deb package."
echo "Installed v3.9.7 via .deb package."
echo " translate-proxy.py -> /usr/bin/translate-proxy.py"
echo " codex-launcher-gui -> /usr/bin/codex-launcher-gui"
echo " cleanup-codex-stale -> /usr/bin/cleanup-codex-stale.sh"

View File

@@ -1,101 +0,0 @@
#!/usr/bin/env python3
"""Cleanup stale Codex Launcher processes and artifacts — cross-platform.
Kills registered process groups and removes stale PID/socket files left
by previous Codex Launcher sessions.
Windows: uses taskkill /F /T /PID
Linux: uses kill -TERM -- -PGID
"""
import json, os, sys, subprocess, time
from pathlib import Path
IS_WINDOWS = sys.platform == "win32"
if IS_WINDOWS:
_local = os.environ.get("LOCALAPPDATA", str(Path.home() / "AppData" / "Local"))
PID_REGISTRY = Path(_local) / "codex-proxy" / "pids.json"
CODEX_DIR = Path.home() / ".codex"
_local_share = Path(_local)
_cache = Path(_local)
else:
PID_REGISTRY = Path.home() / ".cache" / "codex-proxy" / "pids.json"
CODEX_DIR = Path.home() / ".codex"
_local_share = Path.home() / ".local" / "share"
_cache = Path.home() / ".cache"
def kill_group(pid):
if IS_WINDOWS:
subprocess.run(["taskkill", "/F", "/T", "/PID", str(pid)],
capture_output=True, timeout=10)
else:
import signal
try:
pgid = os.getpgid(pid)
os.killpg(pgid, signal.SIGTERM)
time.sleep(0.5)
try:
os.killpg(pgid, signal.SIGKILL)
except OSError:
pass
except OSError:
pass
def main():
print("[cleanup] Cleaning up stale Codex Launcher processes...", file=sys.stderr)
if PID_REGISTRY.exists():
try:
with open(PID_REGISTRY) as f:
registry = json.load(f)
except Exception as e:
print(f"[cleanup] Failed to read PID registry: {e}", file=sys.stderr)
registry = {}
for kind, info in registry.items():
pid = info.get("pid") if isinstance(info, dict) else info
if pid and isinstance(pid, int):
print(f"[cleanup] Killing {kind} (PID {pid})", file=sys.stderr)
kill_group(pid)
try:
PID_REGISTRY.unlink()
except OSError:
pass
else:
print("[cleanup] No PID registry found — nothing to stop", file=sys.stderr)
stale_files = []
if IS_WINDOWS:
stale_files = [
_cache / "codex-desktop" / ".codex-desktop-pid",
_cache / "codex-desktop" / ".webview-pid",
]
else:
stale_files = [
CODEX_DIR / ".launch-action-socket",
CODEX_DIR / ".codex-desktop-launch-action",
CODEX_DIR / ".codex-desktop-pid",
CODEX_DIR / ".webview-pid",
_local_share / "codex-desktop" / ".codex-desktop-pid",
_local_share / "codex-desktop" / ".webview-pid",
_cache / "codex-desktop" / ".codex-desktop-pid",
_cache / "codex-desktop" / ".webview-pid",
]
for fp in stale_files:
try:
if fp.exists():
fp.unlink()
print(f"[cleanup] Removed {fp}", file=sys.stderr)
except OSError:
pass
print("[cleanup] Done", file=sys.stderr)
if __name__ == "__main__":
main()

View File

@@ -26,34 +26,6 @@ model_catalog_json = ""
"""
CHANGELOG = [
("3.10.4", "2026-05-25", [
"OAuth Secrets editor in GUI — update client ID/secret without editing files",
"Secrets stored in ~/.config/codex-launcher/oauth-secrets.json (not in repo)",
]),
("3.10.3", "2026-05-25", [
"Fix Antigravity 404: map display names to verified REST API model IDs",
"REST API uses slugs (gemini-3-flash) not display names (Gemini 3.5 Flash)",
"Match agy CLI model list: Gemini 3.5 Flash (H/M/L), 3.1 Pro (H/L), Claude 4.6, GPT-OSS",
]),
("3.10.2", "2026-05-25", [
"Fetch from API now works for Antigravity — returns current model list",
]),
("3.10.0", "2026-05-25", [
"Provider editor: Remove Selected, Clear All, Sync from Preset buttons for model list",
"Sync from Preset replaces model list with current preset models",
"Stale saved Antigravity models auto-refreshed on preset sync",
]),
("3.9.9", "2026-05-25", [
"Refresh Antigravity preset: Gemini 3.5 Flash, Gemini 3.1 Pro, Claude Sonnet/Opus 4.6, GPT-OSS 120B",
"Fix Antigravity alias map for new tiered model IDs (high/medium/low/thinking)",
"Add model context sizes for Gemini 3.5 Flash, Gemini 3.1 Pro, Claude 4.6, GPT-OSS 120B",
]),
("3.9.8", "2026-05-25", [
"Fix Codex Desktop sending wrong model (gpt-5.4-mini) instead of selected model",
"Proxy remaps Desktop forced models to user-selected model via CODEX_LAUNCHER_MODEL",
"Write review_model + wire_api + retries to config.toml for Desktop compatibility",
"send_json() globally catches BrokenPipeError — no more crashes on disconnect",
]),
("3.9.7", "2026-05-25", [
"Forward real Codebuff error messages to user (not generic 429)",
"Return HTTP 200 with Responses API format for rate limits so Codex displays message",
@@ -364,11 +336,13 @@ PROVIDER_PRESETS = {
"base_url": "https://daily-cloudcode-pa.sandbox.googleapis.com",
"oauth_provider": "google-antigravity",
"models": [
"Gemini 3.5 Flash (High)", "Gemini 3.5 Flash (Medium)", "Gemini 3.5 Flash (Low)",
"Gemini 3.1 Pro (High)", "Gemini 3.1 Pro (Low)",
"Claude Sonnet 4.6 (Thinking)",
"Claude Opus 4.6 (Thinking)",
"GPT-OSS 120B (Medium)",
"antigravity-gemini-3-flash",
"antigravity-gemini-3-pro",
"antigravity-gemini-3.1-pro",
"antigravity-claude-sonnet-4-6",
"antigravity-claude-opus-4-6-thinking",
"gemini-2.5-flash", "gemini-2.5-pro",
"gemini-3-flash-preview", "gemini-3-pro-preview", "gemini-3.1-pro-preview",
],
},
"OpenAdapter": {
@@ -393,25 +367,7 @@ PROVIDER_PRESETS = {
},
"Codebuff (Free DeepSeek/Kimi)": {
"backend_type": "codebuff",
"base_url": "https://www.codebuff.com",
"oauth_provider": "codebuff",
"models": [
"deepseek/deepseek-v4-pro", "deepseek/deepseek-v4-flash",
"moonshotai/kimi-k2.6", "minimax/minimax-m2.7",
],
},
"Freebuff (Free DeepSeek/Kimi)": {
"backend_type": "codebuff",
"base_url": "https://www.codebuff.com",
"oauth_provider": "codebuff",
"models": [
"deepseek/deepseek-v4-pro", "deepseek/deepseek-v4-flash",
"moonshotai/kimi-k2.6", "minimax/minimax-m2.7",
],
},
"FreeBuff": {
"backend_type": "codebuff",
"base_url": "https://www.codebuff.com",
"base_url": "https://codebuff.com",
"oauth_provider": "codebuff",
"models": [
"deepseek/deepseek-v4-pro", "deepseek/deepseek-v4-flash",
@@ -779,18 +735,7 @@ def endpoint_model_headers(endpoint):
headers["Authorization"] = f"Bearer {key}"
return headers
_ANTIGRAVITY_MODELS = [
"Gemini 3.5 Flash (High)", "Gemini 3.5 Flash (Medium)", "Gemini 3.5 Flash (Low)",
"Gemini 3.1 Pro (High)", "Gemini 3.1 Pro (Low)",
"Claude Sonnet 4.6 (Thinking)",
"Claude Opus 4.6 (Thinking)",
"GPT-OSS 120B (Medium)",
]
def fetch_models_for_endpoint(endpoint, timeout=10):
bt = endpoint.get("backend_type", "")
if bt == "gemini-oauth-antigravity":
return list(_ANTIGRAVITY_MODELS), None
url = endpoint_models_url(endpoint)
if not url:
return None, "Base URL is empty"
@@ -991,21 +936,15 @@ def write_config_for_translated(endpoint, selected_model, proxy_port=8080):
lines = [
f'model = "{_toml_safe(selected_model)}"\n',
f'review_model = "{_toml_safe(selected_model)}"\n',
f'model_provider = "{_toml_safe(endpoint["name"])}"\n',
f'model_catalog_json = "{mc_path}"\n',
f'\n[model_providers."{endpoint["name"]}"]\n',
f'name = "{_toml_safe(endpoint["name"])}"\n',
f'base_url = "http://127.0.0.1:{proxy_port}"\n',
f'experimental_bearer_token = "codex-launcher-local"\n',
f'wire_api = "responses"\n',
f'request_max_retries = 1\n',
f'stream_max_retries = 0\n',
f'stream_idle_timeout_ms = 600000\n',
f'\n[profiles."{endpoint["name"]}"]\n',
f'model_provider = "{_toml_safe(endpoint["name"])}"\n',
f'model = "{_toml_safe(selected_model)}"\n',
f'review_model = "{_toml_safe(selected_model)}"\n',
f'model_catalog_json = "{mc_path}"\n',
f'service_tier = "fast"\n',
f'approvals_reviewer = "user"\n',
@@ -1798,7 +1737,7 @@ class LauncherWin(Gtk.Window):
# header row
hdr = Gtk.Box(spacing=8)
vbox.pack_start(hdr, False, False, 0)
lbl = Gtk.Label(label="<b>Codex Launcher v3.10.7</b>")
lbl = Gtk.Label(label="<b>Codex Launcher v3.9.7</b>")
lbl.set_use_markup(True)
hdr.pack_start(lbl, False, False, 0)
changelog_btn = Gtk.Button(label="Changelog")
@@ -1822,9 +1761,6 @@ class LauncherWin(Gtk.Window):
mgr_btn = Gtk.Button(label="Manage Endpoints")
mgr_btn.connect("clicked", lambda b: self._open_mgr())
hdr.pack_end(mgr_btn, False, False, 0)
oauth_btn = Gtk.Button(label="OAuth Secrets")
oauth_btn.connect("clicked", lambda b: self._edit_oauth_secrets())
hdr.pack_end(oauth_btn, False, False, 0)
# verification status bar
self._cli_info = _detect_codex_cli()
@@ -1977,13 +1913,6 @@ class LauncherWin(Gtk.Window):
assist_btn.connect("clicked", lambda b: self._open_assistant())
assist_btn.set_tooltip_text("Open AI coding assistant with streaming, tools, and session management")
bb.pack_start(assist_btn, False, False, 0)
self._clear_log_btn = Gtk.Button(label="Clear Log")
self._clear_log_btn.connect("clicked", lambda b: self._buf.set_text(""))
bb.pack_start(self._clear_log_btn, False, False, 0)
self._restart_btn = Gtk.Button(label="Restart Proxy")
self._restart_btn.connect("clicked", lambda b: self._manual_restart_proxy())
self._restart_btn.set_sensitive(False)
bb.pack_start(self._restart_btn, False, False, 0)
self._kill_btn = Gtk.Button(label="Kill && Cleanup")
self._kill_btn.connect("clicked", lambda b: self._kill())
self._kill_btn.set_sensitive(False)
@@ -2080,7 +2009,6 @@ class LauncherWin(Gtk.Window):
self._btn_codex_desktop.set_sensitive(not busy and has_desk)
self._btn_codex_cli.set_sensitive(not busy and has_cli)
self._kill_btn.set_sensitive(busy)
self._restart_btn.set_sensitive(busy)
GLib.idle_add(_update)
def _rebuild_combo(self):
@@ -2214,32 +2142,16 @@ class LauncherWin(Gtk.Window):
GLib.idle_add(self.log, f"[AI Monitor] Alert: {action} (trigger: {trigger})")
def _restart_proxy_from_watcher(self):
try:
ep_name = load_endpoints().get("default")
if not ep_name:
return
for ep in load_endpoints().get("endpoints", []):
if ep.get("name") == ep_name:
self._start_proxy(ep)
break
except Exception as e:
self.log(f"[AI Monitor] Proxy restart failed: {e}")
def _manual_restart_proxy(self):
self._kill()
time.sleep(1)
try:
ep_name = load_endpoints().get("default")
if not ep_name:
self.log("No default endpoint set")
return
for ep in load_endpoints().get("endpoints", []):
if ep.get("name") == ep_name:
self._start_proxy(ep)
self.log("Proxy restarted")
break
except Exception as e:
self.log(f"Proxy restart failed: {e}")
try:
ep_name = load_endpoints().get("default")
if not ep_name:
return
for ep in load_endpoints().get("endpoints", []):
if ep.get("name") == ep_name:
self._start_proxy(ep)
break
except Exception as e:
self.log(f"[AI Monitor] Proxy restart failed: {e}")
def _open_usage(self):
try:
@@ -2522,7 +2434,6 @@ class LauncherWin(Gtk.Window):
if needs_proxy:
self.log("Starting translation proxy…")
os.environ["CODEX_LAUNCHER_MODEL"] = model
try:
proxy_port = _start_proxy_for(ep, self.log)
except RuntimeError as e:
@@ -2832,339 +2743,6 @@ class LauncherWin(Gtk.Window):
_stop_proxy()
Gtk.main_quit()
def _google_reoauth(self, provider):
secrets_path = os.path.expanduser("~/.config/codex-launcher/oauth-secrets.json")
try:
with open(secrets_path) as f:
secrets = json.load(f)
except Exception:
secrets = {}
is_antigravity = provider == "google-antigravity"
sec_key = "antigravity" if is_antigravity else "gemini_cli"
sec = secrets.get(sec_key, {})
client_id = sec.get("client_id", "")
client_secret = sec.get("client_secret", "")
if not client_id or not client_secret:
self._show_error_dialog("Missing OAuth secrets",
f"No client_id/client_secret for {sec_key}.\nSet them in OAuth Secrets first.")
return
token_file = "google-antigravity-oauth-token.json" if is_antigravity else "google-cli-oauth-token.json"
token_path = os.path.expanduser(f"~/.cache/codex-proxy/{token_file}")
redirect = "urn:ietf:wg:oauth:2.0:oob"
auth_url = (f"https://accounts.google.com/o/oauth2/v2/auth?client_id={client_id}"
f"&redirect_uri={urllib.parse.quote(redirect)}"
f"&response_type=code&scope={urllib.parse.quote('https://www.googleapis.com/auth/cloud-platform')}"
f"&access_type=offline&prompt=consent")
webbrowser.open(auth_url)
code_dlg = Gtk.Dialog(title=f"Re-OAuth: {'Antigravity' if is_antigravity else 'Gemini CLI'}", parent=self, modal=True)
code_dlg.add_button("Cancel", Gtk.ResponseType.CANCEL)
code_dlg.add_button("Exchange", Gtk.ResponseType.OK)
code_dlg.set_default_size(500, 180)
ca = code_dlg.get_content_area()
ca.set_margin_start(12)
ca.set_margin_end(12)
ca.set_spacing(6)
ca.pack_start(Gtk.Label(label="Browser opened for Google OAuth.\nPaste the authorization code below:", xalign=0), False, False, 0)
code_entry = Gtk.Entry()
code_entry.set_placeholder_text("4/0AX...")
ca.pack_start(code_entry, False, False, 4)
ca.show_all()
if code_dlg.run() == Gtk.ResponseType.OK:
code = code_entry.get_text().strip()
if code:
try:
tok_req = urllib.request.Request("https://oauth2.googleapis.com/token",
data=urllib.parse.urlencode({
"code": code, "client_id": client_id, "client_secret": client_secret,
"redirect_uri": redirect, "grant_type": "authorization_code"
}).encode(),
headers={"Content-Type": "application/x-www-form-urlencoded"})
tok_resp = urllib.request.urlopen(tok_req, timeout=30)
tok_data = json.loads(tok_resp.read())
tok_data["_updated"] = time.time()
os.makedirs(os.path.dirname(token_path), exist_ok=True)
with open(token_path, "w") as f:
json.dump(tok_data, f, indent=2)
self._log(f"[oauth] Refreshed {provider} token → {token_path}")
except Exception as e:
self._show_error_dialog("Token exchange failed", str(e)[:300])
code_dlg.destroy()
def _codebuff_reoauth(self):
self._codebuff_oauth_standalone()
def _codebuff_oauth_standalone(self):
import uuid
dlg = Gtk.Dialog(title="Freebuff / Codebuff Login", parent=self, modal=True)
dlg.add_button("Cancel", Gtk.ResponseType.CANCEL)
dlg.set_default_size(500, 240)
area = dlg.get_content_area()
area.set_margin_start(16)
area.set_margin_end(16)
area.set_margin_top(12)
area.set_margin_bottom(12)
area.set_spacing(8)
area.pack_start(Gtk.Label(label="<b>Sign in with GitHub via Codebuff</b>", use_markup=True, xalign=0), False, False, 0)
status_lbl = Gtk.Label(label="Requesting login URL…", xalign=0)
status_lbl.set_line_wrap(True)
status_lbl.set_max_width_chars(60)
area.pack_start(status_lbl, False, False, 4)
link_lbl = Gtk.Label(xalign=0)
link_lbl.set_line_wrap(True)
link_lbl.set_max_width_chars(60)
area.pack_start(link_lbl, False, False, 4)
spinner = Gtk.Spinner()
spinner.start()
area.pack_start(spinner, False, False, 8)
area.show_all()
link_lbl.set_visible(False)
result = {"success": False, "user": None, "error": None}
def _thread():
try:
fp_id = str(uuid.uuid4())
body = json.dumps({"fingerprintId": fp_id}).encode()
req = urllib.request.Request("https://www.codebuff.com/api/auth/cli/code",
data=body, headers={"Content-Type": "application/json", "User-Agent": "codex-launcher/3.10.7"})
resp = urllib.request.urlopen(req, timeout=30)
rdata = json.loads(resp.read())
login_url = rdata.get("loginUrl", "") or rdata.get("login_url", "")
fp_hash = rdata.get("fingerprintHash", "") or rdata.get("fingerprint_hash", "")
expires_at = rdata.get("expiresAt", 0) or rdata.get("expires_at", 0)
if not login_url:
result["error"] = "No login URL"
GLib.idle_add(_done)
return
GLib.idle_add(lambda: (status_lbl.set_text("Open this URL in your browser:"),
link_lbl.set_markup(f'<a href="{login_url}">{login_url}</a>'),
link_lbl.set_visible(True)))
webbrowser.open(login_url)
poll = f"https://www.codebuff.com/api/auth/cli/status?fingerprintId={urllib.parse.quote(fp_id)}&fingerprintHash={urllib.parse.quote(fp_hash)}&expiresAt={expires_at}"
deadline = time.time() + 300
while time.time() < deadline:
time.sleep(2)
try:
pr = urllib.request.Request(poll, headers={"User-Agent": "codex-launcher/3.10.7"})
pd = json.loads(urllib.request.urlopen(pr, timeout=10).read())
if pd.get("user", {}).get("authToken"):
result["success"] = True
result["user"] = pd["user"]
GLib.idle_add(_done)
return
except Exception:
pass
result["error"] = "Timed out"
except Exception as e:
result["error"] = str(e)[:200]
GLib.idle_add(_done)
def _done():
spinner.stop()
if result["success"] and result["user"]:
u = result["user"]
cp = os.path.expanduser("~/.config/manicode/credentials.json")
os.makedirs(os.path.dirname(cp), exist_ok=True)
creds = {"default": {"id": u.get("id", ""), "name": u.get("name", ""),
"email": u.get("email", ""), "authToken": u.get("authToken", ""),
"fingerprintId": u.get("fingerprintId", ""), "fingerprintHash": u.get("fingerprintHash", "")}}
with open(cp, "w") as f:
json.dump(creds, f, indent=2)
os.chmod(cp, 0o600)
status_lbl.set_text(f"Logged in as {u.get('email', 'OK')}")
link_lbl.set_visible(False)
GLib.timeout_add_seconds(2, dlg.destroy)
else:
status_lbl.set_text(f"Failed: {result.get('error', 'unknown')}")
threading.Thread(target=_thread, daemon=True).start()
dlg.connect("response", lambda d, r: d.destroy())
dlg.run()
def _edit_oauth_secrets(self):
secrets_path = os.path.expanduser("~/.config/codex-launcher/oauth-secrets.json")
try:
with open(secrets_path) as f:
data = json.load(f)
except Exception:
data = {"antigravity": {"client_id": "", "client_secret": ""},
"gemini_cli": {"client_id": "", "client_secret": ""}}
dlg = Gtk.Dialog(title="OAuth Secrets & Credentials", parent=self, modal=True)
dlg.add_button("Cancel", Gtk.ResponseType.CANCEL)
dlg.add_button("Save", Gtk.ResponseType.OK)
dlg.set_default_size(580, 650)
area = dlg.get_content_area()
area.set_margin_start(16)
area.set_margin_end(16)
area.set_margin_top(12)
area.set_margin_bottom(12)
area.set_spacing(6)
sw = Gtk.ScrolledWindow()
sw.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
sw.add(vbox)
area.pack_start(sw, True, True, 0)
vbox.pack_start(Gtk.Label(label="<b>Google OAuth 2.0 Client Credentials</b>\n<small>~/.config/codex-launcher/oauth-secrets.json</small>", use_markup=True, xalign=0), False, False, 4)
google_token_dir = os.path.expanduser("~/.cache/codex-proxy")
fields = {}
for section_key, section_label, oauth_prov, token_file in [
("antigravity", "Antigravity (CloudCode)", "google-antigravity", "google-antigravity-oauth-token.json"),
("gemini_cli", "Gemini CLI", "google-cli", "google-cli-oauth-token.json"),
]:
section_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=2)
hdr_row = Gtk.Box(spacing=6)
hdr_row.pack_start(Gtk.Label(label=f"\n<b>{section_label}</b>", use_markup=True, xalign=0), True, True, 0)
reauth_btn = Gtk.Button(label="Re-OAuth")
reauth_btn.set_size_request(80, -1)
reauth_btn.connect("clicked", lambda b, p=oauth_prov: self._google_reoauth(p))
hdr_row.pack_end(reauth_btn, False, False, 0)
import_btn = Gtk.Button(label="Import JSON")
import_btn.set_size_request(100, -1)
hdr_row.pack_end(import_btn, False, False, 0)
section_box.pack_start(hdr_row, False, False, 2)
token_path = os.path.join(google_token_dir, token_file)
has_token = os.path.exists(token_path)
try:
with open(token_path) as tf:
td = json.load(tf)
has_token = bool(td.get("refresh_token") or td.get("access_token"))
except Exception:
pass
tok_status = "Token: <span foreground='#27ae60' weight='bold'>valid</span>" if has_token else "Token: <span foreground='#e67e22' weight='bold'>missing</span>"
section_box.pack_start(Gtk.Label(label=tok_status, use_markup=True, xalign=0), False, False, 0)
sec = data.get(section_key, {})
for fk, fl in [("client_id", "Client ID"), ("client_secret", "Client Secret")]:
row = Gtk.Box(spacing=6)
lbl = Gtk.Label(label=fl + ":", xalign=0)
lbl.set_size_request(100, -1)
entry = Gtk.Entry()
entry.set_text(sec.get(fk, ""))
entry.set_size_request(360, -1)
if fk == "client_secret":
entry.set_visibility(False)
entry.set_invisible_char("*")
row.pack_start(lbl, False, False, 0)
row.pack_start(entry, True, True, 0)
section_box.pack_start(row, False, False, 2)
fields[(section_key, fk)] = entry
import_btn.connect("clicked", lambda b, sk=section_key: self._import_oauth_json(fields, sk))
vbox.pack_start(section_box, False, False, 0)
vbox.pack_start(Gtk.Label(label="<small>Import client_secret_*.json from Google Cloud Console → Credentials</small>", use_markup=True, xalign=0), False, False, 4)
sep = Gtk.Separator()
vbox.pack_start(sep, False, False, 8)
vbox.pack_start(Gtk.Label(label="\n<b>Freebuff / Codebuff Credentials</b>\n<small>~/.config/manicode/credentials.json</small>", use_markup=True, xalign=0), False, False, 4)
cb_creds_path = os.path.expanduser("~/.config/manicode/credentials.json")
cb_fields = {}
try:
with open(cb_creds_path) as f:
cb_data = json.load(f)
except Exception:
cb_data = {}
cb_default = cb_data.get("default", {})
cb_status_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=4)
cb_info = f"Email: {cb_default.get('email', 'not logged in')}"
cb_name = cb_default.get("name", "")
if cb_name:
cb_info = f"{cb_name} — {cb_info}"
has_cb_token = bool(cb_default.get("authToken", ""))
status_text = "Logged in" if has_cb_token else "Not logged in"
status_color = "#27ae60" if has_cb_token else "#e67e22"
cb_info_lbl = Gtk.Label(label=f"{cb_info}\nStatus: <span foreground=\"{status_color}\" weight=\"bold\">{status_text}</span>", use_markup=True, xalign=0)
cb_status_box.pack_start(cb_info_lbl, False, False, 2)
for fk, fl in [("authToken", "Auth Token"), ("fingerprintId", "Fingerprint ID")]:
row = Gtk.Box(spacing=6)
lbl = Gtk.Label(label=fl + ":", xalign=0)
lbl.set_size_request(110, -1)
entry = Gtk.Entry()
entry.set_text(cb_default.get(fk, ""))
entry.set_size_request(360, -1)
entry.set_visibility(False)
entry.set_invisible_char("*")
row.pack_start(lbl, False, False, 0)
row.pack_start(entry, True, True, 0)
cb_status_box.pack_start(row, False, False, 2)
cb_fields[fk] = entry
cb_btn_row = Gtk.Box(spacing=6)
cb_login_btn = Gtk.Button(label="Re-OAuth (GitHub Login)")
cb_login_btn.connect("clicked", lambda b: self._codebuff_reoauth())
cb_btn_row.pack_start(cb_login_btn, False, False, 0)
cb_status_box.pack_start(cb_btn_row, False, False, 4)
vbox.pack_start(cb_status_box, False, False, 0)
cb_accounts = cb_data.get("accounts", [])
if cb_accounts:
vbox.pack_start(Gtk.Label(label=f"\n<small>Additional accounts: {len(cb_accounts)} (edit credentials.json manually)</small>", use_markup=True, xalign=0), False, False, 2)
vbox.show_all()
sw.show_all()
if dlg.run() == Gtk.ResponseType.OK:
for (sk, fk), entry in fields.items():
if sk not in data:
data[sk] = {}
data[sk][fk] = entry.get_text().strip()
try:
os.makedirs(os.path.dirname(secrets_path), exist_ok=True)
with open(secrets_path, "w") as f:
json.dump(data, f, indent=2)
os.chmod(secrets_path, 0o600)
except Exception as e:
self._show_error_dialog("Save failed", str(e))
cb_updated = dict(cb_default)
for fk, entry in cb_fields.items():
val = entry.get_text().strip()
if val:
cb_updated[fk] = val
if cb_updated:
cb_data["default"] = cb_updated
try:
os.makedirs(os.path.dirname(cb_creds_path), exist_ok=True)
with open(cb_creds_path, "w") as f:
json.dump(cb_data, f, indent=2)
os.chmod(cb_creds_path, 0o600)
except Exception as e:
self._show_error_dialog("Save failed", str(e))
dlg.destroy()
def _import_oauth_json(self, fields, section_key):
chooser = Gtk.FileChooserDialog(
title="Import Google OAuth Client Secret JSON",
parent=self, action=Gtk.FileChooserAction.OPEN)
chooser.add_button("Cancel", Gtk.ResponseType.CANCEL)
chooser.add_button("Open", Gtk.ResponseType.OK)
filt = Gtk.FileFilter()
filt.set_name("JSON files")
filt.add_pattern("*.json")
chooser.add_filter(filt)
if chooser.run() == Gtk.ResponseType.OK:
path = chooser.get_filename()
try:
with open(path) as f:
raw = json.load(f)
creds = raw.get("installed") or raw.get("web") or raw
cid = creds.get("client_id", "")
csec = creds.get("client_secret", "")
if not cid or not csec:
raise ValueError("JSON does not contain client_id and client_secret")
fields[(section_key, "client_id")].set_text(cid)
fields[(section_key, "client_secret")].set_text(csec)
except Exception as e:
self._show_error_dialog("Import failed", str(e))
chooser.destroy()
# ═══════════════════════════════════════════════════════════════════
# Endpoint manager dialog
# ═══════════════════════════════════════════════════════════════════
@@ -3503,38 +3081,6 @@ class EditEndpointDialog(Gtk.Dialog):
add_row(7, "Effort:", self._combo_effort)
self._on_reasoning_toggled()
enhancer_box = Gtk.Box(spacing=6)
self._switch_enhancer = Gtk.Switch()
self._switch_enhancer.set_active(self._data.get("prompt_enhancer", False))
enhancer_box.pack_start(self._switch_enhancer, False, False, 0)
self._enhancer_status_lbl = Gtk.Label()
enhancer_box.pack_start(self._enhancer_status_lbl, False, False, 0)
self._switch_enhancer.connect("notify::active", lambda *a: self._on_enhancer_toggled())
self._combo_enhancer_mode = Gtk.ComboBoxText()
for mode in ["offline", "ai-powered"]:
self._combo_enhancer_mode.append(mode, mode.capitalize())
self._combo_enhancer_mode.set_active_id(self._data.get("prompt_enhancer_mode", "offline"))
enhancer_box.pack_start(self._combo_enhancer_mode, False, False, 6)
add_row(8, "Prompt Enhancer:", enhancer_box)
self._on_enhancer_toggled()
self._entry_enhancer_model = Gtk.Entry()
self._entry_enhancer_model.set_placeholder_text("e.g. deepseek/deepseek-v4-flash (ai-powered mode only)")
self._entry_enhancer_model.set_text(self._data.get("prompt_enhancer_model", ""))
add_row(9, "Enhancer Model:", self._entry_enhancer_model)
self._entry_enhancer_url = Gtk.Entry()
self._entry_enhancer_url.set_placeholder_text("e.g. https://www.codebuff.com/api/v1 (ai-powered mode only)")
self._entry_enhancer_url.set_text(self._data.get("prompt_enhancer_url", ""))
add_row(10, "Enhancer URL:", self._entry_enhancer_url)
self._entry_enhancer_key = Gtk.Entry()
self._entry_enhancer_key.set_placeholder_text("API key for enhancer model (ai-powered mode only)")
self._entry_enhancer_key.set_text(self._data.get("prompt_enhancer_key", ""))
self._entry_enhancer_key.set_visibility(False)
self._entry_enhancer_key.set_invisible_char("*")
add_row(11, "Enhancer Key:", self._entry_enhancer_key)
# Models
mlbl = Gtk.Label(label="Models:", xalign=0)
area.pack_start(mlbl, False, False, 4)
@@ -3576,18 +3122,6 @@ class EditEndpointDialog(Gtk.Dialog):
sw.add(self._model_tree)
self._model_tree.connect("row-activated", lambda t, p, c: self._remove_model(p))
model_btn_box = Gtk.Box(spacing=6)
area.pack_start(model_btn_box, False, False, 0)
self._remove_model_btn = Gtk.Button(label="Remove Selected")
self._remove_model_btn.connect("clicked", lambda b: self._remove_selected_model())
model_btn_box.pack_start(self._remove_model_btn, False, False, 0)
self._clear_models_btn = Gtk.Button(label="Clear All")
self._clear_models_btn.connect("clicked", lambda b: self._clear_all_models())
model_btn_box.pack_start(self._clear_models_btn, False, False, 0)
self._sync_preset_btn = Gtk.Button(label="Sync from Preset")
self._sync_preset_btn.connect("clicked", lambda b: self._apply_selected_preset())
model_btn_box.pack_start(self._sync_preset_btn, False, False, 0)
for m in self._data.get("models", []):
self._model_store.append([m])
@@ -3657,12 +3191,10 @@ class EditEndpointDialog(Gtk.Dialog):
cc_ver = preset.get("cc_version", "")
if cc_ver and not self._entry_cc_ver.get_text().strip():
self._entry_cc_ver.set_text(cc_ver)
if preset.get("models") and (not initial or len(self._model_store) == 0):
current = self._combo_default.get_active_text()
self._model_store.clear()
for mid in preset["models"]:
self._model_store.append([mid])
self._refresh_default_combo(current or preset["models"][0])
if preset.get("models") and len(self._model_store) == 0:
for mid in preset["models"]:
self._model_store.append([mid])
self._refresh_default_combo(preset["models"][0])
if initial and self._data.get("models"):
self._refresh_default_combo(self._data.get("default_model", ""))
@@ -3674,13 +3206,6 @@ class EditEndpointDialog(Gtk.Dialog):
else:
self._lbl_reasoning.set_markup('<span foreground="#e67e22" weight="bold">OFF</span>')
def _on_enhancer_toggled(self, *_):
active = self._switch_enhancer.get_active()
if active:
self._enhancer_status_lbl.set_markup('<span foreground="#27ae60" weight="bold">ON</span>')
else:
self._enhancer_status_lbl.set_markup('<span foreground="#888888" weight="bold">OFF</span>')
def _do_oauth_login(self):
preset_name = self._combo_preset.get_active_text() or "Custom"
preset = PROVIDER_PRESETS.get(preset_name, {})
@@ -3694,17 +3219,9 @@ class EditEndpointDialog(Gtk.Dialog):
is_antigravity = oauth_provider == "google-antigravity"
token_path = os.path.expanduser("~/.cache/codex-proxy/google-antigravity-oauth-token.json" if is_antigravity else "~/.cache/codex-proxy/google-cli-oauth-token.json")
_oauth_secrets_path = os.path.expanduser("~/.config/codex-launcher/oauth-secrets.json")
try:
with open(_oauth_secrets_path) as _f:
_oauth_secrets = json.load(_f)
except Exception:
_oauth_secrets = {}
if is_antigravity:
_sec = _oauth_secrets.get("antigravity", {})
CLIENT_ID = _sec.get("client_id", "")
CLIENT_SECRET = _sec.get("client_secret", "")
CLIENT_ID = "1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com"
CLIENT_SECRET = "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"
SCOPES = [
"https://www.googleapis.com/auth/cloud-platform",
"https://www.googleapis.com/auth/userinfo.email",
@@ -3717,9 +3234,8 @@ class EditEndpointDialog(Gtk.Dialog):
callback_path = "/oauth-callback"
provider_kind = "antigravity"
else:
_sec = _oauth_secrets.get("gemini_cli", {})
CLIENT_ID = _sec.get("client_id", "")
CLIENT_SECRET = _sec.get("client_secret", "")
CLIENT_ID = "681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com"
CLIENT_SECRET = "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"
SCOPES = [
"https://www.googleapis.com/auth/cloud-platform",
"https://www.googleapis.com/auth/userinfo.email",
@@ -4007,10 +3523,10 @@ class EditEndpointDialog(Gtk.Dialog):
def _codebuff_auth_thread():
try:
fingerprint_id = str(uuid.uuid4())
auth_url = "https://www.codebuff.com/api/auth/cli/code"
auth_url = "https://codebuff.com/api/auth/cli/code"
body = json.dumps({"fingerprintId": fingerprint_id}).encode()
req = urllib.request.Request(auth_url, data=body,
headers={"Content-Type": "application/json", "User-Agent": "codex-launcher/3.10.7"})
headers={"Content-Type": "application/json", "User-Agent": "codex-launcher/3.9.7"})
resp = urllib.request.urlopen(req, timeout=30)
data = json.loads(resp.read())
login_url = data.get("loginUrl", "") or data.get("login_url", "")
@@ -4029,13 +3545,13 @@ class EditEndpointDialog(Gtk.Dialog):
webbrowser.open(login_url)
poll_url = f"https://www.codebuff.com/api/auth/cli/status?fingerprintId={urllib.parse.quote(fingerprint_id)}&fingerprintHash={urllib.parse.quote(fingerprint_hash)}&expiresAt={expires_at}"
poll_url = f"https://codebuff.com/api/auth/cli/status?fingerprintId={urllib.parse.quote(fingerprint_id)}&fingerprintHash={urllib.parse.quote(fingerprint_hash)}&expiresAt={expires_at}"
deadline = time.time() + 300
while time.time() < deadline:
time.sleep(2)
try:
poll_req = urllib.request.Request(poll_url,
headers={"User-Agent": "codex-launcher/3.10.7"})
headers={"User-Agent": "codex-launcher/3.9.7"})
poll_resp = urllib.request.urlopen(poll_req, timeout=10)
poll_data = json.loads(poll_resp.read())
user = poll_data.get("user")
@@ -4100,21 +3616,6 @@ class EditEndpointDialog(Gtk.Dialog):
self._model_store.remove(self._model_store.get_iter(path))
self._refresh_default_combo(current)
def _remove_selected_model(self):
sel = self._model_tree.get_selection()
model, paths = sel.get_selected_rows()
if not paths:
return
current = self._combo_default.get_active_text()
for p in reversed(paths):
self._model_store.remove(self._model_store.get_iter(p))
self._refresh_default_combo(current)
def _clear_all_models(self):
current = self._combo_default.get_active_text()
self._model_store.clear()
self._refresh_default_combo(current)
def _refresh_default_combo(self, active=None):
if active is None:
active = self._combo_default.get_active_text()
@@ -4234,17 +3735,6 @@ class EditEndpointDialog(Gtk.Dialog):
new_ep["cc_version"] = cc_ver
new_ep["reasoning_enabled"] = self._switch_reasoning.get_active()
new_ep["reasoning_effort"] = self._combo_effort.get_active_id() or "medium"
new_ep["prompt_enhancer"] = self._switch_enhancer.get_active()
new_ep["prompt_enhancer_mode"] = self._combo_enhancer_mode.get_active_id() or "offline"
enh_model = self._entry_enhancer_model.get_text().strip()
enh_url = self._entry_enhancer_url.get_text().strip()
enh_key = self._entry_enhancer_key.get_text().strip()
if enh_model:
new_ep["prompt_enhancer_model"] = enh_model
if enh_url:
new_ep["prompt_enhancer_url"] = enh_url
if enh_key:
new_ep["prompt_enhancer_key"] = enh_key
preset_name = self._combo_preset.get_active_text() or "Custom"
preset = PROVIDER_PRESETS.get(preset_name, {})
if preset.get("oauth_provider"):

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff