diff --git a/CHANGELOG.md b/CHANGELOG.md
index 26736a9..8efdc3b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,15 @@
# Changelog
+## v2.5.1 (2026-05-20)
+
+- **Adaptive retry for transient errors** (429/502/503)
+ - Exponential backoff: 2s → 4s → 8s, up to 3 retries
+ - Works for both single-provider and BGP mode
+ - BGP routes retry before failing over to next route
+ - Connection errors (reset/broken pipe) also retried
+- **Proxy socket reuse** — no more `Address already in use` crashes on restart
+- **BGP startup log** shows route count and names
+
## v2.5.0 (2026-05-20)
- **AI BGP — Multi-provider routing with automatic failover**
diff --git a/codex-launcher_2.5.0_all.deb b/codex-launcher_2.5.0_all.deb
deleted file mode 100644
index bd99b79..0000000
Binary files a/codex-launcher_2.5.0_all.deb and /dev/null differ
diff --git a/codex-launcher_2.5.1_all.deb b/codex-launcher_2.5.1_all.deb
new file mode 100644
index 0000000..22ad5ac
Binary files /dev/null and b/codex-launcher_2.5.1_all.deb differ
diff --git a/src/codex-launcher-gui b/src/codex-launcher-gui
index 8cf1803..9fd8e7c 100755
--- a/src/codex-launcher-gui
+++ b/src/codex-launcher-gui
@@ -25,6 +25,12 @@ model_catalog_json = ""
"""
CHANGELOG = [
+ ("2.5.1", "2026-05-20", [
+ "Adaptive retry for 429/502/503 errors with exponential backoff",
+ "BGP routes also retry transient errors before failing over",
+ "Proxy socket reuse — no more 'Address already in use' crashes",
+ "BGP route count shown at proxy startup",
+ ]),
("2.5.0", "2026-05-20", [
"AI BGP — multi-provider routing with automatic failover",
"Create BGP pools with ordered routes from any configured endpoint",
@@ -629,7 +635,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 v2.5.0")
+ lbl = Gtk.Label(label="Codex Launcher v2.5.1")
lbl.set_use_markup(True)
hdr.pack_start(lbl, False, False, 0)
changelog_btn = Gtk.Button(label="Changelog")
diff --git a/src/translate-proxy.py b/src/translate-proxy.py
index 6d3997b..80e4e69 100755
--- a/src/translate-proxy.py
+++ b/src/translate-proxy.py
@@ -1041,14 +1041,30 @@ class Handler(http.server.BaseHTTPRequestHandler):
"Authorization": f"Bearer {effective_key}",
}, browser_ua=True)
print(f"[translate-proxy] POST {target} model={model} stream={stream} items={len(input_data) if isinstance(input_data,list) else 1}", file=sys.stderr)
- req = urllib.request.Request(target, data=json.dumps(chat_body).encode(), headers=fwd)
- try:
- upstream = urllib.request.urlopen(req, timeout=180)
- except urllib.error.HTTPError as e:
- err = e.read().decode()
- return self.send_json(e.code, {"error": {"type": "upstream_error", "message": err}})
- except Exception as e:
- return self.send_json(500, {"error": {"type": "proxy_error", "message": str(e)}})
+ chat_body_b = json.dumps(chat_body).encode()
+ max_retries = 3
+ for attempt in range(max_retries + 1):
+ req = urllib.request.Request(target, data=chat_body_b, headers=fwd)
+ try:
+ upstream = urllib.request.urlopen(req, timeout=180)
+ except urllib.error.HTTPError as e:
+ err_body = e.read().decode()
+ if e.code in (429, 502, 503) and attempt < max_retries:
+ wait = min(2 ** (attempt + 1), 15)
+ print(f"[translate-proxy] HTTP {e.code} (attempt {attempt+1}/{max_retries}), retrying in {wait}s: {err_body[:150]}", file=sys.stderr)
+ time.sleep(wait)
+ continue
+ return self.send_json(e.code, {"error": {"type": "upstream_error", "message": err_body}})
+ except (ConnectionResetError, ConnectionAbortedError, BrokenPipeError) as e:
+ if attempt < max_retries:
+ wait = min(2 ** (attempt + 1), 10)
+ print(f"[translate-proxy] connection error (attempt {attempt+1}/{max_retries}), retrying in {wait}s: {e}", file=sys.stderr)
+ time.sleep(wait)
+ continue
+ return self.send_json(502, {"error": {"type": "proxy_error", "message": str(e)}})
+ except Exception as e:
+ return self.send_json(500, {"error": {"type": "proxy_error", "message": str(e)}})
+ break
self._forward_oa_compat(upstream, stream, model, chat_body, body, input_data, fwd, target)
def _build_chat_body(self, model, messages, body, stream):
@@ -1108,18 +1124,37 @@ class Handler(http.server.BaseHTTPRequestHandler):
}, browser_ua=True)
print(f"[bgp] trying route '{route.get('name', r_url)}' model={r_model}", file=sys.stderr)
req = urllib.request.Request(target, data=json.dumps(chat_body).encode(), headers=fwd)
- try:
- upstream = urllib.request.urlopen(req, timeout=180)
- print(f"[bgp] route '{route.get('name', r_url)}' connected OK", file=sys.stderr)
- self._forward_oa_compat(upstream, stream, r_model, chat_body, body, input_data, fwd, target)
- return
- except urllib.error.HTTPError as e:
- err = e.read().decode()
- print(f"[bgp] route '{route.get('name', r_url)}' FAILED: HTTP {e.code}: {err[:200]}", file=sys.stderr)
- errors.append(f"{route.get('name','?')}: HTTP {e.code}")
- except Exception as e:
- print(f"[bgp] route '{route.get('name', r_url)}' FAILED: {e}", file=sys.stderr)
- errors.append(f"{route.get('name','?')}: {e}")
+ route_ok = False
+ for attempt in range(3):
+ try:
+ upstream = urllib.request.urlopen(req, timeout=180)
+ print(f"[bgp] route '{route.get('name', r_url)}' connected OK", file=sys.stderr)
+ self._forward_oa_compat(upstream, stream, r_model, chat_body, body, input_data, fwd, target)
+ return
+ except urllib.error.HTTPError as e:
+ err = e.read().decode()
+ if e.code in (429, 502, 503) and attempt < 2:
+ wait = min(2 ** (attempt + 1), 10)
+ print(f"[bgp] route '{route.get('name', r_url)}' HTTP {e.code}, retry {attempt+1}/2 in {wait}s", file=sys.stderr)
+ time.sleep(wait)
+ req = urllib.request.Request(target, data=json.dumps(chat_body).encode(), headers=fwd)
+ continue
+ print(f"[bgp] route '{route.get('name', r_url)}' FAILED: HTTP {e.code}: {err[:200]}", file=sys.stderr)
+ errors.append(f"{route.get('name','?')}: HTTP {e.code}")
+ break
+ except (ConnectionResetError, ConnectionAbortedError, BrokenPipeError) as e:
+ if attempt < 2:
+ wait = min(2 ** (attempt + 1), 8)
+ print(f"[bgp] route '{route.get('name', r_url)}' conn error, retry {attempt+1}/2 in {wait}s: {e}", file=sys.stderr)
+ time.sleep(wait)
+ req = urllib.request.Request(target, data=json.dumps(chat_body).encode(), headers=fwd)
+ continue
+ errors.append(f"{route.get('name','?')}: {e}")
+ break
+ except Exception as e:
+ print(f"[bgp] route '{route.get('name', r_url)}' FAILED: {e}", file=sys.stderr)
+ errors.append(f"{route.get('name','?')}: {e}")
+ break
print(f"[bgp] ALL ROUTES FAILED: {errors}", file=sys.stderr)
self.send_json(502, {"error": {"type": "bgp_all_routes_failed", "message": f"All BGP routes failed: {'; '.join(errors)}"}})
@@ -1440,8 +1475,12 @@ class Handler(http.server.BaseHTTPRequestHandler):
print(f"[translate-proxy] {BACKEND} {msg}", file=sys.stderr)
if __name__ == "__main__":
- server = http.server.HTTPServer(("127.0.0.1", PORT), Handler)
+ class ReusableHTTPServer(http.server.HTTPServer):
+ allow_reuse_address = True
+ server = ReusableHTTPServer(("127.0.0.1", PORT), Handler)
print(f"translate-proxy ({BACKEND}) listening on http://127.0.0.1:{PORT}", flush=True)
print(f"Target: {TARGET_URL}", flush=True)
print(f"Models: {[m['id'] for m in MODELS]}", flush=True)
+ if BGP_ROUTES:
+ print(f"BGP routes: {len(BGP_ROUTES)} ({[r.get('name','?') for r in BGP_ROUTES]})", flush=True)
server.serve_forever()