mirror of
https://github.com/alibaba/higress.git
synced 2026-02-06 15:10:54 +08:00
fix(ai-proxy): ensure basePathHandling works with original protocol (#3225)
This commit is contained in:
@@ -106,6 +106,7 @@ func TestAzure(t *testing.T) {
|
||||
test.RunAzureOnHttpRequestBodyTests(t)
|
||||
test.RunAzureOnHttpResponseHeadersTests(t)
|
||||
test.RunAzureOnHttpResponseBodyTests(t)
|
||||
test.RunAzureBasePathHandlingTests(t)
|
||||
}
|
||||
|
||||
func TestFireworks(t *testing.T) {
|
||||
@@ -114,6 +115,10 @@ func TestFireworks(t *testing.T) {
|
||||
test.RunFireworksOnHttpRequestBodyTests(t)
|
||||
}
|
||||
|
||||
func TestMinimax(t *testing.T) {
|
||||
test.RunMinimaxBasePathHandlingTests(t)
|
||||
}
|
||||
|
||||
func TestUtil(t *testing.T) {
|
||||
test.RunMapRequestPathByCapabilityTests(t)
|
||||
}
|
||||
|
||||
@@ -174,12 +174,14 @@ func (m *azureProvider) TransformRequestBody(ctx wrapper.HttpContext, apiName Ap
|
||||
}
|
||||
|
||||
func (m *azureProvider) transformRequestPath(ctx wrapper.HttpContext, apiName ApiName) string {
|
||||
originalPath := util.GetOriginalRequestPath()
|
||||
|
||||
// When using original protocol, don't overwrite the path.
|
||||
// This ensures basePathHandling works correctly even in TransformRequestBody stage.
|
||||
if m.config.IsOriginal() {
|
||||
return originalPath
|
||||
return ""
|
||||
}
|
||||
|
||||
originalPath := util.GetOriginalRequestPath()
|
||||
|
||||
if m.serviceUrlType == azureServiceUrlTypeFull {
|
||||
log.Debugf("azureProvider: use configured path %s", m.serviceUrlFullPath)
|
||||
return m.serviceUrlFullPath
|
||||
|
||||
@@ -106,8 +106,11 @@ func (m *minimaxProvider) handleRequestBodyByChatCompletionPro(body []byte) (typ
|
||||
}
|
||||
|
||||
// Map the model and rewrite the request path.
|
||||
// When using original protocol, don't overwrite the path to ensure basePathHandling works correctly.
|
||||
request.Model = getMappedModel(request.Model, m.config.modelMapping)
|
||||
_ = util.OverwriteRequestPath(fmt.Sprintf("%s?GroupId=%s", minimaxChatCompletionProPath, m.config.minimaxGroupId))
|
||||
if !m.config.IsOriginal() {
|
||||
_ = util.OverwriteRequestPath(fmt.Sprintf("%s?GroupId=%s", minimaxChatCompletionProPath, m.config.minimaxGroupId))
|
||||
}
|
||||
|
||||
if m.config.context == nil {
|
||||
minimaxRequest := m.buildMinimaxChatCompletionProRequest(request, "")
|
||||
|
||||
@@ -973,12 +973,33 @@ func (c *ProviderConfig) handleRequestBody(
|
||||
func (c *ProviderConfig) handleRequestHeaders(provider Provider, ctx wrapper.HttpContext, apiName ApiName) {
|
||||
headers := util.GetRequestHeaders()
|
||||
originPath := headers.Get(":path")
|
||||
|
||||
// Record the path after removePrefix processing
|
||||
var removePrefixPath string
|
||||
if c.basePath != "" && c.basePathHandling == basePathHandlingRemovePrefix {
|
||||
headers.Set(":path", strings.TrimPrefix(originPath, c.basePath))
|
||||
removePrefixPath = strings.TrimPrefix(originPath, c.basePath)
|
||||
headers.Set(":path", removePrefixPath)
|
||||
}
|
||||
|
||||
if handler, ok := provider.(TransformRequestHeadersHandler); ok {
|
||||
handler.TransformRequestHeaders(ctx, apiName, headers)
|
||||
}
|
||||
|
||||
// When using original protocol with removePrefix, restore the basePath-processed path.
|
||||
// This ensures basePathHandling works correctly even when TransformRequestHeaders
|
||||
// overwrites the path (which most providers do).
|
||||
//
|
||||
// TODO: Most providers (OpenAI, vLLM, DeepSeek, Claude, etc.) unconditionally overwrite
|
||||
// the path in TransformRequestHeaders without checking IsOriginal(). Ideally, each provider
|
||||
// should check IsOriginal() before overwriting the path (like Qwen does). Once all providers
|
||||
// are updated to handle protocol correctly, this workaround can be removed.
|
||||
// Affected providers: OpenAI, vLLM, ZhipuAI, Moonshot, Longcat, DeepSeek, Azure, Yi,
|
||||
// TogetherAI, Stepfun, Ollama, Hunyuan, GitHub, Doubao, Cohere, Baichuan, AI360, Claude,
|
||||
// Groq, Grok, Spark, Fireworks, Cloudflare, Baidu, OpenRouter, DeepL (24+ providers)
|
||||
if c.IsOriginal() && removePrefixPath != "" {
|
||||
headers.Set(":path", removePrefixPath)
|
||||
}
|
||||
|
||||
if c.basePath != "" && c.basePathHandling == basePathHandlingPrepend && !strings.HasPrefix(headers.Get(":path"), c.basePath) {
|
||||
headers.Set(":path", path.Join(c.basePath, headers.Get(":path")))
|
||||
}
|
||||
|
||||
@@ -160,6 +160,59 @@ var azureResponseAPIConfig = func() json.RawMessage {
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:Azure OpenAI basePath移除 + original协议
|
||||
var azureBasePathRemovePrefixOriginalConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"provider": map[string]interface{}{
|
||||
"type": "azure",
|
||||
"apiTokens": []string{
|
||||
"sk-azure-basepath-original",
|
||||
},
|
||||
"azureServiceUrl": "https://basepath-test.openai.azure.com/openai/deployments/gpt-4/chat/completions?api-version=2024-02-15-preview",
|
||||
"basePath": "/azure-gpt4",
|
||||
"basePathHandling": "removePrefix",
|
||||
"protocol": "original",
|
||||
},
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:Azure OpenAI basePath移除 + openai协议
|
||||
var azureBasePathRemovePrefixOpenAIConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"provider": map[string]interface{}{
|
||||
"type": "azure",
|
||||
"apiTokens": []string{
|
||||
"sk-azure-basepath-openai",
|
||||
},
|
||||
"azureServiceUrl": "https://basepath-test.openai.azure.com/openai/deployments/gpt-4/chat/completions?api-version=2024-02-15-preview",
|
||||
"basePath": "/azure-gpt4",
|
||||
"basePathHandling": "removePrefix",
|
||||
"modelMapping": map[string]string{
|
||||
"*": "gpt-4",
|
||||
},
|
||||
},
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:Azure OpenAI basePath prepend + original协议
|
||||
var azureBasePathPrependOriginalConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"provider": map[string]interface{}{
|
||||
"type": "azure",
|
||||
"apiTokens": []string{
|
||||
"sk-azure-prepend-original",
|
||||
},
|
||||
"azureServiceUrl": "https://prepend-test.openai.azure.com/openai/deployments/gpt-4/chat/completions?api-version=2024-02-15-preview",
|
||||
"basePath": "/api/v1",
|
||||
"basePathHandling": "prepend",
|
||||
"protocol": "original",
|
||||
},
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
func RunAzureParseConfigTests(t *testing.T) {
|
||||
test.RunGoTest(t, func(t *testing.T) {
|
||||
// 测试基本Azure OpenAI配置解析
|
||||
@@ -682,3 +735,218 @@ func RunAzureOnHttpResponseBodyTests(t *testing.T) {
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// RunAzureBasePathHandlingTests 测试 basePath 处理在不同协议下的行为
|
||||
func RunAzureBasePathHandlingTests(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
// 核心用例:测试 basePath removePrefix 在 original 协议下能正常工作
|
||||
// 重要:此测试验证在 TransformRequestBody 阶段后 path 仍然保持正确
|
||||
// 之前的 bug 是 transformRequestPath 在 IsOriginal() 时返回 originalPath,
|
||||
// 导致在 Body 阶段 path 被重新覆盖为包含 basePath 的原始路径
|
||||
t.Run("azure basePath removePrefix with original protocol after body processing", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(azureBasePathRemovePrefixOriginalConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 模拟带有 basePath 前缀的请求
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/azure-gpt4/v1/chat/completions"},
|
||||
{":method", "POST"},
|
||||
{"Content-Type", "application/json"},
|
||||
})
|
||||
require.Equal(t, types.HeaderStopIteration, action)
|
||||
|
||||
// 在 Headers 阶段后验证 path(此时 handleRequestHeaders 已执行)
|
||||
headersAfterHeaderStage := host.GetRequestHeaders()
|
||||
pathAfterHeaders, _ := test.GetHeaderValue(headersAfterHeaderStage, ":path")
|
||||
// Headers 阶段后,basePath 应该已被移除
|
||||
require.NotContains(t, pathAfterHeaders, "/azure-gpt4",
|
||||
"After headers stage: basePath should be removed")
|
||||
|
||||
// 执行 Body 阶段(此时 TransformRequestBody 会被调用)
|
||||
requestBody := `{"model": "gpt-4", "messages": [{"role": "user", "content": "Hello"}]}`
|
||||
action = host.CallOnHttpRequestBody([]byte(requestBody))
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
// 核心验证:在 Body 阶段后验证 path
|
||||
// 这是关键测试点:确保 TransformRequestBody 中的 transformRequestPath
|
||||
// 不会将 path 重新覆盖为包含 basePath 的原始路径
|
||||
requestHeaders := host.GetRequestHeaders()
|
||||
require.NotNil(t, requestHeaders)
|
||||
|
||||
pathValue, hasPath := test.GetHeaderValue(requestHeaders, ":path")
|
||||
require.True(t, hasPath, "Path header should exist")
|
||||
// basePath "/azure-gpt4" 不应该出现在最终路径中
|
||||
require.NotContains(t, pathValue, "/azure-gpt4",
|
||||
"After body stage: basePath should still be removed (not restored by TransformRequestBody)")
|
||||
// 路径应该是移除 basePath 后的结果
|
||||
require.Equal(t, "/v1/chat/completions", pathValue,
|
||||
"Path should be the original path without basePath after full request processing")
|
||||
|
||||
// 验证 Host 被正确设置
|
||||
hostValue, hasHost := test.GetHeaderValue(requestHeaders, ":authority")
|
||||
require.True(t, hasHost, "Host header should exist")
|
||||
require.Equal(t, "basepath-test.openai.azure.com", hostValue)
|
||||
|
||||
// 验证 api-key 被正确设置
|
||||
apiKeyValue, hasApiKey := test.GetHeaderValue(requestHeaders, "api-key")
|
||||
require.True(t, hasApiKey, "api-key header should exist")
|
||||
require.Equal(t, "sk-azure-basepath-original", apiKeyValue)
|
||||
})
|
||||
|
||||
// 测试 basePath removePrefix 在 openai 协议下的行为
|
||||
// 在 openai 协议下,path 会被转换为 Azure 格式,但 basePath 仍然应该被移除
|
||||
t.Run("azure basePath removePrefix with openai protocol after body processing", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(azureBasePathRemovePrefixOpenAIConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 模拟带有 basePath 前缀的请求
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/azure-gpt4/v1/chat/completions"},
|
||||
{":method", "POST"},
|
||||
{"Content-Type", "application/json"},
|
||||
})
|
||||
require.Equal(t, types.HeaderStopIteration, action)
|
||||
|
||||
// 执行 Body 阶段(TransformRequestBody 会被调用)
|
||||
requestBody := `{"model": "gpt-4", "messages": [{"role": "user", "content": "Hello"}]}`
|
||||
action = host.CallOnHttpRequestBody([]byte(requestBody))
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
// 在 Body 阶段后验证请求头
|
||||
requestHeaders := host.GetRequestHeaders()
|
||||
require.NotNil(t, requestHeaders)
|
||||
|
||||
// basePath 应该被移除,路径会被转换为 Azure 路径格式
|
||||
pathValue, hasPath := test.GetHeaderValue(requestHeaders, ":path")
|
||||
require.True(t, hasPath, "Path header should exist")
|
||||
// basePath "/azure-gpt4" 不应该出现在最终路径中
|
||||
require.NotContains(t, pathValue, "/azure-gpt4",
|
||||
"After body stage: basePath should be removed from path")
|
||||
// 在 openai 协议下,路径会被转换为 Azure 的路径格式
|
||||
require.Contains(t, pathValue, "/openai/deployments/gpt-4/chat/completions",
|
||||
"Path should be transformed to Azure format")
|
||||
require.Contains(t, pathValue, "api-version=2024-02-15-preview",
|
||||
"Path should contain API version")
|
||||
})
|
||||
|
||||
// 测试 basePath prepend 在 original 协议下能正常工作
|
||||
// 验证在 Body 阶段后 prepend 的 basePath 仍然保持
|
||||
t.Run("azure basePath prepend with original protocol after body processing", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(azureBasePathPrependOriginalConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 模拟不带 basePath 的请求
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/chat/completions"},
|
||||
{":method", "POST"},
|
||||
{"Content-Type", "application/json"},
|
||||
})
|
||||
require.Equal(t, types.HeaderStopIteration, action)
|
||||
|
||||
// 执行 Body 阶段
|
||||
requestBody := `{"model": "gpt-4", "messages": [{"role": "user", "content": "Hello"}]}`
|
||||
action = host.CallOnHttpRequestBody([]byte(requestBody))
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
// 在 Body 阶段后验证请求头
|
||||
requestHeaders := host.GetRequestHeaders()
|
||||
require.NotNil(t, requestHeaders)
|
||||
|
||||
// 验证 basePath 被正确添加且在 Body 阶段后保持
|
||||
pathValue, hasPath := test.GetHeaderValue(requestHeaders, ":path")
|
||||
require.True(t, hasPath, "Path header should exist")
|
||||
// basePath "/api/v1" 应该被添加到路径前面
|
||||
require.True(t, strings.HasPrefix(pathValue, "/api/v1"),
|
||||
"After body stage: Path should still start with prepended basePath")
|
||||
})
|
||||
|
||||
// 测试 original 协议下请求体不被修改,同时验证 path 处理
|
||||
t.Run("azure original protocol preserves request body and path", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(azureBasePathRemovePrefixOriginalConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/azure-gpt4/v1/chat/completions"},
|
||||
{":method", "POST"},
|
||||
{"Content-Type", "application/json"},
|
||||
})
|
||||
require.Equal(t, types.HeaderStopIteration, action)
|
||||
|
||||
// 设置请求体(包含自定义字段)
|
||||
requestBody := `{
|
||||
"model": "custom-model-name",
|
||||
"messages": [{"role": "user", "content": "Hello"}],
|
||||
"custom_field": "custom_value"
|
||||
}`
|
||||
action = host.CallOnHttpRequestBody([]byte(requestBody))
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
// 验证请求体被保持原样
|
||||
transformedBody := host.GetRequestBody()
|
||||
require.NotNil(t, transformedBody)
|
||||
|
||||
var bodyMap map[string]interface{}
|
||||
err := json.Unmarshal(transformedBody, &bodyMap)
|
||||
require.NoError(t, err)
|
||||
|
||||
// model 应该保持原样(original 协议不做模型映射)
|
||||
model, exists := bodyMap["model"]
|
||||
require.True(t, exists, "Model should exist")
|
||||
require.Equal(t, "custom-model-name", model, "Model should remain unchanged")
|
||||
|
||||
// 自定义字段应该保持原样
|
||||
customField, exists := bodyMap["custom_field"]
|
||||
require.True(t, exists, "Custom field should exist")
|
||||
require.Equal(t, "custom_value", customField, "Custom field should remain unchanged")
|
||||
|
||||
// 同时验证 path 在 Body 阶段后仍然正确
|
||||
requestHeaders := host.GetRequestHeaders()
|
||||
pathValue, hasPath := test.GetHeaderValue(requestHeaders, ":path")
|
||||
require.True(t, hasPath, "Path header should exist")
|
||||
require.NotContains(t, pathValue, "/azure-gpt4",
|
||||
"After body stage: basePath should be removed")
|
||||
require.Equal(t, "/v1/chat/completions", pathValue,
|
||||
"Path should be correct after body processing")
|
||||
})
|
||||
|
||||
// 测试无 basePath 前缀的请求(removePrefix 配置不影响)
|
||||
// 验证在 Body 阶段后 path 仍然保持正确
|
||||
t.Run("azure request without basePath prefix after body processing", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(azureBasePathRemovePrefixOriginalConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 模拟不带 basePath 前缀的请求
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/v1/chat/completions"},
|
||||
{":method", "POST"},
|
||||
{"Content-Type", "application/json"},
|
||||
})
|
||||
require.Equal(t, types.HeaderStopIteration, action)
|
||||
|
||||
// 执行 Body 阶段
|
||||
requestBody := `{"model": "gpt-4", "messages": [{"role": "user", "content": "Hello"}]}`
|
||||
action = host.CallOnHttpRequestBody([]byte(requestBody))
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
// 在 Body 阶段后验证请求头
|
||||
requestHeaders := host.GetRequestHeaders()
|
||||
pathValue, hasPath := test.GetHeaderValue(requestHeaders, ":path")
|
||||
require.True(t, hasPath, "Path header should exist")
|
||||
// 路径应该保持原样(没有 basePath 前缀时,removePrefix 不会改变 path)
|
||||
// 同时验证 TransformRequestBody 没有覆盖 path
|
||||
require.Equal(t, "/v1/chat/completions", pathValue,
|
||||
"After body stage: Path should remain unchanged when no basePath prefix")
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
251
plugins/wasm-go/extensions/ai-proxy/test/minimax.go
Normal file
251
plugins/wasm-go/extensions/ai-proxy/test/minimax.go
Normal file
@@ -0,0 +1,251 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
|
||||
"github.com/higress-group/wasm-go/pkg/test"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// 测试配置:Minimax Pro API + basePath removePrefix + original 协议
|
||||
var minimaxProBasePathRemovePrefixOriginalConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"provider": map[string]interface{}{
|
||||
"type": "minimax",
|
||||
"apiTokens": []string{
|
||||
"sk-minimax-test",
|
||||
},
|
||||
"minimaxApiType": "pro",
|
||||
"minimaxGroupId": "test-group-id",
|
||||
"basePath": "/minimax-api",
|
||||
"basePathHandling": "removePrefix",
|
||||
"protocol": "original",
|
||||
},
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:Minimax Pro API + basePath removePrefix + 默认协议(openai)
|
||||
var minimaxProBasePathRemovePrefixOpenAIConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"provider": map[string]interface{}{
|
||||
"type": "minimax",
|
||||
"apiTokens": []string{
|
||||
"sk-minimax-openai",
|
||||
},
|
||||
"minimaxApiType": "pro",
|
||||
"minimaxGroupId": "test-group-id",
|
||||
"basePath": "/minimax-api",
|
||||
"basePathHandling": "removePrefix",
|
||||
},
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:Minimax V2 API + basePath removePrefix + original 协议
|
||||
var minimaxV2BasePathRemovePrefixOriginalConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"provider": map[string]interface{}{
|
||||
"type": "minimax",
|
||||
"apiTokens": []string{
|
||||
"sk-minimax-v2",
|
||||
},
|
||||
"minimaxApiType": "v2",
|
||||
"basePath": "/minimax-v2",
|
||||
"basePathHandling": "removePrefix",
|
||||
"protocol": "original",
|
||||
},
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// RunMinimaxBasePathHandlingTests 测试 Minimax basePath 处理在不同协议下的行为
|
||||
func RunMinimaxBasePathHandlingTests(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
// 核心用例:测试 Minimax Pro API + basePath removePrefix + original 协议
|
||||
// 重要:此测试验证在 handleRequestBodyByChatCompletionPro 阶段后 path 仍然保持正确
|
||||
// 之前的 bug 是 handleRequestBodyByChatCompletionPro 无条件覆盖 path,
|
||||
// 导致在 Body 阶段 path 被重新覆盖为 minimaxChatCompletionProPath
|
||||
t.Run("minimax pro basePath removePrefix with original protocol after body processing", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(minimaxProBasePathRemovePrefixOriginalConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 模拟带有 basePath 前缀的请求
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/minimax-api/v1/chat/completions"},
|
||||
{":method", "POST"},
|
||||
{"Content-Type", "application/json"},
|
||||
})
|
||||
require.Equal(t, types.HeaderStopIteration, action)
|
||||
|
||||
// 在 Headers 阶段后验证 path(此时 handleRequestHeaders 已执行)
|
||||
headersAfterHeaderStage := host.GetRequestHeaders()
|
||||
pathAfterHeaders, _ := test.GetHeaderValue(headersAfterHeaderStage, ":path")
|
||||
// Headers 阶段后,basePath 应该已被移除
|
||||
require.NotContains(t, pathAfterHeaders, "/minimax-api",
|
||||
"After headers stage: basePath should be removed")
|
||||
|
||||
// 执行 Body 阶段(此时 handleRequestBodyByChatCompletionPro 会被调用)
|
||||
requestBody := `{"model": "abab5.5-chat", "messages": [{"role": "user", "content": "Hello"}]}`
|
||||
action = host.CallOnHttpRequestBody([]byte(requestBody))
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
// 核心验证:在 Body 阶段后验证 path
|
||||
// 这是关键测试点:确保 handleRequestBodyByChatCompletionPro
|
||||
// 不会将 path 重新覆盖为 minimaxChatCompletionProPath
|
||||
requestHeaders := host.GetRequestHeaders()
|
||||
require.NotNil(t, requestHeaders)
|
||||
|
||||
pathValue, hasPath := test.GetHeaderValue(requestHeaders, ":path")
|
||||
require.True(t, hasPath, "Path header should exist")
|
||||
// basePath "/minimax-api" 不应该出现在最终路径中
|
||||
require.NotContains(t, pathValue, "/minimax-api",
|
||||
"After body stage: basePath should still be removed")
|
||||
// original 协议下,path 不应该被覆盖为 minimaxChatCompletionProPath
|
||||
require.NotContains(t, pathValue, "chatcompletion_pro",
|
||||
"With original protocol: path should not be overwritten to minimax pro path")
|
||||
// 路径应该是移除 basePath 后的结果
|
||||
require.Equal(t, "/v1/chat/completions", pathValue,
|
||||
"Path should be the original path without basePath after full request processing")
|
||||
|
||||
// 验证 Host 被正确设置
|
||||
hostValue, hasHost := test.GetHeaderValue(requestHeaders, ":authority")
|
||||
require.True(t, hasHost, "Host header should exist")
|
||||
require.Equal(t, "api.minimax.chat", hostValue)
|
||||
|
||||
// 验证 Authorization 被正确设置
|
||||
authValue, hasAuth := test.GetHeaderValue(requestHeaders, "Authorization")
|
||||
require.True(t, hasAuth, "Authorization header should exist")
|
||||
require.Equal(t, "Bearer sk-minimax-test", authValue)
|
||||
})
|
||||
|
||||
// 测试 Minimax Pro API + basePath removePrefix + 默认协议(openai)
|
||||
// 在 openai 协议下,path 应该被覆盖为 minimaxChatCompletionProPath
|
||||
t.Run("minimax pro basePath removePrefix with openai protocol after body processing", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(minimaxProBasePathRemovePrefixOpenAIConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 模拟带有 basePath 前缀的请求
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/minimax-api/v1/chat/completions"},
|
||||
{":method", "POST"},
|
||||
{"Content-Type", "application/json"},
|
||||
})
|
||||
require.Equal(t, types.HeaderStopIteration, action)
|
||||
|
||||
// 执行 Body 阶段
|
||||
requestBody := `{"model": "abab5.5-chat", "messages": [{"role": "user", "content": "Hello"}]}`
|
||||
action = host.CallOnHttpRequestBody([]byte(requestBody))
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
// 在 Body 阶段后验证请求头
|
||||
requestHeaders := host.GetRequestHeaders()
|
||||
require.NotNil(t, requestHeaders)
|
||||
|
||||
pathValue, hasPath := test.GetHeaderValue(requestHeaders, ":path")
|
||||
require.True(t, hasPath, "Path header should exist")
|
||||
// basePath "/minimax-api" 不应该出现在最终路径中
|
||||
require.NotContains(t, pathValue, "/minimax-api",
|
||||
"After body stage: basePath should be removed from path")
|
||||
// 在 openai 协议下,path 应该被覆盖为 minimaxChatCompletionProPath
|
||||
require.True(t, strings.Contains(pathValue, "chatcompletion_pro"),
|
||||
"With openai protocol: path should be overwritten to minimax pro path")
|
||||
require.Contains(t, pathValue, "GroupId=test-group-id",
|
||||
"Path should contain GroupId parameter")
|
||||
})
|
||||
|
||||
// 测试 Minimax V2 API + basePath removePrefix + original 协议
|
||||
// V2 API 使用 handleRequestBody 而不是 handleRequestBodyByChatCompletionPro
|
||||
t.Run("minimax v2 basePath removePrefix with original protocol after body processing", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(minimaxV2BasePathRemovePrefixOriginalConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 模拟带有 basePath 前缀的请求
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/minimax-v2/v1/chat/completions"},
|
||||
{":method", "POST"},
|
||||
{"Content-Type", "application/json"},
|
||||
})
|
||||
require.Equal(t, types.HeaderStopIteration, action)
|
||||
|
||||
// 执行 Body 阶段
|
||||
requestBody := `{"model": "abab5.5-chat", "messages": [{"role": "user", "content": "Hello"}]}`
|
||||
action = host.CallOnHttpRequestBody([]byte(requestBody))
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
// 在 Body 阶段后验证请求头
|
||||
requestHeaders := host.GetRequestHeaders()
|
||||
require.NotNil(t, requestHeaders)
|
||||
|
||||
pathValue, hasPath := test.GetHeaderValue(requestHeaders, ":path")
|
||||
require.True(t, hasPath, "Path header should exist")
|
||||
// basePath "/minimax-v2" 不应该出现在最终路径中
|
||||
require.NotContains(t, pathValue, "/minimax-v2",
|
||||
"After body stage: basePath should be removed from path")
|
||||
// 路径应该是移除 basePath 后的结果
|
||||
require.Equal(t, "/v1/chat/completions", pathValue,
|
||||
"Path should be the original path without basePath")
|
||||
})
|
||||
|
||||
// 测试 original 协议下请求体保持原样
|
||||
t.Run("minimax pro original protocol preserves request body and path", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(minimaxProBasePathRemovePrefixOriginalConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/minimax-api/v1/chat/completions"},
|
||||
{":method", "POST"},
|
||||
{"Content-Type", "application/json"},
|
||||
})
|
||||
require.Equal(t, types.HeaderStopIteration, action)
|
||||
|
||||
// 设置请求体(包含自定义字段)
|
||||
requestBody := `{
|
||||
"model": "custom-model",
|
||||
"messages": [{"role": "user", "content": "Hello"}],
|
||||
"custom_field": "custom_value"
|
||||
}`
|
||||
action = host.CallOnHttpRequestBody([]byte(requestBody))
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
// 验证请求体被保持原样
|
||||
transformedBody := host.GetRequestBody()
|
||||
require.NotNil(t, transformedBody)
|
||||
|
||||
var bodyMap map[string]interface{}
|
||||
err := json.Unmarshal(transformedBody, &bodyMap)
|
||||
require.NoError(t, err)
|
||||
|
||||
// model 应该保持原样(original 协议不做模型映射)
|
||||
model, exists := bodyMap["model"]
|
||||
require.True(t, exists, "Model should exist")
|
||||
require.Equal(t, "custom-model", model, "Model should remain unchanged")
|
||||
|
||||
// 自定义字段应该保持原样
|
||||
customField, exists := bodyMap["custom_field"]
|
||||
require.True(t, exists, "Custom field should exist")
|
||||
require.Equal(t, "custom_value", customField, "Custom field should remain unchanged")
|
||||
|
||||
// 同时验证 path 在 Body 阶段后仍然正确
|
||||
requestHeaders := host.GetRequestHeaders()
|
||||
pathValue, hasPath := test.GetHeaderValue(requestHeaders, ":path")
|
||||
require.True(t, hasPath, "Path header should exist")
|
||||
require.NotContains(t, pathValue, "/minimax-api",
|
||||
"After body stage: basePath should be removed")
|
||||
require.Equal(t, "/v1/chat/completions", pathValue,
|
||||
"Path should be correct after body processing")
|
||||
})
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user