squishling 0.5.0 → 0.5.1

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.
@@ -37,7 +37,8 @@ module Squishling
37
37
  # Optional Logger for routing and escalation decisions.
38
38
  attr_accessor :logger
39
39
 
40
- # Optional callable run after each LLM attempt (narrowed by squawk_on) with the raw output (see Squawk), e.g.
40
+ # Optional callable run after each LLM attempt (narrowed by squawk_on), and once more when a token budget or a
41
+ # disagreement stops the call, with the raw output (see Squawk), e.g.
41
42
  # ->(output:, metadata:, error:) { Tracer.record(output, metadata, error) }
42
43
  attr_reader :squawk
43
44
 
@@ -7,10 +7,11 @@ module Squishling
7
7
  # missing API key). Never retried and never passed to squish_fallback.
8
8
  class ConfigurationError < Error; end
9
9
 
10
- # The provider answered a request with HTTP 400 (or another 4xx that isn't a credentials, model, or redirect
11
- # problem). Still a ConfigurationError: never retried, escalated, or passed to a fallback. The provider's message is
12
- # never repeated; the machine-readable status and code its error body carries are, when they are plain tokens
13
- # (e.g. "INVALID_ARGUMENT", "invalid_request_error"). Both are nil when the body has none.
10
+ # The provider answered a request with HTTP 400 (or another 4xx that isn't a credentials, billing, model, redirect,
11
+ # or too-long-input problem; the adapter decides which, see Adapter::Base#http_failure). Still a ConfigurationError:
12
+ # never retried, escalated, or passed to a fallback. The provider's message is never repeated; the machine-readable
13
+ # status and code its error body carries are, when they are plain tokens (e.g. "INVALID_ARGUMENT",
14
+ # "invalid_request_error"). Both are nil when the body has none.
14
15
  class RequestRejectedError < ConfigurationError
15
16
  attr_reader :provider_status, :provider_code
16
17
 
@@ -130,6 +131,7 @@ module Squishling
130
131
  end
131
132
 
132
133
  # The LLM call itself failed (rate limit, server error, timeout, connection) after the
133
- # adapter's own HTTP retries. The original exception is available as #cause.
134
+ # adapter's own HTTP retries. #cause is the adapter's: a sanitized stand-in (Adapter::ProviderFailure) for a provider
135
+ # exception that carries a response body, or the transport error itself when it carries none.
134
136
  class LLMError < Error; end
135
137
  end
@@ -25,13 +25,14 @@ module Squishling
25
25
  Built = Data.define(:prompt, :payload, :purpose)
26
26
 
27
27
  # Runs the harness (Definition#harness) over a Run of the call: the harness decides how the escalation is
28
- # used, and what it returns is checked against the output schema's type.
28
+ # used, and what it returns is checked against the output schema's type. A call that ends on the token budget
29
+ # or a disagreement is reported to squawk once here, as the stop no attempt of its own reported.
29
30
  def call
30
31
  built = build
31
32
  harness = @definition.harness
32
33
  run = Run.new(definition: @definition, receiver: @receiver, inputs: @inputs, prompt: built.prompt,
33
34
  payload: built.payload, purpose: built.purpose, harness:)
34
- accepted(harness.klass.new.call(run, **harness.options), run, harness)
35
+ accepted(call_harness(harness, run), run, harness)
35
36
  end
36
37
 
37
38
  # The first request the call would make (system prompt, output schema, user message), without making it.
@@ -41,6 +42,13 @@ module Squishling
41
42
 
42
43
  private
43
44
 
45
+ def call_harness(harness, run)
46
+ harness.klass.new.call(run, **harness.options)
47
+ rescue BudgetExhaustedError, DisagreementError => e
48
+ run.report_stop(e)
49
+ raise
50
+ end
51
+
44
52
  def build
45
53
  purpose = @definition.purpose(@receiver)
46
54
  schema = @definition.schema
@@ -70,16 +78,26 @@ module Squishling
70
78
  end
71
79
 
