feat: 实现分层架构,包含 domain、service、repository 和 pkg 层
- 新增 domain 层:model、provider、route、stats 实体 - 新增 service 层:models、providers、routing、stats 业务逻辑 - 新增 repository 层:models、providers、stats 数据访问 - 新增 pkg 工具包:errors、logger、validator - 新增中间件:CORS、logging、recovery、request ID - 新增数据库迁移:初始 schema 和索引 - 新增单元测试和集成测试 - 新增规范文档:config-management、database-migration、error-handling、layered-architecture、middleware-system、request-validation、structured-logging、test-coverage - 移除 config 子包和 model_router(已迁移至分层架构)
This commit is contained in:
@@ -3,7 +3,6 @@ package openai
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
@@ -24,9 +23,6 @@ func (a *Adapter) PrepareRequest(req *ChatCompletionRequest, apiKey, baseURL str
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 调试日志:打印请求体
|
||||
fmt.Printf("[DEBUG] 请求Body: %s\n", string(body))
|
||||
|
||||
// 创建 HTTP 请求
|
||||
// baseURL 已包含版本路径(如 /v1 或 /v4),只需添加端点路径
|
||||
httpReq, err := http.NewRequest("POST", baseURL+"/chat/completions", bytes.NewReader(body))
|
||||
|
||||
190
backend/internal/protocol/openai/adapter_test.go
Normal file
190
backend/internal/protocol/openai/adapter_test.go
Normal file
@@ -0,0 +1,190 @@
|
||||
package openai
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAdapter_PrepareRequest(t *testing.T) {
|
||||
adapter := NewAdapter()
|
||||
req := &ChatCompletionRequest{
|
||||
Model: "gpt-4",
|
||||
Messages: []Message{
|
||||
{Role: "user", Content: "Hello"},
|
||||
},
|
||||
}
|
||||
|
||||
httpReq, err := adapter.PrepareRequest(req, "test-api-key", "https://api.openai.com/v1")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, httpReq)
|
||||
|
||||
assert.Equal(t, "POST", httpReq.Method)
|
||||
assert.Equal(t, "https://api.openai.com/v1/chat/completions", httpReq.URL.String())
|
||||
assert.Equal(t, "application/json", httpReq.Header.Get("Content-Type"))
|
||||
assert.Equal(t, "Bearer test-api-key", httpReq.Header.Get("Authorization"))
|
||||
|
||||
// 验证请求体
|
||||
var body ChatCompletionRequest
|
||||
err = json.NewDecoder(httpReq.Body).Decode(&body)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "gpt-4", body.Model)
|
||||
}
|
||||
|
||||
func TestAdapter_ParseResponse(t *testing.T) {
|
||||
adapter := NewAdapter()
|
||||
resp := &ChatCompletionResponse{
|
||||
ID: "chatcmpl-123",
|
||||
Object: "chat.completion",
|
||||
Created: 1234567890,
|
||||
Model: "gpt-4",
|
||||
Choices: []Choice{
|
||||
{
|
||||
Index: 0,
|
||||
Message: &Message{Role: "assistant", Content: "Hello!"},
|
||||
},
|
||||
},
|
||||
Usage: Usage{PromptTokens: 10, CompletionTokens: 5, TotalTokens: 15},
|
||||
}
|
||||
|
||||
body, err := json.Marshal(resp)
|
||||
require.NoError(t, err)
|
||||
|
||||
httpResp := &http.Response{
|
||||
StatusCode: 200,
|
||||
Body: io.NopCloser(bytes.NewReader(body)),
|
||||
}
|
||||
|
||||
result, err := adapter.ParseResponse(httpResp)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "chatcmpl-123", result.ID)
|
||||
assert.Equal(t, "gpt-4", result.Model)
|
||||
require.Len(t, result.Choices, 1)
|
||||
assert.Equal(t, "Hello!", result.Choices[0].Message.Content)
|
||||
}
|
||||
|
||||
func TestAdapter_ParseErrorResponse(t *testing.T) {
|
||||
adapter := NewAdapter()
|
||||
errResp := &ErrorResponse{
|
||||
Error: ErrorDetail{
|
||||
Message: "Invalid API key",
|
||||
Type: "invalid_request_error",
|
||||
Code: "invalid_api_key",
|
||||
},
|
||||
}
|
||||
|
||||
body, err := json.Marshal(errResp)
|
||||
require.NoError(t, err)
|
||||
|
||||
httpResp := &http.Response{
|
||||
StatusCode: 401,
|
||||
Body: io.NopCloser(bytes.NewReader(body)),
|
||||
}
|
||||
|
||||
result, err := adapter.ParseErrorResponse(httpResp)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "Invalid API key", result.Error.Message)
|
||||
assert.Equal(t, "invalid_request_error", result.Error.Type)
|
||||
}
|
||||
|
||||
func TestAdapter_ParseStreamChunk(t *testing.T) {
|
||||
adapter := NewAdapter()
|
||||
chunk := &StreamChunk{
|
||||
ID: "chatcmpl-123",
|
||||
Object: "chat.completion.chunk",
|
||||
Created: 1234567890,
|
||||
Model: "gpt-4",
|
||||
Choices: []StreamChoice{
|
||||
{
|
||||
Index: 0,
|
||||
Delta: Delta{Content: "Hello"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
data, err := json.Marshal(chunk)
|
||||
require.NoError(t, err)
|
||||
|
||||
result, err := adapter.ParseStreamChunk(data)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "chatcmpl-123", result.ID)
|
||||
require.Len(t, result.Choices, 1)
|
||||
assert.Equal(t, "Hello", result.Choices[0].Delta.Content)
|
||||
}
|
||||
|
||||
func TestParseToolCallArguments(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
wantErr bool
|
||||
}{
|
||||
{"有效JSON", `{"key": "value"}`, false},
|
||||
{"无效JSON", `not json`, true},
|
||||
{"空JSON", `{}`, false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tc := &ToolCall{
|
||||
Function: FunctionCall{Arguments: tt.input},
|
||||
}
|
||||
args, err := tc.ParseToolCallArguments()
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, args)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSerializeToolCallArguments(t *testing.T) {
|
||||
args := map[string]interface{}{"key": "value"}
|
||||
result, err := SerializeToolCallArguments(args)
|
||||
require.NoError(t, err)
|
||||
assert.JSONEq(t, `{"key": "value"}`, result)
|
||||
}
|
||||
|
||||
func TestValidateRequest(t *testing.T) {
|
||||
t.Run("有效请求", func(t *testing.T) {
|
||||
req := &ChatCompletionRequest{
|
||||
Model: "gpt-4",
|
||||
Messages: []Message{{Role: "user", Content: "hello"}},
|
||||
}
|
||||
errs := ValidateRequest(req)
|
||||
assert.Nil(t, errs)
|
||||
})
|
||||
|
||||
t.Run("缺少模型", func(t *testing.T) {
|
||||
req := &ChatCompletionRequest{
|
||||
Messages: []Message{{Role: "user", Content: "hello"}},
|
||||
}
|
||||
errs := ValidateRequest(req)
|
||||
assert.NotNil(t, errs)
|
||||
assert.Contains(t, errs["model"], "不能为空")
|
||||
})
|
||||
|
||||
t.Run("缺少消息", func(t *testing.T) {
|
||||
req := &ChatCompletionRequest{
|
||||
Model: "gpt-4",
|
||||
}
|
||||
errs := ValidateRequest(req)
|
||||
assert.NotNil(t, errs)
|
||||
assert.Contains(t, errs["messages"], "不能为空")
|
||||
})
|
||||
|
||||
t.Run("空消息列表", func(t *testing.T) {
|
||||
req := &ChatCompletionRequest{
|
||||
Model: "gpt-4",
|
||||
Messages: []Message{},
|
||||
}
|
||||
errs := ValidateRequest(req)
|
||||
assert.NotNil(t, errs)
|
||||
})
|
||||
}
|
||||
@@ -1,11 +1,18 @@
|
||||
package openai
|
||||
|
||||
import "encoding/json"
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
|
||||
pkgValidator "nex/backend/pkg/validator"
|
||||
)
|
||||
|
||||
// ChatCompletionRequest OpenAI Chat Completions API 请求结构
|
||||
type ChatCompletionRequest struct {
|
||||
Model string `json:"model"`
|
||||
Messages []Message `json:"messages"`
|
||||
Model string `json:"model" validate:"required"`
|
||||
Messages []Message `json:"messages" validate:"required,min=1"`
|
||||
Temperature *float64 `json:"temperature,omitempty"`
|
||||
MaxTokens *int `json:"max_tokens,omitempty"`
|
||||
TopP *float64 `json:"top_p,omitempty"`
|
||||
@@ -129,3 +136,25 @@ func SerializeToolCallArguments(args map[string]interface{}) (string, error) {
|
||||
}
|
||||
return string(bytes), nil
|
||||
}
|
||||
|
||||
// ValidateRequest 验证 ChatCompletionRequest
|
||||
func ValidateRequest(req *ChatCompletionRequest) map[string]string {
|
||||
errs := pkgValidator.Validate(req)
|
||||
if errs == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
validationErrors := make(map[string]string)
|
||||
for _, err := range errs.(validator.ValidationErrors) {
|
||||
field := err.Field()
|
||||
switch field {
|
||||
case "Model":
|
||||
validationErrors["model"] = "模型名称不能为空"
|
||||
case "Messages":
|
||||
validationErrors["messages"] = "消息列表不能为空"
|
||||
default:
|
||||
validationErrors[field] = fmt.Sprintf("字段 %s 验证失败: %s", field, err.Tag())
|
||||
}
|
||||
}
|
||||
return validationErrors
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user