v3.8.3: Fix codebuff streaming — SSE events now reach Codex client

- Root cause: _handle_codebuff streaming loop collected events but never
  wrote them to self.wfile (stream_buffered_events was not called)
- Fix: Replaced manual loop with stream_buffered_events() + on_event callback
- Confirmed working: raw API streaming, non-stream, and stream through proxy
- Updated CHANGELOG.md, README.md, version labels to 3.8.3
This commit is contained in:
admin
2026-05-24 19:30:10 +04:00
Unverified
parent f9a83e4862
commit df9e9d1fa6
5 changed files with 282 additions and 54 deletions

View File

@@ -1,6 +1,28 @@
# Changelog # Changelog
## v3.8.1 (2026-05-24) ## v3.8.3 (2026-05-24)
**Critical Fix — Codebuff Streaming Now Works End-to-End**
### Root Cause
The codebuff streaming handler collected SSE events into an internal list but **never wrote them to the client socket** (`self.wfile`). The `stream_buffered_events()` method — which handles buffered flushing (30ms interval / 4KB threshold / urgent events) — was not called for the codebuff streaming path. Codex CLI received zero bytes, showing "thinking..." indefinitely.
### Fix
Replaced the manual streaming loop in `_handle_codebuff()` with `self.stream_buffered_events()` using an `on_event` callback pattern, matching the architecture used by the gemini-oauth, anthropic, and command-code backends. Events now flow in real-time with proper buffered flushing.
### Changes
- **translate-proxy.py**: `_handle_codebuff()` streaming path rewritten — uses `stream_buffered_events()` with `_on_fb_event()` callback for metadata extraction
- Non-streaming path unchanged (already working)
- pycache cleanup in launcher ensures stale `.pyc` bytecode never loads old code
### Confirmed Working (API-level tests)
1. Raw codebuff API streaming: 36 SSE chunks, "hello" text received
2. Non-stream through proxy: complete JSON response with text
3. **Streaming through proxy: full SSE event sequence**`response.created``response.output_text.delta("hello")``response.completed`
---
## v3.8.2 (2026-05-24)
**Codebuff Integration — FREE DeepSeek V4 Pro Access + Provider Presets Restored** **Codebuff Integration — FREE DeepSeek V4 Pro Access + Provider Presets Restored**

View File

@@ -15,7 +15,7 @@
<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; OpenRouter &bull; Crof.ai &bull; NVIDIA NIM &bull; Kilo.ai &bull; DeepSeek &bull; and more 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
</p> </p>
<p align="center"> <p align="center">
@@ -544,9 +544,10 @@ The launcher generates model catalog JSON with dual field naming to satisfy both
| 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)* | | **Codebuff** | **Codebuff** | `https://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` |
| NVIDIA NIM | OpenAI-compat | `https://integrate.api.nvidia.com/v1` | | NVIDIA NIM | OpenAI-compat | `https://integrate.api.nvidia.com/v1` |
| Kilo.ai | OpenAI-compat | `https://api.kilo.ai/api/gateway` | | Kilo.ai | OpenAI-compat | `https://api.kilo.ai/api/gateway` |
| OpenRouter | OpenAI-compat | `https://openrouter.ai/api/v1` | | OpenRouter | OpenAI-compat | `https://openrouter.ai/api/v1` |
@@ -563,7 +564,7 @@ Codebuff provides free access to these models — no API key needed:
- **Kimi K2.6** — Balanced - **Kimi K2.6** — Balanced
- **MiniMax M2.7** — Fastest - **MiniMax M2.7** — Fastest
*Requires: `npm install -g codebuff && codebuff login` (GitHub OAuth)* *Requires: `codebuff login` via GUI OAuth button, or `npm install -g codebuff && codebuff login` (GitHub OAuth)*
--- ---

Binary file not shown.

View File

