Files
odidere/internal/service/service.go
T

790 lines
19 KiB
Go
Raw Normal View History

2026-02-13 15:03:02 +00:00
// Package service orchestrates the odidere voice assistant server.
2026-06-19 14:31:41 +00:00
// It coordinates the HTTP server, LLM clients, and handles
2026-02-13 15:03:02 +00:00
// graceful shutdown.
package service
import (
"context"
"embed"
2026-10-30 16:04:10 +00:00
"encoding/json/v2"
2026-02-13 15:03:02 +00:00
"fmt"
"html/template"
"io/fs"
"log/slog"
"net/http"
"os"
"os/signal"
"runtime/debug"
"strings"
2026-06-19 14:31:41 +00:00
"sync"
2026-02-13 15:03:02 +00:00
"syscall"
"time"
2026-10-30 16:04:10 +00:00
"uuid"
2026-02-13 15:03:02 +00:00
2026-02-21 19:47:00 +00:00
"code.chimeric.al/chimerical/odidere/internal/config"
2026-10-30 16:04:10 +00:00
"code.chimeric.al/chimerical/odidere/internal/conversation"
"code.chimeric.al/chimerical/odidere/internal/db"
2026-06-02 12:04:02 +00:00
"code.chimeric.al/chimerical/odidere/internal/job"
2026-02-21 19:47:00 +00:00
"code.chimeric.al/chimerical/odidere/internal/llm"
"code.chimeric.al/chimerical/odidere/internal/service/templates"
2026-10-30 16:04:10 +00:00
"code.chimeric.al/chimerical/odidere/internal/tool"
2026-02-13 15:03:02 +00:00
2026-06-02 12:04:02 +00:00
"github.com/robfig/cron/v3"
2026-02-13 15:03:02 +00:00
)
// Context keys for request-scoped values.
type key string
const (
logKey key = "log"
idKey key = "id"
ipKey key = "ip"
2026-06-19 14:31:41 +00:00
modelsTimeout = 10 * time.Second
)
2026-02-13 15:03:02 +00:00
//go:embed all:static/*
var static embed.FS
// Service is the main application coordinator.
// It owns the HTTP server and all processing clients.
type Service struct {
2026-06-19 14:31:41 +00:00
cfg *config.Config
cron *cron.Cron
2026-10-30 16:04:10 +00:00
db *db.SQLite
dbr *db.Queries
dbw *db.Queries
2026-06-19 14:31:41 +00:00
jobs []*job.Job
llms map[string]*llm.Client
log *slog.Logger
allowedModels map[string]map[string]struct{}
mux *http.ServeMux
server *http.Server
tmpl *template.Template
2026-10-30 16:04:10 +00:00
tools *tool.Registry
ctx context.Context
cancel context.CancelFunc
2026-10-30 17:21:58 +00:00
wg sync.WaitGroup
2026-10-30 16:04:10 +00:00
mu sync.Mutex
conversations map[string]*conversation.Conversation
2026-02-13 15:03:02 +00:00
}
// New creates a Service from the provided configuration.
// It initializes all clients and the HTTP server.
2026-10-30 16:04:10 +00:00
func New(
cfg *config.Config,
log *slog.Logger,
sdb *db.SQLite,
) (*Service, error) {
if cfg == nil {
return nil, fmt.Errorf("config is nil")
}
if err := cfg.Validate(); err != nil {
return nil, fmt.Errorf("invalid config: %w", err)
}
if log == nil {
return nil, fmt.Errorf("logger is nil")
}
if sdb == nil || sdb.Writer == nil || sdb.Reader == nil {
return nil, fmt.Errorf("database is nil")
}
ctx, cancel := context.WithCancel(context.Background())
2026-02-13 15:03:02 +00:00
var svc = &Service{
2026-10-30 16:04:10 +00:00
cfg: cfg,
db: sdb,
dbr: db.New(sdb.Reader),
dbw: db.New(sdb.Writer),
log: log,
mux: http.NewServeMux(),
ctx: ctx,
cancel: cancel,
conversations: make(map[string]*conversation.Conversation),
2026-02-13 15:03:02 +00:00
}
// Setup tool registry.
2026-10-30 16:04:10 +00:00
registry, err := tool.NewRegistry(cfg.Tools)
2026-02-13 15:03:02 +00:00
if err != nil {
return nil, fmt.Errorf("load tools: %v", err)
}
svc.tools = registry
2026-06-19 14:31:41 +00:00
// Create LLM clients for each provider.
svc.llms = make(map[string]*llm.Client, len(cfg.Providers))
for _, pc := range cfg.Providers {
client, err := llm.NewClient(
llm.Config{
2026-10-30 16:04:10 +00:00
Key: pc.Key,
Provider: pc.Name,
Timeout: pc.Timeout,
URL: pc.URL,
2026-06-19 14:31:41 +00:00
},
log,
)
if err != nil {
return nil, fmt.Errorf(
"create LLM client for provider %q: %w",
pc.Name, err,
)
}
svc.llms[pc.Name] = client
log.Info(
"LLM provider registered",
slog.String("provider", pc.Name),
slog.String("url", pc.URL),
)
}
2026-10-30 16:04:10 +00:00
if svc.llms[cfg.DefaultModel.Provider] == nil {
return nil, fmt.Errorf("unknown default provider")
}
2026-06-19 14:31:41 +00:00
// Build model allowed list map: provider name -> set of base model IDs.
svc.allowedModels = make(
map[string]map[string]struct{}, len(cfg.Providers),
)
for _, pc := range cfg.Providers {
if len(pc.Models) == 0 {
continue
}
allowed := make(map[string]struct{}, len(pc.Models))
for _, m := range pc.Models {
base, _, _ := strings.Cut(m, ":")
allowed[base] = struct{}{}
}
svc.allowedModels[pc.Name] = allowed
2026-02-13 15:03:02 +00:00
}
2026-06-02 12:04:02 +00:00
// Convert job configs to jobs.
jobs := make([]*job.Job, 0, len(cfg.Jobs))
for _, jc := range cfg.Jobs {
2026-06-19 14:31:41 +00:00
// Resolve the provider and model for this job.
2026-10-30 16:04:10 +00:00
provider, modelID := jc.Model.Provider, jc.Model.Model
if provider == "" {
provider = cfg.DefaultModel.Provider
}
if modelID == "" {
modelID = cfg.DefaultModel.Model
2026-06-19 14:31:41 +00:00
}
llmc := svc.llms[provider]
if llmc == nil {
return nil, fmt.Errorf(
"create job %q: no LLM client for provider %q",
jc.Name,
provider,
)
}
2026-10-30 16:04:10 +00:00
jc.Model = config.ModelConfig{
Provider: provider,
Model: modelID,
}
if jc.SystemMessage == "" {
jc.SystemMessage = cfg.SystemMessage
}
j, err := job.New(jc, llmc, log)
2026-06-02 12:04:02 +00:00
if err != nil {
2026-06-19 14:31:41 +00:00
return nil, fmt.Errorf(
"create job %q: %w", jc.Name, err,
)
2026-06-02 12:04:02 +00:00
}
jobs = append(jobs, j)
}
svc.jobs = jobs
2026-02-13 15:03:02 +00:00
// Parse templates.
tmpl, err := templates.Parse()
if err != nil {
return nil, fmt.Errorf("parse templates: %v", err)
}
svc.tmpl = tmpl
// Setup static file server.
staticFS, err := fs.Sub(static, "static")
if err != nil {
return nil, fmt.Errorf("setup static fs: %v", err)
}
// Register routes.
svc.mux.HandleFunc("GET /", svc.home)
svc.mux.HandleFunc("GET /status", svc.status)
svc.mux.Handle(
"GET /static/",
http.StripPrefix(
"/static/", http.FileServer(http.FS(staticFS)),
),
)
2026-10-30 16:04:10 +00:00
svc.mux.HandleFunc("GET /models", svc.models)
svc.mux.HandleFunc("GET /conversations", svc.listConversations)
svc.mux.HandleFunc("POST /conversations", svc.createConversation)
svc.mux.HandleFunc("GET /conversations/{id}", svc.getConversationHandler)
svc.mux.HandleFunc("DELETE /conversations/{id}", svc.deleteConversation)
svc.mux.HandleFunc(
"POST /conversations/{id}/inference", svc.inference,
)
svc.mux.HandleFunc("POST /conversations/{id}/stop", svc.stop)
svc.mux.HandleFunc(
"POST /conversations/{id}/subscribe", svc.subscribe,
)
svc.mux.HandleFunc(
"POST /conversations/{id}/messages", svc.insertMessage,
)
svc.mux.HandleFunc(
"PUT /conversations/{id}/messages/{mid}", svc.updateMessage,
)
svc.mux.HandleFunc(
"DELETE /conversations/{id}/messages/{mid}", svc.deleteMessage,
)
svc.mux.HandleFunc(
"POST /conversations/{id}/messages/{mid}/calls",
svc.addToolCall,
)
svc.mux.HandleFunc(
"PUT /conversations/{id}/messages/{mid}/calls/{cid}",
svc.updateToolCall,
)
svc.mux.HandleFunc(
"DELETE /conversations/{id}/messages/{mid}/calls/{cid}",
svc.deleteToolCall,
)
svc.mux.HandleFunc("GET /executions", svc.listExecutions)
svc.mux.HandleFunc("PUT /executions/{id}", svc.updateExecution)
2026-02-13 15:03:02 +00:00
svc.server = &http.Server{
Addr: cfg.Address,
Handler: svc,
}
return svc, nil
}
// ServeHTTP implements http.Handler. It logs requests, assigns a UUID,
// sets context values, handles panics, and delegates to the mux.
func (svc *Service) ServeHTTP(w http.ResponseWriter, r *http.Request) {
var (
start = time.Now()
2026-10-30 16:04:10 +00:00
id = uuid.NewV4().String()
2026-02-13 15:03:02 +00:00
ip = func() string {
if ip := r.Header.Get("X-Forwarded-For"); ip != "" {
2026-06-19 14:31:41 +00:00
before, _, found := strings.Cut(ip, ",")
if found {
return strings.TrimSpace(before)
2026-02-13 15:03:02 +00:00
}
return ip
}
return r.RemoteAddr
}()
log = svc.log.With(slog.Group(
"request",
slog.String("id", id),
slog.String("ip", ip),
slog.String("method", r.Method),
slog.String("path", r.URL.Path),
))
)
// Log completion time.
defer func() {
log.InfoContext(
r.Context(),
"completed",
slog.Duration("duration", time.Since(start)),
)
}()
// Panic recovery.
defer func() {
if err := recover(); err != nil {
log.ErrorContext(
r.Context(),
"panic recovered",
slog.Any("error", err),
slog.String("stack", string(debug.Stack())),
)
http.Error(
w,
http.StatusText(
http.StatusInternalServerError,
),
http.StatusInternalServerError,
)
}
}()
// Enrich context with request-scoped values.
ctx := r.Context()
ctx = context.WithValue(ctx, logKey, log)
ctx = context.WithValue(ctx, idKey, id)
ctx = context.WithValue(ctx, ipKey, ip)
2026-02-13 15:03:02 +00:00
r = r.WithContext(ctx)
log.InfoContext(ctx, "handling")
2026-07-05 18:14:40 +00:00
// Always set request ID header.
w.Header().Set("X-Request-ID", id)
2026-02-13 15:03:02 +00:00
// Pass the request on to the multiplexer.
svc.mux.ServeHTTP(w, r)
}
// Run starts the service and blocks until shutdown.
// Shutdown is triggered by SIGINT or SIGTERM.
2026-10-30 17:21:58 +00:00
//
// The return value tells the caller when it is safe to close the
// database:
//
// - nil: all HTTP handlers returned, and all inference calls and
// scheduled jobs finished. The caller can close the database.
// - non-nil: shutdown did not finish within the configured
// ShutdownTimeout, or the HTTP server failed. Handlers,
// inference calls, or jobs can still use the database. Do not
// close the database. Exit the process instead. The database
// runs in WAL mode, so committed data stays safe when the
// process exits without a close.
2026-02-13 15:03:02 +00:00
func (svc *Service) Run(ctx context.Context) error {
svc.log.Info(
"starting odidere",
slog.Group(
"llm",
2026-06-19 14:31:41 +00:00
slog.String(
"default_provider",
svc.cfg.DefaultModel.Provider,
),
slog.String(
"default_model", svc.cfg.DefaultModel.Model,
),
slog.Int("providers", len(svc.cfg.Providers)),
2026-02-13 15:03:02 +00:00
),
slog.Group(
"server",
slog.String("address", svc.cfg.Address),
),
slog.Group(
"tools",
slog.Int("count", len(svc.tools.List())),
slog.Any(
"names",
strings.Join(svc.tools.List(), ","),
),
),
slog.Group(
2026-06-13 18:55:16 +00:00
"tts_url",
2026-02-13 15:03:02 +00:00
slog.String("url", svc.cfg.TTS.URL),
slog.String("default_voice", svc.cfg.TTS.Voice),
),
2026-06-15 10:51:17 +00:00
slog.Group(
"stt_url",
slog.String("url", svc.cfg.STT.URL),
),
2026-06-02 12:04:02 +00:00
slog.Group(
"jobs",
slog.Int("count", svc.JobCount()),
),
2026-10-30 16:04:10 +00:00
slog.Any("shutdown_timeout", svc.cfg.ShutdownTimeout),
2026-02-13 15:03:02 +00:00
)
// Setup signal handling for graceful shutdown.
ctx, cancel := signal.NotifyContext(
ctx,
os.Interrupt, syscall.SIGTERM,
)
defer cancel()
2026-10-30 16:04:10 +00:00
svc.ctx, svc.cancel = context.WithCancel(ctx)
defer svc.cancel()
2026-02-13 15:03:02 +00:00
2026-06-02 12:04:02 +00:00
// Register jobs with cron scheduler.
svc.cron = cron.New()
for _, j := range svc.jobs {
entry, err := svc.cron.AddFunc(j.Schedule(), func() {
2026-10-30 16:04:10 +00:00
title := j.Name()
system := j.SystemMessage()
_, c, err := svc.newConversation(
svc.ctx, j.UserId(), &title, system,
)
if err != nil {
svc.log.Error("failed to create job conversation",
slog.String("name", j.Name()),
slog.Any("error", err),
)
return
}
if err := j.Run(svc.ctx, c); err != nil {
2026-06-02 12:04:02 +00:00
svc.log.Error("job failed",
slog.String("name", j.Name()),
2026-10-30 16:04:10 +00:00
slog.Any("error", err),
2026-06-02 12:04:02 +00:00
)
}
})
if err != nil {
svc.log.Error("failed to schedule job",
slog.String("name", j.Name()),
slog.Any("error", err),
)
continue
}
svc.log.Info("job scheduled",
slog.String("name", j.Name()),
slog.String("schedule", j.Schedule()),
slog.Int("entry_id", int(entry)),
)
}
svc.cron.Start()
2026-02-13 15:03:02 +00:00
// Start HTTP server in background.
var errs = make(chan error, 1)
go func() {
svc.log.Info(
"HTTP server listening",
slog.String("address", svc.cfg.Address),
)
if err := svc.server.ListenAndServe(); err != nil &&
err != http.ErrServerClosed {
errs <- err
}
close(errs)
}()
// Wait for shutdown signal or server error.
2026-10-30 16:04:10 +00:00
var runErr error
2026-02-13 15:03:02 +00:00
select {
case <-ctx.Done():
svc.log.Info("shutdown signal received")
case err := <-errs:
if err != nil {
2026-10-30 16:04:10 +00:00
runErr = fmt.Errorf("server error: %w", err)
2026-02-13 15:03:02 +00:00
}
}
2026-10-30 16:04:10 +00:00
// Use one deadline for HTTP shutdown and job completion.
// The shutdown context must remain active after svc.ctx is canceled.
2026-02-13 15:03:02 +00:00
shutdownCtx, shutdownCancel := context.WithTimeout(
context.Background(),
2026-10-30 16:04:10 +00:00
svc.cfg.ShutdownTimeout,
2026-02-13 15:03:02 +00:00
)
defer shutdownCancel()
2026-10-30 16:04:10 +00:00
svc.cancel()
2026-06-02 12:04:02 +00:00
2026-10-30 16:04:10 +00:00
svc.log.Info("stopping job scheduler")
cronCtx := svc.cron.Stop()
var shutdownErr error
2026-02-13 15:03:02 +00:00
svc.log.Info("shutting down HTTP server")
if err := svc.server.Shutdown(shutdownCtx); err != nil {
2026-10-30 16:04:10 +00:00
shutdownErr = fmt.Errorf("HTTP shutdown: %w", err)
} else {
2026-10-30 17:21:58 +00:00
// Jobs and HTTP inference can save tool results after
// cancellation. Wait until both return before the caller
// closes the database.
done := make(chan struct{})
go func() {
<-cronCtx.Done()
svc.wg.Wait()
close(done)
}()
2026-10-30 16:04:10 +00:00
select {
2026-10-30 17:21:58 +00:00
case <-done:
2026-10-30 16:04:10 +00:00
case <-shutdownCtx.Done():
shutdownErr = fmt.Errorf(
2026-10-30 17:21:58 +00:00
"wait for background work: %w",
shutdownCtx.Err(),
2026-10-30 16:04:10 +00:00
)
}
}
// On failure, main exits without closing the database because
// handlers or jobs may still use it.
if shutdownErr != nil {
if runErr != nil {
return fmt.Errorf("%w; %w", runErr, shutdownErr)
}
return shutdownErr
2026-02-13 15:03:02 +00:00
}
svc.log.Info("terminating")
2026-10-30 16:04:10 +00:00
return runErr
2026-02-13 15:03:02 +00:00
}
2026-10-30 17:21:58 +00:00
// newConversation saves a conversation and its initial system message.
// It registers the handle only after the transaction commits.
func (svc *Service) newConversation(
ctx context.Context,
userId string,
title *string,
system string,
) (*db.Conversation, *conversation.Conversation, error) {
msg := llm.Message{
Role: llm.RoleSystem,
ContentParts: []llm.ContentPart{{
Type: llm.ContentTypeText,
Text: system,
}},
}
if err := msg.Validate(); err != nil {
return nil, nil, fmt.Errorf("invalid system message: %w", err)
}
content, err := json.Marshal(msg)
if err != nil {
return nil, nil, fmt.Errorf("marshal system message: %w", err)
}
tx, err := svc.db.Writer.BeginTx(ctx, nil)
if err != nil {
return nil, nil, fmt.Errorf("begin tx: %w", err)
}
defer tx.Rollback()
qtx := svc.dbw.WithTx(tx)
saved, err := qtx.InsertConversation(
ctx, db.InsertConversationParams{
Title: title,
UserId: userId,
},
)
if err != nil {
return nil, nil, fmt.Errorf("insert conversation: %w", err)
}
if _, err := qtx.InsertMessage(ctx, db.InsertMessageParams{
Content: content,
ConversationId: saved.Id,
Role: db.MessageRoleSystem,
Sequence: 0,
}); err != nil {
return nil, nil, fmt.Errorf("insert system message: %w", err)
}
c, err := conversation.New(
conversation.Config{Id: saved.Id},
svc.db,
svc.log,
svc.tools,
)
if err != nil {
return nil, nil, fmt.Errorf("create conversation handle: %w", err)
}
if err := tx.Commit(); err != nil {
return nil, nil, fmt.Errorf("commit conversation tx: %w", err)
}
svc.mu.Lock()
// A retrieval can register a handle after the commit.
if existing := svc.conversations[saved.Id]; existing != nil {
c = existing
} else {
svc.conversations[saved.Id] = c
}
svc.mu.Unlock()
return saved, c, nil
}
// getConversation checks saved state before it returns a cached handle.
func (svc *Service) getConversation(
ctx context.Context,
id string,
) (*conversation.Conversation, error) {
if _, err := svc.dbr.SelectConversationId(ctx, id); err != nil {
return nil, fmt.Errorf("select conversation: %w", err)
}
svc.mu.Lock()
defer svc.mu.Unlock()
if c := svc.conversations[id]; c != nil {
return c, nil
}
c, err := conversation.New(
conversation.Config{Id: id},
svc.db,
svc.log,
svc.tools,
)
if err != nil {
return nil, fmt.Errorf("create conversation handle: %w", err)
}
svc.conversations[id] = c
return c, nil
}
2026-06-02 12:04:02 +00:00
// ListJobs returns all registered jobs.
func (svc *Service) ListJobs() []*job.Job {
return svc.jobs
}
// JobCount returns the number of registered jobs.
func (svc *Service) JobCount() int {
return len(svc.jobs)
}
2026-02-13 15:03:02 +00:00
func (svc *Service) home(w http.ResponseWriter, r *http.Request) {
var (
ctx = r.Context()
log = ctx.Value(logKey).(*slog.Logger)
2026-02-13 15:03:02 +00:00
)
if r.URL.Path != "/" {
http.NotFound(w, r)
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
if err := svc.tmpl.ExecuteTemplate(
2026-06-13 18:55:16 +00:00
w, "index.gohtml", struct {
TTSURL string
TTSDefaultVoice string
2026-06-15 10:51:17 +00:00
STTURL string
2026-06-13 18:55:16 +00:00
}{
TTSURL: svc.cfg.TTS.URL,
TTSDefaultVoice: svc.cfg.TTS.Voice,
2026-06-15 10:51:17 +00:00
STTURL: svc.cfg.STT.URL,
2026-06-13 18:55:16 +00:00
},
2026-02-13 15:03:02 +00:00
); err != nil {
log.ErrorContext(
ctx, "template error", slog.Any("error", err),
)
http.Error(
w,
http.StatusText(http.StatusInternalServerError),
http.StatusInternalServerError,
)
}
}
// status returns server status.
func (svc *Service) status(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}
2026-06-19 14:31:41 +00:00
// ModelStatus represents the availability status of a model.
type ModelStatus string
const (
// ModelStatusAvailable indicates the model is available for use.
ModelStatusAvailable ModelStatus = "available"
// ModelStatusNotFound indicates the model is configured but not found
// in the provider's model list.
ModelStatusNotFound ModelStatus = "not_found"
// ModelStatusProviderError indicates the provider could not be reached
// to verify the model.
ModelStatusProviderError ModelStatus = "provider_error"
)
2026-10-30 16:04:10 +00:00
// Model represents a model in the /models response.
2026-06-19 14:31:41 +00:00
type Model struct {
2026-06-27 12:35:11 +00:00
Model string `json:"model"`
Status ModelStatus `json:"status"`
ContextSize *int `json:"context_size,omitempty"`
2026-06-19 14:31:41 +00:00
}
2026-10-30 16:04:10 +00:00
// Models is the response format for the /models endpoint.
2026-06-19 14:31:41 +00:00
type Models struct {
Providers map[string][]Model `json:"providers"`
DefaultProvider string `json:"default_provider"`
DefaultModel string `json:"default_model"`
}
// models returns available LLM models from all configured providers.
2026-02-13 15:03:02 +00:00
func (svc *Service) models(w http.ResponseWriter, r *http.Request) {
var (
ctx = r.Context()
log = ctx.Value(logKey).(*slog.Logger)
2026-06-19 14:31:41 +00:00
mu sync.Mutex
wg sync.WaitGroup
providers = make(map[string][]Model, len(svc.llms))
2026-02-13 15:03:02 +00:00
)
2026-06-19 14:31:41 +00:00
cfg := make(map[string]config.ProviderConfig, len(svc.cfg.Providers))
for _, pc := range svc.cfg.Providers {
cfg[pc.Name] = pc
2026-02-13 15:03:02 +00:00
}
2026-06-19 14:31:41 +00:00
for name, client := range svc.llms {
wg.Add(1)
go func(name string, c *llm.Client, pc config.ProviderConfig) {
defer wg.Done()
var result = []Model{}
fetchCtx, cancel := context.WithTimeout(
ctx, modelsTimeout,
)
defer cancel()
models, err := c.ListModels(fetchCtx)
if err != nil {
log.WarnContext(
ctx,
"failed to list models for provider",
slog.String("provider", name),
slog.Any("error", err),
)
for _, m := range pc.Models {
result = append(result, Model{
Model: m,
Status: ModelStatusProviderError,
})
}
mu.Lock()
providers[name] = result
mu.Unlock()
return
}
available := make(map[string]struct{}, len(models))
2026-06-27 12:35:11 +00:00
contextSizes := make(map[string]int, len(models))
2026-06-19 14:31:41 +00:00
for _, m := range models {
baseID, _, _ := strings.Cut(m.ID, ":")
available[baseID] = struct{}{}
2026-06-29 16:25:42 +00:00
cs := m.ContextLength
if cs == 0 && m.Meta != nil {
cs = m.Meta.NCtx
}
if cs > 0 {
contextSizes[baseID] = cs
2026-06-27 12:35:11 +00:00
}
2026-06-19 14:31:41 +00:00
}
var toCheck []string
if len(pc.Models) > 0 {
toCheck = pc.Models
} else {
toCheck = make([]string, 0, len(models))
for _, m := range models {
toCheck = append(toCheck, m.ID)
}
}
for _, m := range toCheck {
base, _, _ := strings.Cut(m, ":")
mi := Model{Model: m}
if _, ok := available[base]; ok {
mi.Status = ModelStatusAvailable
2026-06-27 12:35:11 +00:00
if cs, ok := contextSizes[base]; ok {
mi.ContextSize = &cs
}
2026-06-19 14:31:41 +00:00
} else {
mi.Status = ModelStatusNotFound
}
result = append(result, mi)
}
mu.Lock()
providers[name] = result
mu.Unlock()
}(name, client, cfg[name])
}
wg.Wait()
2026-02-13 15:03:02 +00:00
w.Header().Set("Content-Type", "application/json")
2026-10-30 16:04:10 +00:00
if err := json.MarshalWrite(w, Models{
2026-06-19 14:31:41 +00:00
Providers: providers,
DefaultProvider: svc.cfg.DefaultModel.Provider,
DefaultModel: svc.cfg.DefaultModel.Model,
2026-02-13 15:03:02 +00:00
}); err != nil {
2026-06-19 14:31:41 +00:00
log.ErrorContext(ctx, "failed to encode models response",
slog.Any("error", err))
2026-02-13 15:03:02 +00:00
}
}