riffer 0.32.0 → 0.33.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/.ruby-version +1 -1
- data/CHANGELOG.md +34 -0
- data/README.md +13 -11
- data/docs/01_OVERVIEW.md +2 -0
- data/docs/04_AGENT_LIFECYCLE.md +15 -13
- data/docs/08_MESSAGES.md +39 -5
- data/docs/09_STREAM_EVENTS.md +14 -0
- data/docs/10_CONFIGURATION.md +73 -4
- data/docs/13_SKILLS.md +66 -4
- data/docs/14_MCP.md +2 -1
- data/docs/16_TRACING.md +250 -0
- data/docs/17_METRICS.md +123 -0
- data/docs/providers/07_CUSTOM_PROVIDERS.md +44 -0
- data/lib/riffer/agent/response.rb +11 -2
- data/lib/riffer/agent/run.rb +136 -35
- data/lib/riffer/agent.rb +5 -5
- data/lib/riffer/config.rb +231 -15
- data/lib/riffer/guardrail.rb +8 -0
- data/lib/riffer/guardrails/runner.rb +33 -0
- data/lib/riffer/helpers/boolean.rb +22 -0
- data/lib/riffer/mcp/authenticated_tool.rb +14 -20
- data/lib/riffer/mcp/registration.rb +4 -4
- data/lib/riffer/mcp/tool.rb +23 -0
- data/lib/riffer/mcp/tool_factory.rb +14 -22
- data/lib/riffer/messages/assistant.rb +15 -3
- data/lib/riffer/messages/base.rb +2 -1
- data/lib/riffer/metrics/instruments.rb +25 -0
- data/lib/riffer/metrics/null.rb +14 -0
- data/lib/riffer/metrics/otel.rb +79 -0
- data/lib/riffer/metrics.rb +93 -0
- data/lib/riffer/providers/amazon_bedrock.rb +57 -21
- data/lib/riffer/providers/anthropic.rb +59 -24
- data/lib/riffer/providers/azure_open_ai.rb +7 -0
- data/lib/riffer/providers/base.rb +247 -15
- data/lib/riffer/providers/finish_reason.rb +27 -0
- data/lib/riffer/providers/gemini.rb +59 -11
- data/lib/riffer/providers/mock.rb +30 -9
- data/lib/riffer/providers/open_ai.rb +78 -24
- data/lib/riffer/providers/open_router.rb +56 -16
- data/lib/riffer/providers/repository.rb +9 -0
- data/lib/riffer/providers/token_usage.rb +27 -11
- data/lib/riffer/skills/activate_tool.rb +12 -2
- data/lib/riffer/skills/adapter.rb +15 -0
- data/lib/riffer/skills/context.rb +78 -11
- data/lib/riffer/skills/frontmatter.rb +13 -5
- data/lib/riffer/skills/markdown_adapter.rb +1 -1
- data/lib/riffer/skills/xml_adapter.rb +1 -1
- data/lib/riffer/stream_events/finish_reason_done.rb +34 -0
- data/lib/riffer/tools/runtime.rb +99 -3
- data/lib/riffer/tracing/capture.rb +92 -0
- data/lib/riffer/tracing/null.rb +61 -0
- data/lib/riffer/tracing/otel.rb +131 -0
- data/lib/riffer/tracing/stream_recorder.rb +51 -0
- data/lib/riffer/tracing.rb +78 -0
- data/lib/riffer/version.rb +1 -1
- data/sig/_private/opentelemetry.rbs +22 -0
- data/sig/generated/riffer/agent/response.rbs +9 -2
- data/sig/generated/riffer/agent/run.rbs +28 -8
- data/sig/generated/riffer/config.rbs +162 -16
- data/sig/generated/riffer/guardrail.rbs +6 -0
- data/sig/generated/riffer/guardrails/runner.rbs +14 -0
- data/sig/generated/riffer/helpers/boolean.rbs +11 -0
- data/sig/generated/riffer/mcp/authenticated_tool.rbs +6 -8
- data/sig/generated/riffer/mcp/registration.rbs +4 -4
- data/sig/generated/riffer/mcp/tool.rbs +19 -0
- data/sig/generated/riffer/mcp/tool_factory.rbs +8 -7
- data/sig/generated/riffer/messages/assistant.rbs +10 -4
- data/sig/generated/riffer/metrics/instruments.rbs +13 -0
- data/sig/generated/riffer/metrics/null.rbs +10 -0
- data/sig/generated/riffer/metrics/otel.rbs +47 -0
- data/sig/generated/riffer/metrics.rbs +71 -0
- data/sig/generated/riffer/providers/amazon_bedrock.rbs +35 -14
- data/sig/generated/riffer/providers/anthropic.rbs +41 -20
- data/sig/generated/riffer/providers/azure_open_ai.rbs +5 -0
- data/sig/generated/riffer/providers/base.rbs +78 -2
- data/sig/generated/riffer/providers/finish_reason.rbs +19 -0
- data/sig/generated/riffer/providers/gemini.rbs +25 -2
- data/sig/generated/riffer/providers/mock.rbs +16 -5
- data/sig/generated/riffer/providers/open_ai.rbs +44 -22
- data/sig/generated/riffer/providers/open_router.rbs +31 -12
- data/sig/generated/riffer/providers/repository.rbs +7 -0
- data/sig/generated/riffer/providers/token_usage.rbs +20 -10
- data/sig/generated/riffer/skills/activate_tool.rbs +7 -1
- data/sig/generated/riffer/skills/adapter.rbs +10 -0
- data/sig/generated/riffer/skills/context.rbs +52 -4
- data/sig/generated/riffer/skills/frontmatter.rbs +10 -3
- data/sig/generated/riffer/stream_events/finish_reason_done.rbs +21 -0
- data/sig/generated/riffer/tools/runtime.rbs +35 -0
- data/sig/generated/riffer/tracing/capture.rbs +46 -0
- data/sig/generated/riffer/tracing/null.rbs +46 -0
- data/sig/generated/riffer/tracing/otel.rbs +83 -0
- data/sig/generated/riffer/tracing/stream_recorder.rbs +31 -0
- data/sig/generated/riffer/tracing.rbs +52 -0
- data/sig/manual/riffer/helpers/boolean.rbs +5 -0
- data/sig/manual/riffer/metrics/null.rbs +5 -0
- data/sig/manual/riffer/metrics.rbs +5 -0
- data/sig/manual/riffer/providers.rbs +9 -0
- data/sig/manual/riffer/tracing/capture.rbs +5 -0
- data/sig/manual/riffer/tracing/null.rbs +5 -0
- data/sig/manual/riffer/tracing.rbs +5 -0
- metadata +40 -4
|
@@ -2,38 +2,35 @@
|
|
|
2
2
|
# rbs_inline: enabled
|
|
3
3
|
|
|
4
4
|
# Wraps MCP-generated tool classes so +tools/call+ resolves
|
|
5
|
-
# +Riffer.config.mcp.credentials+ per invocation,
|
|
6
|
-
# inner class.
|
|
5
|
+
# +Riffer.config.mcp.credentials+ per invocation, copying metadata from the
|
|
6
|
+
# inner class at wrap time.
|
|
7
7
|
module Riffer::Mcp::AuthenticatedTool
|
|
8
8
|
extend self
|
|
9
9
|
|
|
10
10
|
# Returns one wrapper class per inner tool, sharing +manifest+ and +matched_tags+.
|
|
11
11
|
#
|
|
12
12
|
#--
|
|
13
|
-
#: (Array[singleton(Riffer::Tool)], Riffer::Mcp::Manifest, Array[Symbol]) -> Array[singleton(Riffer::Tool)]
|
|
13
|
+
#: (Array[singleton(Riffer::Mcp::Tool)], Riffer::Mcp::Manifest, Array[Symbol]) -> Array[singleton(Riffer::Mcp::Tool)]
|
|
14
14
|
def wrap_all(tool_classes, manifest, matched_tags)
|
|
15
15
|
tool_classes.map { |tc| wrap_one(tc, manifest, matched_tags) }
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
#--
|
|
19
|
-
#: (singleton(Riffer::Tool), Riffer::Mcp::Manifest, Array[Symbol]) -> singleton(Riffer::Tool)
|
|
20
|
-
|
|
21
|
-
# relationship for dynamically created anonymous classes, so the ignore is required.
|
|
22
|
-
def wrap_one(inner_class, manifest, matched_tags) # steep:ignore MethodBodyTypeMismatch
|
|
19
|
+
#: (singleton(Riffer::Mcp::Tool), Riffer::Mcp::Manifest, Array[Symbol]) -> singleton(Riffer::Mcp::Tool)
|
|
20
|
+
def wrap_one(inner_class, manifest, matched_tags)
|
|
23
21
|
inner = inner_class
|
|
24
22
|
man = manifest
|
|
25
23
|
tags = matched_tags
|
|
26
24
|
|
|
27
|
-
Class.new
|
|
28
|
-
|
|
29
|
-
|
|
25
|
+
# steep does not model Class.new's class_eval semantics — the block body
|
|
26
|
+
# typechecks against the enclosing module, so the ivar assignments and the
|
|
27
|
+
# define_method bodies are unresolvable.
|
|
28
|
+
Class.new(Riffer::Mcp::Tool) do
|
|
30
29
|
# steep:ignore:start
|
|
31
30
|
@identifier = inner.identifier
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
define_singleton_method(:description) { inner.description }
|
|
36
|
-
define_singleton_method(:parameters_schema) { |strict: false| inner.parameters_schema(strict: strict) }
|
|
31
|
+
@description = inner.description
|
|
32
|
+
@input_schema = inner.parameters_schema
|
|
33
|
+
@mcp_server_tool_name = inner.mcp_server_tool_name
|
|
37
34
|
|
|
38
35
|
# Creates a fresh client per +tools/call+ so headers from the credentials
|
|
39
36
|
# proc stay current.
|
|
@@ -48,9 +45,6 @@ module Riffer::Mcp::AuthenticatedTool
|
|
|
48
45
|
define_method(:call) do |context:, **kwargs|
|
|
49
46
|
cred = Riffer.config.mcp.credentials
|
|
50
47
|
unless cred
|
|
51
|
-
# `next` rather than `return`: inside define_method the block IS the method
|
|
52
|
-
# body, so both exit :call identically at runtime. `next` avoids a false
|
|
53
|
-
# steep ReturnTypeMismatch that would otherwise need a steep:ignore.
|
|
54
48
|
next inner.new.call(context: context, **kwargs)
|
|
55
49
|
end
|
|
56
50
|
|
|
@@ -61,9 +55,9 @@ module Riffer::Mcp::AuthenticatedTool
|
|
|
61
55
|
end
|
|
62
56
|
|
|
63
57
|
client = build_call_client(man.endpoint, headers)
|
|
64
|
-
text(client.tools_call(
|
|
58
|
+
text(client.tools_call(self.class.mcp_server_tool_name, kwargs))
|
|
65
59
|
end
|
|
66
60
|
# steep:ignore:end
|
|
67
|
-
end
|
|
61
|
+
end #: singleton(Riffer::Mcp::Tool)
|
|
68
62
|
end
|
|
69
63
|
end
|
|
@@ -5,16 +5,16 @@
|
|
|
5
5
|
# +tools/list+ and generates tool classes when a server is registered.
|
|
6
6
|
class Riffer::Mcp::Registration
|
|
7
7
|
# @rbs @cancelled: bool
|
|
8
|
-
# @rbs @tools: Array[singleton(Riffer::Tool)]
|
|
8
|
+
# @rbs @tools: Array[singleton(Riffer::Mcp::Tool)]
|
|
9
9
|
# @rbs @mutex: Thread::Mutex
|
|
10
10
|
|
|
11
11
|
# The manifest that describes this server.
|
|
12
12
|
attr_reader :manifest #: Riffer::Mcp::Manifest
|
|
13
13
|
|
|
14
|
-
# Generated Riffer::Tool subclasses.
|
|
14
|
+
# Generated Riffer::Mcp::Tool subclasses.
|
|
15
15
|
#
|
|
16
16
|
#--
|
|
17
|
-
#: () -> Array[singleton(Riffer::Tool)]
|
|
17
|
+
#: () -> Array[singleton(Riffer::Mcp::Tool)]
|
|
18
18
|
def tools
|
|
19
19
|
@mutex.synchronize { @tools }
|
|
20
20
|
end
|
|
@@ -24,7 +24,7 @@ class Riffer::Mcp::Registration
|
|
|
24
24
|
def initialize(manifest)
|
|
25
25
|
@manifest = manifest
|
|
26
26
|
@cancelled = false
|
|
27
|
-
@tools = [] #: Array[singleton(Riffer::Tool)]
|
|
27
|
+
@tools = [] #: Array[singleton(Riffer::Mcp::Tool)]
|
|
28
28
|
@mutex = Mutex.new
|
|
29
29
|
run_discovery
|
|
30
30
|
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
# Base class for MCP-generated tools.
|
|
5
|
+
class Riffer::Mcp::Tool < Riffer::Tool
|
|
6
|
+
# @rbs self.@mcp_server_tool_name: String?
|
|
7
|
+
# @rbs self.@input_schema: Hash[Symbol, untyped]?
|
|
8
|
+
|
|
9
|
+
# Returns the unprefixed tool name used for +tools/call+ on the MCP server.
|
|
10
|
+
#--
|
|
11
|
+
#: () -> String
|
|
12
|
+
def self.mcp_server_tool_name
|
|
13
|
+
@mcp_server_tool_name || raise(NotImplementedError, "#{self} must set @mcp_server_tool_name")
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Returns the server-published input schema, falling back to the params DSL.
|
|
17
|
+
# MCP schemas are server-defined, so +strict+ is not applied to them.
|
|
18
|
+
#--
|
|
19
|
+
#: (?strict: bool) -> Hash[Symbol, untyped]
|
|
20
|
+
def self.parameters_schema(strict: false)
|
|
21
|
+
@input_schema || super
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
# rbs_inline: enabled
|
|
3
3
|
|
|
4
|
-
# Generates anonymous Riffer::Tool subclasses from MCP tool definitions.
|
|
4
|
+
# Generates anonymous Riffer::Mcp::Tool subclasses from MCP tool definitions.
|
|
5
5
|
# Generated tools delegate +#call+ to the MCP client and skip Riffer's param
|
|
6
6
|
# validation — the MCP server validates inputs.
|
|
7
7
|
module Riffer::Mcp::ToolFactory
|
|
8
8
|
extend self
|
|
9
9
|
|
|
10
|
-
# Builds one Riffer::Tool subclass per tool definition, prefixing names
|
|
11
|
-
# the manifest name to avoid cross-server collisions (e.g.
|
|
12
|
-
# the server-side name stays on +.mcp_server_tool_name+.
|
|
10
|
+
# Builds one Riffer::Mcp::Tool subclass per tool definition, prefixing names
|
|
11
|
+
# with the manifest name to avoid cross-server collisions (e.g.
|
|
12
|
+
# +jira__search+); the server-side name stays on +.mcp_server_tool_name+.
|
|
13
13
|
#--
|
|
14
|
-
#: (String, Riffer::Mcp::Client, Array[Hash[Symbol, untyped]]) -> Array[singleton(Riffer::Tool)]
|
|
14
|
+
#: (String, Riffer::Mcp::Client, Array[Hash[Symbol, untyped]]) -> Array[singleton(Riffer::Mcp::Tool)]
|
|
15
15
|
def build(manifest_name, client, tool_defs)
|
|
16
16
|
tool_defs.map { |td| build_tool_class(manifest_name, client, td) }
|
|
17
17
|
end
|
|
@@ -23,32 +23,24 @@ module Riffer::Mcp::ToolFactory
|
|
|
23
23
|
str.gsub(/[^a-zA-Z0-9_-]/, "_")
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
+
#: (String, Riffer::Mcp::Client, Hash[Symbol, untyped]) -> singleton(Riffer::Mcp::Tool)
|
|
26
27
|
def build_tool_class(manifest_name, client, td)
|
|
27
28
|
prefixed = "#{sanitize_name_component(manifest_name)}__#{sanitize_name_component(td[:name])}"
|
|
28
29
|
|
|
29
|
-
# steep
|
|
30
|
-
#
|
|
31
|
-
#
|
|
32
|
-
Class.new(Riffer::Tool) do
|
|
30
|
+
# steep does not model Class.new's class_eval semantics — the block body
|
|
31
|
+
# typechecks against the enclosing module, so the ivar assignments and the
|
|
32
|
+
# define_method body are unresolvable.
|
|
33
|
+
Class.new(Riffer::Mcp::Tool) do
|
|
33
34
|
# steep:ignore:start
|
|
34
|
-
@mcp_client = client
|
|
35
35
|
@mcp_server_tool_name = td[:name]
|
|
36
|
-
# Set @identifier directly so .identifier does not fall back to
|
|
37
|
-
# Riffer::Helpers::ClassNameConverter.convert(nil) on this anonymous class.
|
|
38
36
|
@identifier = prefixed
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
define_singleton_method(:mcp_server_tool_name) { td[:name] }
|
|
42
|
-
define_singleton_method(:description) { td[:description] }
|
|
43
|
-
define_singleton_method(:parameters_schema) { |strict: false| td[:input_schema] || Riffer::Tool.send(:empty_schema) }
|
|
37
|
+
@description = td[:description]
|
|
38
|
+
@input_schema = td[:input_schema]
|
|
44
39
|
|
|
45
40
|
define_method(:call) do |context:, **kwargs|
|
|
46
|
-
|
|
47
|
-
self.class.instance_variable_get(:@mcp_server_tool_name), kwargs
|
|
48
|
-
)
|
|
49
|
-
text(result)
|
|
41
|
+
text(client.tools_call(self.class.mcp_server_tool_name, kwargs))
|
|
50
42
|
end
|
|
51
43
|
# steep:ignore:end
|
|
52
|
-
end
|
|
44
|
+
end #: singleton(Riffer::Mcp::Tool)
|
|
53
45
|
end
|
|
54
46
|
end
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
# Represents an assistant (LLM) message in a conversation; may include tool
|
|
5
5
|
# calls when the LLM requests tool execution.
|
|
6
6
|
class Riffer::Messages::Assistant < Riffer::Messages::Base
|
|
7
|
-
ToolCall = Struct.new(:call_id, :name, :arguments
|
|
7
|
+
ToolCall = Struct.new(:call_id, :name, :arguments)
|
|
8
8
|
|
|
9
9
|
# Array of tool calls requested by the assistant.
|
|
10
10
|
attr_reader :tool_calls #: Array[Riffer::Messages::Assistant::ToolCall]
|
|
@@ -15,13 +15,24 @@ class Riffer::Messages::Assistant < Riffer::Messages::Base
|
|
|
15
15
|
# Parsed structured output hash, or nil when not applicable.
|
|
16
16
|
attr_reader :structured_output #: Hash[Symbol, untyped]?
|
|
17
17
|
|
|
18
|
+
# Normalized reason the provider finished this response, when reported (see
|
|
19
|
+
# <tt>Riffer::Providers::FinishReason::VALUES</tt>).
|
|
20
|
+
attr_reader :finish_reason #: Symbol?
|
|
21
|
+
|
|
22
|
+
# Raises Riffer::ArgumentError when +finish_reason+ is outside the
|
|
23
|
+
# normalized vocabulary.
|
|
18
24
|
#--
|
|
19
|
-
#: (String, ?id: String?, ?tool_calls: Array[Riffer::Messages::Assistant::ToolCall], ?token_usage: Riffer::Providers::TokenUsage?, ?structured_output: Hash[Symbol, untyped]?) -> void
|
|
20
|
-
def initialize(content, id: nil, tool_calls: [], token_usage: nil, structured_output: nil)
|
|
25
|
+
#: (String, ?id: String?, ?tool_calls: Array[Riffer::Messages::Assistant::ToolCall], ?token_usage: Riffer::Providers::TokenUsage?, ?structured_output: Hash[Symbol, untyped]?, ?finish_reason: Symbol?) -> void
|
|
26
|
+
def initialize(content, id: nil, tool_calls: [], token_usage: nil, structured_output: nil, finish_reason: nil)
|
|
27
|
+
if finish_reason && !Riffer::Providers::FinishReason::VALUES.include?(finish_reason)
|
|
28
|
+
raise Riffer::ArgumentError, "finish_reason must be one of #{Riffer::Providers::FinishReason::VALUES.inspect}, got #{finish_reason.inspect}"
|
|
29
|
+
end
|
|
30
|
+
|
|
21
31
|
super(content, id: id)
|
|
22
32
|
@tool_calls = tool_calls
|
|
23
33
|
@token_usage = token_usage
|
|
24
34
|
@structured_output = structured_output
|
|
35
|
+
@finish_reason = finish_reason
|
|
25
36
|
end
|
|
26
37
|
|
|
27
38
|
#--
|
|
@@ -58,6 +69,7 @@ class Riffer::Messages::Assistant < Riffer::Messages::Base
|
|
|
58
69
|
hash[:tool_calls] = tool_calls.map(&:to_h) unless tool_calls.empty?
|
|
59
70
|
hash[:token_usage] = token_usage.to_h if token_usage
|
|
60
71
|
hash[:structured_output] = structured_output if structured_output?
|
|
72
|
+
hash[:finish_reason] = finish_reason if finish_reason
|
|
61
73
|
hash
|
|
62
74
|
end
|
|
63
75
|
end
|
data/lib/riffer/messages/base.rb
CHANGED
|
@@ -34,7 +34,8 @@ class Riffer::Messages::Base
|
|
|
34
34
|
tc.is_a?(Riffer::Messages::Assistant::ToolCall) ? tc : Riffer::Messages::Assistant::ToolCall.new(**tc)
|
|
35
35
|
}
|
|
36
36
|
structured_output = msg[:structured_output]
|
|
37
|
-
|
|
37
|
+
finish_reason = msg[:finish_reason]&.to_sym
|
|
38
|
+
Riffer::Messages::Assistant.new(content, id: id, tool_calls: tool_calls, structured_output: structured_output, finish_reason: finish_reason)
|
|
38
39
|
when :system
|
|
39
40
|
Riffer::Messages::System.new(content, id: id)
|
|
40
41
|
when :tool
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
# The catalog of metric instruments riffer records. Each handle is a constant
|
|
5
|
+
# that resolves its backend at record time, so it survives a meter-provider swap
|
|
6
|
+
# or a runtime +enabled+ flip.
|
|
7
|
+
module Riffer::Metrics::Instruments # :nodoc: all
|
|
8
|
+
OPERATION_DURATION = Riffer::Metrics.create_histogram(
|
|
9
|
+
"gen_ai.client.operation.duration",
|
|
10
|
+
unit: "s",
|
|
11
|
+
description: "Duration of GenAI client operations"
|
|
12
|
+
) #: Riffer::Metrics::Histogram
|
|
13
|
+
|
|
14
|
+
TOKEN_USAGE = Riffer::Metrics.create_histogram(
|
|
15
|
+
"gen_ai.client.token.usage",
|
|
16
|
+
unit: "{token}",
|
|
17
|
+
description: "Number of input and output tokens used in GenAI operations"
|
|
18
|
+
) #: Riffer::Metrics::Histogram
|
|
19
|
+
|
|
20
|
+
COST = Riffer::Metrics.create_histogram(
|
|
21
|
+
"riffer.gen_ai.cost",
|
|
22
|
+
unit: "USD",
|
|
23
|
+
description: "Cost of GenAI client operations in USD"
|
|
24
|
+
) #: Riffer::Metrics::Histogram
|
|
25
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
# No-op metrics backend, used when the OpenTelemetry metrics API is unavailable
|
|
5
|
+
# or metrics are disabled.
|
|
6
|
+
module Riffer::Metrics::Null # :nodoc: all
|
|
7
|
+
extend self
|
|
8
|
+
|
|
9
|
+
# Ignores the measurement; there is no meter without the OTEL metrics API.
|
|
10
|
+
#--
|
|
11
|
+
#: (String, Numeric, unit: String?, description: String?, attributes: Hash[String, untyped]?) -> void
|
|
12
|
+
def record_histogram(name, value, unit:, description:, attributes:)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
# OTEL-backed metrics backend. <tt>::OpenTelemetry</tt> constants appear only
|
|
5
|
+
# inside method bodies here, so the gem loads and eager-loads cleanly when the
|
|
6
|
+
# OpenTelemetry metrics API is absent.
|
|
7
|
+
class Riffer::Metrics::Otel # :nodoc: all
|
|
8
|
+
SUPPORTED_API_VERSIONS = Gem::Requirement.new(">= 0.2", "< 1.0") #: Gem::Requirement
|
|
9
|
+
|
|
10
|
+
class << self
|
|
11
|
+
# Builds a backend when the OpenTelemetry metrics API is loadable at a
|
|
12
|
+
# supported version; returns +nil+ so resolution falls back to Null.
|
|
13
|
+
#--
|
|
14
|
+
#: (provider: untyped) -> Riffer::Metrics::Otel?
|
|
15
|
+
def build(provider:)
|
|
16
|
+
version = api_version
|
|
17
|
+
return nil unless version
|
|
18
|
+
|
|
19
|
+
unless supported?(version)
|
|
20
|
+
Kernel.warn "riffer: opentelemetry-metrics-api #{version} is outside the supported range (#{SUPPORTED_API_VERSIONS}); metrics are disabled"
|
|
21
|
+
return nil
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
new(provider: provider || ::OpenTelemetry.meter_provider)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Whether the OpenTelemetry metrics API gem is loadable at a supported
|
|
28
|
+
# version.
|
|
29
|
+
#--
|
|
30
|
+
#: () -> bool
|
|
31
|
+
def available?
|
|
32
|
+
version = api_version
|
|
33
|
+
!version.nil? && supported?(version)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Whether the given opentelemetry-metrics-api version is one riffer codes
|
|
37
|
+
# against. The gem is undeclared, so this guard is the only protection
|
|
38
|
+
# against an incompatible, still-pre-1.0 API.
|
|
39
|
+
#--
|
|
40
|
+
#: (Gem::Version) -> bool
|
|
41
|
+
def supported?(version)
|
|
42
|
+
SUPPORTED_API_VERSIONS.satisfied_by?(version)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
#--
|
|
48
|
+
#: () -> Gem::Version?
|
|
49
|
+
def api_version
|
|
50
|
+
require "opentelemetry-metrics-api"
|
|
51
|
+
spec = Gem.loaded_specs["opentelemetry-metrics-api"] #: untyped
|
|
52
|
+
spec&.version
|
|
53
|
+
rescue ::LoadError
|
|
54
|
+
nil
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# @rbs @meter: untyped
|
|
59
|
+
# @rbs @instruments: Hash[String, untyped]
|
|
60
|
+
# @rbs @mutex: Mutex
|
|
61
|
+
|
|
62
|
+
#--
|
|
63
|
+
#: (provider: untyped) -> void
|
|
64
|
+
def initialize(provider:)
|
|
65
|
+
@meter = provider.meter("riffer", version: Riffer::VERSION)
|
|
66
|
+
@instruments = {}
|
|
67
|
+
@mutex = Mutex.new
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Records a value onto the named histogram.
|
|
71
|
+
#--
|
|
72
|
+
#: (String, Numeric, unit: String?, description: String?, attributes: Hash[String, untyped]?) -> void
|
|
73
|
+
def record_histogram(name, value, unit:, description:, attributes:)
|
|
74
|
+
histogram = @mutex.synchronize do
|
|
75
|
+
@instruments[name] ||= @meter.create_histogram(name, unit: unit, description: description)
|
|
76
|
+
end
|
|
77
|
+
histogram.record(value, attributes: attributes)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
# Internal metrics port — records OTEL metric instruments when the host bundles
|
|
5
|
+
# the OpenTelemetry metrics API and no-ops otherwise, so riffer never declares
|
|
6
|
+
# an OTEL dependency.
|
|
7
|
+
module Riffer::Metrics # :nodoc: all
|
|
8
|
+
extend self
|
|
9
|
+
|
|
10
|
+
# @rbs @backend: (Riffer::Metrics::Otel | singleton(Riffer::Metrics::Null))?
|
|
11
|
+
|
|
12
|
+
MUTEX = Mutex.new #: Mutex
|
|
13
|
+
|
|
14
|
+
# The Ruby API cannot attach a schema URL to a meter, so the semconv pin
|
|
15
|
+
# lives here as the documented contract version.
|
|
16
|
+
SCHEMA_URL = "https://opentelemetry.io/schemas/1.37.0" #: String
|
|
17
|
+
|
|
18
|
+
# A handle to a named histogram, safe to hold as a constant: it defers backend
|
|
19
|
+
# resolution to record time, so it survives a meter-provider swap or a runtime
|
|
20
|
+
# +enabled+ flip.
|
|
21
|
+
class Histogram
|
|
22
|
+
# @rbs @name: String
|
|
23
|
+
# @rbs @unit: String?
|
|
24
|
+
# @rbs @description: String?
|
|
25
|
+
|
|
26
|
+
#--
|
|
27
|
+
#: (String, ?unit: String?, ?description: String?) -> void
|
|
28
|
+
def initialize(name, unit: nil, description: nil)
|
|
29
|
+
@name = name
|
|
30
|
+
@unit = unit
|
|
31
|
+
@description = description
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
#--
|
|
35
|
+
#: (Numeric, ?attributes: Hash[String, untyped]?) -> void
|
|
36
|
+
def record(value, attributes: nil)
|
|
37
|
+
Riffer::Metrics.record_histogram(@name, value, unit: @unit, description: @description, attributes: attributes)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Returns a handle to the named histogram.
|
|
42
|
+
#--
|
|
43
|
+
#: (String, ?unit: String?, ?description: String?) -> Riffer::Metrics::Histogram
|
|
44
|
+
def create_histogram(name, unit: nil, description: nil)
|
|
45
|
+
Histogram.new(name, unit: unit, description: description)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Records a value onto the named histogram.
|
|
49
|
+
#--
|
|
50
|
+
#: (String, Numeric, ?unit: String?, ?description: String?, ?attributes: Hash[String, untyped]?) -> void
|
|
51
|
+
def record_histogram(name, value, unit: nil, description: nil, attributes: nil)
|
|
52
|
+
return unless Riffer.config.metrics.enabled
|
|
53
|
+
backend.record_histogram(name, value, unit: unit, description: description, attributes: attributes)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Mirrors a span's +recording?+ so a caller can skip work that exists only to
|
|
57
|
+
# feed a metric.
|
|
58
|
+
#--
|
|
59
|
+
#: () -> bool
|
|
60
|
+
def recording?
|
|
61
|
+
Riffer.config.metrics.enabled && backend.is_a?(Otel)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Reads the monotonic clock in seconds — the time source for duration metrics,
|
|
65
|
+
# immune to wall-clock adjustments.
|
|
66
|
+
#--
|
|
67
|
+
#: () -> Float
|
|
68
|
+
def monotonic_now
|
|
69
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Discards the resolved backend so the next record re-resolves it; cached
|
|
73
|
+
# instruments live on that backend, so this clears them too.
|
|
74
|
+
#--
|
|
75
|
+
#: () -> void
|
|
76
|
+
def reset!
|
|
77
|
+
MUTEX.synchronize { @backend = nil }
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
#--
|
|
83
|
+
#: () -> (Riffer::Metrics::Otel | singleton(Riffer::Metrics::Null))
|
|
84
|
+
def backend
|
|
85
|
+
@backend || MUTEX.synchronize { @backend ||= resolve_backend }
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
#--
|
|
89
|
+
#: () -> (Riffer::Metrics::Otel | singleton(Riffer::Metrics::Null))
|
|
90
|
+
def resolve_backend
|
|
91
|
+
Otel.build(provider: Riffer.config.metrics.meter_provider) || Null
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -10,6 +10,15 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
|
|
|
10
10
|
# cross-region (+us.anthropic.claude-...+) ids.
|
|
11
11
|
ANTHROPIC_MODEL_PATTERN = /(?:^|\.)anthropic\./ #: Regexp
|
|
12
12
|
|
|
13
|
+
FINISH_REASONS = {
|
|
14
|
+
"end_turn" => :stop,
|
|
15
|
+
"stop_sequence" => :stop,
|
|
16
|
+
"max_tokens" => :length,
|
|
17
|
+
"tool_use" => :tool_calls,
|
|
18
|
+
"guardrail_intervened" => :content_filter,
|
|
19
|
+
"content_filtered" => :content_filter
|
|
20
|
+
}.freeze #: Hash[String, Symbol]
|
|
21
|
+
|
|
13
22
|
# Returns the skill adapter for the Bedrock model — XML for Anthropic models
|
|
14
23
|
# (which Bedrock hosts alongside other vendors'), else Markdown.
|
|
15
24
|
#--
|
|
@@ -19,6 +28,13 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
|
|
|
19
28
|
Riffer::Skills::MarkdownAdapter
|
|
20
29
|
end
|
|
21
30
|
|
|
31
|
+
# The GenAI semconv well-known provider name.
|
|
32
|
+
#--
|
|
33
|
+
#: () -> String
|
|
34
|
+
def self.semconv_provider_name
|
|
35
|
+
"aws.bedrock"
|
|
36
|
+
end
|
|
37
|
+
|
|
22
38
|
#--
|
|
23
39
|
#: (?api_token: String?, ?region: String?, **untyped) -> void
|
|
24
40
|
def initialize(api_token: nil, region: nil, **options)
|
|
@@ -120,14 +136,39 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
|
|
|
120
136
|
#: (untyped) -> Riffer::Providers::TokenUsage?
|
|
121
137
|
def extract_token_usage(response)
|
|
122
138
|
typed_response = response #: Aws::BedrockRuntime::Client::_ConverseResponseSuccess
|
|
123
|
-
|
|
139
|
+
build_token_usage(typed_response.usage)
|
|
140
|
+
end
|
|
124
141
|
|
|
125
|
-
|
|
126
|
-
|
|
142
|
+
# Converse's +input_tokens+ excludes the cache buckets; TokenUsage's
|
|
143
|
+
# input includes them.
|
|
144
|
+
#--
|
|
145
|
+
#: (untyped) -> Riffer::Providers::TokenUsage
|
|
146
|
+
def build_token_usage(usage)
|
|
147
|
+
cache_write = usage.cache_write_input_tokens
|
|
148
|
+
cache_read = usage.cache_read_input_tokens
|
|
149
|
+
|
|
150
|
+
apply_pricing(Riffer::Providers::TokenUsage.new(
|
|
151
|
+
input_tokens: usage.input_tokens + (cache_write || 0) + (cache_read || 0),
|
|
127
152
|
output_tokens: usage.output_tokens,
|
|
128
|
-
cache_write_tokens:
|
|
129
|
-
cache_read_tokens:
|
|
130
|
-
)
|
|
153
|
+
cache_write_tokens: cache_write,
|
|
154
|
+
cache_read_tokens: cache_read
|
|
155
|
+
))
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
#--
|
|
159
|
+
#: (untyped) -> Riffer::Providers::FinishReason?
|
|
160
|
+
def extract_finish_reason(response)
|
|
161
|
+
typed_response = response #: Aws::BedrockRuntime::Client::_ConverseResponseSuccess
|
|
162
|
+
build_finish_reason(typed_response.stop_reason)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
#--
|
|
166
|
+
#: (untyped) -> Riffer::Providers::FinishReason?
|
|
167
|
+
def build_finish_reason(stop_reason)
|
|
168
|
+
return nil unless stop_reason
|
|
169
|
+
|
|
170
|
+
raw = stop_reason.to_s
|
|
171
|
+
Riffer::Providers::FinishReason.new(reason: FINISH_REASONS.fetch(raw, :other), raw: raw)
|
|
131
172
|
end
|
|
132
173
|
|
|
133
174
|
#--
|
|
@@ -169,7 +210,7 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
|
|
|
169
210
|
end
|
|
170
211
|
|
|
171
212
|
#--
|
|
172
|
-
#: (Hash[Symbol, untyped],
|
|
213
|
+
#: (Hash[Symbol, untyped], Riffer::Providers::_EventSink) -> void
|
|
173
214
|
def execute_stream(params, yielder)
|
|
174
215
|
current_state = {
|
|
175
216
|
text: nil,
|
|
@@ -187,6 +228,8 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
|
|
|
187
228
|
when Aws::BedrockRuntime::Types::ContentBlockStopEvent
|
|
188
229
|
handle_content_block_stop_text_delta(event, state: current_state, yielder: yielder) if current_state[:text]
|
|
189
230
|
handle_content_block_stop_tool_use(event, state: current_state, yielder: yielder) if current_state[:tool_call]
|
|
231
|
+
when Aws::BedrockRuntime::Types::MessageStopEvent
|
|
232
|
+
yield_finish_reason(yielder, build_finish_reason(event.stop_reason))
|
|
190
233
|
when Aws::BedrockRuntime::Types::ConverseStreamMetadataEvent
|
|
191
234
|
handle_metadata_usage(event, state: current_state, yielder: yielder) if event.usage
|
|
192
235
|
else
|
|
@@ -212,7 +255,7 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
|
|
|
212
255
|
end
|
|
213
256
|
|
|
214
257
|
#--
|
|
215
|
-
#: (untyped, state: Hash[Symbol, untyped], yielder:
|
|
258
|
+
#: (untyped, state: Hash[Symbol, untyped], yielder: Riffer::Providers::_EventSink) -> void
|
|
216
259
|
def handle_content_block_start_tool_use(event, state:, yielder:)
|
|
217
260
|
typed_event = event #: Aws::BedrockRuntime::Types::ContentBlockStartEvent
|
|
218
261
|
state[:tool_call] = {
|
|
@@ -223,7 +266,7 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
|
|
|
223
266
|
end
|
|
224
267
|
|
|
225
268
|
#--
|
|
226
|
-
#: (untyped, state: Hash[Symbol, untyped], yielder:
|
|
269
|
+
#: (untyped, state: Hash[Symbol, untyped], yielder: Riffer::Providers::_EventSink) -> void
|
|
227
270
|
def handle_content_block_delta_text_delta(event, state:, yielder:)
|
|
228
271
|
typed_event = event #: Aws::BedrockRuntime::Types::ContentBlockDeltaEvent
|
|
229
272
|
delta_text = typed_event.delta.text
|
|
@@ -233,7 +276,7 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
|
|
|
233
276
|
end
|
|
234
277
|
|
|
235
278
|
#--
|
|
236
|
-
#: (untyped, state: Hash[Symbol, untyped], yielder:
|
|
279
|
+
#: (untyped, state: Hash[Symbol, untyped], yielder: Riffer::Providers::_EventSink) -> void
|
|
237
280
|
def handle_content_block_delta_tool_use(event, state:, yielder:)
|
|
238
281
|
typed_event = event #: Aws::BedrockRuntime::Types::ContentBlockDeltaEvent
|
|
239
282
|
input_delta = typed_event.delta.tool_use.input
|
|
@@ -248,14 +291,14 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
|
|
|
248
291
|
end
|
|
249
292
|
|
|
250
293
|
#--
|
|
251
|
-
#: (untyped, state: Hash[Symbol, untyped], yielder:
|
|
294
|
+
#: (untyped, state: Hash[Symbol, untyped], yielder: Riffer::Providers::_EventSink) -> void
|
|
252
295
|
def handle_content_block_stop_text_delta(_event, state:, yielder:)
|
|
253
296
|
yielder << Riffer::StreamEvents::TextDone.new(state[:text])
|
|
254
297
|
state[:text] = nil
|
|
255
298
|
end
|
|
256
299
|
|
|
257
300
|
#--
|
|
258
|
-
#: (untyped, state: Hash[Symbol, untyped], yielder:
|
|
301
|
+
#: (untyped, state: Hash[Symbol, untyped], yielder: Riffer::Providers::_EventSink) -> void
|
|
259
302
|
def handle_content_block_stop_tool_use(_event, state:, yielder:)
|
|
260
303
|
tool_call = state[:tool_call]
|
|
261
304
|
yielder << Riffer::StreamEvents::ToolCallDone.new(
|
|
@@ -268,17 +311,10 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
|
|
|
268
311
|
end
|
|
269
312
|
|
|
270
313
|
#--
|
|
271
|
-
#: (untyped, state: Hash[Symbol, untyped], yielder:
|
|
314
|
+
#: (untyped, state: Hash[Symbol, untyped], yielder: Riffer::Providers::_EventSink) -> void
|
|
272
315
|
def handle_metadata_usage(event, state:, yielder:)
|
|
273
316
|
typed_event = event #: Aws::BedrockRuntime::Types::ConverseStreamMetadataEvent
|
|
274
|
-
yielder << Riffer::StreamEvents::TokenUsageDone.new(
|
|
275
|
-
token_usage: Riffer::Providers::TokenUsage.new(
|
|
276
|
-
input_tokens: typed_event.usage.input_tokens,
|
|
277
|
-
output_tokens: typed_event.usage.output_tokens,
|
|
278
|
-
cache_write_tokens: typed_event.usage.cache_write_input_tokens,
|
|
279
|
-
cache_read_tokens: typed_event.usage.cache_read_input_tokens
|
|
280
|
-
)
|
|
281
|
-
)
|
|
317
|
+
yielder << Riffer::StreamEvents::TokenUsageDone.new(token_usage: build_token_usage(typed_event.usage))
|
|
282
318
|
end
|
|
283
319
|
|
|
284
320
|
#--
|