///|
/// A tool (function) definition advertised to the model.
pub(all) struct Tool {
name : String
description : String
/// JSON Schema for the function parameters, as a `Json` value.
parameters : Json
}
///|
pub impl ToJson for Tool with fn to_json(self : Tool) -> Json {
Json::object({
"type": Json::string("function"),
"function": Json::object({
"name": Json::string(self.name),
"description": Json::string(self.description),
"parameters": self.parameters,
}),
})
}
///|
/// A chat completion request. Build with `ChatRequest::new` and the
/// chainable setters, or construct the struct directly.
pub(all) struct ChatRequest {
model : String
messages : Array[Message]
mut temperature : Double?
mut max_tokens : Int?
mut top_p : Double?
mut stop : Array[String]?
mut tools : Array[Tool]?
/// Controls which (if any) tool is called: `"auto"`, `"none"`, `"required"`,
/// or a specific function name.
mut tool_choice : ToolChoice?
mut frequency_penalty : Double?
mut presence_penalty : Double?
/// Deterministic sampling seed (best-effort, model dependent).
mut seed : Int?
/// Number of completions to generate.
mut n : Int?
/// Response format: plain text or a JSON object ("JSON mode").
mut response_format : ResponseFormat?
/// Per-token logit biases keyed by token id (as a string).
mut logit_bias : Map[String, Double]?
/// End-user identifier for abuse monitoring.
mut user : String?
mut stream : Bool
}
///|
/// How the model should choose among the provided tools.
pub(all) enum ToolChoice {
/// Let the model decide (default when tools are present).
Auto
/// Never call a tool.
NoneChoice
/// Force the model to call some tool.
Required
/// Force a specific function by name.
Function(String)
} derive(Eq, Debug)
///|
pub impl ToJson for ToolChoice with fn to_json(self : ToolChoice) -> Json {
match self {
Auto => Json::string("auto")
NoneChoice => Json::string("none")
Required => Json::string("required")
Function(name) =>
Json::object({
"type": Json::string("function"),
"function": Json::object({ "name": Json::string(name) }),
})
}
}
///|
/// The requested response format.
pub(all) enum ResponseFormat {
/// Plain text (the default).
TextFormat
/// A JSON object ("JSON mode").
JsonObject
/// A JSON object conforming to the given JSON Schema.
JsonSchema(name~ : String, schema~ : Json)
} derive(Eq, Debug)
///|
pub impl ToJson for ResponseFormat with fn to_json(self : ResponseFormat) -> Json {
match self {
TextFormat => Json::object({ "type": Json::string("text") })
JsonObject => Json::object({ "type": Json::string("json_object") })
JsonSchema(name~, schema~) =>
Json::object({
"type": Json::string("json_schema"),
"json_schema": Json::object({
"name": Json::string(name),
"schema": schema,
}),
})
}
}
///|
/// Create a request for `model` with the given `messages`.
pub fn ChatRequest::new(
model : String,
messages : Array[Message],
) -> ChatRequest {
{
model,
messages,
temperature: None,
max_tokens: None,
top_p: None,
stop: None,
tools: None,
tool_choice: None,
frequency_penalty: None,
presence_penalty: None,
seed: None,
n: None,
response_format: None,
logit_bias: None,
user: None,
stream: false,
}
}
///|
pub fn ChatRequest::temperature(self : ChatRequest, t : Double) -> ChatRequest {
self.temperature = Some(t)
self
}
///|
pub fn ChatRequest::max_tokens(self : ChatRequest, n : Int) -> ChatRequest {
self.max_tokens = Some(n)
self
}
///|
pub fn ChatRequest::top_p(self : ChatRequest, p : Double) -> ChatRequest {
self.top_p = Some(p)
self
}
///|
pub fn ChatRequest::stop(self : ChatRequest, s : Array[String]) -> ChatRequest {
self.stop = Some(s)
self
}
///|
pub fn ChatRequest::tools(self : ChatRequest, t : Array[Tool]) -> ChatRequest {
self.tools = Some(t)
self
}
///|
pub fn ChatRequest::tool_choice(
self : ChatRequest,
c : ToolChoice,
) -> ChatRequest {
self.tool_choice = Some(c)
self
}
///|
pub fn ChatRequest::frequency_penalty(
self : ChatRequest,
p : Double,
) -> ChatRequest {
self.frequency_penalty = Some(p)
self
}
///|
pub fn ChatRequest::presence_penalty(
self : ChatRequest,
p : Double,
) -> ChatRequest {
self.presence_penalty = Some(p)
self
}
///|
pub fn ChatRequest::seed(self : ChatRequest, s : Int) -> ChatRequest {
self.seed = Some(s)
self
}
///|
pub fn ChatRequest::n(self : ChatRequest, count : Int) -> ChatRequest {
self.n = Some(count)
self
}
///|
pub fn ChatRequest::response_format(
self : ChatRequest,
f : ResponseFormat,
) -> ChatRequest {
self.response_format = Some(f)
self
}
///|
/// Shortcut: request a JSON-object response ("JSON mode").
pub fn ChatRequest::json_mode(self : ChatRequest) -> ChatRequest {
self.response_format = Some(JsonObject)
self
}
///|
pub fn ChatRequest::logit_bias(
self : ChatRequest,
bias : Map[String, Double],
) -> ChatRequest {
self.logit_bias = Some(bias)
self
}
///|
pub fn ChatRequest::user(self : ChatRequest, u : String) -> ChatRequest {
self.user = Some(u)
self
}
///|
/// Validate the request parameters, returning an error message if any value
/// is out of its documented range. Returns `None` when the request is valid.
///
/// This is a best-effort local check against the OpenAI-documented ranges;
/// it does not guarantee the server will accept the request.
pub fn ChatRequest::validate(self : ChatRequest) -> String? {
if self.model == "" {
return Some("model must not be empty")
}
if self.messages.length() == 0 {
return Some("messages must not be empty")
}
if self.temperature is Some(t) && (t < 0.0 || t > 2.0) {
return Some("temperature must be in [0, 2]")
}
if self.top_p is Some(p) && (p < 0.0 || p > 1.0) {
return Some("top_p must be in [0, 1]")
}
if self.frequency_penalty is Some(p) && (p < -2.0 || p > 2.0) {
return Some("frequency_penalty must be in [-2, 2]")
}
if self.presence_penalty is Some(p) && (p < -2.0 || p > 2.0) {
return Some("presence_penalty must be in [-2, 2]")
}
if self.max_tokens is Some(m) && m <= 0 {
return Some("max_tokens must be positive")
}
if self.n is Some(count) && count <= 0 {
return Some("n must be positive")
}
None
}
///|
pub impl ToJson for ChatRequest with fn to_json(self : ChatRequest) -> Json {
let obj : Map[String, Json] = {
"model": Json::string(self.model),
"messages": self.messages.to_json(),
"stream": Json::boolean(self.stream),
}
if self.temperature is Some(t) {
obj["temperature"] = Json::number(t)
}
if self.max_tokens is Some(n) {
obj["max_tokens"] = Json::number(n.to_double())
}
if self.top_p is Some(p) {
obj["top_p"] = Json::number(p)
}
if self.stop is Some(s) {
obj["stop"] = s.to_json()
}
if self.tools is Some(t) {
obj["tools"] = t.to_json()
}
if self.tool_choice is Some(c) {
obj["tool_choice"] = c.to_json()
}
if self.frequency_penalty is Some(p) {
obj["frequency_penalty"] = Json::number(p)
}
if self.presence_penalty is Some(p) {
obj["presence_penalty"] = Json::number(p)
}
if self.seed is Some(s) {
obj["seed"] = Json::number(s.to_double())
}
if self.n is Some(count) {
obj["n"] = Json::number(count.to_double())
}
if self.response_format is Some(f) {
obj["response_format"] = f.to_json()
}
if self.logit_bias is Some(bias) {
let bias_obj : Map[String, Json] = {}
for k, v in bias {
bias_obj[k] = Json::number(v)
}
obj["logit_bias"] = Json::object(bias_obj)
}
if self.user is Some(u) {
obj["user"] = Json::string(u)
}
Json::object(obj)
}
///|
/// Convenience constructors for messages.
pub fn Message::system(text : String) -> Message {
{
role: System,
content: Str(text),
tool_calls: None,
tool_call_id: None,
name: None,
}
}
///|
pub fn Message::user(text : String) -> Message {
{
role: User,
content: Str(text),
tool_calls: None,
tool_call_id: None,
name: None,
}
}
///|
pub fn Message::assistant(text : String) -> Message {
{
role: Assistant,
content: Str(text),
tool_calls: None,
tool_call_id: None,
name: None,
}
}
///|
/// Build a multimodal user message from content parts (text and/or images).
pub fn Message::user_parts(parts : Array[ContentPart]) -> Message {
{
role: User,
content: Parts(parts),
tool_calls: None,
tool_call_id: None,
name: None,
}
}
///|
/// Build a `tool` role message carrying the result of a tool call.
pub fn Message::tool_result(tool_call_id : String, content : String) -> Message {
{
role: Tool,
content: Str(content),
tool_calls: None,
tool_call_id: Some(tool_call_id),
name: None,
}
}
///|
/// A text content part.
pub fn ContentPart::text(s : String) -> ContentPart {
Text(s)
}
///|
/// An image content part from a URL (`https://...` or a `data:` URI).
pub fn ContentPart::image_url(url : String) -> ContentPart {
ImageUrl(url)
}
///|
/// An image content part from raw base64 data, wrapped as a `data:` URI.
///
/// `mime` is e.g. `"image/png"` or `"image/jpeg"`.
pub fn ContentPart::image_base64(mime : String, data : String) -> ContentPart {
ImageUrl("data:" + mime + ";base64," + data)
}