UltrasafeAI Ruby SDK

Gem Version License: MIT

The official Ruby client library for the UltrasafeAI API. Provides access to chat completions, vision, embeddings, reranking, image generation, speech-to-text, real-time audio streaming, vector stores, assistants, threads, and more.

Requires Ruby >= 3.0.

Base URL: https://api.us.tech/v1

Installation

gem install ultrasafeai

Or in your Gemfile:

gem "ultrasafeai"

Client Setup

The client reads ULTRASAFEAI_API_KEY from the environment automatically if api_key: is not passed.

require "ultrasafeai"

client = Ultrasafeai::Client.new(api_key: ENV["ULTRASAFEAI_API_KEY"])

Options:

Keyword Description
api_key: Your UltrasafeAI API key
base_url: Override the base URL
timeout: Request timeout in seconds (default: 60)
max_retries: Max retry attempts (default: 2)

Chat Completions

Non-Streaming

Method: client.chat.completions.create(**params)
Endpoint: POST /chat/completions

require "ultrasafeai"

client = Ultrasafeai::Client.new(api_key: ENV["ULTRASAFEAI_API_KEY"])

completion = client.chat.completions.create(
  model: "usf-mini",
  messages: [
    { role: "system", content: "You are a helpful assistant." },
    { role: "user",   content: "Hello!" }
  ]
)
puts completion.choices.first.message.content

Parameters:

Key Type Required Description
model: String Yes Model ID (e.g. "usf-mini")
messages: Array<Hash> Yes Conversation history. Roles: "system", "user", "assistant", "tool"
tools: Array<Hash> No Function or custom tools the model may call
tool_choice: `String \ Hash` No "none", "auto", "required", or a specific tool
parallel_tool_calls: Boolean No Allow parallel tool calls (default: true)
reasoning_effort: String No Controls reasoning depth: "none", "low", "medium", "high" (default: "none")
web_search: Boolean No Enable web search (default: false)
response_format: Hash No {type: "text"}, {type: "json_object"}, or {type: "json_schema", json_schema: {...}}
max_tokens: Integer No Max tokens to generate
temperature: Float No Sampling temperature 0–2
top_p: Float No Nucleus sampling probability mass
n: Integer No Number of completions to generate
stop: `String \ Array` No Stop sequences (up to 4)
presence_penalty: Float No Penalty for repeated tokens (-2.0 to 2.0)
frequency_penalty: Float No Frequency-based penalty (-2.0 to 2.0)
seed: Integer No Seed for deterministic sampling
store: Boolean No Store conversation for retrieval
conversation_id: String No Continue an existing stored conversation
user: String No Stable end-user identifier

Response: Ultrasafeai::ChatCompletion

completion.id                                    # "chatcmpl-abc123"
completion.object                                # "chat.completion"
completion.created                               # Integer unix timestamp
completion.model                                 # "usf-mini"
completion.conversation_id                       # present when store: true
completion.choices.first.message.role            # "assistant"
completion.choices.first.message.content         # "Hello! How can I help you?"
completion.choices.first.message.tool_calls      # non-nil when finish_reason="tool_calls"
completion.choices.first.finish_reason           # "stop" | "length" | "tool_calls" | "content_filter"
completion.usage.prompt_tokens                   # 12
completion.usage.completion_tokens               # 10
completion.usage.total_tokens                    # 22

Streaming

Method: client.chat.completions.create(stream: true, ...) { |chunk| ... }
Endpoint: POST /chat/completions (with stream: true)

client.chat.completions.create(
  model: "usf-mini",
  stream: true,
  messages: [{ role: "user", content: "Tell me a joke" }]
) do |chunk|
  content = chunk.choices&.first&.delta&.content
  print content if content
end
puts

Response: Yields Ultrasafeai::ChatCompletionChunk per iteration.

chunk.id                                         # "chatcmpl-abc123"
chunk.object                                     # "chat.completion.chunk"
chunk.choices.first.delta.role                   # "assistant" — only on first chunk
chunk.choices.first.delta.content                # incremental text; concatenate across chunks
chunk.choices.first.delta.reasoning_content      # chain-of-thought when available
chunk.choices.first.delta.tool_calls             # incremental tool call data
chunk.choices.first.finish_reason                # non-nil only on the final chunk

Vision

Vision uses the same create method. Pass an array of content part hashes instead of a plain string.

