From 3d3286020114e4f4a1ddf0f3bfed29e9e0a2909e Mon Sep 17 00:00:00 2001 From: "arsenik-dtheo[bot]" <330762117+arsenik-dtheo[bot]@users.noreply.github.com> Date: Sun, 20 Sep 2026 22:33:56 +0000 Subject: [PATCH] docs(README): documente les retentatives automatiques (1.1.0) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le README décrit maintenant la politique de retry livrée par l'épic : erreurs retentées (429, 529, 5xx et erreurs de connexion encapsulées dans Typesafe::ConnectionError), défauts (2 retries / 0,5 s / 8,0 s), Retry-After honoré, backoff exponentiel à jitter, rejeu à l'identique, retries silencieux, et réglage/désactivation via retry_options: (max_retries / base_delay / max_delay, validation stricte à la construction, héritage par Typesafe::Jev). L'ancien exemple de retry manuel des erreurs retentables, contredit par le comportement automatique, est remplacé par un exemple rescue exact (ConnectionError#cause, APIError#status/#request_id/#body). CHANGELOG : la section Unreleased devient l'entrée 1.1.0, augmentée de la section Documentation. Version bumpée 1.0.0 -> 1.1.0 (bump unique de l'épic, porté par son PR final), spec de contrat de version synchronisée, lockfile resynchronisé. --- CHANGELOG.md | 19 ++++++++++++++++++- Gemfile.lock | 2 +- README.md | 36 ++++++++++++++++++++++++++++-------- lib/typesafe/version.rb | 2 +- spec/typesafe_spec.rb | 2 +- 5 files changed, 49 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ff110e2..10ff490 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 @@ -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 diff --git a/Gemfile.lock b/Gemfile.lock index 2fc1b1b..331d29d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - typesafe-jev (1.0.0) + typesafe-jev (1.1.0) GEM remote: https://rubygems.org/ diff --git a/README.md b/README.md index cae1bc8..95f8912 100644 --- a/README.md +++ b/README.md @@ -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 | |---|---|---| @@ -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: @@ -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). diff --git a/lib/typesafe/version.rb b/lib/typesafe/version.rb index 2d14385..a369fe3 100644 --- a/lib/typesafe/version.rb +++ b/lib/typesafe/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module Typesafe - VERSION = "1.0.0" + VERSION = "1.1.0" end diff --git a/spec/typesafe_spec.rb b/spec/typesafe_spec.rb index 271af5e..3c7931d 100644 --- a/spec/typesafe_spec.rb +++ b/spec/typesafe_spec.rb @@ -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