squishling 0.4.0 → 0.5.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.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +75 -0
  3. data/README.md +25 -9
  4. data/docs/adapters.md +189 -0
  5. data/docs/configuration.md +53 -18
  6. data/docs/failures.md +123 -18
  7. data/docs/harnesses.md +24 -14
  8. data/docs/measuring-tokens.md +8 -6
  9. data/docs/routing.md +5 -0
  10. data/docs/schemas.md +11 -2
  11. data/docs/testing.md +183 -0
  12. data/docs/verifying.md +84 -0
  13. data/lib/squishling/adapter/error_body.rb +47 -0
  14. data/lib/squishling/adapter.rb +275 -0
  15. data/lib/squishling/adapters/openai_compatible.rb +304 -0
  16. data/lib/squishling/adapters/openai_options.rb +72 -0
  17. data/lib/squishling/adapters/ruby_llm.rb +193 -0
  18. data/lib/squishling/budget.rb +71 -0
  19. data/lib/squishling/class_methods.rb +43 -7
  20. data/lib/squishling/class_registry.rb +63 -0
  21. data/lib/squishling/configuration.rb +57 -6
  22. data/lib/squishling/definition.rb +90 -16
  23. data/lib/squishling/errors.rb +92 -7
  24. data/lib/squishling/harness/judged_squishsum.rb +2 -2
  25. data/lib/squishling/harness/option_types.rb +2 -1
  26. data/lib/squishling/harness/registry.rb +8 -36
  27. data/lib/squishling/invoker.rb +21 -9
  28. data/lib/squishling/judge.rb +16 -3
  29. data/lib/squishling/llm_client.rb +66 -132
  30. data/lib/squishling/model_path.rb +60 -13
  31. data/lib/squishling/named_registry.rb +64 -0
  32. data/lib/squishling/observer.rb +11 -2
  33. data/lib/squishling/output_check.rb +13 -9
  34. data/lib/squishling/params.rb +6 -23
  35. data/lib/squishling/railtie.rb +10 -0
  36. data/lib/squishling/result.rb +37 -16
  37. data/lib/squishling/rotation.rb +5 -0
  38. data/lib/squishling/rspec.rb +18 -0
  39. data/lib/squishling/run/messages.rb +40 -3
  40. data/lib/squishling/run/rejections.rb +43 -0
  41. data/lib/squishling/run.rb +66 -42
  42. data/lib/squishling/schema.rb +10 -9
  43. data/lib/squishling/squawk.rb +33 -3
  44. data/lib/squishling/tasks/squishling.rake +18 -0
  45. data/lib/squishling/testing/adapter.rb +194 -0
  46. data/lib/squishling/testing/checks.rb +39 -0
  47. data/lib/squishling/testing/direct.rb +41 -0
  48. data/lib/squishling/testing/errors.rb +18 -0
  49. data/lib/squishling/testing/fixtures.rb +206 -0
  50. data/lib/squishling/testing/helpers.rb +71 -0
  51. data/lib/squishling/testing/minitest.rb +32 -0
  52. data/lib/squishling/testing/observer.rb +31 -0
  53. data/lib/squishling/testing/reply.rb +60 -0
  54. data/lib/squishling/testing/request.rb +75 -0
  55. data/lib/squishling/testing/rspec.rb +30 -0
  56. data/lib/squishling/testing/script.rb +189 -0
  57. data/lib/squishling/testing/state.rb +73 -0
  58. data/lib/squishling/testing/stubs.rb +112 -0
  59. data/lib/squishling/testing.rb +126 -0
  60. data/lib/squishling/verification.rb +39 -0
  61. data/lib/squishling/verifier.rb +179 -0
  62. data/lib/squishling/version.rb +1 -1
  63. data/lib/squishling.rb +57 -4
  64. metadata +40 -21
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "squishling"
4
+
5
+ require_relative "testing/errors"
6
+ require_relative "testing/reply"
7
+ require_relative "testing/script"
8
+ require_relative "testing/request"
9
+ require_relative "testing/observer"
10
+ require_relative "testing/adapter"
11
+ require_relative "testing/state"
12
+ require_relative "testing/checks"
13
+ require_relative "testing/stubs"
14
+ require_relative "testing/direct"
15
+ require_relative "testing/fixtures"
16
+ require_relative "testing/helpers"
17
+ require_relative "testing/minitest"
18
+ require_relative "testing/rspec"
19
+
20
+ module Squishling
21
+ # Test support, opt-in: `require "squishling/testing"` (it is not loaded with `require "squishling"`, and needs no
22
+ # test framework). A scripted adapter answers in place of a provider and records what was sent to it, plus stubs
23
+ # for the callers of squished methods, schema-driven fixtures, and assertions. See docs/testing.md and the
24
+ # Squishling::Testing::Minitest and ::RSpec mixins.
25
+ module Testing
26
+ class << self
27
+ # Makes the scripted Adapter the one every squished call goes through, and returns it. The block scripts it
28
+ # (see Script); #script adds to it later.
29
+ def install(adapter = Adapter.new)
30
+ yield adapter.script if block_given?
31
+ State.set(:observer, Observer.new(chained_observer(Squishling.config.observer)))
32
+ State.set(:adapter, adapter)
33
+ adapter
34
+ end
35
+
36
+ # Any request that reaches the provider seam raises ProviderCallError, naming the squished method. Call it in a
37
+ # suite-wide before hook: a test that forgot to script its calls then fails at the call, not far from it.
38
+ def forbid_provider_calls!
39
+ State.set(:adapter, ForbiddenAdapter.new)
40
+ end
41
+
42
+ # Runs the block with provider calls forbidden and returns the calls (the squished methods' labels) that tried
43
+ # to reach the provider, even if the block rescued the error. The adapter is put back afterwards.
44
+ def no_llm_call
45
+ config = Squishling.config
46
+ previous = config.adapter
47
+ guard = ForbiddenAdapter.new
48
+ config.adapter = guard
49
+ begin
50
+ yield
51
+ rescue ProviderCallError
52
+ nil
53
+ ensure
54
+ config.adapter = previous
55
+ end
56
+ guard.attempts
57
+ end
58
+
59
+ # See Stubs.result: Klass.call returns a real, schema-validated result of the method's type.
60
+ def stub_result(klass, attrs = nil, method: :call, squished: true, **more, &)
61
+ Stubs.result(klass, Schema.jsonify((attrs || {}).merge(more)), method:, squished:, &)
62
+ end
63
+
64
+ # See Stubs.error: Klass.call raises the real error class with realistic attributes.
65
+ def stub_error(klass, kind, **, &)
66
+ Stubs.error(klass, kind, **, &)
67
+ end
68
+
69
+ # The messages squish_validate and the schema reject this reply with. See Direct.
70
+ def validate(klass, reply, **inputs)
71
+ Direct.validate(klass, reply, **inputs)
72
+ end
73
+
74
+ def fallback_for(klass, error, **inputs)
75
+ Direct.fallback_for(klass, error, **inputs)
76
+ end
77
+
78
+ def preview(klass, **inputs)
79
+ Direct.preview(klass, **inputs)
80
+ end
81
+
82
+ def valid_reply_for(source, **overrides)
83
+ Fixtures.valid_reply_for(source, **overrides)
84
+ end
85
+
86
+ def invalid_reply_for(source, **)
87
+ Fixtures.invalid_reply_for(source, **)
88
+ end
89
+
90
+ # A recorder to assign as config.squawk (or give to with_squawk): every attempt's output, error and metadata.
91
+ def capture_squawk
92
+ SquawkCapture.new
93
+ end
94
+
95
+ # Runs the block with the capture as config.squawk, and puts the previous hook back. A squawk set on a class or
96
+ # a method still takes precedence over the configured one.
97
+ def with_squawk(capture = capture_squawk)
98
+ config = Squishling.config
99
+ previous = config.squawk
100
+ config.squawk = capture
101
+ yield capture
102
+ capture
103
+ ensure
104
+ config.squawk = previous
105
+ end
106
+
107
+ # Puts back what the helpers changed (the adapter and observer) and clears the stubs. The mixins do this
108
+ # after every test; call it yourself with a custom setup.
109
+ def reset!
110
+ Stubs.clear!
111
+ State.restore!
112
+ end
113
+
114
+ private
115
+
116
+ # An observer the app configured itself (an object, or :coolhand) is kept in the chain; the default Coolhand
117
+ # auto-detection, and our own observer from an earlier install, are not.
118
+ def chained_observer(observer)
119
+ return observer.previous if observer.is_a?(Observer)
120
+ return Squishling::Observer.resolve(observer) if observer.is_a?(Symbol)
121
+
122
+ observer if observer.respond_to?(:around)
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Squishling
4
+ # What Squishling.verify found: the problems in the squished classes' setup, none when it is sound.
5
+ module Verification
6
+ # One thing wrong. label names where ("Triage#call", or "adapter" for a setup that isn't one class's).
7
+ Problem = Data.define(:label, :message) do
8
+ # The existing messages already start with the label; say it once.
9
+ def to_s
10
+ message.start_with?(label) ? message : "#{label}: #{message}"
11
+ end
12
+ end
13
+
14
+ class Report
15
+ attr_reader :problems
16
+
17
+ def initialize(problems)
18
+ @problems = problems.dup.freeze
19
+ freeze
20
+ end
21
+
22
+ def ok?
23
+ problems.empty?
24
+ end
25
+
26
+ def size
27
+ problems.size
28
+ end
29
+
30
+ # A lint-style listing, one problem per line.
31
+ def to_s
32
+ return "Squishling: no problems found" if ok?
33
+
34
+ lines = problems.map { |problem| " #{problem}" }
35
+ "Squishling found #{size} #{size == 1 ? 'problem' : 'problems'}:\n#{lines.join("\n")}"
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,179 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Squishling
4
+ # Checks squished classes without making a request: what can only otherwise fail on a method's first call (its
5
+ # output schema and typed result, its callables' keywords) and the settings the layers resolve to. Collects every
6
+ # problem rather than stopping at the first. See Squishling.verify.
7
+ class Verifier
8
+ # Checks one squished method's Definition. Each check returns messages, or raises what the call would.
9
+ class MethodCheck
10
+ CHECKS = %i[schema declarations harness callables].freeze
11
+
12
+ def initialize(definition)
13
+ @definition = definition
14
+ end
15
+
16
+ def problems
17
+ CHECKS.flat_map { |check| run(check) }
18
+ end
19
+
20
+ private
21
+
22
+ def run(check)
23
+ Array(send(check)).map { |message| problem(message) }
24
+ rescue ConfigurationError, ArgumentError => e
25
+ [problem(e.message)]
26
+ end
27
+
28
+ def problem(message)
29
+ Verification::Problem.new(label: @definition.label, message:)
30
+ end
31
+
32
+ # The output schema, its typed result (the Data class a call returns), and the JSON Schema itself.
33
+ def schema
34
+ schema = @definition.schema
35
+ messages = []
36
+ messages << "has no output_schema" unless schema
37
+ messages << "has no purpose" unless @definition.purpose_declared?
38
+ return messages unless schema
39
+
40
+ schema.result_class
41
+ messages + JSONSchemer.schema(schema.json_schema).validate_schema.map { |error| error["error"] }
42
+ end
43
+
44
+ # The escalation, generation params, and squawk this method resolves to across method, class, and config.
45
+ def declarations
46
+ steps = @definition.declared_path.first.reject { |step| step[:model].nil? }
47
+ ModelPath.from_escalation(steps, "#{@definition.label} escalation") if steps.any?
48
+ Params.normalize(@definition.params, "#{@definition.label} params")
49
+ Squawk.validate(@definition.squawk, @definition.label)
50
+ []
51
+ end
52
+
53
+ def harness
54
+ harness = @definition.harness
55
+ messages = []
56
+ unless Harness::Registry.fetch(harness.type).equal?(harness.klass)
57
+ messages << "harness #{harness.type.inspect} was unregistered or replaced after it was declared"
58
+ end
59
+ messages << "the judge needs an adapter that supports judgments" if unsupported_judgment?(harness)
60
+ messages
61
+ end
62
+
63
+ def unsupported_judgment?(harness)
64
+ judge = harness.options[:judge]
65
+ judge.is_a?(Hash) && judge[:type] == :judgment && !Squishling.config.resolved_adapter.judgments?
66
+ rescue ConfigurationError
67
+ false # no usable adapter: reported once, as the adapter's own problem
68
+ end
69
+
70
+ # Callables the method's inputs are passed to as keywords: include_when: of each step, and squish_when.
71
+ def callables
72
+ names, open = @definition.input_names
73
+ gates = @definition.declared_path.first.filter_map do |step|
74
+ [step[:include_when], "escalation step #{step[:model].inspect} include_when:"] if step[:include_when]
75
+ end
76
+ gates << [@definition.predicate, "squish_when"] if @definition.predicate
77
+ gates.flat_map { |callable, what| KeywordCheck.messages(callable, names, open).map { "#{what} #{_1}" } }
78
+ end
79
+ end
80
+
81
+ # Whether a Proc called as instance_exec(**inputs, &proc) accepts the inputs.
82
+ module KeywordCheck
83
+ module_function
84
+
85
+ # Messages for how the proc can't be called with the inputs. open: any keyword may be an input (the method
86
+ # takes **), so none can be known.
87
+ def messages(callable, inputs, open)
88
+ return ["must be a Proc, got #{callable.inspect}"] unless callable.is_a?(Proc)
89
+ return [] if open
90
+
91
+ parameters = callable.parameters
92
+ [missing_keywords(parameters, inputs), unaccepted_keywords(callable, parameters, inputs),
93
+ missing_positionals(callable, parameters, inputs)].compact
94
+ end
95
+
96
+ def missing_keywords(parameters, inputs)
97
+ missing = parameters.filter_map { |type, name| name if type == :keyreq } - inputs
98
+ return if missing.empty?
99
+
100
+ "requires #{keywords(missing)}, which #{missing.one? ? 'is not an input' : 'are not inputs'} of the method"
101
+ end
102
+
103
+ # A lambda with no positional parameters, or a proc that names keywords, raises on a keyword it doesn't take.
104
+ # Other procs and positional lambdas receive the keywords as a Hash, or ignore them.
105
+ def unaccepted_keywords(callable, parameters, inputs)
106
+ types = parameters.map(&:first)
107
+ return if types.include?(:keyrest) || !strict?(callable, types)
108
+
109
+ unaccepted = inputs - parameters.filter_map { |type, name| name if Squawk::KEYWORD_TYPES.include?(type) }
110
+ return if unaccepted.empty?
111
+
112
+ "doesn't accept #{keywords(unaccepted)}, which the method takes as input when a caller passes it (add **)"
113
+ end
114
+
115
+ # A lambda's required positional parameters get the keywords as one Hash, or nothing when there are none or
116
+ # the lambda names keywords itself.
117
+ def missing_positionals(callable, parameters, inputs)
118
+ return unless callable.lambda?
119
+
120
+ types = parameters.map(&:first)
121
+ required = types.count(:req)
122
+ given = types.intersect?(%i[key keyreq keyrest]) || inputs.empty? ? 0 : 1
123
+ return unless required > given
124
+
125
+ "takes #{required} positional argument(s), but is called with keywords only"
126
+ end
127
+
128
+ def strict?(callable, types)
129
+ keywords = types.intersect?(Squawk::KEYWORD_TYPES)
130
+ callable.lambda? ? keywords || !types.intersect?(%i[req opt rest]) : keywords
131
+ end
132
+
133
+ def keywords(names)
134
+ names.map { |name| "#{name}:" }.join(", ")
135
+ end
136
+ end
137
+
138
+ def initialize(classes)
139
+ classes.each { |klass| check_squished!(klass) }
140
+ @classes = classes
141
+ end
142
+
143
+ def call
144
+ Verification::Report.new(adapter_problems + @classes.flat_map { |klass| class_problems(klass) })
145
+ end
146
+
147
+ private
148
+
149
+ def check_squished!(klass)
150
+ return if klass.is_a?(Class) && klass.include?(Squishling)
151
+
152
+ raise ConfigurationError, "#{klass.inspect} doesn't include Squishling"
153
+ end
154
+
155
+ # The configured adapter, or RubyLLM's, must be usable.
156
+ def adapter_problems
157
+ Squishling.config.resolved_adapter
158
+ []
159
+ rescue ConfigurationError => e
160
+ [Verification::Problem.new(label: "adapter", message: e.message)]
161
+ end
162
+
163
+ def class_problems(klass)
164
+ klass.squished_methods.keys.flat_map do |name|
165
+ definition = klass.squishling_definition(name)
166
+ next [] if unused_default?(definition)
167
+
168
+ MethodCheck.new(definition).problems
169
+ end
170
+ end
171
+
172
+ # Every class has the default method (call) squished, whether or not it uses it.
173
+ def unused_default?(definition)
174
+ definition.name == ClassMethods::DEFAULT_METHOD && !definition.implemented? && definition.schema.nil?
175
+ rescue ConfigurationError, ArgumentError
176
+ false
177
+ end
178
+ end
179
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Squishling
4
- VERSION = "0.4.0"
4
+ VERSION = "0.5.0"
5
5
  end
