typesafe-sdk-ruby 0.6.0 → 0.6.0.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c2f7c356ecf7107179e8d01aeec562af105267ef4fc640b9a95a8d0c0904413f
4
- data.tar.gz: 5a7657f8bc722b9c7ecae8f126b7792b2911b08bbc006a54114f16fb0bf73bb3
3
+ metadata.gz: a081c1995e40528b73408af6b6188d0c9784b497de463b2821694e6a1a895c81
4
+ data.tar.gz: 7c7363485285de50ef55a68dfe3f6606e3c0a08f0f41133fbb182010626144bc
5
5
  SHA512:
6
- metadata.gz: 62fd70fb931ccaae55504bb8ed8db5b674e347978704d938a178003e339c0061ed8f4f5de62574298471dd29b21369e6a82cf8d549985cf9e0e8c22781299a1d
7
- data.tar.gz: ac8620432b7afd9cb36ec5e1a64b16fe3709590c3597234401cc71ab797558676441034af33b4b17ff4f650082a7f7627579dba5248db0a98c1ca5b734a48e04
6
+ metadata.gz: 26a637c0d1fc8afb9f29e20ab2860d287d12491c6a93c7c28c2db574adce747abba8c60eae1d70cff7efd3610b08db1f9ea0fdf4d727c58aeaee08cc12c35811
7
+ data.tar.gz: 1e9bfee179126a48aa3f640b87149f9af99a8e47c03bb13d29c2d550a520522e98496ceeabd63aa9949fced46f242cc600133c34aab5685b74a23ecfd47a14a5
data/CHANGELOG.md CHANGED
@@ -3,7 +3,50 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
- and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+ Versions follow the official JavaScript SDK, with a fourth numeric component for Ruby-only
7
+ corrections. See [RELEASING.md](docs/RELEASING.md).
8
+
9
+ ## [0.6.0.1] - 2026-09-21
10
+
11
+ Compatibility target: official JavaScript SDK 0.6.0, commit `66880cc`.
12
+
13
+ ### Fixed
14
+
15
+ - Reject malformed/blank API keys at construction without including credentials in errors
16
+ (upstream #14); trim surrounding spaces and line endings.
17
+ - Validate state types, meaningful noul questions, score/choice bounds, and empty question
18
+ names before HTTP requests (upstream #6).
19
+ - Reject nil score levels without dropping or renumbering positions; document an explicit
20
+ empty string for an undescribed position (upstream #12).
21
+ - Fall back to configured backoff for blank retry headers while preserving explicit zero
22
+ and valid alternate headers (upstream #9).
23
+
24
+ - Match upstream fallback error messages when API error fields are empty strings.
25
+ - Treat nil retry flags as inherited settings, while keeping explicit false overrides.
26
+ - Enforce the timeout across the entire HTTP attempt, including slow response bodies.
27
+ - Interrupt active requests on cancellation and close/join the request worker before returning.
28
+ - Disable Net::HTTP's implicit retries; detect truncated response bodies as connection errors.
29
+ - Support Ruby/Rails loggers when debug or error details are present.
30
+ - Validate JSON-loaded score questions and reject infinite/NaN/complex timeout and backoff values.
31
+ - Preserve fractional server retry delays; capture HTTP-date rate-limit delays once.
32
+ - Freeze copied retry status overrides to prevent accidental policy mutation.
33
+ - Preserve upstream license attribution and remove the misleading official-company contact.
34
+
35
+ ### Added
36
+
37
+ - `PaymentRequiredError` (402), `ConflictError` (409), and `PayloadTooLargeError` (413),
38
+ retaining `APIError` inheritance and existing retry defaults (upstream #13).
39
+ - An audit of all 11 upstream issues, numeric-label coverage, subprocess cancellation checks,
40
+ and real-socket timeout tests around Node's timer limit (upstream #2, #4, #8).
41
+
42
+ - Complete answer and nullable retry flag comparisons against the published JS package.
43
+ - README onboarding, primitive/confidence examples, JS-to-Ruby mapping, and support routes.
44
+ - `with_response: true` on inference and model listing for data, HTTP metadata, and request ID.
45
+ - `extra_body:` for additional inference fields, including explicit null values.
46
+ - Fixtures generated by executing the published JS SDK, real socket regression tests, and an
47
+ opt-in live API smoke test.
48
+ - A single gem version source, explicit `UPSTREAM_VERSION`, release validation, packaged-gem
49
+ CI checks, Ruby 4.0 coverage, and compatibility/release documentation.
7
50
 
8
51
  ## [0.6.0] - 2026-09-20
9
52
 
data/LICENSE CHANGED
@@ -1,6 +1,7 @@
1
1
  MIT License
2
2
 
3
3
  Copyright (c) 2026 TypeSafe Ruby SDK Contributors
4
+ Copyright (c) 2026 TypeSafe (upstream JavaScript SDK)
4
5
 
5
6
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
7
  of this software and associated documentation files (the "Software"), to deal
data/README.md CHANGED
@@ -4,37 +4,42 @@
4
4
  [![CI](https://github.com/afurm/typesafe-sdk-ruby/actions/workflows/ci.yml/badge.svg)](https://github.com/afurm/typesafe-sdk-ruby/actions/workflows/ci.yml)
5
5
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
6
6
 
7
- **Community-maintained, unofficial** Ruby SDK for [TypeSafe AI](https://typesafe.ai). Ask named
8
- questions about text or structured state and get typed answers back: yes/no (noul), choice, and
9
- score questions — with retries, timeouts, structured logging, and typed errors.
7
+ A community-maintained Ruby client for [TypeSafe AI](https://typesafe.ai), with typed answer
8
+ objects, retries, timeouts, cancellation, and configurable logging.
10
9
 
11
- > This gem is not affiliated with, endorsed by, or supported by TypeSafe AI. It is a faithful
12
- > community port of the official [JavaScript SDK](https://github.com/typesafe-ai/typesafe-sdk-js).
13
- > Official SDKs: [JavaScript](https://github.com/typesafe-ai/typesafe-sdk-js) and Python.
10
+ TypeSafe's Jev model evaluates state against questions defined by your application. It returns
11
+ structured decisions that your code can use for classification, ranking, and routing.
12
+ Read the [official introduction](https://docs.typesafe.ai/introduction) for the product model.
14
13
 
15
- > **Versioning:** gem versions intentionally mirror the official JavaScript SDK so it is
16
- > obvious which upstream release each port tracks (e.g. gem 0.6.0 ≈ JS SDK 0.6.0).
14
+ **Unofficial:** this project is not affiliated with, endorsed by, or supported by TypeSafe AI.
15
+ It targets the official [JavaScript SDK](https://github.com/typesafe-ai/typesafe-sdk-js)
16
+ **0.6.0**. Official [JavaScript and Python SDKs](https://docs.typesafe.ai/sdk) are maintained
17
+ by TypeSafe AI.
17
18
 
18
- ## Requirements
19
-
20
- - Ruby 3.1 or newer
21
- - A TypeSafe API key (`TYPESAFE_API_KEY`)
19
+ **Version:** this README documents Ruby **0.6.0.1**, targeting JS **0.6.0**.
20
+ Ruby-only corrections add a fourth version component. Check
21
+ [RubyGems](https://rubygems.org/gems/typesafe-sdk-ruby) and
22
+ [GitHub releases](https://github.com/afurm/typesafe-sdk-ruby/releases) for published versions;
23
+ merging changes into `main` does not publish a gem. See the [changelog](CHANGELOG.md)
24
+ for changes and migration notes.
22
25
 
23
26
  ## Installation
24
27
 
25
- Install the gem:
28
+ Requires Ruby **3.1 or newer**; CI tests Ruby 3.1, 3.2, 3.3, 3.4, and 4.0.
26
29
 
27
30
  ```sh
28
31
  gem install typesafe-sdk-ruby
29
32
  ```
30
33
 
31
- Or add it to your application's `Gemfile`:
34
+ For Bundler, add the gem to your application's `Gemfile` and run `bundle install`:
32
35
 
33
36
  ```ruby
34
37
  gem "typesafe-sdk-ruby"
35
38
  ```
36
39
 
37
- and run `bundle install`.
40
+ Create an API key in the [TypeSafe console](https://console.typesafe.ai/), then set
41
+ `TYPESAFE_API_KEY` in your environment or application secret store. The SDK reads it
42
+ automatically. See the [official quick start](https://docs.typesafe.ai/introduction/quickstart).
38
43
 
39
44
  ## Quickstart
40
45
 
@@ -42,159 +47,304 @@ and run `bundle install`.
42
47
  require "typesafe-sdk-ruby"
43
48
 
44
49
  client = Typesafe::SDK::Client.new
45
-
46
50
  response = client.system_one(
47
51
  state: { document: "I was charged twice. Please fix this ASAP." },
48
52
  questions: {
49
53
  category: Typesafe::SDK.choice("What is this ticket about?", {
50
- billing: nil,
51
- technical: nil,
52
- other: nil,
54
+ billing: "Payments, invoices, or refunds",
55
+ technical: "A product error or technical problem",
56
+ other: "A different subject",
53
57
  }),
54
58
  },
55
59
  )
56
60
 
57
- puts response.answers["category"].choice
61
+ category = response[:category]
62
+ puts category.choice
63
+ puts category.confidence
64
+ puts category.probabilities
65
+ puts response.model
66
+ puts response.usage.input_tokens
58
67
  ```
59
68
 
60
- Answer objects are typed by the question that produced them:
69
+ `response[:category]` and `response["category"]` work interchangeably.
70
+ `response.answers` is a regular Hash with string keys.
71
+
72
+ ## Choosing a question type
61
73
 
62
- | Question | Answer class | Key fields |
74
+ | Builder | Use it for | Result |
63
75
  | --- | --- | --- |
64
- | `noul` | `Typesafe::SDK::NoulResponse` | `noul` (probability of yes) |
65
- | `choice` | `Typesafe::SDK::ChoiceResponse` | `choice`, `confidence`, `probabilities` |
66
- | `score` | `Typesafe::SDK::ScoreResponse` | `score`, `confidence`, `legend`, `probabilities` |
76
+ | `noul` | A yes/no judgment | `noul`: probability of yes, from 0 to 1 |
77
+ | `choice` | Selecting one named option | `choice`, `probabilities`, `confidence` |
78
+ | `score` | Rating against ordered descriptions | `score`, `legend`, `probabilities`, `confidence` |
79
+
80
+ Mix question types in a single call. Each question is evaluated against the same state,
81
+ independently of the other questions. Put the meaning in the instructions and criteria;
82
+ question IDs identify answers and are not used for inference.
83
+ See [primitives](https://docs.typesafe.ai/primitives) and the [API reference](https://docs.typesafe.ai/api).
84
+
85
+ ```ruby
86
+ response = client.system_one(
87
+ state: "The export fails in Safari, but I can finish the task in Firefox.",
88
+ questions: {
89
+ workaround: Typesafe::SDK.noul("Does the user describe a working alternative?"),
90
+ category: Typesafe::SDK.choice("Which area is affected?", {
91
+ export: "Exporting application data",
92
+ login: "Signing in to the application",
93
+ other: "Another area",
94
+ }),
95
+ severity: Typesafe::SDK.score("How much does this issue affect the user's task?", [
96
+ "The task works; only its appearance is affected",
97
+ "The task needs an alternative method to complete",
98
+ "The task cannot be completed",
99
+ ]),
100
+ },
101
+ )
102
+
103
+ puts response[:workaround].noul
104
+ puts response[:severity].score
105
+ puts response[:severity].probabilities["1"]
106
+ puts response[:severity].legend["1"]
107
+ ```
108
+
109
+ Score criteria are an **ordered array**, with positions starting at zero. A returned score
110
+ is a probability-weighted mean and can be fractional. `legend` and `probabilities` retain
111
+ string keys such as `"0"` and `"1"`. See the [Score guide](https://docs.typesafe.ai/primitives/score).
112
+
113
+ State, instructions, and criterion descriptions can also contain JSON objects or arrays.
114
+ Optional noul criteria can describe either outcome:
115
+
116
+ ```ruby
117
+ question = Typesafe::SDK.noul("Is this an explicit cancellation request?", criteria: {
118
+ true: "The customer asks to end the subscription",
119
+ false: "The customer only asks about cancellation terms",
120
+ })
121
+ ```
122
+
123
+ See [structured questions](https://docs.typesafe.ai/primitives/advanced) for more examples.
124
+
125
+ From Ruby **0.6.0.1**, builders and raw question hashes are checked before sending:
126
+
127
+ - State must be a string, object, or array; `nil` is rejected, while `""`, `{}`, and `[]` are allowed.
128
+ - Noul needs instructions or at least one non-nil true/false outcome description.
129
+ - Score needs **2–10** levels. A nil level is rejected; use `""` explicitly to keep an
130
+ undescribed position. The SDK never drops or renumbers levels.
131
+ - Choice needs **1–255** options. Nil descriptions remain valid for choice labels.
132
+ - Question names must not be empty strings.
133
+
134
+ Failures raise `Typesafe::SDK::TypeSafeError` without an HTTP request. These checks address
135
+ request shapes reported as rejected by the API, even though JS 0.6.0 accepts them locally.
136
+ See the [upstream issue audit](docs/UPSTREAM_ISSUES.md) for evidence and limitations.
137
+
138
+ ## Using confidence
139
+
140
+ Choice and Score include confidence derived from their probability distributions. Confidence
141
+ is distinct from the probability of the selected option and is not a guarantee of correctness.
142
+ Noul returns the probability of yes and has no separate confidence field.
143
+ See the [official confidence guide](https://docs.typesafe.ai/confidence).
144
+
145
+ ```ruby
146
+ category = response[:category]
147
+ threshold = 0.8 # Illustrative: evaluate a suitable threshold on your own labeled examples.
148
+ puts(category.confidence >= threshold ? "Route to #{category.choice}" : "Needs review")
149
+ ```
67
150
 
68
151
  ## Configuration
69
152
 
70
153
  Explicit options take precedence over environment variables, then SDK defaults.
154
+ Blank environment values are ignored for optional settings. An API key is required.
155
+ From Ruby 0.6.0.1, outer spaces, tabs, and line endings are trimmed from keys; blank keys,
156
+ non-ASCII text, embedded whitespace, and control characters raise `TypeSafeError` at
157
+ construction. Error messages do not include the key.
71
158
 
72
159
  | Option | Environment variable | Default |
73
160
  | --- | --- | --- |
74
- | `api_key:` | `TYPESAFE_API_KEY` | (required) |
161
+ | `api_key:` | `TYPESAFE_API_KEY` | Required |
75
162
  | `base_url:` | `TYPESAFE_BASE_URL` | `https://api.typesafe.ai` |
76
163
  | `default_model:` | `TYPESAFE_DEFAULT_MODEL` | `jev-latest` |
77
164
  | `log_level:` | `TYPESAFE_LOG_LEVEL` | `warn` |
78
165
 
79
166
  ```ruby
80
167
  client = Typesafe::SDK::Client.new(
81
- api_key: "sk-...",
82
- base_url: "https://api.typesafe.ai",
83
- default_model: "jev-latest",
84
- log_level: :info, # :debug, :info, :warn, :error, :off
85
- timeout: 10, # seconds per attempt
168
+ timeout: 10, # Seconds per attempt, including connection setup and response body receipt.
86
169
  retry_policy: { max_retries: 2, backoff_initial_ms: 500 },
87
- default_headers: { "X-My-Header" => "value" },
170
+ log_level: :info, # :debug, :info, :warn, :error, or :off
171
+ default_headers: { "X-My-App" => "support" },
88
172
  )
89
173
  ```
90
174
 
175
+ `jev-latest` is a moving alias. For reproducible deployments, select an explicit model from
176
+ `client.models.list` and set `default_model:` or a per-call `model:`. See
177
+ [available models and aliases](https://docs.typesafe.ai/models).
178
+
91
179
  ## Retries and timeouts
92
180
 
93
- The SDK retries HTTP `408`, `429`, and `5xx` responses plus connection failures and timeouts,
94
- with capped exponential backoff and jitter. It honors `Retry-After` and `retry-after-ms`
95
- headers up to a cap. Every option is overridable per client or per call:
181
+ By default, the SDK retries HTTP 408, 429, and 5xx responses, connection failures, and timeouts,
182
+ with up to two retries after the initial attempt. Backoff starts at 500 ms, doubles up to
183
+ 5,000 ms, and uses up to 25% downward jitter. Server `Retry-After` and `retry-after-ms` delays
184
+ are honored up to 60,000 ms; larger delays fall back to backoff. From Ruby 0.6.0.1, blank
185
+ or malformed delay headers also fall back to backoff; an explicit zero remains valid.
186
+ A blank `retry-after-ms` still permits a valid `Retry-After` header to be used.
187
+
188
+ Each attempt receives a fresh timeout, so total call time can include several attempts and
189
+ backoff. Override settings per client or per call:
96
190
 
97
191
  ```ruby
98
192
  client.system_one(
99
- state: "...",
100
- questions: { ... },
193
+ state: "A refund request",
194
+ questions: { billing: Typesafe::SDK.noul("Is this about billing?") },
101
195
  timeout: 30,
102
- retry_policy: { max_retries: 0 }, # disable retries for this call
196
+ retry_policy: { max_retries: 0 },
103
197
  )
104
198
  ```
105
199
 
200
+ The full retry policy supports `max_retries`, `backoff_initial_ms`, `backoff_max_ms`,
201
+ `backoff_jitter`, `http_statuses`, `respect_retry_after`, `max_retry_after_ms`,
202
+ `api_connection_error`, and `api_timeout_error`. Overrides merge field by field.
203
+ Nil boolean flags inherit the existing setting; explicit `false` disables that behavior.
204
+
106
205
  ## Error handling
107
206
 
108
207
  ```ruby
109
208
  begin
110
- client.system_one(state: "...", questions: { ... })
209
+ client.system_one(
210
+ state: "A refund request",
211
+ questions: { billing: Typesafe::SDK.noul("Is this about billing?") },
212
+ )
111
213
  rescue Typesafe::SDK::RateLimitError => e
112
- retry_after e.retry_after_ms
214
+ warn "Rate limited; suggested retry delay: #{e.retry_after_ms.inspect}ms"
113
215
  rescue Typesafe::SDK::APIError => e
114
- warn "API error #{e.status} (request #{e.request_id}): #{e.body}"
216
+ warn "API error #{e.status}; request ID: #{e.request_id}"
217
+ rescue Typesafe::SDK::APIConnectionError => e
218
+ warn e.message
115
219
  end
116
220
  ```
117
221
 
118
- Error hierarchy:
222
+ Errors reach your code after any configured retries. All SDK errors inherit from
223
+ `Typesafe::SDK::TypeSafeError`:
224
+
225
+ | Error | Meaning |
226
+ | --- | --- |
227
+ | `BadRequestError` | HTTP 400 |
228
+ | `AuthenticationError` | HTTP 401 |
229
+ | `PaymentRequiredError` | HTTP 402; added in 0.6.0.1 |
230
+ | `PermissionDeniedError` | HTTP 403 |
231
+ | `NotFoundError` | HTTP 404 |
232
+ | `ConflictError` | HTTP 409; added in 0.6.0.1 |
233
+ | `PayloadTooLargeError` | HTTP 413; added in 0.6.0.1 |
234
+ | `UnprocessableEntityError` | HTTP 422 |
235
+ | `RateLimitError` | HTTP 429; exposes `retry_after_ms` |
236
+ | `InternalServerError` | HTTP 5xx |
237
+ | `APIError` | Other non-2xx responses; exposes status, headers, body, and request ID |
238
+ | `APIConnectionError` | Connection or response-body delivery failure |
239
+ | `APITimeoutError` | Subclass of `APIConnectionError`; exposes `timeout_ms` |
240
+ | `APIUserAbortError` | Caller cancellation; never retried |
241
+
242
+ ## Response metadata and additional fields
243
+
244
+ Added for Ruby **0.6.0.1**:
245
+
246
+ ```ruby
247
+ result = client.system_one(
248
+ state: "A refund request",
249
+ questions: { billing: Typesafe::SDK.noul("Is this about billing?") },
250
+ with_response: true,
251
+ )
252
+ puts result.data[:billing].noul
253
+ puts result.request_id
254
+ puts result.response.status
255
+ ```
119
256
 
120
- - `Typesafe::SDK::TypeSafeError` base class
121
- - `Typesafe::SDK::APIError` non-2xx HTTP responses
122
- - `BadRequestError` (400), `AuthenticationError` (401), `PermissionDeniedError` (403),
123
- `NotFoundError` (404), `UnprocessableEntityError` (422), `RateLimitError` (429),
124
- `InternalServerError` (5xx)
125
- - `Typesafe::SDK::APIConnectionError` — DNS, TLS, connection failures
126
- - `Typesafe::SDK::APITimeoutError`
127
- - `Typesafe::SDK::APIUserAbortError` — caller cancellation
257
+ `client.models.list(with_response: true)` wraps the model cards the same way. The response
258
+ exposes case-insensitive `headers` and a parsed `body`.
259
+ `extra_body: { new_option: nil }` forwards additional JSON fields; named `state`, `questions`,
260
+ and `model` arguments take precedence. The API decides whether a field is supported.
128
261
 
129
262
  ## Cancellation
130
263
 
131
264
  ```ruby
132
265
  signal = Typesafe::SDK::Signal.new
133
- Thread.new { sleep 5; signal.cancel }
134
-
135
- client.system_one(state: "...", questions: { ... }, signal: signal)
136
- # raises Typesafe::SDK::APIUserAbortError once canceled
266
+ canceller = Thread.new { sleep 5; signal.cancel }
267
+ begin
268
+ client.system_one(
269
+ state: "A refund request",
270
+ questions: { billing: Typesafe::SDK.noul("Is this about billing?") },
271
+ signal: signal,
272
+ )
273
+ rescue Typesafe::SDK::APIUserAbortError
274
+ warn "Request canceled"
275
+ ensure
276
+ canceller.kill.join
277
+ end
137
278
  ```
138
279
 
280
+ From Ruby 0.6.0.1, cancellation interrupts active requests and retry backoff, and cleans up
281
+ the request worker and socket before returning.
282
+
139
283
  ## Listing models
140
284
 
141
285
  ```ruby
142
286
  client.models.list.each do |model|
143
- puts "#{model.name}: #{model.description}"
287
+ puts "#{model.name}: #{model.description} (#{model.release_date})"
144
288
  end
145
289
  ```
146
290
 
147
- ## Logging
291
+ ## Logging and Rails
148
292
 
149
- The default logger writes to `$stderr` with a `[typesafe-sdk-ruby]` prefix. `info` logs request
150
- summaries; `debug` adds headers (credentials redacted) and bodies. Pass any object responding
151
- to `debug`/`info`/`warn`/`error`:
293
+ The default logger writes to `$stderr` with a `[typesafe-ai]` prefix. `info` includes request
294
+ summaries; `debug` adds redacted credential headers and request/response bodies. Bodies may
295
+ contain application data and are **not** redacted.
296
+
297
+ From Ruby 0.6.0.1, standard Ruby `Logger` and `Rails.logger` also accept debug details:
152
298
 
153
299
  ```ruby
154
- client = Typesafe::SDK::Client.new(logger: Rails.logger, log_level: :info)
300
+ # config/initializers/typesafe.rb; configure TYPESAFE_API_KEY through your secret store.
301
+ TYPESAFE = Typesafe::SDK::Client.new(logger: Rails.logger, log_level: :info)
155
302
  ```
156
303
 
157
- ## Ruby on Rails
304
+ Call `TYPESAFE.system_one(...)` from application code. In plain Ruby, use
305
+ `require "logger"` and pass `logger: Logger.new($stderr)`; add the `logger` gem to your
306
+ application if your Ruby version does not provide it by default.
158
307
 
159
- The gem is framework-agnostic and works out of the box in Rails. A common pattern is a
160
- wrapped initializer:
308
+ ## Coming from JavaScript
161
309
 
162
- ```ruby
163
- # config/initializers/typesafe.rb
164
- TYPESAFE = Typesafe::SDK::Client.new(log_level: :info)
165
- ```
310
+ | JavaScript | Ruby |
311
+ | --- | --- |
312
+ | `new TypeSafeClient()` | `Typesafe::SDK::Client.new` |
313
+ | `systemOne(request, options)` | `system_one(state:, questions:, **options)` |
314
+ | `apiKey`, `baseURL`, `defaultModel` | `api_key:`, `base_url:`, `default_model:` |
315
+ | `timeout` in milliseconds | `timeout:` in seconds |
316
+ | `retry: { maxRetries: 0 }` | `retry_policy: { max_retries: 0 }` |
317
+ | `.withResponse()` | `with_response: true` (Ruby 0.6.0.1+) |
318
+ | `AbortController` | `Typesafe::SDK::Signal` |
319
+ | `answers.category` | `response[:category]` or `response.answers["category"]` |
166
320
 
167
- ```ruby
168
- # app/models/concerns/typesafe_classifiable.rb
169
- module TypesafeClassifiable
170
- def classify(text)
171
- TYPESAFE.system_one(
172
- state: text,
173
- questions: {
174
- category: Typesafe::SDK.choice("Category?", {
175
- billing: nil, technical: nil, other: nil,
176
- }),
177
- },
178
- ).answers["category"].choice
179
- end
180
- end
181
- ```
321
+ Ruby calls are synchronous and return Ruby answer objects. They do not provide TypeScript
322
+ compile-time inference, Fetch streams, connection pooling, or automatic redirect following.
323
+ See [tested compatibility and intentional differences](docs/COMPATIBILITY.md).
182
324
 
183
- ## Development
325
+ ## Development and contributing
184
326
 
185
327
  ```sh
186
328
  bundle install
187
- bundle exec rake # specs + RuboCop
188
- bundle exec rspec # specs only
189
- bundle exec rubocop # lint only
329
+ bundle exec rake # Tests and RuboCop; no API key or external API calls required.
190
330
  ```
191
331
 
192
- ## Contributing
332
+ Native transport tests bind loopback sockets. CI also builds and installs the packaged gem.
333
+ For an explicit live check, follow [live verification](docs/COMPATIBILITY.md#live-verification).
334
+ The [demo](examples/demo.rb) uses real API calls and can be run with
335
+ `bundle exec ruby examples/demo.rb` after configuring your key.
336
+
337
+ See [CONTRIBUTING.md](CONTRIBUTING.md), the [PR template](.github/PULL_REQUEST_TEMPLATE.md),
338
+ and the [release process](docs/RELEASING.md). Changes are reviewed and merged by the repository owner.
339
+
340
+ ## Support and security
193
341
 
194
- Bug reports and pull requests are welcome on
195
- [GitHub](https://github.com/afurm/typesafe-sdk-ruby/issues). See
196
- [CONTRIBUTING.md](CONTRIBUTING.md).
342
+ Use [issues](https://github.com/afurm/typesafe-sdk-ruby/issues) for Ruby client bugs and
343
+ [discussions](https://github.com/afurm/typesafe-sdk-ruby/discussions) for usage questions.
344
+ See [SUPPORT.md](SUPPORT.md) for API/account questions and useful report details.
345
+ Report vulnerabilities privately using [SECURITY.md](SECURITY.md).
346
+ All participation follows our [Code of Conduct](CODE_OF_CONDUCT.md).
197
347
 
198
348
  ## License
199
349
 
200
- The gem is available as open source under the terms of the [MIT License](LICENSE).
350
+ [MIT](LICENSE), including attribution to the upstream TypeSafe JavaScript SDK.
@@ -0,0 +1,89 @@
1
+ # JavaScript SDK compatibility
2
+
3
+ This unofficial Ruby client targets the official `@typesafe-ai/sdk` **0.6.0** release,
4
+ commit [`66880ccded6cb642dc1809620c2b108c33730214`](https://github.com/typesafe-ai/typesafe-sdk-js/tree/66880ccded6cb642dc1809620c2b108c33730214).
5
+ Audit date: 2026-09-21. This is a tested compatibility target, not a claim of endorsement
6
+ or a guarantee about future API changes.
7
+
8
+ ## What is covered
9
+
10
+ | Surface | Ruby behavior |
11
+ | --- | --- |
12
+ | Endpoints | `POST /v1/systemone`, `GET /v1/models` |
13
+ | Defaults | `jev-latest`, 10-second attempt timeout, two retries |
14
+ | Questions | Noul, choice, ordered score criteria; structured instructions/state and nullable choice descriptions |
15
+ | Configuration | Explicit settings override trimmed environment values |
16
+ | Retries | 408, 429, 500–599; connection and timeout failures independently configurable |
17
+ | Server delays | Fractional milliseconds/seconds, HTTP dates, maximum delay and jitter |
18
+ | Headers | Case-insensitive overrides; protected auth, content type, SDK and retry headers |
19
+ | Errors | HTTP subclasses, validation messages, request IDs, connection/timeout/cancellation errors |
20
+ | Transport | Timeout includes full body delivery; cancellation interrupts an active attempt and backoff; truncated bodies are retried |
21
+ | Results | Typed answers and usage; model cards; `with_response: true` for HTTP metadata |
22
+ | Extension fields | `extra_body:` forwards additional JSON fields without overriding named arguments |
23
+
24
+ The checked-in `spec/fixtures/js-0.6.0.json` was generated by executing the published
25
+ JavaScript package, rather than by copying expected output from the Ruby implementation.
26
+ `parity_spec.rb` compares question JSON, request payloads, metadata, error mapping/messages,
27
+ complete answer fields (including fractional scores and string rubric keys), retry header
28
+ parsing, and nullable retry flags. `http_spec.rb` uses real loopback sockets to exercise behavior
29
+ that mocked HTTP adapters cannot verify. Other specs cover Ruby configuration and logging.
30
+ Known upstream bugs are corrected explicitly in the comparisons; the fixtures retain the
31
+ original JS behavior for those cases.
32
+ See the [issue-by-issue audit](UPSTREAM_ISSUES.md) for every upstream issue reviewed.
33
+
34
+ To regenerate the fixtures, download the official release asset from
35
+ [GitHub](https://github.com/typesafe-ai/typesafe-sdk-js/releases/tag/v0.6.0), unpack it, and run:
36
+
37
+ ```sh
38
+ node script/generate_parity_fixtures.cjs /path/to/package > spec/fixtures/js-0.6.0.json
39
+ bundle exec rake
40
+ ```
41
+
42
+ ## Intentional Ruby differences and limits
43
+
44
+ - Ruby 0.6.0.1 validates API keys without echoing their contents and rejects reported
45
+ API-invalid state/question shapes locally. Score levels must be non-nil and number 2–10;
46
+ choice options must number 1–255; noul requires instructions or an outcome description;
47
+ state must be a string, object, or array; question names cannot be empty.
48
+ Builders and raw question hashes use the same checks. This intentionally tightens the
49
+ permissive JS 0.6.0 behavior; local validation is not a complete server schema validator.
50
+ - Blank retry headers fall back to configured backoff (or another valid delay header).
51
+ JS 0.6.0 incorrectly treats blanks as zero. Literal zero remains valid in Ruby.
52
+ - HTTP 402, 409, and 413 have `PaymentRequiredError`, `ConflictError`, and
53
+ `PayloadTooLargeError` subclasses. Existing `APIError` rescue clauses still catch them.
54
+ - Methods/options use snake_case. `timeout:` is in **seconds**; retry settings ending in
55
+ `_ms` remain milliseconds. `retry_policy:` corresponds to JavaScript's `retry` option.
56
+ - Calls are synchronous. There is no JavaScript `Promise`, `AbortController`, or compile-time
57
+ inference. Ruby returns answer classes and accepts `Typesafe::SDK::Signal` for cancellation.
58
+ - `with_response: true` returns `.data`, `.response`, and `.request_id`. The response exposes
59
+ `.status`, `.headers`, and an already parsed `.body`; it is not a Fetch response or stream.
60
+ Non-2xx responses still raise typed errors. Unlike JavaScript's `asResponse()`, these
61
+ resource methods always construct typed results; there is no unparsed Fetch body API.
62
+ - `result[:question]` and `result["question"]` are both supported. `result.answers` itself
63
+ is a Hash with string keys, as received in JSON.
64
+ - The noul builder serializes omitted criteria as JSON null; upstream omits the property.
65
+ Both are accepted by the upstream question type. Explicit criteria serialize identically.
66
+ - The built-in transport opens one connection and one private worker per attempt. It closes
67
+ and joins them on completion, timeout, or cancellation. It does not pool connections or
68
+ implement a fiber-native asynchronous interface. Custom adapters own timeout/cancellation
69
+ enforcement and must implement the documented `HTTP#request` contract.
70
+ - Net::HTTP does not follow redirects automatically; a 3xx raises `APIError`. Use the canonical
71
+ API URL. JavaScript Fetch may follow redirects.
72
+ - Ruby's HTTP-date parser and JavaScript's Date parser may differ for malformed, non-HTTP dates.
73
+ Standard HTTP dates and the tested numeric forms are covered.
74
+
75
+ ## Live verification
76
+
77
+ Offline tests do not prove account authentication, current model availability, billing, or
78
+ production inference behavior. No live API key was available during this audit, so no live
79
+ TypeSafe request was made.
80
+
81
+ An explicit smoke test checks model listing and all three answer types with synthetic input:
82
+
83
+ ```sh
84
+ # Set TYPESAFE_API_KEY securely in your environment first.
85
+ TYPESAFE_LIVE_TEST=1 bundle exec ruby script/smoke_test.rb
86
+ ```
87
+
88
+ This makes one inference request, which may incur normal API charges. It does not retry or
89
+ print the API key, input bodies, or full responses.
data/docs/RELEASING.md ADDED
@@ -0,0 +1,38 @@
1
+ # Releasing
2
+
3
+ Only the repository owner merges pull requests and initiates releases. Opening or merging
4
+ a pull request does not publish a gem. Publication starts when the owner pushes a version tag.
5
+
6
+ ## Versions
7
+
8
+ `Typesafe::SDK::UPSTREAM_VERSION` records the official JS compatibility target.
9
+ `Typesafe::SDK::VERSION` is the Ruby package version and is the single source for the gemspec.
10
+
11
+ - A new upstream port uses its exact three-part version, for example `0.7.0`.
12
+ - Ruby-only corrections use a fourth numeric component, for example `0.6.0.1` and `0.6.0.2`.
13
+ These still target JS `0.6.0`; they do not imply an upstream `0.6.1` release.
14
+ - Published versions and tags must not be replaced or reused. RubyGems accepts numeric
15
+ fourth-component versions as stable releases.
16
+
17
+ Before a new upstream port, compare the tagged source and tests, update the compatibility
18
+ document and generated fixtures, and run the suite. Version equality alone is not evidence
19
+ of compatibility.
20
+
21
+ ## Owner checklist
22
+
23
+ 1. Review and merge the release pull request after all CI matrix jobs pass.
24
+ 2. Run the opt-in live smoke test described in [COMPATIBILITY.md](COMPATIBILITY.md).
25
+ 3. Set the changelog release date and update the README release-status notice in a reviewed
26
+ pull request before tagging.
27
+ 4. On the merged main commit, run `bundle exec rake`, `ruby script/check_release.rb`, and
28
+ `gem build typesafe-sdk-ruby.gemspec --strict`.
29
+ 5. Create and push the matching annotated tag, for example `v0.6.0.1`.
30
+ 6. Verify the Release workflow, RubyGems version, GitHub release, and attached gem artifact.
31
+
32
+ The workflow rejects non-owner runs, tags that differ from the gem version, commits outside
33
+ main's history, and upstream versions that do not match the official tagged package.
34
+ It reruns tests and lint before publishing with the repository's `RUBYGEMS_API_KEY` secret.
35
+ Keep that credential restricted to this gem. Required branch checks remain in force.
36
+
37
+ If RubyGems publication succeeds but GitHub release creation fails, verify the published
38
+ artifact before recovering the GitHub release; do not delete the tag or republish the version.