feat(settings): minimal display of active AI provider + configured chain

The AI section now renders a small read-only summary of the user's
fallback chain: "Active provider: ChatGPT (Codex)" plus "Configured
chain: ChatGPT (Codex), OpenAI (your key), Admin OpenAI". When the
in-process cache reroutes to a fallback (the primary 401'd in the
last hour), the line gains "(cached: OpenAI)" so the user can see the
runner has shifted without digging through the wide-event log.

The full management UX (drag-to-reorder, on/off toggles, add/remove
provider entries) is owned by phase B2 — this surface is deliberately
tight so the two phases don't collide. The card hides itself when no
provider is configured at all so accounts that haven't set up AI
don't see an empty header. EN + DE i18n keys added under
settings.ai.providerChain.*; provider-type labels are localised so
"Local model" reads "Lokales Modell" in German.

The new GET /api/insights/provider-chain endpoint is the data source.
Returns the resolved chain in priority order plus the cached active
provider (when set), wrapped in the standard apiSuccess envelope.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Marc-André BombeckandClaude Opus 4.7 committed 2026-05-10 00:34:39 +02:00
1 parent cc0f3436ff
commit 613d6619df
5 files changed
+248 -2

No files matched your search

+67
View File
@@ -534,6 +534,8 @@ function InsightsSettingsCard({
</Button>
)}
<ProviderChainSummary />
<UserAIProviderSubsection />
</div>
</div>
@@ -970,3 +972,68 @@ function UserAIProviderSubsection() {
</div>
);
}
interface ProviderChainSummaryData {
activeProvider: string | null;
cachedActiveProvider: string | null;
configuredChain: { providerType: string; available: boolean }[];
}
/**
* v1.4.16 phase B5b — read-only summary of the user's AI provider
* fallback chain. Surfaces "Active provider: codex" + "Configured:
* codex, openai" so the user can see the cascade order without going
* through admin logs. Full management UX (drag-to-reorder, add/remove)
* is owned by phase B2 (AI provider settings cleanup); this panel is
* deliberately tight so the two phases don't collide.
*/
function ProviderChainSummary() {
const { t } = useTranslations();
const { data } = useQuery({
queryKey: ["insights", "provider-chain"],
queryFn: async () => {
const res = await fetch("/api/insights/provider-chain");
if (!res.ok) return null;
const json = await res.json();
return json.data as ProviderChainSummaryData;
},
});
if (!data) return null;
if (data.configuredChain.length === 0) return null;
const activeLabel = data.activeProvider
? t(`settings.ai.providerChain.types.${data.activeProvider}`)
: t("settings.ai.providerChain.none");
const chainLabels = data.configuredChain
.map((entry) => t(`settings.ai.providerChain.types.${entry.providerType}`))
.join(", ");
return (
<div
className="bg-muted/50 rounded-lg p-4 text-xs"
data-slot="ai-provider-chain-summary"
>
<p className="text-sm font-medium">
{t("settings.ai.providerChain.heading")}
</p>
<p className="text-muted-foreground mt-1">
{t("settings.ai.providerChain.activeLine", { provider: activeLabel })}
{data.cachedActiveProvider &&
data.cachedActiveProvider !== data.activeProvider
? ` ${t("settings.ai.providerChain.cachedSuffix", {
provider: t(
`settings.ai.providerChain.types.${data.cachedActiveProvider}`,
),
})}`
: ""}
</p>
<p className="text-muted-foreground mt-1">
{t("settings.ai.providerChain.chainLine", { chain: chainLabels })}
</p>
<p className="text-muted-foreground/80 mt-1 italic">
{t("settings.ai.providerChain.managementHint")}
</p>
</div>
);
}