v3.9.8 — Fix Desktop model leak, global BrokenPipeError protection

This commit is contained in:
Roman
2026-05-25 12:07:40 +04:00
Unverified
parent 490376d346
commit ea63eb3a3f
5 changed files with 53 additions and 22 deletions

View File

@@ -1,5 +1,17 @@
# Changelog # 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) ## v3.9.7 (2026-05-25)
**Codebuff Error Forwarding & Crash Fixes** **Codebuff Error Forwarding & Crash Fixes**

Binary file not shown.

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.9.7_all.deb" ]; then if [ -f "$SCRIPT_DIR/codex-launcher_3.9.8_all.deb" ]; then
echo "Installing codex-launcher_3.9.7_all.deb ..." echo "Installing codex-launcher_3.9.8_all.deb ..."
sudo dpkg -i "$SCRIPT_DIR/codex-launcher_3.9.7_all.deb" sudo dpkg -i "$SCRIPT_DIR/codex-launcher_3.9.8_all.deb"
echo "" 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 " 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,6 +26,12 @@ model_catalog_json = ""
""" """
CHANGELOG = [ 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", [ ("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",
@@ -936,15 +942,21 @@ 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',
@@ -1737,7 +1749,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.9.7</b>") lbl = Gtk.Label(label="<b>Codex Launcher v3.9.8</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")
@@ -2434,6 +2446,7 @@ 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:
@@ -3526,7 +3539,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.9.7"}) headers={"Content-Type": "application/json", "User-Agent": "codex-launcher/3.9.8"})
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", "")
@@ -3551,7 +3564,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.9.7"}) headers={"User-Agent": "codex-launcher/3.9.8"})
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")

View File

@@ -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.9.7", "User-Agent": "codex-launcher/3.9.8",
"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.9.7", "User-Agent": "codex-launcher/3.9.8",
}) })
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.9.7", "User-Agent": "codex-launcher/3.9.8",
}) })
try: try:
urllib.request.urlopen(req, timeout=10) 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") 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:
@@ -5305,7 +5311,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.9.7", "User-Agent": "codex-launcher/3.9.8",
"x-codebuff-model": model, "x-codebuff-model": model,
} }
if instance_id: if instance_id:
@@ -5442,10 +5448,7 @@ 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)
@@ -5474,7 +5477,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.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: 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)
@@ -5830,12 +5833,15 @@ 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):
body = json.dumps(data).encode() try:
self.send_response(status) body = json.dumps(data).encode()
self.send_header("Content-Type", "application/json") self.send_response(status)
self.send_header("Content-Length", str(len(body))) self.send_header("Content-Type", "application/json")
self.end_headers() self.send_header("Content-Length", str(len(body)))
self.wfile.write(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): def stream_buffered_events(self, event_iter, flush_interval=0.03, max_bytes=4096, on_event=None):
buf = bytearray() buf = bytearray()