Skip to content

Commit ddb38ea

Browse files
authored
Merge pull request #51 from DaleStudy/feat/openai-via-ai-gateway
OpenAI ํ˜ธ์ถœ์„ Cloudflare AI Gateway ๊ฒฝ์œ ๋กœ ์ „ํ™˜
2 parents e46b93d + 9b9d10c commit ddb38ea

11 files changed

Lines changed: 99 additions & 59 deletions

โ€ŽAGENTS.mdโ€Ž

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -347,8 +347,8 @@ https://github.com/settings/apps/dalestudy
347347
### 4. Worker Secrets ์„ค์ •
348348

349349
```bash
350-
# OpenAI API Key (AI ์ฝ”๋“œ ๋ฆฌ๋ทฐ์šฉ, ํ•„์ˆ˜)
351-
wrangler secret put OPENAI_API_KEY
350+
# AI Gateway ์ธ์ฆ ํ† ํฐ (AI ์ฝ”๋“œ ๋ฆฌ๋ทฐ์šฉ, ํ•„์ˆ˜)
351+
wrangler secret put AI_GATEWAY_TOKEN
352352

353353
# Webhook Secret (์„ ํƒ์‚ฌํ•ญ)
354354
wrangler secret put WEBHOOK_SECRET

โ€Žhandlers/complexity-analysis.jsโ€Ž

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
* - LLM : actualTime / actualSpace / feedback / suggestion / headerLine ๋งŒ ์ฑ…์ž„.
1111
*/
1212

13+
import { aiGatewayHeaders, OPENAI_CHAT_COMPLETIONS_URL } from "../utils/constants.js";
14+
1315
// โ”€โ”€ ์ƒ์ˆ˜ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
1416

1517
const FILE_DELIMITER = "=====";
@@ -240,20 +242,17 @@ export function composeSolution(modelSol, originalContent) {
240242
};
241243
}
242244

243-
export async function callComplexityAnalysis(fileEntries, apiKey) {
245+
export async function callComplexityAnalysis(fileEntries, gatewayToken) {
244246
const userPrompt = fileEntries
245247
.map(
246248
(f) =>
247249
`${FILE_DELIMITER} ${f.problemName} ${FILE_DELIMITER}\n\`\`\`\n${addLineNumbers(stripComplexityComments(f.content))}\n\`\`\``
248250
)
249251
.join("\n\n");
250252

