Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ on:
workflow_dispatch: # Start a workflow
push:
branches:
- "main"
- main
pull_request:
branches:
- main

jobs:
build:
Expand Down
104 changes: 104 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Build and Development Commands

### Building the Project
```bash
./gradlew build # Build all modules
./gradlew clean build # Clean and build all modules
./gradlew assemble # Assemble outputs without running tests
```

### Running Tests
```bash
./gradlew test # Run tests for all platforms
./gradlew jvmTest # Run JVM tests only
./gradlew macosArm64Test # Run macOS ARM64 tests
./gradlew iosSimulatorArm64Test # Run iOS simulator tests
./gradlew allTests # Run tests for all targets with aggregated report
./gradlew check # Run all verification tasks
```

### Test Coverage
```bash
./gradlew koverHtmlReport # Generate HTML coverage report for all code
./gradlew koverXmlReport # Generate XML coverage report
./gradlew koverVerify # Run coverage verification (min 86% required)
```

### Running Specific Module Tests
```bash
./gradlew :openai-client:openai-client-core:test
./gradlew :anthropic-client:anthropic-client-core:test
./gradlew :ollama-client:ollama-client-core:test
./gradlew :gemini-client:gemini-client-core:test
./gradlew :openai-gateway:openai-gateway-core:test
```

## Project Architecture

This is a Kotlin Multiplatform project providing AI/LLM client implementations for multiple providers. The codebase follows a modular architecture with clear separation of concerns.

### Core Architecture Patterns

1. **Multiplatform Structure**: Each client module has platform-specific implementations:
- `-core`: Common implementation shared across platforms
- `-darwin`: Apple platform specific implementations (iOS, macOS)
- `-cio`: JVM-specific implementations using CIO

2. **Provider Pattern**: The `openai-gateway` module implements a gateway pattern that allows switching between different LLM providers (OpenAI, Anthropic, Ollama, Gemini) using a unified interface.

3. **HTTP Client Abstraction**: The `common` module provides a shared `HttpRequester` interface that abstracts HTTP operations across platforms, using Ktor under the hood.

4. **Dependency Injection**: Uses Koin for dependency injection across the codebase, with platform-specific configurations.

### Module Structure

