zenai

zenai

Zig client for AI APIs, supporting Google Gemini, OpenAI, and Anthropic. Ported from the official Go Gen AI SDK, openai-go, and anthropic-sdk-go. Also ships an agent infrastructure namespace under zenai.search — currently Tavily, with room for sibling providers.

Meditating panda with incense smoke

Installation

zig fetch --save git+https://github.com/lightpanda-io/zenai

Then add the dependency in your build.zig:

const zenai = b.dependency("zenai", .{});
exe.root_module.addImport("zenai", zenai.module("zenai"));

Gemini

Set your API key (get one here):

export GOOGLE_API_KEY='your-api-key'
const zenai = @import("zenai");

const api_key = std.posix.getenv("GOOGLE_API_KEY") orelse return error.MissingApiKey;
var client = zenai.gemini.Client.init(allocator, api_key, .{});
defer client.deinit();

var response = try client.generateContentFromText("gemini-2.5-flash", "What is Zig?", .{}, .{});
defer response.deinit();

std.debug.print("{s}\n", .{response.value.text() orelse ""});

Streaming

try client.generateContentStreamFromText(
    "gemini-2.5-flash",
    "Write a poem about the moon.",
    .{},
    .{},
    {},
    &struct {
        fn cb(_: void, response: zenai.gemini.types.GenerateContentResponse) void {
            if (response.text()) |t| {
                const fd = std.posix.STDOUT_FILENO;
                _ = std.posix.write(fd, t) catch return;
            }
        }
    }.cb,
);

Chat

var chat = zenai.gemini.Chat.init(&client, "gemini-2.5-flash", .{ .temperature = 0 }, .{});
defer chat.deinit();

const r1 = try chat.sendMessage("My name is Alice.");
std.debug.print("{s}\n", .{r1.text() orelse ""});

const r2 = try chat.sendMessage("What is my name?");
std.debug.print("{s}\n", .{r2.text() orelse ""});

Function calling

const tools = [_]zenai.gemini.types.Tool{.{
    .functionDeclarations = &.{.{
        .name = "get_weather",
        .description = "Get the current weather for a city.",
        .parameters = .{
            .type = .OBJECT,
            .properties = &.{
                .{ .key = "city", .value = .{ .type = .STRING } },
            },
            .required = &.{"city"},
        },
    }},
}};

var response = try client.generateContentFromText(
    "gemini-2.5-flash",
    "What's the weather in Paris?",
    .{},
    .{ .tools = &tools },
);
defer response.deinit();

if (response.value.firstFunctionCall()) |fc| {
    std.debug.print("Call: {s}\n", .{fc.name orelse ""});
}

OpenAI

Set your API key (get one here):

export OPENAI_API_KEY='your-api-key'
const zenai = @import("zenai");

const api_key = std.posix.getenv("OPENAI_API_KEY") orelse return error.MissingApiKey;
var client = zenai.openai.Client.init(allocator, api_key, .{});
defer client.deinit();

var response = try client.chatCompletionFromText("gpt-4o", "What is Zig?", .{});
defer response.deinit();

std.debug.print("{s}\n", .{response.value.text() orelse ""});

Streaming

try client.chatCompletionStreamFromText(
    "gpt-4o",
    "Write a poem about the moon.",
    .{},
    {},
    &struct {
        fn cb(_: void, response: zenai.openai.types.ChatCompletionResponse) void {
            if (response.text()) |t| {
                const fd = std.posix.STDOUT_FILENO;
                _ = std.posix.write(fd, t) catch return;
            }
        }
    }.cb,
);

Chat

var chat = zenai.openai.Chat.init(&client, "gpt-4o", .{ .temperature = 0 });
defer chat.deinit();

const r1 = try chat.sendMessage("My name is Alice.");
std.debug.print("{s}\n", .{r1.text() orelse ""});

const r2 = try chat.sendMessage("What is my name?");
std.debug.print("{s}\n", .{r2.text() orelse ""});

Function calling

const tools = [_]zenai.openai.types.Tool{.{
    .type = "function",
    .function = .{
        .name = "get_weather",
        .description = "Get the current weather for a city.",
    },
}};

var response = try client.chatCompletion("gpt-4o", &.{
    .{ .role = .user, .content = "What's the weather in Paris?" },
}, .{ .tools = &tools });
defer response.deinit();

if (response.value.firstToolCall()) |tc| {
    std.debug.print("Call: {s}\n", .{tc.function.?.name orelse ""});
}

Anthropic

Set your API key (get one here):

export ANTHROPIC_API_KEY='your-api-key'
const zenai = @import("zenai");

const api_key = std.posix.getenv("ANTHROPIC_API_KEY") orelse return error.MissingApiKey;
var client = zenai.anthropic.Client.init(allocator, api_key, .{});
defer client.deinit();

var response = try client.createMessageFromText("claude-sonnet-4-6", "What is Zig?", 1024, .{});
defer response.deinit();

std.debug.print("{s}\n", .{response.value.text() orelse ""});

Streaming

try client.createMessageStreamFromText(
    "claude-sonnet-4-6",
    "Write a poem about the moon.",
    1024,
    .{},
    {},
    &struct {
        fn cb(_: void, event: zenai.anthropic.types.StreamEvent) void {
            if (event.delta) |delta| {
                if (delta.text) |t| {
                    const fd = std.posix.STDOUT_FILENO;
                    _ = std.posix.write(fd, t) catch return;
                }
            }
        }
    }.cb,
);

Chat

var chat = zenai.anthropic.Chat.init(&client, "claude-sonnet-4-6", 1024, .{});
defer chat.deinit();

const r1 = try chat.sendMessage("My name is Alice.");
std.debug.print("{s}\n", .{r1.text() orelse ""});

const r2 = try chat.sendMessage("What is my name?");
std.debug.print("{s}\n", .{r2.text() orelse ""});

Function calling

const tools = [_]zenai.anthropic.types.Tool{.{
    .name = "get_weather",
    .description = "Get the current weather for a city.",
    .input_schema = // JSON Schema as std.json.Value
}};

var response = try client.createMessage("claude-sonnet-4-6", &.{
    .{ .role = .user, .content = &.{.{ .text = "What's the weather in Paris?" }} },
}, 1024, .{ .tools = &tools });
defer response.deinit();

if (response.value.firstToolUse()) |tu| {
    std.debug.print("Call: {s}\n", .{tu.name orelse ""});
}

Tavily is an AI-friendly search API that returns clean {title, url, content} JSON results — handy as a low-noise alternative to scraping a SERP. Set your API key (get one here):

export TAVILY_API_KEY='tvly-...'
const zenai = @import("zenai");

const api_key = std.posix.getenv("TAVILY_API_KEY") orelse return error.MissingApiKey;
var client = zenai.search.tavily.Client.init(allocator, api_key, .{});
defer client.deinit();

var response = try client.search("what is zig", .{ .max_results = 5 });
defer response.deinit();

for (response.value.results) |r| {
    std.debug.print("{s} — {s}\n", .{ r.title, r.url });
}

Provider Abstraction

Use zenai.provider.Client to write provider-agnostic code. Swap providers by changing one line:

const zenai = @import("zenai");

// Pick your provider:
var gemini_client = zenai.gemini.Client.init(allocator, gemini_key, .{});
defer gemini_client.deinit();
const ai: zenai.provider.Client = .{ .gemini = &gemini_client };

// Or:
// var openai_client = zenai.openai.Client.init(allocator, openai_key, .{});
// const ai: zenai.provider.Client = .{ .openai = &openai_client };

// Or:
// var anthropic_client = zenai.anthropic.Client.init(allocator, anthropic_key, .{});
// const ai: zenai.provider.Client = .{ .anthropic = &anthropic_client };

var result = try ai.generateContent("gemini-2.5-flash", &.{
    .{ .role = .user, .content = "What is Zig?" },
}, .{});
defer result.deinit();

std.debug.print("{s}\n", .{result.text orelse ""});

Drop down to provider-specific APIs when needed:

switch (ai) {
    .gemini => |g| {
        // Use Gemini-specific features like cached content, file uploads, etc.
        var cached = try g.createCachedContent("gemini-2.5-flash", .{ ... });
    },
    else => {},
}

Features

Gemini:

  • Text generation and streaming (SSE)
  • Multi-turn chat with history management
  • Function calling and tool use
  • Embeddings
  • Token counting
  • File uploads (resumable protocol)
  • Cached content
  • Model listing and info
  • Safety settings and content filtering

OpenAI:

  • Chat completions and streaming (SSE)
  • Multi-turn chat with history management
  • Function calling and tool use
  • Embeddings
  • Model listing and info

Anthropic:

  • Message creation and streaming (SSE)
  • Multi-turn chat with history management
  • Function calling and tool use
  • Extended thinking support

Search providers:

  • Tavily (zenai.search.tavily) — JSON search API with optional synthesized answers, domain include/exclude, news/general topic, time-range filtering

Provider abstraction:

  • Unified text generation, streaming, and embeddings
  • Escape hatches to provider-specific APIs

License

Apache License 2.0 — see LICENSE for details.