Skip to content

Commit 86cdab6

Browse files
committed
convert from gson to jackson
1 parent cf65c34 commit 86cdab6

32 files changed

+285
-455
lines changed

build.gradle.kts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,12 @@ repositories {
1919

2020
dependencies {
2121
implementation("com.squareup.okhttp3:okhttp:4.9.2")
22-
implementation("com.google.code.gson:gson:2.10.1")
22+
23+
implementation("com.fasterxml.jackson.core:jackson-core:2.15.3")
24+
implementation("com.fasterxml.jackson.core:jackson-databind:2.15.3")
25+
implementation("com.fasterxml.jackson.core:jackson-annotations:2.15.3")
26+
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.15.3")
27+
2328
implementation("org.jetbrains:annotations:24.0.1")
2429

2530
testImplementation("io.github.cdimascio:dotenv-kotlin:6.4.1")

src/main/kotlin/com/cjcrafter/openai/AzureOpenAI.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ class AzureOpenAI @JvmOverloads constructor(
2626
) : OpenAIImpl(apiKey, organization, client) {
2727

2828
override fun buildRequest(request: Any, endpoint: String): Request {
29-
val json = gson.toJson(request)
29+
val json = objectMapper.writeValueAsString(request)
3030
val body: RequestBody = json.toRequestBody(mediaType)
3131
return Request.Builder()
3232
.url("$azureBaseUrl/openai/deployments/$modelName/$endpoint?api-version=$apiVersion")

src/main/kotlin/com/cjcrafter/openai/FinishReason.kt

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package com.cjcrafter.openai
22

3-
import com.google.gson.annotations.SerializedName
3+
import com.fasterxml.jackson.annotation.JsonProperty
44

55
/**
66
* [FinishReason] wraps the possible reasons that a generation model may stop
@@ -15,7 +15,7 @@ enum class FinishReason {
1515
* completely generates its entire message, and has nothing else to add.
1616
* Ideally, you always want your finish reason to be [STOP].
1717
*/
18-
@SerializedName("stop")
18+
@JsonProperty("stop")
1919
STOP,
2020

2121
/**
@@ -24,23 +24,23 @@ enum class FinishReason {
2424
* message with finish reason [LENGTH]. Some models have a higher token
2525
* limit than others.
2626
*/
27-
@SerializedName("length")
27+
@JsonProperty("length")
2828
LENGTH,
2929

3030
/**
3131
* Occurs due to a flag from OpenAI's content filters. This occurrence is
3232
* rare, and tends to happen when you blatantly violate OpenAI's terms.
3333
*/
34-
@SerializedName("content_filter")
34+
@JsonProperty("content_filter")
3535
CONTENT_FILTER,
3636

3737
/**
3838
* Occurs when the model uses one of the available tools.
3939
*/
40-
@SerializedName("tool_calls")
40+
@JsonProperty("tool_calls")
4141
TOOL_CALLS,
4242

4343
@Deprecated("functions have been replaced by tools")
44-
@SerializedName("function_call")
44+
@JsonProperty("function_call")
4545
FUNCTION_CALL;
4646
}

src/main/kotlin/com/cjcrafter/openai/OpenAI.kt

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,15 @@ import com.cjcrafter.openai.chat.tool.ToolChoice
55
import com.cjcrafter.openai.completions.CompletionRequest
66
import com.cjcrafter.openai.completions.CompletionResponse
77
import com.cjcrafter.openai.completions.CompletionResponseChunk
8-
import com.google.gson.Gson
9-
import com.google.gson.GsonBuilder
8+
import com.cjcrafter.openai.jackson.ChatChoiceChunkDeserializer
9+
import com.cjcrafter.openai.jackson.ChatChoiceChunkSerializer
10+
import com.cjcrafter.openai.jackson.ToolChoiceDeserializer
11+
import com.cjcrafter.openai.jackson.ToolChoiceSerializer
12+
import com.fasterxml.jackson.annotation.JsonInclude
13+
import com.fasterxml.jackson.databind.DeserializationFeature
14+
import com.fasterxml.jackson.databind.ObjectMapper
15+
import com.fasterxml.jackson.databind.module.SimpleModule
16+
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
1017
import okhttp3.OkHttpClient
1118

1219
interface OpenAI {
@@ -129,22 +136,22 @@ interface OpenAI {
129136
fun azureBuilder() = AzureBuilder()
130137

131138
/**
132-
* Returns a Gson instance with the default OpenAI adapters registered.
139+
* Returns an ObjectMapper instance with the default OpenAI adapters registered.
133140
* This can be used to save conversations (and other data) to file.
134141
*/
135-
@JvmStatic
136-
fun createGson(): Gson = createGsonBuilder().create()
137-
138-
/**
139-
* Returns a GsonBuilder instance with the default OpenAI adapters
140-
* registered.
141-
*/
142-
@JvmStatic
143-
fun createGsonBuilder(): GsonBuilder {
144-
return GsonBuilder()
145-
.serializeNulls()
146-
.registerTypeAdapter(ChatChoiceChunk::class.java, ChatChoiceChunk.adapter())
147-
.registerTypeAdapter(ToolChoice::class.java, ToolChoice.adapter())
142+
fun createObjectMapper(): ObjectMapper = jacksonObjectMapper().apply {
143+
setSerializationInclusion(JsonInclude.Include.NON_NULL)
144+
configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
145+
146+
// Register modules with custom serializers/deserializers
147+
val module = SimpleModule().apply {
148+
addSerializer(ChatChoiceChunk::class.java, ChatChoiceChunkSerializer())
149+
addDeserializer(ChatChoiceChunk::class.java, ChatChoiceChunkDeserializer())
150+
addSerializer(ToolChoice::class.java, ToolChoiceSerializer())
151+
addDeserializer(ToolChoice::class.java, ToolChoiceDeserializer())
152+
}
153+
154+
registerModule(module)
148155
}
149156

150157
/**

src/main/kotlin/com/cjcrafter/openai/OpenAIImpl.kt

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,7 @@ import com.cjcrafter.openai.completions.CompletionRequest
55
import com.cjcrafter.openai.completions.CompletionResponse
66
import com.cjcrafter.openai.completions.CompletionResponseChunk
77
import com.cjcrafter.openai.completions.CompletionUsage
8-
import com.cjcrafter.openai.exception.WrappedIOError
9-
import com.google.gson.JsonObject
10-
import com.google.gson.JsonParser
8+
import com.fasterxml.jackson.databind.node.ObjectNode
119
import okhttp3.*
1210
import okhttp3.MediaType.Companion.toMediaType
1311
import okhttp3.RequestBody.Companion.toRequestBody
@@ -55,10 +53,10 @@ open class OpenAIImpl @JvmOverloads constructor(
5553
private val client: OkHttpClient = OkHttpClient()
5654
): OpenAI {
5755
protected val mediaType = "application/json; charset=utf-8".toMediaType()
58-
protected val gson = OpenAI.createGson()
56+
protected val objectMapper = OpenAI.createObjectMapper()
5957

6058
protected open fun buildRequest(request: Any, endpoint: String): Request {
61-
val json = gson.toJson(request)
59+
val json = objectMapper.writeValueAsString(request)
6260
val body: RequestBody = json.toRequestBody(mediaType)
6361
return Request.Builder()
6462
.url("https://api.openai.com/$endpoint")
@@ -73,12 +71,8 @@ open class OpenAIImpl @JvmOverloads constructor(
7371
request.stream = false // use streamCompletion for stream=true
7472
val httpRequest = buildRequest(request, COMPLETIONS_ENDPOINT)
7573

76-
try {
77-
val httpResponse = client.newCall(httpRequest).execute()
78-
println(httpResponse)
79-
} catch (ex: IOException) {
80-
throw WrappedIOError(ex)
81-
}
74+
val httpResponse = client.newCall(httpRequest).execute()
75+
println(httpResponse)
8276

8377
return CompletionResponse("1", 1, "1", listOf(), CompletionUsage(1, 1, 1))
8478
}
@@ -104,7 +98,7 @@ open class OpenAIImpl @JvmOverloads constructor(
10498
}
10599

106100
val json = httpResponse.body?.byteStream()?.bufferedReader() ?: throw IOException("Response body is null")
107-
return gson.fromJson(json, ChatResponse::class.java)
101+
return objectMapper.readValue(json, ChatResponse::class.java)
108102
}
109103

110104
override fun streamChatCompletion(request: ChatRequest): Iterable<ChatResponseChunk> {
@@ -150,7 +144,7 @@ open class OpenAIImpl @JvmOverloads constructor(
150144
override fun next(): ChatResponseChunk {
151145
val currentLine = nextLine ?: throw NoSuchElementException("No more lines")
152146
//println(" $currentLine")
153-
chunk = chunk?.apply { update(JsonParser.parseString(currentLine).asJsonObject) } ?: gson.fromJson(currentLine, ChatResponseChunk::class.java)
147+
chunk = chunk?.apply { update(objectMapper.readTree(currentLine) as ObjectNode) } ?: objectMapper.readValue(currentLine, ChatResponseChunk::class.java)
154148
nextLine = readNextLine(reader) // Prepare the next line
155149
return chunk!!
156150
}
@@ -159,8 +153,6 @@ open class OpenAIImpl @JvmOverloads constructor(
159153
}
160154
}
161155

162-
163-
164156
companion object {
165157
const val COMPLETIONS_ENDPOINT = "v1/completions"
166158
const val CHAT_ENDPOINT = "v1/chat/completions"

src/main/kotlin/com/cjcrafter/openai/chat/ChatChoice.kt

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
package com.cjcrafter.openai.chat
22

33
import com.cjcrafter.openai.FinishReason
4-
import com.google.gson.JsonObject
5-
import com.google.gson.annotations.SerializedName
4+
import com.fasterxml.jackson.annotation.JsonProperty
65

76
/**
87
* The OpenAI API returns a list of `ChatChoice`. Each choice has a
@@ -24,5 +23,5 @@ import com.google.gson.annotations.SerializedName
2423
data class ChatChoice(
2524
val index: Int,
2625
val message: ChatMessage,
27-
@field:SerializedName("finish_reason") val finishReason: FinishReason
26+
@JsonProperty("finish_reason") val finishReason: FinishReason
2827
)

src/main/kotlin/com/cjcrafter/openai/chat/ChatChoiceChunk.kt

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
package com.cjcrafter.openai.chat
22

33
import com.cjcrafter.openai.FinishReason
4-
import com.cjcrafter.openai.gson.ChatChoiceChunkAdapter
5-
import com.google.gson.JsonObject
6-
import com.google.gson.annotations.SerializedName
4+
import com.cjcrafter.openai.jackson.ChatChoiceChunkDeserializer
5+
import com.cjcrafter.openai.jackson.ChatChoiceChunkSerializer
6+
import com.fasterxml.jackson.annotation.JsonProperty
7+
import com.fasterxml.jackson.databind.JsonNode
8+
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
79

810
/**
911
*
@@ -29,14 +31,22 @@ data class ChatChoiceChunk(
2931
val index: Int,
3032
val message: ChatMessage,
3133
var delta: String,
32-
@field:SerializedName("finish_reason") var finishReason: FinishReason?
34+
@JsonProperty("finish_reason") var finishReason: FinishReason?
3335
) {
3436

35-
internal fun update(json: JsonObject) {
36-
val deltaJson = json["delta"].asJsonObject
37-
delta = if (deltaJson.has("content")) deltaJson["content"].asString else ""
37+
internal fun update(json: String) {
38+
val node: JsonNode = jacksonObjectMapper().readTree(json)
39+
val deltaNode = node.get("delta")
40+
delta = if (deltaNode?.has("content") == true && !deltaNode.get("content").isNull) {
41+
deltaNode.get("content").asText()
42+
} else {
43+
""
44+
}
45+
3846
message.content += delta
39-
finishReason = if (json["finish_reason"].isJsonNull) null else FinishReason.valueOf(json["finish_reason"].asString.uppercase())
47+
finishReason = node.get("finish_reason")?.takeIf { !it.isNull }?.asText()?.let {
48+
FinishReason.valueOf(it.uppercase())
49+
}
4050
}
4151

4252
/**
@@ -48,7 +58,10 @@ data class ChatChoiceChunk(
4858

4959
companion object {
5060
@JvmStatic
51-
fun adapter() = ChatChoiceChunkAdapter()
61+
fun serializer() = ChatChoiceChunkSerializer()
62+
63+
@JvmStatic
64+
fun deserializer() = ChatChoiceChunkDeserializer()
5265
}
5366
}
5467

src/main/kotlin/com/cjcrafter/openai/chat/ChatMessage.kt

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
package com.cjcrafter.openai.chat
22

33
import com.cjcrafter.openai.chat.tool.ToolCall
4-
import com.google.gson.annotations.SerializedName
4+
import com.fasterxml.jackson.annotation.JsonInclude
5+
import com.fasterxml.jackson.annotation.JsonProperty
56

67
/**
78
* ChatGPT's biggest innovation is its conversation memory. To remember the
@@ -14,9 +15,9 @@ import com.google.gson.annotations.SerializedName
1415
*/
1516
data class ChatMessage @JvmOverloads constructor(
1617
var role: ChatUser,
17-
var content: String?,
18-
@field:SerializedName("tool_calls") var toolCalls: List<ToolCall>? = null,
19-
@field:SerializedName("tool_call_id") var toolCallId: String? = null,
18+
@JsonInclude var content: String?, // JsonInclude here is important for tools
19+
@JsonProperty("tool_calls") var toolCalls: List<ToolCall>? = null,
20+
@JsonProperty("tool_call_id") var toolCallId: String? = null,
2021
) {
2122
init {
2223
if (role == ChatUser.TOOL) {

src/main/kotlin/com/cjcrafter/openai/chat/ChatRequest.kt

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,24 @@ package com.cjcrafter.openai.chat
33
import com.cjcrafter.openai.chat.tool.AbstractTool
44
import com.cjcrafter.openai.chat.tool.Tool
55
import com.cjcrafter.openai.chat.tool.ToolChoice
6-
import com.google.gson.annotations.SerializedName
6+
import com.fasterxml.jackson.annotation.JsonProperty
77

88
data class ChatRequest @JvmOverloads internal constructor(
99
var messages: MutableList<ChatMessage>,
1010
var model: String,
11-
@field:SerializedName("frequency_penalty") var frequencyPenalty: Float? = null,
12-
@field:SerializedName("logit_bias") var logitBias: MutableMap<String, Float>? = null,
13-
@field:SerializedName("max_tokens") var maxTokens: Int? = null,
11+
@JsonProperty("frequency_penalty") var frequencyPenalty: Float? = null,
12+
@JsonProperty("logit_bias") var logitBias: MutableMap<String, Float>? = null,
13+
@JsonProperty("max_tokens") var maxTokens: Int? = null,
1414
var n: Int? = null,
15-
@field:SerializedName("presence_penalty") var presencePenalty: Float? = null,
16-
@field:SerializedName("response_format") var responseFormat: ChatResponseFormat? = null,
15+
@JsonProperty("presence_penalty") var presencePenalty: Float? = null,
16+
@JsonProperty("response_format") var responseFormat: ChatResponseFormat? = null,
1717
var seed: Int? = null,
1818
var stop: String? = null,
1919
@Deprecated("Use OpenAI#streamChatCompletion") var stream: Boolean? = null,
2020
var temperature: Float? = null,
21-
@field:SerializedName("top_p") var topP: Float? = null,
21+
@JsonProperty("top_p") var topP: Float? = null,
2222
var tools: MutableList<Tool>? = null,
23-
@field:SerializedName("tool_choice") var toolChoice: ToolChoice? = null,
23+
@JsonProperty("tool_choice") var toolChoice: ToolChoice? = null,
2424
var user: String? = null,
2525
) {
2626

src/main/kotlin/com/cjcrafter/openai/chat/ChatResponseChunk.kt

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package com.cjcrafter.openai.chat
22

3-
import com.google.gson.JsonObject
3+
import com.fasterxml.jackson.databind.JsonNode
4+
import com.fasterxml.jackson.databind.node.ArrayNode
5+
import com.fasterxml.jackson.databind.node.ObjectNode
6+
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
47
import java.time.Instant
58
import java.time.ZoneId
69
import java.time.ZonedDateTime
@@ -26,10 +29,10 @@ data class ChatResponseChunk(
2629
val created: Long,
2730
val choices: List<ChatChoiceChunk>,
2831
) {
29-
30-
internal fun update(json: JsonObject) {
31-
json["choices"].asJsonArray.forEachIndexed { index, jsonElement ->
32-
choices[index].update(jsonElement.asJsonObject)
32+
internal fun update(json: ObjectNode) {
33+
val choicesArray = json.get("choices") as? ArrayNode
34+
choicesArray?.forEachIndexed { index, jsonNode ->
35+
choices[index].update(jsonNode.toString())
3336
}
3437
}
3538

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
package com.cjcrafter.openai.chat
22

3-
import com.google.gson.annotations.SerializedName
3+
import com.fasterxml.jackson.annotation.JsonProperty
44

55
class ChatResponseFormat {
66

77
enum class Type {
8-
@SerializedName("json")
8+
@JsonProperty("json")
99
JSON,
10-
@SerializedName("text")
10+
@JsonProperty("text")
1111
TEXT
1212
}
1313
}

src/main/kotlin/com/cjcrafter/openai/chat/ChatUsage.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package com.cjcrafter.openai.chat
22

3-
import com.google.gson.annotations.SerializedName
3+
import com.fasterxml.jackson.annotation.JsonProperty
44

55
/**
66
* Holds how many tokens that were used by your API request. Use these
@@ -16,7 +16,7 @@ import com.google.gson.annotations.SerializedName
1616
* @see <a href="https://platform.openai.com/docs/guides/chat/managing-tokens">Managing Tokens Guide</a>
1717
*/
1818
data class ChatUsage(
19-
@field:SerializedName("prompt_tokens") val promptTokens: Int,
20-
@field:SerializedName("completion_tokens") val completionTokens: Int,
21-
@field:SerializedName("total_tokens") val totalTokens: Int
19+
@JsonProperty("prompt_tokens") val promptTokens: Int,
20+
@JsonProperty("completion_tokens") val completionTokens: Int,
21+
@JsonProperty("total_tokens") val totalTokens: Int
2222
)

0 commit comments

Comments
 (0)