openai_embeddings.go 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252
  1. package service
  2. import (
  3. "bytes"
  4. "context"
  5. "errors"
  6. "fmt"
  7. "io"
  8. "net/http"
  9. "strings"
  10. "time"
  11. "github.com/Wei-Shaw/sub2api/internal/pkg/logger"
  12. "github.com/Wei-Shaw/sub2api/internal/util/responseheaders"
  13. "github.com/gin-gonic/gin"
  14. "github.com/tidwall/gjson"
  15. "go.uber.org/zap"
  16. )
  17. func (s *OpenAIGatewayService) ForwardEmbeddings(
  18. ctx context.Context,
  19. c *gin.Context,
  20. account *Account,
  21. body []byte,
  22. defaultMappedModel string,
  23. ) (*OpenAIForwardResult, error) {
  24. startTime := time.Now()
  25. originalModel := strings.TrimSpace(gjson.GetBytes(body, "model").String())
  26. if originalModel == "" {
  27. writeOpenAIEmbeddingsError(c, http.StatusBadRequest, "invalid_request_error", "model is required")
  28. return nil, fmt.Errorf("missing model in request")
  29. }
  30. billingModel := resolveOpenAIForwardModel(account, originalModel, defaultMappedModel)
  31. upstreamModel := normalizeOpenAIModelForUpstream(account, billingModel)
  32. SetOpsUpstreamModel(c, upstreamModel)
  33. upstreamBody := body
  34. if upstreamModel != originalModel {
  35. upstreamBody = ReplaceModelInBody(body, upstreamModel)
  36. }
  37. logger.L().Debug("openai embeddings: forwarding",
  38. zap.Int64("account_id", account.ID),
  39. zap.String("original_model", originalModel),
  40. zap.String("billing_model", billingModel),
  41. zap.String("upstream_model", upstreamModel),
  42. )
  43. apiKey := strings.TrimSpace(account.GetOpenAIProtocolAPIKey())
  44. if apiKey == "" {
  45. return nil, fmt.Errorf("account %d missing api_key", account.ID)
  46. }
  47. // 协议感知:Anthropic 协议账号的凭证 base_url 指向 /anthropic 端点,
  48. // embeddings 需使用 OpenAI 格式 base。
  49. baseURL := account.GetOpenAIFormatBaseURL()
  50. if baseURL == "" {
  51. baseURL = "https://api.openai.com"
  52. }
  53. validatedURL, err := s.validateUpstreamBaseURL(baseURL)
  54. if err != nil {
  55. return nil, fmt.Errorf("invalid base_url: %w", err)
  56. }
  57. targetURL := buildOpenAIEmbeddingsURL(validatedURL)
  58. upstreamCtx, releaseUpstreamCtx := detachUpstreamContext(ctx)
  59. upstreamReq, err := http.NewRequestWithContext(upstreamCtx, http.MethodPost, targetURL, bytes.NewReader(upstreamBody))
  60. releaseUpstreamCtx()
  61. if err != nil {
  62. return nil, fmt.Errorf("build upstream request: %w", err)
  63. }
  64. upstreamReq = upstreamReq.WithContext(WithHTTPUpstreamProfile(upstreamReq.Context(), HTTPUpstreamProfileOpenAI))
  65. upstreamReq.Header.Set("Content-Type", "application/json")
  66. upstreamReq.Header.Set("Authorization", "Bearer "+apiKey)
  67. upstreamReq.Header.Set("Accept", "application/json")
  68. for key, values := range c.Request.Header {
  69. lowerKey := strings.ToLower(key)
  70. if openaiCCRawAllowedHeaders[lowerKey] {
  71. for _, v := range values {
  72. upstreamReq.Header.Add(key, v)
  73. }
  74. }
  75. }
  76. if customUA := account.GetOpenAIUserAgent(); customUA != "" {
  77. upstreamReq.Header.Set("user-agent", customUA)
  78. }
  79. // 账号级请求头覆写(仅 openai api_key 账号启用时生效)
  80. account.ApplyHeaderOverrides(upstreamReq.Header)
  81. proxyURL := ""
  82. if account.ProxyID != nil && account.Proxy != nil {
  83. proxyURL = account.Proxy.URL()
  84. }
  85. resp, err := s.doOpenAIUpstream(upstreamReq, proxyURL, account)
  86. if err != nil {
  87. safeErr := sanitizeUpstreamErrorMessage(err.Error())
  88. setOpsUpstreamError(c, 0, safeErr, "")
  89. appendOpsUpstreamError(c, OpsUpstreamErrorEvent{
  90. ProxyID: opsUpstreamProxyID(account),
  91. ProxyName: opsUpstreamProxyName(account),
  92. Platform: account.Platform,
  93. AccountID: account.ID,
  94. AccountName: account.Name,
  95. UpstreamStatusCode: 0,
  96. Kind: "request_error",
  97. Message: safeErr,
  98. })
  99. writeOpenAIEmbeddingsError(c, http.StatusBadGateway, "upstream_error", "Upstream request failed")
  100. return nil, fmt.Errorf("upstream request failed: %s", safeErr)
  101. }
  102. defer func() { _ = resp.Body.Close() }()
  103. if resp.StatusCode >= 400 {
  104. respBody := s.readUpstreamErrorBody(resp)
  105. _ = resp.Body.Close()
  106. resp.Body = io.NopCloser(bytes.NewReader(respBody))
  107. upstreamMsg := strings.TrimSpace(extractUpstreamErrorMessage(respBody))
  108. upstreamMsg = sanitizeUpstreamErrorMessage(upstreamMsg)
  109. if s.shouldFailoverOpenAIUpstreamResponse(account, resp.StatusCode, upstreamMsg, respBody) {
  110. upstreamDetail := ""
  111. if s.cfg != nil && s.cfg.Gateway.LogUpstreamErrorBody {
  112. maxBytes := s.cfg.Gateway.LogUpstreamErrorBodyMaxBytes
  113. if maxBytes <= 0 {
  114. maxBytes = 2048
  115. }
  116. upstreamDetail = truncateString(string(respBody), maxBytes)
  117. }
  118. appendOpsUpstreamError(c, OpsUpstreamErrorEvent{
  119. ProxyID: opsUpstreamProxyID(account),
  120. ProxyName: opsUpstreamProxyName(account),
  121. Platform: account.Platform,
  122. AccountID: account.ID,
  123. AccountName: account.Name,
  124. UpstreamStatusCode: resp.StatusCode,
  125. UpstreamRequestID: resp.Header.Get("x-request-id"),
  126. Kind: "failover",
  127. Message: upstreamMsg,
  128. Detail: upstreamDetail,
  129. })
  130. shouldDisable := s.handleOpenAIAccountUpstreamError(ctx, account, resp.StatusCode, resp.Header, respBody, upstreamModel)
  131. retryableOnSameAccount := !shouldDisable && account.IsPoolMode() && account.IsPoolModeRetryableStatus(resp.StatusCode)
  132. if account.IsOpenAIOAuth() && resp.StatusCode == http.StatusTooManyRequests {
  133. return nil, s.newOpenAIAccountFailoverError(account, resp.StatusCode, resp.Header, respBody, upstreamMsg, shouldDisable, retryableOnSameAccount)
  134. }
  135. if isOpenAIHTTPUpstreamAccessStateError(resp.StatusCode, upstreamMsg, respBody) {
  136. return nil, newOpenAIUpstreamFailoverError(resp.StatusCode, resp.Header, respBody, upstreamMsg, retryableOnSameAccount)
  137. }
  138. return nil, &UpstreamFailoverError{StatusCode: resp.StatusCode, ResponseBody: respBody, RetryableOnSameAccount: retryableOnSameAccount}
  139. }
  140. writeOpenAIEmbeddingsUpstreamResponse(c, resp, respBody, s.responseHeaderFilter)
  141. return nil, fmt.Errorf("upstream returned status %d", resp.StatusCode)
  142. }
  143. respBody, err := ReadUpstreamResponseBody(resp.Body, s.cfg, c, openAITooLargeError)
  144. if err != nil {
  145. if !errors.Is(err, ErrUpstreamResponseBodyTooLarge) {
  146. writeOpenAIEmbeddingsError(c, http.StatusBadGateway, "api_error", "Failed to read upstream response")
  147. }
  148. return nil, fmt.Errorf("read upstream body: %w", err)
  149. }
  150. writeOpenAIEmbeddingsUpstreamResponse(c, resp, respBody, s.responseHeaderFilter)
  151. return &OpenAIForwardResult{
  152. RequestID: firstNonEmptyString(resp.Header.Get("x-request-id"), resp.Header.Get("request-id")),
  153. UpstreamHeaders: resp.Header,
  154. Usage: extractOpenAIEmbeddingsUsage(respBody),
  155. Model: originalModel,
  156. BillingModel: billingModel,
  157. UpstreamModel: upstreamModel,
  158. Stream: false,
  159. Duration: time.Since(startTime),
  160. }, nil
  161. }
  162. func writeOpenAIEmbeddingsUpstreamResponse(c *gin.Context, resp *http.Response, body []byte, filter *responseheaders.CompiledHeaderFilter) {
  163. if c == nil || resp == nil {
  164. return
  165. }
  166. if c.Writer.Written() {
  167. return
  168. }
  169. if resp.Header != nil {
  170. responseheaders.WriteFilteredHeaders(c.Writer.Header(), resp.Header, filter)
  171. }
  172. if ct := resp.Header.Get("Content-Type"); ct != "" {
  173. c.Writer.Header().Set("Content-Type", ct)
  174. } else {
  175. c.Writer.Header().Set("Content-Type", "application/json")
  176. }
  177. c.Writer.WriteHeader(resp.StatusCode)
  178. _, _ = c.Writer.Write(body)
  179. }
  180. func writeOpenAIEmbeddingsError(c *gin.Context, statusCode int, errType, message string) {
  181. c.JSON(statusCode, gin.H{
  182. "error": gin.H{
  183. "type": errType,
  184. "message": message,
  185. },
  186. })
  187. }
  188. func extractOpenAIEmbeddingsUsage(body []byte) OpenAIUsage {
  189. usage := gjson.GetBytes(body, "usage")
  190. if !usage.Exists() || !usage.IsObject() {
  191. return OpenAIUsage{}
  192. }
  193. inputTokens := firstPositiveGJSONInt(
  194. usage.Get("prompt_tokens"),
  195. usage.Get("input_tokens"),
  196. usage.Get("total_tokens"),
  197. )
  198. outputTokens := firstPositiveGJSONInt(
  199. usage.Get("completion_tokens"),
  200. usage.Get("output_tokens"),
  201. )
  202. cacheReadTokens := openAICacheReadTokensFromUsage(usage)
  203. cacheCreationTokens := openAICacheCreationTokensFromUsage(usage)
  204. // 多模态 embedding(如 doubao-embedding-vision)回传图文 token 拆分,
  205. // 用于图文不同价计费;纯文本 embedding 该字段为 0,行为不变。
  206. imageInputTokens := firstPositiveGJSONInt(
  207. usage.Get("prompt_tokens_details.image_tokens"),
  208. usage.Get("input_tokens_details.image_tokens"),
  209. )
  210. return OpenAIUsage{
  211. InputTokens: inputTokens,
  212. ImageInputTokens: imageInputTokens,
  213. OutputTokens: outputTokens,
  214. CacheReadInputTokens: cacheReadTokens,
  215. CacheCreationInputTokens: cacheCreationTokens,
  216. }
  217. }
  218. func firstPositiveGJSONInt(values ...gjson.Result) int {
  219. for _, value := range values {
  220. if !value.Exists() {
  221. continue
  222. }
  223. n := int(value.Int())
  224. if n > 0 {
  225. return n
  226. }
  227. }
  228. return 0
  229. }
  230. func buildOpenAIEmbeddingsURL(base string) string {
  231. return buildOpenAIEndpointURL(base, "/v1/embeddings")
  232. }