Local Development Bring-Your-Own-Key (BYOK)
Depends on: nothing code-blocking; conceptually reuses the provider registry pattern from free-model-catalog-and-selection. Does NOT cover: production use — see production-byok-session-only.md.
On this page ▾
1. Goal
When a developer runs DjangoPlay locally, let them use their own paid AI provider API key (OpenAI, xAI, OpenRouter paid tier, or any OpenAI-compatible endpoint) without editing tracked settings files or committing secrets. This is local-machine-only, single-user, file-based — no browser, no session/tab isolation problem, no encrypted-in-transit design needed (that's the production-BYOK doc).
2. Decisions
- No interactive
manage.pyscaffolding command — a short dedicated doc plus aREADME.mdpointer is enough for v1. - Support switching between two personal API-key configurations, not just one, via named profiles in the same secrets file.
3. Design
3.1 Two layers, on purpose
- Baseline (already existed, generic):
paystream.app_settings.common.get_decrypted_value()already falls back to~/.dplay/.secretsfor any unset setting when that value is absent fromos.environ/.env— that's how a single personal key (AI_CUSTOM_API_KEY/AI_CUSTOM_BASE_URL/AI_CUSTOM_MODEL) already worked before this feature (documented inREADME.md§6.1). This layer is lowest-priority fallback, one flat value per setting, and has no opinion about dev vs. prod — it's shared plumbing used for every secret in the app (DB password, JWT signing key, etc.). - New, narrower layer (
paystream/app_settings/local_ai_dev.py): lets a developer keep two named AI provider profiles side by side in the same~/.dplay/.secretsfile and flip between them by changing one line —AI_LOCAL_ACTIVE_PROFILE. Unlike the baseline layer, these values win over.env/os.environfor the AI_* keys the active profile sets (a true override, not a lowest-priority fallback).
3.2 Gating: DJANGO_SETTINGS_MODULE, not DEBUG
This module is imported from paystream/app_settings/ai.py, which loads
before paystream/settings/base.py assigns DEBUG. So DEBUG isn't
available yet at the point this code runs, and reading
django.conf.settings.DEBUG here would recurse into Django's own settings
loading before it's finished. DJANGO_SETTINGS_MODULE is already resolved
into os.environ before any settings module loads (via
paystream.settings.bootstrap.resolve_settings_module(), called from
manage.py/wsgi.py/celery.py), so checking for the literal string
"paystream.settings.dev" is a safe, import-order-independent proxy for
"this is the settings module that sets DEBUG = True" — today the only
one that does (staging.py and prod.py both hardcode DEBUG = False).
This means: if the file exists but DJANGO_SETTINGS_MODULE isn't
paystream.settings.dev, nothing in this module is read — not even to
check existence.
3.3 File format
AI_LOCAL_ACTIVE_PROFILE=personal
AI_LOCAL_PROFILE_personal__AI_PROVIDER=custom
AI_LOCAL_PROFILE_personal__AI_CUSTOM_BASE_URL=https://openrouter.ai/api/v1
AI_LOCAL_PROFILE_personal__AI_CUSTOM_API_KEY=sk-or-v1-xxxxxxxx
AI_LOCAL_PROFILE_personal__AI_CUSTOM_MODEL=nvidia/nemotron-3.5-lightning:free
AI_LOCAL_PROFILE_work__AI_PROVIDER=openai
AI_LOCAL_PROFILE_work__AI_OPENAI_API_KEY=sk-xxxx
AI_LOCAL_PROFILE_work__AI_OPENAI_MODEL=gpt-4.1-miniAI_LOCAL_ACTIVE_PROFILEpicks which block is live. Change this one line and restart the dev server to switch — nothing else touched.- Each block is
AI_LOCAL_PROFILE_<name>__<SETTING>.<name>can be anything except containing__or=. - Only a fixed allowlist of AI_* keys can be set this way — provider,
base URL, API key, model, for each of the four provider slots
(
AI_PROVIDER,AI_XAI_*,AI_OPENAI_*,AI_OPENROUTER_*,AI_CUSTOM_*). Numeric/limit settings (AI_MAX_TOKENS, rate limits, token budget) are deliberately not overridable this way — this mechanism is about switching which provider/key, not tuning behavior per profile. - Unrecognized keys under a profile are ignored, logged as a warning (key name only, never a value).
- Same tolerant parsing as the baseline secrets-file loader: blank lines
and lines without
=are silently skipped.
3.4 Loading (load_local_ai_dev_overrides)
- Returns
{}immediately if_is_local_dev()is false — no read, no log line referencing the file's existence, nothing. - Parses the file, resolves the active profile's prefix, and returns only the recognized-key overrides for that profile.
_warn_if_permissive()— best-effort nudge (not a hard requirement) if the file is group/other readable, suggestingchmod 600.- Logs (INFO) which profile was applied and how many settings were overridden — never a raw value. Warns (not errors) if the active profile has no matching keys at all.
4. Acceptance criteria
- With no
~/.dplay/.secretsfile, behavior is unchanged from before this feature. - With the file present and running under
paystream.settings.dev, the active profile'sAI_*values override.env/environment defaults for the keys it sets. - With the file present under any non-dev settings module (staging, prod), the file is never read by this module.
- No secret value from this file is ever logged.
- Switching between two profiles is a one-line edit
(
AI_LOCAL_ACTIVE_PROFILE), no other file changes needed. - Unknown/unsupported override keys are ignored with a warning, not a hard failure.
5. Documentation
docs/system/aicore/local-ai-byok.md— full file-format reference and the reasoning above, written for a developer setting this up for the first time; explicitly notes that a single personal key doesn't need any of this (plainAI_CUSTOM_*lines already work via the baseline layer).- Referenced from
README.md.
6. Related Files
paystream/app_settings/local_ai_dev.py— new.paystream/app_settings/ai.py— appliesload_local_ai_dev_overrides()on top of the normal setting resolution.docs/system/aicore/local-ai-byok.md— new.README.md— pointer to the new doc.