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>(.*?)", setOf(RegexOption.MULTILINE, RegexOption.DOT_MATCHES_ALL)) + val regex = Regex( + "<$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