diff --git a/README.md b/README.md index 45763f8..eeb1aa7 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,8 @@ npx prisma dev ルートディレクトリに .env または .env.local という名前のファイルを作成し、以下の内容を記述 ```dotenv API_KEY=GeminiAPIキー +OPENROUTER_API_KEY=OpenRouterAPIキー +OPENROUTER_MODEL=foo;bar BETTER_AUTH_URL=http://localhost:3000 DATABASE_URL="postgres://... (prisma devの出力)" GOOGLE_CLIENT_ID= @@ -26,7 +28,9 @@ GITHUB_CLIENT_ID= GITHUB_CLIENT_SECRET= ``` -* `API_KEY` はGeminiのAPIキーを作成して設定します。未設定の場合チャットが使えません +* チャット用にGeminiのAPIキーまたはOpenRouterのAPIキーのいずれかが必要です。未設定の場合チャットが使えません + * OpenRouterを使う場合は使用するモデルをセミコロン区切りで `OPENROUTER_MODEL` に設定してください (エラー時に2番目以降にフォールバックします) + * 両方設定されている場合はOpenRouterが使われます * `GITHUB_CLIENT_ID` `GITHUB_CLIENT_SECRET` はGitHub OAuthのクライアントIDとシークレットを設定します。未設定の場合「GitHubでログイン」が使えません。 作り方については https://www.better-auth.com/docs/authentication/github を参照 * `GOOGLE_CLIENT_ID` `GOOGLE_CLIENT_SECRET` はGoogle OAuthのクライアントIDとシークレットを設定します。未設定の場合「Googleでログイン」が使えません。 diff --git a/app/actions/gemini.ts b/app/actions/gemini.ts index 8dc7539..50fe707 100644 --- a/app/actions/gemini.ts +++ b/app/actions/gemini.ts @@ -2,19 +2,67 @@ import { GoogleGenAI } from "@google/genai"; -export async function generateContent(prompt: string, systemInstruction?: string) { +export async function generateContent( + prompt: string, + systemInstruction?: string +): Promise<{ text: string }> { + const openRouterApiKey = process.env.OPENROUTER_API_KEY; + const openRouterModel = process.env.OPENROUTER_MODEL; + + if (openRouterApiKey && openRouterModel) { + // Support semicolon-separated list of models for automatic fallback via + // OpenRouter's `models` array parameter. + const models = openRouterModel.split(";").map((m) => m.trim()).filter(Boolean); + + const messages: { role: string; content: string }[] = []; + if (systemInstruction) { + messages.push({ role: "system", content: systemInstruction }); + } + messages.push({ role: "user", content: prompt }); + + const response = await fetch("https://openrouter.ai/api/v1/chat/completions", { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${openRouterApiKey}`, + }, + body: JSON.stringify({ models, messages }), + }); + + if (!response.ok) { + const body = await response.text(); + throw new Error( + `OpenRouter APIエラー: ${response.status} ${response.statusText} - ${body}` + ); + } + + const data = (await response.json()) as { + choices?: { message?: { content?: string | null } }[]; + }; + const text = data.choices?.[0]?.message?.content; + if (!text) { + throw new Error("OpenRouterからの応答が空でした"); + } + return { text }; + } + const params = { model: "gemini-2.5-flash", contents: prompt, config: { systemInstruction, - } + }, }; const ai = new GoogleGenAI({ apiKey: process.env.API_KEY! }); try { - return await ai.models.generateContent(params); + const result = await ai.models.generateContent(params); + const text = result.text; + if (!text) { + throw new Error("Geminiからの応答が空でした"); + } + return { text }; } catch (e: unknown) { if (String(e).includes("User location is not supported")) { // For the new API, we can use httpOptions to set a custom baseUrl @@ -24,7 +72,12 @@ export async function generateContent(prompt: string, systemInstruction?: string baseUrl: "https://gemini-proxy.utcode.net", }, }); - return await aiWithProxy.models.generateContent(params); + const result = await aiWithProxy.models.generateContent(params); + const text = result.text; + if (!text) { + throw new Error("Geminiからの応答が空でした"); + } + return { text }; } else { throw e; }