riffer 0.40.0 → 0.42.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.
Files changed (78) hide show
  1. checksums.yaml +4 -4
  2. data/{AGENTS.md → .claude/CLAUDE.md} +0 -8
  3. data/.claude/rules/comments.md +13 -0
  4. data/.claude/rules/rbs-inline.md +93 -0
  5. data/.release-please-manifest.json +1 -1
  6. data/CHANGELOG.md +22 -0
  7. data/README.md +20 -24
  8. data/docs/AGENTS.md +24 -14
  9. data/docs/AGENT_LIFECYCLE.md +2 -2
  10. data/docs/CONFIGURATION.md +105 -21
  11. data/docs/SERIALIZATION.md +4 -3
  12. data/docs/TOOLS.md +18 -1
  13. data/docs/providers/AMAZON_BEDROCK.md +14 -8
  14. data/docs/providers/ANTHROPIC.md +9 -7
  15. data/docs/providers/AZURE_OPENAI.md +12 -12
  16. data/docs/providers/CUSTOM_PROVIDERS.md +32 -16
  17. data/docs/providers/GEMINI.md +28 -5
  18. data/docs/providers/OPENAI.md +21 -5
  19. data/docs/providers/OPENROUTER.md +11 -6
  20. data/docs/providers/PROVIDERS.md +16 -1
  21. data/lib/riffer/agent/config.rb +0 -6
  22. data/lib/riffer/agent/run.rb +2 -4
  23. data/lib/riffer/agent/serializer.rb +0 -2
  24. data/lib/riffer/agent.rb +4 -26
  25. data/lib/riffer/config.rb +6 -6
  26. data/lib/riffer/evals/judge.rb +3 -5
  27. data/lib/riffer/guardrail.rb +1 -1
  28. data/lib/riffer/helpers/identifier.rb +41 -0
  29. data/lib/riffer/providers/amazon_bedrock.rb +31 -19
  30. data/lib/riffer/providers/anthropic.rb +23 -8
  31. data/lib/riffer/providers/azure_open_ai.rb +19 -11
  32. data/lib/riffer/providers/base.rb +32 -1
  33. data/lib/riffer/providers/gemini/client.rb +120 -0
  34. data/lib/riffer/providers/gemini.rb +13 -62
  35. data/lib/riffer/providers/mock.rb +5 -4
  36. data/lib/riffer/providers/open_ai.rb +23 -8
  37. data/lib/riffer/providers/open_router.rb +23 -9
  38. data/lib/riffer/registrable.rb +81 -0
  39. data/lib/riffer/tool.rb +1 -0
  40. data/lib/riffer/tools/toolable.rb +2 -3
  41. data/lib/riffer/version.rb +1 -1
  42. data/lib/riffer.rb +3 -0
  43. data/sig/_private/riffer/providers/amazon_bedrock.rbs +4 -2
  44. data/sig/_private/riffer/providers/anthropic.rbs +4 -2
  45. data/sig/_private/riffer/providers/gemini.rbs +7 -0
  46. data/sig/_private/riffer/providers/open_ai.rbs +4 -2
  47. data/sig/_private/riffer/providers/open_router.rbs +4 -2
  48. data/sig/generated/riffer/agent/config.rbs +1 -5
  49. data/sig/generated/riffer/agent.rbs +2 -18
  50. data/sig/generated/riffer/config.rbs +25 -15
  51. data/sig/generated/riffer/evals/judge.rbs +2 -4
  52. data/sig/generated/riffer/helpers/identifier.rbs +19 -0
  53. data/sig/generated/riffer/providers/amazon_bedrock.rbs +13 -2
  54. data/sig/generated/riffer/providers/anthropic.rbs +13 -2
  55. data/sig/generated/riffer/providers/azure_open_ai.rbs +14 -4
  56. data/sig/generated/riffer/providers/base.rbs +22 -0
  57. data/sig/generated/riffer/providers/gemini/client.rbs +65 -0
  58. data/sig/generated/riffer/providers/gemini.rbs +7 -23
  59. data/sig/generated/riffer/providers/mock.rbs +4 -3
  60. data/sig/generated/riffer/providers/open_ai.rbs +13 -2
  61. data/sig/generated/riffer/providers/open_router.rbs +16 -3
  62. data/sig/generated/riffer/registrable.rbs +51 -0
  63. data/sig/generated/riffer/tool.rbs +2 -0
  64. data/sig/generated/riffer/tools/toolable.rbs +3 -1
  65. data/sig/generated/riffer.rbs +4 -0
  66. data/sig/manual/riffer/agent.rbs +7 -0
  67. data/sig/manual/riffer/helpers/identifier.rbs +5 -0
  68. data/sig/manual/riffer/tool.rbs +7 -0
  69. metadata +14 -11
  70. data/.agents/architecture.md +0 -265
  71. data/.agents/code-style.md +0 -110
  72. data/.agents/providers.md +0 -45
  73. data/.agents/rbs-inline.md +0 -169
  74. data/.agents/testing.md +0 -60
  75. data/CLAUDE.md +0 -1
  76. data/lib/riffer/helpers/class_name_converter.rb +0 -22
  77. data/sig/generated/riffer/helpers/class_name_converter.rbs +0 -12
  78. data/sig/manual/riffer/helpers/class_name_converter.rbs +0 -5
