// SPDX-FileCopyrightText: Amolith // // SPDX-License-Identifier: AGPL-3.0-or-later import config.{type ReasoningEffort} import gleam/dynamic/decode import gleam/http import gleam/http/request import gleam/httpc import gleam/json import gleam/list import gleam/option.{type Option, None, Some} pub fn send( endpoint: String, api_key: String, model: String, system_prompt: String, user_prompt: String, reasoning: Option(ReasoningEffort), ) -> Result(String, String) { let messages = build_messages(system_prompt, user_prompt) let base_fields = [ #("model", json.string(model)), #("messages", json.array(messages, fn(m) { m })), ] let fields = case reasoning { Some(effort) -> list.append(base_fields, [ #("reasoning_effort", json.string(config.reasoning_to_string(effort))), ]) None -> base_fields } let body = json.object(fields) |> json.to_string let url = endpoint <> "/chat/completions" case request.to(url) { Error(_) -> Error("Invalid endpoint URL: " <> endpoint) Ok(req) -> { let req = req |> request.set_method(http.Post) |> request.set_header("content-type", "application/json") |> request.set_header("authorization", "Bearer " <> api_key) |> request.set_body(body) case httpc.send(req) { Error(_) -> Error("Network error") Ok(resp) if resp.status >= 200 && resp.status < 300 -> parse_response(resp.body) Ok(resp) -> Error("HTTP " <> resp.body) } } } } fn build_messages(system_prompt: String, user_prompt: String) -> List(json.Json) { case system_prompt { "" -> [user_message(user_prompt)] sys -> [system_message(sys), user_message(user_prompt)] } } fn system_message(content: String) -> json.Json { json.object([ #("role", json.string("system")), #("content", json.string(content)), ]) } fn user_message(content: String) -> json.Json { json.object([ #("role", json.string("user")), #("content", json.string(content)), ]) } fn parse_response(body: String) -> Result(String, String) { let choice_decoder = decode.at(["message", "content"], decode.string) let response_decoder = { use choices <- decode.field("choices", decode.list(choice_decoder)) decode.success(choices) } case json.parse(body, response_decoder) { Error(_) -> Error("Failed to parse response") Ok(choices) -> case list.first(choices) { Ok(content) -> Ok(content) Error(_) -> Error("No response content") } } }