arcana-ai
Version, currently 0.5.08 versions
github.com/trans/arcana-ai
AI service library for Arcana (and other applications)
Nothing has been indexed for 0.5.0 yet. The tag is recorded, its shard.yml has not been read, so the manifest and dependency list below are empty because they are unknown rather than because they are absent.
Installation
# Add this to your shard.yml
dependencies:
arcana-ai:
github: trans/arcana-ai
version: ~> 0.5.0Then run:
shards installshard.yml
No shard.yml has been indexed for 0.5.0. You can read it on the repository.
Dependencies
Unknown: the shard.yml for this version has not been read yet.
README
This README is the one indexed from the repository at its latest ref, not from the tag for this version.
arcana-ai
Provider-agnostic AI client library for Crystal. Chat, image, TTS, SFX, and embedding providers behind a common interface.
Extracted from arcana so it can be used without pulling in the bus/server. Usable standalone as an AI client library, or as a dependency of arcana itself.
Providers
- Chat: OpenAI (endpoint-configurable — also drives Grok, DeepSeek), Anthropic (native Messages API), Gemini (native API)
- Image: OpenAI (GPT Image; gpt-image-2.5-sunburst by default), Runware (FLUX)
- TTS: OpenAI (gpt-4o-mini-tts; OpenAI shuts down its speech models on 2027-01-06), ElevenLabs
- SFX: ElevenLabs
- Embed: OpenAI, Voyage (with query/document input types)
Installation
Add to your shard.yml:
dependencies:
arcana-ai:
github: trans/arcana-ai
version: ~> 0.5.3
Then shards install.
Usage
require "arcana-ai"
chat = Arcana::AI::Chat::Anthropic.new(api_key: ENV["ANTHROPIC_API_KEY"])
request = Arcana::AI::Chat::Request.new(
messages: [Arcana::AI::Chat::Message.new("user", "Hello!")],
model: "claude-sonnet-4-6",
max_tokens: 200,
)
response = chat.complete(request)
puts response.content
Images in a message (a file or bytes, or a URL the provider fetches):
Arcana::AI::Chat::Message.user("What does this picture show?", images: [
Arcana::AI::Chat::ImagePart.file("room.webp", detail: "low"),
])
Chat calls retry rate limits (429), server errors (500, 502, 503, 504,
Anthropic's 529) and network errors up to 3 times, waiting as long as the
provider asks (Retry-After, Gemini's retryDelay) or backing off
0.5 s, 1 s, 2 s with jitter. They never retry OpenAI's insufficient_quota.
response.retries and response.retry_wait say what happened; set
chat.retry_policy = Arcana::AI::RetryPolicy.none to turn it off.
Via the registry:
provider = Arcana::AI::Registry.create_chat("anthropic", {
"api_key" => JSON::Any.new(ENV["ANTHROPIC_API_KEY"]),
})
What calls cost
Each chat response and embedding result carries usage: what the call
used, in the categories providers bill by (uncached input, cache reads,
cache writes, output with reasoning included, web searches), the same for
every provider. OpenAI image and speech results carry it too, with input
and output images and output audio counted apart from text. It's nil when
the provider reported none, never zero.
cost prices it at the provider's list rates:
case cost = response.cost
in Arcana::AI::Pricing::Cost
cost.total # => 0.0111 (BigDecimal, USD)
cost.lines # input, cache_read, output..., each with its rate
cost.rates # => "arcana-ai 2026-10-13", the rate table used
in Arcana::AI::Pricing::Unpriced
cost.reason # NoUsage, UnknownModel, Tier, Region or NoRate
cost.detail
end
Images and speech work the same way: result.cost(at) on an
Image::Result (at: when the call was made) and result.cost on a
TTS::Result. An image's cost is the provider's own figure when it
reports one (Runware does, as reported_cost) and its usage at list
rates otherwise. OpenAI's speech reports usage only for gpt-4o-mini-tts,
which arcana-ai now asks for its audio as events; tts-1 and tts-1-hd
calls, ElevenLabs calls, and OpenAI's older image models (whose cached
tokens go unreported) come back Unpriced.
A Cost isn't an estimate. The rates come from the providers' pricing
pages (src/arcana-ai/pricing/rates.yml) and are looked up by the model
the provider says answered, at the time of the call. Prices that change
with prompt length, US-only inference on Anthropic, and dated price
changes are all applied. What can't be known isn't guessed: a model
missing from the table, a call with no usage, or a tier or region without
rates comes back Unpriced. Account-level terms (free allowances, credits,
discounts, taxes) are outside a call's price. cost.complete? is false
when the total leaves out a charge that isn't set per call, such as
Anthropic code-execution time.
Other services, or other terms, go in a table of your own laid over the
built-in one. Pass name: to a provider pointed at an OpenAI-compatible
service so its responses carry that name:
rates = Arcana::AI::Pricing::Table.default.merge(Arcana::AI::Pricing::Table.from_yaml(<<-YAML))
version: my-app 2026-10-13
models:
- {provider: my-gateway, model: my-model, input: 0.50, output: 1.50}
YAML
response.cost(table: rates)
Arcana::AI::Pricing.cost(provider, model, usage, at: logged_at, table: rates) # price a stored call
The older counts in each provider's own terms (Chat::Response's
prompt_tokens, completion_tokens, cache_read_tokens,
cache_creation_tokens and reasoning_tokens, and Embed::Result's
total_tokens) are deprecated: the compiler warns where you use them,
naming what replaces each. So is Image::Result#cost as a Float64, and
building a result with one. They go away in 0.6.0, when an image's plain
cost becomes the Cost or Unpriced above.
Development
crystal spec
License
MIT
Documentation
Built from the current release. The first visit to a release nobody has asked for starts its build.
Links
This release
- Version
0.5.0- Tagged
- Oct 13, 2026
- Commit
30ffecf51bd8- Indexed
- not yet
Dependents
Repository
github.com/trans/arcana-ai
Metadata
- Created
- Aug 12, 2026
- Updated
- Oct 14, 2026
- Synced
- Oct 14, 2026
- Versions
- 8