diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 3d2ac0bdd..10f309169 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.1.0" + ".": "0.2.0" } \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index e935b5d03..393c909c5 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 24 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/openai-2f8ca92b9b1879fd535b685e4767338413fcd533d42f3baac13a9c41da3fce35.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/openai-fb9db2d2c1f0d6b39d8ee042db5d5c59acba6ad1daf47c18792c1f5fb24b3401.yml diff --git a/CHANGELOG.md b/CHANGELOG.md index b8a61dfee..28e24b76e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,30 @@ # Changelog +## 0.2.0 (2024-11-20) + +Full Changelog: [v0.1.0...v0.2.0](https://github.com/openai/openai-java/compare/v0.1.0...v0.2.0) + +### Features + +* **client:** add async streaming methods ([#6](https://github.com/openai/openai-java/issues/6)) ([f805972](https://github.com/openai/openai-java/commit/f805972d9cafdf3ad8a974e660c62587e2b65c06)) +* initial commit ([fa016ee](https://github.com/openai/openai-java/commit/fa016ee58dba10add81cefddbdf1483bfa24d058)) + + +### Performance Improvements + +* **tests:** remove unused dependencies ([#3](https://github.com/openai/openai-java/issues/3)) ([4c94984](https://github.com/openai/openai-java/commit/4c949841e4eed67b82bfe1f40370a0a3db8f2d43)) + + +### Chores + +* **deps:** bump jackson to 2.18.1 ([#7](https://github.com/openai/openai-java/issues/7)) ([9262ca7](https://github.com/openai/openai-java/commit/9262ca7a55c2bdb3dff69f9df328376e23bb11df)) +* **internal:** spec update ([#5](https://github.com/openai/openai-java/issues/5)) ([0df36a4](https://github.com/openai/openai-java/commit/0df36a497f88407e35ca79bacb72f30f2d1350da)) + + +### Documentation + +* bump models in example snippets to gpt-4o ([#4](https://github.com/openai/openai-java/issues/4)) ([359c100](https://github.com/openai/openai-java/commit/359c10065f9615a73e30573b1dea80d5027288a6)) + ## 0.1.0 (2024-11-08) Full Changelog: [v0.0.1...v0.1.0](https://github.com/openai/openai-java/compare/v0.0.1...v0.1.0) diff --git a/README.md b/README.md index aa6f44d28..033bbea25 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ -[![Maven Central](https://img.shields.io/maven-central/v/com.openai/openai-java)](https://central.sonatype.com/artifact/com.openai/openai-java/0.1.0) +[![Maven Central](https://img.shields.io/maven-central/v/com.openai/openai-java)](https://central.sonatype.com/artifact/com.openai/openai-java/0.2.0) @@ -25,7 +25,7 @@ The REST API documentation can be foundĀ on [platform.openai.com](https://platfo ```kotlin -implementation("com.openai:openai-java:0.1.0") +implementation("com.openai:openai-java:0.2.0") ``` #### Maven @@ -34,7 +34,7 @@ implementation("com.openai:openai-java:0.1.0") com.openai openai-java - 0.1.0 + 0.2.0 ``` @@ -86,7 +86,7 @@ import com.openai.models.ChatCompletionCreateParams; import java.util.List; ChatCompletionCreateParams params = ChatCompletionCreateParams.builder() - .model("gpt-3.5-turbo") + .model("gpt-4o") .build(); ChatCompletion chatCompletion = client.chat().completions().create(params); ``` diff --git a/build.gradle.kts b/build.gradle.kts index 7bb01daf0..e24561d6f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -4,7 +4,7 @@ plugins { allprojects { group = "com.openai" - version = "0.1.0" // x-release-please-version + version = "0.2.0" // x-release-please-version } nexusPublishing { diff --git a/openai-java-client-okhttp/build.gradle.kts b/openai-java-client-okhttp/build.gradle.kts index 8b802481c..d979cf9a2 100644 --- a/openai-java-client-okhttp/build.gradle.kts +++ b/openai-java-client-okhttp/build.gradle.kts @@ -10,5 +10,4 @@ dependencies { testImplementation(kotlin("test")) testImplementation("org.assertj:assertj-core:3.25.3") - testImplementation("org.slf4j:slf4j-simple:2.0.12") } diff --git a/openai-java-client-okhttp/src/main/kotlin/com/openai/client/okhttp/OpenAIOkHttpClient.kt b/openai-java-client-okhttp/src/main/kotlin/com/openai/client/okhttp/OpenAIOkHttpClient.kt index ecb4e82f1..c431be866 100644 --- a/openai-java-client-okhttp/src/main/kotlin/com/openai/client/okhttp/OpenAIOkHttpClient.kt +++ b/openai-java-client-okhttp/src/main/kotlin/com/openai/client/okhttp/OpenAIOkHttpClient.kt @@ -3,13 +3,11 @@ package com.openai.client.okhttp import com.fasterxml.jackson.databind.json.JsonMapper -import com.openai.azure.AzureOpenAIServiceVersion import com.openai.client.OpenAIClient import com.openai.client.OpenAIClientImpl import com.openai.core.ClientOptions import com.openai.core.http.Headers import com.openai.core.http.QueryParams -import com.openai.credential.Credential import java.net.Proxy import java.time.Clock import java.time.Duration @@ -132,12 +130,6 @@ class OpenAIOkHttpClient private constructor() { fun apiKey(apiKey: String) = apply { clientOptions.apiKey(apiKey) } - fun credential(credential: Credential) = apply { clientOptions.credential(credential) } - - fun azureServiceVersion(azureServiceVersion: AzureOpenAIServiceVersion) = apply { - clientOptions.azureServiceVersion(azureServiceVersion) - } - fun organization(organization: String?) = apply { clientOptions.organization(organization) } fun project(project: String?) = apply { clientOptions.project(project) } diff --git a/openai-java-client-okhttp/src/main/kotlin/com/openai/client/okhttp/OpenAIOkHttpClientAsync.kt b/openai-java-client-okhttp/src/main/kotlin/com/openai/client/okhttp/OpenAIOkHttpClientAsync.kt index cf4711aa1..9e8e37c38 100644 --- a/openai-java-client-okhttp/src/main/kotlin/com/openai/client/okhttp/OpenAIOkHttpClientAsync.kt +++ b/openai-java-client-okhttp/src/main/kotlin/com/openai/client/okhttp/OpenAIOkHttpClientAsync.kt @@ -3,13 +3,11 @@ package com.openai.client.okhttp import com.fasterxml.jackson.databind.json.JsonMapper -import com.openai.azure.AzureOpenAIServiceVersion import com.openai.client.OpenAIClientAsync import com.openai.client.OpenAIClientAsyncImpl import com.openai.core.ClientOptions import com.openai.core.http.Headers import com.openai.core.http.QueryParams -import com.openai.credential.Credential import java.net.Proxy import java.time.Clock import java.time.Duration @@ -132,12 +130,6 @@ class OpenAIOkHttpClientAsync private constructor() { fun apiKey(apiKey: String) = apply { clientOptions.apiKey(apiKey) } - fun credential(credential: Credential) = apply { clientOptions.credential(credential) } - - fun azureServiceVersion(azureServiceVersion: AzureOpenAIServiceVersion) = apply { - clientOptions.azureServiceVersion(azureServiceVersion) - } - fun organization(organization: String?) = apply { clientOptions.organization(organization) } fun project(project: String?) = apply { clientOptions.project(project) } diff --git a/openai-java-core/build.gradle.kts b/openai-java-core/build.gradle.kts index 55c1bf0e5..256280c02 100644 --- a/openai-java-core/build.gradle.kts +++ b/openai-java-core/build.gradle.kts @@ -4,14 +4,14 @@ plugins { } dependencies { - api("com.fasterxml.jackson.core:jackson-core:2.14.3") - api("com.fasterxml.jackson.core:jackson-databind:2.14.3") + api("com.fasterxml.jackson.core:jackson-core:2.18.1") + api("com.fasterxml.jackson.core:jackson-databind:2.18.1") api("com.google.errorprone:error_prone_annotations:2.33.0") - implementation("com.fasterxml.jackson.core:jackson-annotations:2.14.3") - implementation("com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.14.3") - implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.14.3") - implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.14.3") + implementation("com.fasterxml.jackson.core:jackson-annotations:2.18.1") + implementation("com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.18.1") + implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.18.1") + implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.18.1") implementation("org.apache.httpcomponents.core5:httpcore5:5.2.4") implementation("org.apache.httpcomponents.client5:httpclient5:5.3.1") @@ -19,8 +19,9 @@ dependencies { testImplementation(project(":openai-java-client-okhttp")) testImplementation("com.github.tomakehurst:wiremock-jre8:2.35.2") testImplementation("org.assertj:assertj-core:3.25.3") - testImplementation("org.assertj:assertj-guava:3.25.3") - testImplementation("org.slf4j:slf4j-simple:2.0.12") testImplementation("org.junit.jupiter:junit-jupiter-api:5.9.3") testImplementation("org.junit.jupiter:junit-jupiter-params:5.9.3") + testImplementation("org.mockito:mockito-core:5.14.2") + testImplementation("org.mockito:mockito-junit-jupiter:5.14.2") + testImplementation("org.mockito.kotlin:mockito-kotlin:4.1.0") } diff --git a/openai-java-core/src/main/kotlin/com/openai/azure/AzureOpenAIServiceVersion.kt b/openai-java-core/src/main/kotlin/com/openai/azure/AzureOpenAIServiceVersion.kt deleted file mode 100644 index 1188d2fbd..000000000 --- a/openai-java-core/src/main/kotlin/com/openai/azure/AzureOpenAIServiceVersion.kt +++ /dev/null @@ -1,36 +0,0 @@ -package com.openai.azure - -import java.util.concurrent.ConcurrentHashMap - -class AzureOpenAIServiceVersion private constructor(@get:JvmName("value") val value: String) { - - companion object { - private val values: ConcurrentHashMap = - ConcurrentHashMap() - - @JvmStatic - fun fromString(version: String): AzureOpenAIServiceVersion = - values.computeIfAbsent(version) { AzureOpenAIServiceVersion(version) } - - @JvmStatic val V2022_12_01 = fromString("2022-12-01") - @JvmStatic val V2023_05_15 = fromString("2023-05-15") - @JvmStatic val V2024_02_01 = fromString("2024-02-01") - @JvmStatic val V2024_06_01 = fromString("2024-06-01") - @JvmStatic val V2023_06_01_PREVIEW = fromString("2023-06-01-preview") - @JvmStatic val V2023_07_01_PREVIEW = fromString("2023-07-01-preview") - @JvmStatic val V2024_02_15_PREVIEW = fromString("2024-02-15-preview") - @JvmStatic val V2024_03_01_PREVIEW = fromString("2024-03-01-preview") - @JvmStatic val V2024_04_01_PREVIEW = fromString("2024-04-01-preview") - @JvmStatic val V2024_05_01_PREVIEW = fromString("2024-05-01-preview") - @JvmStatic val V2024_07_01_PREVIEW = fromString("2024-07-01-preview") - @JvmStatic val V2024_08_01_PREVIEW = fromString("2024-08-01-preview") - @JvmStatic val V2024_09_01_PREVIEW = fromString("2024-09-01-preview") - } - - override fun equals(other: Any?): Boolean = - this === other || (other is AzureOpenAIServiceVersion && value == other.value) - - override fun hashCode(): Int = value.hashCode() - - override fun toString(): String = "AzureOpenAIServiceVersion{value=$value}" -} diff --git a/openai-java-core/src/main/kotlin/com/openai/azure/credential/AzureApiKeyCredential.kt b/openai-java-core/src/main/kotlin/com/openai/azure/credential/AzureApiKeyCredential.kt deleted file mode 100644 index 3614e3354..000000000 --- a/openai-java-core/src/main/kotlin/com/openai/azure/credential/AzureApiKeyCredential.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.openai.azure.credential - -import com.openai.credential.Credential - -/** A credential that provides an Azure API key. */ -class AzureApiKeyCredential private constructor(private var apiKey: String) : Credential { - - init { - validateApiKey(apiKey) - } - - companion object { - @JvmStatic fun create(apiKey: String): Credential = AzureApiKeyCredential(apiKey) - - private fun validateApiKey(apiKey: String) { - require(apiKey.isNotEmpty()) { "Azure API key cannot be empty." } - } - } - - fun apiKey(): String = apiKey - - fun update(apiKey: String) = apply { - validateApiKey(apiKey) - this.apiKey = apiKey - } -} diff --git a/openai-java-core/src/main/kotlin/com/openai/core/ClientOptions.kt b/openai-java-core/src/main/kotlin/com/openai/core/ClientOptions.kt index 1dc84bb7d..20ab4e7f7 100644 --- a/openai-java-core/src/main/kotlin/com/openai/core/ClientOptions.kt +++ b/openai-java-core/src/main/kotlin/com/openai/core/ClientOptions.kt @@ -3,30 +3,30 @@ package com.openai.core import com.fasterxml.jackson.databind.json.JsonMapper -import com.openai.azure.AzureOpenAIServiceVersion -import com.openai.azure.AzureOpenAIServiceVersion.Companion.V2024_06_01 -import com.openai.azure.credential.AzureApiKeyCredential import com.openai.core.http.Headers import com.openai.core.http.HttpClient import com.openai.core.http.PhantomReachableClosingHttpClient import com.openai.core.http.QueryParams import com.openai.core.http.RetryingHttpClient -import com.openai.credential.BearerTokenCredential -import com.openai.credential.Credential import java.time.Clock +import java.util.concurrent.Executor +import java.util.concurrent.Executors +import java.util.concurrent.ThreadFactory +import java.util.concurrent.atomic.AtomicLong class ClientOptions private constructor( private val originalHttpClient: HttpClient, @get:JvmName("httpClient") val httpClient: HttpClient, @get:JvmName("jsonMapper") val jsonMapper: JsonMapper, + @get:JvmName("streamHandlerExecutor") val streamHandlerExecutor: Executor, @get:JvmName("clock") val clock: Clock, @get:JvmName("baseUrl") val baseUrl: String, @get:JvmName("headers") val headers: Headers, @get:JvmName("queryParams") val queryParams: QueryParams, @get:JvmName("responseValidation") val responseValidation: Boolean, @get:JvmName("maxRetries") val maxRetries: Int, - @get:JvmName("credential") val credential: Credential, + @get:JvmName("apiKey") val apiKey: String, @get:JvmName("organization") val organization: String?, @get:JvmName("project") val project: String?, ) { @@ -46,14 +46,14 @@ private constructor( private var httpClient: HttpClient? = null private var jsonMapper: JsonMapper = jsonMapper() + private var streamHandlerExecutor: Executor? = null private var clock: Clock = Clock.systemUTC() private var baseUrl: String = PRODUCTION_URL private var headers: Headers.Builder = Headers.builder() private var queryParams: QueryParams.Builder = QueryParams.builder() private var responseValidation: Boolean = false private var maxRetries: Int = 2 - private var credential: Credential? = null - private var azureServiceVersion: AzureOpenAIServiceVersion? = null + private var apiKey: String? = null private var organization: String? = null private var project: String? = null @@ -61,13 +61,14 @@ private constructor( internal fun from(clientOptions: ClientOptions) = apply { httpClient = clientOptions.originalHttpClient jsonMapper = clientOptions.jsonMapper + streamHandlerExecutor = clientOptions.streamHandlerExecutor clock = clientOptions.clock baseUrl = clientOptions.baseUrl headers = clientOptions.headers.toBuilder() queryParams = clientOptions.queryParams.toBuilder() responseValidation = clientOptions.responseValidation maxRetries = clientOptions.maxRetries - credential = clientOptions.credential + apiKey = clientOptions.apiKey organization = clientOptions.organization project = clientOptions.project } @@ -76,6 +77,10 @@ private constructor( fun jsonMapper(jsonMapper: JsonMapper) = apply { this.jsonMapper = jsonMapper } + fun streamHandlerExecutor(streamHandlerExecutor: Executor) = apply { + this.streamHandlerExecutor = streamHandlerExecutor + } + fun clock(clock: Clock) = apply { this.clock = clock } fun baseUrl(baseUrl: String) = apply { this.baseUrl = baseUrl } @@ -166,56 +171,21 @@ private constructor( fun maxRetries(maxRetries: Int) = apply { this.maxRetries = maxRetries } - fun apiKey(apiKey: String) = apply { - this.credential = BearerTokenCredential.create(apiKey) - } - - fun credential(credential: Credential) = apply { this.credential = credential } - - fun azureServiceVersion(azureServiceVersion: AzureOpenAIServiceVersion) = apply { - this.azureServiceVersion = azureServiceVersion - } + fun apiKey(apiKey: String) = apply { this.apiKey = apiKey } fun organization(organization: String?) = apply { this.organization = organization } fun project(project: String?) = apply { this.project = project } fun fromEnv() = apply { - val openAIKey = System.getenv("OPENAI_API_KEY") - val openAIOrgId = System.getenv("OPENAI_ORG_ID") - val openAIProjectId = System.getenv("OPENAI_PROJECT_ID") - val azureOpenAIKey = System.getenv("AZURE_OPENAI_KEY") - val azureEndpoint = System.getenv("AZURE_OPENAI_ENDPOINT") - - when { - !openAIKey.isNullOrEmpty() && !azureOpenAIKey.isNullOrEmpty() -> { - throw IllegalArgumentException( - "Both OpenAI and Azure OpenAI API keys, `OPENAI_API_KEY` and `AZURE_OPENAI_KEY`, are set. Please specify only one" - ) - } - !openAIKey.isNullOrEmpty() -> { - credential(BearerTokenCredential.create(openAIKey)) - organization(openAIOrgId) - project(openAIProjectId) - } - !azureOpenAIKey.isNullOrEmpty() -> { - credential(AzureApiKeyCredential.create(azureOpenAIKey)) - baseUrl(azureEndpoint) - } - !azureEndpoint.isNullOrEmpty() -> { - // Both 'openAIKey' and 'azureOpenAIKey' are not set. - // Only 'azureEndpoint' is set here, and user still needs to call method - // '.credential(BearerTokenCredential(Supplier))' - // to get the token through the supplier, which requires Azure Entra ID as a - // dependency. - baseUrl(azureEndpoint) - } - } + System.getenv("OPENAI_API_KEY")?.let { apiKey(it) } + System.getenv("OPENAI_ORG_ID")?.let { organization(it) } + System.getenv("OPENAI_PROJECT_ID")?.let { project(it) } } fun build(): ClientOptions { checkNotNull(httpClient) { "`httpClient` is required but was not set" } - checkNotNull(credential) { "`credential` is required but was not set" } + checkNotNull(apiKey) { "`apiKey` is required but was not set" } val headers = Headers.builder() val queryParams = QueryParams.builder() @@ -228,26 +198,11 @@ private constructor( headers.put("X-Stainless-Runtime-Version", getJavaVersion()) organization?.let { headers.put("OpenAI-Organization", it) } project?.let { headers.put("OpenAI-Project", it) } - - when (val currentCredential = credential) { - is AzureApiKeyCredential -> { - headers.put("api-key", currentCredential.apiKey()) - } - is BearerTokenCredential -> { - headers.put("Authorization", "Bearer ${currentCredential.token()}") + apiKey?.let { + if (!it.isEmpty()) { + headers.put("Authorization", "Bearer $it") } - else -> { - throw IllegalArgumentException("Invalid credential type") - } - } - - if (isAzureEndpoint(baseUrl)) { - // Default Azure OpenAI version is used if Azure user doesn't - // specific a service API version in 'queryParams'. - // We can update the default value every major announcement if needed. - replaceQueryParams("api-version", (azureServiceVersion ?: V2024_06_01).value) } - headers.replaceAll(this.headers.build()) queryParams.replaceAll(this.queryParams.build()) @@ -261,13 +216,28 @@ private constructor( .build() ), jsonMapper, + streamHandlerExecutor + ?: Executors.newCachedThreadPool( + object : ThreadFactory { + + private val threadFactory: ThreadFactory = + Executors.defaultThreadFactory() + private val count = AtomicLong(0) + + override fun newThread(runnable: Runnable): Thread = + threadFactory.newThread(runnable).also { + it.name = + "openai-stream-handler-thread-${count.getAndIncrement()}" + } + } + ), clock, baseUrl, headers.build(), queryParams.build(), responseValidation, maxRetries, - credential!!, + apiKey!!, organization, project, ) diff --git a/openai-java-core/src/main/kotlin/com/openai/core/PhantomReachable.kt b/openai-java-core/src/main/kotlin/com/openai/core/PhantomReachable.kt index 75dbd2d95..a1662c173 100644 --- a/openai-java-core/src/main/kotlin/com/openai/core/PhantomReachable.kt +++ b/openai-java-core/src/main/kotlin/com/openai/core/PhantomReachable.kt @@ -15,10 +15,20 @@ internal fun closeWhenPhantomReachable(observed: Any, closeable: AutoCloseable) check(observed !== closeable) { "`observed` cannot be the same object as `closeable` because it would never become phantom reachable" } - closeWhenPhantomReachable?.let { it(observed, closeable::close) } + closeWhenPhantomReachable(observed, closeable::close) } -private val closeWhenPhantomReachable: ((Any, AutoCloseable) -> Unit)? by lazy { +/** + * Calls [close] when [observed] becomes only phantom reachable. + * + * This is a wrapper around a Java 9+ [java.lang.ref.Cleaner], or a no-op in older Java versions. + */ +@JvmSynthetic +internal fun closeWhenPhantomReachable(observed: Any, close: () -> Unit) { + closeWhenPhantomReachable?.let { it(observed, close) } +} + +private val closeWhenPhantomReachable: ((Any, () -> Unit) -> Unit)? by lazy { try { val cleanerClass = Class.forName("java.lang.ref.Cleaner") val cleanerCreate = cleanerClass.getMethod("create") @@ -26,9 +36,9 @@ private val closeWhenPhantomReachable: ((Any, AutoCloseable) -> Unit)? by lazy { cleanerClass.getMethod("register", Any::class.java, Runnable::class.java) val cleanerObject = cleanerCreate.invoke(null); - { observed, closeable -> + { observed, close -> try { - cleanerRegister.invoke(cleanerObject, observed, Runnable { closeable.close() }) + cleanerRegister.invoke(cleanerObject, observed, Runnable { close() }) } catch (e: ReflectiveOperationException) { if (e is InvocationTargetException) { when (val cause = e.cause) { diff --git a/openai-java-core/src/main/kotlin/com/openai/core/Utils.kt b/openai-java-core/src/main/kotlin/com/openai/core/Utils.kt index 6601f8ef1..49af26eeb 100644 --- a/openai-java-core/src/main/kotlin/com/openai/core/Utils.kt +++ b/openai-java-core/src/main/kotlin/com/openai/core/Utils.kt @@ -23,11 +23,4 @@ internal fun , V> SortedMap.toImmutable(): SortedMap.openai.azure.com`. - // Or `https://.azure-api.net` for Azure OpenAI Management URL. - return baseUrl.endsWith(".openai.azure.com", true) || baseUrl.endsWith(".azure-api.net", true) -} - internal interface Enum diff --git a/openai-java-core/src/main/kotlin/com/openai/core/http/AsyncStreamResponse.kt b/openai-java-core/src/main/kotlin/com/openai/core/http/AsyncStreamResponse.kt new file mode 100644 index 000000000..902889902 --- /dev/null +++ b/openai-java-core/src/main/kotlin/com/openai/core/http/AsyncStreamResponse.kt @@ -0,0 +1,97 @@ +package com.openai.core.http + +import com.openai.core.http.AsyncStreamResponse.Handler +import java.util.Optional +import java.util.concurrent.CompletableFuture +import java.util.concurrent.Executor +import java.util.concurrent.atomic.AtomicReference + +interface AsyncStreamResponse { + + fun subscribe(handler: Handler): AsyncStreamResponse + + fun subscribe(handler: Handler, executor: Executor): AsyncStreamResponse + + /** + * Closes this resource, relinquishing any underlying resources. + * + * This is purposefully not inherited from [AutoCloseable] because this response should not be + * synchronously closed via try-with-resources. + */ + fun close() + + fun interface Handler { + + fun onNext(value: T) + + fun onComplete(error: Optional) {} + } +} + +@JvmSynthetic +internal fun CompletableFuture>.toAsync(streamHandlerExecutor: Executor) = + PhantomReachableClosingAsyncStreamResponse( + object : AsyncStreamResponse { + + private val state = AtomicReference(State.NEW) + + override fun subscribe(handler: Handler): AsyncStreamResponse = + subscribe(handler, streamHandlerExecutor) + + override fun subscribe( + handler: Handler, + executor: Executor + ): AsyncStreamResponse = apply { + // TODO(JDK): Use `compareAndExchange` once targeting JDK 9. + check(state.compareAndSet(State.NEW, State.SUBSCRIBED)) { + if (state.get() == State.SUBSCRIBED) "Cannot subscribe more than once" + else "Cannot subscribe after the response is closed" + } + + this@toAsync.whenCompleteAsync( + { streamResponse, futureError -> + if (state.get() == State.CLOSED) { + // Avoid doing any work if `close` was called before the future + // completed. + return@whenCompleteAsync + } + + if (futureError != null) { + // An error occurred before we started passing chunks to the handler. + handler.onComplete(Optional.of(futureError)) + return@whenCompleteAsync + } + + var streamError: Throwable? = null + try { + streamResponse.stream().forEach(handler::onNext) + } catch (e: Throwable) { + streamError = e + } + + try { + handler.onComplete(Optional.ofNullable(streamError)) + } finally { + close() + } + }, + executor + ) + } + + override fun close() { + val previousState = state.getAndSet(State.CLOSED) + if (previousState == State.CLOSED) { + return + } + + this@toAsync.whenComplete { streamResponse, _ -> streamResponse?.close() } + } + } + ) + +private enum class State { + NEW, + SUBSCRIBED, + CLOSED +} diff --git a/openai-java-core/src/main/kotlin/com/openai/core/http/PhantomReachableClosingAsyncStreamResponse.kt b/openai-java-core/src/main/kotlin/com/openai/core/http/PhantomReachableClosingAsyncStreamResponse.kt new file mode 100644 index 000000000..7d0cb2f79 --- /dev/null +++ b/openai-java-core/src/main/kotlin/com/openai/core/http/PhantomReachableClosingAsyncStreamResponse.kt @@ -0,0 +1,24 @@ +package com.openai.core.http + +import com.openai.core.closeWhenPhantomReachable +import com.openai.core.http.AsyncStreamResponse.Handler +import java.util.concurrent.Executor + +internal class PhantomReachableClosingAsyncStreamResponse( + private val asyncStreamResponse: AsyncStreamResponse +) : AsyncStreamResponse { + init { + closeWhenPhantomReachable(this, asyncStreamResponse::close) + } + + override fun subscribe(handler: Handler): AsyncStreamResponse = apply { + asyncStreamResponse.subscribe(handler) + } + + override fun subscribe(handler: Handler, executor: Executor): AsyncStreamResponse = + apply { + asyncStreamResponse.subscribe(handler, executor) + } + + override fun close() = asyncStreamResponse.close() +} diff --git a/openai-java-core/src/main/kotlin/com/openai/credential/BearerTokenCredential.kt b/openai-java-core/src/main/kotlin/com/openai/credential/BearerTokenCredential.kt deleted file mode 100644 index 6da402a95..000000000 --- a/openai-java-core/src/main/kotlin/com/openai/credential/BearerTokenCredential.kt +++ /dev/null @@ -1,36 +0,0 @@ -package com.openai.credential - -import java.util.function.Supplier - -/** - *

