From 421aad8d230ff1352133628f671b572678a0c7a1 Mon Sep 17 00:00:00 2001 From: Nicholai Date: Sat, 7 Feb 2026 02:06:41 -0700 Subject: [PATCH] fix(agent): add error handling and plugin tools (#53) * fix(agent): add error handling and plugin tools Replace useless try/catch with streaming-aware error callbacks. streamText is lazy so errors occur during streaming, not at call time. Now unwraps RetryError to extract the actual APICallError with status code and response body from OpenRouter. Also adds API key validation, model ID fallback, and includes plugin tools in the streamText call. * chore(db): fix file permissions on 0015 snapshot --------- Co-authored-by: Nicholai --- drizzle/meta/0015_snapshot.json | 0 src/app/api/agent/route.ts | 92 ++++++++++++++++++++++++++++----- 2 files changed, 80 insertions(+), 12 deletions(-) mode change 100644 => 100755 drizzle/meta/0015_snapshot.json diff --git a/drizzle/meta/0015_snapshot.json b/drizzle/meta/0015_snapshot.json old mode 100644 new mode 100755 diff --git a/src/app/api/agent/route.ts b/src/app/api/agent/route.ts index cedfd76..0be0785 100755 --- a/src/app/api/agent/route.ts +++ b/src/app/api/agent/route.ts @@ -2,12 +2,15 @@ import { streamText, stepCountIs, convertToModelMessages, + RetryError, type UIMessage, } from "ai" +import { APICallError } from "@ai-sdk/provider" import { getCloudflareContext } from "@opennextjs/cloudflare" import { resolveModelForUser, createModelFromId, + DEFAULT_MODEL_ID, } from "@/lib/agent/provider" import { agentTools } from "@/lib/agent/tools" import { githubTools } from "@/lib/agent/github-tools" @@ -28,14 +31,28 @@ export async function POST(req: Request): Promise { const db = getDb(env.DB) const envRecord = env as unknown as Record + const apiKey = envRecord.OPENROUTER_API_KEY + if (!apiKey) { + return new Response( + JSON.stringify({ + error: "OPENROUTER_API_KEY not configured", + }), + { + status: 500, + headers: { "Content-Type": "application/json" }, + } + ) + } + const [memories, registry] = await Promise.all([ loadMemoriesForPrompt(db, user.id), getRegistry(db, envRecord), ]) const pluginSections = registry.getPromptSections() + const pluginTools = registry.getTools() - const body = await req.json() as { + const body = (await req.json()) as { messages: UIMessage[] } @@ -47,14 +64,16 @@ export async function POST(req: Request): Promise { req.headers.get("x-conversation-id") || crypto.randomUUID() - const modelId = await resolveModelForUser( - db, - user.id - ) - const model = createModelFromId( - envRecord.OPENROUTER_API_KEY, - modelId - ) + let modelId = await resolveModelForUser(db, user.id) + if (!modelId || !modelId.includes("/")) { + console.error( + `Invalid model ID resolved: "${modelId}",` + + ` falling back to default` + ) + modelId = DEFAULT_MODEL_ID + } + + const model = createModelFromId(apiKey, modelId) const result = streamText({ model, @@ -67,9 +86,34 @@ export async function POST(req: Request): Promise { pluginSections, mode: "full", }), - messages: await convertToModelMessages(body.messages), - tools: { ...agentTools, ...githubTools }, + messages: await convertToModelMessages( + body.messages + ), + tools: { + ...agentTools, + ...githubTools, + ...pluginTools, + }, stopWhen: stepCountIs(10), + onError({ error }) { + const apiErr = unwrapAPICallError(error) + if (apiErr) { + console.error( + `Agent API error [model=${modelId}]`, + `status=${apiErr.statusCode}`, + `body=${apiErr.responseBody}` + ) + } else { + const msg = + error instanceof Error + ? error.message + : String(error) + console.error( + `Agent error [model=${modelId}]:`, + msg + ) + } + }, }) ctx.waitUntil( @@ -82,5 +126,29 @@ export async function POST(req: Request): Promise { ) ) - return result.toUIMessageStreamResponse() + return result.toUIMessageStreamResponse({ + onError(error) { + const apiErr = unwrapAPICallError(error) + if (apiErr) { + return ( + apiErr.responseBody ?? + `Provider error (${apiErr.statusCode})` + ) + } + return error instanceof Error + ? error.message + : "Unknown error" + }, + }) +} + +function unwrapAPICallError( + error: unknown +): APICallError | undefined { + if (APICallError.isInstance(error)) return error + if (RetryError.isInstance(error)) { + const last: unknown = error.lastError + if (APICallError.isInstance(last)) return last + } + return undefined }