llm_link/
adapters.rs

1use crate::settings::Settings;
2use axum::http::HeaderMap;
3use llm_connector::StreamFormat;
4use serde_json::Value;
5
6/// Client adapter type
7///
8/// Used to identify different clients and apply corresponding response transformations.
9///
10/// # Workflow
11/// 1. Detect client type (through HTTP headers, User-Agent, configuration, etc.)
12/// 2. Determine preferred streaming format (SSE/NDJSON/JSON)
13/// 3. Apply client-specific response adaptations (field addition, format adjustment, etc.)
14///
15/// # Usage Locations
16/// - `src/api/ollama.rs::detect_ollama_client()` - Ollama API client detection
17/// - `src/api/openai.rs::detect_openai_client()` - OpenAI API client detection
18///
19/// # Example
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    /// Standard Ollama client
29    /// - Preferred format: NDJSON
30    /// - Special handling: None
31    Standard,
32
33    /// Zed editor adapter
34    /// - Preferred format: NDJSON
35    /// - Special handling: Add `images` field
36    Zed,
37
38    /// OpenAI API client adapter (including Codex CLI)
39    /// - Preferred format: SSE
40    /// - Special handling: finish_reason correction (handled in 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                            if let Some(msg_obj) = message.as_object_mut() {
120                                msg_obj.insert("images".to_string(), Value::Null);
121                            }
122                        }
123                    }
124                }
125            }
126            ClientAdapter::OpenAI => {
127                // OpenAI 特定适配:无特殊处理
128                // finish_reason 修正在 client.rs 中处理
129            }
130        }
131    }
132}
133
134/// 格式检测器
135#[allow(dead_code)]
136pub struct FormatDetector;
137
138impl FormatDetector {
139    /// 根据 HTTP 标准确定响应格式
140    pub fn determine_format(headers: &HeaderMap) -> (StreamFormat, &'static str) {
141        if let Some(accept) = headers.get("accept") {
142            if let Ok(accept_str) = accept.to_str() {
143                if accept_str.contains("text/event-stream") {
144                    return (StreamFormat::SSE, "text/event-stream");
145                }
146                if accept_str.contains("application/x-ndjson")
147                    || accept_str.contains("application/jsonlines")
148                {
149                    return (StreamFormat::NDJSON, "application/x-ndjson");
150                }
151            }
152        }
153        // 默认:NDJSON
154        (StreamFormat::NDJSON, "application/x-ndjson")
155    }
156
157    /// 获取格式对应的 Content-Type
158    pub fn get_content_type(format: StreamFormat) -> &'static str {
159        match format {
160            StreamFormat::SSE => "text/event-stream",
161            StreamFormat::NDJSON => "application/x-ndjson",
162            StreamFormat::Json => "application/json",
163        }
164    }
165}