diff --git a/examples/foundational/14l-function-calling-openrouter.py b/examples/foundational/14l-function-calling-openrouter.py index 0c885e0ae..2535160e9 100644 --- a/examples/foundational/14l-function-calling-openrouter.py +++ b/examples/foundational/14l-function-calling-openrouter.py @@ -1,3 +1,9 @@ +# +# Copyright (c) 2024–2025, Daily +# +# SPDX-License-Identifier: BSD 2-Clause License +# + import asyncio import os import sys @@ -5,6 +11,7 @@ import sys import aiohttp from dotenv import load_dotenv from loguru import logger +from openai.types.chat import ChatCompletionToolParam from runner import configure from pipecat.audio.vad.silero import SileroVADAnalyzer @@ -12,8 +19,8 @@ from pipecat.pipeline.pipeline import Pipeline from pipecat.pipeline.runner import PipelineRunner from pipecat.pipeline.task import PipelineParams, PipelineTask from pipecat.processors.aggregators.openai_llm_context import OpenAILLMContext -from pipecat.services.openrouter import OpenRouterLLMService from pipecat.services.azure import AzureTTSService +from pipecat.services.openrouter import OpenRouterLLMService from pipecat.transports.services.daily import DailyParams, DailyTransport load_dotenv(override=True) @@ -22,9 +29,17 @@ logger.remove(0) logger.add(sys.stderr, level="DEBUG") -async def get_weather(function_name, tool_call_id, arguments, llm, context, result_callback): - location = arguments["location"] - await result_callback(f"The weather in {location} is currently 72 degrees and sunny.") +async def start_fetch_weather(function_name, llm, context): + # note: we can't push a frame to the LLM here. the bot + # can interrupt itself and/or cause audio overlapping glitches. + # possible question for Aleix and Chad about what the right way + # to trigger speech is, now, with the new queues/async/sync refactors. + # await llm.push_frame(TextFrame("Let me check on that.")) + logger.debug(f"Starting fetch_weather_from_api with function_name: {function_name}") + + +async def fetch_weather_from_api(function_name, tool_call_id, args, llm, context, result_callback): + await result_callback({"conditions": "nice", "temperature": "75"}) async def main(): @@ -51,33 +66,41 @@ async def main(): ) llm = OpenRouterLLMService( - api_key=os.getenv("OPENROUTER_API_KEY"), model="openai/chatgpt-4o-latest" + api_key=os.getenv("OPENROUTER_API_KEY"), model="openai/gpt-4o-2024-11-20" ) - llm.register_function("get_weather", get_weather) + # Register a function_name of None to get all functions + # sent to the same callback with an additional function_name parameter. + llm.register_function(None, fetch_weather_from_api, start_callback=start_fetch_weather) tools = [ - { - "name": "get_weather", - "description": "Get the current weather in a given location", - "input_schema": { - "type": "object", - "properties": { - "location": { - "type": "string", - "description": "The city and state, e.g. San Francisco, CA", - } + ChatCompletionToolParam( + type="function", + function={ + "name": "get_current_weather", + "description": "Get the current weather", + "parameters": { + "type": "object", + "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 users location.", + }, + }, + "required": ["location", "format"], }, - "required": ["location"], }, - } + ) ] - messages = [ { "role": "system", - "content": "You are a helpful assistant and whenever you asked about weather trigger get_weather function.", + "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 converted to audio so don't include special characters in your answers. Respond to what the user said in a creative and helpful way.", }, - {"role": "user", "content": "Say 'hello' to start the conversation."}, ] context = OpenAILLMContext(messages, tools) @@ -85,16 +108,24 @@ async def main(): pipeline = Pipeline( [ - transport.input(), # Transport user input - context_aggregator.user(), # User spoken responses - llm, # LLM - tts, # TTS - transport.output(), # Transport bot output - context_aggregator.assistant(), # Assistant spoken responses and tool context + transport.input(), + context_aggregator.user(), + llm, + tts, + transport.output(), + context_aggregator.assistant(), ] ) - task = PipelineTask(pipeline, PipelineParams(allow_interruptions=True, enable_metrics=True)) + task = PipelineTask( + pipeline, + PipelineParams( + allow_interruptions=True, + enable_metrics=True, + enable_usage_metrics=True, + report_only_initial_ttfb=True, + ), + ) @transport.event_handler("on_first_participant_joined") async def on_first_participant_joined(transport, participant): diff --git a/src/pipecat/services/openrouter.py b/src/pipecat/services/openrouter.py index 5b06dda33..f9067cdb1 100644 --- a/src/pipecat/services/openrouter.py +++ b/src/pipecat/services/openrouter.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2025, Daily +# Copyright (c) 2024–2025, Daily # # SPDX-License-Identifier: BSD 2-Clause License # @@ -23,6 +23,18 @@ except ModuleNotFoundError as e: class OpenRouterLLMService(OpenAILLMService): + """A service for interacting with OpenRouter's API using the OpenAI-compatible interface. + + This service extends OpenAILLMService to connect to OpenRouter's API endpoint while + maintaining full compatibility with OpenAI's interface and functionality. + + Args: + api_key (str): The API key for accessing OpenRouter's API + base_url (str, optional): The base URL for OpenRouter API. Defaults to "https://openrouter.ai/api/v1" + model (str, optional): The model identifier to use. Defaults to "openai/gpt-4o-2024-11-20" + **kwargs: Additional keyword arguments passed to OpenAILLMService + """ + def __init__( self, *, @@ -41,22 +53,3 @@ class OpenRouterLLMService(OpenAILLMService): def create_client(self, api_key=None, base_url=None, **kwargs): logger.debug(f"Creating OpenRouter client with api {base_url}") return super().create_client(api_key, base_url, **kwargs) - - async def get_chat_completions( - self, context: OpenAILLMContext, messages: List[ChatCompletionMessageParam] - ) -> AsyncStream[ChatCompletionChunk]: - params = { - "model": self.model_name, - "stream": True, - "messages": messages, - "temperature": self._settings["temperature"], - "top_p": self._settings["top_p"], - "max_tokens": self._settings["max_tokens"], - "frequency_penalty": self._settings["frequency_penalty"], - "presence_penalty": self._settings["presence_penalty"], - } - - params.update(self._settings["extra"]) - - chunks = await self._client.chat.completions.create(**params) - return chunks