diff --git a/examples/server/embedding.yml b/examples/server/embedding.yml new file mode 100644 index 0000000..9e56b40 --- /dev/null +++ b/examples/server/embedding.yml @@ -0,0 +1,46 @@ +# Beispiel für den embedding-Abschnitt der Server-config.yml +# (tibi-server, server.embedding) — validiert gegen schemas/config/embedding.schema.json. +providers: + - name: bge-m3-external + type: flagembedding + model: BAAI/bge-m3 + dimensions: 1024 + baseURL: https://embed.intern/embed + timeoutPerAttempt: 8s + slowThreshold: 1500ms + - name: bge-m3-local + type: native + model: BAAI/bge-m3 + dimensions: 1024 + modelPath: /models/bge-m3 + timeoutPerAttempt: 60s +groups: + - name: bge-m3-main + description: Bevorzugt extern, sonst lokal, kehrt automatisch zurück. + members: + - bge-m3-external + - bge-m3-local + onSingleQuery: + policy: prefer-in-order + preferredOrder: + - bge-m3-external + - bge-m3-local + timeoutPerAttempt: 8s + timeoutTotal: 25s + autoFailback: true + retryAttempts: 1 + retryBackoff: 200ms + retryBackoffMax: 2s + onBulkWrite: + distribution: lowest-latency + maxConcurrentTotal: 6 + maxConcurrentPerEndpoint: 2 + timeoutPerAttempt: 15s + timeoutTotal: 120s + health: + checkInterval: 30s + checkTimeout: 3s + unhealthyAfterFailures: 3 + healthyAfterSuccesses: 2 + cooldownAfterUnhealthy: 60s +goldenPath: testdata/embedding-golden diff --git a/index.d.ts b/index.d.ts index 2b947b0..2081e61 100644 --- a/index.d.ts +++ b/index.d.ts @@ -109,6 +109,12 @@ declare global { } export interface VectorConfig { + /** + * Name of the embedding provider (server config: `embedding.providers`) or + * of an embedding failover group (`embedding.groups`). Groups route across + * their members (policy, breaker, bulk sharding) and look like one provider + * to the collection. + */ provider: string documentPrefix?: string queryPrefix?: string @@ -735,6 +741,121 @@ declare global { job?: JobConfig } + /** + * Embedding provider as exposed by `config.server()` — mirror of + * `ConfigEmbeddingProvider` (secrets and modelPath are stripped from the JSON). + */ + export interface EmbeddingProviderConfig { + name: string + /** "native" (ONNX in-process) or "flagembedding" (external HTTP endpoint) */ + type: string + model?: string + dimensions?: number + baseURL?: string + documentPrefix?: string + queryPrefix?: string + /** flagembedding: HTTP bound; native: per-call bound (entry-only) */ + timeoutPerAttempt?: string + /** Group-only metric knob (feeds lowest-latency routing) */ + slowThreshold?: string + /** Weights/build tag; part of the model identity used by groups */ + revision?: string + /** false = endpoint/model has no sparse vectors (native skips the sparse session) */ + sparse?: boolean + } + + /** Single-query routing of an embedding group (`onSingleQuery`). */ + export interface EmbeddingOnSingleQueryConfig { + policy?: + | "prefer-in-order" + | "fastest-wins" + | "round-robin" + | "lowest-latency" + | "weighted" + /** Comma-separated names (YAML list is joined with commas before marshalling) */ + preferredOrder?: string + timeoutPerAttempt?: string + timeoutTotal?: string + autoFailback?: boolean + raceDelay?: string + raceMaxFanout?: number + weights?: number[] + latencyWindow?: number + retryAttempts?: number + retryBackoff?: string + retryBackoffMax?: string + hedgeAfterSlow?: string + } + + /** Bulk-write sharding of an embedding group (`onBulkWrite`). */ + export interface EmbeddingOnBulkWriteConfig { + distribution?: "round-robin" | "lowest-latency" | "weighted" + maxConcurrentTotal?: number + maxConcurrentPerEndpoint?: number + timeoutPerAttempt?: string + timeoutTotal?: string + } + + /** Health/breaker timing of an embedding group (`health`). */ + export interface EmbeddingHealthConfig { + /** > 0 also enables late join/recovery of quarantined members */ + checkInterval?: string + checkTimeout?: string + unhealthyAfterFailures?: number + healthyAfterSuccesses?: number + cooldownAfterUnhealthy?: string + } + + /** Embedding failover group as exposed by `config.server()`. */ + export interface EmbeddingGroupConfig { + name: string + description?: string + members: string[] + onSingleQuery?: EmbeddingOnSingleQueryConfig + onBulkWrite?: EmbeddingOnBulkWriteConfig + health?: EmbeddingHealthConfig + } + + /** Embedding section as exposed by `config.server()`. */ + export interface EmbeddingConfig { + providers?: EmbeddingProviderConfig[] + groups?: EmbeddingGroupConfig[] + /** Directory of golden verification files (default testdata/embedding-golden) */ + goldenPath?: string + } + + /** + * Member state from `GET …/admin/embedding/status` (server scope keeps + * `lastError`, project scope redacts it). + */ + export interface EmbeddingMemberHealth { + state: "live" | "degraded" | "quarantined" | "offline" + breaker: "closed" | "open" | "half-open" + lastError?: string + latencyMs?: number + successes?: number + failures?: number + slowCount?: number + } + + /** Group state from `GET …/admin/embedding/status`. */ + export interface EmbeddingGroupHealth { + /** null while no member is live */ + referenceMember: string | null + members: { [member: string]: EmbeddingMemberHealth } + } + + /** Project-scope payload: `{ namespace, health: { groups } }`. */ + export interface EmbeddingHealthProjectResponse { + namespace: string + health: { groups: { [group: string]: EmbeddingGroupHealth } } + } + + /** Server-scope payload: `{ groups }`. */ + export interface EmbeddingHealthServerResponse { + groups: { [group: string]: EmbeddingGroupHealth } + } + interface ConfigPackage { /** * get current namespace string @@ -773,6 +894,8 @@ declare global { maxTokensPerRequest?: number }[] } + /** Raw JSON of config.Server.Embedding (apiKey/modelPath stripped, JSON tags decide) */ + embedding?: EmbeddingConfig audit?: { enabled: boolean defaultTTL: string diff --git a/schemas/config/embedding.schema.json b/schemas/config/embedding.schema.json new file mode 100644 index 0000000..dd212f8 --- /dev/null +++ b/schemas/config/embedding.schema.json @@ -0,0 +1,255 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "embedding.schema.json", + "title": "Tibi server config — embedding section (server-level, not project config)", + "description": "Der Abschnitt `embedding` aus der Server-config.yml (server.embedding): Provider + Failover-Gruppen + Gold-Datei-Pfad. Quelle der Wahrheit: internal/config/server.go und internal/config/embedding_group.go im tibi-server.", + "type": "object", + "additionalProperties": false, + "properties": { + "providers": { + "type": "array", + "items": { + "$ref": "#/definitions/provider" + } + }, + "groups": { + "type": "array", + "items": { + "$ref": "#/definitions/group" + } + }, + "goldenPath": { + "type": "string", + "description": "Verzeichnis der Gold-Dateien für die Mitglieds-Prüfung (Vorgabe: testdata/embedding-golden relativ zum Arbeitsverzeichnis). Erzeugen mit cmd/embed-probe-golden. Ohne verankerte Gold-Datei prüft die Gruppe nur gegen ihre Referenz." + } + }, + "definitions": { + "duration": { + "type": "string", + "description": "Go-Duration, z. B. 120s, 800ms, 25s", + "pattern": "^[0-9]+(\\.[0-9]+)?(ns|us|µs|ms|s|m|h)+$" + }, + "provider": { + "type": "object", + "additionalProperties": false, + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string", + "description": "Provider-Name; Sammlungen referenzieren ihn (oder eine Gruppe) über vector.provider." + }, + "type": { + "type": "string", + "enum": [ + "native", + "flagembedding" + ], + "description": "native = ONNX im Prozess (braucht -tags cgo_embedding), flagembedding = externer HTTP-Endpunkt. Ohne Angabe: native." + }, + "model": { + "type": "string" + }, + "modelPath": { + "type": "string", + "description": "ONNX-Modellverzeichnis (type: native)." + }, + "dimensions": { + "type": "integer", + "minimum": 1 + }, + "baseURL": { + "type": "string", + "description": "Embedding-Endpunkt (type: flagembedding)." + }, + "apiKey": { + "type": "string" + }, + "documentPrefix": { + "type": "string" + }, + "queryPrefix": { + "type": "string" + }, + "timeoutPerAttempt": { + "$ref": "#/definitions/duration", + "description": "flagembedding: HTTP-Timeout (Vorgabe 120s); native: Schranke je Embed-Aufruf (nur am Aufruf-/Stufen-Eintritt wirksam, ONNX-Runs sind nicht unterbrechbar). Ungültige oder nicht-positive Werte werden ignoriert (Warnung im Log)." + }, + "slowThreshold": { + "$ref": "#/definitions/duration", + "description": "Nur gruppenwirksam: langsamere Aufrufe zählen für lowest-latency als langsam." + }, + "revision": { + "type": "string", + "description": "Gewichts-/Build-Tag (Datei-Hash, Quantisierung); fließt in die Modell-Identität ein (Gruppen-Prüfung, Query-Cache)." + }, + "sparse": { + "type": "boolean", + "description": "false = Endpunkt/Modell liefert keine Sparse-Vektoren: native lädt die Sparse-Session nicht, flagembedding schweigt die einmalige Dense-only-Warnung; in einer Gruppe lehnt die Prüfung abweichende Sparse-Formen ab." + } + } + }, + "group": { + "type": "object", + "additionalProperties": false, + "required": [ + "name", + "members" + ], + "properties": { + "name": { + "type": "string", + "description": "Gruppenname; darf kein Provider-Name sein und darf nur einmal vorkommen." + }, + "description": { + "type": "string" + }, + "members": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { + "type": "string" + }, + "description": "Provider-Namen der Gruppe. Alle Mitglieder müssen dieselbe Modell-Identität liefern (Dimensionen, Sparse-Unterstützung, revision)." + }, + "onSingleQuery": { + "type": "object", + "additionalProperties": false, + "properties": { + "policy": { + "type": "string", + "enum": [ + "prefer-in-order", + "fastest-wins", + "round-robin", + "lowest-latency", + "weighted" + ] + }, + "preferredOrder": { + "description": "Bevorzugte Reihenfolge; als Komma-String oder YAML-Liste. Mitglieder, die fehlen, hängen in Config-Reihenfolge hinten an.", + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + }, + "timeoutPerAttempt": { + "$ref": "#/definitions/duration", + "description": "Schranke je Versuch. Der bevorzugte Member läuft genau darin; Ausweich-Member bekommen max(Provider-Timeout, dieser Wert)." + }, + "timeoutTotal": { + "$ref": "#/definitions/duration", + "description": "Gesamt-Deadline je Aufruf; muss >= timeoutPerAttempt sein." + }, + "autoFailback": { + "type": "boolean", + "description": "true (Vorgabe) = jeder Aufruf beginnt wieder beim bevorzugten Mitglied; false = bleibt beim letzten Gewinner." + }, + "raceDelay": { + "$ref": "#/definitions/duration", + "description": "Nur policy fastest-wins: Staffelung zwischen den Teilnehmern." + }, + "raceMaxFanout": { + "type": "integer", + "minimum": 0, + "description": "Nur policy fastest-wins: maximale parallele Teilnehmer (0 = alle)." + }, + "weights": { + "type": "array", + "items": { + "type": "integer", + "minimum": 0 + }, + "description": "Nur policy weighted: relative Gewichte je Mitglied, positional zur members-Liste." + }, + "latencyWindow": { + "type": "integer", + "minimum": 1, + "description": "Fenster für die EWMA-Latenz (lowest-latency)." + }, + "retryAttempts": { + "type": "integer", + "minimum": 0 + }, + "retryBackoff": { + "$ref": "#/definitions/duration" + }, + "retryBackoffMax": { + "$ref": "#/definitions/duration" + }, + "hedgeAfterSlow": { + "$ref": "#/definitions/duration", + "description": "prefer-in-order: nach dieser Dauer starten die übrigen Mitglieder parallel (Hedge)." + } + } + }, + "onBulkWrite": { + "type": "object", + "additionalProperties": false, + "properties": { + "distribution": { + "type": "string", + "enum": [ + "round-robin", + "lowest-latency", + "weighted" + ], + "description": "Verteilung der Batch-Ranges über die Mitglieder; ohne Angabe kein Sharding (serieller Schreibpfad). Jeder Range bleibt ganz auf einem Mitglied." + }, + "maxConcurrentTotal": { + "type": "integer", + "minimum": 0 + }, + "maxConcurrentPerEndpoint": { + "type": "integer", + "minimum": 0, + "description": "Pro Mitglied; lokale (native) Mitglieder sind auf 1 begrenzt." + }, + "timeoutPerAttempt": { + "$ref": "#/definitions/duration", + "description": "Bulk-Schranke je Versuch; pro Mitglied gilt max(Provider-Timeout, dieser Wert)." + }, + "timeoutTotal": { + "$ref": "#/definitions/duration" + } + } + }, + "health": { + "type": "object", + "additionalProperties": false, + "properties": { + "checkInterval": { + "$ref": "#/definitions/duration", + "description": "Abstand der Health-Probes. > 0 aktiviert zusätzlich den Late-Join: quarantänisierte Mitglieder (und offline Gruppen) werden gegen die aktuelle Referenz neu geprüft und kommen zurück, sobald sie passen. 0 = nur Neustart hilft." + }, + "checkTimeout": { + "$ref": "#/definitions/duration", + "description": "Schranke je Probe-Embed; die Rundenschranke beim Start skaliert mit der Zahl der Probe-Texte." + }, + "unhealthyAfterFailures": { + "type": "integer", + "minimum": 0 + }, + "healthyAfterSuccesses": { + "type": "integer", + "minimum": 0 + }, + "cooldownAfterUnhealthy": { + "$ref": "#/definitions/duration", + "description": "Pause, bevor ein offenes Mitglied wieder halb geöffnet wird (danach genügt Verkehr, kein Probe-Lauf)." + } + } + } + } + } + } +} diff --git a/scripts/validate-schemas.mjs b/scripts/validate-schemas.mjs index c43d485..3019c90 100644 --- a/scripts/validate-schemas.mjs +++ b/scripts/validate-schemas.mjs @@ -64,6 +64,9 @@ const validations = [ { schema: "job.schema.json", pattern: "demo-api/jobs/*.y*ml" }, { schema: "action.schema.json", pattern: "demo-api/actions/*.y*ml" }, { schema: "asset.schema.json", pattern: "demo-api/assets/*.y*ml" }, + // Server-level Abschnitt (config.yml des tibi-servers), Beispiel getrennt + // vom Projekt-Demo unter examples/server/: + { schema: "embedding.schema.json", pattern: "examples/server/*.y*ml" }, ] let hasErrors = false