fix(model): require confirmation for expensive model selections

Rebased onto current main and re-ported across the restructured
surfaces: model flows now thread confirm_provider/base_url/api_key
through hermes_cli/model_setup_flows.py, the Discord picker lives in
plugins/platforms/discord/adapter.py, and the web dashboard picker
applies chat-mode switches via config.set so the expensive-model
confirmation can ride the response.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Robin Fernandes
2026-06-10 00:24:06 -07:00
committed by Teknium
co-authored by Claude Fable 5
parent 4eadef18a9
commit af978ecb17
27 changed files with 1354 additions and 111 deletions
+61 -11
View File
@@ -26,7 +26,7 @@ import { Spinner } from "@nous-research/ui/ui/components/spinner";
import { Stats } from "@nous-research/ui/ui/components/stats";
import { Card, CardContent, CardHeader, CardTitle } from "@nous-research/ui/ui/components/card";
import { Badge } from "@nous-research/ui/ui/components/badge";
import { ConfirmDialog } from "@nous-research/ui/ui/components/confirm-dialog";
import { ConfirmDialog } from "@/components/ConfirmDialog";
import { useModalBehavior } from "@/hooks/useModalBehavior";
import { usePageHeader } from "@/contexts/usePageHeader";
import { useI18n } from "@/i18n";
@@ -209,10 +209,16 @@ function UseAsMenu({
const [open, setOpen] = useState(false);
const [busy, setBusy] = useState(false);
const [error, setError] = useState<string | null>(null);
const [pendingConfirm, setPendingConfirm] = useState<{
message: string;
scope: "main" | "auxiliary";
task: string;
} | null>(null);
const assign = async (
scope: "main" | "auxiliary",
task: string,
confirmExpensiveModel = false,
) => {
if (!provider || !model) {
setError("Missing provider/model");
@@ -221,7 +227,23 @@ function UseAsMenu({
setBusy(true);
setError(null);
try {
await api.setModelAssignment({ scope, provider, model, task });
const result = await api.setModelAssignment({
confirm_expensive_model: confirmExpensiveModel,
scope,
provider,
model,
task,
});
if (result.confirm_required) {
setPendingConfirm({
scope,
task,
message:
result.confirm_message ||
"This model has unusually high known pricing.",
});
return;
}
onAssigned();
setOpen(false);
} catch (e) {
@@ -310,6 +332,22 @@ function UseAsMenu({
)}
</div>
)}
<ConfirmDialog
open={!!pendingConfirm}
title="Expensive Model Warning"
description={pendingConfirm?.message}
destructive
confirmLabel="Switch anyway"
cancelLabel="Cancel"
loading={busy}
onCancel={() => setPendingConfirm(null)}
onConfirm={() => {
const pending = pendingConfirm;
if (!pending) return;
setPendingConfirm(null);
void assign(pending.scope, pending.task, true);
}}
/>
</div>
);
}
@@ -619,14 +657,16 @@ function AuxiliaryTasksModal({
AUX_TASKS.find((t) => t.key === picker.task)?.label ??
picker.task
}`}
onApply={async ({ provider, model }) => {
await api.setModelAssignment({
onApply={async ({ provider, model, confirmExpensiveModel }) => {
const result = await api.setModelAssignment({
confirm_expensive_model: confirmExpensiveModel,
scope: "auxiliary",
task: picker.task,
provider,
model,
});
onSaved();
if (!result.confirm_required) onSaved();
return result;
}}
onClose={() => setPicker(null)}
/>
@@ -666,14 +706,23 @@ function ModelSettingsPanel({
task,
provider,
model,
confirmExpensiveModel,
}: {
confirmExpensiveModel?: boolean;
scope: "main" | "auxiliary";
task: string;
provider: string;
model: string;
}) => {
await api.setModelAssignment({ scope, task, provider, model });
onSaved();
const result = await api.setModelAssignment({
confirm_expensive_model: confirmExpensiveModel,
scope,
task,
provider,
model,
});
if (!result.confirm_required) onSaved();
return result;
};
// Count how many aux tasks have overrides
@@ -749,14 +798,15 @@ function ModelSettingsPanel({
loader={api.getModelOptions}
alwaysGlobal
title="Set Main Model"
onApply={async ({ provider, model }) => {
await applyAssignment({
onApply={({ provider, model, confirmExpensiveModel }) =>
applyAssignment({
confirmExpensiveModel,
scope: "main",
task: "",
provider,
model,
});
}}
})
}
onClose={() => setPicker(null)}
/>
)}