Skip to content

Commit 2a5fcea

Browse files
authored
Merge pull request #191 from ut-code/copilot/use-openrouter-api-keys
Add OpenRouter as a configurable AI provider with Gemini fallback
2 parents cf5de02 + f7185cd commit 2a5fcea

File tree

2 files changed

+62
-5
lines changed

2 files changed

+62
-5
lines changed

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ npx prisma dev
1818
ルートディレクトリに .env または .env.local という名前のファイルを作成し、以下の内容を記述
1919
```dotenv
2020
API_KEY=GeminiAPIキー
21+
OPENROUTER_API_KEY=OpenRouterAPIキー
22+
OPENROUTER_MODEL=foo;bar
2123
BETTER_AUTH_URL=http://localhost:3000
2224
DATABASE_URL="postgres://... (prisma devの出力)"
2325
GOOGLE_CLIENT_ID=
@@ -26,7 +28,9 @@ GITHUB_CLIENT_ID=
2628
GITHUB_CLIENT_SECRET=
2729
```
2830

29-
* `API_KEY` はGeminiのAPIキーを作成して設定します。未設定の場合チャットが使えません
31+
* チャット用にGeminiのAPIキーまたはOpenRouterのAPIキーのいずれかが必要です。未設定の場合チャットが使えません
32+
* OpenRouterを使う場合は使用するモデルをセミコロン区切りで `OPENROUTER_MODEL` に設定してください (エラー時に2番目以降にフォールバックします)
33+
* 両方設定されている場合はOpenRouterが使われます
3034
* `GITHUB_CLIENT_ID` `GITHUB_CLIENT_SECRET` はGitHub OAuthのクライアントIDとシークレットを設定します。未設定の場合「GitHubでログイン」が使えません。
3135
作り方については https://www.better-auth.com/docs/authentication/github を参照
3236
* `GOOGLE_CLIENT_ID` `GOOGLE_CLIENT_SECRET` はGoogle OAuthのクライアントIDとシークレットを設定します。未設定の場合「Googleでログイン」が使えません。

app/actions/gemini.ts

Lines changed: 57 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,67 @@
22

33
import { GoogleGenAI } from "@google/genai";
44

5-
export async function generateContent(prompt: string, systemInstruction?: string) {
5+
export async function generateContent(
6+
prompt: string,
7+
systemInstruction?: string
8+
): Promise<{ text: string }> {
9+
const openRouterApiKey = process.env.OPENROUTER_API_KEY;
10+
const openRouterModel = process.env.OPENROUTER_MODEL;
11+
12+
if (openRouterApiKey && openRouterModel) {
13+
// Support semicolon-separated list of models for automatic fallback via
14+
// OpenRouter's `models` array parameter.
15+
const models = openRouterModel.split(";").map((m) => m.trim()).filter(Boolean);
16+
17+
const messages: { role: string; content: string }[] = [];
18+
if (systemInstruction) {
19+
messages.push({ role: "system", content: systemInstruction });
20+
}
21+
messages.push({ role: "user", content: prompt });
22+
23+
const response = await fetch("https://openrouter.ai/api/v1/chat/completions", {
24+
method: "POST",
25+
headers: {
26+
"Content-Type": "application/json",
27+
Authorization: `Bearer ${openRouterApiKey}`,
28+
},
29+
body: JSON.stringify({ models, messages }),
30+
});
31+
32+
if (!response.ok) {
33+
const body = await response.text();
34+
throw new Error(
35+
`OpenRouter APIエラー: ${response.status} ${response.statusText} - ${body}`
36+
);
37+
}
38+
39+
const data = (await response.json()) as {
40+
choices?: { message?: { content?: string | null } }[];
41+
};
42+
const text = data.choices?.[0]?.message?.content;
43+
if (!text) {
44+
throw new Error("OpenRouterからの応答が空でした");
45+
}
46+
return { text };
47+
}
48+
649
const params = {
750
model: "gemini-2.5-flash",
851
contents: prompt,
952
config: {
1053
systemInstruction,
11-
}
54+
},
1255
};
1356

1457
const ai = new GoogleGenAI({ apiKey: process.env.API_KEY! });
1558

1659
try {
17-
return await ai.models.generateContent(params);
60+
const result = await ai.models.generateContent(params);
61+
const text = result.text;
62+
if (!text) {
63+
throw new Error("Geminiからの応答が空でした");
64+
}
65+
return { text };
1866
} catch (e: unknown) {
1967
if (String(e).includes("User location is not supported")) {
2068
// 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
2472
baseUrl: "https://gemini-proxy.utcode.net",
2573
},
2674
});
27-
return await aiWithProxy.models.generateContent(params);
75+
const result = await aiWithProxy.models.generateContent(params);
76+
const text = result.text;
77+
if (!text) {
78+
throw new Error("Geminiからの応答が空でした");
79+
}
80+
return { text };
2881
} else {
2982
throw e;
3083
}

0 commit comments

Comments
 (0)