Routing & default
Ongrid corre N providers en paralelo y hace dispatch de cada llamada LLM a exactamente uno. Esta página cubre cómo funciona ese dispatch en tres capas:
MultiClient— el router a nivel wire usado por la ruta legacyllm.Chat(translate, búsqueda de conocimiento).RoutingChatModel— el wrapper de eino model.ChatModel usado por el agente ReAct con graph-kernel.DefaultResolver— el hook de default dinámico que deja que la configuracióndefault_providertome efecto en medio del proceso.
MultiClient
El fondo del stack del router vive en 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)Cada provider config produce un sub-cliente en construcción:
// 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 hace dispatch sobre req.Provider:
- No vacío → busca el sub-cliente; 404 →
ErrUnknownProvider. - Vacío → cae a
defaultProvider.
El cableado MultiClient.SetProvidersResolver(r) capa el resolver respaldado por DB encima — en cada llamada el set de sub-cliente activo se re-resuelve (cached 60s por SetResolveTTL).
RoutingChatModel
El graph kernel usa la interfaz model.ChatModel de eino, no llm.Chat. RoutingChatModel (eino_routing.go:89) envuelve N ChatModels internos y hace dispatch vía una opción impl-específica:
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
})
}Uso desde un call site:
resp, err := chatModel.Generate(ctx, msgs,
model.WithModel("glm-4.7-flash"),
llm.WithProvider("zhipu"),
)pick() resuelve el inner:
// 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 — el default dinámico
El bug que esto arregla: un admin flippea default_provider de Anthropic a GLM en /settings/llm. La UI del picker de chat inmediatamente respeta el nuevo default (re-fetchea /v1/aiops/models en cada load). Pero el worker investigator RCA — que es process-internal y vincula su provider al boot — sigue ruteando a Anthropic hasta reiniciar.
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 inyecta la salida del resolver como WithProvider + WithModel para llamadas que no pinearon ninguno — el picker de chat pinea provider por-mensaje y bypassa al resolver totalmente; las llamadas default-routed (investigator, translate, fan-out de conocimiento) ahora siguen la configuración en vivo.
Provider por-llamada en Chat
Para la ruta MultiClient.Chat (callers no-graph-kernel), setea ChatReq.Provider explícitamente:
resp, err := llmClient.Chat(ctx, llm.ChatReq{
Provider: "openai",
Model: "gpt-5.5",
Messages: msgs,
})Provider vacío → MultiClient resuelve el default en tiempo de llamada. Model vacío → el sub-cliente resuelto usa su default configurado.
Provider por-llamada en el graph kernel
El agent runtime inyecta llm.WithProvider cada vez que el envelope de chat send contiene un provider no vacío. El registry de personas también puede pinear un provider para una persona entera (p. ej. la persona extractor barata pinea a Anthropic Haiku). Ver la referencia de formato de persona de agente.
Trampas
- Olvidarse de
default_provider— el resolver elige el primer provider id ordenado; enviarás tu id de modelo al endpoint equivocado. Siempre seteaONGRID_LLM_DEFAULT_PROVIDER(o escribe la fila DB). - Pinear un provider que no tiene ChatModel interno — pasa cuando el resolver devuelve un provider id que no fue registrado al boot Y no fue pre-registrado. El slot custom está pre-registrado por exactamente esta razón; todo lo demás gateaa en
cfg.LLM.*.APIKey != "". - Timing de hot-swap — el TTL de cache es 60s en el resolver y 60s en el MultiClient. En el peor caso 120s antes de que una edición de admin tome efecto. La ruta Invalidate está expuesta pero no cableada a la acción save de la SPA aún.
Ver también
- Overview de modelos — ensamblaje de
[]llm.ProviderConfig. - Budget — ortogonal al routing; el mismo tope aplica a cada provider.
- RCA — routing del worker investigator.