diff --git a/generated/generated_v2.zig b/generated/generated_v2.zig index 42d7e50..fbc8524 100644 --- a/generated/generated_v2.zig +++ b/generated/generated_v2.zig @@ -1032,6 +1032,7 @@ pub fn logoutUserRaw(client: *Client) !RawResponse { return requestRaw(client, std.http.Method.GET, uri_buf.written(), payload); } + pub const resources = struct { pub const pet = struct { pub fn addpet(client: *Client, requestBody: Pet) !void { @@ -1121,12 +1122,12 @@ pub const resources = struct { return updateUser(client, username, requestBody); } pub const createwitharray = struct { - pub fn create(client: *Client, requestBody: []const std.json.Value) !void { + pub fn createuserswitharrayinput_(client: *Client, requestBody: []const std.json.Value) !void { return createUsersWithArrayInput(client, requestBody); } }; pub const createwithlist = struct { - pub fn create(client: *Client, requestBody: []const std.json.Value) !void { + pub fn createuserswithlistinput_(client: *Client, requestBody: []const std.json.Value) !void { return createUsersWithListInput(client, requestBody); } }; diff --git a/generated/generated_v2_yaml.zig b/generated/generated_v2_yaml.zig index 593ade2..47906cf 100644 --- a/generated/generated_v2_yaml.zig +++ b/generated/generated_v2_yaml.zig @@ -1032,6 +1032,7 @@ pub fn logoutUserRaw(client: *Client) !RawResponse { return requestRaw(client, std.http.Method.GET, uri_buf.written(), payload); } + pub const resources = struct { pub const pet = struct { pub fn addpet(client: *Client, requestBody: Pet) !void { @@ -1121,12 +1122,12 @@ pub const resources = struct { return updateUser(client, username, requestBody); } pub const createwitharray = struct { - pub fn create(client: *Client, requestBody: []const std.json.Value) !void { + pub fn createuserswitharrayinput_(client: *Client, requestBody: []const std.json.Value) !void { return createUsersWithArrayInput(client, requestBody); } }; pub const createwithlist = struct { - pub fn create(client: *Client, requestBody: []const std.json.Value) !void { + pub fn createuserswithlistinput_(client: *Client, requestBody: []const std.json.Value) !void { return createUsersWithListInput(client, requestBody); } }; diff --git a/generated/generated_v3.zig b/generated/generated_v3.zig index 996f9a1..8aa71e1 100644 --- a/generated/generated_v3.zig +++ b/generated/generated_v3.zig @@ -1093,6 +1093,7 @@ pub fn logoutUserRaw(client: *Client) !RawResponse { return requestRaw(client, std.http.Method.GET, uri_buf.written(), payload); } + pub const resources = struct { pub const pet = struct { pub fn addpet(client: *Client, requestBody: Pet) !Owned(Pet) { @@ -1188,10 +1189,10 @@ pub const resources = struct { return updateUser(client, username, requestBody); } pub const createwithlist = struct { - pub fn create(client: *Client, requestBody: []const std.json.Value) !Owned(User) { + pub fn createuserswithlistinput_(client: *Client, requestBody: []const std.json.Value) !Owned(User) { return createUsersWithListInput(client, requestBody); } - pub fn createResult(client: *Client, requestBody: []const std.json.Value) !ApiResult(User) { + pub fn createuserswithlistinput_Result(client: *Client, requestBody: []const std.json.Value) !ApiResult(User) { return createUsersWithListInputResult(client, requestBody); } }; diff --git a/generated/generated_v32.zig b/generated/generated_v32.zig index 74908fb..1f7e68a 100644 --- a/generated/generated_v32.zig +++ b/generated/generated_v32.zig @@ -747,6 +747,7 @@ pub fn findPetsByStatusResult(client: *Client, status: ?[]const u8) !ApiResult([ return parseRawResponse([]const std.json.Value, try findPetsByStatusRaw(client, status)); } + pub const resources = struct { pub const pet = struct { pub fn addpet(client: *Client, requestBody: Pet) !Owned(Pet) { diff --git a/generated/generated_v3_yaml.zig b/generated/generated_v3_yaml.zig index 996f9a1..8aa71e1 100644 --- a/generated/generated_v3_yaml.zig +++ b/generated/generated_v3_yaml.zig @@ -1093,6 +1093,7 @@ pub fn logoutUserRaw(client: *Client) !RawResponse { return requestRaw(client, std.http.Method.GET, uri_buf.written(), payload); } + pub const resources = struct { pub const pet = struct { pub fn addpet(client: *Client, requestBody: Pet) !Owned(Pet) { @@ -1188,10 +1189,10 @@ pub const resources = struct { return updateUser(client, username, requestBody); } pub const createwithlist = struct { - pub fn create(client: *Client, requestBody: []const std.json.Value) !Owned(User) { + pub fn createuserswithlistinput_(client: *Client, requestBody: []const std.json.Value) !Owned(User) { return createUsersWithListInput(client, requestBody); } - pub fn createResult(client: *Client, requestBody: []const std.json.Value) !ApiResult(User) { + pub fn createuserswithlistinput_Result(client: *Client, requestBody: []const std.json.Value) !ApiResult(User) { return createUsersWithListInputResult(client, requestBody); } }; diff --git a/generated/lmstudio.zig b/generated/lmstudio.zig new file mode 100644 index 0000000..ecb6562 --- /dev/null +++ b/generated/lmstudio.zig @@ -0,0 +1,875 @@ +const std = @import("std"); + +/////////////////////////////////////////// +// Generated Zig structures from OpenAPI +/////////////////////////////////////////// + +pub const LlmLoadConfig = struct { + context_length: i64, + flash_attention: ?bool = null, + eval_batch_size: ?i64 = null, + num_experts: ?i64 = null, + offload_kv_cache_to_gpu: ?bool = null, +}; + +pub const UnloadModelRequest = struct { + instance_id: []const u8, +}; + +pub const ReasoningOutput = struct { + @"type": []const u8, + content: []const u8, +}; + +pub const ListModelsResponse = struct { + models: []const ModelInfo, +}; + +pub const DownloadModelResponse = struct { + status: []const u8, + job_id: ?[]const u8 = null, + completed_at: ?[]const u8 = null, + total_size_bytes: ?i64 = null, + started_at: ?[]const u8 = null, +}; + +pub const ChatOutputItem = std.json.Value; + +pub const LoadedInstance = struct { + id: []const u8, + config: LoadedInstanceConfig, +}; + +pub const TextInput = struct { + @"type": []const u8, + content: []const u8, +}; + +pub const LoadModelRequest = struct { + context_length: ?i64 = null, + flash_attention: ?bool = null, + eval_batch_size: ?i64 = null, + num_experts: ?i64 = null, + offload_kv_cache_to_gpu: ?bool = null, + model: []const u8, + echo_load_config: ?bool = null, +}; + +pub const ModelInfoCapabilitiesReasoning = struct { + allowed_options: []const []const u8, + default: []const u8, +}; + +pub const ModelInfoCapabilities = struct { + trained_for_tool_use: ?bool = null, + reasoning: ?ModelInfoCapabilitiesReasoning = null, + vision: ?bool = null, +}; + +pub const ModelInfoQuantization = struct { + name: []const u8, + bits_per_weight: f64, +}; + +pub const ModelInfo = struct { + params_string: ?[]const u8 = null, + publisher: []const u8, + key: []const u8, + format: []const u8, + variants: ?[]const []const u8 = null, + selected_variant: ?[]const u8 = null, + display_name: []const u8, + size_bytes: i64, + architecture: ?[]const u8 = null, + max_context_length: i64, + capabilities: ?ModelInfoCapabilities = null, + loaded_instances: []const LoadedInstance, + quantization: ?ModelInfoQuantization = null, + description: ?[]const u8 = null, + @"type": []const u8, +}; + +pub const EmbeddingLoadConfig = struct { + context_length: i64, +}; + +pub const DownloadStatusResponse = struct { + completed_at: ?[]const u8 = null, + bytes_per_second: ?i64 = null, + estimated_completion: ?[]const u8 = null, + total_size_bytes: ?i64 = null, + started_at: ?[]const u8 = null, + status: []const u8, + job_id: []const u8, + downloaded_bytes: ?i64 = null, +}; + +pub const PluginIntegration = struct { + allowed_tools: ?[]const []const u8 = null, + id: []const u8, + @"type": []const u8, +}; + +pub const ChatStats = struct { + input_tokens: i64, + reasoning_output_tokens: i64, + tokens_per_second: f64, + time_to_first_token_seconds: f64, + total_output_tokens: i64, + model_load_time_seconds: ?f64 = null, +}; + +pub const ChatResponse = struct { + stats: ChatStats, + response_id: ?[]const u8 = null, + model_instance_id: []const u8, + output: []const ChatOutputItem, +}; + +pub const DownloadModelRequest = struct { + quantization: ?[]const u8 = null, + model: []const u8, +}; + +pub const ChatRequest = struct { + integrations: ?[]const std.json.Value = null, + reasoning: ?[]const u8 = null, + temperature: ?f64 = null, + max_output_tokens: ?i64 = null, + store: ?bool = null, + model: []const u8, + stream: ?bool = null, + context_length: ?i64 = null, + repeat_penalty: ?f64 = null, + top_p: ?f64 = null, + top_k: ?i64 = null, + min_p: ?f64 = null, + previous_response_id: ?[]const u8 = null, + input: std.json.Value, + system_prompt: ?[]const u8 = null, +}; + +pub const MessageOutput = struct { + @"type": []const u8, + content: []const u8, +}; + +pub const LoadedInstanceConfig = struct { + context_length: i64, + num_experts: ?i64 = null, + flash_attention: ?bool = null, + eval_batch_size: ?i64 = null, + parallel: ?i64 = null, + offload_kv_cache_to_gpu: ?bool = null, +}; + +pub const ImageInput = struct { + data_url: []const u8, + @"type": []const u8, +}; + +pub const UnloadModelResponse = struct { + instance_id: []const u8, +}; + +pub const InvalidToolCallOutputMetadata = struct { + arguments: ?std.json.Value = null, + tool_name: []const u8, + @"type": []const u8, + provider_info: ?ProviderInfo = null, +}; + +pub const InvalidToolCallOutput = struct { + metadata: InvalidToolCallOutputMetadata, + reason: []const u8, + @"type": []const u8, +}; + +pub const LoadModelResponse = struct { + load_time_seconds: f64, + status: []const u8, + instance_id: []const u8, + load_config: ?std.json.Value = null, + @"type": []const u8, +}; + +pub const ProviderInfo = struct { + @"type": []const u8, + plugin_id: ?[]const u8 = null, + server_label: ?[]const u8 = null, +}; + +pub const ChatInputItem = std.json.Value; + +pub const EphemeralMcpIntegration = struct { + server_url: []const u8, + allowed_tools: ?[]const []const u8 = null, + @"type": []const u8, + headers: ?std.json.Value = null, + server_label: []const u8, +}; + +pub const ToolCallOutput = struct { + arguments: std.json.Value, + tool: []const u8, + output: []const u8, + @"type": []const u8, + provider_info: ProviderInfo, +}; + + +/////////////////////////////////////////// +// Generated Zig API client from OpenAPI +/////////////////////////////////////////// + + +pub fn Owned(comptime T: type) type { + return struct { + allocator: std.mem.Allocator, + body: []u8, + parsed: std.json.Parsed(T), + + pub fn deinit(self: *@This()) void { + self.parsed.deinit(); + self.allocator.free(self.body); + } + + pub fn value(self: *@This()) *T { + return &self.parsed.value; + } + }; +} + +pub const RawResponse = struct { + allocator: std.mem.Allocator, + status: std.http.Status, + body: []u8, + + pub fn deinit(self: *@This()) void { + self.allocator.free(self.body); + } +}; + +pub const ParseErrorResponse = struct { + raw: RawResponse, + error_name: []const u8, +}; + +pub fn ApiResult(comptime T: type) type { + return union(enum) { + ok: Owned(T), + api_error: RawResponse, + parse_error: ParseErrorResponse, + + pub fn deinit(self: *@This()) void { + switch (self.*) { + .ok => |*value| value.deinit(), + .api_error => |*value| value.deinit(), + .parse_error => |*value| value.raw.deinit(), + } + } + }; +} + +pub const Client = struct { + allocator: std.mem.Allocator, + io: std.Io, + http: std.http.Client, + api_key: []const u8, + base_url: []const u8 = "", + organization: ?[]const u8 = null, + project: ?[]const u8 = null, + default_headers: []const std.http.Header = &.{}, + + pub fn init(allocator: std.mem.Allocator, io: std.Io, api_key: []const u8) Client { + return .{ + .allocator = allocator, + .io = io, + .http = .{ .allocator = allocator, .io = io }, + .api_key = api_key, + }; + } + + pub fn deinit(self: *Client) void { + self.http.deinit(); + } + + pub fn withBaseUrl(self: *Client, base_url: []const u8) void { + self.base_url = base_url; + } +}; + +fn isQueryChar(c: u8) bool { + return std.ascii.isAlphanumeric(c) or switch (c) { + '-', '.', '_', '~' => true, + else => false, + }; +} + +fn writeQueryComponent(writer: *std.Io.Writer, value: []const u8) !void { + try std.Uri.Component.percentEncode(writer, value, isQueryChar); +} + +fn writeQueryValue(writer: *std.Io.Writer, value: anytype) !void { + const T = @TypeOf(value); + switch (@typeInfo(T)) { + .pointer => |ptr| { + if (ptr.size == .slice and ptr.child == u8) { + try writeQueryComponent(writer, value); + } else { + try std.json.Stringify.value(value, .{}, writer); + } + }, + .int, .comptime_int, .float, .comptime_float, .bool => try writer.print("{}", .{value}), + .@"enum" => try writeQueryComponent(writer, @tagName(value)), + else => try std.json.Stringify.value(value, .{}, writer), + } +} + +fn appendQueryParam(writer: *std.Io.Writer, first_query: *bool, name: []const u8, value: anytype) !void { + if (first_query.*) { + try writer.writeByte('?'); + first_query.* = false; + } else { + try writer.writeByte('&'); + } + try writeQueryComponent(writer, name); + try writer.writeByte('='); + try writeQueryValue(writer, value); +} + +pub fn requestRaw(client: *Client, method: std.http.Method, url: []const u8, payload: ?[]const u8) !RawResponse { + return requestRawWithContentType(client, method, url, payload, "application/json"); +} + +pub fn requestRawWithContentType(client: *Client, method: std.http.Method, url: []const u8, payload: ?[]const u8, content_type_value: []const u8) !RawResponse { + const allocator = client.allocator; + var headers = std.ArrayList(std.http.Header).empty; + defer headers.deinit(allocator); + const content_type: ?[]const u8 = if (payload != null) content_type_value else null; + const auth_header = try appendClientHeaders(allocator, &headers, client, content_type, "application/json"); + defer if (auth_header) |value| allocator.free(value); + + const uri = try std.Uri.parse(url); + var response_body: std.Io.Writer.Allocating = .init(allocator); + defer response_body.deinit(); + + const result = try client.http.fetch(.{ + .location = .{ .uri = uri }, + .method = method, + .extra_headers = headers.items, + .payload = payload, + .response_writer = &response_body.writer, + }); + + return .{ + .allocator = allocator, + .status = result.status, + .body = try response_body.toOwnedSlice(), + }; +} + +pub fn getRaw(client: *Client, path: []const u8) !RawResponse { + const url = try std.fmt.allocPrint(client.allocator, "{s}{s}", .{ client.base_url, path }); + defer client.allocator.free(url); + return requestRaw(client, .GET, url, null); +} + +pub fn postJsonRaw(client: *Client, path: []const u8, payload: anytype) !RawResponse { + const allocator = client.allocator; + var str: std.Io.Writer.Allocating = .init(allocator); + defer str.deinit(); + try std.json.Stringify.value(payload, .{ .emit_null_optional_fields = false }, &str.writer); + + const url = try std.fmt.allocPrint(allocator, "{s}{s}", .{ client.base_url, path }); + defer allocator.free(url); + return requestRaw(client, .POST, url, str.written()); +} + +pub fn parseRawResponse(comptime T: type, raw: RawResponse) !ApiResult(T) { + if (raw.status.class() != .success) return .{ .api_error = raw }; + const parsed = std.json.parseFromSlice(T, raw.allocator, raw.body, .{ .ignore_unknown_fields = true }) catch |err| { + return .{ .parse_error = .{ .raw = raw, .error_name = @errorName(err) } }; + }; + return .{ .ok = .{ .allocator = raw.allocator, .body = raw.body, .parsed = parsed } }; +} + +pub fn getJsonResult(comptime T: type, client: *Client, path: []const u8) !ApiResult(T) { + return parseRawResponse(T, try getRaw(client, path)); +} + +pub fn postJsonResult(comptime T: type, client: *Client, path: []const u8, payload: anytype) !ApiResult(T) { + return parseRawResponse(T, try postJsonRaw(client, path, payload)); +} + +const max_sse_line_size = 256 * 1024; +const max_sse_event_size = 1024 * 1024; + +pub fn parseSseBytes(allocator: std.mem.Allocator, bytes: []const u8, callback: anytype) !void { + var reader: std.Io.Reader = .fixed(bytes); + try parseSseReader(allocator, &reader, callback); +} + +pub fn parseSseReader(allocator: std.mem.Allocator, reader: *std.Io.Reader, callback: anytype) !void { + var line_buf: std.Io.Writer.Allocating = .init(allocator); + defer line_buf.deinit(); + + var event_data: std.Io.Writer.Allocating = .init(allocator); + defer event_data.deinit(); + + while (true) { + line_buf.clearRetainingCapacity(); + + _ = reader.streamDelimiterLimit(&line_buf.writer, '\n', .limited(max_sse_line_size)) catch |err| switch (err) { + error.StreamTooLong => return error.SseLineTooLong, + error.ReadFailed => return err, + error.WriteFailed => return err, + }; + + const ended_with_delimiter = blk: { + const byte = reader.peekByte() catch |err| switch (err) { + error.EndOfStream => break :blk false, + error.ReadFailed => return err, + }; + if (byte == '\n') { + _ = try reader.takeByte(); + break :blk true; + } + break :blk false; + }; + + if (try processSseLine(&event_data, line_buf.written(), callback)) return; + if (!ended_with_delimiter) break; + } + + _ = try dispatchSseEvent(&event_data, callback); +} + +fn processSseLine(event_data: *std.Io.Writer.Allocating, raw_line: []const u8, callback: anytype) !bool { + const line = std.mem.trimEnd(u8, raw_line, "\r"); + if (line.len == 0) return try dispatchSseEvent(event_data, callback); + if (line[0] == ':') return false; + + const colon = std.mem.indexOfScalar(u8, line, ':') orelse return false; + const field = line[0..colon]; + if (!std.mem.eql(u8, field, "data")) return false; + + var value = line[colon + 1 ..]; + if (value.len > 0 and value[0] == ' ') value = value[1..]; + const separator_len: usize = if (event_data.written().len == 0) 0 else 1; + if (event_data.written().len + separator_len + value.len > max_sse_event_size) return error.SseEventTooLong; + if (separator_len != 0) try event_data.writer.writeByte('\n'); + try event_data.writer.writeAll(value); + return false; +} + +fn dispatchSseEvent(event_data: *std.Io.Writer.Allocating, callback: anytype) !bool { + const data = event_data.written(); + if (data.len == 0) return false; + defer event_data.clearRetainingCapacity(); + + if (std.mem.eql(u8, data, "[DONE]")) return true; + try callback.event(data); + return false; +} + +fn TypedSseCallback(comptime T: type, comptime Callback: type) type { + return struct { + allocator: std.mem.Allocator, + callback: *Callback, + + pub fn event(self: *@This(), data: []const u8) !void { + var parsed = try std.json.parseFromSlice(T, self.allocator, data, .{ .ignore_unknown_fields = true }); + defer parsed.deinit(); + try self.callback.event(&parsed.value); + } + }; +} + +pub fn parseSseBytesTyped(comptime T: type, allocator: std.mem.Allocator, bytes: []const u8, callback: anytype) !void { + const Callback = @TypeOf(callback.*); + var typed_callback: TypedSseCallback(T, Callback) = .{ .allocator = allocator, .callback = callback }; + try parseSseBytes(allocator, bytes, &typed_callback); +} + +pub fn parseSseReaderTyped(comptime T: type, allocator: std.mem.Allocator, reader: *std.Io.Reader, callback: anytype) !void { + const Callback = @TypeOf(callback.*); + var typed_callback: TypedSseCallback(T, Callback) = .{ .allocator = allocator, .callback = callback }; + try parseSseReader(allocator, reader, &typed_callback); +} + +fn stringifyStreamRequest(allocator: std.mem.Allocator, requestBody: anytype) ![]u8 { + var str: std.Io.Writer.Allocating = .init(allocator); + defer str.deinit(); + try std.json.Stringify.value(requestBody, .{ .emit_null_optional_fields = false }, &str.writer); + + var parsed = try std.json.parseFromSlice(std.json.Value, allocator, str.written(), .{ .ignore_unknown_fields = true }); + defer parsed.deinit(); + + if (parsed.value == .object) { + try parsed.value.object.put(parsed.arena.allocator(), "stream", .{ .bool = true }); + } + + var out: std.Io.Writer.Allocating = .init(allocator); + errdefer out.deinit(); + try std.json.Stringify.value(parsed.value, .{ .emit_null_optional_fields = false }, &out.writer); + return try out.toOwnedSlice(); +} + +fn streamJsonTyped(comptime T: type, client: *Client, path: []const u8, requestBody: anytype, callback: anytype) !void { + const Callback = @TypeOf(callback.*); + var typed_callback: TypedSseCallback(T, Callback) = .{ .allocator = client.allocator, .callback = callback }; + try streamJson(client, path, requestBody, &typed_callback); +} + +fn streamJson(client: *Client, path: []const u8, requestBody: anytype, callback: anytype) !void { + const allocator = client.allocator; + const payload = try stringifyStreamRequest(allocator, requestBody); + defer allocator.free(payload); + + var headers = std.ArrayList(std.http.Header).empty; + defer headers.deinit(allocator); + const auth_header = try appendClientHeaders(allocator, &headers, client, "application/json", "text/event-stream"); + defer if (auth_header) |value| allocator.free(value); + + const url = try std.fmt.allocPrint(allocator, "{s}{s}", .{ client.base_url, path }); + defer allocator.free(url); + const uri = try std.Uri.parse(url); + + var req = try client.http.request(.POST, uri, .{ + .redirect_behavior = .unhandled, + .headers = .{ .accept_encoding = .{ .override = "identity" } }, + .extra_headers = headers.items, + }); + defer req.deinit(); + + req.transfer_encoding = .{ .content_length = payload.len }; + var body = try req.sendBodyUnflushed(&.{}); + try body.writer.writeAll(payload); + try body.end(); + try req.connection.?.flush(); + + var response = try req.receiveHead(&.{}); + if (response.head.status.class() != .success) return error.ResponseError; + + var transfer_buffer: [8 * 1024]u8 = undefined; + const reader = response.reader(&transfer_buffer); + parseSseReader(allocator, reader, callback) catch |err| switch (err) { + error.ReadFailed => return response.bodyErr() orelse err, + else => return err, + }; +} + +fn appendClientHeaders(allocator: std.mem.Allocator, headers: *std.ArrayList(std.http.Header), client: *Client, content_type: ?[]const u8, accept: []const u8) !?[]u8 { + if (content_type) |ct| { + try headers.append(allocator, .{ .name = "Content-Type", .value = ct }); + } + try headers.append(allocator, .{ .name = "Accept", .value = accept }); + + var auth_header: ?[]u8 = null; + if (client.api_key.len > 0) { + auth_header = try std.fmt.allocPrint(allocator, "Bearer {s}", .{client.api_key}); + try headers.append(allocator, .{ .name = "Authorization", .value = auth_header.? }); + } + if (client.organization) |organization| { + try headers.append(allocator, .{ .name = "OpenAI-Organization", .value = organization }); + } + if (client.project) |project| { + try headers.append(allocator, .{ .name = "OpenAI-Project", .value = project }); + } + for (client.default_headers) |header| { + try headers.append(allocator, header); + } + return auth_header; +} + +///////////////// +// Summary: +// List your models +// +// Description: +// Get a list of available models on your system, including both LLMs and embedding models. +// +pub fn listModels(client: *Client) !Owned(ListModelsResponse) { + var result = try listModelsResult(client); + switch (result) { + .ok => |ok| return ok, + .api_error => |*err| { + err.deinit(); + return error.ResponseError; + }, + .parse_error => |*err| { + err.raw.deinit(); + return error.ResponseParseError; + }, + } +} + +pub fn listModelsRaw(client: *Client) !RawResponse { + const allocator = client.allocator; + var uri_buf: std.Io.Writer.Allocating = .init(allocator); + defer uri_buf.deinit(); + try uri_buf.writer.print("{s}/api/v1/models", .{client.base_url}); + const payload: ?[]const u8 = null; + + return requestRaw(client, std.http.Method.GET, uri_buf.written(), payload); +} + +pub fn listModelsResult(client: *Client) !ApiResult(ListModelsResponse) { + return parseRawResponse(ListModelsResponse, try listModelsRaw(client)); +} + +///////////////// +// Summary: +// Download a model +// +// Description: +// Download LLMs and embedding models. +// +pub fn downloadModel(client: *Client, requestBody: DownloadModelRequest) !Owned(DownloadModelResponse) { + var result = try downloadModelResult(client, requestBody); + switch (result) { + .ok => |ok| return ok, + .api_error => |*err| { + err.deinit(); + return error.ResponseError; + }, + .parse_error => |*err| { + err.raw.deinit(); + return error.ResponseParseError; + }, + } +} + +pub fn downloadModelRaw(client: *Client, requestBody: DownloadModelRequest) !RawResponse { + const allocator = client.allocator; + var uri_buf: std.Io.Writer.Allocating = .init(allocator); + defer uri_buf.deinit(); + try uri_buf.writer.print("{s}/api/v1/models/download", .{client.base_url}); + + var str: std.Io.Writer.Allocating = .init(allocator); + defer str.deinit(); + try std.json.Stringify.value(requestBody, .{ .emit_null_optional_fields = false }, &str.writer); + const payload: ?[]const u8 = str.written(); + + return requestRaw(client, std.http.Method.POST, uri_buf.written(), payload); +} + +pub fn downloadModelResult(client: *Client, requestBody: DownloadModelRequest) !ApiResult(DownloadModelResponse) { + return parseRawResponse(DownloadModelResponse, try downloadModelRaw(client, requestBody)); +} + +///////////////// +// Summary: +// Get download status +// +// Description: +// Get the status of model downloads. +// +pub fn getDownloadStatus(client: *Client, job_id: []const u8) !Owned(DownloadStatusResponse) { + var result = try getDownloadStatusResult(client, job_id); + switch (result) { + .ok => |ok| return ok, + .api_error => |*err| { + err.deinit(); + return error.ResponseError; + }, + .parse_error => |*err| { + err.raw.deinit(); + return error.ResponseParseError; + }, + } +} + +pub fn getDownloadStatusRaw(client: *Client, job_id: []const u8) !RawResponse { + const allocator = client.allocator; + var uri_buf: std.Io.Writer.Allocating = .init(allocator); + defer uri_buf.deinit(); + try uri_buf.writer.print("{s}/api/v1/models/download/status/{s}", .{client.base_url, job_id}); + const payload: ?[]const u8 = null; + + return requestRaw(client, std.http.Method.GET, uri_buf.written(), payload); +} + +pub fn getDownloadStatusResult(client: *Client, job_id: []const u8) !ApiResult(DownloadStatusResponse) { + return parseRawResponse(DownloadStatusResponse, try getDownloadStatusRaw(client, job_id)); +} + +///////////////// +// Summary: +// Unload a model +// +// Description: +// Unload a loaded model from memory. +// +pub fn unloadModel(client: *Client, requestBody: UnloadModelRequest) !Owned(UnloadModelResponse) { + var result = try unloadModelResult(client, requestBody); + switch (result) { + .ok => |ok| return ok, + .api_error => |*err| { + err.deinit(); + return error.ResponseError; + }, + .parse_error => |*err| { + err.raw.deinit(); + return error.ResponseParseError; + }, + } +} + +pub fn unloadModelRaw(client: *Client, requestBody: UnloadModelRequest) !RawResponse { + const allocator = client.allocator; + var uri_buf: std.Io.Writer.Allocating = .init(allocator); + defer uri_buf.deinit(); + try uri_buf.writer.print("{s}/api/v1/models/unload", .{client.base_url}); + + var str: std.Io.Writer.Allocating = .init(allocator); + defer str.deinit(); + try std.json.Stringify.value(requestBody, .{ .emit_null_optional_fields = false }, &str.writer); + const payload: ?[]const u8 = str.written(); + + return requestRaw(client, std.http.Method.POST, uri_buf.written(), payload); +} + +pub fn unloadModelResult(client: *Client, requestBody: UnloadModelRequest) !ApiResult(UnloadModelResponse) { + return parseRawResponse(UnloadModelResponse, try unloadModelRaw(client, requestBody)); +} + +///////////////// +// Summary: +// Load a model +// +// Description: +// Load an LLM or Embedding model into memory with custom configuration for inference. +// +pub fn loadModel(client: *Client, requestBody: LoadModelRequest) !Owned(LoadModelResponse) { + var result = try loadModelResult(client, requestBody); + switch (result) { + .ok => |ok| return ok, + .api_error => |*err| { + err.deinit(); + return error.ResponseError; + }, + .parse_error => |*err| { + err.raw.deinit(); + return error.ResponseParseError; + }, + } +} + +pub fn loadModelRaw(client: *Client, requestBody: LoadModelRequest) !RawResponse { + const allocator = client.allocator; + var uri_buf: std.Io.Writer.Allocating = .init(allocator); + defer uri_buf.deinit(); + try uri_buf.writer.print("{s}/api/v1/models/load", .{client.base_url}); + + var str: std.Io.Writer.Allocating = .init(allocator); + defer str.deinit(); + try std.json.Stringify.value(requestBody, .{ .emit_null_optional_fields = false }, &str.writer); + const payload: ?[]const u8 = str.written(); + + return requestRaw(client, std.http.Method.POST, uri_buf.written(), payload); +} + +pub fn loadModelResult(client: *Client, requestBody: LoadModelRequest) !ApiResult(LoadModelResponse) { + return parseRawResponse(LoadModelResponse, try loadModelRaw(client, requestBody)); +} + +///////////////// +// Summary: +// Chat with a model +// +// Description: +// Send a message to a model and receive a response. Supports MCP integration. +// +pub fn chat(client: *Client, requestBody: ChatRequest) !Owned(ChatResponse) { + var result = try chatResult(client, requestBody); + switch (result) { + .ok => |ok| return ok, + .api_error => |*err| { + err.deinit(); + return error.ResponseError; + }, + .parse_error => |*err| { + err.raw.deinit(); + return error.ResponseParseError; + }, + } +} + +pub fn chatRaw(client: *Client, requestBody: ChatRequest) !RawResponse { + const allocator = client.allocator; + var uri_buf: std.Io.Writer.Allocating = .init(allocator); + defer uri_buf.deinit(); + try uri_buf.writer.print("{s}/api/v1/chat", .{client.base_url}); + + var str: std.Io.Writer.Allocating = .init(allocator); + defer str.deinit(); + try std.json.Stringify.value(requestBody, .{ .emit_null_optional_fields = false }, &str.writer); + const payload: ?[]const u8 = str.written(); + + return requestRaw(client, std.http.Method.POST, uri_buf.written(), payload); +} + +pub fn chatResult(client: *Client, requestBody: ChatRequest) !ApiResult(ChatResponse) { + return parseRawResponse(ChatResponse, try chatRaw(client, requestBody)); +} + +const _chat = chat; +const _chatResult = chatResult; + +pub const resources = struct { + pub const api = struct { + pub const chat = struct { + pub fn chat_(client: *Client, requestBody: ChatRequest) !Owned(ChatResponse) { + return _chat(client, requestBody); + } + pub fn chat_Result(client: *Client, requestBody: ChatRequest) !ApiResult(ChatResponse) { + return _chatResult(client, requestBody); + } + }; + pub const models = struct { + pub fn list(client: *Client) !Owned(ListModelsResponse) { + return listModels(client); + } + pub fn listResult(client: *Client) !ApiResult(ListModelsResponse) { + return listModelsResult(client); + } + pub const download = struct { + pub fn downloadmodel(client: *Client, requestBody: DownloadModelRequest) !Owned(DownloadModelResponse) { + return downloadModel(client, requestBody); + } + pub fn downloadmodelResult(client: *Client, requestBody: DownloadModelRequest) !ApiResult(DownloadModelResponse) { + return downloadModelResult(client, requestBody); + } + pub const status = struct { + pub fn get(client: *Client, job_id: []const u8) !Owned(DownloadStatusResponse) { + return getDownloadStatus(client, job_id); + } + pub fn getResult(client: *Client, job_id: []const u8) !ApiResult(DownloadStatusResponse) { + return getDownloadStatusResult(client, job_id); + } + }; + }; + pub const load = struct { + pub fn loadmodel(client: *Client, requestBody: LoadModelRequest) !Owned(LoadModelResponse) { + return loadModel(client, requestBody); + } + pub fn loadmodelResult(client: *Client, requestBody: LoadModelRequest) !ApiResult(LoadModelResponse) { + return loadModelResult(client, requestBody); + } + }; + pub const unload = struct { + pub fn unloadmodel(client: *Client, requestBody: UnloadModelRequest) !Owned(UnloadModelResponse) { + return unloadModel(client, requestBody); + } + pub fn unloadmodelResult(client: *Client, requestBody: UnloadModelRequest) !ApiResult(UnloadModelResponse) { + return unloadModelResult(client, requestBody); + } + }; + }; + }; +}; + +pub const api = resources.api; + diff --git a/generated/main.zig b/generated/main.zig index bd189e4..f0a2b4e 100644 --- a/generated/main.zig +++ b/generated/main.zig @@ -4,6 +4,7 @@ const v2_yaml = @import("generated_v2_yaml.zig"); const v3 = @import("generated_v3.zig"); const v3_yaml = @import("generated_v3_yaml.zig"); const v31_yaml = @import("generated_v31_yaml.zig"); +const lmstudio = @import("lmstudio.zig"); pub fn main(init: std.process.Init) !void { const allocator = init.gpa; diff --git a/openapi/v3.0/lmstudio.json b/openapi/v3.0/lmstudio.json new file mode 100644 index 0000000..2a49d1a --- /dev/null +++ b/openapi/v3.0/lmstudio.json @@ -0,0 +1,1169 @@ +{ + "openapi": "3.0.3", + "info": { + "title": "LM Studio v1 REST API", + "description": "LM Studio offers a powerful REST API with first-class support for local inference and model management.", + "version": "1.0.0", + "x-logo": { + "url": "https://lmstudio.ai/assets/marketing/lmstudio-app-logo.webp" + } + }, + "servers": [ + { + "url": "http://localhost:1234", + "description": "Local LM Studio server" + } + ], + "security": [ + { + "bearerAuth": [] + } + ], + "paths": { + "/api/v1/chat": { + "post": { + "operationId": "chat", + "summary": "Chat with a model", + "description": "Send a message to a model and receive a response. Supports MCP integration.", + "tags": [ + "Chat" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ChatRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Successful chat response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ChatResponse" + } + } + } + } + } + } + }, + "/api/v1/models": { + "get": { + "operationId": "listModels", + "summary": "List your models", + "description": "Get a list of available models on your system, including both LLMs and embedding models.", + "tags": [ + "Models" + ], + "responses": { + "200": { + "description": "List of available models", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ListModelsResponse" + } + } + } + } + } + } + }, + "/api/v1/models/load": { + "post": { + "operationId": "loadModel", + "summary": "Load a model", + "description": "Load an LLM or Embedding model into memory with custom configuration for inference.", + "tags": [ + "Models" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LoadModelRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Model loaded successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LoadModelResponse" + } + } + } + } + } + } + }, + "/api/v1/models/unload": { + "post": { + "operationId": "unloadModel", + "summary": "Unload a model", + "description": "Unload a loaded model from memory.", + "tags": [ + "Models" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnloadModelRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Model unloaded successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnloadModelResponse" + } + } + } + } + } + } + }, + "/api/v1/models/download": { + "post": { + "operationId": "downloadModel", + "summary": "Download a model", + "description": "Download LLMs and embedding models.", + "tags": [ + "Models" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DownloadModelRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Download job started or model already downloaded", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DownloadModelResponse" + } + } + } + } + } + } + }, + "/api/v1/models/download/status/{job_id}": { + "get": { + "operationId": "getDownloadStatus", + "summary": "Get download status", + "description": "Get the status of model downloads.", + "tags": [ + "Models" + ], + "parameters": [ + { + "name": "job_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "The unique identifier of the download job" + } + ], + "responses": { + "200": { + "description": "Download job status", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DownloadStatusResponse" + } + } + } + } + } + } + } + }, + "components": { + "securitySchemes": { + "bearerAuth": { + "type": "http", + "scheme": "bearer", + "bearerFormat": "JWT", + "description": "API token authentication. Tokens are created in LM Studio's Developer > Server Settings." + } + }, + "schemas": { + "ChatRequest": { + "type": "object", + "required": [ + "model", + "input" + ], + "properties": { + "model": { + "type": "string", + "description": "Unique identifier for the model to use" + }, + "input": { + "oneOf": [ + { + "type": "string", + "description": "Text content of the message" + }, + { + "type": "array", + "items": { + "$ref": "#/components/schemas/ChatInputItem" + } + } + ], + "description": "Message to send to the model" + }, + "system_prompt": { + "type": "string", + "description": "System message that sets model behavior or instructions" + }, + "integrations": { + "type": "array", + "description": "List of integrations (plugins, ephemeral MCP servers, etc.) to enable for this request", + "items": { + "oneOf": [ + { + "type": "string", + "description": "Unique identifier of a plugin to use" + }, + { + "$ref": "#/components/schemas/PluginIntegration" + }, + { + "$ref": "#/components/schemas/EphemeralMcpIntegration" + } + ] + } + }, + "stream": { + "type": "boolean", + "description": "Whether to stream partial outputs via SSE", + "default": false + }, + "temperature": { + "type": "number", + "description": "Randomness in token selection. 0 is deterministic, higher values increase creativity", + "minimum": 0, + "maximum": 1 + }, + "top_p": { + "type": "number", + "description": "Minimum cumulative probability for the possible next tokens", + "minimum": 0, + "maximum": 1 + }, + "top_k": { + "type": "integer", + "description": "Limits next token selection to top-k most probable tokens" + }, + "min_p": { + "type": "number", + "description": "Minimum base probability for a token to be selected for output", + "minimum": 0, + "maximum": 1 + }, + "repeat_penalty": { + "type": "number", + "description": "Penalty for repeating token sequences. 1 is no penalty, higher values discourage repetition" + }, + "max_output_tokens": { + "type": "integer", + "description": "Maximum number of tokens to generate" + }, + "reasoning": { + "type": "string", + "description": "Reasoning setting. Will error if the model being used does not support the reasoning setting", + "enum": [ + "off", + "low", + "medium", + "high", + "on" + ] + }, + "context_length": { + "type": "integer", + "description": "Number of tokens to consider as context. Higher values recommended for MCP usage" + }, + "store": { + "type": "boolean", + "description": "Whether to store the chat. If set, response will return a response_id field", + "default": true + }, + "previous_response_id": { + "type": "string", + "description": "Identifier of existing response to append to. Must start with 'resp_'", + "pattern": "^resp_" + } + } + }, + "ChatInputItem": { + "type": "object", + "required": [ + "type" + ], + "oneOf": [ + { + "$ref": "#/components/schemas/TextInput" + }, + { + "$ref": "#/components/schemas/ImageInput" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "message": "#/components/schemas/TextInput", + "image": "#/components/schemas/ImageInput" + } + } + }, + "TextInput": { + "type": "object", + "required": [ + "type", + "content" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "message" + ], + "description": "Type of input item" + }, + "content": { + "type": "string", + "description": "Text content of the message" + } + } + }, + "ImageInput": { + "type": "object", + "required": [ + "type", + "data_url" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "image" + ], + "description": "Type of input item" + }, + "data_url": { + "type": "string", + "description": "Image data as a base64-encoded data URL", + "format": "uri" + } + } + }, + "PluginIntegration": { + "type": "object", + "required": [ + "type", + "id" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "plugin" + ], + "description": "Type of integration" + }, + "id": { + "type": "string", + "description": "Unique identifier of the plugin" + }, + "allowed_tools": { + "type": "array", + "description": "List of tool names the model can call from this plugin", + "items": { + "type": "string" + } + } + } + }, + "EphemeralMcpIntegration": { + "type": "object", + "required": [ + "type", + "server_label", + "server_url" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "ephemeral_mcp" + ], + "description": "Type of integration" + }, + "server_label": { + "type": "string", + "description": "Label to identify the MCP server" + }, + "server_url": { + "type": "string", + "description": "URL of the MCP server", + "format": "uri" + }, + "allowed_tools": { + "type": "array", + "description": "List of tool names the model can call from this server", + "items": { + "type": "string" + } + }, + "headers": { + "type": "object", + "description": "Custom HTTP headers to send with requests to the server", + "additionalProperties": { + "type": "string" + } + } + } + }, + "ChatResponse": { + "type": "object", + "required": [ + "model_instance_id", + "output", + "stats" + ], + "properties": { + "model_instance_id": { + "type": "string", + "description": "Unique identifier for the loaded model instance that generated the response" + }, + "output": { + "type": "array", + "description": "Array of output items generated", + "items": { + "$ref": "#/components/schemas/ChatOutputItem" + } + }, + "stats": { + "$ref": "#/components/schemas/ChatStats" + }, + "response_id": { + "type": "string", + "description": "Identifier of the response for subsequent requests. Starts with 'resp_'", + "pattern": "^resp_" + } + } + }, + "ChatOutputItem": { + "type": "object", + "required": [ + "type" + ], + "oneOf": [ + { + "$ref": "#/components/schemas/MessageOutput" + }, + { + "$ref": "#/components/schemas/ToolCallOutput" + }, + { + "$ref": "#/components/schemas/ReasoningOutput" + }, + { + "$ref": "#/components/schemas/InvalidToolCallOutput" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "message": "#/components/schemas/MessageOutput", + "tool_call": "#/components/schemas/ToolCallOutput", + "reasoning": "#/components/schemas/ReasoningOutput", + "invalid_tool_call": "#/components/schemas/InvalidToolCallOutput" + } + } + }, + "MessageOutput": { + "type": "object", + "required": [ + "type", + "content" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "message" + ], + "description": "Type of output item" + }, + "content": { + "type": "string", + "description": "Text content of the message" + } + } + }, + "ToolCallOutput": { + "type": "object", + "required": [ + "type", + "tool", + "arguments", + "output", + "provider_info" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "tool_call" + ], + "description": "Type of output item" + }, + "tool": { + "type": "string", + "description": "Name of the tool called" + }, + "arguments": { + "type": "object", + "description": "Arguments passed to the tool", + "additionalProperties": true + }, + "output": { + "type": "string", + "description": "Result returned from the tool" + }, + "provider_info": { + "$ref": "#/components/schemas/ProviderInfo" + } + } + }, + "ReasoningOutput": { + "type": "object", + "required": [ + "type", + "content" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "reasoning" + ], + "description": "Type of output item" + }, + "content": { + "type": "string", + "description": "Text content of the reasoning" + } + } + }, + "InvalidToolCallOutput": { + "type": "object", + "required": [ + "type", + "reason", + "metadata" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "invalid_tool_call" + ], + "description": "Type of output item" + }, + "reason": { + "type": "string", + "description": "Reason why the tool call was invalid" + }, + "metadata": { + "type": "object", + "required": [ + "type", + "tool_name" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "invalid_name", + "invalid_arguments" + ], + "description": "Type of error that occurred" + }, + "tool_name": { + "type": "string", + "description": "Name of the tool that was attempted to be called" + }, + "arguments": { + "type": "object", + "description": "Arguments that were passed to the tool (only present for invalid_arguments errors)", + "additionalProperties": true + }, + "provider_info": { + "description": "Information about the tool provider (only present for invalid_arguments errors)", + "$ref": "#/components/schemas/ProviderInfo" + } + } + } + } + }, + "ProviderInfo": { + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "plugin", + "ephemeral_mcp" + ], + "description": "Provider type" + }, + "plugin_id": { + "type": "string", + "description": "Identifier of the plugin (when type is plugin)" + }, + "server_label": { + "type": "string", + "description": "Label of the MCP server (when type is ephemeral_mcp)" + } + } + }, + "ChatStats": { + "type": "object", + "required": [ + "input_tokens", + "total_output_tokens", + "reasoning_output_tokens", + "tokens_per_second", + "time_to_first_token_seconds" + ], + "properties": { + "input_tokens": { + "type": "integer", + "description": "Number of input tokens" + }, + "total_output_tokens": { + "type": "integer", + "description": "Total number of output tokens generated" + }, + "reasoning_output_tokens": { + "type": "integer", + "description": "Number of tokens used for reasoning" + }, + "tokens_per_second": { + "type": "number", + "description": "Generation speed in tokens per second" + }, + "time_to_first_token_seconds": { + "type": "number", + "description": "Time in seconds to generate the first token" + }, + "model_load_time_seconds": { + "type": "number", + "description": "Time taken to load the model for this request in seconds. Present only if the model was not already loaded" + } + } + }, + "ListModelsResponse": { + "type": "object", + "required": [ + "models" + ], + "properties": { + "models": { + "type": "array", + "description": "List of available models (both LLMs and embedding models)", + "items": { + "$ref": "#/components/schemas/ModelInfo" + } + } + } + }, + "ModelInfo": { + "type": "object", + "required": [ + "type", + "publisher", + "key", + "display_name", + "size_bytes", + "loaded_instances", + "max_context_length", + "format" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "llm", + "embedding" + ], + "description": "Type of model" + }, + "publisher": { + "type": "string", + "description": "Model publisher name" + }, + "key": { + "type": "string", + "description": "Unique identifier for the model" + }, + "display_name": { + "type": "string", + "description": "Human-readable model name" + }, + "architecture": { + "type": "string", + "nullable": true, + "description": "Model architecture (e.g., llama, mistral). Absent for embedding models" + }, + "quantization": { + "type": "object", + "nullable": true, + "required": [ + "name", + "bits_per_weight" + ], + "properties": { + "name": { + "type": "string", + "nullable": true, + "description": "Quantization method name" + }, + "bits_per_weight": { + "type": "number", + "nullable": true, + "description": "Bits per weight for the quantization" + } + } + }, + "size_bytes": { + "type": "integer", + "description": "Size of the model in bytes" + }, + "params_string": { + "type": "string", + "nullable": true, + "description": "Human-readable parameter count (e.g., 7B, 13B)" + }, + "loaded_instances": { + "type": "array", + "description": "List of currently loaded instances of this model", + "items": { + "$ref": "#/components/schemas/LoadedInstance" + } + }, + "max_context_length": { + "type": "integer", + "description": "Maximum context length supported by the model in number of tokens" + }, + "format": { + "type": "string", + "nullable": true, + "enum": [ + "gguf", + "mlx", + null + ], + "description": "Model file format" + }, + "capabilities": { + "type": "object", + "description": "Model capabilities. Absent for embedding models", + "properties": { + "vision": { + "type": "boolean", + "description": "Whether the model supports vision/image inputs" + }, + "trained_for_tool_use": { + "type": "boolean", + "description": "Whether the model was trained for tool/function calling" + }, + "reasoning": { + "type": "object", + "description": "Public reasoning configuration for the model. Absent when no reasoning config is exposed", + "required": [ + "allowed_options", + "default" + ], + "properties": { + "allowed_options": { + "type": "array", + "description": "Allowed public reasoning settings for the model", + "items": { + "type": "string", + "enum": [ + "off", + "on", + "low", + "medium", + "high" + ] + } + }, + "default": { + "type": "string", + "enum": [ + "off", + "on", + "low", + "medium", + "high" + ], + "description": "Default public reasoning setting for the model" + } + } + } + } + }, + "description": { + "type": "string", + "nullable": true, + "description": "Model description. Absent for embedding models" + }, + "variants": { + "type": "array", + "description": "List of available quantization variant names for this model. Present for multi-variant models", + "items": { + "type": "string" + } + }, + "selected_variant": { + "type": "string", + "description": "The currently selected variant name. Present when variants is present" + } + } + }, + "LoadedInstance": { + "type": "object", + "required": [ + "id", + "config" + ], + "properties": { + "id": { + "type": "string", + "description": "Unique identifier for the loaded model instance" + }, + "config": { + "$ref": "#/components/schemas/LoadedInstanceConfig" + } + } + }, + "LoadedInstanceConfig": { + "type": "object", + "required": [ + "context_length" + ], + "properties": { + "context_length": { + "type": "integer", + "description": "The maximum context length for the model in number of tokens" + }, + "eval_batch_size": { + "type": "integer", + "description": "Number of input tokens to process together in a single batch during evaluation. Absent for embedding models" + }, + "parallel": { + "type": "integer", + "description": "Maximum number of parallel predictions the instance can handle. Absent for embedding models" + }, + "flash_attention": { + "type": "boolean", + "description": "Whether Flash Attention is enabled. Absent for embedding models" + }, + "num_experts": { + "type": "integer", + "description": "Number of experts for MoE (Mixture of Experts) models. Absent for embedding models" + }, + "offload_kv_cache_to_gpu": { + "type": "boolean", + "description": "Whether KV cache is offloaded to GPU memory. Absent for embedding models" + } + } + }, + "LoadModelRequest": { + "type": "object", + "required": [ + "model" + ], + "properties": { + "model": { + "type": "string", + "description": "Unique identifier for the model to load. Can be an LLM or embedding model" + }, + "context_length": { + "type": "integer", + "description": "Maximum number of tokens that the model will consider" + }, + "eval_batch_size": { + "type": "integer", + "description": "Number of input tokens to process together in a single batch during evaluation" + }, + "flash_attention": { + "type": "boolean", + "description": "Whether to optimize attention computation" + }, + "num_experts": { + "type": "integer", + "description": "Number of experts to use during inference for MoE models" + }, + "offload_kv_cache_to_gpu": { + "type": "boolean", + "description": "Whether KV cache is offloaded to GPU memory" + }, + "echo_load_config": { + "type": "boolean", + "description": "If true, echoes the final load configuration in the response under load_config", + "default": false + } + } + }, + "LoadModelResponse": { + "type": "object", + "required": [ + "type", + "instance_id", + "load_time_seconds", + "status" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "llm", + "embedding" + ], + "description": "Type of the loaded model" + }, + "instance_id": { + "type": "string", + "description": "Unique identifier for the loaded model instance" + }, + "load_time_seconds": { + "type": "number", + "description": "Time taken to load the model in seconds" + }, + "status": { + "type": "string", + "enum": [ + "loaded" + ], + "description": "Load status" + }, + "load_config": { + "description": "The final configuration applied to the loaded model. Included only when echo_load_config is true", + "oneOf": [ + { + "$ref": "#/components/schemas/LlmLoadConfig" + }, + { + "$ref": "#/components/schemas/EmbeddingLoadConfig" + } + ] + } + } + }, + "LlmLoadConfig": { + "type": "object", + "required": [ + "context_length" + ], + "properties": { + "context_length": { + "type": "integer", + "description": "Maximum number of tokens that the model will consider" + }, + "eval_batch_size": { + "type": "integer", + "description": "Number of input tokens to process together in a single batch during evaluation" + }, + "flash_attention": { + "type": "boolean", + "description": "Whether Flash Attention is enabled for optimized attention computation" + }, + "num_experts": { + "type": "integer", + "description": "Number of experts for MoE models" + }, + "offload_kv_cache_to_gpu": { + "type": "boolean", + "description": "Whether KV cache is offloaded to GPU memory" + } + } + }, + "EmbeddingLoadConfig": { + "type": "object", + "required": [ + "context_length" + ], + "properties": { + "context_length": { + "type": "integer", + "description": "Maximum number of tokens that the model will consider" + } + } + }, + "UnloadModelRequest": { + "type": "object", + "required": [ + "instance_id" + ], + "properties": { + "instance_id": { + "type": "string", + "description": "Unique identifier of the model instance to unload" + } + } + }, + "UnloadModelResponse": { + "type": "object", + "required": [ + "instance_id" + ], + "properties": { + "instance_id": { + "type": "string", + "description": "Unique identifier for the unloaded model instance" + } + } + }, + "DownloadModelRequest": { + "type": "object", + "required": [ + "model" + ], + "properties": { + "model": { + "type": "string", + "description": "The model to download. Accepts model catalog identifiers (e.g., openai/gpt-oss-20b) and exact Hugging Face links" + }, + "quantization": { + "type": "string", + "description": "Quantization level of the model to download (e.g., Q4_K_M). Only supported for Hugging Face links" + } + } + }, + "DownloadModelResponse": { + "type": "object", + "required": [ + "status" + ], + "properties": { + "job_id": { + "type": "string", + "description": "Unique identifier for the download job. Absent when status is already_downloaded" + }, + "status": { + "type": "string", + "enum": [ + "downloading", + "paused", + "completed", + "failed", + "already_downloaded" + ], + "description": "Current status of the download" + }, + "completed_at": { + "type": "string", + "format": "date-time", + "description": "Download completion time in ISO 8601 format. Present when status is completed" + }, + "total_size_bytes": { + "type": "integer", + "description": "Total size of the download in bytes. Absent when status is already_downloaded" + }, + "started_at": { + "type": "string", + "format": "date-time", + "description": "Download start time in ISO 8601 format. Absent when status is already_downloaded" + } + } + }, + "DownloadStatusResponse": { + "type": "object", + "required": [ + "job_id", + "status" + ], + "properties": { + "job_id": { + "type": "string", + "description": "Unique identifier for the download job" + }, + "status": { + "type": "string", + "enum": [ + "downloading", + "paused", + "completed", + "failed" + ], + "description": "Current status of the download" + }, + "bytes_per_second": { + "type": "integer", + "description": "Current download speed in bytes per second. Present when status is downloading" + }, + "estimated_completion": { + "type": "string", + "format": "date-time", + "description": "Estimated completion time in ISO 8601 format. Present when status is downloading" + }, + "completed_at": { + "type": "string", + "format": "date-time", + "description": "Download completion time in ISO 8601 format. Present when status is completed" + }, + "total_size_bytes": { + "type": "integer", + "description": "Total size of the download in bytes" + }, + "downloaded_bytes": { + "type": "integer", + "description": "Number of bytes downloaded so far" + }, + "started_at": { + "type": "string", + "format": "date-time", + "description": "Download start time in ISO 8601 format" + } + } + } + } + } +} diff --git a/src/generators/unified/api_generator.zig b/src/generators/unified/api_generator.zig index 57e23a2..c357142 100644 --- a/src/generators/unified/api_generator.zig +++ b/src/generators/unified/api_generator.zig @@ -66,6 +66,7 @@ const ResourceWrapper = struct { path: []const u8, operation: Operation, collides: bool = false, + needs_alias: bool = false, }; fn operationRefLessThan(_: void, lhs: OperationRef, rhs: OperationRef) bool { @@ -909,6 +910,30 @@ pub const UnifiedApiGenerator = struct { std.mem.sort(ResourceWrapper, wrappers.items, {}, resourceWrapperLessThan); + // Detect when wrapper method name matches its containing struct name + for (wrappers.items) |*wrapper| { + const last_segment = wrapper.segments[wrapper.segments.len - 1]; + if (std.mem.eql(u8, wrapper.method_name, last_segment)) { + wrapper.collides = true; + wrapper.needs_alias = true; + } + } + + // Generate aliases for operations whose wrapper needs them + for (wrappers.items) |wrapper| { + if (wrapper.needs_alias) { + const alias_line = try std.fmt.allocPrint(self.allocator, "const _{s} = {s};\n", .{ wrapper.operation_id, wrapper.operation_id }); + defer self.allocator.free(alias_line); + try self.buffer.appendSlice(self.allocator, alias_line); + if (self.hasReturnValue(wrapper.method, wrapper.operation)) { + const result_line = try std.fmt.allocPrint(self.allocator, "const _{s}Result = {s}Result;\n", .{ wrapper.operation_id, wrapper.operation_id }); + defer self.allocator.free(result_line); + try self.buffer.appendSlice(self.allocator, result_line); + } + } + } + if (wrappers.items.len > 0) try self.buffer.appendSlice(self.allocator, "\n"); + try self.buffer.appendSlice(self.allocator, "pub const resources = struct {\n"); try self.generateResourceLevel(wrappers.items, 0, 1, &.{}); try self.buffer.appendSlice(self.allocator, "};\n\n"); @@ -951,6 +976,14 @@ pub const UnifiedApiGenerator = struct { } for (children.items) |child| try declarations.append(self.allocator, child); + + // Detect wrapper method name collision with ancestor/child struct names + for (wrappers) |*wrapper_at_depth| { + if (wrapper_at_depth.segments.len == depth and containsString(declarations.items, wrapper_at_depth.method_name)) { + wrapper_at_depth.collides = true; + } + } + for (wrappers) |wrapper| { if (wrapper.segments.len == depth) { const name = try self.resourceWrapperNameAlloc(wrapper); @@ -1010,6 +1043,7 @@ pub const UnifiedApiGenerator = struct { try self.buffer.appendSlice(self.allocator, " {\n"); try self.appendIndent(indent + 1); try self.buffer.appendSlice(self.allocator, "return "); + if (wrapper.needs_alias) try self.buffer.appendSlice(self.allocator, "_"); try self.appendIdentifier(wrapper.operation_id); try self.appendWrapperCallArguments(wrapper.operation, forbidden_names); try self.buffer.appendSlice(self.allocator, ";\n"); @@ -1032,6 +1066,7 @@ pub const UnifiedApiGenerator = struct { try self.buffer.appendSlice(self.allocator, " {\n"); try self.appendIndent(indent + 1); try self.buffer.appendSlice(self.allocator, "return "); + if (wrapper.needs_alias) try self.buffer.appendSlice(self.allocator, "_"); try self.appendIdentifier(operation_result_name); try self.appendWrapperCallArguments(wrapper.operation, forbidden_names); try self.buffer.appendSlice(self.allocator, ";\n"); @@ -1178,12 +1213,19 @@ pub const UnifiedApiGenerator = struct { while (path_iterator.next()) |entry| { const path_item = entry.value_ptr.*; if (operationHasParameterNamed(self, path_item.get, alias)) return true; + if (operationDeclaresTopLevelName(self, path_item.get, "GET", alias)) return true; if (operationHasParameterNamed(self, path_item.post, alias)) return true; + if (operationDeclaresTopLevelName(self, path_item.post, "POST", alias)) return true; if (operationHasParameterNamed(self, path_item.put, alias)) return true; + if (operationDeclaresTopLevelName(self, path_item.put, "PUT", alias)) return true; if (operationHasParameterNamed(self, path_item.delete, alias)) return true; + if (operationDeclaresTopLevelName(self, path_item.delete, "DELETE", alias)) return true; if (operationHasParameterNamed(self, path_item.patch, alias)) return true; + if (operationDeclaresTopLevelName(self, path_item.patch, "PATCH", alias)) return true; if (operationHasParameterNamed(self, path_item.head, alias)) return true; + if (operationDeclaresTopLevelName(self, path_item.head, "HEAD", alias)) return true; if (operationHasParameterNamed(self, path_item.options, alias)) return true; + if (operationDeclaresTopLevelName(self, path_item.options, "OPTIONS", alias)) return true; } return false; } @@ -1201,6 +1243,32 @@ pub const UnifiedApiGenerator = struct { return false; } + fn operationDeclaresTopLevelName(self: *UnifiedApiGenerator, maybe_operation: ?Operation, method: []const u8, name: []const u8) bool { + const operation = maybe_operation orelse return false; + const operation_id = operation.operationId orelse return false; + if (std.mem.eql(u8, operation_id, name)) return true; + + const raw_name = std.fmt.allocPrint(self.allocator, "{s}Raw", .{operation_id}) catch return true; + defer self.allocator.free(raw_name); + if (std.mem.eql(u8, raw_name, name)) return true; + + if (self.hasReturnValue(method, operation)) { + const result_name = std.fmt.allocPrint(self.allocator, "{s}Result", .{operation_id}) catch return true; + defer self.allocator.free(result_name); + if (std.mem.eql(u8, result_name, name)) return true; + } + + if (self.streamFunctionName(operation_id)) |stream_name| { + if (std.mem.eql(u8, stream_name, name)) return true; + + const events_name = std.fmt.allocPrint(self.allocator, "{s}Events", .{stream_name}) catch return true; + defer self.allocator.free(events_name); + if (std.mem.eql(u8, events_name, name)) return true; + } + + return false; + } + fn resourceSegments(self: *UnifiedApiGenerator, path: []const u8, operation: Operation) ![][]const u8 { return switch (self.args.resource_wrappers) { .none => self.allocator.alloc([]const u8, 0), diff --git a/src/tests/resource_wrapper_tests.zig b/src/tests/resource_wrapper_tests.zig index 84fb8c3..0bd0619 100644 --- a/src/tests/resource_wrapper_tests.zig +++ b/src/tests/resource_wrapper_tests.zig @@ -13,9 +13,29 @@ fn responseMap(allocator: std.mem.Allocator, with_schema: bool) !std.StringHashM return responses; } +fn dupTags(allocator: std.mem.Allocator, tags: []const []const u8) ![][]const u8 { + const out = try allocator.alloc([]const u8, tags.len); + for (tags, 0..) |tag, i| out[i] = tag; + return out; +} + fn op(allocator: std.mem.Allocator, operation_id: []const u8, method: []const u8, has_body: bool, has_path_param: bool, has_response: bool) !common.Operation { + return opWithTags(allocator, operation_id, method, has_body, has_path_param, has_response, null); +} + +fn opWithTags( + allocator: std.mem.Allocator, + operation_id: []const u8, + method: []const u8, + has_body: bool, + has_path_param: bool, + has_response: bool, + tags: ?[]const []const u8, +) !common.Operation { var params = std.ArrayList(common.Parameter).empty; errdefer params.deinit(allocator); + const owned_tags = if (tags) |provided_tags| try dupTags(allocator, provided_tags) else null; + errdefer if (owned_tags) |value| allocator.free(value); if (has_path_param) { try params.append(allocator, .{ @@ -36,6 +56,7 @@ fn op(allocator: std.mem.Allocator, operation_id: []const u8, method: []const u8 _ = method; return .{ + .tags = owned_tags, .operationId = operation_id, .parameters = if (params.items.len == 0) null else try params.toOwnedSlice(allocator), .responses = try responseMap(allocator, has_response), @@ -65,6 +86,21 @@ fn buildFixture(allocator: std.mem.Allocator) !common.UnifiedDocument { }; } +fn buildOperationNameCollisionFixture(allocator: std.mem.Allocator) !common.UnifiedDocument { + var paths = std.StringHashMap(common.PathItem).init(allocator); + errdefer paths.deinit(); + + try paths.put(try allocator.dupe(u8, "/api/v1/chat"), .{ + .post = try opWithTags(allocator, "chat", "POST", true, false, true, &.{"chat"}), + }); + + return .{ + .version = "3.0.0", + .info = .{ .title = "fixture", .version = "1.0.0" }, + .paths = paths, + }; +} + test "resource wrappers derive from paths" { const allocator = std.testing.allocator; var document = try buildFixture(allocator); @@ -105,3 +141,28 @@ test "resource wrappers derive from paths" { try std.testing.expect(std.mem.indexOf(u8, code, "pub const chat = resources.chat;") != null); try std.testing.expect(std.mem.indexOf(u8, code, "pub const pets = resources.pets;") != null); } + +test "resource wrapper aliases skip top-level operation name collisions" { + const allocator = std.testing.allocator; + const modes = [_]cli.ResourceWrapperMode{ .tags, .hybrid }; + + for (modes) |mode| { + var document = try buildOperationNameCollisionFixture(allocator); + defer document.deinit(allocator); + + var generator = UnifiedApiGenerator.init(allocator, .{ + .input_path = "fixture.json", + .resource_wrappers = mode, + }); + defer generator.deinit(); + + const code = try generator.generate(document); + defer allocator.free(code); + + try std.testing.expect(std.mem.indexOf(u8, code, "const _chat = chat;") != null); + try std.testing.expect(std.mem.indexOf(u8, code, "pub const resources = struct") != null); + try std.testing.expect(std.mem.indexOf(u8, code, "pub const chat = struct") != null); + try std.testing.expect(std.mem.indexOf(u8, code, "pub fn chat_(client: *Client, requestBody: std.json.Value)") != null); + try std.testing.expect(std.mem.indexOf(u8, code, "pub const chat = resources.chat;") == null); + } +}