Lemon Configuration (TOML)

May 20, 2026 · View on GitHub

Lemon uses a single canonical configuration file in TOML format. Configuration is layered:

  1. Global: ~/.lemon/config.toml
  2. Project: <project>/.lemon/config.toml (overrides global)
  3. Environment variables (override file values; .env may auto-populate missing env vars at startup)
  4. Lemon secrets referenced from config (for secret-backed fields)

Runtime state and policy are separate from config. Per-session or per-route "current" model/thinking values override config defaults at runtime, but they are not persisted in config.toml.

Example

[providers.anthropic]
api_key_secret = "llm_anthropic_api_key_raw"

# Claude Max / Claude Code subscription auth
# auth_source = "oauth"
# oauth_secret = "llm_anthropic_api_key"

[providers.openai]
api_key_secret = "OPENAI_API_KEY"

# OpenAI-compatible local or hosted endpoint.
# Store a placeholder secret such as "local" if the endpoint ignores API keys.
# base_url = "http://127.0.0.1:11434/v1"

[providers.opencode]
api_key_secret = "OPENCODE_API_KEY"
base_url = "https://opencode.ai/zen/v1"

[defaults]
provider = "anthropic"
model = "anthropic:claude-sonnet-4-20250514"
thinking_level = "medium"
engine = "lemon"

[runtime.compaction]
enabled = true
reserve_tokens = 16384
keep_recent_tokens = 20000

[runtime.retry]
enabled = true
max_retries = 3
base_delay_ms = 1000

[runtime.cli.codex]
extra_args = ["-c", "notify=[]"]
auto_approve = false

[runtime.cli.opencode]
# Optional model override passed to `opencode run --model`.
model = "gpt-4.1"

[runtime.cli.droid]
# Optional model override passed to `droid exec -m`. When omitted, Lemon defaults Droid to `glm-5.1`.
model = "glm-5.1"
# Optional reasoning effort passed to `--reasoning-effort`.
reasoning_effort = "medium"
# Optional tool allow/deny lists passed to `--enabled-tools` / `--disabled-tools`.
enabled_tools = ["grep", "read_file"]
disabled_tools = ["write"]
# Optional planning/spec mode.
use_spec = false
spec_model = "planner-v1"
# Optional extra flags appended before the prompt.
extra_args = []
# Authentication is provided via FACTORY_API_KEY.

[runtime.cli.pi]
# Optional extra flags prepended to the `pi` command.
extra_args = []
# Optional provider/model overrides passed to `pi --provider/--model`.
provider = "openai"
model = "gpt-4.1"

[runtime.cli.claude]
# dangerously_skip_permissions = false  # opt-in only — set true only when you fully trust the model and task

[runtime]
# Explicitly trusted extension directories. Files here can be compiled and executed.
extension_paths = []

[runtime.extensions]
# Disable all extension code execution while keeping manifest diagnostics available.
enabled = true
# Keep default global/project extension directories diagnostics-only unless trusted.
auto_load_default_paths = false

[runtime.tools.web.search]
provider = "brave" # "brave" | "perplexity"
cache_ttl_minutes = 15

[runtime.tools.web.search.perplexity]
model = "perplexity/sonar-pro"

[runtime.tools.web.fetch]
cache_ttl_minutes = 15
allow_private_network = false
allowed_hostnames = []

[runtime.tools.web.fetch.firecrawl]
enabled = true

[runtime.tools.wasm]
enabled = false
auto_build = true
runtime_path = ""
tool_paths = []
default_memory_limit = 10485760
default_timeout_ms = 60000
default_fuel_limit = 10000000
cache_compiled = true
cache_dir = ""
max_tool_invoke_depth = 4

[tui]
theme = "lemon"
debug = false
compact = false
timestamps = false
bell = true
thinking = false

[logging]
# Optional: write logs to a file for later analysis.
# If unset/empty, file logging is disabled and logs go to stdout/stderr only.
file = "~/.lemon/log/lemon.log"
# Optional: handler level for the file (defaults to "debug").
level = "debug"

