diff --git a/server-rs/crates/platform-llm/README.md b/server-rs/crates/platform-llm/README.md index 8a9313433..9fcacd5c2 100644 --- a/server-rs/crates/platform-llm/README.md +++ b/server-rs/crates/platform-llm/README.md @@ -88,5 +88,5 @@ Responses 如果只发送 `response.completed` 或 `response.incomplete`,解 ## 9. 验收证据边界 1. `cargo test -p platform-llm` 的确定性用例把 checked-in SSE fixture 交给 parser,验证归一后的文本、工具调用、slot 聚合、Responses 仅有 completed / incomplete 终态事件时的恢复、参数 JSON 完整性和错误边界。fixture 可以来源于真实端点抓包,但测试不保存原始 SSE,也不逐事件与端点报文比较,因此不能证明抓包转录无偏差。 -2. `tests/live_stream_tool_calls.rs` 是默认 `#[ignore]` 的真实端点工具调用 smoke。它只验证最终归一结果中的工具名、id 和完整参数 JSON;文本增量字符数仅用于打印观测,工具调用不进入 `on_delta`,也没有原始 SSE 录制或逐事件对比能力。 +2. `tests/live_stream_tool_calls.rs` 是默认 `#[ignore]` 的真实端点工具调用 smoke。`PLATFORM_LLM_LIVE_API_KIND` 支持 `openai_responses`、`openai_chat` 和 `anthropic`,未设置或空白时默认 `openai_responses`,未知非空值会直接使验收失败。它只验证最终归一结果中的工具名、id 和完整参数 JSON;文本增量字符数仅用于打印观测,工具调用不进入 `on_delta`,也没有原始 SSE 录制或逐事件对比能力。 3. 因此验收应分别称为“固定 SSE fixture parser 覆盖”和“真实端点归一工具调用 smoke”,不能把后者描述为原始 SSE fidelity 或转录一致性证明。 diff --git a/server-rs/crates/platform-llm/tests/live_stream_tool_calls.rs b/server-rs/crates/platform-llm/tests/live_stream_tool_calls.rs index 8307815e8..d40ee8def 100644 --- a/server-rs/crates/platform-llm/tests/live_stream_tool_calls.rs +++ b/server-rs/crates/platform-llm/tests/live_stream_tool_calls.rs @@ -26,11 +26,58 @@ fn env_var(name: &str) -> Option { .filter(|value| !value.trim().is_empty()) } -fn parse_api_kind(value: &str) -> LlmApiKind { - match value.trim().to_ascii_lowercase().replace('-', "_").as_str() { - "anthropic" => LlmApiKind::Anthropic, - "openai_chat" => LlmApiKind::OpenAiChat, - _ => LlmApiKind::OpenAiResponses, +fn parse_api_kind(value: &str) -> Result { + let normalized = value.trim().to_ascii_lowercase().replace('-', "_"); + if normalized.is_empty() { + return Ok(LlmApiKind::OpenAiResponses); + } + + match normalized.as_str() { + "anthropic" => Ok(LlmApiKind::Anthropic), + "openai_chat" => Ok(LlmApiKind::OpenAiChat), + "openai_responses" => Ok(LlmApiKind::OpenAiResponses), + value => Err(format!( + "PLATFORM_LLM_LIVE_API_KIND 无效:{value},请使用 openai_responses、openai_chat 或 anthropic" + )), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parse_api_kind_defaults_only_for_empty_value() { + assert_eq!( + parse_api_kind("").expect("empty api kind should default"), + LlmApiKind::OpenAiResponses + ); + assert_eq!( + parse_api_kind(" ").expect("whitespace api kind should default"), + LlmApiKind::OpenAiResponses + ); + } + + #[test] + fn parse_api_kind_accepts_supported_values() { + assert_eq!( + parse_api_kind("anthropic").expect("anthropic should parse"), + LlmApiKind::Anthropic + ); + assert_eq!( + parse_api_kind("OPENAI-CHAT").expect("openai chat should parse"), + LlmApiKind::OpenAiChat + ); + assert_eq!( + parse_api_kind("openai_responses").expect("openai responses should parse"), + LlmApiKind::OpenAiResponses + ); + } + + #[test] + fn parse_api_kind_rejects_unknown_non_empty_value() { + let error = parse_api_kind("anthopic").expect_err("misspelled api kind must fail"); + assert!(error.contains("anthopic")); } } @@ -44,7 +91,12 @@ async fn live_stream_run_returns_native_tool_calls() { ) else { panic!("缺少 PLATFORM_LLM_LIVE_BASE_URL / _API_KEY / _MODEL"); }; - let api_kind = parse_api_kind(&env_var("PLATFORM_LLM_LIVE_API_KIND").unwrap_or_default()); + let api_kind = parse_api_kind( + env_var("PLATFORM_LLM_LIVE_API_KIND") + .as_deref() + .unwrap_or_default(), + ) + .unwrap_or_else(|error| panic!("{error}")); let config = LlmConfig::new( LlmProvider::OpenAiCompatible,