72
80
  # Exceptions are sent as their class and message. Plain JSON would send only the message, and with
73
- # json/add/exception loaded it would also send the backtrace, which exposes file paths.
74
- def describe(value)
81
+ # json/add/exception loaded it would also send the backtrace, which exposes file paths. path is the Hashes and
82
+ # Arrays being walked: one that contains itself is a ConfigurationError rather than a SystemStackError.
83
+ def describe(value, path = [])
75
84
  case value
76
85
  when Exception then { class: exception_class_name(value.class), message: value.message }
77
- when Hash then value.transform_values { |item| describe(item) }
78
- when Array then value.map { |item| describe(item) }
86
+ when Hash then within(value, path) { |inner| value.transform_values { |item| describe(item, inner) } }
87
+ when Array then within(value, path) { |inner| value.map { |item| describe(item, inner) } }
79
88
  else value
80
89
  end
81
90
  end
82
91
 
92
+ def within(container, path)
93
+ if path.any? { |ancestor| ancestor.equal?(container) }
94
+ raise ConfigurationError, "#{@definition.label}: an argument or context value contains itself, " \
95
+ "so it can't be sent as JSON"
96
+ end
97
+
98
+ yield path + [container]
99
+ end
100
+
83
101
  # An anonymous error class (Class.new(StandardError)) is named after its closest named ancestor.
84
102
  def exception_class_name(klass)
85
103
  klass = klass.superclass until klass.name
@@ -33,12 +33,12 @@ module Squishling
33
33
  return observer if BUILT_IN.include?(observer)
34
34
 
35
35
  raise ConfigurationError, "#{where} observer must be one of #{BUILT_IN.map(&:inspect).join(', ')}, " \
36
- "an object that responds to around, or false, got #{observer.inspect}"
36
+ "an object that responds to around, or false, got #{Appendices.describe(observer)}"
37
37
  end
38
38
  return observer if observer.respond_to?(:around)
39
39
 
40
40
  raise ConfigurationError, "#{where} observer must respond to around(call) { ... } (or be false), " \
41
- "got #{observer.inspect}"
41
+ "got #{Appendices.describe(observer)}"
42
42
  end
43
43
 
44
44
  # The observer that wraps a request: the configured one, the Coolhand observer when nothing is configured and
@@ -9,8 +9,8 @@ module Squishling
9
9
  # adapter builds, so these would silently replace the model, the conversation, or the strict output format.
10
10
  RESERVED_KEYS = %i[
11
11
  model messages input inputs instructions contents system system_instruction systemInstruction cachedContent
12
- stream stream_options store include response_format text output_config outputConfig tools tool_choice
13
- toolConfig tool_config schema functions function_call
12
+ cached_content stream stream_options store include response_format text output_config outputConfig tools
13
+ tool_choice toolConfig tool_config schema functions function_call
14
14
  ].freeze
15
15
 
16
16
  # Some protocols carry the structured-output format (or tool selection) inside a container that is
@@ -74,6 +74,7 @@ module Squishling
74
74
 
75
75
  return @classes[schema] if @classes.key?(schema)
76
76
 
77
+ require_object_type!(schema)
77
78
  klass = @classes[schema] = define_class(schema)
78
79
  # Memoized first so recursive schemas stop; typing the fields builds nested classes now, so a bad
79
80
  # property name in any object the result types (nested, array items, resolved $refs) raises at schema build.
@@ -83,6 +84,18 @@ module Squishling
83
84
 
84
85
  private
85
86
 
87
+ # `properties` alone constrains objects only, so a schema without `type: "object"` accepts a String or Array
88
+ # that would then sit in the typed result where a nested result is expected. A raw Hash schema must say so.
89
+ def require_object_type!(schema)
90
+ type = schema["type"]
91
+ return if type == "object" || (type.is_a?(Array) && type.include?("object"))
92
+
93
+ raise ConfigurationError,
94
+ "output_schema has a schema with `properties` whose `type` isn't \"object\" (or missing), so a non-object " \
95
+ "value would pass validation and end up in the typed result. Set `type: \"object\"` on it " \
96
+ "(a union such as [\"object\", \"null\"] is also fine)"
97
+ end
98
+
86
99
  def fields_for(schema)
