peregrine/docs/reference/tier-system.md
pyr0ball f1194cacc9 docs: update tier-system reference with BYOK policy + demo user.yaml
docs/reference/tier-system.md:
  - Rewritten tier table: free tier now described as "AI unlocks with BYOK"
  - New BYOK section explaining the policy and rationale
  - Feature gate table gains BYOK-unlocks? column
  - API reference updated: can_use, tier_label, has_configured_llm with examples
  - "Adding a new feature gate" guide updated to cover BYOK_UNLOCKABLE

demo/config/user.yaml:
  - Reformatted by YAML linter; added dismissed_banners for demo UX
2026-03-02 13:22:10 -08:00

7.4 KiB

Tier System

Peregrine uses a three-tier feature gate system defined in app/wizard/tiers.py.


Tiers

free < paid < premium
Tier Description
free Core discovery pipeline, resume matching, basic UI. AI features unlock with BYOK.
paid Managed cloud LLM (no key required), integrations, calendar, notifications
premium Adds fine-tuning and multi-user support

BYOK — Bring Your Own Key

If you configure any LLM backend in config/llm.yaml — local (ollama, vllm) or an external API key (Anthropic, OpenAI, etc.) — all pure LLM-call features unlock automatically, regardless of your subscription tier.

The paid tier gives you access to CircuitForge's managed cloud inference. It does not gate your ability to use AI when you're providing the compute yourself.

Features that unlock with BYOK are listed in BYOK_UNLOCKABLE in tiers.py. Features that depend on CircuitForge-operated infrastructure (integrations, email classifier training, fine-tuned models) remain tier-gated.


Feature Gate Table

Features listed here require a minimum tier. Features not in this table are available to all tiers (free by default).

Wizard LLM generation

Feature key Minimum tier BYOK unlocks? Description
llm_career_summary paid yes LLM-assisted career summary generation in the wizard
llm_expand_bullets paid yes LLM expansion of resume bullet points
llm_suggest_skills paid yes LLM skill suggestions from resume content
llm_voice_guidelines premium yes LLM writing voice and tone guidelines
llm_job_titles paid yes LLM-suggested job title variations for search
llm_mission_notes paid yes LLM-generated mission alignment notes
llm_keywords_blocklist paid no Orchestration pipeline over background keyword data

App features

Feature key Minimum tier BYOK unlocks? Description
company_research paid yes Auto-generated company research briefs pre-interview
interview_prep paid yes Live reference sheet and practice Q&A during calls
survey_assistant paid yes Culture-fit survey Q&A helper (text + screenshot)
email_classifier paid no IMAP email sync with LLM classification (training pipeline)
model_fine_tuning premium no Cover letter model fine-tuning on personal writing
shared_cover_writer_model paid no Access to shared fine-tuned cover letter model (CF infra)
multi_user premium no Multiple user profiles on one instance

Integrations

Integrations depend on CircuitForge-operated infrastructure and are not BYOK-unlockable.

Feature key Minimum tier Description
notion_sync paid Sync jobs to Notion database
google_sheets_sync paid Sync jobs to Google Sheets
airtable_sync paid Sync jobs to Airtable
google_calendar_sync paid Create interview events in Google Calendar
apple_calendar_sync paid Create interview events in Apple Calendar (CalDAV)
slack_notifications paid Pipeline event notifications via Slack

Free integrations (not gated)

The following integrations are free for all tiers and are not in the FEATURES dict:

  • google_drive_sync — upload documents to Google Drive
  • dropbox_sync — upload documents to Dropbox
  • onedrive_sync — upload documents to OneDrive
  • mega_sync — upload documents to MEGA
  • nextcloud_sync — upload documents to Nextcloud
  • discord_notifications — pipeline notifications via Discord webhook
  • home_assistant — pipeline events to Home Assistant REST API

API Reference

can_use(tier, feature, has_byok=False) -> bool

Returns True if the given tier has access to the feature. Pass has_byok=has_configured_llm() to apply BYOK unlock logic.

from app.wizard.tiers import can_use, has_configured_llm

byok = has_configured_llm()

can_use("free", "company_research")               # False — no LLM configured
can_use("free", "company_research", has_byok=True) # True  — BYOK unlocks it
can_use("paid", "company_research")               # True

can_use("free", "notion_sync", has_byok=True)     # False — integration, not BYOK-unlockable
can_use("free", "unknown_feature")                # True  — ungated features return True
can_use("invalid", "company_research")            # False — invalid tier string

has_configured_llm(config_path=None) -> bool

Returns True if at least one non-vision LLM backend is enabled in config/llm.yaml. Local backends (ollama, vllm) and external API keys both count.

from app.wizard.tiers import has_configured_llm

has_configured_llm()  # True if any backend is enabled and not vision_service

tier_label(feature, has_byok=False) -> str

Returns a display badge string for locked features, or "" if the feature is free, unlocked, or BYOK-accessible.

from app.wizard.tiers import tier_label

tier_label("company_research")                    # "🔒 Paid"
tier_label("company_research", has_byok=True)     # ""  (BYOK unlocks, no label shown)
tier_label("model_fine_tuning")                   # "⭐ Premium"
tier_label("notion_sync", has_byok=True)          # "🔒 Paid"  (BYOK doesn't unlock integrations)
tier_label("job_discovery")                       # ""  (ungated)

Dev Tier Override

For local development and testing without a paid licence, set dev_tier_override in config/user.yaml:

tier: free
dev_tier_override: premium    # overrides tier locally for testing

UserProfile.tier returns dev_tier_override when set, falling back to tier otherwise.

!!! warning dev_tier_override is for local development only. It has no effect on production deployments that validate licences server-side.


Adding a New Feature Gate

  1. Add the feature to FEATURES in app/wizard/tiers.py. If it's a pure LLM call that should unlock with BYOK, also add it to BYOK_UNLOCKABLE:
FEATURES: dict[str, str] = {
    # ...existing entries...
    "my_new_llm_feature": "paid",
}

BYOK_UNLOCKABLE: frozenset[str] = frozenset({
    # ...existing entries...
    "my_new_llm_feature",   # add here if it's a pure LLM call
})
  1. Guard the feature in the UI, passing has_byok:
from app.wizard.tiers import can_use, tier_label, has_configured_llm

_byok = has_configured_llm()
if can_use(user.tier, "my_new_llm_feature", has_byok=_byok):
    # show the feature
    pass
else:
    st.info(f"Requires a paid plan or a configured LLM backend.")
  1. Add tests in tests/test_wizard_tiers.py covering both the tier gate and BYOK unlock:
def test_my_new_feature_requires_paid_without_byok():
    assert can_use("free", "my_new_llm_feature") is False
    assert can_use("paid", "my_new_llm_feature") is True

def test_my_new_feature_byok_unlocks():
    assert can_use("free", "my_new_llm_feature", has_byok=True) is True

Future: Ultra Tier

An ultra tier is reserved for future use (e.g. enterprise SLA, dedicated inference). The tier ordering in TIERS = ["free", "paid", "premium"] can be extended without breaking can_use(), since it uses list.index() for comparison.