feat(byok): add model selection to BYOK settings panel and overlay custom model on route resolution
Some checks failed
CI / Deploy production (on server) (push) Has been cancelled
CI / Lint, Unit Tests & Build (push) Has been cancelled

This commit is contained in:
Antigravity
2026-05-28 21:41:34 +00:00
parent 3b2570d981
commit 11a07adee7
3 changed files with 119 additions and 12 deletions

View File

@@ -42,12 +42,40 @@ function providerLabel(t: (key: string) => string, provider: string): string {
return translated === key ? provider : translated
}
const PROVIDER_MODEL_SUGGESTIONS: Record<string, string[]> = {
openai: ['gpt-4o-mini', 'gpt-4o', 'gpt-3.5-turbo'],
anthropic: ['claude-3-5-sonnet-latest', 'claude-3-5-haiku-latest', 'claude-3-opus-latest'],
google: ['gemini-1.5-flash', 'gemini-1.5-pro', 'gemini-2.0-flash-exp'],
deepseek: ['deepseek-chat', 'deepseek-coder'],
minimax: ['abab6.5-chat', 'abab6.5s-chat'],
mistral: ['mistral-small-latest', 'mistral-medium-latest', 'mistral-large-latest'],
glm: ['glm-4', 'glm-4-flash'],
openrouter: ['openai/gpt-4o-mini', 'anthropic/claude-3.5-sonnet', 'deepseek/deepseek-chat'],
custom: [],
}
export function ByokSettingsPanel() {
const { t } = useLanguage()
const queryClient = useQueryClient()
const [provider, setProvider] = useState('')
const [apiKey, setApiKey] = useState('')
const [alias, setAlias] = useState('')
const [model, setModel] = useState('')
const [customModel, setCustomModel] = useState('')
const [isCustomModel, setIsCustomModel] = useState(false)
const handleProviderChange = (p: string) => {
setProvider(p)
const sug = PROVIDER_MODEL_SUGGESTIONS[p] || []
if (sug.length > 0) {
setModel(sug[0])
setIsCustomModel(false)
} else {
setModel('')
setCustomModel('')
setIsCustomModel(true)
}
}
const { data, isLoading, error } = useQuery({
queryKey: ['user', 'api-keys'],
@@ -61,10 +89,16 @@ export function ByokSettingsPanel() {
const saveMutation = useMutation({
mutationFn: async () => {
const resolvedModel = isCustomModel ? customModel : model
const res = await fetch('/api/user/api-keys', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ provider, apiKey, alias: alias || undefined }),
body: JSON.stringify({
provider,
apiKey,
alias: alias || undefined,
model: resolvedModel || undefined,
}),
})
const body = await res.json().catch(() => ({}))
if (!res.ok) {
@@ -77,6 +111,9 @@ export function ByokSettingsPanel() {
setApiKey('')
setAlias('')
setProvider('')
setModel('')
setCustomModel('')
setIsCustomModel(false)
invalidate()
},
onError: (err: Error) => {
@@ -171,7 +208,7 @@ export function ByokSettingsPanel() {
<Label htmlFor="byok-provider" className="text-[10px] font-bold uppercase tracking-widest text-concrete">{t('byokSettings.provider')}</Label>
<Select
value={provider}
onValueChange={setProvider}
onValueChange={handleProviderChange}
disabled={saveMutation.isPending}
>
<SelectTrigger id="byok-provider">
@@ -197,6 +234,64 @@ export function ByokSettingsPanel() {
/>
</div>
</div>
{/* Model Selection Row */}
{provider && (
<div className="grid gap-4 sm:grid-cols-2 pt-2 border-t border-border/40">
<div className="space-y-2">
<Label htmlFor="byok-model-select" className="text-[10px] font-bold uppercase tracking-widest text-concrete">Modèle de l'IA (Optionnel)</Label>
{PROVIDER_MODEL_SUGGESTIONS[provider] && PROVIDER_MODEL_SUGGESTIONS[provider].length > 0 ? (
<Select
value={isCustomModel ? 'custom' : model}
onValueChange={(val) => {
if (val === 'custom') {
setIsCustomModel(true)
} else {
setIsCustomModel(false)
setModel(val)
}
}}
disabled={saveMutation.isPending}
>
<SelectTrigger id="byok-model-select">
<SelectValue placeholder="Choisir un modèle..." />
</SelectTrigger>
<SelectContent>
{PROVIDER_MODEL_SUGGESTIONS[provider].map((m) => (
<SelectItem key={m} value={m}>
{m}
</SelectItem>
))}
<SelectItem value="custom">Autre / Modèle personnalisé...</SelectItem>
</SelectContent>
</Select>
) : (
<div className="text-xs text-concrete py-2 italic">
Spécifiez le modèle ci-contre si besoin.
</div>
)}
</div>
{(isCustomModel || !(PROVIDER_MODEL_SUGGESTIONS[provider] && PROVIDER_MODEL_SUGGESTIONS[provider].length > 0)) && (
<div className="space-y-2">
<Label htmlFor="byok-model-custom" className="text-[10px] font-bold uppercase tracking-widest text-concrete">Saisir le nom du modèle</Label>
<Input
id="byok-model-custom"
value={isCustomModel ? customModel : model}
onChange={(e) => {
if (isCustomModel) {
setCustomModel(e.target.value)
} else {
setModel(e.target.value)
}
}}
placeholder="ex. deepseek-reasoner, minimax-abab6.5"
disabled={saveMutation.isPending}
/>
</div>
)}
</div>
)}
<div className="space-y-2">
<Label htmlFor="byok-key" className="text-[10px] font-bold uppercase tracking-widest text-concrete">{t('byokSettings.apiKey')}</Label>
<Input
@@ -236,9 +331,14 @@ export function ByokSettingsPanel() {
>
<div className="min-w-0">
<div className="text-[13px] font-bold text-ink">{providerLabel(t, key.provider)}</div>
{key.alias ? (
<p className="text-[10px] text-concrete truncate">{key.alias}</p>
) : null}
<div className="flex flex-col gap-0.5 mt-0.5">
{key.alias ? (
<p className="text-[10px] text-concrete truncate">{key.alias}</p>
) : null}
{key.model ? (
<p className="text-[9px] text-brand-accent font-mono truncate">Modèle : {key.model}</p>
) : null}
</div>
</div>
<div className="flex items-center gap-3 shrink-0">
<label className="relative inline-flex items-center cursor-pointer">