Skip to content

ルーティングとデフォルト

Ongrid は N プロバイダーを並列に走らせ、各 LLM 呼び出しを正確に 1 つに dispatch します。このページではその dispatch を 3 レイヤーで説明します:

  1. MultiClient —— レガシー llm.Chat パス(translate、ナレッジ検索) が使うワイヤーレベルのルーター。
  2. RoutingChatModel —— グラフカーネル ReAct エージェントが使う eino model.ChatModel ラッパー。
  3. DefaultResolver —— default_provider 設定をプロセス途中で 効かせる動的デフォルトフック。

MultiClient

ルータースタックの底は 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)

各プロバイダー設定は構築時に 1 つのサブクライアントを生成します:

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)

Chatreq.Provider で dispatch します:

  • 空でない → サブクライアントを引く。404 → ErrUnknownProvider
  • 空 → defaultProvider にフォールバック。

MultiClient.SetProvidersResolver(r) 配線が DB バック resolver を上に 重ねます —— 呼び出しごとにアクティブなサブクライアントセットが 再解決されます(SetResolveTTL で 60 秒キャッシュ)。

RoutingChatModel

グラフカーネルは llm.Chat ではなく eino の model.ChatModel インター フェイスを使います。RoutingChatModel (eino_routing.go:89) は N 個の inner ChatModel をラップし、impl 固有オプションで dispatch します:

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
    })
}

呼び出し側の使い方:

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

pick() が inner を解決します:

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 —— 動的デフォルト

これが修正するバグ:admin が /settings/llmdefault_provider を Anthropic から GLM に切り替えます。チャットピッカー UI はすぐに新しい デフォルトを尊重します(ロードごとに /v1/aiops/models を再フェッチ するので)。しかし RCA investigator worker —— プロセス内部で起動時に プロバイダーをバインドする —— は再起動まで Anthropic にルーティングし 続けます。

修正: 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 は、どちらもピンされていない呼び出しに対し resolver の出力を WithProvider + WithModel として注入します —— チャットピッカー はメッセージごとにプロバイダーをピンして resolver を完全にバイパスし、 デフォルトルーティングされた呼び出し(investigator、translate、ナレッジ ファンアウト)が今やライブ設定に追従します。

Chat での per-call プロバイダー

MultiClient.Chat パス(非グラフカーネル呼び出し元)では、ChatReq.Provider を明示的に設定:

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

空の Provider → MultiClient が呼び出し時にデフォルトを解決。空の Model → 解決されたサブクライアントが設定済みデフォルトを使用。

グラフカーネルでの per-call プロバイダー

エージェントランタイムは、チャット送信エンベロープに空でない provider が含まれるたび llm.WithProvider を注入します。ペルソナレジストリは ペルソナ全体に対してプロバイダーをピン留めすることもできます(例: 安価な抽出器ペルソナは Anthropic Haiku にピン留め)。エージェント ペルソナフォーマットリファレンスを参照。

落とし穴

  • default_provider を忘れる —— resolver は最初にソートされた プロバイダー id を選びます。モデル id を誤ったエンドポイントに送って しまいます。常に ONGRID_LLM_DEFAULT_PROVIDER を設定(または DB 行に 書き込み)してください。
  • inner ChatModel が無いプロバイダーをピン留め —— resolver が起動時に 登録されておらず事前登録もされていないプロバイダー id を返したときに 起きます。custom スロットがこの理由で事前登録されています。それ以外は cfg.LLM.*.APIKey != "" でゲートされます。
  • ホットスワップタイミング —— キャッシュ TTL は resolver で 60 秒、 MultiClient で 60 秒。admin 編集が効くまで最悪 120 秒。Invalidate パス は公開されていますが、まだ SPA の save アクションには配線されて いません。

関連

  • モデル概要 —— []llm.ProviderConfig の組み立て。
  • バジェット —— ルーティングと直交。同じキャップがすべての プロバイダーに適用される。
  • RCA —— investigator worker のルーティング。