data/lib/squishling.rb CHANGED
@@ -2,13 +2,18 @@
2
2
 
3
3
  require "json"
4
4
  require "json_schemer"
5
- require "ruby_llm"
6
5
  require "schematist"
7
6
 
8
7
  require_relative "squishling/version"
9
8
  require_relative "squishling/errors"
10
9
  require_relative "squishling/configuration"
11
10
  require_relative "squishling/params"
11
+ require_relative "squishling/named_registry"
12
+ require_relative "squishling/adapter/error_body"
13
+ require_relative "squishling/adapter"
14
+ require_relative "squishling/adapters/ruby_llm"
15
+ require_relative "squishling/adapters/openai_options"
16
+ require_relative "squishling/adapters/openai_compatible"
12
17
  require_relative "squishling/squawk"
13
18
  require_relative "squishling/observer"
14
19
  require_relative "squishling/observers/coolhand"
@@ -26,9 +31,11 @@ require_relative "squishling/schema"
26
31
  require_relative "squishling/source"
27
32
  require_relative "squishling/appendices"
28
33
  require_relative "squishling/definition"
34
+ require_relative "squishling/budget"
29
35
  require_relative "squishling/llm_client"
30
36
  require_relative "squishling/output_check"
31
37
  require_relative "squishling/run/messages"
