- Updated the system configuration to include new role-level defaults for Light, Vision, and Audio connectors. - Refactored the resolveSystemConnector function to prioritize per-agent overrides, improving connector resolution logic. - Enhanced LLMConnector integration across various components to streamline settings retrieval and capabilities management. - Improved error handling and logging for connector-related operations, ensuring better diagnostics and user feedback.
371 lines
12 KiB
Go
371 lines
12 KiB
Go
package llmprovider
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/yaoapp/gou/connector"
|
|
goullm "github.com/yaoapp/gou/llm"
|
|
)
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// GetModel — by connectorID
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// GetModel returns the runtime connector for a given connectorID.
|
|
// Lookup order:
|
|
// 1. connector.Select (already registered in runtime)
|
|
// 2. Model-level ID with ":" separator (e.g. "t123.openai:gpt-4o")
|
|
// 3. r.Get by store Key (works when connectorID == Key, e.g. builtin)
|
|
// 4. r.GetByConnectorID (linear scan by ConnectorID field, for dynamic providers)
|
|
func (r *Registry) GetModel(connectorID string) (connector.Connector, error) {
|
|
if conn, err := connector.Select(connectorID); err == nil {
|
|
return conn, nil
|
|
}
|
|
|
|
// Path 2: model-level ID "providerCID:modelID"
|
|
if parts := strings.SplitN(connectorID, ":", 2); len(parts) == 2 {
|
|
return r.getModelConnector(parts[0], parts[1])
|
|
}
|
|
|
|
// Path 3: try by Key (fast, works for builtin where Key == ConnectorID)
|
|
if p, err := r.Get(connectorID, true); err == nil {
|
|
if eerr := ensureConnector(p); eerr != nil {
|
|
return nil, fmt.Errorf("model %q ensure connector: %w", connectorID, eerr)
|
|
}
|
|
cid := p.ConnectorID
|
|
if cid == "" {
|
|
cid = connectorID
|
|
}
|
|
return connector.Select(cid)
|
|
}
|
|
|
|
// Path 4: reverse lookup by ConnectorID field (dynamic providers where Key != ConnectorID)
|
|
p, err := r.GetByConnectorID(connectorID, true)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("model %q not found", connectorID)
|
|
}
|
|
|
|
cid := p.ConnectorID
|
|
if cid == "" {
|
|
cid = connectorID
|
|
}
|
|
return connector.Select(cid)
|
|
}
|
|
|
|
// getModelConnector finds a provider by connectorID, locates the model, and
|
|
// ensures a per-model connector is registered in the runtime.
|
|
func (r *Registry) getModelConnector(providerCID, modelID string) (connector.Connector, error) {
|
|
p, err := r.GetByConnectorID(providerCID, true)
|
|
if err != nil {
|
|
if p2, err2 := r.Get(providerCID, true); err2 == nil {
|
|
p = p2
|
|
} else {
|
|
return nil, fmt.Errorf("provider %q not found for model %q", providerCID, modelID)
|
|
}
|
|
}
|
|
|
|
var model *ModelInfo
|
|
for i, m := range p.Models {
|
|
if m.ID == modelID {
|
|
model = &p.Models[i]
|
|
break
|
|
}
|
|
}
|
|
if model == nil {
|
|
return nil, fmt.Errorf("model %q not found in provider %q", modelID, providerCID)
|
|
}
|
|
|
|
if err := ensureModelConnector(p, model); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
cid := providerCID + ":" + modelID
|
|
return connector.Select(cid)
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// GetRoleModel — role → connector
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// GetRoleModel returns the connector for a role at system scope.
|
|
func (r *Registry) GetRoleModel(role string) (connector.Connector, error) {
|
|
cid, err := r.GetRole(role)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return r.GetModel(cid)
|
|
}
|
|
|
|
// GetRoleModelByUser returns the connector for a role, merged user > system.
|
|
func (r *Registry) GetRoleModelByUser(role, userID string) (connector.Connector, error) {
|
|
cid, err := r.GetRoleByUser(role, userID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return r.GetModel(cid)
|
|
}
|
|
|
|
// GetRoleModelByTeam returns the connector for a role, merged team > system.
|
|
func (r *Registry) GetRoleModelByTeam(role, teamID string) (connector.Connector, error) {
|
|
cid, err := r.GetRoleByTeam(role, teamID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return r.GetModel(cid)
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Built-in role shortcuts
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func (r *Registry) GetDefaultModel() (connector.Connector, error) { return r.GetRoleModel("default") }
|
|
func (r *Registry) GetDefaultModelByUser(userID string) (connector.Connector, error) {
|
|
return r.GetRoleModelByUser("default", userID)
|
|
}
|
|
func (r *Registry) GetDefaultModelByTeam(teamID string) (connector.Connector, error) {
|
|
return r.GetRoleModelByTeam("default", teamID)
|
|
}
|
|
func (r *Registry) GetVisionModel() (connector.Connector, error) { return r.GetRoleModel("vision") }
|
|
func (r *Registry) GetVisionModelByUser(userID string) (connector.Connector, error) {
|
|
return r.GetRoleModelByUser("vision", userID)
|
|
}
|
|
func (r *Registry) GetVisionModelByTeam(teamID string) (connector.Connector, error) {
|
|
return r.GetRoleModelByTeam("vision", teamID)
|
|
}
|
|
func (r *Registry) GetAudioModel() (connector.Connector, error) { return r.GetRoleModel("audio") }
|
|
func (r *Registry) GetAudioModelByUser(userID string) (connector.Connector, error) {
|
|
return r.GetRoleModelByUser("audio", userID)
|
|
}
|
|
func (r *Registry) GetAudioModelByTeam(teamID string) (connector.Connector, error) {
|
|
return r.GetRoleModelByTeam("audio", teamID)
|
|
}
|
|
func (r *Registry) GetEmbeddingModel() (connector.Connector, error) {
|
|
return r.GetRoleModel("embedding")
|
|
}
|
|
func (r *Registry) GetEmbeddingModelByUser(userID string) (connector.Connector, error) {
|
|
return r.GetRoleModelByUser("embedding", userID)
|
|
}
|
|
func (r *Registry) GetEmbeddingModelByTeam(teamID string) (connector.Connector, error) {
|
|
return r.GetRoleModelByTeam("embedding", teamID)
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Capabilities
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// GetCapabilities returns capabilities for a connector by connectorID.
|
|
func (r *Registry) GetCapabilities(connectorID string) (*goullm.Capabilities, error) {
|
|
conn, err := r.GetModel(connectorID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return capabilitiesFromConn(conn), nil
|
|
}
|
|
|
|
// GetRoleCapabilities returns capabilities for a role at system scope.
|
|
func (r *Registry) GetRoleCapabilities(role string) (*goullm.Capabilities, error) {
|
|
conn, err := r.GetRoleModel(role)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return capabilitiesFromConn(conn), nil
|
|
}
|
|
|
|
// GetRoleCapabilitiesByUser returns capabilities for a role, merged user > system.
|
|
func (r *Registry) GetRoleCapabilitiesByUser(role, userID string) (*goullm.Capabilities, error) {
|
|
conn, err := r.GetRoleModelByUser(role, userID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return capabilitiesFromConn(conn), nil
|
|
}
|
|
|
|
// GetRoleCapabilitiesByTeam returns capabilities for a role, merged team > system.
|
|
func (r *Registry) GetRoleCapabilitiesByTeam(role, teamID string) (*goullm.Capabilities, error) {
|
|
conn, err := r.GetRoleModelByTeam(role, teamID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return capabilitiesFromConn(conn), nil
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// ListModels
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// ListModels returns all enabled models as []connector.Option (system scope, no owner filter).
|
|
func (r *Registry) ListModels() []connector.Option {
|
|
return r.listModels(nil)
|
|
}
|
|
|
|
// ListModelsByUser returns builtin + user-owned dynamic models.
|
|
func (r *Registry) ListModelsByUser(userID string) []connector.Option {
|
|
return r.listModels(&ProviderOwner{Type: "user", UserID: userID})
|
|
}
|
|
|
|
// ListModelsByTeam returns builtin + team-owned dynamic models.
|
|
func (r *Registry) ListModelsByTeam(teamID string) []connector.Option {
|
|
return r.listModels(&ProviderOwner{Type: "team", TeamID: teamID})
|
|
}
|
|
|
|
// ListModelsBy returns models scoped to the caller's identity (team > user).
|
|
func (r *Registry) ListModelsBy(id Identity) []connector.Option {
|
|
if id.GetTeamID() != "" {
|
|
return r.ListModelsByTeam(id.GetTeamID())
|
|
}
|
|
return r.ListModelsByUser(id.GetUserID())
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// By — Identity-scoped convenience methods
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// GetRoleBy returns the connectorID for a role, scoped by identity.
|
|
func (r *Registry) GetRoleModelBy(role string, id Identity) (connector.Connector, error) {
|
|
if id.GetTeamID() != "" {
|
|
return r.GetRoleModelByTeam(role, id.GetTeamID())
|
|
}
|
|
return r.GetRoleModelByUser(role, id.GetUserID())
|
|
}
|
|
|
|
func (r *Registry) GetDefaultModelBy(id Identity) (connector.Connector, error) {
|
|
return r.GetRoleModelBy("default", id)
|
|
}
|
|
func (r *Registry) GetVisionModelBy(id Identity) (connector.Connector, error) {
|
|
return r.GetRoleModelBy("vision", id)
|
|
}
|
|
func (r *Registry) GetAudioModelBy(id Identity) (connector.Connector, error) {
|
|
return r.GetRoleModelBy("audio", id)
|
|
}
|
|
func (r *Registry) GetEmbeddingModelBy(id Identity) (connector.Connector, error) {
|
|
return r.GetRoleModelBy("embedding", id)
|
|
}
|
|
|
|
func (r *Registry) GetRoleCapabilitiesBy(role string, id Identity) (*goullm.Capabilities, error) {
|
|
conn, err := r.GetRoleModelBy(role, id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return capabilitiesFromConn(conn), nil
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// internal
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// listModels returns enabled models. When owner is non-nil, returns builtin
|
|
// providers plus dynamic providers belonging to that owner.
|
|
// Builtin providers are one-connector-per-model; dynamic providers are expanded
|
|
// to per-model options here.
|
|
func (r *Registry) listModels(owner *ProviderOwner) []connector.Option {
|
|
enabled := true
|
|
providers, err := r.List(&ProviderFilter{
|
|
Source: ProviderSourceAll,
|
|
Enabled: &enabled,
|
|
}, true)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
|
|
var result []connector.Option
|
|
for _, p := range providers {
|
|
if owner != nil && p.Source == ProviderSourceDynamic {
|
|
if !ownerMatch(&p.Owner, owner) {
|
|
continue
|
|
}
|
|
}
|
|
|
|
if p.Source == ProviderSourceDynamic && len(p.Models) > 0 {
|
|
for _, m := range p.Models {
|
|
if !m.Enabled {
|
|
continue
|
|
}
|
|
_ = ensureModelConnector(&p, &m)
|
|
cid := p.ConnectorID + ":" + m.ID
|
|
label := p.Name + " / " + m.Name
|
|
if m.Name == "" {
|
|
label = p.Name + " / " + m.ID
|
|
}
|
|
result = append(result, connector.Option{
|
|
Label: label,
|
|
Value: cid,
|
|
})
|
|
}
|
|
} else {
|
|
result = append(result, connector.Option{
|
|
Label: p.Name,
|
|
Value: p.ConnectorID,
|
|
})
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
// ownerMatch returns true if the provider owner matches the requested scope.
|
|
func ownerMatch(po, want *ProviderOwner) bool {
|
|
if want.Type == "team" {
|
|
return po.Type == "team" && po.TeamID == want.TeamID
|
|
}
|
|
return po.Type == "user" && po.UserID == want.UserID
|
|
}
|
|
|
|
// capabilitiesFromConn extracts *llm.Capabilities from a connector.
|
|
// Prefers LLMConnector.GetCapabilities() when available.
|
|
func capabilitiesFromConn(conn connector.Connector) *goullm.Capabilities {
|
|
if conn == nil {
|
|
return defaultCaps()
|
|
}
|
|
|
|
if lc, ok := conn.(goullm.LLMConnector); ok {
|
|
if caps := lc.GetCapabilities(); caps != nil {
|
|
return caps
|
|
}
|
|
}
|
|
|
|
settings := conn.Setting()
|
|
if settings != nil {
|
|
if caps, ok := settings["capabilities"]; ok {
|
|
if c, ok := caps.(*goullm.Capabilities); ok {
|
|
return c
|
|
}
|
|
if c, ok := caps.(goullm.Capabilities); ok {
|
|
return &c
|
|
}
|
|
}
|
|
}
|
|
return defaultCaps()
|
|
}
|
|
|
|
// capsToMap converts Capabilities to map[string]interface{} for process handlers.
|
|
func capsToMap(caps *goullm.Capabilities) map[string]interface{} {
|
|
if caps == nil {
|
|
return nil
|
|
}
|
|
result := make(map[string]interface{})
|
|
if caps.Vision != nil {
|
|
result["vision"] = caps.Vision
|
|
}
|
|
result["audio"] = caps.Audio
|
|
result["stt"] = caps.STT
|
|
result["tool_calls"] = caps.ToolCalls
|
|
result["reasoning"] = caps.Reasoning
|
|
result["streaming"] = caps.Streaming
|
|
result["json"] = caps.JSON
|
|
result["multimodal"] = caps.Multimodal
|
|
result["temperature_adjustable"] = caps.TemperatureAdjustable
|
|
return result
|
|
}
|
|
|
|
func defaultCaps() *goullm.Capabilities {
|
|
return &goullm.Capabilities{
|
|
Vision: false,
|
|
ToolCalls: false,
|
|
Audio: false,
|
|
Reasoning: false,
|
|
Streaming: false,
|
|
JSON: false,
|
|
Multimodal: false,
|
|
TemperatureAdjustable: true,
|
|
}
|
|
}
|