Minecraft-Friends-AI-ZeroGPU / naldo_opencode_bridge.py
Erinaldordso's picture
Harden embedding startup and AltoClef OpenCode runtime
46e3de3 verified
Raw History Blame Contribute Delete
13.7 kB
"""OpenAI-compatible bridge for Naldo_Bot backed exclusively by OpenCode.
Listens on 127.0.0.1 only. Translates POST /v1/chat/completions into the real
OpenCode session API discovered via /doc:
- POST /session -> create session (body may carry title)
- POST /session/{sessionID}/message -> send prompt, wait for reply
- GET /global/health -> opencode health
Each inference creates a fresh session (isolated per Voyager call) and sends
``tools: {}`` on the message. OpenCode 1.18.33 was verified to return no tool
parts and perform no requested bash side effect in this mode. Custom deny-all
session permissions are not used because Zen free-tier rejects them with 403.
Only assistant ``text`` parts are returned.
Temperature: OpenCode session API exposes no temperature field, so Voyager's
requested temperature is recorded and logged, never forwarded. See
naldo_opencode.TEMPERATURE_NOTE.
Embeddings (SkillManager/QA) are NOT served here; /v1/embeddings proxies to
the existing local 7860 endpoint to preserve upstream behavior.
No OPENCODE_API_KEY is used. No credential file is read or copied. Logs never
include prompts, completions, or credentials.
"""
from __future__ import annotations
import json
import os
import threading
import time
import urllib.error
import urllib.request
import uuid
from typing import Any
from fastapi import FastAPI, HTTPException, Request
from fastapi.responses import JSONResponse, StreamingResponse
from starlette.concurrency import run_in_threadpool
from naldo_opencode import (
ALLOWED_MODELS,
BRIDGE_PORT,
FALLBACK_MODELS,
LOCAL_EMBED_BASE,
OPENCODE_BASE,
OPENCODE_PROVIDER,
OPENCODE_SESSION_DIR,
PRIMARY_MODEL,
TEMPERATURE_NOTE,
build_opencode_prompt,
canonical_model_id,
extract_assistant_text,
is_allowed_model,
opencode_message_body,
)
OPENCODE = OPENCODE_BASE.rstrip("/")
EMBED_BASE = LOCAL_EMBED_BASE.rstrip("/")
PORT = int(os.environ.get("NALDO_BRIDGE_PORT", str(BRIDGE_PORT)))
REQUEST_TIMEOUT = float(os.environ.get("NALDO_BRIDGE_TIMEOUT", "240"))
SESSION_TIMEOUT = float(os.environ.get("NALDO_BRIDGE_SESSION_TIMEOUT", "15"))
EMBED_RETRIES = max(1, int(os.environ.get("NALDO_EMBED_RETRIES", "8")))
EMBED_BACKOFF = max(0.1, float(os.environ.get("NALDO_EMBED_BACKOFF", "1")))
EMBED_RETRYABLE = frozenset({429, 500, 502, 503, 504})
STATE_LOCK = threading.Lock()
state: dict[str, Any] = {
"calls": 0,
"failures": 0,
"fallbacks_used": 0,
"last_model": None,
"last_fallback": None,
"last_elapsed_s": None,
"prompt_tokens": 0,
"completion_tokens": 0,
"last_error": None,
"last_requested_temperature": None,
"last_requested_max_tokens": None,
"effective_temperature": TEMPERATURE_NOTE,
"opencode_base": OPENCODE,
}
def _http_json(
url: str,
method: str = "GET",
body: dict[str, Any] | None = None,
timeout: float = 10.0,
retries: int = 2,
) -> Any:
last: Exception | None = None
for attempt in range(retries + 1):
try:
data = json.dumps(body).encode("utf-8") if body is not None else None
request = urllib.request.Request(url, data=data, method=method)
if data is not None:
request.add_header("Content-Type", "application/json")
with urllib.request.urlopen(request, timeout=timeout) as response:
raw = response.read().decode("utf-8", errors="replace")
return json.loads(raw) if raw else {}
except Exception as exc: # noqa: BLE001 - mapped to HTTP below
last = exc
if attempt < retries:
time.sleep(min(2.0 * (attempt + 1), 5.0))
raise RuntimeError(f"{type(last).__name__}: {str(last)[:200]}" if last else "request failed")
def _create_session(model_id: str, timeout: float = SESSION_TIMEOUT) -> str:
model = canonical_model_id(model_id)
if model not in ALLOWED_MODELS:
raise RuntimeError(f"refusing non-OpenCode/free session model: {model_id}")
OPENCODE_SESSION_DIR.mkdir(parents=True, exist_ok=True)
session_body = {
"title": "naldo-voyager",
"model": {"id": model, "providerID": OPENCODE_PROVIDER},
"location": {"directory": str(OPENCODE_SESSION_DIR)},
}
payload = _http_json(
OPENCODE + "/session", method="POST", body=session_body, timeout=timeout, retries=0
)
session_id = ""
if isinstance(payload, dict):
session_id = str(payload.get("id") or payload.get("sessionID") or payload.get("session_id") or "")
info = payload.get("info")
if not session_id and isinstance(info, dict):
session_id = str(info.get("id") or "")
if not session_id:
raise RuntimeError("OpenCode session creation returned no id")
return session_id
def _send_message(session_id: str, model_id: str, system: str, prompt: str) -> str:
body = opencode_message_body(model_id, system, prompt)
url = OPENCODE + f"/session/{session_id}/message"
try:
# Never retry the same generation POST: a loopback timeout can mean the
# first request is still running, and retrying would duplicate the turn.
result = _http_json(url, method="POST", body=body, timeout=REQUEST_TIMEOUT, retries=0)
finally:
try:
_http_json(OPENCODE + f"/session/{session_id}", method="DELETE", timeout=5.0)
except Exception:
pass
text = extract_assistant_text(result)
if not text:
raise RuntimeError("OpenCode returned no assistant text parts")
return text
def _candidate_models(requested: str) -> list[str]:
ordered: list[str] = []
bare_requested = canonical_model_id(requested)
if bare_requested in ALLOWED_MODELS:
ordered.append(bare_requested)
for model in FALLBACK_MODELS:
if model not in ordered:
ordered.append(model)
return ordered
def _chat_payload(body: dict[str, Any]) -> dict[str, Any]:
requested_model = str(body.get("model") or PRIMARY_MODEL)
if not is_allowed_model(requested_model):
raise ValueError(f"Unknown model for Naldo OpenCode bridge: {requested_model}")
requested_temp = body.get("temperature")
requested_max_tokens = body.get("max_tokens", body.get("max_completion_tokens"))
with STATE_LOCK:
state["last_requested_temperature"] = requested_temp
state["last_requested_max_tokens"] = requested_max_tokens
state["effective_temperature"] = TEMPERATURE_NOTE
if isinstance(requested_temp, (int, float)) and float(requested_temp) != 0.0:
# Voyager normally sends temperature 0; OpenCode session API has no
# temperature parameter, so record drift without inventing support.
print(
f"[naldo-bridge] requested temperature={requested_temp}; {TEMPERATURE_NOTE}",
flush=True,
)
# max_tokens is also not supported by the OpenCode session endpoint. Keep
# it only as telemetry: never character-truncate JavaScript/JSON responses.
rendered = build_opencode_prompt(body.get("messages") or [])
last_error = "OpenCode inference failed"
tried: list[str] = []
for model_id in _candidate_models(requested_model):
tried.append(model_id)
started = time.monotonic()
try:
session_id = _create_session(model_id)
answer = _send_message(session_id, model_id, rendered["system"], rendered["prompt"])
except Exception as exc: # noqa: BLE001 - retried across free models
last_error = f"{type(exc).__name__}: {str(exc)[:200]}"
continue
elapsed = round(time.monotonic() - started, 2)
with STATE_LOCK:
state["calls"] += 1
state["last_model"] = model_id
state["last_elapsed_s"] = elapsed
state["last_error"] = None
if model_id != canonical_model_id(requested_model):
state["fallbacks_used"] += 1
state["last_fallback"] = model_id
else:
state["last_fallback"] = None
return {
"id": "chatcmpl-" + uuid.uuid4().hex[:16],
"object": "chat.completion",
"created": int(time.time()),
"model": canonical_model_id(requested_model),
"choices": [
{"index": 0, "message": {"role": "assistant", "content": answer}, "finish_reason": "stop"}
],
"usage": {"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0},
"backend": "opencode-bridge",
"provider": OPENCODE_PROVIDER,
}
with STATE_LOCK:
state["failures"] += 1
state["last_error"] = f"tried={tried} last={last_error}"
raise RuntimeError(f"All OpenCode free models failed (tried={tried}): {last_error}")
api = FastAPI(title="Naldo OpenCode Bridge", version="1.0.0")
@api.get("/health")
def health() -> dict[str, Any]:
try:
upstream = _http_json(OPENCODE + "/global/health", timeout=5.0, retries=0)
upstream_ok = bool(isinstance(upstream, dict) and upstream.get("healthy") is True)
except Exception as exc: # noqa: BLE001 - health must not raise
upstream = {"error": f"{type(exc).__name__}"}
upstream_ok = False
with STATE_LOCK:
snapshot = dict(state)
return {
"ready": upstream_ok,
"bridge": "naldo-opencode",
"provider": OPENCODE_PROVIDER,
"primary_model": PRIMARY_MODEL,
"fallback_models": list(FALLBACK_MODELS),
"opencode_base": OPENCODE,
"opencode": {"ok": upstream_ok, "detail": "ok" if upstream_ok else upstream},
"temperature": TEMPERATURE_NOTE,
**{k: v for k, v in snapshot.items() if k != "opencode_base"},
}
@api.get("/v1/models")
def models() -> dict[str, Any]:
now = int(time.time())
return {
"object": "list",
"data": [
{"id": model, "object": "model", "owned_by": OPENCODE_PROVIDER}
for model in FALLBACK_MODELS
]
+ [
{"id": f"{OPENCODE_PROVIDER}/{model}", "object": "model", "owned_by": OPENCODE_PROVIDER}
for model in FALLBACK_MODELS
],
}
@api.post("/v1/chat/completions")
async def chat(request: Request) -> Any:
try:
body = await request.json()
except Exception as exc:
raise HTTPException(status_code=400, detail=f"Invalid JSON: {str(exc)[:160]}") from exc
if not isinstance(body.get("messages"), list):
raise HTTPException(status_code=400, detail="messages must be a list")
requested_model = str(body.get("model") or PRIMARY_MODEL)
if not is_allowed_model(requested_model):
raise HTTPException(status_code=404, detail=f"Unknown model: {requested_model}")
try:
payload = await run_in_threadpool(_chat_payload, body)
except ValueError as exc:
raise HTTPException(status_code=404, detail=str(exc)[:200]) from None
except RuntimeError as exc:
raise HTTPException(status_code=503, detail=str(exc)[:800]) from None
if not body.get("stream"):
return JSONResponse(payload)
content = payload["choices"][0]["message"]["content"]
chunk = {
"id": payload["id"],
"object": "chat.completion.chunk",
"created": payload["created"],
"model": payload["model"],
"choices": [{"index": 0, "delta": {"role": "assistant", "content": content}, "finish_reason": "stop"}],
}
async def events(): # pragma: no cover - streaming passthrough
yield "data: " + json.dumps(chunk, ensure_ascii=False) + "\n\n"
yield "data: [DONE]\n\n"
return StreamingResponse(events(), media_type="text/event-stream")
def _forward_embeddings(raw: bytes) -> dict[str, Any]:
last_error: Exception | None = None
for attempt in range(EMBED_RETRIES):
forward = urllib.request.Request(
EMBED_BASE.rstrip("/") + "/embeddings", data=raw, method="POST"
)
forward.add_header("Content-Type", "application/json")
api_key = os.environ.get("API_KEY", "").strip()
if api_key:
forward.add_header("Authorization", f"Bearer {api_key}")
try:
with urllib.request.urlopen(forward, timeout=120) as response:
return json.loads(response.read().decode("utf-8"))
except urllib.error.HTTPError as exc:
last_error = exc
if exc.code not in EMBED_RETRYABLE:
raise
except (urllib.error.URLError, TimeoutError, OSError) as exc:
last_error = exc
if attempt + 1 < EMBED_RETRIES:
time.sleep(min(EMBED_BACKOFF * (2 ** attempt), 8.0))
assert last_error is not None
raise last_error
@api.post("/v1/embeddings")
async def embeddings(request: Request) -> Any:
"""Proxy embeddings with bounded retry so transient boot 503s do not kill Voyager."""
raw = await request.body()
try:
payload = await run_in_threadpool(_forward_embeddings, raw)
return JSONResponse(payload)
except urllib.error.HTTPError as exc:
status = exc.code if 400 <= exc.code < 500 and exc.code not in EMBED_RETRYABLE else 503
raise HTTPException(status_code=status, detail=f"embedding backend unavailable: HTTP {exc.code}") from None
except Exception as exc: # noqa: BLE001 - hide upstream internals
raise HTTPException(status_code=503, detail=f"embedding backend unavailable: {type(exc).__name__}") from None
if __name__ == "__main__":
import uvicorn
# Loopback only: never expose OpenCode or this bridge externally.
uvicorn.run(api, host="127.0.0.1", port=PORT, log_level="warning")