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) }