Configuration
Configuration comes from three layers, lowest precedence first:
- Defaults in
backend/app/config.py(Settings, loaded via pydantic-settings from the environment and.env) - Persisted app settings and database rows (agent configs, diarization and transcription runtime config) edited through the Admin panel
- Per-session agent overrides
Environment variables
Section titled “Environment variables”| Variable | Default | Purpose |
|---|---|---|
GEMINI_API_KEY |
empty | Google API key fallback when no encrypted credential is stored |
OPENAI_API_KEY |
empty | OpenAI API key fallback |
OPENAI_BASE_URL |
https://api.openai.com/v1 |
Base URL for every OpenAI-shaped chat call; the persisted llm.openai_compatible.base_url app setting overrides it for the openai-compatible provider only |
OPENAI_COMPATIBLE_API_KEY |
empty | Optional key for the legacy openai-compatible provider; local servers need none |
OPENAI_COMPATIBLE_MODEL_ID |
empty | Wire model name for the legacy openai-compatible provider; the llm.openai_compatible.model_id app setting takes precedence |
LLM_TIMEOUT_SECONDS |
120 | Ceiling for a hosted chat-completions reply |
LLM_SELF_HOSTED_TIMEOUT_SECONDS |
900 | Ceiling for a self-hosted reply, which can take minutes on CPU |
LLM_SELF_HOSTED_MAX_TOKENS |
8192 | Completion budget sent to self-hosted servers, so long replies are not truncated at the server’s own default |
DATABASE_URL |
postgresql+asyncpg://callhelper:changeme@db:5432/callhelper |
Async SQLAlchemy connection string; set by Docker Compose for the backend container |
FRONTEND_DIST |
empty | Path to a built frontend for the backend to serve directly (the desktop launcher sets it); empty means nginx serves the frontend (Docker) |
DATA_DIR |
/app/data |
Root for recorded audio, downloaded ASR models, and the credentials master key; a named Docker volume (backend_data) in Compose |
BACKCHANNEL_FFMPEG |
empty | Explicit path to the ffmpeg executable used for compressed-audio decoding; the desktop launcher sets it to the bundled copy on Windows and Linux, and PATH lookup is the fallback |
POSTGRES_USER / POSTGRES_PASSWORD / POSTGRES_DB |
callhelper / changeme / callhelper |
Consumed by the db container and interpolated into DATABASE_URL in Compose |
CREDENTIALS_MASTER_KEY |
auto-generated | Overrides the Fernet master key used for encrypted credentials |
DATA_DIR, BACKCHANNEL_FFMPEG, and CREDENTIALS_MASTER_KEY are read
straight from the process environment (os.environ), not through the
pydantic Settings object, so they must be set as real environment
variables; a .env entry alone does not reach them.
Copy .env.example to .env as a starting point; it contains only
commented-out entries for GEMINI_API_KEY, the Sortformer
PYTORCH_INDEX_URL, and the database settings. Other variables, including
OPENAI_API_KEY, must be added by hand.
API credentials
Section titled “API credentials”Provider keys (google, openai) should be set in Admin -> Connections, which
stores them encrypted with Fernet (backend/app/routers/credentials.py).
The master key is auto-generated at DATA_DIR/master.key on first use, or
supplied via CREDENTIALS_MASTER_KEY. Environment variables remain
fallbacks when no credential row exists for a provider.
POST /api/credentials/{provider}/test validates a stored key with a real
provider call.
Transcription and audio
Section titled “Transcription and audio”Transcription model, batch behavior, and audio handling are set in
Admin -> Transcription & Audio. The live-caption model on this tab is the
audio gateway agent’s model: PATCH /api/diagnostics/transcription/config
with live_preview_model_id writes the same audio_gateway AgentConfig
row that Admin -> Agents edits
(backend/app/services/transcription_runtime.py).
Privacy First (local-only) mode
Section titled “Privacy First (local-only) mode”The Admin panel has a “Privacy First” switch (GET/PUT /api/privacy,
persisted as the privacy.local_only app setting). While it is on, no call
audio or transcript text leaves the machine:
- Batch transcription is coerced to a local ONNX model
(
local-whisper-baseby default) for live segments, audio imports, and re-transcription; selecting a cloud transcriber is rejected. - The audio gateway (interim captions) is skipped unless it is set to the
on-device captioner (
local-parakeet-live), which transcribes short chunks with local Parakeet ONNX and needs no cloud call. The cloud gateways (Gemini Live, OpenAI Realtime) are always skipped. - Analysis agents are skipped unless they are pointed at a model served by
a self-hosted endpoint on your own machine or network (see below). The gate
is
allows_local_only()inbackend/app/services/privacy.py: it admits the bundled ONNX models and any endpoint model whose base URL resolves to loopback, a private network, a single-label LAN hostname, a.local/.internal/.lan/.home.arpaname, orhost.docker.internal. An endpoint reachable only over the public internet is treated as cloud even though it speaks the same protocol, because it may be a hosted inference provider. generate_textraisesLocalOnlyModeErrorfor any model that fails that test, so post-import analysis, meeting chat, insight enhancement, and document summarization return HTTP 409/400 with an explanatory message.- Startup provider key verification is skipped.
With an on-prem text endpoint configured, Privacy First and the analysis agents are no longer mutually exclusive: the agents keep working and no call data leaves your perimeter. An agent left on a cloud model while the mode is on does not run; the Admin panel badges it and names the fix, and the live call status says which agents are paused, so a quiet call is never the first sign of it.
Speaker diarization, session recording, file imports, and exports already run
locally and are unaffected. Turning the switch off restores the previously
selected cloud models. The enable flow in the UI shows the full list of
features that stop working before the mode is applied
(privacy_impact() in backend/app/services/privacy.py).
Self-hosted endpoints
Section titled “Self-hosted endpoints”Any number of OpenAI-shaped chat servers can be registered in
Admin -> Connections -> Self-Hosted Models: LM Studio
(http://localhost:1234/v1), Ollama (http://localhost:11434/v1), vLLM,
LiteLLM, or a shared GPU box on the LAN. Each endpoint is a row in the
custom_endpoints table holding a name, base URL, optional Fernet-encrypted
API key, and the list of models it serves.
Every listed model becomes a first-class registry entry with the id
endpoint:<endpoint slug>:<served model name> (for example
endpoint:lm-studio:antares-1b), so it appears by name in the agent,
transcription, and meeting-chat pickers, grouped under the endpoint’s name.
llm.provider_for() recognizes the endpoint: prefix and routes the call to
the OpenAI dialect without a database read; resolve_endpoint() then loads
that endpoint’s base URL, wire model name, and key.
REST surface (backend/app/routers/endpoints.py):
| Route | Purpose |
|---|---|
GET /api/endpoints |
List endpoints, their models, and last test result |
POST /api/endpoints |
Add an endpoint |
PUT /api/endpoints/{id} |
Patch it; an empty api_key clears the stored key |
DELETE /api/endpoints/{id} |
Soft delete: the row is tombstoned (deleted_at set, stored key cleared), not removed |
POST /api/endpoints/{id}/test |
Probe {base_url}/models and record the outcome |
POST /api/endpoints/probe |
Probe an unsaved URL and list what it serves |
No API key is required: requires_key is None for these models and no
Authorization header is sent at all when the endpoint has no key, since an
empty bearer token breaks some servers rather than being ignored.
PUT /api/endpoints/{id} guards the privacy boundary: moving an endpoint
from an on-prem base URL to an off-prem one is refused outright while
Privacy First is on, and without Privacy First it requires
confirm_off_prem=true, because the change can send call data outside the
machine or network (update_endpoint() in
backend/app/services/custom_endpoints.py).
Running Backchannel in Docker? Inside the container localhost is the
container, not your machine. Use http://host.docker.internal:1234/v1 to
reach a server running on the host.
Legacy single-endpoint settings
Section titled “Legacy single-endpoint settings”Before named endpoints, one OpenAI-compatible server was configured through
the llm.openai_compatible.base_url and llm.openai_compatible.model_id app
settings, with OPENAI_BASE_URL and OPENAI_COMPATIBLE_MODEL_ID as
environment fallbacks, and surfaced as a single openai-compatible registry
entry. That configuration is migrated to a named endpoint on first startup
(migrate_legacy_endpoint()), agents using the placeholder are repointed at
the migrated model, and the placeholder is then hidden. Installs configured
purely through the environment variables keep the placeholder and keep
working.
Settings reference (backend/app/config.py)
Section titled “Settings reference (backend/app/config.py)”Models
Section titled “Models”| Setting | Default | Meaning |
|---|---|---|
GEMINI_MODEL |
gemini-3.1-flash-live-preview |
Seeded default for the live audio gateway |
BATCH_TRANSCRIBER_MODEL |
gemini-3.5-flash-lite |
Fallback batch transcription model; the persisted transcription.batch.model_id app setting takes precedence |
REFINEMENT_MODEL |
gemini-3.5-flash |
Fallback text model, used only when the owning agent row has no model set. Post-import Analyze takes its model from the Consolidated Analyst row and Enhance Insights takes its from the Principal Agent row, so on a seeded install this default is not reached |
REFINEMENT_INTERVAL_SECONDS |
45 | Refinement cadence |
Agent timing
Section titled “Agent timing”| Setting | Default | Meaning |
|---|---|---|
TEXT_AGENT_INTERVAL_SECONDS |
40 | Consolidated analyst cycle |
OBJECTION_HANDLER_INTERVAL_SECONDS |
10 | Objection handler fast scan cycle |
OBJECTION_WINDOW_SECONDS |
90 | Transcript window for objection scans |
SYNTHESIZER_COOLDOWN_SECONDS |
75 | Minimum time between synthesizer runs |
SYNTHESIZER_MAX_INTERVAL_SECONDS |
120 | Fallback max gap for the synthesizer |
OPPORTUNITY_SPECIALIST_COOLDOWN_SECONDS |
55 | Batch window for the opportunity specialist |
KNOWLEDGE_CONTEXT_CHAR_BUDGET |
60000 | Max characters of knowledge context per prompt |
Per-agent interval values stored in agent_configs rows override these
defaults (see Agent System).
Diarization
Section titled “Diarization”| Setting | Default | Meaning |
|---|---|---|
LIVE_DIARIZER |
lightweight |
Diarizer implementation (lightweight VAD+embeddings, or Sortformer on GPU) |
VAD_THRESHOLD |
0.6 | Silero speech probability threshold |
MIN_SEGMENT_MS |
750 | Minimum speech segment length |
MAX_SEGMENT_MS |
15000 | Maximum segment length before a forced cut |
SILENCE_GAP_MS |
600 | Silence gap that closes a segment |
SPEAKER_SIMILARITY_THRESHOLD |
0.68 | Embedding similarity to match an existing speaker |
SPEAKER_COHERENCE_WINDOW_MS |
3000 | Window size for within-segment coherence embeddings |
SPEAKER_COHERENCE_THRESHOLD |
0.40 | Adjacent-window similarity below which a segment is split at a speaker change |
MIN_NEW_SPEAKER_MS |
4000 | Minimum speech needed to enroll a new speaker profile |
MAX_SPEAKER_PROFILES_PER_TRACK |
4 | Maximum auto-enrolled speaker profiles per audio track |
SORTFORMER_WINDOW_MS |
15000 | Sortformer processing window |
Diarization values can be changed at runtime through
PATCH /api/diagnostics/diarization/config; the database-persisted runtime
config wins over these defaults.
ONNX Runtime threading
Section titled “ONNX Runtime threading”The lightweight diarizer runs two ONNX models on CPU, and ONNX Runtime sizes its thread pool from the host core count unless told otherwise. Left at that default both models spend most of their CPU on pool overhead rather than arithmetic, and ORT does not see container CPU quotas, so a big host or a small quota both go badly. These settings bound it.
| Setting | Default | Meaning |
|---|---|---|
DIARIZER_VAD_ONNX_THREADS |
1 | Intra-op threads for Silero VAD. It is an LSTM over one 512-sample frame with nothing to parallelize; the ORT default cost 5x the CPU for about 9% of wall time |
DIARIZER_EMBED_ONNX_THREADS |
min(4, cores / 2) |
Intra-op threads for the WeSpeaker embedding model. Bounding the pool cuts CPU roughly 4x and costs some wall time |
DIARIZER_EMBED_ONNX_SPIN |
false |
Let ORT spin-wait between embedding calls. Segments arrive seconds apart, so that spin is charged to the VAD; leaving this off measured no downside |
Both thread counts are clamped to at least 1: ORT reads 0 as “use every core”,
which is the default these settings exist to avoid. Raising
DIARIZER_EMBED_ONNX_THREADS trades CPU for latency on the foreground
transcript-import path; the live path has ample headroom either way.
Legacy agent toggles
Section titled “Legacy agent toggles”AGENT_QUESTION_HUNTER_ENABLED, AGENT_CONSOLIDATED_ENABLED,
AGENT_OBSERVER_ENABLED, AGENT_OPPORTUNITY_SCOUT_ENABLED,
AGENT_ACTION_TRACKER_ENABLED, AGENT_SYNTHESIZER_ENABLED, and
AGENT_OPPORTUNITY_SPECIALIST_ENABLED predate database agent configuration.
The orchestrator primarily uses agent_configs rows; only some subtype
flags are still consulted as fallbacks. Prefer the Admin panel over these
env vars.
Model registry
Section titled “Model registry”MODEL_REGISTRY in backend/app/config.py is the central catalog the app
selects models from (GET /api/models). Each entry declares its provider,
required key, and capabilities:
| Capability | Meaning |
|---|---|
supports_text |
Usable by text agents (analyst, objection handler, synthesizer, chat) |
supports_batch_audio |
Usable for batch/segment transcription and re-transcription |
supports_live_audio |
Usable as the live audio gateway |
GET /api/models also returns runs_locally and endpoint_id on every
entry (backend/app/routers/models.py): runs_locally covers the bundled
ONNX models and models served by an on-prem endpoint, and is the flag the
Privacy First UI admission keys off; endpoint_id is set for models served
by a saved custom endpoint.
Current entries include Google Gemini text/audio models
(gemini-3.7-flash, gemini-3.5-flash-lite, gemini-3.5-flash, gemini-3-flash-preview, gemini-3.1-pro-preview,
gemini-3.1-flash-lite, gemini-2.5-flash, gemini-2.5-flash-lite,
gemini-2.5-pro), the live gateway model
gemini-3.1-flash-live-preview, OpenAI text models (the GPT-5.6 family
gpt-5.6-sol, gpt-5.6-terra, gpt-5.6-luna, plus gpt-5.5, gpt-5.4,
gpt-5.4-mini, gpt-5.4-nano), OpenAI speech-to-text models
(gpt-live-transcribe as a realtime-only gateway; gpt-4o-transcribe and
gpt-4o-mini-transcribe usable both as realtime gateways and as batch
transcription models; gpt-audio-1.5 and gpt-audio-mini as batch-only
audio chat models), the openai-compatible placeholder for the legacy
single self-hosted endpoint (text-capable, keyless, listed only while that
legacy configuration is active), and key-free local models
(local-whisper-base and local-parakeet-tdt-0.6b,
supports_batch_audio only; local-parakeet-live, the experimental
on-device live captioner, supports_live_audio only). No Local entry sets
supports_text, so text agents need a provider key, the legacy
placeholder, or a self-hosted endpoint model. Add new models by appending to
the registry.