Merge pull request #3 from fix/gui-crof-consolidation
fix: GUI buttons, CROF gate, data dir consolidation, sticky proxy port
This commit is contained in:
@@ -15,12 +15,12 @@ IS_WINDOWS = sys.platform == "win32"
|
|||||||
|
|
||||||
if IS_WINDOWS:
|
if IS_WINDOWS:
|
||||||
_local = os.environ.get("LOCALAPPDATA", str(Path.home() / "AppData" / "Local"))
|
_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"
|
CODEX_DIR = Path.home() / ".codex"
|
||||||
_local_share = Path(_local)
|
_local_share = Path(_local)
|
||||||
_cache = Path(_local)
|
_cache = Path(_local)
|
||||||
else:
|
else:
|
||||||
PID_REGISTRY = Path.home() / ".cache" / "codex-launcher" / "pids.json"
|
PID_REGISTRY = Path.home() / ".cache" / "codex-proxy" / "pids.json"
|
||||||
CODEX_DIR = Path.home() / ".codex"
|
CODEX_DIR = Path.home() / ".codex"
|
||||||
_local_share = Path.home() / ".local" / "share"
|
_local_share = Path.home() / ".local" / "share"
|
||||||
_cache = Path.home() / ".cache"
|
_cache = Path.home() / ".cache"
|
||||||
|
|||||||
@@ -2058,10 +2058,13 @@ class LauncherWin:
|
|||||||
# Bottom bar
|
# Bottom bar
|
||||||
bb = ttk.Frame(main)
|
bb = ttk.Frame(main)
|
||||||
bb.pack(fill="x", pady=(6, 0))
|
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 = ttk.Button(bb, text="Kill && Cleanup", command=self._kill, state="disabled")
|
||||||
self._kill_btn.pack(side="left", fill="x", expand=True, padx=(8, 0))
|
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))
|
ttk.Button(bb, text="Close", command=self._do_close).pack(side="left", padx=(8, 0))
|
||||||
|
|
||||||
self._rebuild_combo()
|
self._rebuild_combo()
|
||||||
@@ -2079,6 +2082,25 @@ class LauncherWin:
|
|||||||
self._log_text.see("end")
|
self._log_text.see("end")
|
||||||
self._log_text.configure(state="disabled")
|
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):
|
def _log_dependency_status(self):
|
||||||
if self._cli_info:
|
if self._cli_info:
|
||||||
_, ver = self._cli_info
|
_, ver = self._cli_info
|
||||||
@@ -2191,6 +2213,18 @@ class LauncherWin:
|
|||||||
def _open_benchmark(self):
|
def _open_benchmark(self):
|
||||||
BenchmarkWindow(self._root)
|
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):
|
def _open_assistant(self):
|
||||||
assist_path = str(Path(__file__).resolve().parent / "flet-codex-assist.py")
|
assist_path = str(Path(__file__).resolve().parent / "flet-codex-assist.py")
|
||||||
if Path(assist_path).exists():
|
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_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._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._kill_btn.configure(state="normal" if busy else "disabled")
|
||||||
|
self._restart_btn.configure(state="normal" if busy else "disabled")
|
||||||
self._root.after(0, _update)
|
self._root.after(0, _update)
|
||||||
|
|
||||||
def _launch(self, target):
|
def _launch(self, target):
|
||||||
|
|||||||
@@ -41,8 +41,8 @@ if IS_WINDOWS:
|
|||||||
PROXY_CONFIG_DIR = _LOCAL_APPDATA / "codex-proxy"
|
PROXY_CONFIG_DIR = _LOCAL_APPDATA / "codex-proxy"
|
||||||
CONFIG_DIR = HOME / ".codex"
|
CONFIG_DIR = HOME / ".codex"
|
||||||
BIN_DIR = _LOCAL_APPDATA / "Programs" / "Codex-Launcher"
|
BIN_DIR = _LOCAL_APPDATA / "Programs" / "Codex-Launcher"
|
||||||
LOG_DIR = _LOCAL_APPDATA / "codex-desktop"
|
LOG_DIR = _LOCAL_APPDATA / "codex-proxy"
|
||||||
PID_REGISTRY = _LOCAL_APPDATA / "codex-launcher" / "pids.json"
|
PID_REGISTRY = _LOCAL_APPDATA / "codex-proxy" / "pids.json"
|
||||||
_USAGE_STATS_FILE = _LOCAL_APPDATA / "codex-proxy" / "usage-stats.json"
|
_USAGE_STATS_FILE = _LOCAL_APPDATA / "codex-proxy" / "usage-stats.json"
|
||||||
MONITORING_FILE = _LOCAL_APPDATA / "codex-proxy" / "monitoring-config.json"
|
MONITORING_FILE = _LOCAL_APPDATA / "codex-proxy" / "monitoring-config.json"
|
||||||
INCIDENT_STORE_FILE = _LOCAL_APPDATA / "codex-proxy" / "incident-store.json"
|
INCIDENT_STORE_FILE = _LOCAL_APPDATA / "codex-proxy" / "incident-store.json"
|
||||||
@@ -52,8 +52,8 @@ else:
|
|||||||
PROXY_CONFIG_DIR = HOME / ".cache/codex-proxy"
|
PROXY_CONFIG_DIR = HOME / ".cache/codex-proxy"
|
||||||
CONFIG_DIR = HOME / ".codex"
|
CONFIG_DIR = HOME / ".codex"
|
||||||
BIN_DIR = HOME / ".local/bin"
|
BIN_DIR = HOME / ".local/bin"
|
||||||
LOG_DIR = HOME / ".cache/codex-desktop"
|
LOG_DIR = HOME / ".cache/codex-proxy"
|
||||||
PID_REGISTRY = HOME / ".cache" / "codex-launcher" / "pids.json"
|
PID_REGISTRY = HOME / ".cache/codex-proxy" / "pids.json"
|
||||||
_USAGE_STATS_FILE = HOME / ".cache/codex-proxy/usage-stats.json"
|
_USAGE_STATS_FILE = HOME / ".cache/codex-proxy/usage-stats.json"
|
||||||
MONITORING_FILE = HOME / ".cache/codex-proxy/monitoring-config.json"
|
MONITORING_FILE = HOME / ".cache/codex-proxy/monitoring-config.json"
|
||||||
INCIDENT_STORE_FILE = HOME / ".cache/codex-proxy/incident-store.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_proc = None
|
||||||
_proxy_port = None
|
_proxy_port = None
|
||||||
|
_PROXY_PORT_FILE = PROXY_CONFIG_DIR / ".last-proxy-port"
|
||||||
|
|
||||||
|
|
||||||
def _pick_free_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:
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
||||||
s.bind(("127.0.0.1", 0))
|
s.bind(("127.0.0.1", 0))
|
||||||
return s.getsockname()[1]
|
return s.getsockname()[1]
|
||||||
@@ -1421,6 +1430,8 @@ def start_proxy_for(endpoint, logfn):
|
|||||||
stop_proxy()
|
stop_proxy()
|
||||||
port = _pick_free_port()
|
port = _pick_free_port()
|
||||||
_proxy_port = 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", [])
|
model_list = endpoint.get("models", [])
|
||||||
if (endpoint.get("backend_type") or "").startswith("gemini-oauth") and (endpoint.get("oauth_provider") or "").startswith("google"):
|
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()
|
stop_proxy()
|
||||||
port = _pick_free_port()
|
port = _pick_free_port()
|
||||||
_proxy_port = port
|
_proxy_port = port
|
||||||
|
_PROXY_PORT_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
_PROXY_PORT_FILE.write_text(str(port))
|
||||||
|
|
||||||
bgp_ep = {
|
bgp_ep = {
|
||||||
"name": pool["name"],
|
"name": pool["name"],
|
||||||
|
|||||||
@@ -260,11 +260,6 @@ try:
|
|||||||
for _f in os.listdir(_REQUESTS_DIR):
|
for _f in os.listdir(_REQUESTS_DIR):
|
||||||
if _f.endswith(".tmp"):
|
if _f.endswith(".tmp"):
|
||||||
os.remove(os.path.join(_REQUESTS_DIR, _f))
|
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:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
_stats_path = os.path.join(_LOG_DIR, "usage-stats.json")
|
_stats_path = os.path.join(_LOG_DIR, "usage-stats.json")
|
||||||
@@ -724,7 +719,6 @@ _GEMINI_AGENT_GUARDRAIL = (
|
|||||||
"Always emit the actual tool call in the same response."
|
"Always emit the actual tool call in the same response."
|
||||||
)
|
)
|
||||||
|
|
||||||
_LOG_FILE = None
|
|
||||||
_LOG_FILE_LOCK = threading.Lock()
|
_LOG_FILE_LOCK = threading.Lock()
|
||||||
|
|
||||||
def _fetch_antigravity_version():
|
def _fetch_antigravity_version():
|
||||||
@@ -1303,8 +1297,8 @@ _COMPACT_KEEP_RECENT = 10
|
|||||||
_CROF_ADAPTIVE = {
|
_CROF_ADAPTIVE = {
|
||||||
"fail_history": [],
|
"fail_history": [],
|
||||||
"model_limits": {},
|
"model_limits": {},
|
||||||
"global_item_limit": 30,
|
"global_item_limit": 80,
|
||||||
"min_keep_recent": 4,
|
"min_keep_recent": 6,
|
||||||
}
|
}
|
||||||
|
|
||||||
_BGP_STATS_PATH = os.path.join(_LOG_DIR, "bgp-route-stats.json")
|
_BGP_STATS_PATH = os.path.join(_LOG_DIR, "bgp-route-stats.json")
|
||||||
@@ -1372,6 +1366,8 @@ def _sorted_bgp_routes():
|
|||||||
return sorted(BGP_ROUTES, key=lambda r: _score_route(r, stats))
|
return sorted(BGP_ROUTES, key=lambda r: _score_route(r, stats))
|
||||||
|
|
||||||
def _crof_record(model, n_items, success):
|
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:
|
if not isinstance(n_items, int) or n_items < 1:
|
||||||
return
|
return
|
||||||
entry = {"model": model, "items": n_items, "ok": success}
|
entry = {"model": model, "items": n_items, "ok": success}
|
||||||
@@ -1397,6 +1393,7 @@ def _crof_record(model, n_items, success):
|
|||||||
global_limit = v["limit"]
|
global_limit = v["limit"]
|
||||||
_CROF_ADAPTIVE["global_item_limit"] = global_limit
|
_CROF_ADAPTIVE["global_item_limit"] = global_limit
|
||||||
|
|
||||||
|
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)
|
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):
|
def _crof_item_limit(model):
|
||||||
@@ -1442,6 +1439,7 @@ def _crof_compact_for_retry(input_data, model):
|
|||||||
summary_lines.append(_item_summary(item, max_len=120))
|
summary_lines.append(_item_summary(item, max_len=120))
|
||||||
|
|
||||||
summary_msg = {"type": "message", "role": "user", "content": [{"type": "input_text", "text": "\n".join(summary_lines)}]}
|
summary_msg = {"type": "message", "role": "user", "content": [{"type": "input_text", "text": "\n".join(summary_lines)}]}
|
||||||
|
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)
|
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
|
return head + [summary_msg] + tail
|
||||||
|
|
||||||
@@ -1653,7 +1651,7 @@ def _estimate_tokens(obj):
|
|||||||
def _adaptive_compact(input_data, model, policy=None):
|
def _adaptive_compact(input_data, model, policy=None):
|
||||||
policy = policy or {}
|
policy = policy or {}
|
||||||
context_size = int(policy.get("context_size", _context_limit_for_model(model)))
|
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)
|
estimated = _estimate_tokens(input_data)
|
||||||
if estimated <= input_budget:
|
if estimated <= input_budget:
|
||||||
return input_data, False
|
return input_data, False
|
||||||
@@ -4297,7 +4295,8 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
|||||||
body["input"] = input_data
|
body["input"] = input_data
|
||||||
|
|
||||||
crof_limit = _crof_item_limit(model)
|
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)
|
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)
|
input_data = _crof_compact_for_retry(input_data, model)
|
||||||
body = dict(body)
|
body = dict(body)
|
||||||
@@ -5091,7 +5090,7 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
|||||||
print(f"[provider-sensor] synthetic retry failed: {e}", file=sys.stderr)
|
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.
|
# 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)
|
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)
|
new_input = _crof_compact_for_retry(input_data, model)
|
||||||
if len(new_input) < len(input_data):
|
if len(new_input) < len(input_data):
|
||||||
@@ -6030,6 +6029,15 @@ def main():
|
|||||||
global SERVER, _START_TIME
|
global SERVER, _START_TIME
|
||||||
_START_TIME = time.time()
|
_START_TIME = time.time()
|
||||||
_init_runtime()
|
_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)
|
signal.signal(signal.SIGINT, _handle_shutdown_signal)
|
||||||
if _IS_WINDOWS:
|
if _IS_WINDOWS:
|
||||||
if hasattr(signal, "SIGBREAK"):
|
if hasattr(signal, "SIGBREAK"):
|
||||||
|
|||||||
Reference in New Issue
Block a user