A credential that provides a bearer token.

- * - *

- * If you are using the OpenAI API, you need to provide a bearer token for authentication. All API - * requests should include your API key in an Authorization HTTP header as follows: "Authorization: - * Bearer OPENAI_API_KEY"

- * - *

Two ways to provide the token:

- *
    - * 1. Provide the token directly, 'BearerTokenCredential.create(String)'. The method - * 'ClientOptions.apiKey(String)' is a wrapper for this. 2. Provide a supplier that - * provides the token, 'BearerTokenCredential.create(Supplier)'. - *
- * - * @param tokenSupplier a supplier that provides the token. - * @see OpenAI - * Authentication - */ -class BearerTokenCredential private constructor(private val tokenSupplier: Supplier) : - Credential { - - companion object { - @JvmStatic fun create(token: String): Credential = BearerTokenCredential { token } - - @JvmStatic - fun create(tokenSupplier: Supplier): Credential = - BearerTokenCredential(tokenSupplier) - } - - fun token(): String = tokenSupplier.get() -} diff --git a/openai-java-core/src/main/kotlin/com/openai/credential/Credential.kt b/openai-java-core/src/main/kotlin/com/openai/credential/Credential.kt deleted file mode 100644 index f43ab84c4..000000000 --- a/openai-java-core/src/main/kotlin/com/openai/credential/Credential.kt +++ /dev/null @@ -1,4 +0,0 @@ -package com.openai.credential - -/** An interface that represents a credential. */ -interface Credential diff --git a/openai-java-core/src/main/kotlin/com/openai/services/async/CompletionServiceAsync.kt b/openai-java-core/src/main/kotlin/com/openai/services/async/CompletionServiceAsync.kt index b3b4ee5a0..ce272a947 100644 --- a/openai-java-core/src/main/kotlin/com/openai/services/async/CompletionServiceAsync.kt +++ b/openai-java-core/src/main/kotlin/com/openai/services/async/CompletionServiceAsync.kt @@ -5,6 +5,7 @@ package com.openai.services.async import com.openai.core.RequestOptions +import com.openai.core.http.AsyncStreamResponse import com.openai.models.Completion import com.openai.models.CompletionCreateParams import java.util.concurrent.CompletableFuture @@ -17,4 +18,11 @@ interface CompletionServiceAsync { params: CompletionCreateParams, requestOptions: RequestOptions = RequestOptions.none() ): CompletableFuture + + /** Creates a completion for the provided prompt and parameters. */ + @JvmOverloads + fun createStreaming( + params: CompletionCreateParams, + requestOptions: RequestOptions = RequestOptions.none() + ): AsyncStreamResponse } diff --git a/openai-java-core/src/main/kotlin/com/openai/services/async/CompletionServiceAsyncImpl.kt b/openai-java-core/src/main/kotlin/com/openai/services/async/CompletionServiceAsyncImpl.kt index 4c5ed7df3..d9a137955 100644 --- a/openai-java-core/src/main/kotlin/com/openai/services/async/CompletionServiceAsyncImpl.kt +++ b/openai-java-core/src/main/kotlin/com/openai/services/async/CompletionServiceAsyncImpl.kt @@ -3,13 +3,20 @@ package com.openai.services.async import com.openai.core.ClientOptions +import com.openai.core.JsonValue import com.openai.core.RequestOptions import com.openai.core.handlers.errorHandler import com.openai.core.handlers.jsonHandler +import com.openai.core.handlers.map +import com.openai.core.handlers.mapJson +import com.openai.core.handlers.sseHandler import com.openai.core.handlers.withErrorHandler +import com.openai.core.http.AsyncStreamResponse import com.openai.core.http.HttpMethod import com.openai.core.http.HttpRequest import com.openai.core.http.HttpResponse.Handler +import com.openai.core.http.StreamResponse +import com.openai.core.http.toAsync import com.openai.core.json import com.openai.errors.OpenAIError import com.openai.models.Completion @@ -52,4 +59,47 @@ constructor( } } } + + private val createStreamingHandler: Handler> = + sseHandler(clientOptions.jsonMapper).mapJson().withErrorHandler(errorHandler) + + /** Creates a completion for the provided prompt and parameters. */ + override fun createStreaming( + params: CompletionCreateParams, + requestOptions: RequestOptions + ): AsyncStreamResponse { + val request = + HttpRequest.builder() + .method(HttpMethod.POST) + .addPathSegments("completions") + .putAllQueryParams(clientOptions.queryParams) + .replaceAllQueryParams(params.getQueryParams()) + .putAllHeaders(clientOptions.headers) + .replaceAllHeaders(params.getHeaders()) + .body( + json( + clientOptions.jsonMapper, + params + .getBody() + .toBuilder() + .putAdditionalProperty("stream", JsonValue.from(true)) + .build() + ) + ) + .build() + return clientOptions.httpClient + .executeAsync(request, requestOptions) + .thenApply { response -> + response + .let { createStreamingHandler.handle(it) } + .let { streamResponse -> + if (requestOptions.responseValidation ?: clientOptions.responseValidation) { + streamResponse.map { it.validate() } + } else { + streamResponse + } + } + } + .toAsync(clientOptions.streamHandlerExecutor) + } } diff --git a/openai-java-core/src/main/kotlin/com/openai/services/async/chat/CompletionServiceAsync.kt b/openai-java-core/src/main/kotlin/com/openai/services/async/chat/CompletionServiceAsync.kt index d0214cc6a..ee22a7a6a 100644 --- a/openai-java-core/src/main/kotlin/com/openai/services/async/chat/CompletionServiceAsync.kt +++ b/openai-java-core/src/main/kotlin/com/openai/services/async/chat/CompletionServiceAsync.kt @@ -5,7 +5,9 @@ package com.openai.services.async.chat import com.openai.core.RequestOptions +import com.openai.core.http.AsyncStreamResponse import com.openai.models.ChatCompletion +import com.openai.models.ChatCompletionChunk import com.openai.models.ChatCompletionCreateParams import java.util.concurrent.CompletableFuture @@ -22,4 +24,16 @@ interface CompletionServiceAsync { params: ChatCompletionCreateParams, requestOptions: RequestOptions = RequestOptions.none() ): CompletableFuture + + /** + * Creates a model response for the given chat conversation. Learn more in the + * [text generation](https://platform.openai.com/docs/guides/text-generation), + * [vision](https://platform.openai.com/docs/guides/vision), and + * [audio](https://platform.openai.com/docs/guides/audio) guides. + */ + @JvmOverloads + fun createStreaming( + params: ChatCompletionCreateParams, + requestOptions: RequestOptions = RequestOptions.none() + ): AsyncStreamResponse } diff --git a/openai-java-core/src/main/kotlin/com/openai/services/async/chat/CompletionServiceAsyncImpl.kt b/openai-java-core/src/main/kotlin/com/openai/services/async/chat/CompletionServiceAsyncImpl.kt index 17ce15ddf..f52b53324 100644 --- a/openai-java-core/src/main/kotlin/com/openai/services/async/chat/CompletionServiceAsyncImpl.kt +++ b/openai-java-core/src/main/kotlin/com/openai/services/async/chat/CompletionServiceAsyncImpl.kt @@ -3,18 +3,24 @@ package com.openai.services.async.chat import com.openai.core.ClientOptions +import com.openai.core.JsonValue import com.openai.core.RequestOptions import com.openai.core.handlers.errorHandler import com.openai.core.handlers.jsonHandler +import com.openai.core.handlers.map +import com.openai.core.handlers.mapJson +import com.openai.core.handlers.sseHandler import com.openai.core.handlers.withErrorHandler +import com.openai.core.http.AsyncStreamResponse import com.openai.core.http.HttpMethod import com.openai.core.http.HttpRequest import com.openai.core.http.HttpResponse.Handler -import com.openai.core.isAzureEndpoint +import com.openai.core.http.StreamResponse +import com.openai.core.http.toAsync import com.openai.core.json -import com.openai.credential.BearerTokenCredential import com.openai.errors.OpenAIError import com.openai.models.ChatCompletion +import com.openai.models.ChatCompletionChunk import com.openai.models.ChatCompletionCreateParams import java.util.concurrent.CompletableFuture @@ -41,23 +47,10 @@ constructor( val request = HttpRequest.builder() .method(HttpMethod.POST) - .apply { - if (isAzureEndpoint(clientOptions.baseUrl)) { - addPathSegments("openai", "deployments", params.model().toString()) - } - } .addPathSegments("chat", "completions") .putAllQueryParams(clientOptions.queryParams) .replaceAllQueryParams(params.getQueryParams()) .putAllHeaders(clientOptions.headers) - .apply { - if ( - isAzureEndpoint(clientOptions.baseUrl) && - clientOptions.credential is BearerTokenCredential - ) { - putHeader("Authorization", "Bearer ${clientOptions.credential.token()}") - } - } .replaceAllHeaders(params.getHeaders()) .body(json(clientOptions.jsonMapper, params.getBody())) .build() @@ -72,4 +65,54 @@ constructor( } } } + + private val createStreamingHandler: Handler> = + sseHandler(clientOptions.jsonMapper) + .mapJson() + .withErrorHandler(errorHandler) + + /** + * Creates a model response for the given chat conversation. Learn more in the + * [text generation](https://platform.openai.com/docs/guides/text-generation), + * [vision](https://platform.openai.com/docs/guides/vision), and + * [audio](https://platform.openai.com/docs/guides/audio) guides. + */ + override fun createStreaming( + params: ChatCompletionCreateParams, + requestOptions: RequestOptions + ): AsyncStreamResponse { + val request = + HttpRequest.builder() + .method(HttpMethod.POST) + .addPathSegments("chat", "completions") + .putAllQueryParams(clientOptions.queryParams) + .replaceAllQueryParams(params.getQueryParams()) + .putAllHeaders(clientOptions.headers) + .replaceAllHeaders(params.getHeaders()) + .body( + json( + clientOptions.jsonMapper, + params + .getBody() + .toBuilder() + .putAdditionalProperty("stream", JsonValue.from(true)) + .build() + ) + ) + .build() + return clientOptions.httpClient + .executeAsync(request, requestOptions) + .thenApply { response -> + response + .let { createStreamingHandler.handle(it) } + .let { streamResponse -> + if (requestOptions.responseValidation ?: clientOptions.responseValidation) { + streamResponse.map { it.validate() } + } else { + streamResponse + } + } + } + .toAsync(clientOptions.streamHandlerExecutor) + } } diff --git a/openai-java-core/src/main/kotlin/com/openai/services/blocking/chat/CompletionServiceImpl.kt b/openai-java-core/src/main/kotlin/com/openai/services/blocking/chat/CompletionServiceImpl.kt index f6ea7c91f..4052936fb 100644 --- a/openai-java-core/src/main/kotlin/com/openai/services/blocking/chat/CompletionServiceImpl.kt +++ b/openai-java-core/src/main/kotlin/com/openai/services/blocking/chat/CompletionServiceImpl.kt @@ -15,9 +15,7 @@ import com.openai.core.http.HttpMethod import com.openai.core.http.HttpRequest import com.openai.core.http.HttpResponse.Handler import com.openai.core.http.StreamResponse -import com.openai.core.isAzureEndpoint import com.openai.core.json -import com.openai.credential.BearerTokenCredential import com.openai.errors.OpenAIError import com.openai.models.ChatCompletion import com.openai.models.ChatCompletionChunk @@ -46,23 +44,10 @@ constructor( val request = HttpRequest.builder() .method(HttpMethod.POST) - .apply { - if (isAzureEndpoint(clientOptions.baseUrl)) { - addPathSegments("openai", "deployments", params.model().toString()) - } - } .addPathSegments("chat", "completions") .putAllQueryParams(clientOptions.queryParams) .replaceAllQueryParams(params.getQueryParams()) .putAllHeaders(clientOptions.headers) - .apply { - if ( - isAzureEndpoint(clientOptions.baseUrl) && - clientOptions.credential is BearerTokenCredential - ) { - putHeader("Authorization", "Bearer ${clientOptions.credential.token()}") - } - } .replaceAllHeaders(params.getHeaders()) .body(json(clientOptions.jsonMapper, params.getBody())) .build() diff --git a/openai-java-core/src/test/kotlin/com/openai/core/http/AsyncStreamResponseTest.kt b/openai-java-core/src/test/kotlin/com/openai/core/http/AsyncStreamResponseTest.kt new file mode 100644 index 000000000..257951a2e --- /dev/null +++ b/openai-java-core/src/test/kotlin/com/openai/core/http/AsyncStreamResponseTest.kt @@ -0,0 +1,165 @@ +package com.openai.core.http + +import java.util.* +import java.util.concurrent.CompletableFuture +import java.util.concurrent.Executor +import java.util.stream.Stream +import kotlin.streams.asStream +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.catchThrowable +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertDoesNotThrow +import org.junit.jupiter.api.extension.ExtendWith +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.kotlin.* + +@ExtendWith(MockitoExtension::class) +internal class AsyncStreamResponseTest { + + companion object { + private val ERROR = RuntimeException("ERROR!") + } + + private val streamResponse = + spy> { + doReturn(Stream.of("chunk1", "chunk2", "chunk3")).whenever(it).stream() + } + private val erroringStreamResponse = + spy> { + doReturn( + sequence { + yield("chunk1") + yield("chunk2") + throw ERROR + } + .asStream() + ) + .whenever(it) + .stream() + } + private val executor = + spy { + doAnswer { invocation -> invocation.getArgument(0).run() } + .whenever(it) + .execute(any()) + } + private val handler = mock>() + + @Test + fun subscribe_whenAlreadySubscribed_throws() { + val asyncStreamResponse = CompletableFuture>().toAsync(executor) + asyncStreamResponse.subscribe {} + + val throwable = catchThrowable { asyncStreamResponse.subscribe {} } + + assertThat(throwable).isInstanceOf(IllegalStateException::class.java) + assertThat(throwable).hasMessage("Cannot subscribe more than once") + verify(executor, never()).execute(any()) + } + + @Test + fun subscribe_whenClosed_throws() { + val asyncStreamResponse = CompletableFuture>().toAsync(executor) + asyncStreamResponse.close() + + val throwable = catchThrowable { asyncStreamResponse.subscribe {} } + + assertThat(throwable).isInstanceOf(IllegalStateException::class.java) + assertThat(throwable).hasMessage("Cannot subscribe after the response is closed") + verify(executor, never()).execute(any()) + } + + @Test + fun subscribe_whenFutureCompletesAfterClose_doesNothing() { + val future = CompletableFuture>() + val asyncStreamResponse = future.toAsync(executor) + asyncStreamResponse.subscribe(handler) + asyncStreamResponse.close() + + future.complete(streamResponse) + + verify(handler, never()).onNext(any()) + verify(handler, never()).onComplete(any()) + verify(executor, times(1)).execute(any()) + } + + @Test + fun subscribe_whenFutureErrors_callsOnComplete() { + val future = CompletableFuture>() + val asyncStreamResponse = future.toAsync(executor) + asyncStreamResponse.subscribe(handler) + + future.completeExceptionally(ERROR) + + verify(handler, never()).onNext(any()) + verify(handler, times(1)).onComplete(Optional.of(ERROR)) + verify(executor, times(1)).execute(any()) + } + + @Test + fun subscribe_whenFutureCompletes_runsHandler() { + val future = CompletableFuture>() + val asyncStreamResponse = future.toAsync(executor) + asyncStreamResponse.subscribe(handler) + + future.complete(streamResponse) + + inOrder(handler, streamResponse) { + verify(handler, times(1)).onNext("chunk1") + verify(handler, times(1)).onNext("chunk2") + verify(handler, times(1)).onNext("chunk3") + verify(handler, times(1)).onComplete(Optional.empty()) + verify(streamResponse, times(1)).close() + } + verify(executor, times(1)).execute(any()) + } + + @Test + fun subscribe_whenStreamErrors_callsOnCompleteEarly() { + val future = CompletableFuture>() + val asyncStreamResponse = future.toAsync(executor) + asyncStreamResponse.subscribe(handler) + + future.complete(erroringStreamResponse) + + inOrder(handler, erroringStreamResponse) { + verify(handler, times(1)).onNext("chunk1") + verify(handler, times(1)).onNext("chunk2") + verify(handler, times(1)).onComplete(Optional.of(ERROR)) + verify(erroringStreamResponse, times(1)).close() + } + verify(executor, times(1)).execute(any()) + } + + @Test + fun close_whenNotClosed_closesStreamResponse() { + val future = CompletableFuture>() + val asyncStreamResponse = future.toAsync(executor) + + asyncStreamResponse.close() + future.complete(streamResponse) + + verify(streamResponse, times(1)).close() + } + + @Test + fun close_whenAlreadyClosed_doesNothing() { + val future = CompletableFuture>() + val asyncStreamResponse = future.toAsync(executor) + asyncStreamResponse.close() + future.complete(streamResponse) + + asyncStreamResponse.close() + + verify(streamResponse, times(1)).close() + } + + @Test + fun close_whenFutureErrors_doesNothing() { + val future = CompletableFuture>() + val asyncStreamResponse = future.toAsync(executor) + asyncStreamResponse.close() + + assertDoesNotThrow { future.completeExceptionally(ERROR) } + } +} diff --git a/openai-java-core/src/test/kotlin/com/openai/core/http/ClientOptionsTest.kt b/openai-java-core/src/test/kotlin/com/openai/core/http/ClientOptionsTest.kt deleted file mode 100644 index d53f5d8f9..000000000 --- a/openai-java-core/src/test/kotlin/com/openai/core/http/ClientOptionsTest.kt +++ /dev/null @@ -1,70 +0,0 @@ -package com.openai.core.http - -import com.openai.azure.credential.AzureApiKeyCredential -import com.openai.client.okhttp.OkHttpClient -import com.openai.core.ClientOptions -import com.openai.credential.BearerTokenCredential -import java.util.stream.Stream -import kotlin.test.Test -import org.assertj.core.api.Assertions.assertThat -import org.assertj.core.api.Assertions.assertThatThrownBy -import org.junit.jupiter.params.ParameterizedTest -import org.junit.jupiter.params.provider.MethodSource - -internal class ClientOptionsTest { - - companion object { - private const val FAKE_API_KEY = "test-api-key" - - @JvmStatic - private fun createOkHttpClient(baseUrl: String): OkHttpClient { - return OkHttpClient.builder().baseUrl(baseUrl).build() - } - - @JvmStatic - private fun provideBaseUrls(): Stream { - return Stream.of( - "https://api.openai.com/v1", - "https://example.openai.azure.com", - "https://example.azure-api.net" - ) - } - } - - @ParameterizedTest - @MethodSource("provideBaseUrls") - fun clientOptionsWithoutBaseUrl(baseUrl: String) { - // Arrange - val apiKey = FAKE_API_KEY - - // Act - val clientOptions = - ClientOptions.builder() - .httpClient(createOkHttpClient(baseUrl)) - .credential(BearerTokenCredential.create(apiKey)) - .build() - - // Assert - assertThat(clientOptions.baseUrl).isEqualTo(ClientOptions.PRODUCTION_URL) - } - - @ParameterizedTest - @MethodSource("provideBaseUrls") - fun throwExceptionWhenNullCredential(baseUrl: String) { - // Act - val clientOptionsBuilder = - ClientOptions.builder().httpClient(createOkHttpClient(baseUrl)).baseUrl(baseUrl) - - // Assert - assertThatThrownBy { clientOptionsBuilder.build() } - .isInstanceOf(IllegalStateException::class.java) - .hasMessage("`credential` is required but was not set") - } - - @Test - fun throwExceptionWhenEmptyCredential() { - assertThatThrownBy { AzureApiKeyCredential.create("") } - .isInstanceOf(IllegalArgumentException::class.java) - .hasMessage("Azure API key cannot be empty.") - } -} diff --git a/openai-java-core/src/test/kotlin/com/openai/services/ErrorHandlingTest.kt b/openai-java-core/src/test/kotlin/com/openai/services/ErrorHandlingTest.kt index c9d34bb02..17dc53f01 100644 --- a/openai-java-core/src/test/kotlin/com/openai/services/ErrorHandlingTest.kt +++ b/openai-java-core/src/test/kotlin/com/openai/services/ErrorHandlingTest.kt @@ -31,7 +31,6 @@ import com.openai.models.* import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatThrownBy import org.assertj.core.api.InstanceOfAssertFactories -import org.assertj.guava.api.Assertions.assertThat import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test diff --git a/openai-java-core/src/test/kotlin/com/openai/services/blocking/fineTuning/jobs/CheckpointServiceTest.kt b/openai-java-core/src/test/kotlin/com/openai/services/blocking/fineTuning/jobs/CheckpointServiceTest.kt index d779133bd..5ed44e5f2 100644 --- a/openai-java-core/src/test/kotlin/com/openai/services/blocking/fineTuning/jobs/CheckpointServiceTest.kt +++ b/openai-java-core/src/test/kotlin/com/openai/services/blocking/fineTuning/jobs/CheckpointServiceTest.kt @@ -4,6 +4,7 @@ package com.openai.services.blocking.fineTuning.jobs import com.openai.TestServerExtension import com.openai.client.okhttp.OpenAIOkHttpClient +import com.openai.models.* import com.openai.models.FineTuningJobCheckpointListParams import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith diff --git a/settings.gradle.kts b/settings.gradle.kts index 58e9de020..3c5725fb3 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -4,4 +4,3 @@ include("openai-java") include("openai-java-client-okhttp") include("openai-java-core") include("openai-java-example") -include("openai-azure-java-example")