diff --git a/src-tauri/src/deeplink/provider.rs b/src-tauri/src/deeplink/provider.rs index 32fa77b58..2ae90a013 100644 --- a/src-tauri/src/deeplink/provider.rs +++ b/src-tauri/src/deeplink/provider.rs @@ -428,8 +428,11 @@ fn build_additive_app_settings(request: &DeepLinkImportRequest) -> serde_json::V /// Emitting camelCase here — as the OpenClaw path does — would poison the /// YAML with unknown root fields the Hermes runtime ignores. /// -/// `api_mode` is deliberately omitted so Hermes auto-detects the protocol -/// from the endpoint; callers can still override via the UI later. +/// `api_mode` is always written explicitly. Deeplinks have no field to carry +/// it, so we default to `chat_completions` (the most widely compatible +/// protocol) and let the user adjust via the UI after import. We never rely +/// on Hermes' built-in URL heuristics, which only recognize a handful of +/// official endpoints. fn build_hermes_settings(request: &DeepLinkImportRequest) -> serde_json::Value { let endpoint = get_primary_endpoint(request); @@ -447,6 +450,8 @@ fn build_hermes_settings(request: &DeepLinkImportRequest) -> serde_json::Value { config.insert("api_key".to_string(), json!(api_key)); } + config.insert("api_mode".to_string(), json!("chat_completions")); + if let Some(model) = &request.model { config.insert( "models".to_string(), @@ -784,11 +789,12 @@ mod tests { } #[test] - fn build_hermes_settings_omits_api_mode_for_auto_detect() { + fn build_hermes_settings_writes_default_api_mode() { let settings = build_hermes_settings(&hermes_request()); - assert!( - settings.as_object().unwrap().get("api_mode").is_none(), - "api_mode must be omitted so Hermes auto-detects" + assert_eq!( + settings.as_object().unwrap().get("api_mode").unwrap(), + "chat_completions", + "api_mode must be written explicitly so Hermes never falls back to URL auto-detection" ); } @@ -810,6 +816,7 @@ mod tests { assert!(obj.get("base_url").is_none()); assert!(obj.get("api_key").is_none()); assert!(obj.get("models").is_none()); + assert_eq!(obj.get("api_mode").unwrap(), "chat_completions"); } #[test] diff --git a/src/components/providers/forms/HermesFormFields.tsx b/src/components/providers/forms/HermesFormFields.tsx index e74b5365b..598a2e0d7 100644 --- a/src/components/providers/forms/HermesFormFields.tsx +++ b/src/components/providers/forms/HermesFormFields.tsx @@ -40,7 +40,7 @@ import { } from "@/lib/api/model-fetch"; import { hermesApiModes, - type HermesApiModeChoice, + type HermesApiMode, type HermesModel, } from "@/config/hermesProviderPresets"; import type { ProviderCategory } from "@/types"; @@ -55,8 +55,8 @@ interface HermesFormFieldsProps { websiteUrl: string; isPartner?: boolean; partnerPromotionKey?: string; - apiMode: HermesApiModeChoice; - onApiModeChange: (mode: HermesApiModeChoice) => void; + apiMode: HermesApiMode; + onApiModeChange: (mode: HermesApiMode) => void; models: HermesModel[]; onModelsChange: (models: HermesModel[]) => void; } @@ -181,7 +181,7 @@ export function HermesFormFields({

{t("hermes.form.apiModeHint", { - defaultValue: - "供应商 API 协议。Auto 表示让 Hermes 按端点自动判断。", + defaultValue: "供应商 API 协议。请根据端点选择正确的协议。", })}

