yao/agent/llm/providers/openai/temperature_test.go

378 lines
9.4 KiB
Go

package openai_test
import (
gocontext "context"
"testing"
"github.com/yaoapp/gou/connector"
"github.com/yaoapp/gou/plan"
"github.com/yaoapp/yao/agent/context"
"github.com/yaoapp/yao/agent/llm"
"github.com/yaoapp/yao/config"
"github.com/yaoapp/yao/openapi/oauth/types"
"github.com/yaoapp/yao/test"
)
// TestTemperatureGPT5AutoReset tests that GPT-5 automatically resets temperature to 1.0
func TestTemperatureGPT5AutoReset(t *testing.T) {
test.Prepare(t, config.Conf)
defer test.Clean()
conn, err := connector.Select("openai.gpt-5")
if err != nil {
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,
},
Temperature: &invalidTemp, // Should be reset to 1.0
}
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: "Say 'OK'",
},
}
maxTokens := 10
options.MaxCompletionTokens = &maxTokens
ctx := newTemperatureTestContext("test-gpt5-temp", "openai.gpt-5")
// Should succeed (temperature automatically reset to 1.0)
response, err := llmInstance.Post(ctx, messages, options)
if err != nil {
t.Fatalf("Post failed: %v", err)
}
if response == nil {
t.Fatal("Response is nil")
}
t.Log("✓ GPT-5 successfully handled invalid temperature by resetting to 1.0")
t.Logf("Response: %v", response.Content)
}
// TestTemperatureDeepSeekR1AutoReset tests that DeepSeek R1 automatically resets temperature to 1.0
func TestTemperatureDeepSeekR1AutoReset(t *testing.T) {
test.Prepare(t, config.Conf)
defer test.Clean()
conn, err := connector.Select("deepseek.r1")
if err != nil {
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,
},
Temperature: &invalidTemp, // Should be reset to 1.0
}
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: "Say 'Hello'",
},
}
maxTokens := 100
options.MaxCompletionTokens = &maxTokens
ctx := newTemperatureTestContext("test-deepseek-r1-temp", "deepseek.r1")
// Should succeed (temperature automatically reset to 1.0)
response, err := llmInstance.Post(ctx, messages, options)
if err != nil {
t.Fatalf("Post failed: %v", err)
}
if response == nil {
t.Fatal("Response is nil")
}
t.Log("✓ DeepSeek R1 successfully handled invalid temperature by resetting to 1.0")
t.Logf("Response content: %v", response.Content)
if response.ReasoningContent != "" {
t.Logf("Reasoning content length: %d", len(response.ReasoningContent))
}
}
// TestTemperatureGPT4oPreserved tests that GPT-4o preserves custom temperature
func TestTemperatureGPT4oPreserved(t *testing.T) {
test.Prepare(t, config.Conf)
defer test.Clean()
conn, err := connector.Select("openai.gpt-4o")
if err != nil {
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,
},
Temperature: &customTemp, // Should be preserved
}
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: "Say 'OK'",
},
}
maxTokens := 10
options.MaxCompletionTokens = &maxTokens
ctx := newTemperatureTestContext("test-gpt4o-temp", "openai.gpt-4o")
// Should succeed with custom temperature preserved
response, err := llmInstance.Post(ctx, messages, options)
if err != nil {
t.Fatalf("Post failed: %v", err)
}
if response == nil {
t.Fatal("Response is nil")
}
t.Log("✓ GPT-4o successfully preserved custom temperature (0.3)")
t.Logf("Response: %v", response.Content)
}
// TestTemperatureDeepSeekV3Preserved tests that DeepSeek V3 preserves custom temperature
func TestTemperatureDeepSeekV3Preserved(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)
}
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,
},
Temperature: &customTemp, // Should be preserved
}
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: "Say 'Hello World'",
},
}
maxTokens := 20
options.MaxCompletionTokens = &maxTokens
ctx := newTemperatureTestContext("test-deepseek-v3-temp", "deepseek.v3")
// Should succeed with custom temperature preserved
response, err := llmInstance.Post(ctx, messages, options)
if err != nil {
t.Fatalf("Post failed: %v", err)
}
if response == nil {
t.Fatal("Response is nil")
}
t.Log("✓ DeepSeek V3 successfully preserved custom temperature (0.8)")
t.Logf("Response: %v", response.Content)
}
// TestTemperatureGPT5Default tests that GPT-5 with temperature=1.0 works fine
func TestTemperatureGPT5Default(t *testing.T) {
test.Prepare(t, config.Conf)
defer test.Clean()
conn, err := connector.Select("openai.gpt-5")
if err != nil {
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,
},
Temperature: &defaultTemp, // Should work fine
}
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: "What is 2+2? Reply with just the number.",
},
}
maxTokens := 10
options.MaxCompletionTokens = &maxTokens
ctx := newTemperatureTestContext("test-gpt5-temp-default", "openai.gpt-5")
// Should succeed with default temperature
response, err := llmInstance.Post(ctx, messages, options)
if err != nil {
t.Fatalf("Post failed: %v", err)
}
if response == nil {
t.Fatal("Response is nil")
}
t.Log("✓ GPT-5 successfully handled default temperature (1.0)")
t.Logf("Response: %v", response.Content)
}
// TestTemperatureNoTemperatureProvided tests that models work when no temperature is provided
func TestTemperatureNoTemperatureProvided(t *testing.T) {
test.Prepare(t, config.Conf)
defer test.Clean()
testCases := []struct {
name string
connector string
reasoning bool
}{
{"GPT-5 No Temp", "openai.gpt-5", true},
{"GPT-4o No Temp", "openai.gpt-4o", false},
{"DeepSeek R1 No Temp", "deepseek.r1", true},
{"DeepSeek V3 No Temp", "deepseek.v3", false},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
conn, err := connector.Select(tc.connector)
if err != nil {
t.Fatalf("Failed to select connector: %v", err)
}
trueVal := true
falseVal := false
options := &context.CompletionOptions{
Capabilities: &context.ModelCapabilities{
Reasoning: &falseVal,
ToolCalls: &trueVal,
},
}
if tc.reasoning {
options.Capabilities.Reasoning = &trueVal
}
// Temperature not set - should use API default
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: "Say 'OK'",
},
}
maxTokens := 10
options.MaxCompletionTokens = &maxTokens
ctx := newTemperatureTestContext("test-no-temp-"+tc.connector, tc.connector)
response, err := llmInstance.Post(ctx, messages, options)
if err != nil {
t.Fatalf("Post failed: %v", err)
}
if response == nil {
t.Fatal("Response is nil")
}
t.Logf("✓ %s works fine without temperature parameter", tc.connector)
})
}
}
// ============================================================================
// Helper Functions
// ============================================================================
// newTemperatureTestContext creates a real Context for testing temperature handling
func newTemperatureTestContext(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: "TemperatureTest/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": "temperature",
},
},
},
}
}