Routing & default
Ongrid запускает N providers параллельно и диспетчирует каждый LLM-вызов на ровно одного. Эта страница покрывает, как этот dispatch работает на трёх слоях:
MultiClient— wire-level router, используемый legacyllm.Chatпутём (translate, knowledge search).RoutingChatModel— обёртка eino model.ChatModel, используемая graph-kernel ReAct-агентом.DefaultResolver— dynamic-default hook, который позволяет настройкеdefault_providerвступать в силу mid-process.
MultiClient
Низ router-стека живёт в 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)Каждый provider config производит один sub-клиент при конструировании:
// 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 диспетчирует на req.Provider:
- Непустой → искать sub-клиент; 404 →
ErrUnknownProvider. - Пустой → fall back на
defaultProvider.
Wiring MultiClient.SetProvidersResolver(r) накладывает DB-backed resolver сверху — на каждый вызов активный sub-клиент set ре-разрешается (кэшируется 60с через SetResolveTTL).
RoutingChatModel
Graph kernel использует интерфейс eino model.ChatModel, не llm.Chat. RoutingChatModel (eino_routing.go:89) оборачивает N внутренних ChatModel и диспетчирует через impl-specific опцию:
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
})
}Использование с места вызова:
resp, err := chatModel.Generate(ctx, msgs,
model.WithModel("glm-4.7-flash"),
llm.WithProvider("zhipu"),
)pick() разрешает внутренний:
// 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 — dynamic default
Баг, который это чинит: админ переключает default_provider с Anthropic на GLM в /settings/llm. UI chat-picker немедленно уважает новый дефолт (он ре-fetch'ит /v1/aiops/models на каждой загрузке). Но RCA investigator worker — который process-internal и связывает свой provider при загрузке — продолжает маршрутизировать на Anthropic до рестарта.
Фикс: 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 инъектит output resolver'а как WithProvider + WithModel для вызовов, которые не закрепили ни того, ни другого — chat-picker закрепляет provider per-message и обходит resolver полностью; default-routed вызовы (investigator, translate, knowledge fan-out) теперь следуют live конфигурации.
Per-call provider в Chat
Для пути MultiClient.Chat (non-graph-kernel вызывающие), установите ChatReq.Provider явно:
resp, err := llmClient.Chat(ctx, llm.ChatReq{
Provider: "openai",
Model: "gpt-5.5",
Messages: msgs,
})Пустой Provider → MultiClient разрешает дефолт во время вызова. Пустая Model → разрешённый sub-клиент использует свой сконфигурированный дефолт.
Per-call provider в graph kernel
Agent runtime инъектит llm.WithProvider всякий раз, когда chat send envelope содержит непустой provider. Persona-реестр может также закрепить provider для целой персоны (например, дешёвая extractor-персона закрепляет на Anthropic Haiku). См. справочник формата персоны агента.
Подводные камни
- Забыть
default_provider— resolver выбирает первый отсортированный provider id; вы пошлёте ваш model id на неправильный эндпоинт. Всегда ставьтеONGRID_LLM_DEFAULT_PROVIDER(или пишите DB-строку). - Закрепление provider, у которого нет внутренней ChatModel — случается, когда resolver возвращает provider id, который не был зарегистрирован при загрузке И не был pre-registered. Custom slot pre-registered ровно по этой причине; всё остальное гейтится на
cfg.LLM.*.APIKey != "". - Hot-swap timing — cache TTL — 60с на resolver и 60с на MultiClient. Worst case 120с до того, как admin-правка вступит в силу. Invalidate-путь выставлен, но ещё не подключён к save- действию SPA.
См. также
- Обзор моделей — сборка
[]llm.ProviderConfig. - Budget — ортогонально routing; тот же cap применяется к каждому provider.
- RCA — маршрутизация investigator-воркера.