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:
@@ -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://codebuff.com"
|
||||
_FREEBUFF_AUTH_URL = "https://codebuff.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")
|
||||
_codebuff_token_cache = {"token": None, "checked": 0}
|
||||
_codebuff_session_cache = {"instance_id": None, "expires": 0, "model": None}
|
||||
_codebuff_token_lock = threading.Lock()
|
||||
|
||||
def _get_codebuff_token():
|
||||
@@ -304,7 +307,8 @@ def _get_codebuff_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 _codebuff_token_lock:
|
||||
_codebuff_token_cache["token"] = token
|
||||
_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)
|
||||
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):
|
||||
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 _codebuff_start_run(token, agent_id):
|
||||
return None
|
||||
|
||||
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,
|
||||
"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 codebuff agent run. Check credentials and network."}})
|
||||
|
||||
instance_id = _codebuff_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["codebuff_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}] [codebuff] 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}] [codebuff] client disconnected", file=sys.stderr)
|
||||
return
|
||||
|
||||
success = finish_reason != "length"
|
||||
success = finish_reason[0] != "length"
|
||||
_record_usage("codebuff", 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}] [codebuff] 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}] [codebuff] 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:
|
||||
|
||||
Reference in New Issue
Block a user