Files
2026-07-13 12:35:43 +08:00

156 lines
5.2 KiB
Go

package pluginhost
import (
"bytes"
"context"
"strings"
"github.com/router-for-me/CLIProxyAPI/v7/sdk/pluginapi"
log "github.com/sirupsen/logrus"
)
func (h *Host) RouteModel(ctx context.Context, req pluginapi.ModelRouteRequest) (pluginapi.ModelRouteResponse, bool) {
return h.RouteModelExcept(ctx, req, "")
}
func (h *Host) HasModelRouters() bool {
return h.HasModelRoutersExcept("")
}
func (h *Host) HasModelRoutersExcept(skipPluginID string) bool {
if h == nil {
return false
}
skipPluginID = strings.TrimSpace(skipPluginID)
for _, record := range h.activeRecords() {
if record.plugin.Capabilities.ModelRouter != nil && !h.isPluginFused(record.id) && record.id != skipPluginID {
return true
}
}
return false
}
func (h *Host) RouteModelExcept(ctx context.Context, req pluginapi.ModelRouteRequest, skipPluginID string) (pluginapi.ModelRouteResponse, bool) {
if h == nil {
return pluginapi.ModelRouteResponse{}, false
}
skipPluginID = strings.TrimSpace(skipPluginID)
req.AvailableProviders = h.availableProvidersSnapshot()
for _, record := range h.activeRecords() {
router := record.plugin.Capabilities.ModelRouter
if router == nil || h.isPluginFused(record.id) || record.id == skipPluginID {
continue
}
nextReq := cloneModelRouteRequest(req)
nextReq.Plugin = clonePluginMetadata(record.meta)
nextReq.PluginID = record.id
resp, ok := h.callModelRouter(ctx, record.id, router, nextReq)
if !ok || !resp.Handled {
continue
}
resp, valid := normalizeModelRouteResponse(record.id, resp)
if !valid {
log.WithFields(log.Fields{"plugin_id": record.id, "target_kind": resp.TargetKind, "target": resp.Target}).Warn("pluginhost: model router returned invalid target")
continue
}
switch resp.TargetKind {
case pluginapi.ModelRouteTargetProvider:
if !h.HasBuiltinProvider(resp.Target) {
log.WithFields(log.Fields{"plugin_id": record.id, "target_provider": resp.Target}).Warn("pluginhost: model router returned unavailable provider")
continue
}
return resp, true
case pluginapi.ModelRouteTargetSelf, pluginapi.ModelRouteTargetExecutor:
if !h.executorPluginReady(resp.Target, nextReq) {
log.WithFields(log.Fields{"plugin_id": record.id, "target_plugin_id": resp.Target}).Warn("pluginhost: model router returned unavailable executor plugin")
continue
}
return resp, true
default:
log.WithFields(log.Fields{"plugin_id": record.id, "target_kind": resp.TargetKind}).Warn("pluginhost: model router returned unsupported target kind")
continue
}
}
return pluginapi.ModelRouteResponse{}, false
}
func (h *Host) callModelRouter(ctx context.Context, pluginID string, router pluginapi.ModelRouter, req pluginapi.ModelRouteRequest) (out pluginapi.ModelRouteResponse, ok bool) {
if h == nil || router == nil || h.isPluginFused(pluginID) {
return pluginapi.ModelRouteResponse{}, false
}
defer func() {
if recovered := recover(); recovered != nil {
h.fusePlugin(pluginID, "ModelRouter.RouteModel", recovered)
out = pluginapi.ModelRouteResponse{}
ok = false
}
}()
resp, errRoute := router.RouteModel(ctx, req)
if errRoute != nil {
log.WithField("plugin_id", pluginID).WithError(errRoute).Warn("pluginhost: model router failed")
return pluginapi.ModelRouteResponse{}, false
}
return resp, true
}
func normalizeModelRouteResponse(routerPluginID string, resp pluginapi.ModelRouteResponse) (pluginapi.ModelRouteResponse, bool) {
resp.TargetModel = strings.TrimSpace(resp.TargetModel)
switch resp.TargetKind {
case pluginapi.ModelRouteTargetSelf:
resp.Target = strings.TrimSpace(routerPluginID)
if resp.Target == "" {
return pluginapi.ModelRouteResponse{}, false
}
return resp, true
case pluginapi.ModelRouteTargetExecutor:
resp.Target = strings.TrimSpace(resp.Target)
if resp.Target == "" {
return pluginapi.ModelRouteResponse{}, false
}
return resp, true
case pluginapi.ModelRouteTargetProvider:
resp.Target = strings.ToLower(strings.TrimSpace(resp.Target))
if resp.Target == "" {
return pluginapi.ModelRouteResponse{}, false
}
return resp, true
default:
return pluginapi.ModelRouteResponse{}, false
}
}
func cloneModelRouteRequest(req pluginapi.ModelRouteRequest) pluginapi.ModelRouteRequest {
req.Headers = cloneHeader(req.Headers)
req.Query = cloneValues(req.Query)
req.Body = bytes.Clone(req.Body)
req.Metadata = cloneInterceptorMetadata(req.Metadata)
req.AvailableProviders = cloneStringSlice(req.AvailableProviders)
return req
}
// HasBuiltinProvider reports whether a built-in provider currently has at least one
// registered auth record.
func (h *Host) HasBuiltinProvider(provider string) bool {
if h == nil || h.authManager == nil {
return false
}
return h.authManager.HasProviderAuth(provider)
}
// BuiltinProviders returns built-in provider keys that currently have auth registered.
func (h *Host) BuiltinProviders() []string {
if h == nil || h.authManager == nil {
return nil
}
return h.authManager.AvailableProviders()
}
// availableProvidersSnapshot returns a defensive copy of BuiltinProviders for routing input.
func (h *Host) availableProvidersSnapshot() []string {
providers := h.BuiltinProviders()
if len(providers) == 0 {
return nil
}
return cloneStringSlice(providers)
}