v3.9.0: Multi-account rotation for all OAuth providers

This commit is contained in:
2026-05-24 19:29:37 +00:00
Unverified
parent 483691bbe6
commit cc06df59aa

View File

@@ -335,7 +335,7 @@ def _freebuff_get_session(token, model):
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.4", "User-Agent": "codex-launcher/3.9.0",
"x-freebuff-model": model, "x-freebuff-model": model,
}) })
resp = urllib.request.urlopen(req, timeout=15) resp = urllib.request.urlopen(req, timeout=15)
@@ -360,7 +360,7 @@ def _freebuff_start_run(token, agent_id):
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.4", "User-Agent": "codex-launcher/3.9.0",
}) })
try: try:
resp = urllib.request.urlopen(req, timeout=15) resp = urllib.request.urlopen(req, timeout=15)
@@ -383,13 +383,226 @@ def _freebuff_finish_run(token, run_id, status="completed"):
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.4", "User-Agent": "codex-launcher/3.9.0",
}) })
try: try:
urllib.request.urlopen(req, timeout=10) urllib.request.urlopen(req, timeout=10)
except Exception as e: except Exception as e:
print(f"[freebuff] finish run {run_id} error: {e}", file=sys.stderr) print(f"[freebuff] finish run {run_id} error: {e}", file=sys.stderr)
# ═══════════════════════════════════════════════════════════════════
# Multi-account rotation system
# ═══════════════════════════════════════════════════════════════════
class AccountPool:
"""Manages multiple accounts for a provider. Rotates on rate-limit (429/426)."""
def __init__(self, provider_name):
self.provider_name = provider_name
self._lock = threading.Lock()
self._accounts = []
self._rate_limited = {}
self._current_idx = 0
self._loaded_at = 0
def load_accounts(self, force=False):
with self._lock:
if not force and self._accounts and time.time() - self._loaded_at < 60:
return len(self._accounts)
accounts = self._do_load()
with self._lock:
if accounts:
self._accounts = accounts
self._loaded_at = time.time()
for a in accounts:
key = a.get("id", a.get("email", ""))
if key not in self._rate_limited:
self._rate_limited[key] = 0
return len(self._accounts) if accounts else 0
def _do_load(self):
return []
def get(self):
"""Return the best available account dict, or None."""
self.load_accounts()
with self._lock:
if not self._accounts:
return None
now = time.time()
n = len(self._accounts)
for attempt in range(n):
idx = (self._current_idx + attempt) % n
acct = self._accounts[idx]
key = acct.get("id", acct.get("email", ""))
if self._rate_limited.get(key, 0) < now:
self._current_idx = idx
return acct
best_key = min(self._rate_limited, key=self._rate_limited.get)
wait = self._rate_limited[best_key] - now
print(f"[{self.provider_name}] all accounts rate-limited, earliest free in {wait:.0f}s", file=sys.stderr)
return self._accounts[self._current_idx]
def mark_rate_limited(self, account, duration=120):
key = account.get("id", account.get("email", ""))
with self._lock:
self._rate_limited[key] = time.time() + duration
idx = None
for i, a in enumerate(self._accounts):
if a.get("id", a.get("email", "")) == key:
idx = i
break
if idx is not None:
self._current_idx = (idx + 1) % len(self._accounts)
print(f"[{self.provider_name}] account {key} rate-limited for {duration}s, rotating to next", file=sys.stderr)
def advance(self):
with self._lock:
if self._accounts:
self._current_idx = (self._current_idx + 1) % len(self._accounts)
def status(self):
with self._lock:
now = time.time()
result = []
for a in self._accounts:
key = a.get("id", a.get("email", ""))
rl_until = self._rate_limited.get(key, 0)
info = {"id": key, "email": a.get("email", ""), "rate_limited": rl_until > now}
if rl_until > now:
info["rate_limited_until"] = rl_until
info["resets_in"] = int(rl_until - now)
result.append(info)
return result
class FreebuffAccountPool(AccountPool):
def _do_load(self):
if not os.path.exists(_FREEBUFF_CREDS_PATH):
return None
try:
with open(_FREEBUFF_CREDS_PATH) as f:
creds = json.load(f)
except Exception:
return None
accounts = []
if "accounts" in creds and isinstance(creds["accounts"], list):
for i, ac in enumerate(creds["accounts"]):
token = ac.get("authToken") or ac.get("apiKey") or ""
if token:
acct = {"id": ac.get("email") or ac.get("id") or f"account-{i}", "token": token, "email": ac.get("email", "")}
accounts.append(acct)
default = creds.get("default", {})
default_token = default.get("authToken") or creds.get("apiKey") or ""
if default_token:
default_id = default.get("email") or default.get("id") or "default"
if not any(a["id"] == default_id for a in accounts):
accounts.insert(0, {"id": default_id, "token": default_token, "email": default.get("email", "")})
return accounts if accounts else None
class GoogleAccountPool(AccountPool):
def __init__(self, variant):
super().__init__(f"google-{variant}")
self.variant = variant
def _do_load(self):
cache_dir = os.path.join(os.path.expanduser("~"), ".cache", "codex-proxy")
accounts = []
primary = f"google-{self.variant}-oauth-token.json"
primary_path = os.path.join(cache_dir, primary)
if os.path.exists(primary_path):
try:
with open(primary_path) as f:
tok = json.load(f)
token = tok.get("access_token", "")
if token:
accounts.append({"id": f"google-{self.variant}-primary", "token": token, "email": tok.get("email", ""), "_token_data": tok, "_path": primary_path})
except Exception:
pass
idx = 1
while True:
extra = f"google-{self.variant}-oauth-token-{idx}.json"
extra_path = os.path.join(cache_dir, extra)
if not os.path.exists(extra_path):
break
try:
with open(extra_path) as f:
tok = json.load(f)
token = tok.get("access_token", "")
if token:
accounts.append({"id": f"google-{self.variant}-{idx}", "token": token, "email": tok.get("email", ""), "_token_data": tok, "_path": extra_path})
except Exception:
pass
idx += 1
return accounts if accounts else None
class APIKeyPool(AccountPool):
"""Rotates through comma-separated API keys."""
def __init__(self, provider_name, keys_str):
super().__init__(provider_name)
self._raw_keys = [k.strip() for k in keys_str.split(",") if k.strip()]
self._accounts = [{"id": f"key-{i}", "token": k, "email": f"key-{i}"} for i, k in enumerate(self._raw_keys)]
for a in self._accounts:
self._rate_limited[a["id"]] = 0
self._loaded_at = time.time()
def load_accounts(self, force=False):
return len(self._accounts)
_fb_pool = FreebuffAccountPool("freebuff")
_google_antigravity_pool = GoogleAccountPool("antigravity")
_google_cli_pool = GoogleAccountPool("cli")
def _get_freebuff_account():
"""Return (token, account_dict) for best available freebuff account."""
_fb_pool.load_accounts()
acct = _fb_pool.get()
if not acct:
return "", None
return acct["token"], acct
def _get_google_account(oauth_provider):
"""Return (access_token, account_dict) for best available Google account."""
pool = _google_antigravity_pool if oauth_provider == "google-antigravity" else _google_cli_pool
pool.load_accounts()
acct = pool.get()
if not acct:
return None, None
token_data = acct.get("_token_data", {})
token_path = acct.get("_path", "")
if token_data and token_path:
refreshed = _refresh_google_token(token_data, token_path)
return refreshed, acct
return acct.get("token", ""), acct
def _refresh_google_token(token_data, token_path):
if token_data.get("expires_at", 0) > time.time() + 60:
return token_data.get("access_token", "")
client_id = token_data.get("client_id", "")
client_secret = token_data.get("client_secret", "")
refresh_token = token_data.get("refresh_token", "")
if not all([client_id, client_secret, refresh_token]):
return token_data.get("access_token", "")
print("[oauth] refreshing Google access token...", file=sys.stderr)
try:
data = urllib.parse.urlencode({
"client_id": client_id, "client_secret": client_secret,
"refresh_token": refresh_token, "grant_type": "refresh_token",
}).encode()
req = urllib.request.Request("https://oauth2.googleapis.com/token", data=data,
headers={"Content-Type": "application/x-www-form-urlencoded"})
resp = urllib.request.urlopen(req, timeout=30)
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:
json.dump(token_data, f, indent=2)
print("[oauth] token refreshed OK", file=sys.stderr)
return token_data["access_token"]
except Exception as e:
print(f"[oauth] refresh failed: {e}", file=sys.stderr)
return token_data.get("access_token", "")
_LOG_FILE = None _LOG_FILE = None
_LOG_FILE_LOCK = threading.Lock() _LOG_FILE_LOCK = threading.Lock()
@@ -441,6 +654,7 @@ def _ensure_antigravity_version():
def _init_runtime(): def _init_runtime():
global CONFIG, PORT, BACKEND, TARGET_URL, API_KEY, OAUTH_PROVIDER, _antigravity_version global CONFIG, PORT, BACKEND, TARGET_URL, API_KEY, OAUTH_PROVIDER, _antigravity_version
global MODELS, CC_VERSION, REASONING_ENABLED, REASONING_EFFORT, BGP_ROUTES global MODELS, CC_VERSION, REASONING_ENABLED, REASONING_EFFORT, BGP_ROUTES
global _api_key_pool
CONFIG = load_config() CONFIG = load_config()
PORT = CONFIG["port"] PORT = CONFIG["port"]
@@ -453,6 +667,10 @@ def _init_runtime():
REASONING_ENABLED = CONFIG.get("reasoning_enabled", True) REASONING_ENABLED = CONFIG.get("reasoning_enabled", True)
REASONING_EFFORT = CONFIG.get("reasoning_effort", "medium") REASONING_EFFORT = CONFIG.get("reasoning_effort", "medium")
BGP_ROUTES = CONFIG.get("bgp_routes", []) 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 ("freebuff",):
_api_key_pool = APIKeyPool(BACKEND, API_KEY)
print(f"[multi-account] API key pool: {len(_api_key_pool._accounts)} keys for {BACKEND}", file=sys.stderr)
if OAUTH_PROVIDER == "google-antigravity": if OAUTH_PROVIDER == "google-antigravity":
_antigravity_version = _ensure_antigravity_version() _antigravity_version = _ensure_antigravity_version()
print(f"[antigravity] version={_antigravity_version}", file=sys.stderr) print(f"[antigravity] version={_antigravity_version}", file=sys.stderr)
@@ -614,6 +832,10 @@ def _refresh_oauth_token():
def _refresh_oauth_token_for(api_key, oauth_provider): def _refresh_oauth_token_for(api_key, oauth_provider):
oauth_provider = oauth_provider or "" oauth_provider = oauth_provider or ""
if oauth_provider.startswith("google"):
token, acct = _get_google_account(oauth_provider)
if token and acct:
return token
if not oauth_provider.startswith("google"): if not oauth_provider.startswith("google"):
return api_key return api_key
token_name = "google-antigravity-oauth-token.json" if oauth_provider == "google-antigravity" else "google-cli-oauth-token.json" token_name = "google-antigravity-oauth-token.json" if oauth_provider == "google-antigravity" else "google-cli-oauth-token.json"
@@ -3715,9 +3937,25 @@ class Handler(http.server.BaseHTTPRequestHandler):
protocol_version = "HTTP/1.1" protocol_version = "HTTP/1.1"
def do_GET(self): def do_GET(self):
if self.path in ("/v1/models", "/models"): if self.path in ("/v1/models", "/models"):
self.send_json(200, {"object": "list", "data": MODELS}) self.send_json(200, {"object": "list", "data": MODELS})
elif self.path in ("/health", "/v1/health"): elif self.path in ("/v1/accounts", "/accounts"):
info = {"provider": BACKEND, "oauth_provider": OAUTH_PROVIDER}
if BACKEND == "freebuff":
info["accounts"] = _fb_pool.status()
info["total"] = len(_fb_pool._accounts)
elif OAUTH_PROVIDER and OAUTH_PROVIDER.startswith("google"):
pool = _google_antigravity_pool if OAUTH_PROVIDER == "google-antigravity" else _google_cli_pool
info["accounts"] = pool.status()
info["total"] = len(pool._accounts)
elif _api_key_pool:
info["accounts"] = _api_key_pool.status()
info["total"] = len(_api_key_pool._accounts)
else:
info["accounts"] = []
info["total"] = 0
self.send_json(200, info)
elif self.path in ("/health", "/v1/health"):
import resource as _res import resource as _res
_mem_mb = 0 _mem_mb = 0
try: try:
@@ -3732,8 +3970,8 @@ class Handler(http.server.BaseHTTPRequestHandler):
"uptime_s": round(_uptime, 1), "uptime_s": round(_uptime, 1),
"memory_mb": round(_mem_mb, 1), "memory_mb": round(_mem_mb, 1),
"requests_total": _STATS.get("requests", 0)}) "requests_total": _STATS.get("requests", 0)})
else: else:
self.send_error(404) self.send_error(404)
def do_POST(self): def do_POST(self):
if _shutdown_requested: if _shutdown_requested:
@@ -3868,7 +4106,11 @@ class Handler(http.server.BaseHTTPRequestHandler):
else: else:
chat_body = self._build_chat_body(model, messages, body, stream) chat_body = self._build_chat_body(model, messages, body, stream)
target = upstream_target(TARGET_URL, "/chat/completions") target = upstream_target(TARGET_URL, "/chat/completions")
effective_key = _refresh_oauth_token() if _api_key_pool:
pool_acct = _api_key_pool.get()
effective_key = pool_acct["token"] if pool_acct else API_KEY
else:
effective_key = _refresh_oauth_token()
fwd = forwarded_headers(self.headers, { fwd = forwarded_headers(self.headers, {
"Content-Type": "application/json", "Content-Type": "application/json",
"Authorization": f"Bearer {effective_key}", "Authorization": f"Bearer {effective_key}",
@@ -3883,6 +4125,15 @@ class Handler(http.server.BaseHTTPRequestHandler):
except urllib.error.HTTPError as e: except urllib.error.HTTPError as e:
err_body = e.read().decode() err_body = e.read().decode()
if e.code in (429, 502, 503) and attempt < max_retries: if e.code in (429, 502, 503) and attempt < max_retries:
if e.code == 429 and _api_key_pool:
pool_acct = _api_key_pool.get()
if pool_acct:
_api_key_pool.mark_rate_limited(pool_acct, 60)
next_acct = _api_key_pool.get()
if next_acct:
effective_key = next_acct["token"]
fwd["Authorization"] = f"Bearer {effective_key}"
print(f"[multi-account] rotating to key {next_acct['id']}", file=sys.stderr)
retry_after = e.headers.get("Retry-After") retry_after = e.headers.get("Retry-After")
if retry_after: if retry_after:
try: try:
@@ -4153,6 +4404,11 @@ class Handler(http.server.BaseHTTPRequestHandler):
if e.code == 429 and ep != endpoints[-1]: if e.code == 429 and ep != endpoints[-1]:
print(f"[{self._session_id}] {ep} HTTP 429, trying next endpoint", file=sys.stderr) print(f"[{self._session_id}] {ep} HTTP 429, trying next endpoint", file=sys.stderr)
continue continue
if e.code == 429:
pool = _google_antigravity_pool if OAUTH_PROVIDER == "google-antigravity" else _google_cli_pool
_, acct = _get_google_account(OAUTH_PROVIDER)
if acct:
pool.mark_rate_limited(acct, 60)
return self.send_json(e.code, {"error": {"type": "upstream_error", "message": _sanitize_err_body(err_body)}}) return self.send_json(e.code, {"error": {"type": "upstream_error", "message": _sanitize_err_body(err_body)}})
except Exception as e: except Exception as e:
if ep == endpoints[-1]: if ep == endpoints[-1]:
@@ -4750,11 +5006,6 @@ class Handler(http.server.BaseHTTPRequestHandler):
store_response(rid, body.get("input", ""), result.get("output", [])) store_response(rid, body.get("input", ""), result.get("output", []))
def _handle_freebuff(self, body, model, stream, tracker=None): def _handle_freebuff(self, body, model, stream, tracker=None):
token = _get_freebuff_token()
if not token:
return self.send_json(401, {"error": {"type": "auth_error",
"message": "No freebuff credentials found. Install freebuff (npm i -g freebuff) and login first."}})
agent_id = _FREEBUFF_AGENT_MAP.get(model) agent_id = _FREEBUFF_AGENT_MAP.get(model)
if not agent_id: if not agent_id:
matched = None matched = None
@@ -4771,140 +5022,175 @@ class Handler(http.server.BaseHTTPRequestHandler):
print(f"[freebuff] unknown model '{model}', falling back to {fallback_model}", file=sys.stderr) print(f"[freebuff] unknown model '{model}', falling back to {fallback_model}", file=sys.stderr)
model = fallback_model model = fallback_model
run_id = _freebuff_start_run(token, agent_id) _fb_pool.load_accounts()
if not run_id: pool_status = _fb_pool.status()
return self.send_json(502, {"error": {"type": "upstream_error", n_accounts = len(pool_status)
"message": "Failed to start freebuff agent run. Check credentials and network."}}) if n_accounts == 0:
return self.send_json(401, {"error": {"type": "auth_error",
"message": "No freebuff credentials found. Add accounts to ~/.config/manicode/credentials.json"}})
instance_id = _freebuff_get_session(token, model) last_err = None
for attempt in range(n_accounts):
token, acct = _get_freebuff_account()
if not token:
return self.send_json(401, {"error": {"type": "auth_error",
"message": "No freebuff credentials found. All accounts exhausted."}})
input_data = body.get("input", "") acct_id = acct.get("id", "?") if acct else "?"
instructions = body.get("instructions", "").strip() if attempt > 0:
messages = _fb_input_to_messages(input_data, instructions) print(f"[freebuff] rotation attempt {attempt+1}/{n_accounts}, trying account {acct_id}", file=sys.stderr)
messages = _ds_rebuild_tool_history(messages)
metadata = { run_id = _freebuff_start_run(token, agent_id)
"run_id": run_id, if not run_id:
"cost_mode": "free", _fb_pool.mark_rate_limited(acct, 60)
} last_err = ("upstream_error", 502, "Failed to start freebuff agent run. Check credentials and network.")
if instance_id: continue
metadata["freebuff_instance_id"] = instance_id
chat_body = { instance_id = _freebuff_get_session(token, model)
"model": model,
"messages": messages,
"stream": stream,
"max_tokens": max(body.get("max_output_tokens", 0), 64000),
"codebuff_metadata": metadata,
}
for k in ("temperature", "top_p"):
if k in body:
chat_body[k] = body[k]
tools = oa_convert_tools(body.get("tools"))
if tools:
chat_body["tools"] = tools
if body.get("tool_choice"):
chat_body["tool_choice"] = body["tool_choice"]
target = f"{_FREEBUFF_API_URL}/api/v1/chat/completions" input_data = body.get("input", "")
headers = { instructions = body.get("instructions", "").strip()
"Content-Type": "application/json", messages = _fb_input_to_messages(input_data, instructions)
"Authorization": f"Bearer {token}", messages = _ds_rebuild_tool_history(messages)
"User-Agent": "codex-launcher/3.8.4",
"x-freebuff-model": model,
}
if instance_id:
headers["x-freebuff-instance-id"] = instance_id
print(f"[{self._session_id}] [freebuff] POST {target} model={model} stream={stream} run={run_id}", file=sys.stderr) metadata = {
chat_body_b = json.dumps(chat_body).encode() "run_id": run_id,
"cost_mode": "free",
}
if instance_id:
metadata["freebuff_instance_id"] = instance_id
try: chat_body = {
req = urllib.request.Request(target, data=chat_body_b, headers=headers) "model": model,
upstream = urllib.request.urlopen(req, timeout=_upstream_timeout(body, stream)) "messages": messages,
except urllib.error.HTTPError as e: "stream": stream,
err_body = e.read().decode()[:1000] "max_tokens": max(body.get("max_output_tokens", 0), 64000),
_freebuff_finish_run(token, run_id, "failed") "codebuff_metadata": metadata,
if _is_reasoning_content_error(err_body): }
print(f"[freebuff] reasoning_content error, retrying with thinking disabled (DeepSeek native format): {err_body[:200]}", file=sys.stderr) for k in ("temperature", "top_p"):
result = self._fb_retry_thinking_disabled(body, model, token, agent_id, stream, tracker, input_data, instructions, err_body) if k in body:
return result chat_body[k] = body[k]
print(f"[freebuff] HTTP {e.code}: {err_body[:300]}", file=sys.stderr) tools = oa_convert_tools(body.get("tools"))
return self.send_json(e.code, {"error": {"type": "upstream_error", "message": _sanitize_err_body(err_body)}}) if tools:
except Exception as e: chat_body["tools"] = tools
_freebuff_finish_run(token, run_id, "failed") if body.get("tool_choice"):
return self.send_json(502, {"error": {"type": "proxy_error", "message": str(e)}}) chat_body["tool_choice"] = body["tool_choice"]
t0 = time.time() target = f"{_FREEBUFF_API_URL}/api/v1/chat/completions"
try: headers = {
if stream: "Content-Type": "application/json",
self.send_response(200) "Authorization": f"Bearer {token}",
self.send_header("Content-Type", "text/event-stream") "User-Agent": "codex-launcher/3.9.0",
self.send_header("Cache-Control", "no-cache") "x-freebuff-model": model,
self.send_header("Connection", "keep-alive") }
self.end_headers() if instance_id:
if hasattr(self, 'connection') and self.connection: headers["x-freebuff-instance-id"] = instance_id
print(f"[{self._session_id}] [freebuff] POST {target} model={model} stream={stream} run={run_id} acct={acct_id}", file=sys.stderr)
chat_body_b = json.dumps(chat_body).encode()
try:
req = urllib.request.Request(target, data=chat_body_b, headers=headers)
upstream = urllib.request.urlopen(req, timeout=_upstream_timeout(body, stream))
except urllib.error.HTTPError as e:
err_body = e.read().decode()[:1000]
_freebuff_finish_run(token, run_id, "failed")
if e.code in (429, 426):
reset_ms = 0
try: try:
self.connection.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) err_json = json.loads(err_body)
reset_ms = err_json.get("retryAfterMs", 0)
except Exception: except Exception:
pass pass
duration = max(reset_ms / 1000, 120) if reset_ms else 120
_fb_pool.mark_rate_limited(acct, duration)
last_err = ("upstream_error", e.code, _sanitize_err_body(err_body))
print(f"[freebuff] account {acct_id} got HTTP {e.code}, rotating", file=sys.stderr)
continue
if _is_reasoning_content_error(err_body):
print(f"[freebuff] reasoning_content error, retrying with thinking disabled", file=sys.stderr)
result = self._fb_retry_thinking_disabled(body, model, token, agent_id, stream, tracker, input_data, instructions, err_body, acct)
return result
print(f"[freebuff] HTTP {e.code}: {err_body[:300]}", file=sys.stderr)
return self.send_json(e.code, {"error": {"type": "upstream_error", "message": _sanitize_err_body(err_body)}})
except Exception as e:
_freebuff_finish_run(token, run_id, "failed")
return self.send_json(502, {"error": {"type": "proxy_error", "message": str(e)}})
last_resp_id = [None] t0 = time.time()
last_output = [None] try:
last_status = [None] if stream:
finish_reason = [None] self.send_response(200)
reasoning_out = {} self.send_header("Content-Type", "text/event-stream")
self.send_header("Cache-Control", "no-cache")
self.send_header("Connection", "keep-alive")
self.end_headers()
if hasattr(self, 'connection') and self.connection:
try:
self.connection.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
except Exception:
pass
def _on_fb_event(event): last_resp_id = [None]
if tracker and tracker.cancelled.is_set(): last_output = [None]
return False last_status = [None]
for line in event.strip().split("\n"): finish_reason = [None]
if line.startswith("data: "): reasoning_out = {}
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: def _on_fb_event(event):
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"), return False
_reasoning_out=reasoning_out), for line in event.strip().split("\n"):
on_event=_on_fb_event) if line.startswith("data: "):
except (ConnectionResetError, BrokenPipeError, ConnectionAbortedError): try:
print(f"[{self._session_id}] [freebuff] client disconnected", file=sys.stderr) d = json.loads(line[6:])
return 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
success = finish_reason[0] != "length" try:
_record_usage("freebuff", model, success, time.time() - t0) self.stream_buffered_events(
if last_resp_id[0] and input_data is not None: oa_stream_to_sse(upstream, model, body.get("request_id") or body.get("id"),
store_response(last_resp_id[0], input_data, last_output[0]) _reasoning_out=reasoning_out),
if last_resp_id[0] and reasoning_out.get("text") or reasoning_out.get("tool_calls"): on_event=_on_fb_event)
asm = {"role": "assistant", "content": reasoning_out.get("text", "") or ""} except (ConnectionResetError, BrokenPipeError, ConnectionAbortedError):
if reasoning_out.get("tool_calls"): print(f"[{self._session_id}] [freebuff] client disconnected", file=sys.stderr)
asm["tool_calls"] = reasoning_out["tool_calls"] return
if reasoning_out.get("text"):
asm["reasoning_content"] = reasoning_out["text"]
_ds_store_assistant(last_resp_id[0], asm)
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()
chat_resp = json.loads(raw)
result = oa_resp_to_responses(chat_resp, model)
self.send_json(200, result)
rid = result.get("id")
if rid:
store_response(rid, input_data, result.get("output", []))
print(f"[{self._session_id}] [freebuff] non-stream done in {time.time()-t0:.1f}s", file=sys.stderr)
finally:
_freebuff_finish_run(token, run_id, "completed")
def _fb_retry_thinking_disabled(self, body, model, token, agent_id, stream, tracker, input_data, instructions, original_error): success = finish_reason[0] != "length"
_record_usage("freebuff", model, success, time.time() - t0)
if last_resp_id[0] and input_data is not None:
store_response(last_resp_id[0], input_data, last_output[0])
if last_resp_id[0] and reasoning_out.get("text") or reasoning_out.get("tool_calls"):
asm = {"role": "assistant", "content": reasoning_out.get("text", "") or ""}
if reasoning_out.get("tool_calls"):
asm["tool_calls"] = reasoning_out["tool_calls"]
if reasoning_out.get("text"):
asm["reasoning_content"] = reasoning_out["text"]
_ds_store_assistant(last_resp_id[0], asm)
print(f"[{self._session_id}] [freebuff] stream done status={last_status[0]} in {time.time()-t0:.1f}s acct={acct_id}", file=sys.stderr)
else:
raw = upstream.read().decode()
chat_resp = json.loads(raw)
result = oa_resp_to_responses(chat_resp, model)
self.send_json(200, result)
rid = result.get("id")
if rid:
store_response(rid, input_data, result.get("output", []))
print(f"[{self._session_id}] [freebuff] non-stream done in {time.time()-t0:.1f}s acct={acct_id}", file=sys.stderr)
finally:
_freebuff_finish_run(token, run_id, "completed")
return
if last_err:
return self.send_json(last_err[1], {"error": {"type": last_err[0], "message": f"All {n_accounts} accounts exhausted. {last_err[2]}"}})
def _fb_retry_thinking_disabled(self, body, model, token, agent_id, stream, tracker, input_data, instructions, original_error, acct=None):
run_id = _freebuff_start_run(token, agent_id) run_id = _freebuff_start_run(token, agent_id)
if not run_id: if not run_id:
return self.send_json(502, {"error": {"type": "upstream_error", return self.send_json(502, {"error": {"type": "upstream_error",
@@ -4930,7 +5216,7 @@ 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_API_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.4", "x-freebuff-model": model} headers = {"Content-Type": "application/json", "Authorization": f"Bearer {token}", "User-Agent": "codex-launcher/3.9.0", "x-freebuff-model": model}
if instance_id: if instance_id:
headers["x-freebuff-instance-id"] = instance_id headers["x-freebuff-instance-id"] = instance_id
print(f"[freebuff] retry POST {target} model={model} stream={stream} run={run_id} (thinking disabled via DeepSeek native)", file=sys.stderr) print(f"[freebuff] retry POST {target} model={model} stream={stream} run={run_id} (thinking disabled via DeepSeek native)", file=sys.stderr)
@@ -5352,6 +5638,17 @@ def main():
print(f"translate-proxy ({BACKEND}) listening on http://127.0.0.1:{PORT}", flush=True) print(f"translate-proxy ({BACKEND}) listening on http://127.0.0.1:{PORT}", flush=True)
print(f"Target: {TARGET_URL}", flush=True) print(f"Target: {TARGET_URL}", flush=True)
print(f"Models: {[m['id'] for m in MODELS]}", flush=True) print(f"Models: {[m['id'] for m in MODELS]}", flush=True)
if BACKEND == "freebuff":
_fb_pool.load_accounts(force=True)
fb_status = _fb_pool.status()
print(f"[multi-account] freebuff: {len(fb_status)} accounts loaded {[a['id'] for a in fb_status]}", flush=True)
if OAUTH_PROVIDER and OAUTH_PROVIDER.startswith("google"):
pool = _google_antigravity_pool if OAUTH_PROVIDER == "google-antigravity" else _google_cli_pool
pool.load_accounts(force=True)
g_status = pool.status()
print(f"[multi-account] {OAUTH_PROVIDER}: {len(g_status)} accounts loaded {[a['id'] for a in g_status]}", flush=True)
if _api_key_pool:
print(f"[multi-account] API keys: {len(_api_key_pool._accounts)} keys loaded", flush=True)
if BGP_ROUTES: if BGP_ROUTES:
print(f"BGP routes: {len(BGP_ROUTES)} ({[r.get('name','?') for r in BGP_ROUTES]})", flush=True) print(f"BGP routes: {len(BGP_ROUTES)} ({[r.get('name','?') for r in BGP_ROUTES]})", flush=True)
try: try: