v3.9.8 — Fix Desktop model leak, global BrokenPipeError protection
This commit is contained in:
12
CHANGELOG.md
12
CHANGELOG.md
@@ -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**
|
||||||
|
|||||||
BIN
codex-launcher_3.9.8_all.deb
Normal file
BIN
codex-launcher_3.9.8_all.deb
Normal file
Binary file not shown.
@@ -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"
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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):
|
||||||
|
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()
|
||||||
|
|||||||
Reference in New Issue
Block a user