Skip to content

AI Advisor Providers & Models

The advisor is LLM-agnostic at the domain level. This page covers which providers exist, how credentials reach them, how the fallback chain behaves, and how per-run limits are derived.


🗝️ Providers, the Vault, and Credentials

Section titled “🗝️ Providers, the Vault, and Credentials”

The closed provider set is openai, anthropic, google, opencode, ollama, and ollama-cloud (AI_PROVIDER_IDS). Each one is also registered in the vault provider registry (GetAvailableProvidersUseCase) with category: { kind: 'ai-model' }, so a provider cannot exist in a model chain and be absent from the registry.

flowchart TD
    A["Chain entry from ai_advisor_model_chain"] --> B{"providerId is ollama?"}
    B -- yes --> C["Kept. No vault lookup.<br/>createOllama with OLLAMA_BASE_URL"]
    B -- no --> D{"Vault unlocked?"}
    D -- no --> E["Entry dropped, never contacted"]
    D -- yes --> F{"Credential stored and decrypts<br/>to a non-empty apiKey?"}
    F -- no --> E
    F -- yes --> G["Kept with apiKey.<br/>Router id provider/model"]
    C --> H["Resolved chain"]
    G --> H
    E --> I{"Resolved chain empty?"}
    H --> I
    I -- yes and vault locked --> J["failed: VAULT_LOCKED"]
    I -- yes and vault unlocked --> K["failed: NO_MODEL_AVAILABLE"]
    I -- no --> L["Run continues"]
