Files
HaBit/lib/openrouter.ts
2026-05-24 08:49:37 -06:00

113 lines
2.9 KiB
TypeScript

import {
finalizeNewAiTaskLabels,
sanitizePlausibleAiTaskLabels,
} from "@/lib/ai-habit-utils";
import {
extractSafeJsonTaskArray,
UnsafeAiOutputError,
} from "@/lib/ai-output-safety";
import {
AI_TASK_SYSTEM_PROMPT,
buildAiTaskUserPrompt,
} from "@/lib/ai-prompt-security";
import {
formatAiTaskContextForPrompt,
type AiTaskContext,
} from "@/lib/ai-task-context";
import {
buildGenerationUserPrompt,
MAX_DAILY_AI_TASKS,
type AiTaskGenerationOptions,
} from "@/lib/ai-task-generation";
import { mergeJournalHintsIntoLabels } from "@/lib/ai-journal-hints";
const OPENROUTER_URL = "https://openrouter.ai/api/v1/chat/completions";
export const DEFAULT_OPENROUTER_MODEL =
process.env.OPENROUTER_MODEL ?? "google/gemma-2-9b-it:free";
export async function generateAiTaskLabels(
context: AiTaskContext,
options?: AiTaskGenerationOptions,
): Promise<string[]> {
const apiKey = process.env.OPENROUTER_API_KEY;
if (!apiKey) {
throw new Error("OPENROUTER_API_KEY is not configured.");
}
const maxCount = options?.maxCount ?? MAX_DAILY_AI_TASKS;
const siteUrl =
process.env.NEXT_PUBLIC_SITE_URL ?? "http://localhost:3000";
const response = await fetch(OPENROUTER_URL, {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
"HTTP-Referer": siteUrl,
"X-Title": "HaBit",
},
body: JSON.stringify({
model: DEFAULT_OPENROUTER_MODEL,
temperature: 0.7,
messages: [
{
role: "system",
content: AI_TASK_SYSTEM_PROMPT,
},
{
role: "user",
content: buildAiTaskUserPrompt(
formatAiTaskContextForPrompt(context),
buildGenerationUserPrompt(context, options),
),
},
],
}),
});
if (!response.ok) {
const errorBody = await response.text();
throw new Error(
`OpenRouter request failed (${response.status}): ${errorBody.slice(0, 200)}`,
);
}
const payload = (await response.json()) as {
choices?: Array<{ message?: { content?: string } }>;
};
const content = payload.choices?.[0]?.message?.content?.trim();
if (!content) {
throw new UnsafeAiOutputError("OpenRouter returned an empty response.");
}
let parsedLabels: string[];
try {
parsedLabels = extractSafeJsonTaskArray(content);
} catch (error) {
if (error instanceof UnsafeAiOutputError) {
throw error;
}
throw new UnsafeAiOutputError("OpenRouter response failed safety validation.");
}
const labels = mergeJournalHintsIntoLabels(
sanitizePlausibleAiTaskLabels(parsedLabels),
context,
{ fillInOnly: true, maxCount, excludeLabels: options?.excludeLabels },
);
const finalized = finalizeNewAiTaskLabels(labels, options);
if (finalized.length === 0 && maxCount > 0) {
throw new Error("OpenRouter response did not include valid task labels.");
}
return finalized;
}