diff --git a/.idea/gradle.xml b/.idea/gradle.xml
index e90bd0e..33bcda4 100644
--- a/.idea/gradle.xml
+++ b/.idea/gradle.xml
@@ -21,13 +21,19 @@
+
+
+
+
+
+
diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml
index 03fcfb7..90d1b4b 100644
--- a/.idea/kotlinc.xml
+++ b/.idea/kotlinc.xml
@@ -1,6 +1,7 @@
-
+
+
\ No newline at end of file
diff --git a/app/openai-bridge/Dockerfile b/app/openai-bridge/Dockerfile
new file mode 100644
index 0000000..953871a
--- /dev/null
+++ b/app/openai-bridge/Dockerfile
@@ -0,0 +1,32 @@
+FROM eclipse-temurin:21-jdk AS builder
+WORKDIR /work
+
+COPY . .
+
+RUN chmod +x ./gradlew && ./gradlew --no-daemon :app:openai-bridge:installDist -x test
+
+FROM eclipse-temurin:21-jdk
+ENV DEBIAN_FRONTEND=noninteractive
+WORKDIR /opt
+
+RUN apt-get update \
+ && apt-get install -y --no-install-recommends curl ca-certificates unzip \
+ && rm -rf /var/lib/apt/lists/*
+
+ENV CODEQL_VERSION=2.16.6
+RUN mkdir -p /opt/codeql \
+ && curl -L "https://github.com/github/codeql-action/releases/download/codeql-bundle-v${CODEQL_VERSION}/codeql-bundle-linux64.tar.gz" \
+ -o /tmp/codeql.tgz \
+ && tar -xzf /tmp/codeql.tgz -C /opt/codeql --strip-components=1 \
+ && rm /tmp/codeql.tgz
+ENV PATH="/opt/codeql:${PATH}"
+
+COPY --from=builder /work/app/openai-bridge/build/install/openai-bridge /opt/openai-bridge
+
+ENV PORT="8080"
+ENV OLLAMA_BASE_URL="http://host.docker.internal:11434"
+ENV OLLAMA_KEEP_ALIVE="5m"
+
+EXPOSE 8080
+
+ENTRYPOINT ["/opt/openai-bridge/bin/openai-bridge"]
diff --git a/app/openai-bridge/README.md b/app/openai-bridge/README.md
new file mode 100644
index 0000000..074eb4a
--- /dev/null
+++ b/app/openai-bridge/README.md
@@ -0,0 +1,58 @@
+# OpenAPI Bridge Server
+This module contains the HTTP server. It exposes a minimal OpenAI-style `POST /v1/chat/completions` endpoint backed by the SecureCoder engine.
+
+## Run with Docker
+
+### Configuration
+- OpenRouter mode:
+ - `OPENROUTER_KEY` — your OpenRouter API key
+ - `MODEL` — model ID (e.g.: `openai/gpt-oss-20b`)
+- Ollama mode (used when `OPENROUTER_KEY` is not set):
+ - `MODEL` — Ollama model name (e.g.: `gpt-oss:20`)
+ - `OLLAMA_BASE_URL` — base URL to Ollama (default: 11434 on the host)
+ - `OLLAMA_KEEP_ALIVE` — keep-alive duration (default: `5m`)
+
+### Build and run
+Make sure you have Docker installed and are in the project root directory.
+```
+docker build -f app/openai-bridge/Dockerfile -t openai-bridge:latest .
+```
+
+Run with Ollama on the host (macOS/Windows):
+```
+docker run --rm -p 8080:8080 \
+ -e MODEL="gpt-oss:20b" \
+ openai-bridge:latest
+```
+
+Run with Ollama on the host (Linux):
+```
+docker run --rm -p 8080:8080 \
+ --add-host=host.docker.internal:host-gateway \
+ -e MODEL="gpt-oss:20b" \
+ openai-bridge:latest
+```
+
+Run using OpenRouter instead of Ollama:
+```
+docker run --rm -p 8080:8080 \
+ -e OPENROUTER_KEY=... \
+ -e MODEL=openai/gpt-oss-20b \
+ openai-bridge:latest
+```
+
+## Endpoint
+- `POST /v1/chat/completions` — accepts a minimal OpenAI-style request and returns a single choice with the SecureCoder engine’s response.
+
+Example request (from host):
+```
+curl -X POST "http://localhost:8080/v1/chat/completions" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "model": "llama3.1:8b",
+ "messages": [
+ {"role": "user", "content": "Create a Java class named Hello with a main method that prints Hello"}
+ ],
+ "stream": false
+ }'
+```
diff --git a/app/openapi-bridge/build.gradle.kts b/app/openai-bridge/build.gradle.kts
similarity index 54%
rename from app/openapi-bridge/build.gradle.kts
rename to app/openai-bridge/build.gradle.kts
index e73063a..140ac01 100644
--- a/app/openapi-bridge/build.gradle.kts
+++ b/app/openai-bridge/build.gradle.kts
@@ -15,20 +15,5 @@ dependencies {
}
application {
- mainClass.set("de.tuda.stg.securecoder.openapibridge.MainKt")
-}
-
-tasks.named("run") {
- val keys = listOf(
- "OPENROUTER_KEY",
- "MODEL",
- "OLLAMA_BASE_URL",
- "OLLAMA_KEEP_ALIVE",
- "PORT"
- )
- keys.forEach { key ->
- System.getProperty(key)?.let { value ->
- systemProperty(key, value)
- }
- }
+ mainClass.set("de.tuda.stg.securecoder.openaibridge.MainKt")
}
diff --git a/app/openapi-bridge/src/main/java/de/tuda/stg/securecoder/openapibridge/AgentService.kt b/app/openai-bridge/src/main/java/de/tuda/stg/securecoder/openaibridge/AgentService.kt
similarity index 94%
rename from app/openapi-bridge/src/main/java/de/tuda/stg/securecoder/openapibridge/AgentService.kt
rename to app/openai-bridge/src/main/java/de/tuda/stg/securecoder/openaibridge/AgentService.kt
index 92bd6c9..31a6d09 100644
--- a/app/openapi-bridge/src/main/java/de/tuda/stg/securecoder/openapibridge/AgentService.kt
+++ b/app/openai-bridge/src/main/java/de/tuda/stg/securecoder/openaibridge/AgentService.kt
@@ -1,4 +1,4 @@
-package de.tuda.stg.securecoder.openapibridge
+package de.tuda.stg.securecoder.openaibridge
import de.tuda.stg.securecoder.engine.Engine
import de.tuda.stg.securecoder.engine.file.edit.ApplyChanges.applyEdits
@@ -14,7 +14,7 @@ class AgentService(private val engine: Engine) {
val fileSystem = InMemoryFileSystem()
val userPrompt = messages.lastOrNull { it.role == "user" }?.content ?: ""
val result = engine.run(
- prompt = userPrompt,
+ prompt = "$userPrompt\nOnly create ONE file!",
filesystem = fileSystem,
onEvent = { event ->
println("Internal Agent Event: $event")
diff --git a/app/openapi-bridge/src/main/java/de/tuda/stg/securecoder/openapibridge/EngineFactory.kt b/app/openai-bridge/src/main/java/de/tuda/stg/securecoder/openaibridge/EngineFactory.kt
similarity index 96%
rename from app/openapi-bridge/src/main/java/de/tuda/stg/securecoder/openapibridge/EngineFactory.kt
rename to app/openai-bridge/src/main/java/de/tuda/stg/securecoder/openaibridge/EngineFactory.kt
index 0c1adf4..577518b 100644
--- a/app/openapi-bridge/src/main/java/de/tuda/stg/securecoder/openapibridge/EngineFactory.kt
+++ b/app/openai-bridge/src/main/java/de/tuda/stg/securecoder/openaibridge/EngineFactory.kt
@@ -1,4 +1,4 @@
-package de.tuda.stg.securecoder.openapibridge
+package de.tuda.stg.securecoder.openaibridge
import de.tuda.stg.securecoder.engine.Engine
import de.tuda.stg.securecoder.engine.llm.LlmClient
diff --git a/app/openapi-bridge/src/main/java/de/tuda/stg/securecoder/openapibridge/Main.kt b/app/openai-bridge/src/main/java/de/tuda/stg/securecoder/openaibridge/Main.kt
similarity index 94%
rename from app/openapi-bridge/src/main/java/de/tuda/stg/securecoder/openapibridge/Main.kt
rename to app/openai-bridge/src/main/java/de/tuda/stg/securecoder/openaibridge/Main.kt
index 7336b9d..b3479ed 100644
--- a/app/openapi-bridge/src/main/java/de/tuda/stg/securecoder/openapibridge/Main.kt
+++ b/app/openai-bridge/src/main/java/de/tuda/stg/securecoder/openaibridge/Main.kt
@@ -1,4 +1,4 @@
-package de.tuda.stg.securecoder.openapibridge
+package de.tuda.stg.securecoder.openaibridge
import io.ktor.serialization.kotlinx.json.json
import io.ktor.server.application.install
diff --git a/app/openapi-bridge/src/main/java/de/tuda/stg/securecoder/openapibridge/OpenAIRoutes.kt b/app/openai-bridge/src/main/java/de/tuda/stg/securecoder/openaibridge/OpenAIRoutes.kt
similarity index 89%
rename from app/openapi-bridge/src/main/java/de/tuda/stg/securecoder/openapibridge/OpenAIRoutes.kt
rename to app/openai-bridge/src/main/java/de/tuda/stg/securecoder/openaibridge/OpenAIRoutes.kt
index a1e69f4..6f286a8 100644
--- a/app/openapi-bridge/src/main/java/de/tuda/stg/securecoder/openapibridge/OpenAIRoutes.kt
+++ b/app/openai-bridge/src/main/java/de/tuda/stg/securecoder/openaibridge/OpenAIRoutes.kt
@@ -1,4 +1,4 @@
-package de.tuda.stg.securecoder.openapibridge
+package de.tuda.stg.securecoder.openaibridge
import io.ktor.server.request.*
import io.ktor.server.response.*
diff --git a/app/openapi-bridge/src/main/java/de/tuda/stg/securecoder/openapibridge/OpenApiModels.kt b/app/openai-bridge/src/main/java/de/tuda/stg/securecoder/openaibridge/OpenApiModels.kt
similarity index 94%
rename from app/openapi-bridge/src/main/java/de/tuda/stg/securecoder/openapibridge/OpenApiModels.kt
rename to app/openai-bridge/src/main/java/de/tuda/stg/securecoder/openaibridge/OpenApiModels.kt
index fbc7f36..0656bb0 100644
--- a/app/openapi-bridge/src/main/java/de/tuda/stg/securecoder/openapibridge/OpenApiModels.kt
+++ b/app/openai-bridge/src/main/java/de/tuda/stg/securecoder/openaibridge/OpenApiModels.kt
@@ -1,4 +1,4 @@
-package de.tuda.stg.securecoder.openapibridge
+package de.tuda.stg.securecoder.openaibridge
import kotlinx.serialization.Serializable
diff --git a/app/openapi-bridge/README.md b/app/openapi-bridge/README.md
deleted file mode 100644
index 69cbd3a..0000000
--- a/app/openapi-bridge/README.md
+++ /dev/null
@@ -1,63 +0,0 @@
-# OpenAPI Bridge Server
-This module contains the HTTP server. It exposes a minimal OpenAI-style `POST /v1/chat/completions` endpoint backed by the SecureCoder engine.
-
-## Prerequisites
-- JDK 21
-- Optional but recommended for security analysis features: CodeQL CLI in `PATH` (the Guardian uses it when analyzing code). If not present, some security analysis steps may fail.
-
-
-## Configuration (Environment Variables or -D system properties)
-The server reads its configuration from either environment variables or JVM system properties (`-DNAME=value`).
-
-- `PORT` — HTTP port (default: `8080`)
-- LLM selection (EngineFactory picks the first matching provider):
- - OpenRouter mode:
- - `OPENROUTER_KEY` — your OpenRouter API key
- - `MODEL` — model ID (default: `openai/gpt-oss-20b`)
- - Ollama mode (used when `OPENROUTER_KEY` is not set):
- - `MODEL` — Ollama model name, e.g. `llama3.1:8b`
- - `OLLAMA_BASE_URL` — base URL to Ollama (default: `http://127.0.0.1:11434`)
- - `OLLAMA_KEEP_ALIVE` — keep-alive duration (default: `5m`)
-
-
-## How to run the server
-
-On macOS/Linux (using -D system properties):
-
-```
-./gradlew :app:openapi-bridge:run -DOPENROUTER_KEY=... -DMODEL=...
-```
-
-Or with environment variables:
-
-```
-export OPENROUTER_KEY=...
-export MODEL=...
-./gradlew :app:openapi-bridge:run
-```
-
-On Windows
-
-```
-set OPENROUTER_KEY=...
-set MODEL=...
-gradlew.bat :app:openapi-bridge:run
-```
-
-## Endpoint
-
-- `POST /v1/chat/completions` — accepts a minimal OpenAI-style request and returns a single choice with the SecureCoder engine’s response.
-
-Example request (curl):
-
-```
-curl -X POST "http://localhost:8080/v1/chat/completions" \
- -H "Content-Type: application/json" \
- -d '{
- "model": "llama3.1:8b",
- "messages": [
- {"role": "user", "content": "Create a Java class named Hello with a main method that prints Hello"}
- ],
- "stream": false
- }'
-```
diff --git a/engine/src/main/kotlin/de/tuda/stg/securecoder/engine/file/edit/ApplyChanges.kt b/engine/src/main/kotlin/de/tuda/stg/securecoder/engine/file/edit/ApplyChanges.kt
index 23e459b..f1c216c 100644
--- a/engine/src/main/kotlin/de/tuda/stg/securecoder/engine/file/edit/ApplyChanges.kt
+++ b/engine/src/main/kotlin/de/tuda/stg/securecoder/engine/file/edit/ApplyChanges.kt
@@ -21,6 +21,12 @@ object ApplyChanges {
return when (match) {
is Success.Append -> original + action.replaceText
is Success.Match -> buildString {
+ if (match.end > original.length) {
+ throw IllegalStateException(
+ "Match end index (${match.end}) is out of bounds for string of length ${original.length}. "
+ + "Range: [${match.start}, ${match.end}), Replacement: '${action.replaceText}'"
+ )
+ }
append(original, 0, match.start)
append(action.replaceText)
append(original, match.end, original.length)
@@ -41,7 +47,7 @@ object ApplyChanges {
}
}
- fun match(text: String, search: Changes.SearchedText): MatchResult {
+ fun match(text: String?, search: Changes.SearchedText): MatchResult {
return Matcher.RootMatcher.match(text, search)
}
@@ -50,8 +56,8 @@ object ApplyChanges {
val headerMessage: String = when (matchResult) {
is Error.NoMatch ->
"your *SEARCH* pattern not found in file $file"
- is Error.ReplaceOnEmpty ->
- "can only append to file $file as it is empty or does not exist but your *SEARCH* pattern is not empty"
+ is Error.ReplaceOnNotExistent ->
+ "can only append to file $file as it is does not exist but your *SEARCH* pattern is not empty"
is Error.MultipleMatch ->
"your *SEARCH* pattern has several matches in $file"
}
diff --git a/engine/src/main/kotlin/de/tuda/stg/securecoder/engine/file/edit/EditFilesLlmWrapper.kt b/engine/src/main/kotlin/de/tuda/stg/securecoder/engine/file/edit/EditFilesLlmWrapper.kt
index b7d4447..24191a0 100644
--- a/engine/src/main/kotlin/de/tuda/stg/securecoder/engine/file/edit/EditFilesLlmWrapper.kt
+++ b/engine/src/main/kotlin/de/tuda/stg/securecoder/engine/file/edit/EditFilesLlmWrapper.kt
@@ -6,6 +6,9 @@ import de.tuda.stg.securecoder.engine.llm.ChatMessage.Role
import de.tuda.stg.securecoder.engine.llm.LlmClient
import de.tuda.stg.securecoder.filesystem.FileSystem
import de.tuda.stg.securecoder.engine.llm.ChatExchange
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.toList
import kotlin.collections.plusAssign
class EditFilesLlmWrapper(
@@ -45,7 +48,7 @@ class EditFilesLlmWrapper(
attempts: Int = 3
): ChatResult {
val messages = messages.toMutableList()
- messages += ChatMessage(Role.System, prompt)
+ appendPromptToLastSystem(messages)
repeat(attempts) {
val llmInput = messages.toList()
val response = llmClient.chat(llmInput, params)
@@ -53,7 +56,6 @@ class EditFilesLlmWrapper(
when (val result = parse(response, fileSystem)) {
is ParseResult.Ok -> return ChatResult(messages, result.value)
is ParseResult.Err -> {
- println("LLM parse failed: ${result.buildMessage()}")
messages += ChatMessage(Role.User, result.buildMessage())
onParseError(result.messages, ChatExchange(llmInput, response))
}
@@ -91,7 +93,24 @@ class EditFilesLlmWrapper(
val matches = editsRegex.findAll(contentCopy).toList()
if (matches.isEmpty()) {
- allErrors += "Did not find any *SEARCH/REPLACE* block within the `` tag"
+ allErrors += """
+ Could not find any edit blocks in the response.
+ Example for the expected format:
+
+ src/Main.java
+
+ ...exact old text...
+
+
+ ...new text...
+
+
+
+ src/new.java
+
+ append
+
+ """.trimIndent()
return ParseResult.Err(allErrors)
}
@@ -123,7 +142,7 @@ class EditFilesLlmWrapper(
continue
}
val replace = Changes.SearchReplace(currentFileName, SearchedText(searchPart ?: ""), replacePart ?: "")
- val content = fileSystem.getFile(currentFileName)?.content() ?: ""
+ val content = fileSystem.getFile(currentFileName)?.content()
val match = ApplyChanges.match(content, replace.searchedText)
if (match is Matcher.MatchResult.Error) {
allErrors += ApplyChanges.buildErrorMessage(currentFileName, searchPart ?: "", match)
@@ -145,7 +164,10 @@ class EditFilesLlmWrapper(
}
private fun getTextByXMLTag(container: String, tag: String): String? {
- val regex = Regex("<$tag>(.*?)$tag>", setOf(RegexOption.MULTILINE, RegexOption.DOT_MATCHES_ALL))
+ val regex = Regex(
+ "<$tag>(.*?)$tag>",
+ setOf(RegexOption.MULTILINE, RegexOption.DOT_MATCHES_ALL)
+ )
return regex.find(container)?.groups?.get(1)?.value
}
@@ -153,4 +175,15 @@ class EditFilesLlmWrapper(
if (content == null) return null
return content.replaceFirst(Regex("^\\n"), "")
}
+
+ private fun appendPromptToLastSystem(messages: MutableList) {
+ val lastSystemIndex = messages.indexOfLast { it.role == Role.System }
+ if (lastSystemIndex >= 0) {
+ val existing = messages[lastSystemIndex]
+ val combined = "${existing.content}\n\n$prompt"
+ messages[lastSystemIndex] = ChatMessage(Role.System, combined)
+ } else {
+ messages += ChatMessage(Role.System, prompt)
+ }
+ }
}
diff --git a/engine/src/main/kotlin/de/tuda/stg/securecoder/engine/file/edit/Matcher.kt b/engine/src/main/kotlin/de/tuda/stg/securecoder/engine/file/edit/Matcher.kt
index 445620a..b442e7b 100644
--- a/engine/src/main/kotlin/de/tuda/stg/securecoder/engine/file/edit/Matcher.kt
+++ b/engine/src/main/kotlin/de/tuda/stg/securecoder/engine/file/edit/Matcher.kt
@@ -6,25 +6,31 @@ import de.tuda.stg.securecoder.engine.file.edit.Matcher.MatchResult.Success
interface Matcher {
sealed interface MatchResult {
sealed interface Error : MatchResult {
- object ReplaceOnEmpty : Error
+ object ReplaceOnNotExistent : Error
object NoMatch : Error
data class MultipleMatch (val matches: List) : Error
}
sealed interface Success : MatchResult {
object Append : Success
- class Match (val start: Int, val end: Int) : Success
+ class Match (val start: Int, val end: Int) : Success {
+ init {
+ require(end >= start) {
+ "End index ($end) cannot be less than start index ($start)"
+ }
+ }
+ }
}
}
- fun match(text: String, search: Changes.SearchedText): MatchResult
+ fun match(text: String?, search: Changes.SearchedText): MatchResult
object RootMatcher : Matcher {
- override fun match(text: String, search: Changes.SearchedText): MatchResult {
+ override fun match(text: String?, search: Changes.SearchedText): MatchResult {
if (search.isAppend()) {
return Success.Append
}
- if (text.isEmpty()) {
- return Error.ReplaceOnEmpty
+ if (text == null) {
+ return Error.ReplaceOnNotExistent
}
val matchers = listOf(IndexOfMatcher, TrimmedLinesMatcher)
matchers.forEach {
@@ -37,9 +43,12 @@ interface Matcher {
object IndexOfMatcher : Matcher {
override fun match(
- text: String,
+ text: String?,
search: Changes.SearchedText
): MatchResult {
+ if (text == null) {
+ return Error.NoMatch
+ }
val idx = text.indexesOf(search.text)
return when (idx.size) {
1 -> Success.Match(idx.first(), idx.first() + search.text.length)
@@ -63,7 +72,10 @@ interface Matcher {
}
object TrimmedLinesMatcher : Matcher {
- override fun match(text: String, search: Changes.SearchedText): MatchResult {
+ override fun match(text: String?, search: Changes.SearchedText): MatchResult {
+ if (text == null) {
+ return Error.NoMatch
+ }
val searchedTrimmed = splitToLinesAndTrimLast(search.text).map { it.trim() }
val documentLines = text.split("\n")
val documentLinesTrimmed = documentLines.map { it.trim() }
@@ -81,7 +93,8 @@ interface Matcher {
val lineStarts = IntArray(documentLines.size + 1)
for (i in documentLines.indices) {
- lineStarts[i + 1] = lineStarts[i] + documentLines[i].length + 1
+ val newline = if (i < documentLines.lastIndex) 1 else 0
+ lineStarts[i + 1] = lineStarts[i] + documentLines[i].length + newline
}
val startOffset = lineStarts[firstLine]
diff --git a/engine/src/main/kotlin/de/tuda/stg/securecoder/engine/llm/ChatMessage.kt b/engine/src/main/kotlin/de/tuda/stg/securecoder/engine/llm/ChatMessage.kt
index 6f92101..81668c4 100644
--- a/engine/src/main/kotlin/de/tuda/stg/securecoder/engine/llm/ChatMessage.kt
+++ b/engine/src/main/kotlin/de/tuda/stg/securecoder/engine/llm/ChatMessage.kt
@@ -1,5 +1,5 @@
package de.tuda.stg.securecoder.engine.llm
-class ChatMessage (val role: Role, val content: String) {
+data class ChatMessage (val role: Role, val content: String) {
enum class Role { System, User, Assistant }
}
diff --git a/engine/src/main/kotlin/de/tuda/stg/securecoder/engine/llm/OllamaClient.kt b/engine/src/main/kotlin/de/tuda/stg/securecoder/engine/llm/OllamaClient.kt
index 6d7fe91..ca94d65 100644
--- a/engine/src/main/kotlin/de/tuda/stg/securecoder/engine/llm/OllamaClient.kt
+++ b/engine/src/main/kotlin/de/tuda/stg/securecoder/engine/llm/OllamaClient.kt
@@ -11,16 +11,19 @@ import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
+import kotlinx.serialization.SerializationException
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.buildJsonObject
+import org.slf4j.LoggerFactory
class OllamaClient(
private val model: String,
baseUrl: String = "http://127.0.0.1:11434",
private val keepAlive: String = "5m",
) : LlmClient {
+ private val logger = LoggerFactory.getLogger("OllamaClient")
private val json: Json = Json {
ignoreUnknownKeys = true
explicitNulls = false
@@ -49,6 +52,9 @@ class OllamaClient(
val message: OllamaMsg
)
+ @Serializable
+ private data class OllamaError(val error: String)
+
override suspend fun chat(
messages: List,
params: GenerationParams
@@ -73,14 +79,22 @@ class OllamaClient(
options = options,
keepAlive = keepAlive
)
- println("Sending llm request: $req")
+ logger.debug("Sending llm request: {}", req)
val resp = http.post(endpoint) {
contentType(ContentType.Application.Json)
accept(ContentType.Application.Json)
setBody(req)
}
val body = resp.bodyAsText()
- println("Got llm response: $body")
+ logger.debug("Got llm response: {}", body)
+ if (!resp.status.isSuccess()) {
+ val errorMessage = try {
+ json.decodeFromString(body).error
+ } catch (_: SerializationException) {
+ body.ifBlank { "" }
+ }
+ throw RuntimeException("Failed to call Ollama got ${resp.status}: ${errorMessage}")
+ }
val respObj = json.decodeFromString(body)
return respObj.message.content
diff --git a/engine/src/main/kotlin/de/tuda/stg/securecoder/engine/llm/OpenRouterClient.kt b/engine/src/main/kotlin/de/tuda/stg/securecoder/engine/llm/OpenRouterClient.kt
index abef7da..cfa05b5 100644
--- a/engine/src/main/kotlin/de/tuda/stg/securecoder/engine/llm/OpenRouterClient.kt
+++ b/engine/src/main/kotlin/de/tuda/stg/securecoder/engine/llm/OpenRouterClient.kt
@@ -18,14 +18,17 @@ import io.ktor.serialization.kotlinx.json.json
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
+import kotlinx.serialization.SerializationException
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.buildJsonObject
+import org.slf4j.LoggerFactory
class OpenRouterClient (
private val apiKey: String,
private val model: String,
private val siteName: String? = null,
) : LlmClient {
+ private val logger = LoggerFactory.getLogger("OpenRouterClient")
private val json: Json = Json {
ignoreUnknownKeys = true
explicitNulls = false
@@ -76,6 +79,7 @@ class OpenRouterClient (
maxTokens = params.maxTokens
)
+ logger.debug("Sending llm request: {}", req)
val resp: HttpResponse = http.post(endpoint) {
contentType(ContentType.Application.Json)
accept(ContentType.Application.Json)
@@ -88,11 +92,15 @@ class OpenRouterClient (
if (!resp.status.isSuccess()) {
error("OpenRouter Error ${resp.status.value}: $body")
}
-
- println("OpenRouter response: $body")
- val obj = json.decodeFromString(body)
+ logger.debug("Got llm response: {}", body)
+ val obj = try {
+ json.decodeFromString(body)
+ } catch (e: SerializationException) {
+ val formattedBody = body.ifBlank { "" }
+ throw RuntimeException("Failed to parse OpenRouter response body. Raw body: $formattedBody", e)
+ }
val content = obj.choices.firstOrNull()?.message?.content
- ?: error("OpenRouter lieferte keine Antwortnachricht.")
+ ?: error("OpenRouter did not return any response choices ")
return content
}
diff --git a/engine/src/main/kotlin/de/tuda/stg/securecoder/engine/workflow/WorkflowEngine.kt b/engine/src/main/kotlin/de/tuda/stg/securecoder/engine/workflow/WorkflowEngine.kt
index be4db6e..668108c 100644
--- a/engine/src/main/kotlin/de/tuda/stg/securecoder/engine/workflow/WorkflowEngine.kt
+++ b/engine/src/main/kotlin/de/tuda/stg/securecoder/engine/workflow/WorkflowEngine.kt
@@ -20,7 +20,7 @@ class WorkflowEngine (
llmClient: LlmClient,
guardians: List = emptyList(),
private val maxGuardianRetries: Int = 6,
- private val parseChangesAttempts: Int = 3,
+ private val parseChangesAttempts: Int = 5,
) : Engine {
private val promptEnrichRunner = PromptEnrichRunner(enricher)
private val editFiles = EditFilesLlmWrapper(llmClient)
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 4870f80..f7c48ea 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -19,4 +19,4 @@ include("app:benchmark-securityeval")
include("guardian:api")
include("guardian:codeql")
include("filesystem")
-include("app:openapi-bridge")
\ No newline at end of file
+include("app:openai-bridge")
\ No newline at end of file