mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-09-24 16:05:44 +08:00
Merge pull request #3761 from alfadb/fix/responses-alias-normalization
修复 OpenAI Responses compact 端点归一化
This commit is contained in:
@@ -19,6 +19,7 @@ const (
|
||||
EndpointChatCompletions = "/v1/chat/completions"
|
||||
EndpointEmbeddings = "/v1/embeddings"
|
||||
EndpointResponses = "/v1/responses"
|
||||
EndpointResponsesCompact = "/v1/responses/compact"
|
||||
EndpointImagesGenerations = "/v1/images/generations"
|
||||
EndpointImagesEdits = "/v1/images/edits"
|
||||
EndpointVideosGenerations = "/v1/videos/generations"
|
||||
@@ -42,6 +43,33 @@ const (
|
||||
// "/v1/chat/completions" → "/v1/chat/completions"
|
||||
// "/openai/v1/responses/foo" → "/v1/responses"
|
||||
// "/v1beta/models/gemini:gen" → "/v1beta/models"
|
||||
//
|
||||
// The OpenAI Responses API is also exposed via a few bare/alias
|
||||
// routes that do not carry a "/v1/" prefix (top-level bare route and
|
||||
// the Codex direct route). "/responses/compact" (and "/backend-api/
|
||||
// codex/responses/compact") is a distinct client endpoint — the
|
||||
// "compact" client — and is normalized to its OWN canonical inbound
|
||||
// endpoint, EndpointResponsesCompact, rather than being folded into
|
||||
// the root Responses endpoint. Any other subpath under the bare/alias
|
||||
// roots (i.e. not "compact" itself or nested under it) remains a
|
||||
// subresource suffix of the root Responses endpoint:
|
||||
//
|
||||
// "/v1/responses/compact" → EndpointResponsesCompact
|
||||
// "/v1/responses/compact/detail" → EndpointResponsesCompact
|
||||
// "/openai/v1/responses/compact" → EndpointResponsesCompact
|
||||
// "/openai/v1/responses/compact/detail" → EndpointResponsesCompact
|
||||
// "/responses/compact" → EndpointResponsesCompact
|
||||
// "/responses/compact/detail" → EndpointResponsesCompact
|
||||
// "/backend-api/codex/responses/compact" → EndpointResponsesCompact
|
||||
// "/backend-api/codex/responses/compact/detail" → EndpointResponsesCompact
|
||||
// "/v1/responses" → EndpointResponses
|
||||
// "/openai/v1/responses" → EndpointResponses
|
||||
// "/responses" → EndpointResponses
|
||||
// "/backend-api/codex/responses" → EndpointResponses
|
||||
//
|
||||
// The compact check MUST be evaluated before the root Responses check,
|
||||
// otherwise "/v1/responses" (a prefix of "/v1/responses/compact")
|
||||
// would erroneously match first.
|
||||
func NormalizeInboundEndpoint(path string) string {
|
||||
path = strings.TrimSpace(path)
|
||||
switch {
|
||||
@@ -59,7 +87,9 @@ func NormalizeInboundEndpoint(path string) string {
|
||||
return EndpointVideosGenerations
|
||||
case strings.Contains(path, EndpointVideos) || strings.Contains(path, "/videos/"):
|
||||
return EndpointVideos
|
||||
case strings.Contains(path, EndpointResponses):
|
||||
case strings.Contains(path, EndpointResponsesCompact) || isResponsesCompactAliasPath(path):
|
||||
return EndpointResponsesCompact
|
||||
case strings.Contains(path, EndpointResponses) || isResponsesRootAliasPath(path):
|
||||
return EndpointResponses
|
||||
case strings.Contains(path, EndpointGeminiModels):
|
||||
return EndpointGeminiModels
|
||||
@@ -68,6 +98,59 @@ func NormalizeInboundEndpoint(path string) string {
|
||||
}
|
||||
}
|
||||
|
||||
// isResponsesCompactAliasPath reports whether path is the bare/alias
|
||||
// "compact" client endpoint — i.e. it is rooted at "/responses/compact"
|
||||
// or "/backend-api/codex/responses/compact" (bare routes that serve
|
||||
// the OpenAI Responses API "compact" client without a "/v1/" prefix),
|
||||
// or any subpath nested under either of those roots:
|
||||
//
|
||||
// - "/responses/compact" (bare route, compact client)
|
||||
// - "/responses/compact/*subpath" (nested, e.g. "/responses/compact/detail")
|
||||
// - "/backend-api/codex/responses/compact" (Codex direct route, compact client)
|
||||
// - "/backend-api/codex/responses/compact/*subpath" (nested, e.g.
|
||||
// "/backend-api/codex/responses/compact/detail")
|
||||
//
|
||||
// This MUST be checked before isResponsesRootAliasPath, since
|
||||
// "/responses" is a prefix of "/responses/compact".
|
||||
func isResponsesCompactAliasPath(path string) bool {
|
||||
trimmed := strings.TrimRight(strings.TrimSpace(path), "/")
|
||||
if trimmed == "" {
|
||||
return false
|
||||
}
|
||||
return isBareOrSubpathOf(trimmed, "/responses/compact") || isBareOrSubpathOf(trimmed, "/backend-api/codex/responses/compact")
|
||||
}
|
||||
|
||||
// isResponsesRootAliasPath reports whether path is one of the bare/alias
|
||||
// routes that serve the root OpenAI Responses API without a "/v1/"
|
||||
// prefix, or any non-"compact" subpath registered under them:
|
||||
//
|
||||
// - "/responses" (top-level bare route)
|
||||
// - "/responses/*subpath" (any subpath other than "compact",
|
||||
// since "compact" is its own distinct inbound endpoint)
|
||||
// - "/backend-api/codex/responses" (Codex direct route)
|
||||
// - "/backend-api/codex/responses/*subpath" (any subpath other than
|
||||
// "compact")
|
||||
//
|
||||
// Only the top-level bare route and the Codex direct route (and their
|
||||
// subpaths) are recognized here — this deliberately does NOT generalize
|
||||
// to any path merely ending in "/responses" (e.g. an unrelated
|
||||
// "/foo/responses" must not match).
|
||||
func isResponsesRootAliasPath(path string) bool {
|
||||
trimmed := strings.TrimRight(strings.TrimSpace(path), "/")
|
||||
if trimmed == "" {
|
||||
return false
|
||||
}
|
||||
return isBareOrSubpathOf(trimmed, "/responses") || isBareOrSubpathOf(trimmed, "/backend-api/codex/responses")
|
||||
}
|
||||
|
||||
// isBareOrSubpathOf reports whether path is exactly root, or a subpath
|
||||
// rooted at root (i.e. root followed by "/"). This anchors the match
|
||||
// at the start of path so it cannot match paths where root appears
|
||||
// nested under some other unrelated prefix.
|
||||
func isBareOrSubpathOf(path, root string) bool {
|
||||
return path == root || strings.HasPrefix(path, root+"/")
|
||||
}
|
||||
|
||||
// DeriveUpstreamEndpoint determines the upstream endpoint from the
|
||||
// account platform and the normalized inbound endpoint.
|
||||
//
|
||||
@@ -88,10 +171,20 @@ func DeriveUpstreamEndpoint(inbound, rawRequestPath, platform string) string {
|
||||
return inbound
|
||||
}
|
||||
// OpenAI forwards everything to the Responses API.
|
||||
// Preserve subresource suffix (e.g. /v1/responses/compact).
|
||||
// Preserve subresource suffix (e.g. /v1/responses/compact,
|
||||
// /v1/responses/compact/detail) as derived from the raw path.
|
||||
if suffix := responsesSubpathSuffix(rawRequestPath); suffix != "" {
|
||||
return EndpointResponses + suffix
|
||||
}
|
||||
// The raw path carried no derivable suffix (e.g. it was already
|
||||
// normalized upstream, or the caller only has the canonical
|
||||
// inbound endpoint available) — fall back to the canonical
|
||||
// compact endpoint when that's what the inbound request was
|
||||
// recognized as, so it isn't silently treated as the root
|
||||
// Responses endpoint.
|
||||
if inbound == EndpointResponsesCompact {
|
||||
return EndpointResponsesCompact
|
||||
}
|
||||
return EndpointResponses
|
||||
|
||||
case service.PlatformAnthropic:
|
||||
@@ -142,10 +235,13 @@ func responsesSubpathSuffix(rawPath string) string {
|
||||
// Apply this middleware to all gateway route groups.
|
||||
func InboundEndpointMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
path := c.FullPath()
|
||||
if path == "" && c.Request != nil && c.Request.URL != nil {
|
||||
path := ""
|
||||
if c.Request != nil && c.Request.URL != nil {
|
||||
path = c.Request.URL.Path
|
||||
}
|
||||
if path == "" {
|
||||
path = c.FullPath()
|
||||
}
|
||||
c.Set(ctxKeyInboundEndpoint, NormalizeInboundEndpoint(path))
|
||||
c.Next()
|
||||
}
|
||||
@@ -158,7 +254,11 @@ func InboundEndpointMiddleware() gin.HandlerFunc {
|
||||
|
||||
// GetInboundEndpoint returns the canonical inbound endpoint stored by
|
||||
// InboundEndpointMiddleware. If the middleware did not run (e.g. in
|
||||
// tests), it falls back to normalizing c.FullPath() on the fly.
|
||||
// tests), it falls back to normalizing c.Request.URL.Path on the fly
|
||||
// (preferring the raw request path over c.FullPath(), which collapses
|
||||
// wildcard route patterns such as "/v1/responses/*subpath" and would
|
||||
// otherwise mis-normalize concrete requests like "/v1/responses/compact"
|
||||
// to the root Responses endpoint).
|
||||
func GetInboundEndpoint(c *gin.Context) string {
|
||||
if v, ok := c.Get(ctxKeyInboundEndpoint); ok {
|
||||
if s, ok := v.(string); ok && s != "" {
|
||||
@@ -168,10 +268,12 @@ func GetInboundEndpoint(c *gin.Context) string {
|
||||
// Fallback: normalize on the fly.
|
||||
path := ""
|
||||
if c != nil {
|
||||
path = c.FullPath()
|
||||
if path == "" && c.Request != nil && c.Request.URL != nil {
|
||||
if c.Request != nil && c.Request.URL != nil {
|
||||
path = c.Request.URL.Path
|
||||
}
|
||||
if path == "" {
|
||||
path = c.FullPath()
|
||||
}
|
||||
}
|
||||
return NormalizeInboundEndpoint(path)
|
||||
}
|
||||
|
||||
@@ -26,23 +26,42 @@ func TestNormalizeInboundEndpoint(t *testing.T) {
|
||||
{"/v1/chat/completions", EndpointChatCompletions},
|
||||
{"/v1/embeddings", EndpointEmbeddings},
|
||||
{"/v1/responses", EndpointResponses},
|
||||
{"/v1/responses/compact", EndpointResponsesCompact},
|
||||
{"/v1/responses/compact/detail", EndpointResponsesCompact},
|
||||
{"/v1/images/generations", EndpointImagesGenerations},
|
||||
{"/v1/images/edits", EndpointImagesEdits},
|
||||
{"/v1/videos/generations", EndpointVideosGenerations},
|
||||
{"/v1/videos/req_123", EndpointVideos},
|
||||
{"/v1beta/models", EndpointGeminiModels},
|
||||
|
||||
// Prefixed paths (antigravity, openai).
|
||||
// Prefixed paths (antigravity, openai) — root Responses.
|
||||
{"/antigravity/v1/messages", EndpointMessages},
|
||||
{"/openai/v1/responses", EndpointResponses},
|
||||
{"/openai/v1/responses/compact", EndpointResponses},
|
||||
{"/openai/v1/images/generations", EndpointImagesGenerations},
|
||||
{"/openai/v1/images/edits", EndpointImagesEdits},
|
||||
{"/antigravity/v1beta/models/gemini:generateContent", EndpointGeminiModels},
|
||||
|
||||
// Gin route patterns with wildcards.
|
||||
{"/v1beta/models/*modelAction", EndpointGeminiModels},
|
||||
{"/v1/responses/*subpath", EndpointResponses},
|
||||
// Prefixed paths — "/responses/compact" is its OWN distinct
|
||||
// inbound endpoint, not folded into the root Responses endpoint.
|
||||
{"/openai/v1/responses/compact", EndpointResponsesCompact},
|
||||
{"/openai/v1/responses/compact/detail", EndpointResponsesCompact},
|
||||
|
||||
// Bare top-level alias route "/responses" — root vs. compact.
|
||||
{"/responses", EndpointResponses},
|
||||
{"/responses/compact", EndpointResponsesCompact},
|
||||
{"/responses/compact/detail", EndpointResponsesCompact},
|
||||
|
||||
// Bare Codex direct alias route — root vs. compact.
|
||||
{"/backend-api/codex/responses", EndpointResponses},
|
||||
{"/backend-api/codex/responses/compact", EndpointResponsesCompact},
|
||||
{"/backend-api/codex/responses/compact/detail", EndpointResponsesCompact},
|
||||
|
||||
// Must NOT generalize to arbitrary paths merely ending in
|
||||
// "/responses" (or "/responses/compact") that are unrelated to
|
||||
// the two known bare alias roots, unless they already carry a
|
||||
// supported "/v1/responses..." prefix form.
|
||||
{"/foo/responses", "/foo/responses"},
|
||||
{"/foo/responses/compact", "/foo/responses/compact"},
|
||||
|
||||
// Unknown path is returned as-is.
|
||||
{"/v1/embeddings", "/v1/embeddings"},
|
||||
@@ -74,10 +93,29 @@ func TestDeriveUpstreamEndpoint(t *testing.T) {
|
||||
// Gemini.
|
||||
{"gemini models", EndpointGeminiModels, "/v1beta/models/gemini:gen", service.PlatformGemini, EndpointGeminiModels},
|
||||
|
||||
// OpenAI — always /v1/responses.
|
||||
// OpenAI — root Responses.
|
||||
{"openai responses root", EndpointResponses, "/v1/responses", service.PlatformOpenAI, EndpointResponses},
|
||||
{"openai responses compact", EndpointResponses, "/openai/v1/responses/compact", service.PlatformOpenAI, "/v1/responses/compact"},
|
||||
{"openai responses nested", EndpointResponses, "/openai/v1/responses/compact/detail", service.PlatformOpenAI, "/v1/responses/compact/detail"},
|
||||
|
||||
// OpenAI — compact, raw path carries the derivable "/compact"
|
||||
// (or nested) suffix, which must be preserved on the upstream
|
||||
// endpoint.
|
||||
{"openai responses compact", EndpointResponsesCompact, "/openai/v1/responses/compact", service.PlatformOpenAI, "/v1/responses/compact"},
|
||||
{"openai responses nested", EndpointResponsesCompact, "/openai/v1/responses/compact/detail", service.PlatformOpenAI, "/v1/responses/compact/detail"},
|
||||
{"openai bare responses compact", EndpointResponsesCompact, "/responses/compact", service.PlatformOpenAI, "/v1/responses/compact"},
|
||||
{"openai bare responses compact detail", EndpointResponsesCompact, "/responses/compact/detail", service.PlatformOpenAI, "/v1/responses/compact/detail"},
|
||||
{"openai codex direct responses compact", EndpointResponsesCompact, "/backend-api/codex/responses/compact", service.PlatformOpenAI, "/v1/responses/compact"},
|
||||
{"openai codex direct responses compact detail", EndpointResponsesCompact, "/backend-api/codex/responses/compact/detail", service.PlatformOpenAI, "/v1/responses/compact/detail"},
|
||||
|
||||
// OpenAI — bare root alias routes normalize to root Responses.
|
||||
{"openai bare responses", EndpointResponses, "/responses", service.PlatformOpenAI, EndpointResponses},
|
||||
{"openai codex direct responses", EndpointResponses, "/backend-api/codex/responses", service.PlatformOpenAI, EndpointResponses},
|
||||
|
||||
// OpenAI — inbound is already the canonical compact endpoint but
|
||||
// the raw path carries no derivable "/responses..." suffix (e.g.
|
||||
// it was already normalized upstream). Must not silently fall
|
||||
// back to the root Responses endpoint.
|
||||
{"openai responses compact inbound only, unrelated raw path", EndpointResponsesCompact, "/v1/messages", service.PlatformOpenAI, EndpointResponsesCompact},
|
||||
|
||||
{"openai from messages", EndpointMessages, "/v1/messages", service.PlatformOpenAI, EndpointResponses},
|
||||
{"openai from completions", EndpointChatCompletions, "/v1/chat/completions", service.PlatformOpenAI, EndpointResponses},
|
||||
{"openai embeddings", EndpointEmbeddings, "/v1/embeddings", service.PlatformOpenAI, EndpointEmbeddings},
|
||||
@@ -113,6 +151,12 @@ func TestResponsesSubpathSuffix(t *testing.T) {
|
||||
{"/v1/responses/", ""},
|
||||
{"/v1/responses/compact", "/compact"},
|
||||
{"/openai/v1/responses/compact/detail", "/compact/detail"},
|
||||
{"/responses", ""},
|
||||
{"/responses/compact", "/compact"},
|
||||
{"/responses/compact/detail", "/compact/detail"},
|
||||
{"/backend-api/codex/responses", ""},
|
||||
{"/backend-api/codex/responses/compact", "/compact"},
|
||||
{"/backend-api/codex/responses/compact/detail", "/compact/detail"},
|
||||
{"/v1/messages", ""},
|
||||
{"", ""},
|
||||
}
|
||||
@@ -154,6 +198,132 @@ func TestGetInboundEndpoint_FallbackWithoutMiddleware(t *testing.T) {
|
||||
require.Equal(t, EndpointMessages, got)
|
||||
}
|
||||
|
||||
// TestInboundEndpointMiddleware_WildcardRoutes verifies that, when a
|
||||
// gateway route is registered with a Gin wildcard pattern (e.g.
|
||||
// "/v1/responses/*subpath"), InboundEndpointMiddleware normalizes based
|
||||
// on the concrete request path (c.Request.URL.Path) rather than the
|
||||
// route pattern (c.FullPath()). Using c.FullPath() here would collapse
|
||||
// every request under the wildcard — including "/v1/responses/compact"
|
||||
// — down to the literal pattern string, which never matches the
|
||||
// "compact" alias detection and would incorrectly normalize to the root
|
||||
// Responses endpoint.
|
||||
func TestInboundEndpointMiddleware_WildcardRoutes(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
routePath string
|
||||
requestPath string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "v1 responses wildcard route, compact request",
|
||||
routePath: "/v1/responses/*subpath",
|
||||
requestPath: "/v1/responses/compact",
|
||||
want: EndpointResponsesCompact,
|
||||
},
|
||||
{
|
||||
name: "bare responses wildcard route, compact request",
|
||||
routePath: "/responses/*subpath",
|
||||
requestPath: "/responses/compact",
|
||||
want: EndpointResponsesCompact,
|
||||
},
|
||||
{
|
||||
name: "codex direct wildcard route, compact request",
|
||||
routePath: "/backend-api/codex/responses/*subpath",
|
||||
requestPath: "/backend-api/codex/responses/compact",
|
||||
want: EndpointResponsesCompact,
|
||||
},
|
||||
{
|
||||
name: "v1 responses wildcard route, non-compact subpath request",
|
||||
routePath: "/v1/responses/*subpath",
|
||||
requestPath: "/v1/responses/foo",
|
||||
want: EndpointResponses,
|
||||
},
|
||||
{
|
||||
name: "bare responses wildcard route, non-compact subpath request",
|
||||
routePath: "/responses/*subpath",
|
||||
requestPath: "/responses/foo",
|
||||
want: EndpointResponses,
|
||||
},
|
||||
{
|
||||
name: "codex direct wildcard route, non-compact subpath request",
|
||||
routePath: "/backend-api/codex/responses/*subpath",
|
||||
requestPath: "/backend-api/codex/responses/foo",
|
||||
want: EndpointResponses,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
router := gin.New()
|
||||
router.Use(InboundEndpointMiddleware())
|
||||
|
||||
var captured string
|
||||
router.POST(tt.routePath, func(c *gin.Context) {
|
||||
captured = GetInboundEndpoint(c)
|
||||
c.Status(http.StatusOK)
|
||||
})
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, tt.requestPath, nil)
|
||||
rec := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
require.Equal(t, http.StatusOK, rec.Code)
|
||||
require.Equal(t, tt.want, captured)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestInboundEndpointMiddleware_GeminiWildcardRoute verifies that a Gemini
|
||||
// wildcard route (e.g. "/v1beta/models/*modelAction", used to capture the
|
||||
// ":generateContent"-style action suffix embedded in the path) is normalized
|
||||
// to EndpointGeminiModels via InboundEndpointMiddleware, using the same real
|
||||
// Gin routing path as TestInboundEndpointMiddleware_WildcardRoutes above.
|
||||
func TestInboundEndpointMiddleware_GeminiWildcardRoute(t *testing.T) {
|
||||
router := gin.New()
|
||||
router.Use(InboundEndpointMiddleware())
|
||||
|
||||
var captured string
|
||||
router.POST("/v1beta/models/*modelAction", func(c *gin.Context) {
|
||||
captured = GetInboundEndpoint(c)
|
||||
c.Status(http.StatusOK)
|
||||
})
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/v1beta/models/gemini-2.5-pro:generateContent", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
require.Equal(t, http.StatusOK, rec.Code)
|
||||
require.Equal(t, EndpointGeminiModels, captured)
|
||||
}
|
||||
|
||||
// TestGetInboundEndpoint_FallbackWildcardRouteWithoutMiddleware verifies
|
||||
// that when InboundEndpointMiddleware did NOT run (so no value is stored
|
||||
// in gin.Context), the GetInboundEndpoint fallback path still prefers
|
||||
// c.Request.URL.Path over c.FullPath(). This guards against the fallback
|
||||
// regressing to prefer c.FullPath() again, which would misnormalize
|
||||
// concrete requests matched by a wildcard route pattern (e.g.
|
||||
// "/v1/responses/*subpath" matching "/v1/responses/compact") down to
|
||||
// the root Responses endpoint.
|
||||
func TestGetInboundEndpoint_FallbackWildcardRouteWithoutMiddleware(t *testing.T) {
|
||||
router := gin.New()
|
||||
// Deliberately do NOT register InboundEndpointMiddleware.
|
||||
|
||||
var captured string
|
||||
router.POST("/v1/responses/*subpath", func(c *gin.Context) {
|
||||
// Sanity check: FullPath returns the route pattern, not the
|
||||
// concrete request path, when a wildcard route matches.
|
||||
require.Equal(t, "/v1/responses/*subpath", c.FullPath())
|
||||
captured = GetInboundEndpoint(c)
|
||||
c.Status(http.StatusOK)
|
||||
})
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/v1/responses/compact", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
require.Equal(t, http.StatusOK, rec.Code)
|
||||
require.Equal(t, EndpointResponsesCompact, captured)
|
||||
}
|
||||
|
||||
func TestGetUpstreamEndpoint_FullFlow(t *testing.T) {
|
||||
rec := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(rec)
|
||||
|
||||
@@ -85,22 +85,27 @@ func writeResponsesFailedSSE(c *gin.Context, errType, message string) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// inboundIsResponses 判断当前请求是否落在任何 /responses 路由上。
|
||||
// inboundIsResponses 判断当前请求是否落在任意 Responses 路由上
|
||||
// (不区分 root 还是 compact 变体)。
|
||||
//
|
||||
// 不能直接用 GetInboundEndpoint(c) == EndpointResponses 比较,因为
|
||||
// NormalizeInboundEndpoint 只识别包含 "/v1/responses" 子串的路径;
|
||||
// 项目里实际注册了多组路由(gateway_v1、top-level bare、codex direct),
|
||||
// 其中 r.POST("/responses", ...) 和 codexDirect.POST("/responses", ...)
|
||||
// 的 c.FullPath() 不含 "/v1/" 前缀,会被归一化为原始路径,
|
||||
// 导致协议合规终止事件没法发出去。
|
||||
// GetInboundEndpoint/NormalizeInboundEndpoint 会把 compact 变体归一化为
|
||||
// 单独的 EndpointResponsesCompact(而不是 EndpointResponses),
|
||||
// 而本函数在这里只关心“是不是 Responses 家族的请求”,
|
||||
// 不需要区分 root/compact,所以不能用那个等值比较。
|
||||
//
|
||||
// 这里用 FullPath 的后缀判断,覆盖所有变体:
|
||||
// 这里改用 FullPath 的后缀/子串判断,一次性覆盖 root 和 compact 的所有变体:
|
||||
// - /v1/responses
|
||||
// - /v1/responses/compact
|
||||
// - /responses
|
||||
// - /responses/compact
|
||||
// - /backend-api/codex/responses
|
||||
// - /backend-api/codex/responses/compact
|
||||
//
|
||||
// 对于通配路由(如 "/v1/responses/*action")注册的 FullPath 本身就带有
|
||||
// "/responses/" 子串(例如 "/v1/responses/*action"),所以下面的
|
||||
// strings.Contains(p, "/responses/") 分支同样能覆盖这些通配路由,
|
||||
// 不需要额外处理通配符本身。
|
||||
func inboundIsResponses(c *gin.Context) bool {
|
||||
if c == nil {
|
||||
return false
|
||||
|
||||
Reference in New Issue
Block a user