riffer 0.21.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: 8975a79d79a073d1bca29cd43216049766975f010b74dbc6ba1dbaaf22f69982
4
- data.tar.gz: 5473e7c42541581bf7ad3c2efd046a3f3e76201e8b0c1a9a5ef324cc8175ae47
3
+ metadata.gz: 6c8de18bc9f5531012f3ccbf37098925f0f58a446b1ee528cc2bb60a13ef5989
4
+ data.tar.gz: 32343e215229cb1a02f19ffe61d5d0f128654f2d4bf128b2a23e7ef67908b0aa
5
5
  SHA512:
6
- metadata.gz: a331b45d49617ffaa73b65df988d2b018fcc214c25480880bb9b420ac3415a5abf5a4703e2e1568d3982c7bebad77a3710b8d1f04a83b0f063117e35298bc485
7
- data.tar.gz: 3ed3b7446ce0385cf0a70a795ac7c00c27dc786a815b66bf77fe3927e4cb9bbae806342835eeb0759a5b7dcf8eb3b04df8fad249a69040e8b7a48a88da7435fe
6
+ metadata.gz: 7f8f6bdbe623eabacc481f8264f9d43472c1d152d2383736087102db93e6d55047506d2fd6e7ec63e6647ad03aafb6f08861fcfed5ae6e44d54acd1a722041f1
7
+ data.tar.gz: 2ca5412c0f1c6a4f86e53f7d53495dcf8073e85d6173a92e0dd58a40c111cd2ed1e7f658d92f1d1bbdcd194bbf8c5bc17d00fa890dfc9186ec86885baacf5991
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "0.21.0"
2
+ ".": "0.23.0"
3
3
  }
