Routing & défaut
Ongrid exécute N providers en parallèle et dispatch chaque appel LLM à exactement un. Cette page couvre comment ce dispatch fonctionne à trois couches :
MultiClient— le routeur au niveau du wire utilisé par le chemin legacyllm.Chat(translate, recherche dans la base de connaissances).RoutingChatModel— le wrapper eino model.ChatModel utilisé par l'agent ReAct graph-kernel.DefaultResolver— le hook de défaut dynamique qui fait que le réglagedefault_providerprend effet en cours de processus.
MultiClient
Le bas de la stack du routeur vit dans internal/pkg/llm/router.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)Chaque config de provider produit un sous-client à la construction :
// 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 dispatch sur req.Provider :
- Non vide → recherche du sous-client ; 404 →
ErrUnknownProvider. - Vide → fallback sur
defaultProvider.
Le câblage MultiClient.SetProvidersResolver(r) superpose le resolver adossé à la DB par-dessus — à chaque appel, l'ensemble de sous-clients actif est re-résolu (caché 60s par SetResolveTTL).
RoutingChatModel
Le graph kernel utilise l'interface model.ChatModel d'eino, pas llm.Chat. RoutingChatModel (eino_routing.go:89) wrappe N ChatModels internes et dispatch via une option impl-spécifique :
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
})
}Usage depuis un site d'appel :
resp, err := chatModel.Generate(ctx, msgs,
model.WithModel("glm-4.7-flash"),
llm.WithProvider("zhipu"),
)pick() résout l'interne :
// 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 — le défaut dynamique
Le bug que ceci corrige : un admin bascule default_provider d'Anthropic à GLM dans /settings/llm. L'UI du picker de chat respecte immédiatement le nouveau défaut (elle re-fetch /v1/aiops/models à chaque chargement). Mais le worker investigator RCA — qui est interne au processus et lie son provider au boot — continue à router vers Anthropic jusqu'au redémarrage.
Fix : RoutingChatModelConfig.DefaultResolver.
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 injecte la sortie du resolver comme WithProvider + WithModel pour les appels qui n'ont épinglé ni l'un ni l'autre — le picker de chat épingle le provider par-message et bypass le resolver entièrement ; les appels routés par défaut (investigator, translate, fan-out de base de connaissances) suivent maintenant la configuration en direct.
Provider par appel dans Chat
Pour le chemin MultiClient.Chat (appelants non-graph-kernel), définissez ChatReq.Provider explicitement :
resp, err := llmClient.Chat(ctx, llm.ChatReq{
Provider: "openai",
Model: "gpt-5.5",
Messages: msgs,
})Provider vide → MultiClient résout le défaut au moment de l'appel. Model vide → le sous-client résolu utilise son défaut configuré.
Provider par appel dans le graph kernel
Le runtime de l'agent injecte llm.WithProvider chaque fois que l'enveloppe d'envoi du chat contient un provider non vide. Le registre de persona peut aussi épingler un provider pour toute une persona (par ex. la persona extracteur bon marché s'épingle à Anthropic Haiku). Voir la référence du format de persona d'agent.
Pièges
- Oublier
default_provider— le resolver choisit le premier id de provider trié ; vous enverrez votre id de modèle au mauvais endpoint. Définissez toujoursONGRID_LLM_DEFAULT_PROVIDER(ou écrivez la ligne DB). - Épingler un provider qui n'a pas de ChatModel interne — arrive quand le resolver retourne un id de provider qui n'a pas été enregistré au boot ET n'a pas été pré-enregistré. Le slot custom est pré-enregistré exactement pour cette raison ; tout le reste dépend de
cfg.LLM.*.APIKey != "". - Timing de hot-swap — le TTL du cache est de 60s sur le resolver et 60s sur le MultiClient. Pire cas 120s avant qu'un edit admin prenne effet. Le chemin Invalidate est exposé mais pas encore câblé à l'action de sauvegarde de la SPA.
Voir aussi
- Vue d'ensemble des modèles — assemblage de
[]llm.ProviderConfig. - Budget — orthogonal au routing ; le même plafond s'applique à chaque provider.
- RCA — routing du worker investigator.