38
+ require_relative "squishling/run/rejections"
32
39
  require_relative "squishling/run"
33
40
  require_relative "squishling/invoker"
34
41
  require_relative "squishling/judge"
@@ -36,6 +43,10 @@ require_relative "squishling/router"
36
43
  require_relative "squishling/wrapper"
37
44
  require_relative "squishling/class_methods"
38
45
  require_relative "squishling/collisions"
46
+ require_relative "squishling/class_registry"
47
+ require_relative "squishling/verification"
48
+ require_relative "squishling/verifier"
49
+ require_relative "squishling/railtie" if defined?(Rails::Railtie)
39
50
 
40
51
  # Include Squishling in a class to make it elastic: its squished methods either run their
41
52
  # Ruby implementation or send their inputs through an LLM and return schema-validated results.
@@ -74,6 +85,47 @@ module Squishling
74
85
  Harness.types
75
86
  end
76
87
 
88
+ # Registers a custom adapter class (see Adapter::Base) under a name, usable as config.adapter = :name, or
89
+ # config.adapter = { type: :name, option: value } for an adapter whose initialize takes options. To use an adapter
90
+ # without registering it, assign an instance: config.adapter = MyAdapter.new. Raises ConfigurationError for a name
91
+ # that is invalid or already taken, including the built-in :ruby_llm and :openai_compatible.
92
+ def register_adapter(name, klass)
93
+ Adapter::Registry.register(name, klass)
94
+ end
95
+
96
+ # The names of the adapters that can be configured: the built-ins, then those registered.
97
+ def adapters
98
+ Adapter::Registry.names
99
+ end
100
+
101
+ # Forgets the registered adapters (the built-ins stay), e.g. between specs.
102
+ def reset_adapters!
103
+ Adapter::Registry.reset!
104
+ end
105
+
106
+ # Every class that includes Squishling, and each subclass of one, that is loaded and hasn't been replaced by a
107
+ # reload. Classes are held weakly, so listing them doesn't keep reloaded ones alive. In Rails, eager load first
108
+ # (Rails.application.eager_load!) to include classes not yet autoloaded.
109
+ def squished_classes
110
+ ClassRegistry.classes
111
+ end
112
+
113
+ # Checks squished classes without making a request: each method's output schema and typed result, its resolved
114
+ # escalation, params, squawk and harness, and the keywords its include_when: and squish_when procs take. Checks
115
+ # the given classes, or every squished_classes when none are given. Returns a Verification::Report listing every
116
+ # problem found (ok? when none).
117
+ def verify(*classes)
118
+ Verifier.new(classes.empty? ? squished_classes : classes.flatten).call
119
+ end
120
+
121
+ # Like verify, but raises a VerificationError (a ConfigurationError) naming every problem. For CI and boot checks.
122
+ def verify!(*classes)
123
+ report = verify(*classes)
124
+ raise VerificationError, report unless report.ok?
125
+
126
+ report
127
+ end
128
+
77
129
  def included(base)
