Files
zzet--gortex/internal/serverstack/embedder.go
T
wehub-resource-sync a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:33:42 +08:00

166 lines
6.3 KiB
Go

package serverstack
import (
"fmt"
"os"
"strings"
"github.com/zzet/gortex/internal/config"
"github.com/zzet/gortex/internal/embedding"
)
// EmbedderRequest carries the explicit, per-invocation embedding inputs a
// command collected from its flags and environment. ResolveEmbedder
// merges these with the on-disk config to decide what provider — if any —
// to construct. Fields are exported so cmd entry points can build the
// request across the package boundary.
type EmbedderRequest struct {
// FlagChanged reports whether the `--embeddings` boolean flag was
// explicitly set (cmd.Flags().Changed). Only an explicitly-set flag
// overrides the config.
FlagChanged bool
// FlagEnabled is the value of `--embeddings`. Meaningful only when
// FlagChanged is true.
FlagEnabled bool
// FlagURL / FlagModel are `--embeddings-url` / `--embeddings-model`.
// A non-empty URL forces the API provider — the most explicit request.
FlagURL string
FlagModel string
}
// ResolveEmbedder decides which embedding.Provider (if any) to install,
// applying a fixed precedence: an explicit URL (flag or env) forces the
// API provider; an explicit on/off signal (flag or env) decides
// enablement and the provider comes from the `embedding:` config; else
// the config decides (default: semantic search ON with the zero-download
// static GloVe provider). The returned string describes the decision for
// logging ("" when no embedder was built); the SelectionReport records every
// backend the local auto-selection tried (empty for other providers) so a
// silent degradation to static is observable; a non-nil error means an embedder
// was requested but could not be constructed.
func ResolveEmbedder(req EmbedderRequest, cfg *config.Config) (embedding.Provider, string, embedding.SelectionReport, error) {
if url := firstNonEmpty(req.FlagURL, os.Getenv("GORTEX_EMBEDDINGS_URL")); url != "" {
model := firstNonEmpty(req.FlagModel, os.Getenv("GORTEX_EMBEDDINGS_MODEL"))
return embedding.NewAPIProvider(url, model), fmt.Sprintf("api (%s)", url), embedding.SelectionReport{}, nil
}
embCfg := config.EmbeddingConfig{}
if cfg != nil {
embCfg = cfg.Embedding
}
explicitEnabled, haveExplicit := explicitEmbeddingToggle(req)
if haveExplicit {
if !explicitEnabled {
return nil, "", embedding.SelectionReport{}, nil
}
return buildConfiguredEmbedder(embCfg, "enabled by flag/env")
}
// No explicit flag/env toggle. An explicit `embedding.enabled: false`
// still wins and disables the vector channel.
if embCfg.Enabled != nil && !*embCfg.Enabled {
return nil, "", embedding.SelectionReport{}, nil
}
// An explicit `embedding.enabled: true` builds whatever provider is
// configured, including the baked static GloVe one.
if embCfg.Enabled != nil && *embCfg.Enabled {
return buildConfiguredEmbedder(embCfg, "enabled by config")
}
// Otherwise (the default, unset state) build a vector index ONLY when
// the user has configured a real, model-backed embedder. The static
// GloVe provider's dim-50 word vectors add little over FTS5/BM25 text
// search yet cost 0.6-0.7s of every index to build, so it is now
// opt-in: FTS5 text search serves the default, and semantic search
// turns on when a `local`/`api` provider (or embedding.enabled: true,
// or GORTEX_EMBEDDINGS=1) is configured.
if isRealEmbedder(embCfg.EmbeddingProviderOrDefault()) {
return buildConfiguredEmbedder(embCfg, "real embedder configured")
}
return nil, "", embedding.SelectionReport{}, nil
}
// isRealEmbedder reports whether the named provider is a model-backed
// embedder worth the index-time vector build. The baked `static` GloVe
// provider is excluded: its dim-50 averaged word vectors add little over
// FTS5 text search, so it no longer builds a vector index by default
// (opt in with embedding.enabled: true or GORTEX_EMBEDDINGS=1).
func isRealEmbedder(provider string) bool {
switch strings.ToLower(strings.TrimSpace(provider)) {
case "local", "api":
return true
default:
return false
}
}
// explicitEmbeddingToggle reports whether the caller gave an explicit
// on/off signal for embeddings, and what it was. The flag takes
// precedence over GORTEX_EMBEDDINGS.
func explicitEmbeddingToggle(req EmbedderRequest) (enabled, haveExplicit bool) {
if req.FlagChanged {
return req.FlagEnabled, true
}
switch strings.ToLower(strings.TrimSpace(os.Getenv("GORTEX_EMBEDDINGS"))) {
case "1", "true", "yes", "on", "y":
return true, true
case "0", "false", "no", "off", "n":
return false, true
default:
return false, false
}
}
// buildConfiguredEmbedder constructs the provider named by the config
// block (defaulting to the static GloVe provider). The returned description
// names the backend actually constructed — "local (hugot)" for an
// auto-selected local backend, or "local → static fallback" when the chain
// degraded — so the startup log tells the truth rather than echoing the
// configured name.
func buildConfiguredEmbedder(embCfg config.EmbeddingConfig, why string) (embedding.Provider, string, embedding.SelectionReport, error) {
provider := embCfg.EmbeddingProviderOrDefault()
variant := firstNonEmpty(os.Getenv("GORTEX_EMBEDDINGS_VARIANT"), embCfg.Variant)
p, report, err := embedding.NewProviderFromConfigWithReport(embedding.ProviderConfig{
Provider: provider,
APIURL: embCfg.APIURL,
APIModel: embCfg.APIModel,
Variant: variant,
})
if err != nil {
return nil, "", report, err
}
desc := provider
switch {
case variant != "" && provider == "local":
desc = fmt.Sprintf("%s/%s", provider, variant)
case provider == "local" && report.Chosen == "static":
desc = "local → static fallback"
case provider == "local" && report.Chosen != "":
desc = fmt.Sprintf("local (%s)", report.Chosen)
}
return p, fmt.Sprintf("%s — %s", desc, why), report, nil
}
// EmbeddingChunkOptions translates the chunking knobs of an
// EmbeddingConfig into the embedding package's ChunkOptions. Zero values
// pass through — the chunker substitutes its own defaults.
func EmbeddingChunkOptions(cfg *config.Config) embedding.ChunkOptions {
if cfg == nil {
return embedding.ChunkOptions{}
}
return embedding.ChunkOptions{
ThresholdLines: cfg.Embedding.ChunkThresholdLines,
WindowLines: cfg.Embedding.ChunkWindowLines,
}
}
// firstNonEmpty returns the first non-empty string argument.
func firstNonEmpty(s ...string) string {
for _, v := range s {
if v != "" {
return v
}
}
return ""
}