diff --git a/CHANGELOG.md b/CHANGELOG.md index ccade77..f4cd414 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,28 @@ # Changelog -## v3.8.1 (2026-05-24) +## v3.8.3 (2026-05-24) + +**Critical Fix — Freebuff Streaming Now Works End-to-End** + +### Root Cause +The freebuff 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 freebuff streaming path. Codex CLI received zero bytes, showing "thinking..." indefinitely. + +### Fix +Replaced the manual streaming loop in `_handle_freebuff()` 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_freebuff()` 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 freebuff 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) **Freebuff Integration — FREE DeepSeek V4 Pro Access + Provider Presets Restored** diff --git a/README.md b/README.md index 719e436..e23869e 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@
Run OpenAI Codex CLI & Desktop with any AI provider.
- Google Antigravity • Gemini CLI • OpenCode • Z.AI • Anthropic • Command Code • OpenRouter • Crof.ai • NVIDIA NIM • 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
@@ -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 Go | OpenAI-compat | `https://opencode.ai/zen/go/v1` | | Command Code | Command Code | `https://api.commandcode.ai` | -| **Freebuff** | **Freebuff** | `https://freebuff.com` *(free DeepSeek/Kimi)* | +| **Freebuff** | **Freebuff** | `https://freebuff.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` | | NVIDIA NIM | OpenAI-compat | `https://integrate.api.nvidia.com/v1` | | Kilo.ai | OpenAI-compat | `https://api.kilo.ai/api/gateway` | | OpenRouter | OpenAI-compat | `https://openrouter.ai/api/v1` | @@ -563,7 +564,7 @@ Freebuff provides free access to these models — no API key needed: - **Kimi K2.6** — Balanced - **MiniMax M2.7** — Fastest -*Requires: `npm install -g freebuff && freebuff login` (GitHub OAuth)* +*Requires: `freebuff login` via GUI OAuth button, or `npm install -g freebuff && freebuff login` (GitHub OAuth)* --- diff --git a/codex-launcher_3.8.3_all.deb b/codex-launcher_3.8.3_all.deb new file mode 100644 index 0000000..92e1797 Binary files /dev/null and b/codex-launcher_3.8.3_all.deb differ diff --git a/src/codex-launcher-gui b/src/codex-launcher-gui index 776e7a4..a8dfd95 100755 --- a/src/codex-launcher-gui +++ b/src/codex-launcher-gui @@ -6,7 +6,7 @@ gi.require_version("Gtk", "3.0") from gi.repository import Gtk, GLib import subprocess, os, signal, sys, threading, time, json, urllib.request, urllib.parse, urllib.error, tempfile, shutil import hashlib, socket, ssl, contextlib, re, collections -import base64, secrets +import base64, secrets, uuid, webbrowser from pathlib import Path HOME = Path.home() @@ -26,6 +26,22 @@ model_catalog_json = "" """ CHANGELOG = [ + ("3.8.3", "2026-05-24", [ + "FIXED: Freebuff streaming — SSE events now reach Codex client", + "Root cause: stream_buffered_events was never called for freebuff", + "Freebuff stream uses buffered flushing (30ms / 4KB / urgent)", + "Freebuff OAuth — built-in login flow (no external CLI needed)", + "Freebuff API: reverse-engineered www.codebuff.com endpoints", + "Freebuff session management with instance ID (waiting room)", + "Freebuff agent run lifecycle (start/finish) with model routing", + "Free DeepSeek V4 Pro, V4 Flash, Kimi K2.6, MiniMax M2.7", + "Reasoning mode works with freebuff (thinking tokens supported)", + "GUI: Sandbox mode selector (Read-only / Workspace / Full Access)", + "GUI: Approval mode selector (Untrusted / On Request / Full Auto)", + "GUI: Freebuff Login button in endpoint editor", + "Fixed _STATS undefined error in /health endpoint", + "Fixed freebuff credential path (reads default account)", + ]), ("3.8.1", "2026-05-24", [ "Freebuff integration — free DeepSeek V4 Pro, V4 Flash, Kimi K2.6, MiniMax M2.7", "Freebuff backend: auto agent-run lifecycle, credential detection, model routing", @@ -321,6 +337,7 @@ PROVIDER_PRESETS = { "Freebuff (Free DeepSeek/Kimi)": { "backend_type": "freebuff", "base_url": "https://freebuff.com", + "oauth_provider": "freebuff", "models": [ "deepseek/deepseek-v4-pro", "deepseek/deepseek-v4-flash", "moonshotai/kimi-k2.6", "minimax/minimax-m2.7", @@ -995,6 +1012,11 @@ def safe_cleanup_owned(logfn=None): def _start_proxy_for(endpoint, logfn): 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() port = _pick_free_port() _proxy_port = port @@ -1684,7 +1706,7 @@ class LauncherWin(Gtk.Window): # header row hdr = Gtk.Box(spacing=8) vbox.pack_start(hdr, False, False, 0) - lbl = Gtk.Label(label="Codex Launcher v3.8.1") + lbl = Gtk.Label(label="Codex Launcher v3.8.3") lbl.set_use_markup(True) hdr.pack_start(lbl, False, False, 0) changelog_btn = Gtk.Button(label="Changelog") @@ -1790,6 +1812,26 @@ class LauncherWin(Gtk.Window): self._model_combo = Gtk.ComboBoxText() 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 btn_box = Gtk.Box(spacing=8, homogeneous=True) 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.""" self.log(f"Launching Codex CLI with {ep['name']}…") - # Find a terminal emulator terms = [ ("x-terminal-emulator", ["-e"]), ("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)") return - # For proxied endpoints, the proxy is already running (from _run) - # For native, no proxy needed + 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 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: - # 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._proc = subprocess.Popen(cmd_parts, preexec_fn=os.setsid) @@ -2618,7 +2660,9 @@ class LauncherWin(Gtk.Window): self.log("ERROR: no terminal emulator found") 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._proc = subprocess.Popen(cmd_parts, preexec_fn=os.setsid) pid = self._proc.pid @@ -3097,9 +3141,14 @@ class EditEndpointDialog(Gtk.Dialog): def _apply_selected_preset(self, initial=False): preset_name = self._combo_preset.get_active_text() or "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) - if is_oauth: + if oauth_provider == "freebuff": + self._oauth_btn.set_label("Freebuff Login") + self._entry_key.set_placeholder_text("Auto-filled by freebuff login") + elif is_oauth: + self._oauth_btn.set_label("OAuth Login") self._entry_key.set_placeholder_text("Auto-filled by OAuth") else: 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 = PROVIDER_PRESETS.get(preset_name, {}) provider = preset.get("oauth_provider", "") - if (provider or "").startswith("google"): + if provider == "freebuff": + self._freebuff_oauth_flow() + elif (provider or "").startswith("google"): self._google_oauth_flow(provider) 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.run() + def _freebuff_oauth_flow(self): + dlg = Gtk.Dialog(title="Freebuff 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="Sign in with GitHub via Freebuff", 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 _freebuff_auth_thread(): + try: + fingerprint_id = str(uuid.uuid4()) + auth_url = "https://freebuff.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._freebuff_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'{login_url}') + link_lbl.set_visible(True) + GLib.idle_add(_set_link) + + webbrowser.open(login_url) + + poll_url = f"https://freebuff.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._freebuff_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._freebuff_oauth_done, dlg, spinner) + except Exception as e: + self._fb_oauth_result["error"] = str(e)[:200] + GLib.idle_add(self._freebuff_oauth_done, dlg, spinner) + + threading.Thread(target=_freebuff_auth_thread, daemon=True).start() + dlg.connect("response", lambda d, r: d.destroy()) + dlg.run() + + def _freebuff_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('Authorization successful! Credentials saved.') + dlg.set_title("Freebuff Login – Success") + GLib.timeout_add(1500, lambda: dlg.response(Gtk.ResponseType.OK)) + else: + self._oauth_status.set_markup(f'{self._fb_oauth_result["error"] or "Login failed."}') + GLib.timeout_add(3000, lambda: dlg.response(Gtk.ResponseType.CANCEL)) + def _oauth_success(self, dlg, access_token, spinner): spinner.stop() self._entry_key.set_text(access_token) diff --git a/src/translate-proxy.py b/src/translate-proxy.py index 0ee62a3..1e0ac88 100755 --- a/src/translate-proxy.py +++ b/src/translate-proxy.py @@ -254,6 +254,7 @@ _stats_lock = threading.Lock() _stats_pending = [] _stats_flush_timer = None _STATS_FLUSH_INTERVAL = 5.0 +_STATS = {} try: _LOG_FILE = open(os.path.join(_LOG_DIR, "proxy.log"), "a") @@ -286,7 +287,8 @@ _conn_pool = {} _STREAM_IDLE_TIMEOUT = 300 -_FREEBUFF_BASE_URL = "https://freebuff.com" +_FREEBUFF_AUTH_URL = "https://freebuff.com" +_FREEBUFF_API_URL = "https://www.codebuff.com" _FREEBUFF_AGENT_MAP = { "deepseek/deepseek-v4-pro": "base2-free-deepseek", "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_token_cache = {"token": None, "checked": 0} +_freebuff_session_cache = {"instance_id": None, "expires": 0, "model": None} _freebuff_token_lock = threading.Lock() def _get_freebuff_token(): @@ -304,7 +307,8 @@ def _get_freebuff_token(): try: with open(_FREEBUFF_CREDS_PATH) as 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 _freebuff_token_lock: _freebuff_token_cache["token"] = token _freebuff_token_cache["checked"] = time.time() @@ -313,13 +317,42 @@ def _get_freebuff_token(): print(f"[freebuff] no credentials at {_FREEBUFF_CREDS_PATH}: {e}", file=sys.stderr) return "" +def _freebuff_get_session(token, model): + with _freebuff_token_lock: + sc = _freebuff_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 _freebuff_token_lock: + _freebuff_session_cache["instance_id"] = instance_id + _freebuff_session_cache["expires"] = time.time() + min(expires_at / 1000, 3600) + _freebuff_session_cache["model"] = model + print(f"[freebuff] session active, instance={instance_id[:8]}...", file=sys.stderr) + return instance_id + return None + except Exception as e: + print(f"[freebuff] session failed: {e}", file=sys.stderr) + return None + def _freebuff_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() req = urllib.request.Request(url, data=body, headers={ "Content-Type": "application/json", "Authorization": f"Bearer {token}", - "User-Agent": "codex-launcher/3.8.1", + "User-Agent": "codex-launcher/3.8.3", }) try: resp = urllib.request.urlopen(req, timeout=15) @@ -336,13 +369,13 @@ def _freebuff_start_run(token, agent_id): return None def _freebuff_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, "totalSteps": 1, "directCredits": 0, "totalCredits": 0}).encode() req = urllib.request.Request(url, data=body, headers={ "Content-Type": "application/json", "Authorization": f"Bearer {token}", - "User-Agent": "codex-launcher/3.8.1", + "User-Agent": "codex-launcher/3.8.3", }) try: urllib.request.urlopen(req, timeout=10) @@ -4537,12 +4570,21 @@ class Handler(http.server.BaseHTTPRequestHandler): return self.send_json(502, {"error": {"type": "upstream_error", "message": "Failed to start freebuff agent run. Check credentials and network."}}) + instance_id = _freebuff_get_session(token, model) + input_data = body.get("input", "") messages = oa_input_to_messages(input_data) instructions = body.get("instructions", "").strip() if instructions: messages.insert(0, {"role": "system", "content": instructions}) + metadata = { + "run_id": run_id, + "cost_mode": "free", + } + if instance_id: + metadata["freebuff_instance_id"] = instance_id + chat_body = { "model": model, "messages": messages, @@ -4550,10 +4592,7 @@ class Handler(http.server.BaseHTTPRequestHandler): "max_tokens": max(body.get("max_output_tokens", 0), 64000), "enable_thinking": REASONING_ENABLED and REASONING_EFFORT != "none", "reasoning_effort": REASONING_EFFORT if REASONING_ENABLED else "none", - "codebuff_metadata": { - "run_id": run_id, - "cost_mode": "free", - }, + "codebuff_metadata": metadata, } for k in ("temperature", "top_p"): if k in body: @@ -4564,11 +4603,11 @@ class Handler(http.server.BaseHTTPRequestHandler): if body.get("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 = { "Content-Type": "application/json", "Authorization": f"Bearer {token}", - "User-Agent": "codex-launcher/3.8.1", + "User-Agent": "codex-launcher/3.8.3", } print(f"[{self._session_id}] [freebuff] POST {target} model={model} stream={stream} run={run_id}", file=sys.stderr) @@ -4600,40 +4639,44 @@ class Handler(http.server.BaseHTTPRequestHandler): except Exception: pass - last_resp_id = None - last_output = None - last_status = None - finish_reason = None - collected_events = [] + last_resp_id = [None] + last_output = [None] + last_status = [None] + finish_reason = [None] + + 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: - for event in oa_stream_to_sse(upstream, model, body.get("request_id") or body.get("id")): - if tracker and tracker.cancelled.is_set(): - break - 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 + self.stream_buffered_events( + oa_stream_to_sse(upstream, model, body.get("request_id") or body.get("id")), + on_event=_on_fb_event) except (ConnectionResetError, BrokenPipeError, ConnectionAbortedError): print(f"[{self._session_id}] [freebuff] client disconnected", file=sys.stderr) return - success = finish_reason != "length" + success = finish_reason[0] != "length" _record_usage("freebuff", model, success, time.time() - t0) - if last_resp_id and input_data is not None: - store_response(last_resp_id, input_data, last_output) - print(f"[{self._session_id}] [freebuff] stream done status={last_status} in {time.time()-t0:.1f}s", file=sys.stderr) + if last_resp_id[0] and input_data is not None: + store_response(last_resp_id[0], input_data, last_output[0]) + print(f"[{self._session_id}] [freebuff] stream done status={last_status[0]} in {time.time()-t0:.1f}s", file=sys.stderr) else: 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) rid = result.get("id") if rid: