Ctrl K
ComunidadGuías…

internal/pluginhost/adapters.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package pluginhost

import (
	"context"
	"fmt"
	"strings"

	"github.com/router-for-me/CLIProxyAPI/v8/internal/registry"
	coreauth "github.com/router-for-me/CLIProxyAPI/v8/sdk/cliproxy/auth"
	"github.com/router-for-me/CLIProxyAPI/v8/sdk/pluginapi"
	sdktranslator "github.com/router-for-me/CLIProxyAPI/v8/sdk/translator"
	_ "github.com/router-for-me/CLIProxyAPI/v8/sdk/translator/builtin"
	log "github.com/sirupsen/logrus"
)

type registryModelInfo = registry.ModelInfo

type modelRegistry interface {
	RegisterClient(clientID, clientProvider string, models []*registry.ModelInfo)
	UnregisterClient(clientID string)
}

type modelProviderRegistry interface {
	modelRegistry
	GetModelProviders(modelID string) []string
}

type pluginModelRegistration struct {
	pluginID    string
	provider    string
	priority    int
	models      []*registry.ModelInfo
	hasExecutor bool
}

func normalizedExecutorModelScope(caps pluginapi.Capabilities) pluginapi.ExecutorModelScope {
	if caps.Executor == nil {
		return pluginapi.ExecutorModelScopeBoth
	}
	switch caps.ExecutorModelScope {
	case pluginapi.ExecutorModelScopeStatic, pluginapi.ExecutorModelScopeOAuth, pluginapi.ExecutorModelScopeBoth:
		return caps.ExecutorModelScope
	default:
		return pluginapi.ExecutorModelScopeBoth
	}
}

func executorScopeAllowsStaticModels(caps pluginapi.Capabilities) bool {
	if caps.Executor == nil {
		return true
	}
	scope := normalizedExecutorModelScope(caps)
	return scope == pluginapi.ExecutorModelScopeStatic || scope == pluginapi.ExecutorModelScopeBoth
}

func executorScopeAllowsOAuthModels(caps pluginapi.Capabilities) bool {
	if caps.Executor == nil {
		return true
	}
	scope := normalizedExecutorModelScope(caps)
	return scope == pluginapi.ExecutorModelScopeOAuth || scope == pluginapi.ExecutorModelScopeBoth
}

func normalizeExecutorFormats(raw []string) []sdktranslator.Format {
	if len(raw) == 0 {
		return nil
	}
	out := make([]sdktranslator.Format, 0, len(raw))
	seen := make(map[string]struct{}, len(raw))
	for _, item := range raw {
		format := normalizeExecutorFormatName(item)
		if format == "" {
			continue
		}
		key := format.String()
		if _, exists := seen[key]; exists {
			continue
		}
		seen[key] = struct{}{}
		out = append(out, format)
	}
	return out
}

func normalizeExecutorFormatName(raw string) sdktranslator.Format {
	switch strings.ToLower(strings.TrimSpace(raw)) {
	case "", "none":
		return ""
	case "chat-completions", "chat_completions", "openai-chat-completions", "openai_chat_completions":
		return sdktranslator.FormatOpenAI
	case "responses", "openai-responses", "openai_responses":
		return sdktranslator.FormatOpenAIResponse
	case "anthropic":
		return sdktranslator.FormatClaude
	default:
		return sdktranslator.FromString(strings.TrimSpace(raw))
	}
}

func executorFormatContains(formats []sdktranslator.Format, target sdktranslator.Format) bool {
	if target == "" {
		return false
	}
	for _, format := range formats {
		if format == target {
			return true
		}
	}
	return false
}

type AuthModelResult struct {
	Provider string
	Models   []*registry.ModelInfo
	Auth     *coreauth.Auth
	Handled  bool
	Err      error
}

