拒绝未完整结束的图标规范元数据
图标规范元数据解析统一检查 OpenAI Chat finish_reason length 与 content_filter 结果进入既有重试循环 新增完整 JSON 仍被不完整原因拒绝的回归测试
This commit is contained in:
@@ -664,33 +664,22 @@ async fn run_icon_spec_extra_param_llm(
|
||||
|
||||
for attempt in 1..=ICON_SPEC_LLM_MAX_ATTEMPTS {
|
||||
match llm_client.run(request.clone()).await {
|
||||
Ok(response) => {
|
||||
let text = response.text.trim();
|
||||
if text.is_empty() {
|
||||
if attempt == ICON_SPEC_LLM_MAX_ATTEMPTS {
|
||||
return Err(AppError::from_status(StatusCode::BAD_GATEWAY).with_details(
|
||||
json!({
|
||||
"provider": "editor-icon-spec-llm",
|
||||
"operation": "complete-extra-param",
|
||||
"message": "LLM 未返回可用文本",
|
||||
}),
|
||||
));
|
||||
}
|
||||
} else {
|
||||
match parse_extra_param(text) {
|
||||
Ok(extra_param) => return Ok(extra_param),
|
||||
Err(_) if attempt < ICON_SPEC_LLM_MAX_ATTEMPTS => {}
|
||||
Err(_) => {
|
||||
return Err(AppError::from_status(StatusCode::BAD_GATEWAY)
|
||||
.with_details(json!({
|
||||
"provider": "editor-icon-spec-llm",
|
||||
"operation": "complete-extra-param",
|
||||
"message": "LLM 返回内容不符合图标规范参数契约",
|
||||
})));
|
||||
}
|
||||
}
|
||||
Ok(response) => match parse_extra_param_response(
|
||||
response.text.as_str(),
|
||||
response.finish_reason.as_deref(),
|
||||
) {
|
||||
Ok(extra_param) => return Ok(extra_param),
|
||||
Err(_) if attempt < ICON_SPEC_LLM_MAX_ATTEMPTS => {}
|
||||
Err(error) => {
|
||||
return Err(AppError::from_status(StatusCode::BAD_GATEWAY).with_details(
|
||||
json!({
|
||||
"provider": "editor-icon-spec-llm",
|
||||
"operation": "complete-extra-param",
|
||||
"message": error.message(),
|
||||
}),
|
||||
));
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(error) if is_retryable_llm_error(&error) => {
|
||||
if attempt == ICON_SPEC_LLM_MAX_ATTEMPTS {
|
||||
return Err(map_llm_error(error));
|
||||
@@ -703,6 +692,39 @@ async fn run_icon_spec_extra_param_llm(
|
||||
unreachable!("icon spec extra param retry loop always returns")
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum ExtraParamResponseError {
|
||||
Empty,
|
||||
Incomplete,
|
||||
Invalid,
|
||||
}
|
||||
|
||||
impl ExtraParamResponseError {
|
||||
fn message(self) -> &'static str {
|
||||
match self {
|
||||
Self::Empty => "LLM 未返回可用文本",
|
||||
Self::Incomplete => "LLM 返回内容未完整结束",
|
||||
Self::Invalid => "LLM 返回内容不符合图标规范参数契约",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_extra_param_response(
|
||||
text: &str,
|
||||
finish_reason: Option<&str>,
|
||||
) -> Result<ExtraParam, ExtraParamResponseError> {
|
||||
if finish_reason.is_some_and(|reason| {
|
||||
platform_llm::is_incomplete_finish_reason(platform_llm::LlmApiKind::OpenAiChat, reason)
|
||||
}) {
|
||||
return Err(ExtraParamResponseError::Incomplete);
|
||||
}
|
||||
let text = text.trim();
|
||||
if text.is_empty() {
|
||||
return Err(ExtraParamResponseError::Empty);
|
||||
}
|
||||
parse_extra_param(text).map_err(|_| ExtraParamResponseError::Invalid)
|
||||
}
|
||||
|
||||
fn parse_extra_param(text: &str) -> Result<ExtraParam, ()> {
|
||||
let mut extra_param = serde_json::from_str::<ExtraParam>(text).map_err(|_| ())?;
|
||||
for value in [
|
||||
@@ -2752,6 +2774,20 @@ mod tests {
|
||||
assert!(parse_extra_param(oversized.to_string().as_str()).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extra_param_rejects_explicitly_incomplete_llm_responses() {
|
||||
let complete_json =
|
||||
r#"{"genre":"策略","theme":"现代军事","useCase":"PC","targetUser":"策略游戏玩家"}"#;
|
||||
|
||||
for finish_reason in ["length", "content_filter"] {
|
||||
assert_eq!(
|
||||
parse_extra_param_response(complete_json, Some(finish_reason)),
|
||||
Err(ExtraParamResponseError::Incomplete)
|
||||
);
|
||||
}
|
||||
assert!(parse_extra_param_response(complete_json, Some("stop")).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn icon_spec_generation_text_enforces_prompt_max_length() {
|
||||
assert!(
|
||||
|
||||
Reference in New Issue
Block a user