gateway.go 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676
  1. package routes
  2. import (
  3. "errors"
  4. "net/http"
  5. "strings"
  6. "github.com/Wei-Shaw/sub2api/internal/config"
  7. "github.com/Wei-Shaw/sub2api/internal/handler"
  8. pkghttputil "github.com/Wei-Shaw/sub2api/internal/pkg/httputil"
  9. "github.com/Wei-Shaw/sub2api/internal/pkg/requestmodel"
  10. "github.com/Wei-Shaw/sub2api/internal/server/middleware"
  11. "github.com/Wei-Shaw/sub2api/internal/service"
  12. "github.com/gin-gonic/gin"
  13. "github.com/tidwall/gjson"
  14. "github.com/tidwall/sjson"
  15. )
  16. // RegisterGatewayRoutes 注册 API 网关路由(Claude/OpenAI/Gemini 兼容)
  17. func RegisterGatewayRoutes(
  18. r *gin.Engine,
  19. h *handler.Handlers,
  20. apiKeyAuth middleware.APIKeyAuthMiddleware,
  21. apiKeyService *service.APIKeyService,
  22. subscriptionService *service.SubscriptionService,
  23. opsService *service.OpsService,
  24. settingService *service.SettingService,
  25. compositeResolver *service.CompositeRouteResolver,
  26. cfg *config.Config,
  27. ) {
  28. bodyLimit := middleware.RequestBodyLimit(cfg.Gateway.MaxBodySize)
  29. textBodyLimit := middleware.RequestBodyLimit(cfg.Gateway.TextMaxBodySize)
  30. clientRequestID := middleware.ClientRequestID()
  31. opsErrorLogger := handler.OpsErrorLoggerMiddleware(opsService)
  32. endpointNorm := handler.InboundEndpointMiddleware()
  33. compositeTarget := compositeTargetPlatformMiddleware(compositeResolver)
  34. compositeGeminiTarget := compositeGeminiTargetPlatformMiddleware(compositeResolver)
  35. // 未分组 Key 拦截中间件(按协议格式区分错误响应)
  36. requireGroupAnthropic := middleware.RequireGroupAssignment(settingService, middleware.AnthropicErrorWriter)
  37. requireGroupGoogle := middleware.RequireGroupAssignment(settingService, middleware.GoogleErrorWriter)
  38. // 分组级模型白名单准入:在 apiKeyAuth 之后、compositeTarget 之前,
  39. // 保证校验发生在合成路由改写与调度之前,且只看客户端书写的模型名。
  40. groupModelAllowlist := middleware.GroupModelAllowlist()
  41. isOpenAIResponsesCompatibleGatewayPlatform := func(c *gin.Context) bool {
  42. switch getGroupPlatform(c) {
  43. case service.PlatformOpenAI, service.PlatformGrok,
  44. service.PlatformKimi, service.PlatformZhipu, service.PlatformDeepseek, service.PlatformMiniMax:
  45. // 国产 OpenAI 兼容供应商与 openai/grok 一样经 OpenAI 网关转发。
  46. return true
  47. default:
  48. return false
  49. }
  50. }
  51. countTokensHandler := func(c *gin.Context) {
  52. switch getGroupPlatform(c) {
  53. case service.PlatformOpenAI, service.PlatformKimi, service.PlatformZhipu, service.PlatformDeepseek, service.PlatformMiniMax:
  54. h.OpenAIGateway.CountTokens(c)
  55. case service.PlatformGrok:
  56. h.OpenAIGateway.GrokCountTokens(c)
  57. default:
  58. h.Gateway.CountTokens(c)
  59. }
  60. }
  61. codexModelsHandler := func(c *gin.Context) {
  62. dispatchCodexModelsGateway(c, h.OpenAIGateway.CodexModels, h.Gateway.CodexModels)
  63. }
  64. modelsHandler := func(c *gin.Context) {
  65. if c.Query("client_version") != "" {
  66. codexModelsHandler(c)
  67. return
  68. }
  69. h.Gateway.Models(c)
  70. }
  71. isOpenAIOnlyEndpointGatewayPlatform := func(c *gin.Context) bool {
  72. return getGroupPlatform(c) == service.PlatformOpenAI
  73. }
  74. imagesHandler := func(c *gin.Context) {
  75. switch getGroupPlatform(c) {
  76. case service.PlatformOpenAI:
  77. h.OpenAIGateway.Images(c)
  78. case service.PlatformGrok:
  79. h.OpenAIGateway.GrokImages(c)
  80. default:
  81. service.MarkOpsClientBusinessLimited(c, service.OpsClientBusinessLimitedReasonLocalFeatureGate)
  82. c.JSON(http.StatusNotFound, gin.H{
  83. "error": gin.H{
  84. "type": "not_found_error",
  85. "message": "Images API is not supported for this platform",
  86. },
  87. })
  88. }
  89. }
  90. videoGenerationHandler := func(c *gin.Context) {
  91. // Video status/content lookups below already allow Composite groups; keep
  92. // task creation aligned so composite keys that route to Grok accounts can
  93. // submit video generation jobs.
  94. if platform := getGroupPlatform(c); platform == service.PlatformGrok || platform == service.PlatformComposite {
  95. h.OpenAIGateway.GrokVideoGeneration(c)
  96. return
  97. }
  98. service.MarkOpsClientBusinessLimited(c, service.OpsClientBusinessLimitedReasonLocalFeatureGate)
  99. c.JSON(http.StatusNotFound, gin.H{
  100. "error": gin.H{
  101. "type": "not_found_error",
  102. "message": "Videos API is not supported for this platform",
  103. },
  104. })
  105. }
  106. videoStatusHandler := func(c *gin.Context) {
  107. // Video status requests do not carry a model, so composite groups cannot
  108. // be resolved by compositeTargetPlatformMiddleware. Route them through
  109. // the Grok handler and let scheduler/account selection enforce capacity.
  110. if getGroupPlatform(c) == service.PlatformGrok || getGroupPlatform(c) == service.PlatformComposite {
  111. h.OpenAIGateway.GrokVideoStatus(c)
  112. return
  113. }
  114. service.MarkOpsClientBusinessLimited(c, service.OpsClientBusinessLimitedReasonLocalFeatureGate)
  115. c.JSON(http.StatusNotFound, gin.H{
  116. "error": gin.H{
  117. "type": "not_found_error",
  118. "message": "Videos API is not supported for this platform",
  119. },
  120. })
  121. }
  122. videoContentHandler := func(c *gin.Context) {
  123. // Video content requests do not carry a model, so composite groups cannot
  124. // be resolved by compositeTargetPlatformMiddleware. Route them through
  125. // the Grok handler just like video status lookups.
  126. if getGroupPlatform(c) == service.PlatformGrok || getGroupPlatform(c) == service.PlatformComposite {
  127. h.OpenAIGateway.GrokVideoContent(c)
  128. return
  129. }
  130. service.MarkOpsClientBusinessLimited(c, service.OpsClientBusinessLimitedReasonLocalFeatureGate)
  131. c.JSON(http.StatusNotFound, gin.H{
  132. "error": gin.H{
  133. "type": "not_found_error",
  134. "message": "Videos API is not supported for this platform",
  135. },
  136. })
  137. }
  138. videoEditHandler := func(c *gin.Context) {
  139. if getGroupPlatform(c) == service.PlatformGrok {
  140. h.OpenAIGateway.GrokVideoEdit(c)
  141. return
  142. }
  143. service.MarkOpsClientBusinessLimited(c, service.OpsClientBusinessLimitedReasonLocalFeatureGate)
  144. c.JSON(http.StatusNotFound, gin.H{"error": gin.H{"type": "not_found_error", "message": "Videos API is not supported for this platform"}})
  145. }
  146. videoExtensionHandler := func(c *gin.Context) {
  147. if getGroupPlatform(c) == service.PlatformGrok {
  148. h.OpenAIGateway.GrokVideoExtension(c)
  149. return
  150. }
  151. service.MarkOpsClientBusinessLimited(c, service.OpsClientBusinessLimitedReasonLocalFeatureGate)
  152. c.JSON(http.StatusNotFound, gin.H{"error": gin.H{"type": "not_found_error", "message": "Videos API is not supported for this platform"}})
  153. }
  154. // /responses/*subpath 的子路径会被转发到上游同名端点之后,因此在入口就拒掉
  155. // 不可转发的子路径,不让它进入调度与转发流程。可转发的判定见
  156. // service.IsForwardableOpenAIResponsesRequestPath 及 upstream_path_guard.go。
  157. guardResponsesSubpath := func(next gin.HandlerFunc) gin.HandlerFunc {
  158. return func(c *gin.Context) {
  159. if !service.IsForwardableOpenAIResponsesRequestPath(c) {
  160. service.MarkOpsClientBusinessLimited(c, service.OpsClientBusinessLimitedReasonLocalPolicyDenied)
  161. c.AbortWithStatusJSON(http.StatusNotFound, gin.H{
  162. "error": gin.H{
  163. "type": "not_found_error",
  164. "message": "Unsupported responses subpath",
  165. },
  166. })
  167. return
  168. }
  169. if service.IsOpenAIResponsesInputTokensRequestPath(c) && isOpenAIResponsesCompatibleGatewayPlatform(c) {
  170. h.OpenAIGateway.ResponsesInputTokens(c)
  171. return
  172. }
  173. next(c)
  174. }
  175. }
  176. // API网关(Claude API兼容)
  177. gateway := r.Group("/v1")
  178. gateway.Use(bodyLimit)
  179. gateway.Use(clientRequestID)
  180. gateway.Use(opsErrorLogger)
  181. gateway.Use(endpointNorm)
  182. gateway.Use(gin.HandlerFunc(apiKeyAuth))
  183. gateway.GET("/sub2api/billing", h.Gateway.KeyBillingInfo)
  184. gateway.Use(groupModelAllowlist)
  185. gateway.Use(compositeTarget)
  186. gateway.Use(requireGroupAnthropic)
  187. {
  188. // /v1/messages: auto-route based on group platform
  189. gateway.POST("/messages", func(c *gin.Context) {
  190. if isOpenAIResponsesCompatibleGatewayPlatform(c) {
  191. h.OpenAIGateway.Messages(c)
  192. return
  193. }
  194. h.Gateway.Messages(c)
  195. })
  196. // /v1/messages/count_tokens: OpenAI bridges upstream, Grok estimates
  197. // locally, and Anthropic-compatible platforms retain their existing path.
  198. gateway.POST("/messages/count_tokens", countTokensHandler)
  199. // Codex CLI / Codex app refresh their model picker from the provider's
  200. // /models endpoint with a client_version query and expect the ChatGPT
  201. // Codex manifest format; other clients keep the OpenAI-style list.
  202. gateway.GET("/models", modelsHandler)
  203. gateway.GET("/usage", h.Gateway.Usage)
  204. gateway.POST("/live", h.OpenAIGateway.Live)
  205. gateway.GET("/live/:call_id", h.OpenAIGateway.LiveSideband)
  206. // OpenAI Responses API: auto-route based on group platform
  207. gateway.POST("/responses", func(c *gin.Context) {
  208. if isOpenAIResponsesCompatibleGatewayPlatform(c) {
  209. h.OpenAIGateway.Responses(c)
  210. return
  211. }
  212. h.Gateway.Responses(c)
  213. })
  214. gateway.POST("/responses/*subpath", guardResponsesSubpath(func(c *gin.Context) {
  215. if isOpenAIResponsesCompatibleGatewayPlatform(c) {
  216. h.OpenAIGateway.Responses(c)
  217. return
  218. }
  219. h.Gateway.Responses(c)
  220. }))
  221. gateway.POST("/alpha/search", textBodyLimit, h.OpenAIGateway.AlphaSearch)
  222. gateway.GET("/responses", func(c *gin.Context) {
  223. h.OpenAIGateway.ResponsesWebSocket(c)
  224. })
  225. // OpenAI Chat Completions API: auto-route based on group platform
  226. gateway.POST("/chat/completions", func(c *gin.Context) {
  227. if isOpenAIResponsesCompatibleGatewayPlatform(c) {
  228. h.OpenAIGateway.ChatCompletions(c)
  229. return
  230. }
  231. h.Gateway.ChatCompletions(c)
  232. })
  233. gateway.POST("/embeddings", textBodyLimit, func(c *gin.Context) {
  234. if !isOpenAIOnlyEndpointGatewayPlatform(c) {
  235. service.MarkOpsClientBusinessLimited(c, service.OpsClientBusinessLimitedReasonLocalFeatureGate)
  236. c.JSON(http.StatusNotFound, gin.H{
  237. "error": gin.H{
  238. "type": "not_found_error",
  239. "message": "Embeddings API is not supported for this platform",
  240. },
  241. })
  242. return
  243. }
  244. h.OpenAIGateway.Embeddings(c)
  245. })
  246. gateway.POST("/images/generations", imagesHandler)
  247. gateway.POST("/images/edits", imagesHandler)
  248. gateway.POST("/images/generations/async", h.AsyncImage.Submit)
  249. gateway.POST("/images/edits/async", h.AsyncImage.Submit)
  250. gateway.GET("/images/tasks/:task_id", h.AsyncImage.Get)
  251. gateway.POST("/images/batches", h.BatchImage.Submit)
  252. gateway.GET("/images/batches", h.BatchImage.List)
  253. gateway.GET("/images/batches/models", h.BatchImage.Models)
  254. gateway.GET("/images/batches/:id", h.BatchImage.Get)
  255. gateway.GET("/images/batches/:id/items", h.BatchImage.Items)
  256. gateway.GET("/images/batches/:id/items/:custom_id/content", h.BatchImage.ItemContent)
  257. gateway.GET("/images/batches/:id/download", h.BatchImage.Download)
  258. gateway.POST("/images/batches/:id/cancel", h.BatchImage.Cancel)
  259. gateway.DELETE("/images/batches/:id", h.BatchImage.DeleteRecord)
  260. gateway.DELETE("/images/batches/:id/outputs", h.BatchImage.DeleteOutputs)
  261. // OpenAI-compatible clients may create through /videos; xAI receives the
  262. // canonical /videos/generations route inside the Grok media forwarder.
  263. gateway.POST("/videos", videoGenerationHandler)
  264. gateway.POST("/videos/generations", videoGenerationHandler)
  265. gateway.POST("/videos/edits", videoEditHandler)
  266. gateway.POST("/videos/extensions", videoExtensionHandler)
  267. gateway.GET("/videos/generations/:request_id/content", videoContentHandler)
  268. gateway.GET("/videos/edits/:request_id/content", videoContentHandler)
  269. gateway.GET("/videos/extensions/:request_id/content", videoContentHandler)
  270. gateway.GET("/videos/generations/:request_id", videoStatusHandler)
  271. gateway.GET("/videos/edits/:request_id", videoStatusHandler)
  272. gateway.GET("/videos/extensions/:request_id", videoStatusHandler)
  273. gateway.GET("/videos/:request_id", videoStatusHandler)
  274. gateway.GET("/videos/:request_id/content", videoContentHandler)
  275. // xAI Voice APIs (Grok platform only): HTTP TTS/STT + Realtime WS.
  276. // Not part of the creation-center product surface — gateway relay only.
  277. voiceHandler := func(endpoint string) gin.HandlerFunc {
  278. return func(c *gin.Context) {
  279. if getGroupPlatform(c) != service.PlatformGrok {
  280. service.MarkOpsClientBusinessLimited(c, service.OpsClientBusinessLimitedReasonLocalFeatureGate)
  281. c.JSON(http.StatusNotFound, gin.H{"error": gin.H{"type": "not_found_error", "message": "Voice API is not supported for this platform"}})
  282. return
  283. }
  284. h.OpenAIGateway.GrokVoice(c, endpoint)
  285. }
  286. }
  287. gateway.POST("/tts", voiceHandler("tts"))
  288. gateway.POST("/stt", voiceHandler("stt"))
  289. gateway.POST("/custom-voices", voiceHandler("custom-voices"))
  290. customVoicePathHandler := func(c *gin.Context) {
  291. if getGroupPlatform(c) != service.PlatformGrok {
  292. service.MarkOpsClientBusinessLimited(c, service.OpsClientBusinessLimitedReasonLocalFeatureGate)
  293. c.JSON(http.StatusNotFound, gin.H{"error": gin.H{"type": "not_found_error", "message": "Voice API is not supported for this platform"}})
  294. return
  295. }
  296. h.OpenAIGateway.GrokVoice(c, grokCustomVoiceEndpoint(c))
  297. }
  298. gateway.GET("/custom-voices", voiceHandler("custom-voices"))
  299. gateway.GET("/custom-voices/:voice_id/audio", customVoicePathHandler)
  300. gateway.GET("/custom-voices/:voice_id", customVoicePathHandler)
  301. gateway.PATCH("/custom-voices/:voice_id", customVoicePathHandler)
  302. gateway.DELETE("/custom-voices/:voice_id", customVoicePathHandler)
  303. gateway.GET("/realtime", func(c *gin.Context) {
  304. if getGroupPlatform(c) != service.PlatformGrok {
  305. service.MarkOpsClientBusinessLimited(c, service.OpsClientBusinessLimitedReasonLocalFeatureGate)
  306. c.JSON(http.StatusNotFound, gin.H{"error": gin.H{"type": "not_found_error", "message": "Realtime API is not supported for this platform"}})
  307. return
  308. }
  309. h.OpenAIGateway.GrokRealtime(c)
  310. })
  311. gateway.POST("/web_search", func(c *gin.Context) {
  312. if getGroupPlatform(c) != service.PlatformGrok {
  313. service.MarkOpsClientBusinessLimited(c, service.OpsClientBusinessLimitedReasonLocalFeatureGate)
  314. c.JSON(http.StatusNotFound, gin.H{"error": gin.H{"type": "not_found_error", "message": "Web Search API is not supported for this platform"}})
  315. return
  316. }
  317. h.Gateway.WebSearch(c)
  318. })
  319. gateway.POST("/x_search", func(c *gin.Context) {
  320. if getGroupPlatform(c) != service.PlatformGrok {
  321. service.MarkOpsClientBusinessLimited(c, service.OpsClientBusinessLimitedReasonLocalFeatureGate)
  322. c.JSON(http.StatusNotFound, gin.H{"error": gin.H{"type": "not_found_error", "message": "X Search API is not supported for this platform"}})
  323. return
  324. }
  325. h.Gateway.XSearch(c)
  326. })
  327. }
  328. // Gemini 原生 API 兼容层(Gemini SDK/CLI 直连)
  329. gemini := r.Group("/v1beta")
  330. gemini.Use(bodyLimit)
  331. gemini.Use(clientRequestID)
  332. gemini.Use(opsErrorLogger)
  333. gemini.Use(endpointNorm)
  334. gemini.Use(middleware.APIKeyAuthWithSubscriptionGoogle(apiKeyService, subscriptionService, cfg))
  335. gemini.Use(groupModelAllowlist)
  336. gemini.Use(compositeGeminiTarget)
  337. gemini.Use(requireGroupGoogle)
  338. {
  339. gemini.GET("/models", h.Gateway.GeminiV1BetaListModels)
  340. gemini.GET("/models/:model", h.Gateway.GeminiV1BetaGetModel)
  341. // Gin treats ":" as a param marker, but Gemini uses "{model}:{action}" in the same segment.
  342. gemini.POST("/models/*modelAction", h.Gateway.GeminiV1BetaModels)
  343. }
  344. // OpenAI Responses API(不带v1前缀的别名)— auto-route based on group platform
  345. responsesHandler := func(c *gin.Context) {
  346. if isOpenAIResponsesCompatibleGatewayPlatform(c) {
  347. h.OpenAIGateway.Responses(c)
  348. return
  349. }
  350. h.Gateway.Responses(c)
  351. }
  352. // 根路径别名共用中间件链:白名单准入在 apiKeyAuth 之后、compositeTarget
  353. // 之前,避免逐条路由手工维护链导致漏挂。
  354. rootRoute := func(method, path string, limit gin.HandlerFunc, handler gin.HandlerFunc) {
  355. r.Handle(method, path, limit, clientRequestID, opsErrorLogger, endpointNorm, gin.HandlerFunc(apiKeyAuth), groupModelAllowlist, compositeTarget, requireGroupAnthropic, handler)
  356. }
  357. rootRoute(http.MethodPost, "/responses", bodyLimit, responsesHandler)
  358. rootRoute(http.MethodPost, "/responses/*subpath", bodyLimit, guardResponsesSubpath(responsesHandler))
  359. rootRoute(http.MethodPost, "/alpha/search", textBodyLimit, h.OpenAIGateway.AlphaSearch)
  360. rootRoute(http.MethodGet, "/responses", bodyLimit, func(c *gin.Context) {
  361. h.OpenAIGateway.ResponsesWebSocket(c)
  362. })
  363. rootRoute(http.MethodGet, "/models", bodyLimit, modelsHandler)
  364. rootRoute(http.MethodPost, "/messages/count_tokens", bodyLimit, countTokensHandler)
  365. codexDirect := r.Group("/backend-api/codex")
  366. codexDirect.Use(bodyLimit, clientRequestID, opsErrorLogger, endpointNorm, gin.HandlerFunc(apiKeyAuth), groupModelAllowlist, compositeTarget, requireGroupAnthropic)
  367. {
  368. codexDirect.POST("/realtime/calls", h.OpenAIGateway.Live)
  369. codexDirect.GET("/:call_id", h.OpenAIGateway.LiveSideband)
  370. codexDirect.POST("/responses", responsesHandler)
  371. codexDirect.POST("/responses/*subpath", guardResponsesSubpath(responsesHandler))
  372. codexDirect.POST("/alpha/search", textBodyLimit, h.OpenAIGateway.AlphaSearch)
  373. codexDirect.GET("/responses", func(c *gin.Context) {
  374. h.OpenAIGateway.ResponsesWebSocket(c)
  375. })
  376. codexDirect.GET("/models", codexModelsHandler)
  377. }
  378. // OpenAI Chat Completions API(不带v1前缀的别名)— auto-route based on group platform
  379. rootRoute(http.MethodPost, "/chat/completions", bodyLimit, func(c *gin.Context) {
  380. if isOpenAIResponsesCompatibleGatewayPlatform(c) {
  381. h.OpenAIGateway.ChatCompletions(c)
  382. return
  383. }
  384. h.Gateway.ChatCompletions(c)
  385. })
  386. rootRoute(http.MethodPost, "/embeddings", textBodyLimit, func(c *gin.Context) {
  387. if !isOpenAIOnlyEndpointGatewayPlatform(c) {
  388. service.MarkOpsClientBusinessLimited(c, service.OpsClientBusinessLimitedReasonLocalFeatureGate)
  389. c.JSON(http.StatusNotFound, gin.H{
  390. "error": gin.H{
  391. "type": "not_found_error",
  392. "message": "Embeddings API is not supported for this platform",
  393. },
  394. })
  395. return
  396. }
  397. h.OpenAIGateway.Embeddings(c)
  398. })
  399. rootRoute(http.MethodPost, "/images/generations", bodyLimit, imagesHandler)
  400. rootRoute(http.MethodPost, "/images/edits", bodyLimit, imagesHandler)
  401. rootRoute(http.MethodPost, "/images/generations/async", bodyLimit, h.AsyncImage.Submit)
  402. rootRoute(http.MethodPost, "/images/edits/async", bodyLimit, h.AsyncImage.Submit)
  403. rootRoute(http.MethodGet, "/images/tasks/:task_id", bodyLimit, h.AsyncImage.Get)
  404. rootRoute(http.MethodPost, "/videos", bodyLimit, videoGenerationHandler)
  405. rootRoute(http.MethodPost, "/videos/generations", bodyLimit, videoGenerationHandler)
  406. rootRoute(http.MethodPost, "/videos/edits", bodyLimit, videoEditHandler)
  407. rootRoute(http.MethodPost, "/videos/extensions", bodyLimit, videoExtensionHandler)
  408. rootRoute(http.MethodGet, "/videos/generations/:request_id/content", bodyLimit, videoContentHandler)
  409. rootRoute(http.MethodGet, "/videos/edits/:request_id/content", bodyLimit, videoContentHandler)
  410. rootRoute(http.MethodGet, "/videos/extensions/:request_id/content", bodyLimit, videoContentHandler)
  411. rootRoute(http.MethodGet, "/videos/generations/:request_id", bodyLimit, videoStatusHandler)
  412. rootRoute(http.MethodGet, "/videos/edits/:request_id", bodyLimit, videoStatusHandler)
  413. rootRoute(http.MethodGet, "/videos/extensions/:request_id", bodyLimit, videoStatusHandler)
  414. rootRoute(http.MethodGet, "/videos/:request_id", bodyLimit, videoStatusHandler)
  415. rootRoute(http.MethodGet, "/videos/:request_id/content", bodyLimit, videoContentHandler)
  416. rootVoiceHandler := func(endpoint string) gin.HandlerFunc {
  417. return func(c *gin.Context) {
  418. if getGroupPlatform(c) != service.PlatformGrok {
  419. service.MarkOpsClientBusinessLimited(c, service.OpsClientBusinessLimitedReasonLocalFeatureGate)
  420. c.JSON(http.StatusNotFound, gin.H{"error": gin.H{"type": "not_found_error", "message": "Voice API is not supported for this platform"}})
  421. return
  422. }
  423. h.OpenAIGateway.GrokVoice(c, endpoint)
  424. }
  425. }
  426. rootRoute(http.MethodPost, "/tts", bodyLimit, rootVoiceHandler("tts"))
  427. rootRoute(http.MethodPost, "/stt", bodyLimit, rootVoiceHandler("stt"))
  428. rootRoute(http.MethodPost, "/custom-voices", bodyLimit, rootVoiceHandler("custom-voices"))
  429. rootCustomVoicePathHandler := func(c *gin.Context) {
  430. if getGroupPlatform(c) != service.PlatformGrok {
  431. service.MarkOpsClientBusinessLimited(c, service.OpsClientBusinessLimitedReasonLocalFeatureGate)
  432. c.JSON(http.StatusNotFound, gin.H{"error": gin.H{"type": "not_found_error", "message": "Voice API is not supported for this platform"}})
  433. return
  434. }
  435. h.OpenAIGateway.GrokVoice(c, grokCustomVoiceEndpoint(c))
  436. }
  437. rootRoute(http.MethodGet, "/custom-voices", bodyLimit, rootVoiceHandler("custom-voices"))
  438. rootRoute(http.MethodGet, "/custom-voices/:voice_id/audio", bodyLimit, rootCustomVoicePathHandler)
  439. rootRoute(http.MethodGet, "/custom-voices/:voice_id", bodyLimit, rootCustomVoicePathHandler)
  440. rootRoute(http.MethodPatch, "/custom-voices/:voice_id", bodyLimit, rootCustomVoicePathHandler)
  441. rootRoute(http.MethodDelete, "/custom-voices/:voice_id", bodyLimit, rootCustomVoicePathHandler)
  442. rootRoute(http.MethodGet, "/realtime", bodyLimit, func(c *gin.Context) {
  443. if getGroupPlatform(c) != service.PlatformGrok {
  444. service.MarkOpsClientBusinessLimited(c, service.OpsClientBusinessLimitedReasonLocalFeatureGate)
  445. c.JSON(http.StatusNotFound, gin.H{"error": gin.H{"type": "not_found_error", "message": "Realtime API is not supported for this platform"}})
  446. return
  447. }
  448. h.OpenAIGateway.GrokRealtime(c)
  449. })
  450. rootRoute(http.MethodPost, "/web_search", bodyLimit, func(c *gin.Context) {
  451. if getGroupPlatform(c) != service.PlatformGrok {
  452. service.MarkOpsClientBusinessLimited(c, service.OpsClientBusinessLimitedReasonLocalFeatureGate)
  453. c.JSON(http.StatusNotFound, gin.H{"error": gin.H{"type": "not_found_error", "message": "Web Search API is not supported for this platform"}})
  454. return
  455. }
  456. h.Gateway.WebSearch(c)
  457. })
  458. rootRoute(http.MethodPost, "/x_search", bodyLimit, func(c *gin.Context) {
  459. if getGroupPlatform(c) != service.PlatformGrok {
  460. service.MarkOpsClientBusinessLimited(c, service.OpsClientBusinessLimitedReasonLocalFeatureGate)
  461. c.JSON(http.StatusNotFound, gin.H{"error": gin.H{"type": "not_found_error", "message": "X Search API is not supported for this platform"}})
  462. return
  463. }
  464. h.Gateway.XSearch(c)
  465. })
  466. // Antigravity 模型列表
  467. r.GET("/antigravity/models", gin.HandlerFunc(apiKeyAuth), requireGroupAnthropic, h.Gateway.AntigravityModels)
  468. // Antigravity 专用路由(仅使用 antigravity 账户,不混合调度)
  469. antigravityV1 := r.Group("/antigravity/v1")
  470. antigravityV1.Use(bodyLimit)
  471. antigravityV1.Use(clientRequestID)
  472. antigravityV1.Use(opsErrorLogger)
  473. antigravityV1.Use(endpointNorm)
  474. antigravityV1.Use(middleware.ForcePlatform(service.PlatformAntigravity))
  475. antigravityV1.Use(gin.HandlerFunc(apiKeyAuth))
  476. antigravityV1.Use(groupModelAllowlist)
  477. antigravityV1.Use(requireGroupAnthropic)
  478. {
  479. antigravityV1.POST("/messages", h.Gateway.Messages)
  480. antigravityV1.POST("/messages/count_tokens", h.Gateway.CountTokens)
  481. antigravityV1.GET("/models", h.Gateway.AntigravityModels)
  482. antigravityV1.GET("/usage", h.Gateway.Usage)
  483. }
  484. antigravityV1Beta := r.Group("/antigravity/v1beta")
  485. antigravityV1Beta.Use(bodyLimit)
  486. antigravityV1Beta.Use(clientRequestID)
  487. antigravityV1Beta.Use(opsErrorLogger)
  488. antigravityV1Beta.Use(endpointNorm)
  489. antigravityV1Beta.Use(middleware.ForcePlatform(service.PlatformAntigravity))
  490. antigravityV1Beta.Use(middleware.APIKeyAuthWithSubscriptionGoogle(apiKeyService, subscriptionService, cfg))
  491. antigravityV1Beta.Use(groupModelAllowlist)
  492. antigravityV1Beta.Use(requireGroupGoogle)
  493. {
  494. antigravityV1Beta.GET("/models", h.Gateway.GeminiV1BetaListModels)
  495. antigravityV1Beta.GET("/models/:model", h.Gateway.GeminiV1BetaGetModel)
  496. antigravityV1Beta.POST("/models/*modelAction", h.Gateway.GeminiV1BetaModels)
  497. }
  498. }
  499. func dispatchCodexModelsGateway(c *gin.Context, openAIHandler, generatedHandler gin.HandlerFunc) {
  500. if getGroupPlatform(c) == service.PlatformOpenAI {
  501. openAIHandler(c)
  502. return
  503. }
  504. generatedHandler(c)
  505. }
  506. // getGroupPlatform extracts the group platform from the API Key stored in context.
  507. func getGroupPlatform(c *gin.Context) string {
  508. apiKey, ok := middleware.GetAPIKeyFromContext(c)
  509. if !ok || apiKey.Group == nil {
  510. return ""
  511. }
  512. if apiKey.Group.Platform == service.PlatformComposite {
  513. if platform, ok := service.ResolvedTargetPlatformFromContext(c.Request.Context()); ok {
  514. return platform
  515. }
  516. }
  517. return apiKey.Group.Platform
  518. }
  519. func compositeTargetPlatformMiddleware(resolver *service.CompositeRouteResolver) gin.HandlerFunc {
  520. if resolver == nil {
  521. resolver = service.NewCompositeRouteResolver(nil)
  522. }
  523. return func(c *gin.Context) {
  524. apiKey, ok := middleware.GetAPIKeyFromContext(c)
  525. if !ok || apiKey == nil || apiKey.Group == nil || apiKey.Group.Platform != service.PlatformComposite {
  526. c.Next()
  527. return
  528. }
  529. if c.Request == nil || c.Request.Method == http.MethodGet {
  530. c.Next()
  531. return
  532. }
  533. body, err := pkghttputil.ReadRequestBodyWithPrealloc(c.Request)
  534. if err != nil {
  535. status := http.StatusBadRequest
  536. message := "Failed to read request body"
  537. var maxErr *http.MaxBytesError
  538. if errors.As(err, &maxErr) {
  539. status = http.StatusRequestEntityTooLarge
  540. message = "Request body is too large"
  541. }
  542. c.JSON(status, gin.H{"error": gin.H{"type": "invalid_request_error", "message": message}})
  543. c.Abort()
  544. return
  545. }
  546. // Live 入口按 session.model 分发并改写(与白名单准入、Live handler 的
  547. // 解析保持一致),避免顶层 model 别名与 session 模型不一致时改错对象。
  548. routePath := c.FullPath()
  549. model := requestmodel.FromBodyForRoute(routePath, c.GetHeader("Content-Type"), body)
  550. if model != "" {
  551. decision, err := resolver.Resolve(c.Request.Context(), apiKey.Group.ID, model, compositeRouteEndpointForPath(c.Request.URL.Path))
  552. if err != nil {
  553. c.JSON(http.StatusInternalServerError, gin.H{"error": gin.H{"type": "server_error", "message": "Failed to resolve composite model route"}})
  554. c.Abort()
  555. return
  556. }
  557. if decision.Matched {
  558. c.Request = c.Request.WithContext(service.WithCompositeRouteDecision(c.Request.Context(), decision))
  559. if upstreamModel := strings.TrimSpace(decision.UpstreamModel); upstreamModel != "" && upstreamModel != model && gjson.ValidBytes(body) {
  560. if _, modelPath := requestmodel.JSONModelPathForRoute(routePath, body); modelPath != "" {
  561. if rewritten, rewriteErr := sjson.SetBytes(body, modelPath, upstreamModel); rewriteErr == nil {
  562. body = rewritten
  563. }
  564. }
  565. }
  566. }
  567. }
  568. requestmodel.ResetRequestBody(c.Request, body)
  569. c.Next()
  570. }
  571. }
  572. func compositeGeminiTargetPlatformMiddleware(resolver *service.CompositeRouteResolver) gin.HandlerFunc {
  573. if resolver == nil {
  574. resolver = service.NewCompositeRouteResolver(nil)
  575. }
  576. return func(c *gin.Context) {
  577. apiKey, ok := middleware.GetAPIKeyFromContext(c)
  578. if ok && apiKey != nil && apiKey.Group != nil && apiKey.Group.Platform == service.PlatformComposite {
  579. model := compositeGeminiModelFromParams(c)
  580. if model != "" {
  581. decision, err := resolver.Resolve(c.Request.Context(), apiKey.Group.ID, model, service.CompositeRouteEndpointGemini)
  582. if err != nil {
  583. c.JSON(http.StatusInternalServerError, gin.H{"error": gin.H{"type": "server_error", "message": "Failed to resolve composite model route"}})
  584. c.Abort()
  585. return
  586. }
  587. if decision.Matched {
  588. c.Request = c.Request.WithContext(service.WithCompositeRouteDecision(c.Request.Context(), decision))
  589. }
  590. }
  591. if _, resolved := service.ResolvedTargetPlatformFromContext(c.Request.Context()); !resolved {
  592. c.Request = c.Request.WithContext(service.WithResolvedTargetPlatform(c.Request.Context(), service.PlatformGemini))
  593. }
  594. }
  595. c.Next()
  596. }
  597. }
  598. // grokCustomVoiceEndpoint derives the upstream Voice endpoint for the
  599. // /custom-voices/:voice_id[/audio] routes.
  600. //
  601. // The /audio suffix must be decided from the matched route template, not from
  602. // the raw URL path: a voice literally named "audio" makes GET
  603. // /custom-voices/audio match /custom-voices/:voice_id, and a raw-path suffix
  604. // check would rewrite it to custom-voices/audio/audio — turning a profile
  605. // lookup into an audio download.
  606. func grokCustomVoiceEndpoint(c *gin.Context) string {
  607. endpoint := "custom-voices/" + c.Param("voice_id")
  608. if strings.HasSuffix(c.FullPath(), "/:voice_id/audio") {
  609. endpoint += "/audio"
  610. }
  611. return endpoint
  612. }
  613. func compositeGeminiModelFromParams(c *gin.Context) string {
  614. if c == nil {
  615. return ""
  616. }
  617. if model := strings.TrimSpace(c.Param("model")); model != "" {
  618. return model
  619. }
  620. modelAction := strings.TrimPrefix(strings.TrimSpace(c.Param("modelAction")), "/")
  621. if modelAction == "" {
  622. return ""
  623. }
  624. if idx := strings.LastIndex(modelAction, ":"); idx >= 0 {
  625. return strings.TrimSpace(modelAction[:idx])
  626. }
  627. return modelAction
  628. }
  629. func compositeRouteEndpointForPath(path string) string {
  630. switch {
  631. case strings.Contains(path, "/messages/count_tokens"):
  632. return service.CompositeRouteEndpointCountTokens
  633. case strings.Contains(path, "/messages"):
  634. return service.CompositeRouteEndpointMessages
  635. case strings.Contains(path, "/responses"),
  636. strings.Contains(path, "/alpha/search"),
  637. strings.Contains(path, "/realtime/calls"),
  638. strings.HasSuffix(strings.TrimRight(path, "/"), "/live"):
  639. return service.CompositeRouteEndpointResponses
  640. case strings.Contains(path, "/chat/completions"):
  641. return service.CompositeRouteEndpointChatCompletions
  642. case strings.Contains(path, "/embeddings"):
  643. return service.CompositeRouteEndpointEmbeddings
  644. case strings.Contains(path, "/images/"):
  645. return service.CompositeRouteEndpointImages
  646. case strings.Contains(path, "/v1beta/"):
  647. return service.CompositeRouteEndpointGemini
  648. default:
  649. return service.CompositeRouteEndpointAny
  650. }
  651. }