From 784929113a4f9b8664245c386c53446397f7088b Mon Sep 17 00:00:00 2001 From: Justin Beckwith Date: Mon, 27 Jul 2026 10:33:48 -0700 Subject: [PATCH 1/7] ci: enforce Java version support policy --- .github/workflows/ci.yml | 58 +++++++++++++ .github/workflows/runtime-compatibility.yml | 68 +++++++++++++++ CONTRIBUTING.md | 15 +++- README.md | 14 ++- build.gradle.kts | 64 ++++++++++++++ .../gradle/VerifyVersionSupportPolicyTask.kt | 80 +++++++++++++++++ docs/version-support-policy.md | 87 +++++++++++++++++++ gradle/version-support.properties | 20 +++++ .../openai/core/http/LoggingHttpClientTest.kt | 4 - .../build.gradle.kts | 31 +++++++ .../RuntimeCompatibilitySmokeTest.java | 27 ++++++ scripts/build | 2 +- scripts/version-support-matrix | 34 ++++++++ 13 files changed, 493 insertions(+), 11 deletions(-) create mode 100644 .github/workflows/runtime-compatibility.yml create mode 100644 buildSrc/src/main/kotlin/com/openai/gradle/VerifyVersionSupportPolicyTask.kt create mode 100644 docs/version-support-policy.md create mode 100644 gradle/version-support.properties create mode 100644 openai-java-runtime-compatibility/build.gradle.kts create mode 100644 openai-java-runtime-compatibility/src/main/java/com/openai/compatibility/RuntimeCompatibilitySmokeTest.java create mode 100755 scripts/version-support-matrix diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 072835527..8ebd817ab 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -138,6 +138,60 @@ jobs: chmod +x "$BASE_DETECTOR" "$BASE_DETECTOR" "$BASE_COMMIT" + version_support_matrix: + name: CI / version support matrix + runs-on: ubuntu-24.04 + timeout-minutes: 5 + + outputs: + runtime-matrix: ${{ steps.matrix.outputs.runtime-matrix }} + + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + persist-credentials: false + + - name: Read version support policy + id: matrix + run: echo "runtime-matrix=$(./scripts/version-support-matrix pull-request)" >> "$GITHUB_OUTPUT" + + runtime_compatibility: + name: CI / runtime compatibility / Java ${{ matrix.java }} + needs: version_support_matrix + runs-on: ubuntu-24.04 + timeout-minutes: 20 + + strategy: + fail-fast: false + matrix: + java: ${{ fromJSON(needs.version_support_matrix.outputs.runtime-matrix) }} + + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + persist-credentials: false + + - name: Set up consumer Java + uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5 + with: + distribution: temurin + java-version: ${{ matrix.java }} + + - name: Set up build Java + uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5 + with: + distribution: temurin + java-version: 21 + + - name: Set up Gradle + uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5.0.2 + + - name: Run consumer compatibility smoke test + run: >- + ./scripts/gradle + :openai-java-runtime-compatibility:runRuntimeCompatibility + -PruntimeJavaVersion=${{ matrix.java }} + required: name: CI / required if: always() @@ -146,6 +200,7 @@ jobs: - build - test - api_compatibility + - runtime_compatibility runs-on: ubuntu-24.04 timeout-minutes: 5 @@ -159,6 +214,7 @@ jobs: BUILD_RESULT: ${{ needs.build.result }} TEST_RESULT: ${{ needs.test.result }} API_COMPATIBILITY_RESULT: ${{ needs.api_compatibility.result }} + RUNTIME_COMPATIBILITY_RESULT: ${{ needs.runtime_compatibility.result }} run: | set -euo pipefail @@ -178,6 +234,8 @@ jobs: if [[ "$EVENT_NAME" == "pull_request" && "$API_COMPATIBILITY_RESULT" != "success" ]]; then failed_jobs+=("API compatibility: $API_COMPATIBILITY_RESULT") fi + [[ "$RUNTIME_COMPATIBILITY_RESULT" == "success" ]] || + failed_jobs+=("runtime compatibility: $RUNTIME_COMPATIBILITY_RESULT") if (( ${#failed_jobs[@]} > 0 )); then printf 'Required CI job did not succeed: %s\n' "${failed_jobs[@]}" diff --git a/.github/workflows/runtime-compatibility.yml b/.github/workflows/runtime-compatibility.yml new file mode 100644 index 000000000..cf25e61a5 --- /dev/null +++ b/.github/workflows/runtime-compatibility.yml @@ -0,0 +1,68 @@ +name: Runtime compatibility + +on: + schedule: + - cron: '0 7 * * *' + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: runtime-compatibility-${{ github.ref }} + cancel-in-progress: false + +jobs: + version_support_matrix: + name: Runtime compatibility / version support matrix + runs-on: ubuntu-24.04 + timeout-minutes: 5 + + outputs: + runtime-matrix: ${{ steps.matrix.outputs.runtime-matrix }} + + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + persist-credentials: false + + - name: Read version support policy + id: matrix + run: echo "runtime-matrix=$(./scripts/version-support-matrix nightly)" >> "$GITHUB_OUTPUT" + + runtime_compatibility: + name: Runtime compatibility / Java ${{ matrix.java }} + needs: version_support_matrix + runs-on: ubuntu-24.04 + timeout-minutes: 20 + + strategy: + fail-fast: false + matrix: + java: ${{ fromJSON(needs.version_support_matrix.outputs.runtime-matrix) }} + + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + persist-credentials: false + + - name: Set up consumer Java + uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5 + with: + distribution: temurin + java-version: ${{ matrix.java }} + + - name: Set up build Java + uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5 + with: + distribution: temurin + java-version: 21 + + - name: Set up Gradle + uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5.0.2 + + - name: Run consumer compatibility smoke test + run: >- + ./scripts/gradle + :openai-java-runtime-compatibility:runRuntimeCompatibility + -PruntimeJavaVersion=${{ matrix.java }} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index edc52c816..632057e3b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,11 +2,14 @@ ## Setting up the environment -This repository uses [Gradle](https://gradle.org/) with Kotlin DSL for building and dependency management. The SDK requires Java 8, but development requires JDK 21 for the Kotlin toolchain. +This repository uses [Gradle](https://gradle.org/) with Kotlin DSL for building and dependency +management. The framework-neutral SDK requires Java 8, while development requires JDK 21 for the +Kotlin toolchain. See the [Java version support policy](docs/version-support-policy.md) for +artifact-level runtime, framework, lifecycle, and release rules. ## Project structure -The SDK consists of three artifacts: +The SDK's primary artifacts are: - `openai-java-core` - Contains core SDK logic @@ -18,6 +21,14 @@ The SDK consists of three artifacts: - `openai-java` - Depends on and exposes the APIs of both `openai-java-core` and `openai-java-client-okhttp` - Does not have its own logic +- `openai-java-bedrock` + - Adds optional Amazon Bedrock authentication and credential-provider integration +- `openai-java-spring-boot-starter` + - Provides the legacy Spring Boot 2 integration + - Is OpenAI EOL and receives no new features + +`openai-java-runtime-compatibility` is a non-published fixture that loads representative public +entry points on the JVMs declared in `gradle/version-support.properties`. ## Modifying or adding code diff --git a/README.md b/README.md index 63157594d..5152089d9 100644 --- a/README.md +++ b/README.md @@ -19,8 +19,6 @@ The REST API documentation can be found on [platform.openai.com](https://platfor -[_Try `openai-java-spring-boot-starter` if you're using Spring Boot!_](#spring-boot) - ### Gradle ```kotlin @@ -41,7 +39,8 @@ implementation("com.openai:openai-java:4.45.0") ## Requirements -This library requires Java 8 or later. +The framework-neutral SDK artifacts require Java 8 or later. Runtime floors and lifecycle states +are declared per artifact in the [Java version support policy](docs/version-support-policy.md). ## Usage @@ -1453,7 +1452,14 @@ GraalVM should automatically detect and use the published metadata, but [manual ## Spring Boot -If you're using Spring Boot, then you can use the SDK's [Spring Boot starter](https://docs.spring.io/spring-boot/docs/2.7.18/reference/htmlsingle/#using.build-systems.starters) to simplify configuration and get set up quickly. +> [!WARNING] +> `openai-java-spring-boot-starter` targets Spring Boot 2.7, which is OpenAI EOL under the +> [Java version support policy](docs/version-support-policy.md). The artifact remains available for +> existing applications but does not receive new features. New Spring applications should depend +> on `openai-java` directly and provide an `OpenAIClient` bean until a supported, +> generation-specific integration is available. + +Existing Spring Boot 2 applications can use the legacy starter to simplify configuration. ### Installation diff --git a/build.gradle.kts b/build.gradle.kts index b2a975228..9658f6536 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,3 +1,8 @@ +import com.openai.gradle.VerifyVersionSupportPolicyTask +import java.util.Properties +import org.gradle.api.plugins.JavaPluginExtension +import org.gradle.api.tasks.SourceSetContainer + buildscript { dependencies { constraints { @@ -56,6 +61,65 @@ subprojects { apply(plugin = "org.jetbrains.dokka") } +val versionSupportFile = layout.projectDirectory.file("gradle/version-support.properties") +val versionSupport = + Properties().apply { versionSupportFile.asFile.inputStream().use(::load) } +val artifactRuntimeFloors = + versionSupport + .stringPropertyNames() + .filter { it.startsWith("artifact.") && it.endsWith(".runtime") } + .associate { property -> + property.removePrefix("artifact.").removeSuffix(".runtime") to + versionSupport.getProperty(property).toInt() + } + +tasks.register("verifyVersionSupportPolicy") { + group = "verification" + description = "Verifies published artifact runtime floors and lifecycle declarations." + + inputs.file(versionSupportFile) + dependsOn(artifactRuntimeFloors.keys.map { ":$it:verifyVersionSupportPolicy" }) +} + +subprojects { + val artifactName = name + + pluginManager.withPlugin("openai.publish") { + val runtimeFloor = + artifactRuntimeFloors[artifactName] + ?: error( + "$artifactName is published but missing from ${versionSupportFile.asFile}" + ) + val lifecycle = + versionSupport.getProperty("artifact.$artifactName.lifecycle") + ?: error( + "$artifactName is missing a lifecycle in ${versionSupportFile.asFile}" + ) + + pluginManager.withPlugin("java") { + val java = extensions.getByType(JavaPluginExtension::class.java) + val mainSourceSet = extensions.getByType(SourceSetContainer::class.java).getByName("main") + + tasks.register("verifyVersionSupportPolicy") { + group = "verification" + description = + "Verifies $artifactName against its declared version support policy." + + this.artifactName.set(artifactName) + this.runtimeFloor.set(runtimeFloor) + this.lifecycle.set(lifecycle) + requiredBuildJdk.set(versionSupport.getProperty("build.jdk").toInt()) + configuredBuildJdk.set(java.toolchain.languageVersion.map { it.asInt() }) + sourceCompatibility.set(java.sourceCompatibility.majorVersion.toInt()) + targetCompatibility.set(java.targetCompatibility.majorVersion.toInt()) + classFiles.from(mainSourceSet.output.classesDirs) + + dependsOn(tasks.named("classes")) + } + } + } +} + // Avoid race conditions between `dokkaJavadocCollector` and `dokkaJavadocJar` tasks tasks.named("dokkaJavadocCollector").configure { subprojects.flatMap { it.tasks } diff --git a/buildSrc/src/main/kotlin/com/openai/gradle/VerifyVersionSupportPolicyTask.kt b/buildSrc/src/main/kotlin/com/openai/gradle/VerifyVersionSupportPolicyTask.kt new file mode 100644 index 000000000..7066c649d --- /dev/null +++ b/buildSrc/src/main/kotlin/com/openai/gradle/VerifyVersionSupportPolicyTask.kt @@ -0,0 +1,80 @@ +package com.openai.gradle + +import java.io.DataInputStream +import org.gradle.api.DefaultTask +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.provider.Property +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.PathSensitivity +import org.gradle.api.tasks.TaskAction +import org.gradle.work.DisableCachingByDefault + +@DisableCachingByDefault(because = "This verification task has no outputs") +abstract class VerifyVersionSupportPolicyTask : DefaultTask() { + @get:Input abstract val artifactName: Property + + @get:Input abstract val runtimeFloor: Property + + @get:Input abstract val lifecycle: Property + + @get:Input abstract val requiredBuildJdk: Property + + @get:Input abstract val configuredBuildJdk: Property + + @get:Input abstract val sourceCompatibility: Property + + @get:Input abstract val targetCompatibility: Property + + @get:InputFiles + @get:PathSensitive(PathSensitivity.RELATIVE) + abstract val classFiles: ConfigurableFileCollection + + @TaskAction + fun verify() { + val name = artifactName.get() + val floor = runtimeFloor.get() + val allowedLifecycles = setOf("active", "maintenance", "deprecated", "eol") + + check(lifecycle.get() in allowedLifecycles) { + "$name must declare one of ${allowedLifecycles.sorted()}, but declared '${lifecycle.get()}'" + } + check(configuredBuildJdk.get() == requiredBuildJdk.get()) { + "$name must build with JDK ${requiredBuildJdk.get()}" + } + check(sourceCompatibility.get() == floor) { + "$name source compatibility must be Java $floor" + } + check(targetCompatibility.get() == floor) { + "$name target compatibility must be Java $floor" + } + + val maximumClassMajorVersion = 44 + floor + val bytecodeErrors = + classFiles.asFileTree + .matching { include("**/*.class") } + .mapNotNull { classFile -> + DataInputStream(classFile.inputStream().buffered()).use { input -> + check(input.readInt() == 0xCAFEBABE.toInt()) { + "$classFile is not a valid JVM class file" + } + input.readUnsignedShort() + val classMajorVersion = input.readUnsignedShort() + if (classMajorVersion > maximumClassMajorVersion) { + "$classFile uses class-file version $classMajorVersion; $name permits " + + "at most $maximumClassMajorVersion (Java $floor)" + } else { + null + } + } + } + + check(bytecodeErrors.isEmpty()) { + bytecodeErrors.joinToString( + separator = "\n- ", + prefix = "Published bytecode exceeds its declared runtime floor:\n- ", + ) + } + } +} diff --git a/docs/version-support-policy.md b/docs/version-support-policy.md new file mode 100644 index 000000000..e45dd869d --- /dev/null +++ b/docs/version-support-policy.md @@ -0,0 +1,87 @@ +# Java version support policy + +This policy governs the OpenAI Java SDK's supported JVMs, framework integrations, and +material runtime dependencies. Compatibility is declared per published artifact, not inferred +from the JDK used to build the repository. Dependency-specific decisions belong in short ADRs +that apply this policy. + +## Compatibility contract + +- Every artifact declares its runtime and bytecode/API floor and lifecycle state in + [`gradle/version-support.properties`](../gradle/version-support.properties). Framework + generations and tested ranges belong in the relevant artifact documentation or ADR. +- A newer build JDK must not silently raise the consumer runtime floor. +- The framework-neutral core evolves independently from optional integrations. An integration may + require a newer JVM only when it uses a separate, generation-specific artifact. + +## Admission and retirement + +- Do not add support for a runtime, framework, or major dependency generation with fewer than + **12 months of expected public OSS security maintenance** remaining. Commercial-only maintenance + does not count. +- An already-supported generation enters maintenance during its final 12 months: critical fixes and + migration help only, with no feature expansion. +- At upstream EOL, retire support or grant a documented grace period of **up to six months**. State + the exact end date and the limits of supporting software that no longer receives upstream fixes. +- After the grace period, the generation is OpenAI EOL. Old artifacts may remain downloadable, but + availability is not support. +- Security risk may shorten timelines. It does not turn a breaking change into a compatible one. + +## Framework and dependency ownership + +- Use separate coordinates for materially incompatible framework generations. Do not span them + with reflection or hidden runtime branching. +- The consuming application's framework platform or BOM is authoritative. Do not force isolated + framework components outside its supported dependency graph. +- OpenAI owns core implementation dependencies. Consumers own their application platform. + Optional integration dependencies are shared: OpenAI declares and tests the range; consumers + choose the version through their platform. + +## Release treatment + +| Change | Default release | +| --- | --- | +| Fix or dependency update within the supported contract | Patch | +| Add a new, explicit integration artifact | Minor | +| Move an existing generation to maintenance or deprecate it | Minor | +| Raise an artifact's JVM/API floor; change its framework generation; remove an artifact; or remove a transitive dependency consumers may rely on | Major | +| Urgent security-driven incompatible change | Major by default; security changes timing, not compatibility | + +## Required proof + +- **Every PR:** verify every published artifact's declared JVM floor and emitted bytecode; run a + consumer-facing smoke test on the minimum JVM and current LTS; enforce the JDK API floor; check + public API compatibility; and run the normal build, test, and dependency-compatibility suites. +- **Nightly and before release:** run the consumer smoke test on every supported LTS and use the + current non-LTS JDK as a forward-compatibility signal. +- **Quarterly and before GA:** review upstream lifecycles and revalidate the 12-month admission gate, + generated metadata, dependency ownership, and migration plan. +- Any material runtime, framework, or dependency-generation change requires an ADR covering + lifecycle evidence, affected artifacts, consumer resolution, migration, SemVer treatment, final + compatible release, and verification. + +## Current decisions + +| Surface | Decision | Next action | +| --- | --- | --- | +| Framework-neutral artifacts | Keep Java 8 through SDK v4. | Raise the floor only in an SDK major; first prefer replacing or isolating a constraining dependency. | +| Existing Spring Boot 2 starter | Legacy/OpenAI EOL; upstream EOL and the maximum grace period have elapsed. | Document final compatibility and migration in a Spring ADR; add no features. | +| Potential Spring Boot 4 integration | Candidate Java 17 integration using a new generation-specific artifact. | Pass the admission gate and Spring ADR before implementation. | + +Do not publish an OpenAI Java BOM until independently versioned artifacts or demonstrated consumer +resolution failures create a real alignment need. Add an optional integration only when it passes +the lifecycle gate, solves substantial repeated work for SDK users, and has a named owner and funded +compatibility matrix. + +## Supporting rationale + +- Direct AI SDK peers such as [Anthropic Java](https://github.com/anthropics/anthropic-sdk-java#requirements) + and [Google Gen AI Java](https://github.com/googleapis/java-genai/blob/main/pom.xml) retain a Java 8 + core floor. +- [Google Cloud Java](https://github.com/googleapis/java-datastore#supported-java-versions) treats + raising the JVM floor as a SemVer-major boundary. +- The [AWS SDK maintenance policy](https://docs.aws.amazon.com/sdkref/latest/guide/maint-policy.html) + demonstrates explicit, time-bounded support beyond an upstream dependency's EOL. +- [Jackson](https://github.com/FasterXML/jackson-databind#jdk) and + [Spring Boot](https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-4.0-Migration-Guide) + use major generations for JVM, package, namespace, and framework compatibility boundaries. diff --git a/gradle/version-support.properties b/gradle/version-support.properties new file mode 100644 index 000000000..dc1febd45 --- /dev/null +++ b/gradle/version-support.properties @@ -0,0 +1,20 @@ +# Repository toolchain. This may be newer than any published artifact's runtime floor. +build.jdk=21 + +# JVMs exercised by consumer-facing compatibility smoke tests. +test.minimum=8 +test.current-lts=25 +test.supported-lts=8,11,17,21,25 +test.current-non-lts=26 + +# Published artifacts must declare a runtime floor and lifecycle. +artifact.openai-java.runtime=8 +artifact.openai-java.lifecycle=active +artifact.openai-java-bedrock.runtime=8 +artifact.openai-java-bedrock.lifecycle=active +artifact.openai-java-client-okhttp.runtime=8 +artifact.openai-java-client-okhttp.lifecycle=active +artifact.openai-java-core.runtime=8 +artifact.openai-java-core.lifecycle=active +artifact.openai-java-spring-boot-starter.runtime=8 +artifact.openai-java-spring-boot-starter.lifecycle=eol diff --git a/openai-java-core/src/test/kotlin/com/openai/core/http/LoggingHttpClientTest.kt b/openai-java-core/src/test/kotlin/com/openai/core/http/LoggingHttpClientTest.kt index 0ca2eb9a2..8488c1be7 100644 --- a/openai-java-core/src/test/kotlin/com/openai/core/http/LoggingHttpClientTest.kt +++ b/openai-java-core/src/test/kotlin/com/openai/core/http/LoggingHttpClientTest.kt @@ -23,7 +23,6 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.api.parallel.ResourceLock import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.ValueSource -import org.slf4j.LoggerFactory @ResourceLock("stderr") internal class LoggingHttpClientTest { @@ -33,9 +32,6 @@ internal class LoggingHttpClientTest { @BeforeEach fun beforeEach() { - // Initialize SLF4J before redirecting stderr so its one-time diagnostics cannot - // contaminate these assertions under parallel test execution. - LoggerFactory.getILoggerFactory() originalErr = System.err errContent = ByteArrayOutputStream() System.setErr(PrintStream(errContent)) diff --git a/openai-java-runtime-compatibility/build.gradle.kts b/openai-java-runtime-compatibility/build.gradle.kts new file mode 100644 index 000000000..45df09d73 --- /dev/null +++ b/openai-java-runtime-compatibility/build.gradle.kts @@ -0,0 +1,31 @@ +plugins { + id("openai.java") +} + +dependencies { + implementation(project(":openai-java")) + implementation(project(":openai-java-bedrock")) + implementation(project(":openai-java-spring-boot-starter")) +} + +tasks.withType().configureEach { + // JDK 21 warns that Java 8 is an old source target; this fixture intentionally compiles at the + // SDK's minimum consumer level, and the repository otherwise promotes warnings to errors. + options.compilerArgs.add("-Xlint:-options") +} + +val runtimeJavaVersion = + providers.gradleProperty("runtimeJavaVersion").map(String::toInt).orElse(8) + +tasks.register("runRuntimeCompatibility") { + group = "verification" + description = "Loads representative public SDK entry points on the requested consumer JVM." + + classpath = sourceSets.main.get().runtimeClasspath + mainClass.set("com.openai.compatibility.RuntimeCompatibilitySmokeTest") + javaLauncher.set( + javaToolchains.launcherFor { + languageVersion.set(runtimeJavaVersion.map(JavaLanguageVersion::of)) + } + ) +} diff --git a/openai-java-runtime-compatibility/src/main/java/com/openai/compatibility/RuntimeCompatibilitySmokeTest.java b/openai-java-runtime-compatibility/src/main/java/com/openai/compatibility/RuntimeCompatibilitySmokeTest.java new file mode 100644 index 000000000..222f95b28 --- /dev/null +++ b/openai-java-runtime-compatibility/src/main/java/com/openai/compatibility/RuntimeCompatibilitySmokeTest.java @@ -0,0 +1,27 @@ +package com.openai.compatibility; + +import com.openai.client.OpenAIClient; +import com.openai.client.okhttp.BedrockOpenAIOkHttpClient; +import com.openai.client.okhttp.OpenAIOkHttpClient; +import com.openai.springboot.OpenAIClientAutoConfiguration; + +public final class RuntimeCompatibilitySmokeTest { + private RuntimeCompatibilitySmokeTest() {} + + public static void main(String[] args) { + Class[] publicEntryPoints = { + OpenAIClient.class, + OpenAIOkHttpClient.class, + BedrockOpenAIOkHttpClient.class, + OpenAIClientAutoConfiguration.class, + }; + + for (Class entryPoint : publicEntryPoints) { + entryPoint.getDeclaredMethods(); + } + + System.out.printf( + "Loaded %d SDK entry points on Java %s.%n", + publicEntryPoints.length, System.getProperty("java.version")); + } +} diff --git a/scripts/build b/scripts/build index 23d80780f..4471ee0c3 100755 --- a/scripts/build +++ b/scripts/build @@ -5,4 +5,4 @@ set -e cd "$(dirname "$0")/.." echo "==> Building classes" -./scripts/gradle build testClasses "$@" -x test +./scripts/gradle build testClasses verifyVersionSupportPolicy "$@" -x test diff --git a/scripts/version-support-matrix b/scripts/version-support-matrix new file mode 100755 index 000000000..3abffc0d8 --- /dev/null +++ b/scripts/version-support-matrix @@ -0,0 +1,34 @@ +#!/usr/bin/env bash + +set -euo pipefail + +cd "$(dirname "$0")/.." + +policy_file="gradle/version-support.properties" + +property() { + local name="$1" + awk -F= -v name="$name" '$1 == name { print $2 }' "$policy_file" +} + +case "${1:-}" in + pull-request) + versions="$(property test.minimum),$(property test.current-lts)" + ;; + nightly) + versions="$(property test.supported-lts),$(property test.current-non-lts)" + ;; + *) + echo "Usage: $0 {pull-request|nightly}" >&2 + exit 2 + ;; +esac + +printf '[' +separator="" +IFS=',' read -r -a version_list <<< "$versions" +for version in "${version_list[@]}"; do + printf '%s"%s"' "$separator" "$version" + separator="," +done +printf ']\n' From 2b2a9286fbb353afa09cfda9e51b6f31dc75bc06 Mon Sep 17 00:00:00 2001 From: Justin Beckwith Date: Mon, 27 Jul 2026 11:01:48 -0700 Subject: [PATCH 2/7] fix(ci): fail on invalid version support properties --- scripts/version-support-matrix | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/scripts/version-support-matrix b/scripts/version-support-matrix index 3abffc0d8..1fd703824 100755 --- a/scripts/version-support-matrix +++ b/scripts/version-support-matrix @@ -8,15 +8,40 @@ policy_file="gradle/version-support.properties" property() { local name="$1" - awk -F= -v name="$name" '$1 == name { print $2 }' "$policy_file" + local key + local value + + if [[ ! -r "$policy_file" ]]; then + echo "Cannot read version support policy at $policy_file" >&2 + return 1 + fi + + while IFS='=' read -r key value; do + [[ "$key" == "$name" ]] || continue + + if [[ -z "$value" ]]; then + echo "Required property '$name' in $policy_file must not be empty" >&2 + return 1 + fi + + printf '%s\n' "$value" + return 0 + done < "$policy_file" + + echo "Missing required property '$name' in $policy_file" >&2 + return 1 } case "${1:-}" in pull-request) - versions="$(property test.minimum),$(property test.current-lts)" + minimum="$(property test.minimum)" + current_lts="$(property test.current-lts)" + versions="$minimum,$current_lts" ;; nightly) - versions="$(property test.supported-lts),$(property test.current-non-lts)" + supported_lts="$(property test.supported-lts)" + current_non_lts="$(property test.current-non-lts)" + versions="$supported_lts,$current_non_lts" ;; *) echo "Usage: $0 {pull-request|nightly}" >&2 From 5e5a2b839df460dcb5921ed87acb1e8a0778f16c Mon Sep 17 00:00:00 2001 From: Justin Beckwith Date: Mon, 27 Jul 2026 11:42:05 -0700 Subject: [PATCH 3/7] fix(ci): gate releases on full runtime matrix --- .github/workflows/publish-sonatype-reusable.yml | 5 +++++ .github/workflows/runtime-compatibility.yml | 5 ++++- scripts/version-support-matrix | 4 ++-- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/.github/workflows/publish-sonatype-reusable.yml b/.github/workflows/publish-sonatype-reusable.yml index 96b4137ab..cffff41b8 100644 --- a/.github/workflows/publish-sonatype-reusable.yml +++ b/.github/workflows/publish-sonatype-reusable.yml @@ -11,8 +11,13 @@ concurrency: cancel-in-progress: false jobs: + runtime_compatibility: + name: Publish / runtime compatibility + uses: ./.github/workflows/runtime-compatibility.yml + publish: name: Publish / Sonatype + needs: runtime_compatibility runs-on: ubuntu-24.04 timeout-minutes: 60 environment: publish diff --git a/.github/workflows/runtime-compatibility.yml b/.github/workflows/runtime-compatibility.yml index cf25e61a5..1371dfa81 100644 --- a/.github/workflows/runtime-compatibility.yml +++ b/.github/workflows/runtime-compatibility.yml @@ -1,6 +1,9 @@ +# Exercises representative consumer entry points on every supported JVM. It runs nightly to catch +# ecosystem drift and is reusable so automated and manual publications are gated by the same matrix. name: Runtime compatibility on: + workflow_call: schedule: - cron: '0 7 * * *' workflow_dispatch: @@ -28,7 +31,7 @@ jobs: - name: Read version support policy id: matrix - run: echo "runtime-matrix=$(./scripts/version-support-matrix nightly)" >> "$GITHUB_OUTPUT" + run: echo "runtime-matrix=$(./scripts/version-support-matrix full)" >> "$GITHUB_OUTPUT" runtime_compatibility: name: Runtime compatibility / Java ${{ matrix.java }} diff --git a/scripts/version-support-matrix b/scripts/version-support-matrix index 1fd703824..6e5f054e6 100755 --- a/scripts/version-support-matrix +++ b/scripts/version-support-matrix @@ -38,13 +38,13 @@ case "${1:-}" in current_lts="$(property test.current-lts)" versions="$minimum,$current_lts" ;; - nightly) + full) supported_lts="$(property test.supported-lts)" current_non_lts="$(property test.current-non-lts)" versions="$supported_lts,$current_non_lts" ;; *) - echo "Usage: $0 {pull-request|nightly}" >&2 + echo "Usage: $0 {pull-request|full}" >&2 exit 2 ;; esac From 9f36ea1d40b3eb27318688ad805b7ea32ea5abcb Mon Sep 17 00:00:00 2001 From: Justin Beckwith Date: Mon, 27 Jul 2026 11:42:07 -0700 Subject: [PATCH 4/7] fix(test): prevent SLF4J stderr races --- openai-java-core/build.gradle.kts | 3 +++ .../test/kotlin/com/openai/core/http/LoggingHttpClientTest.kt | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/openai-java-core/build.gradle.kts b/openai-java-core/build.gradle.kts index f36399698..a1ee2daa7 100644 --- a/openai-java-core/build.gradle.kts +++ b/openai-java-core/build.gradle.kts @@ -95,6 +95,9 @@ dependencies { testImplementation("org.mockito:mockito-core:$mockitoVersion") testImplementation("org.mockito:mockito-junit-jupiter:$mockitoVersion") testImplementation("org.mockito.kotlin:mockito-kotlin:4.1.0") + testRuntimeOnly("org.slf4j:slf4j-nop:2.0.17") { + because("prevent SLF4J provider diagnostics from racing stderr assertions") + } mockitoAgent("org.mockito:mockito-core:$mockitoVersion") { isTransitive = false } } diff --git a/openai-java-core/src/test/kotlin/com/openai/core/http/LoggingHttpClientTest.kt b/openai-java-core/src/test/kotlin/com/openai/core/http/LoggingHttpClientTest.kt index 8488c1be7..0ca2eb9a2 100644 --- a/openai-java-core/src/test/kotlin/com/openai/core/http/LoggingHttpClientTest.kt +++ b/openai-java-core/src/test/kotlin/com/openai/core/http/LoggingHttpClientTest.kt @@ -23,6 +23,7 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.api.parallel.ResourceLock import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.ValueSource +import org.slf4j.LoggerFactory @ResourceLock("stderr") internal class LoggingHttpClientTest { @@ -32,6 +33,9 @@ internal class LoggingHttpClientTest { @BeforeEach fun beforeEach() { + // Initialize SLF4J before redirecting stderr so its one-time diagnostics cannot + // contaminate these assertions under parallel test execution. + LoggerFactory.getILoggerFactory() originalErr = System.err errContent = ByteArrayOutputStream() System.setErr(PrintStream(errContent)) From f2e11161c571b76097f42c492bfbb945aafd811a Mon Sep 17 00:00:00 2001 From: Justin Beckwith Date: Mon, 27 Jul 2026 12:31:25 -0700 Subject: [PATCH 5/7] fix(build): enforce artifact-specific Java policy --- .github/workflows/ci.yml | 13 +- .github/workflows/runtime-compatibility.yml | 13 +- CONTRIBUTING.md | 5 +- README.md | 16 +- build.gradle.kts | 59 ++++--- buildSrc/build.gradle.kts | 7 + .../GenerateVersionSupportMatrixTask.kt | 33 ++++ .../gradle/VerifyVersionSupportPolicyTask.kt | 20 +++ .../com/openai/gradle/VersionSupportPolicy.kt | 166 ++++++++++++++++++ .../src/main/kotlin/openai.java.gradle.kts | 13 +- .../src/main/kotlin/openai.kotlin.gradle.kts | 12 +- .../openai/gradle/VersionSupportPolicyTest.kt | 79 +++++++++ docs/spring-boot-2-eol.md | 42 +++++ docs/version-support-policy.md | 15 +- gradle/version-support.properties | 1 - .../build.gradle.kts | 97 ++++++++-- .../compatibility/BedrockRuntimeProbe.java | 39 ++++ .../compatibility/CoreRuntimeProbe.java | 35 ++++ .../compatibility/OkHttpRuntimeProbe.java | 22 +++ .../RuntimeCompatibilitySmokeTest.java | 27 --- scripts/version-support-matrix | 59 ------- 21 files changed, 626 insertions(+), 147 deletions(-) create mode 100644 buildSrc/src/main/kotlin/com/openai/gradle/GenerateVersionSupportMatrixTask.kt create mode 100644 buildSrc/src/main/kotlin/com/openai/gradle/VersionSupportPolicy.kt create mode 100644 buildSrc/src/test/kotlin/com/openai/gradle/VersionSupportPolicyTest.kt create mode 100644 docs/spring-boot-2-eol.md create mode 100644 openai-java-runtime-compatibility/src/main/java/com/openai/compatibility/BedrockRuntimeProbe.java create mode 100644 openai-java-runtime-compatibility/src/main/java/com/openai/compatibility/CoreRuntimeProbe.java create mode 100644 openai-java-runtime-compatibility/src/main/java/com/openai/compatibility/OkHttpRuntimeProbe.java delete mode 100644 openai-java-runtime-compatibility/src/main/java/com/openai/compatibility/RuntimeCompatibilitySmokeTest.java delete mode 100755 scripts/version-support-matrix diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8ebd817ab..55cef32ed 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -151,9 +151,20 @@ jobs: with: persist-credentials: false + - name: Set up Java + uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5 + with: + distribution: temurin + java-version: 21 + + - name: Set up Gradle + uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5.0.2 + - name: Read version support policy id: matrix - run: echo "runtime-matrix=$(./scripts/version-support-matrix pull-request)" >> "$GITHUB_OUTPUT" + run: | + ./scripts/gradle generateVersionSupportMatrix -PversionSupportMatrix=pull-request + echo "runtime-matrix=$(< build/version-support-matrix.json)" >> "$GITHUB_OUTPUT" runtime_compatibility: name: CI / runtime compatibility / Java ${{ matrix.java }} diff --git a/.github/workflows/runtime-compatibility.yml b/.github/workflows/runtime-compatibility.yml index 1371dfa81..5eca862c2 100644 --- a/.github/workflows/runtime-compatibility.yml +++ b/.github/workflows/runtime-compatibility.yml @@ -29,9 +29,20 @@ jobs: with: persist-credentials: false + - name: Set up Java + uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5 + with: + distribution: temurin + java-version: 21 + + - name: Set up Gradle + uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5.0.2 + - name: Read version support policy id: matrix - run: echo "runtime-matrix=$(./scripts/version-support-matrix full)" >> "$GITHUB_OUTPUT" + run: | + ./scripts/gradle generateVersionSupportMatrix -PversionSupportMatrix=full + echo "runtime-matrix=$(< build/version-support-matrix.json)" >> "$GITHUB_OUTPUT" runtime_compatibility: name: Runtime compatibility / Java ${{ matrix.java }} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 632057e3b..e3366f0bc 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -27,8 +27,9 @@ The SDK's primary artifacts are: - Provides the legacy Spring Boot 2 integration - Is OpenAI EOL and receives no new features -`openai-java-runtime-compatibility` is a non-published fixture that loads representative public -entry points on the JVMs declared in `gradle/version-support.properties`. +`openai-java-runtime-compatibility` is a non-published fixture that exercises representative +behavior for each supported artifact on an isolated runtime classpath. Its JVM matrix and each +artifact's eligibility on a given JVM come from `gradle/version-support.properties`. ## Modifying or adding code diff --git a/README.md b/README.md index 5152089d9..b10a7525b 100644 --- a/README.md +++ b/README.md @@ -1453,11 +1453,12 @@ GraalVM should automatically detect and use the published metadata, but [manual ## Spring Boot > [!WARNING] -> `openai-java-spring-boot-starter` targets Spring Boot 2.7, which is OpenAI EOL under the -> [Java version support policy](docs/version-support-policy.md). The artifact remains available for -> existing applications but does not receive new features. New Spring applications should depend -> on `openai-java` directly and provide an `OpenAIClient` bean until a supported, -> generation-specific integration is available. +> `openai-java-spring-boot-starter` targets Spring Boot 2.7 and is OpenAI EOL as of 2026-07-27. +> Version 4.45.0 is the final supported and tested release. The artifact remains downloadable but +> receives no fixes, testing, or compatibility support. See the +> [Spring Boot 2 EOL decision and migration path](docs/spring-boot-2-eol.md). New Spring +> applications should depend on `openai-java` directly and provide an `OpenAIClient` bean until a +> supported, generation-specific integration is available. Existing Spring Boot 2 applications can use the legacy starter to simplify configuration. @@ -1967,6 +1968,11 @@ This package generally follows [SemVer](https://semver.org/spec/v2.0.0.html) con 1. Changes to library internals which are technically public but not intended or documented for external use. _(Please open a GitHub issue to let us know if you are relying on such internals.)_ 2. Changes that we do not expect to impact the vast majority of users in practice. +Those exceptions do not apply to raising a supported JVM or JDK API floor, changing an integration's +framework generation, removing a published artifact, or removing a transitive dependency that +consumers may rely on; those changes require a major release. Announcing EOL without removing or +changing the last available artifact may be a minor release. + We take backwards-compatibility seriously and work hard to ensure you can rely on a smooth upgrade experience. We are keen for your feedback; please open an [issue](https://www.github.com/openai/openai-java/issues) with questions, bugs, or suggestions. diff --git a/build.gradle.kts b/build.gradle.kts index 9658f6536..d8f649df3 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,7 +1,10 @@ +import com.openai.gradle.GenerateVersionSupportMatrixTask +import com.openai.gradle.VersionSupportPolicy import com.openai.gradle.VerifyVersionSupportPolicyTask -import java.util.Properties import org.gradle.api.plugins.JavaPluginExtension import org.gradle.api.tasks.SourceSetContainer +import org.gradle.api.tasks.compile.JavaCompile +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile buildscript { dependencies { @@ -62,60 +65,70 @@ subprojects { } val versionSupportFile = layout.projectDirectory.file("gradle/version-support.properties") -val versionSupport = - Properties().apply { versionSupportFile.asFile.inputStream().use(::load) } -val artifactRuntimeFloors = - versionSupport - .stringPropertyNames() - .filter { it.startsWith("artifact.") && it.endsWith(".runtime") } - .associate { property -> - property.removePrefix("artifact.").removeSuffix(".runtime") to - versionSupport.getProperty(property).toInt() - } +val versionSupport = VersionSupportPolicy.load(versionSupportFile.asFile) + +tasks.register("generateVersionSupportMatrix") { + group = "verification" + description = "Generates a CI runtime matrix from the version support policy." + + policyFile.set(versionSupportFile) + mode.set(providers.gradleProperty("versionSupportMatrix")) + outputFile.set(layout.buildDirectory.file("version-support-matrix.json")) +} tasks.register("verifyVersionSupportPolicy") { group = "verification" description = "Verifies published artifact runtime floors and lifecycle declarations." inputs.file(versionSupportFile) - dependsOn(artifactRuntimeFloors.keys.map { ":$it:verifyVersionSupportPolicy" }) + dependsOn(versionSupport.artifacts.keys.map { ":$it:verifyVersionSupportPolicy" }) } subprojects { val artifactName = name pluginManager.withPlugin("openai.publish") { - val runtimeFloor = - artifactRuntimeFloors[artifactName] + val artifactSupport = + versionSupport.artifacts[artifactName] ?: error( "$artifactName is published but missing from ${versionSupportFile.asFile}" ) - val lifecycle = - versionSupport.getProperty("artifact.$artifactName.lifecycle") - ?: error( - "$artifactName is missing a lifecycle in ${versionSupportFile.asFile}" - ) pluginManager.withPlugin("java") { val java = extensions.getByType(JavaPluginExtension::class.java) val mainSourceSet = extensions.getByType(SourceSetContainer::class.java).getByName("main") + val compileJava = tasks.named("compileJava") - tasks.register("verifyVersionSupportPolicy") { + val verify = + tasks.register("verifyVersionSupportPolicy") { group = "verification" description = "Verifies $artifactName against its declared version support policy." this.artifactName.set(artifactName) - this.runtimeFloor.set(runtimeFloor) - this.lifecycle.set(lifecycle) - requiredBuildJdk.set(versionSupport.getProperty("build.jdk").toInt()) + runtimeFloor.set(artifactSupport.runtime) + lifecycle.set(artifactSupport.lifecycle.propertyValue) + requiredBuildJdk.set(versionSupport.buildJdk) configuredBuildJdk.set(java.toolchain.languageVersion.map { it.asInt() }) sourceCompatibility.set(java.sourceCompatibility.majorVersion.toInt()) targetCompatibility.set(java.targetCompatibility.majorVersion.toInt()) + javaRelease.set(compileJava.flatMap { it.options.release }) classFiles.from(mainSourceSet.output.classesDirs) dependsOn(tasks.named("classes")) } + + pluginManager.withPlugin("org.jetbrains.kotlin.jvm") { + val compileKotlin = tasks.named("compileKotlin") + verify.configure { + kotlinJvmTarget.set( + compileKotlin.flatMap { it.compilerOptions.jvmTarget }.map { it.target } + ) + kotlinFreeCompilerArgs.set( + compileKotlin.flatMap { it.compilerOptions.freeCompilerArgs } + ) + } + } } } } diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index c6dc92ec5..b0b098c84 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -12,4 +12,11 @@ repositories { dependencies { implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.20") implementation("com.vanniktech:gradle-maven-publish-plugin:0.28.0") + + testImplementation(kotlin("test")) + testImplementation("org.junit.jupiter:junit-jupiter:5.9.3") +} + +tasks.test { + useJUnitPlatform() } diff --git a/buildSrc/src/main/kotlin/com/openai/gradle/GenerateVersionSupportMatrixTask.kt b/buildSrc/src/main/kotlin/com/openai/gradle/GenerateVersionSupportMatrixTask.kt new file mode 100644 index 000000000..8596c16e0 --- /dev/null +++ b/buildSrc/src/main/kotlin/com/openai/gradle/GenerateVersionSupportMatrixTask.kt @@ -0,0 +1,33 @@ +package com.openai.gradle + +import org.gradle.api.DefaultTask +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.CacheableTask +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.PathSensitivity +import org.gradle.api.tasks.TaskAction + +@CacheableTask +abstract class GenerateVersionSupportMatrixTask : DefaultTask() { + @get:InputFile + @get:PathSensitive(PathSensitivity.RELATIVE) + abstract val policyFile: RegularFileProperty + + @get:Input abstract val mode: Property + + @get:OutputFile abstract val outputFile: RegularFileProperty + + @TaskAction + fun generate() { + val versions = VersionSupportPolicy.load(policyFile.get().asFile).runtimeMatrix(mode.get()) + val json = versions.joinToString(separator = "\",\"", prefix = "[\"", postfix = "\"]") + outputFile.get().asFile.apply { + parentFile.mkdirs() + writeText("$json\n") + } + } +} diff --git a/buildSrc/src/main/kotlin/com/openai/gradle/VerifyVersionSupportPolicyTask.kt b/buildSrc/src/main/kotlin/com/openai/gradle/VerifyVersionSupportPolicyTask.kt index 7066c649d..f02dbfd5b 100644 --- a/buildSrc/src/main/kotlin/com/openai/gradle/VerifyVersionSupportPolicyTask.kt +++ b/buildSrc/src/main/kotlin/com/openai/gradle/VerifyVersionSupportPolicyTask.kt @@ -3,6 +3,7 @@ package com.openai.gradle import java.io.DataInputStream import org.gradle.api.DefaultTask import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.provider.ListProperty import org.gradle.api.provider.Property import org.gradle.api.tasks.Input import org.gradle.api.tasks.InputFiles @@ -27,6 +28,12 @@ abstract class VerifyVersionSupportPolicyTask : DefaultTask() { @get:Input abstract val targetCompatibility: Property + @get:Input abstract val javaRelease: Property + + @get:Input abstract val kotlinJvmTarget: Property + + @get:Input abstract val kotlinFreeCompilerArgs: ListProperty + @get:InputFiles @get:PathSensitive(PathSensitivity.RELATIVE) abstract val classFiles: ConfigurableFileCollection @@ -49,6 +56,19 @@ abstract class VerifyVersionSupportPolicyTask : DefaultTask() { check(targetCompatibility.get() == floor) { "$name target compatibility must be Java $floor" } + check(javaRelease.get() == floor) { + "$name Java compiler must enforce --release $floor" + } + + val expectedKotlinTarget = if (floor == 8) "1.8" else floor.toString() + check(kotlinJvmTarget.get() == expectedKotlinTarget) { + "$name Kotlin JVM target must be $expectedKotlinTarget" + } + val jdkReleaseArguments = + kotlinFreeCompilerArgs.get().filter { it.startsWith("-Xjdk-release=") } + check(jdkReleaseArguments == listOf("-Xjdk-release=$expectedKotlinTarget")) { + "$name Kotlin compiler must enforce exactly -Xjdk-release=$expectedKotlinTarget" + } val maximumClassMajorVersion = 44 + floor val bytecodeErrors = diff --git a/buildSrc/src/main/kotlin/com/openai/gradle/VersionSupportPolicy.kt b/buildSrc/src/main/kotlin/com/openai/gradle/VersionSupportPolicy.kt new file mode 100644 index 000000000..af990380a --- /dev/null +++ b/buildSrc/src/main/kotlin/com/openai/gradle/VersionSupportPolicy.kt @@ -0,0 +1,166 @@ +package com.openai.gradle + +import java.io.File +import java.io.Serializable +import java.util.Properties + +enum class ArtifactLifecycle(val propertyValue: String) { + ACTIVE("active"), + MAINTENANCE("maintenance"), + DEPRECATED("deprecated"), + EOL("eol"); + + val isSupported: Boolean + get() = this != EOL + + companion object { + fun parse(value: String, property: String): ArtifactLifecycle = + values().find { it.propertyValue == value } + ?: error( + "$property must be one of ${values().map { it.propertyValue }}, but was '$value'" + ) + } +} + +data class ArtifactSupport( + val runtime: Int, + val lifecycle: ArtifactLifecycle, +) : Serializable + +data class VersionSupportPolicy( + val buildJdk: Int, + val currentLts: Int, + val supportedLts: List, + val currentNonLts: Int, + val artifacts: Map, +) : Serializable { + private val supportedArtifacts = artifacts.filterValues { it.lifecycle.isSupported } + val minimumSupportedRuntime: Int = supportedArtifacts.values.minOf { it.runtime } + + fun runtimeFloor(projectName: String): Int = + artifacts[projectName]?.runtime ?: minimumSupportedRuntime + + fun runtimeMatrix(mode: String): List { + val supportedArtifactFloors = + supportedArtifacts.values.map(ArtifactSupport::runtime) + return when (mode) { + "pull-request" -> supportedArtifactFloors + currentLts + "full" -> supportedLts + currentNonLts + else -> + error( + "Unknown version support matrix '$mode'; expected 'pull-request' or 'full'" + ) + } + .distinct() + .sorted() + } + + companion object { + private val requiredKeys = + setOf( + "build.jdk", + "test.current-lts", + "test.supported-lts", + "test.current-non-lts", + ) + private val artifactKey = Regex("""artifact\.(.+)\.(runtime|lifecycle)""") + + fun load(file: File): VersionSupportPolicy { + check(file.isFile) { "Version support policy does not exist: $file" } + + val properties = Properties().apply { file.inputStream().use(::load) } + val unknownKeys = + properties.stringPropertyNames().filterNot { key -> + key in requiredKeys || artifactKey.matches(key) + } + check(unknownKeys.isEmpty()) { + "Unknown version support properties in $file: ${unknownKeys.sorted()}" + } + + val missingKeys = requiredKeys.filterNot(properties::containsKey) + check(missingKeys.isEmpty()) { + "Missing version support properties in $file: ${missingKeys.sorted()}" + } + + val artifactNames = + properties + .stringPropertyNames() + .mapNotNull { artifactKey.matchEntire(it)?.groupValues?.get(1) } + .toSortedSet() + check(artifactNames.isNotEmpty()) { "No artifact policies declared in $file" } + + val artifacts = + artifactNames.associateWith { name -> + val runtime = properties.requiredInt("artifact.$name.runtime", file) + val lifecycleProperty = "artifact.$name.lifecycle" + val lifecycle = + ArtifactLifecycle.parse( + properties.required(lifecycleProperty, file), + lifecycleProperty, + ) + ArtifactSupport(runtime = runtime, lifecycle = lifecycle) + } + check(artifacts.values.any { it.lifecycle.isSupported }) { + "At least one artifact in $file must have a supported lifecycle" + } + + val buildJdk = properties.requiredInt("build.jdk", file) + val currentLts = properties.requiredInt("test.current-lts", file) + val supportedLts = + properties + .required("test.supported-lts", file) + .split(',') + .map { value -> + value.trim().toIntOrNull() + ?: error("test.supported-lts contains invalid JVM '$value' in $file") + } + val currentNonLts = properties.requiredInt("test.current-non-lts", file) + + check(buildJdk > 0) { "build.jdk must be positive" } + check(supportedLts.isNotEmpty() && supportedLts.all { it > 0 }) { + "test.supported-lts must contain positive JVM versions" + } + check(supportedLts == supportedLts.distinct().sorted()) { + "test.supported-lts must contain unique JVMs in ascending order" + } + check(currentLts in supportedLts) { + "test.current-lts ($currentLts) must appear in test.supported-lts" + } + check(currentNonLts > currentLts) { + "test.current-non-lts ($currentNonLts) must be newer than current LTS $currentLts" + } + artifacts.forEach { (name, support) -> + check(support.runtime > 0) { "$name must declare a positive runtime floor" } + check(support.runtime <= buildJdk) { + "$name requires Java ${support.runtime}, newer than build JDK $buildJdk" + } + check(!support.lifecycle.isSupported || support.runtime in supportedLts) { + "$name has supported runtime floor ${support.runtime}, which is absent from " + + "test.supported-lts" + } + } + + return VersionSupportPolicy( + buildJdk = buildJdk, + currentLts = currentLts, + supportedLts = supportedLts, + currentNonLts = currentNonLts, + artifacts = artifacts, + ) + } + + private fun Properties.required( + key: String, + file: File, + ): String = + getProperty(key)?.trim()?.takeIf(String::isNotEmpty) + ?: error("Required version support property '$key' is missing or empty in $file") + + private fun Properties.requiredInt( + key: String, + file: File, + ): Int = + required(key, file).toIntOrNull() + ?: error("Version support property '$key' must be an integer in $file") + } +} diff --git a/buildSrc/src/main/kotlin/openai.java.gradle.kts b/buildSrc/src/main/kotlin/openai.java.gradle.kts index e64ff271c..38e72eab6 100644 --- a/buildSrc/src/main/kotlin/openai.java.gradle.kts +++ b/buildSrc/src/main/kotlin/openai.java.gradle.kts @@ -1,3 +1,4 @@ +import com.openai.gradle.VersionSupportPolicy import org.gradle.api.tasks.testing.logging.TestExceptionFormat plugins { @@ -8,18 +9,22 @@ repositories { mavenCentral() } +val versionSupportPolicy = + VersionSupportPolicy.load(rootProject.file("gradle/version-support.properties")) +val runtimeFloor = versionSupportPolicy.runtimeFloor(project.name) + java { toolchain { - languageVersion.set(JavaLanguageVersion.of(21)) + languageVersion.set(JavaLanguageVersion.of(versionSupportPolicy.buildJdk)) } - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = JavaVersion.toVersion(runtimeFloor) + targetCompatibility = JavaVersion.toVersion(runtimeFloor) } tasks.withType().configureEach { options.compilerArgs.add("-Werror") - options.release.set(8) + options.release.set(runtimeFloor) } tasks.named("jar") { diff --git a/buildSrc/src/main/kotlin/openai.kotlin.gradle.kts b/buildSrc/src/main/kotlin/openai.kotlin.gradle.kts index 37850353a..33c864220 100644 --- a/buildSrc/src/main/kotlin/openai.kotlin.gradle.kts +++ b/buildSrc/src/main/kotlin/openai.kotlin.gradle.kts @@ -1,3 +1,4 @@ +import com.openai.gradle.VersionSupportPolicy import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.dsl.KotlinVersion @@ -10,20 +11,25 @@ repositories { mavenCentral() } +val versionSupportPolicy = + VersionSupportPolicy.load(rootProject.file("gradle/version-support.properties")) +val runtimeFloor = versionSupportPolicy.runtimeFloor(project.name) +val kotlinJvmTarget = if (runtimeFloor == 8) "1.8" else runtimeFloor.toString() + kotlin { jvmToolchain { - languageVersion.set(JavaLanguageVersion.of(21)) + languageVersion.set(JavaLanguageVersion.of(versionSupportPolicy.buildJdk)) } compilerOptions { freeCompilerArgs = listOf( "-Xjvm-default=all", - "-Xjdk-release=1.8", + "-Xjdk-release=$kotlinJvmTarget", // Suppress deprecation warnings because we may still reference and test deprecated members. // TODO: Replace with `-Xsuppress-warning=DEPRECATION` once we use Kotlin compiler 2.1.0+. "-nowarn", ) - jvmTarget.set(JvmTarget.JVM_1_8) + jvmTarget.set(JvmTarget.fromTarget(kotlinJvmTarget)) languageVersion.set(KotlinVersion.KOTLIN_1_8) apiVersion.set(KotlinVersion.KOTLIN_1_8) coreLibrariesVersion = "1.8.0" diff --git a/buildSrc/src/test/kotlin/com/openai/gradle/VersionSupportPolicyTest.kt b/buildSrc/src/test/kotlin/com/openai/gradle/VersionSupportPolicyTest.kt new file mode 100644 index 000000000..500d7ed3e --- /dev/null +++ b/buildSrc/src/test/kotlin/com/openai/gradle/VersionSupportPolicyTest.kt @@ -0,0 +1,79 @@ +package com.openai.gradle + +import java.nio.file.Path +import kotlin.io.path.writeText +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertTrue +import org.junit.jupiter.api.io.TempDir + +class VersionSupportPolicyTest { + @TempDir lateinit var temporaryDirectory: Path + + @Test + fun `matrices include supported artifact floors and exclude EOL floors`() { + val policy = + loadPolicy( + """ + build.jdk=21 + test.current-lts=25 + test.supported-lts=8,11,17,21,25 + test.current-non-lts=26 + artifact.java-8.runtime=8 + artifact.java-8.lifecycle=active + artifact.java-17.runtime=17 + artifact.java-17.lifecycle=maintenance + artifact.eol.runtime=11 + artifact.eol.lifecycle=eol + """ + ) + + assertEquals(listOf(8, 17, 25), policy.runtimeMatrix("pull-request")) + assertEquals(listOf(8, 11, 17, 21, 25, 26), policy.runtimeMatrix("full")) + } + + @Test + fun `Java properties syntax has one validated interpretation`() { + val policy = + loadPolicy( + """ + build.jdk : 21 + test.current-lts = 25 + test.supported-lts = 8,11,17,21,\ + 25 + test.current-non-lts = 26 + artifact.core.runtime = 8 + artifact.core.lifecycle = active + """ + ) + + assertEquals(listOf(8, 11, 17, 21, 25), policy.supportedLts) + } + + @Test + fun `unknown properties fail closed`() { + val error = + assertFailsWith { + loadPolicy( + """ + build.jdk=21 + test.current-lts=25 + test.supported-lts=8,11,17,21,25 + test.current-non-lts=26 + test.minimum=8 + artifact.core.runtime=8 + artifact.core.lifecycle=active + """ + ) + } + + assertTrue(error.message.orEmpty().contains("Unknown version support properties")) + } + + private fun loadPolicy(contents: String): VersionSupportPolicy { + val policyFile = temporaryDirectory.resolve("version-support.properties") + policyFile.writeText(contents.trimIndent()) + return VersionSupportPolicy.load(policyFile.toFile()) + } +} diff --git a/docs/spring-boot-2-eol.md b/docs/spring-boot-2-eol.md new file mode 100644 index 000000000..f6b1ad9aa --- /dev/null +++ b/docs/spring-boot-2-eol.md @@ -0,0 +1,42 @@ +# Spring Boot 2 starter end-of-life + +**Status:** Accepted 2026-07-27 + +## Decision + +`com.openai:openai-java-spring-boot-starter` targets Spring Boot 2.7 and is OpenAI EOL effective +2026-07-27. Version 4.45.0 is its final supported and tested OpenAI SDK release. The coordinate and +old releases may remain downloadable, but later availability does not imply fixes, testing, or +compatibility support. + +Spring ended public OSS support for Boot 2 with +[2.7.18 on 2023-11-23](https://spring.io/blog/2023/11/23/spring-boot-2-7-18-available-now/). +The policy's maximum six-month grace period therefore ended 2024-05-23. Commercial maintenance does +not satisfy the repository's OSS-support admission rule, and the upstream gap is already much +longer than the permitted grace period. + +Declaring the unchanged starter EOL is a minor release. Removing the artifact, raising its Java +floor, or changing the same coordinate to a newer Spring generation would require a SemVer major. +A future supported Spring integration must use generation-specific coordinates and independently +pass the policy's 12-month admission gate. + +## Consumer migration + +Applications that cannot leave Boot 2 may pin the starter to 4.45.0, but do so without OpenAI or +upstream OSS security support. The preferred path is: + +1. Follow Spring's [Boot 3 migration guide](https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-3.0-Migration-Guide): + upgrade to the latest 2.7 release first, move to Java 17 and Spring Framework 6, and migrate + affected Java EE imports to Jakarta EE. +2. Remove `openai-java-spring-boot-starter`. +3. Depend on `com.openai:openai-java` and provide a normal application-owned `OpenAIClient` bean. + +```java +@Bean +OpenAIClient openAIClient() { + return OpenAIOkHttpClient.fromEnv(); +} +``` + +This keeps Spring's dependency platform authoritative and avoids pretending that a Boot 2 +auto-configuration artifact supports Boot 3 or later. diff --git a/docs/version-support-policy.md b/docs/version-support-policy.md index e45dd869d..00cf52cd3 100644 --- a/docs/version-support-policy.md +++ b/docs/version-support-policy.md @@ -43,17 +43,18 @@ that apply this policy. | --- | --- | | Fix or dependency update within the supported contract | Patch | | Add a new, explicit integration artifact | Minor | -| Move an existing generation to maintenance or deprecate it | Minor | +| Move an existing generation to maintenance, deprecate it, or declare it EOL while leaving its last release available | Minor | | Raise an artifact's JVM/API floor; change its framework generation; remove an artifact; or remove a transitive dependency consumers may rely on | Major | | Urgent security-driven incompatible change | Major by default; security changes timing, not compatibility | ## Required proof -- **Every PR:** verify every published artifact's declared JVM floor and emitted bytecode; run a - consumer-facing smoke test on the minimum JVM and current LTS; enforce the JDK API floor; check - public API compatibility; and run the normal build, test, and dependency-compatibility suites. -- **Nightly and before release:** run the consumer smoke test on every supported LTS and use the - current non-LTS JDK as a forward-compatibility signal. +- **Every PR:** verify every published artifact's declared JVM floor, compiler API floor, and + emitted bytecode; run each supported artifact's isolated consumer smoke test on its runtime floor + and the current LTS; check public API compatibility; and run the normal build, test, and + dependency-compatibility suites. +- **Nightly and before release:** run every supported artifact's isolated consumer smoke test on + every compatible supported LTS and use the current non-LTS JDK as a forward-compatibility signal. - **Quarterly and before GA:** review upstream lifecycles and revalidate the 12-month admission gate, generated metadata, dependency ownership, and migration plan. - Any material runtime, framework, or dependency-generation change requires an ADR covering @@ -65,7 +66,7 @@ that apply this policy. | Surface | Decision | Next action | | --- | --- | --- | | Framework-neutral artifacts | Keep Java 8 through SDK v4. | Raise the floor only in an SDK major; first prefer replacing or isolating a constraining dependency. | -| Existing Spring Boot 2 starter | Legacy/OpenAI EOL; upstream EOL and the maximum grace period have elapsed. | Document final compatibility and migration in a Spring ADR; add no features. | +| Existing Spring Boot 2 starter | OpenAI EOL as of 2026-07-27; 4.45.0 is the final supported release. | Follow the [Spring Boot 2 EOL decision](spring-boot-2-eol.md); add no features or compatibility claims. | | Potential Spring Boot 4 integration | Candidate Java 17 integration using a new generation-specific artifact. | Pass the admission gate and Spring ADR before implementation. | Do not publish an OpenAI Java BOM until independently versioned artifacts or demonstrated consumer diff --git a/gradle/version-support.properties b/gradle/version-support.properties index dc1febd45..4c55d7516 100644 --- a/gradle/version-support.properties +++ b/gradle/version-support.properties @@ -2,7 +2,6 @@ build.jdk=21 # JVMs exercised by consumer-facing compatibility smoke tests. -test.minimum=8 test.current-lts=25 test.supported-lts=8,11,17,21,25 test.current-non-lts=26 diff --git a/openai-java-runtime-compatibility/build.gradle.kts b/openai-java-runtime-compatibility/build.gradle.kts index 45df09d73..60adf7099 100644 --- a/openai-java-runtime-compatibility/build.gradle.kts +++ b/openai-java-runtime-compatibility/build.gradle.kts @@ -1,31 +1,100 @@ +import com.openai.gradle.VersionSupportPolicy +import org.gradle.api.attributes.Bundling +import org.gradle.api.attributes.Category +import org.gradle.api.attributes.LibraryElements +import org.gradle.api.attributes.Usage +import org.gradle.api.attributes.java.TargetJvmVersion + plugins { id("openai.java") } +val versionSupportPolicy = + VersionSupportPolicy.load(rootProject.file("gradle/version-support.properties")) +val runtimeProbes = + mapOf( + "openai-java-core" to "com.openai.compatibility.CoreRuntimeProbe", + "openai-java-client-okhttp" to "com.openai.compatibility.OkHttpRuntimeProbe", + "openai-java" to "com.openai.compatibility.OkHttpRuntimeProbe", + "openai-java-bedrock" to "com.openai.compatibility.BedrockRuntimeProbe", + ) +val supportedArtifacts = + versionSupportPolicy.artifacts.filterValues { it.lifecycle.isSupported }.keys + +check(runtimeProbes.keys == supportedArtifacts) { + "Runtime probes must exactly cover supported artifacts. " + + "Missing: ${supportedArtifacts - runtimeProbes.keys}; " + + "unexpected: ${runtimeProbes.keys - supportedArtifacts}" +} + dependencies { - implementation(project(":openai-java")) - implementation(project(":openai-java-bedrock")) - implementation(project(":openai-java-spring-boot-starter")) + runtimeProbes.keys.forEach { compileOnly(project(":$it")) } } tasks.withType().configureEach { - // JDK 21 warns that Java 8 is an old source target; this fixture intentionally compiles at the - // SDK's minimum consumer level, and the repository otherwise promotes warnings to errors. + // This fixture intentionally compiles at the SDK's minimum consumer level. options.compilerArgs.add("-Xlint:-options") } val runtimeJavaVersion = - providers.gradleProperty("runtimeJavaVersion").map(String::toInt).orElse(8) + providers + .gradleProperty("runtimeJavaVersion") + .map(String::toInt) + .orElse(versionSupportPolicy.minimumSupportedRuntime) +val runtimeLauncher = + javaToolchains.launcherFor { + languageVersion.set(runtimeJavaVersion.map(JavaLanguageVersion::of)) + } -tasks.register("runRuntimeCompatibility") { - group = "verification" - description = "Loads representative public SDK entry points on the requested consumer JVM." +fun String.toTaskSegment(): String = + split('-').joinToString("") { segment -> + segment.replaceFirstChar { firstCharacter -> firstCharacter.uppercase() } + } - classpath = sourceSets.main.get().runtimeClasspath - mainClass.set("com.openai.compatibility.RuntimeCompatibilitySmokeTest") - javaLauncher.set( - javaToolchains.launcherFor { - languageVersion.set(runtimeJavaVersion.map(JavaLanguageVersion::of)) +val artifactProbeTasks = + runtimeProbes.mapValues { (artifactName, probeMainClass) -> + val artifactSupport = versionSupportPolicy.artifacts.getValue(artifactName) + val runtimeClasspath = + configurations.create("${artifactName.toTaskSegment()}RuntimeCompatibilityClasspath") { + isCanBeConsumed = false + isCanBeResolved = true + attributes { + attribute(Bundling.BUNDLING_ATTRIBUTE, objects.named(Bundling.EXTERNAL)) + attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category.LIBRARY)) + attribute( + LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, + objects.named(LibraryElements.JAR), + ) + attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage.JAVA_RUNTIME)) + attribute( + TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, + artifactSupport.runtime, + ) + } + } + dependencies.add(runtimeClasspath.name, project(":$artifactName")) + + tasks.register("run${artifactName.toTaskSegment()}RuntimeCompatibility") { + group = "verification" + description = + "Exercises $artifactName on its isolated consumer runtime classpath." + + classpath = sourceSets.main.get().output + runtimeClasspath + mainClass.set(probeMainClass) + javaLauncher.set(runtimeLauncher) } + } + +tasks.register("runRuntimeCompatibility") { + group = "verification" + description = "Exercises every supported artifact compatible with the requested consumer JVM." + + dependsOn( + artifactProbeTasks + .filterKeys { artifactName -> + versionSupportPolicy.artifacts.getValue(artifactName).runtime <= + runtimeJavaVersion.get() + } + .values ) } diff --git a/openai-java-runtime-compatibility/src/main/java/com/openai/compatibility/BedrockRuntimeProbe.java b/openai-java-runtime-compatibility/src/main/java/com/openai/compatibility/BedrockRuntimeProbe.java new file mode 100644 index 000000000..18428f799 --- /dev/null +++ b/openai-java-runtime-compatibility/src/main/java/com/openai/compatibility/BedrockRuntimeProbe.java @@ -0,0 +1,39 @@ +package com.openai.compatibility; + +import com.openai.client.OpenAIClient; +import com.openai.client.okhttp.BedrockOpenAIOkHttpClient; + +public final class BedrockRuntimeProbe { + private static final String[] RUNTIME_PROVIDER_CLASSES = { + "software.amazon.awssdk.services.sts.StsClient", + "software.amazon.awssdk.services.sso.SsoClient", + "software.amazon.awssdk.services.ssooidc.SsoOidcClient", + "software.amazon.awssdk.services.signin.SigninClient", + "software.amazon.awssdk.http.urlconnection.UrlConnectionHttpClient", + }; + + private BedrockRuntimeProbe() {} + + public static void main(String[] args) throws ClassNotFoundException { + for (String providerClass : RUNTIME_PROVIDER_CLASSES) { + Class.forName(providerClass); + } + + OpenAIClient client = BedrockOpenAIOkHttpClient.builder() + .awsRegion("us-east-1") + .baseUrl("https://example.com/openai/v1") + .skipAuth(true) + .build(); + try { + if (client.models() == null) { + throw new IllegalStateException("Bedrock client did not create its model service"); + } + } finally { + client.close(); + } + + System.out.printf( + "Exercised openai-java-bedrock and %d runtime providers on Java %s.%n", + RUNTIME_PROVIDER_CLASSES.length, System.getProperty("java.version")); + } +} diff --git a/openai-java-runtime-compatibility/src/main/java/com/openai/compatibility/CoreRuntimeProbe.java b/openai-java-runtime-compatibility/src/main/java/com/openai/compatibility/CoreRuntimeProbe.java new file mode 100644 index 000000000..99c74d920 --- /dev/null +++ b/openai-java-runtime-compatibility/src/main/java/com/openai/compatibility/CoreRuntimeProbe.java @@ -0,0 +1,35 @@ +package com.openai.compatibility; + +import com.openai.core.JsonValue; +import com.openai.models.chat.completions.ChatCompletionCreateParams; +import com.openai.models.chat.completions.StructuredChatCompletionCreateParams; +import java.util.Collections; +import java.util.Map; + +public final class CoreRuntimeProbe { + private CoreRuntimeProbe() {} + + public static void main(String[] args) { + Map input = Collections.singletonMap("probe", "runtime"); + Map converted = JsonValue.from(input).convert(Map.class); + + if (converted == null || !"runtime".equals(converted.get("probe"))) { + throw new IllegalStateException("Core JSON round trip failed"); + } + + StructuredChatCompletionCreateParams params = ChatCompletionCreateParams.builder() + .addUserMessage("runtime probe") + .model("runtime-probe") + .responseFormat(RuntimeResponse.class) + .build(); + if (!params.rawParams().responseFormat().isPresent()) { + throw new IllegalStateException("Core structured-output schema generation failed"); + } + + System.out.printf("Exercised openai-java-core on Java %s.%n", System.getProperty("java.version")); + } + + public static final class RuntimeResponse { + public String value; + } +} diff --git a/openai-java-runtime-compatibility/src/main/java/com/openai/compatibility/OkHttpRuntimeProbe.java b/openai-java-runtime-compatibility/src/main/java/com/openai/compatibility/OkHttpRuntimeProbe.java new file mode 100644 index 000000000..a586a6347 --- /dev/null +++ b/openai-java-runtime-compatibility/src/main/java/com/openai/compatibility/OkHttpRuntimeProbe.java @@ -0,0 +1,22 @@ +package com.openai.compatibility; + +import com.openai.client.OpenAIClient; +import com.openai.client.okhttp.OpenAIOkHttpClient; + +public final class OkHttpRuntimeProbe { + private OkHttpRuntimeProbe() {} + + public static void main(String[] args) { + OpenAIClient client = + OpenAIOkHttpClient.builder().apiKey("runtime-probe").build(); + try { + if (client.models() == null) { + throw new IllegalStateException("OkHttp client did not create its model service"); + } + } finally { + client.close(); + } + + System.out.printf("Exercised an OkHttp SDK client on Java %s.%n", System.getProperty("java.version")); + } +} diff --git a/openai-java-runtime-compatibility/src/main/java/com/openai/compatibility/RuntimeCompatibilitySmokeTest.java b/openai-java-runtime-compatibility/src/main/java/com/openai/compatibility/RuntimeCompatibilitySmokeTest.java deleted file mode 100644 index 222f95b28..000000000 --- a/openai-java-runtime-compatibility/src/main/java/com/openai/compatibility/RuntimeCompatibilitySmokeTest.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.openai.compatibility; - -import com.openai.client.OpenAIClient; -import com.openai.client.okhttp.BedrockOpenAIOkHttpClient; -import com.openai.client.okhttp.OpenAIOkHttpClient; -import com.openai.springboot.OpenAIClientAutoConfiguration; - -public final class RuntimeCompatibilitySmokeTest { - private RuntimeCompatibilitySmokeTest() {} - - public static void main(String[] args) { - Class[] publicEntryPoints = { - OpenAIClient.class, - OpenAIOkHttpClient.class, - BedrockOpenAIOkHttpClient.class, - OpenAIClientAutoConfiguration.class, - }; - - for (Class entryPoint : publicEntryPoints) { - entryPoint.getDeclaredMethods(); - } - - System.out.printf( - "Loaded %d SDK entry points on Java %s.%n", - publicEntryPoints.length, System.getProperty("java.version")); - } -} diff --git a/scripts/version-support-matrix b/scripts/version-support-matrix deleted file mode 100755 index 6e5f054e6..000000000 --- a/scripts/version-support-matrix +++ /dev/null @@ -1,59 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -cd "$(dirname "$0")/.." - -policy_file="gradle/version-support.properties" - -property() { - local name="$1" - local key - local value - - if [[ ! -r "$policy_file" ]]; then - echo "Cannot read version support policy at $policy_file" >&2 - return 1 - fi - - while IFS='=' read -r key value; do - [[ "$key" == "$name" ]] || continue - - if [[ -z "$value" ]]; then - echo "Required property '$name' in $policy_file must not be empty" >&2 - return 1 - fi - - printf '%s\n' "$value" - return 0 - done < "$policy_file" - - echo "Missing required property '$name' in $policy_file" >&2 - return 1 -} - -case "${1:-}" in - pull-request) - minimum="$(property test.minimum)" - current_lts="$(property test.current-lts)" - versions="$minimum,$current_lts" - ;; - full) - supported_lts="$(property test.supported-lts)" - current_non_lts="$(property test.current-non-lts)" - versions="$supported_lts,$current_non_lts" - ;; - *) - echo "Usage: $0 {pull-request|full}" >&2 - exit 2 - ;; -esac - -printf '[' -separator="" -IFS=',' read -r -a version_list <<< "$versions" -for version in "${version_list[@]}"; do - printf '%s"%s"' "$separator" "$version" - separator="," -done -printf ']\n' From d0b1a1ed3c0c2088367ac2157da08e859add049e Mon Sep 17 00:00:00 2001 From: Justin Beckwith Date: Mon, 27 Jul 2026 13:19:53 -0700 Subject: [PATCH 6/7] fix(release): freeze Spring Boot 2 starter --- CONTRIBUTING.md | 2 +- README.md | 8 ++------ build.gradle.kts | 11 ++++++----- docs/spring-boot-2-eol.md | 6 +++--- docs/version-support-policy.md | 2 +- openai-java-spring-boot-starter/build.gradle.kts | 1 - 6 files changed, 13 insertions(+), 17 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e3366f0bc..860bef5da 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -25,7 +25,7 @@ The SDK's primary artifacts are: - Adds optional Amazon Bedrock authentication and credential-provider integration - `openai-java-spring-boot-starter` - Provides the legacy Spring Boot 2 integration - - Is OpenAI EOL and receives no new features + - Is retained to verify the final 4.45.0 release, but is OpenAI EOL and no longer published `openai-java-runtime-compatibility` is a non-published fixture that exercises representative behavior for each supported artifact on an isolated runtime classpath. Its JVM matrix and each diff --git a/README.md b/README.md index b10a7525b..75dc6621b 100644 --- a/README.md +++ b/README.md @@ -1454,8 +1454,8 @@ GraalVM should automatically detect and use the published metadata, but [manual > [!WARNING] > `openai-java-spring-boot-starter` targets Spring Boot 2.7 and is OpenAI EOL as of 2026-07-27. -> Version 4.45.0 is the final supported and tested release. The artifact remains downloadable but -> receives no fixes, testing, or compatibility support. See the +> Version 4.45.0 is the final supported, tested, and published release. The artifact remains +> downloadable but receives no fixes, testing, or compatibility support. See the > [Spring Boot 2 EOL decision and migration path](docs/spring-boot-2-eol.md). New Spring > applications should depend on `openai-java` directly and provide an `OpenAIClient` bean until a > supported, generation-specific integration is available. @@ -1464,8 +1464,6 @@ Existing Spring Boot 2 applications can use the legacy starter to simplify confi ### Installation - - #### Gradle ```kotlin @@ -1482,8 +1480,6 @@ implementation("com.openai:openai-java-spring-boot-starter:4.45.0") ``` - - ### Configuration The [client's environment variable options](#client-configuration) can be configured in [`application.properties` or `application.yml`](https://docs.spring.io/spring-boot/how-to/properties-and-configuration.html). diff --git a/build.gradle.kts b/build.gradle.kts index d8f649df3..78d111df4 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -86,14 +86,15 @@ tasks.register("verifyVersionSupportPolicy") { subprojects { val artifactName = name + val artifactSupport = versionSupport.artifacts[artifactName] pluginManager.withPlugin("openai.publish") { - val artifactSupport = - versionSupport.artifacts[artifactName] - ?: error( - "$artifactName is published but missing from ${versionSupportFile.asFile}" - ) + checkNotNull(artifactSupport) { + "$artifactName is published but missing from ${versionSupportFile.asFile}" + } + } + if (artifactSupport != null) { pluginManager.withPlugin("java") { val java = extensions.getByType(JavaPluginExtension::class.java) val mainSourceSet = extensions.getByType(SourceSetContainer::class.java).getByName("main") diff --git a/docs/spring-boot-2-eol.md b/docs/spring-boot-2-eol.md index f6b1ad9aa..6b3325a6b 100644 --- a/docs/spring-boot-2-eol.md +++ b/docs/spring-boot-2-eol.md @@ -5,9 +5,9 @@ ## Decision `com.openai:openai-java-spring-boot-starter` targets Spring Boot 2.7 and is OpenAI EOL effective -2026-07-27. Version 4.45.0 is its final supported and tested OpenAI SDK release. The coordinate and -old releases may remain downloadable, but later availability does not imply fixes, testing, or -compatibility support. +2026-07-27. Version 4.45.0 is its final supported, tested, and published OpenAI SDK release. That +release remains downloadable, but no later starter versions will be published and it receives no +fixes, testing, or compatibility support. Spring ended public OSS support for Boot 2 with [2.7.18 on 2023-11-23](https://spring.io/blog/2023/11/23/spring-boot-2-7-18-available-now/). diff --git a/docs/version-support-policy.md b/docs/version-support-policy.md index 00cf52cd3..fc17644f1 100644 --- a/docs/version-support-policy.md +++ b/docs/version-support-policy.md @@ -66,7 +66,7 @@ that apply this policy. | Surface | Decision | Next action | | --- | --- | --- | | Framework-neutral artifacts | Keep Java 8 through SDK v4. | Raise the floor only in an SDK major; first prefer replacing or isolating a constraining dependency. | -| Existing Spring Boot 2 starter | OpenAI EOL as of 2026-07-27; 4.45.0 is the final supported release. | Follow the [Spring Boot 2 EOL decision](spring-boot-2-eol.md); add no features or compatibility claims. | +| Existing Spring Boot 2 starter | OpenAI EOL as of 2026-07-27; 4.45.0 is the final supported and published release. | Follow the [Spring Boot 2 EOL decision](spring-boot-2-eol.md); publish no later versions. | | Potential Spring Boot 4 integration | Candidate Java 17 integration using a new generation-specific artifact. | Pass the admission gate and Spring ADR before implementation. | Do not publish an OpenAI Java BOM until independently versioned artifacts or demonstrated consumer diff --git a/openai-java-spring-boot-starter/build.gradle.kts b/openai-java-spring-boot-starter/build.gradle.kts index c49a36abe..6b68e7ecb 100644 --- a/openai-java-spring-boot-starter/build.gradle.kts +++ b/openai-java-spring-boot-starter/build.gradle.kts @@ -1,6 +1,5 @@ plugins { id("openai.kotlin") - id("openai.publish") } repositories { From 531eb7cb2ec42a1faf1df85eb76c736602d55d6d Mon Sep 17 00:00:00 2001 From: Justin Beckwith Date: Mon, 27 Jul 2026 13:20:00 -0700 Subject: [PATCH 7/7] test(build): run policy tests in CI --- scripts/build | 3 +++ 1 file changed, 3 insertions(+) diff --git a/scripts/build b/scripts/build index 4471ee0c3..edb4c1b68 100755 --- a/scripts/build +++ b/scripts/build @@ -4,5 +4,8 @@ set -e cd "$(dirname "$0")/.." +echo "==> Testing build logic" +./scripts/gradle :buildSrc:test "$@" + echo "==> Building classes" ./scripts/gradle build testClasses verifyVersionSupportPolicy "$@" -x test