feat: add model picker on non-ollama llm

This commit is contained in:
2026-04-21 09:17:48 +02:00
parent 2761282c0b
commit 985768f400
5 changed files with 82 additions and 10 deletions

View File

@ -39,15 +39,7 @@ func (p *Pipeline) IsGenerating() bool {
} }
func (p *Pipeline) BuildProvider(name, apiKey, endpoint string) (Provider, error) { func (p *Pipeline) BuildProvider(name, apiKey, endpoint string) (Provider, error) {
provider, err := p.repo.GetActiveAIProvider() return NewProvider(name, apiKey, "", endpoint)
if err != nil {
return nil, err
}
model := ""
if provider != nil {
model = provider.Model
}
return NewProvider(name, apiKey, model, endpoint)
} }
// buildProviderForRole resolves and builds the AI provider for a given task role. // buildProviderForRole resolves and builds the AI provider for a given task role.

View File

@ -174,6 +174,25 @@ func (h *Handler) DeleteAIProvider(c *gin.Context) {
httputil.NoContent(c) httputil.NoContent(c)
} }
func (h *Handler) ProbeAIModels(c *gin.Context) {
var req aiProviderRequest
if err := c.ShouldBindJSON(&req); err != nil {
httputil.BadRequest(c, err)
return
}
provider, err := h.pipeline.BuildProvider(req.Name, req.APIKey, req.Endpoint)
if err != nil {
httputil.InternalError(c, err)
return
}
models, err := provider.ListModels(c.Request.Context())
if err != nil {
httputil.InternalError(c, err)
return
}
httputil.OK(c, models)
}
func (h *Handler) ListAIModels(c *gin.Context) { func (h *Handler) ListAIModels(c *gin.Context) {
id := c.Param("id") id := c.Param("id")
p, err := h.repo.GetAIProviderByID(id) p, err := h.repo.GetAIProviderByID(id)

View File

@ -63,6 +63,7 @@ func SetupRouter(h *handlers.Handler, jwtSecret string) *gin.Engine {
admin.POST("/ai-providers/:id/activate", h.SetActiveAIProvider) admin.POST("/ai-providers/:id/activate", h.SetActiveAIProvider)
admin.DELETE("/ai-providers/:id", h.DeleteAIProvider) admin.DELETE("/ai-providers/:id", h.DeleteAIProvider)
admin.GET("/ai-providers/:id/models", h.ListAIModels) admin.GET("/ai-providers/:id/models", h.ListAIModels)
admin.POST("/ai-providers/probe-models", h.ProbeAIModels)
admin.GET("/sources", h.ListSources) admin.GET("/sources", h.ListSources)
admin.PUT("/sources/:id", h.UpdateSource) admin.PUT("/sources/:id", h.UpdateSource)

View File

@ -37,6 +37,8 @@ export const adminApi = {
activateProvider: (id: string) => api.post<void>(`/admin/ai-providers/${id}/activate`), activateProvider: (id: string) => api.post<void>(`/admin/ai-providers/${id}/activate`),
deleteProvider: (id: string) => api.delete<void>(`/admin/ai-providers/${id}`), deleteProvider: (id: string) => api.delete<void>(`/admin/ai-providers/${id}`),
listModels: (id: string) => api.get<string[]>(`/admin/ai-providers/${id}/models`), listModels: (id: string) => api.get<string[]>(`/admin/ai-providers/${id}/models`),
probeModels: (data: { name: string; api_key?: string; endpoint?: string }) =>
api.post<string[]>('/admin/ai-providers/probe-models', data),
// AI Roles // AI Roles
getRoles: () => api.get<AIRoles>('/admin/ai-roles'), getRoles: () => api.get<AIRoles>('/admin/ai-roles'),

View File

@ -214,6 +214,58 @@ function ModelTag({ fullName, size, installed, isPulling, pulling, onRequest }:
) )
} }
// ── Cloud model picker (OpenAI / Anthropic / Gemini / …) ──────────────────
function CloudModelPicker({ value, providerName, apiKey, endpoint, onChange }: {
value: string; providerName: string; apiKey: string; endpoint: string
onChange: (model: string) => void
}) {
const [models, setModels] = useState<string[]>([])
const [loading, setLoading] = useState(false)
const [loadError, setLoadError] = useState('')
async function loadModels() {
if (!apiKey) { setLoadError('Renseigne la clé API d\'abord'); return }
setLoading(true); setLoadError('')
try {
const list = await adminApi.probeModels({ name: providerName, api_key: apiKey, endpoint })
setModels(list ?? [])
if ((list ?? []).length === 0) setLoadError('Aucun modèle trouvé')
} catch (e) {
setLoadError(e instanceof Error ? e.message : 'Erreur')
} finally { setLoading(false) }
}
return (
<div className="space-y-1.5">
<div className="flex gap-2">
<Input
placeholder="gpt-4o-mini, claude-sonnet-4-6…"
value={value}
onChange={e => onChange(e.target.value)}
list="cloud-models-list"
className="flex-1"
/>
<Button type="button" variant="outline" size="sm" className="shrink-0" onClick={loadModels} disabled={loading}>
{loading ? <Spinner className="h-3 w-3" /> : 'Charger'}
</Button>
</div>
{models.length > 0 && (
<datalist id="cloud-models-list">
{models.map(m => <option key={m} value={m} />)}
</datalist>
)}
{models.length > 0 && (
<Select value={value} onChange={e => onChange(e.target.value)}>
<option value=""> Sélectionner un modèle </option>
{models.map(m => <option key={m} value={m}>{m}</option>)}
</Select>
)}
{loadError && <p className="text-xs text-destructive">{loadError}</p>}
</div>
)
}
// ── Ollama model picker (inside provider form) ───────────────────────────── // ── Ollama model picker (inside provider form) ─────────────────────────────
function OllamaModelPicker({ value, installedModels, onSelect, onRefresh }: { function OllamaModelPicker({ value, installedModels, onSelect, onRefresh }: {
@ -530,7 +582,13 @@ export function AIProviders() {
onSelect={m => setForm(f => ({ ...f, model: m }))} onSelect={m => setForm(f => ({ ...f, model: m }))}
onRefresh={loadOllamaModels} /> onRefresh={loadOllamaModels} />
) : ( ) : (
<Input placeholder="gpt-4o-mini, claude-sonnet-4-6…" value={form.model} onChange={e => setForm(f => ({ ...f, model: e.target.value }))} /> <CloudModelPicker
value={form.model}
providerName={form.name}
apiKey={form.api_key}
endpoint={form.endpoint}
onChange={m => setForm(f => ({ ...f, model: m }))}
/>
)} )}
</div> </div>
{error && <p className="text-sm text-destructive">{error}</p>} {error && <p className="text-sm text-destructive">{error}</p>}