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.
This commit is contained in:
parent
aa9632d67c
commit
9eef569e4b
41 changed files with 1443 additions and 743 deletions
|
|
@ -2,7 +2,6 @@ package assistant
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
jsoniter "github.com/json-iterator/go"
|
jsoniter "github.com/json-iterator/go"
|
||||||
|
|
@ -628,50 +627,40 @@ func (ast *Assistant) Stream(ctx *context.Context, inputMessages []context.Messa
|
||||||
return finalResponse, nil
|
return finalResponse, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetConnector get the connector object, capabilities, and error with priority:
|
// GetConnector get the connector object, capabilities, and error.
|
||||||
// opts.Connector > ast.Connector > GetRoleBy("default", identity) > GetRole("default") > error
|
// Priority: opts.Connector > ast.Connector (may be "use::<role>") > "default" role > legacy fallback
|
||||||
// Note: opts.Connector may be set by Create hook's applyOptionsAdjustments
|
// Note: opts.Connector may be set by Create hook's applyOptionsAdjustments
|
||||||
// Returns: (connector, capabilities, error)
|
|
||||||
func (ast *Assistant) GetConnector(ctx *context.Context, opts ...*context.Options) (connector.Connector, *goullm.Capabilities, error) {
|
func (ast *Assistant) GetConnector(ctx *context.Context, opts ...*context.Options) (connector.Connector, *goullm.Capabilities, error) {
|
||||||
connectorID := ast.Connector
|
cid := ast.Connector
|
||||||
if len(opts) > 0 && opts[0] != nil && opts[0].Connector != "" {
|
if len(opts) > 0 && opts[0] != nil && opts[0].Connector != "" {
|
||||||
connectorID = opts[0].Connector
|
cid = opts[0].Connector
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback to unified role resolution via llmprovider (team > user > system)
|
// Extract identity for role-based resolution
|
||||||
if connectorID == "" && llmprovider.Global != nil {
|
var identity llmprovider.Identity
|
||||||
if ctx != nil && ctx.Authorized != nil {
|
if ctx != nil && ctx.Authorized != nil {
|
||||||
if cid, err := llmprovider.Global.GetRoleBy("default", ctx.Authorized); err == nil {
|
identity = ctx.Authorized
|
||||||
connectorID = cid
|
}
|
||||||
}
|
|
||||||
}
|
// Unified resolution: explicit connector / use:: prefix / empty → all handled
|
||||||
if connectorID == "" {
|
conn, caps, err := llm.ResolveConnector(cid, identity)
|
||||||
if cid, err := llmprovider.Global.GetRole("default"); err == nil {
|
if err == nil {
|
||||||
connectorID = cid
|
return conn, caps, nil
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Legacy fallback
|
// Legacy fallback
|
||||||
if connectorID == "" {
|
if defaultConnector != "" {
|
||||||
connectorID = defaultConnector
|
if conn, err := connector.Select(defaultConnector); err == nil {
|
||||||
|
return conn, llm.GetCapabilitiesFromConn(conn), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if fallback := findCapableConnector(); fallback != "" {
|
||||||
|
if conn, err := connector.Select(fallback); err == nil {
|
||||||
|
return conn, llm.GetCapabilitiesFromConn(conn), nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if connectorID == "" {
|
return nil, nil, fmt.Errorf("connector not specified")
|
||||||
return nil, nil, fmt.Errorf("connector not specified")
|
|
||||||
}
|
|
||||||
|
|
||||||
conn, err := connector.Select(connectorID)
|
|
||||||
if err != nil && connectorID != defaultConnector && defaultConnector != "" {
|
|
||||||
log.Printf("[Assistant] connector %q not found, falling back to default %q", connectorID, defaultConnector)
|
|
||||||
conn, err = connector.Select(defaultConnector)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
capabilities := llm.GetCapabilitiesFromConn(conn)
|
|
||||||
return conn, capabilities, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Info get the assistant information
|
// Info get the assistant information
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@ import (
|
||||||
"github.com/yaoapp/yao/agent/i18n"
|
"github.com/yaoapp/yao/agent/i18n"
|
||||||
store "github.com/yaoapp/yao/agent/store/types"
|
store "github.com/yaoapp/yao/agent/store/types"
|
||||||
"github.com/yaoapp/yao/data"
|
"github.com/yaoapp/yao/data"
|
||||||
"github.com/yaoapp/yao/llmprovider"
|
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -35,7 +34,13 @@ var systemAgents = []string{
|
||||||
// SystemConfig holds the system agents connector configuration
|
// SystemConfig holds the system agents connector configuration
|
||||||
// This is set from agent.yml system block
|
// This is set from agent.yml system block
|
||||||
type SystemConfig struct {
|
type SystemConfig struct {
|
||||||
Default string // Default connector for all system agents
|
// Role-level defaults (consumed by buildSystemRoles → SetDefaults)
|
||||||
|
Default string // Default connector for the "default" role
|
||||||
|
Light string // Default connector for the "light" role
|
||||||
|
Vision string // Default connector for the "vision" role
|
||||||
|
Audio string // Default connector for the "audio" role
|
||||||
|
|
||||||
|
// Per-agent overrides (consumed by resolveSystemConnector → ast.Connector)
|
||||||
Keyword string // Connector for __yao.keyword agent
|
Keyword string // Connector for __yao.keyword agent
|
||||||
QueryDSL string // Connector for __yao.querydsl agent
|
QueryDSL string // Connector for __yao.querydsl agent
|
||||||
Title string // Connector for __yao.title agent
|
Title string // Connector for __yao.title agent
|
||||||
|
|
@ -43,8 +48,6 @@ type SystemConfig struct {
|
||||||
RobotPrompt string // Connector for __yao.robot_prompt agent
|
RobotPrompt string // Connector for __yao.robot_prompt agent
|
||||||
NeedSearch string // Connector for __yao.needsearch agent
|
NeedSearch string // Connector for __yao.needsearch agent
|
||||||
Entity string // Connector for __yao.entity agent
|
Entity string // Connector for __yao.entity agent
|
||||||
Vision string // Connector for vision capabilities
|
|
||||||
Audio string // Connector for audio/STT capabilities
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// systemConfig holds the system agents configuration (global variable like others in load.go)
|
// systemConfig holds the system agents configuration (global variable like others in load.go)
|
||||||
|
|
@ -161,10 +164,9 @@ func loadSystemAgent(id, pathPrefix string) (*Assistant, error) {
|
||||||
pkgData["type"] = "assistant"
|
pkgData["type"] = "assistant"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolve connector for this system agent
|
// Override connector only if agent.yml has an explicit per-agent setting
|
||||||
connectorID := resolveSystemConnector(id)
|
if override := resolveSystemConnector(id); override != "" {
|
||||||
if connectorID != "" {
|
pkgData["connector"] = override
|
||||||
pkgData["connector"] = connectorID
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read prompts.yml from bindata (default prompts)
|
// Read prompts.yml from bindata (default prompts)
|
||||||
|
|
@ -208,118 +210,34 @@ func loadSystemAgent(id, pathPrefix string) (*Assistant, error) {
|
||||||
return loadMap(pkgData)
|
return loadMap(pkgData)
|
||||||
}
|
}
|
||||||
|
|
||||||
// resolveSystemConnector resolves the connector for a system agent
|
// resolveSystemConnector returns an explicit per-agent connector override from agent.yml.
|
||||||
// Priority: specific agent config > system.default > llmprovider role > defaultConnector > fallback
|
// Returns empty string if no override exists, so the connector declared in package.yao
|
||||||
|
// (e.g. "use::light") is preserved as-is.
|
||||||
func resolveSystemConnector(agentID string) string {
|
func resolveSystemConnector(agentID string) string {
|
||||||
// Try specific agent config first
|
if systemConfig == nil {
|
||||||
if systemConfig != nil {
|
return ""
|
||||||
switch agentID {
|
|
||||||
case "__yao.keyword":
|
|
||||||
if systemConfig.Keyword != "" {
|
|
||||||
return systemConfig.Keyword
|
|
||||||
}
|
|
||||||
case "__yao.querydsl":
|
|
||||||
if systemConfig.QueryDSL != "" {
|
|
||||||
return systemConfig.QueryDSL
|
|
||||||
}
|
|
||||||
case "__yao.title":
|
|
||||||
if systemConfig.Title != "" {
|
|
||||||
return systemConfig.Title
|
|
||||||
}
|
|
||||||
case "__yao.prompt":
|
|
||||||
if systemConfig.Prompt != "" {
|
|
||||||
return systemConfig.Prompt
|
|
||||||
}
|
|
||||||
case "__yao.robot_prompt":
|
|
||||||
if systemConfig.RobotPrompt != "" {
|
|
||||||
return systemConfig.RobotPrompt
|
|
||||||
}
|
|
||||||
case "__yao.needsearch":
|
|
||||||
if systemConfig.NeedSearch != "" {
|
|
||||||
return systemConfig.NeedSearch
|
|
||||||
}
|
|
||||||
case "__yao.entity":
|
|
||||||
if systemConfig.Entity != "" {
|
|
||||||
return systemConfig.Entity
|
|
||||||
}
|
|
||||||
case "__yao.vision":
|
|
||||||
if systemConfig.Vision != "" {
|
|
||||||
return systemConfig.Vision
|
|
||||||
}
|
|
||||||
case "__yao.audio":
|
|
||||||
if systemConfig.Audio != "" {
|
|
||||||
return systemConfig.Audio
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try system default
|
|
||||||
if systemConfig.Default != "" {
|
|
||||||
return systemConfig.Default
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
switch agentID {
|
||||||
// Try unified role resolution: strip __yao. prefix as role name
|
case "__yao.keyword":
|
||||||
if llmprovider.Global != nil {
|
return systemConfig.Keyword
|
||||||
role := strings.TrimPrefix(agentID, "__yao.")
|
case "__yao.querydsl":
|
||||||
if cid, err := llmprovider.Global.GetRole(role); err == nil && cid != "" {
|
return systemConfig.QueryDSL
|
||||||
return cid
|
case "__yao.title":
|
||||||
}
|
return systemConfig.Title
|
||||||
if cid, err := llmprovider.Global.GetRole("default"); err == nil && cid != "" {
|
case "__yao.prompt":
|
||||||
return cid
|
return systemConfig.Prompt
|
||||||
}
|
case "__yao.robot_prompt":
|
||||||
|
return systemConfig.RobotPrompt
|
||||||
|
case "__yao.needsearch":
|
||||||
|
return systemConfig.NeedSearch
|
||||||
|
case "__yao.entity":
|
||||||
|
return systemConfig.Entity
|
||||||
|
case "__yao.vision":
|
||||||
|
return systemConfig.Vision
|
||||||
|
case "__yao.audio":
|
||||||
|
return systemConfig.Audio
|
||||||
}
|
}
|
||||||
|
return ""
|
||||||
// Try global default connector
|
|
||||||
if defaultConnector != "" {
|
|
||||||
return defaultConnector
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback: find first connector that supports tool calling
|
|
||||||
return findCapableConnector()
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetVisionConnector returns the connector for vision capabilities.
|
|
||||||
// Priority: system.vision > system.default > llmprovider GetRole("vision") > defaultConnector > findCapableConnector
|
|
||||||
func GetVisionConnector() string {
|
|
||||||
if systemConfig != nil {
|
|
||||||
if systemConfig.Vision != "" {
|
|
||||||
return systemConfig.Vision
|
|
||||||
}
|
|
||||||
if systemConfig.Default != "" {
|
|
||||||
return systemConfig.Default
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if llmprovider.Global != nil {
|
|
||||||
if cid, err := llmprovider.Global.GetRole("vision"); err == nil && cid != "" {
|
|
||||||
return cid
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if defaultConnector != "" {
|
|
||||||
return defaultConnector
|
|
||||||
}
|
|
||||||
return findCapableConnector()
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAudioConnector returns the connector for audio/STT capabilities.
|
|
||||||
// Priority: system.audio > system.default > llmprovider GetRole("audio") > defaultConnector > findCapableConnector
|
|
||||||
func GetAudioConnector() string {
|
|
||||||
if systemConfig != nil {
|
|
||||||
if systemConfig.Audio != "" {
|
|
||||||
return systemConfig.Audio
|
|
||||||
}
|
|
||||||
if systemConfig.Default != "" {
|
|
||||||
return systemConfig.Default
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if llmprovider.Global != nil {
|
|
||||||
if cid, err := llmprovider.Global.GetRole("audio"); err == nil && cid != "" {
|
|
||||||
return cid
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if defaultConnector != "" {
|
|
||||||
return defaultConnector
|
|
||||||
}
|
|
||||||
return findCapableConnector()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// findCapableConnector finds the first connector that supports tool calling
|
// findCapableConnector finds the first connector that supports tool calling
|
||||||
|
|
|
||||||
58
agent/assistant/load_system_test.go
Normal file
58
agent/assistant/load_system_test.go
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
package assistant
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestResolveSystemConnector_NoConfig(t *testing.T) {
|
||||||
|
saved := systemConfig
|
||||||
|
systemConfig = nil
|
||||||
|
defer func() { systemConfig = saved }()
|
||||||
|
|
||||||
|
assert.Equal(t, "", resolveSystemConnector("__yao.title"))
|
||||||
|
assert.Equal(t, "", resolveSystemConnector("__yao.keyword"))
|
||||||
|
assert.Equal(t, "", resolveSystemConnector("__yao.querydsl"))
|
||||||
|
assert.Equal(t, "", resolveSystemConnector("__yao.vision"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveSystemConnector_PerAgentOverride(t *testing.T) {
|
||||||
|
saved := systemConfig
|
||||||
|
systemConfig = &SystemConfig{
|
||||||
|
Title: "openai.gpt-4o",
|
||||||
|
}
|
||||||
|
defer func() { systemConfig = saved }()
|
||||||
|
|
||||||
|
assert.Equal(t, "openai.gpt-4o", resolveSystemConnector("__yao.title"))
|
||||||
|
assert.Equal(t, "", resolveSystemConnector("__yao.keyword"))
|
||||||
|
assert.Equal(t, "", resolveSystemConnector("__yao.querydsl"))
|
||||||
|
assert.Equal(t, "", resolveSystemConnector("__yao.vision"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveSystemConnector_RoleLevelOnly(t *testing.T) {
|
||||||
|
saved := systemConfig
|
||||||
|
systemConfig = &SystemConfig{
|
||||||
|
Default: "openai.gpt-4o",
|
||||||
|
Light: "openai.gpt-4o-mini",
|
||||||
|
}
|
||||||
|
defer func() { systemConfig = saved }()
|
||||||
|
|
||||||
|
// Role-level keys don't produce per-agent overrides
|
||||||
|
assert.Equal(t, "", resolveSystemConnector("__yao.title"))
|
||||||
|
assert.Equal(t, "", resolveSystemConnector("__yao.keyword"))
|
||||||
|
assert.Equal(t, "", resolveSystemConnector("__yao.querydsl"))
|
||||||
|
assert.Equal(t, "", resolveSystemConnector("__yao.vision"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveSystemConnector_UnknownAgent(t *testing.T) {
|
||||||
|
saved := systemConfig
|
||||||
|
systemConfig = &SystemConfig{
|
||||||
|
Default: "openai.gpt-4o",
|
||||||
|
Title: "openai.gpt-4o",
|
||||||
|
}
|
||||||
|
defer func() { systemConfig = saved }()
|
||||||
|
|
||||||
|
assert.Equal(t, "", resolveSystemConnector("__yao.nonexistent"))
|
||||||
|
assert.Equal(t, "", resolveSystemConnector("custom.agent"))
|
||||||
|
}
|
||||||
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/yaoapp/gou/connector"
|
"github.com/yaoapp/gou/connector"
|
||||||
|
goullm "github.com/yaoapp/gou/llm"
|
||||||
gouMCP "github.com/yaoapp/gou/mcp"
|
gouMCP "github.com/yaoapp/gou/mcp"
|
||||||
mcpProcess "github.com/yaoapp/gou/mcp/process"
|
mcpProcess "github.com/yaoapp/gou/mcp/process"
|
||||||
"github.com/yaoapp/yao/agent/context"
|
"github.com/yaoapp/yao/agent/context"
|
||||||
|
|
@ -266,30 +267,26 @@ func (ast *Assistant) buildSandboxOptions(ctx *context.Context, opts *context.Op
|
||||||
execOpts.ConnectorType = "openai"
|
execOpts.ConnectorType = "openai"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Extract standard fields via LLMConnector when available, fallback to Setting()
|
||||||
setting := conn.Setting()
|
setting := conn.Setting()
|
||||||
if host, ok := setting["host"].(string); ok {
|
if lc, ok := conn.(goullm.LLMConnector); ok {
|
||||||
execOpts.ConnectorHost = host
|
execOpts.ConnectorHost = lc.GetURL()
|
||||||
}
|
execOpts.ConnectorKey = lc.GetKey()
|
||||||
if key, ok := setting["key"].(string); ok {
|
execOpts.Model = lc.GetModel()
|
||||||
execOpts.ConnectorKey = key
|
} else {
|
||||||
}
|
if host, ok := setting["host"].(string); ok {
|
||||||
if model, ok := setting["model"].(string); ok {
|
execOpts.ConnectorHost = host
|
||||||
execOpts.Model = model
|
}
|
||||||
}
|
if key, ok := setting["key"].(string); ok {
|
||||||
|
execOpts.ConnectorKey = key
|
||||||
// Extract extra connector options (thinking, max_tokens, temperature, etc.)
|
}
|
||||||
// These are backend-specific parameters that need to be passed through to the proxy
|
if model, ok := setting["model"].(string); ok {
|
||||||
connectorOptions := make(map[string]interface{})
|
execOpts.Model = model
|
||||||
for k, v := range setting {
|
|
||||||
// Skip standard fields that are already handled
|
|
||||||
switch k {
|
|
||||||
case "host", "key", "model", "azure", "capabilities":
|
|
||||||
continue
|
|
||||||
default:
|
|
||||||
// Include all other fields as extra options
|
|
||||||
connectorOptions[k] = v
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Whitelist-filter remaining settings for sandbox proxy options
|
||||||
|
connectorOptions := connector.FilterRequestBodyParams(setting, conn)
|
||||||
if len(connectorOptions) > 0 {
|
if len(connectorOptions) > 0 {
|
||||||
execOpts.ConnectorOptions = connectorOptions
|
execOpts.ConnectorOptions = connectorOptions
|
||||||
ctx.Logger.Debug("Connector options extracted: %v", connectorOptions)
|
ctx.Logger.Debug("Connector options extracted: %v", connectorOptions)
|
||||||
|
|
|
||||||
|
|
@ -20,12 +20,21 @@ func GetCapabilities(connectorID string) *goullm.Capabilities {
|
||||||
return GetCapabilitiesFromConn(conn)
|
return GetCapabilitiesFromConn(conn)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetCapabilitiesFromConn get the capabilities from a connector instance
|
// GetCapabilitiesFromConn get the capabilities from a connector instance.
|
||||||
|
// Prefers LLMConnector.GetCapabilities() when available, falls back to Setting() parsing.
|
||||||
func GetCapabilitiesFromConn(conn connector.Connector) *goullm.Capabilities {
|
func GetCapabilitiesFromConn(conn connector.Connector) *goullm.Capabilities {
|
||||||
if conn == nil {
|
if conn == nil {
|
||||||
return getDefaultCapabilities()
|
return getDefaultCapabilities()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Prefer typed LLMConnector interface
|
||||||
|
if lc, ok := conn.(goullm.LLMConnector); ok {
|
||||||
|
if caps := lc.GetCapabilities(); caps != nil {
|
||||||
|
return caps
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to Setting() parsing for non-LLMConnector or nil capabilities
|
||||||
settings := conn.Setting()
|
settings := conn.Setting()
|
||||||
if settings != nil {
|
if settings != nil {
|
||||||
if caps, ok := settings["capabilities"]; ok {
|
if caps, ok := settings["capabilities"]; ok {
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ import (
|
||||||
"github.com/yaoapp/yao/agent/llm/adapters"
|
"github.com/yaoapp/yao/agent/llm/adapters"
|
||||||
"github.com/yaoapp/yao/agent/llm/providers/base"
|
"github.com/yaoapp/yao/agent/llm/providers/base"
|
||||||
"github.com/yaoapp/yao/agent/output/message"
|
"github.com/yaoapp/yao/agent/output/message"
|
||||||
|
"github.com/yaoapp/yao/share"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Provider Anthropic Messages API provider
|
// Provider Anthropic Messages API provider
|
||||||
|
|
@ -201,21 +202,10 @@ func (p *Provider) streamWithRetry(ctx *context.Context, messages []context.Mess
|
||||||
return nil, fmt.Errorf("failed to build request body: %w", err)
|
return nil, fmt.Errorf("failed to build request body: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get connector settings
|
// Get connector settings via LLMConnector or fallback
|
||||||
setting := p.Connector.Setting()
|
host, key, version, err := p.resolveHostKeyVersion()
|
||||||
host, ok := setting["host"].(string)
|
if err != nil {
|
||||||
if !ok || host == "" {
|
return nil, err
|
||||||
return nil, fmt.Errorf("no host found in connector settings")
|
|
||||||
}
|
|
||||||
|
|
||||||
key, ok := setting["key"].(string)
|
|
||||||
if !ok || key == "" {
|
|
||||||
return nil, fmt.Errorf("API key is not set")
|
|
||||||
}
|
|
||||||
|
|
||||||
version := "2023-06-01"
|
|
||||||
if v, ok := setting["version"].(string); ok && v != "" {
|
|
||||||
version = v
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build URL: host/v1/messages
|
// Build URL: host/v1/messages
|
||||||
|
|
@ -227,13 +217,13 @@ func (p *Provider) streamWithRetry(ctx *context.Context, messages []context.Mess
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create HTTP request with Anthropic auth headers
|
// Create HTTP request with auth headers
|
||||||
req := http.New(url).
|
req := http.New(url).
|
||||||
SetHeader("Content-Type", "application/json").
|
SetHeader("Content-Type", "application/json").
|
||||||
SetHeader("x-api-key", key).
|
|
||||||
SetHeader("anthropic-version", version).
|
SetHeader("anthropic-version", version).
|
||||||
SetHeader("Accept", "text/event-stream").
|
SetHeader("Accept", "text/event-stream").
|
||||||
SetHeader("User-Agent", "YaoAgent/1.0 (+https://yaoagents.com)")
|
SetHeader("User-Agent", "YaoEngine/"+share.VERSION)
|
||||||
|
setAnthropicAuthHeaders(req, p.Connector, key)
|
||||||
|
|
||||||
// Accumulate response data
|
// Accumulate response data
|
||||||
accumulator := &streamAccumulator{
|
accumulator := &streamAccumulator{
|
||||||
|
|
@ -678,31 +668,20 @@ func (p *Provider) postWithRetry(ctx *context.Context, messages []context.Messag
|
||||||
return nil, fmt.Errorf("failed to build request body: %w", err)
|
return nil, fmt.Errorf("failed to build request body: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get connector settings
|
// Get connector settings via LLMConnector or fallback
|
||||||
setting := p.Connector.Setting()
|
host, key, version, err := p.resolveHostKeyVersion()
|
||||||
host, ok := setting["host"].(string)
|
if err != nil {
|
||||||
if !ok || host == "" {
|
return nil, err
|
||||||
return nil, fmt.Errorf("no host found in connector settings")
|
|
||||||
}
|
|
||||||
|
|
||||||
key, ok := setting["key"].(string)
|
|
||||||
if !ok || key == "" {
|
|
||||||
return nil, fmt.Errorf("API key is not set")
|
|
||||||
}
|
|
||||||
|
|
||||||
version := "2023-06-01"
|
|
||||||
if v, ok := setting["version"].(string); ok && v != "" {
|
|
||||||
version = v
|
|
||||||
}
|
}
|
||||||
|
|
||||||
url := buildAPIURL(host, "/messages")
|
url := buildAPIURL(host, "/messages")
|
||||||
|
|
||||||
// Create HTTP request
|
// Create HTTP request with auth headers
|
||||||
req := http.New(url).
|
req := http.New(url).
|
||||||
SetHeader("Content-Type", "application/json").
|
SetHeader("Content-Type", "application/json").
|
||||||
SetHeader("x-api-key", key).
|
|
||||||
SetHeader("anthropic-version", version).
|
SetHeader("anthropic-version", version).
|
||||||
SetHeader("User-Agent", "YaoAgent/1.0 (+https://yaoagents.com)")
|
SetHeader("User-Agent", "YaoEngine/"+share.VERSION)
|
||||||
|
setAnthropicAuthHeaders(req, p.Connector, key)
|
||||||
|
|
||||||
resp := req.Post(requestBody)
|
resp := req.Post(requestBody)
|
||||||
if resp.Code != 200 {
|
if resp.Code != 200 {
|
||||||
|
|
@ -915,6 +894,11 @@ func (p *Provider) buildRequestBody(messages []context.Message, options *context
|
||||||
} else if mt, ok := setting["max_tokens"].(int); ok && mt > 0 {
|
} else if mt, ok := setting["max_tokens"].(int); ok && mt > 0 {
|
||||||
maxTokens = mt
|
maxTokens = mt
|
||||||
}
|
}
|
||||||
|
if lc, ok := p.Connector.(goullm.LLMConnector); ok {
|
||||||
|
if caps := lc.GetCapabilities(); caps != nil && caps.MaxOutputTokens > 0 && maxTokens > caps.MaxOutputTokens {
|
||||||
|
maxTokens = caps.MaxOutputTokens
|
||||||
|
}
|
||||||
|
}
|
||||||
body["max_tokens"] = maxTokens
|
body["max_tokens"] = maxTokens
|
||||||
|
|
||||||
// Temperature
|
// Temperature
|
||||||
|
|
@ -1176,3 +1160,47 @@ func isRetryableError(err error) bool {
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// resolveHostKeyVersion extracts host, key, and version via LLMConnector or Setting().
|
||||||
|
// Setting() is called at most once, and only when needed.
|
||||||
|
func (p *Provider) resolveHostKeyVersion() (host, key, version string, err error) {
|
||||||
|
setting := p.Connector.Setting()
|
||||||
|
|
||||||
|
if lc, ok := p.Connector.(goullm.LLMConnector); ok {
|
||||||
|
host = lc.GetURL()
|
||||||
|
key = lc.GetKey()
|
||||||
|
} else {
|
||||||
|
host, _ = setting["host"].(string)
|
||||||
|
key, _ = setting["key"].(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Version is Anthropic-specific, not on LLMConnector interface
|
||||||
|
version = "2023-06-01"
|
||||||
|
if v, ok := setting["version"].(string); ok && v != "" {
|
||||||
|
version = v
|
||||||
|
}
|
||||||
|
|
||||||
|
if host == "" {
|
||||||
|
return "", "", "", fmt.Errorf("no host found in connector settings")
|
||||||
|
}
|
||||||
|
if key == "" {
|
||||||
|
return "", "", "", fmt.Errorf("API key is not set")
|
||||||
|
}
|
||||||
|
return host, key, version, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// setAnthropicAuthHeaders sets auth headers based on LLMConnector.GetAuthMode().
|
||||||
|
func setAnthropicAuthHeaders(req *http.Request, conn connector.Connector, key string) {
|
||||||
|
if lc, ok := conn.(goullm.LLMConnector); ok {
|
||||||
|
switch lc.GetAuthMode() {
|
||||||
|
case goullm.AuthAPIKey:
|
||||||
|
req.SetHeader("api-key", key)
|
||||||
|
return
|
||||||
|
case goullm.AuthBearer:
|
||||||
|
req.SetHeader("Authorization", fmt.Sprintf("Bearer %s", key))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Default for Anthropic: x-api-key
|
||||||
|
req.SetHeader("x-api-key", key)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -140,15 +140,30 @@ func (p *Provider) GetConnectorStringSetting(key string) (string, error) {
|
||||||
|
|
||||||
// GetModel gets the model name from connector settings
|
// GetModel gets the model name from connector settings
|
||||||
func (p *Provider) GetModel() (string, error) {
|
func (p *Provider) GetModel() (string, error) {
|
||||||
|
if lc, ok := p.Connector.(llm.LLMConnector); ok {
|
||||||
|
if m := lc.GetModel(); m != "" {
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
return p.GetConnectorStringSetting("model")
|
return p.GetConnectorStringSetting("model")
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAPIKey gets the API key from connector settings
|
// GetAPIKey gets the API key from connector settings
|
||||||
func (p *Provider) GetAPIKey() (string, error) {
|
func (p *Provider) GetAPIKey() (string, error) {
|
||||||
|
if lc, ok := p.Connector.(llm.LLMConnector); ok {
|
||||||
|
if k := lc.GetKey(); k != "" {
|
||||||
|
return k, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
return p.GetConnectorStringSetting("key")
|
return p.GetConnectorStringSetting("key")
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetHost gets the host URL from connector settings
|
// GetHost gets the host URL from connector settings
|
||||||
func (p *Provider) GetHost() (string, error) {
|
func (p *Provider) GetHost() (string, error) {
|
||||||
|
if lc, ok := p.Connector.(llm.LLMConnector); ok {
|
||||||
|
if u := lc.GetURL(); u != "" {
|
||||||
|
return u, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
return p.GetConnectorStringSetting("host")
|
return p.GetConnectorStringSetting("host")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/yaoapp/gou/connector"
|
"github.com/yaoapp/gou/connector"
|
||||||
|
goullm "github.com/yaoapp/gou/llm"
|
||||||
"github.com/yaoapp/yao/agent/context"
|
"github.com/yaoapp/yao/agent/context"
|
||||||
"github.com/yaoapp/yao/agent/llm/providers/anthropic"
|
"github.com/yaoapp/yao/agent/llm/providers/anthropic"
|
||||||
"github.com/yaoapp/yao/agent/llm/providers/openai"
|
"github.com/yaoapp/yao/agent/llm/providers/openai"
|
||||||
|
|
@ -61,16 +62,23 @@ func DetectAPIFormat(conn connector.Connector) string {
|
||||||
return "openai"
|
return "openai"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check connector settings for host URL patterns as fallback
|
// Try LLMConnector for typed URL access, fall back to Setting() map
|
||||||
settings := conn.Setting()
|
var host string
|
||||||
if settings != nil {
|
if lc, ok := conn.(goullm.LLMConnector); ok {
|
||||||
if host, ok := settings["host"].(string); ok {
|
host = lc.GetURL()
|
||||||
if contains(host, "anthropic.com") || contains(host, "api.kimi.com/coding") {
|
}
|
||||||
return "anthropic"
|
if host == "" {
|
||||||
}
|
if settings := conn.Setting(); settings != nil {
|
||||||
if contains(host, "deepseek.com") {
|
host, _ = settings["host"].(string)
|
||||||
return "openai"
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if host != "" {
|
||||||
|
if contains(host, "anthropic.com") || contains(host, "api.kimi.com/coding") {
|
||||||
|
return "anthropic"
|
||||||
|
}
|
||||||
|
if contains(host, "deepseek.com") {
|
||||||
|
return "openai"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import (
|
||||||
"github.com/yaoapp/yao/agent/llm/adapters"
|
"github.com/yaoapp/yao/agent/llm/adapters"
|
||||||
"github.com/yaoapp/yao/agent/llm/providers/base"
|
"github.com/yaoapp/yao/agent/llm/providers/base"
|
||||||
"github.com/yaoapp/yao/agent/output/message"
|
"github.com/yaoapp/yao/agent/output/message"
|
||||||
|
"github.com/yaoapp/yao/share"
|
||||||
"github.com/yaoapp/yao/utils/jsonschema"
|
"github.com/yaoapp/yao/utils/jsonschema"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -385,16 +386,10 @@ func (p *Provider) streamWithRetry(ctx *context.Context, messages []context.Mess
|
||||||
return nil, fmt.Errorf("failed to build request body: %w", err)
|
return nil, fmt.Errorf("failed to build request body: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get connector settings
|
// Get connector settings via LLMConnector or fallback
|
||||||
setting := p.Connector.Setting()
|
host, key, err := p.resolveHostKey()
|
||||||
host, ok := setting["host"].(string)
|
if err != nil {
|
||||||
if !ok || host == "" {
|
return nil, err
|
||||||
return nil, fmt.Errorf("no host found in connector settings")
|
|
||||||
}
|
|
||||||
|
|
||||||
key, ok := setting["key"].(string)
|
|
||||||
if !ok || key == "" {
|
|
||||||
return nil, fmt.Errorf("API key is not set")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build URL
|
// Build URL
|
||||||
|
|
@ -409,9 +404,9 @@ func (p *Provider) streamWithRetry(ctx *context.Context, messages []context.Mess
|
||||||
// Create HTTP request with proxy support
|
// Create HTTP request with proxy support
|
||||||
req := http.New(url).
|
req := http.New(url).
|
||||||
SetHeader("Content-Type", "application/json").
|
SetHeader("Content-Type", "application/json").
|
||||||
SetHeader("Authorization", fmt.Sprintf("Bearer %s", key)).
|
|
||||||
SetHeader("Accept", "text/event-stream").
|
SetHeader("Accept", "text/event-stream").
|
||||||
SetHeader("User-Agent", "YaoAgent/1.0 (+https://yaoagents.com)")
|
SetHeader("User-Agent", "YaoEngine/"+share.VERSION)
|
||||||
|
setAuthHeaders(req, p.Connector, key)
|
||||||
|
|
||||||
// Accumulate response data
|
// Accumulate response data
|
||||||
accumulator := &streamAccumulator{
|
accumulator := &streamAccumulator{
|
||||||
|
|
@ -922,16 +917,10 @@ func (p *Provider) postWithRetry(ctx *context.Context, messages []context.Messag
|
||||||
return nil, fmt.Errorf("failed to build request body: %w", err)
|
return nil, fmt.Errorf("failed to build request body: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get connector settings
|
// Get connector settings via LLMConnector or fallback
|
||||||
setting := p.Connector.Setting()
|
host, key, err := p.resolveHostKey()
|
||||||
host, ok := setting["host"].(string)
|
if err != nil {
|
||||||
if !ok || host == "" {
|
return nil, err
|
||||||
return nil, fmt.Errorf("no host found in connector settings")
|
|
||||||
}
|
|
||||||
|
|
||||||
key, ok := setting["key"].(string)
|
|
||||||
if !ok || key == "" {
|
|
||||||
return nil, fmt.Errorf("API key is not set")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build URL
|
// Build URL
|
||||||
|
|
@ -940,8 +929,8 @@ func (p *Provider) postWithRetry(ctx *context.Context, messages []context.Messag
|
||||||
// Create HTTP request with proxy support
|
// Create HTTP request with proxy support
|
||||||
req := http.New(url).
|
req := http.New(url).
|
||||||
SetHeader("Content-Type", "application/json").
|
SetHeader("Content-Type", "application/json").
|
||||||
SetHeader("Authorization", fmt.Sprintf("Bearer %s", key)).
|
SetHeader("User-Agent", "YaoEngine/"+share.VERSION)
|
||||||
SetHeader("User-Agent", "YaoAgent/1.0 (+https://yaoagents.com)")
|
setAuthHeaders(req, p.Connector, key)
|
||||||
|
|
||||||
// Make request
|
// Make request
|
||||||
resp := req.Post(requestBody)
|
resp := req.Post(requestBody)
|
||||||
|
|
@ -1120,11 +1109,19 @@ func (p *Provider) buildRequestBody(messages []context.Message, options *context
|
||||||
|
|
||||||
// Use max_completion_tokens (modern API parameter for GPT-5+)
|
// Use max_completion_tokens (modern API parameter for GPT-5+)
|
||||||
// GPT-5 models only support max_completion_tokens (not max_tokens)
|
// GPT-5 models only support max_completion_tokens (not max_tokens)
|
||||||
if options.MaxCompletionTokens != nil {
|
if options.MaxCompletionTokens != nil || options.MaxTokens != nil {
|
||||||
body["max_completion_tokens"] = *options.MaxCompletionTokens
|
maxTokens := 0
|
||||||
} else if options.MaxTokens != nil {
|
if options.MaxCompletionTokens != nil {
|
||||||
// Fallback: convert MaxTokens to max_completion_tokens for compatibility
|
maxTokens = *options.MaxCompletionTokens
|
||||||
body["max_completion_tokens"] = *options.MaxTokens
|
} else {
|
||||||
|
maxTokens = *options.MaxTokens
|
||||||
|
}
|
||||||
|
if lc, ok := p.Connector.(goullm.LLMConnector); ok {
|
||||||
|
if caps := lc.GetCapabilities(); caps != nil && caps.MaxOutputTokens > 0 && maxTokens > caps.MaxOutputTokens {
|
||||||
|
maxTokens = caps.MaxOutputTokens
|
||||||
|
}
|
||||||
|
}
|
||||||
|
body["max_completion_tokens"] = maxTokens
|
||||||
}
|
}
|
||||||
|
|
||||||
if options.TopP != nil {
|
if options.TopP != nil {
|
||||||
|
|
@ -1289,3 +1286,37 @@ func isRetryableError(err error) bool {
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// resolveHostKey extracts host and key via LLMConnector or Setting() fallback.
|
||||||
|
func (p *Provider) resolveHostKey() (host, key string, err error) {
|
||||||
|
if lc, ok := p.Connector.(goullm.LLMConnector); ok {
|
||||||
|
host = lc.GetURL()
|
||||||
|
key = lc.GetKey()
|
||||||
|
} else {
|
||||||
|
setting := p.Connector.Setting()
|
||||||
|
host, _ = setting["host"].(string)
|
||||||
|
key, _ = setting["key"].(string)
|
||||||
|
}
|
||||||
|
if host == "" {
|
||||||
|
return "", "", fmt.Errorf("no host found in connector settings")
|
||||||
|
}
|
||||||
|
if key == "" {
|
||||||
|
return "", "", fmt.Errorf("API key is not set")
|
||||||
|
}
|
||||||
|
return host, key, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// setAuthHeaders sets authentication headers based on LLMConnector.GetAuthMode().
|
||||||
|
func setAuthHeaders(req *http.Request, conn connector.Connector, key string) {
|
||||||
|
if lc, ok := conn.(goullm.LLMConnector); ok {
|
||||||
|
switch lc.GetAuthMode() {
|
||||||
|
case goullm.AuthAPIKey:
|
||||||
|
req.SetHeader("api-key", key)
|
||||||
|
return
|
||||||
|
case goullm.AuthXAPIKey:
|
||||||
|
req.SetHeader("x-api-key", key)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
req.SetHeader("Authorization", fmt.Sprintf("Bearer %s", key))
|
||||||
|
}
|
||||||
|
|
|
||||||
91
agent/llm/resolve.go
Normal file
91
agent/llm/resolve.go
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
package llm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/yaoapp/gou/connector"
|
||||||
|
goullm "github.com/yaoapp/gou/llm"
|
||||||
|
"github.com/yaoapp/yao/llmprovider"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RolePrefix marks a Connector field value as a role reference (e.g. "use::light").
|
||||||
|
const RolePrefix = "use::"
|
||||||
|
|
||||||
|
// ResolveConnector resolves an LLM connector using a unified priority chain.
|
||||||
|
//
|
||||||
|
// connectorID may be:
|
||||||
|
// - explicit connector ID (e.g. "openai.gpt-4o") — resolved directly
|
||||||
|
// - role reference with prefix (e.g. "use::light") — resolved via llmprovider roles
|
||||||
|
// - empty string — falls back to the "default" role
|
||||||
|
//
|
||||||
|
// Priority for role-based resolution:
|
||||||
|
// 1. GetRoleBy(role, identity) — user/team scoped setting
|
||||||
|
// 2. GetRole(role) — system-level default for that role
|
||||||
|
// 3. GetRoleBy("default", identity) — fallback to "default" role (user/team)
|
||||||
|
// 4. GetRole("default") — fallback to "default" role (system)
|
||||||
|
// 5. error — caller decides whether to apply legacy fallback
|
||||||
|
func ResolveConnector(connectorID string, identity llmprovider.Identity) (connector.Connector, *goullm.Capabilities, error) {
|
||||||
|
|
||||||
|
// Parse use:: prefix to extract role
|
||||||
|
role := ""
|
||||||
|
if strings.HasPrefix(connectorID, RolePrefix) {
|
||||||
|
role = strings.TrimPrefix(connectorID, RolePrefix)
|
||||||
|
connectorID = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Explicit connector ID takes highest priority
|
||||||
|
if connectorID != "" {
|
||||||
|
return selectWithCapabilities(connectorID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Empty connector with no role → treat as "default"
|
||||||
|
if role == "" {
|
||||||
|
role = "default"
|
||||||
|
}
|
||||||
|
|
||||||
|
if llmprovider.Global == nil {
|
||||||
|
return nil, nil, fmt.Errorf("llmprovider not initialized and no explicit connector specified")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve by the specified role (e.g. "light", "vision")
|
||||||
|
if role != "default" {
|
||||||
|
if identity != nil {
|
||||||
|
if cid, err := llmprovider.Global.GetRoleBy(role, identity); err == nil && cid != "" {
|
||||||
|
if conn, caps, err := selectWithCapabilities(cid); err == nil {
|
||||||
|
return conn, caps, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if cid, err := llmprovider.Global.GetRole(role); err == nil && cid != "" {
|
||||||
|
if conn, caps, err := selectWithCapabilities(cid); err == nil {
|
||||||
|
return conn, caps, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to "default" role
|
||||||
|
if identity != nil {
|
||||||
|
if cid, err := llmprovider.Global.GetRoleBy("default", identity); err == nil && cid != "" {
|
||||||
|
if conn, caps, err := selectWithCapabilities(cid); err == nil {
|
||||||
|
return conn, caps, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if cid, err := llmprovider.Global.GetRole("default"); err == nil && cid != "" {
|
||||||
|
if conn, caps, err := selectWithCapabilities(cid); err == nil {
|
||||||
|
return conn, caps, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil, fmt.Errorf("no connector resolved for role %q", role)
|
||||||
|
}
|
||||||
|
|
||||||
|
func selectWithCapabilities(connectorID string) (connector.Connector, *goullm.Capabilities, error) {
|
||||||
|
conn, err := connector.Select(connectorID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
caps := GetCapabilitiesFromConn(conn)
|
||||||
|
return conn, caps, nil
|
||||||
|
}
|
||||||
171
agent/llm/resolve_test.go
Normal file
171
agent/llm/resolve_test.go
Normal file
|
|
@ -0,0 +1,171 @@
|
||||||
|
package llm_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/yaoapp/gou/connector"
|
||||||
|
"github.com/yaoapp/gou/store"
|
||||||
|
"github.com/yaoapp/yao/agent/llm"
|
||||||
|
"github.com/yaoapp/yao/config"
|
||||||
|
"github.com/yaoapp/yao/llmprovider"
|
||||||
|
"github.com/yaoapp/yao/setting"
|
||||||
|
"github.com/yaoapp/yao/test"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
test.Prepare(nil, config.Conf)
|
||||||
|
defer test.Clean()
|
||||||
|
os.Exit(m.Run())
|
||||||
|
}
|
||||||
|
|
||||||
|
type mockIdentity struct {
|
||||||
|
UserID string
|
||||||
|
TeamID string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockIdentity) GetUserID() string { return m.UserID }
|
||||||
|
func (m *mockIdentity) GetTeamID() string { return m.TeamID }
|
||||||
|
|
||||||
|
func setupResolveTest(t *testing.T) string {
|
||||||
|
t.Helper()
|
||||||
|
test.Prepare(t, config.Conf)
|
||||||
|
|
||||||
|
err := setting.Init()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = llmprovider.Init()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
connIDs := connector.AIConnectors
|
||||||
|
if len(connIDs) == 0 {
|
||||||
|
t.Skip("no AI connectors available in test env")
|
||||||
|
}
|
||||||
|
|
||||||
|
cid := connIDs[0].Value
|
||||||
|
|
||||||
|
t.Cleanup(func() {
|
||||||
|
s, _ := store.Get("__yao.store")
|
||||||
|
if s != nil {
|
||||||
|
s.Del("llmprovider:*")
|
||||||
|
}
|
||||||
|
c, _ := store.Get("__yao.cache")
|
||||||
|
if c != nil {
|
||||||
|
c.Del("llmprovider:*")
|
||||||
|
}
|
||||||
|
test.Clean()
|
||||||
|
})
|
||||||
|
|
||||||
|
return cid
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- use:: prefix tests ---
|
||||||
|
|
||||||
|
func TestResolveConnector_UseLight(t *testing.T) {
|
||||||
|
cid := setupResolveTest(t)
|
||||||
|
|
||||||
|
err := llmprovider.Global.SetDefaults(map[string]string{
|
||||||
|
"default": cid,
|
||||||
|
"light": cid,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
conn, caps, err := llm.ResolveConnector("use::light", nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotNil(t, conn)
|
||||||
|
assert.NotNil(t, caps)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveConnector_UseDefault(t *testing.T) {
|
||||||
|
cid := setupResolveTest(t)
|
||||||
|
|
||||||
|
err := llmprovider.Global.SetDefaults(map[string]string{
|
||||||
|
"default": cid,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
conn, caps, err := llm.ResolveConnector("use::default", nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotNil(t, conn)
|
||||||
|
assert.NotNil(t, caps)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveConnector_UseLightWithIdentity(t *testing.T) {
|
||||||
|
cid := setupResolveTest(t)
|
||||||
|
|
||||||
|
err := llmprovider.Global.SetDefaults(map[string]string{
|
||||||
|
"default": cid,
|
||||||
|
"light": cid,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
conn, caps, err := llm.ResolveConnector("use::light", &mockIdentity{UserID: "u1", TeamID: "t1"})
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotNil(t, conn)
|
||||||
|
assert.NotNil(t, caps)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveConnector_UseLightNoProvider(t *testing.T) {
|
||||||
|
test.Prepare(t, config.Conf)
|
||||||
|
defer test.Clean()
|
||||||
|
|
||||||
|
saved := llmprovider.Global
|
||||||
|
llmprovider.Global = nil
|
||||||
|
defer func() { llmprovider.Global = saved }()
|
||||||
|
|
||||||
|
_, _, err := llm.ResolveConnector("use::light", nil)
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Explicit connector tests ---
|
||||||
|
|
||||||
|
func TestResolveConnector_ExplicitID(t *testing.T) {
|
||||||
|
cid := setupResolveTest(t)
|
||||||
|
|
||||||
|
conn, caps, err := llm.ResolveConnector(cid, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotNil(t, conn)
|
||||||
|
assert.NotNil(t, caps)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveConnector_ExplicitIDPriority(t *testing.T) {
|
||||||
|
cid := setupResolveTest(t)
|
||||||
|
|
||||||
|
err := llmprovider.Global.SetDefaults(map[string]string{
|
||||||
|
"default": cid,
|
||||||
|
"light": cid,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Explicit connector ID is NOT a use:: prefix, so it takes priority
|
||||||
|
conn, caps, err := llm.ResolveConnector(cid, &mockIdentity{UserID: "u1"})
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotNil(t, conn)
|
||||||
|
assert.NotNil(t, caps)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveConnector_InvalidID(t *testing.T) {
|
||||||
|
setupResolveTest(t)
|
||||||
|
|
||||||
|
_, _, err := llm.ResolveConnector("nonexistent-connector-xyz", nil)
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Empty connector fallback ---
|
||||||
|
|
||||||
|
func TestResolveConnector_EmptyFallbackDefault(t *testing.T) {
|
||||||
|
cid := setupResolveTest(t)
|
||||||
|
|
||||||
|
err := llmprovider.Global.SetDefaults(map[string]string{
|
||||||
|
"default": cid,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Empty string → treated as use::default
|
||||||
|
conn, caps, err := llm.ResolveConnector("", nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotNil(t, conn)
|
||||||
|
assert.NotNil(t, caps)
|
||||||
|
}
|
||||||
|
|
@ -227,15 +227,17 @@ func initAssistant() error {
|
||||||
// Set system agents configuration
|
// Set system agents configuration
|
||||||
if agentDSL.System != nil {
|
if agentDSL.System != nil {
|
||||||
assistant.SetSystemConfig(&assistant.SystemConfig{
|
assistant.SetSystemConfig(&assistant.SystemConfig{
|
||||||
Default: agentDSL.System.Default,
|
Default: agentDSL.System.Default,
|
||||||
Keyword: agentDSL.System.Keyword,
|
Light: agentDSL.System.Light,
|
||||||
QueryDSL: agentDSL.System.QueryDSL,
|
Vision: agentDSL.System.Vision,
|
||||||
Title: agentDSL.System.Title,
|
Audio: agentDSL.System.Audio,
|
||||||
Prompt: agentDSL.System.Prompt,
|
Keyword: agentDSL.System.Keyword,
|
||||||
NeedSearch: agentDSL.System.NeedSearch,
|
QueryDSL: agentDSL.System.QueryDSL,
|
||||||
Entity: agentDSL.System.Entity,
|
Title: agentDSL.System.Title,
|
||||||
Vision: agentDSL.System.Vision,
|
Prompt: agentDSL.System.Prompt,
|
||||||
Audio: agentDSL.System.Audio,
|
RobotPrompt: agentDSL.System.RobotPrompt,
|
||||||
|
NeedSearch: agentDSL.System.NeedSearch,
|
||||||
|
Entity: agentDSL.System.Entity,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -479,7 +481,8 @@ func defaultAssistant() (*assistant.Assistant, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildSystemRoles converts the System config block into a role→connectorID map
|
// buildSystemRoles converts the System config block into a role→connectorID map
|
||||||
// for llmprovider.SetDefaults.
|
// for llmprovider.SetDefaults. Only role-level keys are written here; per-agent
|
||||||
|
// overrides (keyword, title, querydsl, etc.) are consumed by resolveSystemConnector.
|
||||||
func buildSystemRoles(sys *types.System) map[string]string {
|
func buildSystemRoles(sys *types.System) map[string]string {
|
||||||
roles := make(map[string]string)
|
roles := make(map[string]string)
|
||||||
add := func(role, cid string) {
|
add := func(role, cid string) {
|
||||||
|
|
@ -488,13 +491,7 @@ func buildSystemRoles(sys *types.System) map[string]string {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
add("default", sys.Default)
|
add("default", sys.Default)
|
||||||
add("keyword", sys.Keyword)
|
add("light", sys.Light)
|
||||||
add("querydsl", sys.QueryDSL)
|
|
||||||
add("title", sys.Title)
|
|
||||||
add("prompt", sys.Prompt)
|
|
||||||
add("robot_prompt", sys.RobotPrompt)
|
|
||||||
add("needsearch", sys.NeedSearch)
|
|
||||||
add("entity", sys.Entity)
|
|
||||||
add("vision", sys.Vision)
|
add("vision", sys.Vision)
|
||||||
add("audio", sys.Audio)
|
add("audio", sys.Audio)
|
||||||
return roles
|
return roles
|
||||||
|
|
@ -506,6 +503,9 @@ func buildSystemRoles(sys *types.System) map[string]string {
|
||||||
func resolveEnvStrings(setting *types.DSL) {
|
func resolveEnvStrings(setting *types.DSL) {
|
||||||
if setting.System != nil {
|
if setting.System != nil {
|
||||||
setting.System.Default = helper.EnvString(setting.System.Default)
|
setting.System.Default = helper.EnvString(setting.System.Default)
|
||||||
|
setting.System.Light = helper.EnvString(setting.System.Light)
|
||||||
|
setting.System.Vision = helper.EnvString(setting.System.Vision)
|
||||||
|
setting.System.Audio = helper.EnvString(setting.System.Audio)
|
||||||
setting.System.Keyword = helper.EnvString(setting.System.Keyword)
|
setting.System.Keyword = helper.EnvString(setting.System.Keyword)
|
||||||
setting.System.QueryDSL = helper.EnvString(setting.System.QueryDSL)
|
setting.System.QueryDSL = helper.EnvString(setting.System.QueryDSL)
|
||||||
setting.System.Title = helper.EnvString(setting.System.Title)
|
setting.System.Title = helper.EnvString(setting.System.Title)
|
||||||
|
|
@ -513,8 +513,6 @@ func resolveEnvStrings(setting *types.DSL) {
|
||||||
setting.System.RobotPrompt = helper.EnvString(setting.System.RobotPrompt)
|
setting.System.RobotPrompt = helper.EnvString(setting.System.RobotPrompt)
|
||||||
setting.System.NeedSearch = helper.EnvString(setting.System.NeedSearch)
|
setting.System.NeedSearch = helper.EnvString(setting.System.NeedSearch)
|
||||||
setting.System.Entity = helper.EnvString(setting.System.Entity)
|
setting.System.Entity = helper.EnvString(setting.System.Entity)
|
||||||
setting.System.Vision = helper.EnvString(setting.System.Vision)
|
|
||||||
setting.System.Audio = helper.EnvString(setting.System.Audio)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if setting.Uses != nil {
|
if setting.Uses != nil {
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import (
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/yaoapp/gou/connector"
|
"github.com/yaoapp/gou/connector"
|
||||||
|
goullm "github.com/yaoapp/gou/llm"
|
||||||
"github.com/yaoapp/gou/store"
|
"github.com/yaoapp/gou/store"
|
||||||
"github.com/yaoapp/kun/str"
|
"github.com/yaoapp/kun/str"
|
||||||
agentContext "github.com/yaoapp/yao/agent/context"
|
agentContext "github.com/yaoapp/yao/agent/context"
|
||||||
|
|
@ -140,9 +141,22 @@ func buildEnv(req *types.StreamRequest, p platform) map[string]string {
|
||||||
|
|
||||||
if req.Connector != nil {
|
if req.Connector != nil {
|
||||||
setting := req.Connector.Setting()
|
setting := req.Connector.Setting()
|
||||||
host, _ := setting["host"].(string)
|
|
||||||
key, _ := setting["key"].(string)
|
var host, key, model string
|
||||||
model, _ := setting["model"].(string)
|
if lc, ok := req.Connector.(goullm.LLMConnector); ok {
|
||||||
|
host = lc.GetURL()
|
||||||
|
key = lc.GetKey()
|
||||||
|
model = lc.GetModel()
|
||||||
|
}
|
||||||
|
if host == "" {
|
||||||
|
host, _ = setting["host"].(string)
|
||||||
|
}
|
||||||
|
if key == "" {
|
||||||
|
key, _ = setting["key"].(string)
|
||||||
|
}
|
||||||
|
if model == "" {
|
||||||
|
model, _ = setting["model"].(string)
|
||||||
|
}
|
||||||
|
|
||||||
roleConnectors := getRoleConnectors(req)
|
roleConnectors := getRoleConnectors(req)
|
||||||
getConn := func(id string) connector.Connector {
|
getConn := func(id string) connector.Connector {
|
||||||
|
|
@ -204,6 +218,17 @@ func buildEnv(req *types.StreamRequest, p platform) map[string]string {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if lc, ok := req.Connector.(goullm.LLMConnector); ok {
|
||||||
|
if caps := lc.GetCapabilities(); caps != nil {
|
||||||
|
if caps.MaxOutputTokens > 0 {
|
||||||
|
env["CLAUDE_CODE_MAX_OUTPUT_TOKENS"] = fmt.Sprintf("%d", caps.MaxOutputTokens)
|
||||||
|
}
|
||||||
|
if caps.MaxInputTokens > 0 {
|
||||||
|
env["CLAUDE_CODE_AUTO_COMPACT_WINDOW"] = fmt.Sprintf("%d", caps.MaxInputTokens)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if thinking, ok := setting["thinking"].(map[string]interface{}); ok {
|
if thinking, ok := setting["thinking"].(map[string]interface{}); ok {
|
||||||
thinkType, _ := thinking["type"].(string)
|
thinkType, _ := thinking["type"].(string)
|
||||||
switch thinkType {
|
switch thinkType {
|
||||||
|
|
@ -416,6 +441,11 @@ func connectorHost(c connector.Connector) string {
|
||||||
if c == nil {
|
if c == nil {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
if lc, ok := c.(goullm.LLMConnector); ok {
|
||||||
|
if u := lc.GetURL(); u != "" {
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
}
|
||||||
host, _ := c.Setting()["host"].(string)
|
host, _ := c.Setting()["host"].(string)
|
||||||
return host
|
return host
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/yaoapp/gou/connector"
|
"github.com/yaoapp/gou/connector"
|
||||||
|
goullm "github.com/yaoapp/gou/llm"
|
||||||
"github.com/yaoapp/kun/log"
|
"github.com/yaoapp/kun/log"
|
||||||
agentContext "github.com/yaoapp/yao/agent/context"
|
agentContext "github.com/yaoapp/yao/agent/context"
|
||||||
"github.com/yaoapp/yao/agent/output/message"
|
"github.com/yaoapp/yao/agent/output/message"
|
||||||
|
|
@ -184,11 +185,13 @@ func (r *Runner) Cleanup(ctx context.Context, computer infra.Computer) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
type a2oConnectorConfig struct {
|
type a2oConnectorConfig struct {
|
||||||
Backend string `json:"backend"`
|
Backend string `json:"backend"`
|
||||||
Model string `json:"model"`
|
Model string `json:"model"`
|
||||||
APIKey string `json:"api_key"`
|
APIKey string `json:"api_key"`
|
||||||
Options map[string]interface{} `json:"options,omitempty"`
|
AuthMode string `json:"auth_mode,omitempty"`
|
||||||
Routes map[string]*a2oConnectorConfig `json:"routes,omitempty"`
|
MaxOutputTokens int `json:"max_output_tokens,omitempty"`
|
||||||
|
Options map[string]interface{} `json:"options,omitempty"`
|
||||||
|
Routes map[string]*a2oConnectorConfig `json:"routes,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildSingleA2OConfig(conn connector.Connector) *a2oConnectorConfig {
|
func buildSingleA2OConfig(conn connector.Connector) *a2oConnectorConfig {
|
||||||
|
|
@ -199,27 +202,33 @@ func buildSingleA2OConfig(conn connector.Connector) *a2oConnectorConfig {
|
||||||
|
|
||||||
cfg := &a2oConnectorConfig{}
|
cfg := &a2oConnectorConfig{}
|
||||||
|
|
||||||
if host, ok := settings["host"].(string); ok && host != "" {
|
// Extract standard fields via LLMConnector methods when available
|
||||||
cfg.Backend = connector.BuildAPIURL(host, "/chat/completions")
|
if lc, ok := conn.(goullm.LLMConnector); ok {
|
||||||
} else if proxy, ok := settings["proxy"].(string); ok && proxy != "" {
|
if url := lc.GetURL(); url != "" {
|
||||||
cfg.Backend = connector.BuildAPIURL(proxy, "/chat/completions")
|
cfg.Backend = connector.BuildAPIURL(url, "/chat/completions")
|
||||||
}
|
}
|
||||||
if model, ok := settings["model"].(string); ok && model != "" {
|
cfg.Model = lc.GetModel()
|
||||||
cfg.Model = model
|
cfg.APIKey = lc.GetKey()
|
||||||
}
|
cfg.AuthMode = string(lc.GetAuthMode())
|
||||||
if key, ok := settings["key"].(string); ok && key != "" {
|
if caps := lc.GetCapabilities(); caps != nil && caps.MaxOutputTokens > 0 {
|
||||||
cfg.APIKey = key
|
cfg.MaxOutputTokens = caps.MaxOutputTokens
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
extra := make(map[string]interface{})
|
if host, ok := settings["host"].(string); ok && host != "" {
|
||||||
for k, v := range settings {
|
cfg.Backend = connector.BuildAPIURL(host, "/chat/completions")
|
||||||
switch k {
|
} else if proxy, ok := settings["proxy"].(string); ok && proxy != "" {
|
||||||
case "host", "model", "key", "proxy", "type":
|
cfg.Backend = connector.BuildAPIURL(proxy, "/chat/completions")
|
||||||
continue
|
}
|
||||||
default:
|
if model, ok := settings["model"].(string); ok && model != "" {
|
||||||
extra[k] = v
|
cfg.Model = model
|
||||||
|
}
|
||||||
|
if key, ok := settings["key"].(string); ok && key != "" {
|
||||||
|
cfg.APIKey = key
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Whitelist-filter remaining settings for the options field
|
||||||
|
extra := connector.FilterRequestBodyParams(settings, conn)
|
||||||
if len(extra) > 0 {
|
if len(extra) > 0 {
|
||||||
cfg.Options = extra
|
cfg.Options = extra
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import (
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/yaoapp/gou/connector"
|
"github.com/yaoapp/gou/connector"
|
||||||
|
goullm "github.com/yaoapp/gou/llm"
|
||||||
"github.com/yaoapp/gou/store"
|
"github.com/yaoapp/gou/store"
|
||||||
"github.com/yaoapp/kun/str"
|
"github.com/yaoapp/kun/str"
|
||||||
agentContext "github.com/yaoapp/yao/agent/context"
|
agentContext "github.com/yaoapp/yao/agent/context"
|
||||||
|
|
@ -400,6 +401,11 @@ func connectorHost(c connector.Connector) string {
|
||||||
if c == nil {
|
if c == nil {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
if lc, ok := c.(goullm.LLMConnector); ok {
|
||||||
|
if u := lc.GetURL(); u != "" {
|
||||||
|
return strings.TrimSpace(u)
|
||||||
|
}
|
||||||
|
}
|
||||||
host, _ := c.Setting()["host"].(string)
|
host, _ := c.Setting()["host"].(string)
|
||||||
return strings.TrimSpace(host)
|
return strings.TrimSpace(host)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/yaoapp/gou/connector"
|
"github.com/yaoapp/gou/connector"
|
||||||
|
goullm "github.com/yaoapp/gou/llm"
|
||||||
"github.com/yaoapp/yao/agent/sandbox/v2/types"
|
"github.com/yaoapp/yao/agent/sandbox/v2/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -78,8 +79,18 @@ func buildOpenCodeConfig(req *types.PrepareRequest, mcpServers []types.MCPServer
|
||||||
// For custom hosts (OpenAI-compatible proxies), we pass the bare host URL.
|
// For custom hosts (OpenAI-compatible proxies), we pass the bare host URL.
|
||||||
func buildProviderConfig(conn connector.Connector) (providerID string, cfg map[string]any, model string) {
|
func buildProviderConfig(conn connector.Connector) (providerID string, cfg map[string]any, model string) {
|
||||||
setting := conn.Setting()
|
setting := conn.Setting()
|
||||||
host, _ := setting["host"].(string)
|
|
||||||
modelName, _ := setting["model"].(string)
|
var host, modelName string
|
||||||
|
if lc, ok := conn.(goullm.LLMConnector); ok {
|
||||||
|
host = lc.GetURL()
|
||||||
|
modelName = lc.GetModel()
|
||||||
|
}
|
||||||
|
if host == "" {
|
||||||
|
host, _ = setting["host"].(string)
|
||||||
|
}
|
||||||
|
if modelName == "" {
|
||||||
|
modelName, _ = setting["model"].(string)
|
||||||
|
}
|
||||||
|
|
||||||
opts := map[string]any{
|
opts := map[string]any{
|
||||||
"apiKey": "{env:YAO_PROVIDER_KEY}",
|
"apiKey": "{env:YAO_PROVIDER_KEY}",
|
||||||
|
|
@ -123,6 +134,21 @@ func buildProviderConfig(conn connector.Connector) (providerID string, cfg map[s
|
||||||
modelCfg["options"] = modelOpts
|
modelCfg["options"] = modelOpts
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if lc, ok := conn.(goullm.LLMConnector); ok {
|
||||||
|
if caps := lc.GetCapabilities(); caps != nil {
|
||||||
|
limit := map[string]any{}
|
||||||
|
if caps.MaxInputTokens > 0 {
|
||||||
|
limit["context"] = caps.MaxInputTokens
|
||||||
|
}
|
||||||
|
if caps.MaxOutputTokens > 0 {
|
||||||
|
limit["output"] = caps.MaxOutputTokens
|
||||||
|
}
|
||||||
|
if len(limit) > 0 {
|
||||||
|
modelCfg["limit"] = limit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return "custom", map[string]any{
|
return "custom", map[string]any{
|
||||||
"npm": "@ai-sdk/openai-compatible",
|
"npm": "@ai-sdk/openai-compatible",
|
||||||
"options": opts,
|
"options": opts,
|
||||||
|
|
@ -309,6 +335,21 @@ func buildRoleProviderConfig(conn connector.Connector, envKeyPrefix string, moda
|
||||||
modelCfg["modalities"] = modalities
|
modelCfg["modalities"] = modalities
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if lc, ok := conn.(goullm.LLMConnector); ok {
|
||||||
|
if caps := lc.GetCapabilities(); caps != nil {
|
||||||
|
limit := map[string]any{}
|
||||||
|
if caps.MaxInputTokens > 0 {
|
||||||
|
limit["context"] = caps.MaxInputTokens
|
||||||
|
}
|
||||||
|
if caps.MaxOutputTokens > 0 {
|
||||||
|
limit["output"] = caps.MaxOutputTokens
|
||||||
|
}
|
||||||
|
if len(limit) > 0 {
|
||||||
|
modelCfg["limit"] = limit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if conn.Is(connector.ANTHROPIC) {
|
if conn.Is(connector.ANTHROPIC) {
|
||||||
if host != "" {
|
if host != "" {
|
||||||
opts["baseURL"] = host
|
opts["baseURL"] = host
|
||||||
|
|
|
||||||
|
|
@ -212,6 +212,7 @@ type AssistantInfo struct {
|
||||||
Avatar string `json:"avatar,omitempty"`
|
Avatar string `json:"avatar,omitempty"`
|
||||||
Description string `json:"description,omitempty"`
|
Description string `json:"description,omitempty"`
|
||||||
Connector string `json:"connector,omitempty"`
|
Connector string `json:"connector,omitempty"`
|
||||||
|
ConnectorRaw string `json:"connector_raw,omitempty"`
|
||||||
ConnectorOptions *ConnectorOptions `json:"connector_options,omitempty"`
|
ConnectorOptions *ConnectorOptions `json:"connector_options,omitempty"`
|
||||||
Modes []string `json:"modes,omitempty"`
|
Modes []string `json:"modes,omitempty"`
|
||||||
DefaultMode string `json:"default_mode,omitempty"`
|
DefaultMode string `json:"default_mode,omitempty"`
|
||||||
|
|
@ -432,7 +433,7 @@ type AssistantModel struct {
|
||||||
Type string `json:"type,omitempty"` // Assistant Type, default is assistant
|
Type string `json:"type,omitempty"` // Assistant Type, default is assistant
|
||||||
Name string `json:"name,omitempty"` // Assistant Name
|
Name string `json:"name,omitempty"` // Assistant Name
|
||||||
Avatar string `json:"avatar,omitempty"` // Assistant Avatar
|
Avatar string `json:"avatar,omitempty"` // Assistant Avatar
|
||||||
Connector string `json:"connector"` // AI Connector (default connector)
|
Connector string `json:"connector"` // AI Connector (default connector, or "use::<role>" for role-based resolution)
|
||||||
ConnectorOptions *ConnectorOptions `json:"connector_options,omitempty"` // Connector selection options for user to choose from
|
ConnectorOptions *ConnectorOptions `json:"connector_options,omitempty"` // Connector selection options for user to choose from
|
||||||
Path string `json:"path,omitempty"` // Assistant Path
|
Path string `json:"path,omitempty"` // Assistant Path
|
||||||
BuiltIn bool `json:"built_in,omitempty"` // Whether this is a built-in assistant
|
BuiltIn bool `json:"built_in,omitempty"` // Whether this is a built-in assistant
|
||||||
|
|
|
||||||
|
|
@ -93,7 +93,13 @@ func (u *Uses) GetPhaseAgent(phase string) string {
|
||||||
// System configures connectors for system agents
|
// System configures connectors for system agents
|
||||||
// ===============================
|
// ===============================
|
||||||
type System struct {
|
type System struct {
|
||||||
Default string `json:"default,omitempty" yaml:"default,omitempty"` // Default connector for all system agents
|
// Role-level defaults (written to llmprovider via SetDefaults)
|
||||||
|
Default string `json:"default,omitempty" yaml:"default,omitempty"` // Default connector for the "default" role
|
||||||
|
Light string `json:"light,omitempty" yaml:"light,omitempty"` // Default connector for the "light" role (titles, keywords, summaries)
|
||||||
|
Vision string `json:"vision,omitempty" yaml:"vision,omitempty"` // Default connector for the "vision" role
|
||||||
|
Audio string `json:"audio,omitempty" yaml:"audio,omitempty"` // Default connector for the "audio" role
|
||||||
|
|
||||||
|
// Per-agent overrides (optional, highest priority — bypasses role resolution)
|
||||||
Keyword string `json:"keyword,omitempty" yaml:"keyword,omitempty"` // Connector for __yao.keyword agent
|
Keyword string `json:"keyword,omitempty" yaml:"keyword,omitempty"` // Connector for __yao.keyword agent
|
||||||
QueryDSL string `json:"querydsl,omitempty" yaml:"querydsl,omitempty"` // Connector for __yao.querydsl agent
|
QueryDSL string `json:"querydsl,omitempty" yaml:"querydsl,omitempty"` // Connector for __yao.querydsl agent
|
||||||
Title string `json:"title,omitempty" yaml:"title,omitempty"` // Connector for __yao.title agent
|
Title string `json:"title,omitempty" yaml:"title,omitempty"` // Connector for __yao.title agent
|
||||||
|
|
@ -101,8 +107,6 @@ type System struct {
|
||||||
RobotPrompt string `json:"robot_prompt,omitempty" yaml:"robot_prompt,omitempty"` // Connector for __yao.robot_prompt agent
|
RobotPrompt string `json:"robot_prompt,omitempty" yaml:"robot_prompt,omitempty"` // Connector for __yao.robot_prompt agent
|
||||||
NeedSearch string `json:"needsearch,omitempty" yaml:"needsearch,omitempty"` // Connector for __yao.needsearch agent
|
NeedSearch string `json:"needsearch,omitempty" yaml:"needsearch,omitempty"` // Connector for __yao.needsearch agent
|
||||||
Entity string `json:"entity,omitempty" yaml:"entity,omitempty"` // Connector for __yao.entity agent
|
Entity string `json:"entity,omitempty" yaml:"entity,omitempty"` // Connector for __yao.entity agent
|
||||||
Vision string `json:"vision,omitempty" yaml:"vision,omitempty"` // Connector for vision capabilities
|
|
||||||
Audio string `json:"audio,omitempty" yaml:"audio,omitempty"` // Connector for audio/STT capabilities
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mention Structure
|
// Mention Structure
|
||||||
|
|
|
||||||
782
data/bindata.go
782
data/bindata.go
File diff suppressed because it is too large
Load diff
|
|
@ -310,12 +310,19 @@ func ownerMatch(po, want *ProviderOwner) bool {
|
||||||
return po.Type == "user" && po.UserID == want.UserID
|
return po.Type == "user" && po.UserID == want.UserID
|
||||||
}
|
}
|
||||||
|
|
||||||
// capabilitiesFromConn extracts *llm.Capabilities from a connector's settings.
|
// capabilitiesFromConn extracts *llm.Capabilities from a connector.
|
||||||
|
// Prefers LLMConnector.GetCapabilities() when available.
|
||||||
func capabilitiesFromConn(conn connector.Connector) *goullm.Capabilities {
|
func capabilitiesFromConn(conn connector.Connector) *goullm.Capabilities {
|
||||||
if conn == nil {
|
if conn == nil {
|
||||||
return defaultCaps()
|
return defaultCaps()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if lc, ok := conn.(goullm.LLMConnector); ok {
|
||||||
|
if caps := lc.GetCapabilities(); caps != nil {
|
||||||
|
return caps
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
settings := conn.Setting()
|
settings := conn.Setting()
|
||||||
if settings != nil {
|
if settings != nil {
|
||||||
if caps, ok := settings["capabilities"]; ok {
|
if caps, ok := settings["capabilities"]; ok {
|
||||||
|
|
|
||||||
|
|
@ -18,14 +18,20 @@
|
||||||
default_models:
|
default_models:
|
||||||
- id: gpt-4o
|
- id: gpt-4o
|
||||||
name: GPT-4o
|
name: GPT-4o
|
||||||
|
max_input_tokens: 128000
|
||||||
|
max_output_tokens: 16384
|
||||||
capabilities: [vision, tool_calls, streaming, json]
|
capabilities: [vision, tool_calls, streaming, json]
|
||||||
enabled: true
|
enabled: true
|
||||||
- id: gpt-4o-mini
|
- id: gpt-4o-mini
|
||||||
name: GPT-4o Mini
|
name: GPT-4o Mini
|
||||||
|
max_input_tokens: 128000
|
||||||
|
max_output_tokens: 16384
|
||||||
capabilities: [tool_calls, streaming, json]
|
capabilities: [tool_calls, streaming, json]
|
||||||
enabled: true
|
enabled: true
|
||||||
- id: o3-mini
|
- id: o3-mini
|
||||||
name: o3-mini
|
name: o3-mini
|
||||||
|
max_input_tokens: 200000
|
||||||
|
max_output_tokens: 100000
|
||||||
capabilities: [tool_calls, streaming, reasoning]
|
capabilities: [tool_calls, streaming, reasoning]
|
||||||
enabled: false
|
enabled: false
|
||||||
|
|
||||||
|
|
@ -37,10 +43,14 @@
|
||||||
default_models:
|
default_models:
|
||||||
- id: claude-sonnet-4-20250514
|
- id: claude-sonnet-4-20250514
|
||||||
name: Claude Sonnet 4
|
name: Claude Sonnet 4
|
||||||
|
max_input_tokens: 200000
|
||||||
|
max_output_tokens: 16000
|
||||||
capabilities: [vision, tool_calls, streaming, reasoning]
|
capabilities: [vision, tool_calls, streaming, reasoning]
|
||||||
enabled: true
|
enabled: true
|
||||||
- id: claude-haiku-3-5-20241022
|
- id: claude-haiku-3-5-20241022
|
||||||
name: Claude Haiku 3.5
|
name: Claude Haiku 3.5
|
||||||
|
max_input_tokens: 200000
|
||||||
|
max_output_tokens: 8192
|
||||||
capabilities: [tool_calls, streaming]
|
capabilities: [tool_calls, streaming]
|
||||||
enabled: true
|
enabled: true
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -141,9 +141,14 @@ func (r *Registry) extractConnectorID(target interface{}) string {
|
||||||
|
|
||||||
p, err := r.Get(rt.Provider, true)
|
p, err := r.Get(rt.Provider, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Builtin providers: Key == ConnectorID
|
if rt.Model != "" {
|
||||||
|
return rt.Provider + ":" + rt.Model
|
||||||
|
}
|
||||||
return rt.Provider
|
return rt.Provider
|
||||||
}
|
}
|
||||||
|
if rt.Model != "" {
|
||||||
|
return p.ConnectorID + ":" + rt.Model
|
||||||
|
}
|
||||||
return p.ConnectorID
|
return p.ConnectorID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,7 @@ func TestGetRole(t *testing.T) {
|
||||||
|
|
||||||
cid, err := r.GetRole("default")
|
cid, err := r.GetRole("default")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, p.ConnectorID, cid)
|
assert.Equal(t, p.ConnectorID+":gpt-4o", cid)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetRoleByUser(t *testing.T) {
|
func TestGetRoleByUser(t *testing.T) {
|
||||||
|
|
@ -101,11 +101,11 @@ func TestGetRoleByUser(t *testing.T) {
|
||||||
|
|
||||||
cid, err := r.GetRoleByUser("default", "u1")
|
cid, err := r.GetRoleByUser("default", "u1")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, userP.ConnectorID, cid, "user scope should override system")
|
assert.Equal(t, userP.ConnectorID+":gpt-4o", cid, "user scope should override system")
|
||||||
|
|
||||||
cidSys, err := r.GetRole("default")
|
cidSys, err := r.GetRole("default")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, sysP.ConnectorID, cidSys, "system scope should still return system provider")
|
assert.Equal(t, sysP.ConnectorID+":gpt-4o", cidSys, "system scope should still return system provider")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetRoleByTeam(t *testing.T) {
|
func TestGetRoleByTeam(t *testing.T) {
|
||||||
|
|
@ -130,7 +130,45 @@ func TestGetRoleByTeam(t *testing.T) {
|
||||||
|
|
||||||
cid, err := r.GetRoleByTeam("default", "t1")
|
cid, err := r.GetRoleByTeam("default", "t1")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, teamP.ConnectorID, cid, "team scope should override system")
|
assert.Equal(t, teamP.ConnectorID+":gpt-4o", cid, "team scope should override system")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetRoleIncludesModel(t *testing.T) {
|
||||||
|
r := setupRegistryWithSetting(t)
|
||||||
|
p := createTestProviderForRole(t, r, "model-inc-prov")
|
||||||
|
|
||||||
|
err := r.SetDefaults(map[string]string{"default": p.Key})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
cid, err := r.GetRole("default")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Contains(t, cid, ":", "connector ID should contain ':' separator for model")
|
||||||
|
assert.Equal(t, p.ConnectorID+":gpt-4o", cid, "should include model suffix")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetRoleNoModel(t *testing.T) {
|
||||||
|
r := setupRegistryWithSetting(t)
|
||||||
|
|
||||||
|
noModelProvider := llmprovider.Provider{
|
||||||
|
Key: "nomodel-prov",
|
||||||
|
Name: "No Model Provider",
|
||||||
|
Type: "openai",
|
||||||
|
APIURL: "https://api.openai.com",
|
||||||
|
APIKey: "sk-test",
|
||||||
|
Enabled: true,
|
||||||
|
Models: []llmprovider.ModelInfo{},
|
||||||
|
Owner: llmprovider.ProviderOwner{Type: "system"},
|
||||||
|
}
|
||||||
|
created, err := r.Create(&noModelProvider)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = r.SetDefaults(map[string]string{"default": created.Key})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
cid, err := r.GetRole("default")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, created.ConnectorID, cid, "should return base connector ID without model when no models defined")
|
||||||
|
assert.NotContains(t, cid, ":", "should not contain model separator")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetRoleNotConfigured(t *testing.T) {
|
func TestGetRoleNotConfigured(t *testing.T) {
|
||||||
|
|
@ -195,7 +233,7 @@ func TestProcessGetRole(t *testing.T) {
|
||||||
proc := process.New("llmprovider.getrole", "default")
|
proc := process.New("llmprovider.getrole", "default")
|
||||||
result, err := proc.Exec()
|
result, err := proc.Exec()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, p.ConnectorID, result)
|
assert.Equal(t, p.ConnectorID+":gpt-4o", result)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestProcessListRoles(t *testing.T) {
|
func TestProcessListRoles(t *testing.T) {
|
||||||
|
|
|
||||||
|
|
@ -245,7 +245,7 @@ func TestGetRoleBy_TeamPriority(t *testing.T) {
|
||||||
info := &oauthTypes.AuthorizedInfo{UserID: "u1", TeamID: "grb-t1"}
|
info := &oauthTypes.AuthorizedInfo{UserID: "u1", TeamID: "grb-t1"}
|
||||||
cid, err := r.GetRoleBy("default", info)
|
cid, err := r.GetRoleBy("default", info)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, teamP.ConnectorID, cid, "should resolve via team scope when TeamID is set")
|
assert.Equal(t, teamP.ConnectorID+":gpt-4o", cid, "should resolve via team scope when TeamID is set")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetRoleBy_UserFallback(t *testing.T) {
|
func TestGetRoleBy_UserFallback(t *testing.T) {
|
||||||
|
|
@ -272,7 +272,7 @@ func TestGetRoleBy_UserFallback(t *testing.T) {
|
||||||
info := &oauthTypes.AuthorizedInfo{UserID: "grbu-u1"}
|
info := &oauthTypes.AuthorizedInfo{UserID: "grbu-u1"}
|
||||||
cid, err := r.GetRoleBy("default", info)
|
cid, err := r.GetRoleBy("default", info)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, userP.ConnectorID, cid, "should resolve via user scope when no TeamID")
|
assert.Equal(t, userP.ConnectorID+":gpt-4o", cid, "should resolve via user scope when no TeamID")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestListRolesBy(t *testing.T) {
|
func TestListRolesBy(t *testing.T) {
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/yaoapp/gou/connector"
|
"github.com/yaoapp/gou/connector"
|
||||||
|
goullm "github.com/yaoapp/gou/llm"
|
||||||
)
|
)
|
||||||
|
|
||||||
// connectorID builds the runtime ID for registering into connector.Connectors.
|
// connectorID builds the runtime ID for registering into connector.Connectors.
|
||||||
|
|
@ -51,6 +52,12 @@ func marshalDSL(p *Provider) ([]byte, error) {
|
||||||
"label": p.Name,
|
"label": p.Name,
|
||||||
"options": opts,
|
"options": opts,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Propagate auth_mode for providers that use non-Bearer authentication
|
||||||
|
if p.PresetKey == "azure" {
|
||||||
|
dsl["auth_mode"] = "api-key"
|
||||||
|
}
|
||||||
|
|
||||||
return json.Marshal(dsl)
|
return json.Marshal(dsl)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -140,7 +147,7 @@ func ensureModelConnector(p *Provider, m *ModelInfo) error {
|
||||||
|
|
||||||
// marshalModelDSL builds a connector DSL for a specific model within a provider.
|
// marshalModelDSL builds a connector DSL for a specific model within a provider.
|
||||||
func marshalModelDSL(p *Provider, m *ModelInfo) ([]byte, error) {
|
func marshalModelDSL(p *Provider, m *ModelInfo) ([]byte, error) {
|
||||||
caps := make(map[string]bool)
|
caps := make(map[string]interface{})
|
||||||
for _, c := range m.Capabilities {
|
for _, c := range m.Capabilities {
|
||||||
caps[c] = true
|
caps[c] = true
|
||||||
}
|
}
|
||||||
|
|
@ -152,6 +159,12 @@ func marshalModelDSL(p *Provider, m *ModelInfo) ([]byte, error) {
|
||||||
caps["temperature_adjustable"] = true
|
caps["temperature_adjustable"] = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if m.MaxInputTokens > 0 {
|
||||||
|
caps["max_input_tokens"] = m.MaxInputTokens
|
||||||
|
}
|
||||||
|
if m.MaxOutputTokens > 0 {
|
||||||
|
caps["max_output_tokens"] = m.MaxOutputTokens
|
||||||
|
}
|
||||||
|
|
||||||
opts := map[string]interface{}{
|
opts := map[string]interface{}{
|
||||||
"host": p.APIURL,
|
"host": p.APIURL,
|
||||||
|
|
@ -172,6 +185,11 @@ func marshalModelDSL(p *Provider, m *ModelInfo) ([]byte, error) {
|
||||||
"label": name,
|
"label": name,
|
||||||
"options": opts,
|
"options": opts,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if p.PresetKey == "azure" {
|
||||||
|
dsl["auth_mode"] = "api-key"
|
||||||
|
}
|
||||||
|
|
||||||
return json.Marshal(dsl)
|
return json.Marshal(dsl)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -228,13 +246,34 @@ func providerFromConnector(id string, conn connector.Connector) Provider {
|
||||||
}
|
}
|
||||||
|
|
||||||
typ := connectorType(conn)
|
typ := connectorType(conn)
|
||||||
apiURL, _ := setting["host"].(string)
|
|
||||||
apiKey, _ := setting["key"].(string)
|
var apiURL, apiKey, model string
|
||||||
model, _ := setting["model"].(string)
|
if lc, ok := conn.(goullm.LLMConnector); ok {
|
||||||
|
apiURL = lc.GetURL()
|
||||||
|
apiKey = lc.GetKey()
|
||||||
|
model = lc.GetModel()
|
||||||
|
}
|
||||||
|
if apiURL == "" {
|
||||||
|
apiURL, _ = setting["host"].(string)
|
||||||
|
}
|
||||||
|
if apiKey == "" {
|
||||||
|
apiKey, _ = setting["key"].(string)
|
||||||
|
}
|
||||||
|
if model == "" {
|
||||||
|
model, _ = setting["model"].(string)
|
||||||
|
}
|
||||||
|
|
||||||
var models []ModelInfo
|
var models []ModelInfo
|
||||||
if model != "" {
|
if model != "" {
|
||||||
caps := capabilitiesFromSetting(setting)
|
var caps []string
|
||||||
|
if lc, ok := conn.(goullm.LLMConnector); ok {
|
||||||
|
if c := lc.GetCapabilities(); c != nil {
|
||||||
|
caps = capabilitiesFromCapabilities(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(caps) == 0 {
|
||||||
|
caps = capabilitiesFromSetting(setting)
|
||||||
|
}
|
||||||
models = []ModelInfo{{
|
models = []ModelInfo{{
|
||||||
ID: model,
|
ID: model,
|
||||||
Name: model,
|
Name: model,
|
||||||
|
|
@ -258,6 +297,48 @@ func providerFromConnector(id string, conn connector.Connector) Provider {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// capabilitiesFromCapabilities converts a typed Capabilities struct to a string slice.
|
||||||
|
func capabilitiesFromCapabilities(c *goullm.Capabilities) []string {
|
||||||
|
var out []string
|
||||||
|
if c.Streaming {
|
||||||
|
out = append(out, "streaming")
|
||||||
|
}
|
||||||
|
if c.ToolCalls {
|
||||||
|
out = append(out, "tool_calls")
|
||||||
|
}
|
||||||
|
if c.TemperatureAdjustable {
|
||||||
|
out = append(out, "temperature_adjustable")
|
||||||
|
}
|
||||||
|
if c.Vision != nil {
|
||||||
|
switch v := c.Vision.(type) {
|
||||||
|
case bool:
|
||||||
|
if v {
|
||||||
|
out = append(out, "vision")
|
||||||
|
}
|
||||||
|
case string:
|
||||||
|
if v != "" {
|
||||||
|
out = append(out, "vision")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if c.Audio {
|
||||||
|
out = append(out, "audio")
|
||||||
|
}
|
||||||
|
if c.STT {
|
||||||
|
out = append(out, "stt")
|
||||||
|
}
|
||||||
|
if c.Reasoning {
|
||||||
|
out = append(out, "reasoning")
|
||||||
|
}
|
||||||
|
if c.JSON {
|
||||||
|
out = append(out, "json")
|
||||||
|
}
|
||||||
|
if c.Multimodal {
|
||||||
|
out = append(out, "multimodal")
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
func connectorType(conn connector.Connector) string {
|
func connectorType(conn connector.Connector) string {
|
||||||
switch {
|
switch {
|
||||||
case conn.Is(6): // OPENAI
|
case conn.Is(6): // OPENAI
|
||||||
|
|
|
||||||
|
|
@ -29,10 +29,12 @@ type Provider struct {
|
||||||
// ModelInfo describes a single model within a provider.
|
// ModelInfo describes a single model within a provider.
|
||||||
// Fields align with the frontend ModelInfo interface.
|
// Fields align with the frontend ModelInfo interface.
|
||||||
type ModelInfo struct {
|
type ModelInfo struct {
|
||||||
ID string `json:"id" yaml:"id"`
|
ID string `json:"id" yaml:"id"`
|
||||||
Name string `json:"name" yaml:"name"`
|
Name string `json:"name" yaml:"name"`
|
||||||
Capabilities []string `json:"capabilities" yaml:"capabilities"`
|
Capabilities []string `json:"capabilities" yaml:"capabilities"`
|
||||||
Enabled bool `json:"enabled" yaml:"enabled"`
|
Enabled bool `json:"enabled" yaml:"enabled"`
|
||||||
|
MaxInputTokens int `json:"max_input_tokens,omitempty" yaml:"max_input_tokens,omitempty"`
|
||||||
|
MaxOutputTokens int `json:"max_output_tokens,omitempty" yaml:"max_output_tokens,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProviderOwner identifies who owns a provider.
|
// ProviderOwner identifies who owns a provider.
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ type OpenAI struct {
|
||||||
baseURL string
|
baseURL string
|
||||||
organization string
|
organization string
|
||||||
maxToken int
|
maxToken int
|
||||||
azure bool // Azure Credentials, "true" or "false" or ""
|
authMode string
|
||||||
}
|
}
|
||||||
|
|
||||||
// New create a new OpenAI instance by connector id
|
// New create a new OpenAI instance by connector id
|
||||||
|
|
@ -95,9 +95,9 @@ func NewOpenAI(setting map[string]interface{}) (*OpenAI, error) {
|
||||||
maxToken = v
|
maxToken = v
|
||||||
}
|
}
|
||||||
|
|
||||||
azure := false
|
authMode := ""
|
||||||
if v, ok := setting["azure"].(string); ok {
|
if v, ok := setting["auth_mode"].(string); ok {
|
||||||
azure = v == "true" || v == "1"
|
authMode = v
|
||||||
}
|
}
|
||||||
|
|
||||||
return &OpenAI{
|
return &OpenAI{
|
||||||
|
|
@ -107,7 +107,7 @@ func NewOpenAI(setting map[string]interface{}) (*OpenAI, error) {
|
||||||
baseURL: baseURL,
|
baseURL: baseURL,
|
||||||
organization: organization,
|
organization: organization,
|
||||||
maxToken: maxToken,
|
maxToken: maxToken,
|
||||||
azure: azure,
|
authMode: authMode,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -269,7 +269,7 @@ func (openai OpenAI) AudioTranscriptionsFile(filePath string, option map[string]
|
||||||
}
|
}
|
||||||
|
|
||||||
req := http.New(url)
|
req := http.New(url)
|
||||||
if openai.azure {
|
if openai.authMode == "api-key" {
|
||||||
req.WithHeader(map[string][]string{
|
req.WithHeader(map[string][]string{
|
||||||
"Content-Type": {"multipart/form-data"},
|
"Content-Type": {"multipart/form-data"},
|
||||||
"api-key": {openai.key},
|
"api-key": {openai.key},
|
||||||
|
|
@ -414,7 +414,7 @@ func (openai OpenAI) post(path string, payload map[string]interface{}) (interfac
|
||||||
payload["model"] = openai.model
|
payload["model"] = openai.model
|
||||||
|
|
||||||
req := http.New(url)
|
req := http.New(url)
|
||||||
if openai.azure {
|
if openai.authMode == "api-key" {
|
||||||
req.WithHeader(map[string][]string{
|
req.WithHeader(map[string][]string{
|
||||||
"Content-Type": {"application/json; charset=utf-8"},
|
"Content-Type": {"application/json; charset=utf-8"},
|
||||||
"api-key": {openai.key},
|
"api-key": {openai.key},
|
||||||
|
|
@ -438,7 +438,7 @@ func (openai OpenAI) postWithoutModel(path string, payload map[string]interface{
|
||||||
|
|
||||||
url := fmt.Sprintf("%s%s", openai.host, path)
|
url := fmt.Sprintf("%s%s", openai.host, path)
|
||||||
req := http.New(url)
|
req := http.New(url)
|
||||||
if openai.azure {
|
if openai.authMode == "api-key" {
|
||||||
req.WithHeader(map[string][]string{"api-key": {openai.key}})
|
req.WithHeader(map[string][]string{"api-key": {openai.key}})
|
||||||
} else {
|
} else {
|
||||||
req.WithHeader(map[string][]string{"Authorization": {fmt.Sprintf("Bearer %s", openai.key)}})
|
req.WithHeader(map[string][]string{"Authorization": {fmt.Sprintf("Bearer %s", openai.key)}})
|
||||||
|
|
@ -461,7 +461,7 @@ func (openai OpenAI) postFile(path string, files map[string][]byte, option map[s
|
||||||
|
|
||||||
req := http.New(url)
|
req := http.New(url)
|
||||||
|
|
||||||
if openai.azure {
|
if openai.authMode == "api-key" {
|
||||||
req.WithHeader(map[string][]string{
|
req.WithHeader(map[string][]string{
|
||||||
"Content-Type": {"multipart/form-data"},
|
"Content-Type": {"multipart/form-data"},
|
||||||
"api-key": {openai.key},
|
"api-key": {openai.key},
|
||||||
|
|
@ -496,7 +496,7 @@ func (openai OpenAI) postFileWithoutModel(path string, files map[string][]byte,
|
||||||
key := fmt.Sprintf("Bearer %s", openai.key)
|
key := fmt.Sprintf("Bearer %s", openai.key)
|
||||||
|
|
||||||
req := http.New(url).WithHeader(map[string][]string{"Authorization": {key}})
|
req := http.New(url).WithHeader(map[string][]string{"Authorization": {key}})
|
||||||
if openai.azure {
|
if openai.authMode == "api-key" {
|
||||||
req.WithHeader(map[string][]string{"api-key": {openai.key}})
|
req.WithHeader(map[string][]string{"api-key": {openai.key}})
|
||||||
} else {
|
} else {
|
||||||
req.WithHeader(map[string][]string{"Authorization": {fmt.Sprintf("Bearer %s", openai.key)}})
|
req.WithHeader(map[string][]string{"Authorization": {fmt.Sprintf("Bearer %s", openai.key)}})
|
||||||
|
|
@ -528,7 +528,7 @@ func (openai OpenAI) stream(ctx context.Context, path string, payload map[string
|
||||||
}
|
}
|
||||||
|
|
||||||
req := http.New(url)
|
req := http.New(url)
|
||||||
if openai.azure {
|
if openai.authMode == "api-key" {
|
||||||
req.WithHeader(map[string][]string{
|
req.WithHeader(map[string][]string{
|
||||||
"Content-Type": {"application/json; charset=utf-8"},
|
"Content-Type": {"application/json; charset=utf-8"},
|
||||||
"api-key": {openai.key},
|
"api-key": {openai.key},
|
||||||
|
|
|
||||||
|
|
@ -175,7 +175,7 @@ func ListAssistants(c *gin.Context) {
|
||||||
|
|
||||||
// Convert sandbox to boolean and filter built-in sensitive fields
|
// Convert sandbox to boolean and filter built-in sensitive fields
|
||||||
resp := map[string]interface{}{
|
resp := map[string]interface{}{
|
||||||
"data": AssistantsToResponse(result.Data),
|
"data": AssistantsToResponse(result.Data, authInfo),
|
||||||
"total": result.Total,
|
"total": result.Total,
|
||||||
"page": result.Page,
|
"page": result.Page,
|
||||||
"pagesize": result.PageSize,
|
"pagesize": result.PageSize,
|
||||||
|
|
@ -274,7 +274,7 @@ func GetAssistant(c *gin.Context) {
|
||||||
// Convert sandbox to boolean and filter built-in sensitive fields
|
// Convert sandbox to boolean and filter built-in sensitive fields
|
||||||
hasSandbox := assistant.Sandbox != nil
|
hasSandbox := assistant.Sandbox != nil
|
||||||
FilterBuiltInAssistant(assistant)
|
FilterBuiltInAssistant(assistant)
|
||||||
resp := AssistantToResponse(assistant, hasSandbox)
|
resp := AssistantToResponse(assistant, hasSandbox, authInfo)
|
||||||
|
|
||||||
// Return the result with standard response format
|
// Return the result with standard response format
|
||||||
response.RespondWithSuccess(c, response.StatusOK, resp)
|
response.RespondWithSuccess(c, response.StatusOK, resp)
|
||||||
|
|
@ -602,7 +602,11 @@ func GetAssistantInfo(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
response.RespondWithSuccess(c, response.StatusOK, ast.GetInfo(locale))
|
info := ast.GetInfo(locale)
|
||||||
|
resolved, raw := resolveConnectorForResponse(info.Connector, authInfo)
|
||||||
|
info.Connector = resolved
|
||||||
|
info.ConnectorRaw = raw
|
||||||
|
response.RespondWithSuccess(c, response.StatusOK, info)
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkAssistantPermission checks if the user has permission to access the assistant
|
// checkAssistantPermission checks if the user has permission to access the assistant
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,13 @@ package agent
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/yaoapp/gou/model"
|
"github.com/yaoapp/gou/model"
|
||||||
"github.com/yaoapp/xun/dbal/query"
|
"github.com/yaoapp/xun/dbal/query"
|
||||||
agenttypes "github.com/yaoapp/yao/agent/store/types"
|
agenttypes "github.com/yaoapp/yao/agent/store/types"
|
||||||
|
"github.com/yaoapp/yao/llmprovider"
|
||||||
"github.com/yaoapp/yao/openapi/oauth/authorized"
|
"github.com/yaoapp/yao/openapi/oauth/authorized"
|
||||||
"github.com/yaoapp/yao/openapi/oauth/types"
|
"github.com/yaoapp/yao/openapi/oauth/types"
|
||||||
)
|
)
|
||||||
|
|
@ -160,10 +162,49 @@ func FilterBuiltInAssistant(assistant *agenttypes.AssistantModel) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// resolveConnectorForResponse resolves a "use::" prefixed connector value
|
||||||
|
// to the actual connector ID for API responses.
|
||||||
|
// Returns (resolvedID, rawValue). rawValue is non-empty only when the original was a use:: prefix.
|
||||||
|
func resolveConnectorForResponse(connectorValue string, identity llmprovider.Identity) (string, string) {
|
||||||
|
if !strings.HasPrefix(connectorValue, "use::") {
|
||||||
|
return connectorValue, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
role := strings.TrimPrefix(connectorValue, "use::")
|
||||||
|
if role == "" {
|
||||||
|
role = "default"
|
||||||
|
}
|
||||||
|
|
||||||
|
if identity != nil && llmprovider.Global != nil {
|
||||||
|
if cid, err := llmprovider.Global.GetRoleBy(role, identity); err == nil && cid != "" {
|
||||||
|
return cid, connectorValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if llmprovider.Global != nil {
|
||||||
|
if cid, err := llmprovider.Global.GetRole(role); err == nil && cid != "" {
|
||||||
|
return cid, connectorValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return connectorValue, connectorValue
|
||||||
|
}
|
||||||
|
|
||||||
|
// applyConnectorResolve resolves the connector field in a response map.
|
||||||
|
func applyConnectorResolve(result map[string]interface{}, identity llmprovider.Identity) {
|
||||||
|
connVal, ok := result["connector"].(string)
|
||||||
|
if !ok || connVal == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resolved, raw := resolveConnectorForResponse(connVal, identity)
|
||||||
|
result["connector"] = resolved
|
||||||
|
if raw != "" {
|
||||||
|
result["connector_raw"] = raw
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// AssistantToResponse converts an AssistantModel to a response map,
|
// AssistantToResponse converts an AssistantModel to a response map,
|
||||||
// replacing the sandbox JSON object with a boolean indicating whether sandbox is configured.
|
// replacing the sandbox JSON object with a boolean indicating whether sandbox is configured.
|
||||||
// hasSandbox must be captured before FilterBuiltInAssistant clears the Sandbox field.
|
// hasSandbox must be captured before FilterBuiltInAssistant clears the Sandbox field.
|
||||||
func AssistantToResponse(assistant *agenttypes.AssistantModel, hasSandbox bool) map[string]interface{} {
|
func AssistantToResponse(assistant *agenttypes.AssistantModel, hasSandbox bool, identity llmprovider.Identity) map[string]interface{} {
|
||||||
if assistant == nil {
|
if assistant == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
@ -182,13 +223,14 @@ func AssistantToResponse(assistant *agenttypes.AssistantModel, hasSandbox bool)
|
||||||
if assistant.ComputerFilter != nil {
|
if assistant.ComputerFilter != nil {
|
||||||
result["computer_filter"] = assistant.ComputerFilter
|
result["computer_filter"] = assistant.ComputerFilter
|
||||||
}
|
}
|
||||||
|
applyConnectorResolve(result, identity)
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
// AssistantsToResponse converts a slice of AssistantModel to response maps,
|
// AssistantsToResponse converts a slice of AssistantModel to response maps,
|
||||||
// replacing sandbox with a boolean for each assistant.
|
// replacing sandbox with a boolean for each assistant.
|
||||||
// Captures sandbox state before filtering, then applies FilterBuiltInAssistant.
|
// Captures sandbox state before filtering, then applies FilterBuiltInAssistant.
|
||||||
func AssistantsToResponse(assistants []*agenttypes.AssistantModel) []map[string]interface{} {
|
func AssistantsToResponse(assistants []*agenttypes.AssistantModel, identity llmprovider.Identity) []map[string]interface{} {
|
||||||
if assistants == nil {
|
if assistants == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
@ -197,7 +239,7 @@ func AssistantsToResponse(assistants []*agenttypes.AssistantModel) []map[string]
|
||||||
for _, a := range assistants {
|
for _, a := range assistants {
|
||||||
hasSandbox := a.Sandbox != nil || a.IsSandbox
|
hasSandbox := a.Sandbox != nil || a.IsSandbox
|
||||||
FilterBuiltInAssistant(a)
|
FilterBuiltInAssistant(a)
|
||||||
result = append(result, AssistantToResponse(a, hasSandbox))
|
result = append(result, AssistantToResponse(a, hasSandbox, identity))
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
|
||||||
80
openapi/agent/filter_test.go
Normal file
80
openapi/agent/filter_test.go
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
package agent
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/yaoapp/yao/llmprovider"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestResolveConnectorForResponse_ExplicitID(t *testing.T) {
|
||||||
|
resolved, raw := resolveConnectorForResponse("some-connector-id", nil)
|
||||||
|
assert.Equal(t, "some-connector-id", resolved)
|
||||||
|
assert.Empty(t, raw, "non use:: prefix should not set raw value")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveConnectorForResponse_ExplicitIDWithModel(t *testing.T) {
|
||||||
|
resolved, raw := resolveConnectorForResponse("t123.openai:gpt-4o", nil)
|
||||||
|
assert.Equal(t, "t123.openai:gpt-4o", resolved)
|
||||||
|
assert.Empty(t, raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveConnectorForResponse_EmptyString(t *testing.T) {
|
||||||
|
resolved, raw := resolveConnectorForResponse("", nil)
|
||||||
|
assert.Empty(t, resolved)
|
||||||
|
assert.Empty(t, raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveConnectorForResponse_NilGlobal(t *testing.T) {
|
||||||
|
orig := llmprovider.Global
|
||||||
|
llmprovider.Global = nil
|
||||||
|
defer func() { llmprovider.Global = orig }()
|
||||||
|
|
||||||
|
resolved, raw := resolveConnectorForResponse("use::default", nil)
|
||||||
|
assert.Equal(t, "use::default", resolved, "should return original when Global is nil")
|
||||||
|
assert.Equal(t, "use::default", raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveConnectorForResponse_UseUnresolvable(t *testing.T) {
|
||||||
|
orig := llmprovider.Global
|
||||||
|
llmprovider.Global = nil
|
||||||
|
defer func() { llmprovider.Global = orig }()
|
||||||
|
|
||||||
|
resolved, raw := resolveConnectorForResponse("use::light", nil)
|
||||||
|
assert.Equal(t, "use::light", resolved, "unresolvable role returns original")
|
||||||
|
assert.Equal(t, "use::light", raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveConnectorForResponse_UseEmptyRole(t *testing.T) {
|
||||||
|
orig := llmprovider.Global
|
||||||
|
llmprovider.Global = nil
|
||||||
|
defer func() { llmprovider.Global = orig }()
|
||||||
|
|
||||||
|
resolved, raw := resolveConnectorForResponse("use::", nil)
|
||||||
|
assert.Equal(t, "use::", resolved, "empty role with nil Global returns original")
|
||||||
|
assert.Equal(t, "use::", raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApplyConnectorResolve_NoConnector(t *testing.T) {
|
||||||
|
result := map[string]interface{}{"name": "test"}
|
||||||
|
applyConnectorResolve(result, nil)
|
||||||
|
assert.Nil(t, result["connector_raw"], "should not add connector_raw when no connector")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApplyConnectorResolve_ExplicitConnector(t *testing.T) {
|
||||||
|
result := map[string]interface{}{"connector": "t123.openai:gpt-4o"}
|
||||||
|
applyConnectorResolve(result, nil)
|
||||||
|
assert.Equal(t, "t123.openai:gpt-4o", result["connector"])
|
||||||
|
assert.Nil(t, result["connector_raw"], "should not add connector_raw for explicit IDs")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApplyConnectorResolve_UsePrefix(t *testing.T) {
|
||||||
|
orig := llmprovider.Global
|
||||||
|
llmprovider.Global = nil
|
||||||
|
defer func() { llmprovider.Global = orig }()
|
||||||
|
|
||||||
|
result := map[string]interface{}{"connector": "use::default"}
|
||||||
|
applyConnectorResolve(result, nil)
|
||||||
|
assert.Equal(t, "use::default", result["connector"], "unresolvable returns original")
|
||||||
|
assert.Equal(t, "use::default", result["connector_raw"])
|
||||||
|
}
|
||||||
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"github.com/yaoapp/xun/dbal/query"
|
"github.com/yaoapp/xun/dbal/query"
|
||||||
"github.com/yaoapp/yao/agent/assistant"
|
"github.com/yaoapp/yao/agent/assistant"
|
||||||
storetypes "github.com/yaoapp/yao/agent/store/types"
|
storetypes "github.com/yaoapp/yao/agent/store/types"
|
||||||
|
"github.com/yaoapp/yao/llmprovider"
|
||||||
"github.com/yaoapp/yao/openapi/oauth/authorized"
|
"github.com/yaoapp/yao/openapi/oauth/authorized"
|
||||||
oauthtypes "github.com/yaoapp/yao/openapi/oauth/types"
|
oauthtypes "github.com/yaoapp/yao/openapi/oauth/types"
|
||||||
"github.com/yaoapp/yao/openapi/response"
|
"github.com/yaoapp/yao/openapi/response"
|
||||||
|
|
@ -378,6 +379,7 @@ func GetMessages(c *gin.Context) {
|
||||||
// Collect unique assistant IDs from messages and fetch their info
|
// Collect unique assistant IDs from messages and fetch their info
|
||||||
assistantIDs := collectAssistantIDs(messages)
|
assistantIDs := collectAssistantIDs(messages)
|
||||||
assistants := assistant.GetInfoByIDs(assistantIDs, locale)
|
assistants := assistant.GetInfoByIDs(assistantIDs, locale)
|
||||||
|
resolveAssistantInfoConnectors(assistants, authInfo)
|
||||||
|
|
||||||
response.RespondWithSuccess(c, response.StatusOK, gin.H{
|
response.RespondWithSuccess(c, response.StatusOK, gin.H{
|
||||||
"chat_id": chatID,
|
"chat_id": chatID,
|
||||||
|
|
@ -598,3 +600,32 @@ func checkChatPermission(chatStore storetypes.ChatStore, authInfo *oauthtypes.Au
|
||||||
|
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// resolveAssistantInfoConnectors resolves use:: prefixed connectors in a map of AssistantInfo.
|
||||||
|
func resolveAssistantInfoConnectors(infos map[string]*storetypes.AssistantInfo, identity llmprovider.Identity) {
|
||||||
|
for _, info := range infos {
|
||||||
|
if info == nil || !strings.HasPrefix(info.Connector, "use::") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
role := strings.TrimPrefix(info.Connector, "use::")
|
||||||
|
if role == "" {
|
||||||
|
role = "default"
|
||||||
|
}
|
||||||
|
raw := info.Connector
|
||||||
|
if identity != nil && llmprovider.Global != nil {
|
||||||
|
if cid, err := llmprovider.Global.GetRoleBy(role, identity); err == nil && cid != "" {
|
||||||
|
info.ConnectorRaw = raw
|
||||||
|
info.Connector = cid
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if llmprovider.Global != nil {
|
||||||
|
if cid, err := llmprovider.Global.GetRole(role); err == nil && cid != "" {
|
||||||
|
info.ConnectorRaw = raw
|
||||||
|
info.Connector = cid
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
info.ConnectorRaw = raw
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
package llm
|
package llm
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
|
@ -46,10 +45,7 @@ func listProviders(c *gin.Context) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("[llm/providers] filtersParam=%q\n", filtersParam)
|
|
||||||
|
|
||||||
info := authorized.GetInfo(c)
|
info := authorized.GetInfo(c)
|
||||||
fmt.Printf("[llm/providers] identity: UserID=%q TeamID=%q\n", info.GetUserID(), info.GetTeamID())
|
|
||||||
|
|
||||||
var opts []connector.Option
|
var opts []connector.Option
|
||||||
if llmprovider.Global != nil {
|
if llmprovider.Global != nil {
|
||||||
|
|
@ -57,10 +53,6 @@ func listProviders(c *gin.Context) {
|
||||||
} else {
|
} else {
|
||||||
opts = connector.AIConnectors
|
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 {
|
for _, opt := range opts {
|
||||||
var conn connector.Connector
|
var conn connector.Connector
|
||||||
|
|
@ -71,19 +63,16 @@ func listProviders(c *gin.Context) {
|
||||||
conn, err = connector.Select(opt.Value)
|
conn, err = connector.Select(opt.Value)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("[llm/providers] GetModel(%q) FAILED: %v\n", opt.Value, err)
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
connType := connectorType(conn)
|
connType := connectorType(conn)
|
||||||
if connType != "openai" && connType != "anthropic" {
|
if connType != "openai" && connType != "anthropic" {
|
||||||
fmt.Printf("[llm/providers] SKIP %q: type=%q (not openai/anthropic)\n", opt.Value, connType)
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
capabilities := getCapabilitiesFromConn(conn)
|
capabilities := getCapabilitiesFromConn(conn)
|
||||||
if len(filters) > 0 && !matchesFilters(capabilities, filters) {
|
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
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -96,7 +85,6 @@ func listProviders(c *gin.Context) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("[llm/providers] returning %d providers\n", len(allProviders))
|
|
||||||
response.RespondWithSuccess(c, response.StatusOK, allProviders)
|
response.RespondWithSuccess(c, response.StatusOK, allProviders)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
"name": "Entity Extractor",
|
"name": "Entity Extractor",
|
||||||
"description": "Extract entities and relationships",
|
"description": "Extract entities and relationships",
|
||||||
"type": "worker",
|
"type": "worker",
|
||||||
|
"connector": "use::light",
|
||||||
"uses": { "search": "disabled" },
|
"uses": { "search": "disabled" },
|
||||||
"options": { "max_tokens": 2000 }
|
"options": { "max_tokens": 2000 }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
"name": "Fetch Helper",
|
"name": "Fetch Helper",
|
||||||
"description": "Fetch and extract content from URLs",
|
"description": "Fetch and extract content from URLs",
|
||||||
"type": "worker",
|
"type": "worker",
|
||||||
|
"connector": "use::default",
|
||||||
"automated": true,
|
"automated": true,
|
||||||
"public": true,
|
"public": true,
|
||||||
"uses": { "search": "disabled" },
|
"uses": { "search": "disabled" },
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
"name": "Keyword Extractor",
|
"name": "Keyword Extractor",
|
||||||
"description": "Extract search keywords",
|
"description": "Extract search keywords",
|
||||||
"type": "worker",
|
"type": "worker",
|
||||||
|
"connector": "use::light",
|
||||||
"uses": { "search": "disabled" },
|
"uses": { "search": "disabled" },
|
||||||
"options": { "max_tokens": 500 }
|
"options": { "max_tokens": 500 }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
"name": "Reference Checker",
|
"name": "Reference Checker",
|
||||||
"description": "Check if references are needed",
|
"description": "Check if references are needed",
|
||||||
"type": "worker",
|
"type": "worker",
|
||||||
|
"connector": "use::light",
|
||||||
"uses": { "search": "disabled" },
|
"uses": { "search": "disabled" },
|
||||||
"options": { "max_tokens": 200 }
|
"options": { "max_tokens": 200 }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
"name": "Prompt Optimizer",
|
"name": "Prompt Optimizer",
|
||||||
"description": "Optimize prompts for better results",
|
"description": "Optimize prompts for better results",
|
||||||
"type": "worker",
|
"type": "worker",
|
||||||
|
"connector": "use::light",
|
||||||
"uses": { "search": "disabled" },
|
"uses": { "search": "disabled" },
|
||||||
"options": { }
|
"options": {}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
"name": "Query Builder",
|
"name": "Query Builder",
|
||||||
"description": "Build database queries",
|
"description": "Build database queries",
|
||||||
"type": "worker",
|
"type": "worker",
|
||||||
|
"connector": "use::default",
|
||||||
"automated": true,
|
"automated": true,
|
||||||
"public": true,
|
"public": true,
|
||||||
"uses": { "search": "disabled" },
|
"uses": { "search": "disabled" },
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
"name": "Robot Prompt Generator",
|
"name": "Robot Prompt Generator",
|
||||||
"description": "Generate system prompts for autonomous robots",
|
"description": "Generate system prompts for autonomous robots",
|
||||||
"type": "worker",
|
"type": "worker",
|
||||||
|
"connector": "use::default",
|
||||||
"uses": { "search": "disabled" },
|
"uses": { "search": "disabled" },
|
||||||
"options": { }
|
"options": {}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
"name": "Title Generator",
|
"name": "Title Generator",
|
||||||
"description": "Generate conversation titles",
|
"description": "Generate conversation titles",
|
||||||
"type": "worker",
|
"type": "worker",
|
||||||
|
"connector": "use::light",
|
||||||
"uses": { "search": "disabled" },
|
"uses": { "search": "disabled" },
|
||||||
"options": { }
|
"options": {}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
"name": "Vision Helper",
|
"name": "Vision Helper",
|
||||||
"description": "Analyze images when the main model doesn't support vision",
|
"description": "Analyze images when the main model doesn't support vision",
|
||||||
"type": "worker",
|
"type": "worker",
|
||||||
|
"connector": "use::vision",
|
||||||
"uses": { "search": "disabled" },
|
"uses": { "search": "disabled" },
|
||||||
"options": {}
|
"options": {}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue