48 Commits

18 changed files with 5121 additions and 697 deletions

4
.gitignore vendored
View File

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

View File

@@ -1,101 +1,5 @@
# Changelog # Changelog
## v3.10.4 (2026-05-25)
**Security: OAuth Secrets Editor + Import JSON**
### Security
- **All hardcoded OAuth secrets removed from source code and git history**
- OAuth client IDs and secrets now stored locally in `~/.config/codex-launcher/oauth-secrets.json`
- Git history rewritten to scrub all leaked credentials (0 matches verified)
- Pre-push hook blocks any future commit containing secrets
- All old Gitea releases deleted (contained leaked secrets in .deb files)
### New Features
- **OAuth Secrets editor** in GUI — "OAuth Secrets" button in header bar
- **Import JSON** button — import `client_secret_*.json` downloaded from Google Cloud Console
- Supports both `"installed"` and `"web"` JSON formats from Google
### Antigravity Fix (from v3.10.3)
- Antigravity REST API uses slug IDs, not display names
- Verified all model IDs with live API testing
## v3.10.3 (2026-05-25)
**Fix Antigravity 404 Errors — Verified REST Model IDs**
### Critical Fix
- Antigravity REST API (`v1internal:generateContent`) uses slug IDs, not display names
- Verified all model IDs with live API testing against `daily-cloudcode-pa.sandbox.googleapis.com`
- Display names map to closest working REST model (e.g. `Gemini 3.5 Flash (High)``gemini-3-flash`)
- Model list now matches agy CLI: Gemini 3.5 Flash (H/M/L), Gemini 3.1 Pro (H/L), Claude Sonnet/Opus 4.6, GPT-OSS 120B
### Working REST Model IDs
| Display Name | REST ID |
|---|---|
| Gemini 3.5 Flash (High) | gemini-3-flash |
| Gemini 3.5 Flash (Medium) | gemini-3-flash |
| Gemini 3.5 Flash (Low) | gemini-3.5-flash-low |
| Gemini 3.1 Pro (High) | gemini-3.1-pro-low |
| Gemini 3.1 Pro (Low) | gemini-3.1-pro-low |
| Claude Sonnet 4.6 (Thinking) | claude-sonnet-4-6 |
| Claude Opus 4.6 (Thinking) | claude-opus-4-6-thinking |
| GPT-OSS 120B (Medium) | gpt-oss-120b-medium |
## v3.10.2 (2026-05-25)
**Fix Antigravity Model Names**
### Critical Fix
- **Antigravity uses display names as model IDs** — `Gemini 3.5 Flash (High)` not `gemini-3.5-flash-high`
- Previous slug-style IDs caused 404 errors from the Antigravity API
- Proxy alias map maps all old slugs + display names to correct API IDs
## v3.10.0 (2026-05-25)
**Provider Model Editor + Antigravity Model Refresh**
### Provider Editor
- **Remove Selected** button to remove highlighted model(s) from provider
- **Clear All** button to empty model list
- **Sync from Preset** button to refresh model list from current preset definition
- Preset sync now replaces (not appends) models — fixes stale saved model lists
### Antigravity Models Updated
- **Gemini 3.5 Flash** (High / Medium)
- **Gemini 3.1 Pro** (High / Low)
- **Claude Sonnet 4.6 Thinking**
- **Claude Opus 4.6 Thinking**
- **GPT-OSS 120B Medium**
## v3.9.9 (2026-05-25)
**Antigravity Model Refresh**
### Updated Models
- **Gemini 3.5 Flash** (High / Medium) — new flagship flash model
- **Gemini 3.1 Pro** (High / Low) — tiered reasoning control
- **Claude Sonnet 4.6 Thinking** — Anthropic partner model via Antigravity
- **Claude Opus 4.6 Thinking** — Anthropic partner model via Antigravity
- **GPT-OSS 120B Medium** — open-weight GPT model via Antigravity
- Removed stale `antigravity-*` prefixed IDs and old preview models
### Proxy Updates
- Alias map updated for tiered model IDs (high/medium/low/thinking)
- Context sizes added for all new Antigravity models
## v3.9.8 (2026-05-25)
**Codex Desktop Model Fix & Global BrokenPipeError Protection**
### Desktop Model Fix
- **Codex Desktop sending wrong model** (gpt-5.4-mini) instead of user-selected model — now remapped via `CODEX_LAUNCHER_MODEL` env var
- **Config.toml** now writes `review_model`, `wire_api`, `request_max_retries`, `stream_max_retries`, `stream_idle_timeout_ms` for Desktop compatibility
- **Proxy model remap** intercepts Desktop forced models (`gpt-5.4-mini`, `gpt-5.5`, etc.) and routes to the user's selected model
### Global Crash Fix
- **`send_json()` globally catches BrokenPipeError** — no more crashes on client disconnect across all backends
## v3.9.7 (2026-05-25) ## v3.9.7 (2026-05-25)
**Codebuff Error Forwarding & Crash Fixes** **Codebuff Error Forwarding & Crash Fixes**

5073
codex-launcher-gui Executable file

File diff suppressed because it is too large Load Diff

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

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

View File

@@ -3,11 +3,11 @@ set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
if [ -f "$SCRIPT_DIR/codex-launcher_3.10.4_all.deb" ]; then if [ -f "$SCRIPT_DIR/codex-launcher_3.9.7_all.deb" ]; then
echo "Installing codex-launcher_3.10.4_all.deb ..." echo "Installing codex-launcher_3.9.7_all.deb ..."
sudo dpkg -i "$SCRIPT_DIR/codex-launcher_3.10.4_all.deb" sudo dpkg -i "$SCRIPT_DIR/codex-launcher_3.9.7_all.deb"
echo "" echo ""
echo "Installed v3.10.4 via .deb package." echo "Installed v3.9.7 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"

View File

@@ -26,34 +26,6 @@ model_catalog_json = ""
""" """
CHANGELOG = [ CHANGELOG = [
("3.10.4", "2026-05-25", [
"OAuth Secrets editor in GUI — update client ID/secret without editing files",
"Secrets stored in ~/.config/codex-launcher/oauth-secrets.json (not in repo)",
]),
("3.10.3", "2026-05-25", [
"Fix Antigravity 404: map display names to verified REST API model IDs",
"REST API uses slugs (gemini-3-flash) not display names (Gemini 3.5 Flash)",
"Match agy CLI model list: Gemini 3.5 Flash (H/M/L), 3.1 Pro (H/L), Claude 4.6, GPT-OSS",
]),
("3.10.2", "2026-05-25", [
"Fetch from API now works for Antigravity — returns current model list",
]),
("3.10.0", "2026-05-25", [
"Provider editor: Remove Selected, Clear All, Sync from Preset buttons for model list",
"Sync from Preset replaces model list with current preset models",
"Stale saved Antigravity models auto-refreshed on preset sync",
]),
("3.9.9", "2026-05-25", [
"Refresh Antigravity preset: Gemini 3.5 Flash, Gemini 3.1 Pro, Claude Sonnet/Opus 4.6, GPT-OSS 120B",
"Fix Antigravity alias map for new tiered model IDs (high/medium/low/thinking)",
"Add model context sizes for Gemini 3.5 Flash, Gemini 3.1 Pro, Claude 4.6, GPT-OSS 120B",
]),
("3.9.8", "2026-05-25", [
"Fix Codex Desktop sending wrong model (gpt-5.4-mini) instead of selected model",
"Proxy remaps Desktop forced models to user-selected model via CODEX_LAUNCHER_MODEL",
"Write review_model + wire_api + retries to config.toml for Desktop compatibility",
"send_json() globally catches BrokenPipeError — no more crashes on disconnect",
]),
("3.9.7", "2026-05-25", [ ("3.9.7", "2026-05-25", [
"Forward real Codebuff error messages to user (not generic 429)", "Forward real Codebuff error messages to user (not generic 429)",
"Return HTTP 200 with Responses API format for rate limits so Codex displays message", "Return HTTP 200 with Responses API format for rate limits so Codex displays message",
@@ -364,11 +336,13 @@ PROVIDER_PRESETS = {
"base_url": "https://daily-cloudcode-pa.sandbox.googleapis.com", "base_url": "https://daily-cloudcode-pa.sandbox.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)", "antigravity-gemini-3-flash",
"Gemini 3.1 Pro (High)", "Gemini 3.1 Pro (Low)", "antigravity-gemini-3-pro",
"Claude Sonnet 4.6 (Thinking)", "antigravity-gemini-3.1-pro",
"Claude Opus 4.6 (Thinking)", "antigravity-claude-sonnet-4-6",
"GPT-OSS 120B (Medium)", "antigravity-claude-opus-4-6-thinking",
"gemini-2.5-flash", "gemini-2.5-pro",
"gemini-3-flash-preview", "gemini-3-pro-preview", "gemini-3.1-pro-preview",
], ],
}, },
"OpenAdapter": { "OpenAdapter": {
@@ -761,18 +735,7 @@ def endpoint_model_headers(endpoint):
headers["Authorization"] = f"Bearer {key}" headers["Authorization"] = f"Bearer {key}"
return headers return headers
_ANTIGRAVITY_MODELS = [
"Gemini 3.5 Flash (High)", "Gemini 3.5 Flash (Medium)", "Gemini 3.5 Flash (Low)",
"Gemini 3.1 Pro (High)", "Gemini 3.1 Pro (Low)",
"Claude Sonnet 4.6 (Thinking)",
"Claude Opus 4.6 (Thinking)",
"GPT-OSS 120B (Medium)",
]
def fetch_models_for_endpoint(endpoint, timeout=10): def fetch_models_for_endpoint(endpoint, timeout=10):
bt = endpoint.get("backend_type", "")
if bt == "gemini-oauth-antigravity":
return list(_ANTIGRAVITY_MODELS), None
url = endpoint_models_url(endpoint) url = endpoint_models_url(endpoint)
if not url: if not url:
return None, "Base URL is empty" return None, "Base URL is empty"
@@ -973,21 +936,15 @@ def write_config_for_translated(endpoint, selected_model, proxy_port=8080):
lines = [ lines = [
f'model = "{_toml_safe(selected_model)}"\n', f'model = "{_toml_safe(selected_model)}"\n',
f'review_model = "{_toml_safe(selected_model)}"\n',
f'model_provider = "{_toml_safe(endpoint["name"])}"\n', f'model_provider = "{_toml_safe(endpoint["name"])}"\n',
f'model_catalog_json = "{mc_path}"\n', f'model_catalog_json = "{mc_path}"\n',
f'\n[model_providers."{endpoint["name"]}"]\n', f'\n[model_providers."{endpoint["name"]}"]\n',
f'name = "{_toml_safe(endpoint["name"])}"\n', f'name = "{_toml_safe(endpoint["name"])}"\n',
f'base_url = "http://127.0.0.1:{proxy_port}"\n', f'base_url = "http://127.0.0.1:{proxy_port}"\n',
f'experimental_bearer_token = "codex-launcher-local"\n', f'experimental_bearer_token = "codex-launcher-local"\n',
f'wire_api = "responses"\n',
f'request_max_retries = 1\n',
f'stream_max_retries = 0\n',
f'stream_idle_timeout_ms = 600000\n',
f'\n[profiles."{endpoint["name"]}"]\n', f'\n[profiles."{endpoint["name"]}"]\n',
f'model_provider = "{_toml_safe(endpoint["name"])}"\n', f'model_provider = "{_toml_safe(endpoint["name"])}"\n',
f'model = "{_toml_safe(selected_model)}"\n', f'model = "{_toml_safe(selected_model)}"\n',
f'review_model = "{_toml_safe(selected_model)}"\n',
f'model_catalog_json = "{mc_path}"\n', f'model_catalog_json = "{mc_path}"\n',
f'service_tier = "fast"\n', f'service_tier = "fast"\n',
f'approvals_reviewer = "user"\n', f'approvals_reviewer = "user"\n',
@@ -1780,7 +1737,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.4</b>") lbl = Gtk.Label(label="<b>Codex Launcher v3.9.7</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")
@@ -1804,9 +1761,6 @@ 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()
@@ -2480,7 +2434,6 @@ class LauncherWin(Gtk.Window):
if needs_proxy: if needs_proxy:
self.log("Starting translation proxy…") self.log("Starting translation proxy…")
os.environ["CODEX_LAUNCHER_MODEL"] = model
try: try:
proxy_port = _start_proxy_for(ep, self.log) proxy_port = _start_proxy_for(ep, self.log)
except RuntimeError as e: except RuntimeError as e:
@@ -2790,98 +2743,6 @@ class LauncherWin(Gtk.Window):
_stop_proxy() _stop_proxy()
Gtk.main_quit() Gtk.main_quit()
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 2.0 Client Secrets", parent=self, modal=True)
dlg.add_button("Cancel", Gtk.ResponseType.CANCEL)
dlg.add_button("Save", Gtk.ResponseType.OK)
dlg.set_default_size(540, 420)
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)
area.pack_start(Gtk.Label(label="<b>Google OAuth 2.0 credentials</b>\n<small>Stored locally in ~/.config/codex-launcher/oauth-secrets.json</small>", use_markup=True, xalign=0), False, False, 4)
fields = {}
for section_key, section_label in [("antigravity", "Antigravity (CloudCode)"), ("gemini_cli", "Gemini CLI")]:
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)
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)
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(380, -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))
area.pack_start(section_box, False, False, 0)
area.pack_start(Gtk.Label(label="\n<small>Import a client_secret_*.json from Google Cloud Console\nor edit fields manually. console.cloud.google.com → Credentials</small>", use_markup=True, xalign=0), False, False, 4)
area.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))
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
# ═══════════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════════
@@ -3261,18 +3122,6 @@ class EditEndpointDialog(Gtk.Dialog):
sw.add(self._model_tree) sw.add(self._model_tree)
self._model_tree.connect("row-activated", lambda t, p, c: self._remove_model(p)) self._model_tree.connect("row-activated", lambda t, p, c: self._remove_model(p))
model_btn_box = Gtk.Box(spacing=6)
area.pack_start(model_btn_box, False, False, 0)
self._remove_model_btn = Gtk.Button(label="Remove Selected")
self._remove_model_btn.connect("clicked", lambda b: self._remove_selected_model())
model_btn_box.pack_start(self._remove_model_btn, False, False, 0)
self._clear_models_btn = Gtk.Button(label="Clear All")
self._clear_models_btn.connect("clicked", lambda b: self._clear_all_models())
model_btn_box.pack_start(self._clear_models_btn, False, False, 0)
self._sync_preset_btn = Gtk.Button(label="Sync from Preset")
self._sync_preset_btn.connect("clicked", lambda b: self._apply_selected_preset())
model_btn_box.pack_start(self._sync_preset_btn, False, False, 0)
for m in self._data.get("models", []): for m in self._data.get("models", []):
self._model_store.append([m]) self._model_store.append([m])
@@ -3342,12 +3191,10 @@ class EditEndpointDialog(Gtk.Dialog):
cc_ver = preset.get("cc_version", "") cc_ver = preset.get("cc_version", "")
if cc_ver and not self._entry_cc_ver.get_text().strip(): if cc_ver and not self._entry_cc_ver.get_text().strip():
self._entry_cc_ver.set_text(cc_ver) self._entry_cc_ver.set_text(cc_ver)
if preset.get("models") and (not initial or len(self._model_store) == 0): if preset.get("models") and len(self._model_store) == 0:
current = self._combo_default.get_active_text()
self._model_store.clear()
for mid in preset["models"]: for mid in preset["models"]:
self._model_store.append([mid]) self._model_store.append([mid])
self._refresh_default_combo(current or preset["models"][0]) self._refresh_default_combo(preset["models"][0])
if initial and self._data.get("models"): if initial and self._data.get("models"):
self._refresh_default_combo(self._data.get("default_model", "")) self._refresh_default_combo(self._data.get("default_model", ""))
@@ -3372,17 +3219,9 @@ 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:
_sec = _oauth_secrets.get("antigravity", {}) CLIENT_ID = "1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com"
CLIENT_ID = _sec.get("client_id", "") CLIENT_SECRET = "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"
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",
@@ -3395,9 +3234,8 @@ class EditEndpointDialog(Gtk.Dialog):
callback_path = "/oauth-callback" callback_path = "/oauth-callback"
provider_kind = "antigravity" provider_kind = "antigravity"
else: else:
_sec = _oauth_secrets.get("gemini_cli", {}) CLIENT_ID = "681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com"
CLIENT_ID = _sec.get("client_id", "") CLIENT_SECRET = "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"
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",
@@ -3688,7 +3526,7 @@ class EditEndpointDialog(Gtk.Dialog):
auth_url = "https://codebuff.com/api/auth/cli/code" auth_url = "https://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.4"}) headers={"Content-Type": "application/json", "User-Agent": "codex-launcher/3.9.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", "")
@@ -3713,7 +3551,7 @@ class EditEndpointDialog(Gtk.Dialog):
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.4"}) headers={"User-Agent": "codex-launcher/3.9.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")
@@ -3778,21 +3616,6 @@ class EditEndpointDialog(Gtk.Dialog):
self._model_store.remove(self._model_store.get_iter(path)) self._model_store.remove(self._model_store.get_iter(path))
self._refresh_default_combo(current) self._refresh_default_combo(current)
def _remove_selected_model(self):
sel = self._model_tree.get_selection()
model, paths = sel.get_selected_rows()
if not paths:
return
current = self._combo_default.get_active_text()
for p in reversed(paths):
self._model_store.remove(self._model_store.get_iter(p))
self._refresh_default_combo(current)
def _clear_all_models(self):
current = self._combo_default.get_active_text()
self._model_store.clear()
self._refresh_default_combo(current)
def _refresh_default_combo(self, active=None): def _refresh_default_combo(self, active=None):
if active is None: if active is None:
active = self._combo_default.get_active_text() active = self._combo_default.get_active_text()

