starlet

Package Version Hex Docs

A unified, provider-agnostic interface for LLM APIs in Gleam.

Installation

gleam add starlet

Quick Start

import gleam/io
import gleam/result
import starlet
import starlet/ollama

pub fn main() {
  let client = ollama.new("http://localhost:11434")

  let chat =
    starlet.chat(client, "gpt-oss:20b")
    |> starlet.system("You are a helpful assistant.")
    |> starlet.user("What is the capital of France?")

  case starlet.send(chat) {
    Ok(#(_chat, turn)) -> io.println(starlet.text(turn))
    Error(_) -> io.println("Request failed")
  }
}

Features

Missing Features

Examples

Multi-turn Conversations

import gleam/result
import starlet
import starlet/ollama

pub fn main() {
  let client = ollama.new("http://localhost:11434")

  let result = {
    let chat =
      starlet.chat(client, "gpt-oss:20b")
      |> starlet.user("Hello!")

    use #(chat, _turn) <- result.try(starlet.send(chat))

    let chat = starlet.user(chat, "How are you?")
    use #(_chat, turn) <- result.try(starlet.send(chat))

    Ok(starlet.text(turn))
  }

  // result contains the final response or first error
}

Tool Calling

import gleam/dynamic/decode
import gleam/json
import gleam/result
import starlet
import starlet/ollama
import starlet/tool

pub fn main() {
  let client = ollama.new("http://localhost:11434")

  // Define a tool
  let weather_tool =
    tool.function(
      name: "get_weather",
      description: "Get weather for a city",
      parameters: json.object([
        #("type", json.string("object")),
        #("properties", json.object([
          #("city", json.object([#("type", json.string("string"))])),
        ])),
      ]),
    )

  // Decoder for the tool arguments
  let city_decoder = {
    use city <- decode.field("city", decode.string)
    decode.success(city)
  }

  // Create a handler that executes tools
  let dispatcher =
    tool.dispatch([
      tool.handler("get_weather", city_decoder, fn(city) {
        let temp = case city {
          "Tokyo" -> 18
          "Paris" -> 22
          _ -> 20
        }
        Ok(json.object([
          #("temp", json.int(temp)),
          #("condition", json.string("sunny")),
        ]))
      }),
    ])

  let chat =
    starlet.chat(client, "gpt-oss:20b")
    |> starlet.with_tools([weather_tool])
    |> starlet.user("What's the weather in Tokyo?")

  // Use step/apply_tool_results loop to handle tool calls
  use step <- result.try(starlet.step(chat))
  case step {
    starlet.ToolCall(chat:, calls:, ..) -> {
      use chat <- result.try(starlet.apply_tool_results(chat, calls, dispatcher))
      starlet.send(chat)  // Continue after tools execute
    }
    starlet.Done(..) -> Ok(step)
  }
}

Structured JSON Output

import gleam/dynamic/decode
import gleam/json
import gleam/result
import jscheam/schema
import starlet
import starlet/ollama

// Define your output type
pub type Person {
  Person(name: String, age: Int)
}

// Create a decoder for the type
fn person_decoder() -> decode.Decoder(Person) {
  use name <- decode.field("name", decode.string)
  use age <- decode.field("age", decode.int)
  decode.success(Person(name:, age:))
}

pub fn main() {
  let client = ollama.new("http://localhost:11434")

  // Define the output schema (must match your type)
  let person_schema =
    schema.object([
      schema.prop("name", schema.string()),
      schema.prop("age", schema.integer()),
    ])

  let chat =
    starlet.chat(client, "gpt-oss:20b")
    |> starlet.with_json_output(person_schema)
    |> starlet.user("Extract: Alice is 30 years old.")

  use #(_chat, turn) <- result.try(starlet.send(chat))

  // Get the JSON string
  let json_string = starlet.json(turn)

  // Parse and decode into your type
  case json.parse(json_string, person_decoder()) {
    Ok(person) -> // person.name == "Alice", person.age == 30
    Error(_) -> // Handle decode error
  }
}

Reasoning (Extended Thinking)

import gleam/option.{None, Some}
import gleam/result
import starlet
import starlet/ollama

pub fn main() {
  let client = ollama.new("http://localhost:11434")

  let chat =
    starlet.chat(client, "gpt-oss:20b")
    |> ollama.with_thinking(True)
    |> starlet.user("What is the sum of primes between 1 and 20?")

  use #(_chat, turn) <- result.try(starlet.send(chat))

  // Access thinking content (provider-specific)
  case ollama.thinking(turn) {
    Some(thinking) -> // The model's thinking process
    None -> // No thinking available
  }

  starlet.text(turn)  // The final answer
}
Search Document