mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-05-23 06:04:43 +08:00
feat(hermes): align provider schema with Hermes Agent 0.10.0
Hermes 0.10.0 tightened custom_providers validation (commit 2cdae233): invalid base_urls are rejected, unknown fields produce warnings, and new fields (rate_limit_delay, bedrock_converse, key_env) landed. - Add bedrock_converse to the api_mode selector (and i18n labels) - Expose rate_limit_delay in a provider-level advanced panel - Validate base_url client-side (URL shape, template-token friendly) - Drop per-model max_tokens — not in _VALID_CUSTOM_PROVIDER_FIELDS - Round-trip test asserts set_provider preserves rate_limit_delay / key_env / any unknown forward-compat field
This commit is contained in:
@@ -11,7 +11,7 @@ mod deeplink;
|
||||
mod error;
|
||||
mod gemini_config;
|
||||
mod gemini_mcp;
|
||||
mod hermes_config;
|
||||
pub mod hermes_config;
|
||||
mod init_status;
|
||||
mod lightweight;
|
||||
#[cfg(target_os = "linux")]
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
mod support;
|
||||
|
||||
use cc_switch_lib::{hermes_config, update_settings, AppSettings};
|
||||
|
||||
/// 读取并回写 Hermes provider 时,Hermes v12+ 新增或未来才会出现的字段
|
||||
/// (例如 `rate_limit_delay`、`key_env`)必须透传,不能因为 UI 不感知就静默丢弃。
|
||||
/// 否则用户在 Hermes Web UI 配置的高级字段会在 CC Switch 编辑后消失。
|
||||
fn with_temp_hermes_dir<F: FnOnce(&std::path::Path)>(f: F) {
|
||||
let guard = support::test_mutex().lock().expect("test mutex poisoned");
|
||||
let home = support::ensure_test_home();
|
||||
support::reset_test_fs();
|
||||
|
||||
let hermes_dir = home.join(".hermes-roundtrip");
|
||||
let _ = std::fs::remove_dir_all(&hermes_dir);
|
||||
std::fs::create_dir_all(&hermes_dir).expect("create temp hermes dir");
|
||||
|
||||
update_settings(AppSettings {
|
||||
hermes_config_dir: Some(hermes_dir.to_string_lossy().into_owned()),
|
||||
..AppSettings::default()
|
||||
})
|
||||
.expect("set hermes_config_dir override");
|
||||
|
||||
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| f(&hermes_dir)));
|
||||
|
||||
// Always restore settings and drop fixture dir, even on test failure.
|
||||
let _ = update_settings(AppSettings::default());
|
||||
let _ = std::fs::remove_dir_all(&hermes_dir);
|
||||
drop(guard);
|
||||
|
||||
if let Err(err) = result {
|
||||
std::panic::resume_unwind(err);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_provider_preserves_unknown_and_future_fields() {
|
||||
with_temp_hermes_dir(|dir| {
|
||||
let yaml = r#"custom_providers:
|
||||
- name: myhost
|
||||
base_url: https://api.example.com/v1
|
||||
api_key: sk-old
|
||||
api_mode: chat_completions
|
||||
rate_limit_delay: 0.5
|
||||
key_env: MY_API_KEY
|
||||
foo_bar: keep-me-around
|
||||
models:
|
||||
gpt-4:
|
||||
context_length: 8192
|
||||
"#;
|
||||
let config_path = dir.join("config.yaml");
|
||||
std::fs::write(&config_path, yaml).expect("seed config.yaml");
|
||||
|
||||
// Simulate the UI sending back only the fields it knows about.
|
||||
let patch = serde_json::json!({
|
||||
"name": "myhost",
|
||||
"base_url": "https://api.example.com/v1",
|
||||
"api_key": "sk-new",
|
||||
"api_mode": "chat_completions",
|
||||
"models": [
|
||||
{ "id": "gpt-4", "context_length": 8192 }
|
||||
]
|
||||
});
|
||||
|
||||
hermes_config::set_provider("myhost", patch).expect("set_provider");
|
||||
|
||||
let written = std::fs::read_to_string(&config_path).expect("read written config");
|
||||
|
||||
assert!(
|
||||
written.contains("rate_limit_delay"),
|
||||
"rate_limit_delay stripped:\n{written}"
|
||||
);
|
||||
assert!(
|
||||
written.contains("key_env"),
|
||||
"key_env key stripped:\n{written}"
|
||||
);
|
||||
assert!(
|
||||
written.contains("MY_API_KEY"),
|
||||
"key_env value stripped:\n{written}"
|
||||
);
|
||||
assert!(
|
||||
written.contains("foo_bar"),
|
||||
"unknown forward-compat field stripped:\n{written}"
|
||||
);
|
||||
assert!(
|
||||
written.contains("sk-new"),
|
||||
"api_key was not updated to sk-new:\n{written}"
|
||||
);
|
||||
assert!(
|
||||
!written.contains("sk-old"),
|
||||
"old api_key still present:\n{written}"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_providers_surfaces_rate_limit_delay_and_key_env() {
|
||||
with_temp_hermes_dir(|dir| {
|
||||
let yaml = r#"custom_providers:
|
||||
- name: myhost
|
||||
base_url: https://api.example.com/v1
|
||||
api_key: sk-xxx
|
||||
api_mode: chat_completions
|
||||
rate_limit_delay: 2.5
|
||||
key_env: FOO_KEY
|
||||
models:
|
||||
m1: {}
|
||||
"#;
|
||||
std::fs::write(dir.join("config.yaml"), yaml).expect("seed config.yaml");
|
||||
|
||||
let providers = hermes_config::get_providers().expect("get_providers");
|
||||
let entry = providers.get("myhost").expect("myhost missing");
|
||||
|
||||
assert_eq!(
|
||||
entry.get("rate_limit_delay").and_then(|v| v.as_f64()),
|
||||
Some(2.5),
|
||||
"rate_limit_delay not surfaced to DAO payload"
|
||||
);
|
||||
assert_eq!(
|
||||
entry.get("key_env").and_then(|v| v.as_str()),
|
||||
Some("FOO_KEY"),
|
||||
"key_env not surfaced to DAO payload"
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -1,5 +1,12 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useState, useRef, useCallback, useMemo } from "react";
|
||||
import {
|
||||
useState,
|
||||
useRef,
|
||||
useCallback,
|
||||
useMemo,
|
||||
useEffect,
|
||||
type ReactNode,
|
||||
} from "react";
|
||||
import { FormLabel } from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -59,6 +66,77 @@ interface HermesFormFieldsProps {
|
||||
onApiModeChange: (mode: HermesApiMode) => void;
|
||||
models: HermesModel[];
|
||||
onModelsChange: (models: HermesModel[]) => void;
|
||||
rateLimitDelay: number | undefined;
|
||||
onRateLimitDelayChange: (delay: number | undefined) => void;
|
||||
}
|
||||
|
||||
type BaseUrlErrorCode = "empty" | "invalid" | "scheme";
|
||||
|
||||
const BASE_URL_ERROR_I18N_KEY: Record<BaseUrlErrorCode, string> = {
|
||||
empty: "hermes.form.baseUrlRequired",
|
||||
scheme: "hermes.form.baseUrlScheme",
|
||||
invalid: "hermes.form.baseUrlInvalid",
|
||||
};
|
||||
|
||||
const TEMPLATE_TOKEN_RE = /\$\{[^}]+\}/g;
|
||||
|
||||
/**
|
||||
* Hermes 0.10.0+ rejects `base_url` entries that don't parse as proper URLs
|
||||
* (commit 2cdae233). Validate client-side so the error surfaces before the
|
||||
* request ever reaches Hermes' startup.
|
||||
*/
|
||||
function validateBaseUrl(raw: string): BaseUrlErrorCode | null {
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed) return "empty";
|
||||
// Presets like KAT-Coder embed `${VAR}` tokens — swap them before URL parse.
|
||||
const candidate = trimmed.replace(TEMPLATE_TOKEN_RE, "placeholder");
|
||||
let u: URL;
|
||||
try {
|
||||
u = new URL(candidate);
|
||||
} catch {
|
||||
return "invalid";
|
||||
}
|
||||
if (!u.protocol.startsWith("http")) return "scheme";
|
||||
if (!u.hostname) return "invalid";
|
||||
return null;
|
||||
}
|
||||
|
||||
interface AdvancedSectionProps {
|
||||
open: boolean;
|
||||
onOpenChange: (next: boolean) => void;
|
||||
labelKey: string;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
function AdvancedSection({
|
||||
open,
|
||||
onOpenChange,
|
||||
labelKey,
|
||||
children,
|
||||
}: AdvancedSectionProps) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Collapsible open={open} onOpenChange={onOpenChange}>
|
||||
<CollapsibleTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 gap-1 text-xs text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
{open ? (
|
||||
<ChevronDown className="h-3.5 w-3.5" />
|
||||
) : (
|
||||
<ChevronRight className="h-3.5 w-3.5" />
|
||||
)}
|
||||
{t(labelKey)}
|
||||
</Button>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className="space-y-3 pt-2">
|
||||
{children}
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
);
|
||||
}
|
||||
|
||||
export function HermesFormFields({
|
||||
@@ -75,6 +153,8 @@ export function HermesFormFields({
|
||||
onApiModeChange,
|
||||
models,
|
||||
onModelsChange,
|
||||
rateLimitDelay,
|
||||
onRateLimitDelayChange,
|
||||
}: HermesFormFieldsProps) {
|
||||
const { t } = useTranslation();
|
||||
const [expandedModels, setExpandedModels] = useState<Record<number, boolean>>(
|
||||
@@ -82,6 +162,24 @@ export function HermesFormFields({
|
||||
);
|
||||
const [fetchedModels, setFetchedModels] = useState<FetchedModel[]>([]);
|
||||
const [isFetchingModels, setIsFetchingModels] = useState(false);
|
||||
const [baseUrlTouched, setBaseUrlTouched] = useState(false);
|
||||
const [providerAdvancedOpen, setProviderAdvancedOpen] = useState(
|
||||
rateLimitDelay !== undefined,
|
||||
);
|
||||
|
||||
// Auto-expand when a preset switch brings in a value so the user sees it;
|
||||
// don't force-collapse on clear, to avoid yanking the panel shut mid-edit.
|
||||
useEffect(() => {
|
||||
if (rateLimitDelay !== undefined) {
|
||||
setProviderAdvancedOpen(true);
|
||||
}
|
||||
}, [rateLimitDelay]);
|
||||
|
||||
const baseUrlErrorCode = useMemo(() => validateBaseUrl(baseUrl), [baseUrl]);
|
||||
const showBaseUrlError = baseUrlTouched && baseUrlErrorCode !== null;
|
||||
const baseUrlErrorMessage = baseUrlErrorCode
|
||||
? t(BASE_URL_ERROR_I18N_KEY[baseUrlErrorCode])
|
||||
: "";
|
||||
|
||||
// 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).
|
||||
@@ -120,7 +218,7 @@ export function HermesFormFields({
|
||||
modelKeysRef.current.push(crypto.randomUUID());
|
||||
onModelsChange([
|
||||
...models,
|
||||
{ id: "", name: "", context_length: undefined, max_tokens: undefined },
|
||||
{ id: "", name: "", context_length: undefined },
|
||||
]);
|
||||
};
|
||||
|
||||
@@ -209,13 +307,24 @@ export function HermesFormFields({
|
||||
id="hermes-baseurl"
|
||||
value={baseUrl}
|
||||
onChange={(e) => onBaseUrlChange(e.target.value)}
|
||||
onBlur={() => setBaseUrlTouched(true)}
|
||||
placeholder="https://api.example.com/v1"
|
||||
aria-invalid={showBaseUrlError}
|
||||
className={
|
||||
showBaseUrlError
|
||||
? "border-destructive focus-visible:ring-destructive"
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("hermes.form.baseUrlHint", {
|
||||
defaultValue: "供应商的 API 端点地址。",
|
||||
})}
|
||||
</p>
|
||||
{showBaseUrlError ? (
|
||||
<p className="text-xs text-destructive">{baseUrlErrorMessage}</p>
|
||||
) : (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("hermes.form.baseUrlHint", {
|
||||
defaultValue: "供应商的 API 端点地址。",
|
||||
})}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<ApiKeySection
|
||||
@@ -377,74 +486,31 @@ export function HermesFormFields({
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Collapsible
|
||||
<AdvancedSection
|
||||
open={expandedModels[index] ?? false}
|
||||
onOpenChange={() => toggleModelAdvanced(index)}
|
||||
labelKey="hermes.form.advancedOptions"
|
||||
>
|
||||
<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: "高级选项",
|
||||
<div className="space-y-1">
|
||||
<label className="text-xs text-muted-foreground">
|
||||
{t("hermes.form.contextLength", {
|
||||
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>
|
||||
</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>
|
||||
</AdvancedSection>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -457,6 +523,44 @@ export function HermesFormFields({
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<AdvancedSection
|
||||
open={providerAdvancedOpen}
|
||||
onOpenChange={setProviderAdvancedOpen}
|
||||
labelKey="hermes.form.providerAdvanced"
|
||||
>
|
||||
<div className="space-y-1">
|
||||
<label className="text-xs text-muted-foreground">
|
||||
{t("hermes.form.rateLimitDelay", {
|
||||
defaultValue: "请求间隔(秒)",
|
||||
})}
|
||||
</label>
|
||||
<Input
|
||||
type="number"
|
||||
step="0.1"
|
||||
min="0"
|
||||
value={rateLimitDelay ?? ""}
|
||||
onChange={(e) => {
|
||||
const v = e.target.value;
|
||||
if (v === "") {
|
||||
onRateLimitDelayChange(undefined);
|
||||
return;
|
||||
}
|
||||
const n = parseFloat(v);
|
||||
onRateLimitDelayChange(
|
||||
Number.isFinite(n) && n >= 0 ? n : undefined,
|
||||
);
|
||||
}}
|
||||
placeholder="0.5"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("hermes.form.rateLimitDelayHint", {
|
||||
defaultValue:
|
||||
"连续请求间的最小间隔秒数(可选)。留空表示无限制。",
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
</AdvancedSection>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1902,6 +1902,10 @@ export function ProviderForm({
|
||||
onApiModeChange={hermesForm.handleHermesApiModeChange}
|
||||
models={hermesForm.hermesModels}
|
||||
onModelsChange={hermesForm.handleHermesModelsChange}
|
||||
rateLimitDelay={hermesForm.hermesRateLimitDelay}
|
||||
onRateLimitDelayChange={
|
||||
hermesForm.handleHermesRateLimitDelayChange
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -37,11 +37,13 @@ export interface HermesFormState {
|
||||
hermesApiKey: string;
|
||||
hermesApiMode: HermesApiMode;
|
||||
hermesModels: HermesModel[];
|
||||
hermesRateLimitDelay: number | undefined;
|
||||
existingHermesKeys: string[];
|
||||
handleHermesBaseUrlChange: (baseUrl: string) => void;
|
||||
handleHermesApiKeyChange: (apiKey: string) => void;
|
||||
handleHermesApiModeChange: (mode: HermesApiMode) => void;
|
||||
handleHermesModelsChange: (models: HermesModel[]) => void;
|
||||
handleHermesRateLimitDelayChange: (delay: number | undefined) => void;
|
||||
resetHermesState: (config?: Partial<HermesProviderSettingsConfig>) => void;
|
||||
}
|
||||
|
||||
@@ -63,6 +65,12 @@ function parseHermesField<T>(
|
||||
}
|
||||
}
|
||||
|
||||
function parseRateLimitDelay(raw: unknown): number | undefined {
|
||||
return typeof raw === "number" && Number.isFinite(raw) && raw >= 0
|
||||
? raw
|
||||
: undefined;
|
||||
}
|
||||
|
||||
export function useHermesFormState({
|
||||
initialData,
|
||||
appId,
|
||||
@@ -108,6 +116,13 @@ export function useHermesFormState({
|
||||
return parseHermesField<HermesModel[]>(initialData, "models", []);
|
||||
});
|
||||
|
||||
const [hermesRateLimitDelay, setHermesRateLimitDelay] = useState<
|
||||
number | undefined
|
||||
>(() => {
|
||||
if (appId !== "hermes") return undefined;
|
||||
return parseRateLimitDelay(initialData?.settingsConfig?.rate_limit_delay);
|
||||
});
|
||||
|
||||
const updateHermesConfig = useCallback(
|
||||
(updater: (config: Record<string, unknown>) => void) => {
|
||||
try {
|
||||
@@ -165,6 +180,20 @@ export function useHermesFormState({
|
||||
[updateHermesConfig],
|
||||
);
|
||||
|
||||
const handleHermesRateLimitDelayChange = useCallback(
|
||||
(delay: number | undefined) => {
|
||||
setHermesRateLimitDelay(delay);
|
||||
updateHermesConfig((config) => {
|
||||
if (delay === undefined) {
|
||||
delete config.rate_limit_delay;
|
||||
} else {
|
||||
config.rate_limit_delay = delay;
|
||||
}
|
||||
});
|
||||
},
|
||||
[updateHermesConfig],
|
||||
);
|
||||
|
||||
const resetHermesState = useCallback(
|
||||
(config?: Partial<HermesProviderSettingsConfig>) => {
|
||||
setHermesProviderKey("");
|
||||
@@ -172,6 +201,7 @@ export function useHermesFormState({
|
||||
setHermesApiKey(config?.api_key || "");
|
||||
setHermesApiMode(config?.api_mode ?? HERMES_DEFAULT_API_MODE);
|
||||
setHermesModels(config?.models ?? []);
|
||||
setHermesRateLimitDelay(parseRateLimitDelay(config?.rate_limit_delay));
|
||||
},
|
||||
[],
|
||||
);
|
||||
@@ -183,11 +213,13 @@ export function useHermesFormState({
|
||||
hermesApiKey,
|
||||
hermesApiMode,
|
||||
hermesModels,
|
||||
hermesRateLimitDelay,
|
||||
existingHermesKeys,
|
||||
handleHermesBaseUrlChange,
|
||||
handleHermesApiKeyChange,
|
||||
handleHermesApiModeChange,
|
||||
handleHermesModelsChange,
|
||||
handleHermesRateLimitDelayChange,
|
||||
resetHermesState,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -37,8 +37,12 @@ export function isHermesReadOnlyProvider(settingsConfig: unknown): boolean {
|
||||
* models:
|
||||
* anthropic/claude-opus-4-7:
|
||||
* context_length: 200000
|
||||
* max_tokens: 32000
|
||||
* ```
|
||||
*
|
||||
* Hermes' `_VALID_CUSTOM_PROVIDER_FIELDS` (hermes_cli/config.py) does not include
|
||||
* `max_tokens` at the per-model level — writing it produces an "unknown field"
|
||||
* warning on Hermes startup. Max tokens is a per-request parameter, not a
|
||||
* provider-level config.
|
||||
*/
|
||||
export interface HermesModel {
|
||||
/** Model ID — becomes the YAML key and the value written to top-level model.default. */
|
||||
@@ -47,16 +51,14 @@ export interface HermesModel {
|
||||
name?: string;
|
||||
/** Override the auto-detected context window. */
|
||||
context_length?: number;
|
||||
/** Response-length cap. */
|
||||
max_tokens?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Top-level `model:` defaults suggested by a preset.
|
||||
*
|
||||
* Written to the YAML `model:` section when the user switches to this provider.
|
||||
* Per-model `context_length` / `max_tokens` live on the individual `HermesModel`
|
||||
* entries and flow through `custom_providers[].models`, not this object.
|
||||
* Per-model `context_length` lives on the individual `HermesModel` entries and
|
||||
* flows through `custom_providers[].models`, not this object.
|
||||
*/
|
||||
export interface HermesSuggestedDefaults {
|
||||
model: {
|
||||
@@ -71,7 +73,8 @@ export interface HermesSuggestedDefaults {
|
||||
export type HermesApiMode =
|
||||
| "chat_completions"
|
||||
| "anthropic_messages"
|
||||
| "codex_responses";
|
||||
| "codex_responses"
|
||||
| "bedrock_converse";
|
||||
|
||||
/** Default mode used when a provider has no stored value yet. */
|
||||
export const HERMES_DEFAULT_API_MODE: HermesApiMode = "chat_completions";
|
||||
@@ -87,6 +90,10 @@ export const hermesApiModes: Array<{
|
||||
labelKey: "hermes.form.apiModeAnthropicMessages",
|
||||
},
|
||||
{ value: "codex_responses", labelKey: "hermes.form.apiModeCodexResponses" },
|
||||
{
|
||||
value: "bedrock_converse",
|
||||
labelKey: "hermes.form.apiModeBedrockConverse",
|
||||
},
|
||||
];
|
||||
|
||||
export interface HermesProviderPreset {
|
||||
@@ -115,6 +122,8 @@ export interface HermesProviderSettingsConfig {
|
||||
api_mode?: HermesApiMode;
|
||||
/** UI-side ordered list; serialized to YAML as a dict keyed by id. */
|
||||
models?: HermesModel[];
|
||||
/** Delay in seconds between consecutive requests to this provider. */
|
||||
rate_limit_delay?: number;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
@@ -154,19 +163,16 @@ export const hermesProviderPresets: HermesProviderPreset[] = [
|
||||
id: "anthropic/claude-opus-4-7",
|
||||
name: "Claude Opus 4.7",
|
||||
context_length: 1000000,
|
||||
max_tokens: 32000,
|
||||
},
|
||||
{
|
||||
id: "anthropic/claude-sonnet-4-6",
|
||||
name: "Claude Sonnet 4.6",
|
||||
context_length: 1000000,
|
||||
max_tokens: 32000,
|
||||
},
|
||||
{
|
||||
id: "anthropic/claude-haiku-4-5",
|
||||
name: "Claude Haiku 4.5",
|
||||
context_length: 200000,
|
||||
max_tokens: 32000,
|
||||
},
|
||||
{
|
||||
id: "openai/gpt-5.4",
|
||||
@@ -202,13 +208,11 @@ export const hermesProviderPresets: HermesProviderPreset[] = [
|
||||
id: "deepseek-chat",
|
||||
name: "DeepSeek V3.2",
|
||||
context_length: 128000,
|
||||
max_tokens: 8000,
|
||||
},
|
||||
{
|
||||
id: "deepseek-reasoner",
|
||||
name: "DeepSeek R1",
|
||||
context_length: 128000,
|
||||
max_tokens: 64000,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -1655,6 +1655,10 @@
|
||||
"apiModeChatCompletions": "OpenAI Chat Completions",
|
||||
"apiModeAnthropicMessages": "Anthropic Messages",
|
||||
"apiModeCodexResponses": "OpenAI Responses",
|
||||
"apiModeBedrockConverse": "AWS Bedrock Converse",
|
||||
"baseUrlRequired": "API endpoint is required",
|
||||
"baseUrlScheme": "Use an http:// or https:// address",
|
||||
"baseUrlInvalid": "API endpoint is not a valid URL",
|
||||
"models": "Models",
|
||||
"addModel": "Add model",
|
||||
"noModels": "No models configured. Switching to this provider won't change the default model.",
|
||||
@@ -1663,11 +1667,13 @@
|
||||
"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"
|
||||
"fallbackModel": "Alternate",
|
||||
"providerAdvanced": "Provider advanced options",
|
||||
"rateLimitDelay": "Rate limit delay (seconds)",
|
||||
"rateLimitDelayHint": "Minimum delay in seconds between consecutive requests (optional). Leave empty for no limit."
|
||||
},
|
||||
"webui": {
|
||||
"open": "Open Hermes Web UI",
|
||||
|
||||
@@ -1655,6 +1655,10 @@
|
||||
"apiModeChatCompletions": "OpenAI Chat Completions",
|
||||
"apiModeAnthropicMessages": "Anthropic Messages",
|
||||
"apiModeCodexResponses": "OpenAI Responses",
|
||||
"apiModeBedrockConverse": "AWS Bedrock Converse",
|
||||
"baseUrlRequired": "API エンドポイントは必須です",
|
||||
"baseUrlScheme": "http:// または https:// で始まるアドレスを指定してください",
|
||||
"baseUrlInvalid": "API エンドポイントは有効な URL ではありません",
|
||||
"models": "モデル一覧",
|
||||
"addModel": "モデルを追加",
|
||||
"noModels": "モデル未設定。このプロバイダーに切り替えてもデフォルトモデルは更新されません。",
|
||||
@@ -1663,11 +1667,13 @@
|
||||
"modelName": "表示名",
|
||||
"modelNamePlaceholder": "Claude Opus 4.7",
|
||||
"contextLength": "コンテキスト長",
|
||||
"maxTokens": "最大出力トークン",
|
||||
"advancedOptions": "詳細オプション",
|
||||
"modelsHint": "切り替え時、最初のモデルが model.default に書き込まれます。",
|
||||
"primaryModel": "デフォルト",
|
||||
"fallbackModel": "予備"
|
||||
"fallbackModel": "予備",
|
||||
"providerAdvanced": "プロバイダー詳細オプション",
|
||||
"rateLimitDelay": "リクエスト間隔(秒)",
|
||||
"rateLimitDelayHint": "連続したリクエスト間の最小待機秒数(任意)。空欄の場合は制限なし。"
|
||||
},
|
||||
"webui": {
|
||||
"open": "Hermes Web UI を開く",
|
||||
|
||||
@@ -1655,6 +1655,10 @@
|
||||
"apiModeChatCompletions": "OpenAI Chat Completions",
|
||||
"apiModeAnthropicMessages": "Anthropic Messages",
|
||||
"apiModeCodexResponses": "OpenAI Responses",
|
||||
"apiModeBedrockConverse": "AWS Bedrock Converse",
|
||||
"baseUrlRequired": "API 端点不能为空",
|
||||
"baseUrlScheme": "请使用 http:// 或 https:// 开头的地址",
|
||||
"baseUrlInvalid": "API 端点不是有效的 URL",
|
||||
"models": "模型列表",
|
||||
"addModel": "添加模型",
|
||||
"noModels": "暂无模型配置。切换到此供应商时将不会更新默认模型。",
|
||||
@@ -1663,11 +1667,13 @@
|
||||
"modelName": "显示名称",
|
||||
"modelNamePlaceholder": "Claude Opus 4.7",
|
||||
"contextLength": "上下文长度",
|
||||
"maxTokens": "最大输出 Tokens",
|
||||
"advancedOptions": "高级选项",
|
||||
"modelsHint": "切换到此供应商时,第一个模型会写入顶层 model.default。",
|
||||
"primaryModel": "默认模型",
|
||||
"fallbackModel": "备选模型"
|
||||
"fallbackModel": "备选模型",
|
||||
"providerAdvanced": "供应商高级选项",
|
||||
"rateLimitDelay": "请求间隔(秒)",
|
||||
"rateLimitDelayHint": "连续请求间的最小间隔秒数(可选)。留空表示无限制。"
|
||||
},
|
||||
"webui": {
|
||||
"open": "打开 Hermes Web UI",
|
||||
|
||||
Reference in New Issue
Block a user