refactor: 实现 ConversionEngine 协议转换引擎,替代旧 protocol 包
- 新增 ConversionEngine 核心引擎,支持 OpenAI 和 Anthropic 协议转换 - 添加 stream decoder/encoder 实现 - 更新 provider client 支持新引擎 - 补充单元测试和集成测试 - 更新 specs 文档
This commit is contained in:
323
backend/internal/conversion/engine_supplemental_test.go
Normal file
323
backend/internal/conversion/engine_supplemental_test.go
Normal file
@@ -0,0 +1,323 @@
|
||||
package conversion
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"nex/backend/internal/conversion/canonical"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestConversionError_WithProviderProtocol(t *testing.T) {
|
||||
err := NewConversionError(ErrorCodeInvalidInput, "test").WithProviderProtocol("anthropic")
|
||||
assert.Equal(t, "anthropic", err.ProviderProtocol)
|
||||
}
|
||||
|
||||
func TestConversionError_WithInterfaceType(t *testing.T) {
|
||||
err := NewConversionError(ErrorCodeInvalidInput, "test").WithInterfaceType("CHAT")
|
||||
assert.Equal(t, "CHAT", err.InterfaceType)
|
||||
}
|
||||
|
||||
func TestConversionError_FullBuilder(t *testing.T) {
|
||||
err := NewConversionError(ErrorCodeInvalidInput, "bad").
|
||||
WithClientProtocol("openai").
|
||||
WithProviderProtocol("anthropic").
|
||||
WithInterfaceType("CHAT").
|
||||
WithDetail("field", "model").
|
||||
WithCause(errors.New("root"))
|
||||
|
||||
assert.Equal(t, ErrorCodeInvalidInput, err.Code)
|
||||
assert.Equal(t, "openai", err.ClientProtocol)
|
||||
assert.Equal(t, "anthropic", err.ProviderProtocol)
|
||||
assert.Equal(t, "CHAT", err.InterfaceType)
|
||||
assert.Equal(t, "model", err.Details["field"])
|
||||
assert.Equal(t, "root", err.Cause.Error())
|
||||
}
|
||||
|
||||
func TestEngine_Use(t *testing.T) {
|
||||
registry := NewMemoryRegistry()
|
||||
engine := NewConversionEngine(registry)
|
||||
called := false
|
||||
engine.Use(&testMiddleware{fn: func(req *canonical.CanonicalRequest, cp, pp string, ctx *ConversionContext) (*canonical.CanonicalRequest, error) {
|
||||
called = true
|
||||
return req, nil
|
||||
}})
|
||||
|
||||
clientAdapter := newMockAdapter("client", false)
|
||||
clientAdapter.decodeReqFn = func(raw []byte) (*canonical.CanonicalRequest, error) {
|
||||
return &canonical.CanonicalRequest{Model: "test"}, nil
|
||||
}
|
||||
providerAdapter := newMockAdapter("provider", false)
|
||||
providerAdapter.encodeReqFn = func(req *canonical.CanonicalRequest, p *TargetProvider) ([]byte, error) {
|
||||
return json.Marshal(req)
|
||||
}
|
||||
_ = engine.RegisterAdapter(clientAdapter)
|
||||
_ = engine.RegisterAdapter(providerAdapter)
|
||||
|
||||
_, err := engine.ConvertHttpRequest(HTTPRequestSpec{
|
||||
URL: "/v1/chat/completions", Method: "POST", Body: []byte(`{}`),
|
||||
}, "client", "provider", NewTargetProvider("https://example.com", "key", "model"))
|
||||
require.NoError(t, err)
|
||||
assert.True(t, called)
|
||||
}
|
||||
|
||||
func TestConvertHttpRequest_DecodeError(t *testing.T) {
|
||||
registry := NewMemoryRegistry()
|
||||
engine := NewConversionEngine(registry)
|
||||
clientAdapter := newMockAdapter("client", false)
|
||||
clientAdapter.decodeReqFn = func(raw []byte) (*canonical.CanonicalRequest, error) {
|
||||
return nil, errors.New("decode failed")
|
||||
}
|
||||
_ = engine.RegisterAdapter(clientAdapter)
|
||||
_ = engine.RegisterAdapter(newMockAdapter("provider", false))
|
||||
|
||||
_, err := engine.ConvertHttpRequest(HTTPRequestSpec{
|
||||
URL: "/v1/chat/completions", Method: "POST", Body: []byte(`{}`),
|
||||
}, "client", "provider", NewTargetProvider("", "", ""))
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestConvertHttpRequest_EncodeError(t *testing.T) {
|
||||
registry := NewMemoryRegistry()
|
||||
engine := NewConversionEngine(registry)
|
||||
_ = engine.RegisterAdapter(newMockAdapter("client", false))
|
||||
providerAdapter := newMockAdapter("provider", false)
|
||||
providerAdapter.encodeReqFn = func(req *canonical.CanonicalRequest, p *TargetProvider) ([]byte, error) {
|
||||
return nil, errors.New("encode failed")
|
||||
}
|
||||
_ = engine.RegisterAdapter(providerAdapter)
|
||||
|
||||
_, err := engine.ConvertHttpRequest(HTTPRequestSpec{
|
||||
URL: "/v1/chat/completions", Method: "POST", Body: []byte(`{}`),
|
||||
}, "client", "provider", NewTargetProvider("", "", ""))
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestConvertHttpResponse_CrossProtocol(t *testing.T) {
|
||||
registry := NewMemoryRegistry()
|
||||
engine := NewConversionEngine(registry)
|
||||
|
||||
clientAdapter := newMockAdapter("client", false)
|
||||
clientAdapter.encodeRespFn = func(resp *canonical.CanonicalResponse) ([]byte, error) {
|
||||
return json.Marshal(map[string]string{"id": resp.ID})
|
||||
}
|
||||
providerAdapter := newMockAdapter("provider", false)
|
||||
providerAdapter.decodeRespFn = func(raw []byte) (*canonical.CanonicalResponse, error) {
|
||||
return &canonical.CanonicalResponse{ID: "resp-1", Model: "test"}, nil
|
||||
}
|
||||
_ = engine.RegisterAdapter(clientAdapter)
|
||||
_ = engine.RegisterAdapter(providerAdapter)
|
||||
|
||||
result, err := engine.ConvertHttpResponse(HTTPResponseSpec{
|
||||
StatusCode: 200, Body: []byte(`{"id":"resp-1"}`),
|
||||
}, "client", "provider", InterfaceTypeChat)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 200, result.StatusCode)
|
||||
assert.Contains(t, string(result.Body), "resp-1")
|
||||
}
|
||||
|
||||
func TestConvertHttpResponse_DecodeError(t *testing.T) {
|
||||
registry := NewMemoryRegistry()
|
||||
engine := NewConversionEngine(registry)
|
||||
providerAdapter := newMockAdapter("provider", false)
|
||||
providerAdapter.decodeRespFn = func(raw []byte) (*canonical.CanonicalResponse, error) {
|
||||
return nil, errors.New("decode error")
|
||||
}
|
||||
_ = engine.RegisterAdapter(providerAdapter)
|
||||
_ = engine.RegisterAdapter(newMockAdapter("client", false))
|
||||
|
||||
_, err := engine.ConvertHttpResponse(HTTPResponseSpec{Body: []byte(`{}`)}, "client", "provider", InterfaceTypeChat)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestConvertHttpRequest_EmbeddingInterface(t *testing.T) {
|
||||
registry := NewMemoryRegistry()
|
||||
engine := NewConversionEngine(registry)
|
||||
|
||||
clientAdapter := newMockAdapter("client", false)
|
||||
clientAdapter.ifaceType = InterfaceTypeEmbeddings
|
||||
clientAdapter.supportsIface = map[InterfaceType]bool{InterfaceTypeEmbeddings: true}
|
||||
clientAdapter.decodeReqFn = func(raw []byte) (*canonical.CanonicalRequest, error) {
|
||||
return &canonical.CanonicalRequest{Model: "test"}, nil
|
||||
}
|
||||
providerAdapter := newMockAdapter("provider", false)
|
||||
providerAdapter.ifaceType = InterfaceTypeEmbeddings
|
||||
providerAdapter.supportsIface = map[InterfaceType]bool{InterfaceTypeEmbeddings: true}
|
||||
_ = engine.RegisterAdapter(clientAdapter)
|
||||
_ = engine.RegisterAdapter(providerAdapter)
|
||||
|
||||
result, err := engine.ConvertHttpRequest(HTTPRequestSpec{
|
||||
URL: "/v1/embeddings", Method: "POST", Body: []byte(`{"model":"text-embedding","input":"hello"}`),
|
||||
}, "client", "provider", NewTargetProvider("https://example.com", "key", "model"))
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, result)
|
||||
}
|
||||
|
||||
func TestConvertHttpRequest_RerankInterface(t *testing.T) {
|
||||
registry := NewMemoryRegistry()
|
||||
engine := NewConversionEngine(registry)
|
||||
|
||||
clientAdapter := newMockAdapter("client", false)
|
||||
clientAdapter.ifaceType = InterfaceTypeRerank
|
||||
clientAdapter.supportsIface = map[InterfaceType]bool{InterfaceTypeRerank: true}
|
||||
providerAdapter := newMockAdapter("provider", false)
|
||||
providerAdapter.ifaceType = InterfaceTypeRerank
|
||||
providerAdapter.supportsIface = map[InterfaceType]bool{InterfaceTypeRerank: true}
|
||||
_ = engine.RegisterAdapter(clientAdapter)
|
||||
_ = engine.RegisterAdapter(providerAdapter)
|
||||
|
||||
result, err := engine.ConvertHttpRequest(HTTPRequestSpec{
|
||||
URL: "/v1/rerank", Method: "POST", Body: []byte(`{"model":"rerank","query":"test","documents":["a"]}`),
|
||||
}, "client", "provider", NewTargetProvider("https://example.com", "key", "model"))
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, result)
|
||||
}
|
||||
|
||||
func TestConvertHttpResponse_EmbeddingInterface(t *testing.T) {
|
||||
registry := NewMemoryRegistry()
|
||||
engine := NewConversionEngine(registry)
|
||||
|
||||
clientAdapter := newMockAdapter("client", false)
|
||||
clientAdapter.supportsIface = map[InterfaceType]bool{InterfaceTypeEmbeddings: true}
|
||||
providerAdapter := newMockAdapter("provider", false)
|
||||
providerAdapter.supportsIface = map[InterfaceType]bool{InterfaceTypeEmbeddings: true}
|
||||
_ = engine.RegisterAdapter(clientAdapter)
|
||||
_ = engine.RegisterAdapter(providerAdapter)
|
||||
|
||||
result, err := engine.ConvertHttpResponse(HTTPResponseSpec{
|
||||
StatusCode: 200, Body: []byte(`{"object":"list","data":[],"model":"test"}`),
|
||||
}, "client", "provider", InterfaceTypeEmbeddings)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, result)
|
||||
}
|
||||
|
||||
func TestConvertHttpResponse_RerankInterface(t *testing.T) {
|
||||
registry := NewMemoryRegistry()
|
||||
engine := NewConversionEngine(registry)
|
||||
|
||||
clientAdapter := newMockAdapter("client", false)
|
||||
clientAdapter.supportsIface = map[InterfaceType]bool{InterfaceTypeRerank: true}
|
||||
providerAdapter := newMockAdapter("provider", false)
|
||||
providerAdapter.supportsIface = map[InterfaceType]bool{InterfaceTypeRerank: true}
|
||||
_ = engine.RegisterAdapter(clientAdapter)
|
||||
_ = engine.RegisterAdapter(providerAdapter)
|
||||
|
||||
result, err := engine.ConvertHttpResponse(HTTPResponseSpec{
|
||||
StatusCode: 200, Body: []byte(`{"results":[],"model":"test"}`),
|
||||
}, "client", "provider", InterfaceTypeRerank)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, result)
|
||||
}
|
||||
|
||||
func TestConvertHttpRequest_ModelsInterface_Passthrough(t *testing.T) {
|
||||
registry := NewMemoryRegistry()
|
||||
engine := NewConversionEngine(registry)
|
||||
clientAdapter := newMockAdapter("client", false)
|
||||
clientAdapter.ifaceType = InterfaceTypeModels
|
||||
providerAdapter := newMockAdapter("provider", false)
|
||||
providerAdapter.ifaceType = InterfaceTypeModels
|
||||
_ = engine.RegisterAdapter(clientAdapter)
|
||||
_ = engine.RegisterAdapter(providerAdapter)
|
||||
|
||||
body := []byte(`{"object":"list","data":[]}`)
|
||||
result, err := engine.ConvertHttpRequest(HTTPRequestSpec{
|
||||
URL: "/v1/models", Method: "GET", Body: body,
|
||||
}, "client", "provider", NewTargetProvider("https://example.com", "key", ""))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, body, result.Body)
|
||||
}
|
||||
|
||||
func TestConvertHttpResponse_ModelsInterface(t *testing.T) {
|
||||
registry := NewMemoryRegistry()
|
||||
engine := NewConversionEngine(registry)
|
||||
clientAdapter := newMockAdapter("client", false)
|
||||
clientAdapter.supportsIface = map[InterfaceType]bool{InterfaceTypeModels: true}
|
||||
providerAdapter := newMockAdapter("provider", false)
|
||||
providerAdapter.supportsIface = map[InterfaceType]bool{InterfaceTypeModels: true}
|
||||
_ = engine.RegisterAdapter(clientAdapter)
|
||||
_ = engine.RegisterAdapter(providerAdapter)
|
||||
|
||||
result, err := engine.ConvertHttpResponse(HTTPResponseSpec{
|
||||
StatusCode: 200, Body: []byte(`{"object":"list","data":[]}`),
|
||||
}, "client", "provider", InterfaceTypeModels)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, result)
|
||||
}
|
||||
|
||||
func TestConvertHttpResponse_ModelInfoInterface(t *testing.T) {
|
||||
registry := NewMemoryRegistry()
|
||||
engine := NewConversionEngine(registry)
|
||||
clientAdapter := newMockAdapter("client", false)
|
||||
clientAdapter.supportsIface = map[InterfaceType]bool{InterfaceTypeModelInfo: true}
|
||||
providerAdapter := newMockAdapter("provider", false)
|
||||
providerAdapter.supportsIface = map[InterfaceType]bool{InterfaceTypeModelInfo: true}
|
||||
_ = engine.RegisterAdapter(clientAdapter)
|
||||
_ = engine.RegisterAdapter(providerAdapter)
|
||||
|
||||
result, err := engine.ConvertHttpResponse(HTTPResponseSpec{
|
||||
StatusCode: 200, Body: []byte(`{"id":"gpt-4","object":"model"}`),
|
||||
}, "client", "provider", InterfaceTypeModelInfo)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, result)
|
||||
}
|
||||
|
||||
func TestRegistry_ListProtocols(t *testing.T) {
|
||||
registry := NewMemoryRegistry()
|
||||
_ = registry.Register(newMockAdapter("openai", true))
|
||||
_ = registry.Register(newMockAdapter("anthropic", true))
|
||||
|
||||
protocols := registry.ListProtocols()
|
||||
assert.Len(t, protocols, 2)
|
||||
assert.Contains(t, protocols, "openai")
|
||||
assert.Contains(t, protocols, "anthropic")
|
||||
}
|
||||
|
||||
func TestRegistry_ConcurrentAccess(t *testing.T) {
|
||||
registry := NewMemoryRegistry()
|
||||
done := make(chan bool, 2)
|
||||
|
||||
go func() {
|
||||
for i := 0; i < 100; i++ {
|
||||
_ = registry.Register(newMockAdapter("proto-"+string(rune(i)), true))
|
||||
}
|
||||
done <- true
|
||||
}()
|
||||
|
||||
go func() {
|
||||
for i := 0; i < 100; i++ {
|
||||
_, _ = registry.Get("proto-" + string(rune(i)))
|
||||
}
|
||||
_ = registry.ListProtocols()
|
||||
done <- true
|
||||
}()
|
||||
|
||||
<-done
|
||||
<-done
|
||||
}
|
||||
|
||||
func TestNewConversionContext(t *testing.T) {
|
||||
ctx := NewConversionContext(InterfaceTypeChat)
|
||||
assert.NotEmpty(t, ctx.ConversionID)
|
||||
assert.Equal(t, InterfaceTypeChat, ctx.InterfaceType)
|
||||
assert.NotNil(t, ctx.Metadata)
|
||||
}
|
||||
|
||||
type testMiddleware struct {
|
||||
fn func(req *canonical.CanonicalRequest, clientProtocol, providerProtocol string, ctx *ConversionContext) (*canonical.CanonicalRequest, error)
|
||||
}
|
||||
|
||||
func (m *testMiddleware) Intercept(req *canonical.CanonicalRequest, clientProtocol, providerProtocol string, ctx *ConversionContext) (*canonical.CanonicalRequest, error) {
|
||||
if m.fn != nil {
|
||||
return m.fn(req, clientProtocol, providerProtocol, ctx)
|
||||
}
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func (m *testMiddleware) InterceptStreamEvent(event *canonical.CanonicalStreamEvent, clientProtocol, providerProtocol string, ctx *ConversionContext) (*canonical.CanonicalStreamEvent, error) {
|
||||
return event, nil
|
||||
}
|
||||
|
||||
var _ = json.Marshal
|
||||
Reference in New Issue
Block a user