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.

  1. 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.

  2. 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.

  3. 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.

  4. Google Gemini

    Chosen when GEMINI_API_KEY is set. GEMINI_SUMMARY_MODEL defaults to gemini-1.5-flash on the v1beta generateContent endpoint.

  5. 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.

  6. 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.

LLM_PROVIDER_MODE=auto resolves top to bottom and stops at the first configured provider. A provider exception at call time falls through to step six rather than raising.

Variables and defaults, in full

Every value below is the shipped default from backend/.env.example. Nothing here is aspirational configuration.

ProviderConnectionModel 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.

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.

How vectors, lexical hits and graph candidates are fused

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.

Multimodal is text-first
The multimodal path summarises caption, OCR text, transcript and notes, and fingerprints the binary. It does not perform native image or video understanding, whichever provider you configure. If you want a picture described, run OCR or a vision model yourself and send the text.
A failure is a fallback, not an error
When a configured provider raises, the exception is logged and the deterministic path runs. The caller gets a 200 and a record marked 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 embeddings are configurable, not default
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.