[gateway]
max_concurrent_runs = 2
default_engine = "lemon"
auto_resume = false
enable_telegram = false
enable_xmtp = false

[gateway.telegram]
bot_token = "123456:token"
allowed_chat_ids = [12345678]
default_account_id = "default"   # optional account for ./bin/lemon send account-scoped lookups
default_chat_id = 12345678       # optional default for ./bin/lemon send --to telegram
default_thread_id = 35           # optional forum topic/thread

[gateway.xmtp]
env = "production"                  # production | dev | local
wallet_address = "${XMTP_WALLET_ADDRESS}"
wallet_key_secret = "xmtp_wallet_key"
db_path = "~/.lemon/xmtp-db"
poll_interval_ms = 1500
connect_timeout_ms = 15000
require_live = true
mock_mode = false

[gateway.voice]
enabled = false
websocket_port = 4047
public_url = "https://example.com"
twilio_account_sid_secret = "twilio_account_sid"
twilio_auth_token_secret = "twilio_auth_token"
twilio_phone_number = "+1234567890"
deepgram_api_key_secret = "deepgram_api_key"
elevenlabs_api_key_secret = "elevenlabs_api_key"
elevenlabs_voice_id = "21m00Tcm4TlvDq8ikWAM"
elevenlabs_output_format = "ulaw_8000"
llm_model = "gpt-4o-mini"
max_call_duration_seconds = 600
silence_timeout_ms = 5000

[profiles.default]
name = "Daily Assistant"
system_prompt = "You are my daily assistant."

[profiles.default.tool_policy]
# Optional preset profile:
# profile = "minimal_core"  # full_access | minimal_core | read_only | safe_mode | subagent_restricted | no_external | custom
allow = "all"
deny = []
require_approval = ["bash", "write", "edit"]
no_reply = false

[[gateway.bindings]]
transport = "telegram"
chat_id = 12345678
agent_id = "default"

Environment Overrides

Environment variables override file values. Common overrides:

  • LEMON_DEFAULT_PROVIDER, LEMON_DEFAULT_MODEL
  • LEMON_PROVIDER_ROUTING_ENABLED, LEMON_PROVIDER_FALLBACK_PROVIDERS, LEMON_PROVIDER_ROUTING_REQUIRE_CREDENTIALS
  • LEMON_THEME, LEMON_DEBUG
  • <PROVIDER>_API_KEY, <PROVIDER>_BASE_URL (e.g., ANTHROPIC_API_KEY, OPENAI_BASE_URL, OPENCODE_API_KEY, ZAI_API_KEY, MINIMAX_API_KEY)
  • LEMON_CODEX_EXTRA_ARGS, LEMON_CODEX_AUTO_APPROVE
  • LEMON_CLAUDE_YOLO
  • LEMON_WASM_ENABLED, LEMON_WASM_RUNTIME_PATH, LEMON_WASM_TOOL_PATHS, LEMON_WASM_AUTO_BUILD
  • LEMON_TERMINAL_BACKENDS_ALLOW, LEMON_TERMINAL_BACKENDS_DENY, LEMON_TERMINAL_BACKENDS_REQUIRE_APPROVAL
  • LEMON_DOCKER_TERMINAL_IMAGE, LEMON_DOCKER_TERMINAL_MEMORY, LEMON_DOCKER_TERMINAL_CPUS, LEMON_DOCKER_TERMINAL_PIDS_LIMIT, LEMON_DOCKER_TERMINAL_NETWORK
  • LEMON_DOCKER_TERMINAL_READ_ONLY_ROOTFS, LEMON_DOCKER_TERMINAL_TMPFS_SIZE, LEMON_DOCKER_TERMINAL_ALLOWED_IMAGES
  • LEMON_SSH_TERMINAL_TARGET, LEMON_SSH_TERMINAL_WORKDIR, LEMON_SSH_TERMINAL_PORT, LEMON_SSH_TERMINAL_CONNECT_TIMEOUT, LEMON_SSH_TERMINAL_STRICT_HOST_KEY_CHECKING, LEMON_SSH_TERMINAL_ALLOWED_TARGETS
  • LEMON_GATEWAY_HEALTH_PORT, LEMON_ROUTER_HEALTH_PORT
  • LEMON_LOG_FILE, LEMON_LOG_LEVEL
  • BRAVE_API_KEY, PERPLEXITY_API_KEY, OPENROUTER_API_KEY, FIRECRAWL_API_KEY

