Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]
## [1.1.0] - 2026-09-20

### Added

Expand Down Expand Up @@ -44,6 +44,23 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
frozen on the client (mutating the Hash passed at initialization has no
effect); `evaluate` keeps its signature.

### Documentation

- **The README documents the automatic retries end to end**: retryable errors
(429, 529 and 5xx) and network failures wrapped in a retryable
`Typesafe::ConnectionError` are retried automatically, 2 retries by
default (3 attempts in total). The wait honors the server's `Retry-After`
/ `Retry-After-Ms` header on a 429; otherwise it follows an exponential
backoff, 0.5 s base, doubled on each retry, capped at 8.0 s, with jitter.
The policy is tuned or disabled per client via `retry_options:` on
`Typesafe::Client.new` (inherited by `Typesafe::Jev.new`): `max_retries` /
`base_delay` / `max_delay`, with `max_retries: 0` restoring a single
attempt. Unknown keys and invalid values raise an `ArgumentError` naming
the key at construction time, and the normalized options are frozen on the
client. Retries are silent (no log, no callback) and replay the POST
verbatim; non-retryable errors (400, 401, 403, 404, 422) raise immediately
without any replay. See the README section "Retries".

## [1.0.0] - 2026-09-20

### Added
Expand Down
2 changes: 1 addition & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PATH
remote: .
specs:
typesafe-jev (1.0.0)
typesafe-jev (1.1.0)

GEM
remote: https://rubygems.org/
Expand Down
36 changes: 28 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ Each answer type exposes its own accessors (`#noul`, `#choice`, `#probabilities`

## Errors

Any non-2xx HTTP response raises a typed error. All inherit from `Typesafe::APIError` (root: `Typesafe::Error`), which carries `#status`, `#body`, `#headers`, `#request_id` and `#retryable?`.
Any non-2xx HTTP response raises a typed error. All inherit from `Typesafe::APIError` (root: `Typesafe::Error`), which carries `#status`, `#body`, `#headers`, `#request_id` and `#retryable?`. A network failure that occurs before any HTTP response raises a retryable `Typesafe::ConnectionError` instead (see [Retries](#retries)).

| Error | Status | Extras |
|---|---|---|
Expand All @@ -102,21 +102,39 @@ Any non-2xx HTTP response raises a typed error. All inherit from `Typesafe::APIE
| `Typesafe::RateLimitError` | 429 | `#retry_after` |
| `Typesafe::OverloadedError` | 529 | |
| `Typesafe::ServerError` | 5xx | |
| `Typesafe::ConnectionError` | n/a (network, no HTTP response) | `#cause` (wrapped exception) |

Retryable errors (429, 529, 5xx) and `ConnectionError` are retried automatically, then raised once the retry budget is exhausted; non-retryable errors (400, 401, 403, 404, 422) raise immediately. See [Retries](#retries) for the full policy.

```ruby
begin
Typesafe::Jev.evaluate(state: state, questions: questions)
rescue Typesafe::RateLimitError => e
sleep(e.retry_after || 1.0)
retry
rescue Typesafe::OverloadedError
sleep(2**attempt)
retry
rescue Typesafe::ConnectionError => e
warn "TypeSafe unreachable: #{e.cause}"
rescue Typesafe::APIError => e
raise "typesafe request failed (#{e.status}, request #{e.request_id}): #{e.body}"
raise "typesafe request failed (status #{e.status}, request #{e.request_id}): #{e.body}"
end
```

## Retries

Retryable failures are retried automatically: rate limits (429), overload (529), server errors (5xx), everything `APIError#retryable?` covers, and network failures that occur before any HTTP response (refused connection, DNS failure, read/write timeout, reset connection), which are wrapped in a retryable `Typesafe::ConnectionError`. By default the client makes **2 retries** (3 attempts in total). The wait honors the server's `Retry-After` / `Retry-After-Ms` header when a 429 carries one; otherwise it is an exponential backoff starting at **0.5 s**, doubled on each retry, capped at **8.0 s**, with jitter so parallel clients do not align. Non-retryable errors (400, 401, 403, 404, 422) raise immediately, without any replay. The POST is replayed verbatim on each attempt (an evaluation is stateless), and retries are silent: no log output, no callback.

The whole policy is configured once per client with a single `retry_options:` Hash:

```ruby
# defaults: { max_retries: 2, base_delay: 0.5, max_delay: 8.0 }
client = Typesafe::Client.new(api_key: "sk-...", retry_options: { max_retries: 3 })

# latency-sensitive path: back to a single attempt
strict = Typesafe::Client.new(api_key: "sk-...", retry_options: { max_retries: 0 })

# Typesafe::Jev inherits the option from Typesafe::Client
jev = Typesafe::Jev.new(api_key: "sk-...", retry_options: { max_retries: 0 })
```

Accepted keys: `max_retries` (non-negative Integer), `base_delay` and `max_delay` (non-negative numbers). An absent key, a `nil` value, or `retry_options: nil` keeps the defaults; `max_retries: 0` restores a single attempt. Options are validated strictly at construction time (an unknown key, a typo for instance, or an invalid value raises an `ArgumentError` naming the key), and the normalized Hash is frozen on the client: mutating the Hash you passed later has no effect. The policy is fixed at initialization, so `evaluate`'s signature does not change and there is no per-call override.

## Configuration

The API key is read from the `TYPESAFE_API_KEY` environment variable unless passed explicitly:
Expand All @@ -129,6 +147,8 @@ client = Typesafe::Client.new(api_key: "sk-...", model: "other-model")
client.evaluate(state:, questions:, model: "one-off-model") # per-call override
```

Both `Typesafe::Client.new` and `Typesafe::Jev.new` also take the retry policy as a `retry_options:` Hash at initialization; see [Retries](#retries).

## Documentation

TypeSafe docs live at [docs.typesafe.ai](https://docs.typesafe.ai). The full API reference for this gem is on [rubydoc.info](https://rubydoc.info/gems/typesafe-jev).
Expand Down
2 changes: 1 addition & 1 deletion lib/typesafe/version.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# frozen_string_literal: true

module Typesafe
VERSION = "1.0.0"
VERSION = "1.1.0"
end
2 changes: 1 addition & 1 deletion spec/typesafe_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

RSpec.describe Typesafe do
it "has a version" do
expect(Typesafe::VERSION).to eq("1.0.0")
expect(Typesafe::VERSION).to eq("1.1.0")
end

it "exposes the question types" do
Expand Down
Loading