Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package me.kpavlov.aimocks.a2a

import io.ktor.http.ContentType
import me.kpavlov.aimocks.core.AbstractBuildingStep
import me.kpavlov.mokksy.BuildingStep
import me.kpavlov.mokksy.MokksyServer
Expand All @@ -13,6 +14,7 @@ public class AgentCardBuildingStep(
val responseDefinition = this.build()
val responseSpecification = AgentCardResponseSpecification(responseDefinition)
block.invoke(responseSpecification)
contentType = ContentType.Application.Json
body = requireNotNull(responseSpecification.card) { "Card must be defined" }
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package me.kpavlov.aimocks.anthropic

import io.ktor.http.ContentType
import io.ktor.sse.TypedServerSentEvent
import io.ktor.utils.io.InternalAPI
import kotlinx.coroutines.ExperimentalCoroutinesApi
Expand Down Expand Up @@ -40,7 +41,7 @@ public class AnthropicBuildingStep(
val stopReason = chatResponseSpecification.stopReason
val completionTokens = LongRange(1, 10).random()
delay = chatResponseSpecification.delay

contentType = ContentType.Application.Json
headers += "x-request-id" to randomIdString("req_")
body =
Message(
Expand Down
1 change: 1 addition & 0 deletions ai-mocks-gemini/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ kotlin {
commonMain {
dependencies {
api(libs.ktor.serialization.kotlinx.json)
api(libs.ktor.sse)
api(project(":ai-mocks-core"))
api(project.dependencies.platform(libs.ktor.bom))
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package me.kpavlov.aimocks.gemini.content

import io.ktor.http.ContentType
import me.kpavlov.aimocks.core.AbstractBuildingStep
import me.kpavlov.aimocks.gemini.GenerateContentRequest
import me.kpavlov.mokksy.BuildingStep
Expand Down Expand Up @@ -35,7 +36,7 @@ public class GeminiContentBuildingStep(
block.invoke(chatResponseSpecification)
val assistantContent = chatResponseSpecification.content
delay = chatResponseSpecification.delay

contentType = ContentType.Application.Json
body =
generateContentResponse(
assistantContent = assistantContent,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package me.kpavlov.aimocks.gemini.content

import io.ktor.sse.TypedServerSentEvent
import io.ktor.utils.io.InternalAPI
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.map
Expand Down Expand Up @@ -84,22 +86,22 @@ public class GeminiStreamingContentBuildingStep(
}
}

@OptIn(InternalAPI::class)
private fun encodeChunk(
chunk: GenerateContentResponse,
sse: Boolean,
lastChunk: Boolean = false,
): String {
val json =
Json.encodeToString(
value = chunk,
serializer = GenerateContentResponse.serializer(),
)
return if (sse) {
"data: $json\r\n\r\n"
TypedServerSentEvent(
data = chunk,
).toString {
Json.encodeToString(it)
}
} else if (lastChunk) {
json
Json.encodeToString(value = chunk)
} else {
"$json,\r\n"
"${Json.encodeToString(value = chunk)},\r\n"
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package me.kpavlov.aimocks.ollama.chat

import io.ktor.http.ContentType
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.asFlow
Expand Down Expand Up @@ -55,6 +56,7 @@ public class OllamaChatBuildingStep(
)
block.invoke(chatResponseSpecification)
delay = chatResponseSpecification.delay
contentType = ContentType.Application.Json

val promptEvalCount = nextInt(1, 200)
val evalCount = nextInt(1, 500)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package me.kpavlov.aimocks.ollama.embed

import io.ktor.http.ContentType
import kotlinx.datetime.Clock
import me.kpavlov.aimocks.core.AbstractBuildingStep
import me.kpavlov.aimocks.core.EmbeddingUtils
Expand Down Expand Up @@ -51,6 +52,7 @@ public class OllamaEmbedBuildingStep(
?: request.input.map { EmbeddingUtils.generateEmbedding(it) }
val modelName = embedResponseSpecification.model ?: request.model
delay = embedResponseSpecification.delay
contentType = ContentType.Application.Json

@Suppress("MagicNumber")
val promptEvalCount = nextInt(1, 200)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package me.kpavlov.aimocks.ollama.generate

import io.ktor.http.ContentType
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.asFlow
Expand Down Expand Up @@ -55,6 +56,7 @@ public class OllamaGenerateBuildingStep(
val responseContent = generateResponseSpecification.responseContent
val doneReason = generateResponseSpecification.doneReason
delay = generateResponseSpecification.delay
contentType = ContentType.Application.Json

val promptEvalCount = nextInt(1, 200)
val evalCount = nextInt(1, 500)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package me.kpavlov.aimocks.openai.completions

import io.ktor.http.ContentType
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.asFlow
Expand Down Expand Up @@ -63,6 +64,7 @@ public class OpenaiChatCompletionsBuildingStep(
val assistantContent = chatResponseSpecification.assistantContent
val finishReason = chatResponseSpecification.finishReason
delay = chatResponseSpecification.delay
contentType = ContentType.Application.Json

val promptTokens = nextInt(1, 200)
val completionTokens = nextInt(1, request.maxCompletionTokens ?: 500)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package me.kpavlov.aimocks.openai.embeddings

import io.ktor.http.ContentType
import me.kpavlov.aimocks.core.AbstractBuildingStep
import me.kpavlov.aimocks.core.EmbeddingUtils
import me.kpavlov.aimocks.openai.model.embeddings.CreateEmbeddingsRequest
Expand Down Expand Up @@ -55,6 +56,7 @@ public class OpenaiEmbedBuildingStep(
responseSpecification.embeddings
?: request.input.map { EmbeddingUtils.generateEmbedding(it) }
delay = responseSpecification.delay
contentType = ContentType.Application.Json

val promptTokens = nextInt(1, 100)
val totalTokens = nextInt(promptTokens, promptTokens + 500)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package me.kpavlov.aimocks.openai.moderation

import io.ktor.http.ContentType
import me.kpavlov.aimocks.core.AbstractBuildingStep
import me.kpavlov.aimocks.openai.model.moderation.CreateModerationRequest
import me.kpavlov.aimocks.openai.model.moderation.Moderation
Expand Down Expand Up @@ -30,6 +31,7 @@ public class OpenaiModerationBuildingStep(
val spec = OpenaiModerationResponseSpecification(responseDefinition)
block.invoke(spec)
delay = spec.delay
contentType = ContentType.Application.Json
val id = spec.id ?: "modr-${Integer.toHexString(counter.addAndGet(1))}"
val createdModel = spec.model
val result: ModerationResult = spec.toResult()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package me.kpavlov.aimocks.openai.responses

import io.ktor.http.ContentType
import me.kpavlov.aimocks.core.AbstractBuildingStep
import me.kpavlov.aimocks.openai.model.OutputContent
import me.kpavlov.aimocks.openai.model.OutputMessage
Expand Down Expand Up @@ -43,6 +44,7 @@ public class OpenaiResponsesBuildingStep(
block.invoke(chatResponseSpecification)
val assistantContent = chatResponseSpecification.assistantContent
delay = chatResponseSpecification.delay
contentType = ContentType.Application.Json

val inputTokens = Random.Default.nextInt(1, 200)
val outputTokens = Random.Default.nextInt(1, request.maxOutputTokens ?: 1500)
Expand Down
1 change: 1 addition & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ ktor-server-core = { module = "io.ktor:ktor-server-core" }
ktor-server-double-receive = { module = "io.ktor:ktor-server-double-receive" }
ktor-server-netty = { module = "io.ktor:ktor-server-netty" }
ktor-server-sse = { module = "io.ktor:ktor-server-sse" }
ktor-sse = { module = "io.ktor:ktor-sse" }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

io.ktor:ktor-sse artifact doesn’t exist in Ktor 3.3.0
Ktor only publishes ktor-server-sse / ktor-client-sse. This alias will break dependency resolution as Maven can’t find io.ktor:ktor-sse. Please point the alias to the actual module you need (e.g., ktor-client-sse) or drop it.

🤖 Prompt for AI Agents
In gradle/libs.versions.toml around line 67, the alias ktor-sse is pointing to a
non-existent artifact "io.ktor:ktor-sse"; replace it with the correct module
published by Ktor 3.3.0 (for example "io.ktor:ktor-client-sse" or
"io.ktor:ktor-server-sse" depending on whether you need client or server SSE) or
remove the alias entirely so dependency resolution won't fail.

langchain4j-anthropic = { group = "dev.langchain4j", name = "langchain4j-anthropic" }
langchain4j-bom = { group = "dev.langchain4j", name = "langchain4j-bom", version.ref = "langchain4j" }
langchain4j-gemini = { group = "dev.langchain4j", name = "langchain4j-google-ai-gemini" }
Expand Down
20 changes: 15 additions & 5 deletions mokksy/src/commonMain/kotlin/me/kpavlov/mokksy/BuildingStep.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import io.ktor.sse.ServerSentEventMetadata
import me.kpavlov.mokksy.request.RequestSpecification
import me.kpavlov.mokksy.response.ResponseDefinitionBuilder
import me.kpavlov.mokksy.response.StreamingResponseDefinitionBuilder
import me.kpavlov.mokksy.utils.logger.HttpFormatter
import java.io.IOException
import kotlin.reflect.KClass

Expand All @@ -25,6 +26,7 @@ public class BuildingStep<P : Any> internal constructor(
private val configuration: StubConfiguration,
private val requestSpecification: RequestSpecification<P>,
private val registerStub: (Stub<*, *>) -> Unit,
private val formatter: HttpFormatter,
) {
/**
* @param P The type of the request payload.
Expand All @@ -37,11 +39,13 @@ public class BuildingStep<P : Any> internal constructor(
name: String?,
requestSpecification: RequestSpecification<P>,
registerStub: (Stub<*, *>) -> Unit,
formatter: HttpFormatter,
) : this(
requestType = requestType,
configuration = StubConfiguration(name),
requestSpecification = requestSpecification,
registerStub = registerStub,
formatter = formatter,
)

/**
Expand All @@ -62,8 +66,10 @@ public class BuildingStep<P : Any> internal constructor(
val req = CapturedRequest(call.request, requestType)
@SuppressWarnings("TooGenericExceptionCaught")
try {
ResponseDefinitionBuilder<P, T>(request = req)
.apply(block)
ResponseDefinitionBuilder<P, T>(
request = req,
formatter = formatter,
).apply(block)
.build()
} catch (e: Exception) {
if (e as? IOException == null) {
Expand Down Expand Up @@ -91,15 +97,19 @@ public class BuildingStep<P : Any> internal constructor(
* @param block A lambda function applied to a [me.kpavlov.mokksy.response.StreamingResponseDefinitionBuilder],
* used to configure the streaming response definition.
*/
public infix fun <T : Any> respondsWithStream(block: StreamingResponseDefinitionBuilder<P, T>.() -> Unit) {
public infix fun <T : Any> respondsWithStream(
block: StreamingResponseDefinitionBuilder<P, T>.() -> Unit,
) {
val stub =
Stub(
configuration = configuration,
requestSpecification = requestSpecification,
) { call ->
val req = CapturedRequest(call.request, requestType)
StreamingResponseDefinitionBuilder<P, T>(request = req)
.apply(block)
StreamingResponseDefinitionBuilder<P, T>(
request = req,
formatter = formatter,
).apply(block)
.build()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ public open class MokksyServer
requestSpecification = requestSpec,
registerStub = this::registerStub,
requestType = requestType,
formatter = httpFormatter,
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,22 @@ import kotlin.time.Duration
internal typealias ResponseDefinitionSupplier<T> = (
ApplicationCall,
) -> AbstractResponseDefinition<T>

/**
* Represents the base definition of an HTTP response in a mapping between a request and its corresponding response.
* Provides the required attributes and behavior for configuring HTTP responses, including status code, headers,
* and content type. This class serves as the foundation for more specialized response definitions.
*
* @param T The type of the response data.
* @property contentType The MIME type of the response content. Defaults to `null`.
* @property contentType The MIME type of the response content.
* @property httpStatusCode The HTTP status code of the response as Int, defaulting to 200.
* @property httpStatus The HTTP status code of the response. Defaults to [HttpStatusCode.OK].
* @property headers A lambda function for configuring the response headers. Defaults to `null`.
* @property headerList A list of header key-value pairs to populate the response headers. Defaults to an empty list.
* @property delay A delay applied before sending the response. Defaults to [Duration.ZERO].
* @property responseBody The optional response payload associated with this definition.
*/
public abstract class AbstractResponseDefinition<T>(
public val contentType: ContentType? = null,
public val contentType: ContentType,
public val httpStatusCode: Int = 200,
public val httpStatus: HttpStatusCode = HttpStatusCode.fromValue(httpStatusCode),
public val headers: (ResponseHeaders.() -> Unit)? = null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import io.ktor.http.ContentType
import io.ktor.http.HttpStatusCode
import io.ktor.server.application.ApplicationCall
import io.ktor.server.application.log
import io.ktor.server.request.httpVersion
import io.ktor.server.response.ResponseHeaders
import io.ktor.server.response.respond
import io.ktor.util.cio.ChannelWriteException
import kotlinx.coroutines.delay
import me.kpavlov.mokksy.utils.logger.HttpFormatter
import kotlin.time.Duration

/**
Expand All @@ -33,6 +35,7 @@ public open class ResponseDefinition<P, T>(
headers: (ResponseHeaders.() -> Unit)? = null,
headerList: List<Pair<String, String>> = emptyList<Pair<String, String>>(),
delay: Duration,
private val formatter: HttpFormatter,
) : AbstractResponseDefinition<T>(
contentType = contentType,
httpStatusCode = httpStatusCode,
Expand All @@ -48,13 +51,25 @@ public open class ResponseDefinition<P, T>(
if (this.delay.isPositive()) {
delay(delay)
}
val effectiveBody = responseBody ?: body
if (verbose) {
call.application.log.debug("Sending {}: {}", httpStatus, body)
call.application.log.debug(
"Sending:\n---\n${
formatter.formatResponse(
httpVersion = call.request.httpVersion,
headers = call.response.headers,
contentType = this.contentType,
status = httpStatus,
body = effectiveBody?.toString(),
)
}---\n",
)
}
try {
val payload: Any = effectiveBody ?: ""
call.respond(
status = httpStatus,
message = body ?: "" as Any,
message = payload,
)
} catch (e: ChannelWriteException) {
// We can't do anything about it
Expand Down
Loading
Loading