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