riffer 0.47.2 → 0.48.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +23 -0
- data/docs/AGENTS.md +37 -3
- data/docs/AGENT_LIFECYCLE.md +1 -0
- data/docs/CONFIGURATION.md +1 -1
- data/docs/EVALS.md +2 -1
- data/docs/MCP.md +0 -4
- data/docs/MESSAGES.md +85 -17
- data/docs/STREAM_EVENTS.md +8 -4
- data/docs/TRACING.md +1 -1
- data/docs/providers/AMAZON_BEDROCK.md +1 -1
- data/docs/providers/CUSTOM_PROVIDERS.md +56 -4
- data/docs/providers/GEMINI.md +1 -1
- data/docs/providers/MOCK_PROVIDER.md +17 -0
- data/lib/riffer/agent/config.rb +30 -6
- data/lib/riffer/agent/response.rb +7 -0
- data/lib/riffer/agent/run.rb +24 -8
- data/lib/riffer/agent/session.rb +1 -0
- data/lib/riffer/agent.rb +29 -5
- data/lib/riffer/config.rb +1 -1
- data/lib/riffer/evals/evaluator.rb +29 -2
- data/lib/riffer/evals/judge.rb +11 -5
- data/lib/riffer/files/resolver.rb +3 -3
- data/lib/riffer/helpers/deep_dup.rb +42 -0
- data/lib/riffer/messages/assistant/reasoning_part.rb +90 -0
- data/lib/riffer/messages/assistant/tool_call.rb +63 -0
- data/lib/riffer/messages/assistant.rb +50 -2
- data/lib/riffer/messages/base.rb +5 -21
- data/lib/riffer/messages/system.rb +10 -0
- data/lib/riffer/messages/tool.rb +17 -0
- data/lib/riffer/messages/{file_part.rb → user/file_part.rb} +15 -11
- data/lib/riffer/messages/user.rb +13 -2
- data/lib/riffer/params/param.rb +12 -0
- data/lib/riffer/params.rb +9 -0
- data/lib/riffer/providers/amazon_bedrock.rb +5 -4
- data/lib/riffer/providers/anthropic.rb +2 -2
- data/lib/riffer/providers/base.rb +27 -8
- data/lib/riffer/providers/gemini.rb +2 -2
- data/lib/riffer/providers/mock.rb +18 -2
- data/lib/riffer/providers/open_ai.rb +6 -5
- data/lib/riffer/providers/open_router.rb +6 -5
- data/lib/riffer/providers/token_usage.rb +16 -0
- data/lib/riffer/skills/config.rb +12 -0
- data/lib/riffer/stream_events/reasoning_done.rb +10 -8
- data/lib/riffer/tools/runtime.rb +3 -4
- data/lib/riffer/tracing/capture.rb +2 -4
- data/lib/riffer/version.rb +1 -1
- data/sig/generated/riffer/agent/config.rbs +19 -5
- data/sig/generated/riffer/agent/response.rbs +6 -1
- data/sig/generated/riffer/agent/run.rbs +15 -8
- data/sig/generated/riffer/agent.rbs +25 -9
- data/sig/generated/riffer/config.rbs +1 -1
- data/sig/generated/riffer/evals/evaluator.rbs +20 -1
- data/sig/generated/riffer/evals/judge.rbs +5 -2
- data/sig/generated/riffer/files/resolver.rbs +6 -6
- data/sig/generated/riffer/helpers/deep_dup.rbs +21 -0
- data/sig/generated/riffer/messages/assistant/reasoning_part.rbs +58 -0
- data/sig/generated/riffer/messages/assistant/tool_call.rbs +42 -0
- data/sig/generated/riffer/messages/assistant.rbs +24 -10
- data/sig/generated/riffer/messages/system.rbs +6 -0
- data/sig/generated/riffer/messages/tool.rbs +6 -0
- data/sig/generated/riffer/messages/{file_part.rbs → user/file_part.rbs} +11 -6
- data/sig/generated/riffer/messages/user.rbs +9 -3
- data/sig/generated/riffer/params/param.rbs +7 -0
- data/sig/generated/riffer/params.rbs +6 -0
- data/sig/generated/riffer/providers/amazon_bedrock.rbs +4 -4
- data/sig/generated/riffer/providers/anthropic.rbs +2 -2
- data/sig/generated/riffer/providers/base.rbs +22 -10
- data/sig/generated/riffer/providers/gemini.rbs +4 -4
- data/sig/generated/riffer/providers/mock.rbs +7 -2
- data/sig/generated/riffer/providers/open_ai.rbs +4 -4
- data/sig/generated/riffer/providers/open_router.rbs +4 -4
- data/sig/generated/riffer/providers/token_usage.rbs +6 -0
- data/sig/generated/riffer/skills/config.rbs +9 -0
- data/sig/generated/riffer/stream_events/reasoning_done.rbs +8 -6
- data/sig/generated/riffer/tools/runtime.rbs +2 -2
- data/sig/generated/riffer/tracing/capture.rbs +4 -4
- data/sig/manual/riffer/helpers/deep_dup.rbs +5 -0
- metadata +10 -3
data/lib/riffer/messages/base.rb
CHANGED
|
@@ -15,27 +15,11 @@ class Riffer::Messages::Base
|
|
|
15
15
|
raise Riffer::ArgumentError, "Message hash must include a 'role' key" if msg[:role].nil? || msg[:role].empty?
|
|
16
16
|
|
|
17
17
|
case msg[:role].to_sym
|
|
18
|
-
when :user
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
when :
|
|
22
|
-
|
|
23
|
-
tc.is_a?(Riffer::Messages::Assistant::ToolCall) ? tc : Riffer::Messages::Assistant::ToolCall.new(**tc)
|
|
24
|
-
end
|
|
25
|
-
Riffer::Messages::Assistant.new(
|
|
26
|
-
msg[:content],
|
|
27
|
-
id: msg[:id],
|
|
28
|
-
tool_calls: tool_calls,
|
|
29
|
-
structured_output: msg[:structured_output],
|
|
30
|
-
finish_reason: msg[:finish_reason]&.to_sym,
|
|
31
|
-
finish_reason_raw: msg[:finish_reason_raw],
|
|
32
|
-
)
|
|
33
|
-
when :system
|
|
34
|
-
Riffer::Messages::System.new(msg[:content], id: msg[:id])
|
|
35
|
-
when :tool
|
|
36
|
-
Riffer::Messages::Tool.new(msg[:content], id: msg[:id], tool_call_id: msg[:tool_call_id], name: msg[:name])
|
|
37
|
-
else
|
|
38
|
-
raise Riffer::ArgumentError, "Unknown message role: #{msg[:role]}"
|
|
18
|
+
when :user then Riffer::Messages::User.from_hash(msg)
|
|
19
|
+
when :assistant then Riffer::Messages::Assistant.from_hash(msg)
|
|
20
|
+
when :system then Riffer::Messages::System.from_hash(msg)
|
|
21
|
+
when :tool then Riffer::Messages::Tool.from_hash(msg)
|
|
22
|
+
else raise Riffer::ArgumentError, "Unknown message role: #{msg[:role]}"
|
|
39
23
|
end
|
|
40
24
|
end
|
|
41
25
|
|
|
@@ -3,6 +3,16 @@
|
|
|
3
3
|
|
|
4
4
|
# Represents a system message (instructions) in a conversation.
|
|
5
5
|
class Riffer::Messages::System < Riffer::Messages::Base
|
|
6
|
+
# Builds a System message from a hash, or returns +msg+ unchanged when it is
|
|
7
|
+
# already a System message.
|
|
8
|
+
#--
|
|
9
|
+
#: ((Hash[Symbol, untyped] | Riffer::Messages::System)) -> Riffer::Messages::System
|
|
10
|
+
def self.from_hash(msg)
|
|
11
|
+
return msg if msg.is_a?(Riffer::Messages::System)
|
|
12
|
+
|
|
13
|
+
new(msg[:content], id: msg[:id])
|
|
14
|
+
end
|
|
15
|
+
|
|
6
16
|
#--
|
|
7
17
|
#: () -> Symbol
|
|
8
18
|
def role
|
data/lib/riffer/messages/tool.rb
CHANGED
|
@@ -3,6 +3,23 @@
|
|
|
3
3
|
|
|
4
4
|
# Represents a tool execution result in a conversation.
|
|
5
5
|
class Riffer::Messages::Tool < Riffer::Messages::Base
|
|
6
|
+
# Builds a Tool message from a hash, or returns +msg+ unchanged when it is
|
|
7
|
+
# already a Tool message.
|
|
8
|
+
#--
|
|
9
|
+
#: ((Hash[Symbol, untyped] | Riffer::Messages::Tool)) -> Riffer::Messages::Tool
|
|
10
|
+
def self.from_hash(msg)
|
|
11
|
+
return msg if msg.is_a?(Riffer::Messages::Tool)
|
|
12
|
+
|
|
13
|
+
new(
|
|
14
|
+
msg[:content],
|
|
15
|
+
id: msg[:id],
|
|
16
|
+
tool_call_id: msg[:tool_call_id],
|
|
17
|
+
name: msg[:name],
|
|
18
|
+
error: msg[:error],
|
|
19
|
+
error_type: msg[:error_type]&.to_sym,
|
|
20
|
+
)
|
|
21
|
+
end
|
|
22
|
+
|
|
6
23
|
# The ID of the tool call this result responds to.
|
|
7
24
|
attr_reader :tool_call_id #: String # @dynamic tool_call_id
|
|
8
25
|
|
|
@@ -6,7 +6,7 @@ require "uri"
|
|
|
6
6
|
|
|
7
7
|
# Represents a file attachment (image or document) — from a URL (+from_url+) or
|
|
8
8
|
# raw base64 data (+new+).
|
|
9
|
-
class Riffer::Messages::FilePart
|
|
9
|
+
class Riffer::Messages::User::FilePart
|
|
10
10
|
# @rbs @url_string: String?
|
|
11
11
|
# @rbs @data: String?
|
|
12
12
|
# @rbs @downloaded_data: String?
|
|
@@ -62,23 +62,18 @@ class Riffer::Messages::FilePart
|
|
|
62
62
|
# Creates a FilePart from a URL, detecting +media_type+ from the path
|
|
63
63
|
# extension when omitted. Raises Riffer::ArgumentError if it can't be detected.
|
|
64
64
|
#--
|
|
65
|
-
#: (String, ?media_type: String?, ?filename: String?, ?sha256: String?) -> Riffer::Messages::FilePart
|
|
65
|
+
#: (String, ?media_type: String?, ?filename: String?, ?sha256: String?) -> Riffer::Messages::User::FilePart
|
|
66
66
|
def self.from_url(url, media_type: nil, filename: nil, sha256: nil)
|
|
67
|
-
|
|
68
|
-
ext = ::File.extname(URI.parse(url).path.to_s).downcase
|
|
69
|
-
media_type = MEDIA_TYPES.fetch(ext) { raise Riffer::ArgumentError, "Cannot detect media type from URL; provide media_type explicitly" }
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
new(url: url, media_type: media_type, filename: filename, sha256: sha256)
|
|
67
|
+
new(url: url, media_type: media_type || detect_media_type(url), filename: filename, sha256: sha256)
|
|
73
68
|
end
|
|
74
69
|
|
|
75
70
|
# Builds a FilePart from a +{url:, media_type:}+ or +{data:, media_type:}+ hash,
|
|
76
71
|
# or returns +file+ unchanged when it is already a FilePart. Raises
|
|
77
72
|
# Riffer::ArgumentError on an invalid hash.
|
|
78
73
|
#--
|
|
79
|
-
#: ((Hash[Symbol, untyped] | Riffer::Messages::FilePart)) -> Riffer::Messages::FilePart
|
|
74
|
+
#: ((Hash[Symbol, untyped] | Riffer::Messages::User::FilePart)) -> Riffer::Messages::User::FilePart
|
|
80
75
|
def self.from_hash(file)
|
|
81
|
-
return file if file.is_a?(Riffer::Messages::FilePart)
|
|
76
|
+
return file if file.is_a?(Riffer::Messages::User::FilePart)
|
|
82
77
|
|
|
83
78
|
url = file[:url]
|
|
84
79
|
data = file[:data]
|
|
@@ -87,7 +82,7 @@ class Riffer::Messages::FilePart
|
|
|
87
82
|
sha256 = file[:sha256]
|
|
88
83
|
|
|
89
84
|
if url
|
|
90
|
-
|
|
85
|
+
new(url: url, data: data, media_type: media_type || detect_media_type(url), filename: filename, sha256: sha256)
|
|
91
86
|
elsif data && media_type
|
|
92
87
|
new(data: data, media_type: media_type, filename: filename, sha256: sha256)
|
|
93
88
|
else
|
|
@@ -95,6 +90,15 @@ class Riffer::Messages::FilePart
|
|
|
95
90
|
end
|
|
96
91
|
end
|
|
97
92
|
|
|
93
|
+
# Raises Riffer::ArgumentError when the URL's extension isn't a known media type.
|
|
94
|
+
#--
|
|
95
|
+
#: (String) -> String
|
|
96
|
+
def self.detect_media_type(url)
|
|
97
|
+
ext = ::File.extname(URI.parse(url).path.to_s).downcase
|
|
98
|
+
MEDIA_TYPES.fetch(ext) { raise Riffer::ArgumentError, "Cannot detect media type from URL; provide media_type explicitly" }
|
|
99
|
+
end
|
|
100
|
+
private_class_method :detect_media_type
|
|
101
|
+
|
|
98
102
|
# The base64-encoded contents - caller-supplied, or filled in by the file
|
|
99
103
|
# resolver after a download. Nil for a URL source riffer hasn't fetched.
|
|
100
104
|
#--
|
data/lib/riffer/messages/user.rb
CHANGED
|
@@ -3,11 +3,22 @@
|
|
|
3
3
|
|
|
4
4
|
# Represents a user message in a conversation.
|
|
5
5
|
class Riffer::Messages::User < Riffer::Messages::Base
|
|
6
|
+
# Builds a User message from a hash, or returns +msg+ unchanged when it is
|
|
7
|
+
# already a User message. Raises Riffer::ArgumentError on an invalid file.
|
|
8
|
+
#--
|
|
9
|
+
#: ((Hash[Symbol, untyped] | Riffer::Messages::User)) -> Riffer::Messages::User
|
|
10
|
+
def self.from_hash(msg)
|
|
11
|
+
return msg if msg.is_a?(Riffer::Messages::User)
|
|
12
|
+
|
|
13
|
+
files = (msg[:files] || []).map { |f| Riffer::Messages::User::FilePart.from_hash(f) }
|
|
14
|
+
new(msg[:content], id: msg[:id], files: files)
|
|
15
|
+
end
|
|
16
|
+
|
|
6
17
|
# File attachments for this message.
|
|
7
|
-
attr_reader :files #: Array[Riffer::Messages::FilePart] # @dynamic files
|
|
18
|
+
attr_reader :files #: Array[Riffer::Messages::User::FilePart] # @dynamic files
|
|
8
19
|
|
|
9
20
|
#--
|
|
10
|
-
#: (String, ?id: String?, ?files: Array[Riffer::Messages::FilePart]) -> void
|
|
21
|
+
#: (String, ?id: String?, ?files: Array[Riffer::Messages::User::FilePart]) -> void
|
|
11
22
|
def initialize(content, id: nil, files: [])
|
|
12
23
|
super(content, id: id)
|
|
13
24
|
@files = files
|
data/lib/riffer/params/param.rb
CHANGED
|
@@ -181,6 +181,18 @@ class Riffer::Params::Param
|
|
|
181
181
|
|
|
182
182
|
private
|
|
183
183
|
|
|
184
|
+
# +dup+ would leave the copy sharing this one's nested Params, enum list and
|
|
185
|
+
# default, so defining a parameter or editing either value on one would reach
|
|
186
|
+
# the other.
|
|
187
|
+
#--
|
|
188
|
+
#: (Riffer::Params::Param) -> void
|
|
189
|
+
def initialize_copy(source)
|
|
190
|
+
super
|
|
191
|
+
@nested_params = source.nested_params&.dup
|
|
192
|
+
@enum = Riffer::Helpers::DeepDup.call(source.enum)
|
|
193
|
+
@default = Riffer::Helpers::DeepDup.call(source.default)
|
|
194
|
+
end
|
|
195
|
+
|
|
184
196
|
#--
|
|
185
197
|
#: () -> void
|
|
186
198
|
def validate_strict_shape!
|
data/lib/riffer/params.rb
CHANGED
|
@@ -151,6 +151,15 @@ class Riffer::Params
|
|
|
151
151
|
|
|
152
152
|
private
|
|
153
153
|
|
|
154
|
+
# +dup+ would leave the copy sharing this one's parameters, so a caller
|
|
155
|
+
# defining a parameter on either would reach the other.
|
|
156
|
+
#--
|
|
157
|
+
#: (Riffer::Params) -> void
|
|
158
|
+
def initialize_copy(source)
|
|
159
|
+
super
|
|
160
|
+
@parameters = source.parameters.map(&:dup)
|
|
161
|
+
end
|
|
162
|
+
|
|
154
163
|
#--
|
|
155
164
|
#: (Module, Module?) ?{ (Riffer::Params) [self: Riffer::Params] -> void } -> Riffer::Params?
|
|
156
165
|
def build_nested(type, of, &block)
|
|
@@ -47,7 +47,7 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
|
|
|
47
47
|
end
|
|
48
48
|
|
|
49
49
|
#--
|
|
50
|
-
#: (Riffer::Messages::FilePart) -> Symbol
|
|
50
|
+
#: (Riffer::Messages::User::FilePart) -> Symbol
|
|
51
51
|
def file_delivery(file)
|
|
52
52
|
file.url&.start_with?("s3://") ? :url : :bytes
|
|
53
53
|
end
|
|
@@ -98,8 +98,9 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
|
|
|
98
98
|
|
|
99
99
|
# requestMetadata is a flat String=>String map used to filter invocation
|
|
100
100
|
# logs; every tag (including the reserved user_id) rides along, since
|
|
101
|
-
# Converse has no dedicated end-user field.
|
|
102
|
-
|
|
101
|
+
# Converse has no dedicated end-user field. Merged over any request_metadata
|
|
102
|
+
# set in model_options; a tag wins on a shared key.
|
|
103
|
+
params[:request_metadata] = (params[:request_metadata] || {}).merge(tags) unless tags.empty?
|
|
103
104
|
|
|
104
105
|
if tools && !tools.empty?
|
|
105
106
|
params[:tool_config] = {
|
|
@@ -449,7 +450,7 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
|
|
|
449
450
|
end
|
|
450
451
|
|
|
451
452
|
#--
|
|
452
|
-
#: (Riffer::Messages::FilePart) -> Hash[Symbol, untyped]
|
|
453
|
+
#: (Riffer::Messages::User::FilePart) -> Hash[Symbol, untyped]
|
|
453
454
|
def convert_file_part_to_bedrock_format(file)
|
|
454
455
|
format = bedrock_format(file.media_type)
|
|
455
456
|
|
|
@@ -338,7 +338,7 @@ class Riffer::Providers::Anthropic < Riffer::Providers::Base
|
|
|
338
338
|
#--
|
|
339
339
|
#: (untyped, state: Hash[Symbol, untyped], yielder: Riffer::Providers::_EventSink) -> void
|
|
340
340
|
def handle_content_block_stop_thinking(_event, state:, yielder:)
|
|
341
|
-
yielder
|
|
341
|
+
yield_reasoning_done(yielder, state[:reasoning])
|
|
342
342
|
state[:reasoning] = nil
|
|
343
343
|
end
|
|
344
344
|
|
|
@@ -444,7 +444,7 @@ class Riffer::Providers::Anthropic < Riffer::Providers::Base
|
|
|
444
444
|
end
|
|
445
445
|
|
|
446
446
|
#--
|
|
447
|
-
#: (Riffer::Messages::FilePart) -> Hash[Symbol, untyped]
|
|
447
|
+
#: (Riffer::Messages::User::FilePart) -> Hash[Symbol, untyped]
|
|
448
448
|
def convert_file_part_to_anthropic_format(file)
|
|
449
449
|
type = file.image? ? "image" : "document"
|
|
450
450
|
|
|
@@ -40,7 +40,7 @@ class Riffer::Providers::Base
|
|
|
40
40
|
# Generates text using the provider.
|
|
41
41
|
#
|
|
42
42
|
#--
|
|
43
|
-
#: (?prompt: String?, ?system: String?, ?messages: Array[Hash[Symbol, untyped] | Riffer::Messages::Base]?, ?model: String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?, **untyped) -> Riffer::Messages::Assistant
|
|
43
|
+
#: (?prompt: String?, ?system: String?, ?messages: Array[Hash[Symbol, untyped] | Riffer::Messages::Base]?, ?model: String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::User::FilePart]?, **untyped) -> Riffer::Messages::Assistant
|
|
44
44
|
def generate_text(prompt: nil, system: nil, messages: nil, model: nil, files: nil, **options)
|
|
45
45
|
validate_input!(prompt: prompt, system: system, messages: messages)
|
|
46
46
|
@current_tools = options[:tools] || [] #: Array[singleton(Riffer::Tool)]
|
|
@@ -56,6 +56,7 @@ class Riffer::Providers::Base
|
|
|
56
56
|
|
|
57
57
|
content = extract_content(response)
|
|
58
58
|
tool_calls = extract_tool_calls(response)
|
|
59
|
+
reasoning = extract_reasoning(response)
|
|
59
60
|
token_usage = extract_token_usage(response)
|
|
60
61
|
finish_reason = extract_finish_reason(response)
|
|
61
62
|
structured_output = parse_structured_output(content) if options[:structured_output] && tool_calls.empty?
|
|
@@ -67,6 +68,7 @@ class Riffer::Providers::Base
|
|
|
67
68
|
Riffer::Messages::Assistant.new(
|
|
68
69
|
content,
|
|
69
70
|
tool_calls: tool_calls,
|
|
71
|
+
reasoning: reasoning,
|
|
70
72
|
token_usage: token_usage,
|
|
71
73
|
structured_output: structured_output,
|
|
72
74
|
finish_reason: finish_reason&.reason,
|
|
@@ -78,7 +80,7 @@ class Riffer::Providers::Base
|
|
|
78
80
|
# Streams text from the provider.
|
|
79
81
|
#
|
|
80
82
|
#--
|
|
81
|
-
#: (?prompt: String?, ?system: String?, ?messages: Array[Hash[Symbol, untyped] | Riffer::Messages::Base]?, ?model: String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?, **untyped) -> Enumerator[Riffer::StreamEvents::Base, void]
|
|
83
|
+
#: (?prompt: String?, ?system: String?, ?messages: Array[Hash[Symbol, untyped] | Riffer::Messages::Base]?, ?model: String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::User::FilePart]?, **untyped) -> Enumerator[Riffer::StreamEvents::Base, void]
|
|
82
84
|
def stream_text(prompt: nil, system: nil, messages: nil, model: nil, files: nil, **options)
|
|
83
85
|
validate_input!(prompt: prompt, system: system, messages: messages)
|
|
84
86
|
@current_tools = options[:tools] || [] #: Array[singleton(Riffer::Tool)]
|
|
@@ -105,7 +107,7 @@ class Riffer::Providers::Base
|
|
|
105
107
|
end
|
|
106
108
|
|
|
107
109
|
#--
|
|
108
|
-
#: (Riffer::Messages::FilePart) -> Symbol
|
|
110
|
+
#: (Riffer::Messages::User::FilePart) -> Symbol
|
|
109
111
|
def file_delivery(_file)
|
|
110
112
|
:url
|
|
111
113
|
end
|
|
@@ -224,6 +226,14 @@ class Riffer::Providers::Base
|
|
|
224
226
|
nil
|
|
225
227
|
end
|
|
226
228
|
|
|
229
|
+
# Defaults to no parts rather than raising — reasoning parts are optional, so
|
|
230
|
+
# providers that don't expose replayable reasoning stay valid.
|
|
231
|
+
#--
|
|
232
|
+
#: (untyped) -> Array[Riffer::Messages::Assistant::ReasoningPart]
|
|
233
|
+
def extract_reasoning(_response)
|
|
234
|
+
[]
|
|
235
|
+
end
|
|
236
|
+
|
|
227
237
|
#--
|
|
228
238
|
#: (untyped) -> String
|
|
229
239
|
def extract_content(response)
|
|
@@ -348,6 +358,15 @@ class Riffer::Providers::Base
|
|
|
348
358
|
Riffer.config.tracing.capture_messages && span.recording?
|
|
349
359
|
end
|
|
350
360
|
|
|
361
|
+
# Wraps reasoning text that an adapter cannot yet replay in a +:text+ part
|
|
362
|
+
# with no +format+, so it persists for display but is never sent back.
|
|
363
|
+
#--
|
|
364
|
+
#: (Riffer::Providers::_EventSink, String) -> void
|
|
365
|
+
def yield_reasoning_done(yielder, text)
|
|
366
|
+
part = Riffer::Messages::Assistant::ReasoningPart.new(type: :text, text: text)
|
|
367
|
+
yielder << Riffer::StreamEvents::ReasoningDone.new(part)
|
|
368
|
+
end
|
|
369
|
+
|
|
351
370
|
#--
|
|
352
371
|
#: (Riffer::Providers::_EventSink, Riffer::Providers::FinishReason?) -> void
|
|
353
372
|
def yield_finish_reason(yielder, finish_reason)
|
|
@@ -368,11 +387,11 @@ class Riffer::Providers::Base
|
|
|
368
387
|
end
|
|
369
388
|
|
|
370
389
|
#--
|
|
371
|
-
#: (
|
|
390
|
+
#: (String) -> Hash[String, untyped]
|
|
372
391
|
def parse_tool_arguments(arguments)
|
|
373
|
-
return {} if arguments.
|
|
392
|
+
return {} if arguments.empty?
|
|
374
393
|
|
|
375
|
-
|
|
394
|
+
JSON.parse(arguments)
|
|
376
395
|
end
|
|
377
396
|
|
|
378
397
|
#--
|
|
@@ -397,7 +416,7 @@ class Riffer::Providers::Base
|
|
|
397
416
|
end
|
|
398
417
|
|
|
399
418
|
#--
|
|
400
|
-
#: (prompt: String?, system: String?, messages: Array[Hash[Symbol, untyped] | Riffer::Messages::Base]?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?) -> Array[Riffer::Messages::Base]
|
|
419
|
+
#: (prompt: String?, system: String?, messages: Array[Hash[Symbol, untyped] | Riffer::Messages::Base]?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::User::FilePart]?) -> Array[Riffer::Messages::Base]
|
|
401
420
|
def normalize_messages(prompt:, system:, messages:, files: nil)
|
|
402
421
|
if messages && files && !files.empty?
|
|
403
422
|
raise Riffer::ArgumentError, "cannot provide both files and messages; attach files to individual messages instead"
|
|
@@ -407,7 +426,7 @@ class Riffer::Providers::Base
|
|
|
407
426
|
|
|
408
427
|
result = [] #: Array[Riffer::Messages::Base]
|
|
409
428
|
result << Riffer::Messages::System.new(system) if system
|
|
410
|
-
file_parts = (files || []).map { |f| Riffer::Messages::FilePart.from_hash(f) }
|
|
429
|
+
file_parts = (files || []).map { |f| Riffer::Messages::User::FilePart.from_hash(f) }
|
|
411
430
|
prompt_text = prompt #: String
|
|
412
431
|
result << Riffer::Messages::User.new(prompt_text, files: file_parts)
|
|
413
432
|
result
|
|
@@ -37,7 +37,7 @@ class Riffer::Providers::Gemini < Riffer::Providers::Base
|
|
|
37
37
|
end
|
|
38
38
|
|
|
39
39
|
#--
|
|
40
|
-
#: (Riffer::Messages::FilePart) -> Symbol
|
|
40
|
+
#: (Riffer::Messages::User::FilePart) -> Symbol
|
|
41
41
|
def file_delivery(_file)
|
|
42
42
|
:base64
|
|
43
43
|
end
|
|
@@ -287,7 +287,7 @@ class Riffer::Providers::Gemini < Riffer::Providers::Base
|
|
|
287
287
|
end
|
|
288
288
|
|
|
289
289
|
#--
|
|
290
|
-
#: (Riffer::Messages::FilePart) -> Hash[Symbol, untyped]
|
|
290
|
+
#: (Riffer::Messages::User::FilePart) -> Hash[Symbol, untyped]
|
|
291
291
|
def convert_file_part_to_gemini_format(file)
|
|
292
292
|
{ inlineData: { mimeType: file.media_type, data: file.data } }
|
|
293
293
|
end
|
|
@@ -54,15 +54,17 @@ class Riffer::Providers::Mock < Riffer::Providers::Base
|
|
|
54
54
|
# provider.stub_response("Final response",
|
|
55
55
|
# token_usage: Riffer::Providers::TokenUsage.new(input_tokens: 10, output_tokens: 5))
|
|
56
56
|
# provider.stub_response("Truncated...", finish_reason: :length)
|
|
57
|
+
# provider.stub_response("Answer", reasoning: [{type: :text, text: "Thinking...", format: "mock-v1"}])
|
|
57
58
|
#
|
|
58
59
|
#--
|
|
59
|
-
#: (String, ?tool_calls: Array[Hash[Symbol, untyped]], ?token_usage: Riffer::Providers::TokenUsage?, ?finish_reason: Symbol?) -> void
|
|
60
|
-
def stub_response(content, tool_calls: [], token_usage: nil, finish_reason: nil)
|
|
60
|
+
#: (String, ?tool_calls: Array[Hash[Symbol, untyped]], ?token_usage: Riffer::Providers::TokenUsage?, ?finish_reason: Symbol?, ?reasoning: Array[Hash[Symbol, untyped] | Riffer::Messages::Assistant::ReasoningPart]) -> void
|
|
61
|
+
def stub_response(content, tool_calls: [], token_usage: nil, finish_reason: nil, reasoning: [])
|
|
61
62
|
@stubbed_responses << normalize_response(
|
|
62
63
|
content: content,
|
|
63
64
|
tool_calls: tool_calls,
|
|
64
65
|
token_usage: token_usage,
|
|
65
66
|
finish_reason: finish_reason,
|
|
67
|
+
reasoning: reasoning,
|
|
66
68
|
)
|
|
67
69
|
end
|
|
68
70
|
|
|
@@ -92,6 +94,7 @@ class Riffer::Providers::Mock < Riffer::Providers::Base
|
|
|
92
94
|
role: response[:role] || "assistant",
|
|
93
95
|
content: response[:content] || "",
|
|
94
96
|
tool_calls: formatted_tool_calls,
|
|
97
|
+
reasoning: (response[:reasoning] || []).map { |part| Riffer::Messages::Assistant::ReasoningPart.from_hash(part) },
|
|
95
98
|
token_usage: response[:token_usage],
|
|
96
99
|
finish_reason: response[:finish_reason] || (formatted_tool_calls.empty? ? :stop : :tool_calls),
|
|
97
100
|
}
|
|
@@ -129,6 +132,12 @@ class Riffer::Providers::Mock < Riffer::Providers::Base
|
|
|
129
132
|
reason ? Riffer::Providers::FinishReason.new(reason: reason) : nil
|
|
130
133
|
end
|
|
131
134
|
|
|
135
|
+
#--
|
|
136
|
+
#: (untyped) -> Array[Riffer::Messages::Assistant::ReasoningPart]
|
|
137
|
+
def extract_reasoning(response)
|
|
138
|
+
response[:reasoning] || []
|
|
139
|
+
end
|
|
140
|
+
|
|
132
141
|
#--
|
|
133
142
|
#: (untyped) -> String
|
|
134
143
|
def extract_content(response)
|
|
@@ -149,6 +158,7 @@ class Riffer::Providers::Mock < Riffer::Providers::Base
|
|
|
149
158
|
tool_calls = response[:tool_calls] || []
|
|
150
159
|
token_usage = response[:token_usage]
|
|
151
160
|
web_search = response[:web_search]
|
|
161
|
+
reasoning = response[:reasoning] || []
|
|
152
162
|
|
|
153
163
|
if web_search
|
|
154
164
|
yielder << Riffer::StreamEvents::WebSearchStatus.new("in_progress")
|
|
@@ -161,6 +171,12 @@ class Riffer::Providers::Mock < Riffer::Providers::Base
|
|
|
161
171
|
)
|
|
162
172
|
end
|
|
163
173
|
|
|
174
|
+
reasoning.each do |part|
|
|
175
|
+
text = part.text
|
|
176
|
+
yielder << Riffer::StreamEvents::ReasoningDelta.new(text) if text
|
|
177
|
+
yielder << Riffer::StreamEvents::ReasoningDone.new(part)
|
|
178
|
+
end
|
|
179
|
+
|
|
164
180
|
unless full_content.empty?
|
|
165
181
|
content_parts = full_content.split(". ").map { |part| part + (part.end_with?(".") ? "" : ".") }
|
|
166
182
|
content_parts.each do |part|
|
|
@@ -35,7 +35,7 @@ class Riffer::Providers::OpenAI < Riffer::Providers::Base
|
|
|
35
35
|
end
|
|
36
36
|
|
|
37
37
|
#--
|
|
38
|
-
#: (Riffer::Messages::FilePart) -> Symbol
|
|
38
|
+
#: (Riffer::Messages::User::FilePart) -> Symbol
|
|
39
39
|
def file_delivery(file)
|
|
40
40
|
file.image? ? :url : :base64
|
|
41
41
|
end
|
|
@@ -80,7 +80,8 @@ class Riffer::Providers::OpenAI < Riffer::Providers::Base
|
|
|
80
80
|
} #: Hash[Symbol, untyped]
|
|
81
81
|
|
|
82
82
|
unless tags.empty?
|
|
83
|
-
|
|
83
|
+
# Merged over any metadata set in model_options; a tag wins on a shared key.
|
|
84
|
+
params[:metadata] = (params[:metadata] || {}).merge(tags)
|
|
84
85
|
# The reserved user_id also maps to the native safety identifier while
|
|
85
86
|
# staying in metadata as an ordinary tag.
|
|
86
87
|
user_id = tags["user_id"]
|
|
@@ -296,7 +297,7 @@ class Riffer::Providers::OpenAI < Riffer::Providers::Base
|
|
|
296
297
|
#--
|
|
297
298
|
#: (untyped, state: Hash[Symbol, untyped], yielder: Riffer::Providers::_EventSink) -> void
|
|
298
299
|
def handle_reasoning_summary_text_done(event, state:, yielder:)
|
|
299
|
-
yielder
|
|
300
|
+
yield_reasoning_done(yielder, event.text)
|
|
300
301
|
end
|
|
301
302
|
|
|
302
303
|
#--
|
|
@@ -402,7 +403,7 @@ class Riffer::Providers::OpenAI < Riffer::Providers::Base
|
|
|
402
403
|
type: "function_call",
|
|
403
404
|
call_id: tc.call_id,
|
|
404
405
|
name: encode_tool_name(tc.name),
|
|
405
|
-
arguments: tc.arguments
|
|
406
|
+
arguments: tc.arguments,
|
|
406
407
|
}
|
|
407
408
|
end
|
|
408
409
|
items
|
|
@@ -410,7 +411,7 @@ class Riffer::Providers::OpenAI < Riffer::Providers::Base
|
|
|
410
411
|
end
|
|
411
412
|
|
|
412
413
|
#--
|
|
413
|
-
#: (Riffer::Messages::FilePart) -> Hash[Symbol, untyped]
|
|
414
|
+
#: (Riffer::Messages::User::FilePart) -> Hash[Symbol, untyped]
|
|
414
415
|
def convert_file_part_to_openai_format(file)
|
|
415
416
|
if file.image?
|
|
416
417
|
image_url = file.url? ? file.url : "data:#{file.media_type};base64,#{file.data}"
|
|
@@ -34,7 +34,7 @@ class Riffer::Providers::OpenRouter < Riffer::Providers::Base
|
|
|
34
34
|
end
|
|
35
35
|
|
|
36
36
|
#--
|
|
37
|
-
#: (Riffer::Messages::FilePart) -> Symbol
|
|
37
|
+
#: (Riffer::Messages::User::FilePart) -> Symbol
|
|
38
38
|
def file_delivery(file)
|
|
39
39
|
file.image? ? :url : :base64
|
|
40
40
|
end
|
|
@@ -74,7 +74,8 @@ class Riffer::Providers::OpenRouter < Riffer::Providers::Base
|
|
|
74
74
|
} #: Hash[Symbol, untyped]
|
|
75
75
|
|
|
76
76
|
unless tags.empty?
|
|
77
|
-
|
|
77
|
+
# Merged over any metadata set in model_options; a tag wins on a shared key.
|
|
78
|
+
params[:metadata] = (params[:metadata] || {}).merge(tags)
|
|
78
79
|
# OpenRouter exposes the legacy Chat Completions user field rather than
|
|
79
80
|
# safety_identifier; the reserved user_id maps there and stays in metadata.
|
|
80
81
|
user = tags["user_id"]
|
|
@@ -223,7 +224,7 @@ class Riffer::Providers::OpenRouter < Riffer::Providers::Base
|
|
|
223
224
|
emit_tool_call_done_events(state: state, yielder: yielder) unless state[:tool_calls].empty?
|
|
224
225
|
|
|
225
226
|
yielder << Riffer::StreamEvents::TextDone.new(state[:text]) unless state[:text].empty?
|
|
226
|
-
yielder
|
|
227
|
+
yield_reasoning_done(yielder, state[:reasoning]) unless state[:reasoning].empty?
|
|
227
228
|
yield_finish_reason(yielder, build_finish_reason(state[:finish_reason], native: state[:native_finish_reason]))
|
|
228
229
|
end
|
|
229
230
|
|
|
@@ -362,7 +363,7 @@ class Riffer::Providers::OpenRouter < Riffer::Providers::Base
|
|
|
362
363
|
type: "function",
|
|
363
364
|
function: {
|
|
364
365
|
name: encode_tool_name(tc.name),
|
|
365
|
-
arguments: tc.arguments
|
|
366
|
+
arguments: tc.arguments,
|
|
366
367
|
},
|
|
367
368
|
}
|
|
368
369
|
end
|
|
@@ -372,7 +373,7 @@ class Riffer::Providers::OpenRouter < Riffer::Providers::Base
|
|
|
372
373
|
end
|
|
373
374
|
|
|
374
375
|
#--
|
|
375
|
-
#: (Riffer::Messages::FilePart) -> Hash[Symbol, untyped]
|
|
376
|
+
#: (Riffer::Messages::User::FilePart) -> Hash[Symbol, untyped]
|
|
376
377
|
def convert_file_part_to_chat_completions_format(file)
|
|
377
378
|
if file.image?
|
|
378
379
|
image_url = file.url? ? file.url : "data:#{file.media_type};base64,#{file.data}"
|
|
@@ -4,6 +4,22 @@
|
|
|
4
4
|
# Normalized token usage for an LLM API call. Buckets carry the same
|
|
5
5
|
# meaning for every provider.
|
|
6
6
|
class Riffer::Providers::TokenUsage
|
|
7
|
+
# Builds a TokenUsage from a hash, or returns +usage+ unchanged when it is
|
|
8
|
+
# already a TokenUsage.
|
|
9
|
+
#--
|
|
10
|
+
#: ((Hash[Symbol, untyped] | Riffer::Providers::TokenUsage)) -> Riffer::Providers::TokenUsage
|
|
11
|
+
def self.from_hash(usage)
|
|
12
|
+
return usage if usage.is_a?(Riffer::Providers::TokenUsage)
|
|
13
|
+
|
|
14
|
+
new(
|
|
15
|
+
input_tokens: usage[:input_tokens],
|
|
16
|
+
output_tokens: usage[:output_tokens],
|
|
17
|
+
cache_write_tokens: usage[:cache_write_tokens],
|
|
18
|
+
cache_read_tokens: usage[:cache_read_tokens],
|
|
19
|
+
cost: usage[:cost],
|
|
20
|
+
)
|
|
21
|
+
end
|
|
22
|
+
|
|
7
23
|
# Number of tokens entering the context window, including cache reads and writes.
|
|
8
24
|
attr_reader :input_tokens #: Integer # @dynamic input_tokens
|
|
9
25
|
|
data/lib/riffer/skills/config.rb
CHANGED
|
@@ -67,4 +67,16 @@ class Riffer::Skills::Config
|
|
|
67
67
|
|
|
68
68
|
@activate_tool = value
|
|
69
69
|
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
# +dup+ would leave the copy sharing this one's activation list, so activating
|
|
74
|
+
# a skill on either would reach the other. DeepDup rebuilds the list and
|
|
75
|
+
# returns a Proc as-is, which has nothing to alias.
|
|
76
|
+
#--
|
|
77
|
+
#: (Riffer::Skills::Config) -> void
|
|
78
|
+
def initialize_copy(source)
|
|
79
|
+
super
|
|
80
|
+
@activate = Riffer::Helpers::DeepDup.call(source.activate)
|
|
81
|
+
end
|
|
70
82
|
end
|
|
@@ -1,22 +1,24 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
# rbs_inline: enabled
|
|
3
3
|
|
|
4
|
-
# Represents completed reasoning during streaming; only emitted by
|
|
5
|
-
# that support reasoning (e.g. OpenAI with the reasoning option).
|
|
4
|
+
# Represents one completed reasoning block during streaming; only emitted by
|
|
5
|
+
# providers that support reasoning (e.g. OpenAI with the reasoning option).
|
|
6
6
|
class Riffer::StreamEvents::ReasoningDone < Riffer::StreamEvents::Base
|
|
7
|
-
# The
|
|
8
|
-
|
|
7
|
+
# The reasoning block, which the agent loop accumulates onto the assistant
|
|
8
|
+
# message. Its +text+ is the content the preceding ReasoningDelta events
|
|
9
|
+
# added up to.
|
|
10
|
+
attr_reader :part #: Riffer::Messages::Assistant::ReasoningPart # @dynamic part
|
|
9
11
|
|
|
10
12
|
#--
|
|
11
|
-
#: (
|
|
12
|
-
def initialize(
|
|
13
|
+
#: (Riffer::Messages::Assistant::ReasoningPart, ?role: Symbol) -> void
|
|
14
|
+
def initialize(part, role: :assistant)
|
|
13
15
|
super(role: role)
|
|
14
|
-
@
|
|
16
|
+
@part = part
|
|
15
17
|
end
|
|
16
18
|
|
|
17
19
|
#--
|
|
18
20
|
#: () -> Hash[Symbol, untyped]
|
|
19
21
|
def to_h
|
|
20
|
-
{ role: @role,
|
|
22
|
+
{ role: @role, part: part.to_h }
|
|
21
23
|
end
|
|
22
24
|
end
|
data/lib/riffer/tools/runtime.rb
CHANGED
|
@@ -101,9 +101,9 @@ class Riffer::Tools::Runtime
|
|
|
101
101
|
end
|
|
102
102
|
|
|
103
103
|
#--
|
|
104
|
-
#: (String
|
|
104
|
+
#: (String) -> untyped
|
|
105
105
|
def parse_arguments(arguments)
|
|
106
|
-
return {} if arguments.
|
|
106
|
+
return {} if arguments.empty?
|
|
107
107
|
|
|
108
108
|
JSON.parse(arguments, symbolize_names: true)
|
|
109
109
|
end
|
|
@@ -168,8 +168,7 @@ class Riffer::Tools::Runtime
|
|
|
168
168
|
def capture_tool_arguments(span, tool_call)
|
|
169
169
|
return unless capture_tool_content?(span)
|
|
170
170
|
|
|
171
|
-
arguments
|
|
172
|
-
span.set_attribute("gen_ai.tool.call.arguments", arguments) if arguments
|
|
171
|
+
span.set_attribute("gen_ai.tool.call.arguments", tool_call.arguments)
|
|
173
172
|
end
|
|
174
173
|
|
|
175
174
|
#--
|
|
@@ -71,7 +71,7 @@ module Riffer::Tracing::Capture # :nodoc: all
|
|
|
71
71
|
end
|
|
72
72
|
|
|
73
73
|
#--
|
|
74
|
-
#: (Riffer::Messages::FilePart) -> Hash[Symbol, untyped]
|
|
74
|
+
#: (Riffer::Messages::User::FilePart) -> Hash[Symbol, untyped]
|
|
75
75
|
def file_part(file)
|
|
76
76
|
part = { type: "file", media_type: file.media_type } #: Hash[Symbol, untyped]
|
|
77
77
|
part[:name] = file.filename if file.filename
|
|
@@ -81,10 +81,8 @@ module Riffer::Tracing::Capture # :nodoc: all
|
|
|
81
81
|
# Semconv's tool_call part carries arguments as a JSON object; riffer holds
|
|
82
82
|
# them as a string — parse so the captured payload isn't double-encoded.
|
|
83
83
|
#--
|
|
84
|
-
#: (
|
|
84
|
+
#: (String) -> untyped
|
|
85
85
|
def parse_arguments(arguments)
|
|
86
|
-
return arguments unless arguments.is_a?(String)
|
|
87
|
-
|
|
88
86
|
JSON.parse(arguments)
|
|
89
87
|
rescue JSON::ParserError
|
|
90
88
|
arguments
|
data/lib/riffer/version.rb
CHANGED