Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.
 
 
 

439 рядки
12 KiB

  1. package gemini
  2. import (
  3. "bytes"
  4. "io"
  5. "net/http"
  6. "net/http/httptest"
  7. "testing"
  8. "github.com/QuantumNous/new-api/common"
  9. "github.com/QuantumNous/new-api/constant"
  10. "github.com/QuantumNous/new-api/dto"
  11. relaycommon "github.com/QuantumNous/new-api/relay/common"
  12. "github.com/QuantumNous/new-api/types"
  13. "github.com/gin-gonic/gin"
  14. "github.com/stretchr/testify/require"
  15. )
  16. func TestGeminiChatHandlerCompletionTokensExcludeToolUsePromptTokens(t *testing.T) {
  17. t.Parallel()
  18. gin.SetMode(gin.TestMode)
  19. c, _ := gin.CreateTestContext(httptest.NewRecorder())
  20. c.Request = httptest.NewRequest(http.MethodPost, "/v1/chat/completions", nil)
  21. info := &relaycommon.RelayInfo{
  22. RelayFormat: types.RelayFormatGemini,
  23. OriginModelName: "gemini-3-flash-preview",
  24. ChannelMeta: &relaycommon.ChannelMeta{
  25. UpstreamModelName: "gemini-3-flash-preview",
  26. },
  27. }
  28. payload := dto.GeminiChatResponse{
  29. Candidates: []dto.GeminiChatCandidate{
  30. {
  31. Content: dto.GeminiChatContent{
  32. Role: "model",
  33. Parts: []dto.GeminiPart{
  34. {Text: "ok"},
  35. },
  36. },
  37. },
  38. },
  39. UsageMetadata: dto.GeminiUsageMetadata{
  40. PromptTokenCount: 151,
  41. ToolUsePromptTokenCount: 18329,
  42. CandidatesTokenCount: 1089,
  43. ThoughtsTokenCount: 1120,
  44. TotalTokenCount: 20689,
  45. },
  46. }
  47. body, err := common.Marshal(payload)
  48. require.NoError(t, err)
  49. resp := &http.Response{
  50. Body: io.NopCloser(bytes.NewReader(body)),
  51. }
  52. usage, newAPIError := GeminiChatHandler(c, info, resp)
  53. require.Nil(t, newAPIError)
  54. require.NotNil(t, usage)
  55. require.Equal(t, 18480, usage.PromptTokens)
  56. require.Equal(t, 2209, usage.CompletionTokens)
  57. require.Equal(t, 20689, usage.TotalTokens)
  58. require.Equal(t, 1120, usage.CompletionTokenDetails.ReasoningTokens)
  59. }
  60. func TestGeminiStreamHandlerCompletionTokensExcludeToolUsePromptTokens(t *testing.T) {
  61. gin.SetMode(gin.TestMode)
  62. c, _ := gin.CreateTestContext(httptest.NewRecorder())
  63. c.Request = httptest.NewRequest(http.MethodPost, "/v1/chat/completions", nil)
  64. oldStreamingTimeout := constant.StreamingTimeout
  65. constant.StreamingTimeout = 300
  66. t.Cleanup(func() {
  67. constant.StreamingTimeout = oldStreamingTimeout
  68. })
  69. info := &relaycommon.RelayInfo{
  70. OriginModelName: "gemini-3-flash-preview",
  71. ChannelMeta: &relaycommon.ChannelMeta{
  72. UpstreamModelName: "gemini-3-flash-preview",
  73. },
  74. }
  75. chunk := dto.GeminiChatResponse{
  76. Candidates: []dto.GeminiChatCandidate{
  77. {
  78. Content: dto.GeminiChatContent{
  79. Role: "model",
  80. Parts: []dto.GeminiPart{
  81. {Text: "partial"},
  82. },
  83. },
  84. },
  85. },
  86. UsageMetadata: dto.GeminiUsageMetadata{
  87. PromptTokenCount: 151,
  88. ToolUsePromptTokenCount: 18329,
  89. CandidatesTokenCount: 1089,
  90. ThoughtsTokenCount: 1120,
  91. TotalTokenCount: 20689,
  92. },
  93. }
  94. chunkData, err := common.Marshal(chunk)
  95. require.NoError(t, err)
  96. streamBody := []byte("data: " + string(chunkData) + "\n" + "data: [DONE]\n")
  97. resp := &http.Response{
  98. Body: io.NopCloser(bytes.NewReader(streamBody)),
  99. }
  100. usage, newAPIError := geminiStreamHandler(c, info, resp, func(_ string, _ *dto.GeminiChatResponse) bool {
  101. return true
  102. })
  103. require.Nil(t, newAPIError)
  104. require.NotNil(t, usage)
  105. require.Equal(t, 18480, usage.PromptTokens)
  106. require.Equal(t, 2209, usage.CompletionTokens)
  107. require.Equal(t, 20689, usage.TotalTokens)
  108. require.Equal(t, 1120, usage.CompletionTokenDetails.ReasoningTokens)
  109. }
  110. func TestGeminiTextGenerationHandlerPromptTokensIncludeToolUsePromptTokens(t *testing.T) {
  111. t.Parallel()
  112. gin.SetMode(gin.TestMode)
  113. c, _ := gin.CreateTestContext(httptest.NewRecorder())
  114. c.Request = httptest.NewRequest(http.MethodPost, "/v1beta/models/gemini-3-flash-preview:generateContent", nil)
  115. info := &relaycommon.RelayInfo{
  116. OriginModelName: "gemini-3-flash-preview",
  117. ChannelMeta: &relaycommon.ChannelMeta{
  118. UpstreamModelName: "gemini-3-flash-preview",
  119. },
  120. }
  121. payload := dto.GeminiChatResponse{
  122. Candidates: []dto.GeminiChatCandidate{
  123. {
  124. Content: dto.GeminiChatContent{
  125. Role: "model",
  126. Parts: []dto.GeminiPart{
  127. {Text: "ok"},
  128. },
  129. },
  130. },
  131. },
  132. UsageMetadata: dto.GeminiUsageMetadata{
  133. PromptTokenCount: 151,
  134. ToolUsePromptTokenCount: 18329,
  135. CandidatesTokenCount: 1089,
  136. ThoughtsTokenCount: 1120,
  137. TotalTokenCount: 20689,
  138. },
  139. }
  140. body, err := common.Marshal(payload)
  141. require.NoError(t, err)
  142. resp := &http.Response{
  143. Body: io.NopCloser(bytes.NewReader(body)),
  144. }
  145. usage, newAPIError := GeminiTextGenerationHandler(c, info, resp)
  146. require.Nil(t, newAPIError)
  147. require.NotNil(t, usage)
  148. require.Equal(t, 18480, usage.PromptTokens)
  149. require.Equal(t, 2209, usage.CompletionTokens)
  150. require.Equal(t, 20689, usage.TotalTokens)
  151. require.Equal(t, 1120, usage.CompletionTokenDetails.ReasoningTokens)
  152. }
  153. func TestGeminiChatHandlerUsesEstimatedPromptTokensWhenUsagePromptMissing(t *testing.T) {
  154. t.Parallel()
  155. gin.SetMode(gin.TestMode)
  156. c, _ := gin.CreateTestContext(httptest.NewRecorder())
  157. c.Request = httptest.NewRequest(http.MethodPost, "/v1/chat/completions", nil)
  158. info := &relaycommon.RelayInfo{
  159. RelayFormat: types.RelayFormatGemini,
  160. OriginModelName: "gemini-3-flash-preview",
  161. ChannelMeta: &relaycommon.ChannelMeta{
  162. UpstreamModelName: "gemini-3-flash-preview",
  163. },
  164. }
  165. info.SetEstimatePromptTokens(20)
  166. payload := dto.GeminiChatResponse{
  167. Candidates: []dto.GeminiChatCandidate{
  168. {
  169. Content: dto.GeminiChatContent{
  170. Role: "model",
  171. Parts: []dto.GeminiPart{
  172. {Text: "ok"},
  173. },
  174. },
  175. },
  176. },
  177. UsageMetadata: dto.GeminiUsageMetadata{
  178. PromptTokenCount: 0,
  179. ToolUsePromptTokenCount: 0,
  180. CandidatesTokenCount: 90,
  181. ThoughtsTokenCount: 10,
  182. TotalTokenCount: 110,
  183. },
  184. }
  185. body, err := common.Marshal(payload)
  186. require.NoError(t, err)
  187. resp := &http.Response{
  188. Body: io.NopCloser(bytes.NewReader(body)),
  189. }
  190. usage, newAPIError := GeminiChatHandler(c, info, resp)
  191. require.Nil(t, newAPIError)
  192. require.NotNil(t, usage)
  193. require.Equal(t, 20, usage.PromptTokens)
  194. require.Equal(t, 100, usage.CompletionTokens)
  195. require.Equal(t, 110, usage.TotalTokens)
  196. }
  197. func TestGeminiStreamHandlerUsesEstimatedPromptTokensWhenUsagePromptMissing(t *testing.T) {
  198. gin.SetMode(gin.TestMode)
  199. c, _ := gin.CreateTestContext(httptest.NewRecorder())
  200. c.Request = httptest.NewRequest(http.MethodPost, "/v1/chat/completions", nil)
  201. oldStreamingTimeout := constant.StreamingTimeout
  202. constant.StreamingTimeout = 300
  203. t.Cleanup(func() {
  204. constant.StreamingTimeout = oldStreamingTimeout
  205. })
  206. info := &relaycommon.RelayInfo{
  207. OriginModelName: "gemini-3-flash-preview",
  208. ChannelMeta: &relaycommon.ChannelMeta{
  209. UpstreamModelName: "gemini-3-flash-preview",
  210. },
  211. }
  212. info.SetEstimatePromptTokens(20)
  213. chunk := dto.GeminiChatResponse{
  214. Candidates: []dto.GeminiChatCandidate{
  215. {
  216. Content: dto.GeminiChatContent{
  217. Role: "model",
  218. Parts: []dto.GeminiPart{
  219. {Text: "partial"},
  220. },
  221. },
  222. },
  223. },
  224. UsageMetadata: dto.GeminiUsageMetadata{
  225. PromptTokenCount: 0,
  226. ToolUsePromptTokenCount: 0,
  227. CandidatesTokenCount: 90,
  228. ThoughtsTokenCount: 10,
  229. TotalTokenCount: 110,
  230. },
  231. }
  232. chunkData, err := common.Marshal(chunk)
  233. require.NoError(t, err)
  234. streamBody := []byte("data: " + string(chunkData) + "\n" + "data: [DONE]\n")
  235. resp := &http.Response{
  236. Body: io.NopCloser(bytes.NewReader(streamBody)),
  237. }
  238. usage, newAPIError := geminiStreamHandler(c, info, resp, func(_ string, _ *dto.GeminiChatResponse) bool {
  239. return true
  240. })
  241. require.Nil(t, newAPIError)
  242. require.NotNil(t, usage)
  243. require.Equal(t, 20, usage.PromptTokens)
  244. require.Equal(t, 100, usage.CompletionTokens)
  245. require.Equal(t, 110, usage.TotalTokens)
  246. }
  247. func TestGeminiStreamHandlerReturnsErrorOnPromptBlock(t *testing.T) {
  248. gin.SetMode(gin.TestMode)
  249. oldStreamingTimeout := constant.StreamingTimeout
  250. constant.StreamingTimeout = 300
  251. t.Cleanup(func() {
  252. constant.StreamingTimeout = oldStreamingTimeout
  253. })
  254. w := httptest.NewRecorder()
  255. c, _ := gin.CreateTestContext(w)
  256. c.Request = httptest.NewRequest(http.MethodPost, "/v1/chat/completions", nil)
  257. blockReason := "SAFETY"
  258. payload := dto.GeminiChatResponse{
  259. PromptFeedback: &dto.GeminiChatPromptFeedback{
  260. BlockReason: &blockReason,
  261. },
  262. }
  263. body, err := common.Marshal(payload)
  264. require.NoError(t, err)
  265. resp := &http.Response{
  266. StatusCode: http.StatusOK,
  267. Body: io.NopCloser(bytes.NewReader([]byte("data: " + string(body) + "\n\n"))),
  268. }
  269. info := &relaycommon.RelayInfo{
  270. RelayFormat: types.RelayFormatOpenAI,
  271. ChannelMeta: &relaycommon.ChannelMeta{
  272. UpstreamModelName: "gemini-test",
  273. },
  274. }
  275. called := false
  276. usage, newAPIError := geminiStreamHandler(c, info, resp, func(_ string, _ *dto.GeminiChatResponse) bool {
  277. called = true
  278. return true
  279. })
  280. require.Nil(t, usage)
  281. require.NotNil(t, newAPIError)
  282. require.False(t, called)
  283. require.Empty(t, w.Body.String())
  284. require.Empty(t, w.Header().Get("Content-Type"))
  285. require.Contains(t, newAPIError.UpstreamBody, "SAFETY")
  286. }
  287. func TestGeminiStreamHandlerDoesNotRetryAfterNormalStreamData(t *testing.T) {
  288. gin.SetMode(gin.TestMode)
  289. oldStreamingTimeout := constant.StreamingTimeout
  290. constant.StreamingTimeout = 300
  291. t.Cleanup(func() {
  292. constant.StreamingTimeout = oldStreamingTimeout
  293. })
  294. w := httptest.NewRecorder()
  295. c, _ := gin.CreateTestContext(w)
  296. c.Request = httptest.NewRequest(http.MethodPost, "/v1/chat/completions", nil)
  297. normalChunk := dto.GeminiChatResponse{
  298. Candidates: []dto.GeminiChatCandidate{
  299. {
  300. Content: dto.GeminiChatContent{
  301. Role: "model",
  302. Parts: []dto.GeminiPart{
  303. {Text: "partial"},
  304. },
  305. },
  306. },
  307. },
  308. }
  309. blockReason := "SAFETY"
  310. blockChunk := dto.GeminiChatResponse{
  311. PromptFeedback: &dto.GeminiChatPromptFeedback{
  312. BlockReason: &blockReason,
  313. },
  314. }
  315. normalData, err := common.Marshal(normalChunk)
  316. require.NoError(t, err)
  317. blockData, err := common.Marshal(blockChunk)
  318. require.NoError(t, err)
  319. resp := &http.Response{
  320. StatusCode: http.StatusOK,
  321. Body: io.NopCloser(bytes.NewReader([]byte("data: " + string(normalData) + "\n\ndata: " + string(blockData) + "\n\n"))),
  322. }
  323. info := &relaycommon.RelayInfo{
  324. RelayFormat: types.RelayFormatOpenAI,
  325. ChannelMeta: &relaycommon.ChannelMeta{
  326. UpstreamModelName: "gemini-test",
  327. },
  328. }
  329. called := false
  330. usage, newAPIError := geminiStreamHandler(c, info, resp, func(_ string, _ *dto.GeminiChatResponse) bool {
  331. called = true
  332. return true
  333. })
  334. require.Nil(t, newAPIError)
  335. require.NotNil(t, usage)
  336. require.True(t, called)
  337. require.Empty(t, w.Body.String())
  338. }
  339. func TestGeminiTextGenerationHandlerUsesEstimatedPromptTokensWhenUsagePromptMissing(t *testing.T) {
  340. t.Parallel()
  341. gin.SetMode(gin.TestMode)
  342. c, _ := gin.CreateTestContext(httptest.NewRecorder())
  343. c.Request = httptest.NewRequest(http.MethodPost, "/v1beta/models/gemini-3-flash-preview:generateContent", nil)
  344. info := &relaycommon.RelayInfo{
  345. OriginModelName: "gemini-3-flash-preview",
  346. ChannelMeta: &relaycommon.ChannelMeta{
  347. UpstreamModelName: "gemini-3-flash-preview",
  348. },
  349. }
  350. info.SetEstimatePromptTokens(20)
  351. payload := dto.GeminiChatResponse{
  352. Candidates: []dto.GeminiChatCandidate{
  353. {
  354. Content: dto.GeminiChatContent{
  355. Role: "model",
  356. Parts: []dto.GeminiPart{
  357. {Text: "ok"},
  358. },
  359. },
  360. },
  361. },
  362. UsageMetadata: dto.GeminiUsageMetadata{
  363. PromptTokenCount: 0,
  364. ToolUsePromptTokenCount: 0,
  365. CandidatesTokenCount: 90,
  366. ThoughtsTokenCount: 10,
  367. TotalTokenCount: 110,
  368. },
  369. }
  370. body, err := common.Marshal(payload)
  371. require.NoError(t, err)
  372. resp := &http.Response{
  373. Body: io.NopCloser(bytes.NewReader(body)),
  374. }
  375. usage, newAPIError := GeminiTextGenerationHandler(c, info, resp)
  376. require.Nil(t, newAPIError)
  377. require.NotNil(t, usage)
  378. require.Equal(t, 20, usage.PromptTokens)
  379. require.Equal(t, 100, usage.CompletionTokens)
  380. require.Equal(t, 110, usage.TotalTokens)
  381. }