riffer 0.22.0 → 0.23.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4e6c1902fc4a3a1ad047491befa79ae44aa7aea3e160dda3303fe18c3670e477
4
- data.tar.gz: d4407ecd6a3af8900e7a2a456870e5e4a4211df016259980bf8f5b559ad8df2f
3
+ metadata.gz: 6c8de18bc9f5531012f3ccbf37098925f0f58a446b1ee528cc2bb60a13ef5989
4
+ data.tar.gz: 32343e215229cb1a02f19ffe61d5d0f128654f2d4bf128b2a23e7ef67908b0aa
5
5
  SHA512:
6
- metadata.gz: 5845e5336564993a8e23ff6a5a1c903fb28925991443d4beb2673a49f7d2d49587a2e799330ea1e30614f7c63343b35a9828d0a1ee1744d6c96e19c7aa476cc2
7
- data.tar.gz: b0c71cf0f09e04982b4942b75d7dee3cee757a007a6d7279ca036fa5f81ae2216690d630f3e0a34e5ba1ea07d680f034b0d85568c5111c37805a3ff72f4d6eb5
6
+ metadata.gz: 7f8f6bdbe623eabacc481f8264f9d43472c1d152d2383736087102db93e6d55047506d2fd6e7ec63e6647ad03aafb6f08861fcfed5ae6e44d54acd1a722041f1
7
+ data.tar.gz: 2ca5412c0f1c6a4f86e53f7d53495dcf8073e85d6173a92e0dd58a40c111cd2ed1e7f658d92f1d1bbdcd194bbf8c5bc17d00fa890dfc9186ec86885baacf5991
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "0.22.0"
2
+ ".": "0.23.0"
3
3
  }
