mirror of
https://github.com/nad4-su/meeting-minutes.git
synced 2026-10-11 15:50:13 +09:00
feat: OpenAI 호환 프로바이더 지원 (OrcaRouter · OpenAI · 로컬 모델) (#12)
* feat: OpenAI 호환 프로바이더 지원 (OrcaRouter · OpenAI · 로컬 모델)
Gemini 직접 호출만 가능하던 구조를 프로바이더 레이어로 분리.
base URL과 모델 ID만 지정하면 OpenAI Chat Completions 형식을 따르는
엔드포인트는 모두 연결된다 (OrcaRouter, OpenAI, Ollama, LM Studio, vLLM).
- src/lib/providers/ 신설 (gemini / openai-compatible 어댑터 + 프리셋)
- /settings에 프로바이더 선택 UI 추가, 기존 Gemini 키는 자동 승계
- LLM_PROVIDER / LLM_BASE_URL / LLM_MODEL / LLM_API_KEY 환경변수 지원
- base URL은 http/https만 허용 (서버가 대신 fetch하므로 SSRF 방어)
- SECURITY.md에 프로바이더별 전송 경로와 SSRF 주의사항 문서화
- 테스트 102 → 141
DB에 저장되는 summaryMode 값('gemini')은 기존 레코드 호환을 위해 유지하고
UI 라벨만 "AI 요약"으로 변경했다.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* chore: 기본 Gemini 모델을 3.5 Flash Lite로 갱신
gemini-2.5-flash-lite → gemini-3.5-flash-lite. 같은 저비용·고속 티어의
최신 세대이며, 무료 등급 중심의 사용 프로필을 그대로 유지한다.
- providers/gemini.ts: DEFAULT_GEMINI_MODEL
- presets.ts: OrcaRouter 기본 모델과 모델 힌트 문구
- .env.example, README 참조 갱신
참고: 화자 분리를 지원하는 gemini-3.5-transcribe는 오디오 입력 전용
음성인식 모델이라 이 자리(텍스트 → 회의록 요약)에 넣을 수 없다.
오디오 캡처가 들어오는 시점에 STT 경로로 별도 추가해야 한다.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: csbae <csbae@RP-002.local>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
25 files changed
+1439
-269
No files matched your search
@@ -1,3 +1,9 @@
|
||||
import {
|
||||
complete,
|
||||
describeModel,
|
||||
toProviderSettings,
|
||||
type ProviderSettings,
|
||||
} from './providers'
|
||||
import {
|
||||
buildPrompt,
|
||||
resolveDepth,
|
||||
@@ -14,12 +20,14 @@ export interface MinutesInput {
|
||||
customPrompt?: string
|
||||
}
|
||||
|
||||
type GeminiResult =
|
||||
type AiMinutesResult =
|
||||
| { success: true; markdown: string }
|
||||
| { success: false; error: string }
|
||||
| { success: false; error: string; rateLimited?: boolean }
|
||||
|
||||
interface GeminiOptions {
|
||||
apiKey: string
|
||||
interface AiMinutesOptions {
|
||||
/** 프로바이더 설정. 생략하면 apiKey로 Gemini를 호출한다. */
|
||||
provider?: ProviderSettings
|
||||
apiKey?: string
|
||||
fetchFn?: typeof fetch
|
||||
}
|
||||
|
||||
@@ -49,15 +57,12 @@ ${content}
|
||||
`
|
||||
}
|
||||
|
||||
export async function generateGeminiMinutes(
|
||||
export async function generateAiMinutes(
|
||||
input: MinutesInput,
|
||||
options: GeminiOptions,
|
||||
): Promise<GeminiResult> {
|
||||
const { apiKey, fetchFn = fetch } = options
|
||||
|
||||
if (!apiKey || apiKey.trim().length === 0) {
|
||||
return { success: false, error: 'Gemini API 키가 필요합니다.' }
|
||||
}
|
||||
options: AiMinutesOptions,
|
||||
): Promise<AiMinutesResult> {
|
||||
const { fetchFn } = options
|
||||
const settings = toProviderSettings(options.provider, options.apiKey)
|
||||
|
||||
const templateId = input.template ?? 'meeting'
|
||||
const depth = resolveDepth(templateId, input.depth)
|
||||
@@ -69,34 +74,23 @@ export async function generateGeminiMinutes(
|
||||
customPrompt: input.customPrompt,
|
||||
})
|
||||
|
||||
const url =
|
||||
'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-lite:generateContent'
|
||||
const result = await complete(prompt, settings, { fetchFn })
|
||||
|
||||
try {
|
||||
const response = await fetchFn(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-goog-api-key': apiKey,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
contents: [{ parts: [{ text: prompt }] }],
|
||||
}),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Gemini API 호출 실패: ${response.status} ${response.statusText}`,
|
||||
}
|
||||
if (!result.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.error,
|
||||
...(result.rateLimited ? { rateLimited: true } : {}),
|
||||
}
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
const generatedText =
|
||||
data?.candidates?.[0]?.content?.parts?.[0]?.text ?? ''
|
||||
const generatedText = result.text.trim()
|
||||
if (generatedText.length === 0) {
|
||||
return { success: false, error: '요약 결과가 비어 있습니다.' }
|
||||
}
|
||||
|
||||
const dateStr = formatDate(input.date)
|
||||
const markdown = `# ${input.title}
|
||||
const dateStr = formatDate(input.date)
|
||||
const markdown = `# ${input.title}
|
||||
|
||||
**날짜**: ${dateStr}
|
||||
|
||||
@@ -106,15 +100,13 @@ ${generatedText}
|
||||
|
||||
---
|
||||
|
||||
*이 회의록은 Gemini AI를 활용하여 자동 생성되었습니다.*
|
||||
*이 회의록은 AI(${describeModel(settings)})를 활용하여 자동 생성되었습니다.*
|
||||
`
|
||||
|
||||
return { success: true, markdown }
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : '알 수 없는 오류'
|
||||
return {
|
||||
success: false,
|
||||
error: `Gemini API 호출 중 오류 발생: ${message}`,
|
||||
}
|
||||
}
|
||||
return { success: true, markdown }
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated `generateAiMinutes`를 사용하세요. Gemini 전용 호출부 하위 호환용입니다.
|
||||
*/
|
||||
export const generateGeminiMinutes = generateAiMinutes
|
||||
Reference in new issue
Block a user