openai_gateway_messages.go 50 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365
  1. package service
  2. import (
  3. "bytes"
  4. "context"
  5. "encoding/json"
  6. "errors"
  7. "fmt"
  8. "io"
  9. "net/http"
  10. "strings"
  11. "sync/atomic"
  12. "time"
  13. "github.com/Wei-Shaw/sub2api/internal/pkg/apicompat"
  14. "github.com/Wei-Shaw/sub2api/internal/pkg/claude"
  15. "github.com/Wei-Shaw/sub2api/internal/pkg/logger"
  16. "github.com/Wei-Shaw/sub2api/internal/util/responseheaders"
  17. "github.com/gin-gonic/gin"
  18. "github.com/tidwall/gjson"
  19. "go.uber.org/zap"
  20. )
  21. // ForwardAsAnthropic accepts an Anthropic Messages request body, converts it
  22. // to OpenAI Responses API format, forwards to the OpenAI upstream, and converts
  23. // the response back to Anthropic Messages format. This enables Claude Code
  24. // clients to access OpenAI models through the standard /v1/messages endpoint.
  25. func (s *OpenAIGatewayService) ForwardAsAnthropic(
  26. ctx context.Context,
  27. c *gin.Context,
  28. account *Account,
  29. body []byte,
  30. promptCacheKey string,
  31. defaultMappedModel string,
  32. ) (*OpenAIForwardResult, error) {
  33. beginUpstreamResponseModelObservation(c)
  34. ClearActualOpenAIUpstreamEndpoint(c)
  35. if shouldForwardOpenAIResponsesViaRawChatCompletions(account) {
  36. SetActualOpenAIUpstreamEndpoint(c, "/v1/chat/completions")
  37. }
  38. setCodexToolNameReverse(c, nil)
  39. if _, err := s.prepareCodexAccountIdentitySource(ctx, c, account); err != nil {
  40. return nil, err
  41. }
  42. // 入口分流(国产供应商 Anthropic 协议):上游为供应商原生 Anthropic 端点时,
  43. // /v1/messages 请求零转换直通(仅模型名映射 + 少量 body 清洗),完整保留
  44. // thinking / tool_use / cache 语义,适配 Claude Code 等原生客户端。
  45. // 必须先于 ShouldUseResponsesAPI 分流:Anthropic 协议账号经 probe 落标
  46. // openai_responses_supported=false,会先命中下方的 CC 直转分支。
  47. if account.IsAnthropicProtocol() || account.IsAdaptiveAPIProtocol() {
  48. return s.forwardAnthropicViaNativeAnthropicEndpoint(ctx, c, account, body, defaultMappedModel)
  49. }
  50. // 固定 chat_completions 的 CN 账号,以及不支持 Responses 的其他 APIKey
  51. // 账号,均将 Messages 转为 CC;固定 responses 的 CN 账号不受探针旧值覆盖。
  52. if shouldForwardOpenAIResponsesViaRawChatCompletions(account) {
  53. return s.forwardAnthropicViaRawChatCompletions(ctx, c, account, body, defaultMappedModel)
  54. }
  55. startTime := time.Now()
  56. // 1. Parse Anthropic request
  57. var anthropicReq apicompat.AnthropicRequest
  58. if err := json.Unmarshal(body, &anthropicReq); err != nil {
  59. return nil, fmt.Errorf("parse anthropic request: %w", err)
  60. }
  61. anthropicDigestReq := cloneAnthropicRequestForDigest(&anthropicReq)
  62. originalModel := anthropicReq.Model
  63. applyOpenAICompatModelNormalization(&anthropicReq)
  64. normalizedModel := anthropicReq.Model
  65. clientStream := anthropicReq.Stream // client's original stream preference
  66. // 2. Model mapping
  67. billingModel := resolveOpenAIForwardModel(account, normalizedModel, defaultMappedModel)
  68. upstreamModel := normalizeOpenAIModelForUpstream(account, billingModel)
  69. promptCacheKey = strings.TrimSpace(promptCacheKey)
  70. apiKeyID := getAPIKeyIDFromContext(c)
  71. anthropicDigestChain := ""
  72. anthropicMatchedDigestChain := ""
  73. compatPromptCacheInjected := false
  74. // Grok is outside the gpt-5/codex compat injector, but Claude Code still
  75. // carries a stable session id. Prefer that as the Grok prompt-cache seed so
  76. // multi-turn /v1/messages traffic can hit xAI's server-side cache.
  77. if promptCacheKey == "" && account.Platform == PlatformGrok {
  78. if sessionSeed := extractClaudeCodeSessionID(c, body); sessionSeed != "" {
  79. promptCacheKey = sessionSeed
  80. compatPromptCacheInjected = true
  81. } else if sessionSeed := promptCacheKeyFromAnthropicMetadataSession(&anthropicReq); sessionSeed != "" {
  82. promptCacheKey = sessionSeed
  83. compatPromptCacheInjected = true
  84. }
  85. }
  86. if promptCacheKey == "" && shouldAutoInjectPromptCacheKeyForCompat(upstreamModel) {
  87. promptCacheKey = promptCacheKeyFromAnthropicMetadataSession(&anthropicReq)
  88. if promptCacheKey == "" {
  89. promptCacheKey = deriveAnthropicCacheControlPromptCacheKey(&anthropicReq)
  90. }
  91. if promptCacheKey == "" {
  92. anthropicDigestChain = buildOpenAICompatAnthropicDigestChain(anthropicDigestReq)
  93. if reusedKey, matchedChain := s.findOpenAICompatAnthropicDigestPromptCacheKey(account, apiKeyID, anthropicDigestChain); reusedKey != "" {
  94. promptCacheKey = reusedKey
  95. anthropicMatchedDigestChain = matchedChain
  96. } else {
  97. promptCacheKey = promptCacheKeyFromAnthropicDigest(anthropicDigestChain)
  98. }
  99. }
  100. compatPromptCacheInjected = promptCacheKey != ""
  101. }
  102. compatReplayTrimmed := false
  103. compatReplayGuardEnabled := shouldAutoInjectPromptCacheKeyForCompat(upstreamModel)
  104. compatContinuationEnabled := openAICompatContinuationEnabled(account, upstreamModel)
  105. previousResponseID := ""
  106. if compatContinuationEnabled {
  107. previousResponseID = s.getOpenAICompatSessionResponseID(ctx, c, account, promptCacheKey)
  108. }
  109. compatContinuationDisabled := compatContinuationEnabled &&
  110. s.isOpenAICompatSessionContinuationDisabled(ctx, c, account, promptCacheKey)
  111. compatTurnState := ""
  112. // ChatGPT/Codex credentials rely on session_id + x-codex-turn-state; trimming to a
  113. // sliding 12-message window makes the cached prefix stall at system/tools.
  114. // Keep full replay there so upstream prompt caching can grow turn by turn.
  115. if compatReplayGuardEnabled && !account.UsesOpenAICodexProtocol() && previousResponseID == "" && !compatContinuationDisabled {
  116. compatReplayTrimmed = applyAnthropicCompatFullReplayGuard(&anthropicReq)
  117. }
  118. // 3. Convert Anthropic → Responses after compatibility-only replay guard.
  119. responsesReq, err := apicompat.AnthropicToResponses(&anthropicReq)
  120. if err != nil {
  121. return nil, fmt.Errorf("convert anthropic to responses: %w", err)
  122. }
  123. // Upstream always uses streaming (upstream may not support sync mode).
  124. // The client's original preference determines the response format.
  125. responsesReq.Stream = true
  126. isStream := true
  127. // 3b. Handle BetaFastMode → service_tier: "priority"
  128. if containsBetaToken(c.GetHeader("anthropic-beta"), claude.BetaFastMode) {
  129. responsesReq.ServiceTier = "priority"
  130. }
  131. responsesReq.Model = upstreamModel
  132. if responsesReq.Reasoning != nil {
  133. responsesReq.Reasoning.Effort = openAICompatAnthropicReasoningEffort(&anthropicReq, upstreamModel, responsesReq.Reasoning.Effort)
  134. }
  135. if previousResponseID != "" {
  136. responsesReq.PreviousResponseID = previousResponseID
  137. trimAnthropicCompatResponsesInputToLatestTurn(responsesReq)
  138. }
  139. if compatReplayGuardEnabled && !account.UsesOpenAICodexProtocol() {
  140. appendOpenAICompatClaudeCodeTodoGuard(responsesReq)
  141. }
  142. logFields := []zap.Field{
  143. zap.Int64("account_id", account.ID),
  144. zap.String("original_model", originalModel),
  145. zap.String("normalized_model", normalizedModel),
  146. zap.String("billing_model", billingModel),
  147. zap.String("upstream_model", upstreamModel),
  148. zap.Bool("stream", isStream),
  149. }
  150. if compatPromptCacheInjected {
  151. logFields = append(logFields,
  152. zap.Bool("compat_prompt_cache_key_injected", true),
  153. zap.String("compat_prompt_cache_key_sha256", hashSensitiveValueForLog(promptCacheKey)),
  154. )
  155. }
  156. if compatReplayTrimmed {
  157. logFields = append(logFields,
  158. zap.Bool("compat_full_replay_trimmed", true),
  159. zap.Int("compat_messages_after_trim", len(anthropicReq.Messages)),
  160. )
  161. }
  162. if previousResponseID != "" {
  163. logFields = append(logFields,
  164. zap.Bool("compat_previous_response_id_attached", true),
  165. zap.String("compat_previous_response_id", truncateOpenAIWSLogValue(previousResponseID, openAIWSIDValueMaxLen)),
  166. )
  167. }
  168. if compatTurnState != "" {
  169. logFields = append(logFields, zap.Bool("compat_turn_state_attached", true))
  170. }
  171. logger.L().Debug("openai messages: model mapping applied", logFields...)
  172. // 4. Marshal Responses request body, then apply the ChatGPT/Codex transform.
  173. responsesBody, err := json.Marshal(responsesReq)
  174. if err != nil {
  175. return nil, fmt.Errorf("marshal responses request: %w", err)
  176. }
  177. if account.UsesOpenAICodexProtocol() && account.Platform != PlatformGrok {
  178. var reqBody map[string]any
  179. if err := json.Unmarshal(responsesBody, &reqBody); err != nil {
  180. return nil, fmt.Errorf("unmarshal for codex transform: %w", err)
  181. }
  182. codexResult := applyCodexOAuthTransformWithOptions(reqBody, codexOAuthTransformOptions{
  183. SkipDefaultInstructions: true,
  184. PreserveToolCallIDs: true,
  185. })
  186. if codexResult.Error != nil {
  187. writeAnthropicError(c, http.StatusBadRequest, "invalid_request_error", codexResult.Error.Error())
  188. return nil, codexResult.Error
  189. }
  190. setCodexToolNameReverse(c, codexResult.ToolNameReverse)
  191. forcedTemplateText := ""
  192. if s.cfg != nil {
  193. forcedTemplateText = s.cfg.Gateway.ForcedCodexInstructionsTemplate
  194. }
  195. templateUpstreamModel := upstreamModel
  196. if codexResult.NormalizedModel != "" {
  197. templateUpstreamModel = codexResult.NormalizedModel
  198. }
  199. existingInstructions, _ := reqBody["instructions"].(string)
  200. if strings.TrimSpace(existingInstructions) == "" {
  201. existingInstructions = extractPromptLikeInstructionsFromInput(reqBody)
  202. }
  203. if _, err := applyForcedCodexInstructionsTemplate(reqBody, forcedTemplateText, forcedCodexInstructionsTemplateData{
  204. ExistingInstructions: strings.TrimSpace(existingInstructions),
  205. OriginalModel: originalModel,
  206. NormalizedModel: normalizedModel,
  207. BillingModel: billingModel,
  208. UpstreamModel: templateUpstreamModel,
  209. }); err != nil {
  210. return nil, err
  211. }
  212. ensureCodexOAuthInstructionsField(reqBody)
  213. if shouldAutoInjectPromptCacheKeyForCompat(upstreamModel) {
  214. appendOpenAICompatClaudeCodeTodoGuardToRequestBody(reqBody)
  215. }
  216. if codexResult.NormalizedModel != "" {
  217. upstreamModel = codexResult.NormalizedModel
  218. }
  219. if codexResult.PromptCacheKey != "" {
  220. promptCacheKey = codexResult.PromptCacheKey
  221. }
  222. applyCodexAccountIdentityClientMetadataMap(reqBody, codexAccountIdentitySource(c, account), apiKeyID)
  223. delete(reqBody, "prompt_cache_key")
  224. if shouldAutoInjectPromptCacheKeyForCompat(upstreamModel) {
  225. compatTurnState = s.getOpenAICompatSessionTurnState(ctx, c, account, promptCacheKey)
  226. }
  227. // OAuth codex transform forces stream=true upstream, so always use
  228. // the streaming response handler regardless of what the client asked.
  229. isStream = true
  230. responsesBody, err = json.Marshal(reqBody)
  231. if err != nil {
  232. return nil, fmt.Errorf("remarshal after codex transform: %w", err)
  233. }
  234. }
  235. // For API key accounts (including OpenAI-compatible upstream gateways),
  236. // ensure promptCacheKey is also propagated via the request body so that
  237. // upstreams using the Responses API can derive a stable session identifier
  238. // from prompt_cache_key. This makes our Anthropic /v1/messages compatibility
  239. // path behave more like a native Responses client.
  240. if account.Type == AccountTypeAPIKey {
  241. if trimmedKey := strings.TrimSpace(promptCacheKey); trimmedKey != "" {
  242. var reqBody map[string]any
  243. if err := json.Unmarshal(responsesBody, &reqBody); err != nil {
  244. return nil, fmt.Errorf("unmarshal for prompt cache key injection: %w", err)
  245. }
  246. if existing, ok := reqBody["prompt_cache_key"].(string); !ok || strings.TrimSpace(existing) == "" {
  247. reqBody["prompt_cache_key"] = trimmedKey
  248. updated, err := json.Marshal(reqBody)
  249. if err != nil {
  250. return nil, fmt.Errorf("remarshal after prompt cache key injection: %w", err)
  251. }
  252. responsesBody = updated
  253. }
  254. }
  255. }
  256. if account.Platform == PlatformOpenAI {
  257. policyBody, changed, policyErr := ApplyOpenAIReasoningEffortPolicyFromContext(ctx, responsesBody)
  258. if policyErr != nil {
  259. if IsReasoningEffortPolicyDenied(policyErr) {
  260. MarkOpsClientBusinessLimited(c, OpsClientBusinessLimitedReasonLocalPolicyDenied)
  261. writeAnthropicError(c, http.StatusForbidden, "forbidden_error", policyErr.Error())
  262. }
  263. return nil, policyErr
  264. }
  265. if changed {
  266. responsesBody = policyBody
  267. if responsesReq.Reasoning != nil {
  268. responsesReq.Reasoning.Effort = gjson.GetBytes(responsesBody, "reasoning.effort").String()
  269. }
  270. }
  271. }
  272. // 4c. Apply OpenAI fast policy (may filter service_tier or block the request).
  273. // Mirrors the Claude anthropic-beta "fast-mode-2026-02-01" filter, but keyed
  274. // on the body-level service_tier field (priority/flex).
  275. updatedBody, policyErr := s.applyOpenAIFastPolicyToBody(ctx, account, upstreamModel, responsesBody)
  276. if policyErr != nil {
  277. var blocked *OpenAIFastBlockedError
  278. if errors.As(policyErr, &blocked) {
  279. MarkOpsClientBusinessLimited(c, OpsClientBusinessLimitedReasonLocalPolicyDenied)
  280. writeAnthropicError(c, http.StatusForbidden, "forbidden_error", blocked.Message)
  281. }
  282. return nil, policyErr
  283. }
  284. responsesBody = updatedBody
  285. responsesReq.ServiceTier = normalizedOpenAIServiceTierValue(gjson.GetBytes(responsesBody, "service_tier").String())
  286. grokCacheIdentity := ""
  287. if account.Platform == PlatformGrok {
  288. grokIntentBody := responsesBody
  289. grokCacheIdentity = resolveGrokCacheIdentity(c, grokIntentBody, promptCacheKey, upstreamModel)
  290. patchedBody, patchErr := patchGrokResponsesBody(grokIntentBody, upstreamModel)
  291. if patchErr != nil {
  292. return nil, patchErr
  293. }
  294. responsesBody, patchErr = applyGrokResponsesCacheIdentity(patchedBody, grokIntentBody, grokCacheIdentity, account.IsGrokOAuth())
  295. if patchErr != nil {
  296. return nil, fmt.Errorf("apply grok prompt cache identity: %w", patchErr)
  297. }
  298. responsesBody, patchErr = applyGrokFreeMessagesFunctionToolCacheRoute(responsesBody, grokIntentBody, account, grokCacheIdentity)
  299. if patchErr != nil {
  300. return nil, fmt.Errorf("apply grok Free function-tool cache route: %w", patchErr)
  301. }
  302. }
  303. // 5. Get access token
  304. token, _, err := s.getRequestCredential(ctx, c, account)
  305. if err != nil {
  306. return nil, fmt.Errorf("get access token: %w", err)
  307. }
  308. // 6. Build upstream request
  309. if account.UsesOpenAICodexProtocol() && account.Platform != PlatformGrok {
  310. // Messages 兼容桥即使 body 未带 todo-guard/prompt_cache_key 标记(如映射到非
  311. // gpt-5/codex 模型),也必须让 buildUpstreamRequest 走 bridge 分支,以保留
  312. // 既有 body/session/conversation 行为。身份头在 post-build 阶段统一恢复。
  313. setOpenAICompatMessagesBridgeContext(c, true)
  314. }
  315. upstreamCtx, releaseUpstreamCtx := detachUpstreamContext(ctx)
  316. var upstreamReq *http.Request
  317. if account.Platform == PlatformGrok {
  318. upstreamReq, err = buildGrokResponsesRequest(upstreamCtx, c, account, responsesBody, token, grokCacheIdentity, s.cfg, s.settingService)
  319. } else {
  320. upstreamReq, err = s.buildUpstreamRequest(upstreamCtx, c, account, responsesBody, token, isStream, promptCacheKey, false)
  321. }
  322. releaseUpstreamCtx()
  323. if err != nil {
  324. return nil, fmt.Errorf("build upstream request: %w", err)
  325. }
  326. // Override session_id with a deterministic UUID derived from the isolated
  327. // session key, ensuring different API keys produce different upstream sessions.
  328. if account.Platform != PlatformGrok && promptCacheKey != "" {
  329. isolatedSessionID := generateSessionUUID(isolateOpenAIUpstreamSessionID(apiKeyID, codexAccountIdentitySource(c, account), promptCacheKey))
  330. upstreamReq.Header.Set("session_id", isolatedSessionID)
  331. if upstreamReq.Header.Get("conversation_id") != "" {
  332. upstreamReq.Header.Set("conversation_id", isolatedSessionID)
  333. }
  334. }
  335. if account.UsesOpenAICodexProtocol() && account.Platform != PlatformGrok {
  336. // buildUpstreamRequest 保留 Messages bridge 的 body/session 兼容行为,并会先
  337. // 清除身份头。真正发送前恢复完整 Codex 身份,避免 ChatGPT Codex 上游因缺失
  338. // originator/OpenAI-Beta 返回 404(issue #3901)。
  339. ensureCodexIdentityHeaders(upstreamReq.Header)
  340. enforceCodexIdentityHeaders(upstreamReq.Header)
  341. logger.L().Debug("openai messages: upstream identity restored",
  342. zap.Int64("account_id", account.ID),
  343. zap.String("upstream_model", upstreamModel),
  344. zap.Bool("compat_identity_restored", true),
  345. )
  346. }
  347. if account.UsesOpenAICodexProtocol() && promptCacheKey != "" && strings.TrimSpace(c.GetHeader("conversation_id")) == "" {
  348. upstreamReq.Header.Del("conversation_id")
  349. }
  350. if compatTurnState != "" && upstreamReq.Header.Get("x-codex-turn-state") == "" {
  351. upstreamReq.Header.Set("x-codex-turn-state", compatTurnState)
  352. }
  353. // 7. Send request
  354. proxyURL := ""
  355. if account.ProxyID != nil && account.Proxy != nil {
  356. proxyURL = account.Proxy.URL()
  357. }
  358. // Grok may reject encrypted reasoning replayed under a different OAuth
  359. // account/cache identity. Match forwardGrokResponses: one strip+retry before
  360. // treating the 400 as a hard failure / failover trigger.
  361. var resp *http.Response
  362. for attempt := 0; ; attempt++ {
  363. if attempt > 0 {
  364. if account.Platform != PlatformGrok {
  365. break
  366. }
  367. upstreamCtxRetry, releaseRetry := detachUpstreamContext(ctx)
  368. upstreamReq, err = buildGrokResponsesRequest(upstreamCtxRetry, c, account, responsesBody, token, grokCacheIdentity, s.cfg, s.settingService)
  369. releaseRetry()
  370. if err != nil {
  371. return nil, fmt.Errorf("build grok retry request: %w", err)
  372. }
  373. }
  374. resp, err = s.doOpenAIUpstream(upstreamReq, proxyURL, account)
  375. if err != nil {
  376. return nil, s.handleOpenAIUpstreamTransportError(ctx, c, account, err, false)
  377. }
  378. if account.Platform != PlatformGrok || attempt > 0 || resp.StatusCode != http.StatusBadRequest {
  379. break
  380. }
  381. respBody := s.readUpstreamErrorBody(resp)
  382. if resp.Body != nil {
  383. _ = resp.Body.Close()
  384. }
  385. // Prefer explicit decrypt errors; also strip once on any 400 when the
  386. // outbound body still carries reasoning.encrypted_content (account
  387. // switch often returns opaque "Upstream error: 400").
  388. shouldStrip := isGrokInvalidEncryptedContentResponse(resp.StatusCode, respBody) ||
  389. requestHasGrokEncryptedReasoning(responsesBody)
  390. if !shouldStrip {
  391. resp.Body = io.NopCloser(bytes.NewReader(respBody))
  392. break
  393. }
  394. retryBody, changed, trimErr := trimGrokInvalidEncryptedContentRetryBody(responsesBody)
  395. if trimErr != nil {
  396. return nil, fmt.Errorf("prepare Grok invalid encrypted_content retry: %w", trimErr)
  397. }
  398. if !changed {
  399. resp.Body = io.NopCloser(bytes.NewReader(respBody))
  400. break
  401. }
  402. responsesBody = retryBody
  403. logger.L().Info("openai messages: retrying after stripping invalid Grok encrypted_content",
  404. zap.Int64("account_id", account.ID),
  405. zap.Bool("cache_identity_present", strings.TrimSpace(grokCacheIdentity) != ""),
  406. zap.String("upstream_error_preview", truncateOpenAIWSLogValue(string(respBody), 240)),
  407. )
  408. }
  409. defer func() { _ = resp.Body.Close() }()
  410. // 8. Handle error response with failover
  411. if resp.StatusCode >= 400 {
  412. respBody, upstreamMsg := s.readOpenAIUpstreamError(resp)
  413. if !agentIdentityTaskRecoveryWasTried(ctx) && s.isAgentIdentityAccount(ctx, account) && isAgentIdentityTaskInvalidHTTPResponse(resp.StatusCode, respBody) {
  414. expectedTaskID := account.GetCredential("task_id")
  415. if err := s.recoverAgentIdentityTask(ctx, account, expectedTaskID); err != nil {
  416. return nil, fmt.Errorf("agent identity task recovery failed: %w", err)
  417. }
  418. return s.ForwardAsAnthropic(markAgentIdentityTaskRecoveryTried(ctx), c, account, body, promptCacheKey, defaultMappedModel)
  419. }
  420. if previousResponseID != "" && (isOpenAICompatPreviousResponseNotFound(resp.StatusCode, upstreamMsg, respBody) || isOpenAICompatPreviousResponseUnsupported(resp.StatusCode, upstreamMsg, respBody)) {
  421. if isOpenAICompatPreviousResponseUnsupported(resp.StatusCode, upstreamMsg, respBody) {
  422. s.disableOpenAICompatSessionContinuation(ctx, c, account, promptCacheKey)
  423. } else {
  424. s.deleteOpenAICompatSessionResponseID(ctx, c, account, promptCacheKey)
  425. }
  426. logger.L().Info("openai messages: previous_response_id unavailable, retrying without continuation",
  427. zap.Int64("account_id", account.ID),
  428. zap.String("previous_response_id", truncateOpenAIWSLogValue(previousResponseID, openAIWSIDValueMaxLen)),
  429. zap.String("upstream_model", upstreamModel),
  430. )
  431. return s.ForwardAsAnthropic(ctx, c, account, body, promptCacheKey, defaultMappedModel)
  432. }
  433. // Grok account-switched history often fails decrypt; strip encrypted
  434. // reasoning once at the client-body level so failover accounts can accept
  435. // the multi-turn tool continuation instead of cascading 400s.
  436. if account.Platform == PlatformGrok &&
  437. isGrokInvalidEncryptedContentResponse(resp.StatusCode, respBody) &&
  438. !grokEncryptedContentStripRetried(ctx) {
  439. if strippedBody, ok := stripAnthropicThinkingSignatures(body); ok {
  440. logger.L().Info("openai messages: stripping thinking signatures for Grok failover retry",
  441. zap.Int64("account_id", account.ID),
  442. )
  443. return s.ForwardAsAnthropic(markGrokEncryptedContentStripRetried(ctx), c, account, strippedBody, promptCacheKey, defaultMappedModel)
  444. }
  445. }
  446. if foErr := s.failoverOpenAIUpstreamHTTPError(ctx, c, account, resp, respBody, upstreamMsg, upstreamModel); foErr != nil {
  447. return nil, foErr
  448. }
  449. // Non-failover error: return Anthropic-formatted error to client
  450. return s.handleAnthropicErrorResponse(resp, c, account, billingModel)
  451. }
  452. if account.Platform == PlatformGrok && account.Type == AccountTypeOAuth && !account.IsShadow() {
  453. s.updateGrokUsageFromResponse(withGrokTeamRateLimitModel(ctx, upstreamModel), account, resp.Header, resp.StatusCode)
  454. }
  455. if account.UsesOpenAICodexProtocol() && promptCacheKey != "" {
  456. if turnState := strings.TrimSpace(resp.Header.Get("x-codex-turn-state")); turnState != "" {
  457. s.bindOpenAICompatSessionTurnState(ctx, c, account, promptCacheKey, turnState)
  458. }
  459. }
  460. // 9. Handle normal response
  461. // Upstream is always streaming; choose response format based on client preference.
  462. var result *OpenAIForwardResult
  463. var handleErr error
  464. if clientStream {
  465. result, handleErr = s.handleAnthropicStreamingResponse(resp, c, account, originalModel, billingModel, upstreamModel, startTime)
  466. } else {
  467. // Client wants JSON: buffer the streaming response and assemble a JSON reply.
  468. result, handleErr = s.handleAnthropicBufferedStreamingResponse(resp, c, account, originalModel, billingModel, upstreamModel, startTime)
  469. }
  470. // cyber_policy:标记已设、error 已按 Anthropic 格式发给客户端。丢弃 result、返回哨兵,
  471. // 使 handler 落入 tokens=0 免费用量行(对齐 /v1/responses),不计费、不 failover。
  472. if GetOpsCyberPolicy(c) != nil {
  473. if handleErr == nil {
  474. handleErr = errOpenAICyberPolicyForwarded
  475. }
  476. return nil, handleErr
  477. }
  478. // Propagate ServiceTier and ReasoningEffort to result for billing
  479. if handleErr == nil && result != nil {
  480. if compatContinuationEnabled && promptCacheKey != "" && result.ResponseID != "" {
  481. s.bindOpenAICompatSessionResponseID(ctx, c, account, promptCacheKey, result.ResponseID)
  482. }
  483. if promptCacheKey != "" && anthropicDigestChain != "" {
  484. s.bindOpenAICompatAnthropicDigestPromptCacheKey(account, apiKeyID, anthropicDigestChain, promptCacheKey, anthropicMatchedDigestChain)
  485. }
  486. // 计费 tier 优先采用上游回显值;上游未回显时回退到最终出站 body(经过
  487. // fast policy filter/force 之后)里的 tier。
  488. if tier := resolvedOpenAIUpstreamServiceTier(c, extractOpenAIServiceTierFromBody(responsesBody)); tier != nil {
  489. result.ServiceTier = tier
  490. }
  491. if responsesReq.Reasoning != nil && responsesReq.Reasoning.Effort != "" {
  492. re := responsesReq.Reasoning.Effort
  493. result.ReasoningEffort = &re
  494. }
  495. }
  496. // Extract and save Codex usage snapshot from response headers (for OAuth accounts).
  497. // 排除 spark 影子:其 codex_* 仅由 QueryUsage(/wham/usage bengalfox)更新(外审第7轮 P1)。
  498. if handleErr == nil && account.Type == AccountTypeOAuth && !account.IsShadow() && account.Platform != PlatformGrok {
  499. if snapshot := ParseCodexRateLimitHeaders(resp.Header); snapshot != nil {
  500. s.updateCodexUsageSnapshot(ctx, account.ID, snapshot)
  501. }
  502. } else if handleErr == nil && account.IsShadow() && account.ParentAccountID != nil {
  503. notifyOpenAIAutoReset(*account.ParentAccountID)
  504. }
  505. return result, handleErr
  506. }
  507. func ensureCodexOAuthInstructionsField(reqBody map[string]any) {
  508. if reqBody == nil {
  509. return
  510. }
  511. if value, ok := reqBody["instructions"]; !ok || value == nil {
  512. reqBody["instructions"] = ""
  513. return
  514. }
  515. if _, ok := reqBody["instructions"].(string); !ok {
  516. reqBody["instructions"] = ""
  517. }
  518. }
  519. // handleAnthropicErrorResponse reads an upstream error and returns it in
  520. // Anthropic error format.
  521. func (s *OpenAIGatewayService) handleAnthropicErrorResponse(
  522. resp *http.Response,
  523. c *gin.Context,
  524. account *Account,
  525. requestedModel ...string,
  526. ) (*OpenAIForwardResult, error) {
  527. return s.handleCompatErrorResponse(resp, c, account, writeAnthropicError, requestedModel...)
  528. }
  529. // handleAnthropicBufferedStreamingResponse reads all Responses SSE events from
  530. // the upstream streaming response, finds the terminal event (response.completed
  531. // / response.incomplete / response.failed), converts the complete response to
  532. // Anthropic Messages JSON format, and writes it to the client.
  533. // This is used when the client requested stream=false but the upstream is always
  534. // streaming.
  535. func (s *OpenAIGatewayService) handleAnthropicBufferedStreamingResponse(
  536. resp *http.Response,
  537. c *gin.Context,
  538. account *Account,
  539. originalModel string,
  540. billingModel string,
  541. upstreamModel string,
  542. startTime time.Time,
  543. ) (*OpenAIForwardResult, error) {
  544. requestID := resp.Header.Get("x-request-id")
  545. finalResponse, usage, acc, err := s.readOpenAICompatBufferedTerminal(resp, c, "openai messages buffered", requestID)
  546. if err != nil {
  547. var readErr *openAICompatBufferedReadError
  548. if errors.As(err, &readErr) && readErr != nil {
  549. return nil, readErr.cause
  550. }
  551. return nil, err
  552. }
  553. if finalResponse == nil {
  554. writeAnthropicError(c, http.StatusBadGateway, "api_error", "Upstream stream ended without a terminal response event")
  555. return nil, fmt.Errorf("upstream stream ended without terminal event")
  556. }
  557. observer := upstreamResponseModelObserverFromContext(c)
  558. if observer == nil {
  559. observer = beginUpstreamResponseModelObservation(c)
  560. }
  561. observer.Observe(finalResponse.Model, true)
  562. observer.ObserveServiceTier(finalResponse.ServiceTier, true)
  563. if strings.TrimSpace(finalResponse.Status) == "failed" {
  564. payload, _ := json.Marshal(gin.H{"type": "response.failed", "response": finalResponse})
  565. if hit, code, msg := detectOpenAICyberPolicy(payload); hit {
  566. MarkOpsCyberPolicy(c, CyberPolicyMark{
  567. Code: code,
  568. Message: msg,
  569. Body: truncateString(string(payload), 4096),
  570. UpstreamStatus: http.StatusOK,
  571. UpstreamInTok: usage.InputTokens,
  572. UpstreamOutTok: usage.OutputTokens,
  573. })
  574. clientMsg := msg
  575. if clientMsg == "" {
  576. clientMsg = "Request blocked by upstream cyber-security policy"
  577. }
  578. writeAnthropicError(c, http.StatusBadRequest, "invalid_request_error", clientMsg)
  579. return nil, fmt.Errorf("openai cyber_policy: %s", msg)
  580. }
  581. message := openAICompatFailedResponseMessage(finalResponse)
  582. if openAIStreamFailedEventShouldFailover(payload, message) {
  583. return nil, s.newOpenAIStreamFailoverErrorWithModel(c, account, false, requestID, payload, message, upstreamModel, resp.Header)
  584. }
  585. message = s.recordOpenAIStreamUpstreamError(c, account, false, requestID, "http_error", payload, message)
  586. // 统一走语义状态推断 + body 归一化(与 /v1/responses 路径一致),
  587. // 使按错误码配置的透传规则可命中。
  588. if status, errType, errMsg, matched := applyOpenAIStreamFailedErrorPassthroughRule(
  589. c, account.Platform, payload, message,
  590. ); matched {
  591. if errMsg == "" {
  592. errMsg = message
  593. }
  594. MarkResponseCommitted(c)
  595. writeAnthropicError(c, status, errType, errMsg)
  596. return nil, fmt.Errorf("upstream response failed (passthrough): %s", errMsg)
  597. }
  598. writeAnthropicError(c, http.StatusBadGateway, "api_error", message)
  599. return nil, fmt.Errorf("upstream response failed: %s", message)
  600. }
  601. if strings.TrimSpace(finalResponse.Status) == "completed" {
  602. logOpenAISuccessMissingUsage(c.Request.Context(), c, account, resp, &usage, "response.completed", false)
  603. }
  604. // When the terminal event has an empty output array, reconstruct from
  605. // accumulated delta events so the client receives the full content.
  606. acc.SupplementResponseOutput(finalResponse)
  607. anthropicResp := apicompat.ResponsesToAnthropic(finalResponse, originalModel)
  608. if s.responseHeaderFilter != nil {
  609. responseheaders.WriteFilteredHeaders(c.Writer.Header(), resp.Header, s.responseHeaderFilter)
  610. }
  611. c.Header("Content-Type", "application/json; charset=utf-8")
  612. c.JSON(http.StatusOK, anthropicResp)
  613. result := &OpenAIForwardResult{
  614. RequestID: requestID,
  615. UpstreamHeaders: resp.Header,
  616. ResponseID: finalResponse.ID,
  617. Usage: usage,
  618. Model: originalModel,
  619. BillingModel: billingModel,
  620. UpstreamModel: upstreamModel,
  621. UpstreamResponseModel: observedUpstreamResponseModel(c),
  622. UpstreamResponseModelConflict: observedUpstreamResponseModelConflict(c),
  623. UpstreamResponseServiceTier: observedUpstreamResponseServiceTier(c),
  624. Stream: false,
  625. Duration: time.Since(startTime),
  626. }
  627. // Grok /v1/messages uses Responses upstream; count native search for surcharge.
  628. if account != nil && account.IsGrok() && finalResponse != nil {
  629. if body, err := json.Marshal(finalResponse); err == nil {
  630. if n := countGrokNativeSearchCallsFromJSONBytes(body); n > 0 {
  631. result.SearchCount = n
  632. }
  633. }
  634. }
  635. return result, nil
  636. }
  637. func isOpenAICompatResponsesTerminalEvent(eventType string) bool {
  638. switch strings.TrimSpace(eventType) {
  639. case "response.completed", "response.done", "response.incomplete", "response.failed", "response.cancelled", "response.canceled", "error":
  640. return true
  641. default:
  642. return false
  643. }
  644. }
  645. func (s *OpenAIGatewayService) recordOpenAIMessagesStreamUpstreamError(c *gin.Context, account *Account, upstreamRequestID, kind, message string) {
  646. if c == nil {
  647. return
  648. }
  649. message = sanitizeUpstreamErrorMessage(message)
  650. setOpsUpstreamError(c, http.StatusBadGateway, message, "")
  651. event := OpsUpstreamErrorEvent{
  652. ProxyID: opsUpstreamProxyID(account),
  653. ProxyName: opsUpstreamProxyName(account),
  654. Platform: PlatformOpenAI,
  655. UpstreamStatusCode: http.StatusBadGateway,
  656. UpstreamRequestID: strings.TrimSpace(upstreamRequestID),
  657. Kind: kind,
  658. Message: message,
  659. }
  660. if account != nil {
  661. event.Platform = account.Platform
  662. event.AccountID = account.ID
  663. event.AccountName = account.Name
  664. }
  665. appendOpsUpstreamError(c, event)
  666. }
  667. func isOpenAICompatDoneSentinelLine(line string) bool {
  668. payload, ok := extractOpenAISSEDataLine(line)
  669. return ok && strings.TrimSpace(payload) == "[DONE]"
  670. }
  671. // openAICompatBufferedReadError 只标记错误发生在上游响应体读取阶段;
  672. // 具体端点自行决定是否允许重放请求,避免共享读取器扩大重试范围。
  673. type openAICompatBufferedReadError struct {
  674. cause error
  675. }
  676. func (e *openAICompatBufferedReadError) Error() string { return e.cause.Error() }
  677. func (e *openAICompatBufferedReadError) Unwrap() error { return e.cause }
  678. func openAICompatTerminalResponse(event *apicompat.ResponsesStreamEvent, payload []byte) *apicompat.ResponsesResponse {
  679. if event == nil {
  680. return nil
  681. }
  682. if event.Response != nil {
  683. return event.Response
  684. }
  685. switch strings.TrimSpace(event.Type) {
  686. case "response.failed", "error":
  687. message := extractOpenAISSEErrorMessage(payload)
  688. if message == "" {
  689. message = "Upstream response failed"
  690. }
  691. return &apicompat.ResponsesResponse{
  692. Status: "failed",
  693. Error: &apicompat.ResponsesError{Code: event.Code, Message: message},
  694. }
  695. default:
  696. return nil
  697. }
  698. }
  699. func (s *OpenAIGatewayService) readOpenAICompatBufferedTerminal(
  700. resp *http.Response,
  701. c *gin.Context,
  702. logPrefix string,
  703. requestID string,
  704. ) (*apicompat.ResponsesResponse, OpenAIUsage, *apicompat.BufferedResponseAccumulator, error) {
  705. acc := apicompat.NewBufferedResponseAccumulator()
  706. var usage OpenAIUsage
  707. if resp == nil || resp.Body == nil {
  708. return nil, usage, acc, errors.New("upstream response body is nil")
  709. }
  710. scanner := s.newUpstreamSSEScanner(resp.Body)
  711. streamInterval := time.Duration(0)
  712. if s.cfg != nil && s.cfg.Gateway.StreamDataIntervalTimeout > 0 {
  713. streamInterval = time.Duration(s.cfg.Gateway.StreamDataIntervalTimeout) * time.Second
  714. }
  715. var timeoutCh <-chan time.Time
  716. var timeoutTimer *time.Timer
  717. resetTimeout := func() {
  718. if streamInterval <= 0 {
  719. return
  720. }
  721. if timeoutTimer == nil {
  722. timeoutTimer = time.NewTimer(streamInterval)
  723. timeoutCh = timeoutTimer.C
  724. return
  725. }
  726. if !timeoutTimer.Stop() {
  727. select {
  728. case <-timeoutTimer.C:
  729. default:
  730. }
  731. }
  732. timeoutTimer.Reset(streamInterval)
  733. }
  734. stopTimeout := func() {
  735. if timeoutTimer == nil {
  736. return
  737. }
  738. if !timeoutTimer.Stop() {
  739. select {
  740. case <-timeoutTimer.C:
  741. default:
  742. }
  743. }
  744. }
  745. resetTimeout()
  746. defer stopTimeout()
  747. type scanEvent struct {
  748. line string
  749. err error
  750. }
  751. events := make(chan scanEvent, 16)
  752. done := make(chan struct{})
  753. go func() {
  754. defer close(events)
  755. for scanner.Scan() {
  756. select {
  757. case events <- scanEvent{line: scanner.Text()}:
  758. case <-done:
  759. return
  760. }
  761. }
  762. if err := scanner.Err(); err != nil {
  763. select {
  764. case events <- scanEvent{err: err}:
  765. case <-done:
  766. }
  767. }
  768. }()
  769. defer close(done)
  770. var parser openAICompatSSEFrameParser
  771. for {
  772. select {
  773. case ev, ok := <-events:
  774. if !ok {
  775. if frame, ok := parser.Finish(); ok {
  776. payload := openAICompatPayloadWithEventType(frame.Data, frame.EventType)
  777. payload = string(restoreCodexToolNamesFromContext(c, []byte(payload)))
  778. var event apicompat.ResponsesStreamEvent
  779. if err := json.Unmarshal([]byte(payload), &event); err == nil {
  780. s.parseSSEUsageBytesWithType([]byte(payload), event.Type, &usage)
  781. acc.ProcessEvent(&event)
  782. if response := openAICompatTerminalResponse(&event, []byte(payload)); isOpenAICompatResponsesTerminalEvent(event.Type) && response != nil {
  783. if event.Usage != nil {
  784. usage = copyOpenAIUsageFromResponsesUsage(event.Usage)
  785. if response.Usage == nil {
  786. response.Usage = event.Usage
  787. }
  788. }
  789. if response.Usage != nil {
  790. usage = copyOpenAIUsageFromResponsesUsage(response.Usage)
  791. }
  792. return response, usage, acc, nil
  793. }
  794. }
  795. }
  796. return nil, usage, acc, nil
  797. }
  798. resetTimeout()
  799. if ev.err != nil {
  800. if !errors.Is(ev.err, context.Canceled) && !errors.Is(ev.err, context.DeadlineExceeded) {
  801. logger.L().Warn(logPrefix+": read error",
  802. zap.Error(ev.err),
  803. zap.String("request_id", requestID),
  804. )
  805. }
  806. return nil, usage, acc, &openAICompatBufferedReadError{cause: ev.err}
  807. }
  808. if isOpenAICompatDoneSentinelLine(ev.line) {
  809. return nil, usage, acc, nil
  810. }
  811. frame, ok := parser.AddLine(ev.line)
  812. if !ok {
  813. continue
  814. }
  815. payload := openAICompatPayloadWithEventType(frame.Data, frame.EventType)
  816. payload = string(restoreCodexToolNamesFromContext(c, []byte(payload)))
  817. var event apicompat.ResponsesStreamEvent
  818. if err := json.Unmarshal([]byte(payload), &event); err != nil {
  819. logger.L().Warn(logPrefix+": failed to parse event",
  820. zap.Error(err),
  821. zap.String("request_id", requestID),
  822. )
  823. continue
  824. }
  825. s.parseSSEUsageBytesWithType([]byte(payload), event.Type, &usage)
  826. acc.ProcessEvent(&event)
  827. if response := openAICompatTerminalResponse(&event, []byte(payload)); isOpenAICompatResponsesTerminalEvent(event.Type) && response != nil {
  828. if event.Usage != nil {
  829. usage = copyOpenAIUsageFromResponsesUsage(event.Usage)
  830. if response.Usage == nil {
  831. response.Usage = event.Usage
  832. }
  833. }
  834. if response.Usage != nil {
  835. usage = copyOpenAIUsageFromResponsesUsage(response.Usage)
  836. }
  837. return response, usage, acc, nil
  838. }
  839. case <-timeoutCh:
  840. _ = resp.Body.Close()
  841. logger.L().Warn(logPrefix+": data interval timeout",
  842. zap.String("request_id", requestID),
  843. zap.Duration("interval", streamInterval),
  844. )
  845. return nil, usage, acc, fmt.Errorf("stream data interval timeout")
  846. }
  847. }
  848. }
  849. // handleAnthropicStreamingResponse reads Responses SSE events from upstream,
  850. // converts each to Anthropic SSE events, and writes them to the client.
  851. // When StreamKeepaliveInterval is configured, it uses a goroutine + channel
  852. // pattern to send Anthropic ping events during periods of upstream silence,
  853. // preventing proxy/client timeout disconnections.
  854. func (s *OpenAIGatewayService) handleAnthropicStreamingResponse(
  855. resp *http.Response,
  856. c *gin.Context,
  857. account *Account,
  858. originalModel string,
  859. billingModel string,
  860. upstreamModel string,
  861. startTime time.Time,
  862. ) (*OpenAIForwardResult, error) {
  863. requestID := resp.Header.Get("x-request-id")
  864. writeStreamHeaders := s.newStreamHeaderWriter(c, resp.Header)
  865. state := apicompat.NewResponsesEventToAnthropicState()
  866. state.Model = originalModel
  867. var usage OpenAIUsage
  868. responseID := ""
  869. var firstTokenMs *int
  870. firstChunk := true
  871. clientDisconnected := false
  872. clientOutputStarted := false
  873. var streamFailoverErr error
  874. var streamNonFailoverErr error
  875. terminalEventType := ""
  876. searchCount := 0
  877. streamSearchSeen := make(map[string]struct{})
  878. countSearch := account != nil && account.IsGrok()
  879. scanner := s.newUpstreamSSEScanner(resp.Body)
  880. streamInterval := time.Duration(0)
  881. if s.cfg != nil && s.cfg.Gateway.StreamDataIntervalTimeout > 0 {
  882. streamInterval = time.Duration(s.cfg.Gateway.StreamDataIntervalTimeout) * time.Second
  883. }
  884. var intervalTicker *time.Ticker
  885. if streamInterval > 0 {
  886. intervalTicker = time.NewTicker(streamInterval)
  887. defer intervalTicker.Stop()
  888. }
  889. var intervalCh <-chan time.Time
  890. if intervalTicker != nil {
  891. intervalCh = intervalTicker.C
  892. }
  893. observer := upstreamResponseModelObserverFromContext(c)
  894. if observer == nil {
  895. observer = beginUpstreamResponseModelObservation(c)
  896. }
  897. // resultWithUsage builds the final result snapshot.
  898. resultWithUsage := func() *OpenAIForwardResult {
  899. out := &OpenAIForwardResult{
  900. RequestID: requestID,
  901. UpstreamHeaders: resp.Header,
  902. ResponseID: responseID,
  903. Usage: usage,
  904. Model: originalModel,
  905. BillingModel: billingModel,
  906. UpstreamModel: upstreamModel,
  907. UpstreamResponseModel: observedUpstreamResponseModel(c),
  908. UpstreamResponseModelConflict: observedUpstreamResponseModelConflict(c),
  909. UpstreamResponseServiceTier: observedUpstreamResponseServiceTier(c),
  910. Stream: true,
  911. Duration: time.Since(startTime),
  912. FirstTokenMs: firstTokenMs,
  913. ClientDisconnect: clientDisconnected,
  914. }
  915. if searchCount > 0 {
  916. out.SearchCount = searchCount
  917. }
  918. return out
  919. }
  920. // processDataLine handles a single "data: ..." SSE line from upstream.
  921. processDataLine := func(payload string) bool {
  922. payload = string(restoreCodexToolNamesFromContext(c, []byte(payload)))
  923. if firstChunk {
  924. firstChunk = false
  925. ms := int(time.Since(startTime).Milliseconds())
  926. firstTokenMs = &ms
  927. }
  928. if countSearch {
  929. searchCount += countGrokNativeSearchCallsInSSEDataDedup([]byte(payload), streamSearchSeen)
  930. }
  931. var event apicompat.ResponsesStreamEvent
  932. if err := json.Unmarshal([]byte(payload), &event); err != nil {
  933. logger.L().Warn("openai messages stream: failed to parse event",
  934. zap.Error(err),
  935. zap.String("request_id", requestID),
  936. )
  937. return false
  938. }
  939. observer.ObserveOpenAI([]byte(payload), event.Type)
  940. s.parseSSEUsageBytesWithType([]byte(payload), event.Type, &usage)
  941. eventType := strings.TrimSpace(event.Type)
  942. isBareErrorEvent := eventType == "error"
  943. isTerminalEvent := isOpenAICompatResponsesTerminalEvent(eventType) || isBareErrorEvent
  944. if isTerminalEvent {
  945. terminalEventType = eventType
  946. if event.Response != nil {
  947. if id := strings.TrimSpace(event.Response.ID); id != "" {
  948. responseID = id
  949. }
  950. if event.Response.Usage != nil {
  951. usage = copyOpenAIUsageFromResponsesUsage(event.Response.Usage)
  952. }
  953. }
  954. if event.Usage != nil {
  955. usage = copyOpenAIUsageFromResponsesUsage(event.Usage)
  956. }
  957. // cyber_policy 致命不可重试:标记供 handler 事后记录;以 Anthropic SSE error 事件
  958. // 回写让客户端感知并停止重试(F4),丢弃后续转换输出。
  959. if eventType == "response.failed" || isBareErrorEvent {
  960. payloadBytes := []byte(payload)
  961. if hit, code, msg := detectOpenAICyberPolicy(payloadBytes); hit {
  962. MarkOpsCyberPolicy(c, CyberPolicyMark{
  963. Code: code,
  964. Message: msg,
  965. Body: truncateString(payload, 4096),
  966. UpstreamStatus: http.StatusOK,
  967. UpstreamInTok: usage.InputTokens,
  968. UpstreamOutTok: usage.OutputTokens,
  969. })
  970. if !clientDisconnected {
  971. writeStreamHeaders()
  972. clientMsg := msg
  973. if clientMsg == "" {
  974. clientMsg = "Request blocked by upstream cyber-security policy"
  975. }
  976. if _, err := fmt.Fprint(c.Writer, buildAnthropicStreamErrorSSE("invalid_request_error", clientMsg)); err == nil {
  977. c.Writer.Flush()
  978. }
  979. clientDisconnected = true
  980. }
  981. return true
  982. }
  983. message := extractOpenAISSEErrorMessage(payloadBytes)
  984. // Once Anthropic output has started, switching accounts would splice
  985. // two model streams together. Surface a proper Anthropic error event
  986. // instead of returning a failover error that the handler cannot retry.
  987. shouldFailover := openAIStreamFailedEventShouldFailover(payloadBytes, message)
  988. if isBareErrorEvent {
  989. shouldFailover = openAIStreamErrorEventShouldFailover(payloadBytes, message)
  990. }
  991. if !clientOutputStarted && shouldFailover {
  992. streamFailoverErr = s.newOpenAIStreamFailoverErrorWithModel(c, account, false, requestID, payloadBytes, message, upstreamModel, resp.Header)
  993. return true
  994. }
  995. message = s.recordOpenAIStreamUpstreamError(c, account, false, requestID, "http_error", payloadBytes, message)
  996. errStatus, errType, errMsg := http.StatusBadGateway, "api_error", message
  997. // 统一走语义状态推断 + body 归一化(与 /v1/responses 路径一致),
  998. // 使按错误码配置的透传规则可命中。
  999. if status, et, em, matched := applyOpenAIStreamFailedErrorPassthroughRule(
  1000. c, account.Platform, payloadBytes, message,
  1001. ); matched {
  1002. if em == "" {
  1003. em = errMsg
  1004. }
  1005. errStatus, errType, errMsg = status, et, em
  1006. MarkResponseCommitted(c)
  1007. }
  1008. if !clientDisconnected {
  1009. if !clientOutputStarted {
  1010. writeAnthropicError(c, errStatus, errType, errMsg)
  1011. clientOutputStarted = true
  1012. } else {
  1013. writeStreamHeaders()
  1014. if _, err := fmt.Fprint(c.Writer, buildAnthropicStreamErrorSSE(errType, errMsg)); err == nil {
  1015. c.Writer.Flush()
  1016. }
  1017. }
  1018. }
  1019. streamNonFailoverErr = fmt.Errorf("upstream response failed: %s", errMsg)
  1020. return true
  1021. }
  1022. }
  1023. // Convert to Anthropic events
  1024. events := apicompat.ResponsesEventToAnthropicEvents(&event, state)
  1025. if !clientDisconnected {
  1026. for _, evt := range events {
  1027. sse, err := apicompat.ResponsesAnthropicEventToSSE(evt)
  1028. if err != nil {
  1029. logger.L().Warn("openai messages stream: failed to marshal event",
  1030. zap.Error(err),
  1031. zap.String("request_id", requestID),
  1032. )
  1033. continue
  1034. }
  1035. writeStreamHeaders()
  1036. if _, err := fmt.Fprint(c.Writer, sse); err != nil {
  1037. clientDisconnected = true
  1038. logger.L().Info("openai messages stream: client disconnected, continuing to drain upstream for billing",
  1039. zap.String("request_id", requestID),
  1040. )
  1041. break
  1042. }
  1043. clientOutputStarted = true
  1044. }
  1045. }
  1046. if len(events) > 0 && !clientDisconnected {
  1047. c.Writer.Flush()
  1048. }
  1049. return isTerminalEvent
  1050. }
  1051. // finalizeStream sends any remaining Anthropic events and returns the result.
  1052. finalizeStream := func() (*OpenAIForwardResult, error) {
  1053. if streamFailoverErr != nil {
  1054. return resultWithUsage(), streamFailoverErr
  1055. }
  1056. if streamNonFailoverErr != nil {
  1057. return resultWithUsage(), streamNonFailoverErr
  1058. }
  1059. if finalEvents := apicompat.FinalizeResponsesAnthropicStream(state); len(finalEvents) > 0 && !clientDisconnected {
  1060. for _, evt := range finalEvents {
  1061. sse, err := apicompat.ResponsesAnthropicEventToSSE(evt)
  1062. if err != nil {
  1063. continue
  1064. }
  1065. writeStreamHeaders()
  1066. if _, err := fmt.Fprint(c.Writer, sse); err != nil {
  1067. clientDisconnected = true
  1068. logger.L().Info("openai messages stream: client disconnected during final flush",
  1069. zap.String("request_id", requestID),
  1070. )
  1071. break
  1072. }
  1073. clientOutputStarted = true
  1074. }
  1075. if !clientDisconnected {
  1076. c.Writer.Flush()
  1077. }
  1078. }
  1079. logOpenAISuccessMissingUsage(c.Request.Context(), c, account, resp, &usage, terminalEventType, clientDisconnected)
  1080. return resultWithUsage(), nil
  1081. }
  1082. // handleScanErr logs scanner errors if meaningful.
  1083. handleScanErr := func(err error) {
  1084. if err != nil && !errors.Is(err, context.Canceled) && !errors.Is(err, context.DeadlineExceeded) {
  1085. logger.L().Warn("openai messages stream: read error",
  1086. zap.Error(err),
  1087. zap.String("request_id", requestID),
  1088. )
  1089. }
  1090. }
  1091. missingTerminalErr := func() (*OpenAIForwardResult, error) {
  1092. result := resultWithUsage()
  1093. if clientDisconnected {
  1094. return result, fmt.Errorf("stream usage incomplete: missing terminal event")
  1095. }
  1096. message := "OpenAI messages stream ended before a terminal event"
  1097. if !clientOutputStarted {
  1098. return result, s.newOpenAIStreamFailoverError(c, account, false, requestID, nil, message)
  1099. }
  1100. s.recordOpenAIMessagesStreamUpstreamError(c, account, requestID, "stream_missing_terminal", message)
  1101. return result, fmt.Errorf("stream usage incomplete: missing terminal event")
  1102. }
  1103. processFrame := func(frame openAICompatSSEFrame) bool {
  1104. payload := openAICompatPayloadWithEventType(frame.Data, frame.EventType)
  1105. return processDataLine(payload)
  1106. }
  1107. // ── Determine keepalive interval ──
  1108. keepaliveInterval := time.Duration(0)
  1109. if s.cfg != nil && s.cfg.Gateway.StreamKeepaliveInterval > 0 {
  1110. keepaliveInterval = time.Duration(s.cfg.Gateway.StreamKeepaliveInterval) * time.Second
  1111. }
  1112. // ── No keepalive: fast synchronous path (no goroutine overhead) ──
  1113. if streamInterval <= 0 && keepaliveInterval <= 0 {
  1114. var parser openAICompatSSEFrameParser
  1115. for scanner.Scan() {
  1116. line := scanner.Text()
  1117. if isOpenAICompatDoneSentinelLine(line) {
  1118. return missingTerminalErr()
  1119. }
  1120. frame, ok := parser.AddLine(line)
  1121. if !ok {
  1122. continue
  1123. }
  1124. if processFrame(frame) {
  1125. return finalizeStream()
  1126. }
  1127. }
  1128. if err := scanner.Err(); err != nil {
  1129. handleScanErr(err)
  1130. return resultWithUsage(), fmt.Errorf("stream usage incomplete: %w", err)
  1131. }
  1132. if frame, ok := parser.Finish(); ok {
  1133. if strings.TrimSpace(frame.Data) == "[DONE]" {
  1134. return missingTerminalErr()
  1135. }
  1136. if processFrame(frame) {
  1137. return finalizeStream()
  1138. }
  1139. }
  1140. return missingTerminalErr()
  1141. }
  1142. // ── With keepalive: goroutine + channel + select ──
  1143. type scanEvent struct {
  1144. line string
  1145. err error
  1146. }
  1147. events := make(chan scanEvent, 16)
  1148. done := make(chan struct{})
  1149. var lastReadAt int64
  1150. atomic.StoreInt64(&lastReadAt, time.Now().UnixNano())
  1151. sendEvent := func(ev scanEvent) bool {
  1152. select {
  1153. case events <- ev:
  1154. return true
  1155. case <-done:
  1156. return false
  1157. }
  1158. }
  1159. go func() {
  1160. defer close(events)
  1161. for scanner.Scan() {
  1162. atomic.StoreInt64(&lastReadAt, time.Now().UnixNano())
  1163. if !sendEvent(scanEvent{line: scanner.Text()}) {
  1164. return
  1165. }
  1166. }
  1167. if err := scanner.Err(); err != nil {
  1168. _ = sendEvent(scanEvent{err: err})
  1169. }
  1170. }()
  1171. defer close(done)
  1172. var keepaliveTicker *time.Ticker
  1173. if keepaliveInterval > 0 {
  1174. keepaliveTicker = time.NewTicker(keepaliveInterval)
  1175. defer keepaliveTicker.Stop()
  1176. }
  1177. var keepaliveCh <-chan time.Time
  1178. if keepaliveTicker != nil {
  1179. keepaliveCh = keepaliveTicker.C
  1180. }
  1181. lastDataAt := time.Now()
  1182. var parser openAICompatSSEFrameParser
  1183. for {
  1184. select {
  1185. case ev, ok := <-events:
  1186. if !ok {
  1187. // Upstream closed
  1188. if frame, ok := parser.Finish(); ok {
  1189. if strings.TrimSpace(frame.Data) == "[DONE]" {
  1190. return missingTerminalErr()
  1191. }
  1192. if processFrame(frame) {
  1193. return finalizeStream()
  1194. }
  1195. }
  1196. return missingTerminalErr()
  1197. }
  1198. if ev.err != nil {
  1199. handleScanErr(ev.err)
  1200. return resultWithUsage(), fmt.Errorf("stream usage incomplete: %w", ev.err)
  1201. }
  1202. lastDataAt = time.Now()
  1203. line := ev.line
  1204. if isOpenAICompatDoneSentinelLine(line) {
  1205. return missingTerminalErr()
  1206. }
  1207. frame, ok := parser.AddLine(line)
  1208. if !ok {
  1209. continue
  1210. }
  1211. if processFrame(frame) {
  1212. return finalizeStream()
  1213. }
  1214. case <-intervalCh:
  1215. lastRead := time.Unix(0, atomic.LoadInt64(&lastReadAt))
  1216. if time.Since(lastRead) < streamInterval {
  1217. continue
  1218. }
  1219. if clientDisconnected {
  1220. return resultWithUsage(), fmt.Errorf("stream usage incomplete after timeout")
  1221. }
  1222. logger.L().Warn("openai messages stream: data interval timeout",
  1223. zap.String("request_id", requestID),
  1224. zap.String("model", originalModel),
  1225. zap.Duration("interval", streamInterval),
  1226. )
  1227. return resultWithUsage(), fmt.Errorf("stream data interval timeout")
  1228. case <-keepaliveCh:
  1229. if clientDisconnected {
  1230. continue
  1231. }
  1232. if time.Since(lastDataAt) < keepaliveInterval {
  1233. continue
  1234. }
  1235. // Send Anthropic-format ping event
  1236. writeStreamHeaders()
  1237. if _, err := fmt.Fprint(c.Writer, "event: ping\ndata: {\"type\":\"ping\"}\n\n"); err != nil {
  1238. // Client disconnected
  1239. logger.L().Info("openai messages stream: client disconnected during keepalive",
  1240. zap.String("request_id", requestID),
  1241. )
  1242. clientDisconnected = true
  1243. continue
  1244. }
  1245. clientOutputStarted = true
  1246. c.Writer.Flush()
  1247. }
  1248. }
  1249. }
  1250. // writeAnthropicError writes an error response in Anthropic Messages API format.
  1251. func writeAnthropicError(c *gin.Context, statusCode int, errType, message string) {
  1252. c.JSON(statusCode, gin.H{
  1253. "type": "error",
  1254. "error": gin.H{
  1255. "type": errType,
  1256. "message": message,
  1257. },
  1258. })
  1259. }
  1260. // buildAnthropicStreamErrorSSE builds one Anthropic SSE `error` event so a
  1261. // streaming response can terminate with a visible error (e.g. upstream
  1262. // cyber_policy) and programmatic clients stop retrying.
  1263. // Marshal 失败的兜底仅保留固定提示。
  1264. func buildAnthropicStreamErrorSSE(errType, message string) string {
  1265. payload, err := json.Marshal(gin.H{
  1266. "type": "error",
  1267. "error": gin.H{
  1268. "type": errType,
  1269. "message": message,
  1270. },
  1271. })
  1272. if err != nil {
  1273. return "event: error\ndata: {\"type\":\"error\",\"error\":{\"type\":\"" + errType + "\",\"message\":\"upstream error\"}}\n\n"
  1274. }
  1275. return "event: error\ndata: " + string(payload) + "\n\n"
  1276. }
  1277. func copyOpenAIUsageFromResponsesUsage(usage *apicompat.ResponsesUsage) OpenAIUsage {
  1278. if usage == nil {
  1279. return OpenAIUsage{}
  1280. }
  1281. result := OpenAIUsage{
  1282. InputTokens: usage.InputTokens,
  1283. OutputTokens: usage.OutputTokens,
  1284. CacheCreationInputTokens: usage.CacheCreationInputTokens,
  1285. }
  1286. if usage.InputTokensDetails != nil {
  1287. result.CacheReadInputTokens = usage.InputTokensDetails.CachedTokens
  1288. }
  1289. return result
  1290. }