llm_link/
adapters.rs

1use crate::settings::Settings;
2use axum::http::HeaderMap;
3use llm_connector::StreamFormat;
4use serde_json::Value;
5
6/// 客户端适配器类型
7///
8/// 用于识别不同的客户端并应用相应的响应转换。
9///
10/// # 工作流程
11/// 1. 检测客户端类型(通过 HTTP 头、User-Agent、配置等)
12/// 2. 确定偏好的流式格式(SSE/NDJSON/JSON)
13/// 3. 应用客户端特定的响应适配(字段添加、格式调整等)
14///
15/// # 使用位置
16/// - `src/api/ollama.rs::detect_ollama_client()` - Ollama API 客户端检测
17/// - `src/api/openai.rs::detect_openai_client()` - OpenAI API 客户端检测
18///
19/// # 示例
20/// ```rust
21/// let adapter = detect_client(&headers, &config);
22/// let format = adapter.preferred_format();
23/// adapter.apply_response_adaptations(&config, &mut response_data);
24/// ```
25#[derive(Debug, Clone, PartialEq)]
26pub enum ClientAdapter {
27    /// 标准 Ollama 客户端
28    /// - 偏好格式: NDJSON
29    /// - 特殊处理: 无
30    Standard,
31
32    /// Zed 编辑器适配
33    /// - 偏好格式: NDJSON
34    /// - 特殊处理: 添加 `images` 字段
35    Zed,
36
37    /// OpenAI API 客户端适配(包括 Codex CLI)
38    /// - 偏好格式: SSE
39    /// - 特殊处理: finish_reason 修正(在 llm/stream.rs 中处理)
40    OpenAI,
41}
42
43impl ClientAdapter {
44    /// 获取该客户端的首选流式格式
45    ///
46    /// 当客户端没有明确指定 Accept 头(或使用 `*/*`)时,
47    /// 使用此方法返回的格式。
48    ///
49    /// # 返回值
50    /// - `StreamFormat::SSE` - Server-Sent Events (OpenAI/Codex 偏好)
51    /// - `StreamFormat::NDJSON` - Newline Delimited JSON (Ollama/Zed 偏好)
52    ///
53    /// # 使用场景
54    /// ```rust
55    /// let format = if headers.get("accept").contains("*/*") {
56    ///     adapter.preferred_format()  // 使用偏好格式
57    /// } else {
58    ///     detected_format  // 使用客户端指定的格式
59    /// };
60    /// ```
61    pub fn preferred_format(&self) -> StreamFormat {
62        match self {
63            ClientAdapter::Standard => StreamFormat::NDJSON, // Ollama 标准
64            ClientAdapter::Zed => StreamFormat::NDJSON,      // Zed 偏好 NDJSON
65            ClientAdapter::OpenAI => StreamFormat::SSE,      // OpenAI/Codex 偏好 SSE
66        }
67    }
68
69    /// 应用客户端特定的响应处理
70    ///
71    /// 根据客户端类型,对 LLM 返回的响应数据进行适配转换。
72    ///
73    /// # 参数
74    /// - `config`: 全局配置
75    /// - `data`: 响应数据(可变引用),会被就地修改
76    ///
77    /// # 适配内容
78    ///
79    /// ## Standard
80    /// - 无特殊处理
81    ///
82    /// ## Zed
83    /// - 添加 `images: null` 字段(Zed 要求)
84    ///
85    /// ## OpenAI
86    /// - finish_reason 修正(在 client.rs 中处理)
87    ///
88    /// # 调用位置
89    /// - `src/handlers/ollama.rs` - 在流式响应的每个 chunk 中调用
90    /// - `src/handlers/openai.rs` - 在流式响应的每个 chunk 中调用
91    ///
92    /// # 示例
93    /// ```rust
94    /// let mut response_data = serde_json::from_str(&chunk)?;
95    /// adapter.apply_response_adaptations(&config, &mut response_data);
96    /// // response_data 已被适配
97    /// ```
98    pub fn apply_response_adaptations(&self, config: &Settings, data: &mut Value) {
99        match self {
100            ClientAdapter::Standard => {
101                // 标准模式:无特殊处理
102            }
103            ClientAdapter::Zed => {
104                // Zed 特定适配:添加 images 字段
105                let should_add_images = if let Some(ref adapters) = config.client_adapters {
106                    if let Some(ref zed_config) = adapters.zed {
107                        zed_config.force_images_field.unwrap_or(true)
108                    } else {
109                        true
110                    }
111                } else {
112                    true
113                };
114
115                if should_add_images {
116                    if let Some(message) = data.get_mut("message") {
117                        if message.get("images").is_none() {
118                            message
119                                .as_object_mut()
120                                .unwrap()
121                                .insert("images".to_string(), Value::Null);
122                        }
123                    }
124                }
125            }
126            ClientAdapter::OpenAI => {
127                // OpenAI 特定适配:无特殊处理
128                // finish_reason 修正在 client.rs 中处理
129            }
130        }
131    }
132}
133
134/// 格式检测器
135pub struct FormatDetector;
136
137impl FormatDetector {
138    /// 根据 HTTP 标准确定响应格式
139    pub fn determine_format(headers: &HeaderMap) -> (StreamFormat, &'static str) {
140        if let Some(accept) = headers.get("accept") {
141            if let Ok(accept_str) = accept.to_str() {
142                if accept_str.contains("text/event-stream") {
143                    return (StreamFormat::SSE, "text/event-stream");
144                }
145                if accept_str.contains("application/x-ndjson")
146                    || accept_str.contains("application/jsonlines")
147                {
148                    return (StreamFormat::NDJSON, "application/x-ndjson");
149                }
150            }
151        }
152        // 默认:NDJSON
153        (StreamFormat::NDJSON, "application/x-ndjson")
154    }
155
156    /// 获取格式对应的 Content-Type
157    pub fn get_content_type(format: StreamFormat) -> &'static str {
158        match format {
159            StreamFormat::SSE => "text/event-stream",
160            StreamFormat::NDJSON => "application/x-ndjson",
161            StreamFormat::Json => "application/json",
162        }
163    }
164}