Merge pull request #1 from cobra91/windows-support
feat: Windows native support — tkinter GUI + cross-platform proxy
This commit is contained in:
101
src/cleanup-codex-stale.py
Normal file
101
src/cleanup-codex-stale.py
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Cleanup stale Codex Launcher processes and artifacts — cross-platform.
|
||||||
|
|
||||||
|
Kills registered process groups and removes stale PID/socket files left
|
||||||
|
by previous Codex Launcher sessions.
|
||||||
|
|
||||||
|
Windows: uses taskkill /F /T /PID
|
||||||
|
Linux: uses kill -TERM -- -PGID
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json, os, sys, subprocess, time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
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"
|
||||||
|
CODEX_DIR = Path.home() / ".codex"
|
||||||
|
_local_share = Path(_local)
|
||||||
|
_cache = Path(_local)
|
||||||
|
else:
|
||||||
|
PID_REGISTRY = Path.home() / ".cache" / "codex-launcher" / "pids.json"
|
||||||
|
CODEX_DIR = Path.home() / ".codex"
|
||||||
|
_local_share = Path.home() / ".local" / "share"
|
||||||
|
_cache = Path.home() / ".cache"
|
||||||
|
|
||||||
|
|
||||||
|
def kill_group(pid):
|
||||||
|
if IS_WINDOWS:
|
||||||
|
subprocess.run(["taskkill", "/F", "/T", "/PID", str(pid)],
|
||||||
|
capture_output=True, timeout=10)
|
||||||
|
else:
|
||||||
|
import signal
|
||||||
|
try:
|
||||||
|
pgid = os.getpgid(pid)
|
||||||
|
os.killpg(pgid, signal.SIGTERM)
|
||||||
|
time.sleep(0.5)
|
||||||
|
try:
|
||||||
|
os.killpg(pgid, signal.SIGKILL)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("[cleanup] Cleaning up stale Codex Launcher processes...", file=sys.stderr)
|
||||||
|
|
||||||
|
if PID_REGISTRY.exists():
|
||||||
|
try:
|
||||||
|
with open(PID_REGISTRY) as f:
|
||||||
|
registry = json.load(f)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[cleanup] Failed to read PID registry: {e}", file=sys.stderr)
|
||||||
|
registry = {}
|
||||||
|
|
||||||
|
for kind, info in registry.items():
|
||||||
|
pid = info.get("pid") if isinstance(info, dict) else info
|
||||||
|
if pid and isinstance(pid, int):
|
||||||
|
print(f"[cleanup] Killing {kind} (PID {pid})", file=sys.stderr)
|
||||||
|
kill_group(pid)
|
||||||
|
|
||||||
|
try:
|
||||||
|
PID_REGISTRY.unlink()
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
print("[cleanup] No PID registry found — nothing to stop", file=sys.stderr)
|
||||||
|
|
||||||
|
stale_files = []
|
||||||
|
if IS_WINDOWS:
|
||||||
|
stale_files = [
|
||||||
|
_cache / "codex-desktop" / ".codex-desktop-pid",
|
||||||
|
_cache / "codex-desktop" / ".webview-pid",
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
stale_files = [
|
||||||
|
CODEX_DIR / ".launch-action-socket",
|
||||||
|
CODEX_DIR / ".codex-desktop-launch-action",
|
||||||
|
CODEX_DIR / ".codex-desktop-pid",
|
||||||
|
CODEX_DIR / ".webview-pid",
|
||||||
|
_local_share / "codex-desktop" / ".codex-desktop-pid",
|
||||||
|
_local_share / "codex-desktop" / ".webview-pid",
|
||||||
|
_cache / "codex-desktop" / ".codex-desktop-pid",
|
||||||
|
_cache / "codex-desktop" / ".webview-pid",
|
||||||
|
]
|
||||||
|
|
||||||
|
for fp in stale_files:
|
||||||
|
try:
|
||||||
|
if fp.exists():
|
||||||
|
fp.unlink()
|
||||||
|
print(f"[cleanup] Removed {fp}", file=sys.stderr)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
print("[cleanup] Done", file=sys.stderr)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
2700
src/codex-launcher-gui.py
Normal file
2700
src/codex-launcher-gui.py
Normal file
File diff suppressed because it is too large
Load Diff
1972
src/codex_launcher_lib.py
Normal file
1972
src/codex_launcher_lib.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -160,6 +160,9 @@ import time, uuid, os, sys, argparse, threading, socket, collections, contextlib
|
|||||||
import dataclasses
|
import dataclasses
|
||||||
import http.client
|
import http.client
|
||||||
import selectors
|
import selectors
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
_IS_WINDOWS = sys.platform == "win32"
|
||||||
|
|
||||||
# ═══════════════════════════════════════════════════════════════════
|
# ═══════════════════════════════════════════════════════════════════
|
||||||
# Config
|
# Config
|
||||||
@@ -241,13 +244,28 @@ MODELS = []
|
|||||||
CC_VERSION = ""
|
CC_VERSION = ""
|
||||||
REASONING_ENABLED = True
|
REASONING_ENABLED = True
|
||||||
REASONING_EFFORT = "medium"
|
REASONING_EFFORT = "medium"
|
||||||
|
FORCE_MODEL = ""
|
||||||
BGP_ROUTES = []
|
BGP_ROUTES = []
|
||||||
SERVER = None
|
SERVER = None
|
||||||
|
|
||||||
_LOG_DIR = os.path.join(os.path.expanduser("~"), ".cache", "codex-proxy")
|
if _IS_WINDOWS:
|
||||||
|
_LOG_DIR = os.path.join(os.environ.get("LOCALAPPDATA", os.path.expanduser("~")), "codex-proxy")
|
||||||
|
else:
|
||||||
|
_LOG_DIR = os.path.join(os.path.expanduser("~"), ".cache", "codex-proxy")
|
||||||
os.makedirs(_LOG_DIR, exist_ok=True)
|
os.makedirs(_LOG_DIR, exist_ok=True)
|
||||||
_REQUESTS_DIR = os.path.join(_LOG_DIR, "requests")
|
_REQUESTS_DIR = os.path.join(_LOG_DIR, "requests")
|
||||||
os.makedirs(_REQUESTS_DIR, exist_ok=True)
|
os.makedirs(_REQUESTS_DIR, exist_ok=True)
|
||||||
|
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")
|
_stats_path = os.path.join(_LOG_DIR, "usage-stats.json")
|
||||||
_provider_caps_path = os.path.join(_LOG_DIR, "provider-caps.json")
|
_provider_caps_path = os.path.join(_LOG_DIR, "provider-caps.json")
|
||||||
_stats_lock = threading.Lock()
|
_stats_lock = threading.Lock()
|
||||||
@@ -257,7 +275,7 @@ _STATS_FLUSH_INTERVAL = 5.0
|
|||||||
_STATS = {}
|
_STATS = {}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
_LOG_FILE = open(os.path.join(_LOG_DIR, "proxy.log"), "a")
|
_LOG_FILE = open(os.path.join(_LOG_DIR, "proxy.log"), "a", encoding="utf-8")
|
||||||
except Exception:
|
except Exception:
|
||||||
_LOG_FILE = None
|
_LOG_FILE = None
|
||||||
|
|
||||||
@@ -273,6 +291,9 @@ _deepseek_reasoning_store = {}
|
|||||||
_deepseek_reasoning_lock = threading.Lock()
|
_deepseek_reasoning_lock = threading.Lock()
|
||||||
_MAX_DS_STORED = 100
|
_MAX_DS_STORED = 100
|
||||||
|
|
||||||
|
_last_reasoning_store = {}
|
||||||
|
_last_reasoning_lock = threading.Lock()
|
||||||
|
|
||||||
_crof_lock = threading.Lock()
|
_crof_lock = threading.Lock()
|
||||||
_provider_caps_lock = threading.Lock()
|
_provider_caps_lock = threading.Lock()
|
||||||
_provider_caps = None
|
_provider_caps = None
|
||||||
@@ -302,7 +323,10 @@ _CODEBUFF_AGENT_MAP = {
|
|||||||
"moonshotai/kimi-k2.6": "base2-free-kimi",
|
"moonshotai/kimi-k2.6": "base2-free-kimi",
|
||||||
"minimax/minimax-m2.7": "base2-free",
|
"minimax/minimax-m2.7": "base2-free",
|
||||||
}
|
}
|
||||||
_CODEBUFF_CREDS_PATH = os.path.join(os.path.expanduser("~"), ".config", "manicode", "credentials.json")
|
if _IS_WINDOWS:
|
||||||
|
_CODEBUFF_CREDS_PATH = os.path.join(os.environ.get("APPDATA", os.path.expanduser("~")), "manicode", "credentials.json")
|
||||||
|
else:
|
||||||
|
_CODEBUFF_CREDS_PATH = os.path.join(os.path.expanduser("~"), ".config", "manicode", "credentials.json")
|
||||||
_codebuff_token_cache = {"token": None, "checked": 0}
|
_codebuff_token_cache = {"token": None, "checked": 0}
|
||||||
_codebuff_session_cache = {"instance_id": None, "expires": 0, "model": None}
|
_codebuff_session_cache = {"instance_id": None, "expires": 0, "model": None}
|
||||||
_codebuff_token_lock = threading.Lock()
|
_codebuff_token_lock = threading.Lock()
|
||||||
@@ -634,7 +658,7 @@ def _refresh_google_token(token_data, token_path):
|
|||||||
new_tokens = json.loads(resp.read())
|
new_tokens = json.loads(resp.read())
|
||||||
token_data["access_token"] = new_tokens.get("access_token", token_data.get("access_token"))
|
token_data["access_token"] = new_tokens.get("access_token", token_data.get("access_token"))
|
||||||
token_data["expires_at"] = time.time() + new_tokens.get("expires_in", 3600)
|
token_data["expires_at"] = time.time() + new_tokens.get("expires_in", 3600)
|
||||||
with open(token_path, "w") as f:
|
with open(token_path, "w", encoding="utf-8") as f:
|
||||||
json.dump(token_data, f, indent=2)
|
json.dump(token_data, f, indent=2)
|
||||||
print("[oauth] token refreshed OK", file=sys.stderr)
|
print("[oauth] token refreshed OK", file=sys.stderr)
|
||||||
return token_data["access_token"]
|
return token_data["access_token"]
|
||||||
@@ -727,7 +751,7 @@ def _fetch_antigravity_version():
|
|||||||
version = m.group(0)
|
version = m.group(0)
|
||||||
try:
|
try:
|
||||||
os.makedirs(os.path.dirname(cache_path), exist_ok=True)
|
os.makedirs(os.path.dirname(cache_path), exist_ok=True)
|
||||||
with open(cache_path, "w") as f:
|
with open(cache_path, "w", encoding="utf-8") as f:
|
||||||
json.dump({"version": version, "checked_at": time.time()}, f)
|
json.dump({"version": version, "checked_at": time.time()}, f)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
@@ -762,6 +786,7 @@ def _init_runtime():
|
|||||||
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")
|
||||||
|
FORCE_MODEL = (CONFIG.get("force_model") or "").strip()
|
||||||
BGP_ROUTES = CONFIG.get("bgp_routes", [])
|
BGP_ROUTES = CONFIG.get("bgp_routes", [])
|
||||||
_api_key_pool = None
|
_api_key_pool = None
|
||||||
if API_KEY and "," in API_KEY and not OAUTH_PROVIDER.startswith("google") and BACKEND not in ("codebuff", "freebuff"):
|
if API_KEY and "," in API_KEY and not OAUTH_PROVIDER.startswith("google") and BACKEND not in ("codebuff", "freebuff"):
|
||||||
@@ -903,7 +928,7 @@ def _load_provider_caps():
|
|||||||
def _save_provider_caps():
|
def _save_provider_caps():
|
||||||
try:
|
try:
|
||||||
os.makedirs(os.path.dirname(_provider_caps_path), exist_ok=True)
|
os.makedirs(os.path.dirname(_provider_caps_path), exist_ok=True)
|
||||||
with open(_provider_caps_path, "w") as f:
|
with open(_provider_caps_path, "w", encoding="utf-8") as f:
|
||||||
json.dump(_provider_caps or {}, f, indent=2)
|
json.dump(_provider_caps or {}, f, indent=2)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[provider-sensor] failed to save caps: {e}", file=sys.stderr)
|
print(f"[provider-sensor] failed to save caps: {e}", file=sys.stderr)
|
||||||
@@ -959,7 +984,7 @@ def _refresh_oauth_token_for(api_key, oauth_provider):
|
|||||||
new_tokens = json.loads(resp.read())
|
new_tokens = json.loads(resp.read())
|
||||||
tokens["access_token"] = new_tokens.get("access_token", tokens.get("access_token"))
|
tokens["access_token"] = new_tokens.get("access_token", tokens.get("access_token"))
|
||||||
tokens["expires_at"] = time.time() + new_tokens.get("expires_in", 3600)
|
tokens["expires_at"] = time.time() + new_tokens.get("expires_in", 3600)
|
||||||
with open(token_path, "w") as f:
|
with open(token_path, "w", encoding="utf-8") as f:
|
||||||
json.dump(tokens, f, indent=2)
|
json.dump(tokens, f, indent=2)
|
||||||
print("[oauth] token refreshed OK", file=sys.stderr)
|
print("[oauth] token refreshed OK", file=sys.stderr)
|
||||||
return tokens["access_token"]
|
return tokens["access_token"]
|
||||||
@@ -983,7 +1008,7 @@ def _load_stats():
|
|||||||
|
|
||||||
def _atomic_write_json(path, obj):
|
def _atomic_write_json(path, obj):
|
||||||
tmp = path + ".tmp"
|
tmp = path + ".tmp"
|
||||||
with open(tmp, "w") as f:
|
with open(tmp, "w", encoding="utf-8") as f:
|
||||||
json.dump(obj, f, indent=2, ensure_ascii=False)
|
json.dump(obj, f, indent=2, ensure_ascii=False)
|
||||||
os.replace(tmp, path)
|
os.replace(tmp, path)
|
||||||
|
|
||||||
@@ -1297,7 +1322,7 @@ def _load_bgp_stats():
|
|||||||
|
|
||||||
def _save_bgp_stats(stats):
|
def _save_bgp_stats(stats):
|
||||||
tmp = _BGP_STATS_PATH + ".tmp"
|
tmp = _BGP_STATS_PATH + ".tmp"
|
||||||
with open(tmp, "w") as f:
|
with open(tmp, "w", encoding="utf-8") as f:
|
||||||
json.dump(stats, f, indent=2)
|
json.dump(stats, f, indent=2)
|
||||||
os.replace(tmp, _BGP_STATS_PATH)
|
os.replace(tmp, _BGP_STATS_PATH)
|
||||||
|
|
||||||
@@ -1790,7 +1815,7 @@ def save_request_snapshot(request_id, body):
|
|||||||
}
|
}
|
||||||
path = os.path.join(_REQUESTS_DIR, f"{request_id}.json")
|
path = os.path.join(_REQUESTS_DIR, f"{request_id}.json")
|
||||||
tmp = path + ".tmp"
|
tmp = path + ".tmp"
|
||||||
with open(tmp, "w") as f:
|
with open(tmp, "w", encoding="utf-8") as f:
|
||||||
json.dump(snapshot, f, ensure_ascii=False, indent=2)
|
json.dump(snapshot, f, ensure_ascii=False, indent=2)
|
||||||
os.replace(tmp, path)
|
os.replace(tmp, path)
|
||||||
_rotate_snapshots()
|
_rotate_snapshots()
|
||||||
@@ -1813,7 +1838,7 @@ def update_snapshot_response(request_id, status, duration_s=None, error=None):
|
|||||||
meta["error"] = str(error)[:200]
|
meta["error"] = str(error)[:200]
|
||||||
snapshot["_meta"] = meta
|
snapshot["_meta"] = meta
|
||||||
tmp = path + ".tmp"
|
tmp = path + ".tmp"
|
||||||
with open(tmp, "w") as f:
|
with open(tmp, "w", encoding="utf-8") as f:
|
||||||
json.dump(snapshot, f, ensure_ascii=False, indent=2)
|
json.dump(snapshot, f, ensure_ascii=False, indent=2)
|
||||||
os.replace(tmp, path)
|
os.replace(tmp, path)
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -1865,6 +1890,27 @@ def _bucket_for_route(route):
|
|||||||
# OpenAI-compat backend
|
# OpenAI-compat backend
|
||||||
# ═══════════════════════════════════════════════════════════════════
|
# ═══════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
def _inject_stored_reasoning(messages):
|
||||||
|
with _last_reasoning_lock:
|
||||||
|
snapshot = dict(_last_reasoning_store)
|
||||||
|
if not snapshot:
|
||||||
|
return messages
|
||||||
|
expired = [k for k, v in snapshot.items() if time.time() - v["ts"] > _RESPONSE_TTL]
|
||||||
|
for k in expired:
|
||||||
|
with _last_reasoning_lock:
|
||||||
|
_last_reasoning_store.pop(k, None)
|
||||||
|
snapshot.pop(k, None)
|
||||||
|
if not snapshot:
|
||||||
|
return messages
|
||||||
|
latest = max(snapshot.values(), key=lambda v: v["ts"])
|
||||||
|
reasoning = latest.get("reasoning", "")
|
||||||
|
if not reasoning:
|
||||||
|
return messages
|
||||||
|
for msg in messages:
|
||||||
|
if msg.get("role") == "assistant" and "reasoning_content" not in msg and msg.get("tool_calls"):
|
||||||
|
msg["reasoning_content"] = reasoning
|
||||||
|
return messages
|
||||||
|
|
||||||
def oa_input_to_messages(input_data):
|
def oa_input_to_messages(input_data):
|
||||||
msgs = []
|
msgs = []
|
||||||
tool_name_by_id = {}
|
tool_name_by_id = {}
|
||||||
@@ -2384,10 +2430,10 @@ def an_stream_to_sse(stream, model, req_id):
|
|||||||
"status": status, "created": int(time.time()), "output": completed}})
|
"status": status, "created": int(time.time()), "output": completed}})
|
||||||
|
|
||||||
_DEFAULT_CC_CONFIG = {
|
_DEFAULT_CC_CONFIG = {
|
||||||
"workingDir": "/tmp",
|
"workingDir": tempfile.gettempdir(),
|
||||||
"date": "",
|
"date": "",
|
||||||
"environment": "linux",
|
"environment": "windows" if _IS_WINDOWS else "linux",
|
||||||
"shell": "bash",
|
"shell": "powershell" if _IS_WINDOWS else "bash",
|
||||||
"files": [],
|
"files": [],
|
||||||
"structure": [],
|
"structure": [],
|
||||||
"isGitRepo": False,
|
"isGitRepo": False,
|
||||||
@@ -2462,6 +2508,17 @@ def _build_explore_cmd(text_for_url):
|
|||||||
api_base = repo_url.replace("/admin/", "/api/v1/repos/")
|
api_base = repo_url.replace("/admin/", "/api/v1/repos/")
|
||||||
else:
|
else:
|
||||||
api_base = repo_url
|
api_base = repo_url
|
||||||
|
if _IS_WINDOWS:
|
||||||
|
cmd = (
|
||||||
|
f"cd $env:TEMP; "
|
||||||
|
f"$r = Invoke-WebRequest -Uri '{api_base}/contents/README.md' -UseBasicParsing -TimeoutSec 15 2>$null; "
|
||||||
|
f"if ($r) {{ $j = $r.Content | ConvertFrom-Json; [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($j.content)) | Select-Object -First 600 }}; "
|
||||||
|
f"$r2 = Invoke-WebRequest -Uri '{api_base}/contents' -UseBasicParsing -TimeoutSec 15 2>$null; "
|
||||||
|
f"if ($r2) {{ $j2 = $r2.Content | ConvertFrom-Json; $j2 | Select-Object -First 50 | ForEach-Object {{ $_.path + ' ' + $_.type }} }}; "
|
||||||
|
f"$r3 = Invoke-WebRequest -Uri '{api_base}/releases' -UseBasicParsing -TimeoutSec 15 2>$null; "
|
||||||
|
f"if ($r3) {{ ($r3.Content | ConvertFrom-Json | Select-Object -First 3 | ConvertTo-Json).Substring(0, [Math]::Min(2000, ($r3.Content | ConvertFrom-Json | Select-Object -First 3 | ConvertTo-Json).Length)) }}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
cmd = (
|
cmd = (
|
||||||
f"cd /tmp && "
|
f"cd /tmp && "
|
||||||
f"curl -sL --max-time 15 '{api_base}/contents/README.md' 2>/dev/null | "
|
f"curl -sL --max-time 15 '{api_base}/contents/README.md' 2>/dev/null | "
|
||||||
@@ -3322,6 +3379,9 @@ def cc_stream_to_sse(cc_stream, model, req_id):
|
|||||||
_url_in_text = re.search(r"https?://[^\s\]'\\>\",]+", text_buf)
|
_url_in_text = re.search(r"https?://[^\s\]'\\>\",]+", text_buf)
|
||||||
if _url_in_text:
|
if _url_in_text:
|
||||||
_synth_url = _url_in_text.group(0).rstrip(")].,;'\\\"")
|
_synth_url = _url_in_text.group(0).rstrip(")].,;'\\\"")
|
||||||
|
if _IS_WINDOWS:
|
||||||
|
_synth_cmd = f"Invoke-WebRequest -Uri '{_synth_url}' -UseBasicParsing -TimeoutSec 15 | Select-Object -ExpandProperty Content | Select-Object -First 200"
|
||||||
|
else:
|
||||||
_synth_cmd = f"curl -sL --max-time 15 '{_synth_url}' 2>/dev/null | head -200"
|
_synth_cmd = f"curl -sL --max-time 15 '{_synth_url}' 2>/dev/null | head -200"
|
||||||
_synth_just = "Auto-synthesized: URL detected in text, fetching"
|
_synth_just = "Auto-synthesized: URL detected in text, fetching"
|
||||||
|
|
||||||
@@ -3330,6 +3390,9 @@ def cc_stream_to_sse(cc_stream, model, req_id):
|
|||||||
_file_m = re.search(r"(?:read|open|view|check|examine|cat|show)\s+(?:the\s+)?(?:file\s+)?[`'\"]?(/[^\s'\"]+\.\w+)", _tl)
|
_file_m = re.search(r"(?:read|open|view|check|examine|cat|show)\s+(?:the\s+)?(?:file\s+)?[`'\"]?(/[^\s'\"]+\.\w+)", _tl)
|
||||||
if _file_m:
|
if _file_m:
|
||||||
_fpath = _file_m.group(1)
|
_fpath = _file_m.group(1)
|
||||||
|
if _IS_WINDOWS:
|
||||||
|
_synth_cmd = f"Get-Content '{_fpath}' -ErrorAction SilentlyContinue | Select-Object -First 200; if (-not $?) {{ Get-Item '{_fpath}' | Select-Object Name,Length,LastWriteTime }}"
|
||||||
|
else:
|
||||||
_synth_cmd = f"cat '{_fpath}' 2>/dev/null | head -200 || ls -la '{_fpath}'"
|
_synth_cmd = f"cat '{_fpath}' 2>/dev/null | head -200 || ls -la '{_fpath}'"
|
||||||
_synth_just = f"Auto-synthesized: file reference detected ({_fpath})"
|
_synth_just = f"Auto-synthesized: file reference detected ({_fpath})"
|
||||||
|
|
||||||
@@ -3358,6 +3421,9 @@ def cc_stream_to_sse(cc_stream, model, req_id):
|
|||||||
if _intent_m:
|
if _intent_m:
|
||||||
_intent_text = _intent_m.group(1).strip()
|
_intent_text = _intent_m.group(1).strip()
|
||||||
if len(_intent_text) > 10 and len(_intent_text) < 200:
|
if len(_intent_text) > 10 and len(_intent_text) < 200:
|
||||||
|
if _IS_WINDOWS:
|
||||||
|
_synth_cmd = f"Write-Output 'Stuck recovery: model intent was: {_intent_text[:100]}'"
|
||||||
|
else:
|
||||||
_synth_cmd = f"echo 'Stuck recovery: model intent was: {_intent_text[:100]}'"
|
_synth_cmd = f"echo 'Stuck recovery: model intent was: {_intent_text[:100]}'"
|
||||||
_synth_just = f"Auto-synthesized from intent text: {_intent_text[:80]}"
|
_synth_just = f"Auto-synthesized from intent text: {_intent_text[:80]}"
|
||||||
|
|
||||||
@@ -3891,11 +3957,13 @@ def _extract_text(content):
|
|||||||
# HTTP Server
|
# HTTP Server
|
||||||
# ═══════════════════════════════════════════════════════════════════
|
# ═══════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
_MAX_REQLOG_LINES = 2000
|
||||||
|
|
||||||
def _log_resp(resp_id, status, output):
|
def _log_resp(resp_id, status, output):
|
||||||
try:
|
try:
|
||||||
import datetime as _dt
|
import datetime as _dt
|
||||||
_lp = os.path.join(_LOG_DIR, "requests.log")
|
_lp = os.path.join(_LOG_DIR, "requests.log")
|
||||||
with open(_lp, "a") as _f:
|
with open(_lp, "a", encoding="utf-8") as _f:
|
||||||
_f.write(f" RESPONSE id={resp_id} status={status}\n")
|
_f.write(f" RESPONSE id={resp_id} status={status}\n")
|
||||||
if output:
|
if output:
|
||||||
for o in output:
|
for o in output:
|
||||||
@@ -3908,6 +3976,11 @@ def _log_resp(resp_id, status, output):
|
|||||||
_f.write(f" -> {ot}\n")
|
_f.write(f" -> {ot}\n")
|
||||||
_f.write(f"{'='*60}\n")
|
_f.write(f"{'='*60}\n")
|
||||||
_f.flush()
|
_f.flush()
|
||||||
|
_f.seek(0)
|
||||||
|
lines = _f.readlines()
|
||||||
|
if len(lines) > _MAX_REQLOG_LINES:
|
||||||
|
with open(_lp, "w", encoding="utf-8") as _f2:
|
||||||
|
_f2.writelines(lines[-_MAX_REQLOG_LINES:])
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -4064,9 +4137,25 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
|||||||
info["total"] = 0
|
info["total"] = 0
|
||||||
self.send_json(200, info)
|
self.send_json(200, info)
|
||||||
elif self.path in ("/health", "/v1/health"):
|
elif self.path in ("/health", "/v1/health"):
|
||||||
import resource as _res
|
|
||||||
_mem_mb = 0
|
_mem_mb = 0
|
||||||
try:
|
try:
|
||||||
|
if _IS_WINDOWS:
|
||||||
|
import ctypes
|
||||||
|
class _PMI(ctypes.Structure):
|
||||||
|
_fields_ = [("cb", ctypes.c_ulong), ("PageFaultCount", ctypes.c_ulong),
|
||||||
|
("PeakWorkingSetSize", ctypes.c_size_t), ("WorkingSetSize", ctypes.c_size_t),
|
||||||
|
("QuotaPeakPagedPoolUsage", ctypes.c_size_t), ("QuotaPagedPoolUsage", ctypes.c_size_t),
|
||||||
|
("QuotaPeakNonPagedPoolUsage", ctypes.c_size_t), ("QuotaNonPagedPoolUsage", ctypes.c_size_t),
|
||||||
|
("PagefileUsage", ctypes.c_size_t), ("PeakPagefileUsage", ctypes.c_size_t)]
|
||||||
|
_pmi = _PMI()
|
||||||
|
_pmi.cb = ctypes.sizeof(_PMI)
|
||||||
|
ctypes.windll.psapi.GetProcessMemoryInfo.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_ulong]
|
||||||
|
ctypes.windll.psapi.GetProcessMemoryInfo.restype = ctypes.c_int
|
||||||
|
ctypes.windll.psapi.GetProcessMemoryInfo(
|
||||||
|
ctypes.windll.kernel32.GetCurrentProcess(), ctypes.byref(_pmi), _pmi.cb)
|
||||||
|
_mem_mb = _pmi.PeakWorkingSetSize / (1024 * 1024)
|
||||||
|
else:
|
||||||
|
import resource as _res
|
||||||
_mem_mb = _res.getrusage(_res.RUSAGE_SELF).ru_maxrss / 1024
|
_mem_mb = _res.getrusage(_res.RUSAGE_SELF).ru_maxrss / 1024
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
@@ -4122,12 +4211,12 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
|||||||
resolved_types = [i.get("type") for i in input_data] if isinstance(input_data, list) else "str"
|
resolved_types = [i.get("type") for i in input_data] if isinstance(input_data, list) else "str"
|
||||||
|
|
||||||
print(f"[{_sid}] prev_id={prev_id} raw={raw_types} resolved={resolved_types}", file=sys.stderr)
|
print(f"[{_sid}] prev_id={prev_id} raw={raw_types} resolved={resolved_types}", file=sys.stderr)
|
||||||
with open(_log_path, "a") as _lf:
|
with open(_log_path, "a", encoding="utf-8") as _lf:
|
||||||
_lf.write(f"\n{'='*60}\n{_ts} [session={_sid}] REQUEST {self.path}\n")
|
_lf.write(f"\n{'='*60}\n{_ts} [session={_sid}] REQUEST {self.path}\n")
|
||||||
_lf.write(f" prev_id={prev_id}\n")
|
_lf.write(f" prev_id={prev_id}\n")
|
||||||
_lf.write(f" raw_input_types={raw_types}\n")
|
_lf.write(f" raw_input_types={raw_types}\n")
|
||||||
_lf.write(f" resolved_input_types={resolved_types}\n")
|
_lf.write(f" resolved_input_types={resolved_types}\n")
|
||||||
_lf.write(f" stream={body.get('stream')} model={body.get('model')}\n")
|
_lf.write(f" stream={body.get('stream')} model={body.get('model')} force_model={FORCE_MODEL}\n")
|
||||||
_lf.write(f" store_keys={list(_response_store.keys())}\n")
|
_lf.write(f" store_keys={list(_response_store.keys())}\n")
|
||||||
if isinstance(input_data, list):
|
if isinstance(input_data, list):
|
||||||
for i, item in enumerate(input_data):
|
for i, item in enumerate(input_data):
|
||||||
@@ -4143,6 +4232,9 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
|||||||
_lf.flush()
|
_lf.flush()
|
||||||
|
|
||||||
model = body.get("model", MODELS[0]["id"] if MODELS else "unknown")
|
model = body.get("model", MODELS[0]["id"] if MODELS else "unknown")
|
||||||
|
if FORCE_MODEL:
|
||||||
|
model = FORCE_MODEL
|
||||||
|
body["model"] = FORCE_MODEL
|
||||||
stream = body.get("stream", False)
|
stream = body.get("stream", False)
|
||||||
_desktop_forced_models = {"gpt-5.4-mini", "gpt-5.4", "gpt-5.5", "gpt-5-codex", "gpt-5.3-codex"}
|
_desktop_forced_models = {"gpt-5.4-mini", "gpt-5.4", "gpt-5.5", "gpt-5-codex", "gpt-5.3-codex"}
|
||||||
_launcher_model = os.environ.get("CODEX_LAUNCHER_MODEL", "")
|
_launcher_model = os.environ.get("CODEX_LAUNCHER_MODEL", "")
|
||||||
@@ -4211,6 +4303,7 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
|||||||
body["input"] = input_data
|
body["input"] = input_data
|
||||||
|
|
||||||
messages = oa_input_to_messages(input_data)
|
messages = oa_input_to_messages(input_data)
|
||||||
|
messages = _inject_stored_reasoning(messages)
|
||||||
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})
|
||||||
@@ -4612,7 +4705,7 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
|||||||
if n_contents > 10:
|
if n_contents > 10:
|
||||||
debug_path = os.path.join(_LOG_DIR, f"gemini-long-ctx-{self._session_id}.json")
|
debug_path = os.path.join(_LOG_DIR, f"gemini-long-ctx-{self._session_id}.json")
|
||||||
try:
|
try:
|
||||||
with open(debug_path, "w") as dbg:
|
with open(debug_path, "w", encoding="utf-8") as dbg:
|
||||||
json.dump({"contents_count": n_contents, "contents_roles": [c.get("role") for c in contents], "has_tools": has_tools, "model": model, "wrapped_size": len(body_b)}, dbg, indent=2)
|
json.dump({"contents_count": n_contents, "contents_roles": [c.get("role") for c in contents], "has_tools": has_tools, "model": model, "wrapped_size": len(body_b)}, dbg, indent=2)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
@@ -4628,7 +4721,7 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
|||||||
if e.code == 400 and OAUTH_PROVIDER.startswith("google"):
|
if e.code == 400 and OAUTH_PROVIDER.startswith("google"):
|
||||||
try:
|
try:
|
||||||
debug_path = os.path.join(_LOG_DIR, "gemini-last-400-request.json")
|
debug_path = os.path.join(_LOG_DIR, "gemini-last-400-request.json")
|
||||||
with open(debug_path, "w") as dbg:
|
with open(debug_path, "w", encoding="utf-8") as dbg:
|
||||||
json.dump({"endpoint": ep, "model": model, "wrapped": wrapped, "error": err_body}, dbg, indent=2)
|
json.dump({"endpoint": ep, "model": model, "wrapped": wrapped, "error": err_body}, dbg, indent=2)
|
||||||
print(f"[{self._session_id}] saved 400 debug request to {debug_path}", file=sys.stderr)
|
print(f"[{self._session_id}] saved 400 debug request to {debug_path}", file=sys.stderr)
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -4940,7 +5033,8 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
try:
|
try:
|
||||||
for event in oa_stream_to_sse(upstream, model, body.get("request_id") or body.get("id")):
|
reasoning_out = {}
|
||||||
|
for event in oa_stream_to_sse(upstream, model, body.get("request_id") or body.get("id"), _reasoning_out=reasoning_out):
|
||||||
if tracker and tracker.cancelled.is_set():
|
if tracker and tracker.cancelled.is_set():
|
||||||
print("[translate-proxy] stream cancelled", file=sys.stderr)
|
print("[translate-proxy] stream cancelled", file=sys.stderr)
|
||||||
break
|
break
|
||||||
@@ -4958,6 +5052,16 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
|||||||
_log_resp(last_resp_id, last_status, last_output)
|
_log_resp(last_resp_id, last_status, last_output)
|
||||||
if last_resp_id and input_data is not None:
|
if last_resp_id and input_data is not None:
|
||||||
store_response(last_resp_id, input_data, last_output)
|
store_response(last_resp_id, input_data, last_output)
|
||||||
|
if reasoning_out.get("text"):
|
||||||
|
with _last_reasoning_lock:
|
||||||
|
_last_reasoning_store[last_resp_id or ""] = {
|
||||||
|
"reasoning": reasoning_out["text"],
|
||||||
|
"tool_calls": reasoning_out.get("tool_calls", []),
|
||||||
|
"ts": time.time(),
|
||||||
|
}
|
||||||
|
while len(_last_reasoning_store) > _MAX_STORED:
|
||||||
|
oldest = next(iter(_last_reasoning_store))
|
||||||
|
del _last_reasoning_store[oldest]
|
||||||
_record_usage(provider, model, success, time.time() - t0, error_type="length" if not success else None)
|
_record_usage(provider, model, success, time.time() - t0, error_type="length" if not success else None)
|
||||||
|
|
||||||
# Auto-learn provider quirks before flushing the bad response to Codex.
|
# Auto-learn provider quirks before flushing the bad response to Codex.
|
||||||
@@ -5925,8 +6029,14 @@ def main():
|
|||||||
global SERVER, _START_TIME
|
global SERVER, _START_TIME
|
||||||
_START_TIME = time.time()
|
_START_TIME = time.time()
|
||||||
_init_runtime()
|
_init_runtime()
|
||||||
signal.signal(signal.SIGTERM, _handle_shutdown_signal)
|
|
||||||
signal.signal(signal.SIGINT, _handle_shutdown_signal)
|
signal.signal(signal.SIGINT, _handle_shutdown_signal)
|
||||||
|
if _IS_WINDOWS:
|
||||||
|
if hasattr(signal, "SIGBREAK"):
|
||||||
|
signal.signal(signal.SIGBREAK, _handle_shutdown_signal)
|
||||||
|
import atexit
|
||||||
|
atexit.register(lambda: setattr(sys.modules[__name__], '_SHUTDOWN_REQUESTED', True))
|
||||||
|
else:
|
||||||
|
signal.signal(signal.SIGTERM, _handle_shutdown_signal)
|
||||||
try:
|
try:
|
||||||
from http.server import ThreadingHTTPServer as _BaseSrv
|
from http.server import ThreadingHTTPServer as _BaseSrv
|
||||||
except ImportError:
|
except ImportError:
|
||||||
@@ -6133,7 +6243,7 @@ Postamble text."""
|
|||||||
_check("FIX23 explore nested JSON: parsed", len(_calls_m) == 1, f"got {len(_calls_m)} calls")
|
_check("FIX23 explore nested JSON: parsed", len(_calls_m) == 1, f"got {len(_calls_m)} calls")
|
||||||
if _calls_m:
|
if _calls_m:
|
||||||
_args_m = json.loads(_calls_m[0].get("arguments", "{}"))
|
_args_m = json.loads(_calls_m[0].get("arguments", "{}"))
|
||||||
_check("FIX23 explore nested JSON: cmd has curl", "curl" in _args_m.get("cmd", ""), f"got {_args_m.get('cmd')!r}")
|
_check("FIX23 explore nested JSON: cmd has fetch cmd", "curl" in _args_m.get("cmd", "") or "Invoke-WebRequest" in _args_m.get("cmd", ""), f"got {_args_m.get('cmd')!r}")
|
||||||
_check("FIX23 explore nested JSON: URL in cmd", "github.rommark.dev" in _args_m.get("cmd", ""), f"missing URL in cmd")
|
_check("FIX23 explore nested JSON: URL in cmd", "github.rommark.dev" in _args_m.get("cmd", ""), f"missing URL in cmd")
|
||||||
|
|
||||||
# Pattern N: require_escalation block (FIX 24)
|
# Pattern N: require_escalation block (FIX 24)
|
||||||
@@ -6143,7 +6253,7 @@ Postamble text."""
|
|||||||
if _calls_n:
|
if _calls_n:
|
||||||
_args_n = json.loads(_calls_n[0].get("arguments", "{}"))
|
_args_n = json.loads(_calls_n[0].get("arguments", "{}"))
|
||||||
_check("FIX24 require_escalation: name is exec_command", _calls_n[0].get("name") == "exec_command", f"got {_calls_n[0].get('name')}")
|
_check("FIX24 require_escalation: name is exec_command", _calls_n[0].get("name") == "exec_command", f"got {_calls_n[0].get('name')}")
|
||||||
_check("FIX24 require_escalation: cmd has curl or echo", "curl" in _args_n.get("cmd", "") or "echo" in _args_n.get("cmd", ""), f"got {_args_n.get('cmd')!r}")
|
_check("FIX24 require_escalation: cmd has fetch or echo", "curl" in _args_n.get("cmd", "") or "echo" in _args_n.get("cmd", "") or "Invoke-WebRequest" in _args_n.get("cmd", "") or "Write-Output" in _args_n.get("cmd", ""), f"got {_args_n.get('cmd')!r}")
|
||||||
|
|
||||||
# Pattern N2: bare request_escalation_permission tag (FIX 24b)
|
# Pattern N2: bare request_escalation_permission tag (FIX 24b)
|
||||||
_esc_bare = 'I want to proceed.\n<request_escalation_permission />\nPlease let me continue.'
|
_esc_bare = 'I want to proceed.\n<request_escalation_permission />\nPlease let me continue.'
|
||||||
@@ -6155,13 +6265,13 @@ Postamble text."""
|
|||||||
# Pattern O: _build_explore_cmd module-level function (FIX 23/25)
|
# Pattern O: _build_explore_cmd module-level function (FIX 23/25)
|
||||||
_cmd_o, _just_o = _build_explore_cmd("https://github.rommark.dev/admin/Z.AI-Chat-for-Android")
|
_cmd_o, _just_o = _build_explore_cmd("https://github.rommark.dev/admin/Z.AI-Chat-for-Android")
|
||||||
_check("FIX23/25 _build_explore_cmd: returns cmd", _cmd_o is not None, "returned None")
|
_check("FIX23/25 _build_explore_cmd: returns cmd", _cmd_o is not None, "returned None")
|
||||||
_check("FIX23/25 _build_explore_cmd: has curl", _cmd_o and "curl" in _cmd_o, f"no curl in {_cmd_o!r}")
|
_check("FIX23/25 _build_explore_cmd: has fetch cmd", _cmd_o and ("curl" in _cmd_o or "Invoke-WebRequest" in _cmd_o), f"no fetch cmd in {_cmd_o!r}")
|
||||||
_check("FIX23/25 _build_explore_cmd: has api path", _cmd_o and "/api/v1/repos/" in _cmd_o, f"no api path in {_cmd_o!r}")
|
_check("FIX23/25 _build_explore_cmd: has api path", _cmd_o and "/api/v1/repos/" in _cmd_o, f"no api path in {_cmd_o!r}")
|
||||||
|
|
||||||
# Pattern O2: _build_explore_cmd with JSON array containing URL
|
# Pattern O2: _build_explore_cmd with JSON array containing URL
|
||||||
_cmd_o2, _ = _build_explore_cmd('[{"content": "https://github.rommark.dev/admin/Z.AI-Chat-for-Android"}]')
|
_cmd_o2, _ = _build_explore_cmd('[{"content": "https://github.rommark.dev/admin/Z.AI-Chat-for-Android"}]')
|
||||||
_check("FIX23/25 _build_explore_cmd from JSON array: returns cmd", _cmd_o2 is not None, "returned None")
|
_check("FIX23/25 _build_explore_cmd from JSON array: returns cmd", _cmd_o2 is not None, "returned None")
|
||||||
_check("FIX23/25 _build_explore_cmd from JSON array: has curl", _cmd_o2 and "curl" in _cmd_o2, f"no curl in {_cmd_o2!r}")
|
_check("FIX23/25 _build_explore_cmd from JSON array: has fetch cmd", _cmd_o2 and ("curl" in _cmd_o2 or "Invoke-WebRequest" in _cmd_o2), f"no fetch cmd in {_cmd_o2!r}")
|
||||||
|
|
||||||
print(f"[CC-SELF-TEST] Results: {_counts[0]} passed, {_counts[1]} failed",
|
print(f"[CC-SELF-TEST] Results: {_counts[0]} passed, {_counts[1]} failed",
|
||||||
file=sys.stderr)
|
file=sys.stderr)
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user