diff --git a/src/codex-launcher-gui b/src/codex-launcher-gui
index 1a30076..776e7a4 100755
--- a/src/codex-launcher-gui
+++ b/src/codex-launcher-gui
@@ -26,11 +26,14 @@ model_catalog_json = ""
"""
CHANGELOG = [
- ("3.8.0", "2026-05-22", [
+ ("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",
+ "Restored all provider presets (Command Code, Crof, OpenAdapter, OpenRouter, etc.)",
"AI Monitoring — self-healing watchdog with 3-tier response system",
"HealthWatcher: monitors proxy health every 5s, auto-restarts on crash",
"LogAnalyzer: tails debug logs for 18 failure signal patterns",
- "Tier 1: 14 rule-based auto-recovery rules (< 1s response)",
+ "Tier 1: 14 rule-based auto-recovery rules (< 1 s response)",
"Tier 2: Incident pattern store with success rate tracking",
"Tier 3: AI diagnostic agent — configurable provider/model for novel failures",
"30 fault types catalogued across 5 categories (A-E)",
@@ -315,6 +318,14 @@ PROVIDER_PRESETS = {
"GLM-4-Flash", "GLM-4-FlashX", "GLM-Z1-Flash",
],
},
+ "Freebuff (Free DeepSeek/Kimi)": {
+ "backend_type": "freebuff",
+ "base_url": "https://freebuff.com",
+ "models": [
+ "deepseek/deepseek-v4-pro", "deepseek/deepseek-v4-flash",
+ "moonshotai/kimi-k2.6", "minimax/minimax-m2.7",
+ ],
+ },
}
def safe_name(name):
@@ -327,6 +338,7 @@ def label_for_backend(backend_type):
"openai-compat": "OpenAI-compatible",
"anthropic": "Anthropic",
"command-code": "Command Code",
+ "freebuff": "Freebuff (Free AI)",
"native": "Native",
}.get(backend_type, backend_type)
@@ -1672,7 +1684,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.0")
+ lbl = Gtk.Label(label="Codex Launcher v3.8.1")
lbl.set_use_markup(True)
hdr.pack_start(lbl, False, False, 0)
changelog_btn = Gtk.Button(label="Changelog")
@@ -2923,6 +2935,7 @@ class EditEndpointDialog(Gtk.Dialog):
for val, lab in [("openai-compat", "OpenAI-compatible (needs proxy)"),
("anthropic", "Anthropic (needs proxy)"),
("command-code", "Command Code (needs proxy)"),
+ ("freebuff", "Freebuff - Free DeepSeek/Kimi (needs proxy)"),
("gemini-oauth-cli", "Gemini CLI OAuth (needs proxy)"),
("gemini-oauth-antigravity", "Antigravity OAuth (needs proxy)"),
("native", "Native OpenAI (no proxy)")]:
diff --git a/src/translate-proxy.py b/src/translate-proxy.py
index 335af93..0ee62a3 100755
--- a/src/translate-proxy.py
+++ b/src/translate-proxy.py
@@ -172,6 +172,12 @@ DEFAULT_MODELS = {
"anthropic": [
{"id": "claude-sonnet-4-20250514", "object": "model", "created": 1700000000, "owned_by": "anthropic"},
],
+ "freebuff": [
+ {"id": "deepseek/deepseek-v4-pro", "object": "model", "created": 1700000000, "owned_by": "freebuff"},
+ {"id": "deepseek/deepseek-v4-flash", "object": "model", "created": 1700000000, "owned_by": "freebuff"},
+ {"id": "moonshotai/kimi-k2.6", "object": "model", "created": 1700000000, "owned_by": "freebuff"},
+ {"id": "minimax/minimax-m2.7", "object": "model", "created": 1700000000, "owned_by": "freebuff"},
+ ],
"auto": [
{"id": "default-model", "object": "model", "created": 1700000000, "owned_by": "auto"},
],
@@ -181,7 +187,7 @@ def load_config():
p = argparse.ArgumentParser(description="Responses API translation proxy")
p.add_argument("--config", help="JSON config file path")
p.add_argument("--port", type=int, default=None)
- p.add_argument("--backend", default=None, choices=["openai-compat", "anthropic", "command-code", "auto"])
+ p.add_argument("--backend", default=None, choices=["openai-compat", "anthropic", "command-code", "freebuff", "auto"])
p.add_argument("--target-url", default=None)
p.add_argument("--api-key", default=None)
p.add_argument("--models-file", default=None, help="JSON file with model list array")
@@ -280,6 +286,69 @@ _conn_pool = {}
_STREAM_IDLE_TIMEOUT = 300
+_FREEBUFF_BASE_URL = "https://freebuff.com"
+_FREEBUFF_AGENT_MAP = {
+ "deepseek/deepseek-v4-pro": "base2-free-deepseek",
+ "deepseek/deepseek-v4-flash": "base2-free-deepseek-flash",
+ "moonshotai/kimi-k2.6": "base2-free-kimi",
+ "minimax/minimax-m2.7": "base2-free",
+}
+_FREEBUFF_CREDS_PATH = os.path.join(os.path.expanduser("~"), ".config", "manicode", "credentials.json")
+_freebuff_token_cache = {"token": None, "checked": 0}
+_freebuff_token_lock = threading.Lock()
+
+def _get_freebuff_token():
+ with _freebuff_token_lock:
+ if _freebuff_token_cache["token"] and _freebuff_token_cache["checked"] > time.time() - 300:
+ return _freebuff_token_cache["token"]
+ try:
+ with open(_FREEBUFF_CREDS_PATH) as f:
+ creds = json.load(f)
+ token = creds.get("authToken") or creds.get("apiKey") or ""
+ with _freebuff_token_lock:
+ _freebuff_token_cache["token"] = token
+ _freebuff_token_cache["checked"] = time.time()
+ return token
+ except Exception as e:
+ print(f"[freebuff] no credentials at {_FREEBUFF_CREDS_PATH}: {e}", file=sys.stderr)
+ return ""
+
+def _freebuff_start_run(token, agent_id):
+ url = f"{_FREEBUFF_BASE_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",
+ })
+ try:
+ resp = urllib.request.urlopen(req, timeout=15)
+ data = json.loads(resp.read())
+ run_id = data.get("runId")
+ print(f"[freebuff] started run {run_id} for agent {agent_id}", file=sys.stderr)
+ return run_id
+ except urllib.error.HTTPError as e:
+ err = e.read().decode()[:300]
+ print(f"[freebuff] start run failed: HTTP {e.code}: {err}", file=sys.stderr)
+ return None
+ except Exception as e:
+ print(f"[freebuff] start run error: {e}", file=sys.stderr)
+ return None
+
+def _freebuff_finish_run(token, run_id, status="completed"):
+ url = f"{_FREEBUFF_BASE_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",
+ })
+ try:
+ urllib.request.urlopen(req, timeout=10)
+ except Exception as e:
+ print(f"[freebuff] finish run {run_id} error: {e}", file=sys.stderr)
+
_LOG_FILE = None
_LOG_FILE_LOCK = threading.Lock()
@@ -3507,6 +3576,8 @@ class Handler(http.server.BaseHTTPRequestHandler):
self._handle_anthropic(body, model, stream, tracker)
elif BACKEND == "command-code":
self._handle_command_code(body, model, stream, tracker)
+ elif BACKEND == "freebuff":
+ self._handle_freebuff(body, model, stream, tracker)
elif (BACKEND or "").startswith("gemini-oauth"):
self._handle_gemini_oauth(body, model, stream, tracker)
else:
@@ -4439,6 +4510,138 @@ class Handler(http.server.BaseHTTPRequestHandler):
if rid:
store_response(rid, body.get("input", ""), result.get("output", []))
+ 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)
+ if not agent_id:
+ matched = None
+ for m in _FREEBUFF_AGENT_MAP:
+ if model.lower().replace("/", "").replace("-", "") in m.lower().replace("/", "").replace("-", ""):
+ matched = m
+ break
+ if matched:
+ agent_id = _FREEBUFF_AGENT_MAP[matched]
+ model = matched
+ else:
+ fallback_model = "deepseek/deepseek-v4-flash"
+ agent_id = _FREEBUFF_AGENT_MAP.get(fallback_model, "base2-free-deepseek-flash")
+ print(f"[freebuff] unknown model '{model}', falling back to {fallback_model}", file=sys.stderr)
+ model = fallback_model
+
+ run_id = _freebuff_start_run(token, agent_id)
+ if not run_id:
+ return self.send_json(502, {"error": {"type": "upstream_error",
+ "message": "Failed to start freebuff agent run. Check credentials and network."}})
+
+ 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})
+
+ chat_body = {
+ "model": model,
+ "messages": messages,
+ "stream": stream,
+ "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",
+ },
+ }
+ 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_BASE_URL}/api/v1/chat/completions"
+ headers = {
+ "Content-Type": "application/json",
+ "Authorization": f"Bearer {token}",
+ "User-Agent": "codex-launcher/3.8.1",
+ }
+
+ print(f"[{self._session_id}] [freebuff] POST {target} model={model} stream={stream} run={run_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()[:500]
+ _freebuff_finish_run(token, run_id, "failed")
+ print(f"[freebuff] HTTP {e.code}: {err_body}", 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)}})
+
+ t0 = time.time()
+ try:
+ if stream:
+ self.send_response(200)
+ 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
+
+ last_resp_id = None
+ last_output = None
+ last_status = None
+ finish_reason = None
+ collected_events = []
+
+ 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
+ except (ConnectionResetError, BrokenPipeError, ConnectionAbortedError):
+ print(f"[{self._session_id}] [freebuff] client disconnected", file=sys.stderr)
+ return
+
+ success = finish_reason != "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)
+ else:
+ raw = upstream.read().decode()
+ result = oa_chat_to_responses(raw, 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 _handle_auto(self, body, model, stream, tracker=None):
"""Auto-sensing backend: probe schema, adapt, retry on errors.
Uses hostname heuristics as initial guess, then learns from errors