fix(dashboard): normalize model assignments + confirm-modal for backup import (#44237)
Two beta-reported dashboard bugs:
1. Models page: 'Use as -> Main model' on an analytics card sends
entry.provider, which falls back to the model's VENDOR prefix
(modelVendor('anthropic/claude-opus-4.6') == 'anthropic') when the
session row has no billing_provider. That persisted
provider: anthropic + default: anthropic/claude-opus-4.6 — a
vendor-prefixed OpenRouter slug on the NATIVE Anthropic provider.
New sessions then 400 against api.anthropic.com and the user reads
it as 'changing models does nothing'. Unknown vendors (moonshotai,
poolside, ...) were worse: a provider that can never resolve
credentials.
Fix: _normalize_main_model_assignment() at the single write
chokepoint — maps non-provider vendor names back to the user's
current aggregator (else openrouter), and runs the model through
normalize_model_for_provider() so the persisted name matches the
target provider's API format. Wired into both /api/model/set and
the profile-scoped _write_profile_model.
2. System page: 'Restore from backup' spawns hermes import with
stdin=DEVNULL, so the CLI's interactive 'Continue? [y/N]' overwrite
prompt hits EOF and auto-aborts whenever a config already exists
(always, when the dashboard is running). Fix: ConfirmDialog in the
dashboard owns the consent, then the endpoint passes --force so the
restore runs non-interactively.
Validated live: dashboard on a temp HERMES_HOME, repro'd both failure
modes pre-fix (vendor-slug write verified via config.yaml + tui
session.create; import 'Aborted.' in action-import.log), then verified
post-fix (normalized writes, modal -> --force -> restored marker file).
This commit is contained in:
+2
-2
@@ -985,11 +985,11 @@ export const api = {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ output }),
|
||||
}),
|
||||
runImport: (archive: string) =>
|
||||
runImport: (archive: string, force = false) =>
|
||||
fetchJSON<ActionResponse>("/api/ops/import", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ archive }),
|
||||
body: JSON.stringify({ archive, force }),
|
||||
}),
|
||||
getHooks: () => fetchJSON<HooksResponse>("/api/ops/hooks"),
|
||||
createHook: (body: HookCreate) =>
|
||||
|
||||
@@ -170,6 +170,11 @@ export default function SystemPage() {
|
||||
const [addingCred, setAddingCred] = useState(false);
|
||||
|
||||
const [importPath, setImportPath] = useState("");
|
||||
// Restore-from-backup is destructive (overwrites the live config) and the
|
||||
// spawned `hermes import` runs non-interactively (stdin is /dev/null), so
|
||||
// its CLI "Continue? [y/N]" prompt would auto-abort. The dashboard owns the
|
||||
// consent: confirm here, then call the endpoint with force=true.
|
||||
const [importConfirmOpen, setImportConfirmOpen] = useState(false);
|
||||
|
||||
// Create-hook modal.
|
||||
const [hookModalOpen, setHookModalOpen] = useState(false);
|
||||
@@ -1181,11 +1186,24 @@ export default function SystemPage() {
|
||||
disabled={!importPath.trim()}
|
||||
onClick={() => {
|
||||
if (!importPath.trim()) return;
|
||||
runOp(() => api.runImport(importPath.trim()), "Import");
|
||||
setImportConfirmOpen(true);
|
||||
}}
|
||||
>
|
||||
Import
|
||||
</Button>
|
||||
<ConfirmDialog
|
||||
open={importConfirmOpen}
|
||||
title="Restore from backup?"
|
||||
description={`This will overwrite your current Hermes configuration, skills, sessions, and data with the contents of ${importPath.trim() || "the archive"}. This cannot be undone.`}
|
||||
destructive
|
||||
confirmLabel="Restore"
|
||||
cancelLabel="Cancel"
|
||||
onCancel={() => setImportConfirmOpen(false)}
|
||||
onConfirm={() => {
|
||||
setImportConfirmOpen(false);
|
||||
runOp(() => api.runImport(importPath.trim(), true), "Import");
|
||||
}}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</section>
|
||||
|
||||
Reference in New Issue
Block a user