TypeSafe AI Ruby Library
September 20, 2026 ยท View on GitHub
The TypeSafe AI Ruby library provides convenient access to the TypeSafe System One API from applications written in Ruby. Send state and typed questions to Jev, and get back structured answers with probabilities and confidence that your code can use directly.
See the TypeSafe documentation for the concepts behind the API.
Installation
Add this line to your application's Gemfile:
gem "typesafe-ai-ruby"
Or install it yourself:
gem install typesafe-ai-ruby
Requirements
- Ruby 3.1 or newer.
- No runtime dependencies beyond the Ruby standard library.
Usage
Get an API key from the TypeSafe console and export it as TYPESAFE_API_KEY:
export TYPESAFE_API_KEY=...
The client reads it automatically, so TypeSafe::Client.new needs no arguments. You can also pass api_key: explicitly.
require "typesafe-ai-ruby"
client = TypeSafe::Client.new
response = client.system_one(
state: "I was charged twice. Please fix this ASAP.",
questions: {
department: {
type: :choice,
instructions: "Which team should handle this?",
criteria: { billing: "Payment issues", technical: "Bugs or integrations", sales: "Pricing questions" }
},
frustration: {
type: :score,
instructions: "How frustrated is the customer?",
criteria: ["Calm", "Frustrated but civil", "Very angry"]
},
is_urgent: { type: :noul, instructions: "Does the message convey urgency?" }
}
)
response.choices[:department].choice # => "billing"
response.choices[:department].confidence # => 1.0
response.scores[:frustration].score # => 1.06
response.nouls[:is_urgent].noul # => 0.97
response.model # => "jev-1.13.0"
Questions
Questions are plain Hashes matching the API reference: a type, optional instructions, and type-specific criteria. Instructions and descriptions can be a string, a Hash, or an Array.
noulasks a yes/no question and returns the probability of yes. Criteria are optional:criteria: { true: "Spam", false: "A real conversation" }.choicepicks one option from a set. Criteria are required; usenilwhen the name speaks for itself.scorerates the state against ordered levels. Criteria are an Array of at least two levels, and each level's index is its score.
Extra keys are passed through untouched, so new API fields work before this library knows about them.
To reuse a question, keep a TypeSafe::Noul, TypeSafe::Choice or TypeSafe::Score in a constant and pass it in place of a Hash:
DEPARTMENT = TypeSafe::Choice.new(instructions: "Which team should handle this?",
criteria: { billing: nil, technical: nil, sales: nil })
client.system_one(state: ticket, questions: { department: DEPARTMENT })
state is whatever the questions are about: a String, a Hash, or an Array. Symbols are converted to strings, and objects that respond to as_json are converted through it.
Answers
system_one returns a TypeSafe::Responses::SystemOneResponse. Answers are keyed by question name and accept String or Symbol keys.
response.answers # every answer
response.nouls # only Noul answers
response.choices # only Choice answers, with #choice, #probabilities and #confidence
response.scores # only Score answers, with #score, #legend, #probabilities and #confidence
response.usage.input_tokens
response.request_id
response.to_h # the raw JSON body
Use confidence to decide whether to act on an answer automatically or hand it to a human. See examples/ for confidence-gated routing and asking many questions in one request.
Errors
Errors inherit from TypeSafe::Error. HTTP failures raise a subclass of TypeSafe::APIError with status, body, headers, endpoint and request_id.
begin
client.system_one(state: ticket, questions: questions)
rescue TypeSafe::RateLimitError => e
sleep(e.retry_after || 1)
rescue TypeSafe::APIError => e
logger.error("TypeSafe #{e.status}: #{e.message} (request #{e.request_id})")
rescue TypeSafe::APIConnectionError => e
# no HTTP response; includes TypeSafe::APITimeoutError
end
Malformed questions raise TypeSafe::ValidationError before anything is sent.
Configuration
Options can be passed to TypeSafe::Client.new or set once for the default client. Explicit options win over environment variables, which win over the defaults.
TypeSafe.configure do |c|
c.api_key = ENV.fetch("TYPESAFE_API_KEY")
c.model = "jev-1.13.0" # TYPESAFE_DEFAULT_MODEL, default jev-latest
c.base_url = "https://api.typesafe.ai" # TYPESAFE_BASE_URL
c.timeout = 10 # seconds per attempt
c.logger = Logger.new($stdout)
c.log_level = :info # TYPESAFE_LOG_LEVEL, default :warn
end
TypeSafe.client.system_one(state: "...", questions: { urgent: { type: :noul, instructions: "Is this urgent?" } })
Per-call overrides go in request_options:
client.system_one(state: "...", questions: questions, model: "jev-preview",
request_options: { timeout: 30, headers: { "X-Team" => "growth" } })
Retries
Requests that fail with 408, 429, 5xx, a connection error or a timeout are retried twice with exponential backoff, honoring Retry-After. Adjust or disable this with a retry_policy:
client = TypeSafe::Client.new(retry_policy: { max_retries: 5, backoff_max: 10 })
client.models.list(request_options: { retry_policy: { max_retries: 0 } })
Logging and instrumentation
At :info the client logs one line per request and each retry. At :debug it also logs headers and bodies, with credential headers redacted.
TypeSafe::Instrumentation reports every call once it finishes, including retries:
TypeSafe::Instrumentation.subscribe(:request_end) do |event|
StatsD.timing("typesafe.request", event.duration, tags: ["status:#{event.http_status}"])
end
Rails
The library has no Rails specific code. Configure the default client in an initializer, keep question definitions in frozen constants, and layer ActiveJob retries on top of the built-in ones. examples/rails/ shows each of these.
Development
After checking out the repo, run bin/setup to install dependencies. Then run bundle exec rake to run the tests, RuboCop and the RBS validation.
The live API tests are skipped unless TYPESAFE_API_KEY is set:
TYPESAFE_API_KEY=... bundle exec rake test:integration
To release a new version, update the version number in lib/typesafe/version.rb and the changelog, then push a matching v* tag. The release workflow publishes the gem to RubyGems through trusted publishing.
License
The gem is available as open source under the terms of the MIT License.