SIGNALS Documentation
API Reference

AI embeddings & configuration

Native vector embeddings for semantic search and similar-record panels — provider setup, scheduling, cost controls, privacy, and graceful degradation.

Overview

Signals can generate vector embeddings for core records so global search can blend semantic matches with full-text ranking, and so detail pages can surface similar rentals, accounts, and catalogue_items.

Embeddings are optional. If no API key is configured, pgvector is unavailable, or sync is paused, every consumer behaves exactly as it did before embeddings existed.

What gets embedded

Core: Rentals, Accounts, Catalogue items

Tier 1: Activities, Conversations (+ comments), Invoices, Credit notes, Purchase orders, Equipment test results, Repairs, Attachments

Each model implements the Embeddable contract via toEmbeddableText(). Content is hashed so status/total-only updates do not cost tokens.

Configuration

Supported labs

Lab (SIGNALS_AI_PROVIDER) Models Default model Dimensions
openai text-embedding-3-small, text-embedding-3-large text-embedding-3-small 1536 / 3072
gemini gemini-embedding-001 gemini-embedding-001 3072
mistral mistral-embed mistral-embed 1024
openai_compatible any (you supply the id) — any (you supply the width)

Anthropic and Moonshot are not offered — those labs expose no embeddings API. If stored settings still reference a removed lab, Signals falls back to the default provider/model and logs a warning.

Bring your own endpoint (openai_compatible)

The three hosted labs above all mean your record text crosses the network to a company you do not run. openai_compatible is the way out of that: point Signals at any server that speaks the OpenAI embeddings contract — POST {base_url}/embeddings taking {model, input, dimensions} and returning {data: [{embedding: [...]}], usage: {prompt_tokens}} — and it becomes the provider.

Run that server yourself and no record text leaves your infrastructure at all. This is the same posture the MCP server documents from the other direction: core Signals ships no model and phones nothing home, and embeddings are the one place it calls out — so this setting is what decides whether "out" means the public internet or a box on your own network.

Known-good hosts:

Host Base URL Example model
Ollama http://127.0.0.1:11434/v1 nomic-embed-text, mxbai-embed-large
vLLM http://127.0.0.1:8000/v1 intfloat/e5-mistral-7b-instruct
LM Studio http://127.0.0.1:1234/v1 text-embedding-nomic-embed-text-v1.5
LocalAI http://127.0.0.1:8080/v1 bert-embeddings
HF text-embeddings-inference http://127.0.0.1:8080/v1 whatever the container serves
Any OpenAI-compatible gateway (LiteLLM, OpenRouter, Azure-fronted proxies) your gateway URL your gateway's model id

Three things you must get right, because there is no catalogue to fall back on:

  • Base URL — the root the /embeddings path is appended to. Include the version segment (/v1) if your server uses one. Required whenever this lab is selected.
  • Model — the id exactly as your endpoint names it. Never coerced to a default.
  • Dimensions — the vector width your model actually returns. A mismatch does not error at write time; it silently poisons similarity ranking and the HNSW expression index, so check it against your model card.

The API key is optional for this lab. Most local servers ignore auth, so an empty key is treated as configured as long as the base URL is set (Signals sends a placeholder bearer token so the header stays well-formed). Set a key when your gateway needs one.

Changing the base URL or dimensions queues a full re-embed, exactly as changing lab or model does — all three define the vector space.

Signals talks to your endpoint over plain HTTP(S) with no certificate pinning. Keep local endpoints bound to loopback or a private network; if the endpoint is remote, terminate TLS.

Environment variables

Variable Purpose
SIGNALS_AI_API_KEY Provider API key (Cloud: required; self-hosted: optional override path is settings). Optional for openai_compatible
SIGNALS_AI_PROVIDER Lab key: openai, gemini, mistral, or openai_compatible
SIGNALS_AI_EMBEDDING_MODEL Model id for that lab
SIGNALS_AI_EMBEDDING_DIMENSIONS Optional dimension override (otherwise catalogue default); effectively required for openai_compatible
SIGNALS_AI_BASE_URL Endpoint root for openai_compatible only — ignored by the hosted labs, whose URLs are fixed

Resolution order for provider, model, dimensions, and base URL: env (when set) → admin settings store (self-hosted only) → config / catalogue default.

A fully self-contained local setup:

SIGNALS_AI_PROVIDER=openai_compatible
SIGNALS_AI_BASE_URL=http://127.0.0.1:11434/v1
SIGNALS_AI_EMBEDDING_MODEL=nomic-embed-text
SIGNALS_AI_EMBEDDING_DIMENSIONS=768

Self-hosted

Admin → Integrations → AI embeddings

  • Encrypted API key (optional for openai_compatible)
  • Lab / provider and embedding model (table above)
  • Endpoint base URL and vector dimensions (shown only for openai_compatible)
  • Full refresh cadence (weekly or monthly)
  • Optional monthly token budget
  • Usage log retention

Changing lab or model queues a full re-embed. Similarity queries always filter to the active provider/model.

SIGNALS_AI_PROVIDER, SIGNALS_AI_EMBEDDING_MODEL, and SIGNALS_AI_EMBEDDING_DIMENSIONS override the admin store when set (useful for ops / staging without changing DB settings).

Signals Cloud

Provider credentials and model selection are env-only (SIGNALS_AI_*). The AI settings form and usage totals are hidden in the admin UI.

TODO (cloud privacy): per-tenant opt-out flag and DPA line before cloud rollout — see Solo todo #960 privacy section.

Scheduling

Job When Purpose
Daily sync 01:30 UTC Embed new/changed records since last run
Full refresh Weekly or monthly (setting) Re-validate hashes and backfill gaps

Jobs run on the embeddings queue, batched via Bus::batch(), with rate-limit middleware for provider API calls.

Cost logging & budgets (self-hosted)

Every AI call through the shared client is logged to ai_usage_logs (function, provider, model, tokens). The AI tab shows totals for the last 7 days, last 30 days, this month, last month, and year-to-date.

An optional monthly token budget pauses sync and notifies admins when exceeded. Logs are pruned like action logs (ai-usage:prune).

PostgreSQL / pgvector

Production similarity ranking requires PostgreSQL with the pgvector extension. The migration enables the extension when available and creates a dimensionless vector column so provider/model swaps do not need a schema change. SQLite (and pgsql without the extension) keep a text column so the schema contract is identical. Without pgvector, similarity features stay off.

The HNSW cosine index is not created by the migration (HNSW requires a fixed dimension). When an AI model with known dimensions is activated — or a full re-embed runs — Signals creates an expression index (embedding::vector(N)) and similarity queries use the same cast so the index is used.

Privacy

Embeddable text is sent to the selected lab. Self-hosted admins see an informed-choice notice on the AI tab before enabling the feature, and that notice changes wording for openai_compatible because the destination is one you chose rather than one Signals picked. With a self-run endpoint the text never leaves your network; with a hosted lab or a third-party gateway, that operator receives it.

Graceful degradation

Condition Behaviour
No API key / disabled Sync no-ops (one log line); search is full-text only; similar panels hidden
No pgvector Same as above for similarity
Budget paused Sync skipped until cleared
Record not yet embedded Full-text / existing UI unchanged for that record

API

Semantic search mirrors global search:

MethodURLSummaryAbilityWeight
GET/api/v1/searchSearch across every registered searchable entity typesearch:read2
GET/api/v1/search/semanticSemantic (vector) search across embeddable entity typessearch:read4

Entity index endpoints accept q[similar_to_eq]=… (text, numeric id, or Class:id). Response DTOs for rentals, accounts, and catalogue_items include embedded_at and embedding_model (never the raw vector by default).

Adding a new embeddable model

  1. Implement App\Contracts\Embeddable and use App\Models\Traits\Embeddable
  2. Override toEmbeddableText()
  3. Register the class in App\Services\Embeddings\EmbeddableRegistry