riffer 0.22.0 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,323 @@
1
+ # frozen_string_literal: true
2
+ # rbs_inline: enabled
3
+
4
+ require "json"
5
+ require "net/http"
6
+ require "securerandom"
7
+ require "uri"
8
+
9
+ # Google Gemini provider for Gemini models via the Gemini REST API.
10
+ class Riffer::Providers::Gemini < Riffer::Providers::Base
11
+ BASE_URI = URI("https://generativelanguage.googleapis.com") #: URI::Generic
12
+ VALID_MODEL_PATTERN = /\A[a-zA-Z0-9._-]+\z/ #: Regexp
13
+ DEFAULT_OPEN_TIMEOUT = 10 #: Integer
14
+ DEFAULT_READ_TIMEOUT = 60 #: Integer
15
+
16
+ # Initializes the Gemini provider.
17
+ #
18
+ #--
19
+ #: (?api_key: String?, ?open_timeout: Integer?, ?read_timeout: Integer?, **untyped) -> void
20
+ def initialize(api_key: nil, open_timeout: nil, read_timeout: nil, **options)
21
+ api_key ||= Riffer.config.gemini.api_key
22
+ @api_key = api_key
23
+ @open_timeout = open_timeout || Riffer.config.gemini.open_timeout || DEFAULT_OPEN_TIMEOUT
24
+ @read_timeout = read_timeout || Riffer.config.gemini.read_timeout || DEFAULT_READ_TIMEOUT
25
+ end
26
+
27
+ private
28
+
29
+ #--
30
+ #: (Array[Riffer::Messages::Base], String?, Hash[Symbol, untyped]) -> Hash[Symbol, untyped]
31
+ def build_request_params(messages, model, options)
32
+ partitioned = partition_messages(messages)
33
+ tools = options[:tools]
34
+ structured_output = options[:structured_output]
35
+
36
+ params = {
37
+ model: model,
38
+ contents: partitioned[:contents]
39
+ }
40
+
41
+ params[:systemInstruction] = partitioned[:system_instruction] if partitioned[:system_instruction]
42
+
43
+ if tools && !tools.empty?
44
+ params[:tools] = [{
45
+ functionDeclarations: tools.map { |t| convert_tool_to_gemini_format(t) }
46
+ }]
47
+ end
48
+
49
+ generation_config = options.except(:tools, :structured_output)
50
+
51
+ if structured_output
52
+ generation_config[:responseMimeType] = "application/json"
53
+ generation_config[:responseSchema] = strip_additional_properties(structured_output.json_schema)
54
+ end
55
+
56
+ params[:generationConfig] = generation_config unless generation_config.empty?
57
+
58
+ params
59
+ end
60
+
61
+ #--
62
+ #: (Hash[Symbol, untyped]) -> Hash[Symbol, untyped]
63
+ def execute_generate(params)
64
+ model = params[:model]
65
+ body = params.except(:model)
66
+ response = post_request(api_path(model, "generateContent"), body)
67
+ handle_api_error!(response) unless response.is_a?(Net::HTTPSuccess)
68
+ JSON.parse(response.body, symbolize_names: true)
69
+ end
70
+
71
+ #--
72
+ #: (Hash[Symbol, untyped]) -> String
73
+ def extract_content(response)
74
+ parts = response.dig(:candidates, 0, :content, :parts)
75
+ return "" unless parts
76
+
77
+ parts.filter_map { |part| part[:text] }.join
78
+ end
79
+
80
+ #--
81
+ #: (Hash[Symbol, untyped]) -> Array[Riffer::Messages::Assistant::ToolCall]
82
+ def extract_tool_calls(response)
83
+ parts = response.dig(:candidates, 0, :content, :parts)
84
+ return [] unless parts
85
+
86
+ parts.filter_map do |part|
87
+ next unless part[:functionCall]
88
+
89
+ fc = part[:functionCall]
90
+ Riffer::Messages::Assistant::ToolCall.new(
91
+ call_id: "gemini_call_#{SecureRandom.hex(12)}",
92
+ name: fc[:name],
93
+ arguments: encode_tool_arguments(fc[:args])
94
+ )
95
+ end
96
+ end
97
+
98
+ #--
99
+ #: (Hash[Symbol, untyped]) -> Riffer::TokenUsage?
100
+ def extract_token_usage(response)
101
+ usage = response[:usageMetadata]
102
+ return nil unless usage
103
+
104
+ Riffer::TokenUsage.new(
105
+ input_tokens: usage[:promptTokenCount] || 0,
106
+ output_tokens: usage[:candidatesTokenCount] || 0
107
+ )
108
+ end
109
+
110
+ #--
111
+ #: (Hash[Symbol, untyped], Enumerator::Yielder) -> void
112
+ def execute_stream(params, yielder)
113
+ model = params[:model]
114
+ body = params.except(:model)
115
+
116
+ uri = URI("#{BASE_URI}/#{api_path(model, "streamGenerateContent")}?alt=sse")
117
+ request = Net::HTTP::Post.new(uri)
118
+ request["Content-Type"] = "application/json"
119
+ request["x-goog-api-key"] = @api_key
120
+ request.body = body.to_json
121
+
122
+ full_text = +""
123
+ buffer = +""
124
+
125
+ process_chunk = lambda do |chunk|
126
+ buffer << chunk
127
+
128
+ while (match = buffer.match(/\r?\n\r?\n/))
129
+ frame = buffer.slice!(0, match.end(0)).strip
130
+ next unless frame.start_with?("data: ")
131
+
132
+ json_str = frame.delete_prefix("data: ").strip
133
+ next if json_str.empty?
134
+
135
+ parsed = JSON.parse(json_str, symbolize_names: true)
136
+ parts = parsed.dig(:candidates, 0, :content, :parts)
137
+
138
+ parts&.each do |part|
139
+ if part[:text]
140
+ full_text << part[:text]
141
+ yielder << Riffer::StreamEvents::TextDelta.new(part[:text])
142
+ elsif part[:functionCall]
143
+ fc = part[:functionCall]
144
+ call_id = "gemini_call_#{SecureRandom.hex(12)}"
145
+ arguments = encode_tool_arguments(fc[:args])
146
+ yielder << Riffer::StreamEvents::ToolCallDone.new(
147
+ item_id: call_id,
148
+ call_id: call_id,
149
+ name: fc[:name],
150
+ arguments: arguments
151
+ )
152
+ end
153
+ end
154
+
155
+ usage = parsed[:usageMetadata]
156
+ if usage && usage[:candidatesTokenCount]
157
+ yielder << Riffer::StreamEvents::TokenUsageDone.new(
158
+ token_usage: Riffer::TokenUsage.new(
159
+ input_tokens: usage[:promptTokenCount] || 0,
160
+ output_tokens: usage[:candidatesTokenCount] || 0
161
+ )
162
+ )
163
+ end
164
+ end
165
+ end
166
+
167
+ Net::HTTP.start(uri.hostname, uri.port, use_ssl: true, open_timeout: @open_timeout, read_timeout: @read_timeout) do |http|
168
+ http.request(request) do |response|
169
+ handle_api_error!(response) unless response.is_a?(Net::HTTPSuccess)
170
+
171
+ begin
172
+ response.read_body { |chunk| process_chunk.call(chunk) }
173
+ rescue IOError
174
+ process_chunk.call(response.body)
175
+ end
176
+ end
177
+ end
178
+
179
+ yielder << Riffer::StreamEvents::TextDone.new(full_text) unless full_text.empty?
180
+ end
181
+
182
+ #--
183
+ #: (Array[Riffer::Messages::Base]) -> Hash[Symbol, untyped]
184
+ def partition_messages(messages)
185
+ system_parts = []
186
+ contents = []
187
+
188
+ messages.each do |message|
189
+ case message
190
+ when Riffer::Messages::System
191
+ system_parts << {text: message.content}
192
+ when Riffer::Messages::User
193
+ if message.files.empty?
194
+ contents << {role: "user", parts: [{text: message.content}]}
195
+ else
196
+ parts = [{text: message.content}]
197
+ message.files.each { |file| parts << convert_file_part_to_gemini_format(file) }
198
+ contents << {role: "user", parts: parts}
199
+ end
200
+ when Riffer::Messages::Assistant
201
+ contents << convert_assistant_to_gemini_format(message)
202
+ when Riffer::Messages::Tool
203
+ contents << {
204
+ role: "user",
205
+ parts: [{
206
+ functionResponse: {
207
+ name: message.name,
208
+ response: {result: message.content}
209
+ }
210
+ }]
211
+ }
212
+ end
213
+ end
214
+
215
+ result = {contents: contents}
216
+ result[:system_instruction] = {parts: system_parts} unless system_parts.empty?
217
+ result
218
+ end
219
+
220
+ #--
221
+ #: (Riffer::Messages::Assistant) -> Hash[Symbol, untyped]
222
+ def convert_assistant_to_gemini_format(message)
223
+ parts = []
224
+ parts << {text: message.content} if message.content && !message.content.empty?
225
+
226
+ message.tool_calls.each do |tc|
227
+ parts << {
228
+ functionCall: {
229
+ name: tc.name,
230
+ args: parse_tool_arguments(tc.arguments)
231
+ }
232
+ }
233
+ end
234
+
235
+ {role: "model", parts: parts}
236
+ end
237
+
238
+ #--
239
+ #: (Riffer::FilePart) -> Hash[Symbol, untyped]
240
+ def convert_file_part_to_gemini_format(file)
241
+ if file.url?
242
+ raise Riffer::ArgumentError,
243
+ "Gemini provider does not support URL-based file references. Provide base64-encoded data instead."
244
+ end
245
+
246
+ {inlineData: {mimeType: file.media_type, data: file.data}}
247
+ end
248
+
249
+ #--
250
+ #: (singleton(Riffer::Tool)) -> Hash[Symbol, untyped]
251
+ def convert_tool_to_gemini_format(tool)
252
+ {
253
+ name: tool.name,
254
+ description: tool.description,
255
+ parameters: strip_additional_properties(tool.parameters_schema)
256
+ }
257
+ end
258
+
259
+ #--
260
+ #: (untyped) -> String
261
+ def encode_tool_arguments(args)
262
+ return "{}" unless args
263
+
264
+ args.is_a?(String) ? args : args.to_json
265
+ end
266
+
267
+ #--
268
+ #: (String, Hash[Symbol, untyped]) -> Net::HTTPResponse
269
+ def post_request(path, body)
270
+ uri = URI("#{BASE_URI}/#{path}")
271
+ request = Net::HTTP::Post.new(uri)
272
+ request["Content-Type"] = "application/json"
273
+ request["x-goog-api-key"] = @api_key
274
+ request.body = body.to_json
275
+ Net::HTTP.start(uri.hostname, uri.port, use_ssl: true, open_timeout: @open_timeout, read_timeout: @read_timeout) { |http| http.request(request) }
276
+ end
277
+
278
+ #--
279
+ #: (String, String) -> String
280
+ def api_path(model, method)
281
+ validate_model!(model)
282
+ "v1beta/models/#{model}:#{method}"
283
+ end
284
+
285
+ #--
286
+ #: (String) -> void
287
+ def validate_model!(model)
288
+ return if model.match?(VALID_MODEL_PATTERN)
289
+
290
+ raise Riffer::ArgumentError, "Invalid model name: #{model.inspect}. Model must contain only alphanumeric characters, hyphens, dots, and underscores."
291
+ end
292
+
293
+ #--
294
+ #: (Hash[Symbol, untyped]) -> Hash[Symbol, untyped]
295
+ def strip_additional_properties(schema)
296
+ schema = schema.dup
297
+ schema.delete(:additionalProperties)
298
+
299
+ if schema[:properties]
300
+ schema[:properties] = schema[:properties].transform_values do |prop|
301
+ strip_additional_properties(prop)
302
+ end
303
+ end
304
+
305
+ if schema[:items].is_a?(Hash)
306
+ schema[:items] = strip_additional_properties(schema[:items])
307
+ end
308
+
309
+ schema
310
+ end
311
+
312
+ #--
313
+ #: (Net::HTTPResponse) -> void
314
+ def handle_api_error!(response)
315
+ body = begin
316
+ JSON.parse(response.body, symbolize_names: true)
317
+ rescue JSON::ParserError
318
+ {message: response.body}
319
+ end
320
+ error_message = body.dig(:error, :message) || body[:message] || response.body
321
+ raise Riffer::Error, "Gemini API error (#{response.code}): #{error_message}"
322
+ end
323
+ end
@@ -8,6 +8,7 @@ class Riffer::Providers::Repository
8
8
  amazon_bedrock: -> { Riffer::Providers::AmazonBedrock },
9
9
  anthropic: -> { Riffer::Providers::Anthropic },
10
10
  azure_openai: -> { Riffer::Providers::AzureOpenAI },
11
+ gemini: -> { Riffer::Providers::Gemini },
11
12
  openai: -> { Riffer::Providers::OpenAI },
12
13
  mock: -> { Riffer::Providers::Mock }
13
14
  }.freeze #: Hash[Symbol, ^() -> singleton(Riffer::Providers::Base)]
@@ -12,6 +12,8 @@
12
12
  class Riffer::Skills::Backend
13
13
  SKILL_FILENAME = "SKILL.md" #: String
14
14
 
15
+ def initialize = nil
16
+
15
17
  # Returns frontmatter for all available skills.
16
18
  #
17
19
  # Called once at the start of generate/stream.
data/lib/riffer/tool.rb CHANGED
@@ -24,68 +24,9 @@ require "timeout"
24
24
  # end
25
25
  #
26
26
  class Riffer::Tool
27
- DEFAULT_TIMEOUT = 10 #: Integer
27
+ extend Riffer::Toolable
28
28
 
29
- extend Riffer::Helpers::ClassNameConverter
30
-
31
- # Gets or sets the tool description.
32
- #
33
- #--
34
- #: (?String?) -> String?
35
- def self.description(value = nil)
36
- return @description if value.nil?
37
- @description = value.to_s
38
- end
39
-
40
- # Gets or sets the tool identifier/name.
41
- #
42
- #--
43
- #: (?String?) -> String
44
- def self.identifier(value = nil)
45
- return @identifier || class_name_to_path(Module.instance_method(:name).bind_call(self)) if value.nil?
46
- @identifier = value.to_s
47
- end
48
-
49
- # Alias for identifier - used by providers.
50
- #
51
- #--
52
- #: (?String?) -> String
53
- def self.name(value = nil)
54
- return identifier(value) unless value.nil?
55
- identifier
56
- end
57
-
58
- # Gets or sets the tool timeout in seconds.
59
- #
60
- #--
61
- #: (?(Integer | Float)?) -> (Integer | Float)
62
- def self.timeout(value = nil)
63
- return @timeout || DEFAULT_TIMEOUT if value.nil?
64
- @timeout = value.to_f
65
- end
66
-
67
- # Defines parameters using the Params DSL.
68
- #
69
- #--
70
- #: () ?{ () -> void } -> Riffer::Params?
71
- def self.params(&block)
72
- return @params_builder if block.nil?
73
- @params_builder = Riffer::Params.new
74
- @params_builder.instance_eval(&block)
75
- end
76
-
77
- # Returns the JSON Schema for the tool's parameters.
78
- #
79
- #--
80
- #: (?strict: bool) -> Hash[Symbol, untyped]
81
- def self.parameters_schema(strict: false)
82
- @params_builder&.to_json_schema(strict: strict) || empty_schema
83
- end
84
-
85
- def self.empty_schema # :nodoc:
86
- {type: "object", properties: {}, required: [], additionalProperties: false}
87
- end
88
- private_class_method :empty_schema
29
+ kind :tool
89
30
 
90
31
  # Executes the tool with the given arguments.
91
32
  #
@@ -0,0 +1,140 @@
1
+ # frozen_string_literal: true
2
+ # rbs_inline: enabled
3
+
4
+ # Riffer::Toolable provides the shared class-level DSL for anything that can
5
+ # present as a tool to an LLM — tools today, and subagents/workflows in the
6
+ # future.
7
+ #
8
+ # Extend this module to make a class discoverable as a tool by LLM providers.
9
+ # Provides identifier, description, params, timeout, and JSON schema
10
+ # generation.
11
+ #
12
+ # Instance-level execution concerns (+call+, +call_with_validation+, etc.)
13
+ # are NOT part of Toolable — those belong on Riffer::Tool.
14
+ #
15
+ # class MyTool
16
+ # extend Riffer::Toolable
17
+ #
18
+ # description "Does something useful"
19
+ #
20
+ # params do
21
+ # required :input, String
22
+ # end
23
+ # end
24
+ #
25
+ module Riffer::Toolable
26
+ DEFAULT_TIMEOUT = 10 #: Integer
27
+
28
+ # Tracks all classes that extend Toolable.
29
+ #
30
+ #--
31
+ #: (Module) -> void
32
+ def self.extended(base)
33
+ base.extend Riffer::Helpers::ClassNameConverter
34
+ @extenders ||= []
35
+ @extenders << base
36
+ end
37
+
38
+ # Returns all classes that have extended Toolable.
39
+ #
40
+ #--
41
+ #: () -> Array[Module]
42
+ def self.all
43
+ @extenders || []
44
+ end
45
+
46
+ # Gets or sets the tool description.
47
+ #
48
+ #--
49
+ #: (?String?) -> String?
50
+ def description(value = nil)
51
+ return @description if value.nil?
52
+ @description = value.to_s
53
+ end
54
+
55
+ # Gets or sets the tool identifier/name.
56
+ #
57
+ #--
58
+ #: (?String?) -> String
59
+ def identifier(value = nil)
60
+ return @identifier || class_name_to_path(Module.instance_method(:name).bind_call(self)) if value.nil?
61
+ @identifier = value.to_s
62
+ end
63
+
64
+ # Alias for identifier — used by providers.
65
+ #
66
+ #--
67
+ #: (?String?) -> String
68
+ def name(value = nil)
69
+ return identifier(value) unless value.nil?
70
+ identifier
71
+ end
72
+
73
+ # Gets or sets the tool timeout in seconds.
74
+ #
75
+ #--
76
+ #: (?(Integer | Float)?) -> (Integer | Float)
77
+ def timeout(value = nil)
78
+ return @timeout || DEFAULT_TIMEOUT if value.nil?
79
+ @timeout = value.to_f
80
+ end
81
+
82
+ # Defines parameters using the Params DSL.
83
+ #
84
+ #--
85
+ #: () ?{ () -> void } -> Riffer::Params?
86
+ def params(&block)
87
+ return @params_builder if block.nil?
88
+ @params_builder = Riffer::Params.new
89
+ @params_builder.instance_eval(&block)
90
+ end
91
+
92
+ # Returns the JSON Schema for the tool's parameters.
93
+ #
94
+ #--
95
+ #: (?strict: bool) -> Hash[Symbol, untyped]
96
+ def parameters_schema(strict: false)
97
+ @params_builder&.to_json_schema(strict: strict) || empty_schema
98
+ end
99
+
100
+ # Returns the kind of toolable entity.
101
+ #
102
+ # Defaults to +:tool+. Extensible to +:agent+, +:workflow+, etc.
103
+ #
104
+ #--
105
+ #: (?Symbol?) -> Symbol
106
+ def kind(value = nil)
107
+ return @kind || :tool if value.nil?
108
+ @kind = value.to_sym
109
+ end
110
+
111
+ # Returns a provider-agnostic tool schema hash.
112
+ #
113
+ #--
114
+ #: (?strict: bool) -> Hash[Symbol, untyped]
115
+ def to_tool_schema(strict: false)
116
+ {
117
+ name: name,
118
+ description: description,
119
+ parameters_schema: parameters_schema(strict: strict)
120
+ }
121
+ end
122
+
123
+ # Validates that the minimum required metadata is present for LLM tool use.
124
+ #
125
+ # Raises Riffer::ArgumentError if validation fails.
126
+ #
127
+ #--
128
+ #: () -> true
129
+ def validate_as_tool!
130
+ raise Riffer::ArgumentError, "#{self} must define a description" if description.nil? || description.to_s.strip.empty?
131
+ raise Riffer::ArgumentError, "#{self} must have an identifier" if identifier.nil? || identifier.to_s.strip.empty?
132
+ true
133
+ end
134
+
135
+ private
136
+
137
+ def empty_schema # :nodoc:
138
+ {type: "object", properties: {}, required: [], additionalProperties: false}
139
+ end
140
+ end
@@ -2,5 +2,5 @@
2
2
  # rbs_inline: enabled
3
3
 
4
4
  module Riffer
5
- VERSION = "0.22.0" #: String
5
+ VERSION = "0.24.0" #: String
6
6
  end
@@ -247,6 +247,10 @@ class Riffer::Agent
247
247
  # : ((String | Array[Hash[Symbol, untyped] | Riffer::Messages::Base]), ?files: Array[Hash[Symbol, untyped] | Riffer::FilePart]?) -> void
248
248
  def initialize_messages: (String | Array[Hash[Symbol, untyped] | Riffer::Messages::Base], ?files: Array[Hash[Symbol, untyped] | Riffer::FilePart]?) -> void
249
249
 
250
+ # --
251
+ # : (Array[Hash[Symbol, untyped] | Riffer::Messages::Base]) -> void
252
+ def validate_seed_ids!: (Array[Hash[Symbol, untyped] | Riffer::Messages::Base]) -> void
253
+
250
254
  # --
251
255
  # : (?Hash[Symbol, untyped]?) -> Riffer::Messages::System?
252
256
  def build_instruction_message: (?Hash[Symbol, untyped]?) -> Riffer::Messages::System?
@@ -38,6 +38,17 @@ class Riffer::Config
38
38
  | ({ ?api_key: untyped, ?endpoint: untyped }) -> instance
39
39
  end
