Models¶
The model gateway routes completion requests to the right provider adapter, handles retries, and tracks usage and cost.
Gateway¶
arcana.models.gateway.ModelGateway
¶
ModelGateway(
connections,
*,
providers=None,
retry=None,
pricing=None,
on_cost=None,
unhealthy_cooldown=_UNHEALTHY_COOLDOWN,
)
Single entry point the Agent uses to talk to any model.
Routes provider/model_id strings to the correct adapter, pools
adapter instances per connection, retries transient failures with
exponential backoff, and emits a CostEvent per completed call.
Token usage is recorded on the session regardless. Per-call cost is
emitted only if on_cost is provided at construction; without it,
no CostEvent fires.
Usage::
async with ModelGateway(connections=ConnectionStore()) as gw:
response = await gw.complete("ollama/hermes-3", request)
# Or with a cost sink:
def record(event: CostEvent) -> None:
...
gw = ModelGateway(connections=store, on_cost=record)
Source code in packages/arcana-core/arcana/models/gateway.py
complete
async
¶
Dispatch a completion request, retrying transient errors with backoff.
Source code in packages/arcana-core/arcana/models/gateway.py
stream
async
¶
Stream a response as ModelChunk deltas.
Retry applies only before the first token arrives — mid-stream failures
are surfaced immediately since output cannot be cleanly replayed.
Emits one CostEvent after the stream completes.
Source code in packages/arcana-core/arcana/models/gateway.py
health
async
¶
Check health for one model string or all cached adapters.
A successful check resets the unhealthy flag so the connection is allowed back into the request path without waiting for cooldown.
Source code in packages/arcana-core/arcana/models/gateway.py
resolve
¶
Parse a model reference and return a ModelConnection with default_model set.
Accepted forms:
- provider/model_id
- provider:connection_name/model_id
- provider (bare — uses connection's default_model)
- provider:connection_name (bare named — uses connection's default_model)
When a connection name is given, looks it up by name in ConnectionStore and raises ValueError if it doesn't exist. Without a name, checks by provider first then falls back to ProviderRegistry defaults so out-of-the-box usage requires no config file.
Source code in packages/arcana-core/arcana/models/gateway.py
aclose
async
¶
Close all cached adapters. Called automatically by the context manager.
Source code in packages/arcana-core/arcana/models/gateway.py
arcana.models.gateway.RetryPolicy
dataclass
¶
Exponential backoff with full jitter. No fallback to a different model.
arcana.models.gateway.ProviderRegistry
¶
Maps provider strings to adapter factories.
Adding a provider = one register() call; no gateway changes needed.
Source code in packages/arcana-core/arcana/models/gateway.py
arcana.models.gateway.ProviderEntry
dataclass
¶
Maps a provider string to an adapter factory, its default endpoint, and canonical enum.
Connection store¶
arcana.models.connection_store.ConnectionStore
¶
Reads ModelConnection records from disk and credentials from the OS keyring.
Connections are loaded lazily on first access; call reload() to invalidate
the cache if the file changes at runtime.
Usage::
store = ConnectionStore()
conn = store.get_by_provider(ModelProvider.ANTHROPIC)
key = store.get_api_key(conn.id)
Source code in packages/arcana-core/arcana/models/connection_store.py
upsert
¶
Insert conn, or replace the existing provider connection with the same name.
Always updates updated_at to now on write.
Source code in packages/arcana-core/arcana/models/connection_store.py
delete
¶
Remove the connection with the given name and delete its keyring credential.
Source code in packages/arcana-core/arcana/models/connection_store.py
set_credential
¶
Write a secret to the OS keyring under the given ref key.
delete_credential
¶
Delete a secret from the OS keyring. No-op if the entry does not exist.
Source code in packages/arcana-core/arcana/models/connection_store.py
Adapters¶
arcana.models.adapters.base.ModelAdapter
¶
arcana.models.adapters.base.CompletionRequest
dataclass
¶
CompletionRequest(
system,
messages,
temperature=0.7,
max_tokens=4096,
tools=None,
stream=False,
model_id="",
metadata=None,
)
arcana.models.adapters.base.CompletionResponse
dataclass
¶
CompletionResponse(
content,
input_tokens=0,
output_tokens=0,
tool_calls=None,
stop_reason="end_turn",
)
arcana.models.adapters.base.MessageParam
¶
Bases: TypedDict
A single chat message in the canonical adapter wire format.
arcana.models.adapters.base.ModelChunk
dataclass
¶
A single streaming text delta from the gateway.
input_tokens and output_tokens are non-zero only on the final
chunk (providers differ on when they send usage information).
Embedding adapters¶
Embedding generation is separate from both vector storage and the completion
ModelAdapter above: an EmbeddingAdapter turns text into a dense vector.
Concrete adapters target one provider each.
OllamaEmbeddingAdapter— Tier 1,nomic-embed-text(768d) via Ollama's/api/embed; health probed through/api/tags.FastEmbedEmbeddingAdapter— Tier 2,nomic-embed-text-v1.5(768d) via fastembed's in-process ONNX runtime. Optional dependency (arcana-core[embed]), imported lazily —health_check()reports unhealthy when the package is absent rather than failing at import.
Both report a shared model_family, so a database pinned to one can fall back to
the other when its vectors are interchangeable. See
model pinning.
arcana.models.adapters.embedding.EmbeddingAdapter
¶
Bases: ABC
Converts text into a dense embedding vector. One concrete adapter per provider.
model_name and dimensions are the source of truth for model identity:
callers read them to decide — before any vector is generated — whether the
resolved model is the one a store is locked to. They are properties rather
than fields on the returned vector so that decision needs no embedding call.
model_name
abstractmethod
property
¶
Stable identifier for the embedding model.
Must be identical across process restarts: callers use it as a persistence key to detect when a store's model has changed.
model_family
property
¶
Identifier shared by models that embed into the same vector space.
Two adapters reporting the same model_family produce interchangeable
vectors, so one may serve a database the other pinned. Defaults to
model_name — each model is its own family unless an adapter
explicitly declares membership in a shared one.
embed
abstractmethod
async
¶
Embed a single string into a dense vector of length dimensions.
Raises EmbeddingError (or a subclass) on backend failure — never
returns an empty or wrong-length vector silently. Callers that want
graceful degradation gate on health_check first.
Source code in packages/arcana-core/arcana/models/adapters/embedding.py
embed_batch
async
¶
Embed many strings, order- and length-preserving (result[i] ↔ texts[i]).
Default implementation calls embed serially. Override for providers
with a native batch endpoint.
Source code in packages/arcana-core/arcana/models/adapters/embedding.py
health_check
abstractmethod
async
¶
Probe whether the provider is reachable and usable.
Must not raise — return AdapterHealth(adapter_id=..., healthy=False,
message=...) on failure, so callers can probe several adapters without
exception handling.
Source code in packages/arcana-core/arcana/models/adapters/embedding.py
ensure_model
async
¶
Download or warm up the model on first use; no-op if already present.
Default is a no-op for providers that need no local model.
arcana.models.adapters.embedding.EmbeddingError
¶
Bases: Exception
Base for embedding-backend failures.
Concrete adapters raise this (or a subclass) when generation fails, so
callers can except EmbeddingError without knowing the provider.
arcana.models.adapters.ollama_embedding.OllamaEmbeddingAdapter
¶
OllamaEmbeddingAdapter(
model=_DEFAULT_MODEL,
dimensions=_DEFAULT_DIMENSIONS,
endpoint=_DEFAULT_ENDPOINT,
timeout=60.0,
)
Bases: EmbeddingAdapter
Generates embeddings from a local Ollama instance.
Defaults to nomic-embed-text (768-dimensional). Talks to Ollama's
/api/embed endpoint, which embeds a single string or a batch in one
request; embed_batch uses that batch form directly.
Source code in packages/arcana-core/arcana/models/adapters/ollama_embedding.py
arcana.models.adapters.fastembed_embedding.FastEmbedEmbeddingAdapter
¶
FastEmbedEmbeddingAdapter(
model_name=_DEFAULT_MODEL_NAME,
repo_id=_DEFAULT_REPO_ID,
dimensions=_DEFAULT_DIMENSIONS,
cache_dir=None,
)
Bases: EmbeddingAdapter
In-process embeddings via fastembed's ONNX runtime.
Defaults to nomic-embed-text-v1.5 (768-dimensional), the same model
family as the Ollama tier, so vectors are dimension-compatible. The ONNX
model (~130 MB) downloads to ~/.arcana/models/ on first use.
Source code in packages/arcana-core/arcana/models/adapters/fastembed_embedding.py
Pricing¶
arcana.models.pricing.CostEvent
dataclass
¶
CostEvent(
model,
usage,
estimated=False,
priced=True,
timestamp=(lambda: now(UTC))(),
metadata=None,
)
Emitted by the gateway after every completed call. Not persisted — sink aggregates it.
metadata is copied verbatim from the originating CompletionRequest. The gateway
never reads or validates it; callers use it to attribute cost to a session or agent.
arcana.models.pricing.PricingTable
¶
Token cost lookup by provider/model_id. Local (Ollama) models are always $0.
Source code in packages/arcana-core/arcana/models/pricing.py
cost
¶
Return the cost in USD, or None if the model has no known price.
Lookup priority:
1. Per-connection cost_per_1k_* overrides (if both are set).
2. Global pricing table entry.
3. 0.0 for local/Ollama models (known to be free).
4. None — price is unknown; callers should flag the event as unpriced.
Source code in packages/arcana-core/arcana/models/pricing.py
Errors¶
arcana.models.errors.ModelError
¶
Bases: Exception
Base class for all model errors.
arcana.models.errors.ModelTransientError
¶
Bases: ModelError
Retryable error: timeout, connection reset, 429, 500/502/503.
Source code in packages/arcana-core/arcana/models/errors.py
arcana.models.errors.ModelUnavailableError
¶
Bases: ModelTransientError
Connection refused — server not running or still cold (Ollama, local endpoints).
Source code in packages/arcana-core/arcana/models/errors.py
arcana.models.errors.ModelAuthError
¶
Bases: ModelError
Fatal: 401 / 403. Retrying is pointless until credentials change.
arcana.models.errors.ModelBadRequestError
¶
Bases: ModelError
Fatal: 400, malformed request, context-length exceeded.
arcana.models.errors.ModelNotFoundError
¶
Bases: ModelError
Fatal: model not pulled or unknown model ID.
The message should tell the user how to fix it (e.g. ollama pull <model>).
arcana.models.errors.ModelNotConfiguredError
¶
Bases: ModelError
No effective model_id: the reference omitted it and no default_model is set on the connection.