251-
const response = await fetch("https://api.openai.com/v1/chat/completions", {
253+
const response = await fetch(OPENAI_CHAT_COMPLETIONS_URL, {
252254
method: "POST",
253-
headers: {
254-
Authorization: `Bearer ${apiKey}`,
255-
"Content-Type": "application/json",
256-
},
255+
headers: aiGatewayHeaders(gatewayToken),
257256
body: JSON.stringify({
258257
model: "gpt-5-nano",
259258
messages: [

โ€Žhandlers/complexity-analysis.test.jsโ€Ž

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -557,6 +557,26 @@ function solution() { return 0; }`;
557557
expect(captured).not.toContain("TC: O(n)");
558558
expect(captured).not.toContain("SC: O(1)");
559559
});
560+
561+
it("AI Gateway ์ธ์ฆ ํ—ค๋”์™€ ์žฌ์‹œ๋„ ํ—ค๋”๋ฅผ ํ•จ๊ป˜ ๋ณด๋‚ธ๋‹ค", async () => {
562+
let captured = null;
563+
globalThis.fetch = vi.fn().mockImplementation(async (url, opts) => {
564+
captured = opts.headers;
565+
return await makeOpenAIResponse([makeSingleSolutionAnalysis("two-sum")]);
566+
});
567+
568+
await callComplexityAnalysis(
569+
[{ problemName: "two-sum", content: PLAIN_SOURCE }],
570+
"fake-key"
571+
);
572+
573+
expect(captured).toMatchObject({
574+
"cf-aig-authorization": "Bearer fake-key",
575+
"cf-aig-max-attempts": "3",
576+
"cf-aig-retry-delay": "1000",
577+
"cf-aig-backoff": "exponential",
578+
});
579+
});
560580
});
561581

562582
// โ”€โ”€ renderComplexitySection โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

โ€Žhandlers/internal-dispatch.jsโ€Ž

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ async function handleTagPatterns(payload, appToken, env) {
7272
headSha,
7373
prData,
7474
appToken,
75-
env.OPENAI_API_KEY
75+
env.AI_GATEWAY_TOKEN
7676
);
7777
return corsResponse({ handler: "tag-patterns", result });
7878
}
@@ -85,7 +85,7 @@ async function handleLearningStatus(payload, appToken, env) {
8585
prNumber,
8686
username,
8787
appToken,
88-
env.OPENAI_API_KEY
88+
env.AI_GATEWAY_TOKEN
8989
);
9090
return corsResponse({ handler: "learning-status", result });
9191
}

โ€Žhandlers/internal-dispatch.test.jsโ€Ž

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ describe("handleInternalDispatch โ€” ์ธ์ฆ", () => {
8888
});
8989

9090
describe("handleInternalDispatch โ€” ๋ผ์šฐํŒ…", () => {
91-
const env = { INTERNAL_SECRET: VALID_SECRET, OPENAI_API_KEY: "fake-openai" };
91+
const env = { INTERNAL_SECRET: VALID_SECRET, AI_GATEWAY_TOKEN: "fake-gateway-token" };
9292

9393
beforeEach(() => {
9494
vi.clearAllMocks();
@@ -123,7 +123,7 @@ describe("handleInternalDispatch โ€” ๋ผ์šฐํŒ…", () => {
123123
"abc123",
124124
prData,
125125
"fake-token",
126-
"fake-openai"
126+
"fake-gateway-token"
127127
);
128128
expect(postLearningStatus).not.toHaveBeenCalled();
129129
});
@@ -154,7 +154,7 @@ describe("handleInternalDispatch โ€” ๋ผ์šฐํŒ…", () => {
154154
42,
155155
"testuser",
156156
"fake-token",
157-
"fake-openai"
157+
"fake-gateway-token"
158158
);
159159
expect(tagPatterns).not.toHaveBeenCalled();
160160
});
@@ -200,7 +200,7 @@ describe("handleInternalDispatch โ€” ๋ผ์šฐํŒ…", () => {
200200
});
201201

202202
describe("handleInternalDispatch โ€” ์—๋Ÿฌ ์ฒ˜๋ฆฌ", () => {
203-
const env = { INTERNAL_SECRET: VALID_SECRET, OPENAI_API_KEY: "fake-openai" };
203+
const env = { INTERNAL_SECRET: VALID_SECRET, AI_GATEWAY_TOKEN: "fake-gateway-token" };
204204

205205
beforeEach(() => {
206206
vi.clearAllMocks();

โ€Žhandlers/webhooks.jsโ€Ž

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,7 @@ async function handlePullRequestEvent(payload, env, ctx) {
260260
}
261261

262262
// AI ํ•ธ๋“ค๋Ÿฌ๋“ค์„ ๋ณ„๋„ Worker ํ˜ธ์ถœ๋กœ ๋””์ŠคํŒจ์น˜ (๊ฐ๊ฐ ๋…๋ฆฝ์ ์ธ subrequest ์˜ˆ์‚ฐ)
263-
if (env.OPENAI_API_KEY && env.INTERNAL_SECRET && env.WORKER_URL) {
263+
if (env.AI_GATEWAY_TOKEN && env.INTERNAL_SECRET && env.WORKER_URL) {
264264
const baseUrl = env.WORKER_URL;
265265

266266
const dispatchHeaders = {
@@ -300,7 +300,7 @@ async function handlePullRequestEvent(payload, env, ctx) {
300300
);
301301

302302
console.log(`[handlePullRequestEvent] Dispatched 2 AI handlers for PR #${prNumber}`);
303-
} else if (env.OPENAI_API_KEY) {
303+
} else if (env.AI_GATEWAY_TOKEN) {
304304
// INTERNAL_SECRET/WORKER_URL ๋ฏธ์„ค์ • ์‹œ ๊ธฐ์กด ๋ฐฉ์‹์œผ๋กœ ํด๋ฐฑ (๋™์ผ invocation์—์„œ ์ˆœ์ฐจ ์‹คํ–‰)
305305
console.warn("[handlePullRequestEvent] INTERNAL_SECRET or WORKER_URL not set, running handlers in-process");
306306

@@ -312,14 +312,14 @@ async function handlePullRequestEvent(payload, env, ctx) {
312312
pr.head.sha,
313313
pr,
314314
appToken,
315-
env.OPENAI_API_KEY
315+
env.AI_GATEWAY_TOKEN
316316
);
317317
} catch (error) {
318318
console.error(`[handlePullRequestEvent] tagPatterns failed: ${error.message}`);
319319
}
320320

321321
try {
322-
await postLearningStatus(repoOwner, repoName, prNumber, pr.user.login, appToken, env.OPENAI_API_KEY);
322+
await postLearningStatus(repoOwner, repoName, prNumber, pr.user.login, appToken, env.AI_GATEWAY_TOKEN);
323323
} catch (error) {
324324
console.error(`[handlePullRequestEvent] learningStatus failed: ${error.message}`);
325325
}
@@ -483,9 +483,9 @@ async function handleIssueCommentEvent(payload, env) {
483483
// AI ์ฝ”๋“œ ๋ฆฌ๋ทฐ ์š”์ฒญ ์ฒ˜๋ฆฌ
484484
console.log(`AI review requested for PR #${prNumber}${mention.userRequest ? ` - Request: ${mention.userRequest}` : ""}`);
485485

486-
// OPENAI_API_KEY ํ™•์ธ
487-
if (!env.OPENAI_API_KEY) {
488-
console.log("OPENAI_API_KEY not configured");
486+
// AI_GATEWAY_TOKEN ํ™•์ธ
487+
if (!env.AI_GATEWAY_TOKEN) {
488+
console.log("AI_GATEWAY_TOKEN not configured");
489489
return corsResponse({ message: "AI review not configured" });
490490
}
491491

@@ -508,7 +508,7 @@ async function handleIssueCommentEvent(payload, env) {
508508
issue.title,
509509
issue.body,
510510
appToken,
511-
env.OPENAI_API_KEY,
511+
env.AI_GATEWAY_TOKEN,
512512
mention.userRequest
513513
);
514514

@@ -553,9 +553,9 @@ async function handlePullRequestReviewCommentEvent(payload, env) {
553553

554554
console.log(`AI review requested for PR #${prNumber} (review comment)${mention.userRequest ? ` - Request: ${mention.userRequest}` : ""}`);
555555

556-
// OPENAI_API_KEY ํ™•์ธ
557-
if (!env.OPENAI_API_KEY) {
558-
console.log("OPENAI_API_KEY not configured");
556+
// AI_GATEWAY_TOKEN ํ™•์ธ
557+
if (!env.AI_GATEWAY_TOKEN) {
558+
console.log("AI_GATEWAY_TOKEN not configured");
559559
return corsResponse({ message: "AI review not configured" });
560560
}
561561

@@ -581,7 +581,7 @@ async function handlePullRequestReviewCommentEvent(payload, env) {
581581
pullRequest.title,
582582
pullRequest.body,
583583
appToken,
584-
env.OPENAI_API_KEY,
584+
env.AI_GATEWAY_TOKEN,
585585
mention.userRequest,
586586
comment.id // ์Šค๋ ˆ๋“œ ๋‹ต๋ณ€์œผ๋กœ ์ž‘์„ฑ
587587
);

โ€Žhandlers/webhooks.test.jsโ€Ž

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -264,10 +264,10 @@ describe("handlePullRequestEvent โ€” AI ํ•ธ๋“ค๋Ÿฌ ๋””์ŠคํŒจ์น˜", () => {
264264
});
265265
});
266266

267-
it("OPENAI_API_KEY, INTERNAL_SECRET, WORKER_URL ์ด ๋ชจ๋‘ ์„ค์ •๋˜๋ฉด ctx.waitUntil ๋กœ self-fetch 2 ํšŒ๋ฅผ ๋””์ŠคํŒจ์น˜ํ•œ๋‹ค", async () => {
267+
it("AI_GATEWAY_TOKEN, INTERNAL_SECRET, WORKER_URL ์ด ๋ชจ๋‘ ์„ค์ •๋˜๋ฉด ctx.waitUntil ๋กœ self-fetch 2 ํšŒ๋ฅผ ๋””์ŠคํŒจ์น˜ํ•œ๋‹ค", async () => {
268268
const ctx = makeCtx();
269269
const env = {
270-
OPENAI_API_KEY: "fake-openai",
270+
AI_GATEWAY_TOKEN: "fake-gateway-token",
271271
INTERNAL_SECRET: "fake-secret",
272272
WORKER_URL: "https://worker.test",
273273
};
@@ -299,7 +299,7 @@ describe("handlePullRequestEvent โ€” AI ํ•ธ๋“ค๋Ÿฌ ๋””์ŠคํŒจ์น˜", () => {
299299
it("INTERNAL_SECRET ์ด ์—†์œผ๋ฉด in-process ํ•ธ๋“ค๋Ÿฌ ํ˜ธ์ถœ๋กœ ํด๋ฐฑํ•œ๋‹ค", async () => {
300300
const ctx = makeCtx();
301301
const env = {
302-
OPENAI_API_KEY: "fake-openai",
302+
AI_GATEWAY_TOKEN: "fake-gateway-token",
303303
WORKER_URL: "https://worker.test",
304304
};
305305

@@ -323,7 +323,7 @@ describe("handlePullRequestEvent โ€” AI ํ•ธ๋“ค๋Ÿฌ ๋””์ŠคํŒจ์น˜", () => {
323323
it("WORKER_URL ์ด ์—†์œผ๋ฉด in-process ํ•ธ๋“ค๋Ÿฌ ํ˜ธ์ถœ๋กœ ํด๋ฐฑํ•œ๋‹ค", async () => {
324324
const ctx = makeCtx();
325325
const env = {
326-
OPENAI_API_KEY: "fake-openai",
326+
AI_GATEWAY_TOKEN: "fake-gateway-token",
327327
INTERNAL_SECRET: "fake-secret",
328328
};
329329

@@ -339,7 +339,7 @@ describe("handlePullRequestEvent โ€” AI ํ•ธ๋“ค๋Ÿฌ ๋””์ŠคํŒจ์น˜", () => {
339339
expect(postLearningStatus).toHaveBeenCalledTimes(1);
340340
});
341341

342-
it("OPENAI_API_KEY ๊ฐ€ ์—†์œผ๋ฉด ๋””์ŠคํŒจ์น˜๋„ ํ•ธ๋“ค๋Ÿฌ ํ˜ธ์ถœ๋„ ํ•˜์ง€ ์•Š๋Š”๋‹ค", async () => {
342+
it("AI_GATEWAY_TOKEN ๊ฐ€ ์—†์œผ๋ฉด ๋””์ŠคํŒจ์น˜๋„ ํ•ธ๋“ค๋Ÿฌ ํ˜ธ์ถœ๋„ ํ•˜์ง€ ์•Š๋Š”๋‹ค", async () => {
343343
const ctx = makeCtx();
344344
const env = {
345345
INTERNAL_SECRET: "fake-secret",

โ€Žtests/subrequest-budget.test.jsโ€Ž

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ describe("subrequest ์˜ˆ์‚ฐ โ€” ํ•ธ๋“ค๋Ÿฌ๋ณ„ invocation", () => {
5959
return okText("function solution() { return 0; }");
6060
}
6161

62-
if (urlStr.includes("openai.com/v1/chat/completions")) {
62+
if (urlStr.includes("/openai/chat/completions")) {
6363
const body = JSON.parse(opts.body);
6464
const isComplexity = body.messages[0].content.includes(
6565
"์‹œ๊ฐ„/๊ณต๊ฐ„ ๋ณต์žก๋„๋ฅผ ๋ถ„์„"
@@ -225,7 +225,7 @@ describe("subrequest ์˜ˆ์‚ฐ โ€” ํ•ธ๋“ค๋Ÿฌ๋ณ„ invocation", () => {
225225
return okText("function solution() { return 0; }");
226226
}
227227

228-
if (urlStr.includes("openai.com/v1/chat/completions")) {
228+
if (urlStr.includes("/openai/chat/completions")) {
229229
return okJson({
230230
choices: [
231231
{

โ€Žtests/tag-patterns.test.jsโ€Ž

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ function makeFetchMock({
9999
return okText(rawContent);
100100
}
101101

102-
if (urlStr.includes("openai.com/v1/chat/completions")) {
102+
if (urlStr.includes("/openai/chat/completions")) {
103103
const body = JSON.parse(opts.body);
104104
const isComplexity = body.messages[0].content.includes(
105105
"์‹œ๊ฐ„/๊ณต๊ฐ„ ๋ณต์žก๋„๋ฅผ ๋ถ„์„"
@@ -427,7 +427,7 @@ describe("tagPatterns โ€” ๋ ˆ๊ฑฐ์‹œ ๋‹จ๋… ๋ณต์žก๋„ issue comment ๋งˆ์ด๊ทธ๋ ˆ
427427
if (urlStr.startsWith("https://raw.example.com/")) {
428428
return okText(PLAIN_SOURCE);
429429
}
430-
if (urlStr.includes("openai.com")) {
430+
if (urlStr.includes("/openai/chat/completions")) {
431431
const body = JSON.parse(opts.body);
432432
const isComplexity = body.messages[0].content.includes(
433433
"์‹œ๊ฐ„/๊ณต๊ฐ„ ๋ณต์žก๋„๋ฅผ ๋ถ„์„"
@@ -495,7 +495,7 @@ describe("tagPatterns โ€” ๋ ˆ๊ฑฐ์‹œ ๋‹จ๋… ๋ณต์žก๋„ issue comment ๋งˆ์ด๊ทธ๋ ˆ
495495
if (urlStr.startsWith("https://raw.example.com/")) {
496496
return okText(PLAIN_SOURCE);
497497
}
498-
if (urlStr.includes("openai.com")) {
498+
if (urlStr.includes("/openai/chat/completions")) {
499499
const body = JSON.parse(opts.body);
500500
const isComplexity = body.messages[0].content.includes(
501501
"์‹œ๊ฐ„/๊ณต๊ฐ„ ๋ณต์žก๋„๋ฅผ ๋ถ„์„"
@@ -557,7 +557,7 @@ describe("tagPatterns โ€” ๋ ˆ๊ฑฐ์‹œ ๋‹จ๋… ๋ณต์žก๋„ issue comment ๋งˆ์ด๊ทธ๋ ˆ
557557
if (urlStr.startsWith("https://raw.example.com/")) {
558558
return okText(PLAIN_SOURCE);
559559
}
560-
if (urlStr.includes("openai.com")) {
560+
if (urlStr.includes("/openai/chat/completions")) {
561561
const body = JSON.parse(opts.body);
562562
const isComplexity = body.messages[0].content.includes(
563563
"์‹œ๊ฐ„/๊ณต๊ฐ„ ๋ณต์žก๋„๋ฅผ ๋ถ„์„"

โ€Žutils/constants.jsโ€Ž

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,32 @@ export const ALLOWED_REPO = "leetcode-study";
1515

1616
// ๋ผ๋ฒจ
1717
export const MAINTENANCE_LABEL = "maintenance";
18+
19+
// AI Gateway ๋ฅผ ๊ฑฐ์น˜๋Š” OpenAI ์—”๋“œํฌ์ธํŠธ.
20+
// OpenAI ํ‚ค๋Š” ๊ฒŒ์ดํŠธ์›จ์ด์— ์ €์žฅ๋ผ ์žˆ์–ด ์š”์ฒญ์— ์‹ฃ์ง€ ์•Š๋Š”๋‹ค. ํ‚ค๋ฅผ ์‹ค์œผ๋ฉด
21+
// ๊ฒŒ์ดํŠธ์›จ์ด๊ฐ€ ์ €์žฅ๋œ ํ‚ค๋ฅผ ๋ผ์›Œ ๋„ฃ์ง€ ์•Š๊ณ  ๊ทธ๋Œ€๋กœ ์ „๋‹ฌํ•œ๋‹ค.
22+
const AI_GATEWAY_ACCOUNT_ID = "86aa227176a624680f7d34e691472576";
23+
const AI_GATEWAY_ID = "dalestudy";
24+
export const OPENAI_CHAT_COMPLETIONS_URL = `https://gateway.ai.cloudflare.com/v1/${AI_GATEWAY_ACCOUNT_ID}/${AI_GATEWAY_ID}/openai/chat/completions`;
25+
26+
// ๊ฒŒ์ดํŠธ์›จ์ด์— ์žฌ์‹œ๋„๋ฅผ ๋งก๊ธด๋‹ค. Worker ์•ˆ์—์„œ ์žฌ์‹œ๋„ํ•˜๋ฉด CPU ์˜ˆ์‚ฐ์„ ๊นŽ์ง€๋งŒ,
27+
// ์—ฃ์ง€์—์„œ ์žฌ์‹œ๋„ํ•˜๋ฉด ์‘๋‹ต์„ ๊ธฐ๋‹ค๋ฆฌ๋Š” ์‹œ๊ฐ„์ผ ๋ฟ์ด๋‹ค.
28+
const AI_GATEWAY_RETRY_HEADERS = {
29+
"cf-aig-max-attempts": "3",
30+
"cf-aig-retry-delay": "1000",
31+
"cf-aig-backoff": "exponential",
32+
};
33+
34+
/**
35+
* AI Gateway ๊ฒฝ์œ  OpenAI ํ˜ธ์ถœ์— ๋ถ™์ผ ํ—ค๋”.
36+
*
37+
* @param {string} gatewayToken - AI Gateway ์ธ์ฆ ํ† ํฐ
38+
* @returns {Record<string, string>}
39+
*/
40+
export function aiGatewayHeaders(gatewayToken) {
41+
return {
42+
"cf-aig-authorization": `Bearer ${gatewayToken}`,
43+
"Content-Type": "application/json",
44+
...AI_GATEWAY_RETRY_HEADERS,
45+
};
46+
}

0 commit comments

Comments
ย (0)