feat(llmx): 添加 OpenAI接口支持 #48
| @ -84,7 +84,6 @@ subprojects { | |||||||
|                 implementation(libs.okHttp) |                 implementation(libs.okHttp) | ||||||
|                 // forgeBoot依赖 |                 // forgeBoot依赖 | ||||||
|                 implementation(libs.forgeBoot.core.extension) |                 implementation(libs.forgeBoot.core.extension) | ||||||
|                 implementation(libs.forgeBoot.core.extension) |  | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         if(project.getPropertyByBoolean(ProjectFlags.USE_DAO_DEPENDENCE)){ |         if(project.getPropertyByBoolean(ProjectFlags.USE_DAO_DEPENDENCE)){ | ||||||
|  | |||||||
| @ -17,7 +17,7 @@ sealed class MultiModalContent { | |||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Image数据类,表示图像内容的模态 |      * Image数据类,表示图像内容的模态 | ||||||
|      * @param image 图像的base64 |      * @param image 图像的base64或者url | ||||||
|      */ |      */ | ||||||
|     data class Image(val image: String) : MultiModalContent() |     data class Image(val image: String) : MultiModalContent() | ||||||
| } | } | ||||||
| @ -35,8 +35,8 @@ sealed class MultiModalContent { | |||||||
| data class ChatRequest( | data class ChatRequest( | ||||||
|     val prompt: String? = "", |     val prompt: String? = "", | ||||||
|     val model: String, |     val model: String, | ||||||
|     val messages: List<MultiModalMessage> =listOf(), |     val messages: List<MultiModalMessage> = listOf(), | ||||||
|     val options: Map<String, String> = mapOf() |     val options: Map<String, String> = mapOf(), | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
| @ -46,7 +46,5 @@ data class ChatRequest( | |||||||
|  */ |  */ | ||||||
| data class MultiModalMessage( | data class MultiModalMessage( | ||||||
|     val role: String, // "system", "user", "assistant" |     val role: String, // "system", "user", "assistant" | ||||||
|     val content: List<MultiModalContent> |     val content: List<MultiModalContent>, | ||||||
| ) | ) | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|  | |||||||
| @ -32,8 +32,8 @@ class ModelRouteManager( | |||||||
|         val modelServiceMap = modelRouteMappingRepository.findAllByEnabled(true) |         val modelServiceMap = modelRouteMappingRepository.findAllByEnabled(true) | ||||||
|             .associate { it.model to it.serviceName } |             .associate { it.model to it.serviceName } | ||||||
|         log.info("modelServiceMap: $modelServiceMap") |         log.info("modelServiceMap: $modelServiceMap") | ||||||
|         for ((model, serviceName) in modelServiceMap) { |         for ((modelId, serviceName) in modelServiceMap) { | ||||||
|             if (model.startsWith(model)) { |             if (modelId == model) { | ||||||
|                 return serviceName |                 return serviceName | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  | |||||||
| @ -5,6 +5,12 @@ apply { | |||||||
| } | } | ||||||
| dependencies { | dependencies { | ||||||
|     compileOnly(libs.springBootStarter.web) |     compileOnly(libs.springBootStarter.web) | ||||||
|  |     // kt协程依赖 | ||||||
|  |     compileOnly(libs.kotlinx.coruntes.reactor) | ||||||
|  |     // okHttp依赖 | ||||||
|  |     compileOnly(libs.okHttp) | ||||||
|     api(libs.org.reactivestreams.reactiveStreams) |     api(libs.org.reactivestreams.reactiveStreams) | ||||||
|     api(project(Modules.Core.COMMON)) |     api(project(Modules.Core.COMMON)) | ||||||
|  |     // forgeBoot依赖 | ||||||
|  |     implementation(libs.forgeBoot.core.extension) | ||||||
| } | } | ||||||
| @ -0,0 +1,100 @@ | |||||||
|  | package org.jcnc.llmx.core.spi.adapter | ||||||
|  | 
 | ||||||
|  | import com.fasterxml.jackson.databind.ObjectMapper | ||||||
|  | import com.gewuyou.forgeboot.core.extension.log | ||||||
|  | import kotlinx.coroutines.CoroutineDispatcher | ||||||
|  | import kotlinx.coroutines.flow.FlowCollector | ||||||
|  | import kotlinx.coroutines.flow.flow | ||||||
|  | import kotlinx.coroutines.reactive.asPublisher | ||||||
|  | import okhttp3.Headers.Companion.toHeaders | ||||||
|  | import okhttp3.MediaType.Companion.toMediaType | ||||||
|  | import okhttp3.OkHttpClient | ||||||
|  | import okhttp3.Request | ||||||
|  | import okhttp3.RequestBody.Companion.toRequestBody | ||||||
|  | import org.jcnc.llmx.core.common.entities.response.ChatResponsePart | ||||||
|  | import org.reactivestreams.Publisher | ||||||
|  | import org.springframework.http.MediaType | ||||||
|  | import java.io.BufferedReader | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * 抽象聊天流适配器 | ||||||
|  |  * | ||||||
|  |  * @since 2025-05-11 20:43:16 | ||||||
|  |  * @author gewuyou | ||||||
|  |  */ | ||||||
|  | abstract class AbstractChatStreamAdapter( | ||||||
|  |     private val okHttpClient: OkHttpClient, | ||||||
|  |     private val objectMapper: ObjectMapper, | ||||||
|  | ) : ChatStreamAdapter { | ||||||
|  |     /** | ||||||
|  |      * 发送流式聊天请求 | ||||||
|  |      * | ||||||
|  |      * 该函数负责向指定URL发送聊天请求,并根据响应流提取内容 | ||||||
|  |      * 使用协程调度器来控制并发和异步处理逻辑 | ||||||
|  |      * | ||||||
|  |      * @param url 请求的URL地址 | ||||||
|  |      * @param headers 请求头,包含认证等信息 | ||||||
|  |      * @param requestBody 请求体,可以是任意类型,根据API需求而定 | ||||||
|  |      * @param extractContent 函数用于从响应流中提取聊天响应的部分内容 | ||||||
|  |      * @param dispatcher 协程调度器,用于执行异步或并发操作 | ||||||
|  |      * @return 返回一个发布者,用于订阅聊天响应流 | ||||||
|  |      */ | ||||||
|  |     override fun sendStreamChat( | ||||||
|  |         url: String, | ||||||
|  |         headers: Map<String, String>, | ||||||
|  |         requestBody: Any, | ||||||
|  |         extractContent: (String) -> ChatResponsePart, | ||||||
|  |         dispatcher: CoroutineDispatcher, | ||||||
|  |     ): Publisher<ChatResponsePart> = flow { | ||||||
|  |         val requestJson = objectMapper.writeValueAsString(requestBody) | ||||||
|  |         log.info("📤 请求参数: $requestJson") | ||||||
|  | 
 | ||||||
|  |         val request = buildRequest(url, headers, requestJson) | ||||||
|  | 
 | ||||||
|  |         okHttpClient.newCall(request).execute().use { response -> | ||||||
|  |             if (!response.isSuccessful) { | ||||||
|  |                 log.error("🚨 请求失败: ${response.code} ${response.message}") | ||||||
|  |                 throw RuntimeException("❌ 请求失败: HTTP ${response.code}") | ||||||
|  |             } | ||||||
|  |             val responseBody = response.body ?: throw RuntimeException("❌ 响应体为空") | ||||||
|  |             responseBody.charStream().buffered().use { reader -> | ||||||
|  |                 val allContent = StringBuilder() | ||||||
|  |                 processResponse(reader, extractContent, allContent, dispatcher) | ||||||
|  |                 log.info("📦 完整内容: $allContent") | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     }.asPublisher() | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * 构建HTTP请求 | ||||||
|  |      * | ||||||
|  |      * @param url 请求的URL地址 | ||||||
|  |      * @param headers 请求头,包含认证等信息 | ||||||
|  |      * @param json 请求体的JSON字符串 | ||||||
|  |      * @return 返回构建好的HTTP请求对象 | ||||||
|  |      */ | ||||||
|  |     open fun buildRequest(url: String, headers: Map<String, String>, json: String): Request { | ||||||
|  |         return Request.Builder() | ||||||
|  |             .url(url) | ||||||
|  |             .headers(headers.toHeaders()) | ||||||
|  |             .post(json.toRequestBody(MediaType.APPLICATION_JSON_VALUE.toMediaType())) | ||||||
|  |             .build() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * 处理响应流的抽象方法 | ||||||
|  |      * | ||||||
|  |      * 此方法需由子类实现,用以解析响应流并发射聊天响应部分 | ||||||
|  |      * | ||||||
|  |      * @param reader 响应流的BufferedReader对象 | ||||||
|  |      * @param extractContent 函数用于从响应流中提取聊天响应的部分内容 | ||||||
|  |      * @param allContent 用于存储响应流的完整内容 | ||||||
|  |      * @param dispatcher 协程调度器,用于执行异步或并发操作 | ||||||
|  |      */ | ||||||
|  |     abstract suspend fun FlowCollector<ChatResponsePart>.processResponse( | ||||||
|  |         reader: BufferedReader, | ||||||
|  |         extractContent: (String) -> ChatResponsePart, | ||||||
|  |         allContent: StringBuilder, | ||||||
|  |         dispatcher: CoroutineDispatcher | ||||||
|  |     ) | ||||||
|  | } | ||||||
| @ -0,0 +1,38 @@ | |||||||
|  | package org.jcnc.llmx.core.spi.adapter | ||||||
|  | 
 | ||||||
|  | import kotlinx.coroutines.CoroutineDispatcher | ||||||
|  | import kotlinx.coroutines.Dispatchers | ||||||
|  | import org.jcnc.llmx.core.common.entities.response.ChatResponsePart | ||||||
|  | import org.reactivestreams.Publisher | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * 聊天流适配器 | ||||||
|  |  * | ||||||
|  |  * 该接口定义了一个函数,用于发送流式聊天请求,并处理响应流 | ||||||
|  |  * 主要用途是与聊天API进行交互,以流的方式处理聊天消息,从而实现高效的实时通信 | ||||||
|  |  * | ||||||
|  |  * @since 2025-05-11 20:19:29 | ||||||
|  |  * @author gewuyou | ||||||
|  |  */ | ||||||
|  | interface ChatStreamAdapter { | ||||||
|  |     /** | ||||||
|  |      * 发送流式聊天请求 | ||||||
|  |      * | ||||||
|  |      * 该函数负责向指定URL发送聊天请求,并根据响应流提取内容 | ||||||
|  |      * 使用协程调度器来控制并发和异步处理逻辑 | ||||||
|  |      * | ||||||
|  |      * @param url 请求的URL地址 | ||||||
|  |      * @param headers 请求头,包含认证等信息 | ||||||
|  |      * @param requestBody 请求体,可以是任意类型,根据API需求而定 | ||||||
|  |      * @param extractContent 函数用于从响应流中提取聊天响应的部分内容 | ||||||
|  |      * @param dispatcher 协程调度器,用于执行异步或并发操作 | ||||||
|  |      * @return 返回一个发布者,用于订阅聊天响应流 | ||||||
|  |      */ | ||||||
|  |     fun sendStreamChat( | ||||||
|  |         url: String, | ||||||
|  |         headers: Map<String, String>, | ||||||
|  |         requestBody: Any, | ||||||
|  |         extractContent: (String) -> ChatResponsePart, | ||||||
|  |         dispatcher: CoroutineDispatcher=Dispatchers.IO | ||||||
|  |     ): Publisher<ChatResponsePart> | ||||||
|  | } | ||||||
| @ -8,6 +8,7 @@ import org.jcnc.llmx.core.common.entities.response.EmbeddingResponse | |||||||
| import org.reactivestreams.Publisher | import org.reactivestreams.Publisher | ||||||
| import org.springframework.http.MediaType | import org.springframework.http.MediaType | ||||||
| import org.springframework.web.bind.annotation.PostMapping | import org.springframework.web.bind.annotation.PostMapping | ||||||
|  | import org.springframework.web.bind.annotation.RequestBody | ||||||
| import org.springframework.web.bind.annotation.RequestMapping | import org.springframework.web.bind.annotation.RequestMapping | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
| @ -31,7 +32,7 @@ interface LLMProvider { | |||||||
|      * @return 返回一个Flow流,通过该流可以接收到聊天响应的部分数据,如消息、状态更新等 |      * @return 返回一个Flow流,通过该流可以接收到聊天响应的部分数据,如消息、状态更新等 | ||||||
|      */ |      */ | ||||||
|     @PostMapping("/chat", produces = [MediaType.APPLICATION_NDJSON_VALUE]) |     @PostMapping("/chat", produces = [MediaType.APPLICATION_NDJSON_VALUE]) | ||||||
|     fun chat(request: ChatRequest): Publisher<ChatResponsePart> |     fun chat(@RequestBody request: ChatRequest): Publisher<ChatResponsePart> | ||||||
|     /** |     /** | ||||||
|      * 处理多模态聊天请求的函数 |      * 处理多模态聊天请求的函数 | ||||||
|      * 该函数通过 POST 方法接收一个聊天请求,并以 NDJSON 的形式返回聊天响应的部分 |      * 该函数通过 POST 方法接收一个聊天请求,并以 NDJSON 的形式返回聊天响应的部分 | ||||||
| @ -43,7 +44,7 @@ interface LLMProvider { | |||||||
|      * 它使用了响应式编程模型,适合处理高并发和大数据量的响应 |      * 它使用了响应式编程模型,适合处理高并发和大数据量的响应 | ||||||
|      */ |      */ | ||||||
|     @PostMapping("/multimodalityChat", produces = [MediaType.APPLICATION_NDJSON_VALUE]) |     @PostMapping("/multimodalityChat", produces = [MediaType.APPLICATION_NDJSON_VALUE]) | ||||||
|     fun multimodalityChat(request: ChatRequest): Publisher<ChatResponsePart> |     fun multimodalityChat(@RequestBody request: ChatRequest): Publisher<ChatResponsePart> | ||||||
|     /** |     /** | ||||||
|      * 嵌入功能方法 |      * 嵌入功能方法 | ||||||
|      * 该方法允许用户发送嵌入请求,以获取LLM生成的嵌入向量 |      * 该方法允许用户发送嵌入请求,以获取LLM生成的嵌入向量 | ||||||
|  | |||||||
| @ -2,18 +2,14 @@ package org.jcnc.llmx.impl.baiLian.adapter | |||||||
| 
 | 
 | ||||||
| import com.fasterxml.jackson.databind.ObjectMapper | import com.fasterxml.jackson.databind.ObjectMapper | ||||||
| import com.gewuyou.forgeboot.core.extension.log | import com.gewuyou.forgeboot.core.extension.log | ||||||
| import kotlinx.coroutines.* | import kotlinx.coroutines.CoroutineDispatcher | ||||||
| import kotlinx.coroutines.flow.Flow | import kotlinx.coroutines.currentCoroutineContext | ||||||
| import kotlinx.coroutines.flow.FlowCollector | import kotlinx.coroutines.flow.FlowCollector | ||||||
| import kotlinx.coroutines.flow.flow | import kotlinx.coroutines.isActive | ||||||
| import okhttp3.Headers.Companion.toHeaders | import kotlinx.coroutines.withContext | ||||||
| import okhttp3.MediaType.Companion.toMediaType |  | ||||||
| import okhttp3.OkHttpClient | import okhttp3.OkHttpClient | ||||||
| import okhttp3.Request |  | ||||||
| import okhttp3.RequestBody.Companion.toRequestBody |  | ||||||
| import org.jcnc.llmx.core.common.entities.response.ChatResponsePart | import org.jcnc.llmx.core.common.entities.response.ChatResponsePart | ||||||
| 
 | import org.jcnc.llmx.core.spi.adapter.AbstractChatStreamAdapter | ||||||
| 
 |  | ||||||
| import org.springframework.stereotype.Component | import org.springframework.stereotype.Component | ||||||
| import java.io.BufferedReader | import java.io.BufferedReader | ||||||
| 
 | 
 | ||||||
| @ -30,71 +26,26 @@ import java.io.BufferedReader | |||||||
|  */ |  */ | ||||||
| @Component | @Component | ||||||
| class DashScopeAdapter( | class DashScopeAdapter( | ||||||
|     private val okHttpClient: OkHttpClient, |     okHttpClient: OkHttpClient, | ||||||
|     private val objectMapper: ObjectMapper, |     objectMapper: ObjectMapper, | ||||||
| ) { | ) : AbstractChatStreamAdapter(okHttpClient, objectMapper) { | ||||||
|     /** |     /** | ||||||
|      * 发送流式聊天请求 |      * 处理响应流的抽象方法 | ||||||
|      * |      * | ||||||
|      * 本函数构建并发送一个聊天请求,然后以流的形式接收和处理响应。 |      * 该方法用于处理从API接收到的响应流。它读取响应流的每一行, | ||||||
|      * 它主要用于与DashScope API进行交互,提取并发布聊天响应的部分内容。 |      * 如果行以"data:"开头,则提取该行的内容,解析为ChatResponsePart对象, | ||||||
|  |      * 并将解析后的对象发射到流中。 | ||||||
|      * |      * | ||||||
|      * @param url 请求的URL |      * @param reader 响应流的BufferedReader对象 | ||||||
|      * @param headers 请求的头部信息 |      * @param extractContent 函数用于从响应流中提取聊天响应的部分内容 | ||||||
|      * @param requestBody 请求的主体内容 |      * @param allContent 用于存储响应流的完整内容 | ||||||
|      * @param extractContent 一个函数,用于从JSON响应中提取内容 |      * @param dispatcher 协程调度器,用于执行异步或并发操作 | ||||||
|      * @param dispatcher 协程调度器,默认为IO调度器 |  | ||||||
|      * @return 返回一个Flow,发布聊天响应的部分内容 |  | ||||||
|      */ |      */ | ||||||
|     fun sendStreamChat( |     override suspend fun FlowCollector<ChatResponsePart>.processResponse( | ||||||
|         url: String, |  | ||||||
|         headers: Map<String, String>, |  | ||||||
|         requestBody: Any, |  | ||||||
|         extractContent: (String) -> ChatResponsePart, |  | ||||||
|         dispatcher: CoroutineDispatcher = Dispatchers.IO, |  | ||||||
|     ): Flow<ChatResponsePart> = flow { |  | ||||||
|         val requestJson = objectMapper.writeValueAsString(requestBody) |  | ||||||
|         log.info("📤 请求参数: {}", requestJson) |  | ||||||
| 
 |  | ||||||
|         val request = buildRequest(url, headers, requestJson) |  | ||||||
| 
 |  | ||||||
|         okHttpClient.newCall(request).execute().use { response -> |  | ||||||
|             if (!response.isSuccessful) { |  | ||||||
|                 log.error("🚨 DashScope 请求失败: code ${response.code} message: ${response.message} body: ${response.body}") |  | ||||||
|                 throw RuntimeException("❌ DashScope 请求失败: HTTP ${response.code}") |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             val responseBody = response.body ?: throw RuntimeException("❌ DashScope 响应体为空") |  | ||||||
| 
 |  | ||||||
|             responseBody.charStream().buffered().use { reader -> |  | ||||||
|                 val allContent = StringBuilder() |  | ||||||
|                 try { |  | ||||||
|                     processResponse(dispatcher, reader, extractContent, allContent) |  | ||||||
|                     log.info("📦 完整内容: {}", allContent) |  | ||||||
|                 } catch (e: Exception) { |  | ||||||
|                     log.error("🚨 读取 DashScope 响应流失败: {}", e.message, e) |  | ||||||
|                     throw e |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * 处理响应流 |  | ||||||
|      * |  | ||||||
|      * 本函数读取HTTP响应中的数据行,解析并提取内容。 |  | ||||||
|      * 它在循环中读取每一行,并使用提供的函数提取内容。 |  | ||||||
|      * |  | ||||||
|      * @param dispatcher 协程调度器 |  | ||||||
|      * @param reader 响应体的BufferedReader |  | ||||||
|      * @param extractContent 一个函数,用于从JSON响应中提取内容 |  | ||||||
|      * @param allContent 保存所有提取内容的StringBuilder |  | ||||||
|      */ |  | ||||||
|     private suspend fun FlowCollector<ChatResponsePart>.processResponse( |  | ||||||
|         dispatcher: CoroutineDispatcher, |  | ||||||
|         reader: BufferedReader, |         reader: BufferedReader, | ||||||
|         extractContent: (String) -> ChatResponsePart, |         extractContent: (String) -> ChatResponsePart, | ||||||
|         allContent: StringBuilder, |         allContent: StringBuilder, | ||||||
|  |         dispatcher: CoroutineDispatcher, | ||||||
|     ) { |     ) { | ||||||
|         while (currentCoroutineContext().isActive) { |         while (currentCoroutineContext().isActive) { | ||||||
|             val line = withContext(dispatcher) { |             val line = withContext(dispatcher) { | ||||||
| @ -116,22 +67,4 @@ class DashScopeAdapter( | |||||||
|             } |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * 构建请求 |  | ||||||
|      * |  | ||||||
|      * 本函数构建一个OkHttp请求对象,用于发送聊天请求。 |  | ||||||
|      * |  | ||||||
|      * @param url 请求的URL |  | ||||||
|      * @param headers 请求的头部信息 |  | ||||||
|      * @param json 请求的主体内容的JSON字符串 |  | ||||||
|      * @return 返回构建好的Request对象 |  | ||||||
|      */ |  | ||||||
|     private fun buildRequest(url: String, headers: Map<String, String>, json: String): Request { |  | ||||||
|         return Request.Builder() |  | ||||||
|             .url(url) |  | ||||||
|             .headers(headers.toHeaders()) |  | ||||||
|             .post(json.toRequestBody("application/json".toMediaType())) |  | ||||||
|             .build() |  | ||||||
|     } |  | ||||||
| } | } | ||||||
|  | |||||||
| @ -34,12 +34,12 @@ class BaiLianProvider( | |||||||
|      * @param request 聊天请求对象,包含建立聊天所需的信息,如用户标识、会话标识等 |      * @param request 聊天请求对象,包含建立聊天所需的信息,如用户标识、会话标识等 | ||||||
|      * @return 返回一个Flow流,通过该流可以接收到聊天响应的部分数据,如消息、状态更新等 |      * @return 返回一个Flow流,通过该流可以接收到聊天响应的部分数据,如消息、状态更新等 | ||||||
|      */ |      */ | ||||||
|     override fun chat(@RequestBody request: ChatRequest): Publisher<ChatResponsePart> { |     override fun chat(request: ChatRequest): Publisher<ChatResponsePart> { | ||||||
|         return baiLianModelService.streamChat(request).asPublisher() |         return baiLianModelService.streamChat(request) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     override fun multimodalityChat(@RequestBody request: ChatRequest): Publisher<ChatResponsePart> { |     override fun multimodalityChat(request: ChatRequest): Publisher<ChatResponsePart> { | ||||||
|         return baiLianModelService.streamMultimodalityChat(request).asPublisher() |         return baiLianModelService.streamMultimodalityChat(request) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|  | |||||||
| @ -14,7 +14,7 @@ import com.fasterxml.jackson.annotation.JsonProperty | |||||||
|  * @param requestId 请求ID,用于跟踪和调试请求 |  * @param requestId 请求ID,用于跟踪和调试请求 | ||||||
|  */ |  */ | ||||||
| @JsonIgnoreProperties(ignoreUnknown = true) | @JsonIgnoreProperties(ignoreUnknown = true) | ||||||
| data class DashScopeResponse( | data class DashScopeMultimodalityChatResponse( | ||||||
|     val output: Output?, |     val output: Output?, | ||||||
|     val usage: Usage?, |     val usage: Usage?, | ||||||
|     @JsonProperty("request_id") |     @JsonProperty("request_id") | ||||||
| @ -1,9 +1,8 @@ | |||||||
| package org.jcnc.llmx.impl.baiLian.service | package org.jcnc.llmx.impl.baiLian.service | ||||||
| 
 | 
 | ||||||
| import kotlinx.coroutines.flow.Flow |  | ||||||
| import org.jcnc.llmx.core.common.entities.request.ChatRequest | import org.jcnc.llmx.core.common.entities.request.ChatRequest | ||||||
| import org.jcnc.llmx.core.common.entities.response.ChatResponsePart | import org.jcnc.llmx.core.common.entities.response.ChatResponsePart | ||||||
| 
 | import org.reactivestreams.Publisher | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * 百炼模型服务接口 |  * 百炼模型服务接口 | ||||||
| @ -17,14 +16,15 @@ interface BaiLianModelService { | |||||||
|      * 使用流式聊天交互 |      * 使用流式聊天交互 | ||||||
|      * |      * | ||||||
|      * @param request 聊天请求对象,包含用户输入、上下文等信息 |      * @param request 聊天请求对象,包含用户输入、上下文等信息 | ||||||
|      * @return 返回一个Flow流,包含部分聊天响应,允许逐步处理和消费响应 |      * @return 返回一个发布者对象,用于订阅聊天响应的部分数据 | ||||||
|      */ |      */ | ||||||
|     fun streamChat(request: ChatRequest): Flow<ChatResponsePart> |     fun streamChat(request: ChatRequest): Publisher<ChatResponsePart> | ||||||
|  | 
 | ||||||
|     /** |     /** | ||||||
|      * 使用流式多模态聊天交互 |      * 使用流式多模态聊天交互 | ||||||
|      * |      * | ||||||
|      * @param request 聊天请求对象,包含用户输入、上下文等信息 |      * @param request 聊天请求对象,包含用户输入、上下文等信息 | ||||||
|      * @return 返回一个Flow流,包含部分聊天响应,允许逐步处理和消费响应 |      * @return 返回一个发布者对象,用于订阅聊天响应的部分数据 | ||||||
|      */ |      */ | ||||||
|     fun streamMultimodalityChat(request: ChatRequest) : Flow<ChatResponsePart> |     fun streamMultimodalityChat(request: ChatRequest): Publisher<ChatResponsePart> | ||||||
| } | } | ||||||
|  | |||||||
| @ -10,8 +10,9 @@ import org.jcnc.llmx.core.common.entities.response.Usage | |||||||
| 
 | 
 | ||||||
| import org.jcnc.llmx.impl.baiLian.adapter.DashScopeAdapter | import org.jcnc.llmx.impl.baiLian.adapter.DashScopeAdapter | ||||||
| import org.jcnc.llmx.impl.baiLian.config.entities.DashScopeProperties | import org.jcnc.llmx.impl.baiLian.config.entities.DashScopeProperties | ||||||
| import org.jcnc.llmx.impl.baiLian.entities.response.DashScopeResponse | import org.jcnc.llmx.impl.baiLian.entities.response.DashScopeMultimodalityChatResponse | ||||||
| import org.jcnc.llmx.impl.baiLian.service.BaiLianModelService | import org.jcnc.llmx.impl.baiLian.service.BaiLianModelService | ||||||
|  | import org.reactivestreams.Publisher | ||||||
| 
 | 
 | ||||||
| import org.springframework.stereotype.Service | import org.springframework.stereotype.Service | ||||||
| import org.springframework.util.CollectionUtils | import org.springframework.util.CollectionUtils | ||||||
| @ -35,7 +36,7 @@ class BaiLianModelServiceImpl( | |||||||
|      * @param request 聊天请求对象,包含用户输入、上下文等信息 |      * @param request 聊天请求对象,包含用户输入、上下文等信息 | ||||||
|      * @return 返回一个Flow流,包含部分聊天响应,允许逐步处理和消费响应 |      * @return 返回一个Flow流,包含部分聊天响应,允许逐步处理和消费响应 | ||||||
|      */ |      */ | ||||||
|     override fun streamChat(request: ChatRequest): Flow<ChatResponsePart> { |     override fun streamChat(request: ChatRequest): Publisher<ChatResponsePart> { | ||||||
|         // 构造请求URL |         // 构造请求URL | ||||||
|         val url = "${dashScopeProperties.baseUrl}${dashScopeProperties.appId}/completion" |         val url = "${dashScopeProperties.baseUrl}${dashScopeProperties.appId}/completion" | ||||||
|         log.info("请求URL: $url") |         log.info("请求URL: $url") | ||||||
| @ -132,7 +133,7 @@ class BaiLianModelServiceImpl( | |||||||
|      * @param request 聊天请求对象,包含用户输入、上下文等信息 |      * @param request 聊天请求对象,包含用户输入、上下文等信息 | ||||||
|      * @return 返回一个Flow流,包含部分聊天响应,允许逐步处理和消费响应 |      * @return 返回一个Flow流,包含部分聊天响应,允许逐步处理和消费响应 | ||||||
|      */ |      */ | ||||||
|     override fun streamMultimodalityChat(request: ChatRequest): Flow<ChatResponsePart> { |     override fun streamMultimodalityChat(request: ChatRequest): Publisher<ChatResponsePart> { | ||||||
|         // 构造请求URL |         // 构造请求URL | ||||||
|         val url = dashScopeProperties.multimodalityUrl |         val url = dashScopeProperties.multimodalityUrl | ||||||
|         log.info("请求URL: $url") |         log.info("请求URL: $url") | ||||||
| @ -159,7 +160,7 @@ class BaiLianModelServiceImpl( | |||||||
|         return dashScopeAdapter.sendStreamChat( |         return dashScopeAdapter.sendStreamChat( | ||||||
|             url, headers, body, |             url, headers, body, | ||||||
|             { json: String -> |             { json: String -> | ||||||
|                 val response = objectMapper.readValue(json, DashScopeResponse::class.java) |                 val response = objectMapper.readValue(json, DashScopeMultimodalityChatResponse::class.java) | ||||||
|                 val choices = response |                 val choices = response | ||||||
|                     .output |                     .output | ||||||
|                     ?.choices |                     ?.choices | ||||||
|  | |||||||
| @ -0,0 +1,60 @@ | |||||||
|  | package org.jcnc.llmx.impl.openai.adapter | ||||||
|  | 
 | ||||||
|  | import com.fasterxml.jackson.databind.ObjectMapper | ||||||
|  | import com.gewuyou.forgeboot.core.extension.log | ||||||
|  | import kotlinx.coroutines.CoroutineDispatcher | ||||||
|  | import kotlinx.coroutines.currentCoroutineContext | ||||||
|  | import kotlinx.coroutines.flow.FlowCollector | ||||||
|  | import kotlinx.coroutines.isActive | ||||||
|  | import kotlinx.coroutines.withContext | ||||||
|  | import okhttp3.OkHttpClient | ||||||
|  | import org.jcnc.llmx.core.common.entities.response.ChatResponsePart | ||||||
|  | import org.jcnc.llmx.core.spi.adapter.AbstractChatStreamAdapter | ||||||
|  | import org.springframework.stereotype.Component | ||||||
|  | import java.io.BufferedReader | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  *Open AI 适配器 | ||||||
|  |  * | ||||||
|  |  * @since 2025-05-11 18:01:15 | ||||||
|  |  * @author gewuyou | ||||||
|  |  */ | ||||||
|  | @Component | ||||||
|  | class OpenAiAdapter( | ||||||
|  |     okHttpClient: OkHttpClient, | ||||||
|  |     objectMapper: ObjectMapper, | ||||||
|  | ) : AbstractChatStreamAdapter(okHttpClient, objectMapper) { | ||||||
|  | 
 | ||||||
|  |     override suspend fun FlowCollector<ChatResponsePart>.processResponse( | ||||||
|  |         reader: BufferedReader, | ||||||
|  |         extractContent: (String) -> ChatResponsePart, | ||||||
|  |         allContent: StringBuilder, | ||||||
|  |         dispatcher: CoroutineDispatcher, | ||||||
|  |     ) { | ||||||
|  |         while (currentCoroutineContext().isActive) { | ||||||
|  |             val line = withContext(dispatcher) { | ||||||
|  |                 reader.readLine() | ||||||
|  |             } ?: break | ||||||
|  | 
 | ||||||
|  |             if (line.startsWith("event:")) { | ||||||
|  |                 // 可选:可读取事件类型 | ||||||
|  |                 continue | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             if (line.startsWith("data:")) { | ||||||
|  |                 val jsonPart = line.removePrefix("data:").trim() | ||||||
|  |                 try { | ||||||
|  |                     val part = extractContent(jsonPart) | ||||||
|  |                     allContent.append(part.content) | ||||||
|  |                     log.debug("✅ 提取内容: {}", part) | ||||||
|  |                     if (part.content.isNotBlank()) { | ||||||
|  |                         emit(part) | ||||||
|  |                     } | ||||||
|  |                 } catch (e: Exception) { | ||||||
|  |                     log.warn("⚠️ 无法解析 JSON: {}", jsonPart, e) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
| @ -0,0 +1,60 @@ | |||||||
|  | package org.jcnc.llmx.impl.openai.config | ||||||
|  | 
 | ||||||
|  | import com.gewuyou.forgeboot.core.extension.log | ||||||
|  | import okhttp3.OkHttpClient | ||||||
|  | import org.springframework.context.annotation.Bean | ||||||
|  | import org.springframework.context.annotation.Configuration | ||||||
|  | import org.springframework.core.env.Environment | ||||||
|  | import java.security.SecureRandom | ||||||
|  | import java.security.cert.X509Certificate | ||||||
|  | import java.util.concurrent.TimeUnit | ||||||
|  | import javax.net.ssl.SSLContext | ||||||
|  | import javax.net.ssl.TrustManager | ||||||
|  | import javax.net.ssl.X509TrustManager | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  *客户端配置 | ||||||
|  |  * | ||||||
|  |  * @since 2025-03-28 17:03:48 | ||||||
|  |  * @author gewuyou | ||||||
|  |  */ | ||||||
|  | @Configuration | ||||||
|  | class ClientConfig { | ||||||
|  |     /** | ||||||
|  |      * OkHttpClient | ||||||
|  |      */ | ||||||
|  |     @Bean | ||||||
|  |     fun createOkHttpClient(environment: Environment): OkHttpClient { | ||||||
|  |         val activeProfiles = environment.activeProfiles.getOrNull(0) | ||||||
|  |         return when (activeProfiles) { | ||||||
|  |             "dev", "test" -> { | ||||||
|  |                 val trustAllCerts = arrayOf<TrustManager>( | ||||||
|  |                     object : X509TrustManager { | ||||||
|  |                         override fun checkClientTrusted(chain: Array<out X509Certificate>?, authType: String?) {} | ||||||
|  |                         override fun checkServerTrusted(chain: Array<out X509Certificate>?, authType: String?) {} | ||||||
|  |                         override fun getAcceptedIssuers(): Array<X509Certificate> = arrayOf() | ||||||
|  |                     } | ||||||
|  |                 ) | ||||||
|  |                 val sslContext = SSLContext.getInstance("TLS") | ||||||
|  |                 sslContext.init(null, trustAllCerts, SecureRandom()) | ||||||
|  |                 val sslSocketFactory = sslContext.socketFactory | ||||||
|  |                 log.warn("警告: 开启了不安全的https请求,请不要在正式环境使用!") | ||||||
|  |                 OkHttpClient.Builder() | ||||||
|  |                     .sslSocketFactory(sslSocketFactory, trustAllCerts[0] as X509TrustManager) | ||||||
|  |                     .hostnameVerifier { _, _ -> true } | ||||||
|  |                     .connectTimeout(30, TimeUnit.SECONDS) | ||||||
|  |                     .readTimeout(30, TimeUnit.SECONDS) | ||||||
|  |                     .writeTimeout(30, TimeUnit.SECONDS) | ||||||
|  |                     .build() | ||||||
|  |             } | ||||||
|  |             else -> { | ||||||
|  |                 OkHttpClient.Builder() | ||||||
|  |                     .connectTimeout(30, TimeUnit.SECONDS) | ||||||
|  |                     .readTimeout(30, TimeUnit.SECONDS) | ||||||
|  |                     .writeTimeout(30, TimeUnit.SECONDS) | ||||||
|  |                     .build() | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @ -0,0 +1,15 @@ | |||||||
|  | package org.jcnc.llmx.impl.openai.config | ||||||
|  | 
 | ||||||
|  | import org.jcnc.llmx.impl.openai.config.entities.OpenAiProperties | ||||||
|  | import org.springframework.boot.context.properties.EnableConfigurationProperties | ||||||
|  | import org.springframework.context.annotation.Configuration | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  *Open AI 配置 | ||||||
|  |  * | ||||||
|  |  * @since 2025-05-11 16:57:14 | ||||||
|  |  * @author gewuyou | ||||||
|  |  */ | ||||||
|  | @Configuration | ||||||
|  | @EnableConfigurationProperties(OpenAiProperties::class) | ||||||
|  | class OpenAiConfig | ||||||
| @ -0,0 +1,15 @@ | |||||||
|  | package org.jcnc.llmx.impl.openai.config.entities | ||||||
|  | 
 | ||||||
|  | import org.springframework.boot.context.properties.ConfigurationProperties | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  *Open AI 属性 | ||||||
|  |  * | ||||||
|  |  * @since 2025-05-11 16:56:15 | ||||||
|  |  * @author gewuyou | ||||||
|  |  */ | ||||||
|  | @ConfigurationProperties(prefix = "openai") | ||||||
|  | class OpenAiProperties { | ||||||
|  |     var apiKey: String = "" | ||||||
|  |     var baseUrl: String = "" | ||||||
|  | } | ||||||
| @ -0,0 +1,29 @@ | |||||||
|  | package org.jcnc.llmx.impl.openai.config.service | ||||||
|  | 
 | ||||||
|  | import org.jcnc.llmx.core.common.entities.request.ChatRequest | ||||||
|  | import org.jcnc.llmx.core.common.entities.response.ChatResponsePart | ||||||
|  | import org.reactivestreams.Publisher | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  *Open AI 模型服务 | ||||||
|  |  * | ||||||
|  |  * @since 2025-05-11 17:25:15 | ||||||
|  |  * @author gewuyou | ||||||
|  |  */ | ||||||
|  | interface OpenAiModelService{ | ||||||
|  |     /** | ||||||
|  |      * 使用流式聊天交互 | ||||||
|  |      * | ||||||
|  |      * @param request 聊天请求对象,包含用户输入、上下文等信息 | ||||||
|  |      * @return 返回一个发布者对象,用于订阅聊天响应的部分数据 | ||||||
|  |      */ | ||||||
|  |     fun streamChat(request: ChatRequest): Publisher<ChatResponsePart> | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * 使用流式多模态聊天交互 | ||||||
|  |      * | ||||||
|  |      * @param request 聊天请求对象,包含用户输入、上下文等信息 | ||||||
|  |      * @return 返回一个发布者对象,用于订阅聊天响应的部分数据 | ||||||
|  |      */ | ||||||
|  |     fun streamMultimodalityChat(request: ChatRequest): Publisher<ChatResponsePart> | ||||||
|  | } | ||||||
| @ -0,0 +1,89 @@ | |||||||
|  | package org.jcnc.llmx.impl.openai.config.service.impl | ||||||
|  | 
 | ||||||
|  | import com.fasterxml.jackson.databind.ObjectMapper | ||||||
|  | import com.gewuyou.forgeboot.core.extension.log | ||||||
|  | import org.jcnc.llmx.core.common.entities.request.ChatRequest | ||||||
|  | import org.jcnc.llmx.core.common.entities.response.ChatResponsePart | ||||||
|  | import org.jcnc.llmx.core.common.entities.response.Usage | ||||||
|  | import org.jcnc.llmx.impl.openai.adapter.OpenAiAdapter | ||||||
|  | import org.jcnc.llmx.impl.openai.config.entities.OpenAiProperties | ||||||
|  | import org.jcnc.llmx.impl.openai.config.service.OpenAiModelService | ||||||
|  | import org.jcnc.llmx.impl.openai.entities.response.FullResponse | ||||||
|  | import org.jcnc.llmx.impl.openai.entities.response.OutputTextDelta | ||||||
|  | import org.jcnc.llmx.impl.openai.entities.response.OutputTextDone | ||||||
|  | import org.jcnc.llmx.impl.openai.extension.toOpenAIMultimodalityChatRequest | ||||||
|  | import org.reactivestreams.Publisher | ||||||
|  | import org.springframework.stereotype.Service | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  *Open AI 模型服务实现 | ||||||
|  |  * | ||||||
|  |  * @since 2025-05-11 17:26:23 | ||||||
|  |  * @author gewuyou | ||||||
|  |  */ | ||||||
|  | @Service | ||||||
|  | class OpenAiModelServiceImpl( | ||||||
|  |     private val openAiProperties: OpenAiProperties, | ||||||
|  |     private val openAiAdapter: OpenAiAdapter, | ||||||
|  |     private val objectMapper: ObjectMapper, | ||||||
|  | ) : OpenAiModelService { | ||||||
|  |     /** | ||||||
|  |      * 使用流式聊天交互 | ||||||
|  |      * | ||||||
|  |      * @param request 聊天请求对象,包含用户输入、上下文等信息 | ||||||
|  |      * @return 返回一个发布者对象,用于订阅聊天响应的部分数据 | ||||||
|  |      */ | ||||||
|  |     override fun streamChat(request: ChatRequest): Publisher<ChatResponsePart> { | ||||||
|  |         TODO("Not yet implemented") | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun streamMultimodalityChat(request: ChatRequest): Publisher<ChatResponsePart> { | ||||||
|  |         val url = "${openAiProperties.baseUrl}/v1/responses" | ||||||
|  |         val headers = mapOf( | ||||||
|  |             "Authorization" to "Bearer ${openAiProperties.apiKey}", | ||||||
|  |             "Content-Type" to "application/json" | ||||||
|  |         ) | ||||||
|  |         val openAIRequest = request.toOpenAIMultimodalityChatRequest() | ||||||
|  |         val body = mutableMapOf<String, Any>( | ||||||
|  |             "stream" to true | ||||||
|  |         ).also { | ||||||
|  |             it.putAll(openAIRequest) | ||||||
|  |         } | ||||||
|  |         return openAiAdapter.sendStreamChat( | ||||||
|  |             url, headers, body, | ||||||
|  |             extractContent = { json -> | ||||||
|  |                 val node = objectMapper.readTree(json) | ||||||
|  |                 val type = node["type"]?.asText() | ||||||
|  | 
 | ||||||
|  |                 when (type) { | ||||||
|  |                     "response.output_text.delta" -> { | ||||||
|  |                         val outputTextDelta = objectMapper.treeToValue(node, OutputTextDelta::class.java) | ||||||
|  |                         ChatResponsePart(content = outputTextDelta.delta) | ||||||
|  |                     } | ||||||
|  | 
 | ||||||
|  |                     "response.output_text.done" -> { | ||||||
|  |                         val outputTextDone = objectMapper.treeToValue(node, OutputTextDone::class.java) | ||||||
|  |                         ChatResponsePart(content = outputTextDone.text, done = true) | ||||||
|  |                     } | ||||||
|  |                     "response.completed" -> { | ||||||
|  |                         val completed = objectMapper.treeToValue(node, FullResponse::class.java) | ||||||
|  |                         val usage = completed.usage?.let { | ||||||
|  |                             Usage( | ||||||
|  |                                 promptTokens = it.inputTokens ?: 0, | ||||||
|  |                                 completionTokens = it.outputTokens ?: 0, | ||||||
|  |                                 totalTokens = it.totalTokens ?: 0 | ||||||
|  |                             ) | ||||||
|  |                         } | ||||||
|  |                         ChatResponsePart(content = "", done = true, usage = usage) | ||||||
|  |                     } | ||||||
|  | 
 | ||||||
|  |                     else -> { | ||||||
|  |                         log.debug("忽略事件类型: {}", type) | ||||||
|  |                         ChatResponsePart(content = "") | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  | } | ||||||
| @ -0,0 +1,33 @@ | |||||||
|  | package org.jcnc.llmx.impl.openai.controller | ||||||
|  | 
 | ||||||
|  | import org.jcnc.llmx.core.common.entities.request.ChatRequest | ||||||
|  | import org.jcnc.llmx.core.common.entities.request.EmbeddingRequest | ||||||
|  | import org.jcnc.llmx.core.common.entities.response.ChatResponsePart | ||||||
|  | import org.jcnc.llmx.core.common.entities.response.EmbeddingResponse | ||||||
|  | import org.jcnc.llmx.core.spi.provider.LLMProvider | ||||||
|  | import org.jcnc.llmx.impl.openai.config.service.OpenAiModelService | ||||||
|  | import org.reactivestreams.Publisher | ||||||
|  | import org.springframework.web.bind.annotation.RestController | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  *Open AI 提供商 | ||||||
|  |  * | ||||||
|  |  * @since 2025-05-11 16:32:51 | ||||||
|  |  * @author gewuyou | ||||||
|  |  */ | ||||||
|  | @RestController | ||||||
|  | class OpenAiProvider( | ||||||
|  |     private val openAiModelService: OpenAiModelService | ||||||
|  | ): LLMProvider { | ||||||
|  |     override fun chat(request: ChatRequest): Publisher<ChatResponsePart> { | ||||||
|  |         TODO("Not yet implemented") | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun multimodalityChat(request: ChatRequest): Publisher<ChatResponsePart> { | ||||||
|  |         return openAiModelService.streamMultimodalityChat(request) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun embedding(request: EmbeddingRequest): EmbeddingResponse { | ||||||
|  |         TODO("Not yet implemented") | ||||||
|  |     } | ||||||
|  | } | ||||||
| @ -0,0 +1,140 @@ | |||||||
|  | package org.jcnc.llmx.impl.openai.entities.response | ||||||
|  | 
 | ||||||
|  | import com.fasterxml.jackson.annotation.JsonProperty | ||||||
|  | 
 | ||||||
|  | /** 通用 SSE 响应封装类型 */ | ||||||
|  | data class SseEvent<T>( | ||||||
|  |     @JsonProperty("type") | ||||||
|  |     val type: String, | ||||||
|  |     @JsonProperty("data") | ||||||
|  |     val data: T | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | /** response.created, response.in_progress, response.completed 共用结构 */ | ||||||
|  | data class FullResponse( | ||||||
|  |     @JsonProperty("id") | ||||||
|  |     val id: String, | ||||||
|  |     @JsonProperty("object") | ||||||
|  |     val objectType: String, | ||||||
|  |     @JsonProperty("created_at") | ||||||
|  |     val createdAt: Long, | ||||||
|  |     @JsonProperty("status") | ||||||
|  |     val status: String, | ||||||
|  |     @JsonProperty("model") | ||||||
|  |     val model: String, | ||||||
|  |     @JsonProperty("output") | ||||||
|  |     val output: List<MessageItem>?, | ||||||
|  |     @JsonProperty("service_tier") | ||||||
|  |     val serviceTier: String?, | ||||||
|  |     @JsonProperty("store") | ||||||
|  |     val store: Boolean?, | ||||||
|  |     @JsonProperty("temperature") | ||||||
|  |     val temperature: Double?, | ||||||
|  |     @JsonProperty("top_p") | ||||||
|  |     val topP: Double?, | ||||||
|  |     @JsonProperty("tool_choice") | ||||||
|  |     val toolChoice: String?, | ||||||
|  |     @JsonProperty("tools") | ||||||
|  |     val tools: List<Any>?, | ||||||
|  |     @JsonProperty("text") | ||||||
|  |     val text: TextFormat?, | ||||||
|  |     @JsonProperty("usage") | ||||||
|  |     val usage: UsageInfo? | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | data class TextFormat( | ||||||
|  |     @JsonProperty("format") | ||||||
|  |     val format: FormatType | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | data class FormatType( | ||||||
|  |     @JsonProperty("type") | ||||||
|  |     val type: String | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | data class UsageInfo( | ||||||
|  |     @JsonProperty("input_tokens") | ||||||
|  |     val inputTokens: Int?, | ||||||
|  |     @JsonProperty("output_tokens") | ||||||
|  |     val outputTokens: Int?, | ||||||
|  |     @JsonProperty("total_tokens") | ||||||
|  |     val totalTokens: Int? | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | /** response.output_item.added, response.output_item.done 共用结构 */ | ||||||
|  | data class OutputItemEvent( | ||||||
|  |     @JsonProperty("output_index") | ||||||
|  |     val outputIndex: Int, | ||||||
|  |     @JsonProperty("item") | ||||||
|  |     val item: MessageItem | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | data class MessageItem( | ||||||
|  |     @JsonProperty("id") | ||||||
|  |     val id: String, | ||||||
|  |     @JsonProperty("type") | ||||||
|  |     val type: String, | ||||||
|  |     @JsonProperty("status") | ||||||
|  |     val status: String, | ||||||
|  |     @JsonProperty("content") | ||||||
|  |     val content: List<MessageContent>, | ||||||
|  |     @JsonProperty("role") | ||||||
|  |     val role: String | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | data class MessageContent( | ||||||
|  |     @JsonProperty("type") | ||||||
|  |     val type: String, | ||||||
|  |     @JsonProperty("text") | ||||||
|  |     val text: String, | ||||||
|  |     @JsonProperty("annotations") | ||||||
|  |     val annotations: List<Any>? | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | /** response.output_text.delta */ | ||||||
|  | data class OutputTextDelta( | ||||||
|  |     @JsonProperty("item_id") | ||||||
|  |     val itemId: String, | ||||||
|  |     @JsonProperty("output_index") | ||||||
|  |     val outputIndex: Int, | ||||||
|  |     @JsonProperty("content_index") | ||||||
|  |     val contentIndex: Int, | ||||||
|  |     @JsonProperty("delta") | ||||||
|  |     val delta: String | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | /** response.output_text.done */ | ||||||
|  | data class OutputTextDone( | ||||||
|  |     @JsonProperty("item_id") | ||||||
|  |     val itemId: String, | ||||||
|  |     @JsonProperty("output_index") | ||||||
|  |     val outputIndex: Int, | ||||||
|  |     @JsonProperty("content_index") | ||||||
|  |     val contentIndex: Int, | ||||||
|  |     @JsonProperty("text") | ||||||
|  |     val text: String | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | /** response.content_part.done */ | ||||||
|  | data class ContentPartDone( | ||||||
|  |     @JsonProperty("item_id") | ||||||
|  |     val itemId: String, | ||||||
|  |     @JsonProperty("output_index") | ||||||
|  |     val outputIndex: Int, | ||||||
|  |     @JsonProperty("content_index") | ||||||
|  |     val contentIndex: Int, | ||||||
|  |     @JsonProperty("part") | ||||||
|  |     val part: MessageContent | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | /** response.content_part.added */ | ||||||
|  | data class ContentPartAdded( | ||||||
|  |     @JsonProperty("item_id") | ||||||
|  |     val itemId: String, | ||||||
|  |     @JsonProperty("output_index") | ||||||
|  |     val outputIndex: Int, | ||||||
|  |     @JsonProperty("content_index") | ||||||
|  |     val contentIndex: Int, | ||||||
|  |     @JsonProperty("part") | ||||||
|  |     val part: MessageContent | ||||||
|  | ) | ||||||
| @ -0,0 +1,36 @@ | |||||||
|  | package org.jcnc.llmx.impl.openai.extension | ||||||
|  | 
 | ||||||
|  | import org.jcnc.llmx.core.common.entities.request.ChatRequest | ||||||
|  | import org.jcnc.llmx.core.common.entities.request.MultiModalContent | ||||||
|  | 
 | ||||||
|  | fun ChatRequest.toOpenAIMultimodalityChatRequest(): Map<String, Any> { | ||||||
|  |     val input = this.messages.map { msg -> | ||||||
|  |         mapOf( | ||||||
|  |             "role" to msg.role, | ||||||
|  |             "content" to msg.content.map { content -> | ||||||
|  |                 when (content) { | ||||||
|  |                     is MultiModalContent.Text -> mapOf( | ||||||
|  |                         "type" to "input_text", | ||||||
|  |                         "text" to content.text | ||||||
|  |                     ) | ||||||
|  | 
 | ||||||
|  |                     is MultiModalContent.Image -> mapOf( | ||||||
|  |                         "type" to "input_image", | ||||||
|  |                         "image_url" to content.image | ||||||
|  |                     ) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     val base = mutableMapOf( | ||||||
|  |         "model" to model, | ||||||
|  |         "input" to input, | ||||||
|  |     ) | ||||||
|  | 
 | ||||||
|  |     // 附加参数(比如 temperature, max_tokens 等) | ||||||
|  |     base.putAll(options) | ||||||
|  | 
 | ||||||
|  |     return base | ||||||
|  | } | ||||||
|  | 
 | ||||||
| @ -0,0 +1,2 @@ | |||||||
|  | server: | ||||||
|  |   port: 8083 | ||||||
| @ -0,0 +1,2 @@ | |||||||
|  | server: | ||||||
|  |   port: 9004 | ||||||
| @ -0,0 +1,2 @@ | |||||||
|  | server: | ||||||
|  |   port: 9004 | ||||||
| @ -1 +0,0 @@ | |||||||
| spring.application.name=llmx-impl-openai |  | ||||||
| @ -0,0 +1,3 @@ | |||||||
|  | spring: | ||||||
|  |   profiles: | ||||||
|  |     active: dev | ||||||
| @ -0,0 +1,22 @@ | |||||||
|  | spring: | ||||||
|  |   cloud: | ||||||
|  |     nacos: | ||||||
|  |       ip: 127.0.0.1 | ||||||
|  |       username: nacos | ||||||
|  |       password: L4s6f9y3, | ||||||
|  |       server-addr: 49.235.96.75:8848 | ||||||
|  |       discovery: | ||||||
|  |         ip: ${spring.cloud.nacos.ip} | ||||||
|  |         username: ${spring.cloud.nacos.username} | ||||||
|  |         password: ${spring.cloud.nacos.password} | ||||||
|  |         server-addr: ${spring.cloud.nacos.server-addr} | ||||||
|  |         group: llmx-${spring.profiles.active} | ||||||
|  |         namespace: a17d57ec-4fd9-44c7-a617-7f6003a0b332 | ||||||
|  |       config: | ||||||
|  |         file-extension: yaml | ||||||
|  |         namespace: a17d57ec-4fd9-44c7-a617-7f6003a0b332 | ||||||
|  |         refresh-enabled: true | ||||||
|  |         extension-configs: | ||||||
|  |           - data-id: ${spring.application.name}-${spring.profiles.active}.yaml | ||||||
|  |             refresh: true | ||||||
|  |             group: ${spring.application.name} | ||||||
| @ -0,0 +1,20 @@ | |||||||
|  | spring: | ||||||
|  |   cloud: | ||||||
|  |     nacos: | ||||||
|  |       username: nacos | ||||||
|  |       password: L4s6f9y3, | ||||||
|  |       server-addr: 49.235.96.75:8848 | ||||||
|  |       discovery: | ||||||
|  |         username: ${spring.cloud.nacos.username} | ||||||
|  |         password: ${spring.cloud.nacos.password} | ||||||
|  |         server-addr: ${spring.cloud.nacos.server-addr} | ||||||
|  |         group: llmx-${spring.profiles.active} | ||||||
|  |         namespace: ab34d859-6f1a-4f28-ac6b-27a7410ab27b | ||||||
|  |       config: | ||||||
|  |         file-extension: yaml | ||||||
|  |         namespace: ab34d859-6f1a-4f28-ac6b-27a7410ab27b | ||||||
|  |         refresh-enabled: true | ||||||
|  |         extension-configs: | ||||||
|  |           - data-id: ${spring.application.name}-${spring.profiles.active}.yaml | ||||||
|  |             refresh: true | ||||||
|  |             group: ${spring.application.name} | ||||||
| @ -0,0 +1,20 @@ | |||||||
|  | spring: | ||||||
|  |   cloud: | ||||||
|  |     nacos: | ||||||
|  |       username: nacos | ||||||
|  |       password: L4s6f9y3, | ||||||
|  |       server-addr: llmx-nacos:8848 | ||||||
|  |       discovery: | ||||||
|  |         username: ${spring.cloud.nacos.username} | ||||||
|  |         password: ${spring.cloud.nacos.password} | ||||||
|  |         server-addr: ${spring.cloud.nacos.server-addr} | ||||||
|  |         group: llmx-${spring.profiles.active} | ||||||
|  |         namespace: 54a289f7-5f4a-4c83-8a0a-199defa35458 | ||||||
|  |       config: | ||||||
|  |         file-extension: yaml | ||||||
|  |         namespace: 54a289f7-5f4a-4c83-8a0a-199defa35458 | ||||||
|  |         refresh-enabled: true | ||||||
|  |         extension-configs: | ||||||
|  |           - data-id: ${spring.application.name}-${spring.profiles.active}.yaml | ||||||
|  |             refresh: true | ||||||
|  |             group: ${spring.application.name} | ||||||
| @ -0,0 +1,5 @@ | |||||||
|  | spring: | ||||||
|  |   application: | ||||||
|  |     name: llmx-impl-openai | ||||||
|  |   profiles: | ||||||
|  |     active: dev | ||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user