riffer 0.40.0 → 0.41.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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/.agents/providers.md +10 -1
  3. data/.agents/rbs-inline.md +2 -2
  4. data/.release-please-manifest.json +1 -1
  5. data/CHANGELOG.md +11 -0
  6. data/README.md +20 -24
  7. data/docs/AGENTS.md +0 -11
  8. data/docs/CONFIGURATION.md +105 -21
  9. data/docs/SERIALIZATION.md +4 -3
  10. data/docs/providers/AMAZON_BEDROCK.md +14 -8
  11. data/docs/providers/ANTHROPIC.md +9 -7
  12. data/docs/providers/AZURE_OPENAI.md +12 -12
  13. data/docs/providers/CUSTOM_PROVIDERS.md +32 -16
  14. data/docs/providers/GEMINI.md +28 -5
  15. data/docs/providers/OPENAI.md +21 -5
  16. data/docs/providers/OPENROUTER.md +11 -6
  17. data/docs/providers/PROVIDERS.md +16 -1
  18. data/lib/riffer/agent/config.rb +0 -6
  19. data/lib/riffer/agent/serializer.rb +0 -2
  20. data/lib/riffer/agent.rb +1 -9
  21. data/lib/riffer/config.rb +6 -6
  22. data/lib/riffer/evals/judge.rb +3 -5
  23. data/lib/riffer/providers/amazon_bedrock.rb +31 -19
  24. data/lib/riffer/providers/anthropic.rb +19 -8
  25. data/lib/riffer/providers/azure_open_ai.rb +19 -11
  26. data/lib/riffer/providers/base.rb +28 -0
  27. data/lib/riffer/providers/gemini/client.rb +120 -0
  28. data/lib/riffer/providers/gemini.rb +13 -62
  29. data/lib/riffer/providers/mock.rb +5 -4
  30. data/lib/riffer/providers/open_ai.rb +23 -8
  31. data/lib/riffer/providers/open_router.rb +23 -9
  32. data/lib/riffer/version.rb +1 -1
  33. data/sig/_private/riffer/providers/amazon_bedrock.rbs +4 -2
  34. data/sig/_private/riffer/providers/anthropic.rbs +4 -2
  35. data/sig/_private/riffer/providers/gemini.rbs +7 -0
  36. data/sig/_private/riffer/providers/open_ai.rbs +4 -2
  37. data/sig/_private/riffer/providers/open_router.rbs +4 -2
  38. data/sig/generated/riffer/agent/config.rbs +1 -5
  39. data/sig/generated/riffer/agent.rbs +0 -6
  40. data/sig/generated/riffer/config.rbs +25 -15
  41. data/sig/generated/riffer/evals/judge.rbs +2 -4
  42. data/sig/generated/riffer/providers/amazon_bedrock.rbs +13 -2
  43. data/sig/generated/riffer/providers/anthropic.rbs +13 -2
  44. data/sig/generated/riffer/providers/azure_open_ai.rbs +14 -4
  45. data/sig/generated/riffer/providers/base.rbs +20 -0
  46. data/sig/generated/riffer/providers/gemini/client.rbs +65 -0
  47. data/sig/generated/riffer/providers/gemini.rbs +7 -23
  48. data/sig/generated/riffer/providers/mock.rbs +4 -3
  49. data/sig/generated/riffer/providers/open_ai.rbs +13 -2
  50. data/sig/generated/riffer/providers/open_router.rbs +16 -3
  51. metadata +4 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6c89776aeb032fbf837df103366fed830edcc20769cc9d4958a3a9bfa56a7803
4
- data.tar.gz: 2153cf2967b9b70366987aeb9e59b215ff641bcd44668525c07d822ff9757579
3
+ metadata.gz: fd309de6be56bd5fbe778bbf277b9125dd63d4ce7b360c3e5e85554a944cb81d
4
+ data.tar.gz: 965daf888849a0386b2826af572fd95d0a7fb8a4ac6f9f91788acb0254705f8e
5
5
  SHA512:
6
- metadata.gz: edfb2a10f17e1c0f6fb0630f5c2244370e01c5f32d05e9f065183da0b5b2d2cfec2453cac9a08714bd921716cd747609e4db61fb105a57769d79ab2f8988356a
7
- data.tar.gz: 7d6dcdbab4be152033fcf75581252d3c9bda60c9e49825e75028d19670bae0490087ee8a90267a2af2f4fd8daabb5f370c8c2b7bb7cdcffe77a75b94a6b190ff
6
+ metadata.gz: deedbd28b2c79faceea167422fb69147cb2453d7e06df48c46c2eb1e601068de7b9018026675a973f6de6036373d324d5120fc43269e863dd43ce39b88ee7ea4
7
+ data.tar.gz: 2007cc98bd5955ae2db4776e72551d546038deea60be3e397a5838e028cd2901465f2fe9c00d944b18e4ce9508599bcd7603ad61e0e75efaa8264793461cf283
data/.agents/providers.md CHANGED
@@ -5,9 +5,18 @@
5
5
  1. Create `lib/riffer/providers/your_provider.rb` extending `Riffer::Providers::Base`
6
6
  2. Implement the required hook methods (see [Custom Providers](../docs/providers/CUSTOM_PROVIDERS.md) for the full API)
7
7
  3. Register in `Riffer::Providers::Repository::REPO`
8
- 4. Add provider config to `Riffer::Config` if needed
8
+ 4. Add provider config to `Riffer::Config` if needed — a `Struct` with credential members plus a `client` member
9
9
  5. Create tests in `test/riffer/providers/your_provider_test.rb`
10
10
 
11
+ ## Constructor and client contract
12
+
13
+ - Constructors take **no arguments** — define one only when the provider needs `depends_on`, and give it no parameters. Credentials live in config; never accept them as kwargs or hold them in ivars.
14
+ - Never hold a client ivar; call the private `client` method from `execute_generate`/`execute_stream`. Base resolves: `global_client` (a client instance, or a no-argument Proc resolved on every call) → memoized `build_client`.
15
+ - Providers hold no agent state — no context, no reference to the owning agent. Client selection is process-global by design, so a configured Proc takes no arguments.
16
+ - Implement `build_client` (build the SDK client by reading `Riffer.config.<provider>.<credential>` directly) and override `global_client` to return `Riffer.config.<provider>.client`.
17
+ - **Never pass an SDK an explicit nil credential.** Build the kwargs as a hash and `.compact` it, so an unset value stays _absent_: SDKs distinguish absent from nil to decide whether to read their own env vars, and an explicit nil suppresses that. Passing `base_url: nil` skips `OPENAI_BASE_URL` and pins requests to api.openai.com; passing `region: nil` makes the AWS SDK raise `MissingRegionError` even with `AWS_REGION` exported.
18
+ - **Exception — a provider borrowing another vendor's SDK** (`OpenRouter` and `AzureOpenAI` reuse `::OpenAI::Client`) must keep its credential and endpoint concrete, nil included. Compacting there would let the OpenAI SDK fall back to `OPENAI_API_KEY` / `OPENAI_BASE_URL` and send one vendor's credential to another's endpoint. Compact a key only when the SDK's env fallback for it names the same service the provider talks to.
19
+
11
20
  ## Architecture
12
21
 
13
22
  The base class uses the **template method** pattern. The public methods `generate_text` and `stream_text` orchestrate the flow, delegating to hook methods that each provider implements:
@@ -154,8 +154,8 @@ end
154
154
 
155
155
  ### Where stubs and stdlib deps live
156
156
 