@@ -14,9 +14,8 @@ gem 'openai'
14
14
 
15
15
  Credentials are resolved in order:
16
16
 
17
- 1. Keyword arguments (`api_key`, `base_url`)
18
- 2. Global config (`Riffer.config.azure_openai.api_key` / `.endpoint`)
19
- 3. Environment variables (`AZURE_OPENAI_API_KEY` / `AZURE_OPENAI_ENDPOINT`)
17
+ 1. Global config (`Riffer.config.azure_openai.api_key` / `.endpoint`)
18
+ 2. Environment variables (`AZURE_OPENAI_API_KEY` / `AZURE_OPENAI_ENDPOINT`)
20
19
 
21
20
  ### Global config
22
21
 
@@ -27,16 +26,20 @@ Riffer.configure do |config|
27
26
  end
28
27
  ```
29
28
 
30
- ### Per-agent
29
+ ### Custom client
30
+
31
+ Azure AD token auth, timeouts, and retries are configured on your own `OpenAI::Client`. A `Proc` is the right shape for expiring AD tokens — it is resolved on every LLM call:
31
32
 
32
33
  ```ruby
33
- class MyAgent < Riffer::Agent
34
- model 'azure_openai/gpt-5-mini'
35
- provider_options api_key: ENV['AZURE_OPENAI_API_KEY'],
36
- base_url: ENV['AZURE_OPENAI_ENDPOINT']
34
+ Riffer.configure do |config|
35
+ config.azure_openai.client = -> {
36
+ OpenAI::Client.new(api_key: AzureAd.current_token, base_url: ENV['AZURE_OPENAI_ENDPOINT'])
37
+ }
37
38
  end
38
39
  ```
39
40
 
41
+ See [Configuration → Provider Clients](../CONFIGURATION.md#provider-clients).
42
+
40
43
  ### Environment variables only
41
44
 
42
45
  If `AZURE_OPENAI_API_KEY` and `AZURE_OPENAI_ENDPOINT` are set, no explicit configuration is needed:
@@ -151,10 +154,7 @@ end
151
154
  ## Direct Provider Usage
152
155
 
153
156
  ```ruby
154
- provider = Riffer::Providers::AzureOpenAI.new(
155
- api_key: ENV['AZURE_OPENAI_API_KEY'],
156
- base_url: ENV['AZURE_OPENAI_ENDPOINT']
157
- )
157
+ provider = Riffer::Providers::AzureOpenAI.new
158
158
 