data/CHANGELOG.md CHANGED
@@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.23.0](https://github.com/janeapp/riffer/compare/riffer/v0.22.0...riffer/v0.23.0) (2026-04-15)
9
+
10
+
11
+ ### Features
12
+
13
+ * add Gemini provider ([#199](https://github.com/janeapp/riffer/issues/199)) ([d0f0823](https://github.com/janeapp/riffer/commit/d0f08237052258be64a8fb63e0d1c23508258176))
14
+
8
15
  ## [0.22.0](https://github.com/janeapp/riffer/compare/riffer/v0.21.0...riffer/v0.22.0) (2026-04-09)
9
16
 
10
17
 
@@ -10,6 +10,7 @@ Providers are adapters that connect Riffer to LLM services. They implement a com
10
10
  | Azure OpenAI | `azure_openai` | `openai` |
11
11
  | Amazon Bedrock | `amazon_bedrock` | `aws-sdk-bedrockruntime` |
12
12
  | Anthropic | `anthropic` | `anthropic` |
13
+ | Gemini | `gemini` | None |
13
14
  | Mock | `mock` | None |
14
15
 
15
16
  ## Model String Format
@@ -22,6 +23,7 @@ class MyAgent < Riffer::Agent
22
23
  model 'azure_openai/gpt-5-mini' # Azure OpenAI
23
24
  model 'amazon_bedrock/us.anthropic.claude-haiku-4-5-20251001-v1:0' # Bedrock
24
25
  model 'anthropic/claude-haiku-4-5-20251001' # Anthropic
26
+ model 'gemini/gemini-2.5-flash-lite' # Gemini
25
27
  model 'mock/any' # Mock provider
26
28
  end
27
29
  ```
@@ -160,6 +162,9 @@ Riffer::Providers::Repository.find(:amazon_bedrock)
160
162
  Riffer::Providers::Repository.find(:anthropic)
161
163
  # => Riffer::Providers::Anthropic
162
164
 
165
+ Riffer::Providers::Repository.find(:gemini)
166
+ # => Riffer::Providers::Gemini
167
+
163
168
  Riffer::Providers::Repository.find(:mock)
164
169
  # => Riffer::Providers::Mock
165
170
  ```
@@ -172,3 +177,4 @@ Riffer::Providers::Repository.find(:mock)
172
177
  - [Azure OpenAI](05_AZURE_OPENAI.md) - GPT models via Azure
173
178
  - [Mock](06_MOCK_PROVIDER.md) - Mock provider for testing
174
179
  - [Custom Providers](07_CUSTOM_PROVIDERS.md) - Creating your own provider
180
+ - [Gemini](08_GEMINI.md) - Gemini models via Google GenAI API
@@ -0,0 +1,142 @@
1
+ # Gemini Provider
2
+
3
+ The Gemini provider connects to Google's Gemini models via the Gemini REST API.
4
+
5
+ ## Configuration
6
+
7
+ Configure your Gemini API key:
8
+
9
+ ```ruby
10
+ Riffer.configure do |config|
11
+ config.gemini.api_key = ENV['GEMINI_API_KEY']
12
+ end
13
+ ```
14
+
15
+ Or per-agent:
16
+
17
+ ```ruby
18
+ class MyAgent < Riffer::Agent
19
+ model 'gemini/gemini-2.5-flash-lite'
20
+ provider_options api_key: ENV['GEMINI_API_KEY']
21
+ end
22
+ ```
23
+
24
+ ## Supported Models
25
+
26
+ Use Gemini model IDs in the `gemini/model` format:
27
+
28
+ ```ruby
29
+ model 'gemini/gemini-2.5-flash-lite'
30
+ model 'gemini/gemini-2.5-pro'
31
+ model 'gemini/gemini-2.5-flash'
32
+ ```
33
+
34
+ ## Model Options
35
+
36
+ ### temperature
37
+
38
+ Controls randomness:
39
+
40
+ ```ruby
41
+ model_options temperature: 0.7
42
+ ```
43
+
44
+ ### maxOutputTokens
45
+
46
+ Maximum tokens in response:
47
+
48
+ ```ruby
49
+ model_options maxOutputTokens: 4096
50
+ ```
51
+
52
+ ### topP
53
+
54
+ Nucleus sampling:
55
+
56
+ ```ruby
57
+ model_options topP: 0.9
58
+ ```
59
+
60
+ ## Usage
61
+
62
+ ### Basic Generation
63
+
64
+ ```ruby
65
+ provider = Riffer::Providers::Gemini.new(api_key: ENV['GEMINI_API_KEY'])
66
+
67
+ response = provider.generate_text(
68
+ prompt: "Hello!",
69
+ model: "gemini-2.5-flash-lite"
70
+ )
71
+ puts response.content
72
+ ```
73
+
74
+ ### Streaming
75
+
76
+ ```ruby
77
+ provider.stream_text(prompt: "Tell me a story", model: "gemini-2.5-flash-lite").each do |event|
78
+ case event
79
+ when Riffer::StreamEvents::TextDelta
80
+ print event.content
81
+ when Riffer::StreamEvents::TextDone
82
+ puts "\n---"
83
+ end
84
+ end
85
+ ```
86
+
87
+ ### Structured Output
88
+
89
+ ```ruby
90
+ params = Riffer::Params.new
91
+ params.required(:sentiment, String)
92
+ params.required(:score, Float)
93
+ structured_output = Riffer::StructuredOutput.new(params)
94
+
95
+ response = provider.generate_text(
96
+ prompt: "Analyze: 'This is great!'",
97
+ model: "gemini-2.5-flash-lite",
98
+ structured_output: structured_output
99
+ )
100
+ puts response.structured_output
101
+ ```
102
+
103
+ ### Tool Calling
104
+
105
+ ```ruby
106
+ class WeatherTool < Riffer::Tool
107
+ description "Gets weather"
108
+ params do
109
+ required :city, String
110
+ end
111
+ def call(context:, city:)
112
+ text("Sunny in #{city}")
113
+ end
114
+ end
115
+
116
+ response = provider.generate_text(
117
+ prompt: "What's the weather in Tokyo?",
118
+ model: "gemini-2.5-flash-lite",
119
+ tools: [WeatherTool]
120
+ )
121
+ ```
122
+
123
+ ### File Support
124
+
125
+ Gemini supports inline base64-encoded files (images and documents):
126
+
127
+ ```ruby
128
+ file = Riffer::FilePart.new(data: base64_data, media_type: "image/png")
129
+ response = provider.generate_text(
130
+ prompt: "Describe this image",
131
+ model: "gemini-2.5-flash-lite",
132
+ files: [file]
133
+ )
134
+ ```
135
+
136
+ **Note:** URL-based file references are not supported. Provide base64-encoded data instead.
137
+
138
+ ## Limitations
139
+
140
+ - **No web search** - Gemini's standard API does not include a web search tool
141
+ - **No URL files** - Only base64 inline data is supported for file attachments
142
+ - **Tool call IDs** - Gemini does not return unique call IDs for tool invocations; IDs are generated client-side
data/lib/riffer/config.rb CHANGED
@@ -18,6 +18,7 @@ class Riffer::Config
18
18
  AmazonBedrock = Struct.new(:api_token, :region, keyword_init: true)
19
19
  Anthropic = Struct.new(:api_key, keyword_init: true)
20
20
  AzureOpenAI = Struct.new(:api_key, :endpoint, keyword_init: true)
21
+ Gemini = Struct.new(:api_key, :open_timeout, :read_timeout, keyword_init: true)
21
22
  OpenAI = Struct.new(:api_key, keyword_init: true)
22
23
  Evals = Struct.new(:judge_model, keyword_init: true)
23
24
 
@@ -30,6 +31,9 @@ class Riffer::Config
30
31
  # Azure OpenAI configuration (Struct with +api_key+ and +endpoint+).
31
32
  attr_reader :azure_openai #: Riffer::Config::AzureOpenAI
32
33
 
34
+ # Google Gemini configuration (Struct with +api_key+, +open_timeout+, and +read_timeout+).
35
+ attr_reader :gemini #: Riffer::Config::Gemini
36
+
33
37
  # OpenAI configuration (Struct with +api_key+).
34
38
  attr_reader :openai #: Riffer::Config::OpenAI
35
39
 
@@ -61,6 +65,7 @@ class Riffer::Config
61
65
  @amazon_bedrock = AmazonBedrock.new
62
66
  @anthropic = Anthropic.new
63
67
  @azure_openai = AzureOpenAI.new
68
+ @gemini = Gemini.new
64
69
  @openai = OpenAI.new
65
70
  @evals = Evals.new
66
71
  @tool_runtime = Riffer::ToolRuntime::Inline.new
@@ -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.23.0" #: String
6
6
  end
@@ -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
 
@@ -61,6 +72,9 @@ class Riffer::Config
61
72
  # Azure OpenAI configuration (Struct with +api_key+ and +endpoint+).
62
73
  attr_reader azure_openai: Riffer::Config::AzureOpenAI
63
74
 
75
+ # Google Gemini configuration (Struct with +api_key+, +open_timeout+, and +read_timeout+).
76
+ attr_reader gemini: Riffer::Config::Gemini
77
+
64
78
  # OpenAI configuration (Struct with +api_key+).
65
79
  attr_reader openai: Riffer::Config::OpenAI
66
80
 
@@ -0,0 +1,84 @@
1
+ # Generated from lib/riffer/providers/gemini.rb with RBS::Inline
2
+
3
+ # Google Gemini provider for Gemini models via the Gemini REST API.
4
+ class Riffer::Providers::Gemini < Riffer::Providers::Base
5
+ BASE_URI: URI::Generic
6
+
7
+ VALID_MODEL_PATTERN: Regexp
8
+
9
+ DEFAULT_OPEN_TIMEOUT: Integer
10
+
11
+ DEFAULT_READ_TIMEOUT: Integer
12
+
13
+ # Initializes the Gemini provider.
14
+ #
15
+ # --
16
+ # : (?api_key: String?, ?open_timeout: Integer?, ?read_timeout: Integer?, **untyped) -> void
17
+ def initialize: (?api_key: String?, ?open_timeout: Integer?, ?read_timeout: Integer?, **untyped) -> void
18
+
19
+ private
20
+
21
+ # --
22
+ # : (Array[Riffer::Messages::Base], String?, Hash[Symbol, untyped]) -> Hash[Symbol, untyped]
23
+ def build_request_params: (Array[Riffer::Messages::Base], String?, Hash[Symbol, untyped]) -> Hash[Symbol, untyped]
24
+
25
+ # --
26
+ # : (Hash[Symbol, untyped]) -> Hash[Symbol, untyped]
27
+ def execute_generate: (Hash[Symbol, untyped]) -> Hash[Symbol, untyped]
28
+
29
+ # --
30
+ # : (Hash[Symbol, untyped]) -> String
31
+ def extract_content: (Hash[Symbol, untyped]) -> String
32
+
33
+ # --
34
+ # : (Hash[Symbol, untyped]) -> Array[Riffer::Messages::Assistant::ToolCall]
35
+ def extract_tool_calls: (Hash[Symbol, untyped]) -> Array[Riffer::Messages::Assistant::ToolCall]
36
+
37
+ # --
38
+ # : (Hash[Symbol, untyped]) -> Riffer::TokenUsage?
39
+ def extract_token_usage: (Hash[Symbol, untyped]) -> Riffer::TokenUsage?
40
+
41
+ # --
42
+ # : (Hash[Symbol, untyped], Enumerator::Yielder) -> void
43
+ def execute_stream: (Hash[Symbol, untyped], Enumerator::Yielder) -> void
44
+
45
+ # --
46
+ # : (Array[Riffer::Messages::Base]) -> Hash[Symbol, untyped]
47
+ def partition_messages: (Array[Riffer::Messages::Base]) -> Hash[Symbol, untyped]
48
+
49
+ # --
50
+ # : (Riffer::Messages::Assistant) -> Hash[Symbol, untyped]
51
+ def convert_assistant_to_gemini_format: (Riffer::Messages::Assistant) -> Hash[Symbol, untyped]
52
+
53
+ # --
54
+ # : (Riffer::FilePart) -> Hash[Symbol, untyped]
55
+ def convert_file_part_to_gemini_format: (Riffer::FilePart) -> Hash[Symbol, untyped]
56
+
57
+ # --
58
+ # : (singleton(Riffer::Tool)) -> Hash[Symbol, untyped]
59
+ def convert_tool_to_gemini_format: (singleton(Riffer::Tool)) -> Hash[Symbol, untyped]
60
+
61
+ # --
62
+ # : (untyped) -> String
63
+ def encode_tool_arguments: (untyped) -> String
64
+
65
+ # --
66
+ # : (String, Hash[Symbol, untyped]) -> Net::HTTPResponse
67
+ def post_request: (String, Hash[Symbol, untyped]) -> Net::HTTPResponse
68
+
69
+ # --
70
+ # : (String, String) -> String
71
+ def api_path: (String, String) -> String
72
+
73
+ # --
74
+ # : (String) -> void
75
+ def validate_model!: (String) -> void
76
+
77
+ # --
78
+ # : (Hash[Symbol, untyped]) -> Hash[Symbol, untyped]
79
+ def strip_additional_properties: (Hash[Symbol, untyped]) -> Hash[Symbol, untyped]
80
+
81
+ # --
82
+ # : (Net::HTTPResponse) -> void
83
+ def handle_api_error!: (Net::HTTPResponse) -> void
84
+ end
@@ -11,6 +11,8 @@
11
11
  class Riffer::Skills::Backend
12
12
  SKILL_FILENAME: String
13
13
 
14
+ def initialize: () -> untyped
15
+
14
16
  # Returns frontmatter for all available skills.
15
17
  #
16
18
  # Called once at the start of generate/stream.
@@ -20,47 +20,7 @@
20
20
  # end
21
21
  # end
22
22
  class Riffer::Tool
23
- DEFAULT_TIMEOUT: Integer
24
-
25
- extend Riffer::Helpers::ClassNameConverter
26
-
27
- # Gets or sets the tool description.
28
- #
29
- # --
30
- # : (?String?) -> String?
31
- def self.description: (?String?) -> String?
32
-
33
- # Gets or sets the tool identifier/name.
34
- #
35
- # --
36
- # : (?String?) -> String
37
- def self.identifier: (?String?) -> String
38
-
39
- # Alias for identifier - used by providers.
40
- #
41
- # --
42
- # : (?String?) -> String
43
- def self.name: (?String?) -> String
44
-
45
- # Gets or sets the tool timeout in seconds.
46
- #
47
- # --
48
- # : (?(Integer | Float)?) -> (Integer | Float)
49
- def self.timeout: (?(Integer | Float)?) -> (Integer | Float)
50
-
51
- # Defines parameters using the Params DSL.
52
- #
53
- # --
54
- # : () ?{ () -> void } -> Riffer::Params?
55
- def self.params: () ?{ () -> void } -> Riffer::Params?
56
-
57
- # Returns the JSON Schema for the tool's parameters.
58
- #
59
- # --
60
- # : (?strict: bool) -> Hash[Symbol, untyped]
61
- def self.parameters_schema: (?strict: bool) -> Hash[Symbol, untyped]
62
-
63
- def self.empty_schema: () -> untyped
23
+ extend Riffer::Toolable
64
24
 
65
25
  # Executes the tool with the given arguments.
66
26
  #
@@ -0,0 +1,99 @@
1
+ # Generated from lib/riffer/toolable.rb with RBS::Inline
2
+
3
+ # Riffer::Toolable provides the shared class-level DSL for anything that can
4
+ # present as a tool to an LLM — tools today, and subagents/workflows in the
5
+ # future.
6
+ #
7
+ # Extend this module to make a class discoverable as a tool by LLM providers.
8
+ # Provides identifier, description, params, timeout, and JSON schema
9
+ # generation.
10
+ #
11
+ # Instance-level execution concerns (+call+, +call_with_validation+, etc.)
12
+ # are NOT part of Toolable — those belong on Riffer::Tool.
13
+ #
14
+ # class MyTool
15
+ # extend Riffer::Toolable
16
+ #
17
+ # description "Does something useful"
18
+ #
19
+ # params do
20
+ # required :input, String
21
+ # end
22
+ # end
23
+ module Riffer::Toolable
24
+ DEFAULT_TIMEOUT: Integer
25
+
26
+ # Tracks all classes that extend Toolable.
27
+ #
28
+ # --
29
+ # : (Module) -> void
30
+ def self.extended: (Module) -> void
31
+
32
+ # Returns all classes that have extended Toolable.
33
+ #
34
+ # --
35
+ # : () -> Array[Module]
36
+ def self.all: () -> Array[Module]
37
+
38
+ # Gets or sets the tool description.
39
+ #
40
+ # --
41
+ # : (?String?) -> String?
42
+ def description: (?String?) -> String?
43
+
44
+ # Gets or sets the tool identifier/name.
45
+ #
46
+ # --
47
+ # : (?String?) -> String
48
+ def identifier: (?String?) -> String
49
+
50
+ # Alias for identifier — used by providers.
51
+ #
52
+ # --
53
+ # : (?String?) -> String
54
+ def name: (?String?) -> String
55
+
56
+ # Gets or sets the tool timeout in seconds.
57
+ #
58
+ # --
59
+ # : (?(Integer | Float)?) -> (Integer | Float)
60
+ def timeout: (?(Integer | Float)?) -> (Integer | Float)
61
+
62
+ # Defines parameters using the Params DSL.
63
+ #
64
+ # --
65
+ # : () ?{ () -> void } -> Riffer::Params?
66
+ def params: () ?{ () -> void } -> Riffer::Params?
67
+
68
+ # Returns the JSON Schema for the tool's parameters.
69
+ #
70
+ # --
71
+ # : (?strict: bool) -> Hash[Symbol, untyped]
72
+ def parameters_schema: (?strict: bool) -> Hash[Symbol, untyped]
73
+
74
+ # Returns the kind of toolable entity.
75
+ #
76
+ # Defaults to +:tool+. Extensible to +:agent+, +:workflow+, etc.
77
+ #
78
+ # --
79
+ # : (?Symbol?) -> Symbol
80
+ def kind: (?Symbol?) -> Symbol
81
+
82
+ # Returns a provider-agnostic tool schema hash.
83
+ #
84
+ # --
85
+ # : (?strict: bool) -> Hash[Symbol, untyped]
86
+ def to_tool_schema: (?strict: bool) -> Hash[Symbol, untyped]
87
+
88
+ # Validates that the minimum required metadata is present for LLM tool use.
89
+ #
90
+ # Raises Riffer::ArgumentError if validation fails.
91
+ #
92
+ # --
93
+ # : () -> true
94
+ def validate_as_tool!: () -> true
95
+
96
+ private
97
+
98
+ def empty_schema: () -> untyped
99
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: riffer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.22.0
4
+ version: 0.23.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jake Bottrall
@@ -35,14 +35,14 @@ dependencies:
35
35
  requirements:
36
36
  - - "~>"
37
37
  - !ruby/object:Gem::Version
38
- version: 1.28.0
38
+ version: 1.32.0
39
39
  type: :development
40
40
  prerelease: false
41
41
  version_requirements: !ruby/object:Gem::Requirement
42
42
  requirements:
43
43
  - - "~>"
44
44
  - !ruby/object:Gem::Version
45
- version: 1.28.0
45
+ version: 1.32.0
46
46
  - !ruby/object:Gem::Dependency
47
47
  name: aws-sdk-bedrockruntime
48
48
  requirement: !ruby/object:Gem::Requirement
@@ -63,14 +63,14 @@ dependencies:
63
63
  requirements:
64
64
  - - "~>"
65
65
  - !ruby/object:Gem::Version
66
- version: 0.57.0
66
+ version: 0.58.0
67
67
  type: :development
68
68
  prerelease: false
69
69
  version_requirements: !ruby/object:Gem::Requirement
70
70
  requirements:
71
71
  - - "~>"
72
72
  - !ruby/object:Gem::Version
73
- version: 0.57.0
73
+ version: 0.58.0
74
74
  - !ruby/object:Gem::Dependency
75
75
  name: async
76
76
  requirement: !ruby/object:Gem::Requirement
@@ -254,6 +254,7 @@ files:
254
254
  - docs/providers/05_AZURE_OPENAI.md
255
255
  - docs/providers/06_MOCK_PROVIDER.md
256
256
  - docs/providers/07_CUSTOM_PROVIDERS.md
257
+ - docs/providers/08_GEMINI.md
257
258
  - lib/riffer.rb
258
259
  - lib/riffer/agent.rb
259
260
  - lib/riffer/agent/response.rb
@@ -292,6 +293,7 @@ files:
292
293
  - lib/riffer/providers/anthropic.rb
293
294
  - lib/riffer/providers/azure_open_ai.rb
294
295
  - lib/riffer/providers/base.rb
296
+ - lib/riffer/providers/gemini.rb
295
297
  - lib/riffer/providers/mock.rb
296
298
  - lib/riffer/providers/open_ai.rb
297
299
  - lib/riffer/providers/repository.rb
@@ -332,6 +334,7 @@ files:
332
334
  - lib/riffer/tool_runtime/fibers.rb
333
335
  - lib/riffer/tool_runtime/inline.rb
334
336
  - lib/riffer/tool_runtime/threaded.rb
337
+ - lib/riffer/toolable.rb
335
338
  - lib/riffer/tools.rb
336
339
  - lib/riffer/tools/response.rb
337
340
  - lib/riffer/version.rb
@@ -373,6 +376,7 @@ files:
373
376
  - sig/generated/riffer/providers/anthropic.rbs
374
377
  - sig/generated/riffer/providers/azure_open_ai.rbs
375
378
  - sig/generated/riffer/providers/base.rbs
379
+ - sig/generated/riffer/providers/gemini.rbs
376
380
  - sig/generated/riffer/providers/mock.rbs
377
381
  - sig/generated/riffer/providers/open_ai.rbs
378
382
  - sig/generated/riffer/providers/repository.rbs
@@ -413,6 +417,7 @@ files:
413
417
  - sig/generated/riffer/tool_runtime/fibers.rbs
414
418
  - sig/generated/riffer/tool_runtime/inline.rbs
415
419
  - sig/generated/riffer/tool_runtime/threaded.rbs
420
+ - sig/generated/riffer/toolable.rbs
416
421
  - sig/generated/riffer/tools.rbs
417
422
  - sig/generated/riffer/tools/response.rbs
418
423
  - sig/generated/riffer/version.rbs