157
- - `sig/_private/` — signatures that must **not** ship. RBS **skips** `_`-prefixed directories in library mode, so consumers never load them; riffer's own `steep check` does (via the `Steepfile`). Two kinds, by predictable path: external-gem signatures are named by gem at the top level (`async.rbs`, `mcp.rbs`, `zeitwerk.rbs`, `openai.rbs`, `anthropic.rbs`, `aws-sdk-core/*` — full stubs for RBS-less gems plus arity patches for the provider SDKs); riffer's own hidden stubs mirror `lib/` under `riffer/` (e.g. `riffer/providers/anthropic.rbs` declares the SDK-typed `@client` ivar).
158
- - `sig/manual/` — hand-written riffer-only signatures that are **safe to ship**, for the few things rbs-inline can't generate _at all_ (mirroring `lib/`). In practice that's `extend self` modules (`riffer/agent/run.rbs`, `riffer/helpers/call_or_value.rbs`) and modeling an include applied dynamically (`riffer/tools/toolable.rbs`). SDK-free ivars are **not** hand-written here — declare them inline with `# @rbs` (see "Instance variables"). SDK-typed ivars can't ship, so they go in `_private/riffer/providers/` (`@client`).
157
+ - `sig/_private/` — signatures that must **not** ship. RBS **skips** `_`-prefixed directories in library mode, so consumers never load them; riffer's own `steep check` does (via the `Steepfile`). Two kinds, by predictable path: external-gem signatures are named by gem at the top level (`async.rbs`, `mcp.rbs`, `zeitwerk.rbs`, `openai.rbs`, `anthropic.rbs`, `aws-sdk-core/*` — full stubs for RBS-less gems plus arity patches for the provider SDKs); riffer's own hidden stubs mirror `lib/` under `riffer/` (e.g. `riffer/providers/anthropic.rbs` narrows the private `client` method to the SDK-typed client).
158
+ - `sig/manual/` — hand-written riffer-only signatures that are **safe to ship**, for the few things rbs-inline can't generate _at all_ (mirroring `lib/`). In practice that's `extend self` modules (`riffer/agent/run.rbs`, `riffer/helpers/call_or_value.rbs`) and modeling an include applied dynamically (`riffer/tools/toolable.rbs`). SDK-free ivars are **not** hand-written here — declare them inline with `# @rbs` (see "Instance variables"). SDK-typed signatures can't ship, so they go in `_private/riffer/providers/` (the narrowed `client`).
159
159
  - `sig/manifest.yaml` — declares the **stdlib** RBS the shipped sigs reference (`uri`, `net-http`) so `rbs -r riffer` resolves them.
160
160
 
161
161
  ## Workflow
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "0.40.0"
2
+ ".": "0.41.0"
3
3
  }
data/CHANGELOG.md CHANGED
@@ -5,6 +5,17 @@ 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.41.0](https://github.com/janeapp/riffer/compare/riffer/v0.40.0...riffer/v0.41.0) (2026-08-14)
9
+
10
+
11
+ ### ⚠ BREAKING CHANGES
12
+
13
+ * Inference providers no longer support extra configuration parameters (e.g. timeouts). Instead, a provider client object can be defined in the riffer config directly. Credential and endpoint parameters are still supported.
14
+
15
+ ### Features
16
+
17
+ * config-injected provider clients, retire provider_options ([#391](https://github.com/janeapp/riffer/issues/391)) ([72fa0e3](https://github.com/janeapp/riffer/commit/72fa0e32ff6cfeee29bae9e2618d001cbc387d99))
18
+
8
19
  ## [0.40.0](https://github.com/janeapp/riffer/compare/riffer/v0.39.0...riffer/v0.40.0) (2026-08-12)
9
20
 
10
21
 
data/README.md CHANGED
@@ -45,31 +45,31 @@ puts agent.generate('Hello world')
45
45
 
46
46
  ## Documentation
47
47
 
48
- Comprehensive guides live at [docs.riffer.ai](https://docs.riffer.ai):
49
-
50
- - [Overview](https://docs.riffer.ai/guides/overview/) - Core concepts and architecture
51
- - [Getting Started](https://docs.riffer.ai/guides/getting-started/) - Installation and first steps
52
- - [Agents](https://docs.riffer.ai/guides/agents/) - Defining and configuring agents
53
- - [Agent Lifecycle](https://docs.riffer.ai/guides/agent-lifecycle/) - Generate, stream, and responses
54
- - [Agent Loop](https://docs.riffer.ai/guides/agent-loop/) - Tool execution flow and stopping
55
- - [Tools](https://docs.riffer.ai/guides/tools/) - Creating tools for agents
56
- - [Advanced Tools](https://docs.riffer.ai/guides/advanced-tools/) - Timeouts, runtime, and registration
57
- - [Messages](https://docs.riffer.ai/guides/messages/) - Message types and formats
58
- - [Stream Events](https://docs.riffer.ai/guides/stream-events/) - Streaming responses
59
- - [Configuration](https://docs.riffer.ai/guides/configuration/) - Framework configuration
60
- - [Evals](https://docs.riffer.ai/guides/evals/) - Evaluating agent quality
61
- - [Guardrails](https://docs.riffer.ai/guides/guardrails/) - Input/output validation
62
- - [Skills](https://docs.riffer.ai/guides/skills/) - Packaged agent capabilities
63
- - [MCP](https://docs.riffer.ai/guides/mcp/) - Integrating third-party MCP servers
64
- - [Serialization](https://docs.riffer.ai/guides/serialization/) - Persisting and transferring agent definitions
65
- - [Tracing](https://docs.riffer.ai/guides/tracing/) - OpenTelemetry span contract and host wiring
66
- - [Providers](https://docs.riffer.ai/guides/providers/overview/) - LLM provider adapters
48
+ Comprehensive guides live at [riffer.ai](https://riffer.ai):
49
+
50
+ - [Overview](https://riffer.ai/guides/overview/) - Core concepts and architecture
51
+ - [Getting Started](https://riffer.ai/guides/getting-started/) - Installation and first steps
52
+ - [Agents](https://riffer.ai/guides/agents/) - Defining and configuring agents
53
+ - [Agent Lifecycle](https://riffer.ai/guides/agent-lifecycle/) - Generate, stream, and responses
54
+ - [Agent Loop](https://riffer.ai/guides/agent-loop/) - Tool execution flow and stopping
55
+ - [Tools](https://riffer.ai/guides/tools/) - Creating tools for agents
56
+ - [Advanced Tools](https://riffer.ai/guides/advanced-tools/) - Timeouts, runtime, and registration
57
+ - [Messages](https://riffer.ai/guides/messages/) - Message types and formats
58
+ - [Stream Events](https://riffer.ai/guides/stream-events/) - Streaming responses
59
+ - [Configuration](https://riffer.ai/guides/configuration/) - Framework configuration
60
+ - [Evals](https://riffer.ai/guides/evals/) - Evaluating agent quality
61
+ - [Guardrails](https://riffer.ai/guides/guardrails/) - Input/output validation
62
+ - [Skills](https://riffer.ai/guides/skills/) - Packaged agent capabilities
63
+ - [MCP](https://riffer.ai/guides/mcp/) - Integrating third-party MCP servers
64
+ - [Serialization](https://riffer.ai/guides/serialization/) - Persisting and transferring agent definitions
65
+ - [Tracing](https://riffer.ai/guides/tracing/) - OpenTelemetry span contract and host wiring
66
+ - [Providers](https://riffer.ai/guides/providers/overview/) - LLM provider adapters
67
67
 
68
68
  The guide sources are in the [docs](docs/) directory.
69
69
 
70
70
  ### API Reference
71
71
 
72
- The full API reference is published at [docs.riffer.ai/api](https://docs.riffer.ai/api/). Preview the site locally with:
72
+ The full API reference is published at [riffer.ai/api](https://riffer.ai/api/). Preview the site locally with:
73
73
 
74
74
  ```bash
75
75
  bin/rake docs:serve
@@ -140,7 +140,3 @@ All notable changes to this project are documented in `CHANGELOG.md`.
140
140
  ## License
141
141
 
142
142
  Licensed under the MIT License. See `LICENSE.txt` for details.
143
-
144
- ## Maintainers
145
-
146
- - Jake Bottrall - https://github.com/bottrall
data/docs/AGENTS.md CHANGED
@@ -128,17 +128,6 @@ end
128
128
 
129
129
  Loads tools from registered [MCP](MCP.md) servers by tag. Like `uses_tools`, **`use_mcp` is not inherited**—add it on each subclass that should include MCP tools.
130
130
 
131
- ### provider_options
132
-
133
- Passes options to the provider client:
134
-
135
- ```ruby
136
- class MyAgent < Riffer::Agent
137
- model 'openai/gpt-5-mini'
138
- provider_options api_key: ENV['CUSTOM_OPENAI_KEY']
139
- end
140
- ```
141
-
142
131
  ### model_options
143
132
 
144
133
  Passes options to each LLM request:
@@ -9,12 +9,15 @@ Use `Riffer.configure` to set up provider credentials:
9
9
  ```ruby
10
10
  Riffer.configure do |config|
11
11
  config.openai.api_key = ENV['OPENAI_API_KEY']
12
+ config.openai.base_url = ENV['OPENAI_BASE_URL'] # Optional — gateways, proxies
12
13
  config.amazon_bedrock.region = 'us-east-1'
13
14
  config.amazon_bedrock.api_token = ENV['BEDROCK_API_TOKEN']
14
15
  config.anthropic.api_key = ENV['ANTHROPIC_API_KEY']
15
16
  end
16
17
  ```
17
18
 
19
+ Providers take no constructor arguments — these settings are the only way to give a provider its credentials.
20
+
18
21
  ## Accessing Configuration
19
22
 
20
23
  Access the current configuration via `Riffer.config`:
@@ -34,6 +37,66 @@ Riffer.config.anthropic.api_key
34
37
 
35
38
  For provider credentials and setup, see the individual [Provider guides](providers/PROVIDERS.md).
36
39
 
40
+ ### Provider Clients
41
+
42
+ Out of the box, each provider builds a default SDK client from its configured credentials. For anything beyond credentials — timeouts, retries, proxies, gateways — assign your own client to `config.<provider>.client`:
43
+
44
+ ```ruby
45
+ Riffer.configure do |config|
46
+ config.openai.client = OpenAI::Client.new(
47
+ api_key: ENV['OPENAI_API_KEY'],
48
+ timeout: 30,
49
+ max_retries: 4
50
+ )
51
+ config.gemini.client = Riffer::Providers::Gemini::Client.new(
52
+ api_key: ENV['GEMINI_API_KEY'],
53
+ read_timeout: 120
54
+ )
55
+ end
56
+ ```
57
+
58
+ Every provider accepts a client instance or a `Proc` returning one:
59
+
60
+ | Provider | Setting | Default client built from credentials |
61
+ | -------------- | ------------------------------ | ------------------------------------------------------------- |
62
+ | OpenAI | `config.openai.client` | `OpenAI::Client` |
63
+ | Azure OpenAI | `config.azure_openai.client` | `OpenAI::Client` (with the Azure endpoint as `base_url`) |
64
+ | Anthropic | `config.anthropic.client` | `Anthropic::Client` |
65
+ | Amazon Bedrock | `config.amazon_bedrock.client` | `Aws::BedrockRuntime::Client` |
66
+ | Gemini | `config.gemini.client` | `Riffer::Providers::Gemini::Client` (riffer-owned, see below) |
67
+ | OpenRouter | `config.openrouter.client` | `OpenAI::Client` (pinned to the OpenRouter endpoint) |
68
+
69
+ A `Proc` takes **no arguments** and is resolved on **every LLM call**, never cached by riffer — memoize inside the Proc when construction is expensive. This makes the Proc the right tool for:
70
+
71
+ - **Fork safety** (Puma clustered, Sidekiq swarm): a client built at boot holds sockets that break across `fork`; build (and cache) per process instead.
72
+ - **Expiring credentials** (Azure AD tokens, STS-vended keys): re-resolve before they go stale.
73
+
74
+ ```ruby
75
+ Riffer.configure do |config|
76
+ # Fork-safe shared client: one per process, built on first use after fork.
77
+ config.anthropic.client = -> {
78
+ ClientRegistry.anthropic_for(Process.pid)
79
+ }
80
+
81
+ # Re-resolved before the token goes stale.
82
+ config.azure_openai.client = -> {
83
+ OpenAI::Client.new(api_key: AzureAd.current_token, base_url: ENV['AZURE_OPENAI_ENDPOINT'])
84
+ }
85
+ end
86
+ ```
87
+
88
+ Because the Proc receives no arguments, it can only vary the client by process-wide state — it cannot route per agent or per request. Client selection is a global concern; to talk to different accounts or endpoints from different agents, register a provider subclass with its own config (see [Multiple Configurations](#multiple-configurations)).
89
+
90
+ A configured client always wins over configured credentials: once `config.<provider>.client` is set, the credential members are unused, since riffer no longer builds the client.
91
+
92
+ ### Falling through to the SDK
93
+
94
+ A credential you leave unset in riffer is omitted from the default client rather than passed as `nil`, so each vendor SDK still applies its own resolution. `OPENAI_API_KEY` / `OPENAI_BASE_URL`, `ANTHROPIC_API_KEY`, and the AWS chains — region from `AWS_REGION` / `AWS_DEFAULT_REGION` / shared config, credentials from the environment, the shared credentials file, or an instance/task IAM role — all work with no riffer configuration at all.
95
+
96
+ Two providers deliberately opt out: `OpenRouter` and `AzureOpenAI` borrow `OpenAI::Client` to reach a **different** vendor, so they always pass their credential and endpoint explicitly. Falling through would let the OpenAI SDK pick up `OPENAI_API_KEY` / `OPENAI_BASE_URL` and send an OpenAI credential to `openrouter.ai` or your Azure endpoint. With nothing configured they raise instead — set `config.openrouter.api_key` / `OPENROUTER_API_KEY`, or `config.azure_openai.api_key` and `.endpoint` / `AZURE_OPENAI_API_KEY` and `AZURE_OPENAI_ENDPOINT`.
97
+
98
+ The Gemini provider has no vendor SDK, so riffer ships its own transport: `Riffer::Providers::Gemini::Client` exposes `base_url`, `open_timeout`, `read_timeout`, `write_timeout`, and `proxy_address`/`proxy_port`. Anything implementing its two-method contract (`post`, `post_stream`) can be assigned to `config.gemini.client` — see [Gemini](providers/GEMINI.md).
99
+
37
100
  ### MCP (Model Context Protocol)
38
101
 
39
102
  Optional settings for [MCP server integrations](MCP.md):
@@ -113,10 +176,10 @@ Riffer.configure do |config|
113
176
  end
114
177
  ```
115
178
 
116
- | Option | Description |
117
- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
118
- | `enabled` | The kill switch, consulted on every span — flipping it at runtime takes effect immediately, short-circuiting to a no-op ahead of the backend. Accepts booleans or `'true'`/`'false'`/`'1'`/`'0'`. Defaults to `true`. |
119
- | `capture_messages` | Opt-in capture of full message content on LLM-call spans (`gen_ai.input.messages`, `gen_ai.output.messages`, `gen_ai.system_instructions`) as GenAI-semconv JSON. Defaults to `false` — message content routinely carries sensitive data. File attachments serialize as metadata-only stubs (media type and name, never bytes), and riffer applies no size limit of its own — cap oversized attributes with the OTEL SDK attribute length limits. |
179
+ | Option | Description |
180
+ | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
181
+ | `enabled` | The kill switch, consulted on every span — flipping it at runtime takes effect immediately, short-circuiting to a no-op ahead of the backend. Accepts booleans or `'true'`/`'false'`/`'1'`/`'0'`. Defaults to `true`. |
182
+ | `capture_messages` | Opt-in capture of full message content on LLM-call spans (`gen_ai.input.messages`, `gen_ai.output.messages`, `gen_ai.system_instructions`) as GenAI-semconv JSON. Defaults to `false` — message content routinely carries sensitive data. File attachments serialize as metadata-only stubs (media type and name, never bytes), and riffer applies no size limit of its own — cap oversized attributes with the OTEL SDK attribute length limits. |
120
183
  | `backend` | The backend riffer routes spans through. Assign `Riffer::Tracing::Otel.build` (pass `provider:` to override the global tracer provider — e.g. an in-memory provider in tests), or any object satisfying the duck-typed contract (`in_span` / `current_context` / `with_context`) to route into a non-OTEL system (e.g. Datadog APM). Defaults to `nil` — a no-op. Raises `Riffer::ArgumentError` unless the value is `nil` or responds to `in_span`. See [Tracing → Routing to a non-OpenTelemetry backend](TRACING.md#routing-to-a-non-opentelemetry-backend). |
121
184
 
122
185
  ### Pricing
@@ -200,19 +263,6 @@ There is no per-call override and no customizable placeholder. Callers needing f
200
263
 
201
264
  Override global configuration at the agent level:
202
265
 
203
- ### provider_options
204
-
205
- Pass options directly to the provider client:
206
-
207
- ```ruby
208
- class MyAgent < Riffer::Agent
209
- model 'openai/gpt-5-mini'
210
-
211
- # Override API key for this agent only
212
- provider_options api_key: ENV['CUSTOM_OPENAI_KEY']
213
- end
214
- ```
215
-
216
266
  ### model_options
217
267
 
218
268
  Pass options to each LLM request:
@@ -305,17 +355,51 @@ end
305
355
 
306
356
  ## Multiple Configurations
307
357
 
308
- For different environments or use cases, use agent-level overrides:
358
+ Provider credentials and clients are **global**, resolved per process. For different environments, branch at boot:
359
+
360
+ ```ruby
361
+ Riffer.configure do |config|
362
+ config.openai.client = if Rails.env.production?
363
+ OpenAI::Client.new(api_key: ENV['PRODUCTION_OPENAI_KEY'], max_retries: 4)
364
+ else
365
+ OpenAI::Client.new(api_key: ENV['DEV_OPENAI_KEY'], timeout: 10)
366
+ end
367
+ end
368
+ ```
369
+
370
+ When a single process has to reach two different accounts or endpoints, give the second one its own provider class and config, then register it under its own identifier:
371
+
372
+ ```ruby
373
+ class InternalOpenAI < Riffer::Providers::OpenAI
374
+ InternalConfig = Struct.new(:api_key, :base_url, :client)
375
+
376
+ def self.config
377
+ @config ||= InternalConfig.new(ENV.fetch('INTERNAL_OPENAI_KEY'), ENV.fetch('INTERNAL_GATEWAY'))
378
+ end
379
+
380
+ private
381
+
382
+ def global_client
383
+ self.class.config.client
384
+ end
385
+
386
+ def build_client
387
+ ::OpenAI::Client.new(api_key: self.class.config.api_key, base_url: self.class.config.base_url)
388
+ end
389
+ end
390
+
391
+ Riffer::Providers::Repository.register(:internal_openai) { InternalOpenAI }
392
+ ```
393
+
394
+ Agents then select it by model prefix (`model 'internal_openai/gpt-5-mini'`), and the two accounts never interfere. What _can_ vary per agent is the model and the generation parameters:
309
395
 
310
396
  ```ruby
311
397
  class ProductionAgent < Riffer::Agent
312
398
  model 'openai/gpt-5-mini'
313
- provider_options api_key: ENV['PRODUCTION_OPENAI_KEY']
314
399
  end
315
400
 
316
401
  class DevelopmentAgent < Riffer::Agent
317
402
  model 'openai/gpt-5-mini'
318
- provider_options api_key: ENV['DEV_OPENAI_KEY']
319
- model_options temperature: 0.0 # Deterministic for testing
403
+ model_options temperature: 0.0 # Deterministic for testing
320
404
  end
321
405
  ```
@@ -1,6 +1,6 @@
1
1
  # Serialization
2
2
 
3
- `Riffer::Agent::Serializer` turns a **resolved agent** into a self-contained, provider-neutral data hash (`to_h`) and reconstructs a **runnable agent** from that hash (`from_h`). Use it to persist agent definitions outside of code, or to transfer them across a process/service boundary.
3
+ `Riffer::Agent::Serializer` turns a **resolved agent** into a provider-neutral data hash (`to_h`) and rebuilds an agent from that hash (`from_h`). The hash carries the agent **definition** — not the credentials or client it needs to reach a provider (see [Secrets](#secrets)). Use it to persist agent definitions outside of code, or to transfer them across a process/service boundary.
4
4
 
5
5
  You normally reach it through the delegators on `Riffer::Agent`:
6
6
 
@@ -43,7 +43,6 @@ The session is used **as-is**: the rebuilt agent does not prepend anything to it
43
43
  model: "openai/gpt-4o", # resolved "provider/model" string
44
44
  instructions: "You are…", # resolved system prompt
45
45
  model_options: { temperature: 0.2 },
46
- provider_options: { … }, # see the secrets warning below
47
46
  max_steps: 8, # integer; -1 = unlimited (see below)
48
47
  structured_output: { type: "object", … }, # JSON Schema, or null
49
48
  tools: [ { name:, description:, parameters_schema:, timeout: }, … ]
@@ -101,7 +100,9 @@ A finite integer round-trips as-is; a hash missing the key falls back to the def
101
100
 
102
101
  ## Secrets
103
102
 
104
- `provider_options` and `model_options` **ride on the wire as plain data** — they are part of the hash and _will_ transfer. Prefer configuring API keys via environment/global provider configuration rather than `provider_options`. **Never serialize an agent whose options carry sensitive values** and if a serialized definition ever does, handle it as a secret (encrypt it, keep it out of logs).
103
+ `model_options` **rides on the wire as plain data** — it is part of the hash and _will_ transfer. Keep sensitive values out of it; credentials live in global provider configuration and never serialize.
104
+
105
+ The hash describes the agent, not how to authenticate as one. The destination process configures the provider itself — `config.<provider>.client`, or the credentials riffer builds a default client from. Clients resolve lazily on the first LLM call, so `from_h` succeeds in a process with no provider configuration; the `generate` is what fails. See [Configuration](CONFIGURATION.md).
105
106
 
106
107
  ## What does **not** transfer
107
108
 
@@ -22,6 +22,8 @@ Riffer.configure do |config|
22
22
  end
23
23
  ```
24
24
 
25
+ `region` resolves in order: `Riffer.config.amazon_bedrock.region` → the AWS SDK's own resolution (`AWS_REGION`, `AWS_DEFAULT_REGION`, shared config). Leaving it unset in riffer defers to the SDK rather than failing.
26
+
25
27
  ### Bearer Token Authentication
26
28
 
27
29
  For API token authentication:
@@ -33,15 +35,22 @@ Riffer.configure do |config|
33
35
  end
34
36
  ```
35
37
 
36
- Or per-agent:
38
+ ### Custom client
39
+
40
+ AWS auth beyond the bearer token or default credential chain (profiles, STS, IRSA), plus retries, timeouts, and endpoints, is configured on your own `Aws::BedrockRuntime::Client`:
37
41
 
38
42
  ```ruby
39
- class MyAgent < Riffer::Agent
40
- model 'amazon_bedrock/us.anthropic.claude-haiku-4-5-20251001-v1:0'
41
- provider_options region: 'us-west-2', api_token: ENV['BEDROCK_API_TOKEN']
43
+ Riffer.configure do |config|
44
+ config.amazon_bedrock.client = Aws::BedrockRuntime::Client.new(
45
+ region: 'us-east-1',
46
+ credentials: Aws::AssumeRoleCredentials.new(role_arn: ENV['BEDROCK_ROLE_ARN'], role_session_name: 'riffer'),
47
+ retry_limit: 5
48
+ )
42
49
  end
43
50
  ```
44
51
 
52
+ The setting accepts a client instance or a no-argument `Proc`, resolved on every LLM call — see [Configuration → Provider Clients](../CONFIGURATION.md#provider-clients).
53
+
45
54
  ## Supported Models
46
55
 
47
56
  Use Bedrock model IDs in the `amazon_bedrock/model` format:
@@ -168,10 +177,7 @@ The provider converts Riffer messages to Bedrock format:
168
177
  ## Direct Provider Usage
169
178
 
170
179
  ```ruby
171
- provider = Riffer::Providers::AmazonBedrock.new(
172
- region: 'us-east-1',
173
- api_token: ENV['BEDROCK_API_TOKEN'] # Optional
174
- )
180
+ provider = Riffer::Providers::AmazonBedrock.new
175
181
 
176
182
  response = provider.generate_text(
177
183
  prompt: "Hello!",
@@ -20,15 +20,19 @@ Riffer.configure do |config|
20
20
  end
21
21
  ```
22
22
 
23
- Or per-agent:
23
+ For anything beyond the API key — timeouts, retries, proxies — supply your own `Anthropic::Client`:
24
24
 
25
25
  ```ruby
26
- class MyAgent < Riffer::Agent
27
- model 'anthropic/claude-haiku-4-5-20251001'
28
- provider_options api_key: ENV['ANTHROPIC_API_KEY']
26
+ Riffer.configure do |config|
27
+ config.anthropic.client = Anthropic::Client.new(
28
+ api_key: ENV['ANTHROPIC_API_KEY'],
29
+ timeout: 60
30
+ )
29
31
  end
30
32
  ```
31
33
 
34
+ The setting accepts a client instance or a no-argument `Proc`, resolved on every LLM call — see [Configuration → Provider Clients](../CONFIGURATION.md#provider-clients).
35
+
32
36
  ## Supported Models
33
37
 
34
38
  Use Anthropic model IDs in the `anthropic/model` format:
@@ -224,9 +228,7 @@ The provider converts Riffer messages to Anthropic format:
224
228
  ## Direct Provider Usage
225
229
 
226
230
  ```ruby
227
- provider = Riffer::Providers::Anthropic.new(
228
- api_key: ENV['ANTHROPIC_API_KEY']
229
- )
231
+ provider = Riffer::Providers::Anthropic.new
230
232
 
231
233
  response = provider.generate_text(
232
234
  prompt: "Hello!",
@@ -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