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}