smith-agents 0.12.4 → 0.12.6
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 +4 -4
- data/CHANGELOG.md +42 -0
- data/README.md +1 -1
- data/docs/AGENTS.md +11 -1
- data/docs/ERRORS_AND_RETRIES.md +12 -2
- data/lib/smith/agent/attempt_chat.rb +44 -0
- data/lib/smith/agent/provider_call_timing.rb +4 -2
- data/lib/smith/agent/provider_completion.rb +1 -1
- data/lib/smith/agent/provider_streaming.rb +4 -0
- data/lib/smith/provider_errors.rb +98 -0
- data/lib/smith/version.rb +1 -1
- data/lib/smith.rb +1 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 97813bcba79ce652b05eb860da067e3c566b8ba3450344d66940c9f65d23a1dc
|
|
4
|
+
data.tar.gz: ea20e0a30b7517aab44a43b2668cb5e59fde6b98a829056e8e73ba585338f216
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 37d47a1f6268dc96cd05ce69f4138998d1c5a9649bbb41921882b6ae93a8f5d030586c273f37231501c121dd43492e1a37034cefe88981a5f89138cbf1ccd128
|
|
7
|
+
data.tar.gz: 40eb2c1a4ac979411860e4971b4814e0c2cdaebc39f1a827e956f7fcb2a5ca33c3f34d18aad339edfa1460a8fce07188f505aa174a2e59233d735c50bab258d3
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,48 @@ All notable changes to Smith are documented in this file.
|
|
|
4
4
|
|
|
5
5
|
Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Versioning follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Smith is pre-1.0 and under active development; expect occasional contract tightening between minor versions until 1.0.
|
|
6
6
|
|
|
7
|
+
## [0.12.6] - 2026-10-13
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- An error of a streamed request is what the provider said it was
|
|
12
|
+
(`Smith::ProviderErrors`). RubyLLM builds a streamed request's error
|
|
13
|
+
response itself, guessing its status from the error event's type:
|
|
14
|
+
Anthropic's adapter says 500 unless overloaded, OpenAI's 400 unless a
|
|
15
|
+
server error or a rate limit. With 0.12.5 sending a streamed request
|
|
16
|
+
once, an Anthropic bad request, wrong key or unknown model looked like a
|
|
17
|
+
passing outage (fallbacks and retries instead of failing or moving to
|
|
18
|
+
another account), and an OpenAI wrong key, unknown model, regional refusal
|
|
19
|
+
or rate limit like a bad request (no fallback). Before, RubyLLM's retry
|
|
20
|
+
hid this by garbling the error into one of the response's real status.
|
|
21
|
+
Smith reads the type the provider gave in the body (Anthropic's
|
|
22
|
+
`error.type`, OpenAI's `error.code`, else its `error.type`) and, when it
|
|
23
|
+
names another status, raises what RubyLLM raises for that status, through
|
|
24
|
+
RubyLLM's own mapping (context length included), with the provider's
|
|
25
|
+
message and the response's headers, its cause the error RubyLLM raised.
|
|
26
|
+
An error the provider sent whole keeps its own status.
|
|
27
|
+
|
|
28
|
+
## [0.12.5] - 2026-10-13
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
|
|
32
|
+
- A streamed step's requests are each sent once: its chat takes a copy of
|
|
33
|
+
the agent's config (its context, or RubyLLM's) with RubyLLM's HTTP retries
|
|
34
|
+
(`max_retries`) off, its model resolved again as RubyLLM resolves a chat
|
|
35
|
+
built with a context (the same id and provider, and the agent's own
|
|
36
|
+
`assume_model_exists`, so a model RubyLLM lists keeps its output limit and
|
|
37
|
+
thinking options). Retrying a stream read the next response's error into
|
|
38
|
+
the buffer the first had filled, so the provider's words were lost: a
|
|
39
|
+
refusal for want of credit reached Smith as a bare "Invalid request"
|
|
40
|
+
(`RubyLLM::BadRequestError`) and was never classified as
|
|
41
|
+
`RubyLLM::PaymentRequiredError`, and a spend-cap 429 as a plain rate limit.
|
|
42
|
+
A stream that failed part way was sent again, its text streamed twice.
|
|
43
|
+
Every provider failure of a streamed request, a transient one at connect
|
|
44
|
+
time included, now goes straight to the step's fallbacks and retries, so
|
|
45
|
+
a streamed step that should ride out an outage declares them. Calls that
|
|
46
|
+
do not stream, and requests OpenAI's Responses route sends whole, keep
|
|
47
|
+
RubyLLM's retries. The global config and the agent's are never changed.
|
|
48
|
+
|
|
7
49
|
## [0.12.4] - 2026-10-13
|
|
8
50
|
|
|
9
51
|
### Changed
|
data/README.md
CHANGED
data/docs/AGENTS.md
CHANGED
|
@@ -155,7 +155,17 @@ reason and message id, with `mode: :stream`), and a replayed step streams again
|
|
|
155
155
|
(unless a response memo kept its reply: then its sink gets the text at once).
|
|
156
156
|
Graph inspection shows the stream, and runtime readiness reports a missing sink
|
|
157
157
|
(`:stream_sink_missing`) and an agent whose calls go to the Responses API
|
|
158
|
-
(`:stream_whole_replies`).
|
|
158
|
+
(`:stream_whole_replies`). A streamed step's requests are each sent once:
|
|
159
|
+
its chat takes a copy of the agent's config with RubyLLM's HTTP retries
|
|
160
|
+
(`max_retries`) off and its model resolved again as RubyLLM resolves a chat
|
|
161
|
+
built with a context (a model RubyLLM lists keeps its limits), since a
|
|
162
|
+
retried stream loses the first response's error body (a refusal for want of
|
|
163
|
+
credit would reach Smith as a bare "Invalid request") or streams its text
|
|
164
|
+
again. Every provider failure of a streamed request, a transient one at
|
|
165
|
+
connect time included, takes the step's fallbacks and retries directly, so
|
|
166
|
+
declare them (`fallback_models`, `retry_on Smith::AgentError`) on a streamed
|
|
167
|
+
step that should ride out an outage. A request OpenAI's Responses route sends
|
|
168
|
+
whole keeps the retries.
|
|
159
169
|
|
|
160
170
|
## Batch mode
|
|
161
171
|
|
data/docs/ERRORS_AND_RETRIES.md
CHANGED
|
@@ -113,8 +113,18 @@ naming `RubyLLM::PaymentRequiredError` makes a step that meets it wait for
|
|
|
113
113
|
credit instead of failing, even when a fallback then fails some other way.
|
|
114
114
|
`Smith::Providers::Anthropic::Batches#submit` raises it too. Any other error
|
|
115
115
|
stays as it is: a rate limit whose message mentions a quota (Gemini's) is a
|
|
116
|
-
rate limit. RubyLLM's own HTTP retries (`max_retries`) may repeat a 429
|
|
117
|
-
|
|
116
|
+
rate limit. RubyLLM's own HTTP retries (`max_retries`) may repeat a 429 of a
|
|
117
|
+
call that does not stream before Smith sees it (a refused request is not
|
|
118
|
+
charged); a streamed call is sent once, so its refusal reaches Smith whole.
|
|
119
|
+
|
|
120
|
+
RubyLLM guesses the status of a streamed request's error from the error
|
|
121
|
+
event's type (Anthropic's adapter says 500 unless overloaded, OpenAI's 400
|
|
122
|
+
unless a server error or a rate limit). Smith takes it as what the provider
|
|
123
|
+
said it was, from the type in the body (Anthropic's `error.type`, OpenAI's
|
|
124
|
+
`error.code`, else its `error.type`), through RubyLLM's own mapping, with the
|
|
125
|
+
response's headers: a streamed bad request fails, a wrong key is the
|
|
126
|
+
account's, an unknown model moves to the next candidate, an overload is a
|
|
127
|
+
passing outage. An error the provider sent whole keeps its own status.
|
|
118
128
|
|
|
119
129
|
Name only errors that mean your own systems failed. A provider's own failure
|
|
120
130
|
(`RubyLLM::Error`, or its HTTP client's `Faraday::Error`) is the provider's
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smith
|
|
4
|
+
class Agent
|
|
5
|
+
module AttemptChat
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
# An attempt's chat. A streamed step's sends each request it streams
|
|
9
|
+
# over the wire once: RubyLLM's HTTP retries would send it again on an
|
|
10
|
+
# error, its error body already read into the first request's buffer
|
|
11
|
+
# (so a refusal for want of credit reaches Smith as a bare "Invalid
|
|
12
|
+
# request", never classified) or its text already streamed (the
|
|
13
|
+
# retry's text would join it). Smith's fallbacks and the step's
|
|
14
|
+
# retries take their place. Its chat takes a copy of the agent's config
|
|
15
|
+
# (its context, or RubyLLM's) without the retries, and resolves its
|
|
16
|
+
# model again as RubyLLM builds a chat with a context (the same id and
|
|
17
|
+
# provider, and the agent's own assume_model_exists, so a model RubyLLM
|
|
18
|
+
# knows keeps its limits), before any hold attaches to its provider. A
|
|
19
|
+
# request OpenAI's Responses route sends answers whole, unstreamed,
|
|
20
|
+
# and keeps the retries.
|
|
21
|
+
def attempt_chat(agent_class, model_reference)
|
|
22
|
+
chat = agent_class.chat(**model_reference.chat_options, **bridge_workflow_inputs(agent_class))
|
|
23
|
+
return chat unless @step_stream && streams_over_the_wire?(chat)
|
|
24
|
+
|
|
25
|
+
sent_once(chat, agent_class)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def sent_once(chat, agent_class)
|
|
29
|
+
model = chat.model
|
|
30
|
+
config = (agent_class.context&.config || RubyLLM.config).dup
|
|
31
|
+
config.max_retries = 0
|
|
32
|
+
chat.with_context(RubyLLM::Context.new(config))
|
|
33
|
+
chat.with_model(model.id, provider: model.provider,
|
|
34
|
+
assume_exists: agent_class.chat_kwargs.fetch(:assume_model_exists, false))
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def streams_over_the_wire?(chat)
|
|
38
|
+
return false unless chat.respond_to?(:with_context) && chat.respond_to?(:with_model)
|
|
39
|
+
|
|
40
|
+
(chat.params[:openai_api_mode] || chat.params["openai_api_mode"]).to_s != "responses"
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -123,9 +123,11 @@ module Smith
|
|
|
123
123
|
# have their join target; the error then propagates unchanged. A
|
|
124
124
|
# provider failure's own reported usage is accounted beside the
|
|
125
125
|
# completed prefix, so its trace totals both.
|
|
126
|
-
#
|
|
126
|
+
# An error is recorded as what the provider said it was
|
|
127
|
+
# (ProviderErrors), a billing refusal as the provider account's
|
|
128
|
+
# (BillingRefusals).
|
|
127
129
|
def failed_provider_attempt(error, observed_reference, timer, facts, prefix_usages)
|
|
128
|
-
error = BillingRefusals.classified(error)
|
|
130
|
+
error = BillingRefusals.classified(ProviderErrors.classified(error))
|
|
129
131
|
aborted = !provider_failure?(error)
|
|
130
132
|
usages = aborted ? prefix_usages : [*prefix_usages, ProviderUsage.from_message(error)].compact
|
|
131
133
|
attempt = ProviderAttempt.failure(
|
|
@@ -147,7 +147,7 @@ module Smith
|
|
|
147
147
|
# rubocop:enable Metrics/AbcSize
|
|
148
148
|
|
|
149
149
|
def prepared_attempt_chat(agent_class, prepared_input, model_reference, shape)
|
|
150
|
-
chat = agent_class
|
|
150
|
+
chat = attempt_chat(agent_class, model_reference)
|
|
151
151
|
add_prepared_input(chat, prepared_input, observed_model_reference(chat, fallback: model_reference).provider)
|
|
152
152
|
continue_paused_loop(chat)
|
|
153
153
|
shaped_chat(chat, model_reference, shape)
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "attempt_chat"
|
|
4
|
+
|
|
3
5
|
module Smith
|
|
4
6
|
class Agent
|
|
5
7
|
# The provider calls of a streamed step (Smith::Streaming): before a
|
|
@@ -8,6 +10,8 @@ module Smith
|
|
|
8
10
|
# reason is read from the stream (Providers::StreamFacts), as a
|
|
9
11
|
# streamed reply keeps no raw body.
|
|
10
12
|
module ProviderStreaming
|
|
13
|
+
include AttemptChat
|
|
14
|
+
|
|
11
15
|
# Room for what a pause adds besides its messages (its retry, the
|
|
12
16
|
# attempt's usage).
|
|
13
17
|
PAUSE_MARGIN = 4096
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "ruby_llm"
|
|
5
|
+
|
|
6
|
+
module Smith
|
|
7
|
+
# An error of a streamed request as the provider said it was. RubyLLM
|
|
8
|
+
# builds a streamed request's error response itself, its status a guess
|
|
9
|
+
# from the error event's type (Anthropic's adapter says 500 unless
|
|
10
|
+
# overloaded, OpenAI's 400 unless a server error or a rate limit), so a
|
|
11
|
+
# wrong key, a model the provider does not serve or a refusal by region
|
|
12
|
+
# could look like a passing outage or a bad request. Smith reads the type
|
|
13
|
+
# the provider gave in the body (Anthropic's `error.type`, OpenAI's
|
|
14
|
+
# `error.code`, else its `error.type`) and, when it names another status,
|
|
15
|
+
# raises what RubyLLM raises for that status (its own mapping, context
|
|
16
|
+
# length included), with the provider's message, the response's headers
|
|
17
|
+
# kept, its cause the error RubyLLM raised. An error from a response the
|
|
18
|
+
# provider sent whole (its status the provider's own), or whose body
|
|
19
|
+
# names nothing it knows, stays as it is.
|
|
20
|
+
module ProviderErrors
|
|
21
|
+
STATUSES = {
|
|
22
|
+
"invalid_request_error" => 400,
|
|
23
|
+
"authentication_error" => 401,
|
|
24
|
+
"invalid_api_key" => 401,
|
|
25
|
+
"billing_error" => 402,
|
|
26
|
+
"permission_error" => 403,
|
|
27
|
+
"unsupported_country_region_territory" => 403,
|
|
28
|
+
"not_found_error" => 404,
|
|
29
|
+
"model_not_found" => 404,
|
|
30
|
+
"request_too_large" => 413,
|
|
31
|
+
"rate_limit_error" => 429,
|
|
32
|
+
"rate_limit_exceeded" => 429,
|
|
33
|
+
"timeout_error" => 504
|
|
34
|
+
}.freeze
|
|
35
|
+
private_constant :STATUSES
|
|
36
|
+
|
|
37
|
+
Response = Struct.new(:status, :body)
|
|
38
|
+
Said = Struct.new(:message) do
|
|
39
|
+
def parse_error(_response) = message
|
|
40
|
+
end
|
|
41
|
+
private_constant :Response, :Said
|
|
42
|
+
|
|
43
|
+
module_function
|
|
44
|
+
|
|
45
|
+
# The error as Smith takes it: a streamed request's error whose body
|
|
46
|
+
# names another status as the error RubyLLM raises for that status,
|
|
47
|
+
# anything else unchanged.
|
|
48
|
+
def classified(error)
|
|
49
|
+
return error unless error.is_a?(RubyLLM::Error) && streamed?(error)
|
|
50
|
+
|
|
51
|
+
said = said_of(body_of(error))
|
|
52
|
+
status = named_status(said)
|
|
53
|
+
return error if status.nil? || status == error.response.status.to_i
|
|
54
|
+
|
|
55
|
+
reclassify(error, with_status(error.response, status), said["message"])
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def named_status(said) = said && (STATUSES[said["code"].to_s] || STATUSES[said["type"].to_s])
|
|
59
|
+
|
|
60
|
+
# A response RubyLLM built for a streamed request's error, never one the
|
|
61
|
+
# provider sent whole (a Faraday::Response).
|
|
62
|
+
def streamed?(error)
|
|
63
|
+
response = error.response
|
|
64
|
+
response.respond_to?(:status) && response.respond_to?(:body) && !response.is_a?(Faraday::Response)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def reclassify(error, response, message)
|
|
68
|
+
named = begin
|
|
69
|
+
RubyLLM::ErrorMiddleware.parse_error(provider: Said.new(message.is_a?(String) ? message : nil), response:)
|
|
70
|
+
rescue RubyLLM::Error => e
|
|
71
|
+
e
|
|
72
|
+
end
|
|
73
|
+
return error unless named.is_a?(RubyLLM::Error)
|
|
74
|
+
|
|
75
|
+
begin
|
|
76
|
+
raise named.class.new(response, named.message), cause: error
|
|
77
|
+
rescue RubyLLM::Error => e
|
|
78
|
+
e
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def with_status(response, status)
|
|
83
|
+
response.respond_to?(:merge) ? response.merge(status:) : Response.new(status, response.body)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def said_of(body)
|
|
87
|
+
said = body.is_a?(Hash) ? body["error"] : nil
|
|
88
|
+
said.is_a?(Hash) ? said : nil
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def body_of(error)
|
|
92
|
+
body = error.response.body
|
|
93
|
+
body.is_a?(String) ? JSON.parse(body) : body
|
|
94
|
+
rescue JSON::ParserError
|
|
95
|
+
nil
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
data/lib/smith/version.rb
CHANGED
data/lib/smith.rb
CHANGED
|
@@ -212,6 +212,7 @@ require_relative "smith/tool_failure_notification_failed"
|
|
|
212
212
|
require_relative "smith/persistence_payload_conflict"
|
|
213
213
|
require_relative "smith/exponential_backoff"
|
|
214
214
|
require_relative "smith/interruptions"
|
|
215
|
+
require_relative "smith/provider_errors"
|
|
215
216
|
require_relative "smith/billing_refusals"
|
|
216
217
|
|
|
217
218
|
# Event system (depends on Types)
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: smith-agents
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.12.
|
|
4
|
+
version: 0.12.6
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Samuel Ralak
|
|
@@ -161,6 +161,7 @@ files:
|
|
|
161
161
|
- lib/generators/smith/install/templates/workflow_state.rb.tt
|
|
162
162
|
- lib/smith.rb
|
|
163
163
|
- lib/smith/agent.rb
|
|
164
|
+
- lib/smith/agent/attempt_chat.rb
|
|
164
165
|
- lib/smith/agent/chat_construction.rb
|
|
165
166
|
- lib/smith/agent/completion.rb
|
|
166
167
|
- lib/smith/agent/completion_usage_recording.rb
|
|
@@ -297,6 +298,7 @@ files:
|
|
|
297
298
|
- lib/smith/pricing.rb
|
|
298
299
|
- lib/smith/pricing/catalog_validation.rb
|
|
299
300
|
- lib/smith/pricing_configuration_error.rb
|
|
301
|
+
- lib/smith/provider_errors.rb
|
|
300
302
|
- lib/smith/provider_permanent_failure.rb
|
|
301
303
|
- lib/smith/providers/anthropic/batches.rb
|
|
302
304
|
- lib/smith/providers/batch_exchange.rb
|