From b95c509c2ec282593540512db7cab5181bb4d476 Mon Sep 17 00:00:00 2001 From: slam Date: Sat, 16 Mar 2024 14:28:59 +0800 Subject: [PATCH] feat(BE-174): integrate anthropic api - code refactor. - make create api working. --- .../com/tddworks/anthropic/api/Anthropic.kt | 35 ++++++++ .../api/messages/api/AnthropicConfig.kt | 6 ++ .../api/messages/api/CreateMessageRequest.kt | 12 +-- .../anthropic/api/messages/api/Messages.kt | 12 +++ .../api/messages/api/StreamMessageRequest.kt | 5 ++ ...reamModule.kt => StreamMessageResponse.kt} | 36 ++++---- .../api/internal/DefaultMessagesApi.kt | 26 +++--- .../api/messages/api/internal/JsonLenient.kt | 4 +- .../api/internal/json/AnthropicModule.kt | 37 ++++++++ .../api/internal/json/SerializersConfig.kt | 89 ------------------- .../com/tddworks/anthropic/api/JsonUtils.kt | 29 +++++- .../messages/api/CreateMessageRequestTest.kt | 33 +++++++ .../api/internal/DefaultMessagesApiTest.kt | 23 ++--- .../common/network/api/ktor/api/Stream.kt | 6 +- .../network/api/ktor/internal/HttpClient.kt | 19 ++-- .../network/api/ktor/internal/JsonLenient.kt | 27 ++++++ .../commonMain/kotlin/com/tddworks/di/Koin.kt | 1 - .../ktor/internal/DefaultHttpRequester.jvm.kt | 2 +- .../internal/DefaultHttpRequester.macos.kt | 2 +- .../kotlin/com/tddworks/openai/api/OpenAI.kt | 3 +- 20 files changed, 250 insertions(+), 157 deletions(-) create mode 100644 anthropic-client/anthropic-client-core/src/commonMain/kotlin/com/tddworks/anthropic/api/Anthropic.kt create mode 100644 anthropic-client/anthropic-client-core/src/commonMain/kotlin/com/tddworks/anthropic/api/messages/api/AnthropicConfig.kt create mode 100644 anthropic-client/anthropic-client-core/src/commonMain/kotlin/com/tddworks/anthropic/api/messages/api/Messages.kt create mode 100644 anthropic-client/anthropic-client-core/src/commonMain/kotlin/com/tddworks/anthropic/api/messages/api/StreamMessageRequest.kt rename anthropic-client/anthropic-client-core/src/commonMain/kotlin/com/tddworks/anthropic/api/messages/api/{stream/StreamModule.kt => StreamMessageResponse.kt} (66%) create mode 100644 anthropic-client/anthropic-client-core/src/commonMain/kotlin/com/tddworks/anthropic/api/messages/api/internal/json/AnthropicModule.kt delete mode 100644 anthropic-client/anthropic-client-core/src/commonMain/kotlin/com/tddworks/anthropic/api/messages/api/internal/json/SerializersConfig.kt create mode 100644 anthropic-client/anthropic-client-core/src/jvmTest/kotlin/com/tddworks/anthropic/api/messages/api/CreateMessageRequestTest.kt diff --git a/anthropic-client/anthropic-client-core/src/commonMain/kotlin/com/tddworks/anthropic/api/Anthropic.kt b/anthropic-client/anthropic-client-core/src/commonMain/kotlin/com/tddworks/anthropic/api/Anthropic.kt new file mode 100644 index 0000000..31e9d9b --- /dev/null +++ b/anthropic-client/anthropic-client-core/src/commonMain/kotlin/com/tddworks/anthropic/api/Anthropic.kt @@ -0,0 +1,35 @@ +package com.tddworks.anthropic.api + +import com.tddworks.anthropic.api.Anthropic.Companion.BASE_URL +import com.tddworks.anthropic.api.messages.api.AnthropicConfig +import com.tddworks.anthropic.api.messages.api.Messages +import com.tddworks.anthropic.api.messages.api.internal.DefaultMessagesApi +import com.tddworks.common.network.api.ktor.api.HttpRequester +import com.tddworks.common.network.api.ktor.internal.createHttpClient +import com.tddworks.common.network.api.ktor.internal.default +import io.ktor.client.engine.* + +interface Anthropic : Messages { + companion object { + const val BASE_URL = "klaude.asusual.life" + } +} + +fun Anthropic(apiKey: String, engine: HttpClientEngineFactory): Anthropic = AnthropicApi( + apiKey = apiKey, + HttpRequester.default( + createHttpClient( + url = BASE_URL, engine = engine + ) + ), +) + +class AnthropicApi( + private val apiKey: String, + private val requester: HttpRequester, +) : Anthropic, Messages by DefaultMessagesApi( + AnthropicConfig( + apiKey = apiKey + ), + requester +) \ No newline at end of file diff --git a/anthropic-client/anthropic-client-core/src/commonMain/kotlin/com/tddworks/anthropic/api/messages/api/AnthropicConfig.kt b/anthropic-client/anthropic-client-core/src/commonMain/kotlin/com/tddworks/anthropic/api/messages/api/AnthropicConfig.kt new file mode 100644 index 0000000..66cff6b --- /dev/null +++ b/anthropic-client/anthropic-client-core/src/commonMain/kotlin/com/tddworks/anthropic/api/messages/api/AnthropicConfig.kt @@ -0,0 +1,6 @@ +package com.tddworks.anthropic.api.messages.api + +data class AnthropicConfig( + val apiKey: String = "CONFIG_API_KEY", + val anthropicVersion: String = "2023-06-01", +) \ No newline at end of file diff --git a/anthropic-client/anthropic-client-core/src/commonMain/kotlin/com/tddworks/anthropic/api/messages/api/CreateMessageRequest.kt b/anthropic-client/anthropic-client-core/src/commonMain/kotlin/com/tddworks/anthropic/api/messages/api/CreateMessageRequest.kt index 8054123..fa93e79 100644 --- a/anthropic-client/anthropic-client-core/src/commonMain/kotlin/com/tddworks/anthropic/api/messages/api/CreateMessageRequest.kt +++ b/anthropic-client/anthropic-client-core/src/commonMain/kotlin/com/tddworks/anthropic/api/messages/api/CreateMessageRequest.kt @@ -1,19 +1,21 @@ package com.tddworks.anthropic.api.messages.api -import com.tddworks.common.network.api.StreamableRequest -import com.tddworks.openllm.api.ChatRequest +import com.tddworks.anthropic.api.Model import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable -@SerialName("CreateMessageRequest") data class CreateMessageRequest( val messages: List, val systemPrompt: String? = null, -) : ChatRequest, StreamableRequest { + @SerialName("max_tokens") + val maxTokens: Int = 1024, + @SerialName("model") + val model: Model = Model.CLAUDE_3_HAIKU, +) : StreamMessageRequest { companion object { fun streamRequest(messages: List, systemPrompt: String? = null) = - CreateMessageRequest(messages, systemPrompt) as StreamableRequest + CreateMessageRequest(messages, systemPrompt) as StreamMessageRequest } } diff --git a/anthropic-client/anthropic-client-core/src/commonMain/kotlin/com/tddworks/anthropic/api/messages/api/Messages.kt b/anthropic-client/anthropic-client-core/src/commonMain/kotlin/com/tddworks/anthropic/api/messages/api/Messages.kt new file mode 100644 index 0000000..d6f5730 --- /dev/null +++ b/anthropic-client/anthropic-client-core/src/commonMain/kotlin/com/tddworks/anthropic/api/messages/api/Messages.kt @@ -0,0 +1,12 @@ +package com.tddworks.anthropic.api.messages.api + +import kotlinx.coroutines.flow.Flow + +/** + * * Anthropic Messages API -https://docs.anthropic.com/claude/reference/messages_post + */ +interface Messages { + suspend fun create(request: CreateMessageRequest): CreateMessageResponse + + fun stream(request: StreamMessageRequest): Flow +} \ No newline at end of file diff --git a/anthropic-client/anthropic-client-core/src/commonMain/kotlin/com/tddworks/anthropic/api/messages/api/StreamMessageRequest.kt b/anthropic-client/anthropic-client-core/src/commonMain/kotlin/com/tddworks/anthropic/api/messages/api/StreamMessageRequest.kt new file mode 100644 index 0000000..d7d8f58 --- /dev/null +++ b/anthropic-client/anthropic-client-core/src/commonMain/kotlin/com/tddworks/anthropic/api/messages/api/StreamMessageRequest.kt @@ -0,0 +1,5 @@ +package com.tddworks.anthropic.api.messages.api + +import com.tddworks.common.network.api.StreamableRequest + +interface StreamMessageRequest : StreamableRequest \ No newline at end of file diff --git a/anthropic-client/anthropic-client-core/src/commonMain/kotlin/com/tddworks/anthropic/api/messages/api/stream/StreamModule.kt b/anthropic-client/anthropic-client-core/src/commonMain/kotlin/com/tddworks/anthropic/api/messages/api/StreamMessageResponse.kt similarity index 66% rename from anthropic-client/anthropic-client-core/src/commonMain/kotlin/com/tddworks/anthropic/api/messages/api/stream/StreamModule.kt rename to anthropic-client/anthropic-client-core/src/commonMain/kotlin/com/tddworks/anthropic/api/messages/api/StreamMessageResponse.kt index d6ca938..b8bc03f 100644 --- a/anthropic-client/anthropic-client-core/src/commonMain/kotlin/com/tddworks/anthropic/api/messages/api/stream/StreamModule.kt +++ b/anthropic-client/anthropic-client-core/src/commonMain/kotlin/com/tddworks/anthropic/api/messages/api/StreamMessageResponse.kt @@ -1,67 +1,63 @@ -package com.tddworks.anthropic.api.messages.api.stream +package com.tddworks.anthropic.api.messages.api -import com.tddworks.anthropic.api.messages.api.CreateMessageResponse -import com.tddworks.anthropic.api.messages.api.Usage -import com.tddworks.common.network.api.StreamChatResponse +import com.tddworks.anthropic.api.messages.api.internal.json.StreamMessageResponseSerializer import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable -@SerialName("message_start") + +@Serializable(with = StreamMessageResponseSerializer::class) +sealed interface StreamMessageResponse { + val type: String +} + @Serializable data class MessageStart( override val type: String, val message: CreateMessageResponse, -) : StreamChatResponse +) : StreamMessageResponse @Serializable -@SerialName("content_block_start") data class ContentBlockStart( override val type: String, val index: Int, @SerialName("content_block") val contentBlock: ContentBlock, -) : StreamChatResponse +) : StreamMessageResponse @Serializable -@SerialName("content_block_delta") data class ContentBlock( override val type: String, val text: String, -) : StreamChatResponse +) : StreamMessageResponse @Serializable -@SerialName("content_block_delta") data class ContentBlockDelta( override val type: String, val index: Int, val delta: Delta, -) : StreamChatResponse +) : StreamMessageResponse @Serializable -@SerialName("content_block_stop") data class ContentBlockStop( override val type: String, val index: Int, -) : StreamChatResponse +) : StreamMessageResponse @Serializable -@SerialName("message_delta") data class MessageDelta( override val type: String, val delta: Delta, -) : StreamChatResponse +) : StreamMessageResponse @Serializable -@SerialName("message_stop") data class MessageStop( override val type: String, -) : StreamChatResponse +) : StreamMessageResponse @Serializable -@SerialName("ping") data class Ping( override val type: String, -) : StreamChatResponse +) : StreamMessageResponse /** * { diff --git a/anthropic-client/anthropic-client-core/src/commonMain/kotlin/com/tddworks/anthropic/api/messages/api/internal/DefaultMessagesApi.kt b/anthropic-client/anthropic-client-core/src/commonMain/kotlin/com/tddworks/anthropic/api/messages/api/internal/DefaultMessagesApi.kt index 67f0d41..4d05239 100644 --- a/anthropic-client/anthropic-client-core/src/commonMain/kotlin/com/tddworks/anthropic/api/messages/api/internal/DefaultMessagesApi.kt +++ b/anthropic-client/anthropic-client-core/src/commonMain/kotlin/com/tddworks/anthropic/api/messages/api/internal/DefaultMessagesApi.kt @@ -1,28 +1,25 @@ package com.tddworks.anthropic.api.messages.api.internal -import com.tddworks.common.network.api.StreamChatResponse -import com.tddworks.common.network.api.StreamableRequest +import com.tddworks.anthropic.api.messages.api.* 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 com.tddworks.openllm.api.ChatApi -import com.tddworks.openllm.api.ChatRequest -import com.tddworks.openllm.api.ChatResponse import io.ktor.client.request.* import io.ktor.http.* import kotlinx.coroutines.flow.Flow import kotlinx.serialization.json.Json /** - * * Anthropic Messages API -https://docs.anthropic.com/claude/reference/messages_post + * * Anthropic Messages API - https://docs.anthropic.com/claude/reference/messages_post */ class DefaultMessagesApi( + private val anthropicConfig: AnthropicConfig = AnthropicConfig(), private val requester: HttpRequester, private val jsonLenient: Json = JsonLenient, -) : ChatApi { +) : Messages { - override fun chat(request: StreamableRequest): Flow { - return requester.streamRequest { + override fun stream(request: StreamMessageRequest): Flow { + return requester.streamRequest { method = HttpMethod.Post url(path = CHAT_COMPLETIONS_PATH) setBody(request.asStreamRequest(jsonLenient)) @@ -31,6 +28,8 @@ class DefaultMessagesApi( headers { append(HttpHeaders.CacheControl, "no-cache") append(HttpHeaders.Connection, "keep-alive") + append("anthropic-version", anthropicConfig.anthropicVersion) + append("x-api-key", anthropicConfig.apiKey) } } } @@ -40,12 +39,17 @@ class DefaultMessagesApi( * @param request Send a structured list of input messages with text and/or image content, and the model will generate the next message in the conversation. * @return The chat completion. */ - override suspend fun chat(request: ChatRequest): ChatResponse { - return requester.performRequest { + override suspend fun create(request: CreateMessageRequest): CreateMessageResponse { + return requester.performRequest { method = HttpMethod.Post url(path = CHAT_COMPLETIONS_PATH) setBody(request) contentType(ContentType.Application.Json) + // anthropic API uses API key and + headers { + append("anthropic-version", anthropicConfig.anthropicVersion) + append("x-api-key", anthropicConfig.apiKey) + } } } diff --git a/anthropic-client/anthropic-client-core/src/commonMain/kotlin/com/tddworks/anthropic/api/messages/api/internal/JsonLenient.kt b/anthropic-client/anthropic-client-core/src/commonMain/kotlin/com/tddworks/anthropic/api/messages/api/internal/JsonLenient.kt index 1b9c8f5..6a836d0 100644 --- a/anthropic-client/anthropic-client-core/src/commonMain/kotlin/com/tddworks/anthropic/api/messages/api/internal/JsonLenient.kt +++ b/anthropic-client/anthropic-client-core/src/commonMain/kotlin/com/tddworks/anthropic/api/messages/api/internal/JsonLenient.kt @@ -1,6 +1,6 @@ package com.tddworks.anthropic.api.messages.api.internal -import com.tddworks.anthropic.api.messages.api.internal.json.chatModule +import com.tddworks.anthropic.api.messages.api.internal.json.anthropicModule import kotlinx.serialization.json.Json @@ -17,5 +17,5 @@ val JsonLenient = Json { ignoreUnknownKeys = true // https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/json.md#class-discriminator-for-polymorphism classDiscriminator = "#class" - serializersModule = chatModule + serializersModule = anthropicModule } diff --git a/anthropic-client/anthropic-client-core/src/commonMain/kotlin/com/tddworks/anthropic/api/messages/api/internal/json/AnthropicModule.kt b/anthropic-client/anthropic-client-core/src/commonMain/kotlin/com/tddworks/anthropic/api/messages/api/internal/json/AnthropicModule.kt new file mode 100644 index 0000000..076bb47 --- /dev/null +++ b/anthropic-client/anthropic-client-core/src/commonMain/kotlin/com/tddworks/anthropic/api/messages/api/internal/json/AnthropicModule.kt @@ -0,0 +1,37 @@ +package com.tddworks.anthropic.api.messages.api.internal.json + +import com.tddworks.anthropic.api.messages.api.* +import com.tddworks.common.network.api.StreamableRequest +import kotlinx.serialization.KSerializer +import kotlinx.serialization.json.JsonContentPolymorphicSerializer +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.modules.SerializersModule +import kotlinx.serialization.modules.polymorphic + +val anthropicModule = SerializersModule { + + polymorphic(StreamableRequest::class) { + subclass(CreateMessageRequest::class, CreateMessageRequest.serializer()) + defaultDeserializer { CreateMessageRequest.serializer() } + } +} + +object StreamMessageResponseSerializer : + JsonContentPolymorphicSerializer(StreamMessageResponse::class) { + override fun selectDeserializer(element: JsonElement): KSerializer { + val type = element.jsonObject["type"]?.jsonPrimitive?.content + + return when (type) { + "message_start" -> MessageStart.serializer() + "content_block_start" -> ContentBlockStart.serializer() + "content_block_delta" -> ContentBlockDelta.serializer() + "content_block_stop" -> ContentBlockStop.serializer() + "message_delta" -> MessageDelta.serializer() + "message_stop" -> MessageStop.serializer() + "ping" -> Ping.serializer() + else -> throw IllegalArgumentException("Unknown type of message") + } + } +} \ No newline at end of file diff --git a/anthropic-client/anthropic-client-core/src/commonMain/kotlin/com/tddworks/anthropic/api/messages/api/internal/json/SerializersConfig.kt b/anthropic-client/anthropic-client-core/src/commonMain/kotlin/com/tddworks/anthropic/api/messages/api/internal/json/SerializersConfig.kt deleted file mode 100644 index 8ed925d..0000000 --- a/anthropic-client/anthropic-client-core/src/commonMain/kotlin/com/tddworks/anthropic/api/messages/api/internal/json/SerializersConfig.kt +++ /dev/null @@ -1,89 +0,0 @@ -package com.tddworks.anthropic.api.messages.api.internal.json - -import com.tddworks.anthropic.api.messages.api.CreateMessageRequest -import com.tddworks.anthropic.api.messages.api.CreateMessageResponse -import com.tddworks.anthropic.api.messages.api.stream.* -import com.tddworks.common.network.api.StreamChatResponse -import com.tddworks.common.network.api.StreamableRequest -import com.tddworks.openllm.api.ChatRequest -import com.tddworks.openllm.api.ChatResponse -import kotlinx.serialization.KSerializer -import kotlinx.serialization.SerializationStrategy -import kotlinx.serialization.json.JsonContentPolymorphicSerializer -import kotlinx.serialization.json.JsonElement -import kotlinx.serialization.json.jsonObject -import kotlinx.serialization.json.jsonPrimitive -import kotlinx.serialization.modules.SerializersModule -import kotlinx.serialization.modules.polymorphic - -val chatModule = SerializersModule { - -// polymorphicDefaultSerializer(StreamableRequest::class) { instance -> -// @Suppress("UNCHECKED_CAST") -// when (instance) { -// is CreateMessageRequest -> CreateMessageRequest.serializer() as SerializationStrategy -// else -> null -// } -// } - - polymorphic(StreamableRequest::class) { - subclass(CreateMessageRequest::class, CreateMessageRequest.serializer()) - defaultDeserializer { CreateMessageRequest.serializer() } - } -// - polymorphic(ChatRequest::class) { - subclass(CreateMessageRequest::class, CreateMessageRequest.serializer()) - defaultDeserializer { CreateMessageRequest.serializer() } - } -// -// polymorphic(ChatResponse::class) { -// subclass(CreateMessageResponse::class, CreateMessageResponse.serializer()) -// defaultDeserializer { ChatResponseSerializer } -// } - - polymorphic(StreamChatResponse::class) { - defaultDeserializer { StreamChatResponseSerializer } - } - -// polymorphicDefaultSerializer(ChatResponse::class) { instance -> -// @Suppress("UNCHECKED_CAST") -// when (instance) { -// is CreateMessageResponse -> CreateMessageResponse.serializer() as SerializationStrategy -// else -> null -// } -// } -// - polymorphic(ChatResponse::class) { - defaultDeserializer { ChatResponseSerializer } - } -} - -object ChatResponseSerializer : - JsonContentPolymorphicSerializer(ChatResponse::class) { - override fun selectDeserializer(element: JsonElement): KSerializer { - val content = element.jsonObject["content"] - - return when { - content != null -> CreateMessageResponse.serializer() - else -> throw IllegalArgumentException("Unknown type of message") - } - } -} - -object StreamChatResponseSerializer : - JsonContentPolymorphicSerializer(StreamChatResponse::class) { - override fun selectDeserializer(element: JsonElement): KSerializer { - val type = element.jsonObject["type"]?.jsonPrimitive?.content - - return when (type) { - "message_start" -> MessageStart.serializer() - "content_block_start" -> ContentBlockStart.serializer() - "content_block_delta" -> ContentBlockDelta.serializer() - "content_block_stop" -> ContentBlockStop.serializer() - "message_delta" -> MessageDelta.serializer() - "message_stop" -> MessageStop.serializer() - "ping" -> Ping.serializer() - else -> throw IllegalArgumentException("Unknown type of message") - } - } -} \ No newline at end of file diff --git a/anthropic-client/anthropic-client-core/src/jvmTest/kotlin/com/tddworks/anthropic/api/JsonUtils.kt b/anthropic-client/anthropic-client-core/src/jvmTest/kotlin/com/tddworks/anthropic/api/JsonUtils.kt index c245943..2516722 100644 --- a/anthropic-client/anthropic-client-core/src/jvmTest/kotlin/com/tddworks/anthropic/api/JsonUtils.kt +++ b/anthropic-client/anthropic-client-core/src/jvmTest/kotlin/com/tddworks/anthropic/api/JsonUtils.kt @@ -1,6 +1,7 @@ package com.tddworks.anthropic.api -import com.tddworks.anthropic.api.messages.api.internal.json.chatModule +import com.tddworks.anthropic.api.messages.api.internal.json.anthropicModule +import kotlinx.serialization.EncodeDefault import kotlinx.serialization.json.Json val prettyJson = Json { // this returns the JsonBuilder @@ -8,6 +9,29 @@ val prettyJson = Json { // this returns the JsonBuilder ignoreUnknownKeys = true // optional: specify indent prettyPrintIndent = " " + + /** + * Controls whether the target property is serialized when its value is equal to a default value, + * regardless of the format settings. + * Does not affect decoding and deserialization process. + * + * Example of usage: + * ``` + * @Serializable + * data class Foo( + * @EncodeDefault(ALWAYS) val a: Int = 42, + * @EncodeDefault(NEVER) val b: Int = 43, + * val c: Int = 44 + * ) + * + * Json { encodeDefaults = false }.encodeToString((Foo()) // {"a": 42} + * Json { encodeDefaults = true }.encodeToString((Foo()) // {"a": 42, "c":44} + * ``` + * + * @see EncodeDefault.Mode.ALWAYS + * @see EncodeDefault.Mode.NEVER + */ + encodeDefaults = true } /** @@ -23,5 +47,6 @@ internal val JsonLenient = Json { ignoreUnknownKeys = true // https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/json.md#class-discriminator-for-polymorphism classDiscriminator = "#class" - serializersModule = chatModule + serializersModule = anthropicModule + encodeDefaults = true } \ No newline at end of file diff --git a/anthropic-client/anthropic-client-core/src/jvmTest/kotlin/com/tddworks/anthropic/api/messages/api/CreateMessageRequestTest.kt b/anthropic-client/anthropic-client-core/src/jvmTest/kotlin/com/tddworks/anthropic/api/messages/api/CreateMessageRequestTest.kt new file mode 100644 index 0000000..8b8d394 --- /dev/null +++ b/anthropic-client/anthropic-client-core/src/jvmTest/kotlin/com/tddworks/anthropic/api/messages/api/CreateMessageRequestTest.kt @@ -0,0 +1,33 @@ +package com.tddworks.anthropic.api.messages.api + +import com.tddworks.anthropic.api.Model +import com.tddworks.anthropic.api.prettyJson +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test + +class CreateMessageRequestTest { + @Test + fun `should convert request to correct json`() { + val json = """ + { + "messages": [ + { + "role": "user", + "content": "hello" + } + ], + "systemPrompt": null, + "max_tokens": 1024, + "model": "claude-3-haiku-20240307" + } + """.trimIndent() + + val request = CreateMessageRequest( + messages = listOf(Message.user("hello")), + maxTokens = 1024, + model = Model.CLAUDE_3_HAIKU + ) + + assertEquals(json, prettyJson.encodeToString(CreateMessageRequest.serializer(), request)) + } +} \ No newline at end of file diff --git a/anthropic-client/anthropic-client-core/src/jvmTest/kotlin/com/tddworks/anthropic/api/messages/api/internal/DefaultMessagesApiTest.kt b/anthropic-client/anthropic-client-core/src/jvmTest/kotlin/com/tddworks/anthropic/api/messages/api/internal/DefaultMessagesApiTest.kt index 0b1b66a..8c8cd99 100644 --- a/anthropic-client/anthropic-client-core/src/jvmTest/kotlin/com/tddworks/anthropic/api/messages/api/internal/DefaultMessagesApiTest.kt +++ b/anthropic-client/anthropic-client-core/src/jvmTest/kotlin/com/tddworks/anthropic/api/messages/api/internal/DefaultMessagesApiTest.kt @@ -1,14 +1,9 @@ package com.tddworks.anthropic.api.messages.api.internal import app.cash.turbine.test -import com.tddworks.anthropic.api.messages.api.CreateMessageRequest -import com.tddworks.anthropic.api.messages.api.CreateMessageResponse -import com.tddworks.anthropic.api.messages.api.Message -import com.tddworks.anthropic.api.messages.api.Usage -import com.tddworks.anthropic.api.messages.api.stream.* +import com.tddworks.anthropic.api.messages.api.* import com.tddworks.anthropic.api.mockHttpClient import com.tddworks.common.network.api.ktor.internal.DefaultHttpRequester -import com.tddworks.openllm.api.ChatRequest import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest @@ -51,7 +46,7 @@ class DefaultMessagesApiTest : KoinTest { val request = CreateMessageRequest.streamRequest(listOf(Message.user(("hello")))) // When - chatsApi.chat(request).test { + chatsApi.stream(request).test { // Then assertEquals( MessageStop( @@ -75,7 +70,7 @@ class DefaultMessagesApiTest : KoinTest { val request = CreateMessageRequest.streamRequest(listOf(Message.user(("hello")))) // When - chatsApi.chat(request).test { + chatsApi.stream(request).test { // Then assertEquals( MessageDelta( @@ -106,7 +101,7 @@ class DefaultMessagesApiTest : KoinTest { val request = CreateMessageRequest.streamRequest(listOf(Message.user(("hello")))) // When - chatsApi.chat(request).test { + chatsApi.stream(request).test { // Then assertEquals( ContentBlockStop( @@ -131,7 +126,7 @@ class DefaultMessagesApiTest : KoinTest { val request = CreateMessageRequest.streamRequest(listOf(Message.user(("hello")))) // When - chatsApi.chat(request).test { + chatsApi.stream(request).test { // Then assertEquals( ContentBlockDelta( @@ -160,7 +155,7 @@ class DefaultMessagesApiTest : KoinTest { val request = CreateMessageRequest.streamRequest(listOf(Message.user(("hello")))) // When - chatsApi.chat(request).test { + chatsApi.stream(request).test { // Then assertEquals( Ping( @@ -184,7 +179,7 @@ class DefaultMessagesApiTest : KoinTest { val request = CreateMessageRequest.streamRequest(listOf(Message.user(("hello")))) // When - chatsApi.chat(request).test { + chatsApi.stream(request).test { // Then assertEquals( ContentBlockStart( @@ -214,7 +209,7 @@ class DefaultMessagesApiTest : KoinTest { val request = CreateMessageRequest.streamRequest(listOf(Message.user(("hello")))) - chatsApi.chat(request).test { + chatsApi.stream(request).test { assertEquals( MessageStart( type = "message_start", @@ -270,7 +265,7 @@ class DefaultMessagesApiTest : KoinTest { ) - val r = chat.chat(request as ChatRequest) as CreateMessageResponse + val r = chat.create(request) with(r) { assertEquals("msg_013Zva2CMHLNnXjNJJKqJ2EF", id) 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 index 72ec910..453ddf0 100644 --- 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 @@ -18,12 +18,16 @@ suspend inline fun FlowCollector.streamEventsFrom(response: HttpR while (!channel.isClosedForRead) { channel.readUTF8Line()?.let { streamResponse -> if (notEndStreamResponse(streamResponse)) { - emit(getKoin().get().decodeFromString(streamResponse.removePrefix(STREAM_PREFIX))) + emit(json().decodeFromString(streamResponse.removePrefix(STREAM_PREFIX))) } } ?: break } } +fun json(): Json { + return getKoin().get() +} + private fun isStreamResponse(line: String) = line.startsWith(STREAM_PREFIX) diff --git a/common/src/commonMain/kotlin/com/tddworks/common/network/api/ktor/internal/HttpClient.kt b/common/src/commonMain/kotlin/com/tddworks/common/network/api/ktor/internal/HttpClient.kt index 14757f2..e2f0d7c 100644 --- a/common/src/commonMain/kotlin/com/tddworks/common/network/api/ktor/internal/HttpClient.kt +++ b/common/src/commonMain/kotlin/com/tddworks/common/network/api/ktor/internal/HttpClient.kt @@ -17,12 +17,12 @@ import kotlin.time.Duration.Companion.minutes * Creates a new [HttpClient] with [OkHttp] engine and [ContentNegotiation] plugin. * * @param url the base URL of the API - * @param token the authentication token + * @param authToken the authentication token * @return a new [HttpClient] instance */ fun createHttpClient( url: String, - token: String, + authToken: String? = null, engine: HttpClientEngineFactory, json: Json = JsonLenient, ): HttpClient { @@ -48,25 +48,26 @@ fun createHttpClient( * SIMPLE - Logger using println. * Empty - Empty Logger for test purpose. */ - logger = Logger.DEFAULT + logger = Logger.SIMPLE /** * ALL - log all * HEADERS - log headers * INFO - log info * NONE - none */ - level = LogLevel.INFO + level = LogLevel.ALL } - install(Auth) { - bearer { - loadTokens { - BearerTokens(accessToken = token, refreshToken = "") + authToken?.let { + install(Auth) { + bearer { + loadTokens { + BearerTokens(accessToken = authToken, refreshToken = "") + } } } } - /** * Installs an [HttpRequestRetry] with default maxRetries of 3, * retryIf checks for rate limit error with status code 429, diff --git a/common/src/commonMain/kotlin/com/tddworks/common/network/api/ktor/internal/JsonLenient.kt b/common/src/commonMain/kotlin/com/tddworks/common/network/api/ktor/internal/JsonLenient.kt index 6cb192f..b23ab9f 100644 --- a/common/src/commonMain/kotlin/com/tddworks/common/network/api/ktor/internal/JsonLenient.kt +++ b/common/src/commonMain/kotlin/com/tddworks/common/network/api/ktor/internal/JsonLenient.kt @@ -1,5 +1,6 @@ package com.tddworks.common.network.api.ktor.internal +import kotlinx.serialization.EncodeDefault import kotlinx.serialization.json.Json @@ -14,4 +15,30 @@ import kotlinx.serialization.json.Json val JsonLenient = Json { isLenient = true ignoreUnknownKeys = true + /** + * When this flag is disabled properties with null values without default are not encoded. + */ + explicitNulls = false + /** + * Controls whether the target property is serialized when its value is equal to a default value, + * regardless of the format settings. + * Does not affect decoding and deserialization process. + * + * Example of usage: + * ``` + * @Serializable + * data class Foo( + * @EncodeDefault(ALWAYS) val a: Int = 42, + * @EncodeDefault(NEVER) val b: Int = 43, + * val c: Int = 44 + * ) + * + * Json { encodeDefaults = false }.encodeToString((Foo()) // {"a": 42} + * Json { encodeDefaults = true }.encodeToString((Foo()) // {"a": 42, "c":44} + * ``` + * + * @see EncodeDefault.Mode.ALWAYS + * @see EncodeDefault.Mode.NEVER + */ + encodeDefaults = true } \ No newline at end of file diff --git a/common/src/commonMain/kotlin/com/tddworks/di/Koin.kt b/common/src/commonMain/kotlin/com/tddworks/di/Koin.kt index 3f2efbe..3427466 100644 --- a/common/src/commonMain/kotlin/com/tddworks/di/Koin.kt +++ b/common/src/commonMain/kotlin/com/tddworks/di/Koin.kt @@ -21,7 +21,6 @@ fun commonModule(enableNetworkLogs: Boolean) = module { fun createJson() = Json { isLenient = true; ignoreUnknownKeys = true } - inline fun getInstance(): T { return object : KoinComponent { val value: T by inject() diff --git a/common/src/jvmMain/kotlin/com/tddworks/common/network/api/ktor/internal/DefaultHttpRequester.jvm.kt b/common/src/jvmMain/kotlin/com/tddworks/common/network/api/ktor/internal/DefaultHttpRequester.jvm.kt index a57b5a1..42db789 100644 --- a/common/src/jvmMain/kotlin/com/tddworks/common/network/api/ktor/internal/DefaultHttpRequester.jvm.kt +++ b/common/src/jvmMain/kotlin/com/tddworks/common/network/api/ktor/internal/DefaultHttpRequester.jvm.kt @@ -10,7 +10,7 @@ actual fun HttpRequester.Companion.default( return DefaultHttpRequester( createHttpClient( url = url, - token = token, + authToken = token, engine = CIO ) ) diff --git a/common/src/macosMain/kotlin/com/tddworks/common/network/api/ktor/internal/DefaultHttpRequester.macos.kt b/common/src/macosMain/kotlin/com/tddworks/common/network/api/ktor/internal/DefaultHttpRequester.macos.kt index 24766cb..fe47e96 100644 --- a/common/src/macosMain/kotlin/com/tddworks/common/network/api/ktor/internal/DefaultHttpRequester.macos.kt +++ b/common/src/macosMain/kotlin/com/tddworks/common/network/api/ktor/internal/DefaultHttpRequester.macos.kt @@ -11,7 +11,7 @@ actual fun HttpRequester.Companion.default( return DefaultHttpRequester( createHttpClient( url = url, - token = token, + authToken = token, engine = Darwin ) ) 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 d849365..07feed6 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 @@ -9,6 +9,7 @@ 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.* +import io.ktor.http.* interface OpenAI : Chat, Images { companion object { @@ -20,7 +21,7 @@ fun OpenAI(token: String, engine: HttpClientEngineFactory