Settings struct and the
ConfigManager runtime API — build the rustdoc with
cargo doc -p cognee --no-deps --open to browse every field and setter with
its type. This page groups those fields by subsystem and gives the env-var name
and default for each.
Configuration model
Every behaviour knob is a field on a typed config struct. The environment is a source of defaults for those fields, not the only way to set them: a variable bound to aSettings field is also settable from code (the Rust API, the language
bindings, and the CLI). Env vars are read only at a configuration boundary,
the one place that turns the environment into a config struct. Adapters,
retrievers and pipelines receive resolved values through constructors and
BackendBuildContext, and never read the process environment themselves.
Precedence
Lowest precedence first:- Defaults —
Settings::default()incrates/lib/src/config.rs. - Persisted config file (CLI only) — JSON at
~/.config/cognee-rust/config.json($XDG_CONFIG_HOME/cognee-rust/config.json), managed bycognee-cli config. Seecrates/cli/src/config_store.rs. .envfile — loaded into the process environment bySettings::overlay_from_env()(viacognee_config_core::load_dotenv). The file is found in the working directory or any ancestor. Variables already set in the process environment win over.env.- Process environment — bound by
Settings::overlay_from_env(). - Explicit code —
ConfigManagersetters (below), a binding’s constructor settings object, the binding config APIs, and CLI--flags.
defaults < config.json < .env < env < code.
Parsing notes:
- Booleans accept
true|1|yes|onandfalse|0|no(cognee_utils::parse_env_bool). - Empty env values are treated as unset.
- Numeric vars that fail to parse are ignored.
Two scopes
Component config sections
Settings that belong to one component are declared by that component, not inSettings. A section is a typed struct declared with
cognee_config_core::config_section!. Each
field becomes a setting with:
- a canonical, namespaced key:
"<section>.<field>", e.g.graph.ladybug.max_db_size; - its env vars, in priority order;
- optional plain aliases, typically the Python SDK’s flat name, e.g.
graph_max_db_size; - a secret flag.
config_sections(). Registering
the factory registers its settings. ComponentRegistry::config_schema() collects
the sections of every registered factory, plus any added with
register_config_section (for settings that no factory owns, e.g. a server
module’s).
So the settings a build accepts follow the components it contains:
- a factory compiled out by a disabled feature brings no keys: its env vars are not read, and setting its keys is an unknown-key condition;
- a downstream build that registers its own factory (
register_llm,register_vector,register_storage,register_ontology_resolver,register_graph_extractor,register_relational,register_session_store, …) brings that factory’s keys, settable through the sameConfigManager::set, bindingsetand config files as the built-in ones.
ComponentManager::with_registry adds the registry’s sections to its
ConfigManager. cognee::config::builtin_schema() is the schema of a default
build. A process built on a custom registry must load the environment with the
full schema, so the env vars of the registry’s own sections are read too:
ConfigManager::from_env_with_schema(®istry.config_schema()?);HandleState::from_env_with_registry(registry)in the bindings;Settings::load_from_env_with_schema(&cognee::config::schema_with(…)?), or the CLI’sload_settings_with_schema.
Settings, in its flattened sections map, so
config files and binding settings objects carry them unchanged. A factory reads
its typed section from the build context with ctx.section::<T>(). A section key,
its canonical key and its aliases are all accepted by ConfigManager::set /
get; set and unset also clear any alias spelling of the setting. When a
loaded config carries one setting under both its canonical key and an alias, the
canonical value wins and the alias is dropped with a warning, so a stale flat key
(llm_api_key from an older config file) never overrides what an env var, a CLI
flag or set wrote under llm.api_key. cognee-cli rewrites those flat keys to
canonical ones when it loads a pre-v4 config file.
The TS and Java constructors also accept a section key with its dots flattened to
underscores (llm_debug_requests), so the camelCase keys a TS config.get()
returns can be passed back to new Cognee(...): a section key with a Python alias
reads back under the alias’s camelCase (llm.auto_rate_limit -> autoRateLimit),
one without an alias with its dots as word breaks (llm.debug_requests ->
llmDebugRequests). Two sections claiming the same key, alias or primary env var
is a registration error. So is a section claiming a Settings key.
Unknown keys and strict mode
A key can be unknown because it is misspelled, or because the component that owns it is not compiled into, or registered with, this build. The two look the same, so the policy is a setting:cognee-cli config unset <key> removes a stale entry even in strict mode, so a
config can always be repaired. cognee-cli config set / unset see strict mode
the way every other command does: the saved strict_config with
COGNEE_STRICT_CONFIG overlaid.
A value of the wrong type for a known key is always an error. From set it fails
the call; in a loaded config it is dropped (the setting keeps its default) and the
error is raised at first use, as for an unknown key in strict mode, until the key
is set again. A loaded config may also carry keys under a name serde does not
know: a section key’s flat alias (such as llm_args for llm.args, or
embedding_model for embedding.model) or the in-memory summarization_schema
setting. Those are applied as the setting they name, not treated as unknown.
A setting marked secret (a registry row or a section key) is masked as
***REDACTED*** in every binding’s config.get() output.
Resolving from something other than the process environment
The boundary loaders read through anEnvSource (from the
cognee-config-core
crate), so you can hand them a MapEnv — an in-memory set of variables — instead
of mutating the shared process environment:
Settings::overlay_from(&dyn EnvSource)— same rules asoverlay_from_env()(only set, non-empty variables change a field), but it reads only the source it is given and does not load.env.EnvSourceandMapEnvare re-exported ascognee::config::{EnvSource, MapEnv}.LoggingConfig::from_env_source(&dyn EnvSource)EnvSettingsView::from_env_source(&dyn EnvSource)(observability; andoverlay)TelemetryConfig::from_env_source(&dyn EnvSource)(product analytics)
from_env() / overlay_from_env() entry points are thin boundaries over these.
A few conventional variables are deliberately read straight from the environment
and are not part of any config struct: proxy variables, HOME, the AWS default
credential chain, COGNEE_CONFIG_HOME (it locates the config file itself),
COGNEE_BINDING_SUPPRESS_LOGS (read at module load, before user code runs) and
LOG_FILE_NAME (hands the log file to child processes).
Inside cognee-rs, the rule is enforced by clippy::disallowed_methods in the root
clippy.toml and
the scripts/check_env_boundary.sh
ratchet. Contributors adding a setting should follow the checklist in
upstream docs/configuration.md § Adding a setting.
LLM
Thellm section, registered by the LLM kind and shared by every LLM adapter
(see Component config sections). Each key also
answers to its Python flat name, the alias shown in brackets. The deep reference is
CLI: LLM retries for retries and the cognee-llm
rustdoc for the adapter.
Adapter sections, registered by their factory and present only when it is:
LLM_MAX_COMPLETION_TOKENS is a completion-token budget, and it does double
duty: besides capping the option-less generate call, it is the LLM term in
cognify’s chunk-size auto-calculation
(min(<embedding engine limit>, llm_max_completion_tokens / 2) — Python’s
get_max_chunk_tokens()). Lowering it for a small-context endpoint therefore
shrinks the prompts cognify sends. On adapters that know their model’s documented
output cap (Anthropic, Bedrock) the configured value is clamped to it first, as
Python does via litellm’s model_cost.
Structured-output calls do not apply this ceiling — option-less ones send
the built-in
16384 default. Cognify’s extraction calls send no cap at all
(Python parity).LLM_ARGS is the escape hatch for that last case. It takes a JSON object
whose keys are merged into each chat and structured-output request body — the
port of Python’s llm_config.llm_args and its {**self.llm_args, **kwargs}
merge, so an explicitly-passed option always wins over an LLM_ARGS key of the
same name. Its practical use is giving the extraction path more output headroom:
because those calls send no max_tokens, the provider’s own default applies, and
a low one (Baseten’s is 4096) truncates the structured JSON mid-object. The
adapter detects that case (finish_reason: "length") and re-asks at a raised
budget, but starting from a sufficient budget avoids the wasted first attempt.
max_tokens is folded into
max_completion_tokens for reasoning models, and sampling params those models
reject (temperature, top_p, frequency_penalty, presence_penalty) are
dropped rather than passed through. A malformed value, or valid JSON that is not
an object, is ignored with a warn! instead of aborting startup. And the
vision/transcription path does not see it at all: it bypasses the merge.
A fallback LLM (llm.fallback_provider/_model/_endpoint/_api_key, aliases
llm_fallback_*) is configurable programmatically (no env binding) but not used
yet. MOCK_LLM + cassettes power the offline
benchmark — see performance/mock-benchmark.md.
Supported LLM providers
Several providers are OpenAI-compatible HTTP endpoints, so they route through the same adapter — differing only in base URL and litellm-style model-prefix stripping.LLM_API_KEY is required for every provider except bedrock (matching the
Python SDK; for a local Ollama any non-empty value works). On bedrock an unset
or empty LLM_API_KEY is a supported configuration — it falls through to SigV4
signing rather than erroring (see Bedrock below). The OpenAI-only
request quirks are gated on the api.openai.com host, so they never fire against
another endpoint.
LLM_ENDPOINT always overrides the default when set on the OpenAI-compatible
providers. Audio transcription (Whisper) is wired only for openai and
custom/openai_compatible (which may expose /audio/transcriptions);
ollama/mistral/gemini/bedrock get graceful no-audio.
anthropic uses a native Messages-API adapter (not the OpenAI-compatible
factory): it authenticates with x-api-key, hoists the system prompt into the
top-level system field, and produces structured output via a forced tool_use.
It requires LLM_API_KEY, strips an anthropic/ model prefix, honors LLM_ARGS,
and supports image description (vision) but not audio transcription. The endpoint
defaults to https://api.anthropic.com/v1 and deliberately ignores
LLM_ENDPOINT/OPENAI_URL so a stray OPENAI_URL cannot misroute Anthropic
traffic (matching Python, which passes no base URL). To route Anthropic traffic
through a proxy / gateway, set the dedicated ANTHROPIC_BASE_URL (alias
ANTHROPIC_API_BASE); other providers ignore it.
azure reuses the OpenAI request path with Azure’s auth and URL conventions:
it authenticates with the api-key header and appends an ?api-version=<v>
query. Set LLM_PROVIDER=azure, LLM_API_KEY, LLM_API_VERSION (e.g.
2024-12-01-preview), and LLM_ENDPOINT to the deployment URL
(https://<resource>.openai.azure.com/openai/deployments/<deployment>). Azure
ignores the model in the request body for routing, but LLM_MODEL still selects
the reasoning-model parameter shape, so for an o-series/gpt-5 deployment set
LLM_MODEL to the underlying model name (a .../deployments/o3 style segment is
also treated as reasoning), or pin it with LLM_REASONING.
Bedrock
LLM_PROVIDER=bedrock is a native adapter over Amazon Bedrock’s Converse API
(POST {endpoint}/model/{modelId}/converse), not an OpenAI-compatible endpoint.
Its AWS settings live in the aws config section (registered by the Bedrock LLM
and embedding factories), so they can be set like any other section key: with
set, in a config file or through the bindings, as aws.access_key_id or its
flat alias aws_access_key_id, and so on. A field left unset there falls back to
the AWS_* environment variable of the same name inside the adapter. The region
has no env binding in the section (aws.region, alias aws_region_name); the
adapter applies the env rungs of the region chain itself. The env names mirror the
ones litellm reads, and several of them are not the boto3-standard spellings:
Auth ladder.
LLM_API_KEY, when set, is sent verbatim as
Authorization: Bearer <key> and the request is not signed — no credential
lookup runs, so a bearer-only deployment needs no AWS credentials. Otherwise
AWS_BEARER_TOKEN_BEDROCK takes the same short-circuit. Only when both are absent
does the request fall through to SigV4 (signing service bedrock), whose
credentials resolve in this order:
- web-identity token + role + session name → STS
AssumeRoleWithWebIdentity; - role ARN → STS
AssumeRole— skipped when the process is already running as that role, so an IRSA pod that already holds it does not assume itself; - profile name → the shared-config profile;
- static access key + secret (plus session token when present);
- otherwise the default chain (env / shared config / SSO / ECS / IMDS).
AWS_REGION_NAME → AWS_REGION →
the profile / default chain → the hard default us-west-2. The endpoint
resolves as: an explicit API base → AWS_BEDROCK_RUNTIME_ENDPOINT →
https://bedrock-runtime.{region}.amazonaws.com. LLM_ENDPOINT is not that
API base: like the Anthropic path, the Bedrock factory deliberately passes no base
URL, so a stray LLM_ENDPOINT cannot misroute Bedrock traffic. Use
AWS_BEDROCK_RUNTIME_ENDPOINT for a custom runtime host.
Converse only. Every model cognee ships routes to Converse after the model id
is normalised (an ARN wrapper is unwrapped; cross-region inference prefixes such
as eu./us./apac. and provisioned-throughput / context-window suffixes are
stripped). Normalisation feeds routing and capability decisions only; the request
URL always carries the model id exactly as configured. A model that routes to
Bedrock’s legacy invoke API for chat is unsupported: construction fails
with LlmError::FeatureNotSupported. (Embeddings are a separate path and do go
over InvokeModel — see Embedding.)
Capabilities. No streaming. No audio transcription. Image description
(vision) is implemented over Converse image blocks — deliberately exceeding
Python parity — but gated on a per-model capability table
(caps.rs):
an unlisted model is treated as text-only and returns
LlmError::FeatureNotSupported. Structured output is capability-gated from the
same table: models that advertise native structured output get Converse’s
outputConfig.textFormat carrying the JSON schema, the rest get a synthetic
json_tool_call tool (with toolChoice forced only for models that support it).
Feature gating. The adapter and the embedding engine live behind the
bedrock Cargo feature. It is non-default on the two leaf crates that carry
the code (cognee-llm, cognee-embedding), but it is in the default
feature list of cognee-components, cognee and cognee-cli (and of
cognee-http-server), so every shipped binary has Bedrock available.
Building with --no-default-features (or without bedrock) drops the whole AWS
stack, and LLM_PROVIDER=bedrock then reports an unsupported provider.
Reasoning-model parameters
OpenAI reasoning families (o1*, o3*, o4*, gpt-5*) require a different
request shape — max_completion_tokens instead of max_tokens, and no
temperature/top_p/penalties. The adapter auto-detects this from the model
name for any non-local endpoint (loopback and Ollama’s port keep the legacy
shape). LLM_REASONING overrides that detection:
auto(default) — name/host auto-detection as above.always— force the reasoning shape (e.g. a proxy that hides a reasoning model behind an opaque alias).never— force the legacymax_tokens+temperatureshape (e.g. a gateway serving a reasoning-named model that only accepts the legacy parameters).
Structured-output request shape
Nothing in the OpenAI-compatible protocol advertises which structured-output shape a server understands, so by default the adapter cascades: nativetools,
then legacy functions, then response_format: {"type": "json_object"}. The
first shape that yields a parseable payload wins. The adapter keeps a per-mode
miss counter and stops sending a mode that has produced nothing three calls
running (see How retries and pacing interact).
LLM_STRUCTURED_OUTPUT_MODE skips the discovery when you already know the answer:
auto(default) — cascade as above, bounded by the miss counter.json_schema— try constrained decoding first, then cascade. See below.tools— only native tool-calling.functions— only the legacyfunctions/function_callpair.json— only JSON mode.
json when the server rejects tools/functions outright. If it
merely ignores them — accepting the request and echoing JSON in content —
leave it on auto: tool-calling mode already succeeds there in one request, and
it is the only mode that puts the real JSON schema on the wire (JSON mode sends a
prose example derived from the schema, which means weaker schema pressure).
Pinning a mode means the other modes are never sent, and exhausting the pinned
mode fails the call rather than falling through — the error names the pin. An
unrecognised value falls back to auto. This is the counterpart of Python
cognee’s llm_instructor_mode.
json_schema — constrained decoding. json_schema is not a pin. It sends
gpt-oss-120b) answer
HTTP 501 to a constrained request. The mode demotes rather than failing:
strict: truewith the all-required /additionalProperties: falseschema;- on rejection, the same envelope without
strict, carrying the ordinary schema; - on rejection again, out of the mode entirely: the
tools→functions→jsoncascade runs exactly as underauto.
response_format / json_schema / schema /
strict. Rate limits, other 5xx, network errors, model refusals and unrelated
400s leave the (process-lifetime, monotonic) memo untouched. The probe costs one
request and is single-flighted per schema. A schema whose root is not an object
never enters the mode, and a schema using a feature outside OpenAI’s strict subset
(a map field, patternProperties, if/then/else) is refused and demoted.
The Bedrock adapter does not read this knob; it picks Converse’s native
outputConfig.textFormat.jsonSchema or a synthetic json_tool_call from its own
capability table.
Ollama embeddings: set
EMBEDDING_ENDPOINT explicitly when using
EMBEDDING_PROVIDER=ollama. The Ollama embedder needs the /api/embed route, and
when EMBEDDING_ENDPOINT is unset the embedding endpoint falls back to
LLM_ENDPOINT, which for Ollama points at the /v1 chat base, so the embedder
would POST to the wrong path. Set
EMBEDDING_ENDPOINT=http://localhost:11434/api/embed.Embedding
Theembedding section is registered by the embedding kind and shared by every
provider. Each provider’s factory registers the settings only it reads (see
Component config sections). The Python flat name of
each key is accepted as an alias, shown in brackets.
Provider sections, registered by the provider’s factory and present only when it
is compiled in:
Provider values:
onnx, fastembed, openai, openai_compatible, ollama,
bedrock, mock, plus any provider a custom registry adds with
ComponentRegistry::register_embedding. A provider whose factory is not
registered (a misspelling, or a backend compiled out) is an error, never a silent
ONNX fallback.
MOCK_EMBEDDING diverges from the Python SDK, deliberately. Python’s
embedding engines return [0.0] * dimensions under a truthy MOCK_EMBEDDING.
Rust defaults to SHA-256-derived vectors instead, because zero vectors are
silently discarded by cosine-distance KNN (the distance is NaN, and LanceDB
filters NaN rows out), making retrieval vacuously empty. The mock engine is
test-only. Set MOCK_EMBEDDING=zero to reproduce Python’s behaviour.
EMBEDDING_PROVIDER=bedrock uses Amazon Bedrock’s InvokeModel API
(POST {endpoint}/model/{modelId}/invoke) — Converse is never used here. Two
model families are supported, selected from the normalised model id: Titan
(amazon.titan-embed-text-v1, amazon.titan-embed-text-v2:0,
amazon.titan-embed-image-v1), which takes one text per request, and Cohere
(cohere.embed-*), which batches. Set EMBEDDING_MODEL to the Bedrock model id;
EMBEDDING_DIMENSIONS drives the engine’s reported dimension() and is sent as
dimensions on Titan v2, so it must match what the model returns. AWS
configuration comes from the same AWS_* variables as the LLM path (see
Bedrock), and EMBEDDING_API_KEY (falling back to LLM_API_KEY) is
the Bedrock API key, which short-circuits to a bearer header with no SigV4;
leaving it unset runs the ambient credential ladder. Returned vectors are always
L2-normalised (server-side on Titan v2 via normalize: true, client-side for the
others).
Bedrock embeddings and
EMBEDDING_ENDPOINT: through Settings,
EMBEDDING_ENDPOINT defaults to empty, and a Bedrock embedder inherits
LLM_ENDPOINT only when the LLM provider is also bedrock. Direct users of
EmbeddingConfig::default() start from the OpenAI base URL instead; the Bedrock
engine ignores an endpoint that points at api.openai.com rather than POSTing
Bedrock bodies there. Set it only to a real Bedrock runtime host (or use
AWS_BEDROCK_RUNTIME_ENDPOINT).Vector database
Thevector section is registered by the vector kind. Backend-specific settings
are registered by the backend’s factory, for example
vector.pgvector. Python flat names are accepted as
aliases, shown in brackets.
Supported providers:
This list is the built-in set, not the set of valid values. Any crate can
register an additional provider under any id on a
ComponentRegistry, and a
binary that injects that registry (ComponentManager::with_registry, or
HandleState::from_settings_with_registry in the bindings) accepts the id in
VECTOR_DB_PROVIDER / GRAPH_DATABASE_PROVIDER — including
Postgres-backed providers, which receive the assembled vector_postgres_url /
graph_postgres_url from the *_HOST / *_PORT / *_NAME / *_USERNAME /
*_PASSWORD settings exactly as the built-ins do, whether or not the
pgvector / pggraph features are compiled in. Registering over a built-in id
replaces it. See
External providers.lancedb— embedded Apache-Arrow / Lance vector store, on disk. The default provider, and default-on incognee,cognee-cliand every binding; requires thelancedbCargo feature (the Arrow/Lance native stack is a large build, so a consumer on another vector store can drop it — and must then setvector_db_providerexplicitly). Unavailable on Android, where its native stack does not cross-compile. The on-disk layout matches the Python SDK’s default LanceDB store, so a Rust deployment can be opened from Python and vice versa.brute-force— pure-Rust in-memory linear scan. Default on Android. Selected on any target by settingvector_db_url = ":memory:".pgvector— Postgres + thepgvectorextension; requires thepgvectorCargo feature on the binary build (default-on incognee-cli). See Pluggable Backends for HNSW indexing andcognee-cli vector-reindex.
vector_db_provider to qdrant is rejected at component
initialization in OSS (it returns a config error rather than falling back).
pgvector index tuning
Thevector.pgvector section, registered by the pgvector factory (pgvector
builds only): four optional overrides for the adapter’s HNSW build, applied per
adapter and clamped to the range shown. Keys are vector.pgvector.hnsw_m,
.hnsw_ef_construction, .maintenance_workers and .maintenance_work_mem_mb,
with aliases pgvector_hnsw_m and so on. Leave them unset unless a
measurement says otherwise: the defaults are calibrated against a 100k-point
corpus of 384-d embeddings, and the two that cost server resources exist so
they can be turned down on a small or shared Postgres without patching the
crate.
m/ef_constructionapply to indexes built after the change. One already on disk keeps the parameters it was built with until it is rebuilt —create_missing_vector_indexes()(cognee-cli vector-reindex) only builds a missing or invalid index, never a valid one — so raisingmon an existing store is aDROP INDEX+ reindex, done deliberately.ef_construction’s floor followsmbecause pgvector requiresef_construction >= 2 × m; no combination of these two can produce aCREATE INDEXthat fails.- Asking for more parallel maintenance workers than the server can start is
not an error. Postgres launches as many as the server’s
max_parallel_workers/max_worker_processesstill allow, down to zero (a serial build). The only cost of over-asking is a slower build, so no server-side configuration is required to adopt the defaults. - Surrounding whitespace in an env value is ignored. A blank, negative or non-numeric env value is ignored and the default applies; an out-of-range value (however large) clamps. A typo in a deployment environment should not take the store down.
Graph database
Thegraph section is registered by the graph kind; backend sections such as
graph.ladybug are registered by their factory. Python flat names are accepted as
aliases.
Supported providers:
ladybug/kuzu (embedded), postgres (feature pggraph).
As with the vector store, these are the built-ins; an externally registered
provider id is accepted too (see the note under Vector database).
The default is derived from the graph backends the binary was compiled with,
not hardcoded. In order:
ladybug, when that feature is on — every default build;- otherwise
postgres, whenpggraphis on (e.g. a--no-default-features … pggraphimage), however many other ids are registered; - otherwise the single registered backend, when the build has exactly one.
ladybug nor
postgres has to set GRAPH_DATABASE_PROVIDER explicitly.
When Postgres graph credentials are unset they fall back to the relational DB_*
config (see roadmap/cognify-compatibility-plan.md).
Relational database
Therelational section is registered by the relational kind. Backends are
RelationalDbFactorys on the ComponentRegistry, selected by relational.provider
(when that is empty, by the scheme of the resolved URL). An external crate adds a
backend with register_relational, together with its own config sections; it
implements cognee_database::RelationalDb. The built-in SeaORM factory is
registered as sqlite (feature sqlite) and postgres / postgresql (feature
postgres). An unregistered provider is an error.
For
postgres the URL is built from the connection parts; otherwise relational.url
is used verbatim. db_sslmode and single_process stay cognee-wide Settings keys:
the TLS mode is appended to every Postgres URL cognee builds (relational, pgvector
and the Postgres graph), and single_process gates startup recovery.
Single-process startup recovery
single_process / COGNEE_SINGLE_PROCESS asserts that one process owns this
database. In practice it is an opt-in: startup recovery does nothing until you
turn it on. The setting is tri-state:
null restores the derived answer (as for chunk_size). The env var is parsed
by the same code as the config key; a blank or whitespace-only value means
“derive”, not false. The derivation resolves true only for an in-memory
SQLite URL (sqlite::memory:, ?mode=memory) and false for everything else —
Postgres and file-backed SQLite, including the shipped default
sqlite:./cognee.db?mode=rwc. An in-memory database cannot hold a leftover
from a dead run, so the derived true gives the sweep nothing to do.
Which surfaces read it.
The HTTP server never loads the CLI config file and builds no
Settings, so its
environment is the only channel. The CLI runs no sweep on any command: every
invocation is its own process, and two concurrent cognee-cli cognify runs
against one SQLite file are exactly the case the claim exists to catch.
Set it before the first operation. The setting is read once per relational
database per process, on the first operation that opens that database. Flipping
it on afterwards is ignored for the rest of that process’s life (the process may
have live runs a sweep could not tell apart from a dead predecessor’s); a binding
that notices the flip logs a WARN. Restart to recover a dataset that is already
wedged.
What it turns on. Startup recovery for a run killed mid-flight (SIGKILL, an
OOM kill, Android killing the app). A killed run leaves three things behind,
cleared in this order:
- What it had already written into the graph and vector stores, recorded in
the ownership ledger against its
pipeline_run_id— rolled back through the sameRunSweepertheforgetpath uses, scoped strictly to the dead run in its own dataset (artifacts another run also claims are kept); - The
pipeline_runsrow left atStarted— read first by the run gate and never expiring; - The exclusive-run claim on
(dataset, pipeline)— otherwise expiring 24 h later.
cognify_rollback_handler). A rollback that fails is logged and the status row
is retired anyway, so the dataset never wedges permanently. Retiring an orphan
writes a new Errored row in Rust, where Python resets the run to
INITIATED; both resolve to “you may run” at the gate, but a status poller sees
a failed build on Rust versus a never-started run on Python.
Who needs to set it. Any single-process deployment on a file-backed
database: an Android app, a desktop app, a one-process service, a CLI-only setup.
Without it nothing regresses — the claim still ages out after 24 h and the
orphaned row is still cleared by cognee-cli pipeline-unblock --clear or an
HTTP-server restart — but nothing else removes what a killed run wrote. Set it to
false to pin the safe behaviour in a config file.
Chunking & tokenizer
Read bycrates/chunking/src/config.rs.
chunk_strategy (default PARAGRAPH) and chunk_engine are Settings /
CognifyConfig fields without env bindings. Chunk size and overlap are
env-bindable:
chunk_size unset means “auto-calculate at pipeline time”, mirroring Python’s
chunk_size=None: min(<embedding engine limit>, llm_max_completion_tokens / 2). The engine limit is
EMBEDDING_MAX_COMPLETION_TOKENS (default 8191) for the OpenAI-compatible and
Ollama engines and EMBEDDING_MAX_SEQUENCE_LENGTH (default 512) for the local
ONNX engine. With OpenAI-compatible embeddings at their defaults that is
min(8191, 8192) = 8191 tokens; with the local ONNX/BGE engine it is its
512-token sequence limit. Set it explicitly (env, cognee-cli config set chunk_size, or --chunk-size) to override — an explicit value is always
honoured. cognee-cli config set chunk_size null restores the auto-calculation.
The token counter is chosen by the cognify engine section (registered with the
kind sections), resolved when cognee lowers Settings into a CognifyConfig
(Settings::cognify_config; CognifyConfig::default() itself reads nothing):
COGNEE_LEGACY_PER_WORD_TOKEN_COUNT is a deliberate divergence from Python.
By default a chunk’s size is the tokenizer’s count of the chunk itself. Before
SDK-632 it was the sum of the counts of its words, each tokenized in isolation,
which over-states the span for sub-word tokenizers (measured 1.78x on Alice
in Wonderland: a chunk configured at 8191 tokens held 4,669, and the run made
~1.8x the LLM calls). The default now fills that budget to 8,188 of 8,191.
Python 1.5.x still counts per word, so the Rust default is knowingly not
byte-for-byte with Python’s chunk boundaries; setting
COGNEE_LEGACY_PER_WORD_TOKEN_COUNT=1 restores that parity at the cost of the
over-count. Only 1/true/yes/on enable it. Full measurement record:
performance/chunk-token-overcount.md.
Cognify failure handling
Read bycrates/cognify/src/failure.rs.
When a stage cannot process one chunk or one file it records the failure —
which stage, which file, which chunk, and why — instead of aborting the batch.
The report rides every stage output to CognifyResult.failures (on success) or
to CognifyError::RunFailed (when the run failed). Whether the run fails is
decided once, at the end, from two independent axes.
Each knob is a CognifyConfig builder, and each is also a key of the cognify
engine section, which Settings::cognify_config applies. They live in the
section, not as Settings fields, so GET /api/v1/settings keeps Python’s shape.
RAISE_INCREMENTAL_LOADING_ERRORS is Python’s own flag (default true), so one
.env configures both SDKs. Its polarity is Python’s: true (raise on the first
error) is FailFast, false is RunToEnd. It is a strict boolean:
true/1/yes/on is FailFast, false/0/no/off is RunToEnd, and
any other value is ignored with a warning, so a typo such as ture keeps the
FailFast default rather than silently switching to the costlier run-to-end
mode. Python raises only on a literal true (case-insensitive), so in a shared
.env use true or false: 1/yes/on (or a typo) mean FailFast in Rust
but run-to-end in Python. The other four are Rust-only (COGNEE_COGNIFY_ prefix). An unset value
leaves the default in place, and out-of-range values are ignored rather than
failing the process: a ratio outside (0, 1] or an unknown rollback scope logs a
warning, and a zero cap is dropped silently.
Axis 1 — when to stop. FailFast stops the failing stage from scheduling
further work once a failure is recorded; files that already completed still
travel down the rest of the pipeline. RunToEnd continues past failures and
decides at the end (a full run’s LLM cost for the complete failure list). Each
stage stops at its own loop boundary:
So any dataset under 2000 chunks is one extraction batch, and
FailFast saves
nothing there; lowering chunks_per_batch tightens the boundary at the cost of
serialising the run into waves. In summarization, the first failing chunk closes
the gate: undispatched chunks are skipped (their files recorded as unreached),
while in-flight calls complete and keep their summaries. A tolerated
summarization failure does not close the gate.
Axis 2 — what to sweep. WholeRun removes everything the run created;
FailedItems removes only the failed files’ contributions and keeps the rest —
but only while the run as a whole is survivable (below the failure ratio, with
at least one surviving item). Once a FailedItems run is judged failed it
escalates to a whole-run sweep that deletes the completed files and their
completion markers too. A third variant, Nothing, removes nothing and is not
selectable from configuration (nothing/none are rejected like any unknown
value, from the env var or the cognify section key); it is reachable only via
CognifyConfig::with_rollback_scope(RollbackScope::Nothing), because a run that
wrote graph edges and died before the vector stage leaves orphans a retry never
repairs.
Any combination that sweeps and ends
ERRORED returns Err (matching Python,
which re-raises after rolling back). A FailedItems run that completes below the
ratio returns Ok with the failed-file list in CognifyResult.failures. A sweep
removes an artifact only when nothing outside the swept scope still claims it
(another run, another dataset, a pre-ownership row, or a surviving file in the
same run). Cancellation is swept too — a deliberate divergence from Python —
though there is no caller-facing cancel handle yet, and dropping the cognify()
future runs no sweep.
Per-stage policy.
The failure ratio is counted in chunks, evaluated per run, excludes
summarization failures, and applies only under
FailedItems; if no item
survived, the run failed regardless of the ratio. The report cap bounds the
individual entries listed (and the error message); totals, the failed-file set and
counters are never capped. A run that dies in a persistence stage loses the
report of failures collected earlier (persistence errors keep their own
CognifyError variants).
Completion markers and incremental loading
incremental_loading (default on) is live. After a successful run, every
data item the run finished is marked complete on its Data row, under
pipeline_status["cognify_pipeline"][<dataset id>] = "DATA_ITEM_PROCESSING_COMPLETED" — Python’s format byte for byte, so the two
SDKs can share a database. The next run reads those markers before it builds a
pipeline and skips items already carrying one: they are not classified, chunked,
or sent to an LLM.
- Re-cognifying an already-complete dataset is a no-op. The call returns
Okwithalready_completed = trueand empty payloads. (Previously every cognify re-processed everything.) - A failed run marks nothing, and a sweep clears the markers of every item it rolled back, so the next run redoes exactly the work that was lost.
- A run that completed with failures outstanding is not a pipeline-cache
hit, even with
use_pipeline_cacheon.
pipeline_runs rows (nothing appears under
GET /api/v1/activity/pipeline-runs), whereas Python logs a complete run trail
for a repeat cognify — the two SDKs agree on what a repeat run did (nothing) but
not on whether it is visible as a run.
Temporal runs share the markers. Both branches write and read the one
cognify_pipeline key (as in Python), so a cognify() followed by a temporal
cognify() over the same dataset is a no-op, and vice versa. To build both
graphs over one dataset, or to reprocess everything on every run, set
with_incremental_loading(false).
Search
Thesearch and search.hybrid engine sections tune the standard retrievers;
cognee hands them to SearchBuilder::with_tuning (Settings::search_tuning).
Triplet retriever hub cap
Measured on a synthetic 100k corpus, a cap of 100 keeps a Jaccard similarity of
0.93–0.96 against the uncapped result set, with a worst query at 0.67; a cap of
200 keeps 0.92–0.93 but has a worst query at 0.18. The latency it buys is small
on a uniform corpus and large on real hub-heavy text (one
graph_completion
search went from 332 ms to 76 ms). Treat it as a knob for a corpus with known
hubs, measured on that corpus, not as a default.
PgGraphAdapter applies the cap in SQL; every other backend gets the same
semantics from GraphDBTrait::get_neighborhood_scoped’s client-side default.
Hybrid retriever knobs
TheHYBRID_COMPLETION search type (SearchType::HybridCompletion) accepts a
set of per-request tuning knobs. These are library / orchestrator-only —
typed Option<...> fields on
SearchParams,
populated from the snake_case JSON keys inside
SearchRequest.retriever_specific_config. They have no env binding.
When a knob is unset the retriever falls back through
retriever_specific_config.<knob> → SearchRequest.top_k capped at 10 → the
HybridRetriever::new constructor default (an explicit per-lane knob is never
capped):
Chunk-lane fusion.
chunk_lane_fusion chooses how the DocumentChunk_text
lane and the TextSummary_text lane are combined:
"relative_score"(default) normalises each lane’s similarities to[0, 1]against that lane’s own candidates and adds them, the summary lane weighted bysummary_lane_weight(the chunk lane always weighs1.0)."reciprocal_rank"is Python’srank_chunk_summary_pairsverbatim — RRF over ranks only.
chunk_lane_fusion = "reciprocal_rank" to get the
Python ranking back.
Reserved (Phase 2, inert). These keys are parsed and stored but have no
effect yet: use_truth_weight (default false), include_global_context_index
(default false) and global_context_index_top_k (default 3). The
HTTP-facing degradation notes live with the server’s search-router docs, which
are not part of the cognee-rs repository.
Ontology
Theontology section is registered by the ontology-resolver kind. Resolvers are
factories (OntologyResolverFactory) on the ComponentRegistry, selected by
ontology.resolver; an external crate adds one with register_ontology_resolver.
With no file path the no-op resolver is used, whatever the resolver id.
Only
rdflib is built in; an unregistered resolver id with a file path set is an
error. rdflib implements only fuzzy matching and warns on any other strategy.
The CLI’s --ontology-file overrides ontology.file_path for one run.
File storage
Thestorage section is registered by the storage kind. Backends are factories
(StorageFactory) selected by storage.backend; an external crate adds one (an
object store, say) with register_storage.
Only
local (files under data_root_directory) is built in. An unregistered
backend id is an error. No built-in backend reads storage.bucket_name.
Graph extraction
Thegraph_extraction section is registered by the graph-extraction kind. It
picks the extractor cognify uses to turn chunks into graph facts.
llm is cognify’s built-in LLM extractor. Other kinds (GLiNER, …) are
GraphExtractorFactorys that an external crate registers with
register_graph_extractor, together with their own config sections. A binding’s
per-call graphBackend cognify option (a kind string, or {"kind": …, …tuning})
overrides the default. A kind nothing is registered under is an error; cognify
never falls back to the LLM extractor silently.
Every operation that cognifies honours the setting: cognify and
add-and-cognify in the CLI and the bindings, and the operations that cognify as
a sub-step (remember, improve, and the bindings’ update). Each one resolves
the extractor before any side effect, so an unregistered kind fails before data is
ingested or deleted. Library callers that build a CognifyConfig themselves get
the configured backend from ComponentManager::configured_graph_extractor and
attach it with with_graph_backend.
System paths, users & datasets
Setting
system_root_directory cascades to the default graph_file_path and
vector_db_url unless those are set explicitly.
Session / cache & rate limiting
The session (Q&A history) store is chosen by thesession section, registered by
the session-store kind. Stores are SessionStoreFactorys on the
ComponentRegistry, selected by session.store; an external crate adds one with
register_session_store. A factory is handed the shared relational database.
Built in:
relational, also registered asseaorm: the session tables in the relational database (anyRelationalDb). This is the default.fs(crate featuresession-fs): JSON files undersession.fs.directory.redis(crate featuresession-redis): Python’s Redis layout, atsession.redis.*.
CACHE_HOST … variables are not read, and setting cache_host is an unknown key.
An unregistered store id is an error.
How retries and pacing interact
These knobs form one resilience stack, matching Python cognee’s:- Two retry counts, one per loop.
LLM_MAX_RETRIES(Python’s_MAX_VALIDATION_RETRIES) is the number of attempts at one structured-output call — the first ask plus corrective re-asks, so the default3is at most two re-asks. Every adapter floors it at1.LLM_NETWORK_RETRIES(Python’sstop_after_attempt(2)) is the transport ladder underneath, for 429s, 5xx and connection failures. They are separate because they nest; one shared value multiplied the ladders into each other. - Retrying stops only once BOTH floors are met — at least
LLM_NETWORK_RETRIESattempts and at leastLLM_MIN_RETRY_SECONDSelapsed. The time floor lets a call outlast a provider’s rate-limit window. Backoff is exponential with jitter, 8s doubling to a 128s ceiling, and aRetry-Afterhint of up to 60s is honoured; a longer hint is ignored and the normal backoff applies. SetLLM_MIN_RETRY_SECONDS=0to fail fast on attempts alone. Time spent queued for anLLM_MAX_PARALLEL_REQUESTSslot does not count against the floor. - Bedrock makes one attempt more. Its attempt floor is
LLM_NETWORK_RETRIES + 1(so2buys three attempts there), and the time floor applies to it like everywhere else. Bedrock ignoresRetry-After, so its gaps are always the 8s-to-128s ladder. - Three timeouts, three scopes.
LLM_CONNECT_TIMEOUT_SECONDSbounds the TCP handshake;LLM_REQUEST_TIMEOUT_SECONDSbounds one HTTP request (0means “no limit” for both, matching curl). Both apply to the OpenAI-compatible (including Azure), Anthropic and Bedrock adapters; the Responses client uses a fixed 600s request / 10s connect pair.LLM_REQUEST_DEADLINE_SECONDSbounds one logical structured-extraction call and is the only bound on total time. Keep it aboveLLM_MAX_RETRIES x LLM_MIN_RETRY_SECONDS(720s at the defaults) and above 900s: the deadline includes time spent paced and queued, and a provider 429/503 opens a 900-second pacing episode. The1200default clears both; a warning is logged at startup if the deadline does not fit the computed ladder. The deadline gates starting new work rather than cancelling in flight, so the true ceiling isLLM_REQUEST_DEADLINE_SECONDS + LLM_REQUEST_TIMEOUT_SECONDS; retries’ pacing and slot waits are bounded by the remaining budget, while the first attempt may wait out a whole episode. Set it to0to disable. - The cascade stops sending a mode that never produces anything. On an
endpoint without a server-side tool parser (e.g. vLLM started without
--enable-auto-tool-choice --tool-call-parser),toolsandfunctionscan never produce a payload. The OpenAI-compatible adapter tracks, per endpoint and per mode, three consecutive structured calls that exhausted their attempts with no native payload, then skips that mode, re-probing once every 64 calls so a redeploy recovers without a restart. Errored requests (4xx/5xx, resets, deadline aborts) and truncated responses do not count against a mode, and any answered call clears its count. JSON mode is never skipped. There is no setting; transitions are logged atwarn. - Concurrency and rate are separate ceilings.
LLM_MAX_PARALLEL_REQUESTSbounds how many LLM requests are in flight at once (enforced inside the LLM transport, so it applies on every surface); theLLM_RATE_LIMIT_*bucket bounds how often they start. A slot is held for the HTTP exchange itself and released while a request waits for pacing or a retry backoff. It defaults to 1000 (matching the connection-pool limitopenai-pythonandlitellmset), and 128 on Android/iOS. - Pacing is off until it is needed. With
LLM_RATE_LIMIT_ENABLED=false(the default) requests dispatch unbounded up to the concurrency ceiling. When the provider reports overload — HTTP 429, 503, 529, or a timeout —AUTO_RATE_LIMIT(default on) opens an episode: for the next 15 minutes dispatch is paced atLLM_RATE_LIMIT_REQUESTSperLLM_RATE_LIMIT_INTERVALseconds. Fresh overload extends it; it lapses on quiet. SetLLM_RATE_LIMIT_ENABLED=trueto pace from the start, orAUTO_RATE_LIMIT=falseto never pace automatically. - Some failures are never retried, because no wait can fix them: bad request, authentication, unknown model, payment required, and a 429 whose body reports exhausted quota or billing rather than a per-minute limit.
EMBEDDING_RATE_LIMIT_* bucket, which is
flag-gated only — provider overload does not switch it on automatically.
Logging
Canonical table. Binding READMEs andLogging configures a process-wide singleton (the.env.examplelink here. cognee writes structured logs to stdout and (when writable) to a rotating file. File logging is owned bycognee-logging: the CLI installs it at startup, the language bindings when you call theirsetup_loggingentry point. Exception — machine-readable CLI output.cognee-clisends console logs to stderr for any invocation whose stdout is a contract with a program:search/recallwith-f jsonor-f simple, andexport,visualizeandbench, which print a path or a JSON document. Human-facingprettyoutput, and every other subcommand, still log to stdout.
tracing subscriber), so it is
process config: one LoggingConfig, resolved
once, at startup. Its only window onto the environment is
LoggingConfig::from_env_source (the from_env() boundary). Every field is also
settable from code: assign it, or use LoggingConfig::set(key, value) with the
option keys below.
Precedence: defaults < environment < options from code.
- CLI. The CLI loads
Settingsbefore it installs logging, so the twoSettingskeyslog_levelandlogs_root_directory(default empty: use the logging default) are lowered into the logging config. Each fills only what the logging environment left unset:RUST_LOG>LOG_LEVEL> the config file’slog_level> the default filter, andCOGNEE_LOGS_DIR> the config file’slogs_root_directory>~/.cognee/logs. Socognee-cli config set log_level debugpersists a level. - Bindings. A binding installs logging from a module-level function that runs
before any handle exists, so a handle’s
Settings(andconfig.set("log_level", …)) do not affect logging there. Pass the options to the entry point instead; they win over the environment. The first call wins, later calls are no-ops.
Option keys are snake_case; the bindings also accept camelCase (
logLevel). An
unknown key or an invalid value is an error (Python ValueError, a JS exception,
CogneeException with VALIDATION_ERROR, C return code 2 with
cg_last_error_message()). The default bridge subscriber a binding installs on
import (into Python logging, or stderr) reads its level filter through the same
LoggingConfig boundary (RUST_LOG > LOG_LEVEL > default).
Multi-process warning — when several cognee processes share one log file viaLOG_FILE_NAME, rotation is not coordinated; concurrent rotation can corrupt the log. For sharded workers, give each shard its ownCOGNEE_LOGS_DIR(or unsetLOG_FILE_NAMEper shard).
Observability & telemetry
cognee emits OpenTelemetry traces (behind thetelemetry feature) and opt-out
product analytics. The deep references are
observability/opentelemetry.md and
observability/send_telemetry.md.
OpenTelemetry traces
Precedence: defaults < environment < code. The exporter is process-wide too,
but where it reads its settings depends on who initialises it:
- CLI — from
Settings(defaults < config file < env <--flags), socognee-cli config set otel_exporter_otlp_endpoint …works. - Rust embedders —
cognee::telemetry::init_telemetry(&settings)takes anySettingsView: aSettings, or anEnvSettingsView(env only, plusEnvSettingsView::set(key, value)from code). - Bindings — from a module-level function that runs before any handle exists,
so a handle’s
Settingsdo not apply. The binding’s default service name (cognee.python-binding,cognee.node-binding,cognee.java-binding,cognee.capi-binding) is a default:OTEL_SERVICE_NAMEand an explicitotel_service_nameoption both win over it, and the process environment is never modified. Options use theSettingskey names above (camelCase also accepted):
Product analytics
Product analytics are on by default (opt-out, matching the Python SDK). The switch is process-wide: oneTelemetryConfig governs every event the
process sends, whichever handle emits it. It is not a Settings key and not a
cognee-cli config key.
Precedence: defaults (on) < environment < code. The environment is read when
an event is sent, so a
.env loaded when the first handle is built, or a variable
set later, is honoured. An explicit call wins over the environment in both
directions and stays in force until the next one: set_enabled pins only the
switch, configure pins every field, reset_to_env returns to the environment.
Disabling also drops events that are queued but not yet sent.
The existing
setup_telemetry_analytics() / setupTelemetryAnalytics() /
Cognee.initTelemetry() / cognee_init_telemetry() arm the binding (which
importing it already does) and report the same state as the query functions.
Building without the telemetry cargo feature compiles analytics out entirely;
the switch still exists and is_enabled() is always false.
The api_key_tracking_id in each event is a salted hash of the resolved LLM
key (llm.api_key, from env, a config file or code). Every ComponentManager
holds a cognee::telemetry::LlmKeyRegistration for its key, updated when its
configuration changes and dropped with the manager; embedders without a manager
call cognee::telemetry::set_llm_api_key. The LLM_API_KEY variable is not read
by the analytics client itself. When every live registration uses the same key,
its hash is reported; when live handles use different keys, process-wide events
cannot be attributed to one of them and the id is reported empty rather than
mislabelled.
Divergence from Python: Python hashes
os.getenv("LLM_API_KEY") only. Rust
hashes the key it actually uses, which can also come from its OPENAI_TOKEN
alias, a config file or code. The derivation is identical (PBKDF2-HMAC-SHA256,
same salt), so the same key gives the same id in both SDKs, which is what
cross-SDK grouping needs; a Rust process configured only through OPENAI_TOKEN
reports an id where a Python process without LLM_API_KEY (and so without a
working LLM) reports none.HTTP server
The server binary is not part of thecognee-rs repository. It reads its own
env surface (host/port, auth, body limits, pipeline registry, notebooks, health
probes), which is documented with the server rather than here; see
HTTP Server Tools.
Cloud
Cloud/Auth0 configuration (COGNEE_CLOUD_URL, COGNEE_AUTH0_*) and the
serve()/disconnect() flow are not part of OSS.
Runtime configuration API
ConfigManager (Arc<RwLock<Settings>>) is the runtime API that the bindings
and the CLI use.
- Generic setter:
set(key, value)accepts every key in the key registry (eachSettingsfield name, plus aliases such asembedding_model) and every key of a registered component section, canonical or alias (see Component config sections). An unknown key follows strict mode.get(key)reads any of them. A JSONnullclears the optional settings (chunk_size,llm_temperature,single_process,summarization_schema). Settingsystem_root_directoryalso moves the graph and vector paths derived from the old root. - Typed setters for the common keys, in the families
set_llm_*,set_embedding_*,set_vector_db_*,set_graph_*,set_chunk_*,set_relational_db_*,set_*_root_directory,set_ontology_*,set_classification_model/set_summarization_model/set_summarization_schema. - Bulk setters:
set_llm_config,set_embedding_config,set_vector_db_configandset_graph_db_config. - Introspection:
read(),version(),get_settings()(secrets masked).Settings::get_key/set_key/reset_keywork on a plainSettings,cognee::config::keys()lists the registry, andConfigManager::schema()the component sections it knows.ConfigManager::apply_patchapplies a settings object in bulk, which is how the C and Python constructors take theirs.
ConfigManager rustdoc.
The binding ergonomics (granular JS setters vs generic set in Python/C/Java) are
documented in Language bindings: configuration.
CLI config subcommand
cognee-cli config get|set|unset|list|reset reads/writes the persisted JSON file.
The settable keys are the registry keys whose value is saved in that file
(every Settings field except llm_args and summarization_schema), plus the
keys of the component sections compiled into this build. cognee-cli config list
prints them. Example: