Show product usage types in credit ledger
Persist hotword request origin so every reservation lifecycle entry can be classified without mutating the immutable ledger.
This commit is contained in:
@@ -214,6 +214,7 @@ class AdminRoutesTest {
|
||||
balanceAfter = 102,
|
||||
referenceId = null,
|
||||
createdAt = "2026-08-19T09:00:00Z",
|
||||
usageType = "hotword",
|
||||
),
|
||||
),
|
||||
nextCursor = null,
|
||||
@@ -233,6 +234,7 @@ class AdminRoutesTest {
|
||||
assertEquals(HttpStatusCode.OK, response.status)
|
||||
response.bodyAsText() shouldContain """"userId":"11111111-1111-4111-8111-111111111111""""
|
||||
response.bodyAsText() shouldContain """"reasonCode":"USAGE_SETTLE""""
|
||||
response.bodyAsText() shouldContain """"usageType":"hotword""""
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -42,4 +42,16 @@ class TextRequestPolicyTest : StringSpec({
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
"accepts hotword source only for AI requests" {
|
||||
val request = TextGatewayRequest(
|
||||
input = "latest news",
|
||||
requestSource = GatewayRequestSource.HOTWORD,
|
||||
)
|
||||
|
||||
TextRequestPolicy.validate(request, GatewayCapability.AI)
|
||||
shouldThrow<IllegalArgumentException> {
|
||||
TextRequestPolicy.validate(request, GatewayCapability.POLISH)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.osglab.account.features.gateway.routes
|
||||
|
||||
import com.osglab.account.features.gateway.models.GatewayCapability
|
||||
import com.osglab.account.features.gateway.models.GatewayPrincipal
|
||||
import com.osglab.account.features.gateway.models.GatewayRequestSource
|
||||
import com.osglab.account.features.gateway.models.ProviderDescriptor
|
||||
import com.osglab.account.features.gateway.models.ProviderOutput
|
||||
import com.osglab.account.features.gateway.models.ProviderRequest
|
||||
@@ -62,11 +63,12 @@ class GatewayRequestIdTest : StringSpec({
|
||||
val response = client.post("/v1/gateway/llm/ai") {
|
||||
header("X-Request-ID", "request-route-123")
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody("""{"input":"hello","maxOutputTokens":8}""")
|
||||
setBody("""{"input":"hello","maxOutputTokens":8,"requestSource":"hotword"}""")
|
||||
}
|
||||
|
||||
response.status shouldBe HttpStatusCode.OK
|
||||
provider.lastRequestId shouldBe "request-route-123"
|
||||
provider.lastRequestSource shouldBe GatewayRequestSource.HOTWORD
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -93,6 +95,7 @@ private fun io.ktor.server.application.Application.gatewayTestApplication(provid
|
||||
private class RequestIdProvider : GatewayProvider {
|
||||
var calls = 0
|
||||
var lastRequestId: String? = null
|
||||
var lastRequestSource: GatewayRequestSource? = null
|
||||
|
||||
override val descriptor = ProviderDescriptor(
|
||||
id = "request-id-provider",
|
||||
@@ -104,6 +107,7 @@ private class RequestIdProvider : GatewayProvider {
|
||||
override suspend fun execute(request: ProviderRequest, output: ProviderOutput): ProviderUsage {
|
||||
calls += 1
|
||||
lastRequestId = request.requestId
|
||||
lastRequestSource = request.requestSource
|
||||
output.emit("""{"result":"ok"}""".encodeToByteArray())
|
||||
return ProviderUsage(
|
||||
meter = UsageMeter.LLM_TOKEN,
|
||||
|
||||
+21
-2
@@ -2,6 +2,7 @@ package com.osglab.account.features.gateway.services
|
||||
|
||||
import com.osglab.account.features.gateway.models.GatewayCapability
|
||||
import com.osglab.account.features.gateway.models.GatewayPrincipal
|
||||
import com.osglab.account.features.gateway.models.GatewayRequestSource
|
||||
import com.osglab.account.features.gateway.models.ProviderDescriptor
|
||||
import com.osglab.account.features.gateway.models.ProviderOutput
|
||||
import com.osglab.account.features.gateway.models.ProviderRequest
|
||||
@@ -66,6 +67,19 @@ class GatewayServiceBillingTest : StringSpec({
|
||||
credits.lastEstimate?.outputUnits shouldBe 32L
|
||||
}
|
||||
|
||||
"records hotword request source with provider metadata" {
|
||||
val usageRecords = FakeUsageRecords()
|
||||
val service = service(FakeCredits(), FakeProvider(), usageRecords)
|
||||
|
||||
service.execute(
|
||||
PRINCIPAL,
|
||||
request(GatewayRequestSource.HOTWORD),
|
||||
DISCARD_OUTPUT,
|
||||
)
|
||||
|
||||
usageRecords.lastClaim?.requestSource shouldBe GatewayRequestSource.HOTWORD
|
||||
}
|
||||
|
||||
"uses one reservation when a buffered DeepSeek empty result succeeds on retry" {
|
||||
val credits = FakeCredits()
|
||||
var attempts = 0
|
||||
@@ -203,7 +217,7 @@ private fun service(
|
||||
usageRecords = usageRecords,
|
||||
)
|
||||
|
||||
private fun request() = TextProviderRequest(
|
||||
private fun request(requestSource: GatewayRequestSource? = null) = TextProviderRequest(
|
||||
requestId = "request-123",
|
||||
capability = GatewayCapability.AI,
|
||||
input = "hello",
|
||||
@@ -211,6 +225,7 @@ private fun request() = TextProviderRequest(
|
||||
maxOutputTokens = 32,
|
||||
temperature = 0.2,
|
||||
stream = false,
|
||||
requestSource = requestSource,
|
||||
)
|
||||
|
||||
private class FakeCredits(
|
||||
@@ -293,7 +308,11 @@ private class EmptyResultProvider : GatewayProvider {
|
||||
private class FakeUsageRecords(
|
||||
private val pending: MutableList<PendingSettlement> = mutableListOf(),
|
||||
) : GatewayUsagePort {
|
||||
override suspend fun claim(metadata: ProviderRequestMetadata) = Unit
|
||||
var lastClaim: ProviderRequestMetadata? = null
|
||||
|
||||
override suspend fun claim(metadata: ProviderRequestMetadata) {
|
||||
lastClaim = metadata
|
||||
}
|
||||
override suspend fun markStarted(accountId: String, requestId: String) = Unit
|
||||
override suspend fun markSettlementPending(
|
||||
accountId: String,
|
||||
|
||||
Reference in New Issue
Block a user