func pluginModelInfoToRegistryModelInfo(model pluginapi.ModelInfo) *registry.ModelInfo {
	return &registry.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:            int(model.InputTokenLimit),
		OutputTokenLimit:           int(model.OutputTokenLimit),
		SupportedGenerationMethods: cloneStringSlice(model.SupportedGenerationMethods),
		ContextLength:              int(model.ContextLength),
		MaxCompletionTokens:        int(model.MaxCompletionTokens),
		SupportedParameters:        cloneStringSlice(model.SupportedParameters),
		SupportedInputModalities:   cloneStringSlice(model.SupportedInputModalities),
		SupportedOutputModalities:  cloneStringSlice(model.SupportedOutputModalities),
		Thinking:                   pluginThinkingSupportToRegistryThinkingSupport(model.Thinking),
		UserDefined:                model.UserDefined,
	}
}

func pluginThinkingSupportToRegistryThinkingSupport(thinking *pluginapi.ThinkingSupport) *registry.ThinkingSupport {
	if thinking == nil {
		return nil
	}
	return &registry.ThinkingSupport{
		Min:            thinking.Min,
		Max:            thinking.Max,
		ZeroAllowed:    thinking.ZeroAllowed,
		DynamicAllowed: thinking.DynamicAllowed,
		DisableType:    thinking.DisableType,
		Levels:         cloneStringSlice(thinking.Levels),
	}
}

func registryModelInfoToPluginModelInfo(model *registry.ModelInfo) pluginapi.ModelInfo {
	if model == nil {
		return pluginapi.ModelInfo{}
	}
	return pluginapi.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:                   registryThinkingSupportToPluginThinkingSupport(model.Thinking),
		UserDefined:                model.UserDefined,
	}
}

func registryThinkingSupportToPluginThinkingSupport(thinking *registry.ThinkingSupport) *pluginapi.ThinkingSupport {
	if thinking == nil {
		return nil
	}
	return &pluginapi.ThinkingSupport{
		Min:            thinking.Min,
		Max:            thinking.Max,
		ZeroAllowed:    thinking.ZeroAllowed,
		DynamicAllowed: thinking.DynamicAllowed,
		DisableType:    thinking.DisableType,
		Levels:         cloneStringSlice(thinking.Levels),
	}
}

func cloneStringSlice(in []string) []string {
	if len(in) == 0 {
		return nil
	}
	return append([]string(nil), in...)
}

func cloneRegistryModels(in []*registry.ModelInfo) []*registry.ModelInfo {
	if len(in) == 0 {
		return nil
	}
	out := make([]*registry.ModelInfo, 0, len(in))
	for _, model := range in {
		if model == nil {
			continue
		}
		copyModel := *model
		copyModel.SupportedGenerationMethods = cloneStringSlice(model.SupportedGenerationMethods)
		copyModel.SupportedParameters = cloneStringSlice(model.SupportedParameters)
		copyModel.SupportedInputModalities = cloneStringSlice(model.SupportedInputModalities)
		copyModel.SupportedOutputModalities = cloneStringSlice(model.SupportedOutputModalities)
		if model.Thinking != nil {
			thinking := *model.Thinking
			thinking.Levels = cloneStringSlice(model.Thinking.Levels)
			copyModel.Thinking = &thinking
		}
		out = append(out, &copyModel)
	}
	return out
}

func normalizeModelRegistrationResponse(resp pluginapi.ModelRegistrationResponse) []pluginapi.ProviderModels {
	if len(resp.Registrations) > 0 {
		return resp.Registrations
	}
	return []pluginapi.ProviderModels{{
		Provider: resp.Provider,
		Models:   resp.Models,
	}}
}

// RegisterModels collects static catalogs for executor and per-auth registration.
// Catalog-only plugins must not advertise availability independently of credentials.
func (h *Host) RegisterModels(ctx context.Context, modelRegistry modelRegistry) {
	if h == nil || modelRegistry == nil {
		return
	}

	snap := h.Snapshot()
	records := h.activeRecordsFromSnapshot(snap)
	nextProviders := make(map[string]string)
	nextProviderSets := make(map[string][]string)
	nextModelRegistrations := make(map[string]pluginModelRegistration)
	nextPluginModelRegistrations := make(map[string][]pluginModelRegistration)
	for _, record := range records {
		modelProvider := record.plugin.Capabilities.ModelProvider
		registrar := record.plugin.Capabilities.ModelRegistrar
		if modelProvider == nil && registrar == nil {
			continue
		}
		if !executorScopeAllowsStaticModels(record.plugin.Capabilities) {
			continue
		}
		var resp pluginapi.ModelRegistrationResponse
		var errRegisterModels error
		if modelProvider != nil {
			modelResp, errStaticModels := h.callModelProviderStaticModels(ctx, record, modelProvider)
			errRegisterModels = errStaticModels
			resp = pluginapi.ModelRegistrationResponse{
				Provider:      modelResp.Provider,
				Models:        modelResp.Models,
				Registrations: modelResp.Registrations,
			}
		} else {
			resp, errRegisterModels = h.callModelRegistrar(ctx, record, registrar)
		}
		if errRegisterModels != nil {
			log.Warnf("pluginhost: model registrar %s failed: %v", record.id, errRegisterModels)
			continue
		}

		entries := normalizeModelRegistrationResponse(resp)
		var providersForPlugin []string
		seenProviders := make(map[string]struct{}, len(entries))
		var regsForPlugin []pluginModelRegistration
		regIndexByProvider := make(map[string]int, len(entries))
		for _, entry := range entries {
			provider := strings.ToLower(strings.TrimSpace(entry.Provider))
			if provider == "" || len(entry.Models) == 0 {
				continue
			}

			models := make([]*registry.ModelInfo, 0, len(entry.Models))
			for _, item := range entry.Models {
				model := pluginModelInfoToRegistryModelInfo(item)
				if model == nil || strings.TrimSpace(model.ID) == "" {
					continue
				}
				model.ID = strings.TrimSpace(model.ID)
				models = append(models, model)
			}
			if len(models) == 0 {
				continue
			}

			if _, exists := seenProviders[provider]; !exists {
				seenProviders[provider] = struct{}{}
				providersForPlugin = append(providersForPlugin, provider)
			}
			if idx, exists := regIndexByProvider[provider]; exists {
				merged := map[string][]*registry.ModelInfo{provider: regsForPlugin[idx].models}
				appendModelsForProvider(merged, provider, models)
				regsForPlugin[idx].models = merged[provider]
				continue
			}
			regIndexByProvider[provider] = len(regsForPlugin)
			regsForPlugin = append(regsForPlugin, pluginModelRegistration{
				pluginID:    record.id,
				provider:    provider,
				priority:    record.priority,
				models:      cloneRegistryModels(models),
				hasExecutor: record.plugin.Capabilities.Executor != nil,
			})
		}
		if len(regsForPlugin) == 0 {
			continue
		}

		nextModelRegistrations[record.id] = regsForPlugin[0]
		nextPluginModelRegistrations[record.id] = regsForPlugin
		nextProviders[record.id] = providersForPlugin[0]
		nextProviderSets[record.id] = providersForPlugin
	}
	h.mu.Lock()
	defer h.mu.Unlock()
	if h.Snapshot() != snap {
		return
	}
	h.modelProviders = nextProviders
	h.modelProviderSets = nextProviderSets
	h.modelRegistrations = nextModelRegistrations
	h.pluginModelRegistrations = nextPluginModelRegistrations
}

// HasAuthModelProvider reports whether an active plugin declares per-auth model
// discovery for provider. It does not fetch models or refresh credentials.
func (h *Host) HasAuthModelProvider(provider string) bool {
	if h == nil {
		return false
	}
	provider = normalizeProviderID(provider)
	if provider == "" {
		return false
	}
	for _, record := range h.activeRecords() {
		if h.authModelProviderMatches(record, provider) {
			return true
		}
	}
	return false
}

