Kontakt

Features

Governance & Modell-Routing

Eine Routing-Entscheidung mit Begründung für jeden Kandidaten, ein Kontextbudget nach dem Modell, das tatsächlich antwortet, Datenklassen gegen Vertrauenszonen der Provider und Kosten pro Aufruf, zugeordnet zur aufrufenden Extension.

Warum es existiert

Eine Konfiguration im Kriterienmodus nennt kein Modell. Das Modell wird erst beim Aufruf anhand der gespeicherten Kriterien gewählt. Früher lieferte die Auswahl einen Treffer oder nichts – wer wissen wollte, warum ein neu angelegtes Modell nie zum Zug kam, fand nichts zum Nachlesen. Heute liefert die Entscheidungsstelle eine Routing-Entscheidung: das gewählte Modell, jeden zulässigen Kandidaten mit seinem Score und jeden abgelehnten Kandidaten mit dem Grund (ADR-142). Eine Zusammenfassung jeder echten Entscheidung landet in der Telemetriezeile (ADR-156).

Eine Konfiguration im Kriterienmodus hat keine Modellrelation. Code, der das Kontextfenster aus der Konfiguration las, fiel deshalb auf einen Standardwert zurück und bemaß das Budget an einem Modell, das gar nicht angefragt wurde. Heute kommt das Sendebudget von dem Modell, das den Aufruf tatsächlich bedient (ADR-143). Die Vertrauenszone eines Aufrufs im Kriterienmodus richtet sich nach demselben aufgelösten Modell (ADR-149).

Datenschutz braucht eine Deklaration, nicht nur einen Screener. Tools deklarieren, welche Art von Daten sie zurückgeben. Snippets, Skills und der System-Prompt können deklarieren, was sie enthalten. Jeder Provider liegt in einer Vertrauenszone mit einer Obergrenze. Ein Schalter entscheidet, ob etwas oberhalb dieser Grenze entfernt und abgelehnt oder nur protokolliert wird (ADR-094, ADR-144, ADR-155).

Administratoren müssen die geltende Richtlinie und die Kosten pro Aufrufer sehen, ohne dass eine zweite Richtlinien-Engine entsteht. Der Tab „Governance“ liest jeden Wert über dieselben Resolver, die auch die Laufzeit verwendet, vergleicht ihn mit einer benannten Haltung und ändert nichts (ADR-145). Ein Aufruf kann die Extension nennen, von der er stammt, und die Kosten werden pro Extension zusammengefasst (ADR-177, ADR-178).

Wann man es einsetzt

  • Konfigurationen im Kriterienmodus laufen, und es soll nachvollziehbar sein, warum ein Aufruf auf einem bestimmten Modell lief: „Warum dieses Modell?“ erklärt einen hypothetischen Aufruf, „Aufrufe, die geroutet wurden“ listet die Entscheidungen der letzten sieben Tage.
  • Vertrauliche Inhalte dürfen keinen externen Provider erreichen – trust_zone am Provider, data_class an Snippets und Skills und system_prompt_data_class an der Konfiguration setzen.
  • Ein Tool gibt sensible Daten zurück – ToolDataClassInterface implementieren, damit das Tool-Gate die Klasse mit der Obergrenze der Zone vergleichen kann, die ein Lauf erreicht.
  • Eine Installation soll mit einer angestrebten Haltung verglichen werden (local-only, controlled-cloud, enterprise-strict, development) – im Tab „Governance“ ein Profil wählen und ablesen, welche Werte abweichen und wo sie gesetzt werden.
  • Es ist zu klären, ob ein Backend-Benutzer ein Tool über eine Konfiguration ausführen könnte – vor jeder Rechtevergabe die Simulation im Tab „Governance“ starten.
  • Eine Extension ruft nr-llm auf, und ihre Kosten sollen sichtbar sein – die Optionen mit withCallerSource() kennzeichnen; das Analytics-Modul gruppiert die Kosten nach Extension.

Wie es funktioniert

ModelSelectionServiceInterface::resolveModelForCall() gibt eine ModelResolution zurück: das Modell und eine RoutingSummary. Dahinter führt RoutingDecisionService zuerst die Discovery aus, dann EligibilityEvaluator, dann CandidateRanker. Zulässigkeit ist eine harte Bedingung: Ein abgelehnter Kandidat erhält einen RoutingRejectionReason (fehlende Fähigkeit, Adapter nicht erlaubt, Kontext zu klein, Kosten über dem Limit, fehlende Fähigkeit für die Operation) und keinen Score. Die Provider-Priorität sortiert vor dem Score, denn eine Priorität ist eine Anweisung. routing.policyMode wählt providerPriority (Standard, keine gemessenen Signale), balanced, quality oder economy; ein Modell ohne Messwert trägt nichts bei statt einer Null. ConfigurationCallPlanner legt die Zusammenfassung auf TelemetrySignals ab. Die Telemetriezeile speichert den Modus, die Zahl der Kandidaten, die unterschiedlichen Ablehnungsgründe und welche Signale die Entscheidung tatsächlich bewegt haben. Der feste Modus speichert einen leeren Modus, weil nichts gewählt wurde.

ContextWindowManager::fit() nimmt das aufgelöste Modell entgegen und zieht es der Relation der Konfiguration vor; ist die Kontextlänge unbekannt, gilt ein Fenster von 8192 Tokens. LlmServiceManager begrenzt Chat-, Tool-Calling- und Streaming-Aufrufe am Ende der Middleware-Pipeline – dort existiert das aufgelöste Modell zum ersten Mal –, und Tool-Schemas zählen gegen dasselbe Budget. Ein Completion-Prompt wird gemessen und ein Überlauf protokolliert, aber nie gekürzt. Ein Transkript, das selbst nach maximaler Kürzung nicht passt, geht ungekürzt mit einer Warnung hinaus.

ToolDataClass ist eine Skala von publicContent bis secretAdjacent. TrustZone (local, privateHosted, externalEu, externalGlobal) ordnet jeder Zone eine höchste Klasse zu. TrustZoneResolver::zoneFor() liefert die am wenigsten vertrauenswürdige Zone, die ein Lauf erreichen kann: den Provider der Konfiguration – im Kriterienmodus den Provider des bedienenden Modells – und eine Stufe der Fallback-Kette; alles Unbekannte gilt als externalGlobal. Das Tool-Gate entfernt Tools oberhalb der Obergrenze. InputContextTrustGate lehnt einen Aufruf ab, dessen strengste deklarierte Quelle über der Zone liegt – Snippets, Skills, der System-Prompt und die Quellen, die ein Lauf zusätzlich erzwingt (ADR-164). Eine nicht deklarierte Quelle schränkt nichts ein. tools.dataClassEnforcement gilt für beide Richtungen: Nur ein wörtliches observe schaltet auf Beobachten. Ablehnungen landen in tx_nrllm_governance_event.

Im Tab „Governance“ zeigt EffectivePolicyReadout privacy.level, privacy.retentionDays, tools.dataClassEnforcement und skills.minTrustLevel so, wie die Laufzeit sie auflöst; eine Zeile, die sich nicht beantworten lässt, zeigt unknown. GovernanceProfile legt die erwarteten Werte je Haltung fest, und GovernanceProfileEvaluator::deviations() vergleicht sie mit den Zeilen der Anzeige und nennt, wo der jeweilige Wert gesetzt wird. Einen Weg zum Übernehmen gibt es nicht (ADR-140). GovernanceSimulator fragt Konfigurationszugriff, Tool-Gate, Input-Context-Gate, Routing und menschliche Freigabe ab und liefert allow, allowWithApproval oder block. Jede Telemetriezeile enthält außerdem actual_input_tokens, actual_output_tokens, actual_cost, response_model, provider_retries und modellunabhängige facts_*-Spalten; UsageMiddleware reicht source_extension an UsageTrackerServiceInterface::trackUsage() weiter, das die Kosten pro Tag und Extension in tx_nrllm_service_usage zusammenfasst.

Netresearch\NrLlm\Service\Routing\RoutingDecisionService

Die Entscheidungsstelle für das Routing: Discovery, Zulässigkeit, Ranking. Liefert eine RoutingDecision mit allen Kandidaten; genutzt von der Laufzeit und vom Tab „Governance“.

Netresearch\NrLlm\Service\Routing\EligibilityEvaluator

Die einzige Implementierung der Frage „darf dieses Modell diesen Aufruf bedienen“. Gibt einen RoutingRejectionReason statt false zurück; modelMatchesCriteria() delegiert hierher.

Netresearch\NrLlm\Domain\ValueObject\RoutingSummary

Sechs Werte, die pro geroutetem Aufruf gespeichert werden: Modus, Zahl der Kandidaten, unterschiedliche Ablehnungsgründe und ob Qualität, Health und Kosten die Entscheidung bewegt haben.

Netresearch\NrLlm\Service\Context\ContextWindowManager

Bemisst das Sendebudget am bedienenden Modell, entfernt die ältesten Runden vollständig und meldet einen Überlauf, statt den Aufruf zu verweigern.

Netresearch\NrLlm\Service\Governance\TrustZoneResolver

Die am wenigsten vertrauenswürdige Zone, die ein Lauf über den Provider und eine Fallback-Stufe erreicht; ceilingFor() macht daraus die Datenklassen-Obergrenze für Tools. Unbekanntes fällt sicher auf externalGlobal zurück.

Netresearch\NrLlm\Service\Context\InputContextTrustGate

Lehnt einen Aufruf ab (enforce) oder protokolliert ihn (observe), wenn die strengste deklarierte Klasse von Snippet, Skill, System-Prompt oder erzwungener Quelle über der erreichbaren Zone liegt.

Netresearch\NrLlm\Service\Governance\GovernanceProfileEvaluator

Vergleicht die Zeilen der Anzeige mit einem GovernanceProfile und gibt jede Abweichung mit dem Ort zurück, an dem sie geändert wird. Liest selbst nichts und schreibt nichts.

Netresearch\NrLlm\Service\Tool\GovernanceSimulator

Der Bereich „Wäre das erlaubt?“: ruft die Gates der Laufzeit für eine Konfiguration, ein Tool und optional einen Backend-Benutzer auf und protokolliert nichts.

Für Entwickler

Die eigene Extension an ihren Aufrufen nennen, die Stelle kennen, an der das Routing entschieden wird, deklarieren, was eigene Tools zurückgeben, und die Kosten pro Anfrageform aus der Telemetrietabelle lesen.

Die aufrufende Extension nennen

withCallerSource() versieht die Optionen mit dem Extension-Key der aufrufenden Extension und optional einer Operation. Beide Werte reisen als Aufruf-Metadaten mit und erreichen den Provider nie. Sie werden als source_extension und source_operation in der Telemetriezeile gespeichert, und source_extension wird Teil des täglichen Kosten-Aggregats. Nicht gekennzeichnete Aufrufe erscheinen als „Nicht zugeordnet“. Der Name wird nicht geprüft.

php
use Netresearch\NrLlm\Service\LlmServiceManagerInterface;
use Netresearch\NrLlm\Service\Option\ChatOptions;

final class TeaserGenerator
{
    public function __construct(
        private readonly LlmServiceManagerInterface $llm,
    ) {}

    public function teaser(string $text): string
    {
        $options = (new ChatOptions())->withCallerSource('my_extension', 'teaser');

        return $this->llm->complete('Write a teaser: ' . $text, $options)->content;
    }
}

Eine Auswertung für Modell und Begründung

ConfigurationCallPlanner löst das Modell über resolveModelForCall() auf und legt die RoutingSummary auf den TelemetrySignals des Aufrufs ab. Dieselbe Auswertung beantwortet, welches Modell und warum – die gespeicherte Begründung kann also keine Entscheidung beschreiben, die nie lief. Bei einer Konfiguration im festen Modus ist die Zusammenfassung null.

php
// One evaluation answers both questions — which model, and why. See
// ModelSelectionServiceInterface::resolveModelForCall() for why this
// is not resolveModel() plus explainRouting().
$resolution = $this->modelSelectionService->resolveModelForCall($configuration, $operation);
$llmModel   = $resolution->model;
$signals?->recordRoutingSummary($resolution->routingSummary);

Deklarieren, was ein Tool zurückgibt