Provider id Where the request goes Vault key Classified as
openai, anthropic, google, opencode The provider’s hosted API through Mastra’s model router (<provider>/<model>). Yes (apiKey). metered
ollama (model id not cloud-suffixed) The local Ollama daemon (default http://localhost:11434/api). None; the registry entry has no fields. local
ollama (model id ending :cloud or -cloud) The local daemon, which proxies to Ollama Cloud. None (the daemon holds the account auth). metered
ollama-cloud Ollama’s direct cloud API through the router as ollama-cloud/<model>. Yes (apiKey). metered

Credentials are stored and scoped like every other vault secret: AES-256-GCM, unlocked with the vault master password (see Local Encrypted Vault). They are entered in Settings → Vault, grouped under an “AI Providers” heading (vault.category.ai-model), through the existing credentials route (POST /api/credentials/vault/:service, where :service is the provider id). Behaviour that follows from the code:

  • A provider without a usable key is skipped at request time and never contacted. Mastra’s environment-variable auto-detection is never relied on.
  • A locked vault skips every keyed provider. Because the master key lives only in process memory, every backend restart re-locks it. The adjacent change vault-remember-unlock-on-device (proposal only, not implemented) targets this.
  • GET /api/advisor/config reports credential state per provider: present, absent, or locked. ollama is always present.

A Cloud-Suffixed Model Through the Local Daemon Still Leaves the Machine

Section titled “A Cloud-Suffixed Model Through the Local Daemon Still Leaves the Machine”

A signed-in local Ollama daemon accepts model ids ending :cloud (for example qwen3:cloud) and the older -cloud aliases (for example gpt-oss:120b-cloud) and proxies them to ollama.com. The daemon holds that authentication, so no key exists in the vault.

[!CAUTION] Such a request sends the conversation and tool results off the machine and is therefore classified metered, not local. isOllamaCloudModelId is the single rule; a genuinely local model whose name happens to end in -cloud is classified cloud too — a deliberate false positive that errs toward disclosure. The classification is derived and cannot be toggled by the user.

Ollama’s published free tier, at the time of writing, consists of monthly starter credits, starter models only, and one concurrent request. This comes from Ollama’s pricing page and is not enforced or verified by this repository; check Ollama’s current terms.


The user orders a model chain in Settings; it is stored as the ai_advisor_model_chain user setting (JSON). Entry shapes are content-discriminated:

Entry Shape Rule
Local { providerId: 'ollama', modelId, contextWindow } Model id must not be cloud-suffixed; contextWindow (tokens, positive integer) is required.
Metered { providerId, modelId } contextWindow is forbidden. A plain ollama entry without contextWindow is rejected.
flowchart LR
    R["Request"] --> E1["Entry 1"]
    E1 -- "answers" --> Done["Stream continues"]
    E1 -- "fails after retries" --> E2["Entry 2"]
    E2 -- "answers" --> Done
    E2 -- "fails after retries" --> EN["Entry N"]
    EN -- "fails" --> F["failed: ALL_PROVIDERS_FAILED<br/>cause of the last failure"]
  • The chain becomes Mastra’s dynamic fallback array (buildModelChainConfig). Retries per entry: 1 for local daemon entries, 2 for everything else (cloud-suffixed ollama and router entries).
  • Local entries also pass their contextWindow to the daemon as options.num_ctx.
  • A stored chain that cannot be parsed or no longer validates is treated as unset (NO_MODEL_AVAILABLE), so a corrupt value never produces a 500. A stored -cloud entry that still carries a contextWindow from before cloud suffixes were recognised has the window discarded on read (dropMeaninglessCloudContextWindow).
  • Failure codes: NO_MODEL_AVAILABLE (unset, or every entry filtered out with the vault unlocked), VAULT_LOCKED (every entry filtered out and the vault was locked), ALL_PROVIDERS_FAILED (entries were tried and the last error is classified as cause), INTERNAL_ERROR (unexpected server-side error).
  • Mastra surfaces only the final failure once the chain is exhausted, so cause.providerId and cause.modelId name the last entry that failed, not necessarily the first.

classifyProviderError inspects the error’s cause chain (depth 5), status first and error code or class second:

cause.kind Classified from
auth-rejected HTTP 401 or 403
model-not-found HTTP 404, or provider error code model_not_found
rate-limited HTTP 429
provider-unavailable HTTP 5xx
network No HTTP answer and a connection error (ECONNREFUSED, ECONNRESET, ENOTFOUND, EAI_AGAIN, ETIMEDOUT, EHOSTUNREACH, ENETUNREACH, undici connect/socket codes, a TimeoutError, or the AI SDK’s failed-fetch wrapper)
unknown Anything else

The wire carries only this classification, never the provider’s error text, URL, headers, key, or request body. The server logs one warning per failed run with provider, model, kind, HTTP status, and a sanitised message (known keys, URLs, and long opaque tokens removed, truncated to 200 characters).


Every run executes under one execution profile that sets step limits, history depth, page sizes, and tool-result budgets. The profile is derived from the resolved chain, never user-toggled.

Resolved chain Limits applied Recorded execution_profile
Every entry is a local ollama entry local local
Every entry is metered metered metered
Both kinds present metered mixed

A mixed chain runs under the metered limits because any cloud entry may be the one that answers; an all-local chain is required to get the full local headroom. Filtering happens first, so a chain that mixes local and keyed entries becomes all-local when the vault is locked.

Limits are user-editable (Settings → AI advisor) and stored as ai_advisor_execution_profiles. When the key is unset, defaultExecutionProfiles() applies.

Limit Metered default Local default Hard ceiling
maxSteps 5 15 30
lastMessages 20 50 200
topNHoldings 15 50 none
lotsPageSize 20 100 200
rowsPageSize 25 100 200

The maxSteps and lastMessages ceilings are runaway-loop protection, not cost control. The page-size ceilings bound a single page. Both PUT validation and the Settings form enforce them.

A tool result is measured as the length of its JSON encoding in characters, never tokens, so the check is deterministic and needs no per-provider tokenizer. An over-budget result is not partially delivered: the model receives { "kind": "truncated", "maxChars": ..., "actualChars": ... } and is instructed to say so.

Metered: a per-tool table (user-editable, no run-wide cap).

Tool Default budget (chars)
portfolio_summary 4000
fiscal_integrity 6000
token_history 6000
asset_allocation 3000
risk_metrics 1500
drawdown_curve 4000
performance_history 4000
kpis 3000
volatility_heatmap 4000
spanish_tax_report 5000
live_prices 2000
fiscal_integrity_rows 4000
token_lots 4000
holding_detail 3000
account_holdings 5000
tax_year_comparison 4000
data_gaps 4000
explain_metric 2000
scenario_position_value 1500
breakeven_price 1500
derivatives_pnl 4000
custody_locations 4000
scenario_portfolio_shock 4000
concentration_risk 2000
tx_search 5000

A stored ai_advisor_execution_profiles row written before a tool existed still loads: on read, the stored toolBudgets are laid over the defaults key by key, so a new tool takes its default budget. Only toolBudgets is merged; every other stored field stays required, and PUT is still strict and demands all 25 keys. A tool whose budget is too small for its payload returns truncated; raise its budget in Settings.

Tool exposure per profile: the local profile exposes only the 14 core tools, while metered and mixed expose all 25. This is static code (ADVISOR_TOOL_TIERS), not a setting. See Tiers per execution profile.

Local: budgets are never stored; they derive from the declared contextWindow at 4 characters per token.

per-tool budget = floor(contextWindow x 4 x 0.15) characters, for every tool
run-wide cap = floor(contextWindow x 4 x 0.60) characters, summed across one run's tool results

For a declared window of 8192: 4915 per tool and 19660 per run. Both formulas live in deriveLocalToolBudget and deriveLocalRunBudget (shared-types), so Settings shows exactly what the backend enforces. The run-wide total counts every attempted tool-result size, including truncated ones, and a fresh RunBudgetTracker is created per run. When an all-local chain declares several windows, the smallest is used.


Name Kind Purpose
ai_advisor_model_chain User setting Ordered model chain (JSON). Unset or invalid means NO_MODEL_AVAILABLE.
ai_advisor_execution_profiles User setting Execution-profile limits (JSON). Code defaults apply when unset.
language User setting Answer language (en or es; default en).
base_currency User setting Base currency for currency-aware tools (default USD).
OLLAMA_BASE_URL Backend env Local Ollama daemon URL (default http://localhost:11434/api).
ADVISOR_DB_PATH Backend env Conversation database path; relative values anchor to the data root; :memory: allowed.
KRYPTOFOLIO_DATA_DIR Backend env Data root that holds ai-advisor.db beside the other databases.
VITE_API_URL Frontend env Backend base URL used by bffClient and the stream adapter (default http://localhost:3001).
Provider API keys Vault Stored per provider id in the encrypted vault; never environment variables.

User settings are stored by IUserSettingsPort in the vault SQLite database. See also Configuration & Env.