feat(aihr): expose model usage telemetry

This commit is contained in:
2026-07-14 11:39:46 +08:00
parent d22e849c9f
commit ab26750fb8
5 changed files with 50 additions and 9 deletions
@@ -70,6 +70,14 @@ public final class AihrModelDto {
public record ChatRequest(String prompt, String model, String systemPrompt) {
}
public record ModelUsage(
Integer promptTokens,
Integer completionTokens,
Integer totalTokens,
long latencyMs
) {
}
public record ChatResponse(
boolean configured,
String providerCode,
@@ -77,7 +85,8 @@ public final class AihrModelDto {
String answer,
String mode,
String error,
List<String> hints
List<String> hints,
ModelUsage usage
) {
}
}
@@ -14,6 +14,7 @@ import org.dromara.aihr.domain.AihrModelDto.ConfigResponse;
import org.dromara.aihr.domain.AihrModelDto.ProviderRequest;
import org.dromara.aihr.domain.AihrModelDto.ProviderResponse;
import org.dromara.aihr.domain.AihrModelDto.ProviderStatusRequest;
import org.dromara.aihr.domain.AihrModelDto.ModelUsage;
import org.dromara.common.satoken.utils.LoginHelper;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.dao.DataAccessException;
@@ -200,7 +201,8 @@ public class AihrModelSeedService {
"AI 调用已被试点成本闸门关闭,当前返回文本兜底:先安抚业主情绪,确认事实与责任人,再承诺首次反馈时间。",
"cost-guard-fallback",
null,
List.of("需要恢复真实调用时设置 AIHR_AI_RUNTIME_ENABLED=true 且 AIHR_AI_CHAT_ENABLED=true。")
List.of("需要恢复真实调用时设置 AIHR_AI_RUNTIME_ENABLED=true 且 AIHR_AI_CHAT_ENABLED=true。"),
null
);
}
@@ -215,13 +217,14 @@ public class AihrModelSeedService {
List.of(
"在 aihr_model_provider / aihr_model_config 配置 api_host、api_key、model_name 后可切换到真实 OpenAI-compatible 调用。",
"接口不会返回 api_key。"
)
),
null
);
}
try {
String content = callOpenAiCompatible(runtime, modelName, prompt, request == null ? null : request.systemPrompt(), 0.2);
return new ChatResponse(true, runtime.providerCode(), modelName, content, "openai-compatible", null, List.of());
ModelCallResult call = callOpenAiCompatible(runtime, modelName, prompt, request == null ? null : request.systemPrompt(), 0.2);
return new ChatResponse(true, runtime.providerCode(), modelName, call.content(), "openai-compatible", null, List.of(), call.usage());
} catch (Exception e) {
return new ChatResponse(
true,
@@ -230,7 +233,8 @@ public class AihrModelSeedService {
"真实模型调用失败,已回退内置默认兜底:先确认事实、同步时限、再生成工单闭环。",
"openai-compatible-failed",
"模型调用失败,已回退内置兜底;请检查模型供应商配置。",
List.of("检查 aihr_model_provider.api_host/api_key 和 aihr_model_config.model_name 是否与供应商一致。")
List.of("检查 aihr_model_provider.api_host/api_key 和 aihr_model_config.model_name 是否与供应商一致。"),
null
);
}
}
@@ -284,7 +288,7 @@ public class AihrModelSeedService {
return Optional.empty();
}
try {
return Optional.of(callOpenAiCompatible(runtime, runtime.modelName(), userPrompt, systemPrompt, temperature));
return Optional.of(callOpenAiCompatible(runtime, runtime.modelName(), userPrompt, systemPrompt, temperature).content());
} catch (Exception e) {
log.warn("aihr llm tryChat failed, caller falls back to seed(处理错误已隐藏)");
return Optional.empty();
@@ -376,7 +380,8 @@ public class AihrModelSeedService {
}
}
private String callOpenAiCompatible(RuntimeConfig runtime, String modelName, String prompt, String systemPrompt, double temperature) throws Exception {
private ModelCallResult callOpenAiCompatible(RuntimeConfig runtime, String modelName, String prompt, String systemPrompt, double temperature) throws Exception {
long startedAt = System.nanoTime();
ObjectNode body = objectMapper.createObjectNode();
body.put("model", modelName);
body.put("temperature", temperature);
@@ -422,7 +427,21 @@ public class AihrModelSeedService {
if (isBlank(content)) {
throw new IllegalStateException("LLM response missing message.content");
}
return content;
JsonNode usage = root.path("usage");
ModelUsage modelUsage = new ModelUsage(
integerUsage(usage, "prompt_tokens"),
integerUsage(usage, "completion_tokens"),
integerUsage(usage, "total_tokens"),
Duration.ofNanos(System.nanoTime() - startedAt).toMillis()
);
return new ModelCallResult(content, modelUsage);
}
private static Integer integerUsage(JsonNode usage, String field) {
if (usage == null || usage.isMissingNode() || !usage.has(field) || !usage.get(field).canConvertToInt()) {
return null;
}
return usage.get(field).intValue();
}
private RuntimeConfig runtimeConfig(String requestedModel) {
@@ -593,6 +612,9 @@ public class AihrModelSeedService {
private record RuntimeConfig(String providerCode, String modelName, String baseUrl, String apiKey, boolean configured, String source) {
}
private record ModelCallResult(String content, ModelUsage usage) {
}
private record ProviderData(
String providerName,
String providerCode,
@@ -48,6 +48,9 @@ class AihrSensitiveTextTest {
String speechControllerSource = Files.readString(controllerSource("AihrSpeechController.java"));
assertTrue(modelSource.contains("AihrSensitiveText.forModel(prompt)"));
assertTrue(modelSource.contains("prompt_tokens"));
assertTrue(modelSource.contains("completion_tokens"));
assertTrue(modelSource.contains("latencyMs"));
assertTrue(modelSource.contains("外部响应体已隐藏"));
assertTrue(modelSource.contains("模型调用失败,已回退内置兜底;请检查模型供应商配置。"));
assertTrue(sopSource.contains("AihrSensitiveText.forModel(renderPrompt(prompt.template(), queryText, context))"));