fix(realtime-token): spend the session quota only after a token is minted and stop leaking provider errors
This commit is contained in:
parent
83c2deb561
commit
2251fe2da1
7 changed files with 673 additions and 155 deletions
82
server/supabase/functions/realtime-token/policy.ts
Normal file
82
server/supabase/functions/realtime-token/policy.ts
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
// 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<Record<RealtimeTier, readonly string[]>> = {
|
||||
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<Record<RealtimeTier, string | null>> = {
|
||||
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 } },
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue