diff --git a/CHANGELOG.md b/CHANGELOG.md index efe6fa0..df84b4e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## v3.9.8 (2026-05-25) + +**Codex Desktop Model Fix & Global BrokenPipeError Protection** + +### Desktop Model Fix +- **Codex Desktop sending wrong model** (gpt-5.4-mini) instead of user-selected model — now remapped via `CODEX_LAUNCHER_MODEL` env var +- **Config.toml** now writes `review_model`, `wire_api`, `request_max_retries`, `stream_max_retries`, `stream_idle_timeout_ms` for Desktop compatibility +- **Proxy model remap** intercepts Desktop forced models (`gpt-5.4-mini`, `gpt-5.5`, etc.) and routes to the user's selected model + +### Global Crash Fix +- **`send_json()` globally catches BrokenPipeError** — no more crashes on client disconnect across all backends + ## v3.9.7 (2026-05-25) **Codebuff Error Forwarding & Crash Fixes** diff --git a/codex-launcher_3.9.8_all.deb b/codex-launcher_3.9.8_all.deb new file mode 100644 index 0000000..bd01fa9 Binary files /dev/null and b/codex-launcher_3.9.8_all.deb differ diff --git a/install.sh b/install.sh index 4735faf..b8002b3 100755 --- a/install.sh +++ b/install.sh @@ -3,11 +3,11 @@ set -e SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -if [ -f "$SCRIPT_DIR/codex-launcher_3.9.7_all.deb" ]; then - echo "Installing codex-launcher_3.9.7_all.deb ..." - sudo dpkg -i "$SCRIPT_DIR/codex-launcher_3.9.7_all.deb" +if [ -f "$SCRIPT_DIR/codex-launcher_3.9.8_all.deb" ]; then + echo "Installing codex-launcher_3.9.8_all.deb ..." + sudo dpkg -i "$SCRIPT_DIR/codex-launcher_3.9.8_all.deb" echo "" - echo "Installed v3.9.7 via .deb package." + echo "Installed v3.9.8 via .deb package." echo " translate-proxy.py -> /usr/bin/translate-proxy.py" echo " codex-launcher-gui -> /usr/bin/codex-launcher-gui" echo " cleanup-codex-stale -> /usr/bin/cleanup-codex-stale.sh" diff --git a/src/codex-launcher-gui b/src/codex-launcher-gui index acdc992..9c538f2 100755 --- a/src/codex-launcher-gui +++ b/src/codex-launcher-gui @@ -26,6 +26,12 @@ model_catalog_json = "" """ CHANGELOG = [ + ("3.9.8", "2026-05-25", [ + "Fix Codex Desktop sending wrong model (gpt-5.4-mini) instead of selected model", + "Proxy remaps Desktop forced models to user-selected model via CODEX_LAUNCHER_MODEL", + "Write review_model + wire_api + retries to config.toml for Desktop compatibility", + "send_json() globally catches BrokenPipeError — no more crashes on disconnect", + ]), ("3.9.7", "2026-05-25", [ "Forward real Codebuff error messages to user (not generic 429)", "Return HTTP 200 with Responses API format for rate limits so Codex displays message", @@ -936,15 +942,21 @@ def write_config_for_translated(endpoint, selected_model, proxy_port=8080): lines = [ f'model = "{_toml_safe(selected_model)}"\n', + f'review_model = "{_toml_safe(selected_model)}"\n', f'model_provider = "{_toml_safe(endpoint["name"])}"\n', f'model_catalog_json = "{mc_path}"\n', f'\n[model_providers."{endpoint["name"]}"]\n', f'name = "{_toml_safe(endpoint["name"])}"\n', f'base_url = "http://127.0.0.1:{proxy_port}"\n', f'experimental_bearer_token = "codex-launcher-local"\n', + f'wire_api = "responses"\n', + f'request_max_retries = 1\n', + f'stream_max_retries = 0\n', + f'stream_idle_timeout_ms = 600000\n', f'\n[profiles."{endpoint["name"]}"]\n', f'model_provider = "{_toml_safe(endpoint["name"])}"\n', f'model = "{_toml_safe(selected_model)}"\n', + f'review_model = "{_toml_safe(selected_model)}"\n', f'model_catalog_json = "{mc_path}"\n', f'service_tier = "fast"\n', f'approvals_reviewer = "user"\n', @@ -1737,7 +1749,7 @@ class LauncherWin(Gtk.Window): # header row hdr = Gtk.Box(spacing=8) vbox.pack_start(hdr, False, False, 0) - lbl = Gtk.Label(label="Codex Launcher v3.9.7") + lbl = Gtk.Label(label="Codex Launcher v3.9.8") lbl.set_use_markup(True) hdr.pack_start(lbl, False, False, 0) changelog_btn = Gtk.Button(label="Changelog") @@ -2434,6 +2446,7 @@ class LauncherWin(Gtk.Window): if needs_proxy: self.log("Starting translation proxy…") + os.environ["CODEX_LAUNCHER_MODEL"] = model try: proxy_port = _start_proxy_for(ep, self.log) except RuntimeError as e: @@ -3526,7 +3539,7 @@ class EditEndpointDialog(Gtk.Dialog): auth_url = "https://codebuff.com/api/auth/cli/code" body = json.dumps({"fingerprintId": fingerprint_id}).encode() req = urllib.request.Request(auth_url, data=body, - headers={"Content-Type": "application/json", "User-Agent": "codex-launcher/3.9.7"}) + headers={"Content-Type": "application/json", "User-Agent": "codex-launcher/3.9.8"}) resp = urllib.request.urlopen(req, timeout=30) data = json.loads(resp.read()) login_url = data.get("loginUrl", "") or data.get("login_url", "") @@ -3551,7 +3564,7 @@ class EditEndpointDialog(Gtk.Dialog): time.sleep(2) try: poll_req = urllib.request.Request(poll_url, - headers={"User-Agent": "codex-launcher/3.9.7"}) + headers={"User-Agent": "codex-launcher/3.9.8"}) poll_resp = urllib.request.urlopen(poll_req, timeout=10) poll_data = json.loads(poll_resp.read()) user = poll_data.get("user") diff --git a/src/translate-proxy.py b/src/translate-proxy.py index 1237438..942690d 100755 --- a/src/translate-proxy.py +++ b/src/translate-proxy.py @@ -335,7 +335,7 @@ def _codebuff_get_session(token, model): req = urllib.request.Request(url, data=body, headers={ "Content-Type": "application/json", "Authorization": f"Bearer {token}", - "User-Agent": "codex-launcher/3.9.7", + "User-Agent": "codex-launcher/3.9.8", "x-codebuff-model": model, }) try: @@ -383,7 +383,7 @@ def _codebuff_start_run(token, agent_id): req = urllib.request.Request(url, data=body, headers={ "Content-Type": "application/json", "Authorization": f"Bearer {token}", - "User-Agent": "codex-launcher/3.9.7", + "User-Agent": "codex-launcher/3.9.8", }) try: 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={ "Content-Type": "application/json", "Authorization": f"Bearer {token}", - "User-Agent": "codex-launcher/3.9.7", + "User-Agent": "codex-launcher/3.9.8", }) try: urllib.request.urlopen(req, timeout=10) @@ -4132,6 +4132,12 @@ class Handler(http.server.BaseHTTPRequestHandler): model = body.get("model", MODELS[0]["id"] if MODELS else "unknown") 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") if isinstance(input_data, list): for item in input_data: @@ -5305,7 +5311,7 @@ class Handler(http.server.BaseHTTPRequestHandler): headers = { "Content-Type": "application/json", "Authorization": f"Bearer {token}", - "User-Agent": "codex-launcher/3.9.7", + "User-Agent": "codex-launcher/3.9.8", "x-codebuff-model": model, } if instance_id: @@ -5442,10 +5448,7 @@ class Handler(http.server.BaseHTTPRequestHandler): }], "usage": {"input_tokens": 0, "output_tokens": 0, "total_tokens": 0}, } - try: - return self.send_json(200, result) - except (BrokenPipeError, ConnectionResetError, ConnectionAbortedError): - return + return self.send_json(200, result) 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) @@ -5474,7 +5477,7 @@ class Handler(http.server.BaseHTTPRequestHandler): if body.get("tool_choice"): chat_body["tool_choice"] = body["tool_choice"] target = f"{_CODEBUFF_API_URL}/api/v1/chat/completions" - headers = {"Content-Type": "application/json", "Authorization": f"Bearer {token}", "User-Agent": "codex-launcher/3.9.7", "x-codebuff-model": model} + headers = {"Content-Type": "application/json", "Authorization": f"Bearer {token}", "User-Agent": "codex-launcher/3.9.8", "x-codebuff-model": model} if 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) @@ -5830,12 +5833,15 @@ class Handler(http.server.BaseHTTPRequestHandler): store_response(rid, input_data, result.get("output", [])) def send_json(self, status, data): - body = json.dumps(data).encode() - self.send_response(status) - self.send_header("Content-Type", "application/json") - self.send_header("Content-Length", str(len(body))) - self.end_headers() - self.wfile.write(body) + try: + body = json.dumps(data).encode() + self.send_response(status) + self.send_header("Content-Type", "application/json") + self.send_header("Content-Length", str(len(body))) + self.end_headers() + 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): buf = bytearray()