From a1f785bc7d12d5ba4312b653f281294a28dceb95 Mon Sep 17 00:00:00 2001 From: Patrick Ribbsaeter Date: Sat, 1 Aug 2026 23:23:44 +0200 Subject: [PATCH 1/2] fix: parse string-encoded organization costs --- .../organization/usage/UsageCostsResponse.kt | 17 +++- .../usage/UsageCostsResponseTest.kt | 77 +++++++++++++++++++ 2 files changed, 93 insertions(+), 1 deletion(-) diff --git a/openai-java-core/src/main/kotlin/com/openai/models/admin/organization/usage/UsageCostsResponse.kt b/openai-java-core/src/main/kotlin/com/openai/models/admin/organization/usage/UsageCostsResponse.kt index 19dd63b37..50d7776f7 100644 --- a/openai-java-core/src/main/kotlin/com/openai/models/admin/organization/usage/UsageCostsResponse.kt +++ b/openai-java-core/src/main/kotlin/com/openai/models/admin/organization/usage/UsageCostsResponse.kt @@ -7104,7 +7104,22 @@ private constructor( * @throws OpenAIInvalidDataException if the JSON field has an unexpected type * (e.g. if the server responded with an unexpected value). */ - fun value(): Optional = value.getOptional("value") + fun value(): Optional { + val knownValue = value.asKnown() + if (knownValue.isPresent || value.isMissing() || value.isNull()) { + return knownValue + } + + val stringValue = value.asString() + if (stringValue.isPresent) { + val parsedValue = stringValue.get().toDoubleOrNull() + if (parsedValue != null && parsedValue.isFinite()) { + return Optional.of(parsedValue) + } + } + + throw OpenAIInvalidDataException("`value` is invalid, received $value") + } /** * Returns the raw JSON value of [currency]. diff --git a/openai-java-core/src/test/kotlin/com/openai/models/admin/organization/usage/UsageCostsResponseTest.kt b/openai-java-core/src/test/kotlin/com/openai/models/admin/organization/usage/UsageCostsResponseTest.kt index ba2f20641..34523a0a0 100644 --- a/openai-java-core/src/test/kotlin/com/openai/models/admin/organization/usage/UsageCostsResponseTest.kt +++ b/openai-java-core/src/test/kotlin/com/openai/models/admin/organization/usage/UsageCostsResponseTest.kt @@ -4,11 +4,88 @@ package com.openai.models.admin.organization.usage import com.fasterxml.jackson.module.kotlin.jacksonTypeRef import com.openai.core.jsonMapper +import com.openai.errors.OpenAIInvalidDataException import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.assertThatThrownBy import org.junit.jupiter.api.Test internal class UsageCostsResponseTest { + @Test + fun parsesStringEncodedCostAmounts() { + val response = + jsonMapper() + .readValue( + """ + { + "data": [ + { + "end_time": 1, + "results": [ + { + "object": "organization.costs.result", + "amount": {"value": "0E-6176", "currency": "usd"} + }, + { + "object": "organization.costs.result", + "amount": { + "value": "0.003627500000000000000000000000000000", + "currency": "usd" + } + } + ], + "start_time": 0 + } + ], + "has_more": false, + "object": "page" + } + """ + .trimIndent(), + jacksonTypeRef(), + ) + + val amounts = + response.data().single().results().map { + it.asOrganizationCosts().amount().orElseThrow().value().orElseThrow() + } + + assertThat(amounts).containsExactly(0.0, 0.0036275) + } + + @Test + fun rejectsInvalidStringEncodedCostAmount() { + val response = + jsonMapper() + .readValue( + """ + { + "data": [ + { + "end_time": 1, + "results": [ + { + "object": "organization.costs.result", + "amount": {"value": "not-a-number", "currency": "usd"} + } + ], + "start_time": 0 + } + ], + "has_more": false, + "object": "page" + } + """ + .trimIndent(), + jacksonTypeRef(), + ) + + val amount = + response.data().single().results().single().asOrganizationCosts().amount().orElseThrow() + + assertThatThrownBy { amount.value() }.isInstanceOf(OpenAIInvalidDataException::class.java) + } + @Test fun create() { val usageCostsResponse = From 07dfe0fc5b88597f160243dfbe560ddd40c0647a Mon Sep 17 00:00:00 2001 From: Patrick Ribbsaeter Date: Sun, 2 Aug 2026 23:07:15 +0200 Subject: [PATCH 2/2] fix: restrict string cost parsing --- .../admin/organization/usage/UsageCostsResponse.kt | 13 +++++++++++-- .../organization/usage/UsageCostsResponseTest.kt | 13 ++++++++----- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/openai-java-core/src/main/kotlin/com/openai/models/admin/organization/usage/UsageCostsResponse.kt b/openai-java-core/src/main/kotlin/com/openai/models/admin/organization/usage/UsageCostsResponse.kt index 50d7776f7..ce61f887f 100644 --- a/openai-java-core/src/main/kotlin/com/openai/models/admin/organization/usage/UsageCostsResponse.kt +++ b/openai-java-core/src/main/kotlin/com/openai/models/admin/organization/usage/UsageCostsResponse.kt @@ -7112,8 +7112,14 @@ private constructor( val stringValue = value.asString() if (stringValue.isPresent) { - val parsedValue = stringValue.get().toDoubleOrNull() - if (parsedValue != null && parsedValue.isFinite()) { + val rawValue = stringValue.get() + val parsedValue = + if (DECIMAL_NUMBER_PATTERN.matches(rawValue)) { + rawValue.toDoubleOrNull() + } else { + null + } + if (parsedValue?.isFinite() == true) { return Optional.of(parsedValue) } } @@ -7153,6 +7159,9 @@ private constructor( companion object { + private val DECIMAL_NUMBER_PATTERN = + Regex("""-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?""") + /** Returns a mutable builder for constructing an instance of [Amount]. */ @JvmStatic fun builder() = Builder() } diff --git a/openai-java-core/src/test/kotlin/com/openai/models/admin/organization/usage/UsageCostsResponseTest.kt b/openai-java-core/src/test/kotlin/com/openai/models/admin/organization/usage/UsageCostsResponseTest.kt index 34523a0a0..2e306d332 100644 --- a/openai-java-core/src/test/kotlin/com/openai/models/admin/organization/usage/UsageCostsResponseTest.kt +++ b/openai-java-core/src/test/kotlin/com/openai/models/admin/organization/usage/UsageCostsResponseTest.kt @@ -8,6 +8,8 @@ import com.openai.errors.OpenAIInvalidDataException import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatThrownBy import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource internal class UsageCostsResponseTest { @@ -47,14 +49,15 @@ internal class UsageCostsResponseTest { val amounts = response.data().single().results().map { - it.asOrganizationCosts().amount().orElseThrow().value().orElseThrow() + it.asOrganizationCosts().amount().get().value().get() } assertThat(amounts).containsExactly(0.0, 0.0036275) } - @Test - fun rejectsInvalidStringEncodedCostAmount() { + @ParameterizedTest + @ValueSource(strings = ["not-a-number", "1f", "0x1.0p0", " 1 ", "01", "+1"]) + fun rejectsInvalidStringEncodedCostAmount(invalidValue: String) { val response = jsonMapper() .readValue( @@ -66,7 +69,7 @@ internal class UsageCostsResponseTest { "results": [ { "object": "organization.costs.result", - "amount": {"value": "not-a-number", "currency": "usd"} + "amount": {"value": "$invalidValue", "currency": "usd"} } ], "start_time": 0 @@ -81,7 +84,7 @@ internal class UsageCostsResponseTest { ) val amount = - response.data().single().results().single().asOrganizationCosts().amount().orElseThrow() + response.data().single().results().single().asOrganizationCosts().amount().get() assertThatThrownBy { amount.value() }.isInstanceOf(OpenAIInvalidDataException::class.java) }