fix(quotas): unifier le décompte IA (BYOK, rollback) et combler les fuites
Centralise la réserve via ai-quota, corrige admin unavailable (-1), brancher les routes sans quota et le host-pays brainstorm, avec usage-meter élargi, noms de clusters, MCP et ajustements dashboard/insights. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -4,6 +4,7 @@ import { auth } from '@/auth'
|
||||
import { autoLabelCreationService } from '@/lib/ai/services'
|
||||
import { getAISettings } from '@/app/actions/ai-settings'
|
||||
import { hasUserAiConsent } from '@/lib/consent/server-consent'
|
||||
import { withAiQuota, handleQuotaHttpError } from '@/lib/ai-quota'
|
||||
|
||||
/**
|
||||
* POST /api/ai/auto-labels - Suggest new labels for a notebook
|
||||
@@ -59,11 +60,16 @@ export async function POST(request: NextRequest) {
|
||||
)
|
||||
}
|
||||
|
||||
// Get label suggestions
|
||||
const suggestions = await autoLabelCreationService.suggestLabels(
|
||||
notebookId,
|
||||
const suggestions = await withAiQuota(
|
||||
session.user.id,
|
||||
language
|
||||
'auto_tag',
|
||||
() =>
|
||||
autoLabelCreationService.suggestLabels(
|
||||
notebookId,
|
||||
session.user!.id,
|
||||
language,
|
||||
),
|
||||
{ lane: 'tags' },
|
||||
)
|
||||
|
||||
if (!suggestions) {
|
||||
@@ -79,8 +85,10 @@ export async function POST(request: NextRequest) {
|
||||
data: suggestions,
|
||||
})
|
||||
} catch (error) {
|
||||
const quotaResp = handleQuotaHttpError(error)
|
||||
if (quotaResp) return quotaResp
|
||||
console.error('[/api/ai/auto-labels] POST failed:', error)
|
||||
return NextResponse.json({ success: true, data: null })
|
||||
return NextResponse.json({ success: false, error: 'auto_labels_failed' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from 'next/server'
|
||||
import { auth } from '@/auth'
|
||||
import { batchOrganizationService } from '@/lib/ai/services'
|
||||
import { hasUserAiConsent } from '@/lib/consent/server-consent'
|
||||
import { reserveUsageOrThrow, QuotaExceededError } from '@/lib/entitlements'
|
||||
|
||||
/**
|
||||
* POST /api/ai/batch-organize - Create organization plan for notes in Inbox
|
||||
@@ -40,6 +41,8 @@ export async function POST(request: NextRequest) {
|
||||
}
|
||||
}
|
||||
|
||||
await reserveUsageOrThrow(session.user.id, 'reformulate')
|
||||
|
||||
// Create organization plan
|
||||
const plan = await batchOrganizationService.createOrganizationPlan(
|
||||
session.user.id,
|
||||
|
||||
@@ -3,6 +3,7 @@ import { auth } from '@/auth'
|
||||
import { getAISettings } from '@/app/actions/ai-settings'
|
||||
import { describeImages } from '@/lib/ai/services/image-description.service'
|
||||
import { hasUserAiConsent } from '@/lib/consent/server-consent'
|
||||
import { withAiQuota, handleQuotaHttpError } from '@/lib/ai-quota'
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
@@ -11,7 +12,6 @@ export async function POST(req: NextRequest) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
// GDPR AI Consent check
|
||||
if (!(await hasUserAiConsent())) {
|
||||
return NextResponse.json({ error: 'ai_consent_required' }, { status: 403 })
|
||||
}
|
||||
@@ -27,23 +27,31 @@ export async function POST(req: NextRequest) {
|
||||
return NextResponse.json({ error: 'imageUrls must be a non-empty array' }, { status: 400 })
|
||||
}
|
||||
|
||||
const result = await describeImages(
|
||||
imageUrls,
|
||||
mode === 'title' ? 'title' : 'description',
|
||||
language || 'fr'
|
||||
const isTitleMode = mode === 'title'
|
||||
const feature = isTitleMode ? 'auto_title' : 'reformulate'
|
||||
const lane = isTitleMode ? 'tags' : 'chat'
|
||||
|
||||
const result = await withAiQuota(
|
||||
session.user.id,
|
||||
feature,
|
||||
() => describeImages(
|
||||
imageUrls,
|
||||
isTitleMode ? 'title' : 'description',
|
||||
language || 'fr'
|
||||
),
|
||||
{ lane },
|
||||
)
|
||||
|
||||
// For title mode, return suggestions in same format as /api/ai/title-suggestions
|
||||
if (mode === 'title' && result.suggestions) {
|
||||
if (isTitleMode && result.suggestions) {
|
||||
return NextResponse.json({ suggestions: result.suggestions })
|
||||
}
|
||||
|
||||
return NextResponse.json(result)
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
const quotaResp = handleQuotaHttpError(error)
|
||||
if (quotaResp) return quotaResp
|
||||
console.error('[describe-image] Error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: error.message || 'Failed to describe image' },
|
||||
{ status: 500 }
|
||||
)
|
||||
const message = error instanceof Error ? error.message : 'Failed to describe image'
|
||||
return NextResponse.json({ error: message }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { getChatProvider } from '@/lib/ai/factory'
|
||||
import { getSystemConfig } from '@/lib/config'
|
||||
import prisma from '@/lib/prisma'
|
||||
import { hasUserAiConsent } from '@/lib/consent/server-consent'
|
||||
import { withAiQuota, handleQuotaHttpError } from '@/lib/ai-quota'
|
||||
|
||||
/**
|
||||
* POST /api/ai/echo/fusion
|
||||
@@ -34,7 +35,6 @@ export async function POST(req: NextRequest) {
|
||||
)
|
||||
}
|
||||
|
||||
// Fetch the notes
|
||||
const notes = await prisma.note.findMany({
|
||||
where: {
|
||||
id: { in: noteIds },
|
||||
@@ -55,11 +55,9 @@ export async function POST(req: NextRequest) {
|
||||
)
|
||||
}
|
||||
|
||||
// Get AI provider
|
||||
const config = await getSystemConfig()
|
||||
const provider = getChatProvider(config)
|
||||
|
||||
// Build fusion prompt
|
||||
const notesDescriptions = notes.map((note, index) => {
|
||||
return `Note ${index + 1}: "${note.title || 'Untitled'}"
|
||||
${note.content}`
|
||||
@@ -90,21 +88,21 @@ Output format:
|
||||
|
||||
Begin:`
|
||||
|
||||
try {
|
||||
const fusedContent = await provider.generateText(fusionPrompt)
|
||||
|
||||
return NextResponse.json({
|
||||
fusedNote: fusedContent,
|
||||
notesCount: notes.length
|
||||
})
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to generate fusion' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
const fusedContent = await withAiQuota(
|
||||
session.user.id,
|
||||
'reformulate',
|
||||
() => provider.generateText(fusionPrompt),
|
||||
{ lane: 'chat' },
|
||||
)
|
||||
|
||||
return NextResponse.json({
|
||||
fusedNote: fusedContent,
|
||||
notesCount: notes.length
|
||||
})
|
||||
} catch (error) {
|
||||
const quotaResp = handleQuotaHttpError(error)
|
||||
if (quotaResp) return quotaResp
|
||||
console.error('[/api/ai/echo/fusion] Error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to process fusion request' },
|
||||
{ status: 500 }
|
||||
|
||||
@@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from 'next/server'
|
||||
import { auth } from '@/auth'
|
||||
import { memoryEchoService } from '@/lib/ai/services/memory-echo.service'
|
||||
import { hasUserAiConsent } from '@/lib/consent/server-consent'
|
||||
import { reserveUsageOrThrow, QuotaExceededError } from '@/lib/entitlements'
|
||||
|
||||
/**
|
||||
* GET /api/ai/echo
|
||||
@@ -27,6 +28,7 @@ export async function GET(req: NextRequest) {
|
||||
}
|
||||
|
||||
// Get next insight (respects frequency limits)
|
||||
await reserveUsageOrThrow(session.user.id, 'reformulate')
|
||||
const insight = await memoryEchoService.getNextInsight(session.user.id)
|
||||
|
||||
if (!insight) {
|
||||
@@ -41,6 +43,9 @@ export async function GET(req: NextRequest) {
|
||||
return NextResponse.json({ insight })
|
||||
|
||||
} catch (error) {
|
||||
if (error instanceof QuotaExceededError) {
|
||||
return NextResponse.json(error.toJSON(), { status: 429 })
|
||||
}
|
||||
console.error('[/api/ai/echo] GET error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch Memory Echo insight' },
|
||||
|
||||
@@ -3,6 +3,7 @@ import { auth } from '@/auth'
|
||||
import { getSystemConfig } from '@/lib/config'
|
||||
import { getTagsProvider } from '@/lib/ai/factory'
|
||||
import { hasUserAiConsent } from '@/lib/consent/server-consent'
|
||||
import { withAiQuota, handleQuotaHttpError } from '@/lib/ai-quota'
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
@@ -32,7 +33,6 @@ export async function POST(request: NextRequest) {
|
||||
let prompt: string
|
||||
|
||||
if (mode === 'complete') {
|
||||
// Add missing info from resource without rewriting the existing note
|
||||
prompt = `You are an expert note editor. Your task is to enrich an existing note by adding relevant information from a provided resource, WITHOUT modifying or rewriting the existing content.
|
||||
|
||||
LANGUAGE RULE: Respond in ${lang}. Match the language of the existing note.
|
||||
@@ -56,7 +56,6 @@ INSTRUCTIONS:
|
||||
- Format the new content consistently with the existing note style and the requested FORMAT RULE
|
||||
- Respond ONLY with the enriched note content, no explanations`
|
||||
} else {
|
||||
// Merge: intelligently rewrite integrating both sources
|
||||
prompt = `You are an expert note writer. Your task is to intelligently merge an existing note with a resource into a single, coherent, well-structured document.
|
||||
|
||||
LANGUAGE RULE: Respond in ${lang}. Match the language of the existing note.
|
||||
@@ -81,14 +80,19 @@ INSTRUCTIONS:
|
||||
- Respond ONLY with the merged content, no meta-commentary or explanations`
|
||||
}
|
||||
|
||||
const enrichedContent = await provider.generateText(prompt)
|
||||
const enrichedContent = await withAiQuota(
|
||||
session.user.id,
|
||||
'reformulate',
|
||||
() => provider.generateText(prompt),
|
||||
{ lane: 'chat' },
|
||||
)
|
||||
|
||||
return NextResponse.json({ enrichedContent: enrichedContent.trim() })
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
const quotaResp = handleQuotaHttpError(error)
|
||||
if (quotaResp) return quotaResp
|
||||
console.error('[enrich-from-resource] Error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: error.message || 'Failed to enrich content' },
|
||||
{ status: 500 }
|
||||
)
|
||||
const message = error instanceof Error ? error.message : 'Failed to enrich content'
|
||||
return NextResponse.json({ error: message }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { NextRequest, NextResponse } from 'next/server'
|
||||
import { auth } from '@/auth'
|
||||
import { notebookSummaryService } from '@/lib/ai/services'
|
||||
import { hasUserAiConsent } from '@/lib/consent/server-consent'
|
||||
import { reserveUsageOrThrow, QuotaExceededError } from '@/lib/entitlements'
|
||||
|
||||
/**
|
||||
* POST /api/ai/notebook-summary - Generate summary for a notebook
|
||||
@@ -51,6 +52,8 @@ export async function POST(request: NextRequest) {
|
||||
)
|
||||
}
|
||||
|
||||
await reserveUsageOrThrow(session.user.id, 'reformulate')
|
||||
|
||||
// Generate summary
|
||||
const summary = await notebookSummaryService.generateSummary(
|
||||
notebookId,
|
||||
|
||||
@@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from 'next/server'
|
||||
import { auth } from '@/auth'
|
||||
import prisma from '@/lib/prisma'
|
||||
import { notebookOrganizerService } from '@/lib/ai/services/notebook-organizer.service'
|
||||
import { reserveUsageOrThrow, QuotaExceededError } from '@/lib/entitlements'
|
||||
import { withAiQuota, handleQuotaHttpError } from '@/lib/ai-quota'
|
||||
import { syncNoteLabels } from '@/app/actions/notes'
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
@@ -17,19 +17,6 @@ export async function POST(request: NextRequest) {
|
||||
return NextResponse.json({ error: 'notebookId is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
try {
|
||||
await reserveUsageOrThrow(session.user.id, 'reformulate')
|
||||
} catch (err) {
|
||||
if (err instanceof QuotaExceededError) {
|
||||
const isTierLocked = err.currentQuota === 0
|
||||
return NextResponse.json(
|
||||
{ error: isTierLocked ? 'feature_locked' : 'quota_exceeded', errorKey: isTierLocked ? 'ai.featureLocked' : 'ai.quotaExceeded' },
|
||||
{ status: 402 },
|
||||
)
|
||||
}
|
||||
throw err
|
||||
}
|
||||
|
||||
const notes = await prisma.note.findMany({
|
||||
where: { notebookId, trashedAt: null, userId: session.user.id },
|
||||
select: { id: true, title: true, content: true },
|
||||
@@ -46,13 +33,20 @@ export async function POST(request: NextRequest) {
|
||||
contentPreview: n.content.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim().slice(0, 300),
|
||||
}))
|
||||
|
||||
const result = await notebookOrganizerService.analyze(notesForAnalysis)
|
||||
|
||||
const result = await withAiQuota(
|
||||
session.user.id,
|
||||
'reformulate',
|
||||
() => notebookOrganizerService.analyze(notesForAnalysis),
|
||||
{ lane: 'chat' },
|
||||
)
|
||||
|
||||
return NextResponse.json(result)
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
const quotaResp = handleQuotaHttpError(error)
|
||||
if (quotaResp) return quotaResp
|
||||
console.error('[Notebook Organizer] Error:', error)
|
||||
return NextResponse.json({ error: error.message || 'Failed to organize notebook' }, { status: 500 })
|
||||
const message = error instanceof Error ? error.message : 'Failed to organize notebook'
|
||||
return NextResponse.json({ error: message }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { rateLimit } from '@/lib/rate-limit'
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { auth } from '@/auth'
|
||||
import { searchOverviewService } from '@/lib/ai/services/search-overview.service'
|
||||
import { withAiQuota, handleQuotaHttpError } from '@/lib/ai-quota'
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
@@ -15,11 +15,19 @@ export async function POST(request: NextRequest) {
|
||||
return NextResponse.json({ error: 'query and results required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const overview = await searchOverviewService.generate(query, results, language)
|
||||
const overview = await withAiQuota(
|
||||
session.user.id,
|
||||
'semantic_search',
|
||||
() => searchOverviewService.generate(query, results, language),
|
||||
{ lane: 'chat' },
|
||||
)
|
||||
|
||||
return NextResponse.json(overview)
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
const quotaResp = handleQuotaHttpError(error)
|
||||
if (quotaResp) return quotaResp
|
||||
console.error('[Search Overview] Error:', error)
|
||||
return NextResponse.json({ error: error.message, hasRelevantInfo: false, answer: '' }, { status: 500 })
|
||||
const message = error instanceof Error ? error.message : 'Search overview failed'
|
||||
return NextResponse.json({ error: message, hasRelevantInfo: false, answer: '' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from 'next/server'
|
||||
import { auth } from '@/auth'
|
||||
import { notebookSuggestionService } from '@/lib/ai/services/notebook-suggestion.service'
|
||||
import { hasUserAiConsent } from '@/lib/consent/server-consent'
|
||||
import { withAiQuota, handleQuotaHttpError } from '@/lib/ai-quota'
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
@@ -21,7 +22,6 @@ export async function POST(req: NextRequest) {
|
||||
return NextResponse.json({ error: 'noteContent is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
// Minimum content length for suggestion (20 words as per specs)
|
||||
const wordCount = noteContent.trim().split(/\s+/).length
|
||||
if (wordCount < 20) {
|
||||
return NextResponse.json({
|
||||
@@ -31,18 +31,25 @@ export async function POST(req: NextRequest) {
|
||||
})
|
||||
}
|
||||
|
||||
// Get suggestion from AI service
|
||||
const suggestedNotebook = await notebookSuggestionService.suggestNotebook(
|
||||
noteContent,
|
||||
const suggestedNotebook = await withAiQuota(
|
||||
session.user.id,
|
||||
language
|
||||
'auto_tag',
|
||||
() => notebookSuggestionService.suggestNotebook(
|
||||
noteContent,
|
||||
session.user!.id,
|
||||
language
|
||||
),
|
||||
{ lane: 'tags' },
|
||||
)
|
||||
|
||||
return NextResponse.json({
|
||||
suggestion: suggestedNotebook,
|
||||
confidence: suggestedNotebook ? 0.8 : 0 // Placeholder confidence score
|
||||
confidence: suggestedNotebook ? 0.8 : 0
|
||||
})
|
||||
} catch (error) {
|
||||
const quotaResp = handleQuotaHttpError(error)
|
||||
if (quotaResp) return quotaResp
|
||||
console.error('[/api/ai/suggest-notebook] Error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to generate suggestion' },
|
||||
{ status: 500 }
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { rateLimit } from '@/lib/rate-limit'
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { runLaneWithBillingUser, willUseByokForLane } from '@/lib/ai/provider-for-user'
|
||||
import { runLaneWithBillingUser } from '@/lib/ai/provider-for-user'
|
||||
import { getSystemConfig } from '@/lib/config'
|
||||
import { auth } from '@/auth'
|
||||
import { getAISettings } from '@/app/actions/ai-settings'
|
||||
import { reserveUsageOrThrow, QuotaExceededError, QuotaServiceUnavailableError } from '@/lib/entitlements'
|
||||
import { reserveAiUsageOrThrow } from '@/lib/ai-quota'
|
||||
import { QuotaExceededError, QuotaServiceUnavailableError } from '@/lib/entitlements'
|
||||
import { z } from 'zod'
|
||||
import { hasUserAiConsent } from '@/lib/consent/server-consent'
|
||||
|
||||
@@ -61,19 +62,16 @@ export async function POST(req: NextRequest) {
|
||||
}
|
||||
|
||||
const config = await getSystemConfig()
|
||||
const { usedByok: willUseByok } = await willUseByokForLane('tags', config, session.user.id)
|
||||
if (!willUseByok) {
|
||||
try {
|
||||
await reserveUsageOrThrow(session.user.id, 'auto_title')
|
||||
} catch (err) {
|
||||
if (err instanceof QuotaExceededError) {
|
||||
return NextResponse.json(err.toJSON(), { status: 402 })
|
||||
}
|
||||
if (err instanceof QuotaServiceUnavailableError || process.env.NODE_ENV === 'production') {
|
||||
return NextResponse.json({ error: 'QUOTA_SERVICE_UNAVAILABLE' }, { status: 503 })
|
||||
}
|
||||
console.error('[/api/ai/title-suggestions] Quota check error (fail-open):', err)
|
||||
try {
|
||||
await reserveAiUsageOrThrow(session.user.id, 'auto_title', { lane: 'tags' })
|
||||
} catch (err) {
|
||||
if (err instanceof QuotaExceededError) {
|
||||
return NextResponse.json(err.toJSON(), { status: 402 })
|
||||
}
|
||||
if (err instanceof QuotaServiceUnavailableError || process.env.NODE_ENV === 'production') {
|
||||
return NextResponse.json({ error: 'QUOTA_SERVICE_UNAVAILABLE' }, { status: 503 })
|
||||
}
|
||||
console.error('[/api/ai/title-suggestions] Quota check error (fail-open):', err)
|
||||
}
|
||||
|
||||
// Détecter la langue du contenu (simple détection basée sur les caractères et mots)
|
||||
|
||||
@@ -3,6 +3,7 @@ import { auth } from '@/auth'
|
||||
import { getChatProvider } from '@/lib/ai/factory'
|
||||
import { getSystemConfig } from '@/lib/config'
|
||||
import { hasUserAiConsent } from '@/lib/consent/server-consent'
|
||||
import { withAiQuota, handleQuotaHttpError } from '@/lib/ai-quota'
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
@@ -18,12 +19,10 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
const { text } = await request.json()
|
||||
|
||||
// Validation
|
||||
if (!text || typeof text !== 'string') {
|
||||
return NextResponse.json({ error: 'Text is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
// Validate word count
|
||||
const wordCount = text.split(/\s+/).length
|
||||
if (wordCount < 10) {
|
||||
return NextResponse.json(
|
||||
@@ -42,11 +41,9 @@ export async function POST(request: NextRequest) {
|
||||
const config = await getSystemConfig()
|
||||
const provider = getChatProvider(config)
|
||||
|
||||
// Detect language from text
|
||||
const hasFrench = /[àâäéèêëïîôùûüÿç]/i.test(text)
|
||||
const responseLanguage = hasFrench ? 'French' : 'English'
|
||||
|
||||
// Build prompt to transform text to Markdown
|
||||
const prompt = hasFrench
|
||||
? `Tu es un expert en Markdown. Transforme ce texte ${responseLanguage} en Markdown bien formaté.
|
||||
|
||||
@@ -77,19 +74,22 @@ ${text}
|
||||
|
||||
Respond ONLY with the transformed Markdown text, no explanations.`
|
||||
|
||||
|
||||
const transformedText = await provider.generateText(prompt)
|
||||
const transformedText = await withAiQuota(
|
||||
session.user.id,
|
||||
'reformulate',
|
||||
() => provider.generateText(prompt),
|
||||
{ lane: 'chat' },
|
||||
)
|
||||
|
||||
|
||||
return NextResponse.json({
|
||||
originalText: text,
|
||||
transformedText: transformedText,
|
||||
transformedText,
|
||||
language: responseLanguage
|
||||
})
|
||||
} catch (error: any) {
|
||||
return NextResponse.json(
|
||||
{ error: error.message || 'Failed to transform text to Markdown' },
|
||||
{ status: 500 }
|
||||
)
|
||||
} catch (error: unknown) {
|
||||
const quotaResp = handleQuotaHttpError(error)
|
||||
if (quotaResp) return quotaResp
|
||||
const message = error instanceof Error ? error.message : 'Failed to transform text to Markdown'
|
||||
return NextResponse.json({ error: message }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { auth } from '@/auth'
|
||||
import { getTagsProvider } from '@/lib/ai/factory'
|
||||
import { getSystemConfig } from '@/lib/config'
|
||||
import { hasUserAiConsent } from '@/lib/consent/server-consent'
|
||||
import { reserveUsageOrThrow, QuotaExceededError } from '@/lib/entitlements'
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
@@ -24,12 +25,18 @@ export async function POST(request: NextRequest) {
|
||||
const config = await getSystemConfig()
|
||||
const provider = getTagsProvider(config)
|
||||
|
||||
await reserveUsageOrThrow(session.user.id, 'reformulate')
|
||||
|
||||
const prompt = `Translate the following text to ${targetLanguage}. Return ONLY the translated text, no explanation, no preamble, no quotes:\n\n${text}`
|
||||
|
||||
const translatedText = await provider.generateText(prompt)
|
||||
|
||||
return NextResponse.json({ translatedText: translatedText.trim() })
|
||||
} catch (error: any) {
|
||||
return NextResponse.json({ error: error.message || 'Translation failed' }, { status: 500 })
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof QuotaExceededError) {
|
||||
return NextResponse.json(error.toJSON(), { status: 429 })
|
||||
}
|
||||
const message = error instanceof Error ? error.message : 'Translation failed'
|
||||
return NextResponse.json({ error: message }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user