ask-core
The foundation gem. Zero dependencies. Every ask-rb app depends on this.
gem "ask-core"
What it provides
Conversation
The core data structure — an ordered list of messages that can be serialized for any provider API.
require "ask"
conversation = Ask::Conversation.new
conversation.system("You are a helpful assistant")
conversation.user("What's the weather in Paris?")
conversation.assistant(tool_calls: [
{ id: "call_1", name: "get_weather", arguments: { city: "Paris" } }
])
conversation.size # => 3
conversation.last.tool_call? # => true
Streaming
Receive responses incrementally without mutating state.
require "ask"
stream = Ask::Stream.new
stream.add(Ask::Chunk.new(content: "Hello"))
stream.add(Ask::Chunk.new(content: " world"))
stream.finish!
stream.accumulated_text # => "Hello world"
Provider interface
A base class that all provider gems implement.
require "ask"
class MyProvider < Ask::Provider
def chat(messages, model:, tools: nil, temperature: nil, stream: nil, schema: nil, **params, &block)
# Return an Ask::Message, or yield Ask::Chunk when streaming
end
end
Role mapping
Message roles are normalized on construction. "user", :user, and any string form become the canonical symbol; unknown roles raise InvalidRole.
require "ask"
Ask::Message.new(role: "user", content: "hi").user? # => true
error = begin
Ask::Message.new(role: :bogus, content: "hi")
rescue Ask::InvalidRole => e
e.message
end
error # => "Invalid role: :bogus. Valid: system, user, assistant, tool"
The valid roles are :system, :user, :assistant, and :tool.
Credential resolution
Credentials are resolved by the ask-auth gem, not by ask-core. Ask::Auth.resolve(:openai_api_key) walks env vars, ~/.ask/credentials.yml, Rails credentials, a database-backed store, and OAuth — see Credential Resolution.
Model Catalog
A process-wide singleton registry of known LLM models. Each entry is an immutable Ask::ModelInfo value object.
require "ask"
require "ask-llm-providers" # loads the bundled model catalog
# Find a model by ID
model = Ask::ModelCatalog.find("gpt-4o")
model.provider # => "openai"
model.context_window # => 128000
model.max_output_tokens # => 16384
model.supports?(:vision) # => true
# Filter by capability
Ask::ModelCatalog.chat_models.size # => 388
Ask::ModelCatalog.embedding_models.size # => 5
# Filter by metadata
Ask::ModelCatalog.by_family("gpt").size # => 19
Models are loaded into the catalog by Ask::LLM::Catalog.load! (from ask-llm-providers) or registered individually:
require "ask"
model = Ask::ModelInfo.new(
id: "my-model",
provider: "local",
context_window: 4096,
max_output_tokens: 1024,
capabilities: ["chat"]
)
model.id # => "my-model"
model.context_window # => 4096
model.supports?(:chat) # => true
Ask::ModelCatalog.instance.register(model)
Ask::ModelCatalog.find("my-model") == model # => true
ModelInfo
Immutable value object for model metadata.
| Attribute | Type | Description |
|---|---|---|
id | String | Model identifier (e.g. "gpt-4o") |
name | String | Human-readable name |
provider | String | Provider slug (e.g. "openai") |
family | String, nil | Model family (e.g. "gpt", "claude") |
capabilities | Array | Capability flags |
context_window | Integer, nil | Max context window in tokens |
max_output_tokens | Integer, nil | Max output tokens |
modalities | Hash | Input/output modality lists |
pricing | Hash | Cost per token |
knowledge_cutoff | Date, nil | Training data cutoff |
created_at | Date, nil | Release date |
Provider Override
When using the catalog through ask-agent, you can override which provider serves a model by passing provider: to Ask::Agent::Session or Ask::Agent::Chat:
require "ask-agent"
require "ask-tools-shell"
# deepseek-v4-flash is cataloged under the "deepseek" provider,
# but can be served through a different compatible provider:
session = Ask::Agent::Session.new(
model: "deepseek-v4-flash",
provider: :opencode_go,
tools: [Ask::Tools::Bash]
)
session.tools.size # => 2
This bypasses the catalog’s provider field and uses the specified provider instead. Useful when a model is available through multiple providers (e.g., self-hosted vs API).
Error Types
Structured errors with actionable metadata. RateLimitError carries category, type, and retry-after for intelligent handling:
rescue Ask::RateLimitError => e
e.category # => :vendor (upstream provider) or :local (ask-rb)
e.rate_limit_type # => :requests, :tokens, :concurrent, or :budget
e.retry_after # => seconds to wait before retrying (from provider headers)
if e.category == :vendor && e.retry_after
sleep e.retry_after
retry
end
end
| Error | When |
|---|---|
Ask::RateLimitError | 429 — carries category, rate_limit_type, retry_after |
Ask::Unauthorized | 401/403 — API key missing or invalid |
Ask::ServerError | 5xx — provider outage |
Ask::ServiceUnavailable | 503 — temporary downtime |
Ask::ContextLengthExceeded | Context window exceeded |
Ask::ProviderError | Other provider API errors (carries status_code, response_body) |
Document
Ask::Document is a frozen value object for text content with metadata. It flows through every stage of a RAG pipeline — loaded from files, split into chunks, embedded and stored, and returned by retrieval queries.
require "ask"
doc = Ask::Document.new(
content: "Ruby was created by Matz in 1995.",
metadata: { source: "history.pdf", page: 3 }
)
doc.content # => "Ruby was created by Matz in 1995."
doc.metadata # => {source: "history.pdf", page: 3}
doc.id # => nil (optional)
doc.to_h # => {content: "Ruby was created by Matz in 1995.", metadata: {source: "history.pdf", page: 3}}
Two documents are equal when their content and metadata match. The id field is ignored for equality comparisons.
Content (Multi-Modal)
Ask::Content provides typed value objects for multi-modal message content. Messages can carry images, audio, video, and files alongside text.
require "ask"
# Text
Ask::Content::Text.new("What's in this image?")
# Image from URL
Ask::Content::Image.new(url: "https://example.com/photo.jpg", mime_type: "image/jpeg")
# Image from base64 data
Ask::Content::Image.new(base64: "...base64...", mime_type: "image/png")
# Audio
Ask::Content::Audio.new(url: "https://example.com/audio.mp3", mime_type: "audio/mpeg")
# Video
Ask::Content::Video.new(url: "https://example.com/video.mp4", mime_type: "video/mp4")
# File (inline content)
Ask::Content::File.new(data: "file content", mime_type: "text/plain", filename: "notes.txt")
Use content blocks in messages:
require "ask"
msg = Ask::Message.new(role: :user, content: [
Ask::Content::Text.new("What's in this image?"),
Ask::Content::Image.new(url: "https://example.com/photo.jpg", mime_type: "image/jpeg")
])
msg.multimodal? # => true
msg.content_blocks.size # => 2
# Backward compatible: plain text is extracted from the blocks
msg.content # => "What's in this image?"
# Or via conversation
conv = Ask::Conversation.new
conv.user([
Ask::Content::Text.new("Describe this"),
Ask::Content::Image.new(base64: "data", mime_type: "image/png")
])
conv.last.multimodal? # => true
Content blocks are frozen value objects with structural equality and #to_h serialization. Each provider (OpenAI, Anthropic, etc.) transforms them to its own wire format automatically.
Cost calculation and pricing data
Ask::LLM::CostCalculator ships with ask-llm-providers, alongside the model catalog data: 402 models across 12 providers with pricing from models.dev and OpenRouter. Run rake models:update in the ask-llm-providers repo before a release. See LLM Providers.
Exports
Conversation, Message, Stream, Chunk, Provider, ModelCatalog, ModelInfo, ToolDef, Result, Content (Text, Image, Audio, Video, File), Document, ProviderTool, State::Adapter, and the Ask::Error hierarchy.