- **common/**: Shared networking and utility code
- HTTP client abstraction (`HttpRequester`)
- Ktor configuration for different platforms
- JSON serialization utilities

- **openai-client/**: OpenAI API client implementation
- Chat completions, images, and legacy completions APIs
- Streaming support for chat completions

- **anthropic-client/**: Anthropic Claude API client
- Messages API implementation
- Image support with base64 encoding

- **ollama-client/**: Ollama local LLM client
- Chat and generate endpoints
- Local model management

- **gemini-client/**: Google Gemini API client
- Text generation with multimodal support

- **openai-gateway/**: Unified gateway for all providers
- Provider abstraction allowing runtime switching
- Adapter pattern to convert between provider-specific and OpenAI formats
- Extensions for converting requests/responses between different provider formats

### Key Interfaces

- `OpenAI`: Main interface for OpenAI operations (Chat, Images, Completions)
- `OpenAIGateway`: Gateway interface for multi-provider support
- `HttpRequester`: HTTP client abstraction for cross-platform requests
- `OpenAIProvider`: Provider interface for different LLM services

### Configuration

Each client uses a configuration pattern (e.g., `OpenAIConfig`, `AnthropicConfig`) that requires:
- Base URL (with defaults for each provider)
- API key
- Optional provider-specific settings

### Testing Strategy

The project uses:
- Unit tests with mocked HTTP clients (`MockHttpClient`)
- Integration tests (files ending with `ITest`) for actual API calls
- Platform-specific test configurations
- Kover for code coverage with 86% minimum threshold
3 changes: 2 additions & 1 deletion anthropic-client/anthropic-client-core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,10 @@ kotlin {
implementation(libs.koin.test)
implementation(libs.koin.test.junit5)
implementation(libs.app.cash.turbine)
implementation("com.tngtech.archunit:archunit-junit5:1.1.0")
implementation("com.tngtech.archunit:archunit-junit5:1.4.1")
implementation("org.reflections:reflections:0.10.2")
implementation(libs.org.skyscreamer.jsonassert)
implementation("org.junit.platform:junit-platform-launcher")
}
}
}
Expand Down
7 changes: 7 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import nl.littlerobots.vcu.plugin.resolver.VersionSelectors

plugins {
`maven-publish`
//trick: for the same plugin versions in all sub-modules
Expand All @@ -8,6 +10,7 @@ plugins {
alias(libs.plugins.build.dokka.plugin)

alias(libs.plugins.kotlinx.binary.validator) apply false
id("nl.littlerobots.version-catalog-update") version "1.0.0"
id("com.tddworks.central-portal-publisher") version "0.0.5"
}

Expand All @@ -28,6 +31,10 @@ dependencies {
kover(projects.common)
}

versionCatalogUpdate {
versionSelector(VersionSelectors.STABLE)
}

val autoVersion = project.property(
if (project.hasProperty("AUTO_VERSION")) {
"AUTO_VERSION"
Expand Down
4 changes: 3 additions & 1 deletion common/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ plugins {
kotlin {
jvm()
macosArm64()
macosX64()
iosArm64()
iosSimulatorArm64()

Expand Down Expand Up @@ -40,8 +41,9 @@ kotlin {
implementation(libs.koin.test)
implementation(libs.koin.test.junit5)
implementation(libs.app.cash.turbine)
implementation("com.tngtech.archunit:archunit-junit5:1.1.0")
implementation("com.tngtech.archunit:archunit-junit5:1.4.1")
implementation("org.reflections:reflections:0.10.2")
implementation("org.junit.platform:junit-platform-launcher")
}
}
}
Expand Down
19 changes: 16 additions & 3 deletions gemini-client/gemini-client-core/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import com.google.devtools.ksp.gradle.KspAATask
import com.google.devtools.ksp.gradle.KspTaskMetadata
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import org.jetbrains.kotlin.gradle.tasks.KotlinNativeCompile

plugins {
alias(libs.plugins.kotlinx.serialization)
Expand Down Expand Up @@ -46,9 +48,10 @@ kotlin {
implementation(libs.koin.test)
implementation(libs.koin.test.junit5)
implementation(libs.app.cash.turbine)
implementation("com.tngtech.archunit:archunit-junit5:1.1.0")
implementation("com.tngtech.archunit:archunit-junit5:1.4.1")
implementation("org.reflections:reflections:0.10.2")
implementation(libs.org.skyscreamer.jsonassert)
implementation("org.junit.platform:junit-platform-launcher")
}
}
}
Expand All @@ -59,12 +62,22 @@ dependencies {
}

// WORKAROUND: ADD this dependsOn("kspCommonMainKotlinMetadata") instead of above dependencies
tasks.withType<KotlinCompile>().configureEach {
//tasks.withType<KotlinCompile>().configureEach {
// if (name != "kspCommonMainKotlinMetadata") {
// dependsOn("kspCommonMainKotlinMetadata")
// }
//}

// Add dependency for native compilation tasks as well
tasks.withType<KspAATask>().configureEach {
if (name != "kspCommonMainKotlinMetadata") {
dependsOn("kspCommonMainKotlinMetadata")
}
}

// `tasks.sourcesJar` is not exists, so `tasks.metadataSourcesJar`
tasks.sourcesJar.configure {
dependsOn("kspCommonMainKotlinMetadata")
}

ksp {
arg("KOIN_DEFAULT_MODULE", "false")
Expand Down
2 changes: 2 additions & 0 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ kotlin.mpp.enableCInteropCommonization=true
android.useAndroidX=true
android.nonTransitiveRClass=true

org.jetbrains.dokka.experimental.gradle.pluginMode=V2EnabledWithHelpers

#publishing for kmmbridge
## Darwin Publish require from - nextVersion parameter must be a valid semver string. Current value: 0.1.4.
## So we need set version to 0.1 or 0.2 ......
Expand Down
46 changes: 19 additions & 27 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ jvm-test = ["juinit-jupiter", "mockito-junit-jupiter", "mockito-kotlin", "ktor-c
#kotlin-gradle-plugin = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-plugin", version.ref = "kotlin" }
ksp = { id ="com.google.devtools.ksp", version.ref = "ksp" }


androidLibrary = { id = "com.android.library", version.ref = "agp" }
kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
kotlinx-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
Expand All @@ -20,13 +19,7 @@ touchlab-skie = { id = "co.touchlab.skie", version.ref = "touchlab-skie" }

com-linecorp-build-recipe = { id = "com.linecorp.build-recipe-plugin", version.ref = "com-linecorp-build-recipe-plugin" }

kotlinx-binary-validator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version = "0.14.0" }
## ⬆ = "0.15.0" }
## ⬆ = "0.15.1" }
## ⬆ = "0.16.0" }
## ⬆ = "0.16.1" }
## ⬆ = "0.16.2" }
## ⬆ = "0.16.3" }
kotlinx-binary-validator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version = "0.18.1" }

# Quality and coverage
kover = { id = "org.jetbrains.kotlinx.kover", version.ref = "kover" }
Expand All @@ -41,42 +34,42 @@ gradle-kotlinter = { id = "org.jmailen.kotlinter", version.ref = "gradle-kotlint

[versions]

agp = "8.7.0"
kotlin = "2.0.20"
agp = "8.12.1"
kotlin = "2.2.0"

ktor = "2.3.12"
kotlinx-serialization = "1.7.3"
kover = "0.8.3"
kotlinx-coroutines = "1.9.0"
ktor = "3.2.3"
kotlinx-serialization = "1.9.0"
kover = "0.9.1"
kotlinx-coroutines = "1.10.2"

#logging-versions
napier = "2.6.1"

#test-versions
junit = "5.11.2"
mockito-junit-jupiter = "5.14.1"
mockito-kotlin = "5.4.0"
assertj-core = "3.26.3"
junit = "5.11.3"
mockito-junit-jupiter = "5.19.0"
mockito-kotlin = "6.0.0"
assertj-core = "3.27.4"
app-cash-turbine = "1.0.0"
org-skyscreamer-jsonassert = "2.0-rc1"

#formatting-versions
gradle-kotlinter = "4.2.0"
gradle-kotlinter = "5.2.0"

# DI
koin-core = "4.0.1"
koin-annotations = "2.0.0-Beta3"
koin-core = "4.1.0"
koin-annotations = "2.1.0"

# plugins
touchlab-skie = "0.9.2"
touchlab-skie = "0.10.5"

touchlab-kmmbridge = "1.2.0"
touchlab-kmmbridge = "1.2.1"

com-linecorp-build-recipe-plugin = "1.1.1"

dokka = "1.9.20"
dokka = "2.0.0"

ksp = "2.0.20-1.0.25"
ksp = "2.2.0-2.0.2"

android-minSdk = "24"
android-compileSdk = "34"
Expand Down Expand Up @@ -125,8 +118,7 @@ kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-t

org-skyscreamer-jsonassert = { module = "org.skyscreamer:jsonassert", version.ref = "org-skyscreamer-jsonassert" }

app-cash-turbine = { module = "app.cash.turbine:turbine", version = "1.0.0" }
## ⬆ = "1.1.0" }
app-cash-turbine = { module = "app.cash.turbine:turbine", version = "1.2.1" }

# Formatting
gradle-kotlinter = { module = "org.jmailen.gradle:kotlinter-gradle", version.ref = "gradle-kotlinter" }
3 changes: 2 additions & 1 deletion ollama-client/ollama-client-core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,9 @@ kotlin {
implementation(libs.koin.test)
implementation(libs.koin.test.junit5)
implementation(libs.app.cash.turbine)
implementation("com.tngtech.archunit:archunit-junit5:1.1.0")
implementation("com.tngtech.archunit:archunit-junit5:1.4.1")
implementation("org.reflections:reflections:0.10.2")
implementation("org.junit.platform:junit-platform-launcher")
}
}
}
Expand Down
4 changes: 3 additions & 1 deletion openai-client/openai-client-core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ plugins {
kotlin {
jvm()
macosArm64()
macosX64()
iosArm64()
iosSimulatorArm64()
sourceSets {
Expand Down Expand Up @@ -34,8 +35,9 @@ kotlin {
implementation(libs.koin.test)
implementation(libs.koin.test.junit5)
implementation(libs.app.cash.turbine)
implementation("com.tngtech.archunit:archunit-junit5:1.1.0")
implementation("com.tngtech.archunit:archunit-junit5:1.4.1")
implementation("org.reflections:reflections:0.10.2")
implementation("org.junit.platform:junit-platform-launcher")
}
}
}
Expand Down
1 change: 1 addition & 0 deletions openai-client/openai-client-darwin/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ plugins {
kotlin {
listOf(
macosArm64(),
macosX64(),
iosArm64(),
iosSimulatorArm64()
).forEach { macosTarget ->
Expand Down
3 changes: 2 additions & 1 deletion openai-gateway/openai-gateway-core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,9 @@ kotlin {
implementation(libs.koin.test)
implementation(libs.koin.test.junit5)
implementation(libs.app.cash.turbine)
implementation("com.tngtech.archunit:archunit-junit5:1.1.0")
implementation("com.tngtech.archunit:archunit-junit5:1.4.1")
implementation("org.reflections:reflections:0.10.2")
implementation("org.junit.platform:junit-platform-launcher")
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")


plugins {
id("de.fayard.refreshVersions") version "0.60.5"
id("de.fayard.refreshVersions") version "0.60.6"
}

fun String.isNonStable(): Boolean {
Expand Down
Loading