Refactor model capabilities to use OpenAI struct

- Updated the model capabilities throughout the agent to utilize the new gouOpenAI.Capabilities struct instead of the previous ModelCapabilities.
- Adjusted related methods and types to ensure compatibility with the new capabilities structure, enhancing clarity and maintainability.
- Improved context handling and message processing by directly integrating OpenAI capabilities, streamlining the overall architecture.
This commit is contained in:
Max 2025-12-02 11:33:09 +08:00
parent 7e8b2d8d9f
commit 3dd63e5530
21 changed files with 244 additions and 363 deletions

View file

@ -6,6 +6,7 @@ import (
jsoniter "github.com/json-iterator/go"
"github.com/yaoapp/gou/connector"
"github.com/yaoapp/gou/connector/openai"
"github.com/yaoapp/kun/log"
"github.com/yaoapp/yao/agent/assistant/handlers"
"github.com/yaoapp/yao/agent/context"
@ -303,7 +304,7 @@ func (ast *Assistant) Stream(ctx *context.Context, inputMessages []context.Messa
// GetConnector get the connector object, capabilities, and error with priority: createResponse > ctx > ast
// Note: createResponse.Connector is already applied to ctx.Connector by applyContextAdjustments in create.go
// Returns: (connector, capabilities, error)
func (ast *Assistant) GetConnector(ctx *context.Context) (connector.Connector, *context.ModelCapabilities, error) {
func (ast *Assistant) GetConnector(ctx *context.Context) (connector.Connector, *openai.Capabilities, error) {
// Determine connector ID with priority
connectorID := ast.Connector
if ctx.Connector != "" {
@ -328,62 +329,27 @@ func (ast *Assistant) GetConnector(ctx *context.Context) (connector.Connector, *
}
// getConnectorCapabilities get the capabilities of a connector from settings
func (ast *Assistant) getConnectorCapabilities(connectorID string) *context.ModelCapabilities {
// Initialize with default capabilities (all disabled)
falseVal := false
capabilities := &context.ModelCapabilities{
Vision: falseVal,
ToolCalls: &falseVal,
Audio: &falseVal,
Reasoning: &falseVal,
Streaming: &falseVal,
}
func (ast *Assistant) getConnectorCapabilities(connectorID string) *openai.Capabilities {
// Get model capabilities from global configuration
modelCaps, exists := modelCapabilities[connectorID]
if !exists {
// Return default capabilities if model not found in configuration
return capabilities
falseVal := false
return &openai.Capabilities{
Vision: falseVal,
ToolCalls: false,
Audio: false,
Reasoning: false,
Streaming: false,
JSON: false,
Multimodal: false,
TemperatureAdjustable: true, // Default to true for non-reasoning models
}
}
// Update capabilities based on model configuration
// Vision can be bool or string (VisionFormat)
if modelCaps.Vision != nil {
capabilities.Vision = modelCaps.Vision
}
// Handle both Tools (deprecated) and ToolCalls
if modelCaps.ToolCalls || modelCaps.Tools {
v := true
capabilities.ToolCalls = &v
}
if modelCaps.Audio {
v := true
capabilities.Audio = &v
}
if modelCaps.Reasoning {
v := true
capabilities.Reasoning = &v
}
if modelCaps.Streaming {
v := true
capabilities.Streaming = &v
}
if modelCaps.JSON {
v := true
capabilities.JSON = &v
}
if modelCaps.Multimodal {
v := true
capabilities.Multimodal = &v
}
return capabilities
// Return capabilities directly
// Note: TemperatureAdjustable is automatically set in connector.Setting() based on Reasoning flag
return &modelCaps
}
// Info get the assistant information

View file

@ -11,6 +11,7 @@ import (
jsoniter "github.com/json-iterator/go"
"github.com/spf13/cast"
"github.com/yaoapp/gou/application"
gouOpenAI "github.com/yaoapp/gou/connector/openai"
"github.com/yaoapp/gou/fs"
v8 "github.com/yaoapp/gou/runtime/v8"
"github.com/yaoapp/yao/agent/assistant/hook"
@ -26,7 +27,7 @@ import (
var loaded = NewCache(200) // 200 is the default capacity
var storage store.Store = nil
var search interface{} = nil
var modelCapabilities map[string]ModelCapabilities = map[string]ModelCapabilities{}
var modelCapabilities map[string]gouOpenAI.Capabilities = map[string]gouOpenAI.Capabilities{}
var defaultConnector string = "" // default connector
var globalUses *context.Uses = nil // global uses configuration from agent.yml
@ -131,7 +132,7 @@ func SetStorage(s store.Store) {
}
// SetModelCapabilities set the model capabilities configuration
func SetModelCapabilities(capabilities map[string]ModelCapabilities) {
func SetModelCapabilities(capabilities map[string]gouOpenAI.Capabilities) {
modelCapabilities = capabilities
}

View file

@ -3,6 +3,7 @@ package assistant
import (
"fmt"
"github.com/yaoapp/gou/connector/openai"
"github.com/yaoapp/kun/log"
"github.com/yaoapp/yao/agent/context"
"github.com/yaoapp/yao/agent/i18n"
@ -48,7 +49,7 @@ func (ast *Assistant) traceCreateHook(agentNode types.Node, createResponse *cont
}
// traceConnectorCapabilities logs the connector capabilities to the agent trace node
func (ast *Assistant) traceConnectorCapabilities(agentNode types.Node, capabilities *context.ModelCapabilities) {
func (ast *Assistant) traceConnectorCapabilities(agentNode types.Node, capabilities *openai.Capabilities) {
if agentNode == nil {
return
}

View file

@ -40,19 +40,6 @@ type Assistant struct {
// toolCalls bool // Whether this assistant supports tool_calls
}
// ModelCapabilities defines the capabilities of a language model
// This configuration is loaded from agent/models.yml
type ModelCapabilities struct {
Vision interface{} `json:"vision,omitempty" yaml:"vision,omitempty"` // Supports vision/image input: bool or VisionFormat string ("openai", "claude"/"base64", "default")
Tools bool `json:"tools,omitempty" yaml:"tools,omitempty"` // Supports tool/function calling (deprecated, use ToolCalls)
ToolCalls bool `json:"tool_calls,omitempty" yaml:"tool_calls,omitempty"` // Supports tool/function calling
Audio bool `json:"audio,omitempty" yaml:"audio,omitempty"` // Supports audio input/output
Reasoning bool `json:"reasoning,omitempty" yaml:"reasoning,omitempty"` // Supports reasoning/thinking mode (o1, DeepSeek R1)
Streaming bool `json:"streaming,omitempty" yaml:"streaming,omitempty"` // Supports streaming responses
JSON bool `json:"json,omitempty" yaml:"json,omitempty"` // Supports JSON mode
Multimodal bool `json:"multimodal,omitempty" yaml:"multimodal,omitempty"` // Supports multimodal input
}
// VisionCapableModels list of LLM models that support vision capabilities
var VisionCapableModels = map[string]bool{
// OpenAI Models

View file

@ -3,6 +3,7 @@ package context
import (
"time"
"github.com/yaoapp/gou/connector/openai"
"github.com/yaoapp/yao/agent/output"
"github.com/yaoapp/yao/agent/output/message"
)
@ -290,18 +291,10 @@ func (ctx *Context) getOutput() (*output.Output, error) {
Accept: string(ctx.Accept),
}
// Convert ModelCapabilities to message.ModelCapabilities
// Set ModelCapabilities (now using openai.Capabilities directly)
if ctx.Capabilities != nil {
options.Capabilities = &message.ModelCapabilities{
Vision: ctx.Capabilities.Vision,
ToolCalls: ctx.Capabilities.ToolCalls,
Audio: ctx.Capabilities.Audio,
Reasoning: ctx.Capabilities.Reasoning,
Streaming: ctx.Capabilities.Streaming,
JSON: ctx.Capabilities.JSON,
Multimodal: ctx.Capabilities.Multimodal,
TemperatureAdjustable: ctx.Capabilities.TemperatureAdjustable,
}
caps := openai.Capabilities(*ctx.Capabilities)
options.Capabilities = &caps
}
var err error

View file

@ -5,6 +5,7 @@ import (
"sync"
"time"
"github.com/yaoapp/gou/connector/openai"
"github.com/yaoapp/gou/plan"
"github.com/yaoapp/gou/store"
"github.com/yaoapp/yao/agent/output"
@ -238,7 +239,7 @@ type Context struct {
Skip *Skip `json:"skip,omitempty"` // Skip configuration (history, trace, etc.), nil means don't skip anything
// Model capabilities (set by assistant, used by output adapters)
Capabilities *ModelCapabilities `json:"-"` // Model capabilities for the current connector
Capabilities *openai.Capabilities `json:"-"` // Model capabilities for the current connector
// Interrupt control (all interrupt-related logic is encapsulated in InterruptController)
Interrupt *InterruptController `json:"-"` // Interrupt controller for handling user interrupts during streaming

View file

@ -1,6 +1,9 @@
package context
import "github.com/yaoapp/yao/agent/output/message"
import (
"github.com/yaoapp/gou/connector/openai"
"github.com/yaoapp/yao/agent/output/message"
)
// Uses represents the wrapper configurations for assistant
// Used to specify which assistant or MCP server to use for vision, audio, search, and fetch operations
@ -28,17 +31,13 @@ const (
VisionFormatDefault VisionFormat = "default"
)
// ModelCapabilities defines the capabilities of a language model
// Used by LLM to select appropriate provider and validate requests
type ModelCapabilities message.ModelCapabilities
// GetVisionSupport returns whether vision is supported and the format
func (m *ModelCapabilities) GetVisionSupport() (bool, VisionFormat) {
if m == nil || m.Vision == nil {
func GetVisionSupport(cap *openai.Capabilities) (bool, VisionFormat) {
if cap == nil || cap.Vision == nil {
return false, VisionFormatNone
}
switch v := m.Vision.(type) {
switch v := cap.Vision.(type) {
case bool:
// Legacy bool format
return v, VisionFormatDefault
@ -65,7 +64,7 @@ func (m *ModelCapabilities) GetVisionSupport() (bool, VisionFormat) {
type CompletionOptions struct {
// Model capabilities (used by LLM to select appropriate provider)
// nil means capabilities are not specified/checked
Capabilities *ModelCapabilities `json:"capabilities,omitempty"`
Capabilities *openai.Capabilities `json:"capabilities,omitempty"`
// User-specified tools for vision, audio, search, and fetch processing
Uses *Uses `json:"uses,omitempty"`

View file

@ -1,6 +1,7 @@
package adapters
import (
"github.com/yaoapp/gou/connector/openai"
"github.com/yaoapp/yao/agent/context"
)
@ -28,7 +29,7 @@ type ReasoningAdapter struct {
// NewReasoningAdapter creates a new reasoning adapter
// If cap.TemperatureAdjustable is provided, it overrides the default behavior
func NewReasoningAdapter(format ReasoningFormat, cap *context.ModelCapabilities) *ReasoningAdapter {
func NewReasoningAdapter(format ReasoningFormat, cap *openai.Capabilities) *ReasoningAdapter {
supportsEffort := false
supportsTemperature := true
@ -49,8 +50,8 @@ func NewReasoningAdapter(format ReasoningFormat, cap *context.ModelCapabilities)
}
// Override with explicit capability if provided
if cap != nil && cap.TemperatureAdjustable != nil {
supportsTemperature = *cap.TemperatureAdjustable
if cap != nil {
supportsTemperature = cap.TemperatureAdjustable
}
return &ReasoningAdapter{

View file

@ -4,6 +4,7 @@ import (
"fmt"
"github.com/yaoapp/gou/connector"
"github.com/yaoapp/gou/connector/openai"
"github.com/yaoapp/yao/agent/context"
)
@ -11,11 +12,11 @@ import (
// Provides common functionality for all LLM providers
type Provider struct {
Connector connector.Connector
Capabilities *context.ModelCapabilities
Capabilities *openai.Capabilities
}
// NewProvider create a new base provider
func NewProvider(conn connector.Connector, capabilities *context.ModelCapabilities) *Provider {
func NewProvider(conn connector.Connector, capabilities *openai.Capabilities) *Provider {
return &Provider{
Connector: conn,
Capabilities: capabilities,
@ -74,33 +75,33 @@ func (p *Provider) SupportsVision() bool {
if p.Capabilities == nil {
return false
}
supported, _ := p.Capabilities.GetVisionSupport()
supported, _ := context.GetVisionSupport(p.Capabilities)
return supported
}
// SupportsAudio check if this provider supports audio
func (p *Provider) SupportsAudio() bool {
return p.Capabilities != nil && p.Capabilities.Audio != nil && *p.Capabilities.Audio
return p.Capabilities != nil && p.Capabilities.Audio
}
// SupportsTools check if this provider supports tool calls
func (p *Provider) SupportsTools() bool {
return p.Capabilities != nil && p.Capabilities.ToolCalls != nil && *p.Capabilities.ToolCalls
return p.Capabilities != nil && p.Capabilities.ToolCalls
}
// SupportsStreaming check if this provider supports streaming
func (p *Provider) SupportsStreaming() bool {
return p.Capabilities != nil && p.Capabilities.Streaming != nil && *p.Capabilities.Streaming
return p.Capabilities != nil && p.Capabilities.Streaming
}
// SupportsJSON check if this provider supports JSON mode
func (p *Provider) SupportsJSON() bool {
return p.Capabilities != nil && p.Capabilities.JSON != nil && *p.Capabilities.JSON
return p.Capabilities != nil && p.Capabilities.JSON
}
// SupportsReasoning check if this provider supports reasoning mode
func (p *Provider) SupportsReasoning() bool {
return p.Capabilities != nil && p.Capabilities.Reasoning != nil && *p.Capabilities.Reasoning
return p.Capabilities != nil && p.Capabilities.Reasoning
}
// GetConnectorSetting gets a setting value from the connector

View file

@ -5,6 +5,7 @@ import (
"testing"
"github.com/yaoapp/gou/connector"
"github.com/yaoapp/gou/connector/openai"
"github.com/yaoapp/gou/plan"
"github.com/yaoapp/yao/agent/context"
"github.com/yaoapp/yao/agent/llm"
@ -60,15 +61,13 @@ func TestClaudeSonnet4StreamBasic(t *testing.T) {
t.Fatalf("Failed to select connector: %v", err)
}
trueVal := true
falseVal := false
options := &context.CompletionOptions{
Capabilities: &context.ModelCapabilities{
Streaming: &trueVal,
Reasoning: &falseVal, // Claude Sonnet 4 (non-thinking) doesn't expose reasoning
ToolCalls: &trueVal,
Vision: &trueVal,
Multimodal: &trueVal,
Capabilities: &openai.Capabilities{
Streaming: true,
Reasoning: false, // Claude Sonnet 4 (non-thinking) doesn't expose reasoning
ToolCalls: true,
Vision: "claude", // Claude requires base64 format
Multimodal: true,
},
}
@ -140,15 +139,13 @@ func TestClaudeSonnet4PostBasic(t *testing.T) {
t.Fatalf("Failed to select connector: %v", err)
}
trueVal := true
falseVal := false
options := &context.CompletionOptions{
Capabilities: &context.ModelCapabilities{
Streaming: &falseVal,
Reasoning: &falseVal,
ToolCalls: &trueVal,
Vision: &trueVal,
Multimodal: &trueVal,
Capabilities: &openai.Capabilities{
Streaming: false,
Reasoning: false,
ToolCalls: true,
Vision: "claude", // Claude requires base64 format
Multimodal: true,
},
}
@ -206,15 +203,13 @@ func TestClaudeSonnet4WithToolCalls(t *testing.T) {
t.Fatalf("Failed to select connector: %v", err)
}
trueVal := true
falseVal := false
options := &context.CompletionOptions{
Capabilities: &context.ModelCapabilities{
Streaming: &falseVal,
Reasoning: &falseVal,
ToolCalls: &trueVal,
Vision: &trueVal,
Multimodal: &trueVal,
Capabilities: &openai.Capabilities{
Streaming: false,
Reasoning: false,
ToolCalls: true,
Vision: "claude", // Claude requires base64 format
Multimodal: true,
},
}
@ -244,8 +239,8 @@ func TestClaudeSonnet4WithToolCalls(t *testing.T) {
options.Tools = []map[string]interface{}{simpleTool}
options.ToolChoice = "auto"
// Set lower max_tokens for faster response
maxTokens := 50
// Set enough tokens for tool call response
maxTokens := 150
options.MaxTokens = &maxTokens
llmInstance, err := llm.New(conn, options)
@ -256,7 +251,7 @@ func TestClaudeSonnet4WithToolCalls(t *testing.T) {
messages := []context.Message{
{
Role: context.RoleUser,
Content: "Call get_info with query='A' and count=1",
Content: "Please use the get_info function to retrieve information. Pass 'A' as the query parameter and 1 as the count parameter.",
},
}
@ -299,15 +294,13 @@ func TestClaudeSonnet4Vision(t *testing.T) {
t.Fatalf("Failed to select connector: %v", err)
}
trueVal := true
falseVal := false
options := &context.CompletionOptions{
Capabilities: &context.ModelCapabilities{
Streaming: &falseVal,
Reasoning: &falseVal,
ToolCalls: &trueVal,
Vision: &trueVal,
Multimodal: &trueVal,
Capabilities: &openai.Capabilities{
Streaming: false,
Reasoning: false,
ToolCalls: true,
Vision: "claude", // Claude requires base64 format
Multimodal: true,
},
}
@ -376,15 +369,13 @@ func TestClaudeSonnet4ThinkingStream(t *testing.T) {
t.Fatalf("Failed to select connector: %v", err)
}
trueVal := true
falseVal := false
options := &context.CompletionOptions{
Capabilities: &context.ModelCapabilities{
Streaming: &trueVal,
Reasoning: &trueVal, // Claude Thinking mode exposes reasoning
ToolCalls: &falseVal,
Vision: &trueVal,
Multimodal: &trueVal,
Capabilities: &openai.Capabilities{
Streaming: true,
Reasoning: true, // Claude Thinking mode exposes reasoning
ToolCalls: false,
Vision: "claude", // Claude requires base64 format
Multimodal: true,
},
}
@ -456,15 +447,13 @@ func TestClaudeSonnet4ThinkingPost(t *testing.T) {
t.Fatalf("Failed to select connector: %v", err)
}
trueVal := true
falseVal := false
options := &context.CompletionOptions{
Capabilities: &context.ModelCapabilities{
Streaming: &falseVal,
Reasoning: &trueVal,
ToolCalls: &falseVal,
Vision: &trueVal,
Multimodal: &trueVal,
Capabilities: &openai.Capabilities{
Streaming: false,
Reasoning: true,
ToolCalls: false,
Vision: "claude", // Claude requires base64 format
Multimodal: true,
},
}
@ -550,14 +539,12 @@ func TestClaudeTemperatureHandling(t *testing.T) {
t.Fatalf("Failed to select connector: %v", err)
}
trueVal := true
falseVal := false
options := &context.CompletionOptions{
Capabilities: &context.ModelCapabilities{
Streaming: &falseVal,
Reasoning: &tt.reasoning,
ToolCalls: &trueVal,
Vision: &trueVal,
Capabilities: &openai.Capabilities{
Streaming: false,
Reasoning: tt.reasoning,
ToolCalls: true,
Vision: true,
},
}

View file

@ -7,6 +7,7 @@ import (
jsoniter "github.com/json-iterator/go"
"github.com/yaoapp/gou/connector"
"github.com/yaoapp/gou/connector/openai"
"github.com/yaoapp/gou/plan"
"github.com/yaoapp/yao/agent/context"
"github.com/yaoapp/yao/agent/llm"
@ -28,16 +29,14 @@ func TestDeepSeekR1StreamBasic(t *testing.T) {
}
// Create LLM instance with capabilities
trueVal := true
falseVal := false
options := &context.CompletionOptions{
Capabilities: &context.ModelCapabilities{
Streaming: &trueVal,
Reasoning: &trueVal, // DeepSeek R1 supports reasoning
ToolCalls: &falseVal, // R1 doesn't support native tool calls
Vision: &falseVal,
Audio: &falseVal,
Multimodal: &falseVal,
Capabilities: &openai.Capabilities{
Streaming: true,
Reasoning: true, // DeepSeek R1 supports reasoning
ToolCalls: false, // R1 doesn't support native tool calls
Vision: false,
Audio: false,
Multimodal: false,
},
}
@ -207,15 +206,13 @@ func TestDeepSeekR1PostBasic(t *testing.T) {
}
// Create LLM instance
trueVal := true
falseVal := false
options := &context.CompletionOptions{
Capabilities: &context.ModelCapabilities{
Reasoning: &trueVal,
ToolCalls: &falseVal,
Vision: &falseVal,
Audio: &falseVal,
Multimodal: &falseVal,
Capabilities: &openai.Capabilities{
Reasoning: true,
ToolCalls: false,
Vision: false,
Audio: false,
Multimodal: false,
},
}
@ -295,16 +292,14 @@ func TestDeepSeekR1LogicPuzzle(t *testing.T) {
t.Fatalf("Failed to select connector: %v", err)
}
trueVal := true
falseVal := false
options := &context.CompletionOptions{
Capabilities: &context.ModelCapabilities{
Streaming: &trueVal,
Reasoning: &trueVal,
ToolCalls: &falseVal,
Vision: &falseVal,
Audio: &falseVal,
Multimodal: &falseVal,
Capabilities: &openai.Capabilities{
Streaming: true,
Reasoning: true,
ToolCalls: false,
Vision: false,
Audio: false,
Multimodal: false,
},
}

View file

@ -5,6 +5,7 @@ import (
"testing"
"github.com/yaoapp/gou/connector"
"github.com/yaoapp/gou/connector/openai"
"github.com/yaoapp/gou/plan"
"github.com/yaoapp/yao/agent/context"
"github.com/yaoapp/yao/agent/llm"
@ -24,16 +25,14 @@ func TestDeepSeekV3StreamBasic(t *testing.T) {
t.Fatalf("Failed to select connector: %v", err)
}
trueVal := true
falseVal := false
options := &context.CompletionOptions{
Capabilities: &context.ModelCapabilities{
Streaming: &trueVal,
Reasoning: &falseVal, // V3 doesn't support reasoning
ToolCalls: &trueVal, // V3 supports tool calls
Vision: &falseVal,
Audio: &falseVal,
Multimodal: &falseVal,
Capabilities: &openai.Capabilities{
Streaming: true,
Reasoning: false, // V3 doesn't support reasoning
ToolCalls: true, // V3 supports tool calls
Vision: false,
Audio: false,
Multimodal: false,
},
}
@ -135,15 +134,13 @@ func TestDeepSeekV3PostBasic(t *testing.T) {
t.Fatalf("Failed to select connector: %v", err)
}
trueVal := true
falseVal := false
options := &context.CompletionOptions{
Capabilities: &context.ModelCapabilities{
Reasoning: &falseVal,
ToolCalls: &trueVal,
Vision: &falseVal,
Audio: &falseVal,
Multimodal: &falseVal,
Capabilities: &openai.Capabilities{
Reasoning: false,
ToolCalls: true,
Vision: false,
Audio: false,
Multimodal: false,
},
}
@ -226,12 +223,10 @@ func TestDeepSeekV3WithToolCalls(t *testing.T) {
t.Fatalf("Failed to select connector: %v", err)
}
trueVal := true
falseVal := false
options := &context.CompletionOptions{
Capabilities: &context.ModelCapabilities{
Reasoning: &falseVal,
ToolCalls: &trueVal,
Capabilities: &openai.Capabilities{
Reasoning: false,
ToolCalls: true,
},
}
@ -318,13 +313,11 @@ func TestDeepSeekV3NoReasoningEffort(t *testing.T) {
t.Fatalf("Failed to select connector: %v", err)
}
trueVal := true
falseVal := false
effort := "high"
options := &context.CompletionOptions{
Capabilities: &context.ModelCapabilities{
Reasoning: &falseVal, // V3 doesn't support reasoning
ToolCalls: &trueVal,
Capabilities: &openai.Capabilities{
Reasoning: false, // V3 doesn't support reasoning
ToolCalls: true,
},
ReasoningEffort: &effort, // Should be ignored by adapter
}

View file

@ -5,6 +5,7 @@ import (
"testing"
"github.com/yaoapp/gou/connector"
"github.com/yaoapp/gou/connector/openai"
"github.com/yaoapp/gou/plan"
"github.com/yaoapp/yao/agent/context"
"github.com/yaoapp/yao/agent/llm"
@ -24,14 +25,13 @@ func TestGPT5StreamBasic(t *testing.T) {
t.Fatalf("Failed to select connector: %v", err)
}
trueVal := true
options := &context.CompletionOptions{
Capabilities: &context.ModelCapabilities{
Streaming: &trueVal,
Reasoning: &trueVal, // GPT-5 supports reasoning
ToolCalls: &trueVal,
Vision: &trueVal,
Multimodal: &trueVal,
Capabilities: &openai.Capabilities{
Streaming: true,
Reasoning: true, // GPT-5 supports reasoning
ToolCalls: true,
Vision: true,
Multimodal: true,
},
}
@ -105,11 +105,10 @@ func TestGPT5ReasoningEffort(t *testing.T) {
for _, effort := range effortLevels {
t.Run("effort_"+effort, func(t *testing.T) {
trueVal := true
options := &context.CompletionOptions{
Capabilities: &context.ModelCapabilities{
Reasoning: &trueVal,
ToolCalls: &trueVal,
Capabilities: &openai.Capabilities{
Reasoning: true,
ToolCalls: true,
},
ReasoningEffort: &effort,
}
@ -172,11 +171,10 @@ func TestGPT5PostWithToolCalls(t *testing.T) {
t.Fatalf("Failed to select connector: %v", err)
}
trueVal := true
options := &context.CompletionOptions{
Capabilities: &context.ModelCapabilities{
Reasoning: &trueVal,
ToolCalls: &trueVal,
Capabilities: &openai.Capabilities{
Reasoning: true,
ToolCalls: true,
},
}
@ -259,12 +257,11 @@ func TestGPT5Vision(t *testing.T) {
t.Fatalf("Failed to select connector: %v", err)
}
trueVal := true
options := &context.CompletionOptions{
Capabilities: &context.ModelCapabilities{
Reasoning: &trueVal,
Vision: &trueVal,
Multimodal: &trueVal,
Capabilities: &openai.Capabilities{
Reasoning: true,
Vision: true,
Multimodal: true,
},
}
@ -331,13 +328,11 @@ func TestGPT5ReasoningEffortWithGPT4o(t *testing.T) {
t.Fatalf("Failed to select connector: %v", err)
}
trueVal := true
falseVal := false
effort := "high"
options := &context.CompletionOptions{
Capabilities: &context.ModelCapabilities{
Reasoning: &falseVal, // GPT-4o doesn't support reasoning
ToolCalls: &trueVal,
Capabilities: &openai.Capabilities{
Reasoning: false, // GPT-4o doesn't support reasoning
ToolCalls: true,
},
ReasoningEffort: &effort, // Should be ignored by adapter
}

View file

@ -8,6 +8,7 @@ import (
jsoniter "github.com/json-iterator/go"
"github.com/yaoapp/gou/connector"
gouOpenAI "github.com/yaoapp/gou/connector/openai"
"github.com/yaoapp/gou/http"
"github.com/yaoapp/kun/log"
"github.com/yaoapp/yao/agent/context"
@ -142,7 +143,7 @@ func buildAPIURL(host, endpoint string) string {
}
// New create a new OpenAI provider with capability adapters
func New(conn connector.Connector, capabilities *context.ModelCapabilities) *Provider {
func New(conn connector.Connector, capabilities *gouOpenAI.Capabilities) *Provider {
return &Provider{
Provider: base.NewProvider(conn, capabilities),
adapters: buildAdapters(capabilities),
@ -150,7 +151,7 @@ func New(conn connector.Connector, capabilities *context.ModelCapabilities) *Pro
}
// buildAdapters builds capability adapters based on model capabilities
func buildAdapters(cap *context.ModelCapabilities) []adapters.CapabilityAdapter {
func buildAdapters(cap *gouOpenAI.Capabilities) []adapters.CapabilityAdapter {
if cap == nil {
return []adapters.CapabilityAdapter{}
}
@ -158,12 +159,10 @@ func buildAdapters(cap *context.ModelCapabilities) []adapters.CapabilityAdapter
result := make([]adapters.CapabilityAdapter, 0)
// Tool call adapter
if cap.ToolCalls != nil {
result = append(result, adapters.NewToolCallAdapter(*cap.ToolCalls))
}
result = append(result, adapters.NewToolCallAdapter(cap.ToolCalls))
// Vision adapter
visionSupport, visionFormat := cap.GetVisionSupport()
visionSupport, visionFormat := context.GetVisionSupport(cap)
if visionSupport {
result = append(result, adapters.NewVisionAdapter(true, visionFormat))
} else if cap.Vision != nil {
@ -172,31 +171,27 @@ func buildAdapters(cap *context.ModelCapabilities) []adapters.CapabilityAdapter
}
// Audio adapter
if cap.Audio != nil {
result = append(result, adapters.NewAudioAdapter(*cap.Audio))
}
result = append(result, adapters.NewAudioAdapter(cap.Audio))
// Reasoning adapter (always add to handle reasoning_effort and temperature parameters)
// Even if the model doesn't support reasoning, we need the adapter to strip reasoning_effort
if cap.Reasoning != nil {
if *cap.Reasoning {
// Detect reasoning format based on capabilities
format := detectReasoningFormat(cap)
result = append(result, adapters.NewReasoningAdapter(format, cap))
} else {
// Model doesn't support reasoning, use None format to strip reasoning parameters
result = append(result, adapters.NewReasoningAdapter(adapters.ReasoningFormatNone, cap))
}
if cap.Reasoning {
// Detect reasoning format based on capabilities
format := detectReasoningFormat(cap)
result = append(result, adapters.NewReasoningAdapter(format, cap))
} else {
// Model doesn't support reasoning, use None format to strip reasoning parameters
result = append(result, adapters.NewReasoningAdapter(adapters.ReasoningFormatNone, cap))
}
return result
}
// detectReasoningFormat detects the reasoning format based on capabilities
func detectReasoningFormat(cap *context.ModelCapabilities) adapters.ReasoningFormat {
func detectReasoningFormat(cap *gouOpenAI.Capabilities) adapters.ReasoningFormat {
// TODO: Implement better detection logic
// For now, default to OpenAI o1 format if reasoning is supported
if cap.Reasoning != nil && *cap.Reasoning {
if cap.Reasoning {
return adapters.ReasoningFormatOpenAI
}
return adapters.ReasoningFormatNone

View file

@ -8,6 +8,7 @@ import (
"time"
"github.com/yaoapp/gou/connector"
"github.com/yaoapp/gou/connector/openai"
"github.com/yaoapp/gou/plan"
"github.com/yaoapp/yao/agent/context"
"github.com/yaoapp/yao/agent/llm"
@ -29,11 +30,10 @@ func TestOpenAIStreamBasic(t *testing.T) {
}
// Create LLM instance with capabilities
trueVal := true
options := &context.CompletionOptions{
Capabilities: &context.ModelCapabilities{
Streaming: &trueVal,
ToolCalls: &trueVal,
Capabilities: &openai.Capabilities{
Streaming: true,
ToolCalls: true,
},
}
@ -117,10 +117,9 @@ func TestOpenAIPostBasic(t *testing.T) {
}
// Create LLM instance
trueVal := true
options := &context.CompletionOptions{
Capabilities: &context.ModelCapabilities{
ToolCalls: &trueVal,
Capabilities: &openai.Capabilities{
ToolCalls: true,
},
}
@ -192,11 +191,10 @@ func TestOpenAIStreamWithToolCalls(t *testing.T) {
}
// Create LLM instance with tool call capabilities
trueVal := true
options := &context.CompletionOptions{
Capabilities: &context.ModelCapabilities{
Streaming: &trueVal,
ToolCalls: &trueVal,
Capabilities: &openai.Capabilities{
Streaming: true,
ToolCalls: true,
},
}
@ -307,10 +305,9 @@ func TestOpenAIPostWithToolCalls(t *testing.T) {
}
// Create LLM instance with tool call capabilities
trueVal := true
options := &context.CompletionOptions{
Capabilities: &context.ModelCapabilities{
ToolCalls: &trueVal,
Capabilities: &openai.Capabilities{
ToolCalls: true,
},
}
@ -424,11 +421,10 @@ func TestOpenAIStreamWithInvalidToolCall(t *testing.T) {
}
// Create LLM instance
trueVal := true
options := &context.CompletionOptions{
Capabilities: &context.ModelCapabilities{
Streaming: &trueVal,
ToolCalls: &trueVal,
Capabilities: &openai.Capabilities{
Streaming: true,
ToolCalls: true,
},
}
@ -523,11 +519,10 @@ func TestOpenAIStreamRetry(t *testing.T) {
}
// Create LLM instance
trueVal := true
options := &context.CompletionOptions{
Capabilities: &context.ModelCapabilities{
Streaming: &trueVal,
ToolCalls: &trueVal, // Need this to select OpenAI provider
Capabilities: &openai.Capabilities{
Streaming: true,
ToolCalls: true, // Need this to select OpenAI provider
},
}
@ -582,11 +577,10 @@ func TestOpenAIStreamChunkTypes(t *testing.T) {
t.Fatalf("Failed to select connector: %v", err)
}
trueVal := true
options := &context.CompletionOptions{
Capabilities: &context.ModelCapabilities{
Streaming: &trueVal,
ToolCalls: &trueVal,
Capabilities: &openai.Capabilities{
Streaming: true,
ToolCalls: true,
},
}
@ -649,11 +643,10 @@ func TestOpenAIStreamErrorCallback(t *testing.T) {
t.Fatalf("Failed to create test connector: %v", err)
}
trueVal := true
options := &context.CompletionOptions{
Capabilities: &context.ModelCapabilities{
Streaming: &trueVal,
ToolCalls: &trueVal,
Capabilities: &openai.Capabilities{
Streaming: true,
ToolCalls: true,
},
}
@ -711,11 +704,10 @@ func TestOpenAIToolCallValidationRetry(t *testing.T) {
t.Fatalf("Failed to select connector: %v", err)
}
trueVal := true
options := &context.CompletionOptions{
Capabilities: &context.ModelCapabilities{
Streaming: &trueVal,
ToolCalls: &trueVal,
Capabilities: &openai.Capabilities{
Streaming: true,
ToolCalls: true,
},
Tools: []map[string]interface{}{
{
@ -815,11 +807,10 @@ func TestOpenAIJSONMode(t *testing.T) {
t.Fatalf("Failed to select connector: %v", err)
}
trueVal := true
options := &context.CompletionOptions{
Capabilities: &context.ModelCapabilities{
Streaming: &trueVal,
ToolCalls: &trueVal,
Capabilities: &openai.Capabilities{
Streaming: true,
ToolCalls: true,
},
ResponseFormat: &context.ResponseFormat{
Type: context.ResponseFormatJSON,
@ -902,10 +893,9 @@ func TestOpenAIJSONModePost(t *testing.T) {
t.Fatalf("Failed to select connector: %v", err)
}
trueVal := true
options := &context.CompletionOptions{
Capabilities: &context.ModelCapabilities{
ToolCalls: &trueVal,
Capabilities: &openai.Capabilities{
ToolCalls: true,
},
ResponseFormat: &context.ResponseFormat{
Type: context.ResponseFormatJSON,
@ -973,8 +963,6 @@ func TestOpenAIJSONSchema(t *testing.T) {
t.Fatalf("Failed to select connector: %v", err)
}
trueVal := true
// Define a strict JSON schema
// Note: For OpenAI strict mode, 'required' must include ALL properties
schema := map[string]interface{}{
@ -1009,9 +997,9 @@ func TestOpenAIJSONSchema(t *testing.T) {
}
options := &context.CompletionOptions{
Capabilities: &context.ModelCapabilities{
Streaming: &trueVal,
ToolCalls: &trueVal,
Capabilities: &openai.Capabilities{
Streaming: true,
ToolCalls: true,
},
ResponseFormat: &context.ResponseFormat{
Type: context.ResponseFormatJSONSchema,
@ -1019,7 +1007,7 @@ func TestOpenAIJSONSchema(t *testing.T) {
Name: "user_info",
Description: "User information schema",
Schema: schema,
Strict: &trueVal,
Strict: func() *bool { v := true; return &v }(),
},
},
}
@ -1121,8 +1109,6 @@ func TestOpenAIJSONSchemaPost(t *testing.T) {
t.Fatalf("Failed to select connector: %v", err)
}
trueVal := true
// Simple schema for testing
// Note: For OpenAI strict mode, 'required' must include ALL properties
schema := map[string]interface{}{
@ -1144,8 +1130,8 @@ func TestOpenAIJSONSchemaPost(t *testing.T) {
}
options := &context.CompletionOptions{
Capabilities: &context.ModelCapabilities{
ToolCalls: &trueVal,
Capabilities: &openai.Capabilities{
ToolCalls: true,
},
ResponseFormat: &context.ResponseFormat{
Type: context.ResponseFormatJSONSchema,
@ -1153,7 +1139,7 @@ func TestOpenAIJSONSchemaPost(t *testing.T) {
Name: "api_response",
Description: "API response format",
Schema: schema,
Strict: &trueVal,
Strict: func() *bool { v := true; return &v }(),
},
},
}
@ -1263,11 +1249,10 @@ func TestOpenAIStreamLifecycleEvents(t *testing.T) {
t.Fatalf("Failed to select connector: %v", err)
}
trueVal := true
options := &context.CompletionOptions{
Capabilities: &context.ModelCapabilities{
Streaming: &trueVal,
ToolCalls: &trueVal,
Capabilities: &openai.Capabilities{
Streaming: true,
ToolCalls: true,
},
}
@ -1369,11 +1354,10 @@ func TestOpenAIStreamContextCancellation(t *testing.T) {
t.Fatalf("Failed to select connector: %v", err)
}
trueVal := true
options := &context.CompletionOptions{
Capabilities: &context.ModelCapabilities{
Streaming: &trueVal,
ToolCalls: &trueVal,
Capabilities: &openai.Capabilities{
Streaming: true,
ToolCalls: true,
},
}
@ -1443,13 +1427,12 @@ func TestOpenAIStreamWithTemperature(t *testing.T) {
t.Fatalf("Failed to select connector: %v", err)
}
trueVal := true
temperature := 0.7 // Moderate temperature
options := &context.CompletionOptions{
Capabilities: &context.ModelCapabilities{
Streaming: &trueVal,
ToolCalls: &trueVal, // Need this to select OpenAI provider
Capabilities: &openai.Capabilities{
Streaming: true,
ToolCalls: true, // Need this to select OpenAI provider
},
Temperature: &temperature,
}

View file

@ -5,6 +5,7 @@ import (
"testing"
"github.com/yaoapp/gou/connector"
"github.com/yaoapp/gou/connector/openai"
"github.com/yaoapp/gou/plan"
"github.com/yaoapp/yao/agent/context"
"github.com/yaoapp/yao/agent/llm"
@ -23,11 +24,10 @@ func TestTemperatureGPT5AutoReset(t *testing.T) {
t.Fatalf("Failed to select connector: %v", err)
}
trueVal := true
invalidTemp := 0.7 // GPT-5 doesn't support this
options := &context.CompletionOptions{
Capabilities: &context.ModelCapabilities{
Reasoning: &trueVal,
Capabilities: &openai.Capabilities{
Reasoning: true,
},
Temperature: &invalidTemp, // Should be reset to 1.0
}
@ -73,11 +73,10 @@ func TestTemperatureDeepSeekR1AutoReset(t *testing.T) {
t.Fatalf("Failed to select connector: %v", err)
}
trueVal := true
invalidTemp := 0.5 // DeepSeek R1 doesn't support this
options := &context.CompletionOptions{
Capabilities: &context.ModelCapabilities{
Reasoning: &trueVal,
Capabilities: &openai.Capabilities{
Reasoning: true,
},
Temperature: &invalidTemp, // Should be reset to 1.0
}
@ -126,13 +125,11 @@ func TestTemperatureGPT4oPreserved(t *testing.T) {
t.Fatalf("Failed to select connector: %v", err)
}
trueVal := true
falseVal := false
customTemp := 0.3 // GPT-4o should preserve this
options := &context.CompletionOptions{
Capabilities: &context.ModelCapabilities{
Reasoning: &falseVal, // Not a reasoning model
ToolCalls: &trueVal,
Capabilities: &openai.Capabilities{
Reasoning: false, // Not a reasoning model
ToolCalls: true,
},
Temperature: &customTemp, // Should be preserved
}
@ -178,13 +175,11 @@ func TestTemperatureDeepSeekV3Preserved(t *testing.T) {
t.Fatalf("Failed to select connector: %v", err)
}
trueVal := true
falseVal := false
customTemp := 0.8 // DeepSeek V3 should preserve this
options := &context.CompletionOptions{
Capabilities: &context.ModelCapabilities{
Reasoning: &falseVal, // Not a reasoning model
ToolCalls: &trueVal,
Capabilities: &openai.Capabilities{
Reasoning: false, // Not a reasoning model
ToolCalls: true,
},
Temperature: &customTemp, // Should be preserved
}
@ -230,11 +225,10 @@ func TestTemperatureGPT5Default(t *testing.T) {
t.Fatalf("Failed to select connector: %v", err)
}
trueVal := true
defaultTemp := 1.0 // GPT-5's valid temperature
options := &context.CompletionOptions{
Capabilities: &context.ModelCapabilities{
Reasoning: &trueVal,
Capabilities: &openai.Capabilities{
Reasoning: true,
},
Temperature: &defaultTemp, // Should work fine
}
@ -293,16 +287,14 @@ func TestTemperatureNoTemperatureProvided(t *testing.T) {
t.Fatalf("Failed to select connector: %v", err)
}
trueVal := true
falseVal := false
options := &context.CompletionOptions{
Capabilities: &context.ModelCapabilities{
Reasoning: &falseVal,
ToolCalls: &trueVal,
Capabilities: &openai.Capabilities{
Reasoning: false,
ToolCalls: true,
},
}
if tc.reasoning {
options.Capabilities.Reasoning = &trueVal
options.Capabilities.Reasoning = true
}
// Temperature not set - should use API default

View file

@ -6,6 +6,7 @@ import (
"github.com/yaoapp/gou/application"
"github.com/yaoapp/gou/connector"
gouOpenAI "github.com/yaoapp/gou/connector/openai"
"github.com/yaoapp/yao/agent/assistant"
"github.com/yaoapp/yao/agent/context"
"github.com/yaoapp/yao/agent/i18n"
@ -116,7 +117,7 @@ func initModelCapabilities() error {
return err
}
var models map[string]assistant.ModelCapabilities = map[string]assistant.ModelCapabilities{}
var models map[string]gouOpenAI.Capabilities = map[string]gouOpenAI.Capabilities{}
err = application.Parse("models.yml", bytes, &models)
if err != nil {
return err

View file

@ -35,7 +35,7 @@ type AdapterConfig struct {
Locale string
}
// ModelCapabilities is a simplified version of context.ModelCapabilities
// ModelCapabilities is a simplified version of openai.Capabilities
// We use a local type to avoid circular dependencies
type ModelCapabilities struct {
Reasoning *bool // Supports reasoning/thinking mode (o1, DeepSeek R1)

View file

@ -22,9 +22,10 @@ type Writer struct {
func NewWriter(options message.Options) (*Writer, error) {
// Get model capabilities from context (set by assistant)
var capabilities *ModelCapabilities
if options.Capabilities != nil && options.Capabilities.Reasoning != nil {
if options.Capabilities != nil && options.Capabilities.Reasoning {
v := true
capabilities = &ModelCapabilities{
Reasoning: options.Capabilities.Reasoning,
Reasoning: &v,
}
}

View file

@ -3,6 +3,7 @@ package message
import (
"net/http"
"github.com/yaoapp/gou/connector/openai"
traceTypes "github.com/yaoapp/yao/trace/types"
)
@ -12,23 +13,10 @@ type Options struct {
Accept string
Writer http.ResponseWriter
Trace traceTypes.Manager
Capabilities *ModelCapabilities
Capabilities *openai.Capabilities
Locale string
}
// ModelCapabilities defines the capabilities of a language model
// Used by LLM to select appropriate provider and validate requests
type ModelCapabilities struct {
Vision interface{} `json:"vision,omitempty"` // Supports vision/image input: bool or VisionFormat string ("openai", "claude"/"base64", "default")
ToolCalls *bool `json:"tool_calls,omitempty"` // Supports tool/function calling
Audio *bool `json:"audio,omitempty"` // Supports audio input/output
Reasoning *bool `json:"reasoning,omitempty"` // Supports reasoning/thinking mode (o1, DeepSeek R1)
Streaming *bool `json:"streaming,omitempty"` // Supports streaming responses
JSON *bool `json:"json,omitempty"` // Supports JSON mode
Multimodal *bool `json:"multimodal,omitempty"` // Supports multimodal input (text + images + audio)
TemperatureAdjustable *bool `json:"temperature_adjustable,omitempty"` // Supports temperature adjustment (reasoning models typically don't)
}
// Message represents a universal message structure (DSL)
// All messages are expressed through Type + Props, without predefining specific types
type Message struct {

View file

@ -1,6 +1,7 @@
package types
import (
"github.com/yaoapp/gou/connector/openai"
"github.com/yaoapp/yao/agent/assistant"
store "github.com/yaoapp/yao/agent/store/types"
)
@ -16,7 +17,7 @@ type DSL struct {
// Global External Settings - model capabilities, tools, etc.
// ===============================
Models map[string]assistant.ModelCapabilities `json:"models,omitempty" yaml:"models,omitempty"` // The model capabilities configuration
Models map[string]openai.Capabilities `json:"models,omitempty" yaml:"models,omitempty"` // The model capabilities configuration
// Internal
// ===============================