summaryrefslogtreecommitdiff
path: root/libpanto/src/provider.zig
blob: 6d97bb7af34ba8617e6bd24674d05fc71a0804eb (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
const std = @import("std");
const conversation = @import("conversation.zig");
const config_mod = @import("config.zig");

pub const ContentBlockType = enum {
    Text,
    Thinking,
    ToolUse,
    ToolResult,
};

pub const BlockMeta = struct {
    /// Only populated for ToolUse blocks. Null for Text/Thinking.
    tool_id: ?[]const u8 = null,
    tool_name: ?[]const u8 = null,
};

/// Vtable for receiving streaming events from a Provider.
///
/// The lifecycle callbacks (`onMessageStart` ... `onMessageComplete`) return
/// `anyerror!void`. Returning an error aborts the in-flight turn: the Provider
/// stops streaming, calls `onError(err)`, and propagates `err` out of
/// `streamStep`. No partial assistant message is appended to the conversation.
///
/// `onError` is the receiver's cleanup hook. It fires exactly once per failed
/// turn, whether the error originated in the receiver itself (a write failure)
/// or in the Provider (HTTP/parse/stream failure). It is the last callback the
/// receiver will see for that turn. `onError` itself cannot fail; receivers
/// must swallow secondary failures during cleanup.
pub const ReceiverVTable = struct {
    onMessageStart: *const fn (*anyopaque, conversation.MessageRole) anyerror!void,
    onBlockStart: *const fn (*anyopaque, ContentBlockType, usize, ?BlockMeta) anyerror!void,
    onContentDelta: *const fn (*anyopaque, usize, []const u8) anyerror!void,
    onBlockComplete: *const fn (*anyopaque, usize, conversation.ContentBlock) anyerror!void,
    onMessageComplete: *const fn (*anyopaque, conversation.Message) anyerror!void,
    onError: *const fn (*anyopaque, anyerror) void,
};

pub const Receiver = struct {
    ptr: *anyopaque,
    vtable: *const ReceiverVTable,

    pub fn onMessageStart(self: Receiver, role: conversation.MessageRole) !void {
        try self.vtable.onMessageStart(self.ptr, role);
    }

    pub fn onBlockStart(self: Receiver, block_type: ContentBlockType, index: usize, meta: ?BlockMeta) !void {
        try self.vtable.onBlockStart(self.ptr, block_type, index, meta);
    }

    pub fn onContentDelta(self: Receiver, block_index: usize, delta: []const u8) !void {
        try self.vtable.onContentDelta(self.ptr, block_index, delta);
    }

    pub fn onBlockComplete(self: Receiver, block_index: usize, block: conversation.ContentBlock) !void {
        try self.vtable.onBlockComplete(self.ptr, block_index, block);
    }

    pub fn onMessageComplete(self: Receiver, message: conversation.Message) !void {
        try self.vtable.onMessageComplete(self.ptr, message);
    }

    pub fn onError(self: Receiver, err: anyerror) void {
        self.vtable.onError(self.ptr, err);
    }
};

pub const ProviderVTable = struct {
    streamStep: *const fn (*anyopaque, *conversation.Conversation, *Receiver) anyerror!void,
    deinit: *const fn (*anyopaque) void,
};

pub const Provider = struct {
    ptr: *anyopaque,
    vtable: *const ProviderVTable,

    /// Construct the concrete provider implementation that matches `cfg`'s
    /// API style, heap-allocate it, and return a `Provider` interface bound
    /// to it. `deinit` tears down the impl and frees the allocation.
    ///
    /// The concrete provider types (`OpenAIChatProvider`, etc.) are
    /// implementation details; callers interact only with this interface.
    pub fn init(
        allocator: std.mem.Allocator,
        io: std.Io,
        cfg: config_mod.Config,
    ) !Provider {
        // Imported lazily to break the circular module graph:
        // provider.zig <- provider_openai_chat.zig <- provider.zig.
        const provider_openai_chat = @import("provider_openai_chat.zig");
        const provider_anthropic_messages = @import("provider_anthropic_messages.zig");
        switch (cfg) {
            .openai_chat => |c| {
                const impl = try allocator.create(provider_openai_chat.OpenAIChatProvider);
                impl.* = provider_openai_chat.OpenAIChatProvider.init(allocator, io, c);
                return impl.provider();
            },
            .anthropic_messages => |c| {
                const impl = try allocator.create(provider_anthropic_messages.AnthropicMessagesProvider);
                impl.* = provider_anthropic_messages.AnthropicMessagesProvider.init(allocator, io, c);
                return impl.provider();
            },
        }
    }

    pub fn streamStep(self: Provider, conv: *conversation.Conversation, receiver: *Receiver) anyerror!void {
        return self.vtable.streamStep(self.ptr, conv, receiver);
    }

    pub fn deinit(self: Provider) void {
        self.vtable.deinit(self.ptr);
    }
};