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); + } +}