BoxLang is the AI-native software productivity platform for building, modernizing and running applications, with developers and AI agents working together.
Core BIF: aiChat()
// Signature
aiChat( messages, params={}, options={}, headers={} )
messages: string, a message struct, an array of message structs, or anaiMessage()objectparams: model parameters sent to the provider (temperature,max_tokens,model,top_p,stop, etc.)options:provider,apiKey,timeout(seconds, module default 90),returnFormat,logRequest,logResponse,logRequestToConsole,logResponseToConsoleheaders: struct of extra HTTP headers for the request- Returns: the message content string by default (
returnFormat: "single")
Simple Usage
// Uses the configured default provider
answer = aiChat( "What is the capital of France?" )
// With parameters
code = aiChat(
"Write a Fibonacci function in BoxLang",
{ temperature: 0.2, max_tokens: 500 }
)
// With a specific provider
result = aiChat(
"Summarize this text: ...",
{ temperature: 0.5 },
{ provider: "claude" }
)
Parameters Reference
params = {
temperature : 0.7, // 0.0 (deterministic) up to creative
max_tokens : 1000, // max response length
model : "gpt-4o", // provider-specific model name
top_p : 1.0, // nucleus sampling (use OR temperature, not both)
stop : ["\n\n"] // stop sequences
}
Params are passed through to the provider, so valid names depend on the provider.
Return Formats
Valid options.returnFormat strings: single (default), all, raw, json, xml. Anything else throws InvalidArgument.
| Value | Returns |
|---|---|
single |
The message content string |
all |
Array of choices (OpenAI-shaped providers); for claude, the array of content blocks |
raw |
The full provider response (struct) |
json |
Parsed JSON from the reply (struct/array); empty struct if none is found |
xml |
Parsed XML document |
text = aiChat( "Hello" )
// Full provider response (OpenAI-shaped providers)
response = aiChat( "Hello", {}, { returnFormat: "raw" } )
println( response.choices.first().message.content )
println( response.usage.total_tokens ) // usage keys: prompt_tokens, completion_tokens, total_tokens
// A class, struct, or array may also be passed as returnFormat for structured output
raw returns the provider's own envelope, so for claude it is Anthropic's native shape, not choices[].
Multi-Turn Conversations
messages = [
{ role: "system", content: "You are a helpful assistant." },
{ role: "user", content: "What is 2+2?" },
{ role: "assistant", content: "4" },
{ role: "user", content: "Multiply that by 10." }
]
result = aiChat( messages )
aiMessage() is a fluent builder; the method name is the role, and bind() fills ${placeholders}:
msg = aiMessage()
.system( "You are a helpful assistant." )
.user( "Tell me about ${topic}" )
.bind( { topic: "BoxLang" } )
result = aiChat( msg.render() )
Async Chat
// Non-blocking, returns a BoxLang Future
future = aiChatAsync( "Explain quantum entanglement" )
// Do other work here...
response = future.get()
aiChatAsync() takes the same arguments as aiChat().
Streaming Responses
// aiChatStream( messages, callback, params={}, options={}, headers={} )
aiChatStream(
"Write a short story about a robot",
( chunk ) => {
// chunk is an OpenAI-shaped chat.completion.chunk struct
print( chunk.choices?.first()?.delta?.content ?: "" )
},
{ temperature: 0.7 },
{ provider: "openai" }
)
- The callback is the 2nd argument, and is called once per chunk with the parsed chunk struct.
aiChatStream()returns nothing. It has noreturnFormat.delta.contentmay be absent on some chunks (role, reasoning, or usage chunks), so default it with?: "".
Reasoning (Normalized)
Reasoning/thinking text is surfaced on one standard key regardless of provider, and is never merged into content.
- Streaming:
chunk.choices.first().delta.reasoning. Providers that emitreasoning_content(for example DeepSeek) orthinkingare mapped ontodelta.reasoning. Claude extended thinking is emitted asdelta.reasoningchunks before the text. - Sync (OpenAI-shaped providers):
response.choices[ i ].message.reasoning, available withreturnFormat: "raw"or"all".reasoning_contentandthinkingare mapped onto it. - Sync Claude with
returnFormat: "raw":response.reasoningat the top level of the native envelope (only present when the model produced thinking blocks). - Absence is normal: if a model has no reasoning, the key is simply not set. Always read it with
?.and a default.
aiChatStream( "Solve 17 * 24", ( chunk ) => {
var delta = chunk.choices?.first()?.delta ?: {}
if( delta.keyExists( "reasoning" ) ) print( "[thinking] " & delta.reasoning )
if( delta.keyExists( "content" ) ) print( delta.content ?: "" )
}, {}, { provider: "deepseek" } )
Provider Configuration
result = aiChat( "Hello", {}, { provider: "openai", apiKey: "sk-..." } )
result = aiChat( "Hello", {}, { provider: "claude" } )
result = aiChat( "Hello", {}, { provider: "ollama" } ) // local
The provider must support chat, otherwise UnsupportedCapability is thrown. If no apiKey is passed, the <PROVIDER>_API_KEY environment setting is used, then the module apiKey setting. Valid provider keys and their capabilities are in the bx-ai-models skill. For speech and transcription, see the bx-ai-audio skill.
Error Handling
There is no exceptions package. Errors are thrown as plain BoxLang exceptions with a type:
ProviderError: a streaming request failed (thrown fromaiChatStream())JsonDeserializationError: the provider response was not valid JSONProviderNotSupported: unknownproviderkeyUnsupportedCapability: the provider cannot chatInvalidArgument: invalidreturnFormat
try {
result = aiChat( "Hello", {}, { provider: "openai" } )
} catch ( any e ) {
if( e.type == "ProviderNotSupported" ) {
logError( "Bad provider: #e.message#" )
} else {
logError( "AI error [#e.type#]: #e.message#" )
}
}
Failures and rate limits also announce the onAIError and onAIRateLimitHit interception points.
Common Pitfalls
- Do NOT pass
model:as a top-level BIF argument: put it inparams - Do NOT use both
temperatureandtop_psimultaneously - Do NOT put the callback last in
aiChatStream(): it is the 2nd argument - Do NOT use
returnFormat: "full"or"choices": they are not valid values - Handle provider errors in production code
- Use
returnFormat: "raw"when you need token counts, finish reasons or reasoning - Use
aiChatAsync()for long-running requests in web handlers