v2.5.1: adaptive retry for 429/502/503, socket reuse, BGP retry
- Exponential backoff retry (2s/4s/8s) for rate limits and transient errors - BGP routes retry before failing over to next route - Socket SO_REUSEADDR prevents 'Address already in use' crashes - Connection reset/broken pipe also retried - BGP route count shown at proxy startup
This commit is contained in:
10
CHANGELOG.md
10
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**
|
||||
|
||||
Binary file not shown.
BIN
codex-launcher_2.5.1_all.deb
Normal file
BIN
codex-launcher_2.5.1_all.deb
Normal file
Binary file not shown.
@@ -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="<b>Codex Launcher v2.5.0</b>")
|
||||
lbl = Gtk.Label(label="<b>Codex Launcher v2.5.1</b>")
|
||||
lbl.set_use_markup(True)
|
||||
hdr.pack_start(lbl, False, False, 0)
|
||||
changelog_btn = Gtk.Button(label="Changelog")
|
||||
|
||||
@@ -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)
|
||||
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 = e.read().decode()
|
||||
return self.send_json(e.code, {"error": {"type": "upstream_error", "message": err}})
|
||||
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,6 +1124,8 @@ 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)
|
||||
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)
|
||||
@@ -1115,11 +1133,28 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
||||
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()
|
||||
|
||||
Reference in New Issue
Block a user