cuddly-vr-85751
12/31/2025, 9:50 AMtall-airline-16756
12/31/2025, 9:50 AMtall-airline-16756
12/31/2025, 9:51 AMcuddly-vr-85751
12/31/2025, 9:51 AMtall-airline-16756
12/31/2025, 9:51 AMLLMStream expects an existing LLM instance (argument llm), not a model name string.
What to change
• Return an openai.LLM instance from get_llm_from_config, not LLMStream.
Example fix:
def get_llm_from_config(llm_config: dict):
provider = llm_config.get("provider", "")
model = llm_config.get("model", "")
if provider == "OPENAI":
return openai.LLM(model=model or "gpt-4.1-mini")
else:
return openai.LLM(model="gpt-4.1-mini")
If you need streaming later
• Create the LLM first, then call .chat() to get a stream and iterate chunks:
llm = openai.LLM(model="gpt-4.1-mini")
async with llm.chat(chat_ctx=chat_ctx) as stream:
async for chunk in stream:
# handle chunk.delta.content
If you want, tell me whether you need streaming output now or later and share the surrounding code where you consume the LLM — I’ll help adapt it.
Sources: LLM Output Replacement | LiveKit docscuddly-vr-85751
12/31/2025, 9:52 AMLLMStream i need o use thistall-airline-16756
12/31/2025, 9:52 AMLLMStream. I can help—quick clarifying questions so I give the exact code you need:
1. Do you want to stream chat responses (i.e., call .chat() and handle incremental deltas) or stream completion tokens from a non-chat API?
2. Where will LLMStream be created/consumed in your code? (show the function or file that will iterate the stream — e.g., job entrypoint or agent handler)
3. Are you using async context (async/await) already where you plan to consume the stream?
Short answer summary while you reply:
• You must pass an LLM instance into LLMStream (not a model string).
• Typical flow: create openai.LLM(...) → call .chat() to get a stream → wrap or consume that stream with LLMStream if you need its helpers.
Share the small snippet where you plan to consume the stream and I’ll provide the exact corrected code.cuddly-vr-85751
12/31/2025, 9:53 AMtall-airline-16756
12/31/2025, 9:53 AM.chat()) and consuming incremental deltas, or a completion-style stream?
3. Share the spot in your code where you plan to consume the stream (the function that will iterate chunks). If you can paste 6–12 lines around that consumer, I’ll adapt the exact LLMStream usage.
4. Confirm you’re using async/await and an async event loop (yes/no).
Answer those and I’ll provide a concise, copy-pasteable example that creates an openai.LLM, gets the chat stream, wraps/uses LLMStream, and shows sending chunks to TTS for voice output.cuddly-vr-85751
12/31/2025, 9:53 AMLLMStream giev codetall-airline-16756
12/31/2025, 9:53 AMopenai.LLM, get a chat stream, wrap/consume it with LLMStream, and forward incremental text chunks to a hypothetical TTS function for voice output.
Notes:
• LLMStream requires an LLM instance (passed via .chat()), not a model string.
• Replace send_to_tts(chunk_text) with your TTS sending function (sync/async as needed).
• This example consumes chunk.delta.content incremental deltas from the chat stream.
import asyncio
from openai import LLM, LLMStream
# Replace with your actual TTS handler
async def send_to_tts(text: str):
# send chunk to your TTS engine (websocket, API, etc.)
print("TTS:", text)
async def stream_llm_to_tts(model: str = "gpt-4.1-mini"):
# 1) create the LLM
llm = LLM(model=model)
# 2) open a chat stream (chat_ctx may include system/user messages)
chat_ctx = {
"messages": [
{"role": "system", "content": "You are a concise voice assistant."},
{"role": "user", "content": "Read this aloud progressively."}
]
}
# 3) use the LLM.chat() async context and consume with LLMStream utilities
async with llm.chat(chat_ctx=chat_ctx) as raw_stream:
stream = LLMStream(llm=llm, stream=raw_stream) # wrap if you want helpers
async for chunk in stream: # chunk is a stream delta object
# incremental text usually at chunk.delta.content
content = getattr(chunk.delta, "content", None)
if content:
# send each incremental piece to TTS
await send_to_tts(content)
# Run example
if __name__ == "__main__":
asyncio.run(stream_llm_to_tts())
If you want:
• A sync-style wrapper for your specific TTS API, paste a small snippet of that API and I’ll adapt this.
• Handling of partial tokens (buffering) or final assembled text before TTS, tell me whether your TTS accepts short fragments or needs full sentences.cuddly-vr-85751
12/31/2025, 9:54 AMtall-airline-16756
12/31/2025, 9:54 AMLLM first; LLMStream cannot be instantiated with a model string. If you need your get_llm_from_config to return something you can later call to stream, return the LLM and create the LLMStream where you actually consume it.
Minimal change to your function (returns an LLM):
def get_llm_from_config(llm_config: dict):
provider = llm_config.get("provider", "")
model = llm_config.get("model", "")
if provider == "OPENAI":
return openai.LLM(model=model or "gpt-4.1-mini")
else:
return openai.LLM(model="gpt-4.1-mini")
Example of creating an LLMStream when consuming the LLM (voice-agent streaming):
async def consume_and_tts(llm, chat_ctx, send_to_tts):
async with llm.chat(chat_ctx=chat_ctx) as raw_stream:
stream = openai.LLMStream(llm=llm, stream=raw_stream)
async for chunk in stream:
content = getattr(chunk.delta, "content", None)
if content:
await send_to_tts(content)
If you truly must have get_llm_from_config return an object named LLMStream, return a thin wrapper object that creates the underlying LLM and exposes an async generator method to stream. I can provide that wrapper if you want — do you want the wrapper returned or keep the LLM return and create LLMStream at consume time?cuddly-vr-85751
12/31/2025, 9:55 AMtall-airline-16756
12/31/2025, 9:55 AMLLMStream to forward incremental text to a TTS function. Drop this into your project (replace auth and TTS send logic).
import asyncio
from openai import LLM, LLMStream
# --- Replace this with your real TTS sender (websocket/API) ---
async def send_to_tts(text: str):
# Example: push incremental text to your TTS engine
print("TTS chunk:", text)
# --- Return an LLM from config (must return LLM, not LLMStream) ---
def get_llm_from_config(llm_config: dict) -> LLM:
provider = llm_config.get("provider", "")
model = llm_config.get("model", "")
# Only OPENAI supported in this example; adjust if you have other providers
if provider == "OPENAI":
return LLM(model=model or "gpt-4.1-mini")
else:
return LLM(model="gpt-4.1-mini")
# --- Helper that creates LLMStream and forwards increments to TTS ---
async def stream_llm_to_tts(llm: LLM, chat_ctx: dict, send_to_tts_fn):
# open a chat stream (chat_ctx contains initial messages)
async with llm.chat(chat_ctx=chat_ctx) as raw_stream:
# Create LLMStream wrapper with the LLM and the raw_stream
stream = LLMStream(llm=llm, stream=raw_stream)
# Iterate incremental chunks
async for chunk in stream:
# incremental content is usually at chunk.delta.content
partial = getattr(chunk.delta, "content", None)
if partial:
# send each partial piece to TTS
await send_to_tts_fn(partial)
# --- Example usage: create LLM from config and start streaming ---
async def main():
llm_config = {"provider": "OPENAI", "model": "gpt-4.1-mini"} # or from your meta_data
llm = get_llm_from_config(llm_config)
chat_ctx = {
"messages": [
{"role": "system", "content": "You are a concise voice assistant."},
{"role": "user", "content": "Read this aloud progressively for TTS."}
]
}
await stream_llm_to_tts(llm, chat_ctx, send_to_tts)
if __name__ == "__main__":
asyncio.run(main())
If you want a wrapper so get_llm_from_config directly returns an object that exposes an async generator (so callers don't need to call .chat()), I can provide that version next. Which do you prefer?cuddly-vr-85751
12/31/2025, 10:49 AMtall-airline-16756
12/31/2025, 10:50 AMagent_function spec using aiohttp. It uses @function_tool() so the agent can call it by name. Replace env/loading of API_KEY and adjust error handling as needed.
import os
import asyncio
import aiohttp
from dotenv import load_dotenv
from livekit.agents import function_tool, RunContext
load_dotenv()
API_KEY = os.getenv("API_KEY") # set in env or .env
@function_tool()
async def get_user_details(context: RunContext, user_id: str):
"""
Fetch user details from external service.
Matches the provided agent_function configuration.
"""
url = f"<https://api.example.com/users/{user_id}>"
headers = {
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json",
}
body = {"user_id": user_id, "include_profile": True}
params = {"verbose": "true"}
timeout = aiohttp.ClientTimeout(total=30) # timeout_seconds
try:
async with aiohttp.ClientSession(timeout=timeout) as session:
async with <http://session.post|session.post>(url, headers=headers, json=body, params=params) as resp:
text = await resp.text()
if resp.status >= 200 and resp.status < 300:
try:
data = await resp.json()
except Exception:
data = text
return {"user_data": data}
else:
return {"error": f"HTTP {resp.status}: {text}"}
except asyncio.TimeoutError:
return {"error": "Request timed out after 30 seconds"}
except Exception as e:
return {"error": f"Request failed: {str(e)}"}
If you want this generated from the agent_function dict dynamically (factory that creates @function_tool wrappers), tell me and I’ll provide that pattern.
Sources: Tool definition and use | LiveKit docscuddly-vr-85751
12/31/2025, 10:50 AMtall-airline-16756
12/31/2025, 10:50 AM@function_tool() so LiveKit will register it.
import os
import re
import asyncio
import aiohttp
from typing import Dict, Any
from livekit.agents import function_tool, RunContext
TEMPLATE_RE = re.compile(r"\{\{([^}]+)\}\}")
def render_template(obj, params: Dict[str, Any], env=os.environ):
"""Recursively replace {{var}} in strings using params then env."""
if isinstance(obj, str):
def repl(m):
key = m.group(1).strip()
return str(params.get(key, env.get(key, "")))
return TEMPLATE_RE.sub(repl, obj)
if isinstance(obj, dict):
return {k: render_template(v, params, env) for k, v in obj.items()}
if isinstance(obj, list):
return [render_template(x, params, env) for x in obj]
return obj
def create_tool_from_spec(spec: Dict[str, Any]):
"""
spec: dict like your agent_function JSON.
Returns: a function decorated with @function_tool that performs the request.
"""
name = spec.get("name")
method = spec.get("method", "GET").upper()
endpoint_tmpl = spec.get("endpoint", "")
headers_tmpl = spec.get("headers", {})
body_tmpl = spec.get("body_template")
params_tmpl = spec.get("query_params", {})
timeout_seconds = spec.get("timeout_seconds", 30)
param_defs = spec.get("params", {})
# Build required param list for runtime validation
required_params = [p for p, d in param_defs.items() if d.get("required")]
@function_tool(name=name)
async def tool_fn(context: RunContext, **call_args):
# Validate required params
for p in required_params:
if p not in call_args or call_args.get(p) in (None, ""):
return {"error": f"Missing required parameter: {p}"}
# Render endpoint, headers, body, query params with incoming args and env
rendered_endpoint = render_template(endpoint_tmpl, call_args)
rendered_headers = render_template(headers_tmpl, call_args)
rendered_body = render_template(body_tmpl, call_args) if body_tmpl is not None else None
rendered_query = render_template(params_tmpl, call_args)
timeout = aiohttp.ClientTimeout(total=timeout_seconds)
try:
async with aiohttp.ClientSession(timeout=timeout) as session:
http_method = getattr(session, method.lower(), None)
if http_method is None:
return {"error": f"Unsupported HTTP method: {method}"}
async with http_method(
rendered_endpoint,
headers=rendered_headers or None,
json=rendered_body if rendered_body is not None else None,
params=rendered_query or None,
) as resp:
text = await resp.text()
if 200 <= resp.status < 300:
try:
data = await resp.json()
except Exception:
data = text
return {"result": data}
else:
return {"error": f"HTTP {resp.status}: {text}"}
except asyncio.TimeoutError:
return {"error": f"Request timed out after {timeout_seconds} seconds"}
except Exception as e:
return {"error": f"Request failed: {str(e)}"}
# Attach metadata for introspection if needed
tool_fn.__name__ = name
tool_fn.__doc__ = spec.get("description", "")
return tool_fn
Usage example (register at runtime):
# your JSON spec
agent_function = {
"name": "get_user_details",
"description": "Fetch user details from external service",
"endpoint": "<https://api.example.com/users/{{user_id}}>",
"method": "POST",
"headers": {
"Authorization": "Bearer {{API_KEY}}",
"Content-Type": "application/json"
},
"body_template": {
"user_id": "{{user_id}}",
"include_profile": True
},
"query_params": {"verbose": "true"},
"params": {
"user_id": {"type": "string", "required": True, "description": "User identifier"}
},
"timeout_seconds": 30
}
# create and register the tool (decorator already registers on import)
dynamic_tool = create_tool_from_spec(agent_function)
# If your framework needs the function object to be imported/loaded, ensure this module is imported
# at agent startup so LiveKit picks up the @function_tool-decorated tool.
# Example local call (for testing) - simulate LiveKit passing RunContext
# await dynamic_tool(RunContext(...), user_id="abc123")
If you want:
• automatic type coercion/validation for params,
• retries/backoff,
• logging or metrics,
I can add those. Which one do you want next?cuddly-vr-85751
12/31/2025, 11:21 AMtall-airline-16756
12/31/2025, 11:22 AM@function_tool at runtime from your JSON spec, (2) validates/constructs the required call data from the spec’s params, and (3) shows how to call the generated tool with the required data.
Paste this module into your agent code and import it at startup so LiveKit registers the dynamic tool.
import os
import re
import asyncio
import aiohttp
from typing import Dict, Any
from livekit.agents import function_tool, RunContext
TEMPLATE_RE = re.compile(r"\{\{([^}]+)\}\}")
def render_template(obj, params: Dict[str, Any], env=os.environ):
if isinstance(obj, str):
def repl(m):
key = m.group(1).strip()
return str(params.get(key, env.get(key, "")))
return TEMPLATE_RE.sub(repl, obj)
if isinstance(obj, dict):
return {k: render_template(v, params, env) for k, v in obj.items()}
if isinstance(obj, list):
return [render_template(x, params, env) for x in obj]
return obj
def build_call_data_from_spec(spec: Dict[str, Any], call_args: Dict[str, Any]):
"""
Validate required params and build a concrete payload dict for the HTTP call.
Returns (ok: bool, payload_or_error: dict or str).
"""
param_defs = spec.get("params", {})
# validate required
for name, defn in param_defs.items():
if defn.get("required") and (name not in call_args or call_args.get(name) in (None, "")):
return False, f"Missing required parameter: {name}"
# All params passed are available to template rendering (and env vars)
return True, call_args
def create_tool_from_spec(spec: Dict[str, Any]):
name = spec.get("name")
method = spec.get("method", "GET").upper()
endpoint_tmpl = spec.get("endpoint", "")
headers_tmpl = spec.get("headers", {})
body_tmpl = spec.get("body_template")
params_tmpl = spec.get("query_params", {})
timeout_seconds = spec.get("timeout_seconds", 30)
@function_tool(name=name)
async def tool_fn(context: RunContext, **call_args):
ok, payload_or_err = build_call_data_from_spec(spec, call_args)
if not ok:
return {"error": payload_or_err}
rendered_endpoint = render_template(endpoint_tmpl, payload_or_err)
rendered_headers = render_template(headers_tmpl, payload_or_err)
rendered_body = render_template(body_tmpl, payload_or_err) if body_tmpl is not None else None
rendered_query = render_template(params_tmpl, payload_or_err)
timeout = aiohttp.ClientTimeout(total=timeout_seconds)
try:
async with aiohttp.ClientSession(timeout=timeout) as session:
http_method = getattr(session, method.lower(), None)
if http_method is None:
return {"error": f"Unsupported HTTP method: {method}"}
async with http_method(
rendered_endpoint,
headers=rendered_headers or None,
json=rendered_body if rendered_body is not None else None,
params=rendered_query or None,
) as resp:
text = await resp.text()
if 200 <= resp.status < 300:
try:
data = await resp.json()
except Exception:
data = text
return {"result": data}
else:
return {"error": f"HTTP {resp.status}: {text}"}
except asyncio.TimeoutError:
return {"error": f"Request timed out after {timeout_seconds} seconds"}
except Exception as e:
return {"error": f"Request failed: {str(e)}"}
tool_fn.__name__ = name
tool_fn.__doc__ = spec.get("description", "")
return tool_fn
# -------------------------
# Example: create + call
# -------------------------
# Your JSON spec
agent_function = {
"name": "get_user_details",
"description": "Fetch user details from external service",
"endpoint": "<https://api.example.com/users/{{user_id}}>",
"method": "POST",
"headers": {
"Authorization": "Bearer {{API_KEY}}",
"Content-Type": "application/json"
},
"body_template": {
"user_id": "{{user_id}}",
"include_profile": True
},
"query_params": {"verbose": "true"},
"params": {
"user_id": {"type": "string", "required": True, "description": "User identifier"}
},
"timeout_seconds": 30
}
# Create and register the tool (decorator registers at import time)
dynamic_tool = create_tool_from_spec(agent_function)
# Example local test call (simulate RunContext)
async def test_call():
# Build the required call data from spec (user supplies these)
call_data = {"user_id": "abc123"}
# Call the function directly for testing (no real RunContext needed for direct call)
res = await dynamic_tool(None, **call_data)
print("tool response:", res)
# For manual testing
if __name__ == "__main__":
asyncio.run(test_call())
How to create the required data for the tool call
• Look at spec["params"]. Required params are those with "required": True.
• Build a dict containing those keys before calling the tool — e.g., {"user_id": "abc123"} for your spec.
• The render_template function will substitute {{user_id}} into endpoint/body/headers and {{API_KEY}} from environment variables.
Quick checklist before calling:
• Ensure required params present in the call dict.
• Ensure env vars used in headers/body (e.g., API_KEY) are set.
• Call the function as await tool_fn(context, **call_data) or let the agent call it via tool invocation.
If you want, I can:
• add type coercion/validation,
• add retry/backoff,
• produce OpenAPI-style param validation errors.
Which enhancement do you want next?cuddly-vr-85751
12/31/2025, 4:46 PMtall-airline-16756
12/31/2025, 4:47 PM@function_tool is safe to be invoked from many sessions at once because the function is stateless (per-call data comes in as arguments).
• Ensure any shared resources used by the tool (file writes, in-memory caches, DB connections, or synchronous blocking calls) are concurrency-safe or use per-session instances.
Practical recommendations
• Use async HTTP (aiohttp) like in the provided code so calls don’t block the event loop.
• Keep tool functions stateless; pass per-call data via arguments.
• If you use environment secrets (API_KEY), read them from env once at startup or via safe secrets storage.
• Add retries/backoff for transient errors and reasonable timeouts (your spec uses 30s).
• Monitor and scale agent servers if load grows (LiveKit scales agent servers and job distribution automatically).
Sizing hints
• Start with recommended baseline resources (for example, ~4 CPU cores and 8GB RAM per agent server) and scale horizontally as concurrency increases.
• If many sessions use heavy STT/LLM/TTS inference simultaneously, ensure your inference quota/plan and agent servers are scaled accordingly.
If you want, I can:
• Add retry/backoff to the dynamic tool,
• Show a concurrency-safe pattern for shared DB or cache access,
• Recommend autoscaling settings based on expected concurrent calls.
Which of these would you like next?
Sources: Server lifecycle | LiveKit docs | examples/frontdesk | livekit/livekit_composite | DeepWiki | Agents telephony integration | LiveKit docs | LiveKit Pricing | Agent server | LiveKit docscuddly-vr-85751
12/31/2025, 4:48 PMtall-airline-16756
12/31/2025, 4:48 PMcuddly-vr-85751
12/31/2025, 4:49 PMtall-airline-16756
12/31/2025, 4:49 PMcuddly-vr-85751
12/31/2025, 4:49 PMtall-airline-16756
12/31/2025, 4:50 PMcuddly-vr-85751
12/31/2025, 4:50 PMtall-airline-16756
12/31/2025, 4:50 PM