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.
- checksums.yaml +4 -4
- data/.agents/providers.md +10 -1
- data/.agents/rbs-inline.md +2 -2
- data/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +11 -0
- data/README.md +20 -24
- data/docs/AGENTS.md +0 -11
- data/docs/CONFIGURATION.md +105 -21
- data/docs/SERIALIZATION.md +4 -3
- data/docs/providers/AMAZON_BEDROCK.md +14 -8
- data/docs/providers/ANTHROPIC.md +9 -7
- data/docs/providers/AZURE_OPENAI.md +12 -12
- data/docs/providers/CUSTOM_PROVIDERS.md +32 -16
- data/docs/providers/GEMINI.md +28 -5
- data/docs/providers/OPENAI.md +21 -5
- data/docs/providers/OPENROUTER.md +11 -6
- data/docs/providers/PROVIDERS.md +16 -1
- data/lib/riffer/agent/config.rb +0 -6
- data/lib/riffer/agent/serializer.rb +0 -2
- data/lib/riffer/agent.rb +1 -9
- data/lib/riffer/config.rb +6 -6
- data/lib/riffer/evals/judge.rb +3 -5
- data/lib/riffer/providers/amazon_bedrock.rb +31 -19
- data/lib/riffer/providers/anthropic.rb +19 -8
- data/lib/riffer/providers/azure_open_ai.rb +19 -11
- data/lib/riffer/providers/base.rb +28 -0
- data/lib/riffer/providers/gemini/client.rb +120 -0
- data/lib/riffer/providers/gemini.rb +13 -62
- data/lib/riffer/providers/mock.rb +5 -4
- data/lib/riffer/providers/open_ai.rb +23 -8
- data/lib/riffer/providers/open_router.rb +23 -9
- data/lib/riffer/version.rb +1 -1
- data/sig/_private/riffer/providers/amazon_bedrock.rbs +4 -2
- data/sig/_private/riffer/providers/anthropic.rbs +4 -2
- data/sig/_private/riffer/providers/gemini.rbs +7 -0
- data/sig/_private/riffer/providers/open_ai.rbs +4 -2
- data/sig/_private/riffer/providers/open_router.rbs +4 -2
- data/sig/generated/riffer/agent/config.rbs +1 -5
- data/sig/generated/riffer/agent.rbs +0 -6
- data/sig/generated/riffer/config.rbs +25 -15
- data/sig/generated/riffer/evals/judge.rbs +2 -4
- data/sig/generated/riffer/providers/amazon_bedrock.rbs +13 -2
- data/sig/generated/riffer/providers/anthropic.rbs +13 -2
- data/sig/generated/riffer/providers/azure_open_ai.rbs +14 -4
- data/sig/generated/riffer/providers/base.rbs +20 -0
- data/sig/generated/riffer/providers/gemini/client.rbs +65 -0
- data/sig/generated/riffer/providers/gemini.rbs +7 -23
- data/sig/generated/riffer/providers/mock.rbs +4 -3
- data/sig/generated/riffer/providers/open_ai.rbs +13 -2
- data/sig/generated/riffer/providers/open_router.rbs +16 -3
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fd309de6be56bd5fbe778bbf277b9125dd63d4ce7b360c3e5e85554a944cb81d
|
|
4
|
+
data.tar.gz: 965daf888849a0386b2826af572fd95d0a7fb8a4ac6f9f91788acb0254705f8e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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:
|
data/.agents/rbs-inline.md
CHANGED
|
@@ -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`
|
|
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
|
|
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
|
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 [
|
|
49
|
-
|
|
50
|
-
- [Overview](https://
|
|
51
|
-
- [Getting Started](https://
|
|
52
|
-
- [Agents](https://
|
|
53
|
-
- [Agent Lifecycle](https://
|
|
54
|
-
- [Agent Loop](https://
|
|
55
|
-
- [Tools](https://
|
|
56
|
-
- [Advanced Tools](https://
|
|
57
|
-
- [Messages](https://
|
|
58
|
-
- [Stream Events](https://
|
|
59
|
-
- [Configuration](https://
|
|
60
|
-
- [Evals](https://
|
|
61
|
-
- [Guardrails](https://
|
|
62
|
-
- [Skills](https://
|
|
63
|
-
- [MCP](https://
|
|
64
|
-
- [Serialization](https://
|
|
65
|
-
- [Tracing](https://
|
|
66
|
-
- [Providers](https://
|
|
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 [
|
|
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:
|
data/docs/CONFIGURATION.md
CHANGED
|
@@ -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
|
|
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
|
-
|
|
319
|
-
model_options temperature: 0.0 # Deterministic for testing
|
|
403
|
+
model_options temperature: 0.0 # Deterministic for testing
|
|
320
404
|
end
|
|
321
405
|
```
|
data/docs/SERIALIZATION.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Serialization
|
|
2
2
|
|
|
3
|
-
`Riffer::Agent::Serializer` turns a **resolved agent** into a
|
|
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
|
-
`
|
|
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
|
-
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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!",
|
data/docs/providers/ANTHROPIC.md
CHANGED
|
@@ -20,15 +20,19 @@ Riffer.configure do |config|
|
|
|
20
20
|
end
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
For anything beyond the API key — timeouts, retries, proxies — supply your own `Anthropic::Client`:
|
|
24
24
|
|
|
25
25
|
```ruby
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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.
|
|
18
|
-
2.
|
|
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
|
-
###
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
38
|
+
client.generate(**params)
|
|
39
39
|
end
|
|
40
40
|
|
|
41
41
|
def execute_stream(params, yielder)
|
|
42
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
342
|
+
client.create(**params)
|
|
327
343
|
end
|
|
328
344
|
|
|
329
345
|
def execute_stream(params, yielder)
|
|
330
346
|
accumulated_text = ""
|
|
331
347
|
|
|
332
|
-
|
|
348
|
+
client.stream(**params) do |event|
|
|
333
349
|
case event.type
|
|
334
350
|
when :text_delta
|
|
335
351
|
accumulated_text += event.text
|