Terminal backend policy validates Docker image/network/resource settings and SSH port/timeout/host-key settings before exec launches a backend. Invalid Docker limits, invalid Docker image/network names, invalid SSH ports, invalid SSH connect timeouts, and unsupported strict-host-key values fail closed at the policy boundary instead of reaching Docker or OpenSSH.

[tui].thinking controls whether assistant reasoning/thinking blocks are shown in the Python CLI. It is loaded at startup and can be toggled for the current process with /thinking.

Async followup queue defaults for background task completions and delegated agent completions are currently umbrella app config, not TOML. The default lives in config/config.exs as config :coding_agent, :async_followups, default_queue_mode: :steer_backlog. Per-call tool inputs still override it: task.queue_mode and agent.followup_queue_mode.

Feature Flags

Feature flags gate in-progress behaviour changes so they can be shipped incrementally without ad-hoc environment variables. All flags default to "off".

[features]
product_runtime              = "off"   # M1 runtime boot/profile/health/setup/update
skills_hub_v2                = "off"   # M2/M3 manifest v2 + progressive skill loading
skill_manifest_v2            = "off"   # M2-01 manifest v2 parser/validator
progressive_skill_loading_v2 = "off"   # M3-02/M3-03 partial skill body loading
session_search               = "off"   # M5-02 SessionSearch API + search_memory tool
routing_feedback             = "off"   # M6-02 task fingerprinting + routing feedback
skill_synthesis_drafts       = "off"   # M7-02 skill synthesis draft pipeline

Valid rollout states:

StateMeaning
"off"Feature fully disabled (kill-switch).
"opt-in"Available but disabled by default; must be explicitly opted in.
"default-on"Enabled unless explicitly disabled.

Each flag can be overridden via an environment variable using the pattern LEMON_FEATURE_<FLAG_NAME> (SCREAMING_SNAKE_CASE):

LEMON_FEATURE_SESSION_SEARCH=opt-in
LEMON_FEATURE_PRODUCT_RUNTIME=default-on

Config validation fails cleanly if a flag is set to an unrecognised state.

Canonical Sections

Use only these top-level sections:

  • defaults
  • runtime
  • features
  • profiles.<agent_id>
  • providers.<name>
  • gateway
  • tui
  • logging

Deprecated sections now fail validation and runtime loading:

  • [agent] -> move defaults to [defaults] and runtime settings to [runtime]
  • [agents.<id>] -> move to [profiles.<id>]
  • [agent.tools.*] -> move to [runtime.tools.*]
  • [tools.*] -> move to [runtime.tools.*]

Dotenv Autoload

Lemon can auto-load a .env file at startup:

  • ./bin/lemon-dev / lemon-tui: loads <cwd>/.env where <cwd> is the agent working directory (--cwd, or current directory).
  • clients/lemon-web/server bridge: loads <cwd>/.env from --cwd (or current directory).
  • ./bin/lemon-gateway: loads .env from the directory where you launch the script.

By default, existing environment variables are preserved. .env values only fill missing variables.

OpenAI Codex (ChatGPT OAuth)

Lemon supports the Codex subscription provider as openai-codex (it uses the ChatGPT OAuth JWT, not OPENAI_API_KEY). The canonical config key is providers.openai-codex; providers.openai_codex is also accepted for backward compatibility by native Lemon/CodingAgent runs.

Primary setup paths:

mix lemon.onboard
mix lemon.onboard.codex

What it does:

  • Resolves Codex OAuth credentials via Ai.Auth.OpenAICodexOAuth
  • Stores credentials in encrypted secrets
  • Writes providers.openai-codex.auth_source = "oauth" plus providers.openai-codex.oauth_secret
  • Optionally updates [defaults] provider/model
  • Uses an interactive arrow-key TUI for selection steps when running in a real terminal
  • Listens on the localhost OAuth callback automatically and falls back to manual paste only if the callback cannot be captured

