AI Advisor Providers & Models
🔌 Providers, Model Chain & Budgets
Section titled “🔌 Providers, Model Chain & Budgets”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/configreports credential state per provider:present,absent, orlocked.ollamais alwayspresent.
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.
isOllamaCloudModelIdis the single rule; a genuinely local model whose name happens to end in-cloudis classified cloud too — a deliberate false positive that errs toward disclosure. The classification is derived and cannot be toggled by the user.
Ollama Cloud Free-Tier Limits
Section titled “Ollama Cloud Free-Tier Limits”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.
🔗 Model Chain and Fallback
Section titled “🔗 Model Chain and Fallback”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-suffixedollamaand router entries). - Local entries also pass their
contextWindowto the daemon asoptions.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-cloudentry that still carries acontextWindowfrom 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 ascause),INTERNAL_ERROR(unexpected server-side error). - Mastra surfaces only the final failure once the chain is exhausted, so
cause.providerIdandcause.modelIdname the last entry that failed, not necessarily the first.
Failure Cause Classification
Section titled “Failure Cause Classification”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).
📏 Execution Profiles and Budgets
Section titled “📏 Execution Profiles and Budgets”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.
Settings and Hard Ceilings
Section titled “Settings and Hard Ceilings”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.
Tool-Result Budgets
Section titled “Tool-Result Budgets”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 toolrun-wide cap = floor(contextWindow x 4 x 0.60) characters, summed across one run's tool resultsFor 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.
⚙️ Configuration Reference
Section titled “⚙️ Configuration Reference”| 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.