Claude API Module for Duso

Access Anthropic’s Claude API directly from Duso scripts with an options-based, idiomatic interface.

Setup

Set your API key as an environment variable:

export ANTHROPIC_API_KEY=sk-ant-xxxxx
duso script.du

Or pass it explicitly in your script:

claude = require("claude")
response = claude.prompt("Hello", {key = "sk-ant-xxxxx"})

Quick Start

One-shot query

claude = require("claude")
response = claude.prompt("What is Duso?")
print(response)

Multi-turn conversation

claude = require("claude")

chat = claude.session({
  system = "You are a helpful assistant"
})

response1 = chat.prompt("What is a closure?")
response2 = chat.prompt("Can you give me an example?")

print(chat.usage)  // Check token usage

With temperature control

claude = require("claude")

// Lower temperature = more deterministic
response = claude.prompt("Solve this math problem: 2 + 2", {
  temperature = 0.5
})

// Higher temperature = more creative
response = claude.prompt("Write a poem about code", {
  temperature = 1.0
})

With tools (Agent patterns)

claude = require("claude")

// Define a tool using standard format
var calculator = {
  name = "calculator",
  description = "Performs basic math operations",
  parameters = {
    operation = {type = "string"},
    a = {type = "number"},
    b = {type = "number"}
  },
  required = ["operation", "a", "b"],
  handler = function(data)
    if data.operation == "add" then return data.a + data.b end
    if data.operation == "multiply" then return data.a * data.b end
  end
}

// Create agent - handler is automatically extracted!
agent = claude.session({
  tools = [calculator]
})

// Ask the agent - it will automatically call tools
response = agent.prompt("What is 15 * 27?")
print(response)  // "405"

With request timeout

Set a custom timeout for API requests (default 30 seconds):

claude = require("claude")

// 10 second timeout for fast failure on slow connections
chat = claude.session({
  system = "You are helpful",
  timeout = 10
})

response = chat.prompt("Quick question")

Prompt caching differences

Claude requires explicit cache_control markers. OpenAI caches automatically on GPT-4o+.

With prompt caching (save 90% on input tokens!) - Claude

Enable caching on system prompts and tool definitions to reduce API costs. Cached content is reused across requests:

claude = require("claude")

var tools = [
  {
    name = "search",
    description = "Search the knowledge base",
    parameters = {query = {type = "string"}},
    required = ["query"]
  }
]

// Enable caching on system prompt and tools
chat = claude.session({
  system = "You are a helpful research assistant with access to tools.",
  tools = tools,
  cache_control = {
    system = true,    // Cache the system prompt
    tools = true      // Cache all tool definitions
  }
})

// First request: builds cache (costs normal tokens)
response1 = chat.prompt("Search for information about prompt caching")

// Second request: reuses cache (saves ~90% on system + tools input tokens!)
response2 = chat.prompt("What did you find?")

// usage tracks cache tokens separately, so you can confirm caching worked:
print(chat.usage)
// {input_tokens = 42, output_tokens = 310,
//  cache_creation_input_tokens = 1840, cache_read_input_tokens = 1840}

Measuring cache effectiveness:

usage accumulates four counters, not two. cache_creation_input_tokens is what it cost to write the cache, cache_read_input_tokens is what you read back instead of paying full input price:

var u = chat.usage
if u.cache_read_input_tokens > 0 then
  print("cache hit: " + tostring(u.cache_read_input_tokens) + " tokens reused")
end

A first request shows creation tokens and zero reads. Subsequent requests inside the cache lifetime show reads climbing while input_tokens stays small. If reads stay at zero across turns, caching is not taking effect.

The invariant: input_tokens never includes cached reads. The four fields are disjoint, so they sum to the true input total and each can be priced at its own rate. The openai module normalizes to this same shape, so accounting code works across vendors without knowing which one it is on.

Benefits:

API Reference

claude.prompt(message, config)

Send a one-shot query to Claude.

Parameters:

Returns:

Examples:

// Basic
response = claude.prompt("What is the capital of France?")

// With system prompt
response = claude.prompt("Translate 'hello' to Spanish", {
  system = "You are a translator"
})

// With model override
response = claude.prompt("Solve this complex problem", {
  max_tokens = 4096
})

// With temperature
response = claude.prompt("Write a story", {
  temperature = 1.5
})

claude.session(config)

Create a multi-turn conversation session.

Parameters:

Returns:

Examples:

// Basic conversation
chat = claude.session({
  system = "You are a helpful assistant",
  temperature = 0.8
})

response1 = chat.prompt("Tell me about Duso")
response2 = chat.prompt("What are its main features?")

print(chat.usage)
// {input_tokens = 234, output_tokens = 567,
//  cache_creation_input_tokens = 0, cache_read_input_tokens = 0}

// With tools
agent = claude.session({
  tools = [my_tool],
  tool_handlers = {my_tool_name = my_handler_function},
  auto_execute_tools = true
})

response = agent.prompt("Use the tool to answer this")

// Manual tool handling
chat = claude.session({
  tools = [my_tool],
  tool_handlers = {},
  auto_execute_tools = false
})

response = chat.prompt("Use the tool")
// Process response.content manually
chat.add_tool_result(tool_use_id, result)
chat.continue_conversation()

Tool Format

Define tools using the standard format with name, description, parameters, required, and optional handler:

Tool Structure:

Examples:

// Tool with handler
var greet = {
  name = "greet",
  description = "Greet someone",
  parameters = {name = {type = "string"}},
  required = ["name"],
  handler = function(data)
    return "Hello, " + data.name + "!"
  end
}

// Tool without handler (manual handling)
var info = {
  name = "get_info",
  description = "Get information",
  parameters = {topic = {type = "string"}},
  required = ["topic"]
}

// Use in session
agent = claude.session({
  tools = [greet, info]
})

claude.models(key)

List all models available for your account.

Parameters:

Returns:

Example:

models = claude.models()
for m in models do
  print(m.id)
end

Configuration Options

All config options that can be passed to prompt() or session():

Option Type Default Description
model string claude-haiku-4-5-20251001 Model ID to use
max_tokens number 2048 Maximum tokens in response
temperature number 1.0 Sampling temperature (0-2)
top_p number nil Nucleus sampling parameter (0-1)
top_k number nil Top-k sampling parameter
system string nil System prompt
tools array nil Array of tool definitions
tool_handlers object {} Map of tool names to handler functions
auto_execute_tools bool true Auto-execute tools in response loop
tool_choice string auto Tool selection: auto, any, none
cache_control object nil Prompt caching config: {system = true, tools = true}
timeout number 30 Request timeout in seconds
skills array nil Agent skills (experimental)
key string nil API key (uses ANTHROPIC_API_KEY if not provided)

Available Models

As of 2025, Anthropic’s latest models are:

See Anthropic’s models page for the latest.

Tool Use

Define tools using the standard format and handlers are automatically extracted:

var my_tool = {
  name = "web_search",
  description = "Search the web",
  parameters = {
    query = {type = "string", description = "Search query"}
  },
  required = ["query"],
  handler = function(data)
    // Implement search
    return results
  end
}

// Handler is automatically extracted and registered!
chat = claude.session({
  tools = [my_tool]
})

response = chat.prompt("Search for Duso")

You can also include vendor-specific extras in tools that other vendors will ignore:

var multi_vendor_tool = {
  name = "tool_name",
  description = "Tool description",
  parameters = {query = {type = "string"}},
  required = ["query"],
  handler = function(data) return "result" end,
  some_vendor_field = "value"
}

session = claude.session({
  tools = [multi_vendor_tool]
})

When auto_execute_tools = true (default), Claude’s tool calls are automatically executed and results integrated into the conversation. When false, you can process tool calls manually:

chat = claude.session({
  tools = [my_tool],
  auto_execute_tools = false
})

response = chat.prompt("Use the tool")

// Check if Claude requested tool use
if contains(response, "tool") then
  // Process manually - here tool_id and result would come from response
  // chat.add_tool_result(tool_id, result)
  // response = chat.continue()
end

Temperature and Sampling

Control response creativity with temperature and sampling parameters:

Typical configurations:

// Analytical
{temperature = 0.5, top_p = 0.9}

// Balanced
{temperature = 1.0}

// Creative
{temperature = 1.5, top_p = 0.95}

Environment Variables

Error Handling

try
  claude = require("claude")
  response = claude.prompt("Hello")
  print(response)
catch (e)
  print("Error: " + error)
end

Common errors:

Pricing

Claude API uses pay-as-you-go pricing based on tokens:

See Anthropic pricing for current rates.

Tips to reduce costs:

Examples

Duso Idioms

This module uses Duso’s best practices:

  1. Constructor pattern - default_config(overrides) for clean config merging
  2. Options objects - Single config object instead of many parameters
  3. Idiomatic style - Uses closures, functional patterns
  4. Proper error handling - Try/catch for API and tool errors
  5. Resource efficiency - Reusable sessions for multi-turn conversations

Advanced: Manual Tool Handling

For complex agent patterns, disable auto execution and handle tools manually:

chat = claude.session({
  tools = [my_tool],
  auto_execute_tools = false
})

// In a loop:
response = chat.prompt(user_input)
if has_tool_calls(response) then
  tool_calls = extract_tool_calls(response)
  results = {}
  for tool_call in tool_calls do
    results[tool_call.id] = execute_tool(tool_call)
  end
  // chat.add_tool_result(...)
  response = chat.continue_conversation()
end

Notes

See Also