Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,10 +128,13 @@ their repo-root counterparts. Install-specific entrypoints live in `.codex/`,

| Variable | Default | Description |
| ------------------------ | ------------------------ | ------------------------------------------ |
| `LUMEN_BACKEND` | `ollama` | Embedding backend (`ollama` or `lmstudio`) |
| `LUMEN_BACKEND` | `ollama` | Embedding backend (`ollama`, `lmstudio`, or `openai`) |
| `LUMEN_EMBED_MODEL` | see note ¹ | Embedding model (must be in registry) |
| `OLLAMA_HOST` | `http://localhost:11434` | Ollama server URL |
| `LM_STUDIO_HOST` | `http://localhost:1234` | LM Studio server URL |
| `OPENAI_BASE_URL` | — | OpenAI-compatible server URL (`openai` backend) |
| `OPENAI_API_KEY` | — | Bearer token for the `openai` backend |
| `LUMEN_EMBED_SKIP_HEALTH_CHECK` | `false` | Skip `/v1/models` probe (`openai` backend) |
| `LUMEN_MAX_CHUNK_TOKENS` | `512` | Max tokens per chunk before splitting |
| `LUMEN_VECTOR_STORAGE` | `int8` | Vector precision (`int8` or `float32`) |

Expand Down
40 changes: 38 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -273,9 +273,12 @@ All configuration is via environment variables:
| Variable | Default | Description |
| ------------------------ | ------------------------ | ------------------------------------------------------------- |
| `LUMEN_EMBED_MODEL` | see note ¹ | Embedding model; use with `LUMEN_EMBED_DIMS` for unlisted models |
| `LUMEN_BACKEND` | `ollama` | Embedding backend (`ollama` or `lmstudio`) |
| `LUMEN_BACKEND` | `ollama` | Embedding backend (`ollama`, `lmstudio`, or `openai`) |
| `OLLAMA_HOST` | `http://localhost:11434` | Ollama server URL |
| `LM_STUDIO_HOST` | `http://localhost:1234` | LM Studio server URL |
| `OPENAI_BASE_URL` | — | OpenAI-compatible server URL (required for `openai` backend) |
| `OPENAI_API_KEY` | — | Bearer token for the `openai` backend; omit for gateways that trust network position |
| `LUMEN_EMBED_SKIP_HEALTH_CHECK` | `false` | Skip the `/v1/models` probe (`openai` backend); needed when a gateway doesn't expose that endpoint |
| `LUMEN_MAX_CHUNK_TOKENS` | `512` | Max tokens per chunk before splitting |
| `LUMEN_VECTOR_STORAGE` | `int8` | Vector precision (`int8` or `float32`) |
| `LUMEN_EMBED_DIMS` | — | Override embedding dimensions (required for unlisted models) |
Expand Down Expand Up @@ -304,7 +307,9 @@ of the database path hash, so different models never collide.
> **Caveat**: the DB path hash includes the model name but not the backend. If
> the same model name is configured on two backends (e.g. an Ollama and an LM
> Studio entry both named `foo`), they share the same index — use distinct
> model names per backend to avoid collisions.
> model names per backend to avoid collisions. This applies to the `openai`
> backend too — a model name shared with an Ollama or LM Studio entry collides
> in the index cache.

### Selecting a server per invocation

