Skip to content
Closed
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
74 changes: 74 additions & 0 deletions tests/tools/test_tts_provider_authority.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
"""Regression tests for model-boundary TTS provider authority.

Programmatic callers may select a provider, but model-generated tool arguments
must not override ``tts.provider``. Platform hints are advisory, so enforcement
belongs in the registered schema and handler.
"""

import pytest

from tools import tts_tool
from tools.registry import registry


class _ProviderProbe(Exception):
def __init__(self, provider: str):
super().__init__(provider)
self.provider = provider


def test_programmatic_provider_override_remains_supported(monkeypatch):
"""Trusted Python callers retain upstream's explicit provider-selection API."""
seen = []
monkeypatch.setattr(tts_tool, "_load_tts_config", lambda: {"provider": "xai"})
monkeypatch.setattr(
tts_tool,
"_resolve_max_text_length",
lambda provider, _config: seen.append(provider) or 4096,
)
monkeypatch.setattr(tts_tool, "_split_text_for_tts", lambda _text, _limit: [])

tts_tool.text_to_speech_tool("hello", provider="openai")

assert seen == ["openai"]


def test_inner_dispatch_preserves_programmatic_provider_override(monkeypatch):
"""The explicit provider must reach synthesis rather than reverting to config."""
monkeypatch.setattr(
tts_tool,
"_resolve_max_text_length",
lambda provider, _config: (_ for _ in ()).throw(_ProviderProbe(provider)),
)

with pytest.raises(_ProviderProbe) as caught:
tts_tool._text_to_speech_single(
"hello",
provider="openai",
tts_config_override={"provider": "xai"},
)

assert caught.value.provider == "openai"


def test_registered_handler_discards_requested_provider(monkeypatch):
"""Even a rogue payload must not cross the model-facing handler boundary."""
seen = {}
monkeypatch.setattr(
tts_tool,
"text_to_speech_tool",
lambda **kwargs: seen.update(kwargs) or "{}",
)

entry = registry.get_entry("text_to_speech")
assert entry is not None
entry.handler({"text": "hello", "provider": "openai"})

assert seen["provider"] is None


def test_registered_model_schema_cannot_request_provider_override():
"""Provider selection belongs to config, not model-generated tool arguments."""
entry = registry.get_entry("text_to_speech")
assert entry is not None
assert "provider" not in entry.schema["parameters"]["properties"]
22 changes: 7 additions & 15 deletions tools/tts_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -3521,7 +3521,7 @@ def text_to_speech_tool(
output_path: Optional custom save path.
speed: Optional playback speed multiplier (0.25-4.0).
instructions: Optional voice-design guidance (tone, emotion, pacing).
provider: Optional TTS provider override.
provider: Optional TTS provider override for programmatic callers.

Returns:
str: JSON result with success, file_path, file_paths, and MEDIA tag.
Expand All @@ -3548,7 +3548,8 @@ def text_to_speech_tool(
tts_config = dict(tts_config) # shallow copy to avoid mutating the cache
tts_config["speed"] = clamped

# Allow per-call provider override; fall back to the configured default.
# Allow per-call provider override for trusted programmatic callers; the
# model-facing registry handler deliberately never forwards this argument.
if provider:
provider = provider.lower().strip()
else:
Expand Down Expand Up @@ -4518,18 +4519,8 @@ def _check(importer, label):
"description": (
"Optional voice-design guidance: tone, emotion, pacing, accent, "
"whispering, impressions (e.g. 'Speak in a cheerful, excited whisper'). "
"Forwarded to the OpenAI backend (gpt-4o-mini-tts and OpenAI-compatible "
"voice-design servers). Silently ignored by backends that don't support it."
)
},
"provider": {
"type": "string",
"description": (
"Optional TTS provider override. Accepts built-in names "
"(edge, openai, elevenlabs, minimax, xai, mistral, gemini, "
"neutts, kittentts, piper), user-declared command provider "
"names from tts.providers.<name>, or plugin-registered names. "
"When omitted, the configured tts.provider from config.yaml is used."
"Only used when the configured tts.provider supports voice-design "
"instructions. Silently ignored by backends that don't support it."
)
}
},
Expand All @@ -4546,7 +4537,8 @@ def _check(importer, label):
output_path=args.get("output_path"),
speed=args.get("speed"),
instructions=args.get("instructions"),
provider=args.get("provider")),
# provider deliberately not taken from model args — house config only
provider=None),
check_fn=check_tts_requirements,
emoji="🔊",
)