Non-Streaming

require "base64"

image_data = File.binread("photo.jpg")
b64 = Base64.strict_encode64(image_data)

completion = client.chat.completions.create(
  model: "usf-mini-vision",
  messages: [{
    role: "user",
    content: [
      { type: "text",      text: "What is in this image?" },
      { type: "image_url", image_url: { url: "data:image/jpeg;base64,#{b64}" } }
    ]
  }]
)
puts completion.choices.first.message.content

URL image:

completion = client.chat.completions.create(
  model: "usf-mini-vision",
  messages: [{
    role: "user",
    content: [
      { type: "text",      text: "What is in this image?" },
      { type: "image_url", image_url: { url: "https://example.com/image.jpg" } }
    ]
  }]
)

Streaming

client.chat.completions.create(
  model: "usf-mini-vision",
  stream: true,
  messages: [{
    role: "user",
    content: [
      { type: "text",      text: "What's in this image?" },
      { type: "image_url", image_url: { url: "https://example.com/image.jpg" } }
    ]
  }]
) do |chunk|
  content = chunk.choices&.first&.delta&.content
  print content if content
end

Content part types:

type: value Additional keys Description
"text" text: String Plain text content
"image_url" image_url: { url: String } URL or data:image/...;base64,... string

Response: Same ChatCompletion / yielded ChatCompletionChunk as standard chat completions.


Embeddings

Method: client.embeddings.create(**params)
Endpoint: POST /embeddings

# Single string
response = client.embeddings.create(
  model: "usf-embed",
  input: "The quick brown fox"
)
puts response.data.first.embedding.inspect  # Array<Float>

# Multiple strings
response = client.embeddings.create(
  model: "usf-embed",
  input: ["First sentence", "Second sentence"],
  dimensions: 512
)

Parameters:

Key Type Required Description
model: String Yes Embedding model ID (e.g. "usf-embed")
input: `String \ Array` Yes Text or token arrays to embed. Max 8192 tokens per input, 300k total
dimensions: Integer No Output embedding dimensions
encoding_format: String No "float" (default) or "base64"
user: String No End-user identifier

Response: Ultrasafeai::EmbeddingResponse

response.object                              # "list"
response.data.first.object                   # "embedding"
response.data.first.index                    # 0
response.data.first.embedding                # Array<Float>
response.model                               # "usf-embed"
response.usage.prompt_tokens                 # 8
response.usage.total_tokens                  # 8

Reranker

Method: client.rerank.create(**params)
Endpoint: POST /rerank

response = client.rerank.create(
  model: "usf-rerank",
  query: "What is machine learning?",
  texts: [
    "Machine learning is a subset of AI.",
    "The weather is sunny today.",
    "Deep learning uses neural networks."
  ],
  top_n: 2
)

response.results.each do |result|
  puts "#{result.index}  #{result.relevance_score}  #{result.text}"
end

Parameters:

Key Type Required Description
model: String Yes Rerank model ID (e.g. "usf-rerank")
query: String Yes Search query to rank documents against
texts: Array<String> Yes Documents to rerank
top_n: Integer No Number of top results to return

Response: Ultrasafeai::RerankResponse

response.results.first.index            # 0
response.results.first.relevance_score  # 0.97
response.results.first.text             # "Machine learning is a subset of AI."

Image Generation

Method: client.images.generate(**params)
Endpoint: POST /images/generations

response = client.images.generate(
  model: "usf-mini-image",
  prompt: "A futuristic city at sunset",
  size: "1024x1024",
  n: 1,
  response_format: "url"
)
puts response.data.first.url

Parameters:

Key Type Required Description
model: String Yes Image model ID (e.g. "usf-mini-image")
prompt: String Yes Text description of the image to generate
size: String No "256x256", "512x512", "1024x1024"
n: Integer No Number of images to generate
response_format: String No "url" (default) or "b64_json"

Response: Ultrasafeai::ImageResponse

response.created              # Integer unix timestamp
response.images.first.url     # "https://..." (when response_format: "url")
response.images.first.b64_json # base64 string (when response_format: "b64_json")
response.data.first.url       # OpenAI-compat alias; same contents as images

Speech to Text

Method: client.audio.transcriptions.create(**params)
Endpoint: POST /audio/transcribe

