20 Commits

11 changed files with 6369 additions and 269 deletions

View File

@@ -1,5 +1,60 @@
# Changelog # 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**
### Windows Native GUI (tkinter)
- **Windows GUI** in `windows/` folder — full tkinter port by cobra91
- OAuth Secrets editor, Import JSON, Antigravity model list
- Shared backend with Linux (same translate-proxy.py)
- See README for Windows installation and usage
**Context Compaction for Antigravity/Gemini OAuth**
### Fix
- **Prevent `input token count exceeds maximum` errors** during long conversations
- Added aggressive compaction policies for Antigravity (`cloudcode-pa`) and Gemini CLI (`googleapis`)
- Auto-trims old turns when approaching 60% of model context limit (1M tokens for Gemini, 200K for Claude, 128K for GPT-OSS)
- Added REST model IDs to context size map (`gemini-3-flash`, `gemini-3.1-pro-low`, `claude-sonnet-4-6`, etc.)
## v3.10.4 (2026-05-25) ## v3.10.4 (2026-05-25)
**Security: OAuth Secrets Editor + Import JSON** **Security: OAuth Secrets Editor + Import JSON**

View File

@@ -9,13 +9,28 @@
<a href="https://z.ai/subscribe?ic=ROK78RJKNW">z.ai/subscribe</a> <a href="https://z.ai/subscribe?ic=ROK78RJKNW">z.ai/subscribe</a>
</p> </p>
<p align="center">
--- ---
If you want fork it, use the Github copy, here it is:
<a href="https://github.com/roman-ryzenadvanced/Codex-Launcher-Any-AI-Provider">Codex-Any-AI-Provider on Github (Official)</a>
---
</p>
<h1 align="center">Codex Launcher — Any AI Provider</h1> <h1 align="center">Codex Launcher — Any AI Provider</h1>
<p align="center"> <p align="center">
<strong>Run OpenAI Codex CLI &amp; Desktop with <em>any</em> AI provider.</strong><br/> <strong>Run OpenAI Codex CLI &amp; Desktop with <em>any</em> AI provider.</strong><br/>
Google Antigravity &bull; Gemini CLI &bull; OpenCode &bull; Z.AI &bull; Anthropic &bull; Command Code &bull; Codebuff &bull; OpenRouter &bull; Crof.ai &bull; NVIDIA NIM &bull; OpenAdapter &bull; Kilo.ai &bull; DeepSeek &bull; and more Google Antigravity &bull; Gemini CLI &bull; OpenCode &bull; Z.AI &bull; Anthropic &bull; Command Code &bull; Freebuff &bull; OpenRouter &bull; Crof.ai &bull; NVIDIA NIM &bull; OpenAdapter &bull; Kilo.ai &bull; DeepSeek &bull; and more
</p>
<p align="center">
<sub>
Windows version by <a href="https://github.com/cobra91">cobra91</a> &bull;
Original Linux development by <a href="https://github.com/roman-ryzenadvanced">roman-ryzenadvanced</a>
</sub>
</p> </p>
<p align="center"> <p align="center">
@@ -546,6 +561,16 @@ Codex Launcher includes special handling for Gemini 3 / Antigravity OAuth:
- **User instruction enforcement**: The latest user message is guaranteed to be the - **User instruction enforcement**: The latest user message is guaranteed to be the
final content turn sent to Gemini, even after compaction. final content turn sent to Gemini, even after compaction.
- **Smart compaction**: Old tool outputs capped at 3000 chars, recent 6 at 20000 chars. - **Smart compaction**: Old tool outputs capped at 3000 chars, recent 6 at 20000 chars.
- **Context compaction**: Aggressive auto-trimming when approaching 60% of model context
limit (1M tokens Gemini, 200K Claude, 128K GPT-OSS). Prevents token limit errors.
- **Model ID mapping**: Display names (e.g. `Gemini 3.5 Flash (High)`) mapped to REST API
slugs (e.g. `gemini-3-flash`). See `docs/ANTIGRAVITY.md` for details.
### OAuth Secrets
Google OAuth credentials are stored locally in `~/.config/codex-launcher/oauth-secrets.json`
and never committed to the repository. Use the **OAuth Secrets** button in the launcher
header to edit or import `client_secret_*.json` files from Google Cloud Console.
--- ---
@@ -605,7 +630,7 @@ curl http://127.0.0.1:PORT/v1/accounts
| OpenCode Zen | OpenAI-compat | `https://opencode.ai/zen/v1` | | OpenCode Zen | OpenAI-compat | `https://opencode.ai/zen/v1` |
| OpenCode Go | OpenAI-compat | `https://opencode.ai/zen/go/v1` | | OpenCode Go | OpenAI-compat | `https://opencode.ai/zen/go/v1` |
| Command Code | Command Code | `https://api.commandcode.ai` | | 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` | | Crof.ai | OpenAI-compat | `https://crof.ai/v1` |
| OpenAdapter | OpenAI-compat | `https://api.openadapter.in/v1` | | OpenAdapter | OpenAI-compat | `https://api.openadapter.in/v1` |
| Z.ai Coding | OpenAI-compat | `https://api.z.ai/api/coding/paas/v4` | | Z.ai Coding | OpenAI-compat | `https://api.z.ai/api/coding/paas/v4` |
@@ -618,14 +643,14 @@ curl http://127.0.0.1:PORT/v1/accounts
| Google Antigravity (OAuth) | Antigravity OAuth | `daily-cloudcode-pa.sandbox.googleapis.com` | | Google Antigravity (OAuth) | Antigravity OAuth | `daily-cloudcode-pa.sandbox.googleapis.com` |
| Custom | Any | User-defined | | Custom | Any | User-defined |
### Free Models (via Codebuff) ### Free Models (via Codebuff/Freebuff)
Codebuff provides free access to these models — no API key needed: Codebuff/Freebuff provides free access to these models — no API key needed:
- **DeepSeek V4 Pro** — Smartest model - **DeepSeek V4 Pro** — Smartest model
- **DeepSeek V4 Flash** — Most efficient - **DeepSeek V4 Flash** — Most efficient
- **Kimi K2.6** — Balanced - **Kimi K2.6** — Balanced
- **MiniMax M2.7** — Fastest - **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)*
--- ---
@@ -762,15 +787,70 @@ codex --profile my-profile -c model=my-model
--- ---
## Windows 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>
Windows version by <a href="https://github.com/cobra91">cobra91</a> &bull;
Original Linux development by <a href="https://github.com/roman-ryzenadvanced">roman-ryzenadvanced</a>
</sub>
</p>
### Files
| File | Purpose |
|---|---|
| `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)
Python ≥ 3.8 with tkinter is required (comes with the official Python installer).
```powershell
# From repo root
cd src
python codex-launcher-gui.py
```
The GUI will:
1. Auto-create default endpoints on first run
2. Show a toolbar with Endpoints, OAuth Secrets, AI Monitor, and more
3. Launch Codex CLI/Desktop with your chosen provider
### OAuth Credentials
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.
The **OAuth Secrets** dialog shows all providers (Google + Freebuff/Codebuff) with **Re-OAuth buttons** to instantly re-authenticate any provider.
### 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
---
## Requirements ## Requirements
- Python ≥ 3.8 - Python ≥ 3.8
- python3-gi (`sudo apt install python3-gi`) - python3-gi (`sudo apt install python3-gi`) — Linux only
- tkinter (`python3-tk`) — Windows / Linux GUI
- Codex CLI ≥ 2.0 - Codex CLI ≥ 2.0
- Codex Desktop (optional, for Desktop mode) - Codex Desktop (optional, for Desktop mode)
- bash, curl, lsof - bash, curl, lsof — Linux only
**No pip dependencies.** Zero. Pure stdlib + system GTK. **No pip dependencies.** Zero. Pure stdlib.
--- ---

Binary file not shown.

Binary file not shown.

View File

@@ -3,11 +3,11 @@ set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
if [ -f "$SCRIPT_DIR/codex-launcher_3.10.5_all.deb" ]; then if [ -f "$SCRIPT_DIR/codex-launcher_3.10.6_all.deb" ]; then
echo "Installing codex-launcher_3.10.5_all.deb ..." echo "Installing codex-launcher_3.10.6_all.deb ..."
sudo dpkg -i "$SCRIPT_DIR/codex-launcher_3.10.5_all.deb" sudo dpkg -i "$SCRIPT_DIR/codex-launcher_3.10.6_all.deb"
echo "" 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 " translate-proxy.py -> /usr/bin/translate-proxy.py"
echo " codex-launcher-gui -> /usr/bin/codex-launcher-gui" echo " codex-launcher-gui -> /usr/bin/codex-launcher-gui"
echo " cleanup-codex-stale -> /usr/bin/cleanup-codex-stale.sh" echo " cleanup-codex-stale -> /usr/bin/cleanup-codex-stale.sh"

101
src/cleanup-codex-stale.py Normal file
View 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()

View File

@@ -393,7 +393,25 @@ PROVIDER_PRESETS = {
}, },
"Codebuff (Free DeepSeek/Kimi)": { "Codebuff (Free DeepSeek/Kimi)": {
"backend_type": "codebuff", "backend_type": "codebuff",
"base_url": "https://codebuff.com", "base_url": "https://www.codebuff.com",
"oauth_provider": "codebuff",
"models": [
"deepseek/deepseek-v4-pro", "deepseek/deepseek-v4-flash",
"moonshotai/kimi-k2.6", "minimax/minimax-m2.7",
],
},
"Freebuff (Free DeepSeek/Kimi)": {
"backend_type": "codebuff",
"base_url": "https://www.codebuff.com",
"oauth_provider": "codebuff",
"models": [
"deepseek/deepseek-v4-pro", "deepseek/deepseek-v4-flash",
"moonshotai/kimi-k2.6", "minimax/minimax-m2.7",
],
},
"FreeBuff": {
"backend_type": "codebuff",
"base_url": "https://www.codebuff.com",
"oauth_provider": "codebuff", "oauth_provider": "codebuff",
"models": [ "models": [
"deepseek/deepseek-v4-pro", "deepseek/deepseek-v4-flash", "deepseek/deepseek-v4-pro", "deepseek/deepseek-v4-flash",
@@ -1780,7 +1798,7 @@ class LauncherWin(Gtk.Window):
# header row # header row
hdr = Gtk.Box(spacing=8) hdr = Gtk.Box(spacing=8)
vbox.pack_start(hdr, False, False, 0) 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) lbl.set_use_markup(True)
hdr.pack_start(lbl, False, False, 0) hdr.pack_start(lbl, False, False, 0)
changelog_btn = Gtk.Button(label="Changelog") changelog_btn = Gtk.Button(label="Changelog")
@@ -1959,6 +1977,13 @@ class LauncherWin(Gtk.Window):
assist_btn.connect("clicked", lambda b: self._open_assistant()) assist_btn.connect("clicked", lambda b: self._open_assistant())
assist_btn.set_tooltip_text("Open AI coding assistant with streaming, tools, and session management") assist_btn.set_tooltip_text("Open AI coding assistant with streaming, tools, and session management")
bb.pack_start(assist_btn, False, False, 0) 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 = Gtk.Button(label="Kill && Cleanup")
self._kill_btn.connect("clicked", lambda b: self._kill()) self._kill_btn.connect("clicked", lambda b: self._kill())
self._kill_btn.set_sensitive(False) self._kill_btn.set_sensitive(False)
@@ -2055,6 +2080,7 @@ class LauncherWin(Gtk.Window):
self._btn_codex_desktop.set_sensitive(not busy and has_desk) self._btn_codex_desktop.set_sensitive(not busy and has_desk)
self._btn_codex_cli.set_sensitive(not busy and has_cli) self._btn_codex_cli.set_sensitive(not busy and has_cli)
self._kill_btn.set_sensitive(busy) self._kill_btn.set_sensitive(busy)
self._restart_btn.set_sensitive(busy)
GLib.idle_add(_update) GLib.idle_add(_update)
def _rebuild_combo(self): def _rebuild_combo(self):
@@ -2188,16 +2214,32 @@ class LauncherWin(Gtk.Window):
GLib.idle_add(self.log, f"[AI Monitor] Alert: {action} (trigger: {trigger})") GLib.idle_add(self.log, f"[AI Monitor] Alert: {action} (trigger: {trigger})")
def _restart_proxy_from_watcher(self): def _restart_proxy_from_watcher(self):
try: try:
ep_name = load_endpoints().get("default") ep_name = load_endpoints().get("default")
if not ep_name: if not ep_name:
return return
for ep in load_endpoints().get("endpoints", []): for ep in load_endpoints().get("endpoints", []):
if ep.get("name") == ep_name: if ep.get("name") == ep_name:
self._start_proxy(ep) self._start_proxy(ep)
break break
except Exception as e: except Exception as e:
self.log(f"[AI Monitor] Proxy restart failed: {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): def _open_usage(self):
try: try:
@@ -2790,6 +2832,154 @@ class LauncherWin(Gtk.Window):
_stop_proxy() _stop_proxy()
Gtk.main_quit() 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): def _edit_oauth_secrets(self):
secrets_path = os.path.expanduser("~/.config/codex-launcher/oauth-secrets.json") secrets_path = os.path.expanduser("~/.config/codex-launcher/oauth-secrets.json")
try: try:
@@ -2799,10 +2989,10 @@ class LauncherWin(Gtk.Window):
data = {"antigravity": {"client_id": "", "client_secret": ""}, data = {"antigravity": {"client_id": "", "client_secret": ""},
"gemini_cli": {"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("Cancel", Gtk.ResponseType.CANCEL)
dlg.add_button("Save", Gtk.ResponseType.OK) 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 = dlg.get_content_area()
area.set_margin_start(16) area.set_margin_start(16)
area.set_margin_end(16) area.set_margin_end(16)
@@ -2810,17 +3000,43 @@ class LauncherWin(Gtk.Window):
area.set_margin_bottom(12) area.set_margin_bottom(12)
area.set_spacing(6) 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 = {} 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) section_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=2)
hdr_row = Gtk.Box(spacing=6) 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) 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 = Gtk.Button(label="Import JSON")
import_btn.set_size_request(100, -1) import_btn.set_size_request(100, -1)
hdr_row.pack_end(import_btn, False, False, 0) hdr_row.pack_end(import_btn, False, False, 0)
section_box.pack_start(hdr_row, False, False, 2) 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, {}) sec = data.get(section_key, {})
for fk, fl in [("client_id", "Client ID"), ("client_secret", "Client Secret")]: for fk, fl in [("client_id", "Client ID"), ("client_secret", "Client Secret")]:
row = Gtk.Box(spacing=6) row = Gtk.Box(spacing=6)
@@ -2828,7 +3044,7 @@ class LauncherWin(Gtk.Window):
lbl.set_size_request(100, -1) lbl.set_size_request(100, -1)
entry = Gtk.Entry() entry = Gtk.Entry()
entry.set_text(sec.get(fk, "")) entry.set_text(sec.get(fk, ""))
entry.set_size_request(380, -1) entry.set_size_request(360, -1)
if fk == "client_secret": if fk == "client_secret":
entry.set_visibility(False) entry.set_visibility(False)
entry.set_invisible_char("*") entry.set_invisible_char("*")
@@ -2837,10 +3053,63 @@ class LauncherWin(Gtk.Window):
section_box.pack_start(row, False, False, 2) section_box.pack_start(row, False, False, 2)
fields[(section_key, fk)] = entry fields[(section_key, fk)] = entry
import_btn.connect("clicked", lambda b, sk=section_key: self._import_oauth_json(fields, sk)) 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) 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)
area.show_all()
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: if dlg.run() == Gtk.ResponseType.OK:
for (sk, fk), entry in fields.items(): for (sk, fk), entry in fields.items():
@@ -2854,6 +3123,20 @@ class LauncherWin(Gtk.Window):
os.chmod(secrets_path, 0o600) os.chmod(secrets_path, 0o600)
except Exception as e: except Exception as e:
self._show_error_dialog("Save failed", str(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() dlg.destroy()
def _import_oauth_json(self, fields, section_key): def _import_oauth_json(self, fields, section_key):
@@ -3685,10 +3968,10 @@ class EditEndpointDialog(Gtk.Dialog):
def _codebuff_auth_thread(): def _codebuff_auth_thread():
try: try:
fingerprint_id = str(uuid.uuid4()) fingerprint_id = str(uuid.uuid4())
auth_url = "https://codebuff.com/api/auth/cli/code" auth_url = "https://www.codebuff.com/api/auth/cli/code"
body = json.dumps({"fingerprintId": fingerprint_id}).encode() body = json.dumps({"fingerprintId": fingerprint_id}).encode()
req = urllib.request.Request(auth_url, data=body, 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) resp = urllib.request.urlopen(req, timeout=30)
data = json.loads(resp.read()) data = json.loads(resp.read())
login_url = data.get("loginUrl", "") or data.get("login_url", "") login_url = data.get("loginUrl", "") or data.get("login_url", "")
@@ -3707,13 +3990,13 @@ class EditEndpointDialog(Gtk.Dialog):
webbrowser.open(login_url) webbrowser.open(login_url)
poll_url = f"https://codebuff.com/api/auth/cli/status?fingerprintId={urllib.parse.quote(fingerprint_id)}&fingerprintHash={urllib.parse.quote(fingerprint_hash)}&expiresAt={expires_at}" poll_url = f"https://www.codebuff.com/api/auth/cli/status?fingerprintId={urllib.parse.quote(fingerprint_id)}&fingerprintHash={urllib.parse.quote(fingerprint_hash)}&expiresAt={expires_at}"
deadline = time.time() + 300 deadline = time.time() + 300
while time.time() < deadline: while time.time() < deadline:
time.sleep(2) time.sleep(2)
try: try:
poll_req = urllib.request.Request(poll_url, 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_resp = urllib.request.urlopen(poll_req, timeout=10)
poll_data = json.loads(poll_resp.read()) poll_data = json.loads(poll_resp.read())
user = poll_data.get("user") user = poll_data.get("user")

3123
src/codex-launcher-gui.py Normal file

File diff suppressed because it is too large Load Diff

2107
src/codex_launcher_lib.py Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -157,9 +157,13 @@ Architecture:
import json, http.server, socketserver, urllib.request, urllib.parse, urllib.error, re import json, http.server, socketserver, urllib.request, urllib.parse, urllib.error, re
import time, uuid, os, sys, argparse, threading, socket, collections, contextlib, signal import time, uuid, os, sys, argparse, threading, socket, collections, contextlib, signal
import secrets, string
import dataclasses import dataclasses
import http.client import http.client
import selectors import selectors
import tempfile
_IS_WINDOWS = sys.platform == "win32"
# ═══════════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════════
# Config # Config
@@ -241,13 +245,23 @@ MODELS = []
CC_VERSION = "" CC_VERSION = ""
REASONING_ENABLED = True REASONING_ENABLED = True
REASONING_EFFORT = "medium" REASONING_EFFORT = "medium"
FORCE_MODEL = ""
BGP_ROUTES = [] BGP_ROUTES = []
SERVER = None SERVER = None
_LOG_DIR = os.path.join(os.path.expanduser("~"), ".cache", "codex-proxy") if _IS_WINDOWS:
_LOG_DIR = os.path.join(os.environ.get("LOCALAPPDATA", os.path.expanduser("~")), "codex-proxy")
else:
_LOG_DIR = os.path.join(os.path.expanduser("~"), ".cache", "codex-proxy")
os.makedirs(_LOG_DIR, exist_ok=True) os.makedirs(_LOG_DIR, exist_ok=True)
_REQUESTS_DIR = os.path.join(_LOG_DIR, "requests") _REQUESTS_DIR = os.path.join(_LOG_DIR, "requests")
os.makedirs(_REQUESTS_DIR, exist_ok=True) os.makedirs(_REQUESTS_DIR, exist_ok=True)
try:
for _f in os.listdir(_REQUESTS_DIR):
if _f.endswith(".tmp"):
os.remove(os.path.join(_REQUESTS_DIR, _f))
except Exception:
pass
_stats_path = os.path.join(_LOG_DIR, "usage-stats.json") _stats_path = os.path.join(_LOG_DIR, "usage-stats.json")
_provider_caps_path = os.path.join(_LOG_DIR, "provider-caps.json") _provider_caps_path = os.path.join(_LOG_DIR, "provider-caps.json")
_stats_lock = threading.Lock() _stats_lock = threading.Lock()
@@ -257,7 +271,7 @@ _STATS_FLUSH_INTERVAL = 5.0
_STATS = {} _STATS = {}
try: try:
_LOG_FILE = open(os.path.join(_LOG_DIR, "proxy.log"), "a") _LOG_FILE = open(os.path.join(_LOG_DIR, "proxy.log"), "a", encoding="utf-8")
except Exception: except Exception:
_LOG_FILE = None _LOG_FILE = None
@@ -273,6 +287,9 @@ _deepseek_reasoning_store = {}
_deepseek_reasoning_lock = threading.Lock() _deepseek_reasoning_lock = threading.Lock()
_MAX_DS_STORED = 100 _MAX_DS_STORED = 100
_last_reasoning_store = {}
_last_reasoning_lock = threading.Lock()
_crof_lock = threading.Lock() _crof_lock = threading.Lock()
_provider_caps_lock = threading.Lock() _provider_caps_lock = threading.Lock()
_provider_caps = None _provider_caps = None
@@ -294,7 +311,7 @@ _conn_pool = {}
_STREAM_IDLE_TIMEOUT = 300 _STREAM_IDLE_TIMEOUT = 300
_CODEBUFF_AUTH_URL = "https://codebuff.com" _CODEBUFF_AUTH_URL = "https://www.codebuff.com"
_CODEBUFF_API_URL = "https://www.codebuff.com" _CODEBUFF_API_URL = "https://www.codebuff.com"
_CODEBUFF_AGENT_MAP = { _CODEBUFF_AGENT_MAP = {
"deepseek/deepseek-v4-pro": "base2-free-deepseek", "deepseek/deepseek-v4-pro": "base2-free-deepseek",
@@ -302,7 +319,10 @@ _CODEBUFF_AGENT_MAP = {
"moonshotai/kimi-k2.6": "base2-free-kimi", "moonshotai/kimi-k2.6": "base2-free-kimi",
"minimax/minimax-m2.7": "base2-free", "minimax/minimax-m2.7": "base2-free",
} }
_CODEBUFF_CREDS_PATH = os.path.join(os.path.expanduser("~"), ".config", "manicode", "credentials.json") if _IS_WINDOWS:
_CODEBUFF_CREDS_PATH = os.path.join(os.environ.get("APPDATA", os.path.expanduser("~")), "manicode", "credentials.json")
else:
_CODEBUFF_CREDS_PATH = os.path.join(os.path.expanduser("~"), ".config", "manicode", "credentials.json")
_codebuff_token_cache = {"token": None, "checked": 0} _codebuff_token_cache = {"token": None, "checked": 0}
_codebuff_session_cache = {"instance_id": None, "expires": 0, "model": None} _codebuff_session_cache = {"instance_id": None, "expires": 0, "model": None}
_codebuff_token_lock = threading.Lock() _codebuff_token_lock = threading.Lock()
@@ -331,11 +351,11 @@ def _codebuff_get_session(token, model):
return sc["instance_id"] return sc["instance_id"]
try: try:
url = f"{_CODEBUFF_API_URL}/api/v1/freebuff/session" url = f"{_CODEBUFF_API_URL}/api/v1/freebuff/session"
body = json.dumps({"model": model}).encode() body = json.dumps({}).encode()
req = urllib.request.Request(url, data=body, headers={ req = urllib.request.Request(url, data=body, headers={
"Content-Type": "application/json", "Content-Type": "application/json",
"Authorization": f"Bearer {token}", "Authorization": f"Bearer {token}",
"User-Agent": "codex-launcher/3.10.5", "User-Agent": "ai-sdk/openai-compatible/1.0.25/codebuff",
"x-codebuff-model": model, "x-codebuff-model": model,
}) })
try: try:
@@ -383,7 +403,7 @@ def _codebuff_start_run(token, agent_id):
req = urllib.request.Request(url, data=body, headers={ req = urllib.request.Request(url, data=body, headers={
"Content-Type": "application/json", "Content-Type": "application/json",
"Authorization": f"Bearer {token}", "Authorization": f"Bearer {token}",
"User-Agent": "codex-launcher/3.10.5", "User-Agent": "ai-sdk/openai-compatible/1.0.25/codebuff",
}) })
try: try:
resp = urllib.request.urlopen(req, timeout=15) resp = urllib.request.urlopen(req, timeout=15)
@@ -416,7 +436,7 @@ def _codebuff_finish_run(token, run_id, status="completed"):
req = urllib.request.Request(url, data=body, headers={ req = urllib.request.Request(url, data=body, headers={
"Content-Type": "application/json", "Content-Type": "application/json",
"Authorization": f"Bearer {token}", "Authorization": f"Bearer {token}",
"User-Agent": "codex-launcher/3.10.5", "User-Agent": "ai-sdk/openai-compatible/1.0.25/codebuff",
}) })
try: try:
urllib.request.urlopen(req, timeout=10) urllib.request.urlopen(req, timeout=10)
@@ -634,7 +654,7 @@ def _refresh_google_token(token_data, token_path):
new_tokens = json.loads(resp.read()) new_tokens = json.loads(resp.read())
token_data["access_token"] = new_tokens.get("access_token", token_data.get("access_token")) token_data["access_token"] = new_tokens.get("access_token", token_data.get("access_token"))
token_data["expires_at"] = time.time() + new_tokens.get("expires_in", 3600) token_data["expires_at"] = time.time() + new_tokens.get("expires_in", 3600)
with open(token_path, "w") as f: with open(token_path, "w", encoding="utf-8") as f:
json.dump(token_data, f, indent=2) json.dump(token_data, f, indent=2)
print("[oauth] token refreshed OK", file=sys.stderr) print("[oauth] token refreshed OK", file=sys.stderr)
return token_data["access_token"] return token_data["access_token"]
@@ -699,7 +719,6 @@ _GEMINI_AGENT_GUARDRAIL = (
"Always emit the actual tool call in the same response." "Always emit the actual tool call in the same response."
) )
_LOG_FILE = None
_LOG_FILE_LOCK = threading.Lock() _LOG_FILE_LOCK = threading.Lock()
def _fetch_antigravity_version(): def _fetch_antigravity_version():
@@ -727,7 +746,7 @@ def _fetch_antigravity_version():
version = m.group(0) version = m.group(0)
try: try:
os.makedirs(os.path.dirname(cache_path), exist_ok=True) os.makedirs(os.path.dirname(cache_path), exist_ok=True)
with open(cache_path, "w") as f: with open(cache_path, "w", encoding="utf-8") as f:
json.dump({"version": version, "checked_at": time.time()}, f) json.dump({"version": version, "checked_at": time.time()}, f)
except Exception: except Exception:
pass pass
@@ -762,6 +781,7 @@ def _init_runtime():
CC_VERSION = CONFIG.get("cc_version", "") CC_VERSION = CONFIG.get("cc_version", "")
REASONING_ENABLED = CONFIG.get("reasoning_enabled", True) REASONING_ENABLED = CONFIG.get("reasoning_enabled", True)
REASONING_EFFORT = CONFIG.get("reasoning_effort", "medium") REASONING_EFFORT = CONFIG.get("reasoning_effort", "medium")
FORCE_MODEL = (CONFIG.get("force_model") or "").strip()
BGP_ROUTES = CONFIG.get("bgp_routes", []) BGP_ROUTES = CONFIG.get("bgp_routes", [])
_api_key_pool = None _api_key_pool = None
if API_KEY and "," in API_KEY and not OAUTH_PROVIDER.startswith("google") and BACKEND not in ("codebuff", "freebuff"): if API_KEY and "," in API_KEY and not OAUTH_PROVIDER.startswith("google") and BACKEND not in ("codebuff", "freebuff"):
@@ -903,7 +923,7 @@ def _load_provider_caps():
def _save_provider_caps(): def _save_provider_caps():
try: try:
os.makedirs(os.path.dirname(_provider_caps_path), exist_ok=True) os.makedirs(os.path.dirname(_provider_caps_path), exist_ok=True)
with open(_provider_caps_path, "w") as f: with open(_provider_caps_path, "w", encoding="utf-8") as f:
json.dump(_provider_caps or {}, f, indent=2) json.dump(_provider_caps or {}, f, indent=2)
except Exception as e: except Exception as e:
print(f"[provider-sensor] failed to save caps: {e}", file=sys.stderr) print(f"[provider-sensor] failed to save caps: {e}", file=sys.stderr)
@@ -959,7 +979,7 @@ def _refresh_oauth_token_for(api_key, oauth_provider):
new_tokens = json.loads(resp.read()) new_tokens = json.loads(resp.read())
tokens["access_token"] = new_tokens.get("access_token", tokens.get("access_token")) tokens["access_token"] = new_tokens.get("access_token", tokens.get("access_token"))
tokens["expires_at"] = time.time() + new_tokens.get("expires_in", 3600) tokens["expires_at"] = time.time() + new_tokens.get("expires_in", 3600)
with open(token_path, "w") as f: with open(token_path, "w", encoding="utf-8") as f:
json.dump(tokens, f, indent=2) json.dump(tokens, f, indent=2)
print("[oauth] token refreshed OK", file=sys.stderr) print("[oauth] token refreshed OK", file=sys.stderr)
return tokens["access_token"] return tokens["access_token"]
@@ -983,7 +1003,7 @@ def _load_stats():
def _atomic_write_json(path, obj): def _atomic_write_json(path, obj):
tmp = path + ".tmp" tmp = path + ".tmp"
with open(tmp, "w") as f: with open(tmp, "w", encoding="utf-8") as f:
json.dump(obj, f, indent=2, ensure_ascii=False) json.dump(obj, f, indent=2, ensure_ascii=False)
os.replace(tmp, path) os.replace(tmp, path)
@@ -1277,8 +1297,8 @@ _COMPACT_KEEP_RECENT = 10
_CROF_ADAPTIVE = { _CROF_ADAPTIVE = {
"fail_history": [], "fail_history": [],
"model_limits": {}, "model_limits": {},
"global_item_limit": 30, "global_item_limit": 80,
"min_keep_recent": 4, "min_keep_recent": 6,
} }
_BGP_STATS_PATH = os.path.join(_LOG_DIR, "bgp-route-stats.json") _BGP_STATS_PATH = os.path.join(_LOG_DIR, "bgp-route-stats.json")
@@ -1297,7 +1317,7 @@ def _load_bgp_stats():
def _save_bgp_stats(stats): def _save_bgp_stats(stats):
tmp = _BGP_STATS_PATH + ".tmp" tmp = _BGP_STATS_PATH + ".tmp"
with open(tmp, "w") as f: with open(tmp, "w", encoding="utf-8") as f:
json.dump(stats, f, indent=2) json.dump(stats, f, indent=2)
os.replace(tmp, _BGP_STATS_PATH) os.replace(tmp, _BGP_STATS_PATH)
@@ -1346,6 +1366,8 @@ def _sorted_bgp_routes():
return sorted(BGP_ROUTES, key=lambda r: _score_route(r, stats)) return sorted(BGP_ROUTES, key=lambda r: _score_route(r, stats))
def _crof_record(model, n_items, success): def _crof_record(model, n_items, success):
if TARGET_URL and "crof.ai" not in TARGET_URL:
return
if not isinstance(n_items, int) or n_items < 1: if not isinstance(n_items, int) or n_items < 1:
return return
entry = {"model": model, "items": n_items, "ok": success} entry = {"model": model, "items": n_items, "ok": success}
@@ -1371,7 +1393,8 @@ def _crof_record(model, n_items, success):
global_limit = v["limit"] global_limit = v["limit"]
_CROF_ADAPTIVE["global_item_limit"] = global_limit _CROF_ADAPTIVE["global_item_limit"] = global_limit
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): def _crof_item_limit(model):
ml = _CROF_ADAPTIVE["model_limits"].get(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_lines.append(_item_summary(item, max_len=120))
summary_msg = {"type": "message", "role": "user", "content": [{"type": "input_text", "text": "\n".join(summary_lines)}]} summary_msg = {"type": "message", "role": "user", "content": [{"type": "input_text", "text": "\n".join(summary_lines)}]}
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 return head + [summary_msg] + tail
def _item_summary(item, max_len=200): def _item_summary(item, max_len=200):
@@ -1627,7 +1651,7 @@ def _estimate_tokens(obj):
def _adaptive_compact(input_data, model, policy=None): def _adaptive_compact(input_data, model, policy=None):
policy = policy or {} policy = policy or {}
context_size = int(policy.get("context_size", _context_limit_for_model(model))) context_size = int(policy.get("context_size", _context_limit_for_model(model)))
input_budget = int(context_size * 0.60) input_budget = int(context_size * 0.80)
estimated = _estimate_tokens(input_data) estimated = _estimate_tokens(input_data)
if estimated <= input_budget: if estimated <= input_budget:
return input_data, False return input_data, False
@@ -1790,7 +1814,7 @@ def save_request_snapshot(request_id, body):
} }
path = os.path.join(_REQUESTS_DIR, f"{request_id}.json") path = os.path.join(_REQUESTS_DIR, f"{request_id}.json")
tmp = path + ".tmp" tmp = path + ".tmp"
with open(tmp, "w") as f: with open(tmp, "w", encoding="utf-8") as f:
json.dump(snapshot, f, ensure_ascii=False, indent=2) json.dump(snapshot, f, ensure_ascii=False, indent=2)
os.replace(tmp, path) os.replace(tmp, path)
_rotate_snapshots() _rotate_snapshots()
@@ -1813,7 +1837,7 @@ def update_snapshot_response(request_id, status, duration_s=None, error=None):
meta["error"] = str(error)[:200] meta["error"] = str(error)[:200]
snapshot["_meta"] = meta snapshot["_meta"] = meta
tmp = path + ".tmp" tmp = path + ".tmp"
with open(tmp, "w") as f: with open(tmp, "w", encoding="utf-8") as f:
json.dump(snapshot, f, ensure_ascii=False, indent=2) json.dump(snapshot, f, ensure_ascii=False, indent=2)
os.replace(tmp, path) os.replace(tmp, path)
except Exception: except Exception:
@@ -1865,6 +1889,27 @@ def _bucket_for_route(route):
# OpenAI-compat backend # OpenAI-compat backend
# ═══════════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════════
def _inject_stored_reasoning(messages):
with _last_reasoning_lock:
snapshot = dict(_last_reasoning_store)
if not snapshot:
return messages
expired = [k for k, v in snapshot.items() if time.time() - v["ts"] > _RESPONSE_TTL]
for k in expired:
with _last_reasoning_lock:
_last_reasoning_store.pop(k, None)
snapshot.pop(k, None)
if not snapshot:
return messages
latest = max(snapshot.values(), key=lambda v: v["ts"])
reasoning = latest.get("reasoning", "")
if not reasoning:
return messages
for msg in messages:
if msg.get("role") == "assistant" and "reasoning_content" not in msg and msg.get("tool_calls"):
msg["reasoning_content"] = reasoning
return messages
def oa_input_to_messages(input_data): def oa_input_to_messages(input_data):
msgs = [] msgs = []
tool_name_by_id = {} tool_name_by_id = {}
@@ -2384,10 +2429,10 @@ def an_stream_to_sse(stream, model, req_id):
"status": status, "created": int(time.time()), "output": completed}}) "status": status, "created": int(time.time()), "output": completed}})
_DEFAULT_CC_CONFIG = { _DEFAULT_CC_CONFIG = {
"workingDir": "/tmp", "workingDir": tempfile.gettempdir(),
"date": "", "date": "",
"environment": "linux", "environment": "windows" if _IS_WINDOWS else "linux",
"shell": "bash", "shell": "powershell" if _IS_WINDOWS else "bash",
"files": [], "files": [],
"structure": [], "structure": [],
"isGitRepo": False, "isGitRepo": False,
@@ -2462,13 +2507,24 @@ def _build_explore_cmd(text_for_url):
api_base = repo_url.replace("/admin/", "/api/v1/repos/") api_base = repo_url.replace("/admin/", "/api/v1/repos/")
else: else:
api_base = repo_url api_base = repo_url
cmd = ( if _IS_WINDOWS:
f"cd /tmp && " cmd = (
f"curl -sL --max-time 15 '{api_base}/contents/README.md' 2>/dev/null | " f"cd $env:TEMP; "
f"python3 -c \"import sys,json,base64; d=json.load(sys.stdin); print(base64.b64decode(d['content']).decode())\" 2>/dev/null | head -600 && " f"$r = Invoke-WebRequest -Uri '{api_base}/contents/README.md' -UseBasicParsing -TimeoutSec 15 2>$null; "
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"if ($r) {{ $j = $r.Content | ConvertFrom-Json; [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($j.content)) | Select-Object -First 600 }}; "
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" 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." 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): 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) _url_in_text = re.search(r"https?://[^\s\]'\\>\",]+", text_buf)
if _url_in_text: if _url_in_text:
_synth_url = _url_in_text.group(0).rstrip(")].,;'\\\"") _synth_url = _url_in_text.group(0).rstrip(")].,;'\\\"")
_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" _synth_just = "Auto-synthesized: URL detected in text, fetching"
# Heuristic 2: File path references → list or read # 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) _file_m = re.search(r"(?:read|open|view|check|examine|cat|show)\s+(?:the\s+)?(?:file\s+)?[`'\"]?(/[^\s'\"]+\.\w+)", _tl)
if _file_m: if _file_m:
_fpath = _file_m.group(1) _fpath = _file_m.group(1)
_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})" _synth_just = f"Auto-synthesized: file reference detected ({_fpath})"
# Heuristic 3: Shell command mentioned in backticks or quotes # 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: if _intent_m:
_intent_text = _intent_m.group(1).strip() _intent_text = _intent_m.group(1).strip()
if len(_intent_text) > 10 and len(_intent_text) < 200: if len(_intent_text) > 10 and len(_intent_text) < 200:
_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]}" _synth_just = f"Auto-synthesized from intent text: {_intent_text[:80]}"
if _synth_cmd: if _synth_cmd:
@@ -3891,11 +3956,13 @@ def _extract_text(content):
# HTTP Server # HTTP Server
# ═══════════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════════
_MAX_REQLOG_LINES = 2000
def _log_resp(resp_id, status, output): def _log_resp(resp_id, status, output):
try: try:
import datetime as _dt import datetime as _dt
_lp = os.path.join(_LOG_DIR, "requests.log") _lp = os.path.join(_LOG_DIR, "requests.log")
with open(_lp, "a") as _f: with open(_lp, "a", encoding="utf-8") as _f:
_f.write(f" RESPONSE id={resp_id} status={status}\n") _f.write(f" RESPONSE id={resp_id} status={status}\n")
if output: if output:
for o in output: for o in output:
@@ -3908,6 +3975,11 @@ def _log_resp(resp_id, status, output):
_f.write(f" -> {ot}\n") _f.write(f" -> {ot}\n")
_f.write(f"{'='*60}\n") _f.write(f"{'='*60}\n")
_f.flush() _f.flush()
_f.seek(0)
lines = _f.readlines()
if len(lines) > _MAX_REQLOG_LINES:
with open(_lp, "w", encoding="utf-8") as _f2:
_f2.writelines(lines[-_MAX_REQLOG_LINES:])
except Exception: except Exception:
pass pass
@@ -4064,10 +4136,26 @@ class Handler(http.server.BaseHTTPRequestHandler):
info["total"] = 0 info["total"] = 0
self.send_json(200, info) self.send_json(200, info)
elif self.path in ("/health", "/v1/health"): elif self.path in ("/health", "/v1/health"):
import resource as _res
_mem_mb = 0 _mem_mb = 0
try: try:
_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: except Exception:
pass pass
_uptime = time.time() - _START_TIME if '_START_TIME' in dir() else 0 _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" resolved_types = [i.get("type") for i in input_data] if isinstance(input_data, list) else "str"
print(f"[{_sid}] prev_id={prev_id} raw={raw_types} resolved={resolved_types}", file=sys.stderr) print(f"[{_sid}] prev_id={prev_id} raw={raw_types} resolved={resolved_types}", file=sys.stderr)
with open(_log_path, "a") as _lf: with open(_log_path, "a", encoding="utf-8") as _lf:
_lf.write(f"\n{'='*60}\n{_ts} [session={_sid}] REQUEST {self.path}\n") _lf.write(f"\n{'='*60}\n{_ts} [session={_sid}] REQUEST {self.path}\n")
_lf.write(f" prev_id={prev_id}\n") _lf.write(f" prev_id={prev_id}\n")
_lf.write(f" raw_input_types={raw_types}\n") _lf.write(f" raw_input_types={raw_types}\n")
_lf.write(f" resolved_input_types={resolved_types}\n") _lf.write(f" resolved_input_types={resolved_types}\n")
_lf.write(f" stream={body.get('stream')} model={body.get('model')}\n") _lf.write(f" stream={body.get('stream')} model={body.get('model')} force_model={FORCE_MODEL}\n")
_lf.write(f" store_keys={list(_response_store.keys())}\n") _lf.write(f" store_keys={list(_response_store.keys())}\n")
if isinstance(input_data, list): if isinstance(input_data, list):
for i, item in enumerate(input_data): for i, item in enumerate(input_data):
@@ -4143,6 +4231,9 @@ class Handler(http.server.BaseHTTPRequestHandler):
_lf.flush() _lf.flush()
model = body.get("model", MODELS[0]["id"] if MODELS else "unknown") model = body.get("model", MODELS[0]["id"] if MODELS else "unknown")
if FORCE_MODEL:
model = FORCE_MODEL
body["model"] = FORCE_MODEL
stream = body.get("stream", False) stream = body.get("stream", False)
_desktop_forced_models = {"gpt-5.4-mini", "gpt-5.4", "gpt-5.5", "gpt-5-codex", "gpt-5.3-codex"} _desktop_forced_models = {"gpt-5.4-mini", "gpt-5.4", "gpt-5.5", "gpt-5-codex", "gpt-5.3-codex"}
_launcher_model = os.environ.get("CODEX_LAUNCHER_MODEL", "") _launcher_model = os.environ.get("CODEX_LAUNCHER_MODEL", "")
@@ -4204,13 +4295,15 @@ class Handler(http.server.BaseHTTPRequestHandler):
body["input"] = input_data body["input"] = input_data
crof_limit = _crof_item_limit(model) crof_limit = _crof_item_limit(model)
if not compacted and isinstance(input_data, list) and len(input_data) > crof_limit: _crof_eligible = TARGET_URL and "crof.ai" in TARGET_URL
if _crof_eligible and not compacted and isinstance(input_data, list) and len(input_data) > crof_limit:
print(f"[crof-adaptive] proactive compact: {len(input_data)} items > limit {crof_limit}", file=sys.stderr) print(f"[crof-adaptive] proactive compact: {len(input_data)} items > limit {crof_limit}", file=sys.stderr)
input_data = _crof_compact_for_retry(input_data, model) input_data = _crof_compact_for_retry(input_data, model)
body = dict(body) body = dict(body)
body["input"] = input_data body["input"] = input_data
messages = oa_input_to_messages(input_data) messages = oa_input_to_messages(input_data)
messages = _inject_stored_reasoning(messages)
instructions = body.get("instructions", "").strip() instructions = body.get("instructions", "").strip()
if instructions: if instructions:
messages.insert(0, {"role": "system", "content": instructions}) messages.insert(0, {"role": "system", "content": instructions})
@@ -4612,7 +4705,7 @@ class Handler(http.server.BaseHTTPRequestHandler):
if n_contents > 10: if n_contents > 10:
debug_path = os.path.join(_LOG_DIR, f"gemini-long-ctx-{self._session_id}.json") debug_path = os.path.join(_LOG_DIR, f"gemini-long-ctx-{self._session_id}.json")
try: try:
with open(debug_path, "w") as dbg: with open(debug_path, "w", encoding="utf-8") as dbg:
json.dump({"contents_count": n_contents, "contents_roles": [c.get("role") for c in contents], "has_tools": has_tools, "model": model, "wrapped_size": len(body_b)}, dbg, indent=2) json.dump({"contents_count": n_contents, "contents_roles": [c.get("role") for c in contents], "has_tools": has_tools, "model": model, "wrapped_size": len(body_b)}, dbg, indent=2)
except Exception: except Exception:
pass pass
@@ -4628,7 +4721,7 @@ class Handler(http.server.BaseHTTPRequestHandler):
if e.code == 400 and OAUTH_PROVIDER.startswith("google"): if e.code == 400 and OAUTH_PROVIDER.startswith("google"):
try: try:
debug_path = os.path.join(_LOG_DIR, "gemini-last-400-request.json") debug_path = os.path.join(_LOG_DIR, "gemini-last-400-request.json")
with open(debug_path, "w") as dbg: with open(debug_path, "w", encoding="utf-8") as dbg:
json.dump({"endpoint": ep, "model": model, "wrapped": wrapped, "error": err_body}, dbg, indent=2) json.dump({"endpoint": ep, "model": model, "wrapped": wrapped, "error": err_body}, dbg, indent=2)
print(f"[{self._session_id}] saved 400 debug request to {debug_path}", file=sys.stderr) print(f"[{self._session_id}] saved 400 debug request to {debug_path}", file=sys.stderr)
except Exception: except Exception:
@@ -4940,7 +5033,8 @@ class Handler(http.server.BaseHTTPRequestHandler):
pass pass
try: try:
for event in oa_stream_to_sse(upstream, model, body.get("request_id") or body.get("id")): reasoning_out = {}
for event in oa_stream_to_sse(upstream, model, body.get("request_id") or body.get("id"), _reasoning_out=reasoning_out):
if tracker and tracker.cancelled.is_set(): if tracker and tracker.cancelled.is_set():
print("[translate-proxy] stream cancelled", file=sys.stderr) print("[translate-proxy] stream cancelled", file=sys.stderr)
break break
@@ -4958,6 +5052,16 @@ class Handler(http.server.BaseHTTPRequestHandler):
_log_resp(last_resp_id, last_status, last_output) _log_resp(last_resp_id, last_status, last_output)
if last_resp_id and input_data is not None: if last_resp_id and input_data is not None:
store_response(last_resp_id, input_data, last_output) store_response(last_resp_id, input_data, last_output)
if reasoning_out.get("text"):
with _last_reasoning_lock:
_last_reasoning_store[last_resp_id or ""] = {
"reasoning": reasoning_out["text"],
"tool_calls": reasoning_out.get("tool_calls", []),
"ts": time.time(),
}
while len(_last_reasoning_store) > _MAX_STORED:
oldest = next(iter(_last_reasoning_store))
del _last_reasoning_store[oldest]
_record_usage(provider, model, success, time.time() - t0, error_type="length" if not success else None) _record_usage(provider, model, success, time.time() - t0, error_type="length" if not success else None)
# Auto-learn provider quirks before flushing the bad response to Codex. # Auto-learn provider quirks before flushing the bad response to Codex.
@@ -4986,7 +5090,7 @@ class Handler(http.server.BaseHTTPRequestHandler):
print(f"[provider-sensor] synthetic retry failed: {e}", file=sys.stderr) print(f"[provider-sensor] synthetic retry failed: {e}", file=sys.stderr)
# Auto-retry on finish_reason=length with no content due to too much context. # Auto-retry on finish_reason=length with no content due to too much context.
if finish_reason == "length" and not has_content and isinstance(input_data, list) and len(input_data) > 5: if finish_reason == "length" and not has_content and isinstance(input_data, list) and len(input_data) > 5 and TARGET_URL and "crof.ai" in TARGET_URL:
print(f"[crof-adaptive] RETRY: finish_reason=length with no content, compacting {n_items} items", file=sys.stderr) print(f"[crof-adaptive] RETRY: finish_reason=length with no content, compacting {n_items} items", file=sys.stderr)
new_input = _crof_compact_for_retry(input_data, model) new_input = _crof_compact_for_retry(input_data, model)
if len(new_input) < len(input_data): if len(new_input) < len(input_data):
@@ -5324,9 +5428,10 @@ class Handler(http.server.BaseHTTPRequestHandler):
metadata = { metadata = {
"run_id": run_id, "run_id": run_id,
"cost_mode": "free", "cost_mode": "free",
"client_id": "".join(secrets.choice(string.digits + string.ascii_lowercase) for _ in range(13)),
} }
if instance_id: if instance_id:
metadata["codebuff_instance_id"] = instance_id metadata["freebuff_instance_id"] = instance_id
chat_body = { chat_body = {
"model": model, "model": model,
@@ -5348,7 +5453,7 @@ class Handler(http.server.BaseHTTPRequestHandler):
headers = { headers = {
"Content-Type": "application/json", "Content-Type": "application/json",
"Authorization": f"Bearer {token}", "Authorization": f"Bearer {token}",
"User-Agent": "codex-launcher/3.10.5", "User-Agent": "ai-sdk/openai-compatible/1.0.25/codebuff",
"x-codebuff-model": model, "x-codebuff-model": model,
} }
if instance_id: if instance_id:
@@ -5496,9 +5601,9 @@ class Handler(http.server.BaseHTTPRequestHandler):
instance_id = _codebuff_get_session(token, model) instance_id = _codebuff_get_session(token, model)
messages = _cb_input_to_messages(input_data, instructions) messages = _cb_input_to_messages(input_data, instructions)
_codebuff_hard_disable_reasoning(messages) _codebuff_hard_disable_reasoning(messages)
metadata = {"run_id": run_id, "cost_mode": "free"} metadata = {"run_id": run_id, "cost_mode": "free", "client_id": secrets.token_hex(7)[:13]}
if instance_id: if instance_id:
metadata["codebuff_instance_id"] = instance_id metadata["freebuff_instance_id"] = instance_id
chat_body = { chat_body = {
"model": model, "messages": messages, "stream": stream, "model": model, "messages": messages, "stream": stream,
"max_tokens": max(body.get("max_output_tokens", 0), 64000), "max_tokens": max(body.get("max_output_tokens", 0), 64000),
@@ -5514,7 +5619,7 @@ class Handler(http.server.BaseHTTPRequestHandler):
if body.get("tool_choice"): if body.get("tool_choice"):
chat_body["tool_choice"] = body["tool_choice"] chat_body["tool_choice"] = body["tool_choice"]
target = f"{_CODEBUFF_API_URL}/api/v1/chat/completions" target = f"{_CODEBUFF_API_URL}/api/v1/chat/completions"
headers = {"Content-Type": "application/json", "Authorization": f"Bearer {token}", "User-Agent": "codex-launcher/3.10.5", "x-codebuff-model": model} headers = {"Content-Type": "application/json", "Authorization": f"Bearer {token}", "User-Agent": "ai-sdk/openai-compatible/1.0.25/codebuff", "x-codebuff-model": model}
if instance_id: if instance_id:
headers["x-codebuff-instance-id"] = instance_id headers["x-codebuff-instance-id"] = instance_id
print(f"[codebuff] retry POST {target} model={model} stream={stream} run={run_id} (thinking disabled via DeepSeek native)", file=sys.stderr) print(f"[codebuff] retry POST {target} model={model} stream={stream} run={run_id} (thinking disabled via DeepSeek native)", file=sys.stderr)
@@ -5924,8 +6029,23 @@ def main():
global SERVER, _START_TIME global SERVER, _START_TIME
_START_TIME = time.time() _START_TIME = time.time()
_init_runtime() _init_runtime()
signal.signal(signal.SIGTERM, _handle_shutdown_signal) try:
_current_cfg = os.path.basename(args.config) if args.config else ""
for _f in os.listdir(_LOG_DIR):
if _f.startswith("proxy-") and _f.endswith(".json") and _f != _current_cfg:
os.remove(os.path.join(_LOG_DIR, _f))
if _f.startswith("models-") and _f.endswith(".json"):
os.remove(os.path.join(_LOG_DIR, _f))
except Exception:
pass
signal.signal(signal.SIGINT, _handle_shutdown_signal) signal.signal(signal.SIGINT, _handle_shutdown_signal)
if _IS_WINDOWS:
if hasattr(signal, "SIGBREAK"):
signal.signal(signal.SIGBREAK, _handle_shutdown_signal)
import atexit
atexit.register(lambda: setattr(sys.modules[__name__], '_SHUTDOWN_REQUESTED', True))
else:
signal.signal(signal.SIGTERM, _handle_shutdown_signal)
try: try:
from http.server import ThreadingHTTPServer as _BaseSrv from http.server import ThreadingHTTPServer as _BaseSrv
except ImportError: except ImportError:
@@ -6132,7 +6252,7 @@ Postamble text."""
_check("FIX23 explore nested JSON: parsed", len(_calls_m) == 1, f"got {len(_calls_m)} calls") _check("FIX23 explore nested JSON: parsed", len(_calls_m) == 1, f"got {len(_calls_m)} calls")
if _calls_m: if _calls_m:
_args_m = json.loads(_calls_m[0].get("arguments", "{}")) _args_m = json.loads(_calls_m[0].get("arguments", "{}"))
_check("FIX23 explore nested JSON: cmd has curl", "curl" in _args_m.get("cmd", ""), f"got {_args_m.get('cmd')!r}") _check("FIX23 explore nested JSON: cmd has fetch cmd", "curl" in _args_m.get("cmd", "") or "Invoke-WebRequest" in _args_m.get("cmd", ""), f"got {_args_m.get('cmd')!r}")
_check("FIX23 explore nested JSON: URL in cmd", "github.rommark.dev" in _args_m.get("cmd", ""), f"missing URL in cmd") _check("FIX23 explore nested JSON: URL in cmd", "github.rommark.dev" in _args_m.get("cmd", ""), f"missing URL in cmd")
# Pattern N: require_escalation block (FIX 24) # Pattern N: require_escalation block (FIX 24)
@@ -6142,7 +6262,7 @@ Postamble text."""
if _calls_n: if _calls_n:
_args_n = json.loads(_calls_n[0].get("arguments", "{}")) _args_n = json.loads(_calls_n[0].get("arguments", "{}"))
_check("FIX24 require_escalation: name is exec_command", _calls_n[0].get("name") == "exec_command", f"got {_calls_n[0].get('name')}") _check("FIX24 require_escalation: name is exec_command", _calls_n[0].get("name") == "exec_command", f"got {_calls_n[0].get('name')}")
_check("FIX24 require_escalation: cmd has curl or echo", "curl" in _args_n.get("cmd", "") or "echo" in _args_n.get("cmd", ""), f"got {_args_n.get('cmd')!r}") _check("FIX24 require_escalation: cmd has fetch or echo", "curl" in _args_n.get("cmd", "") or "echo" in _args_n.get("cmd", "") or "Invoke-WebRequest" in _args_n.get("cmd", "") or "Write-Output" in _args_n.get("cmd", ""), f"got {_args_n.get('cmd')!r}")
# Pattern N2: bare request_escalation_permission tag (FIX 24b) # Pattern N2: bare request_escalation_permission tag (FIX 24b)
_esc_bare = 'I want to proceed.\n<request_escalation_permission />\nPlease let me continue.' _esc_bare = 'I want to proceed.\n<request_escalation_permission />\nPlease let me continue.'
@@ -6154,13 +6274,13 @@ Postamble text."""
# Pattern O: _build_explore_cmd module-level function (FIX 23/25) # Pattern O: _build_explore_cmd module-level function (FIX 23/25)
_cmd_o, _just_o = _build_explore_cmd("https://github.rommark.dev/admin/Z.AI-Chat-for-Android") _cmd_o, _just_o = _build_explore_cmd("https://github.rommark.dev/admin/Z.AI-Chat-for-Android")
_check("FIX23/25 _build_explore_cmd: returns cmd", _cmd_o is not None, "returned None") _check("FIX23/25 _build_explore_cmd: returns cmd", _cmd_o is not None, "returned None")
_check("FIX23/25 _build_explore_cmd: has curl", _cmd_o and "curl" in _cmd_o, f"no curl in {_cmd_o!r}") _check("FIX23/25 _build_explore_cmd: has fetch cmd", _cmd_o and ("curl" in _cmd_o or "Invoke-WebRequest" in _cmd_o), f"no fetch cmd in {_cmd_o!r}")
_check("FIX23/25 _build_explore_cmd: has api path", _cmd_o and "/api/v1/repos/" in _cmd_o, f"no api path in {_cmd_o!r}") _check("FIX23/25 _build_explore_cmd: has api path", _cmd_o and "/api/v1/repos/" in _cmd_o, f"no api path in {_cmd_o!r}")
# Pattern O2: _build_explore_cmd with JSON array containing URL # Pattern O2: _build_explore_cmd with JSON array containing URL
_cmd_o2, _ = _build_explore_cmd('[{"content": "https://github.rommark.dev/admin/Z.AI-Chat-for-Android"}]') _cmd_o2, _ = _build_explore_cmd('[{"content": "https://github.rommark.dev/admin/Z.AI-Chat-for-Android"}]')
_check("FIX23/25 _build_explore_cmd from JSON array: returns cmd", _cmd_o2 is not None, "returned None") _check("FIX23/25 _build_explore_cmd from JSON array: returns cmd", _cmd_o2 is not None, "returned None")
_check("FIX23/25 _build_explore_cmd from JSON array: has curl", _cmd_o2 and "curl" in _cmd_o2, f"no curl in {_cmd_o2!r}") _check("FIX23/25 _build_explore_cmd from JSON array: has fetch cmd", _cmd_o2 and ("curl" in _cmd_o2 or "Invoke-WebRequest" in _cmd_o2), f"no fetch cmd in {_cmd_o2!r}")
print(f"[CC-SELF-TEST] Results: {_counts[0]} passed, {_counts[1]} failed", print(f"[CC-SELF-TEST] Results: {_counts[0]} passed, {_counts[1]} failed",
file=sys.stderr) file=sys.stderr)

File diff suppressed because it is too large Load Diff