mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-05-06 22:01:44 +08:00
refactor(hermes): drop "Auto" api_mode and require explicit protocol
Hermes' built-in api_mode detection only matches a handful of official endpoints (api.openai.com, api.anthropic.com, api.x.ai, AWS Bedrock); third-party / proxy endpoints silently fall back to chat_completions, which causes opaque 401/404s on Anthropic-protocol or Codex-Responses providers. The "Auto" option was misleading for the common third-party case. - Drop the "Auto" option from the API Mode dropdown; remove the HermesApiModeChoice sentinel type so writes always emit api_mode. - Default new providers and legacy entries lacking api_mode to chat_completions (only persisted on user save). - Deeplink imports now write api_mode: chat_completions explicitly instead of relying on URL heuristics; test renamed accordingly. - Rename the "Codex Responses (Copilot / OpenCode)" label to "OpenAI Responses" to match OpenAI's /v1/responses naming.
This commit is contained in:
@@ -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]
|
||||
|
||||
@@ -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({
|
||||
</FormLabel>
|
||||
<Select
|
||||
value={apiMode}
|
||||
onValueChange={(v) => onApiModeChange(v as HermesApiModeChoice)}
|
||||
onValueChange={(v) => onApiModeChange(v as HermesApiMode)}
|
||||
>
|
||||
<SelectTrigger id="hermes-api-mode">
|
||||
<SelectValue />
|
||||
@@ -196,8 +196,7 @@ export function HermesFormFields({
|
||||
</Select>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("hermes.form.apiModeHint", {
|
||||
defaultValue:
|
||||
"供应商 API 协议。Auto 表示让 Hermes 按端点自动判断。",
|
||||
defaultValue: "供应商 API 协议。请根据端点选择正确的协议。",
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -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<HermesProviderSettingsConfig>) => void;
|
||||
}
|
||||
@@ -92,17 +93,15 @@ 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 [hermesApiMode, setHermesApiMode] = useState<HermesApiMode>(() => {
|
||||
if (appId !== "hermes") return HERMES_DEFAULT_API_MODE;
|
||||
const stored = parseHermesField<HermesApiMode | "">(
|
||||
initialData,
|
||||
"api_mode",
|
||||
"",
|
||||
);
|
||||
return stored || HERMES_DEFAULT_API_MODE;
|
||||
});
|
||||
|
||||
const [hermesModels, setHermesModels] = useState<HermesModel[]>(() => {
|
||||
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 ?? []);
|
||||
},
|
||||
[],
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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": "モデル未設定。このプロバイダーに切り替えてもデフォルトモデルは更新されません。",
|
||||
|
||||
@@ -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": "暂无模型配置。切换到此供应商时将不会更新默认模型。",
|
||||
|
||||
Reference in New Issue
Block a user