159
159
  response = provider.generate_text(
160
160
  prompt: "Hello!",
@@ -8,14 +8,14 @@ Extend `Riffer::Providers::Base` and implement the five required hook methods:
8
8
 
9
9
  ```ruby
10
10
  class Riffer::Providers::MyProvider < Riffer::Providers::Base
11
- def initialize(**options)
12
- # Initialize your client
13
- @api_key = options[:api_key] || ENV['MY_PROVIDER_API_KEY']
14
- @client = MyProviderClient.new(api_key: @api_key)
15
- end
16
-
17
11
  private
18
12
 
13
+ # Client hook — see "Client resolution" below.
14
+
15
+ def build_client
16
+ MyProviderClient.new(api_key: ENV['MY_PROVIDER_API_KEY'])
17
+ end
18
+
19
19
  # Hook methods (matching base.rb order)
20
20
 
21
21
  def build_request_params(messages, model, options)
@@ -35,11 +35,11 @@ class Riffer::Providers::MyProvider < Riffer::Providers::Base
35
35
  end
36
36
 
37
37
  def execute_generate(params)
38
- @client.generate(**params)
38
+ client.generate(**params)
39
39
  end
40
40
 
41
41
  def execute_stream(params, yielder)
42
- @client.stream(**params) do |chunk|
42
+ client.stream(**params) do |chunk|
43
43
  case chunk.type
44
44
  when :text
45
45
  yielder << Riffer::StreamEvents::TextDelta.new(chunk.content)
@@ -121,16 +121,30 @@ class Riffer::Providers::MyProvider < Riffer::Providers::Base
121
121
  end
122
122
  ```
123
123
 
124
+ ## Client resolution
125
+
126
+ Riffer constructs providers with `provider_class.new`, so `initialize` takes no arguments; read credentials from configuration inside `build_client`. `Riffer::Providers::Base` provides a private `client` method for your `execute_generate`/`execute_stream` to call. It resolves, in order:
127
+
128
+ 1. **A configured client** — whatever `global_client` returns: a client instance, or a no-argument `Proc` resolved on **every** call. Override that hook to read the client off your own configuration; it defaults to `nil`.
129
+ 2. **A memoized client** from `build_client` — implement this hook to build your SDK client from configured credentials.
130
+
131
+ This gives your provider the same "works out of the box, bring your own client in production" behavior as the built-ins. See [Configuration → Provider Clients](../CONFIGURATION.md#provider-clients).
132
+
124
133
  ## Using depends_on
125
134
 
126
135
  For lazy loading of external gems:
127
136
 
128
137
  ```ruby
129
138
  class Riffer::Providers::MyProvider < Riffer::Providers::Base
130
- def initialize(**options)
139
+ def initialize
140
+ super
131
141
  depends_on "my_provider_gem" # Only loaded when provider is used
142
+ end
143
+
144
+ private
132
145
 
133
- @client = ::MyProviderGem::Client.new(**options)
146
+ def build_client
147
+ ::MyProviderGem::Client.new(api_key: ENV["MY_PROVIDER_API_KEY"])
134
148
  end
135
149
  end
136
150
  ```
@@ -291,17 +305,19 @@ end
291
305
  # lib/riffer/providers/my_provider.rb
292
306
 
293
307
  class Riffer::Providers::MyProvider < Riffer::Providers::Base
294
- def initialize(**options)
308
+ def initialize
309
+ super
295
310
  depends_on "my_provider_gem"
296
-
297
- api_key = options[:api_key] || ENV["MY_PROVIDER_API_KEY"]
298
- @client = ::MyProviderGem::Client.new(api_key: api_key)
299
311
  end
300
312
 
301
313
  private
302
314
 
303
315
  # Hook methods
304
316
 
317
+ def build_client
318
+ ::MyProviderGem::Client.new(api_key: ENV["MY_PROVIDER_API_KEY"])
319
+ end
320
+
305
321
  def build_request_params(messages, model, options)
306
322
  system_message = extract_system(messages)
307
323
  conversation = messages.reject { |m| m.is_a?(Riffer::Messages::System) }
@@ -323,13 +339,13 @@ class Riffer::Providers::MyProvider < Riffer::Providers::Base
323
339
  end
324
340
 
325
341
  def execute_generate(params)
326
- @client.create(**params)
342
+ client.create(**params)
327
343
  end
328
344
 
329
345
  def execute_stream(params, yielder)
330
346
  accumulated_text = ""
331
347
 
332
- @client.stream(**params) do |event|
348
+ client.stream(**params) do |event|
333
349
  case event.type
334
350
  when :text_delta
335
351
  accumulated_text += event.text
@@ -12,15 +12,38 @@ Riffer.configure do |config|
12
12
  end
13
13
  ```
14
14
 
15
- Or per-agent:
15
+ ## HTTP Client
16
+
17
+ Gemini has no vendor SDK, so riffer ships its own transport: `Riffer::Providers::Gemini::Client`. The provider builds one from the configured `api_key` by default; construct your own to tune the HTTP knobs and assign it to `config.gemini.client`:
16
18
 
17
19
  ```ruby
18
- class MyAgent < Riffer::Agent
19
- model 'gemini/gemini-2.5-flash-lite'
20
- provider_options api_key: ENV['GEMINI_API_KEY']
20
+ Riffer.configure do |config|
21
+ config.gemini.client = Riffer::Providers::Gemini::Client.new(
22
+ api_key: ENV['GEMINI_API_KEY'],
23
+ read_timeout: 120
24
+ )
21
25
  end
22
26
  ```
23
27
 
28
+ | Option | Default | Description |
29
+ | --------------- | ------------------------------------------- | -------------------------------------- |
30
+ | `api_key` | `nil` | Sent as the `x-goog-api-key` header |
31
+ | `base_url` | `https://generativelanguage.googleapis.com` | API origin (proxies, regional mirrors) |
32
+ | `open_timeout` | `10` | Connection-open timeout in seconds |
33
+ | `read_timeout` | `60` | Read timeout in seconds |
34
+ | `write_timeout` | `nil` | Write timeout in seconds |
35
+ | `proxy_address` | `nil` | HTTP proxy host |
36
+ | `proxy_port` | `nil` | HTTP proxy port |
37
+
38
+ The setting accepts a client instance or a no-argument `Proc`, resolved on every LLM call — see [Configuration → Provider Clients](../CONFIGURATION.md#provider-clients).
39
+
40
+ The class is a default implementation, not a required base: any object implementing the two-method contract works, e.g. a Faraday-based or instrumented transport.
41
+
42
+ | Method | Contract |
43
+ | --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
44
+ | `post(path, body)` | POST `body` as JSON to `path`; return the parsed response `Hash` (symbol keys); raise `Riffer::Error` on a non-success status |
45
+ | `post_stream(path, body) { \|chunk\| }` | POST `body` as JSON to `path`; yield raw response body chunks; raise `Riffer::Error` on a non-success status |
46
+
24
47
  ## Supported Models
25
48
 
26
49
  Use Gemini model IDs in the `gemini/model` format:
@@ -62,7 +85,7 @@ model_options topP: 0.9
62
85
  ### Basic Generation
63
86
 
64
87
  ```ruby
65
- provider = Riffer::Providers::Gemini.new(api_key: ENV['GEMINI_API_KEY'])
88
+ provider = Riffer::Providers::Gemini.new
66
89
 
67
90
  response = provider.generate_text(
68
91
  prompt: "Hello!",
@@ -20,12 +20,28 @@ Riffer.configure do |config|
20
20
  end
21
21
  ```
22
22
 
23
- Or per-agent:
23
+ Both `api_key` and `base_url` resolve in order: `Riffer.config.openai.*` → the OpenAI SDK's own `OPENAI_API_KEY` / `OPENAI_BASE_URL` lookup. Leaving one unset in riffer means the SDK resolves it, so an `OPENAI_BASE_URL` gateway is honored without any riffer configuration.
24
+
25
+ For anything beyond the API key — timeouts, retries, proxies — supply your own `OpenAI::Client`:
24
26
 
25
27
  ```ruby
26
- class MyAgent < Riffer::Agent
27
- model 'openai/gpt-5-mini'
28
- provider_options api_key: ENV['CUSTOM_API_KEY']
28
+ Riffer.configure do |config|
29
+ config.openai.client = OpenAI::Client.new(
30
+ api_key: ENV['OPENAI_API_KEY'],
31
+ timeout: 30,
32
+ max_retries: 4
33
+ )
34
+ end
35
+ ```
36
+
37
+ The setting accepts a client instance or a no-argument `Proc`, resolved on every LLM call — see [Configuration → Provider Clients](../CONFIGURATION.md#provider-clients).
38
+
39
+ For OpenAI-compatible servers (LiteLLM, vLLM, corporate gateways), configure a `base_url`:
40
+
41
+ ```ruby
42
+ Riffer.configure do |config|
43
+ config.openai.api_key = ENV['GATEWAY_KEY']
44
+ config.openai.base_url = 'http://localhost:4000/v1'
29
45
  end
30
46
  ```
31
47
 
@@ -185,7 +201,7 @@ The provider converts Riffer messages to OpenAI format:
185
201
  ## Direct Provider Usage
186
202
 
187
203
  ```ruby
188
- provider = Riffer::Providers::OpenAI.new(api_key: ENV['OPENAI_API_KEY'])
204
+ provider = Riffer::Providers::OpenAI.new
189
205
 
190
206
  response = provider.generate_text(
191
207
  prompt: "Hello!",
@@ -24,16 +24,21 @@ Riffer.configure do |config|
24
24
  end
25
25
  ```
26
26
 
27
- Or per-agent:
27
+ The `api_key` resolves in order: `Riffer.config.openrouter.api_key` → `ENV['OPENROUTER_API_KEY']`.
28
+
29
+ For anything beyond the API key — timeouts, retries, proxies — supply your own `OpenAI::Client` pinned to the OpenRouter endpoint:
28
30
 
29
31
  ```ruby
30
- class MyAgent < Riffer::Agent
31
- model 'openrouter/anthropic/claude-sonnet-4.6'
32
- provider_options api_key: ENV['MY_OR_KEY']
32
+ Riffer.configure do |config|
33
+ config.openrouter.client = OpenAI::Client.new(
34
+ api_key: ENV['OPENROUTER_API_KEY'],
35
+ base_url: 'https://openrouter.ai/api/v1',
36
+ timeout: 60
37
+ )
33
38
  end
34
39
  ```
35
40
 
36
- The `api_key` resolves in order: keyword arg → `Riffer.config.openrouter.api_key` → `ENV['OPENROUTER_API_KEY']`.
41
+ The setting accepts a client instance or a no-argument `Proc`, resolved on every LLM call — see [Configuration → Provider Clients](../CONFIGURATION.md#provider-clients).
37
42
 
38
43
  ## Supported Models
39
44
 
@@ -229,7 +234,7 @@ User messages with files become multi-part content (`image_url` for images, `fil
229
234
  ## Direct Provider Usage
230
235
 
231
236
  ```ruby
232
- provider = Riffer::Providers::OpenRouter.new(api_key: ENV['OPENROUTER_API_KEY'])
237
+ provider = Riffer::Providers::OpenRouter.new
233
238
 
234
239
  response = provider.generate_text(
235
240
  prompt: 'Hello!',
@@ -30,6 +30,21 @@ class MyAgent < Riffer::Agent
30
30
  end
31
31
  ```
32
32
 
33
+ ## Credentials and Clients
34
+
35
+ Every credential comes from configuration: `Riffer::Providers::OpenAI.new` takes no arguments.
36
+
37
+ | Provider | Configured credentials |
38
+ | -------------- | ----------------------------------------------------------------- |
39
+ | OpenAI | `config.openai.api_key`, `config.openai.base_url` |
40
+ | Azure OpenAI | `config.azure_openai.api_key`, `config.azure_openai.endpoint` |
41
+ | Anthropic | `config.anthropic.api_key` |
42
+ | Amazon Bedrock | `config.amazon_bedrock.api_token`, `config.amazon_bedrock.region` |
43
+ | Gemini | `config.gemini.api_key` |
44
+ | OpenRouter | `config.openrouter.api_key` |
45
+
46
+ Out of the box, each provider builds an SDK client from these credentials. Everything else — timeouts, retries, proxies, custom auth — is configured by assigning your own client (an instance, or a `Proc` resolved on every LLM call) to `Riffer.config.<provider>.client`. See [Configuration → Provider Clients](../CONFIGURATION.md#provider-clients).
47
+
33
48
  ## Provider Interface
34
49
 
35
50
  All providers inherit from `Riffer::Providers::Base` and implement:
@@ -39,7 +54,7 @@ All providers inherit from `Riffer::Providers::Base` and implement:
39
54
  Generates a response synchronously:
40
55
 
41
56
  ```ruby
42
- provider = Riffer::Providers::OpenAI.new(api_key: "...")
57
+ provider = Riffer::Providers::OpenAI.new
43
58
 
44
59
  response = provider.generate_text(
45
60
  prompt: "Hello!",
@@ -16,9 +16,6 @@ class Riffer::Agent::Config
16
16
  # The configured instructions.
17
17
  attr_reader :instructions #: (String | Proc)?
18
18
 
19
- # Options passed to the provider client.
20
- attr_accessor :provider_options #: Hash[Symbol, untyped]
21
-
22
19
  # Options passed to generate_text/stream_text.
23
20
  attr_accessor :model_options #: Hash[Symbol, untyped]
24
21
 
@@ -50,7 +47,6 @@ class Riffer::Agent::Config
50
47
  # ?identifier: String?,
51
48
  # ?model: (String | Proc)?,
52
49
  # ?instructions: (String | Proc)?,
53
- # ?provider_options: Hash[Symbol, untyped],
54
50
  # ?model_options: Hash[Symbol, untyped],
55
51
  # ?structured_output: Riffer::Params?,
56
52
  # ?max_steps: Numeric?,
@@ -64,7 +60,6 @@ class Riffer::Agent::Config
64
60
  identifier: nil,
65
61
  model: nil,
66
62
  instructions: nil,
67
- provider_options: {},
68
63
  model_options: {},
69
64
  structured_output: nil,
70
65
  max_steps: DEFAULT_MAX_STEPS,
@@ -74,7 +69,6 @@ class Riffer::Agent::Config
74
69
  skills_config: nil,
75
70
  guardrails: { before: [], after: [] }
76
71
  )
77
- @provider_options = provider_options
78
72
  @model_options = model_options
79
73
  @max_steps = max_steps
80
74
  @tools_config = tools_config
@@ -465,10 +465,8 @@ module Riffer::Agent::Run
465
465
  tripwire = response.tripwire
466
466
  return unless tripwire
467
467
 
468
- class_name = tripwire.guardrail.name
469
- if class_name
470
- span.set_attribute("riffer.tripwire.guardrail", Riffer::Helpers::ClassNameConverter.convert(class_name))
471
- end
468
+ identifier = Riffer::Helpers::Identifier.for(tripwire.guardrail)
469
+ span.set_attribute("riffer.tripwire.guardrail", identifier) unless identifier.empty?
472
470
  span.set_attribute("riffer.tripwire.reason", tripwire.reason)
473
471
  span.set_attribute("riffer.tripwire.phase", tripwire.phase.to_s)
474
472
  end
@@ -37,7 +37,6 @@ module Riffer::Agent::Serializer
37
37
  model: "#{agent.provider_name}/#{agent.model_name}",
38
38
  instructions: agent.instruction_message&.content,
39
39
  model_options: config.model_options,
40
- provider_options: config.provider_options,
41
40
  max_steps: encode_max_steps(config.max_steps),
42
41
  structured_output: config.structured_output&.to_json_schema(strict: false),
43
42
  tools: agent.tools.map { |tool_class| tool_descriptor(tool_class) },
@@ -96,7 +95,6 @@ module Riffer::Agent::Serializer
96
95
  identifier: hash[:identifier],
97
96
  model: hash[:model],
98
97
  instructions: hash[:instructions],
99
- provider_options: hash[:provider_options] || {},
100
98
  model_options: hash[:model_options] || {},
101
99
  structured_output: decode_structured_output(hash[:structured_output]),
102
100
  max_steps: decode_max_steps(hash),
data/lib/riffer/agent.rb CHANGED
@@ -15,6 +15,8 @@ require "json"
15
15
  # agent.generate('Hello!')
16
16
  #
17
17
  class Riffer::Agent
18
+ extend Riffer::Registrable
19
+
18
20
  # @rbs self.@config: Riffer::Agent::Config?
19
21
 
20
22
  INTERRUPT_MAX_STEPS = :max_steps #: Symbol
@@ -31,7 +33,7 @@ class Riffer::Agent
31
33
  #--
32
34
  #: (?String?) -> String
33
35
  def self.identifier(value = nil)
34
- value.nil? ? (config.identifier || Riffer::Helpers::ClassNameConverter.convert(name)) : (config.identifier = value)
36
+ value.nil? ? (config.identifier || Riffer::Helpers::Identifier.for(self)) : (config.identifier = value)
35
37
  end
36
38
 
37
39
  # Gets or sets the model string (e.g., "openai/gpt-4o").
@@ -57,14 +59,6 @@ class Riffer::Agent
57
59
  value.nil? ? config.instructions : (config.instructions = value)
58
60
  end
59
61
 
60
- # Gets or sets provider options passed to the provider client.
61
- #
62
- #--
63
- #: (?Hash[Symbol, untyped]?) -> Hash[Symbol, untyped]
64
- def self.provider_options(options = nil)
65
- options.nil? ? config.provider_options : (config.provider_options = options)
66
- end
67
-
68
62
  # Gets or sets model options passed to generate_text/stream_text.
69
63
  #
70
64
  #--
@@ -152,22 +146,6 @@ class Riffer::Agent
152
146
  config.skills_config
153
147
  end
154
148
 
155
- # Finds an agent class by identifier.
156
- #
157
- #--
158
- #: (String) -> singleton(Riffer::Agent)?
159
- def self.find(identifier)
160
- all.find { |agent_class| agent_class.identifier == identifier.to_s }
161
- end
162
-
163
- # Returns all agent subclasses.
164
- #
165
- #--
166
- #: () -> Array[singleton(Riffer::Agent)]
167
- def self.all
168
- subclasses #: Array[singleton(Riffer::Agent)]
169
- end
170
-
171
149
  # Generates a response using a new agent instance.
172
150
  #--
173
151
  #: (?String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?, ?context: Hash[Symbol, untyped]?, ?tags: Hash[(String | Symbol), untyped]) -> Riffer::Agent::Response
@@ -408,7 +386,7 @@ class Riffer::Agent
408
386
  provider_class = Riffer::Providers::Repository.find(@provider_name)
409
387
  raise Riffer::ArgumentError, "Provider not found: #{@provider_name}" unless provider_class
410
388
 
411
- provider_class.new(**@config.provider_options)
389
+ provider_class.new
412
390
  end
413
391
 
414
392
  #--
data/lib/riffer/config.rb CHANGED
@@ -3,12 +3,12 @@
3
3
 
4
4
  # Configuration for the Riffer framework.
5
5
  class Riffer::Config
6
- AmazonBedrock = Struct.new(:api_token, :region)
7
- Anthropic = Struct.new(:api_key)
8
- AzureOpenAI = Struct.new(:api_key, :endpoint)
9
- Gemini = Struct.new(:api_key, :open_timeout, :read_timeout)
10
- OpenAI = Struct.new(:api_key)
11
- OpenRouter = Struct.new(:api_key)
6
+ AmazonBedrock = Struct.new(:api_token, :region, :client)
7
+ Anthropic = Struct.new(:api_key, :client)
8
+ AzureOpenAI = Struct.new(:api_key, :endpoint, :client)
9
+ Gemini = Struct.new(:api_key, :client)
10
+ OpenAI = Struct.new(:api_key, :base_url, :client)
11
+ OpenRouter = Struct.new(:api_key, :client)
12
12
  Evals = Struct.new(:judge_model)
13
13
  Mcp = Struct.new(:credentials, :discovery_runner)
14
14
 
@@ -6,7 +6,6 @@ require "json"
6
6
  # Executes LLM-as-judge evaluations, using tool calling internally to get
7
7
  # structured output from the judge model.
8
8
  class Riffer::Evals::Judge
9
- # @rbs @provider_options: Hash[Symbol, untyped]
10
9
  # @rbs @provider_instance: Riffer::Providers::Base?
11
10
  # @rbs @provider_name: String?
12
11
  # @rbs @model_name: String?
@@ -33,15 +32,14 @@ class Riffer::Evals::Judge
33
32
 
34
33
  # Raises Riffer::ArgumentError unless +model+ is "provider/model" format.
35
34
  #--
36
- #: (model: String, ?provider_options: Hash[Symbol, untyped]) -> void
37
- def initialize(model:, provider_options: {})
35
+ #: (model: String) -> void
36
+ def initialize(model:)
38
37
  provider_name, model_name = model.split("/", 2)
39
38
  unless [provider_name, model_name].all? { |part| part.is_a?(String) && !part.strip.empty? }
40
39
  raise Riffer::ArgumentError, "Invalid model string: #{model}"
41
40
  end
42
41
 
43
42
  @model = model
44
- @provider_options = provider_options
45
43
  end
46
44
 
47
45
  # Evaluates an input/output pair using the configured LLM.
@@ -92,7 +90,7 @@ class Riffer::Evals::Judge
92
90
  provider_class = Riffer::Providers::Repository.find(provider_name)
93
91
  raise Riffer::ArgumentError, "Provider not found: #{provider_name}" unless provider_class
94
92
 
95
- provider_class.new(**@provider_options)
93
+ provider_class.new
96
94
  end
97
95
  end
98
96
 
@@ -36,7 +36,7 @@ class Riffer::Guardrail
36
36
  #--
37
37
  #: () -> String
38
38
  def name
39
- Riffer::Helpers::ClassNameConverter.convert(self.class.name)
39
+ Riffer::Helpers::Identifier.for(self.class)
40
40
  end
41
41
 
42
42
  protected
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+ # rbs_inline: enabled
3
+
4
+ # Helper module for deriving snake_case identifiers from class names.
5
+ module Riffer::Helpers::Identifier
6
+ extend self
7
+
8
+ # Derives a snake_case identifier from a class name string.
9
+ #
10
+ #--
11
+ #: (String?) -> String
12
+ def derive(class_name)
13
+ class_name.
14
+ to_s.
15
+ gsub("::", "/").
16
+ gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2').
17
+ gsub(/([a-z\d])([A-Z])/, '\1_\2').
18
+ downcase
19
+ end
20
+
21
+ # Derives and memoizes the identifier for a class or module. Anonymous
22
+ # classes return "" without caching, so a class named later still derives its
23
+ # real identifier — a guard that must travel with the cache, so callers never
24
+ # memoize their own.
25
+ #
26
+ #--
27
+ #: (Module) -> String
28
+ def for(klass)
29
+ cached = klass.instance_variable_get(:@derived_identifier) #: String?
30
+ return cached if cached
31
+
32
+ # Tool classes shadow Module#name with the identifier DSL, so the real
33
+ # class-path name must come from Module's own implementation.
34
+ real_name = Module.instance_method(:name).bind_call(klass) #: String?
35
+ return "" if real_name.nil?
36
+
37
+ derived = derive(real_name)
38
+ klass.instance_variable_set(:@derived_identifier, derived)
39
+ derived
40
+ end
41
+ end
@@ -37,28 +37,40 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
37
37
  end
38
38
 
39
39
  #--
40
- #: (?api_token: String?, ?region: String?, **untyped) -> void
41
- def initialize(api_token: nil, region: nil, **)
42
- super()
40
+ #: () -> void
41
+ def initialize
42
+ super
43
43
  depends_on "aws-sdk-bedrockruntime"
44
-
45
- api_token ||= Riffer.config.amazon_bedrock.api_token
46
- region ||= Riffer.config.amazon_bedrock.region
47
-
48
- @client = if api_token && !api_token.empty?
49
- Aws::BedrockRuntime::Client.new(
50
- region: region,
51
- token_provider: Aws::StaticTokenProvider.new(api_token),
52
- auth_scheme_preference: ["httpBearerAuth"],
53
- **,
54
- )
55
- else
56
- Aws::BedrockRuntime::Client.new(region: region, **)
57
- end
58
44
  end
59
45
 
60
46
  private
61
47
 
48
+ #--
49
+ #: () -> untyped
50
+ def global_client
51
+ Riffer.config.amazon_bedrock.client
52
+ end
53
+
54
+ # Compacted so an unset region stays absent: the AWS SDK resolves +AWS_REGION+
55
+ # and the shared config only for a missing argument, and raises
56
+ # +Aws::Errors::MissingRegionError+ on an explicit nil.
57
+ #--
58
+ #: () -> untyped
59
+ def build_client
60
+ api_token = Riffer.config.amazon_bedrock.api_token
61
+ region = Riffer.config.amazon_bedrock.region
62
+
63
+ if api_token && !api_token.empty?
64
+ Aws::BedrockRuntime::Client.new(**{
65
+ region: region,
66
+ token_provider: Aws::StaticTokenProvider.new(api_token),
67
+ auth_scheme_preference: ["httpBearerAuth"],
68
+ }.compact)
69
+ else
70
+ Aws::BedrockRuntime::Client.new(**{ region: region }.compact)
71
+ end
72
+ end
73
+
62
74
  #--
63
75
  #: (Array[Riffer::Messages::Base], String?, Hash[Symbol, untyped]) -> Hash[Symbol, untyped]
64
76
  def build_request_params(messages, model, options)
@@ -137,7 +149,7 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
137
149
  #--
138
150
  #: (Hash[Symbol, untyped]) -> untyped
139
151
  def execute_generate(params)
140
- @client.converse(**params)
152
+ client.converse(**params)
141
153
  end
142
154
 
143
155
  #--
@@ -227,7 +239,7 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
227
239
  tool_call: nil,
228
240
  } #: Hash[Symbol, untyped]
229
241
 
230
- @client.converse_stream(**params) do |stream|
242
+ client.converse_stream(**params) do |stream|
231
243
  stream.on_event do |event|
232
244
  case event
233
245
  when Aws::BedrockRuntime::Types::ContentBlockStartEvent