use axum::http::HeaderMap;
use serde_json::{Map, Value, json};
use crate::adapters::{
AdapterOutcome, ClassificationRules, classify, common_session_event, event_name, metadata,
normalize_name, session_id, value_at,
};
use crate::model::{AgentKind, LlmEvent, NormalizedEvent};
pub(crate) fn adapt(payload: Value, headers: &HeaderMap) -> AdapterOutcome {
let event_name = event_name(&payload);
let normalized = normalize_name(&event_name);
if normalized == "preapirequest" {
return AdapterOutcome {
events: vec![crate::model::NormalizedEvent::LlmStarted(hermes_llm_event(
&payload,
headers,
&event_name,
))],
response: json!({}),
};
}
if normalized == "postapirequest" {
return AdapterOutcome {
events: vec![crate::model::NormalizedEvent::LlmEnded(hermes_llm_event(
&payload,
headers,
&event_name,
))],
response: json!({}),
};
}
if normalized == "apirequesterror" {
return AdapterOutcome {
events: vec![crate::model::NormalizedEvent::LlmEnded(hermes_llm_event(
&payload,
headers,
&event_name,
))],
response: json!({}),
};
}
if normalized == "onsessionend" {
return AdapterOutcome {
events: vec![NormalizedEvent::TurnEnded(common_session_event(
&payload,
headers,
AgentKind::Hermes,
))],
response: json!({}),
};
}
let events = classify(
&payload,
headers,
&ClassificationRules {
kind: AgentKind::Hermes,
agent_start: &["on_session_start", "sessionStart"],
agent_end: &["on_session_finalize", "on_session_reset"],
subagent_start: &["subagent_start", "subagentStart"],
subagent_end: &["subagent_stop", "subagentStop"],
tool_start: &["pre_tool_call", "preToolCall"],
tool_end: &["post_tool_call", "postToolCall"],
},
);
AdapterOutcome {
events,
response: json!({}),
}
}
fn hermes_llm_event(payload: &Value, headers: &HeaderMap, event_name: &str) -> LlmEvent {
let session_id = session_id(payload, headers);
let api_call_id = hermes_api_call_id(payload, &session_id);
let provider = hermes_string_at(payload, "provider")
.or_else(|| hermes_string_at(payload, "api_mode"))
.unwrap_or_else(|| "hermes_api_request".to_string());
let model_name =
hermes_string_at(payload, "response_model").or_else(|| hermes_string_at(payload, "model"));
let payload_exact = hermes_payload_exact(payload, event_name);
let mut event_metadata = metadata(payload, headers, AgentKind::Hermes, event_name);
if let Value::Object(ref mut object) = event_metadata {
object.insert("api_call_id".into(), json!(api_call_id.clone()));
object.insert("provider_payload_exact".into(), json!(payload_exact));
object.insert(
"fidelity_source".into(),
json!(if payload_exact {
"hermes_api_hooks_sanitized"
} else {
"hermes_api_hooks"
}),
);
}
LlmEvent {
session_id,
agent_kind: AgentKind::Hermes,
event_name: event_name.to_string(),
api_call_id,
provider,
model_name,
request: hermes_llm_request(payload),
response: hermes_llm_response(payload),
metadata: event_metadata,
}
}
fn hermes_api_call_id(payload: &Value, session_id: &str) -> String {
if let Some(api_request_id) = hermes_string_at(payload, "api_request_id") {
return api_request_id;
}
let task_id = hermes_string_at(payload, "task_id").unwrap_or_default();
let api_call_count = hermes_string_at(payload, "api_call_count").unwrap_or_default();
format!("{session_id}:{task_id}:{api_call_count}")
}
fn hermes_llm_request(payload: &Value) -> Value {
if let Some(request) = hermes_exact_request(payload) {
return request;
}
let mut object = Map::new();
for key in [
"task_id",
"session_id",
"platform",
"model",
"provider",
"base_url",
"api_mode",
"api_call_count",
"message_count",
"tool_count",
"approx_input_tokens",
"request_char_count",
"max_tokens",
] {
if let Some(value) = hermes_value_at(payload, key) {
object.insert(key.into(), value);
}
}
object.insert(
"fidelity".into(),
json!({
"provider_payload_exact": false,
"source": "hermes_pre_api_request"
}),
);
Value::Object(object)
}
fn hermes_llm_response(payload: &Value) -> Value {
if let Some(response) = hermes_exact_response(payload) {
return response;
}
let mut object = Map::new();
for key in [
"task_id",
"session_id",
"platform",
"model",
"provider",
"base_url",
"api_mode",
"api_call_count",
"api_duration",
"finish_reason",
"message_count",
"response_model",
"usage",
"assistant_content_chars",
"assistant_tool_call_count",
"status_code",
"retry_count",
"max_retries",
"retryable",
"reason",
"error",
] {
if let Some(value) = hermes_value_at(payload, key) {
object.insert(key.into(), value);
}
}
Value::Object(object)
}
fn hermes_payload_exact(payload: &Value, event_name: &str) -> bool {
match normalize_name(event_name).as_str() {
"preapirequest" => hermes_exact_request(payload).is_some(),
"postapirequest" => hermes_exact_response(payload).is_some(),
_ => false,
}
}
fn hermes_exact_request(payload: &Value) -> Option<Value> {
let request = hermes_value_at(payload, "request")?;
if request.is_null() || is_truncated_payload(&request) {
return None;
}
request
.get("body")
.filter(|body| !body.is_null())
.cloned()
.or(Some(request))
}
fn hermes_exact_response(payload: &Value) -> Option<Value> {
let response = hermes_value_at(payload, "response")?;
if is_truncated_payload(&response) {
return None;
}
if let Some(raw_response) = response
.get("raw_response")
.filter(|raw_response| !raw_response.is_null() && !is_truncated_payload(raw_response))
{
return Some(raw_response.clone());
}
if response.get("choices").is_some()
|| response.get("output").is_some()
|| response.get("content").is_some()
{
return Some(response);
}
let assistant_message = response.get("assistant_message")?;
let mut object = Map::new();
if let Some(content) = assistant_message.get("content") {
object.insert("content".into(), content.clone());
}
if let Some(tool_calls) = assistant_message.get("tool_calls") {
object.insert("tool_calls".into(), tool_calls.clone());
}
if let Some(usage) = response
.get("usage")
.cloned()
.or_else(|| hermes_value_at(payload, "usage"))
{
object.insert("usage".into(), usage);
}
for key in ["model", "finish_reason"] {
if let Some(value) = response
.get(key)
.cloned()
.or_else(|| hermes_value_at(payload, key))
{
object.insert(key.into(), value);
}
}
(!object.is_empty()).then_some(Value::Object(object))
}
fn is_truncated_payload(value: &Value) -> bool {
value
.get("_truncated")
.and_then(Value::as_bool)
.unwrap_or(false)
}
fn hermes_string_at(payload: &Value, key: &str) -> Option<String> {
value_at(payload, &[key])
.or_else(|| value_at(payload, &["extra", key]))
.and_then(|value| match value {
Value::String(value) => Some(value),
Value::Number(value) => Some(value.to_string()),
Value::Bool(value) => Some(value.to_string()),
_ => None,
})
.filter(|value| !value.is_empty())
}
fn hermes_value_at(payload: &Value, key: &str) -> Option<Value> {
value_at(payload, &[key]).or_else(|| value_at(payload, &["extra", key]))
}