v2.5.0: AI BGP multi-provider routing with automatic failover
- New AI BGP pool manager (create/edit/delete pools) - Each pool has ordered routes from any configured endpoint - Failover: tries primary, falls back to next route on error - Pools appear in endpoint dropdown with shuffle icon - Pool editor with route add/remove/reorder - Fixed TOML breakage from multi-line paste - Added OpenAdapter preset with 0G models
This commit is contained in:
14
CHANGELOG.md
14
CHANGELOG.md
@@ -1,5 +1,19 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## v2.5.0 (2026-05-20)
|
||||||
|
|
||||||
|
- **AI BGP — Multi-provider routing with automatic failover**
|
||||||
|
- New "AI BGP" button in main window → pool manager
|
||||||
|
- Create BGP pools with ordered routes from any configured endpoint
|
||||||
|
- Each route has its own endpoint URL, API key, model, and priority
|
||||||
|
- **Failover strategy**: tries primary route, automatically falls back to next on error/timeout
|
||||||
|
- BGP pools appear in endpoint dropdown with 🔀 icon
|
||||||
|
- Pool editor: add/remove/reorder routes, pick endpoint + model per route
|
||||||
|
- Up/down buttons for priority reordering
|
||||||
|
- Proxy logs `[bgp] trying route 'Name'` and `[bgp] route 'Name' FAILED` on fallback
|
||||||
|
- If all routes fail: returns 502 with detailed error per route
|
||||||
|
- Fixed TOML config breakage from multi-line paste in API key field (`_toml_safe()`)
|
||||||
|
|
||||||
## v2.4.0 (2026-05-20)
|
## v2.4.0 (2026-05-20)
|
||||||
|
|
||||||
- **Added OpenAdapter provider preset**
|
- **Added OpenAdapter provider preset**
|
||||||
|
|||||||
Binary file not shown.
BIN
codex-launcher_2.5.0_all.deb
Normal file
BIN
codex-launcher_2.5.0_all.deb
Normal file
Binary file not shown.
@@ -15,6 +15,7 @@ CONFIG_BAK = HOME / ".codex/config.toml.launcher-bak"
|
|||||||
CLEANUP = HOME / ".local/bin/cleanup-codex-stale.sh"
|
CLEANUP = HOME / ".local/bin/cleanup-codex-stale.sh"
|
||||||
PROXY = HOME / ".local/bin/translate-proxy.py"
|
PROXY = HOME / ".local/bin/translate-proxy.py"
|
||||||
ENDPOINTS_FILE = HOME / ".codex/endpoints.json"
|
ENDPOINTS_FILE = HOME / ".codex/endpoints.json"
|
||||||
|
BGP_POOLS_FILE = HOME / ".codex/bgp-pools.json"
|
||||||
LOG_DIR = HOME / ".cache/codex-desktop"
|
LOG_DIR = HOME / ".cache/codex-desktop"
|
||||||
LAUNCH_LOG = LOG_DIR / "launcher.log"
|
LAUNCH_LOG = LOG_DIR / "launcher.log"
|
||||||
PROXY_CONFIG_DIR = HOME / ".cache/codex-proxy"
|
PROXY_CONFIG_DIR = HOME / ".cache/codex-proxy"
|
||||||
@@ -24,6 +25,15 @@ model_catalog_json = ""
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
CHANGELOG = [
|
CHANGELOG = [
|
||||||
|
("2.5.0", "2026-05-20", [
|
||||||
|
"AI BGP — multi-provider routing with automatic failover",
|
||||||
|
"Create BGP pools with ordered routes from any configured endpoint",
|
||||||
|
"Each route uses its own endpoint URL, API key, and model",
|
||||||
|
"Failover strategy: tries primary, falls back on error/timeout",
|
||||||
|
"BGP pools appear in endpoint dropdown with shuffle icon",
|
||||||
|
"Up/down reordering for route priority in pool editor",
|
||||||
|
"Fixed TOML config breakage from multi-line paste in fields",
|
||||||
|
]),
|
||||||
("2.4.0", "2026-05-20", [
|
("2.4.0", "2026-05-20", [
|
||||||
"Added OpenAdapter provider preset (api.openadapter.in)",
|
"Added OpenAdapter provider preset (api.openadapter.in)",
|
||||||
"One API key access to 40+ models — GLM, DeepSeek, Kimi, Qwen, Claude, GPT, Gemini",
|
"One API key access to 40+ models — GLM, DeepSeek, Kimi, Qwen, Claude, GPT, Gemini",
|
||||||
@@ -345,6 +355,18 @@ def save_endpoints(data):
|
|||||||
ENDPOINTS_FILE.parent.mkdir(parents=True, exist_ok=True)
|
ENDPOINTS_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||||
ENDPOINTS_FILE.write_text(json.dumps(data, indent=2))
|
ENDPOINTS_FILE.write_text(json.dumps(data, indent=2))
|
||||||
|
|
||||||
|
def load_bgp_pools():
|
||||||
|
if BGP_POOLS_FILE.exists():
|
||||||
|
try:
|
||||||
|
return json.loads(BGP_POOLS_FILE.read_text())
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return {"pools": []}
|
||||||
|
|
||||||
|
def save_bgp_pools(data):
|
||||||
|
BGP_POOLS_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
BGP_POOLS_FILE.write_text(json.dumps(data, indent=2))
|
||||||
|
|
||||||
def get_endpoint(name):
|
def get_endpoint(name):
|
||||||
for e in load_endpoints()["endpoints"]:
|
for e in load_endpoints()["endpoints"]:
|
||||||
if e["name"] == name:
|
if e["name"] == name:
|
||||||
@@ -513,13 +535,15 @@ def _start_proxy_for(endpoint, logfn):
|
|||||||
pcfg_path = PROXY_CONFIG_DIR / f"proxy-{safe_name(endpoint['name'])}.json"
|
pcfg_path = PROXY_CONFIG_DIR / f"proxy-{safe_name(endpoint['name'])}.json"
|
||||||
pcfg_path.parent.mkdir(parents=True, exist_ok=True)
|
pcfg_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
pcfg_path.write_text(json.dumps(pcfg, indent=2))
|
pcfg_path.write_text(json.dumps(pcfg, indent=2))
|
||||||
|
_start_proxy_with_config(pcfg_path, logfn)
|
||||||
|
|
||||||
|
def _start_proxy_with_config(pcfg_path, logfn):
|
||||||
|
global _proxy_proc
|
||||||
_proxy_proc = subprocess.Popen(
|
_proxy_proc = subprocess.Popen(
|
||||||
["python3", str(PROXY), "--config", str(pcfg_path)],
|
["python3", str(PROXY), "--config", str(pcfg_path)],
|
||||||
stdout=subprocess.DEVNULL,
|
stdout=subprocess.DEVNULL,
|
||||||
preexec_fn=os.setsid,
|
preexec_fn=os.setsid,
|
||||||
)
|
)
|
||||||
|
|
||||||
for _ in range(30):
|
for _ in range(30):
|
||||||
try:
|
try:
|
||||||
urllib.request.urlopen("http://127.0.0.1:8080/v1/models", timeout=2)
|
urllib.request.urlopen("http://127.0.0.1:8080/v1/models", timeout=2)
|
||||||
@@ -605,12 +629,15 @@ 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 v2.4.0</b>")
|
lbl = Gtk.Label(label="<b>Codex Launcher v2.5.0</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")
|
||||||
changelog_btn.connect("clicked", lambda b: self._show_changelog())
|
changelog_btn.connect("clicked", lambda b: self._show_changelog())
|
||||||
hdr.pack_end(changelog_btn, False, False, 0)
|
hdr.pack_end(changelog_btn, False, False, 0)
|
||||||
|
bgp_btn = Gtk.Button(label="AI BGP")
|
||||||
|
bgp_btn.connect("clicked", lambda b: self._open_bgp())
|
||||||
|
hdr.pack_end(bgp_btn, False, False, 0)
|
||||||
mgr_btn = Gtk.Button(label="Manage Endpoints")
|
mgr_btn = Gtk.Button(label="Manage Endpoints")
|
||||||
mgr_btn.connect("clicked", lambda b: self._open_mgr())
|
mgr_btn.connect("clicked", lambda b: self._open_mgr())
|
||||||
hdr.pack_end(mgr_btn, False, False, 0)
|
hdr.pack_end(mgr_btn, False, False, 0)
|
||||||
@@ -844,7 +871,10 @@ class LauncherWin(Gtk.Window):
|
|||||||
names = [e["name"] for e in self._endpoints_data["endpoints"]]
|
names = [e["name"] for e in self._endpoints_data["endpoints"]]
|
||||||
for n in names:
|
for n in names:
|
||||||
self._combo.append_text(n)
|
self._combo.append_text(n)
|
||||||
if names:
|
bgp_names = [p["name"] for p in load_bgp_pools().get("pools", [])]
|
||||||
|
for n in bgp_names:
|
||||||
|
self._combo.append_text(f"🔀 {n}")
|
||||||
|
if names or bgp_names:
|
||||||
default = self._endpoints_data.get("default")
|
default = self._endpoints_data.get("default")
|
||||||
if default and default in names:
|
if default and default in names:
|
||||||
self._combo.set_active(names.index(default))
|
self._combo.set_active(names.index(default))
|
||||||
@@ -854,9 +884,26 @@ class LauncherWin(Gtk.Window):
|
|||||||
|
|
||||||
def _on_endpoint_changed(self):
|
def _on_endpoint_changed(self):
|
||||||
name = self._combo.get_active_text()
|
name = self._combo.get_active_text()
|
||||||
ep = get_endpoint(name) if name else None
|
is_bgp = name and name.startswith("🔀 ")
|
||||||
|
bgp_name = name[2:] if is_bgp else None
|
||||||
|
ep = get_endpoint(name) if name and not is_bgp else None
|
||||||
self._model_combo.remove_all()
|
self._model_combo.remove_all()
|
||||||
if ep:
|
if is_bgp:
|
||||||
|
pool = None
|
||||||
|
for p in load_bgp_pools().get("pools", []):
|
||||||
|
if p["name"] == bgp_name:
|
||||||
|
pool = p
|
||||||
|
break
|
||||||
|
if pool:
|
||||||
|
seen = set()
|
||||||
|
for r in pool.get("routes", []):
|
||||||
|
m = r.get("model", "")
|
||||||
|
if m and m not in seen:
|
||||||
|
self._model_combo.append_text(m)
|
||||||
|
seen.add(m)
|
||||||
|
if seen:
|
||||||
|
self._model_combo.set_active(0)
|
||||||
|
elif ep:
|
||||||
for m in ep.get("models", []):
|
for m in ep.get("models", []):
|
||||||
self._model_combo.append_text(m)
|
self._model_combo.append_text(m)
|
||||||
GLib.idle_add(self._select_default_model, ep)
|
GLib.idle_add(self._select_default_model, ep)
|
||||||
@@ -880,6 +927,15 @@ class LauncherWin(Gtk.Window):
|
|||||||
d = Gtk.MessageDialog(self, 0, Gtk.MessageType.ERROR, Gtk.ButtonsType.OK, f"Error: {e}")
|
d = Gtk.MessageDialog(self, 0, Gtk.MessageType.ERROR, Gtk.ButtonsType.OK, f"Error: {e}")
|
||||||
d.run(); d.destroy()
|
d.run(); d.destroy()
|
||||||
|
|
||||||
|
def _open_bgp(self):
|
||||||
|
try:
|
||||||
|
self._bgp_window = BGPPoolMgr(self)
|
||||||
|
self._bgp_window.connect("destroy", lambda *_: setattr(self, "_bgp_window", None))
|
||||||
|
except Exception as e:
|
||||||
|
import traceback; traceback.print_exc()
|
||||||
|
d = Gtk.MessageDialog(self, 0, Gtk.MessageType.ERROR, Gtk.ButtonsType.OK, f"Error: {e}")
|
||||||
|
d.run(); d.destroy()
|
||||||
|
|
||||||
def _backup_profile(self):
|
def _backup_profile(self):
|
||||||
chooser = Gtk.FileChooserDialog(
|
chooser = Gtk.FileChooserDialog(
|
||||||
title="Backup Codex Profile",
|
title="Backup Codex Profile",
|
||||||
@@ -1067,8 +1123,7 @@ class LauncherWin(Gtk.Window):
|
|||||||
|
|
||||||
def _launch(self, target):
|
def _launch(self, target):
|
||||||
name = self._combo.get_active_text()
|
name = self._combo.get_active_text()
|
||||||
ep = get_endpoint(name) if name else None
|
if not name:
|
||||||
if not ep:
|
|
||||||
self.log("ERROR: no endpoint selected")
|
self.log("ERROR: no endpoint selected")
|
||||||
return
|
return
|
||||||
model = self._model_combo.get_active_text()
|
model = self._model_combo.get_active_text()
|
||||||
@@ -1076,6 +1131,26 @@ class LauncherWin(Gtk.Window):
|
|||||||
self.log("ERROR: no model selected")
|
self.log("ERROR: no model selected")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
is_bgp = name.startswith("🔀 ")
|
||||||
|
if is_bgp:
|
||||||
|
pool_name = name[2:]
|
||||||
|
pool = None
|
||||||
|
for p in load_bgp_pools().get("pools", []):
|
||||||
|
if p["name"] == pool_name:
|
||||||
|
pool = p
|
||||||
|
break
|
||||||
|
if not pool:
|
||||||
|
self.log(f"ERROR: BGP pool '{pool_name}' not found")
|
||||||
|
return
|
||||||
|
self._set_busy(True)
|
||||||
|
self.log(f"=== 🔀 BGP: {pool_name} / {model} → {'Desktop' if target == 'desktop' else 'CLI'} ===")
|
||||||
|
threading.Thread(target=self._run_bgp, args=(pool, model, target), daemon=True).start()
|
||||||
|
return
|
||||||
|
|
||||||
|
ep = get_endpoint(name)
|
||||||
|
if not ep:
|
||||||
|
self.log("ERROR: endpoint not found")
|
||||||
|
return
|
||||||
self._set_busy(True)
|
self._set_busy(True)
|
||||||
self.log(f"=== {ep['name']} / {model} → {'Desktop' if target == 'desktop' else 'CLI'} ===")
|
self.log(f"=== {ep['name']} / {model} → {'Desktop' if target == 'desktop' else 'CLI'} ===")
|
||||||
threading.Thread(target=self._run, args=(ep, model, target), daemon=True).start()
|
threading.Thread(target=self._run, args=(ep, model, target), daemon=True).start()
|
||||||
@@ -1128,6 +1203,48 @@ class LauncherWin(Gtk.Window):
|
|||||||
self._set_busy(False)
|
self._set_busy(False)
|
||||||
self.log("Ready.")
|
self.log("Ready.")
|
||||||
|
|
||||||
|
def _run_bgp(self, pool, model, target):
|
||||||
|
try:
|
||||||
|
self.log("Cleaning up stale processes…")
|
||||||
|
_run_cleanup()
|
||||||
|
|
||||||
|
self.log(f"Starting BGP proxy with {len(pool.get('routes', []))} routes…")
|
||||||
|
bgp_ep = {
|
||||||
|
"name": pool["name"],
|
||||||
|
"backend_type": "openai-compat",
|
||||||
|
"base_url": "http://bgp.placeholder",
|
||||||
|
"api_key": "",
|
||||||
|
"default_model": model,
|
||||||
|
"models": list(dict.fromkeys(r.get("model", model) for r in pool.get("routes", []))),
|
||||||
|
}
|
||||||
|
pcfg = {
|
||||||
|
"port": 8080,
|
||||||
|
"backend_type": "openai-compat",
|
||||||
|
"target_url": "http://bgp.placeholder",
|
||||||
|
"api_key": "",
|
||||||
|
"bgp_routes": pool.get("routes", []),
|
||||||
|
"models": [{"id": m, "object": "model", "created": 1700000000, "owned_by": "bgp"} for m in bgp_ep["models"]],
|
||||||
|
}
|
||||||
|
pcfg_path = PROXY_CONFIG_DIR / f"proxy-{safe_name(pool['name'])}.json"
|
||||||
|
pcfg_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
pcfg_path.write_text(json.dumps(pcfg, indent=2))
|
||||||
|
_start_proxy_with_config(pcfg_path, self.log)
|
||||||
|
|
||||||
|
write_config_for_translated(bgp_ep, model)
|
||||||
|
|
||||||
|
if target == "desktop":
|
||||||
|
self._launch_desktop(bgp_ep, model)
|
||||||
|
else:
|
||||||
|
self._launch_cli(bgp_ep, model)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.log(f"ERROR: {e}")
|
||||||
|
finally:
|
||||||
|
_stop_proxy()
|
||||||
|
restore_config()
|
||||||
|
self._set_busy(False)
|
||||||
|
self.log("Ready.")
|
||||||
|
|
||||||
def _run_codex_default(self, target):
|
def _run_codex_default(self, target):
|
||||||
try:
|
try:
|
||||||
self.log("Cleaning up stale processes…")
|
self.log("Cleaning up stale processes…")
|
||||||
@@ -1967,6 +2084,381 @@ class EditEndpointDialog(Gtk.Dialog):
|
|||||||
# Entry point
|
# Entry point
|
||||||
# ═══════════════════════════════════════════════════════════════════
|
# ═══════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════
|
||||||
|
# BGP Pool Manager
|
||||||
|
# ═══════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
class BGPPoolMgr(Gtk.Window):
|
||||||
|
def __init__(self, parent):
|
||||||
|
super().__init__(title="AI BGP — Pool Manager")
|
||||||
|
self.set_transient_for(parent)
|
||||||
|
self.set_default_size(620, 440)
|
||||||
|
self._parent = parent
|
||||||
|
|
||||||
|
vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8)
|
||||||
|
vbox.set_margin_start(12)
|
||||||
|
vbox.set_margin_end(12)
|
||||||
|
vbox.set_margin_top(12)
|
||||||
|
vbox.set_margin_bottom(12)
|
||||||
|
self.add(vbox)
|
||||||
|
|
||||||
|
hdr = Gtk.Box(spacing=8)
|
||||||
|
vbox.pack_start(hdr, False, False, 0)
|
||||||
|
hdr.pack_start(Gtk.Label(label="<b>AI BGP Pools</b> — multi-provider routing with automatic failover", use_markup=True), False, False, 0)
|
||||||
|
|
||||||
|
self._store = Gtk.ListStore(str, str, str)
|
||||||
|
self._tree = Gtk.TreeView(model=self._store)
|
||||||
|
for i, (title, w) in enumerate([("Pool Name", 200), ("Routes", 250), ("Strategy", 100)]):
|
||||||
|
r = Gtk.CellRendererText()
|
||||||
|
c = Gtk.TreeViewColumn(title, r, text=i)
|
||||||
|
c.set_min_width(w)
|
||||||
|
self._tree.append_column(c)
|
||||||
|
self._tree.set_headers_visible(True)
|
||||||
|
sw = Gtk.ScrolledWindow()
|
||||||
|
sw.add(self._tree)
|
||||||
|
vbox.pack_start(sw, True, True, 0)
|
||||||
|
|
||||||
|
sel = self._tree.get_selection()
|
||||||
|
sel.connect("changed", lambda *_: self._on_select())
|
||||||
|
|
||||||
|
bbox = Gtk.Box(spacing=8)
|
||||||
|
vbox.pack_start(bbox, False, False, 0)
|
||||||
|
self._add_btn = Gtk.Button(label="Create Pool")
|
||||||
|
self._add_btn.connect("clicked", lambda b: self._add_pool())
|
||||||
|
bbox.pack_start(self._add_btn, True, True, 0)
|
||||||
|
self._edit_btn = Gtk.Button(label="Edit Pool")
|
||||||
|
self._edit_btn.connect("clicked", lambda b: self._edit_pool())
|
||||||
|
self._edit_btn.set_sensitive(False)
|
||||||
|
bbox.pack_start(self._edit_btn, True, True, 0)
|
||||||
|
self._del_btn = Gtk.Button(label="Delete Pool")
|
||||||
|
self._del_btn.connect("clicked", lambda b: self._del_pool())
|
||||||
|
self._del_btn.set_sensitive(False)
|
||||||
|
bbox.pack_start(self._del_btn, True, True, 0)
|
||||||
|
close_btn = Gtk.Button(label="Close")
|
||||||
|
close_btn.connect("clicked", lambda b: self.destroy())
|
||||||
|
bbox.pack_start(close_btn, True, True, 0)
|
||||||
|
|
||||||
|
self._rebuild()
|
||||||
|
self.show_all()
|
||||||
|
|
||||||
|
def _rebuild(self):
|
||||||
|
self._store.clear()
|
||||||
|
for pool in load_bgp_pools().get("pools", []):
|
||||||
|
routes_str = " → ".join(f'{r.get("name","?")}/{r.get("model","?")}' for r in pool.get("routes", []))
|
||||||
|
self._store.append([pool["name"], routes_str, pool.get("strategy", "failover")])
|
||||||
|
|
||||||
|
def _selected_name(self):
|
||||||
|
sel = self._tree.get_selection()
|
||||||
|
m, i = sel.get_selected()
|
||||||
|
return self._store[i][0] if i else None
|
||||||
|
|
||||||
|
def _on_select(self):
|
||||||
|
name = self._selected_name()
|
||||||
|
self._edit_btn.set_sensitive(bool(name))
|
||||||
|
self._del_btn.set_sensitive(bool(name))
|
||||||
|
|
||||||
|
def _add_pool(self):
|
||||||
|
d = BGPPoolEditDialog(self, None)
|
||||||
|
d.connect("response", lambda *_: self._rebuild())
|
||||||
|
|
||||||
|
def _edit_pool(self):
|
||||||
|
name = self._selected_name()
|
||||||
|
if name:
|
||||||
|
d = BGPPoolEditDialog(self, name)
|
||||||
|
d.connect("response", lambda *_: self._rebuild())
|
||||||
|
|
||||||
|
def _del_pool(self):
|
||||||
|
name = self._selected_name()
|
||||||
|
if not name:
|
||||||
|
return
|
||||||
|
d = Gtk.MessageDialog(self, 0, Gtk.MessageType.QUESTION, Gtk.ButtonsType.YES_NO,
|
||||||
|
f'Delete BGP pool "{name}"?')
|
||||||
|
r = d.run(); d.destroy()
|
||||||
|
if r != Gtk.ResponseType.YES:
|
||||||
|
return
|
||||||
|
data = load_bgp_pools()
|
||||||
|
data["pools"] = [p for p in data["pools"] if p["name"] != name]
|
||||||
|
save_bgp_pools(data)
|
||||||
|
self._rebuild()
|
||||||
|
self._parent._on_endpoints_updated()
|
||||||
|
|
||||||
|
|
||||||
|
class BGPPoolEditDialog(Gtk.Dialog):
|
||||||
|
def __init__(self, parent, existing_name):
|
||||||
|
title = "Edit BGP Pool" if existing_name else "Create BGP Pool"
|
||||||
|
Gtk.Dialog.__init__(self, title=title, parent=parent, modal=True)
|
||||||
|
self.add_button("Cancel", Gtk.ResponseType.CANCEL)
|
||||||
|
self.add_button("Save", Gtk.ResponseType.OK)
|
||||||
|
self.set_default_size(580, 480)
|
||||||
|
|
||||||
|
self._existing_name = existing_name
|
||||||
|
self._parent_mgr = parent
|
||||||
|
|
||||||
|
data = load_bgp_pools()
|
||||||
|
pool = None
|
||||||
|
if existing_name:
|
||||||
|
for p in data.get("pools", []):
|
||||||
|
if p["name"] == existing_name:
|
||||||
|
pool = p
|
||||||
|
break
|
||||||
|
if not pool:
|
||||||
|
pool = {"name": "", "strategy": "failover", "routes": []}
|
||||||
|
|
||||||
|
area = self.get_content_area()
|
||||||
|
area.set_margin_start(12)
|
||||||
|
area.set_margin_end(12)
|
||||||
|
area.set_margin_top(12)
|
||||||
|
area.set_margin_bottom(12)
|
||||||
|
area.set_spacing(8)
|
||||||
|
|
||||||
|
grid = Gtk.Grid(column_spacing=8, row_spacing=6)
|
||||||
|
area.pack_start(grid, False, False, 0)
|
||||||
|
|
||||||
|
grid.attach(Gtk.Label(label="Pool Name:", xalign=1), 0, 0, 1, 1)
|
||||||
|
self._entry_name = Gtk.Entry(text=pool["name"])
|
||||||
|
grid.attach(self._entry_name, 1, 0, 1, 1)
|
||||||
|
|
||||||
|
grid.attach(Gtk.Label(label="Strategy:", xalign=1), 0, 1, 1, 1)
|
||||||
|
self._combo_strategy = Gtk.ComboBoxText()
|
||||||
|
self._combo_strategy.append("failover", "Failover (try primary, fall back on error)")
|
||||||
|
self._combo_strategy.append("race", "Race (send to all, return fastest)")
|
||||||
|
self._combo_strategy.set_active_id(pool.get("strategy", "failover"))
|
||||||
|
grid.attach(self._combo_strategy, 1, 1, 1, 1)
|
||||||
|
|
||||||
|
area.pack_start(Gtk.Label(label="<b>Routes</b> (drag to reorder priority)", use_markup=True, xalign=0), False, False, 8)
|
||||||
|
|
||||||
|
self._route_store = Gtk.ListStore(str, str, str, str, str, str)
|
||||||
|
for r in pool.get("routes", []):
|
||||||
|
self._route_store.append([
|
||||||
|
r.get("name", ""), r.get("endpoint_name", ""),
|
||||||
|
r.get("target_url", ""), r.get("api_key", ""),
|
||||||
|
r.get("model", ""), str(r.get("priority", 99))
|
||||||
|
])
|
||||||
|
|
||||||
|
self._route_tree = Gtk.TreeView(model=self._route_store)
|
||||||
|
for i, (title, w) in enumerate([
|
||||||
|
("Route Name", 120), ("Endpoint", 120), ("URL", 150),
|
||||||
|
("API Key", 80), ("Model", 120), ("Priority", 60)
|
||||||
|
]):
|
||||||
|
renderer = Gtk.CellRendererText()
|
||||||
|
renderer.set_property("editable", False)
|
||||||
|
col = Gtk.TreeViewColumn(title, renderer, text=i)
|
||||||
|
col.set_min_width(w)
|
||||||
|
col.set_resizable(True)
|
||||||
|
self._route_tree.append_column(col)
|
||||||
|
self._route_tree.set_headers_visible(True)
|
||||||
|
sw = Gtk.ScrolledWindow()
|
||||||
|
sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
|
||||||
|
sw.add(self._route_tree)
|
||||||
|
sw.set_min_content_height(200)
|
||||||
|
area.pack_start(sw, True, True, 0)
|
||||||
|
|
||||||
|
bbox = Gtk.Box(spacing=6)
|
||||||
|
area.pack_start(bbox, False, False, 0)
|
||||||
|
add_r = Gtk.Button(label="Add Route")
|
||||||
|
add_r.connect("clicked", lambda b: self._add_route())
|
||||||
|
bbox.pack_start(add_r, True, True, 0)
|
||||||
|
edit_r = Gtk.Button(label="Edit Route")
|
||||||
|
edit_r.connect("clicked", lambda b: self._edit_route())
|
||||||
|
bbox.pack_start(edit_r, True, True, 0)
|
||||||
|
rm_r = Gtk.Button(label="Remove Route")
|
||||||
|
rm_r.connect("clicked", lambda b: self._remove_route())
|
||||||
|
bbox.pack_start(rm_r, True, True, 0)
|
||||||
|
up_r = Gtk.Button(label="↑ Up")
|
||||||
|
up_r.connect("clicked", lambda b: self._move_route(-1))
|
||||||
|
bbox.pack_start(up_r, True, True, 0)
|
||||||
|
down_r = Gtk.Button(label="↓ Down")
|
||||||
|
down_r.connect("clicked", lambda b: self._move_route(1))
|
||||||
|
bbox.pack_start(down_r, True, True, 0)
|
||||||
|
|
||||||
|
self.show_all()
|
||||||
|
|
||||||
|
if self.run() == Gtk.ResponseType.OK:
|
||||||
|
self._save()
|
||||||
|
|
||||||
|
self.destroy()
|
||||||
|
|
||||||
|
def _save(self):
|
||||||
|
name = self._entry_name.get_text().strip()
|
||||||
|
if not name:
|
||||||
|
return
|
||||||
|
strategy = self._combo_strategy.get_active_id() or "failover"
|
||||||
|
routes = []
|
||||||
|
for i, row in enumerate(self._route_store):
|
||||||
|
if not row[2]:
|
||||||
|
continue
|
||||||
|
routes.append({
|
||||||
|
"name": row[0] or f"Route {i+1}",
|
||||||
|
"endpoint_name": row[1],
|
||||||
|
"target_url": row[2],
|
||||||
|
"api_key": row[3],
|
||||||
|
"model": row[4],
|
||||||
|
"priority": i + 1,
|
||||||
|
"reasoning_enabled": True,
|
||||||
|
"reasoning_effort": "medium",
|
||||||
|
})
|
||||||
|
data = load_bgp_pools()
|
||||||
|
if self._existing_name:
|
||||||
|
data["pools"] = [p for p in data["pools"] if p["name"] != self._existing_name]
|
||||||
|
data["pools"].append({"name": name, "strategy": strategy, "routes": routes})
|
||||||
|
save_bgp_pools(data)
|
||||||
|
self._parent_mgr._parent._on_endpoints_updated()
|
||||||
|
|
||||||
|
def _add_route(self):
|
||||||
|
endpoints = load_endpoints().get("endpoints", [])
|
||||||
|
if not endpoints:
|
||||||
|
d = Gtk.MessageDialog(self, 0, Gtk.MessageType.INFO, Gtk.ButtonsType.OK,
|
||||||
|
"No endpoints configured. Add endpoints in Manage Endpoints first.")
|
||||||
|
d.run(); d.destroy()
|
||||||
|
return
|
||||||
|
d = BGPRouteDialog(self, endpoints, None)
|
||||||
|
if d.result:
|
||||||
|
r = d.result
|
||||||
|
self._route_store.append([
|
||||||
|
r.get("name", ""), r.get("endpoint_name", ""),
|
||||||
|
r.get("target_url", ""), r.get("api_key", ""),
|
||||||
|
r.get("model", ""), str(r.get("priority", 99))
|
||||||
|
])
|
||||||
|
|
||||||
|
def _edit_route(self):
|
||||||
|
sel = self._route_tree.get_selection()
|
||||||
|
m, i = sel.get_selected()
|
||||||
|
if not i:
|
||||||
|
return
|
||||||
|
endpoints = load_endpoints().get("endpoints", [])
|
||||||
|
existing = {
|
||||||
|
"name": m[i][0], "endpoint_name": m[i][1],
|
||||||
|
"target_url": m[i][2], "api_key": m[i][3],
|
||||||
|
"model": m[i][4], "priority": int(m[i][5]) if m[i][5] else 99,
|
||||||
|
}
|
||||||
|
d = BGPRouteDialog(self, endpoints, existing)
|
||||||
|
if d.result:
|
||||||
|
r = d.result
|
||||||
|
m[i][0] = r.get("name", "")
|
||||||
|
m[i][1] = r.get("endpoint_name", "")
|
||||||
|
m[i][2] = r.get("target_url", "")
|
||||||
|
m[i][3] = r.get("api_key", "")
|
||||||
|
m[i][4] = r.get("model", "")
|
||||||
|
m[i][5] = str(r.get("priority", 99))
|
||||||
|
|
||||||
|
def _remove_route(self):
|
||||||
|
sel = self._route_tree.get_selection()
|
||||||
|
m, i = sel.get_selected()
|
||||||
|
if i:
|
||||||
|
self._route_store.remove(i)
|
||||||
|
|
||||||
|
def _move_route(self, direction):
|
||||||
|
sel = self._route_tree.get_selection()
|
||||||
|
m, i = sel.get_selected()
|
||||||
|
if not i:
|
||||||
|
return
|
||||||
|
path = m.get_path(i)
|
||||||
|
idx = path.get_indices()[0]
|
||||||
|
new_idx = idx + direction
|
||||||
|
if new_idx < 0 or new_idx >= len(self._route_store):
|
||||||
|
return
|
||||||
|
row_data = [m[idx][c] for c in range(6)]
|
||||||
|
self._route_store.remove(m.get_iter(Gtk.TreePath(idx)))
|
||||||
|
new_iter = self._route_store.insert(new_idx)
|
||||||
|
for c, v in enumerate(row_data):
|
||||||
|
self._route_store.set_value(new_iter, c, v)
|
||||||
|
|
||||||
|
|
||||||
|
class BGPRouteDialog(Gtk.Dialog):
|
||||||
|
def __init__(self, parent, endpoints, existing):
|
||||||
|
Gtk.Dialog.__init__(self, title="BGP Route", parent=parent, modal=True)
|
||||||
|
self.add_button("Cancel", Gtk.ResponseType.CANCEL)
|
||||||
|
self.add_button("OK", Gtk.ResponseType.OK)
|
||||||
|
self.set_default_size(440, 300)
|
||||||
|
self.result = None
|
||||||
|
|
||||||
|
area = self.get_content_area()
|
||||||
|
area.set_margin_start(12)
|
||||||
|
area.set_margin_end(12)
|
||||||
|
area.set_margin_top(12)
|
||||||
|
area.set_margin_bottom(12)
|
||||||
|
area.set_spacing(6)
|
||||||
|
|
||||||
|
grid = Gtk.Grid(column_spacing=8, row_spacing=6)
|
||||||
|
area.pack_start(grid, False, False, 0)
|
||||||
|
|
||||||
|
def add_row(row, label, widget):
|
||||||
|
grid.attach(Gtk.Label(label=label, xalign=1), 0, row, 1, 1)
|
||||||
|
grid.attach(widget, 1, row, 1, 1)
|
||||||
|
|
||||||
|
self._entry_name = Gtk.Entry(text=existing.get("name", "") if existing else "")
|
||||||
|
add_row(0, "Route Name:", self._entry_name)
|
||||||
|
|
||||||
|
self._combo_ep = Gtk.ComboBoxText()
|
||||||
|
ep_names = [e["name"] for e in endpoints]
|
||||||
|
for en in ep_names:
|
||||||
|
self._combo_ep.append(en, en)
|
||||||
|
if existing and existing.get("endpoint_name") in ep_names:
|
||||||
|
self._combo_ep.set_active_id(existing["endpoint_name"])
|
||||||
|
elif ep_names:
|
||||||
|
self._combo_ep.set_active(0)
|
||||||
|
self._combo_ep.connect("changed", lambda b: self._on_ep_changed(endpoints))
|
||||||
|
add_row(1, "Endpoint:", self._combo_ep)
|
||||||
|
|
||||||
|
self._entry_url = Gtk.Entry()
|
||||||
|
add_row(2, "URL:", self._entry_url)
|
||||||
|
|
||||||
|
self._entry_key = Gtk.Entry()
|
||||||
|
self._entry_key.set_visibility(False)
|
||||||
|
add_row(3, "API Key:", self._entry_key)
|
||||||
|
|
||||||
|
self._combo_model = Gtk.ComboBoxText()
|
||||||
|
add_row(4, "Model:", self._combo_model)
|
||||||
|
|
||||||
|
if existing:
|
||||||
|
self._entry_url.set_text(existing.get("target_url", ""))
|
||||||
|
self._entry_key.set_text(existing.get("api_key", ""))
|
||||||
|
self._on_ep_changed(endpoints)
|
||||||
|
if existing and existing.get("model"):
|
||||||
|
self._combo_model.set_active_id(existing["model"])
|
||||||
|
|
||||||
|
self.show_all()
|
||||||
|
if self.run() == Gtk.ResponseType.OK:
|
||||||
|
ep_name = self._combo_ep.get_active_text() or ""
|
||||||
|
ep = None
|
||||||
|
for e in endpoints:
|
||||||
|
if e["name"] == ep_name:
|
||||||
|
ep = e
|
||||||
|
break
|
||||||
|
self.result = {
|
||||||
|
"name": self._entry_name.get_text().strip() or ep_name,
|
||||||
|
"endpoint_name": ep_name,
|
||||||
|
"target_url": self._entry_url.get_text().strip(),
|
||||||
|
"api_key": self._entry_key.get_text().strip(),
|
||||||
|
"model": self._combo_model.get_active_text() or "",
|
||||||
|
"priority": 99,
|
||||||
|
}
|
||||||
|
if ep:
|
||||||
|
self.result["reasoning_enabled"] = ep.get("reasoning_enabled", True)
|
||||||
|
self.result["reasoning_effort"] = ep.get("reasoning_effort", "medium")
|
||||||
|
self.result["oauth_provider"] = ep.get("oauth_provider", "")
|
||||||
|
self.destroy()
|
||||||
|
|
||||||
|
def _on_ep_changed(self, endpoints):
|
||||||
|
ep_name = self._combo_ep.get_active_text()
|
||||||
|
ep = None
|
||||||
|
for e in endpoints:
|
||||||
|
if e["name"] == ep_name:
|
||||||
|
ep = e
|
||||||
|
break
|
||||||
|
if ep:
|
||||||
|
self._entry_url.set_text(normalize_base_url(ep.get("base_url", "")))
|
||||||
|
self._entry_key.set_text(ep.get("api_key", ""))
|
||||||
|
self._combo_model.remove_all()
|
||||||
|
for m in ep.get("models", []):
|
||||||
|
mid = normalize_model_id(m) if m else ""
|
||||||
|
self._combo_model.append(mid, m)
|
||||||
|
if ep.get("default_model"):
|
||||||
|
self._combo_model.set_active_id(normalize_model_id(ep["default_model"]))
|
||||||
|
elif len(ep.get("models", [])) > 0:
|
||||||
|
self._combo_model.set_active(0)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
for d in [LOG_DIR, PROXY_CONFIG_DIR]:
|
for d in [LOG_DIR, PROXY_CONFIG_DIR]:
|
||||||
d.mkdir(parents=True, exist_ok=True)
|
d.mkdir(parents=True, exist_ok=True)
|
||||||
|
|||||||
@@ -84,23 +84,35 @@ MODELS = CONFIG["models"]
|
|||||||
CC_VERSION = CONFIG.get("cc_version", "")
|
CC_VERSION = CONFIG.get("cc_version", "")
|
||||||
REASONING_ENABLED = CONFIG.get("reasoning_enabled", True)
|
REASONING_ENABLED = CONFIG.get("reasoning_enabled", True)
|
||||||
REASONING_EFFORT = CONFIG.get("reasoning_effort", "medium")
|
REASONING_EFFORT = CONFIG.get("reasoning_effort", "medium")
|
||||||
|
BGP_ROUTES = CONFIG.get("bgp_routes", [])
|
||||||
|
BGP_MODELS = []
|
||||||
|
for _r in BGP_ROUTES:
|
||||||
|
for _m in _r.get("models", [{"id": _r.get("model", "unknown")}]):
|
||||||
|
if _m.get("id", _m) not in BGP_MODELS:
|
||||||
|
BGP_MODELS.append(_m.get("id", _m) if isinstance(_m, dict) else _m)
|
||||||
|
if BGP_ROUTES and not MODELS:
|
||||||
|
MODELS = [{"id": m, "object": "model", "created": 1700000000, "owned_by": "bgp"} for m in BGP_MODELS]
|
||||||
|
CONFIG["models"] = MODELS
|
||||||
|
|
||||||
def _refresh_oauth_token():
|
def _refresh_oauth_token():
|
||||||
if OAUTH_PROVIDER != "google":
|
return _refresh_oauth_token_for(API_KEY, OAUTH_PROVIDER)
|
||||||
return API_KEY
|
|
||||||
|
def _refresh_oauth_token_for(api_key, oauth_provider):
|
||||||
|
if oauth_provider != "google":
|
||||||
|
return api_key
|
||||||
token_path = os.path.join(os.path.expanduser("~"), ".cache", "codex-proxy", "google-oauth-token.json")
|
token_path = os.path.join(os.path.expanduser("~"), ".cache", "codex-proxy", "google-oauth-token.json")
|
||||||
if not os.path.exists(token_path):
|
if not os.path.exists(token_path):
|
||||||
return API_KEY
|
return api_key
|
||||||
try:
|
try:
|
||||||
with open(token_path) as f:
|
with open(token_path) as f:
|
||||||
tokens = json.load(f)
|
tokens = json.load(f)
|
||||||
if tokens.get("expires_at", 0) > time.time() + 60:
|
if tokens.get("expires_at", 0) > time.time() + 60:
|
||||||
return tokens.get("access_token", API_KEY)
|
return tokens.get("access_token", api_key)
|
||||||
client_id = tokens.get("client_id", "")
|
client_id = tokens.get("client_id", "")
|
||||||
client_secret = tokens.get("client_secret", "")
|
client_secret = tokens.get("client_secret", "")
|
||||||
refresh_token = tokens.get("refresh_token", "")
|
refresh_token = tokens.get("refresh_token", "")
|
||||||
if not all([client_id, client_secret, refresh_token]):
|
if not all([client_id, client_secret, refresh_token]):
|
||||||
return tokens.get("access_token", API_KEY)
|
return tokens.get("access_token", api_key)
|
||||||
print("[oauth] refreshing Google access token...", file=sys.stderr)
|
print("[oauth] refreshing Google access token...", file=sys.stderr)
|
||||||
data = urllib.parse.urlencode({
|
data = urllib.parse.urlencode({
|
||||||
"client_id": client_id, "client_secret": client_secret,
|
"client_id": client_id, "client_secret": client_secret,
|
||||||
@@ -1006,7 +1018,6 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
|||||||
def _handle_openai_compat(self, body, model, stream):
|
def _handle_openai_compat(self, body, model, stream):
|
||||||
input_data = body.get("input", "")
|
input_data = body.get("input", "")
|
||||||
|
|
||||||
# Adaptive: proactively compact if above learned Crof limit
|
|
||||||
crof_limit = _crof_item_limit(model)
|
crof_limit = _crof_item_limit(model)
|
||||||
if isinstance(input_data, list) and len(input_data) > crof_limit:
|
if isinstance(input_data, list) and len(input_data) > crof_limit:
|
||||||
print(f"[crof-adaptive] proactive compact: {len(input_data)} items > limit {crof_limit}", file=sys.stderr)
|
print(f"[crof-adaptive] proactive compact: {len(input_data)} items > limit {crof_limit}", file=sys.stderr)
|
||||||
@@ -1018,6 +1029,29 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
|||||||
instructions = body.get("instructions", "").strip()
|
instructions = body.get("instructions", "").strip()
|
||||||
if instructions:
|
if instructions:
|
||||||
messages.insert(0, {"role": "system", "content": instructions})
|
messages.insert(0, {"role": "system", "content": instructions})
|
||||||
|
|
||||||
|
if BGP_ROUTES:
|
||||||
|
self._handle_bgp(body, model, stream, messages, input_data)
|
||||||
|
else:
|
||||||
|
chat_body = self._build_chat_body(model, messages, body, stream)
|
||||||
|
target = upstream_target(TARGET_URL, "/chat/completions")
|
||||||
|
effective_key = _refresh_oauth_token()
|
||||||
|
fwd = forwarded_headers(self.headers, {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"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)}})
|
||||||
|
self._forward_oa_compat(upstream, stream, model, chat_body, body, input_data, fwd, target)
|
||||||
|
|
||||||
|
def _build_chat_body(self, model, messages, body, stream):
|
||||||
chat_body = {"model": model, "messages": messages}
|
chat_body = {"model": model, "messages": messages}
|
||||||
for k in ("temperature", "top_p"):
|
for k in ("temperature", "top_p"):
|
||||||
if k in body:
|
if k in body:
|
||||||
@@ -1034,31 +1068,63 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
|||||||
chat_body["reasoning_effort"] = "none"
|
chat_body["reasoning_effort"] = "none"
|
||||||
else:
|
else:
|
||||||
chat_body["reasoning_effort"] = REASONING_EFFORT
|
chat_body["reasoning_effort"] = REASONING_EFFORT
|
||||||
|
return chat_body
|
||||||
|
|
||||||
target = upstream_target(TARGET_URL, "/chat/completions")
|
def _handle_bgp(self, body, model, stream, messages, input_data):
|
||||||
effective_key = _refresh_oauth_token()
|
routes = sorted(BGP_ROUTES, key=lambda r: r.get("priority", 99))
|
||||||
fwd = forwarded_headers(self.headers, {
|
errors = []
|
||||||
"Content-Type": "application/json",
|
for route in routes:
|
||||||
"Authorization": f"Bearer {effective_key}",
|
r_model = route.get("model", model)
|
||||||
}, browser_ua=True)
|
r_url = route["target_url"].rstrip("/")
|
||||||
print(f"[translate-proxy] POST {target} model={model} stream={stream} items={len(input_data) if isinstance(input_data,list) else 1} ua={fwd.get('User-Agent','')[:50]}", file=sys.stderr)
|
r_key = route.get("api_key", "")
|
||||||
|
r_reasoning = route.get("reasoning_enabled", True)
|
||||||
|
r_effort = route.get("reasoning_effort", "medium")
|
||||||
|
r_oauth = route.get("oauth_provider", "")
|
||||||
|
|
||||||
req = urllib.request.Request(
|
chat_body = dict(messages=list(messages))
|
||||||
target,
|
chat_body["model"] = r_model
|
||||||
data=json.dumps(chat_body).encode(),
|
for k in ("temperature", "top_p"):
|
||||||
headers=fwd,
|
if k in body:
|
||||||
)
|
chat_body[k] = body[k]
|
||||||
self._forward_oa_compat(req, stream, model, chat_body, body, input_data, fwd, target, tools)
|
chat_body["max_tokens"] = max(body.get("max_output_tokens", 0), 64000)
|
||||||
|
tools = oa_convert_tools(body.get("tools"))
|
||||||
|
if tools:
|
||||||
|
chat_body["tools"] = tools
|
||||||
|
if body.get("tool_choice"):
|
||||||
|
chat_body["tool_choice"] = body["tool_choice"]
|
||||||
|
chat_body["stream"] = stream
|
||||||
|
if not r_reasoning or r_effort == "none":
|
||||||
|
chat_body["enable_thinking"] = False
|
||||||
|
chat_body["reasoning_effort"] = "none"
|
||||||
|
else:
|
||||||
|
chat_body["reasoning_effort"] = r_effort
|
||||||
|
|
||||||
def _forward_oa_compat(self, req, stream, model, chat_body, body, input_data, fwd, target, tools):
|
target = upstream_target(r_url, "/chat/completions")
|
||||||
try:
|
if r_oauth == "google":
|
||||||
upstream = urllib.request.urlopen(req, timeout=180)
|
r_key = _refresh_oauth_token_for(r_key, r_oauth)
|
||||||
except urllib.error.HTTPError as e:
|
fwd = forwarded_headers(self.headers, {
|
||||||
err = e.read().decode()
|
"Content-Type": "application/json",
|
||||||
return self.send_json(e.code, {"error": {"type": "upstream_error", "message": err}})
|
"Authorization": f"Bearer {r_key}",
|
||||||
except Exception as e:
|
}, browser_ua=True)
|
||||||
return self.send_json(500, {"error": {"type": "proxy_error", "message": str(e)}})
|
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}")
|
||||||
|
|
||||||
|
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)}"}})
|
||||||
|
|
||||||
|
def _forward_oa_compat(self, upstream, stream, model, chat_body, body, input_data, fwd, target):
|
||||||
n_items = len(input_data) if isinstance(input_data, list) else 1
|
n_items = len(input_data) if isinstance(input_data, list) else 1
|
||||||
|
|
||||||
if stream:
|
if stream:
|
||||||
|
|||||||
Reference in New Issue
Block a user