mirror of
https://github.com/alibaba/higress.git
synced 2026-06-04 18:17:33 +08:00
Fix OpenAI capability rewrite dropping query string (#3168)
This commit is contained in:
@@ -27,6 +27,10 @@ func Test_getApiName(t *testing.T) {
|
|||||||
{"openai files", "/v1/files", provider.ApiNameFiles},
|
{"openai files", "/v1/files", provider.ApiNameFiles},
|
||||||
{"openai retrieve file", "/v1/files/fileid", provider.ApiNameRetrieveFile},
|
{"openai retrieve file", "/v1/files/fileid", provider.ApiNameRetrieveFile},
|
||||||
{"openai retrieve file content", "/v1/files/fileid/content", provider.ApiNameRetrieveFileContent},
|
{"openai retrieve file content", "/v1/files/fileid/content", provider.ApiNameRetrieveFileContent},
|
||||||
|
{"openai videos", "/v1/videos", provider.ApiNameVideos},
|
||||||
|
{"openai retrieve video", "/v1/videos/videoid", provider.ApiNameRetrieveVideo},
|
||||||
|
{"openai retrieve video content", "/v1/videos/videoid/content", provider.ApiNameRetrieveVideoContent},
|
||||||
|
{"openai video remix", "/v1/videos/videoid/remix", provider.ApiNameVideoRemix},
|
||||||
{"openai models", "/v1/models", provider.ApiNameModels},
|
{"openai models", "/v1/models", provider.ApiNameModels},
|
||||||
{"openai fine tuning jobs", "/v1/fine_tuning/jobs", provider.ApiNameFineTuningJobs},
|
{"openai fine tuning jobs", "/v1/fine_tuning/jobs", provider.ApiNameFineTuningJobs},
|
||||||
{"openai retrieve fine tuning job", "/v1/fine_tuning/jobs/jobid", provider.ApiNameRetrieveFineTuningJob},
|
{"openai retrieve fine tuning job", "/v1/fine_tuning/jobs/jobid", provider.ApiNameRetrieveFineTuningJob},
|
||||||
@@ -109,3 +113,7 @@ func TestFireworks(t *testing.T) {
|
|||||||
test.RunFireworksOnHttpRequestHeadersTests(t)
|
test.RunFireworksOnHttpRequestHeadersTests(t)
|
||||||
test.RunFireworksOnHttpRequestBodyTests(t)
|
test.RunFireworksOnHttpRequestBodyTests(t)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestUtil(t *testing.T) {
|
||||||
|
test.RunMapRequestPathByCapabilityTests(t)
|
||||||
|
}
|
||||||
|
|||||||
116
plugins/wasm-go/extensions/ai-proxy/test/util.go
Normal file
116
plugins/wasm-go/extensions/ai-proxy/test/util.go
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
package test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/alibaba/higress/plugins/wasm-go/extensions/ai-proxy/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
func RunMapRequestPathByCapabilityTests(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
apiName string
|
||||||
|
origin string
|
||||||
|
mapping map[string]string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "no mapping returns empty",
|
||||||
|
apiName: "openai/v1/chatcompletions",
|
||||||
|
origin: "/v1/chat/completions",
|
||||||
|
mapping: map[string]string{},
|
||||||
|
expected: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "file placeholder is replaced",
|
||||||
|
apiName: "openai/v1/retrievefile",
|
||||||
|
origin: "/openai/v1/files/file-abc",
|
||||||
|
mapping: map[string]string{
|
||||||
|
"openai/v1/retrievefile": "/v1/files/{file_id}",
|
||||||
|
},
|
||||||
|
expected: "/v1/files/file-abc",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "file content keeps query parameters",
|
||||||
|
apiName: "openai/v1/retrievefilecontent",
|
||||||
|
origin: "/openai/v1/files/file-123/content?variant=thumbnail",
|
||||||
|
mapping: map[string]string{
|
||||||
|
"openai/v1/retrievefilecontent": "/v1/files/{file_id}/content",
|
||||||
|
},
|
||||||
|
expected: "/v1/files/file-123/content?variant=thumbnail",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "file content merges query string with mapped query",
|
||||||
|
apiName: "openai/v1/retrievefilecontent",
|
||||||
|
origin: "/openai/v1/files/file-123/content?variant=thumbnail",
|
||||||
|
mapping: map[string]string{
|
||||||
|
"openai/v1/retrievefilecontent": "/v1/files/{file_id}/content?download=1",
|
||||||
|
},
|
||||||
|
expected: "/v1/files/file-123/content?download=1&variant=thumbnail",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "retrieve batch replaces batch id",
|
||||||
|
apiName: "openai/v1/retrievebatch",
|
||||||
|
origin: "/openai/v1/batches/batch-001",
|
||||||
|
mapping: map[string]string{
|
||||||
|
"openai/v1/retrievebatch": "/v1/batches/{batch_id}",
|
||||||
|
},
|
||||||
|
expected: "/v1/batches/batch-001",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "cancel batch replaces batch id",
|
||||||
|
apiName: "openai/v1/cancelbatch",
|
||||||
|
origin: "/openai/v1/batches/batch-002/cancel",
|
||||||
|
mapping: map[string]string{
|
||||||
|
"openai/v1/cancelbatch": "/v1/batches/{batch_id}/cancel",
|
||||||
|
},
|
||||||
|
expected: "/v1/batches/batch-002/cancel",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "video placeholder is replaced",
|
||||||
|
apiName: "openai/v1/retrievevideo",
|
||||||
|
origin: "/openai/v1/videos/video-xyz",
|
||||||
|
mapping: map[string]string{
|
||||||
|
"openai/v1/retrievevideo": "/v1/videos/{video_id}",
|
||||||
|
},
|
||||||
|
expected: "/v1/videos/video-xyz",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "video content placeholder with query",
|
||||||
|
apiName: "openai/v1/retrievevideocontent",
|
||||||
|
origin: "/openai/v1/videos/video-xyz/content?variant=thumbnail",
|
||||||
|
mapping: map[string]string{
|
||||||
|
"openai/v1/retrievevideocontent": "/v1/videos/{video_id}/content",
|
||||||
|
},
|
||||||
|
expected: "/v1/videos/video-xyz/content?variant=thumbnail",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "video remix placeholder is replaced",
|
||||||
|
apiName: "openai/v1/videoremix",
|
||||||
|
origin: "/openai/v1/videos/video-xyz/remix",
|
||||||
|
mapping: map[string]string{
|
||||||
|
"openai/v1/videoremix": "/v1/videos/{video_id}/remix",
|
||||||
|
},
|
||||||
|
expected: "/v1/videos/video-xyz/remix",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "non placeholder mapping returns mapped path directly",
|
||||||
|
apiName: "openai/v1/videos",
|
||||||
|
origin: "/openai/v1/videos",
|
||||||
|
mapping: map[string]string{
|
||||||
|
"openai/v1/videos": "/v1/videos",
|
||||||
|
},
|
||||||
|
expected: "/v1/videos",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
tc := tc
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
got := util.MapRequestPathByCapability(tc.apiName, tc.origin, tc.mapping)
|
||||||
|
if got != tc.expected {
|
||||||
|
t.Fatalf("expected %q, got %q", tc.expected, got)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -93,6 +93,19 @@ func MapRequestPathByCapability(apiName string, originPath string, mapping map[s
|
|||||||
if !exist {
|
if !exist {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
mappedPathOnly := mappedPath
|
||||||
|
mappedQuery := ""
|
||||||
|
if queryIndex := strings.Index(mappedPathOnly, "?"); queryIndex >= 0 {
|
||||||
|
mappedPathOnly = mappedPathOnly[:queryIndex]
|
||||||
|
mappedQuery = mappedPath[queryIndex:]
|
||||||
|
}
|
||||||
|
// 将查询字符串从原始路径中剥离,避免干扰正则匹配 video_id 等占位符
|
||||||
|
pathOnly := originPath
|
||||||
|
query := ""
|
||||||
|
if queryIndex := strings.Index(originPath, "?"); queryIndex >= 0 {
|
||||||
|
pathOnly = originPath[:queryIndex]
|
||||||
|
query = originPath[queryIndex:]
|
||||||
|
}
|
||||||
if strings.Contains(mappedPath, "{") && strings.Contains(mappedPath, "}") {
|
if strings.Contains(mappedPath, "{") && strings.Contains(mappedPath, "}") {
|
||||||
replacements := []struct {
|
replacements := []struct {
|
||||||
regx *regexp.Regexp
|
regx *regexp.Regexp
|
||||||
@@ -108,8 +121,8 @@ func MapRequestPathByCapability(apiName string, originPath string, mapping map[s
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, r := range replacements {
|
for _, r := range replacements {
|
||||||
if r.regx.MatchString(originPath) {
|
if r.regx.MatchString(pathOnly) {
|
||||||
subMatch := r.regx.FindStringSubmatch(originPath)
|
subMatch := r.regx.FindStringSubmatch(pathOnly)
|
||||||
if subMatch == nil {
|
if subMatch == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -118,12 +131,25 @@ func MapRequestPathByCapability(apiName string, originPath string, mapping map[s
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
id := subMatch[index]
|
id := subMatch[index]
|
||||||
mappedPath = r.regx.ReplaceAllStringFunc(mappedPath, func(s string) string {
|
mappedPathOnly = r.regx.ReplaceAllStringFunc(mappedPathOnly, func(s string) string {
|
||||||
return strings.Replace(s, "{"+r.key+"}", id, 1)
|
return strings.Replace(s, "{"+r.key+"}", id, 1)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if mappedQuery != "" {
|
||||||
|
mappedPath = mappedPathOnly + mappedQuery
|
||||||
|
} else {
|
||||||
|
mappedPath = mappedPathOnly
|
||||||
|
}
|
||||||
|
if query != "" {
|
||||||
|
// 保留原始查询参数,例如 variant=thumbnail
|
||||||
|
if strings.Contains(mappedPath, "?") {
|
||||||
|
mappedPath = mappedPath + "&" + strings.TrimPrefix(query, "?")
|
||||||
|
} else {
|
||||||
|
mappedPath += query
|
||||||
|
}
|
||||||
|
}
|
||||||
return mappedPath
|
return mappedPath
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user