Add openrouter
This commit is contained in:
parent
7c03c594c1
commit
36b686bbd4
8 changed files with 380 additions and 61 deletions
|
|
@ -1,15 +1,18 @@
|
|||
import { GoogleGenAI, Type, createUserContent, createPartFromUri } from "@google/genai";
|
||||
import { Quiz, Question, AnswerOption, GenerateQuizOptions, ProcessedDocument } from "../types";
|
||||
import { Quiz, Question, AnswerOption, GenerateQuizOptions, ProcessedDocument, AIProvider } from "../types";
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
const getClient = () => {
|
||||
const apiKey = process.env.API_KEY;
|
||||
if (!apiKey) {
|
||||
throw new Error("API_KEY environment variable is missing");
|
||||
const getGeminiClient = (apiKey?: string) => {
|
||||
const key = apiKey || process.env.API_KEY;
|
||||
if (!key) {
|
||||
throw new Error("Gemini API key is missing");
|
||||
}
|
||||
return new GoogleGenAI({ apiKey });
|
||||
return new GoogleGenAI({ apiKey: key });
|
||||
};
|
||||
|
||||
const OPENROUTER_API_URL = 'https://openrouter.ai/api/v1/chat/completions';
|
||||
const DEFAULT_OPENROUTER_MODEL = 'google/gemini-3-flash-preview';
|
||||
|
||||
const QUIZ_SCHEMA = {
|
||||
type: Type.OBJECT,
|
||||
properties: {
|
||||
|
|
@ -40,13 +43,35 @@ const QUIZ_SCHEMA = {
|
|||
required: ["title", "questions"]
|
||||
};
|
||||
|
||||
function buildPrompt(options: GenerateQuizOptions, hasDocuments: boolean): string {
|
||||
function buildPrompt(options: GenerateQuizOptions, hasDocuments: boolean, includeJsonExample: boolean = false): string {
|
||||
const questionCount = options.questionCount || 10;
|
||||
|
||||
const baseInstructions = `Create ${questionCount} engaging multiple-choice questions. Each question must have exactly 4 options, and exactly one correct answer. Vary the difficulty.
|
||||
let baseInstructions = `Create ${questionCount} engaging multiple-choice questions. Each question must have exactly 4 options, and exactly one correct answer. Vary the difficulty.
|
||||
|
||||
IMPORTANT: For each option's reason, write as if you are directly explaining facts - never reference "the document", "the text", "the material", or "the source". Write explanations as standalone factual statements.`;
|
||||
|
||||
if (includeJsonExample) {
|
||||
baseInstructions += `
|
||||
|
||||
You MUST respond with a single JSON object in this exact structure:
|
||||
{
|
||||
"title": "Quiz Title Here",
|
||||
"questions": [
|
||||
{
|
||||
"text": "Question text here?",
|
||||
"options": [
|
||||
{ "text": "Option A", "isCorrect": false, "reason": "Explanation why this is wrong" },
|
||||
{ "text": "Option B", "isCorrect": true, "reason": "Explanation why this is correct" },
|
||||
{ "text": "Option C", "isCorrect": false, "reason": "Explanation why this is wrong" },
|
||||
{ "text": "Option D", "isCorrect": false, "reason": "Explanation why this is wrong" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Return ONLY valid JSON with no additional text before or after.`;
|
||||
}
|
||||
|
||||
if (hasDocuments) {
|
||||
const topicContext = options.topic
|
||||
? ` Focus on aspects related to "${options.topic}".`
|
||||
|
|
@ -118,8 +143,38 @@ async function uploadNativeDocument(ai: GoogleGenAI, doc: ProcessedDocument): Pr
|
|||
return { uri: uploadedFile.uri, mimeType: uploadedFile.mimeType };
|
||||
}
|
||||
|
||||
export const generateQuiz = async (options: GenerateQuizOptions): Promise<Quiz> => {
|
||||
const ai = getClient();
|
||||
const JSON_SCHEMA_FOR_OPENROUTER = {
|
||||
type: "object",
|
||||
properties: {
|
||||
title: { type: "string", description: "A catchy title for the quiz" },
|
||||
questions: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "object",
|
||||
properties: {
|
||||
text: { type: "string", description: "The question text" },
|
||||
options: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "object",
|
||||
properties: {
|
||||
text: { type: "string" },
|
||||
isCorrect: { type: "boolean" },
|
||||
reason: { type: "string", description: "Brief explanation of why this answer is correct or incorrect" }
|
||||
},
|
||||
required: ["text", "isCorrect", "reason"]
|
||||
},
|
||||
}
|
||||
},
|
||||
required: ["text", "options"]
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ["title", "questions"]
|
||||
};
|
||||
|
||||
async function generateQuizWithGemini(options: GenerateQuizOptions): Promise<Quiz> {
|
||||
const ai = getGeminiClient(options.apiKey);
|
||||
|
||||
const docs = options.documents || [];
|
||||
const hasDocuments = docs.length > 0;
|
||||
|
|
@ -160,4 +215,91 @@ export const generateQuiz = async (options: GenerateQuizOptions): Promise<Quiz>
|
|||
|
||||
const data = JSON.parse(response.text);
|
||||
return transformToQuiz(data);
|
||||
}
|
||||
|
||||
async function generateQuizWithOpenRouter(options: GenerateQuizOptions): Promise<Quiz> {
|
||||
const apiKey = options.apiKey;
|
||||
if (!apiKey) {
|
||||
throw new Error("OpenRouter API key is missing");
|
||||
}
|
||||
|
||||
const docs = options.documents || [];
|
||||
const hasDocuments = docs.length > 0;
|
||||
const prompt = buildPrompt(options, hasDocuments, true);
|
||||
|
||||
let fullPrompt = prompt;
|
||||
|
||||
// For OpenRouter, we can't upload files directly - include text content in the prompt
|
||||
if (hasDocuments) {
|
||||
const textParts: string[] = [];
|
||||
for (const doc of docs) {
|
||||
if (doc.type === 'text') {
|
||||
textParts.push(doc.content as string);
|
||||
} else if (doc.type === 'native') {
|
||||
// For native documents, they should have been converted to text on the backend
|
||||
// If not, skip them with a warning
|
||||
console.warn('Native document type not supported with OpenRouter - document will be skipped');
|
||||
}
|
||||
}
|
||||
|
||||
if (textParts.length > 0) {
|
||||
fullPrompt = `Here is the content to create a quiz from:\n\n${textParts.join('\n\n---\n\n')}\n\n${prompt}`;
|
||||
}
|
||||
}
|
||||
|
||||
const model = options.openRouterModel || DEFAULT_OPENROUTER_MODEL;
|
||||
|
||||
const response = await fetch(OPENROUTER_API_URL, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
'HTTP-Referer': window.location.origin,
|
||||
'X-Title': 'Kaboot Quiz Generator',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model,
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
content: fullPrompt
|
||||
}
|
||||
],
|
||||
response_format: {
|
||||
type: 'json_schema',
|
||||
json_schema: {
|
||||
name: 'quiz',
|
||||
strict: true,
|
||||
schema: JSON_SCHEMA_FOR_OPENROUTER
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({ error: { message: 'Unknown error' } }));
|
||||
throw new Error(error.error?.message || `OpenRouter API error: ${response.status}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
const content = result.choices?.[0]?.message?.content;
|
||||
|
||||
// console.log('[OpenRouter] Raw response:', content);
|
||||
|
||||
if (!content) {
|
||||
throw new Error("Failed to generate quiz content from OpenRouter");
|
||||
}
|
||||
|
||||
const data = JSON.parse(content);
|
||||
return transformToQuiz(data);
|
||||
}
|
||||
|
||||
export const generateQuiz = async (options: GenerateQuizOptions): Promise<Quiz> => {
|
||||
const provider = options.aiProvider || 'gemini';
|
||||
|
||||
if (provider === 'openrouter') {
|
||||
return generateQuizWithOpenRouter(options);
|
||||
}
|
||||
|
||||
return generateQuizWithGemini(options);
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue