mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-15 23:22:59 +08:00
/v1/responses/compact 及其别名路径(/responses/compact、/openai/v1/responses/compact、 /backend-api/codex/responses/compact 等,含嵌套子路径)是独立的客户端端点, 新增 EndpointResponsesCompact 常量并单独归一化,不再与根 /v1/responses 混同。 根 Responses 别名(/responses、/backend-api/codex/responses 等)仍归一到 EndpointResponses。NormalizeInboundEndpoint 优先匹配 compact 分支,避免根路径 前缀提前命中。DeriveUpstreamEndpoint 在 inbound 已为 compact 但原始路径无法 派生后缀时,回退到 compact 而非根 upstream。
222 lines
10 KiB
Go
222 lines
10 KiB
Go
package handler
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/Wei-Shaw/sub2api/internal/service"
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func init() { gin.SetMode(gin.TestMode) }
|
|
|
|
// ──────────────────────────────────────────────────────────
|
|
// NormalizeInboundEndpoint
|
|
// ──────────────────────────────────────────────────────────
|
|
|
|
func TestNormalizeInboundEndpoint(t *testing.T) {
|
|
tests := []struct {
|
|
path string
|
|
want string
|
|
}{
|
|
// Direct canonical paths.
|
|
{"/v1/messages", EndpointMessages},
|
|
{"/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) — root Responses.
|
|
{"/antigravity/v1/messages", EndpointMessages},
|
|
{"/openai/v1/responses", EndpointResponses},
|
|
{"/openai/v1/images/generations", EndpointImagesGenerations},
|
|
{"/openai/v1/images/edits", EndpointImagesEdits},
|
|
{"/antigravity/v1beta/models/gemini:generateContent", EndpointGeminiModels},
|
|
|
|
// 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"},
|
|
|
|
// Gin route patterns with wildcards. The literal wildcard token
|
|
// ("*subpath") is not the "compact" segment itself, so these
|
|
// generic FullPath patterns normalize to the root Responses
|
|
// endpoint; only a concrete "compact" path segment (tested above)
|
|
// resolves to EndpointResponsesCompact.
|
|
{"/v1beta/models/*modelAction", EndpointGeminiModels},
|
|
{"/v1/responses/*subpath", EndpointResponses},
|
|
{"/responses/*subpath", EndpointResponses},
|
|
{"/backend-api/codex/responses/*subpath", EndpointResponses},
|
|
|
|
// Unknown path is returned as-is.
|
|
{"/v1/embeddings", "/v1/embeddings"},
|
|
{"", ""},
|
|
{" /v1/messages ", EndpointMessages},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.path, func(t *testing.T) {
|
|
require.Equal(t, tt.want, NormalizeInboundEndpoint(tt.path))
|
|
})
|
|
}
|
|
}
|
|
|
|
// ──────────────────────────────────────────────────────────
|
|
// DeriveUpstreamEndpoint
|
|
// ──────────────────────────────────────────────────────────
|
|
|
|
func TestDeriveUpstreamEndpoint(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
inbound string
|
|
rawPath string
|
|
platform string
|
|
want string
|
|
}{
|
|
// Anthropic.
|
|
{"anthropic messages", EndpointMessages, "/v1/messages", service.PlatformAnthropic, EndpointMessages},
|
|
|
|
// Gemini.
|
|
{"gemini models", EndpointGeminiModels, "/v1beta/models/gemini:gen", service.PlatformGemini, EndpointGeminiModels},
|
|
|
|
// OpenAI — root Responses.
|
|
{"openai responses root", EndpointResponses, "/v1/responses", service.PlatformOpenAI, EndpointResponses},
|
|
|
|
// 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},
|
|
{"openai image generations", EndpointImagesGenerations, "/v1/images/generations", service.PlatformOpenAI, EndpointImagesGenerations},
|
|
{"openai image edits", EndpointImagesEdits, "/openai/v1/images/edits", service.PlatformOpenAI, EndpointImagesEdits},
|
|
{"grok video generations", EndpointVideosGenerations, "/v1/videos/generations", service.PlatformGrok, EndpointVideosGenerations},
|
|
{"grok video status", EndpointVideos, "/videos/req_123", service.PlatformGrok, EndpointVideos},
|
|
|
|
// Antigravity — uses inbound to pick Claude vs Gemini upstream.
|
|
{"antigravity claude", EndpointMessages, "/antigravity/v1/messages", service.PlatformAntigravity, EndpointMessages},
|
|
{"antigravity gemini", EndpointGeminiModels, "/antigravity/v1beta/models", service.PlatformAntigravity, EndpointGeminiModels},
|
|
|
|
// Unknown platform — passthrough.
|
|
{"unknown platform", "/v1/embeddings", "/v1/embeddings", "unknown", "/v1/embeddings"},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
require.Equal(t, tt.want, DeriveUpstreamEndpoint(tt.inbound, tt.rawPath, tt.platform))
|
|
})
|
|
}
|
|
}
|
|
|
|
// ──────────────────────────────────────────────────────────
|
|
// responsesSubpathSuffix
|
|
// ──────────────────────────────────────────────────────────
|
|
|
|
func TestResponsesSubpathSuffix(t *testing.T) {
|
|
tests := []struct {
|
|
raw string
|
|
want string
|
|
}{
|
|
{"/v1/responses", ""},
|
|
{"/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", ""},
|
|
{"", ""},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.raw, func(t *testing.T) {
|
|
require.Equal(t, tt.want, responsesSubpathSuffix(tt.raw))
|
|
})
|
|
}
|
|
}
|
|
|
|
// ──────────────────────────────────────────────────────────
|
|
// InboundEndpointMiddleware + context helpers
|
|
// ──────────────────────────────────────────────────────────
|
|
|
|
func TestInboundEndpointMiddleware(t *testing.T) {
|
|
router := gin.New()
|
|
router.Use(InboundEndpointMiddleware())
|
|
|
|
var captured string
|
|
router.POST("/v1/messages", func(c *gin.Context) {
|
|
captured = GetInboundEndpoint(c)
|
|
c.Status(http.StatusOK)
|
|
})
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/v1/messages", nil)
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
|
|
require.Equal(t, EndpointMessages, captured)
|
|
}
|
|
|
|
func TestGetInboundEndpoint_FallbackWithoutMiddleware(t *testing.T) {
|
|
rec := httptest.NewRecorder()
|
|
c, _ := gin.CreateTestContext(rec)
|
|
c.Request = httptest.NewRequest(http.MethodPost, "/antigravity/v1/messages", nil)
|
|
|
|
// Middleware did not run — fallback to normalizing c.Request.URL.Path.
|
|
got := GetInboundEndpoint(c)
|
|
require.Equal(t, EndpointMessages, got)
|
|
}
|
|
|
|
func TestGetUpstreamEndpoint_FullFlow(t *testing.T) {
|
|
rec := httptest.NewRecorder()
|
|
c, _ := gin.CreateTestContext(rec)
|
|
c.Request = httptest.NewRequest(http.MethodPost, "/openai/v1/responses/compact", nil)
|
|
|
|
// Simulate middleware.
|
|
c.Set(ctxKeyInboundEndpoint, NormalizeInboundEndpoint(c.Request.URL.Path))
|
|
|
|
got := GetUpstreamEndpoint(c, service.PlatformOpenAI)
|
|
require.Equal(t, "/v1/responses/compact", got)
|
|
}
|