87
100
  schema["properties"].to_h { |key, property| [key.to_sym, converter_for(property)] }
88
101
  end
@@ -25,9 +25,10 @@ module Squishling
25
25
  begin
26
26
  frame.phase = :ruby
27
27
  value = impl.call
28
- rescue NotImplementedError
28
+ rescue NotImplementedError => e
29
29
  # After squish!, the error came from the LLM path (a fallback, a proc), not a missing implementation.
30
- raise if frame.handed_off
30
+ # Otherwise it propagates unless it is the method's own stub (see Wrapper.stub?).
31
+ raise if frame.handed_off || !Wrapper.stub?(e)
31
32
 
32
33
  return route_to_llm(frame, definition, "not implemented")
33
34
  end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Squishling
4
+ class Run
5
+ # What a Run tells the squawk hook (see Squawk): the report of each attempt, and the one final report of a call that
6
+ # a token budget or a disagreement stopped.
7
+ module Reports
8
+ # Set on a stop error once it has been reported (see #report_stop).
9
+ REPORTED = :@squawk_reported
10
+ private_constant :REPORTED
11
+
12
+ # Reports to squawk the BudgetExhaustedError or DisagreementError that stopped the call: one final report with
13
+ # the error, for an ending that no attempt of its own reported (the attempt before a budget stop was already
14
+ # reported, as not final, and a disagreement's samples were both accepted). Nothing was sent, so there is no
15
+ # output, and the provider and params are unknown (the stop comes from the harness, not from a step).
16
+ # Internal: Invoker calls it once, whatever the harness, so a custom harness has nothing to do.
17
+ def report_stop(error)
18
+ hook = @definition.squawk
19
+ attempts = error.attempts.to_i
20
+ return if error.instance_variable_get(REPORTED) || !hook
21
+ return unless Squawk.report?(@definition.squawk_on, attempts, error, true)
22
+
23
+ # Marked first, so an error that comes back through an outer squished call (or a second report_stop) is not
24
+ # reported again, and a hook that raises doesn't get the same error again either.
25
+ error.instance_variable_set(REPORTED, true)
26
+ metadata = squawk_metadata(
27
+ attempt: attempts, attempts:, final: true, model: error.models&.last, input: @prompt.input,
28
+ stopped: error.is_a?(BudgetExhaustedError) ? :budget : :disagreement
29
+ )
30
+ Squawk.call(hook, output: nil, metadata:, error:)
31
+ end
32
+
33
+ private
34
+
35
+ # Runs the observability hook, if any, with this attempt's raw output (nil when the call itself failed),
36
+ # the error that ended it (nil when it was accepted), and what was asked. role is nil for escalation
37
+ # attempts, :sample (with the sample's name) or :judge. reask is whether the attempt retried in the same chat,
38
+ # repeated whether it sent back the reply the attempt before it was rejected for, and final whether no attempt
39
+ # follows it.
40
+ def squawk(prompt, path, attempt, response, error, sample: nil, reask: false, repeated: false,
41
+ final: attempt == path.size)
42
+ hook = @definition.squawk
43
+ return unless hook && Squawk.report?(@definition.squawk_on, attempt, error, final)
44
+
45
+ step = path[attempt - 1]
46
+ metadata = squawk_metadata(
47
+ attempt:, attempts: path.size, final:, repeated:, model: response&.model || step.model,
48
+ provider: step.provider, params: step.request_params(reask), input: prompt.input, response:,
49
+ role: sample ? :sample : prompt.role, sample: sample&.name
50
+ )
51
+ Squawk.call(hook, output: response&.content, metadata:, error:)
52
+ end
53
+
54
+ def squawk_metadata(attempt:, attempts:, final:, model:, input:, repeated: false, provider: nil, params: nil,
55
+ response: nil, role: nil, sample: nil, stopped: nil)
56
+ {
57
+ label:, attempt:, attempts:, final:, repeated:, model:, provider:, params:, input:,
58
+ usage: response&.usage, finish_reason: response&.finish_reason, role:, sample:, stopped:
59
+ }
60
+ end
61
+ end
62
+ end
63
+ end
@@ -14,8 +14,9 @@ module Squishling
14
14
  class Run
15
15
  include Messages
16
16
  include Rejections
17
+ include Reports
17
18
 
18
- private_constant :Messages, :Rejections
19
+ private_constant :Messages, :Rejections, :Reports
19
20
 
20
21
  # One of #samples' outputs, between rounds.
21
22
  Sample = Struct.new(:name, :stage, :chat, :rejected, :result, :reask)
@@ -41,7 +42,9 @@ module Squishling
41
42
  @produced = {}.compare_by_identity
42
43
  end
43
44
 
44
- # Whether value is a result one of this run's primitives returned (not one built by hand).
45
+ # Whether value is a result one of this run's primitives returned (not one built by hand). By identity, so it
46
+ # tells results that are objects (the typed Data of an object schema) apart; a schema that isn't an object
47
+ # returns the bare JSON value, and an immediate such as 1 or true is the same object in every run.
45
48
  def produced?(value)
46
49
  @produced.key?(value)
47
50
  end
@@ -287,26 +290,6 @@ module Squishling
287
290
  @client.chat(step, instructions: prompt.instructions, schema: prompt.schema.llm_schema)
288
291
  end
289
292
 
290
- # Runs the observability hook, if any, with this attempt's raw output (nil when the call itself failed),
291
- # the error that ended it (nil when it was accepted), and what was asked. role is nil for escalation
292
- # attempts, :sample (with the sample's name) or :judge. reask is whether the attempt retried in the same chat,
293
- # repeated whether it sent back the reply the attempt before it was rejected for, and final whether no attempt
294
- # follows it.
295
- def squawk(prompt, path, attempt, response, error, sample: nil, reask: false, repeated: false,
296
- final: attempt == path.size)
297
- hook = @definition.squawk
298
- return unless hook && Squawk.report?(@definition.squawk_on, attempt, error, final)
299
-
300
- step = path[attempt - 1]
301
- metadata = {
302
- label:, attempt:, attempts: path.size, final:, repeated:,
303
- model: response&.model || step.model, provider: step.provider, params: step.request_params(reask),
304
- input: prompt.input, usage: response&.usage, finish_reason: response&.finish_reason,
305
- role: sample ? :sample : prompt.role, sample: sample&.name
306
- }
307
- Squawk.call(hook, output: response&.content, metadata:, error:)
308
- end
309
-
310
293
  # following is the attempt that comes next, which is not the next one when a repeated reply skips ahead.
311
294
  def log_failure(role, attempt, path, reason, following: attempt + 1, repeated: false)
312
295
  step = path[attempt - 1]
@@ -4,8 +4,15 @@ module Squishling
4
4
  # Normalizes the supported output-schema forms into a JSON Schema, validates data against it,
5
5
  # and builds typed results.
6
6
  class Schema
7
- CACHE = {}.compare_by_identity
7
+ # Held weakly by the raw schema (a Schematist class, a Hash), so classes replaced by code reloading can be
8
+ # collected along with their schemas. A Hash compares by content, which yields the same schema.
9
+ CACHE = ObjectSpace::WeakKeyMap.new
8
10
  CACHE_LOCK = Mutex.new
11
+ # What building a validator raises for a $ref (or $schema) it can't resolve. json_schemer's default resolver
12
+ # refuses remote URIs, so none is fetched.
13
+ REF_ERRORS = [
14
+ JSONSchemer::UnknownRef, JSONSchemer::InvalidRefResolution, JSONSchemer::InvalidRefPointer, URI::InvalidURIError
15
+ ].freeze
9
16
 
10
17
  # Conditional keywords (Schematist's `given` and `dependent`) that providers' strict modes don't support.
11
18
  # They're validated locally and kept out of the schema sent to the LLM.
@@ -49,15 +56,15 @@ module Squishling
49
56
  "schema" => provider_schema(@json_schema),
50
57
  "strict" => true
51
58
  }.compact
52
- @validator = JSONSchemer.schema(@json_schema)
59
+ @validator = validator_for(@json_schema)
53
60
  # Built now so a property name the result can't take fails here, not on the first response.
54
61
  @result_class = Result.build(@json_schema)
55
62
  end
56
63
 
57
64
  def validate(data)
58
65
  errors = @validator.validate(data).map { |error| error["error"] }
59
- # A raw Hash schema can leave out type: "object"; the typed result is built from a Hash, so anything else is
60
- # invalid output rather than a NoMethodError out of the builder.
66
+ # A raw Hash schema's type can be a union that includes "object" (a bare `properties` is refused at build);
67
+ # the typed result is built from a Hash, so anything else is invalid output rather than a NoMethodError.
61
68
  errors.empty? && result_class && !data.is_a?(Hash) ? ["root must be an object"] : errors
62
69
  end
63
70
 
@@ -67,6 +74,39 @@ module Squishling
67
74
 
68
75
  private
69
76
 
77
+ # The json_schemer validator, with every $ref resolved now, so one that points nowhere (a remote URI, a missing
78
+ # $defs entry) fails at build and in Squishling.verify, not on the first response.
79
+ def validator_for(json_schema)
80
+ validator = JSONSchemer.schema(json_schema)
81
+ resolve_refs!(validator, {}.compare_by_identity)
82
+ validator
83
+ rescue *REF_ERRORS => e
84
+ raise ConfigurationError, "output_schema has a $ref that can't be resolved (#{e.message}); " \
85
+ "only references within the schema are supported"
86
+ end
87
+
88
+ # Walks the parsed schema (json_schemer's Schema#parsed and Keyword#ref_schema) and into each target a $ref
89
+ # resolves to, so a reference inside a subschema kept under any keyword is checked too. seen stops cycles.
90
+ def resolve_refs!(schema, seen)
91
+ return if seen.key?(schema)
92
+
93
+ seen[schema] = true
94
+ schema.parsed.each_value do |keyword|
95
+ resolve_refs!(keyword.ref_schema, seen) if keyword.respond_to?(:ref_schema)
96
+ subschemas(keyword.parsed).each { |subschema| resolve_refs!(subschema, seen) }
97
+ end
98
+ end
99
+
100
+ # The Schema objects directly under a parsed keyword: itself, in a list, or in a map of names.
101
+ def subschemas(parsed)
102
+ case parsed
103
+ when JSONSchemer::Schema then [parsed]
104
+ when Array then parsed.grep(JSONSchemer::Schema)
105
+ when Hash then parsed.values.grep(JSONSchemer::Schema)
106
+ else []
107
+ end
108
+ end
109
+
70
110
  def to_hash(raw)
71
111
  case raw
72
112
  when Hash then raw
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Squishling
4
+ # Holds a credential (an API key, or the headers that may carry one) so a serializer that walks an object's instance
5
+ # variables can't write it out: to_s, inspect, to_yaml, as_json (so ActiveSupport's instance_values walk too) and
6
+ # to_json all show "[REDACTED]", and Marshal refuses. The value sits in a closure rather than an instance variable;
7
+ # #reveal is the only way to read it, and it is for the code that sends the request. Code running in the process can
8
+ # still call #reveal; this guards against accidental serialization (a logged config, an error tracker, a cache
9
+ # dump), not against a hostile caller.
10
+ class Secret
11
+ REDACTED = "[REDACTED]"
12
+
13
+ def initialize(value)
14
+ @reveal = -> { value }
15
+ freeze
16
+ end
17
+
18
+ def reveal
19
+ @reveal.call
20
+ end
21
+
22
+ def inspect
23
+ REDACTED
24
+ end
25
+
26
+ alias_method :to_s, :inspect
27
+
28
+ def encode_with(coder)
29
+ coder.scalar = REDACTED
30
+ end
31
+
32
+ def as_json(*)
33
+ REDACTED
34
+ end
35
+
36
+ def to_json(*)
37
+ REDACTED.to_json
38
+ end
39
+
40
+ def marshal_dump
41
+ raise TypeError, "Squishling::Secret can't be dumped: it holds a credential"
42
+ end
43
+ end
44
+ private_constant :Secret
45
+ end
@@ -4,14 +4,16 @@ module Squishling
4
4
  # The opt-in observability hook: a callable run after every LLM attempt (or only the rejected and retried
