yao/agent/llm/providers/openai/deepseek_v3_test.go
Max 3dd63e5530 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.
2025-12-02 11:33:09 +08:00

403 lines
10 KiB
Go

package openai_test
import (
gocontext "context"
"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"
"github.com/yaoapp/yao/agent/output/message"
"github.com/yaoapp/yao/config"
"github.com/yaoapp/yao/openapi/oauth/types"
"github.com/yaoapp/yao/test"
)
// TestDeepSeekV3StreamBasic tests basic streaming completion with DeepSeek V3
func TestDeepSeekV3StreamBasic(t *testing.T) {
test.Prepare(t, config.Conf)
defer test.Clean()
conn, err := connector.Select("deepseek.v3")
if err != nil {
t.Fatalf("Failed to select connector: %v", err)
}
options := &context.CompletionOptions{
Capabilities: &openai.Capabilities{
Streaming: true,
Reasoning: false, // V3 doesn't support reasoning
ToolCalls: true, // V3 supports tool calls
Vision: false,
Audio: false,
Multimodal: false,
},
}
llmInstance, err := llm.New(conn, options)
if err != nil {
t.Fatalf("Failed to create LLM instance: %v", err)
}
// Simple math question
messages := []context.Message{
{
Role: context.RoleUser,
Content: "What is 5 + 3?",
},
}
// Set max tokens
maxTokens := 100
options.MaxTokens = &maxTokens
ctx := newDeepSeekV3TestContext("test-deepseek-v3-basic", "deepseek.v3")
// Track streaming chunks
var contentChunks []string
handler := func(chunkType message.StreamChunkType, data []byte) int {
dataStr := string(data)
t.Logf("Stream chunk [%s]: %s", chunkType, dataStr)
if chunkType == message.ChunkText {
contentChunks = append(contentChunks, dataStr)
}
return 0 // Continue
}
// Call Stream
response, err := llmInstance.Stream(ctx, messages, options, handler)
if err != nil {
t.Fatalf("Stream failed: %v", err)
}
// Validate response
if response == nil {
t.Fatal("Response is nil")
}
if response.ID == "" {
t.Error("Response ID is empty")
}
if response.Model == "" {
t.Error("Response Model is empty")
}
// Should have content (V3 is not a reasoning model)
contentStr, ok := response.Content.(string)
if !ok || contentStr == "" {
t.Error("Expected content but got empty")
} else {
t.Logf("Response content: %s", contentStr)
}
// Should NOT have reasoning content (V3 doesn't support reasoning)
if response.ReasoningContent != "" {
t.Errorf("Expected no reasoning_content for V3, but got: %s", response.ReasoningContent)
}
// Check usage
if response.Usage == nil {
t.Error("Response Usage is nil")
} else {
t.Logf("Usage: prompt=%d, completion=%d, total=%d",
response.Usage.PromptTokens, response.Usage.CompletionTokens, response.Usage.TotalTokens)
// Should have 0 reasoning tokens
if response.Usage.CompletionTokensDetails != nil {
reasoningTokens := response.Usage.CompletionTokensDetails.ReasoningTokens
if reasoningTokens != 0 {
t.Errorf("Expected reasoning_tokens=0 for V3, got %d", reasoningTokens)
}
}
}
if len(contentChunks) == 0 {
t.Error("Expected content chunks but got none")
} else {
t.Logf("Received %d content chunks", len(contentChunks))
}
t.Logf("Final response: %+v", response)
}
// TestDeepSeekV3PostBasic tests basic non-streaming completion
func TestDeepSeekV3PostBasic(t *testing.T) {
test.Prepare(t, config.Conf)
defer test.Clean()
conn, err := connector.Select("deepseek.v3")
if err != nil {
t.Fatalf("Failed to select connector: %v", err)
}
options := &context.CompletionOptions{
Capabilities: &openai.Capabilities{
Reasoning: false,
ToolCalls: true,
Vision: false,
Audio: false,
Multimodal: false,
},
}
llmInstance, err := llm.New(conn, options)
if err != nil {
t.Fatalf("Failed to create LLM instance: %v", err)
}
// Simple question
messages := []context.Message{
{
Role: context.RoleUser,
Content: "What is 2 * 4?",
},
}
// Set max tokens
maxTokens := 100
options.MaxTokens = &maxTokens
ctx := newDeepSeekV3TestContext("test-deepseek-v3-post", "deepseek.v3")
// Call Post
response, err := llmInstance.Post(ctx, messages, options)
if err != nil {
t.Fatalf("Post failed: %v", err)
}
// Validate response
if response == nil {
t.Fatal("Response is nil")
}
if response.ID == "" {
t.Error("Response ID is empty")
}
if response.Model == "" {
t.Error("Response Model is empty")
}
// Should have content
contentStr, ok := response.Content.(string)
if !ok || contentStr == "" {
t.Error("Expected content but got empty")
} else {
t.Logf("Response content: %s", contentStr)
}
// Should NOT have reasoning content
if response.ReasoningContent != "" {
t.Errorf("V3 should not have reasoning_content, but got: %s", response.ReasoningContent)
}
// Check usage
if response.Usage == nil {
t.Error("Response Usage is nil")
} else {
t.Logf("Usage: prompt=%d, completion=%d, total=%d",
response.Usage.PromptTokens, response.Usage.CompletionTokens, response.Usage.TotalTokens)
// Should have 0 reasoning tokens
if response.Usage.CompletionTokensDetails != nil {
reasoningTokens := response.Usage.CompletionTokensDetails.ReasoningTokens
if reasoningTokens != 0 {
t.Errorf("Expected reasoning_tokens=0 for V3, got %d", reasoningTokens)
}
}
}
t.Logf("Response: %+v", response)
}
// TestDeepSeekV3WithToolCalls tests V3 with tool calls
func TestDeepSeekV3WithToolCalls(t *testing.T) {
test.Prepare(t, config.Conf)
defer test.Clean()
conn, err := connector.Select("deepseek.v3")
if err != nil {
t.Fatalf("Failed to select connector: %v", err)
}
options := &context.CompletionOptions{
Capabilities: &openai.Capabilities{
Reasoning: false,
ToolCalls: true,
},
}
// Define a simple tool with minimal parameters
simpleTool := map[string]interface{}{
"type": "function",
"function": map[string]interface{}{
"name": "get_info",
"description": "Get information",
"parameters": map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"query": map[string]interface{}{
"type": "string",
"description": "Query string (single letter)",
},
"count": map[string]interface{}{
"type": "number",
"description": "Count (single digit)",
},
},
"required": []string{"query", "count"},
},
},
}
options.Tools = []map[string]interface{}{simpleTool}
options.ToolChoice = "auto"
// Set lower max_tokens for faster response
maxTokens := 50
options.MaxTokens = &maxTokens
llmInstance, err := llm.New(conn, options)
if err != nil {
t.Fatalf("Failed to create LLM instance: %v", err)
}
messages := []context.Message{
{
Role: context.RoleUser,
Content: "Call get_info with query='A' and count=1",
},
}
ctx := newDeepSeekV3TestContext("test-deepseek-v3-tools", "deepseek.v3")
response, err := llmInstance.Post(ctx, messages, options)
if err != nil {
t.Fatalf("Post with tool calls failed: %v", err)
}
if response == nil {
t.Fatal("Response is nil")
}
// Should have tool calls
if len(response.ToolCalls) == 0 {
t.Error("Expected tool calls but got none")
} else {
tc := response.ToolCalls[0]
t.Logf("✓ Tool call: %s(%s)", tc.Function.Name, tc.Function.Arguments)
if tc.Function.Name != "get_info" {
t.Errorf("Expected tool name 'get_info', got '%s'", tc.Function.Name)
}
}
if response.Usage != nil {
t.Logf("Usage: prompt=%d, completion=%d, total=%d",
response.Usage.PromptTokens, response.Usage.CompletionTokens, response.Usage.TotalTokens)
}
t.Logf("Response: %+v", response)
}
// TestDeepSeekV3NoReasoningEffort tests that V3 ignores reasoning_effort parameter
func TestDeepSeekV3NoReasoningEffort(t *testing.T) {
test.Prepare(t, config.Conf)
defer test.Clean()
conn, err := connector.Select("deepseek.v3")
if err != nil {
t.Fatalf("Failed to select connector: %v", err)
}
effort := "high"
options := &context.CompletionOptions{
Capabilities: &openai.Capabilities{
Reasoning: false, // V3 doesn't support reasoning
ToolCalls: true,
},
ReasoningEffort: &effort, // Should be ignored by adapter
}
llmInstance, err := llm.New(conn, options)
if err != nil {
t.Fatalf("Failed to create LLM instance: %v", err)
}
messages := []context.Message{
{
Role: context.RoleUser,
Content: "Reply with just: OK",
},
}
maxTokens := 20
options.MaxTokens = &maxTokens
ctx := newDeepSeekV3TestContext("test-deepseek-v3-no-reasoning", "deepseek.v3")
// Should succeed (adapter removes reasoning_effort parameter)
response, err := llmInstance.Post(ctx, messages, options)
if err != nil {
t.Fatalf("Post failed: %v", err)
}
if response == nil {
t.Fatal("Response is nil")
}
// Should have 0 reasoning tokens
if response.Usage != nil && response.Usage.CompletionTokensDetails != nil {
reasoningTokens := response.Usage.CompletionTokensDetails.ReasoningTokens
if reasoningTokens != 0 {
t.Errorf("Expected reasoning_tokens=0 for V3, got %d", reasoningTokens)
} else {
t.Log("✓ V3 correctly shows reasoning_tokens=0")
}
}
t.Log("✓ ReasoningAdapter correctly removed reasoning_effort parameter for V3")
}
// ============================================================================
// Helper Functions
// ============================================================================
// newDeepSeekV3TestContext creates a real Context for testing DeepSeek V3 provider
func newDeepSeekV3TestContext(chatID, connectorID string) *context.Context {
return &context.Context{
Context: gocontext.Background(),
Space: plan.NewMemorySharedSpace(),
ChatID: chatID,
AssistantID: "test-assistant",
Connector: connectorID,
Locale: "en-us",
Theme: "light",
Client: context.Client{
Type: "web",
UserAgent: "DeepSeekV3ProviderTest/1.0",
IP: "127.0.0.1",
},
Referer: context.RefererAPI,
Accept: context.AcceptStandard,
Route: "/api/test",
Metadata: make(map[string]interface{}),
Authorized: &types.AuthorizedInfo{
Subject: "test-user",
ClientID: "test-client",
UserID: "test-user-123",
TeamID: "test-team-456",
TenantID: "test-tenant-789",
SessionID: "test-session-id",
Constraints: types.DataConstraints{
TeamOnly: true,
Extra: map[string]interface{}{
"test": "deepseek-v3-provider",
},
},
},
}
}