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

- Root cause: _handle_freebuff 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 a1920ad88e
commit cc4127f963
5 changed files with 282 additions and 54 deletions

View File

@@ -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: