Add Anthropic API integration, remove locale settings UI
All checks were successful
Build and Push Docker Image / build (push) Successful in 13m15s
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:
@@ -1,5 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { z } from 'zod'
|
||||
@@ -36,6 +37,7 @@ const formSchema = z.object({
|
||||
ai_model: z.string(),
|
||||
ai_send_descriptions: z.boolean(),
|
||||
openai_api_key: z.string().optional(),
|
||||
anthropic_api_key: z.string().optional(),
|
||||
openai_base_url: z.string().optional(),
|
||||
})
|
||||
|
||||
@@ -48,6 +50,7 @@ interface AISettingsFormProps {
|
||||
ai_model?: string
|
||||
ai_send_descriptions?: string
|
||||
openai_api_key?: string
|
||||
anthropic_api_key?: string
|
||||
openai_base_url?: string
|
||||
}
|
||||
}
|
||||
@@ -63,12 +66,29 @@ export function AISettingsForm({ settings }: AISettingsFormProps) {
|
||||
ai_model: settings.ai_model || 'gpt-4o',
|
||||
ai_send_descriptions: settings.ai_send_descriptions === 'true',
|
||||
openai_api_key: '',
|
||||
anthropic_api_key: '',
|
||||
openai_base_url: settings.openai_base_url || '',
|
||||
},
|
||||
})
|
||||
|
||||
const watchProvider = form.watch('ai_provider')
|
||||
const isLiteLLM = watchProvider === 'litellm'
|
||||
const isAnthropic = watchProvider === 'anthropic'
|
||||
const prevProviderRef = useRef(settings.ai_provider || 'openai')
|
||||
|
||||
// Auto-reset model when provider changes
|
||||
useEffect(() => {
|
||||
if (watchProvider !== prevProviderRef.current) {
|
||||
prevProviderRef.current = watchProvider
|
||||
if (watchProvider === 'anthropic') {
|
||||
form.setValue('ai_model', 'claude-sonnet-4-5-20250514')
|
||||
} else if (watchProvider === 'openai') {
|
||||
form.setValue('ai_model', 'gpt-4o')
|
||||
} else if (watchProvider === 'litellm') {
|
||||
form.setValue('ai_model', '')
|
||||
}
|
||||
}
|
||||
}, [watchProvider, form])
|
||||
|
||||
// Fetch available models from OpenAI API (skip for LiteLLM — no models.list support)
|
||||
const {
|
||||
@@ -119,6 +139,9 @@ export function AISettingsForm({ settings }: AISettingsFormProps) {
|
||||
if (data.openai_api_key && data.openai_api_key.trim()) {
|
||||
settingsToUpdate.push({ key: 'openai_api_key', value: data.openai_api_key })
|
||||
}
|
||||
if (data.anthropic_api_key && data.anthropic_api_key.trim()) {
|
||||
settingsToUpdate.push({ key: 'anthropic_api_key', value: data.anthropic_api_key })
|
||||
}
|
||||
|
||||
// Save base URL (empty string clears it)
|
||||
settingsToUpdate.push({ key: 'openai_base_url', value: data.openai_base_url?.trim() || '' })
|
||||
@@ -139,6 +162,9 @@ export function AISettingsForm({ settings }: AISettingsFormProps) {
|
||||
)
|
||||
|
||||
const categoryLabels: Record<string, string> = {
|
||||
'claude-4.5': 'Claude 4.5 Series (Latest)',
|
||||
'claude-4': 'Claude 4 Series',
|
||||
'claude-3.5': 'Claude 3.5 Series',
|
||||
'gpt-5+': 'GPT-5+ Series (Latest)',
|
||||
'gpt-4o': 'GPT-4o Series',
|
||||
'gpt-4': 'GPT-4 Series',
|
||||
@@ -147,7 +173,7 @@ export function AISettingsForm({ settings }: AISettingsFormProps) {
|
||||
other: 'Other Models',
|
||||
}
|
||||
|
||||
const categoryOrder = ['gpt-5+', 'gpt-4o', 'gpt-4', 'gpt-3.5', 'reasoning', 'other']
|
||||
const categoryOrder = ['claude-4.5', 'claude-4', 'claude-3.5', 'gpt-5+', 'gpt-4o', 'gpt-4', 'gpt-3.5', 'reasoning', 'other']
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
@@ -187,13 +213,16 @@ export function AISettingsForm({ settings }: AISettingsFormProps) {
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="openai">OpenAI (API Key)</SelectItem>
|
||||
<SelectItem value="anthropic">Anthropic (Claude API)</SelectItem>
|
||||
<SelectItem value="litellm">LiteLLM Proxy (ChatGPT Subscription)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormDescription>
|
||||
{field.value === 'litellm'
|
||||
? 'Route AI calls through a LiteLLM proxy connected to your ChatGPT Plus/Pro subscription'
|
||||
: 'Direct OpenAI API access using your API key'}
|
||||
: field.value === 'anthropic'
|
||||
? 'Direct Anthropic API access using Claude models'
|
||||
: 'Direct OpenAI API access using your API key'}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -211,37 +240,71 @@ export function AISettingsForm({ settings }: AISettingsFormProps) {
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="openai_api_key"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{isLiteLLM ? 'API Key (Optional)' : 'API Key'}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder={isLiteLLM
|
||||
? 'Optional — leave blank for default'
|
||||
: (settings.openai_api_key ? '••••••••' : 'Enter API key')}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{isLiteLLM
|
||||
? 'LiteLLM proxy usually does not require an API key. Leave blank to use default.'
|
||||
: 'Your OpenAI API key. Leave blank to keep the existing key.'}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{isAnthropic && (
|
||||
<Alert>
|
||||
<Info className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
<strong>Anthropic Claude Mode</strong> — AI calls use the Anthropic Messages API.
|
||||
Claude Opus models include extended thinking for deeper analysis.
|
||||
JSON responses are validated with automatic retry.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{isAnthropic ? (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="anthropic_api_key"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Anthropic API Key</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder={settings.anthropic_api_key ? '••••••••' : 'Enter Anthropic API key'}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Your Anthropic API key. Leave blank to keep the existing key.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="openai_api_key"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{isLiteLLM ? 'API Key (Optional)' : 'OpenAI API Key'}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder={isLiteLLM
|
||||
? 'Optional — leave blank for default'
|
||||
: (settings.openai_api_key ? '••••••••' : 'Enter API key')}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{isLiteLLM
|
||||
? 'LiteLLM proxy usually does not require an API key. Leave blank to use default.'
|
||||
: 'Your OpenAI API key. Leave blank to keep the existing key.'}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="openai_base_url"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{isLiteLLM ? 'LiteLLM Proxy URL' : 'API Base URL (Optional)'}</FormLabel>
|
||||
<FormLabel>{isLiteLLM ? 'LiteLLM Proxy URL' : isAnthropic ? 'Anthropic Base URL (Optional)' : 'API Base URL (Optional)'}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder={isLiteLLM ? 'http://localhost:4000' : 'https://api.openai.com/v1'}
|
||||
@@ -255,6 +318,10 @@ export function AISettingsForm({ settings }: AISettingsFormProps) {
|
||||
<code className="text-xs bg-muted px-1 rounded">http://localhost:4000</code>{' '}
|
||||
or your server address.
|
||||
</>
|
||||
) : isAnthropic ? (
|
||||
<>
|
||||
Custom base URL for Anthropic API proxy or gateway. Leave blank for default Anthropic API.
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Custom base URL for OpenAI-compatible providers. Leave blank for OpenAI.
|
||||
@@ -288,7 +355,42 @@ export function AISettingsForm({ settings }: AISettingsFormProps) {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isLiteLLM || modelsData?.manualEntry ? (
|
||||
{isAnthropic ? (
|
||||
// Anthropic: fetch models from server (hardcoded list)
|
||||
modelsLoading ? (
|
||||
<Skeleton className="h-10 w-full" />
|
||||
) : modelsData?.success && modelsData.models && modelsData.models.length > 0 ? (
|
||||
<Select onValueChange={field.onChange} value={field.value}>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select Claude model" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{categoryOrder
|
||||
.filter((cat) => groupedModels?.[cat]?.length)
|
||||
.map((category) => (
|
||||
<SelectGroup key={category}>
|
||||
<SelectLabel className="text-xs font-semibold text-muted-foreground">
|
||||
{categoryLabels[category] || category}
|
||||
</SelectLabel>
|
||||
{groupedModels?.[category]?.map((model) => (
|
||||
<SelectItem key={model.id} value={model.id}>
|
||||
{model.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
) : (
|
||||
<Input
|
||||
value={field.value}
|
||||
onChange={(e) => field.onChange(e.target.value)}
|
||||
placeholder="claude-sonnet-4-5-20250514"
|
||||
/>
|
||||
)
|
||||
) : isLiteLLM || modelsData?.manualEntry ? (
|
||||
<Input
|
||||
value={field.value}
|
||||
onChange={(e) => field.onChange(e.target.value)}
|
||||
@@ -341,7 +443,16 @@ export function AISettingsForm({ settings }: AISettingsFormProps) {
|
||||
</Select>
|
||||
)}
|
||||
<FormDescription>
|
||||
{isLiteLLM ? (
|
||||
{isAnthropic ? (
|
||||
form.watch('ai_model')?.includes('opus') ? (
|
||||
<span className="flex items-center gap-1 text-amber-600">
|
||||
<SlidersHorizontal className="h-3 w-3" />
|
||||
Opus model — includes extended thinking for deeper analysis
|
||||
</span>
|
||||
) : (
|
||||
'Anthropic Claude model to use for AI features'
|
||||
)
|
||||
) : isLiteLLM ? (
|
||||
<>
|
||||
Enter the model ID with the{' '}
|
||||
<code className="text-xs bg-muted px-1 rounded">chatgpt/</code> prefix.
|
||||
|
||||
Reference in New Issue
Block a user