Skip to content

Commit 4397e60

Browse files
committed
Merge remote-tracking branch 'origin/main' into ai-diff
2 parents 34509f8 + 2a5fcea commit 4397e60

File tree

3 files changed

+65
-8
lines changed

3 files changed

+65
-8
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/chatActions.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -193,14 +193,14 @@ export async function askAI(params: ChatParams): Promise<ChatResult> {
193193
if (!text) {
194194
throw new Error("AIからの応答が空でした");
195195
}
196-
let targetSectionId = text.split(/-{3,}/)[0].trim() as SectionId;
196+
let targetSectionId = text.split(/\n-{3,}\n/)[0].trim() as SectionId;
197197
if (!targetSectionId) {
198198
targetSectionId = introSectionId(path);
199199
}
200-
const responseMessage = text.split(/-{3,}/)[1].trim();
200+
const responseMessage = text.split(/\n-{3,}\n/)[1].trim();
201201
const diffRaw: CreateChatDiff[] = [];
202202
for (const m of text
203-
.split(/-{3,}/)[2]
203+
.split(/\n-{3,}\n/)[2]
204204
.matchAll(
205205
/<{3,}\s*SEARCH\n([\s\S]*?)\n={3,}\n([\s\S]*?)\n>{3,}\s*REPLACE/g
206206
)) {

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)