sync: PR #21 - MiMo compat fix, endpoint edit dedup, anti-stall Windows compat, AGENTS.md/CLAUDE.md
This commit is contained in:
83
AGENTS.md
Normal file
83
AGENTS.md
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
# Project: Codex Launcher — Any AI Provider
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
OpenAI Codex CLI & Desktop launcher that proxies to **any** AI provider.
|
||||||
|
Python-only (stdlib), zero pip dependencies. Supports Responses API, Chat Completions,
|
||||||
|
Anthropic Messages API, Command Code, and more via a translation proxy.
|
||||||
|
|
||||||
|
Maintained by:
|
||||||
|
- **roman-ryzenadvanced** — original Linux/GTK development
|
||||||
|
- **cobra91** — Windows port (tkinter GUI, MSIX support)
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
codex-launcher-gui.py (tkinter on Windows / GTK on Linux)
|
||||||
|
→ codex_launcher_lib.py (shared library: endpoints, config, process mgmt)
|
||||||
|
→ translate-proxy.py (HTTP proxy: Responses API → backend API)
|
||||||
|
→ upstream provider (OpenAI, Anthropic, DeepSeek, Antigravity, etc.)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Files
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| `src/codex-launcher-gui.py` | Windows tkinter GUI |
|
||||||
|
| `src/codex_launcher_lib.py` | Shared library (endpoints, config, process management) |
|
||||||
|
| `src/translate-proxy.py` | Translation proxy (core routing, adapters, streaming) |
|
||||||
|
| `src/antigravity_grpc/` | gRPC client for Antigravity provider |
|
||||||
|
|
||||||
|
### Backend Types
|
||||||
|
|
||||||
|
| Type | Wire Protocol | Example |
|
||||||
|
|------|--------------|---------|
|
||||||
|
| `openai-compat` | Chat Completions | DeepSeek, OpenRouter, Crof.ai |
|
||||||
|
| `anthropic` | Anthropic Messages | Anthropic direct, OpenCode Zen |
|
||||||
|
| `command-code` | Command Code /alpha/generate | CommandCode API |
|
||||||
|
| `gemini-oauth-*` | Google OAuth | Google Antigravity |
|
||||||
|
|
||||||
|
## Platform Compatibility
|
||||||
|
|
||||||
|
**MUST work on both Linux and Windows.** No exceptions.
|
||||||
|
|
||||||
|
### Platform-Specific Patterns
|
||||||
|
|
||||||
|
- **Process management**: `os.setsid()` + `os.killpg()` on Linux, `CREATE_NEW_PROCESS_GROUP` on Windows
|
||||||
|
- **Process listing**: `pgrep` on Linux, `tasklist` / `wmic` on Windows
|
||||||
|
- **Desktop launch**: exe path on Linux, `shell:AppsFolder\{AUMID}` for MSIX on Windows
|
||||||
|
- **Signals**: `signal.SIGTERM` on Linux, `taskkill /F` on Windows
|
||||||
|
- **Paths**: `~/.local/bin/` on Linux, `%LOCALAPPDATA%\Programs\Codex-Launcher\` on Windows
|
||||||
|
- **Config**: `~/.codex/config.toml` (same format on both)
|
||||||
|
- **POSIX-only APIs**: `os.getpgid()`, `/proc/{pid}/stat`, `os.setsid()` — always guard with `sys.platform` checks
|
||||||
|
|
||||||
|
### Testing Cross-Platform
|
||||||
|
|
||||||
|
- Never assume Unix-only APIs exist (`pgrep`, `getpgid`, `SIGTERM`)
|
||||||
|
- Use `sys.platform == "win32"` for Windows branches
|
||||||
|
- Test proxy startup on both platforms before committing
|
||||||
|
- Provider presets (PROVIDER_PRESETS) work identically on both
|
||||||
|
|
||||||
|
## Coding Conventions
|
||||||
|
|
||||||
|
- Python 3.8+ stdlib only, zero pip dependencies
|
||||||
|
- `snake_case` for functions/variables, `UPPER_CASE` for globals
|
||||||
|
- Immutable patterns: create new dicts/objects, don't mutate in-place
|
||||||
|
- Error handling: catch at boundaries, never silently swallow errors
|
||||||
|
- Thread-safe: use `threading.Lock` for shared state, `threading.Semaphore` for concurrency
|
||||||
|
|
||||||
|
## Common Pitfalls
|
||||||
|
|
||||||
|
- **MSIX exe paths**: `C:\Program Files\WindowsApps\` exes cannot be launched via `subprocess.Popen` — use `shell:AppsFolder` protocol
|
||||||
|
- **File locking on Windows**: Python can't overwrite files open in another process
|
||||||
|
- **Path separators**: always use `os.path.join()` or `Path` objects, never hardcoded `/`
|
||||||
|
- **Signal handling**: Windows doesn't support `SIGUSR1`/`SIGUSR2` — use events or named pipes
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
- **Run before every commit**: `python -m pytest tests/ -v`
|
||||||
|
- **All tests must pass** before pushing a PR
|
||||||
|
- Test files live in `tests/` directory
|
||||||
|
- Tests use `pytest` (not unittest runner)
|
||||||
|
- Platform-specific tests must skip gracefully on other OS: `pytest.mark.skipif(sys.platform != "linux", reason="Linux-only")`
|
||||||
|
- Never mock filesystem paths with hardcoded separators — use `os.path.join` or `tmp_path`
|
||||||
32
CLAUDE.md
Normal file
32
CLAUDE.md
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Project
|
||||||
|
|
||||||
|
Codex Launcher — Any AI Provider. Run OpenAI Codex CLI & Desktop with any AI provider.
|
||||||
|
|
||||||
|
## Pre-Commit Checklist
|
||||||
|
|
||||||
|
- [ ] Run unit tests: `python -m pytest tests/ -v` (all must pass)
|
||||||
|
- [ ] Verify cross-platform: no `os.getpgid`, `/proc/`, `pgrep`, `SIGUSR*` without `sys.platform` guard
|
||||||
|
- [ ] Check syntax: `python -c "import py_compile; py_compile.compile('src/translate-proxy.py', doraise=True)"`
|
||||||
|
- [ ] No hardcoded Unix paths or Windows-only APIs without platform checks
|
||||||
|
- [ ] No secrets or API keys in source code
|
||||||
|
|
||||||
|
## Development Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run tests
|
||||||
|
python -m pytest tests/ -v
|
||||||
|
|
||||||
|
# Syntax check
|
||||||
|
python -c "import py_compile; py_compile.compile('src/translate-proxy.py', doraise=True)"
|
||||||
|
|
||||||
|
# Run proxy locally
|
||||||
|
python src/translate-proxy.py --port 8080
|
||||||
|
```
|
||||||
|
|
||||||
|
## Agent Guidelines
|
||||||
|
|
||||||
|
See @AGENTS.md for architecture details, platform compatibility rules, and coding conventions.
|
||||||
@@ -5677,23 +5677,40 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
|||||||
break
|
break
|
||||||
self._forward_oa_compat(upstream, stream, model, chat_body, body, input_data, fwd, target, tracker)
|
self._forward_oa_compat(upstream, stream, model, chat_body, body, input_data, fwd, target, tracker)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _is_mimo_provider():
|
||||||
|
return "xiaomimimo.com" in TARGET_URL
|
||||||
|
|
||||||
def _build_chat_body(self, model, messages, body, stream):
|
def _build_chat_body(self, model, messages, body, stream):
|
||||||
chat_body = {"model": model, "messages": messages}
|
chat_body = {"model": model, "messages": messages}
|
||||||
|
is_mimo = self._is_mimo_provider()
|
||||||
for k in ("temperature", "top_p"):
|
for k in ("temperature", "top_p"):
|
||||||
if k in body:
|
if k in body:
|
||||||
chat_body[k] = body[k]
|
chat_body[k] = body[k]
|
||||||
chat_body["max_tokens"] = max(body.get("max_output_tokens", 0), 64000)
|
max_tok = max(body.get("max_output_tokens", 0), 64000)
|
||||||
|
if is_mimo:
|
||||||
|
chat_body["max_completion_tokens"] = max_tok
|
||||||
|
else:
|
||||||
|
chat_body["max_tokens"] = max_tok
|
||||||
tools = oa_convert_tools(body.get("tools"))
|
tools = oa_convert_tools(body.get("tools"))
|
||||||
if tools:
|
if tools:
|
||||||
chat_body["tools"] = tools
|
chat_body["tools"] = tools
|
||||||
if body.get("tool_choice"):
|
if body.get("tool_choice"):
|
||||||
chat_body["tool_choice"] = body["tool_choice"]
|
chat_body["tool_choice"] = body["tool_choice"]
|
||||||
chat_body["stream"] = stream
|
chat_body["stream"] = stream
|
||||||
if not REASONING_ENABLED or REASONING_EFFORT == "none":
|
if is_mimo:
|
||||||
chat_body["enable_thinking"] = False
|
if not REASONING_ENABLED or REASONING_EFFORT == "none":
|
||||||
chat_body["reasoning_effort"] = "none"
|
chat_body["thinking"] = {"type": "disabled"}
|
||||||
|
else:
|
||||||
|
mimo_effort = {"minimal": "low", "max": "high"}.get(REASONING_EFFORT, REASONING_EFFORT)
|
||||||
|
chat_body["thinking"] = {"type": "enabled"}
|
||||||
|
chat_body["reasoning_effort"] = mimo_effort
|
||||||
else:
|
else:
|
||||||
chat_body["reasoning_effort"] = REASONING_EFFORT
|
if not REASONING_ENABLED or REASONING_EFFORT == "none":
|
||||||
|
chat_body["enable_thinking"] = False
|
||||||
|
chat_body["reasoning_effort"] = "none"
|
||||||
|
else:
|
||||||
|
chat_body["reasoning_effort"] = REASONING_EFFORT
|
||||||
return chat_body
|
return chat_body
|
||||||
|
|
||||||
def _handle_antigravity_v2(self, body, model, stream, tracker=None):
|
def _handle_antigravity_v2(self, body, model, stream, tracker=None):
|
||||||
@@ -8572,42 +8589,67 @@ def _handle_shutdown_signal(sig, frame):
|
|||||||
SERVER.shutdown()
|
SERVER.shutdown()
|
||||||
|
|
||||||
def _anti_stall_cleanup():
|
def _anti_stall_cleanup():
|
||||||
|
import subprocess as _sp
|
||||||
my_pid = os.getpid()
|
my_pid = os.getpid()
|
||||||
my_ppid = os.getppid()
|
|
||||||
my_pgid = os.getpgid(0)
|
|
||||||
killed = []
|
killed = []
|
||||||
try:
|
try:
|
||||||
import subprocess as _sp
|
if sys.platform == "win32":
|
||||||
out = _sp.run(["pgrep", "-f", "translate-proxy"], capture_output=True, text=True, timeout=5).stdout.strip()
|
out = _sp.run(
|
||||||
for pid_str in out.splitlines():
|
["tasklist", "/FI", "IMAGENAME eq python.exe", "/FO", "CSV", "/NH"],
|
||||||
pid_str = pid_str.strip()
|
capture_output=True, text=True, timeout=5,
|
||||||
if not pid_str or not pid_str.isdigit():
|
).stdout.strip()
|
||||||
continue
|
for line in out.splitlines():
|
||||||
pid = int(pid_str)
|
parts = line.split(",")
|
||||||
if pid == my_pid or pid == my_ppid:
|
if len(parts) >= 2:
|
||||||
continue
|
pid_str = parts[1].strip('"')
|
||||||
try:
|
if not pid_str.isdigit():
|
||||||
pgid = os.getpgid(pid)
|
continue
|
||||||
if pgid == my_pgid:
|
pid = int(pid_str)
|
||||||
|
if pid == my_pid:
|
||||||
|
continue
|
||||||
|
cmd = _sp.run(
|
||||||
|
["wmic", "process", "where", f"ProcessId={pid}", "/FORMAT:CommandLine", "/VALUE"],
|
||||||
|
capture_output=True, text=True, timeout=5,
|
||||||
|
).stdout.strip()
|
||||||
|
if "translate-proxy" not in cmd:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
_sp.run(["taskkill", "/PID", str(pid), "/F"], capture_output=True, timeout=5)
|
||||||
|
killed.append(pid)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
my_ppid = os.getppid()
|
||||||
|
my_pgid = os.getpgid(0)
|
||||||
|
out = _sp.run(["pgrep", "-f", "translate-proxy"], capture_output=True, text=True, timeout=5).stdout.strip()
|
||||||
|
for pid_str in out.splitlines():
|
||||||
|
pid_str = pid_str.strip()
|
||||||
|
if not pid_str or not pid_str.isdigit():
|
||||||
continue
|
continue
|
||||||
except OSError:
|
pid = int(pid_str)
|
||||||
pass
|
if pid == my_pid or pid == my_ppid:
|
||||||
try:
|
|
||||||
stat = open(f"/proc/{pid}/stat").read().split()
|
|
||||||
start_ticks = int(stat[21])
|
|
||||||
import time as _t
|
|
||||||
ticks_per_sec = os.sysconf('SC_CLK_TCK')
|
|
||||||
start_time = start_ticks / ticks_per_sec
|
|
||||||
age = _t.time() - start_time
|
|
||||||
if age < 60:
|
|
||||||
continue
|
continue
|
||||||
except Exception:
|
try:
|
||||||
continue
|
pgid = os.getpgid(pid)
|
||||||
try:
|
if pgid == my_pgid:
|
||||||
os.kill(pid, signal.SIGTERM)
|
continue
|
||||||
killed.append(pid)
|
except OSError:
|
||||||
except (ProcessLookupError, PermissionError):
|
pass
|
||||||
pass
|
try:
|
||||||
|
stat = open(f"/proc/{pid}/stat").read().split()
|
||||||
|
start_ticks = int(stat[21])
|
||||||
|
ticks_per_sec = os.sysconf('SC_CLK_TCK')
|
||||||
|
start_time = start_ticks / ticks_per_sec
|
||||||
|
age = time.time() - start_time
|
||||||
|
if age < 60:
|
||||||
|
continue
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
os.kill(pid, signal.SIGTERM)
|
||||||
|
killed.append(pid)
|
||||||
|
except (ProcessLookupError, PermissionError):
|
||||||
|
pass
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
try:
|
try:
|
||||||
@@ -8621,6 +8663,7 @@ def _anti_stall_cleanup():
|
|||||||
print(f"[anti-stall] killed {len(killed)} stale proxy process(es): {killed}", flush=True)
|
print(f"[anti-stall] killed {len(killed)} stale proxy process(es): {killed}", flush=True)
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
global SERVER, _START_TIME
|
global SERVER, _START_TIME
|
||||||
_START_TIME = time.time()
|
_START_TIME = time.time()
|
||||||
|
|||||||
Reference in New Issue
Block a user