mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-13 06:32:03 +00:00
fix(coach): offer only providers that can answer and say when a switch does not take
The provider-chain summary marked every entry available, so the Coach's provider select offered entries without a key or sign-in. Choosing one saved it to the front of the chain, the resolver skipped it, and the select jumped back without a word. Availability now comes from the presence probe, which names every chain type whose credential is present. After a switch the picker reads the chain back and says so when the server still answers with another provider.
This commit is contained in:
1 parent
a2ef2eec24
commit
2a556b47c3
14 files changed
+188
-45
No files matched your search
@@ -10,6 +10,7 @@ import {
|
||||
modelControlFor,
|
||||
reasoningFieldFor,
|
||||
selectableProviders,
|
||||
switchDidNotTake,
|
||||
} from "../coach-model-picker";
|
||||
|
||||
const CHAIN: ProviderChainData["configuredChain"] = [
|
||||
@@ -72,6 +73,23 @@ describe("selectableProviders", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("switchDidNotTake", () => {
|
||||
it("is quiet when the chosen provider leads the chain the server resolves", () => {
|
||||
expect(switchDidNotTake("openai", { activeProvider: "openai" })).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it("speaks up when the server still answers with another provider", () => {
|
||||
// A provider without a working key was saved to the front of the chain;
|
||||
// the resolver skipped it and the select would jump back silently.
|
||||
expect(switchDidNotTake("anthropic", { activeProvider: "openai" })).toBe(
|
||||
true,
|
||||
);
|
||||
expect(switchDidNotTake("anthropic", { activeProvider: null })).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("modelControlFor", () => {
|
||||
it("offers the presets for the provider whose key the person saved", () => {
|
||||
const control = modelControlFor("openai", userProvider());
|
||||
|
||||
@@ -70,6 +70,18 @@ export function selectableProviders(
|
||||
.map((entry) => entry.providerType);
|
||||
}
|
||||
|
||||
/**
|
||||
* True when a saved switch did not take: the chain the server resolves after
|
||||
* the save is not led by the provider the person chose. A provider whose key
|
||||
* stops working between the read and the save lands here.
|
||||
*/
|
||||
export function switchDidNotTake(
|
||||
chosen: ProviderType,
|
||||
fresh: Pick<ProviderChainData, "activeProvider">,
|
||||
): boolean {
|
||||
return fresh.activeProvider !== chosen;
|
||||
}
|
||||
|
||||
export type ModelControl =
|
||||
| { kind: "preset"; presets: readonly string[]; value: string | null }
|
||||
| { kind: "gateway"; value: string | null }
|
||||
@@ -139,6 +151,7 @@ export function CoachModelPicker() {
|
||||
});
|
||||
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [notice, setNotice] = useState<string | null>(null);
|
||||
|
||||
const onSaved = () => {
|
||||
setError(null);
|
||||
@@ -152,11 +165,26 @@ export function CoachModelPicker() {
|
||||
);
|
||||
|
||||
const switchProvider = useMutation({
|
||||
mutationFn: (provider: ProviderType) =>
|
||||
apiPut("/api/insights/provider-chain", {
|
||||
mutationFn: async (provider: ProviderType) => {
|
||||
setNotice(null);
|
||||
await apiPut("/api/insights/provider-chain", {
|
||||
chain: chainWithFirst(chainQuery.data?.configuredChain ?? [], provider),
|
||||
}),
|
||||
onSuccess: onSaved,
|
||||
});
|
||||
// Read back what the server now resolves, so a switch that did not
|
||||
// take is said rather than shown as the select jumping back.
|
||||
return apiGet<ProviderChainData>("/api/insights/provider-chain");
|
||||
},
|
||||
onSuccess: (fresh, provider) => {
|
||||
queryClient.setQueryData(queryKeys.insightsProviderChain(), fresh);
|
||||
onSaved();
|
||||
if (switchDidNotTake(provider, fresh)) {
|
||||
setNotice(
|
||||
t("insights.coach.frame.providerNotActive", {
|
||||
provider: t(`settings.ai.providerChain.types.${provider}`),
|
||||
}),
|
||||
);
|
||||
}
|
||||
},
|
||||
onError: onFailed,
|
||||
});
|
||||
const patchProvider = useMutation({
|
||||
@@ -294,6 +322,15 @@ export function CoachModelPicker() {
|
||||
{t("insights.coach.frame.saving")}
|
||||
</p>
|
||||
) : null}
|
||||
{notice ? (
|
||||
<p
|
||||
role="status"
|
||||
data-slot="coach-quick-provider-notice"
|
||||
className="text-muted-foreground text-xs"
|
||||
>
|
||||
{notice}
|
||||
</p>
|
||||
) : null}
|
||||
{error ? (
|
||||
<p role="alert" className="text-destructive text-xs">
|
||||
{error}
|
||||
|
||||
Reference in new issue
Block a user