View File

@@ -187,7 +187,7 @@ def load_config():
p = argparse.ArgumentParser(description="Responses API translation proxy") p = argparse.ArgumentParser(description="Responses API translation proxy")
p.add_argument("--config", help="JSON config file path") p.add_argument("--config", help="JSON config file path")
p.add_argument("--port", type=int, default=None) p.add_argument("--port", type=int, default=None)
p.add_argument("--backend", default=None, choices=["openai-compat", "anthropic", "command-code", "codebuff", "freebuff", "auto"]) p.add_argument("--backend", default=None, choices=["openai-compat", "anthropic", "command-code", "codebuff", "auto"])
p.add_argument("--target-url", default=None) p.add_argument("--target-url", default=None)
p.add_argument("--api-key", default=None) p.add_argument("--api-key", default=None)
p.add_argument("--models-file", default=None, help="JSON file with model list array") p.add_argument("--models-file", default=None, help="JSON file with model list array")
@@ -335,7 +335,7 @@ def _codebuff_get_session(token, model):
req = urllib.request.Request(url, data=body, headers={ req = urllib.request.Request(url, data=body, headers={
"Content-Type": "application/json", "Content-Type": "application/json",
"Authorization": f"Bearer {token}", "Authorization": f"Bearer {token}",
"User-Agent": "codex-launcher/3.10.4", "User-Agent": "codex-launcher/3.9.7",
"x-codebuff-model": model, "x-codebuff-model": model,
}) })
try: try:
@@ -383,7 +383,7 @@ def _codebuff_start_run(token, agent_id):
req = urllib.request.Request(url, data=body, headers={ req = urllib.request.Request(url, data=body, headers={
"Content-Type": "application/json", "Content-Type": "application/json",
"Authorization": f"Bearer {token}", "Authorization": f"Bearer {token}",
"User-Agent": "codex-launcher/3.10.4", "User-Agent": "codex-launcher/3.9.7",
}) })
try: try:
resp = urllib.request.urlopen(req, timeout=15) resp = urllib.request.urlopen(req, timeout=15)
@@ -416,7 +416,7 @@ def _codebuff_finish_run(token, run_id, status="completed"):
req = urllib.request.Request(url, data=body, headers={ req = urllib.request.Request(url, data=body, headers={
"Content-Type": "application/json", "Content-Type": "application/json",
"Authorization": f"Bearer {token}", "Authorization": f"Bearer {token}",
"User-Agent": "codex-launcher/3.10.4", "User-Agent": "codex-launcher/3.9.7",
}) })
try: try:
urllib.request.urlopen(req, timeout=10) urllib.request.urlopen(req, timeout=10)
@@ -764,7 +764,7 @@ def _init_runtime():
REASONING_EFFORT = CONFIG.get("reasoning_effort", "medium") REASONING_EFFORT = CONFIG.get("reasoning_effort", "medium")
BGP_ROUTES = CONFIG.get("bgp_routes", []) BGP_ROUTES = CONFIG.get("bgp_routes", [])
_api_key_pool = None _api_key_pool = None
if API_KEY and "," in API_KEY and not OAUTH_PROVIDER.startswith("google") and BACKEND not in ("codebuff", "freebuff"): if API_KEY and "," in API_KEY and not OAUTH_PROVIDER.startswith("google") and BACKEND not in ("codebuff",):
_api_key_pool = APIKeyPool(BACKEND, API_KEY) _api_key_pool = APIKeyPool(BACKEND, API_KEY)
print(f"[multi-account] API key pool: {len(_api_key_pool._accounts)} keys for {BACKEND}", file=sys.stderr) print(f"[multi-account] API key pool: {len(_api_key_pool._accounts)} keys for {BACKEND}", file=sys.stderr)
if OAUTH_PROVIDER == "google-antigravity": if OAUTH_PROVIDER == "google-antigravity":
@@ -1588,12 +1588,6 @@ _MODEL_CONTEXT = {
"claude-sonnet": 200000, "claude-haiku": 200000, "claude-sonnet": 200000, "claude-haiku": 200000,
"glm-5.1": 128000, "glm-5": 128000, "glm-4": 128000, "glm-5.1": 128000, "glm-5": 128000, "glm-4": 128000,
"deepseek": 64000, "gemini-2.5-flash": 1000000, "gemini-2.5-pro": 2000000, "deepseek": 64000, "gemini-2.5-flash": 1000000, "gemini-2.5-pro": 2000000,
"gemini-3.5-flash": 1000000, "gemini-3.1-pro": 2000000,
"Gemini 3.5 Flash": 1000000, "Gemini 3.1 Pro": 2000000,
"Claude Sonnet 4.6": 200000, "Claude Opus 4.6": 200000,
"GPT-OSS 120B": 128000,
"claude-sonnet-4.6-thinking": 200000, "claude-opus-4.6-thinking": 200000,
"gpt-oss-120b": 128000,
"mimo": 32768, "minimax": 32768, "kimi": 128000, "mimo": 32768, "minimax": 32768, "kimi": 128000,
"_default": 32768, "_default": 32768,
} }
@@ -4043,7 +4037,7 @@ class Handler(http.server.BaseHTTPRequestHandler):
self.send_json(200, {"object": "list", "data": MODELS}) self.send_json(200, {"object": "list", "data": MODELS})
elif self.path in ("/v1/accounts", "/accounts"): elif self.path in ("/v1/accounts", "/accounts"):
info = {"provider": BACKEND, "oauth_provider": OAUTH_PROVIDER} info = {"provider": BACKEND, "oauth_provider": OAUTH_PROVIDER}
if BACKEND in ("codebuff", "freebuff"): if BACKEND == "codebuff":
info["accounts"] = _cb_pool.status() info["accounts"] = _cb_pool.status()
info["total"] = len(_cb_pool._accounts) info["total"] = len(_cb_pool._accounts)
elif OAUTH_PROVIDER and OAUTH_PROVIDER.startswith("google"): elif OAUTH_PROVIDER and OAUTH_PROVIDER.startswith("google"):
@@ -4138,12 +4132,6 @@ class Handler(http.server.BaseHTTPRequestHandler):
model = body.get("model", MODELS[0]["id"] if MODELS else "unknown") model = body.get("model", MODELS[0]["id"] if MODELS else "unknown")
stream = body.get("stream", False) stream = body.get("stream", False)
_desktop_forced_models = {"gpt-5.4-mini", "gpt-5.4", "gpt-5.5", "gpt-5-codex", "gpt-5.3-codex"}
_launcher_model = os.environ.get("CODEX_LAUNCHER_MODEL", "")
if _launcher_model and model in _desktop_forced_models:
print(f"[{_sid}] remap desktop model {model} -> {_launcher_model}", file=sys.stderr)
model = _launcher_model
body["model"] = model
request_id = body.get("request_id") or body.get("id") or uid("req") request_id = body.get("request_id") or body.get("id") or uid("req")
if isinstance(input_data, list): if isinstance(input_data, list):
for item in input_data: for item in input_data:
@@ -4161,7 +4149,7 @@ class Handler(http.server.BaseHTTPRequestHandler):
self._handle_anthropic(body, model, stream, tracker) self._handle_anthropic(body, model, stream, tracker)
elif BACKEND == "command-code": elif BACKEND == "command-code":
self._handle_command_code(body, model, stream, tracker) self._handle_command_code(body, model, stream, tracker)
elif BACKEND in ("codebuff", "freebuff"): elif BACKEND == "codebuff":
self._handle_codebuff(body, model, stream, tracker) self._handle_codebuff(body, model, stream, tracker)
elif (BACKEND or "").startswith("gemini-oauth"): elif (BACKEND or "").startswith("gemini-oauth"):
self._handle_gemini_oauth(body, model, stream, tracker) self._handle_gemini_oauth(body, model, stream, tracker)
@@ -4315,41 +4303,16 @@ class Handler(http.server.BaseHTTPRequestHandler):
if OAUTH_PROVIDER == "google-antigravity": if OAUTH_PROVIDER == "google-antigravity":
alias_map = { alias_map = {
"Gemini 3.5 Flash (High)": "gemini-3-flash",
"Gemini 3.5 Flash (Medium)": "gemini-3-flash",
"Gemini 3.5 Flash (Low)": "gemini-3.5-flash-low",
"gemini-3.5-flash-high": "gemini-3-flash",
"gemini-3.5-flash-medium": "gemini-3-flash",
"gemini-3.5-flash-low": "gemini-3.5-flash-low",
"gemini-3-flash-preview": "gemini-3-flash",
"gemini-3-flash": "gemini-3-flash",
"antigravity-gemini-3-flash": "gemini-3-flash", "antigravity-gemini-3-flash": "gemini-3-flash",
"Gemini 3.1 Pro (High)": "gemini-3.1-pro-low", "antigravity-gemini-3-pro": "gemini-3-pro-low",
"Gemini 3.1 Pro (Low)": "gemini-3.1-pro-low",
"gemini-3.1-pro-high": "gemini-3.1-pro-low",
"gemini-3.1-pro-low": "gemini-3.1-pro-low",
"gemini-3.1-pro-preview": "gemini-3.1-pro-low",
"gemini-3.1-pro": "gemini-3.1-pro-low",
"gemini-3-pro-preview": "gemini-3.1-pro-low",
"gemini-3-pro": "gemini-3.1-pro-low",
"gemini-3-pro-low": "gemini-3.1-pro-low",
"gemini-3-pro-high": "gemini-3.1-pro-low",
"antigravity-gemini-3-pro": "gemini-3.1-pro-low",
"antigravity-gemini-3.1-pro": "gemini-3.1-pro-low", "antigravity-gemini-3.1-pro": "gemini-3.1-pro-low",
"Claude Sonnet 4.6 (Thinking)": "claude-sonnet-4-6", "gemini-3-flash-preview": "gemini-3-flash",
"Claude Sonnet 4.6 Thinking": "claude-sonnet-4-6", "gemini-3-pro-preview": "gemini-3-pro-low",
"claude-sonnet-4.6-thinking": "claude-sonnet-4-6", "gemini-3.1-pro-preview": "gemini-3.1-pro-low",
"gemini-3-pro": "gemini-3-pro-low",
"gemini-3.1-pro": "gemini-3.1-pro-low",
"antigravity-claude-sonnet-4-6": "claude-sonnet-4-6", "antigravity-claude-sonnet-4-6": "claude-sonnet-4-6",
"Claude Opus 4.6 (Thinking)": "claude-opus-4-6-thinking",
"Claude Opus 4.6 Thinking": "claude-opus-4-6-thinking",
"claude-opus-4.6-thinking": "claude-opus-4-6-thinking",
"antigravity-claude-opus-4-6-thinking": "claude-opus-4-6-thinking", "antigravity-claude-opus-4-6-thinking": "claude-opus-4-6-thinking",
"GPT-OSS 120B (Medium)": "gpt-oss-120b-medium",
"GPT-OSS 120B Medium": "gpt-oss-120b-medium",
"gpt-oss-120b": "gpt-oss-120b-medium",
"gemini-2.5-flash": "gemini-2.5-flash",
"gemini-2.5-pro": "gemini-2.5-pro",
"gemini-2.5-flash-lite": "gemini-2.5-flash-lite",
} }
model = alias_map.get(model, model) model = alias_map.get(model, model)
if model != original_model: if model != original_model:
@@ -5342,7 +5305,7 @@ class Handler(http.server.BaseHTTPRequestHandler):
headers = { headers = {
"Content-Type": "application/json", "Content-Type": "application/json",
"Authorization": f"Bearer {token}", "Authorization": f"Bearer {token}",
"User-Agent": "codex-launcher/3.10.4", "User-Agent": "codex-launcher/3.9.7",
"x-codebuff-model": model, "x-codebuff-model": model,
} }
if instance_id: if instance_id:
@@ -5479,7 +5442,10 @@ class Handler(http.server.BaseHTTPRequestHandler):
}], }],
"usage": {"input_tokens": 0, "output_tokens": 0, "total_tokens": 0}, "usage": {"input_tokens": 0, "output_tokens": 0, "total_tokens": 0},
} }
try:
return self.send_json(200, result) return self.send_json(200, result)
except (BrokenPipeError, ConnectionResetError, ConnectionAbortedError):
return
def _cb_retry_thinking_disabled(self, body, model, token, agent_id, stream, tracker, input_data, instructions, original_error, acct=None): def _cb_retry_thinking_disabled(self, body, model, token, agent_id, stream, tracker, input_data, instructions, original_error, acct=None):
run_id, run_err = _codebuff_start_run(token, agent_id) run_id, run_err = _codebuff_start_run(token, agent_id)
@@ -5508,7 +5474,7 @@ class Handler(http.server.BaseHTTPRequestHandler):
if body.get("tool_choice"): if body.get("tool_choice"):
chat_body["tool_choice"] = body["tool_choice"] chat_body["tool_choice"] = body["tool_choice"]
target = f"{_CODEBUFF_API_URL}/api/v1/chat/completions" target = f"{_CODEBUFF_API_URL}/api/v1/chat/completions"
headers = {"Content-Type": "application/json", "Authorization": f"Bearer {token}", "User-Agent": "codex-launcher/3.10.4", "x-codebuff-model": model} headers = {"Content-Type": "application/json", "Authorization": f"Bearer {token}", "User-Agent": "codex-launcher/3.9.7", "x-codebuff-model": model}
if instance_id: if instance_id:
headers["x-codebuff-instance-id"] = instance_id headers["x-codebuff-instance-id"] = instance_id
print(f"[codebuff] retry POST {target} model={model} stream={stream} run={run_id} (thinking disabled via DeepSeek native)", file=sys.stderr) print(f"[codebuff] retry POST {target} model={model} stream={stream} run={run_id} (thinking disabled via DeepSeek native)", file=sys.stderr)
@@ -5864,15 +5830,12 @@ class Handler(http.server.BaseHTTPRequestHandler):
store_response(rid, input_data, result.get("output", [])) store_response(rid, input_data, result.get("output", []))
def send_json(self, status, data): def send_json(self, status, data):
try:
body = json.dumps(data).encode() body = json.dumps(data).encode()
self.send_response(status) self.send_response(status)
self.send_header("Content-Type", "application/json") self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body))) self.send_header("Content-Length", str(len(body)))
self.end_headers() self.end_headers()
self.wfile.write(body) self.wfile.write(body)
except (BrokenPipeError, ConnectionResetError, ConnectionAbortedError):
pass
def stream_buffered_events(self, event_iter, flush_interval=0.03, max_bytes=4096, on_event=None): def stream_buffered_events(self, event_iter, flush_interval=0.03, max_bytes=4096, on_event=None):
buf = bytearray() buf = bytearray()
@@ -5933,7 +5896,7 @@ def main():
print(f"translate-proxy ({BACKEND}) listening on http://127.0.0.1:{PORT}", flush=True) print(f"translate-proxy ({BACKEND}) listening on http://127.0.0.1:{PORT}", flush=True)
print(f"Target: {TARGET_URL}", flush=True) print(f"Target: {TARGET_URL}", flush=True)
print(f"Models: {[m['id'] for m in MODELS]}", flush=True) print(f"Models: {[m['id'] for m in MODELS]}", flush=True)
if BACKEND in ("codebuff", "freebuff"): if BACKEND == "codebuff":
_cb_pool.load_accounts(force=True) _cb_pool.load_accounts(force=True)
fb_status = _cb_pool.status() fb_status = _cb_pool.status()
print(f"[multi-account] codebuff: {len(fb_status)} accounts loaded {[a['id'] for a in fb_status]}", flush=True) print(f"[multi-account] codebuff: {len(fb_status)} accounts loaded {[a['id'] for a in fb_status]}", flush=True)