Skip to content

Commit 9082055

Browse files
feat(client): ensure compat with proguard
1 parent 88056ab commit 9082055

File tree

6 files changed

+338
-0
lines changed

6 files changed

+338
-0
lines changed

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1304,6 +1304,12 @@ Or to `debug` for more verbose logging:
13041304
$ export OPENAI_LOG=debug
13051305
```
13061306

1307+
## ProGuard and R8
1308+
1309+
Although the SDK uses reflection, it is still usable with [ProGuard](https://github.com/Guardsquare/proguard) and [R8](https://developer.android.com/topic/performance/app-optimization/enable-app-optimization) because `openai-java-core` is published with a [configuration file](openai-java-core/src/main/resources/META-INF/proguard/openai-java-core.pro) containing [keep rules](https://www.guardsquare.com/manual/configuration/usage).
1310+
1311+
ProGuard and R8 should automatically detect and use the published rules, but you can also manually copy the keep rules if necessary.
1312+
13071313
## GraalVM
13081314

13091315
Although the SDK uses reflection, it is still usable in [GraalVM](https://www.graalvm.org) because `openai-java-core` is published with [reachability metadata](https://www.graalvm.org/latest/reference-manual/native-image/metadata/).
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# Jackson uses reflection and depends heavily on runtime attributes.
2+
-keepattributes
3+
4+
# Jackson uses Kotlin reflection utilities, which themselves use reflection to access things.
5+
-keep class kotlin.reflect.** { *; }
6+
-keep class kotlin.Metadata { *; }
7+
8+
# Jackson uses reflection to access enum members (e.g. via `java.lang.Class.getEnumConstants()`).
9+
-keepclassmembers class com.fasterxml.jackson.** extends java.lang.Enum {
10+
<fields>;
11+
public static **[] values();
12+
public static ** valueOf(java.lang.String);
13+
}
14+
15+
# Jackson uses reflection to access annotation members.
16+
-keepclassmembers @interface com.fasterxml.jackson.annotation.** {
17+
*;
18+
}
19+
20+
# Jackson uses reflection to access the default constructors of serializers and deserializers.
21+
-keepclassmembers class * extends com.openai.core.BaseSerializer {
22+
<init>();
23+
}
24+
-keepclassmembers class * extends com.openai.core.BaseDeserializer {
25+
<init>();
26+
}
27+
28+
# Jackson uses reflection to serialize and deserialize our classes based on their constructors and annotated members.
29+
-keepclassmembers class com.openai.** {
30+
<init>(...);
31+
@com.fasterxml.jackson.annotation.* *;
32+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
plugins {
2+
id("openai.kotlin")
3+
id("com.gradleup.shadow") version "8.3.8"
4+
}
5+
6+
buildscript {
7+
dependencies {
8+
classpath("com.guardsquare:proguard-gradle:7.4.2")
9+
}
10+
}
11+
12+
dependencies {
13+
testImplementation(project(":openai-java"))
14+
testImplementation(kotlin("test"))
15+
testImplementation("org.junit.jupiter:junit-jupiter-api:5.9.3")
16+
testImplementation("org.assertj:assertj-core:3.25.3")
17+
testImplementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.4")
18+
testImplementation("org.junit.platform:junit-platform-console:1.10.1")
19+
}
20+
21+
tasks.shadowJar {
22+
from(sourceSets.test.get().output)
23+
configurations = listOf(project.configurations.testRuntimeClasspath.get())
24+
}
25+
26+
val proguardJarPath = "${layout.buildDirectory.get()}/libs/${project.name}-${project.version}-proguard.jar"
27+
val proguardJar by tasks.registering(proguard.gradle.ProGuardTask::class) {
28+
group = "verification"
29+
dependsOn(tasks.shadowJar)
30+
notCompatibleWithConfigurationCache("ProGuard")
31+
32+
injars(tasks.shadowJar)
33+
outjars(proguardJarPath)
34+
printmapping("${layout.buildDirectory.get()}/proguard-mapping.txt")
35+
36+
verbose()
37+
dontwarn()
38+
39+
val javaHome = System.getProperty("java.home")
40+
if (System.getProperty("java.version").startsWith("1.")) {
41+
// Before Java 9, the runtime classes were packaged in a single jar file.
42+
libraryjars("$javaHome/lib/rt.jar")
43+
} else {
44+
// As of Java 9, the runtime classes are packaged in modular jmod files.
45+
libraryjars(
46+
// Filters must be specified first, as a map.
47+
mapOf("jarfilter" to "!**.jar", "filter" to "!module-info.class"),
48+
"$javaHome/jmods/java.base.jmod"
49+
)
50+
}
51+
52+
configuration("./test.pro")
53+
configuration("../openai-java-core/src/main/resources/META-INF/proguard/openai-java-core.pro")
54+
}
55+
56+
val testProGuard by tasks.registering(JavaExec::class) {
57+
group = "verification"
58+
dependsOn(proguardJar)
59+
notCompatibleWithConfigurationCache("ProGuard")
60+
61+
mainClass.set("org.junit.platform.console.ConsoleLauncher")
62+
classpath = files(proguardJarPath)
63+
args = listOf(
64+
"--classpath", proguardJarPath,
65+
"--scan-classpath",
66+
"--details", "verbose",
67+
)
68+
}
69+
70+
tasks.test {
71+
dependsOn(testProGuard)
72+
// We defer to the tests run via the ProGuard JAR.
73+
enabled = false
74+
}
Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
// File generated from our OpenAPI spec by Stainless.
2+
3+
package com.openai.proguard
4+
5+
import com.fasterxml.jackson.module.kotlin.jacksonTypeRef
6+
import com.openai.client.okhttp.OpenAIOkHttpClient
7+
import com.openai.core.jsonMapper
8+
import com.openai.models.chat.completions.ChatCompletion
9+
import com.openai.models.chat.completions.ChatCompletionAudio
10+
import com.openai.models.chat.completions.ChatCompletionContentPart
11+
import com.openai.models.chat.completions.ChatCompletionContentPartText
12+
import com.openai.models.chat.completions.ChatCompletionMessage
13+
import com.openai.models.chat.completions.ChatCompletionMessageToolCall
14+
import com.openai.models.chat.completions.ChatCompletionModality
15+
import com.openai.models.chat.completions.ChatCompletionTokenLogprob
16+
import com.openai.models.completions.CompletionUsage
17+
import org.assertj.core.api.Assertions.assertThat
18+
import org.junit.jupiter.api.BeforeAll
19+
import org.junit.jupiter.api.Test
20+
21+
internal class ProGuardCompatibilityTest {
22+
23+
companion object {
24+
25+
@BeforeAll
26+
@JvmStatic
27+
fun setUp() {
28+
// To debug that we're using the right JAR.
29+
val jarPath = this::class.java.getProtectionDomain().codeSource.location
30+
println("JAR being used: $jarPath")
31+
}
32+
}
33+
34+
@Test
35+
fun proguardRules() {
36+
val rulesFile =
37+
javaClass.classLoader.getResourceAsStream("META-INF/proguard/openai-java-core.pro")
38+
39+
assertThat(rulesFile).isNotNull()
40+
}
41+
42+
@Test
43+
fun client() {
44+
val client = OpenAIOkHttpClient.builder().apiKey("My API Key").build()
45+
46+
assertThat(client).isNotNull()
47+
assertThat(client.completions()).isNotNull()
48+
assertThat(client.chat()).isNotNull()
49+
assertThat(client.embeddings()).isNotNull()
50+
assertThat(client.files()).isNotNull()
51+
assertThat(client.images()).isNotNull()
52+
assertThat(client.audio()).isNotNull()
53+
assertThat(client.moderations()).isNotNull()
54+
assertThat(client.models()).isNotNull()
55+
assertThat(client.fineTuning()).isNotNull()
56+
assertThat(client.graders()).isNotNull()
57+
assertThat(client.vectorStores()).isNotNull()
58+
assertThat(client.webhooks()).isNotNull()
59+
assertThat(client.beta()).isNotNull()
60+
assertThat(client.batches()).isNotNull()
61+
assertThat(client.uploads()).isNotNull()
62+
assertThat(client.responses()).isNotNull()
63+
assertThat(client.evals()).isNotNull()
64+
assertThat(client.containers()).isNotNull()
65+
}
66+
67+
@Test
68+
fun chatCompletionRoundtrip() {
69+
val jsonMapper = jsonMapper()
70+
val chatCompletion =
71+
ChatCompletion.builder()
72+
.id("id")
73+
.addChoice(
74+
ChatCompletion.Choice.builder()
75+
.finishReason(ChatCompletion.Choice.FinishReason.STOP)
76+
.index(0L)
77+
.logprobs(
78+
ChatCompletion.Choice.Logprobs.builder()
79+
.addContent(
80+
ChatCompletionTokenLogprob.builder()
81+
.token("token")
82+
.addByte(0L)
83+
.logprob(0.0)
84+
.addTopLogprob(
85+
ChatCompletionTokenLogprob.TopLogprob.builder()
86+
.token("token")
87+
.addByte(0L)
88+
.logprob(0.0)
89+
.build()
90+
)
91+
.build()
92+
)
93+
.addRefusal(
94+
ChatCompletionTokenLogprob.builder()
95+
.token("token")
96+
.addByte(0L)
97+
.logprob(0.0)
98+
.addTopLogprob(
99+
ChatCompletionTokenLogprob.TopLogprob.builder()
100+
.token("token")
101+
.addByte(0L)
102+
.logprob(0.0)
103+
.build()
104+
)
105+
.build()
106+
)
107+
.build()
108+
)
109+
.message(
110+
ChatCompletionMessage.builder()
111+
.content("content")
112+
.refusal("refusal")
113+
.addAnnotation(
114+
ChatCompletionMessage.Annotation.builder()
115+
.urlCitation(
116+
ChatCompletionMessage.Annotation.UrlCitation.builder()
117+
.endIndex(0L)
118+
.startIndex(0L)
119+
.title("title")
120+
.url("url")
121+
.build()
122+
)
123+
.build()
124+
)
125+
.audio(
126+
ChatCompletionAudio.builder()
127+
.id("id")
128+
.data("data")
129+
.expiresAt(0L)
130+
.transcript("transcript")
131+
.build()
132+
)
133+
.functionCall(
134+
ChatCompletionMessage.FunctionCall.builder()
135+
.arguments("arguments")
136+
.name("name")
137+
.build()
138+
)
139+
.addToolCall(
140+
ChatCompletionMessageToolCall.builder()
141+
.id("id")
142+
.function(
143+
ChatCompletionMessageToolCall.Function.builder()
144+
.arguments("arguments")
145+
.name("name")
146+
.build()
147+
)
148+
.build()
149+
)
150+
.build()
151+
)
152+
.build()
153+
)
154+
.created(0L)
155+
.model("model")
156+
.serviceTier(ChatCompletion.ServiceTier.AUTO)
157+
.systemFingerprint("system_fingerprint")
158+
.usage(
159+
CompletionUsage.builder()
160+
.completionTokens(0L)
161+
.promptTokens(0L)
162+
.totalTokens(0L)
163+
.completionTokensDetails(
164+
CompletionUsage.CompletionTokensDetails.builder()
165+
.acceptedPredictionTokens(0L)
166+
.audioTokens(0L)
167+
.reasoningTokens(0L)
168+
.rejectedPredictionTokens(0L)
169+
.build()
170+
)
171+
.promptTokensDetails(
172+
CompletionUsage.PromptTokensDetails.builder()
173+
.audioTokens(0L)
174+
.cachedTokens(0L)
175+
.build()
176+
)
177+
.build()
178+
)
179+
.build()
180+
181+
val roundtrippedChatCompletion =
182+
jsonMapper.readValue(
183+
jsonMapper.writeValueAsString(chatCompletion),
184+
jacksonTypeRef<ChatCompletion>(),
185+
)
186+
187+
assertThat(roundtrippedChatCompletion).isEqualTo(chatCompletion)
188+
}
189+
190+
@Test
191+
fun chatCompletionContentPartRoundtrip() {
192+
val jsonMapper = jsonMapper()
193+
val chatCompletionContentPart =
194+
ChatCompletionContentPart.ofText(
195+
ChatCompletionContentPartText.builder().text("text").build()
196+
)
197+
198+
val roundtrippedChatCompletionContentPart =
199+
jsonMapper.readValue(
200+
jsonMapper.writeValueAsString(chatCompletionContentPart),
201+
jacksonTypeRef<ChatCompletionContentPart>(),
202+
)
203+
204+
assertThat(roundtrippedChatCompletionContentPart).isEqualTo(chatCompletionContentPart)
205+
}
206+
207+
@Test
208+
fun chatCompletionModalityRoundtrip() {
209+
val jsonMapper = jsonMapper()
210+
val chatCompletionModality = ChatCompletionModality.TEXT
211+
212+
val roundtrippedChatCompletionModality =
213+
jsonMapper.readValue(
214+
jsonMapper.writeValueAsString(chatCompletionModality),
215+
jacksonTypeRef<ChatCompletionModality>(),
216+
)
217+
218+
assertThat(roundtrippedChatCompletionModality).isEqualTo(chatCompletionModality)
219+
}
220+
}

openai-java-proguard-test/test.pro

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Specify the entrypoint where ProGuard starts to determine what's reachable.
2+
-keep class com.openai.proguard.** { *; }
3+
4+
# For the testing framework.
5+
-keep class org.junit.** { *; }

settings.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@ include("openai-java")
44
include("openai-java-client-okhttp")
55
include("openai-java-core")
66
include("openai-java-spring-boot-starter")
7+
include("openai-java-proguard-test")
78
include("openai-java-example")

0 commit comments

Comments
 (0)