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
data/docs/providers/GEMINI.md
CHANGED
|
@@ -12,15 +12,38 @@ Riffer.configure do |config|
|
|
|
12
12
|
end
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
|
88
|
+
provider = Riffer::Providers::Gemini.new
|
|
66
89
|
|
|
67
90
|
response = provider.generate_text(
|
|
68
91
|
prompt: "Hello!",
|
data/docs/providers/OPENAI.md
CHANGED
|
@@ -20,12 +20,28 @@ Riffer.configure do |config|
|
|
|
20
20
|
end
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
|
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
|
|
237
|
+
provider = Riffer::Providers::OpenRouter.new
|
|
233
238
|
|
|
234
239
|
response = provider.generate_text(
|
|
235
240
|
prompt: 'Hello!',
|
data/docs/providers/PROVIDERS.md
CHANGED
|
@@ -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
|
|
57
|
+
provider = Riffer::Providers::OpenAI.new
|
|
43
58
|
|
|
44
59
|
response = provider.generate_text(
|
|
45
60
|
prompt: "Hello!",
|
data/lib/riffer/agent/config.rb
CHANGED
|
@@ -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
|
|
@@ -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
|
@@ -57,14 +57,6 @@ class Riffer::Agent
|
|
|
57
57
|
value.nil? ? config.instructions : (config.instructions = value)
|
|
58
58
|
end
|
|
59
59
|
|
|
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
60
|
# Gets or sets model options passed to generate_text/stream_text.
|
|
69
61
|
#
|
|
70
62
|
#--
|
|
@@ -408,7 +400,7 @@ class Riffer::Agent
|
|
|
408
400
|
provider_class = Riffer::Providers::Repository.find(@provider_name)
|
|
409
401
|
raise Riffer::ArgumentError, "Provider not found: #{@provider_name}" unless provider_class
|
|
410
402
|
|
|
411
|
-
provider_class.new
|
|
403
|
+
provider_class.new
|
|
412
404
|
end
|
|
413
405
|
|
|
414
406
|
#--
|
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, :
|
|
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
|
|
data/lib/riffer/evals/judge.rb
CHANGED
|
@@ -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
|
|
37
|
-
def initialize(model
|
|
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
|
|
93
|
+
provider_class.new
|
|
96
94
|
end
|
|
97
95
|
end
|
|
98
96
|
|
|
@@ -37,28 +37,40 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
|
|
|
37
37
|
end
|
|
38
38
|
|
|
39
39
|
#--
|
|
40
|
-
#: (
|
|
41
|
-
def initialize
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
@@ -30,17 +30,28 @@ class Riffer::Providers::Anthropic < Riffer::Providers::Base
|
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
#--
|
|
33
|
-
#: (
|
|
34
|
-
def initialize
|
|
35
|
-
super
|
|
33
|
+
#: () -> void
|
|
34
|
+
def initialize
|
|
35
|
+
super
|
|
36
36
|
depends_on "anthropic"
|
|
37
|
+
end
|
|
37
38
|
|
|
38
|
-
|
|
39
|
+
private
|
|
39
40
|
|
|
40
|
-
|
|
41
|
+
#--
|
|
42
|
+
#: () -> untyped
|
|
43
|
+
def global_client
|
|
44
|
+
Riffer.config.anthropic.client
|
|
41
45
|
end
|
|
42
46
|
|
|
43
|
-
|
|
47
|
+
# Compacted for the same reason as the other providers: never hand an SDK an
|
|
48
|
+
# explicit nil credential, so its own +ANTHROPIC_API_KEY+ resolution stays
|
|
49
|
+
# reachable regardless of how that SDK distinguishes nil from absent.
|
|
50
|
+
#--
|
|
51
|
+
#: () -> untyped
|
|
52
|
+
def build_client
|
|
53
|
+
::Anthropic::Client.new(**{ api_key: Riffer.config.anthropic.api_key }.compact)
|
|
54
|
+
end
|
|
44
55
|
|
|
45
56
|
#--
|
|
46
57
|
#: (Array[Riffer::Messages::Base], String?, Hash[Symbol, untyped]) -> Hash[Symbol, untyped]
|
|
@@ -97,7 +108,7 @@ class Riffer::Providers::Anthropic < Riffer::Providers::Base
|
|
|
97
108
|
#--
|
|
98
109
|
#: (Hash[Symbol, untyped]) -> untyped
|
|
99
110
|
def execute_generate(params)
|
|
100
|
-
|
|
111
|
+
client.messages.create(**params)
|
|
101
112
|
end
|
|
102
113
|
|
|
103
114
|
#--
|
|
@@ -193,7 +204,7 @@ class Riffer::Providers::Anthropic < Riffer::Providers::Base
|
|
|
193
204
|
|
|
194
205
|
# Workaround for anthropics/anthropic-sdk-ruby#182: force identity
|
|
195
206
|
# encoding so Net::HTTP/Zlib doesn't buffer SSE chunks until EOF.
|
|
196
|
-
stream =
|
|
207
|
+
stream = client.messages.stream(
|
|
197
208
|
**params,
|
|
198
209
|
request_options: { extra_headers: { "accept-encoding" => "identity" } },
|
|
199
210
|
)
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
# rbs_inline: enabled
|
|
3
3
|
|
|
4
4
|
# Azure OpenAI provider for GPT models hosted on Azure. Requires the +openai+
|
|
5
|
-
# gem. Credentials resolve from
|
|
6
|
-
# +
|
|
5
|
+
# gem. Credentials resolve from config, then +AZURE_OPENAI_API_KEY+ /
|
|
6
|
+
# +AZURE_OPENAI_ENDPOINT+.
|
|
7
7
|
class Riffer::Providers::AzureOpenAI < Riffer::Providers::OpenAI
|
|
8
8
|
# The GenAI semconv well-known provider name.
|
|
9
9
|
#--
|
|
@@ -12,15 +12,23 @@ class Riffer::Providers::AzureOpenAI < Riffer::Providers::OpenAI
|
|
|
12
12
|
"azure.ai.openai"
|
|
13
13
|
end
|
|
14
14
|
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
#--
|
|
18
|
+
#: () -> untyped
|
|
19
|
+
def global_client
|
|
20
|
+
Riffer.config.azure_openai.client
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Deliberately not compacted: this borrows the OpenAI SDK to talk to Azure, so
|
|
24
|
+
# omitting an unset argument would let the SDK fall back to +OPENAI_API_KEY+
|
|
25
|
+
# and +OPENAI_BASE_URL+ — sending Azure traffic, and an OpenAI credential, to
|
|
26
|
+
# whatever those name. Passing nil raises in the SDK instead.
|
|
15
27
|
#--
|
|
16
|
-
#: (
|
|
17
|
-
def
|
|
18
|
-
api_key =
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
base_url = options.fetch(:base_url) do
|
|
22
|
-
Riffer.config.azure_openai.endpoint || ENV.fetch("AZURE_OPENAI_ENDPOINT", nil)
|
|
23
|
-
end
|
|
24
|
-
super(api_key: api_key, base_url: base_url, **options.except(:api_key, :base_url))
|
|
28
|
+
#: () -> untyped
|
|
29
|
+
def build_client
|
|
30
|
+
api_key = Riffer.config.azure_openai.api_key || ENV.fetch("AZURE_OPENAI_API_KEY", nil)
|
|
31
|
+
base_url = Riffer.config.azure_openai.endpoint || ENV.fetch("AZURE_OPENAI_ENDPOINT", nil)
|
|
32
|
+
::OpenAI::Client.new(api_key: api_key, base_url: base_url)
|
|
25
33
|
end
|
|
26
34
|
end
|
|
@@ -10,6 +10,7 @@ require "json"
|
|
|
10
10
|
class Riffer::Providers::Base
|
|
11
11
|
# @rbs @current_tools: Array[singleton(Riffer::Tool)]
|
|
12
12
|
# @rbs @current_model: String?
|
|
13
|
+
# @rbs @client: untyped
|
|
13
14
|
|
|
14
15
|
WIRE_SEPARATOR = "__" #: String
|
|
15
16
|
|
|
@@ -105,6 +106,33 @@ class Riffer::Providers::Base
|
|
|
105
106
|
Riffer::Helpers::Dependencies.depends_on(gem_name)
|
|
106
107
|
end
|
|
107
108
|
|
|
109
|
+
# Returns the client for the current LLM call. A configured client wins,
|
|
110
|
+
# resolved on every call so a Proc can vary the client by process or
|
|
111
|
+
# credential lifetime; otherwise the provider builds one from the configured
|
|
112
|
+
# credentials, memoized for the life of the provider.
|
|
113
|
+
#--
|
|
114
|
+
#: () -> untyped
|
|
115
|
+
def client
|
|
116
|
+
configured = global_client
|
|
117
|
+
return Riffer::Helpers::CallOrValue.resolve(configured) if configured
|
|
118
|
+
|
|
119
|
+
@client ||= build_client
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Returns the consumer-configured client for this provider; nil when none is
|
|
123
|
+
# configured, and for providers that take no configuration at all.
|
|
124
|
+
#--
|
|
125
|
+
#: () -> untyped
|
|
126
|
+
def global_client
|
|
127
|
+
nil
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
#--
|
|
131
|
+
#: () -> untyped
|
|
132
|
+
def build_client
|
|
133
|
+
raise NotImplementedError, "Subclasses must implement #build_client"
|
|
134
|
+
end
|
|
135
|
+
|
|
108
136
|
#--
|
|
109
137
|
#: (String) -> String
|
|
110
138
|
def encode_tool_name(name)
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
require "json"
|
|
5
|
+
require "net/http"
|
|
6
|
+
require "uri"
|
|
7
|
+
|
|
8
|
+
# HTTP transport for the Gemini REST API. Riffer builds one from the
|
|
9
|
+
# configured +api_key+ by default; construct your own to tune the HTTP knobs
|
|
10
|
+
# and assign it to <tt>Riffer.config.gemini.client</tt>. Any object
|
|
11
|
+
# implementing +post+ and +post_stream+ with these contracts works there —
|
|
12
|
+
# the class is a default implementation, not a required base.
|
|
13
|
+
#
|
|
14
|
+
# Riffer.configure do |config|
|
|
15
|
+
# config.gemini.client = Riffer::Providers::Gemini::Client.new(
|
|
16
|
+
# api_key: ENV["GEMINI_API_KEY"],
|
|
17
|
+
# read_timeout: 120
|
|
18
|
+
# )
|
|
19
|
+
# end
|
|
20
|
+
class Riffer::Providers::Gemini::Client
|
|
21
|
+
# @rbs @api_key: String?
|
|
22
|
+
# @rbs @base_url: String
|
|
23
|
+
# @rbs @open_timeout: Integer
|
|
24
|
+
# @rbs @read_timeout: Integer
|
|
25
|
+
# @rbs @write_timeout: Integer?
|
|
26
|
+
# @rbs @proxy_address: String?
|
|
27
|
+
# @rbs @proxy_port: Integer?
|
|
28
|
+
|
|
29
|
+
DEFAULT_BASE_URL = "https://generativelanguage.googleapis.com" #: String
|
|
30
|
+
DEFAULT_OPEN_TIMEOUT = 10 #: Integer
|
|
31
|
+
DEFAULT_READ_TIMEOUT = 60 #: Integer
|
|
32
|
+
|
|
33
|
+
#: (?api_key: String?, ?base_url: String, ?open_timeout: Integer, ?read_timeout: Integer, ?write_timeout: Integer?, ?proxy_address: String?, ?proxy_port: Integer?) -> void
|
|
34
|
+
def initialize(api_key: nil, base_url: DEFAULT_BASE_URL, open_timeout: DEFAULT_OPEN_TIMEOUT,
|
|
35
|
+
read_timeout: DEFAULT_READ_TIMEOUT, write_timeout: nil,
|
|
36
|
+
proxy_address: nil, proxy_port: nil)
|
|
37
|
+
@api_key = api_key
|
|
38
|
+
@base_url = base_url
|
|
39
|
+
@open_timeout = open_timeout
|
|
40
|
+
@read_timeout = read_timeout
|
|
41
|
+
@write_timeout = write_timeout
|
|
42
|
+
@proxy_address = proxy_address
|
|
43
|
+
@proxy_port = proxy_port
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# POSTs a JSON body to an API path and returns the parsed response hash.
|
|
47
|
+
# Raises Riffer::Error when the API responds with a non-success status.
|
|
48
|
+
#--
|
|
49
|
+
#: (String, Hash[Symbol, untyped]) -> Hash[Symbol, untyped]
|
|
50
|
+
def post(path, body)
|
|
51
|
+
uri = URI("#{@base_url}/#{path}")
|
|
52
|
+
response = start_http(uri) { |http| http.request(build_request(uri, body)) }
|
|
53
|
+
handle_api_error!(response) unless response.is_a?(Net::HTTPSuccess)
|
|
54
|
+
JSON.parse(response.body, symbolize_names: true)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# POSTs a JSON body to an API path, yielding raw response body chunks as
|
|
58
|
+
# they arrive. Raises Riffer::Error when the API responds with a
|
|
59
|
+
# non-success status.
|
|
60
|
+
#--
|
|
61
|
+
#: (String, Hash[Symbol, untyped]) { (String) -> void } -> void
|
|
62
|
+
def post_stream(path, body, &block)
|
|
63
|
+
uri = URI("#{@base_url}/#{path}")
|
|
64
|
+
start_http(uri) do |http|
|
|
65
|
+
http.request(build_request(uri, body)) do |response|
|
|
66
|
+
handle_api_error!(response) unless response.is_a?(Net::HTTPSuccess)
|
|
67
|
+
|
|
68
|
+
begin
|
|
69
|
+
response.read_body(&block)
|
|
70
|
+
rescue IOError
|
|
71
|
+
# A pre-buffered body (VCR/WebMock playback) raises IOError on a
|
|
72
|
+
# streaming read; hand over the full body instead.
|
|
73
|
+
yield(response.body)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
#--
|
|
82
|
+
#: (URI::Generic, Hash[Symbol, untyped]) -> Net::HTTP::Post
|
|
83
|
+
def build_request(uri, body)
|
|
84
|
+
request = Net::HTTP::Post.new(uri)
|
|
85
|
+
request["Content-Type"] = "application/json"
|
|
86
|
+
request["x-goog-api-key"] = @api_key
|
|
87
|
+
request.body = body.to_json
|
|
88
|
+
request
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
#--
|
|
92
|
+
#: [R] (URI::Generic) { (Net::HTTP) -> R } -> R
|
|
93
|
+
def start_http(uri, &)
|
|
94
|
+
host = uri.hostname #: String
|
|
95
|
+
options = {
|
|
96
|
+
use_ssl: uri.scheme == "https",
|
|
97
|
+
open_timeout: @open_timeout,
|
|
98
|
+
read_timeout: @read_timeout,
|
|
99
|
+
} #: Hash[Symbol, untyped]
|
|
100
|
+
options[:write_timeout] = @write_timeout if @write_timeout
|
|
101
|
+
|
|
102
|
+
if @proxy_address
|
|
103
|
+
Net::HTTP.start(host, uri.port, @proxy_address, @proxy_port, nil, nil, **options, &)
|
|
104
|
+
else
|
|
105
|
+
Net::HTTP.start(host, uri.port, **options, &)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
#--
|
|
110
|
+
#: (Net::HTTPResponse) -> void
|
|
111
|
+
def handle_api_error!(response)
|
|
112
|
+
parsed = begin
|
|
113
|
+
JSON.parse(response.body, symbolize_names: true)
|
|
114
|
+
rescue JSON::ParserError
|
|
115
|
+
{ message: response.body }
|
|
116
|
+
end
|
|
117
|
+
error_message = parsed.dig(:error, :message) || parsed[:message] || response.body
|
|
118
|
+
raise Riffer::Error, "Gemini API error (#{response.code}): #{error_message}"
|
|
119
|
+
end
|
|
120
|
+
end
|