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 +4 -4
- data/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +23 -0
- data/docs/providers/01_PROVIDERS.md +6 -0
- data/docs/providers/08_GEMINI.md +142 -0
- data/lib/riffer/config.rb +5 -0
- data/lib/riffer/providers/amazon_bedrock.rb +4 -4
- data/lib/riffer/providers/anthropic.rb +4 -4
- data/lib/riffer/providers/base.rb +17 -0
- data/lib/riffer/providers/gemini.rb +323 -0
- data/lib/riffer/providers/open_ai.rb +4 -4
- data/lib/riffer/providers/repository.rb +1 -0
- data/lib/riffer/skills/backend.rb +2 -0
- data/lib/riffer/tool.rb +2 -64
- data/lib/riffer/toolable.rb +140 -0
- data/lib/riffer/version.rb +1 -1
- data/sig/generated/riffer/config.rbs +14 -0
- data/sig/generated/riffer/providers/base.rbs +10 -0
- data/sig/generated/riffer/providers/gemini.rbs +84 -0
- data/sig/generated/riffer/skills/backend.rbs +2 -0
- data/sig/generated/riffer/tool.rbs +1 -44
- data/sig/generated/riffer/toolable.rbs +99 -0
- metadata +10 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6c8de18bc9f5531012f3ccbf37098925f0f58a446b1ee528cc2bb60a13ef5989
|
|
4
|
+
data.tar.gz: 32343e215229cb1a02f19ffe61d5d0f128654f2d4bf128b2a23e7ef67908b0aa
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7f8f6bdbe623eabacc481f8264f9d43472c1d152d2383736087102db93e6d55047506d2fd6e7ec63e6647ad03aafb6f08861fcfed5ae6e44d54acd1a722041f1
|
|
7
|
+
data.tar.gz: 2ca5412c0f1c6a4f86e53f7d53495dcf8073e85d6173a92e0dd58a40c111cd2ed1e7f658d92f1d1bbdcd194bbf8c5bc17d00fa890dfc9186ec86885baacf5991
|
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)]
|
data/lib/riffer/tool.rb
CHANGED
|
@@ -24,71 +24,9 @@ require "timeout"
|
|
|
24
24
|
# end
|
|
25
25
|
#
|
|
26
26
|
class Riffer::Tool
|
|
27
|
-
|
|
27
|
+
extend Riffer::Toolable
|
|
28
28
|
|
|
29
|
-
|
|
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
|
data/lib/riffer/version.rb
CHANGED
|
@@ -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
|
|
@@ -20,50 +20,7 @@
|
|
|
20
20
|
# end
|
|
21
21
|
# end
|
|
22
22
|
class Riffer::Tool
|
|
23
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|