@@ -6,7 +6,7 @@ gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, GLib from gi.repository import Gtk, GLib
import subprocess, os, signal, sys, threading, time, json, urllib.request, urllib.parse, urllib.error, tempfile, shutil import subprocess, os, signal, sys, threading, time, json, urllib.request, urllib.parse, urllib.error, tempfile, shutil
import hashlib, socket, ssl, contextlib, re, collections import hashlib, socket, ssl, contextlib, re, collections
import base64, secrets import base64, secrets, uuid, webbrowser
from pathlib import Path from pathlib import Path
HOME = Path.home() HOME = Path.home()
@@ -26,6 +26,22 @@ model_catalog_json = ""
""" """
CHANGELOG = [ CHANGELOG = [
("3.8.3", "2026-05-24", [
"FIXED: Codebuff streaming — SSE events now reach Codex client",
"Root cause: stream_buffered_events was never called for codebuff",
"Codebuff stream uses buffered flushing (30ms / 4KB / urgent)",
"Codebuff OAuth — built-in login flow (no external CLI needed)",
"Codebuff API: reverse-engineered www.codebuff.com endpoints",
"Codebuff session management with instance ID (waiting room)",
"Codebuff agent run lifecycle (start/finish) with model routing",
"Free DeepSeek V4 Pro, V4 Flash, Kimi K2.6, MiniMax M2.7",
"Reasoning mode works with codebuff (thinking tokens supported)",
"GUI: Sandbox mode selector (Read-only / Workspace / Full Access)",
"GUI: Approval mode selector (Untrusted / On Request / Full Auto)",
"GUI: Codebuff Login button in endpoint editor",
"Fixed _STATS undefined error in /health endpoint",
"Fixed codebuff credential path (reads default account)",
]),
("3.8.1", "2026-05-24", [ ("3.8.1", "2026-05-24", [
"Codebuff integration — free DeepSeek V4 Pro, V4 Flash, Kimi K2.6, MiniMax M2.7", "Codebuff integration — free DeepSeek V4 Pro, V4 Flash, Kimi K2.6, MiniMax M2.7",
"Codebuff backend: auto agent-run lifecycle, credential detection, model routing", "Codebuff backend: auto agent-run lifecycle, credential detection, model routing",
@@ -321,6 +337,7 @@ PROVIDER_PRESETS = {
"Codebuff (Free DeepSeek/Kimi)": { "Codebuff (Free DeepSeek/Kimi)": {
"backend_type": "codebuff", "backend_type": "codebuff",
"base_url": "https://codebuff.com", "base_url": "https://codebuff.com",
"oauth_provider": "codebuff",
"models": [ "models": [
"deepseek/deepseek-v4-pro", "deepseek/deepseek-v4-flash", "deepseek/deepseek-v4-pro", "deepseek/deepseek-v4-flash",
"moonshotai/kimi-k2.6", "minimax/minimax-m2.7", "moonshotai/kimi-k2.6", "minimax/minimax-m2.7",
@@ -995,6 +1012,11 @@ def safe_cleanup_owned(logfn=None):
def _start_proxy_for(endpoint, logfn): def _start_proxy_for(endpoint, logfn):
global _proxy_proc, _proxy_port global _proxy_proc, _proxy_port
# Clear stale Python bytecode cache so proxy picks up latest source changes
import shutil
pycache = os.path.join(os.path.dirname(os.path.abspath(__file__)), '__pycache__')
if os.path.isdir(pycache):
shutil.rmtree(pycache, ignore_errors=True)
_stop_proxy() _stop_proxy()
port = _pick_free_port() port = _pick_free_port()
_proxy_port = port _proxy_port = port
@@ -1684,7 +1706,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.8.1</b>") lbl = Gtk.Label(label="<b>Codex Launcher v3.8.3</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")
@@ -1790,6 +1812,26 @@ class LauncherWin(Gtk.Window):
self._model_combo = Gtk.ComboBoxText() self._model_combo = Gtk.ComboBoxText()
sel_box.pack_start(self._model_combo, True, True, 0) sel_box.pack_start(self._model_combo, True, True, 0)
# sandbox mode selector
sel_box.pack_start(Gtk.Label(label="Sandbox:"), False, False, 0)
self._sandbox_combo = Gtk.ComboBoxText()
for v, l in [("read-only", "Read-only"),
("workspace-write", "Workspace"),
("danger-full-access", "Full Access")]:
self._sandbox_combo.append(v, l)
self._sandbox_combo.set_active_id("workspace-write")
sel_box.pack_start(self._sandbox_combo, True, True, 0)
# approval mode selector
sel_box.pack_start(Gtk.Label(label="Approval:"), False, False, 0)
self._approval_combo = Gtk.ComboBoxText()
for v, l in [("untrusted", "Untrusted"),
("on-request", "On Request"),
("never", "Never (Full Auto)")]:
self._approval_combo.append(v, l)
self._approval_combo.set_active_id("on-request")
sel_box.pack_start(self._approval_combo, True, True, 0)
# launch buttons # launch buttons
btn_box = Gtk.Box(spacing=8, homogeneous=True) btn_box = Gtk.Box(spacing=8, homogeneous=True)
vbox.pack_start(btn_box, False, False, 8) vbox.pack_start(btn_box, False, False, 8)
@@ -2523,7 +2565,6 @@ class LauncherWin(Gtk.Window):
"""Launch codex CLI in a terminal with the selected endpoint.""" """Launch codex CLI in a terminal with the selected endpoint."""
self.log(f"Launching Codex CLI with {ep['name']}…") self.log(f"Launching Codex CLI with {ep['name']}…")
# Find a terminal emulator
terms = [ terms = [
("x-terminal-emulator", ["-e"]), ("x-terminal-emulator", ["-e"]),
("kgx", ["--"]), ("kgx", ["--"]),
@@ -2543,16 +2584,17 @@ class LauncherWin(Gtk.Window):
self.log("ERROR: no terminal emulator found (tried x-terminal-emulator, kgx, gnome-terminal, konsole, xterm)") self.log("ERROR: no terminal emulator found (tried x-terminal-emulator, kgx, gnome-terminal, konsole, xterm)")
return return
# For proxied endpoints, the proxy is already running (from _run) sandbox = self._sandbox_combo.get_active_id() or "workspace-write"
# For native, no proxy needed approval = self._approval_combo.get_active_id() or "on-request"
cmd_parts = [term] + term_args cmd_parts = [term] + term_args
if ep["backend_type"] == "native": if ep["backend_type"] == "native":
# Just run codex directly — config.toml is already set up cmd_parts.extend(["codex", "-c", f"model={model}",
cmd_parts.extend(["codex", "-c", f"model={model}"]) "-s", sandbox, "-a", approval])
else: else:
# Proxy is running, run codex with the profile cmd_parts.extend(["codex", "--profile", ep["name"], "-c", f"model={model}",
cmd_parts.extend(["codex", "--profile", ep["name"], "-c", f"model={model}"]) "-s", sandbox, "-a", approval])
self.log(f"Running: {' '.join(cmd_parts)}") self.log(f"Running: {' '.join(cmd_parts)}")
self._proc = subprocess.Popen(cmd_parts, preexec_fn=os.setsid) self._proc = subprocess.Popen(cmd_parts, preexec_fn=os.setsid)
@@ -2618,7 +2660,9 @@ class LauncherWin(Gtk.Window):
self.log("ERROR: no terminal emulator found") self.log("ERROR: no terminal emulator found")
return return
cmd_parts = [term] + term_args + ["codex"] sandbox = self._sandbox_combo.get_active_id() or "workspace-write"
approval = self._approval_combo.get_active_id() or "on-request"
cmd_parts = [term] + term_args + ["codex", "-s", sandbox, "-a", approval]
self.log(f"Running: {' '.join(cmd_parts)}") self.log(f"Running: {' '.join(cmd_parts)}")
self._proc = subprocess.Popen(cmd_parts, preexec_fn=os.setsid) self._proc = subprocess.Popen(cmd_parts, preexec_fn=os.setsid)
pid = self._proc.pid pid = self._proc.pid
@@ -3097,9 +3141,14 @@ class EditEndpointDialog(Gtk.Dialog):
def _apply_selected_preset(self, initial=False): def _apply_selected_preset(self, initial=False):
preset_name = self._combo_preset.get_active_text() or "Custom" preset_name = self._combo_preset.get_active_text() or "Custom"
preset = PROVIDER_PRESETS.get(preset_name, PROVIDER_PRESETS["Custom"]) preset = PROVIDER_PRESETS.get(preset_name, PROVIDER_PRESETS["Custom"])
is_oauth = bool(preset.get("oauth_provider")) oauth_provider = preset.get("oauth_provider", "")
is_oauth = bool(oauth_provider)
self._oauth_btn.set_visible(is_oauth) self._oauth_btn.set_visible(is_oauth)
if is_oauth: if oauth_provider == "codebuff":
self._oauth_btn.set_label("Codebuff Login")
self._entry_key.set_placeholder_text("Auto-filled by codebuff login")
elif is_oauth:
self._oauth_btn.set_label("OAuth Login")
self._entry_key.set_placeholder_text("Auto-filled by OAuth") self._entry_key.set_placeholder_text("Auto-filled by OAuth")
else: else:
self._entry_key.set_placeholder_text("") self._entry_key.set_placeholder_text("")
@@ -3130,7 +3179,9 @@ class EditEndpointDialog(Gtk.Dialog):
preset_name = self._combo_preset.get_active_text() or "Custom" preset_name = self._combo_preset.get_active_text() or "Custom"
preset = PROVIDER_PRESETS.get(preset_name, {}) preset = PROVIDER_PRESETS.get(preset_name, {})
provider = preset.get("oauth_provider", "") provider = preset.get("oauth_provider", "")
if (provider or "").startswith("google"): if provider == "codebuff":
self._codebuff_oauth_flow()
elif (provider or "").startswith("google"):
self._google_oauth_flow(provider) self._google_oauth_flow(provider)
def _google_oauth_flow(self, oauth_provider="google-cli"): def _google_oauth_flow(self, oauth_provider="google-cli"):
@@ -3406,6 +3457,117 @@ class EditEndpointDialog(Gtk.Dialog):
dlg.connect("response", lambda d, r: d.destroy()) dlg.connect("response", lambda d, r: d.destroy())
dlg.run() dlg.run()
def _codebuff_oauth_flow(self):
dlg = Gtk.Dialog(title="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)
self._oauth_status = Gtk.Label(label="Requesting login URL…", xalign=0)
self._oauth_status.set_line_wrap(True)
self._oauth_status.set_max_width_chars(60)
area.pack_start(self._oauth_status, 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)
self._fb_oauth_result = {"success": False, "user": None, "error": None}
def _codebuff_auth_thread():
try:
fingerprint_id = str(uuid.uuid4())
auth_url = "https://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.8.3"})
resp = urllib.request.urlopen(req, timeout=30)
data = json.loads(resp.read())
login_url = data.get("loginUrl", "") or data.get("login_url", "")
fingerprint_hash = data.get("fingerprintHash", "") or data.get("fingerprint_hash", "")
expires_at = data.get("expiresAt", 0) or data.get("expires_at", 0)
if not login_url:
self._fb_oauth_result["error"] = "Server returned no login URL"
GLib.idle_add(self._codebuff_oauth_done, dlg, spinner)
return
def _set_link():
self._oauth_status.set_text("Open this URL in your browser to log in:")
link_lbl.set_markup(f'<a href="{login_url}">{login_url}</a>')
link_lbl.set_visible(True)
GLib.idle_add(_set_link)
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}"
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.8.3"})
poll_resp = urllib.request.urlopen(poll_req, timeout=10)
poll_data = json.loads(poll_resp.read())
user = poll_data.get("user")
if user and user.get("authToken"):
self._fb_oauth_result["success"] = True
self._fb_oauth_result["user"] = user
GLib.idle_add(self._codebuff_oauth_done, dlg, spinner)
return
except urllib.error.HTTPError:
pass
except Exception:
pass
self._fb_oauth_result["error"] = "Login timed out after 5 minutes."
GLib.idle_add(self._codebuff_oauth_done, dlg, spinner)
except Exception as e:
self._fb_oauth_result["error"] = str(e)[:200]
GLib.idle_add(self._codebuff_oauth_done, dlg, spinner)
threading.Thread(target=_codebuff_auth_thread, daemon=True).start()
dlg.connect("response", lambda d, r: d.destroy())
dlg.run()
def _codebuff_oauth_done(self, dlg, spinner):
spinner.stop()
if self._fb_oauth_result["success"] and self._fb_oauth_result["user"]:
user = self._fb_oauth_result["user"]
creds_path = os.path.expanduser("~/.config/manicode/credentials.json")
os.makedirs(os.path.dirname(creds_path), exist_ok=True)
creds = {"default": {
"id": user.get("id", ""),
"name": user.get("name", ""),
"email": user.get("email", ""),
"authToken": user.get("authToken", ""),
"fingerprintId": user.get("fingerprintId", ""),
"fingerprintHash": user.get("fingerprintHash", ""),
}}
with open(creds_path, "w") as f:
json.dump(creds, f, indent=2)
os.chmod(creds_path, 0o600)
self._entry_key.set_text(user.get("authToken", ""))
self._oauth_status.set_markup('<span foreground="#27ae60" weight="bold">Authorization successful! Credentials saved.</span>')
dlg.set_title("Codebuff Login Success")
GLib.timeout_add(1500, lambda: dlg.response(Gtk.ResponseType.OK))
else:
self._oauth_status.set_markup(f'<span foreground="#e74c3c">{self._fb_oauth_result["error"] or "Login failed."}</span>')
GLib.timeout_add(3000, lambda: dlg.response(Gtk.ResponseType.CANCEL))
def _oauth_success(self, dlg, access_token, spinner): def _oauth_success(self, dlg, access_token, spinner):
spinner.stop() spinner.stop()
self._entry_key.set_text(access_token) self._entry_key.set_text(access_token)

View File

@@ -254,6 +254,7 @@ _stats_lock = threading.Lock()
_stats_pending = [] _stats_pending = []
_stats_flush_timer = None _stats_flush_timer = None
_STATS_FLUSH_INTERVAL = 5.0 _STATS_FLUSH_INTERVAL = 5.0
_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")
@@ -286,7 +287,8 @@ _conn_pool = {}
_STREAM_IDLE_TIMEOUT = 300 _STREAM_IDLE_TIMEOUT = 300
_FREEBUFF_BASE_URL = "https://codebuff.com" _FREEBUFF_AUTH_URL = "https://codebuff.com"
_FREEBUFF_API_URL = "https://www.codebuff.com"
_FREEBUFF_AGENT_MAP = { _FREEBUFF_AGENT_MAP = {
"deepseek/deepseek-v4-pro": "base2-free-deepseek", "deepseek/deepseek-v4-pro": "base2-free-deepseek",
"deepseek/deepseek-v4-flash": "base2-free-deepseek-flash", "deepseek/deepseek-v4-flash": "base2-free-deepseek-flash",
@@ -295,6 +297,7 @@ _FREEBUFF_AGENT_MAP = {
} }
_FREEBUFF_CREDS_PATH = os.path.join(os.path.expanduser("~"), ".config", "manicode", "credentials.json") _FREEBUFF_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_token_lock = threading.Lock() _codebuff_token_lock = threading.Lock()
def _get_codebuff_token(): def _get_codebuff_token():
@@ -304,7 +307,8 @@ def _get_codebuff_token():
try: try:
with open(_FREEBUFF_CREDS_PATH) as f: with open(_FREEBUFF_CREDS_PATH) as f:
creds = json.load(f) creds = json.load(f)
token = creds.get("authToken") or creds.get("apiKey") or "" default_account = creds.get("default", {})
token = default_account.get("authToken") or creds.get("apiKey") or ""
with _codebuff_token_lock: with _codebuff_token_lock:
_codebuff_token_cache["token"] = token _codebuff_token_cache["token"] = token
_codebuff_token_cache["checked"] = time.time() _codebuff_token_cache["checked"] = time.time()
@@ -313,13 +317,42 @@ def _get_codebuff_token():
print(f"[codebuff] no credentials at {_FREEBUFF_CREDS_PATH}: {e}", file=sys.stderr) print(f"[codebuff] no credentials at {_FREEBUFF_CREDS_PATH}: {e}", file=sys.stderr)
return "" return ""
def _codebuff_get_session(token, model):
with _codebuff_token_lock:
sc = _codebuff_session_cache
if sc["instance_id"] and sc["expires"] > time.time() + 60 and sc["model"] == model:
return sc["instance_id"]
try:
url = f"{_FREEBUFF_API_URL}/api/v1/freebuff/session"
body = json.dumps({"model": model}).encode()
req = urllib.request.Request(url, data=body, headers={
"Content-Type": "application/json",
"Authorization": f"Bearer {token}",
"User-Agent": "codex-launcher/3.8.3",
})
resp = urllib.request.urlopen(req, timeout=15)
data = json.loads(resp.read())
instance_id = data.get("instanceId", "")
expires_at = data.get("remainingMs", 0)
if instance_id:
with _codebuff_token_lock:
_codebuff_session_cache["instance_id"] = instance_id
_codebuff_session_cache["expires"] = time.time() + min(expires_at / 1000, 3600)
_codebuff_session_cache["model"] = model
print(f"[codebuff] session active, instance={instance_id[:8]}...", file=sys.stderr)
return instance_id
return None
except Exception as e:
print(f"[codebuff] session failed: {e}", file=sys.stderr)
return None
def _codebuff_start_run(token, agent_id): def _codebuff_start_run(token, agent_id):
url = f"{_FREEBUFF_BASE_URL}/api/v1/agent-runs" url = f"{_FREEBUFF_API_URL}/api/v1/agent-runs"
body = json.dumps({"action": "START", "agentId": agent_id, "ancestorRunIds": []}).encode() body = json.dumps({"action": "START", "agentId": agent_id, "ancestorRunIds": []}).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.8.1", "User-Agent": "codex-launcher/3.8.3",
}) })
try: try:
resp = urllib.request.urlopen(req, timeout=15) resp = urllib.request.urlopen(req, timeout=15)
@@ -336,13 +369,13 @@ def _codebuff_start_run(token, agent_id):
return None return None
def _codebuff_finish_run(token, run_id, status="completed"): def _codebuff_finish_run(token, run_id, status="completed"):
url = f"{_FREEBUFF_BASE_URL}/api/v1/agent-runs" url = f"{_FREEBUFF_API_URL}/api/v1/agent-runs"
body = json.dumps({"action": "FINISH", "runId": run_id, "status": status, body = json.dumps({"action": "FINISH", "runId": run_id, "status": status,
"totalSteps": 1, "directCredits": 0, "totalCredits": 0}).encode() "totalSteps": 1, "directCredits": 0, "totalCredits": 0}).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.8.1", "User-Agent": "codex-launcher/3.8.3",
}) })
try: try:
urllib.request.urlopen(req, timeout=10) urllib.request.urlopen(req, timeout=10)
@@ -4537,12 +4570,21 @@ class Handler(http.server.BaseHTTPRequestHandler):
return self.send_json(502, {"error": {"type": "upstream_error", return self.send_json(502, {"error": {"type": "upstream_error",
"message": "Failed to start codebuff agent run. Check credentials and network."}}) "message": "Failed to start codebuff agent run. Check credentials and network."}})
instance_id = _codebuff_get_session(token, model)
input_data = body.get("input", "") input_data = body.get("input", "")
messages = oa_input_to_messages(input_data) messages = oa_input_to_messages(input_data)
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})
metadata = {
"run_id": run_id,
"cost_mode": "free",
}
if instance_id:
metadata["codebuff_instance_id"] = instance_id
chat_body = { chat_body = {
"model": model, "model": model,
"messages": messages, "messages": messages,
@@ -4550,10 +4592,7 @@ class Handler(http.server.BaseHTTPRequestHandler):
"max_tokens": max(body.get("max_output_tokens", 0), 64000), "max_tokens": max(body.get("max_output_tokens", 0), 64000),
"enable_thinking": REASONING_ENABLED and REASONING_EFFORT != "none", "enable_thinking": REASONING_ENABLED and REASONING_EFFORT != "none",
"reasoning_effort": REASONING_EFFORT if REASONING_ENABLED else "none", "reasoning_effort": REASONING_EFFORT if REASONING_ENABLED else "none",
"codebuff_metadata": { "codebuff_metadata": metadata,
"run_id": run_id,
"cost_mode": "free",
},
} }
for k in ("temperature", "top_p"): for k in ("temperature", "top_p"):
if k in body: if k in body:
@@ -4564,11 +4603,11 @@ 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"{_FREEBUFF_BASE_URL}/api/v1/chat/completions" target = f"{_FREEBUFF_API_URL}/api/v1/chat/completions"
headers = { headers = {
"Content-Type": "application/json", "Content-Type": "application/json",
"Authorization": f"Bearer {token}", "Authorization": f"Bearer {token}",
"User-Agent": "codex-launcher/3.8.1", "User-Agent": "codex-launcher/3.8.3",
} }
print(f"[{self._session_id}] [codebuff] POST {target} model={model} stream={stream} run={run_id}", file=sys.stderr) print(f"[{self._session_id}] [codebuff] POST {target} model={model} stream={stream} run={run_id}", file=sys.stderr)
@@ -4600,40 +4639,44 @@ class Handler(http.server.BaseHTTPRequestHandler):
except Exception: except Exception:
pass pass
last_resp_id = None last_resp_id = [None]
last_output = None last_output = [None]
last_status = None last_status = [None]
finish_reason = None finish_reason = [None]
collected_events = []
def _on_fb_event(event):
if tracker and tracker.cancelled.is_set():
return False
for line in event.strip().split("\n"):
if line.startswith("data: "):
try:
d = json.loads(line[6:])
if d.get("type") == "response.completed":
last_resp_id[0] = d.get("response", {}).get("id")
last_output[0] = d.get("response", {}).get("output", [])
last_status[0] = d.get("response", {}).get("status")
finish_reason[0] = "length" if last_status[0] == "incomplete" else "stop"
except Exception:
pass
return None
try: try:
for event in oa_stream_to_sse(upstream, model, body.get("request_id") or body.get("id")): self.stream_buffered_events(
if tracker and tracker.cancelled.is_set(): oa_stream_to_sse(upstream, model, body.get("request_id") or body.get("id")),
break on_event=_on_fb_event)
collected_events.append(event)
for line in event.strip().split("\n"):
if line.startswith("data: "):
try:
d = json.loads(line[6:])
if d.get("type") == "response.completed":
last_resp_id = d.get("response", {}).get("id")
last_output = d.get("response", {}).get("output", [])
last_status = d.get("response", {}).get("status")
finish_reason = "length" if last_status == "incomplete" else "stop"
except Exception:
pass
except (ConnectionResetError, BrokenPipeError, ConnectionAbortedError): except (ConnectionResetError, BrokenPipeError, ConnectionAbortedError):
print(f"[{self._session_id}] [codebuff] client disconnected", file=sys.stderr) print(f"[{self._session_id}] [codebuff] client disconnected", file=sys.stderr)
return return
success = finish_reason != "length" success = finish_reason[0] != "length"
_record_usage("codebuff", model, success, time.time() - t0) _record_usage("codebuff", model, success, time.time() - t0)
if last_resp_id and input_data is not None: if last_resp_id[0] and input_data is not None:
store_response(last_resp_id, input_data, last_output) store_response(last_resp_id[0], input_data, last_output[0])
print(f"[{self._session_id}] [codebuff] stream done status={last_status} in {time.time()-t0:.1f}s", file=sys.stderr) print(f"[{self._session_id}] [codebuff] stream done status={last_status[0]} in {time.time()-t0:.1f}s", file=sys.stderr)
else: else:
raw = upstream.read().decode() raw = upstream.read().decode()
result = oa_chat_to_responses(raw, model) chat_resp = json.loads(raw)
result = oa_resp_to_responses(chat_resp, model)
self.send_json(200, result) self.send_json(200, result)
rid = result.get("id") rid = result.get("id")
if rid: if rid: