Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions agent-mcp-servers/vlm-mcp/vlm_mcp_server/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@
load_models_config_from_dict,
make_vlm,
)
from xr_ai_models.config import KIND_OPENAI_COMPAT
from xr_ai_models.config import KIND_OPENAI_COMPAT, parse_bool
from xr_ai_models.protocols import VLMService


Expand All @@ -89,7 +89,7 @@ def _make_vlm_from_cfg(cfg: dict[str, Any]) -> tuple[VLMService, float]:
models_block: dict[str, Any] | None = cfg.get("models")
vlm_server: str | None = cfg.get("vlm_server")
vlm_request_timeout_s = float(cfg.get("vlm_request_timeout_s", 60.0))
enable_thinking = bool(cfg.get("enable_thinking", False))
enable_thinking = parse_bool(cfg.get("enable_thinking", False), "enable_thinking")

if models_block:
vlm_entry = dict(models_block.get("vlm") or {})
Expand Down
21 changes: 20 additions & 1 deletion agent-sdk/xr-ai-models/xr_ai_models/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,25 @@

KIND_OPENAI_COMPAT: ModelKind = "openai_compat"

_TRUE_BOOL_STRINGS = {"1", "true", "yes", "on"}
_FALSE_BOOL_STRINGS = {"0", "false", "no", "off"}


def parse_bool(value: object, key: str) -> bool:
"""Parse a config boolean without treating every non-empty string as true."""
if isinstance(value, bool):
return value
if isinstance(value, str):
normalized = value.strip().lower()
if normalized in _TRUE_BOOL_STRINGS:
return True
if normalized in _FALSE_BOOL_STRINGS:
return False
raise ValueError(
f"{key} must be a boolean or one of "
f"{sorted(_TRUE_BOOL_STRINGS | _FALSE_BOOL_STRINGS)} (got {value!r})"
)