78
130
  raise ConfigurationError, "Squishling can only be included in a class" unless base.is_a?(Class)
79
131
 
@@ -108,10 +160,11 @@ module Squishling
108
160
  # it); return it from the method. The overrides apply to this call only: append_to_purpose adds to (or,
109
161
  # with false, replaces) the declared sections, context is sent alongside the declared squish_context,
110
162
  # model/escalation (one or the other)/provider/purpose/harness replace the declared ones, and params merge
111
- # key by key over them.
163
+ # key by key over them. max_total_output_tokens/max_total_input_tokens cap what the call may spend across all its
164
+ # requests (false lifts a declared cap).
112
165
  def squish!(append_to_purpose: nil, context: nil, purpose: nil, model: nil, escalation: nil, provider: nil,
113
- params: nil, harness: nil)
166
+ params: nil, harness: nil, max_total_output_tokens: nil, max_total_input_tokens: nil)
114
167
  Router.hand_off(self, { append_to_purpose:, context:, purpose:, model:, escalation:, provider:, params:,
115
- harness: })
168
+ harness:, max_total_output_tokens:, max_total_input_tokens: })
116
169
  end
117
170
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: squishling
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michael Carroll
@@ -23,20 +23,6 @@ dependencies:
23
23
  - - "~>"
24
24
  - !ruby/object:Gem::Version
25
25
  version: '2.0'
26
- - !ruby/object:Gem::Dependency
27
- name: ruby_llm
28
- requirement: !ruby/object:Gem::Requirement
29
- requirements:
30
- - - "~>"
31
- - !ruby/object:Gem::Version
32
- version: '2.1'
33
- type: :runtime
34
- prerelease: false
35
- version_requirements: !ruby/object:Gem::Requirement
36
- requirements:
37
- - - "~>"
38
- - !ruby/object:Gem::Version
39
- version: '2.1'
40
26
  - !ruby/object:Gem::Dependency
41
27
  name: schematist
42
28
  requirement: !ruby/object:Gem::Requirement
@@ -53,12 +39,13 @@ dependencies:
53
39
  version: '1.1'
54
40
  description: 'Squishling makes Ruby classes elastic. Add a squishling to a class and
55
41
  it can send inputs straight to an LLM (OpenAI, Anthropic Claude, Google Gemini,
56
- or any provider RubyLLM supports) and return strict-schema-validated, typed results:
57
- the same type your Ruby code returns. Use it to gracefully maintain fickle integrations,
58
- build interfaces for unknown data formats, and rescue production errors by handing
59
- the failing call to the LLM with the source class as context. Write Ruby only for
60
- the paths where token costs justify maintaining it. Per-input routing, validated
61
- retries, and fallbacks for your LLM calls included.'
42
+ or any provider RubyLLM supports, or any OpenAI-compatible endpoint, or your own
43
+ adapter) and return strict-schema-validated, typed results: the same type your Ruby
44
+ code returns. Use it to gracefully maintain fickle integrations, build interfaces
45
+ for unknown data formats, and rescue production errors by handing the failing call
46
+ to the LLM with the source class as context. Write Ruby only for the paths where
47
+ token costs justify maintaining it. Per-input routing, validated retries, and fallbacks
48
+ for your LLM calls included.'
62
49
  email:
63
50
  - mc@coolhandlabs.com
64
51
  executables: []
@@ -70,6 +57,7 @@ files:
70
57
  - README.md
71
58
  - Rakefile
72
59
  - SECURITY.md
60
+ - docs/adapters.md
73
61
  - docs/configuration.md
74
62
  - docs/coolhand.md
75
63
  - docs/failures.md
@@ -79,9 +67,18 @@ files:
79
67
  - docs/observers.md
80
68
  - docs/routing.md
81
69
  - docs/schemas.md
70
+ - docs/testing.md
71
+ - docs/verifying.md
82
72
  - lib/squishling.rb
73
+ - lib/squishling/adapter.rb
74
+ - lib/squishling/adapter/error_body.rb
75
+ - lib/squishling/adapters/openai_compatible.rb
76
+ - lib/squishling/adapters/openai_options.rb
77
+ - lib/squishling/adapters/ruby_llm.rb
83
78
  - lib/squishling/appendices.rb
79
+ - lib/squishling/budget.rb
84
80
  - lib/squishling/class_methods.rb
81
+ - lib/squishling/class_registry.rb
85
82
  - lib/squishling/collisions.rb
86
83
  - lib/squishling/configuration.rb
87
84
  - lib/squishling/definition.rb
@@ -97,18 +94,40 @@ files:
97
94
  - lib/squishling/judge.rb