40
40
 
41
+ class Gemini < Struct[untyped]
42
+ attr_accessor api_key(): untyped
43
+
44
+ attr_accessor open_timeout(): untyped
45
+
46
+ attr_accessor read_timeout(): untyped
47
+
48
+ def self.new: (?api_key: untyped, ?open_timeout: untyped, ?read_timeout: untyped) -> instance
49
+ | ({ ?api_key: untyped, ?open_timeout: untyped, ?read_timeout: untyped }) -> instance
50
+ end
51
+
41
52
  class OpenAI < Struct[untyped]
42
53
  attr_accessor api_key(): untyped
43
54
 
@@ -52,6 +63,8 @@ class Riffer::Config
52
63
  | ({ ?judge_model: untyped }) -> instance
53
64
  end
54
65
 
66
+ VALID_MESSAGE_ID_STRATEGIES: untyped
67
+
55
68
  # Amazon Bedrock configuration (Struct with +api_token+ and +region+).
56
69
  attr_reader amazon_bedrock: Riffer::Config::AmazonBedrock
57
70
 
@@ -61,6 +74,9 @@ class Riffer::Config
61
74
  # Azure OpenAI configuration (Struct with +api_key+ and +endpoint+).
62
75
  attr_reader azure_openai: Riffer::Config::AzureOpenAI
