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
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
|
const std = @import("std");
const config_mod = @import("config.zig");
const conversation = @import("conversation.zig");
const tool_registry_mod = @import("tool_registry.zig");
const session_mod = @import("session.zig");
pub const ToolRegistry = tool_registry_mod.ToolRegistry;
pub const Usage = session_mod.Usage;
pub const ContentBlockType = enum {
Text,
Thinking,
ToolUse,
ToolResult,
};
/// 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.
///
/// Tool-use identity (`id`, `name`) is delivered via `onToolDetails`, fired
/// once per ToolUse block at the earliest moment both fields are known. For
/// Anthropic this is immediately after `onBlockStart`, before any deltas. For
/// OpenAI Chat Completions this may be partway through the arg-deltas, since
/// the wire protocol can split `id` and `name` across multiple streaming
/// chunks. The only guarantees are: it fires strictly after the block's
/// `onBlockStart`, strictly before its `onBlockComplete`, and at most once
/// per ToolUse block. It never fires for non-ToolUse blocks. If a tool_use
/// block is dropped because identity never fully arrived, `onToolDetails`
/// (and `onBlockComplete`) never fire for it.
///
/// `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.
///
/// `onMessageComplete`'s `usage` argument carries the wire-reported token
/// counts for the just-finished assistant turn. Providers fire this exactly
/// once per successful turn. `usage` is `null` only when the wire genuinely
/// did not deliver any usage information — chiefly OpenAI-compatible proxies
/// (OpenRouter, vLLM, some self-hosted backends) that ignore
/// `stream_options.include_usage`. Receivers that compute cost should record
/// the null case explicitly ("unknown") rather than treating it as zero.
pub const ReceiverVTable = struct {
onMessageStart: *const fn (*anyopaque, conversation.MessageRole) anyerror!void,
onBlockStart: *const fn (*anyopaque, ContentBlockType, usize) anyerror!void,
onToolDetails: *const fn (*anyopaque, usize, []const u8, []const u8) 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, ?Usage) 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) !void {
try self.vtable.onBlockStart(self.ptr, block_type, index);
}
pub fn onToolDetails(self: Receiver, block_index: usize, id: []const u8, name: []const u8) !void {
try self.vtable.onToolDetails(self.ptr, block_index, id, name);
}
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, usage: ?Usage) !void {
try self.vtable.onMessageComplete(self.ptr, message, usage);
}
pub fn onError(self: Receiver, err: anyerror) void {
self.vtable.onError(self.ptr, err);
}
};
pub const ProviderVTable = struct {
streamStep: *const fn (
*anyopaque,
*conversation.Conversation,
*const ToolRegistry,
*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,
tools: *const ToolRegistry,
receiver: *Receiver,
) anyerror!void {
return self.vtable.streamStep(self.ptr, conv, tools, receiver);
}
pub fn deinit(self: Provider) void {
self.vtable.deinit(self.ptr);
}
};
|