diff --git a/.gitignore b/.gitignore
index 4e81838d6..b1346e6d1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,6 +2,6 @@
 .gradle
 .idea
 .kotlin
-build
+build/
 codegen.log
 kls_database.db
diff --git a/build.gradle.kts b/build.gradle.kts
index 2827d420a..71faa7d69 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -11,6 +11,19 @@ allprojects {
     version = "3.0.3" // x-release-please-version
 }
 
+subprojects {
+    // These are populated with dependencies by `buildSrc` scripts.
+    tasks.register("format") {
+        group = "Verification"
+        description = "Formats all source files."
+    }
+    tasks.register("lint") {
+        group = "Verification"
+        description = "Verifies all source files are formatted."
+    }
+    apply(plugin = "org.jetbrains.dokka")
+}
+
 subprojects {
     apply(plugin = "org.jetbrains.dokka")
 }
diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts
index 778c89de5..c6dc92ec5 100644
--- a/buildSrc/build.gradle.kts
+++ b/buildSrc/build.gradle.kts
@@ -10,7 +10,6 @@ repositories {
 }
 
 dependencies {
-    implementation("com.diffplug.spotless:spotless-plugin-gradle:7.0.2")
     implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.20")
     implementation("com.vanniktech:gradle-maven-publish-plugin:0.28.0")
 }
diff --git a/buildSrc/src/main/kotlin/openai.java.gradle.kts b/buildSrc/src/main/kotlin/openai.java.gradle.kts
index dfbacb86e..70fc33f41 100644
--- a/buildSrc/src/main/kotlin/openai.java.gradle.kts
+++ b/buildSrc/src/main/kotlin/openai.java.gradle.kts
@@ -1,24 +1,13 @@
-import com.diffplug.gradle.spotless.SpotlessExtension
 import org.gradle.api.tasks.testing.logging.TestExceptionFormat
 
 plugins {
     `java-library`
-    id("com.diffplug.spotless")
 }
 
 repositories {
     mavenCentral()
 }
 
-configure<SpotlessExtension> {
-    java {
-        importOrder()
-        removeUnusedImports()
-        palantirJavaFormat()
-        toggleOffOn()
-    }
-}
-
 java {
     toolchain {
         languageVersion.set(JavaLanguageVersion.of(21))
@@ -53,3 +42,86 @@ tasks.withType<Test>().configureEach {
         exceptionFormat = TestExceptionFormat.FULL
     }
 }
+
+val palantir by configurations.creating
+dependencies {
+    palantir("com.palantir.javaformat:palantir-java-format:2.73.0")
+}
+
+fun registerPalantir(
+    name: String,
+    description: String,
+) {
+    val javaName = "${name}Java"
+    tasks.register<JavaExec>(javaName) {
+        group = "Verification"
+        this.description = description
+
+        classpath = palantir
+        mainClass = "com.palantir.javaformat.java.Main"
+
+        // Avoid an `IllegalAccessError` on Java 9+.
+        jvmArgs(
+            "--add-exports", "jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED",
+            "--add-exports", "jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED",
+            "--add-exports", "jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED",
+            "--add-exports", "jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED",
+            "--add-exports", "jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED",
+        )
+
+        // Use paths relative to the current module.
+        val argumentFile =
+            project.layout.buildDirectory.file("palantir-$name-args.txt").get().asFile
+        val lastRunTimeFile =
+            project.layout.buildDirectory.file("palantir-$name-last-run.txt").get().asFile
+
+        // Read the time when this task was last executed for this module (if ever).
+        val lastRunTime = lastRunTimeFile.takeIf { it.exists() }?.readText()?.toLongOrNull() ?: 0L
+
+        // Use a `fileTree` relative to the module's source directory.
+        val javaFiles = project.fileTree("src") { include("**/*.java") }
+
+        // Determine if any files need to be formatted or linted and continue only if there is at least
+        // one file.
+        onlyIf { javaFiles.any { it.lastModified() > lastRunTime } }
+
+        inputs.files(javaFiles)
+
+        doFirst {
+            // Create the argument file and set the preferred formatting style.
+            argumentFile.parentFile.mkdirs()
+            argumentFile.writeText("--palantir\n")
+
+            if (name == "lint") {
+                // For lint, do a dry run, so no files are modified. Set the exit code to 1 (instead of
+                // the default 0) if any files need to be formatted, indicating that linting has failed.
+                argumentFile.appendText("--dry-run\n")
+                argumentFile.appendText("--set-exit-if-changed\n")
+            } else {
+                // `--dry-run` and `--replace` (for in-place formatting) are mutually exclusive.
+                argumentFile.appendText("--replace\n")
+            }
+
+            // Write the modified files to the argument file.
+            javaFiles.filter { it.lastModified() > lastRunTime }
+                .forEach { argumentFile.appendText("${it.absolutePath}\n") }
+        }
+
+        doLast {
+            // Record the last execution time for later up-to-date checking.
+            lastRunTimeFile.writeText(System.currentTimeMillis().toString())
+        }
+
+        // Pass the argument file using the @ symbol
+        args = listOf("@${argumentFile.absolutePath}")
+
+        outputs.upToDateWhen { javaFiles.none { it.lastModified() > lastRunTime } }
+    }
+
+    tasks.named(name) {
+        dependsOn(tasks.named(javaName))
+    }
+}
+
+registerPalantir(name = "format", description = "Formats all Java source files.")
+registerPalantir(name = "lint", description = "Verifies all Java source files are formatted.")
diff --git a/buildSrc/src/main/kotlin/openai.kotlin.gradle.kts b/buildSrc/src/main/kotlin/openai.kotlin.gradle.kts
index 0405b0e58..bf7aa8055 100644
--- a/buildSrc/src/main/kotlin/openai.kotlin.gradle.kts
+++ b/buildSrc/src/main/kotlin/openai.kotlin.gradle.kts
@@ -1,4 +1,3 @@
-import com.diffplug.gradle.spotless.SpotlessExtension
 import org.jetbrains.kotlin.gradle.dsl.JvmTarget
 import org.jetbrains.kotlin.gradle.dsl.KotlinVersion
 
@@ -7,6 +6,10 @@ plugins {
     kotlin("jvm")
 }
 
+repositories {
+    mavenCentral()
+}
+
 kotlin {
     jvmToolchain {
         languageVersion.set(JavaLanguageVersion.of(21))
@@ -27,14 +30,77 @@ kotlin {
     }
 }
 
-configure<SpotlessExtension> {
-    kotlin {
-        ktfmt().kotlinlangStyle()
-        toggleOffOn()
-    }
-}
-
 tasks.withType<Test>().configureEach {
     systemProperty("junit.jupiter.execution.parallel.enabled", true)
     systemProperty("junit.jupiter.execution.parallel.mode.default", "concurrent")
 }
+
+val ktfmt by configurations.creating
+dependencies {
+    ktfmt("com.facebook:ktfmt:0.56")
+}
+
+fun registerKtfmt(
+    name: String,
+    description: String,
+) {
+    val kotlinName = "${name}Kotlin"
+    tasks.register<JavaExec>(kotlinName) {
+        group = "Verification"
+        this.description = description
+
+        classpath = ktfmt
+        mainClass = "com.facebook.ktfmt.cli.Main"
+
+        // Use paths relative to the current module.
+        val argumentFile = project.layout.buildDirectory.file("ktfmt-$name-args.txt").get().asFile
+        val lastRunTimeFile =
+            project.layout.buildDirectory.file("ktfmt-$name-last-run.txt").get().asFile
+
+        // Read the time when this task was last executed for this module (if ever).
+        val lastRunTime = lastRunTimeFile.takeIf { it.exists() }?.readText()?.toLongOrNull() ?: 0L
+
+        // Use a `fileTree` relative to the module's source directory.
+        val kotlinFiles = project.fileTree("src") { include("**/*.kt") }
+
+        // Determine if any files need to be formatted or linted and continue only if there is at least
+        // one file (otherwise Ktfmt will fail).
+        onlyIf { kotlinFiles.any { it.lastModified() > lastRunTime } }
+
+        inputs.files(kotlinFiles)
+
+        doFirst {
+            // Create the argument file and set the preferred formatting style.
+            argumentFile.parentFile.mkdirs()
+            argumentFile.writeText("--kotlinlang-style\n")
+
+            if (name == "lint") {
+                // For lint, do a dry run, so no files are modified. Set the exit code to 1 (instead of
+                // the default 0) if any files need to be formatted, indicating that linting has failed.
+                argumentFile.appendText("--dry-run\n")
+                argumentFile.appendText("--set-exit-if-changed\n")
+            }
+
+            // Write the modified files to the argument file.
+            kotlinFiles.filter { it.lastModified() > lastRunTime }
+                .forEach { argumentFile.appendText("${it.absolutePath}\n") }
+        }
+
+        doLast {
+            // Record the last execution time for later up-to-date checking.
+            lastRunTimeFile.writeText(System.currentTimeMillis().toString())
+        }
+
+        // Pass the argument file using the @ symbol
+        args = listOf("@${argumentFile.absolutePath}")
+
+        outputs.upToDateWhen { kotlinFiles.none { it.lastModified() > lastRunTime } }
+    }
+
+    tasks.named(name) {
+        dependsOn(tasks.named(kotlinName))
+    }
+}
+
+registerKtfmt(name = "format", description = "Formats all Kotlin source files.")
+registerKtfmt(name = "lint", description = "Verifies all Kotlin source files are formatted.")
diff --git a/openai-java-core/src/main/kotlin/com/openai/models/embeddings/EmbeddingValue.kt b/openai-java-core/src/main/kotlin/com/openai/models/embeddings/EmbeddingValue.kt
index 1bd26ded0..4bf046d1e 100644
--- a/openai-java-core/src/main/kotlin/com/openai/models/embeddings/EmbeddingValue.kt
+++ b/openai-java-core/src/main/kotlin/com/openai/models/embeddings/EmbeddingValue.kt
@@ -115,10 +115,10 @@ private constructor(
             return true
         }
 
-        return /* spotless:off */ other is EmbeddingValue && floats == other.floats && base64 == other.base64 /* spotless:on */
+        return other is EmbeddingValue && floats == other.floats && base64 == other.base64
     }
 
-    override fun hashCode(): Int = /* spotless:off */  Objects.hash(floats, base64) /* spotless:on */
+    override fun hashCode(): Int = Objects.hash(floats, base64)
 
     override fun toString(): String =
         when {
diff --git a/scripts/build b/scripts/build
new file mode 100755
index 000000000..f40634826
--- /dev/null
+++ b/scripts/build
@@ -0,0 +1,8 @@
+#!/usr/bin/env bash
+
+set -e
+
+cd "$(dirname "$0")/.."
+
+echo "==> Building classes"
+./gradlew build testClasses -x test
diff --git a/scripts/format b/scripts/format
index 456a69db9..7c0be4d57 100755
--- a/scripts/format
+++ b/scripts/format
@@ -4,5 +4,5 @@ set -e
 
 cd "$(dirname "$0")/.."
 
-echo "==> Running spotlessApply"
-./gradlew spotlessApply
+echo "==> Running formatters"
+./gradlew format
diff --git a/scripts/lint b/scripts/lint
index e3a5f5e24..aea8af713 100755
--- a/scripts/lint
+++ b/scripts/lint
@@ -4,5 +4,5 @@ set -e
 
 cd "$(dirname "$0")/.."
 
-echo "==> Build classes"
-./gradlew build testClasses -x test
+echo "==> Running lints"
+./gradlew lint
diff --git a/scripts/test b/scripts/test
index 2177cb8ae..047bc1dbb 100755
--- a/scripts/test
+++ b/scripts/test
@@ -53,4 +53,4 @@ else
 fi
 
 echo "==> Running tests"
-./gradlew test
+./gradlew test "$@"
diff --git a/settings.gradle.kts b/settings.gradle.kts
index b435600ed..76435c79f 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -1,8 +1,14 @@
 rootProject.name = "openai-java-root"
 
-include("openai-java")
-include("openai-java-client-okhttp")
-include("openai-java-core")
-include("openai-java-spring-boot-starter")
-include("openai-java-proguard-test")
-include("openai-java-example")
+val projectNames = rootDir.listFiles()
+    ?.asSequence()
+    .orEmpty()
+    .filter { file ->
+        file.isDirectory &&
+        file.name.startsWith("openai-java") &&
+        file.listFiles()?.asSequence().orEmpty().any { it.name == "build.gradle.kts" }
+    }
+    .map { it.name }
+    .toList()
+println("projects: $projectNames")
+projectNames.forEach { include(it) }
