adaptive_api_protocol_test.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266
  1. //go:build unit
  2. package service
  3. import (
  4. "bytes"
  5. "context"
  6. "errors"
  7. "net/http"
  8. "net/http/httptest"
  9. "testing"
  10. "github.com/Wei-Shaw/sub2api/internal/pkg/openai_compat"
  11. "github.com/gin-gonic/gin"
  12. "github.com/stretchr/testify/require"
  13. "github.com/tidwall/gjson"
  14. )
  15. func adaptiveProtocolTestAccount(platform string, baseURLs map[string]any) *Account {
  16. return &Account{
  17. ID: 701,
  18. Name: "adaptive-cn",
  19. Platform: platform,
  20. Type: AccountTypeAPIKey,
  21. Concurrency: 1,
  22. Credentials: map[string]any{
  23. "api_key": "sk-test",
  24. "api_protocol": APIProtocolAdaptive,
  25. "account_mode": AccountModePayG,
  26. "api_base_urls": baseURLs,
  27. },
  28. }
  29. }
  30. func adaptiveProtocolTestContext(path string, body []byte) *gin.Context {
  31. recorder := httptest.NewRecorder()
  32. c, _ := gin.CreateTestContext(recorder)
  33. c.Request = httptest.NewRequest(http.MethodPost, path, bytes.NewReader(body))
  34. c.Request.Header.Set("Content-Type", "application/json")
  35. return c
  36. }
  37. type cnProtocolIngressCase struct {
  38. name string
  39. path string
  40. body []byte
  41. forward func(*OpenAIGatewayService, *gin.Context, *Account, []byte) error
  42. }
  43. func cnProtocolIngressCases() []cnProtocolIngressCase {
  44. return []cnProtocolIngressCase{
  45. {
  46. name: "chat completions",
  47. path: "/v1/chat/completions",
  48. body: []byte(`{"model":"deepseek-chat","messages":[{"role":"user","content":"hello"}],"stream":false}`),
  49. forward: func(svc *OpenAIGatewayService, c *gin.Context, account *Account, body []byte) error {
  50. _, err := svc.ForwardAsChatCompletions(context.Background(), c, account, body, "", "")
  51. return err
  52. },
  53. },
  54. {
  55. name: "messages",
  56. path: "/v1/messages",
  57. body: []byte(`{"model":"deepseek-chat","max_tokens":32,"messages":[{"role":"user","content":"hello"}],"stream":false}`),
  58. forward: func(svc *OpenAIGatewayService, c *gin.Context, account *Account, body []byte) error {
  59. _, err := svc.ForwardAsAnthropic(context.Background(), c, account, body, "", "")
  60. return err
  61. },
  62. },
  63. {
  64. name: "responses",
  65. path: "/v1/responses",
  66. body: []byte(`{"model":"deepseek-chat","input":"hello","stream":false}`),
  67. forward: func(svc *OpenAIGatewayService, c *gin.Context, account *Account, body []byte) error {
  68. _, err := svc.Forward(context.Background(), c, account, body)
  69. return err
  70. },
  71. },
  72. }
  73. }
  74. func TestAdaptiveProtocolRoutesChatCompletionsToNativeChat(t *testing.T) {
  75. gin.SetMode(gin.TestMode)
  76. body := []byte(`{"model":"glm-4.7","messages":[{"role":"user","content":"hello"}],"stream":false}`)
  77. upstream := &httpUpstreamRecorder{err: errors.New("stop after capture")}
  78. svc := &OpenAIGatewayService{cfg: rawChatCompletionsTestConfig(), httpUpstream: upstream}
  79. account := adaptiveProtocolTestAccount(PlatformZhipu, map[string]any{
  80. APIProtocolChatCompletions: "http://chat.example",
  81. APIProtocolAnthropic: "http://anthropic.example",
  82. })
  83. _, err := svc.ForwardAsChatCompletions(context.Background(), adaptiveProtocolTestContext("/v1/chat/completions", body), account, body, "", "")
  84. require.Error(t, err)
  85. require.Equal(t, "http://chat.example/v1/chat/completions", upstream.lastReq.URL.String())
  86. require.True(t, gjson.GetBytes(upstream.lastBody, "messages").IsArray())
  87. require.False(t, gjson.GetBytes(upstream.lastBody, "input").Exists())
  88. }
  89. func TestAdaptiveProtocolRoutesResponsesShapedChatToNativeResponses(t *testing.T) {
  90. gin.SetMode(gin.TestMode)
  91. body := []byte(`{"model":"deepseek-v4","input":"hello","max_output_tokens":32,"stream":false}`)
  92. upstream := &httpUpstreamRecorder{err: errors.New("stop after capture")}
  93. svc := &OpenAIGatewayService{cfg: rawChatCompletionsTestConfig(), httpUpstream: upstream}
  94. account := adaptiveProtocolTestAccount(PlatformDeepseek, map[string]any{
  95. APIProtocolChatCompletions: "http://chat.example",
  96. APIProtocolAnthropic: "http://anthropic.example",
  97. APIProtocolResponses: "http://responses.example",
  98. })
  99. _, err := svc.ForwardAsChatCompletions(context.Background(), adaptiveProtocolTestContext("/v1/chat/completions", body), account, body, "", "")
  100. require.Error(t, err)
  101. require.Equal(t, "http://responses.example/responses", upstream.lastReq.URL.String())
  102. require.True(t, gjson.GetBytes(upstream.lastBody, "input").Exists())
  103. require.False(t, gjson.GetBytes(upstream.lastBody, "messages").Exists())
  104. }
  105. func TestAdaptiveProtocolConvertsResponsesShapedChatForChatOnlyProvider(t *testing.T) {
  106. gin.SetMode(gin.TestMode)
  107. body := []byte(`{"model":"glm-4.7","input":"hello","max_output_tokens":32,"stream":false}`)
  108. upstream := &httpUpstreamRecorder{err: errors.New("stop after capture")}
  109. svc := &OpenAIGatewayService{cfg: rawChatCompletionsTestConfig(), httpUpstream: upstream}
  110. account := adaptiveProtocolTestAccount(PlatformZhipu, map[string]any{
  111. APIProtocolChatCompletions: "http://chat.example",
  112. APIProtocolAnthropic: "http://anthropic.example",
  113. })
  114. _, err := svc.ForwardAsChatCompletions(context.Background(), adaptiveProtocolTestContext("/v1/chat/completions", body), account, body, "", "")
  115. require.Error(t, err)
  116. require.Equal(t, "http://chat.example/v1/chat/completions", upstream.lastReq.URL.String())
  117. require.True(t, gjson.GetBytes(upstream.lastBody, "messages").IsArray())
  118. require.False(t, gjson.GetBytes(upstream.lastBody, "input").Exists())
  119. }
  120. func TestAdaptiveProtocolRoutesKimiResponsesShapedChatToNativeResponses(t *testing.T) {
  121. gin.SetMode(gin.TestMode)
  122. body := []byte(`{"model":"k3-256k","input":"hello","max_output_tokens":32,"stream":false}`)
  123. upstream := &httpUpstreamRecorder{err: errors.New("stop after capture")}
  124. svc := &OpenAIGatewayService{cfg: rawChatCompletionsTestConfig(), httpUpstream: upstream}
  125. account := adaptiveProtocolTestAccount(PlatformKimi, map[string]any{
  126. APIProtocolChatCompletions: "http://chat.example",
  127. APIProtocolAnthropic: "http://anthropic.example",
  128. APIProtocolResponses: "http://responses.example/v1",
  129. })
  130. _, err := svc.ForwardAsChatCompletions(context.Background(), adaptiveProtocolTestContext("/v1/chat/completions", body), account, body, "", "")
  131. require.Error(t, err)
  132. require.Equal(t, "http://responses.example/v1/responses", upstream.lastReq.URL.String())
  133. require.True(t, gjson.GetBytes(upstream.lastBody, "input").Exists())
  134. require.False(t, gjson.GetBytes(upstream.lastBody, "messages").Exists())
  135. }
  136. func TestAdaptiveProtocolRoutesMessagesToNativeAnthropic(t *testing.T) {
  137. gin.SetMode(gin.TestMode)
  138. body := []byte(`{"model":"glm-4.7","max_tokens":32,"messages":[{"role":"user","content":"hello"}],"stream":false}`)
  139. upstream := &httpUpstreamRecorder{err: errors.New("stop after capture")}
  140. svc := &OpenAIGatewayService{cfg: rawChatCompletionsTestConfig(), httpUpstream: upstream}
  141. account := adaptiveProtocolTestAccount(PlatformZhipu, map[string]any{
  142. APIProtocolChatCompletions: "http://chat.example",
  143. APIProtocolAnthropic: "http://anthropic.example",
  144. })
  145. _, err := svc.ForwardAsAnthropic(context.Background(), adaptiveProtocolTestContext("/v1/messages", body), account, body, "", "")
  146. require.Error(t, err)
  147. require.Equal(t, "http://anthropic.example/v1/messages", upstream.lastReq.URL.String())
  148. require.Equal(t, "glm-4.7", gjson.GetBytes(upstream.lastBody, "model").String())
  149. }
  150. func TestAdaptiveProtocolRoutesKimiResponsesToNativeResponses(t *testing.T) {
  151. gin.SetMode(gin.TestMode)
  152. body := []byte(`{"model":"k3-256k","input":"hello","store":true,"previous_response_id":"resp_old","stream":false}`)
  153. upstream := &httpUpstreamRecorder{err: errors.New("stop after capture")}
  154. svc := &OpenAIGatewayService{cfg: rawChatCompletionsTestConfig(), httpUpstream: upstream}
  155. account := adaptiveProtocolTestAccount(PlatformKimi, map[string]any{
  156. APIProtocolChatCompletions: "http://chat.example",
  157. APIProtocolAnthropic: "http://anthropic.example",
  158. APIProtocolResponses: "http://responses.example/v1",
  159. })
  160. _, err := svc.Forward(context.Background(), adaptiveProtocolTestContext("/v1/responses", body), account, body)
  161. require.Error(t, err)
  162. require.Equal(t, "http://responses.example/v1/responses", upstream.lastReq.URL.String())
  163. require.True(t, gjson.GetBytes(upstream.lastBody, "input").Exists())
  164. require.False(t, gjson.GetBytes(upstream.lastBody, "messages").Exists())
  165. require.False(t, gjson.GetBytes(upstream.lastBody, "store").Bool())
  166. require.False(t, gjson.GetBytes(upstream.lastBody, "previous_response_id").Exists())
  167. }
  168. func TestAdaptiveProtocolRoutesKimiCodingResponsesToNativeResponses(t *testing.T) {
  169. gin.SetMode(gin.TestMode)
  170. body := []byte(`{"model":"k3-256k","input":"hello","stream":false}`)
  171. upstream := &httpUpstreamRecorder{err: errors.New("stop after capture")}
  172. svc := &OpenAIGatewayService{cfg: rawChatCompletionsTestConfig(), httpUpstream: upstream}
  173. account := adaptiveProtocolTestAccount(PlatformKimi, map[string]any{
  174. APIProtocolChatCompletions: "https://api.kimi.com/coding/v1",
  175. APIProtocolAnthropic: "https://api.kimi.com/coding",
  176. APIProtocolResponses: "https://api.kimi.com/coding/v1",
  177. })
  178. account.Credentials["account_mode"] = AccountModeCoding
  179. _, err := svc.Forward(context.Background(), adaptiveProtocolTestContext("/v1/responses", body), account, body)
  180. require.Error(t, err)
  181. require.Equal(t, "https://api.kimi.com/coding/v1/responses", upstream.lastReq.URL.String())
  182. require.True(t, gjson.GetBytes(upstream.lastBody, "input").Exists())
  183. }
  184. func TestAdaptiveProtocolRoutesDeepSeekResponsesToNativeResponses(t *testing.T) {
  185. gin.SetMode(gin.TestMode)
  186. body := []byte(`{"model":"deepseek-v4","input":"hello","max_output_tokens":32,"store":true,"previous_response_id":"resp_old","stream":false}`)
  187. upstream := &httpUpstreamRecorder{err: errors.New("stop after capture")}
  188. svc := &OpenAIGatewayService{cfg: rawChatCompletionsTestConfig(), httpUpstream: upstream}
  189. account := adaptiveProtocolTestAccount(PlatformDeepseek, map[string]any{
  190. APIProtocolChatCompletions: "http://chat.example",
  191. APIProtocolAnthropic: "http://anthropic.example",
  192. APIProtocolResponses: "http://responses.example",
  193. })
  194. _, err := svc.Forward(context.Background(), adaptiveProtocolTestContext("/v1/responses", body), account, body)
  195. require.Error(t, err)
  196. require.Equal(t, "http://responses.example/responses", upstream.lastReq.URL.String())
  197. require.False(t, gjson.GetBytes(upstream.lastBody, "store").Bool())
  198. require.False(t, gjson.GetBytes(upstream.lastBody, "previous_response_id").Exists())
  199. require.Equal(t, int64(32), gjson.GetBytes(upstream.lastBody, "max_output_tokens").Int())
  200. require.False(t, gjson.GetBytes(upstream.lastBody, "instructions").Exists())
  201. }
  202. func TestFixedCNChatProtocolOverridesStaleResponsesMode(t *testing.T) {
  203. gin.SetMode(gin.TestMode)
  204. for _, tc := range cnProtocolIngressCases() {
  205. t.Run(tc.name, func(t *testing.T) {
  206. upstream := &httpUpstreamRecorder{err: errors.New("stop after capture")}
  207. svc := &OpenAIGatewayService{cfg: rawChatCompletionsTestConfig(), httpUpstream: upstream}
  208. account := adaptiveProtocolTestAccount(PlatformDeepseek, nil)
  209. account.Credentials["api_protocol"] = APIProtocolChatCompletions
  210. account.Credentials["base_url"] = "http://chat.example"
  211. account.Extra = map[string]any{
  212. openai_compat.ExtraKeyResponsesMode: string(openai_compat.ResponsesSupportModeForceResponses),
  213. }
  214. err := tc.forward(svc, adaptiveProtocolTestContext(tc.path, tc.body), account, tc.body)
  215. require.Error(t, err)
  216. require.Equal(t, "http://chat.example/v1/chat/completions", upstream.lastReq.URL.String())
  217. })
  218. }
  219. }
  220. func TestFixedCNResponsesProtocolOverridesStaleChatMode(t *testing.T) {
  221. gin.SetMode(gin.TestMode)
  222. for _, tc := range cnProtocolIngressCases() {
  223. t.Run(tc.name, func(t *testing.T) {
  224. upstream := &httpUpstreamRecorder{err: errors.New("stop after capture")}
  225. svc := &OpenAIGatewayService{cfg: rawChatCompletionsTestConfig(), httpUpstream: upstream}
  226. account := adaptiveProtocolTestAccount(PlatformDeepseek, nil)
  227. account.Credentials["api_protocol"] = APIProtocolResponses
  228. account.Credentials["base_url"] = "http://responses.example"
  229. account.Extra = map[string]any{
  230. openai_compat.ExtraKeyResponsesMode: string(openai_compat.ResponsesSupportModeForceChatCompletions),
  231. }
  232. err := tc.forward(svc, adaptiveProtocolTestContext(tc.path, tc.body), account, tc.body)
  233. require.Error(t, err)
  234. require.Equal(t, "http://responses.example/responses", upstream.lastReq.URL.String())
  235. })
  236. }
  237. }