mirror of
https://github.com/mudler/LocalAI.git
synced 2026-11-02 02:29:05 -05:00
* feat(images): add upscale model capabilities * fix(config): keep SD GGML upscale opt-in * feat(stablediffusion): add lazy ESRGAN upscale bridge Keep native image ownership in C++ and serialize context creation, inference and teardown. Use upscale_scale in backend options as the explicit upscale-only marker because ModelOptions does not carry usecases. Assisted-by: Codex:gpt-5 * fix(stablediffusion): validate ESRGAN settings * fix(upscale): forward typed settings and contain resolved model paths Typed settings replace legacy options without duplicates. Resolve model and trusted root symlinks before caching the model path, rejecting missing files and escapes. Assisted-by: Codex:gpt-5 * fix(upscale): validate scale and clean failed HTTP outputs Require an explicit scale bounded to 1-16, check backend results, and keep destination cleanup owned by the allocating endpoint. Cover validation, result failures, cleanup, and successful URL responses. Assisted-by: Codex:gpt-5 * feat(studio): add inline image upscale workflow Assisted-by: OpenAI:gpt-5 * test(studio): serve upscale capability fixtures * fix(stablediffusion): default ESRGAN tiling to full frame * feat(gallery): add RealESRGAN anime upscaler * fix(stablediffusion): allow full-frame ESRGAN tiles * fix(routes): select explicit upscale models Give image upscaling dedicated usecase-filtered middleware and keep ordinary image models out of the endpoint. Mark and verify the legacy Diffusers x4 gallery model as an explicit upscaler. Assisted-by: OpenAI:gpt-5 [shell] * refactor(upscale): keep settings in backend options * test(config): keep upscale options backend-specific * fix: keep upscale routing metadata out of backend options --------- Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
308 lines
15 KiB
Go
308 lines
15 KiB
Go
package routes
|
|
|
|
import (
|
|
"net/http"
|
|
|
|
"github.com/labstack/echo/v4"
|
|
"github.com/mudler/LocalAI/core/application"
|
|
"github.com/mudler/LocalAI/core/config"
|
|
"github.com/mudler/LocalAI/core/http/endpoints/localai"
|
|
mcpTools "github.com/mudler/LocalAI/core/http/endpoints/mcp"
|
|
"github.com/mudler/LocalAI/core/http/endpoints/openai"
|
|
"github.com/mudler/LocalAI/core/http/middleware"
|
|
"github.com/mudler/LocalAI/core/schema"
|
|
compressionservice "github.com/mudler/LocalAI/core/services/compression"
|
|
"github.com/mudler/LocalAI/core/services/routing/pii"
|
|
"github.com/mudler/LocalAI/core/services/routing/piiadapter"
|
|
"github.com/mudler/LocalAI/core/services/routing/router"
|
|
"github.com/mudler/LocalAI/pkg/tokens"
|
|
)
|
|
|
|
func RegisterOpenAIRoutes(app *echo.Echo,
|
|
re *middleware.RequestExtractor,
|
|
application *application.Application,
|
|
) {
|
|
// openAI compatible API endpoint
|
|
traceMiddleware := middleware.TraceMiddleware(application)
|
|
usageMiddleware := middleware.UsageMiddleware(application.StatsRecorder(), application.FallbackUser())
|
|
// X-LocalAI-Node attribution middleware: wraps the response writer and
|
|
// stamps the header on first write when --expose-node-header is on. No-op
|
|
// otherwise. Applied to every inference path that routes through
|
|
// ml.Load (chat, completion, embeddings, audio transcriptions/speech,
|
|
// image generation/inpainting) so distributed-mode operators can observe
|
|
// which worker served each request.
|
|
nodeHeaderMiddleware := middleware.ExposeNodeHeader(application.ApplicationConfig())
|
|
|
|
// realtime
|
|
// TODO: Modify/disable the API key middleware for this endpoint to allow ephemeral keys created by sessions
|
|
app.GET("/v1/realtime", openai.Realtime(application))
|
|
app.POST("/v1/realtime/sessions", openai.RealtimeTranscriptionSession(application), traceMiddleware)
|
|
app.POST("/v1/realtime/transcription_session", openai.RealtimeTranscriptionSession(application), traceMiddleware)
|
|
app.POST("/v1/realtime/calls", openai.RealtimeCalls(application), traceMiddleware)
|
|
|
|
// Agent control for distributed MCP tool routing (nil when not in distributed mode)
|
|
var agentControl mcpTools.AgentControl
|
|
if d := application.Distributed(); d != nil {
|
|
agentControl = d.AgentControl
|
|
}
|
|
|
|
// chat
|
|
chatCompressor := compressionservice.New(
|
|
compressionservice.CounterFunc(tokens.CountMessages),
|
|
compressionservice.NewInferenceSummarizer(application.ModelConfigLoader(), application.ModelLoader(), application.ApplicationConfig()),
|
|
)
|
|
chatHandler := openai.ChatEndpoint(application.ModelConfigLoader(), application.ModelLoader(), application.TemplatesEvaluator(), application.ApplicationConfig(), agentControl, application.LocalAIAssistant(), chatCompressor)
|
|
chatMiddleware := []echo.MiddlewareFunc{
|
|
nodeHeaderMiddleware,
|
|
usageMiddleware,
|
|
traceMiddleware,
|
|
re.SetModelAndConfigWithDefault(func() schema.LocalAIRequest { return new(schema.OpenAIRequest) }, config.BuildUsecaseFilterFn(config.FLAG_CHAT)),
|
|
func(next echo.HandlerFunc) echo.HandlerFunc {
|
|
return func(c echo.Context) error {
|
|
if err := re.SetOpenAIRequest(c); err != nil {
|
|
return err
|
|
}
|
|
return next(c)
|
|
}
|
|
},
|
|
// RouteModel runs AFTER the schema-specific request parser so
|
|
// the classifier sees a populated *schema.OpenAIRequest. When
|
|
// the resolved model has a Router config, the middleware
|
|
// rewrites input.Model to the chosen candidate, swaps
|
|
// MODEL_CONFIG, and stamps RequestedModel/ServedModel for the
|
|
// usage log. Models without a Router pass through.
|
|
middleware.RouteModel(
|
|
application.ModelConfigLoader(),
|
|
application.ApplicationConfig(),
|
|
application.RouterDecisions(),
|
|
application.FallbackUser(),
|
|
middleware.OpenAIProbe,
|
|
router.SourceChat,
|
|
middleware.NewClassifierDeps(application),
|
|
),
|
|
// Admission control runs after RouteModel so the SERVED
|
|
// model's limits apply — a router fanout that lands on a
|
|
// saturated downstream gets rejected even when the requested
|
|
// router-model has slack.
|
|
middleware.AdmissionControl(application.AdmissionLimiter(), application.PIIEvents()),
|
|
// PII redaction runs after RouteModel has resolved the actual served
|
|
// model and before compression. This makes per-model PII
|
|
// configs honour the routed target (e.g., a router fans out to
|
|
// claude-strict; that model's pii block applies, not the router
|
|
// model's), and prevents the compressor from seeing unredacted input.
|
|
pii.RequestMiddleware(application.PIIRedactor(), application.PIIEvents(), piiadapter.OpenAI(), application.FallbackUser(), pii.WithNERResolver(application.PIINERResolver()), pii.WithPolicyResolver(application.PIIPolicyResolver())),
|
|
}
|
|
app.POST("/v1/chat/completions", chatHandler, chatMiddleware...)
|
|
app.POST("/chat/completions", chatHandler, chatMiddleware...)
|
|
|
|
// edit
|
|
editHandler := openai.EditEndpoint(application.ModelConfigLoader(), application.ModelLoader(), application.TemplatesEvaluator(), application.ApplicationConfig())
|
|
editMiddleware := []echo.MiddlewareFunc{
|
|
usageMiddleware,
|
|
traceMiddleware,
|
|
re.BuildFilteredFirstAvailableDefaultModel(config.BuildUsecaseFilterFn(config.FLAG_EDIT)),
|
|
re.BuildConstantDefaultModelNameMiddleware("gpt-4o"),
|
|
re.SetModelAndConfig(func() schema.LocalAIRequest { return new(schema.OpenAIRequest) }),
|
|
func(next echo.HandlerFunc) echo.HandlerFunc {
|
|
return func(c echo.Context) error {
|
|
if err := re.SetOpenAIRequest(c); err != nil {
|
|
return err
|
|
}
|
|
return next(c)
|
|
}
|
|
},
|
|
pii.RequestMiddleware(application.PIIRedactor(), application.PIIEvents(), piiadapter.OpenAICompletion(), application.FallbackUser(), pii.WithNERResolver(application.PIINERResolver()), pii.WithPolicyResolver(application.PIIPolicyResolver())),
|
|
}
|
|
app.POST("/v1/edits", editHandler, editMiddleware...)
|
|
app.POST("/edits", editHandler, editMiddleware...)
|
|
|
|
// completion
|
|
completionHandler := openai.CompletionEndpoint(application.ModelConfigLoader(), application.ModelLoader(), application.TemplatesEvaluator(), application.ApplicationConfig())
|
|
completionMiddleware := []echo.MiddlewareFunc{
|
|
nodeHeaderMiddleware,
|
|
usageMiddleware,
|
|
traceMiddleware,
|
|
re.BuildFilteredFirstAvailableDefaultModel(config.BuildUsecaseFilterFn(config.FLAG_COMPLETION)),
|
|
re.BuildConstantDefaultModelNameMiddleware("gpt-4o"),
|
|
re.SetModelAndConfig(func() schema.LocalAIRequest { return new(schema.OpenAIRequest) }),
|
|
func(next echo.HandlerFunc) echo.HandlerFunc {
|
|
return func(c echo.Context) error {
|
|
if err := re.SetOpenAIRequest(c); err != nil {
|
|
return err
|
|
}
|
|
return next(c)
|
|
}
|
|
},
|
|
pii.RequestMiddleware(application.PIIRedactor(), application.PIIEvents(), piiadapter.OpenAICompletion(), application.FallbackUser(), pii.WithNERResolver(application.PIINERResolver()), pii.WithPolicyResolver(application.PIIPolicyResolver())),
|
|
}
|
|
app.POST("/v1/completions", completionHandler, completionMiddleware...)
|
|
app.POST("/completions", completionHandler, completionMiddleware...)
|
|
app.POST("/v1/engines/:model/completions", completionHandler, completionMiddleware...)
|
|
|
|
// moderation
|
|
moderationHandler := openai.ModerationEndpoint(application.ModelConfigLoader(), application.ModelLoader(), application.TemplatesEvaluator(), application.ApplicationConfig())
|
|
moderationMiddleware := []echo.MiddlewareFunc{
|
|
nodeHeaderMiddleware,
|
|
usageMiddleware,
|
|
traceMiddleware,
|
|
re.BuildFilteredFirstAvailableDefaultModel(config.BuildUsecaseFilterFn(config.FLAG_COMPLETION)),
|
|
re.BuildConstantDefaultModelNameMiddleware("gpt-4o"),
|
|
re.SetModelAndConfig(func() schema.LocalAIRequest { return new(schema.ModerationRequest) }),
|
|
middleware.AdmissionControl(application.AdmissionLimiter(), application.PIIEvents()),
|
|
}
|
|
app.POST("/v1/moderations", moderationHandler, moderationMiddleware...)
|
|
app.POST("/moderations", moderationHandler, moderationMiddleware...)
|
|
|
|
// embeddings
|
|
embeddingHandler := openai.EmbeddingsEndpoint(application.ModelConfigLoader(), application.ModelLoader(), application.TemplatesEvaluator(), application.ApplicationConfig())
|
|
embeddingMiddleware := []echo.MiddlewareFunc{
|
|
nodeHeaderMiddleware,
|
|
usageMiddleware,
|
|
traceMiddleware,
|
|
re.BuildFilteredFirstAvailableDefaultModel(config.BuildUsecaseFilterFn(config.FLAG_EMBEDDINGS)),
|
|
re.BuildConstantDefaultModelNameMiddleware("gpt-4o"),
|
|
re.SetModelAndConfig(func() schema.LocalAIRequest { return new(schema.OpenAIRequest) }),
|
|
func(next echo.HandlerFunc) echo.HandlerFunc {
|
|
return func(c echo.Context) error {
|
|
if err := re.SetOpenAIRequest(c); err != nil {
|
|
return err
|
|
}
|
|
return next(c)
|
|
}
|
|
},
|
|
pii.RequestMiddleware(application.PIIRedactor(), application.PIIEvents(), piiadapter.OpenAICompletion(), application.FallbackUser(), pii.WithNERResolver(application.PIINERResolver()), pii.WithPolicyResolver(application.PIIPolicyResolver())),
|
|
}
|
|
app.POST("/v1/embeddings", embeddingHandler, embeddingMiddleware...)
|
|
app.POST("/embeddings", embeddingHandler, embeddingMiddleware...)
|
|
app.POST("/v1/engines/:model/embeddings", embeddingHandler, embeddingMiddleware...)
|
|
|
|
audioHandler := openai.TranscriptEndpoint(application.ModelConfigLoader(), application.ModelLoader(), application.ApplicationConfig())
|
|
audioMiddleware := []echo.MiddlewareFunc{
|
|
nodeHeaderMiddleware,
|
|
usageMiddleware,
|
|
traceMiddleware,
|
|
re.BuildFilteredFirstAvailableDefaultModel(config.BuildUsecaseFilterFn(config.FLAG_TRANSCRIPT)),
|
|
re.SetModelAndConfig(func() schema.LocalAIRequest { return new(schema.OpenAIRequest) }),
|
|
func(next echo.HandlerFunc) echo.HandlerFunc {
|
|
return func(c echo.Context) error {
|
|
if err := re.SetOpenAIRequest(c); err != nil {
|
|
return err
|
|
}
|
|
return next(c)
|
|
}
|
|
},
|
|
}
|
|
// audio
|
|
app.POST("/v1/audio/transcriptions", audioHandler, audioMiddleware...)
|
|
app.POST("/audio/transcriptions", audioHandler, audioMiddleware...)
|
|
|
|
diarizationHandler := openai.DiarizationEndpoint(application.ModelConfigLoader(), application.ModelLoader(), application.ApplicationConfig(), application.VoiceRegistry(), application.AuthDB())
|
|
diarizationMiddleware := []echo.MiddlewareFunc{
|
|
traceMiddleware,
|
|
re.BuildFilteredFirstAvailableDefaultModel(config.BuildUsecaseFilterFn(config.FLAG_DIARIZATION)),
|
|
re.SetModelAndConfig(func() schema.LocalAIRequest { return new(schema.OpenAIRequest) }),
|
|
func(next echo.HandlerFunc) echo.HandlerFunc {
|
|
return func(c echo.Context) error {
|
|
if err := re.SetOpenAIRequest(c); err != nil {
|
|
return err
|
|
}
|
|
return next(c)
|
|
}
|
|
},
|
|
}
|
|
app.POST("/v1/audio/diarization", diarizationHandler, diarizationMiddleware...)
|
|
app.POST("/audio/diarization", diarizationHandler, diarizationMiddleware...)
|
|
|
|
soundClassificationHandler := openai.SoundClassificationEndpoint(application.ModelConfigLoader(), application.ModelLoader(), application.ApplicationConfig())
|
|
soundClassificationMiddleware := []echo.MiddlewareFunc{
|
|
traceMiddleware,
|
|
re.BuildFilteredFirstAvailableDefaultModel(config.BuildUsecaseFilterFn(config.FLAG_SOUND_CLASSIFICATION)),
|
|
re.SetModelAndConfig(func() schema.LocalAIRequest { return new(schema.OpenAIRequest) }),
|
|
func(next echo.HandlerFunc) echo.HandlerFunc {
|
|
return func(c echo.Context) error {
|
|
if err := re.SetOpenAIRequest(c); err != nil {
|
|
return err
|
|
}
|
|
return next(c)
|
|
}
|
|
},
|
|
}
|
|
app.POST("/v1/audio/classification", soundClassificationHandler, soundClassificationMiddleware...)
|
|
app.POST("/audio/classification", soundClassificationHandler, soundClassificationMiddleware...)
|
|
|
|
audioSpeechHandler := localai.TTSEndpoint(application.ModelConfigLoader(), application.ModelLoader(), application.ApplicationConfig(), application.VoiceProfileStore())
|
|
audioSpeechMiddleware := []echo.MiddlewareFunc{
|
|
nodeHeaderMiddleware,
|
|
traceMiddleware,
|
|
re.BuildFilteredFirstAvailableDefaultModel(config.BuildUsecaseFilterFn(config.FLAG_TTS)),
|
|
re.SetModelAndConfig(func() schema.LocalAIRequest { return new(schema.TTSRequest) }),
|
|
}
|
|
|
|
app.POST("/v1/audio/speech", audioSpeechHandler, audioSpeechMiddleware...)
|
|
app.POST("/audio/speech", audioSpeechHandler, audioSpeechMiddleware...)
|
|
app.GET("/v1/audio/voices", localai.TTSVoicesEndpoint(application.ModelConfigLoader(), application.AuthDB()))
|
|
app.GET("/audio/voices", localai.TTSVoicesEndpoint(application.ModelConfigLoader(), application.AuthDB()))
|
|
|
|
// images
|
|
imageHandler := openai.ImageEndpoint(application.ModelConfigLoader(), application.ModelLoader(), application.ApplicationConfig())
|
|
imageMiddleware := []echo.MiddlewareFunc{
|
|
nodeHeaderMiddleware,
|
|
traceMiddleware,
|
|
// Default: use the first available image generation model
|
|
re.BuildFilteredFirstAvailableDefaultModel(config.BuildUsecaseFilterFn(config.FLAG_IMAGE)),
|
|
re.SetModelAndConfig(func() schema.LocalAIRequest { return new(schema.OpenAIRequest) }),
|
|
func(next echo.HandlerFunc) echo.HandlerFunc {
|
|
return func(c echo.Context) error {
|
|
if err := re.SetOpenAIRequest(c); err != nil {
|
|
return err
|
|
}
|
|
return next(c)
|
|
}
|
|
},
|
|
}
|
|
|
|
app.POST("/v1/images/generations", imageHandler, imageMiddleware...)
|
|
app.POST("/images/generations", imageHandler, imageMiddleware...)
|
|
|
|
// inpainting endpoint (image + mask) - reuse same middleware config as images
|
|
inpaintingHandler := openai.InpaintingEndpoint(application.ModelConfigLoader(), application.ModelLoader(), application.ApplicationConfig())
|
|
app.POST("/v1/images/inpainting", inpaintingHandler, imageMiddleware...)
|
|
app.POST("/images/inpainting", inpaintingHandler, imageMiddleware...)
|
|
|
|
// Upscaling has its own model usecase; generation models are not defaults.
|
|
upscaleMiddleware := []echo.MiddlewareFunc{
|
|
nodeHeaderMiddleware,
|
|
traceMiddleware,
|
|
// Default: use the first available upscaling model
|
|
re.BuildFilteredFirstAvailableDefaultModel(config.BuildUsecaseFilterFn(config.FLAG_UPSCALE)),
|
|
re.SetModelAndConfig(func() schema.LocalAIRequest { return new(schema.OpenAIRequest) }),
|
|
func(next echo.HandlerFunc) echo.HandlerFunc {
|
|
return func(c echo.Context) error {
|
|
// The default-model filter does not validate explicitly named models.
|
|
cfg, ok := c.Get(middleware.CONTEXT_LOCALS_KEY_MODEL_CONFIG).(*config.ModelConfig)
|
|
if !ok || cfg == nil || !cfg.HasUsecases(config.FLAG_UPSCALE) {
|
|
return echo.NewHTTPError(http.StatusBadRequest, "model does not support image upscaling")
|
|
}
|
|
if err := re.SetOpenAIRequest(c); err != nil {
|
|
return err
|
|
}
|
|
return next(c)
|
|
}
|
|
},
|
|
}
|
|
|
|
upscaleHandler := openai.UpscaleEndpoint(application.ModelConfigLoader(), application.ModelLoader(), application.ApplicationConfig())
|
|
app.POST("/v1/images/upscale", upscaleHandler, upscaleMiddleware...)
|
|
app.POST("/images/upscale", upscaleHandler, upscaleMiddleware...)
|
|
|
|
// List models
|
|
app.GET("/v1/models", openai.ListModelsEndpoint(application.ModelConfigLoader(), application.ModelLoader(), application.ApplicationConfig(), application.AuthDB()))
|
|
app.GET("/models", openai.ListModelsEndpoint(application.ModelConfigLoader(), application.ModelLoader(), application.ApplicationConfig(), application.AuthDB()))
|
|
|
|
// List models enriched with capabilities + input/output modalities
|
|
// (LocalAI-specific, additive superset of /v1/models).
|
|
capabilitiesHandler := openai.ListModelCapabilitiesEndpoint(application.ModelConfigLoader(), application.ModelLoader(), application.ApplicationConfig(), application.AuthDB())
|
|
app.GET("/v1/models/capabilities", capabilitiesHandler)
|
|
app.GET("/models/capabilities", capabilitiesHandler)
|
|
}
|