Ein Tool, dessen Gruppen-Standard seine Ausgabe zu niedrig einstuft, implementiert ToolDataClassInterface. Der Wert ist Teil des Codes und lässt sich im Backend nicht umetikettieren. Das Tool-Gate vergleicht ihn mit der Obergrenze der am wenigsten vertrauenswürdigen Zone, die der Lauf erreicht, und entfernt das Tool, wenn er darüber liegt (enforce), oder protokolliert es (observe).

php
final readonly class GetEnvRawTool implements ToolInterface, ToolDataClassInterface
{
    // getSpec(), execute(), isEnabledByDefault(), requiresAdmin(), getGroup() ...

    /**
     * Environment variables routinely carry database passwords, API keys and vault credentials.
     */
    public function getDataClass(): ToolDataClass
    {
        return ToolDataClass::SECRET_ADJACENT;
    }
}

Kosten pro Anfrageform lesen

Die Kosten pro Aufruf und die Fakten vor dem Routing stehen in tx_nrllm_telemetry und werden direkt abgefragt; der Tab „Governance“ zeigt sie nicht an. NULL bedeutet „kein Messwert vorhanden“: AVG() überspringt unbekannte Kosten. Mit actual_cost IS NOT NULL bleiben nur Aufrufe mit Kostenangabe übrig.

sql
SELECT facts_shape,
       COUNT(*)                AS calls,
       AVG(actual_cost)        AS mean_cost,
       SUM(actual_cost)        AS total_cost,
       AVG(actual_input_tokens)  AS mean_in,
       AVG(actual_output_tokens) AS mean_out
  FROM tx_nrllm_telemetry
 WHERE crdate >= UNIX_TIMESTAMP() - 30*86400
   AND facts_shape <> ''
 GROUP BY facts_shape;

Fallstricke

Ein externer Fallback senkt die Zone

Die erreichbare Zone schließt eine Stufe der Fallback-Kette ein. Ein lokaler Primär-Provider mit externem Fallback wird nach der externen Obergrenze beurteilt – Tools und Snippets, die ein lokaler Lauf nutzen dürfte, werden entfernt oder abgelehnt. Abhilfe: den Fallback entfernen, ihn auf einen vertrauenswürdigeren Provider legen oder die niedrigere Grenze hinnehmen.

Der Kriterienmodus liest das bedienende Modell nur auf der Sendeseite

Das Input-Context-Gate nimmt die Zone vom aufgelösten Modell. Das Tool-Gate nicht: ceilingFor() fragt zoneFor() ohne Modell, deshalb bekommt ein Lauf im Kriterienmodus nur die Tools angeboten, die externalGlobal erlaubt. Auch eine Fallback-Stufe im Kriterienmodus zählt als externalGlobal. Die Simulation fragt das Input-Context-Gate ebenfalls ohne Modell.

observe gilt für die ganze Instanz, und ein Tippfehler erzwingt

Nur ein wörtliches observe (Groß- und Kleinschreibung sowie umgebende Leerzeichen spielen keine Rolle) schaltet tools.dataClassEnforcement ab; jeder andere Wert erzwingt. Im Modus observe betrifft das jede Konfiguration, jeden Lauf und jeden Benutzer. Tools eines MCP-Servers werden oberhalb der Obergrenze auch im Modus observe entfernt.

„Aufrufe, die geroutet wurden“ bleibt bei festem Modus leer

Die Abfrage filtert per SQL auf einen nicht leeren routing_policy_mode. Aufrufe im festen Modus haben nichts gewählt, schreiben einen leeren Modus und erscheinen daher nie. Ist die Tabelle auf einer stark genutzten Installation leer, nennt sehr wahrscheinlich jede Konfiguration ihr Modell, oder telemetry.enabled ist ausgeschaltet.

„Erlaubt“ in der Simulation ist kein Versprechen

Die Simulation fragt weder die Budgetprüfung noch die Guardrail-Pipeline ab, denn beide entscheiden anhand des konkreten Aufrufs. Sie schreibt auch kein Governance-Ereignis – wer wann was geprüft hat, lässt sich dem Audit daher nicht entnehmen.