mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-24 12:44:18 +08:00
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:
@@ -1,7 +1,48 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useState, useRef, useCallback, useMemo } from "react";
|
||||
import { FormLabel } from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from "@/components/ui/collapsible";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
Download,
|
||||
Plus,
|
||||
Trash2,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
Loader2,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { ApiKeySection } from "./shared";
|
||||
import {
|
||||
fetchModelsForConfig,
|
||||
showFetchModelsError,
|
||||
type FetchedModel,
|
||||
} from "@/lib/api/model-fetch";
|
||||
import {
|
||||
hermesApiModes,
|
||||
type HermesApiModeChoice,
|
||||
type HermesModel,
|
||||
} from "@/config/hermesProviderPresets";
|
||||
import type { ProviderCategory } from "@/types";
|
||||
|
||||
interface HermesFormFieldsProps {
|
||||
@@ -14,6 +55,10 @@ interface HermesFormFieldsProps {
|
||||
websiteUrl: string;
|
||||
isPartner?: boolean;
|
||||
partnerPromotionKey?: string;
|
||||
apiMode: HermesApiModeChoice;
|
||||
onApiModeChange: (mode: HermesApiModeChoice) => void;
|
||||
models: HermesModel[];
|
||||
onModelsChange: (models: HermesModel[]) => void;
|
||||
}
|
||||
|
||||
export function HermesFormFields({
|
||||
@@ -26,15 +71,140 @@ export function HermesFormFields({
|
||||
websiteUrl,
|
||||
isPartner,
|
||||
partnerPromotionKey,
|
||||
apiMode,
|
||||
onApiModeChange,
|
||||
models,
|
||||
onModelsChange,
|
||||
}: HermesFormFieldsProps) {
|
||||
const { t } = useTranslation();
|
||||
const [expandedModels, setExpandedModels] = useState<Record<number, boolean>>(
|
||||
{},
|
||||
);
|
||||
const [fetchedModels, setFetchedModels] = useState<FetchedModel[]>([]);
|
||||
const [isFetchingModels, setIsFetchingModels] = useState(false);
|
||||
|
||||
// Stable list keys: a manual ref rather than UUID-in-state so adding/removing
|
||||
// rows doesn't re-mount unrelated inputs (would drop focus mid-typing).
|
||||
const modelKeysRef = useRef<string[]>([]);
|
||||
while (modelKeysRef.current.length < models.length) {
|
||||
modelKeysRef.current.push(crypto.randomUUID());
|
||||
}
|
||||
if (modelKeysRef.current.length > models.length) {
|
||||
modelKeysRef.current.length = models.length;
|
||||
}
|
||||
const modelKeys = modelKeysRef.current;
|
||||
|
||||
// Group fetched models by vendor once — Radix DropdownMenuContent doesn't
|
||||
// lazy-mount, so computing this in JSX would re-run per model row per render.
|
||||
const groupedFetchedModels = useMemo(
|
||||
() =>
|
||||
Object.entries(
|
||||
fetchedModels.reduce(
|
||||
(acc, m) => {
|
||||
const v = m.ownedBy || "Other";
|
||||
if (!acc[v]) acc[v] = [];
|
||||
acc[v].push(m);
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, FetchedModel[]>,
|
||||
),
|
||||
).sort(([a], [b]) => a.localeCompare(b)),
|
||||
[fetchedModels],
|
||||
);
|
||||
|
||||
const toggleModelAdvanced = (index: number) => {
|
||||
setExpandedModels((prev) => ({ ...prev, [index]: !prev[index] }));
|
||||
};
|
||||
|
||||
const handleAddModel = () => {
|
||||
modelKeysRef.current.push(crypto.randomUUID());
|
||||
onModelsChange([
|
||||
...models,
|
||||
{ id: "", name: "", context_length: undefined, max_tokens: undefined },
|
||||
]);
|
||||
};
|
||||
|
||||
const handleFetchModels = useCallback(() => {
|
||||
if (!baseUrl || !apiKey) {
|
||||
showFetchModelsError(null, t, {
|
||||
hasApiKey: !!apiKey,
|
||||
hasBaseUrl: !!baseUrl,
|
||||
});
|
||||
return;
|
||||
}
|
||||
setIsFetchingModels(true);
|
||||
fetchModelsForConfig(baseUrl, apiKey)
|
||||
.then((fetched) => {
|
||||
setFetchedModels(fetched);
|
||||
if (fetched.length === 0) {
|
||||
toast.info(t("providerForm.fetchModelsEmpty"));
|
||||
} else {
|
||||
toast.success(
|
||||
t("providerForm.fetchModelsSuccess", { count: fetched.length }),
|
||||
);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.warn("[ModelFetch] Failed:", err);
|
||||
showFetchModelsError(err, t);
|
||||
})
|
||||
.finally(() => setIsFetchingModels(false));
|
||||
}, [baseUrl, apiKey, t]);
|
||||
|
||||
const handleRemoveModel = (index: number) => {
|
||||
modelKeysRef.current.splice(index, 1);
|
||||
const next = [...models];
|
||||
next.splice(index, 1);
|
||||
onModelsChange(next);
|
||||
setExpandedModels((prev) => {
|
||||
const updated = { ...prev };
|
||||
delete updated[index];
|
||||
return updated;
|
||||
});
|
||||
};
|
||||
|
||||
const handleModelChange = (
|
||||
index: number,
|
||||
field: keyof HermesModel,
|
||||
value: unknown,
|
||||
) => {
|
||||
const next = [...models];
|
||||
next[index] = { ...next[index], [field]: value };
|
||||
onModelsChange(next);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Base URL */}
|
||||
<div className="space-y-2">
|
||||
<FormLabel htmlFor="hermes-api-mode">
|
||||
{t("hermes.form.apiMode", { defaultValue: "API 模式" })}
|
||||
</FormLabel>
|
||||
<Select
|
||||
value={apiMode}
|
||||
onValueChange={(v) => onApiModeChange(v as HermesApiModeChoice)}
|
||||
>
|
||||
<SelectTrigger id="hermes-api-mode">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{hermesApiModes.map((mode) => (
|
||||
<SelectItem key={mode.value} value={mode.value}>
|
||||
{t(mode.labelKey)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("hermes.form.apiModeHint", {
|
||||
defaultValue:
|
||||
"供应商 API 协议。Auto 表示让 Hermes 按端点自动判断。",
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<FormLabel htmlFor="hermes-baseurl">
|
||||
{t("hermes.form.baseUrl", { defaultValue: "API Endpoint" })}
|
||||
{t("hermes.form.baseUrl", { defaultValue: "API 端点" })}
|
||||
</FormLabel>
|
||||
<Input
|
||||
id="hermes-baseurl"
|
||||
@@ -44,12 +214,11 @@ export function HermesFormFields({
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("hermes.form.baseUrlHint", {
|
||||
defaultValue: "The API endpoint URL for this provider.",
|
||||
defaultValue: "供应商的 API 端点地址。",
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* API Key */}
|
||||
<ApiKeySection
|
||||
value={apiKey}
|
||||
onChange={onApiKeyChange}
|
||||
@@ -59,6 +228,236 @@ export function HermesFormFields({
|
||||
isPartner={isPartner}
|
||||
partnerPromotionKey={partnerPromotionKey}
|
||||
/>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<FormLabel>
|
||||
{t("hermes.form.models", { defaultValue: "模型列表" })}
|
||||
</FormLabel>
|
||||
<div className="flex gap-1">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleFetchModels}
|
||||
disabled={isFetchingModels}
|
||||
className="h-7 gap-1"
|
||||
>
|
||||
{isFetchingModels ? (
|
||||
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||||
) : (
|
||||
<Download className="h-3.5 w-3.5" />
|
||||
)}
|
||||
{t("providerForm.fetchModels")}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleAddModel}
|
||||
className="h-7 gap-1"
|
||||
>
|
||||
<Plus className="h-3.5 w-3.5" />
|
||||
{t("hermes.form.addModel", { defaultValue: "添加模型" })}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{models.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground py-2">
|
||||
{t("hermes.form.noModels", {
|
||||
defaultValue: "暂无模型配置。切换到此供应商时将无默认模型。",
|
||||
})}
|
||||
</p>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{models.map((model, index) => (
|
||||
<div
|
||||
key={modelKeys[index]}
|
||||
className="p-3 border border-border/50 rounded-lg space-y-3"
|
||||
>
|
||||
{/* Role badge — first entry is the default written to model.default on switch */}
|
||||
<div className="flex items-center">
|
||||
<span
|
||||
className={`text-[10px] font-medium px-1.5 py-0.5 rounded ${
|
||||
index === 0
|
||||
? "bg-blue-500/15 text-blue-600 dark:text-blue-400"
|
||||
: "bg-muted text-muted-foreground"
|
||||
}`}
|
||||
>
|
||||
{index === 0
|
||||
? t("hermes.form.primaryModel", {
|
||||
defaultValue: "默认模型",
|
||||
})
|
||||
: t("hermes.form.fallbackModel", {
|
||||
defaultValue: "备选模型",
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex-1 space-y-1">
|
||||
<label className="text-xs text-muted-foreground">
|
||||
{t("hermes.form.modelId", { defaultValue: "模型 ID" })}
|
||||
</label>
|
||||
<div className="flex gap-1">
|
||||
<Input
|
||||
value={model.id}
|
||||
onChange={(e) =>
|
||||
handleModelChange(index, "id", e.target.value)
|
||||
}
|
||||
placeholder={t("hermes.form.modelIdPlaceholder", {
|
||||
defaultValue: "anthropic/claude-opus-4-7",
|
||||
})}
|
||||
className="flex-1"
|
||||
/>
|
||||
{fetchedModels.length > 0 && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="shrink-0"
|
||||
>
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="end"
|
||||
className="max-h-64 overflow-y-auto z-[200]"
|
||||
>
|
||||
{groupedFetchedModels.map(
|
||||
([vendor, vModels], vi) => (
|
||||
<div key={vendor}>
|
||||
{vi > 0 && <DropdownMenuSeparator />}
|
||||
<DropdownMenuLabel>
|
||||
{vendor}
|
||||
</DropdownMenuLabel>
|
||||
{vModels.map((m) => (
|
||||
<DropdownMenuItem
|
||||
key={m.id}
|
||||
onSelect={() =>
|
||||
handleModelChange(index, "id", m.id)
|
||||
}
|
||||
>
|
||||
{m.id}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 space-y-1">
|
||||
<label className="text-xs text-muted-foreground">
|
||||
{t("hermes.form.modelName", {
|
||||
defaultValue: "显示名称",
|
||||
})}
|
||||
</label>
|
||||
<Input
|
||||
value={model.name ?? ""}
|
||||
onChange={(e) =>
|
||||
handleModelChange(index, "name", e.target.value)
|
||||
}
|
||||
placeholder={t("hermes.form.modelNamePlaceholder", {
|
||||
defaultValue: "Claude Opus 4.7",
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => handleRemoveModel(index)}
|
||||
className="h-9 w-9 mt-5 text-muted-foreground hover:text-destructive"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Collapsible
|
||||
open={expandedModels[index] ?? false}
|
||||
onOpenChange={() => toggleModelAdvanced(index)}
|
||||
>
|
||||
<CollapsibleTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 gap-1 text-xs text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
{expandedModels[index] ? (
|
||||
<ChevronDown className="h-3.5 w-3.5" />
|
||||
) : (
|
||||
<ChevronRight className="h-3.5 w-3.5" />
|
||||
)}
|
||||
{t("hermes.form.advancedOptions", {
|
||||
defaultValue: "高级选项",
|
||||
})}
|
||||
</Button>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className="space-y-3 pt-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex-1 space-y-1">
|
||||
<label className="text-xs text-muted-foreground">
|
||||
{t("hermes.form.contextLength", {
|
||||
defaultValue: "上下文长度",
|
||||
})}
|
||||
</label>
|
||||
<Input
|
||||
type="number"
|
||||
value={model.context_length ?? ""}
|
||||
onChange={(e) =>
|
||||
handleModelChange(
|
||||
index,
|
||||
"context_length",
|
||||
e.target.value
|
||||
? parseInt(e.target.value)
|
||||
: undefined,
|
||||
)
|
||||
}
|
||||
placeholder="200000"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 space-y-1">
|
||||
<label className="text-xs text-muted-foreground">
|
||||
{t("hermes.form.maxTokens", {
|
||||
defaultValue: "最大输出 Tokens",
|
||||
})}
|
||||
</label>
|
||||
<Input
|
||||
type="number"
|
||||
value={model.max_tokens ?? ""}
|
||||
onChange={(e) =>
|
||||
handleModelChange(
|
||||
index,
|
||||
"max_tokens",
|
||||
e.target.value
|
||||
? parseInt(e.target.value)
|
||||
: undefined,
|
||||
)
|
||||
}
|
||||
placeholder="32000"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("hermes.form.modelsHint", {
|
||||
defaultValue:
|
||||
"切换到此供应商时,第一个模型会写入顶层 model.default。",
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1898,6 +1898,10 @@ export function ProviderForm({
|
||||
websiteUrl={hermesWebsiteUrl}
|
||||
isPartner={isHermesPartner}
|
||||
partnerPromotionKey={hermesPartnerPromotionKey}
|
||||
apiMode={hermesForm.hermesApiMode}
|
||||
onApiModeChange={hermesForm.handleHermesApiModeChange}
|
||||
models={hermesForm.hermesModels}
|
||||
onModelsChange={hermesForm.handleHermesModelsChange}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -47,6 +47,31 @@ export interface HermesSuggestedDefaults {
|
||||
/** Hermes custom_provider protocol mode (optional; auto-detected when omitted). */
|
||||
export type HermesApiMode = "chat_completions" | "anthropic_messages";
|
||||
|
||||
/**
|
||||
* Form-facing value used by the API Mode dropdown.
|
||||
*
|
||||
* `auto` is the UI-only sentinel for "omit `api_mode` and let Hermes detect the
|
||||
* protocol from the endpoint". When serialized to `settings_config`, `auto`
|
||||
* becomes `undefined` so the YAML doesn't include `api_mode` at all.
|
||||
*/
|
||||
export type HermesApiModeChoice = "auto" | HermesApiMode;
|
||||
|
||||
/**
|
||||
* Dropdown options for the API Mode selector. `labelKey` is looked up in i18n;
|
||||
* `value` of "auto" means "don't write `api_mode` to the config".
|
||||
*/
|
||||
export const hermesApiModes: Array<{
|
||||
value: HermesApiModeChoice;
|
||||
labelKey: string;
|
||||
}> = [
|
||||
{ value: "auto", labelKey: "hermes.form.apiModeAuto" },
|
||||
{ value: "chat_completions", labelKey: "hermes.form.apiModeChatCompletions" },
|
||||
{
|
||||
value: "anthropic_messages",
|
||||
labelKey: "hermes.form.apiModeAnthropicMessages",
|
||||
},
|
||||
];
|
||||
|
||||
export interface HermesProviderPreset {
|
||||
name: string;
|
||||
nameKey?: string;
|
||||
|
||||
@@ -1640,7 +1640,25 @@
|
||||
"providerKeyLockedHint": "This provider is in Hermes config; key is locked.",
|
||||
"providerKeyRequired": "Provider key is required",
|
||||
"providerKeyInvalid": "Provider key can only contain lowercase letters, numbers, and hyphens",
|
||||
"providerKeyDuplicate": "This provider key already exists"
|
||||
"providerKeyDuplicate": "This provider key already exists",
|
||||
"apiMode": "API Mode",
|
||||
"apiModeHint": "Provider API protocol. Auto lets Hermes detect it from the endpoint.",
|
||||
"apiModeAuto": "Auto-detect",
|
||||
"apiModeChatCompletions": "OpenAI Chat Completions",
|
||||
"apiModeAnthropicMessages": "Anthropic Messages",
|
||||
"models": "Models",
|
||||
"addModel": "Add model",
|
||||
"noModels": "No models configured. Switching to this provider won't change the default model.",
|
||||
"modelId": "Model ID",
|
||||
"modelIdPlaceholder": "anthropic/claude-opus-4-7",
|
||||
"modelName": "Display name",
|
||||
"modelNamePlaceholder": "Claude Opus 4.7",
|
||||
"contextLength": "Context length",
|
||||
"maxTokens": "Max output tokens",
|
||||
"advancedOptions": "Advanced options",
|
||||
"modelsHint": "On switch, the first model is written to top-level model.default.",
|
||||
"primaryModel": "Default",
|
||||
"fallbackModel": "Alternate"
|
||||
},
|
||||
"model": {
|
||||
"title": "Model Config",
|
||||
|
||||
@@ -1640,7 +1640,25 @@
|
||||
"providerKeyLockedHint": "このプロバイダーは Hermes 設定に追加済みのため、キーは変更できません。",
|
||||
"providerKeyRequired": "プロバイダーキーは必須です",
|
||||
"providerKeyInvalid": "プロバイダーキーは小文字、数字、ハイフンのみ使用できます",
|
||||
"providerKeyDuplicate": "このプロバイダーキーは既に存在します"
|
||||
"providerKeyDuplicate": "このプロバイダーキーは既に存在します",
|
||||
"apiMode": "API モード",
|
||||
"apiModeHint": "プロバイダーの API プロトコル。Auto はエンドポイントから自動判定します。",
|
||||
"apiModeAuto": "自動検出",
|
||||
"apiModeChatCompletions": "OpenAI Chat Completions",
|
||||
"apiModeAnthropicMessages": "Anthropic Messages",
|
||||
"models": "モデル一覧",
|
||||
"addModel": "モデルを追加",
|
||||
"noModels": "モデル未設定。このプロバイダーに切り替えてもデフォルトモデルは更新されません。",
|
||||
"modelId": "モデル ID",
|
||||
"modelIdPlaceholder": "anthropic/claude-opus-4-7",
|
||||
"modelName": "表示名",
|
||||
"modelNamePlaceholder": "Claude Opus 4.7",
|
||||
"contextLength": "コンテキスト長",
|
||||
"maxTokens": "最大出力トークン",
|
||||
"advancedOptions": "詳細オプション",
|
||||
"modelsHint": "切り替え時、最初のモデルが model.default に書き込まれます。",
|
||||
"primaryModel": "デフォルト",
|
||||
"fallbackModel": "予備"
|
||||
},
|
||||
"model": {
|
||||
"title": "モデル設定",
|
||||
|
||||
@@ -1641,7 +1641,25 @@
|
||||
"providerKeyLockedHint": "该供应商已添加到 Hermes 配置中,标识不可修改。",
|
||||
"providerKeyRequired": "供应商标识不能为空",
|
||||
"providerKeyInvalid": "供应商标识只能包含小写字母、数字和连字符",
|
||||
"providerKeyDuplicate": "该供应商标识已存在"
|
||||
"providerKeyDuplicate": "该供应商标识已存在",
|
||||
"apiMode": "API 模式",
|
||||
"apiModeHint": "供应商 API 协议。Auto 表示让 Hermes 按端点自动判断。",
|
||||
"apiModeAuto": "自动检测",
|
||||
"apiModeChatCompletions": "OpenAI Chat Completions",
|
||||
"apiModeAnthropicMessages": "Anthropic Messages",
|
||||
"models": "模型列表",
|
||||
"addModel": "添加模型",
|
||||
"noModels": "暂无模型配置。切换到此供应商时将不会更新默认模型。",
|
||||
"modelId": "模型 ID",
|
||||
"modelIdPlaceholder": "anthropic/claude-opus-4-7",
|
||||
"modelName": "显示名称",
|
||||
"modelNamePlaceholder": "Claude Opus 4.7",
|
||||
"contextLength": "上下文长度",
|
||||
"maxTokens": "最大输出 Tokens",
|
||||
"advancedOptions": "高级选项",
|
||||
"modelsHint": "切换到此供应商时,第一个模型会写入顶层 model.default。",
|
||||
"primaryModel": "默认模型",
|
||||
"fallbackModel": "备选模型"
|
||||
},
|
||||
"model": {
|
||||
"title": "模型配置",
|
||||
|
||||
Reference in New Issue
Block a user