feat: add model picker on non-ollama llm
This commit is contained in:
@ -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.
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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'),
|
||||||
|
|||||||
@ -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>}
|
||||||
|
|||||||
Reference in New Issue
Block a user