Compare commits
20 Commits
v3.10.5
...
869a2625fc
55
CHANGELOG.md
55
CHANGELOG.md
@@ -1,5 +1,60 @@
|
||||
# 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)
|
||||
|
||||
**Security: OAuth Secrets Editor + Import JSON**
|
||||
|
||||
96
README.md
96
README.md
@@ -9,13 +9,28 @@
|
||||
<a href="https://z.ai/subscribe?ic=ROK78RJKNW">z.ai/subscribe</a>
|
||||
</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>
|
||||
|
||||
<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">
|
||||
<sub>
|
||||
Windows version by <a href="https://github.com/cobra91">cobra91</a> •
|
||||
Original Linux development by <a href="https://github.com/roman-ryzenadvanced">roman-ryzenadvanced</a>
|
||||
</sub>
|
||||
</p>
|
||||
|
||||
<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
|
||||
final content turn sent to Gemini, even after compaction.
|
||||
- **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 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` |
|
||||
@@ -618,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)*
|
||||
|
||||
---
|
||||
|
||||
@@ -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> •
|
||||
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
|
||||
|
||||
- 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 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.
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()
|
||||
@@ -393,7 +393,25 @@ PROVIDER_PRESETS = {
|
||||
},
|
||||
"Codebuff (Free DeepSeek/Kimi)": {
|
||||
"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",
|
||||
"models": [
|
||||
"deepseek/deepseek-v4-pro", "deepseek/deepseek-v4-flash",
|
||||
@@ -1780,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")
|
||||
@@ -1959,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)
|
||||
@@ -2055,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):
|
||||
@@ -2199,6 +2225,22 @@ class LauncherWin(Gtk.Window):
|
||||
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:
|
||||
self._usage_window = UsageWindow(self)
|
||||
@@ -2790,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:
|
||||
@@ -2799,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)
|
||||
@@ -2810,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)
|
||||
@@ -2828,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("*")
|
||||
@@ -2837,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():
|
||||
@@ -2854,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):
|
||||
@@ -3685,10 +3968,10 @@ class EditEndpointDialog(Gtk.Dialog):
|
||||
def _codebuff_auth_thread():
|
||||
try:
|
||||
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()
|
||||
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", "")
|
||||
@@ -3707,13 +3990,13 @@ class EditEndpointDialog(Gtk.Dialog):
|
||||
|
||||
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
|
||||
while time.time() < deadline:
|
||||
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")
|
||||
|
||||
3123
src/codex-launcher-gui.py
Normal file
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
2107
src/codex_launcher_lib.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
@@ -294,7 +311,7 @@ _conn_pool = {}
|
||||
|
||||
_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_AGENT_MAP = {
|
||||
"deepseek/deepseek-v4-pro": "base2-free-deepseek",
|
||||
@@ -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()
|
||||
@@ -331,11 +351,11 @@ def _codebuff_get_session(token, model):
|
||||
return sc["instance_id"]
|
||||
try:
|
||||
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={
|
||||
"Content-Type": "application/json",
|
||||
"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,
|
||||
})
|
||||
try:
|
||||
@@ -383,7 +403,7 @@ def _codebuff_start_run(token, agent_id):
|
||||
req = urllib.request.Request(url, data=body, headers={
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {token}",
|
||||
"User-Agent": "codex-launcher/3.10.5",
|
||||
"User-Agent": "ai-sdk/openai-compatible/1.0.25/codebuff",
|
||||
})
|
||||
try:
|
||||
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={
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {token}",
|
||||
"User-Agent": "codex-launcher/3.10.5",
|
||||
"User-Agent": "ai-sdk/openai-compatible/1.0.25/codebuff",
|
||||
})
|
||||
try:
|
||||
urllib.request.urlopen(req, timeout=10)
|
||||
@@ -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,6 +1393,7 @@ def _crof_record(model, n_items, success):
|
||||
global_limit = v["limit"]
|
||||
_CROF_ADAPTIVE["global_item_limit"] = global_limit
|
||||
|
||||
if TARGET_URL and "crof.ai" in TARGET_URL:
|
||||
print(f"[crof-adaptive] model={model} items={n_items} {'OK' if success else 'FAIL'} -> limit={ml.get('limit',30)} global={global_limit}", file=sys.stderr)
|
||||
|
||||
def _crof_item_limit(model):
|
||||
@@ -1416,6 +1439,7 @@ 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)}]}
|
||||
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
|
||||
|
||||
@@ -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,6 +2507,17 @@ def _build_explore_cmd(text_for_url):
|
||||
api_base = repo_url.replace("/admin/", "/api/v1/repos/")
|
||||
else:
|
||||
api_base = repo_url
|
||||
if _IS_WINDOWS:
|
||||
cmd = (
|
||||
f"cd $env:TEMP; "
|
||||
f"$r = Invoke-WebRequest -Uri '{api_base}/contents/README.md' -UseBasicParsing -TimeoutSec 15 2>$null; "
|
||||
f"if ($r) {{ $j = $r.Content | ConvertFrom-Json; [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($j.content)) | Select-Object -First 600 }}; "
|
||||
f"$r2 = Invoke-WebRequest -Uri '{api_base}/contents' -UseBasicParsing -TimeoutSec 15 2>$null; "
|
||||
f"if ($r2) {{ $j2 = $r2.Content | ConvertFrom-Json; $j2 | Select-Object -First 50 | ForEach-Object {{ $_.path + ' ' + $_.type }} }}; "
|
||||
f"$r3 = Invoke-WebRequest -Uri '{api_base}/releases' -UseBasicParsing -TimeoutSec 15 2>$null; "
|
||||
f"if ($r3) {{ ($r3.Content | ConvertFrom-Json | Select-Object -First 3 | ConvertTo-Json).Substring(0, [Math]::Min(2000, ($r3.Content | ConvertFrom-Json | Select-Object -First 3 | ConvertTo-Json).Length)) }}"
|
||||
)
|
||||
else:
|
||||
cmd = (
|
||||
f"cd /tmp && "
|
||||
f"curl -sL --max-time 15 '{api_base}/contents/README.md' 2>/dev/null | "
|
||||
@@ -3322,6 +3378,9 @@ 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(")].,;'\\\"")
|
||||
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"
|
||||
|
||||
@@ -3330,6 +3389,9 @@ def cc_stream_to_sse(cc_stream, model, req_id):
|
||||
_file_m = re.search(r"(?:read|open|view|check|examine|cat|show)\s+(?:the\s+)?(?:file\s+)?[`'\"]?(/[^\s'\"]+\.\w+)", _tl)
|
||||
if _file_m:
|
||||
_fpath = _file_m.group(1)
|
||||
if _IS_WINDOWS:
|
||||
_synth_cmd = f"Get-Content '{_fpath}' -ErrorAction SilentlyContinue | Select-Object -First 200; if (-not $?) {{ Get-Item '{_fpath}' | Select-Object Name,Length,LastWriteTime }}"
|
||||
else:
|
||||
_synth_cmd = f"cat '{_fpath}' 2>/dev/null | head -200 || ls -la '{_fpath}'"
|
||||
_synth_just = f"Auto-synthesized: file reference detected ({_fpath})"
|
||||
|
||||
@@ -3358,6 +3420,9 @@ 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:
|
||||
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]}"
|
||||
|
||||
@@ -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,9 +4136,25 @@ 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:
|
||||
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
|
||||
@@ -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,9 +5428,10 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
||||
metadata = {
|
||||
"run_id": run_id,
|
||||
"cost_mode": "free",
|
||||
"client_id": "".join(secrets.choice(string.digits + string.ascii_lowercase) for _ in range(13)),
|
||||
}
|
||||
if instance_id:
|
||||
metadata["codebuff_instance_id"] = instance_id
|
||||
metadata["freebuff_instance_id"] = instance_id
|
||||
|
||||
chat_body = {
|
||||
"model": model,
|
||||
@@ -5348,7 +5453,7 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"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,
|
||||
}
|
||||
if instance_id:
|
||||
@@ -5496,9 +5601,9 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
||||
instance_id = _codebuff_get_session(token, model)
|
||||
messages = _cb_input_to_messages(input_data, instructions)
|
||||
_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:
|
||||
metadata["codebuff_instance_id"] = instance_id
|
||||
metadata["freebuff_instance_id"] = instance_id
|
||||
chat_body = {
|
||||
"model": model, "messages": messages, "stream": stream,
|
||||
"max_tokens": max(body.get("max_output_tokens", 0), 64000),
|
||||
@@ -5514,7 +5619,7 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
||||
if body.get("tool_choice"):
|
||||
chat_body["tool_choice"] = body["tool_choice"]
|
||||
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:
|
||||
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)
|
||||
@@ -5924,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:
|
||||
@@ -6132,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)
|
||||
@@ -6142,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.'
|
||||
@@ -6154,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