Fix managed ASR settlement and empty polish recovery
Accept the final sequence format returned by Volcengine without weakening ordering checks, and retry one safe buffered DeepSeek empty response under the same credit reservation.
This commit is contained in:
+90
@@ -49,6 +49,90 @@ class DeepSeekClientTest : StringSpec({
|
||||
}
|
||||
}
|
||||
|
||||
"captures only safe metadata for an empty buffered result" {
|
||||
val client = client(
|
||||
"""
|
||||
{
|
||||
"choices":[{
|
||||
"message":{"content":"","reasoning_content":"internal reasoning"},
|
||||
"finish_reason":"length"
|
||||
}],
|
||||
"usage":{"prompt_tokens":10,"completion_tokens":32,"total_tokens":42}
|
||||
}
|
||||
""".trimIndent(),
|
||||
)
|
||||
var emitted = false
|
||||
try {
|
||||
val failure = shouldThrow<DeepSeekEmptyResultException> {
|
||||
KtorDeepSeekClient(client, CONFIG).complete(
|
||||
request(),
|
||||
ProviderOutput { emitted = true },
|
||||
)
|
||||
}
|
||||
|
||||
failure.finishReason shouldBe "length"
|
||||
failure.reasoningContentPresent shouldBe true
|
||||
failure.usagePresent shouldBe true
|
||||
emitted shouldBe false
|
||||
} finally {
|
||||
client.close()
|
||||
}
|
||||
}
|
||||
|
||||
"retries one buffered empty result and returns the successful retry" {
|
||||
var attempts = 0
|
||||
val provider = DeepSeekProvider(
|
||||
DeepSeekClient { _, _ ->
|
||||
attempts += 1
|
||||
if (attempts == 1) {
|
||||
throw DeepSeekEmptyResultException(
|
||||
finishReason = "length",
|
||||
reasoningContentPresent = true,
|
||||
usagePresent = true,
|
||||
)
|
||||
}
|
||||
SUCCESS_USAGE
|
||||
},
|
||||
)
|
||||
|
||||
val usage = provider.execute(request(), DISCARD_OUTPUT)
|
||||
|
||||
attempts shouldBe 2
|
||||
usage shouldBe SUCCESS_USAGE
|
||||
}
|
||||
|
||||
"stops after one buffered empty-result retry" {
|
||||
var attempts = 0
|
||||
val provider = DeepSeekProvider(
|
||||
DeepSeekClient { _, _ ->
|
||||
attempts += 1
|
||||
throw DeepSeekEmptyResultException()
|
||||
},
|
||||
)
|
||||
|
||||
shouldThrow<DeepSeekEmptyResultException> {
|
||||
provider.execute(request(), DISCARD_OUTPUT)
|
||||
}
|
||||
|
||||
attempts shouldBe 2
|
||||
}
|
||||
|
||||
"does not retry an empty streaming result after output may have started" {
|
||||
var attempts = 0
|
||||
val provider = DeepSeekProvider(
|
||||
DeepSeekClient { _, _ ->
|
||||
attempts += 1
|
||||
throw DeepSeekEmptyResultException()
|
||||
},
|
||||
)
|
||||
|
||||
shouldThrow<DeepSeekEmptyResultException> {
|
||||
provider.execute(request().copy(stream = true), DISCARD_OUTPUT)
|
||||
}
|
||||
|
||||
attempts shouldBe 1
|
||||
}
|
||||
|
||||
"meters provider input and output tokens from a streamed response" {
|
||||
val response = """
|
||||
data: {"choices":[{"delta":{"content":"ok"}}]}
|
||||
@@ -180,4 +264,10 @@ private val CONFIG = DeepSeekConfig(
|
||||
apiKey = "test-key",
|
||||
model = "configured-model",
|
||||
)
|
||||
private val SUCCESS_USAGE = com.osglab.account.features.gateway.models.ProviderUsage(
|
||||
meter = com.osglab.account.features.gateway.models.UsageMeter.LLM_TOKEN,
|
||||
units = 13,
|
||||
inputUnits = 10,
|
||||
outputUnits = 3,
|
||||
)
|
||||
private val DISCARD_OUTPUT = ProviderOutput { }
|
||||
|
||||
+23
-4
@@ -41,13 +41,32 @@ class SaucV3ProtocolTest : StringSpec({
|
||||
extractFinalDuration(final) shouldBe 1_234L
|
||||
}
|
||||
|
||||
"rejects a positive final sequence" {
|
||||
val frame = SaucV3Codec().decodeServerFrame(
|
||||
serverFrame(1, true, """{"audio_info":{"duration":100}}"""),
|
||||
"accepts the positive final sequence returned by Volcengine production" {
|
||||
val codec = SaucV3Codec()
|
||||
val validator = SaucSequenceValidator()
|
||||
validator.accept(codec.decodeServerFrame(serverFrame(1, false, "{}")))
|
||||
val final = codec.decodeServerFrame(
|
||||
serverFrame(2, true, """{"audio_info":{"duration":100}}"""),
|
||||
)
|
||||
|
||||
validator.accept(final)
|
||||
extractFinalDuration(final) shouldBe 100L
|
||||
}
|
||||
|
||||
"rejects a non-contiguous final sequence regardless of its sign" {
|
||||
val codec = SaucV3Codec()
|
||||
val positive = codec.decodeServerFrame(
|
||||
serverFrame(2, true, """{"audio_info":{"duration":100}}"""),
|
||||
)
|
||||
val negative = codec.decodeServerFrame(
|
||||
serverFrame(-2, true, """{"audio_info":{"duration":100}}"""),
|
||||
)
|
||||
|
||||
shouldThrow<SaucProtocolException> {
|
||||
SaucSequenceValidator().accept(frame)
|
||||
SaucSequenceValidator().accept(positive)
|
||||
}
|
||||
shouldThrow<SaucProtocolException> {
|
||||
SaucSequenceValidator().accept(negative)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+44
@@ -16,6 +16,9 @@ import com.osglab.account.features.gateway.ports.ProviderRequestMetadata
|
||||
import com.osglab.account.features.gateway.ports.ProviderUsageEstimate
|
||||
import com.osglab.account.features.gateway.providers.GatewayProvider
|
||||
import com.osglab.account.features.gateway.providers.ProviderCatalog
|
||||
import com.osglab.account.features.gateway.providers.deepseek.DeepSeekClient
|
||||
import com.osglab.account.features.gateway.providers.deepseek.DeepSeekEmptyResultException
|
||||
import com.osglab.account.features.gateway.providers.deepseek.DeepSeekProvider
|
||||
import io.kotest.assertions.throwables.shouldThrow
|
||||
import io.kotest.core.spec.style.StringSpec
|
||||
import io.kotest.matchers.collections.shouldContainExactly
|
||||
@@ -63,6 +66,47 @@ class GatewayServiceBillingTest : StringSpec({
|
||||
credits.lastEstimate?.outputUnits shouldBe 32L
|
||||
}
|
||||
|
||||
"uses one reservation when a buffered DeepSeek empty result succeeds on retry" {
|
||||
val credits = FakeCredits()
|
||||
var attempts = 0
|
||||
val provider = DeepSeekProvider(
|
||||
DeepSeekClient { _, _ ->
|
||||
attempts += 1
|
||||
if (attempts == 1) throw DeepSeekEmptyResultException()
|
||||
TOKEN_USAGE
|
||||
},
|
||||
)
|
||||
val service = service(credits, provider)
|
||||
|
||||
service.execute(PRINCIPAL, request(), DISCARD_OUTPUT)
|
||||
|
||||
attempts shouldBe 2
|
||||
credits.reserveCalls shouldBe 1
|
||||
credits.settled.shouldContainExactly(RESERVATION_ID to TOKEN_USAGE.units)
|
||||
credits.released shouldBe emptyList()
|
||||
}
|
||||
|
||||
"releases one reservation after both buffered DeepSeek attempts are empty" {
|
||||
val credits = FakeCredits()
|
||||
var attempts = 0
|
||||
val provider = DeepSeekProvider(
|
||||
DeepSeekClient { _, _ ->
|
||||
attempts += 1
|
||||
throw DeepSeekEmptyResultException()
|
||||
},
|
||||
)
|
||||
val service = service(credits, provider)
|
||||
|
||||
shouldThrow<DeepSeekEmptyResultException> {
|
||||
service.execute(PRINCIPAL, request(), DISCARD_OUTPUT)
|
||||
}
|
||||
|
||||
attempts shouldBe 2
|
||||
credits.reserveCalls shouldBe 1
|
||||
credits.settled shouldBe emptyList()
|
||||
credits.released.shouldContainExactly(RESERVATION_ID)
|
||||
}
|
||||
|
||||
"releases a reservation when a provider call is cancelled" {
|
||||
val credits = FakeCredits()
|
||||
val started = CompletableDeferred<Unit>()
|
||||
|
||||
Reference in New Issue
Block a user