fix(hermes): stop health check from borrowing OpenClaw schema

Hermes providers were routed through check_additive_app_stream, the
OpenClaw dispatcher, which reads camelCase fields (baseUrl/apiKey/api)
and emits "OpenClaw is missing ..." errors. Hermes stores snake_case
fields (base_url/api_key/api_mode) with different protocol tags, so
users saw "OpenClaw provider is missing baseUrl" even after filling in
every Hermes field correctly.

Introduce check_hermes_stream with Hermes-specific extractors. Route
api_mode (chat_completions / anthropic_messages / codex_responses) to
the matching check_claude_stream api_format, and return bedrock_converse
as unsupported. Resolve api_mode before extracting URL/API key so users
who picked bedrock_converse see the real cause first rather than a
misleading "missing base_url" message.
This commit is contained in:
Jason
2026-04-21 23:00:37 +08:00
parent 4dd11ab427
commit 7ac822d241
+107 -1
View File
@@ -704,7 +704,7 @@ impl StreamCheckService {
.await
}
AppType::Hermes => {
Self::check_additive_app_stream(
Self::check_hermes_stream(
&client,
provider,
&model_to_test,
@@ -982,6 +982,112 @@ impl StreamCheckService {
.filter(|s| !s.is_empty())
}
// Hermes 的 settings_config 用 snake_casebase_url / api_key / api_mode),
// 与 OpenClaw 的 camelCasebaseUrl / apiKey / api)是两套独立命名。
// 见 src/config/hermesProviderPresets.ts 的 HermesProviderSettingsConfig。
fn extract_hermes_base_url(provider: &Provider) -> Result<String, AppError> {
provider
.settings_config
.get("base_url")
.and_then(|v| v.as_str())
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.ok_or_else(|| {
AppError::localized(
"hermes_base_url_missing",
"Hermes 供应商缺少 base_url",
"Hermes provider is missing `base_url`",
)
})
}
fn extract_hermes_api_key(provider: &Provider) -> Result<String, AppError> {
provider
.settings_config
.get("api_key")
.and_then(|v| v.as_str())
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.ok_or_else(|| {
AppError::localized(
"hermes_api_key_missing",
"Hermes 供应商缺少 api_key",
"Hermes provider is missing `api_key`",
)
})
}
fn extract_hermes_api_mode(provider: &Provider) -> Option<String> {
provider
.settings_config
.get("api_mode")
.and_then(|v| v.as_str())
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
}
/// Hermes 流式检查分发器
///
/// Hermes 以 `api_mode` 字段显式指定协议,取值来自
/// `HermesApiMode`hermesProviderPresets.ts):
/// - `chat_completions` → check_claude_stream + api_format="openai_chat"Bearer
/// - `anthropic_messages` → check_claude_stream + api_format="anthropic"ClaudeAuth,与 OpenClaw 的 anthropic-messages 同策略)
/// - `codex_responses` → check_claude_stream + api_format="openai_responses"Bearer
/// - `bedrock_converse` → 不支持(需要 AWS SigV4 签名)
async fn check_hermes_stream(
client: &Client,
provider: &Provider,
model: &str,
test_prompt: &str,
timeout: std::time::Duration,
) -> Result<(u16, String), AppError> {
// 先把 api_mode 路由出协议格式与认证策略。
// 纯错误路径(bedrock / 未知 / 缺失)直接 return,避免在用户
// 选了 bedrock_converse 时被"缺 base_url"的二级错误盖住真正原因。
let (api_format, auth_strategy) = match Self::extract_hermes_api_mode(provider).as_deref() {
Some("chat_completions") => ("openai_chat", AuthStrategy::Bearer),
Some("anthropic_messages") => ("anthropic", AuthStrategy::ClaudeAuth),
Some("codex_responses") => ("openai_responses", AuthStrategy::Bearer),
Some("bedrock_converse") => {
return Err(AppError::localized(
"hermes_bedrock_not_supported",
"AWS Bedrock 需要 SigV4 签名,当前不支持健康检查。",
"AWS Bedrock requires SigV4 signing and is not supported by stream health check.",
));
}
Some(other) => {
return Err(AppError::localized(
"hermes_protocol_not_yet_supported",
format!("Hermes 暂不支持协议: {other}"),
format!("Hermes protocol not yet supported: {other}"),
));
}
None => {
return Err(AppError::localized(
"hermes_api_mode_missing",
"Hermes 供应商缺少 api_mode 字段",
"Hermes provider is missing the `api_mode` field",
));
}
};
let base_url = Self::extract_hermes_base_url(provider)?;
let api_key = Self::extract_hermes_api_key(provider)?;
let auth = AuthInfo::new(api_key, auth_strategy);
Self::check_claude_stream(
client,
&base_url,
&auth,
model,
test_prompt,
timeout,
provider,
Some(api_format),
None,
)
.await
}
/// OpenCode 流式检查分发器
///
/// OpenCode 用 `npm` 字段(AI SDK 包名)隐式指定协议。映射关系参见