feat(hermes): add API mode dropdown and per-provider model editor in form

The Hermes provider form previously only exposed Base URL and API Key,
forcing users to drop into the Model panel to hand-edit model IDs after
adding a provider. Following OpenClaw's shape, the form now carries:

- An API Mode selector (auto-detect / chat_completions / anthropic_messages).
  "auto" is a UI-only sentinel — selecting it deletes api_mode from the
  config so the YAML doesn't leak a redundant field.
- A model list editor where the first row is badged as the default and
  each row has a collapsible Advanced panel for context_length and
  max_tokens. Adding/removing rows uses a UUID-keyed ref so typing in
  one input doesn't drop focus when another row is added.
- A Fetch Models button that pulls /v1/models from the configured
  endpoint and exposes the catalog in a per-row dropdown, identical to
  OpenClaw's flow. The vendor grouping is memoized so keystrokes don't
  trigger a reduce+sort per model row — Radix DropdownMenuContent does
  not lazy-mount, so the inner JSX evaluates on every render regardless
  of whether the menu is open.

Three-locale i18n keys are added together (zh/en/ja).
This commit is contained in:
Jason
2026-04-18 22:28:22 +08:00
parent f935bac633
commit 497a543c1b
7 changed files with 555 additions and 14 deletions
@@ -1,6 +1,11 @@
import { useState, useCallback, useMemo } from "react";
import type { AppId } from "@/lib/api";
import { useProvidersQuery } from "@/lib/query/queries";
import type {
HermesApiModeChoice,
HermesModel,
HermesProviderSettingsConfig,
} from "@/config/hermesProviderPresets";
interface UseHermesFormStateParams {
initialData?: {
@@ -29,14 +34,14 @@ export interface HermesFormState {
setHermesProviderKey: (key: string) => void;
hermesBaseUrl: string;
hermesApiKey: string;
hermesApiMode: HermesApiModeChoice;
hermesModels: HermesModel[];
existingHermesKeys: string[];
handleHermesBaseUrlChange: (baseUrl: string) => void;
handleHermesApiKeyChange: (apiKey: string) => void;
resetHermesState: (config?: {
name?: string;
base_url?: string;
api_key?: string;
}) => void;
handleHermesApiModeChange: (mode: HermesApiModeChoice) => void;
handleHermesModelsChange: (models: HermesModel[]) => void;
resetHermesState: (config?: Partial<HermesProviderSettingsConfig>) => void;
}
function parseHermesField<T>(
@@ -48,7 +53,10 @@ function parseHermesField<T>(
if (initialData?.settingsConfig) {
return (initialData.settingsConfig[field] as T) || fallback;
}
return ((HERMES_DEFAULT_CONFIG_OBJ as Record<string, unknown>)[field] as T) || fallback;
return (
((HERMES_DEFAULT_CONFIG_OBJ as Record<string, unknown>)[field] as T) ||
fallback
);
} catch {
return fallback;
}
@@ -84,6 +92,23 @@ export function useHermesFormState({
return parseHermesField(initialData, "api_key", "");
});
const [hermesApiMode, setHermesApiMode] = useState<HermesApiModeChoice>(
() => {
if (appId !== "hermes") return "auto";
const stored = parseHermesField<HermesApiModeChoice | "">(
initialData,
"api_mode",
"",
);
return stored || "auto";
},
);
const [hermesModels, setHermesModels] = useState<HermesModel[]>(() => {
if (appId !== "hermes") return [];
return parseHermesField<HermesModel[]>(initialData, "models", []);
});
const updateHermesConfig = useCallback(
(updater: (config: Record<string, unknown>) => void) => {
try {
@@ -117,11 +142,41 @@ export function useHermesFormState({
[updateHermesConfig],
);
const handleHermesApiModeChange = useCallback(
(mode: HermesApiModeChoice) => {
setHermesApiMode(mode);
updateHermesConfig((config) => {
if (mode === "auto") {
delete config.api_mode;
} else {
config.api_mode = mode;
}
});
},
[updateHermesConfig],
);
const handleHermesModelsChange = useCallback(
(models: HermesModel[]) => {
setHermesModels(models);
updateHermesConfig((config) => {
if (models.length === 0) {
delete config.models;
} else {
config.models = models;
}
});
},
[updateHermesConfig],
);
const resetHermesState = useCallback(
(config?: { name?: string; base_url?: string; api_key?: string }) => {
(config?: Partial<HermesProviderSettingsConfig>) => {
setHermesProviderKey("");
setHermesBaseUrl(config?.base_url || "");
setHermesApiKey(config?.api_key || "");
setHermesApiMode(config?.api_mode ?? "auto");
setHermesModels(config?.models ?? []);
},
[],
);
@@ -131,9 +186,13 @@ export function useHermesFormState({
setHermesProviderKey,
hermesBaseUrl,
hermesApiKey,
hermesApiMode,
hermesModels,
existingHermesKeys,
handleHermesBaseUrlChange,
handleHermesApiKeyChange,
handleHermesApiModeChange,
handleHermesModelsChange,
resetHermesState,
};
}