ruby_llm-resilience 0.6.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 +7 -0
- data/CHANGELOG.md +100 -0
- data/LICENSE.txt +22 -0
- data/README.md +226 -0
- data/app/controllers/ruby_llm/resilience/breakers_controller.rb +49 -0
- data/app/views/layouts/ruby_llm/resilience/application.html.erb +60 -0
- data/app/views/ruby_llm/resilience/breakers/index.html.erb +150 -0
- data/config/routes.rb +9 -0
- data/lib/generators/resilience/install_generator.rb +40 -0
- data/lib/generators/resilience/templates/initializer.rb +66 -0
- data/lib/ruby_llm/resilience/breaker.rb +217 -0
- data/lib/ruby_llm/resilience/chain.rb +100 -0
- data/lib/ruby_llm/resilience/configuration.rb +146 -0
- data/lib/ruby_llm/resilience/engine.rb +40 -0
- data/lib/ruby_llm/resilience/errors.rb +29 -0
- data/lib/ruby_llm/resilience/memory_store.rb +90 -0
- data/lib/ruby_llm/resilience/shorthand.rb +10 -0
- data/lib/ruby_llm/resilience/tier_namer.rb +62 -0
- data/lib/ruby_llm/resilience/version.rb +7 -0
- data/lib/ruby_llm/resilience.rb +83 -0
- data/lib/ruby_llm-resilience.rb +3 -0
- metadata +122 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Opt-in top-level alias:
|
|
4
|
+
#
|
|
5
|
+
# require "ruby_llm/resilience/shorthand"
|
|
6
|
+
# Resilience.run("api:openai:embeddings") { ... }
|
|
7
|
+
#
|
|
8
|
+
# Kept out of the default require deliberately — claiming a top-level
|
|
9
|
+
# ::Resilience constant is namespace pollution unless you ask for it.
|
|
10
|
+
Resilience = RubyLLM::Resilience unless defined?(::Resilience)
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Resilience
|
|
5
|
+
# Default service_namer: maps a model name to a per-provider, per-TIER
|
|
6
|
+
# breaker name ("api:anthropic:sonnet").
|
|
7
|
+
#
|
|
8
|
+
# Tier-level (not per-model) breakers are deliberate: provider capacity
|
|
9
|
+
# incidents are tier-correlated (when Sonnet is overloaded, every Sonnet
|
|
10
|
+
# version typically is), and a version rollover (sonnet-4-5 → sonnet-4-6)
|
|
11
|
+
# shouldn't fragment health state across two breakers.
|
|
12
|
+
#
|
|
13
|
+
# Custom naming is one lambda away:
|
|
14
|
+
# config.service_namer = ->(model) { "api:#{model}" } # per-model
|
|
15
|
+
module TierNamer
|
|
16
|
+
module_function
|
|
17
|
+
|
|
18
|
+
def call(model_name)
|
|
19
|
+
provider = Resilience.config.provider_resolver.call(model_name).to_s
|
|
20
|
+
|
|
21
|
+
case provider
|
|
22
|
+
when "anthropic" then anthropic(model_name)
|
|
23
|
+
when "gemini", "google" then google(model_name)
|
|
24
|
+
when "openai" then openai(model_name)
|
|
25
|
+
else "api:unknown:other"
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def anthropic(model_name)
|
|
30
|
+
case model_name.to_s
|
|
31
|
+
when /haiku/ then "api:anthropic:haiku"
|
|
32
|
+
when /sonnet/ then "api:anthropic:sonnet"
|
|
33
|
+
when /opus/ then "api:anthropic:opus"
|
|
34
|
+
else "api:anthropic:other"
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Order matters — flash-lite must be tested before flash, since
|
|
39
|
+
# "gemini-3.1-flash-lite" matches both /flash/ and /flash-lite/.
|
|
40
|
+
def google(model_name)
|
|
41
|
+
case model_name.to_s
|
|
42
|
+
when /flash-lite/ then "api:google:flash-lite"
|
|
43
|
+
when /flash/ then "api:google:flash"
|
|
44
|
+
when /pro/ then "api:google:pro"
|
|
45
|
+
else "api:google:other"
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Order matters — `-nano` / `-mini` / `-pro` must be tested before the
|
|
50
|
+
# bare `^gpt` match (gpt-5.5-pro matches both `-pro` and `^gpt`).
|
|
51
|
+
def openai(model_name)
|
|
52
|
+
case model_name.to_s
|
|
53
|
+
when /-nano/ then "api:openai:gpt-nano"
|
|
54
|
+
when /-mini/ then "api:openai:gpt-mini"
|
|
55
|
+
when /-pro/ then "api:openai:gpt-pro"
|
|
56
|
+
when /^gpt/ then "api:openai:gpt"
|
|
57
|
+
else "api:openai:other"
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "resilience/version"
|
|
4
|
+
require_relative "resilience/errors"
|
|
5
|
+
require_relative "resilience/memory_store"
|
|
6
|
+
require_relative "resilience/tier_namer"
|
|
7
|
+
require_relative "resilience/configuration"
|
|
8
|
+
require_relative "resilience/breaker"
|
|
9
|
+
require_relative "resilience/chain"
|
|
10
|
+
|
|
11
|
+
# Under Rails the dashboard engine loads automatically (mounting it stays
|
|
12
|
+
# opt-in via routes). Outside Rails the core works alone — zero dependencies.
|
|
13
|
+
require_relative "resilience/engine" if defined?(::Rails::Engine)
|
|
14
|
+
|
|
15
|
+
module RubyLLM
|
|
16
|
+
# Circuit breakers and fallback chains for LLM apps.
|
|
17
|
+
#
|
|
18
|
+
# RubyLLM::Resilience.configure do |c|
|
|
19
|
+
# c.cache_store = ActiveSupport::Cache::RedisCacheStore.new(...)
|
|
20
|
+
# c.fallback_models = { "claude-haiku-4-5" => "claude-sonnet-4-6" }
|
|
21
|
+
# c.on_error = ->(error, ctx) { Rails.error.report(error, context: ctx) }
|
|
22
|
+
# c.on_status = ->(service, state) { Appsignal.set_gauge("circuit_breaker.state", state == :open ? 1 : 0, service:) }
|
|
23
|
+
# end
|
|
24
|
+
#
|
|
25
|
+
# RubyLLM::Resilience.run("api:openai:embeddings") { RubyLLM.embed(text) }
|
|
26
|
+
module Resilience
|
|
27
|
+
class << self
|
|
28
|
+
def config
|
|
29
|
+
@config ||= Configuration.new
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Configure once at boot; the configuration freezes afterwards so
|
|
33
|
+
# fiber/thread safety comes from immutability. Use
|
|
34
|
+
# reset_configuration! in tests.
|
|
35
|
+
def configure
|
|
36
|
+
@config ||= Configuration.new
|
|
37
|
+
yield @config
|
|
38
|
+
@config.freeze
|
|
39
|
+
@config
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def reset_configuration!
|
|
43
|
+
@config = Configuration.new
|
|
44
|
+
Breaker.reset_registry!
|
|
45
|
+
@config
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def run(service, &block)
|
|
49
|
+
Chain.run(service, &block)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def run_with_model_fallback(primary_model, fallback: :map, &block)
|
|
53
|
+
Chain.run_with_model_fallback(primary_model, fallback: fallback, &block)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def run_with_fallback(*steps)
|
|
57
|
+
Chain.run_with_fallback(*steps)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def trippable?(error)
|
|
61
|
+
config.resolved_trippable_errors.any? { |klass| error.is_a?(klass) }
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Convenience: the breaker name the configured service_namer would
|
|
65
|
+
# assign to a model.
|
|
66
|
+
def service_for(model_name)
|
|
67
|
+
config.service_namer.call(model_name)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Fallback routing grouped by breaker service, for dashboards:
|
|
71
|
+
# { "api:anthropic:haiku" => ["claude-haiku-4-5 → claude-sonnet-4-6"], ... }
|
|
72
|
+
# A service can carry several routes when multiple models map into the
|
|
73
|
+
# same tier breaker.
|
|
74
|
+
def fallback_routes
|
|
75
|
+
config.fallback_models.keys.each_with_object(Hash.new { |h, k| h[k] = [] }) do |model, routes|
|
|
76
|
+
service = service_for(model)
|
|
77
|
+
chain = [ model, *config.fallbacks_for(model) ]
|
|
78
|
+
routes[service] << chain.join(" → ")
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: ruby_llm-resilience
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.6.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Daniel St Paul
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: rails
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '7.1'
|
|
19
|
+
type: :development
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '7.1'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: rake
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '13.0'
|
|
33
|
+
type: :development
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '13.0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: rspec
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '3.13'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '3.13'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: rspec-rails
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '7.0'
|
|
61
|
+
type: :development
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '7.0'
|
|
68
|
+
description: 'RubyLLM gives you every provider. This gives you what happens when one
|
|
69
|
+
of them goes down: tier-aware circuit breakers, cross-provider fallback chains,
|
|
70
|
+
and fail-open store semantics — battle-tested behind 450k+ production LLM calls.'
|
|
71
|
+
email:
|
|
72
|
+
- danielstpaul@hotmail.com
|
|
73
|
+
executables: []
|
|
74
|
+
extensions: []
|
|
75
|
+
extra_rdoc_files: []
|
|
76
|
+
files:
|
|
77
|
+
- CHANGELOG.md
|
|
78
|
+
- LICENSE.txt
|
|
79
|
+
- README.md
|
|
80
|
+
- app/controllers/ruby_llm/resilience/breakers_controller.rb
|
|
81
|
+
- app/views/layouts/ruby_llm/resilience/application.html.erb
|
|
82
|
+
- app/views/ruby_llm/resilience/breakers/index.html.erb
|
|
83
|
+
- config/routes.rb
|
|
84
|
+
- lib/generators/resilience/install_generator.rb
|
|
85
|
+
- lib/generators/resilience/templates/initializer.rb
|
|
86
|
+
- lib/ruby_llm-resilience.rb
|
|
87
|
+
- lib/ruby_llm/resilience.rb
|
|
88
|
+
- lib/ruby_llm/resilience/breaker.rb
|
|
89
|
+
- lib/ruby_llm/resilience/chain.rb
|
|
90
|
+
- lib/ruby_llm/resilience/configuration.rb
|
|
91
|
+
- lib/ruby_llm/resilience/engine.rb
|
|
92
|
+
- lib/ruby_llm/resilience/errors.rb
|
|
93
|
+
- lib/ruby_llm/resilience/memory_store.rb
|
|
94
|
+
- lib/ruby_llm/resilience/shorthand.rb
|
|
95
|
+
- lib/ruby_llm/resilience/tier_namer.rb
|
|
96
|
+
- lib/ruby_llm/resilience/version.rb
|
|
97
|
+
homepage: https://github.com/danielstpaul/ruby_llm-resilience
|
|
98
|
+
licenses:
|
|
99
|
+
- MIT
|
|
100
|
+
metadata:
|
|
101
|
+
homepage_uri: https://github.com/danielstpaul/ruby_llm-resilience
|
|
102
|
+
source_code_uri: https://github.com/danielstpaul/ruby_llm-resilience
|
|
103
|
+
changelog_uri: https://github.com/danielstpaul/ruby_llm-resilience/blob/main/CHANGELOG.md
|
|
104
|
+
rubygems_mfa_required: 'true'
|
|
105
|
+
rdoc_options: []
|
|
106
|
+
require_paths:
|
|
107
|
+
- lib
|
|
108
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
109
|
+
requirements:
|
|
110
|
+
- - ">="
|
|
111
|
+
- !ruby/object:Gem::Version
|
|
112
|
+
version: '3.2'
|
|
113
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
114
|
+
requirements:
|
|
115
|
+
- - ">="
|
|
116
|
+
- !ruby/object:Gem::Version
|
|
117
|
+
version: '0'
|
|
118
|
+
requirements: []
|
|
119
|
+
rubygems_version: 3.6.9
|
|
120
|
+
specification_version: 4
|
|
121
|
+
summary: Circuit breakers and fallback chains for LLM apps.
|
|
122
|
+
test_files: []
|