修复(LLM目录契约): 响应枚举未知取值前向兼容

- LlmModelsResponse 内 agentMode / protocol 遇到未知 token 回退 Codex / 由 agentMode 推导默认协议,不再让旧客户端整份反序列化失败
- 输入校验仍走严格 parse;补未知 token 用例
This commit is contained in:
2026-10-05 12:19:15 +08:00
parent c7150b590c
commit 4bd080eff5
@@ -124,15 +124,44 @@ impl AgcModelProtocol {
}
}
/// 响应 DTO 的前向兼容:未知执行器回退 `Codex`,避免后端新增取值时旧客户端整份
/// [`LlmModelsResponse`] 反序列化失败。写入 / 输入校验仍走严格的 [`AgcAgentMode::parse`]。
fn deserialize_forward_compat_agent_mode<'de, D>(deserializer: D) -> Result<AgcAgentMode, D::Error>
where
D: serde::Deserializer<'de>,
{
let raw = Option::<String>::deserialize(deserializer)?;
Ok(match raw.as_deref() {
Some("cc") | Some("anthropic") => AgcAgentMode::Cc,
_ => AgcAgentMode::Codex,
})
}
/// 响应 DTO 的前向兼容:未知协议回退 `None`,由 `agent_mode` 的默认协议补齐。
fn deserialize_forward_compat_protocol<'de, D>(
deserializer: D,
) -> Result<Option<AgcModelProtocol>, D::Error>
where
D: serde::Deserializer<'de>,
{
let raw = Option::<String>::deserialize(deserializer)?;
Ok(match raw.as_deref() {
Some("openai_responses") => Some(AgcModelProtocol::OpenAiResponses),
Some("openai_chat") => Some(AgcModelProtocol::OpenAiChat),
Some("anthropic") => Some(AgcModelProtocol::Anthropic),
_ => None,
})
}
/// 反序列化中间态:`protocol` 缺失时按 `agent_mode` 的默认协议补齐(见 `From` 实现)。
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct LlmModelSummaryWire {
display_name: String,
id: String,
#[serde(default)]
#[serde(default, deserialize_with = "deserialize_forward_compat_agent_mode")]
agent_mode: AgcAgentMode,
#[serde(default)]
#[serde(default, deserialize_with = "deserialize_forward_compat_protocol")]
protocol: Option<AgcModelProtocol>,
}
@@ -181,9 +210,9 @@ pub struct LlmModelSummary {
struct LlmUnavailableModelWire {
display_name: String,
id: String,
#[serde(default)]
#[serde(default, deserialize_with = "deserialize_forward_compat_agent_mode")]
agent_mode: AgcAgentMode,
#[serde(default)]
#[serde(default, deserialize_with = "deserialize_forward_compat_protocol")]
protocol: Option<AgcModelProtocol>,
reason: LlmModelUnavailableReason,
}
@@ -320,4 +349,23 @@ mod tests {
assert_eq!(cc.protocol, AgcModelProtocol::Anthropic);
assert_eq!(cc.reason, LlmModelUnavailableReason::Disabled);
}
#[test]
fn unknown_enum_tokens_fall_back_for_forward_compat() {
// 后端将来新增执行器 / 协议时,旧客户端仍能解析整份响应,而不是 fail closed。
let summary: LlmModelSummary = serde_json::from_str(
r#"{"displayName":"未来模型","id":"model-x","agentMode":"future-mode","protocol":"future-proto"}"#,
)
.unwrap();
assert_eq!(summary.agent_mode, AgcAgentMode::Codex);
assert_eq!(summary.protocol, AgcModelProtocol::OpenAiResponses);
let unavailable: LlmUnavailableModel = serde_json::from_str(
r#"{"displayName":"未来模型","id":"model-x","agentMode":"future-mode","protocol":"future-proto","reason":"future-reason"}"#,
)
.unwrap();
assert_eq!(unavailable.agent_mode, AgcAgentMode::Codex);
assert_eq!(unavailable.protocol, AgcModelProtocol::OpenAiResponses);
assert_eq!(unavailable.reason, LlmModelUnavailableReason::Unknown);
}
}