Files
vessel/backend/internal/api/routes.go
vikingowl a80ddc0fe4 feat: add multi-backend LLM support (Ollama, llama.cpp, LM Studio)
Add unified backend abstraction layer supporting multiple LLM providers:

Backend (Go):
- New backends package with interface, registry, and adapters
- Ollama adapter wrapping existing functionality
- OpenAI-compatible adapter for llama.cpp and LM Studio
- Unified API routes under /api/v1/ai/*
- SSE to NDJSON streaming conversion for OpenAI backends
- Auto-discovery of backends on default ports

Frontend (Svelte 5):
- New backendsState store for backend management
- Unified LLM client routing through backend API
- AI Providers tab combining Backends and Models sub-tabs
- Backend-aware chat streaming (uses appropriate client)
- Model name display for non-Ollama backends in top nav
- Persist and restore last selected backend

Key features:
- Switch between backends without restart
- Conditional UI based on backend capabilities
- Models tab only visible when Ollama active
- llama.cpp/LM Studio show loaded model name
2026-01-23 15:04:49 +01:00

150 lines
5.2 KiB
Go

package api
import (
"database/sql"
"log"
"github.com/gin-gonic/gin"
"vessel-backend/internal/backends"
)
// SetupRoutes configures all API routes
func SetupRoutes(r *gin.Engine, db *sql.DB, ollamaURL string, appVersion string, registry *backends.Registry) {
// Initialize Ollama service with official client
ollamaService, err := NewOllamaService(ollamaURL)
if err != nil {
log.Printf("Warning: Failed to initialize Ollama service: %v", err)
}
// Initialize model registry service
var modelRegistry *ModelRegistryService
if ollamaService != nil {
modelRegistry = NewModelRegistryService(db, ollamaService.Client())
} else {
modelRegistry = NewModelRegistryService(db, nil)
}
// Health check
r.GET("/health", func(c *gin.Context) {
c.JSON(200, gin.H{"status": "ok"})
})
// Version endpoint (for update notifications)
r.GET("/api/v1/version", VersionHandler(appVersion))
// API v1 routes
v1 := r.Group("/api/v1")
{
// Chat routes
chats := v1.Group("/chats")
{
chats.GET("", ListChatsHandler(db))
chats.GET("/grouped", ListGroupedChatsHandler(db))
chats.POST("", CreateChatHandler(db))
chats.GET("/:id", GetChatHandler(db))
chats.PUT("/:id", UpdateChatHandler(db))
chats.DELETE("/:id", DeleteChatHandler(db))
// Message routes (nested under chats)
chats.POST("/:id/messages", CreateMessageHandler(db))
}
// Sync routes
sync := v1.Group("/sync")
{
sync.POST("/push", PushChangesHandler(db))
sync.GET("/pull", PullChangesHandler(db))
}
// URL fetch proxy (for tools that need to fetch external URLs)
// Uses curl/wget when available, falls back to native Go HTTP client
v1.POST("/proxy/fetch", URLFetchProxyHandler())
v1.GET("/proxy/fetch-method", GetFetchMethodHandler())
// Web search proxy (for web_search tool)
v1.POST("/proxy/search", WebSearchProxyHandler())
// IP-based geolocation (fallback when browser geolocation fails)
v1.GET("/location", IPGeolocationHandler())
// Tool execution (for Python tools)
v1.POST("/tools/execute", ExecuteToolHandler())
// Model registry routes (cached models from ollama.com)
models := v1.Group("/models")
{
// === Local Models (from Ollama instance) ===
// List local models with filtering, sorting, pagination
models.GET("/local", modelRegistry.ListLocalModelsHandler())
// Get unique model families for filter dropdowns
models.GET("/local/families", modelRegistry.GetLocalFamiliesHandler())
// Check for available updates (compares local vs remote registry)
models.GET("/local/updates", modelRegistry.CheckUpdatesHandler())
// === Remote Models (from ollama.com cache) ===
// List/search remote models (from cache)
models.GET("/remote", modelRegistry.ListRemoteModelsHandler())
// Get unique model families for filter dropdowns
models.GET("/remote/families", modelRegistry.GetRemoteFamiliesHandler())
// Get single model details
models.GET("/remote/:slug", modelRegistry.GetRemoteModelHandler())
// Fetch detailed info from Ollama (requires model to be pulled)
models.POST("/remote/:slug/details", modelRegistry.FetchModelDetailsHandler())
// Fetch tag sizes from ollama.com (scrapes model detail page)
models.POST("/remote/:slug/sizes", modelRegistry.FetchTagSizesHandler())
// Sync models from ollama.com
models.POST("/remote/sync", modelRegistry.SyncModelsHandler())
// Get sync status
models.GET("/remote/status", modelRegistry.SyncStatusHandler())
}
// Unified AI routes (multi-backend support)
if registry != nil {
aiHandlers := NewAIHandlers(registry)
ai := v1.Group("/ai")
{
// Backend management
ai.GET("/backends", aiHandlers.ListBackendsHandler())
ai.POST("/backends/discover", aiHandlers.DiscoverBackendsHandler())
ai.POST("/backends/active", aiHandlers.SetActiveHandler())
ai.GET("/backends/:type/health", aiHandlers.HealthCheckHandler())
ai.POST("/backends/register", aiHandlers.RegisterBackendHandler())
// Unified model and chat endpoints (route to active backend)
ai.GET("/models", aiHandlers.ListModelsHandler())
ai.POST("/chat", aiHandlers.ChatHandler())
}
}
// Ollama API routes (using official client)
if ollamaService != nil {
ollama := v1.Group("/ollama")
{
// Model management
ollama.GET("/api/tags", ollamaService.ListModelsHandler())
ollama.POST("/api/show", ollamaService.ShowModelHandler())
ollama.POST("/api/pull", ollamaService.PullModelHandler())
ollama.POST("/api/create", ollamaService.CreateModelHandler())
ollama.DELETE("/api/delete", ollamaService.DeleteModelHandler())
ollama.POST("/api/copy", ollamaService.CopyModelHandler())
// Chat and generation
ollama.POST("/api/chat", ollamaService.ChatHandler())
ollama.POST("/api/generate", ollamaService.GenerateHandler())
// Embeddings
ollama.POST("/api/embed", ollamaService.EmbedHandler())
ollama.POST("/api/embeddings", ollamaService.EmbedHandler()) // Legacy endpoint
// Status
ollama.GET("/api/version", ollamaService.VersionHandler())
ollama.GET("/", ollamaService.HeartbeatHandler())
}
}
// Fallback proxy for direct Ollama access (separate path to avoid conflicts)
v1.Any("/ollama-proxy/*path", OllamaProxyHandler(ollamaURL))
}
}