5
5
  # ones, see squawk_on:) with the attempt's raw output, so it can be sent somewhere (an error tracker, a
6
6
  # tracing tool). It's the one sanctioned way for model output to leave the process; nothing runs unless one
7
- # is configured.
7
+ # is configured. A call that a token budget or a disagreement stops gets one more report, with the error and no
8
+ # output (metadata's stopped:), since neither has an attempt of its own.
8
9
  module Squawk
9
10
  KEYWORD_TYPES = %i[key keyreq].freeze
10
11
  POSITIONAL_TYPES = %i[req opt].freeze
11
12
  KEYWORD_CAPTURE_TYPES = %i[key keyreq keyrest].freeze
12
13
  KEYWORDS = %i[output metadata error].freeze
13
14
  # The squawk_on: conditions, each answering "does the hook run for this attempt?" from the attempt's 1-based
14
- # number (the one its metadata carries), its error (nil when accepted), and whether it is the last one.
15
+ # number (the one its metadata carries), its error (nil when accepted), and whether it is the last one. A stopped
16
+ # call's report has an error and is final, so it passes every condition whatever its attempt number (0 or more).
15
17
  CONDITIONS = {
16
18
  all: ->(_attempt, _error, _final) { true },
17
19
  rejections: ->(_attempt, error, _final) { !error.nil? },
@@ -25,7 +27,8 @@ module Squishling
25
27
  # Which attempts reach the hook (squawk_on:): one condition or a list of them, or nil to inherit. :all is
26
28
  # every attempt, :rejections only the ones that errored, :retries those plus any accepted attempt after the
27
29
  # first, and :final the last attempt the escalation allows (metadata's final:), however it ended; an earlier
28
- # accepted attempt is not final. A list runs the hook when any of them holds.
30
+ # accepted attempt is not final. The report of a stopped call is final and has an error, so every
31
+ # condition lets it through. A list runs the hook when any of them holds.
29
32
  def validate_on(value, where)
30
33
  return value if value.nil?
31
34
 
@@ -48,7 +51,7 @@ module Squishling
48
51
  return hook if hook.nil? || hook == false
49
52
 
50
53
  unless hook.respond_to?(:call)
51
- raise ConfigurationError, "#{where} squawk must respond to call (or be false), got #{hook.inspect}"
54
+ raise ConfigurationError, "#{where} squawk must respond to call (or be false), got #{Appendices.describe(hook)}"
52
55
  end
53
56
  if positional_only?(parameters(hook))
54
57
  raise ConfigurationError,
@@ -52,7 +52,8 @@ module Squishling
52
52
  end
53
53
 
54
54
  # What a test asked squawk to record: every attempt's raw output, error, and metadata (role, sample, attempt,
55
- # final, usage, ...). Use Testing.with_squawk, which puts the previous hook back.
55
+ # final, usage, ...), and the report of a call stopped by a token budget or a disagreement (stopped).
56
+ # Use Testing.with_squawk, which puts the previous hook back.
56
57
  class SquawkCapture
57
58
  Call = Data.define(:output, :error, :metadata)
58
59
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Squishling
4
- VERSION = "0.5.0"
4
+ VERSION = "0.5.1"
5
5
  end
@@ -4,6 +4,24 @@ module Squishling
4
4
  # Module prepended to a squishling class. It intercepts squished methods regardless of
5
5
  # whether they're defined before or after the `squish` declaration (or at all).
6
6
  class Wrapper < Module
7
+ # Backtrace locations report real paths, so resolve this one too (a symlinked install keeps __FILE__ as given).
8
+ FILE = File.realpath(__FILE__)
9
+ private_constant :FILE
10
+
11
+ # Raised by the wrapper when a squished method has no implementation at all.
12
+ class NoImplementation < NotImplementedError; end
13
+ private_constant :NoImplementation
14
+
15
+ # Whether a NotImplementedError is the "no implementation" signal rather than an error from called code: the
16
+ # wrapper's own, or one raised directly in the method's own frame (`def x = raise NotImplementedError`). The
17
+ # implementation lambda below sits one frame above the method, so anything raised deeper (a helper, a block,
18
+ # another object, a `super` into an unsquished parent) has more frames in between. A missing backtrace fails
19
+ # closed; an exception re-raised or reused keeps the backtrace of its first raise, so a stub that escapes
20
+ # another squished call (after its `squish!`) still reads as a stub to the caller.
21
+ def self.stub?(error)
22
+ error.is_a?(NoImplementation) || error.backtrace_locations&.at(1)&.absolute_path == FILE
23
+ end
24
+
7
25
  # The method beneath any Squishling wrappers (instance_method resolves to the prepended wrapper first),
8
26
  # or nil when the method has no implementation.
9
27
  def self.implementation(method)
@@ -20,7 +38,7 @@ module Squishling
20
38
  if defined?(super)
21
39
  -> { super(*args, **kwargs, &block) }
22
40
  else
23
- -> { raise NotImplementedError, "#{self.class}##{name} has no implementation" }
41
+ -> { raise NoImplementation, "#{self.class}##{name} has no implementation" }
24
42
  end
25
43
 
26
44
  Router.dispatch(self, name, args, kwargs, impl, wrapper:)
data/lib/squishling.rb CHANGED
@@ -9,10 +9,12 @@ require_relative "squishling/errors"
9
9
  require_relative "squishling/configuration"
10
10
  require_relative "squishling/params"
11
11
  require_relative "squishling/named_registry"
12
+ require_relative "squishling/secret"
12
13
  require_relative "squishling/adapter/error_body"
13
14
  require_relative "squishling/adapter"
14
15
  require_relative "squishling/adapters/ruby_llm"
15
16
  require_relative "squishling/adapters/openai_options"
17
+ require_relative "squishling/adapters/openai_transport"
16
18
  require_relative "squishling/adapters/openai_compatible"
17
19
  require_relative "squishling/squawk"
18
20
  require_relative "squishling/observer"
@@ -36,6 +38,7 @@ require_relative "squishling/llm_client"
36
38
  require_relative "squishling/output_check"
37
39
  require_relative "squishling/run/messages"
38
40
  require_relative "squishling/run/rejections"
41
+ require_relative "squishling/run/reports"
39
42
  require_relative "squishling/run"
40
43
  require_relative "squishling/invoker"
41
44
  require_relative "squishling/judge"
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.5.0
4
+ version: 0.5.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michael Carroll
@@ -74,6 +74,7 @@ files:
74
74
  - lib/squishling/adapter/error_body.rb
75
75
  - lib/squishling/adapters/openai_compatible.rb
76
76
  - lib/squishling/adapters/openai_options.rb
77
+ - lib/squishling/adapters/openai_transport.rb
77
78
  - lib/squishling/adapters/ruby_llm.rb
78
79
  - lib/squishling/appendices.rb
79
80
  - lib/squishling/budget.rb
@@ -107,7 +108,9 @@ files:
107
108
  - lib/squishling/run.rb
108
109
  - lib/squishling/run/messages.rb
109
110
  - lib/squishling/run/rejections.rb
111
+ - lib/squishling/run/reports.rb
110
112
  - lib/squishling/schema.rb
113
+ - lib/squishling/secret.rb
111
114
  - lib/squishling/source.rb
112
115
  - lib/squishling/squawk.rb
113
116
  - lib/squishling/tasks/squishling.rake