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: 0

All 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: user

The 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: 2

Conversation Rules

  • The first message must have role system
  • Messages must alternate roles (no two consecutive messages from the same role; consecutive tool results are the exception)
  • At least content, tool_calls, or refusal_message must 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")
end
Request failed — see result for details

One-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")
end
Request failed — see result for details

Multi-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")
end
Request 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.")
end
No 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))  # false
Is chat valid? true
Is empty chat valid? false

This 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:

ModelUsage
"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:

ParameterValid Range
temperature0.0–2.0
top_p0.0–1.0
n1–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