openai_messages_continuation.go 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334
  1. package service
  2. import (
  3. "context"
  4. "encoding/json"
  5. "net/http"
  6. "strconv"
  7. "strings"
  8. "time"
  9. "github.com/Wei-Shaw/sub2api/internal/pkg/apicompat"
  10. "github.com/gin-gonic/gin"
  11. "github.com/tidwall/gjson"
  12. )
  13. type openAICompatSessionResponseBinding struct {
  14. ResponseID string
  15. TurnState string
  16. ContinuationDisabled bool
  17. ExpiresAt time.Time
  18. }
  19. func openAICompatContinuationEnabled(account *Account, model string) bool {
  20. if account == nil || account.Type != AccountTypeAPIKey {
  21. return false
  22. }
  23. return shouldAutoInjectPromptCacheKeyForCompat(model)
  24. }
  25. func trimAnthropicCompatResponsesInputToLatestTurn(req *apicompat.ResponsesRequest) {
  26. if req == nil || len(req.Input) == 0 {
  27. return
  28. }
  29. var items []apicompat.ResponsesInputItem
  30. if err := json.Unmarshal(req.Input, &items); err != nil || len(items) == 0 {
  31. return
  32. }
  33. start := latestAnthropicCompatResponsesInputTurnStart(items)
  34. trimmed := append([]apicompat.ResponsesInputItem(nil), items[start:]...)
  35. if len(trimmed) == len(items) {
  36. return
  37. }
  38. if input, err := json.Marshal(trimmed); err == nil {
  39. req.Input = input
  40. }
  41. }
  42. func latestAnthropicCompatResponsesInputTurnStart(items []apicompat.ResponsesInputItem) int {
  43. if len(items) == 0 {
  44. return 0
  45. }
  46. start := len(items) - 1
  47. last := items[start]
  48. switch {
  49. case last.Type == "function_call_output":
  50. for start > 0 && items[start-1].Type == "function_call_output" {
  51. start--
  52. }
  53. case last.Type == "message" && last.Role == "user":
  54. for start > 0 && items[start-1].Type == "function_call_output" {
  55. start--
  56. }
  57. default:
  58. return start
  59. }
  60. return expandAnthropicCompatResponsesInputToolCallStart(items, start)
  61. }
  62. func expandAnthropicCompatResponsesInputToolCallStart(items []apicompat.ResponsesInputItem, start int) int {
  63. if start < 0 || start >= len(items) {
  64. return start
  65. }
  66. needed := make(map[string]struct{})
  67. for i := start; i < len(items); i++ {
  68. if items[i].Type != "function_call_output" {
  69. continue
  70. }
  71. callID := strings.TrimSpace(items[i].CallID)
  72. if callID != "" {
  73. needed[callID] = struct{}{}
  74. }
  75. }
  76. if len(needed) == 0 {
  77. return start
  78. }
  79. expandedStart := start
  80. for i := start - 1; i >= 0 && len(needed) > 0; i-- {
  81. if items[i].Type != "function_call" {
  82. continue
  83. }
  84. callID := strings.TrimSpace(items[i].CallID)
  85. if _, ok := needed[callID]; !ok {
  86. continue
  87. }
  88. delete(needed, callID)
  89. expandedStart = i
  90. }
  91. return expandedStart
  92. }
  93. func isOpenAICompatPreviousResponseNotFound(statusCode int, upstreamMsg string, upstreamBody []byte) bool {
  94. if statusCode != http.StatusBadRequest && statusCode != http.StatusNotFound {
  95. return false
  96. }
  97. check := func(s string) bool {
  98. lower := strings.ToLower(strings.TrimSpace(s))
  99. return strings.Contains(lower, "previous_response_not_found") ||
  100. lower == "previous_response_id is not available for this user" ||
  101. (strings.Contains(lower, "previous response") && strings.Contains(lower, "not found")) ||
  102. (strings.Contains(lower, "unsupported parameter") && strings.Contains(lower, "previous_response_id"))
  103. }
  104. if check(upstreamMsg) || check(string(upstreamBody)) {
  105. return true
  106. }
  107. return check(gjson.GetBytes(upstreamBody, "error.code").String()) ||
  108. check(gjson.GetBytes(upstreamBody, "error.message").String())
  109. }
  110. func isOpenAICompatPreviousResponseUnsupported(statusCode int, upstreamMsg string, upstreamBody []byte) bool {
  111. if statusCode != http.StatusBadRequest {
  112. return false
  113. }
  114. check := func(s string) bool {
  115. lower := strings.ToLower(strings.TrimSpace(s))
  116. if !strings.Contains(lower, "previous_response_id") {
  117. return false
  118. }
  119. return strings.Contains(lower, "unsupported parameter") ||
  120. strings.Contains(lower, "only supported on responses websocket") ||
  121. strings.Contains(lower, "not supported") ||
  122. strings.Contains(lower, "is not available for this user") ||
  123. strings.Contains(lower, "requires an openai api-key account for http requests")
  124. }
  125. if check(upstreamMsg) || check(string(upstreamBody)) {
  126. return true
  127. }
  128. return check(gjson.GetBytes(upstreamBody, "error.code").String()) ||
  129. check(gjson.GetBytes(upstreamBody, "error.message").String())
  130. }
  131. func openAICompatSessionResponseKey(c *gin.Context, account *Account, promptCacheKey string) string {
  132. key := strings.TrimSpace(promptCacheKey)
  133. if account == nil || key == "" {
  134. return ""
  135. }
  136. apiKeyID := int64(0)
  137. if c != nil {
  138. apiKeyID = getAPIKeyIDFromContext(c)
  139. }
  140. return strings.Join([]string{
  141. strconv.FormatInt(account.ID, 10),
  142. strconv.FormatInt(apiKeyID, 10),
  143. key,
  144. }, "\x00")
  145. }
  146. func (s *OpenAIGatewayService) getOpenAICompatSessionResponseID(_ context.Context, c *gin.Context, account *Account, promptCacheKey string) string {
  147. if s == nil {
  148. return ""
  149. }
  150. key := openAICompatSessionResponseKey(c, account, promptCacheKey)
  151. if key == "" {
  152. return ""
  153. }
  154. raw, ok := s.openaiCompatSessionResponses.Load(key)
  155. if !ok {
  156. return ""
  157. }
  158. binding, ok := raw.(openAICompatSessionResponseBinding)
  159. if !ok {
  160. s.openaiCompatSessionResponses.Delete(key)
  161. return ""
  162. }
  163. if !binding.ExpiresAt.IsZero() && time.Now().After(binding.ExpiresAt) {
  164. s.openaiCompatSessionResponses.Delete(key)
  165. return ""
  166. }
  167. if binding.ContinuationDisabled {
  168. return ""
  169. }
  170. if strings.TrimSpace(binding.ResponseID) == "" {
  171. s.openaiCompatSessionResponses.Delete(key)
  172. return ""
  173. }
  174. return strings.TrimSpace(binding.ResponseID)
  175. }
  176. func (s *OpenAIGatewayService) bindOpenAICompatSessionResponseID(_ context.Context, c *gin.Context, account *Account, promptCacheKey, responseID string) {
  177. if s == nil {
  178. return
  179. }
  180. key := openAICompatSessionResponseKey(c, account, promptCacheKey)
  181. id := strings.TrimSpace(responseID)
  182. if key == "" || id == "" {
  183. return
  184. }
  185. binding := openAICompatSessionResponseBinding{
  186. ResponseID: id,
  187. ExpiresAt: time.Now().Add(s.openAIWSResponseStickyTTL()),
  188. }
  189. if raw, ok := s.openaiCompatSessionResponses.Load(key); ok {
  190. if existing, ok := raw.(openAICompatSessionResponseBinding); ok {
  191. if existing.ContinuationDisabled {
  192. existing.ResponseID = ""
  193. existing.ExpiresAt = time.Now().Add(s.openAIWSResponseStickyTTL())
  194. s.openaiCompatSessionResponses.Store(key, existing)
  195. return
  196. }
  197. binding.TurnState = existing.TurnState
  198. }
  199. }
  200. s.openaiCompatSessionResponses.Store(key, binding)
  201. }
  202. func (s *OpenAIGatewayService) deleteOpenAICompatSessionResponseID(_ context.Context, c *gin.Context, account *Account, promptCacheKey string) {
  203. if s == nil {
  204. return
  205. }
  206. key := openAICompatSessionResponseKey(c, account, promptCacheKey)
  207. if key == "" {
  208. return
  209. }
  210. raw, ok := s.openaiCompatSessionResponses.Load(key)
  211. if !ok {
  212. return
  213. }
  214. binding, ok := raw.(openAICompatSessionResponseBinding)
  215. if !ok {
  216. s.openaiCompatSessionResponses.Delete(key)
  217. return
  218. }
  219. binding.ResponseID = ""
  220. if strings.TrimSpace(binding.TurnState) == "" && !binding.ContinuationDisabled {
  221. s.openaiCompatSessionResponses.Delete(key)
  222. return
  223. }
  224. binding.ExpiresAt = time.Now().Add(s.openAIWSResponseStickyTTL())
  225. s.openaiCompatSessionResponses.Store(key, binding)
  226. }
  227. func (s *OpenAIGatewayService) disableOpenAICompatSessionContinuation(_ context.Context, c *gin.Context, account *Account, promptCacheKey string) {
  228. if s == nil {
  229. return
  230. }
  231. key := openAICompatSessionResponseKey(c, account, promptCacheKey)
  232. if key == "" {
  233. return
  234. }
  235. binding := openAICompatSessionResponseBinding{
  236. ContinuationDisabled: true,
  237. ExpiresAt: time.Now().Add(s.openAIWSResponseStickyTTL()),
  238. }
  239. if raw, ok := s.openaiCompatSessionResponses.Load(key); ok {
  240. if existing, ok := raw.(openAICompatSessionResponseBinding); ok {
  241. binding.TurnState = existing.TurnState
  242. }
  243. }
  244. s.openaiCompatSessionResponses.Store(key, binding)
  245. }
  246. func (s *OpenAIGatewayService) isOpenAICompatSessionContinuationDisabled(_ context.Context, c *gin.Context, account *Account, promptCacheKey string) bool {
  247. if s == nil {
  248. return false
  249. }
  250. key := openAICompatSessionResponseKey(c, account, promptCacheKey)
  251. if key == "" {
  252. return false
  253. }
  254. raw, ok := s.openaiCompatSessionResponses.Load(key)
  255. if !ok {
  256. return false
  257. }
  258. binding, ok := raw.(openAICompatSessionResponseBinding)
  259. if !ok {
  260. s.openaiCompatSessionResponses.Delete(key)
  261. return false
  262. }
  263. if !binding.ExpiresAt.IsZero() && time.Now().After(binding.ExpiresAt) {
  264. s.openaiCompatSessionResponses.Delete(key)
  265. return false
  266. }
  267. return binding.ContinuationDisabled
  268. }
  269. func (s *OpenAIGatewayService) getOpenAICompatSessionTurnState(_ context.Context, c *gin.Context, account *Account, promptCacheKey string) string {
  270. if s == nil {
  271. return ""
  272. }
  273. key := openAICompatSessionResponseKey(c, account, promptCacheKey)
  274. if key == "" {
  275. return ""
  276. }
  277. raw, ok := s.openaiCompatSessionResponses.Load(key)
  278. if !ok {
  279. return ""
  280. }
  281. binding, ok := raw.(openAICompatSessionResponseBinding)
  282. if !ok || strings.TrimSpace(binding.TurnState) == "" {
  283. return ""
  284. }
  285. if !binding.ExpiresAt.IsZero() && time.Now().After(binding.ExpiresAt) {
  286. s.openaiCompatSessionResponses.Delete(key)
  287. return ""
  288. }
  289. return strings.TrimSpace(binding.TurnState)
  290. }
  291. func (s *OpenAIGatewayService) bindOpenAICompatSessionTurnState(_ context.Context, c *gin.Context, account *Account, promptCacheKey, turnState string) {
  292. if s == nil {
  293. return
  294. }
  295. key := openAICompatSessionResponseKey(c, account, promptCacheKey)
  296. state := strings.TrimSpace(turnState)
  297. if key == "" || state == "" {
  298. return
  299. }
  300. binding := openAICompatSessionResponseBinding{
  301. TurnState: state,
  302. ExpiresAt: time.Now().Add(s.openAIWSResponseStickyTTL()),
  303. }
  304. if raw, ok := s.openaiCompatSessionResponses.Load(key); ok {
  305. if existing, ok := raw.(openAICompatSessionResponseBinding); ok {
  306. binding.ResponseID = existing.ResponseID
  307. binding.ContinuationDisabled = existing.ContinuationDisabled
  308. }
  309. }
  310. s.openaiCompatSessionResponses.Store(key, binding)
  311. }