@@ -10,6 +10,7 @@ import (
1010 "net/http"
1111 "net/textproto"
1212 "os"
13+ "regexp"
1314 "strings"
1415 "time"
1516 "unicode/utf8"
@@ -79,6 +80,12 @@ func (p *Proxy) Forward(w http.ResponseWriter, r *http.Request) {
7980 return
8081 }
8182
83+ // 检查是否需要模拟 /models 响应
84+ if p .cfg .MockModels && p .isModelsRequest (r .URL .Path ) {
85+ p .handleMockModels (w , r , startTime , clientIP )
86+ return
87+ }
88+
8289 // 读取请求体
8390 body , err := io .ReadAll (http .MaxBytesReader (w , r .Body , p .cfg .MaxRequestBodySize ))
8491 if err != nil {
@@ -160,6 +167,46 @@ func (p *Proxy) Embeddings(w http.ResponseWriter, r *http.Request) {
160167 p .Forward (w , r )
161168}
162169
170+ // isModelsRequest 检查是否是 /models 请求
171+ // 匹配规则:
172+ // - 始终匹配 /models
173+ // - 始终匹配 /v1/models, /v2/models, /v3/models (版本前缀格式)
174+ func (p * Proxy ) isModelsRequest (path string ) bool {
175+ path = strings .TrimSuffix (path , "/" )
176+
177+ // 匹配 /models
178+ if path == "/models" {
179+ return true
180+ }
181+
182+ // 匹配 /v1/models, /v2/models, /v3/models 等
183+ if strings .HasSuffix (path , "/models" ) {
184+ prefix := strings .TrimSuffix (path , "/models" )
185+ return prefix == "/v1" || prefix == "/v2" || prefix == "/v3"
186+ }
187+
188+ return false
189+ }
190+
191+ // handleMockModels 处理模拟 /models 响应
192+ func (p * Proxy ) handleMockModels (w http.ResponseWriter , r * http.Request , startTime time.Time , clientIP string ) {
193+ duration := time .Since (startTime ).Milliseconds ()
194+
195+ // 验证本地 API Key
196+ if ! p .validateLocalAPIKey (r ) {
197+ p .writeError (w , http .StatusUnauthorized , "API Key 无效" )
198+ return
199+ }
200+
201+ // 返回模拟响应
202+ w .Header ().Set ("Content-Type" , "application/json" )
203+ w .WriteHeader (http .StatusOK )
204+ w .Write ([]byte (p .cfg .MockModelsResp ))
205+
206+ // 打印日志
207+ p .logResponse (r .Method , r .URL .Path , "mock://models" , http .StatusOK , duration , clientIP , p .cfg .MockModelsResp )
208+ }
209+
163210// validateLocalAPIKey 验证本地 API Key
164211func (p * Proxy ) validateLocalAPIKey (r * http.Request ) bool {
165212 localAPIKey := p .cfg .LocalAPIKey
@@ -437,27 +484,20 @@ func buildTargetURL(baseURL string, r *http.Request, removeVersionPath bool) str
437484 return targetURL
438485}
439486
487+ // versionPrefixRegex 匹配版本前缀的正则表达式
488+ // 匹配: /v1, /v2, /v1beta, /v2alpha, /v3rc 等(可选带尾部斜杠)
489+ var versionPrefixRegex = regexp .MustCompile (`^/?v\d+[a-z]*(?:/|$)` )
490+
440491// removeVersionPrefix 移除路径中的版本前缀(如 /v1, /v2 等)
441492func removeVersionPrefix (path string ) string {
442- // 匹配 /v1, /v2, /v1beta, /v2alpha 等版本前缀
443- // 正则匹配:/v 后面跟数字,可选跟 alpha/beta/rc 等
444- path = strings .TrimLeft (path , "/" )
445-
446- // 常见版本前缀模式
447- versionPatterns := []string {"v1/" , "v2/" , "v3/" , "v1beta/" , "v1alpha/" , "v2beta/" , "v2alpha/" }
448-
449- for _ , pattern := range versionPatterns {
450- if strings .HasPrefix (path , pattern ) {
451- return strings .TrimPrefix (path , pattern )
452- }
453- }
454-
455- // 如果是单纯的版本路径如 /v1 或 /v2(没有后续路径),返回空
456- if path == "v1" || path == "v2" || path == "v3" {
457- return ""
458- }
459-
460- return path
493+ // 使用正则匹配:/v 后面跟数字,可选跟 alpha/beta/rc 等后缀
494+ // 如果匹配到,移除版本前缀部分
495+ if versionPrefixRegex .MatchString (path ) {
496+ // 移除开头的 / 和版本号部分
497+ path = versionPrefixRegex .ReplaceAllString (path , "" )
498+ return strings .Trim (path , "/" )
499+ }
500+ return strings .Trim (path , "/" )
461501}
462502
463503func isHopByHopHeader (key string ) bool {
0 commit comments