Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

149 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

zenai

Zig client for AI APIs, supporting Google Gemini (Developer API and Vertex AI), OpenAI, and Anthropic. OpenAI-compatible endpoints — Ollama, Hugging Face Inference, and llama.cpp (llama-server) — are supported through the OpenAI client. 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 and Brave Search, 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"));

Requires Zig >= 0.16.0. The examples below assume allocator, io, and environ are in scope; with Zig 0.16's main signature they come straight from std.process.Init:

pub fn main(init: std.process.Init) !void {
    const allocator = init.gpa;
    const io = init.io;
    const environ = init.minimal.environ;
    // ...
}

Gemini

Set your API key (get one here):

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

const api_key = environ.getPosix("GOOGLE_API_KEY") orelse return error.MissingApiKey;
var client = zenai.gemini.Client.init(io, 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| {
                std.debug.print("{s}", .{t});
            }
        }
    }.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 ""});
}

Google Vertex AI

The same Gemini client can target Vertex AI instead of the Gemini Developer API. Two modes:

Express mode — a plain Vertex API key, no project needed:

var client = zenai.vertex.Client.init(io, allocator, api_key, .{ .vertex = .{} });
defer client.deinit();

Project/location mode — pass an OAuth access token as the key (refreshing it is your job; tokens expire after ~1 hour):

export TOKEN=$(gcloud auth print-access-token)
var client = zenai.vertex.Client.init(io, allocator, token, .{
    .vertex = .{ .project = "my-project", .location = "global" },
});
defer client.deinit();

Generation, streaming, Chat, and countTokens work in both modes. Model listing works on Vertex too, but Google's ListPublisherModels rejects API keys, so it needs project/location mode (OAuth). Embeddings, file uploads, and cached content are Developer-API-only and return error.UnsupportedByBackend on Vertex.

Through the provider abstraction, .vertex is env-detected when GOOGLE_GENAI_USE_VERTEXAI=1 (or true) is set: express mode reads GOOGLE_API_KEY; project mode reads GOOGLE_CLOUD_PROJECT and GOOGLE_CLOUD_LOCATION/GOOGLE_CLOUD_REGION (default "global") but needs the access token passed explicitly as the credential.

OpenAI

Set your API key (get one here):

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

const api_key = environ.getPosix("OPENAI_API_KEY") orelse return error.MissingApiKey;
var client = zenai.openai.Client.init(io, 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| {
                std.debug.print("{s}", .{t});
            }
        }
    }.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 = environ.getPosix("ANTHROPIC_API_KEY") orelse return error.MissingApiKey;
var client = zenai.anthropic.Client.init(io, 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| {
                    std.debug.print("{s}", .{t});
                }
            }
        }
    }.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 (search)

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 = environ.getPosix("TAVILY_API_KEY") orelse return error.MissingApiKey;
var client = zenai.search.tavily.Client.init(io, 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 });
}

Brave (search)

Brave Search is a web search API over Brave's independent index, returning JSON sections of {title, url, description} results. Set your API key (get one here):

export BRAVE_API_KEY='BSA...'
const zenai = @import("zenai");

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

// text_decorations=false strips <strong> highlight markup from descriptions.
var response = try client.search("what is zig", .{ .count = 5, .text_decorations = false });
defer response.deinit();

if (response.value.web) |web| {
    for (web.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(io, allocator, gemini_key, .{});
defer gemini_client.deinit();
const ai: zenai.provider.Client = .{ .gemini = &gemini_client };

// Or Vertex AI (same Gemini client, Vertex backend — see the Vertex section):
// var vertex_client = zenai.vertex.Client.init(io, allocator, token, .{
//     .vertex = .{ .project = "my-project" },
// });
// const ai: zenai.provider.Client = .{ .vertex = &vertex_client };

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

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

// Or Hugging Face (OpenAI-compatible). Token comes from HF_TOKEN. Defaults to the
// serverless router; pass `.base_url` for a dedicated Inference Endpoint:
// var hf_client = zenai.huggingface.Client.init(io, allocator, hf_token, .{
//     .base_url = "https://router.huggingface.co/v1",
// });
// const ai: zenai.provider.Client = .{ .huggingface = &hf_client };

// Or a local llama.cpp `llama-server` (OpenAI-compatible). No key needed;
// defaults to http://localhost:8080/v1 — override with `.base_url`:
// var llama_client = zenai.llama_cpp.Client.init(io, allocator, "llama.cpp", .{
//     .base_url = "http://localhost:8080/v1",
// });
// const ai: zenai.provider.Client = .{ .llama_cpp = &llama_client };

// Or any OpenAI-compatible server (vLLM, LiteLLM, Together, Groq, etc.).
// Auto-detected by `provider.Client.init` when OPENAI_BASE_URL is set; the
// key comes from OPENAI_API_KEY. With the raw client, pass `.base_url`:
// var custom_client = zenai.openai_compatible.Client.init(io, allocator, api_key, .{
//     .base_url = "https://my-custom-server.com/v1",
// });
// const ai: zenai.provider.Client = .{ .openai_compatible = &custom_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
  • Vertex AI backend (express-mode API key, or project/location with an OAuth access token) for generation, streaming, chat, token counting, and model listing

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
  • Brave (zenai.search.brave) — independent-index web search with country/language targeting, safesearch, freshness and section filtering, extra snippets

Provider abstraction:

  • Unified text generation, streaming, and embeddings
  • OpenAI-compatible backends: Ollama, Hugging Face, llama.cpp (llama-server), and any custom server via OPENAI_BASE_URL
  • lastError() to surface the status and message behind a failed request
  • Escape hatches to provider-specific APIs

License

Apache License 2.0 — see LICENSE for details.

Releases

Packages

Contributors

Languages