Skip to content

DO NOT MERGE! Only intended for showing changes since 2025/01/16 generated code! #1

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 9 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .github/workflows/setup-gradle.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
name: Build

on:
push:

jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@v4
- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: 17
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
- name: Build with Gradle
run: ./gradlew build
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@

.gradle/
.idea/
lib/build/

.DS_Store
local.properties
23 changes: 15 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,22 +32,29 @@ I prefer to save a snapshot of the specification that was used for the generatio
## Generate Whole OpenAI API
1. `time openapi-generator generate -i openapi.yaml -g kotlin -o ./lib --skip-validate-spec --additional-properties=artifactId=openai-kotlin-client,artifactVersion=0.0.1,groupId=com.openai,packageName=com.openai`
(< 5 seconds on MacBook Pro M4 Pro)
...
2. cd ./lib
3. `chmod +x ./gradlew`
4. `./gradlew build`
(< 20 seconds on MacBook Pro M4 Pro to successfully compile)
2. `cp lib/build.gradle .`
3. `mv lib/gradle* lib/settings.gradle .`
4. `echo -e "\ninclude(\":lib\")" >> settings.gradle`
5. Edit `build.gradle` to be a project file and `lib/build.gradle` to be a module file.
6. `chmod +x ./gradlew`
7. `./gradlew build`
(< 20 seconds on MacBook Pro M4 Pro to [eventually; see "Changes"] successfully compile from clean)

All of this is also shown in the `openai-kotlin-client.sh` file.

## Changes
At this point, the build will actually fail.
I had to make a few changes in order to get it to compile.
I had to make some manual changes in order to get it to compile successfully:
1. AudioApi.kt:
1. change `AudioResponseFormat? = json` to `AudioResponseFormat? = AudioResponseFormat.json`
2. change `timestampGranularities?.value` to `timestampGranularities`
2. replace a couple of `data class Foo() {}` with `class Foo`
2. remove `data` (`data class ...`->`class ...`) in:
1. CreateAssistantRequestToolResourcesFileSearch.kt
2. CreateThreadRequestToolResourcesFileSearch.kt

This just got it to **COMPILE** successfully!

I had to make further changes in order to get it to **RUN** successfully.

I had to make further changes in order to get it to run correctly.
All of my changes can be seen at:
https://github.com/swooby/openai-openapi-kotlin/pull/1/files
1 change: 1 addition & 0 deletions gradle.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
org.gradle.daemon=false
14 changes: 14 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[versions]
kotlin = "2.0.21"
kotlintestRunnerJunit5 = "3.4.2"
squareupMoshiKotlin = "1.15.1"
squareupOkhttpBom = "4.12.0"

[libraries]
kotlintest-runner-junit5 = { module = "io.kotlintest:kotlintest-runner-junit5", version.ref = "kotlintestRunnerJunit5" }
squareup-moshi-kotlin = { module = "com.squareup.moshi:moshi-kotlin", version.ref = "squareupMoshiKotlin" }
squareup-okhttp3-bom = { module = "com.squareup.okhttp3:okhttp-bom", version.ref = "squareupOkhttpBom" }
squareup-okhttp3 = { module = "com.squareup.okhttp3:okhttp" }

[plugins]
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
#Sun Dec 15 17:37:56 PST 2024
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-all.zip
networkTimeout=10000
validateDistributionUrl=true
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
0 lib/gradlew → gradlew
100644 → 100755
File renamed without changes.
File renamed without changes.
62 changes: 0 additions & 62 deletions lib/build.gradle

This file was deleted.

32 changes: 32 additions & 0 deletions lib/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompilationTask

plugins {
alias(libs.plugins.kotlin.jvm)
// id("maven-publish")
}

group = "com.openai"
version = "0.0.1"

tasks.test {
useJUnitPlatform()
}

tasks.named<KotlinCompilationTask<*>>("compileTestKotlin").configure {
compilerOptions {
suppressWarnings = true
}
}

tasks.named<KotlinCompilationTask<*>>("compileKotlin").configure {
compilerOptions {
suppressWarnings = true
}
}

dependencies {
implementation(libs.squareup.moshi.kotlin)
implementation(platform(libs.squareup.okhttp3.bom))
implementation(libs.squareup.okhttp3)
testImplementation(libs.kotlintest.runner.junit5)
}
1 change: 0 additions & 1 deletion lib/settings.gradle

This file was deleted.

