- Replaced 'Voice' with 'Audio' in the system configuration and related tests to better reflect functionality. - Introduced new methods for role management in the llmprovider, allowing for dynamic retrieval of roles based on user and team context. - Updated the OpenAPI settings to support new role management endpoints and capabilities. - Enhanced the handling of API keys in provider management, allowing for optional plain-text retrieval.
294 lines
6.4 KiB
Go
294 lines
6.4 KiB
Go
package llmprovider
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
|
|
"github.com/yaoapp/gou/connector"
|
|
)
|
|
|
|
// connectorID builds the runtime ID for registering into connector.Connectors.
|
|
// Dynamic providers get an owner prefix to avoid collision with builtin IDs.
|
|
func connectorID(p *Provider) string {
|
|
switch p.Owner.Type {
|
|
case "user":
|
|
return "u" + p.Owner.UserID + "." + p.Key
|
|
case "team":
|
|
return "t" + p.Owner.TeamID + "." + p.Key
|
|
default:
|
|
return "s." + p.Key
|
|
}
|
|
}
|
|
|
|
// defaultModel returns the first enabled model ID, or empty string.
|
|
func defaultModel(p *Provider) string {
|
|
for _, m := range p.Models {
|
|
if m.Enabled {
|
|
return m.ID
|
|
}
|
|
}
|
|
if len(p.Models) > 0 {
|
|
return p.Models[0].ID
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// marshalDSL builds a connector DSL JSON from the flat Provider fields.
|
|
func marshalDSL(p *Provider) ([]byte, error) {
|
|
opts := map[string]interface{}{
|
|
"host": p.APIURL,
|
|
"key": p.APIKey,
|
|
"model": defaultModel(p),
|
|
}
|
|
|
|
if caps := aggregateCapabilities(p); len(caps) > 0 {
|
|
opts["capabilities"] = caps
|
|
}
|
|
|
|
dsl := map[string]interface{}{
|
|
"type": p.Type,
|
|
"name": p.Name,
|
|
"label": p.Name,
|
|
"options": opts,
|
|
}
|
|
return json.Marshal(dsl)
|
|
}
|
|
|
|
// aggregateCapabilities merges all model capabilities into a single map.
|
|
// Falls back to type-based defaults when no model declares explicit caps.
|
|
func aggregateCapabilities(p *Provider) map[string]bool {
|
|
caps := make(map[string]bool)
|
|
for _, m := range p.Models {
|
|
for _, c := range m.Capabilities {
|
|
caps[c] = true
|
|
}
|
|
}
|
|
if len(caps) == 0 {
|
|
switch p.Type {
|
|
case "openai", "anthropic":
|
|
caps["streaming"] = true
|
|
caps["tool_calls"] = true
|
|
caps["temperature_adjustable"] = true
|
|
}
|
|
}
|
|
return caps
|
|
}
|
|
|
|
// ensureConnector makes sure the provider's connector is registered in the runtime.
|
|
// Builtin providers are managed by engine.Load and skipped here.
|
|
func ensureConnector(p *Provider) error {
|
|
if p.Source == ProviderSourceBuiltIn {
|
|
return nil
|
|
}
|
|
if !p.Enabled {
|
|
return nil
|
|
}
|
|
|
|
cid := p.ConnectorID
|
|
if cid == "" {
|
|
cid = connectorID(p)
|
|
}
|
|
|
|
if _, err := connector.Select(cid); err == nil {
|
|
return nil
|
|
}
|
|
|
|
dslJSON, err := marshalDSL(p)
|
|
if err != nil {
|
|
return fmt.Errorf("ensureConnector %s: marshal DSL: %w", p.Key, err)
|
|
}
|
|
|
|
_, err = connector.LoadSourceSync(dslJSON, cid, "__registry/"+cid+".conn.yao")
|
|
if err != nil {
|
|
return fmt.Errorf("ensureConnector %s: LoadSourceSync: %w", p.Key, err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ensureModelConnector registers a per-model connector for a dynamic provider.
|
|
// The connector ID format is "{providerConnectorID}:{modelID}".
|
|
func ensureModelConnector(p *Provider, m *ModelInfo) error {
|
|
if p.Source == ProviderSourceBuiltIn {
|
|
return nil
|
|
}
|
|
if !p.Enabled || !m.Enabled {
|
|
return nil
|
|
}
|
|
|
|
baseCID := p.ConnectorID
|
|
if baseCID == "" {
|
|
baseCID = connectorID(p)
|
|
}
|
|
cid := baseCID + ":" + m.ID
|
|
|
|
if _, err := connector.Select(cid); err == nil {
|
|
return nil
|
|
}
|
|
|
|
dslJSON, err := marshalModelDSL(p, m)
|
|
if err != nil {
|
|
return fmt.Errorf("ensureModelConnector %s:%s: %w", p.Key, m.ID, err)
|
|
}
|
|
|
|
_, err = connector.LoadSourceSync(dslJSON, cid, "__registry/"+baseCID+"/"+m.ID+".conn.yao")
|
|
if err != nil {
|
|
return fmt.Errorf("ensureModelConnector %s:%s: LoadSourceSync: %w", p.Key, m.ID, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// marshalModelDSL builds a connector DSL for a specific model within a provider.
|
|
func marshalModelDSL(p *Provider, m *ModelInfo) ([]byte, error) {
|
|
caps := make(map[string]bool)
|
|
for _, c := range m.Capabilities {
|
|
caps[c] = true
|
|
}
|
|
if len(caps) == 0 {
|
|
switch p.Type {
|
|
case "openai", "anthropic":
|
|
caps["streaming"] = true
|
|
caps["tool_calls"] = true
|
|
caps["temperature_adjustable"] = true
|
|
}
|
|
}
|
|
|
|
opts := map[string]interface{}{
|
|
"host": p.APIURL,
|
|
"key": p.APIKey,
|
|
"model": m.ID,
|
|
}
|
|
if len(caps) > 0 {
|
|
opts["capabilities"] = caps
|
|
}
|
|
|
|
name := m.Name
|
|
if name == "" {
|
|
name = m.ID
|
|
}
|
|
dsl := map[string]interface{}{
|
|
"type": p.Type,
|
|
"name": name,
|
|
"label": name,
|
|
"options": opts,
|
|
}
|
|
return json.Marshal(dsl)
|
|
}
|
|
|
|
// unregisterConnector removes the provider's connector from the runtime.
|
|
func unregisterConnector(p *Provider) error {
|
|
if p.Source == ProviderSourceBuiltIn {
|
|
return nil
|
|
}
|
|
cid := p.ConnectorID
|
|
if cid == "" {
|
|
cid = connectorID(p)
|
|
}
|
|
return connector.Unregister(cid)
|
|
}
|
|
|
|
// importFromConnectors scans existing AI connectors loaded by engine.Load
|
|
// and imports them as builtin providers into the Registry store.
|
|
// If a store record with the same key already exists (dynamic), it is not overwritten.
|
|
func importFromConnectors(r *Registry) error {
|
|
for _, opt := range connector.AIConnectors {
|
|
id := opt.Value
|
|
if r.store.Has(storeKey(id)) {
|
|
continue
|
|
}
|
|
|
|
conn, err := connector.Select(id)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
p := providerFromConnector(id, conn)
|
|
m, err := providerToMap(&p, r.encKey)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
sk := storeKey(id)
|
|
_ = r.store.Set(sk, m, 0)
|
|
if r.cache != nil {
|
|
_ = r.cache.Set(sk, m, 0)
|
|
}
|
|
_ = indexAdd(r.store, r.cache, id)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// providerFromConnector builds a Provider from a runtime Connector interface.
|
|
func providerFromConnector(id string, conn connector.Connector) Provider {
|
|
meta := conn.GetMetaInfo()
|
|
setting := conn.Setting()
|
|
|
|
name := meta.Label
|
|
if name == "" {
|
|
name = id
|
|
}
|
|
|
|
typ := connectorType(conn)
|
|
apiURL, _ := setting["host"].(string)
|
|
apiKey, _ := setting["key"].(string)
|
|
model, _ := setting["model"].(string)
|
|
|
|
var models []ModelInfo
|
|
if model != "" {
|
|
caps := capabilitiesFromSetting(setting)
|
|
models = []ModelInfo{{
|
|
ID: model,
|
|
Name: model,
|
|
Capabilities: caps,
|
|
Enabled: true,
|
|
}}
|
|
}
|
|
|
|
return Provider{
|
|
Key: id,
|
|
ConnectorID: id,
|
|
Name: name,
|
|
Type: typ,
|
|
APIURL: apiURL,
|
|
APIKey: apiKey,
|
|
Models: models,
|
|
Enabled: true,
|
|
Status: "connected",
|
|
Source: ProviderSourceBuiltIn,
|
|
Owner: ProviderOwner{Type: "system"},
|
|
}
|
|
}
|
|
|
|
func connectorType(conn connector.Connector) string {
|
|
switch {
|
|
case conn.Is(6): // OPENAI
|
|
return "openai"
|
|
case conn.Is(11): // ANTHROPIC
|
|
return "anthropic"
|
|
case conn.Is(9): // FASTEMBED
|
|
return "fastembed"
|
|
case conn.Is(8): // MOAPI
|
|
return "moapi"
|
|
default:
|
|
return "custom"
|
|
}
|
|
}
|
|
|
|
func capabilitiesFromSetting(setting map[string]interface{}) []string {
|
|
raw, ok := setting["capabilities"]
|
|
if !ok {
|
|
return nil
|
|
}
|
|
|
|
switch caps := raw.(type) {
|
|
case map[string]interface{}:
|
|
var out []string
|
|
for k, v := range caps {
|
|
if b, ok := v.(bool); ok && b {
|
|
out = append(out, k)
|
|
}
|
|
}
|
|
return out
|
|
default:
|
|
return nil
|
|
}
|
|
}
|