Language providers
In this section
Every engine feature that reads or writes with a language model — classification, summarization, entity and knowledge extraction, judging, imagining, composing, rewriting — goes through ONE seam, and that seam calls no model unless you configured one. Brainy calls no model unless you configured one. That is true whether "you" means a Node process, brainy serve, or an enterprise's own outbound policy.
You bring your own model — your own local server, or any of three hosted vendors — or you use Soulcraft's hosted anima. Your choice, in every environment: a local machine, your own server, an enterprise.
Zero config, six rows
Nothing is required. A feature that needs no language model works with nothing set. A feature that DOES need one, and finds none configured, refuses by name with the exact line to add.
Provider | The one thing you set | What you get |
|---|---|---|
none | nothing | every language-needing feature refuses by name, naming its own cure |
anima |
| Soulcraft's hosted |
Anthropic |
|
|
OpenAI |
|
|
Gemini |
|
|
a local model you run yourself | nothing — or | the first of Ollama ( |
Precedence, when more than one signal is present: an explicit config (the Node API's language: field, or brainy serve's own resolution) always wins; failing that, BRAINY_LANGUAGE_PROVIDER=<name> says which vendor; failing that, exactly one vendor key present in the environment; failing that, the local auto-detect; failing all four, none. Two vendor keys present with no explicit choice is a refusal, never a silent pick:
[brainy] language: two language providers configured: anthropic-messages and
openai-compatible — set BRAINY_LANGUAGE_PROVIDER to say which one. Never
picked silently.The cure for the base case — nothing configured at all — names every door at once:
[brainy] no language provider served this job (not-configured, provider
'none', job 'classify'): this brain was opened with no language provider.
Cure: no language provider configured — set ANTHROPIC_API_KEY, OPENAI_API_KEY,
GEMINI_API_KEY, BRAINY_ANIMA_KEY, or run a local model on localhost:11434
(Ollama), :1234 (LM Studio) or :8000 (vLLM); or construct the brain with a
language provider the host names; or stop asking for language work on this
host.Your own local model — open weights, your machine
"Your own LLM" includes an open-source or open-weights model you run yourself. Today, that means any local server that speaks the openai-compatible shape: Ollama, vLLM, llama.cpp's own server, or LM Studio. Point brainy at it, or let it find one for you:
// Nothing set at all: brainy probes 127.0.0.1:11434 (Ollama), then :1234
// (LM Studio), then :8000 (vLLM), and uses the first that answers — narrating
// once, to stderr, so you see what it picked:
// language provider: local Ollama at http://127.0.0.1:11434/v1, model llama3.1
const brain = new Brainy({ storage: { type: 'filesystem', path: './brain' } })TS# Or name it explicitly — any private address, no key:
export BRAINY_LANGUAGE_ENDPOINT=http://127.0.0.1:11434/v1BASHOnly 127.0.0.1 and ::1 are ever probed automatically — never a network scan, never a default outside call. A remote private address (your own server on your LAN) is always explicit, via BRAINY_LANGUAGE_ENDPOINT or the Node API's language: field.
Roadmap: a brainy-native local runner for your own weights — no external server process at all — is on the roadmap, as part of the model runner's LLM lane. No date yet; the openai-compatible shape against your own local server is the way to do this today.
Anima — Soulcraft's hosted provider
anima is a hosted API on Soulcraft's servers, reached with a Soulcraft key — exactly like using OpenAI, Anthropic or Gemini's hosted APIs. No Soulcraft weights are ever shipped to or run on your machine; the model runs on Soulcraft's own hardware, and your key is what authorizes the call, the same way any other vendor's key does.
export BRAINY_ANIMA_KEY=sc_live_your_key_here
# or, preferred, a key file:
# ~/.config/soulcraft/anima.key (mode 0600)BASHTwo model names: anima (the composed table — Anima's own voice; a leading system message is prepended to it, never replaces it) and anima-fleet (the raw fleet model, no composition — the default, and the right choice for judging, classification and synthesis).
The public door is not live yet (thread BRAINY-STANDALONE-PARITY): until GET https://anima.soulcraft.com/v1/health answers, every job on this provider refuses by name:
anima provider: contract published, door not yet live (GET
https://anima.soulcraft.com/v1/health answered 404)The cure: use a different provider until anima's door is live, or point baseUrl at an endpoint that already serves it.
Configuring it — three environments
1. The Node API
import { Brainy } from '@soulcraft/brainy'
// OpenAI-compatible (any vendor speaking that shape — OpenAI, Together,
// Groq, or your own local server):
const brain = new Brainy({
storage: { type: 'filesystem', path: './brain' },
language: {
provider: 'openai-compatible',
baseUrl: 'https://api.openai.com/v1',
bearer: process.env.OPENAI_API_KEY,
model: 'gpt-4.1',
contextWindow: 128_000,
},
})
// Anthropic:
const brain2 = new Brainy({
language: {
provider: 'anthropic-messages',
apiKeyFile: '/run/secrets/anthropic-api-key', // a key FILE is preferred
model: 'claude-sonnet-5',
contextWindow: 200_000,
},
})
// Gemini:
const brain3 = new Brainy({
language: {
provider: 'gemini',
apiKey: process.env.GEMINI_API_KEY, // a literal key is fine on a local machine
model: 'gemini-2.5-flash',
contextWindow: 1_048_576,
},
})
// Anima:
const brain4 = new Brainy({
language: {
provider: 'anima',
apiKeyFile: '~/.config/soulcraft/anima.key',
model: 'anima-fleet',
},
})
// Nothing named at all — resolved from the environment at first use:
const brain5 = new Brainy({ storage: { type: 'filesystem', path: './brain' } })TSA config with an unreadable apiKeyFile refuses at construction:
[brainy] language: 'anthropic-messages' could not read apiKeyFile
'/run/secrets/anthropic-api-key' (ENOENT: no such file or directory, open
'/run/secrets/anthropic-api-key').2. brainy serve
brainy serve reads the SAME environment variables, in the SAME precedence, as a bare new Brainy() — an operator configures a provider once and it means the same thing whether a brain is opened by the Node package or by the native server. There is no --language flag yet; set the environment before starting the process:
export ANTHROPIC_API_KEY=sk-ant-your-key-here
brainy serve --brains-dir ./brains --listen 0.0.0.0:8443
# [brainy serve] language provider: anthropic-messages (anthropic-messages)BASHA misconfiguration — two vendor keys with no explicit choice, or an endpoint the outbound gate refuses — stops the server before it binds anything, in the same voice a malformed --brain argument already does:
[brainy serve] REFUSED TO START — two language providers configured:
anthropic-messages and gemini — set BRAINY_LANGUAGE_PROVIDER to say which
one. Never picked silently.Per-brain override
A tenant that brings its own vendor overrides the process-wide default with its own language: config on the Node API (each Brainy instance holds its own seam — two brains in one process may legitimately run two different providers):
const tenantBrain = new Brainy({
storage: { type: 'filesystem', path: `./brains/${tenantId}` },
language: tenantLanguageConfig, // this tenant's own vendor and key
})TSbrainy serve resolves one provider per process today; per-brain override there is the same shape once --brain-scoped configuration lands.
3. Enterprise: the outbound gate
An operator that must guarantee no outbound network call ever leaves the building sets:
export BRAINY_LANGUAGE_OUTBOUND=off
# Only a private/local endpoint is now permitted — 127.0.0.0/8, ::1,
# 10.0.0.0/8, 172.16.0.0/12, 192.168/16, or 'localhost':
export BRAINY_LANGUAGE_ENDPOINT=http://10.0.4.12:8000/v1 # your own vLLM boxBASHA config or an auto-detected provider whose endpoint is NOT private is refused, by name, before anything is contacted:
[brainy] language: BRAINY_LANGUAGE_OUTBOUND=off refuses
'https://api.anthropic.com' — only a local endpoint on a private address
(127.0.0.0/8, ::1, 10.0.0.0/8, 172.16.0.0/12, 192.168/16, or 'localhost')
is permitted. Point language at a local model server, or unset
BRAINY_LANGUAGE_OUTBOUND.A more general allowlist, independent of the outbound-off rule (or alongside it), names exactly which origins are permitted:
export BRAINY_LANGUAGE_ALLOWED_ENDPOINTS=https://api.anthropic.com,http://10.0.4.12:8000BASH[brainy] language: 'https://api.openai.com' is not in
BRAINY_LANGUAGE_ALLOWED_ENDPOINTS (https://api.anthropic.com,
http://10.0.4.12:8000) — add it to the allowlist, or point language at an
endpoint already on it.Both gates apply to EVERY provider shape — an explicit config and an auto-detected one are held to the same rule, and brainy serve and the Node API enforce them identically.
The refusals
Every provider shape raises the same five named refusals, whichever vendor answered:
Refusal | When |
|---|---|
| no provider configured, or the configured one is unreachable, unauthorized, or would not identify its model |
| the work is projected not to fit |
| the provider does not serve this job kind, or the input does not fit the model's context window |
| the answer did not parse to the job's typed shape — never coerced |
| grounding was required and no citation survived verification |
A vendor-specific failure still arrives as one of these five, with the vendor's own status and message kept in the refusal's detail — a 401 from Anthropic and a 401 from Gemini both become LanguageProviderUnavailableError with reason: 'unauthorized', so code that handles one handles all four providers.
The sovereignty rule
No language model ships inside brainy, and brainy never reaches an outside host by itself. Small encoders — the embedder, the cross-encoder — stay inside as reflexes, because they are bounded, deterministic and local. Generative work is not: it is somebody's model, on somebody's hardware, under somebody's terms. So a provider is always CONFIGURED — by you, by your environment, or refused by name. Nothing is guessed, nothing degrades silently, and nothing about a call leaves your process except to the endpoint you named.