ruby_decision_model 0.0.1 → 0.2.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.
@@ -0,0 +1,155 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "time"
4
+
5
+ module RubyDecisionModel
6
+ # Retry rules shared by every provider. Defaults follow the official
7
+ # Typesafe SDKs: two retries after the initial attempt, exponential
8
+ # backoff from 0.5s capped at 5s with up to 25% jitter subtracted,
9
+ # Retry-After honored up to 60s, and a 30s total budget across attempts.
10
+ class RetryPolicy < Data.define(
11
+ :max_retries,
12
+ :backoff_initial,
13
+ :backoff_max,
14
+ :backoff_jitter,
15
+ :http_statuses,
16
+ :respect_retry_after,
17
+ :max_retry_after,
18
+ :retry_connection_errors,
19
+ :retry_timeouts,
20
+ :total_timeout
21
+ )
22
+ DEFAULT_STATUSES = ([408, 429] + (500..599).to_a).freeze
23
+
24
+ TIMEOUT_EXCEPTIONS = [Net::OpenTimeout, Net::ReadTimeout, Net::WriteTimeout].freeze
25
+ CONNECTION_EXCEPTIONS = [
26
+ Errno::ECONNRESET,
27
+ Errno::ECONNREFUSED,
28
+ Errno::ECONNABORTED,
29
+ Errno::EHOSTUNREACH,
30
+ Errno::ENETUNREACH,
31
+ Errno::EPIPE,
32
+ SocketError,
33
+ IOError,
34
+ OpenSSL::SSL::SSLError
35
+ ].freeze
36
+
37
+ def initialize(max_retries: 2, backoff_initial: 0.5, backoff_max: 5.0, backoff_jitter: 0.25,
38
+ http_statuses: DEFAULT_STATUSES, respect_retry_after: true, max_retry_after: 60.0,
39
+ retry_connection_errors: true, retry_timeouts: true, total_timeout: 30.0)
40
+ super
41
+ validate!
42
+ end
43
+
44
+ # Accepts a RetryPolicy, a Hash of overrides, or nil (defaults).
45
+ def self.from(value)
46
+ case value
47
+ when RetryPolicy then value
48
+ when nil then new
49
+ when Hash then new(**value.transform_keys(&:to_sym))
50
+ else
51
+ raise ConfigurationError, "retry must be a RetryPolicy or a Hash of overrides, got #{value.class}"
52
+ end
53
+ end
54
+
55
+ def retryable_status?(status)
56
+ http_statuses.include?(status)
57
+ end
58
+
59
+ def timeout_exception?(exception)
60
+ TIMEOUT_EXCEPTIONS.any? { |klass| exception.is_a?(klass) }
61
+ end
62
+
63
+ def connection_exception?(exception)
64
+ CONNECTION_EXCEPTIONS.any? { |klass| exception.is_a?(klass) }
65
+ end
66
+
67
+ def retryable_exception?(exception)
68
+ return retry_timeouts if timeout_exception?(exception)
69
+ return retry_connection_errors if connection_exception?(exception)
70
+
71
+ false
72
+ end
73
+
74
+ # Seconds to wait before the retry numbered `retry_number` (0 for the
75
+ # first retry). `random` returns a Float in 0...1 and exists so tests can
76
+ # pin the jitter.
77
+ def backoff(retry_number, random: -> { rand })
78
+ base = [backoff_initial * (2**retry_number), backoff_max].min
79
+ [base * (1.0 - (backoff_jitter * random.call)), 0.0].max
80
+ end
81
+
82
+ # Delay before the next retry: the server's Retry-After when present and
83
+ # honored (clamped to max_retry_after), otherwise the computed backoff.
84
+ def delay(retry_number, headers: {}, random: -> { rand })
85
+ hinted = respect_retry_after ? retry_after_seconds(headers) : nil
86
+ return [hinted, max_retry_after].min if hinted
87
+
88
+ backoff(retry_number, random: random)
89
+ end
90
+
91
+ # Reads retry-after-ms (preferred) or Retry-After (seconds or HTTP date).
92
+ # Header names are matched case-insensitively. Returns nil when absent
93
+ # or unparseable.
94
+ def retry_after_seconds(headers)
95
+ return nil unless headers.is_a?(Hash)
96
+
97
+ ms = header_value(headers, "retry-after-ms")
98
+ if ms
99
+ parsed = Float(ms, exception: false)
100
+ return parsed / 1000.0 if parsed && parsed >= 0
101
+ end
102
+
103
+ raw = header_value(headers, "retry-after")
104
+ return nil if raw.nil?
105
+
106
+ seconds = Float(raw, exception: false)
107
+ return seconds if seconds && seconds >= 0
108
+
109
+ begin
110
+ [Time.httpdate(raw.to_s) - Time.now, 0.0].max
111
+ rescue ArgumentError
112
+ nil
113
+ end
114
+ end
115
+
116
+ private
117
+
118
+ def validate!
119
+ unless max_retries.is_a?(Integer) && max_retries >= 0
120
+ raise ConfigurationError, "max_retries must be a non-negative Integer, got #{max_retries.inspect}"
121
+ end
122
+
123
+ { backoff_initial: backoff_initial, backoff_max: backoff_max, max_retry_after: max_retry_after }.each do |name, value|
124
+ next if finite_non_negative?(value)
125
+
126
+ raise ConfigurationError, "#{name} must be a finite non-negative number, got #{value.inspect}"
127
+ end
128
+
129
+ unless finite_non_negative?(backoff_jitter) && backoff_jitter <= 1.0
130
+ raise ConfigurationError, "backoff_jitter must be a number between 0 and 1, got #{backoff_jitter.inspect}"
131
+ end
132
+
133
+ unless total_timeout.nil? || finite_non_negative?(total_timeout)
134
+ raise ConfigurationError, "total_timeout must be nil or a finite non-negative number, got #{total_timeout.inspect}"
135
+ end
136
+
137
+ return if http_statuses.respond_to?(:include?)
138
+
139
+ raise ConfigurationError, "http_statuses must respond to include?, got #{http_statuses.inspect}"
140
+ end
141
+
142
+ def finite_non_negative?(value)
143
+ value.is_a?(Numeric) && value.finite? && value >= 0
144
+ end
145
+
146
+ def header_value(headers, name)
147
+ headers.each do |key, value|
148
+ next unless key.to_s.casecmp?(name)
149
+
150
+ return value.is_a?(Array) ? value.first : value
151
+ end
152
+ nil
153
+ end
154
+ end
155
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RubyDecisionModel
4
- VERSION = "0.0.1"
4
+ VERSION = "0.2.0"
5
5
  end
