Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
2000abc
docs(ai): add epic/ai-engine integration-branch strategy to spec
thewrz May 25, 2026
6783ae7
feat(ai): provider-agnostic LLM gateway + 3 connectors + recommendati…
thewrz May 25, 2026
c2d7659
feat(ai): add xAI Grok LLM provider adapter (#350)
thewrz May 25, 2026
1c119ff
feat(ai): add OpenRouter provider adapter (#352)
thewrz May 25, 2026
24712c0
feat(ai): add AWS Bedrock provider adapter (#353)
thewrz May 25, 2026
4e4c623
feat(ai): add Azure OpenAI provider adapter (#349)
thewrz May 25, 2026
4d36057
feat(ai): add Gemini provider adapter (#351)
thewrz May 25, 2026
3baf828
fix(ai): OpenAI/Azure use max_completion_tokens + surface real recomm…
thewrz May 25, 2026
27a9dd7
fix(ai): address CodeRabbit review on PR #354 (content blocks, SigV4 …
thewrz May 25, 2026
71a34e3
feat(ai): audit-trail admin UI tab (#341) (#360)
thewrz May 26, 2026
502c0d0
feat(ai): auto-fallback policy in gateway (#338) (#358)
thewrz May 26, 2026
fd7e650
feat(ai): DJ-readable LLM connector policy endpoint (#355) (#359)
thewrz May 26, 2026
eb0432f
fix(ai): harden openai-compatible base_url + tool-arg parsing (PR #35…
thewrz May 26, 2026
8fa64c4
fix(ai): address PR #354 review round 2 (CSV space-prefix, audit fall…
thewrz May 26, 2026
59a8bbb
feat(ai): move DJ AI connector settings into account page (#357) (#361)
thewrz May 27, 2026
c759caf
chore(ai): remove deprecated ANTHROPIC_API_KEY env-var reads (#343) (…
thewrz May 27, 2026
1ea99f2
feat(ai): configurable llm_call_log retention (#342) (#363)
thewrz May 27, 2026
d37fb5f
ci: ignore withdrawn false-positive MAL-2026-4750 in pip-audit
thewrz May 27, 2026
4e7fe92
test(verification): fix flaky tamper vector in verify-status test (#365)
thewrz May 28, 2026
648ae31
fix(sse): release pooled DB connection for SSE stream lifetime (#356)…
thewrz May 28, 2026
ba200b7
feat(ai): LLM adapter plug-in SDK with docs, skeleton, and optional l…
thewrz May 28, 2026
d48c64f
feat(ai): per-DJ explicit default LLM connector (#336) (#371)
thewrz May 28, 2026
2b8979f
feat(ai): background connector health monitor + surface result in adm…
thewrz May 28, 2026
b78aa73
test(sse): rebind SSE module SessionLocal to test engine
thewrz May 28, 2026
3465f5a
refactor(llm): extract shared adapter helpers to cut duplication (#373)
thewrz May 29, 2026
e043cf6
refactor(llm): dedupe connector endpoint + storage boilerplate (#374)
thewrz May 29, 2026
ed48fb4
refactor(ai-ui): data-drive placeholders + dedupe handlers/headers (#…
thewrz May 29, 2026
832feb1
refactor(tests): parametrize + dedupe LLM test boilerplate (#376)
thewrz May 29, 2026
365a237
feat(llm): streaming response support in the LLM gateway (#335) (#379)
thewrz May 29, 2026
5d8a9cd
feat(llm): per-feature connector preference (#337) (#378)
thewrz May 29, 2026
2231223
feat(llm): per-DJ monthly token caps (#339) (#377)
thewrz May 29, 2026
5a8b858
Merge branch 'main' into epic/ai-engine
thewrz Jun 3, 2026
b674ad8
Merge branch 'main' into epic/ai-engine
thewrz Jun 3, 2026
dc1cad4
fix(migrations): re-anchor 046 atop a11334c031bb so prod applies AI m…
thewrz Jun 7, 2026
e01d485
Merge branch 'main' into epic/ai-engine
thewrz Jun 9, 2026
9f7f379
fix(llm): harden SSE streaming + add monthly_token_cap DB guard
thewrz Jun 9, 2026
c440b3f
Merge main into epic/ai-engine: integrate WrzDJSet Phase 0 with AI ga…
thewrz Jun 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,20 @@ BOOTSTRAP_ADMIN_PASSWORD=your-secure-password
# TURNSTILE_SITE_KEY=your-site-key
# TURNSTILE_SECRET_KEY=your-secret-key

# =============================================================================
# LLM / AI providers
# =============================================================================
# LLM credentials are managed per-DJ via the gateway connector system
# (admin: /admin/ai, DJ: /settings/ai) — there is NO env-var credential path.
# The recommendation engine routes every call through the gateway, which
# resolves the actor DJ's connector (or the org default).
#
# Historical note: the one-shot Alembic data migration (046_admin_ai_oauth)
# reads ANTHROPIC_API_KEY *once* on first upgrade, converting it into a
# system-default "anthropic_apikey" connector. Once that migration has run on a
# deploy, the env var is no longer consumed at runtime and can be dropped. The
# legacy env-var fallback in the recommendation engine was removed in #343.

# =============================================================================
# Frontend (Next.js)
# =============================================================================
Expand Down
12 changes: 11 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,17 @@ jobs:
# PYSEC-2025-183 (pyjwt 2.10.1+ weak encryption, DISPUTED) - no fix released, the
# pyjwt maintainers contest the advisory. We already pin pyjwt to the
# latest available (2.12.1). Revisit when an upstream fix lands.
run: pip-audit --ignore-vuln CVE-2024-23342 --ignore-vuln CVE-2026-3219 --ignore-vuln CVE-2026-6357 --ignore-vuln PYSEC-2025-183
# MAL-2026-4750 (fastapi 0.136.3 "malicious code", WITHDRAWN by OSV 2026-05-26) -
# False positive. 0.136.3 is an official tiangolo release; the flagged
# dependency 'fastar' is a legitimate Rust-tar-bindings package
# (published Oct 2025, predates the release) and is pulled ONLY via
# fastapi's [standard] extra, which we do NOT install (we use plain
# fastapi + uvicorn[standard]) - so it never enters our dependency tree.
# We deliberately stay on 0.136.3 for its underscore-header rejection
# (PR #15589) and SSE field validation (PR #15588). OSV withdrew the
# advisory; pip-audit's feed still serves it. REMOVE this ignore once
# the withdrawn entry is purged from the feed.
run: pip-audit --ignore-vuln CVE-2024-23342 --ignore-vuln CVE-2026-3219 --ignore-vuln CVE-2026-6357 --ignore-vuln PYSEC-2025-183 --ignore-vuln MAL-2026-4750

- name: Run tests with coverage
env:
Expand Down
23 changes: 21 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ NEXT_PUBLIC_API_URL="http://LAN_IP:8000" npm run dev
- Encryption: `TOKEN_ENCRYPTION_KEY` (Fernet, 44 chars base64) — required in production for OAuth token encryption
- Beatport: `BEATPORT_CLIENT_ID`, `BEATPORT_CLIENT_SECRET`, `BEATPORT_REDIRECT_URI`, `BEATPORT_AUTH_BASE_URL`
- Soundcharts: `SOUNDCHARTS_APP_ID`, `SOUNDCHARTS_API_KEY` (song discovery for recommendations)
- Anthropic (LLM recommendations): `ANTHROPIC_API_KEY`, `ANTHROPIC_MODEL` (default: `claude-haiku-4-5-20251001`), `ANTHROPIC_MAX_TOKENS`, `ANTHROPIC_TIMEOUT_SECONDS`
- Anthropic (LLM recommendations): credentials live in the LLM Gateway connector system — there is **no env-var credential path**. The one-shot Alembic migration `046_admin_ai_oauth` reads `ANTHROPIC_API_KEY` *once* on first upgrade to seed a connector; the legacy env-var fallback in the recommendation engine was removed in #343. `ANTHROPIC_MODEL` (default: `claude-haiku-4-5-20251001`) is retained only as the default model-name label on recommendation responses and for the admin AI-settings/model-listing endpoints. The `ANTHROPIC_MAX_TOKENS` / `ANTHROPIC_TIMEOUT_SECONDS` settings were removed.

## Running CI Checks Locally

Expand Down Expand Up @@ -312,13 +312,32 @@ REJECTED → NEW (re-open)
- `server/app/services/track_normalizer.py` — track normalization & remix detection
- `server/app/services/version_filter.py` — filters unwanted versions (karaoke, demo) with fuzzy matching

### LLM Gateway (provider-agnostic dispatch)
- `server/app/services/llm/` — connector-based dispatch usable by any agentic feature:
- `gateway.py` — `Gateway.dispatch(db, actor, request, *, purpose)` resolves a connector (per-DJ MRU → org default → raise `NoLlmConfigured`) and routes through the matching adapter. Logs every call to `llm_call_log` (counts only — never prompt/completion content) and writes a `llm_audit_event` row for credential lifecycle events.
- `base.py` — canonical `ChatRequest` / `ChatResponse` / `ToolSpec` / `LlmAdapter` ABC
- `registry.py` — connector_type → adapter class lookup; auto-registers all adapters on import
- `tool_translation.py` — JSON-Schema ToolSpec ↔ per-provider tool/function shape + response parsers
- `url_validator.py` — validates custom OpenAI-compatible base URLs (HTTPS any host; HTTP loopback + RFC1918 only)
- `connector_storage.py` — CRUD + validation + audit/call logging helpers
- `exceptions.py` — `AuthInvalid` / `RateLimited` / `QuotaExceeded` / `ProviderUnavailable` / `ToolTranslationError` / `NoLlmConfigured`
- `adapters/openai_apikey.py` — OpenAI Platform API-key adapter (httpx-based)
- `adapters/openai_compatible.py` — Custom OpenAI-compatible endpoint (Hermes Agent, Ollama, vLLM, LMStudio)
- `adapters/anthropic_apikey.py` — Anthropic API-key adapter (uses the `anthropic` SDK)
- Models: `LlmConnector` (encrypted credentials via `EncryptedText`), `LlmCallLog`, `LlmAuditEvent`
- Admin endpoints (`/api/admin/llm/*`): connector policy, force-revoke, usage rollup
- DJ endpoints (`/api/llm/connectors`): list/create/rotate/test/delete (rate-limited, scoped to current user)
- Admin UI: `/admin/ai` (policy + per-DJ table + usage)
- DJ UI: `/settings/ai` (connect/test/delete; includes Hermes onboarding for ChatGPT subscription path)
- The recommendation engine routes through the gateway (`actor = event.created_by`, `purpose = "recommendation"`); `call_llm` now **requires** a `db` session — the legacy direct-Anthropic env-var fallback was removed in #343 (the connector system is the sole credential source).

### Recommendation Engine
- `server/app/services/recommendation/` — multi-stage pipeline:
- `service.py` — orchestrator: profile analysis → search → scoring → deduplication
- `enrichment.py` — fills missing BPM/key/genre from Beatport/MusicBrainz/Tidal (for recommendations; request-level enrichment is in `sync/orchestrator.py`)
- `scorer.py` — multi-dimensional scoring: BPM compatibility, harmonic mixing, genre affinity, artist diversity penalties
- `camelot.py` — harmonic mixing wheel (Camelot key compatibility, half-time/double-time BPM)
- `llm_client.py` — Claude Haiku integration (6/min rate limit, forced tool_use schema for structured JSON)
- `llm_client.py` — gateway-backed query generation (forced `tool_use` schema for structured JSON; requires `db` — the legacy direct-Anthropic env-var fallback was removed in #343)
- `llm_hooks.py` — structured response models for LLM queries
- `template.py` — playlist-based template recommendations (DJ picks a Tidal/Beatport playlist as "vibe" source)
- `mb_verify.py` — MusicBrainz artist verification to detect AI-generated filler tracks (cached in DB)
Expand Down
14 changes: 14 additions & 0 deletions dashboard/app/(dj)/account/__tests__/page.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,13 @@ const { mockGetMe, mockChangePassword, mockRequestEmailChange, mockUpdateMyPrefe
changePassword: (...args: unknown[]) => changePassword(...args),
requestEmailChange: (...args: unknown[]) => requestEmailChange(...args),
updateMyPreferences: (...args: unknown[]) => updateMyPreferences(...args),
// The AI providers section (relocated from /settings/ai, #357) mounts
// inside the account page. Stub its API surface so the section can render
// without network access. getLlmPolicy rejects → fail-closed (no extra UI).
// These live on the shared mockApi object so vi.spyOn(mockApi, ...) in
// individual tests still rebinds the same reference the page calls.
listLlmConnectors: () => Promise.resolve([]),
getLlmPolicy: () => Promise.reject(new Error('forbidden')),
},
};
});
Expand Down Expand Up @@ -58,6 +65,13 @@ describe('AccountPage', () => {
});
});

it('renders the relocated AI / Model providers section', async () => {
render(<AccountPage />);
await waitFor(() => {
expect(screen.getByText('AI / Model providers')).toBeInTheDocument();
});
});

it('submits password change with correct payload', async () => {
mockChangePassword.mockResolvedValue({ status: 'ok', message: 'Updated' });
render(<AccountPage />);
Expand Down
7 changes: 6 additions & 1 deletion dashboard/app/(dj)/account/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { useRouter } from 'next/navigation';

import { useAuth } from '@/lib/auth';
import { api } from '@/lib/api';
import AiProvidersSection from '@/components/AiProvidersSection';

export default function AccountPage() {
const router = useRouter();
Expand Down Expand Up @@ -115,7 +116,7 @@ export default function AccountPage() {
if (isLoading || !isAuthenticated) return null;

return (
<main style={{ maxWidth: '480px', margin: '0 auto', padding: '2rem 1rem' }}>
<main style={{ maxWidth: '720px', margin: '0 auto', padding: '2rem 1rem' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem', marginBottom: '2rem' }}>
<Link href="/dashboard" style={{ color: 'var(--text-secondary)', textDecoration: 'none', fontSize: '0.875rem' }}>
← Dashboard
Expand Down Expand Up @@ -223,6 +224,10 @@ export default function AccountPage() {
)}
</div>

<div style={{ background: 'var(--card)', borderRadius: '0.75rem', padding: '1.5rem', marginTop: '1.5rem' }}>
<AiProvidersSection />
</div>

<div style={{ background: 'var(--card)', borderRadius: '0.75rem', padding: '1.5rem', marginTop: '1.5rem' }}>
<h2 style={{ marginTop: 0, marginBottom: '1.25rem', fontSize: '1.1rem' }}>Guest Experience</h2>
<label style={{ display: 'flex', alignItems: 'center', gap: '0.75rem' }}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,9 @@ export function RecommendationsCard({
return false;
})();

// Derive short display name from model ID (e.g., "claude-haiku-4-5-20251001" → "Haiku 4.5")
// Derive short display name from model ID (e.g., "claude-haiku-4-5-20251001" → "Haiku 4.5").
// Non-Anthropic models (gpt-5.x, gemini, grok, bedrock, …) fall back to the raw model id
// so the badge reflects whichever provider connector actually produced the suggestions.
const modelDisplayName = (() => {
if (!llmModel) return 'AI';
const m = llmModel.toLowerCase();
Expand All @@ -250,7 +252,7 @@ export function RecommendationsCard({
const ver = m.match(/opus-(\d+)-(\d+)/);
return ver ? `Opus ${ver[1]}.${ver[2]}` : 'Opus';
}
return 'AI';
return llmModel;
})();

const modeButtonStyle = (active: boolean) => ({
Expand Down
Loading
Loading