pub struct AiConfig {
pub provider: String,
pub endpoint: String,
pub model: String,
pub api_key: String,
pub system: String,
pub max_tokens: u32,
pub temperature: f32,
pub thinking: Option<bool>,
pub reasoning_format: String,
pub access: String,
pub timeout_secs: u64,
}Expand description
An AI chat assistant: which service answers, and what to say to it.
Off by default (provider = "none"). Turning it on connects one client and
mounts <base>/ai/chat, which takes a list of messages and streams the
reply back token by token — and gives every function a chat call over the
same provider.
The three providers differ only in wire format. custom is the one that
matters most in practice: anything speaking the OpenAI chat-completions
shape — llama.cpp, vLLM, Ollama, LM Studio, a gateway of your own — is
reached by pointing endpoint at it, with no key at all
if it wants none.
Fields§
§provider: Stringnone (default), openai, anthropic or custom.
endpoint: StringWhere to send the request.
Empty uses the provider’s own API (https://api.openai.com,
https://api.anthropic.com) and is required for custom. A bare origin
or a base path (http://localhost:8080, .../v1) gets the provider’s
standard path appended; a URL that already names the full path
(…/v1/chat/completions, …/v1/messages) is used exactly as written,
for a gateway that mounts it somewhere of its own.
model: StringModel to ask for when a request doesn’t name one, e.g. gpt-4o-mini.
Some local servers serve a single model and ignore this.
api_key: StringThe provider’s API key. Optional: a local model behind
provider = "custom" usually wants no credential, and sending an empty
one is different from sending none — so an empty key means the request
carries no authorization header at all.
system: StringPrepended to every conversation as the system prompt, unless the request carries its own. Empty = none.
max_tokens: u32Cap on the tokens generated per reply. Anthropic requires one, so this is sent to every provider rather than being special-cased.
temperature: f32Sampling temperature sent when a request doesn’t name one. Negative (the default) sends nothing and lets the provider choose.
thinking: Option<bool>Whether to ask the provider to think, using its own switch for it.
This is the only reasoning switch there is. Whatever thinking comes back
is surfaced: reasoning stream events, kept on the stored message, and
revealed by the Show reasoning toggle. A reply with no thinking in it
has no toggle. Paying a model to think and then throwing the trace away
was never worth a configuration key of its own.
None (the default) sends nothing and leaves the model on whatever its
template does. Some(false) turns thinking off, Some(true) turns it
on. Worth setting: thinking is billed against max_tokens like any
other output, so a thinking model on a small budget can spend the whole
thing reasoning and answer with nothing at all.
How it is sent depends on the provider: Anthropic has a thinking
parameter, and OpenAI-compatible local servers (llama.cpp, vLLM, SGLang,
Ollama) take chat_template_kwargs.enable_thinking, which is what the
Qwen-family templates read. OpenAI’s own reasoning models expose only
reasoning_effort and cannot be switched off, so this is not sent to
them.
reasoning_format: StringHow the provider hands back the model’s thinking, when it is not already in a field of its own.
A reasoning model emits its thinking in one of three shapes, and which one you get is decided by the server’s template and flags, not by the model:
| value | meaning |
|---|---|
auto (default) | native fields if present, otherwise read the tags out of the text — including a template that opened the block for the model, so the answer arrives with a closing tag and no opening one |
native | the server always fills reasoning_content (llama.cpp --reasoning-format deepseek, vLLM --reasoning-parser); text is never scanned |
tags | thinking arrives inline as a matched <think>…</think> pair in the content |
implicit | the chat template pre-opens the block, so every reply starts inside the thinking and the first </think> ends it (Qwen3 and DeepSeek-R1 on a server with no reasoning parser) |
auto is right almost always. The one case it cannot settle on its own
is a streamed pre-opened block: while the tokens are arriving there is
nothing yet to say whether they are thinking or an answer, so auto
treats them as thinking only when thinking is true
— the app having said the model will think. Set implicit when the
template thinks by default and you are leaving thinking unset.
access: StringWho may call <base>/ai/chat, in the grammar a resource’s
[permissions] uses: public, authenticated (the default), member,
role:<name>.
Defaulting to authenticated is deliberate. The endpoint spends money
(or a GPU) on behalf of whoever calls it, and a public one is an open
proxy to your provider account — which is a decision an app should have
to write down.
timeout_secs: u64How long one completion may take before it is abandoned. Generous by default: a long answer from a local model is slow, not broken.