v3.8.1: Freebuff integration + restore all provider presets
- Add Freebuff backend: free DeepSeek V4 Pro, V4 Flash, Kimi K2.6, MiniMax M2.7 - Freebuff backend auto-manages agent run lifecycle (start/finish) - Credential detection from ~/.config/manicode/credentials.json - Model-to-agent routing for freebuff free tier - Restore all provider presets (Command Code, Crof, OpenAdapter, OpenRouter, etc.) - Fix endpoints.json overwritten with only AG X entries - Version bump to 3.8.1 - 54 self-tests passing
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user