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
55
server/supabase/functions/realtime-token/openai-provider.ts
Normal file
55
server/supabase/functions/realtime-token/openai-provider.ts
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
// server/supabase/functions/realtime-token/openai-provider.ts
|
||||
// RealtimeProviderPort의 OpenAI 어댑터. 공급자 에러 본문은 읽지 않고 버린다
|
||||
// (조직 ID·마스킹된 키 힌트가 섞여 있을 수 있음) — 상태 코드만 돌려준다.
|
||||
|
||||
import type { RealtimeSessionConfig } from './policy.ts'
|
||||
import type { ClientSecretResult, RealtimeProviderPort } from './handler.ts'
|
||||
|
||||
export const OPENAI_CLIENT_SECRETS_URL = 'https://api.openai.com/v1/realtime/client_secrets'
|
||||
|
||||
export interface OpenAiRealtimeProviderOptions {
|
||||
/** 호출마다 읽는다 — 시크릿 교체가 재배포 없이 반영되도록. 빈 문자열이면 미설정. */
|
||||
readApiKey(): string
|
||||
fetchImpl?: typeof fetch
|
||||
}
|
||||
|
||||
export function createOpenAiRealtimeProvider(options: OpenAiRealtimeProviderOptions): RealtimeProviderPort {
|
||||
const fetchImpl = options.fetchImpl ?? fetch
|
||||
|
||||
return {
|
||||
isConfigured(): boolean {
|
||||
return options.readApiKey() !== ''
|
||||
},
|
||||
|
||||
async mintClientSecret(userId: string, session: RealtimeSessionConfig): Promise<ClientSecretResult> {
|
||||
const apiKey = options.readApiKey()
|
||||
if (!apiKey) return { ok: false, reason: 'network' }
|
||||
|
||||
let response: Response
|
||||
try {
|
||||
response = await fetchImpl(OPENAI_CLIENT_SECRETS_URL, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
'OpenAI-Safety-Identifier': userId,
|
||||
},
|
||||
body: JSON.stringify({ session }),
|
||||
})
|
||||
} catch {
|
||||
return { ok: false, reason: 'network' }
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
await response.body?.cancel().catch(() => undefined)
|
||||
return { ok: false, reason: 'upstream_status', status: response.status }
|
||||
}
|
||||
|
||||
const data: unknown = await response.json().catch(() => null)
|
||||
if (!data || typeof data !== 'object' || Array.isArray(data)) {
|
||||
return { ok: false, reason: 'invalid_response' }
|
||||
}
|
||||
return { ok: true, data: data as Record<string, unknown> }
|
||||
},
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue