// server/supabase/functions/realtime-token/policy.ts // Realtime 세션 발급 정책 — 순수 함수만 둔다 (IO·Deno 전역·네트워크 import 없음). // 티어별 허용 모델 결정과 OpenAI client_secrets 세션 설정 조립을 담당한다. import type { PlanQuotaTier } from '../_shared/core-contract.generated.ts' export type RealtimeTier = PlanQuotaTier export interface RealtimeTokenRequest { model?: unknown voice?: unknown instructions?: unknown } /** 티어별 허용 Realtime 모델 — free 차단, pro는 mini만, 상위 티어는 풀 모델까지 */ export const TIER_MODELS: Readonly> = { free: [], pro: ['gpt-realtime-2.1-mini'], pro_plus: ['gpt-realtime-2.1', 'gpt-realtime-2.1-mini'], team: ['gpt-realtime-2.1', 'gpt-realtime-2.1-mini'], enterprise: ['gpt-realtime-2.1', 'gpt-realtime-2.1-mini'], } export const DEFAULT_MODEL: Readonly> = { free: null, pro: 'gpt-realtime-2.1-mini', pro_plus: 'gpt-realtime-2.1', team: 'gpt-realtime-2.1', enterprise: 'gpt-realtime-2.1', } export const DEFAULT_VOICE = 'marin' export const MAX_INSTRUCTIONS_LENGTH = 2000 export type ModelResolution = | { allowed: true; model: string } | { allowed: false error: 'tier_not_allowed' | 'model_not_allowed' requested: string | null allowedModels: readonly string[] } /** 요청 모델(없으면 티어 기본값)이 티어에서 허용되는지 판정한다. */ export function resolveRealtimeModel(tier: RealtimeTier, requested: unknown): ModelResolution { const requestedModel = typeof requested === 'string' ? requested : null const allowedModels = TIER_MODELS[tier] ?? [] const model = requestedModel ?? DEFAULT_MODEL[tier] ?? null if (model && allowedModels.includes(model)) { return { allowed: true, model } } return { allowed: false, error: tier === 'free' ? 'tier_not_allowed' : 'model_not_allowed', requested: requestedModel, allowedModels, } } export interface RealtimeSessionConfig { type: 'realtime' model: string instructions?: string audio: { output: { voice: string } } } /** OpenAI client_secrets 요청의 session 객체를 조립한다. */ export function buildRealtimeSessionConfig( body: RealtimeTokenRequest, model: string, ): RealtimeSessionConfig { const instructions = typeof body.instructions === 'string' ? body.instructions.slice(0, MAX_INSTRUCTIONS_LENGTH) : undefined const voice = typeof body.voice === 'string' && body.voice ? body.voice : DEFAULT_VOICE return { type: 'realtime', model, ...(instructions ? { instructions } : {}), audio: { output: { voice } }, } }