# ═══════════════════════════════════════════════════════════════ # ai.openvibe.services — OpenVibe.AI Environment Configuration # The public home and API origin. BASE_URL defaults to it in production. # Copy to .env and fill in values. In production this lives at /etc/openvibe/ai.env (0600). # Secrets are read by NAME only (providers store 'env:NAME' references); nothing logs or returns them. # ═══════════════════════════════════════════════════════════════ # Server PORT=4700 HOST=127.0.0.1 NODE_ENV=development BASE_URL=http://localhost:4700 # The human site: the moved public pages answer 301 here (defaults to https://openvibe.exchange). The API, # a run's receipt and the operator console stay on this origin. #OV_EXCHANGE_URL=https://openvibe.exchange # Exact browser origins allowed to read GET /stats and /release.json (and their GET preflight). # Defaults to https://ai.openvibe.services,https://openvibe.services; no credentials or token/console CORS. #AI_CORS_ORIGINS=https://ai.openvibe.services,https://openvibe.services # PostgreSQL (ADR-035), written by OpenVibe.Host roles/data/add-service.sh ai: the runtime role through PgBouncer, # the owner on a direct connection (migrations), and Valkey (shared limit counters). # Without DATABASE_URL, development uses an embedded PGlite database in data/pglite. DATABASE_URL= DATABASE_DIRECT_URL= VALKEY_URL= VALKEY_PREFIX= # Identity: OpenVibe.Network signs the service tokens callers present (audience openvibe.ai). # The public key is fetched from /api/.well-known/jwks by openvibe-sdk/auth # (fresh keys, the last good ones through an outage, backoff, rotation); OV_NETWORK_PUBLIC_KEY pins a PEM instead. OV_NETWORK_URL=https://openvibe.network OV_NETWORK_INTERNAL_URL=http://127.0.0.1:4000 #OV_NETWORK_JWKS_URL=http://127.0.0.1:4000 #OV_NETWORK_ISSUER=https://openvibe.network # Namespaces fail closed: a token runs only the namespaces its `ns` claim (its Network grant) names; a token # without `ns` runs nothing. (AI_NS_FALLBACK and AI_NS_REQUIRED were retired on 2026-09-28.) #OV_NETWORK_PUBLIC_KEY= # ── Operator console (/console; README "Operator console") ── # Network SSO as OAuth client `ai` (the client AI's service tokens use). The Network must list # ${BASE_URL}/auth/callback (https://ai.openvibe.services/auth/callback) as a redirect URI of client `ai`. # OV_OAUTH_CLIENT_SECRET is also what the events relay and the user modules authenticate with. #OV_OAUTH_CLIENT_ID=ai #OV_OAUTH_CLIENT_SECRET= # Signs the sign-in flow cookie and keys the client-address hashes: at least 32 random characters # (openssl rand -hex 32). Required in production: without it or the client secret the console answers 503. #AI_CONSOLE_SESSION_SECRET= # Absolute console session lifetime in minutes (5-720) #AI_CONSOLE_SESSION_TTL_MIN=60 # A person's run-receipt session lifetime in minutes (5-10080) #AI_PERSON_SESSION_TTL_MIN=1440 # Audience a Network user token must carry #AI_SSO_AUDIENCE=openvibe.network # ── Events (the ai.run.* outbox relay and the Bench ratings subscription) ── # EVENTS_URL is where AI publishes its own events and ensures its subscription. AI_EVENTS_SECRET is the # v2 signing secret Events hands back on that subscription (32+ characters; comma-separated during rotation); # without it POST /internal/events is off. AI_EVENTS_SUBSCRIBE=0 skips creating the subscription (an # operator/script may create topic bench.rating.changed → http://127.0.0.1:4700/internal/events). #EVENTS_URL=http://127.0.0.1:4300 #AI_EVENTS_SECRET= #AI_EVENTS_SUBSCRIBE=on # Where a sparse bench.rating.changed is filled from (boards, cost per solved). #AI_BENCH_URL=http://127.0.0.1:5230 #AI_BENCH_TIMEOUT_MS=15000 # ── Billing (plan T5 lane F) ── # One platform.usage-sample@1 reading per accounted run, posted to billing.usage.record with AI's # service token, so Network must grant client `ai` billing.usage.record on openvibe.billing. Readings # are queued from the start. In production they are sent to colocated Billing at 127.0.0.1:4600 by # default; OV_BILLING_INTERNAL_URL overrides the address, and USAGE_SAMPLES=off stops queueing them. #OV_BILLING_INTERNAL_URL=http://127.0.0.1:4600 #USAGE_SAMPLES=on #USAGE_SAMPLES_INTERVAL_MS=2000 # ── Billing Balance (AI Exchange F, CHECKPOINT D2/D9/D10/D12), off by default ── # A request on a paid managed route (the model catalog's provider_access OWN_DEPLOYMENT or AUTHORIZED_WHOLESALE) # holds its worst-case cost before a provider is called, extends the hold while streaming and settles once. # Network must grant client `ai` billing.credit.hold.{create,extend,settle,release} on openvibe.billing. An app # holds against its project's Balance (subject app + project_id); a person against their own (no project_id); no # network.project.read is needed. # Reads OV_BILLING_INTERNAL_URL, OV_NETWORK_INTERNAL_URL and OV_OAUTH_CLIENT_SECRET above; a missing Billing refuses # paid requests (503). A settle Billing could not take is retried from credit_settle_queue with backoff. #AI_CREDITS=0 #AI_CREDITS_TIMEOUT_MS=5000 #AI_CREDITS_SETTLE_RETRY_MS=5000 #AI_CREDITS_SETTLE_MAX_RETRY_MS=300000 # ── Tier budgets (server/govern.js, T6 step 8): openvibe-sdk/govern in Valkey (VALKEY_URL above), off by default ── # Units ai-token and ai-usd per subject and per project; tiers free, paid, staff. See README "Tier budgets". #AI_GOVERN_TIERS=0 #AI_GOVERN_POLICY_JSON= #AI_GOVERN_TIER_MAP= #AI_GOVERN_DEFAULT_TIER=free #AI_GOVERN_HOLD_TOKENS=4000 #AI_GOVERN_HOLD_USD=0.02 #AI_GOVERN_TIMEOUT_MS=1000 # ── A person's own provider keys (server/credentials.js, WS-O task 2) ── # 64 hex characters (openssl rand -hex 32). Keys stored through PUT /api/v1/credentials/:subject are AES-256-GCM # encrypted with it; unset turns the credentials routes and credentialed runs off (503). Changing it makes every # stored key unreadable: the storing service then has to send them again. #AI_CREDENTIALS_KEY= # ── AI owner settings (D11, server/settings.js; openvibe-shared/config) ── # The ai.exchange and ai.provider_keys namespaces. OV_CONFIG_KEKS is ":[,:…]" and # OV_CONFIG_KEK_CURRENT names the one to use; they come from the host's env file, never the database. With a KEK, # secret values (each provider's ai.provider_keys ".key") are sealed envelopes in the config tables and a # DB dump has no key; unset, the store still works and reports "sealed": false. At boot AI moves the old # provider_secrets rows (AI_CREDENTIALS_KEY) into sealed ".key" values, bound to each provider's origin, # and deletes them after a successful read-back. #OV_CONFIG_KEKS= #OV_CONFIG_KEK_CURRENT= # ── Memory (server/memory/, Memory M2a) ── # 64 hex characters (openssl rand -hex 32). Memory item text and source references are AES-256-GCM sealed with it; # the dedupe MAC is HKDF-SHA256 (info "memory-mac") of the same key. Unset turns /api/v1/memory off (503 # memory.unavailable). A key of its own, so credentials and memory rotate separately. Do not set it in production until # the Memory M2a review; changing it makes existing items unreadable. #AI_MEMORY_KEY= # The store's own maintenance (Memory M2c): the loop walks owner by owner, with a per-run cap. It is # off in test mode and off while a provider is in a drill, so a drill never spends the platform's budget. #AI_MEMORY_JOBS=1 #AI_MEMORY_JOB_INTERVAL_MS=900000 #AI_MEMORY_JOB_MAX_OWNERS=10 #AI_MEMORY_JOB_MAX_SCOPES=4 #AI_MEMORY_JOB_MAX_EMBEDDINGS=100 #AI_MEMORY_JOB_MAX_MODEL_CALLS=20 #AI_MEMORY_JOB_MAX_PROFILES=10 # ── The shared provider — the same names as OpenVibe.Live's admin AI settings ── # (ai_enabled, ai_provider, ai_base_url, ai_api_key, ai_model, ai_pricing_json, # ai_input/output_cost_per_mtok, ai_max_cost_usd_per_day). #AI_ENABLED=true # anthropic | openai | openrouter | groq | ollama | (blank = OpenAI-shaped). Anthropic only when # AI_BASE_URL is blank or anthropic.com; any other base URL is treated as OpenAI-compatible. AI_PROVIDER= #AI_BASE_URL=https://api.openai.com/v1 AI_API_KEY= # Blank = claude-sonnet-5 for anthropic, gpt-4o-mini otherwise (Live's defaults) AI_MODEL= #AI_EMBEDDING_MODEL=text-embedding-3-small # Image generation (POST /api/v1/image, route default.image): off unless set. The shared provider's image model. #AI_IMAGE_MODEL=gpt-image-1 #AI_IMAGE_SIZE=1024x1024 #AI_PROVIDER_TIMEOUT_MS=30000 # USD per million tokens, longest model-prefix match: {"gpt-5-nano":{"in":0.05,"out":0.4,"cached":0.005}} # Each entry may also carry its platform.rate-card@1 terms: source (URI), verified_at, effective_from (YYYY-MM-DD), # free_allowance (tokens per metric) and reset_period (day|month|none). Unset = unknown (1970-01-01, urn:openvibe:ai:unverified:*). #AI_PRICING_JSON= #AI_INPUT_COST_PER_MTOK=3 #AI_OUTPUT_COST_PER_MTOK=15 # ── DeepSeek as its own provider (chat models; the same key research uses) ── # With DEEPSEEK_API_KEY (or AI_DEEPSEEK_KEY_REF) set, boot registers provider `deepseek` and its two chat # models (deepseek-flash, deepseek-v4-pro) with prices from DeepSeek's own price page. Without the key it # stays disabled and its models are not offers. #AI_DEEPSEEK_ENABLED=true #AI_DEEPSEEK_BASE_URL=https://api.deepseek.com #AI_DEEPSEEK_KEY_REF=env:DEEPSEEK_API_KEY #AI_DEEPSEEK_TIMEOUT_MS=90000 # ── Spend safety for runs nobody pays for ── # The free allowance (a service naming a person, without their own token) and the anonymous Try it may use only # offers from these sources AND at or below this output price in USD per 1M tokens. A source is a provider key; # `local` covers every local model server (local, local2, whisper.cpp) and `community` the community machines. # Anything else needs the person's own Balance (their own token) or their own key; the router answers 402 # billing.payer_required instead. Default providers: deepseek,local,community. Default ceiling: 1.5. #AI_FREE_PROVIDERS=deepseek,local,community #AI_FREE_MAX_OUTPUT_PRICE_USD_PER_MTOK=1.5 # ── Abuse prevention for the free tier (plan E5; server/abuse/, docs/abuse-prevention.md) ── # The full daily allowance (verified tier); a new account gets a quarter, over 7 days half, kudos add up to # one more full. Zero turns the free allowance off. The salt keys the device cookie and every hash; set it. #AI_FREE_DAILY_TOKENS=20000 #AI_ABUSE=1 #AI_ABUSE_SALT= # Network trust signals (account age, passkey, links, 30-day activity) through the identity route. #AI_ABUSE_NETWORK=1 # Only these service-token ids may forward x-ov-client-ip / x-ov-device-id (the Exchange). #AI_FIRST_PARTY_SERVICES=exchange # Set for openvibe.exchange so the site and api.openvibe.exchange share the signed device cookie. #AI_DEVICE_COOKIE_DOMAIN=.openvibe.exchange # The local monthly iptoasn.com table (node scripts/update-ip2asn.js); never a paid API. #AI_IP2ASN_V4=/srv/openvibe/ai/data/ip2asn/ip2asn-v4.tsv #AI_IP2ASN_V6=/srv/openvibe/ai/data/ip2asn/ip2asn-v6.tsv #ASNs an operator knows are hosting ones, on top of the organisation-name match. #AI_HOSTING_ASNS= #AI_ABUSE_RATE_PER_MIN=60 #AI_ABUSE_HOSTING_RATE_PER_MIN=20 # Free tokens per minute against the week's per-minute baseline, per ASN, device and input bucket. #AI_ABUSE_VELOCITY_FACTOR=10 #AI_ABUSE_VELOCITY_MIN_TOKENS_PER_MIN=10000 # In-house hashcash for the anonymous Try it (no account); required in production, off in tests. #AI_POW_REQUIRED=1 #AI_POW_BITS=18 #AI_POW_TTL_MS=300000 #AI_POW_CHALLENGES_PER_MIN=10 # Optional local HTTP seam provider (POST {operation, request} -> {text, json, usage}) #AI_HTTP_SEAM_URL=http://127.0.0.1:4799/complete # A local model server speaking the OpenAI API (llama.cpp llama-server, Ollama /v1): provider `local`, route # media.local, used by media.analyze's overview. No key; never a paid provider. #AI_LOCAL_LLM_URL=http://127.0.0.1:8090/v1 # media.analyze reads a recording where it lies through a loopback reader (server/media-proxy.js): nothing is # downloaded, and speech windows seek with range requests. 0 downloads it first (the disk cap then applies). #AI_MEDIA_STREAM=1 # Object storage Media redirects offloaded recordings to (presigned URLs), followed only from Media itself. #AI_MEDIA_STORAGE_HOSTS=*.backblazeb2.com,*.r2.cloudflarestorage.com,edge.openvibe.media #AI_LOCAL_LLM_MODEL=qwen2.5-1.5b-instruct #AI_LOCAL_LLM_TIMEOUT_MS=180000 # The host's model is a last offer for short text only: never more than this many tokens, and one generation at # a time across local and local2, so an outage upstream cannot starve the host. #AI_LOCAL_LLM_MAX_TOKENS=512 #AI_LOCAL_LLM_CONCURRENCY=1 # A second local chat model server (its own llama.cpp process and another model family): provider `local2`, # so a caller can pin two local offers, e.g. Games' AI-vs-AI matches. Same rules: no key, free, no JSON. #AI_LOCAL_LLM2_URL=http://127.0.0.1:8092/v1 #AI_LOCAL_LLM2_MODEL=llama-3.2-1b-instruct #AI_LOCAL_LLM2_TIMEOUT_MS=180000 # A second local OpenAI-compatible server for embeddings, when the chat model's llama.cpp process cannot also # serve the embedding GGUF (llama.cpp serves one model per process). Default: AI_LOCAL_LLM_URL. #AI_LOCAL_EMBED_URL=http://127.0.0.1:8091/v1 # An embedding model the local server answers at /embeddings: `local` then serves default.embedding as its free # member. The server must really serve embeddings (llama-server only with --embedding and an embedding GGUF; a # chat-only model answers 400 and every embedding call falls over to the shared provider). Vector dimensions # differ per upstream: never compare or mix vectors from different providers. Unset: no local embeddings. #AI_LOCAL_EMBED_MODEL=bge-small-en-v1.5 # media.analyze: file cap (also bounded by free temp disk minus the reserve), download timeout, scene threshold #AI_MEDIA_ANALYSIS_DIR=/var/lib/openvibe-ai/media-tmp (default: data/media-tmp; never a tmpfs) #AI_MEDIA_ANALYSIS_MAX_BYTES=4294967296 #AI_MEDIA_ANALYSIS_DISK_RESERVE_BYTES=3221225472 #AI_MEDIA_ANALYSIS_FETCH_TIMEOUT_MS=1200000 #AI_MEDIA_SCENE_THRESHOLD=10 #FFMPEG_BIN=ffmpeg #FFPROBE_BIN=ffprobe # Let routes fall back to the deterministic stub when every real provider fails. Default: on outside # production, off in production (synthetic output must never stand in for a real answer there). #AI_STUB_FALLBACK=false # ── AI Exchange P4 (CHECKPOINT D3): the truthful model catalog, exact ids and dynamic aliases ── # Off: today's behaviour (one virtual `openvibe` model, no disclosure object). On: # - GET /v1/models lists the registry's real models in the ai.model@1 shape plus the dynamic aliases # openvibe/auto, openvibe/economy, openvibe/balanced, openvibe/fast, openvibe/private (legacy openvibe = auto); # - `model` may be an exact id `/[:]@` (it runs only that artifact, or fails with # 400 ai.model_unavailable / 404 ai.model_not_found, and never substitutes another model), or one of those aliases; # - every response carries the `openvibe` disclosure (a leading SSE comment and the final chunk when streaming); # - paid managed routing uses only offers whose provider_access (API /api/v1/providers) is OWN_DEPLOYMENT or # AUTHORIZED_WHOLESALE. RESTRICTED and UNVERIFIED (the default) offers are not resold; a person's own key is # CUSTOMER_BYOK and never goes through that gate. # Exact ids need artifact metadata on the model row (POST/PATCH /api/v1/models `metadata`: org, model, quant, # revision, family, variant, sha256, license, rate_version). #AI_MODEL_CATALOG=1 # ── Local speech-to-text: whisper.cpp, the same names Live uses ── #WHISPER_BIN=/home/ubuntu/whisper.cpp/build/bin/whisper-cli #WHISPER_MODEL=/home/ubuntu/whisper.cpp/models/ggml-base.en.bin #WHISPER_MODEL_LIVE= #WHISPER_MODEL_MULTI=/home/ubuntu/whisper.cpp/models/ggml-small.bin #WHISPER_VAD_MODEL= #WHISPER_VAD=1 #WHISPER_THREADS=4 #WHISPER_BEAM=1 #WHISPER_MAX_CONCURRENT=1 # NVIDIA Parakeet TDT 0.6B v3 for English and 24 European languages (whisper keeps the rest and takes any failure). # Set the model to turn it on; the binary defaults to parakeet-cli next to WHISPER_BIN. ASR_ENGINE=whisper forces whisper. #PARAKEET_MODEL=/home/ubuntu/whisper.cpp/models/ggml-parakeet-tdt-0.6b-v3-q8_0.bin #PARAKEET_BIN= #ASR_ENGINE= # ── Quotas seeded at boot (admin API edits them afterwards; 0 = none) ── # Live's ai_max_cost_usd_per_day: one daily spend cap across every caller AI_MAX_COST_USD_PER_DAY=0 # Requests per minute / per day for EACH calling service #AI_QUOTA_SERVICE_RPM=600 #AI_QUOTA_SERVICE_RPD=50000 # ── Runs, cache, logs ── #AI_MAX_WAIT_MS=60000 #AI_MAX_CONCURRENT_RUNS=4 # Runs waiting for a slot; past either cap a new run is refused (429 queue.full, Retry-After) #AI_MAX_QUEUED_RUNS=200 #AI_MAX_QUEUED_RUNS_PER_CALLER=50 #AI_CACHE_TTL_SEC=604800 #AI_MAX_INPUT_BYTES=6291456 #AI_RUN_RETENTION_DAYS=30 # Compiled-schema cache (LRU shared by every caller; ai.extract/ai.enrich schemas are caller-supplied) #AI_SCHEMA_CACHE_MAX=500 #AI_SCHEMA_CACHE_MAX_BYTES=8388608 # Raw prompt/response logging: opt-in debugging only, and only for runs whose caller sets options.debug #AI_DEBUG_RAW_LOG=false #AI_BREAKER_FAILURES=3 # The pause after a run of transient failures (timeouts, 5xx, a broken stream): 30 seconds, doubling on every # failed probe up to 10 minutes. Per provider+model (migrations/0060), so another offer still answers. #AI_BREAKER_BACKOFF_MS=30000 #AI_BREAKER_MAX_BACKOFF_MS=600000 # A provider that answered "insufficient balance" (HTTP 402, or a 429/403 insufficient_quota) or refused its key # (401/403) opens that offer's circuit at once. This long after it opened (15 minutes) the next real request may # probe it once; it stays open until a probe succeeds or the provider's key changes. #AI_BREAKER_EXHAUSTED_COOLDOWN_MS=900000 # The owner's Network subject (usr_…): one notification per open balance/auth circuit, through the Network # notification push. Unset means no push; the console, the catalogue and the metrics still say what happened. #AI_OWNER_SUBJECT= #AI_PROVIDER_RETRIES=1 #AI_PROVIDER_RETRY_DELAY_MS=800 # Hourly balance probe (DeepSeek's GET /user/balance) exposed as ai_provider_balance_usd{provider} on /metrics; # the operator alert fires below $2. Off under a restore drill (AI_DRILL=1). #AI_BALANCE_PROBE=1 #AI_BALANCE_PROBE_INTERVAL_MS=3600000 #AI_BALANCE_PROBE_TIMEOUT_MS=10000 # ── Media inputs: only these https hosts are fetched (plus the Media internal origin) ── OV_MEDIA_URL=https://openvibe.media #OV_MEDIA_INTERNAL_URL=http://127.0.0.1:4100 #AI_FETCH_ALLOW_HOSTS=openvibe.media,openvibe.live,openvibe.network,*.openvibe.network,openvibe.community,openvibe.tools,openvibe.games #AI_FETCH_MAX_BYTES=536870912 #AI_FETCH_MAX_IMAGE_BYTES=12582912 #AI_FETCH_TIMEOUT_MS=120000 # MegaSource research (server/research/, research.gather): web search asks the Search Fabric's web scope first # (OpenVibe.Search POST /api/v1/query) as AI's service principal, and falls back to DeepSeek when the fabric is off, # refuses or answers nothing. Network must grant `ai` search.query.delegate on audience openvibe.search (Search's # viewer requires it for a service token); the route's public capability is search.fabric.query. #OV_SEARCH_INTERNAL_URL=http://127.0.0.1:4710 #AI_RESEARCH_SEARCH=1 #AI_RESEARCH_SEARCH_TIMEOUT_MS=15000 # DeepSeek's paid search is the fallback. The key is read by name; set DEEPSEEK_API_KEY (or point # AI_RESEARCH_DEEPSEEK_KEY_REF elsewhere). #AI_RESEARCH_DEEPSEEK=1 #AI_RESEARCH_DEEPSEEK_KEY_REF=env:DEEPSEEK_API_KEY #AI_RESEARCH_DEEPSEEK_DAILY=1500 #AI_RESEARCH_TIMEOUT_MS=15000 # ── Exchange webhooks (server/exchange/webhooks.js): a developer registers an https receiver and AI signs # usage/earnings/offer.down with X-OpenVibe-Signature-V2, the header openvibe-sdk/exchange verifies. A delivery is # retried with an exponential backoff; the receiver URL goes through the egress guard. On by default. #AI_WEBHOOKS=1 #AI_WEBHOOK_TIMEOUT_MS=10000 #AI_WEBHOOK_MAX_BYTES=65536 #AI_WEBHOOK_MAX_ATTEMPTS=6 #AI_WEBHOOK_MAX_PER_OWNER=10 #AI_WEBHOOK_BASE_DELAY_MS=10000 #AI_WEBHOOK_MAX_DELAY_MS=3600000 #AI_WEBHOOK_TICK_MS=1000 # ── The 18+ board (server/willing/): two measurement jobs, both OFF by default and both budgeted ── # The willingness job asks every live text offer the published 60 lawful adult prompts (server/willing/datasets/ # prompts.json) and records the refusal and hedge rates, each with n and a Wilson interval. The # abliteration tax runs the objective set (server/willing/datasets/objective-set.json) on each declared # base/abliterated pair (server/willing/datasets/abliteration-pairs.json) and records the score difference with # its Newcombe interval. Neither spends anything until enabled AND given a budget above zero; GET # /api/v1/exchange/willingness?plan=1 and .../abliteration-tax?plan=1 are the dry runs. # An uncensored offer is asked under the adult tier, which needs a real 18+ measurement account # (usr_…); without it the plan is blocked instead of bypassing the 18+ gate. The hard floor still holds. #AI_WILLINGNESS_JOB=1 #AI_WILLINGNESS_BUDGET_USD=0 #AI_WILLINGNESS_SUBJECT= #AI_WILLINGNESS_INTERVAL_MS=604800000 #AI_WILLINGNESS_MAX_OFFERS=500 #AI_WILLINGNESS_OUTPUT_TOKENS=256 #AI_ABLITERATION_TAX=1 #AI_ABLITERATION_TAX_BUDGET_USD=0 #AI_ABLITERATION_TAX_SUBJECT= #AI_ABLITERATION_TAX_INTERVAL_MS=604800000 #AI_ABLITERATION_TAX_OUTPUT_TOKENS=256