63
76
 
77
+ # Google Gemini configuration (Struct with +api_key+, +open_timeout+, and +read_timeout+).
78
+ attr_reader gemini: Riffer::Config::Gemini
79
+
64
80
  # OpenAI configuration (Struct with +api_key+).
65
81
  attr_reader openai: Riffer::Config::OpenAI
66
82
 
@@ -82,6 +98,23 @@ class Riffer::Config
82
98
  # : ((singleton(Riffer::ToolRuntime) | Riffer::ToolRuntime | Proc)) -> void
83
99
  def tool_runtime=: (singleton(Riffer::ToolRuntime) | Riffer::ToolRuntime | Proc) -> void
84
100
 
101
+ # Strategy for auto-generating message ids. One of +:none+ (default, no id),
102
+ # +:uuid+ (UUIDv4), or +:uuidv7+ (time-ordered UUIDv7).
103
+ #
104
+ # When set to anything other than +:none+, each +Riffer::Messages::Base+
105
+ # instance gets an +id+ populated at construction time, and seeded messages
106
+ # passed to +Riffer::Agent#generate+ must carry their own +:id+.
107
+ attr_reader message_id_strategy: Symbol
108
+
109
+ # Sets the message id strategy.
110
+ #
111
+ # Raises +Riffer::ArgumentError+ if the value is not one of
112
+ # +:none+, +:uuid+, or +:uuidv7+.
113
+ #
114
+ # --
115
+ # : (Symbol) -> void
116
+ def message_id_strategy=: (Symbol) -> void
117
+
85
118
  # --
86
119
  # : () -> void
87
120
  def initialize: () -> void
@@ -30,8 +30,8 @@ class Riffer::Messages::Assistant < Riffer::Messages::Base
30
30
  attr_reader structured_output: Hash[Symbol, untyped]?
31
31
 
32
32
  # --
33
- # : (String, ?tool_calls: Array[Riffer::Messages::Assistant::ToolCall], ?token_usage: Riffer::TokenUsage?, ?structured_output: Hash[Symbol, untyped]?) -> void
34
- def initialize: (String, ?tool_calls: Array[Riffer::Messages::Assistant::ToolCall], ?token_usage: Riffer::TokenUsage?, ?structured_output: Hash[Symbol, untyped]?) -> void
33
+ # : (String, ?id: String?, ?tool_calls: Array[Riffer::Messages::Assistant::ToolCall], ?token_usage: Riffer::TokenUsage?, ?structured_output: Hash[Symbol, untyped]?) -> void
34
+ def initialize: (String, ?id: String?, ?tool_calls: Array[Riffer::Messages::Assistant::ToolCall], ?token_usage: Riffer::TokenUsage?, ?structured_output: Hash[Symbol, untyped]?) -> void
35
35
 
36
36
  # --
37
37
  # : () -> Symbol
@@ -7,9 +7,12 @@ class Riffer::Messages::Base
7
7
  # The message content.
8
8
  attr_reader content: String
9
9
 
10
+ # The message id, or nil when +Riffer.config.message_id_strategy+ is +:none+.
11
+ attr_reader id: String?
12
+
10
13
  # --
11
- # : (String) -> void
12
- def initialize: (String) -> void
14
+ # : (String, ?id: String?) -> void
15
+ def initialize: (String, ?id: String?) -> void
13
16
 
14
17
  # Converts the message to a hash.
15
18
  #
@@ -24,4 +27,9 @@ class Riffer::Messages::Base
24
27
  # --
25
28
  # : () -> Symbol
26
29
  def role: () -> Symbol
30
+
31
+ private
32
+
33
+ # : () -> String?
34
+ def generate_id: () -> String?
27
35
  end
@@ -24,8 +24,8 @@ class Riffer::Messages::Tool < Riffer::Messages::Base
24
24
  attr_reader error_type: Symbol?
25
25
 
26
26
  # --
27
- # : (String, tool_call_id: String, name: String, ?error: String?, ?error_type: Symbol?) -> void
28
- def initialize: (String, tool_call_id: String, name: String, ?error: String?, ?error_type: Symbol?) -> void
27
+ # : (String, tool_call_id: String, name: String, ?id: String?, ?error: String?, ?error_type: Symbol?) -> void
28
+ def initialize: (String, tool_call_id: String, name: String, ?id: String?, ?error: String?, ?error_type: Symbol?) -> void
29
29
 
30
30
  # Returns true if the tool execution resulted in an error.
31
31
  #
@@ -15,8 +15,8 @@ class Riffer::Messages::User < Riffer::Messages::Base
15
15
  # Initializes a user message.
16
16
  #
17
17
  # --
18
- # : (String, ?files: Array[Riffer::FilePart]) -> void
19
- def initialize: (String, ?files: Array[Riffer::FilePart]) -> void
18
+ # : (String, ?id: String?, ?files: Array[Riffer::FilePart]) -> void
19
+ def initialize: (String, ?id: String?, ?files: Array[Riffer::FilePart]) -> void
20
20
 
21
21
  # --
22
22
  # : () -> Symbol