yao/llmprovider/models.go
Max 9eef569e4b feat(agent): enhance system configuration and connector management
- 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.
2026-05-01 10:31:58 +08:00

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