Files
CLIProxyAPI/sdk/pluginhost/host.go
2026-06-22 22:25:44 +08:00

279 lines
8.0 KiB
Go

package pluginhost
import (
"context"
internalconfig "github.com/router-for-me/CLIProxyAPI/v7/internal/config"
internalpluginhost "github.com/router-for-me/CLIProxyAPI/v7/internal/pluginhost"
internalregistry "github.com/router-for-me/CLIProxyAPI/v7/internal/registry"
coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
"github.com/router-for-me/CLIProxyAPI/v7/sdk/pluginapi"
"gopkg.in/yaml.v3"
)
// ModelInfo describes a plugin-provided model using public plugin SDK types.
type ModelInfo = pluginapi.ModelInfo
// ThinkingSupport describes plugin-provided thinking controls.
type ThinkingSupport = pluginapi.ThinkingSupport
// OAuthModelAlias defines a model ID alias for OAuth/file-backed auth channels.
type OAuthModelAlias struct {
Name string
Alias string
Fork bool
}
// RuntimeConfig is the public plugin host configuration used by embedders.
type RuntimeConfig struct {
Enabled bool
Dir string
AuthDir string
ProxyURL string
ForceModelPrefix bool
OAuthModelAlias map[string][]OAuthModelAlias
OAuthExcludedModels map[string][]string
Configs map[string]PluginInstanceConfig
}
// PluginInstanceConfig stores host-owned plugin settings and the original plugin YAML subtree.
type PluginInstanceConfig struct {
Enabled *bool
Priority int
Raw yaml.Node
}
// AuthModelResult is the public result for per-auth model discovery.
type AuthModelResult struct {
Provider string
Models []ModelInfo
Auth *coreauth.Auth
Handled bool
Err error
}
// Host wraps the internal plugin host behind a public SDK surface.
type Host struct {
inner *internalpluginhost.Host
}
// New creates a plugin host.
func New() *Host {
return &Host{inner: internalpluginhost.New()}
}
// ApplyConfig applies plugin runtime configuration.
func (h *Host) ApplyConfig(ctx context.Context, cfg RuntimeConfig) {
if h == nil || h.inner == nil {
return
}
internalCfg := runtimeConfigToInternalConfig(cfg)
h.inner.ApplyConfig(ctx, internalCfg)
}
// ShutdownAll unloads every active plugin.
func (h *Host) ShutdownAll() {
if h == nil || h.inner == nil {
return
}
h.inner.ShutdownAll()
}
// ParseAuth lets plugin auth providers parse a credential payload.
func (h *Host) ParseAuth(ctx context.Context, req pluginapi.AuthParseRequest) (*coreauth.Auth, bool, error) {
if h == nil || h.inner == nil {
return nil, false, nil
}
return h.inner.ParseAuth(ctx, req)
}
// ModelsForAuth lets plugin model providers discover auth-bound models.
func (h *Host) ModelsForAuth(ctx context.Context, auth *coreauth.Auth) AuthModelResult {
if h == nil || h.inner == nil {
return AuthModelResult{}
}
result := h.inner.ModelsForAuth(ctx, auth)
return AuthModelResult{
Provider: result.Provider,
Models: registryModelsToPluginModels(result.Models),
Auth: result.Auth,
Handled: result.Handled,
Err: result.Err,
}
}
// ModelsForProvider returns static models registered for a provider by plugins.
func (h *Host) ModelsForProvider(provider string) []ModelInfo {
if h == nil || h.inner == nil {
return nil
}
return registryModelsToPluginModels(h.inner.ModelsForProvider(provider))
}
// RefreshAuth lets plugin auth providers refresh a credential.
func (h *Host) RefreshAuth(ctx context.Context, auth *coreauth.Auth) (*coreauth.Auth, bool, error) {
if h == nil || h.inner == nil {
return nil, false, nil
}
return h.inner.RefreshAuth(ctx, auth)
}
// PickAuth lets a scheduler plugin choose an auth candidate.
func (h *Host) PickAuth(ctx context.Context, req pluginapi.SchedulerPickRequest) (pluginapi.SchedulerPickResponse, bool, error) {
if h == nil || h.inner == nil {
return pluginapi.SchedulerPickResponse{}, false, nil
}
return h.inner.PickAuth(ctx, req)
}
// HasScheduler reports whether any active plugin provides a scheduler.
func (h *Host) HasScheduler() bool {
return h != nil && h.inner != nil && h.inner.HasScheduler()
}
func runtimeConfigToInternalConfig(cfg RuntimeConfig) *internalconfig.Config {
out := &internalconfig.Config{
SDKConfig: internalconfig.SDKConfig{
ProxyURL: cfg.ProxyURL,
ForceModelPrefix: cfg.ForceModelPrefix,
},
AuthDir: cfg.AuthDir,
OAuthExcludedModels: cloneStringSliceMap(cfg.OAuthExcludedModels),
OAuthModelAlias: oauthModelAliasToInternal(cfg.OAuthModelAlias),
Plugins: internalconfig.PluginsConfig{
Enabled: cfg.Enabled,
Dir: cfg.Dir,
Configs: pluginConfigsToInternal(cfg.Configs),
},
}
out.NormalizePluginsConfig()
out.SanitizeOAuthModelAlias()
return out
}
func pluginConfigsToInternal(in map[string]PluginInstanceConfig) map[string]internalconfig.PluginInstanceConfig {
if len(in) == 0 {
return nil
}
out := make(map[string]internalconfig.PluginInstanceConfig, len(in))
for id, item := range in {
out[id] = internalconfig.PluginInstanceConfig{
Enabled: item.Enabled,
Priority: item.Priority,
Raw: *deepCopyYAMLNode(&item.Raw),
}
}
return out
}
func oauthModelAliasToInternal(in map[string][]OAuthModelAlias) map[string][]internalconfig.OAuthModelAlias {
if len(in) == 0 {
return nil
}
out := make(map[string][]internalconfig.OAuthModelAlias, len(in))
for provider, aliases := range in {
if len(aliases) == 0 {
continue
}
items := make([]internalconfig.OAuthModelAlias, 0, len(aliases))
for _, alias := range aliases {
items = append(items, internalconfig.OAuthModelAlias{
Name: alias.Name,
Alias: alias.Alias,
Fork: alias.Fork,
})
}
out[provider] = items
}
if len(out) == 0 {
return nil
}
return out
}
func registryModelsToPluginModels(models []*internalregistry.ModelInfo) []ModelInfo {
if len(models) == 0 {
return nil
}
out := make([]ModelInfo, 0, len(models))
for _, model := range models {
if model == nil {
continue
}
out = append(out, registryModelToPluginModel(model))
}
return out
}
func registryModelToPluginModel(model *internalregistry.ModelInfo) ModelInfo {
if model == nil {
return ModelInfo{}
}
return ModelInfo{
ID: model.ID,
Object: model.Object,
Created: model.Created,
OwnedBy: model.OwnedBy,
Type: model.Type,
DisplayName: model.DisplayName,
Name: model.Name,
Version: model.Version,
Description: model.Description,
InputTokenLimit: int64(model.InputTokenLimit),
OutputTokenLimit: int64(model.OutputTokenLimit),
SupportedGenerationMethods: cloneStringSlice(model.SupportedGenerationMethods),
ContextLength: int64(model.ContextLength),
MaxCompletionTokens: int64(model.MaxCompletionTokens),
SupportedParameters: cloneStringSlice(model.SupportedParameters),
SupportedInputModalities: cloneStringSlice(model.SupportedInputModalities),
SupportedOutputModalities: cloneStringSlice(model.SupportedOutputModalities),
Thinking: thinkingSupportToPlugin(model.Thinking),
UserDefined: model.UserDefined,
}
}
func thinkingSupportToPlugin(thinking *internalregistry.ThinkingSupport) *ThinkingSupport {
if thinking == nil {
return nil
}
return &ThinkingSupport{
Min: thinking.Min,
Max: thinking.Max,
ZeroAllowed: thinking.ZeroAllowed,
DynamicAllowed: thinking.DynamicAllowed,
Levels: cloneStringSlice(thinking.Levels),
}
}
func cloneStringSlice(in []string) []string {
if len(in) == 0 {
return nil
}
return append([]string(nil), in...)
}
func cloneStringSliceMap(in map[string][]string) map[string][]string {
if len(in) == 0 {
return nil
}
out := make(map[string][]string, len(in))
for key, values := range in {
out[key] = cloneStringSlice(values)
}
return out
}
func deepCopyYAMLNode(node *yaml.Node) *yaml.Node {
if node == nil {
return &yaml.Node{}
}
copyNode := *node
if len(node.Content) > 0 {
copyNode.Content = make([]*yaml.Node, 0, len(node.Content))
for _, child := range node.Content {
copyNode.Content = append(copyNode.Content, deepCopyYAMLNode(child))
}
}
return &copyNode
}