Module: Sashiko::Adapters::Anthropic
- Defined in:
- lib/sashiko/adapters/anthropic.rb
Overview
Auto-instrumentation for Anthropic Ruby SDK (or any duck-typed client exposing #create on a Messages-like resource).
Attribute names follow OpenTelemetry GenAI semantic conventions:
https://opentelemetry.io/docs/specs/semconv/gen-ai/
Anthropic-specific prompt-cache metrics are namespaced under gen_ai.anthropic.* since the spec does not yet cover them.
Defined Under Namespace
Modules: Wrapper Classes: Price
Constant Summary collapse
- DEFAULT_PRICING =
Snapshot as of 2026-04. Anthropic adds and retires models on its own schedule and this table will go stale; override at runtime via ‘Sashiko::Adapters::Anthropic.pricing = { … }` when that happens. The cost attribute is silently skipped for models not in this Hash.
::Ractor.make_shareable({ "claude-opus-4-7" => Price.new(input: 15.00, output: 75.00, cache_write: 18.75, cache_read: 1.50), "claude-sonnet-4-6" => Price.new(input: 3.00, output: 15.00, cache_write: 3.75, cache_read: 0.30), "claude-haiku-4-5" => Price.new(input: 1.00, output: 5.00, cache_write: 1.25, cache_read: 0.10), })
- RESPONSE_ATTRS =
Map of response key to span attribute name. Iterating this once is clearer than four near-identical case/in stanzas.
{ id: "gen_ai.response.id", model: "gen_ai.response.model", }.freeze
- USAGE_ATTRS =
{ input_tokens: "gen_ai.usage.input_tokens", output_tokens: "gen_ai.usage.output_tokens", cache_creation_input_tokens: "gen_ai.anthropic.cache_creation_input_tokens", cache_read_input_tokens: "gen_ai.anthropic.cache_read_input_tokens", }.freeze
Class Attribute Summary collapse
Class Method Summary collapse
-
.instrument!(messages_class, tracer: nil) ⇒ Object
Idempotent: prepends Wrapper once per class, then returns the class on every call so callers can chain and so a re-invocation is a safe no-op rather than a silent nil.
-
.instrument_in_box!(box, messages_class_name) ⇒ Object
Ruby 4.0 Ruby::Box variant: apply the prepend only inside the given Box, so the monkey-patch does NOT leak into the main Ruby process.
- .record_response(span, response) ⇒ Object
Class Attribute Details
.pricing ⇒ Object
31 |
# File 'lib/sashiko/adapters/anthropic.rb', line 31 def pricing = @pricing ||= DEFAULT_PRICING |
Class Method Details
.instrument!(messages_class, tracer: nil) ⇒ Object
Idempotent: prepends Wrapper once per class, then returns the class on every call so callers can chain and so a re-invocation is a safe no-op rather than a silent nil.
Pass tracer: to bind this instrumentation to a specific tracer (e.g. a Ruby::Box-local tracer). Subsequent re-invocations with a different tracer overwrite the previous binding so callers can rebind without re-prepending.
41 42 43 44 45 46 47 48 |
# File 'lib/sashiko/adapters/anthropic.rb', line 41 def instrument!(, tracer: nil) unless .instance_variable_get(:@__sashiko_instrumented) .prepend(Wrapper) .instance_variable_set(:@__sashiko_instrumented, true) end .instance_variable_set(:@__sashiko_tracer, tracer) end |
.instrument_in_box!(box, messages_class_name) ⇒ Object
Ruby 4.0 Ruby::Box variant: apply the prepend only inside the given Box, so the monkey-patch does NOT leak into the main Ruby process. Useful when multiple services in the same process want to instrument Anthropic calls independently, or when you want to A/B different adapter versions side-by-side.
Requires the process to be started with RUBY_BOX=1.
box = Ruby::Box.new
box.require "anthropic"
Sashiko::Adapters::Anthropic.instrument_in_box!(box, "Anthropic::Messages")
# Main process's Anthropic::Messages remains untouched.
63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 |
# File 'lib/sashiko/adapters/anthropic.rb', line 63 def instrument_in_box!(box, ) raise Sashiko::Box::NotEnabledError unless Sashiko::Box.enabled? # When OpenTelemetry is loaded inside the box, bind to the # box-local tracer explicitly so Wrapper doesn't fall back to # main's Sashiko.tracer. If OTel isn't loaded yet, fall through # with tracer: nil — Wrapper will resolve at call time, by # which point the caller has typically configured OTel. box.eval(<<~RUBY) require "sashiko/adapters/anthropic" klass = Object.const_get(#{.inspect}) local_tracer = defined?(::OpenTelemetry) ? ::OpenTelemetry.tracer_provider.tracer("sashiko/anthropic") : nil Sashiko::Adapters::Anthropic.instrument!(klass, tracer: local_tracer) RUBY end |
.record_response(span, response) ⇒ Object
93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 |
# File 'lib/sashiko/adapters/anthropic.rb', line 93 def record_response(span, response) if (usage = response[:usage]).is_a?(Hash) set_usage_attributes(span, usage) set_cost(span, response[:model], usage) set_cache_hit_ratio(span, usage) end RESPONSE_ATTRS.each do |key, attr| response[key]&.then { |v| span.set_attribute(attr, v) } end case response[:stop_reason] in String | Symbol => reason span.set_attribute("gen_ai.response.finish_reasons", [reason.to_s]) else end end |