1
0
Files
nex/backend/internal/conversion/openai/decoder_test.go
lanyuanxiaoyao 1dac347d3b refactor: 实现 ConversionEngine 协议转换引擎,替代旧 protocol 包
引入 Canonical Model 和 ProtocolAdapter 架构,支持 OpenAI/Anthropic 协议间
无缝转换,统一 ProxyHandler 替代分散的 OpenAI/Anthropic Handler,简化
ProviderClient 为协议无关的 HTTP 发送器,Provider 新增 protocol 字段。
2026-04-20 00:36:27 +08:00

412 lines
11 KiB
Go

package openai
import (
"fmt"
"testing"
"nex/backend/internal/conversion/canonical"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestDecodeRequest_BasicChat(t *testing.T) {
body := []byte(`{
"model": "gpt-4",
"messages": [
{"role": "user", "content": "你好"}
],
"temperature": 0.7
}`)
req, err := decodeRequest(body)
require.NoError(t, err)
assert.Equal(t, "gpt-4", req.Model)
assert.Len(t, req.Messages, 1)
assert.Equal(t, canonical.RoleUser, req.Messages[0].Role)
assert.NotNil(t, req.Parameters.Temperature)
assert.Equal(t, 0.7, *req.Parameters.Temperature)
}
func TestDecodeRequest_SystemAndDeveloper(t *testing.T) {
body := []byte(`{
"model": "gpt-4",
"messages": [
{"role": "system", "content": "你是助手"},
{"role": "developer", "content": "额外指令"},
{"role": "user", "content": "你好"}
]
}`)
req, err := decodeRequest(body)
require.NoError(t, err)
assert.Equal(t, "你是助手\n\n额外指令", req.System)
assert.Len(t, req.Messages, 1)
assert.Equal(t, canonical.RoleUser, req.Messages[0].Role)
}
func TestDecodeRequest_ToolCalls(t *testing.T) {
body := []byte(`{
"model": "gpt-4",
"messages": [
{"role": "user", "content": "天气"},
{
"role": "assistant",
"tool_calls": [{
"id": "call_123",
"type": "function",
"function": {"name": "get_weather", "arguments": "{\"city\":\"北京\"}"}
}]
}
]
}`)
req, err := decodeRequest(body)
require.NoError(t, err)
assert.Len(t, req.Messages, 2)
assistantMsg := req.Messages[1]
assert.Equal(t, canonical.RoleAssistant, assistantMsg.Role)
found := false
for _, b := range assistantMsg.Content {
if b.Type == "tool_use" {
found = true
assert.Equal(t, "call_123", b.ID)
assert.Equal(t, "get_weather", b.Name)
}
}
assert.True(t, found)
}
func TestDecodeRequest_ToolMessage(t *testing.T) {
body := []byte(`{
"model": "gpt-4",
"messages": [
{"role": "user", "content": "天气"},
{
"role": "assistant",
"tool_calls": [{"id": "call_1", "type": "function", "function": {"name": "get_weather", "arguments": "{}"}}]
},
{
"role": "tool",
"tool_call_id": "call_1",
"content": "晴天 25°C"
}
]
}`)
req, err := decodeRequest(body)
require.NoError(t, err)
toolMsg := req.Messages[2]
assert.Equal(t, canonical.RoleTool, toolMsg.Role)
assert.Equal(t, "call_1", toolMsg.Content[0].ToolUseID)
}
func TestDecodeRequest_MissingModel(t *testing.T) {
body := []byte(`{"messages":[{"role":"user","content":"hi"}]}`)
_, err := decodeRequest(body)
require.Error(t, err)
assert.Contains(t, err.Error(), "INVALID_INPUT")
}
func TestDecodeRequest_MissingMessages(t *testing.T) {
body := []byte(`{"model":"gpt-4"}`)
_, err := decodeRequest(body)
require.Error(t, err)
assert.Contains(t, err.Error(), "INVALID_INPUT")
}
func TestDecodeRequest_DeprecatedFunctions(t *testing.T) {
body := []byte(`{
"model": "gpt-4",
"messages": [{"role": "user", "content": "test"}],
"functions": [{
"name": "get_weather",
"description": "获取天气",
"parameters": {"type":"object","properties":{"city":{"type":"string"}}}
}]
}`)
req, err := decodeRequest(body)
require.NoError(t, err)
assert.Len(t, req.Tools, 1)
assert.Equal(t, "get_weather", req.Tools[0].Name)
}
func TestDecodeResponse_Basic(t *testing.T) {
body := []byte(`{
"id": "chatcmpl-123",
"model": "gpt-4",
"choices": [{
"index": 0,
"message": {"role": "assistant", "content": "你好"},
"finish_reason": "stop"
}],
"usage": {"prompt_tokens": 10, "completion_tokens": 5, "total_tokens": 15}
}`)
resp, err := decodeResponse(body)
require.NoError(t, err)
assert.Equal(t, "chatcmpl-123", resp.ID)
assert.Equal(t, "gpt-4", resp.Model)
assert.Len(t, resp.Content, 1)
assert.Equal(t, "你好", resp.Content[0].Text)
assert.NotNil(t, resp.StopReason)
assert.Equal(t, canonical.StopReasonEndTurn, *resp.StopReason)
assert.Equal(t, 10, resp.Usage.InputTokens)
assert.Equal(t, 5, resp.Usage.OutputTokens)
}
func TestDecodeResponse_ToolCalls(t *testing.T) {
body := []byte(`{
"id": "chatcmpl-456",
"model": "gpt-4",
"choices": [{
"index": 0,
"message": {
"role": "assistant",
"tool_calls": [{
"id": "call_abc",
"type": "function",
"function": {"name": "search", "arguments": "{\"q\":\"test\"}"}
}]
},
"finish_reason": "tool_calls"
}]
}`)
resp, err := decodeResponse(body)
require.NoError(t, err)
found := false
for _, b := range resp.Content {
if b.Type == "tool_use" {
found = true
assert.Equal(t, "call_abc", b.ID)
assert.Equal(t, "search", b.Name)
}
}
assert.True(t, found)
assert.Equal(t, canonical.StopReasonToolUse, *resp.StopReason)
}
func TestDecodeResponse_Thinking(t *testing.T) {
body := []byte(`{
"id": "chatcmpl-789",
"model": "gpt-4",
"choices": [{
"index": 0,
"message": {
"role": "assistant",
"content": "回答",
"reasoning_content": "思考过程"
},
"finish_reason": "stop"
}]
}`)
resp, err := decodeResponse(body)
require.NoError(t, err)
assert.Len(t, resp.Content, 2)
assert.Equal(t, "回答", resp.Content[0].Text)
assert.Equal(t, "thinking", resp.Content[1].Type)
assert.Equal(t, "思考过程", resp.Content[1].Thinking)
}
func TestDecodeModelsResponse(t *testing.T) {
body := []byte(`{
"object": "list",
"data": [
{"id": "gpt-4", "object": "model", "created": 1700000000, "owned_by": "openai"},
{"id": "gpt-3.5-turbo", "object": "model", "created": 1700000001, "owned_by": "openai"}
]
}`)
list, err := decodeModelsResponse(body)
require.NoError(t, err)
assert.Len(t, list.Models, 2)
assert.Equal(t, "gpt-4", list.Models[0].ID)
assert.Equal(t, "gpt-3.5-turbo", list.Models[1].ID)
assert.Equal(t, int64(1700000000), list.Models[0].Created)
}
func TestDecodeRequest_InvalidJSON(t *testing.T) {
_, err := decodeRequest([]byte(`invalid json`))
require.Error(t, err)
assert.Contains(t, err.Error(), "JSON_PARSE_ERROR")
}
func TestDecodeRequest_Parameters(t *testing.T) {
body := []byte(`{
"model": "gpt-4",
"messages": [{"role": "user", "content": "hi"}],
"temperature": 0.5,
"max_completion_tokens": 2048,
"top_p": 0.9,
"frequency_penalty": 0.1,
"presence_penalty": 0.2,
"stop": ["STOP"]
}`)
req, err := decodeRequest(body)
require.NoError(t, err)
assert.NotNil(t, req.Parameters.Temperature)
assert.Equal(t, 0.5, *req.Parameters.Temperature)
assert.NotNil(t, req.Parameters.MaxTokens)
assert.Equal(t, 2048, *req.Parameters.MaxTokens)
assert.NotNil(t, req.Parameters.TopP)
assert.Equal(t, 0.9, *req.Parameters.TopP)
assert.NotNil(t, req.Parameters.FrequencyPenalty)
assert.Equal(t, 0.1, *req.Parameters.FrequencyPenalty)
assert.NotNil(t, req.Parameters.PresencePenalty)
assert.Equal(t, 0.2, *req.Parameters.PresencePenalty)
assert.Equal(t, []string{"STOP"}, req.Parameters.StopSequences)
}
func TestDecodeRequest_ToolChoice(t *testing.T) {
tests := []struct {
name string
jsonBody string
want *canonical.ToolChoice
}{
{
name: "auto",
jsonBody: `{"model":"gpt-4","messages":[{"role":"user","content":"hi"}],"tool_choice":"auto"}`,
want: canonical.NewToolChoiceAuto(),
},
{
name: "none",
jsonBody: `{"model":"gpt-4","messages":[{"role":"user","content":"hi"}],"tool_choice":"none"}`,
want: canonical.NewToolChoiceNone(),
},
{
name: "required",
jsonBody: `{"model":"gpt-4","messages":[{"role":"user","content":"hi"}],"tool_choice":"required"}`,
want: canonical.NewToolChoiceAny(),
},
{
name: "named",
jsonBody: `{"model":"gpt-4","messages":[{"role":"user","content":"hi"}],"tool_choice":{"type":"function","function":{"name":"x"}}}`,
want: canonical.NewToolChoiceNamed("x"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req, err := decodeRequest([]byte(tt.jsonBody))
require.NoError(t, err)
require.NotNil(t, req.ToolChoice)
assert.Equal(t, tt.want.Type, req.ToolChoice.Type)
assert.Equal(t, tt.want.Name, req.ToolChoice.Name)
})
}
}
func TestDecodeRequest_OutputFormat_JSONSchema(t *testing.T) {
body := []byte(`{
"model": "gpt-4",
"messages": [{"role": "user", "content": "hi"}],
"response_format": {
"type": "json_schema",
"json_schema": {
"name": "my_schema",
"schema": {"type":"object","properties":{"name":{"type":"string"}}},
"strict": true
}
}
}`)
req, err := decodeRequest(body)
require.NoError(t, err)
require.NotNil(t, req.OutputFormat)
assert.Equal(t, "json_schema", req.OutputFormat.Type)
assert.Equal(t, "my_schema", req.OutputFormat.Name)
assert.NotNil(t, req.OutputFormat.Schema)
require.NotNil(t, req.OutputFormat.Strict)
assert.True(t, *req.OutputFormat.Strict)
}
func TestDecodeRequest_OutputFormat_JSON(t *testing.T) {
body := []byte(`{
"model": "gpt-4",
"messages": [{"role": "user", "content": "hi"}],
"response_format": {"type": "json_object"}
}`)
req, err := decodeRequest(body)
require.NoError(t, err)
require.NotNil(t, req.OutputFormat)
assert.Equal(t, "json_object", req.OutputFormat.Type)
}
func TestDecodeResponse_StopReasons(t *testing.T) {
tests := []struct {
name string
finishReason string
want canonical.StopReason
}{
{"stop→end_turn", "stop", canonical.StopReasonEndTurn},
{"length→max_tokens", "length", canonical.StopReasonMaxTokens},
{"tool_calls→tool_use", "tool_calls", canonical.StopReasonToolUse},
{"content_filter→content_filter", "content_filter", canonical.StopReasonContentFilter},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
body := []byte(fmt.Sprintf(`{
"id": "resp-1",
"model": "gpt-4",
"choices": [{"index": 0, "message": {"role": "assistant", "content": "ok"}, "finish_reason": "%s"}],
"usage": {"prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 2}
}`, tt.finishReason))
resp, err := decodeResponse(body)
require.NoError(t, err)
require.NotNil(t, resp.StopReason)
assert.Equal(t, tt.want, *resp.StopReason)
})
}
}
func TestDecodeResponse_Usage(t *testing.T) {
body := []byte(`{
"id": "resp-1",
"model": "gpt-4",
"choices": [{"index": 0, "message": {"role": "assistant", "content": "ok"}, "finish_reason": "stop"}],
"usage": {
"prompt_tokens": 100,
"completion_tokens": 50,
"total_tokens": 150,
"prompt_tokens_details": {"cached_tokens": 80}
}
}`)
resp, err := decodeResponse(body)
require.NoError(t, err)
assert.Equal(t, 100, resp.Usage.InputTokens)
assert.Equal(t, 50, resp.Usage.OutputTokens)
require.NotNil(t, resp.Usage.CacheReadTokens)
assert.Equal(t, 80, *resp.Usage.CacheReadTokens)
}
func TestDecodeResponse_Refusal(t *testing.T) {
body := []byte(`{
"id": "resp-1",
"model": "gpt-4",
"choices": [{
"index": 0,
"message": {"role": "assistant", "content": null, "refusal": "我拒绝回答"},
"finish_reason": "stop"
}],
"usage": {"prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 2}
}`)
resp, err := decodeResponse(body)
require.NoError(t, err)
found := false
for _, b := range resp.Content {
if b.Text == "我拒绝回答" {
found = true
}
}
assert.True(t, found)
}