From 09cdf2b2df5d65e7a5eddcc67503847e446639a2 Mon Sep 17 00:00:00 2001 From: Beta-Devin AI <248786709+beta-devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 7 Apr 2026 18:01:05 +0000 Subject: [PATCH] Add embeddings, models, moderations, and image generation endpoints - Add createEmbedding() for POST /v1/embeddings - Add listModels() for GET /v1/models - Add retrieveModel() for GET /v1/models/{model} - Add createModeration() for POST /v1/moderations - Add createImage() for POST /v1/images/generations - Add makeRequest() helper for GET requests - Fix double-free bug in chat() error path - Update README with API reference for all endpoints Co-Authored-By: ahlback.emil+coggitgrant --- README.md | 63 +++++++++++++++- src/llm.zig | 214 +++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 272 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index be512f2..2abc2f4 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,65 @@ pub fn build(b: *std.Build) void { } ``` -## Usage +## API Reference + +### Chat Completions + +```zig +// Non-streaming +const result = try openai.chat(payload, false); +defer result.deinit(); + +// Streaming +var stream = try openai.streamChat(payload, false); +defer stream.deinit(); +while (try stream.next()) |response| { ... } +``` + +### Embeddings + +```zig +const result = try openai.createEmbedding(.{ + .model = "text-embedding-3-small", + .input = "The quick brown fox", +}); +defer result.deinit(); +const vector = result.value.data[0].embedding; +``` + +### Models + +```zig +// List all models +const models = try openai.listModels(); +defer models.deinit(); + +// Retrieve a specific model +const model = try openai.retrieveModel("gpt-4o"); +defer model.deinit(); +``` + +### Moderations + +```zig +const result = try openai.createModeration(.{ + .input = "Some text to check", +}); +defer result.deinit(); +const flagged = result.value.results[0].flagged; +``` + +### Image Generation + +```zig +const result = try openai.createImage(.{ + .prompt = "A white siamese cat", + .model = "dall-e-3", + .n = 1, + .size = "1024x1024", +}); +defer result.deinit(); +const url = result.value.data[0].url; +``` -See the `examples` directory for usage examples. +See the `examples` directory for more usage examples. diff --git a/src/llm.zig b/src/llm.zig index bfc0b80..340b7eb 100644 --- a/src/llm.zig +++ b/src/llm.zig @@ -66,6 +66,99 @@ pub const ModelResponse = struct { data: []Model, }; +// Embeddings types +pub const Embedding = struct { + object: []const u8, + embedding: []f64, + index: usize, +}; + +pub const EmbeddingUsage = struct { + prompt_tokens: u64, + total_tokens: u64, +}; + +pub const EmbeddingResponse = struct { + object: []const u8, + data: []Embedding, + model: []const u8, + usage: EmbeddingUsage, +}; + +pub const EmbeddingPayload = struct { + model: []const u8, + input: []const u8, + dimensions: ?u32 = null, +}; + +// Moderation types +pub const ModerationCategories = struct { + hate: bool, + @"hate/threatening": bool, + harassment: bool, + @"harassment/threatening": bool, + @"self-harm": bool, + @"self-harm/intent": bool, + @"self-harm/instructions": bool, + sexual: bool, + @"sexual/minors": bool, + violence: bool, + @"violence/graphic": bool, +}; + +pub const ModerationCategoryScores = struct { + hate: f64, + @"hate/threatening": f64, + harassment: f64, + @"harassment/threatening": f64, + @"self-harm": f64, + @"self-harm/intent": f64, + @"self-harm/instructions": f64, + sexual: f64, + @"sexual/minors": f64, + violence: f64, + @"violence/graphic": f64, +}; + +pub const ModerationResult = struct { + flagged: bool, + categories: ModerationCategories, + category_scores: ModerationCategoryScores, +}; + +pub const ModerationResponse = struct { + id: []const u8, + model: []const u8, + results: []ModerationResult, +}; + +pub const ModerationPayload = struct { + input: []const u8, + model: ?[]const u8 = null, +}; + +// Image generation types +pub const ImageData = struct { + url: ?[]const u8 = null, + b64_json: ?[]const u8 = null, + revised_prompt: ?[]const u8 = null, +}; + +pub const ImageResponse = struct { + created: u64, + data: []ImageData, +}; + +pub const ImagePayload = struct { + prompt: []const u8, + model: ?[]const u8 = null, + n: ?u32 = null, + size: ?[]const u8 = null, + quality: ?[]const u8 = null, + response_format: ?[]const u8 = null, + style: ?[]const u8 = null, +}; + const StreamReader = struct { arena: std.heap.ArenaAllocator, request: std.http.Client.Request, @@ -175,6 +268,26 @@ pub const Client = struct { return headers; } + fn makeRequest(self: *Client, method: std.http.Method, endpoint: []const u8) !std.http.Client.Request { + const headers = try get_headers(self.allocator, self.api_key); + defer self.allocator.free(headers.authorization.override); + + var buf: [16 * 1024]u8 = undefined; + + const path = try std.fmt.allocPrint(self.allocator, "{s}{s}", .{ self.base_url, endpoint }); + defer self.allocator.free(path); + const uri = try std.Uri.parse(path); + + var req = try self.http_client.open(method, uri, .{ .headers = headers, .server_header_buffer = &buf }); + errdefer req.deinit(); + + try req.send(); + try req.finish(); + try req.wait(); + + return req; + } + fn makeCall(self: *Client, endpoint: []const u8, body: []const u8, _: bool) !std.http.Client.Request { const headers = try get_headers(self.allocator, self.api_key); defer self.allocator.free(headers.authorization.override); @@ -239,9 +352,7 @@ pub const Client = struct { defer req.deinit(); if (req.response.status != .ok) { - const err = getError(req.response.status); - req.deinit(); - return err; + return getError(req.response.status); } const response = try req.reader().readAllAlloc(self.allocator, 1024 * 8); @@ -252,6 +363,103 @@ pub const Client = struct { return parsed; } + /// Lists the currently available models. + pub fn listModels(self: *Client) !std.json.Parsed(ModelResponse) { + var req = try self.makeRequest(.GET, "/models"); + defer req.deinit(); + + if (req.response.status != .ok) { + return getError(req.response.status); + } + + const response = try req.reader().readAllAlloc(self.allocator, 1024 * 64); + defer self.allocator.free(response); + + const parsed = try std.json.parseFromSlice(ModelResponse, self.allocator, response, .{ .ignore_unknown_fields = true, .allocate = .alloc_always }); + + return parsed; + } + + /// Retrieves a model instance by its ID. + pub fn retrieveModel(self: *Client, model_id: []const u8) !std.json.Parsed(Model) { + const endpoint = try std.fmt.allocPrint(self.allocator, "/models/{s}", .{model_id}); + defer self.allocator.free(endpoint); + + var req = try self.makeRequest(.GET, endpoint); + defer req.deinit(); + + if (req.response.status != .ok) { + return getError(req.response.status); + } + + const response = try req.reader().readAllAlloc(self.allocator, 1024 * 8); + defer self.allocator.free(response); + + const parsed = try std.json.parseFromSlice(Model, self.allocator, response, .{ .ignore_unknown_fields = true, .allocate = .alloc_always }); + + return parsed; + } + + /// Creates an embedding vector representing the input text. + pub fn createEmbedding(self: *Client, payload: EmbeddingPayload) !std.json.Parsed(EmbeddingResponse) { + const body = try std.json.stringifyAlloc(self.allocator, payload, .{}); + defer self.allocator.free(body); + + var req = try self.makeCall("/embeddings", body, false); + defer req.deinit(); + + if (req.response.status != .ok) { + return getError(req.response.status); + } + + const response = try req.reader().readAllAlloc(self.allocator, 1024 * 64); + defer self.allocator.free(response); + + const parsed = try std.json.parseFromSlice(EmbeddingResponse, self.allocator, response, .{ .ignore_unknown_fields = true, .allocate = .alloc_always }); + + return parsed; + } + + /// Classifies if the input text is potentially harmful. + pub fn createModeration(self: *Client, payload: ModerationPayload) !std.json.Parsed(ModerationResponse) { + const body = try std.json.stringifyAlloc(self.allocator, payload, .{}); + defer self.allocator.free(body); + + var req = try self.makeCall("/moderations", body, false); + defer req.deinit(); + + if (req.response.status != .ok) { + return getError(req.response.status); + } + + const response = try req.reader().readAllAlloc(self.allocator, 1024 * 16); + defer self.allocator.free(response); + + const parsed = try std.json.parseFromSlice(ModerationResponse, self.allocator, response, .{ .ignore_unknown_fields = true, .allocate = .alloc_always }); + + return parsed; + } + + /// Creates an image given a prompt. + pub fn createImage(self: *Client, payload: ImagePayload) !std.json.Parsed(ImageResponse) { + const body = try std.json.stringifyAlloc(self.allocator, payload, .{}); + defer self.allocator.free(body); + + var req = try self.makeCall("/images/generations", body, false); + defer req.deinit(); + + if (req.response.status != .ok) { + return getError(req.response.status); + } + + const response = try req.reader().readAllAlloc(self.allocator, 1024 * 256); + defer self.allocator.free(response); + + const parsed = try std.json.parseFromSlice(ImageResponse, self.allocator, response, .{ .ignore_unknown_fields = true, .allocate = .alloc_always }); + + return parsed; + } + pub fn deinit(self: *Client) void { self.allocator.free(self.api_key); if (self.organization_id) |org_id| {