fix(llm): keep system prompts on premium chat, reject incomplete streams, stop double-charging quota
This commit is contained in:
parent
d96601a283
commit
d311e8123f
10 changed files with 1225 additions and 237 deletions
|
|
@ -66,6 +66,16 @@ async function issueGenerationReceipt(
|
|||
return parseGenerationReceiptId(data)
|
||||
}
|
||||
|
||||
/** 쓰지 않을 공급자 응답 본문을 닫아 연결과 생성을 정리한다. */
|
||||
async function discardBody(resp: Response): Promise<void> {
|
||||
if (!resp.body || resp.bodyUsed) return
|
||||
try {
|
||||
await resp.body.cancel()
|
||||
} catch {
|
||||
// 이미 닫힌 스트림 — 무시
|
||||
}
|
||||
}
|
||||
|
||||
function generationHeaders(generationId: string | null): Record<string, string> {
|
||||
return generationId === null ? {} : { [GENERATION_ID_HEADER]: generationId }
|
||||
}
|
||||
|
|
@ -147,27 +157,9 @@ Deno.serve(async (req: Request) => {
|
|||
)
|
||||
}
|
||||
|
||||
// 3단계: 쿼터 소비 (원자적 base → overage fallback)
|
||||
const policy = getQuotaPolicy(tier, quotaKey)
|
||||
const consume = await consumeQuota(user.id, quotaKey, serviceClient, policy.limit)
|
||||
if (!consume.allowed) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: 'quota_exceeded',
|
||||
model: requestedModel,
|
||||
current: consume.current,
|
||||
limit: consume.limit,
|
||||
tier,
|
||||
overage_credits: consume.overageCredits,
|
||||
}),
|
||||
{
|
||||
status: 429,
|
||||
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// 실제 Anthropic API 호출 (Prompt Caching 2024-07-31 활성화)
|
||||
// 3단계: 공급자 호출 (Prompt Caching 2024-07-31 활성화)
|
||||
// 쿼터는 공급자가 요청을 받아들인 뒤에만 소비한다. 공급자 5xx/과부하/타임아웃으로
|
||||
// 답을 주지 못한 요청이 쿼터를 깎고, 클라이언트 재시도가 한 번 더 깎는 일을 막는다.
|
||||
const systemPayload = buildAnthropicSystemBlocks(body.system)
|
||||
|
||||
const anthropicResp = await fetch('https://api.anthropic.com/v1/messages', {
|
||||
|
|
@ -190,12 +182,54 @@ Deno.serve(async (req: Request) => {
|
|||
|
||||
if (!anthropicResp.ok) {
|
||||
console.error('Anthropic request failed', { status: anthropicResp.status })
|
||||
await discardBody(anthropicResp)
|
||||
return new Response(JSON.stringify({ error: 'provider_request_failed' }), {
|
||||
status: 502,
|
||||
headers: { ...corsHeaders, 'Content-Type': 'application/json', 'Cache-Control': 'no-store' },
|
||||
})
|
||||
}
|
||||
|
||||
// 비스트리밍 응답은 형식 검증까지 통과한 뒤에 소비한다.
|
||||
let nonStreamData: unknown = null
|
||||
if (!body.stream || !anthropicResp.body) {
|
||||
nonStreamData = await anthropicResp.json()
|
||||
if (!hasAssistantText(nonStreamData)) {
|
||||
console.error('Anthropic returned an invalid response shape')
|
||||
return new Response(JSON.stringify({ error: 'provider_invalid_response' }), {
|
||||
status: 502,
|
||||
headers: { ...corsHeaders, 'Content-Type': 'application/json', 'Cache-Control': 'no-store' },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 4단계: 쿼터 소비 (원자적 base → overage fallback).
|
||||
// 동시 요청이 checkQuota 를 함께 통과했다면 여기서 초과분이 거절된다.
|
||||
const policy = getQuotaPolicy(tier, quotaKey)
|
||||
let consume: Awaited<ReturnType<typeof consumeQuota>>
|
||||
try {
|
||||
consume = await consumeQuota(user.id, quotaKey, serviceClient, policy.limit)
|
||||
} catch (err) {
|
||||
await discardBody(anthropicResp)
|
||||
throw err
|
||||
}
|
||||
if (!consume.allowed) {
|
||||
await discardBody(anthropicResp)
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: 'quota_exceeded',
|
||||
model: requestedModel,
|
||||
current: consume.current,
|
||||
limit: consume.limit,
|
||||
tier,
|
||||
overage_credits: consume.overageCredits,
|
||||
}),
|
||||
{
|
||||
status: 429,
|
||||
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
if (body.stream && anthropicResp.body) {
|
||||
const generationId = await issueGenerationReceipt(
|
||||
serviceClient,
|
||||
|
|
@ -215,21 +249,13 @@ Deno.serve(async (req: Request) => {
|
|||
})
|
||||
}
|
||||
|
||||
const data: unknown = await anthropicResp.json()
|
||||
if (!hasAssistantText(data)) {
|
||||
console.error('Anthropic returned an invalid response shape')
|
||||
return new Response(JSON.stringify({ error: 'provider_invalid_response' }), {
|
||||
status: 502,
|
||||
headers: { ...corsHeaders, 'Content-Type': 'application/json', 'Cache-Control': 'no-store' },
|
||||
})
|
||||
}
|
||||
const generationId = await issueGenerationReceipt(
|
||||
serviceClient,
|
||||
user.id,
|
||||
generationPurpose,
|
||||
requestedModel,
|
||||
)
|
||||
return new Response(JSON.stringify(data), {
|
||||
return new Response(JSON.stringify(nonStreamData), {
|
||||
status: 200,
|
||||
headers: {
|
||||
...corsHeaders,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue