라우팅 & 기본
Ongrid 는 N 개의 provider 를 병렬로 실행하고 각 LLM 호출을 정확히 하나로 디스패치합니다. 이 페이지는 세 레이어에서 그 디스패치가 어떻게 동작하는지 다룹니다:
MultiClient— 레거시llm.Chat경로 (번역, 지식 검색) 가 사용하는 와이어 레벨 라우터.RoutingChatModel— 그래프 커널 기반 ReAct 에이전트가 사용하는 eino model.ChatModel 래퍼.DefaultResolver—default_provider설정이 프로세스 중간에 효과를 내게 하는 동적 기본 훅.
MultiClient
라우터 스택의 바닥은 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 구성이 생성 시 하나의 서브 클라이언트를 생산:
// 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 에 디스패치:
- 비어 있지 않음 → 서브 클라이언트 조회; 404 →
ErrUnknownProvider. - 비어 있음 →
defaultProvider로 폴백.
MultiClient.SetProvidersResolver(r) 배선은 그 위에 DB 기반 resolver 를 레이어 — 매 호출마다 활성 서브 클라이언트 셋이 재해석 됨 (SetResolveTTL 로 60 초 캐시).
RoutingChatModel
그래프 커널은 llm.Chat 가 아닌 eino 의 model.ChatModel 인터페이스 사용. RoutingChatModel (eino_routing.go:89) 은 N 개의 내부 ChatModel 을 래핑하고 impl 특화 옵션으로 디스패치:
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 — 동적 기본
이것이 고치는 버그: 관리자가 /settings/llm 에서 default_provider 를 Anthropic 에서 GLM 으로 뒤집음. 채팅 picker UI 는 즉시 새 기본을 존중 (각 로드마다 /v1/aiops/models 재 fetch). 그러나 RCA investigator worker — 프로세스 내부이고 부팅 시 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 가 둘 다 고정하지 않은 호출에 대해 resolver 의 출력을 WithProvider + WithModel 로 주입 — 채팅 picker 는 메시지별로 provider 고정하고 resolver 를 완전히 우회; 기본 라우팅 호출 (investigator, 번역, 지식 fan-out) 은 이제 라이브 구성을 따릅니다.
Chat 의 호출별 provider
MultiClient.Chat 경로 (비 그래프 커널 호출자) 의 경우 ChatReq.Provider 를 명시적으로 설정:
resp, err := llmClient.Chat(ctx, llm.ChatReq{
Provider: "openai",
Model: "gpt-5.5",
Messages: msgs,
})빈 Provider → MultiClient 가 호출 시 기본 해석. 빈 Model → 해석된 서브 클라이언트가 구성된 기본 사용.
그래프 커널의 호출별 provider
에이전트 런타임이 채팅 send envelope 에 비어 있지 않은 provider 가 포함될 때마다 llm.WithProvider 를 주입. Persona 레지스트리는 전체 persona 에 대해 provider 를 고정할 수도 있음 (예: 저렴한 추출기 persona 가 Anthropic Haiku 에 고정). 에이전트 persona 포맷 참조 참고.
함정
default_provider잊기 — resolver 가 첫 정렬 provider id 를 고름; 모델 id 를 잘못된 엔드포인트로 보내게 됩니다. 항상ONGRID_LLM_DEFAULT_PROVIDER설정 (또는 DB 행 작성).- 내부 ChatModel 이 없는 provider 고정 — 부팅 시 등록되지 않았고 사전 등록되지 않은 provider id 를 resolver 가 반환할 때 발생. 정확히 이 이유로 custom 슬롯이 사전 등록; 나머지는 모두
cfg.LLM.*.APIKey != ""에 게이트. - 핫 스왑 타이밍 — 캐시 TTL 은 resolver 에 60 초, MultiClient 에 60 초. 관리자 편집이 효과 내기 전 최악 120 초. Invalidate 경로는 노출됐지만 아직 SPA 의 save 액션에 배선 안 됨.