refactor(api): simplify AI event route with extracted utilities and env-based model
Replace inline JSON extraction with the shared json-utils module, extract chat response content parsing into a dedicated helper, make the AI model configurable via AI_MODEL env var, and improve error messages for production safety.
This commit is contained in:
@@ -1,10 +1,11 @@
|
|||||||
|
import { headers } from "next/headers";
|
||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import { auth } from "@/auth";
|
import { auth } from "@/auth";
|
||||||
import { headers } from "next/headers";
|
import { extractJsonFromText } from "@/lib/json-utils";
|
||||||
import { openRouterClient } from "@/lib/openrouter-client";
|
import { openRouterClient } from "@/lib/openrouter-client";
|
||||||
import { AiEventRequestSchema, AiEventResponseSchema } from "@/lib/types";
|
import { AiEventRequestSchema, AiEventResponseSchema } from "@/lib/types";
|
||||||
|
|
||||||
const MODEL = "openai/gpt-5.4-mini";
|
const MODEL = process.env.AI_MODEL ?? "openai/gpt-5.4-mini";
|
||||||
|
|
||||||
const buildSystemPrompt = () => `
|
const buildSystemPrompt = () => `
|
||||||
You are an assistant that converts natural language and images into an ARRAY of calendar events.
|
You are an assistant that converts natural language and images into an ARRAY of calendar events.
|
||||||
@@ -42,7 +43,26 @@ const callTextOnly = async (systemPrompt: string, prompt: string) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const rawResponse = await result.getText();
|
const rawResponse = await result.getText();
|
||||||
return { rawResponse, startTime: performance.now() };
|
return { rawResponse };
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Extract the text content from an OpenRouter chat.send response. */
|
||||||
|
const extractContentFromChatResponse = (response: unknown): string => {
|
||||||
|
if (
|
||||||
|
typeof response === "object" &&
|
||||||
|
response !== null &&
|
||||||
|
"choices" in response
|
||||||
|
) {
|
||||||
|
const choices = (
|
||||||
|
response as {
|
||||||
|
choices: Array<{ message: { content: string | unknown } }>;
|
||||||
|
}
|
||||||
|
).choices;
|
||||||
|
const content = choices?.[0]?.message?.content;
|
||||||
|
if (typeof content === "string") return content;
|
||||||
|
if (content) return JSON.stringify(content);
|
||||||
|
}
|
||||||
|
throw new Error("Unexpected response format from AI chat API");
|
||||||
};
|
};
|
||||||
|
|
||||||
const callMultimodal = async (
|
const callMultimodal = async (
|
||||||
@@ -70,8 +90,6 @@ const callMultimodal = async (
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const startTime = performance.now();
|
|
||||||
|
|
||||||
const response = await openRouterClient.chat.send({
|
const response = await openRouterClient.chat.send({
|
||||||
chatRequest: {
|
chatRequest: {
|
||||||
model: MODEL,
|
model: MODEL,
|
||||||
@@ -79,32 +97,8 @@ const callMultimodal = async (
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const rawResponse =
|
const rawResponse = extractContentFromChatResponse(response);
|
||||||
typeof response === "object" &&
|
return { rawResponse };
|
||||||
"choices" in response &&
|
|
||||||
response.choices?.[0]?.message
|
|
||||||
? typeof response.choices[0].message.content === "string"
|
|
||||||
? response.choices[0].message.content
|
|
||||||
: JSON.stringify(response.choices[0].message.content)
|
|
||||||
: JSON.stringify(response);
|
|
||||||
|
|
||||||
return { rawResponse, startTime };
|
|
||||||
};
|
|
||||||
|
|
||||||
const extractJsonFromText = (text: string): unknown => {
|
|
||||||
try {
|
|
||||||
return JSON.parse(text);
|
|
||||||
} catch {
|
|
||||||
const codeBlockMatch = text.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
||||||
if (codeBlockMatch) {
|
|
||||||
return JSON.parse(codeBlockMatch[1].trim());
|
|
||||||
}
|
|
||||||
const arrayMatch = text.match(/\[[\s\S]*\]/);
|
|
||||||
if (arrayMatch) {
|
|
||||||
return JSON.parse(arrayMatch[0]);
|
|
||||||
}
|
|
||||||
throw new Error(`No JSON found in response: ${text.slice(0, 200)}`);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function POST(request: Request) {
|
export async function POST(request: Request) {
|
||||||
@@ -133,25 +127,19 @@ export async function POST(request: Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { prompt, imageBase64 } = parsedInput.data;
|
const { prompt, imageBase64 } = parsedInput.data;
|
||||||
const inputMode = imageBase64 ? "multimodal" : "text";
|
|
||||||
const systemPrompt = buildSystemPrompt();
|
const systemPrompt = buildSystemPrompt();
|
||||||
let rawResponse: string | undefined;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result =
|
const result = imageBase64
|
||||||
inputMode === "multimodal"
|
? await callMultimodal(systemPrompt, prompt, imageBase64)
|
||||||
? await callMultimodal(systemPrompt, prompt, imageBase64!)
|
: await callTextOnly(systemPrompt, prompt ?? "");
|
||||||
: await callTextOnly(systemPrompt, prompt!);
|
|
||||||
|
|
||||||
rawResponse = result.rawResponse;
|
const rawJson = extractJsonFromText(result.rawResponse);
|
||||||
|
|
||||||
const rawJson = extractJsonFromText(rawResponse);
|
|
||||||
const validated = AiEventResponseSchema.safeParse(rawJson);
|
const validated = AiEventResponseSchema.safeParse(rawJson);
|
||||||
|
|
||||||
if (!validated.success) {
|
if (!validated.success) {
|
||||||
console.error("AI response validation failed:", {
|
console.error("AI response validation failed:", {
|
||||||
issues: validated.error.flatten().fieldErrors,
|
issues: validated.error.flatten().fieldErrors,
|
||||||
rawResponse,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
@@ -167,10 +155,7 @@ export async function POST(request: Request) {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("AI Event Creation Error:", error);
|
console.error("AI Event Creation Error:", error);
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{
|
{ error: "Failed to process AI response. Please try again." },
|
||||||
error: "Failed to parse AI output",
|
|
||||||
raw: error instanceof Error ? error.message : String(error),
|
|
||||||
},
|
|
||||||
{ status: 500 },
|
{ status: 500 },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user