Compare commits
132 Commits
ca604f2dff
...
v3.11.10
4
.gitignore
vendored
4
.gitignore
vendored
@@ -11,3 +11,7 @@ config.toml
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
DEBIAN/
|
DEBIAN/
|
||||||
usr/
|
usr/
|
||||||
|
oauth-secrets.json
|
||||||
|
secrets/
|
||||||
|
*.secret
|
||||||
|
.env
|
||||||
|
|||||||
311
CHANGELOG.md
311
CHANGELOG.md
@@ -1,5 +1,316 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## v3.11.10 (2026-05-26)
|
||||||
|
|
||||||
|
**Antigravity Fix: Interleave function_call/output Pairs, Gemini Turn Trimming (PR #11)**
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
- **Fix Antigravity function_call/output ordering**: Tool calls and their responses are now properly interleaved in sequence (`function_call` → `function_call_output` → `function_call` → ...) instead of being grouped separately
|
||||||
|
- **Gemini sanitizer trimming**: Leading/trailing non-user turns removed for Google API compliance (Google requires conversation to start and end with user turn)
|
||||||
|
- **Stricter role boundary enforcement**: `functionCall` (model) and `functionResponse` (user) never merged across role boundaries
|
||||||
|
- **Merge PR #11**: Fix by qwen-chat coder
|
||||||
|
|
||||||
|
## v3.11.9 (2026-05-26)
|
||||||
|
|
||||||
|
## v3.11.9 (2026-05-26)
|
||||||
|
|
||||||
|
**Antigravity Fix: Preserve functionCall/functionResponse in Gemini Sanitizer (PR #10)**
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- **Fix Antigravity multi-turn tool use**: The Gemini message sanitizer was incorrectly merging/dropping `functionCall` and `functionResponse` turns, causing Antigravity to think forever without responding. These turns are now always preserved as separate messages.
|
||||||
|
- **Merge PR #10**: `fix: preserve functionCall/functionResponse in Gemini sanitizer` (qwen-chat coder)
|
||||||
|
|
||||||
|
## v3.11.8 (2026-05-26)
|
||||||
|
|
||||||
|
## v3.11.8 (2026-05-26)
|
||||||
|
|
||||||
|
**Vision Cache Persistence, PR #8 Merge**
|
||||||
|
|
||||||
|
### New Features
|
||||||
|
|
||||||
|
- **Vision description cache persisted across requests**: Image descriptions from the vision fallback API are now cached in a file (`~/.cache/codex-proxy/vision-cache.json`) so the same image URL is never described twice — saves API calls and latency
|
||||||
|
- **Merge PR #8**: `fix: persist vision description cache across requests` (cobra91)
|
||||||
|
|
||||||
|
## v3.11.7 (2026-05-26)
|
||||||
|
|
||||||
|
**Vision Auto-Detect, Proactive Non-Vision Model Detection, Unit Tests, Bug Fixes**
|
||||||
|
|
||||||
|
### New Features
|
||||||
|
|
||||||
|
- **Vision auto-detect fallback**: When no explicit vision fallback is configured, automatically uses the current provider's own vision model (e.g., `0G-Qwen-VL` for OpenAdapter) as the image description API — no separate API key needed
|
||||||
|
- **Proactive non-vision model detection**: Models matching name patterns (`glm`, `deepseek`, `llama`, `qwen` without `vl`, etc.) are detected as non-vision on first request without waiting for an error from the provider
|
||||||
|
- **Vision preprocessing is now the primary image handling solution**: Replaces old `_strip_images_from_input()` (which just removed images with a placeholder). Images are now described via API and sent as rich text descriptions to text-only models
|
||||||
|
- **Merge PR #6**: Vision/OCR preprocessing for text-only models (cobra91)
|
||||||
|
- **Merge PR #7**: 177 unit tests for translate-proxy.py (cobra91)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- **AttributeError fix**: `image_url` field can be a string (bare URL) not always a dict — fixed in both `_preprocess_vision_input()` and old strip function
|
||||||
|
- **Auth os error 2 fix**: GUI shows "Config missing" message instead of raw OSError when `~/.codex/` directory doesn't exist
|
||||||
|
- **Removed duplicate vision functions**: Cleaned up duplicate `_vision_describe_image()`, `_preprocess_vision()`, `_preprocess_vision_input()` from merge
|
||||||
|
|
||||||
|
## v3.11.6 (2026-05-26)
|
||||||
|
|
||||||
|
**Antigravity Loop Breakers, Vision/OCR Preprocessing, has_content Fix, Auth Error Fix**
|
||||||
|
|
||||||
|
### New Features (Antigravity-only, no other providers affected)
|
||||||
|
|
||||||
|
- **Per-session loop tracking**: `_ANTIGRAVITY_LOOP_TRACKER` global dict with `_antigravity_loop_key()` function tracks state per session: `latest_user_hash`, `nudge_injected`, `latest_user_appended`, `tool_calls_for_request`, `repeated_tool`, `force_finalize`, `last_tool`, `last_tool_count`
|
||||||
|
- **Edit-intent nudge injection**: Injected only on the first turn per request, preventing duplicate nudges across retries
|
||||||
|
- **Latest user instruction append**: Appended exactly once per request to prevent redundant instruction stacking
|
||||||
|
- **Loop breaker**: If the same tool + arguments is repeated ≥ 5 times in a session, `force_finalize` is triggered to break the infinite loop
|
||||||
|
- **Detailed `[antigravity-loop]` logging**: All tracking fields logged on every Antigravity request for debugging
|
||||||
|
|
||||||
|
### New Features (All OpenAI-compatible providers)
|
||||||
|
|
||||||
|
- **Vision/OCR preprocessing**: When a provider doesn't support images (detected via error messages like "unknown variant image_url", "does not support image"), the proxy automatically calls a configurable vision fallback API (default: Kilo.ai) to describe images as text, then replaces image blocks with text descriptions before sending to text-only models
|
||||||
|
- **`_vision_describe_image()`**: Calls vision fallback model to describe a single image, with MD5-based caching to avoid re-describing same URL
|
||||||
|
- **`_preprocess_vision()`**: Replaces `image_url`/`input_image` blocks in Chat Completions message format with text descriptions when provider lacks vision support
|
||||||
|
- **`_preprocess_vision_input()`**: Same for Responses API input format — runs BEFORE adapter conversion so images are replaced early
|
||||||
|
- **Vision error retry**: On HTTP 4xx errors containing image-related keywords, automatically retries with images preprocessed instead of failing
|
||||||
|
- **Configurable via env vars**: `VISION_FALLBACK_URL`, `VISION_FALLBACK_MODEL`, `VISION_FALLBACK_KEY`
|
||||||
|
- **ProviderSchema `supports_vision` field**: Auto-detected from error responses and persisted in provider-caps.json
|
||||||
|
|
||||||
|
### Critical Fixes
|
||||||
|
|
||||||
|
- **`has_content` now includes `function_call`** (v3.11.5 fix): `_observe_event` only checked for `"type": "message"` — when models return only tool calls (no text), `has_content` was `False`, causing Codex to loop infinitely and build context until `context_length_exceeded`. Now checks both `"message"` and `"function_call"`.
|
||||||
|
- **`has_message`/`has_tool_call` initialized in all 5 locations**: Previous fix added variables inside `_observe_event` closure but missed 4 other `has_content = False` locations, causing `NameError: name 'has_message' is not defined` crashes.
|
||||||
|
- **Auth config-not-found error handling**: When Codex's `config.toml` is missing or deleted, `codex login status` returns "Error loading configuration: No such file or directory (os error 2)". Now caught specifically (`OSError errno==2`) and returns ("not_configured", "Config missing — launch once to create") with clear GUI guidance.
|
||||||
|
|
||||||
|
### Bug Fixes (GUI)
|
||||||
|
|
||||||
|
- **Active endpoint sync**: GUI auto-removes stale endpoint references on startup
|
||||||
|
|
||||||
|
## v3.11.5 (2026-05-26)
|
||||||
|
|
||||||
|
**Vision Filter, Token-Aware Compaction, Universal Adaptive Compaction, Smart-Continue Text Detection**
|
||||||
|
|
||||||
|
### Critical Fixes
|
||||||
|
|
||||||
|
- **Token-aware compaction for small-context models (FIX)**: `_crof_compact_for_retry()` had an early return at `len(input_data) <= limit` (item count) — if you had 25 items × 1600 tokens = 40K tokens, it skipped compaction entirely because 25 < 30 (the default item limit). Now also checks estimated token count vs learned model max, and compacts when either item count OR token count exceeds limits. Fixes repeated `context_length_exceeded` errors on models like 0G-GLM-5.1 (~35K token context).
|
||||||
|
- **Proactive compaction now token-aware**: Previously only triggered when item count > 30. Now also triggers when estimated tokens exceed 80% of the model's learned token limit, even if item count is below the threshold. Prevents the first-request failure pattern on small-context models.
|
||||||
|
- **Compaction aggression threshold**: Changed `est > max_tok` to `est >= max_tok * 0.9` to avoid edge case where estimated tokens exactly equal the limit and compaction is skipped.
|
||||||
|
- **Removed all `crof.ai` gates from adaptive compaction**: Proactive compaction, `finish_reason=length` retry, `_crof_record`, and compaction logging were gated behind `"crof.ai" in TARGET_URL`. These gates prevented OpenAdapter and other providers from getting proactive/retry compaction, causing repeated `context_length_exceeded` failures. Now applies universally to ALL providers.
|
||||||
|
|
||||||
|
### New Features
|
||||||
|
|
||||||
|
- **Vision model detection + image stripping**: `_strip_images_from_input()` and `_model_supports_vision()` detect vision capability by model name pattern. Non-vision models (deepseek, glm, mixtral, llama, command, dbrx, qwen, phi-3) have `input_image`/`image_url` parts stripped and replaced with `[User attached image: filename — this model does not support vision]` text notice. Vision models (gpt-4o, gemini, claude, qwen-vl, glm-5v) keep images intact. Applied in 3 paths: main request, context_length_exceeded retry, smart-continue nudge.
|
||||||
|
- **Token estimation and per-model limit learning**: `_estimate_tokens()`, `_estimate_input_tokens()`, `_get_model_max_tokens()`, `_set_model_max_tokens()`. Extracts `~N tokens` from `context_length_exceeded` error messages and stores per-model token limits. Used by proactive compaction and retry compaction to adjust `keep` count dynamically.
|
||||||
|
- **Compaction aggression levels**: `_crof_compact_for_retry()` accepts `aggression` parameter (0=normal, 1=extreme). Extreme mode kicks in when estimated tokens > 1.5× the learned limit or on 2nd+ retry attempt. Reduces `keep` count to minimum, ensuring the compacted request fits within model limits.
|
||||||
|
- **Smart-continue text-tool detection**: Removed hard requirement for `has_function_call_output(input_data)`. Added `_TOOL_CALL_TEXT_PATTERNS` and `_text_looks_like_tool_calls()` to trigger nudging when model outputs text matching tool-call patterns (e.g., `• (exec_command cmd ...)`, `write_to_file`, `exec_command`) even without prior `function_call_output` in context. Essential for models like 0G-GLM-5.1 that never emit real `function_call_output` items.
|
||||||
|
- **Parenthesized tool call regex**: `_PAREN_TC_RE` pattern to match `• (name args...)` format from non-vision models that output tool calls as parenthesized text.
|
||||||
|
|
||||||
|
### GUI Fixes
|
||||||
|
|
||||||
|
- **Active endpoint sync**: Added `set_active_endpoint()` and `validate_active_endpoint()` to Linux GTK GUI. Syncs `.active-endpoint.json` with `config.toml` on every launch; auto-removes stale references to deleted providers. Fixed `"Error loading configuration: No such file or directory (os error 2)"` crash when active endpoint referenced a deleted provider.
|
||||||
|
- **Config state**: `~/.codex/.active-endpoint.json` and `config.toml` model catalog path validated and auto-corrected on GUI startup.
|
||||||
|
|
||||||
|
## v3.11.0 (2026-05-26)
|
||||||
|
|
||||||
|
**Cobra PR Merge + Smart Continuation + API Key Hot-Reload**
|
||||||
|
|
||||||
|
### New Features
|
||||||
|
- **Concurrency semaphore (max 3)**: limits parallel upstream requests to prevent rate-limiting
|
||||||
|
- **Auto-continue for truncated text**: detects text ending in `:`, `(`, `;`, `…` or `finish_reason=length`, continues seamlessly
|
||||||
|
- **SO_REUSEADDR on sticky port**: prevents `TIME_WAIT` from changing port on restart
|
||||||
|
- **proxy-stderr.log**: persistent log file for proxy errors
|
||||||
|
- **Stream diagnostics**: logs event count, finish reason, content flag, elapsed time after each stream
|
||||||
|
- **Timeout/OSError handler**: sends proper `response.failed` SSE event instead of silently dropping connection
|
||||||
|
- **Restart Proxy button**: now only restarts proxy without killing Codex Desktop
|
||||||
|
- **Tool call argument normalizer**: fixes capital-A `Arguments` key, strips markdown/JSON code block wrapping from tool call arguments
|
||||||
|
- **Smart-continue loop (2× retries)**: escalating nudge messages when model returns text-only stop mid-task
|
||||||
|
- **XML tool call extraction**: parses `<tool_call>name{args}</tool_call>` from model text output, injects as real `function_call` items
|
||||||
|
- **Auto-continue + smart-continue ordered with skip guard**: prevents both from double-firing on the same response
|
||||||
|
- **API key hot-reload**: mtime tracking detects config changes, `/admin/reload` endpoint triggers hot-reload, `/admin/verify-key` tests key against upstream
|
||||||
|
- **GUI hot-reload**: auto-refreshes proxy key on endpoint edit, verifies with upstream — no proxy restart needed
|
||||||
|
- **Synthetic tool-results disabled**: was causing deepseek-v4-pro truncation on opencode.ai
|
||||||
|
|
||||||
|
## v3.10.12 (2026-05-26)
|
||||||
|
|
||||||
|
**Sticky Endpoint, Claude Fixes, Guardrail Skip, Anti-Stall**
|
||||||
|
|
||||||
|
### New Features
|
||||||
|
- **Sticky endpoint caching**: remembers which endpoint last succeeded, reuses it on every subsequent request (zero overhead)
|
||||||
|
- **Sequential fallback**: if sticky endpoint fails (429/502/503), tries next endpoint in order — no parallel probing, no wasted requests
|
||||||
|
- **Endpoint order**: `cloudcode-pa.googleapis.com` first (matches agy CLI), `daily-cloudcode-pa.googleapis.com` as fallback
|
||||||
|
- **Anti-stall engine**: kills stale proxy processes and clears `__pycache__` on every new session start
|
||||||
|
- **Smart error classification**: distinguishes `quota_exhausted` vs `capacity_exhausted` vs `account_banned` vs `validation_required` vs `service_disabled` vs `auth_permanent`
|
||||||
|
- **Rate limit reset time parsing**: extracts cooldown from error body (`quotaResetDelay`, `Resets in ~1h27m`, etc.) for accurate cooldown
|
||||||
|
- **Missing Antigravity headers**: `X-Client-Name`, `X-Client-Version`, `x-goog-api-client`, platform-aware `User-Agent`
|
||||||
|
- **Session ID**: added `sessionId` to request wrapper for proper session tracking
|
||||||
|
|
||||||
|
### Bug Fixes (TRAE Agent)
|
||||||
|
- **Guardrail skip for simple messages**: when user sends simple messages (e.g. "hi"), skip injecting `_GEMINI_AGENT_GUARDRAIL` — prevents model from aggressively calling tools and looping `ls -la` 50+ times
|
||||||
|
- **Claude tool preservation**: Claude models through Antigravity now keep ALL tool outputs in normalizer (no summarization/truncation) — prevents context loss that broke Claude sessions
|
||||||
|
- **Claude compaction guard**: `_adaptive_compact` skipped for Claude models — Claude handles its own context, no forced compaction
|
||||||
|
- **Claude normalizer guard**: `_antigravity_normalize_context` skipped for Claude models — avoids stripping Claude-specific message structure
|
||||||
|
- **Claude sanitization guard**: Google content sanitization loop skipped for Claude models — prevents mangling Claude's response format
|
||||||
|
- **Normalizer model parameter**: `_antigravity_normalize_context` now receives `model` param to distinguish Claude vs Gemini behavior
|
||||||
|
|
||||||
|
## v3.10.11 (2026-05-26)
|
||||||
|
|
||||||
|
**Hybrid Endpoint Fallback — Redundant Antigravity Endpoints**
|
||||||
|
|
||||||
|
### New Features
|
||||||
|
- Hybrid endpoint fallback: tries `cloudcode-pa.googleapis.com` then `daily-cloudcode-pa.googleapis.com` on 429
|
||||||
|
- `daily-cloudcode-pa.googleapis.com` is the same production endpoint agy-core uses (separate rate limit bucket)
|
||||||
|
- 429 errors now log full response body for debugging
|
||||||
|
- SERVICE_DISABLED (403) still falls through to next endpoint
|
||||||
|
- Rate-limit marking only happens after ALL endpoints fail
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
- Fixed 429 on one endpoint immediately failing — now tries fallback before giving up
|
||||||
|
- Restored SERVICE_DISABLED fallthrough (was accidentally removed)
|
||||||
|
|
||||||
|
## 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)
|
## v3.10.3 (2026-05-25)
|
||||||
|
|
||||||
**Fix Antigravity 404 Errors — Verified REST Model IDs**
|
**Fix Antigravity 404 Errors — Verified REST Model IDs**
|
||||||
|
|||||||
105
README.md
105
README.md
@@ -9,13 +9,28 @@
|
|||||||
<a href="https://z.ai/subscribe?ic=ROK78RJKNW">z.ai/subscribe</a>
|
<a href="https://z.ai/subscribe?ic=ROK78RJKNW">z.ai/subscribe</a>
|
||||||
</p>
|
</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>
|
<h1 align="center">Codex Launcher — Any AI Provider</h1>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<strong>Run OpenAI Codex CLI & Desktop with <em>any</em> AI provider.</strong><br/>
|
<strong>Run OpenAI Codex CLI & Desktop with <em>any</em> AI provider.</strong><br/>
|
||||||
Google Antigravity • Gemini CLI • OpenCode • Z.AI • Anthropic • Command Code • Codebuff • OpenRouter • Crof.ai • NVIDIA NIM • OpenAdapter • Kilo.ai • DeepSeek • and more
|
Google Antigravity • Gemini CLI • OpenCode • Z.AI • Anthropic • Command Code • Freebuff • OpenRouter • Crof.ai • NVIDIA NIM • OpenAdapter • Kilo.ai • DeepSeek • and more
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<sub>
|
||||||
|
Windows version by <a href="https://github.com/cobra91">cobra91</a> •
|
||||||
|
Original Linux development by <a href="https://github.com/roman-ryzenadvanced">roman-ryzenadvanced</a>
|
||||||
|
</sub>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
@@ -115,6 +130,14 @@ A three-component system:
|
|||||||
- **Response store TTL** — evicts stored responses older than 10 minutes, prevents memory leaks
|
- **Response store TTL** — evicts stored responses older than 10 minutes, prevents memory leaks
|
||||||
- **Bounded stream buffers** — 8MB cap prevents OOM on pathological responses
|
- **Bounded stream buffers** — 8MB cap prevents OOM on pathological responses
|
||||||
- **Dual logging** — all proxy messages written to both stderr and `~/.cache/codex-proxy/proxy.log`
|
- **Dual logging** — all proxy messages written to both stderr and `~/.cache/codex-proxy/proxy.log`
|
||||||
|
- **Vision model detection** (v3.11.5) — automatically strips images for non-vision models (DeepSeek, GLM, Qwen, etc.) and replaces with text notice; vision-capable models (GPT-4o, Gemini, Claude, Qwen-VL) keep images intact
|
||||||
|
- **Token-aware compaction** (v3.11.5) — learns per-model token limits from `context_length_exceeded` errors; proactively compacts when estimated tokens exceed 80% of limit; prevents repeated context overflow on small-context models (~35K tokens)
|
||||||
|
- **Universal adaptive compaction** (v3.11.5) — compaction now works for ALL providers (was Crof.ai-only); proactive + retry compaction with aggression levels (normal/extreme)
|
||||||
|
- **Smart-continue text detection** (v3.11.5) — triggers continuation nudging when model outputs text matching tool-call patterns, essential for text-only models that never emit real `function_call_output` items
|
||||||
|
- **Antigravity loop breakers** (v3.11.6) — per-session tracking with automatic finalization when same tool+args repeats 5+ times; edit-intent nudge injected only on first turn; latest user instruction appended exactly once per request
|
||||||
|
- **has_content function_call fix** (v3.11.6) — tool-call-only responses now correctly flagged as having content, preventing infinite loops on OpenAdapter/Z.AI/OpenRouter providers
|
||||||
|
- **Vision/OCR preprocessing** (v3.11.6) — when provider rejects images, automatically calls a configurable vision fallback API (Kilo.ai) to describe images as text for text-only models; MD5-cached; retries on vision errors with preprocessed text
|
||||||
|
- **Auth config-missing fix** (v3.11.6) — graceful handling when Codex config.toml is missing instead of showing raw os error
|
||||||
- Zero dependencies — pure Python stdlib
|
- Zero dependencies — pure Python stdlib
|
||||||
|
|
||||||
### Command Code Adapter
|
### Command Code Adapter
|
||||||
@@ -539,6 +562,7 @@ The launcher generates model catalog JSON with dual field naming to satisfy both
|
|||||||
|
|
||||||
Codex Launcher includes special handling for Gemini 3 / Antigravity OAuth:
|
Codex Launcher includes special handling for Gemini 3 / Antigravity OAuth:
|
||||||
|
|
||||||
|
- **Sticky endpoint with parallel discovery**: First request probes `cloudcode-pa.googleapis.com` and `daily-cloudcode-pa.googleapis.com` simultaneously — first 200 wins and is cached. All subsequent requests go straight to the cached endpoint. If it fails (429/502/503), cache is cleared and all endpoints are re-probed in parallel. Zero wasted time on rate-limited endpoints.
|
||||||
- **Thought signature preservation**: Captures `thoughtSignature` from Gemini responses
|
- **Thought signature preservation**: Captures `thoughtSignature` from Gemini responses
|
||||||
and reattaches them on follow-up requests to maintain tool-call continuity.
|
and reattaches them on follow-up requests to maintain tool-call continuity.
|
||||||
- **Edit-intent detection**: When follow-up requests contain edit keywords, a tool-use
|
- **Edit-intent detection**: When follow-up requests contain edit keywords, a tool-use
|
||||||
@@ -546,6 +570,16 @@ Codex Launcher includes special handling for Gemini 3 / Antigravity OAuth:
|
|||||||
- **User instruction enforcement**: The latest user message is guaranteed to be the
|
- **User instruction enforcement**: The latest user message is guaranteed to be the
|
||||||
final content turn sent to Gemini, even after compaction.
|
final content turn sent to Gemini, even after compaction.
|
||||||
- **Smart compaction**: Old tool outputs capped at 3000 chars, recent 6 at 20000 chars.
|
- **Smart compaction**: Old tool outputs capped at 3000 chars, recent 6 at 20000 chars.
|
||||||
|
- **Context compaction**: Aggressive auto-trimming when approaching 80% 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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -605,7 +639,7 @@ curl http://127.0.0.1:PORT/v1/accounts
|
|||||||
| OpenCode Zen | OpenAI-compat | `https://opencode.ai/zen/v1` |
|
| OpenCode Zen | OpenAI-compat | `https://opencode.ai/zen/v1` |
|
||||||
| OpenCode Go | OpenAI-compat | `https://opencode.ai/zen/go/v1` |
|
| OpenCode Go | OpenAI-compat | `https://opencode.ai/zen/go/v1` |
|
||||||
| Command Code | Command Code | `https://api.commandcode.ai` |
|
| Command Code | Command Code | `https://api.commandcode.ai` |
|
||||||
| **Codebuff** | **Codebuff** | `https://codebuff.com` *(free DeepSeek/Kimi — OAuth login built-in)* |
|
| **Codebuff / Freebuff** | **Codebuff** | `https://www.codebuff.com` *(free DeepSeek/Kimi — OAuth login built-in)* |
|
||||||
| Crof.ai | OpenAI-compat | `https://crof.ai/v1` |
|
| Crof.ai | OpenAI-compat | `https://crof.ai/v1` |
|
||||||
| OpenAdapter | OpenAI-compat | `https://api.openadapter.in/v1` |
|
| OpenAdapter | OpenAI-compat | `https://api.openadapter.in/v1` |
|
||||||
| Z.ai Coding | OpenAI-compat | `https://api.z.ai/api/coding/paas/v4` |
|
| Z.ai Coding | OpenAI-compat | `https://api.z.ai/api/coding/paas/v4` |
|
||||||
@@ -618,14 +652,14 @@ curl http://127.0.0.1:PORT/v1/accounts
|
|||||||
| Google Antigravity (OAuth) | Antigravity OAuth | `daily-cloudcode-pa.sandbox.googleapis.com` |
|
| Google Antigravity (OAuth) | Antigravity OAuth | `daily-cloudcode-pa.sandbox.googleapis.com` |
|
||||||
| Custom | Any | User-defined |
|
| Custom | Any | User-defined |
|
||||||
|
|
||||||
### Free Models (via Codebuff)
|
### Free Models (via Codebuff/Freebuff)
|
||||||
Codebuff provides free access to these models — no API key needed:
|
Codebuff/Freebuff provides free access to these models — no API key needed:
|
||||||
- **DeepSeek V4 Pro** — Smartest model
|
- **DeepSeek V4 Pro** — Smartest model
|
||||||
- **DeepSeek V4 Flash** — Most efficient
|
- **DeepSeek V4 Flash** — Most efficient
|
||||||
- **Kimi K2.6** — Balanced
|
- **Kimi K2.6** — Balanced
|
||||||
- **MiniMax M2.7** — Fastest
|
- **MiniMax M2.7** — Fastest
|
||||||
|
|
||||||
*Requires: `codebuff login` via GUI OAuth button, or `npm install -g codebuff && codebuff login` (GitHub OAuth)*
|
*Requires: `freebuff login` via GUI OAuth button, or `npm install -g freebuff && freebuff login` (GitHub OAuth)*
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -762,15 +796,70 @@ 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> •
|
||||||
|
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
|
## Requirements
|
||||||
|
|
||||||
- Python ≥ 3.8
|
- Python ≥ 3.8
|
||||||
- python3-gi (`sudo apt install python3-gi`)
|
- python3-gi (`sudo apt install python3-gi`) — Linux only
|
||||||
|
- tkinter (`python3-tk`) — Windows / Linux GUI
|
||||||
- Codex CLI ≥ 2.0
|
- Codex CLI ≥ 2.0
|
||||||
- Codex Desktop (optional, for Desktop mode)
|
- Codex Desktop (optional, for Desktop mode)
|
||||||
- bash, curl, lsof
|
- bash, curl, lsof — Linux only
|
||||||
|
|
||||||
**No pip dependencies.** Zero. Pure stdlib + system GTK.
|
**No pip dependencies.** Zero. Pure stdlib.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
3292
codex-launcher-gui.py
Normal file
3292
codex-launcher-gui.py
Normal file
File diff suppressed because it is too large
Load Diff
Binary file not shown.
BIN
codex-launcher_3.10.10_all.deb
Normal file
BIN
codex-launcher_3.10.10_all.deb
Normal file
Binary file not shown.
BIN
codex-launcher_3.10.11_all.deb
Normal file
BIN
codex-launcher_3.10.11_all.deb
Normal file
Binary file not shown.
BIN
codex-launcher_3.10.12_all.deb
Normal file
BIN
codex-launcher_3.10.12_all.deb
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
codex-launcher_3.10.9_all.deb
Normal file
BIN
codex-launcher_3.10.9_all.deb
Normal file
Binary file not shown.
BIN
codex-launcher_3.11.10_all.deb
Normal file
BIN
codex-launcher_3.11.10_all.deb
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
2132
codex_launcher_lib.py
Normal file
2132
codex_launcher_lib.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -69,8 +69,7 @@ For regular Gemini CLI OAuth, only `cloudcode-pa.googleapis.com` is used.
|
|||||||
|
|
||||||
### 4.1 OAuth Flow
|
### 4.1 OAuth Flow
|
||||||
|
|
||||||
- **Client ID**: `884354919052-36trc1jjb3tguiac32ov6cod268c5blh.apps.googleusercontent.com`
|
- **Client IDs**: Stored locally in `~/.config/codex-launcher/oauth-secrets.json` (not in repo)
|
||||||
(also `1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com`)
|
|
||||||
- **OAuth callback**: `https://antigravity.google/oauth-callback`
|
- **OAuth callback**: `https://antigravity.google/oauth-callback`
|
||||||
- **Token storage**: `~/.cache/codex-proxy/google-antigravity-oauth-token.json`
|
- **Token storage**: `~/.cache/codex-proxy/google-antigravity-oauth-token.json`
|
||||||
- **Token refresh**: via `https://oauth2.googleapis.com/token`
|
- **Token refresh**: via `https://oauth2.googleapis.com/token`
|
||||||
|
|||||||
127
install.ps1
Normal file
127
install.ps1
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Codex Launcher Windows Installer
|
||||||
|
.DESCRIPTION
|
||||||
|
Installs Codex Launcher for the current user.
|
||||||
|
.NOTES
|
||||||
|
Requires: Python 3.8+ (stdlib only, zero pip dependencies).
|
||||||
|
#>
|
||||||
|
|
||||||
|
param(
|
||||||
|
[switch]$Uninstall
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
$BinDir = Join-Path $env:LOCALAPPDATA 'Programs\Codex-Launcher'
|
||||||
|
$StartMenu = Join-Path $env:APPDATA 'Microsoft\Windows\Start Menu\Programs'
|
||||||
|
|
||||||
|
if ($Uninstall) {
|
||||||
|
Write-Host 'Uninstalling Codex Launcher...' -ForegroundColor Yellow
|
||||||
|
|
||||||
|
if (Test-Path $BinDir) {
|
||||||
|
Remove-Item -Recurse -Force $BinDir
|
||||||
|
Write-Host " Removed $BinDir"
|
||||||
|
}
|
||||||
|
|
||||||
|
$shortcut = Join-Path $StartMenu 'Codex Launcher.lnk'
|
||||||
|
if (Test-Path $shortcut) {
|
||||||
|
Remove-Item -Force $shortcut
|
||||||
|
Write-Host ' Removed Start Menu shortcut'
|
||||||
|
}
|
||||||
|
|
||||||
|
$userPath = [Environment]::GetEnvironmentVariable('PATH', 'User')
|
||||||
|
if ($userPath -like "*$BinDir*") {
|
||||||
|
$newPath = ($userPath -split ';' | Where-Object { $_ -ne $BinDir }) -join ';'
|
||||||
|
[Environment]::SetEnvironmentVariable('PATH', $newPath, 'User')
|
||||||
|
Write-Host ' Removed from PATH'
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host 'Uninstall complete.' -ForegroundColor Green
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host ''
|
||||||
|
Write-Host ' Codex Launcher - Windows Installer' -ForegroundColor Cyan
|
||||||
|
Write-Host ' ====================================' -ForegroundColor Cyan
|
||||||
|
Write-Host ''
|
||||||
|
|
||||||
|
# Check Python
|
||||||
|
$pythonExe = Get-Command python -ErrorAction SilentlyContinue
|
||||||
|
if (-not $pythonExe) {
|
||||||
|
$pythonExe = Get-Command python3 -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
|
if (-not $pythonExe) {
|
||||||
|
Write-Host 'ERROR: Python not found. Install Python 3.8+ and add to PATH.' -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
Write-Host " Python: $($pythonExe.Source)" -ForegroundColor Gray
|
||||||
|
|
||||||
|
# Create install directory
|
||||||
|
New-Item -ItemType Directory -Force -Path $BinDir | Out-Null
|
||||||
|
|
||||||
|
# Copy files
|
||||||
|
$srcDir = Join-Path $PSScriptRoot 'src'
|
||||||
|
$files = @(
|
||||||
|
'translate-proxy.py',
|
||||||
|
'codex-launcher-gui.py',
|
||||||
|
'codex_launcher_lib.py',
|
||||||
|
'cleanup-codex-stale.py'
|
||||||
|
)
|
||||||
|
|
||||||
|
foreach ($file in $files) {
|
||||||
|
$src = Join-Path $srcDir $file
|
||||||
|
if (Test-Path $src) {
|
||||||
|
Copy-Item -Force $src $BinDir
|
||||||
|
Write-Host " Installed: $file" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host " WARNING: $file not found in src/" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create Start Menu shortcut
|
||||||
|
$WshShell = New-Object -ComObject WScript.Shell
|
||||||
|
$shortcutPath = Join-Path $StartMenu 'Codex Launcher.lnk'
|
||||||
|
$Shortcut = $WshShell.CreateShortcut($shortcutPath)
|
||||||
|
|
||||||
|
# Find pythonw.exe for no-console launch
|
||||||
|
$pythonw = Get-Command pythonw -ErrorAction SilentlyContinue
|
||||||
|
if (-not $pythonw) {
|
||||||
|
$pythonDir = Split-Path $pythonExe.Source
|
||||||
|
$pythonwCandidate = Join-Path $pythonDir 'pythonw.exe'
|
||||||
|
if (Test-Path $pythonwCandidate) {
|
||||||
|
$pythonw = $pythonwCandidate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($pythonw) {
|
||||||
|
$targetPath = if ($pythonw.Source) { $pythonw.Source } else { $pythonw }
|
||||||
|
} else {
|
||||||
|
$targetPath = $pythonExe.Source
|
||||||
|
}
|
||||||
|
$Shortcut.TargetPath = $targetPath
|
||||||
|
$guiPath = Join-Path $BinDir 'codex-launcher-gui.py'
|
||||||
|
$Shortcut.Arguments = $guiPath
|
||||||
|
$Shortcut.WorkingDirectory = $BinDir
|
||||||
|
$Shortcut.Description = 'Launch Codex Desktop with any AI provider'
|
||||||
|
$Shortcut.Save()
|
||||||
|
Write-Host ' Created Start Menu shortcut' -ForegroundColor Green
|
||||||
|
|
||||||
|
# Add to PATH
|
||||||
|
$userPath = [Environment]::GetEnvironmentVariable('PATH', 'User')
|
||||||
|
if ($userPath -notlike "*$BinDir*") {
|
||||||
|
$newUserPath = $userPath + ';' + $BinDir
|
||||||
|
[Environment]::SetEnvironmentVariable('PATH', $newUserPath, 'User')
|
||||||
|
$env:PATH = $env:PATH + ';' + $BinDir
|
||||||
|
Write-Host ' Added to user PATH' -ForegroundColor Green
|
||||||
|
}
|
||||||
|
|
||||||
|
# Verify
|
||||||
|
Write-Host ''
|
||||||
|
Write-Host ' Installation complete!' -ForegroundColor Cyan
|
||||||
|
Write-Host " Install dir: $BinDir" -ForegroundColor Gray
|
||||||
|
Write-Host ''
|
||||||
|
Write-Host ' Launch options:' -ForegroundColor White
|
||||||
|
Write-Host ' Start Menu: Codex Launcher' -ForegroundColor Gray
|
||||||
|
Write-Host ' Command: codex-launcher-gui.py' -ForegroundColor Gray
|
||||||
|
Write-Host ' Uninstall: powershell -File install.ps1 -Uninstall' -ForegroundColor Gray
|
||||||
|
Write-Host ''
|
||||||
12
install.sh
12
install.sh
@@ -3,11 +3,13 @@ set -e
|
|||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
|
||||||
if [ -f "$SCRIPT_DIR/codex-launcher_3.10.3_all.deb" ]; then
|
if [ -f "$SCRIPT_DIR/codex-launcher_3.11.6_all.deb" ]; then
|
||||||
echo "Installing codex-launcher_3.10.3_all.deb ..."
|
echo "Installing codex-launcher_3.11.6_all.deb ..."
|
||||||
sudo dpkg -i "$SCRIPT_DIR/codex-launcher_3.10.3_all.deb"
|
sudo dpkg -i "$SCRIPT_DIR/codex-launcher_3.11.6_all.deb"
|
||||||
echo ""
|
else
|
||||||
echo "Installed v3.10.3 via .deb package."
|
echo "WARNING: codex-launcher_3.11.6_all.deb not found; copying files manually."
|
||||||
|
fi
|
||||||
|
echo "Installed v3.11.6 via .deb package."
|
||||||
echo " translate-proxy.py -> /usr/bin/translate-proxy.py"
|
echo " translate-proxy.py -> /usr/bin/translate-proxy.py"
|
||||||
echo " codex-launcher-gui -> /usr/bin/codex-launcher-gui"
|
echo " codex-launcher-gui -> /usr/bin/codex-launcher-gui"
|
||||||
echo " cleanup-codex-stale -> /usr/bin/cleanup-codex-stale.sh"
|
echo " cleanup-codex-stale -> /usr/bin/cleanup-codex-stale.sh"
|
||||||
|
|||||||
101
src/cleanup-codex-stale.py
Normal file
101
src/cleanup-codex-stale.py
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
#!/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()
|
||||||
@@ -20,12 +20,64 @@ BGP_POOLS_FILE = HOME / ".codex/bgp-pools.json"
|
|||||||
LOG_DIR = HOME / ".cache/codex-desktop"
|
LOG_DIR = HOME / ".cache/codex-desktop"
|
||||||
LAUNCH_LOG = LOG_DIR / "launcher.log"
|
LAUNCH_LOG = LOG_DIR / "launcher.log"
|
||||||
PROXY_CONFIG_DIR = HOME / ".cache/codex-proxy"
|
PROXY_CONFIG_DIR = HOME / ".cache/codex-proxy"
|
||||||
|
ACTIVE_ENDPOINT_FILE = HOME / ".codex/.active-endpoint.json"
|
||||||
DEFAULT_CONFIG = """model = ""
|
DEFAULT_CONFIG = """model = ""
|
||||||
model_provider = ""
|
model_provider = ""
|
||||||
model_catalog_json = ""
|
model_catalog_json = ""
|
||||||
"""
|
"""
|
||||||
|
|
||||||
CHANGELOG = [
|
CHANGELOG = [
|
||||||
|
("3.11.10", "2026-05-26", [
|
||||||
|
"Fix Antigravity: interleave function_call/output pairs (PR #11)",
|
||||||
|
"Gemini sanitizer: trim non-user turns for Google API compliance",
|
||||||
|
]),
|
||||||
|
("3.11.9", "2026-05-26", [
|
||||||
|
"Fix Antigravity: preserve functionCall/functionResponse (PR #10)",
|
||||||
|
"Prevents tool responses from being dropped in multi-turn sessions",
|
||||||
|
]),
|
||||||
|
("3.11.8", "2026-05-26", [
|
||||||
|
"Vision cache persisted across requests (PR #8 merge)",
|
||||||
|
"No redundant vision API calls for same image URL",
|
||||||
|
]),
|
||||||
|
("3.11.7", "2026-05-26", [
|
||||||
|
"Vision auto-detect: uses provider's vision model for image description",
|
||||||
|
"Vision preprocessing replaces image stripping",
|
||||||
|
"Fix AttributeError in image_url string handling",
|
||||||
|
"Merge PR #6: vision/OCR preprocessing, PR #7: 177 unit tests",
|
||||||
|
"Auth os error 2 fix: proper config-missing message in GUI",
|
||||||
|
]),
|
||||||
|
("3.11.6", "2026-05-26", [
|
||||||
|
"Antigravity loop breakers: per-session tracking, repeated tool detection",
|
||||||
|
"has_content fix: function_call counts as valid output",
|
||||||
|
"Latest user instruction appended once per request for Antigravity",
|
||||||
|
"Antigravity-only changes, no touch to other providers",
|
||||||
|
]),
|
||||||
|
("3.11.5", "2026-05-26", [
|
||||||
|
"Token-aware compaction: fixes context_length_exceeded on small-context models",
|
||||||
|
"Proactive compaction triggers on token count, not just item count",
|
||||||
|
"Universal adaptive compaction for all providers (removed crof.ai gates)",
|
||||||
|
"Vision model detection + image stripping for non-vision models",
|
||||||
|
"Per-model token limit learning from error messages",
|
||||||
|
"Smart-continue text-tool detection for text-only models",
|
||||||
|
"Active endpoint sync: auto-removes stale references on startup",
|
||||||
|
]),
|
||||||
|
("3.11.0", "2026-05-26", [
|
||||||
|
"Merge cobra PR: concurrency semaphore (max 3), auto-continue for truncated text",
|
||||||
|
"SO_REUSEADDR on sticky port, proxy-stderr.log, stream diagnostics logging",
|
||||||
|
"Timeout/OSError handler sends response.failed SSE instead of silent drop",
|
||||||
|
"Restart Proxy button: only restarts proxy without killing Codex Desktop",
|
||||||
|
"Tool call argument normalizer: fixes Arguments→arguments, strips markdown wrapping",
|
||||||
|
"Smart-continue loop (2× retries): escalating nudges when model stops text-only mid-task",
|
||||||
|
"XML tool call extraction: parses <name> patterns from text, injects as real calls",
|
||||||
|
"Auto-continue + smart-continue ordered with skip guard to avoid double-firing",
|
||||||
|
"API key hot-reload with mtime tracking + /admin/reload + /admin/verify-key endpoints",
|
||||||
|
"GUI hot-reload: auto-refreshes proxy key on endpoint edit, verifies with upstream",
|
||||||
|
"Synthetic tool-results disabled: was causing deepseek-v4-pro truncation on opencode.ai",
|
||||||
|
]),
|
||||||
|
("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", [
|
("3.10.3", "2026-05-25", [
|
||||||
"Fix Antigravity 404: map display names to verified REST API model IDs",
|
"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)",
|
"REST API uses slugs (gemini-3-flash) not display names (Gemini 3.5 Flash)",
|
||||||
@@ -357,7 +409,7 @@ PROVIDER_PRESETS = {
|
|||||||
},
|
},
|
||||||
"Google Antigravity (OAuth)": {
|
"Google Antigravity (OAuth)": {
|
||||||
"backend_type": "gemini-oauth-antigravity",
|
"backend_type": "gemini-oauth-antigravity",
|
||||||
"base_url": "https://daily-cloudcode-pa.sandbox.googleapis.com",
|
"base_url": "https://cloudcode-pa.googleapis.com",
|
||||||
"oauth_provider": "google-antigravity",
|
"oauth_provider": "google-antigravity",
|
||||||
"models": [
|
"models": [
|
||||||
"Gemini 3.5 Flash (High)", "Gemini 3.5 Flash (Medium)", "Gemini 3.5 Flash (Low)",
|
"Gemini 3.5 Flash (High)", "Gemini 3.5 Flash (Medium)", "Gemini 3.5 Flash (Low)",
|
||||||
@@ -389,7 +441,25 @@ PROVIDER_PRESETS = {
|
|||||||
},
|
},
|
||||||
"Codebuff (Free DeepSeek/Kimi)": {
|
"Codebuff (Free DeepSeek/Kimi)": {
|
||||||
"backend_type": "codebuff",
|
"backend_type": "codebuff",
|
||||||
"base_url": "https://codebuff.com",
|
"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",
|
||||||
"oauth_provider": "codebuff",
|
"oauth_provider": "codebuff",
|
||||||
"models": [
|
"models": [
|
||||||
"deepseek/deepseek-v4-pro", "deepseek/deepseek-v4-flash",
|
"deepseek/deepseek-v4-pro", "deepseek/deepseek-v4-flash",
|
||||||
@@ -888,6 +958,27 @@ def restore_config():
|
|||||||
shutil.copy2(str(CONFIG_BAK), str(tmp))
|
shutil.copy2(str(CONFIG_BAK), str(tmp))
|
||||||
os.replace(str(tmp), str(CONFIG))
|
os.replace(str(tmp), str(CONFIG))
|
||||||
|
|
||||||
|
def set_active_endpoint(name):
|
||||||
|
ACTIVE_ENDPOINT_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
write_secure_text(ACTIVE_ENDPOINT_FILE, json.dumps({"active": name}, indent=2))
|
||||||
|
|
||||||
|
def validate_active_endpoint(logfn=None):
|
||||||
|
if not ACTIVE_ENDPOINT_FILE.exists():
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
d = json.loads(ACTIVE_ENDPOINT_FILE.read_text())
|
||||||
|
active = d.get("active", "")
|
||||||
|
if not active:
|
||||||
|
return
|
||||||
|
eps = load_endpoints()
|
||||||
|
names = {ep.get("name", "") for ep in eps}
|
||||||
|
if active not in names:
|
||||||
|
ACTIVE_ENDPOINT_FILE.unlink()
|
||||||
|
if logfn:
|
||||||
|
logfn(f"Removed stale active-endpoint '{active}' (provider no longer exists)")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
def write_secure_text(path, text):
|
def write_secure_text(path, text):
|
||||||
path.parent.mkdir(parents=True, exist_ok=True)
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
tmp = path.with_suffix(path.suffix + ".tmp")
|
tmp = path.with_suffix(path.suffix + ".tmp")
|
||||||
@@ -1231,6 +1322,9 @@ def _check_codex_auth():
|
|||||||
if out.returncode == 0 and text:
|
if out.returncode == 0 and text:
|
||||||
return ("logged_in", text)
|
return ("logged_in", text)
|
||||||
if text:
|
if text:
|
||||||
|
_tl = text.lower()
|
||||||
|
if "no such file" in _tl or "os error 2" in _tl or "not found" in _tl:
|
||||||
|
return ("not_configured", "Config missing — launch once to create")
|
||||||
return ("error", text)
|
return ("error", text)
|
||||||
return ("unknown", "No output from codex login status")
|
return ("unknown", "No output from codex login status")
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
@@ -1760,6 +1854,64 @@ class AIMonitoringWindow(Gtk.Window):
|
|||||||
# Main window
|
# Main window
|
||||||
# ═══════════════════════════════════════════════════════════════════
|
# ═══════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
def _oauth_discover_project(access_token, token_path, tokens):
|
||||||
|
project_id = ""
|
||||||
|
try:
|
||||||
|
lr = urllib.request.Request(
|
||||||
|
"https://cloudcode-pa.googleapis.com/v1internal:loadCodeAssist",
|
||||||
|
data=json.dumps({}).encode(),
|
||||||
|
headers={"Content-Type": "application/json",
|
||||||
|
"Authorization": f"Bearer {access_token}",
|
||||||
|
"User-Agent": "google-api-nodejs-client/9.15.1"})
|
||||||
|
lresp = urllib.request.urlopen(lr, timeout=15)
|
||||||
|
ldata = json.loads(lresp.read())
|
||||||
|
p = ldata.get("cloudaicompanionProject", "")
|
||||||
|
if isinstance(p, dict):
|
||||||
|
project_id = p.get("id", "")
|
||||||
|
elif isinstance(p, str):
|
||||||
|
project_id = p
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
if not project_id:
|
||||||
|
return ""
|
||||||
|
try:
|
||||||
|
test_url = f"https://cloudcode-pa.googleapis.com/v1internal:listModels?project={project_id}"
|
||||||
|
test_req = urllib.request.Request(test_url,
|
||||||
|
headers={"Authorization": f"Bearer {access_token}",
|
||||||
|
"User-Agent": "google-api-nodejs-client/9.15.1"})
|
||||||
|
urllib.request.urlopen(test_req, timeout=10)
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
if e.code == 403 and "SERVICE_DISABLED" in (e.read().decode()[:500]):
|
||||||
|
print(f"[oauth] project {project_id} has API disabled, searching for valid project...", file=sys.stderr)
|
||||||
|
try:
|
||||||
|
list_req = urllib.request.Request(
|
||||||
|
"https://cloudresourcemanager.googleapis.com/v1/projects?filter=lifecycleState:ACTIVE",
|
||||||
|
headers={"Authorization": f"Bearer {access_token}"})
|
||||||
|
list_resp = urllib.request.urlopen(list_req, timeout=15)
|
||||||
|
projects = json.loads(list_resp.read()).get("projects", [])
|
||||||
|
for proj in projects:
|
||||||
|
pid = proj.get("projectId", "")
|
||||||
|
if not pid or pid == project_id:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
t2 = urllib.request.Request(
|
||||||
|
f"https://cloudcode-pa.googleapis.com/v1internal:listModels?project={pid}",
|
||||||
|
headers={"Authorization": f"Bearer {access_token}",
|
||||||
|
"User-Agent": "google-api-nodejs-client/9.15.1"})
|
||||||
|
urllib.request.urlopen(t2, timeout=10)
|
||||||
|
project_id = pid
|
||||||
|
print(f"[oauth] found working project: {pid}", file=sys.stderr)
|
||||||
|
break
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
tokens["project_id"] = project_id
|
||||||
|
with open(token_path, "w") as f:
|
||||||
|
json.dump(tokens, f, indent=2)
|
||||||
|
os.chmod(token_path, 0o600)
|
||||||
|
return project_id
|
||||||
|
|
||||||
class LauncherWin(Gtk.Window):
|
class LauncherWin(Gtk.Window):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__(title="Codex Launcher")
|
super().__init__(title="Codex Launcher")
|
||||||
@@ -1769,6 +1921,7 @@ class LauncherWin(Gtk.Window):
|
|||||||
self._proc = None
|
self._proc = None
|
||||||
self._endpoints_data = load_endpoints()
|
self._endpoints_data = load_endpoints()
|
||||||
recover_config_if_needed()
|
recover_config_if_needed()
|
||||||
|
validate_active_endpoint()
|
||||||
|
|
||||||
vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8)
|
vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8)
|
||||||
self.add(vbox)
|
self.add(vbox)
|
||||||
@@ -1776,7 +1929,7 @@ class LauncherWin(Gtk.Window):
|
|||||||
# header row
|
# header row
|
||||||
hdr = Gtk.Box(spacing=8)
|
hdr = Gtk.Box(spacing=8)
|
||||||
vbox.pack_start(hdr, False, False, 0)
|
vbox.pack_start(hdr, False, False, 0)
|
||||||
lbl = Gtk.Label(label="<b>Codex Launcher v3.10.3</b>")
|
lbl = Gtk.Label(label=f"<b>Codex Launcher v{CHANGELOG[0][0]}</b>")
|
||||||
lbl.set_use_markup(True)
|
lbl.set_use_markup(True)
|
||||||
hdr.pack_start(lbl, False, False, 0)
|
hdr.pack_start(lbl, False, False, 0)
|
||||||
changelog_btn = Gtk.Button(label="Changelog")
|
changelog_btn = Gtk.Button(label="Changelog")
|
||||||
@@ -1800,6 +1953,9 @@ class LauncherWin(Gtk.Window):
|
|||||||
mgr_btn = Gtk.Button(label="Manage Endpoints")
|
mgr_btn = Gtk.Button(label="Manage Endpoints")
|
||||||
mgr_btn.connect("clicked", lambda b: self._open_mgr())
|
mgr_btn.connect("clicked", lambda b: self._open_mgr())
|
||||||
hdr.pack_end(mgr_btn, False, False, 0)
|
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
|
# verification status bar
|
||||||
self._cli_info = _detect_codex_cli()
|
self._cli_info = _detect_codex_cli()
|
||||||
@@ -1952,6 +2108,13 @@ class LauncherWin(Gtk.Window):
|
|||||||
assist_btn.connect("clicked", lambda b: self._open_assistant())
|
assist_btn.connect("clicked", lambda b: self._open_assistant())
|
||||||
assist_btn.set_tooltip_text("Open AI coding assistant with streaming, tools, and session management")
|
assist_btn.set_tooltip_text("Open AI coding assistant with streaming, tools, and session management")
|
||||||
bb.pack_start(assist_btn, False, False, 0)
|
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 = Gtk.Button(label="Kill && Cleanup")
|
||||||
self._kill_btn.connect("clicked", lambda b: self._kill())
|
self._kill_btn.connect("clicked", lambda b: self._kill())
|
||||||
self._kill_btn.set_sensitive(False)
|
self._kill_btn.set_sensitive(False)
|
||||||
@@ -2005,6 +2168,8 @@ class LauncherWin(Gtk.Window):
|
|||||||
self._relogin_btn.set_sensitive("cli" not in self._missing)
|
self._relogin_btn.set_sensitive("cli" not in self._missing)
|
||||||
elif status == "not_installed":
|
elif status == "not_installed":
|
||||||
self._auth_label.set_markup("<span foreground='#888'>Auth: N/A (CLI not installed)</span>")
|
self._auth_label.set_markup("<span foreground='#888'>Auth: N/A (CLI not installed)</span>")
|
||||||
|
elif status == "not_configured":
|
||||||
|
self._auth_label.set_markup("<span foreground='#d29922'>⚠ Config missing — launch once to create</span>")
|
||||||
else:
|
else:
|
||||||
self._auth_label.set_markup(f"<span foreground='#d29922'>⚠ Auth: {msg}</span>")
|
self._auth_label.set_markup(f"<span foreground='#d29922'>⚠ Auth: {msg}</span>")
|
||||||
self._relogin_btn.set_sensitive("cli" not in self._missing)
|
self._relogin_btn.set_sensitive("cli" not in self._missing)
|
||||||
@@ -2048,6 +2213,7 @@ class LauncherWin(Gtk.Window):
|
|||||||
self._btn_codex_desktop.set_sensitive(not busy and has_desk)
|
self._btn_codex_desktop.set_sensitive(not busy and has_desk)
|
||||||
self._btn_codex_cli.set_sensitive(not busy and has_cli)
|
self._btn_codex_cli.set_sensitive(not busy and has_cli)
|
||||||
self._kill_btn.set_sensitive(busy)
|
self._kill_btn.set_sensitive(busy)
|
||||||
|
self._restart_btn.set_sensitive(busy)
|
||||||
GLib.idle_add(_update)
|
GLib.idle_add(_update)
|
||||||
|
|
||||||
def _rebuild_combo(self):
|
def _rebuild_combo(self):
|
||||||
@@ -2192,6 +2358,22 @@ class LauncherWin(Gtk.Window):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log(f"[AI Monitor] Proxy restart failed: {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}")
|
||||||
|
|
||||||
def _open_usage(self):
|
def _open_usage(self):
|
||||||
try:
|
try:
|
||||||
self._usage_window = UsageWindow(self)
|
self._usage_window = UsageWindow(self)
|
||||||
@@ -2487,6 +2669,8 @@ class LauncherWin(Gtk.Window):
|
|||||||
begin_config_transaction(f"launch:{ep['name']}")
|
begin_config_transaction(f"launch:{ep['name']}")
|
||||||
write_config_for_native(ep, model)
|
write_config_for_native(ep, model)
|
||||||
|
|
||||||
|
set_active_endpoint(ep["name"])
|
||||||
|
|
||||||
if target == "desktop":
|
if target == "desktop":
|
||||||
if needs_proxy:
|
if needs_proxy:
|
||||||
_kill_existing_desktop(self.log)
|
_kill_existing_desktop(self.log)
|
||||||
@@ -2544,6 +2728,7 @@ class LauncherWin(Gtk.Window):
|
|||||||
|
|
||||||
begin_config_transaction(f"launch:bgp:{pool['name']}")
|
begin_config_transaction(f"launch:bgp:{pool['name']}")
|
||||||
write_config_for_translated(bgp_ep, model, port)
|
write_config_for_translated(bgp_ep, model, port)
|
||||||
|
set_active_endpoint(pool["name"])
|
||||||
|
|
||||||
if target == "desktop":
|
if target == "desktop":
|
||||||
_kill_existing_desktop(self.log)
|
_kill_existing_desktop(self.log)
|
||||||
@@ -2783,6 +2968,439 @@ class LauncherWin(Gtk.Window):
|
|||||||
_stop_proxy()
|
_stop_proxy()
|
||||||
Gtk.main_quit()
|
Gtk.main_quit()
|
||||||
|
|
||||||
|
def _google_reoauth(self, provider, parent_dlg=None):
|
||||||
|
import http.server
|
||||||
|
is_antigravity = provider == "google-antigravity"
|
||||||
|
sec_key = "antigravity" if is_antigravity else "gemini_cli"
|
||||||
|
_sp = os.path.expanduser("~/.config/codex-launcher/oauth-secrets.json")
|
||||||
|
try:
|
||||||
|
with open(_sp) as _f:
|
||||||
|
_secrets_data = json.load(_f)
|
||||||
|
except Exception:
|
||||||
|
_secrets_data = {}
|
||||||
|
sec = _secrets_data.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}")
|
||||||
|
provider_kind = "antigravity" if is_antigravity else "cli"
|
||||||
|
|
||||||
|
if is_antigravity:
|
||||||
|
SCOPES = [
|
||||||
|
"https://www.googleapis.com/auth/cloud-platform",
|
||||||
|
"https://www.googleapis.com/auth/userinfo.email",
|
||||||
|
"https://www.googleapis.com/auth/userinfo.profile",
|
||||||
|
"https://www.googleapis.com/auth/cclog",
|
||||||
|
"https://www.googleapis.com/auth/experimentsandconfigs",
|
||||||
|
]
|
||||||
|
port = 51121
|
||||||
|
redirect_uri = f"http://localhost:{port}/oauth-callback"
|
||||||
|
callback_path = "/oauth-callback"
|
||||||
|
else:
|
||||||
|
SCOPES = [
|
||||||
|
"https://www.googleapis.com/auth/cloud-platform",
|
||||||
|
"https://www.googleapis.com/auth/userinfo.email",
|
||||||
|
"https://www.googleapis.com/auth/userinfo.profile",
|
||||||
|
]
|
||||||
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
||||||
|
s.bind(("127.0.0.1", 0))
|
||||||
|
port = s.getsockname()[1]
|
||||||
|
redirect_uri = f"http://127.0.0.1:{port}/oauth2callback"
|
||||||
|
callback_path = "/oauth2callback"
|
||||||
|
|
||||||
|
state = secrets.token_hex(32)
|
||||||
|
verifier = secrets.token_urlsafe(64)
|
||||||
|
challenge = base64.urlsafe_b64encode(hashlib.sha256(verifier.encode()).digest()).rstrip(b"=").decode()
|
||||||
|
|
||||||
|
scope_str = " ".join(SCOPES)
|
||||||
|
auth_url = (
|
||||||
|
f"https://accounts.google.com/o/oauth2/v2/auth?"
|
||||||
|
f"client_id={CLIENT_ID}"
|
||||||
|
f"&redirect_uri={urllib.parse.quote(redirect_uri)}"
|
||||||
|
f"&response_type=code"
|
||||||
|
f"&scope={urllib.parse.quote(scope_str)}"
|
||||||
|
f"&access_type=offline"
|
||||||
|
f"&prompt=select_account%20consent"
|
||||||
|
f"&state={state}"
|
||||||
|
f"&code_challenge={challenge}"
|
||||||
|
f"&code_challenge_method=S256"
|
||||||
|
)
|
||||||
|
|
||||||
|
oauth_dlg = Gtk.Dialog(title=f"Re-OAuth: {'Antigravity' if is_antigravity else 'Gemini CLI'}", parent=parent_dlg or self, modal=True)
|
||||||
|
oauth_dlg.add_button("Cancel", Gtk.ResponseType.CANCEL)
|
||||||
|
oauth_dlg.set_default_size(520, 200)
|
||||||
|
ca = oauth_dlg.get_content_area()
|
||||||
|
ca.set_margin_start(12)
|
||||||
|
ca.set_margin_end(12)
|
||||||
|
ca.set_spacing(6)
|
||||||
|
ca.pack_start(Gtk.Label(label=f"<b>Re-authenticating {'Antigravity' if is_antigravity else 'Gemini CLI'}</b>", use_markup=True, xalign=0), False, False, 0)
|
||||||
|
link_lbl = Gtk.Label(label="Click here to open Google authorization", use_markup=True, xalign=0)
|
||||||
|
link_lbl.set_markup(f'<a href="{auth_url}">Click here to open Google authorization</a>')
|
||||||
|
ca.pack_start(link_lbl, False, False, 4)
|
||||||
|
status_lbl = Gtk.Label(label="Waiting for browser callback...", xalign=0)
|
||||||
|
ca.pack_start(status_lbl, False, False, 4)
|
||||||
|
ca.show_all()
|
||||||
|
|
||||||
|
code_holder = [None]
|
||||||
|
error_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:
|
||||||
|
if params.get("state", [None])[0] != state:
|
||||||
|
self2.send_response(400)
|
||||||
|
self2.end_headers()
|
||||||
|
self2.wfile.write(b"CSRF state mismatch")
|
||||||
|
error_holder[0] = "CSRF state mismatch"
|
||||||
|
return
|
||||||
|
code_holder[0] = params["code"][0]
|
||||||
|
self2.send_response(302)
|
||||||
|
self2.send_header("Location", "https://developers.google.com/gemini-code-assist/auth_success_gemini")
|
||||||
|
self2.end_headers()
|
||||||
|
else:
|
||||||
|
error_holder[0] = params.get("error", ["unknown"])[0]
|
||||||
|
self2.send_response(302)
|
||||||
|
self2.send_header("Location", "https://developers.google.com/gemini-code-assist/auth_failure_gemini")
|
||||||
|
self2.end_headers()
|
||||||
|
def log_message(self2, fmt, *args):
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
bind_host = "localhost" if is_antigravity else "127.0.0.1"
|
||||||
|
server = http.server.HTTPServer((bind_host, port), OAuthHandler)
|
||||||
|
except OSError:
|
||||||
|
status_lbl.set_text(f"Port {port} in use — close other apps and retry.")
|
||||||
|
oauth_dlg.run()
|
||||||
|
oauth_dlg.destroy()
|
||||||
|
return
|
||||||
|
|
||||||
|
def _wait():
|
||||||
|
deadline = time.time() + 120
|
||||||
|
while code_holder[0] is None and error_holder[0] is None and time.time() < deadline:
|
||||||
|
server.handle_request()
|
||||||
|
server.server_close()
|
||||||
|
if code_holder[0]:
|
||||||
|
try:
|
||||||
|
tok_data = urllib.parse.urlencode({
|
||||||
|
"code": code_holder[0], "client_id": CLIENT_ID, "client_secret": CLIENT_SECRET,
|
||||||
|
"redirect_uri": redirect_uri, "grant_type": "authorization_code",
|
||||||
|
"code_verifier": verifier,
|
||||||
|
}).encode()
|
||||||
|
req = urllib.request.Request("https://oauth2.googleapis.com/token", data=tok_data,
|
||||||
|
headers={"Content-Type": "application/x-www-form-urlencoded"})
|
||||||
|
resp = urllib.request.urlopen(req, timeout=30)
|
||||||
|
tokens = json.loads(resp.read())
|
||||||
|
tokens["client_id"] = CLIENT_ID
|
||||||
|
tokens["client_secret"] = CLIENT_SECRET
|
||||||
|
tokens["provider_kind"] = provider_kind
|
||||||
|
tokens["expires_at"] = time.time() + tokens.get("expires_in", 3600)
|
||||||
|
os.makedirs(os.path.dirname(token_path), exist_ok=True)
|
||||||
|
with open(token_path, "w") as f:
|
||||||
|
json.dump(tokens, f, indent=2)
|
||||||
|
os.chmod(token_path, 0o600)
|
||||||
|
project_id = _oauth_discover_project(tokens["access_token"], token_path, tokens)
|
||||||
|
def _on_success():
|
||||||
|
status_lbl.set_text(f"Authorization successful! Project: {project_id or 'none'}")
|
||||||
|
GLib.timeout_add_seconds(2, lambda: oauth_dlg.destroy())
|
||||||
|
return False
|
||||||
|
GLib.idle_add(_on_success)
|
||||||
|
except Exception as e:
|
||||||
|
def _on_err(exc=str(e)):
|
||||||
|
status_lbl.set_text(f"Token exchange failed: {exc[:200]}")
|
||||||
|
return False
|
||||||
|
GLib.idle_add(_on_err)
|
||||||
|
else:
|
||||||
|
def _on_fail(err=error_holder[0]):
|
||||||
|
status_lbl.set_text(f"Failed: {err or 'No code received'}")
|
||||||
|
return False
|
||||||
|
GLib.idle_add(_on_fail)
|
||||||
|
|
||||||
|
webbrowser.open(auth_url)
|
||||||
|
threading.Thread(target=_wait, daemon=True).start()
|
||||||
|
oauth_dlg.run()
|
||||||
|
oauth_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, dlg))
|
||||||
|
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
|
# Endpoint manager dialog
|
||||||
# ═══════════════════════════════════════════════════════════════════
|
# ═══════════════════════════════════════════════════════════════════
|
||||||
@@ -3121,6 +3739,38 @@ class EditEndpointDialog(Gtk.Dialog):
|
|||||||
add_row(7, "Effort:", self._combo_effort)
|
add_row(7, "Effort:", self._combo_effort)
|
||||||
self._on_reasoning_toggled()
|
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
|
# Models
|
||||||
mlbl = Gtk.Label(label="Models:", xalign=0)
|
mlbl = Gtk.Label(label="Models:", xalign=0)
|
||||||
area.pack_start(mlbl, False, False, 4)
|
area.pack_start(mlbl, False, False, 4)
|
||||||
@@ -3260,6 +3910,13 @@ class EditEndpointDialog(Gtk.Dialog):
|
|||||||
else:
|
else:
|
||||||
self._lbl_reasoning.set_markup('<span foreground="#e67e22" weight="bold">OFF</span>')
|
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):
|
def _do_oauth_login(self):
|
||||||
preset_name = self._combo_preset.get_active_text() or "Custom"
|
preset_name = self._combo_preset.get_active_text() or "Custom"
|
||||||
preset = PROVIDER_PRESETS.get(preset_name, {})
|
preset = PROVIDER_PRESETS.get(preset_name, {})
|
||||||
@@ -3273,9 +3930,17 @@ class EditEndpointDialog(Gtk.Dialog):
|
|||||||
is_antigravity = oauth_provider == "google-antigravity"
|
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")
|
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:
|
if is_antigravity:
|
||||||
CLIENT_ID = "1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com"
|
_sec = _oauth_secrets.get("antigravity", {})
|
||||||
CLIENT_SECRET = "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"
|
CLIENT_ID = _sec.get("client_id", "")
|
||||||
|
CLIENT_SECRET = _sec.get("client_secret", "")
|
||||||
SCOPES = [
|
SCOPES = [
|
||||||
"https://www.googleapis.com/auth/cloud-platform",
|
"https://www.googleapis.com/auth/cloud-platform",
|
||||||
"https://www.googleapis.com/auth/userinfo.email",
|
"https://www.googleapis.com/auth/userinfo.email",
|
||||||
@@ -3288,8 +3953,9 @@ class EditEndpointDialog(Gtk.Dialog):
|
|||||||
callback_path = "/oauth-callback"
|
callback_path = "/oauth-callback"
|
||||||
provider_kind = "antigravity"
|
provider_kind = "antigravity"
|
||||||
else:
|
else:
|
||||||
CLIENT_ID = "681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com"
|
_sec = _oauth_secrets.get("gemini_cli", {})
|
||||||
CLIENT_SECRET = "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"
|
CLIENT_ID = _sec.get("client_id", "")
|
||||||
|
CLIENT_SECRET = _sec.get("client_secret", "")
|
||||||
SCOPES = [
|
SCOPES = [
|
||||||
"https://www.googleapis.com/auth/cloud-platform",
|
"https://www.googleapis.com/auth/cloud-platform",
|
||||||
"https://www.googleapis.com/auth/userinfo.email",
|
"https://www.googleapis.com/auth/userinfo.email",
|
||||||
@@ -3438,32 +4104,8 @@ class EditEndpointDialog(Gtk.Dialog):
|
|||||||
json.dump(tokens, f, indent=2)
|
json.dump(tokens, f, indent=2)
|
||||||
os.chmod(token_path, 0o600)
|
os.chmod(token_path, 0o600)
|
||||||
_oauth_log(f"Token saved to {token_path}")
|
_oauth_log(f"Token saved to {token_path}")
|
||||||
project_id = ""
|
project_id = _oauth_discover_project(tokens["access_token"], token_path, tokens)
|
||||||
try:
|
|
||||||
_oauth_log("Discovering project ID via loadCodeAssist...")
|
|
||||||
lr = urllib.request.Request(
|
|
||||||
"https://cloudcode-pa.googleapis.com/v1internal:loadCodeAssist",
|
|
||||||
data=json.dumps({}).encode(),
|
|
||||||
headers={
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"Authorization": f"Bearer {tokens['access_token']}",
|
|
||||||
"User-Agent": "google-api-nodejs-client/9.15.1",
|
|
||||||
})
|
|
||||||
lresp = urllib.request.urlopen(lr, timeout=15)
|
|
||||||
ldata = json.loads(lresp.read())
|
|
||||||
p = ldata.get("cloudaicompanionProject", "")
|
|
||||||
if isinstance(p, dict):
|
|
||||||
project_id = p.get("id", "")
|
|
||||||
elif isinstance(p, str):
|
|
||||||
project_id = p
|
|
||||||
_oauth_log(f"Project ID: {project_id or '(none)'}")
|
_oauth_log(f"Project ID: {project_id or '(none)'}")
|
||||||
if project_id:
|
|
||||||
tokens["project_id"] = project_id
|
|
||||||
with open(token_path, "w") as f2:
|
|
||||||
json.dump(tokens, f2, indent=2)
|
|
||||||
os.chmod(token_path, 0o600)
|
|
||||||
except Exception as pe:
|
|
||||||
_oauth_log(f"loadCodeAssist failed (non-fatal): {pe}")
|
|
||||||
if is_antigravity:
|
if is_antigravity:
|
||||||
found_models = [
|
found_models = [
|
||||||
"gemini-2.5-flash", "gemini-2.5-pro",
|
"gemini-2.5-flash", "gemini-2.5-pro",
|
||||||
@@ -3485,7 +4127,7 @@ class EditEndpointDialog(Gtk.Dialog):
|
|||||||
for mc in probe_candidates:
|
for mc in probe_candidates:
|
||||||
try:
|
try:
|
||||||
pr = urllib.request.Request(
|
pr = urllib.request.Request(
|
||||||
"https://daily-cloudcode-pa.sandbox.googleapis.com/v1internal:generateContent",
|
"https://cloudcode-pa.googleapis.com/v1internal:generateContent",
|
||||||
data=json.dumps({
|
data=json.dumps({
|
||||||
"project": project_id,
|
"project": project_id,
|
||||||
"model": mc,
|
"model": mc,
|
||||||
@@ -3577,10 +4219,10 @@ class EditEndpointDialog(Gtk.Dialog):
|
|||||||
def _codebuff_auth_thread():
|
def _codebuff_auth_thread():
|
||||||
try:
|
try:
|
||||||
fingerprint_id = str(uuid.uuid4())
|
fingerprint_id = str(uuid.uuid4())
|
||||||
auth_url = "https://codebuff.com/api/auth/cli/code"
|
auth_url = "https://www.codebuff.com/api/auth/cli/code"
|
||||||
body = json.dumps({"fingerprintId": fingerprint_id}).encode()
|
body = json.dumps({"fingerprintId": fingerprint_id}).encode()
|
||||||
req = urllib.request.Request(auth_url, data=body,
|
req = urllib.request.Request(auth_url, data=body,
|
||||||
headers={"Content-Type": "application/json", "User-Agent": "codex-launcher/3.10.3"})
|
headers={"Content-Type": "application/json", "User-Agent": "codex-launcher/3.10.7"})
|
||||||
resp = urllib.request.urlopen(req, timeout=30)
|
resp = urllib.request.urlopen(req, timeout=30)
|
||||||
data = json.loads(resp.read())
|
data = json.loads(resp.read())
|
||||||
login_url = data.get("loginUrl", "") or data.get("login_url", "")
|
login_url = data.get("loginUrl", "") or data.get("login_url", "")
|
||||||
@@ -3599,13 +4241,13 @@ class EditEndpointDialog(Gtk.Dialog):
|
|||||||
|
|
||||||
webbrowser.open(login_url)
|
webbrowser.open(login_url)
|
||||||
|
|
||||||
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}"
|
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}"
|
||||||
deadline = time.time() + 300
|
deadline = time.time() + 300
|
||||||
while time.time() < deadline:
|
while time.time() < deadline:
|
||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
try:
|
try:
|
||||||
poll_req = urllib.request.Request(poll_url,
|
poll_req = urllib.request.Request(poll_url,
|
||||||
headers={"User-Agent": "codex-launcher/3.10.3"})
|
headers={"User-Agent": "codex-launcher/3.10.7"})
|
||||||
poll_resp = urllib.request.urlopen(poll_req, timeout=10)
|
poll_resp = urllib.request.urlopen(poll_req, timeout=10)
|
||||||
poll_data = json.loads(poll_resp.read())
|
poll_data = json.loads(poll_resp.read())
|
||||||
user = poll_data.get("user")
|
user = poll_data.get("user")
|
||||||
@@ -3804,6 +4446,17 @@ class EditEndpointDialog(Gtk.Dialog):
|
|||||||
new_ep["cc_version"] = cc_ver
|
new_ep["cc_version"] = cc_ver
|
||||||
new_ep["reasoning_enabled"] = self._switch_reasoning.get_active()
|
new_ep["reasoning_enabled"] = self._switch_reasoning.get_active()
|
||||||
new_ep["reasoning_effort"] = self._combo_effort.get_active_id() or "medium"
|
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_name = self._combo_preset.get_active_text() or "Custom"
|
||||||
preset = PROVIDER_PRESETS.get(preset_name, {})
|
preset = PROVIDER_PRESETS.get(preset_name, {})
|
||||||
if preset.get("oauth_provider"):
|
if preset.get("oauth_provider"):
|
||||||
@@ -3823,10 +4476,54 @@ class EditEndpointDialog(Gtk.Dialog):
|
|||||||
data["default"] = name
|
data["default"] = name
|
||||||
|
|
||||||
save_endpoints(data)
|
save_endpoints(data)
|
||||||
|
self._hot_reload_proxy_key(new_ep)
|
||||||
self._parent_mgr._rebuild()
|
self._parent_mgr._rebuild()
|
||||||
self._parent_mgr._parent._on_endpoints_updated()
|
self._parent_mgr._parent._on_endpoints_updated()
|
||||||
self.destroy()
|
self.destroy()
|
||||||
|
|
||||||
|
def _hot_reload_proxy_key(self, ep):
|
||||||
|
try:
|
||||||
|
ep_name = ep.get("name", "")
|
||||||
|
proxy_port = None
|
||||||
|
import glob as _glob
|
||||||
|
for cfg_file in _glob.glob(str(PROXY_CONFIG_DIR / "proxy-*.json")):
|
||||||
|
try:
|
||||||
|
with open(cfg_file) as f:
|
||||||
|
pcfg = json.load(f)
|
||||||
|
if ep_name.lower().replace(" ", "-") in cfg_file.lower():
|
||||||
|
proxy_port = pcfg.get("port")
|
||||||
|
pcfg["api_key"] = ep.get("api_key", "")
|
||||||
|
with open(cfg_file, "w") as f:
|
||||||
|
json.dump(pcfg, f, indent=2)
|
||||||
|
break
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
if proxy_port:
|
||||||
|
import urllib.request as _ur
|
||||||
|
try:
|
||||||
|
url = f"http://127.0.0.1:{proxy_port}/admin/reload"
|
||||||
|
resp = _ur.urlopen(url, timeout=3)
|
||||||
|
result = json.loads(resp.read())
|
||||||
|
reloaded = result.get("reloaded", False)
|
||||||
|
preview = result.get("api_key_preview", "?")
|
||||||
|
self._parent_mgr._parent.log(
|
||||||
|
f"[hot-reload] key {'updated' if reloaded else 'unchanged'}: {preview}")
|
||||||
|
if reloaded:
|
||||||
|
verify_url = f"http://127.0.0.1:{proxy_port}/admin/verify-key"
|
||||||
|
vresp = _ur.urlopen(verify_url, timeout=10)
|
||||||
|
vresult = json.loads(vresp.read())
|
||||||
|
valid = vresult.get("valid", False)
|
||||||
|
if valid:
|
||||||
|
self._parent_mgr._parent.log(
|
||||||
|
f"[hot-reload] key verified OK ({vresult.get('models', '?')} models)")
|
||||||
|
else:
|
||||||
|
self._parent_mgr._parent.log(
|
||||||
|
f"[hot-reload] WARNING: key verification failed: {vresult.get('error', 'unknown')}")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
def _show_error(self, msg):
|
def _show_error(self, msg):
|
||||||
d = Gtk.MessageDialog(self, 0, Gtk.MessageType.ERROR, Gtk.ButtonsType.OK, msg)
|
d = Gtk.MessageDialog(self, 0, Gtk.MessageType.ERROR, Gtk.ButtonsType.OK, msg)
|
||||||
d.run(); d.destroy()
|
d.run(); d.destroy()
|
||||||
|
|||||||
3313
src/codex-launcher-gui.py
Normal file
3313
src/codex-launcher-gui.py
Normal file
File diff suppressed because it is too large
Load Diff
2223
src/codex_launcher_lib.py
Normal file
2223
src/codex_launcher_lib.py
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
1166
tests/test_translate_proxy.py
Normal file
1166
tests/test_translate_proxy.py
Normal file
File diff suppressed because it is too large
Load Diff
6067
translate-proxy.py
6067
translate-proxy.py
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user