@@ -27,17 +27,17 @@ import kotlinx.coroutines.flow.flow
2727import kotlinx.serialization.json.Json
2828
2929/*
30- Description: Ktor-based HTTP client for OpenAI chat completions
30+ Description: Ktor-based HTTP client for multi-provider chat completions
3131 Author: Junerver
3232 Date: 2024
3333 Email: junerver@gmail.com
34- Version: v1 .0
34+ Version: v2 .0
3535*/
3636
3737/* *
3838 * Represents a streaming event from the chat API.
3939 */
40- internal sealed class StreamEvent {
40+ sealed class StreamEvent {
4141 data class Delta (
4242 val content : String ,
4343 val role : String? = null ,
@@ -51,10 +51,11 @@ internal sealed class StreamEvent {
5151}
5252
5353/* *
54- * HTTP client for interacting with OpenAI-compatible chat APIs.
54+ * HTTP client for interacting with chat APIs.
55+ *
56+ * Supports multiple providers through the [ChatProvider] abstraction.
5557 */
5658internal class ChatClient (private val options : ChatOptions ) {
57-
5859 private val json = Json {
5960 ignoreUnknownKeys = true
6061 isLenient = true
@@ -84,13 +85,7 @@ internal class ChatClient(private val options: ChatOptions) {
8485 */
8586 suspend fun streamChat (messages : List <Message >): Flow <StreamEvent > = flow {
8687 try {
87- val requestBody = ChatCompletionRequest (
88- model = options.model,
89- messages = messages.toRequestMessages(),
90- stream = true ,
91- temperature = options.temperature,
92- maxTokens = options.maxTokens,
93- )
88+ val requestBody = options.buildRequestBody(messages, stream = true )
9489
9590 httpClient.preparePost(options.buildEndpoint()) {
9691 // SSE streams need longer/no timeout
@@ -99,7 +94,10 @@ internal class ChatClient(private val options: ChatOptions) {
9994 socketTimeoutMillis = Long .MAX_VALUE
10095 }
10196 contentType(ContentType .Application .Json .withCharset(Charsets .UTF_8 ))
102- header(HttpHeaders .Authorization , " Bearer ${options.apiKey} " )
97+ // Use provider-specific auth headers
98+ options.buildAuthHeaders().forEach { (key, value) ->
99+ header(key, value)
100+ }
103101 header(HttpHeaders .Accept , " text/event-stream" )
104102 header(HttpHeaders .CacheControl , " no-cache" )
105103 header(HttpHeaders .Connection , " keep-alive" )
@@ -120,8 +118,8 @@ internal class ChatClient(private val options: ChatOptions) {
120118 errorMessage = errorResponse.error.message,
121119 errorType = errorResponse.error.type,
122120 errorCode = errorResponse.error.code,
123- )
124- )
121+ ),
122+ ),
125123 )
126124 } catch (e: Exception ) {
127125 emit(StreamEvent .Error (Exception (" HTTP ${response.status.value} : $errorBody " )))
@@ -133,37 +131,11 @@ internal class ChatClient(private val options: ChatOptions) {
133131 while (! channel.isClosedForRead) {
134132 val line = channel.readUTF8Line() ? : continue
135133
136- if (line.isBlank()) continue
137-
138- if (! line.startsWith(" data: " )) continue
139-
140- val data = line.removePrefix(" data: " ).trim()
141-
142- if (data == " [DONE]" ) {
143- emit(StreamEvent .Done )
144- break
145- }
146-
147- try {
148- val chunk = json.decodeFromString<ChatCompletionChunk >(data)
149- val choice = chunk.choices?.firstOrNull()
150- val delta = choice?.delta
151- val content = delta?.content ? : " "
152- val role = delta?.role
153- val finishReason = choice?.finishReason
154-
155- if (content.isNotEmpty() || role != null || finishReason != null ) {
156- emit(
157- StreamEvent .Delta (
158- content = content,
159- role = role,
160- finishReason = finishReason,
161- usage = chunk.usage,
162- )
163- )
164- }
165- } catch (e: Exception ) {
166- // Skip malformed JSON chunks
134+ // Use provider-specific stream parsing
135+ val event = options.provider.parseStreamLine(line)
136+ if (event != null ) {
137+ emit(event)
138+ if (event is StreamEvent .Done ) break
167139 }
168140 }
169141 }
@@ -179,17 +151,14 @@ internal class ChatClient(private val options: ChatOptions) {
179151 * @return The complete assistant message
180152 */
181153 suspend fun chat (messages : List <Message >): Message {
182- val requestBody = ChatCompletionRequest (
183- model = options.model,
184- messages = messages.toRequestMessages(),
185- stream = false ,
186- temperature = options.temperature,
187- maxTokens = options.maxTokens,
188- )
154+ val requestBody = options.buildRequestBody(messages, stream = false )
189155
190156 val response: HttpResponse = httpClient.post(options.buildEndpoint()) {
191157 contentType(ContentType .Application .Json .withCharset(Charsets .UTF_8 ))
192- header(HttpHeaders .Authorization , " Bearer ${options.apiKey} " )
158+ // Use provider-specific auth headers
159+ options.buildAuthHeaders().forEach { (key, value) ->
160+ header(key, value)
161+ }
193162 options.headers.forEach { (key, value) ->
194163 header(key, value)
195164 }
@@ -215,11 +184,9 @@ internal class ChatClient(private val options: ChatOptions) {
215184 }
216185
217186 val responseBody = response.bodyAsChannel().readUTF8Line() ? : throw Exception (" Empty response" )
218- val completionResponse = json.decodeFromString<ChatCompletionResponse >(responseBody)
219- val choice = completionResponse.choices.firstOrNull()
220- ? : throw Exception (" No choices in response" )
221-
222- return Message .assistant(content = choice.message.content ? : " " )
187+ // Use provider-specific response parsing
188+ val result = options.provider.parseResponse(responseBody)
189+ return result.message
223190 }
224191
225192 /* *
0 commit comments