Skip to content

Routing & Default

Ongrid betreibt N Provider parallel und dispatcht jeden LLM-Aufruf an genau einen. Diese Seite deckt ab, wie dieser Dispatch in drei Schichten funktioniert:

  1. MultiClient — der Wire-Level-Router, der vom Legacy-llm.Chat-Pfad verwendet wird (Translate, Wissens-Suche).
  2. RoutingChatModel — der eino model.ChatModel-Wrapper, der vom Graph-Kernel-ReAct-Agenten verwendet wird.
  3. DefaultResolver — der Dynamic-Default-Hook, der die default_provider-Einstellung mitten im Prozess in Kraft treten lässt.

MultiClient

Der Boden des Router-Stacks lebt in internal/pkg/llm/router.go.

go
// router.go:67
type MultiClient struct {
    // map: provider id -> sub-Client built from a ProviderConfig
    // ...
}

func (m *MultiClient) Chat(ctx context.Context, req ChatReq) (*ChatResp, error)

Jede Provider-Konfiguration produziert einen Sub-Client beim Construct:

go
// cmd/ongrid/main.go:492
providerCfgs := []llm.ProviderConfig{}
if cfg.OpenAI.APIKey != "" {
    providerCfgs = append(providerCfgs, llm.ProviderConfig{
        ID: "openai", Label: "OpenAI",
        APIKey: cfg.OpenAI.APIKey,
        Model: firstNonEmpty(cfg.OpenAI.Model, "gpt-5.4"),
        BaseURL: cfg.OpenAI.BaseURL,
        Models: dedupeModels(...),
    })
}
// ...same for each provider...
llmRouter := llm.NewMultiClient(providerCfgs, cfg.LLM.Default, openaiClient)

Chat dispatcht auf req.Provider:

  • Nicht-leer → Sub-Client nachschlagen; 404 → ErrUnknownProvider.
  • Leer → auf defaultProvider zurückfallen.

Die MultiClient.SetProvidersResolver(r)-Verdrahtung legt den DB-gestützten Resolver oben drauf — bei jedem Aufruf wird das aktive Sub-Client-Set neu aufgelöst (60s gecacht durch SetResolveTTL).

RoutingChatModel

Der Graph-Kernel verwendet eino's model.ChatModel-Interface, nicht llm.Chat. RoutingChatModel (eino_routing.go:89) wickelt N innere ChatModels und dispatcht via eine impl-spezifische Option:

go
type RoutingChatModel struct {
    inner           map[string]model.ChatModel
    defaultProvider string
    defaultResolver func(context.Context) (provider, mdl string)
}

func WithProvider(provider string) model.Option {
    return model.WrapImplSpecificOptFn(func(o *providerOpts) {
        o.provider = provider
    })
}

Verwendung von einer Aufrufstelle:

go
resp, err := chatModel.Generate(ctx, msgs,
    model.WithModel("glm-4.7-flash"),
    llm.WithProvider("zhipu"),
)

pick() löst den Inneren auf:

go
// eino_routing.go:173
func (r *RoutingChatModel) pick(opts ...model.Option) (model.ChatModel, string, error) {
    po := model.GetImplSpecificOptions(&providerOpts{}, opts...)
    prov := po.provider
    if prov == "" {
        prov = r.defaultProvider
    }
    inner, ok := r.inner[prov]
    if !ok {
        return nil, prov, fmt.Errorf("%w: %s", ErrUnknownProvider, prov)
    }
    return inner, prov, nil
}

DefaultResolver — der dynamische Default

Der Bug, den das behebt: ein Admin schaltet default_provider in /settings/llm von Anthropic auf GLM um. Die Chat-Picker-UI respektiert den neuen Default sofort (sie re-fetcht /v1/aiops/models bei jedem Laden). Aber der RCA-Investigator-Worker — der prozess-intern ist und seinen Provider beim Boot bindet — routet weiterhin zu Anthropic bis zum Restart.

Fix: RoutingChatModelConfig.DefaultResolver.

go
var defaultResolver func(context.Context) (string, string)
if resolver != nil {
    defaultResolver = func(rctx context.Context) (string, string) {
        provCfgs, resolvedDefault, rerr := resolver.ResolveProviders(rctx)
        if rerr != nil || resolvedDefault == "" {
            return "", ""
        }
        for _, pc := range provCfgs {
            if pc.ID == resolvedDefault {
                return resolvedDefault, pc.Model
            }
        }
        return resolvedDefault, ""
    }
}
chatModel, err := llm.NewRoutingChatModel(llm.RoutingChatModelConfig{
    Inner:           innerModels,
    DefaultProvider: defProv,
    DefaultResolver: defaultResolver,
})

withDynamicDefault injiziert die Ausgabe des Resolvers als WithProvider + WithModel für Aufrufe, die weder noch gepinnt haben — der Chat-Picker pinnt Provider pro Message und umgeht den Resolver vollständig; Default-geroutete Aufrufe (Investigator, Translate, Wissens-Fan-out) folgen jetzt der Live-Konfiguration.

Per-Call-Provider in Chat

Für den MultiClient.Chat-Pfad (Nicht-Graph-Kernel-Caller) setzen Sie ChatReq.Provider explizit:

go
resp, err := llmClient.Chat(ctx, llm.ChatReq{
    Provider: "openai",
    Model:    "gpt-5.5",
    Messages: msgs,
})

Leerer Provider → MultiClient löst den Default zur Aufrufzeit auf. Leeres Model → der aufgelöste Sub-Client verwendet seinen konfigurierten Default.

Per-Call-Provider im Graph-Kernel

Die Agent-Runtime injiziert llm.WithProvider, wann immer das Chat-Send-Envelope einen nicht-leeren provider enthält. Die Persona-Registry kann auch einen Provider für eine gesamte Persona pinnen (z. B. die günstige Extraktor-Persona pinnt auf Anthropic Haiku). Siehe die Agent-Persona-Format-Referenz.

Fallen

  • default_provider vergessen — der Resolver wählt die erstsortierte Provider-ID; Sie senden Ihre Modell-ID an den falschen Endpunkt. Setzen Sie immer ONGRID_LLM_DEFAULT_PROVIDER (oder schreiben Sie die DB-Zeile).
  • Einen Provider pinnen, der keinen inneren ChatModel hat — passiert, wenn der Resolver eine Provider-ID zurückgibt, die nicht beim Boot registriert wurde UND nicht vorab registriert wurde. Der Custom-Slot ist genau aus diesem Grund vorab registriert; alles andere gatet auf cfg.LLM.*.APIKey != "".
  • Hot-Swap-Timing — der Cache-TTL ist 60s auf dem Resolver und 60s auf dem MultiClient. Worst-Case 120s, bevor ein Admin-Edit in Kraft tritt. Der Invalidate-Pfad ist exponiert, aber noch nicht mit der Save-Aktion der SPA verdrahtet.

Siehe auch

  • Modelle-Übersicht[]llm.ProviderConfig-Zusammenstellung.
  • Budget — orthogonal zum Routing; gleicher Cap gilt für jeden Provider.
  • RCA — Investigator-Worker-Routing.