The onboarding flow opens the OpenAI auth URL directly and stores the returned OAuth credentials in Lemon secrets.

To force a token explicitly, set:

  • OPENAI_CODEX_API_KEY (preferred)
  • CHATGPT_TOKEN (fallback)

OpenAI-Compatible Endpoints

For local or hosted services that expose an OpenAI-compatible API, configure the normal openai provider with a custom base_url:

[providers.openai]
api_key_secret = "llm_local_openai_api_key"
base_url = "http://127.0.0.1:11434/v1"

[defaults]
provider = "openai"
model = "openai:local-model-name"
engine = "lemon"

Then store the endpoint key:

mix lemon.secrets.set llm_local_openai_api_key "local"

The same provider config shape also works for hosted OpenAI-compatible provider ids handled by Lemon, including opencode, openrouter, zai, minimax, kimi, xai, mistral, groq, deepseek, qwen, and related compatible providers. Each provider gets its own [providers.<id>] table with api_key_secret and optional base_url.

Provider Onboarding (CLI)

Lemon includes a top-level onboarding picker for provider credentials:

mix lemon.onboard
mix lemon.onboard anthropic
mix lemon.onboard codex
mix lemon.onboard gemini
mix lemon.onboard zai
mix lemon.onboard minimax

# Provider-specific aliases still work
mix lemon.onboard.antigravity
mix lemon.onboard.gemini
mix lemon.onboard.codex
mix lemon.onboard.copilot

All onboarding flows:

  • Verify encrypted secrets are configured
  • Let you choose a provider when none is passed
  • Use an interactive arrow-key TUI for provider/auth/model selection when a TTY is available
  • Run provider OAuth flow by default when supported, or prompt for an API key/token otherwise
  • Capture localhost OAuth callbacks automatically when the provider redirect URI is local
  • Store credentials in encrypted secrets with provider metadata
  • Write the relevant providers.<provider> config keys
  • Support --set-default, --model, and --config-path

Provider readiness is visible through the read-only control-plane providers.status method and the Web /ops provider panel. It uses the same LemonAiRuntime credential resolver as model execution, so env keys, encrypted secret references, OAuth/default-secret paths, and provider-specific credential shapes are checked the same way runtime calls check them. The response reports booleans such as credentialReady, apiKeyConfigured, apiKeySecretConfigured, oauthSecretConfigured, baseUrlConfigured, and envConfigured; it does not return raw API keys, secret names, base URLs, or env var names.

Memory-provider readiness is visible through read-only memory.status, Web /ops, and support-bundle memory_diagnostics.json. These surfaces expose provider ids, enabled state, source labels, scopes, timeout shape, and module load state without memory document contents, raw provider config, secret values, prompts, tool output, or provider error payloads.

Doctor support bundles include a core-owned provider_diagnostics.json snapshot for offline support. That snapshot reports provider setup shape, credential reference counts, ambient-provider booleans, and routing/pool/profile shape without depending on runtime provider modules. It intentionally omits raw API keys, secret names, raw base URLs, env var names, model prompts, and provider responses.

Provider route previews are controlled by runtime.provider_routing:

[runtime.provider_routing]
enabled = true
fallback_providers = ["zai", "anthropic"]
default_pool = "burst"
default_profile = "ops"
require_credentials = true

[runtime.provider_routing.credential_pools.burst]
providers = ["openai", "zai", "anthropic"]
strategy = "round_robin" # priority | round_robin

[runtime.provider_routing.profiles.ops]
fallback_providers = ["zai"]
credential_pool = "burst"
distribution = { openai = 70, zai = 20, anthropic = 10 }

providers.status includes a redacted routing block with the requested provider/model, selected provider/model, fallback candidates, candidate readiness booleans, selected routing profile, selected credential pool, profile distribution weights, pool provider names, pool strategy, and credential-reference counts. Pool/profile names and provider names are visible; raw API keys, secret names, base URLs, and env var names are not.

