wooden-smartphone-55787
12/24/2025, 12:21 PMtall-airline-16756
12/24/2025, 12:21 PMtall-airline-16756
12/24/2025, 12:21 PMgoogle.realtime.RealtimeModel (Live API) or a standard LLM integration?
• Which exact model ID are you calling?
• Is the object appearing in streaming chunks or only in the final output?
• If possible, paste a small redacted example of the output you’re receiving.
What you can try now
• If you’re using google.realtime.RealtimeModel, disable thoughts by setting thinking_config.include_thoughts=False. Example:
from livekit.plugins import google
session = AgentSession(
llm=google.realtime.RealtimeModel(
model="your-gemini-model-id",
thinking_config=google.types.ThinkingConfig(include_thoughts=False),
),
)
This suppresses the thought field so only the final response is returned.
• If you’re not using the Live API (standard LLM), the thought content is likely coming from the model or prompt formatting. Two approaches:
◦ Adjust the prompt to not request chain-of-thought or internal reasoning.
◦ Add a small output-filtering step (llm_node override) that parses the model’s JSON and returns only the response field.
Next step Reply with the clarifying answers (Realtime vs LLM, model ID, streaming vs final, and a sample output) and I’ll give the exact code snippet or prompt change tailored to your setup.
Sources: Gemini Live API plugin | LiveKit docs | LLM Output Replacement | LiveKit docs | Modifying LLM output before TTS | LiveKit docswooden-smartphone-55787
12/24/2025, 12:21 PMtall-airline-16756
12/24/2025, 12:22 PMthought field. Return only the final answer as plain text."
◦ Place that instruction clearly in system or initial prompt so the model treats it as a rule.
• Post-process the model output (safe fallback):
◦ Parse the returned JSON and extract the response field, discarding thought.
◦ If model sometimes returns plain text and sometimes structured JSON, detect JSON and fall back to parsing; otherwise use the raw string.
• If you use an output node / pipeline (LLM node):
◦ Add a small filter that checks for a JSON object with response and returns that string.
If you want, tell me:
• A redacted sample of the actual output you’re receiving (one example).
• The client/library you’re calling the model with (SDK, HTTP API, or custom wrapper).
I’ll provide a concrete prompt template or a short parsing snippet tailored to your stack.wooden-smartphone-55787
12/24/2025, 12:22 PMresponse and returns that string.
how to acheive thistall-airline-16756
12/24/2025, 12:23 PMllm_node) that detects JSON output with a response field and returns only that string. Below is a concise, actionable Python example you can drop into your Agent to do exactly that.
Note: This example assumes a non-realtime Gemini model streaming JSON chunks. If you’re using a different language or realtime model, tell me and I’ll adapt it.
1) Minimal TypedDict for expected structure
from typing import TypedDict, Optional
class GeminiResponse(TypedDict, total=False):
thought: Optional[str]
response: str
2) Override llm_node to parse streamed JSON and yield only response
import json
from json_repair import from_json # pip install json-repair
from typing import AsyncIterable
from livekit.agents import Agent, ChatContext, FunctionTool, ModelSettings
from livekit.agents.llm import ChatChunk
async def llm_node(
self,
chat_ctx: ChatContext,
tools: list[FunctionTool],
model_settings: ModelSettings,
) -> AsyncIterable[ChatChunk]:
async def stream_processor():
accumulated = ""
last_yielded = ""
# Stream from default implementation
async for chunk in Agent.default.llm_node(self, chat_ctx, tools, model_settings):
# Extract text from chunk (handle string or ChatChunk)
content = chunk if isinstance(chunk, str) else getattr(chunk.delta, "content", None)
if not content:
yield chunk
continue
accumulated += content
# Try to parse incremental/partial JSON
try:
parsed: GeminiResponse = from_json(accumulated, allow_partial="trailing-strings")
response_text = parsed.get("response", "")
# Yield only new portion of response
new_part = response_text[len(last_yielded):]
if new_part:
last_yielded = response_text
yield chunk.model_copy(update={
"delta": ChatChunk.TextDelta(content=new_part, end_of_part=True)
})
# If you want to ignore thoughts, simply do nothing with parsed.get("thought")
except json.JSONDecodeError:
# not valid JSON yet — keep accumulating
continue
# Fallback: if no JSON parsed, optionally emit raw accumulated text
if not last_yielded and accumulated.strip():
yield ChatChunk(delta=ChatChunk.TextDelta(content=accumulated, end_of_part=True))
return stream_processor()
3) Important notes and options
• Use json-repair to handle partial/streamed JSON: pip install json-repair.
• If the model sometimes returns plain text, the final fallback emits the raw accumulated content.
• If you don’t want any streaming parsing, you can buffer entire output and parse once at the end (simpler but increases latency).
• If you want to log or store thought, read parsed.get("thought") and handle it (e.g., save or ignore).
If you want, tell me:
• the SDK/language you use (Node.js or Python),
• an example raw output you get,
and I’ll adapt the snippet or provide a non-streaming (post-process) variant.
Sources: Pipeline nodes and hooks | LiveKit docs | Tool definition and use | LiveKit docswooden-smartphone-55787
12/24/2025, 12:28 PMtall-airline-16756
12/24/2025, 12:28 PMllm_node in your Agent subclass. Do it directly on the class you use to run the agent so LiveKit uses your custom node instead of the default.
Concise Python example showing where to place it:
from livekit.agents import Agent, AgentSession
# (import other types used in the llm_node implementation)
class MyAgent(Agent):
async def llm_node(self, chat_ctx, tools, model_settings):
# paste the stream_processor implementation here (from previous snippet)
return await super().llm_node(chat_ctx, tools, model_settings) # replace with custom stream_processor()
# create session with your agent
agent = MyAgent()
session = AgentSession(agent=agent, ...)
Tips:
• Return an AsyncIterable[ChatChunk] as shown in the earlier snippet.
• Keep a fallback to emit raw text if JSON parsing never succeeds.
• Test first with buffering (parse after full output) if streaming parsing is tricky, then move to incremental parsing for lower latency.
• If you want a Node.js version or a final drop-in snippet (full method body inserted), tell me and I’ll provide it.wooden-smartphone-55787
12/26/2025, 8:45 AMtall-airline-16756
12/26/2025, 8:45 AMMyAgent (subclassing Agent) and overrides llm_node to stream, parse incremental JSON, and yield only the response text. It also includes a fallback that emits raw text if JSON never parses. Adjust imports, model ID, and session setup to match your environment.
# Install dependency:
# pip install json-repair
import json
from json_repair import from_json # helps parse partial/incremental JSON
from typing import AsyncIterable, Optional, TypedDict
from livekit.agents import Agent, AgentSession, ChatContext, FunctionTool, ModelSettings
from livekit.agents.llm import ChatChunk
class GeminiResponse(TypedDict, total=False):
thought: Optional[str]
response: str
class MyAgent(Agent):
async def llm_node(
self,
chat_ctx: ChatContext,
tools: list[FunctionTool],
model_settings: ModelSettings,
) -> AsyncIterable[ChatChunk]:
async def stream_processor():
accumulated = ""
last_yielded = ""
# Stream from the default LLM node implementation
async for chunk in Agent.default.llm_node(self, chat_ctx, tools, model_settings):
# Extract text content from chunk (support ChatChunk or raw str)
content = chunk if isinstance(chunk, str) else getattr(chunk.delta, "content", None)
if not content:
# yield non-text chunks unchanged
yield chunk
continue
accumulated += content
# Try to parse accumulated text as (partial) JSON
try:
parsed: GeminiResponse = from_json(accumulated, allow_partial="trailing-strings")
response_text = parsed.get("response", "")
# Determine the new portion to yield
new_part = response_text[len(last_yielded):]
if new_part:
last_yielded = response_text
# Create a ChatChunk with only the new response content
yield chunk.model_copy(update={
"delta": ChatChunk.TextDelta(content=new_part, end_of_part=True)
})
# Optionally handle thought: parsed.get("thought")
# e.g., log or ignore
except json.JSONDecodeError:
# Not valid JSON yet — keep accumulating until it is
continue
# Final fallback: if no JSON parsed, emit accumulated raw text
if not last_yielded and accumulated.strip():
yield ChatChunk(delta=ChatChunk.TextDelta(content=accumulated, end_of_part=True))
return stream_processor()
# Example AgentSession usage (adjust model and other settings as needed)
if __name__ == "__main__":
# Create agent instance
agent = MyAgent()
# Example session creation — replace parameters with your config
session = AgentSession(
agent=agent,
# ... configure llm, stt, tts, etc. as required by your setup
)
# Run or attach session per your application flow.
# (This demo does not start a live session; integrate into your runtime.)
If you want:
• A non-streaming simpler variant (buffer full output then parse once) — I can provide that.
• A Node.js version of the override — tell me and I’ll translate.wooden-smartphone-55787
12/26/2025, 8:51 AMtall-airline-16756
12/26/2025, 8:52 AMfrom_json import can fail because that function isn’t in the json_repair package. Below is a complete, working MyAgent Python example that does not rely on json_repair. It uses a small incremental JSON extractor (counts braces) to handle streamed/partial JSON and yields only the response field — falling back to raw text if parsing never completes.
Paste this into your project and replace session/LLM config as needed.
import json
from typing import AsyncIterable, Optional, TypedDict
from livekit.agents import Agent, AgentSession, ChatContext, FunctionTool, ModelSettings
from livekit.agents.llm import ChatChunk
class GeminiResponse(TypedDict, total=False):
thought: Optional[str]
response: str
class MyAgent(Agent):
async def llm_node(
self,
chat_ctx: ChatContext,
tools: list[FunctionTool],
model_settings: ModelSettings,
) -> AsyncIterable[ChatChunk]:
async def stream_processor():
buffer = ""
last_yielded = ""
# Stream from the default LLM node implementation
async for chunk in Agent.default.llm_node(self, chat_ctx, tools, model_settings):
# Extract text content (support ChatChunk or raw str)
content = chunk if isinstance(chunk, str) else getattr(chunk.delta, "content", None)
if not content:
yield chunk
continue
buffer += content
# Try to find and parse complete JSON objects inside buffer
start = buffer.find("{")
if start == -1:
# no JSON start yet; continue accumulating
continue
# find matching closing brace by tracking nesting
depth = 0
end_index = None
for i, ch in enumerate(buffer[start:], start=start):
if ch == "{":
depth += 1
elif ch == "}":
depth -= 1
if depth == 0:
end_index = i
break
if end_index is None:
# incomplete JSON, keep accumulating
continue
# Extract the full JSON substring and try to parse
full_json = buffer[start : end_index + 1]
try:
parsed = json.loads(full_json)
except json.JSONDecodeError:
# If parse fails, drop the processed part to avoid infinite loop
# (or keep more conservative trimming if needed)
buffer = buffer[end_index + 1 :]
continue
# Remove the processed JSON from buffer
buffer = buffer[end_index + 1 :]
# If parsed contains 'response', yield its new content only
if isinstance(parsed, dict) and "response" in parsed:
response_text = str(parsed.get("response", "") or "")
new_part = response_text[len(last_yielded):]
if new_part:
last_yielded = response_text
yield chunk.model_copy(update={
"delta": ChatChunk.TextDelta(content=new_part, end_of_part=True)
})
# Otherwise ignore or handle 'thought' if you wish
# Loop continues to process further chunks/JSON objects
# Final fallback: if no JSON parsed, emit raw accumulated text
if not last_yielded and buffer.strip():
yield ChatChunk(delta=ChatChunk.TextDelta(content=buffer, end_of_part=True))
return stream_processor()
# Example usage (adapt to your config)
if __name__ == "__main__":
agent = MyAgent()
session = AgentSession(
agent=agent,
# configure llm, stt, tts, etc.
)
# integrate session start into your app flow
If you’d like:
• A simpler non-streaming variant that buffers the entire LLM output and parses once (less complex, higher latency).
• A Node.js version of this extractor.
Which would you prefer?
Sources: Tool definition and use | LiveKit docs