From c47b661633844b609f82cc01735e4f382038abf8 Mon Sep 17 00:00:00 2001 From: David Scandurra <31861387+SplotyCode@users.noreply.github.com> Date: Tue, 30 Dec 2025 18:00:17 +0100 Subject: [PATCH 1/9] Add Docker support for openapi-bridge --- app/openapi-bridge/Dockerfile | 32 ++++++++++++++++++ app/openapi-bridge/README.md | 63 ++++++++++++++++------------------- 2 files changed, 61 insertions(+), 34 deletions(-) create mode 100644 app/openapi-bridge/Dockerfile diff --git a/app/openapi-bridge/Dockerfile b/app/openapi-bridge/Dockerfile new file mode 100644 index 0000000..1660c71 --- /dev/null +++ b/app/openapi-bridge/Dockerfile @@ -0,0 +1,32 @@ +FROM eclipse-temurin:21-jdk AS builder +WORKDIR /work + +COPY . . + +RUN chmod +x ./gradlew && ./gradlew --no-daemon :app:openapi-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/openapi-bridge/build/install/openapi-bridge /opt/openapi-bridge + +ENV PORT="8080" +ENV OLLAMA_BASE_URL="http://host.docker.internal:11434" +ENV OLLAMA_KEEP_ALIVE="5m" + +EXPOSE 8080 + +ENTRYPOINT ["/opt/openapi-bridge/bin/openapi-bridge"] diff --git a/app/openapi-bridge/README.md b/app/openapi-bridge/README.md index 69cbd3a..10a1a57 100644 --- a/app/openapi-bridge/README.md +++ b/app/openapi-bridge/README.md @@ -1,55 +1,50 @@ # 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. +## 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`) -## 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): - +### Build and run +Make sure you have Docker installed and are in the project root directory. ``` -./gradlew :app:openapi-bridge:run -DOPENROUTER_KEY=... -DMODEL=... +docker build -f app/openapi-bridge/Dockerfile -t openapi-bridge:latest . ``` -Or with environment variables: - +Run with Ollama on the host (macOS/Windows): ``` -export OPENROUTER_KEY=... -export MODEL=... -./gradlew :app:openapi-bridge:run +docker run --rm -p 8080:8080 \ + -e MODEL="llama3.1:8b" \ + openapi-bridge:latest ``` -On Windows +Run with Ollama on the host (Linux): +``` +docker run --rm -p 8080:8080 \ + --add-host=host.docker.internal:host-gateway \ + -e MODEL="llama3.1:8b" \ + openapi-bridge:latest +``` +Run using OpenRouter instead of Ollama: ``` -set OPENROUTER_KEY=... -set MODEL=... -gradlew.bat :app:openapi-bridge:run +docker run --rm -p 8080:8080 \ + -e OPENROUTER_KEY=... \ + -e MODEL=openai/gpt-oss-20b \ + openapi-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 (curl): - +Example request (from host): ``` curl -X POST "http://localhost:8080/v1/chat/completions" \ -H "Content-Type: application/json" \ From e73e479115a9d6abe94f5dc70cbe2f747065c803 Mon Sep 17 00:00:00 2001 From: David Scandurra <31861387+SplotyCode@users.noreply.github.com> Date: Tue, 30 Dec 2025 18:01:52 +0100 Subject: [PATCH 2/9] openapi -> openai --- app/{openapi-bridge => openai-bridge}/Dockerfile | 0 app/{openapi-bridge => openai-bridge}/README.md | 0 app/{openapi-bridge => openai-bridge}/build.gradle.kts | 0 .../java/de/tuda/stg/securecoder/openapibridge/AgentService.kt | 0 .../java/de/tuda/stg/securecoder/openapibridge/EngineFactory.kt | 0 .../src/main/java/de/tuda/stg/securecoder/openapibridge/Main.kt | 0 .../java/de/tuda/stg/securecoder/openapibridge/OpenAIRoutes.kt | 0 .../java/de/tuda/stg/securecoder/openapibridge/OpenApiModels.kt | 0 settings.gradle.kts | 2 +- 9 files changed, 1 insertion(+), 1 deletion(-) rename app/{openapi-bridge => openai-bridge}/Dockerfile (100%) rename app/{openapi-bridge => openai-bridge}/README.md (100%) rename app/{openapi-bridge => openai-bridge}/build.gradle.kts (100%) rename app/{openapi-bridge => openai-bridge}/src/main/java/de/tuda/stg/securecoder/openapibridge/AgentService.kt (100%) rename app/{openapi-bridge => openai-bridge}/src/main/java/de/tuda/stg/securecoder/openapibridge/EngineFactory.kt (100%) rename app/{openapi-bridge => openai-bridge}/src/main/java/de/tuda/stg/securecoder/openapibridge/Main.kt (100%) rename app/{openapi-bridge => openai-bridge}/src/main/java/de/tuda/stg/securecoder/openapibridge/OpenAIRoutes.kt (100%) rename app/{openapi-bridge => openai-bridge}/src/main/java/de/tuda/stg/securecoder/openapibridge/OpenApiModels.kt (100%) diff --git a/app/openapi-bridge/Dockerfile b/app/openai-bridge/Dockerfile similarity index 100% rename from app/openapi-bridge/Dockerfile rename to app/openai-bridge/Dockerfile diff --git a/app/openapi-bridge/README.md b/app/openai-bridge/README.md similarity index 100% rename from app/openapi-bridge/README.md rename to app/openai-bridge/README.md diff --git a/app/openapi-bridge/build.gradle.kts b/app/openai-bridge/build.gradle.kts similarity index 100% rename from app/openapi-bridge/build.gradle.kts rename to app/openai-bridge/build.gradle.kts 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/openapibridge/AgentService.kt similarity index 100% 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/openapibridge/AgentService.kt 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/openapibridge/EngineFactory.kt similarity index 100% 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/openapibridge/EngineFactory.kt 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/openapibridge/Main.kt similarity index 100% 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/openapibridge/Main.kt 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/openapibridge/OpenAIRoutes.kt similarity index 100% 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/openapibridge/OpenAIRoutes.kt 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/openapibridge/OpenApiModels.kt similarity index 100% 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/openapibridge/OpenApiModels.kt 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 From 6d411d8d90498d2bc47cd801216790f356a0294d Mon Sep 17 00:00:00 2001 From: David Scandurra <31861387+SplotyCode@users.noreply.github.com> Date: Tue, 30 Dec 2025 18:02:55 +0100 Subject: [PATCH 3/9] Dont inherit java properties for openai-bridge java exec task --- app/openai-bridge/build.gradle.kts | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/app/openai-bridge/build.gradle.kts b/app/openai-bridge/build.gradle.kts index e73063a..e69b6f4 100644 --- a/app/openai-bridge/build.gradle.kts +++ b/app/openai-bridge/build.gradle.kts @@ -17,18 +17,3 @@ 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) - } - } -} From 88d708eb8659a4386206b6f0ca0b2613732ca9f8 Mon Sep 17 00:00:00 2001 From: David Scandurra <31861387+SplotyCode@users.noreply.github.com> Date: Tue, 30 Dec 2025 18:18:53 +0100 Subject: [PATCH 4/9] Improve error handling of OllamaClient --- .idea/gradle.xml | 6 ++++++ .idea/kotlinc.xml | 3 ++- .../tuda/stg/securecoder/engine/llm/OllamaClient.kt | 12 ++++++++++++ 3 files changed, 20 insertions(+), 1 deletion(-) 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/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..61273f3 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,6 +11,7 @@ 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 @@ -49,6 +50,9 @@ class OllamaClient( val message: OllamaMsg ) + @Serializable + private data class OllamaError(val error: String) + override suspend fun chat( messages: List, params: GenerationParams @@ -81,6 +85,14 @@ class OllamaClient( } val body = resp.bodyAsText() println("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 From 8edfe63e4d1be125d92331e644f3f5897a400649 Mon Sep 17 00:00:00 2001 From: David Scandurra <31861387+SplotyCode@users.noreply.github.com> Date: Tue, 30 Dec 2025 18:19:49 +0100 Subject: [PATCH 5/9] Fix openai-bridge rename --- app/openai-bridge/Dockerfile | 6 +++--- app/openai-bridge/README.md | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/app/openai-bridge/Dockerfile b/app/openai-bridge/Dockerfile index 1660c71..953871a 100644 --- a/app/openai-bridge/Dockerfile +++ b/app/openai-bridge/Dockerfile @@ -3,7 +3,7 @@ WORKDIR /work COPY . . -RUN chmod +x ./gradlew && ./gradlew --no-daemon :app:openapi-bridge:installDist -x test +RUN chmod +x ./gradlew && ./gradlew --no-daemon :app:openai-bridge:installDist -x test FROM eclipse-temurin:21-jdk ENV DEBIAN_FRONTEND=noninteractive @@ -21,7 +21,7 @@ RUN mkdir -p /opt/codeql \ && rm /tmp/codeql.tgz ENV PATH="/opt/codeql:${PATH}" -COPY --from=builder /work/app/openapi-bridge/build/install/openapi-bridge /opt/openapi-bridge +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" @@ -29,4 +29,4 @@ ENV OLLAMA_KEEP_ALIVE="5m" EXPOSE 8080 -ENTRYPOINT ["/opt/openapi-bridge/bin/openapi-bridge"] +ENTRYPOINT ["/opt/openai-bridge/bin/openai-bridge"] diff --git a/app/openai-bridge/README.md b/app/openai-bridge/README.md index 10a1a57..074eb4a 100644 --- a/app/openai-bridge/README.md +++ b/app/openai-bridge/README.md @@ -15,22 +15,22 @@ This module contains the HTTP server. It exposes a minimal OpenAI-style `POST /v ### Build and run Make sure you have Docker installed and are in the project root directory. ``` -docker build -f app/openapi-bridge/Dockerfile -t openapi-bridge:latest . +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="llama3.1:8b" \ - openapi-bridge:latest + -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="llama3.1:8b" \ - openapi-bridge:latest + -e MODEL="gpt-oss:20b" \ + openai-bridge:latest ``` Run using OpenRouter instead of Ollama: @@ -38,7 +38,7 @@ Run using OpenRouter instead of Ollama: docker run --rm -p 8080:8080 \ -e OPENROUTER_KEY=... \ -e MODEL=openai/gpt-oss-20b \ - openapi-bridge:latest + openai-bridge:latest ``` ## Endpoint From 04a4515d8ace9f454b9e48b66e4e67428722068d Mon Sep 17 00:00:00 2001 From: David Scandurra <31861387+SplotyCode@users.noreply.github.com> Date: Tue, 30 Dec 2025 18:39:16 +0100 Subject: [PATCH 6/9] Add in the prompt to only create ONE file when using openai bridge --- .../java/de/tuda/stg/securecoder/openapibridge/AgentService.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/openai-bridge/src/main/java/de/tuda/stg/securecoder/openapibridge/AgentService.kt b/app/openai-bridge/src/main/java/de/tuda/stg/securecoder/openapibridge/AgentService.kt index 92bd6c9..cf1fd24 100644 --- a/app/openai-bridge/src/main/java/de/tuda/stg/securecoder/openapibridge/AgentService.kt +++ b/app/openai-bridge/src/main/java/de/tuda/stg/securecoder/openapibridge/AgentService.kt @@ -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") From d0281d2965c31765e1f9669e2f932664d19a18e1 Mon Sep 17 00:00:00 2001 From: David Scandurra <31861387+SplotyCode@users.noreply.github.com> Date: Tue, 30 Dec 2025 18:39:58 +0100 Subject: [PATCH 7/9] Rename openai bridge package --- app/openai-bridge/build.gradle.kts | 2 +- .../securecoder/{openapibridge => openaibridge}/AgentService.kt | 2 +- .../{openapibridge => openaibridge}/EngineFactory.kt | 2 +- .../stg/securecoder/{openapibridge => openaibridge}/Main.kt | 2 +- .../securecoder/{openapibridge => openaibridge}/OpenAIRoutes.kt | 2 +- .../{openapibridge => openaibridge}/OpenApiModels.kt | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) rename app/openai-bridge/src/main/java/de/tuda/stg/securecoder/{openapibridge => openaibridge}/AgentService.kt (97%) rename app/openai-bridge/src/main/java/de/tuda/stg/securecoder/{openapibridge => openaibridge}/EngineFactory.kt (96%) rename app/openai-bridge/src/main/java/de/tuda/stg/securecoder/{openapibridge => openaibridge}/Main.kt (94%) rename app/openai-bridge/src/main/java/de/tuda/stg/securecoder/{openapibridge => openaibridge}/OpenAIRoutes.kt (89%) rename app/openai-bridge/src/main/java/de/tuda/stg/securecoder/{openapibridge => openaibridge}/OpenApiModels.kt (94%) diff --git a/app/openai-bridge/build.gradle.kts b/app/openai-bridge/build.gradle.kts index e69b6f4..140ac01 100644 --- a/app/openai-bridge/build.gradle.kts +++ b/app/openai-bridge/build.gradle.kts @@ -15,5 +15,5 @@ dependencies { } application { - mainClass.set("de.tuda.stg.securecoder.openapibridge.MainKt") + mainClass.set("de.tuda.stg.securecoder.openaibridge.MainKt") } diff --git a/app/openai-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 97% rename from app/openai-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 cf1fd24..31a6d09 100644 --- a/app/openai-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 diff --git a/app/openai-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/openai-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/openai-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/openai-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/openai-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/openai-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/openai-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/openai-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/openai-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/openai-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/openai-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/openai-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 From 9e479e7e124997e5c9b3fac7d3aa8c40771b5e8d Mon Sep 17 00:00:00 2001 From: David Scandurra <31861387+SplotyCode@users.noreply.github.com> Date: Thu, 1 Jan 2026 18:21:28 +0100 Subject: [PATCH 8/9] Add debug logging to llm clients --- .../de/tuda/stg/securecoder/engine/llm/OllamaClient.kt | 6 ++++-- .../de/tuda/stg/securecoder/engine/llm/OpenRouterClient.kt | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) 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 61273f3..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 @@ -16,12 +16,14 @@ 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 @@ -77,14 +79,14 @@ 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 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..87e3335 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 @@ -20,12 +20,14 @@ import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json 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 +78,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,8 +91,7 @@ class OpenRouterClient ( if (!resp.status.isSuccess()) { error("OpenRouter Error ${resp.status.value}: $body") } - - println("OpenRouter response: $body") + logger.debug("Got llm response: {}", body) val obj = json.decodeFromString(body) val content = obj.choices.firstOrNull()?.message?.content ?: error("OpenRouter lieferte keine Antwortnachricht.") From 504a14e1d399732ae32aded7070e4e3a7d46b1ae Mon Sep 17 00:00:00 2001 From: David Scandurra <31861387+SplotyCode@users.noreply.github.com> Date: Fri, 2 Jan 2026 01:12:54 +0100 Subject: [PATCH 9/9] Improved response handling for smaller llms --- .../engine/file/edit/ApplyChanges.kt | 12 ++++-- .../engine/file/edit/EditFilesLlmWrapper.kt | 43 ++++++++++++++++--- .../securecoder/engine/file/edit/Matcher.kt | 31 +++++++++---- .../stg/securecoder/engine/llm/ChatMessage.kt | 2 +- .../engine/llm/OpenRouterClient.kt | 10 ++++- .../engine/workflow/WorkflowEngine.kt | 2 +- 6 files changed, 79 insertions(+), 21 deletions(-) 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/OpenRouterClient.kt b/engine/src/main/kotlin/de/tuda/stg/securecoder/engine/llm/OpenRouterClient.kt index 87e3335..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,6 +18,7 @@ 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 @@ -92,9 +93,14 @@ class OpenRouterClient ( error("OpenRouter Error ${resp.status.value}: $body") } logger.debug("Got llm response: {}", body) - val obj = json.decodeFromString(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)