Chat Completions

The Chat Completions API is the classic way to interact with OpenAI's language models. UniLM.jl wraps it in a type-safe, stateful Chat object that tracks conversation history automatically.

Creating a Chat

chat = Chat(
    model="gpt-5.2",        # model name
    temperature=0.7,        # sampling temperature
)
println("Model: ", chat.model)
println("Messages: ", length(chat))
Model: gpt-5.2
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)
  • At least content, tool_calls, or refusal_message must be non-nothing
  • Attempting to violate these rules logs a warning and the message is not added
# Demonstrate validation
chat3 = Chat()
push!(chat3, Message(Val(:system), "sys"))
push!(chat3, Message(Val(:user), "hello"))
push!(chat3, Message(Val(:user), "hello again"))  # rejected — same role
println("Length after duplicate push: ", length(chat3), " (second user msg rejected)")
┌ Warning: Cannot add message Message("user", "hello again", nothing, nothing, nothing, nothing, nothing) to conversation: Chat(OPENAIServiceEndpoint, "gpt-5.2", Message[Message("system", "sys", nothing, nothing, nothing, nothing, nothing), Message("user", "hello", nothing, nothing, nothing, nothing, nothing)], true, nothing, nothing, nothing, nothing, nothing, nothing, nothing, nothing, nothing, nothing, nothing, nothing, nothing, nothing, nothing, Base.RefValue{Float64}(0.0))
└ @ UniLM ~/work/UniLM.jl/UniLM.jl/src/api.jl:561
Length after duplicate push: 2 (second user msg rejected)

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
In Julia, **parametric types** are types that are *parameterized by one or more values (usually other types)*. They let you write generic, reusable code while still keeping type information precise for performance.

### 1) Parametric composite types
You define a type with type parameters in curly braces:

```julia
struct Point{T}
    x::T
    y::T
end
```

Here `Point{T}` is a family of types:
- `Point{Int}` is a concrete type
- `Point{Float64}` is a concrete type
- `Point` by itself is an abstract “union” over all `T` (more precisely, a `UnionAll`)

Usage:

```julia
p1 = Point(1, 2)          # Point{Int64}
p2 = Point(1.0, 2.0)      # Point{Float64}
```

### 2) Parametric abstract types
Abstract types can also be parameterized:

```julia
abstract type MyAbstract{T} end
```

Then concrete subtypes can choose how they use `T`.

### 3) Parametric methods (generic functions)
Functions can be parameterized too, typically with `where`:

```julia
norm2(p::Point{T}) where {T} = p.x*p.x + p.y*p.y
```

Often you don’t even need to name `T` unless you use it.

### 4) Why they matter
- **Code reuse:** one definition works for many element types (`T`).
- **Performance:** Julia can compile specialized versions for `Point{Float64}`, `Point{Int}`, etc.
- **Type safety/clarity:** you can express constraints and relationships between fields/arguments.

### 5) Common examples in Base
Many standard Julia types are parametric:
- `Array{T,N}` (element type `T`, dimension `N`)
- `Dict{K,V}` (key and value types)
- `Tuple{T1,T2,...}`

If you want, I can show how to add constraints like `Point{T} where T<:Real`, or explain the difference between `Point` vs `Point{T}` in dispatch.

Finish reason: stop
Conversation length: 3

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-4o-mini",
    temperature=0.0
)
if result isa LLMSuccess
    println(result.message.content)
else
    println("Request failed — see result for details")
end
714

Multi-Turn Conversations

Because chatrequest! appends the response, you can keep chatting:

chat = Chat(model="gpt-4o-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
Multiple dispatch is a core feature of the Julia programming language that allows a function to be defined with different implementations based on the types of all its arguments, not just the first one. This enables more flexible and efficient code, as the appropriate method is selected at runtime based on the combination of argument types, allowing for polymorphism and better code organization.
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
Here’s a simple example of multiple dispatch in Julia:

```julia
# Define a function `area` for different shapes
function area(radius::Float64)
    return π * radius^2  # Area of a circle
end

function area(length::Float64, width::Float64)
    return length * width  # Area of a rectangle
end

# Calling the functions
circle_area = area(5.0)               # Calls the circle version
rectangle_area = area(4.0, 6.0)       # Calls the rectangle version

println("Circle area: ", circle_area)
println("Rectangle area: ", rectangle_area)
```

In this example, the `area` function behaves differently based on whether it's given one or two arguments.

Conversation length: 5

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? false
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.2"Best quality (default)
"gpt-4o-mini"Fast and cheap
"gpt-4.1-mini"Balanced performance
"o3"Extended reasoning
"o4-mini"Fast reasoning

JSON Serialization

The Chat object serializes cleanly to JSON for the API:

println(JSON.json(chat))
{"messages":[{"role":"system","content":"You are a concise Julia programming tutor."},{"role":"user","content":"What is multiple dispatch? Answer in 2-3 sentences."},{"role":"assistant","content":"Multiple dispatch is a core feature of the Julia programming language that allows a function to be defined with different implementations based on the types of all its arguments, not just the first one. This enables more flexible and efficient code, as the appropriate method is selected at runtime based on the combination of argument types, allowing for polymorphism and better code organization.","finish_reason":"stop"},{"role":"user","content":"Give a short Julia code example of it."},{"role":"assistant","content":"Here’s a simple example of multiple dispatch in Julia:\n\n```julia\n# Define a function `area` for different shapes\nfunction area(radius::Float64)\n    return π * radius^2  # Area of a circle\nend\n\nfunction area(length::Float64, width::Float64)\n    return length * width  # Area of a rectangle\nend\n\n# Calling the functions\ncircle_area = area(5.0)               # Calls the circle version\nrectangle_area = area(4.0, 6.0)       # Calls the rectangle version\n\nprintln(\"Circle area: \", circle_area)\nprintln(\"Rectangle area: \", rectangle_area)\n```\n\nIn this example, the `area` function behaves differently based on whether it's given one or two arguments.","finish_reason":"stop"}],"model":"gpt-4o-mini"}

Retry Behaviour

chatrequest! automatically retries on HTTP 429, 500, and 503 errors with exponential backoff and jitter (up to 30 attempts, max 60s delay). On 429 responses, the Retry-After header is respected. This is transparent and requires no configuration.

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