├── ollama-client ├── jvm │ └── .gitkeep ├── ollama-client-darwin │ ├── .gitkeep │ ├── src │ │ └── appleMain │ │ │ └── kotlin │ │ │ └── com │ │ │ └── tddworks │ │ │ └── ollama │ │ │ └── api │ │ │ └── DarwinOllama.kt │ └── build.gradle.kts ├── build.gradle.kts └── ollama-client-core │ ├── src │ ├── commonMain │ │ └── kotlin │ │ │ └── com │ │ │ └── tddworks │ │ │ └── ollama │ │ │ ├── api │ │ │ ├── chat │ │ │ │ ├── OllamaChat.kt │ │ │ │ ├── internal │ │ │ │ │ └── DefaultOllamaChatApi.kt │ │ │ │ ├── OllamaChatRequest.kt │ │ │ │ └── OllamaChatResponse.kt │ │ │ ├── OllamaConfig.kt │ │ │ ├── generate │ │ │ │ ├── OllamaGenerate.kt │ │ │ │ ├── OllamaGenerateRequest.kt │ │ │ │ ├── internal │ │ │ │ │ └── DefaultOllamaGenerateApi.kt │ │ │ │ └── OllamaGenerateResponse.kt │ │ │ ├── OllamaModel.kt │ │ │ ├── json │ │ │ │ └── JsonLenient.kt │ │ │ ├── internal │ │ │ │ └── OllamaApi.kt │ │ │ └── Ollama.kt │ │ │ └── di │ │ │ └── Koin.kt │ └── jvmTest │ │ └── kotlin │ │ └── com │ │ └── tddworks │ │ └── ollama │ │ └── api │ │ ├── OllamaModelTest.kt │ │ ├── OllamaConfigTest.kt │ │ ├── TestKoinCoroutineExtension.kt │ │ ├── OllamaTest.kt │ │ ├── MockHttpClient.kt │ │ ├── chat │ │ ├── internal │ │ │ └── DefaultOllamaChatITest.kt │ │ └── OllamaChatRequestTest.kt │ │ └── JsonUtils.kt │ └── build.gradle.kts ├── openai-client ├── jvm │ └── .gitkeep ├── openai-client-core │ ├── src │ │ ├── jvmMain │ │ │ └── .gitkeep │ │ ├── commonMain │ │ │ └── kotlin │ │ │ │ └── com │ │ │ │ └── tddworks │ │ │ │ └── openai │ │ │ │ ├── api │ │ │ │ ├── chat │ │ │ │ │ └── api │ │ │ │ │ │ ├── Settings.kt │ │ │ │ │ │ ├── Role.kt │ │ │ │ │ │ ├── ChatCompletion.kt │ │ │ │ │ │ ├── vision │ │ │ │ │ │ └── VisionMessageContentSerializer.kt │ │ │ │ │ │ ├── Chat.kt │ │ │ │ │ │ └── ChatCompletionChunk.kt │ │ │ │ ├── OpenAIConfig.kt │ │ │ │ ├── images │ │ │ │ │ ├── api │ │ │ │ │ │ ├── ResponseFormat.kt │ │ │ │ │ │ ├── Quality.kt │ │ │ │ │ │ ├── Style.kt │ │ │ │ │ │ ├── Size.kt │ │ │ │ │ │ ├── Images.kt │ │ │ │ │ │ └── Image.kt │ │ │ │ │ └── internal │ │ │ │ │ │ └── DefaultImagesApi.kt │ │ │ │ ├── legacy │ │ │ │ │ └── completions │ │ │ │ │ │ └── api │ │ │ │ │ │ ├── Completions.kt │ │ │ │ │ │ ├── internal │ │ │ │ │ │ └── DefaultCompletionsApi.kt │ │ │ │ │ │ └── Completion.kt │ │ │ │ └── OpenAI.kt │ │ │ │ └── di │ │ │ │ └── Koin.kt │ │ └── jvmTest │ │ │ └── kotlin │ │ │ └── com │ │ │ └── tddworks │ │ │ ├── openai │ │ │ ├── api │ │ │ │ ├── images │ │ │ │ │ ├── api │ │ │ │ │ │ ├── ResponseFormatTest.kt │ │ │ │ │ │ └── ImageCreateTest.kt │ │ │ │ │ └── internal │ │ │ │ │ │ └── DefaultImagesApiTest.kt │ │ │ │ ├── chat │ │ │ │ │ └── api │ │ │ │ │ │ ├── ChatCompletionTest.kt │ │ │ │ │ │ ├── ChatCompletionChunkTest.kt │ │ │ │ │ │ ├── ChatChoiceTest.kt │ │ │ │ │ │ ├── ModelTest.kt │ │ │ │ │ │ └── ChatCompletionRequestTest.kt │ │ │ │ ├── legacy │ │ │ │ │ └── completions │ │ │ │ │ │ └── api │ │ │ │ │ │ └── CompletionRequestTest.kt │ │ │ │ ├── common │ │ │ │ │ ├── JsonUtils.kt │ │ │ │ │ └── MockHttpClient.kt │ │ │ │ ├── InternalPackageTest.kt │ │ │ │ └── OpenAIITest.kt │ │ │ └── di │ │ │ │ └── OpenAIKoinTest.kt │ │ │ └── common │ │ │ └── network │ │ │ └── api │ │ │ └── ktor │ │ │ └── internal │ │ │ ├── exception │ │ │ └── OpenAIErrorTest.kt │ │ │ └── HostPortConnectionConfigTest.kt │ └── build.gradle.kts ├── openai-client-cio │ ├── src │ │ └── jvmMain │ │ │ └── kotlin │ │ │ └── Stub.kt │ └── build.gradle.kts ├── build.gradle.kts └── openai-client-darwin │ ├── src │ └── appleMain │ │ └── kotlin │ │ └── com │ │ └── tddworks │ │ └── openai │ │ └── darwin │ │ └── api │ │ └── DarwinOpenAI.kt │ └── build.gradle.kts ├── openai-gateway ├── jvm │ └── .gitkeep ├── openai-gateway-core │ ├── src │ │ ├── jvmMain │ │ │ └── .gitkeep │ │ ├── macosMain │ │ │ └── kotlin │ │ │ │ └── com │ │ │ │ └── tddworks │ │ │ │ └── openai │ │ │ │ └── gateway │ │ │ │ └── di │ │ │ │ └── Koin.macos.kt │ │ ├── commonMain │ │ │ └── kotlin │ │ │ │ └── com │ │ │ │ └── tddworks │ │ │ │ ├── openai │ │ │ │ └── gateway │ │ │ │ │ └── api │ │ │ │ │ ├── LLMProvider.kt │ │ │ │ │ ├── OpenAIProviderConfig.kt │ │ │ │ │ ├── OpenAIProvider.kt │ │ │ │ │ ├── internal │ │ │ │ │ ├── DefaultOpenAIProviderConfig.kt │ │ │ │ │ ├── GeminiOpenAIProviderConfig.kt │ │ │ │ │ ├── OllamaOpenAIProviderConfig.kt │ │ │ │ │ ├── AnthropicOpenAIProviderConfig.kt │ │ │ │ │ ├── GeminiOpenAIProvider.kt │ │ │ │ │ └── DefaultOpenAIProvider.kt │ │ │ │ │ └── OpenAIGateway.kt │ │ │ │ ├── anthropic │ │ │ │ └── api │ │ │ │ │ └── messages │ │ │ │ │ └── api │ │ │ │ │ └── OpenAIStopReason.kt │ │ │ │ └── azure │ │ │ │ └── api │ │ │ │ └── internal │ │ │ │ └── AzureChatApi.kt │ │ └── jvmTest │ │ │ └── kotlin │ │ │ └── com │ │ │ └── tddworks │ │ │ ├── anthropic │ │ │ └── api │ │ │ │ ├── AnthropicConfigTest.kt │ │ │ │ └── AnthropicTest.kt │ │ │ ├── azure │ │ │ └── api │ │ │ │ └── AzureAIProviderConfigTest.kt │ │ │ └── openai │ │ │ └── gateway │ │ │ ├── di │ │ │ └── KoinTest.kt │ │ │ └── api │ │ │ └── internal │ │ │ └── AnthropicOpenAIProviderConfigTest.kt │ └── build.gradle.kts ├── build.gradle.kts └── openai-gateway-darwin │ ├── build.gradle.kts │ └── src │ └── appleMain │ └── kotlin │ └── com │ └── tddworks │ └── openai │ └── gateway │ └── api │ └── DarwinOpenAIGateway.kt ├── anthropic-client ├── jvm │ └── .gitkeep ├── anthropic-client-core │ ├── src │ │ ├── jvm │ │ │ └── .gitkeep │ │ ├── commonMain │ │ │ └── kotlin │ │ │ │ └── com │ │ │ │ └── tddworks │ │ │ │ └── anthropic │ │ │ │ ├── api │ │ │ │ ├── messages │ │ │ │ │ └── api │ │ │ │ │ │ ├── ContentMessage.kt │ │ │ │ │ │ ├── Usage.kt │ │ │ │ │ │ ├── Messages.kt │ │ │ │ │ │ ├── Role.kt │ │ │ │ │ │ ├── CreateMessageRequest.kt │ │ │ │ │ │ ├── MessageContentSerializer.kt │ │ │ │ │ │ ├── Delta.kt │ │ │ │ │ │ ├── internal │ │ │ │ │ │ ├── JsonLenient.kt │ │ │ │ │ │ └── json │ │ │ │ │ │ │ └── StreamMessageResponseSerializer.kt │ │ │ │ │ │ ├── StreamMessageResponse.kt │ │ │ │ │ │ └── CreateMessageResponse.kt │ │ │ │ ├── AnthropicConfig.kt │ │ │ │ ├── internal │ │ │ │ │ └── AnthropicApi.kt │ │ │ │ ├── AnthropicModel.kt │ │ │ │ └── Anthropic.kt │ │ │ │ └── di │ │ │ │ └── Koin.kt │ │ └── jvmTest │ │ │ └── kotlin │ │ │ └── com │ │ │ └── tddworks │ │ │ └── anthropic │ │ │ └── api │ │ │ ├── messages │ │ │ └── api │ │ │ │ ├── RoleTest.kt │ │ │ │ ├── ContentMessageTest.kt │ │ │ │ ├── DeltaTest.kt │ │ │ │ ├── internal │ │ │ │ └── TestKoinCoroutineExtension.kt │ │ │ │ ├── CreateMessageResponseTest.kt │ │ │ │ ├── UsageTest.kt │ │ │ │ ├── BlockMessageContentTest.kt │ │ │ │ └── ContentTest.kt │ │ │ ├── AnthropicTest.kt │ │ │ ├── ModelTest.kt │ │ │ ├── MockHttpClient.kt │ │ │ └── JsonUtils.kt │ └── build.gradle.kts ├── build.gradle.kts └── anthropic-client-darwin │ ├── src │ └── appleMain │ │ └── kotlin │ │ └── com │ │ └── tddworks │ │ └── anthropic │ │ └── darwin │ │ └── api │ │ └── DarwinAnthropic.kt │ └── build.gradle.kts ├── .github ├── FUNDING.yml └── workflows │ ├── KMMBridge-publish.yml │ └── main.yml ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gemini-client ├── build.gradle.kts ├── gemini-client-core │ └── src │ │ ├── commonMain │ │ └── kotlin │ │ │ └── com │ │ │ └── tddworks │ │ │ └── gemini │ │ │ ├── api │ │ │ └── textGeneration │ │ │ │ └── api │ │ │ │ ├── GeminiConfig.kt │ │ │ │ ├── Gemini.kt │ │ │ │ ├── GenerationConfig.kt │ │ │ │ ├── PartSerializer.kt │ │ │ │ ├── TextGeneration.kt │ │ │ │ ├── GenerateContentRequest.kt │ │ │ │ ├── internal │ │ │ │ └── DefaultTextGenerationApi.kt │ │ │ │ ├── GenerateContentResponse.kt │ │ │ │ └── GeminiModel.kt │ │ │ └── di │ │ │ ├── Koin.kt │ │ │ └── GeminiModule.kt │ │ └── jvmTest │ │ └── kotlin │ │ └── com │ │ └── tddworks │ │ └── gemini │ │ └── api │ │ ├── GeminiTest.kt │ │ └── textGeneration │ │ └── api │ │ ├── GenerationConfigTest.kt │ │ ├── MockHttpClient.kt │ │ └── GenerateContentResponseTest.kt └── gemini-client-darwin │ ├── src │ └── appleMain │ │ └── kotlin │ │ └── com │ │ └── tddworks │ │ └── gemini │ │ └── api │ │ └── DarwinGemini.kt │ └── build.gradle.kts ├── common ├── src │ ├── commonMain │ │ └── kotlin │ │ │ └── com │ │ │ └── tddworks │ │ │ ├── common │ │ │ └── network │ │ │ │ └── api │ │ │ │ └── ktor │ │ │ │ ├── internal │ │ │ │ ├── AuthConfig.kt │ │ │ │ ├── UrlBasedConnectionConfig.kt │ │ │ │ ├── exception │ │ │ │ │ ├── OpenLLMException.kt │ │ │ │ │ ├── OpenLLMErrorDetails.kt │ │ │ │ │ └── OpenLLMAPIException.kt │ │ │ │ ├── HostPortConnectionConfig.kt │ │ │ │ ├── ClientFeatures.kt │ │ │ │ ├── ConnectionConfig.kt │ │ │ │ ├── JsonLenient.kt │ │ │ │ └── HttpClient.kt │ │ │ │ └── api │ │ │ │ ├── ListResponse.kt │ │ │ │ ├── HttpRequester.kt │ │ │ │ └── Stream.kt │ │ │ └── di │ │ │ └── Koin.kt │ ├── jvmTest │ │ └── kotlin │ │ │ └── com │ │ │ └── tddworks │ │ │ ├── common │ │ │ └── network │ │ │ │ └── api │ │ │ │ ├── ktor │ │ │ │ ├── StreamResponse.kt │ │ │ │ ├── TestKoinCoroutineExtension.kt │ │ │ │ └── api │ │ │ │ │ └── StreamTest.kt │ │ │ │ ├── JsonUtils.kt │ │ │ │ ├── MockHttpClient.kt │ │ │ │ └── InternalPackageTest.kt │ │ │ └── di │ │ │ └── CommonKoinTest.kt │ ├── jvmMain │ │ └── kotlin │ │ │ └── com │ │ │ └── tddworks │ │ │ └── common │ │ │ └── network │ │ │ └── api │ │ │ └── ktor │ │ │ └── internal │ │ │ └── HttpClient.jvm.kt │ └── appleMain │ │ └── kotlin │ │ └── com │ │ └── tddworks │ │ └── common │ │ └── network │ │ └── api │ │ └── ktor │ │ └── internal │ │ └── HttpClient.apple.kt └── build.gradle.kts ├── versions.properties ├── .gitignore ├── gradle.properties └── settings.gradle.kts /ollama-client/jvm/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /openai-client/jvm/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /openai-gateway/jvm/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /anthropic-client/jvm/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ollama-client/ollama-client-darwin/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /openai-client/openai-client-core/src/jvmMain/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /anthropic-client/anthropic-client-core/src/jvm/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /openai-gateway/openai-gateway-core/src/jvmMain/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [hanrw] 4 | 5 | -------------------------------------------------------------------------------- /openai-client/openai-client-cio/src/jvmMain/kotlin/Stub.kt: -------------------------------------------------------------------------------- 1 | internal fun publicationStub() = Unit 2 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tddworks/openai-kotlin/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gemini-client/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { `maven-publish` } 2 | 3 | kotlin { 4 | jvm() 5 | sourceSets { commonMain { dependencies { api(projects.geminiClient.geminiClientCore) } } } 6 | } 7 | -------------------------------------------------------------------------------- /ollama-client/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { `maven-publish` } 2 | 3 | kotlin { 4 | jvm() 5 | sourceSets { commonMain { dependencies { api(projects.ollamaClient.ollamaClientCore) } } } 6 | } 7 | -------------------------------------------------------------------------------- /openai-client/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { `maven-publish` } 2 | 3 | kotlin { 4 | jvm() 5 | sourceSets { commonMain { dependencies { api(projects.openaiClient.openaiClientCore) } } } 6 | } 7 | -------------------------------------------------------------------------------- /openai-gateway/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { `maven-publish` } 2 | 3 | kotlin { 4 | jvm() 5 | sourceSets { commonMain { dependencies { api(projects.openaiGateway.openaiGatewayCore) } } } 6 | } 7 | -------------------------------------------------------------------------------- /anthropic-client/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { `maven-publish` } 2 | 3 | kotlin { 4 | jvm() 5 | sourceSets { commonMain { dependencies { api(projects.anthropicClient.anthropicClientCore) } } } 6 | } 7 | -------------------------------------------------------------------------------- /common/src/commonMain/kotlin/com/tddworks/common/network/api/ktor/internal/AuthConfig.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.common.network.api.ktor.internal 2 | 3 | data class AuthConfig(val authToken: (() -> String)? = null) 4 | -------------------------------------------------------------------------------- /openai-gateway/openai-gateway-core/src/macosMain/kotlin/com/tddworks/openai/gateway/di/Koin.macos.kt: -------------------------------------------------------------------------------- 1 | import org.koin.core.module.Module 2 | 3 | fun platformModule(): Module { 4 | TODO("Not yet implemented") 5 | } 6 | -------------------------------------------------------------------------------- /common/src/jvmTest/kotlin/com/tddworks/common/network/api/ktor/StreamResponse.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.common.network.api.ktor 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable data class StreamResponse(val content: String) 6 | -------------------------------------------------------------------------------- /common/src/commonMain/kotlin/com/tddworks/common/network/api/ktor/internal/UrlBasedConnectionConfig.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.common.network.api.ktor.internal 2 | 3 | data class UrlBasedConnectionConfig(val baseUrl: () -> String = { "" }) : ConnectionConfig 4 | -------------------------------------------------------------------------------- /common/src/commonMain/kotlin/com/tddworks/common/network/api/ktor/api/ListResponse.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.common.network.api.ktor.api 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable data class ListResponse(val created: Long, val data: List) 6 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-9.0.0-bin.zip 4 | networkTimeout=10000 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /openai-client/openai-client-core/src/commonMain/kotlin/com/tddworks/openai/api/chat/api/Settings.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.openai.api.chat.api 2 | 3 | // import com.snacks.openai.api.OpenAISettings 4 | // 5 | // 6 | // interface Settings { 7 | // fun settings(): OpenAISettings 8 | // } 9 | -------------------------------------------------------------------------------- /openai-gateway/openai-gateway-core/src/commonMain/kotlin/com/tddworks/openai/gateway/api/LLMProvider.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.openai.gateway.api 2 | 3 | enum class LLMProvider { 4 | ANTHROPIC, 5 | DEEPSEEK, 6 | GEMINI, 7 | MOONSHOT, 8 | OLLAMA, 9 | OPENAI, 10 | } 11 | -------------------------------------------------------------------------------- /anthropic-client/anthropic-client-core/src/commonMain/kotlin/com/tddworks/anthropic/api/messages/api/ContentMessage.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.anthropic.api.messages.api 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable data class ContentMessage(val text: String, val type: String) 6 | -------------------------------------------------------------------------------- /openai-gateway/openai-gateway-core/src/commonMain/kotlin/com/tddworks/anthropic/api/messages/api/OpenAIStopReason.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.anthropic.api.messages.api 2 | 3 | enum class OpenAIStopReason { 4 | Stop, 5 | Length, 6 | FunctionCall, 7 | ToolCalls, 8 | Other, 9 | } 10 | -------------------------------------------------------------------------------- /gemini-client/gemini-client-core/src/commonMain/kotlin/com/tddworks/gemini/api/textGeneration/api/GeminiConfig.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.gemini.api.textGeneration.api 2 | 3 | data class GeminiConfig( 4 | val apiKey: () -> String = { "CONFIG_API_KEY" }, 5 | val baseUrl: () -> String = { Gemini.BASE_URL }, 6 | ) 7 | -------------------------------------------------------------------------------- /common/src/jvmMain/kotlin/com/tddworks/common/network/api/ktor/internal/HttpClient.jvm.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.common.network.api.ktor.internal 2 | 3 | import io.ktor.client.engine.* 4 | import io.ktor.client.engine.okhttp.* 5 | 6 | internal actual fun httpClientEngine(): HttpClientEngine { 7 | return OkHttp.create() 8 | } 9 | -------------------------------------------------------------------------------- /common/src/commonMain/kotlin/com/tddworks/common/network/api/ktor/internal/exception/OpenLLMException.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.common.network.api.ktor.internal.exception 2 | 3 | /** OpenAI client exception */ 4 | sealed class OpenAIException(message: String? = null, throwable: Throwable? = null) : 5 | RuntimeException(message, throwable) 6 | -------------------------------------------------------------------------------- /common/src/commonMain/kotlin/com/tddworks/common/network/api/ktor/internal/HostPortConnectionConfig.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.common.network.api.ktor.internal 2 | 3 | data class HostPortConnectionConfig( 4 | val protocol: () -> String? = { null }, 5 | val port: () -> Int? = { null }, 6 | val host: () -> String, 7 | ) : ConnectionConfig 8 | -------------------------------------------------------------------------------- /openai-client/openai-client-core/src/commonMain/kotlin/com/tddworks/openai/api/OpenAIConfig.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.openai.api 2 | 3 | import org.koin.core.component.KoinComponent 4 | 5 | data class OpenAIConfig( 6 | val apiKey: () -> String = { "CONFIG_API_KEY" }, 7 | val baseUrl: () -> String = { OpenAI.BASE_URL }, 8 | ) : KoinComponent 9 | -------------------------------------------------------------------------------- /openai-gateway/openai-gateway-core/src/commonMain/kotlin/com/tddworks/openai/gateway/api/OpenAIProviderConfig.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.openai.gateway.api 2 | 3 | /** Represents the configuration for the OpenAI API. */ 4 | interface OpenAIProviderConfig { 5 | val apiKey: () -> String 6 | val baseUrl: () -> String 7 | 8 | companion object 9 | } 10 | -------------------------------------------------------------------------------- /openai-client/openai-client-cio/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.kotlinx.serialization) 3 | // alias(libs.plugins.touchlab.kmmbridge) 4 | // id("module.publication") 5 | `maven-publish` 6 | } 7 | 8 | kotlin { 9 | jvm() 10 | sourceSets { jvmMain { dependencies { api(projects.openaiClient.openaiClientCore) } } } 11 | } 12 | -------------------------------------------------------------------------------- /ollama-client/ollama-client-core/src/commonMain/kotlin/com/tddworks/ollama/api/chat/OllamaChat.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.ollama.api.chat 2 | 3 | import kotlinx.coroutines.flow.Flow 4 | 5 | interface OllamaChat { 6 | fun stream(request: OllamaChatRequest): Flow 7 | 8 | suspend fun request(request: OllamaChatRequest): OllamaChatResponse 9 | } 10 | -------------------------------------------------------------------------------- /common/src/commonMain/kotlin/com/tddworks/common/network/api/ktor/internal/ClientFeatures.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.common.network.api.ktor.internal 2 | 3 | import kotlinx.serialization.json.Json 4 | 5 | data class ClientFeatures( 6 | val json: Json = Json, 7 | val queryParams: () -> Map = { emptyMap() }, 8 | val expectSuccess: Boolean = true, 9 | ) 10 | -------------------------------------------------------------------------------- /ollama-client/ollama-client-core/src/commonMain/kotlin/com/tddworks/ollama/api/OllamaConfig.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.ollama.api 2 | 3 | import org.koin.core.component.KoinComponent 4 | 5 | data class OllamaConfig( 6 | val baseUrl: () -> String = { Ollama.BASE_URL }, 7 | val protocol: () -> String = { Ollama.PROTOCOL }, 8 | val port: () -> Int = { Ollama.PORT }, 9 | ) : KoinComponent 10 | -------------------------------------------------------------------------------- /common/src/appleMain/kotlin/com/tddworks/common/network/api/ktor/internal/HttpClient.apple.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.common.network.api.ktor.internal 2 | 3 | import io.ktor.client.engine.* 4 | import io.ktor.client.engine.darwin.* 5 | 6 | internal actual fun httpClientEngine(): HttpClientEngine { 7 | return Darwin.create { 8 | this.configureSession { connectionProxyDictionary = emptyMap() } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /anthropic-client/anthropic-client-core/src/commonMain/kotlin/com/tddworks/anthropic/api/messages/api/Usage.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.anthropic.api.messages.api 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class Usage( 8 | @SerialName("input_tokens") val inputTokens: Int? = null, 9 | @SerialName("output_tokens") val outputTokens: Int? = null, 10 | ) 11 | -------------------------------------------------------------------------------- /ollama-client/ollama-client-core/src/commonMain/kotlin/com/tddworks/ollama/api/generate/OllamaGenerate.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.ollama.api.generate 2 | 3 | import kotlinx.coroutines.flow.Flow 4 | 5 | /** Interface for Ollama generate */ 6 | interface OllamaGenerate { 7 | fun stream(request: OllamaGenerateRequest): Flow 8 | 9 | suspend fun request(request: OllamaGenerateRequest): OllamaGenerateResponse 10 | } 11 | -------------------------------------------------------------------------------- /anthropic-client/anthropic-client-core/src/jvmTest/kotlin/com/tddworks/anthropic/api/messages/api/RoleTest.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.anthropic.api.messages.api 2 | 3 | import org.junit.jupiter.api.Assertions.* 4 | import org.junit.jupiter.api.Test 5 | 6 | class RoleTest { 7 | 8 | @Test 9 | fun `should return correct role name`() { 10 | assertEquals("user", Role.User.name) 11 | assertEquals("assistant", Role.Assistant.name) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /openai-client/openai-client-core/src/commonMain/kotlin/com/tddworks/openai/api/images/api/ResponseFormat.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.openai.api.images.api 2 | 3 | import kotlin.jvm.JvmInline 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | @JvmInline 8 | value class ResponseFormat(val value: String) { 9 | companion object { 10 | val base64 = ResponseFormat("b64_json") 11 | val url = ResponseFormat("url") 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /anthropic-client/anthropic-client-core/src/commonMain/kotlin/com/tddworks/anthropic/api/messages/api/Messages.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.anthropic.api.messages.api 2 | 3 | import kotlinx.coroutines.flow.Flow 4 | 5 | /** * Anthropic Messages API -https://docs.anthropic.com/claude/reference/messages_post */ 6 | interface Messages { 7 | suspend fun create(request: CreateMessageRequest): CreateMessageResponse 8 | 9 | fun stream(request: CreateMessageRequest): Flow 10 | } 11 | -------------------------------------------------------------------------------- /.github/workflows/KMMBridge-publish.yml: -------------------------------------------------------------------------------- 1 | name: Darwin Publish 2 | on: 3 | workflow_dispatch: 4 | # push: 5 | # branches: 6 | # - "main" 7 | jobs: 8 | call-kmmbridge-publish: 9 | permissions: 10 | contents: write 11 | packages: write 12 | uses: touchlab/KMMBridgeGithubWorkflow/.github/workflows/faktorybuildautoversion.yml@v1.1 13 | with: 14 | jvmVersion: 17 15 | versionBaseProperty: LIBRARY_VERSION 16 | # secrets: 17 | # PODSPEC_SSH_KEY: ${{ secrets.PODSPEC_SSH_KEY }} -------------------------------------------------------------------------------- /versions.properties: -------------------------------------------------------------------------------- 1 | #### Dependencies and Plugin versions with their available updates. 2 | #### Generated by `./gradlew refreshVersions` version 0.60.6 3 | #### 4 | #### Don't manually edit or split the comments that start with four hashtags (####), 5 | #### they will be overwritten by refreshVersions. 6 | #### 7 | #### suppress inspection "SpellCheckingInspection" for whole file 8 | #### suppress inspection "UnusedProperty" for whole file 9 | #### 10 | #### NOTE: Some versions are filtered by the rejectVersionIf predicate. See the settings.gradle.kts file. 11 | -------------------------------------------------------------------------------- /openai-client/openai-client-core/src/jvmTest/kotlin/com/tddworks/openai/api/images/api/ResponseFormatTest.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.openai.api.images.api 2 | 3 | import org.junit.jupiter.api.Assertions.* 4 | import org.junit.jupiter.api.Test 5 | 6 | class ResponseFormatTest { 7 | @Test 8 | fun `should return correct base 64 format type`() { 9 | assertEquals("b64_json", ResponseFormat.base64.value) 10 | } 11 | 12 | @Test 13 | fun `should return correct url format type`() { 14 | assertEquals("url", ResponseFormat.url.value) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /anthropic-client/anthropic-client-core/src/jvmTest/kotlin/com/tddworks/anthropic/api/messages/api/ContentMessageTest.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.anthropic.api.messages.api 2 | 3 | import org.junit.jupiter.api.Assertions.* 4 | import org.junit.jupiter.api.Test 5 | 6 | class ContentMessageTest { 7 | 8 | @Test 9 | fun `should create a ContentMessage`() { 10 | val dummyMessage = ContentMessage("Hi! My name is Claude.", "text") 11 | 12 | assertEquals("Hi! My name is Claude.", dummyMessage.text) 13 | assertEquals("text", dummyMessage.type) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /openai-gateway/openai-gateway-core/src/jvmTest/kotlin/com/tddworks/anthropic/api/AnthropicConfigTest.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.anthropic.api 2 | 3 | import org.junit.jupiter.api.Assertions.assertNotNull 4 | import org.junit.jupiter.api.Test 5 | import org.koin.core.context.startKoin 6 | import org.koin.test.junit5.AutoCloseKoinTest 7 | 8 | class AnthropicConfigTest : AutoCloseKoinTest() { 9 | 10 | @Test 11 | fun `fix the coverage by this case`() { 12 | startKoin { 13 | val r = AnthropicConfig().getKoin() 14 | assertNotNull(r) 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /common/src/jvmTest/kotlin/com/tddworks/di/CommonKoinTest.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.di 2 | 3 | import kotlin.test.assertNotNull 4 | import kotlinx.serialization.json.Json 5 | import org.junit.jupiter.api.Test 6 | import org.koin.dsl.koinApplication 7 | import org.koin.test.check.checkModules 8 | import org.koin.test.junit5.AutoCloseKoinTest 9 | 10 | class CommonKoinTest : AutoCloseKoinTest() { 11 | @Test 12 | fun `should initialize common koin modules`() { 13 | koinApplication { initKoin {} }.checkModules() 14 | 15 | val json = getInstance() 16 | 17 | assertNotNull(json) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /anthropic-client/anthropic-client-core/src/jvmTest/kotlin/com/tddworks/anthropic/api/AnthropicTest.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.anthropic.api 2 | 3 | import org.junit.jupiter.api.Assertions.assertEquals 4 | import org.junit.jupiter.api.Test 5 | 6 | class AnthropicTest { 7 | 8 | val anthropic: Anthropic = Anthropic.create(AnthropicConfig()) 9 | 10 | @Test 11 | fun `should return default settings`() { 12 | assertEquals("https://api.anthropic.com", anthropic.baseUrl()) 13 | 14 | assertEquals("CONFIG_API_KEY", anthropic.apiKey()) 15 | 16 | assertEquals("2023-06-01", anthropic.anthropicVersion()) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /openai-client/openai-client-core/src/commonMain/kotlin/com/tddworks/openai/api/legacy/completions/api/Completions.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.openai.api.legacy.completions.api 2 | 3 | /** 4 | * https://platform.openai.com/docs/api-reference/completions Given a prompt, the model will return 5 | * one or more predicted completions along with the probabilities of alternative tokens at each 6 | * position. Most developer should use our Chat Completions API to leverage our best and newest 7 | * models. 8 | */ 9 | interface Completions { 10 | suspend fun completions(request: CompletionRequest): Completion 11 | 12 | companion object 13 | } 14 | -------------------------------------------------------------------------------- /gemini-client/gemini-client-core/src/commonMain/kotlin/com/tddworks/gemini/di/Koin.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.gemini.di 2 | 3 | import com.tddworks.di.commonModule 4 | import com.tddworks.gemini.api.textGeneration.api.GeminiConfig 5 | import com.tddworks.gemini.di.GeminiModule.Companion.geminiModules 6 | import org.koin.core.context.startKoin 7 | import org.koin.dsl.KoinAppDeclaration 8 | 9 | fun initGemini( 10 | config: GeminiConfig, 11 | enableNetworkLogs: Boolean = false, 12 | appDeclaration: KoinAppDeclaration = {}, 13 | ) = startKoin { 14 | appDeclaration() 15 | modules(commonModule(enableNetworkLogs), geminiModules(config)) 16 | } 17 | -------------------------------------------------------------------------------- /openai-client/openai-client-core/src/commonMain/kotlin/com/tddworks/openai/api/images/api/Quality.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.openai.api.images.api 2 | 3 | import kotlin.jvm.JvmInline 4 | import kotlinx.serialization.Serializable 5 | 6 | /** 7 | * The quality of the image that will be generated. hd creates images with finer details and greater 8 | * consistency across the image. This param is only supported for dall-e-3. Defaults to standard 9 | */ 10 | @Serializable 11 | @JvmInline 12 | value class Quality(val value: String) { 13 | companion object { 14 | val hd = Quality("hd") 15 | val standard = Quality("standard") 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /anthropic-client/anthropic-client-core/src/jvmTest/kotlin/com/tddworks/anthropic/api/messages/api/DeltaTest.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.anthropic.api.messages.api 2 | 3 | import org.junit.jupiter.api.Assertions.* 4 | import org.junit.jupiter.api.Test 5 | 6 | class DeltaTest { 7 | 8 | @Test 9 | fun `should create dummy Delta`() { 10 | val delta = Delta.dummy() 11 | assertEquals("text_delta", delta.type) 12 | assertEquals("Hello", delta.text) 13 | assertEquals("end_turn", delta.stopReason) 14 | assertEquals(null, delta.stopSequence) 15 | assertEquals(Usage(outputTokens = 15), delta.usage) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /ollama-client/ollama-client-core/src/commonMain/kotlin/com/tddworks/ollama/api/generate/OllamaGenerateRequest.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.ollama.api.generate 2 | 3 | import com.tddworks.common.network.api.ktor.api.AnySerial 4 | import kotlinx.serialization.SerialName 5 | import kotlinx.serialization.Serializable 6 | 7 | @Serializable 8 | data class OllamaGenerateRequest( 9 | @SerialName("model") val model: String, 10 | @SerialName("prompt") val prompt: String, 11 | @SerialName("stream") val stream: Boolean = false, 12 | @SerialName("raw") val raw: Boolean = false, 13 | @SerialName("options") val options: Map? = null, 14 | ) 15 | -------------------------------------------------------------------------------- /openai-client/openai-client-core/src/commonMain/kotlin/com/tddworks/openai/api/chat/api/Role.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.openai.api.chat.api 2 | 3 | import kotlin.jvm.JvmInline 4 | import kotlinx.serialization.Serializable 5 | 6 | /** 7 | * Representing the available message sender roles. deprecated or obsolete role like `Function` is 8 | * removed. 9 | */ 10 | @JvmInline 11 | @Serializable 12 | value class Role(val name: String) { 13 | companion object { 14 | val System: Role = Role("system") 15 | val User: Role = Role("user") 16 | val Assistant: Role = Role("assistant") 17 | val Tool: Role = Role("tool") 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /openai-client/openai-client-core/src/commonMain/kotlin/com/tddworks/openai/api/chat/api/ChatCompletion.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.openai.api.chat.api 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class ChatCompletion( 7 | val id: String, 8 | val created: Long, 9 | val model: String, 10 | val choices: List, 11 | ) { 12 | companion object { 13 | fun dummy() = 14 | ChatCompletion( 15 | "chatcmpl-8Zu4AF8QMK3zFgdzXIPjFS4VkWErX", 16 | 1634160000, 17 | "gpt-3.5-turbo", 18 | listOf(ChatChoice.dummy()), 19 | ) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /gemini-client/gemini-client-core/src/commonMain/kotlin/com/tddworks/gemini/api/textGeneration/api/Gemini.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.gemini.api.textGeneration.api 2 | 3 | import com.tddworks.di.getInstance 4 | 5 | interface Gemini : TextGeneration { 6 | companion object { 7 | const val HOST = "generativelanguage.googleapis.com" 8 | const val BASE_URL = "https://$HOST" 9 | 10 | fun default(): Gemini { 11 | return object : Gemini, TextGeneration by getInstance() {} 12 | } 13 | 14 | fun create(config: GeminiConfig): Gemini { 15 | return object : Gemini, TextGeneration by getInstance() {} 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /ollama-client/ollama-client-core/src/jvmTest/kotlin/com/tddworks/ollama/api/OllamaModelTest.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.ollama.api 2 | 3 | import org.junit.jupiter.api.Assertions.* 4 | import org.junit.jupiter.api.Test 5 | 6 | class OllamaModelTest { 7 | 8 | @Test 9 | fun `should return correct latest API model name`() { 10 | assertEquals("deepseek-coder:6.7b-base", OllamaModel.DEEPSEEK_CODER.value) 11 | assertEquals("llama3", OllamaModel.LLAMA3.value) 12 | assertEquals("llama2", OllamaModel.LLAMA2.value) 13 | assertEquals("codellama", OllamaModel.CODE_LLAMA.value) 14 | assertEquals("mistral", OllamaModel.MISTRAL.value) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /anthropic-client/anthropic-client-core/src/jvmTest/kotlin/com/tddworks/anthropic/api/ModelTest.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.anthropic.api 2 | 3 | import org.junit.jupiter.api.Assertions.* 4 | import org.junit.jupiter.api.Test 5 | 6 | class ModelTest { 7 | 8 | @Test 9 | fun `should return correct latest API model name`() { 10 | assertEquals("claude-3-opus-20240229", AnthropicModel.CLAUDE_3_OPUS.value) 11 | assertEquals("claude-3-sonnet-20240229", AnthropicModel.CLAUDE_3_Sonnet.value) 12 | assertEquals("claude-3-haiku-20240307", AnthropicModel.CLAUDE_3_HAIKU.value) 13 | assertEquals("claude-3-5-sonnet-20240620", AnthropicModel.CLAUDE_3_5_Sonnet.value) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /ollama-client/ollama-client-core/src/commonMain/kotlin/com/tddworks/ollama/api/OllamaModel.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.ollama.api 2 | 3 | import kotlin.jvm.JvmInline 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | @JvmInline 8 | value class OllamaModel(val value: String) { 9 | companion object { 10 | val LLAMA2 = OllamaModel("llama2") 11 | val LLAMA3 = OllamaModel("llama3") 12 | val CODE_LLAMA = OllamaModel("codellama") 13 | val DEEPSEEK_CODER = OllamaModel("deepseek-coder:6.7b-base") 14 | val MISTRAL = OllamaModel("mistral") 15 | val availableModels = listOf(LLAMA2, LLAMA3, CODE_LLAMA, MISTRAL, DEEPSEEK_CODER) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | build/ 3 | .kotlin 4 | !gradle/wrapper/gradle-wrapper.jar 5 | !**/src/main/**/build/ 6 | !**/src/test/**/build/ 7 | 8 | ### IntelliJ IDEA ### 9 | .idea 10 | *.iws 11 | *.iml 12 | *.ipr 13 | out/ 14 | !**/src/main/**/out/ 15 | !**/src/test/**/out/ 16 | .run 17 | 18 | ### Eclipse ### 19 | .apt_generated 20 | .classpath 21 | .factorypath 22 | .project 23 | .settings 24 | .springBeans 25 | .sts4-cache 26 | bin/ 27 | !**/src/main/**/bin/ 28 | !**/src/test/**/bin/ 29 | 30 | ### NetBeans ### 31 | /nbproject/private/ 32 | /nbbuild/ 33 | /dist/ 34 | /nbdist/ 35 | /.nb-gradle/ 36 | 37 | ### VS Code ### 38 | .vscode/ 39 | 40 | ### Mac OS ### 41 | .DS_Store 42 | 43 | # Ignore .env file 44 | *.env -------------------------------------------------------------------------------- /openai-gateway/openai-gateway-core/src/commonMain/kotlin/com/tddworks/openai/gateway/api/OpenAIProvider.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.openai.gateway.api 2 | 3 | import com.tddworks.openai.api.chat.api.Chat 4 | import com.tddworks.openai.api.images.api.Images 5 | import com.tddworks.openai.api.legacy.completions.api.Completions 6 | 7 | /** Represents a provider for the OpenAI chat functionality. */ 8 | interface OpenAIProvider : Chat, Completions, Images { 9 | 10 | /** The id of the provider. */ 11 | val id: String 12 | 13 | /** The name of the provider. */ 14 | val name: String 15 | 16 | /** The configuration for the provider. */ 17 | val config: OpenAIProviderConfig 18 | 19 | companion object 20 | } 21 | -------------------------------------------------------------------------------- /openai-client/openai-client-core/src/commonMain/kotlin/com/tddworks/openai/api/images/api/Style.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.openai.api.images.api 2 | 3 | import kotlin.jvm.JvmInline 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | @JvmInline 8 | /** 9 | * The style of the generated images. Must be one of vivid or natural. Vivid causes the model to 10 | * lean towards generating hyper-real and dramatic images. Natural causes the model to produce more 11 | * natural, less hyper-real looking images. This param is only supported for dall-e-3. 12 | */ 13 | value class Style(val value: String) { 14 | companion object { 15 | val vivid = Style("vivid") 16 | val natural = Style("natural") 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /openai-client/openai-client-core/src/jvmTest/kotlin/com/tddworks/openai/api/chat/api/ChatCompletionTest.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.openai.api.chat.api 2 | 3 | import org.junit.jupiter.api.Assertions.* 4 | import org.junit.jupiter.api.Test 5 | 6 | class ChatCompletionTest { 7 | 8 | @Test 9 | fun `should return correct dummy chatCompletion`() { 10 | val chatCompletion = ChatCompletion.dummy() 11 | 12 | assertEquals("chatcmpl-8Zu4AF8QMK3zFgdzXIPjFS4VkWErX", chatCompletion.id) 13 | assertEquals(1634160000, chatCompletion.created) 14 | assertEquals("gpt-3.5-turbo", chatCompletion.model) 15 | assertEquals(1, chatCompletion.choices.size) 16 | assertEquals(ChatChoice.dummy(), chatCompletion.choices[0]) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /openai-client/openai-client-core/src/jvmTest/kotlin/com/tddworks/openai/api/chat/api/ChatCompletionChunkTest.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.openai.api.chat.api 2 | 3 | import org.junit.jupiter.api.Assertions.* 4 | import org.junit.jupiter.api.Test 5 | 6 | class ChatCompletionChunkTest { 7 | 8 | @Test 9 | fun `should create dummy chunk`() { 10 | val chunk = ChatCompletionChunk.dummy() 11 | assertEquals("fake-id", chunk.id) 12 | assertEquals("text", chunk.`object`) 13 | assertEquals(0, chunk.created) 14 | assertEquals("fake-model", chunk.model) 15 | assertEquals(1, chunk.choices.size) 16 | assertEquals(ChatChunk.fake(), chunk.choices[0]) 17 | assertEquals("fake-content", chunk.content()) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /openai-gateway/openai-gateway-core/src/commonMain/kotlin/com/tddworks/openai/gateway/api/internal/DefaultOpenAIProviderConfig.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.openai.gateway.api.internal 2 | 3 | import com.tddworks.openai.api.OpenAI 4 | import com.tddworks.openai.api.OpenAIConfig 5 | import com.tddworks.openai.gateway.api.OpenAIProviderConfig 6 | 7 | data class DefaultOpenAIProviderConfig( 8 | override val apiKey: () -> String, 9 | override val baseUrl: () -> String = { OpenAI.BASE_URL }, 10 | ) : OpenAIProviderConfig 11 | 12 | fun OpenAIProviderConfig.toOpenAIConfig() = OpenAIConfig(apiKey, baseUrl) 13 | 14 | fun OpenAIProviderConfig.Companion.default( 15 | apiKey: () -> String, 16 | baseUrl: () -> String = { OpenAI.BASE_URL }, 17 | ) = DefaultOpenAIProviderConfig(apiKey, baseUrl) 18 | -------------------------------------------------------------------------------- /openai-client/openai-client-core/src/jvmTest/kotlin/com/tddworks/common/network/api/ktor/internal/exception/OpenAIErrorTest.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.common.network.api.ktor.internal.exception 2 | 3 | import org.junit.jupiter.api.Assertions.* 4 | import org.junit.jupiter.api.Test 5 | 6 | class OpenAIErrorTest { 7 | 8 | @Test 9 | fun `should convert OpenAIError to OpenAIErrorDetails`() { 10 | val openAIError = OpenAIError(OpenAIErrorDetails("code", "message", "param", "type")) 11 | val openAIErrorDetails = openAIError.detail 12 | assertEquals("code", openAIErrorDetails?.code) 13 | assertEquals("message", openAIErrorDetails?.message) 14 | assertEquals("param", openAIErrorDetails?.param) 15 | assertEquals("type", openAIErrorDetails?.type) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /anthropic-client/anthropic-client-core/src/commonMain/kotlin/com/tddworks/anthropic/api/messages/api/Role.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.anthropic.api.messages.api 2 | 3 | import kotlin.jvm.JvmInline 4 | import kotlinx.serialization.Serializable 5 | 6 | /** 7 | * https://docs.anthropic.com/claude/reference/messages_post Our models are trained to operate on 8 | * alternating user and assistant conversational turns. When creating a new Message, you specify the 9 | * prior conversational turns with the messages parameter, and the model then generates the next 10 | * Message in the conversation. 11 | */ 12 | @JvmInline 13 | @Serializable 14 | value class Role(val name: String) { 15 | companion object { 16 | val User: Role = Role("user") 17 | val Assistant: Role = Role("assistant") 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /openai-client/openai-client-core/src/commonMain/kotlin/com/tddworks/openai/api/images/api/Size.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.openai.api.images.api 2 | 3 | import kotlin.jvm.JvmInline 4 | import kotlinx.serialization.Serializable 5 | 6 | /** 7 | * The size of the generated images. Must be one of 256x256, 512x512, or 1024x1024 for dall-e-2. 8 | * Must be one of 1024x1024, 1792x1024, or 1024x1792 for dall-e-3 models. 9 | */ 10 | @JvmInline 11 | @Serializable 12 | value class Size(val value: String) { 13 | 14 | companion object { 15 | val size256x256: Size = Size("256x256") 16 | val size512x512: Size = Size("512x512") 17 | val size1024x1024: Size = Size("1024x1024") 18 | val size1792x1024: Size = Size("1792x1024") 19 | val size1024x1792: Size = Size("1024x1792") 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /openai-client/openai-client-core/src/commonMain/kotlin/com/tddworks/openai/api/images/api/Images.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.openai.api.images.api 2 | 3 | import com.tddworks.common.network.api.ktor.api.ListResponse 4 | 5 | /** 6 | * Given a prompt and/or an input image, the model will generate a new image. 7 | * 8 | * @see [Images API](https://platform.openai.com/docs/api-reference/images) 9 | */ 10 | interface Images { 11 | 12 | /** 13 | * Creates an image given a prompt. Get images as URLs or base64-encoded JSON. 14 | * 15 | * @param request image creation request. 16 | * @return list of images. 17 | */ 18 | suspend fun generate(request: ImageCreate): ListResponse 19 | 20 | companion object { 21 | const val IMAGES_GENERATIONS_PATH = "/v1/images/generations" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /openai-gateway/openai-gateway-core/src/commonMain/kotlin/com/tddworks/openai/gateway/api/internal/GeminiOpenAIProviderConfig.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.openai.gateway.api.internal 2 | 3 | import com.tddworks.gemini.api.textGeneration.api.Gemini 4 | import com.tddworks.gemini.api.textGeneration.api.GeminiConfig 5 | import com.tddworks.openai.gateway.api.OpenAIProviderConfig 6 | 7 | class GeminiOpenAIProviderConfig( 8 | override val apiKey: () -> String, 9 | override val baseUrl: () -> String = { Gemini.BASE_URL }, 10 | ) : OpenAIProviderConfig 11 | 12 | fun OpenAIProviderConfig.toGeminiConfig() = GeminiConfig(apiKey = apiKey, baseUrl = baseUrl) 13 | 14 | fun OpenAIProviderConfig.Companion.gemini( 15 | apiKey: () -> String, 16 | baseUrl: () -> String = { Gemini.BASE_URL }, 17 | ) = GeminiOpenAIProviderConfig(apiKey, baseUrl) 18 | -------------------------------------------------------------------------------- /gemini-client/gemini-client-core/src/commonMain/kotlin/com/tddworks/gemini/api/textGeneration/api/GenerationConfig.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.gemini.api.textGeneration.api 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | /** 7 | * { "contents": [{ "parts": [{ "text": "Write a story about a magic backpack." }] }], 8 | * "safetySettings": 9 | * [{ "category": "HARM_CATEGORY_DANGEROUS_CONTENT", "threshold": "BLOCK_ONLY_HIGH" }], 10 | * "generationConfig": { "stopSequences": [ "Title" ], "temperature": 1.0, "response_mime_type": 11 | * "application/json", "maxOutputTokens": 800, "topP": 0.8, "topK": 10 } } 12 | */ 13 | @Serializable 14 | data class GenerationConfig( 15 | val temperature: Float? = null, 16 | @SerialName("response_mime_type") val responseMimeType: String? = null, 17 | ) 18 | -------------------------------------------------------------------------------- /gemini-client/gemini-client-core/src/jvmTest/kotlin/com/tddworks/gemini/api/GeminiTest.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.gemini.api 2 | 3 | import com.tddworks.di.getInstance 4 | import com.tddworks.gemini.api.textGeneration.api.Gemini 5 | import com.tddworks.gemini.api.textGeneration.api.GeminiConfig 6 | import com.tddworks.gemini.di.initGemini 7 | import org.junit.jupiter.api.Assertions.* 8 | import org.junit.jupiter.api.BeforeEach 9 | import org.junit.jupiter.api.Test 10 | import org.koin.test.junit5.AutoCloseKoinTest 11 | 12 | class GeminiTest : AutoCloseKoinTest() { 13 | 14 | @BeforeEach 15 | fun setUp() { 16 | initGemini(config = GeminiConfig()) 17 | } 18 | 19 | @Test 20 | fun `should get gemini default api`() { 21 | val gemini = getInstance() 22 | 23 | assertNotNull(gemini) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /anthropic-client/anthropic-client-core/src/commonMain/kotlin/com/tddworks/anthropic/api/messages/api/CreateMessageRequest.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.anthropic.api.messages.api 2 | 3 | import com.tddworks.anthropic.api.AnthropicModel 4 | import kotlinx.serialization.SerialName 5 | import kotlinx.serialization.Serializable 6 | 7 | @Serializable 8 | data class CreateMessageRequest( 9 | val messages: List, 10 | @SerialName("system") val systemPrompt: String? = null, 11 | @SerialName("max_tokens") val maxTokens: Int = 1024, 12 | @SerialName("model") val model: AnthropicModel = AnthropicModel.CLAUDE_3_HAIKU, 13 | val stream: Boolean? = null, 14 | ) { 15 | companion object { 16 | fun streamRequest(messages: List, systemPrompt: String? = null) = 17 | CreateMessageRequest(messages, systemPrompt, stream = true) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /anthropic-client/anthropic-client-core/src/commonMain/kotlin/com/tddworks/anthropic/api/AnthropicConfig.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.anthropic.api 2 | 3 | import org.koin.core.component.KoinComponent 4 | 5 | /** 6 | * @param apiKey a function that returns the API key to be used for authentication. Defaults to 7 | * "CONFIGURE_ME" if not provided. 8 | * @param baseUrl a function that returns the base URL of the API. Defaults to the value specified 9 | * in the Anthropic companion object if not provided. 10 | * @param anthropicVersion a function that returns the version of the Anthropic API to be used. 11 | * Defaults to "2023-06-01" if not provided. 12 | */ 13 | data class AnthropicConfig( 14 | val apiKey: () -> String = { "CONFIG_API_KEY" }, 15 | val baseUrl: () -> String = { Anthropic.BASE_URL }, 16 | val anthropicVersion: () -> String = { Anthropic.ANTHROPIC_VERSION }, 17 | ) : KoinComponent 18 | -------------------------------------------------------------------------------- /ollama-client/ollama-client-core/src/jvmTest/kotlin/com/tddworks/ollama/api/OllamaConfigTest.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.ollama.api 2 | 3 | import org.junit.jupiter.api.Assertions.assertEquals 4 | import org.junit.jupiter.api.Test 5 | 6 | class OllamaConfigTest { 7 | 8 | @Test 9 | fun `should return overridden settings`() { 10 | val target = OllamaConfig(baseUrl = { "some-url" }, port = { 8080 }, protocol = { "https" }) 11 | 12 | assertEquals("some-url", target.baseUrl()) 13 | 14 | assertEquals(8080, target.port()) 15 | 16 | assertEquals("https", target.protocol()) 17 | } 18 | 19 | @Test 20 | fun `should return default settings`() { 21 | val target = OllamaConfig() 22 | 23 | assertEquals("localhost", target.baseUrl()) 24 | 25 | assertEquals(11434, target.port()) 26 | 27 | assertEquals("http", target.protocol()) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /openai-gateway/openai-gateway-core/src/jvmTest/kotlin/com/tddworks/anthropic/api/AnthropicTest.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.anthropic.api 2 | 3 | import com.tddworks.anthropic.di.iniAnthropic 4 | import org.junit.jupiter.api.Assertions.* 5 | import org.junit.jupiter.api.BeforeEach 6 | import org.junit.jupiter.api.Test 7 | import org.koin.test.junit5.AutoCloseKoinTest 8 | 9 | class AnthropicTest : AutoCloseKoinTest() { 10 | 11 | @BeforeEach 12 | fun setUp() { 13 | iniAnthropic(config = AnthropicConfig()) 14 | } 15 | 16 | @Test 17 | fun `should create anthropic with default settings`() { 18 | 19 | val anthropic = Anthropic.create(anthropicConfig = AnthropicConfig()) 20 | 21 | assertEquals("CONFIG_API_KEY", anthropic.apiKey()) 22 | assertEquals(Anthropic.BASE_URL, anthropic.baseUrl()) 23 | assertEquals("2023-06-01", anthropic.anthropicVersion()) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /openai-client/openai-client-core/src/commonMain/kotlin/com/tddworks/openai/api/images/api/Image.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.openai.api.images.api 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | /** 7 | * Represents the url or the content of an image generated by the OpenAI API. 8 | * 9 | * @see [Image](https://beta.openai.com/docs/api-reference/images/create) 10 | */ 11 | @Serializable 12 | data class Image( 13 | 14 | /** The URL of the generated image, if response_format is url (default). */ 15 | @SerialName("url") val url: String? = null, 16 | 17 | /** The base64-encoded JSON of the generated image, if response_format is b64_json. */ 18 | @SerialName("b64_json") val b64JSON: String? = null, 19 | 20 | /** The prompt that was used to generate the image, if there was any revision to the prompt. */ 21 | @SerialName("revised_prompt") val revisedPrompt: String? = null, 22 | ) 23 | -------------------------------------------------------------------------------- /ollama-client/ollama-client-core/src/jvmTest/kotlin/com/tddworks/ollama/api/TestKoinCoroutineExtension.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.ollama.api 2 | 3 | import kotlinx.coroutines.Dispatchers 4 | import kotlinx.coroutines.test.StandardTestDispatcher 5 | import kotlinx.coroutines.test.TestDispatcher 6 | import kotlinx.coroutines.test.resetMain 7 | import kotlinx.coroutines.test.setMain 8 | import org.junit.jupiter.api.extension.AfterEachCallback 9 | import org.junit.jupiter.api.extension.BeforeEachCallback 10 | import org.junit.jupiter.api.extension.ExtensionContext 11 | 12 | class TestKoinCoroutineExtension( 13 | private val testDispatcher: TestDispatcher = StandardTestDispatcher() 14 | ) : BeforeEachCallback, AfterEachCallback { 15 | override fun beforeEach(context: ExtensionContext) { 16 | Dispatchers.setMain(testDispatcher) 17 | } 18 | 19 | override fun afterEach(context: ExtensionContext) { 20 | Dispatchers.resetMain() 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /common/src/jvmTest/kotlin/com/tddworks/common/network/api/ktor/TestKoinCoroutineExtension.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.common.network.api.ktor 2 | 3 | import kotlinx.coroutines.Dispatchers 4 | import kotlinx.coroutines.test.StandardTestDispatcher 5 | import kotlinx.coroutines.test.TestDispatcher 6 | import kotlinx.coroutines.test.resetMain 7 | import kotlinx.coroutines.test.setMain 8 | import org.junit.jupiter.api.extension.AfterEachCallback 9 | import org.junit.jupiter.api.extension.BeforeEachCallback 10 | import org.junit.jupiter.api.extension.ExtensionContext 11 | 12 | class TestKoinCoroutineExtension( 13 | private val testDispatcher: TestDispatcher = StandardTestDispatcher() 14 | ) : BeforeEachCallback, AfterEachCallback { 15 | 16 | override fun beforeEach(context: ExtensionContext) { 17 | Dispatchers.setMain(testDispatcher) 18 | } 19 | 20 | override fun afterEach(context: ExtensionContext) { 21 | Dispatchers.resetMain() 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /openai-client/openai-client-darwin/src/appleMain/kotlin/com/tddworks/openai/darwin/api/DarwinOpenAI.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.openai.darwin.api 2 | 3 | import com.tddworks.openai.api.OpenAI 4 | import com.tddworks.openai.api.OpenAIConfig 5 | import com.tddworks.openai.di.initOpenAI 6 | 7 | /** Object for accessing OpenAI API functions */ 8 | object DarwinOpenAI { 9 | 10 | /** 11 | * Function to initialize the OpenAI API client with the given API key and base URL. 12 | * 13 | * @param apiKey A lambda function that returns the API key to be used for authentication. 14 | * Defaults to "CONFIG_API_KEY". 15 | * @param baseUrl A lambda function that returns the base URL of the OpenAI API. Defaults to the 16 | * constant OpenAI.BASE_URL. 17 | */ 18 | fun openAI( 19 | apiKey: () -> String = { "CONFIG_API_KEY" }, 20 | baseUrl: () -> String = { OpenAI.BASE_URL }, 21 | ) = initOpenAI(OpenAIConfig(apiKey, baseUrl)) 22 | } 23 | -------------------------------------------------------------------------------- /common/src/commonMain/kotlin/com/tddworks/di/Koin.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.di 2 | 3 | import com.tddworks.common.network.api.ktor.internal.JsonLenient 4 | import org.koin.core.component.KoinComponent 5 | import org.koin.core.component.inject 6 | import org.koin.core.context.startKoin 7 | import org.koin.core.module.dsl.singleOf 8 | import org.koin.dsl.KoinAppDeclaration 9 | import org.koin.dsl.module 10 | 11 | fun initKoin(enableNetworkLogs: Boolean = false, appDeclaration: KoinAppDeclaration = {}) = 12 | startKoin { 13 | appDeclaration() 14 | modules(commonModule(enableNetworkLogs = enableNetworkLogs)) 15 | } 16 | 17 | // TODO - enableNetworkLogs is not used 18 | fun commonModule(enableNetworkLogs: Boolean) = module { singleOf(::createJson) } 19 | 20 | fun createJson() = JsonLenient 21 | 22 | inline fun getInstance(): T { 23 | return object : KoinComponent { 24 | val value: T by inject() 25 | } 26 | .value 27 | } 28 | -------------------------------------------------------------------------------- /anthropic-client/anthropic-client-core/src/commonMain/kotlin/com/tddworks/anthropic/api/messages/api/MessageContentSerializer.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.anthropic.api.messages.api 2 | 3 | /** 4 | * { "role": "user", "content": 5 | * [ { "type": "image", "source": { "type": "base64", "media_type": image1_media_type, "data": image1_data, }, }, { "type": "text", "text": "Describe this image." } ], 6 | * } 7 | */ 8 | // internal object MessageContentSerializer : 9 | // JsonContentPolymorphicSerializer(Content::class) { 10 | // override fun selectDeserializer(element: JsonElement): KSerializer { 11 | // val jsonObject = element.jsonObject 12 | // return when { 13 | // "source" in jsonObject -> BlockMessageContent.ImageContent.serializer() 14 | // "text" in jsonObject -> BlockMessageContent.TextContent.serializer() 15 | // else -> throw SerializationException("Unknown Content type") 16 | // } 17 | // 18 | // } 19 | // } 20 | -------------------------------------------------------------------------------- /openai-gateway/openai-gateway-core/src/jvmTest/kotlin/com/tddworks/azure/api/AzureAIProviderConfigTest.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.azure.api 2 | 3 | import com.tddworks.openai.gateway.api.OpenAIProviderConfig 4 | import org.junit.jupiter.api.Assertions.* 5 | import org.junit.jupiter.api.Test 6 | 7 | class AzureAIProviderConfigTest { 8 | 9 | @Test 10 | fun `should create AzureAIProviderConfig`() { 11 | val config = 12 | OpenAIProviderConfig.azure( 13 | apiKey = { "api-key" }, 14 | baseUrl = { "YOUR_RESOURCE_NAME.openai.azure.com" }, 15 | deploymentId = { "deployment-id" }, 16 | apiVersion = { "api-version" }, 17 | ) 18 | 19 | assertEquals("api-key", config.apiKey()) 20 | assertEquals("YOUR_RESOURCE_NAME.openai.azure.com", config.baseUrl()) 21 | assertEquals("deployment-id", config.deploymentId()) 22 | assertEquals("api-version", config.apiVersion()) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /anthropic-client/anthropic-client-core/src/jvmTest/kotlin/com/tddworks/anthropic/api/messages/api/internal/TestKoinCoroutineExtension.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.anthropic.api.messages.api.internal 2 | 3 | import kotlinx.coroutines.Dispatchers 4 | import kotlinx.coroutines.test.StandardTestDispatcher 5 | import kotlinx.coroutines.test.TestDispatcher 6 | import kotlinx.coroutines.test.resetMain 7 | import kotlinx.coroutines.test.setMain 8 | import org.junit.jupiter.api.extension.AfterEachCallback 9 | import org.junit.jupiter.api.extension.BeforeEachCallback 10 | import org.junit.jupiter.api.extension.ExtensionContext 11 | 12 | class TestKoinCoroutineExtension( 13 | private val testDispatcher: TestDispatcher = StandardTestDispatcher() 14 | ) : BeforeEachCallback, AfterEachCallback { 15 | override fun beforeEach(context: ExtensionContext) { 16 | Dispatchers.setMain(testDispatcher) 17 | } 18 | 19 | override fun afterEach(context: ExtensionContext) { 20 | Dispatchers.resetMain() 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /anthropic-client/anthropic-client-core/src/commonMain/kotlin/com/tddworks/anthropic/api/messages/api/Delta.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.anthropic.api.messages.api 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | /** 7 | * { "stop_reason": "end_turn", "stop_sequence": null, "usage": { "output_tokens": 15 } } or { 8 | * "type": "text_delta", "text": "!" } 9 | */ 10 | @Serializable 11 | data class Delta( 12 | val type: String? = null, 13 | val text: String? = null, 14 | @SerialName("stop_reason") val stopReason: String? = null, 15 | @SerialName("stop_sequence") val stopSequence: String? = null, 16 | val usage: Usage? = null, 17 | ) { 18 | companion object { 19 | fun dummy() = 20 | Delta( 21 | type = "text_delta", 22 | text = "Hello", 23 | stopReason = "end_turn", 24 | stopSequence = null, 25 | usage = Usage(outputTokens = 15), 26 | ) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /gemini-client/gemini-client-core/src/jvmTest/kotlin/com/tddworks/gemini/api/textGeneration/api/GenerationConfigTest.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.gemini.api.textGeneration.api 2 | 3 | import kotlinx.serialization.json.Json 4 | import org.junit.jupiter.api.Test 5 | import org.skyscreamer.jsonassert.JSONAssert 6 | 7 | class GenerationConfigTest { 8 | 9 | @Test 10 | fun `should return correct generation config`() { 11 | // Given 12 | val generationConfig = 13 | GenerationConfig(temperature = 1.0f, responseMimeType = "application/json") 14 | 15 | // When 16 | val result = Json.encodeToString(GenerationConfig.serializer(), generationConfig) 17 | 18 | // Then 19 | JSONAssert.assertEquals( 20 | """ 21 | { 22 | "temperature": 1.0, 23 | "response_mime_type": "application/json" 24 | } 25 | """ 26 | .trimIndent(), 27 | result, 28 | false, 29 | ) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /openai-gateway/openai-gateway-core/src/commonMain/kotlin/com/tddworks/openai/gateway/api/internal/OllamaOpenAIProviderConfig.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.openai.gateway.api.internal 2 | 3 | import com.tddworks.ollama.api.OllamaConfig 4 | import com.tddworks.openai.gateway.api.OpenAIProviderConfig 5 | 6 | data class OllamaOpenAIProviderConfig( 7 | val port: () -> Int = { 11434 }, 8 | val protocol: () -> String = { "http" }, 9 | override val baseUrl: () -> String = { "http//:localhost:11434" }, 10 | override val apiKey: () -> String = { "ollama-ignore-this" }, 11 | ) : OpenAIProviderConfig 12 | 13 | fun OllamaOpenAIProviderConfig.toOllamaConfig() = 14 | OllamaConfig(baseUrl = baseUrl, protocol = protocol, port = port) 15 | 16 | fun OpenAIProviderConfig.Companion.ollama( 17 | apiKey: () -> String = { "ollama-ignore-this" }, 18 | baseUrl: () -> String = { "localhost" }, 19 | protocol: () -> String = { "http" }, 20 | port: () -> Int = { 11434 }, 21 | ) = OllamaOpenAIProviderConfig(port, protocol, baseUrl, apiKey) 22 | -------------------------------------------------------------------------------- /common/src/commonMain/kotlin/com/tddworks/common/network/api/ktor/internal/ConnectionConfig.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.common.network.api.ktor.internal 2 | 3 | import io.ktor.client.plugins.* 4 | import io.ktor.http.* 5 | 6 | interface ConnectionConfig { 7 | fun setupUrl(builder: DefaultRequest.DefaultRequestBuilder) { 8 | builder.setupUrl(this) 9 | } 10 | } 11 | 12 | private fun DefaultRequest.DefaultRequestBuilder.setupUrl(connectionConfig: ConnectionConfig) { 13 | when (connectionConfig) { 14 | is HostPortConnectionConfig -> setupHostPortConnectionConfig(connectionConfig) 15 | is UrlBasedConnectionConfig -> url.takeFrom(connectionConfig.baseUrl()) 16 | } 17 | } 18 | 19 | private fun DefaultRequest.DefaultRequestBuilder.setupHostPortConnectionConfig( 20 | config: HostPortConnectionConfig 21 | ) { 22 | url { 23 | protocol = config.protocol()?.let { URLProtocol.createOrDefault(it) } ?: URLProtocol.HTTPS 24 | host = config.host() 25 | config.port()?.let { port = it } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /openai-client/openai-client-core/src/jvmTest/kotlin/com/tddworks/openai/api/legacy/completions/api/CompletionRequestTest.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.openai.api.legacy.completions.api 2 | 3 | import com.tddworks.openai.api.common.prettyJson 4 | import kotlinx.serialization.encodeToString 5 | import org.junit.jupiter.api.Assertions.* 6 | import org.junit.jupiter.api.Test 7 | 8 | class CompletionRequestTest { 9 | @Test 10 | fun `should return to correct stream json`() { 11 | val chatCompletionRequest = 12 | CompletionRequest.asStream(prompt = "some-prompt", suffix = "some-suffix") 13 | 14 | val result = prettyJson.encodeToString(chatCompletionRequest) 15 | 16 | assertEquals( 17 | """ 18 | { 19 | "model": "gpt-3.5-turbo-instruct", 20 | "prompt": "some-prompt", 21 | "stream": true, 22 | "suffix": "some-suffix" 23 | } 24 | """ 25 | .trimIndent(), 26 | result, 27 | ) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /anthropic-client/anthropic-client-core/src/jvmTest/kotlin/com/tddworks/anthropic/api/messages/api/CreateMessageResponseTest.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.anthropic.api.messages.api 2 | 3 | import org.junit.jupiter.api.Assertions.* 4 | import org.junit.jupiter.api.Test 5 | 6 | class CreateMessageResponseTest { 7 | 8 | @Test 9 | fun `should create a dummy CreateMessageResponse`() { 10 | 11 | val dummyResponse = CreateMessageResponse.dummy() 12 | 13 | assertEquals("msg_1nZdL29xx5MUA1yADyHTEsnR8uuvGzszyY", dummyResponse.id) 14 | assertEquals("claude-3-opus-20240229", dummyResponse.model) 15 | assertEquals("assistant", dummyResponse.role) 16 | assertNull(dummyResponse.stopReason) 17 | assertNull(dummyResponse.stopSequence) 18 | assertEquals("message", dummyResponse.type) 19 | assertEquals(Usage(25, 1), dummyResponse.usage) 20 | assertEquals( 21 | listOf(ContentMessage("Hi! My name is Claude.", "text")), 22 | dummyResponse.content, 23 | ) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /openai-client/openai-client-core/src/jvmTest/kotlin/com/tddworks/openai/di/OpenAIKoinTest.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.openai.di 2 | 3 | import com.tddworks.di.getInstance 4 | import com.tddworks.openai.api.OpenAI 5 | import com.tddworks.openai.api.OpenAIConfig 6 | import kotlinx.serialization.json.Json 7 | import org.junit.jupiter.api.Test 8 | import org.koin.dsl.koinApplication 9 | import org.koin.test.check.checkModules 10 | import org.koin.test.junit5.AutoCloseKoinTest 11 | 12 | class OpenAIKoinTest : AutoCloseKoinTest() { 13 | @Test 14 | fun `should initialize common koin modules`() { 15 | koinApplication { 16 | initOpenAI( 17 | OpenAIConfig( 18 | baseUrl = { OpenAI.BASE_URL }, 19 | apiKey = { System.getenv("OPENAI_API_KEY") ?: "CONFIGURE_ME" }, 20 | ) 21 | ) 22 | } 23 | .checkModules() 24 | 25 | val json = getInstance() 26 | 27 | kotlin.test.assertNotNull(json) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /openai-gateway/openai-gateway-core/src/commonMain/kotlin/com/tddworks/openai/gateway/api/internal/AnthropicOpenAIProviderConfig.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.openai.gateway.api.internal 2 | 3 | import com.tddworks.anthropic.api.Anthropic 4 | import com.tddworks.anthropic.api.AnthropicConfig 5 | import com.tddworks.openai.gateway.api.OpenAIProviderConfig 6 | 7 | class AnthropicOpenAIProviderConfig( 8 | val anthropicVersion: () -> String = { Anthropic.ANTHROPIC_VERSION }, 9 | override val apiKey: () -> String, 10 | override val baseUrl: () -> String = { Anthropic.BASE_URL }, 11 | ) : OpenAIProviderConfig 12 | 13 | fun AnthropicOpenAIProviderConfig.toAnthropicOpenAIConfig() = 14 | AnthropicConfig(apiKey = apiKey, baseUrl = baseUrl, anthropicVersion = anthropicVersion) 15 | 16 | fun OpenAIProviderConfig.Companion.anthropic( 17 | apiKey: () -> String, 18 | baseUrl: () -> String = { Anthropic.BASE_URL }, 19 | anthropicVersion: () -> String = { Anthropic.ANTHROPIC_VERSION }, 20 | ) = AnthropicOpenAIProviderConfig(anthropicVersion, apiKey, baseUrl) 21 | -------------------------------------------------------------------------------- /openai-client/openai-client-core/src/jvmTest/kotlin/com/tddworks/common/network/api/ktor/internal/HostPortConnectionConfigTest.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.common.network.api.ktor.internal 2 | 3 | import org.junit.jupiter.api.Assertions.* 4 | import org.junit.jupiter.api.Test 5 | 6 | class HostPortConnectionConfigTest { 7 | 8 | @Test 9 | fun `should return default protocol and port`() { 10 | val config = HostPortConnectionConfig { "example.com" } 11 | 12 | assertNull(config.protocol(), "Protocol should default to null") 13 | 14 | assertNull(config.port(), "Port should default to null") 15 | } 16 | 17 | @Test 18 | fun `should able to specified protocol and port`() { 19 | val protocol: () -> String? = { "http" } 20 | val port: () -> Int? = { 8080 } 21 | val config = 22 | HostPortConnectionConfig(protocol = protocol, port = port, host = { "example.com" }) 23 | 24 | assertEquals("http", config.protocol(), "Protocol should be 'http'") 25 | assertEquals(8080, config.port(), "Port should be 8080") 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /anthropic-client/anthropic-client-core/src/commonMain/kotlin/com/tddworks/anthropic/api/messages/api/internal/JsonLenient.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.anthropic.api.messages.api.internal 2 | 3 | import kotlinx.serialization.json.Json 4 | 5 | /** 6 | * Represents a JSON object that allows for leniency and ignores unknown keys. 7 | * 8 | * @property isLenient Removes JSON specification restriction (RFC-4627) and makes parser more 9 | * liberal to the malformed input. In lenient mode quoted boolean literals, and unquoted string 10 | * literals are allowed. Its relaxations can be expanded in the future, so that lenient parser 11 | * becomes even more permissive to invalid value in the input, replacing them with defaults. false 12 | * by default. 13 | * @property ignoreUnknownKeys Specifies whether encounters of unknown properties in the input JSON 14 | * should be ignored instead of throwing SerializationException. false by default.. 15 | */ 16 | val JsonLenient = Json { 17 | isLenient = true 18 | ignoreUnknownKeys = true 19 | encodeDefaults = true 20 | explicitNulls = false 21 | } 22 | -------------------------------------------------------------------------------- /gemini-client/gemini-client-core/src/commonMain/kotlin/com/tddworks/gemini/api/textGeneration/api/PartSerializer.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.gemini.api.textGeneration.api 2 | 3 | import kotlinx.serialization.KSerializer 4 | import kotlinx.serialization.SerializationException 5 | import kotlinx.serialization.json.JsonContentPolymorphicSerializer 6 | import kotlinx.serialization.json.JsonElement 7 | import kotlinx.serialization.json.jsonObject 8 | 9 | /** 10 | * { "contents": 11 | * [{ "parts":[ {"text": "Tell me about this instrument"}, { "inline_data": { "mime_type":"image/jpeg", "data": "$(cat "$TEMP_B64")" } } ] 12 | * }] } 13 | */ 14 | object PartSerializer : JsonContentPolymorphicSerializer(Part::class) { 15 | override fun selectDeserializer(element: JsonElement): KSerializer { 16 | val jsonObject = element.jsonObject 17 | return when { 18 | "text" in jsonObject -> Part.TextPart.serializer() 19 | "inline_data" in jsonObject -> Part.InlineDataPart.serializer() 20 | else -> throw SerializationException("Unknown Part type") 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /openai-client/openai-client-core/src/commonMain/kotlin/com/tddworks/openai/api/chat/api/vision/VisionMessageContentSerializer.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.openai.api.chat.api.vision 2 | 3 | import kotlinx.serialization.KSerializer 4 | import kotlinx.serialization.json.JsonContentPolymorphicSerializer 5 | import kotlinx.serialization.json.JsonElement 6 | import kotlinx.serialization.json.jsonObject 7 | import kotlinx.serialization.json.jsonPrimitive 8 | 9 | object VisionMessageContentSerializer : 10 | JsonContentPolymorphicSerializer(VisionMessageContent::class) { 11 | override fun selectDeserializer(element: JsonElement): KSerializer { 12 | val typeObject = element.jsonObject["type"] 13 | val jsonPrimitive = typeObject?.jsonPrimitive 14 | val type = jsonPrimitive?.content 15 | 16 | return when (type) { 17 | ContentType.TEXT.value -> VisionMessageContent.TextContent.serializer() 18 | ContentType.IMAGE.value -> VisionMessageContent.ImageContent.serializer() 19 | else -> throw IllegalArgumentException("Unknown type") 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /anthropic-client/anthropic-client-darwin/src/appleMain/kotlin/com/tddworks/anthropic/darwin/api/DarwinAnthropic.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.anthropic.darwin.api 2 | 3 | import com.tddworks.anthropic.api.Anthropic 4 | import com.tddworks.anthropic.api.AnthropicConfig 5 | import com.tddworks.anthropic.di.iniAnthropic 6 | 7 | /** Object responsible for setting up and initializing the Anthropoc API client. */ 8 | object DarwinAnthropic { 9 | 10 | /** 11 | * Initializes the Anthropic library with the provided configuration parameters. 12 | * 13 | * @param apiKey a lambda function that returns the API key to be used for authentication 14 | * @param baseUrl a lambda function that returns the base URL of the Anthropic API 15 | * @param anthropicVersion a lambda function that returns the version of the Anthropic API to 16 | * use 17 | */ 18 | fun anthropic( 19 | apiKey: () -> String = { "CONFIG_API_KEY" }, 20 | baseUrl: () -> String = { Anthropic.BASE_URL }, 21 | anthropicVersion: () -> String = { Anthropic.ANTHROPIC_VERSION }, 22 | ) = iniAnthropic(AnthropicConfig(apiKey, baseUrl, anthropicVersion)) 23 | } 24 | -------------------------------------------------------------------------------- /ollama-client/ollama-client-core/src/commonMain/kotlin/com/tddworks/ollama/api/json/JsonLenient.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.ollama.api.json 2 | 3 | import kotlinx.serialization.json.Json 4 | 5 | /** 6 | * Represents a JSON object that allows for leniency and ignores unknown keys. 7 | * 8 | * @property isLenient Removes JSON specification restriction (RFC-4627) and makes parser more 9 | * liberal to the malformed input. In lenient mode quoted boolean literals, and unquoted string 10 | * literals are allowed. Its relaxations can be expanded in the future, so that lenient parser 11 | * becomes even more permissive to invalid value in the input, replacing them with defaults. false 12 | * by default. 13 | * @property ignoreUnknownKeys Specifies whether encounters of unknown properties in the input JSON 14 | * should be ignored instead of throwing SerializationException. false by default.. 15 | */ 16 | val JsonLenient = Json { 17 | isLenient = true 18 | ignoreUnknownKeys = true 19 | // https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/json.md#class-discriminator-for-polymorphism 20 | encodeDefaults = true 21 | explicitNulls = false 22 | } 23 | -------------------------------------------------------------------------------- /common/src/jvmTest/kotlin/com/tddworks/common/network/api/JsonUtils.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.common.network.api 2 | 3 | import kotlinx.serialization.json.Json 4 | 5 | val prettyJson = Json { // this returns the JsonBuilder 6 | prettyPrint = true 7 | ignoreUnknownKeys = true 8 | // optional: specify indent 9 | prettyPrintIndent = " " 10 | } 11 | 12 | /** 13 | * Represents a JSON object that allows for leniency and ignores unknown keys. 14 | * 15 | * @property isLenient Removes JSON specification restriction (RFC-4627) and makes parser more 16 | * liberal to the malformed input. In lenient mode quoted boolean literals, and unquoted string 17 | * literals are allowed. Its relaxations can be expanded in the future, so that lenient parser 18 | * becomes even more permissive to invalid value in the input, replacing them with defaults. false 19 | * by default. 20 | * @property ignoreUnknownKeys Specifies whether encounters of unknown properties in the input JSON 21 | * should be ignored instead of throwing SerializationException. false by default.. 22 | */ 23 | internal val JsonLenient = Json { 24 | isLenient = true 25 | ignoreUnknownKeys = true 26 | } 27 | -------------------------------------------------------------------------------- /openai-client/openai-client-core/src/jvmTest/kotlin/com/tddworks/openai/api/chat/api/ChatChoiceTest.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.openai.api.chat.api 2 | 3 | import com.tddworks.openai.api.common.prettyJson 4 | import org.junit.jupiter.api.Assertions.* 5 | import org.junit.jupiter.api.Test 6 | 7 | class ChatChoiceTest { 8 | 9 | @Test 10 | fun `should return chat choice from json`() { 11 | val json = 12 | """ 13 | { 14 | "index": 0, 15 | "message": { 16 | "role": "assistant", 17 | "content": "Hello! How can I assist you today?" 18 | }, 19 | "logprobs": null, 20 | "finish_reason": "stop" 21 | } 22 | """ 23 | .trimIndent() 24 | 25 | val chatChoice = prettyJson.decodeFromString(ChatChoice.serializer(), json) 26 | 27 | with(chatChoice) { 28 | assertEquals(0, index) 29 | assertEquals("Hello! How can I assist you today?", message.content) 30 | assertEquals("assistant", message.role.name) 31 | assertEquals("stop", finishReason?.value) 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /ollama-client/ollama-client-core/src/commonMain/kotlin/com/tddworks/ollama/api/internal/OllamaApi.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.ollama.api.internal 2 | 3 | import com.tddworks.di.getInstance 4 | import com.tddworks.ollama.api.Ollama 5 | import com.tddworks.ollama.api.OllamaConfig 6 | import com.tddworks.ollama.api.chat.OllamaChat 7 | import com.tddworks.ollama.api.generate.OllamaGenerate 8 | 9 | class OllamaApi( 10 | private val config: OllamaConfig, 11 | private val ollamaChat: OllamaChat, 12 | private val ollamaGenerate: OllamaGenerate, 13 | ) : Ollama, OllamaChat by ollamaChat, OllamaGenerate by ollamaGenerate { 14 | 15 | override fun baseUrl(): String { 16 | return config.baseUrl() 17 | } 18 | 19 | override fun port(): Int { 20 | return config.port() 21 | } 22 | 23 | override fun protocol(): String { 24 | return config.protocol() 25 | } 26 | } 27 | 28 | fun Ollama.Companion.create( 29 | config: OllamaConfig, 30 | ollamaChat: OllamaChat = getInstance(), 31 | ollamaGenerate: OllamaGenerate = getInstance(), 32 | ): Ollama { 33 | return OllamaApi(config = config, ollamaChat = ollamaChat, ollamaGenerate = ollamaGenerate) 34 | } 35 | -------------------------------------------------------------------------------- /openai-client/openai-client-core/src/commonMain/kotlin/com/tddworks/openai/api/chat/api/Chat.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.openai.api.chat.api 2 | 3 | import kotlinx.coroutines.flow.Flow 4 | import kotlinx.serialization.ExperimentalSerializationApi 5 | 6 | /** 7 | * Chat API - https://platform.openai.com/docs/api-reference/chat Given a list of messages 8 | * comprising a conversation, the model will return a response. Related guide: Chat Completions 9 | */ 10 | @OptIn(ExperimentalSerializationApi::class) 11 | interface Chat { 12 | /** 13 | * Create a chat completion. 14 | * 15 | * @param request The request to create a chat completion. 16 | * @return The chat completion. 17 | */ 18 | suspend fun chatCompletions(request: ChatCompletionRequest): ChatCompletion 19 | 20 | /** 21 | * Stream a chat completion. 22 | * 23 | * @param request The request to stream a chat completion. 24 | * @return The chat completion chunks as a stream 25 | */ 26 | fun streamChatCompletions(request: ChatCompletionRequest): Flow 27 | 28 | companion object { 29 | const val CHAT_COMPLETIONS_PATH = "/v1/chat/completions" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /openai-client/openai-client-core/src/jvmTest/kotlin/com/tddworks/openai/api/common/JsonUtils.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.openai.api.common 2 | 3 | import kotlinx.serialization.json.Json 4 | 5 | val prettyJson = Json { // this returns the JsonBuilder 6 | prettyPrint = true 7 | ignoreUnknownKeys = true 8 | // optional: specify indent 9 | prettyPrintIndent = " " 10 | } 11 | 12 | /** 13 | * Represents a JSON object that allows for leniency and ignores unknown keys. 14 | * 15 | * @property isLenient Removes JSON specification restriction (RFC-4627) and makes parser more 16 | * liberal to the malformed input. In lenient mode quoted boolean literals, and unquoted string 17 | * literals are allowed. Its relaxations can be expanded in the future, so that lenient parser 18 | * becomes even more permissive to invalid value in the input, replacing them with defaults. false 19 | * by default. 20 | * @property ignoreUnknownKeys Specifies whether encounters of unknown properties in the input JSON 21 | * should be ignored instead of throwing SerializationException. false by default.. 22 | */ 23 | internal val JsonLenient = Json { 24 | isLenient = true 25 | ignoreUnknownKeys = true 26 | } 27 | -------------------------------------------------------------------------------- /openai-gateway/openai-gateway-core/src/jvmTest/kotlin/com/tddworks/openai/gateway/di/KoinTest.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.openai.gateway.di 2 | 3 | import com.tddworks.openai.gateway.api.internal.AnthropicOpenAIProviderConfig 4 | import com.tddworks.openai.gateway.api.internal.DefaultOpenAIProviderConfig 5 | import com.tddworks.openai.gateway.api.internal.GeminiOpenAIProviderConfig 6 | import com.tddworks.openai.gateway.api.internal.OllamaOpenAIProviderConfig 7 | import org.junit.jupiter.api.Test 8 | import org.koin.dsl.koinApplication 9 | import org.koin.test.KoinTest 10 | import org.koin.test.check.checkModules 11 | 12 | class OpenAIGatewayKoinTest : KoinTest { 13 | 14 | @Test 15 | fun `should initialize OpenAI Gateway Koin modules`() { 16 | val openAIConfig = DefaultOpenAIProviderConfig(apiKey = { "" }) 17 | val anthropicConfig = AnthropicOpenAIProviderConfig(apiKey = { "" }) 18 | val ollamaConfig = OllamaOpenAIProviderConfig() 19 | val geminiConfig = GeminiOpenAIProviderConfig(apiKey = { "" }) 20 | 21 | koinApplication { 22 | initOpenAIGateway(openAIConfig, anthropicConfig, ollamaConfig, geminiConfig) 23 | } 24 | .checkModules() 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /anthropic-client/anthropic-client-core/src/jvmTest/kotlin/com/tddworks/anthropic/api/messages/api/UsageTest.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.anthropic.api.messages.api 2 | 3 | import kotlinx.serialization.encodeToString 4 | import kotlinx.serialization.json.Json 5 | import org.junit.jupiter.api.Assertions.assertEquals 6 | import org.junit.jupiter.api.Test 7 | 8 | class UsageTest { 9 | @Test 10 | fun `should create a empty Usage`() { 11 | val usage = Usage() 12 | val json = Json.encodeToString(usage) 13 | 14 | val expectedJson = "{}" 15 | assertEquals(expectedJson, json) 16 | } 17 | 18 | @Test 19 | fun `should create a dummy Usage`() { 20 | val usage = Usage(inputTokens = 10, outputTokens = 20) 21 | val json = Json.encodeToString(usage) 22 | 23 | val expectedJson = "{\"input_tokens\":10,\"output_tokens\":20}" 24 | assertEquals(expectedJson, json) 25 | } 26 | 27 | @Test 28 | fun `should parse a Usage from json`() { 29 | val json = "{\"input_tokens\":5,\"output_tokens\":15}" 30 | val usage = Json.decodeFromString(json) 31 | 32 | assertEquals(5, usage.inputTokens) 33 | assertEquals(15, usage.outputTokens) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /openai-client/openai-client-core/src/commonMain/kotlin/com/tddworks/openai/api/images/internal/DefaultImagesApi.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.openai.api.images.internal 2 | 3 | import com.tddworks.common.network.api.ktor.api.HttpRequester 4 | import com.tddworks.common.network.api.ktor.api.ListResponse 5 | import com.tddworks.common.network.api.ktor.api.performRequest 6 | import com.tddworks.openai.api.images.api.Image 7 | import com.tddworks.openai.api.images.api.ImageCreate 8 | import com.tddworks.openai.api.images.api.Images 9 | import io.ktor.client.request.* 10 | import io.ktor.http.* 11 | import kotlinx.serialization.ExperimentalSerializationApi 12 | 13 | internal class DefaultImagesApi(private val requester: HttpRequester) : Images { 14 | @OptIn(ExperimentalSerializationApi::class) 15 | override suspend fun generate(request: ImageCreate): ListResponse { 16 | return requester.performRequest> { 17 | method = HttpMethod.Post 18 | url(path = Images.IMAGES_GENERATIONS_PATH) 19 | setBody(request) 20 | contentType(ContentType.Application.Json) 21 | } 22 | } 23 | } 24 | 25 | fun Images.Companion.default(requester: HttpRequester): Images = DefaultImagesApi(requester) 26 | -------------------------------------------------------------------------------- /openai-client/openai-client-core/src/commonMain/kotlin/com/tddworks/openai/api/legacy/completions/api/internal/DefaultCompletionsApi.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.openai.api.legacy.completions.api.internal 2 | 3 | import com.tddworks.common.network.api.ktor.api.HttpRequester 4 | import com.tddworks.common.network.api.ktor.api.performRequest 5 | import com.tddworks.openai.api.legacy.completions.api.Completion 6 | import com.tddworks.openai.api.legacy.completions.api.CompletionRequest 7 | import com.tddworks.openai.api.legacy.completions.api.Completions 8 | import io.ktor.client.request.* 9 | import io.ktor.http.* 10 | 11 | internal class DefaultCompletionsApi(private val requester: HttpRequester) : Completions { 12 | override suspend fun completions(request: CompletionRequest): Completion { 13 | return requester.performRequest { 14 | method = HttpMethod.Post 15 | url(path = COMPLETIONS_PATH) 16 | setBody(request) 17 | contentType(ContentType.Application.Json) 18 | } 19 | } 20 | 21 | companion object { 22 | const val COMPLETIONS_PATH = "/v1/completions" 23 | } 24 | } 25 | 26 | fun Completions.Companion.default(requester: HttpRequester): Completions = 27 | DefaultCompletionsApi(requester) 28 | -------------------------------------------------------------------------------- /openai-gateway/openai-gateway-core/src/commonMain/kotlin/com/tddworks/azure/api/internal/AzureChatApi.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.azure.api.internal 2 | 3 | import com.tddworks.common.network.api.ktor.api.HttpRequester 4 | import com.tddworks.openai.api.chat.api.Chat 5 | import com.tddworks.openai.api.chat.internal.default 6 | 7 | internal class AzureChatApi( 8 | private val chatCompletionPath: String, 9 | private val requester: HttpRequester, 10 | private val extraHeaders: Map = mapOf(), 11 | private val chatApi: Chat = 12 | Chat.default( 13 | requester = requester, 14 | chatCompletionPath = chatCompletionPath, 15 | extraHeaders = extraHeaders, 16 | ), 17 | ) : Chat by chatApi { 18 | companion object { 19 | const val BASE_URL = "https://YOUR_RESOURCE_NAME.openai.azure.com" 20 | const val CHAT_COMPLETIONS = "chat/completions" 21 | } 22 | } 23 | 24 | fun Chat.Companion.azure( 25 | apiKey: () -> String, 26 | requester: HttpRequester, 27 | chatCompletionPath: String = AzureChatApi.CHAT_COMPLETIONS, 28 | ): Chat = 29 | AzureChatApi( 30 | requester = requester, 31 | chatCompletionPath = chatCompletionPath, 32 | extraHeaders = mapOf("api-key" to apiKey()), 33 | ) 34 | -------------------------------------------------------------------------------- /gemini-client/gemini-client-darwin/src/appleMain/kotlin/com/tddworks/gemini/api/DarwinGemini.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.gemini.api 2 | 3 | import com.tddworks.gemini.api.textGeneration.api.Gemini 4 | import com.tddworks.gemini.api.textGeneration.api.GeminiConfig 5 | import com.tddworks.gemini.di.initGemini 6 | 7 | /** 8 | * A singleton object that initializes the Gemini configuration with the specified API key and base 9 | * URL. 10 | */ 11 | object DarwinGemini { 12 | 13 | /** 14 | * Initializes the Gemini configuration with the specified API key and base URL. 15 | * 16 | * This function sets up the Gemini environment by creating a configuration using the provided 17 | * API key and base URL, then initializing Gemini with this configuration. 18 | * 19 | * @param apiKey A lambda function that returns the API key as a string. Defaults to returning 20 | * "CONFIG_API_KEY". 21 | * @param baseUrl A lambda function that returns the base URL as a string. Defaults to returning 22 | * `Gemini.BASE_URL`. 23 | * @return The initialized Gemini configuration. 24 | */ 25 | fun gemini( 26 | apiKey: () -> String = { "CONFIG_API_KEY" }, 27 | baseUrl: () -> String = { Gemini.BASE_URL }, 28 | ): Gemini = initGemini(GeminiConfig(apiKey, baseUrl)).koin.get() 29 | } 30 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: GitHub Actions - CI 2 | 3 | on: 4 | workflow_dispatch: # Start a workflow 5 | push: 6 | branches: 7 | - main 8 | pull_request: 9 | branches: 10 | - main 11 | 12 | jobs: 13 | build: 14 | runs-on: macOS-latest 15 | env: 16 | SIGNING_KEY: ${{ secrets.SIGNING_KEY }} 17 | SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }} 18 | steps: 19 | - uses: actions/checkout@v5 20 | with: 21 | fetch-depth: 0 22 | - uses: actions/cache@v4 23 | with: 24 | path: | 25 | ~/.gradle/caches 26 | ~/.gradle/wrapper 27 | key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} 28 | restore-keys: | 29 | ${{ runner.os }}-gradle- 30 | - name: Set up JDK 31 | uses: actions/setup-java@v5 32 | with: 33 | java-version: 17 34 | distribution: 'zulu' 35 | - name: Build with Gradle 36 | run: ./gradlew clean build koverVerify koverXmlReport 37 | - name: Upload coverage reports to Codecov 38 | uses: codecov/codecov-action@v5 39 | with: 40 | token: ${{ secrets.CODECOV_TOKEN }} 41 | slug: tddworks/openai-kotlin 42 | files: ${{ github.workspace }}/build/reports/kover/report.xml -------------------------------------------------------------------------------- /openai-client/openai-client-core/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.kotlinx.serialization) 3 | alias(libs.plugins.kover) 4 | `maven-publish` 5 | } 6 | 7 | kotlin { 8 | jvm() 9 | macosArm64() 10 | macosX64() 11 | iosArm64() 12 | iosSimulatorArm64() 13 | sourceSets { 14 | commonMain.dependencies { 15 | // put your Multiplatform dependencies here 16 | api(projects.common) 17 | } 18 | 19 | commonTest.dependencies { implementation(libs.ktor.client.mock) } 20 | 21 | macosMain.dependencies { api(libs.ktor.client.darwin) } 22 | 23 | jvmMain.dependencies { api(libs.ktor.client.cio) } 24 | 25 | jvmTest.dependencies { 26 | implementation(project.dependencies.platform(libs.junit.bom)) 27 | implementation(libs.bundles.jvm.test) 28 | implementation(libs.kotlinx.coroutines.test) 29 | implementation(libs.koin.test) 30 | implementation(libs.koin.test.junit5) 31 | implementation(libs.app.cash.turbine) 32 | implementation("com.tngtech.archunit:archunit-junit5:1.4.1") 33 | implementation("org.reflections:reflections:0.10.2") 34 | implementation("org.junit.platform:junit-platform-launcher") 35 | } 36 | } 37 | } 38 | 39 | tasks { named("jvmTest") { useJUnitPlatform() } } 40 | -------------------------------------------------------------------------------- /ollama-client/ollama-client-darwin/src/appleMain/kotlin/com/tddworks/ollama/api/DarwinOllama.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.ollama.api 2 | 3 | import com.tddworks.ollama.di.initOllama 4 | 5 | /** 6 | * Creates an instance of Ollama with the specified configuration options. 7 | * 8 | * @param port a function that returns the port number for the Ollama instance, defaults to 9 | * Ollama.PORT 10 | * @param protocol a function that returns the protocol for the Ollama instance, defaults to 11 | * Ollama.PROTOCOL 12 | * @param baseUrl a function that returns the base URL for the Ollama instance, defaults to 13 | * Ollama.BASE_URL 14 | * @return an Ollama instance initialized with the provided configuration 15 | */ 16 | object DarwinOllama { 17 | 18 | /** 19 | * Function to create an Ollama instance with the provided configuration. 20 | * 21 | * @param port function returning an integer representing the port, defaults to Ollama.PORT 22 | * @param protocol function returning a string representing the protocol, defaults to 23 | * Ollama.PROTOCOL 24 | * @param baseUrl function returning a string representing the base URL, defaults to 25 | * Ollama.BASE_URL 26 | * @return an Ollama instance created with the provided configuration 27 | */ 28 | fun ollama(baseUrl: () -> String = { Ollama.BASE_URL }): Ollama = 29 | initOllama(OllamaConfig(baseUrl = baseUrl)) 30 | } 31 | -------------------------------------------------------------------------------- /ollama-client/ollama-client-core/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.kotlinx.serialization) 3 | alias(libs.plugins.kover) 4 | `maven-publish` 5 | } 6 | 7 | kotlin { 8 | jvm() 9 | macosArm64() 10 | iosArm64() 11 | iosSimulatorArm64() 12 | 13 | sourceSets { 14 | commonMain.dependencies { 15 | // put your Multiplatform dependencies here 16 | api(projects.common) 17 | } 18 | 19 | commonTest.dependencies { 20 | implementation(libs.ktor.client.mock) 21 | api(projects.common) 22 | } 23 | 24 | macosMain.dependencies { api(libs.ktor.client.darwin) } 25 | 26 | jvmMain.dependencies { api(libs.ktor.client.cio) } 27 | 28 | jvmTest.dependencies { 29 | implementation(project.dependencies.platform(libs.junit.bom)) 30 | implementation(libs.bundles.jvm.test) 31 | implementation(libs.kotlinx.coroutines.test) 32 | implementation(libs.koin.test) 33 | implementation(libs.koin.test.junit5) 34 | implementation(libs.app.cash.turbine) 35 | implementation("com.tngtech.archunit:archunit-junit5:1.4.1") 36 | implementation("org.reflections:reflections:0.10.2") 37 | implementation("org.junit.platform:junit-platform-launcher") 38 | } 39 | } 40 | } 41 | 42 | tasks { named("jvmTest") { useJUnitPlatform() } } 43 | -------------------------------------------------------------------------------- /common/src/commonMain/kotlin/com/tddworks/common/network/api/ktor/internal/exception/OpenLLMErrorDetails.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.common.network.api.ktor.internal.exception 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | /** 7 | * Represents an error response from the OpenAI API. 8 | * 9 | * @param detail information about the error that occurred. 10 | */ 11 | @Serializable data class OpenAIError(@SerialName("error") val detail: OpenAIErrorDetails?) 12 | 13 | /** 14 | * Represents an error object returned by the OpenAI API. { "code": null, "type": "server_error", 15 | * "param": null, "message": "That model is currently overloaded with other requests. You can retry 16 | * your request, or contact us through our help center at help.openai.com if the error persists. 17 | * (Please include the request ID c58c33110e4907638de58bec34af86e5 in your message.)" } 18 | * 19 | * @param code error code returned by the OpenAI API. 20 | * @param message human-readable error message describing the error that occurred. 21 | * @param param the parameter that caused the error, if applicable. 22 | * @param type the type of error that occurred. 23 | */ 24 | @Serializable 25 | data class OpenAIErrorDetails( 26 | @SerialName("code") val code: String?, 27 | @SerialName("message") val message: String?, 28 | @SerialName("param") val param: String? = null, 29 | @SerialName("type") val type: String? = null, 30 | ) 31 | -------------------------------------------------------------------------------- /gemini-client/gemini-client-core/src/commonMain/kotlin/com/tddworks/gemini/api/textGeneration/api/TextGeneration.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.gemini.api.textGeneration.api 2 | 3 | import kotlinx.coroutines.flow.Flow 4 | 5 | /** https://ai.google.dev/api/generate-content#v1beta.Candidate */ 6 | interface TextGeneration { 7 | suspend fun generateContent(request: GenerateContentRequest): GenerateContentResponse 8 | 9 | /** 10 | * data: {"candidates": 11 | * [{"content": {"parts": [{"text": " understand that AI is a constantly evolving field. New techniques and approaches are continually being developed, pushing the boundaries of what's possible. While AI can achieve impressive feats, it's important to remember that it's a tool, and its capabilities are limited by the data it's trained on and the algorithms"}],"role": 12 | * "model"}}],"usageMetadata": {"promptTokenCount": 4,"totalTokenCount": 4},"modelVersion": 13 | * "gemini-1.5-flash"} 14 | * 15 | * data: {"candidates": 16 | * [{"content": {"parts": [{"text": " it uses. It doesn't possess consciousness or genuine understanding in the human sense.\n"}],"role": 17 | * "model"},"finishReason": "STOP"}],"usageMetadata": {"promptTokenCount": 18 | * 4,"candidatesTokenCount": 724,"totalTokenCount": 728},"modelVersion": "gemini-1.5-flash"} 19 | */ 20 | fun streamGenerateContent(request: GenerateContentRequest): Flow 21 | 22 | companion object 23 | } 24 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | #Gradle 2 | org.gradle.jvmargs=-Xmx4048M -Dfile.encoding=UTF-8 -Dkotlin.daemon.jvm.options\="-Xmx4048M" 3 | org.gradle.caching=true 4 | org.gradle.configuration-cache=false 5 | #Kotlin 6 | kotlin.code.style=official 7 | #MPP 8 | kotlin.mpp.enableCInteropCommonization=true 9 | #Android 10 | android.useAndroidX=true 11 | android.nonTransitiveRClass=true 12 | 13 | org.jetbrains.dokka.experimental.gradle.pluginMode=V2EnabledWithHelpers 14 | 15 | #publishing for kmmbridge 16 | ## Darwin Publish require from - nextVersion parameter must be a valid semver string. Current value: 0.1.4. 17 | ## So we need set version to 0.1 or 0.2 ...... 18 | LIBRARY_VERSION=0.2.3 19 | GROUP=com.tddworks 20 | 21 | # POM 22 | POM_NAME=OpenAI Kotlin 23 | POM_DESCRIPTION=OpenAI API KMP Client 24 | POM_URL=https://github.com/tddworks/openai-kotlin 25 | POM_SCM_URL=https://github.com/tddworks/openai-kotlin 26 | POM_SCM_CONNECTION=scm:git:git://github.com/tddworks/openai-kotlin.git 27 | POM_SCM_DEV_CONNECTION=scm:git:ssh://github.com/tddworks/openai-kotlin.git 28 | POM_LICENCE_NAME=Apache License 2.0 29 | POM_LICENCE_URL=https://github.com/tddworks/openai-kotlin/blob/main/LICENSE 30 | POM_LICENCE_DIST=repo 31 | POM_DEVELOPER_ID=tddworks 32 | POM_DEVELOPER_ORGANIZATION=tddworks 33 | POM_DEVELOPER_ORGANIZATION_URL=https://tddworks.com 34 | POM_DEVELOPER_NAME=itshan 35 | POM_DEVELOPER_EMAIL=itshan@ttdworks.com 36 | POM_ISSUE_SYSTEM=github 37 | POM_ISSUE_URL=https://github.com/tddworks/openai-kotlin/issues 38 | 39 | -------------------------------------------------------------------------------- /openai-client/openai-client-core/src/jvmTest/kotlin/com/tddworks/openai/api/chat/api/ModelTest.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.openai.api.chat.api 2 | 3 | import org.junit.jupiter.api.Assertions.assertEquals 4 | import org.junit.jupiter.api.Test 5 | 6 | class ModelTest { 7 | 8 | @Test 9 | fun `should return model from value`() { 10 | assertEquals(OpenAIModel.GPT_3_5_TURBO, OpenAIModel("gpt-3.5-turbo")) 11 | assertEquals(OpenAIModel.GPT_3_5_TURBO_0125, OpenAIModel("gpt-3.5-turbo-0125")) 12 | assertEquals(OpenAIModel.GPT_4_TURBO, OpenAIModel("gpt-4-turbo")) 13 | assertEquals(OpenAIModel.GPT4_VISION_PREVIEW, OpenAIModel("gpt-4-vision-preview")) 14 | assertEquals(OpenAIModel.GPT_4O, OpenAIModel("gpt-4o")) 15 | assertEquals(OpenAIModel.DALL_E_2, OpenAIModel("dall-e-2")) 16 | assertEquals(OpenAIModel.DALL_E_3, OpenAIModel("dall-e-3")) 17 | } 18 | 19 | @Test 20 | fun `should return correct model values`() { 21 | assertEquals("gpt-3.5-turbo", OpenAIModel.GPT_3_5_TURBO.value) 22 | assertEquals("gpt-3.5-turbo-0125", OpenAIModel.GPT_3_5_TURBO_0125.value) 23 | assertEquals("gpt-4-turbo", OpenAIModel.GPT_4_TURBO.value) 24 | assertEquals("gpt-4-vision-preview", OpenAIModel.GPT4_VISION_PREVIEW.value) 25 | assertEquals("gpt-4o", OpenAIModel.GPT_4O.value) 26 | assertEquals("dall-e-2", OpenAIModel.DALL_E_2.value) 27 | assertEquals("dall-e-3", OpenAIModel.DALL_E_3.value) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /anthropic-client/anthropic-client-core/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.kotlinx.serialization) 3 | alias(libs.plugins.kover) 4 | `maven-publish` 5 | } 6 | 7 | kotlin { 8 | jvm() 9 | macosArm64() 10 | iosArm64() 11 | iosSimulatorArm64() 12 | 13 | sourceSets { 14 | commonMain.dependencies { 15 | // put your Multiplatform dependencies here 16 | api(projects.common) 17 | } 18 | 19 | commonTest.dependencies { 20 | implementation(libs.ktor.client.mock) 21 | api(projects.common) 22 | } 23 | 24 | macosMain.dependencies { api(libs.ktor.client.darwin) } 25 | 26 | jvmMain.dependencies { api(libs.ktor.client.cio) } 27 | 28 | jvmTest.dependencies { 29 | implementation(project.dependencies.platform(libs.junit.bom)) 30 | implementation(libs.bundles.jvm.test) 31 | implementation(libs.kotlinx.coroutines.test) 32 | implementation(libs.koin.test) 33 | implementation(libs.koin.test.junit5) 34 | implementation(libs.app.cash.turbine) 35 | implementation("com.tngtech.archunit:archunit-junit5:1.4.1") 36 | implementation("org.reflections:reflections:0.10.2") 37 | implementation(libs.org.skyscreamer.jsonassert) 38 | implementation("org.junit.platform:junit-platform-launcher") 39 | } 40 | } 41 | } 42 | 43 | tasks { named("jvmTest") { useJUnitPlatform() } } 44 | -------------------------------------------------------------------------------- /anthropic-client/anthropic-client-core/src/jvmTest/kotlin/com/tddworks/anthropic/api/MockHttpClient.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.anthropic.api 2 | 3 | import io.ktor.client.* 4 | import io.ktor.client.engine.mock.* 5 | import io.ktor.client.plugins.* 6 | import io.ktor.client.plugins.contentnegotiation.* 7 | import io.ktor.client.request.* 8 | import io.ktor.http.* 9 | import io.ktor.serialization.kotlinx.* 10 | 11 | /** See https://ktor.io/docs/http-client-testing.html#usage */ 12 | fun mockHttpClient(mockResponse: String) = 13 | HttpClient(MockEngine) { 14 | val headers = headersOf("Content-Type" to listOf(ContentType.Application.Json.toString())) 15 | 16 | install(ContentNegotiation) { 17 | register(ContentType.Application.Json, KotlinxSerializationConverter(JsonLenient)) 18 | } 19 | 20 | engine { 21 | addHandler { request -> 22 | if (request.url.encodedPath == "/v1/messages") { 23 | respond(mockResponse, HttpStatusCode.OK, headers) 24 | } else { 25 | error("Unhandled ${request.url.encodedPath}") 26 | } 27 | } 28 | } 29 | 30 | defaultRequest { 31 | url { 32 | protocol = URLProtocol.HTTPS 33 | host = "api.lemonsqueezy.com" 34 | } 35 | 36 | header(HttpHeaders.ContentType, ContentType.Application.Json) 37 | contentType(ContentType.Application.Json) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /anthropic-client/anthropic-client-core/src/commonMain/kotlin/com/tddworks/anthropic/api/messages/api/StreamMessageResponse.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.anthropic.api.messages.api 2 | 3 | import com.tddworks.anthropic.api.messages.api.internal.json.StreamMessageResponseSerializer 4 | import kotlinx.serialization.SerialName 5 | import kotlinx.serialization.Serializable 6 | 7 | @Serializable(with = StreamMessageResponseSerializer::class) 8 | sealed interface StreamMessageResponse { 9 | val type: String 10 | } 11 | 12 | @Serializable 13 | data class MessageStart(override val type: String, val message: CreateMessageResponse) : 14 | StreamMessageResponse 15 | 16 | @Serializable 17 | data class ContentBlockStart( 18 | override val type: String, 19 | val index: Int, 20 | @SerialName("content_block") val contentBlock: ContentBlock, 21 | ) : StreamMessageResponse 22 | 23 | @Serializable data class ContentBlock(val type: String, val text: String) 24 | 25 | @Serializable 26 | data class ContentBlockDelta(override val type: String, val index: Int, val delta: Delta) : 27 | StreamMessageResponse 28 | 29 | @Serializable 30 | data class ContentBlockStop(override val type: String, val index: Int) : StreamMessageResponse 31 | 32 | @Serializable 33 | data class MessageDelta(override val type: String, val delta: Delta) : StreamMessageResponse 34 | 35 | @Serializable data class MessageStop(override val type: String) : StreamMessageResponse 36 | 37 | @Serializable data class Ping(override val type: String) : StreamMessageResponse 38 | -------------------------------------------------------------------------------- /openai-gateway/openai-gateway-core/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.kotlinx.serialization) 3 | alias(libs.plugins.kover) 4 | `maven-publish` 5 | } 6 | 7 | kotlin { 8 | jvm() 9 | macosArm64() 10 | iosArm64() 11 | iosSimulatorArm64() 12 | 13 | sourceSets { 14 | commonMain.dependencies { 15 | api(projects.geminiClient.geminiClientCore) 16 | api(projects.openaiClient.openaiClientCore) 17 | api(projects.anthropicClient.anthropicClientCore) 18 | api(projects.ollamaClient.ollamaClientCore) 19 | } 20 | 21 | commonTest.dependencies { implementation(libs.ktor.client.mock) } 22 | 23 | macosMain.dependencies { api(libs.ktor.client.darwin) } 24 | 25 | jvmMain.dependencies { api(libs.ktor.client.cio) } 26 | 27 | jvmTest.dependencies { 28 | implementation(project.dependencies.platform(libs.junit.bom)) 29 | implementation(libs.bundles.jvm.test) 30 | implementation(libs.kotlinx.coroutines.test) 31 | implementation(libs.koin.test) 32 | implementation(libs.koin.test.junit5) 33 | implementation(libs.app.cash.turbine) 34 | implementation("com.tngtech.archunit:archunit-junit5:1.4.1") 35 | implementation("org.reflections:reflections:0.10.2") 36 | implementation("org.junit.platform:junit-platform-launcher") 37 | } 38 | } 39 | } 40 | 41 | tasks { named("jvmTest") { useJUnitPlatform() } } 42 | -------------------------------------------------------------------------------- /anthropic-client/anthropic-client-core/src/commonMain/kotlin/com/tddworks/anthropic/api/messages/api/internal/json/StreamMessageResponseSerializer.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.anthropic.api.messages.api.internal.json 2 | 3 | import com.tddworks.anthropic.api.messages.api.* 4 | import kotlinx.serialization.KSerializer 5 | import kotlinx.serialization.json.JsonContentPolymorphicSerializer 6 | import kotlinx.serialization.json.JsonElement 7 | import kotlinx.serialization.json.jsonObject 8 | import kotlinx.serialization.json.jsonPrimitive 9 | 10 | object StreamMessageResponseSerializer : 11 | JsonContentPolymorphicSerializer(StreamMessageResponse::class) { 12 | override fun selectDeserializer(element: JsonElement): KSerializer { 13 | val jsonElement = element.jsonObject["type"] 14 | 15 | val jsonPrimitive = jsonElement?.jsonPrimitive 16 | 17 | val type = jsonPrimitive?.content 18 | 19 | return when (type) { 20 | "message_start" -> MessageStart.serializer() 21 | "content_block_start" -> ContentBlockStart.serializer() 22 | "content_block_delta" -> ContentBlockDelta.serializer() 23 | "content_block_stop" -> ContentBlockStop.serializer() 24 | "message_delta" -> MessageDelta.serializer() 25 | "message_stop" -> MessageStop.serializer() 26 | "ping" -> Ping.serializer() 27 | else -> throw IllegalArgumentException("Unknown type of message") 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /anthropic-client/anthropic-client-core/src/commonMain/kotlin/com/tddworks/anthropic/api/messages/api/CreateMessageResponse.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.anthropic.api.messages.api 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | /** 7 | * { "content": [ { "text": "Hi! My name is Claude.", "type": "text" } ], "id": 8 | * "msg_013Zva2CMHLNnXjNJJKqJ2EF", "model": "claude-3-opus-20240229", "role": "assistant", 9 | * "stop_reason": "end_turn", "stop_sequence": null, "type": "message", "usage": { "input_tokens": 10 | * 10, "output_tokens": 25 } } 11 | */ 12 | @Serializable 13 | data class CreateMessageResponse( 14 | val content: List, 15 | val id: String, 16 | val model: String, 17 | val role: String, 18 | @SerialName("stop_reason") val stopReason: String?, 19 | @SerialName("stop_sequence") val stopSequence: String?, 20 | val type: String, 21 | val usage: Usage, 22 | ) { 23 | companion object { 24 | fun dummy() = 25 | CreateMessageResponse( 26 | content = listOf(ContentMessage(text = "Hi! My name is Claude.", type = "text")), 27 | id = "msg_1nZdL29xx5MUA1yADyHTEsnR8uuvGzszyY", 28 | model = "claude-3-opus-20240229", 29 | role = "assistant", 30 | stopReason = null, 31 | stopSequence = null, 32 | type = "message", 33 | usage = Usage(inputTokens = 25, outputTokens = 1), 34 | ) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /common/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.kotlinx.serialization) 3 | alias(libs.plugins.kover) 4 | `maven-publish` 5 | } 6 | 7 | kotlin { 8 | jvm() 9 | macosArm64() 10 | macosX64() 11 | iosArm64() 12 | iosSimulatorArm64() 13 | 14 | sourceSets { 15 | commonMain.dependencies { 16 | // put your Multiplatform dependencies here 17 | api(libs.kotlinx.coroutines.core) 18 | api(libs.kotlinx.serialization.json) 19 | api(libs.bundles.ktor.client) 20 | // di 21 | api(libs.koin.core) 22 | api(libs.koin.annotations) 23 | } 24 | 25 | commonTest.dependencies { implementation(libs.ktor.client.mock) } 26 | 27 | appleMain.dependencies { api(libs.ktor.client.darwin) } 28 | 29 | jvmMain.dependencies { api(libs.ktor.client.okhttp) } 30 | 31 | jvmTest.dependencies { 32 | implementation(project.dependencies.platform(libs.junit.bom)) 33 | implementation(libs.bundles.jvm.test) 34 | implementation(libs.kotlinx.coroutines.test) 35 | implementation(libs.koin.test) 36 | implementation(libs.koin.test.junit5) 37 | implementation(libs.app.cash.turbine) 38 | implementation("com.tngtech.archunit:archunit-junit5:1.4.1") 39 | implementation("org.reflections:reflections:0.10.2") 40 | implementation("org.junit.platform:junit-platform-launcher") 41 | } 42 | } 43 | } 44 | 45 | tasks { named("jvmTest") { useJUnitPlatform() } } 46 | -------------------------------------------------------------------------------- /common/src/commonMain/kotlin/com/tddworks/common/network/api/ktor/api/HttpRequester.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.common.network.api.ktor.api 2 | 3 | import io.ktor.client.request.* 4 | import io.ktor.client.statement.* 5 | import io.ktor.util.reflect.* 6 | import kotlinx.coroutines.flow.Flow 7 | import kotlinx.coroutines.flow.flow 8 | 9 | /** Interface for performing HTTP requests. */ 10 | interface HttpRequester { 11 | suspend fun performRequest(info: TypeInfo, builder: HttpRequestBuilder.() -> Unit): T 12 | 13 | /** 14 | * Perform an HTTP request and get a result. 15 | * 16 | * Note: [HttpResponse] instance shouldn't be passed outside of [block]. 17 | */ 18 | suspend fun streamRequest( 19 | builder: HttpRequestBuilder.() -> Unit, 20 | block: suspend (response: HttpResponse) -> T, 21 | ) 22 | 23 | companion object 24 | } 25 | 26 | /** 27 | * Perform an HTTP request and retrieve a result. 28 | * 29 | * @param builder The HttpRequestBuilder that contains the HTTP request details. 30 | * @return The result of the HTTP request. 31 | */ 32 | suspend inline fun HttpRequester.performRequest( 33 | noinline builder: HttpRequestBuilder.() -> Unit 34 | ): T { 35 | return performRequest(typeInfo(), builder) 36 | } 37 | 38 | /** Perform an HTTP request and get a result */ 39 | inline fun HttpRequester.streamRequest( 40 | noinline builder: HttpRequestBuilder.() -> Unit 41 | ): Flow { 42 | return flow { streamRequest(builder) { response -> streamEventsFrom(response) } } 43 | } 44 | -------------------------------------------------------------------------------- /openai-gateway/openai-gateway-core/src/jvmTest/kotlin/com/tddworks/openai/gateway/api/internal/AnthropicOpenAIProviderConfigTest.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.openai.gateway.api.internal 2 | 3 | import com.tddworks.openai.gateway.api.OpenAIProviderConfig 4 | import org.junit.jupiter.api.Assertions.* 5 | import org.junit.jupiter.api.Test 6 | 7 | class AnthropicOpenAIProviderConfigTest { 8 | 9 | @Test 10 | fun `should create anthropic openai config`() { 11 | // When 12 | val r = 13 | OpenAIProviderConfig.anthropic( 14 | apiKey = { "apiKey" }, 15 | baseUrl = { "baseUrl" }, 16 | anthropicVersion = { "2023-06-01" }, 17 | ) 18 | 19 | // Then 20 | assertEquals("apiKey", r.apiKey()) 21 | assertEquals("baseUrl", r.baseUrl()) 22 | assertEquals("2023-06-01", r.anthropicVersion()) 23 | } 24 | 25 | @Test 26 | fun `should convert to anthropic openai config`() { 27 | // Given 28 | val anthropicOpenAIProviderConfig = 29 | AnthropicOpenAIProviderConfig( 30 | anthropicVersion = { "2023-06-01" }, 31 | apiKey = { "apiKey" }, 32 | baseUrl = { "baseUrl" }, 33 | ) 34 | 35 | // When 36 | val anthropicConfig = anthropicOpenAIProviderConfig.toAnthropicOpenAIConfig() 37 | 38 | // Then 39 | assertEquals("apiKey", anthropicConfig.apiKey()) 40 | assertEquals("baseUrl", anthropicConfig.baseUrl()) 41 | assertEquals("2023-06-01", anthropicConfig.anthropicVersion()) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /ollama-client/ollama-client-core/src/jvmTest/kotlin/com/tddworks/ollama/api/OllamaTest.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.ollama.api 2 | 3 | import com.tddworks.di.getInstance 4 | import com.tddworks.ollama.di.initOllama 5 | import org.junit.jupiter.api.Assertions.assertEquals 6 | import org.junit.jupiter.api.BeforeEach 7 | import org.junit.jupiter.api.Test 8 | import org.koin.dsl.koinApplication 9 | import org.koin.test.check.checkModules 10 | import org.koin.test.junit5.AutoCloseKoinTest 11 | 12 | class OllamaTestTest : AutoCloseKoinTest() { 13 | 14 | @BeforeEach 15 | fun setUp() { 16 | koinApplication { 17 | initOllama( 18 | config = 19 | OllamaConfig( 20 | baseUrl = { "127.0.0.1" }, 21 | port = { 8080 }, 22 | protocol = { "https" }, 23 | ) 24 | ) 25 | } 26 | .checkModules() 27 | } 28 | 29 | @Test 30 | fun `should return overridden settings`() { 31 | val target = getInstance() 32 | 33 | assertEquals("127.0.0.1", target.baseUrl()) 34 | 35 | assertEquals(8080, target.port()) 36 | 37 | assertEquals("https", target.protocol()) 38 | } 39 | 40 | @Test 41 | fun `should return default settings`() { 42 | val target = Ollama.create(ollamaConfig = OllamaConfig()) 43 | 44 | assertEquals("localhost", target.baseUrl()) 45 | 46 | assertEquals(11434, target.port()) 47 | 48 | assertEquals("http", target.protocol()) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /gemini-client/gemini-client-core/src/commonMain/kotlin/com/tddworks/gemini/api/textGeneration/api/GenerateContentRequest.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.gemini.api.textGeneration.api 2 | 3 | import com.tddworks.gemini.api.textGeneration.api.internal.DefaultTextGenerationApi.Companion.GEMINI_API_PATH 4 | import kotlinx.serialization.SerialName 5 | import kotlinx.serialization.Serializable 6 | import kotlinx.serialization.Transient 7 | 8 | // curl 9 | // https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:streamGenerateContent?alt=sse&key=$GOOGLE_API_KEY \ 10 | // curl 11 | // https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key=$GOOGLE_API_KEY \ 12 | 13 | /** 14 | * { "contents": [ {"role":"user", "parts":[{ "text": "Hello"}]}, {"role": "model", "parts":[{ 15 | * "text": "Great to meet you. What would you like to know?"}]}, {"role":"user", "parts":[{ "text": 16 | * "I have two dogs in my house. How many paws are in my house?"}]}, ] } 17 | */ 18 | @Serializable 19 | data class GenerateContentRequest( 20 | @SerialName("system_instruction") val systemInstruction: Content? = null, 21 | val contents: List, 22 | val generationConfig: GenerationConfig? = null, 23 | @Transient val model: GeminiModel = GeminiModel.GEMINI_1_5_FLASH, 24 | @Transient val stream: Boolean = false, 25 | ) { 26 | fun toRequestUrl(): String { 27 | val endpoint = 28 | if (stream) { 29 | "streamGenerateContent" 30 | } else { 31 | "generateContent" 32 | } 33 | return "$GEMINI_API_PATH/${model.value}:$endpoint" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /ollama-client/ollama-client-core/src/jvmTest/kotlin/com/tddworks/ollama/api/MockHttpClient.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.ollama.api 2 | 3 | import com.tddworks.common.network.api.ktor.internal.JsonLenient 4 | import io.ktor.client.* 5 | import io.ktor.client.engine.mock.* 6 | import io.ktor.client.plugins.* 7 | import io.ktor.client.plugins.contentnegotiation.* 8 | import io.ktor.client.request.* 9 | import io.ktor.http.* 10 | import io.ktor.serialization.kotlinx.* 11 | 12 | /** See https://ktor.io/docs/http-client-testing.html#usage */ 13 | fun mockHttpClient(mockResponse: String) = 14 | HttpClient(MockEngine) { 15 | val headers = headersOf("Content-Type" to listOf(ContentType.Application.Json.toString())) 16 | 17 | install(ContentNegotiation) { 18 | register(ContentType.Application.Json, KotlinxSerializationConverter(JsonLenient)) 19 | } 20 | 21 | engine { 22 | addHandler { request -> 23 | if ( 24 | request.url.encodedPath == "/api/chat" || 25 | request.url.encodedPath == "/api/generate" 26 | ) { 27 | respond(mockResponse, HttpStatusCode.OK, headers) 28 | } else { 29 | error("Unhandled ${request.url.encodedPath}") 30 | } 31 | } 32 | } 33 | 34 | defaultRequest { 35 | url { 36 | protocol = URLProtocol.HTTPS 37 | host = "api.lemonsqueezy.com" 38 | } 39 | 40 | header(HttpHeaders.ContentType, ContentType.Application.Json) 41 | contentType(ContentType.Application.Json) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /openai-client/openai-client-core/src/commonMain/kotlin/com/tddworks/openai/api/chat/api/ChatCompletionChunk.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.openai.api.chat.api 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | /** 7 | * Data class representing a chat completion. 8 | * 9 | * @property id The ID of the chat completion. 10 | * @property `object` The type of object returned (always "text"). 11 | * @property created The timestamp of when the completion was created. 12 | * @property model The name of the GPT model used to generate the completion. 13 | * @property choices A list of possible chat completion options. 14 | */ 15 | @Serializable 16 | data class ChatCompletionChunk( 17 | val id: String, 18 | val `object`: String, 19 | val created: Long, 20 | val model: String, 21 | @SerialName("system_fingerprint") val systemFingerprint: String? = null, 22 | val choices: List, 23 | ) { 24 | companion object { 25 | fun dummy() = 26 | ChatCompletionChunk( 27 | id = "fake-id", 28 | `object` = "text", 29 | created = 0, 30 | model = "fake-model", 31 | choices = listOf(ChatChunk.fake()), 32 | ) 33 | 34 | fun error(exception: Throwable) = 35 | ChatCompletionChunk( 36 | id = "error-id", 37 | `object` = "error", 38 | created = 0, 39 | model = "error-model", 40 | choices = listOf(ChatChunk.error(exception.message)), 41 | ) 42 | } 43 | 44 | fun content() = choices.joinToString(separator = "") { it.delta.content ?: "" } 45 | } 46 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | google() 4 | mavenLocal() 5 | mavenCentral() 6 | gradlePluginPortal() 7 | } 8 | } 9 | 10 | dependencyResolutionManagement { 11 | repositories { 12 | google() 13 | mavenCentral() 14 | } 15 | } 16 | 17 | rootProject.name = "openai-kotlin" 18 | 19 | enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") 20 | 21 | plugins { id("de.fayard.refreshVersions") version "0.60.6" } 22 | 23 | fun String.isNonStable(): Boolean { 24 | val stableKeyword = listOf("RELEASE", "FINAL", "GA").any { uppercase().contains(it) } 25 | val regex = "^[0-9,.v-]+(-r)?$".toRegex() 26 | val isStable = stableKeyword || regex.matches(this) 27 | return isStable.not() 28 | } 29 | 30 | refreshVersions { rejectVersionIf { candidate.value.isNonStable() } } 31 | 32 | include(":common") 33 | 34 | // include(":library") 35 | include(":openai-client") 36 | 37 | include(":openai-client:openai-client-core") 38 | 39 | include(":openai-client:openai-client-darwin") 40 | 41 | include(":openai-client:openai-client-cio") 42 | 43 | include(":anthropic-client") 44 | 45 | include(":anthropic-client:anthropic-client-core") 46 | 47 | include(":anthropic-client:anthropic-client-darwin") 48 | 49 | include(":openai-gateway") 50 | 51 | include(":openai-gateway:openai-gateway-core") 52 | 53 | include(":openai-gateway:openai-gateway-darwin") 54 | 55 | include(":ollama-client") 56 | 57 | include(":ollama-client:ollama-client-core") 58 | 59 | include(":ollama-client:ollama-client-darwin") 60 | 61 | include(":gemini-client") 62 | 63 | include(":gemini-client:gemini-client-core") 64 | 65 | include(":gemini-client:gemini-client-darwin") 66 | -------------------------------------------------------------------------------- /anthropic-client/anthropic-client-darwin/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.kotlinx.serialization) 3 | alias(libs.plugins.touchlab.kmmbridge) 4 | alias(libs.plugins.touchlab.skie) 5 | `maven-publish` 6 | } 7 | 8 | kotlin { 9 | listOf(macosArm64(), iosArm64(), iosSimulatorArm64()).forEach { macosTarget -> 10 | macosTarget.binaries.framework { 11 | baseName = "AnthropicClient" 12 | export(projects.anthropicClient.anthropicClientCore) 13 | isStatic = true 14 | } 15 | } 16 | 17 | sourceSets { 18 | commonMain { 19 | dependencies { 20 | api(projects.anthropicClient.anthropicClientCore) 21 | implementation(libs.ktor.client.darwin) 22 | } 23 | } 24 | appleMain {} 25 | } 26 | } 27 | 28 | addGithubPackagesRepository() // <- Add the GitHub Packages repo 29 | 30 | kmmbridge { 31 | /** 32 | * reference: https://kmmbridge.touchlab.co/docs/artifacts/MAVEN_REPO_ARTIFACTS#github-packages 33 | * In kmmbridge, notice mavenPublishArtifacts() tells the plugin to push KMMBridge artifacts to 34 | * a Maven repo. You then need to define a repo. Rather than do everything manually, you can 35 | * just call addGithubPackagesRepository(), which will add the correct repo given parameters 36 | * that are passed in from GitHub Actions. 37 | */ 38 | mavenPublishArtifacts() // <- Publish using a Maven repo 39 | /** https://github.com/touchlab/KMMBridge/issues/258 */ 40 | spm(swiftToolVersion = "5.9", useCustomPackageFile = true, perModuleVariablesBlock = true) { 41 | iOS { v("18") } 42 | macOS { v("15") } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /gemini-client/gemini-client-darwin/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.kotlinx.serialization) 3 | alias(libs.plugins.touchlab.kmmbridge) 4 | alias(libs.plugins.touchlab.skie) 5 | `maven-publish` 6 | } 7 | 8 | kotlin { 9 | listOf(macosArm64(), iosArm64(), iosSimulatorArm64()).forEach { macosTarget -> 10 | macosTarget.binaries.framework { 11 | export(projects.geminiClient.geminiClientCore) 12 | baseName = "GeminiClient" 13 | isStatic = true 14 | } 15 | } 16 | 17 | sourceSets { 18 | commonMain { 19 | dependencies { 20 | api(projects.geminiClient.geminiClientCore) 21 | implementation(libs.ktor.client.darwin) 22 | } 23 | } 24 | appleMain {} 25 | } 26 | } 27 | 28 | addGithubPackagesRepository() // <- Add the GitHub Packages repo 29 | 30 | kmmbridge { 31 | /** 32 | * reference: https://kmmbridge.touchlab.co/docs/artifacts/MAVEN_REPO_ARTIFACTS#github-packages 33 | * In kmmbridge, notice mavenPublishArtifacts() tells the plugin to push KMMBridge artifacts to 34 | * a Maven repo. You then need to define a repo. Rather than do everything manually, you can 35 | * just call addGithubPackagesRepository(), which will add the correct repo given parameters 36 | * that are passed in from GitHub Actions. 37 | */ 38 | mavenPublishArtifacts() // <- Publish using a Maven repo 39 | /** https://github.com/touchlab/KMMBridge/issues/258 */ 40 | spm( 41 | swiftToolVersion = "5.9" 42 | // useCustomPackageFile = true, 43 | // perModuleVariablesBlock = true 44 | ) { 45 | iOS { v("15") } 46 | // macOS { v("15") } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /openai-client/openai-client-darwin/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.kotlinx.serialization) 3 | alias(libs.plugins.touchlab.kmmbridge) 4 | alias(libs.plugins.touchlab.skie) 5 | `maven-publish` 6 | } 7 | 8 | kotlin { 9 | listOf(macosArm64(), macosX64(), iosArm64(), iosSimulatorArm64()).forEach { macosTarget -> 10 | macosTarget.binaries.framework { 11 | baseName = "openai-client-darwin" 12 | export(projects.openaiClient.openaiClientCore) 13 | isStatic = true 14 | } 15 | } 16 | 17 | sourceSets { 18 | commonMain { 19 | dependencies { 20 | api(projects.openaiClient.openaiClientCore) 21 | implementation(libs.ktor.client.darwin) 22 | } 23 | } 24 | appleMain {} 25 | } 26 | } 27 | 28 | addGithubPackagesRepository() // <- Add the GitHub Packages repo 29 | 30 | kmmbridge { 31 | /** 32 | * reference: https://kmmbridge.touchlab.co/docs/artifacts/MAVEN_REPO_ARTIFACTS#github-packages 33 | * In kmmbridge, notice mavenPublishArtifacts() tells the plugin to push KMMBridge artifacts to 34 | * a Maven repo. You then need to define a repo. Rather than do everything manually, you can 35 | * just call addGithubPackagesRepository(), which will add the correct repo given parameters 36 | * that are passed in from GitHub Actions. 37 | */ 38 | mavenPublishArtifacts() // <- Publish using a Maven repo 39 | // spm(swiftToolVersion = "5.9") 40 | spm( 41 | swiftToolVersion = "5.9" 42 | // useCustomPackageFile = true, 43 | // perModuleVariablesBlock = true 44 | ) { 45 | iOS { v("15") } 46 | // macOS { v("15") } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /ollama-client/ollama-client-core/src/commonMain/kotlin/com/tddworks/ollama/api/chat/internal/DefaultOllamaChatApi.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.ollama.api.chat.internal 2 | 3 | import com.tddworks.common.network.api.ktor.api.HttpRequester 4 | import com.tddworks.common.network.api.ktor.api.performRequest 5 | import com.tddworks.common.network.api.ktor.api.streamRequest 6 | import com.tddworks.ollama.api.chat.OllamaChat 7 | import com.tddworks.ollama.api.chat.OllamaChatRequest 8 | import com.tddworks.ollama.api.chat.OllamaChatResponse 9 | import io.ktor.client.request.* 10 | import io.ktor.http.* 11 | import kotlinx.coroutines.flow.Flow 12 | import kotlinx.serialization.json.Json 13 | 14 | class DefaultOllamaChatApi(private val requester: HttpRequester) : OllamaChat { 15 | override fun stream(request: OllamaChatRequest): Flow { 16 | return requester.streamRequest { 17 | method = HttpMethod.Post 18 | url(path = CHAT_API_PATH) 19 | setBody(request.copy(stream = true)) 20 | contentType(ContentType.Application.Json) 21 | accept(ContentType.Text.EventStream) 22 | headers { 23 | append(HttpHeaders.CacheControl, "no-cache") 24 | append(HttpHeaders.Connection, "keep-alive") 25 | } 26 | } 27 | } 28 | 29 | override suspend fun request(request: OllamaChatRequest): OllamaChatResponse { 30 | return requester.performRequest { 31 | method = HttpMethod.Post 32 | url(path = CHAT_API_PATH) 33 | setBody(request) 34 | contentType(ContentType.Application.Json) 35 | } 36 | } 37 | 38 | companion object { 39 | const val CHAT_API_PATH = "/api/chat" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /ollama-client/ollama-client-darwin/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.kotlinx.serialization) 3 | alias(libs.plugins.touchlab.kmmbridge) 4 | alias(libs.plugins.touchlab.skie) 5 | `maven-publish` 6 | } 7 | 8 | kotlin { 9 | listOf(macosArm64(), iosArm64(), iosSimulatorArm64()).forEach { macosTarget -> 10 | macosTarget.binaries.framework { 11 | baseName = "ollama-client-darwin" 12 | export(projects.ollamaClient.ollamaClientCore) 13 | isStatic = true 14 | } 15 | } 16 | 17 | sourceSets { 18 | commonMain { 19 | dependencies { 20 | api(projects.ollamaClient.ollamaClientCore) 21 | implementation(libs.ktor.client.darwin) 22 | } 23 | } 24 | appleMain {} 25 | } 26 | } 27 | 28 | addGithubPackagesRepository() // <- Add the GitHub Packages repo 29 | 30 | kmmbridge { 31 | /** 32 | * reference: https://kmmbridge.touchlab.co/docs/artifacts/MAVEN_REPO_ARTIFACTS#github-packages 33 | * In kmmbridge, notice mavenPublishArtifacts() tells the plugin to push KMMBridge artifacts to 34 | * a Maven repo. You then need to define a repo. Rather than do everything manually, you can 35 | * just call addGithubPackagesRepository(), which will add the correct repo given parameters 36 | * that are passed in from GitHub Actions. 37 | */ 38 | mavenPublishArtifacts() // <- Publish using a Maven repo 39 | // spm(swiftToolVersion = "5.9") 40 | // spm { 41 | // swiftToolsVersion = "5.9" 42 | // platforms { 43 | // iOS("14") 44 | // macOS("13") 45 | // watchOS("7") 46 | // tvOS("14") 47 | // } 48 | // } 49 | } 50 | -------------------------------------------------------------------------------- /gemini-client/gemini-client-core/src/commonMain/kotlin/com/tddworks/gemini/api/textGeneration/api/internal/DefaultTextGenerationApi.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.gemini.api.textGeneration.api.internal 2 | 3 | import com.tddworks.common.network.api.ktor.api.HttpRequester 4 | import com.tddworks.common.network.api.ktor.api.performRequest 5 | import com.tddworks.common.network.api.ktor.api.streamRequest 6 | import com.tddworks.gemini.api.textGeneration.api.GenerateContentRequest 7 | import com.tddworks.gemini.api.textGeneration.api.GenerateContentResponse 8 | import com.tddworks.gemini.api.textGeneration.api.TextGeneration 9 | import io.ktor.client.request.* 10 | import io.ktor.http.* 11 | import kotlinx.coroutines.flow.Flow 12 | import org.koin.core.annotation.Single 13 | 14 | @Single 15 | class DefaultTextGenerationApi(private val requester: HttpRequester) : TextGeneration { 16 | override suspend fun generateContent(request: GenerateContentRequest): GenerateContentResponse { 17 | return requester.performRequest { configureRequest(request) } 18 | } 19 | 20 | override fun streamGenerateContent( 21 | request: GenerateContentRequest 22 | ): Flow { 23 | return requester.streamRequest { configureRequest(request) } 24 | } 25 | 26 | private fun HttpRequestBuilder.configureRequest(request: GenerateContentRequest) { 27 | method = HttpMethod.Post 28 | url(path = request.toRequestUrl()) 29 | if (request.stream) { 30 | parameter("alt", "sse") 31 | } 32 | setBody(request) 33 | contentType(ContentType.Application.Json) 34 | } 35 | 36 | companion object { 37 | const val GEMINI_API_PATH = "/v1beta/models" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /openai-client/openai-client-core/src/jvmTest/kotlin/com/tddworks/openai/api/common/MockHttpClient.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.openai.api.common 2 | 3 | import io.ktor.client.* 4 | import io.ktor.client.engine.mock.* 5 | import io.ktor.client.plugins.* 6 | import io.ktor.client.plugins.contentnegotiation.* 7 | import io.ktor.client.request.* 8 | import io.ktor.http.* 9 | import io.ktor.serialization.kotlinx.* 10 | import io.ktor.serialization.kotlinx.json.* 11 | import kotlinx.serialization.json.Json 12 | 13 | /** See https://ktor.io/docs/http-client-testing.html#usage */ 14 | fun mockHttpClient(mockResponse: String) = 15 | HttpClient(MockEngine) { 16 | val headers = headersOf("Content-Type" to listOf(ContentType.Application.Json.toString())) 17 | 18 | install(ContentNegotiation) { 19 | register(ContentType.Application.Json, KotlinxSerializationConverter(JsonLenient)) 20 | } 21 | 22 | engine { 23 | addHandler { request -> 24 | if ( 25 | request.url.encodedPath == "/v1/chat/completions" || 26 | request.url.encodedPath == "/v1/images/generations" || 27 | request.url.encodedPath == "/v1/completions" 28 | ) { 29 | respond(mockResponse, HttpStatusCode.OK, headers) 30 | } else { 31 | error("Unhandled ${request.url.encodedPath}") 32 | } 33 | } 34 | } 35 | 36 | defaultRequest { 37 | url { 38 | protocol = URLProtocol.HTTPS 39 | host = "api.lemonsqueezy.com" 40 | } 41 | 42 | header(HttpHeaders.ContentType, ContentType.Application.Json) 43 | contentType(ContentType.Application.Json) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /anthropic-client/anthropic-client-core/src/commonMain/kotlin/com/tddworks/anthropic/api/internal/AnthropicApi.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.anthropic.api.internal 2 | 3 | import com.tddworks.anthropic.api.Anthropic 4 | import com.tddworks.anthropic.api.AnthropicConfig 5 | import com.tddworks.anthropic.api.messages.api.Messages 6 | import com.tddworks.di.getInstance 7 | 8 | /** 9 | * The Anthropic API class encapsulates the necessary properties and methods to interact with the 10 | * Anthropic API. 11 | * 12 | * @property apiKey the unique identifier for your Anthropic API account 13 | * @property apiURL the base URL for making API requests to the Anthropic API 14 | * @property anthropicVersion the version of the Anthropics library being used 15 | */ 16 | class AnthropicApi( 17 | private val anthropicConfig: AnthropicConfig, 18 | private val messages: Messages = getInstance(), 19 | ) : Anthropic, Messages by messages { 20 | /** 21 | * Gets the API key. 22 | * 23 | * @return The API key string. 24 | */ 25 | override fun apiKey(): String { 26 | return anthropicConfig.apiKey() 27 | } 28 | 29 | /** 30 | * Returns the base URL for API requests. 31 | * 32 | * @return The base URL of the API 33 | */ 34 | override fun baseUrl(): String { 35 | return anthropicConfig.baseUrl() 36 | } 37 | 38 | /** 39 | * Returns the anthropic version string. 40 | * 41 | * @return The anthropic version string. 42 | */ 43 | override fun anthropicVersion(): String { 44 | return anthropicConfig.anthropicVersion() 45 | } 46 | } 47 | 48 | fun Anthropic.Companion.create( 49 | anthropicConfig: AnthropicConfig, 50 | messages: Messages = getInstance(), 51 | ): Anthropic { 52 | return AnthropicApi(anthropicConfig = anthropicConfig, messages = messages) 53 | } 54 | -------------------------------------------------------------------------------- /ollama-client/ollama-client-core/src/commonMain/kotlin/com/tddworks/ollama/api/generate/internal/DefaultOllamaGenerateApi.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.ollama.api.generate.internal 2 | 3 | import com.tddworks.common.network.api.ktor.api.HttpRequester 4 | import com.tddworks.common.network.api.ktor.api.performRequest 5 | import com.tddworks.common.network.api.ktor.api.streamRequest 6 | import com.tddworks.ollama.api.generate.OllamaGenerate 7 | import com.tddworks.ollama.api.generate.OllamaGenerateRequest 8 | import com.tddworks.ollama.api.generate.OllamaGenerateResponse 9 | import io.ktor.client.request.* 10 | import io.ktor.http.* 11 | import kotlinx.coroutines.flow.Flow 12 | 13 | /** Default implementation of Ollama generate */ 14 | class DefaultOllamaGenerateApi(private val requester: HttpRequester) : OllamaGenerate { 15 | override fun stream(request: OllamaGenerateRequest): Flow { 16 | return requester.streamRequest { 17 | method = HttpMethod.Post 18 | url(path = GENERATE_API_PATH) 19 | setBody(request.copy(stream = true)) 20 | contentType(ContentType.Application.Json) 21 | accept(ContentType.Text.EventStream) 22 | headers { 23 | append(HttpHeaders.CacheControl, "no-cache") 24 | append(HttpHeaders.Connection, "keep-alive") 25 | } 26 | } 27 | } 28 | 29 | override suspend fun request(request: OllamaGenerateRequest): OllamaGenerateResponse { 30 | return requester.performRequest { 31 | method = HttpMethod.Post 32 | url(path = GENERATE_API_PATH) 33 | setBody(request) 34 | contentType(ContentType.Application.Json) 35 | } 36 | } 37 | 38 | companion object { 39 | const val GENERATE_API_PATH = "/api/generate" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /common/src/commonMain/kotlin/com/tddworks/common/network/api/ktor/api/Stream.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.common.network.api.ktor.api 2 | 3 | import com.tddworks.di.getInstance 4 | import io.ktor.client.call.* 5 | import io.ktor.client.statement.* 6 | import io.ktor.utils.io.* 7 | import kotlinx.coroutines.flow.FlowCollector 8 | import kotlinx.serialization.json.Json 9 | 10 | const val STREAM_PREFIX = "data:" 11 | private const val STREAM_END_TOKEN = "$STREAM_PREFIX [DONE]" 12 | 13 | /** 14 | * Get data as 15 | * [Server-Sent Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#Event_stream_format). 16 | */ 17 | suspend inline fun FlowCollector.streamEventsFrom(response: HttpResponse) { 18 | val channel: ByteReadChannel = response.body() 19 | val json = json() 20 | while (!channel.isClosedForRead) { 21 | val line = channel.readUTF8Line() ?: continue 22 | val value: T = 23 | when { 24 | endStreamResponse(line) -> break 25 | isStreamResponse(line) -> 26 | json.decodeFromString( 27 | line.removePrefix(STREAM_PREFIX) 28 | ) // If the response indicates streaming data, decode and emit it. 29 | isJsonResponse(line) -> 30 | json.decodeFromString( 31 | line 32 | ) // Ollama - response is a json object without `data:` prefix 33 | else -> continue 34 | } 35 | emit(value) 36 | } 37 | } 38 | 39 | fun json(): Json { 40 | return getInstance() 41 | } 42 | 43 | fun isStreamResponse(line: String) = line.startsWith(STREAM_PREFIX) 44 | 45 | fun endStreamResponse(line: String) = line.startsWith(STREAM_END_TOKEN) 46 | 47 | fun isJsonResponse(line: String) = line.startsWith("{") && line.endsWith("}") 48 | -------------------------------------------------------------------------------- /gemini-client/gemini-client-core/src/jvmTest/kotlin/com/tddworks/gemini/api/textGeneration/api/MockHttpClient.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.gemini.api.textGeneration.api 2 | 3 | import com.tddworks.common.network.api.ktor.internal.JsonLenient 4 | import io.ktor.client.* 5 | import io.ktor.client.engine.mock.* 6 | import io.ktor.client.plugins.* 7 | import io.ktor.client.plugins.contentnegotiation.* 8 | import io.ktor.client.request.* 9 | import io.ktor.http.* 10 | import io.ktor.serialization.kotlinx.* 11 | 12 | /** See https://ktor.io/docs/http-client-testing.html#usage */ 13 | fun mockHttpClient(mockResponse: String) = 14 | HttpClient(MockEngine) { 15 | val headers = headersOf("Content-Type" to listOf(ContentType.Application.Json.toString())) 16 | 17 | install(ContentNegotiation) { 18 | register(ContentType.Application.Json, KotlinxSerializationConverter(JsonLenient)) 19 | } 20 | 21 | engine { 22 | addHandler { request -> 23 | when (request.url.encodedPath) { 24 | "/v1beta/models/gemini-1.5-flash:generateContent" -> { 25 | respond(mockResponse, HttpStatusCode.OK, headers) 26 | } 27 | 28 | "/v1beta/models/gemini-1.5-flash:streamGenerateContent" -> { 29 | respond(mockResponse, HttpStatusCode.OK, headers) 30 | } 31 | 32 | else -> { 33 | error("Unhandled ${request.url.encodedPath}") 34 | } 35 | } 36 | } 37 | } 38 | 39 | defaultRequest { 40 | url { 41 | protocol = URLProtocol.HTTPS 42 | host = Gemini.HOST 43 | } 44 | 45 | header(HttpHeaders.ContentType, ContentType.Application.Json) 46 | contentType(ContentType.Application.Json) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /gemini-client/gemini-client-core/src/commonMain/kotlin/com/tddworks/gemini/di/GeminiModule.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.gemini.di 2 | 3 | import com.tddworks.common.network.api.ktor.api.HttpRequester 4 | import com.tddworks.common.network.api.ktor.internal.ClientFeatures 5 | import com.tddworks.common.network.api.ktor.internal.UrlBasedConnectionConfig 6 | import com.tddworks.common.network.api.ktor.internal.createHttpClient 7 | import com.tddworks.common.network.api.ktor.internal.default 8 | import com.tddworks.di.createJson 9 | import com.tddworks.gemini.api.textGeneration.api.Gemini 10 | import com.tddworks.gemini.api.textGeneration.api.GeminiConfig 11 | import com.tddworks.gemini.api.textGeneration.api.TextGeneration 12 | import org.koin.core.annotation.ComponentScan 13 | import org.koin.core.annotation.Module 14 | import org.koin.core.annotation.Provided 15 | import org.koin.core.annotation.Single 16 | import org.koin.dsl.module 17 | import org.koin.ksp.generated.module 18 | 19 | @Module 20 | @ComponentScan("com.tddworks.gemini") 21 | class GeminiModule { 22 | 23 | @Single 24 | fun httpRequester(@Provided config: GeminiConfig): HttpRequester { 25 | return HttpRequester.default( 26 | createHttpClient( 27 | connectionConfig = UrlBasedConnectionConfig(config.baseUrl), 28 | features = 29 | ClientFeatures( 30 | json = createJson(), 31 | queryParams = { mapOf("key" to config.apiKey()) }, 32 | ), 33 | ) 34 | ) 35 | } 36 | 37 | @Single 38 | fun gemini(textGeneration: TextGeneration): Gemini { 39 | return object : Gemini, TextGeneration by textGeneration {} 40 | } 41 | 42 | companion object { 43 | fun geminiModules(config: GeminiConfig) = module { 44 | single { config } 45 | includes(GeminiModule().module) 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /ollama-client/ollama-client-core/src/commonMain/kotlin/com/tddworks/ollama/di/Koin.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.ollama.di 2 | 3 | import com.tddworks.common.network.api.ktor.api.HttpRequester 4 | import com.tddworks.common.network.api.ktor.internal.* 5 | import com.tddworks.di.commonModule 6 | import com.tddworks.ollama.api.Ollama 7 | import com.tddworks.ollama.api.OllamaConfig 8 | import com.tddworks.ollama.api.chat.OllamaChat 9 | import com.tddworks.ollama.api.chat.internal.DefaultOllamaChatApi 10 | import com.tddworks.ollama.api.generate.OllamaGenerate 11 | import com.tddworks.ollama.api.generate.internal.DefaultOllamaGenerateApi 12 | import com.tddworks.ollama.api.json.JsonLenient 13 | import kotlinx.serialization.json.Json 14 | import org.koin.core.context.startKoin 15 | import org.koin.core.qualifier.named 16 | import org.koin.dsl.KoinAppDeclaration 17 | import org.koin.dsl.module 18 | 19 | fun initOllama(config: OllamaConfig, appDeclaration: KoinAppDeclaration = {}): Ollama { 20 | return startKoin { 21 | appDeclaration() 22 | modules(commonModule(false) + ollamaModules(config)) 23 | } 24 | .koin 25 | .get() 26 | } 27 | 28 | fun ollamaModules(config: OllamaConfig) = module { 29 | single { Ollama.create(ollamaConfig = config) } 30 | 31 | single(named("ollamaJson")) { JsonLenient } 32 | 33 | single(named("ollamaHttpRequester")) { 34 | HttpRequester.default( 35 | createHttpClient( 36 | connectionConfig = UrlBasedConnectionConfig(baseUrl = config.baseUrl), 37 | features = ClientFeatures(json = get(named("ollamaJson"))), 38 | ) 39 | ) 40 | } 41 | 42 | single { DefaultOllamaChatApi(requester = get(named("ollamaHttpRequester"))) } 43 | 44 | single { 45 | DefaultOllamaGenerateApi(requester = get(named("ollamaHttpRequester"))) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /anthropic-client/anthropic-client-core/src/commonMain/kotlin/com/tddworks/anthropic/di/Koin.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.anthropic.di 2 | 3 | import com.tddworks.anthropic.api.Anthropic 4 | import com.tddworks.anthropic.api.AnthropicConfig 5 | import com.tddworks.anthropic.api.messages.api.Messages 6 | import com.tddworks.anthropic.api.messages.api.internal.DefaultMessagesApi 7 | import com.tddworks.anthropic.api.messages.api.internal.JsonLenient 8 | import com.tddworks.common.network.api.ktor.api.HttpRequester 9 | import com.tddworks.common.network.api.ktor.internal.* 10 | import com.tddworks.di.commonModule 11 | import kotlinx.serialization.json.Json 12 | import org.koin.core.context.startKoin 13 | import org.koin.core.qualifier.named 14 | import org.koin.dsl.KoinAppDeclaration 15 | import org.koin.dsl.module 16 | 17 | fun iniAnthropic(config: AnthropicConfig, appDeclaration: KoinAppDeclaration = {}): Anthropic { 18 | return startKoin { 19 | appDeclaration() 20 | modules(commonModule(false) + anthropicModules(config)) 21 | } 22 | .koin 23 | .get() 24 | } 25 | 26 | fun anthropicModules(config: AnthropicConfig) = module { 27 | single { Anthropic.create(anthropicConfig = config) } 28 | 29 | single(named("anthropicJson")) { JsonLenient } 30 | 31 | single(named("anthropicHttpRequester")) { 32 | HttpRequester.default( 33 | createHttpClient( 34 | connectionConfig = UrlBasedConnectionConfig(config.baseUrl), 35 | authConfig = AuthConfig(config.apiKey), 36 | // get from commonModule 37 | features = ClientFeatures(json = get(named("anthropicJson"))), 38 | ) 39 | ) 40 | } 41 | 42 | single { 43 | DefaultMessagesApi( 44 | anthropicConfig = config, 45 | requester = get(named("anthropicHttpRequester")), 46 | ) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /openai-client/openai-client-core/src/jvmTest/kotlin/com/tddworks/openai/api/chat/api/ChatCompletionRequestTest.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.openai.api.chat.api 2 | 3 | import com.tddworks.openai.api.common.prettyJson 4 | import kotlinx.serialization.ExperimentalSerializationApi 5 | import kotlinx.serialization.encodeToString 6 | import org.junit.jupiter.api.Assertions.* 7 | import org.junit.jupiter.api.Test 8 | 9 | @OptIn(ExperimentalSerializationApi::class) 10 | class ChatCompletionRequestTest { 11 | 12 | @Test 13 | fun `should return to correct stream json`() { 14 | val chatCompletionRequest = 15 | ChatCompletionRequest(messages = listOf(ChatMessage.user("hello")), stream = true) 16 | 17 | val result = prettyJson.encodeToString(chatCompletionRequest) 18 | 19 | assertEquals( 20 | """ 21 | { 22 | "messages": [ 23 | { 24 | "content": "hello", 25 | "role": "user" 26 | } 27 | ], 28 | "model": "gpt-3.5-turbo", 29 | "stream": true 30 | } 31 | """ 32 | .trimIndent(), 33 | result, 34 | ) 35 | } 36 | 37 | @Test 38 | fun `should return to correct json`() { 39 | val chatCompletionRequest = 40 | ChatCompletionRequest(messages = listOf(ChatMessage.user("hello"))) 41 | 42 | val result = 43 | prettyJson.encodeToString(ChatCompletionRequest.serializer(), chatCompletionRequest) 44 | 45 | assertEquals( 46 | """ 47 | { 48 | "messages": [ 49 | { 50 | "content": "hello", 51 | "role": "user" 52 | } 53 | ], 54 | "model": "gpt-3.5-turbo" 55 | } 56 | """ 57 | .trimIndent(), 58 | result, 59 | ) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /gemini-client/gemini-client-core/src/commonMain/kotlin/com/tddworks/gemini/api/textGeneration/api/GenerateContentResponse.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.gemini.api.textGeneration.api 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | internal typealias Base64String = String 7 | 8 | @Serializable 9 | data class GenerateContentResponse( 10 | val candidates: List, 11 | val usageMetadata: UsageMetadata, 12 | val modelVersion: String, 13 | ) { 14 | companion object { 15 | fun dummy() = 16 | GenerateContentResponse( 17 | candidates = emptyList(), 18 | usageMetadata = UsageMetadata(0, 0, 0), 19 | modelVersion = "", 20 | ) 21 | } 22 | } 23 | 24 | @Serializable 25 | data class Candidate( 26 | val content: Content, 27 | val finishReason: String? = null, 28 | val avgLogprobs: Double? = null, 29 | ) 30 | 31 | @Serializable data class Content(val parts: List, val role: String? = null) 32 | 33 | @Serializable(with = PartSerializer::class) 34 | sealed interface Part { 35 | 36 | @Serializable data class TextPart(val text: String) : Part 37 | 38 | /** 39 | * https://ai.google.dev/gemini-api/docs/text-generation?lang=rest#generate-text-from-text-and-image 40 | * 41 | * @param mimeType The MIME type of the data. Supported MIME types are the following: 42 | * - image/jpeg 43 | * - image/png 44 | * 45 | * @param data The base64-encoded data. 46 | */ 47 | @Serializable 48 | data class InlineDataPart(@SerialName("inline_data") val inlineData: InlineData) : Part { 49 | @Serializable 50 | data class InlineData(@SerialName("mime_type") val mimeType: String, val data: Base64String) 51 | } 52 | } 53 | 54 | @Serializable 55 | data class UsageMetadata( 56 | val promptTokenCount: Int, 57 | val candidatesTokenCount: Int? = null, 58 | val totalTokenCount: Int, 59 | ) 60 | -------------------------------------------------------------------------------- /common/src/commonMain/kotlin/com/tddworks/common/network/api/ktor/internal/JsonLenient.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.common.network.api.ktor.internal 2 | 3 | import kotlinx.serialization.EncodeDefault 4 | import kotlinx.serialization.json.Json 5 | 6 | /** 7 | * Represents a JSON object that allows for leniency and ignores unknown keys. 8 | * 9 | * @property isLenient Removes JSON specification restriction (RFC-4627) and makes parser more 10 | * liberal to the malformed input. In lenient mode quoted boolean literals, and unquoted string 11 | * literals are allowed. Its relaxations can be expanded in the future, so that lenient parser 12 | * becomes even more permissive to invalid value in the input, replacing them with defaults. false 13 | * by default. 14 | * @property ignoreUnknownKeys Specifies whether encounters of unknown properties in the input JSON 15 | * should be ignored instead of throwing SerializationException. false by default.. 16 | */ 17 | val JsonLenient = Json { 18 | isLenient = true 19 | ignoreUnknownKeys = true 20 | /** When this flag is disabled properties with null values without default are not encoded. */ 21 | explicitNulls = false 22 | /** 23 | * Controls whether the target property is serialized when its value is equal to a default 24 | * value, regardless of the format settings. Does not affect decoding and deserialization 25 | * process. 26 | * 27 | * Example of usage: 28 | * ``` 29 | * @Serializable 30 | * data class Foo( 31 | * @EncodeDefault(ALWAYS) val a: Int = 42, 32 | * @EncodeDefault(NEVER) val b: Int = 43, 33 | * val c: Int = 44 34 | * ) 35 | * 36 | * Json { encodeDefaults = false }.encodeToString((Foo()) // {"a": 42} 37 | * Json { encodeDefaults = true }.encodeToString((Foo()) // {"a": 42, "c":44} 38 | * ``` 39 | * 40 | * @see EncodeDefault.Mode.ALWAYS 41 | * @see EncodeDefault.Mode.NEVER 42 | */ 43 | encodeDefaults = true 44 | } 45 | -------------------------------------------------------------------------------- /anthropic-client/anthropic-client-core/src/jvmTest/kotlin/com/tddworks/anthropic/api/messages/api/BlockMessageContentTest.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.anthropic.api.messages.api 2 | 3 | import kotlinx.serialization.json.Json 4 | import org.junit.jupiter.api.Test 5 | import org.skyscreamer.jsonassert.JSONAssert 6 | 7 | class BlockMessageContentTest { 8 | 9 | @Test 10 | fun `should serialize image message content`() { 11 | // Given 12 | val messageContent = 13 | BlockMessageContent.ImageContent( 14 | source = 15 | BlockMessageContent.ImageContent.Source( 16 | mediaType = "image1_media_type", 17 | data = "image1_data", 18 | type = "base64", 19 | ) 20 | ) 21 | 22 | // When 23 | val result = Json.encodeToString(BlockMessageContent.serializer(), messageContent) 24 | 25 | // Then 26 | JSONAssert.assertEquals( 27 | """ 28 | { 29 | "type": "image", 30 | "source": { 31 | "type": "base64", 32 | "media_type": "image1_media_type", 33 | "data": "image1_data" 34 | } 35 | } 36 | """ 37 | .trimIndent(), 38 | result, 39 | false, 40 | ) 41 | } 42 | 43 | @Test 44 | fun `should serialize message content`() { 45 | // Given 46 | val messageContent = BlockMessageContent.TextContent(text = "some-text") 47 | 48 | // When 49 | val result = Json.encodeToString(BlockMessageContent.serializer(), messageContent) 50 | 51 | // Then 52 | JSONAssert.assertEquals( 53 | """ 54 | { 55 | "text": "some-text", 56 | "type": "text" 57 | } 58 | """ 59 | .trimIndent(), 60 | result, 61 | false, 62 | ) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /ollama-client/ollama-client-core/src/jvmTest/kotlin/com/tddworks/ollama/api/chat/internal/DefaultOllamaChatITest.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.ollama.api.chat.internal 2 | 3 | import com.tddworks.di.getInstance 4 | import com.tddworks.ollama.api.Ollama 5 | import com.tddworks.ollama.api.OllamaConfig 6 | import com.tddworks.ollama.api.chat.OllamaChatMessage 7 | import com.tddworks.ollama.api.chat.OllamaChatRequest 8 | import com.tddworks.ollama.di.initOllama 9 | import kotlinx.coroutines.test.runTest 10 | import org.junit.jupiter.api.Assertions.assertEquals 11 | import org.junit.jupiter.api.Assertions.assertNotNull 12 | import org.junit.jupiter.api.BeforeEach 13 | import org.junit.jupiter.api.Disabled 14 | import org.junit.jupiter.api.Test 15 | import org.koin.test.junit5.AutoCloseKoinTest 16 | 17 | @Disabled 18 | class DefaultOllamaChatITest : AutoCloseKoinTest() { 19 | 20 | @BeforeEach 21 | fun setUp() { 22 | initOllama(config = OllamaConfig(baseUrl = { "http://localhost:11434" })) 23 | } 24 | 25 | @Test 26 | fun `should return correct base url`() { 27 | assertEquals("localhost", Ollama.BASE_URL) 28 | } 29 | 30 | @Test 31 | fun `should return stream response`() = runTest { 32 | val ollama = getInstance() 33 | 34 | ollama 35 | .stream( 36 | OllamaChatRequest( 37 | model = "llama2", 38 | messages = listOf(OllamaChatMessage(role = "user", content = "hello")), 39 | ) 40 | ) 41 | .collect { println("stream response: $it") } 42 | } 43 | 44 | @Test 45 | fun `should return create response`() = runTest { 46 | val ollama = getInstance() 47 | 48 | val r = 49 | ollama.request( 50 | OllamaChatRequest( 51 | model = "llama2", 52 | messages = listOf(OllamaChatMessage(role = "user", content = "hello")), 53 | ) 54 | ) 55 | 56 | assertNotNull(r.message?.content) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /openai-client/openai-client-core/src/commonMain/kotlin/com/tddworks/openai/api/OpenAI.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.openai.api 2 | 3 | import com.tddworks.common.network.api.ktor.api.HttpRequester 4 | import com.tddworks.common.network.api.ktor.internal.* 5 | import com.tddworks.di.createJson 6 | import com.tddworks.di.getInstance 7 | import com.tddworks.openai.api.chat.api.Chat 8 | import com.tddworks.openai.api.chat.internal.default 9 | import com.tddworks.openai.api.images.api.Images 10 | import com.tddworks.openai.api.images.internal.default 11 | import com.tddworks.openai.api.legacy.completions.api.Completions 12 | import com.tddworks.openai.api.legacy.completions.api.internal.default 13 | 14 | interface OpenAI : Chat, Images, Completions { 15 | companion object { 16 | const val BASE_URL = "https://api.openai.com" 17 | 18 | fun default(config: OpenAIConfig): OpenAI { 19 | val requester = 20 | HttpRequester.default( 21 | createHttpClient( 22 | connectionConfig = UrlBasedConnectionConfig(config.baseUrl), 23 | authConfig = AuthConfig(config.apiKey), 24 | features = ClientFeatures(json = createJson()), 25 | ) 26 | ) 27 | return default(requester) 28 | } 29 | 30 | fun default( 31 | requester: HttpRequester, 32 | chatCompletionPath: String = Chat.CHAT_COMPLETIONS_PATH, 33 | ): OpenAI { 34 | val chatApi = 35 | Chat.default(requester = requester, chatCompletionPath = chatCompletionPath) 36 | 37 | val imagesApi = Images.default(requester = requester) 38 | 39 | val completionsApi = Completions.default(requester = requester) 40 | 41 | return object : 42 | OpenAI, Chat by chatApi, Images by imagesApi, Completions by completionsApi {} 43 | } 44 | } 45 | } 46 | 47 | class OpenAIApi : 48 | OpenAI, Chat by getInstance(), Images by getInstance(), Completions by getInstance() 49 | -------------------------------------------------------------------------------- /openai-client/openai-client-core/src/commonMain/kotlin/com/tddworks/openai/di/Koin.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.openai.di 2 | 3 | import com.tddworks.common.network.api.ktor.api.HttpRequester 4 | import com.tddworks.common.network.api.ktor.internal.* 5 | import com.tddworks.di.commonModule 6 | import com.tddworks.openai.api.OpenAI 7 | import com.tddworks.openai.api.OpenAIApi 8 | import com.tddworks.openai.api.OpenAIConfig 9 | import com.tddworks.openai.api.chat.api.Chat 10 | import com.tddworks.openai.api.chat.internal.DefaultChatApi 11 | import com.tddworks.openai.api.images.api.Images 12 | import com.tddworks.openai.api.images.internal.DefaultImagesApi 13 | import com.tddworks.openai.api.legacy.completions.api.Completions 14 | import com.tddworks.openai.api.legacy.completions.api.internal.DefaultCompletionsApi 15 | import org.koin.core.context.startKoin 16 | import org.koin.core.qualifier.named 17 | import org.koin.dsl.KoinAppDeclaration 18 | import org.koin.dsl.module 19 | 20 | fun initOpenAI(config: OpenAIConfig, appDeclaration: KoinAppDeclaration = {}): OpenAI { 21 | return startKoin { 22 | appDeclaration() 23 | modules(commonModule(false) + openAIModules(config)) 24 | } 25 | .koin 26 | .get() 27 | } 28 | 29 | fun openAIModules(config: OpenAIConfig) = module { 30 | single { OpenAIApi() } 31 | 32 | single(named("openAIHttpRequester")) { 33 | HttpRequester.default( 34 | createHttpClient( 35 | connectionConfig = UrlBasedConnectionConfig(config.baseUrl), 36 | authConfig = AuthConfig(config.apiKey), 37 | // get from commonModule 38 | features = ClientFeatures(json = get()), 39 | ) 40 | ) 41 | } 42 | 43 | single { DefaultChatApi(requester = get(named("openAIHttpRequester"))) } 44 | 45 | single { DefaultImagesApi(requester = get(named("openAIHttpRequester"))) } 46 | 47 | single { DefaultCompletionsApi(requester = get(named("openAIHttpRequester"))) } 48 | } 49 | -------------------------------------------------------------------------------- /ollama-client/ollama-client-core/src/commonMain/kotlin/com/tddworks/ollama/api/chat/OllamaChatRequest.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.ollama.api.chat 2 | 3 | import com.tddworks.common.network.api.ktor.api.AnySerial 4 | import kotlinx.serialization.SerialName 5 | import kotlinx.serialization.Serializable 6 | 7 | /** https://github.com/ollama/ollama/blob/main/docs/api.md Generate a chat completion */ 8 | @Serializable 9 | data class OllamaChatRequest( 10 | /** (required) the model name */ 11 | @SerialName("model") val model: String, 12 | /** (required) a list of messages to send to the model */ 13 | @SerialName("messages") val messages: List, 14 | /** 15 | * Advanced parameters (optional): the format to return a response in. Currently the only 16 | * accepted value is json 17 | */ 18 | @SerialName("format") val format: String? = null, 19 | /** 20 | * additional model parameters listed in the documentation for the Modelfile such as temperature 21 | */ 22 | @SerialName("options") val options: Map? = null, 23 | /** 24 | * keep_alive: controls how long the model will stay loaded into memory following the request 25 | * (default: 5m) 26 | */ 27 | @SerialName("keep_alive") val keepAlive: String? = null, 28 | /** 29 | * stream: if false the response will be returned as a single response object, rather than a 30 | * stream of objects 31 | */ 32 | @SerialName("stream") val stream: Boolean? = null, 33 | ) { 34 | 35 | companion object { 36 | fun dummy() = 37 | OllamaChatRequest( 38 | model = "llama2", 39 | messages = listOf(OllamaChatMessage(role = "user", content = "Hello!")), 40 | ) 41 | } 42 | } 43 | 44 | @Serializable 45 | data class OllamaChatMessage( 46 | /** `role`: the role of the message, either `system`, `user` or `assistant` */ 47 | @SerialName("role") val role: String, 48 | @SerialName("content") val content: String, 49 | @SerialName("images") val images: List? = null, 50 | ) 51 | -------------------------------------------------------------------------------- /common/src/commonMain/kotlin/com/tddworks/common/network/api/ktor/internal/HttpClient.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.common.network.api.ktor.internal 2 | 3 | import io.ktor.client.* 4 | import io.ktor.client.engine.* 5 | import io.ktor.client.plugins.* 6 | import io.ktor.client.plugins.contentnegotiation.* 7 | import io.ktor.client.plugins.logging.* 8 | import io.ktor.client.request.* 9 | import io.ktor.http.* 10 | import io.ktor.serialization.kotlinx.* 11 | import io.ktor.util.* 12 | 13 | internal expect fun httpClientEngine(): HttpClientEngine 14 | 15 | fun createHttpClient( 16 | connectionConfig: ConnectionConfig = UrlBasedConnectionConfig(), 17 | authConfig: AuthConfig = AuthConfig(), 18 | features: ClientFeatures = ClientFeatures(), 19 | httpClientEngine: HttpClientEngine = httpClientEngine(), 20 | ): HttpClient { 21 | 22 | return HttpClient(httpClientEngine) { 23 | install(ContentNegotiation) { 24 | register(ContentType.Application.Json, KotlinxSerializationConverter(features.json)) 25 | } 26 | 27 | install(Logging) { 28 | logger = Logger.DEFAULT 29 | level = LogLevel.INFO 30 | } 31 | 32 | install(HttpRequestRetry) { 33 | maxRetries = 3 34 | retryIf { _, response -> response.status.value == 429 } 35 | exponentialDelay(base = 5.0, maxDelayMs = 60_000) 36 | } 37 | 38 | defaultRequest { 39 | connectionConfig.setupUrl(this) 40 | commonSettings(features.queryParams, authConfig.authToken) 41 | } 42 | 43 | expectSuccess = features.expectSuccess 44 | } 45 | } 46 | 47 | private fun DefaultRequest.DefaultRequestBuilder.commonSettings( 48 | queryParams: (() -> Map), 49 | authToken: (() -> String)?, 50 | ) { 51 | queryParams().forEach { (key, value) -> url.parameters.appendIfNameAbsent(key, value) } 52 | 53 | authToken?.let { header(HttpHeaders.Authorization, "Bearer ${it()}") } 54 | 55 | header(HttpHeaders.ContentType, ContentType.Application.Json) 56 | contentType(ContentType.Application.Json) 57 | } 58 | -------------------------------------------------------------------------------- /gemini-client/gemini-client-core/src/commonMain/kotlin/com/tddworks/gemini/api/textGeneration/api/GeminiModel.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.gemini.api.textGeneration.api 2 | 3 | import kotlin.jvm.JvmInline 4 | import kotlinx.serialization.Serializable 5 | 6 | /** https://ai.google.dev/gemini-api/docs/models/gemini */ 7 | @Serializable 8 | @JvmInline 9 | value class GeminiModel(val value: String) { 10 | companion object { 11 | 12 | /** 13 | * Input(s): Audio, images, videos, and text Output(s): Text, images (coming soon), and 14 | * audio (coming soon) Optimized for: Next generation features, speed, and multimodal 15 | * generation for a diverse variety of tasks 16 | */ 17 | val GEMINI_2_0_FLASH = GeminiModel("gemini-2.0-flash") 18 | 19 | /** 20 | * Audio, images, videos, and text Output(s): Text A Gemini 2.0 Flash model optimized for 21 | * cost efficiency and low latency 22 | */ 23 | val GEMINI_2_0_FLASH_LITE = GeminiModel("gemini-2.0-flash-lite-preview-02-05") 24 | 25 | /** 26 | * Input(s): Audio, images, videos, and text Output(s): Text Optimized for: Complex 27 | * reasoning tasks requiring more intelligence 28 | */ 29 | val GEMINI_1_5_PRO = GeminiModel("gemini-1.5-pro") 30 | 31 | /** 32 | * Input(s): Audio, images, videos, and text Output(s): Text Optimized for: High volume and 33 | * lower intelligence tasks 34 | */ 35 | val GEMINI_1_5_FLASH_8b = GeminiModel("gemini-1.5-flash-8b") 36 | 37 | /** 38 | * Input(s): Audio, images, videos, and text Output(s): Text Optimized for: Fast and 39 | * versatile performance across a diverse variety of tasks 40 | */ 41 | val GEMINI_1_5_FLASH = GeminiModel("gemini-1.5-flash") 42 | 43 | val availableModels = 44 | listOf( 45 | GEMINI_1_5_PRO, 46 | GEMINI_1_5_FLASH_8b, 47 | GEMINI_1_5_FLASH, 48 | GEMINI_2_0_FLASH, 49 | GEMINI_2_0_FLASH_LITE, 50 | ) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /ollama-client/ollama-client-core/src/jvmTest/kotlin/com/tddworks/ollama/api/chat/OllamaChatRequestTest.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.ollama.api.chat 2 | 3 | import com.tddworks.ollama.api.json.JsonLenient 4 | import kotlin.test.assertEquals 5 | import org.junit.jupiter.api.Test 6 | 7 | class OllamaChatRequestTest { 8 | 9 | @Test 10 | fun `should convert json to object`() { 11 | // given 12 | val json = 13 | """ 14 | { 15 | "model": "llama3", 16 | "messages": [{ 17 | "role": "user", 18 | "content": "Why is the sky blue?" 19 | }], 20 | "format": "json", 21 | "keep_alive": "5m", 22 | "stream": false, 23 | "options": { 24 | "num_predict": 100, 25 | "temperature": 0.8, 26 | "stop": ["\n", "user:"] 27 | } 28 | } 29 | """ 30 | .trimIndent() 31 | 32 | // when 33 | val request = JsonLenient.decodeFromString(OllamaChatRequest.serializer(), json) 34 | 35 | // then 36 | assertEquals("llama3", request.model) 37 | assertEquals(1, request.messages.size) 38 | assertEquals("user", request.messages[0].role) 39 | assertEquals("Why is the sky blue?", request.messages[0].content) 40 | assertEquals("json", request.format) 41 | assertEquals("5m", request.keepAlive) 42 | 43 | assertEquals(100, request.options?.get("num_predict")) 44 | assertEquals(0.8, request.options?.get("temperature")) 45 | assertEquals(listOf("\n", "user:"), request.options?.get("stop")) 46 | } 47 | 48 | @Test 49 | fun `should return dummy request`() { 50 | // given 51 | val request = OllamaChatRequest.dummy() 52 | 53 | // then 54 | assertEquals("llama2", request.model) 55 | assertEquals(1, request.messages.size) 56 | assertEquals("user", request.messages[0].role) 57 | assertEquals("Hello!", request.messages[0].content) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /common/src/jvmTest/kotlin/com/tddworks/common/network/api/MockHttpClient.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.common.network.api 2 | 3 | import io.ktor.client.* 4 | import io.ktor.client.engine.mock.* 5 | import io.ktor.client.plugins.* 6 | import io.ktor.client.plugins.contentnegotiation.* 7 | import io.ktor.client.request.* 8 | import io.ktor.http.* 9 | import io.ktor.serialization.kotlinx.* 10 | import io.ktor.utils.io.* 11 | 12 | /** See https://ktor.io/docs/http-client-testing.html#usage */ 13 | fun mockHttpClient( 14 | mockResponse: String, 15 | mockHttpStatusCode: HttpStatusCode = HttpStatusCode.OK, 16 | exception: Exception? = null, 17 | ) = 18 | HttpClient(MockEngine) { 19 | val headers = headersOf("Content-Type" to listOf(ContentType.Application.Json.toString())) 20 | 21 | install(ContentNegotiation) { 22 | register(ContentType.Application.Json, KotlinxSerializationConverter(JsonLenient)) 23 | } 24 | 25 | engine { 26 | addHandler { request -> 27 | exception?.let { throw it } 28 | 29 | if ( 30 | mockHttpStatusCode == HttpStatusCode.OK || 31 | mockHttpStatusCode == HttpStatusCode.Forbidden 32 | ) { 33 | if ( 34 | request.url.encodedPath == "/v1/chat/completions" || 35 | request.url.encodedPath == "/v1/images/generations" 36 | ) { 37 | respond(mockResponse, mockHttpStatusCode, headers) 38 | } else { 39 | error("Unhandled ${request.url.encodedPath}") 40 | } 41 | } else { 42 | respondError(mockHttpStatusCode) 43 | } 44 | } 45 | } 46 | 47 | defaultRequest { 48 | url { 49 | protocol = URLProtocol.HTTPS 50 | host = "some-host" 51 | } 52 | 53 | header(HttpHeaders.ContentType, ContentType.Application.Json) 54 | contentType(ContentType.Application.Json) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /openai-gateway/openai-gateway-darwin/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.kotlinx.serialization) 3 | alias(libs.plugins.touchlab.kmmbridge) 4 | alias(libs.plugins.touchlab.skie) 5 | `maven-publish` 6 | } 7 | 8 | kotlin { 9 | listOf(macosArm64(), iosArm64(), iosSimulatorArm64()).forEach { macosTarget -> 10 | macosTarget.binaries.framework { 11 | baseName = "openai-gateway-darwin" 12 | export(projects.openaiGateway.openaiGatewayCore) 13 | export(projects.openaiClient.openaiClientDarwin) 14 | export(projects.ollamaClient.ollamaClientDarwin) 15 | export(projects.anthropicClient.anthropicClientDarwin) 16 | isStatic = true 17 | } 18 | } 19 | 20 | sourceSets { 21 | commonMain { 22 | dependencies { 23 | api(projects.openaiGateway.openaiGatewayCore) 24 | api(projects.openaiClient.openaiClientDarwin) 25 | api(projects.ollamaClient.ollamaClientDarwin) 26 | api(projects.anthropicClient.anthropicClientDarwin) 27 | implementation(libs.ktor.client.darwin) 28 | } 29 | } 30 | appleMain {} 31 | } 32 | } 33 | 34 | addGithubPackagesRepository() // <- Add the GitHub Packages repo 35 | 36 | kmmbridge { 37 | /** 38 | * reference: https://kmmbridge.touchlab.co/docs/artifacts/MAVEN_REPO_ARTIFACTS#github-packages 39 | * In kmmbridge, notice mavenPublishArtifacts() tells the plugin to push KMMBridge artifacts to 40 | * a Maven repo. You then need to define a repo. Rather than do everything manually, you can 41 | * just call addGithubPackagesRepository(), which will add the correct repo given parameters 42 | * that are passed in from GitHub Actions. 43 | */ 44 | mavenPublishArtifacts() // <- Publish using a Maven repo 45 | 46 | // spm { 47 | // swiftToolsVersion = "5.9" 48 | // platforms { 49 | // iOS("14") 50 | // macOS("13") 51 | // watchOS("7") 52 | // tvOS("14") 53 | // } 54 | // } 55 | } 56 | -------------------------------------------------------------------------------- /gemini-client/gemini-client-core/src/jvmTest/kotlin/com/tddworks/gemini/api/textGeneration/api/GenerateContentResponseTest.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.gemini.api.textGeneration.api 2 | 3 | import kotlin.test.assertEquals 4 | import kotlinx.serialization.json.Json 5 | import org.junit.jupiter.api.Test 6 | 7 | class GenerateContentResponseTest { 8 | 9 | @Test 10 | fun `should deserialize GenerateContentResponse`() { 11 | // Given 12 | val json = 13 | """ 14 | { 15 | "candidates": [ 16 | { 17 | "content": { 18 | "parts": [ 19 | { 20 | "text": "some-text" 21 | } 22 | ], 23 | "role": "model" 24 | }, 25 | "finishReason": "STOP", 26 | "avgLogprobs": -0.24741496906413898 27 | } 28 | ], 29 | "usageMetadata": { 30 | "promptTokenCount": 4, 31 | "candidatesTokenCount": 715, 32 | "totalTokenCount": 719 33 | }, 34 | "modelVersion": "gemini-1.5-flash" 35 | } 36 | """ 37 | .trimIndent() 38 | 39 | // When 40 | val response = Json.decodeFromString(json) 41 | 42 | // Then 43 | assertEquals(1, response.candidates.size) 44 | assertEquals("gemini-1.5-flash", response.modelVersion) 45 | assertEquals(4, response.usageMetadata.promptTokenCount) 46 | assertEquals(715, response.usageMetadata.candidatesTokenCount) 47 | assertEquals(719, response.usageMetadata.totalTokenCount) 48 | assertEquals("STOP", response.candidates[0].finishReason) 49 | assertEquals(-0.24741496906413898, response.candidates[0].avgLogprobs) 50 | assertEquals("model", response.candidates[0].content.role) 51 | assertEquals(1, response.candidates[0].content.parts.size) 52 | assertEquals("some-text", (response.candidates[0].content.parts[0] as Part.TextPart).text) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /common/src/commonMain/kotlin/com/tddworks/common/network/api/ktor/internal/exception/OpenLLMAPIException.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.common.network.api.ktor.internal.exception 2 | 3 | /** 4 | * Represents an exception thrown when an error occurs while interacting with the OpenAI API. 5 | * 6 | * @property statusCode the HTTP status code associated with the error. 7 | * @property error an instance of [OpenAIError] containing information about the error that 8 | * occurred. 9 | */ 10 | sealed class OpenAIAPIException( 11 | private val statusCode: Int, 12 | private val error: OpenAIError, 13 | throwable: Throwable? = null, 14 | ) : OpenAIException(message = error.detail?.message, throwable = throwable) 15 | 16 | /** Represents an exception thrown when the OpenAI API rate limit is exceeded. */ 17 | class RateLimitException(statusCode: Int, error: OpenAIError, throwable: Throwable? = null) : 18 | OpenAIAPIException(statusCode, error, throwable) 19 | 20 | /** Represents an exception thrown when an invalid request is made to the OpenAI API. */ 21 | class InvalidRequestException(statusCode: Int, error: OpenAIError, throwable: Throwable? = null) : 22 | OpenAIAPIException(statusCode, error, throwable) 23 | 24 | /** 25 | * Represents an exception thrown when an authentication error occurs while interacting with the 26 | * OpenAI API. 27 | */ 28 | class AuthenticationException(statusCode: Int, error: OpenAIError, throwable: Throwable? = null) : 29 | OpenAIAPIException(statusCode, error, throwable) 30 | 31 | /** 32 | * Represents an exception thrown when a permission error occurs while interacting with the OpenAI 33 | * API. 34 | */ 35 | class PermissionException(statusCode: Int, error: OpenAIError, throwable: Throwable? = null) : 36 | OpenAIAPIException(statusCode, error, throwable) 37 | 38 | /** 39 | * Represents an exception thrown when an unknown error occurs while interacting with the OpenAI 40 | * API. This exception is used when the specific type of error is not covered by the existing 41 | * subclasses. 42 | */ 43 | class UnknownAPIException(statusCode: Int, error: OpenAIError, throwable: Throwable? = null) : 44 | OpenAIAPIException(statusCode, error, throwable) 45 | -------------------------------------------------------------------------------- /anthropic-client/anthropic-client-core/src/jvmTest/kotlin/com/tddworks/anthropic/api/JsonUtils.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.anthropic.api 2 | 3 | import kotlinx.serialization.EncodeDefault 4 | import kotlinx.serialization.json.Json 5 | 6 | val prettyJson = Json { // this returns the JsonBuilder 7 | prettyPrint = true 8 | ignoreUnknownKeys = true 9 | // optional: specify indent 10 | prettyPrintIndent = " " 11 | 12 | /** 13 | * Controls whether the target property is serialized when its value is equal to a default 14 | * value, regardless of the format settings. Does not affect decoding and deserialization 15 | * process. 16 | * 17 | * Example of usage: 18 | * ``` 19 | * @Serializable 20 | * data class Foo( 21 | * @EncodeDefault(ALWAYS) val a: Int = 42, 22 | * @EncodeDefault(NEVER) val b: Int = 43, 23 | * val c: Int = 44 24 | * ) 25 | * 26 | * Json { encodeDefaults = false }.encodeToString((Foo()) // {"a": 42} 27 | * Json { encodeDefaults = true }.encodeToString((Foo()) // {"a": 42, "c":44} 28 | * ``` 29 | * 30 | * @see EncodeDefault.Mode.ALWAYS 31 | * @see EncodeDefault.Mode.NEVER 32 | */ 33 | encodeDefaults = true 34 | } 35 | 36 | /** 37 | * Represents a JSON object that allows for leniency and ignores unknown keys. 38 | * 39 | * @property isLenient Removes JSON specification restriction (RFC-4627) and makes parser more 40 | * liberal to the malformed input. In lenient mode quoted boolean literals, and unquoted string 41 | * literals are allowed. Its relaxations can be expanded in the future, so that lenient parser 42 | * becomes even more permissive to invalid value in the input, replacing them with defaults. false 43 | * by default. 44 | * @property ignoreUnknownKeys Specifies whether encounters of unknown properties in the input JSON 45 | * should be ignored instead of throwing SerializationException. false by default.. 46 | */ 47 | internal val JsonLenient = Json { 48 | isLenient = true 49 | ignoreUnknownKeys = true 50 | // https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/json.md#class-discriminator-for-polymorphism 51 | encodeDefaults = true 52 | } 53 | -------------------------------------------------------------------------------- /ollama-client/ollama-client-core/src/commonMain/kotlin/com/tddworks/ollama/api/chat/OllamaChatResponse.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.ollama.api.chat 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | /** 7 | * { "model": "llama2", "created_at": "2023-08-04T08:52:19.385406455-07:00", "message": { "role": 8 | * "assistant", "content": "The" }, "done": false } ======== final response ======== { "model": 9 | * "llama2", "created_at": "2023-08-04T19:22:45.499127Z", "done": true, "total_duration": 10 | * 8113331500, "load_duration": 6396458, "prompt_eval_count": 61, "prompt_eval_duration": 398801000, 11 | * "eval_count": 468, "eval_duration": 7701267000 } 12 | * 13 | * ======= Non-streaming response ======= { "model": "llama2", "created_at": 14 | * "2023-12-12T14:13:43.416799Z", "message": { "role": "assistant", "content": "Hello! How are you 15 | * today?" }, "done": true, "total_duration": 5191566416, "load_duration": 2154458, 16 | * "prompt_eval_count": 26, "prompt_eval_duration": 383809000, "eval_count": 298, "eval_duration": 17 | * 4799921000 } 18 | */ 19 | @Serializable 20 | data class OllamaChatResponse( 21 | @SerialName("model") val model: String, 22 | @SerialName("created_at") val createdAt: String, 23 | @SerialName("message") val message: OllamaChatMessage? = null, 24 | @SerialName("done") val done: Boolean, 25 | // Below are the fields that are for final response or non-streaming response 26 | @SerialName("total_duration") val totalDuration: Long? = null, 27 | @SerialName("load_duration") val loadDuration: Long? = null, 28 | @SerialName("prompt_eval_count") val promptEvalCount: Int? = null, 29 | @SerialName("prompt_eval_duration") val promptEvalDuration: Long? = null, 30 | @SerialName("eval_count") val evalCount: Int? = null, 31 | @SerialName("eval_duration") val evalDuration: Long? = null, 32 | ) { 33 | companion object { 34 | fun dummy() = 35 | OllamaChatResponse( 36 | model = "llama2", 37 | createdAt = "2023-08-04T08:52:19.385406455-07:00", 38 | message = OllamaChatMessage(role = "assistant", content = "The"), 39 | done = false, 40 | ) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /openai-client/openai-client-core/src/jvmTest/kotlin/com/tddworks/openai/api/images/internal/DefaultImagesApiTest.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.openai.api.images.internal 2 | 3 | import com.tddworks.common.network.api.ktor.internal.DefaultHttpRequester 4 | import com.tddworks.openai.api.chat.api.OpenAIModel 5 | import com.tddworks.openai.api.common.mockHttpClient 6 | import com.tddworks.openai.api.images.api.ImageCreate 7 | import com.tddworks.openai.api.images.api.Images 8 | import kotlinx.coroutines.runBlocking 9 | import org.junit.jupiter.api.Assertions.assertEquals 10 | import org.junit.jupiter.api.Test 11 | 12 | class DefaultImagesApiTest { 13 | 14 | @Test 15 | fun `should return correct image generations path url`() { 16 | assertEquals("/v1/images/generations", Images.IMAGES_GENERATIONS_PATH) 17 | } 18 | 19 | @Test 20 | fun `should return generated images`() = runBlocking { 21 | val request = 22 | ImageCreate(prompt = "Hello! How can I assist you today?", model = OpenAIModel.DALL_E_3) 23 | 24 | val chat = 25 | DefaultImagesApi( 26 | requester = 27 | DefaultHttpRequester( 28 | httpClient = 29 | mockHttpClient( 30 | """ 31 | { 32 | "created": 1589478378, 33 | "data": [ 34 | { 35 | "url": "https://..." 36 | }, 37 | { 38 | "url": "https://..." 39 | } 40 | ] 41 | } 42 | """ 43 | .trimIndent() 44 | ) 45 | ) 46 | ) 47 | 48 | val r = chat.generate(request) 49 | 50 | with(r) { 51 | assertEquals(1589478378, created) 52 | assertEquals(2, data.size) 53 | assertEquals("https://...", data[0].url) 54 | assertEquals("https://...", data[1].url) 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /openai-client/openai-client-core/src/commonMain/kotlin/com/tddworks/openai/api/legacy/completions/api/Completion.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.openai.api.legacy.completions.api 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | /** https://platform.openai.com/docs/api-reference/completions/object */ 7 | @Serializable 8 | data class Completion( 9 | /** A unique identifier for the completion. */ 10 | val id: String, 11 | /** The list of completion choices the model generated for the input prompt. */ 12 | val choices: List, 13 | /** The Unix timestamp (in seconds) of when the completion was created. */ 14 | val created: Int, 15 | /** The model used for completion. */ 16 | val model: String, 17 | /** 18 | * This fingerprint represents the backend configuration that the model runs with. 19 | * 20 | * Can be used in conjunction with the seed request parameter to understand when backend changes 21 | * have been made that might impact determinism. 22 | */ 23 | @SerialName("system_fingerprint") val systemFingerprint: String? = null, 24 | /** The object type, which is always "text_completion" */ 25 | val `object`: String? = null, 26 | /** Usage statistics for the completion request. */ 27 | val usage: Usage? = null, 28 | ) { 29 | companion object { 30 | fun dummy() = 31 | Completion( 32 | id = "id", 33 | choices = emptyList(), 34 | created = 0, 35 | model = "model", 36 | systemFingerprint = "systemFingerprint", 37 | `object` = "object", 38 | usage = Usage(), 39 | ) 40 | } 41 | } 42 | 43 | @Serializable 44 | data class CompletionChoice( 45 | @SerialName("text") val text: String, 46 | @SerialName("index") val index: Int, 47 | @SerialName("logprobs") val logprobs: Map? = null, 48 | @SerialName("finish_reason") val finishReason: String = "", 49 | ) 50 | 51 | @Serializable 52 | data class Usage( 53 | @SerialName("prompt_tokens") val promptTokens: Int? = null, 54 | @SerialName("completion_tokens") val completionTokens: Int? = null, 55 | @SerialName("total_tokens") val totalTokens: Int? = null, 56 | ) 57 | -------------------------------------------------------------------------------- /common/src/jvmTest/kotlin/com/tddworks/common/network/api/ktor/api/StreamTest.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.common.network.api.ktor.api 2 | 3 | import app.cash.turbine.test 4 | import com.tddworks.common.network.api.ktor.StreamResponse 5 | import com.tddworks.common.network.api.ktor.internal.JsonLenient 6 | import io.ktor.client.* 7 | import io.ktor.client.engine.mock.* 8 | import io.ktor.client.request.* 9 | import io.ktor.http.* 10 | import io.ktor.utils.io.* 11 | import kotlin.test.assertEquals 12 | import kotlinx.coroutines.flow.flow 13 | import kotlinx.coroutines.runBlocking 14 | import kotlinx.serialization.json.Json 15 | import org.junit.jupiter.api.Test 16 | import org.junit.jupiter.api.extension.RegisterExtension 17 | import org.koin.dsl.module 18 | import org.koin.test.junit5.AutoCloseKoinTest 19 | import org.koin.test.junit5.KoinTestExtension 20 | 21 | class StreamTest : AutoCloseKoinTest() { 22 | 23 | @JvmField 24 | @RegisterExtension 25 | val koinTestExtension = 26 | KoinTestExtension.create { modules(module { single { JsonLenient } }) } 27 | 28 | @Test 29 | fun `test streamEventsFrom with stream response`(): Unit = runBlocking { 30 | val channel = ByteChannel(autoFlush = true) 31 | val mockEngine = MockEngine { request -> 32 | when (request.url.toString()) { 33 | "http://example.com/stream" -> 34 | respond(content = channel, status = HttpStatusCode.OK) 35 | 36 | else -> respond("", HttpStatusCode.NotFound) 37 | } 38 | } 39 | 40 | val client = HttpClient(mockEngine) 41 | 42 | val content = 43 | flow { 44 | client.preparePost("http://example.com/stream").execute { streamEventsFrom(it) } 45 | } 46 | 47 | channel.writeStringUtf8("Hello world!\n") 48 | channel.writeStringUtf8("data: {\"content\": \"some-content-1\"}\n") 49 | channel.writeStringUtf8("data: {\"content\": \"some-content-2\"}\n") 50 | channel.writeStringUtf8("data: [DONE]\n") 51 | content.test { 52 | assertEquals(StreamResponse("some-content-1"), awaitItem()) 53 | assertEquals(StreamResponse("some-content-2"), awaitItem()) 54 | awaitComplete() 55 | channel.close() 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /anthropic-client/anthropic-client-core/src/commonMain/kotlin/com/tddworks/anthropic/api/AnthropicModel.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.anthropic.api 2 | 3 | import kotlin.jvm.JvmInline 4 | import kotlinx.serialization.Serializable 5 | 6 | /** 7 | * https://docs.anthropic.com/claude/docs/models-overview 8 | * https://docs.anthropic.com/en/docs/about-claude/models Claude is a family of state-of-the-art 9 | * large language models developed by Anthropic. Our models are designed to provide you with the 10 | * best possible experience when interacting with AI, offering a range of capabilities and 11 | * performance levels to suit your needs and make it easy to deploy high performing, safe, and 12 | * steerable models. In this guide, we'll introduce you to our latest and greatest models, the 13 | * Claude 3 family, as well as our legacy models, which are still available for those who need them. 14 | */ 15 | @Serializable 16 | @JvmInline 17 | value class AnthropicModel(val value: String) { 18 | companion object { 19 | /** 20 | * Most powerful model for highly complex tasks Max output length: 4096 tokens Cost (Input / 21 | * Output per MTok^) $15.00 / $75.00 22 | */ 23 | val CLAUDE_3_OPUS = AnthropicModel("claude-3-opus-20240229") 24 | 25 | /** 26 | * Ideal balance of intelligence and speed for enterprise workloads Max output length: 4096 27 | * tokens Cost (Input / Output per MTok^) $3.00 / $15.00 28 | */ 29 | val CLAUDE_3_Sonnet = AnthropicModel("claude-3-sonnet-20240229") 30 | 31 | /** 32 | * Most intelligent model The model costs $3 per million input tokens and $15 per million 33 | * output tokens, with a 200K token context window. Cost (Input / Output per MTok^) $3.00 / 34 | * $15.00 Training data cut-off: Apr 2024 35 | */ 36 | val CLAUDE_3_5_Sonnet = AnthropicModel("claude-3-5-sonnet-20240620") 37 | 38 | /** 39 | * Fastest and most compact model for near-instant responsiveness Max output length: 4096 40 | * tokens Cost (Input / Output per MTok^) $0.25 / $1.25 41 | */ 42 | val CLAUDE_3_HAIKU = AnthropicModel("claude-3-haiku-20240307") 43 | 44 | val availableModels = 45 | listOf(CLAUDE_3_OPUS, CLAUDE_3_Sonnet, CLAUDE_3_HAIKU, CLAUDE_3_5_Sonnet) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /ollama-client/ollama-client-core/src/jvmTest/kotlin/com/tddworks/ollama/api/JsonUtils.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.ollama.api 2 | 3 | import kotlinx.serialization.EncodeDefault 4 | import kotlinx.serialization.json.Json 5 | 6 | val prettyJson = Json { // this returns the JsonBuilder 7 | prettyPrint = true 8 | ignoreUnknownKeys = true 9 | // optional: specify indent 10 | prettyPrintIndent = " " 11 | 12 | /** When this flag is disabled properties with null values without default are not encoded. */ 13 | explicitNulls = false 14 | 15 | /** 16 | * Controls whether the target property is serialized when its value is equal to a default 17 | * value, regardless of the format settings. Does not affect decoding and deserialization 18 | * process. 19 | * 20 | * Example of usage: 21 | * ``` 22 | * @Serializable 23 | * data class Foo( 24 | * @EncodeDefault(ALWAYS) val a: Int = 42, 25 | * @EncodeDefault(NEVER) val b: Int = 43, 26 | * val c: Int = 44 27 | * ) 28 | * 29 | * Json { encodeDefaults = false }.encodeToString((Foo()) // {"a": 42} 30 | * Json { encodeDefaults = true }.encodeToString((Foo()) // {"a": 42, "c":44} 31 | * ``` 32 | * 33 | * @see EncodeDefault.Mode.ALWAYS 34 | * @see EncodeDefault.Mode.NEVER 35 | */ 36 | encodeDefaults = true 37 | } 38 | 39 | /** 40 | * Represents a JSON object that allows for leniency and ignores unknown keys. 41 | * 42 | * @property isLenient Removes JSON specification restriction (RFC-4627) and makes parser more 43 | * liberal to the malformed input. In lenient mode quoted boolean literals, and unquoted string 44 | * literals are allowed. Its relaxations can be expanded in the future, so that lenient parser 45 | * becomes even more permissive to invalid value in the input, replacing them with defaults. false 46 | * by default. 47 | * @property ignoreUnknownKeys Specifies whether encounters of unknown properties in the input JSON 48 | * should be ignored instead of throwing SerializationException. false by default.. 49 | */ 50 | internal val JsonLenient = Json { 51 | isLenient = true 52 | ignoreUnknownKeys = true 53 | // https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/json.md#class-discriminator-for-polymorphism 54 | encodeDefaults = true 55 | } 56 | -------------------------------------------------------------------------------- /common/src/jvmTest/kotlin/com/tddworks/common/network/api/InternalPackageTest.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.common.network.api 2 | 3 | import com.tngtech.archunit.core.importer.ClassFileImporter 4 | import com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses 5 | import java.io.IOException 6 | import org.assertj.core.api.Assertions.assertThat 7 | import org.junit.jupiter.api.Test 8 | import org.reflections.Reflections 9 | import org.reflections.scanners.Scanners 10 | 11 | class InternalPackageTest { 12 | 13 | companion object { 14 | private val BASE_PACKAGE = InternalPackageTest::class.java.`package`.name 15 | } 16 | 17 | private val analyzedClasses = ClassFileImporter().importPackages(BASE_PACKAGE) 18 | 19 | @Test 20 | @Throws(IOException::class) 21 | fun internalPackagesAreNotAccessedFromOutside() { 22 | 23 | // so that the test will break when the base package is re-named 24 | assertPackageExists(BASE_PACKAGE) 25 | val internalPackages = internalPackages(BASE_PACKAGE) 26 | for (internalPackage in internalPackages) { 27 | assertPackageExists(internalPackage) 28 | assertPackageIsNotAccessedFromOutside(internalPackage) 29 | } 30 | } 31 | 32 | /** Finds all packages named "internal". */ 33 | private fun internalPackages(basePackage: String): List { 34 | val scanner = Scanners.SubTypes 35 | val reflections = Reflections(basePackage, scanner) 36 | return reflections 37 | .getSubTypesOf(Object::class.java) 38 | .map { it.`package`.name } 39 | .filter { it.endsWith(".internal") } 40 | } 41 | 42 | private fun assertPackageIsNotAccessedFromOutside(internalPackage: String) { 43 | noClasses() 44 | .that() 45 | .resideOutsideOfPackage(packageMatcher(internalPackage)) 46 | .should() 47 | .dependOnClassesThat() 48 | .resideInAPackage(packageMatcher(internalPackage)) 49 | .check(analyzedClasses) 50 | } 51 | 52 | private fun assertPackageExists(packageName: String?) { 53 | assertThat(analyzedClasses.containPackage(packageName)) 54 | .`as`("package %s exists", packageName) 55 | .isTrue() 56 | } 57 | 58 | private fun packageMatcher(fullyQualifiedPackage: String): String? { 59 | return "$fullyQualifiedPackage.." 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /openai-gateway/openai-gateway-core/src/commonMain/kotlin/com/tddworks/openai/gateway/api/internal/GeminiOpenAIProvider.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.openai.gateway.api.internal 2 | 3 | import com.tddworks.common.network.api.ktor.api.ListResponse 4 | import com.tddworks.gemini.api.textGeneration.api.* 5 | import com.tddworks.openai.api.chat.api.ChatCompletion 6 | import com.tddworks.openai.api.chat.api.ChatCompletionChunk 7 | import com.tddworks.openai.api.chat.api.ChatCompletionRequest 8 | import com.tddworks.openai.api.images.api.Image 9 | import com.tddworks.openai.api.images.api.ImageCreate 10 | import com.tddworks.openai.api.legacy.completions.api.Completion 11 | import com.tddworks.openai.api.legacy.completions.api.CompletionRequest 12 | import com.tddworks.openai.gateway.api.OpenAIProvider 13 | import kotlinx.coroutines.flow.Flow 14 | import kotlinx.coroutines.flow.transform 15 | import kotlinx.serialization.ExperimentalSerializationApi 16 | 17 | @OptIn(ExperimentalSerializationApi::class) 18 | class GeminiOpenAIProvider( 19 | override val id: String = "gemini", 20 | override val name: String = "Gemini", 21 | override val config: GeminiOpenAIProviderConfig, 22 | val client: Gemini, 23 | ) : OpenAIProvider { 24 | 25 | override suspend fun chatCompletions(request: ChatCompletionRequest): ChatCompletion { 26 | val geminiRequest = request.toGeminiGenerateContentRequest() 27 | return client.generateContent(geminiRequest).toOpenAIChatCompletion() 28 | } 29 | 30 | override fun streamChatCompletions(request: ChatCompletionRequest): Flow { 31 | val geminiRequest = request.toGeminiGenerateContentRequest().copy(stream = true) 32 | 33 | return client.streamGenerateContent(geminiRequest).transform { 34 | emit(it.toOpenAIChatCompletionChunk()) 35 | } 36 | } 37 | 38 | override suspend fun completions(request: CompletionRequest): Completion { 39 | throw UnsupportedOperationException("Not supported") 40 | } 41 | 42 | override suspend fun generate(request: ImageCreate): ListResponse { 43 | throw UnsupportedOperationException("Not supported") 44 | } 45 | } 46 | 47 | fun OpenAIProvider.Companion.gemini( 48 | id: String = "gemini", 49 | config: GeminiOpenAIProviderConfig, 50 | client: Gemini, 51 | ): OpenAIProvider { 52 | return GeminiOpenAIProvider(id = id, config = config, client = client) 53 | } 54 | -------------------------------------------------------------------------------- /openai-client/openai-client-core/src/jvmTest/kotlin/com/tddworks/openai/api/InternalPackageTest.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.openai.api 2 | 3 | import com.tngtech.archunit.core.importer.ClassFileImporter 4 | import com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses 5 | import java.io.IOException 6 | import org.assertj.core.api.Assertions.assertThat 7 | import org.junit.jupiter.api.Test 8 | import org.reflections.Reflections 9 | import org.reflections.scanners.Scanners 10 | 11 | class InternalPackageTest { 12 | 13 | companion object { 14 | private val BASE_PACKAGE = InternalPackageTest::class.java.`package`.name 15 | } 16 | 17 | private val analyzedClasses = ClassFileImporter().importPackages(BASE_PACKAGE) 18 | 19 | @Test 20 | @Throws(IOException::class) 21 | fun internalPackagesAreNotAccessedFromOutside() { 22 | 23 | // so that the test will break when the base package is re-named 24 | assertPackageExists(BASE_PACKAGE) 25 | val internalPackages = internalPackages(BASE_PACKAGE) 26 | for (internalPackage in internalPackages) { 27 | assertPackageExists(internalPackage) 28 | assertPackageIsNotAccessedFromOutside(internalPackage) 29 | } 30 | } 31 | 32 | /** Finds all packages named "internal". */ 33 | private fun internalPackages(basePackage: String): List { 34 | val scanner = Scanners.SubTypes 35 | val reflections = Reflections(basePackage, scanner) 36 | return reflections 37 | .getSubTypesOf(Object::class.java) 38 | .map { it.`package`.name } 39 | .filter { it.endsWith(".internal") } 40 | } 41 | 42 | private fun assertPackageIsNotAccessedFromOutside(internalPackage: String) { 43 | noClasses() 44 | .that() 45 | .resideOutsideOfPackage(packageMatcher(internalPackage)) 46 | .should() 47 | .dependOnClassesThat() 48 | .resideInAPackage(packageMatcher(internalPackage)) 49 | .check(analyzedClasses) 50 | } 51 | 52 | private fun assertPackageExists(packageName: String?) { 53 | assertThat(analyzedClasses.containPackage(packageName)) 54 | .`as`("package %s exists", packageName) 55 | .isTrue() 56 | } 57 | 58 | private fun packageMatcher(fullyQualifiedPackage: String): String? { 59 | return "$fullyQualifiedPackage.." 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /openai-client/openai-client-core/src/jvmTest/kotlin/com/tddworks/openai/api/OpenAIITest.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.openai.api 2 | 3 | import app.cash.turbine.test 4 | import com.tddworks.openai.api.chat.api.ChatCompletionRequest 5 | import com.tddworks.openai.api.chat.api.ChatMessage 6 | import com.tddworks.openai.api.chat.api.OpenAIModel 7 | import com.tddworks.openai.di.initOpenAI 8 | import kotlin.time.Duration.Companion.seconds 9 | import kotlinx.coroutines.test.runTest 10 | import kotlinx.serialization.ExperimentalSerializationApi 11 | import org.junit.jupiter.api.Assertions.assertNotNull 12 | import org.junit.jupiter.api.BeforeEach 13 | import org.junit.jupiter.api.Test 14 | import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable 15 | import org.koin.test.junit5.AutoCloseKoinTest 16 | 17 | @ExperimentalSerializationApi 18 | @EnabledIfEnvironmentVariable(named = "OPENAI_API_KEY", matches = ".+") 19 | class OpenAIITest : AutoCloseKoinTest() { 20 | private lateinit var openAI: OpenAI 21 | 22 | @BeforeEach 23 | fun setUp() { 24 | openAI = 25 | initOpenAI( 26 | OpenAIConfig( 27 | baseUrl = { OpenAI.BASE_URL }, 28 | apiKey = { System.getenv("OPENAI_API_KEY") ?: "CONFIGURE_ME" }, 29 | ) 30 | ) 31 | } 32 | 33 | @Test 34 | fun `should use openai client to get stream chat completions`() = runTest { 35 | openAI 36 | .streamChatCompletions( 37 | ChatCompletionRequest( 38 | messages = listOf(ChatMessage.UserMessage("hello")), 39 | maxTokens = 1024, 40 | model = OpenAIModel.GPT_3_5_TURBO, 41 | ) 42 | ) 43 | .test(timeout = 10.seconds) { 44 | assertNotNull(awaitItem()) 45 | assertNotNull(awaitItem()) 46 | cancelAndIgnoreRemainingEvents() 47 | } 48 | } 49 | 50 | @Test 51 | fun `should use openai client to get chat completions`() = runTest { 52 | val response = 53 | openAI.chatCompletions( 54 | ChatCompletionRequest( 55 | messages = listOf(ChatMessage.UserMessage("hello")), 56 | maxTokens = 1024, 57 | model = OpenAIModel.GPT_3_5_TURBO, 58 | ) 59 | ) 60 | assertNotNull(response) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /ollama-client/ollama-client-core/src/commonMain/kotlin/com/tddworks/ollama/api/Ollama.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.ollama.api 2 | 3 | import com.tddworks.common.network.api.ktor.api.HttpRequester 4 | import com.tddworks.common.network.api.ktor.internal.* 5 | import com.tddworks.ollama.api.chat.OllamaChat 6 | import com.tddworks.ollama.api.chat.internal.DefaultOllamaChatApi 7 | import com.tddworks.ollama.api.generate.OllamaGenerate 8 | import com.tddworks.ollama.api.generate.internal.DefaultOllamaGenerateApi 9 | import com.tddworks.ollama.api.internal.OllamaApi 10 | import com.tddworks.ollama.api.json.JsonLenient 11 | 12 | /** Interface for interacting with the Ollama API. */ 13 | interface Ollama : OllamaChat, OllamaGenerate { 14 | 15 | companion object { 16 | const val BASE_URL = "localhost" 17 | const val PORT = 11434 18 | const val PROTOCOL = "http" 19 | 20 | fun create(ollamaConfig: OllamaConfig): Ollama { 21 | val requester = 22 | HttpRequester.default( 23 | createHttpClient( 24 | connectionConfig = 25 | HostPortConnectionConfig( 26 | protocol = ollamaConfig.protocol, 27 | port = ollamaConfig.port, 28 | host = ollamaConfig.baseUrl, 29 | ), 30 | // get from commonModule 31 | features = ClientFeatures(json = JsonLenient), 32 | ) 33 | ) 34 | val ollamaChat = DefaultOllamaChatApi(requester = requester) 35 | val ollamaGenerate = DefaultOllamaGenerateApi(requester = requester) 36 | 37 | return OllamaApi( 38 | config = ollamaConfig, 39 | ollamaChat = ollamaChat, 40 | ollamaGenerate = ollamaGenerate, 41 | ) 42 | } 43 | } 44 | 45 | /** 46 | * This function returns the base URL as a string. 47 | * 48 | * @return a string representing the base URL 49 | */ 50 | fun baseUrl(): String 51 | 52 | /** 53 | * This function returns the port as an integer. 54 | * 55 | * @return an integer representing the port 56 | */ 57 | fun port(): Int 58 | 59 | /** 60 | * This function returns the protocol as a string. 61 | * 62 | * @return a string representing the protocol 63 | */ 64 | fun protocol(): String 65 | } 66 | -------------------------------------------------------------------------------- /openai-gateway/openai-gateway-core/src/commonMain/kotlin/com/tddworks/openai/gateway/api/OpenAIGateway.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.openai.gateway.api 2 | 3 | import com.tddworks.common.network.api.ktor.api.ListResponse 4 | import com.tddworks.openai.api.chat.api.ChatCompletion 5 | import com.tddworks.openai.api.chat.api.ChatCompletionChunk 6 | import com.tddworks.openai.api.chat.api.ChatCompletionRequest 7 | import com.tddworks.openai.api.images.api.Image 8 | import com.tddworks.openai.api.images.api.ImageCreate 9 | import com.tddworks.openai.api.legacy.completions.api.Completion 10 | import com.tddworks.openai.api.legacy.completions.api.CompletionRequest 11 | import kotlinx.coroutines.flow.Flow 12 | 13 | /** Interface for connecting to the OpenAI Gateway to chat. */ 14 | interface OpenAIGateway { 15 | fun updateProvider(id: String, name: String, config: OpenAIProviderConfig) 16 | 17 | fun addProvider(provider: OpenAIProvider): OpenAIGateway 18 | 19 | fun removeProvider(id: String) 20 | 21 | fun getProviders(): List 22 | 23 | /** 24 | * Creates an image given a prompt. Get images as URLs or base64-encoded JSON. 25 | * 26 | * @param request image creation request. 27 | * @return list of images. 28 | */ 29 | suspend fun generate(request: ImageCreate, provider: LLMProvider): ListResponse 30 | 31 | /** 32 | * Fetch a completion. 33 | * 34 | * @param request The request to fetch a completion. 35 | * @param provider The provider to use for the completion. 36 | * @return The completion 37 | */ 38 | suspend fun completions(request: CompletionRequest, provider: LLMProvider): Completion 39 | 40 | /** 41 | * Fetch a chat completion. 42 | * 43 | * @param request The request to fetch a chat completion. 44 | * @param provider The provider to use for the chat completion. 45 | * @return The chat completion 46 | */ 47 | suspend fun chatCompletions( 48 | request: ChatCompletionRequest, 49 | provider: LLMProvider, 50 | ): ChatCompletion 51 | 52 | /** 53 | * Stream a chat completion. 54 | * 55 | * @param request The request to stream a chat completion. 56 | * @param provider The provider to use for the chat completion. 57 | * @return The chat completion chunks as a stream 58 | */ 59 | fun streamChatCompletions( 60 | request: ChatCompletionRequest, 61 | provider: LLMProvider, 62 | ): Flow 63 | } 64 | -------------------------------------------------------------------------------- /anthropic-client/anthropic-client-core/src/commonMain/kotlin/com/tddworks/anthropic/api/Anthropic.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.anthropic.api 2 | 3 | import com.tddworks.anthropic.api.internal.AnthropicApi 4 | import com.tddworks.anthropic.api.messages.api.Messages 5 | import com.tddworks.anthropic.api.messages.api.internal.DefaultMessagesApi 6 | import com.tddworks.anthropic.api.messages.api.internal.JsonLenient 7 | import com.tddworks.common.network.api.ktor.api.HttpRequester 8 | import com.tddworks.common.network.api.ktor.internal.* 9 | 10 | /** Interface for interacting with the Anthropic API. */ 11 | interface Anthropic : Messages { 12 | 13 | /** Companion object containing a constant variable for the base URL of the API. */ 14 | companion object { 15 | const val BASE_URL = "https://api.anthropic.com" 16 | const val ANTHROPIC_VERSION = "2023-06-01" 17 | 18 | /** 19 | * Creates an instance of Anthropic API with the provided configurations. 20 | * 21 | * @return an instance of Anthropic API configured with the provided settings. 22 | */ 23 | fun create(anthropicConfig: AnthropicConfig): Anthropic { 24 | 25 | val requester = 26 | HttpRequester.default( 27 | createHttpClient( 28 | connectionConfig = UrlBasedConnectionConfig(anthropicConfig.baseUrl), 29 | authConfig = AuthConfig(anthropicConfig.apiKey), 30 | // get from commonModule 31 | features = ClientFeatures(json = JsonLenient), 32 | ) 33 | ) 34 | val messages = 35 | DefaultMessagesApi(anthropicConfig = anthropicConfig, requester = requester) 36 | 37 | return AnthropicApi(anthropicConfig = anthropicConfig, messages = messages) 38 | } 39 | } 40 | 41 | /** 42 | * Function to retrieve the API key. 43 | * 44 | * @return a String representing the API key 45 | */ 46 | fun apiKey(): String 47 | 48 | /** 49 | * This function returns the base URL as a string. 50 | * 51 | * @return a string representing the base URL 52 | */ 53 | fun baseUrl(): String 54 | 55 | /** 56 | * Returns the anthropic version of the provided class. The anthropic version is a String 57 | * representation of the class name with "Anthropic" prefixed to it. 58 | * 59 | * @return The anthropic version of the class as a String. 60 | */ 61 | fun anthropicVersion(): String 62 | } 63 | -------------------------------------------------------------------------------- /openai-gateway/openai-gateway-core/src/commonMain/kotlin/com/tddworks/openai/gateway/api/internal/DefaultOpenAIProvider.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(ExperimentalSerializationApi::class) 2 | 3 | package com.tddworks.openai.gateway.api.internal 4 | 5 | import com.tddworks.azure.api.AzureAIProviderConfig 6 | import com.tddworks.azure.api.azure 7 | import com.tddworks.common.network.api.ktor.api.ListResponse 8 | import com.tddworks.openai.api.OpenAI 9 | import com.tddworks.openai.api.chat.api.ChatCompletion 10 | import com.tddworks.openai.api.chat.api.ChatCompletionChunk 11 | import com.tddworks.openai.api.chat.api.ChatCompletionRequest 12 | import com.tddworks.openai.api.images.api.Image 13 | import com.tddworks.openai.api.images.api.ImageCreate 14 | import com.tddworks.openai.api.legacy.completions.api.Completion 15 | import com.tddworks.openai.api.legacy.completions.api.CompletionRequest 16 | import com.tddworks.openai.gateway.api.OpenAIProvider 17 | import com.tddworks.openai.gateway.api.OpenAIProviderConfig 18 | import kotlinx.coroutines.flow.Flow 19 | import kotlinx.serialization.ExperimentalSerializationApi 20 | 21 | class DefaultOpenAIProvider( 22 | override val id: String = "openai", 23 | override val name: String = "OpenAI", 24 | override val config: OpenAIProviderConfig, 25 | private val openAI: OpenAI = OpenAI.default(config.toOpenAIConfig()), 26 | ) : OpenAIProvider { 27 | 28 | override suspend fun chatCompletions(request: ChatCompletionRequest): ChatCompletion { 29 | return openAI.chatCompletions(request) 30 | } 31 | 32 | override fun streamChatCompletions(request: ChatCompletionRequest): Flow { 33 | return openAI.streamChatCompletions(request) 34 | } 35 | 36 | override suspend fun completions(request: CompletionRequest): Completion { 37 | return openAI.completions(request) 38 | } 39 | 40 | override suspend fun generate(request: ImageCreate): ListResponse { 41 | return openAI.generate(request) 42 | } 43 | } 44 | 45 | fun OpenAIProvider.Companion.openAI( 46 | id: String = "openai", 47 | config: OpenAIProviderConfig, 48 | openAI: OpenAI = OpenAI.default(config.toOpenAIConfig()), 49 | ): OpenAIProvider { 50 | return DefaultOpenAIProvider(id = id, config = config, openAI = openAI) 51 | } 52 | 53 | fun OpenAIProvider.Companion.azure( 54 | id: String = "azure", 55 | config: OpenAIProviderConfig, 56 | openAI: OpenAI = OpenAI.azure(config as AzureAIProviderConfig), 57 | ): OpenAIProvider { 58 | return DefaultOpenAIProvider(id = id, config = config, openAI = openAI) 59 | } 60 | -------------------------------------------------------------------------------- /ollama-client/ollama-client-core/src/commonMain/kotlin/com/tddworks/ollama/api/generate/OllamaGenerateResponse.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.ollama.api.generate 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | /** 7 | * { "model": "llama3", "created_at": "2023-08-04T19:22:45.499127Z", "response": "The sky is blue 8 | * because it is the color of the sky.", "done": true, "context": [1, 2, 3], "total_duration": 9 | * 4935886791, "load_duration": 534986708, "prompt_eval_count": 26, "prompt_eval_duration": 10 | * 107345000, "eval_count": 237, "eval_duration": 4289432000 } 11 | */ 12 | @Serializable 13 | data class OllamaGenerateResponse( 14 | @SerialName("model") val model: String, 15 | @SerialName("created_at") val createdAt: String, 16 | /** empty if the response was streamed, if not streamed, this will contain the full response */ 17 | @SerialName("response") val response: String, 18 | @SerialName("done") val done: Boolean, 19 | 20 | /** reason for the conversation ending E.g "length" */ 21 | @SerialName("done_reason") val doneReason: String? = null, 22 | /** 23 | * an encoding of the conversation used in this response, this can be sent in the next request 24 | * to keep a conversational mem 25 | */ 26 | @SerialName("context") val context: List? = null, 27 | /** time spent generating the response */ 28 | @SerialName("total_duration") val totalDuration: Long? = null, 29 | /** time spent in nanoseconds loading the model */ 30 | @SerialName("load_duration") val loadDuration: Long? = null, 31 | /** number of tokens in the prompt */ 32 | @SerialName("prompt_eval_count") val promptEvalCount: Int? = null, 33 | /** time spent in nanoseconds evaluating the prompt */ 34 | @SerialName("prompt_eval_duration") val promptEvalDuration: Long? = null, 35 | /** number of tokens in the response */ 36 | @SerialName("eval_count") val evalCount: Int? = null, 37 | /** time in nanoseconds spent generating the response */ 38 | @SerialName("eval_duration") val evalDuration: Long? = null, 39 | ) { 40 | companion object { 41 | fun dummy() = 42 | OllamaGenerateResponse( 43 | model = "some-model", 44 | createdAt = "createdAt", 45 | response = "response", 46 | done = false, 47 | doneReason = "doneReason", 48 | evalCount = 10, 49 | evalDuration = 1000, 50 | loadDuration = 1000, 51 | promptEvalCount = 10, 52 | promptEvalDuration = 1000, 53 | ) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /openai-client/openai-client-core/src/jvmTest/kotlin/com/tddworks/openai/api/images/api/ImageCreateTest.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.openai.api.images.api 2 | 3 | import com.tddworks.openai.api.chat.api.OpenAIModel 4 | import com.tddworks.openai.api.common.prettyJson 5 | import kotlinx.serialization.encodeToString 6 | import org.junit.jupiter.api.Assertions.assertEquals 7 | import org.junit.jupiter.api.Test 8 | 9 | class ImageCreateTest { 10 | 11 | @Test 12 | fun `should create image with custom model setting`() { 13 | // Given 14 | val json = 15 | """ 16 | { 17 | "prompt": "some prompt", 18 | "model": "dall-e-3" 19 | } 20 | """ 21 | .trimIndent() 22 | 23 | val createImage = 24 | ImageCreate.create( 25 | prompt = "some prompt", 26 | size = null, 27 | style = null, 28 | quality = null, 29 | model = OpenAIModel.DALL_E_3, 30 | ) 31 | 32 | // When 33 | val result = prettyJson.encodeToString(createImage) 34 | 35 | // Then 36 | assertEquals(json, result) 37 | } 38 | 39 | @Test 40 | fun `should create image with all custom settings`() { 41 | // Given 42 | val json = 43 | """ 44 | { 45 | "prompt": "some prompt", 46 | "model": "dall-e-3", 47 | "response_format": "url", 48 | "size": "1024x1024", 49 | "style": "vivid", 50 | "quality": "hd" 51 | } 52 | """ 53 | .trimIndent() 54 | 55 | val createImage = 56 | ImageCreate.create( 57 | prompt = "some prompt", 58 | model = OpenAIModel.DALL_E_3, 59 | size = Size.size1024x1024, 60 | style = Style.vivid, 61 | quality = Quality.hd, 62 | format = ResponseFormat.url, 63 | ) 64 | 65 | // When 66 | val result = prettyJson.encodeToString(createImage) 67 | 68 | // Then 69 | assertEquals(json, result) 70 | } 71 | 72 | @Test 73 | fun `should create image with default settings`() { 74 | // Given 75 | val json = 76 | """ 77 | { 78 | "prompt": "A cute baby sea otter", 79 | "model": "dall-e-3" 80 | } 81 | """ 82 | .trimIndent() 83 | 84 | val createImage = ImageCreate.create(prompt = "A cute baby sea otter") 85 | 86 | // When 87 | val result = prettyJson.encodeToString(createImage) 88 | 89 | // Then 90 | assertEquals(json, result) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /anthropic-client/anthropic-client-core/src/jvmTest/kotlin/com/tddworks/anthropic/api/messages/api/ContentTest.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.anthropic.api.messages.api 2 | 3 | import kotlinx.serialization.json.Json 4 | import org.junit.jupiter.api.Assertions.assertEquals 5 | import org.junit.jupiter.api.Test 6 | import org.skyscreamer.jsonassert.JSONAssert 7 | 8 | /** 9 | * Each input message content may be either a single string or an array of content blocks, where 10 | * each block has a specific type. Using a string for content is shorthand for an array of one 11 | * content block of type "text". The following input messages are equivalent: {"role": "user", 12 | * "content": "Hello, Claude"} {"role": "user", "content": 13 | * [{"type": "text", "text": "Hello, Claude"}]} 14 | */ 15 | class ContentTest { 16 | 17 | @Test 18 | fun `should serialize multiple content`() { 19 | // Given 20 | val content = 21 | Content.BlockContent( 22 | listOf( 23 | BlockMessageContent.ImageContent( 24 | source = 25 | BlockMessageContent.ImageContent.Source( 26 | mediaType = "image1_media_type", 27 | data = "image1_data", 28 | type = "base64", 29 | ) 30 | ), 31 | BlockMessageContent.TextContent(text = "some-text"), 32 | ) 33 | ) 34 | 35 | // When 36 | val result = Json.encodeToString(Content.serializer(), content) 37 | 38 | // Then 39 | JSONAssert.assertEquals( 40 | """ 41 | [ 42 | { 43 | "source": { 44 | "data": "image1_data", 45 | "media_type": "image1_media_type", 46 | "type": "base64" 47 | }, 48 | "type": "image" 49 | }, 50 | { 51 | "text": "some-text", 52 | "type": "text" 53 | } 54 | ] 55 | """ 56 | .trimIndent(), 57 | result, 58 | false, 59 | ) 60 | } 61 | 62 | @Test 63 | fun `should serialize single string content`() { 64 | // Given 65 | val content = Content.TextContent("Hello, Claude") 66 | 67 | // When 68 | val result = Json.encodeToString(Content.serializer(), content) 69 | 70 | // Then 71 | assertEquals( 72 | """ 73 | "Hello, Claude" 74 | """ 75 | .trimIndent(), 76 | result, 77 | ) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /openai-gateway/openai-gateway-darwin/src/appleMain/kotlin/com/tddworks/openai/gateway/api/DarwinOpenAIGateway.kt: -------------------------------------------------------------------------------- 1 | package com.tddworks.openai.gateway.api 2 | 3 | import com.tddworks.anthropic.api.Anthropic 4 | import com.tddworks.ollama.api.Ollama 5 | import com.tddworks.openai.api.OpenAI 6 | import com.tddworks.openai.gateway.api.internal.anthropic 7 | import com.tddworks.openai.gateway.api.internal.default 8 | import com.tddworks.openai.gateway.api.internal.gemini 9 | import com.tddworks.openai.gateway.api.internal.ollama 10 | import com.tddworks.openai.gateway.di.initOpenAIGateway 11 | 12 | /** An object for initializing and configuring the OpenAI Gateway. */ 13 | object DarwinOpenAIGateway { 14 | 15 | /** 16 | * Initializes an OpenAI gateway with the specified configurations. 17 | * 18 | * @param openAIBaseUrl function that provides the base URL for OpenAI services 19 | * @param openAIKey function that provides the API key for OpenAI services 20 | * @param anthropicBaseUrl function that provides the base URL for Anthropic services 21 | * @param anthropicKey function that provides the API key for Anthropic services 22 | * @param anthropicVersion function that provides the version for Anthropic services 23 | * @param ollamaBaseUrl function that provides the base URL for Ollama services 24 | * @param ollamaPort function that provides the port for Ollama services 25 | * @param ollamaProtocol function that provides the protocol (e.g. http, https) for Ollama 26 | * services 27 | * @return a new instance of OpenAIGateway initialized with the provided configurations 28 | */ 29 | fun openAIGateway( 30 | openAIBaseUrl: () -> String = { OpenAI.BASE_URL }, 31 | openAIKey: () -> String = { "CONFIGURE_ME" }, 32 | anthropicBaseUrl: () -> String = { Anthropic.BASE_URL }, 33 | anthropicKey: () -> String = { "CONFIGURE_ME" }, 34 | anthropicVersion: () -> String = { Anthropic.ANTHROPIC_VERSION }, 35 | ollamaBaseUrl: () -> String = { Ollama.BASE_URL }, 36 | geminiBaseUrl: () -> String = { "api.gemini.com" }, 37 | geminiKey: () -> String = { "CONFIGURE_ME" }, 38 | ) = 39 | initOpenAIGateway( 40 | openAIConfig = 41 | OpenAIProviderConfig.default(baseUrl = openAIBaseUrl, apiKey = openAIKey), 42 | anthropicConfig = 43 | OpenAIProviderConfig.anthropic( 44 | baseUrl = anthropicBaseUrl, 45 | apiKey = anthropicKey, 46 | anthropicVersion = anthropicVersion, 47 | ), 48 | ollamaConfig = OpenAIProviderConfig.ollama(baseUrl = ollamaBaseUrl), 49 | geminiConfig = OpenAIProviderConfig.gemini(baseUrl = geminiBaseUrl, apiKey = geminiKey), 50 | ) 51 | } 52 | --------------------------------------------------------------------------------