Parent directory

bootstrap.go

3635 bytes
  1// Package bootstrap builds the service clients shared by the kaiwari entry points:
  2// scenario load, self-hosted model services (unless disabled), one LLM client per
  3// prompt slot, and the prompt warmup. Every failure is returned as an error.
  4package bootstrap
  5
  6import (
  7	"context"
  8	"fmt"
  9	"net/http"
 10	"time"
 11
 12	"git.theedgeofrage.com/TheEdgeOfRage/kaiwari/internal/config"
 13	"git.theedgeofrage.com/TheEdgeOfRage/kaiwari/internal/llm"
 14	"git.theedgeofrage.com/TheEdgeOfRage/kaiwari/internal/scenario"
 15	"git.theedgeofrage.com/TheEdgeOfRage/kaiwari/internal/services"
 16)
 17
 18const (
 19	warmupTimeout = 30 * time.Second
 20
 21	// llama-server slot assignments are fixed for this deployment; they are not
 22	// user-configurable. One slot per prompt family keeps each system prompt
 23	// cached in the server's unified KV pool; scratch questions and flashcards
 24	// share a slot since both are rare one-shot side tasks.
 25	gameSlot       = 0
 26	judgeSlot      = 1
 27	compactionSlot = 2
 28	sheetSlot      = 3
 29	scratchSlot    = 4
 30)
 31
 32// Services is the set of shared clients one kaiwari entry point builds at startup.
 33type Services struct {
 34	Cfg        *config.Config
 35	Brief      string
 36	HTTP       *http.Client
 37	Game       *llm.Client
 38	Judge      *llm.Client
 39	Compaction *llm.Client
 40	Sheet      *llm.Client
 41	Scratch    *llm.Client
 42
 43	manager *services.Manager
 44}
 45
 46// New loads the scenario, starts the self-hosted model services unless disabled,
 47// builds one LLM client per slot, and warms up the prompt slots. Any failure
 48// tears down whatever was started and is returned as an error. Cancelling ctx
 49// aborts startup and cleans up any spawned children.
 50func New(ctx context.Context, cfg *config.Config) (*Services, error) {
 51	sc, err := scenario.Load(cfg.Scenario)
 52	if err != nil {
 53		return nil, fmt.Errorf("load scenario: %w", err)
 54	}
 55
 56	c := *cfg
 57	var manager *services.Manager
 58	if !c.DisableModelLoading {
 59		manager = services.NewManager()
 60		if err := manager.Start(ctx); err != nil {
 61			return nil, err
 62		}
 63		// Managed children bind loopback; pin the endpoints regardless of URL flags.
 64		c.LLMConfig.BaseURL = "http://" + services.LLMListen
 65		c.AudioConfig.AudioBaseURL = "http://" + services.AudioListen
 66	}
 67
 68	hc := &http.Client{}
 69	gameClient := newLLMClient(&c, hc, gameSlot)
 70	judgeClient := newLLMClient(&c, hc, judgeSlot)
 71	compactionClient := newLLMClient(&c, hc, compactionSlot)
 72	sheetClient := newLLMClient(&c, hc, sheetSlot)
 73	scratchClient := newLLMClient(&c, hc, scratchSlot)
 74
 75	warmups := []struct {
 76		client *llm.Client
 77		prompt string
 78	}{
 79		{gameClient, llm.GameSystemPrompt(sc.Brief)},
 80		{judgeClient, llm.JudgeSystemPrompt()},
 81		{compactionClient, llm.CompactionPrompt()},
 82		{sheetClient, llm.SheetSystemPrompt()},
 83	}
 84	warmCtx, cancel := context.WithTimeout(ctx, warmupTimeout)
 85	defer cancel()
 86	var warmErr error
 87	for _, w := range warmups {
 88		if warmErr = w.client.Warmup(warmCtx, []string{w.prompt}); warmErr != nil {
 89			break
 90		}
 91	}
 92	if warmErr != nil {
 93		if manager != nil {
 94			manager.Stop()
 95		}
 96		return nil, fmt.Errorf("llm warmup: %w", warmErr)
 97	}
 98
 99	return &Services{
100		Cfg:        &c,
101		Brief:      sc.Brief,
102		HTTP:       hc,
103		Game:       gameClient,
104		Judge:      judgeClient,
105		Compaction: compactionClient,
106		Sheet:      sheetClient,
107		Scratch:    scratchClient,
108		manager:    manager,
109	}, nil
110}
111
112// Close stops the spawned model services if any were started. Safe to call when
113// none were and safe to call multiple times.
114func (s *Services) Close() {
115	if s.manager != nil {
116		s.manager.Stop()
117	}
118}
119
120func newLLMClient(cfg *config.Config, hc *http.Client, slot int) *llm.Client {
121	c := llm.NewClient(cfg, hc)
122	c.Slot = slot
123	return c
124}