djangoplay-web / Apps / Local Development Bring-Your-Own-Key (BYOK)
DocsDjangoPlay WebAppsLocal Development Bring-Your-Own-Key (BYOK)

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.

4 min readApplies to v1.2.2Added in 1.2.2
On this page ▾
  1. 1. Goal
  2. 2. Decisions
  3. 3. Design
  4. 3.1 Two layers, on purpose
  5. 3.2 Gating: DJANGO_SETTINGS_MODULE, not DEBUG
  6. 3.3 File format
  7. 3.4 Loading (load_local_ai_dev_overrides)
  8. 4. Acceptance criteria
  9. 5. Documentation
  10. 6. Related Files

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.py scaffolding command — a short dedicated doc plus a README.md pointer 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

  1. Baseline (already existed, generic): paystream.app_settings.common.get_decrypted_value() already falls back to ~/.dplay/.secrets for any unset setting when that value is absent from os.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 in README.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.).
  2. 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/.secrets file and flip between them by changing one line — AI_LOCAL_ACTIVE_PROFILE. Unlike the baseline layer, these values win over .env/os.environ for 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

plaintext
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-mini
  • AI_LOCAL_ACTIVE_PROFILE picks 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, suggesting chmod 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/.secrets file, behavior is unchanged from before this feature.
  • With the file present and running under paystream.settings.dev, the active profile's AI_* 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 (plain AI_CUSTOM_* lines already work via the baseline layer).
  • Referenced from README.md.
  • paystream/app_settings/local_ai_dev.py — new.
  • paystream/app_settings/ai.py — applies load_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.