diff --git a/web/src/app/playground/page.tsx b/web/src/app/playground/page.tsx index a5a9828..e060094 100644 --- a/web/src/app/playground/page.tsx +++ b/web/src/app/playground/page.tsx @@ -45,6 +45,11 @@ interface PlaygroundSessionList { items: PlaygroundSessionOut[]; } +interface LLMCredentialRow { + provider: string; + revoked_at: string | null; +} + export default async function PlaygroundPage() { const { active, all, reason } = await resolveActiveProject(); if (!active) { @@ -61,17 +66,32 @@ export default async function PlaygroundPage() { ); } - const [promptsRes, sessionsRes] = await Promise.all([ + const [promptsRes, sessionsRes, credentialsRes] = await Promise.all([ apiGet( `/v1/prompts?project_id=${encodeURIComponent(active.id)}`, ), apiGet( `/v1/playground/runs?project_id=${encodeURIComponent(active.id)}&limit=20`, ), + apiGet( + `/v1/llm-credentials?workspace_id=${encodeURIComponent(active.workspace_id)}`, + ), ]); const prompts = promptsRes.data ?? []; const sessions = sessionsRes.data?.items ?? []; + // Active credentials (revoked_at IS NULL) -> distinct provider list. + // The playground filters its model picker by this so users see only + // providers they actually have keys for. An admin/root user can see + // empty configured-providers (and pick anything via "Custom..."); a + // member without credentials sees the empty-state nudge to settings. + const configuredProviders = Array.from( + new Set( + (credentialsRes.data ?? []) + .filter((c) => c.revoked_at === null) + .map((c) => c.provider), + ), + ).sort(); const promptOptions = await Promise.all( prompts.map(async (p): Promise => { @@ -109,6 +129,7 @@ export default async function PlaygroundPage() { diff --git a/web/src/components/ModelPicker.tsx b/web/src/components/ModelPicker.tsx index 84d38d5..6c71266 100644 --- a/web/src/components/ModelPicker.tsx +++ b/web/src/components/ModelPicker.tsx @@ -31,12 +31,25 @@ export function ModelPicker({ onChange, label, ariaLabel, + availableProviders, }: { value: string; onChange: (next: string) => void; /** Optional label for the field wrapper. If omitted, render just the inputs. */ label?: string; ariaLabel?: string; + /** + * Filter the picker to providers the workspace has credentials for. + * Pass the result of /v1/llm-credentials projected to distinct + * provider names. Omit (undefined) for callers without workspace + * context — judges and others — which keeps today's "show + * everything" behavior. + * + * - undefined: show all providers (legacy) + * - non-empty array: show only those providers + Custom escape hatch + * - empty array: show only Custom + nudge to /workspace/credentials + */ + availableProviders?: string[]; }) { // If the current value isn't in the catalog, expose a free-text mode // so the user can edit it directly without losing it. @@ -44,6 +57,14 @@ export function ModelPicker({ const initialCustom = value !== "" && !inCatalog; const [customMode, setCustomMode] = useState(initialCustom); + const visibleProviders = + availableProviders === undefined + ? PROVIDERS + : PROVIDERS.filter((p) => availableProviders.includes(p.value)); + + const noCredentials = + availableProviders !== undefined && availableProviders.length === 0; + const select = (