Compare commits
12 Commits
946f871762
...
869a2625fc
37
CHANGELOG.md
37
CHANGELOG.md
@@ -1,5 +1,42 @@
|
||||
# Changelog
|
||||
|
||||
## v3.10.6 (2026-05-25)
|
||||
|
||||
**Freebuff Integration + Codebuff OAuth Fix + Windows Consolidation**
|
||||
|
||||
### Freebuff (Free DeepSeek/Kimi)
|
||||
- **Freebuff integration**: Free DeepSeek/Kimi models via codebuff.com API
|
||||
- Fixed User-Agent to match official SDK: `ai-sdk/openai-compatible/1.0.25/codebuff`
|
||||
- Fixed metadata fields: `freebuff_instance_id` + `client_id` (base36 random) + `cost_mode: "free"`
|
||||
- Fixed session endpoint: POST empty `{}` body (not `{"model": model}`)
|
||||
- GUI preset aliases: "Freebuff (Free DeepSeek/Kimi)", "FreeBuff", "Codebuff (Free DeepSeek/Kimi)" all map to same backend
|
||||
|
||||
### Codebuff Fix
|
||||
- Fixed Codebuff OAuth: use `www.codebuff.com` (bare `codebuff.com` returns 307 redirect)
|
||||
|
||||
### OAuth Secrets & Credentials (All Providers)
|
||||
- **OAuth Secrets dialog now shows ALL providers**: Google (Antigravity + Gemini CLI) AND Freebuff/Codebuff
|
||||
- **Re-OAuth buttons** for each provider: instantly re-authenticate Google or GitHub/Codebuff
|
||||
- Token status indicators (valid/missing) for each Google provider
|
||||
- Shows logged-in email and auth status for Freebuff/Codebuff
|
||||
- Editable auth token and fingerprint fields for Freebuff/Codebuff
|
||||
|
||||
### Windows
|
||||
- Windows GUI files consolidated into `src/` (merged by cobra91 via PR #1 and PR #2)
|
||||
|
||||
### Proxy & GUI Improvements (cobra91 PR #3)
|
||||
- CROF adaptive logic gated to `crof.ai` only — no more log pollution for other providers
|
||||
- Data directory consolidation: all data now in `codex-proxy/` (was split across `codex-desktop/`, `codex-launcher/`, `codex-proxy/`)
|
||||
- Sticky proxy port: persists in `.last-proxy-port`, reused on restart so Codex Desktop keeps connection
|
||||
- Adaptive compact budget raised from 60% to 80% — avoids premature compaction on large-context models (DeepSeek v4 Pro 1M)
|
||||
- Config cleanup fix: stale `proxy-*.json` cleanup moved after `_init_runtime()` to avoid deleting active config
|
||||
- Windows GUI: added Clear Log, Restart Proxy, View Log buttons
|
||||
- **Linux/Windows feature parity**: both GUIs now have identical features
|
||||
- Windows GUI: ported OAuth Secrets all-providers dialog (Google + Freebuff/Codebuff with Re-OAuth buttons, token status)
|
||||
- Windows GUI: added Codebuff/Freebuff OAuth login flow (GitHub browser-based)
|
||||
- Windows GUI: added Sync from Preset button in endpoint editor
|
||||
- Linux GUI: added Clear Log + Restart Proxy buttons (matching Windows)
|
||||
|
||||
## v3.10.5 (2026-05-25)
|
||||
|
||||
**Windows GUI + Context Compaction for Antigravity/Gemini OAuth**
|
||||
|
||||
38
README.md
38
README.md
@@ -23,7 +23,7 @@ If you want fork it, use the Github copy, here it is:
|
||||
|
||||
<p align="center">
|
||||
<strong>Run OpenAI Codex CLI & Desktop with <em>any</em> AI provider.</strong><br/>
|
||||
Google Antigravity • Gemini CLI • OpenCode • Z.AI • Anthropic • Command Code • Codebuff • OpenRouter • Crof.ai • NVIDIA NIM • OpenAdapter • Kilo.ai • DeepSeek • and more
|
||||
Google Antigravity • Gemini CLI • OpenCode • Z.AI • Anthropic • Command Code • Freebuff • OpenRouter • Crof.ai • NVIDIA NIM • OpenAdapter • Kilo.ai • DeepSeek • and more
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
@@ -630,7 +630,7 @@ curl http://127.0.0.1:PORT/v1/accounts
|
||||
| OpenCode Zen | OpenAI-compat | `https://opencode.ai/zen/v1` |
|
||||
| OpenCode Go | OpenAI-compat | `https://opencode.ai/zen/go/v1` |
|
||||
| Command Code | Command Code | `https://api.commandcode.ai` |
|
||||
| **Codebuff** | **Codebuff** | `https://codebuff.com` *(free DeepSeek/Kimi — OAuth login built-in)* |
|
||||
| **Codebuff / Freebuff** | **Codebuff** | `https://www.codebuff.com` *(free DeepSeek/Kimi — OAuth login built-in)* |
|
||||
| Crof.ai | OpenAI-compat | `https://crof.ai/v1` |
|
||||
| OpenAdapter | OpenAI-compat | `https://api.openadapter.in/v1` |
|
||||
| Z.ai Coding | OpenAI-compat | `https://api.z.ai/api/coding/paas/v4` |
|
||||
@@ -643,14 +643,14 @@ curl http://127.0.0.1:PORT/v1/accounts
|
||||
| Google Antigravity (OAuth) | Antigravity OAuth | `daily-cloudcode-pa.sandbox.googleapis.com` |
|
||||
| Custom | Any | User-defined |
|
||||
|
||||
### Free Models (via Codebuff)
|
||||
Codebuff provides free access to these models — no API key needed:
|
||||
### Free Models (via Codebuff/Freebuff)
|
||||
Codebuff/Freebuff provides free access to these models — no API key needed:
|
||||
- **DeepSeek V4 Pro** — Smartest model
|
||||
- **DeepSeek V4 Flash** — Most efficient
|
||||
- **Kimi K2.6** — Balanced
|
||||
- **MiniMax M2.7** — Fastest
|
||||
|
||||
*Requires: `codebuff login` via GUI OAuth button, or `npm install -g codebuff && codebuff login` (GitHub OAuth)*
|
||||
*Requires: `freebuff login` via GUI OAuth button, or `npm install -g freebuff && freebuff login` (GitHub OAuth)*
|
||||
|
||||
---
|
||||
|
||||
@@ -789,7 +789,7 @@ codex --profile my-profile -c model=my-model
|
||||
|
||||
## Windows Version
|
||||
|
||||
A native **Windows GUI** (tkinter) is available in the [`windows/`](./windows/) folder, ported from the Linux GTK version.
|
||||
A native **Windows GUI** (tkinter) is available in the `src/` folder alongside the Linux version. Both GUIs have **full feature parity**.
|
||||
|
||||
<p align="center">
|
||||
<sub>
|
||||
@@ -802,8 +802,10 @@ A native **Windows GUI** (tkinter) is available in the [`windows/`](./windows/)
|
||||
|
||||
| File | Purpose |
|
||||
|---|---|
|
||||
| `windows/codex-launcher-gui.py` | tkinter GUI — manage endpoints, launch Codex CLI/Desktop |
|
||||
| `windows/codex_launcher_lib.py` | Shared library — proxy lifecycle, config, OAuth, diagnostics |
|
||||
| `src/codex-launcher-gui.py` | tkinter GUI (Windows) — manage endpoints, launch Codex CLI/Desktop |
|
||||
| `src/codex-launcher-gui` | GTK GUI (Linux) — same features, native GTK look |
|
||||
| `src/codex_launcher_lib.py` | Shared library — proxy lifecycle, config, OAuth, diagnostics |
|
||||
| `src/translate-proxy.py` | Proxy — translates Responses API for any provider |
|
||||
|
||||
### How to Run (Windows)
|
||||
|
||||
@@ -811,7 +813,7 @@ Python ≥ 3.8 with tkinter is required (comes with the official Python installe
|
||||
|
||||
```powershell
|
||||
# From repo root
|
||||
cd windows
|
||||
cd src
|
||||
python codex-launcher-gui.py
|
||||
```
|
||||
|
||||
@@ -824,14 +826,18 @@ The GUI will:
|
||||
|
||||
Google OAuth (Antigravity / Gemini CLI) requires a `client_secret_*.json` from [Google Cloud Console](https://console.cloud.google.com/apis/credentials). Use the **OAuth Secrets** button in the GUI to import it — credentials are stored locally in `~/.config/codex-launcher/oauth-secrets.json`, never in the repo.
|
||||
|
||||
### Shared Backend
|
||||
The **OAuth Secrets** dialog shows all providers (Google + Freebuff/Codebuff) with **Re-OAuth buttons** to instantly re-authenticate any provider.
|
||||
|
||||
The same `translate-proxy.py` powers both Linux and Windows. All fixes apply to both:
|
||||
- Antigravity REST model ID mapping
|
||||
- Context compaction (60% of token limit)
|
||||
- Multi-account rotation
|
||||
- Rate limit handling
|
||||
- AI Monitoring / self-revive watchdog
|
||||
### Feature Parity
|
||||
|
||||
Both Linux (GTK) and Windows (tkinter) GUIs have identical features:
|
||||
- All provider presets, endpoint management, BGP routing
|
||||
- OAuth Secrets with all providers + Re-OAuth buttons
|
||||
- AI Monitor, Usage Dashboard, Request History, Benchmark
|
||||
- Clear Log, Restart Proxy, View Log
|
||||
- Doctor, Diagnostic Agent, Profile Backup/Import
|
||||
- Antigravity model mapping, context compaction (80% budget)
|
||||
- Multi-account rotation, rate limit handling
|
||||
|
||||
---
|
||||
|
||||
|
||||
Binary file not shown.
BIN
codex-launcher_3.10.6_all.deb
Normal file
BIN
codex-launcher_3.10.6_all.deb
Normal file
Binary file not shown.
@@ -3,11 +3,11 @@ set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
|
||||
if [ -f "$SCRIPT_DIR/codex-launcher_3.10.5_all.deb" ]; then
|
||||
echo "Installing codex-launcher_3.10.5_all.deb ..."
|
||||
sudo dpkg -i "$SCRIPT_DIR/codex-launcher_3.10.5_all.deb"
|
||||
if [ -f "$SCRIPT_DIR/codex-launcher_3.10.6_all.deb" ]; then
|
||||
echo "Installing codex-launcher_3.10.6_all.deb ..."
|
||||
sudo dpkg -i "$SCRIPT_DIR/codex-launcher_3.10.6_all.deb"
|
||||
echo ""
|
||||
echo "Installed v3.10.5 via .deb package."
|
||||
echo "Installed v3.10.6 via .deb package."
|
||||
echo " translate-proxy.py -> /usr/bin/translate-proxy.py"
|
||||
echo " codex-launcher-gui -> /usr/bin/codex-launcher-gui"
|
||||
echo " cleanup-codex-stale -> /usr/bin/cleanup-codex-stale.sh"
|
||||
|
||||
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-proxy" / "pids.json"
|
||||
CODEX_DIR = Path.home() / ".codex"
|
||||
_local_share = Path(_local)
|
||||
_cache = Path(_local)
|
||||
else:
|
||||
PID_REGISTRY = Path.home() / ".cache" / "codex-proxy" / "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()
|
||||
@@ -1798,7 +1798,7 @@ class LauncherWin(Gtk.Window):
|
||||
# header row
|
||||
hdr = Gtk.Box(spacing=8)
|
||||
vbox.pack_start(hdr, False, False, 0)
|
||||
lbl = Gtk.Label(label="<b>Codex Launcher v3.10.5</b>")
|
||||
lbl = Gtk.Label(label="<b>Codex Launcher v3.10.6</b>")
|
||||
lbl.set_use_markup(True)
|
||||
hdr.pack_start(lbl, False, False, 0)
|
||||
changelog_btn = Gtk.Button(label="Changelog")
|
||||
@@ -1977,6 +1977,13 @@ class LauncherWin(Gtk.Window):
|
||||
assist_btn.connect("clicked", lambda b: self._open_assistant())
|
||||
assist_btn.set_tooltip_text("Open AI coding assistant with streaming, tools, and session management")
|
||||
bb.pack_start(assist_btn, False, False, 0)
|
||||
self._clear_log_btn = Gtk.Button(label="Clear Log")
|
||||
self._clear_log_btn.connect("clicked", lambda b: self._buf.set_text(""))
|
||||
bb.pack_start(self._clear_log_btn, False, False, 0)
|
||||
self._restart_btn = Gtk.Button(label="Restart Proxy")
|
||||
self._restart_btn.connect("clicked", lambda b: self._manual_restart_proxy())
|
||||
self._restart_btn.set_sensitive(False)
|
||||
bb.pack_start(self._restart_btn, False, False, 0)
|
||||
self._kill_btn = Gtk.Button(label="Kill && Cleanup")
|
||||
self._kill_btn.connect("clicked", lambda b: self._kill())
|
||||
self._kill_btn.set_sensitive(False)
|
||||
@@ -2073,6 +2080,7 @@ class LauncherWin(Gtk.Window):
|
||||
self._btn_codex_desktop.set_sensitive(not busy and has_desk)
|
||||
self._btn_codex_cli.set_sensitive(not busy and has_cli)
|
||||
self._kill_btn.set_sensitive(busy)
|
||||
self._restart_btn.set_sensitive(busy)
|
||||
GLib.idle_add(_update)
|
||||
|
||||
def _rebuild_combo(self):
|
||||
@@ -2206,16 +2214,32 @@ class LauncherWin(Gtk.Window):
|
||||
GLib.idle_add(self.log, f"[AI Monitor] Alert: {action} (trigger: {trigger})")
|
||||
|
||||
def _restart_proxy_from_watcher(self):
|
||||
try:
|
||||
ep_name = load_endpoints().get("default")
|
||||
if not ep_name:
|
||||
return
|
||||
for ep in load_endpoints().get("endpoints", []):
|
||||
if ep.get("name") == ep_name:
|
||||
self._start_proxy(ep)
|
||||
break
|
||||
except Exception as e:
|
||||
self.log(f"[AI Monitor] Proxy restart failed: {e}")
|
||||
try:
|
||||
ep_name = load_endpoints().get("default")
|
||||
if not ep_name:
|
||||
return
|
||||
for ep in load_endpoints().get("endpoints", []):
|
||||
if ep.get("name") == ep_name:
|
||||
self._start_proxy(ep)
|
||||
break
|
||||
except Exception as e:
|
||||
self.log(f"[AI Monitor] Proxy restart failed: {e}")
|
||||
|
||||
def _manual_restart_proxy(self):
|
||||
self._kill()
|
||||
time.sleep(1)
|
||||
try:
|
||||
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:
|
||||
self._start_proxy(ep)
|
||||
self.log("Proxy restarted")
|
||||
break
|
||||
except Exception as e:
|
||||
self.log(f"Proxy restart failed: {e}")
|
||||
|
||||
def _open_usage(self):
|
||||
try:
|
||||
@@ -2808,6 +2832,154 @@ class LauncherWin(Gtk.Window):
|
||||
_stop_proxy()
|
||||
Gtk.main_quit()
|
||||
|
||||
def _google_reoauth(self, provider):
|
||||
secrets_path = os.path.expanduser("~/.config/codex-launcher/oauth-secrets.json")
|
||||
try:
|
||||
with open(secrets_path) as f:
|
||||
secrets = json.load(f)
|
||||
except Exception:
|
||||
secrets = {}
|
||||
is_antigravity = provider == "google-antigravity"
|
||||
sec_key = "antigravity" if is_antigravity else "gemini_cli"
|
||||
sec = secrets.get(sec_key, {})
|
||||
client_id = sec.get("client_id", "")
|
||||
client_secret = sec.get("client_secret", "")
|
||||
if not client_id or not client_secret:
|
||||
self._show_error_dialog("Missing OAuth secrets",
|
||||
f"No client_id/client_secret for {sec_key}.\nSet them in OAuth Secrets first.")
|
||||
return
|
||||
token_file = "google-antigravity-oauth-token.json" if is_antigravity else "google-cli-oauth-token.json"
|
||||
token_path = os.path.expanduser(f"~/.cache/codex-proxy/{token_file}")
|
||||
redirect = "urn:ietf:wg:oauth:2.0:oob"
|
||||
auth_url = (f"https://accounts.google.com/o/oauth2/v2/auth?client_id={client_id}"
|
||||
f"&redirect_uri={urllib.parse.quote(redirect)}"
|
||||
f"&response_type=code&scope={urllib.parse.quote('https://www.googleapis.com/auth/cloud-platform')}"
|
||||
f"&access_type=offline&prompt=consent")
|
||||
webbrowser.open(auth_url)
|
||||
code_dlg = Gtk.Dialog(title=f"Re-OAuth: {'Antigravity' if is_antigravity else 'Gemini CLI'}", parent=self, modal=True)
|
||||
code_dlg.add_button("Cancel", Gtk.ResponseType.CANCEL)
|
||||
code_dlg.add_button("Exchange", Gtk.ResponseType.OK)
|
||||
code_dlg.set_default_size(500, 180)
|
||||
ca = code_dlg.get_content_area()
|
||||
ca.set_margin_start(12)
|
||||
ca.set_margin_end(12)
|
||||
ca.set_spacing(6)
|
||||
ca.pack_start(Gtk.Label(label="Browser opened for Google OAuth.\nPaste the authorization code below:", xalign=0), False, False, 0)
|
||||
code_entry = Gtk.Entry()
|
||||
code_entry.set_placeholder_text("4/0AX...")
|
||||
ca.pack_start(code_entry, False, False, 4)
|
||||
ca.show_all()
|
||||
if code_dlg.run() == Gtk.ResponseType.OK:
|
||||
code = code_entry.get_text().strip()
|
||||
if code:
|
||||
try:
|
||||
tok_req = urllib.request.Request("https://oauth2.googleapis.com/token",
|
||||
data=urllib.parse.urlencode({
|
||||
"code": code, "client_id": client_id, "client_secret": client_secret,
|
||||
"redirect_uri": redirect, "grant_type": "authorization_code"
|
||||
}).encode(),
|
||||
headers={"Content-Type": "application/x-www-form-urlencoded"})
|
||||
tok_resp = urllib.request.urlopen(tok_req, timeout=30)
|
||||
tok_data = json.loads(tok_resp.read())
|
||||
tok_data["_updated"] = time.time()
|
||||
os.makedirs(os.path.dirname(token_path), exist_ok=True)
|
||||
with open(token_path, "w") as f:
|
||||
json.dump(tok_data, f, indent=2)
|
||||
self._log(f"[oauth] Refreshed {provider} token → {token_path}")
|
||||
except Exception as e:
|
||||
self._show_error_dialog("Token exchange failed", str(e)[:300])
|
||||
code_dlg.destroy()
|
||||
|
||||
def _codebuff_reoauth(self):
|
||||
self._codebuff_oauth_standalone()
|
||||
|
||||
def _codebuff_oauth_standalone(self):
|
||||
import uuid
|
||||
dlg = Gtk.Dialog(title="Freebuff / Codebuff Login", parent=self, modal=True)
|
||||
dlg.add_button("Cancel", Gtk.ResponseType.CANCEL)
|
||||
dlg.set_default_size(500, 240)
|
||||
area = dlg.get_content_area()
|
||||
area.set_margin_start(16)
|
||||
area.set_margin_end(16)
|
||||
area.set_margin_top(12)
|
||||
area.set_margin_bottom(12)
|
||||
area.set_spacing(8)
|
||||
area.pack_start(Gtk.Label(label="<b>Sign in with GitHub via Codebuff</b>", use_markup=True, xalign=0), False, False, 0)
|
||||
status_lbl = Gtk.Label(label="Requesting login URL…", xalign=0)
|
||||
status_lbl.set_line_wrap(True)
|
||||
status_lbl.set_max_width_chars(60)
|
||||
area.pack_start(status_lbl, False, False, 4)
|
||||
link_lbl = Gtk.Label(xalign=0)
|
||||
link_lbl.set_line_wrap(True)
|
||||
link_lbl.set_max_width_chars(60)
|
||||
area.pack_start(link_lbl, False, False, 4)
|
||||
spinner = Gtk.Spinner()
|
||||
spinner.start()
|
||||
area.pack_start(spinner, False, False, 8)
|
||||
area.show_all()
|
||||
link_lbl.set_visible(False)
|
||||
result = {"success": False, "user": None, "error": None}
|
||||
|
||||
def _thread():
|
||||
try:
|
||||
fp_id = str(uuid.uuid4())
|
||||
body = json.dumps({"fingerprintId": fp_id}).encode()
|
||||
req = urllib.request.Request("https://www.codebuff.com/api/auth/cli/code",
|
||||
data=body, headers={"Content-Type": "application/json", "User-Agent": "codex-launcher/3.10.6"})
|
||||
resp = urllib.request.urlopen(req, timeout=30)
|
||||
rdata = json.loads(resp.read())
|
||||
login_url = rdata.get("loginUrl", "") or rdata.get("login_url", "")
|
||||
fp_hash = rdata.get("fingerprintHash", "") or rdata.get("fingerprint_hash", "")
|
||||
expires_at = rdata.get("expiresAt", 0) or rdata.get("expires_at", 0)
|
||||
if not login_url:
|
||||
result["error"] = "No login URL"
|
||||
GLib.idle_add(_done)
|
||||
return
|
||||
GLib.idle_add(lambda: (status_lbl.set_text("Open this URL in your browser:"),
|
||||
link_lbl.set_markup(f'<a href="{login_url}">{login_url}</a>'),
|
||||
link_lbl.set_visible(True)))
|
||||
webbrowser.open(login_url)
|
||||
poll = f"https://www.codebuff.com/api/auth/cli/status?fingerprintId={urllib.parse.quote(fp_id)}&fingerprintHash={urllib.parse.quote(fp_hash)}&expiresAt={expires_at}"
|
||||
deadline = time.time() + 300
|
||||
while time.time() < deadline:
|
||||
time.sleep(2)
|
||||
try:
|
||||
pr = urllib.request.Request(poll, headers={"User-Agent": "codex-launcher/3.10.6"})
|
||||
pd = json.loads(urllib.request.urlopen(pr, timeout=10).read())
|
||||
if pd.get("user", {}).get("authToken"):
|
||||
result["success"] = True
|
||||
result["user"] = pd["user"]
|
||||
GLib.idle_add(_done)
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
result["error"] = "Timed out"
|
||||
except Exception as e:
|
||||
result["error"] = str(e)[:200]
|
||||
GLib.idle_add(_done)
|
||||
|
||||
def _done():
|
||||
spinner.stop()
|
||||
if result["success"] and result["user"]:
|
||||
u = result["user"]
|
||||
cp = os.path.expanduser("~/.config/manicode/credentials.json")
|
||||
os.makedirs(os.path.dirname(cp), exist_ok=True)
|
||||
creds = {"default": {"id": u.get("id", ""), "name": u.get("name", ""),
|
||||
"email": u.get("email", ""), "authToken": u.get("authToken", ""),
|
||||
"fingerprintId": u.get("fingerprintId", ""), "fingerprintHash": u.get("fingerprintHash", "")}}
|
||||
with open(cp, "w") as f:
|
||||
json.dump(creds, f, indent=2)
|
||||
os.chmod(cp, 0o600)
|
||||
status_lbl.set_text(f"Logged in as {u.get('email', 'OK')}")
|
||||
link_lbl.set_visible(False)
|
||||
GLib.timeout_add_seconds(2, dlg.destroy)
|
||||
else:
|
||||
status_lbl.set_text(f"Failed: {result.get('error', 'unknown')}")
|
||||
|
||||
threading.Thread(target=_thread, daemon=True).start()
|
||||
dlg.connect("response", lambda d, r: d.destroy())
|
||||
dlg.run()
|
||||
|
||||
def _edit_oauth_secrets(self):
|
||||
secrets_path = os.path.expanduser("~/.config/codex-launcher/oauth-secrets.json")
|
||||
try:
|
||||
@@ -2817,10 +2989,10 @@ class LauncherWin(Gtk.Window):
|
||||
data = {"antigravity": {"client_id": "", "client_secret": ""},
|
||||
"gemini_cli": {"client_id": "", "client_secret": ""}}
|
||||
|
||||
dlg = Gtk.Dialog(title="OAuth 2.0 Client Secrets", parent=self, modal=True)
|
||||
dlg = Gtk.Dialog(title="OAuth Secrets & Credentials", parent=self, modal=True)
|
||||
dlg.add_button("Cancel", Gtk.ResponseType.CANCEL)
|
||||
dlg.add_button("Save", Gtk.ResponseType.OK)
|
||||
dlg.set_default_size(540, 420)
|
||||
dlg.set_default_size(580, 650)
|
||||
area = dlg.get_content_area()
|
||||
area.set_margin_start(16)
|
||||
area.set_margin_end(16)
|
||||
@@ -2828,17 +3000,43 @@ class LauncherWin(Gtk.Window):
|
||||
area.set_margin_bottom(12)
|
||||
area.set_spacing(6)
|
||||
|
||||
area.pack_start(Gtk.Label(label="<b>Google OAuth 2.0 credentials</b>\n<small>Stored locally in ~/.config/codex-launcher/oauth-secrets.json</small>", use_markup=True, xalign=0), False, False, 4)
|
||||
sw = Gtk.ScrolledWindow()
|
||||
sw.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
|
||||
vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
|
||||
sw.add(vbox)
|
||||
area.pack_start(sw, True, True, 0)
|
||||
|
||||
vbox.pack_start(Gtk.Label(label="<b>Google OAuth 2.0 Client Credentials</b>\n<small>~/.config/codex-launcher/oauth-secrets.json</small>", use_markup=True, xalign=0), False, False, 4)
|
||||
|
||||
google_token_dir = os.path.expanduser("~/.cache/codex-proxy")
|
||||
fields = {}
|
||||
for section_key, section_label in [("antigravity", "Antigravity (CloudCode)"), ("gemini_cli", "Gemini CLI")]:
|
||||
for section_key, section_label, oauth_prov, token_file in [
|
||||
("antigravity", "Antigravity (CloudCode)", "google-antigravity", "google-antigravity-oauth-token.json"),
|
||||
("gemini_cli", "Gemini CLI", "google-cli", "google-cli-oauth-token.json"),
|
||||
]:
|
||||
section_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=2)
|
||||
hdr_row = Gtk.Box(spacing=6)
|
||||
hdr_row.pack_start(Gtk.Label(label=f"\n<b>{section_label}</b>", use_markup=True, xalign=0), True, True, 0)
|
||||
reauth_btn = Gtk.Button(label="Re-OAuth")
|
||||
reauth_btn.set_size_request(80, -1)
|
||||
reauth_btn.connect("clicked", lambda b, p=oauth_prov: self._google_reoauth(p))
|
||||
hdr_row.pack_end(reauth_btn, False, False, 0)
|
||||
import_btn = Gtk.Button(label="Import JSON")
|
||||
import_btn.set_size_request(100, -1)
|
||||
hdr_row.pack_end(import_btn, False, False, 0)
|
||||
section_box.pack_start(hdr_row, False, False, 2)
|
||||
|
||||
token_path = os.path.join(google_token_dir, token_file)
|
||||
has_token = os.path.exists(token_path)
|
||||
try:
|
||||
with open(token_path) as tf:
|
||||
td = json.load(tf)
|
||||
has_token = bool(td.get("refresh_token") or td.get("access_token"))
|
||||
except Exception:
|
||||
pass
|
||||
tok_status = "Token: <span foreground='#27ae60' weight='bold'>valid</span>" if has_token else "Token: <span foreground='#e67e22' weight='bold'>missing</span>"
|
||||
section_box.pack_start(Gtk.Label(label=tok_status, use_markup=True, xalign=0), False, False, 0)
|
||||
|
||||
sec = data.get(section_key, {})
|
||||
for fk, fl in [("client_id", "Client ID"), ("client_secret", "Client Secret")]:
|
||||
row = Gtk.Box(spacing=6)
|
||||
@@ -2846,7 +3044,7 @@ class LauncherWin(Gtk.Window):
|
||||
lbl.set_size_request(100, -1)
|
||||
entry = Gtk.Entry()
|
||||
entry.set_text(sec.get(fk, ""))
|
||||
entry.set_size_request(380, -1)
|
||||
entry.set_size_request(360, -1)
|
||||
if fk == "client_secret":
|
||||
entry.set_visibility(False)
|
||||
entry.set_invisible_char("*")
|
||||
@@ -2855,10 +3053,63 @@ class LauncherWin(Gtk.Window):
|
||||
section_box.pack_start(row, False, False, 2)
|
||||
fields[(section_key, fk)] = entry
|
||||
import_btn.connect("clicked", lambda b, sk=section_key: self._import_oauth_json(fields, sk))
|
||||
area.pack_start(section_box, False, False, 0)
|
||||
vbox.pack_start(section_box, False, False, 0)
|
||||
|
||||
area.pack_start(Gtk.Label(label="\n<small>Import a client_secret_*.json from Google Cloud Console\nor edit fields manually. console.cloud.google.com → Credentials</small>", use_markup=True, xalign=0), False, False, 4)
|
||||
area.show_all()
|
||||
vbox.pack_start(Gtk.Label(label="<small>Import client_secret_*.json from Google Cloud Console → Credentials</small>", use_markup=True, xalign=0), False, False, 4)
|
||||
|
||||
sep = Gtk.Separator()
|
||||
vbox.pack_start(sep, False, False, 8)
|
||||
|
||||
vbox.pack_start(Gtk.Label(label="\n<b>Freebuff / Codebuff Credentials</b>\n<small>~/.config/manicode/credentials.json</small>", use_markup=True, xalign=0), False, False, 4)
|
||||
|
||||
cb_creds_path = os.path.expanduser("~/.config/manicode/credentials.json")
|
||||
cb_fields = {}
|
||||
try:
|
||||
with open(cb_creds_path) as f:
|
||||
cb_data = json.load(f)
|
||||
except Exception:
|
||||
cb_data = {}
|
||||
cb_default = cb_data.get("default", {})
|
||||
cb_status_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=4)
|
||||
|
||||
cb_info = f"Email: {cb_default.get('email', 'not logged in')}"
|
||||
cb_name = cb_default.get("name", "")
|
||||
if cb_name:
|
||||
cb_info = f"{cb_name} — {cb_info}"
|
||||
has_cb_token = bool(cb_default.get("authToken", ""))
|
||||
status_text = "Logged in" if has_cb_token else "Not logged in"
|
||||
status_color = "#27ae60" if has_cb_token else "#e67e22"
|
||||
cb_info_lbl = Gtk.Label(label=f"{cb_info}\nStatus: <span foreground=\"{status_color}\" weight=\"bold\">{status_text}</span>", use_markup=True, xalign=0)
|
||||
cb_status_box.pack_start(cb_info_lbl, False, False, 2)
|
||||
|
||||
for fk, fl in [("authToken", "Auth Token"), ("fingerprintId", "Fingerprint ID")]:
|
||||
row = Gtk.Box(spacing=6)
|
||||
lbl = Gtk.Label(label=fl + ":", xalign=0)
|
||||
lbl.set_size_request(110, -1)
|
||||
entry = Gtk.Entry()
|
||||
entry.set_text(cb_default.get(fk, ""))
|
||||
entry.set_size_request(360, -1)
|
||||
entry.set_visibility(False)
|
||||
entry.set_invisible_char("*")
|
||||
row.pack_start(lbl, False, False, 0)
|
||||
row.pack_start(entry, True, True, 0)
|
||||
cb_status_box.pack_start(row, False, False, 2)
|
||||
cb_fields[fk] = entry
|
||||
|
||||
cb_btn_row = Gtk.Box(spacing=6)
|
||||
cb_login_btn = Gtk.Button(label="Re-OAuth (GitHub Login)")
|
||||
cb_login_btn.connect("clicked", lambda b: self._codebuff_reoauth())
|
||||
cb_btn_row.pack_start(cb_login_btn, False, False, 0)
|
||||
cb_status_box.pack_start(cb_btn_row, False, False, 4)
|
||||
|
||||
vbox.pack_start(cb_status_box, False, False, 0)
|
||||
|
||||
cb_accounts = cb_data.get("accounts", [])
|
||||
if cb_accounts:
|
||||
vbox.pack_start(Gtk.Label(label=f"\n<small>Additional accounts: {len(cb_accounts)} (edit credentials.json manually)</small>", use_markup=True, xalign=0), False, False, 2)
|
||||
|
||||
vbox.show_all()
|
||||
sw.show_all()
|
||||
|
||||
if dlg.run() == Gtk.ResponseType.OK:
|
||||
for (sk, fk), entry in fields.items():
|
||||
@@ -2872,6 +3123,20 @@ class LauncherWin(Gtk.Window):
|
||||
os.chmod(secrets_path, 0o600)
|
||||
except Exception as e:
|
||||
self._show_error_dialog("Save failed", str(e))
|
||||
cb_updated = dict(cb_default)
|
||||
for fk, entry in cb_fields.items():
|
||||
val = entry.get_text().strip()
|
||||
if val:
|
||||
cb_updated[fk] = val
|
||||
if cb_updated:
|
||||
cb_data["default"] = cb_updated
|
||||
try:
|
||||
os.makedirs(os.path.dirname(cb_creds_path), exist_ok=True)
|
||||
with open(cb_creds_path, "w") as f:
|
||||
json.dump(cb_data, f, indent=2)
|
||||
os.chmod(cb_creds_path, 0o600)
|
||||
except Exception as e:
|
||||
self._show_error_dialog("Save failed", str(e))
|
||||
dlg.destroy()
|
||||
|
||||
def _import_oauth_json(self, fields, section_key):
|
||||
@@ -3706,7 +3971,7 @@ class EditEndpointDialog(Gtk.Dialog):
|
||||
auth_url = "https://www.codebuff.com/api/auth/cli/code"
|
||||
body = json.dumps({"fingerprintId": fingerprint_id}).encode()
|
||||
req = urllib.request.Request(auth_url, data=body,
|
||||
headers={"Content-Type": "application/json", "User-Agent": "codex-launcher/3.10.5"})
|
||||
headers={"Content-Type": "application/json", "User-Agent": "codex-launcher/3.10.6"})
|
||||
resp = urllib.request.urlopen(req, timeout=30)
|
||||
data = json.loads(resp.read())
|
||||
login_url = data.get("loginUrl", "") or data.get("login_url", "")
|
||||
@@ -3731,7 +3996,7 @@ class EditEndpointDialog(Gtk.Dialog):
|
||||
time.sleep(2)
|
||||
try:
|
||||
poll_req = urllib.request.Request(poll_url,
|
||||
headers={"User-Agent": "codex-launcher/3.10.5"})
|
||||
headers={"User-Agent": "codex-launcher/3.10.6"})
|
||||
poll_resp = urllib.request.urlopen(poll_req, timeout=10)
|
||||
poll_data = json.loads(poll_resp.read())
|
||||
user = poll_data.get("user")
|
||||
|
||||
@@ -236,6 +236,7 @@ class EditEndpointDialog:
|
||||
ttk.Button(model_input_frame, text="Add", command=self._add_model).pack(side="left", padx=(4, 0))
|
||||
ttk.Button(model_input_frame, text="Bulk Add", command=self._add_models_from_text).pack(side="left", padx=(4, 0))
|
||||
ttk.Button(model_input_frame, text="Fetch from API", command=self._fetch_models).pack(side="left", padx=(4, 0))
|
||||
ttk.Button(model_input_frame, text="Sync from Preset", command=lambda: self._apply_selected_preset_force()).pack(side="left", padx=(4, 0))
|
||||
ttk.Button(model_input_frame, text="Test Endpoint", command=self._diagnose_endpoint).pack(side="left", padx=(4, 0))
|
||||
|
||||
ttk.Label(main, text="Bulk add (one per line or comma-separated):").pack(anchor="w", pady=(4, 0))
|
||||
@@ -298,6 +299,26 @@ class EditEndpointDialog:
|
||||
if preset["models"]:
|
||||
self._combo_default.set(preset["models"][0])
|
||||
|
||||
def _apply_selected_preset_force(self):
|
||||
preset_name = self._combo_preset.get() or "Custom"
|
||||
preset = PROVIDER_PRESETS.get(preset_name, {})
|
||||
bt = preset.get("backend_type", "openai-compat")
|
||||
bt_display = next((k for k, v in self._bt_map.items() if v == bt), list(self._bt_map.keys())[0])
|
||||
self._combo_type.set(bt_display)
|
||||
self._entry_url.delete(0, "end")
|
||||
self._entry_url.insert(0, preset.get("base_url", ""))
|
||||
cc_ver = preset.get("cc_version", "")
|
||||
if cc_ver:
|
||||
self._entry_cc_ver.delete(0, "end")
|
||||
self._entry_cc_ver.insert(0, cc_ver)
|
||||
if preset.get("models"):
|
||||
self._model_listbox.delete(0, "end")
|
||||
for mid in preset["models"]:
|
||||
self._model_listbox.insert("end", mid)
|
||||
self._refresh_default_combo()
|
||||
if preset["models"]:
|
||||
self._combo_default.set(preset["models"][0])
|
||||
|
||||
def _add_model(self):
|
||||
m = normalize_model_id(self._entry_model.get())
|
||||
if m:
|
||||
@@ -373,7 +394,9 @@ class EditEndpointDialog:
|
||||
preset_name = self._combo_preset.get() or "Custom"
|
||||
preset = PROVIDER_PRESETS.get(preset_name, {})
|
||||
provider = preset.get("oauth_provider", "")
|
||||
if (provider or "").startswith("google"):
|
||||
if provider == "codebuff":
|
||||
self._codebuff_oauth_flow()
|
||||
elif (provider or "").startswith("google"):
|
||||
self._google_oauth_flow(provider)
|
||||
|
||||
def _google_oauth_flow(self, oauth_provider="google-cli"):
|
||||
@@ -564,6 +587,81 @@ class EditEndpointDialog:
|
||||
self._oauth_status_var.set(f"Failed: {msg}")
|
||||
self._dlg.after(3000, dlg.destroy)
|
||||
|
||||
def _codebuff_oauth_flow(self):
|
||||
import uuid
|
||||
oauth_dlg = tk.Toplevel(self._dlg)
|
||||
oauth_dlg.title("Codebuff / Freebuff Login")
|
||||
oauth_dlg.geometry("520x240")
|
||||
oauth_dlg.transient(self._dlg)
|
||||
oauth_dlg.grab_set()
|
||||
tk.Label(oauth_dlg, text="Sign in with GitHub via Codebuff", font=("Segoe UI", 11, "bold")).pack(padx=16, pady=(12, 0), anchor="w")
|
||||
self._cb_status_var = tk.StringVar(value="Requesting login URL...")
|
||||
tk.Label(oauth_dlg, textvariable=self._cb_status_var).pack(padx=16, pady=(8, 0), anchor="w")
|
||||
self._cb_link_lbl = tk.Label(oauth_dlg, text="", fg="blue", cursor="hand2")
|
||||
self._cb_link_lbl.pack(padx=16, anchor="w")
|
||||
self._cb_oauth_result = {"success": False, "user": None, "error": None}
|
||||
self._cb_oauth_dlg = oauth_dlg
|
||||
|
||||
def _thread():
|
||||
try:
|
||||
fp_id = str(uuid.uuid4())
|
||||
body = json.dumps({"fingerprintId": fp_id}).encode()
|
||||
req = urllib.request.Request("https://www.codebuff.com/api/auth/cli/code",
|
||||
data=body, headers={"Content-Type": "application/json", "User-Agent": UA})
|
||||
resp = urllib.request.urlopen(req, timeout=30)
|
||||
rdata = json.loads(resp.read())
|
||||
login_url = rdata.get("loginUrl", "") or rdata.get("login_url", "")
|
||||
fp_hash = rdata.get("fingerprintHash", "") or rdata.get("fingerprint_hash", "")
|
||||
expires_at = rdata.get("expiresAt", 0) or rdata.get("expires_at", 0)
|
||||
if not login_url:
|
||||
self._cb_oauth_result["error"] = "No login URL"
|
||||
self._dlg.after(0, self._codebuff_oauth_done)
|
||||
return
|
||||
def _set_link():
|
||||
self._cb_status_var.set("Open this URL in your browser to log in:")
|
||||
self._cb_link_lbl.configure(text=login_url)
|
||||
self._cb_link_lbl.bind("<Button-1>", lambda e: open_url(login_url))
|
||||
self._dlg.after(0, _set_link)
|
||||
open_url(login_url)
|
||||
poll = f"https://www.codebuff.com/api/auth/cli/status?fingerprintId={urllib.parse.quote(fp_id)}&fingerprintHash={urllib.parse.quote(fp_hash)}&expiresAt={expires_at}"
|
||||
deadline = time.time() + 300
|
||||
while time.time() < deadline:
|
||||
time.sleep(2)
|
||||
try:
|
||||
pr = urllib.request.Request(poll, headers={"User-Agent": UA})
|
||||
pd = json.loads(urllib.request.urlopen(pr, timeout=10).read())
|
||||
if pd.get("user", {}).get("authToken"):
|
||||
self._cb_oauth_result["success"] = True
|
||||
self._cb_oauth_result["user"] = pd["user"]
|
||||
self._dlg.after(0, self._codebuff_oauth_done)
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
self._cb_oauth_result["error"] = "Timed out"
|
||||
except Exception as e:
|
||||
self._cb_oauth_result["error"] = str(e)[:200]
|
||||
self._dlg.after(0, self._codebuff_oauth_done)
|
||||
|
||||
threading.Thread(target=_thread, daemon=True).start()
|
||||
|
||||
def _codebuff_oauth_done(self):
|
||||
if self._cb_oauth_result["success"] and self._cb_oauth_result["user"]:
|
||||
u = self._cb_oauth_result["user"]
|
||||
cb_creds_path = str(HOME / ".config" / "manicode" / "credentials.json")
|
||||
os.makedirs(os.path.dirname(cb_creds_path), exist_ok=True)
|
||||
creds = {"default": {"id": u.get("id", ""), "name": u.get("name", ""),
|
||||
"email": u.get("email", ""), "authToken": u.get("authToken", ""),
|
||||
"fingerprintId": u.get("fingerprintId", ""), "fingerprintHash": u.get("fingerprintHash", "")}}
|
||||
with open(cb_creds_path, "w") as f:
|
||||
json.dump(creds, f, indent=2)
|
||||
self._cb_status_var.set(f"Logged in as {u.get('email', 'OK')}")
|
||||
self._cb_link_lbl.configure(text="")
|
||||
self._entry_key.delete(0, "end")
|
||||
self._entry_key.insert(0, u.get("authToken", ""))
|
||||
self._dlg.after(2000, self._cb_oauth_dlg.destroy)
|
||||
else:
|
||||
self._cb_status_var.set(f"Failed: {self._cb_oauth_result.get('error', 'unknown')}")
|
||||
|
||||
def _cancel(self):
|
||||
self._dlg.destroy()
|
||||
|
||||
@@ -2058,10 +2156,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 +2180,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,45 +2311,204 @@ 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():
|
||||
subprocess.Popen([sys.executable, assist_path], creationflags=subprocess.CREATE_NEW_PROCESS_GROUP if IS_WINDOWS else 0)
|
||||
|
||||
def _google_reoauth(self, provider, parent_dlg=None):
|
||||
is_antigravity = provider == "google-antigravity"
|
||||
sec_key = "antigravity" if is_antigravity else "gemini_cli"
|
||||
secrets = load_oauth_secrets()
|
||||
sec = secrets.get(sec_key, {})
|
||||
client_id = sec.get("client_id", "")
|
||||
client_secret = sec.get("client_secret", "")
|
||||
if not client_id or not client_secret:
|
||||
messagebox.showerror("Missing OAuth secrets",
|
||||
f"No client_id/client_secret for {sec_key}.\nSet them in OAuth Secrets first.")
|
||||
return
|
||||
token_file = "google-antigravity-oauth-token.json" if is_antigravity else "google-cli-oauth-token.json"
|
||||
token_path = str(PROXY_CONFIG_DIR / token_file)
|
||||
redirect = "urn:ietf:wg:oauth:2.0:oob"
|
||||
scope_str = "https://www.googleapis.com/auth/cloud-platform"
|
||||
auth_url = (f"https://accounts.google.com/o/oauth2/v2/auth?client_id={client_id}"
|
||||
f"&redirect_uri={urllib.parse.quote(redirect)}"
|
||||
f"&response_type=code&scope={urllib.parse.quote(scope_str)}"
|
||||
f"&access_type=offline&prompt=consent")
|
||||
open_url(auth_url)
|
||||
code = tk.simpledialog.askstring("Re-OAuth",
|
||||
f"Paste the authorization code for {'Antigravity' if is_antigravity else 'Gemini CLI'}:",
|
||||
parent=parent_dlg or self._root)
|
||||
if not code:
|
||||
return
|
||||
try:
|
||||
tok_req = urllib.request.Request("https://oauth2.googleapis.com/token",
|
||||
data=urllib.parse.urlencode({
|
||||
"code": code, "client_id": client_id, "client_secret": client_secret,
|
||||
"redirect_uri": redirect, "grant_type": "authorization_code"
|
||||
}).encode(),
|
||||
headers={"Content-Type": "application/x-www-form-urlencoded"})
|
||||
tok_resp = urllib.request.urlopen(tok_req, timeout=30)
|
||||
tok_data = json.loads(tok_resp.read())
|
||||
tok_data["_updated"] = time.time()
|
||||
tok_data["client_id"] = client_id
|
||||
tok_data["client_secret"] = client_secret
|
||||
tok_data["provider_kind"] = "antigravity" if is_antigravity else "cli"
|
||||
os.makedirs(os.path.dirname(token_path), exist_ok=True)
|
||||
with open(token_path, "w") as f:
|
||||
json.dump(tok_data, f, indent=2)
|
||||
self.log(f"[oauth] Refreshed {provider} token")
|
||||
except Exception as e:
|
||||
messagebox.showerror("Token exchange failed", str(e)[:300])
|
||||
|
||||
def _codebuff_reoauth_standalone(self, parent_dlg=None):
|
||||
import uuid
|
||||
oauth_dlg = tk.Toplevel(parent_dlg or self._root)
|
||||
oauth_dlg.title("Freebuff / Codebuff Login")
|
||||
oauth_dlg.geometry("520x240")
|
||||
if parent_dlg:
|
||||
oauth_dlg.transient(parent_dlg)
|
||||
else:
|
||||
oauth_dlg.transient(self._root)
|
||||
oauth_dlg.grab_set()
|
||||
tk.Label(oauth_dlg, text="Sign in with GitHub via Codebuff", font=("Segoe UI", 11, "bold")).pack(padx=16, pady=(12, 0), anchor="w")
|
||||
status_var = tk.StringVar(value="Requesting login URL...")
|
||||
tk.Label(oauth_dlg, textvariable=status_var).pack(padx=16, pady=(8, 0), anchor="w")
|
||||
link_lbl = tk.Label(oauth_dlg, text="", fg="blue", cursor="hand2")
|
||||
link_lbl.pack(padx=16, anchor="w")
|
||||
result = {"success": False, "user": None, "error": None}
|
||||
|
||||
def _thread():
|
||||
try:
|
||||
fp_id = str(uuid.uuid4())
|
||||
body = json.dumps({"fingerprintId": fp_id}).encode()
|
||||
req = urllib.request.Request("https://www.codebuff.com/api/auth/cli/code",
|
||||
data=body, headers={"Content-Type": "application/json", "User-Agent": UA})
|
||||
resp = urllib.request.urlopen(req, timeout=30)
|
||||
rdata = json.loads(resp.read())
|
||||
login_url = rdata.get("loginUrl", "") or rdata.get("login_url", "")
|
||||
fp_hash = rdata.get("fingerprintHash", "") or rdata.get("fingerprint_hash", "")
|
||||
expires_at = rdata.get("expiresAt", 0) or rdata.get("expires_at", 0)
|
||||
if not login_url:
|
||||
result["error"] = "No login URL"
|
||||
self._root.after(0, _done)
|
||||
return
|
||||
def _set():
|
||||
status_var.set("Open this URL in your browser to log in:")
|
||||
link_lbl.configure(text=login_url)
|
||||
link_lbl.bind("<Button-1>", lambda e: open_url(login_url))
|
||||
self._root.after(0, _set)
|
||||
open_url(login_url)
|
||||
poll = f"https://www.codebuff.com/api/auth/cli/status?fingerprintId={urllib.parse.quote(fp_id)}&fingerprintHash={urllib.parse.quote(fp_hash)}&expiresAt={expires_at}"
|
||||
deadline = time.time() + 300
|
||||
while time.time() < deadline:
|
||||
time.sleep(2)
|
||||
try:
|
||||
pr = urllib.request.Request(poll, headers={"User-Agent": UA})
|
||||
pd = json.loads(urllib.request.urlopen(pr, timeout=10).read())
|
||||
if pd.get("user", {}).get("authToken"):
|
||||
result["success"] = True
|
||||
result["user"] = pd["user"]
|
||||
self._root.after(0, _done)
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
result["error"] = "Timed out"
|
||||
except Exception as e:
|
||||
result["error"] = str(e)[:200]
|
||||
self._root.after(0, _done)
|
||||
|
||||
def _done():
|
||||
if result["success"] and result["user"]:
|
||||
u = result["user"]
|
||||
cb_creds_path = str(HOME / ".config" / "manicode" / "credentials.json")
|
||||
os.makedirs(os.path.dirname(cb_creds_path), exist_ok=True)
|
||||
creds = {"default": {"id": u.get("id", ""), "name": u.get("name", ""),
|
||||
"email": u.get("email", ""), "authToken": u.get("authToken", ""),
|
||||
"fingerprintId": u.get("fingerprintId", ""), "fingerprintHash": u.get("fingerprintHash", "")}}
|
||||
with open(cb_creds_path, "w") as f:
|
||||
json.dump(creds, f, indent=2)
|
||||
status_var.set(f"Logged in as {u.get('email', 'OK')}")
|
||||
link_lbl.configure(text="")
|
||||
self._root.after(2000, oauth_dlg.destroy)
|
||||
else:
|
||||
status_var.set(f"Failed: {result.get('error', 'unknown')}")
|
||||
|
||||
threading.Thread(target=_thread, daemon=True).start()
|
||||
oauth_dlg.wait_window()
|
||||
|
||||
def _edit_oauth_secrets(self):
|
||||
import tkinter.simpledialog
|
||||
data = load_oauth_secrets()
|
||||
if not data:
|
||||
data = {"antigravity": {"client_id": "", "client_secret": ""},
|
||||
"gemini_cli": {"client_id": "", "client_secret": ""}}
|
||||
|
||||
dlg = tk.Toplevel(self._root)
|
||||
dlg.title("OAuth 2.0 Client Secrets")
|
||||
dlg.geometry("600x450")
|
||||
dlg.title("OAuth Secrets & Credentials")
|
||||
dlg.geometry("620x650")
|
||||
dlg.transient(self._root)
|
||||
dlg.grab_set()
|
||||
|
||||
frame = ttk.Frame(dlg, padding=16)
|
||||
frame.pack(fill="both", expand=True)
|
||||
canvas = tk.Canvas(dlg)
|
||||
scrollbar = ttk.Scrollbar(dlg, orient="vertical", command=canvas.yview)
|
||||
frame = ttk.Frame(canvas, padding=16)
|
||||
frame.bind("<Configure>", lambda e: canvas.configure(scrollregion=canvas.bbox("all")))
|
||||
canvas.create_window((0, 0), window=frame, anchor="nw")
|
||||
canvas.configure(yscrollcommand=scrollbar.set)
|
||||
canvas.pack(side="left", fill="both", expand=True)
|
||||
scrollbar.pack(side="right", fill="y")
|
||||
|
||||
ttk.Label(frame, text="Google OAuth 2.0 credentials", font=("Segoe UI", 10, "bold")).pack(anchor="w")
|
||||
ttk.Label(frame, text=f"Stored locally in {OAUTH_SECRETS_PATH}", foreground="gray").pack(anchor="w", pady=(0, 8))
|
||||
ttk.Label(frame, text="Google OAuth 2.0 Client Credentials", font=("Segoe UI", 10, "bold")).pack(anchor="w")
|
||||
ttk.Label(frame, text=str(OAUTH_SECRETS_PATH), foreground="gray").pack(anchor="w", pady=(0, 8))
|
||||
|
||||
fields = {}
|
||||
nf = ttk.Frame(frame)
|
||||
nf.pack(fill="x")
|
||||
row = 0
|
||||
for section_key, section_label in [("antigravity", "Antigravity (CloudCode)"), ("gemini_cli", "Gemini CLI")]:
|
||||
ttk.Label(nf, text=f"\n{section_label}", font=("Segoe UI", 9, "bold")).grid(row=row, column=0, columnspan=3, sticky="w", pady=(8, 2))
|
||||
google_token_dir = str(PROXY_CONFIG_DIR)
|
||||
for section_key, section_label, oauth_prov, token_file in [
|
||||
("antigravity", "Antigravity (CloudCode)", "google-antigravity", "google-antigravity-oauth-token.json"),
|
||||
("gemini_cli", "Gemini CLI", "google-cli", "google-cli-oauth-token.json"),
|
||||
]:
|
||||
ttk.Label(nf, text=f"\n{section_label}", font=("Segoe UI", 9, "bold")).grid(row=row, column=0, columnspan=4, sticky="w", pady=(8, 2))
|
||||
row += 1
|
||||
sec = data.get(section_key, {})
|
||||
token_path = os.path.join(google_token_dir, token_file)
|
||||
has_token = False
|
||||
try:
|
||||
with open(token_path) as tf:
|
||||
td = json.load(tf)
|
||||
has_token = bool(td.get("refresh_token") or td.get("access_token"))
|
||||
except Exception:
|
||||
pass
|
||||
token_status = "Token: valid" if has_token else "Token: missing"
|
||||
token_color = "#2ea043" if has_token else "#d29922"
|
||||
ttk.Label(nf, text=token_status, foreground=token_color).grid(row=row, column=0, sticky="w", padx=(8, 4), pady=2)
|
||||
import_btn = ttk.Button(nf, text="Import JSON",
|
||||
command=lambda sk=section_key: self._import_oauth_json(fields, sk))
|
||||
import_btn.grid(row=row, column=2, padx=(4, 0), pady=2, sticky="e")
|
||||
reauth_btn = ttk.Button(nf, text="Re-OAuth",
|
||||
command=lambda p=oauth_prov: self._google_reoauth(p, dlg))
|
||||
reauth_btn.grid(row=row, column=3, padx=(4, 0), pady=2, sticky="e")
|
||||
row += 1
|
||||
for fk, fl in [("client_id", "Client ID"), ("client_secret", "Client Secret")]:
|
||||
ttk.Label(nf, text=fl + ":").grid(row=row, column=0, sticky="w", padx=(8, 4), pady=2)
|
||||
entry = ttk.Entry(nf, width=60)
|
||||
entry = ttk.Entry(nf, width=55)
|
||||
entry.insert(0, sec.get(fk, ""))
|
||||
entry.grid(row=row, column=1, sticky="ew", pady=2)
|
||||
entry.grid(row=row, column=1, columnspan=3, sticky="ew", pady=2)
|
||||
if fk == "client_secret":
|
||||
entry.configure(show="*")
|
||||
fields[(section_key, fk)] = entry
|
||||
@@ -2237,7 +2516,50 @@ class LauncherWin:
|
||||
|
||||
nf.columnconfigure(1, weight=1)
|
||||
|
||||
ttk.Label(frame, text="\nImport a client_secret_*.json from Google Cloud Console\nconsole.cloud.google.com → Credentials", foreground="gray").pack(anchor="w")
|
||||
ttk.Label(frame, text="Import client_secret_*.json from Google Cloud Console → Credentials", foreground="gray").pack(anchor="w")
|
||||
|
||||
ttk.Separator(frame).pack(fill="x", pady=(12, 8))
|
||||
|
||||
ttk.Label(frame, text="Freebuff / Codebuff Credentials", font=("Segoe UI", 10, "bold")).pack(anchor="w")
|
||||
ttk.Label(frame, text=str(HOME / ".config" / "manicode" / "credentials.json"), foreground="gray").pack(anchor="w", pady=(0, 8))
|
||||
|
||||
cb_creds_path = str(HOME / ".config" / "manicode" / "credentials.json")
|
||||
cb_fields = {}
|
||||
try:
|
||||
with open(cb_creds_path) as f:
|
||||
cb_data = json.load(f)
|
||||
except Exception:
|
||||
cb_data = {}
|
||||
cb_default = cb_data.get("default", {})
|
||||
|
||||
cb_info = f"Email: {cb_default.get('email', 'not logged in')}"
|
||||
cb_name = cb_default.get("name", "")
|
||||
if cb_name:
|
||||
cb_info = f"{cb_name} — {cb_info}"
|
||||
has_cb_token = bool(cb_default.get("authToken", ""))
|
||||
status_text = "Logged in" if has_cb_token else "Not logged in"
|
||||
status_color = "#2ea043" if has_cb_token else "#d29922"
|
||||
ttk.Label(frame, text=cb_info).pack(anchor="w")
|
||||
ttk.Label(frame, text=f"Status: {status_text}", foreground=status_color, font=("Segoe UI", 9, "bold")).pack(anchor="w", pady=(0, 4))
|
||||
|
||||
cb_nf = ttk.Frame(frame)
|
||||
cb_nf.pack(fill="x")
|
||||
cb_row = [0]
|
||||
for fk, fl in [("authToken", "Auth Token"), ("fingerprintId", "Fingerprint ID")]:
|
||||
ttk.Label(cb_nf, text=fl + ":").grid(row=cb_row[0], column=0, sticky="w", padx=(8, 4), pady=2)
|
||||
entry = ttk.Entry(cb_nf, width=55, show="*")
|
||||
entry.insert(0, cb_default.get(fk, ""))
|
||||
entry.grid(row=cb_row[0], column=1, sticky="ew", pady=2)
|
||||
cb_fields[fk] = entry
|
||||
cb_row[0] += 1
|
||||
cb_nf.columnconfigure(1, weight=1)
|
||||
|
||||
ttk.Button(frame, text="Re-OAuth (GitHub Login)",
|
||||
command=lambda: self._codebuff_reoauth_standalone(dlg)).pack(anchor="w", pady=(4, 0))
|
||||
|
||||
cb_accounts = cb_data.get("accounts", [])
|
||||
if cb_accounts:
|
||||
ttk.Label(frame, text=f"Additional accounts: {len(cb_accounts)} (edit credentials.json manually)", foreground="gray").pack(anchor="w")
|
||||
|
||||
btnf = ttk.Frame(frame)
|
||||
btnf.pack(fill="x", pady=(12, 0))
|
||||
@@ -2255,6 +2577,20 @@ class LauncherWin:
|
||||
except Exception as e:
|
||||
messagebox.showerror("Save failed", str(e), parent=dlg)
|
||||
return
|
||||
cb_updated = dict(cb_default)
|
||||
for fk, entry in cb_fields.items():
|
||||
val = entry.get().strip()
|
||||
if val:
|
||||
cb_updated[fk] = val
|
||||
if cb_updated:
|
||||
cb_data["default"] = cb_updated
|
||||
try:
|
||||
os.makedirs(os.path.dirname(cb_creds_path), exist_ok=True)
|
||||
with open(cb_creds_path, "w") as f:
|
||||
json.dump(cb_data, f, indent=2)
|
||||
except Exception as e:
|
||||
messagebox.showerror("Save failed", str(e), parent=dlg)
|
||||
return
|
||||
dlg.destroy()
|
||||
|
||||
save_btn.configure(command=_save)
|
||||
@@ -2452,6 +2788,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):
|
||||
@@ -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"
|
||||
@@ -83,6 +83,27 @@ model_catalog_json = ""
|
||||
"""
|
||||
|
||||
CHANGELOG = [
|
||||
("3.10.6", "2026-05-25", [
|
||||
"Freebuff integration: free DeepSeek/Kimi via codebuff.com API",
|
||||
"Fixed Freebuff User-Agent to match official SDK (ai-sdk/openai-compatible/1.0.25/codebuff)",
|
||||
"Fixed Freebuff metadata: freebuff_instance_id + client_id (base36) + cost_mode: free",
|
||||
"Fixed Codebuff OAuth: use www.codebuff.com (307 redirect on bare domain)",
|
||||
"GUI preset aliases: Freebuff, FreeBuff, Codebuff all map to same backend",
|
||||
"Windows GUI consolidated into src/ (merged by cobra91)",
|
||||
"CROF adaptive logic gated to crof.ai only — no log pollution for other providers",
|
||||
"Data dir consolidation: all data in codex-proxy/",
|
||||
"Sticky proxy port: persists in .last-proxy-port for restart persistence",
|
||||
"Adaptive compact budget raised 60% to 80% for large-context models",
|
||||
"Config cleanup fix: stale proxy-*.json moved after _init_runtime()",
|
||||
"Windows GUI: Clear Log, Restart Proxy, View Log buttons (cobra91 PR #3)",
|
||||
"OAuth Secrets dialog shows all providers: Google + Freebuff/Codebuff",
|
||||
"Re-OAuth buttons for each provider: re-authenticate Google or GitHub/Codebuff",
|
||||
"Token status indicators (valid/missing) for each Google provider",
|
||||
"Shows logged-in email and auth status for Freebuff/Codebuff",
|
||||
"Linux/Windows feature parity: both GUIs have identical features",
|
||||
"Windows: OAuth Secrets all-providers + Codebuff OAuth + Sync from Preset",
|
||||
"Linux: Clear Log + Restart Proxy buttons added",
|
||||
]),
|
||||
("3.10.5", "2026-05-25", [
|
||||
"Context compaction for Antigravity/Gemini OAuth — prevents token limit errors",
|
||||
"Aggressive compaction policies at 60% of model context limit",
|
||||
@@ -1388,9 +1409,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 +1451,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 +1539,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"],
|
||||
@@ -157,9 +157,13 @@ Architecture:
|
||||
|
||||
import json, http.server, socketserver, urllib.request, urllib.parse, urllib.error, re
|
||||
import time, uuid, os, sys, argparse, threading, socket, collections, contextlib, signal
|
||||
import secrets, string
|
||||
import dataclasses
|
||||
import http.client
|
||||
import selectors
|
||||
import tempfile
|
||||
|
||||
_IS_WINDOWS = sys.platform == "win32"
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
# Config
|
||||
@@ -241,13 +245,23 @@ MODELS = []
|
||||
CC_VERSION = ""
|
||||
REASONING_ENABLED = True
|
||||
REASONING_EFFORT = "medium"
|
||||
FORCE_MODEL = ""
|
||||
BGP_ROUTES = []
|
||||
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)
|
||||
_REQUESTS_DIR = os.path.join(_LOG_DIR, "requests")
|
||||
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))
|
||||
except Exception:
|
||||
pass
|
||||
_stats_path = os.path.join(_LOG_DIR, "usage-stats.json")
|
||||
_provider_caps_path = os.path.join(_LOG_DIR, "provider-caps.json")
|
||||
_stats_lock = threading.Lock()
|
||||
@@ -257,7 +271,7 @@ _STATS_FLUSH_INTERVAL = 5.0
|
||||
_STATS = {}
|
||||
|
||||
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:
|
||||
_LOG_FILE = None
|
||||
|
||||
@@ -273,6 +287,9 @@ _deepseek_reasoning_store = {}
|
||||
_deepseek_reasoning_lock = threading.Lock()
|
||||
_MAX_DS_STORED = 100
|
||||
|
||||
_last_reasoning_store = {}
|
||||
_last_reasoning_lock = threading.Lock()
|
||||
|
||||
_crof_lock = threading.Lock()
|
||||
_provider_caps_lock = threading.Lock()
|
||||
_provider_caps = None
|
||||
@@ -302,7 +319,10 @@ _CODEBUFF_AGENT_MAP = {
|
||||
"moonshotai/kimi-k2.6": "base2-free-kimi",
|
||||
"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_session_cache = {"instance_id": None, "expires": 0, "model": None}
|
||||
_codebuff_token_lock = threading.Lock()
|
||||
@@ -634,7 +654,7 @@ def _refresh_google_token(token_data, token_path):
|
||||
new_tokens = json.loads(resp.read())
|
||||
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)
|
||||
with open(token_path, "w") as f:
|
||||
with open(token_path, "w", encoding="utf-8") as f:
|
||||
json.dump(token_data, f, indent=2)
|
||||
print("[oauth] token refreshed OK", file=sys.stderr)
|
||||
return token_data["access_token"]
|
||||
@@ -699,7 +719,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():
|
||||
@@ -727,7 +746,7 @@ def _fetch_antigravity_version():
|
||||
version = m.group(0)
|
||||
try:
|
||||
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)
|
||||
except Exception:
|
||||
pass
|
||||
@@ -762,6 +781,7 @@ def _init_runtime():
|
||||
CC_VERSION = CONFIG.get("cc_version", "")
|
||||
REASONING_ENABLED = CONFIG.get("reasoning_enabled", True)
|
||||
REASONING_EFFORT = CONFIG.get("reasoning_effort", "medium")
|
||||
FORCE_MODEL = (CONFIG.get("force_model") or "").strip()
|
||||
BGP_ROUTES = CONFIG.get("bgp_routes", [])
|
||||
_api_key_pool = None
|
||||
if API_KEY and "," in API_KEY and not OAUTH_PROVIDER.startswith("google") and BACKEND not in ("codebuff", "freebuff"):
|
||||
@@ -903,7 +923,7 @@ def _load_provider_caps():
|
||||
def _save_provider_caps():
|
||||
try:
|
||||
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)
|
||||
except Exception as e:
|
||||
print(f"[provider-sensor] failed to save caps: {e}", file=sys.stderr)
|
||||
@@ -959,7 +979,7 @@ def _refresh_oauth_token_for(api_key, oauth_provider):
|
||||
new_tokens = json.loads(resp.read())
|
||||
tokens["access_token"] = new_tokens.get("access_token", tokens.get("access_token"))
|
||||
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)
|
||||
print("[oauth] token refreshed OK", file=sys.stderr)
|
||||
return tokens["access_token"]
|
||||
@@ -983,7 +1003,7 @@ def _load_stats():
|
||||
|
||||
def _atomic_write_json(path, obj):
|
||||
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)
|
||||
os.replace(tmp, path)
|
||||
|
||||
@@ -1277,8 +1297,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")
|
||||
@@ -1297,7 +1317,7 @@ def _load_bgp_stats():
|
||||
|
||||
def _save_bgp_stats(stats):
|
||||
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)
|
||||
os.replace(tmp, _BGP_STATS_PATH)
|
||||
|
||||
@@ -1346,6 +1366,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}
|
||||
@@ -1371,7 +1393,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, {})
|
||||
@@ -1416,7 +1439,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):
|
||||
@@ -1627,7 +1651,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
|
||||
@@ -1790,7 +1814,7 @@ def save_request_snapshot(request_id, body):
|
||||
}
|
||||
path = os.path.join(_REQUESTS_DIR, f"{request_id}.json")
|
||||
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)
|
||||
os.replace(tmp, path)
|
||||
_rotate_snapshots()
|
||||
@@ -1813,7 +1837,7 @@ def update_snapshot_response(request_id, status, duration_s=None, error=None):
|
||||
meta["error"] = str(error)[:200]
|
||||
snapshot["_meta"] = meta
|
||||
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)
|
||||
os.replace(tmp, path)
|
||||
except Exception:
|
||||
@@ -1865,6 +1889,27 @@ def _bucket_for_route(route):
|
||||
# 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):
|
||||
msgs = []
|
||||
tool_name_by_id = {}
|
||||
@@ -2384,10 +2429,10 @@ def an_stream_to_sse(stream, model, req_id):
|
||||
"status": status, "created": int(time.time()), "output": completed}})
|
||||
|
||||
_DEFAULT_CC_CONFIG = {
|
||||
"workingDir": "/tmp",
|
||||
"workingDir": tempfile.gettempdir(),
|
||||
"date": "",
|
||||
"environment": "linux",
|
||||
"shell": "bash",
|
||||
"environment": "windows" if _IS_WINDOWS else "linux",
|
||||
"shell": "powershell" if _IS_WINDOWS else "bash",
|
||||
"files": [],
|
||||
"structure": [],
|
||||
"isGitRepo": False,
|
||||
@@ -2462,13 +2507,24 @@ def _build_explore_cmd(text_for_url):
|
||||
api_base = repo_url.replace("/admin/", "/api/v1/repos/")
|
||||
else:
|
||||
api_base = repo_url
|
||||
cmd = (
|
||||
f"cd /tmp && "
|
||||
f"curl -sL --max-time 15 '{api_base}/contents/README.md' 2>/dev/null | "
|
||||
f"python3 -c \"import sys,json,base64; d=json.load(sys.stdin); print(base64.b64decode(d['content']).decode())\" 2>/dev/null | head -600 && "
|
||||
f"curl -sL --max-time 15 '{api_base}/contents' 2>/dev/null | python3 -c \"import sys,json; d=json.load(sys.stdin); print('\\n'.join(f'{{x.get(\'path\')}} {{x.get(\'type\')}}' for x in d[:50]))\" 2>/dev/null && "
|
||||
f"curl -sL --max-time 15 '{api_base}/releases' 2>/dev/null | python3 -c \"import sys,json; d=json.load(sys.stdin); print(json.dumps(d[:3], indent=2)[:2000])\" 2>/dev/null"
|
||||
)
|
||||
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 = (
|
||||
f"cd /tmp && "
|
||||
f"curl -sL --max-time 15 '{api_base}/contents/README.md' 2>/dev/null | "
|
||||
f"python3 -c \"import sys,json,base64; d=json.load(sys.stdin); print(base64.b64decode(d['content']).decode())\" 2>/dev/null | head -600 && "
|
||||
f"curl -sL --max-time 15 '{api_base}/contents' 2>/dev/null | python3 -c \"import sys,json; d=json.load(sys.stdin); print('\\n'.join(f'{{x.get(\'path\')}} {{x.get(\'type\')}}' for x in d[:50]))\" 2>/dev/null && "
|
||||
f"curl -sL --max-time 15 '{api_base}/releases' 2>/dev/null | python3 -c \"import sys,json; d=json.load(sys.stdin); print(json.dumps(d[:3], indent=2)[:2000])\" 2>/dev/null"
|
||||
)
|
||||
return cmd, "Explore repository to understand the app and gather README, root contents, and releases for the landing page."
|
||||
|
||||
def _parse_commandcode_text_tool_calls(text):
|
||||
@@ -3322,7 +3378,10 @@ def cc_stream_to_sse(cc_stream, model, req_id):
|
||||
_url_in_text = re.search(r"https?://[^\s\]'\\>\",]+", text_buf)
|
||||
if _url_in_text:
|
||||
_synth_url = _url_in_text.group(0).rstrip(")].,;'\\\"")
|
||||
_synth_cmd = f"curl -sL --max-time 15 '{_synth_url}' 2>/dev/null | head -200"
|
||||
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_just = "Auto-synthesized: URL detected in text, fetching"
|
||||
|
||||
# Heuristic 2: File path references → list or read
|
||||
@@ -3330,7 +3389,10 @@ 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)
|
||||
if _file_m:
|
||||
_fpath = _file_m.group(1)
|
||||
_synth_cmd = f"cat '{_fpath}' 2>/dev/null | head -200 || ls -la '{_fpath}'"
|
||||
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_just = f"Auto-synthesized: file reference detected ({_fpath})"
|
||||
|
||||
# Heuristic 3: Shell command mentioned in backticks or quotes
|
||||
@@ -3358,7 +3420,10 @@ def cc_stream_to_sse(cc_stream, model, req_id):
|
||||
if _intent_m:
|
||||
_intent_text = _intent_m.group(1).strip()
|
||||
if len(_intent_text) > 10 and len(_intent_text) < 200:
|
||||
_synth_cmd = f"echo 'Stuck recovery: model intent was: {_intent_text[:100]}'"
|
||||
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_just = f"Auto-synthesized from intent text: {_intent_text[:80]}"
|
||||
|
||||
if _synth_cmd:
|
||||
@@ -3891,11 +3956,13 @@ def _extract_text(content):
|
||||
# HTTP Server
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
_MAX_REQLOG_LINES = 2000
|
||||
|
||||
def _log_resp(resp_id, status, output):
|
||||
try:
|
||||
import datetime as _dt
|
||||
_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")
|
||||
if output:
|
||||
for o in output:
|
||||
@@ -3908,6 +3975,11 @@ def _log_resp(resp_id, status, output):
|
||||
_f.write(f" -> {ot}\n")
|
||||
_f.write(f"{'='*60}\n")
|
||||
_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:
|
||||
pass
|
||||
|
||||
@@ -4064,10 +4136,26 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
||||
info["total"] = 0
|
||||
self.send_json(200, info)
|
||||
elif self.path in ("/health", "/v1/health"):
|
||||
import resource as _res
|
||||
_mem_mb = 0
|
||||
try:
|
||||
_mem_mb = _res.getrusage(_res.RUSAGE_SELF).ru_maxrss / 1024
|
||||
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
|
||||
except Exception:
|
||||
pass
|
||||
_uptime = time.time() - _START_TIME if '_START_TIME' in dir() else 0
|
||||
@@ -4122,12 +4210,12 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
||||
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)
|
||||
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" prev_id={prev_id}\n")
|
||||
_lf.write(f" raw_input_types={raw_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")
|
||||
if isinstance(input_data, list):
|
||||
for i, item in enumerate(input_data):
|
||||
@@ -4143,6 +4231,9 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
||||
_lf.flush()
|
||||
|
||||
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)
|
||||
_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", "")
|
||||
@@ -4204,13 +4295,15 @@ 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)
|
||||
body["input"] = input_data
|
||||
|
||||
messages = oa_input_to_messages(input_data)
|
||||
messages = _inject_stored_reasoning(messages)
|
||||
instructions = body.get("instructions", "").strip()
|
||||
if instructions:
|
||||
messages.insert(0, {"role": "system", "content": instructions})
|
||||
@@ -4612,7 +4705,7 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
||||
if n_contents > 10:
|
||||
debug_path = os.path.join(_LOG_DIR, f"gemini-long-ctx-{self._session_id}.json")
|
||||
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)
|
||||
except Exception:
|
||||
pass
|
||||
@@ -4628,7 +4721,7 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
||||
if e.code == 400 and OAUTH_PROVIDER.startswith("google"):
|
||||
try:
|
||||
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)
|
||||
print(f"[{self._session_id}] saved 400 debug request to {debug_path}", file=sys.stderr)
|
||||
except Exception:
|
||||
@@ -4940,7 +5033,8 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
||||
pass
|
||||
|
||||
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():
|
||||
print("[translate-proxy] stream cancelled", file=sys.stderr)
|
||||
break
|
||||
@@ -4958,6 +5052,16 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
||||
_log_resp(last_resp_id, last_status, last_output)
|
||||
if last_resp_id and input_data is not None:
|
||||
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)
|
||||
|
||||
# Auto-learn provider quirks before flushing the bad response to Codex.
|
||||
@@ -4986,7 +5090,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):
|
||||
@@ -5324,7 +5428,7 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
||||
metadata = {
|
||||
"run_id": run_id,
|
||||
"cost_mode": "free",
|
||||
"client_id": secrets.token_hex(7)[:13],
|
||||
"client_id": "".join(secrets.choice(string.digits + string.ascii_lowercase) for _ in range(13)),
|
||||
}
|
||||
if instance_id:
|
||||
metadata["freebuff_instance_id"] = instance_id
|
||||
@@ -5925,8 +6029,23 @@ def main():
|
||||
global SERVER, _START_TIME
|
||||
_START_TIME = time.time()
|
||||
_init_runtime()
|
||||
signal.signal(signal.SIGTERM, _handle_shutdown_signal)
|
||||
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"):
|
||||
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:
|
||||
from http.server import ThreadingHTTPServer as _BaseSrv
|
||||
except ImportError:
|
||||
@@ -6133,7 +6252,7 @@ Postamble text."""
|
||||
_check("FIX23 explore nested JSON: parsed", len(_calls_m) == 1, f"got {len(_calls_m)} calls")
|
||||
if _calls_m:
|
||||
_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")
|
||||
|
||||
# Pattern N: require_escalation block (FIX 24)
|
||||
@@ -6143,7 +6262,7 @@ Postamble text."""
|
||||
if _calls_n:
|
||||
_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: 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)
|
||||
_esc_bare = 'I want to proceed.\n<request_escalation_permission />\nPlease let me continue.'
|
||||
@@ -6155,13 +6274,13 @@ Postamble text."""
|
||||
# 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")
|
||||
_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}")
|
||||
|
||||
# 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"}]')
|
||||
_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",
|
||||
file=sys.stderr)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user