@@ -1,11 +1,26 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "net/http"
4
+ require "openssl"
5
+
3
6
  require_relative "ruby_decision_model/version"
4
7
  require_relative "ruby_decision_model/errors"
5
8
  require_relative "ruby_decision_model/questions"
9
+ require_relative "ruby_decision_model/images"
6
10
  require_relative "ruby_decision_model/answers"
7
11
  require_relative "ruby_decision_model/response"
12
+ require_relative "ruby_decision_model/providers"
13
+ require_relative "ruby_decision_model/retry_policy"
8
14
  require_relative "ruby_decision_model/client"
9
15
 
10
16
  module RubyDecisionModel
17
+ class << self
18
+ # A memoized default client built from the environment. Reset with
19
+ # `RubyDecisionModel.client = nil`.
20
+ def client
21
+ @client ||= Client.new
22
+ end
23
+
24
+ attr_writer :client
25
+ end
11
26
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby_decision_model
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Obie Fernandez
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-09-18 00:00:00.000000000 Z
11
+ date: 2026-10-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: minitest
@@ -40,9 +40,11 @@ dependencies:
40
40
  version: '13.0'
41
41
  description: Decision models answer typed questions about a state instead of generating
42
42
  text. ruby_decision_model builds noul (yes/no probability), choice, and score questions,
43
- posts them with a state to a decisions endpoint (OpenRouter's /decisions with Typesafe
44
- Jev to start), and returns normalized answers with probabilities, confidence, legends,
45
- and usage.
43
+ posts them with a state and optional images to a decision-model provider (OpenRouter,
44
+ Typesafe, OpenAI, Cloudflare, Perplexity, Databricks, or any System One server such
45
+ as Ollama), and returns the same normalized answers with probabilities, confidence,
46
+ legends, and usage whichever vendor served them. Retries follow the official Typesafe
47
+ SDKs.
46
48
  email:
47
49
  - obiefernandez@gmail.com
48
50
  executables: []
@@ -56,8 +58,19 @@ files:
56
58
  - lib/ruby_decision_model/answers.rb
57
59
  - lib/ruby_decision_model/client.rb
58
60
  - lib/ruby_decision_model/errors.rb
61
+ - lib/ruby_decision_model/images.rb
62
+ - lib/ruby_decision_model/providers.rb
63
+ - lib/ruby_decision_model/providers/base.rb
64
+ - lib/ruby_decision_model/providers/cloudflare.rb
65
+ - lib/ruby_decision_model/providers/databricks.rb
66
+ - lib/ruby_decision_model/providers/open_router.rb
67
+ - lib/ruby_decision_model/providers/openai.rb
68
+ - lib/ruby_decision_model/providers/perplexity.rb
69
+ - lib/ruby_decision_model/providers/system_one.rb
70
+ - lib/ruby_decision_model/providers/typesafe.rb
59
71
  - lib/ruby_decision_model/questions.rb
60
72
  - lib/ruby_decision_model/response.rb
73
+ - lib/ruby_decision_model/retry_policy.rb
61
74
  - lib/ruby_decision_model/version.rb
62
75
  homepage: https://github.com/obie/ruby_decision_model
63
76
  licenses:
@@ -83,5 +96,6 @@ requirements: []
83
96
  rubygems_version: 3.5.11
84
97
  signing_key:
85
98
  specification_version: 4
86
- summary: Ruby client for decision models such as Typesafe Jev
99
+ summary: 'The decision-model interface for Ruby: one client for Jev, gpt-6-luna, Clef,
100
+ pplx-decider, and more'
87
101
  test_files: []