File.open("audio.mp3", "rb") do |f|
  response = client.audio.transcriptions.create(
    file: f,
    model: "usf-mini-asr",
    language: "en",
    response_format: "json"
  )
  puts response.text
end

Parameters:

Key Type Required Description
file: IO Yes Audio file (mp3, mp4, wav, flac, ogg, webm, etc.)
model: String Yes ASR model ID (e.g. "usf-mini-asr")
language: String No ISO 639-1 language code (e.g. "en", "es")
response_format: String No "json" (default), "text", "srt", "verbose_json", "vtt"

Response: Ultrasafeai::TranscriptionResponse

response.text      # "Hello, this is a transcription."
response.language  # "en"
response.duration  # Float seconds

Live ASR (WebSocket)

Live ASR uses a WebSocket-based client. Access it via client.audio.stream.

Class: Ultrasafeai::Audio::Stream::StreamClient
Endpoint: wss://api.us.tech/v1/audio/stream

Dependency: Add gem "websocket-client-simple" to your Gemfile.

connect is synchronous — it blocks until the WebSocket handshake completes, then returns a live AudioStreamSession. Event handlers fire on a background thread.

require "ultrasafeai"
require "ultrasafeai/audio/stream/stream_client"

client = Ultrasafeai::Client.new(api_key: ENV["ULTRASAFEAI_API_KEY"])

session = client.audio.stream.connect(
  Ultrasafeai::Audio::Stream::ConnectOptions.new(
    model:                      "usf-mini-asr",
    sample_rate:                16_000,
    audio_format:               "pcm_s16le",
    enable_vad:                 false,
    partial_results:            true,
    interim_min_duration_ms:    500,
    full_context_retranscription: true
  )
)

session.on("ready")        { |event| puts "Connected — streaming audio" }
session.on("transcript")   { |event| puts event["full_text"] }
session.on("close")        { |code, reason| puts "closed (#{code}) #{reason}" }
session.on("ws_error")     { |exc| warn "WebSocket error: #{exc}" }
session.on("parse_error")  { |exc, raw| warn "bad frame: #{exc}" }

# Send PCM audio (binary String)
session.send(pcm_chunk)

# Keep the main thread alive until the server closes the connection
close_event = Queue.new
session.on("close") { close_event.push(:done) }
close_event.pop

session.close

ConnectOptions keyword arguments:

Keyword Type Default Description
model: String "usf-mini-asr" ASR model ID
sample_rate: Integer 16000 Audio sample rate in Hz
audio_format: String "pcm_s16le" "pcm_s16le" or "pcm_f32le"
enable_vad: Boolean false Enable voice activity detection
partial_results: Boolean true Emit partial results before segment is final
interim_min_duration_ms: Integer 500 Min audio duration (ms) before emitting interim
full_context_retranscription: Boolean true Re-transcribe with full audio context for accuracy
max_retries: Integer 3 Max reconnect attempts on initial connection
backoff_ms: Float 500.0 Base backoff (ms) between retries; doubles each attempt

Session methods:

Method Description
`on(event) { \ *args\ ... }` Subscribe to a named event
`off(event) { \ *args\ ... }` Unsubscribe a handler (by block object identity)
send(audio) Send a PCM audio frame (binary String)
close Close the session gracefully

Session events:

Event Block arguments Description
"ready" `\ event\ Hash` Server ready to receive audio
"transcript" `\ event\ Hash` Transcription result (partial or final)
"speech_activity" `\ event\ Hash` VAD speech start/end
"control" `\ event\ Hash` Lifecycle signal (action: "stop")
"error" `\ event\ Hash` Server-side error
"close" `\ code, reason\ Integer, String` Connection closed
"ws_error" `\ exception\ StandardError` WebSocket transport error
"parse_error" `\ exception, raw\ StandardError, String` Frame could not be decoded

Transcript event hash:

event = ...  # received in the "transcript" handler
event["type"]           # "transcript" | "ready" | "speech_activity" | "control" | "error"
event["request_id"]     # "req_abc"
event["is_final"]       # true | false
event["full_text"]      # "Hello world this is a test"
event["committed_text"] # "Hello world"

segment = event["segment"]
segment["id"]           # Integer
segment["text"]         # "this is a test"
segment["is_final"]     # true | false
segment["start"]        # Float seconds
segment["end"]          # Float seconds
segment["confidence"]   # Float

Vector Stores

Access: client.vector_stores

Create Vector Store

Method: client.vector_stores.create(**params)
Endpoint: POST /vector_stores

File.open("document.pdf", "rb") do |f|
  store = client.vector_stores.create(
    name: "My Knowledge Base",
    files: [f]
  )
  puts store.id      # "vs_abc123"
  puts store.status  # poll until "ready"
end

Parameters:

Key Type Required Description
name: String Yes Display name for the vector store
files: Array<IO> No Files to upload and index immediately

Response: Ultrasafeai::VectorStore

store.id                        # "vs_abc123"
store.object                    # "vector_store"
store.created_at                # Integer
store.name                      # "My Knowledge Base"
store.status                    # "ready"
store.file_counts.in_progress   # 0
store.file_counts.completed     # 1
store.file_counts.total         # 1

List Vector Stores

response = client.vector_stores.list(limit: 20)
response.data.each do |store|
  puts "#{store.id} #{store.name} #{store.status}"
end

Retrieve Vector Store

store = client.vector_stores.retrieve("vs_abc123")
puts store.status

Delete Vector Store

result = client.vector_stores.delete("vs_abc123")
puts result.deleted  # true

Search Vector Store

Method: client.vector_stores.search(vector_store_id, **params)
Endpoint: POST /vector_stores/{vector_store_id}/search

results = client.vector_stores.search("vs_abc123", query: "What is the refund policy?")
results.data.each { |item| puts item }

File Management

Upload File to Vector Store

File.open("doc.pdf", "rb") do |f|
  file = client.vector_stores.upload_file("vs_abc123", file: f)
end

List Vector Store Files

files = client.vector_stores.list_files("vs_abc123", limit: 20)
files.data.each { |f| puts f.id }

Retrieve / Delete Vector Store File

file   = client.vector_stores.retrieve_file("vs_abc123", "file_xyz")
result = client.vector_stores.delete_file("vs_abc123", "file_xyz")
puts result.deleted  # true

File Batches

# Create a batch of files by ID
batch = client.vector_stores.create_file_batch(
  "vs_abc123",
  file_ids: ["file_abc", "file_def"]
)

# Retrieve batch status
status = client.vector_stores.retrieve_file_batch("vs_abc123", batch.id)

# Cancel a running batch
client.vector_stores.cancel_file_batch("vs_abc123", batch.id)

# List files in a batch
batch_files = client.vector_stores.list_batch_files("vs_abc123", batch.id)

Assistants

Access: client.assistants

Create Assistant

assistant = client.assistants.create(
  model: "usf-mini",
  name: "My Assistant",
  description: "A helpful customer support bot",
  instructions: "You are a customer support agent. Be concise and friendly.",
  tools: [{ type: "code_interpreter" }],
  temperature: 0.5
)
puts assistant.id

Parameters:

Key Type Required Description
model: String Yes Model ID
name: String No Assistant name
description: String No Short description
instructions: String No System prompt / instructions
tools: Array<Hash> No Tool definitions (e.g. [{ type: "code_interpreter" }])
tool_resources: Hash No Resources for tools
metadata: Hash No Arbitrary key-value metadata
temperature: Float No Sampling temperature
top_p: Float No Nucleus sampling
response_format: String No Response format

Response: Ultrasafeai::Assistant

assistant.id           # "asst_abc123"
assistant.object       # "assistant"
assistant.created_at   # Integer
assistant.name         # "My Assistant"
assistant.model        # "usf-mini"
assistant.instructions # "You are a customer support agent."

List Assistants

response = client.assistants.list(limit: 20)
response.data.each { |a| puts "#{a.id} #{a.name}" }

Retrieve Assistant

assistant = client.assistants.retrieve("asst_abc123")
puts assistant.name

Delete Assistant

result = client.assistants.delete("asst_abc123")
puts result.deleted  # true

Threads

Access: client.threads

Create Thread

Method: client.threads.create(**params)
Endpoint: POST /threads

thread = client.threads.create(
  messages: [{ role: "user", content: "Hello, I need help with my account." }]
)
puts thread.id  # "thread_abc123"

Parameters:

Key Type Required Description
messages: Array<Hash> No Initial messages to seed the thread
metadata: Hash No Arbitrary key-value metadata

Response: Ultrasafeai::Thread

thread.id         # "thread_abc123"
thread.object     # "thread"
thread.created_at # Integer

List Threads

response = client.threads.list(limit: 20)
response.data.each { |t| puts "#{t.id} #{t.created_at}" }

Retrieve Thread

thread = client.threads.retrieve("thread_abc123")
puts thread.id

Thread Messages

Add Message to Thread

Method: client.threads.add_message(thread_id, **params)
Endpoint: POST /threads/{thread_id}/messages

message = client.threads.add_message(
  "thread_abc123",
  role: "user",
  content: "Can you summarize my previous question?"
)
puts message.id    # "msg_xyz"
puts message.role  # "user"

Parameters:

Key Type Required Description
role: String Yes Message role: "user" or "assistant"
content: String Yes Text content of the message
attachments: Array<Hash> No File attachments
metadata: Hash No Arbitrary key-value metadata

List Messages in Thread

Method: client.threads.list_messages(thread_id, **params)
Endpoint: GET /threads/{thread_id}/messages

messages = client.threads.list_messages("thread_abc123", limit: 20)
messages.data.each { |msg| puts "#{msg.role}: #{msg.content}" }

Run Thread with Assistant

Method: client.threads.run(thread_id, **params)
Endpoint: POST /threads/{thread_id}/runs

run = client.threads.run(
  "thread_abc123",
  assistant_id: "asst_abc123",
  model: "usf-mini",
  instructions: "Be concise."
)
puts run.id      # "run_abc"
puts run.status  # "queued" | "in_progress" | "completed" | "failed"

Parameters:

Key Type Required Description
assistant_id: String Yes Assistant to use for this run
model: String No Override the assistant's model
instructions: String No Override the assistant's instructions
tools: Array<Hash> No Override the assistant's tools
metadata: Hash No Arbitrary key-value metadata

Models

Access: client.models

List Models

Method: client.models.list
Endpoint: GET /models

response = client.models.list
response.data.each do |model|
  puts "#{model.id}  #{model.type}  #{model.description}"
end

Response: Ultrasafeai::ListModelsResponse

response.object                      # "list"
response.data.first.id               # "usf-mini"
response.data.first.object           # "model"
response.data.first.name             # "USF Mini"
response.data.first.type             # "chat"
response.data.first.description      # "Fast and efficient chat model"
response.data.first.is_active        # true
response.data.first.created          # Integer
response.data.first.owned_by         # "ultrasafeai"

Retrieve Model

Method: client.models.retrieve(model_id)
Endpoint: GET /models/{model}

model = client.models.retrieve("usf-mini")
puts "#{model.id} #{model.is_active}"

Error Handling

require "ultrasafeai/errors"

begin
  completion = client.chat.completions.create(
    model: "usf-mini",
    messages: [{ role: "user", content: "Hello" }]
  )
rescue Ultrasafeai::UnauthorizedError => e
  puts "Invalid API key: #{e.message}"
rescue Ultrasafeai::BadRequestError => e
  puts "Bad request: #{e.message}"
rescue Ultrasafeai::PaymentRequiredError => e
  puts "Insufficient credits: #{e.message}"
rescue Ultrasafeai::APIError => e
  puts "API error #{e.status_code}: #{e.message}"
end
Exception HTTP Status Description
Ultrasafeai::UnauthorizedError 401 Invalid or missing API key
Ultrasafeai::BadRequestError 400 Invalid request parameters
Ultrasafeai::PaymentRequiredError 402 Insufficient account credits
Ultrasafeai::RateLimitError 429 Rate limit exceeded
Ultrasafeai::APIError any Base class with status_code and message

Retries

The client automatically retries on connection errors, timeouts, and 429/5xx responses with exponential backoff. Default is 2 retries.

# Disable retries
client = Ultrasafeai::Client.new(api_key: "...", max_retries: 0)

# Increase retries
client = Ultrasafeai::Client.new(api_key: "...", max_retries: 5)

# Override per request
client.chat.completions.create(
  model: "usf-mini",
  messages: [{ role: "user", content: "Hello" }],
  request_options: { max_retries: 0 }
)

Timeouts

Requests time out after 60 seconds by default.

# Set globally
client = Ultrasafeai::Client.new(api_key: "...", timeout: 30)

# Override per request
client.chat.completions.create(
  model: "usf-mini",
  messages: [{ role: "user", content: "Hello" }],
  request_options: { timeout: 10 }
)

License

MIT