@@ -4,6 +4,7 @@ import com.cjcrafter.openai.gson.ChatChoiceChunkAdapter
44import com.cjcrafter.openai.chat.*
55import com.cjcrafter.openai.completions.CompletionRequest
66import com.cjcrafter.openai.completions.CompletionResponse
7+ import com.cjcrafter.openai.completions.CompletionResponseChunk
78import com.cjcrafter.openai.exception.OpenAIError
89import com.cjcrafter.openai.exception.WrappedIOError
910import com.cjcrafter.openai.gson.ChatUserAdapter
@@ -16,6 +17,7 @@ import okhttp3.*
1617import okhttp3.MediaType.Companion.toMediaType
1718import okhttp3.RequestBody.Companion.toRequestBody
1819import java.io.IOException
20+ import java.lang.IllegalStateException
1921import java.util.function.Consumer
2022
2123/* *
@@ -56,22 +58,27 @@ class OpenAI @JvmOverloads constructor(
5658 .post(body).build()
5759 }
5860
61+ /* *
62+ * Create completion
63+ *
64+ * @param request
65+ * @return
66+ * @since 1.3.0
67+ */
5968 @Throws(OpenAIError ::class )
6069 fun createCompletion (request : CompletionRequest ): CompletionResponse {
6170 @Suppress(" DEPRECATION" )
6271 request.stream = false // use streamCompletion for stream=true
6372 val httpRequest = buildRequest(request, " completions" )
6473
65- // Save the JsonObject to check for errors
66- var rootObject: JsonObject ?
6774 try {
6875 client.newCall(httpRequest).execute().use { response ->
6976
7077 // Servers respond to API calls with json blocks. Since raw JSON isn't
7178 // very developer friendly, we wrap for easy data access.
72- rootObject = JsonParser .parseString(response.body!! .string()).asJsonObject
73- if (rootObject!! .has(" error" ))
74- throw OpenAIError .fromJson(rootObject!! .get(" error" ).asJsonObject)
79+ val rootObject = JsonParser .parseString(response.body!! .string()).asJsonObject
80+ if (rootObject.has(" error" ))
81+ throw OpenAIError .fromJson(rootObject.get(" error" ).asJsonObject)
7582
7683 return gson.fromJson(rootObject, CompletionResponse ::class .java)
7784 }
@@ -81,6 +88,78 @@ class OpenAI @JvmOverloads constructor(
8188 }
8289 }
8390
91+ /* *
92+ * Helper method to call [streamCompletion].
93+ *
94+ * @param request The input information for ChatGPT.
95+ * @param onResponse The method to call for each chunk.
96+ * @since 1.3.0
97+ */
98+ fun streamCompletionKotlin (request : CompletionRequest , onResponse : CompletionResponseChunk .() -> Unit ) {
99+ streamCompletion(request, { it.onResponse() })
100+ }
101+
102+ /* *
103+ * This method does not block the thread. Method calls to [onResponse] are
104+ * not handled by the main thread. It is crucial to consider thread safety
105+ * within the context of your program.
106+ *
107+ * @param request The input information for ChatGPT.
108+ * @param onResponse The method to call for each chunk.
109+ * @param onFailure The method to call if the HTTP fails. This method will
110+ * not be called if OpenAI returns an error.
111+ * @see createCompletion
112+ * @see streamCompletionKotlin
113+ * @since 1.3.0
114+ */
115+ @JvmOverloads
116+ fun streamCompletion (
117+ request : CompletionRequest ,
118+ onResponse : Consumer <CompletionResponseChunk >, // use Consumer instead of Kotlin for better Java syntax
119+ onFailure : Consumer <OpenAIError > = Consumer { it.printStackTrace() }
120+ ) {
121+ @Suppress(" DEPRECATION" )
122+ request.stream = true // use requestResponse for stream=false
123+ val httpRequest = buildRequest(request, " completions" )
124+
125+ client.newCall(httpRequest).enqueue(object : Callback {
126+
127+ override fun onFailure (call : Call , e : IOException ) {
128+ onFailure.accept(WrappedIOError (e))
129+ }
130+
131+ override fun onResponse (call : Call , response : Response ) {
132+ response.body?.source()?.use { source ->
133+ while (! source.exhausted()) {
134+
135+ // Parse the JSON string as a map. Every string starts
136+ // with "data: ", so we need to remove that.
137+ var jsonResponse = source.readUtf8Line() ? : continue
138+ if (jsonResponse.isEmpty())
139+ continue
140+
141+ // TODO comment
142+ if (! jsonResponse.startsWith(" data: " )) {
143+ System .err.println (jsonResponse)
144+ continue
145+ }
146+
147+ jsonResponse = jsonResponse.substring(" data: " .length)
148+ if (jsonResponse == " [DONE]" )
149+ continue
150+
151+ val rootObject = JsonParser .parseString(jsonResponse).asJsonObject
152+ if (rootObject.has(" error" ))
153+ throw OpenAIError .fromJson(rootObject.get(" error" ).asJsonObject)
154+
155+ val cache = gson.fromJson(rootObject, CompletionResponseChunk ::class .java)
156+ onResponse.accept(cache)
157+ }
158+ }
159+ }
160+ })
161+ }
162+
84163 /* *
85164 * Blocks the current thread until OpenAI responds to https request. The
86165 * returned value includes information including tokens, generated text,
@@ -97,16 +176,14 @@ class OpenAI @JvmOverloads constructor(
97176 request.stream = false // use streamResponse for stream=true
98177 val httpRequest = buildRequest(request, " chat/completions" )
99178
100- // Save the JsonObject to check for errors
101- var rootObject: JsonObject ?
102179 try {
103180 client.newCall(httpRequest).execute().use { response ->
104181
105182 // Servers respond to API calls with json blocks. Since raw JSON isn't
106183 // very developer friendly, we wrap for easy data access.
107- rootObject = JsonParser .parseString(response.body!! .string()).asJsonObject
108- if (rootObject!! .has(" error" ))
109- throw OpenAIError .fromJson(rootObject!! .get(" error" ).asJsonObject)
184+ val rootObject = JsonParser .parseString(response.body!! .string()).asJsonObject
185+ if (rootObject.has(" error" ))
186+ throw OpenAIError .fromJson(rootObject.get(" error" ).asJsonObject)
110187
111188 return gson.fromJson(rootObject, ChatResponse ::class .java)
112189 }
@@ -176,7 +253,7 @@ class OpenAI @JvmOverloads constructor(
176253 fun streamChatCompletion (
177254 request : ChatRequest ,
178255 onResponse : Consumer <ChatResponseChunk >, // use Consumer instead of Kotlin for better Java syntax
179- onFailure : Consumer <IOException > = Consumer { it.printStackTrace() }
256+ onFailure : Consumer <WrappedIOError > = Consumer { it.printStackTrace() }
180257 ) {
181258 @Suppress(" DEPRECATION" )
182259 request.stream = true // use requestResponse for stream=false
@@ -186,7 +263,7 @@ class OpenAI @JvmOverloads constructor(
186263 var cache: ChatResponseChunk ? = null
187264
188265 override fun onFailure (call : Call , e : IOException ) {
189- onFailure.accept(e )
266+ onFailure.accept(WrappedIOError (e) )
190267 }
191268
192269 override fun onResponse (call : Call , response : Response ) {
@@ -203,6 +280,9 @@ class OpenAI @JvmOverloads constructor(
203280 continue
204281
205282 val rootObject = JsonParser .parseString(jsonResponse).asJsonObject
283+ if (rootObject.has(" error" ))
284+ throw OpenAIError .fromJson(rootObject.get(" error" ).asJsonObject)
285+
206286 if (cache == null )
207287 cache = gson.fromJson(rootObject, ChatResponseChunk ::class .java)
208288 else
0 commit comments