Erinaldordso's picture
Run Naldo on AltoClef with Muse supervision
8f6b7b7 verified
Raw History Blame Contribute Delete
49.5 kB
"""Programa: CPU-only OpenAI-compatible LLM endpoint for the original Voyager agent."""
from __future__ import annotations
import ast
import json
import math
import os
import pathlib
import re
import threading
import time
import uuid
from typing import Any, Iterator
from cpu_backend import cpu_only_report, cpu_only_summary
# The Space is hosted on ZeroGPU hardware only to retain the 16-vCPU / ~97-GiB
# CPU envelope. ZeroGPU requires at least one decorated function. The sentinel
# below is NEVER called by the API or UI, so it never requests GPU time.
import spaces
# Strict CPU mode before llama.cpp is imported.
os.environ["CUDA_VISIBLE_DEVICES"] = ""
os.environ["HIP_VISIBLE_DEVICES"] = ""
os.environ["ROCR_VISIBLE_DEVICES"] = ""
os.environ["GGML_VK_VISIBLE_DEVICES"] = ""
os.environ["GGML_CUDA"] = "0"
os.environ["HF_HUB_DISABLE_PROGRESS_BARS"] = "1"
os.environ["TQDM_DISABLE"] = "1"
os.environ.setdefault("MALLOC_ARENA_MAX", "2")
import gradio as gr
import psutil
import tiktoken
import uvicorn
from fastapi import FastAPI, Header, HTTPException, Request
from fastapi.responses import JSONResponse, StreamingResponse
from huggingface_hub import hf_hub_download
from starlette.concurrency import run_in_threadpool
from minecraft_stack import STACK
from voyager_stack import VOYAGER
from gemini_bot_stack import GEMINI_BOT
from altoclef_stack import ALTOCLEF
MODEL_REPO = os.environ.get(
"MODEL_REPO",
"unsloth/Qwen3-Coder-30B-A3B-Instruct-GGUF",
)
MODEL_FILE = os.environ.get(
"MODEL_FILE",
"Qwen3-Coder-30B-A3B-Instruct-Q4_K_M.gguf",
)
MODEL_REVISION = os.environ.get(
"MODEL_REVISION",
"b17cb02dd882d5b6ab62fc777ad2995f19668350",
)
MODEL_ALIAS = os.environ.get("MODEL_ALIAS", "qwen3-coder-30b-a3b-instruct")
MODEL_ALIASES = {
MODEL_ALIAS,
"Qwen3-Coder-30B-A3B-Instruct",
"voyager-qwen3-coder",
# Kept so an older client or checkpoint that still names the previous brain
# resolves instead of 404ing on the model name.
"granite-4.0-h-tiny",
"Granite-4.0-H-Tiny",
"voyager-granite-4-h-tiny",
"qwen2.5-coder-7b-instruct",
"Qwen2.5-Coder-7B-Instruct",
"voyager-qwen2.5-coder-7b",
"qwen2.5-coder-3b-instruct",
"Qwen2.5-Coder-3B-Instruct",
"voyager-qwen2.5-coder-3b",
"qwen3-4b-instruct-2507",
"voyager-qwen3-4b",
}
# The model is 18.56 GB. huggingface_hub defaults to ~/.cache, which this
# container loses on every rebuild, so a rebuild re-downloaded the whole file
# before the first request could be served. /persist survives, so the download
# happens once.
HF_CACHE_DIR = os.environ.get("HF_CACHE_DIR", "/persist/hf-cache")
AUX_MODEL_REPO = os.environ.get("AUX_MODEL_REPO", "ibm-granite/granite-4.0-h-tiny-GGUF")
AUX_MODEL_FILE = os.environ.get("AUX_MODEL_FILE", "granite-4.0-h-tiny-Q4_K_M.gguf")
AUX_MODEL_REVISION = os.environ.get("AUX_MODEL_REVISION", "08d5a8a9741dd5c1a95d2d39e25253226aa1464e")
AUX_MODEL_ALIAS = os.environ.get("AUX_MODEL_ALIAS", "granite-4.0-h-tiny-aux")
AUX_MODEL_ALIASES = {AUX_MODEL_ALIAS, "voyager-aux-granite-tiny"}
AUX_CTX = int(os.environ.get("AUX_CPU_CTX", "8192"))
AUX_PRELOAD = os.environ.get("AUX_CPU_PRELOAD", "0") == "1"
EMBED_REPO = os.environ.get("EMBED_REPO", "Qwen/Qwen3-Embedding-0.6B-GGUF")
EMBED_FILE = os.environ.get("EMBED_FILE", "Qwen3-Embedding-0.6B-Q8_0.gguf")
EMBED_REVISION = os.environ.get(
"EMBED_REVISION",
"370f27d7550e0def9b39c1f16d3fbaa13aa67728",
)
EMBED_ALIAS = os.environ.get("EMBED_ALIAS", "text-embedding-ada-002")
EMBED_CTX = int(os.environ.get("EMBED_CTX", "8192"))
EMBED_BATCH = int(os.environ.get("EMBED_BATCH", "8192"))
PORT = int(os.environ.get("PORT", "7860"))
GRADIO_PORT = int(os.environ.get("GRADIO_PORT", "7862"))
# Qwen3-Coder-30B-A3B declares max_position_embeddings 262144 with
# rope_scaling null, so 32768 needs no YaRN and costs no quality. The previous
# 8192 window was already tight: the Action prompt measured 6909 tokens and the
# model then had to fit a 1000+ token program into what was left.
CTX = int(os.environ.get("CPU_CTX", "32768"))
BATCH = int(os.environ.get("CPU_BATCH", "2048"))
UBATCH = int(os.environ.get("CPU_UBATCH", "512"))
# The Action Agent must be able to emit the official multi-function program.
# A 512-token ceiling truncated valid JavaScript into a parse error, so the cap
# is raised rather than the upstream response contract narrowed. Curriculum,
# Critic and SkillManager still use the smaller default.
MAX_TOKENS_CAP = int(os.environ.get("MAX_TOKENS_CAP", "2048"))
DEFAULT_MAX_TOKENS = int(os.environ.get("DEFAULT_MAX_TOKENS", "384"))
API_KEY = os.environ.get("API_KEY", "").strip()
PLAYER_MODE = os.environ.get("PLAYER_MODE", "0") == "1"
PRELOAD = os.environ.get("CPU_PRELOAD", "1") == "1"
# Server-only mode. The brain lives in the Space `Erinaldordso/Mine`; this Space
# keeps the Minecraft server, the Mineflayer bridge and the Voyager runner, and
# nothing else. Every role the bot needs -- Action, Curriculum, Critic, QA,
# SkillManager -- plus embeddings is served at VOYAGER_BRAIN_BASE_URL.
#
# Measured reason: the local Granite aux and the local Qwen main were both
# loading here even though no caller used them. The Voyager runner sets
# OPENAI_API_BASE to the Mine endpoint, so its chat AND embedding calls already
# left this Space; the only remaining local user was the Gemini bot's embeddings.
# Holding two GGUF models resident for that cost RAM the Java server needs, and
# a failed local load surfaced as `aux_ready: false` in /health, which reads as a
# broken bot when it is only an unused, broken loader.
#
# SERVER_ONLY=1 skips loading them and makes the chat route refuse instead of
# silently starting an 18 GB download on first request.
SERVER_ONLY = os.environ.get("MINECRAFT_SERVER_ONLY", "1") == "1"
BRAIN_BASE_URL = os.environ.get(
"VOYAGER_BRAIN_BASE_URL", "https://erinaldordso-mine.hf.space/v1"
)
@spaces.GPU(duration=1)
def _zerogpu_compatibility_sentinel():
# Never invoked. Presence only satisfies the ZeroGPU host scanner.
return {"gpu_used": False, "inference_backend": "cpu-only"}
PLAYER_SYSTEM = os.environ.get(
"PLAYER_SYSTEM_PROMPT",
(
"Voce e o cerebro estrategico do jogador autonomo iaautono em Minecraft Bedrock Survival. "
"A cada chamada escolha UMA unica proxima acao curta, segura, verificavel e executavel. "
"Use somente fatos presentes no estado real recebido; nunca invente item, recurso, entidade ou coordenada. "
"Prioridade de emergencia: se estiver morto, respawn; se estiver se afogando ou sem ar, sair da agua/subir; "
"depois preservar vida, comida e equipamentos. "
"Progressao permanente: madeira -> bancada -> ferramentas de pedra -> fornalha/comida -> ferro -> "
"escudo/armas/armadura -> base segura e armazenamento -> fazenda irrigada com replantio -> "
"diamante -> encantamentos/reparo -> Nether -> netherite -> infraestrutura e estoques avancados. "
"Nao ataque jogadores nem destrua construcoes de terceiros. Nao use comandos administrativos ou cheats. "
"O controlador local executa fisica, movimento, mineracao, crafting, colocacao, inventario, combate contra mobs, "
"respawn e reconexao; voce decide apenas o objetivo imediato. "
"Se uma acao anterior falhou, escolha uma alternativa segura em vez de repetir cegamente. "
"Nunca declare sucesso sem evidencia no estado. Nao fique ocioso se houver progresso survival seguro possivel. "
"Responda estritamente no formato solicitado pelo cliente e mantenha o motivo curto."
),
)
def _read_text(path: str) -> str:
try:
return pathlib.Path(path).read_text().strip()
except Exception:
return ""
def _cgroup_cpu_quota() -> int | None:
raw = _read_text("/sys/fs/cgroup/cpu.max")
if raw:
parts = raw.split()
if len(parts) >= 2 and parts[0] != "max":
try:
q, period = int(parts[0]), int(parts[1])
if q > 0 and period > 0:
return max(1, int(math.floor(q / period)))
except Exception:
pass
return None
def _affinity_cpus() -> int:
try:
return max(1, len(os.sched_getaffinity(0)))
except Exception:
return max(1, os.cpu_count() or 1)
def effective_cpus() -> int:
affinity = _affinity_cpus()
quota = _cgroup_cpu_quota()
return min(affinity, quota) if quota else affinity
def _memory_limit_gib() -> float | None:
for path in ("/sys/fs/cgroup/memory.max", "/sys/fs/cgroup/memory/memory.limit_in_bytes"):
raw = _read_text(path)
if raw and raw != "max":
try:
value = int(raw)
if 0 < value < (1 << 60):
return round(value / (1024**3), 2)
except Exception:
pass
return None
THREADS = int(os.environ.get("CPU_THREADS", "16"))
THREADS = max(1, min(THREADS, effective_cpus()))
THREADS_BATCH = int(os.environ.get("CPU_THREADS_BATCH", "16"))
THREADS_BATCH = max(1, min(THREADS_BATCH, effective_cpus()))
FLASH_ATTN = os.environ.get("CPU_FLASH_ATTN", "1") == "1"
CGROUP_CPU_QUOTA = _cgroup_cpu_quota()
MEMORY_LIMIT_GIB = _memory_limit_gib()
os.environ["OMP_NUM_THREADS"] = str(THREADS)
os.environ["OPENBLAS_NUM_THREADS"] = str(THREADS)
os.environ["MKL_NUM_THREADS"] = str(THREADS)
os.environ["NUMEXPR_NUM_THREADS"] = str(THREADS)
os.environ["OMP_DYNAMIC"] = "FALSE"
_model = None
_model_path: str | None = None
_model_error = ""
_model_load_seconds = 0.0
_model_loaded_at = 0.0
_loading = False
_load_lock = threading.Lock()
_llm_lock = threading.Lock()
_aux_model = None
_aux_model_path: str | None = None
_aux_model_error = ""
_aux_model_load_seconds = 0.0
_aux_model_loaded_at = 0.0
_aux_loading = False
_aux_load_lock = threading.Lock()
_embed_model = None
_embed_model_path: str | None = None
_embed_error = ""
_embed_loading = False
_embed_load_seconds = 0.0
_embed_load_lock = threading.Lock()
_embed_lock = threading.Lock()
_token_decoder = None
def _load_model() -> None:
global _model, _model_path, _model_error, _model_load_seconds, _model_loaded_at, _loading
if SERVER_ONLY:
# No local brain in this Space. See the SERVER_ONLY note above.
raise RuntimeError(
"this Space runs the Minecraft server only; the brain is at "
f"{BRAIN_BASE_URL}"
)
if _model is not None:
return
with _load_lock:
if _model is not None:
return
_loading = True
started = time.perf_counter()
try:
print(
f"[cpu] loading {MODEL_REPO}/{MODEL_FILE} threads={THREADS} "
f"ctx={CTX} batch={BATCH}/{UBATCH} n_gpu_layers=0",
flush=True,
)
_model_path = hf_hub_download(
repo_id=MODEL_REPO,
filename=MODEL_FILE,
revision=MODEL_REVISION,
cache_dir=HF_CACHE_DIR,
)
from llama_cpp import Llama
_model = Llama(
model_path=_model_path,
n_ctx=CTX,
n_threads=THREADS,
n_threads_batch=THREADS_BATCH,
n_gpu_layers=0,
n_batch=BATCH,
n_ubatch=UBATCH,
flash_attn=FLASH_ATTN,
offload_kqv=False,
op_offload=False,
use_mmap=True,
use_mlock=False,
numa=False,
no_perf=True,
verbose=False,
)
_model_load_seconds = time.perf_counter() - started
_model_loaded_at = time.time()
_model_error = ""
print(f"[cpu] ready load_seconds={_model_load_seconds:.2f}", flush=True)
except Exception as exc:
_model_error = repr(exc)
print(f"[cpu] load failed: {_model_error}", flush=True)
finally:
_loading = False
def _get_model(timeout: float = 900.0):
if _model is not None:
return _model
if not _loading:
_load_model()
deadline = time.time() + timeout
while time.time() < deadline:
if _model is not None:
return _model
if not _loading and _model_error:
raise RuntimeError(_model_error)
time.sleep(0.25)
raise RuntimeError("model readiness timeout")
def _load_aux_model() -> None:
global _aux_model, _aux_model_path, _aux_model_error, _aux_model_load_seconds, _aux_model_loaded_at, _aux_loading
if SERVER_ONLY:
# Granite used to be resident here as the QA/SkillManager model. Those
# roles are served by the Mine Space now, so this loader is dead code
# and is refused rather than left to download 2 GB on a stray request.
raise RuntimeError(
"this Space runs the Minecraft server only; the auxiliary model is "
f"served at {BRAIN_BASE_URL}"
)
if _aux_model is not None:
return
with _aux_load_lock:
if _aux_model is not None:
return
_aux_loading = True
started = time.perf_counter()
try:
print(
f"[cpu-aux] loading {AUX_MODEL_REPO}/{AUX_MODEL_FILE} threads={THREADS} "
f"ctx={AUX_CTX} batch={BATCH}/{UBATCH} n_gpu_layers=0",
flush=True,
)
_aux_model_path = hf_hub_download(
repo_id=AUX_MODEL_REPO,
filename=AUX_MODEL_FILE,
revision=AUX_MODEL_REVISION,
cache_dir=HF_CACHE_DIR,
)
from llama_cpp import Llama
_aux_model = Llama(
model_path=_aux_model_path,
n_ctx=AUX_CTX,
n_threads=THREADS,
n_threads_batch=THREADS_BATCH,
n_gpu_layers=0,
n_batch=BATCH,
n_ubatch=UBATCH,
flash_attn=FLASH_ATTN,
offload_kqv=False,
op_offload=False,
use_mmap=True,
use_mlock=False,
numa=False,
no_perf=True,
verbose=False,
)
_aux_model_load_seconds = time.perf_counter() - started
_aux_model_loaded_at = time.time()
_aux_model_error = ""
print(f"[cpu-aux] ready load_seconds={_aux_model_load_seconds:.2f}", flush=True)
except Exception as exc:
_aux_model_error = repr(exc)
print(f"[cpu-aux] load failed: {_aux_model_error}", flush=True)
finally:
_aux_loading = False
def _get_aux_model(timeout: float = 300.0):
if _aux_model is not None:
return _aux_model
if not _aux_loading:
_load_aux_model()
deadline = time.time() + timeout
while time.time() < deadline:
if _aux_model is not None:
return _aux_model
if not _aux_loading and _aux_model_error:
raise RuntimeError(_aux_model_error)
time.sleep(0.25)
raise RuntimeError("aux model readiness timeout")
def _select_chat_model(requested_model: str):
if SERVER_ONLY:
# Fail with a redirect instead of a load attempt. A caller that still
# names a chat model here is pointed at the Space that actually serves
# it, so the misroute is obvious in the response body and in the log.
raise RuntimeError(
f"chat is not served by this Space (model requested: {requested_model}); "
f"the brain is at {BRAIN_BASE_URL}"
)
if requested_model in AUX_MODEL_ALIASES:
return _get_aux_model(), "aux"
if requested_model in MODEL_ALIASES:
return _get_model(), "main"
raise ValueError(f"Unknown model: {requested_model}")
def _load_embed_model() -> None:
global _embed_model, _embed_model_path, _embed_error, _embed_loading, _embed_load_seconds
if SERVER_ONLY:
# Embeddings moved to the Mine Space with the rest of the brain. Both
# Spaces load the same Qwen3-Embedding-0.6B and both return 1024-dim
# vectors, so the existing Chroma indexes stay valid.
raise RuntimeError(
"this Space runs the Minecraft server only; embeddings are served at "
f"{BRAIN_BASE_URL}"
)
if _embed_model is not None:
return
with _embed_load_lock:
if _embed_model is not None:
return
_embed_loading = True
started = time.perf_counter()
try:
_embed_model_path = hf_hub_download(
repo_id=EMBED_REPO,
filename=EMBED_FILE,
revision=EMBED_REVISION,
cache_dir=HF_CACHE_DIR,
)
from llama_cpp import Llama
from llama_cpp import llama_cpp as llama_backend
_embed_model = Llama(
model_path=_embed_model_path,
n_ctx=EMBED_CTX,
n_threads=THREADS,
n_threads_batch=THREADS_BATCH,
n_gpu_layers=0,
n_batch=EMBED_BATCH,
n_ubatch=EMBED_BATCH,
embedding=True,
pooling_type=llama_backend.LLAMA_POOLING_TYPE_LAST,
flash_attn=True,
offload_kqv=False,
op_offload=False,
use_mmap=True,
use_mlock=False,
numa=False,
verbose=False,
)
_embed_load_seconds = time.perf_counter() - started
_embed_error = ""
print(f"[cpu] embedding ready load_seconds={_embed_load_seconds:.2f}", flush=True)
except Exception as exc:
_embed_error = repr(exc)
print(f"[cpu] embedding load failed: {_embed_error}", flush=True)
finally:
_embed_loading = False
def _get_embed_model(timeout: float = 300.0):
if _embed_model is not None:
return _embed_model
if not _embed_loading:
_load_embed_model()
deadline = time.time() + timeout
while time.time() < deadline:
if _embed_model is not None:
return _embed_model
if not _embed_loading and _embed_error:
raise RuntimeError(_embed_error)
time.sleep(0.25)
raise RuntimeError("embedding model readiness timeout")
def _auth(authorization: str | None) -> None:
if API_KEY and authorization != f"Bearer {API_KEY}":
raise HTTPException(status_code=401, detail="Invalid API key")
def _get_token_decoder():
global _token_decoder
if _token_decoder is None:
_token_decoder = tiktoken.get_encoding("cl100k_base")
return _token_decoder
def _normalize_embedding_inputs(inputs: Any) -> list[str]:
if isinstance(inputs, str):
return [inputs]
if isinstance(inputs, list) and all(isinstance(item, str) for item in inputs):
return inputs
decoder = _get_token_decoder()
if isinstance(inputs, list) and all(isinstance(item, int) for item in inputs):
return [decoder.decode(inputs)]
if isinstance(inputs, list) and all(
isinstance(item, list) and all(isinstance(token, int) for token in item)
for item in inputs
):
return [decoder.decode(item) for item in inputs]
raise HTTPException(
status_code=400,
detail="input must be a string, list of strings, token list, or list of token lists",
)
def _messages(messages: list[dict[str, Any]]) -> list[dict[str, Any]]:
out: list[dict[str, Any]] = []
for raw_message in messages:
if not isinstance(raw_message, dict):
continue
message = dict(raw_message)
tool_calls = message.get("tool_calls")
if isinstance(tool_calls, list):
normalized_calls: list[dict[str, Any]] = []
for raw_call in tool_calls:
if not isinstance(raw_call, dict):
continue
call = dict(raw_call)
function = call.get("function")
if isinstance(function, dict):
function = dict(function)
arguments = function.get("arguments")
if isinstance(arguments, str):
try:
parsed_arguments = json.loads(arguments)
if isinstance(parsed_arguments, dict):
function["arguments"] = parsed_arguments
except json.JSONDecodeError:
pass
call["function"] = function
normalized_calls.append(call)
message["tool_calls"] = normalized_calls
out.append(message)
if PLAYER_MODE:
if out and out[0].get("role") == "system":
out[0]["content"] = PLAYER_SYSTEM + "\n\n" + str(out[0].get("content") or "")
else:
out.insert(0, {"role": "system", "content": PLAYER_SYSTEM})
return out
def _kwargs(payload: dict[str, Any], stream: bool) -> dict[str, Any]:
max_tokens = payload.get("max_tokens", payload.get("max_completion_tokens", DEFAULT_MAX_TOKENS))
out: dict[str, Any] = {
"messages": _messages(payload.get("messages") or []),
"max_tokens": max(1, min(int(max_tokens), MAX_TOKENS_CAP)),
"temperature": float(payload.get("temperature", 0.0)),
"top_p": float(payload.get("top_p", 1.0)),
"top_k": int(payload.get("top_k", 0)),
"min_p": float(payload.get("min_p", 0.0)),
"presence_penalty": float(payload.get("presence_penalty", 0.0)),
"stream": stream,
}
for key in ("tools", "tool_choice", "stop", "response_format", "seed"):
if key in payload and payload[key] is not None:
out[key] = payload[key]
if PLAYER_MODE and not payload.get("tools") and "response_format" not in out:
out["response_format"] = {"type": "json_object"}
return out
def _tool_argument_config(tools: list[dict[str, Any]] | None, function_name: str) -> dict[str, Any]:
for tool in tools or []:
if not isinstance(tool, dict) or tool.get("type") != "function":
continue
function = tool.get("function")
if not isinstance(function, dict) or function.get("name") != function_name:
continue
parameters = function.get("parameters")
if isinstance(parameters, dict):
properties = parameters.get("properties", parameters)
return properties if isinstance(properties, dict) else {}
return {}
def _convert_tool_value(value: str, name: str, config: dict[str, Any]) -> Any:
spec = config.get(name, {}) if isinstance(config, dict) else {}
param_type = spec.get("type", "string") if isinstance(spec, dict) else "string"
if isinstance(param_type, list):
param_type = next((item for item in param_type if item != "null"), "string")
param_type = str(param_type).lower()
if param_type == "null":
return None
if param_type.startswith("int"):
try:
return int(value)
except (TypeError, ValueError):
return value
if param_type.startswith(("num", "float")):
try:
number = float(value)
return int(number) if number.is_integer() else number
except (TypeError, ValueError):
return value
if param_type in {"boolean", "bool", "binary"}:
lowered = value.lower()
return lowered == "true" if lowered in {"true", "false"} else False
if param_type in {"object", "array", "arr"} or param_type.startswith(("dict", "list")):
try:
return json.loads(value)
except (TypeError, ValueError, json.JSONDecodeError):
try:
return ast.literal_eval(value)
except (ValueError, SyntaxError):
return value
return value
_TOOL_CALL_RE = re.compile(r"<tool_call>(.*?)</tool_call>|<tool_call>(.*?)$", re.DOTALL)
_TOOL_FUNCTION_RE = re.compile(r"<function=(.*?)</function>|<function=(.*)$", re.DOTALL)
_TOOL_PARAMETER_RE = re.compile(
r"<parameter=(.*?)(?:</parameter>|(?=<parameter=)|(?=</function>)|$)",
re.DOTALL,
)
def _parse_qwen3_coder_tool_calls(
model_output: str,
tools: list[dict[str, Any]] | None,
) -> tuple[str | None, list[dict[str, Any]]]:
# Mirrors Qwen's official qwen3coder_tool_parser semantics for non-streaming output.
if "<function=" not in model_output:
return model_output, []
matches = _TOOL_CALL_RE.findall(model_output)
raw_tool_calls = [left if left else right for left, right in matches] or [model_output]
raw_functions: list[str] = []
for raw_tool_call in raw_tool_calls:
for complete, partial in _TOOL_FUNCTION_RE.findall(raw_tool_call):
raw_functions.append(complete if complete else partial)
parsed_calls: list[dict[str, Any]] = []
for function_call in raw_functions:
if ">" not in function_call:
continue
function_name, parameters = function_call.split(">", 1)
function_name = function_name.strip()
if not function_name:
continue
config = _tool_argument_config(tools, function_name)
arguments: dict[str, Any] = {}
for match_text in _TOOL_PARAMETER_RE.findall(parameters):
if ">" not in match_text:
continue
param_name, param_value = match_text.split(">", 1)
if param_value.startswith("\n"):
param_value = param_value[1:]
if param_value.endswith("\n"):
param_value = param_value[:-1]
param_name = param_name.strip()
arguments[param_name] = _convert_tool_value(param_value, param_name, config)
parsed_calls.append(
{
"id": f"call_{uuid.uuid4().hex[:24]}",
"type": "function",
"function": {
"name": function_name,
"arguments": json.dumps(arguments, ensure_ascii=False),
},
}
)
if not parsed_calls:
return model_output, []
content_index = model_output.find("<tool_call>")
if content_index < 0:
content_index = model_output.find("<function=")
content = model_output[:content_index] if content_index >= 0 else ""
return content if content else None, parsed_calls
def _nonstream(payload: dict[str, Any]) -> dict[str, Any]:
requested_model = str(payload.get("model") or MODEL_ALIAS)
llm, model_role = _select_chat_model(requested_model)
started = time.perf_counter()
with _llm_lock:
print(f"[chat] start role={model_role} model={requested_model} temperature={payload.get('temperature', 0.0)}", flush=True)
result = llm.create_chat_completion(**_kwargs(payload, False))
result = dict(result)
if payload.get("tools") and result.get("choices"):
choice = result["choices"][0]
message = choice.get("message") or {}
content = message.get("content")
if isinstance(content, str):
parsed_content, tool_calls = _parse_qwen3_coder_tool_calls(
content,
payload.get("tools"),
)
if tool_calls:
message["content"] = parsed_content
message["tool_calls"] = tool_calls
choice["message"] = message
choice["finish_reason"] = "tool_calls"
result["model"] = requested_model
result["backend"] = "cpu-only"
result["program_metrics"] = {
"elapsed_seconds": round(time.perf_counter() - started, 4),
"threads": THREADS,
"context": AUX_CTX if model_role == "aux" else CTX,
"model_role": model_role,
"n_gpu_layers": 0,
}
print(f"[chat] done role={model_role} model={requested_model} metrics={result['program_metrics']} usage={result.get('usage', {})}", flush=True)
return result
def _stream(payload: dict[str, Any]) -> Iterator[str]:
requested_model = str(payload.get("model") or MODEL_ALIAS)
llm, _model_role = _select_chat_model(requested_model)
with _llm_lock:
for chunk in llm.create_chat_completion(**_kwargs(payload, True)):
item = dict(chunk)
item["model"] = requested_model
item["backend"] = "cpu-only"
yield "data: " + json.dumps(item, ensure_ascii=False) + "\n\n"
yield "data: [DONE]\n\n"
# Computed once: the answer cannot change while the process lives, and the
# status endpoint must not walk the package directory on every request.
_CPU_ONLY = cpu_only_report()
print(f"[cpu] {cpu_only_summary(_CPU_ONLY)}", flush=True)
api = FastAPI(title="Programa Voyager OpenAI Endpoint", version="6.0.0")
_supervisor_lock = threading.Lock()
_supervisor_recoveries = 0
_supervisor_last_action = "startup"
_supervisor_last_check = 0.0
# Set during shutdown BEFORE any save/stop, so the 30s loop cannot restart a
# bot or the Java server mid-shutdown. All supervisor sleeps wait on it.
_supervisor_stop = threading.Event()
def _service_supervisor() -> None:
global _supervisor_recoveries, _supervisor_last_action, _supervisor_last_check
backoff = 10
while not _supervisor_stop.is_set():
try:
if _supervisor_stop.wait(30):
break
_supervisor_last_check = time.time()
mc = STACK.status()
processes = mc.get("processes") or {}
critical = ("minecraft", "viaproxy", "friends")
dead = [name for name in critical if name in processes and not processes[name].get("running")]
missing = [name for name in critical if mc.get("state") == "running" and name not in processes]
if mc.get("state") == "error" or dead or missing:
with _supervisor_lock:
_supervisor_recoveries += 1
_supervisor_last_action = f"minecraft recovery #{_supervisor_recoveries}: dead={dead} missing={missing} state={mc.get('state')}"
VOYAGER.stop()
GEMINI_BOT.stop()
ALTOCLEF.stop()
STACK.stop()
if _supervisor_stop.wait(backoff):
break
STACK.start_async()
# Voyager stays available on demand but never auto-starts:
# VOYAGER_AUTOSTART defaults to 0 (AltoClef is the default
# controller). The recovery path must respect the same
# flag, otherwise a restart silently re-enables it.
if os.environ.get("VOYAGER_AUTOSTART", "0") == "1":
VOYAGER.start_async()
if os.environ.get("GEMINI_BOT_AUTOSTART", "1") == "1":
GEMINI_BOT.start_async()
if os.environ.get("ALTOCLEF_AUTOSTART", "1") == "1":
ALTOCLEF.start_async()
backoff = min(backoff * 2, 120)
continue
vs = VOYAGER.status()
if (os.environ.get("VOYAGER_AUTOSTART", "0") == "1"
and mc.get("state") == "running" and vs.get("state") in {"error", "exited", "stopped"} and not vs.get("running")):
with _supervisor_lock:
_supervisor_recoveries += 1
_supervisor_last_action = f"voyager recovery #{_supervisor_recoveries}: state={vs.get('state')}"
VOYAGER.start_async()
backoff = min(backoff * 2, 120)
continue
gs = GEMINI_BOT.status()
if (os.environ.get("GEMINI_BOT_AUTOSTART", "1") == "1" and mc.get("state") == "running"
and gs.get("state") in {"error", "stopped"} and not gs.get("running")):
GEMINI_BOT.start_async()
als = ALTOCLEF.status()
if (os.environ.get("ALTOCLEF_AUTOSTART", "1") == "1" and mc.get("state") == "running"
and als.get("state") in {"error", "stopped"} and not als.get("running")):
ALTOCLEF.start_async()
if mc.get("access_state") == "degraded":
# nether2rak already retries its Xbox session internally. Report
# loss of access without restarting the Minecraft world or bot.
_supervisor_last_action = "xbox session degraded; relay retrying internally"
continue
if mc.get("state") == "running" and vs.get("running"):
backoff = 10
_supervisor_last_action = "healthy"
except Exception as exc:
_supervisor_last_action = f"supervisor exception: {type(exc).__name__}: {str(exc)[:160]}"
if _supervisor_stop.wait(min(backoff, 120)):
break
backoff = min(backoff * 2, 120)
@api.on_event("startup")
def _startup() -> None:
if SERVER_ONLY:
# Only the Minecraft server, the Mineflayer bridge and the Voyager
# runner start here. No local GGUF is loaded and no chat preload thread
# is spawned, so the RAM budget belongs to Java.
print(
"[startup] server-only mode: no local model will be loaded; the "
f"brain is at {BRAIN_BASE_URL}",
flush=True,
)
if PRELOAD and not SERVER_ONLY:
threading.Thread(target=_load_model, daemon=True, name="cpu-model-preload").start()
threading.Thread(target=_load_embed_model, daemon=True, name="cpu-embedding-preload").start()
if AUX_PRELOAD and not SERVER_ONLY:
threading.Thread(target=_load_aux_model, daemon=True, name="cpu-aux-model-preload").start()
if os.environ.get("MINECRAFT_AUTOSTART", "1") == "1":
STACK.start_async()
if os.environ.get("VOYAGER_AUTOSTART", "0") == "1":
VOYAGER.start_async()
if os.environ.get("GEMINI_BOT_AUTOSTART", "1") == "1":
GEMINI_BOT.start_async()
if os.environ.get("ALTOCLEF_AUTOSTART", "1") == "1":
ALTOCLEF.start_async()
threading.Thread(target=_service_supervisor, daemon=True, name="service-supervisor").start()
@api.on_event("shutdown")
def _shutdown() -> None:
# Graceful stop on SIGTERM/redeploy: stop the supervisor FIRST so the 30s
# loop cannot restart a bot or Java mid-shutdown, then flush the world and
# stop the Java server (vanilla saves again on "stop"). Best-effort and
# local only; no remote command endpoint is exposed for this.
_supervisor_stop.set()
try:
STACK.save_all(timeout=20.0)
except Exception:
pass
for runner in (VOYAGER, GEMINI_BOT, ALTOCLEF):
try:
runner.stop()
except Exception:
pass
try:
STACK.stop()
except Exception:
pass
@api.get("/")
def root():
return {
"status": "ok",
"service": "Minecraft server and autonomous bot runners" if SERVER_ONLY else "CPU model endpoint",
"api": "/v1",
"health": "/health",
}
@api.get("/health")
def health():
if SERVER_ONLY:
# No local weights exist, so there is nothing to report as ready or
# broken. Reporting `aux_ready: false` here (the old behaviour) was
# actively misleading: it reads as a failed load when the truth is that
# no local load is intended. The role map points at the real host.
return {
"status": "ok",
"mode": "server-only",
"server_only": True,
"brain_base_url": BRAIN_BASE_URL,
"local_models": False,
"goal": os.environ.get("MINECRAFT_GAME_GOAL") or None,
"roles": {
"chat": f"{BRAIN_BASE_URL}/chat/completions",
"embeddings": f"{BRAIN_BASE_URL}/embeddings",
},
"backend": "cpu-only",
"n_gpu_layers": 0,
"supervisor_recoveries": _supervisor_recoveries,
"supervisor_last_action": _supervisor_last_action,
"supervisor_last_check": _supervisor_last_check or None,
}
return {
"status": "ok",
"ready": _model is not None,
"loading": _loading,
"model": MODEL_ALIAS,
"aux_model": AUX_MODEL_ALIAS,
"aux_ready": _aux_model is not None,
"aux_loading": _aux_loading,
"aux_error": _aux_model_error or None,
"embedding_model": EMBED_ALIAS,
"embedding_ready": _embed_model is not None,
"embedding_loading": _embed_loading,
"embedding_error": _embed_error or None,
"backend": "cpu-only",
"n_gpu_layers": 0,
"supervisor_recoveries": _supervisor_recoveries,
"supervisor_last_action": _supervisor_last_action,
"supervisor_last_check": _supervisor_last_check or None,
"error": _model_error or None,
}
@api.get("/status")
def status():
memory = psutil.virtual_memory()
process = psutil.Process()
if SERVER_ONLY:
return {
"status": "ok",
"mode": "server-only",
"server_only": True,
"brain_base_url": BRAIN_BASE_URL,
"local_models": False,
"roles": {
"chat": f"{BRAIN_BASE_URL}/chat/completions",
"embeddings": f"{BRAIN_BASE_URL}/embeddings",
},
"backend": "cpu-only",
"cpu_only": _CPU_ONLY["cpu_only"],
"gpu_offload_supported": _CPU_ONLY["gpu_offload_supported"],
"gpu_backend_libraries": _CPU_ONLY["gpu_backend_libraries"],
"cuda_libraries_present": _CPU_ONLY["cuda_libraries_present"],
"gpu_enabled": bool(_CPU_ONLY["gpu_offload_supported"] is True),
"n_gpu_layers": _CPU_ONLY["n_gpu_layers_requested"],
"threads": THREADS,
"threads_batch": THREADS_BATCH,
"effective_cpus": effective_cpus(),
"cgroup_cpu_quota": CGROUP_CPU_QUOTA,
"player_mode": PLAYER_MODE,
"memory_limit_gib": MEMORY_LIMIT_GIB,
"system_memory_total_gib": round(memory.total / (1024**3), 2),
"system_memory_available_gib": round(memory.available / (1024**3), 2),
"process_rss_gib": round(process.memory_info().rss / (1024**3), 2),
}
return {
"status": "ok",
"ready": _model is not None,
"loading": _loading,
"model": MODEL_ALIAS,
"model_repo": MODEL_REPO,
"model_file": MODEL_FILE,
"model_revision": MODEL_REVISION,
"quantization": "Q4_K_M",
"model_family": "Granite-4.0-H-Tiny",
"total_params_b": 7.0,
"active_params_b": 1.0,
"reasoning_policy": "voyager-granite-h-tiny-cpu-low-latency",
"aux_model": AUX_MODEL_ALIAS,
"aux_model_repo": AUX_MODEL_REPO,
"aux_model_file": AUX_MODEL_FILE,
"aux_model_revision": AUX_MODEL_REVISION,
"aux_context": AUX_CTX,
"aux_ready": _aux_model is not None,
"aux_loading": _aux_loading,
"aux_load_seconds": round(_aux_model_load_seconds, 3),
"aux_loaded_at": _aux_model_loaded_at or None,
"aux_error": _aux_model_error or None,
"embedding_repo": EMBED_REPO,
"embedding_file": EMBED_FILE,
"embedding_revision": EMBED_REVISION,
"embedding_model": EMBED_ALIAS,
"embedding_ready": _embed_model is not None,
"embedding_loading": _embed_loading,
"embedding_load_seconds": round(_embed_load_seconds, 3),
"embedding_error": _embed_error or None,
"backend": "cpu-only",
# Derived from the linked binary and the backend libraries actually
# present, not a literal. See cpu_backend.cpu_only_report().
"cpu_only": _CPU_ONLY["cpu_only"],
"gpu_offload_supported": _CPU_ONLY["gpu_offload_supported"],
"gpu_backend_libraries": _CPU_ONLY["gpu_backend_libraries"],
"cuda_libraries_present": _CPU_ONLY["cuda_libraries_present"],
"gpu_enabled": bool(_CPU_ONLY["gpu_offload_supported"] is True),
"n_gpu_layers": _CPU_ONLY["n_gpu_layers_requested"],
"context": CTX,
"context_mode": "native-no-yarn",
"threads": THREADS,
"threads_batch": THREADS_BATCH,
"effective_cpus": effective_cpus(),
"cgroup_cpu_quota": CGROUP_CPU_QUOTA,
"batch": BATCH,
"ubatch": UBATCH,
"flash_attn": FLASH_ATTN,
"aux_context": AUX_CTX,
"default_max_tokens": DEFAULT_MAX_TOKENS,
"max_tokens_cap": MAX_TOKENS_CAP,
"player_mode": PLAYER_MODE,
"memory_limit_gib": MEMORY_LIMIT_GIB,
"system_memory_total_gib": round(memory.total / (1024**3), 2),
"system_memory_available_gib": round(memory.available / (1024**3), 2),
"process_rss_gib": round(process.memory_info().rss / (1024**3), 2),
"model_load_seconds": round(_model_load_seconds, 3),
"model_loaded_at": _model_loaded_at or None,
"model_error": _model_error or None,
}
@api.get("/v1/models")
def models(authorization: str | None = Header(default=None)):
_auth(authorization)
now = int(time.time())
if SERVER_ONLY:
# Advertising a chat model this Space will refuse to serve sends clients
# to the wrong host on the strength of a successful discovery call. The
# list stays empty and the response names the real endpoint.
return {
"object": "list",
"data": [],
"server_only": True,
"brain_base_url": BRAIN_BASE_URL,
}
return {
"object": "list",
"data": [
*[
{"id": alias, "object": "model", "created": now, "owned_by": "Qwen"}
for alias in sorted(MODEL_ALIASES | AUX_MODEL_ALIASES)
],
{"id": EMBED_ALIAS, "object": "model", "created": now, "owned_by": "Qwen"},
],
}
@api.post("/v1/embeddings")
async def embeddings(request: Request, authorization: str | None = Header(default=None)):
_auth(authorization)
if SERVER_ONLY:
# Refuse before parsing the body. Without this the request would fall
# through to `_get_embed_model()`, which raises from `_load_embed_model`
# and is reported as a generic 503 -- a status a caller reads as "the
# server is briefly busy, retry me" rather than "you are calling the
# wrong host". The detail names the right host instead.
raise HTTPException(
status_code=503,
detail=(
"this Space runs the Minecraft server only; embeddings are "
f"served at {BRAIN_BASE_URL}"
),
)
try:
payload = await request.json()
except Exception as exc:
raise HTTPException(status_code=400, detail=f"Invalid JSON: {str(exc)[:160]}") from exc
inputs = payload.get("input")
texts = _normalize_embedding_inputs(inputs)
if not texts:
raise HTTPException(status_code=400, detail="input must not be empty")
requested_model = str(payload.get("model") or EMBED_ALIAS)
try:
model = await run_in_threadpool(_get_embed_model)
def _run_embedding():
with _embed_lock:
vectors, token_count = model.embed(texts, normalize=True, return_count=True)
return {
"object": "list",
"data": [
{"object": "embedding", "embedding": vector, "index": index}
for index, vector in enumerate(vectors)
],
"model": requested_model,
"usage": {"prompt_tokens": token_count, "total_tokens": token_count},
}
return JSONResponse(await run_in_threadpool(_run_embedding))
except RuntimeError as exc:
raise HTTPException(status_code=503, detail=str(exc)[:800]) from exc
@api.get("/minecraft/status")
def minecraft_status():
data = STACK.status().copy()
data.pop("microsoft_device_code", None)
data.pop("log_tail", None)
return data
@api.get("/voyager/status")
def voyager_status():
data = VOYAGER.status().copy()
data.pop("log_tail", None)
return data
@api.get("/voyager/log")
def voyager_log(lines: int = 200, find: str | None = None):
"""Expose the runner log.
/voyager/status deliberately drops log_tail: that payload is polled by the
dashboard and the log is large. Dropping it there left the loop's own
traceback unobservable from outside the Space -- the Hugging Face log
stream can lag hours behind, so a wedged runner looked identical to a
healthy one. This endpoint reads the same file the status builds from, on
demand, without changing the status contract.
"""
lines = max(1, min(int(lines or 200), 5000))
tail = VOYAGER.tail(lines)
if find:
kept = [row for row in tail.splitlines() if find in row]
tail = "\n".join(kept)
return {
"lines": lines,
"find": find,
"bytes": len(tail),
"log": tail,
}
@api.get("/voyager/probe")
def voyager_probe(seconds: int = 6):
"""Is the runner child alive, and is it using CPU or blocked?
A wedged runner and a slow one look identical from the outside: both keep
the process alive and write nothing. Sampling utime/stime across a short
interval separates them. A process whose CPU counters do not move at all
over several seconds while producing no output is blocked on a syscall, not
thinking -- which is the distinction that took three rounds to establish by
reading logs alone.
"""
seconds = max(1, min(int(seconds or 6), 30))
snapshot = VOYAGER.probe_runner(seconds)
return snapshot
@api.get("/gemini-bot/status")
def gemini_bot_status():
return GEMINI_BOT.status()
@api.get("/opencode-bot/status")
@api.get("/altoclef/status")
def altoclef_status():
return ALTOCLEF.status()
@api.get("/opencode-bot/log")
def altoclef_log(lines: int = 100):
return {"log": ALTOCLEF.tail(lines)}
@api.post("/v1/chat/completions")
async def chat(request: Request, authorization: str | None = Header(default=None)):
_auth(authorization)
try:
payload = await request.json()
except Exception as exc:
raise HTTPException(status_code=400, detail=f"Invalid JSON: {str(exc)[:160]}") from exc
if not isinstance(payload.get("messages"), list):
raise HTTPException(status_code=400, detail="messages must be a list")
requested_model = str(payload.get("model") or MODEL_ALIAS)
if requested_model not in (MODEL_ALIASES | AUX_MODEL_ALIASES):
raise HTTPException(status_code=404, detail=f"Unknown model: {requested_model}")
if payload.get("stream"):
return StreamingResponse(
_stream(payload),
media_type="text/event-stream",
headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"},
)
try:
return JSONResponse(await run_in_threadpool(_nonstream, payload))
except RuntimeError as exc:
raise HTTPException(status_code=503, detail=str(exc)[:800]) from exc
with gr.Blocks(title="Programa Voyager LLM Endpoint") as demo:
gr.Markdown(
"# Programa — Voyager LLM Endpoint\n"
"**Granite-4.0-H-Tiny Q4_K_M**, 7B total / ~1B ativo, contexto 8K, 16 threads de decode / 16 de prefill, inferencia 100% CPU."
)
output = gr.JSON(label="IA / CPU")
gr.Button("Status IA / CPU").click(status, outputs=output)
gr.Markdown("## Minecraft + Xbox Friends")
mc_output = gr.JSON(label="Minecraft")
with gr.Row():
gr.Button("Status Minecraft").click(STACK.status, outputs=mc_output)
gr.Button("Iniciar Minecraft", variant="primary").click(STACK.start_async, outputs=mc_output)
gr.Button("Parar Minecraft").click(STACK.stop, outputs=mc_output)
gr.Markdown("## Voyager (alternativa manual)\nO Voyager continua disponível se você quiser ligar manualmente; o Naldo usa o AltoClef.")
voyager_output = gr.JSON(label="Voyager")
with gr.Row():
gr.Button("Status Voyager").click(VOYAGER.status, outputs=voyager_output)
gr.Button("Iniciar Voyager").click(VOYAGER.start_async, outputs=voyager_output)
gr.Button("Parar Voyager").click(VOYAGER.stop, outputs=voyager_output)
gr.Markdown("## Naldo — jogador autônomo\nO Naldo joga survival sozinho para zerar o jogo (mapa e inventário atual mantidos).")
altoclef_output = gr.JSON(label="Naldo")
with gr.Row():
gr.Button("Status Naldo").click(ALTOCLEF.status, outputs=altoclef_output)
gr.Button("Iniciar Naldo", variant="primary").click(ALTOCLEF.start_async, outputs=altoclef_output)
gr.Button("Parar Naldo").click(ALTOCLEF.stop, outputs=altoclef_output)
if __name__ == "__main__":
print(
f"[boot] cpu-only model={MODEL_REPO}/{MODEL_FILE} threads={THREADS} "
f"ctx={CTX} n_gpu_layers=0 mem_limit_gib={MEMORY_LIMIT_GIB}",
flush=True,
)
# Keep the Gradio SDK app on a private port; the public OpenAI-compatible API
# owns PORT=7860 exactly as in the previously stable deployment.
demo.queue(default_concurrency_limit=1)
demo.launch(
server_name="0.0.0.0",
server_port=GRADIO_PORT,
prevent_thread_lock=True,
ssr_mode=False,
show_error=True,
)
uvicorn.run(api, host="0.0.0.0", port=PORT, log_level="info")