diff --git a/src/components/providers/forms/hooks/useHermesFormState.ts b/src/components/providers/forms/hooks/useHermesFormState.ts index d788e367b..44c5450eb 100644 --- a/src/components/providers/forms/hooks/useHermesFormState.ts +++ b/src/components/providers/forms/hooks/useHermesFormState.ts @@ -1,10 +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, +import { + HERMES_DEFAULT_API_MODE, + type HermesApiMode, + type HermesModel, + type HermesProviderSettingsConfig, } from "@/config/hermesProviderPresets"; interface UseHermesFormStateParams { @@ -34,12 +35,12 @@ export interface HermesFormState { setHermesProviderKey: (key: string) => void; hermesBaseUrl: string; hermesApiKey: string; - hermesApiMode: HermesApiModeChoice; + hermesApiMode: HermesApiMode; hermesModels: HermesModel[]; existingHermesKeys: string[]; handleHermesBaseUrlChange: (baseUrl: string) => void; handleHermesApiKeyChange: (apiKey: string) => void; - handleHermesApiModeChange: (mode: HermesApiModeChoice) => void; + handleHermesApiModeChange: (mode: HermesApiMode) => void; handleHermesModelsChange: (models: HermesModel[]) => void; resetHermesState: (config?: Partial) => void; } @@ -92,17 +93,15 @@ export function useHermesFormState({ return parseHermesField(initialData, "api_key", ""); }); - const [hermesApiMode, setHermesApiMode] = useState( - () => { - if (appId !== "hermes") return "auto"; - const stored = parseHermesField( - initialData, - "api_mode", - "", - ); - return stored || "auto"; - }, - ); + const [hermesApiMode, setHermesApiMode] = useState(() => { + if (appId !== "hermes") return HERMES_DEFAULT_API_MODE; + const stored = parseHermesField( + initialData, + "api_mode", + "", + ); + return stored || HERMES_DEFAULT_API_MODE; + }); const [hermesModels, setHermesModels] = useState(() => { if (appId !== "hermes") return []; @@ -143,14 +142,10 @@ export function useHermesFormState({ ); const handleHermesApiModeChange = useCallback( - (mode: HermesApiModeChoice) => { + (mode: HermesApiMode) => { setHermesApiMode(mode); updateHermesConfig((config) => { - if (mode === "auto") { - delete config.api_mode; - } else { - config.api_mode = mode; - } + config.api_mode = mode; }); }, [updateHermesConfig], @@ -175,7 +170,7 @@ export function useHermesFormState({ setHermesProviderKey(""); setHermesBaseUrl(config?.base_url || ""); setHermesApiKey(config?.api_key || ""); - setHermesApiMode(config?.api_mode ?? "auto"); + setHermesApiMode(config?.api_mode ?? HERMES_DEFAULT_API_MODE); setHermesModels(config?.models ?? []); }, [], diff --git a/src/config/hermesProviderPresets.ts b/src/config/hermesProviderPresets.ts index bcc41e523..de5dbabbd 100644 --- a/src/config/hermesProviderPresets.ts +++ b/src/config/hermesProviderPresets.ts @@ -67,30 +67,20 @@ export interface HermesSuggestedDefaults { }; } -/** Hermes custom_provider protocol mode (optional; auto-detected when omitted). */ +/** Hermes custom_provider protocol mode. Always written explicitly. */ export type HermesApiMode = | "chat_completions" | "anthropic_messages" | "codex_responses"; -/** - * 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; +/** Default mode used when a provider has no stored value yet. */ +export const HERMES_DEFAULT_API_MODE: HermesApiMode = "chat_completions"; -/** - * 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". - */ +/** Dropdown options for the API Mode selector. `labelKey` is looked up in i18n. */ export const hermesApiModes: Array<{ - value: HermesApiModeChoice; + value: HermesApiMode; labelKey: string; }> = [ - { value: "auto", labelKey: "hermes.form.apiModeAuto" }, { value: "chat_completions", labelKey: "hermes.form.apiModeChatCompletions" }, { value: "anthropic_messages", diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index f3616a1d3..144b7eb16 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -1651,11 +1651,10 @@ "providerKeyInvalid": "Provider key can only contain lowercase letters, numbers, and hyphens", "providerKeyDuplicate": "This provider key already exists", "apiMode": "API Mode", - "apiModeHint": "Provider API protocol. Auto lets Hermes detect it from the endpoint.", - "apiModeAuto": "Auto-detect", + "apiModeHint": "Provider API protocol. Choose the format that matches your endpoint.", "apiModeChatCompletions": "OpenAI Chat Completions", "apiModeAnthropicMessages": "Anthropic Messages", - "apiModeCodexResponses": "Codex Responses (Copilot / OpenCode)", + "apiModeCodexResponses": "OpenAI Responses", "models": "Models", "addModel": "Add model", "noModels": "No models configured. Switching to this provider won't change the default model.", diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json index 1389b7ce6..3dc46b8b6 100644 --- a/src/i18n/locales/ja.json +++ b/src/i18n/locales/ja.json @@ -1651,11 +1651,10 @@ "providerKeyInvalid": "プロバイダーキーは小文字、数字、ハイフンのみ使用できます", "providerKeyDuplicate": "このプロバイダーキーは既に存在します", "apiMode": "API モード", - "apiModeHint": "プロバイダーの API プロトコル。Auto はエンドポイントから自動判定します。", - "apiModeAuto": "自動検出", + "apiModeHint": "プロバイダーの API プロトコル。エンドポイントに合わせて正しい形式を選択してください。", "apiModeChatCompletions": "OpenAI Chat Completions", "apiModeAnthropicMessages": "Anthropic Messages", - "apiModeCodexResponses": "Codex Responses(Copilot / OpenCode)", + "apiModeCodexResponses": "OpenAI Responses", "models": "モデル一覧", "addModel": "モデルを追加", "noModels": "モデル未設定。このプロバイダーに切り替えてもデフォルトモデルは更新されません。", diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json index 74ec9e379..0df36ad9e 100644 --- a/src/i18n/locales/zh.json +++ b/src/i18n/locales/zh.json @@ -1651,11 +1651,10 @@ "providerKeyInvalid": "供应商标识只能包含小写字母、数字和连字符", "providerKeyDuplicate": "该供应商标识已存在", "apiMode": "API 模式", - "apiModeHint": "供应商 API 协议。Auto 表示让 Hermes 按端点自动判断。", - "apiModeAuto": "自动检测", + "apiModeHint": "供应商 API 协议。请根据端点选择正确的协议。", "apiModeChatCompletions": "OpenAI Chat Completions", "apiModeAnthropicMessages": "Anthropic Messages", - "apiModeCodexResponses": "Codex Responses(Copilot / OpenCode)", + "apiModeCodexResponses": "OpenAI Responses", "models": "模型列表", "addModel": "添加模型", "noModels": "暂无模型配置。切换到此供应商时将不会更新默认模型。",