mirror of
https://github.com/mudler/LocalAI.git
synced 2026-09-21 21:54:52 -04:00
Distributed mode has not dialled a message broker since the control plane moved onto the workers' own outward tunnels and every fan-out family moved onto PostgreSQL LISTEN/NOTIFY. What was left was the dependency itself, and the code that existed only to feed it. Dropped from go.mod: nats-io/jwt/v2, nats-io/nats.go, nats-io/nkeys, nats-io/nuid and testcontainers-go/modules/nats, along with the fourteen indirect requires that only the NATS testcontainer pulled in. go.sum carries no nats line either, so the removal is not the partial kind where the require goes and the checksum stays. Deleted with them: pkg/natsauth in full, the broker client's remaining options and TLS files, the per-node JWT minting on both the register and the approve path, and the natsauth.Config parameter threaded through the node routes. The credential manager is renamed and stripped rather than deleted, because it still holds the tunnel token that every re-registration rotates. The bus flags stay accepted and ignored, and are now hidden, on every command that had them, so an existing unit file, compose file or Helm values file still starts on the day of the upgrade. What is not kept is the validation that REQUIRED one: a distributed frontend started with no bus URL is no longer fatal. The TLS paths lose type:"existingfile" deliberately, so a certificate deleted along with the broker cannot fail a startup. One operator-visible behaviour change: --nats-require-auth no longer makes an agent worker wait through admin approval. Ask for that wait with --distributed-require-auth, which already implied it. It is documented in the migration section and pinned from both sides. A deployment now needs PostgreSQL and the frontends' own HTTP listener, and nothing else. coverage-baseline.txt moves from 54.2 to 62.0. Assisted-by: Claude Opus 5 [claude-code] Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
112 lines
5.1 KiB
Go
112 lines
5.1 KiB
Go
package routes
|
|
|
|
import (
|
|
"context"
|
|
|
|
"github.com/labstack/echo/v4"
|
|
"github.com/mudler/LocalAI/core/application"
|
|
"github.com/mudler/LocalAI/core/config"
|
|
localai "github.com/mudler/LocalAI/core/http/endpoints/localai"
|
|
"github.com/mudler/LocalAI/core/http/endpoints/openresponses"
|
|
"github.com/mudler/LocalAI/core/http/middleware"
|
|
"github.com/mudler/LocalAI/core/schema"
|
|
"github.com/mudler/LocalAI/core/services/distributed"
|
|
"github.com/mudler/xlog"
|
|
)
|
|
|
|
func RegisterOpenResponsesRoutes(app *echo.Echo,
|
|
re *middleware.RequestExtractor,
|
|
application *application.Application) {
|
|
|
|
// How the MCP endpoints reach an agent worker; nil outside distributed mode.
|
|
agentControl := mcpAgentControl(application)
|
|
if d := application.Distributed(); d != nil {
|
|
if err := enableDistributedResponses(application.ApplicationConfig().Context, d,
|
|
openresponses.GetGlobalStore(), application.InstanceID()); err != nil {
|
|
xlog.Error("Failed to enable cross-replica Open Responses store", "error", err)
|
|
}
|
|
}
|
|
|
|
// Open Responses API endpoint
|
|
responsesHandler := openresponses.ResponsesEndpoint(
|
|
application.ModelConfigLoader(),
|
|
application.ModelLoader(),
|
|
application.TemplatesEvaluator(),
|
|
application.ApplicationConfig(),
|
|
agentControl,
|
|
)
|
|
|
|
responsesMiddleware := []echo.MiddlewareFunc{
|
|
// Intercept requests where the model name matches an agent — route directly
|
|
// to the agent pool without going through the model config resolution pipeline.
|
|
localai.AgentResponsesInterceptor(application),
|
|
middleware.UsageMiddleware(application.StatsRecorder(), application.FallbackUser()),
|
|
middleware.TraceMiddleware(application),
|
|
re.BuildFilteredFirstAvailableDefaultModel(config.BuildUsecaseFilterFn(config.FLAG_CHAT)),
|
|
re.SetModelAndConfig(func() schema.LocalAIRequest { return new(schema.OpenResponsesRequest) }),
|
|
setOpenResponsesRequestContext(re),
|
|
}
|
|
|
|
// Main Open Responses endpoint
|
|
app.POST("/v1/responses", responsesHandler, responsesMiddleware...)
|
|
|
|
// Also support without version prefix for compatibility
|
|
app.POST("/responses", responsesHandler, responsesMiddleware...)
|
|
|
|
// WebSocket mode for Responses API
|
|
wsHandler := openresponses.WebSocketEndpoint(application)
|
|
app.GET("/v1/responses", wsHandler, middleware.UsageMiddleware(application.StatsRecorder(), application.FallbackUser()), middleware.TraceMiddleware(application))
|
|
app.GET("/responses", wsHandler, middleware.UsageMiddleware(application.StatsRecorder(), application.FallbackUser()), middleware.TraceMiddleware(application))
|
|
|
|
// GET /responses/:id - Retrieve a response (for polling background requests)
|
|
getResponseHandler := openresponses.GetResponseEndpoint()
|
|
app.GET("/v1/responses/:id", getResponseHandler, middleware.TraceMiddleware(application))
|
|
app.GET("/responses/:id", getResponseHandler, middleware.TraceMiddleware(application))
|
|
|
|
// POST /responses/:id/cancel - Cancel a background response
|
|
cancelResponseHandler := openresponses.CancelResponseEndpoint()
|
|
app.POST("/v1/responses/:id/cancel", cancelResponseHandler, middleware.TraceMiddleware(application))
|
|
app.POST("/responses/:id/cancel", cancelResponseHandler, middleware.TraceMiddleware(application))
|
|
}
|
|
|
|
// setOpenResponsesRequestContext sets up the context and cancel function for Open Responses requests
|
|
func setOpenResponsesRequestContext(re *middleware.RequestExtractor) echo.MiddlewareFunc {
|
|
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
|
return func(c echo.Context) error {
|
|
if err := re.SetOpenResponsesRequest(c); err != nil {
|
|
return err
|
|
}
|
|
return next(c)
|
|
}
|
|
}
|
|
}
|
|
|
|
// enableDistributedResponses replicates response metadata across frontend
|
|
// replicas and subscribes to delegated cancels. Without it a GET, a
|
|
// previous_response_id lookup or a cancel that the load balancer sends to a
|
|
// replica other than the creator 404s, and the cancel never reaches the
|
|
// CancelFunc (#10993). Standalone deployments never reach here and stay
|
|
// process-local.
|
|
//
|
|
// The durable store is what a replica re-hydrates from after its subscription
|
|
// missed a delta; without it the same response_id answers 404 here and 200 on
|
|
// the peer that created it, forever.
|
|
//
|
|
// A named function rather than a block inside route registration, and that is
|
|
// the point of it. EnableDistributed takes a messaging.Broadcaster, as it must:
|
|
// its own specs publish through a double. So handing it any carrier other than
|
|
// the deployment's COMPILES and reddens nothing anywhere, and the only symptom
|
|
// is a cancel that answers 404 on every replica but one. The broker client that
|
|
// used to be the second carrier in scope is gone; what pins the choice is the
|
|
// spec beside this file, which drives it from the OTHER carrier. Registering
|
|
// routes needs a whole Application and therefore has no spec; this needs a
|
|
// DistributedServices and a store, and therefore has one.
|
|
func enableDistributedResponses(ctx context.Context, d *application.DistributedServices,
|
|
store *openresponses.ResponseStore, replicaID string) error {
|
|
var responseStore *distributed.ResponseMetadataStore
|
|
if d.DistStores != nil {
|
|
responseStore = d.DistStores.Responses
|
|
}
|
|
return store.EnableDistributed(ctx, d.Broadcast(), replicaID, responseStore)
|
|
}
|