Merge remote-tracking branch 'origin/main' into feat/user-onboarding

# Conflicts:
#	docs/api-reference/openapi.json
#	sdk/python/src/dograh_sdk/_generated_models.py
#	ui/src/client/index.ts
#	ui/src/components/AIModelConfigurationV2Editor.tsx
This commit is contained in:
Abhishek Kumar 2026-06-17 19:19:20 +05:30
commit 5559ed686f
44 changed files with 2155 additions and 321 deletions

View file

@ -0,0 +1,131 @@
from types import SimpleNamespace
from unittest.mock import patch
from api.services.configuration.check_validity import UserConfigurationValidator
from api.services.configuration.registry import (
REGISTRY,
HuggingFaceLLMConfiguration,
HuggingFaceSTTConfiguration,
ServiceProviders,
ServiceType,
)
from api.services.pipecat.service_factory import (
create_llm_service,
create_stt_service,
)
def test_huggingface_stt_configuration_defaults_and_registry():
config = HuggingFaceSTTConfiguration(api_key="hf_test")
assert config.provider == ServiceProviders.HUGGINGFACE
assert config.model == "openai/whisper-large-v3-turbo"
assert config.base_url == "https://router.huggingface.co/hf-inference"
assert config.return_timestamps is False
assert (
REGISTRY[ServiceType.STT][ServiceProviders.HUGGINGFACE]
is HuggingFaceSTTConfiguration
)
def test_huggingface_llm_configuration_defaults_and_registry():
config = HuggingFaceLLMConfiguration(api_key="hf_test")
assert config.provider == ServiceProviders.HUGGINGFACE
assert config.model == "openai/gpt-oss-120b:cerebras"
assert config.base_url == "https://router.huggingface.co/v1"
assert (
REGISTRY[ServiceType.LLM][ServiceProviders.HUGGINGFACE]
is HuggingFaceLLMConfiguration
)
def test_create_huggingface_llm_service_uses_openai_compatible_router():
user_config = SimpleNamespace(
llm=SimpleNamespace(
provider=ServiceProviders.HUGGINGFACE.value,
api_key="hf_test",
model="deepseek-ai/DeepSeek-R1:fastest",
base_url="https://router.huggingface.co/v1",
bill_to="demo-org",
)
)
with patch(
"api.services.pipecat.service_factory.HuggingFaceLLMService"
) as mock_service:
create_llm_service(user_config)
assert mock_service.call_count == 1
kwargs = mock_service.call_args.kwargs
assert kwargs["api_key"] == "hf_test"
assert kwargs["base_url"] == "https://router.huggingface.co/v1"
assert kwargs["bill_to"] == "demo-org"
assert kwargs["settings"].model == "deepseek-ai/DeepSeek-R1:fastest"
assert kwargs["settings"].temperature == 0.1
def test_create_huggingface_stt_service_uses_hosted_defaults():
user_config = SimpleNamespace(
stt=SimpleNamespace(
provider=ServiceProviders.HUGGINGFACE.value,
api_key="hf_test",
model="openai/whisper-large-v3-turbo",
base_url="https://router.huggingface.co/hf-inference",
bill_to="demo-org",
return_timestamps=True,
)
)
audio_config = SimpleNamespace(transport_in_sample_rate=16000)
with patch(
"api.services.pipecat.service_factory.HuggingFaceSTTService"
) as mock_service:
create_stt_service(user_config, audio_config)
assert mock_service.call_count == 1
kwargs = mock_service.call_args.kwargs
assert kwargs["api_key"] == "hf_test"
assert kwargs["base_url"] == "https://router.huggingface.co/hf-inference"
assert kwargs["bill_to"] == "demo-org"
assert kwargs["sample_rate"] == 16000
assert kwargs["settings"].model == "openai/whisper-large-v3-turbo"
assert kwargs["settings"].return_timestamps is True
def test_validator_accepts_huggingface_stt_token_format():
validator = UserConfigurationValidator()
assert (
validator._validate_service(
HuggingFaceSTTConfiguration(api_key="hf_test"),
"stt",
)
== []
)
assert (
validator._validate_service(
HuggingFaceLLMConfiguration(api_key="hf_test"),
"llm",
)
== []
)
def test_validator_rejects_non_huggingface_token_format():
validator = UserConfigurationValidator()
errors = validator._validate_service(
HuggingFaceSTTConfiguration(api_key="not-hf-token"),
"stt",
)
assert errors == [
{
"model": "stt",
"message": (
"Invalid Hugging Face API token format. Use a token that starts with "
"'hf_' and has Inference Providers permission."
),
}
]

View file

@ -0,0 +1,31 @@
from types import SimpleNamespace
from unittest.mock import patch
from pipecat.services.openai._constants import OPENAI_SAMPLE_RATE
from api.services.configuration.registry import ServiceProviders
from api.services.pipecat.service_factory import create_tts_service
def test_create_openai_tts_service_uses_openai_pcm_sample_rate():
user_config = SimpleNamespace(
tts=SimpleNamespace(
provider=ServiceProviders.OPENAI.value,
api_key="test-key",
model="gpt-4o-mini-tts",
voice="alloy",
base_url=None,
)
)
audio_config = SimpleNamespace(
transport_out_sample_rate=16000,
transport_in_sample_rate=16000,
)
with patch("api.services.pipecat.service_factory.OpenAITTSService") as mock_service:
create_tts_service(user_config, audio_config)
assert mock_service.call_count == 1
kwargs = mock_service.call_args.kwargs
assert kwargs["sample_rate"] == OPENAI_SAMPLE_RATE
assert kwargs["settings"].model == "gpt-4o-mini-tts"

