From 4bd080eff580b1a6375305408f4c989e70516508 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Mon, 5 Oct 2026 12:19:15 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D(LLM=E7=9B=AE=E5=BD=95?= =?UTF-8?q?=E5=A5=91=E7=BA=A6):=20=E5=93=8D=E5=BA=94=E6=9E=9A=E4=B8=BE?= =?UTF-8?q?=E6=9C=AA=E7=9F=A5=E5=8F=96=E5=80=BC=E5=89=8D=E5=90=91=E5=85=BC?= =?UTF-8?q?=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - LlmModelsResponse 内 agentMode / protocol 遇到未知 token 回退 Codex / 由 agentMode 推导默认协议,不再让旧客户端整份反序列化失败 - 输入校验仍走严格 parse;补未知 token 用例 --- .../shared-contracts/src/llm_catalog.rs | 56 +++++++++++++++++-- 1 file changed, 52 insertions(+), 4 deletions(-) diff --git a/server-rs/crates/shared-contracts/src/llm_catalog.rs b/server-rs/crates/shared-contracts/src/llm_catalog.rs index e25478082..3b67537d5 100644 --- a/server-rs/crates/shared-contracts/src/llm_catalog.rs +++ b/server-rs/crates/shared-contracts/src/llm_catalog.rs @@ -124,15 +124,44 @@ impl AgcModelProtocol { } } +/// 响应 DTO 的前向兼容:未知执行器回退 `Codex`,避免后端新增取值时旧客户端整份 +/// [`LlmModelsResponse`] 反序列化失败。写入 / 输入校验仍走严格的 [`AgcAgentMode::parse`]。 +fn deserialize_forward_compat_agent_mode<'de, D>(deserializer: D) -> Result +where + D: serde::Deserializer<'de>, +{ + let raw = Option::::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, D::Error> +where + D: serde::Deserializer<'de>, +{ + let raw = Option::::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, } @@ -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, 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); + } }