1
0

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:
2026-04-16 00:47:20 +08:00
parent 915b004924
commit f18904af1e
77 changed files with 5727 additions and 1257 deletions

View File

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

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

View File

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