// server/supabase/functions/llm-proxy/index.ts // Anthropic Claude Messages API 프록시. // 요청: application/json { messages, system?, max_tokens?, model? } // 응답: JSON (non-stream) 또는 SSE (stream=true) import { corsHeaders, handleCorsPreflightRequest } from '../_shared/cors.ts' import { requireUser, authErrorResponse, type AuthError } from '../_shared/auth.ts' import { checkQuota, consumeQuota, createServiceRoleClient, type Tier } from '../_shared/quota.ts' interface LlmRequest { messages: Array<{ role: 'user' | 'assistant'; content: string }> system?: string max_tokens?: number model?: string stream?: boolean } /** 티어별 허용 모델 */ const TIER_MODELS: Record = { free: ['claude-haiku-4-5-20251001'], pro: ['claude-haiku-4-5-20251001', 'claude-sonnet-4-6'], team: ['claude-haiku-4-5-20251001', 'claude-sonnet-4-6', 'claude-opus-4-6'] } const DEFAULT_MODEL: Record = { free: 'claude-haiku-4-5-20251001', pro: 'claude-sonnet-4-6', team: 'claude-sonnet-4-6' } // @ts-expect-error — Deno 런타임 전역 Deno.serve(async (req: Request) => { const preflight = handleCorsPreflightRequest(req) if (preflight) return preflight if (req.method !== 'POST') { return new Response(JSON.stringify({ error: 'Method not allowed' }), { status: 405, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }) } try { const user = await requireUser(req) const serviceClient = createServiceRoleClient() const quota = await checkQuota(user.id, 'llm_process', serviceClient) if (!quota.allowed) { return new Response( JSON.stringify({ error: 'quota_exceeded', current: quota.current, limit: quota.limit, tier: quota.tier }), { status: 429, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } ) } const body = (await req.json()) as LlmRequest // 모델 선택 + 티어 검증 const requestedModel = body.model ?? DEFAULT_MODEL[quota.tier] if (!TIER_MODELS[quota.tier].includes(requestedModel)) { return new Response( JSON.stringify({ error: 'model_not_allowed', tier: quota.tier, requested: requestedModel, allowed: TIER_MODELS[quota.tier] }), { status: 403, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } ) } // Anthropic API 호출 — placeholder // // 실제 구현 시: // const anthropicKey = Deno.env.get('ANTHROPIC_API_KEY')! // const resp = await fetch('https://api.anthropic.com/v1/messages', { // method: 'POST', // headers: { // 'Content-Type': 'application/json', // 'x-api-key': anthropicKey, // 'anthropic-version': '2023-06-01' // }, // body: JSON.stringify({ // model: requestedModel, // max_tokens: body.max_tokens ?? 2048, // system: body.system, // messages: body.messages, // stream: body.stream ?? false // }) // }) // if (body.stream) { // return new Response(resp.body, { // headers: { ...corsHeaders, 'Content-Type': 'text/event-stream' } // }) // } // const data = await resp.json() // ... await consumeQuota(user.id, 'llm_process', serviceClient, 1) const placeholder = { id: `msg_placeholder_${Date.now()}`, model: requestedModel, role: 'assistant', content: [ { type: 'text', text: '[llm-proxy placeholder — Anthropic API not yet wired]' } ], stop_reason: 'end_turn', usage: { input_tokens: 0, output_tokens: 0 } } return new Response(JSON.stringify(placeholder), { status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }) } catch (err) { if (err && typeof err === 'object' && 'status' in err && 'message' in err) { return authErrorResponse(err as AuthError, corsHeaders) } const message = err instanceof Error ? err.message : 'Unknown error' return new Response(JSON.stringify({ error: message }), { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }) } })