import OpenAI from 'openai' import { logger } from '@/util/logger' // A/B 테스트 기본 모델 — 변경하려면 OPENROUTER_MODEL env 설정. // PDF 파일 입력을 지원하는 모델(anthropic/claude-fable-5 등)만 사용 가능하다. const DEFAULT_MODEL = 'anthropic/claude-fable-5' // gemini.js 의 FLOOR_PLAN_SCHEMA(SchemaType)와 동일 구조의 표준 JSON Schema — A/B 조건 대등 유지. const FLOOR_PLAN_JSON_SCHEMA = { type: 'object', properties: { outerline: { type: 'array', items: { type: 'object', properties: { x: { type: 'number' }, y: { type: 'number' }, }, required: ['x', 'y'], }, }, unit: { type: 'string' }, scale: { type: 'number' }, confidence: { type: 'number' }, notes: { type: 'string' }, selectedDrawingType: { type: 'string' }, roofShape: { type: 'object', properties: { type: { type: 'string' }, ridge: { type: 'string' }, flows: { type: 'array', items: { type: 'string' } }, confidence: { type: 'number' }, }, required: ['type', 'ridge', 'flows'], }, }, required: ['outerline', 'unit'], } export async function analyzeFloorPlan({ pdfBuffer, fileName, prompt }) { const apiKey = process.env.OPENROUTER_API_KEY const model = process.env.OPENROUTER_MODEL || DEFAULT_MODEL if (!apiKey) { throw new Error('OPENROUTER_API_KEY 가 설정되지 않았습니다.') } const client = new OpenAI({ apiKey, baseURL: 'https://openrouter.ai/api/v1', defaultHeaders: { 'HTTP-Referer': 'https://hanasys.jp', 'X-Title': 'qcast', }, timeout: 120000, }) const dataUrl = `data:application/pdf;base64,${pdfBuffer.toString('base64')}` const generateStarted = Date.now() const completion = await client.chat.completions.create({ model, temperature: 0.1, // Gemini responseSchema 와 대등한 스키마 강제 — OpenRouter 가 provider 별 변환을 관리한다. response_format: { type: 'json_schema', json_schema: { name: 'floor_plan', strict: true, schema: FLOOR_PLAN_JSON_SCHEMA }, }, // PDF 파싱 엔진을 native 로 명시 고정 — 모델 교체 시 OCR(mistral-ocr 등)로 조용히 떨어지는 것 방지. plugins: [{ id: 'file-parser', pdf: { engine: 'native' } }], messages: [ { role: 'user', content: [ { type: 'text', text: prompt }, { type: 'file', file: { filename: fileName || 'floor-plan.pdf', file_data: dataUrl }, }, ], }, ], }) const elapsed = Date.now() - generateStarted const choice = completion?.choices?.[0] if (choice?.error) { throw new Error(`OpenRouter provider error: ${choice.error.message}`) } const content = choice?.message?.content const raw = typeof content === 'string' ? content : Array.isArray(content) ? content.map((p) => p?.text ?? '').join('') : null if (typeof raw !== 'string') { logger.error('[pdf-analyzer/openrouter] no content', completion) throw new Error('OpenRouter 응답에 content 가 없습니다.') } const usage = completion?.usage ? { prompt_tokens: completion.usage.prompt_tokens, completion_tokens: completion.usage.completion_tokens, total_tokens: completion.usage.total_tokens, } : undefined logger.debug('[pdf-analyzer/openrouter] timing(ms)', { generate: elapsed }) logger.debug('[pdf-analyzer/openrouter] raw response', raw) logger.debug('[pdf-analyzer/openrouter] usage', completion?.usage) return { raw, usage, provider: 'openrouter' } }