Coding-agent default model resolution consumes the same routing policy conservatively: if the default provider is not credential-ready and a configured fallback/profile/pool provider is credential-ready with the same model id in Ai.Models, Lemon selects that fallback before starting the supervised agent loop. Pools default to priority order; strategy = "round_robin" rotates the pool's starting provider through LemonCore.ProviderPoolRotator, a supervised BEAM process. Explicit user model specs are not rewritten.

The supervised coding-agent loop also wraps default-model streams with the same fallback ordering. If a provider returns a terminal stream error before useful assistant content or tool calls are emitted, Lemon retries the same turn against the next credential-ready fallback provider with the same model id. Once visible content or a tool call has started, the error is surfaced instead of replayed so the transcript cannot duplicate partial output.

Google Gemini CLI onboarding (mix lemon.onboard gemini) resolves OAuth credentials via Ai.Auth.GoogleGeminiCliOAuth, stores the encrypted payload in providers.google_gemini_cli.api_key_secret, writes providers.google_gemini_cli.auth_source = "oauth", and can take --project-id <gcp-project-id> to force a specific Code Assist project. At runtime, Lemon re-resolves the active Gemini project from providers.google_gemini_cli.project_id, providers.google_gemini_cli.project_secret, LEMON_GEMINI_PROJECT_ID, GOOGLE_CLOUD_PROJECT, GOOGLE_CLOUD_PROJECT_ID, or GCLOUD_PROJECT, with those values overriding the projectId stored inside the OAuth payload.

The onboarding alias gemini maps to the runtime provider google_gemini_cli. This is distinct from the AI Studio provider google, which expects a separate API key such as GOOGLE_GENERATIVE_AI_API_KEY.

Antigravity OAuth client credentials resolve from Lemon secrets first:

  • google_antigravity_oauth_client_id
  • google_antigravity_oauth_client_secret

Environment variables are supported as fallback:

  • GOOGLE_ANTIGRAVITY_OAUTH_CLIENT_ID
  • GOOGLE_ANTIGRAVITY_OAUTH_CLIENT_SECRET

Common non-interactive usage:

# Google Antigravity
mix lemon.onboard.antigravity --token <token> --set-default --model gemini-3-pro-high

# Google Gemini CLI / Code Assist
mix lemon.onboard.gemini --project-id your-gcp-project
mix lemon.onboard.gemini --token <token> --set-default --model gemini-2.5-pro

# OpenAI Codex
mix lemon.onboard.codex --token <token> --set-default --model gpt-5.2

# GitHub Copilot (enterprise + optional model enablement toggle)
mix lemon.onboard.copilot --enterprise-domain company.ghe.com
mix lemon.onboard.copilot --skip-enable-models
mix lemon.onboard.copilot --token <token> --set-default --model gpt-5
mix lemon.onboard.copilot --token <token> --config-path /path/to/config.toml

# Z.AI
mix lemon.onboard zai --token <token> --set-default --model glm-5

# MiniMax
mix lemon.onboard minimax --token <token> --set-default --model MiniMax-M2.7

Anthropic provider auth supports either API keys or Claude subscription OAuth.

API key flow:

mix lemon.secrets.set llm_anthropic_api_key_raw <token>

OAuth flow:

[providers.anthropic]
auth_source = "oauth"
oauth_secret = "llm_anthropic_api_key" # optional if this secret stores an Anthropic OAuth payload

When auth_source = "oauth", Lemon will also use ambient Claude Code credentials from ~/.claude/.credentials.json or CLAUDE_CODE_OAUTH_TOKEN / ANTHROPIC_TOKEN. If both a static env token and refreshable Claude Code credentials exist, Lemon prefers the refreshable Claude Code credentials. You can also create/store that payload through onboarding with mix lemon.onboard anthropic --auth oauth.

Web Tools (websearch / webfetch)

Lemon includes web tools under runtime.tools.web. For full setup and troubleshooting, see:

[runtime.tools.web.search]
enabled = true
provider = "brave"   # "brave" | "perplexity"
max_results = 5
timeout_seconds = 30
cache_ttl_minutes = 15

[runtime.tools.web.search.failover]
enabled = true
provider = "perplexity"

[runtime.tools.web.search.perplexity]
# Optional if PERPLEXITY_API_KEY / OPENROUTER_API_KEY is set.
api_key = "pplx-..."
base_url = "https://api.perplexity.ai"
model = "perplexity/sonar-pro"

[runtime.tools.web.fetch]
enabled = true
max_chars = 50000
timeout_seconds = 30
cache_ttl_minutes = 15
max_redirects = 3
readability = true
allow_private_network = false
allowed_hostnames = []

[runtime.tools.web.fetch.firecrawl]
# Optional if FIRECRAWL_API_KEY is set.
enabled = true
api_key = "fc-..."
base_url = "https://api.firecrawl.dev"
only_main_content = true
max_age_ms = 172800000
timeout_seconds = 60

[runtime.tools.web.cache]
persistent = true
path = "~/.lemon/cache/web_tools"
max_entries = 100

WASM Tools

WASM tools are disabled by default and run in a per-session Rust sidecar. See docs/tools/wasm.md for runtime behavior and troubleshooting.

[runtime.tools.wasm]
enabled = false
auto_build = true
runtime_path = ""
tool_paths = []
default_memory_limit = 10485760
default_timeout_ms = 60000
default_fuel_limit = 10000000
cache_compiled = true
cache_dir = ""
max_tool_invoke_depth = 4

Sections

  • providers.<name>: API keys and base URLs per provider.
  • defaults: global default model/provider/thinking/engine.
  • runtime: runtime behavior and tool settings.
  • runtime.tools.web: websearch / webfetch providers, guardrails, cache, and Firecrawl fallback.
  • runtime.tools.wasm: WASM sidecar runtime controls and discovery paths.
  • profiles.<agent_id>: assistant profiles (identity + defaults) used by gateway/control-plane.
  • runtime.compaction: context compaction settings.
  • runtime.retry: retry settings.
  • runtime.cli: CLI runner settings (codex, claude, droid, kimi, opencode, pi).
  • tui: terminal UI settings.
  • gateway: Lemon gateway settings, including queue, telegram, discord, sms, voice, xmtp, projects, bindings, and engines.
  • logging: optional file logging configuration.

Gateway Projects and Bindings

When LemonGateway handles a Telegram message, it can optionally map that chat (or topic/thread) to a named project. A project is just a working directory root (repo path) plus optional defaults.

Why it matters:

  • The gateway will run engines with cwd set to the project root (so file edits/commands happen in the right repo).
  • The gateway will load per-project config from <project_root>/.lemon/config.toml (which can override agent profiles, models, tool policy, etc. compared to your global ~/.lemon/config.toml).
  • If a chat has no bound project, gateway falls back to gateway.default_cwd (or ~/ by default).

Projects

Define projects under [gateway.projects.<project_id>]:

[gateway.projects.myrepo]
root = "/path/to/myrepo"
# Optional: project-level default engine if a binding doesn't set one.
default_engine = "lemon"

Bindings

Bindings connect an incoming chat scope to a project/agent/defaults:

[[gateway.bindings]]
transport = "telegram"
chat_id = 123456789

# Optional: bind this chat to a project (must match the `[gateway.projects.<id>]` key)
project = "myrepo"

# Optional: choose which agent profile to use (defaults to "default")
agent_id = "default"

# Optional: per-chat default engine/queue overrides
default_engine = "claude"
queue_mode = "steer"

Notes:

  • If you omit project, LemonGateway will run with cwd set to gateway.default_cwd when configured, otherwise ~/.
  • You can also bind at the topic/thread level by setting topic_id in the binding (takes precedence over the chat-level binding when a matching topic exists).
  • topic_id corresponds to Telegram's message_thread_id (only present in forum topics).
  • LemonGateway loads gateway.* config on startup; after changing gateway.projects or gateway.bindings, restart the gateway process.

Optional fallback cwd:

[gateway]
default_cwd = "~/"

Tip:

  • In Telegram, you can set or inspect the current chat/topic working directory at runtime with /cwd [project_id|path|clear].
  • /new <project_id|path> still works, and setting /cwd makes future /new sessions in that chat/topic use the same directory.
  • /new confirmation replies include model, provider, cwd, and session context details.
  • If you pass a path, Lemon will register it as a project named after the last path segment (e.g. ~/dev/lemon => project lemon).

XMTP (Base App / Wallet Chats)

Lemon can run as an XMTP bot through the lemon_channels XMTP adapter.

1. Install XMTP bridge dependencies

./bin/lemon-xmtp-bootstrap

This installs bridge dependencies in apps/lemon_gateway/priv/node_modules (where xmtp_bridge.mjs resolves imports).

2. Configure gateway XMTP

[gateway]
enable_xmtp = true

[gateway.xmtp]
env = "production"                  # production | dev | local
wallet_address = "${XMTP_WALLET_ADDRESS}"
wallet_key_secret = "XMTP_WALLET_KEY"  # secret ref; env fallback works if XMTP_WALLET_KEY is set
db_path = "~/.lemon/xmtp-db"
poll_interval_ms = 1500
connect_timeout_ms = 15000
require_live = true                 # production default: do not allow mock fallback
mock_mode = false                   # set true only for local bridge testing
# Optional:
# api_url = "https://api.xmtp.network"
# inbox_id = "..."
# sdk_module = "@xmtp/node-sdk"

Notes:

  • When enable_xmtp = true, Lemon auto-registers and starts the XMTP channel adapter.
  • require_live = true keeps health/readiness red unless the bridge is truly live (not mock mode).
  • wallet_key_secret is the canonical credential field. It can point to a Lemon secret name or to an env var name when using secret resolution with env fallback.
  • Non-text XMTP messages currently receive a text-only fallback response.

Voice

Voice transport is configured under [gateway.voice].

[gateway.voice]
enabled = true
websocket_port = 4047
public_url = "https://example.com"
twilio_account_sid_secret = "twilio_account_sid"
twilio_auth_token_secret = "twilio_auth_token"
twilio_phone_number = "+1234567890"
deepgram_api_key_secret = "deepgram_api_key"
elevenlabs_api_key_secret = "elevenlabs_api_key"
elevenlabs_voice_id = "21m00Tcm4TlvDq8ikWAM"
elevenlabs_output_format = "ulaw_8000"
llm_model = "gpt-4o-mini"
system_prompt = "You are a helpful phone assistant."
max_call_duration_seconds = 600
silence_timeout_ms = 5000

Canonical voice settings are loaded from gateway.voice. Legacy :lemon_gateway app env fallbacks remain only as temporary compatibility shims and should not be used for new setup.

Telegram Voice Transcription

If enabled, Telegram voice notes are transcribed and the transcript is routed as a normal text message.

[gateway.telegram]
voice_transcription = true
voice_transcription_provider = "openai_transcribe"       # "openai_transcribe" | "local_transcript"
voice_transcription_model = "gpt-4o-mini-transcribe"  # optional
voice_max_bytes = 10485760                            # optional (default: 10MB)

# Optional OpenAI-compatible overrides (defaults to providers.openai)
voice_transcription_base_url = "https://api.openai.com/v1"
voice_transcription_api_key = "sk-..."

Use voice_transcription_provider = "local_transcript" for deterministic no-credential voice-note proof. It routes a local transcript preview through the normal Telegram inbound path and does not require an API key. Use openai_transcribe for real speech-to-text.

The deterministic local proof runner writes a redacted artifact under .lemon/proofs/:

MIX_ENV=test mix run scripts/live_telegram_voice_local_smoke.exs

When that artifact is present, mix lemon.doctor --verbose reports channels.telegram.voice_transcription as a passing readiness check. If voice transcription is enabled with local_transcript and the proof is missing, doctor tells the operator to run the local smoke.

Telegram File Transfer

Enable /file put and /file get (and optional auto-save for plain document uploads).