6 changes: 3 additions & 3 deletions lib/src/main/kotlin/com/openai/apis/AudioApi.kt
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ class AudioApi(basePath: kotlin.String = defaultBasePath, client: Call.Factory =
*/
@Suppress("UNCHECKED_CAST")
@Throws(IllegalStateException::class, IOException::class, UnsupportedOperationException::class, ClientException::class, ServerException::class)
fun createTranscription(file: java.io.File, model: CreateTranscriptionRequestModel, language: kotlin.String? = null, prompt: kotlin.String? = null, responseFormat: AudioResponseFormat? = json, temperature: java.math.BigDecimal? = java.math.BigDecimal("0"), timestampGranularities: kotlin.collections.List<TimestampGranularitiesCreateTranscription>? = null) : CreateTranscription200Response {
fun createTranscription(file: java.io.File, model: CreateTranscriptionRequestModel, language: kotlin.String? = null, prompt: kotlin.String? = null, responseFormat: AudioResponseFormat? = AudioResponseFormat.json, temperature: java.math.BigDecimal? = java.math.BigDecimal("0"), timestampGranularities: kotlin.collections.List<TimestampGranularitiesCreateTranscription>? = null) : CreateTranscription200Response {
val localVarResponse = createTranscriptionWithHttpInfo(file = file, model = model, language = language, prompt = prompt, responseFormat = responseFormat, temperature = temperature, timestampGranularities = timestampGranularities)

return when (localVarResponse.responseType) {
Expand Down Expand Up @@ -219,7 +219,7 @@ class AudioApi(basePath: kotlin.String = defaultBasePath, client: Call.Factory =
"prompt" to PartConfig(body = prompt, headers = mutableMapOf()),
"response_format" to PartConfig(body = responseFormat, headers = mutableMapOf()),
"temperature" to PartConfig(body = temperature, headers = mutableMapOf()),
"timestamp_granularities[]" to PartConfig(body = timestampGranularities?.value, headers = mutableMapOf()),)
"timestamp_granularities[]" to PartConfig(body = timestampGranularities, headers = mutableMapOf()),)
val localVariableQuery: MultiValueMap = mutableMapOf()
val localVariableHeaders: MutableMap<String, String> = mutableMapOf("Content-Type" to "multipart/form-data")
localVariableHeaders["Accept"] = "application/json"
Expand Down Expand Up @@ -251,7 +251,7 @@ class AudioApi(basePath: kotlin.String = defaultBasePath, client: Call.Factory =
*/
@Suppress("UNCHECKED_CAST")
@Throws(IllegalStateException::class, IOException::class, UnsupportedOperationException::class, ClientException::class, ServerException::class)
fun createTranslation(file: java.io.File, model: CreateTranscriptionRequestModel, prompt: kotlin.String? = null, responseFormat: AudioResponseFormat? = json, temperature: java.math.BigDecimal? = java.math.BigDecimal("0")) : CreateTranslation200Response {
fun createTranslation(file: java.io.File, model: CreateTranscriptionRequestModel, prompt: kotlin.String? = null, responseFormat: AudioResponseFormat? = AudioResponseFormat.json, temperature: java.math.BigDecimal? = java.math.BigDecimal("0")) : CreateTranslation200Response {
val localVarResponse = createTranslationWithHttpInfo(file = file, model = model, prompt = prompt, responseFormat = responseFormat, temperature = temperature)

return when (localVarResponse.responseType) {
Expand Down
44 changes: 25 additions & 19 deletions lib/src/main/kotlin/com/openai/infrastructure/ApiClient.kt
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,16 @@ val EMPTY_REQUEST: RequestBody = ByteArray(0).toRequestBody()

open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClient) {
companion object {
protected const val ContentType: String = "Content-Type"
protected const val Accept: String = "Accept"
protected const val Authorization: String = "Authorization"
protected const val JsonMediaType: String = "application/json"
protected const val FormDataMediaType: String = "multipart/form-data"
protected const val FormUrlEncMediaType: String = "application/x-www-form-urlencoded"
protected const val XmlMediaType: String = "application/xml"
protected const val OctetMediaType: String = "application/octet-stream"
const val ContentType: String = "Content-Type"
const val Accept: String = "Accept"
const val Authorization: String = "Authorization"
const val JsonMediaType: String = "application/json"
const val SdpMediaType: String = "application/sdp"
const val TextPlainMediaType: String = "text/plain"
const val FormDataMediaType: String = "multipart/form-data"
const val FormUrlEncMediaType: String = "application/x-www-form-urlencoded"
const val XmlMediaType: String = "application/xml"
const val OctetMediaType: String = "application/octet-stream"

val apiKey: MutableMap<String, String> = mutableMapOf()
val apiKeyPrefix: MutableMap<String, String> = mutableMapOf()
Expand All @@ -64,7 +66,7 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie
* @param byteArray The given file
* @return The guessed Content-Type
*/
protected fun guessContentTypeFromByteArray(byteArray: ByteArray): String {
fun guessContentTypeFromByteArray(byteArray: ByteArray): String {
val contentType = try {
URLConnection.guessContentTypeFromStream(byteArray.inputStream())
} catch (io: IOException) {
Expand All @@ -79,12 +81,12 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie
* @param file The given file
* @return The guessed Content-Type
*/
protected fun guessContentTypeFromFile(file: File): String {
fun guessContentTypeFromFile(file: File): String {
val contentType = URLConnection.guessContentTypeFromName(file.name)
return contentType ?: "application/octet-stream"
}

protected inline fun <reified T> requestBody(content: T, mediaType: String?): RequestBody =
inline fun <reified T> requestBody(content: T, mediaType: String?): RequestBody =
when {
content is ByteArray -> content.toRequestBody((mediaType ?: guessContentTypeFromByteArray(content)).toMediaTypeOrNull())
content is File -> content.asRequestBody((mediaType ?: guessContentTypeFromFile(content)).toMediaTypeOrNull())
Expand Down Expand Up @@ -126,18 +128,21 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie
if (content == null) {
EMPTY_REQUEST
} else {
Serializer.moshi.adapter(T::class.java).toJson(content)
Serializer.serialize<T>(content)
.toRequestBody((mediaType ?: JsonMediaType).toMediaTypeOrNull())
}
mediaType.startsWith("application/") && mediaType.endsWith("sdp") ->
content?.toString()?.toByteArray()?.toRequestBody(mediaType.toMediaTypeOrNull())
?: EMPTY_REQUEST
mediaType == XmlMediaType -> throw UnsupportedOperationException("xml not currently supported.")
mediaType == OctetMediaType && content is ByteArray ->
content.toRequestBody(OctetMediaType.toMediaTypeOrNull())
// TODO: this should be extended with other serializers
else -> throw UnsupportedOperationException("requestBody currently only supports JSON body, byte body and File body.")
else -> throw UnsupportedOperationException("requestBody currently only supports JSON body, SDP body, byte body, and File body.")
}

@OptIn(ExperimentalStdlibApi::class)
protected inline fun <reified T: Any?> responseBody(response: Response, mediaType: String? = JsonMediaType): T? {
inline fun <reified T: Any?> responseBody(response: Response, mediaType: String? = JsonMediaType): T? {
val body = response.body
if(body == null) {
return null
Expand Down Expand Up @@ -201,22 +206,23 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie
if (bodyContent.isEmpty()) {
return null
}
Serializer.moshi.adapter<T>().fromJson(bodyContent)
Serializer.deserialize<T>(bodyContent)
}
mediaType.startsWith("text/") && mediaType.endsWith("plain") -> body.string() as T
mediaType == OctetMediaType -> body.bytes() as? T
else -> throw UnsupportedOperationException("responseBody currently only supports JSON body.")
else -> throw UnsupportedOperationException("responseBody currently only supports JSON body, byte body, and text body.")
}
}

protected fun <T> updateAuthParams(requestConfig: RequestConfig<T>) {
fun <T> updateAuthParams(requestConfig: RequestConfig<T>) {
if (requestConfig.headers[Authorization].isNullOrEmpty()) {
accessToken?.let { accessToken ->
requestConfig.headers[Authorization] = "Bearer $accessToken"
}
}
}

protected inline fun <reified I, reified T: Any?> request(requestConfig: RequestConfig<I>): ApiResponse<T?> {
inline fun <reified I, reified T: Any?> request(requestConfig: RequestConfig<I>): ApiResponse<T?> {
val httpUrl = baseUrl.toHttpUrlOrNull() ?: throw IllegalStateException("baseUrl is invalid.")

// take authMethod from operation
Expand Down Expand Up @@ -302,7 +308,7 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie
}
}

protected fun parameterToString(value: Any?): String = when (value) {
fun parameterToString(value: Any?): String = when (value) {
null -> ""
is Array<*> -> toMultiValue(value, "csv").toString()
is Iterable<*> -> toMultiValue(value, "csv").toString()
Expand Down
39 changes: 39 additions & 0 deletions lib/src/main/kotlin/com/openai/infrastructure/BigDecimalAdapter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,50 @@ import com.squareup.moshi.FromJson
import com.squareup.moshi.ToJson
import java.math.BigDecimal

/**
* This Adapter was obviously added because...
* ```
* Platform class java.math.BigDecimal requires explicit JsonAdapter to be registered
* ```
*
* However, the original `fun toJson(value: BigDecimal): String` implementation
* returns a number as a string that gets serialized with quotes,
* which OpenAI complains about:
* ```
* --> POST https://api.openai.com/v1/realtime/sessions
* Content-Length: 720
* Content-Type: application/json
* Accept: application/json
* ...
* {...,"temperature":"0.800000011920928955078125"}
* --> END POST (720-byte body)
* <-- 400 https://api.openai.com/v1/realtime/sessions (470ms)
* ...
* {
* "error": {
* "message": "Invalid type for 'temperature': expected a decimal, but got a string instead.",
* "type": "invalid_request_error",
* "param": "temperature",
* "code": "invalid_type"
* }
* }
* <-- END HTTP (208-byte body)
* ```
*
* Changing this to return Double causes OpenAI to stop complaining.
*/
class BigDecimalAdapter {
/*
@ToJson
fun toJson(value: BigDecimal): String {
return value.toPlainString()
}
*/

@ToJson
fun toJson(value: BigDecimal): Double {
return value.toDouble()
}

@FromJson
fun fromJson(value: String): BigDecimal {
Expand Down
Loading