View file

@ -0,0 +1,30 @@
from api.routes.s3_signed_url import (
_extract_legacy_workflow_run_id,
_extract_org_id_from_key,
)
def test_split_recording_keys_are_workflow_run_artifacts_not_org_keys():
assert _extract_legacy_workflow_run_id("recordings/1855/user.wav") == 1855
assert _extract_legacy_workflow_run_id("recordings/1855/bot.wav") == 1855
assert _extract_org_id_from_key("recordings/1855/user.wav") is None
assert _extract_org_id_from_key("recordings/1855/bot.wav") is None
def test_legacy_recording_keys_do_not_fall_through_to_org_scoped_auth():
assert _extract_legacy_workflow_run_id("recordings/1855.wav") == 1855
assert _extract_legacy_workflow_run_id("recordings/1855/other.wav") is None
assert _extract_org_id_from_key("recordings/1855.wav") is None
assert _extract_org_id_from_key("recordings/1855/other.wav") is None
def test_known_org_scoped_keys_extract_org_id():
assert _extract_org_id_from_key("campaigns/42/source.csv") == 42
assert _extract_org_id_from_key("knowledge_base/42/document/file.pdf") == 42
assert _extract_legacy_workflow_run_id("campaigns/42/source.csv") is None
def test_unknown_numeric_prefix_is_not_treated_as_org_scoped():
assert _extract_org_id_from_key("unknown/42/file.wav") is None

View file

@ -7,6 +7,7 @@ from pipecat.transcriptions.language import Language
from api.services.configuration.registry import (
SarvamLLMConfiguration,
SarvamTTSConfiguration,
ServiceProviders,
)
from api.services.pipecat.audio_config import AudioConfig
@ -14,6 +15,7 @@ from api.services.pipecat.service_factory import (
create_llm_service,
create_llm_service_from_provider,
create_stt_service,
create_tts_service,
)
@ -112,3 +114,41 @@ class TestSarvamSTTServiceFactory:
kwargs = mock_service.call_args.kwargs
assert kwargs["settings"].language == expected_language
class TestSarvamTTSServiceFactory:
def test_sarvam_tts_configuration_defaults(self):
config = SarvamTTSConfiguration(api_key="test-key")
assert config.provider == ServiceProviders.SARVAM
assert config.model == "bulbul:v2"
assert config.voice == "anushka"
assert config.language == "hi-IN"
assert config.speed == 1.0
def test_create_sarvam_tts_service_maps_speed_to_pace(self):
user_config = SimpleNamespace(
tts=SimpleNamespace(
provider=ServiceProviders.SARVAM.value,
api_key="test-key",
model="bulbul:v2",
voice="anushka",
language="hi-IN",
speed=1.25,
)
)
audio_config = AudioConfig(
transport_in_sample_rate=16000, transport_out_sample_rate=16000
)
with patch(
"api.services.pipecat.service_factory.SarvamTTSService"
) as mock_service:
create_tts_service(user_config, audio_config)
kwargs = mock_service.call_args.kwargs
assert kwargs["api_key"] == "test-key"
assert kwargs["settings"].model == "bulbul:v2"
assert kwargs["settings"].voice == "anushka"
assert kwargs["settings"].language == Language.HI
assert kwargs["settings"].pace == 1.25

View file

@ -0,0 +1,80 @@
from types import SimpleNamespace
from unittest.mock import patch
from api.services.configuration.check_validity import UserConfigurationValidator
from api.services.configuration.registry import (
REGISTRY,
ServiceProviders,
ServiceType,
SmallestAISTTConfiguration,
SmallestAITTSConfiguration,
)
from api.services.pipecat.service_factory import create_tts_service
def test_smallest_tts_configuration_defaults_and_registry():
config = SmallestAITTSConfiguration(api_key="test-key")
assert config.provider == ServiceProviders.SMALLEST
assert config.model == "lightning_v3.1"
assert config.voice == "sophia"
assert config.language == "en"
assert config.speed == 1.0
assert (
REGISTRY[ServiceType.TTS][ServiceProviders.SMALLEST]
is SmallestAITTSConfiguration
)
def test_smallest_stt_configuration_defaults_and_registry():
config = SmallestAISTTConfiguration(api_key="test-key")
assert config.provider == ServiceProviders.SMALLEST
assert config.model == "pulse"
assert config.language == "en"
assert (
REGISTRY[ServiceType.STT][ServiceProviders.SMALLEST]
is SmallestAISTTConfiguration
)
def test_validator_accepts_smallest_services():
validator = UserConfigurationValidator()
assert (
validator._validate_service(
SmallestAITTSConfiguration(api_key="test-key"),
"tts",
)
== []
)
assert (
validator._validate_service(
SmallestAISTTConfiguration(api_key="test-key"),
"stt",
)
== []
)
def test_create_smallest_tts_service_normalizes_hyphenated_model_values():
user_config = SimpleNamespace(
tts=SimpleNamespace(
provider=ServiceProviders.SMALLEST.value,
api_key="test-key",
model="lightning-v3.1",
voice="sophia",
language="en",
speed=1.0,
)
)
audio_config = SimpleNamespace(transport_in_sample_rate=16000)
with patch(
"api.services.pipecat.service_factory.SmallestTTSService"
) as mock_service:
create_tts_service(user_config, audio_config)
assert mock_service.call_count == 1
kwargs = mock_service.call_args.kwargs
assert kwargs["settings"].model == "lightning_v3.1"