Files
Codex d8a29bfbdd Add full shell, replaceable Omics adapter and bilingual interaction
Implement approved specification #32 and tickets #33-#37. Keep host authentication server-verified and pin session interaction language. Compile scoped base selectors for browser compatibility and retain full gutters during CSS pruning.
2026-09-13 14:26:39 +02:00

384 lines
14 KiB
Go

// Package modelprojection renders disposable model and public frontend installation projections.
package modelprojection
import (
"bytes"
"crypto/sha256"
"encoding/json"
"fmt"
"os"
"path/filepath"
"runtime"
"sort"
"strconv"
"github.com/aritmolab/thothii/tools/tht/internal/config"
)
const (
CatalogFile = "catalog.json"
PiModelsFile = "pi/models.json"
PiSettingsFile = "pi/settings.json"
ComposeFile = "compose.models.yaml"
FrontendConfigFile = "frontend/config.js"
)
var renameProjectionDirectory = os.Rename
type runtimeCatalog struct {
SchemaVersion int `json:"schemaVersion"`
DefaultInteraction string `json:"defaultInteraction"`
Embedding config.ModelCatalogEmbedding `json:"embedding"`
Models []runtimeCatalogModel `json:"models"`
}
type runtimeCatalogModel struct {
ID string `json:"id"`
Provider string `json:"provider"`
Model string `json:"model"`
Label string `json:"label"`
UpstreamModel string `json:"upstreamModel"`
Endpoint *config.ModelEndpoint `json:"endpoint,omitempty"`
Authentication config.ModelAuthentication `json:"authentication"`
SessionAdapter *config.ModelSessionAdapter `json:"sessionAdapter,omitempty"`
MetadataAdapter *config.ModelMetadataAdapter `json:"metadataAdapter,omitempty"`
Session *config.SessionModel `json:"session,omitempty"`
MetadataGeneration *config.MetadataGenerationModel `json:"metadataGeneration,omitempty"`
}
type piModels struct {
Providers map[string]piProvider `json:"providers"`
}
type piProvider struct {
BaseURL string `json:"baseUrl"`
API string `json:"api"`
APIKey string `json:"apiKey"`
Models []piModel `json:"models"`
}
type piModel struct {
ID string `json:"id"`
Name string `json:"name"`
Reasoning bool `json:"reasoning"`
Input []string `json:"input,omitempty"`
Cost *config.ModelCost `json:"cost,omitempty"`
ContextWindow int `json:"contextWindow"`
MaxTokens int `json:"maxTokens"`
Compat *config.ModelCompatibility `json:"compat,omitempty"`
}
type piSettings struct {
DefaultProjectTrust string `json:"defaultProjectTrust"`
EnabledModels []string `json:"enabledModels"`
}
// Render builds every projection in memory so no partial candidate can be published.
func Render(installation config.Installation) (map[string][]byte, error) {
if err := installation.ModelCatalog.NormalizeDefaults(); err != nil {
return nil, err
}
if err := installation.Shell.NormalizeDefaults(); err != nil {
return nil, err
}
models := installation.ModelCatalog.RuntimeModels()
hasMetadata := false
for _, model := range models {
hasMetadata = hasMetadata || model.MetadataGeneration != nil
}
runtimeModels := make([]runtimeCatalogModel, 0, len(models))
customProviders := make(map[string]piProvider)
enabled := make([]string, 0)
for _, model := range models {
sessionAdapter, metadataAdapter := model.SessionAdapter, model.MetadataAdapter
if model.Session == nil {
sessionAdapter = nil
}
if model.MetadataGeneration == nil {
metadataAdapter = nil
}
runtimeModels = append(runtimeModels, runtimeCatalogModel{
ID: model.ID, Provider: model.Provider, Model: model.Model, Label: model.Label,
UpstreamModel: model.UpstreamModel, Endpoint: model.Endpoint,
Authentication: model.Authentication, SessionAdapter: sessionAdapter,
MetadataAdapter: metadataAdapter, Session: model.Session,
MetadataGeneration: model.MetadataGeneration,
})
if model.Session == nil || (hasMetadata && model.MetadataGeneration == nil) {
continue
}
if model.SessionAdapter == nil || model.SessionAdapter.Mode != "openai_compatible" {
enabled = append(enabled, model.ID)
continue
}
enabled = append(enabled, model.Provider+"/"+model.UpstreamModel)
provider := customProviders[model.Provider]
provider.BaseURL = model.Endpoint.BaseURL
provider.API = "openai-completions"
if model.Authentication.Mode == "secret_env" {
provider.APIKey = "$" + model.Authentication.APIKeyEnv
} else {
provider.APIKey = "local"
}
provider.Models = append(provider.Models, piModel{
ID: model.UpstreamModel, Name: model.Label, Reasoning: model.Session.Reasoning,
Input: model.Session.Input, Cost: model.Session.Cost,
ContextWindow: model.Session.ContextWindow, MaxTokens: model.Session.MaxTokens,
Compat: model.Session.Compatibility,
})
customProviders[model.Provider] = provider
}
for id, provider := range customProviders {
sort.Slice(provider.Models, func(left, right int) bool { return provider.Models[left].ID < provider.Models[right].ID })
customProviders[id] = provider
}
sort.Strings(enabled)
catalogBytes, err := marshalJSON(runtimeCatalog{
SchemaVersion: 2, DefaultInteraction: installation.ModelCatalog.Defaults.Interaction,
Embedding: installation.ModelCatalog.Embedding, Models: runtimeModels,
})
if err != nil {
return nil, fmt.Errorf("render runtime model catalog: %w", err)
}
piModelsBytes, err := marshalJSON(piModels{Providers: customProviders})
if err != nil {
return nil, fmt.Errorf("render Pi model projection: %w", err)
}
piSettingsBytes, err := marshalJSON(piSettings{DefaultProjectTrust: "always", EnabledModels: enabled})
if err != nil {
return nil, fmt.Errorf("render Pi settings projection: %w", err)
}
fingerprint := sha256.Sum256(bytes.Join([][]byte{catalogBytes, piModelsBytes, piSettingsBytes}, nil))
publicJSON, err := marshalJSON(struct {
BackendBaseURL string `json:"backendBaseUrl"`
Shell config.Shell `json:"shell"`
}{BackendBaseURL: "/api", Shell: installation.Shell})
if err != nil {
return nil, fmt.Errorf("render public frontend configuration: %w", err)
}
publicJS := append([]byte("window.__THOTHII_CONFIG__ = "), bytes.TrimSpace(publicJSON)...)
publicJS = append(publicJS, ';', '\n')
publicFingerprint := sha256.Sum256(publicJS)
composeBytes := renderCompose(installation, fmt.Sprintf("sha256:%x", fingerprint), fmt.Sprintf("sha256:%x", publicFingerprint))
return map[string][]byte{
CatalogFile: catalogBytes, PiModelsFile: piModelsBytes,
PiSettingsFile: piSettingsBytes, ComposeFile: composeBytes,
FrontendConfigFile: publicJS,
}, nil
}
// Generate publishes all adapters as one directory generation. An unchanged generation stays in
// place so active file bind mounts keep their source identity. A failed replacement restores the
// previous directory, so callers never observe a successfully returned mixed generation.
func Generate(installation config.Installation) error {
artifacts, err := Render(installation)
if err != nil {
return err
}
target := installation.GeneratedDirectory()
parent := filepath.Dir(target)
if err := os.MkdirAll(parent, 0o755); err != nil {
return fmt.Errorf("create model projection parent: %w", err)
}
info, statErr := os.Lstat(target)
if statErr == nil {
if !info.IsDir() || info.Mode()&os.ModeSymlink != 0 {
return fmt.Errorf("current model projection path is not a regular directory")
}
if projectionMatches(target, artifacts) {
return nil
}
} else if !os.IsNotExist(statErr) {
return fmt.Errorf("inspect current model projection generation: %w", statErr)
}
candidate, err := os.MkdirTemp(parent, ".model-projections-candidate-*")
if err != nil {
return fmt.Errorf("create model projection candidate: %w", err)
}
defer func() { _ = os.RemoveAll(candidate) }()
if err := writeProjectionCandidate(candidate, artifacts); err != nil {
return err
}
if os.IsNotExist(statErr) {
if err := renameProjectionDirectory(candidate, target); err != nil {
return fmt.Errorf("publish model projection generation: %w", err)
}
return nil
}
previous, err := absentTemporaryPath(parent)
if err != nil {
return fmt.Errorf("reserve previous model projection generation: %w", err)
}
if err := renameProjectionDirectory(target, previous); err != nil {
return fmt.Errorf("prepare model projection generation replacement: %w", err)
}
if err := renameProjectionDirectory(candidate, target); err != nil {
if restoreErr := renameProjectionDirectory(previous, target); restoreErr != nil {
return fmt.Errorf("publish model projection generation: %v; restore previous generation: %w", err, restoreErr)
}
return fmt.Errorf("publish model projection generation: %w", err)
}
_ = os.RemoveAll(previous)
return nil
}
func projectionMatches(directory string, artifacts map[string][]byte) bool {
if !projectionModeMatches(directory, 0o755) {
return false
}
for _, relative := range sortedArtifactPaths(artifacts) {
path := filepath.Join(directory, filepath.FromSlash(relative))
info, err := os.Lstat(path)
if err != nil || !info.Mode().IsRegular() || !projectionModeMatches(path, 0o644) ||
!projectionModeMatches(filepath.Dir(path), 0o755) {
return false
}
actual, err := os.ReadFile(path)
if err != nil || !bytes.Equal(actual, artifacts[relative]) {
return false
}
}
return true
}
func projectionModeMatches(path string, mode os.FileMode) bool {
if runtime.GOOS == "windows" {
return true // POSIX mode bits do not represent Windows access controls.
}
info, err := os.Lstat(path)
return err == nil && info.Mode()&os.ModeSymlink == 0 && info.Mode().Perm() == mode
}
func writeProjectionCandidate(directory string, artifacts map[string][]byte) error {
if err := os.Chmod(directory, 0o755); err != nil {
return fmt.Errorf("protect model projection candidate: %w", err)
}
for _, relative := range sortedArtifactPaths(artifacts) {
destination := filepath.Join(directory, filepath.FromSlash(relative))
if err := os.MkdirAll(filepath.Dir(destination), 0o755); err != nil {
return fmt.Errorf("create model projection directory: %w", err)
}
if err := os.Chmod(filepath.Dir(destination), 0o755); err != nil {
return fmt.Errorf("set runtime projection directory permissions: %w", err)
}
if err := os.WriteFile(destination, artifacts[relative], 0o644); err != nil {
return fmt.Errorf("write model projection candidate: %w", err)
}
// Creation modes are filtered by the host umask; container readers have another UID.
if err := os.Chmod(destination, 0o644); err != nil {
return fmt.Errorf("set runtime projection file permissions: %w", err)
}
}
return nil
}
func absentTemporaryPath(parent string) (string, error) {
path, err := os.MkdirTemp(parent, ".model-projections-previous-*")
if err != nil {
return "", err
}
if err := os.Remove(path); err != nil {
return "", err
}
return path, nil
}
// Check returns relative artifact names whose bytes or POSIX modes differ from the projection.
func Check(installation config.Installation) ([]string, error) {
artifacts, err := Render(installation)
if err != nil {
return nil, err
}
drift := make([]string, 0)
for _, relative := range sortedArtifactPaths(artifacts) {
path := filepath.Join(installation.GeneratedDirectory(), filepath.FromSlash(relative))
actual, readErr := os.ReadFile(path)
if readErr != nil || !bytes.Equal(actual, artifacts[relative]) ||
!projectionModeMatches(installation.GeneratedDirectory(), 0o755) ||
!projectionModeMatches(filepath.Dir(path), 0o755) || !projectionModeMatches(path, 0o644) {
drift = append(drift, relative)
}
}
return drift, nil
}
func marshalJSON(value any) ([]byte, error) {
contents, err := json.MarshalIndent(value, "", " ")
if err != nil {
return nil, err
}
return append(contents, '\n'), nil
}
func renderCompose(installation config.Installation, fingerprint, publicFingerprint string) []byte {
embedding := installation.ModelCatalog.Embedding
return []byte(fmt.Sprintf(`services:
frontend:
environment:
THT_FRONTEND_CONFIG_REVISION: %s
volumes:
- type: bind
source: %s
target: /usr/share/nginx/html/config.js
read_only: true
bind:
create_host_path: false
core:
environment:
THT_HOST_PLATFORM: %s
THT_MODEL_CATALOG_FILE: /run/thothii-model-catalog/catalog.json
THT_MODEL_CATALOG_REVISION: %s
THT_DEFAULT_SESSION_MODEL: %s
THT_INTERNAL_EMBEDDING_ID: %s
THT_INTERNAL_EMBEDDING_MODEL: %s
THT_INTERNAL_EMBEDDING_DIMENSIONS: %s
volumes:
- type: bind
source: %s
target: /run/thothii-model-catalog/catalog.json
read_only: true
- type: bind
source: %s
target: /home/thoth/.pi/agent/models.json
read_only: true
- type: bind
source: %s
target: /home/thoth/.pi/agent/settings.json
read_only: true
workspace-maintenance:
environment:
THT_INTERNAL_EMBEDDING_ID: %s
THT_INTERNAL_EMBEDDING_MODEL: %s
THT_INTERNAL_EMBEDDING_DIMENSIONS: %s
embedding-model-init:
environment:
OLLAMA_MODEL: %s
`, strconv.Quote(publicFingerprint), strconv.Quote(installation.GeneratedFrontendConfigPath()),
strconv.Quote(runtime.GOOS), strconv.Quote(fingerprint), strconv.Quote(installation.ModelCatalog.Defaults.Interaction),
strconv.Quote(embedding.ID), strconv.Quote(embeddingModelName(embedding.ID)), strconv.Quote(strconv.Itoa(embedding.Dimensions)),
strconv.Quote(installation.GeneratedModelCatalogPath()), strconv.Quote(installation.GeneratedPiModelsPath()),
strconv.Quote(installation.GeneratedPiSettingsPath()), strconv.Quote(embedding.ID),
strconv.Quote(embeddingModelName(embedding.ID)), strconv.Quote(strconv.Itoa(embedding.Dimensions)),
strconv.Quote(embeddingModelName(embedding.ID))))
}
func embeddingModelName(canonical string) string {
for index := 0; index < len(canonical); index++ {
if canonical[index] == '/' {
return canonical[index+1:]
}
}
return canonical
}
func sortedArtifactPaths(artifacts map[string][]byte) []string {
paths := make([]string, 0, len(artifacts))
for path := range artifacts {
paths = append(paths, path)
}
sort.Strings(paths)
return paths
}