account_test_service_cn_adaptive_test.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292
  1. //go:build unit
  2. package service
  3. import (
  4. "io"
  5. "net/http"
  6. "strings"
  7. "testing"
  8. "github.com/stretchr/testify/require"
  9. "github.com/tidwall/gjson"
  10. )
  11. func adaptiveCNAccountTestAccount(id int64, platform string) *Account {
  12. return &Account{
  13. ID: id,
  14. Name: "adaptive-cn-test",
  15. Platform: platform,
  16. Type: AccountTypeAPIKey,
  17. Status: StatusActive,
  18. Concurrency: 1,
  19. Credentials: map[string]any{
  20. "api_key": "sk-adaptive-test",
  21. "api_protocol": APIProtocolAdaptive,
  22. "api_base_urls": map[string]any{
  23. APIProtocolChatCompletions: "http://chat.example/v1",
  24. APIProtocolAnthropic: "http://anthropic.example",
  25. APIProtocolResponses: "http://responses.example",
  26. },
  27. },
  28. }
  29. }
  30. func adaptiveCNAccountTestService(account *Account, responses ...*http.Response) (*AccountTestService, *httpUpstreamRecorder) {
  31. repo := &openAIAccountTestRepo{
  32. mockAccountRepoForGemini: mockAccountRepoForGemini{
  33. accountsByID: map[int64]*Account{account.ID: account},
  34. },
  35. }
  36. upstream := &httpUpstreamRecorder{responses: responses}
  37. return &AccountTestService{
  38. accountRepo: repo,
  39. httpUpstream: upstream,
  40. cfg: rawChatCompletionsTestConfig(),
  41. }, upstream
  42. }
  43. func adaptiveCNChatTestResponse() *http.Response {
  44. return &http.Response{
  45. StatusCode: http.StatusOK,
  46. Header: http.Header{"Content-Type": []string{"text/event-stream"}},
  47. Body: io.NopCloser(strings.NewReader(`data: {"choices":[{"delta":{"content":"chat ok"},"finish_reason":"stop"}]}
  48. data: [DONE]
  49. `)),
  50. }
  51. }
  52. func adaptiveCNAnthropicTestResponse() *http.Response {
  53. return &http.Response{
  54. StatusCode: http.StatusOK,
  55. Header: http.Header{"Content-Type": []string{"text/event-stream"}},
  56. Body: io.NopCloser(strings.NewReader(`data: {"type":"content_block_delta","delta":{"text":"anthropic ok"}}
  57. data: {"type":"message_stop"}
  58. `)),
  59. }
  60. }
  61. func adaptiveCNResponsesTestResponse() *http.Response {
  62. return &http.Response{
  63. StatusCode: http.StatusOK,
  64. Header: http.Header{"Content-Type": []string{"text/event-stream"}},
  65. Body: io.NopCloser(strings.NewReader(`data: {"type":"response.output_text.delta","delta":"responses ok"}
  66. data: {"type":"response.completed"}
  67. `)),
  68. }
  69. }
  70. func TestAccountTestService_AdaptiveChatOnlyProvidersTestChatAndAnthropicEndpoints(t *testing.T) {
  71. account := adaptiveCNAccountTestAccount(301, PlatformZhipu)
  72. svc, upstream := adaptiveCNAccountTestService(
  73. account,
  74. adaptiveCNChatTestResponse(),
  75. adaptiveCNAnthropicTestResponse(),
  76. )
  77. c, recorder := newTestContext()
  78. err := svc.TestAccountConnection(c, account.ID, "glm-4.7", "hello", AccountTestModeDefault)
  79. require.NoError(t, err)
  80. require.Len(t, upstream.requests, 2)
  81. require.Equal(t, "http://chat.example/v1/chat/completions", upstream.requests[0].URL.String())
  82. require.Equal(t, "http://anthropic.example/v1/messages", upstream.requests[1].URL.String())
  83. require.Equal(t, "Bearer sk-adaptive-test", upstream.requests[0].Header.Get("Authorization"))
  84. require.Equal(t, "sk-adaptive-test", upstream.requests[1].Header.Get("x-api-key"))
  85. require.Equal(t, 1, strings.Count(recorder.Body.String(), `"type":"test_start"`))
  86. require.Equal(t, 1, strings.Count(recorder.Body.String(), `"type":"test_complete"`))
  87. require.Contains(t, recorder.Body.String(), "已通过原生 /v1/messages 验证")
  88. }
  89. func TestAccountTestService_AdaptiveDeepSeekAlsoTestsResponsesEndpoint(t *testing.T) {
  90. account := adaptiveCNAccountTestAccount(302, PlatformDeepseek)
  91. svc, upstream := adaptiveCNAccountTestService(
  92. account,
  93. adaptiveCNChatTestResponse(),
  94. adaptiveCNAnthropicTestResponse(),
  95. adaptiveCNResponsesTestResponse(),
  96. )
  97. c, recorder := newTestContext()
  98. err := svc.TestAccountConnection(c, account.ID, "deepseek-chat", "", AccountTestModeDefault)
  99. require.NoError(t, err)
  100. require.Len(t, upstream.requests, 3)
  101. require.Equal(t, "http://responses.example/responses", upstream.requests[2].URL.String())
  102. require.Equal(t, HTTPUpstreamProfileOpenAI, HTTPUpstreamProfileFromContext(upstream.requests[2].Context()))
  103. require.Equal(t, "Bearer sk-adaptive-test", upstream.requests[2].Header.Get("Authorization"))
  104. require.True(t, gjson.GetBytes(upstream.bodies[2], "stream").Bool())
  105. require.False(t, gjson.GetBytes(upstream.bodies[2], "store").Bool())
  106. require.False(t, gjson.GetBytes(upstream.bodies[2], "instructions").Exists())
  107. require.Equal(t, 1, strings.Count(recorder.Body.String(), `"type":"test_complete"`))
  108. require.Contains(t, recorder.Body.String(), "已通过原生 /responses 验证")
  109. }
  110. func TestAccountTestService_AdaptiveKimiAlsoTestsResponsesEndpoint(t *testing.T) {
  111. account := adaptiveCNAccountTestAccount(306, PlatformKimi)
  112. svc, upstream := adaptiveCNAccountTestService(
  113. account,
  114. adaptiveCNChatTestResponse(),
  115. adaptiveCNAnthropicTestResponse(),
  116. adaptiveCNResponsesTestResponse(),
  117. )
  118. c, recorder := newTestContext()
  119. err := svc.TestAccountConnection(c, account.ID, "k3-256k", "", AccountTestModeDefault)
  120. require.NoError(t, err)
  121. require.Len(t, upstream.requests, 3)
  122. require.Equal(t, "http://responses.example/v1/responses", upstream.requests[2].URL.String())
  123. require.Equal(t, HTTPUpstreamProfileOpenAI, HTTPUpstreamProfileFromContext(upstream.requests[2].Context()))
  124. require.Equal(t, "Bearer sk-adaptive-test", upstream.requests[2].Header.Get("Authorization"))
  125. require.True(t, gjson.GetBytes(upstream.bodies[2], "stream").Bool())
  126. require.False(t, gjson.GetBytes(upstream.bodies[2], "store").Bool())
  127. require.False(t, gjson.GetBytes(upstream.bodies[2], "instructions").Exists())
  128. require.Equal(t, 1, strings.Count(recorder.Body.String(), `"type":"test_complete"`))
  129. require.Contains(t, recorder.Body.String(), "已通过原生 /responses 验证")
  130. }
  131. func TestAccountTestService_AdaptiveStopsAndNamesFailingEndpoint(t *testing.T) {
  132. account := adaptiveCNAccountTestAccount(303, PlatformDeepseek)
  133. svc, upstream := adaptiveCNAccountTestService(
  134. account,
  135. adaptiveCNChatTestResponse(),
  136. newJSONResponse(http.StatusNotFound, `{"error":{"message":"missing messages route"}}`),
  137. )
  138. c, recorder := newTestContext()
  139. err := svc.TestAccountConnection(c, account.ID, "deepseek-chat", "", AccountTestModeDefault)
  140. require.Error(t, err)
  141. require.Contains(t, err.Error(), "Adaptive Anthropic endpoint returned 404")
  142. require.Len(t, upstream.requests, 2)
  143. require.Contains(t, recorder.Body.String(), `"type":"error"`)
  144. require.NotContains(t, recorder.Body.String(), `"type":"test_complete"`)
  145. }
  146. func TestAccountTestService_AdaptiveRejectsInvalidAnthropicSuccessBody(t *testing.T) {
  147. account := adaptiveCNAccountTestAccount(305, PlatformKimi)
  148. svc, upstream := adaptiveCNAccountTestService(
  149. account,
  150. adaptiveCNChatTestResponse(),
  151. newJSONResponse(http.StatusOK, `<html>not an Anthropic stream</html>`),
  152. )
  153. c, recorder := newTestContext()
  154. err := svc.TestAccountConnection(c, account.ID, "kimi-k2.5", "", AccountTestModeDefault)
  155. require.Error(t, err)
  156. require.Contains(t, err.Error(), "Adaptive Anthropic stream ended before message_stop")
  157. require.Len(t, upstream.requests, 2)
  158. require.Contains(t, recorder.Body.String(), `"type":"error"`)
  159. require.NotContains(t, recorder.Body.String(), `"type":"test_complete"`)
  160. }
  161. func TestAccountTestService_FixedCNChatProtocolStillTestsOnlyChatEndpoint(t *testing.T) {
  162. account := adaptiveCNAccountTestAccount(304, PlatformZhipu)
  163. account.Credentials["api_protocol"] = APIProtocolChatCompletions
  164. account.Credentials["base_url"] = "http://fixed-chat.example/v1"
  165. svc, upstream := adaptiveCNAccountTestService(account, adaptiveCNChatTestResponse())
  166. c, recorder := newTestContext()
  167. err := svc.TestAccountConnection(c, account.ID, "glm-4.7", "", AccountTestModeDefault)
  168. require.NoError(t, err)
  169. require.Len(t, upstream.requests, 1)
  170. require.Equal(t, "http://fixed-chat.example/v1/chat/completions", upstream.requests[0].URL.String())
  171. require.Equal(t, 1, strings.Count(recorder.Body.String(), `"type":"test_complete"`))
  172. }
  173. func anthropicProtocolCNAccount(id int64, platform string, credentials map[string]any) *Account {
  174. base := map[string]any{
  175. "api_key": "sk-anthropic-test",
  176. "api_protocol": APIProtocolAnthropic,
  177. }
  178. for key, value := range credentials {
  179. base[key] = value
  180. }
  181. return &Account{
  182. ID: id,
  183. Name: "anthropic-protocol-cn-test",
  184. Platform: platform,
  185. Type: AccountTypeAPIKey,
  186. Status: StatusActive,
  187. Concurrency: 1,
  188. Credentials: base,
  189. }
  190. }
  191. func TestAccountTestService_AnthropicProtocolProbesNativeEndpointWithoutBetaQuery(t *testing.T) {
  192. account := anthropicProtocolCNAccount(311, PlatformZhipu, map[string]any{
  193. "base_url": "https://open.bigmodel.cn/api/anthropic",
  194. })
  195. svc, upstream := adaptiveCNAccountTestService(account, adaptiveCNAnthropicTestResponse())
  196. c, recorder := newTestContext()
  197. err := svc.TestAccountConnection(c, account.ID, "glm-4.7", "", AccountTestModeDefault)
  198. require.NoError(t, err)
  199. require.Len(t, upstream.requests, 1)
  200. req := upstream.requests[0]
  201. // Native Anthropic path without the ?beta=true suffix the generic Claude tester appends.
  202. require.Equal(t, "https://open.bigmodel.cn/api/anthropic/v1/messages", req.URL.String())
  203. require.Empty(t, req.URL.RawQuery)
  204. require.Equal(t, "sk-anthropic-test", req.Header.Get("x-api-key"))
  205. require.Equal(t, "2023-06-01", req.Header.Get("anthropic-version"))
  206. require.Contains(t, recorder.Body.String(), `"type":"test_complete"`)
  207. }
  208. func TestAccountTestService_AnthropicProtocolFallsBackToProviderDefaultNotAnthropicDotCom(t *testing.T) {
  209. // base_url intentionally absent: forwarding resolves the per-platform default
  210. // Anthropic endpoint. The old fall-through probed https://api.anthropic.com
  211. // with the provider's API key.
  212. account := anthropicProtocolCNAccount(312, PlatformZhipu, nil)
  213. svc, upstream := adaptiveCNAccountTestService(account, adaptiveCNAnthropicTestResponse())
  214. c, _ := newTestContext()
  215. err := svc.TestAccountConnection(c, account.ID, "glm-4.7", "", AccountTestModeDefault)
  216. require.NoError(t, err)
  217. require.Len(t, upstream.requests, 1)
  218. require.Equal(t, "https://open.bigmodel.cn/api/anthropic/v1/messages", upstream.requests[0].URL.String())
  219. }
  220. func TestAccountTestService_AnthropicProtocolRejectsOpenAICompatBaseURL(t *testing.T) {
  221. account := anthropicProtocolCNAccount(313, PlatformZhipu, map[string]any{
  222. "base_url": "https://open.bigmodel.cn/api/paas/v4",
  223. })
  224. svc, upstream := adaptiveCNAccountTestService(account)
  225. c, recorder := newTestContext()
  226. err := svc.TestAccountConnection(c, account.ID, "glm-4.7", "", AccountTestModeDefault)
  227. require.Error(t, err)
  228. // Fails fast locally: no upstream request with the wrong endpoint shape.
  229. require.Empty(t, upstream.requests)
  230. require.Contains(t, recorder.Body.String(), "looks like an OpenAI-compatible endpoint")
  231. require.Contains(t, recorder.Body.String(), "https://open.bigmodel.cn/api/anthropic")
  232. }
  233. func TestAccountTestService_AnthropicProtocol401MarksAccountError(t *testing.T) {
  234. account := anthropicProtocolCNAccount(314, PlatformKimi, map[string]any{
  235. "base_url": "https://api.moonshot.cn/anthropic",
  236. })
  237. svc, _ := adaptiveCNAccountTestService(
  238. account,
  239. newJSONResponse(http.StatusUnauthorized, `{"error":{"message":"invalid key"}}`),
  240. )
  241. c, _ := newTestContext()
  242. err := svc.TestAccountConnection(c, account.ID, "kimi-k2.5", "", AccountTestModeDefault)
  243. require.Error(t, err)
  244. require.Contains(t, err.Error(), "Anthropic endpoint returned 401")
  245. repo := svc.accountRepo.(*openAIAccountTestRepo)
  246. require.Equal(t, account.ID, repo.setErrorID)
  247. }