Expand Down Expand Up @@ -348,6 +353,37 @@ LUMEN_EMBED_DIMS=4096
LUMEN_EMBED_CTX=40960 # optional, defaults to 8192
```

### Remote / internal OpenAI-compatible servers

The `openai` backend targets any service exposing an OpenAI-compatible
`/v1/embeddings` endpoint — OpenAI itself, or an internal gateway. Configure
it via `config.yaml`:

```yaml
servers:
- backend: openai
host: https://api.example.com
model: text-embedding-3-small
dims: 1536
api_key: sk-... # optional — omit for gateways that trust network position
skip_health_check: true # optional — set when the gateway doesn't expose /v1/models
```

Or via environment variables:

```sh
LUMEN_BACKEND=openai
OPENAI_BASE_URL=https://api.example.com
OPENAI_API_KEY=sk-...
LUMEN_EMBED_MODEL=text-embedding-3-small
LUMEN_EMBED_DIMS=1536
```

`skip_health_check` exists because many custom gateways proxy a different
provider underneath (Bedrock, Gemini, etc.) and don't implement `/v1/models`
in the OpenAI shape, or don't implement it at all. Without it, Lumen's health
probe would incorrectly mark a working server as unhealthy.

## Controlling what gets indexed

Lumen filters files through six layers: built-in directory and lock file skips →
Expand Down
8 changes: 4 additions & 4 deletions cmd/index.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import (

func init() {
indexCmd.Flags().StringP("model", "m", "", "embedding model (default: $LUMEN_EMBED_MODEL or "+embedder.DefaultModel+")")
indexCmd.Flags().StringP("backend", "b", "", "embedding backend to select (\"ollama\" or \"lmstudio\"); disambiguates when --model is configured on multiple backends")
indexCmd.Flags().StringP("backend", "b", "", "embedding backend to select (\"ollama\", \"lmstudio\", or \"openai\"); disambiguates when --model is configured on multiple backends")
indexCmd.Flags().BoolP("force", "f", false, "force full re-index")
rootCmd.AddCommand(indexCmd)
}
Expand Down Expand Up @@ -195,9 +195,9 @@ func loadConfigWithFlags(cmd *cobra.Command) (*config.ConfigService, error) {
if model == "" && backend == "" {
return config.NewConfigService(path)
}
if backend != "" && backend != config.BackendOllama && backend != config.BackendLMStudio {
return nil, fmt.Errorf("unknown backend %q (must be %q or %q)",
backend, config.BackendOllama, config.BackendLMStudio)
if backend != "" && backend != config.BackendOllama && backend != config.BackendLMStudio && backend != config.BackendOpenAI {
return nil, fmt.Errorf("unknown backend %q (must be %q, %q, or %q)",
backend, config.BackendOllama, config.BackendLMStudio, config.BackendOpenAI)
}

cfg, selErr := config.NewConfigService(path, config.WithServerSelection(model, backend))
Expand Down
2 changes: 1 addition & 1 deletion cmd/search.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ func init() {
searchCmd.Flags().BoolP("force", "f", false, "force full re-index before searching")
searchCmd.Flags().Bool("trace", false, "print per-phase timing to stderr")
searchCmd.Flags().StringP("model", "m", "", "embedding model override")
searchCmd.Flags().StringP("backend", "b", "", "embedding backend to select (\"ollama\" or \"lmstudio\")")
searchCmd.Flags().StringP("backend", "b", "", "embedding backend to select (\"ollama\", \"lmstudio\", or \"openai\")")
rootCmd.AddCommand(searchCmd)
}

Expand Down
4 changes: 4 additions & 0 deletions cmd/stdio.go
Original file line number Diff line number Diff line change
Expand Up @@ -1132,6 +1132,10 @@ func (ic *indexerCache) handleHealthCheck(ctx context.Context, _ *mcp.CallToolRe
host := srv.Host
model := srv.Model

if srv.SkipHealthCheck {
return healthResult(backend, host, model, true, "skip_health_check is set; service was not probed"), nil, nil
}

probeCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()

Expand Down
47 changes: 47 additions & 0 deletions cmd/stdio_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -487,6 +487,53 @@ servers:
}
}

func TestHandleHealthCheck_RespectsSkipHealthCheck(t *testing.T) {
for _, k := range []string{"LUMEN_BACKEND", "LUMEN_EMBED_MODEL", "LUMEN_EMBED_DIMS", "LUMEN_EMBED_CTX", "OLLAMA_HOST", "LM_STUDIO_HOST", "OPENAI_API_KEY", "OPENAI_BASE_URL", "LUMEN_EMBED_SKIP_HEALTH_CHECK"} {
t.Setenv(k, "")
}

dir := t.TempDir()
cfgFile := filepath.Join(dir, "config.yaml")

// /v1/models always 503s, simulating a gateway that doesn't implement it,
// even though /v1/embeddings itself works fine.
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/v1/models" {
w.WriteHeader(http.StatusServiceUnavailable)
return
}
w.WriteHeader(http.StatusNotFound)
}))
defer srv.Close()

if err := os.WriteFile(cfgFile, []byte(fmt.Sprintf(`
servers:
- backend: openai
host: %s
model: remote-embed
dims: 3
skip_health_check: true
`, srv.URL)), 0o644); err != nil {
t.Fatalf("WriteFile: %v", err)
}

svc, err := config.NewConfigService(cfgFile)
if err != nil {
t.Fatalf("NewConfigService: %v", err)
}
fe := embedder.NewFailoverEmbedder(svc)

ic := &indexerCache{embedder: fe, cfg: svc}
result, _, err := ic.handleHealthCheck(context.Background(), &mcp.CallToolRequest{}, HealthCheckInput{})
if err != nil {
t.Fatalf("handleHealthCheck: %v", err)
}
text := mustTextResult(t, result)
if !strings.Contains(text, "Status: OK") {
t.Fatalf("expected skip_health_check to report OK without probing /v1/models, got: %s", text)
}
}

func TestHandleHealthCheck_ModelMissingIsError(t *testing.T) {
for _, k := range []string{"LUMEN_BACKEND", "LUMEN_EMBED_MODEL", "LUMEN_EMBED_DIMS", "LUMEN_EMBED_CTX", "OLLAMA_HOST", "LM_STUDIO_HOST"} {
t.Setenv(k, "")
Expand Down
4 changes: 4 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ const (
BackendOllama = "ollama"
// BackendLMStudio is the backend identifier for LM Studio.
BackendLMStudio = "lmstudio"
// BackendOpenAI is the backend identifier for OpenAI-compatible remote
// embedding servers (OpenAI itself, or any internal gateway exposing the
// same /v1/embeddings wire format).
BackendOpenAI = "openai"
)

// DBPathForProject returns the default int8/512-token collection path. New
Expand Down
Loading
Loading