From 70b176e155289a1da43806eb67d3d256b80a2ca7 Mon Sep 17 00:00:00 2001 From: slam Date: Thu, 14 Mar 2024 19:37:14 +0800 Subject: [PATCH] feat(BE-174): integrate anthropic api --- .../anthropic-client-core/build.gradle.kts | 45 +++++++++++++ anthropic-client/build.gradle.kts | 15 +++++ common/build.gradle.kts | 47 ++++++++++++++ .../network/api/ktor/api}/HttpRequester.kt | 6 +- .../network/api/ktor/api}/ListResponse.kt | 2 +- .../common/network/api/ktor/api/Stream.kt | 28 ++++++++ .../ktor/internal}/DefaultHttpRequester.kt | 9 ++- .../network/api}/ktor/internal/HttpClient.kt | 5 +- .../network/api/ktor/internal}/JsonLenient.kt | 2 +- .../internal/exception/OpenLLMAPIException.kt | 2 +- .../internal/exception/OpenLLMErrorDetails.kt | 2 +- .../internal/exception/OpenLLMException.kt | 2 +- .../internal/exception/OpenLLMIOException.kt | 2 +- .../internal}/DefaultHttpRequester.jvm.kt | 4 +- .../common/network/api/InternalPackageTest.kt | 65 +++++++++++++++++++ .../tddworks/common/network/api/JsonUtils.kt | 23 +++++++ .../common/network/api/MockHttpClient.kt | 44 +++++++++++++ .../api}/ktor/DefaultHttpRequesterTest.kt | 6 +- .../internal}/DefaultHttpRequester.macos.kt | 5 +- .../openai-client-core/build.gradle.kts | 1 + .../kotlin/com/tddworks/openai/api/OpenAI.kt | 6 +- .../openai/api/chat/api/StreamableRequest.kt | 2 +- .../api/chat/internal/DefaultChatApi.kt | 6 +- .../tddworks/openai/api/images/api/Images.kt | 3 +- .../api/images/internal/DefaultImagesApi.kt | 6 +- .../api/internal/network/ktor/Stream.kt | 41 ------------ .../com/tddworks/openai/api/JvmOpenAI.kt | 5 +- .../api/chat/internal/DefaultChatApiTest.kt | 2 +- .../images/internal/DefaultImagesApiTest.kt | 2 +- .../openai/darwin/api/DarwinOpenAI.kt | 4 +- settings.gradle.kts | 5 ++ 31 files changed, 315 insertions(+), 82 deletions(-) create mode 100644 anthropic-client/anthropic-client-core/build.gradle.kts create mode 100644 anthropic-client/build.gradle.kts create mode 100644 common/build.gradle.kts rename {openai-client/openai-client-core/src/commonMain/kotlin/com/tddworks/openai/api/internal/network/ktor => common/src/commonMain/kotlin/com/tddworks/common/network/api/ktor/api}/HttpRequester.kt (82%) rename {openai-client/openai-client-core/src/commonMain/kotlin/com/tddworks/openai/api/common => common/src/commonMain/kotlin/com/tddworks/common/network/api/ktor/api}/ListResponse.kt (73%) create mode 100644 common/src/commonMain/kotlin/com/tddworks/common/network/api/ktor/api/Stream.kt rename {openai-client/openai-client-core/src/commonMain/kotlin/com/tddworks/openai/api/internal/network/ktor => common/src/commonMain/kotlin/com/tddworks/common/network/api/ktor/internal}/DefaultHttpRequester.kt (89%) rename {openai-client/openai-client-core/src/commonMain/kotlin/com/tddworks/openai/api/internal/network => common/src/commonMain/kotlin/com/tddworks/common/network/api}/ktor/internal/HttpClient.kt (95%) rename {openai-client/openai-client-core/src/commonMain/kotlin/com/tddworks/openai/api/common => common/src/commonMain/kotlin/com/tddworks/common/network/api/ktor/internal}/JsonLenient.kt (93%) rename openai-client/openai-client-core/src/commonMain/kotlin/com/tddworks/openai/api/internal/network/ktor/exception/OpenAIAPIException.kt => common/src/commonMain/kotlin/com/tddworks/common/network/api/ktor/internal/exception/OpenLLMAPIException.kt (96%) rename openai-client/openai-client-core/src/commonMain/kotlin/com/tddworks/openai/api/internal/network/ktor/exception/OpenAIErrorDetails.kt => common/src/commonMain/kotlin/com/tddworks/common/network/api/ktor/internal/exception/OpenLLMErrorDetails.kt (93%) rename openai-client/openai-client-core/src/commonMain/kotlin/com/tddworks/openai/api/internal/network/ktor/exception/OpenAIException.kt => common/src/commonMain/kotlin/com/tddworks/common/network/api/ktor/internal/exception/OpenLLMException.kt (88%) rename openai-client/openai-client-core/src/commonMain/kotlin/com/tddworks/openai/api/internal/network/ktor/exception/OpenAIIOException.kt => common/src/commonMain/kotlin/com/tddworks/common/network/api/ktor/internal/exception/OpenLLMIOException.kt (89%) rename {openai-client/openai-client-core/src/jvmMain/kotlin/com/tddworks/openai/api/internal/network/ktor => common/src/jvmMain/kotlin/com/tddworks/common/network/api/ktor/internal}/DefaultHttpRequester.jvm.kt (68%) create mode 100644 common/src/jvmTest/kotlin/com/tddworks/common/network/api/InternalPackageTest.kt create mode 100644 common/src/jvmTest/kotlin/com/tddworks/common/network/api/JsonUtils.kt create mode 100644 common/src/jvmTest/kotlin/com/tddworks/common/network/api/MockHttpClient.kt rename {openai-client/openai-client-core/src/jvmTest/kotlin/com/tddworks/openai/api/internal/network => common/src/jvmTest/kotlin/com/tddworks/common/network/api}/ktor/DefaultHttpRequesterTest.kt (85%) rename {openai-client/openai-client-core/src/macosMain/kotlin/com/tddworks/openai/api/internal/network/ktor => common/src/macosMain/kotlin/com/tddworks/common/network/api/ktor/internal}/DefaultHttpRequester.macos.kt (68%) delete mode 100644 openai-client/openai-client-core/src/commonMain/kotlin/com/tddworks/openai/api/internal/network/ktor/Stream.kt diff --git a/anthropic-client/anthropic-client-core/build.gradle.kts b/anthropic-client/anthropic-client-core/build.gradle.kts new file mode 100644 index 0000000..b307519 --- /dev/null +++ b/anthropic-client/anthropic-client-core/build.gradle.kts @@ -0,0 +1,45 @@ +plugins { + alias(libs.plugins.kotlinx.serialization) + alias(libs.plugins.kover) +// `maven-publish` +} + +kotlin { + jvm() + macosArm64() + macosX64() + + sourceSets { + commonMain.dependencies { + // put your Multiplatform dependencies here + api(libs.kotlinx.serialization.json) + api(libs.bundles.ktor.client) + } + + commonTest.dependencies { + implementation(libs.ktor.client.mock) + } + + macosMain.dependencies { + api(libs.ktor.client.darwin) + } + + jvmMain.dependencies { + api(libs.ktor.client.cio) + } + + jvmTest.dependencies { + implementation(project.dependencies.platform(libs.junit.bom)) + implementation(libs.bundles.jvm.test) + implementation(libs.app.cash.turbine) + implementation("com.tngtech.archunit:archunit-junit5:1.1.0") + implementation("org.reflections:reflections:0.10.2") + } + } +} + +tasks { + named("jvmTest") { + useJUnitPlatform() + } +} \ No newline at end of file diff --git a/anthropic-client/build.gradle.kts b/anthropic-client/build.gradle.kts new file mode 100644 index 0000000..9021eae --- /dev/null +++ b/anthropic-client/build.gradle.kts @@ -0,0 +1,15 @@ +plugins { + alias(libs.plugins.kover) +// `maven-publish` +} + +kotlin { + jvm() + sourceSets { + commonMain { + dependencies { + api(projects.anthropicClient.anthropicClientCore) + } + } + } +} \ No newline at end of file diff --git a/common/build.gradle.kts b/common/build.gradle.kts new file mode 100644 index 0000000..9ecb701 --- /dev/null +++ b/common/build.gradle.kts @@ -0,0 +1,47 @@ +plugins { + alias(libs.plugins.kotlinx.serialization) + alias(libs.plugins.kover) +// alias(libs.plugins.touchlab.kmmbridge) +// id("module.publication") + `maven-publish` +} + +kotlin { + jvm() + macosArm64() + macosX64() + + sourceSets { + commonMain.dependencies { + // put your Multiplatform dependencies here + api(libs.kotlinx.serialization.json) + api(libs.bundles.ktor.client) + } + + commonTest.dependencies { + implementation(libs.ktor.client.mock) + } + + macosMain.dependencies { + api(libs.ktor.client.darwin) + } + + jvmMain.dependencies { + api(libs.ktor.client.cio) + } + + jvmTest.dependencies { + implementation(project.dependencies.platform(libs.junit.bom)) + implementation(libs.bundles.jvm.test) + implementation(libs.app.cash.turbine) + implementation("com.tngtech.archunit:archunit-junit5:1.1.0") + implementation("org.reflections:reflections:0.10.2") + } + } +} + +tasks { + named("jvmTest") { + useJUnitPlatform() + } +} \ No newline at end of file diff --git a/openai-client/openai-client-core/src/commonMain/kotlin/com/tddworks/openai/api/internal/network/ktor/HttpRequester.kt b/common/src/commonMain/kotlin/com/tddworks/common/network/api/ktor/api/HttpRequester.kt similarity index 82% rename from openai-client/openai-client-core/src/commonMain/kotlin/com/tddworks/openai/api/internal/network/ktor/HttpRequester.kt rename to common/src/commonMain/kotlin/com/tddworks/common/network/api/ktor/api/HttpRequester.kt index c47e27f..d22734c 100644 --- a/openai-client/openai-client-core/src/commonMain/kotlin/com/tddworks/openai/api/internal/network/ktor/HttpRequester.kt +++ b/common/src/commonMain/kotlin/com/tddworks/common/network/api/ktor/api/HttpRequester.kt @@ -1,4 +1,4 @@ -package com.tddworks.openai.api.internal.network.ktor +package com.tddworks.common.network.api.ktor.api import io.ktor.client.request.* import io.ktor.client.statement.* @@ -31,7 +31,7 @@ interface HttpRequester { * @param builder The HttpRequestBuilder that contains the HTTP request details. * @return The result of the HTTP request. */ -internal suspend inline fun HttpRequester.performRequest(noinline builder: HttpRequestBuilder.() -> Unit): T { +suspend inline fun HttpRequester.performRequest(noinline builder: HttpRequestBuilder.() -> Unit): T { return performRequest(typeInfo(), builder) } @@ -39,7 +39,7 @@ internal suspend inline fun HttpRequester.performRequest(noinline bu /** * Perform an HTTP request and get a result */ -internal inline fun HttpRequester.streamRequest( +inline fun HttpRequester.streamRequest( noinline builder: HttpRequestBuilder.() -> Unit, ): Flow { return flow { diff --git a/openai-client/openai-client-core/src/commonMain/kotlin/com/tddworks/openai/api/common/ListResponse.kt b/common/src/commonMain/kotlin/com/tddworks/common/network/api/ktor/api/ListResponse.kt similarity index 73% rename from openai-client/openai-client-core/src/commonMain/kotlin/com/tddworks/openai/api/common/ListResponse.kt rename to common/src/commonMain/kotlin/com/tddworks/common/network/api/ktor/api/ListResponse.kt index b649a40..72c94ef 100644 --- a/openai-client/openai-client-core/src/commonMain/kotlin/com/tddworks/openai/api/common/ListResponse.kt +++ b/common/src/commonMain/kotlin/com/tddworks/common/network/api/ktor/api/ListResponse.kt @@ -1,4 +1,4 @@ -package com.tddworks.openai.api.common +package com.tddworks.common.network.api.ktor.api import kotlinx.serialization.Serializable diff --git a/common/src/commonMain/kotlin/com/tddworks/common/network/api/ktor/api/Stream.kt b/common/src/commonMain/kotlin/com/tddworks/common/network/api/ktor/api/Stream.kt new file mode 100644 index 0000000..6ddd781 --- /dev/null +++ b/common/src/commonMain/kotlin/com/tddworks/common/network/api/ktor/api/Stream.kt @@ -0,0 +1,28 @@ +package com.tddworks.common.network.api.ktor.api + +import com.tddworks.common.network.api.ktor.internal.JsonLenient +import io.ktor.client.call.* +import io.ktor.client.statement.* +import io.ktor.utils.io.* +import kotlinx.coroutines.flow.FlowCollector + +const val STREAM_PREFIX = "data:" +private const val STREAM_END_TOKEN = "$STREAM_PREFIX [DONE]" + +/** + * Get data as [Server-Sent Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#Event_stream_format). + */ +suspend inline fun FlowCollector.streamEventsFrom(response: HttpResponse) { + val channel: ByteReadChannel = response.body() + while (!channel.isClosedForRead) { + channel.readUTF8Line()?.let { streamResponse -> + if (notEndStreamResponse(streamResponse)) { + emit(JsonLenient.decodeFromString(streamResponse.removePrefix(STREAM_PREFIX))) + } + } ?: break + } +} + +private fun isStreamResponse(line: String) = line.startsWith(STREAM_PREFIX) + +fun notEndStreamResponse(line: String) = line != STREAM_END_TOKEN && isStreamResponse(line) \ No newline at end of file diff --git a/openai-client/openai-client-core/src/commonMain/kotlin/com/tddworks/openai/api/internal/network/ktor/DefaultHttpRequester.kt b/common/src/commonMain/kotlin/com/tddworks/common/network/api/ktor/internal/DefaultHttpRequester.kt similarity index 89% rename from openai-client/openai-client-core/src/commonMain/kotlin/com/tddworks/openai/api/internal/network/ktor/DefaultHttpRequester.kt rename to common/src/commonMain/kotlin/com/tddworks/common/network/api/ktor/internal/DefaultHttpRequester.kt index 975846c..9f362d1 100644 --- a/openai-client/openai-client-core/src/commonMain/kotlin/com/tddworks/openai/api/internal/network/ktor/DefaultHttpRequester.kt +++ b/common/src/commonMain/kotlin/com/tddworks/common/network/api/ktor/internal/DefaultHttpRequester.kt @@ -1,10 +1,9 @@ -package com.tddworks.openai.api.internal.network.ktor +package com.tddworks.common.network.api.ktor.internal -import com.tddworks.openai.api.internal.network.ktor.exception.* -import com.tddworks.openai.api.internal.network.ktor.internal.createHttpClient +import com.tddworks.common.network.api.ktor.api.HttpRequester +import com.tddworks.common.network.api.ktor.internal.exception.* import io.ktor.client.* import io.ktor.client.call.* -import io.ktor.client.engine.* import io.ktor.client.network.sockets.* import io.ktor.client.plugins.* import io.ktor.client.request.* @@ -17,7 +16,7 @@ import kotlinx.coroutines.CancellationException * Default implementation of [HttpRequester]. * @property httpClient The HttpClient to use for performing HTTP requests. */ -internal class DefaultHttpRequester(private val httpClient: HttpClient) : HttpRequester { +class DefaultHttpRequester(private val httpClient: HttpClient) : HttpRequester { override suspend fun performRequest(info: TypeInfo, builder: HttpRequestBuilder.() -> Unit): T { try { diff --git a/openai-client/openai-client-core/src/commonMain/kotlin/com/tddworks/openai/api/internal/network/ktor/internal/HttpClient.kt b/common/src/commonMain/kotlin/com/tddworks/common/network/api/ktor/internal/HttpClient.kt similarity index 95% rename from openai-client/openai-client-core/src/commonMain/kotlin/com/tddworks/openai/api/internal/network/ktor/internal/HttpClient.kt rename to common/src/commonMain/kotlin/com/tddworks/common/network/api/ktor/internal/HttpClient.kt index 6a30d02..1dfab3d 100644 --- a/openai-client/openai-client-core/src/commonMain/kotlin/com/tddworks/openai/api/internal/network/ktor/internal/HttpClient.kt +++ b/common/src/commonMain/kotlin/com/tddworks/common/network/api/ktor/internal/HttpClient.kt @@ -1,6 +1,5 @@ -package com.tddworks.openai.api.internal.network.ktor.internal +package com.tddworks.common.network.api.ktor.internal -import com.tddworks.openai.api.common.JsonLenient import io.ktor.client.* import io.ktor.client.engine.* import io.ktor.client.plugins.* @@ -20,7 +19,7 @@ import kotlin.time.Duration.Companion.minutes * @param token the authentication token * @return a new [HttpClient] instance */ -internal fun createHttpClient( +fun createHttpClient( url: String, token: String, engine: HttpClientEngineFactory, diff --git a/openai-client/openai-client-core/src/commonMain/kotlin/com/tddworks/openai/api/common/JsonLenient.kt b/common/src/commonMain/kotlin/com/tddworks/common/network/api/ktor/internal/JsonLenient.kt similarity index 93% rename from openai-client/openai-client-core/src/commonMain/kotlin/com/tddworks/openai/api/common/JsonLenient.kt rename to common/src/commonMain/kotlin/com/tddworks/common/network/api/ktor/internal/JsonLenient.kt index 0781b36..1e9d3cb 100644 --- a/openai-client/openai-client-core/src/commonMain/kotlin/com/tddworks/openai/api/common/JsonLenient.kt +++ b/common/src/commonMain/kotlin/com/tddworks/common/network/api/ktor/internal/JsonLenient.kt @@ -1,4 +1,4 @@ -package com.tddworks.openai.api.common +package com.tddworks.common.network.api.ktor.internal import kotlinx.serialization.json.Json diff --git a/openai-client/openai-client-core/src/commonMain/kotlin/com/tddworks/openai/api/internal/network/ktor/exception/OpenAIAPIException.kt b/common/src/commonMain/kotlin/com/tddworks/common/network/api/ktor/internal/exception/OpenLLMAPIException.kt similarity index 96% rename from openai-client/openai-client-core/src/commonMain/kotlin/com/tddworks/openai/api/internal/network/ktor/exception/OpenAIAPIException.kt rename to common/src/commonMain/kotlin/com/tddworks/common/network/api/ktor/internal/exception/OpenLLMAPIException.kt index d33d651..a519824 100644 --- a/openai-client/openai-client-core/src/commonMain/kotlin/com/tddworks/openai/api/internal/network/ktor/exception/OpenAIAPIException.kt +++ b/common/src/commonMain/kotlin/com/tddworks/common/network/api/ktor/internal/exception/OpenLLMAPIException.kt @@ -1,4 +1,4 @@ -package com.tddworks.openai.api.internal.network.ktor.exception +package com.tddworks.common.network.api.ktor.internal.exception /** diff --git a/openai-client/openai-client-core/src/commonMain/kotlin/com/tddworks/openai/api/internal/network/ktor/exception/OpenAIErrorDetails.kt b/common/src/commonMain/kotlin/com/tddworks/common/network/api/ktor/internal/exception/OpenLLMErrorDetails.kt similarity index 93% rename from openai-client/openai-client-core/src/commonMain/kotlin/com/tddworks/openai/api/internal/network/ktor/exception/OpenAIErrorDetails.kt rename to common/src/commonMain/kotlin/com/tddworks/common/network/api/ktor/internal/exception/OpenLLMErrorDetails.kt index ebde38e..6d67cc1 100644 --- a/openai-client/openai-client-core/src/commonMain/kotlin/com/tddworks/openai/api/internal/network/ktor/exception/OpenAIErrorDetails.kt +++ b/common/src/commonMain/kotlin/com/tddworks/common/network/api/ktor/internal/exception/OpenLLMErrorDetails.kt @@ -1,4 +1,4 @@ -package com.tddworks.openai.api.internal.network.ktor.exception +package com.tddworks.common.network.api.ktor.internal.exception import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable diff --git a/openai-client/openai-client-core/src/commonMain/kotlin/com/tddworks/openai/api/internal/network/ktor/exception/OpenAIException.kt b/common/src/commonMain/kotlin/com/tddworks/common/network/api/ktor/internal/exception/OpenLLMException.kt similarity index 88% rename from openai-client/openai-client-core/src/commonMain/kotlin/com/tddworks/openai/api/internal/network/ktor/exception/OpenAIException.kt rename to common/src/commonMain/kotlin/com/tddworks/common/network/api/ktor/internal/exception/OpenLLMException.kt index 1e73c66..58501f0 100644 --- a/openai-client/openai-client-core/src/commonMain/kotlin/com/tddworks/openai/api/internal/network/ktor/exception/OpenAIException.kt +++ b/common/src/commonMain/kotlin/com/tddworks/common/network/api/ktor/internal/exception/OpenLLMException.kt @@ -1,4 +1,4 @@ -package com.tddworks.openai.api.internal.network.ktor.exception +package com.tddworks.common.network.api.ktor.internal.exception /** OpenAI client exception */ sealed class OpenAIException( diff --git a/openai-client/openai-client-core/src/commonMain/kotlin/com/tddworks/openai/api/internal/network/ktor/exception/OpenAIIOException.kt b/common/src/commonMain/kotlin/com/tddworks/common/network/api/ktor/internal/exception/OpenLLMIOException.kt similarity index 89% rename from openai-client/openai-client-core/src/commonMain/kotlin/com/tddworks/openai/api/internal/network/ktor/exception/OpenAIIOException.kt rename to common/src/commonMain/kotlin/com/tddworks/common/network/api/ktor/internal/exception/OpenLLMIOException.kt index 5984803..7f99470 100644 --- a/openai-client/openai-client-core/src/commonMain/kotlin/com/tddworks/openai/api/internal/network/ktor/exception/OpenAIIOException.kt +++ b/common/src/commonMain/kotlin/com/tddworks/common/network/api/ktor/internal/exception/OpenLLMIOException.kt @@ -1,4 +1,4 @@ -package com.tddworks.openai.api.internal.network.ktor.exception; +package com.tddworks.common.network.api.ktor.internal.exception; /** * An exception thrown in case of an I/O error diff --git a/openai-client/openai-client-core/src/jvmMain/kotlin/com/tddworks/openai/api/internal/network/ktor/DefaultHttpRequester.jvm.kt b/common/src/jvmMain/kotlin/com/tddworks/common/network/api/ktor/internal/DefaultHttpRequester.jvm.kt similarity index 68% rename from openai-client/openai-client-core/src/jvmMain/kotlin/com/tddworks/openai/api/internal/network/ktor/DefaultHttpRequester.jvm.kt rename to common/src/jvmMain/kotlin/com/tddworks/common/network/api/ktor/internal/DefaultHttpRequester.jvm.kt index 8d60e50..a57b5a1 100644 --- a/openai-client/openai-client-core/src/jvmMain/kotlin/com/tddworks/openai/api/internal/network/ktor/DefaultHttpRequester.jvm.kt +++ b/common/src/jvmMain/kotlin/com/tddworks/common/network/api/ktor/internal/DefaultHttpRequester.jvm.kt @@ -1,6 +1,6 @@ -package com.tddworks.openai.api.internal.network.ktor +package com.tddworks.common.network.api.ktor.internal -import com.tddworks.openai.api.internal.network.ktor.internal.createHttpClient +import com.tddworks.common.network.api.ktor.api.HttpRequester import io.ktor.client.engine.cio.* actual fun HttpRequester.Companion.default( diff --git a/common/src/jvmTest/kotlin/com/tddworks/common/network/api/InternalPackageTest.kt b/common/src/jvmTest/kotlin/com/tddworks/common/network/api/InternalPackageTest.kt new file mode 100644 index 0000000..d74387d --- /dev/null +++ b/common/src/jvmTest/kotlin/com/tddworks/common/network/api/InternalPackageTest.kt @@ -0,0 +1,65 @@ +package com.tddworks.common.network.api + +import com.tngtech.archunit.core.importer.ClassFileImporter +import com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.reflections.Reflections +import org.reflections.scanners.Scanners +import java.io.IOException + +class InternalPackageTest { + + companion object { + private val BASE_PACKAGE = InternalPackageTest::class.java.`package`.name + } + + private val analyzedClasses = ClassFileImporter().importPackages(BASE_PACKAGE) + + @Test + @Throws(IOException::class) + fun internalPackagesAreNotAccessedFromOutside() { + + // so that the test will break when the base package is re-named + assertPackageExists(BASE_PACKAGE) + val internalPackages = internalPackages(BASE_PACKAGE) + for (internalPackage in internalPackages) { + assertPackageExists(internalPackage) + assertPackageIsNotAccessedFromOutside(internalPackage) + } + } + + /** + * Finds all packages named "internal". + */ + private fun internalPackages(basePackage: String): List { + val scanner = Scanners.SubTypes + val reflections = Reflections(basePackage, scanner) + return reflections.getSubTypesOf(Object::class.java).map { + it.`package`.name + }.filter { + it.endsWith(".internal") + } + } + + private fun assertPackageIsNotAccessedFromOutside(internalPackage: String) { + noClasses() + .that() + .resideOutsideOfPackage(packageMatcher(internalPackage)) + .should() + .dependOnClassesThat() + .resideInAPackage(packageMatcher(internalPackage)) + .check(analyzedClasses) + } + + private fun assertPackageExists(packageName: String?) { + assertThat(analyzedClasses.containPackage(packageName)) + .`as`("package %s exists", packageName) + .isTrue() + } + + private fun packageMatcher(fullyQualifiedPackage: String): String? { + return "$fullyQualifiedPackage.." + } + +} diff --git a/common/src/jvmTest/kotlin/com/tddworks/common/network/api/JsonUtils.kt b/common/src/jvmTest/kotlin/com/tddworks/common/network/api/JsonUtils.kt new file mode 100644 index 0000000..cb84cae --- /dev/null +++ b/common/src/jvmTest/kotlin/com/tddworks/common/network/api/JsonUtils.kt @@ -0,0 +1,23 @@ +package com.tddworks.common.network.api + +import kotlinx.serialization.json.Json + +val prettyJson = Json { // this returns the JsonBuilder + prettyPrint = true + ignoreUnknownKeys = true + // optional: specify indent + prettyPrintIndent = " " +} + +/** + * Represents a JSON object that allows for leniency and ignores unknown keys. + * + * @property isLenient Removes JSON specification restriction (RFC-4627) and makes parser more liberal to the malformed input. In lenient mode quoted boolean literals, and unquoted string literals are allowed. + * Its relaxations can be expanded in the future, so that lenient parser becomes even more permissive to invalid value in the input, replacing them with defaults. + * false by default. + * @property ignoreUnknownKeys Specifies whether encounters of unknown properties in the input JSON should be ignored instead of throwing SerializationException. false by default.. + */ +internal val JsonLenient = Json { + isLenient = true + ignoreUnknownKeys = true +} \ No newline at end of file diff --git a/common/src/jvmTest/kotlin/com/tddworks/common/network/api/MockHttpClient.kt b/common/src/jvmTest/kotlin/com/tddworks/common/network/api/MockHttpClient.kt new file mode 100644 index 0000000..cbe5653 --- /dev/null +++ b/common/src/jvmTest/kotlin/com/tddworks/common/network/api/MockHttpClient.kt @@ -0,0 +1,44 @@ +package com.tddworks.common.network.api + + +import io.ktor.client.* +import io.ktor.client.engine.mock.* +import io.ktor.client.plugins.* +import io.ktor.client.plugins.contentnegotiation.* +import io.ktor.client.request.* +import io.ktor.http.* +import io.ktor.serialization.kotlinx.* + +/** + * See https://ktor.io/docs/http-client-testing.html#usage + */ +fun mockHttpClient(mockResponse: String) = HttpClient(MockEngine) { + + val headers = headersOf("Content-Type" to listOf(ContentType.Application.Json.toString())) + + install(ContentNegotiation) { + register(ContentType.Application.Json, KotlinxSerializationConverter(JsonLenient)) + } + + engine { + addHandler { request -> + if (request.url.encodedPath == "/v1/chat/completions" + || request.url.encodedPath == "/v1/images/generations" + ) { + respond(mockResponse, HttpStatusCode.OK, headers) + } else { + error("Unhandled ${request.url.encodedPath}") + } + } + } + + defaultRequest { + url { + protocol = URLProtocol.HTTPS + host = "api.lemonsqueezy.com" + } + + header(HttpHeaders.ContentType, ContentType.Application.Json) + contentType(ContentType.Application.Json) + } +} \ No newline at end of file diff --git a/openai-client/openai-client-core/src/jvmTest/kotlin/com/tddworks/openai/api/internal/network/ktor/DefaultHttpRequesterTest.kt b/common/src/jvmTest/kotlin/com/tddworks/common/network/api/ktor/DefaultHttpRequesterTest.kt similarity index 85% rename from openai-client/openai-client-core/src/jvmTest/kotlin/com/tddworks/openai/api/internal/network/ktor/DefaultHttpRequesterTest.kt rename to common/src/jvmTest/kotlin/com/tddworks/common/network/api/ktor/DefaultHttpRequesterTest.kt index 949eb1f..13f44e8 100644 --- a/openai-client/openai-client-core/src/jvmTest/kotlin/com/tddworks/openai/api/internal/network/ktor/DefaultHttpRequesterTest.kt +++ b/common/src/jvmTest/kotlin/com/tddworks/common/network/api/ktor/DefaultHttpRequesterTest.kt @@ -1,6 +1,8 @@ -package com.tddworks.openai.api.internal.network.ktor +package com.tddworks.common.network.api.ktor -import com.tddworks.openai.api.common.mockHttpClient +import com.tddworks.common.network.api.ktor.internal.DefaultHttpRequester +import com.tddworks.common.network.api.ktor.api.performRequest +import com.tddworks.common.network.api.mockHttpClient import io.ktor.client.* import io.ktor.client.request.* import kotlinx.coroutines.runBlocking diff --git a/openai-client/openai-client-core/src/macosMain/kotlin/com/tddworks/openai/api/internal/network/ktor/DefaultHttpRequester.macos.kt b/common/src/macosMain/kotlin/com/tddworks/common/network/api/ktor/internal/DefaultHttpRequester.macos.kt similarity index 68% rename from openai-client/openai-client-core/src/macosMain/kotlin/com/tddworks/openai/api/internal/network/ktor/DefaultHttpRequester.macos.kt rename to common/src/macosMain/kotlin/com/tddworks/common/network/api/ktor/internal/DefaultHttpRequester.macos.kt index 637b33c..24766cb 100644 --- a/openai-client/openai-client-core/src/macosMain/kotlin/com/tddworks/openai/api/internal/network/ktor/DefaultHttpRequester.macos.kt +++ b/common/src/macosMain/kotlin/com/tddworks/common/network/api/ktor/internal/DefaultHttpRequester.macos.kt @@ -1,8 +1,9 @@ -package com.tddworks.openai.api.internal.network.ktor +package com.tddworks.common.network.api.ktor.internal -import com.tddworks.openai.api.internal.network.ktor.internal.createHttpClient +import com.tddworks.common.network.api.ktor.api.HttpRequester import io.ktor.client.engine.darwin.* + actual fun HttpRequester.Companion.default( url: String, token: String, diff --git a/openai-client/openai-client-core/build.gradle.kts b/openai-client/openai-client-core/build.gradle.kts index 9ecb701..d4cd1a4 100644 --- a/openai-client/openai-client-core/build.gradle.kts +++ b/openai-client/openai-client-core/build.gradle.kts @@ -16,6 +16,7 @@ kotlin { // put your Multiplatform dependencies here api(libs.kotlinx.serialization.json) api(libs.bundles.ktor.client) + api(projects.common) } commonTest.dependencies { diff --git a/openai-client/openai-client-core/src/commonMain/kotlin/com/tddworks/openai/api/OpenAI.kt b/openai-client/openai-client-core/src/commonMain/kotlin/com/tddworks/openai/api/OpenAI.kt index c09f0f1..d849365 100644 --- a/openai-client/openai-client-core/src/commonMain/kotlin/com/tddworks/openai/api/OpenAI.kt +++ b/openai-client/openai-client-core/src/commonMain/kotlin/com/tddworks/openai/api/OpenAI.kt @@ -5,9 +5,9 @@ import com.tddworks.openai.api.chat.api.Chat import com.tddworks.openai.api.chat.internal.DefaultChatApi import com.tddworks.openai.api.images.api.Images import com.tddworks.openai.api.images.internal.DefaultImagesApi -import com.tddworks.openai.api.internal.network.ktor.HttpRequester -import com.tddworks.openai.api.internal.network.ktor.default -import com.tddworks.openai.api.internal.network.ktor.internal.createHttpClient +import com.tddworks.common.network.api.ktor.api.HttpRequester +import com.tddworks.common.network.api.ktor.internal.default +import com.tddworks.common.network.api.ktor.internal.createHttpClient import io.ktor.client.engine.* interface OpenAI : Chat, Images { diff --git a/openai-client/openai-client-core/src/commonMain/kotlin/com/tddworks/openai/api/chat/api/StreamableRequest.kt b/openai-client/openai-client-core/src/commonMain/kotlin/com/tddworks/openai/api/chat/api/StreamableRequest.kt index 3edd23e..2ea750f 100644 --- a/openai-client/openai-client-core/src/commonMain/kotlin/com/tddworks/openai/api/chat/api/StreamableRequest.kt +++ b/openai-client/openai-client-core/src/commonMain/kotlin/com/tddworks/openai/api/chat/api/StreamableRequest.kt @@ -1,6 +1,6 @@ package com.tddworks.openai.api.chat.api -import com.tddworks.openai.api.common.JsonLenient +import com.tddworks.common.network.api.ktor.internal.JsonLenient import io.ktor.client.request.* import io.ktor.util.reflect.* import kotlinx.serialization.Serializable diff --git a/openai-client/openai-client-core/src/commonMain/kotlin/com/tddworks/openai/api/chat/internal/DefaultChatApi.kt b/openai-client/openai-client-core/src/commonMain/kotlin/com/tddworks/openai/api/chat/internal/DefaultChatApi.kt index 637e4c6..bded9f1 100644 --- a/openai-client/openai-client-core/src/commonMain/kotlin/com/tddworks/openai/api/chat/internal/DefaultChatApi.kt +++ b/openai-client/openai-client-core/src/commonMain/kotlin/com/tddworks/openai/api/chat/internal/DefaultChatApi.kt @@ -5,9 +5,9 @@ import com.tddworks.openai.api.chat.api.Chat import com.tddworks.openai.api.chat.api.Chat.Companion.CHAT_COMPLETIONS_PATH import com.tddworks.openai.api.chat.api.ChatCompletion import com.tddworks.openai.api.chat.api.ChatCompletionRequest -import com.tddworks.openai.api.internal.network.ktor.HttpRequester -import com.tddworks.openai.api.internal.network.ktor.performRequest -import com.tddworks.openai.api.internal.network.ktor.streamRequest +import com.tddworks.common.network.api.ktor.api.HttpRequester +import com.tddworks.common.network.api.ktor.api.performRequest +import com.tddworks.common.network.api.ktor.api.streamRequest import io.ktor.client.request.* import io.ktor.http.* import kotlinx.coroutines.flow.Flow diff --git a/openai-client/openai-client-core/src/commonMain/kotlin/com/tddworks/openai/api/images/api/Images.kt b/openai-client/openai-client-core/src/commonMain/kotlin/com/tddworks/openai/api/images/api/Images.kt index 3f49e20..1321666 100644 --- a/openai-client/openai-client-core/src/commonMain/kotlin/com/tddworks/openai/api/images/api/Images.kt +++ b/openai-client/openai-client-core/src/commonMain/kotlin/com/tddworks/openai/api/images/api/Images.kt @@ -1,6 +1,7 @@ package com.tddworks.openai.api.images.api -import com.tddworks.openai.api.common.ListResponse +import com.tddworks.common.network.api.ktor.api.ListResponse + /** * Given a prompt and/or an input image, the model will generate a new image. diff --git a/openai-client/openai-client-core/src/commonMain/kotlin/com/tddworks/openai/api/images/internal/DefaultImagesApi.kt b/openai-client/openai-client-core/src/commonMain/kotlin/com/tddworks/openai/api/images/internal/DefaultImagesApi.kt index eb91c1b..3786f75 100644 --- a/openai-client/openai-client-core/src/commonMain/kotlin/com/tddworks/openai/api/images/internal/DefaultImagesApi.kt +++ b/openai-client/openai-client-core/src/commonMain/kotlin/com/tddworks/openai/api/images/internal/DefaultImagesApi.kt @@ -1,11 +1,11 @@ package com.tddworks.openai.api.images.internal -import com.tddworks.openai.api.common.ListResponse +import com.tddworks.common.network.api.ktor.api.HttpRequester +import com.tddworks.common.network.api.ktor.api.ListResponse +import com.tddworks.common.network.api.ktor.api.performRequest import com.tddworks.openai.api.images.api.Image import com.tddworks.openai.api.images.api.ImageCreate import com.tddworks.openai.api.images.api.Images -import com.tddworks.openai.api.internal.network.ktor.HttpRequester -import com.tddworks.openai.api.internal.network.ktor.performRequest import io.ktor.client.request.* import io.ktor.http.* diff --git a/openai-client/openai-client-core/src/commonMain/kotlin/com/tddworks/openai/api/internal/network/ktor/Stream.kt b/openai-client/openai-client-core/src/commonMain/kotlin/com/tddworks/openai/api/internal/network/ktor/Stream.kt deleted file mode 100644 index 94e2b3e..0000000 --- a/openai-client/openai-client-core/src/commonMain/kotlin/com/tddworks/openai/api/internal/network/ktor/Stream.kt +++ /dev/null @@ -1,41 +0,0 @@ -package com.tddworks.openai.api.internal.network.ktor - -import io.ktor.client.call.* -import io.ktor.client.statement.* -import io.ktor.utils.io.* -import kotlinx.coroutines.flow.FlowCollector -import kotlinx.serialization.json.Json - -private const val STREAM_PREFIX = "data:" -private const val STREAM_END_TOKEN = "$STREAM_PREFIX [DONE]" - -/** - * Get data as [Server-Sent Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#Event_stream_format). - */ -internal suspend inline fun FlowCollector.streamEventsFrom(response: HttpResponse) { - val channel: ByteReadChannel = response.body() - while (!channel.isClosedForRead) { - channel.readUTF8Line()?.let { streamResponse -> - if (notEndStreamResponse(streamResponse)) { - emit(JsonLenient.decodeFromString(streamResponse.removePrefix(STREAM_PREFIX))) - } - } ?: break - } -} - -private fun isStreamResponse(line: String) = line.startsWith(STREAM_PREFIX) - -private fun notEndStreamResponse(line: String) = line != STREAM_END_TOKEN && isStreamResponse(line) - -/** - * Represents a JSON object that allows for leniency and ignores unknown keys. - * - * @property isLenient Removes JSON specification restriction (RFC-4627) and makes parser more liberal to the malformed input. In lenient mode quoted boolean literals, and unquoted string literals are allowed. - * Its relaxations can be expanded in the future, so that lenient parser becomes even more permissive to invalid value in the input, replacing them with defaults. - * false by default. - * @property ignoreUnknownKeys Specifies whether encounters of unknown properties in the input JSON should be ignored instead of throwing SerializationException. false by default.. - */ -internal val JsonLenient = Json { - isLenient = true - ignoreUnknownKeys = true -} diff --git a/openai-client/openai-client-core/src/jvmMain/kotlin/com/tddworks/openai/api/JvmOpenAI.kt b/openai-client/openai-client-core/src/jvmMain/kotlin/com/tddworks/openai/api/JvmOpenAI.kt index 4de5a53..22f6e54 100644 --- a/openai-client/openai-client-core/src/jvmMain/kotlin/com/tddworks/openai/api/JvmOpenAI.kt +++ b/openai-client/openai-client-core/src/jvmMain/kotlin/com/tddworks/openai/api/JvmOpenAI.kt @@ -1,8 +1,7 @@ package com.tddworks.openai.api -import com.tddworks.openai.api.internal.network.ktor.HttpRequester -import com.tddworks.openai.api.internal.network.ktor.default -import io.ktor.client.engine.cio.* +import com.tddworks.common.network.api.ktor.api.HttpRequester +import com.tddworks.common.network.api.ktor.internal.default fun JvmOpenAI(token: String): OpenAI = OpenAIApi( HttpRequester.default( diff --git a/openai-client/openai-client-core/src/jvmTest/kotlin/com/tddworks/openai/api/chat/internal/DefaultChatApiTest.kt b/openai-client/openai-client-core/src/jvmTest/kotlin/com/tddworks/openai/api/chat/internal/DefaultChatApiTest.kt index a1f8977..ae8c23f 100644 --- a/openai-client/openai-client-core/src/jvmTest/kotlin/com/tddworks/openai/api/chat/internal/DefaultChatApiTest.kt +++ b/openai-client/openai-client-core/src/jvmTest/kotlin/com/tddworks/openai/api/chat/internal/DefaultChatApiTest.kt @@ -3,7 +3,7 @@ package com.tddworks.openai.api.chat.internal import app.cash.turbine.test import com.tddworks.openai.api.chat.api.* import com.tddworks.openai.api.common.mockHttpClient -import com.tddworks.openai.api.internal.network.ktor.DefaultHttpRequester +import com.tddworks.common.network.api.ktor.internal.DefaultHttpRequester import kotlinx.coroutines.runBlocking import kotlinx.serialization.ExperimentalSerializationApi import org.junit.jupiter.api.Assertions.assertEquals diff --git a/openai-client/openai-client-core/src/jvmTest/kotlin/com/tddworks/openai/api/images/internal/DefaultImagesApiTest.kt b/openai-client/openai-client-core/src/jvmTest/kotlin/com/tddworks/openai/api/images/internal/DefaultImagesApiTest.kt index 8dd6123..b684054 100644 --- a/openai-client/openai-client-core/src/jvmTest/kotlin/com/tddworks/openai/api/images/internal/DefaultImagesApiTest.kt +++ b/openai-client/openai-client-core/src/jvmTest/kotlin/com/tddworks/openai/api/images/internal/DefaultImagesApiTest.kt @@ -3,7 +3,7 @@ package com.tddworks.openai.api.images.internal import com.tddworks.openai.api.chat.api.Model import com.tddworks.openai.api.common.mockHttpClient import com.tddworks.openai.api.images.api.ImageCreate -import com.tddworks.openai.api.internal.network.ktor.DefaultHttpRequester +import com.tddworks.common.network.api.ktor.internal.DefaultHttpRequester import kotlinx.coroutines.runBlocking import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test diff --git a/openai-client/openai-client-darwin/src/appleMain/kotlin/com/tddworks/openai/darwin/api/DarwinOpenAI.kt b/openai-client/openai-client-darwin/src/appleMain/kotlin/com/tddworks/openai/darwin/api/DarwinOpenAI.kt index d088a1e..5be41d2 100644 --- a/openai-client/openai-client-darwin/src/appleMain/kotlin/com/tddworks/openai/darwin/api/DarwinOpenAI.kt +++ b/openai-client/openai-client-darwin/src/appleMain/kotlin/com/tddworks/openai/darwin/api/DarwinOpenAI.kt @@ -2,8 +2,8 @@ package com.tddworks.openai.darwin.api import com.tddworks.openai.api.OpenAI import com.tddworks.openai.api.OpenAIApi -import com.tddworks.openai.api.internal.network.ktor.HttpRequester -import com.tddworks.openai.api.internal.network.ktor.default +import com.tddworks.common.network.api.ktor.api.HttpRequester +import com.tddworks.common.network.api.ktor.internal.default object DarwinOpenAI { /** diff --git a/settings.gradle.kts b/settings.gradle.kts index 6ad99a3..02fc760 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -18,9 +18,14 @@ dependencyResolutionManagement { rootProject.name = "openai-kotlin" enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") +include(":common") + //include(":library") include(":openai-client") include(":openai-client:openai-client-core") include(":openai-client:openai-client-darwin") //include(":openai-client:openai-client-ios") include(":openai-client:openai-client-cio") + +include(":anthropic-client") +include(":anthropic-client:anthropic-client-core")