From dc0386937a703131ffc9027902fa808d4c20ed3f Mon Sep 17 00:00:00 2001 From: dhruvladia-sarvam Date: Wed, 11 Mar 2026 02:27:57 +0530 Subject: [PATCH 1/8] Initial --- changelog/3978.added.md | 1 + .../55zzq-update-settings-sarvam-llm.py | 146 +++++++++++ src/pipecat/services/sarvam/__init__.py | 2 +- src/pipecat/services/sarvam/llm.py | 235 ++++++++++++++++++ 4 files changed, 383 insertions(+), 1 deletion(-) create mode 100644 changelog/3978.added.md create mode 100644 examples/foundational/55zzq-update-settings-sarvam-llm.py create mode 100644 src/pipecat/services/sarvam/llm.py diff --git a/changelog/3978.added.md b/changelog/3978.added.md new file mode 100644 index 000000000..bb80b10c0 --- /dev/null +++ b/changelog/3978.added.md @@ -0,0 +1 @@ +- Added `SarvamLLMService` with support for `sarvam-30b`, `sarvam-30b-16k`, `sarvam-105b` and `sarvam-105b-32k` diff --git a/examples/foundational/55zzq-update-settings-sarvam-llm.py b/examples/foundational/55zzq-update-settings-sarvam-llm.py new file mode 100644 index 000000000..1d3f9d754 --- /dev/null +++ b/examples/foundational/55zzq-update-settings-sarvam-llm.py @@ -0,0 +1,146 @@ +# +# Copyright (c) 2024-2026, Daily +# +# SPDX-License-Identifier: BSD 2-Clause License +# + +import asyncio +import os +from typing import Any + +from dotenv import load_dotenv +from loguru import logger + +from pipecat.audio.vad.silero import SileroVADAnalyzer +from pipecat.frames.frames import LLMRunFrame, LLMUpdateSettingsFrame +from pipecat.pipeline.pipeline import Pipeline +from pipecat.pipeline.runner import PipelineRunner +from pipecat.pipeline.task import PipelineParams, PipelineTask +from pipecat.processors.aggregators.llm_context import LLMContext +from pipecat.processors.aggregators.llm_response_universal import ( + LLMContextAggregatorPair, + LLMUserAggregatorParams, +) +from pipecat.runner.types import RunnerArguments +from pipecat.runner.utils import create_transport +from pipecat.services.openai.base_llm import OpenAILLMSettings +from pipecat.services.sarvam.llm import SarvamLLMService +from pipecat.services.sarvam.stt import SarvamSTTService +from pipecat.services.sarvam.tts import SarvamTTSService +from pipecat.transports.base_transport import BaseTransport, TransportParams +from pipecat.transports.daily.transport import DailyParams +from pipecat.transports.websocket.fastapi import FastAPIWebsocketParams + +load_dotenv(override=True) + +transport_params = { + "daily": lambda: DailyParams( + audio_in_enabled=True, + audio_out_enabled=True, + ), + "twilio": lambda: FastAPIWebsocketParams( + audio_in_enabled=True, + audio_out_enabled=True, + ), + "webrtc": lambda: TransportParams( + audio_in_enabled=True, + audio_out_enabled=True, + ), +} + + +def _require_env(name: str) -> str: + value = os.getenv(name) + if not value: + raise ValueError(f"Environment variable `{name}` is required.") + return value + + +async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): + logger.info("Starting bot") + + stt = SarvamSTTService( + model="saaras:v3", + api_key=_require_env("SARVAM_API_KEY"), + ) + + tts = SarvamTTSService( + model="bulbul:v3", + api_key=_require_env("SARVAM_API_KEY"), + ) + + llm = SarvamLLMService( + api_key=_require_env("SARVAM_API_KEY"), + model="sarvam-30b", + ) + + messages: list[Any] = [ + { + "role": "system", + "content": ( + "You are a helpful LLM in a WebRTC call. Your goal is to " + "demonstrate your capabilities in a succinct way. Your output " + "will be spoken aloud, so avoid special characters that can't " + "easily be spoken, such as emojis or bullet points. Respond to " + "what the user said in a creative and helpful way." + ), + }, + ] + + context = LLMContext(messages) + user_aggregator, assistant_aggregator = LLMContextAggregatorPair( + context, + user_params=LLMUserAggregatorParams(vad_analyzer=SileroVADAnalyzer()), + ) + + pipeline = Pipeline( + [ + transport.input(), + stt, + user_aggregator, + llm, + tts, + transport.output(), + assistant_aggregator, + ] + ) + + task = PipelineTask( + pipeline, + params=PipelineParams( + enable_metrics=True, + enable_usage_metrics=True, + ), + idle_timeout_secs=runner_args.pipeline_idle_timeout_secs, + ) + + @transport.event_handler("on_client_connected") + async def on_client_connected(transport, client): + logger.info("Client connected") + messages.append({"role": "system", "content": "Please introduce yourself to the user."}) + await task.queue_frames([LLMRunFrame()]) + + await asyncio.sleep(10) + logger.info("Updating Sarvam LLM settings: temperature=0.1") + await task.queue_frame(LLMUpdateSettingsFrame(delta=OpenAILLMSettings(temperature=0.1))) + + @transport.event_handler("on_client_disconnected") + async def on_client_disconnected(transport, client): + logger.info("Client disconnected") + await task.cancel() + + runner = PipelineRunner(handle_sigint=runner_args.handle_sigint) + + await runner.run(task) + + +async def bot(runner_args: RunnerArguments): + """Main bot entry point compatible with Pipecat Cloud.""" + transport = await create_transport(runner_args, transport_params) + await run_bot(transport, runner_args) + + +if __name__ == "__main__": + from pipecat.runner.run import main + + main() diff --git a/src/pipecat/services/sarvam/__init__.py b/src/pipecat/services/sarvam/__init__.py index e8af1401e..1357f737a 100644 --- a/src/pipecat/services/sarvam/__init__.py +++ b/src/pipecat/services/sarvam/__init__.py @@ -4,5 +4,5 @@ # SPDX-License-Identifier: BSD 2-Clause License # - +from .llm import SarvamLLMService from .tts import * diff --git a/src/pipecat/services/sarvam/llm.py b/src/pipecat/services/sarvam/llm.py new file mode 100644 index 000000000..6b440d7f9 --- /dev/null +++ b/src/pipecat/services/sarvam/llm.py @@ -0,0 +1,235 @@ +# +# Copyright (c) 2024-2026, Daily +# +# SPDX-License-Identifier: BSD 2-Clause License +# + +"""Sarvam LLM service implementation using OpenAI-compatible interface.""" + +import asyncio +import json +from typing import Any, Literal, Mapping, Optional + +import httpx +from loguru import logger +from openai import NOT_GIVEN, APITimeoutError +from pydantic import BaseModel + +from pipecat.adapters.services.open_ai_adapter import OpenAILLMInvocationParams +from pipecat.processors.aggregators.llm_context import LLMContext +from pipecat.processors.aggregators.openai_llm_context import OpenAILLMContext +from pipecat.services.openai.llm import OpenAILLMService +from pipecat.services.sarvam._sdk import sdk_headers + +__all__ = ["SarvamLLMService"] + + +class SarvamLLMService(OpenAILLMService): + """Sarvam LLM service using Sarvam's OpenAI-compatible chat completions API. + + This service extends ``OpenAILLMService`` while adding Sarvam-specific behavior: + + - model allow-list validation + - request shaping for Sarvam-compatible parameters + - Sarvam auth header wiring (``api-subscription-key``) + - SDK User-Agent propagation on every API call + - raw Sarvam server error passthrough + """ + + SUPPORTED_MODELS = frozenset({"sarvam-30b", "sarvam-30b-16k", "sarvam-105b", "sarvam-105b-32k"}) + TOOL_CALLING_MODELS = frozenset( + {"sarvam-30b", "sarvam-30b-16k", "sarvam-105b", "sarvam-105b-32k"} + ) + + class InputParams(OpenAILLMService.InputParams): + """Configuration parameters for Sarvam LLM service. + + Parameters: + frequency_penalty: Penalty for frequent tokens (-2.0 to 2.0). + presence_penalty: Penalty for new tokens (-2.0 to 2.0). + seed: Random seed for deterministic outputs. + temperature: Sampling temperature (0.0 to 2.0). + top_k: Top-k sampling parameter (currently ignored by OpenAI client). + top_p: Top-p (nucleus) sampling parameter (0.0 to 1.0). + max_tokens: Maximum tokens in response. + max_completion_tokens: Maximum completion tokens (not sent to Sarvam API). + service_tier: Service tier (not sent to Sarvam API). + extra: Additional model-specific parameters. + wiki_grounding: Sarvam wiki grounding toggle. + reasoning_effort: Reasoning effort level (low, medium, high). + """ + + wiki_grounding: Optional[bool] = None + reasoning_effort: Optional[Literal["low", "medium", "high"]] = None + + def __init__( + self, + *, + api_key: str, + model: str = "sarvam-30b", + base_url: str = "https://api.sarvam.ai/v1", + default_headers: Optional[Mapping[str, str]] = None, + params: Optional[InputParams] = None, + **kwargs, + ): + """Initialize Sarvam LLM service. + + Args: + api_key: Sarvam API key used for both OpenAI auth and Sarvam subscription header. + model: Sarvam model identifier. Supported values: ``sarvam-30b``, ``sarvam-105b``. + base_url: Sarvam OpenAI-compatible base URL. + default_headers: Additional HTTP headers to include in requests. + params: Input parameters for model configuration. + **kwargs: Additional keyword arguments passed to ``OpenAILLMService``. + """ + self._validate_model(model) + + params = (params or SarvamLLMService.InputParams()).model_copy(deep=True) + params.extra = self._build_extra_params(params) + + super().__init__( + api_key=api_key, + base_url=base_url, + model=model, + default_headers=default_headers, + params=params, + **kwargs, + ) + + def create_client( + self, + api_key=None, + base_url=None, + organization=None, + project=None, + default_headers=None, + **kwargs, + ): + """Create OpenAI-compatible client for Sarvam API endpoint. + + Ensures Sarvam auth and SDK identification headers are always attached. + """ + merged_headers = dict(default_headers or {}) + merged_headers.update(sdk_headers()) + if api_key: + merged_headers["api-subscription-key"] = api_key + + # Keep SDK User-Agent stable even when caller-provided headers include User-Agent. + merged_headers["User-Agent"] = sdk_headers()["User-Agent"] + + logger.debug(f"Creating Sarvam client with API {base_url}") + return super().create_client( + api_key=api_key, + base_url=base_url, + organization=organization, + project=project, + default_headers=merged_headers, + **kwargs, + ) + + def build_chat_completion_params(self, params_from_context: OpenAILLMInvocationParams) -> dict: + """Build parameters for Sarvam chat completion request. + + Starts from OpenAI-compatible defaults, then removes unsupported + request fields and applies Sarvam-specific options. + """ + self._validate_tool_parameters(params_from_context) + + params = super().build_chat_completion_params(params_from_context) + params.pop("stream_options", None) + params.pop("max_completion_tokens", None) + params.pop("service_tier", None) + + extra = self._settings.extra if isinstance(self._settings.extra, dict) else {} + if "wiki_grounding" in extra and extra["wiki_grounding"] is not None: + params["wiki_grounding"] = extra["wiki_grounding"] + if "reasoning_effort" in extra and extra["reasoning_effort"] is not None: + params["reasoning_effort"] = extra["reasoning_effort"] + + return params + + async def get_chat_completions(self, params_from_context: OpenAILLMInvocationParams): + """Get streaming chat completions with Sarvam raw error passthrough.""" + try: + return await super().get_chat_completions(params_from_context) + except (APITimeoutError, asyncio.TimeoutError, httpx.TimeoutException): + raise + except Exception as e: + raise RuntimeError(self._format_raw_server_error(e)) from e + + async def run_inference( + self, context: LLMContext | OpenAILLMContext, max_tokens: Optional[int] = None + ) -> Optional[str]: + """Run one-shot inference and preserve Sarvam raw server errors.""" + try: + return await super().run_inference(context, max_tokens=max_tokens) + except (APITimeoutError, asyncio.TimeoutError, httpx.TimeoutException): + raise + except Exception as e: + raise RuntimeError(self._format_raw_server_error(e)) from e + + def _validate_model(self, model: str): + if model not in self.SUPPORTED_MODELS: + allowed = ", ".join(sorted(self.SUPPORTED_MODELS)) + raise ValueError(f"Unsupported Sarvam LLM model '{model}'. Allowed values: {allowed}.") + + def _build_extra_params(self, params: BaseModel) -> dict[str, Any]: + extra = dict(getattr(params, "extra", {}) or {}) + if getattr(params, "wiki_grounding", None) is not None: + extra["wiki_grounding"] = params.wiki_grounding + if getattr(params, "reasoning_effort", None) is not None: + extra["reasoning_effort"] = params.reasoning_effort + return extra + + def _validate_tool_parameters(self, params_from_context: OpenAILLMInvocationParams): + tools = params_from_context.get("tools", NOT_GIVEN) + tool_choice = params_from_context.get("tool_choice", NOT_GIVEN) + + has_tools = ( + tools is not NOT_GIVEN + and tools is not None + and (not isinstance(tools, list) or len(tools) > 0) + ) + has_tool_choice = tool_choice is not NOT_GIVEN and tool_choice is not None + + if has_tool_choice and not has_tools: + raise ValueError("Sarvam requires non-empty `tools` when `tool_choice` is provided.") + + if has_tools and self._settings.model not in self.TOOL_CALLING_MODELS: + allowed = ", ".join(sorted(self.TOOL_CALLING_MODELS)) + raise ValueError( + f"Model '{self._settings.model}' does not support tools. " + f"Supported models: {allowed}." + ) + + def _format_raw_server_error(self, error: Exception) -> str: + raw_message = self._extract_raw_server_message(error) + return f"Sarvam server error: {raw_message}" + + def _extract_raw_server_message(self, error: Exception) -> str: + body = getattr(error, "body", None) + if body is not None: + return self._payload_to_message(body) + + response = getattr(error, "response", None) + if response is not None: + try: + return self._payload_to_message(response.json()) + except Exception: + text = getattr(response, "text", None) + if text: + return str(text) + + return str(error) + + def _payload_to_message(self, payload: Any) -> str: + if isinstance(payload, dict): + error_obj = payload.get("error") + if isinstance(error_obj, dict) and isinstance(error_obj.get("message"), str): + return error_obj["message"] + if isinstance(payload.get("message"), str): + return payload["message"] + return json.dumps(payload, ensure_ascii=False) + if isinstance(payload, list): + return json.dumps(payload, ensure_ascii=False) + return str(payload) From 8745f203302fcc68ecb9e9533999fadd2421a06d Mon Sep 17 00:00:00 2001 From: dhruvladia-sarvam Date: Sun, 15 Mar 2026 22:24:06 +0530 Subject: [PATCH 2/8] fix llm wrapper redundancy and restore run_inference parity --- src/pipecat/services/sarvam/llm.py | 151 ++++++++++++++++++----------- 1 file changed, 97 insertions(+), 54 deletions(-) diff --git a/src/pipecat/services/sarvam/llm.py b/src/pipecat/services/sarvam/llm.py index 6b440d7f9..c99e3dfb9 100644 --- a/src/pipecat/services/sarvam/llm.py +++ b/src/pipecat/services/sarvam/llm.py @@ -8,20 +8,40 @@ import asyncio import json -from typing import Any, Literal, Mapping, Optional +from dataclasses import dataclass, field +from typing import Any, Awaitable, Literal, Mapping, Optional, TypeVar import httpx from loguru import logger -from openai import NOT_GIVEN, APITimeoutError -from pydantic import BaseModel +from openai import NOT_GIVEN, APITimeoutError, AsyncStream +from openai.types.chat import ChatCompletionChunk from pipecat.adapters.services.open_ai_adapter import OpenAILLMInvocationParams from pipecat.processors.aggregators.llm_context import LLMContext from pipecat.processors.aggregators.openai_llm_context import OpenAILLMContext +from pipecat.services.openai.base_llm import OpenAILLMSettings from pipecat.services.openai.llm import OpenAILLMService from pipecat.services.sarvam._sdk import sdk_headers +from pipecat.services.settings import NOT_GIVEN as _NOT_GIVEN +from pipecat.services.settings import _NotGiven, _warn_deprecated_param, is_given -__all__ = ["SarvamLLMService"] +__all__ = ["SarvamLLMService", "SarvamLLMSettings"] +_T = TypeVar("_T") + + +@dataclass +class SarvamLLMSettings(OpenAILLMSettings): + """Settings for SarvamLLMService. + + Parameters: + wiki_grounding: Sarvam wiki grounding toggle. + reasoning_effort: Reasoning effort level (low, medium, high). + """ + + wiki_grounding: bool | None | _NotGiven = field(default_factory=lambda: _NOT_GIVEN) + reasoning_effort: Literal["low", "medium", "high"] | None | _NotGiven = field( + default_factory=lambda: _NOT_GIVEN + ) class SarvamLLMService(OpenAILLMService): @@ -40,59 +60,59 @@ class SarvamLLMService(OpenAILLMService): TOOL_CALLING_MODELS = frozenset( {"sarvam-30b", "sarvam-30b-16k", "sarvam-105b", "sarvam-105b-32k"} ) - - class InputParams(OpenAILLMService.InputParams): - """Configuration parameters for Sarvam LLM service. - - Parameters: - frequency_penalty: Penalty for frequent tokens (-2.0 to 2.0). - presence_penalty: Penalty for new tokens (-2.0 to 2.0). - seed: Random seed for deterministic outputs. - temperature: Sampling temperature (0.0 to 2.0). - top_k: Top-k sampling parameter (currently ignored by OpenAI client). - top_p: Top-p (nucleus) sampling parameter (0.0 to 1.0). - max_tokens: Maximum tokens in response. - max_completion_tokens: Maximum completion tokens (not sent to Sarvam API). - service_tier: Service tier (not sent to Sarvam API). - extra: Additional model-specific parameters. - wiki_grounding: Sarvam wiki grounding toggle. - reasoning_effort: Reasoning effort level (low, medium, high). - """ - - wiki_grounding: Optional[bool] = None - reasoning_effort: Optional[Literal["low", "medium", "high"]] = None + Settings = SarvamLLMSettings + _settings: SarvamLLMSettings def __init__( self, *, api_key: str, - model: str = "sarvam-30b", base_url: str = "https://api.sarvam.ai/v1", + model: Optional[str] = None, + settings: Optional[SarvamLLMSettings] = None, default_headers: Optional[Mapping[str, str]] = None, - params: Optional[InputParams] = None, **kwargs, ): """Initialize Sarvam LLM service. Args: api_key: Sarvam API key used for both OpenAI auth and Sarvam subscription header. - model: Sarvam model identifier. Supported values: ``sarvam-30b``, ``sarvam-105b``. base_url: Sarvam OpenAI-compatible base URL. + model: Sarvam model identifier. Supported values: ``sarvam-30b``, + ``sarvam-30b-16k``, ``sarvam-105b``, ``sarvam-105b-32k``. + + .. deprecated:: 0.0.105 + Use ``settings=SarvamLLMSettings(model=...)`` instead. + + settings: Runtime-updatable settings. When provided alongside deprecated + parameters, ``settings`` values take precedence. default_headers: Additional HTTP headers to include in requests. - params: Input parameters for model configuration. **kwargs: Additional keyword arguments passed to ``OpenAILLMService``. """ - self._validate_model(model) + # 1. Initialize default_settings with hardcoded defaults + default_settings = SarvamLLMSettings(model="sarvam-30b") - params = (params or SarvamLLMService.InputParams()).model_copy(deep=True) - params.extra = self._build_extra_params(params) + # 2. Apply direct init arg overrides (deprecated) + if model is not None: + _warn_deprecated_param("model", SarvamLLMSettings, "model") + default_settings.model = model + + # 3. Apply settings delta (canonical API, always wins) + if settings is not None: + default_settings.apply_update(settings) + + # BaseOpenAILLMService stores settings as OpenAILLMSettings, so keep + # Sarvam-specific runtime knobs in ``extra``. + default_settings.extra = dict(default_settings.extra) + default_settings.extra.update(self._extract_sarvam_extra_from_settings(default_settings)) + + self._validate_model(default_settings.model) super().__init__( api_key=api_key, base_url=base_url, - model=model, + settings=default_settings, default_headers=default_headers, - params=params, **kwargs, ) @@ -110,13 +130,11 @@ class SarvamLLMService(OpenAILLMService): Ensures Sarvam auth and SDK identification headers are always attached. """ merged_headers = dict(default_headers or {}) + # sdk_headers() carries Pipecat User-Agent and should override caller-provided value. merged_headers.update(sdk_headers()) if api_key: merged_headers["api-subscription-key"] = api_key - # Keep SDK User-Agent stable even when caller-provided headers include User-Agent. - merged_headers["User-Agent"] = sdk_headers()["User-Agent"] - logger.debug(f"Creating Sarvam client with API {base_url}") return super().create_client( api_key=api_key, @@ -148,38 +166,63 @@ class SarvamLLMService(OpenAILLMService): return params - async def get_chat_completions(self, params_from_context: OpenAILLMInvocationParams): - """Get streaming chat completions with Sarvam raw error passthrough.""" + async def _update_settings(self, delta: OpenAILLMSettings) -> dict[str, Any]: + """Apply settings updates, preserving Sarvam-specific runtime knobs.""" + sarvam_extra = self._extract_sarvam_extra_from_settings(delta) + if sarvam_extra: + delta.extra = dict(delta.extra) + delta.extra.update(sarvam_extra) + + return await super()._update_settings(delta) + + async def _call_with_raw_sarvam_errors(self, awaitable: Awaitable[_T]) -> _T: + """Await an OpenAI call while preserving Sarvam raw error payloads.""" try: - return await super().get_chat_completions(params_from_context) + return await awaitable except (APITimeoutError, asyncio.TimeoutError, httpx.TimeoutException): raise except Exception as e: raise RuntimeError(self._format_raw_server_error(e)) from e + async def get_chat_completions( + self, params_from_context: OpenAILLMInvocationParams + ) -> AsyncStream[ChatCompletionChunk]: + """Get streaming chat completions with Sarvam raw error passthrough.""" + return await self._call_with_raw_sarvam_errors( + super().get_chat_completions(params_from_context) + ) + async def run_inference( - self, context: LLMContext | OpenAILLMContext, max_tokens: Optional[int] = None + self, + context: LLMContext | OpenAILLMContext, + max_tokens: Optional[int] = None, + system_instruction: Optional[str] = None, ) -> Optional[str]: """Run one-shot inference and preserve Sarvam raw server errors.""" - try: - return await super().run_inference(context, max_tokens=max_tokens) - except (APITimeoutError, asyncio.TimeoutError, httpx.TimeoutException): - raise - except Exception as e: - raise RuntimeError(self._format_raw_server_error(e)) from e + return await self._call_with_raw_sarvam_errors( + super().run_inference( + context, + max_tokens=max_tokens, + system_instruction=system_instruction, + ) + ) def _validate_model(self, model: str): if model not in self.SUPPORTED_MODELS: allowed = ", ".join(sorted(self.SUPPORTED_MODELS)) raise ValueError(f"Unsupported Sarvam LLM model '{model}'. Allowed values: {allowed}.") - def _build_extra_params(self, params: BaseModel) -> dict[str, Any]: - extra = dict(getattr(params, "extra", {}) or {}) - if getattr(params, "wiki_grounding", None) is not None: - extra["wiki_grounding"] = params.wiki_grounding - if getattr(params, "reasoning_effort", None) is not None: - extra["reasoning_effort"] = params.reasoning_effort - return extra + def _extract_sarvam_extra_from_settings(self, settings_obj: Any) -> dict[str, Any]: + updates: dict[str, Any] = {} + wiki_grounding = getattr(settings_obj, "wiki_grounding", _NOT_GIVEN) + if is_given(wiki_grounding): + updates["wiki_grounding"] = wiki_grounding + + reasoning_effort = getattr(settings_obj, "reasoning_effort", _NOT_GIVEN) + if is_given(reasoning_effort): + updates["reasoning_effort"] = reasoning_effort + + return updates def _validate_tool_parameters(self, params_from_context: OpenAILLMInvocationParams): tools = params_from_context.get("tools", NOT_GIVEN) From 8a4f6b486e2070ca01d9f6e72606561b8f2b2371 Mon Sep 17 00:00:00 2001 From: dhruvladia-sarvam Date: Tue, 17 Mar 2026 02:47:47 +0530 Subject: [PATCH 3/8] wrapper fixes --- .../55zzq-update-settings-sarvam-llm.py | 31 +++++++----------- src/pipecat/services/sarvam/__init__.py | 1 - src/pipecat/services/sarvam/llm.py | 32 +++++++++++++------ 3 files changed, 33 insertions(+), 31 deletions(-) diff --git a/examples/foundational/55zzq-update-settings-sarvam-llm.py b/examples/foundational/55zzq-update-settings-sarvam-llm.py index 1d3f9d754..227adb3cb 100644 --- a/examples/foundational/55zzq-update-settings-sarvam-llm.py +++ b/examples/foundational/55zzq-update-settings-sarvam-llm.py @@ -23,7 +23,6 @@ from pipecat.processors.aggregators.llm_response_universal import ( ) from pipecat.runner.types import RunnerArguments from pipecat.runner.utils import create_transport -from pipecat.services.openai.base_llm import OpenAILLMSettings from pipecat.services.sarvam.llm import SarvamLLMService from pipecat.services.sarvam.stt import SarvamSTTService from pipecat.services.sarvam.tts import SarvamTTSService @@ -60,34 +59,24 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): logger.info("Starting bot") stt = SarvamSTTService( - model="saaras:v3", + settings=SarvamSTTService.Settings(model="saaras:v3"), api_key=_require_env("SARVAM_API_KEY"), ) tts = SarvamTTSService( - model="bulbul:v3", + settings=SarvamTTSService.Settings(model="bulbul:v3"), api_key=_require_env("SARVAM_API_KEY"), ) llm = SarvamLLMService( api_key=_require_env("SARVAM_API_KEY"), - model="sarvam-30b", + settings=SarvamLLMService.Settings(model="sarvam-30b"), + system_instruction=( + "You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way." + ), ) - messages: list[Any] = [ - { - "role": "system", - "content": ( - "You are a helpful LLM in a WebRTC call. Your goal is to " - "demonstrate your capabilities in a succinct way. Your output " - "will be spoken aloud, so avoid special characters that can't " - "easily be spoken, such as emojis or bullet points. Respond to " - "what the user said in a creative and helpful way." - ), - }, - ] - - context = LLMContext(messages) + context = LLMContext() user_aggregator, assistant_aggregator = LLMContextAggregatorPair( context, user_params=LLMUserAggregatorParams(vad_analyzer=SileroVADAnalyzer()), @@ -117,12 +106,14 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): @transport.event_handler("on_client_connected") async def on_client_connected(transport, client): logger.info("Client connected") - messages.append({"role": "system", "content": "Please introduce yourself to the user."}) + context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) await task.queue_frames([LLMRunFrame()]) await asyncio.sleep(10) logger.info("Updating Sarvam LLM settings: temperature=0.1") - await task.queue_frame(LLMUpdateSettingsFrame(delta=OpenAILLMSettings(temperature=0.1))) + await task.queue_frame( + LLMUpdateSettingsFrame(delta=SarvamLLMService.Settings(temperature=0.1)) + ) @transport.event_handler("on_client_disconnected") async def on_client_disconnected(transport, client): diff --git a/src/pipecat/services/sarvam/__init__.py b/src/pipecat/services/sarvam/__init__.py index 1357f737a..30d115d3f 100644 --- a/src/pipecat/services/sarvam/__init__.py +++ b/src/pipecat/services/sarvam/__init__.py @@ -4,5 +4,4 @@ # SPDX-License-Identifier: BSD 2-Clause License # -from .llm import SarvamLLMService from .tts import * diff --git a/src/pipecat/services/sarvam/llm.py b/src/pipecat/services/sarvam/llm.py index c99e3dfb9..3512778f1 100644 --- a/src/pipecat/services/sarvam/llm.py +++ b/src/pipecat/services/sarvam/llm.py @@ -25,7 +25,6 @@ from pipecat.services.sarvam._sdk import sdk_headers from pipecat.services.settings import NOT_GIVEN as _NOT_GIVEN from pipecat.services.settings import _NotGiven, _warn_deprecated_param, is_given -__all__ = ["SarvamLLMService", "SarvamLLMSettings"] _T = TypeVar("_T") @@ -56,10 +55,10 @@ class SarvamLLMService(OpenAILLMService): - raw Sarvam server error passthrough """ - SUPPORTED_MODELS = frozenset({"sarvam-30b", "sarvam-30b-16k", "sarvam-105b", "sarvam-105b-32k"}) - TOOL_CALLING_MODELS = frozenset( + _SUPPORTED_MODELS = frozenset( {"sarvam-30b", "sarvam-30b-16k", "sarvam-105b", "sarvam-105b-32k"} ) + _TOOL_CALLING_MODELS = _SUPPORTED_MODELS Settings = SarvamLLMSettings _settings: SarvamLLMSettings @@ -94,6 +93,8 @@ class SarvamLLMService(OpenAILLMService): # 2. Apply direct init arg overrides (deprecated) if model is not None: + # Keep deprecated init arg for backward compatibility while steering callers + # to settings=SarvamLLMService.Settings(model=...). _warn_deprecated_param("model", SarvamLLMSettings, "model") default_settings.model = model @@ -101,8 +102,9 @@ class SarvamLLMService(OpenAILLMService): if settings is not None: default_settings.apply_update(settings) - # BaseOpenAILLMService stores settings as OpenAILLMSettings, so keep - # Sarvam-specific runtime knobs in ``extra``. + # BaseOpenAILLMService currently stores settings as OpenAILLMSettings. + # Preserve Sarvam-only runtime knobs in ``extra`` so they survive + # initialization and future update frames. default_settings.extra = dict(default_settings.extra) default_settings.extra.update(self._extract_sarvam_extra_from_settings(default_settings)) @@ -158,6 +160,7 @@ class SarvamLLMService(OpenAILLMService): params.pop("max_completion_tokens", None) params.pop("service_tier", None) + # Sarvam-only fields are bridged through settings.extra (see __init__ and _update_settings). extra = self._settings.extra if isinstance(self._settings.extra, dict) else {} if "wiki_grounding" in extra and extra["wiki_grounding"] is not None: params["wiki_grounding"] = extra["wiki_grounding"] @@ -168,6 +171,8 @@ class SarvamLLMService(OpenAILLMService): async def _update_settings(self, delta: OpenAILLMSettings) -> dict[str, Any]: """Apply settings updates, preserving Sarvam-specific runtime knobs.""" + # LLMUpdateSettingsFrame commonly carries OpenAILLMSettings deltas. + # Lift Sarvam-only fields into delta.extra before delegating to base. sarvam_extra = self._extract_sarvam_extra_from_settings(delta) if sarvam_extra: delta.extra = dict(delta.extra) @@ -176,7 +181,13 @@ class SarvamLLMService(OpenAILLMService): return await super()._update_settings(delta) async def _call_with_raw_sarvam_errors(self, awaitable: Awaitable[_T]) -> _T: - """Await an OpenAI call while preserving Sarvam raw error payloads.""" + """Await an OpenAI call while preserving Sarvam raw error payloads. + + BaseOpenAILLMService handles pipeline-frame exceptions via push_error(), + but direct helper methods like ``get_chat_completions`` and + ``run_inference`` are often consumed directly. We normalize those errors + here so applications consistently receive server-provided messages. + """ try: return await awaitable except (APITimeoutError, asyncio.TimeoutError, httpx.TimeoutException): @@ -208,8 +219,8 @@ class SarvamLLMService(OpenAILLMService): ) def _validate_model(self, model: str): - if model not in self.SUPPORTED_MODELS: - allowed = ", ".join(sorted(self.SUPPORTED_MODELS)) + if model not in self._SUPPORTED_MODELS: + allowed = ", ".join(sorted(self._SUPPORTED_MODELS)) raise ValueError(f"Unsupported Sarvam LLM model '{model}'. Allowed values: {allowed}.") def _extract_sarvam_extra_from_settings(self, settings_obj: Any) -> dict[str, Any]: @@ -238,8 +249,9 @@ class SarvamLLMService(OpenAILLMService): if has_tool_choice and not has_tools: raise ValueError("Sarvam requires non-empty `tools` when `tool_choice` is provided.") - if has_tools and self._settings.model not in self.TOOL_CALLING_MODELS: - allowed = ", ".join(sorted(self.TOOL_CALLING_MODELS)) + # Validate early to provide deterministic errors before network calls. + if has_tools and self._settings.model not in self._TOOL_CALLING_MODELS: + allowed = ", ".join(sorted(self._TOOL_CALLING_MODELS)) raise ValueError( f"Model '{self._settings.model}' does not support tools. " f"Supported models: {allowed}." From 3428a4c6ad3c6ec79d5edcecc7fbab6f431443e8 Mon Sep 17 00:00:00 2001 From: dhruvladia-sarvam Date: Mon, 23 Mar 2026 23:45:27 +0530 Subject: [PATCH 4/8] fixes post PR 4081 --- src/pipecat/services/sarvam/llm.py | 128 ++++++++--------------------- 1 file changed, 34 insertions(+), 94 deletions(-) diff --git a/src/pipecat/services/sarvam/llm.py b/src/pipecat/services/sarvam/llm.py index 3512778f1..6085cbb08 100644 --- a/src/pipecat/services/sarvam/llm.py +++ b/src/pipecat/services/sarvam/llm.py @@ -23,7 +23,7 @@ from pipecat.services.openai.base_llm import OpenAILLMSettings from pipecat.services.openai.llm import OpenAILLMService from pipecat.services.sarvam._sdk import sdk_headers from pipecat.services.settings import NOT_GIVEN as _NOT_GIVEN -from pipecat.services.settings import _NotGiven, _warn_deprecated_param, is_given +from pipecat.services.settings import _NotGiven, is_given _T = TypeVar("_T") @@ -58,17 +58,15 @@ class SarvamLLMService(OpenAILLMService): _SUPPORTED_MODELS = frozenset( {"sarvam-30b", "sarvam-30b-16k", "sarvam-105b", "sarvam-105b-32k"} ) - _TOOL_CALLING_MODELS = _SUPPORTED_MODELS Settings = SarvamLLMSettings - _settings: SarvamLLMSettings + _settings: Settings def __init__( self, *, api_key: str, base_url: str = "https://api.sarvam.ai/v1", - model: Optional[str] = None, - settings: Optional[SarvamLLMSettings] = None, + settings: Optional[Settings] = None, default_headers: Optional[Mapping[str, str]] = None, **kwargs, ): @@ -77,39 +75,33 @@ class SarvamLLMService(OpenAILLMService): Args: api_key: Sarvam API key used for both OpenAI auth and Sarvam subscription header. base_url: Sarvam OpenAI-compatible base URL. - model: Sarvam model identifier. Supported values: ``sarvam-30b``, - ``sarvam-30b-16k``, ``sarvam-105b``, ``sarvam-105b-32k``. - - .. deprecated:: 0.0.105 - Use ``settings=SarvamLLMSettings(model=...)`` instead. - - settings: Runtime-updatable settings. When provided alongside deprecated - parameters, ``settings`` values take precedence. + settings: Runtime-updatable settings. default_headers: Additional HTTP headers to include in requests. **kwargs: Additional keyword arguments passed to ``OpenAILLMService``. """ - # 1. Initialize default_settings with hardcoded defaults - default_settings = SarvamLLMSettings(model="sarvam-30b") + # Initialize defaults with concrete values for Sarvam-specific fields. + default_settings = self.Settings( + model="sarvam-30b", + system_instruction=None, + frequency_penalty=NOT_GIVEN, + presence_penalty=NOT_GIVEN, + seed=NOT_GIVEN, + temperature=NOT_GIVEN, + top_p=NOT_GIVEN, + top_k=None, + max_tokens=NOT_GIVEN, + max_completion_tokens=NOT_GIVEN, + filter_incomplete_user_turns=False, + user_turn_completion_config=None, + extra={}, + wiki_grounding=None, + reasoning_effort=None, + ) - # 2. Apply direct init arg overrides (deprecated) - if model is not None: - # Keep deprecated init arg for backward compatibility while steering callers - # to settings=SarvamLLMService.Settings(model=...). - _warn_deprecated_param("model", SarvamLLMSettings, "model") - default_settings.model = model - - # 3. Apply settings delta (canonical API, always wins) + # Apply settings delta (canonical API, always wins). if settings is not None: default_settings.apply_update(settings) - # BaseOpenAILLMService currently stores settings as OpenAILLMSettings. - # Preserve Sarvam-only runtime knobs in ``extra`` so they survive - # initialization and future update frames. - default_settings.extra = dict(default_settings.extra) - default_settings.extra.update(self._extract_sarvam_extra_from_settings(default_settings)) - - self._validate_model(default_settings.model) - super().__init__( api_key=api_key, base_url=base_url, @@ -117,6 +109,9 @@ class SarvamLLMService(OpenAILLMService): default_headers=default_headers, **kwargs, ) + # Keep Sarvam-specific settings object so runtime updates include + # ``wiki_grounding`` and ``reasoning_effort`` without extra bridging. + self._settings = default_settings def create_client( self, @@ -160,26 +155,16 @@ class SarvamLLMService(OpenAILLMService): params.pop("max_completion_tokens", None) params.pop("service_tier", None) - # Sarvam-only fields are bridged through settings.extra (see __init__ and _update_settings). - extra = self._settings.extra if isinstance(self._settings.extra, dict) else {} - if "wiki_grounding" in extra and extra["wiki_grounding"] is not None: - params["wiki_grounding"] = extra["wiki_grounding"] - if "reasoning_effort" in extra and extra["reasoning_effort"] is not None: - params["reasoning_effort"] = extra["reasoning_effort"] + if is_given(self._settings.wiki_grounding) and self._settings.wiki_grounding is not None: + params["wiki_grounding"] = self._settings.wiki_grounding + if ( + is_given(self._settings.reasoning_effort) + and self._settings.reasoning_effort is not None + ): + params["reasoning_effort"] = self._settings.reasoning_effort return params - async def _update_settings(self, delta: OpenAILLMSettings) -> dict[str, Any]: - """Apply settings updates, preserving Sarvam-specific runtime knobs.""" - # LLMUpdateSettingsFrame commonly carries OpenAILLMSettings deltas. - # Lift Sarvam-only fields into delta.extra before delegating to base. - sarvam_extra = self._extract_sarvam_extra_from_settings(delta) - if sarvam_extra: - delta.extra = dict(delta.extra) - delta.extra.update(sarvam_extra) - - return await super()._update_settings(delta) - async def _call_with_raw_sarvam_errors(self, awaitable: Awaitable[_T]) -> _T: """Await an OpenAI call while preserving Sarvam raw error payloads. @@ -193,7 +178,7 @@ class SarvamLLMService(OpenAILLMService): except (APITimeoutError, asyncio.TimeoutError, httpx.TimeoutException): raise except Exception as e: - raise RuntimeError(self._format_raw_server_error(e)) from e + raise RuntimeError(str(e)) from e async def get_chat_completions( self, params_from_context: OpenAILLMInvocationParams @@ -218,23 +203,6 @@ class SarvamLLMService(OpenAILLMService): ) ) - def _validate_model(self, model: str): - if model not in self._SUPPORTED_MODELS: - allowed = ", ".join(sorted(self._SUPPORTED_MODELS)) - raise ValueError(f"Unsupported Sarvam LLM model '{model}'. Allowed values: {allowed}.") - - def _extract_sarvam_extra_from_settings(self, settings_obj: Any) -> dict[str, Any]: - updates: dict[str, Any] = {} - wiki_grounding = getattr(settings_obj, "wiki_grounding", _NOT_GIVEN) - if is_given(wiki_grounding): - updates["wiki_grounding"] = wiki_grounding - - reasoning_effort = getattr(settings_obj, "reasoning_effort", _NOT_GIVEN) - if is_given(reasoning_effort): - updates["reasoning_effort"] = reasoning_effort - - return updates - def _validate_tool_parameters(self, params_from_context: OpenAILLMInvocationParams): tools = params_from_context.get("tools", NOT_GIVEN) tool_choice = params_from_context.get("tool_choice", NOT_GIVEN) @@ -249,34 +217,6 @@ class SarvamLLMService(OpenAILLMService): if has_tool_choice and not has_tools: raise ValueError("Sarvam requires non-empty `tools` when `tool_choice` is provided.") - # Validate early to provide deterministic errors before network calls. - if has_tools and self._settings.model not in self._TOOL_CALLING_MODELS: - allowed = ", ".join(sorted(self._TOOL_CALLING_MODELS)) - raise ValueError( - f"Model '{self._settings.model}' does not support tools. " - f"Supported models: {allowed}." - ) - - def _format_raw_server_error(self, error: Exception) -> str: - raw_message = self._extract_raw_server_message(error) - return f"Sarvam server error: {raw_message}" - - def _extract_raw_server_message(self, error: Exception) -> str: - body = getattr(error, "body", None) - if body is not None: - return self._payload_to_message(body) - - response = getattr(error, "response", None) - if response is not None: - try: - return self._payload_to_message(response.json()) - except Exception: - text = getattr(response, "text", None) - if text: - return str(text) - - return str(error) - def _payload_to_message(self, payload: Any) -> str: if isinstance(payload, dict): error_obj = payload.get("error") From 0f6cc231cff3c060add5634bcca0075d013f3c85 Mon Sep 17 00:00:00 2001 From: dhruvladia-sarvam Date: Tue, 24 Mar 2026 00:06:15 +0530 Subject: [PATCH 5/8] removing error wraps and model validation check --- src/pipecat/services/sarvam/llm.py | 25 ++----------------------- 1 file changed, 2 insertions(+), 23 deletions(-) diff --git a/src/pipecat/services/sarvam/llm.py b/src/pipecat/services/sarvam/llm.py index 6085cbb08..5b25e3068 100644 --- a/src/pipecat/services/sarvam/llm.py +++ b/src/pipecat/services/sarvam/llm.py @@ -6,27 +6,19 @@ """Sarvam LLM service implementation using OpenAI-compatible interface.""" -import asyncio -import json from dataclasses import dataclass, field -from typing import Any, Awaitable, Literal, Mapping, Optional, TypeVar +from typing import Literal, Mapping, Optional -import httpx from loguru import logger -from openai import NOT_GIVEN, APITimeoutError, AsyncStream -from openai.types.chat import ChatCompletionChunk +from openai import NOT_GIVEN from pipecat.adapters.services.open_ai_adapter import OpenAILLMInvocationParams -from pipecat.processors.aggregators.llm_context import LLMContext -from pipecat.processors.aggregators.openai_llm_context import OpenAILLMContext from pipecat.services.openai.base_llm import OpenAILLMSettings from pipecat.services.openai.llm import OpenAILLMService from pipecat.services.sarvam._sdk import sdk_headers from pipecat.services.settings import NOT_GIVEN as _NOT_GIVEN from pipecat.services.settings import _NotGiven, is_given -_T = TypeVar("_T") - @dataclass class SarvamLLMSettings(OpenAILLMSettings): @@ -52,7 +44,6 @@ class SarvamLLMService(OpenAILLMService): - request shaping for Sarvam-compatible parameters - Sarvam auth header wiring (``api-subscription-key``) - SDK User-Agent propagation on every API call - - raw Sarvam server error passthrough """ _SUPPORTED_MODELS = frozenset( @@ -216,15 +207,3 @@ class SarvamLLMService(OpenAILLMService): if has_tool_choice and not has_tools: raise ValueError("Sarvam requires non-empty `tools` when `tool_choice` is provided.") - - def _payload_to_message(self, payload: Any) -> str: - if isinstance(payload, dict): - error_obj = payload.get("error") - if isinstance(error_obj, dict) and isinstance(error_obj.get("message"), str): - return error_obj["message"] - if isinstance(payload.get("message"), str): - return payload["message"] - return json.dumps(payload, ensure_ascii=False) - if isinstance(payload, list): - return json.dumps(payload, ensure_ascii=False) - return str(payload) From 696196e30c459375c2bd0c18014233fd94eb443a Mon Sep 17 00:00:00 2001 From: dhruvladia-sarvam Date: Tue, 24 Mar 2026 16:29:58 +0530 Subject: [PATCH 6/8] alignment with pr 4081 --- README.md | 2 +- .../07z-interruptible-sarvam-http.py | 10 +-- .../foundational/07z-interruptible-sarvam.py | 19 ++--- scripts/evals/run-release-evals.py | 1 + src/pipecat/services/sarvam/llm.py | 80 +++++-------------- 5 files changed, 36 insertions(+), 76 deletions(-) diff --git a/README.md b/README.md index 8af4f942c..2f6eb672d 100644 --- a/README.md +++ b/README.md @@ -84,7 +84,7 @@ Catch new features, interviews, and how-tos on our [Pipecat TV](https://www.yout | Category | Services | | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Speech-to-Text | [AssemblyAI](https://docs.pipecat.ai/server/services/stt/assemblyai), [AWS](https://docs.pipecat.ai/server/services/stt/aws), [Azure](https://docs.pipecat.ai/server/services/stt/azure), [Cartesia](https://docs.pipecat.ai/server/services/stt/cartesia), [Deepgram](https://docs.pipecat.ai/server/services/stt/deepgram), [ElevenLabs](https://docs.pipecat.ai/server/services/stt/elevenlabs), [Fal Wizper](https://docs.pipecat.ai/server/services/stt/fal), [Gladia](https://docs.pipecat.ai/server/services/stt/gladia), [Google](https://docs.pipecat.ai/server/services/stt/google), [Gradium](https://docs.pipecat.ai/server/services/stt/gradium), [Groq (Whisper)](https://docs.pipecat.ai/server/services/stt/groq), [NVIDIA Riva](https://docs.pipecat.ai/server/services/stt/riva), [OpenAI (Whisper)](https://docs.pipecat.ai/server/services/stt/openai), [SambaNova (Whisper)](https://docs.pipecat.ai/server/services/stt/sambanova), [Sarvam](https://docs.pipecat.ai/server/services/stt/sarvam), [Soniox](https://docs.pipecat.ai/server/services/stt/soniox), [Speechmatics](https://docs.pipecat.ai/server/services/stt/speechmatics), [Whisper](https://docs.pipecat.ai/server/services/stt/whisper) | -| LLMs | [Anthropic](https://docs.pipecat.ai/server/services/llm/anthropic), [AWS](https://docs.pipecat.ai/server/services/llm/aws), [Azure](https://docs.pipecat.ai/server/services/llm/azure), [Cerebras](https://docs.pipecat.ai/server/services/llm/cerebras), [DeepSeek](https://docs.pipecat.ai/server/services/llm/deepseek), [Fireworks AI](https://docs.pipecat.ai/server/services/llm/fireworks), [Gemini](https://docs.pipecat.ai/server/services/llm/gemini), [Grok](https://docs.pipecat.ai/server/services/llm/grok), [Groq](https://docs.pipecat.ai/server/services/llm/groq), [Mistral](https://docs.pipecat.ai/server/services/llm/mistral), [NVIDIA NIM](https://docs.pipecat.ai/server/services/llm/nim), [Ollama](https://docs.pipecat.ai/server/services/llm/ollama), [OpenAI](https://docs.pipecat.ai/server/services/llm/openai), [OpenRouter](https://docs.pipecat.ai/server/services/llm/openrouter), [Perplexity](https://docs.pipecat.ai/server/services/llm/perplexity), [Qwen](https://docs.pipecat.ai/server/services/llm/qwen), [SambaNova](https://docs.pipecat.ai/server/services/llm/sambanova) [Together AI](https://docs.pipecat.ai/server/services/llm/together) | +| LLMs | [Anthropic](https://docs.pipecat.ai/server/services/llm/anthropic), [AWS](https://docs.pipecat.ai/server/services/llm/aws), [Azure](https://docs.pipecat.ai/server/services/llm/azure), [Cerebras](https://docs.pipecat.ai/server/services/llm/cerebras), [DeepSeek](https://docs.pipecat.ai/server/services/llm/deepseek), [Fireworks AI](https://docs.pipecat.ai/server/services/llm/fireworks), [Gemini](https://docs.pipecat.ai/server/services/llm/gemini), [Grok](https://docs.pipecat.ai/server/services/llm/grok), [Groq](https://docs.pipecat.ai/server/services/llm/groq), [Mistral](https://docs.pipecat.ai/server/services/llm/mistral), [NVIDIA NIM](https://docs.pipecat.ai/server/services/llm/nim), [Ollama](https://docs.pipecat.ai/server/services/llm/ollama), [OpenAI](https://docs.pipecat.ai/server/services/llm/openai), [OpenRouter](https://docs.pipecat.ai/server/services/llm/openrouter), [Perplexity](https://docs.pipecat.ai/server/services/llm/perplexity), [Qwen](https://docs.pipecat.ai/server/services/llm/qwen), [SambaNova](https://docs.pipecat.ai/server/services/llm/sambanova), [Sarvam](https://docs.pipecat.ai/server/services/llm/sarvam), [Together AI](https://docs.pipecat.ai/server/services/llm/together) | | Text-to-Speech | [Async](https://docs.pipecat.ai/server/services/tts/asyncai), [AWS](https://docs.pipecat.ai/server/services/tts/aws), [Azure](https://docs.pipecat.ai/server/services/tts/azure), [Camb AI](https://docs.pipecat.ai/server/services/tts/camb), [Cartesia](https://docs.pipecat.ai/server/services/tts/cartesia), [Deepgram](https://docs.pipecat.ai/server/services/tts/deepgram), [ElevenLabs](https://docs.pipecat.ai/server/services/tts/elevenlabs), [Fish](https://docs.pipecat.ai/server/services/tts/fish), [Google](https://docs.pipecat.ai/server/services/tts/google), [Gradium](https://docs.pipecat.ai/server/services/tts/gradium), [Groq](https://docs.pipecat.ai/server/services/tts/groq), [Hume](https://docs.pipecat.ai/server/services/tts/hume), [Inworld](https://docs.pipecat.ai/server/services/tts/inworld), [LMNT](https://docs.pipecat.ai/server/services/tts/lmnt), [MiniMax](https://docs.pipecat.ai/server/services/tts/minimax), [Neuphonic](https://docs.pipecat.ai/server/services/tts/neuphonic), [NVIDIA Riva](https://docs.pipecat.ai/server/services/tts/riva), [OpenAI](https://docs.pipecat.ai/server/services/tts/openai), [Piper](https://docs.pipecat.ai/server/services/tts/piper), [Resemble](https://docs.pipecat.ai/server/services/tts/resemble), [Rime](https://docs.pipecat.ai/server/services/tts/rime), [Sarvam](https://docs.pipecat.ai/server/services/tts/sarvam), [Speechmatics](https://docs.pipecat.ai/server/services/tts/speechmatics), [XTTS](https://docs.pipecat.ai/server/services/tts/xtts) | | Speech-to-Speech | [AWS Nova Sonic](https://docs.pipecat.ai/server/services/s2s/aws), [Gemini Multimodal Live](https://docs.pipecat.ai/server/services/s2s/gemini), [Grok Voice Agent](https://docs.pipecat.ai/server/services/s2s/grok), [OpenAI Realtime](https://docs.pipecat.ai/server/services/s2s/openai), [Ultravox](https://docs.pipecat.ai/server/services/s2s/ultravox), | | Transport | [Daily (WebRTC)](https://docs.pipecat.ai/server/services/transport/daily), [FastAPI Websocket](https://docs.pipecat.ai/server/services/transport/fastapi-websocket), [SmallWebRTCTransport](https://docs.pipecat.ai/server/services/transport/small-webrtc), [WebSocket Server](https://docs.pipecat.ai/server/services/transport/websocket-server), Local | diff --git a/examples/foundational/07z-interruptible-sarvam-http.py b/examples/foundational/07z-interruptible-sarvam-http.py index 76c892fd9..09938cf96 100644 --- a/examples/foundational/07z-interruptible-sarvam-http.py +++ b/examples/foundational/07z-interruptible-sarvam-http.py @@ -23,7 +23,7 @@ from pipecat.processors.aggregators.llm_response_universal import ( ) from pipecat.runner.types import RunnerArguments from pipecat.runner.utils import create_transport -from pipecat.services.openai.llm import OpenAILLMService +from pipecat.services.sarvam.llm import SarvamLLMService from pipecat.services.sarvam.stt import SarvamSTTService from pipecat.services.sarvam.tts import SarvamHttpTTSService from pipecat.transcriptions.language import Language @@ -72,10 +72,10 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): ), ) - llm = OpenAILLMService( - api_key=os.getenv("OPENAI_API_KEY"), - settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + llm = SarvamLLMService( + api_key=os.getenv("SARVAM_API_KEY"), + settings=SarvamLLMService.Settings( + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/07z-interruptible-sarvam.py b/examples/foundational/07z-interruptible-sarvam.py index 915ea2c3d..50192bd7f 100644 --- a/examples/foundational/07z-interruptible-sarvam.py +++ b/examples/foundational/07z-interruptible-sarvam.py @@ -13,7 +13,7 @@ from pipecat.audio.vad.silero import SileroVADAnalyzer from pipecat.frames.frames import LLMRunFrame from pipecat.pipeline.pipeline import Pipeline from pipecat.pipeline.runner import PipelineRunner -from pipecat.pipeline.task import PipelineParams, PipelineTask +from pipecat.pipeline.task import IDLE_TIMEOUT_SECS, PipelineParams, PipelineTask from pipecat.processors.aggregators.llm_context import LLMContext from pipecat.processors.aggregators.llm_response_universal import ( LLMContextAggregatorPair, @@ -21,7 +21,7 @@ from pipecat.processors.aggregators.llm_response_universal import ( ) from pipecat.runner.types import RunnerArguments from pipecat.runner.utils import create_transport -from pipecat.services.openai.llm import OpenAILLMService +from pipecat.services.sarvam.llm import SarvamLLMService from pipecat.services.sarvam.stt import SarvamSTTService from pipecat.services.sarvam.tts import SarvamTTSService from pipecat.transports.base_transport import BaseTransport, TransportParams @@ -55,21 +55,21 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): stt = SarvamSTTService( api_key=os.getenv("SARVAM_API_KEY"), settings=SarvamSTTService.Settings( - model="saarika:v2.5", + model="saaras:v3", ), ) tts = SarvamTTSService( api_key=os.getenv("SARVAM_API_KEY"), settings=SarvamTTSService.Settings( - model="bulbul:v2", - voice="manisha", + model="bulbul:v3", + voice="shubh", ), ) - llm = OpenAILLMService( - api_key=os.getenv("OPENAI_API_KEY"), - settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + llm = SarvamLLMService( + api_key=os.getenv("SARVAM_API_KEY"), + settings=SarvamLLMService.Settings( + model="sarvam-30b", ), ) @@ -96,6 +96,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): params=PipelineParams( enable_metrics=True, enable_usage_metrics=True, + allow_interruptions=True, ), ) diff --git a/scripts/evals/run-release-evals.py b/scripts/evals/run-release-evals.py index 625f33564..b96e608c6 100644 --- a/scripts/evals/run-release-evals.py +++ b/scripts/evals/run-release-evals.py @@ -184,6 +184,7 @@ TESTS_14 = [ ("14v-function-calling-openai.py", EVAL_WEATHER), ("14w-function-calling-mistral.py", EVAL_WEATHER), ("14x-function-calling-openpipe.py", EVAL_WEATHER), + ("14y-function-calling-sarvam.py", EVAL_WEATHER), # Video ("14d-function-calling-anthropic-video.py", EVAL_VISION_CAMERA), ("14d-function-calling-aws-video.py", EVAL_VISION_CAMERA), diff --git a/src/pipecat/services/sarvam/llm.py b/src/pipecat/services/sarvam/llm.py index 5b25e3068..3b14273cd 100644 --- a/src/pipecat/services/sarvam/llm.py +++ b/src/pipecat/services/sarvam/llm.py @@ -36,19 +36,16 @@ class SarvamLLMSettings(OpenAILLMSettings): class SarvamLLMService(OpenAILLMService): - """Sarvam LLM service using Sarvam's OpenAI-compatible chat completions API. + """A service for interacting with Sarvam's API using the OpenAI-compatible interface. - This service extends ``OpenAILLMService`` while adding Sarvam-specific behavior: - - - model allow-list validation - - request shaping for Sarvam-compatible parameters - - Sarvam auth header wiring (``api-subscription-key``) - - SDK User-Agent propagation on every API call + This service extends OpenAILLMService to connect to Sarvam's API endpoint while + maintaining full compatibility with OpenAI's interface and functionality. """ _SUPPORTED_MODELS = frozenset( {"sarvam-30b", "sarvam-30b-16k", "sarvam-105b", "sarvam-105b-32k"} ) + _TOOL_CALLING_MODELS = _SUPPORTED_MODELS Settings = SarvamLLMSettings _settings: Settings @@ -70,29 +67,20 @@ class SarvamLLMService(OpenAILLMService): default_headers: Additional HTTP headers to include in requests. **kwargs: Additional keyword arguments passed to ``OpenAILLMService``. """ - # Initialize defaults with concrete values for Sarvam-specific fields. + # Initialize only Sarvam-specific defaults; inherited defaults are + # provided by the OpenAI base service initialization. default_settings = self.Settings( model="sarvam-30b", - system_instruction=None, - frequency_penalty=NOT_GIVEN, - presence_penalty=NOT_GIVEN, - seed=NOT_GIVEN, - temperature=NOT_GIVEN, - top_p=NOT_GIVEN, - top_k=None, - max_tokens=NOT_GIVEN, - max_completion_tokens=NOT_GIVEN, - filter_incomplete_user_turns=False, - user_turn_completion_config=None, - extra={}, wiki_grounding=None, reasoning_effort=None, ) - # Apply settings delta (canonical API, always wins). + # Apply settings delta (canonical API, always wins) if settings is not None: default_settings.apply_update(settings) + self._validate_model(default_settings.model) + super().__init__( api_key=api_key, base_url=base_url, @@ -100,9 +88,12 @@ class SarvamLLMService(OpenAILLMService): default_headers=default_headers, **kwargs, ) - # Keep Sarvam-specific settings object so runtime updates include - # ``wiki_grounding`` and ``reasoning_effort`` without extra bridging. - self._settings = default_settings + # Rehydrate into Sarvam settings using inherited concrete store values + # from base initialization, then layer Sarvam-specific fields. + sarvam_settings = self.Settings.from_mapping(self._settings.given_fields()) + sarvam_settings.wiki_grounding = default_settings.wiki_grounding + sarvam_settings.reasoning_effort = default_settings.reasoning_effort + self._settings = sarvam_settings def create_client( self, @@ -156,43 +147,10 @@ class SarvamLLMService(OpenAILLMService): return params - async def _call_with_raw_sarvam_errors(self, awaitable: Awaitable[_T]) -> _T: - """Await an OpenAI call while preserving Sarvam raw error payloads. - - BaseOpenAILLMService handles pipeline-frame exceptions via push_error(), - but direct helper methods like ``get_chat_completions`` and - ``run_inference`` are often consumed directly. We normalize those errors - here so applications consistently receive server-provided messages. - """ - try: - return await awaitable - except (APITimeoutError, asyncio.TimeoutError, httpx.TimeoutException): - raise - except Exception as e: - raise RuntimeError(str(e)) from e - - async def get_chat_completions( - self, params_from_context: OpenAILLMInvocationParams - ) -> AsyncStream[ChatCompletionChunk]: - """Get streaming chat completions with Sarvam raw error passthrough.""" - return await self._call_with_raw_sarvam_errors( - super().get_chat_completions(params_from_context) - ) - - async def run_inference( - self, - context: LLMContext | OpenAILLMContext, - max_tokens: Optional[int] = None, - system_instruction: Optional[str] = None, - ) -> Optional[str]: - """Run one-shot inference and preserve Sarvam raw server errors.""" - return await self._call_with_raw_sarvam_errors( - super().run_inference( - context, - max_tokens=max_tokens, - system_instruction=system_instruction, - ) - ) + def _validate_model(self, model: str): + if model not in self._SUPPORTED_MODELS: + allowed = ", ".join(sorted(self._SUPPORTED_MODELS)) + raise ValueError(f"Unsupported Sarvam LLM model '{model}'. Allowed values: {allowed}.") def _validate_tool_parameters(self, params_from_context: OpenAILLMInvocationParams): tools = params_from_context.get("tools", NOT_GIVEN) From 1c8a8f51d46a4e23f74b68b26902fa7b837b7e54 Mon Sep 17 00:00:00 2001 From: Mark Backman Date: Tue, 24 Mar 2026 08:46:03 -0400 Subject: [PATCH 7/8] Code review fixes --- README.md | 4 +- .../07z-interruptible-sarvam-http.py | 4 +- .../foundational/07z-interruptible-sarvam.py | 6 +- .../14y-function-calling-sarvam.py | 177 ++++++++++++++++++ src/pipecat/services/sarvam/llm.py | 7 - 5 files changed, 184 insertions(+), 14 deletions(-) create mode 100644 examples/foundational/14y-function-calling-sarvam.py diff --git a/README.md b/README.md index afc866d7d..f00234cbc 100644 --- a/README.md +++ b/README.md @@ -88,7 +88,7 @@ Catch new features, interviews, and how-tos on our [Pipecat TV](https://www.yout | Category | Services | | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Speech-to-Text | [AssemblyAI](https://docs.pipecat.ai/server/services/stt/assemblyai), [AWS](https://docs.pipecat.ai/server/services/stt/aws), [Azure](https://docs.pipecat.ai/server/services/stt/azure), [Cartesia](https://docs.pipecat.ai/server/services/stt/cartesia), [Deepgram](https://docs.pipecat.ai/server/services/stt/deepgram), [ElevenLabs](https://docs.pipecat.ai/server/services/stt/elevenlabs), [Fal Wizper](https://docs.pipecat.ai/server/services/stt/fal), [Gladia](https://docs.pipecat.ai/server/services/stt/gladia), [Google](https://docs.pipecat.ai/server/services/stt/google), [Gradium](https://docs.pipecat.ai/server/services/stt/gradium), [Groq (Whisper)](https://docs.pipecat.ai/server/services/stt/groq), [NVIDIA Riva](https://docs.pipecat.ai/server/services/stt/riva), [OpenAI (Whisper)](https://docs.pipecat.ai/server/services/stt/openai), [SambaNova (Whisper)](https://docs.pipecat.ai/server/services/stt/sambanova), [Sarvam](https://docs.pipecat.ai/server/services/stt/sarvam), [Soniox](https://docs.pipecat.ai/server/services/stt/soniox), [Speechmatics](https://docs.pipecat.ai/server/services/stt/speechmatics), [Whisper](https://docs.pipecat.ai/server/services/stt/whisper) | -| LLMs | [Anthropic](https://docs.pipecat.ai/server/services/llm/anthropic), [AWS](https://docs.pipecat.ai/server/services/llm/aws), [Azure](https://docs.pipecat.ai/server/services/llm/azure), [Cerebras](https://docs.pipecat.ai/server/services/llm/cerebras), [DeepSeek](https://docs.pipecat.ai/server/services/llm/deepseek), [Fireworks AI](https://docs.pipecat.ai/server/services/llm/fireworks), [Gemini](https://docs.pipecat.ai/server/services/llm/gemini), [Grok](https://docs.pipecat.ai/server/services/llm/grok), [Groq](https://docs.pipecat.ai/server/services/llm/groq), [Mistral](https://docs.pipecat.ai/server/services/llm/mistral), [NVIDIA NIM](https://docs.pipecat.ai/server/services/llm/nim), [Ollama](https://docs.pipecat.ai/server/services/llm/ollama), [OpenAI](https://docs.pipecat.ai/server/services/llm/openai), [OpenRouter](https://docs.pipecat.ai/server/services/llm/openrouter), [Perplexity](https://docs.pipecat.ai/server/services/llm/perplexity), [Qwen](https://docs.pipecat.ai/server/services/llm/qwen), [SambaNova](https://docs.pipecat.ai/server/services/llm/sambanova), [Sarvam](https://docs.pipecat.ai/server/services/llm/sarvam), [Together AI](https://docs.pipecat.ai/server/services/llm/together) | +| LLMs | [Anthropic](https://docs.pipecat.ai/server/services/llm/anthropic), [AWS](https://docs.pipecat.ai/server/services/llm/aws), [Azure](https://docs.pipecat.ai/server/services/llm/azure), [Cerebras](https://docs.pipecat.ai/server/services/llm/cerebras), [DeepSeek](https://docs.pipecat.ai/server/services/llm/deepseek), [Fireworks AI](https://docs.pipecat.ai/server/services/llm/fireworks), [Gemini](https://docs.pipecat.ai/server/services/llm/gemini), [Grok](https://docs.pipecat.ai/server/services/llm/grok), [Groq](https://docs.pipecat.ai/server/services/llm/groq), [Mistral](https://docs.pipecat.ai/server/services/llm/mistral), [NVIDIA NIM](https://docs.pipecat.ai/server/services/llm/nim), [Ollama](https://docs.pipecat.ai/server/services/llm/ollama), [OpenAI](https://docs.pipecat.ai/server/services/llm/openai), [OpenRouter](https://docs.pipecat.ai/server/services/llm/openrouter), [Perplexity](https://docs.pipecat.ai/server/services/llm/perplexity), [Qwen](https://docs.pipecat.ai/server/services/llm/qwen), [SambaNova](https://docs.pipecat.ai/server/services/llm/sambanova), [Sarvam](https://docs.pipecat.ai/server/services/llm/sarvam), [Together AI](https://docs.pipecat.ai/server/services/llm/together) | | Text-to-Speech | [Async](https://docs.pipecat.ai/server/services/tts/asyncai), [AWS](https://docs.pipecat.ai/server/services/tts/aws), [Azure](https://docs.pipecat.ai/server/services/tts/azure), [Camb AI](https://docs.pipecat.ai/server/services/tts/camb), [Cartesia](https://docs.pipecat.ai/server/services/tts/cartesia), [Deepgram](https://docs.pipecat.ai/server/services/tts/deepgram), [ElevenLabs](https://docs.pipecat.ai/server/services/tts/elevenlabs), [Fish](https://docs.pipecat.ai/server/services/tts/fish), [Google](https://docs.pipecat.ai/server/services/tts/google), [Gradium](https://docs.pipecat.ai/server/services/tts/gradium), [Groq](https://docs.pipecat.ai/server/services/tts/groq), [Hume](https://docs.pipecat.ai/server/services/tts/hume), [Inworld](https://docs.pipecat.ai/server/services/tts/inworld), [LMNT](https://docs.pipecat.ai/server/services/tts/lmnt), [MiniMax](https://docs.pipecat.ai/server/services/tts/minimax), [Neuphonic](https://docs.pipecat.ai/server/services/tts/neuphonic), [NVIDIA Riva](https://docs.pipecat.ai/server/services/tts/riva), [OpenAI](https://docs.pipecat.ai/server/services/tts/openai), [Piper](https://docs.pipecat.ai/server/services/tts/piper), [Resemble](https://docs.pipecat.ai/server/services/tts/resemble), [Rime](https://docs.pipecat.ai/server/services/tts/rime), [Sarvam](https://docs.pipecat.ai/server/services/tts/sarvam), [Speechmatics](https://docs.pipecat.ai/server/services/tts/speechmatics), [XTTS](https://docs.pipecat.ai/server/services/tts/xtts) | | Speech-to-Speech | [AWS Nova Sonic](https://docs.pipecat.ai/server/services/s2s/aws), [Gemini Multimodal Live](https://docs.pipecat.ai/server/services/s2s/gemini), [Grok Voice Agent](https://docs.pipecat.ai/server/services/s2s/grok), [OpenAI Realtime](https://docs.pipecat.ai/server/services/s2s/openai), [Ultravox](https://docs.pipecat.ai/server/services/s2s/ultravox), | | Transport | [Daily (WebRTC)](https://docs.pipecat.ai/server/services/transport/daily), [FastAPI Websocket](https://docs.pipecat.ai/server/services/transport/fastapi-websocket), [SmallWebRTCTransport](https://docs.pipecat.ai/server/services/transport/small-webrtc), [WebSocket Server](https://docs.pipecat.ai/server/services/transport/websocket-server), Local | @@ -98,7 +98,7 @@ Catch new features, interviews, and how-tos on our [Pipecat TV](https://www.yout | Vision & Image | [fal](https://docs.pipecat.ai/server/services/image-generation/fal), [Google Imagen](https://docs.pipecat.ai/server/services/image-generation/google-imagen), [Moondream](https://docs.pipecat.ai/server/services/vision/moondream) | | Audio Processing | [Silero VAD](https://docs.pipecat.ai/server/utilities/audio/silero-vad-analyzer), [Krisp](https://docs.pipecat.ai/server/utilities/audio/krisp-filter), [Koala](https://docs.pipecat.ai/server/utilities/audio/koala-filter), [ai-coustics](https://docs.pipecat.ai/server/utilities/audio/aic-filter) | | Analytics & Metrics | [OpenTelemetry](https://docs.pipecat.ai/server/utilities/opentelemetry), [Sentry](https://docs.pipecat.ai/server/services/analytics/sentry) | -| Community | [Browse community integrations →](https://docs.pipecat.ai/server/services/community-integrations) | +| Community | [Browse community integrations →](https://docs.pipecat.ai/server/services/community-integrations) | 📚 [View full services documentation →](https://docs.pipecat.ai/server/services/supported-services) diff --git a/examples/foundational/07z-interruptible-sarvam-http.py b/examples/foundational/07z-interruptible-sarvam-http.py index 8fc5a9d28..a9cb55dcc 100644 --- a/examples/foundational/07z-interruptible-sarvam-http.py +++ b/examples/foundational/07z-interruptible-sarvam-http.py @@ -72,9 +72,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): ), ) - llm = OpenAILLMService( + llm = SarvamLLMService( api_key=os.getenv("OPENAI_API_KEY"), - settings=OpenAILLMService.Settings( + settings=SarvamLLMService.Settings( system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/07z-interruptible-sarvam.py b/examples/foundational/07z-interruptible-sarvam.py index ed8a01c58..17bcbdbb3 100644 --- a/examples/foundational/07z-interruptible-sarvam.py +++ b/examples/foundational/07z-interruptible-sarvam.py @@ -13,7 +13,7 @@ from pipecat.audio.vad.silero import SileroVADAnalyzer from pipecat.frames.frames import LLMRunFrame from pipecat.pipeline.pipeline import Pipeline from pipecat.pipeline.runner import PipelineRunner -from pipecat.pipeline.task import IDLE_TIMEOUT_SECS, PipelineParams, PipelineTask +from pipecat.pipeline.task import PipelineParams, PipelineTask from pipecat.processors.aggregators.llm_context import LLMContext from pipecat.processors.aggregators.llm_response_universal import ( LLMContextAggregatorPair, @@ -66,9 +66,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): voice="shubh", ), ) - llm = OpenAILLMService( + llm = SarvamLLMService( api_key=os.getenv("OPENAI_API_KEY"), - settings=OpenAILLMService.Settings( + settings=SarvamLLMService.Settings( system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/14y-function-calling-sarvam.py b/examples/foundational/14y-function-calling-sarvam.py new file mode 100644 index 000000000..5cadc8210 --- /dev/null +++ b/examples/foundational/14y-function-calling-sarvam.py @@ -0,0 +1,177 @@ +# +# Copyright (c) 2024-2026, Daily +# +# SPDX-License-Identifier: BSD 2-Clause License +# + +import os + +from dotenv import load_dotenv +from loguru import logger + +from pipecat.adapters.schemas.function_schema import FunctionSchema +from pipecat.adapters.schemas.tools_schema import ToolsSchema +from pipecat.audio.vad.silero import SileroVADAnalyzer +from pipecat.frames.frames import LLMRunFrame, TTSSpeakFrame +from pipecat.pipeline.pipeline import Pipeline +from pipecat.pipeline.runner import PipelineRunner +from pipecat.pipeline.task import PipelineParams, PipelineTask +from pipecat.processors.aggregators.llm_context import LLMContext +from pipecat.processors.aggregators.llm_response_universal import ( + LLMContextAggregatorPair, + LLMUserAggregatorParams, +) +from pipecat.runner.types import RunnerArguments +from pipecat.runner.utils import create_transport +from pipecat.services.llm_service import FunctionCallParams +from pipecat.services.sarvam.llm import SarvamLLMService +from pipecat.services.sarvam.stt import SarvamSTTService +from pipecat.services.sarvam.tts import SarvamTTSService +from pipecat.transports.base_transport import BaseTransport, TransportParams +from pipecat.transports.daily.transport import DailyParams +from pipecat.transports.websocket.fastapi import FastAPIWebsocketParams + +load_dotenv(override=True) + + +async def fetch_weather_from_api(params: FunctionCallParams): + await params.result_callback({"conditions": "nice", "temperature": "75"}) + + +async def fetch_restaurant_recommendation(params: FunctionCallParams): + await params.result_callback({"name": "The Golden Dragon"}) + + +# We use lambdas to defer transport parameter creation until the transport +# type is selected at runtime. +transport_params = { + "daily": lambda: DailyParams( + audio_in_enabled=True, + audio_out_enabled=True, + ), + "twilio": lambda: FastAPIWebsocketParams( + audio_in_enabled=True, + audio_out_enabled=True, + ), + "webrtc": lambda: TransportParams( + audio_in_enabled=True, + audio_out_enabled=True, + ), +} + + +async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): + logger.info(f"Starting bot") + + stt = SarvamSTTService( + api_key=os.getenv("SARVAM_API_KEY"), + settings=SarvamSTTService.Settings( + model="saaras:v3", + ), + ) + + tts = SarvamTTSService( + api_key=os.getenv("SARVAM_API_KEY"), + settings=SarvamTTSService.Settings( + model="bulbul:v3", + voice="shubh", + ), + ) + llm = SarvamLLMService( + api_key=os.getenv("OPENAI_API_KEY"), + settings=SarvamLLMService.Settings( + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", + ), + ) + + # You can also register a function_name of None to get all functions + # sent to the same callback with an additional function_name parameter. + llm.register_function("get_current_weather", fetch_weather_from_api) + llm.register_function("get_restaurant_recommendation", fetch_restaurant_recommendation) + + @llm.event_handler("on_function_calls_started") + async def on_function_calls_started(service, function_calls): + await tts.queue_frame(TTSSpeakFrame("Let me check on that.")) + + weather_function = FunctionSchema( + name="get_current_weather", + description="Get the current weather", + properties={ + "location": { + "type": "string", + "description": "The city and state, e.g. San Francisco, CA", + }, + "format": { + "type": "string", + "enum": ["celsius", "fahrenheit"], + "description": "The temperature unit to use. Infer this from the user's location.", + }, + }, + required=["location", "format"], + ) + restaurant_function = FunctionSchema( + name="get_restaurant_recommendation", + description="Get a restaurant recommendation", + properties={ + "location": { + "type": "string", + "description": "The city and state, e.g. San Francisco, CA", + }, + }, + required=["location"], + ) + tools = ToolsSchema(standard_tools=[weather_function, restaurant_function]) + + context = LLMContext(tools=tools) + user_aggregator, assistant_aggregator = LLMContextAggregatorPair( + context, + user_params=LLMUserAggregatorParams(vad_analyzer=SileroVADAnalyzer()), + ) + + pipeline = Pipeline( + [ + transport.input(), + stt, + user_aggregator, + llm, + tts, + transport.output(), + assistant_aggregator, + ] + ) + + task = PipelineTask( + pipeline, + params=PipelineParams( + enable_metrics=True, + enable_usage_metrics=True, + ), + idle_timeout_secs=runner_args.pipeline_idle_timeout_secs, + ) + + @transport.event_handler("on_client_connected") + async def on_client_connected(transport, client): + logger.info(f"Client connected") + # Kick off the conversation. + await task.queue_frames([LLMRunFrame()]) + + @transport.event_handler("on_client_disconnected") + async def on_client_disconnected(transport, client): + logger.info(f"Client disconnected") + await task.cancel() + + runner = PipelineRunner(handle_sigint=runner_args.handle_sigint) + + await runner.run(task) + + +async def bot(runner_args: RunnerArguments): + """Main bot entry point compatible with Pipecat Cloud.""" + transport = await create_transport(runner_args, transport_params) + await run_bot(transport, runner_args) + + +if __name__ == "__main__": + from pipecat.runner.run import main + + main() diff --git a/src/pipecat/services/sarvam/llm.py b/src/pipecat/services/sarvam/llm.py index 3b14273cd..c1fc6990e 100644 --- a/src/pipecat/services/sarvam/llm.py +++ b/src/pipecat/services/sarvam/llm.py @@ -45,7 +45,6 @@ class SarvamLLMService(OpenAILLMService): _SUPPORTED_MODELS = frozenset( {"sarvam-30b", "sarvam-30b-16k", "sarvam-105b", "sarvam-105b-32k"} ) - _TOOL_CALLING_MODELS = _SUPPORTED_MODELS Settings = SarvamLLMSettings _settings: Settings @@ -88,12 +87,6 @@ class SarvamLLMService(OpenAILLMService): default_headers=default_headers, **kwargs, ) - # Rehydrate into Sarvam settings using inherited concrete store values - # from base initialization, then layer Sarvam-specific fields. - sarvam_settings = self.Settings.from_mapping(self._settings.given_fields()) - sarvam_settings.wiki_grounding = default_settings.wiki_grounding - sarvam_settings.reasoning_effort = default_settings.reasoning_effort - self._settings = sarvam_settings def create_client( self, From cdd8c3e5bb125908a6d1d6a5c23d9bc46ef00760 Mon Sep 17 00:00:00 2001 From: Mark Backman Date: Tue, 24 Mar 2026 08:53:56 -0400 Subject: [PATCH 8/8] Fix examples --- examples/foundational/07z-interruptible-sarvam-http.py | 2 +- examples/foundational/07z-interruptible-sarvam.py | 2 +- examples/foundational/14y-function-calling-sarvam.py | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/examples/foundational/07z-interruptible-sarvam-http.py b/examples/foundational/07z-interruptible-sarvam-http.py index a9cb55dcc..09938cf96 100644 --- a/examples/foundational/07z-interruptible-sarvam-http.py +++ b/examples/foundational/07z-interruptible-sarvam-http.py @@ -73,7 +73,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): ) llm = SarvamLLMService( - api_key=os.getenv("OPENAI_API_KEY"), + api_key=os.getenv("SARVAM_API_KEY"), settings=SarvamLLMService.Settings( system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), diff --git a/examples/foundational/07z-interruptible-sarvam.py b/examples/foundational/07z-interruptible-sarvam.py index 17bcbdbb3..031b82116 100644 --- a/examples/foundational/07z-interruptible-sarvam.py +++ b/examples/foundational/07z-interruptible-sarvam.py @@ -67,7 +67,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): ), ) llm = SarvamLLMService( - api_key=os.getenv("OPENAI_API_KEY"), + api_key=os.getenv("SARVAM_API_KEY"), settings=SarvamLLMService.Settings( system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), diff --git a/examples/foundational/14y-function-calling-sarvam.py b/examples/foundational/14y-function-calling-sarvam.py index 5cadc8210..fece01a3a 100644 --- a/examples/foundational/14y-function-calling-sarvam.py +++ b/examples/foundational/14y-function-calling-sarvam.py @@ -78,7 +78,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): ), ) llm = SarvamLLMService( - api_key=os.getenv("OPENAI_API_KEY"), + api_key=os.getenv("SARVAM_API_KEY"), settings=SarvamLLMService.Settings( system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), @@ -153,6 +153,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): async def on_client_connected(transport, client): logger.info(f"Client connected") # Kick off the conversation. + context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) await task.queue_frames([LLMRunFrame()]) @transport.event_handler("on_client_disconnected")