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 (
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
|
|
@ -628,50 +627,40 @@ func (ast *Assistant) Stream(ctx *context.Context, inputMessages []context.Messa
|
|||
return finalResponse, nil
|
||||
}
|
||||
|
||||
// GetConnector get the connector object, capabilities, and error with priority:
|
||||
// opts.Connector > ast.Connector > GetRoleBy("default", identity) > GetRole("default") > error
|
||||
// GetConnector get the connector object, capabilities, and 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
|
||||
// Returns: (connector, 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 != "" {
|
||||
connectorID = opts[0].Connector
|
||||
cid = opts[0].Connector
|
||||
}
|
||||
|
||||
// Fallback to unified role resolution via llmprovider (team > user > system)
|
||||
if connectorID == "" && llmprovider.Global != nil {
|
||||
if ctx != nil && ctx.Authorized != nil {
|
||||
if cid, err := llmprovider.Global.GetRoleBy("default", ctx.Authorized); err == nil {
|
||||
connectorID = cid
|
||||
}
|
||||
}
|
||||
if connectorID == "" {
|
||||
if cid, err := llmprovider.Global.GetRole("default"); err == nil {
|
||||
connectorID = cid
|
||||
}
|
||||
}
|
||||
// Extract identity for role-based resolution
|
||||
var identity llmprovider.Identity
|
||||
if ctx != nil && ctx.Authorized != nil {
|
||||
identity = ctx.Authorized
|
||||
}
|
||||
|
||||
// Unified resolution: explicit connector / use:: prefix / empty → all handled
|
||||
conn, caps, err := llm.ResolveConnector(cid, identity)
|
||||
if err == nil {
|
||||
return conn, caps, nil
|
||||
}
|
||||
|
||||
// Legacy fallback
|
||||
if connectorID == "" {
|
||||
connectorID = defaultConnector
|
||||
if 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")
|
||||
}
|
||||
|
||||
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
|
||||
return nil, nil, fmt.Errorf("connector not specified")
|
||||
}
|
||||
|
||||
// Info get the assistant information
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ import (
|
|||
"github.com/yaoapp/yao/agent/i18n"
|
||||
store "github.com/yaoapp/yao/agent/store/types"
|
||||
"github.com/yaoapp/yao/data"
|
||||
"github.com/yaoapp/yao/llmprovider"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
|
|
@ -35,7 +34,13 @@ var systemAgents = []string{
|
|||
// SystemConfig holds the system agents connector configuration
|
||||
// This is set from agent.yml system block
|
||||
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
|
||||
QueryDSL string // Connector for __yao.querydsl agent
|
||||
Title string // Connector for __yao.title agent
|
||||
|
|
@ -43,8 +48,6 @@ type SystemConfig struct {
|
|||
RobotPrompt string // Connector for __yao.robot_prompt agent
|
||||
NeedSearch string // Connector for __yao.needsearch 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)
|
||||
|
|
@ -161,10 +164,9 @@ func loadSystemAgent(id, pathPrefix string) (*Assistant, error) {
|
|||
pkgData["type"] = "assistant"
|
||||
}
|
||||
|
||||
// Resolve connector for this system agent
|
||||
connectorID := resolveSystemConnector(id)
|
||||
if connectorID != "" {
|
||||
pkgData["connector"] = connectorID
|
||||
// Override connector only if agent.yml has an explicit per-agent setting
|
||||
if override := resolveSystemConnector(id); override != "" {
|
||||
pkgData["connector"] = override
|
||||
}
|
||||
|
||||
// Read prompts.yml from bindata (default prompts)
|
||||
|
|
@ -208,118 +210,34 @@ func loadSystemAgent(id, pathPrefix string) (*Assistant, error) {
|
|||
return loadMap(pkgData)
|
||||
}
|
||||
|
||||
// resolveSystemConnector resolves the connector for a system agent
|
||||
// Priority: specific agent config > system.default > llmprovider role > defaultConnector > fallback
|
||||
// resolveSystemConnector returns an explicit per-agent connector override from agent.yml.
|
||||
// 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 {
|
||||
// Try specific agent config first
|
||||
if systemConfig != nil {
|
||||
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
|
||||
}
|
||||
if systemConfig == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Try unified role resolution: strip __yao. prefix as role name
|
||||
if llmprovider.Global != nil {
|
||||
role := strings.TrimPrefix(agentID, "__yao.")
|
||||
if cid, err := llmprovider.Global.GetRole(role); err == nil && cid != "" {
|
||||
return cid
|
||||
}
|
||||
if cid, err := llmprovider.Global.GetRole("default"); err == nil && cid != "" {
|
||||
return cid
|
||||
}
|
||||
switch agentID {
|
||||
case "__yao.keyword":
|
||||
return systemConfig.Keyword
|
||||
case "__yao.querydsl":
|
||||
return systemConfig.QueryDSL
|
||||
case "__yao.title":
|
||||
return systemConfig.Title
|
||||
case "__yao.prompt":
|
||||
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
|
||||
}
|
||||
|
||||
// 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()
|
||||
return ""
|
||||
}
|
||||
|
||||
// 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"
|
||||
|
||||
"github.com/yaoapp/gou/connector"
|
||||
goullm "github.com/yaoapp/gou/llm"
|
||||
gouMCP "github.com/yaoapp/gou/mcp"
|
||||
mcpProcess "github.com/yaoapp/gou/mcp/process"
|
||||
"github.com/yaoapp/yao/agent/context"
|
||||
|
|
@ -266,30 +267,26 @@ func (ast *Assistant) buildSandboxOptions(ctx *context.Context, opts *context.Op
|
|||
execOpts.ConnectorType = "openai"
|
||||
}
|
||||
|
||||
// Extract standard fields via LLMConnector when available, fallback to Setting()
|
||||
setting := conn.Setting()
|
||||
if host, ok := setting["host"].(string); ok {
|
||||
execOpts.ConnectorHost = host
|
||||
}
|
||||
if key, ok := setting["key"].(string); ok {
|
||||
execOpts.ConnectorKey = key
|
||||
}
|
||||
if model, ok := setting["model"].(string); ok {
|
||||
execOpts.Model = model
|
||||
}
|
||||
|
||||
// Extract extra connector options (thinking, max_tokens, temperature, etc.)
|
||||
// These are backend-specific parameters that need to be passed through to the proxy
|
||||
connectorOptions := make(map[string]interface{})
|
||||
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
|
||||
if lc, ok := conn.(goullm.LLMConnector); ok {
|
||||
execOpts.ConnectorHost = lc.GetURL()
|
||||
execOpts.ConnectorKey = lc.GetKey()
|
||||
execOpts.Model = lc.GetModel()
|
||||
} else {
|
||||
if host, ok := setting["host"].(string); ok {
|
||||
execOpts.ConnectorHost = host
|
||||
}
|
||||
if key, ok := setting["key"].(string); ok {
|
||||
execOpts.ConnectorKey = key
|
||||
}
|
||||
if model, ok := setting["model"].(string); ok {
|
||||
execOpts.Model = model
|
||||
}
|
||||
}
|
||||
|
||||
// Whitelist-filter remaining settings for sandbox proxy options
|
||||
connectorOptions := connector.FilterRequestBodyParams(setting, conn)
|
||||
if len(connectorOptions) > 0 {
|
||||
execOpts.ConnectorOptions = connectorOptions
|
||||
ctx.Logger.Debug("Connector options extracted: %v", connectorOptions)
|
||||
|
|
|
|||
|
|
@ -20,12 +20,21 @@ func GetCapabilities(connectorID string) *goullm.Capabilities {
|
|||
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 {
|
||||
if conn == nil {
|
||||
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()
|
||||
if settings != nil {
|
||||
if caps, ok := settings["capabilities"]; ok {
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import (
|
|||
"github.com/yaoapp/yao/agent/llm/adapters"
|
||||
"github.com/yaoapp/yao/agent/llm/providers/base"
|
||||
"github.com/yaoapp/yao/agent/output/message"
|
||||
"github.com/yaoapp/yao/share"
|
||||
)
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// Get connector settings
|
||||
setting := p.Connector.Setting()
|
||||
host, ok := setting["host"].(string)
|
||||
if !ok || host == "" {
|
||||
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
|
||||
// Get connector settings via LLMConnector or fallback
|
||||
host, key, version, err := p.resolveHostKeyVersion()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 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).
|
||||
SetHeader("Content-Type", "application/json").
|
||||
SetHeader("x-api-key", key).
|
||||
SetHeader("anthropic-version", version).
|
||||
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
|
||||
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)
|
||||
}
|
||||
|
||||
// Get connector settings
|
||||
setting := p.Connector.Setting()
|
||||
host, ok := setting["host"].(string)
|
||||
if !ok || host == "" {
|
||||
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
|
||||
// Get connector settings via LLMConnector or fallback
|
||||
host, key, version, err := p.resolveHostKeyVersion()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
url := buildAPIURL(host, "/messages")
|
||||
|
||||
// Create HTTP request
|
||||
// Create HTTP request with auth headers
|
||||
req := http.New(url).
|
||||
SetHeader("Content-Type", "application/json").
|
||||
SetHeader("x-api-key", key).
|
||||
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)
|
||||
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 {
|
||||
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
|
||||
|
||||
// Temperature
|
||||
|
|
@ -1176,3 +1160,47 @@ func isRetryableError(err error) bool {
|
|||
|
||||
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
|
||||
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")
|
||||
}
|
||||
|
||||
// GetAPIKey gets the API key from connector settings
|
||||
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")
|
||||
}
|
||||
|
||||
// GetHost gets the host URL from connector settings
|
||||
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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"fmt"
|
||||
|
||||
"github.com/yaoapp/gou/connector"
|
||||
goullm "github.com/yaoapp/gou/llm"
|
||||
"github.com/yaoapp/yao/agent/context"
|
||||
"github.com/yaoapp/yao/agent/llm/providers/anthropic"
|
||||
"github.com/yaoapp/yao/agent/llm/providers/openai"
|
||||
|
|
@ -61,16 +62,23 @@ func DetectAPIFormat(conn connector.Connector) string {
|
|||
return "openai"
|
||||
}
|
||||
|
||||
// Check connector settings for host URL patterns as fallback
|
||||
settings := conn.Setting()
|
||||
if settings != nil {
|
||||
if host, ok := settings["host"].(string); ok {
|
||||
if contains(host, "anthropic.com") || contains(host, "api.kimi.com/coding") {
|
||||
return "anthropic"
|
||||
}
|
||||
if contains(host, "deepseek.com") {
|
||||
return "openai"
|
||||
}
|
||||
// Try LLMConnector for typed URL access, fall back to Setting() map
|
||||
var host string
|
||||
if lc, ok := conn.(goullm.LLMConnector); ok {
|
||||
host = lc.GetURL()
|
||||
}
|
||||
if host == "" {
|
||||
if settings := conn.Setting(); settings != nil {
|
||||
host, _ = settings["host"].(string)
|
||||
}
|
||||
}
|
||||
|
||||
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/providers/base"
|
||||
"github.com/yaoapp/yao/agent/output/message"
|
||||
"github.com/yaoapp/yao/share"
|
||||
"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)
|
||||
}
|
||||
|
||||
// Get connector settings
|
||||
setting := p.Connector.Setting()
|
||||
host, ok := setting["host"].(string)
|
||||
if !ok || host == "" {
|
||||
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")
|
||||
// Get connector settings via LLMConnector or fallback
|
||||
host, key, err := p.resolveHostKey()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Build URL
|
||||
|
|
@ -409,9 +404,9 @@ func (p *Provider) streamWithRetry(ctx *context.Context, messages []context.Mess
|
|||
// Create HTTP request with proxy support
|
||||
req := http.New(url).
|
||||
SetHeader("Content-Type", "application/json").
|
||||
SetHeader("Authorization", fmt.Sprintf("Bearer %s", key)).
|
||||
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
|
||||
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)
|
||||
}
|
||||
|
||||
// Get connector settings
|
||||
setting := p.Connector.Setting()
|
||||
host, ok := setting["host"].(string)
|
||||
if !ok || host == "" {
|
||||
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")
|
||||
// Get connector settings via LLMConnector or fallback
|
||||
host, key, err := p.resolveHostKey()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Build URL
|
||||
|
|
@ -940,8 +929,8 @@ func (p *Provider) postWithRetry(ctx *context.Context, messages []context.Messag
|
|||
// Create HTTP request with proxy support
|
||||
req := http.New(url).
|
||||
SetHeader("Content-Type", "application/json").
|
||||
SetHeader("Authorization", fmt.Sprintf("Bearer %s", key)).
|
||||
SetHeader("User-Agent", "YaoAgent/1.0 (+https://yaoagents.com)")
|
||||
SetHeader("User-Agent", "YaoEngine/"+share.VERSION)
|
||||
setAuthHeaders(req, p.Connector, key)
|
||||
|
||||
// Make request
|
||||
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+)
|
||||
// GPT-5 models only support max_completion_tokens (not max_tokens)
|
||||
if options.MaxCompletionTokens != nil {
|
||||
body["max_completion_tokens"] = *options.MaxCompletionTokens
|
||||
} else if options.MaxTokens != nil {
|
||||
// Fallback: convert MaxTokens to max_completion_tokens for compatibility
|
||||
body["max_completion_tokens"] = *options.MaxTokens
|
||||
if options.MaxCompletionTokens != nil || options.MaxTokens != nil {
|
||||
maxTokens := 0
|
||||
if options.MaxCompletionTokens != nil {
|
||||
maxTokens = *options.MaxCompletionTokens
|
||||
} 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 {
|
||||
|
|
@ -1289,3 +1286,37 @@ func isRetryableError(err error) bool {
|
|||
|
||||
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
|
||||
if agentDSL.System != nil {
|
||||
assistant.SetSystemConfig(&assistant.SystemConfig{
|
||||
Default: agentDSL.System.Default,
|
||||
Keyword: agentDSL.System.Keyword,
|
||||
QueryDSL: agentDSL.System.QueryDSL,
|
||||
Title: agentDSL.System.Title,
|
||||
Prompt: agentDSL.System.Prompt,
|
||||
NeedSearch: agentDSL.System.NeedSearch,
|
||||
Entity: agentDSL.System.Entity,
|
||||
Vision: agentDSL.System.Vision,
|
||||
Audio: agentDSL.System.Audio,
|
||||
Default: agentDSL.System.Default,
|
||||
Light: agentDSL.System.Light,
|
||||
Vision: agentDSL.System.Vision,
|
||||
Audio: agentDSL.System.Audio,
|
||||
Keyword: agentDSL.System.Keyword,
|
||||
QueryDSL: agentDSL.System.QueryDSL,
|
||||
Title: agentDSL.System.Title,
|
||||
Prompt: agentDSL.System.Prompt,
|
||||
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
|
||||
// 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 {
|
||||
roles := make(map[string]string)
|
||||
add := func(role, cid string) {
|
||||
|
|
@ -488,13 +491,7 @@ func buildSystemRoles(sys *types.System) map[string]string {
|
|||
}
|
||||
}
|
||||
add("default", sys.Default)
|
||||
add("keyword", sys.Keyword)
|
||||
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("light", sys.Light)
|
||||
add("vision", sys.Vision)
|
||||
add("audio", sys.Audio)
|
||||
return roles
|
||||
|
|
@ -506,6 +503,9 @@ func buildSystemRoles(sys *types.System) map[string]string {
|
|||
func resolveEnvStrings(setting *types.DSL) {
|
||||
if setting.System != nil {
|
||||
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.QueryDSL = helper.EnvString(setting.System.QueryDSL)
|
||||
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.NeedSearch = helper.EnvString(setting.System.NeedSearch)
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import (
|
|||
|
||||
"github.com/google/uuid"
|
||||
"github.com/yaoapp/gou/connector"
|
||||
goullm "github.com/yaoapp/gou/llm"
|
||||
"github.com/yaoapp/gou/store"
|
||||
"github.com/yaoapp/kun/str"
|
||||
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 {
|
||||
setting := req.Connector.Setting()
|
||||
host, _ := setting["host"].(string)
|
||||
key, _ := setting["key"].(string)
|
||||
model, _ := setting["model"].(string)
|
||||
|
||||
var host, key, 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)
|
||||
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 {
|
||||
thinkType, _ := thinking["type"].(string)
|
||||
switch thinkType {
|
||||
|
|
@ -416,6 +441,11 @@ func connectorHost(c connector.Connector) string {
|
|||
if c == nil {
|
||||
return ""
|
||||
}
|
||||
if lc, ok := c.(goullm.LLMConnector); ok {
|
||||
if u := lc.GetURL(); u != "" {
|
||||
return u
|
||||
}
|
||||
}
|
||||
host, _ := c.Setting()["host"].(string)
|
||||
return host
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/yaoapp/gou/connector"
|
||||
goullm "github.com/yaoapp/gou/llm"
|
||||
"github.com/yaoapp/kun/log"
|
||||
agentContext "github.com/yaoapp/yao/agent/context"
|
||||
"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 {
|
||||
Backend string `json:"backend"`
|
||||
Model string `json:"model"`
|
||||
APIKey string `json:"api_key"`
|
||||
Options map[string]interface{} `json:"options,omitempty"`
|
||||
Routes map[string]*a2oConnectorConfig `json:"routes,omitempty"`
|
||||
Backend string `json:"backend"`
|
||||
Model string `json:"model"`
|
||||
APIKey string `json:"api_key"`
|
||||
AuthMode string `json:"auth_mode,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 {
|
||||
|
|
@ -199,27 +202,33 @@ func buildSingleA2OConfig(conn connector.Connector) *a2oConnectorConfig {
|
|||
|
||||
cfg := &a2oConnectorConfig{}
|
||||
|
||||
if host, ok := settings["host"].(string); ok && host != "" {
|
||||
cfg.Backend = connector.BuildAPIURL(host, "/chat/completions")
|
||||
} else if proxy, ok := settings["proxy"].(string); ok && proxy != "" {
|
||||
cfg.Backend = connector.BuildAPIURL(proxy, "/chat/completions")
|
||||
}
|
||||
if model, ok := settings["model"].(string); ok && model != "" {
|
||||
cfg.Model = model
|
||||
}
|
||||
if key, ok := settings["key"].(string); ok && key != "" {
|
||||
cfg.APIKey = key
|
||||
}
|
||||
|
||||
extra := make(map[string]interface{})
|
||||
for k, v := range settings {
|
||||
switch k {
|
||||
case "host", "model", "key", "proxy", "type":
|
||||
continue
|
||||
default:
|
||||
extra[k] = v
|
||||
// Extract standard fields via LLMConnector methods when available
|
||||
if lc, ok := conn.(goullm.LLMConnector); ok {
|
||||
if url := lc.GetURL(); url != "" {
|
||||
cfg.Backend = connector.BuildAPIURL(url, "/chat/completions")
|
||||
}
|
||||
cfg.Model = lc.GetModel()
|
||||
cfg.APIKey = lc.GetKey()
|
||||
cfg.AuthMode = string(lc.GetAuthMode())
|
||||
if caps := lc.GetCapabilities(); caps != nil && caps.MaxOutputTokens > 0 {
|
||||
cfg.MaxOutputTokens = caps.MaxOutputTokens
|
||||
}
|
||||
} else {
|
||||
if host, ok := settings["host"].(string); ok && host != "" {
|
||||
cfg.Backend = connector.BuildAPIURL(host, "/chat/completions")
|
||||
} else if proxy, ok := settings["proxy"].(string); ok && proxy != "" {
|
||||
cfg.Backend = connector.BuildAPIURL(proxy, "/chat/completions")
|
||||
}
|
||||
if model, ok := settings["model"].(string); ok && model != "" {
|
||||
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 {
|
||||
cfg.Options = extra
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import (
|
|||
|
||||
"github.com/google/uuid"
|
||||
"github.com/yaoapp/gou/connector"
|
||||
goullm "github.com/yaoapp/gou/llm"
|
||||
"github.com/yaoapp/gou/store"
|
||||
"github.com/yaoapp/kun/str"
|
||||
agentContext "github.com/yaoapp/yao/agent/context"
|
||||
|
|
@ -400,6 +401,11 @@ func connectorHost(c connector.Connector) string {
|
|||
if c == nil {
|
||||
return ""
|
||||
}
|
||||
if lc, ok := c.(goullm.LLMConnector); ok {
|
||||
if u := lc.GetURL(); u != "" {
|
||||
return strings.TrimSpace(u)
|
||||
}
|
||||
}
|
||||
host, _ := c.Setting()["host"].(string)
|
||||
return strings.TrimSpace(host)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/yaoapp/gou/connector"
|
||||
goullm "github.com/yaoapp/gou/llm"
|
||||
"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.
|
||||
func buildProviderConfig(conn connector.Connector) (providerID string, cfg map[string]any, model string) {
|
||||
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{
|
||||
"apiKey": "{env:YAO_PROVIDER_KEY}",
|
||||
|
|
@ -123,6 +134,21 @@ func buildProviderConfig(conn connector.Connector) (providerID string, cfg map[s
|
|||
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{
|
||||
"npm": "@ai-sdk/openai-compatible",
|
||||
"options": opts,
|
||||
|
|
@ -309,6 +335,21 @@ func buildRoleProviderConfig(conn connector.Connector, envKeyPrefix string, moda
|
|||
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 host != "" {
|
||||
opts["baseURL"] = host
|
||||
|
|
|
|||
|
|
@ -212,6 +212,7 @@ type AssistantInfo struct {
|
|||
Avatar string `json:"avatar,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Connector string `json:"connector,omitempty"`
|
||||
ConnectorRaw string `json:"connector_raw,omitempty"`
|
||||
ConnectorOptions *ConnectorOptions `json:"connector_options,omitempty"`
|
||||
Modes []string `json:"modes,omitempty"`
|
||||
DefaultMode string `json:"default_mode,omitempty"`
|
||||
|
|
@ -432,7 +433,7 @@ type AssistantModel struct {
|
|||
Type string `json:"type,omitempty"` // Assistant Type, default is assistant
|
||||
Name string `json:"name,omitempty"` // Assistant Name
|
||||
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
|
||||
Path string `json:"path,omitempty"` // Assistant Path
|
||||
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
|
||||
// ===============================
|
||||
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
|
||||
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
|
||||
|
|
@ -101,8 +107,6 @@ type System struct {
|
|||
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
|
||||
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
|
||||
|
|
|
|||
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
|
||||
}
|
||||
|
||||
// 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 {
|
||||
if conn == nil {
|
||||
return defaultCaps()
|
||||
}
|
||||
|
||||
if lc, ok := conn.(goullm.LLMConnector); ok {
|
||||
if caps := lc.GetCapabilities(); caps != nil {
|
||||
return caps
|
||||
}
|
||||
}
|
||||
|
||||
settings := conn.Setting()
|
||||
if settings != nil {
|
||||
if caps, ok := settings["capabilities"]; ok {
|
||||
|
|
|
|||
|
|
@ -18,14 +18,20 @@
|
|||
default_models:
|
||||
- id: gpt-4o
|
||||
name: GPT-4o
|
||||
max_input_tokens: 128000
|
||||
max_output_tokens: 16384
|
||||
capabilities: [vision, tool_calls, streaming, json]
|
||||
enabled: true
|
||||
- id: gpt-4o-mini
|
||||
name: GPT-4o Mini
|
||||
max_input_tokens: 128000
|
||||
max_output_tokens: 16384
|
||||
capabilities: [tool_calls, streaming, json]
|
||||
enabled: true
|
||||
- id: o3-mini
|
||||
name: o3-mini
|
||||
max_input_tokens: 200000
|
||||
max_output_tokens: 100000
|
||||
capabilities: [tool_calls, streaming, reasoning]
|
||||
enabled: false
|
||||
|
||||
|
|
@ -37,10 +43,14 @@
|
|||
default_models:
|
||||
- id: claude-sonnet-4-20250514
|
||||
name: Claude Sonnet 4
|
||||
max_input_tokens: 200000
|
||||
max_output_tokens: 16000
|
||||
capabilities: [vision, tool_calls, streaming, reasoning]
|
||||
enabled: true
|
||||
- id: claude-haiku-3-5-20241022
|
||||
name: Claude Haiku 3.5
|
||||
max_input_tokens: 200000
|
||||
max_output_tokens: 8192
|
||||
capabilities: [tool_calls, streaming]
|
||||
enabled: true
|
||||
|
||||
|
|
|
|||
|
|
@ -141,9 +141,14 @@ func (r *Registry) extractConnectorID(target interface{}) string {
|
|||
|
||||
p, err := r.Get(rt.Provider, true)
|
||||
if err != nil {
|
||||
// Builtin providers: Key == ConnectorID
|
||||
if rt.Model != "" {
|
||||
return rt.Provider + ":" + rt.Model
|
||||
}
|
||||
return rt.Provider
|
||||
}
|
||||
if rt.Model != "" {
|
||||
return p.ConnectorID + ":" + rt.Model
|
||||
}
|
||||
return p.ConnectorID
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ func TestGetRole(t *testing.T) {
|
|||
|
||||
cid, err := r.GetRole("default")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, p.ConnectorID, cid)
|
||||
assert.Equal(t, p.ConnectorID+":gpt-4o", cid)
|
||||
}
|
||||
|
||||
func TestGetRoleByUser(t *testing.T) {
|
||||
|
|
@ -101,11 +101,11 @@ func TestGetRoleByUser(t *testing.T) {
|
|||
|
||||
cid, err := r.GetRoleByUser("default", "u1")
|
||||
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")
|
||||
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) {
|
||||
|
|
@ -130,7 +130,45 @@ func TestGetRoleByTeam(t *testing.T) {
|
|||
|
||||
cid, err := r.GetRoleByTeam("default", "t1")
|
||||
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) {
|
||||
|
|
@ -195,7 +233,7 @@ func TestProcessGetRole(t *testing.T) {
|
|||
proc := process.New("llmprovider.getrole", "default")
|
||||
result, err := proc.Exec()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, p.ConnectorID, result)
|
||||
assert.Equal(t, p.ConnectorID+":gpt-4o", result)
|
||||
}
|
||||
|
||||
func TestProcessListRoles(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -245,7 +245,7 @@ func TestGetRoleBy_TeamPriority(t *testing.T) {
|
|||
info := &oauthTypes.AuthorizedInfo{UserID: "u1", TeamID: "grb-t1"}
|
||||
cid, err := r.GetRoleBy("default", info)
|
||||
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) {
|
||||
|
|
@ -272,7 +272,7 @@ func TestGetRoleBy_UserFallback(t *testing.T) {
|
|||
info := &oauthTypes.AuthorizedInfo{UserID: "grbu-u1"}
|
||||
cid, err := r.GetRoleBy("default", info)
|
||||
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) {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import (
|
|||
"fmt"
|
||||
|
||||
"github.com/yaoapp/gou/connector"
|
||||
goullm "github.com/yaoapp/gou/llm"
|
||||
)
|
||||
|
||||
// connectorID builds the runtime ID for registering into connector.Connectors.
|
||||
|
|
@ -51,6 +52,12 @@ func marshalDSL(p *Provider) ([]byte, error) {
|
|||
"label": p.Name,
|
||||
"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)
|
||||
}
|
||||
|
||||
|
|
@ -140,7 +147,7 @@ func ensureModelConnector(p *Provider, m *ModelInfo) error {
|
|||
|
||||
// marshalModelDSL builds a connector DSL for a specific model within a provider.
|
||||
func marshalModelDSL(p *Provider, m *ModelInfo) ([]byte, error) {
|
||||
caps := make(map[string]bool)
|
||||
caps := make(map[string]interface{})
|
||||
for _, c := range m.Capabilities {
|
||||
caps[c] = true
|
||||
}
|
||||
|
|
@ -152,6 +159,12 @@ func marshalModelDSL(p *Provider, m *ModelInfo) ([]byte, error) {
|
|||
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{}{
|
||||
"host": p.APIURL,
|
||||
|
|
@ -172,6 +185,11 @@ func marshalModelDSL(p *Provider, m *ModelInfo) ([]byte, error) {
|
|||
"label": name,
|
||||
"options": opts,
|
||||
}
|
||||
|
||||
if p.PresetKey == "azure" {
|
||||
dsl["auth_mode"] = "api-key"
|
||||
}
|
||||
|
||||
return json.Marshal(dsl)
|
||||
}
|
||||
|
||||
|
|
@ -228,13 +246,34 @@ func providerFromConnector(id string, conn connector.Connector) Provider {
|
|||
}
|
||||
|
||||
typ := connectorType(conn)
|
||||
apiURL, _ := setting["host"].(string)
|
||||
apiKey, _ := setting["key"].(string)
|
||||
model, _ := setting["model"].(string)
|
||||
|
||||
var apiURL, apiKey, 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
|
||||
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{{
|
||||
ID: 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 {
|
||||
switch {
|
||||
case conn.Is(6): // OPENAI
|
||||
|
|
|
|||
|
|
@ -29,10 +29,12 @@ type Provider struct {
|
|||
// ModelInfo describes a single model within a provider.
|
||||
// Fields align with the frontend ModelInfo interface.
|
||||
type ModelInfo struct {
|
||||
ID string `json:"id" yaml:"id"`
|
||||
Name string `json:"name" yaml:"name"`
|
||||
Capabilities []string `json:"capabilities" yaml:"capabilities"`
|
||||
Enabled bool `json:"enabled" yaml:"enabled"`
|
||||
ID string `json:"id" yaml:"id"`
|
||||
Name string `json:"name" yaml:"name"`
|
||||
Capabilities []string `json:"capabilities" yaml:"capabilities"`
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ type OpenAI struct {
|
|||
baseURL string
|
||||
organization string
|
||||
maxToken int
|
||||
azure bool // Azure Credentials, "true" or "false" or ""
|
||||
authMode string
|
||||
}
|
||||
|
||||
// New create a new OpenAI instance by connector id
|
||||
|
|
@ -95,9 +95,9 @@ func NewOpenAI(setting map[string]interface{}) (*OpenAI, error) {
|
|||
maxToken = v
|
||||
}
|
||||
|
||||
azure := false
|
||||
if v, ok := setting["azure"].(string); ok {
|
||||
azure = v == "true" || v == "1"
|
||||
authMode := ""
|
||||
if v, ok := setting["auth_mode"].(string); ok {
|
||||
authMode = v
|
||||
}
|
||||
|
||||
return &OpenAI{
|
||||
|
|
@ -107,7 +107,7 @@ func NewOpenAI(setting map[string]interface{}) (*OpenAI, error) {
|
|||
baseURL: baseURL,
|
||||
organization: organization,
|
||||
maxToken: maxToken,
|
||||
azure: azure,
|
||||
authMode: authMode,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
|
@ -269,7 +269,7 @@ func (openai OpenAI) AudioTranscriptionsFile(filePath string, option map[string]
|
|||
}
|
||||
|
||||
req := http.New(url)
|
||||
if openai.azure {
|
||||
if openai.authMode == "api-key" {
|
||||
req.WithHeader(map[string][]string{
|
||||
"Content-Type": {"multipart/form-data"},
|
||||
"api-key": {openai.key},
|
||||
|
|
@ -414,7 +414,7 @@ func (openai OpenAI) post(path string, payload map[string]interface{}) (interfac
|
|||
payload["model"] = openai.model
|
||||
|
||||
req := http.New(url)
|
||||
if openai.azure {
|
||||
if openai.authMode == "api-key" {
|
||||
req.WithHeader(map[string][]string{
|
||||
"Content-Type": {"application/json; charset=utf-8"},
|
||||
"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)
|
||||
req := http.New(url)
|
||||
if openai.azure {
|
||||
if openai.authMode == "api-key" {
|
||||
req.WithHeader(map[string][]string{"api-key": {openai.key}})
|
||||
} else {
|
||||
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)
|
||||
|
||||
if openai.azure {
|
||||
if openai.authMode == "api-key" {
|
||||
req.WithHeader(map[string][]string{
|
||||
"Content-Type": {"multipart/form-data"},
|
||||
"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)
|
||||
|
||||
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}})
|
||||
} else {
|
||||
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)
|
||||
if openai.azure {
|
||||
if openai.authMode == "api-key" {
|
||||
req.WithHeader(map[string][]string{
|
||||
"Content-Type": {"application/json; charset=utf-8"},
|
||||
"api-key": {openai.key},
|
||||
|
|
|
|||
|
|
@ -175,7 +175,7 @@ func ListAssistants(c *gin.Context) {
|
|||
|
||||
// Convert sandbox to boolean and filter built-in sensitive fields
|
||||
resp := map[string]interface{}{
|
||||
"data": AssistantsToResponse(result.Data),
|
||||
"data": AssistantsToResponse(result.Data, authInfo),
|
||||
"total": result.Total,
|
||||
"page": result.Page,
|
||||
"pagesize": result.PageSize,
|
||||
|
|
@ -274,7 +274,7 @@ func GetAssistant(c *gin.Context) {
|
|||
// Convert sandbox to boolean and filter built-in sensitive fields
|
||||
hasSandbox := assistant.Sandbox != nil
|
||||
FilterBuiltInAssistant(assistant)
|
||||
resp := AssistantToResponse(assistant, hasSandbox)
|
||||
resp := AssistantToResponse(assistant, hasSandbox, authInfo)
|
||||
|
||||
// Return the result with standard response format
|
||||
response.RespondWithSuccess(c, response.StatusOK, resp)
|
||||
|
|
@ -602,7 +602,11 @@ func GetAssistantInfo(c *gin.Context) {
|
|||
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
|
||||
|
|
|
|||
|
|
@ -2,11 +2,13 @@ package agent
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/yaoapp/gou/model"
|
||||
"github.com/yaoapp/xun/dbal/query"
|
||||
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/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,
|
||||
// replacing the sandbox JSON object with a boolean indicating whether sandbox is configured.
|
||||
// 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 {
|
||||
return nil
|
||||
}
|
||||
|
|
@ -182,13 +223,14 @@ func AssistantToResponse(assistant *agenttypes.AssistantModel, hasSandbox bool)
|
|||
if assistant.ComputerFilter != nil {
|
||||
result["computer_filter"] = assistant.ComputerFilter
|
||||
}
|
||||
applyConnectorResolve(result, identity)
|
||||
return result
|
||||
}
|
||||
|
||||
// AssistantsToResponse converts a slice of AssistantModel to response maps,
|
||||
// replacing sandbox with a boolean for each assistant.
|
||||
// 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 {
|
||||
return nil
|
||||
}
|
||||
|
|
@ -197,7 +239,7 @@ func AssistantsToResponse(assistants []*agenttypes.AssistantModel) []map[string]
|
|||
for _, a := range assistants {
|
||||
hasSandbox := a.Sandbox != nil || a.IsSandbox
|
||||
FilterBuiltInAssistant(a)
|
||||
result = append(result, AssistantToResponse(a, hasSandbox))
|
||||
result = append(result, AssistantToResponse(a, hasSandbox, identity))
|
||||
}
|
||||
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/yao/agent/assistant"
|
||||
storetypes "github.com/yaoapp/yao/agent/store/types"
|
||||
"github.com/yaoapp/yao/llmprovider"
|
||||
"github.com/yaoapp/yao/openapi/oauth/authorized"
|
||||
oauthtypes "github.com/yaoapp/yao/openapi/oauth/types"
|
||||
"github.com/yaoapp/yao/openapi/response"
|
||||
|
|
@ -378,6 +379,7 @@ func GetMessages(c *gin.Context) {
|
|||
// Collect unique assistant IDs from messages and fetch their info
|
||||
assistantIDs := collectAssistantIDs(messages)
|
||||
assistants := assistant.GetInfoByIDs(assistantIDs, locale)
|
||||
resolveAssistantInfoConnectors(assistants, authInfo)
|
||||
|
||||
response.RespondWithSuccess(c, response.StatusOK, gin.H{
|
||||
"chat_id": chatID,
|
||||
|
|
@ -598,3 +600,32 @@ func checkChatPermission(chatStore storetypes.ChatStore, authInfo *oauthtypes.Au
|
|||
|
||||
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
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"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)
|
||||
fmt.Printf("[llm/providers] identity: UserID=%q TeamID=%q\n", info.GetUserID(), info.GetTeamID())
|
||||
|
||||
var opts []connector.Option
|
||||
if llmprovider.Global != nil {
|
||||
|
|
@ -57,10 +53,6 @@ func listProviders(c *gin.Context) {
|
|||
} else {
|
||||
opts = connector.AIConnectors
|
||||
}
|
||||
fmt.Printf("[llm/providers] ListModelsBy returned %d options\n", len(opts))
|
||||
for i, o := range opts {
|
||||
fmt.Printf("[llm/providers] [%d] label=%q value=%q\n", i, o.Label, o.Value)
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
var conn connector.Connector
|
||||
|
|
@ -71,19 +63,16 @@ func listProviders(c *gin.Context) {
|
|||
conn, err = connector.Select(opt.Value)
|
||||
}
|
||||
if err != nil {
|
||||
fmt.Printf("[llm/providers] GetModel(%q) FAILED: %v\n", opt.Value, err)
|
||||
continue
|
||||
}
|
||||
|
||||
connType := connectorType(conn)
|
||||
if connType != "openai" && connType != "anthropic" {
|
||||
fmt.Printf("[llm/providers] SKIP %q: type=%q (not openai/anthropic)\n", opt.Value, connType)
|
||||
continue
|
||||
}
|
||||
|
||||
capabilities := getCapabilitiesFromConn(conn)
|
||||
if len(filters) > 0 && !matchesFilters(capabilities, filters) {
|
||||
fmt.Printf("[llm/providers] SKIP %q: caps filter %v not matched (streaming=%v)\n", opt.Value, filters, capabilities["streaming"])
|
||||
continue
|
||||
}
|
||||
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
"name": "Entity Extractor",
|
||||
"description": "Extract entities and relationships",
|
||||
"type": "worker",
|
||||
"connector": "use::light",
|
||||
"uses": { "search": "disabled" },
|
||||
"options": { "max_tokens": 2000 }
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
"name": "Fetch Helper",
|
||||
"description": "Fetch and extract content from URLs",
|
||||
"type": "worker",
|
||||
"connector": "use::default",
|
||||
"automated": true,
|
||||
"public": true,
|
||||
"uses": { "search": "disabled" },
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
"name": "Keyword Extractor",
|
||||
"description": "Extract search keywords",
|
||||
"type": "worker",
|
||||
"connector": "use::light",
|
||||
"uses": { "search": "disabled" },
|
||||
"options": { "max_tokens": 500 }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
"name": "Reference Checker",
|
||||
"description": "Check if references are needed",
|
||||
"type": "worker",
|
||||
"connector": "use::light",
|
||||
"uses": { "search": "disabled" },
|
||||
"options": { "max_tokens": 200 }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
"name": "Prompt Optimizer",
|
||||
"description": "Optimize prompts for better results",
|
||||
"type": "worker",
|
||||
"connector": "use::light",
|
||||
"uses": { "search": "disabled" },
|
||||
"options": { }
|
||||
"options": {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
"name": "Query Builder",
|
||||
"description": "Build database queries",
|
||||
"type": "worker",
|
||||
"connector": "use::default",
|
||||
"automated": true,
|
||||
"public": true,
|
||||
"uses": { "search": "disabled" },
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
"name": "Robot Prompt Generator",
|
||||
"description": "Generate system prompts for autonomous robots",
|
||||
"type": "worker",
|
||||
"connector": "use::default",
|
||||
"uses": { "search": "disabled" },
|
||||
"options": { }
|
||||
"options": {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
"name": "Title Generator",
|
||||
"description": "Generate conversation titles",
|
||||
"type": "worker",
|
||||
"connector": "use::light",
|
||||
"uses": { "search": "disabled" },
|
||||
"options": { }
|
||||
"options": {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
"name": "Vision Helper",
|
||||
"description": "Analyze images when the main model doesn't support vision",
|
||||
"type": "worker",
|
||||
"connector": "use::vision",
|
||||
"uses": { "search": "disabled" },
|
||||
"options": {}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue