typesafe-sdk-ruby 0.6.0 → 0.7.0

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.
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, image assets, retries, timeouts, cancellation, request compression, 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.7.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.7.0**, targeting JS **0.7.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,425 @@ 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
+ | `box` | Locating something in an image ([Images](#images)) | `box` (`top`, `bottom`, `left`, `right`), `confidence` |
80
+
81
+ Mix question types in a single call. Each question is evaluated against the same state,
82
+ independently of the other questions. Put the meaning in the instructions and criteria;
83
+ question IDs identify answers and are not used for inference.
84
+ See [primitives](https://docs.typesafe.ai/primitives) and the [API reference](https://docs.typesafe.ai/api).
85
+
86
+ ```ruby
87
+ response = client.system_one(
88
+ state: "The export fails in Safari, but I can finish the task in Firefox.",
89
+ questions: {
90
+ workaround: Typesafe::SDK.noul("Does the user describe a working alternative?"),
91
+ category: Typesafe::SDK.choice("Which area is affected?", {
92
+ export: "Exporting application data",
93
+ login: "Signing in to the application",
94
+ other: "Another area",
95
+ }),
96
+ severity: Typesafe::SDK.score("How much does this issue affect the user's task?", [
97
+ "The task works; only its appearance is affected",
98
+ "The task needs an alternative method to complete",
99
+ "The task cannot be completed",
100
+ ]),
101
+ },
102
+ )
103
+
104
+ puts response[:workaround].noul
105
+ puts response[:severity].score
106
+ puts response[:severity].probabilities["1"]
107
+ puts response[:severity].legend["1"]
108
+ ```
109
+
110
+ Score criteria are an **ordered array**, with positions starting at zero. A returned score
111
+ is a probability-weighted mean and can be fractional. `legend` and `probabilities` retain
112
+ string keys such as `"0"` and `"1"`. See the [Score guide](https://docs.typesafe.ai/primitives/score).
113
+
114
+ State, instructions, and criterion descriptions can also contain JSON objects or arrays.
115
+ Optional noul criteria can describe either outcome:
116
+
117
+ ```ruby
118
+ question = Typesafe::SDK.noul("Is this an explicit cancellation request?", criteria: {
119
+ true: "The customer asks to end the subscription",
120
+ false: "The customer only asks about cancellation terms",
121
+ })
122
+ ```
123
+
124
+ See [structured questions](https://docs.typesafe.ai/primitives/advanced) for more examples.
125
+
126
+ From Ruby **0.6.0.1**, builders and raw question hashes are checked before sending:
127
+
128
+ - State must be a string, object, array, or image; `nil` is rejected, while `""`, `{}`, and `[]` are allowed.
129
+ - Noul needs instructions or at least one non-nil true/false outcome description; its criteria,
130
+ when given, must be a Hash (from 0.7.0).
131
+ - Score needs **2–10** levels. A nil level is rejected; use `""` explicitly to keep an
132
+ undescribed position. The SDK never drops or renumbers levels.
133
+ - Choice needs **1–255** options. Nil descriptions remain valid for choice labels.
134
+ - Question names must not be empty strings.
135
+
136
+ Failures raise `Typesafe::SDK::TypeSafeError` without an HTTP request. These checks address
137
+ request shapes reported as rejected by the API. JS 0.7.0 now rejects nil state and empty noul
138
+ questions too; the score and choice bounds, nil score levels, and empty names remain Ruby-only checks.
139
+ See the [upstream issue audit](docs/UPSTREAM_ISSUES.md) for evidence and limitations.
140
+
141
+ From Ruby **0.7.0**, as in JS 0.7.0, non-finite floats such as `Float::NAN` are sent as the
142
+ strings `"NaN"`, `"Infinity"`, and `"-Infinity"`, because JSON cannot represent them. Every answer
143
+ also responds to `to_h`, which returns all of its fields, including ones this SDK doesn't model.
144
+ Usage token counts are `nil` when the API doesn't report them.
145
+
146
+ ## Images
147
+
148
+ Added in Ruby **0.7.0**. Create an image with `Typesafe::SDK.image` from file bytes (a binary
149
+ `String`), a `Pathname` (read when the request is sent), or an IO (read immediately), or with
150
+ `Typesafe::SDK::Image.from_base64` from base64 text. Images must be PNG or JPEG. The API detects
151
+ the format from the file bytes and rejects other formats, such as WebP and GIF, with a
152
+ `BadRequestError`. You can use an image in a request in three ways, and you can combine them.
153
+
154
+ Image support is an experimental API feature. Opt in by sending the
155
+ `TypeSafe-Danger-Zone-Images: true` header for a client, or per call with `headers:`:
156
+
157
+ ```ruby
158
+ client = Typesafe::SDK::Client.new(default_headers: { "TypeSafe-Danger-Zone-Images" => "true" })
159
+ ```
160
+
161
+ **Place the image in the content.** Put it anywhere in `state`, question instructions, or criteria:
162
+
163
+ ```ruby
164
+ require "pathname"
165
+
166
+ photo = Typesafe::SDK.image(Pathname("returns/RMA-1042.jpg"))
167
+ response = client.system_one(
168
+ state: { claim: "The mug arrived with a chipped rim.", photo: photo },
169
+ questions: {
170
+ damaged: Typesafe::SDK.noul("Is the product in the photo damaged?"),
171
+ severity: Typesafe::SDK.score("How severe is the damage?", %w[none cosmetic unusable]),
172
+ },
173
+ )
174
+ puts response[:damaged].noul, response[:severity].score
175
+ ```
176
+
177
+ **Write the image into text with `with_assets`.** Ruby has no tagged template literals, so pass
178
+ the text and images as separate arguments, in order; other values are converted with `to_s`.
179
+ `join_assets` joins a list like `Array#join`, with `","` as the default separator, as in
180
+ JavaScript's `withAssets.join`:
181
+
182
+ ```ruby
183
+ upload = Typesafe::SDK.image(params[:photo].read)
184
+ catalog = Typesafe::SDK.image(Pathname("catalog/mug.png"))
185
+ uploads = params[:photos].map { |file| Typesafe::SDK.image(file.read) }
186
+
187
+ response = client.system_one(
188
+ state: Typesafe::SDK.with_assets("The seller uploaded ", Typesafe::SDK.join_assets(uploads, ", "), "."),
189
+ questions: {
190
+ same_product: Typesafe::SDK.noul(Typesafe::SDK.with_assets("Does ", upload, " show the product in ", catalog, "?")),
191
+ },
192
+ )
193
+ ```
194
+
195
+ Each image is sent once, however many times it is used. Plain string interpolation, such as
196
+ `"Compare #{upload}"`, writes only the image's reference, so the call raises `TypeSafeError`
197
+ before the request is sent.
198
+
199
+ **Choose your own references with `assets:`.** Pass images keyed by names of your choice, then
200
+ use those names in plain strings, such as prompts stored in configuration:
201
+
202
+ ```ruby
203
+ response = client.system_one(
204
+ state: "A customer returned <delivered>. The catalog photo is <catalog>.",
205
+ questions: { matches: Typesafe::SDK.noul("Is <delivered> the product shown in <catalog>?") },
206
+ assets: {
207
+ "<delivered>" => Typesafe::SDK.image(File.binread("returns/RMA-1042.jpg")),
208
+ "<catalog>" => Typesafe::SDK.image(Pathname("catalog/mug.png")),
209
+ },
210
+ )
211
+ ```
212
+
213
+ Use distinct delimiters, such as `<delivered>`, so names don't match ordinary text. An image in
214
+ `assets:` that also appears in content or `with_assets` text is written there under its name.
215
+ Every name in `assets:` must appear in the request; otherwise the call raises `TypeSafeError`
216
+ before sending.
217
+
218
+ **Locate something with `box`.** A box question locates what its instructions describe. The
219
+ answer's `box` holds the edges as fractions of the image's height (`top`, `bottom`) and width
220
+ (`left`, `right`). Its `confidence` is `nil` when the API doesn't report one. Use the
221
+ `jev-preview` model for box questions:
222
+
223
+ ```ruby
224
+ response = client.system_one(
225
+ state: Typesafe::SDK.image(Pathname("returns/RMA-1042.jpg")),
226
+ questions: { damage: Typesafe::SDK.box("The damaged area of the product") },
227
+ model: "jev-preview",
228
+ )
229
+ box = response[:damage].box
230
+ puts [box.top, box.bottom, box.left, box.right].inspect
231
+ ```
232
+
233
+ Requests with images are sent as `multipart/form-data`, with each image's file bytes in its own part.
234
+
235
+ ## Using confidence
236
+
237
+ Choice and Score include confidence derived from their probability distributions. Confidence
238
+ is distinct from the probability of the selected option and is not a guarantee of correctness.
239
+ Noul returns the probability of yes and has no separate confidence field.
240
+ See the [official confidence guide](https://docs.typesafe.ai/confidence).
241
+
242
+ ```ruby
243
+ category = response[:category]
244
+ threshold = 0.8 # Illustrative: evaluate a suitable threshold on your own labeled examples.
245
+ puts(category.confidence >= threshold ? "Route to #{category.choice}" : "Needs review")
246
+ ```
67
247
 
68
248
  ## Configuration
69
249
 
70
250
  Explicit options take precedence over environment variables, then SDK defaults.
251
+ Blank environment values are ignored for optional settings. An API key is required.
252
+ From Ruby 0.6.0.1, outer spaces, tabs, and line endings are trimmed from keys; blank keys,
253
+ non-ASCII text, embedded whitespace, and control characters raise `TypeSafeError` at
254
+ construction. Error messages do not include the key.
71
255
 
72
256
  | Option | Environment variable | Default |
73
257
  | --- | --- | --- |
74
- | `api_key:` | `TYPESAFE_API_KEY` | — (required) |
258
+ | `api_key:` | `TYPESAFE_API_KEY` | Required |
75
259
  | `base_url:` | `TYPESAFE_BASE_URL` | `https://api.typesafe.ai` |
76
260
  | `default_model:` | `TYPESAFE_DEFAULT_MODEL` | `jev-latest` |
77
261
  | `log_level:` | `TYPESAFE_LOG_LEVEL` | `warn` |
262
+ | `compression:` | None | `:zstd` when `zstd-ruby` is installed, otherwise `false` |
78
263
 
79
264
  ```ruby
80
265
  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
266
+ timeout: 10, # Seconds per attempt, including connection setup and response body receipt.
86
267
  retry_policy: { max_retries: 2, backoff_initial_ms: 500 },
87
- default_headers: { "X-My-Header" => "value" },
268
+ log_level: :info, # :debug, :info, :warn, :error, or :off
269
+ default_headers: { "X-My-App" => "support" },
88
270
  )
89
271
  ```
90
272
 
273
+ ### Request compression
274
+
275
+ Added in Ruby **0.7.0**. When the optional [`zstd-ruby`](https://rubygems.org/gems/zstd-ruby) gem
276
+ is available (add `gem "zstd-ruby"` to your Gemfile), the client compresses request bodies of
277
+ 1 KiB or more with zstd, which reduces upload size and time, especially for large states and
278
+ images. Smaller bodies are sent uncompressed. Without the gem, all bodies are sent uncompressed;
279
+ `compression: :zstd` then raises `TypeSafeError`. Pass `compression: false` to send all bodies
280
+ uncompressed, for example to an API that doesn't accept compressed requests:
281
+
282
+ ```ruby
283
+ client = Typesafe::SDK::Client.new(compression: false)
284
+ ```
285
+
286
+ `jev-latest` is a moving alias. For reproducible deployments, select an explicit model from
287
+ `client.models.list` and set `default_model:` or a per-call `model:`. See
288
+ [available models and aliases](https://docs.typesafe.ai/models).
289
+
91
290
  ## Retries and timeouts
92
291
 
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:
292
+ By default, the SDK retries HTTP 408, 429, and 5xx responses, connection failures, and timeouts,
293
+ with up to two retries after the initial attempt. Backoff starts at 500 ms, doubles up to
294
+ 5,000 ms, and uses up to 25% downward jitter. Server `Retry-After` and `retry-after-ms` delays
295
+ are honored up to 60,000 ms; larger delays fall back to backoff. From Ruby 0.6.0.1, blank
296
+ or malformed delay headers also fall back to backoff; an explicit zero remains valid.
297
+ A blank `retry-after-ms` still permits a valid `Retry-After` header to be used.
298
+
299
+ Each attempt receives a fresh timeout, so total call time can include several attempts and
300
+ backoff. Override settings per client or per call:
96
301
 
97
302
  ```ruby
98
303
  client.system_one(
99
- state: "...",
100
- questions: { ... },
304
+ state: "A refund request",
305
+ questions: { billing: Typesafe::SDK.noul("Is this about billing?") },
101
306
  timeout: 30,
102
- retry_policy: { max_retries: 0 }, # disable retries for this call
307
+ retry_policy: { max_retries: 0 },
103
308
  )
104
309
  ```
105
310
 
311
+ The full retry policy supports `max_retries`, `backoff_initial_ms`, `backoff_max_ms`,
312
+ `backoff_jitter`, `http_statuses`, `respect_retry_after`, `max_retry_after_ms`,
313
+ `api_connection_error`, and `api_timeout_error`. Overrides merge field by field.
314
+ Nil boolean flags inherit the existing setting; explicit `false` disables that behavior.
315
+
106
316
  ## Error handling
107
317
 
108
318
  ```ruby
109
319
  begin
110
- client.system_one(state: "...", questions: { ... })
320
+ client.system_one(
321
+ state: "A refund request",
322
+ questions: { billing: Typesafe::SDK.noul("Is this about billing?") },
323
+ )
111
324
  rescue Typesafe::SDK::RateLimitError => e
112
- retry_after e.retry_after_ms
325
+ warn "Rate limited; suggested retry delay: #{e.retry_after_ms.inspect}ms"
113
326
  rescue Typesafe::SDK::APIError => e
114
- warn "API error #{e.status} (request #{e.request_id}): #{e.body}"
327
+ warn "API error #{e.status}; request ID: #{e.request_id}"
328
+ rescue Typesafe::SDK::APIConnectionError => e
329
+ warn e.message
115
330
  end
116
331
  ```
117
332
 
118
- Error hierarchy:
333
+ Errors reach your code after any configured retries. All SDK errors inherit from
334
+ `Typesafe::SDK::TypeSafeError`:
335
+
336
+ | Error | Meaning |
337
+ | --- | --- |
338
+ | `BadRequestError` | HTTP 400 |
339
+ | `AuthenticationError` | HTTP 401 |
340
+ | `PaymentRequiredError` | HTTP 402; added in 0.6.0.1 |
341
+ | `PermissionDeniedError` | HTTP 403 |
342
+ | `NotFoundError` | HTTP 404 |
343
+ | `ConflictError` | HTTP 409; added in 0.6.0.1 |
344
+ | `PayloadTooLargeError` | HTTP 413; added in 0.6.0.1 |
345
+ | `UnprocessableEntityError` | HTTP 422 |
346
+ | `RateLimitError` | HTTP 429; exposes `retry_after_ms` |
347
+ | `InternalServerError` | HTTP 5xx |
348
+ | `APIError` | Other non-2xx responses; exposes status, headers, body, and request ID |
349
+ | `APIConnectionError` | Connection or response-body delivery failure |
350
+ | `APITimeoutError` | Subclass of `APIConnectionError`; exposes `timeout_ms` |
351
+ | `APIUserAbortError` | Caller cancellation; never retried |
352
+
353
+ From Ruby 0.7.0, when an HTML page from Cloudflare answers instead of the API, for example because
354
+ its firewall matched text such as a shell command in `state`, the error message says so and
355
+ includes the `cf-ray` ID to report to TypeSafe support. The error class and `body` are unchanged.
356
+
357
+ ## Response metadata and additional fields
358
+
359
+ Added for Ruby **0.6.0.1**:
360
+
361
+ ```ruby
362
+ result = client.system_one(
363
+ state: "A refund request",
364
+ questions: { billing: Typesafe::SDK.noul("Is this about billing?") },
365
+ with_response: true,
366
+ )
367
+ puts result.data[:billing].noul
368
+ puts result.request_id
369
+ puts result.response.status
370
+ ```
119
371
 
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
372
+ `client.models.list(with_response: true)` wraps the model cards the same way. The response
373
+ exposes case-insensitive `headers` and a parsed `body`.
374
+ `extra_body: { new_option: nil }` forwards additional JSON fields; named `state`, `questions`,
375
+ and `model` arguments take precedence. The API decides whether a field is supported.
128
376
 
129
377
  ## Cancellation
130
378
 
131
379
  ```ruby
132
380
  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
381
+ canceller = Thread.new { sleep 5; signal.cancel }
382
+ begin
383
+ client.system_one(
384
+ state: "A refund request",
385
+ questions: { billing: Typesafe::SDK.noul("Is this about billing?") },
386
+ signal: signal,
387
+ )
388
+ rescue Typesafe::SDK::APIUserAbortError
389
+ warn "Request canceled"
390
+ ensure
391
+ canceller.kill.join
392
+ end
137
393
  ```
138
394
 
395
+ From Ruby 0.6.0.1, cancellation interrupts active requests and retry backoff, and cleans up
396
+ the request worker and socket before returning.
397
+
139
398
  ## Listing models
140
399
 
141
400
  ```ruby
142
401
  client.models.list.each do |model|
143
- puts "#{model.name}: #{model.description}"
402
+ puts "#{model.name}: #{model.description} (#{model.release_date})"
144
403
  end
145
404
  ```
146
405
 
147
- ## Logging
148
-
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`:
152
-
153
- ```ruby
154
- client = Typesafe::SDK::Client.new(logger: Rails.logger, log_level: :info)
155
- ```
406
+ ## Logging and Rails
156
407
 
157
- ## Ruby on Rails
408
+ The default logger writes to `$stderr` with a `[typesafe-ai]` prefix. `info` includes request
409
+ summaries; `debug` adds redacted credential headers and request/response bodies. Bodies may
410
+ contain application data and are **not** redacted. Image bytes are not logged; requests show each
411
+ image's multipart part name. From Ruby 0.7.0, `x-api-key` values are always fully masked, and
412
+ authorization headers keep only an alphabetic scheme, such as `Bearer`, that a credential follows.
158
413
 
159
- The gem is framework-agnostic and works out of the box in Rails. A common pattern is a
160
- wrapped initializer:
414
+ From Ruby 0.6.0.1, standard Ruby `Logger` and `Rails.logger` also accept debug details:
161
415
 
162
416
  ```ruby
163
- # config/initializers/typesafe.rb
164
- TYPESAFE = Typesafe::SDK::Client.new(log_level: :info)
417
+ # config/initializers/typesafe.rb; configure TYPESAFE_API_KEY through your secret store.
418
+ TYPESAFE = Typesafe::SDK::Client.new(logger: Rails.logger, log_level: :info)
165
419
  ```
166
420
 
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
- ```
182
-
183
- ## Development
421
+ Call `TYPESAFE.system_one(...)` from application code. In plain Ruby, use
422
+ `require "logger"` and pass `logger: Logger.new($stderr)`; add the `logger` gem to your
423
+ application if your Ruby version does not provide it by default.
424
+
425
+ ## Coming from JavaScript
426
+
427
+ | JavaScript | Ruby |
428
+ | --- | --- |
429
+ | `new TypeSafeClient()` | `Typesafe::SDK::Client.new` |
430
+ | `systemOne(request, options)` | `system_one(state:, questions:, **options)` |
431
+ | `apiKey`, `baseURL`, `defaultModel` | `api_key:`, `base_url:`, `default_model:` |
432
+ | `timeout` in milliseconds | `timeout:` in seconds |
433
+ | `retry: { maxRetries: 0 }` | `retry_policy: { max_retries: 0 }` |
434
+ | `.withResponse()` | `with_response: true` (Ruby 0.6.0.1+) |
435
+ | `AbortController` | `Typesafe::SDK::Signal` |
436
+ | `image(blob)`, `Image.fromBase64(text)` | `Typesafe::SDK.image(bytes_or_pathname)`, `Typesafe::SDK::Image.from_base64(text)` |
437
+ | `` withAssets`Compare ${a} with ${b}` `` | `Typesafe::SDK.with_assets("Compare ", a, " with ", b)` |
438
+ | `withAssets.join(images, ", ")` | `Typesafe::SDK.join_assets(images, ", ")` |
439
+ | `compression: false` | `compression: false` |
440
+ | `answers.category` | `response[:category]` or `response.answers["category"]` |
441
+
442
+ Ruby calls are synchronous and return Ruby answer objects. They do not provide TypeScript
443
+ compile-time inference, Fetch streams, connection pooling, or automatic redirect following.
444
+ See [tested compatibility and intentional differences](docs/COMPATIBILITY.md).
445
+
446
+ ## Development and contributing
184
447
 
185
448
  ```sh
186
449
  bundle install
187
- bundle exec rake # specs + RuboCop
188
- bundle exec rspec # specs only
189
- bundle exec rubocop # lint only
450
+ bundle exec rake # Tests and RuboCop; no API key or external API calls required.
190
451
  ```
191
452
 
192
- ## Contributing
453
+ Native transport tests bind loopback sockets. CI also builds and installs the packaged gem.
454
+ For an explicit live check, follow [live verification](docs/COMPATIBILITY.md#live-verification).
455
+ The [demo](examples/demo.rb) uses real API calls and can be run with
456
+ `bundle exec ruby examples/demo.rb` after configuring your key.
457
+
458
+ See [CONTRIBUTING.md](CONTRIBUTING.md), the [PR template](.github/PULL_REQUEST_TEMPLATE.md),
459
+ and the [release process](docs/RELEASING.md). Changes are reviewed and merged by the repository owner.
460
+
461
+ ## Support and security
193
462
 
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).
463
+ Use [issues](https://github.com/afurm/typesafe-sdk-ruby/issues) for Ruby client bugs and
464
+ [discussions](https://github.com/afurm/typesafe-sdk-ruby/discussions) for usage questions.
465
+ See [SUPPORT.md](SUPPORT.md) for API/account questions and useful report details.
466
+ Report vulnerabilities privately using [SECURITY.md](SECURITY.md).
467
+ All participation follows our [Code of Conduct](CODE_OF_CONDUCT.md).
197
468
 
198
469
  ## License
199
470
 
200
- The gem is available as open source under the terms of the [MIT License](LICENSE).
471
+ [MIT](LICENSE), including attribution to the upstream TypeSafe JavaScript SDK.