From c7150b590c6c1afb20e4567269908400a44dc66b 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:18:05 +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):=20protocol=20=E7=BC=BA=E7=9C=81=E6=94=B9?= =?UTF-8?q?=E4=B8=BA=E6=8C=89=20agent=5Fmode=20=E6=8E=A8=E5=AF=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - LlmModelSummary/LlmUnavailableModel 反序列化缺失 protocol 时取 agent_mode 的默认协议,不再一律 openai_responses - 补 cc / anthropic 别名 / 显式 protocol 用例;同步生成绑定注释 --- .../contracts/generated/LlmModelSummary.ts | 2 +- .../generated/LlmUnavailableModel.ts | 6 +- .../shared-contracts/src/llm_catalog.rs | 103 +++++++++++++++++- 3 files changed, 106 insertions(+), 5 deletions(-) diff --git a/packages/shared/src/contracts/generated/LlmModelSummary.ts b/packages/shared/src/contracts/generated/LlmModelSummary.ts index be2e3c30c..97c5e6ad6 100644 --- a/packages/shared/src/contracts/generated/LlmModelSummary.ts +++ b/packages/shared/src/contracts/generated/LlmModelSummary.ts @@ -11,6 +11,6 @@ export type LlmModelSummary = { displayName: string, id: string, */ agentMode: AgcAgentMode, /** - * 上游线协议;旧服务端缺字段时按 `openai_responses` 兼容。 + * 上游线协议;旧服务端缺字段时按 `agent_mode` 的默认协议兼容。 */ protocol: AgcModelProtocol, }; diff --git a/packages/shared/src/contracts/generated/LlmUnavailableModel.ts b/packages/shared/src/contracts/generated/LlmUnavailableModel.ts index f41401bbc..119e03dc7 100644 --- a/packages/shared/src/contracts/generated/LlmUnavailableModel.ts +++ b/packages/shared/src/contracts/generated/LlmUnavailableModel.ts @@ -6,4 +6,8 @@ import type { LlmModelUnavailableReason } from "./LlmModelUnavailableReason"; /** * 目录里存在、但当前账号不可用的一条模型。字段与 [`LlmModelSummary`] 对齐,另带原因。 */ -export type LlmUnavailableModel = { displayName: string, id: string, agentMode: AgcAgentMode, protocol: AgcModelProtocol, reason: LlmModelUnavailableReason, }; +export type LlmUnavailableModel = { displayName: string, id: string, agentMode: AgcAgentMode, +/** + * 上游线协议;旧服务端缺字段时按 `agent_mode` 的默认协议兼容。 + */ +protocol: AgcModelProtocol, reason: LlmModelUnavailableReason, }; diff --git a/server-rs/crates/shared-contracts/src/llm_catalog.rs b/server-rs/crates/shared-contracts/src/llm_catalog.rs index 72a79bfce..e25478082 100644 --- a/server-rs/crates/shared-contracts/src/llm_catalog.rs +++ b/server-rs/crates/shared-contracts/src/llm_catalog.rs @@ -124,9 +124,35 @@ impl AgcModelProtocol { } } +/// 反序列化中间态:`protocol` 缺失时按 `agent_mode` 的默认协议补齐(见 `From` 实现)。 +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct LlmModelSummaryWire { + display_name: String, + id: String, + #[serde(default)] + agent_mode: AgcAgentMode, + #[serde(default)] + protocol: Option, +} + +impl From for LlmModelSummary { + fn from(wire: LlmModelSummaryWire) -> Self { + let protocol = wire + .protocol + .unwrap_or_else(|| wire.agent_mode.default_protocol()); + Self { + display_name: wire.display_name, + id: wire.id, + agent_mode: wire.agent_mode, + protocol, + } + } +} + /// 本档可用的一条模型摘要。不包含上游真实模型名。 #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] +#[serde(rename_all = "camelCase", from = "LlmModelSummaryWire")] #[cfg_attr(feature = "ts-bindings", derive(ts_rs::TS))] #[cfg_attr( feature = "ts-bindings", @@ -144,14 +170,42 @@ pub struct LlmModelSummary { /// 执行器;旧服务端缺字段时按 `codex` 兼容。 #[serde(default)] pub agent_mode: AgcAgentMode, - /// 上游线协议;旧服务端缺字段时按 `openai_responses` 兼容。 + /// 上游线协议;旧服务端缺字段时按 `agent_mode` 的默认协议兼容。 #[serde(default)] pub protocol: AgcModelProtocol, } +/// 反序列化中间态:与 [`LlmModelSummaryWire`] 同规则,额外携带 `reason`。 +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct LlmUnavailableModelWire { + display_name: String, + id: String, + #[serde(default)] + agent_mode: AgcAgentMode, + #[serde(default)] + protocol: Option, + reason: LlmModelUnavailableReason, +} + +impl From for LlmUnavailableModel { + fn from(wire: LlmUnavailableModelWire) -> Self { + let protocol = wire + .protocol + .unwrap_or_else(|| wire.agent_mode.default_protocol()); + Self { + display_name: wire.display_name, + id: wire.id, + agent_mode: wire.agent_mode, + protocol, + reason: wire.reason, + } + } +} + /// 目录里存在、但当前账号不可用的一条模型。字段与 [`LlmModelSummary`] 对齐,另带原因。 #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] +#[serde(rename_all = "camelCase", from = "LlmUnavailableModelWire")] #[cfg_attr(feature = "ts-bindings", derive(ts_rs::TS))] #[cfg_attr( feature = "ts-bindings", @@ -168,6 +222,7 @@ pub struct LlmUnavailableModel { pub id: String, #[serde(default)] pub agent_mode: AgcAgentMode, + /// 上游线协议;旧服务端缺字段时按 `agent_mode` 的默认协议兼容。 #[serde(default)] pub protocol: AgcModelProtocol, pub reason: LlmModelUnavailableReason, @@ -224,3 +279,45 @@ pub struct LlmModelsResponse { #[cfg_attr(feature = "ts-bindings", ts(type = "number"))] pub revision: u64, } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn missing_protocol_follows_agent_mode() { + let cc: LlmModelSummary = + serde_json::from_str(r#"{"displayName":"Claude","id":"model-a","agentMode":"cc"}"#) + .unwrap(); + assert_eq!(cc.protocol, AgcModelProtocol::Anthropic); + + let legacy_alias: LlmModelSummary = serde_json::from_str( + r#"{"displayName":"Claude","id":"model-a","agentMode":"anthropic"}"#, + ) + .unwrap(); + assert_eq!(legacy_alias.agent_mode, AgcAgentMode::Cc); + assert_eq!(legacy_alias.protocol, AgcModelProtocol::Anthropic); + + let codex: LlmModelSummary = + serde_json::from_str(r#"{"displayName":"GPT","id":"model-b"}"#).unwrap(); + assert_eq!(codex.agent_mode, AgcAgentMode::Codex); + assert_eq!(codex.protocol, AgcModelProtocol::OpenAiResponses); + + // 显式 protocol 优先,不被 agent_mode 覆盖。 + let explicit: LlmModelSummary = serde_json::from_str( + r#"{"displayName":"GPT","id":"model-c","agentMode":"codex","protocol":"openai_chat"}"#, + ) + .unwrap(); + assert_eq!(explicit.protocol, AgcModelProtocol::OpenAiChat); + } + + #[test] + fn unavailable_model_missing_protocol_follows_agent_mode() { + let cc: LlmUnavailableModel = serde_json::from_str( + r#"{"displayName":"Claude","id":"model-a","agentMode":"cc","reason":"disabled"}"#, + ) + .unwrap(); + assert_eq!(cc.protocol, AgcModelProtocol::Anthropic); + assert_eq!(cc.reason, LlmModelUnavailableReason::Disabled); + } +}