fix(ai-proxy): ensure basePathHandling works with original protocol (#3225)

This commit is contained in:
woody
2025-12-16 20:49:21 +08:00
committed by GitHub
parent 3cc745a6f5
commit 6998800c64
6 changed files with 555 additions and 5 deletions

View File

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