func (h *Host) authModelProviderMatches(record capabilityRecord, provider string) bool {
	caps := record.plugin.Capabilities
	if caps.ModelProvider == nil || h.isPluginFused(record.id) || !executorScopeAllowsOAuthModels(caps) {
		return false
	}
	if hasRecordAuthProvider(record) {
		identifiers, ok := h.callRecordAuthProviderIdentifiers(record)
		if !ok {
			return false
		}
		for _, identifier := range identifiers {
			if normalizeProviderID(identifier) == provider {
				return true
			}
		}
	}
	candidates := h.modelProvidersForPlugin(record.id)
	if len(candidates) > 0 {
		for _, candidate := range candidates {
			if normalizeProviderID(candidate) == provider {
				return true
			}
		}
		return false
	}
	if caps.Executor != nil {
		candidate, _ := h.executorProvider(record, caps.Executor)
		return candidate == provider
	}
	return false
}

func (h *Host) ModelsForAuth(ctx context.Context, auth *coreauth.Auth) AuthModelResult {
	if h == nil || auth == nil {
		return AuthModelResult{}
	}
	providerKey := normalizeProviderID(auth.Provider)
	if providerKey == "" {
		return AuthModelResult{}
	}
	for _, record := range h.activeRecords() {
		if !h.authModelProviderMatches(record, providerKey) {
			continue
		}
		resp, errModels := h.callModelsForAuth(ctx, record, record.plugin.Capabilities.ModelProvider, auth)
		if errModels != nil {
			log.Warnf("pluginhost: models for auth %s failed: %v", auth.ID, errModels)
			return AuthModelResult{Handled: true, Err: errModels}
		}
		respProvider := normalizeProviderID(resp.Provider)
		if respProvider != "" && respProvider != providerKey {
			continue
		}
		if respProvider == "" {
			respProvider = providerKey
		}
		models := make([]*registry.ModelInfo, 0, len(resp.Models))
		for _, item := range resp.Models {
			model := pluginModelInfoToRegistryModelInfo(item)
			if model != nil {
				model.ID = strings.TrimSpace(model.ID)
			}
			if model != nil && model.ID != "" {
				models = append(models, model)
			}
		}
		path := ""
		if auth.Attributes != nil {
			path = auth.Attributes["path"]
		}
		var updated *coreauth.Auth
		if authDataHasValue(resp.AuthUpdate) {
			updated = h.AuthDataToCoreAuth(authDataWithDefaults(resp.AuthUpdate, auth), path, auth.FileName)
		}
		return AuthModelResult{Provider: respProvider, Models: models, Auth: updated, Handled: true}
	}
	return AuthModelResult{}
}

func authDataHasValue(data pluginapi.AuthData) bool {
	return strings.TrimSpace(data.Provider) != "" ||
		strings.TrimSpace(data.ID) != "" ||
		strings.TrimSpace(data.FileName) != "" ||
		strings.TrimSpace(data.Label) != "" ||
		strings.TrimSpace(data.Prefix) != "" ||
		strings.TrimSpace(data.ProxyURL) != "" ||
		data.Disabled ||
		len(data.StorageJSON) > 0 ||
		len(data.Metadata) > 0 ||
		len(data.Attributes) > 0 ||
		!data.NextRefreshAfter.IsZero()
}

func authDataWithDefaults(data pluginapi.AuthData, auth *coreauth.Auth) pluginapi.AuthData {
	if auth == nil {
		return data
	}
	if strings.TrimSpace(data.Provider) == "" {
		data.Provider = auth.Provider
	}
	if strings.TrimSpace(data.ID) == "" {
		data.ID = auth.ID
	}
	if strings.TrimSpace(data.FileName) == "" {
		data.FileName = auth.FileName
	}
	if strings.TrimSpace(data.Label) == "" {
		data.Label = auth.Label
	}
	if strings.TrimSpace(data.Prefix) == "" {
		data.Prefix = auth.Prefix
	}
	if strings.TrimSpace(data.ProxyURL) == "" {
		data.ProxyURL = auth.ProxyURL
	}
	if len(data.Metadata) == 0 {
		data.Metadata = cloneAnyMap(auth.Metadata)
	} else {
		metadata := cloneAnyMap(data.Metadata)
		for key, value := range auth.Metadata {
			if _, exists := metadata[key]; !exists {
				metadata[key] = value
			}
		}
		data.Metadata = metadata
	}
	if len(data.Attributes) == 0 {
		data.Attributes = cloneStringMap(auth.Attributes)
	} else {
		attributes := cloneStringMap(data.Attributes)
		for key, value := range auth.Attributes {
			if _, exists := attributes[key]; !exists {
				attributes[key] = value
			}
		}
		data.Attributes = attributes
	}
	if len(data.StorageJSON) == 0 {
		data.StorageJSON = storageJSONFromAuth(auth)
	}
	if data.NextRefreshAfter.IsZero() {
		data.NextRefreshAfter = auth.NextRefreshAfter
	}
	return data
}

type modelClientRegistration struct {
	clientID string
	provider string
	models   []*registry.ModelInfo
}

func (h *Host) callModelRegistrar(ctx context.Context, record capabilityRecord, registrar pluginapi.ModelRegistrar) (resp pluginapi.ModelRegistrationResponse, err error) {
	if h == nil || registrar == nil || h.isPluginFused(record.id) || !h.recordCurrent(record) {
		return pluginapi.ModelRegistrationResponse{}, nil
	}
	defer func() {
		if recovered := recover(); recovered != nil {
			h.fusePlugin(record.id, "ModelRegistrar.RegisterModels", recovered)
			resp = pluginapi.ModelRegistrationResponse{}
			err = fmt.Errorf("model registrar panic: %v", recovered)
		}
	}()
	return registrar.RegisterModels(ctx, pluginapi.ModelRegistrationRequest{
		Plugin:              record.meta,
		MultiProviderModels: true,
	})
}

func (h *Host) callModelProviderStaticModels(ctx context.Context, record capabilityRecord, provider pluginapi.ModelProvider) (resp pluginapi.ModelResponse, err error) {
	if h == nil || provider == nil || h.isPluginFused(record.id) || !h.recordCurrent(record) {
		return pluginapi.ModelResponse{}, nil
	}
	defer func() {
		if recovered := recover(); recovered != nil {
			h.fusePlugin(record.id, "ModelProvider.StaticModels", recovered)
			resp = pluginapi.ModelResponse{}
			err = fmt.Errorf("model provider panic: %v", recovered)
		}
	}()
	return provider.StaticModels(ctx, pluginapi.StaticModelRequest{
		Plugin:              record.meta,
		Host:                h.hostConfigSummary(),
		MultiProviderModels: true,
	})
}

func (h *Host) callModelsForAuth(ctx context.Context, record capabilityRecord, provider pluginapi.ModelProvider, auth *coreauth.Auth) (resp pluginapi.ModelResponse, err error) {
	if h == nil || provider == nil || auth == nil || h.isPluginFused(record.id) || !h.recordCurrent(record) {
		return pluginapi.ModelResponse{}, nil
	}
	defer func() {
		if recovered := recover(); recovered != nil {
			h.fusePlugin(record.id, "ModelProvider.ModelsForAuth", recovered)
			resp = pluginapi.ModelResponse{}
			err = fmt.Errorf("model provider per-auth models panic: %v", recovered)
		}
	}()
	return provider.ModelsForAuth(ctx, pluginapi.AuthModelRequest{
		Plugin:       record.meta,
		AuthID:       auth.ID,
		AuthProvider: auth.Provider,
		StorageJSON:  storageJSONFromAuth(auth),
		Metadata:     cloneAnyMap(auth.Metadata),
		Attributes:   cloneStringMap(auth.Attributes),
		Host:         h.hostConfigSummary(),
		HTTPClient:   h.newHTTPClient(auth),
	})
}