It works with no API keys at all
Most memory products stop being a product when the provider key is missing or the vendor has an outage. This one degrades to a deterministic path and keeps writing, retrieving and answering.
- 6 provider modes
- Zero-key path for every feature
- BYOK per tenant and user
- Local embeddings by default
What still works with an empty .env
The claim above is only worth making if it survives a column-by-column check, so here is the check. Left is what happens with a provider configured; right is what happens with none.
| Capability | With a provider | With no key at all |
|---|---|---|
| Summarising a session or document | Provider chat completion at temperature 0. | Extractive summary — first distinct sentences up to seven sentences or 1,800 characters, model extractive-summary-v1. |
| Voice memo | Transcription through the OpenAI-style audio endpoint. | Your transcript_hint if you supplied one; otherwise a SHA-256 audio fingerprint plus filename and byte count, kept for later reprocessing. |
| Multimodal item | Text-first summary over the caption, OCR text and transcript you send. | SHA-256 fingerprints of each media field, stored with the metadata so the item is findable and re-processable. |
| Retrieval embeddings | Same local model — provider keys do not change this. | all-MiniLM-L6-v2 running in-process, 384 dimensions, stored in pgvector. |
| Lexical search | Postgres tsvector with a GIN index. | Identical. No model is involved at any point. |
| Query planning, temporal intent, reranking | Optional cross-encoder reranking if you enable it. | Regex and heuristic code by default, which is also the default when keys are present. |
| Fabric, facts, sessions, webhooks, audit, RLS | No model involved. | No model involved. These layers never call a provider at all. |
The point is not that an extractive summary is as good as a model
summary. It is that a missing key degrades one column of the table
rather than taking the system down — and that the record it writes says
fallback: true, so you can find and reprocess everything
that was captured while the provider was unavailable.
The order, and how to override it
LLM_PROVIDER_MODE accepts
auto, pcnaid, openai,
anthropic, gemini,
openai_compatible and fallback. On
auto — the default — the resolver walks the ladder
opposite and takes the first provider that is actually configured.
Setting the mode to a specific name pins it. Passing a
provider field on an individual call overrides both, so
you can send a cheap summarisation job to a local model and a
careful one to a hosted model in the same deployment.
Selection is per capability, not global: transcription only supports the Pcnaid and OpenAI adapters, so a deployment with an Anthropic key summarises through Anthropic and transcribes through the fingerprint path unless an OpenAI key is also present.
GET /v1/integrations/providers returns exactly this
resolution — which providers are configured, their base URLs and
models, and the auto_provider the resolver would pick
right now. It never returns key material.
-
Pcnaid AI Provider API
Chosen when both PCNAID_AI_PROVIDER_BASE_URL and PCNAID_AI_PROVIDER_API_KEY are set. Model defaults to pcnaid-memory-default; transcription to whisper-1. Usage is metered in 1M-token units.
-
OpenAI
Chosen when OPENAI_API_KEY is set. OPENAI_SUMMARY_MODEL defaults to gpt-4o-mini and OPENAI_TRANSCRIPTION_MODEL to whisper-1, against OPENAI_BASE_URL.
-
Anthropic
Chosen when ANTHROPIC_API_KEY is set. ANTHROPIC_SUMMARY_MODEL defaults to claude-3-5-sonnet-latest, called on the Messages API with anthropic-version 2023-06-01.
-
Google Gemini
Chosen when GEMINI_API_KEY is set. GEMINI_SUMMARY_MODEL defaults to gemini-1.5-flash on the v1beta generateContent endpoint.
-
Any OpenAI-compatible server
Chosen when LLM_BASE_URL is set — Ollama, vLLM, LM Studio, LiteLLM. Defaults to http://localhost:11434/v1 with LLM_MODEL=llama3.1:8b, and LLM_EXTRA_HEADERS is passed through for gateways that need it.
-
Deterministic fallback
Nothing configured, or every configured provider raised. Summaries become extractive, transcription becomes a fingerprint, and the response is stamped fallback: true in its metadata instead of failing.
Variables and defaults, in full
Every value below is the shipped default from
backend/.env.example. Nothing here is aspirational
configuration.
| Provider | Connection | Model defaults |
|---|---|---|
| Pcnaid AI Provider API | PCNAID_AI_PROVIDER_BASE_URL · PCNAID_AI_PROVIDER_API_KEY · PCNAID_AI_METERING_ENABLED | PCNAID_AI_PROVIDER_MODEL = pcnaid-memory-default |
| OpenAI | OPENAI_API_KEY · OPENAI_BASE_URL = https://api.openai.com/v1 | OPENAI_SUMMARY_MODEL = gpt-4o-mini |
| Anthropic | ANTHROPIC_API_KEY · ANTHROPIC_BASE_URL = https://api.anthropic.com | ANTHROPIC_SUMMARY_MODEL = claude-3-5-sonnet-latest |
| Google Gemini | GEMINI_API_KEY · GEMINI_BASE_URL = https://generativelanguage.googleapis.com | GEMINI_SUMMARY_MODEL = gemini-1.5-flash · GEMINI_EMBEDDING_MODEL = text-embedding-004 |
| OpenAI-compatible | LLM_BASE_URL = http://localhost:11434/v1 · LLM_API_KEY = ollama | LLM_MODEL = llama3.1:8b |
| Local embeddings | EMBEDDING_DIM = 384 | EMBEDDING_MODEL = sentence-transformers/all-MiniLM-L6-v2 |
Shared across all adapters:
PROVIDER_TIMEOUT_SECONDS = 120 and
PROVIDER_SUMMARY_MAX_TOKENS = 900. Summarisation runs at
temperature 0 on every provider, because a memory summary that varies
between runs is a memory you cannot reason about.
Bring your own key, per tenant
Environment variables set the deployment default.
POST /v1/integrations/api registers credentials for a
specific tenant and user, so one installation can serve several
customers who each pay their own provider bill.
When FERNET_KEY is configured, the secret is encrypted
before it is written and a SHA-256 digest of it is stored in the
record's metadata so you can confirm which key is registered without
revealing it. When the key is absent — local development — the
record is marked secret_storage: plain_ref_local_dev
rather than pretending to be encrypted.
GET /v1/integrations/api lists integrations by
provider and user. The listing query selects id, tenant, user,
provider, label, active flag, timestamps and metadata — the secret
column is not in the projection. There is no reveal endpoint. If you
lose the key, you register a new one.
secret_ref is the production path: store a vault path,
a Kubernetes secret name or an environment key instead of the
material itself.
{
"provider": "openai",
"label": "acme-production",
"secret": "sk-…"
}
GET /v1/integrations/api
{
"provider": "openai",
"label": "acme-production",
"active": true,
"meta": {
"secret_storage": "encrypted_ref",
"secret_sha256": "a41f…"
}
}
// no secret field. ever.
Embeddings stay local
The default embedding model is
sentence-transformers/all-MiniLM-L6-v2 at 384
dimensions, running inside the API process. Vectors go into pgvector
columns in the same Postgres that holds the memories, so a
similarity search and a tenancy check are one query rather than two
systems that have to agree.
The model is loaded lazily and cached with an LRU of four. Importing sentence-transformers pulls in transformers and torch, which costs seconds; keeping it out of module import time means the CLI, the test suite and the OpenAPI export all start immediately, and the cost is paid once, on the first call that genuinely needs a vector.
Practically: your memories are not sent to an embedding vendor by default, embedding has no per-token cost, and it keeps working when the network does not.
Two endpoints before you commit
POST /v1/integrations/providers/summarize-test runs a
real summarisation through a named provider and returns the provider
it used, the model, the text, token estimates and whether it fell
back. It is the fastest way to prove a key works before you route
traffic through it.
POST /v1/integrations/providers/token-estimate returns
input and output token counts and the estimated cost against
PCNAID_AI_INPUT_PRICE_PER_MILLION and
PCNAID_AI_OUTPUT_PRICE_PER_MILLION, with
billing_unit: per_1M_tokens. Counts are a conservative
word-based approximation used for quota accounting when a provider
returns no usage metadata — Anthropic and Gemini usage figures are
preferred when present.
Engineering notes
Where the words are narrower than they sound.
fallback: true with a lower stored confidence — 0.62
rather than 0.82 for a transcription, for example. That is
deliberate: losing the capture is worse than degrading it. Alert on
the fallback flag if that trade is wrong for you.
GEMINI_EMBEDDING_MODEL = text-embedding-004 exists in
the configuration, but the shipped retrieval path uses the local
sentence-transformers model. Changing embedding models means
re-embedding the corpus; that is a migration, not a toggle.
Give your agents a memory you can audit
Run the whole system on your own hardware under the MIT licence, or ask us about hosted access. Both start from the same place.