yao/openapi/llm/llm.go
Max 6f78f6066b feat(llm): refactor audio capabilities and enhance role management
- 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.
2026-04-30 12:33:05 +08:00

177 lines
5.1 KiB
Go

package llm
import (
"fmt"
"strings"
"github.com/gin-gonic/gin"
"github.com/yaoapp/gou/connector"
agentllm "github.com/yaoapp/yao/agent/llm"
"github.com/yaoapp/yao/llmprovider"
"github.com/yaoapp/yao/openapi/oauth/authorized"
oauthTypes "github.com/yaoapp/yao/openapi/oauth/types"
"github.com/yaoapp/yao/openapi/response"
)
// Provider represents an LLM provider option
type Provider struct {
Label string `json:"label"`
Value string `json:"value"`
Type string `json:"type"` // "openai"
Builtin bool `json:"builtin"` // true for system built-in, false for user-defined
Capabilities map[string]interface{} `json:"capabilities"` // Model capabilities from connector settings
}
// Attach attaches the LLM management handlers to the router with OAuth protection
func Attach(group *gin.RouterGroup, oauth oauthTypes.OAuth) {
// Create providers group with OAuth guard
group.Use(oauth.Guard)
// LLM Providers endpoints
group.GET("/providers", listProviders) // GET /providers - List all LLM providers
}
// listProviders lists all available LLM providers (built-in + user-defined)
// Supports filtering by capabilities using query parameter: ?filters=vision,tool_calls,audio
func listProviders(c *gin.Context) {
allProviders := make([]Provider, 0)
filtersParam := c.Query("filters")
var filters []string
if filtersParam != "" {
filters = strings.Split(filtersParam, ",")
for i, filter := range filters {
filters[i] = strings.TrimSpace(strings.ToLower(filter))
}
}
fmt.Printf("[llm/providers] filtersParam=%q\n", filtersParam)
info := authorized.GetInfo(c)
fmt.Printf("[llm/providers] identity: UserID=%q TeamID=%q\n", info.GetUserID(), info.GetTeamID())
var opts []connector.Option
if llmprovider.Global != nil {
opts = llmprovider.Global.ListModelsBy(info)
} else {
opts = connector.AIConnectors
}
fmt.Printf("[llm/providers] ListModelsBy returned %d options\n", len(opts))
for i, o := range opts {
fmt.Printf("[llm/providers] [%d] label=%q value=%q\n", i, o.Label, o.Value)
}
for _, opt := range opts {
var conn connector.Connector
var err error
if llmprovider.Global != nil {
conn, err = llmprovider.Global.GetModel(opt.Value)
} else {
conn, err = connector.Select(opt.Value)
}
if err != nil {
fmt.Printf("[llm/providers] GetModel(%q) FAILED: %v\n", opt.Value, err)
continue
}
connType := connectorType(conn)
if connType != "openai" && connType != "anthropic" {
fmt.Printf("[llm/providers] SKIP %q: type=%q (not openai/anthropic)\n", opt.Value, connType)
continue
}
capabilities := getCapabilitiesFromConn(conn)
if len(filters) > 0 && !matchesFilters(capabilities, filters) {
fmt.Printf("[llm/providers] SKIP %q: caps filter %v not matched (streaming=%v)\n", opt.Value, filters, capabilities["streaming"])
continue
}
allProviders = append(allProviders, Provider{
Label: opt.Label,
Value: opt.Value,
Type: connType,
Builtin: conn.GetMetaInfo().Builtin,
Capabilities: capabilities,
})
}
fmt.Printf("[llm/providers] returning %d providers\n", len(allProviders))
response.RespondWithSuccess(c, response.StatusOK, allProviders)
}
// connectorType returns the type string for a connector.
func connectorType(conn connector.Connector) string {
if conn.Is(connector.OPENAI) {
return "openai"
}
if conn.Is(connector.ANTHROPIC) {
return "anthropic"
}
return "unknown"
}
// getCapabilitiesFromConn extracts capabilities from connector settings
func getCapabilitiesFromConn(conn connector.Connector) map[string]interface{} {
if conn == nil {
return nil
}
caps := agentllm.GetCapabilitiesFromConn(conn)
return agentllm.ToMap(caps)
}
// matchesFilters checks if capabilities match all requested filters
// Filters are matched case-insensitively and support the following capability keys:
// - vision: true or string value like "openai", "claude"
// - audio: bool (LLM supports audio input/understanding)
// - stt: bool (Speech-to-Text / audio transcription model, e.g. Whisper)
// - tool_calls: bool
// - reasoning: bool
// - streaming: bool
// - json: bool
// - multimodal: bool
// - temperature_adjustable: bool
func matchesFilters(capabilities map[string]interface{}, filters []string) bool {
if capabilities == nil {
return false
}
// All filters must match (AND logic)
for _, filter := range filters {
matched := false
// Check each capability field
for key, value := range capabilities {
keyLower := strings.ToLower(key)
// Match the filter against capability key
if keyLower == filter {
// For vision, check if it's true or a non-empty string
if filter == "vision" {
if boolVal, ok := value.(bool); ok && boolVal {
matched = true
break
}
if strVal, ok := value.(string); ok && strVal != "" {
matched = true
break
}
} else {
// For other capabilities, check if it's true
if boolVal, ok := value.(bool); ok && boolVal {
matched = true
break
}
}
}
}
// If any filter doesn't match, return false
if !matched {
return false
}
}
return true
}