diff --git a/src/cleanup-codex-stale.py b/src/cleanup-codex-stale.py index 0157190..278b45f 100644 --- a/src/cleanup-codex-stale.py +++ b/src/cleanup-codex-stale.py @@ -15,12 +15,12 @@ IS_WINDOWS = sys.platform == "win32" if IS_WINDOWS: _local = os.environ.get("LOCALAPPDATA", str(Path.home() / "AppData" / "Local")) - PID_REGISTRY = Path(_local) / "codex-launcher" / "pids.json" + PID_REGISTRY = Path(_local) / "codex-proxy" / "pids.json" CODEX_DIR = Path.home() / ".codex" _local_share = Path(_local) _cache = Path(_local) else: - PID_REGISTRY = Path.home() / ".cache" / "codex-launcher" / "pids.json" + PID_REGISTRY = Path.home() / ".cache" / "codex-proxy" / "pids.json" CODEX_DIR = Path.home() / ".codex" _local_share = Path.home() / ".local" / "share" _cache = Path.home() / ".cache" diff --git a/src/codex-launcher-gui.py b/src/codex-launcher-gui.py index c33f5fa..8045bb1 100644 --- a/src/codex-launcher-gui.py +++ b/src/codex-launcher-gui.py @@ -2058,10 +2058,13 @@ class LauncherWin: # Bottom bar bb = ttk.Frame(main) bb.pack(fill="x", pady=(6, 0)) - ttk.Button(bb, text="AI Assistant", command=self._open_assistant).pack(side="left") + ttk.Button(bb, text="Clear Log", command=self._clear_log).pack(side="left") + self._restart_btn = ttk.Button(bb, text="Restart Proxy", command=self._restart_proxy, state="disabled") + self._restart_btn.pack(side="left", padx=(4, 0)) + ttk.Button(bb, text="AI Assistant", command=self._open_assistant).pack(side="left", padx=(4, 0)) self._kill_btn = ttk.Button(bb, text="Kill && Cleanup", command=self._kill, state="disabled") self._kill_btn.pack(side="left", fill="x", expand=True, padx=(8, 0)) - ttk.Button(bb, text="View Log", command=lambda: open_file(str(LAUNCH_LOG))).pack(side="left") + ttk.Button(bb, text="View Log", command=self._open_proxy_log_dir).pack(side="left") ttk.Button(bb, text="Close", command=self._do_close).pack(side="left", padx=(8, 0)) self._rebuild_combo() @@ -2079,6 +2082,25 @@ class LauncherWin: self._log_text.see("end") self._log_text.configure(state="disabled") + def _clear_log(self): + self._log_text.configure(state="normal") + self._log_text.delete("1.0", "end") + self._log_text.configure(state="disabled") + + def _restart_proxy(self): + self._kill() + ep_name = load_endpoints().get("default") + if not ep_name: + self.log("No default endpoint set.") + return + for ep in load_endpoints().get("endpoints", []): + if ep.get("name") == ep_name: + time.sleep(0.3) + start_proxy_for(ep, self.log) + self.log(f"Proxy restarted for {ep_name}") + return + self.log(f"Endpoint '{ep_name}' not found.") + def _log_dependency_status(self): if self._cli_info: _, ver = self._cli_info @@ -2191,6 +2213,18 @@ class LauncherWin: def _open_benchmark(self): BenchmarkWindow(self._root) + def _open_proxy_log_dir(self): + log_dir = str(PROXY_CONFIG_DIR) + req_log = PROXY_CONFIG_DIR / "requests.log" + if IS_WINDOWS: + if req_log.exists(): + os.startfile(str(req_log)) + else: + os.startfile(log_dir) + else: + import subprocess as _sp + _sp.Popen(["xdg-open", log_dir]) + def _open_assistant(self): assist_path = str(Path(__file__).resolve().parent / "flet-codex-assist.py") if Path(assist_path).exists(): @@ -2452,6 +2486,7 @@ class LauncherWin: self._btn_codex_desktop.configure(state="disabled" if busy or not has_desk else "normal") self._btn_codex_cli.configure(state="disabled" if busy or not has_cli else "normal") self._kill_btn.configure(state="normal" if busy else "disabled") + self._restart_btn.configure(state="normal" if busy else "disabled") self._root.after(0, _update) def _launch(self, target): diff --git a/src/codex_launcher_lib.py b/src/codex_launcher_lib.py index abd2e6a..2bff5e1 100644 --- a/src/codex_launcher_lib.py +++ b/src/codex_launcher_lib.py @@ -41,8 +41,8 @@ if IS_WINDOWS: PROXY_CONFIG_DIR = _LOCAL_APPDATA / "codex-proxy" CONFIG_DIR = HOME / ".codex" BIN_DIR = _LOCAL_APPDATA / "Programs" / "Codex-Launcher" - LOG_DIR = _LOCAL_APPDATA / "codex-desktop" - PID_REGISTRY = _LOCAL_APPDATA / "codex-launcher" / "pids.json" + LOG_DIR = _LOCAL_APPDATA / "codex-proxy" + PID_REGISTRY = _LOCAL_APPDATA / "codex-proxy" / "pids.json" _USAGE_STATS_FILE = _LOCAL_APPDATA / "codex-proxy" / "usage-stats.json" MONITORING_FILE = _LOCAL_APPDATA / "codex-proxy" / "monitoring-config.json" INCIDENT_STORE_FILE = _LOCAL_APPDATA / "codex-proxy" / "incident-store.json" @@ -52,8 +52,8 @@ else: PROXY_CONFIG_DIR = HOME / ".cache/codex-proxy" CONFIG_DIR = HOME / ".codex" BIN_DIR = HOME / ".local/bin" - LOG_DIR = HOME / ".cache/codex-desktop" - PID_REGISTRY = HOME / ".cache" / "codex-launcher" / "pids.json" + LOG_DIR = HOME / ".cache/codex-proxy" + PID_REGISTRY = HOME / ".cache/codex-proxy" / "pids.json" _USAGE_STATS_FILE = HOME / ".cache/codex-proxy/usage-stats.json" MONITORING_FILE = HOME / ".cache/codex-proxy/monitoring-config.json" INCIDENT_STORE_FILE = HOME / ".cache/codex-proxy/incident-store.json" @@ -1388,9 +1388,18 @@ def safe_cleanup_owned(logfn=None): _proxy_proc = None _proxy_port = None +_PROXY_PORT_FILE = PROXY_CONFIG_DIR / ".last-proxy-port" def _pick_free_port(): + saved = None + try: + saved = int(_PROXY_PORT_FILE.read_text().strip()) + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.bind(("127.0.0.1", saved)) + return saved + except (ValueError, OSError, FileNotFoundError): + pass with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: s.bind(("127.0.0.1", 0)) return s.getsockname()[1] @@ -1421,6 +1430,8 @@ def start_proxy_for(endpoint, logfn): stop_proxy() port = _pick_free_port() _proxy_port = port + _PROXY_PORT_FILE.parent.mkdir(parents=True, exist_ok=True) + _PROXY_PORT_FILE.write_text(str(port)) model_list = endpoint.get("models", []) if (endpoint.get("backend_type") or "").startswith("gemini-oauth") and (endpoint.get("oauth_provider") or "").startswith("google"): @@ -1507,6 +1518,8 @@ def start_bgp_proxy(pool, model, logfn): stop_proxy() port = _pick_free_port() _proxy_port = port + _PROXY_PORT_FILE.parent.mkdir(parents=True, exist_ok=True) + _PROXY_PORT_FILE.write_text(str(port)) bgp_ep = { "name": pool["name"], diff --git a/src/translate-proxy.py b/src/translate-proxy.py index 9f3f200..3e924e2 100755 --- a/src/translate-proxy.py +++ b/src/translate-proxy.py @@ -259,11 +259,6 @@ try: for _f in os.listdir(_REQUESTS_DIR): if _f.endswith(".tmp"): os.remove(os.path.join(_REQUESTS_DIR, _f)) - for _f in os.listdir(_LOG_DIR): - if _f.startswith("proxy-") and _f.endswith(".json"): - os.remove(os.path.join(_LOG_DIR, _f)) - if _f.startswith("models-") and _f.endswith(".json"): - os.remove(os.path.join(_LOG_DIR, _f)) except Exception: pass _stats_path = os.path.join(_LOG_DIR, "usage-stats.json") @@ -723,7 +718,6 @@ _GEMINI_AGENT_GUARDRAIL = ( "Always emit the actual tool call in the same response." ) -_LOG_FILE = None _LOG_FILE_LOCK = threading.Lock() def _fetch_antigravity_version(): @@ -1302,8 +1296,8 @@ _COMPACT_KEEP_RECENT = 10 _CROF_ADAPTIVE = { "fail_history": [], "model_limits": {}, - "global_item_limit": 30, - "min_keep_recent": 4, + "global_item_limit": 80, + "min_keep_recent": 6, } _BGP_STATS_PATH = os.path.join(_LOG_DIR, "bgp-route-stats.json") @@ -1371,6 +1365,8 @@ def _sorted_bgp_routes(): return sorted(BGP_ROUTES, key=lambda r: _score_route(r, stats)) def _crof_record(model, n_items, success): + if TARGET_URL and "crof.ai" not in TARGET_URL: + return if not isinstance(n_items, int) or n_items < 1: return entry = {"model": model, "items": n_items, "ok": success} @@ -1396,7 +1392,8 @@ def _crof_record(model, n_items, success): global_limit = v["limit"] _CROF_ADAPTIVE["global_item_limit"] = global_limit - print(f"[crof-adaptive] model={model} items={n_items} {'OK' if success else 'FAIL'} -> limit={ml.get('limit',30)} global={global_limit}", file=sys.stderr) + if TARGET_URL and "crof.ai" in TARGET_URL: + print(f"[crof-adaptive] model={model} items={n_items} {'OK' if success else 'FAIL'} -> limit={ml.get('limit',30)} global={global_limit}", file=sys.stderr) def _crof_item_limit(model): ml = _CROF_ADAPTIVE["model_limits"].get(model, {}) @@ -1441,7 +1438,8 @@ def _crof_compact_for_retry(input_data, model): summary_lines.append(_item_summary(item, max_len=120)) summary_msg = {"type": "message", "role": "user", "content": [{"type": "input_text", "text": "\n".join(summary_lines)}]} - print(f"[crof-adaptive] RETRY compact: {len(input_data)} -> {len(head)+1+len(tail)} (limit={limit}, keep={len(tail)})", file=sys.stderr) + if TARGET_URL and "crof.ai" in TARGET_URL: + print(f"[crof-adaptive] RETRY compact: {len(input_data)} -> {len(head)+1+len(tail)} (limit={limit}, keep={len(tail)})", file=sys.stderr) return head + [summary_msg] + tail def _item_summary(item, max_len=200): @@ -1652,7 +1650,7 @@ def _estimate_tokens(obj): def _adaptive_compact(input_data, model, policy=None): policy = policy or {} context_size = int(policy.get("context_size", _context_limit_for_model(model))) - input_budget = int(context_size * 0.60) + input_budget = int(context_size * 0.80) estimated = _estimate_tokens(input_data) if estimated <= input_budget: return input_data, False @@ -4296,7 +4294,8 @@ class Handler(http.server.BaseHTTPRequestHandler): body["input"] = input_data crof_limit = _crof_item_limit(model) - if not compacted and isinstance(input_data, list) and len(input_data) > crof_limit: + _crof_eligible = TARGET_URL and "crof.ai" in TARGET_URL + if _crof_eligible and not compacted and 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) input_data = _crof_compact_for_retry(input_data, model) body = dict(body) @@ -5090,7 +5089,7 @@ class Handler(http.server.BaseHTTPRequestHandler): print(f"[provider-sensor] synthetic retry failed: {e}", file=sys.stderr) # Auto-retry on finish_reason=length with no content due to too much context. - if finish_reason == "length" and not has_content and isinstance(input_data, list) and len(input_data) > 5: + if finish_reason == "length" and not has_content and isinstance(input_data, list) and len(input_data) > 5 and TARGET_URL and "crof.ai" in TARGET_URL: print(f"[crof-adaptive] RETRY: finish_reason=length with no content, compacting {n_items} items", file=sys.stderr) new_input = _crof_compact_for_retry(input_data, model) if len(new_input) < len(input_data): @@ -6029,6 +6028,15 @@ def main(): global SERVER, _START_TIME _START_TIME = time.time() _init_runtime() + try: + _current_cfg = os.path.basename(args.config) if args.config else "" + for _f in os.listdir(_LOG_DIR): + if _f.startswith("proxy-") and _f.endswith(".json") and _f != _current_cfg: + os.remove(os.path.join(_LOG_DIR, _f)) + if _f.startswith("models-") and _f.endswith(".json"): + os.remove(os.path.join(_LOG_DIR, _f)) + except Exception: + pass signal.signal(signal.SIGINT, _handle_shutdown_signal) if _IS_WINDOWS: if hasattr(signal, "SIGBREAK"):