Skip to content

Chat & Streaming

Send a message and get a response:

import asyncio
import os
from liter_llm import create_client
from liter_llm._internal_bindings import ChatCompletionRequest
async def main() -> None:
client = create_client(api_key=os.environ["OPENAI_API_KEY"])
request = ChatCompletionRequest.from_json(
'{"model":"openai/gpt-4o","messages":[{"role":"user","content":"Hello!"}]}'
)
response = await client.chat(request)
print(response.choices[0].message.content)
asyncio.run(main())

Liter-llm uses a provider/model prefix convention. The prefix determines which API endpoint, auth header, and parameter mappings to use:

openai/gpt-4o -> OpenAI
anthropic/claude-sonnet-4-20250514 -> Anthropic
groq/llama3-70b -> Groq
google/gemini-2.0-flash -> Google AI
mistral/mistral-large -> Mistral
bedrock/anthropic.claude-v2 -> AWS Bedrock

Switch providers by changing the model string – no other code changes needed.

Role Purpose
system Sets the assistant’s behavior. Sent once at the start.
user User input – questions, instructions, data.
assistant Previous assistant responses for multi-turn context.
tool Results from tool calls.
developer Developer-level instructions (some providers).

Append the assistant’s response and the next user message, then call chat again:

import asyncio
import json
import os
from liter_llm import create_client
from liter_llm._internal_bindings import ChatCompletionRequest
async def main() -> None:
client = create_client(api_key=os.environ["OPENAI_API_KEY"])
messages = [
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "What is the capital of France?"},
]
first = await client.chat(
ChatCompletionRequest.from_json(json.dumps({"model": "openai/gpt-4o", "messages": messages}))
)
reply = first.choices[0].message.content
print(f"Assistant: {reply}")
messages.append({"role": "assistant", "content": reply})
messages.append({"role": "user", "content": "What about Germany?"})
second = await client.chat(
ChatCompletionRequest.from_json(json.dumps({"model": "openai/gpt-4o", "messages": messages}))
)
print(f"Assistant: {second.choices[0].message.content}")
if second.usage:
print(f"Tokens: {second.usage.prompt_tokens} in, {second.usage.completion_tokens} out")
asyncio.run(main())

Stream tokens as they arrive instead of waiting for the full response:

import asyncio
import os
from liter_llm import create_client
from liter_llm._internal_bindings import ChatCompletionRequest
async def main() -> None:
client = create_client(api_key=os.environ["OPENAI_API_KEY"])
request = ChatCompletionRequest.from_json(
'{"model":"openai/gpt-4o","messages":[{"role":"user","content":"Tell me a story"}],"stream":true}'
)
async for chunk in client.chat_stream(request):
if chunk.choices and chunk.choices[0].delta.content:
print(chunk.choices[0].delta.content, end="", flush=True)
print()
asyncio.run(main())

Each chunk contains choices[].delta.content with incremental text. The final chunk includes finish_reason: "stop".

Accumulate deltas to get both real-time output and the complete text:

import asyncio
import os
from liter_llm import create_client
from liter_llm._internal_bindings import ChatCompletionRequest
async def main() -> None:
client = create_client(api_key=os.environ["OPENAI_API_KEY"])
request = ChatCompletionRequest.from_json(
'{"model":"openai/gpt-4o","messages":[{"role":"user","content":"Explain quantum computing briefly"}],"stream":true}'
)
full_text = ""
async for chunk in client.chat_stream(request):
delta = chunk.choices[0].delta.content if chunk.choices else None
if delta:
full_text += delta
print(delta, end="", flush=True)
print()
print(f"Full response length: {len(full_text)} characters")
asyncio.run(main())

Define tools as JSON schema functions. The model can request tool calls, which you execute and return results for:

import asyncio
import json
import os
from liter_llm import create_client
from liter_llm._internal_bindings import ChatCompletionRequest
REQUEST = {
"model": "openai/gpt-4o",
"messages": [{"role": "user", "content": "What is the weather in Berlin?"}],
"tools": [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get the current weather for a location",
"parameters": {
"type": "object",
"properties": {"location": {"type": "string"}},
"required": ["location"],
},
},
}
],
"tool_choice": "auto",
}
async def main() -> None:
client = create_client(api_key=os.environ["OPENAI_API_KEY"])
request = ChatCompletionRequest.from_json(json.dumps(REQUEST))
response = await client.chat(request)
for call in response.choices[0].message.tool_calls or []:
print(f"Tool: {call.function.name}, Args: {call.function.arguments}")
asyncio.run(main())

All chat parameters work with both chat and chat_stream:

Parameter Type Description
model string Provider/model identifier (e.g. "openai/gpt-4o")
messages array Conversation messages
temperature float Sampling temperature (0.0-2.0)
max_tokens int Maximum tokens to generate
top_p float Nucleus sampling threshold
n int Number of completions to generate
stop string/array Stop sequences
tools array Tool/function definitions
tool_choice string/object Tool selection strategy
response_format object Force JSON output ({"type": "json_object"})
seed int Deterministic sampling seed
presence_penalty float Penalize new topics (-2.0 to 2.0)
frequency_penalty float Penalize repetition (-2.0 to 2.0)
reasoning_effort string Reasoning budget for o-series and extended-thinking models.
extra_body object Provider-specific fields passed through verbatim.

OpenAI o-series models and Anthropic extended-thinking models accept a reasoning_effort parameter that controls how much compute the model spends on internal reasoning before producing the final response.

response = client.chat({
"model": "openai/o3-mini",
"messages": [{"role": "user", "content": "Prove the Pythagorean theorem."}],
"reasoning_effort": "high",
})

Accepted values for OpenAI o-series: "low", "medium", "high". Anthropic extended thinking uses a budget_tokens integer instead, which maps to reasoning_effort when the binding converts the field.

Pass a JSON Schema to response_format to constrain the model output to a specific structure. Use "type": "json_schema" instead of "type": "json_object" for schema-validated output.

schema = {
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer"},
},
"required": ["name", "age"],
"additionalProperties": False,
}
response = client.chat({
"model": "openai/gpt-4o",
"messages": [{"role": "user", "content": "Extract: Alice is 30 years old."}],
"response_format": {
"type": "json_schema",
"json_schema": {
"name": "person",
"strict": True,
"schema": schema,
},
},
})

Structured output availability depends on provider support. OpenAI gpt-4o and later support json_schema. Providers that do not support it fall back to json_object or return EndpointNotSupported.

Pass provider-specific parameters that liter-llm does not model natively via extra_body. Fields in extra_body are merged into the top-level request JSON before it is sent to the provider.

response = client.chat({
"model": "openai/gpt-4o",
"messages": [{"role": "user", "content": "Hello"}],
"extra_body": {
"store": True, # OpenAI conversation store
"metadata": {"user": "alice"},
},
})

extra_body fields take lower precedence than named fields. If a named field and an extra_body key conflict, the named field wins.

Send audio inline in a user message using the input_audio content part type. The audio must be base64-encoded.

import base64
with open("audio.wav", "rb") as f:
audio_b64 = base64.b64encode(f.read()).decode()
response = client.chat({
"model": "openai/gpt-4o-audio-preview",
"messages": [{
"role": "user",
"content": [
{
"type": "input_audio",
"input_audio": {
"data": audio_b64,
"format": "wav",
},
},
{"type": "text", "text": "Transcribe and summarize this audio."},
],
}],
})

Supported formats depend on the provider. OpenAI gpt-4o-audio-preview accepts wav, mp3, ogg, flac, m4a.

When routing to Bedrock providers, responses arrive in AWS EventStream framing rather than SSE. Liter-llm handles the framing transparently. chat_stream works the same way regardless of provider.

// EventStream framing is transparent to the caller.
let stream = client.chat_stream(ChatCompletionRequest {
model: "bedrock/anthropic.claude-3-5-sonnet-20241022-v2:0".into(),
messages: vec![/* ... */],
..Default::default()
}).await?;
// Consume exactly like any other stream.
pin_mut!(stream);
while let Some(chunk) = stream.next().await {
let chunk = chunk?;
if let Some(content) = chunk.choices[0].delta.content.as_deref() {
print!("{content}");
}
}