fix: improve proxyai error handling and clean up code

This commit is contained in:
Carl-Robert Linnupuu 2026-01-13 10:58:07 +00:00
parent dbb6158162
commit 4c293dff73
8 changed files with 111 additions and 126 deletions

View file

@ -166,6 +166,16 @@ public class ChatMessageResponseBody extends JPanel {
});
}
public void displayInvalidCredential() {
String message = "Invalid API key. Open <a href=\"#\">Settings</a> to update your API key.";
displayErrorMessage(message, e -> {
if (e.getEventType() == ACTIVATED) {
ShowSettingsUtil.getInstance()
.showSettingsDialog(project, GeneralSettingsConfigurable.class);
}
});
}
public void displayQuotaExceeded() {
String message = "You exceeded your current quota, please check your plan and billing details, "
+ "or <a href=\"#CHANGE_PROVIDER\">change</a> to a different LLM provider.";

View file

@ -47,7 +47,7 @@ public class ChatToolWindowScrollablePanel extends ScrollablePanel {
It looks like you haven't configured your API key yet. Visit <a href="#OPEN_SETTINGS">ProxyAI settings</a> to do so.
</p>
<p style="margin-top: 4px; margin-bottom: 4px;">
Don't have an account? <a href="https://tryproxy.io/signin">Sign up</a> to get the most out of ProxyAI.
Don't have an account? <a href="https://tryproxy.io/signin">Sign up</a> to get started.
</p>
</html>""",
false,

View file

@ -39,7 +39,6 @@ import ee.carlrobert.codegpt.settings.service.ServiceType
import ee.carlrobert.codegpt.settings.service.custom.CustomServicesSettings
import ee.carlrobert.codegpt.toolwindow.agent.AgentCreditsEvent
import java.time.LocalDate
import kotlin.time.Duration.Companion.seconds
object AgentFactory {
@ -184,17 +183,7 @@ object AgentFactory {
}
private fun createRetryingExecutor(client: LLMClient): PromptExecutor {
val retryingClient = RetryingLLMClient(
client,
RetryConfig(
maxAttempts = 3,
initialDelay = 1.seconds,
maxDelay = 20.seconds,
backoffMultiplier = 2.0,
jitterFactor = 0.2
)
)
return SingleLLMPromptExecutor(retryingClient)
return SingleLLMPromptExecutor(RetryingLLMClient(client))
}
private fun createGeneralPurposeAgent(

View file

@ -168,6 +168,10 @@ object ProxyAIAgent {
}
}
onNodeExecutionFailed { ctx ->
logger.error(ctx.throwable) { "Node execution failed: $ctx" }
}
onToolCallStarting { ctx ->
val id = ctx.toolCallId ?: UUID.randomUUID().toString()
if (ctx.toolCallId == null) {
@ -196,6 +200,10 @@ object ProxyAIAgent {
onAgentCompleted { context ->
events.onAgentCompleted(context)
}
onAgentExecutionFailed {
logger.error(it.throwable) { "Agent execution failed: $it" }
}
}
}
return agent

View file

@ -16,6 +16,7 @@ import ai.koog.prompt.message.Message
import ai.koog.prompt.message.ResponseMetaInfo
import ai.koog.prompt.params.LLMParams
import ai.koog.prompt.streaming.StreamFrameFlowBuilder
import com.intellij.openapi.util.text.StringUtil
import ee.carlrobert.codegpt.settings.service.custom.CustomServiceChatCompletionSettingsState
import io.github.oshai.kotlinlogging.KotlinLogging
import io.ktor.client.*
@ -185,9 +186,9 @@ public class CustomOpenAILLMClient(
If the tool has no arguments, OpenRouter puts an empty string in the arguments instead of an empty object
But we always expect arguments to be a JSON object. Fixing this.
*/
content = toolCall.function.arguments
content = StringUtil.escapeStringCharacters(toolCall.function.arguments
.takeIf { it.isNotEmpty() }
?: "{}",
?: "{}"),
metaInfo = metaInfo
)
)

View file

@ -31,10 +31,7 @@ import kotlin.io.encoding.ExperimentalEncodingApi
public class ProxyAIClientSettings(
baseUrl: String = DEFAULT_BASE_URL,
chatCompletionsPath: String = DEFAULT_CHAT_COMPLETIONS_PATH,
timeoutConfig: ConnectionTimeoutConfig = ConnectionTimeoutConfig(
requestTimeoutMillis = 120_000,
socketTimeoutMillis = 120_000
)
timeoutConfig: ConnectionTimeoutConfig = ConnectionTimeoutConfig()
) : OpenAIBaseSettings(baseUrl, chatCompletionsPath, timeoutConfig) {
public companion object {
public const val DEFAULT_BASE_URL: String = "https://codegpt-api.carlrobert.ee"

View file

@ -67,19 +67,6 @@ internal class SingleRunStrategyProvider : AgentRunStrategyProvider {
llm.writeSession {
if (previousCheckpoint == null) {
projectInstructions?.let {
appendPrompt {
message(
Message.User(
it,
RequestMetaInfo(clock.now(), buildJsonObject {
put("cache_control", buildJsonObject {
put("type", JsonPrimitive("ephemeral"))
})
})
)
)
}
appendPrompt {
user(it)
}
@ -136,7 +123,7 @@ internal class SingleRunStrategyProvider : AgentRunStrategyProvider {
msg is Message.Tool.Call && msg.tool == "TodoWrite"
}
if (toolCallMessages >= 2 && !todoWriteToolUsed) {
if (toolCallMessages >= 3 && !todoWriteToolUsed) {
appendPrompt {
user("It seems that you haven't created a todo list yet. If the task on hand requires multiple steps then create a todo list to track your changes.")
}
@ -166,6 +153,7 @@ internal class SingleRunStrategyProvider : AgentRunStrategyProvider {
edge(nodeCallLLM forwardTo nodeExecuteTool onMultipleToolCalls { true })
edge(nodeCallLLM forwardTo nodeFinish onSingleAssistantResponse { true })
edge(nodeExecuteTool forwardTo nodeSendToolResult)
edge(nodeSendToolResult forwardTo nodeFinish onEmptyOutput { true })
edge(nodeSendToolResult forwardTo nodeFinish onSingleAssistantResponse { true })
edge(nodeSendToolResult forwardTo nodeExecuteTool onMultipleToolCalls { true })
}
@ -299,3 +287,17 @@ private infix fun <IncomingOutput, OutgoingInput> AIAgentEdgeBuilderIntermediate
.onCondition { messages -> block(messages[0]) }
.transformed { it[0].content }
}
@EdgeTransformationDslMarker
private infix fun <IncomingOutput, OutgoingInput> AIAgentEdgeBuilderIntermediate<IncomingOutput, List<Message.Response>, OutgoingInput>.onEmptyOutput(
block: suspend (Message.Response) -> Boolean
): AIAgentEdgeBuilderIntermediate<IncomingOutput, String, OutgoingInput> {
return onIsInstance(List::class)
.transformed { response ->
response.filter { item -> item is Message.Response && item !is Message.Reasoning }
.filterIsInstance<Message.Response>()
}
.onCondition { it.isEmpty() }
.onCondition { messages -> block(messages[0]) }
.transformed { it[0].content }
}

View file

@ -43,6 +43,10 @@ class AgentEventHandler(
private val mainToolCards = ConcurrentHashMap<String, ToolCallCard>()
private val toolOutputPublisher = ApplicationManager.getApplication()
.messageBus
.syncPublisher(AgentToolOutputNotifier.AGENT_TOOL_OUTPUT_TOPIC)
@Volatile
private var lastReportedPromptTokens: Long = 0
@ -90,14 +94,64 @@ class AgentEventHandler(
approvalQueue.clear()
currentQuestion = null
questionQueue.clear()
approvalContainer.removeAll()
approvalContainer.isVisible = false
clearApprovalContainer()
todoListPanel.clearTodos()
runViewHolder = null
subagentViewHolders.clear()
lastReportedPromptTokens = 0
}
private fun clearApprovalContainer() {
approvalContainer.removeAll()
approvalContainer.isVisible = false
approvalContainer.revalidate()
approvalContainer.repaint()
}
private fun monitorBackgroundProcessOutput(
bgId: String,
toolId: String,
onComplete: (() -> Unit)? = null
) {
serviceScope.launch {
try {
var outPos = 0
var errPos = 0
while (true) {
val po = BackgroundProcessManager.getOutput(bgId) ?: break
val stdout = po.stdout.toString()
val stderr = po.stderr.toString()
if (outPos < stdout.length) {
stdout.substring(outPos).split('\n').forEach { line ->
if (line.isNotEmpty()) toolOutputPublisher.toolOutput(
toolId,
line,
false
)
}
outPos = stdout.length
}
if (errPos < stderr.length) {
stderr.substring(errPos).split('\n').forEach { line ->
if (line.isNotEmpty()) toolOutputPublisher.toolOutput(
toolId,
line,
true
)
}
errPos = stderr.length
}
if (po.isComplete) break
delay(300)
}
} catch (ex: Exception) {
logger.warn("Failed to monitor background process output", ex)
} finally {
onComplete?.invoke()
}
}
}
fun setCurrentResponseBody(responseBody: ChatMessageResponseBody) {
currentResponseBody = responseBody
}
@ -135,11 +189,16 @@ class AgentEventHandler(
private fun handleProxyAIException(ex: KoogHttpClientException) {
when (ex.statusCode) {
403 -> {
401 -> {
currentResponseBody?.displayMissingCredential()
handleDone()
}
403 -> {
currentResponseBody?.displayInvalidCredential()
handleDone()
}
429 -> {
currentResponseBody?.displayCreditsExhausted()
handleDone()
@ -276,38 +335,9 @@ class AgentEventHandler(
if (bgId == null) {
mainToolCards.remove(keyFor(id))
} else {
val publisher =
ApplicationManager.getApplication()
.messageBus
.syncPublisher(AgentToolOutputNotifier.AGENT_TOOL_OUTPUT_TOPIC)
serviceScope.launch {
try {
var outPos = 0
var errPos = 0
while (true) {
val po = BackgroundProcessManager.getOutput(bgId) ?: break
val stdout = po.stdout.toString()
val stderr = po.stderr.toString()
if (outPos < stdout.length) {
stdout.substring(outPos).split('\n').forEach { line ->
if (line.isNotEmpty()) publisher.toolOutput(id, line, false)
}
outPos = stdout.length
}
if (errPos < stderr.length) {
stderr.substring(errPos).split('\n').forEach { line ->
if (line.isNotEmpty()) publisher.toolOutput(id, line, true)
}
errPos = stderr.length
}
if (po.isComplete) break
delay(300)
}
} catch (_: Exception) {
} finally {
runInEdt {
mainToolCards.remove(keyFor(id))
}
monitorBackgroundProcessOutput(bgId, id) {
runInEdt {
mainToolCards.remove(keyFor(id))
}
}
}
@ -405,44 +435,7 @@ class AgentEventHandler(
val bgId = (result as? BashTool.Result)?.bashId
if (bgId != null) {
val publisher =
ApplicationManager.getApplication()
.messageBus
.syncPublisher(AgentToolOutputNotifier.AGENT_TOOL_OUTPUT_TOPIC)
serviceScope.launch {
try {
var outPos = 0
var errPos = 0
while (true) {
val po = BackgroundProcessManager.getOutput(bgId) ?: break
val stdout = po.stdout.toString()
val stderr = po.stderr.toString()
if (outPos < stdout.length) {
stdout.substring(outPos).split('\n').forEach { line ->
if (line.isNotEmpty()) publisher.toolOutput(
childId,
line,
false
)
}
outPos = stdout.length
}
if (errPos < stderr.length) {
stderr.substring(errPos).split('\n').forEach { line ->
if (line.isNotEmpty()) publisher.toolOutput(
childId,
line,
true
)
}
errPos = stderr.length
}
if (po.isComplete) break
delay(300)
}
} catch (_: Exception) {
}
}
monitorBackgroundProcessOutput(bgId, childId)
}
}
scrollablePanel.update()
@ -486,10 +479,7 @@ class AgentEventHandler(
private fun maybeShowNextApproval() {
if (currentApproval != null) return
val next = approvalQueue.pollFirst() ?: run {
approvalContainer.removeAll()
approvalContainer.isVisible = false
approvalContainer.revalidate()
approvalContainer.repaint()
clearApprovalContainer()
maybeShowNextQuestion()
return
}
@ -536,10 +526,7 @@ class AgentEventHandler(
next.deferred.complete(true)
currentApproval = null
approvalContainer.removeAll()
approvalContainer.isVisible = false
approvalContainer.revalidate()
approvalContainer.repaint()
clearApprovalContainer()
runCatching {
project.service<AgentToolWindowContentManager>()
.setTabStatus(sessionId, AgentToolWindowTabbedPane.TabStatus.RUNNING)
@ -549,10 +536,7 @@ class AgentEventHandler(
onReject = {
next.deferred.complete(false)
currentApproval = null
approvalContainer.removeAll()
approvalContainer.isVisible = false
approvalContainer.revalidate()
approvalContainer.repaint()
clearApprovalContainer()
try {
project.service<AgentService>().cancelCurrentRun(sessionId)
@ -585,10 +569,7 @@ class AgentEventHandler(
onSubmit = { answers ->
next.deferred.complete(answers)
currentQuestion = null
approvalContainer.removeAll()
approvalContainer.isVisible = false
approvalContainer.revalidate()
approvalContainer.repaint()
clearApprovalContainer()
runCatching {
project.service<AgentToolWindowContentManager>()
.setTabStatus(sessionId, AgentToolWindowTabbedPane.TabStatus.RUNNING)
@ -599,10 +580,7 @@ class AgentEventHandler(
onCancel = {
next.deferred.complete(emptyMap())
currentQuestion = null
approvalContainer.removeAll()
approvalContainer.isVisible = false
approvalContainer.revalidate()
approvalContainer.repaint()
clearApprovalContainer()
runCatching {
project.service<AgentToolWindowContentManager>()
.setTabStatus(sessionId, AgentToolWindowTabbedPane.TabStatus.RUNNING)