data/CHANGELOG.md CHANGED
@@ -5,6 +5,29 @@ 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
+
15
+ ## [0.22.0](https://github.com/janeapp/riffer/compare/riffer/v0.21.0...riffer/v0.22.0) (2026-04-09)
16
+
17
+
18
+ ### ⚠ BREAKING CHANGES
19
+
20
+ * `Tool.name` returns `namespace/tool_name` instead of `namespace__tool_name`.
21
+
22
+ ### Features
23
+
24
+ * use human-friendly `/` separator for tool names ([#202](https://github.com/janeapp/riffer/issues/202)) ([8dda251](https://github.com/janeapp/riffer/commit/8dda251ff6d91a6cebbbb82082b7ac21c2a51253))
25
+
26
+
27
+ ### Bug Fixes
28
+
29
+ * release 0.22.0 ([#204](https://github.com/janeapp/riffer/issues/204)) ([860a500](https://github.com/janeapp/riffer/commit/860a500bb6e991de8018b43cda0c5f2662e22402))
30
+
8
31
  ## [0.21.0](https://github.com/janeapp/riffer/compare/riffer/v0.20.0...riffer/v0.21.0) (2026-04-09)
9
32
 
10
33
 
@@ -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
@@ -120,7 +120,7 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
120
120
  if block.respond_to?(:tool_use) && block.tool_use
121
121
  tool_calls << Riffer::Messages::Assistant::ToolCall.new(
122
122
  call_id: block.tool_use.tool_use_id,
123
- name: block.tool_use.name,
123
+ name: decode_tool_name(block.tool_use.name, tools: @current_tools),
124
124
  arguments: block.tool_use.input.to_json
125
125
  )
126
126
  end
@@ -160,7 +160,7 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
160
160
  def handle_content_block_start_tool_use(event, state:, yielder:)
161
161
  state[:tool_call] = {
162
162
  id: event.start.tool_use.tool_use_id,
163
- name: event.start.tool_use.name,
163
+ name: decode_tool_name(event.start.tool_use.name, tools: @current_tools),
164
164
  arguments: ""
165
165
  }
166
166
  end
@@ -258,7 +258,7 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
258
258
  content << {
259
259
  tool_use: {
260
260
  tool_use_id: tc.call_id,
261
- name: tc.name,
261
+ name: encode_tool_name(tc.name),
262
262
  input: parse_tool_arguments(tc.arguments)
263
263
  }
264
264
  }
@@ -327,7 +327,7 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
327
327
  def convert_tool_to_bedrock_format(tool)
328
328
  {
329
329
  tool_spec: {
330
- name: tool.name,
330
+ name: encode_tool_name(tool.name),
331
331
  description: tool.description,
332
332
  input_schema: {
333
333
  json: tool.parameters_schema(strict: true)
@@ -123,7 +123,7 @@ class Riffer::Providers::Anthropic < Riffer::Providers::Base
123
123
  if block.type.to_s == "tool_use"
124
124
  tool_calls << Riffer::Messages::Assistant::ToolCall.new(
125
125
  call_id: block.id,
126
- name: block.name,
126
+ name: decode_tool_name(block.name, tools: @current_tools),
127
127
  arguments: block.input.to_json
128
128
  )
129
129
  end
@@ -229,7 +229,7 @@ class Riffer::Providers::Anthropic < Riffer::Providers::Base
229
229
  yielder << Riffer::StreamEvents::ToolCallDone.new(
230
230
  item_id: content_block.id,
231
231
  call_id: content_block.id,
232
- name: content_block.name,
232
+ name: decode_tool_name(content_block.name, tools: @current_tools),
233
233
  arguments: arguments
234
234
  )
235
235
  state[:tool_call] = nil
@@ -339,7 +339,7 @@ class Riffer::Providers::Anthropic < Riffer::Providers::Base
339
339
  content << {
340
340
  type: "tool_use",
341
341
  id: tc.call_id,
342
- name: tc.name,
342
+ name: encode_tool_name(tc.name),
343
343
  input: parse_tool_arguments(tc.arguments)
344
344
  }
345
345
  end
@@ -365,7 +365,7 @@ class Riffer::Providers::Anthropic < Riffer::Providers::Base
365
365
  #: (singleton(Riffer::Tool)) -> Hash[Symbol, untyped]
366
366
  def convert_tool_to_anthropic_format(tool)
367
367
  {
368
- name: tool.name,
368
+ name: encode_tool_name(tool.name),
369
369
  description: tool.description,
370
370
  input_schema: tool.parameters_schema(strict: true)
371
371
  }
@@ -20,6 +20,8 @@ class Riffer::Providers::Base
20
20
  include Riffer::Helpers::Dependencies
21
21
  include Riffer::Messages::Converter
22
22
 
23
+ WIRE_SEPARATOR = "__" #: String
24
+
23
25
  # Returns the preferred skill adapter for this provider.
24
26
  #
25
27
  # Override in subclasses for provider-specific formats.
@@ -36,6 +38,7 @@ class Riffer::Providers::Base
36
38
  #: (?prompt: String?, ?system: String?, ?messages: Array[Hash[Symbol, untyped] | Riffer::Messages::Base]?, ?model: String?, ?files: Array[Hash[Symbol, untyped] | Riffer::FilePart]?, **untyped) -> Riffer::Messages::Assistant
37
39
  def generate_text(prompt: nil, system: nil, messages: nil, model: nil, files: nil, **options)
38
40
  validate_input!(prompt: prompt, system: system, messages: messages)
41
+ @current_tools = options[:tools] || []
39
42
  messages = normalize_messages(prompt: prompt, system: system, messages: messages, files: files)
40
43
  validate_normalized_messages!(messages)
41
44
  messages = merge_consecutive_messages(messages)
@@ -61,6 +64,7 @@ class Riffer::Providers::Base
61
64
  #: (?prompt: String?, ?system: String?, ?messages: Array[Hash[Symbol, untyped] | Riffer::Messages::Base]?, ?model: String?, ?files: Array[Hash[Symbol, untyped] | Riffer::FilePart]?, **untyped) -> Enumerator[Riffer::StreamEvents::Base, void]
62
65
  def stream_text(prompt: nil, system: nil, messages: nil, model: nil, files: nil, **options)
63
66
  validate_input!(prompt: prompt, system: system, messages: messages)
67
+ @current_tools = options[:tools] || []
64
68
  messages = normalize_messages(prompt: prompt, system: system, messages: messages, files: files)
65
69
  validate_normalized_messages!(messages)
66
70
  messages = merge_consecutive_messages(messages)
@@ -72,6 +76,19 @@ class Riffer::Providers::Base
72
76
 
73
77
  private
74
78
 
79
+ #--
80
+ #: (String) -> String
81
+ def encode_tool_name(name)
82
+ name.gsub("/", WIRE_SEPARATOR)
83
+ end
84
+
85
+ #--
86
+ #: (String, tools: Array[Riffer::Tool]) -> String
87
+ def decode_tool_name(wire_name, tools:)
88
+ tool = tools.find { |t| encode_tool_name(t.name) == wire_name }
89
+ tool ? tool.name : wire_name
90
+ end
91
+
75
92
  #--
76
93
  #: (Array[Riffer::Messages::Base], String?, Hash[Symbol, untyped]) -> Hash[Symbol, untyped]
77
94
  def build_request_params(messages, model, options)
@@ -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
@@ -107,7 +107,7 @@ class Riffer::Providers::OpenAI < Riffer::Providers::Base
107
107
  if item.type == :function_call
108
108
  tool_calls << Riffer::Messages::Assistant::ToolCall.new(
109
109
  call_id: item.call_id,
110
- name: item.name,
110
+ name: decode_tool_name(item.name, tools: @current_tools),
111
111
  arguments: item.arguments
112
112
  )
113
113
  end
@@ -158,7 +158,7 @@ class Riffer::Providers::OpenAI < Riffer::Providers::Base
158
158
  #: (untyped, state: Hash[Symbol, untyped], yielder: Enumerator::Yielder) -> void
159
159
  def handle_output_item_added_function_call(event, state:, yielder:)
160
160
  state[:tool_info][event.item.id] = {
161
- name: event.item.name,
161
+ name: decode_tool_name(event.item.name, tools: @current_tools),
162
162
  call_id: event.item.call_id
163
163
  }
164
164
  end
@@ -284,7 +284,7 @@ class Riffer::Providers::OpenAI < Riffer::Providers::Base
284
284
  items << {
285
285
  type: "function_call",
286
286
  call_id: tc.call_id,
287
- name: tc.name,
287
+ name: encode_tool_name(tc.name),
288
288
  arguments: tc.arguments.is_a?(String) ? tc.arguments : tc.arguments.to_json
289
289
  }
290
290
  end
@@ -311,7 +311,7 @@ class Riffer::Providers::OpenAI < Riffer::Providers::Base
311
311
  def convert_tool_to_openai_format(tool)
312
312
  {
313
313
  type: "function",
314
- name: tool.name,
314
+ name: encode_tool_name(tool.name),
315
315
  description: tool.description,
316
316
  parameters: tool.parameters_schema(strict: true),
317
317
  strict: true
@@ -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,71 +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
- # Some providers do not allow "/" in tool names, so we use "__" as separator.
30
- TOOL_SEPARATOR = "__" #: String
31
-
32
- extend Riffer::Helpers::ClassNameConverter
33
-
34
- # Gets or sets the tool description.
35
- #
36
- #--
37
- #: (?String?) -> String?
38
- def self.description(value = nil)
39
- return @description if value.nil?
40
- @description = value.to_s
41
- end
42
-
43
- # Gets or sets the tool identifier/name.
44
- #
45
- #--
46
- #: (?String?) -> String
47
- def self.identifier(value = nil)
48
- return @identifier || class_name_to_path(Module.instance_method(:name).bind_call(self), separator: TOOL_SEPARATOR) if value.nil?
49
- @identifier = value.to_s
50
- end
51
-
52
- # Alias for identifier - used by providers.
53
- #
54
- #--
55
- #: (?String?) -> String
56
- def self.name(value = nil)
57
- return identifier(value) unless value.nil?
58
- identifier
59
- end
60
-
61
- # Gets or sets the tool timeout in seconds.
62
- #
63
- #--
64
- #: (?(Integer | Float)?) -> (Integer | Float)
65
- def self.timeout(value = nil)
66
- return @timeout || DEFAULT_TIMEOUT if value.nil?
67
- @timeout = value.to_f
68
- end
69
-
70
- # Defines parameters using the Params DSL.
71
- #
72
- #--
73
- #: () ?{ () -> void } -> Riffer::Params?
74
- def self.params(&block)
75
- return @params_builder if block.nil?
76
- @params_builder = Riffer::Params.new
77
- @params_builder.instance_eval(&block)
78
- end
79
-
80
- # Returns the JSON Schema for the tool's parameters.
81
- #
82
- #--
83
- #: (?strict: bool) -> Hash[Symbol, untyped]
84
- def self.parameters_schema(strict: false)
85
- @params_builder&.to_json_schema(strict: strict) || empty_schema
86
- end
87
-
88
- def self.empty_schema # :nodoc:
89
- {type: "object", properties: {}, required: [], additionalProperties: false}
90
- end
91
- private_class_method :empty_schema
29
+ kind :tool
92
30
 
93
31
  # Executes the tool with the given arguments.
94
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.21.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
 
@@ -18,6 +18,8 @@ class Riffer::Providers::Base
18
18
 
19
19
  include Riffer::Messages::Converter
20
20
 
21
+ WIRE_SEPARATOR: String
22
+
21
23
  # Returns the preferred skill adapter for this provider.
22
24
  #
23
25
  # Override in subclasses for provider-specific formats.
@@ -40,6 +42,14 @@ class Riffer::Providers::Base
40
42
 
41
43
  private
42
44
 
45
+ # --
46
+ # : (String) -> String
47
+ def encode_tool_name: (String) -> String
48
+
49
+ # --
50
+ # : (String, tools: Array[Riffer::Tool]) -> String
51
+ def decode_tool_name: (String, tools: Array[Riffer::Tool]) -> String
52
+
43
53
  # --
44
54
  # : (Array[Riffer::Messages::Base], String?, Hash[Symbol, untyped]) -> Hash[Symbol, untyped]
45
55
  def build_request_params: (Array[Riffer::Messages::Base], String?, Hash[Symbol, untyped]) -> Hash[Symbol, untyped]
@@ -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,50 +20,7 @@
20
20
  # end
21
21
  # end
22
22
  class Riffer::Tool
23
- DEFAULT_TIMEOUT: Integer
24
-
25
- # Some providers do not allow "/" in tool names, so we use "__" as separator.
26
- TOOL_SEPARATOR: String
27
-
28
- extend Riffer::Helpers::ClassNameConverter
29
-
30
- # Gets or sets the tool description.
31
- #
32
- # --
33
- # : (?String?) -> String?
34
- def self.description: (?String?) -> String?
35
-
36
- # Gets or sets the tool identifier/name.
37
- #
38
- # --
39
- # : (?String?) -> String
40
- def self.identifier: (?String?) -> String
41
-
42
- # Alias for identifier - used by providers.
43
- #
44
- # --
45
- # : (?String?) -> String
46
- def self.name: (?String?) -> String
47
-
48
- # Gets or sets the tool timeout in seconds.
49
- #
50
- # --
51
- # : (?(Integer | Float)?) -> (Integer | Float)
52
- def self.timeout: (?(Integer | Float)?) -> (Integer | Float)
53
-
54
- # Defines parameters using the Params DSL.
55
- #
56
- # --
57
- # : () ?{ () -> void } -> Riffer::Params?
58
- def self.params: () ?{ () -> void } -> Riffer::Params?
59
-
60
- # Returns the JSON Schema for the tool's parameters.
61
- #
62
- # --
63
- # : (?strict: bool) -> Hash[Symbol, untyped]
64
- def self.parameters_schema: (?strict: bool) -> Hash[Symbol, untyped]
65
-
66
- def self.empty_schema: () -> untyped
23
+ extend Riffer::Toolable
67
24
 
68
25
  # Executes the tool with the given arguments.
69
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.21.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