98
95
  - lib/squishling/llm_client.rb
99
96
  - lib/squishling/model_path.rb
97
+ - lib/squishling/named_registry.rb
100
98
  - lib/squishling/observer.rb
101
99
  - lib/squishling/observers/coolhand.rb
102
100
  - lib/squishling/output_check.rb
103
101
  - lib/squishling/params.rb
102
+ - lib/squishling/railtie.rb
104
103
  - lib/squishling/result.rb
105
104
  - lib/squishling/rotation.rb
106
105
  - lib/squishling/router.rb
106
+ - lib/squishling/rspec.rb
107
107
  - lib/squishling/run.rb
108
108
  - lib/squishling/run/messages.rb
109
+ - lib/squishling/run/rejections.rb
109
110
  - lib/squishling/schema.rb
110
111
  - lib/squishling/source.rb
111
112
  - lib/squishling/squawk.rb
113
+ - lib/squishling/tasks/squishling.rake
114
+ - lib/squishling/testing.rb
115
+ - lib/squishling/testing/adapter.rb
116
+ - lib/squishling/testing/checks.rb
117
+ - lib/squishling/testing/direct.rb
118
+ - lib/squishling/testing/errors.rb
119
+ - lib/squishling/testing/fixtures.rb
120
+ - lib/squishling/testing/helpers.rb
121
+ - lib/squishling/testing/minitest.rb
122
+ - lib/squishling/testing/observer.rb
123
+ - lib/squishling/testing/reply.rb
124
+ - lib/squishling/testing/request.rb
125
+ - lib/squishling/testing/rspec.rb
126
+ - lib/squishling/testing/script.rb
127
+ - lib/squishling/testing/state.rb
128
+ - lib/squishling/testing/stubs.rb
129
+ - lib/squishling/verification.rb
130
+ - lib/squishling/verifier.rb
112
131
  - lib/squishling/version.rb
113
132
  - lib/squishling/wrapper.rb
114
133
  homepage: https://github.com/Coolhand-Labs/squishling