From c62433a7c6e5784db36834ba07b220367d16cbcd Mon Sep 17 00:00:00 2001 From: Collin Date: Sun, 12 Nov 2023 16:27:54 -0500 Subject: [PATCH] add logging and test cases --- .gitignore | 3 +- build.gradle.kts | 4 + examples/build.gradle.kts | 2 + examples/src/main/resources/logback.xml | 13 ++ .../com/cjcrafter/openai/AzureOpenAI.kt | 7 +- .../kotlin/com/cjcrafter/openai/OpenAI.kt | 43 ++++--- .../kotlin/com/cjcrafter/openai/OpenAIDsl.kt | 15 +++ .../kotlin/com/cjcrafter/openai/OpenAIImpl.kt | 9 +- .../cjcrafter/openai/chat/ChatRequestTest.kt | 111 ++++++++++++++++++ .../openai/chat/StreamChatCompletionTest.kt | 88 ++++++++++++++ .../resources/stream_chat_completion_1.txt | 29 +++++ .../resources/stream_chat_completion_2.txt | 35 ++++++ 12 files changed, 334 insertions(+), 25 deletions(-) create mode 100644 examples/src/main/resources/logback.xml create mode 100644 src/main/kotlin/com/cjcrafter/openai/OpenAIDsl.kt create mode 100644 src/test/kotlin/com/cjcrafter/openai/chat/ChatRequestTest.kt create mode 100644 src/test/kotlin/com/cjcrafter/openai/chat/StreamChatCompletionTest.kt create mode 100644 src/test/resources/stream_chat_completion_1.txt create mode 100644 src/test/resources/stream_chat_completion_2.txt diff --git a/.gitignore b/.gitignore index 0a2e07e..6ab6d16 100644 --- a/.gitignore +++ b/.gitignore @@ -29,4 +29,5 @@ log/ target/ # ChatGPT-Java-API Specific Ignore -.env \ No newline at end of file +.env +debug.log \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 3b8df90..3a5e601 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -25,10 +25,14 @@ dependencies { implementation("com.fasterxml.jackson.core:jackson-annotations:2.15.3") implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.15.3") + implementation("org.slf4j:slf4j-api:2.0.9") + implementation("org.jetbrains:annotations:24.0.1") testImplementation("io.github.cdimascio:dotenv-kotlin:6.4.1") testImplementation("org.junit.jupiter:junit-jupiter:5.9.2") + testImplementation("com.squareup.okhttp3:okhttp:4.9.2") + testImplementation("com.squareup.okhttp3:mockwebserver:4.9.2") } kotlin { diff --git a/examples/build.gradle.kts b/examples/build.gradle.kts index 1285176..2720042 100644 --- a/examples/build.gradle.kts +++ b/examples/build.gradle.kts @@ -14,6 +14,8 @@ dependencies { implementation("com.fasterxml.jackson.core:jackson-databind:2.15.3") implementation("io.github.cdimascio:dotenv-kotlin:6.4.1") + implementation("ch.qos.logback:logback-classic:1.4.11") + // https://mvnrepository.com/artifact/org.mariuszgromada.math/MathParser.org-mXparser // Used for tool tests implementation("org.mariuszgromada.math:MathParser.org-mXparser:5.2.1") diff --git a/examples/src/main/resources/logback.xml b/examples/src/main/resources/logback.xml new file mode 100644 index 0000000..d2334ba --- /dev/null +++ b/examples/src/main/resources/logback.xml @@ -0,0 +1,13 @@ + + + debug.log + false + + %date %level [%thread] %logger{10} %msg%n + + + + + + + diff --git a/src/main/kotlin/com/cjcrafter/openai/AzureOpenAI.kt b/src/main/kotlin/com/cjcrafter/openai/AzureOpenAI.kt index ea6e7ad..871c863 100644 --- a/src/main/kotlin/com/cjcrafter/openai/AzureOpenAI.kt +++ b/src/main/kotlin/com/cjcrafter/openai/AzureOpenAI.kt @@ -13,7 +13,6 @@ import org.jetbrains.annotations.ApiStatus * * This class constructs url in the form of: https:///openai/deployments//?api-version= * - * @property azureBaseUrl The base URL for the Azure OpenAI API. Usually https://.openai.azure.com * @property apiVersion The API version to use. Defaults to 2023-03-15-preview. * @property modelName The model name to use. This is the name of the model deployed to Azure. */ @@ -21,16 +20,16 @@ class AzureOpenAI @ApiStatus.Internal constructor( apiKey: String, organization: String? = null, client: OkHttpClient = OkHttpClient(), - private val azureBaseUrl: String = "", + baseUrl: String = "https://api.openai.com", private val apiVersion: String = "2023-03-15-preview", private val modelName: String = "" -) : OpenAIImpl(apiKey, organization, client) { +) : OpenAIImpl(apiKey, organization, client, baseUrl) { override fun buildRequest(request: Any, endpoint: String): Request { val json = objectMapper.writeValueAsString(request) val body: RequestBody = json.toRequestBody(mediaType) return Request.Builder() - .url("$azureBaseUrl/openai/deployments/$modelName/$endpoint?api-version=$apiVersion") + .url("$baseUrl/openai/deployments/$modelName/$endpoint?api-version=$apiVersion") .addHeader("Content-Type", "application/json") .addHeader("api-key", apiKey) .apply { if (organization != null) addHeader("OpenAI-Organization", organization) } diff --git a/src/main/kotlin/com/cjcrafter/openai/OpenAI.kt b/src/main/kotlin/com/cjcrafter/openai/OpenAI.kt index b9efa01..2c55671 100644 --- a/src/main/kotlin/com/cjcrafter/openai/OpenAI.kt +++ b/src/main/kotlin/com/cjcrafter/openai/OpenAI.kt @@ -6,6 +6,7 @@ import com.cjcrafter.openai.completions.CompletionRequest import com.cjcrafter.openai.completions.CompletionResponse import com.cjcrafter.openai.completions.CompletionResponseChunk import com.cjcrafter.openai.util.OpenAIDslMarker +import com.fasterxml.jackson.annotation.JsonAutoDetect import com.fasterxml.jackson.annotation.JsonInclude import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.databind.ObjectMapper @@ -14,6 +15,7 @@ import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import okhttp3.OkHttpClient import org.jetbrains.annotations.ApiStatus import org.jetbrains.annotations.Contract +import org.slf4j.LoggerFactory interface OpenAI { @@ -91,46 +93,49 @@ interface OpenAI { protected var apiKey: String? = null protected var organization: String? = null protected var client: OkHttpClient = OkHttpClient() + protected var baseUrl: String = "https://api.openai.com" fun apiKey(apiKey: String) = apply { this.apiKey = apiKey } fun organization(organization: String?) = apply { this.organization = organization } fun client(client: OkHttpClient) = apply { this.client = client } + fun baseUrl(baseUrl: String) = apply { this.baseUrl = baseUrl } @Contract(pure = true) open fun build(): OpenAI { return OpenAIImpl( - apiKey ?: throw IllegalStateException("apiKey must be defined to use OpenAI"), - organization, - client + apiKey = apiKey ?: throw IllegalStateException("apiKey must be defined to use OpenAI"), + organization = organization, + client = client, + baseUrl = baseUrl, ) } } @OpenAIDslMarker class AzureBuilder internal constructor(): Builder() { - private var azureBaseUrl: String? = null private var apiVersion: String? = null private var modelName: String? = null - fun azureBaseUrl(azureBaseUrl: String) = apply { this.azureBaseUrl = azureBaseUrl } fun apiVersion(apiVersion: String) = apply { this.apiVersion = apiVersion } fun modelName(modelName: String) = apply { this.modelName = modelName } @Contract(pure = true) override fun build(): OpenAI { return AzureOpenAI( - apiKey ?: throw IllegalStateException("apiKey must be defined to use OpenAI"), - organization, - client, - azureBaseUrl ?: throw IllegalStateException("azureBaseUrl must be defined for azure"), - apiVersion ?: throw IllegalStateException("apiVersion must be defined for azure"), - modelName ?: throw IllegalStateException("modelName must be defined for azure") + apiKey = apiKey ?: throw IllegalStateException("apiKey must be defined to use OpenAI"), + organization = organization, + client = client, + baseUrl = if (baseUrl == "https://api.openai.com") throw IllegalStateException("baseUrl must be set to an azure endpoint") else baseUrl, + apiVersion = apiVersion ?: throw IllegalStateException("apiVersion must be defined for azure"), + modelName = modelName ?: throw IllegalStateException("modelName must be defined for azure") ) } } companion object { + internal val logger = LoggerFactory.getLogger(OpenAI::class.java) + /** * Instantiates a builder for a default OpenAI instance. For Azure's * OpenAI, use [azureBuilder] instead. @@ -155,6 +160,14 @@ interface OpenAI { setSerializationInclusion(JsonInclude.Include.NON_NULL) configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + // By default, Jackson can serialize fields AND getters. We just want fields. + setVisibility(serializationConfig.getDefaultVisibilityChecker() + .withFieldVisibility(JsonAutoDetect.Visibility.ANY) + .withGetterVisibility(JsonAutoDetect.Visibility.NONE) + .withSetterVisibility(JsonAutoDetect.Visibility.NONE) + .withCreatorVisibility(JsonAutoDetect.Visibility.NONE) + ) + // Register modules with custom serializers/deserializers val module = SimpleModule().apply { addSerializer(ToolChoice::class.java, ToolChoice.serializer()) @@ -180,10 +193,4 @@ interface OpenAI { consumer(chunk) } } -} - -@Contract(pure = true) -fun openAI(init: OpenAI.Builder.() -> Unit) = OpenAI.builder().apply(init).build() - -@Contract(pure = true) -fun azureOpenAI(init: OpenAI.AzureBuilder.() -> Unit) = OpenAI.azureBuilder().apply(init).build() \ No newline at end of file +} \ No newline at end of file diff --git a/src/main/kotlin/com/cjcrafter/openai/OpenAIDsl.kt b/src/main/kotlin/com/cjcrafter/openai/OpenAIDsl.kt new file mode 100644 index 0000000..dd514d1 --- /dev/null +++ b/src/main/kotlin/com/cjcrafter/openai/OpenAIDsl.kt @@ -0,0 +1,15 @@ +package com.cjcrafter.openai + +import org.jetbrains.annotations.Contract + +/** + * Builds an [OpenAI] instance using the default implementation. + */ +@Contract(pure = true) +fun openAI(init: OpenAI.Builder.() -> Unit) = OpenAI.builder().apply(init).build() + +/** + * Builds an [OpenAI] instance using the Azure implementation. + */ +@Contract(pure = true) +fun azureOpenAI(init: OpenAI.AzureBuilder.() -> Unit) = OpenAI.azureBuilder().apply(init).build() \ No newline at end of file diff --git a/src/main/kotlin/com/cjcrafter/openai/OpenAIImpl.kt b/src/main/kotlin/com/cjcrafter/openai/OpenAIImpl.kt index 11b1f9c..04f0fc7 100644 --- a/src/main/kotlin/com/cjcrafter/openai/OpenAIImpl.kt +++ b/src/main/kotlin/com/cjcrafter/openai/OpenAIImpl.kt @@ -16,7 +16,8 @@ import java.io.IOException open class OpenAIImpl @ApiStatus.Internal constructor( protected val apiKey: String, protected val organization: String? = null, - private val client: OkHttpClient = OkHttpClient() + protected val client: OkHttpClient = OkHttpClient(), + protected val baseUrl: String = "https://api.openai.com", ): OpenAI { protected val mediaType = "application/json; charset=utf-8".toMediaType() protected val objectMapper = OpenAI.createObjectMapper() @@ -25,7 +26,7 @@ open class OpenAIImpl @ApiStatus.Internal constructor( val json = objectMapper.writeValueAsString(request) val body: RequestBody = json.toRequestBody(mediaType) return Request.Builder() - .url("https://api.openai.com/$endpoint") + .url("$baseUrl/$endpoint") .addHeader("Content-Type", "application/json") .addHeader("Authorization", "Bearer $apiKey") .apply { if (organization != null) addHeader("OpenAI-Organization", organization) } @@ -43,6 +44,7 @@ open class OpenAIImpl @ApiStatus.Internal constructor( val jsonReader = httpResponse.body?.byteStream()?.bufferedReader() ?: throw IOException("Response body is null") val responseStr = jsonReader.readText() + OpenAI.logger.debug(responseStr) return objectMapper.readValue(responseStr, responseType) } @@ -72,6 +74,8 @@ open class OpenAIImpl @ApiStatus.Internal constructor( var line: String? do { line = reader.readLine() + OpenAI.logger.debug(line) + if (line == "data: [DONE]") { reader.close() return null @@ -86,6 +90,7 @@ open class OpenAIImpl @ApiStatus.Internal constructor( override fun next(): T { val line = nextLine ?: throw NoSuchElementException("No more lines") + currentResponse = if (currentResponse == null) { objectMapper.readValue(line, responseType) } else { diff --git a/src/test/kotlin/com/cjcrafter/openai/chat/ChatRequestTest.kt b/src/test/kotlin/com/cjcrafter/openai/chat/ChatRequestTest.kt new file mode 100644 index 0000000..a8b3a6f --- /dev/null +++ b/src/test/kotlin/com/cjcrafter/openai/chat/ChatRequestTest.kt @@ -0,0 +1,111 @@ +package com.cjcrafter.openai.chat + +import com.cjcrafter.openai.OpenAI +import com.cjcrafter.openai.chat.ChatMessage.Companion.toSystemMessage +import org.intellij.lang.annotations.Language +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource +import java.util.stream.Stream + +class ChatRequestTest { + + @ParameterizedTest + @MethodSource("provide_serialize") + fun `test deserialize to json`(obj: Any, json: String) { + val objectMapper = OpenAI.createObjectMapper() + val expected = objectMapper.readTree(json) + val actual = objectMapper.readTree(objectMapper.writeValueAsString(obj)) + assertEquals(expected, actual) + } + + @ParameterizedTest + @MethodSource("provide_serialize") + fun `test serialize from json`(expected: Any, json: String) { + val objectMapper = OpenAI.createObjectMapper() + val actual = objectMapper.readValue(json, expected::class.java) + assertEquals(expected, actual) + } + + companion object { + @JvmStatic + fun provide_serialize(): Stream { + return buildList { + + @Language("JSON") + var json = """ + { + "messages": [ + { + "role": "system", + "content": "Be as helpful as possible" + } + ], + "model": "gpt-3.5-turbo" + } + """.trimIndent() + add(Arguments.of( + ChatRequest.builder() + .model("gpt-3.5-turbo") + .messages(mutableListOf("Be as helpful as possible".toSystemMessage())) + .build(), + json + )) + + json = """ + { + "messages": [ + { + "role": "system", + "content": "Be as helpful as possible" + }, + { + "role": "user", + "content": "What is 2 + 2?" + } + ], + "model": "gpt-3.5-turbo", + "tools": [ + { + "type": "function", + "function": { + "name": "solve_math_problem", + "parameters": { + "type": "object", + "properties": { + "equation": { + "type": "string", + "description": "The math problem for you to solve" + } + }, + "required": [ + "equation" + ] + }, + "description": "Returns the result of a math problem as a double" + } + } + ] + } + """.trimIndent() + add(Arguments.of( + chatRequest { + model("gpt-3.5-turbo") + messages(mutableListOf( + ChatMessage(ChatUser.SYSTEM, "Be as helpful as possible"), + ChatMessage(ChatUser.USER, "What is 2 + 2?") + )) + function { + name("solve_math_problem") + description("Returns the result of a math problem as a double") + addStringParameter("equation", "The math problem for you to solve", true) + } + }, + json + )) + + }.stream() + } + } +} diff --git a/src/test/kotlin/com/cjcrafter/openai/chat/StreamChatCompletionTest.kt b/src/test/kotlin/com/cjcrafter/openai/chat/StreamChatCompletionTest.kt new file mode 100644 index 0000000..a625324 --- /dev/null +++ b/src/test/kotlin/com/cjcrafter/openai/chat/StreamChatCompletionTest.kt @@ -0,0 +1,88 @@ +package com.cjcrafter.openai.chat + +import com.cjcrafter.openai.chat.ChatMessage.Companion.toSystemMessage +import com.cjcrafter.openai.chat.tool.ToolType +import com.cjcrafter.openai.openAI +import okhttp3.OkHttpClient +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test + +class StreamChatCompletionTest { + + private val mockWebServer = MockWebServer() + private lateinit var client: OkHttpClient + + @BeforeEach + fun setUp() { + mockWebServer.start() + client = OkHttpClient.Builder().build() + } + + @AfterEach + fun tearDown() { + mockWebServer.shutdown() + } + + @Test + fun `test stream`() { + + // Create a mock webserver with our 2 responses (see stream_chat_completion_1.txt and 2) + mockWebServer.enqueue(MockResponse().setBody(readResource("stream_chat_completion_1.txt"))) + mockWebServer.enqueue(MockResponse().setBody(readResource("stream_chat_completion_2.txt"))) + + // Create a new OpenAI instance with our mock webserver + val openai = openAI { + apiKey("sk-123456789") + client(client) + baseUrl(mockWebServer.url("/").toString()) + } + + val dummyRequest = chatRequest { + model("gpt-3.5-turbo") + addMessage("Help the user".toSystemMessage()) // this doesn't matter since we are using a mock webserver + + function { + name("solve_math_problem") + description("Does math") + addStringParameter("equation", "The equation to evaluate", true) + } + } + + // This first stream will return a tool call. We will + lateinit var toolMessage: ChatMessage + for (chunk in openai.streamChatCompletion(dummyRequest)) { + if (chunk[0].isFinished()) + toolMessage = chunk[0].message + } + + // If this were a call to the actual api.openai.com endpoint, we would + // be *required* to respond to the assistant tool request with a tool + // message. Since we are using a mock webserver, we can skip that. + + // This second stream will return a message + lateinit var message: ChatMessage + for (chunk in openai.streamChatCompletion(dummyRequest)) { + if (chunk[0].isFinished()) + message = chunk[0].message + } + + // Assertions + assertEquals(ChatUser.ASSISTANT, toolMessage.role, "Tool call should be from the assistant") + assertEquals(ToolType.FUNCTION, toolMessage.toolCalls?.get(0)?.type, "Tool call should be a function") + assertEquals("solve_math_problem", toolMessage.toolCalls?.get(0)?.function?.name) + assertEquals("3/2", toolMessage.toolCalls?.get(0)?.function?.tryParseArguments()?.get("equation")?.asText()) + + assertEquals(ChatUser.ASSISTANT, message.role, "Message should be from the assistant") + assertEquals("The result of 3 divided by 2 is 1.5.", message.content) + assertEquals(null, message.toolCalls) + assertEquals(null, message.toolCallId) + } + + private fun readResource(resource: String): String { + return this::class.java.classLoader.getResource(resource)?.readText() ?: throw Exception("Resource '$resource' not found") + } +} \ No newline at end of file diff --git a/src/test/resources/stream_chat_completion_1.txt b/src/test/resources/stream_chat_completion_1.txt new file mode 100644 index 0000000..29155a9 --- /dev/null +++ b/src/test/resources/stream_chat_completion_1.txt @@ -0,0 +1,29 @@ +data: {"id":"chatcmpl-8JvTuU5JnuiIPTIhTSnKniBR124IN","object":"chat.completion.chunk","created":1699759882,"model":"gpt-3.5-turbo-0613","choices":[{"index":0,"delta":{"role":"assistant","content":null,"tool_calls":[{"index":0,"id":"call_LGmanlgIVbXEWlujxNisRROy","type":"function","function":{"name":"solve_math_problem","arguments":""}}]},"finish_reason":null}]} + +data: {"id":"chatcmpl-8JvTuU5JnuiIPTIhTSnKniBR124IN","object":"chat.completion.chunk","created":1699759882,"model":"gpt-3.5-turbo-0613","choices":[{"index":0,"delta":{"tool_calls":[{"function":{"arguments":"{\n"},"index":0}]},"finish_reason":null}]} + +data: {"id":"chatcmpl-8JvTuU5JnuiIPTIhTSnKniBR124IN","object":"chat.completion.chunk","created":1699759882,"model":"gpt-3.5-turbo-0613","choices":[{"index":0,"delta":{"tool_calls":[{"function":{"arguments":" "},"index":0}]},"finish_reason":null}]} + +data: {"id":"chatcmpl-8JvTuU5JnuiIPTIhTSnKniBR124IN","object":"chat.completion.chunk","created":1699759882,"model":"gpt-3.5-turbo-0613","choices":[{"index":0,"delta":{"tool_calls":[{"function":{"arguments":" \""},"index":0}]},"finish_reason":null}]} + +data: {"id":"chatcmpl-8JvTuU5JnuiIPTIhTSnKniBR124IN","object":"chat.completion.chunk","created":1699759882,"model":"gpt-3.5-turbo-0613","choices":[{"index":0,"delta":{"tool_calls":[{"function":{"arguments":"equ"},"index":0}]},"finish_reason":null}]} + +data: {"id":"chatcmpl-8JvTuU5JnuiIPTIhTSnKniBR124IN","object":"chat.completion.chunk","created":1699759882,"model":"gpt-3.5-turbo-0613","choices":[{"index":0,"delta":{"tool_calls":[{"function":{"arguments":"ation"},"index":0}]},"finish_reason":null}]} + +data: {"id":"chatcmpl-8JvTuU5JnuiIPTIhTSnKniBR124IN","object":"chat.completion.chunk","created":1699759882,"model":"gpt-3.5-turbo-0613","choices":[{"index":0,"delta":{"tool_calls":[{"function":{"arguments":"\":"},"index":0}]},"finish_reason":null}]} + +data: {"id":"chatcmpl-8JvTuU5JnuiIPTIhTSnKniBR124IN","object":"chat.completion.chunk","created":1699759882,"model":"gpt-3.5-turbo-0613","choices":[{"index":0,"delta":{"tool_calls":[{"function":{"arguments":" \""},"index":0}]},"finish_reason":null}]} + +data: {"id":"chatcmpl-8JvTuU5JnuiIPTIhTSnKniBR124IN","object":"chat.completion.chunk","created":1699759882,"model":"gpt-3.5-turbo-0613","choices":[{"index":0,"delta":{"tool_calls":[{"function":{"arguments":"3"},"index":0}]},"finish_reason":null}]} + +data: {"id":"chatcmpl-8JvTuU5JnuiIPTIhTSnKniBR124IN","object":"chat.completion.chunk","created":1699759882,"model":"gpt-3.5-turbo-0613","choices":[{"index":0,"delta":{"tool_calls":[{"function":{"arguments":"/"},"index":0}]},"finish_reason":null}]} + +data: {"id":"chatcmpl-8JvTuU5JnuiIPTIhTSnKniBR124IN","object":"chat.completion.chunk","created":1699759882,"model":"gpt-3.5-turbo-0613","choices":[{"index":0,"delta":{"tool_calls":[{"function":{"arguments":"2"},"index":0}]},"finish_reason":null}]} + +data: {"id":"chatcmpl-8JvTuU5JnuiIPTIhTSnKniBR124IN","object":"chat.completion.chunk","created":1699759882,"model":"gpt-3.5-turbo-0613","choices":[{"index":0,"delta":{"tool_calls":[{"function":{"arguments":"\"\n"},"index":0}]},"finish_reason":null}]} + +data: {"id":"chatcmpl-8JvTuU5JnuiIPTIhTSnKniBR124IN","object":"chat.completion.chunk","created":1699759882,"model":"gpt-3.5-turbo-0613","choices":[{"index":0,"delta":{"tool_calls":[{"function":{"arguments":"}"},"index":0}]},"finish_reason":null}]} + +data: {"id":"chatcmpl-8JvTuU5JnuiIPTIhTSnKniBR124IN","object":"chat.completion.chunk","created":1699759882,"model":"gpt-3.5-turbo-0613","choices":[{"index":0,"delta":{},"finish_reason":"tool_calls"}]} + +data: [DONE] \ No newline at end of file diff --git a/src/test/resources/stream_chat_completion_2.txt b/src/test/resources/stream_chat_completion_2.txt new file mode 100644 index 0000000..d883a0e --- /dev/null +++ b/src/test/resources/stream_chat_completion_2.txt @@ -0,0 +1,35 @@ +data: {"id":"chatcmpl-8JvTvURfF94EqLJJyViH4g7xGC62w","object":"chat.completion.chunk","created":1699759883,"model":"gpt-3.5-turbo-0613","choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null}]} + +data: {"id":"chatcmpl-8JvTvURfF94EqLJJyViH4g7xGC62w","object":"chat.completion.chunk","created":1699759883,"model":"gpt-3.5-turbo-0613","choices":[{"index":0,"delta":{"content":"The"},"finish_reason":null}]} + +data: {"id":"chatcmpl-8JvTvURfF94EqLJJyViH4g7xGC62w","object":"chat.completion.chunk","created":1699759883,"model":"gpt-3.5-turbo-0613","choices":[{"index":0,"delta":{"content":" result"},"finish_reason":null}]} + +data: {"id":"chatcmpl-8JvTvURfF94EqLJJyViH4g7xGC62w","object":"chat.completion.chunk","created":1699759883,"model":"gpt-3.5-turbo-0613","choices":[{"index":0,"delta":{"content":" of"},"finish_reason":null}]} + +data: {"id":"chatcmpl-8JvTvURfF94EqLJJyViH4g7xGC62w","object":"chat.completion.chunk","created":1699759883,"model":"gpt-3.5-turbo-0613","choices":[{"index":0,"delta":{"content":" "},"finish_reason":null}]} + +data: {"id":"chatcmpl-8JvTvURfF94EqLJJyViH4g7xGC62w","object":"chat.completion.chunk","created":1699759883,"model":"gpt-3.5-turbo-0613","choices":[{"index":0,"delta":{"content":"3"},"finish_reason":null}]} + +data: {"id":"chatcmpl-8JvTvURfF94EqLJJyViH4g7xGC62w","object":"chat.completion.chunk","created":1699759883,"model":"gpt-3.5-turbo-0613","choices":[{"index":0,"delta":{"content":" divided"},"finish_reason":null}]} + +data: {"id":"chatcmpl-8JvTvURfF94EqLJJyViH4g7xGC62w","object":"chat.completion.chunk","created":1699759883,"model":"gpt-3.5-turbo-0613","choices":[{"index":0,"delta":{"content":" by"},"finish_reason":null}]} + +data: {"id":"chatcmpl-8JvTvURfF94EqLJJyViH4g7xGC62w","object":"chat.completion.chunk","created":1699759883,"model":"gpt-3.5-turbo-0613","choices":[{"index":0,"delta":{"content":" "},"finish_reason":null}]} + +data: {"id":"chatcmpl-8JvTvURfF94EqLJJyViH4g7xGC62w","object":"chat.completion.chunk","created":1699759883,"model":"gpt-3.5-turbo-0613","choices":[{"index":0,"delta":{"content":"2"},"finish_reason":null}]} + +data: {"id":"chatcmpl-8JvTvURfF94EqLJJyViH4g7xGC62w","object":"chat.completion.chunk","created":1699759883,"model":"gpt-3.5-turbo-0613","choices":[{"index":0,"delta":{"content":" is"},"finish_reason":null}]} + +data: {"id":"chatcmpl-8JvTvURfF94EqLJJyViH4g7xGC62w","object":"chat.completion.chunk","created":1699759883,"model":"gpt-3.5-turbo-0613","choices":[{"index":0,"delta":{"content":" "},"finish_reason":null}]} + +data: {"id":"chatcmpl-8JvTvURfF94EqLJJyViH4g7xGC62w","object":"chat.completion.chunk","created":1699759883,"model":"gpt-3.5-turbo-0613","choices":[{"index":0,"delta":{"content":"1"},"finish_reason":null}]} + +data: {"id":"chatcmpl-8JvTvURfF94EqLJJyViH4g7xGC62w","object":"chat.completion.chunk","created":1699759883,"model":"gpt-3.5-turbo-0613","choices":[{"index":0,"delta":{"content":"."},"finish_reason":null}]} + +data: {"id":"chatcmpl-8JvTvURfF94EqLJJyViH4g7xGC62w","object":"chat.completion.chunk","created":1699759883,"model":"gpt-3.5-turbo-0613","choices":[{"index":0,"delta":{"content":"5"},"finish_reason":null}]} + +data: {"id":"chatcmpl-8JvTvURfF94EqLJJyViH4g7xGC62w","object":"chat.completion.chunk","created":1699759883,"model":"gpt-3.5-turbo-0613","choices":[{"index":0,"delta":{"content":"."},"finish_reason":null}]} + +data: {"id":"chatcmpl-8JvTvURfF94EqLJJyViH4g7xGC62w","object":"chat.completion.chunk","created":1699759883,"model":"gpt-3.5-turbo-0613","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]} + +data: [DONE]