[gateway.telegram.files]
enabled = true
auto_put = true
auto_put_mode = "upload"  # "upload" | "prompt"
auto_send_generated_files = true       # optional: send generated files automatically after a run
auto_send_generated_max_files = 3      # optional: max generated files auto-sent per run (default: 3)
uploads_dir = "incoming"
media_group_debounce_ms = 1000  # optional (default: 1000ms)

# Optional safety rails
allowed_user_ids = [123456789]  # if empty, group uploads require admin
deny_globs = [".git/**", ".env", ".envrc", "**/*.pem", "**/.ssh/**"]
max_upload_bytes = 20971520     # optional (default: 20MB)
max_download_bytes = 52428800   # optional (default: 50MB)
outbound_send_delay_ms = 1000   # optional: delay between auto-sent files/batches to reduce 429s

Commands:

  • /file put [--force] <path>: upload a Telegram document into the active working root.
  • /file get <path>: fetch a file (or zip a directory) from the active working root back into Telegram.

If no project is bound for the chat, the active root falls back to gateway.default_cwd (or ~/).

When auto_send_generated_files = true, Lemon tracks generated files requested by browser/media tools and sends up to auto_send_generated_max_files files back to Telegram automatically at completion (using the same max_download_bytes limit as /file get). auto_send_generated_images remains accepted as a backward-compatible alias. SVG outputs are uploaded as Telegram documents instead of photos because Telegram rejects SVG photo processing.

Discord File Transfer

Enable Discord file transfer and optional generated-file auto-send.

[gateway.discord.files]
enabled = true
auto_put = true
auto_send_generated_files = true       # optional: send generated files automatically after a run
auto_send_generated_max_files = 3      # optional: max generated files auto-sent per run (default: 3)

# Optional safety rails
max_upload_bytes = 26214400            # optional (default: 25MB)
max_download_bytes = 26214400          # optional (default: 25MB)

When auto_send_generated_files = true, Lemon tracks generated files requested by browser/media tools and sends up to auto_send_generated_max_files files back to Discord automatically at completion. Generated files must fit within max_download_bytes; explicit file-send requests are still delivered through the normal attachment path. auto_send_generated_images remains accepted as a backward-compatible alias.

Discord Trigger Mode and Message Content Intent

Discord defaults to mention-gated routing in group channels and public threads. Use /trigger all to opt a channel or thread into free-response routing for unmentioned messages, and /trigger mentions to restore the default behavior.

[gateway.discord]
message_content_intent_enabled = true  # diagnostics declaration only
default_account_id = "default"          # optional account for ./bin/lemon send account-scoped lookups
default_channel_id = "1475727416549969980"  # optional default for ./bin/lemon send --to discord
default_thread_id = "1475727416549969991"   # optional thread

message_content_intent_enabled does not change Discord application settings. It lets channel_diagnostics.json record that the operator has enabled the privileged Message Content Intent in the Discord Developer Portal. Free-response Discord support requires that portal setting plus a passing live external-sender proof; mention-triggered Discord prompts do not depend on this declaration.

Telegram Context Compaction

When a Telegram run approaches the model context limit, Lemon can proactively mark the session for compaction so the next user message is automatically rewritten with a compact transcript and sent as a fresh session.

[gateway.telegram.compaction]
enabled = true
context_window_tokens = 400000  # optional override; if unset Lemon infers from model/engine
reserve_tokens = 16384          # optional safety margin before limit
trigger_ratio = 0.9             # optional; 0.9 means trigger at 90% of context window

Trigger Mode (Mentions-Only)

In Telegram group chats, you can gate runs so Lemon only triggers when explicitly invoked:

  • /trigger: show current trigger mode.
  • /trigger mentions: only run on @botname, reply-to-bot, or slash commands.
  • /trigger all: run on all messages.
  • /trigger clear: clear a topic override (forum topics only).
  • /cwd [project_id|path|clear]: show, set, or clear the chat/topic working directory override used by future /new sessions.

Forum topic management:

  • /topic <name>: create a new topic in the current Telegram forum supergroup.