Add Anthropic API integration, remove locale settings UI
All checks were successful
Build and Push Docker Image / build (push) Successful in 13m15s

Anthropic API:
- Add @anthropic-ai/sdk with adapter wrapping OpenAI-shaped interface
- Support Claude models (opus, sonnet, haiku) with extended thinking
- Auto-reset model on provider switch, JSON retry logic
- Add Claude model pricing to ai-usage tracker
- Update AI settings form with Anthropic provider option
- Add provider field to AIUsageLog for cross-provider cost tracking

Locale Settings Removal:
- Strip Localization tab from admin settings (mobile + desktop)
- Remove i18n settings from router and feature flags
- Remove LOCALIZATION from SettingCategory enum
- Keep franc document language detection intact

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-21 17:26:59 +01:00
parent 161cd1684a
commit f42b452899
9 changed files with 453 additions and 213 deletions

View File

@@ -17,6 +17,11 @@ function categorizeModel(modelId: string): string {
if (id.startsWith('gpt-4')) return 'gpt-4'
if (id.startsWith('gpt-3.5')) return 'gpt-3.5'
if (id.startsWith('o1') || id.startsWith('o3') || id.startsWith('o4')) return 'reasoning'
// Anthropic Claude models
if (id.startsWith('claude-opus-4-5') || id.startsWith('claude-sonnet-4-5')) return 'claude-4.5'
if (id.startsWith('claude-opus-4') || id.startsWith('claude-sonnet-4')) return 'claude-4'
if (id.startsWith('claude-haiku') || id.startsWith('claude-3')) return 'claude-3.5'
if (id.startsWith('claude')) return 'claude-4'
return 'other'
}
@@ -26,16 +31,10 @@ export const settingsRouter = router({
* These are non-sensitive settings that can be exposed to any user
*/
getFeatureFlags: protectedProcedure.query(async ({ ctx }) => {
const [whatsappEnabled, defaultLocale, availableLocales, juryCompareEnabled] = await Promise.all([
const [whatsappEnabled, juryCompareEnabled] = await Promise.all([
ctx.prisma.systemSettings.findUnique({
where: { key: 'whatsapp_enabled' },
}),
ctx.prisma.systemSettings.findUnique({
where: { key: 'i18n_default_locale' },
}),
ctx.prisma.systemSettings.findUnique({
where: { key: 'i18n_available_locales' },
}),
ctx.prisma.systemSettings.findUnique({
where: { key: 'jury_compare_enabled' },
}),
@@ -43,8 +42,6 @@ export const settingsRouter = router({
return {
whatsappEnabled: whatsappEnabled?.value === 'true',
defaultLocale: defaultLocale?.value || 'en',
availableLocales: availableLocales?.value ? JSON.parse(availableLocales.value) : ['en', 'fr'],
juryCompareEnabled: juryCompareEnabled?.value === 'true',
}
}),
@@ -171,14 +168,13 @@ export const settingsRouter = router({
)
.mutation(async ({ ctx, input }) => {
// Infer category from key prefix if not provided
const inferCategory = (key: string): 'AI' | 'BRANDING' | 'EMAIL' | 'STORAGE' | 'SECURITY' | 'DEFAULTS' | 'WHATSAPP' | 'LOCALIZATION' => {
if (key.startsWith('openai') || key.startsWith('ai_')) return 'AI'
const inferCategory = (key: string): 'AI' | 'BRANDING' | 'EMAIL' | 'STORAGE' | 'SECURITY' | 'DEFAULTS' | 'WHATSAPP' => {
if (key.startsWith('openai') || key.startsWith('ai_') || key.startsWith('anthropic')) return 'AI'
if (key.startsWith('smtp_') || key.startsWith('email_')) return 'EMAIL'
if (key.startsWith('storage_') || key.startsWith('local_storage') || key.startsWith('max_file') || key.startsWith('avatar_') || key.startsWith('allowed_file')) return 'STORAGE'
if (key.startsWith('brand_') || key.startsWith('logo_') || key.startsWith('primary_') || key.startsWith('theme_')) return 'BRANDING'
if (key.startsWith('whatsapp_')) return 'WHATSAPP'
if (key.startsWith('security_') || key.startsWith('session_')) return 'SECURITY'
if (key.startsWith('i18n_') || key.startsWith('locale_')) return 'LOCALIZATION'
return 'DEFAULTS'
}
@@ -206,7 +202,7 @@ export const settingsRouter = router({
}
// Reset OpenAI client if API key, base URL, model, or provider changed
if (input.settings.some((s) => s.key === 'openai_api_key' || s.key === 'openai_base_url' || s.key === 'ai_model' || s.key === 'ai_provider')) {
if (input.settings.some((s) => s.key === 'openai_api_key' || s.key === 'anthropic_api_key' || s.key === 'openai_base_url' || s.key === 'ai_model' || s.key === 'ai_provider')) {
const { resetOpenAIClient } = await import('@/lib/openai')
resetOpenAIClient()
}
@@ -276,9 +272,9 @@ export const settingsRouter = router({
category: categorizeModel(model),
}))
// Sort: GPT-5+ first, then GPT-4o, then other GPT-4, then GPT-3.5, then reasoning models
// Sort by category priority
const sorted = categorizedModels.sort((a, b) => {
const order = ['gpt-5+', 'gpt-4o', 'gpt-4', 'gpt-3.5', 'reasoning', 'other']
const order = ['claude-4.5', 'claude-4', 'claude-3.5', 'gpt-5+', 'gpt-4o', 'gpt-4', 'gpt-3.5', 'reasoning', 'other']
const aOrder = order.findIndex(cat => a.category === cat)
const bOrder = order.findIndex(cat => b.category === cat)
if (aOrder !== bOrder) return aOrder - bOrder
@@ -740,62 +736,4 @@ export const settingsRouter = router({
return results
}),
/**
* Get localization settings
*/
getLocalizationSettings: adminProcedure.query(async ({ ctx }) => {
const settings = await ctx.prisma.systemSettings.findMany({
where: { category: 'LOCALIZATION' },
orderBy: { key: 'asc' },
})
return settings
}),
/**
* Update localization settings
*/
updateLocalizationSettings: superAdminProcedure
.input(
z.object({
settings: z.array(
z.object({
key: z.string(),
value: z.string(),
})
),
})
)
.mutation(async ({ ctx, input }) => {
const results = await Promise.all(
input.settings.map((s) =>
ctx.prisma.systemSettings.upsert({
where: { key: s.key },
update: { value: s.value, updatedBy: ctx.user.id },
create: {
key: s.key,
value: s.value,
category: 'LOCALIZATION',
updatedBy: ctx.user.id,
},
})
)
)
try {
await logAudit({
prisma: ctx.prisma,
userId: ctx.user.id,
action: 'UPDATE_LOCALIZATION_SETTINGS',
entityType: 'SystemSettings',
detailsJson: { keys: input.settings.map((s) => s.key) },
ipAddress: ctx.ip,
userAgent: ctx.userAgent,
})
} catch {
// Never throw on audit failure
}
return results
}),
})