@dataclass(frozen=True)
class LLMSpec:
Expand Down Expand Up @@ -171,7 +190,7 @@ def _construct(category: Category, body: dict[str, Any]) -> Spec:
"base_url": _require_str(body, "base_url"),
# Remote endpoints (e.g. hosted NIM) have no local /health route; set
# ``health_check: false`` so the worker readiness gate doesn't block.
"health_check": bool(body.get("health_check", True)),
"health_check": parse_bool(body.get("health_check", True), "health_check"),
}
if "api_key_env" in body:
common["api_key_env"] = body["api_key_env"]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
from xr_ai_vllm import (
DEFAULT_IMAGE,
load_config,
parse_bool,
resolve_model_cache,
serve,
setup_hf_env,
Expand Down Expand Up @@ -74,9 +75,12 @@ def run() -> None:
tp_size = int(cfg.get("tensor_parallel_size", _DEFAULT_TP))
max_ctx = int(cfg.get("max_model_len", _DEFAULT_CTX))
gpu_mem = float(cfg.get("gpu_memory_utilization", _DEFAULT_GPU_MEM))
enforce_eager = bool(cfg.get("enforce_eager", _DEFAULT_EAGER))
enforce_eager = parse_bool(cfg.get("enforce_eager", _DEFAULT_EAGER), "enforce_eager")
tool_call_parser = cfg.get("tool_call_parser", _DEFAULT_TOOL_CALL_PARSER)
enable_tool_choice = bool(cfg.get("enable_tool_choice", _DEFAULT_ENABLE_TOOL_CHOICE))
enable_tool_choice = parse_bool(
cfg.get("enable_tool_choice", _DEFAULT_ENABLE_TOOL_CHOICE),
"enable_tool_choice",
)
backend = cfg.get("vllm_backend", "pip")
image = cfg.get("vllm_image", DEFAULT_IMAGE)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
DEFAULT_IMAGE,
gpu_compute_major,
load_config,
parse_bool,
resolve_model_cache,
serve,
setup_hf_env,
Expand Down Expand Up @@ -100,7 +101,7 @@ def run() -> None:
tp_size = int(cfg.get("tensor_parallel_size", _DEFAULT_TP))
max_ctx = int(cfg.get("max_model_len", _DEFAULT_CTX))
gpu_mem = float(cfg.get("gpu_memory_utilization", _DEFAULT_GPU_MEM))
enforce_eager = bool(cfg.get("enforce_eager", _DEFAULT_EAGER))
enforce_eager = parse_bool(cfg.get("enforce_eager", _DEFAULT_EAGER), "enforce_eager")
parser_url = cfg.get("parser_url", _PARSER_URL_DEFAULT)
backend = cfg.get("vllm_backend", "pip")
image = cfg.get("vllm_image", DEFAULT_IMAGE)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
DEFAULT_IMAGE,
gpu_compute_major,
load_config,
parse_bool,
resolve_model_cache,
serve,
setup_hf_env,
Expand Down Expand Up @@ -81,7 +82,8 @@ def run() -> None:
# nvidia-smi queries the right device.
cuda_devices = setup_hf_env(cfg, model_cache)

if cfg.get("use_bf16", False):
use_bf16 = parse_bool(cfg.get("use_bf16", False), "use_bf16")
if use_bf16:
model = cfg.get("model_bf16", _MODEL_BF16)
use_kv_fp8 = False
logger.info("use_bf16=true → {}", model)
Expand All @@ -104,7 +106,7 @@ def run() -> None:
tp_size = int(cfg.get("tensor_parallel_size", _DEFAULT_TP))
max_ctx = int(cfg.get("max_model_len", _DEFAULT_CTX))
gpu_mem = float(cfg.get("gpu_memory_utilization", _DEFAULT_GPU_MEM))
enforce_eager = bool(cfg.get("enforce_eager", _DEFAULT_EAGER))
enforce_eager = parse_bool(cfg.get("enforce_eager", _DEFAULT_EAGER), "enforce_eager")
prune_rate = float(cfg.get("video_pruning_rate", _DEFAULT_PRUNE))
video_fps = int(cfg.get("video_fps", _DEFAULT_FPS))
video_frames = int(cfg.get("video_num_frames", _DEFAULT_FRAMES))
Expand Down
20 changes: 19 additions & 1 deletion ai-services/tts/piper/piper_tts_server/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,24 @@
# Callers/tests can treat this as retry-or-skip rather than a hard failure.
_EXIT_VOICE_UNAVAILABLE = 3

_TRUE_BOOL_STRINGS = {"1", "true", "yes", "on"}
_FALSE_BOOL_STRINGS = {"0", "false", "no", "off"}


def _parse_bool(value: object, key: str) -> bool:
if isinstance(value, bool):
return value
if isinstance(value, str):
normalized = value.strip().lower()
if normalized in _TRUE_BOOL_STRINGS:
return True
if normalized in _FALSE_BOOL_STRINGS:
return False
raise ValueError(
f"{key} must be a boolean or one of "
f"{sorted(_TRUE_BOOL_STRINGS | _FALSE_BOOL_STRINGS)} (got {value!r})"
)


def _resolve_model_cache(cfg: dict, yaml_dir: Path) -> Path:
raw = cfg.get("model_cache", "../models")
Expand Down Expand Up @@ -202,7 +220,7 @@ def _build_app(cfg: dict, model_cache: Path):
from pydantic import BaseModel

voice_name = cfg["voice"]
use_cuda = bool(cfg.get("use_cuda", False))
use_cuda = _parse_bool(cfg.get("use_cuda", False), "use_cuda")
backend = _PiperBackend(voice_name, model_cache, use_cuda)

app = FastAPI(title="Piper TTS Server", version="0.1.0")
Expand Down
3 changes: 2 additions & 1 deletion ai-services/vlm-server/vlm_server/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
from xr_ai_vllm import (
DEFAULT_IMAGE,
load_config,
parse_bool,
resolve_model_cache,
serve,
setup_hf_env,
Expand Down Expand Up @@ -81,7 +82,7 @@ def run() -> None:
tp_size = int(cfg.get("tensor_parallel_size", _DEFAULT_TP))
max_ctx = int(cfg.get("max_model_len", _DEFAULT_CTX))
gpu_mem = float(cfg.get("gpu_memory_utilization", _DEFAULT_GPU_MEM))
enforce_eager = bool(cfg.get("enforce_eager", _DEFAULT_EAGER))
enforce_eager = parse_bool(cfg.get("enforce_eager", _DEFAULT_EAGER), "enforce_eager")
max_images = int(cfg.get("max_images_per_prompt", _DEFAULT_MAX_IMAGES))
max_videos = int(cfg.get("max_videos_per_prompt", _DEFAULT_MAX_VIDEOS))
backend = cfg.get("vllm_backend", "pip")
Expand Down
24 changes: 24 additions & 0 deletions tests/test_models_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,30 @@ def test_health_check_defaults_true_and_parses_false(tmp_path) -> None:
assert nim.base_url == "https://integrate.api.nvidia.com"


def test_health_check_parses_quoted_false(tmp_path) -> None:
cfg = load_models_config(_write(tmp_path, """
remote_llm:
kind: openai_compat
category: llm
base_url: https://integrate.api.nvidia.com
model_name: meta/llama-3.1-8b-instruct
health_check: "false"
"""))
assert cfg.llm("remote_llm").health_check is False


def test_health_check_rejects_unknown_string(tmp_path) -> None:
with pytest.raises(ValueError, match="health_check"):
load_models_config(_write(tmp_path, """
remote_llm:
kind: openai_compat
category: llm
base_url: https://integrate.api.nvidia.com
model_name: meta/llama-3.1-8b-instruct
health_check: maybe
"""))


def test_vlm_preset(tmp_path) -> None:
cfg = load_models_config(_write(tmp_path, """
vlm:
Expand Down
22 changes: 22 additions & 0 deletions tests/test_piper_tts.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from __future__ import annotations

import asyncio
import importlib.util
import json
import os
import shutil
Expand Down Expand Up @@ -50,6 +51,27 @@
_EXIT_VOICE_UNAVAILABLE = 3


def _load_piper_main_module():
spec = importlib.util.spec_from_file_location(
"piper_tts_server_main",
_PIPER_PROJECT / "piper_tts_server" / "__main__.py",
)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod


async def test_piper_config_parses_quoted_use_cuda_false() -> None:
mod = _load_piper_main_module()
assert mod._parse_bool("false", "use_cuda") is False


async def test_piper_config_rejects_unknown_use_cuda_string() -> None:
mod = _load_piper_main_module()
with pytest.raises(ValueError, match="use_cuda"):
mod._parse_bool("sometimes", "use_cuda")


class _ServerExited(Exception):
"""Raised when piper_tts_server exits before binding its port.

Expand Down
27 changes: 26 additions & 1 deletion tests/test_vllm_lifecycle.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,41 @@
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

"""Unit tests for xr_ai_vllm._lifecycle pure helpers."""
"""Unit tests for xr_ai_vllm pure helpers."""
from __future__ import annotations

from unittest.mock import MagicMock, patch

import pytest

from xr_ai_vllm import parse_bool
from xr_ai_vllm._lifecycle import health_ok, health_url, wait_until_healthy


class TestParseBool:
@pytest.mark.parametrize(
("value", "expected"),
[
(True, True),
(False, False),
("true", True),
("YES", True),
("1", True),
("on", True),
("false", False),
("No", False),
("0", False),
(" off ", False),
],
)
def test_accepts_booleans_and_known_strings(self, value, expected):
assert parse_bool(value, "enforce_eager") is expected

def test_rejects_unknown_string(self):
with pytest.raises(ValueError, match="enforce_eager"):
parse_bool("sometimes", "enforce_eager")


class TestHealthUrl:
def test_always_probes_localhost(self):
# The host param is intentionally ignored — always 127.0.0.1.
Expand Down
35 changes: 35 additions & 0 deletions tests/test_vlm_mcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,41 @@ async def test_make_vlm_from_cfg_new_models_block(mock_vlm, png_path: Path):
assert payload["chat_template_kwargs"] == {"enable_thinking": False}


async def test_make_vlm_from_cfg_parses_quoted_enable_thinking_false(mock_vlm, png_path: Path):
server, base_url = mock_vlm
server.answer = "quoted false"

vlm, _ = _make_vlm_from_cfg({
"models": {
"vlm": {
"kind": "preset:cosmos_vlm",
"base_url": base_url,
},
},
"enable_thinking": "false",
})
mcp = build_mcp(vlm)
try:
result = await mcp.call_tool(
"ask_image",
{"question": "cosmos q", "image_path": str(png_path)},
)
finally:
await vlm.close()

assert result.structured_content["result"] == "quoted false"
payload = server.requests[0]
assert payload["chat_template_kwargs"] == {"enable_thinking": False}


async def test_make_vlm_from_cfg_rejects_unknown_enable_thinking_string():
with pytest.raises(ValueError, match="enable_thinking"):
_make_vlm_from_cfg({
"vlm_server": "http://localhost:8100",
"enable_thinking": "sometimes",
})


async def test_make_vlm_from_cfg_missing_required_keys_raises():
"""Neither models: nor vlm_server: present → ValueError."""
with pytest.raises(ValueError, match="must specify either"):
Expand Down
15 changes: 15 additions & 0 deletions tests/test_xr_ai_voicegate.py
Original file line number Diff line number Diff line change
Expand Up @@ -799,6 +799,21 @@ def test_load_voice_gate_config_null_phrases_normalizes_to_empty(tmp_path: pathl
assert cfg.followup_grace_s == 3.0


def test_load_voice_gate_config_parses_quoted_listening_chime_false(tmp_path: pathlib.Path):
p = tmp_path / "voice_gate.yaml"
p.write_text('magic_phrases: agent\nlistening_chime: "false"\n')
cfg = load_voice_gate_config(p)
assert cfg.magic_phrases == ("agent",)
assert cfg.listening_chime is False


def test_load_voice_gate_config_rejects_unknown_listening_chime_string(tmp_path: pathlib.Path):
p = tmp_path / "voice_gate.yaml"
p.write_text("listening_chime: sometimes\n")
with pytest.raises(ValueError, match="listening_chime"):
load_voice_gate_config(p)


def test_load_voice_gate_config_strips_whitespace_and_drops_empty(tmp_path: pathlib.Path):
"""Case 39: phrase entries are stripped, and empty entries (after
strip) are dropped — same normalization the inline parser ran."""
Expand Down
2 changes: 2 additions & 0 deletions utils/xr-ai-vllm/xr_ai_vllm/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
from ._config import (
gpu_compute_major,
load_config,
parse_bool,
resolve_model_cache,
setup_hf_env,
)
Expand Down Expand Up @@ -237,6 +238,7 @@ def stop_persistent_servers(
"DEFAULT_IMAGE",
"resolve_model_cache",
"load_config",
"parse_bool",
"setup_hf_env",
"gpu_compute_major",
]
19 changes: 19 additions & 0 deletions utils/xr-ai-vllm/xr_ai_vllm/_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,25 @@

log = logging.getLogger(__name__)

_TRUE_BOOL_STRINGS = {"1", "true", "yes", "on"}
_FALSE_BOOL_STRINGS = {"0", "false", "no", "off"}


def parse_bool(value: object, key: str) -> bool:
"""Parse a config boolean without treating every non-empty string as true."""
if isinstance(value, bool):
return value
if isinstance(value, str):
normalized = value.strip().lower()
if normalized in _TRUE_BOOL_STRINGS:
return True
if normalized in _FALSE_BOOL_STRINGS:
return False
raise ValueError(
f"{key} must be a boolean or one of "
f"{sorted(_TRUE_BOOL_STRINGS | _FALSE_BOOL_STRINGS)} (got {value!r})"
)


def resolve_model_cache(cfg: dict, yaml_dir: Path, *, default: str) -> Path:
"""Resolve ``model_cache`` (relative to the YAML dir) and ensure it exists."""
Expand Down
Loading