Brainy Brainy
Docs Brainy

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

BRAINY_ANIMA_KEY=sc_live_… (env) or the key file ~/.config/soulcraft/anima.key

Soulcraft's hosted anima-fleet model — a hosted API, reached with your Soulcraft key, exactly like using OpenAI, Anthropic or Gemini

Anthropic

ANTHROPIC_API_KEY=…

claude-sonnet-5 via the Messages API

OpenAI

OPENAI_API_KEY=…

gpt-4.1 via the openai-compatible shape, pointed at OpenAI's own endpoint

Gemini

GEMINI_API_KEY=…

gemini-2.5-flash via generateContent

a local model you run yourself

nothing — or BRAINY_LANGUAGE_ENDPOINT=http://host:port/v1 for a private address

the first of Ollama (:11434), LM Studio (:1234) or vLLM (:8000) that answers on 127.0.0.1/::1, its first listed model

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/v1BASH

Only 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)BASH

Two 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' } })TS

A 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)BASH

A 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
})TS

brainy 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 boxBASH

A 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

LanguageProviderUnavailableError

no provider configured, or the configured one is unreachable, unauthorized, or would not identify its model

LanguageBudgetExceededError

the work is projected not to fit budgetMs (refused before the call), or overran it

LanguageJobUnsupportedError

the provider does not serve this job kind, or the input does not fit the model's context window

LanguageOutputInvalidError

the answer did not parse to the job's typed shape — never coerced

LanguageOutputUngroundedError

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.