Chat Completions
The Chat Completions API is the standard way to talk to LLM providers. UniLM.jl wraps it in a type-safe, stateful Chat object that tracks conversation history automatically — and works with every supported backend (OpenAI, DeepSeek, Ollama, Gemini, Mistral, and more).
Creating a Chat
chat = Chat(
model="gpt-5.4-mini", # model name
temperature=0.7, # sampling temperature
)
println("Model: ", chat.model)
println("Messages: ", length(chat))Model: gpt-5.4-mini
Messages: 0All parameters are optional with sensible defaults. See Chat for the full list.
Building Conversations
Messages are added with push!. UniLM.jl enforces conversation structure at the type level — you cannot create invalid message sequences:
# System message must come first
push!(chat, Message(Val(:system), "You are a helpful Julia programming tutor."))
# Then user messages
push!(chat, Message(Val(:user), "What are parametric types?"))
println("Conversation length: ", length(chat))
println("First message role: ", chat[1].role)
println("Last message role: ", chat[end].role)Conversation length: 2
First message role: system
Last message role: userThe convenience Val(:system) and Val(:user) constructors keep things concise. You can also use the keyword constructor:
chat2 = Chat()
push!(chat2, Message(role="system", content="Be helpful"))
push!(chat2, Message(role="user", content="Tell me more"))
println("chat2 length: ", length(chat2))chat2 length: 2Conversation Rules
- The first message must have role
system - Messages must alternate roles (no two consecutive messages from the same role; consecutive
toolresults are the exception) - At least
content,tool_calls, orrefusal_messagemust be non-nothing - Attempting to violate these rules throws
InvalidConversationError— the invalid message is never added
# Demonstrate validation — an invalid mutation throws and leaves the chat unchanged
chat3 = Chat()
push!(chat3, Message(Val(:system), "sys"))
push!(chat3, Message(Val(:user), "hello"))
try
push!(chat3, Message(Val(:user), "hello again")) # same role — rejected
catch e
println("Rejected: ", e isa InvalidConversationError)
end
println("Length after rejected push: ", length(chat3), " (still 2 — the invalid message was not added)")Rejected: true
Length after rejected push: 2 (still 2 — the invalid message was not added)Sending Requests
result = chatrequest!(chat)The ! suffix is a Julia convention — chatrequest! mutates chat by appending the assistant's response to the message history (when history=true).
Result Handling
result = chatrequest!(chat)
if result isa LLMSuccess
println(result.message.content)
println("\nFinish reason: ", result.message.finish_reason)
println("Conversation length: ", length(chat))
else
println("Request failed — see result for details")
endRequest failed — see result for detailsOne-Shot Requests via Keywords
Skip the Chat object entirely for simple one-off requests:
result = chatrequest!(
systemprompt="You are a calculator. Respond only with the number.",
userprompt="What is 42 * 17?",
model="gpt-5.4-mini",
temperature=0.0
)
if result isa LLMSuccess
println(result.message.content)
else
println("Request failed — see result for details")
endRequest failed — see result for detailsMulti-Turn Conversations
Because chatrequest! appends the response, you can keep chatting:
chat = Chat(model="gpt-5.4-mini")
push!(chat, Message(Val(:system), "You are a concise Julia programming tutor."))
push!(chat, Message(Val(:user), "What is multiple dispatch? Answer in 2-3 sentences."))
result = chatrequest!(chat)
if result isa LLMSuccess
println(result.message.content)
else
println("Request failed — see result for details")
endRequest failed — see result for details# `chatrequest!` appends the assistant reply on success, which is what makes the next
# user turn valid. If the previous call appended none (e.g. it failed), pushing another
# user message would throw InvalidConversationError — so guard the follow-up turn.
if !isempty(chat) && chat[end].role == RoleAssistant
push!(chat, Message(Val(:user), "Give a short Julia code example of it."))
result = chatrequest!(chat)
if result isa LLMSuccess
println(result.message.content)
println("\nConversation length: ", length(chat))
else
println("Request failed — see result for details")
end
else
println("No assistant reply to build on — skipping the follow-up turn.")
endNo assistant reply to build on — skipping the follow-up turn.Checking Conversation Validity
println("Is chat valid? ", issendvalid(chat)) # true — system + user
empty_chat = Chat()
println("Is empty chat valid? ", issendvalid(empty_chat)) # falseIs chat valid? true
Is empty chat valid? falseThis checks:
- At least 2 messages
- First message is
system - Last message is
user - No consecutive same-role messages
Models
UniLM.jl works with any model name string. Common choices:
| Model | Usage |
|---|---|
"gpt-5.6-sol" | Default; Chat tools require no reasoning |
"gpt-5.6-luna" | Fast and cheap |
"gpt-4.1-mini" | Balanced performance |
"o3" | Extended reasoning |
"o4-mini" | Fast reasoning |
All five are keys in DEFAULT_PRICING, so cost accounting works out of the box. Any other model name is accepted — it is just a string on the wire — but an unpriced one silently estimates at 0.0 — see Unpriced models return $0 silently before relying on estimated_cost.
Using Other Providers
Pass a service to target any supported backend:
# DeepSeek
chat = Chat(service=DeepSeekEndpoint(), model="deepseek-chat")
# Ollama (local)
chat = Chat(service=OllamaEndpoint(), model="llama3.1")
# Mistral
chat = Chat(service=MistralEndpoint(), model="mistral-large-latest")See the Multi-Backend Guide for the full list of providers and configuration.
JSON Serialization
The Chat object serializes cleanly to JSON for the API:
println(JSON.json(chat)){"messages":[{"content":"You are a concise Julia programming tutor.","role":"system"},{"content":"What is multiple dispatch? Answer in 2-3 sentences.","role":"user"}],"model":"gpt-5.4-mini"}Retry Behaviour
chatrequest! automatically retries transient HTTP statuses (408, 429, 500, 502, 503, 504, 529) with exponential backoff and jitter, honoring Retry-After. Attempts and total time are bounded by the resolved RequestConfig (max_attempts, default 3; total_deadline, default 900 s). Pass config=RequestConfig(max_attempts=1) to disable retries for a call, or set scoped/process-wide defaults with with_request_config / set_default_config!. Timeouts surface as LLMCallError with status = nothing and the UniLMTimeout (phase, elapsed, limit) in .cause.
Parameter Validation
The Chat constructor validates parameter ranges at construction time:
| Parameter | Valid Range |
|---|---|
temperature | 0.0–2.0 |
top_p | 0.0–1.0 |
n | 1–10 |
presence_penalty | -2.0–2.0 |
frequency_penalty | -2.0–2.0 |
Out-of-range values throw ArgumentError. Additionally, temperature and top_p are mutually exclusive.
See Also
Chat— full type referenceMessage— message type reference- Tool Calling — function calling with Chat Completions
- Streaming — real-time streaming
- Structured Output — JSON-constrained generation