openai_gateway_response_handling.go 84 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363
  1. package service
  2. import (
  3. "bufio"
  4. "bytes"
  5. "context"
  6. "encoding/json"
  7. "errors"
  8. "fmt"
  9. "net/http"
  10. "sort"
  11. "strconv"
  12. "strings"
  13. "sync/atomic"
  14. "time"
  15. "github.com/Wei-Shaw/sub2api/internal/pkg/apicompat"
  16. "github.com/Wei-Shaw/sub2api/internal/pkg/logger"
  17. "github.com/Wei-Shaw/sub2api/internal/pkg/xai"
  18. "github.com/Wei-Shaw/sub2api/internal/util/responseheaders"
  19. "github.com/gin-gonic/gin"
  20. "github.com/google/uuid"
  21. "github.com/tidwall/gjson"
  22. "github.com/tidwall/sjson"
  23. "go.uber.org/zap"
  24. )
  25. // openaiStreamingResult streaming response result
  26. type openaiStreamingResult struct {
  27. usage *OpenAIUsage
  28. firstTokenMs *int
  29. responseID string
  30. imageCount int
  31. imageOutputSizes []string
  32. searchCount int
  33. }
  34. type openaiNonStreamingResult struct {
  35. *OpenAIUsage
  36. usage *OpenAIUsage
  37. responseID string
  38. imageCount int
  39. imageOutputSizes []string
  40. searchCount int
  41. }
  42. func (s *OpenAIGatewayService) handleStreamingResponse(ctx context.Context, resp *http.Response, c *gin.Context, account *Account, startTime time.Time, originalModel, mappedModel string) (*openaiStreamingResult, error) {
  43. return s.handleStreamingResponseWithReasoning(ctx, resp, c, account, startTime, originalModel, mappedModel, "")
  44. }
  45. func (s *OpenAIGatewayService) handleStreamingResponseWithReasoning(ctx context.Context, resp *http.Response, c *gin.Context, account *Account, startTime time.Time, originalModel, mappedModel, reasoningEffort string) (*openaiStreamingResult, error) {
  46. observer := upstreamResponseModelObserverFromContext(c)
  47. if observer == nil {
  48. observer = beginUpstreamResponseModelObservation(c)
  49. }
  50. firstOutputTimeout := time.Duration(0)
  51. if account != nil && account.Platform == PlatformOpenAI {
  52. firstOutputTimeout = s.openAIFirstOutputTimeout(reasoningEffort)
  53. }
  54. guardFirstOutput := firstOutputTimeout > 0
  55. stageFirstOutput := account != nil && account.Platform == PlatformOpenAI
  56. var attemptResponseHeaders http.Header
  57. if stageFirstOutput {
  58. if s.responseHeaderFilter != nil {
  59. attemptResponseHeaders = responseheaders.FilterHeaders(resp.Header, s.responseHeaderFilter)
  60. } else if requestID := strings.TrimSpace(resp.Header.Get("x-request-id")); requestID != "" {
  61. attemptResponseHeaders = http.Header{"X-Request-Id": []string{requestID}}
  62. }
  63. } else if s.responseHeaderFilter != nil {
  64. responseheaders.WriteFilteredHeaders(c.Writer.Header(), resp.Header, s.responseHeaderFilter)
  65. }
  66. // x-codex-turn-state 不在通用响应头白名单内,按 Codex 协议显式回传:
  67. // 客户端会在同回合的后续请求中回带(openai_codex_turn_state.go)。
  68. // OpenAI 首个语义输出前只暂存,溯源在 applyAttemptResponseHeaders 真正提交时记录。
  69. if stageFirstOutput {
  70. stageOpenAICodexTurnState(&attemptResponseHeaders, resp.Header)
  71. } else {
  72. s.relayOpenAICodexTurnState(c, account, resp.Header)
  73. }
  74. // Set SSE response headers
  75. c.Header("Content-Type", "text/event-stream")
  76. c.Header("Cache-Control", "no-cache")
  77. c.Header("Connection", "keep-alive")
  78. c.Header("X-Accel-Buffering", "no")
  79. // Pass through other headers
  80. if !stageFirstOutput && resp.Header.Get("x-request-id") != "" {
  81. v := resp.Header.Get("x-request-id")
  82. c.Header("x-request-id", v)
  83. }
  84. applyAttemptResponseHeaders := func() {
  85. if !stageFirstOutput || len(attemptResponseHeaders) == 0 || c.Writer.Written() {
  86. return
  87. }
  88. for key, values := range attemptResponseHeaders {
  89. for _, value := range values {
  90. c.Writer.Header().Add(key, value)
  91. }
  92. }
  93. // 暂存头此刻才真正写给客户端:turn-state 溯源在这里记录(见
  94. // noteStagedOpenAICodexTurnStateCommitted 的 failover 说明)。
  95. s.noteStagedOpenAICodexTurnStateCommitted(c, account, attemptResponseHeaders)
  96. // These headers describe this gateway's SSE stream and are stable across
  97. // account attempts. Keep them authoritative over upstream values.
  98. c.Header("Content-Type", "text/event-stream")
  99. c.Header("Cache-Control", "no-cache")
  100. c.Header("Connection", "keep-alive")
  101. c.Header("X-Accel-Buffering", "no")
  102. }
  103. w := c.Writer
  104. flusher, ok := w.(http.Flusher)
  105. if !ok {
  106. return nil, errors.New("streaming not supported")
  107. }
  108. maxLineSize := defaultMaxLineSize
  109. if s.cfg != nil && s.cfg.Gateway.MaxLineSize > 0 {
  110. maxLineSize = s.cfg.Gateway.MaxLineSize
  111. }
  112. var firstTokenMs *int
  113. ttftMode := s.openAITTFTMode(ctx)
  114. firstOutputProgressObserved := false
  115. bufferedWriter := bufio.NewWriterSize(w, 4*1024)
  116. var firstOutputStage *openAIFirstOutputStage
  117. if stageFirstOutput {
  118. firstOutputStage = newDefaultOpenAIFirstOutputStage()
  119. defer func() {
  120. if err := firstOutputStage.Close(); err != nil {
  121. logger.LegacyPrintf("service.openai_gateway", "OpenAI first-output staging cleanup failed: account=%d model=%s error=%v", account.ID, originalModel, err)
  122. }
  123. }()
  124. }
  125. writePendingString := func(value string) (int, error) {
  126. if firstOutputStage != nil && !firstOutputStage.closed {
  127. return firstOutputStage.WriteString(value)
  128. }
  129. return bufferedWriter.WriteString(value)
  130. }
  131. pendingBytes := func() int64 {
  132. if firstOutputStage != nil && !firstOutputStage.closed {
  133. return firstOutputStage.Buffered()
  134. }
  135. return int64(bufferedWriter.Buffered())
  136. }
  137. flushBuffered := func() error {
  138. if firstOutputStage != nil && !firstOutputStage.closed {
  139. if err := firstOutputStage.CommitTo(w); err != nil {
  140. return err
  141. }
  142. } else {
  143. if err := bufferedWriter.Flush(); err != nil {
  144. return err
  145. }
  146. }
  147. flusher.Flush()
  148. return nil
  149. }
  150. usage := &OpenAIUsage{}
  151. imageCounter := newOpenAIImageOutputCounter()
  152. responseID := ""
  153. var firstOutputScanGuard atomic.Bool
  154. firstOutputScanGuard.Store(stageFirstOutput)
  155. scanner := bufio.NewScanner(resp.Body)
  156. scanBuf := getSSEScannerBuf64K()
  157. scanner.Buffer(scanBuf[:0], maxLineSize)
  158. if stageFirstOutput {
  159. scanner.Split(openAIFirstOutputDynamicScanLines(&firstOutputScanGuard))
  160. }
  161. documentScanner := newOpenAISSEJSONDocumentScanner(scanner)
  162. streamInterval := time.Duration(0)
  163. if s.cfg != nil && s.cfg.Gateway.StreamDataIntervalTimeout > 0 {
  164. streamInterval = time.Duration(s.cfg.Gateway.StreamDataIntervalTimeout) * time.Second
  165. }
  166. // Grok: always enforce an upstream-read idle so hung SSE bodies fail over
  167. // instead of holding the OAuth slot until the client cancels. Prefer the
  168. // global gateway setting when set; otherwise apply a Grok-only default.
  169. if account != nil && account.Platform == PlatformGrok {
  170. cfgSec := 0
  171. if s.cfg != nil {
  172. cfgSec = s.cfg.Gateway.StreamDataIntervalTimeout
  173. }
  174. streamInterval = resolveGrokStreamIdleTimeout(cfgSec)
  175. }
  176. // 仅监控上游数据间隔超时,不被下游写入阻塞影响
  177. var intervalTicker *time.Ticker
  178. if streamInterval > 0 {
  179. intervalTicker = time.NewTicker(streamInterval)
  180. defer intervalTicker.Stop()
  181. }
  182. var intervalCh <-chan time.Time
  183. if intervalTicker != nil {
  184. intervalCh = intervalTicker.C
  185. }
  186. keepaliveInterval := time.Duration(0)
  187. if s.cfg != nil && s.cfg.Gateway.StreamKeepaliveInterval > 0 {
  188. keepaliveInterval = time.Duration(s.cfg.Gateway.StreamKeepaliveInterval) * time.Second
  189. }
  190. // 下游 keepalive 仅用于防止代理空闲断开
  191. var keepaliveTicker *time.Ticker
  192. if keepaliveInterval > 0 {
  193. keepaliveTicker = time.NewTicker(keepaliveInterval)
  194. defer keepaliveTicker.Stop()
  195. }
  196. var keepaliveCh <-chan time.Time
  197. if keepaliveTicker != nil {
  198. keepaliveCh = keepaliveTicker.C
  199. }
  200. var firstOutputTimer *time.Timer
  201. var firstOutputCh <-chan time.Time
  202. if firstOutputTimeout > 0 {
  203. remaining := time.Until(startTime.Add(firstOutputTimeout))
  204. if remaining <= 0 {
  205. remaining = time.Nanosecond
  206. }
  207. firstOutputTimer = time.NewTimer(remaining)
  208. firstOutputCh = firstOutputTimer.C
  209. defer firstOutputTimer.Stop()
  210. }
  211. stopFirstOutputTimer := func() {
  212. if firstOutputTimer == nil {
  213. return
  214. }
  215. if !firstOutputTimer.Stop() {
  216. select {
  217. case <-firstOutputTimer.C:
  218. default:
  219. }
  220. }
  221. firstOutputTimer = nil
  222. firstOutputCh = nil
  223. }
  224. // Track downstream writes separately from upstream reads: pre-output failover
  225. // can buffer response.created / response.in_progress, so keepalive must be
  226. // based on downstream idle time.
  227. lastDownstreamWriteAt := time.Now()
  228. // 仅发送一次错误事件,避免多次写入导致协议混乱。
  229. // 注意:OpenAI `/v1/responses` streaming 事件必须符合 OpenAI Responses schema;
  230. // 否则下游 SDK(例如 OpenCode)会因为类型校验失败而报错。
  231. errorEventSent := false
  232. clientDisconnected := false // 客户端断开后继续 drain 上游以收集 usage
  233. sawTerminalEvent := false
  234. sawFailedEvent := false
  235. sawBareError := false
  236. sawResponseFailed := false
  237. terminalEventType := ""
  238. responsesSemanticOutputSeen := false
  239. capacityFailoverSuppressedLogged := false
  240. failedMessage := ""
  241. clientOutputStarted := false
  242. codexFailureTerminal := account != nil && account.IsOpenAIOAuthLike()
  243. upstreamRequestID := strings.TrimSpace(resp.Header.Get("x-request-id"))
  244. var streamEarlyErr error
  245. terminalFailurePending := false
  246. failureDelivered := false
  247. suppressCurrentEvent := false
  248. var bareErrorPayload []byte
  249. bareErrorAccountSideEffectsPending := false
  250. pendingSSEEventType := ""
  251. eventInProgress := false
  252. eventStartsClientOutput := false
  253. eventStartsTTFTOutput := false
  254. eventShouldFlush := false
  255. handlePendingWriteError := func(err error) {
  256. if firstOutputStage != nil && !firstOutputStage.closed {
  257. message := "OpenAI first-output staging failed"
  258. if errors.Is(err, errOpenAIFirstOutputStageLimit) {
  259. message = "OpenAI first-output staging limit exceeded"
  260. }
  261. logger.LegacyPrintf("service.openai_gateway", "%s: account=%d model=%s error=%v", message, account.ID, originalModel, err)
  262. failoverErr := s.newOpenAIStreamFailoverError(c, account, false, upstreamRequestID, nil, message)
  263. failoverErr.SafeToFailoverAfterWrite = true
  264. streamEarlyErr = failoverErr
  265. _ = resp.Body.Close()
  266. return
  267. }
  268. clientDisconnected = true
  269. logger.LegacyPrintf("service.openai_gateway", "Client disconnected during streaming, continuing to drain upstream for billing")
  270. }
  271. completeGuardedEvent := func(queueDrained bool) {
  272. completedProgressEvent := eventStartsClientOutput
  273. completedTTFTEvent := eventStartsTTFTOutput
  274. shouldFlush := eventShouldFlush || (queueDrained && clientOutputStarted)
  275. eventInProgress = false
  276. if !clientDisconnected {
  277. if completedProgressEvent {
  278. applyAttemptResponseHeaders()
  279. }
  280. if shouldFlush {
  281. if err := flushBuffered(); err != nil {
  282. clientDisconnected = true
  283. logger.LegacyPrintf("service.openai_gateway", "Client disconnected during streaming flush, continuing to drain upstream for billing")
  284. } else {
  285. clientOutputStarted = true
  286. lastDownstreamWriteAt = time.Now()
  287. }
  288. }
  289. }
  290. if completedProgressEvent && !firstOutputProgressObserved {
  291. firstOutputScanGuard.Store(false)
  292. firstOutputProgressObserved = true
  293. stopFirstOutputTimer()
  294. }
  295. if completedTTFTEvent && firstTokenMs == nil {
  296. ms := int(time.Since(startTime).Milliseconds())
  297. firstTokenMs = &ms
  298. }
  299. eventStartsClientOutput = false
  300. eventStartsTTFTOutput = false
  301. eventShouldFlush = false
  302. }
  303. sendErrorEvent := func(reason string) {
  304. if errorEventSent || clientDisconnected || failureDelivered {
  305. return
  306. }
  307. errorEventSent = true
  308. payload := `{"type":"error","sequence_number":0,"error":{"type":"upstream_error","message":` + strconv.Quote(reason) + `,"code":` + strconv.Quote(reason) + `}}`
  309. if err := flushBuffered(); err != nil {
  310. clientDisconnected = true
  311. return
  312. }
  313. if _, err := writePendingString("data: " + payload + "\n\n"); err != nil {
  314. clientDisconnected = true
  315. return
  316. }
  317. if err := flushBuffered(); err != nil {
  318. clientDisconnected = true
  319. return
  320. }
  321. clientOutputStarted = true
  322. lastDownstreamWriteAt = time.Now()
  323. }
  324. needModelReplace := originalModel != mappedModel
  325. streamOutputAccumulator := apicompat.NewBufferedResponseAccumulator()
  326. streamDoneItems := newResponsesStreamOutputItems()
  327. streamImageOutputs := make([]json.RawMessage, 0, 1)
  328. streamSeenImages := make(map[string]struct{})
  329. searchCounter := 0
  330. // Dedup search tool calls across SSE events (item.done + response.completed
  331. // both list the same call_id — counting both would ~2× the surcharge).
  332. streamSearchSeen := make(map[string]struct{})
  333. resultWithUsage := func() *openaiStreamingResult {
  334. return &openaiStreamingResult{
  335. usage: usage,
  336. firstTokenMs: firstTokenMs,
  337. responseID: responseID,
  338. imageCount: imageCounter.Count(),
  339. imageOutputSizes: imageCounter.Sizes(),
  340. searchCount: searchCounter,
  341. }
  342. }
  343. flushPending := func(disconnectMessage string) {
  344. if clientDisconnected || pendingBytes() == 0 {
  345. return
  346. }
  347. if err := flushBuffered(); err != nil {
  348. clientDisconnected = true
  349. logger.LegacyPrintf("service.openai_gateway", "%s", disconnectMessage)
  350. return
  351. }
  352. clientOutputStarted = true
  353. lastDownstreamWriteAt = time.Now()
  354. }
  355. finalizeStream := func() (*openaiStreamingResult, error) {
  356. if stageFirstOutput && eventInProgress {
  357. // EOF dispatches the final SSE event even without a trailing blank line.
  358. completeGuardedEvent(true)
  359. }
  360. if codexFailureTerminal && sawBareError && !sawResponseFailed && bareErrorAccountSideEffectsPending {
  361. s.handleOpenAIStreamTerminalAccountSideEffects(c, account, bareErrorPayload, failedMessage, resp.Header, mappedModel)
  362. bareErrorAccountSideEffectsPending = false
  363. }
  364. if codexFailureTerminal && sawBareError && !sawResponseFailed && !clientDisconnected {
  365. applyAttemptResponseHeaders()
  366. if _, err := writePendingString(buildOpenAIResponseFailedSSE(responseID, originalModel, bareErrorPayload, failedMessage)); err != nil {
  367. handlePendingWriteError(err)
  368. } else {
  369. failureDelivered = true
  370. }
  371. }
  372. if sawTerminalEvent && !sawFailedEvent {
  373. s.clearOpenAIProxyStreamDisconnect(account)
  374. }
  375. if !sawTerminalEvent && !openAIStreamClientOutputStarted(c, clientOutputStarted) && !eventShouldFlush {
  376. return resultWithUsage(), s.newOpenAIStreamFailoverError(
  377. c,
  378. account,
  379. false,
  380. upstreamRequestID,
  381. nil,
  382. "OpenAI stream ended before a terminal event",
  383. )
  384. }
  385. flushPending("Client disconnected during final flush, returning collected usage")
  386. if !sawTerminalEvent {
  387. if openAIStreamClientOutputStarted(c, clientOutputStarted) && !clientDisconnected {
  388. s.recordOpenAIProxyStreamDisconnect(account, errors.New("stream ended before terminal event"), upstreamRequestID)
  389. }
  390. return resultWithUsage(), fmt.Errorf("stream usage incomplete: missing terminal event")
  391. }
  392. if sawFailedEvent {
  393. return resultWithUsage(), fmt.Errorf("upstream response failed: %s", failedMessage)
  394. }
  395. logOpenAISuccessMissingUsage(ctx, c, account, resp, usage, terminalEventType, clientDisconnected)
  396. return resultWithUsage(), nil
  397. }
  398. handleScanErr := func(scanErr error) (*openaiStreamingResult, error, bool) {
  399. if scanErr == nil {
  400. return nil, nil, false
  401. }
  402. if errors.Is(scanErr, errOpenAIFirstOutputScannerLimit) && !firstOutputProgressObserved {
  403. logger.LegacyPrintf("service.openai_gateway", "SSE token exceeded guarded first-output limit: account=%d limit=%d error=%v", account.ID, openAIFirstOutputStageMaxBytes+openAIFirstOutputScannerFramingAllowance, scanErr)
  404. failoverErr := s.newOpenAIStreamFailoverError(
  405. c, account, false, upstreamRequestID, nil,
  406. "OpenAI SSE line exceeds guarded first-output limit",
  407. )
  408. failoverErr.SafeToFailoverAfterWrite = true
  409. return resultWithUsage(), failoverErr, true
  410. }
  411. if errors.Is(scanErr, bufio.ErrTooLong) && stageFirstOutput && !firstOutputProgressObserved {
  412. logger.LegacyPrintf("service.openai_gateway", "SSE line too long before first output: account=%d max_size=%d error=%v", account.ID, maxLineSize, scanErr)
  413. failoverErr := s.newOpenAIStreamFailoverError(
  414. c, account, false, upstreamRequestID, nil,
  415. "OpenAI SSE line exceeds guarded first-output limit",
  416. )
  417. failoverErr.SafeToFailoverAfterWrite = true
  418. return resultWithUsage(), failoverErr, true
  419. }
  420. if sawTerminalEvent {
  421. if !sawFailedEvent {
  422. s.clearOpenAIProxyStreamDisconnect(account)
  423. logger.LegacyPrintf("service.openai_gateway", "Upstream scan ended after terminal event: %v", scanErr)
  424. }
  425. result, err := finalizeStream()
  426. return result, err, true
  427. }
  428. // 客户端断开/取消请求时,上游读取往往会返回 context canceled。
  429. // /v1/responses 的 SSE 事件必须符合 OpenAI 协议;这里不注入自定义 error event,避免下游 SDK 解析失败。
  430. if errors.Is(scanErr, context.Canceled) || errors.Is(scanErr, context.DeadlineExceeded) {
  431. if eventShouldFlush {
  432. flushPending("Client disconnected during canceled stream flush, returning collected usage")
  433. }
  434. return resultWithUsage(), fmt.Errorf("stream usage incomplete: %w", scanErr), true
  435. }
  436. if errors.Is(scanErr, bufio.ErrTooLong) {
  437. logger.LegacyPrintf("service.openai_gateway", "SSE line too long: account=%d max_size=%d error=%v", account.ID, maxLineSize, scanErr)
  438. sendErrorEvent("response_too_large")
  439. return resultWithUsage(), scanErr, true
  440. }
  441. if !openAIStreamClientOutputStarted(c, clientOutputStarted) && !eventShouldFlush {
  442. msg := "OpenAI stream disconnected before completion"
  443. if errText := strings.TrimSpace(scanErr.Error()); errText != "" {
  444. msg += ": " + errText
  445. }
  446. return resultWithUsage(), s.newOpenAIStreamFailoverError(c, account, false, upstreamRequestID, nil, msg), true
  447. }
  448. // 客户端已断开时,上游出错仅影响体验,不影响计费;返回已收集 usage
  449. if clientDisconnected {
  450. return resultWithUsage(), fmt.Errorf("stream usage incomplete after disconnect: %w", scanErr), true
  451. }
  452. s.recordOpenAIProxyStreamDisconnect(account, scanErr, upstreamRequestID)
  453. sendErrorEvent("stream_read_error")
  454. return resultWithUsage(), fmt.Errorf("stream read error: %w", scanErr), true
  455. }
  456. processSSELine := func(line string, queueDrained bool) {
  457. if streamEarlyErr != nil {
  458. return
  459. }
  460. if eventType, ok := extractOpenAISSEEventLine(line); ok {
  461. pendingSSEEventType = eventType
  462. eventType = strings.TrimSpace(eventType)
  463. suppressCurrentEvent = codexFailureTerminal && (eventType == "error" || (sawBareError && !sawResponseFailed && eventType != "response.failed"))
  464. }
  465. // Extract data from SSE line (supports both "data: " and "data:" formats)
  466. if data, ok := extractOpenAISSEDataLine(line); ok {
  467. dataBytes := []byte(data)
  468. eventType := effectiveOpenAISSEEventType(dataBytes, pendingSSEEventType)
  469. if codexFailureTerminal && sawBareError && !sawResponseFailed &&
  470. (eventType == "response.completed" || eventType == "response.done") {
  471. // A later successful terminal is authoritative over a pending bare
  472. // error. Keep its usage and terminal visible to the client.
  473. sawBareError = false
  474. sawFailedEvent = false
  475. terminalFailurePending = false
  476. suppressCurrentEvent = false
  477. bareErrorPayload = nil
  478. bareErrorAccountSideEffectsPending = false
  479. failedMessage = ""
  480. }
  481. if codexFailureTerminal && sawBareError && !sawResponseFailed && eventType != "response.failed" {
  482. suppressCurrentEvent = true
  483. }
  484. observer.ObserveOpenAI(dataBytes, eventType)
  485. // 初始上游 data 的 type 只解析一次:原始值保持终止事件的精确匹配,规范化值供后续分支复用。
  486. if openAIStreamEventIsTerminalWithType(data, eventType) {
  487. sawTerminalEvent = true
  488. terminalEventType = eventType
  489. if strings.TrimSpace(data) == "[DONE]" {
  490. terminalEventType = "[DONE]"
  491. }
  492. }
  493. if responseID == "" {
  494. responseID = extractOpenAIResponseIDFromJSONBytes(dataBytes)
  495. }
  496. forceFlushFailedEvent := false
  497. if !capacityFailoverSuppressedLogged && account != nil && account.Platform == PlatformOpenAI &&
  498. (eventType == "error" || eventType == "response.failed") &&
  499. openAIStreamClientOutputStarted(c, clientOutputStarted) &&
  500. isOpenAIUpstreamCapacityShedEvent(dataBytes) {
  501. logOpenAICapacityFailoverSuppressed(ctx, account, "native_sse", upstreamRequestID, eventType)
  502. capacityFailoverSuppressedLogged = true
  503. }
  504. cyberHit := false
  505. if eventType == "response.failed" || eventType == "error" {
  506. if codexFailureTerminal && eventType == "error" {
  507. sawBareError = true
  508. bareErrorPayload = append(bareErrorPayload[:0], dataBytes...)
  509. suppressCurrentEvent = true
  510. } else if codexFailureTerminal && eventType == "response.failed" {
  511. sawResponseFailed = true
  512. }
  513. failedMessage = extractOpenAISSEErrorMessage(dataBytes)
  514. if failedMessage == "" {
  515. failedMessage = "Upstream response failed"
  516. }
  517. // response.failed 自带上游已消耗的 usage(input token 通常已扣);必须先解析
  518. // 再打 cyber 标记,否则 mark 记到的是解析前的 0,导致流式 cyber 按 0 token 计费
  519. // 而漏记真实用量。对齐 WS V2 / Chat 流式路径(均先解析 usage 再 Mark)。
  520. s.parseSSEUsageBytesWithType(dataBytes, eventType, usage)
  521. if hit, code, msg := detectOpenAICyberPolicy(dataBytes); hit {
  522. cyberHit = true
  523. MarkOpsCyberPolicy(c, CyberPolicyMark{
  524. Code: code,
  525. Message: msg,
  526. Body: truncateString(string(dataBytes), 4096),
  527. UpstreamStatus: http.StatusOK,
  528. UpstreamInTok: usage.InputTokens,
  529. UpstreamOutTok: usage.OutputTokens,
  530. })
  531. }
  532. outputStarted := openAIStreamClientOutputStarted(c, clientOutputStarted)
  533. if !outputStarted && !cyberHit {
  534. if compactErr := newOpenAICompactFallbackSignal(c, dataBytes, failedMessage); compactErr != nil {
  535. sawFailedEvent = true
  536. streamEarlyErr = compactErr
  537. return
  538. }
  539. }
  540. if outputStarted && !cyberHit {
  541. if codexFailureTerminal && eventType == "error" {
  542. // OpenAI commonly follows a bare error with response.failed.
  543. // Defer account health updates so the pair is applied once.
  544. bareErrorAccountSideEffectsPending = true
  545. } else {
  546. s.handleOpenAIStreamTerminalAccountSideEffects(c, account, dataBytes, failedMessage, resp.Header, mappedModel)
  547. bareErrorAccountSideEffectsPending = false
  548. }
  549. if eventType == "response.failed" {
  550. // Once semantic output is committed, failover replay is unsafe. Keep
  551. // the terminal event on the existing stream, but retain the upstream
  552. // request ID and payload for operations diagnostics.
  553. s.recordOpenAIStreamUpstreamError(c, account, false, upstreamRequestID, "stream_failed", dataBytes, failedMessage)
  554. }
  555. }
  556. if !outputStarted {
  557. shouldFailover := false
  558. if !cyberHit {
  559. if eventType == "error" {
  560. shouldFailover = openAIStreamErrorEventShouldFailover(dataBytes, failedMessage)
  561. } else {
  562. shouldFailover = openAIStreamFailedEventShouldFailover(dataBytes, failedMessage)
  563. }
  564. }
  565. if shouldFailover {
  566. sawFailedEvent = true
  567. streamEarlyErr = s.newOpenAIStreamFailoverErrorWithModel(c, account, false, upstreamRequestID, dataBytes, failedMessage, mappedModel, resp.Header)
  568. return
  569. }
  570. if !cyberHit && !sawBareError {
  571. if status, errType, errMsg, matched := applyOpenAIStreamFailedErrorPassthroughRule(c, account.Platform, dataBytes, failedMessage); matched {
  572. sawFailedEvent = true
  573. // 命中透传规则也要记录 ops 上游错误事件(对齐 CC/Messages 与
  574. // antigravity 先例),否则透传命中的 failed 在监控中不可见。
  575. s.recordOpenAIStreamUpstreamError(c, account, false, upstreamRequestID, "http_error", dataBytes, failedMessage)
  576. MarkResponseCommitted(c)
  577. c.Writer.Header().Set("Content-Type", "application/json; charset=utf-8")
  578. c.JSON(status, gin.H{
  579. "error": gin.H{
  580. "type": errType,
  581. "message": errMsg,
  582. },
  583. })
  584. streamEarlyErr = fmt.Errorf("upstream response failed: passthrough rule matched message=%s", errMsg)
  585. return
  586. }
  587. }
  588. }
  589. forceFlushFailedEvent = true
  590. sawFailedEvent = true
  591. terminalFailurePending = !codexFailureTerminal || eventType == "response.failed"
  592. }
  593. if normalizedData, normalized := normalizeCompletedImageGenerationStatus(dataBytes); normalized {
  594. dataBytes = normalizedData
  595. data = string(normalizedData)
  596. line = "data: " + data
  597. }
  598. imageCounter.AddSSEData(dataBytes)
  599. searchCounter += countGrokNativeSearchCallsInSSEDataDedup(dataBytes, streamSearchSeen)
  600. // Correct Codex tool calls if needed (apply_patch -> edit, etc.)
  601. if correctedData, corrected := s.toolCorrector.CorrectToolCallsInSSEBytes(dataBytes); corrected {
  602. dataBytes = correctedData
  603. data = string(correctedData)
  604. line = "data: " + data
  605. eventType = effectiveOpenAISSEEventType(dataBytes, eventType)
  606. }
  607. if imageOutput, ok := extractImageGenerationOutputFromSSEData(dataBytes, streamSeenImages); ok {
  608. streamImageOutputs = append(streamImageOutputs, imageOutput)
  609. }
  610. streamDoneItems.Observe(dataBytes)
  611. if responsesStreamEventMayContributeToOutput(eventType) {
  612. var streamEvent apicompat.ResponsesStreamEvent
  613. if err := json.Unmarshal(dataBytes, &streamEvent); err == nil {
  614. streamOutputAccumulator.ProcessEvent(&streamEvent)
  615. }
  616. }
  617. if normalizedData, normalized := normalizeResponsesStreamingTerminalOutput(dataBytes, streamOutputAccumulator, streamDoneItems, streamImageOutputs); normalized {
  618. dataBytes = normalizedData
  619. data = string(normalizedData)
  620. line = "data: " + data
  621. eventType = effectiveOpenAISSEEventType(dataBytes, eventType)
  622. }
  623. restoredData, restoreErr := restoreGrokResponsesClientToolPayload(c, dataBytes)
  624. if restoreErr != nil {
  625. streamEarlyErr = fmt.Errorf("restore Grok Responses client tool response: %w", restoreErr)
  626. return
  627. }
  628. restoredData, restoreErr = restoreOpenAIResponsesNamespacePayload(c, restoredData)
  629. if restoreErr != nil {
  630. streamEarlyErr = fmt.Errorf("restore OpenAI namespace response: %w", restoreErr)
  631. return
  632. }
  633. restoredData = restoreCodexToolNamesFromSSEContext(c, restoredData, eventType)
  634. if !bytes.Equal(restoredData, dataBytes) {
  635. dataBytes = restoredData
  636. data = string(restoredData)
  637. line = "data: " + data
  638. eventType = effectiveOpenAISSEEventType(dataBytes, eventType)
  639. }
  640. if sanitizedData, sanitized := sanitizeOpenAIResponseFailedEventForClient(
  641. dataBytes,
  642. eventType,
  643. openAIStreamClientOutputStarted(c, clientOutputStarted),
  644. ); sanitized {
  645. dataBytes = sanitizedData
  646. data = string(sanitizedData)
  647. line = "data: " + data
  648. }
  649. // Replace model in response if needed.
  650. // Fast path: most events do not contain model field values.
  651. if needModelReplace && mappedModel != "" && strings.Contains(line, mappedModel) {
  652. line = s.replaceModelInSSELine(line, mappedModel, originalModel)
  653. }
  654. startsClientOutput := forceFlushFailedEvent || openAIStreamDataStartsClientOutput(data, eventType)
  655. startsVisibleOutput := openAIStreamDataStartsVisibleOutput(data, eventType)
  656. startsTTFTOutput := openAIStreamDataStartsTTFT(data, eventType, forceFlushFailedEvent, ttftMode)
  657. if stageFirstOutput {
  658. eventStartsClientOutput = eventStartsClientOutput || startsClientOutput
  659. eventStartsTTFTOutput = eventStartsTTFTOutput || startsTTFTOutput
  660. if startsClientOutput {
  661. firstOutputScanGuard.Store(false)
  662. }
  663. }
  664. if startsClientOutput && !openAIStreamEventTypeIsTerminal(eventType) {
  665. responsesSemanticOutputSeen = true
  666. }
  667. // OpenAI Responses streams that terminate with an empty
  668. // response.completed (no output, no usage, no error, nothing sent
  669. // to the client) are silent upstream refusals: fail over instead of
  670. // recording a successful 0/0 usage turn (issue #5009).
  671. if account != nil && account.Platform == PlatformOpenAI &&
  672. (eventType == "response.completed" || eventType == "response.done") &&
  673. !sawFailedEvent && !responsesSemanticOutputSeen && !clientOutputStarted &&
  674. openAIResponsesCompletedEventIsEmpty(dataBytes, usage) {
  675. sawTerminalEvent = true
  676. streamEarlyErr = newOpenAIResponsesEmptyCompletedFailoverError(c, account, upstreamRequestID)
  677. return
  678. }
  679. // 写入客户端(客户端断开后继续 drain 上游)
  680. if !clientDisconnected && !failureDelivered && !suppressCurrentEvent {
  681. shouldFlush := queueDrained && (clientOutputStarted || startsClientOutput)
  682. if firstTokenMs == nil && startsVisibleOutput {
  683. // 保证首个 token 事件尽快出站,避免影响 TTFT。
  684. shouldFlush = true
  685. }
  686. eventShouldFlush = eventShouldFlush || shouldFlush
  687. if _, err := writePendingString(line); err != nil {
  688. handlePendingWriteError(err)
  689. } else if _, err := writePendingString("\n"); err != nil {
  690. handlePendingWriteError(err)
  691. } else {
  692. eventInProgress = true
  693. }
  694. }
  695. // Record first token time
  696. if !guardFirstOutput && firstTokenMs == nil && startsTTFTOutput {
  697. ms := int(time.Since(startTime).Milliseconds())
  698. firstTokenMs = &ms
  699. stopFirstOutputTimer()
  700. }
  701. s.parseSSEUsageBytesWithType(dataBytes, eventType, usage)
  702. return
  703. }
  704. // A blank line dispatches a guarded event from the attempt-local stage.
  705. if stageFirstOutput && line == "" {
  706. pendingSSEEventType = ""
  707. if suppressCurrentEvent {
  708. suppressCurrentEvent = false
  709. terminalFailurePending = false
  710. eventInProgress = false
  711. eventStartsClientOutput = false
  712. eventStartsTTFTOutput = false
  713. eventShouldFlush = false
  714. return
  715. }
  716. if failureDelivered {
  717. terminalFailurePending = false
  718. eventInProgress = false
  719. eventStartsClientOutput = false
  720. eventStartsTTFTOutput = false
  721. eventShouldFlush = false
  722. return
  723. }
  724. if !clientDisconnected {
  725. if _, err := writePendingString("\n"); err != nil {
  726. handlePendingWriteError(err)
  727. }
  728. }
  729. if streamEarlyErr == nil {
  730. completeGuardedEvent(queueDrained)
  731. }
  732. if terminalFailurePending && streamEarlyErr == nil {
  733. terminalFailurePending = false
  734. failureDelivered = true
  735. }
  736. return
  737. }
  738. // Non-guarded streams retain upstream's event-boundary flushing: a keepalive
  739. // or queue-drain flush must never split an open SSE event.
  740. shouldFlush := false
  741. if line == "" {
  742. pendingSSEEventType = ""
  743. if suppressCurrentEvent {
  744. suppressCurrentEvent = false
  745. terminalFailurePending = false
  746. eventInProgress = false
  747. eventShouldFlush = false
  748. return
  749. }
  750. shouldFlush = eventShouldFlush || (queueDrained && clientOutputStarted)
  751. eventShouldFlush = false
  752. if failureDelivered {
  753. terminalFailurePending = false
  754. }
  755. }
  756. if !clientDisconnected && !failureDelivered && !suppressCurrentEvent {
  757. if _, err := writePendingString(line); err != nil {
  758. handlePendingWriteError(err)
  759. } else if _, err := writePendingString("\n"); err != nil {
  760. handlePendingWriteError(err)
  761. } else {
  762. eventInProgress = line != ""
  763. if shouldFlush {
  764. if err := flushBuffered(); err != nil {
  765. clientDisconnected = true
  766. logger.LegacyPrintf("service.openai_gateway", "Client disconnected during streaming flush, continuing to drain upstream for billing")
  767. } else {
  768. clientOutputStarted = true
  769. lastDownstreamWriteAt = time.Now()
  770. }
  771. }
  772. }
  773. if line == "" && terminalFailurePending && streamEarlyErr == nil {
  774. terminalFailurePending = false
  775. failureDelivered = true
  776. }
  777. }
  778. }
  779. // 无超时/无 keepalive 的常见路径走同步扫描,减少 goroutine 与 channel 开销。
  780. if streamInterval <= 0 && keepaliveInterval <= 0 && firstOutputTimeout <= 0 {
  781. defer putSSEScannerBuf64K(scanBuf)
  782. for documentScanner.Scan() {
  783. processSSELine(documentScanner.Text(), true)
  784. if streamEarlyErr != nil {
  785. return resultWithUsage(), streamEarlyErr
  786. }
  787. }
  788. if result, err, done := handleScanErr(documentScanner.Err()); done {
  789. return result, err
  790. }
  791. return finalizeStream()
  792. }
  793. type scanEvent struct {
  794. line string
  795. err error
  796. processed chan struct{}
  797. }
  798. // 独立 goroutine 读取上游,避免读取阻塞影响 keepalive/超时处理
  799. // Guard mode permits one queued token plus the token being processed. With
  800. // the guarded scanner cap this bounds scanner/channel retention near 16 MiB;
  801. // the timeout-disabled path preserves the legacy depth of 16.
  802. events := make(chan scanEvent, openAIFirstOutputEventQueueSize(guardFirstOutput))
  803. done := make(chan struct{})
  804. sendEvent := func(ev scanEvent) bool {
  805. if firstOutputScanGuard.Load() {
  806. ev.processed = make(chan struct{})
  807. }
  808. select {
  809. case events <- ev:
  810. case <-done:
  811. return false
  812. }
  813. if ev.processed == nil {
  814. return true
  815. }
  816. select {
  817. case <-ev.processed:
  818. return true
  819. case <-done:
  820. return false
  821. }
  822. }
  823. markEventProcessed := func(ev scanEvent) {
  824. if ev.processed != nil {
  825. close(ev.processed)
  826. }
  827. }
  828. var lastReadAt int64
  829. atomic.StoreInt64(&lastReadAt, time.Now().UnixNano())
  830. go func(scanBuf *sseScannerBuf64K) {
  831. defer putSSEScannerBuf64K(scanBuf)
  832. defer close(events)
  833. for documentScanner.Scan() {
  834. atomic.StoreInt64(&lastReadAt, time.Now().UnixNano())
  835. if !sendEvent(scanEvent{line: documentScanner.Text()}) {
  836. return
  837. }
  838. }
  839. if err := documentScanner.Err(); err != nil {
  840. _ = sendEvent(scanEvent{err: err})
  841. }
  842. }(scanBuf)
  843. defer close(done)
  844. for {
  845. select {
  846. case ev, ok := <-events:
  847. if !ok {
  848. if stageFirstOutput && eventInProgress {
  849. // EOF dispatches the final SSE event even without a trailing blank
  850. // line. Do not synthesize extra bytes on the downstream wire.
  851. completeGuardedEvent(true)
  852. }
  853. return finalizeStream()
  854. }
  855. if result, err, done := handleScanErr(ev.err); done {
  856. markEventProcessed(ev)
  857. return result, err
  858. }
  859. processSSELine(ev.line, len(events) == 0)
  860. markEventProcessed(ev)
  861. if streamEarlyErr != nil {
  862. return resultWithUsage(), streamEarlyErr
  863. }
  864. case <-intervalCh:
  865. if failureDelivered {
  866. return resultWithUsage(), fmt.Errorf("upstream response failed: %s", failedMessage)
  867. }
  868. lastRead := time.Unix(0, atomic.LoadInt64(&lastReadAt))
  869. if time.Since(lastRead) < streamInterval {
  870. continue
  871. }
  872. if codexFailureTerminal && sawBareError && !sawResponseFailed {
  873. _ = resp.Body.Close()
  874. return finalizeStream()
  875. }
  876. if clientDisconnected {
  877. return resultWithUsage(), fmt.Errorf("stream usage incomplete after timeout")
  878. }
  879. logger.LegacyPrintf("service.openai_gateway", "Stream data interval timeout: account=%d model=%s interval=%s", account.ID, originalModel, streamInterval)
  880. // 处理流超时,可能标记账户为临时不可调度或错误状态
  881. if s.rateLimitService != nil {
  882. s.rateLimitService.HandleStreamTimeout(ctx, account, originalModel)
  883. }
  884. // Grok: short cool + account failover when no client-visible bytes
  885. // were committed yet (pre-commit). After output started we keep the
  886. // legacy stream_timeout path so partial SSE is not dual-written.
  887. if account != nil && account.Platform == PlatformGrok {
  888. s.tempUnscheduleGrok(ctx, account, grokStreamIdleCooldown, "grok stream idle timeout")
  889. if !openAIStreamClientOutputStarted(c, clientOutputStarted) && !eventShouldFlush {
  890. _ = resp.Body.Close()
  891. return resultWithUsage(), grokStreamIdleFailoverError(account, streamInterval)
  892. }
  893. }
  894. sendErrorEvent("stream_timeout")
  895. return resultWithUsage(), fmt.Errorf("stream data interval timeout")
  896. case <-firstOutputCh:
  897. if firstOutputProgressObserved {
  898. stopFirstOutputTimer()
  899. continue
  900. }
  901. if codexFailureTerminal && sawBareError && !sawResponseFailed && len(events) == 0 {
  902. _ = resp.Body.Close()
  903. return finalizeStream()
  904. }
  905. _ = resp.Body.Close()
  906. for ev := range events {
  907. markEventProcessed(ev)
  908. }
  909. return resultWithUsage(), s.newOpenAIFirstOutputTimeoutError(
  910. ctx, c, account, opsUpstreamProxyID(account), opsUpstreamProxyName(account),
  911. startTime, originalModel, reasoningEffort,
  912. firstOutputTimeout, "semantic_output", resp.Header,
  913. )
  914. case <-keepaliveCh:
  915. if clientDisconnected || failureDelivered {
  916. continue
  917. }
  918. if eventInProgress {
  919. continue
  920. }
  921. if time.Since(lastDownstreamWriteAt) < keepaliveInterval {
  922. continue
  923. }
  924. if stageFirstOutput {
  925. // Bypass attempt-local buffered frames. The stable SSE headers may be
  926. // committed here, but account headers remain private until semantic output.
  927. n, err := w.Write([]byte(":\n\n"))
  928. recordOpenAIStreamKeepaliveBytes(c, n)
  929. if err != nil {
  930. clientDisconnected = true
  931. logger.LegacyPrintf("service.openai_gateway", "Client disconnected during streaming, continuing to drain upstream for billing")
  932. continue
  933. }
  934. flusher.Flush()
  935. lastDownstreamWriteAt = time.Now()
  936. continue
  937. }
  938. if _, err := writePendingString(":\n\n"); err != nil {
  939. clientDisconnected = true
  940. logger.LegacyPrintf("service.openai_gateway", "Client disconnected during streaming, continuing to drain upstream for billing")
  941. continue
  942. }
  943. if err := flushBuffered(); err != nil {
  944. clientDisconnected = true
  945. logger.LegacyPrintf("service.openai_gateway", "Client disconnected during keepalive flush, continuing to drain upstream for billing")
  946. } else {
  947. lastDownstreamWriteAt = time.Now()
  948. }
  949. }
  950. }
  951. }
  952. // extractOpenAISSEDataLine 低开销提取 SSE `data:` 行内容。
  953. // 兼容 `data: xxx` 与 `data:xxx` 两种格式。
  954. func extractOpenAISSEDataLine(line string) (string, bool) {
  955. if !strings.HasPrefix(line, "data:") {
  956. return "", false
  957. }
  958. start := len("data:")
  959. for start < len(line) {
  960. if line[start] != ' ' && line[start] != ' ' {
  961. break
  962. }
  963. start++
  964. }
  965. return line[start:], true
  966. }
  967. func extractOpenAISSEEventLine(line string) (string, bool) {
  968. if !strings.HasPrefix(line, "event:") {
  969. return "", false
  970. }
  971. start := len("event:")
  972. for start < len(line) {
  973. if line[start] != ' ' && line[start] != ' ' {
  974. break
  975. }
  976. start++
  977. }
  978. return strings.TrimSpace(line[start:]), true
  979. }
  980. type openAICompatSSEFrame struct {
  981. EventType string
  982. Data string
  983. }
  984. type openAICompatSSEFrameParser struct {
  985. eventType string
  986. dataLines []string
  987. }
  988. func (p *openAICompatSSEFrameParser) AddLine(line string) (openAICompatSSEFrame, bool) {
  989. if line == "" {
  990. return p.dispatch()
  991. }
  992. if strings.HasPrefix(line, ":") {
  993. return openAICompatSSEFrame{}, false
  994. }
  995. if eventType, ok := extractOpenAISSEEventLine(line); ok {
  996. p.eventType = eventType
  997. return openAICompatSSEFrame{}, false
  998. }
  999. if data, ok := extractOpenAISSEDataLine(line); ok {
  1000. p.dataLines = append(p.dataLines, data)
  1001. }
  1002. return openAICompatSSEFrame{}, false
  1003. }
  1004. func (p *openAICompatSSEFrameParser) Finish() (openAICompatSSEFrame, bool) {
  1005. return p.dispatch()
  1006. }
  1007. func (p *openAICompatSSEFrameParser) dispatch() (openAICompatSSEFrame, bool) {
  1008. frame := openAICompatSSEFrame{
  1009. EventType: p.eventType,
  1010. Data: strings.Join(p.dataLines, "\n"),
  1011. }
  1012. p.eventType = ""
  1013. p.dataLines = nil
  1014. return frame, frame.Data != ""
  1015. }
  1016. func openAICompatPayloadWithEventType(payload, eventType string) string {
  1017. eventType = strings.TrimSpace(eventType)
  1018. if eventType == "" || strings.TrimSpace(payload) == "" || strings.TrimSpace(payload) == "[DONE]" {
  1019. return payload
  1020. }
  1021. if strings.TrimSpace(gjson.Get(payload, "type").String()) != "" {
  1022. return payload
  1023. }
  1024. patched, err := sjson.Set(payload, "type", eventType)
  1025. if err != nil {
  1026. return payload
  1027. }
  1028. return patched
  1029. }
  1030. func effectiveOpenAISSEEventType(payload []byte, eventType string) string {
  1031. if payloadType := strings.TrimSpace(gjson.GetBytes(payload, "type").String()); payloadType != "" {
  1032. return payloadType
  1033. }
  1034. return strings.TrimSpace(eventType)
  1035. }
  1036. func (s *OpenAIGatewayService) replaceModelInSSELine(line, fromModel, toModel string) string {
  1037. data, ok := extractOpenAISSEDataLine(line)
  1038. if !ok {
  1039. return line
  1040. }
  1041. if data == "" || data == "[DONE]" {
  1042. return line
  1043. }
  1044. // 使用 gjson 精确检查 model 字段,避免全量 JSON 反序列化
  1045. if m := gjson.Get(data, "model"); m.Exists() && m.Str == fromModel {
  1046. newData, err := sjson.Set(data, "model", toModel)
  1047. if err != nil {
  1048. return line
  1049. }
  1050. return "data: " + newData
  1051. }
  1052. // 检查嵌套的 response.model 字段
  1053. if m := gjson.Get(data, "response.model"); m.Exists() && m.Str == fromModel {
  1054. newData, err := sjson.Set(data, "response.model", toModel)
  1055. if err != nil {
  1056. return line
  1057. }
  1058. return "data: " + newData
  1059. }
  1060. return line
  1061. }
  1062. // correctToolCallsInResponseBody 修正响应体中的工具调用
  1063. func (s *OpenAIGatewayService) correctToolCallsInResponseBody(body []byte) []byte {
  1064. if len(body) == 0 {
  1065. return body
  1066. }
  1067. updated := body
  1068. if s != nil && s.toolCorrector != nil {
  1069. if corrected, changed := s.toolCorrector.CorrectToolCallsInSSEBytes(updated); changed {
  1070. updated = corrected
  1071. }
  1072. }
  1073. if normalized, changed := normalizeOpenAIResponsesFunctionCallArguments(updated); changed {
  1074. updated = normalized
  1075. }
  1076. return updated
  1077. }
  1078. func normalizeOpenAIResponsesFunctionCallArguments(data []byte) ([]byte, bool) {
  1079. if len(bytes.TrimSpace(data)) == 0 || !bytes.Contains(data, []byte(`"arguments"`)) {
  1080. return data, false
  1081. }
  1082. if !gjson.ValidBytes(data) {
  1083. return data, false
  1084. }
  1085. updated := data
  1086. changed := false
  1087. setDedupedArgument := func(path string) {
  1088. arg := gjson.GetBytes(updated, path)
  1089. if !arg.Exists() || arg.Type != gjson.String {
  1090. return
  1091. }
  1092. deduped, ok := dedupeRepeatedJSONArgumentString(arg.Str)
  1093. if !ok {
  1094. return
  1095. }
  1096. next, err := sjson.SetBytes(updated, path, deduped)
  1097. if err != nil {
  1098. return
  1099. }
  1100. updated = next
  1101. changed = true
  1102. }
  1103. eventType := strings.TrimSpace(gjson.GetBytes(updated, "type").String())
  1104. if eventType == "response.function_call_arguments.done" {
  1105. setDedupedArgument("arguments")
  1106. }
  1107. if itemType := strings.TrimSpace(gjson.GetBytes(updated, "item.type").String()); isResponsesFunctionCallItemType(itemType) {
  1108. setDedupedArgument("item.arguments")
  1109. }
  1110. dedupeResponsesFunctionCallOutputArguments(updated, "response.output", setDedupedArgument)
  1111. dedupeResponsesFunctionCallOutputArguments(updated, "output", setDedupedArgument)
  1112. return updated, changed
  1113. }
  1114. func dedupeResponsesFunctionCallOutputArguments(data []byte, outputPath string, setDedupedArgument func(string)) {
  1115. output := gjson.GetBytes(data, outputPath)
  1116. if !output.Exists() || !output.IsArray() {
  1117. return
  1118. }
  1119. for i, item := range output.Array() {
  1120. if !isResponsesFunctionCallItemType(strings.TrimSpace(item.Get("type").String())) {
  1121. continue
  1122. }
  1123. setDedupedArgument(outputPath + "." + strconv.Itoa(i) + ".arguments")
  1124. }
  1125. }
  1126. func isResponsesFunctionCallItemType(itemType string) bool {
  1127. return itemType == "function_call" || itemType == "custom_tool_call"
  1128. }
  1129. func dedupeRepeatedJSONArgumentString(arguments string) (string, bool) {
  1130. if len(arguments) == 0 || len(arguments)%2 != 0 {
  1131. return "", false
  1132. }
  1133. halfLen := len(arguments) / 2
  1134. first := arguments[:halfLen]
  1135. if first != arguments[halfLen:] {
  1136. return "", false
  1137. }
  1138. trimmed := strings.TrimSpace(first)
  1139. if trimmed == "" || (!strings.HasPrefix(trimmed, "{") && !strings.HasPrefix(trimmed, "[")) {
  1140. return "", false
  1141. }
  1142. if !json.Valid([]byte(first)) {
  1143. return "", false
  1144. }
  1145. return first, true
  1146. }
  1147. func (s *OpenAIGatewayService) parseSSEUsage(data string, usage *OpenAIUsage) {
  1148. s.parseSSEUsageBytes([]byte(data), usage)
  1149. }
  1150. func (s *OpenAIGatewayService) parseSSEUsageBytes(data []byte, usage *OpenAIUsage) {
  1151. s.parseSSEUsageBytesWithType(data, "", usage)
  1152. }
  1153. func (s *OpenAIGatewayService) parseSSEUsageBytesWithType(data []byte, eventType string, usage *OpenAIUsage) {
  1154. if usage == nil || len(data) == 0 || bytes.Equal(bytes.TrimSpace(data), []byte("[DONE]")) {
  1155. return
  1156. }
  1157. // Usage is absent from nearly every delta event. Avoid full JSON validation
  1158. // and four gjson path scans on that hot path while retaining progressive
  1159. // usage from compatible upstreams on any event type.
  1160. if !bytes.Contains(data, []byte(`"usage"`)) {
  1161. return
  1162. }
  1163. parsedUsage, ok := extractOpenAIUsageFromJSONBytes(data)
  1164. if !ok {
  1165. return
  1166. }
  1167. if openAIStreamEventTypeIsTerminal(effectiveOpenAISSEEventType(data, eventType)) {
  1168. if !openAIUsageHasTokens(&parsedUsage) && openAIUsageHasTokens(usage) {
  1169. return
  1170. }
  1171. *usage = parsedUsage
  1172. return
  1173. }
  1174. mergeOpenAIUsageNonZero(usage, parsedUsage)
  1175. }
  1176. // Compatible Responses upstreams may report usage before the terminal event.
  1177. // Retain those non-zero fields as a fallback. A terminal usage with any billed
  1178. // tokens is authoritative as a whole; an all-zero terminal does not erase a
  1179. // non-zero progressive observation from the same turn.
  1180. func mergeOpenAIUsageNonZero(dst *OpenAIUsage, src OpenAIUsage) {
  1181. if dst == nil {
  1182. return
  1183. }
  1184. if src.InputTokens > 0 {
  1185. dst.InputTokens = src.InputTokens
  1186. }
  1187. if src.ImageInputTokens > 0 {
  1188. dst.ImageInputTokens = src.ImageInputTokens
  1189. }
  1190. if src.OutputTokens > 0 {
  1191. dst.OutputTokens = src.OutputTokens
  1192. }
  1193. if src.CacheCreationInputTokens > 0 {
  1194. dst.CacheCreationInputTokens = src.CacheCreationInputTokens
  1195. }
  1196. if src.CacheReadInputTokens > 0 {
  1197. dst.CacheReadInputTokens = src.CacheReadInputTokens
  1198. }
  1199. if src.ImageOutputTokens > 0 {
  1200. dst.ImageOutputTokens = src.ImageOutputTokens
  1201. }
  1202. }
  1203. func openAIUsageHasTokens(usage *OpenAIUsage) bool {
  1204. return usage != nil && (usage.InputTokens > 0 || usage.ImageInputTokens > 0 ||
  1205. usage.OutputTokens > 0 || usage.CacheCreationInputTokens > 0 ||
  1206. usage.CacheReadInputTokens > 0 || usage.ImageOutputTokens > 0)
  1207. }
  1208. const openAIMissingUsageLogInterval = time.Minute
  1209. type openAIMissingUsageLogSampler struct {
  1210. total atomic.Uint64
  1211. suppressed atomic.Uint64
  1212. lastLog atomic.Int64
  1213. }
  1214. var openAIMissingUsageSampler openAIMissingUsageLogSampler
  1215. func (s *openAIMissingUsageLogSampler) sample(now time.Time) (logNow bool, total uint64, suppressed uint64) {
  1216. total = s.total.Add(1)
  1217. nowNanos := now.UnixNano()
  1218. for {
  1219. last := s.lastLog.Load()
  1220. if last != 0 && nowNanos-last < int64(openAIMissingUsageLogInterval) {
  1221. s.suppressed.Add(1)
  1222. return false, total, 0
  1223. }
  1224. if s.lastLog.CompareAndSwap(last, nowNanos) {
  1225. return true, total, s.suppressed.Swap(0)
  1226. }
  1227. }
  1228. }
  1229. func logOpenAISuccessMissingUsage(ctx context.Context, c *gin.Context, account *Account, resp *http.Response, usage *OpenAIUsage, terminalEvent string, clientDisconnected bool) {
  1230. if resp == nil || resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices || openAIUsageHasTokens(usage) {
  1231. return
  1232. }
  1233. terminalEvent = strings.TrimSpace(terminalEvent)
  1234. if terminalEvent != "response.completed" && terminalEvent != "response.done" && terminalEvent != "[DONE]" && terminalEvent != "json" {
  1235. return
  1236. }
  1237. logNow, total, suppressed := openAIMissingUsageSampler.sample(time.Now())
  1238. if !logNow {
  1239. return
  1240. }
  1241. accountID := int64(0)
  1242. accountType := ""
  1243. if account != nil {
  1244. accountID = account.ID
  1245. accountType = string(account.Type)
  1246. }
  1247. inboundEndpoint := ""
  1248. if c != nil && c.Request != nil && c.Request.URL != nil {
  1249. inboundEndpoint = c.Request.URL.Path
  1250. }
  1251. logger.FromContext(ctx).With(
  1252. zap.String("component", "service.openai_gateway"),
  1253. zap.Int64("account_id", accountID),
  1254. zap.String("account_type", accountType),
  1255. zap.String("inbound_endpoint", inboundEndpoint),
  1256. zap.String("upstream_request_id", strings.TrimSpace(resp.Header.Get("x-request-id"))),
  1257. zap.Int("upstream_status_code", resp.StatusCode),
  1258. zap.String("terminal_event", terminalEvent),
  1259. zap.Bool("client_disconnected", clientDisconnected),
  1260. zap.Uint64("missing_usage_total", total),
  1261. zap.Uint64("suppressed_since_last", suppressed),
  1262. ).Warn("openai_usage.success_missing_usage")
  1263. }
  1264. func extractOpenAIUsageFromJSONBytes(body []byte) (OpenAIUsage, bool) {
  1265. if len(body) == 0 || !gjson.ValidBytes(body) {
  1266. return OpenAIUsage{}, false
  1267. }
  1268. // 部分 OpenAI 兼容上游(例如 Cline API)会将标准响应包在 data 字段中:
  1269. // {"data":{"choices": [...], "usage": {...}}, "success":true}。
  1270. // 按优先级先保留原有路径,再尝试兼容层 data 包装,
  1271. // 避免同步请求能正常返回但用量被静默记录为 0。
  1272. candidates := []struct {
  1273. usagePath string
  1274. imageUsagePath string
  1275. }{
  1276. {usagePath: "usage", imageUsagePath: "tool_usage.image_gen"},
  1277. {usagePath: "response.usage", imageUsagePath: "response.tool_usage.image_gen"},
  1278. {usagePath: "data.usage", imageUsagePath: "data.tool_usage.image_gen"},
  1279. {usagePath: "data.response.usage", imageUsagePath: "data.response.tool_usage.image_gen"},
  1280. }
  1281. for _, candidate := range candidates {
  1282. if usage, ok := openAIUsageFromGJSON(gjson.GetBytes(body, candidate.usagePath)); ok {
  1283. mergeHostedImageGenToolUsage(gjson.GetBytes(body, candidate.imageUsagePath), &usage)
  1284. return usage, true
  1285. }
  1286. }
  1287. return OpenAIUsage{}, false
  1288. }
  1289. // openAIResponsesCompletedEventIsEmpty reports whether a response.completed /
  1290. // response.done SSE payload carries no usage, no error and no output items.
  1291. // The accumulated usage is consulted too, because OpenAI may deliver usage on
  1292. // an earlier event. An empty terminal event after a stream with no semantic
  1293. // output is treated as a silent upstream refusal (issue #5009).
  1294. func openAIResponsesCompletedEventIsEmpty(data []byte, usage *OpenAIUsage) bool {
  1295. if len(data) == 0 || !gjson.ValidBytes(data) {
  1296. return false
  1297. }
  1298. if usage != nil && (usage.InputTokens > 0 || usage.OutputTokens > 0 ||
  1299. usage.ImageInputTokens > 0 || usage.ImageOutputTokens > 0 ||
  1300. usage.CacheCreationInputTokens > 0 || usage.CacheReadInputTokens > 0) {
  1301. return false
  1302. }
  1303. if gjson.GetBytes(data, "usage").Exists() || gjson.GetBytes(data, "response.usage").Exists() {
  1304. return false
  1305. }
  1306. if gjson.GetBytes(data, "error").Exists() || gjson.GetBytes(data, "response.error").Exists() {
  1307. return false
  1308. }
  1309. if output := gjson.GetBytes(data, "response.output"); output.Exists() && output.IsArray() && len(output.Array()) > 0 {
  1310. return false
  1311. }
  1312. return true
  1313. }
  1314. func mergeHostedImageGenToolUsage(imageGen gjson.Result, usage *OpenAIUsage) {
  1315. if !imageGen.Exists() || !imageGen.IsObject() {
  1316. return
  1317. }
  1318. if usage.ImageOutputTokens == 0 {
  1319. if v := imageGen.Get("output_tokens_details.image_tokens").Int(); v > 0 {
  1320. usage.ImageOutputTokens = int(v)
  1321. }
  1322. }
  1323. if usage.ImageInputTokens == 0 {
  1324. if v := imageGen.Get("input_tokens_details.image_tokens").Int(); v > 0 {
  1325. usage.ImageInputTokens = int(v)
  1326. }
  1327. }
  1328. }
  1329. func extractOpenAIResponseIDFromJSONBytes(body []byte) string {
  1330. if len(body) == 0 || !gjson.ValidBytes(body) {
  1331. return ""
  1332. }
  1333. if id := strings.TrimSpace(gjson.GetBytes(body, "id").String()); id != "" {
  1334. return id
  1335. }
  1336. return strings.TrimSpace(gjson.GetBytes(body, "response.id").String())
  1337. }
  1338. const openAIHTTPResponseOwnerContextKey = "openai_http_response_owner"
  1339. type openAIHTTPResponseOwner struct {
  1340. userID int64
  1341. apiKeyID int64
  1342. }
  1343. // SetOpenAIHTTPResponseOwner marks the authenticated downstream owner whose
  1344. // successful Responses IDs may be used for later HTTP continuations.
  1345. func SetOpenAIHTTPResponseOwner(c *gin.Context, userID, apiKeyID int64) {
  1346. if c == nil || userID <= 0 || apiKeyID <= 0 {
  1347. return
  1348. }
  1349. c.Set(openAIHTTPResponseOwnerContextKey, openAIHTTPResponseOwner{userID: userID, apiKeyID: apiKeyID})
  1350. }
  1351. // ValidateOpenAIHTTPResponseOwner authorizes a continuation by downstream
  1352. // tenant. API key identity is retained in the binding, while keys owned by the
  1353. // same user remain interoperable.
  1354. func (s *OpenAIGatewayService) ValidateOpenAIHTTPResponseOwner(
  1355. ctx context.Context,
  1356. groupID int64,
  1357. responseID string,
  1358. userID, apiKeyID int64,
  1359. ) (bool, error) {
  1360. if s == nil || strings.TrimSpace(responseID) == "" || userID <= 0 || apiKeyID <= 0 {
  1361. return false, nil
  1362. }
  1363. ownerUserID, ownerAPIKeyID, found, err := s.getOpenAIWSStateStore().GetHTTPResponseOwner(ctx, groupID, responseID)
  1364. if err != nil || !found {
  1365. return false, err
  1366. }
  1367. return ownerUserID == userID || (ownerUserID <= 0 && ownerAPIKeyID == apiKeyID), nil
  1368. }
  1369. // BindOpenAIHTTPResponseOwner records an HTTP continuation owner independently
  1370. // from the upstream account selected for that response.
  1371. func (s *OpenAIGatewayService) BindOpenAIHTTPResponseOwner(
  1372. ctx context.Context,
  1373. groupID int64,
  1374. responseID string,
  1375. userID, apiKeyID int64,
  1376. ) error {
  1377. if s == nil {
  1378. return nil
  1379. }
  1380. return s.getOpenAIWSStateStore().BindHTTPResponseOwner(
  1381. ctx, groupID, responseID, userID, apiKeyID, s.openAIWSResponseStickyTTL(),
  1382. )
  1383. }
  1384. func (s *OpenAIGatewayService) bindHTTPResponseAccount(ctx context.Context, c *gin.Context, account *Account, responseID string) {
  1385. if s == nil || account == nil || account.ID <= 0 {
  1386. return
  1387. }
  1388. responseID = strings.TrimSpace(responseID)
  1389. if responseID == "" {
  1390. return
  1391. }
  1392. store := s.getOpenAIWSStateStore()
  1393. if store == nil {
  1394. return
  1395. }
  1396. groupID := getOpenAIGroupIDFromContext(c)
  1397. ttl := s.openAIWSResponseStickyTTL()
  1398. logOpenAIWSBindResponseAccountWarn(groupID, account.ID, responseID, store.BindResponseAccount(ctx, groupID, responseID, account.ID, ttl))
  1399. if rawOwner, ok := c.Get(openAIHTTPResponseOwnerContextKey); ok {
  1400. if owner, ok := rawOwner.(openAIHTTPResponseOwner); ok && owner.userID > 0 && owner.apiKeyID > 0 {
  1401. if err := s.BindOpenAIHTTPResponseOwner(ctx, groupID, responseID, owner.userID, owner.apiKeyID); err != nil {
  1402. logger.L().Warn(
  1403. "openai.http_bind_response_owner_failed",
  1404. zap.Int64("group_id", groupID),
  1405. zap.Int64("account_id", account.ID),
  1406. zap.Int64("user_id", owner.userID),
  1407. zap.Int64("api_key_id", owner.apiKeyID),
  1408. zap.String("response_id", truncateOpenAIWSLogValue(responseID, openAIWSIDValueMaxLen)),
  1409. zap.Error(err),
  1410. )
  1411. }
  1412. }
  1413. }
  1414. }
  1415. func openAIUsageFromGJSON(value gjson.Result) (OpenAIUsage, bool) {
  1416. if !value.Exists() || !value.IsObject() {
  1417. return OpenAIUsage{}, false
  1418. }
  1419. inputTokens := value.Get("input_tokens").Int()
  1420. if inputTokens == 0 {
  1421. inputTokens = value.Get("prompt_tokens").Int()
  1422. }
  1423. outputTokens := value.Get("output_tokens").Int()
  1424. if outputTokens == 0 {
  1425. outputTokens = value.Get("completion_tokens").Int()
  1426. }
  1427. // xAI reports visible output separately from reasoning_tokens; OpenAI
  1428. // folds reasoning into completion/output. Use total_tokens to tell them apart.
  1429. reasoningTokens := max(int(firstPositiveGJSONInt(
  1430. value.Get("completion_tokens_details.reasoning_tokens"),
  1431. value.Get("output_tokens_details.reasoning_tokens"),
  1432. )), 0)
  1433. if reasoningTokens > 0 {
  1434. outputTokens = xai.IncludeIndependentReasoningTokens(
  1435. inputTokens, outputTokens, value.Get("total_tokens").Int(), int64(reasoningTokens),
  1436. )
  1437. }
  1438. cacheReadTokens := openAICacheReadTokensFromUsage(value)
  1439. cacheCreationTokens := openAICacheCreationTokensFromUsage(value)
  1440. imageOutputTokens := value.Get("output_tokens_details.image_tokens").Int()
  1441. if imageOutputTokens == 0 {
  1442. imageOutputTokens = value.Get("completion_tokens_details.image_tokens").Int()
  1443. }
  1444. // 图片输入 token(如 gpt-image-2 的 /v1/images/edits 带图请求),
  1445. // 上游在 input_tokens_details.image_tokens 单独回传,用于图/文输入分价计费。
  1446. // 普通文本请求该字段为 0,走原路径行为不变。
  1447. imageInputTokens := firstPositiveGJSONInt(
  1448. value.Get("input_tokens_details.image_tokens"),
  1449. value.Get("prompt_tokens_details.image_tokens"),
  1450. )
  1451. return OpenAIUsage{
  1452. InputTokens: int(inputTokens),
  1453. ImageInputTokens: imageInputTokens,
  1454. OutputTokens: int(outputTokens),
  1455. CacheCreationInputTokens: cacheCreationTokens,
  1456. CacheReadInputTokens: cacheReadTokens,
  1457. ImageOutputTokens: int(imageOutputTokens),
  1458. }, true
  1459. }
  1460. func openAICacheReadTokensFromUsage(value gjson.Result) int {
  1461. for _, nested := range []gjson.Result{
  1462. value.Get("input_tokens_details.cached_tokens"),
  1463. value.Get("prompt_tokens_details.cached_tokens"),
  1464. } {
  1465. if nested.Exists() {
  1466. return max(int(nested.Int()), 0)
  1467. }
  1468. }
  1469. return firstPositiveGJSONInt(
  1470. value.Get("cache_read_input_tokens"),
  1471. value.Get("cache_read_tokens"),
  1472. value.Get("cached_tokens"),
  1473. )
  1474. }
  1475. func openAICacheCreationTokensFromUsage(value gjson.Result) int {
  1476. for _, nested := range []gjson.Result{
  1477. value.Get("input_tokens_details.cache_write_tokens"),
  1478. value.Get("prompt_tokens_details.cache_write_tokens"),
  1479. value.Get("input_tokens_details.cache_creation_tokens"),
  1480. value.Get("prompt_tokens_details.cache_creation_tokens"),
  1481. } {
  1482. if nested.Exists() {
  1483. return max(int(nested.Int()), 0)
  1484. }
  1485. }
  1486. return firstPositiveGJSONInt(
  1487. value.Get("cache_write_tokens"),
  1488. value.Get("cache_creation_input_tokens"),
  1489. value.Get("cache_write_input_tokens"),
  1490. value.Get("cache_creation_tokens"),
  1491. )
  1492. }
  1493. func (s *OpenAIGatewayService) handleNonStreamingResponse(ctx context.Context, resp *http.Response, c *gin.Context, account *Account, originalModel, mappedModel string) (*openaiNonStreamingResult, error) {
  1494. body, err := ReadUpstreamResponseBody(resp.Body, s.cfg, c, openAITooLargeError)
  1495. if err != nil {
  1496. return nil, err
  1497. }
  1498. observer := upstreamResponseModelObserverFromContext(c)
  1499. if observer == nil {
  1500. observer = beginUpstreamResponseModelObservation(c)
  1501. }
  1502. if bodyHasSSEFraming(body) {
  1503. observeOpenAISSEBody(observer, string(body))
  1504. } else {
  1505. observer.ObserveOpenAI(body, strings.TrimSpace(gjson.GetBytes(body, "type").String()))
  1506. }
  1507. // Detect SSE responses for ALL account types via Content-Type header.
  1508. // Some OpenAI-compatible upstreams (including other sub2api instances)
  1509. // may return SSE even when stream=false was requested.
  1510. if isEventStreamResponse(resp.Header) {
  1511. return s.handleSSEToJSON(resp, c, account, body, originalModel, mappedModel)
  1512. }
  1513. // bodyLooksLikeSSE is a line-level heuristic: real SSE framing requires
  1514. // "data:"/"event:" field names at the very start of a physical line. A
  1515. // plain bytes.Contains scan would also match ordinary JSON responses
  1516. // whose string content merely echoes the literal text "data:" or
  1517. // "event:" (e.g. compact tool output), causing those JSON bodies to be
  1518. // misrouted into handleSSEToJSON and lose their usage accounting.
  1519. bodyLooksLikeSSE := bodyHasSSEFraming(body)
  1520. // For OAuth accounts, also fall back to a body-content heuristic because
  1521. // the upstream may omit the Content-Type header while still sending SSE.
  1522. // This heuristic is NOT applied to API-key accounts to avoid false
  1523. // positives on JSON responses that coincidentally contain "data:" or
  1524. // "event:" in their text content.
  1525. if account.Type == AccountTypeOAuth && bodyLooksLikeSSE {
  1526. return s.handleSSEToJSON(resp, c, account, body, originalModel, mappedModel)
  1527. }
  1528. if account != nil && account.IsGrok() && isOpenAIResponsesCompactPath(c) {
  1529. body, err = convertGrokResponseToOpenAICompact(body)
  1530. if err != nil {
  1531. return nil, fmt.Errorf("convert Grok compact response: %w", err)
  1532. }
  1533. }
  1534. usageValue, usageOK := extractOpenAIUsageFromJSONBytes(body)
  1535. if !usageOK {
  1536. if bodyLooksLikeSSE {
  1537. return s.handleSSEToJSON(resp, c, account, body, originalModel, mappedModel)
  1538. }
  1539. return nil, fmt.Errorf("parse response: invalid json response")
  1540. }
  1541. usage := &usageValue
  1542. logOpenAISuccessMissingUsage(ctx, c, account, resp, usage, "json", false)
  1543. // Replace model in response if needed
  1544. if originalModel != mappedModel {
  1545. body = s.replaceModelInResponseBody(body, mappedModel, originalModel)
  1546. }
  1547. body, err = restoreGrokResponsesClientToolPayload(c, body)
  1548. if err != nil {
  1549. return nil, fmt.Errorf("restore Grok Responses client tool response: %w", err)
  1550. }
  1551. body, err = restoreOpenAIResponsesClientToolPayload(c, body)
  1552. if err != nil {
  1553. return nil, fmt.Errorf("restore OpenAI Responses client tool response: %w", err)
  1554. }
  1555. body, err = restoreOpenAIResponsesNamespacePayload(c, body)
  1556. if err != nil {
  1557. return nil, fmt.Errorf("restore OpenAI namespace response: %w", err)
  1558. }
  1559. body = restoreCodexToolNamesFromContext(c, body)
  1560. responseheaders.WriteFilteredHeaders(c.Writer.Header(), resp.Header, s.responseHeaderFilter)
  1561. // Codex 协议要求 /responses/compact JSON 响应携带 x-codex-turn-state
  1562. // (codex-api/src/endpoint/compact.rs 从响应头捕获),显式回传。
  1563. s.relayOpenAICodexTurnState(c, account, resp.Header)
  1564. contentType := "application/json"
  1565. if s.cfg != nil && !s.cfg.Security.ResponseHeaders.Enabled {
  1566. if upstreamType := resp.Header.Get("Content-Type"); upstreamType != "" {
  1567. contentType = upstreamType
  1568. }
  1569. }
  1570. if !writeOpenAICompactSSEBridge(c, resp.StatusCode, body) {
  1571. c.Data(resp.StatusCode, contentType, body)
  1572. }
  1573. return &openaiNonStreamingResult{
  1574. OpenAIUsage: usage,
  1575. usage: usage,
  1576. responseID: extractOpenAIResponseIDFromJSONBytes(body),
  1577. imageCount: countOpenAIResponseImageOutputsFromJSONBytes(body),
  1578. imageOutputSizes: collectOpenAIResponseImageOutputSizesFromJSONBytes(body),
  1579. searchCount: countGrokNativeSearchCallsFromJSONBytes(body),
  1580. }, nil
  1581. }
  1582. func isEventStreamResponse(header http.Header) bool {
  1583. contentType := strings.ToLower(header.Get("Content-Type"))
  1584. return strings.Contains(contentType, "text/event-stream")
  1585. }
  1586. // bodyHasSSEFraming reports whether body contains genuine SSE framing by
  1587. // scanning for physical lines that begin with the "data:" or "event:"
  1588. // field names, per the SSE spec. Unlike a raw substring scan, this does not
  1589. // match when those strings only appear embedded inside JSON string values
  1590. // (e.g. "data: foo" quoted as part of an assistant text field), since such
  1591. // occurrences never start a physical line in a valid JSON encoding.
  1592. func bodyHasSSEFraming(body []byte) bool {
  1593. for _, line := range bytes.Split(body, []byte("\n")) {
  1594. line = bytes.TrimRight(line, "\r")
  1595. if bytes.HasPrefix(line, []byte("data:")) || bytes.HasPrefix(line, []byte("event:")) {
  1596. return true
  1597. }
  1598. }
  1599. return false
  1600. }
  1601. func (s *OpenAIGatewayService) handleSSEToJSON(resp *http.Response, c *gin.Context, account *Account, body []byte, originalModel, mappedModel string) (*openaiNonStreamingResult, error) {
  1602. bodyText := string(body)
  1603. terminalType, terminalPayload, terminalOK := extractOpenAISSETerminalEvent(bodyText)
  1604. if terminalOK && (terminalType == "response.failed" || terminalType == "error") {
  1605. msg := extractOpenAISSEErrorMessage(terminalPayload)
  1606. if msg == "" {
  1607. msg = "Upstream compact response failed"
  1608. }
  1609. if compactErr := newOpenAICompactFallbackSignal(c, terminalPayload, msg); compactErr != nil {
  1610. return nil, compactErr
  1611. }
  1612. if failoverErr := s.nonStreamingTerminalFailureFailover(c, resp, account, false, terminalType, terminalPayload, msg, mappedModel); failoverErr != nil {
  1613. return nil, failoverErr
  1614. }
  1615. return nil, s.writeOpenAINonStreamingProtocolError(resp, c, msg)
  1616. }
  1617. finalResponse, ok := extractCodexFinalResponse(bodyText)
  1618. usage := s.parseSSEUsageFromBody(bodyText)
  1619. if ok {
  1620. if parsedUsage, parsed := extractOpenAIUsageFromJSONBytes(finalResponse); parsed {
  1621. *usage = parsedUsage
  1622. }
  1623. // When the terminal event has an empty output array, reconstruct
  1624. // output from accumulated delta events so the client gets full content.
  1625. // gjson Array() returns empty slice for null, missing, or empty arrays.
  1626. if len(gjson.GetBytes(finalResponse, "output").Array()) == 0 {
  1627. if outputJSON, reconstructed := reconstructResponseOutputFromSSE(bodyText); reconstructed {
  1628. if patched, err := sjson.SetRawBytes(finalResponse, "output", outputJSON); err == nil {
  1629. finalResponse = patched
  1630. }
  1631. }
  1632. }
  1633. finalResponse = supplementCompactionItemFromSSE(c, finalResponse, bodyText)
  1634. body = finalResponse
  1635. if originalModel != mappedModel {
  1636. body = s.replaceModelInResponseBody(body, mappedModel, originalModel)
  1637. }
  1638. // Correct tool calls in final response
  1639. body = s.correctToolCallsInResponseBody(body)
  1640. restoredBody, restoreErr := restoreGrokResponsesClientToolPayload(c, body)
  1641. if restoreErr != nil {
  1642. return nil, fmt.Errorf("restore Grok Responses client tool response: %w", restoreErr)
  1643. }
  1644. restoredBody, restoreErr = restoreOpenAIResponsesClientToolPayload(c, restoredBody)
  1645. if restoreErr != nil {
  1646. return nil, fmt.Errorf("restore OpenAI Responses client tool response: %w", restoreErr)
  1647. }
  1648. restoredBody, restoreErr = restoreOpenAIResponsesNamespacePayload(c, restoredBody)
  1649. if restoreErr != nil {
  1650. return nil, fmt.Errorf("restore OpenAI namespace response: %w", restoreErr)
  1651. }
  1652. restoredBody = restoreCodexToolNamesFromContext(c, restoredBody)
  1653. body = restoredBody
  1654. } else {
  1655. if originalModel != mappedModel {
  1656. bodyText = s.replaceModelInSSEBody(bodyText, mappedModel, originalModel)
  1657. }
  1658. body = []byte(bodyText)
  1659. }
  1660. responseheaders.WriteFilteredHeaders(c.Writer.Header(), resp.Header, s.responseHeaderFilter)
  1661. logOpenAISuccessMissingUsage(c.Request.Context(), c, account, resp, usage, terminalType, false)
  1662. s.relayOpenAICodexTurnState(c, account, resp.Header)
  1663. contentType := "application/json; charset=utf-8"
  1664. if !ok {
  1665. contentType = resp.Header.Get("Content-Type")
  1666. if contentType == "" {
  1667. contentType = "text/event-stream"
  1668. }
  1669. }
  1670. if !writeOpenAICompactSSEBridge(c, resp.StatusCode, body) {
  1671. c.Data(resp.StatusCode, contentType, body)
  1672. }
  1673. return &openaiNonStreamingResult{
  1674. OpenAIUsage: usage,
  1675. usage: usage,
  1676. responseID: extractOpenAIResponseIDFromJSONBytes(body),
  1677. imageCount: countOpenAIImageOutputsFromSSEBody(bodyText),
  1678. imageOutputSizes: collectOpenAIImageOutputSizesFromSSEBody(bodyText),
  1679. searchCount: countGrokNativeSearchCallsFromSSEBody(bodyText),
  1680. }, nil
  1681. }
  1682. func extractOpenAISSETerminalEvent(body string) (string, []byte, bool) {
  1683. var terminalType string
  1684. var terminalPayload []byte
  1685. forEachOpenAISSEFrame(body, func(eventType string, data []byte) {
  1686. switch eventType {
  1687. case "response.completed", "response.done", "response.failed", "response.incomplete", "response.cancelled", "response.canceled", "error":
  1688. terminalType = eventType
  1689. terminalPayload = append([]byte(nil), data...)
  1690. }
  1691. })
  1692. if terminalPayload != nil {
  1693. return terminalType, terminalPayload, true
  1694. }
  1695. return "", nil, false
  1696. }
  1697. func extractOpenAISSEErrorMessage(payload []byte) string {
  1698. if len(payload) == 0 {
  1699. return ""
  1700. }
  1701. for _, path := range []string{"response.error.message", "error.message", "message"} {
  1702. if msg := strings.TrimSpace(gjson.GetBytes(payload, path).String()); msg != "" {
  1703. return sanitizeUpstreamErrorMessage(msg)
  1704. }
  1705. }
  1706. return sanitizeUpstreamErrorMessage(strings.TrimSpace(extractUpstreamErrorMessage(payload)))
  1707. }
  1708. func buildOpenAIResponseFailedSSE(responseID, model string, source []byte, fallbackMessage string) string {
  1709. responseID = strings.TrimSpace(responseID)
  1710. if responseID == "" {
  1711. responseID = "resp_" + strings.ReplaceAll(uuid.NewString(), "-", "")
  1712. }
  1713. errorType := strings.TrimSpace(gjson.GetBytes(source, "error.type").String())
  1714. if errorType == "" {
  1715. errorType = strings.TrimSpace(gjson.GetBytes(source, "response.error.type").String())
  1716. }
  1717. code := strings.TrimSpace(gjson.GetBytes(source, "error.code").String())
  1718. if code == "" {
  1719. code = strings.TrimSpace(gjson.GetBytes(source, "response.error.code").String())
  1720. }
  1721. if code == "" {
  1722. code = "upstream_error"
  1723. }
  1724. message := extractOpenAISSEErrorMessage(source)
  1725. if message == "" {
  1726. message = strings.TrimSpace(fallbackMessage)
  1727. }
  1728. if message == "" {
  1729. message = "Upstream response failed"
  1730. }
  1731. errorBody := gin.H{"code": code, "message": message}
  1732. if errorType != "" {
  1733. errorBody["type"] = errorType
  1734. }
  1735. response := gin.H{
  1736. "id": responseID,
  1737. "object": "response",
  1738. "status": "failed",
  1739. "output": []any{},
  1740. "error": errorBody,
  1741. }
  1742. if model = strings.TrimSpace(model); model != "" {
  1743. response["model"] = model
  1744. }
  1745. payload, err := marshalOpenAIUpstreamJSON(gin.H{
  1746. "type": "response.failed",
  1747. "response": response,
  1748. })
  1749. if err != nil {
  1750. // All values above are JSON primitives, so this is only a defensive fallback.
  1751. payload = []byte(`{"type":"response.failed","response":{"status":"failed","output":[],"error":{"code":"upstream_error","message":"Upstream response failed"}}}`)
  1752. }
  1753. return "event: response.failed\ndata: " + string(payload) + "\n\n"
  1754. }
  1755. func sanitizeOpenAIResponseFailedEventForClient(payload []byte, eventType string, clientOutputStarted bool) ([]byte, bool) {
  1756. eventType = strings.TrimSpace(eventType)
  1757. isFailedEvent := eventType == "response.failed"
  1758. if (!isFailedEvent && eventType != "error") || len(payload) == 0 || !gjson.ValidBytes(payload) {
  1759. return payload, false
  1760. }
  1761. updated := payload
  1762. // 容量降载码对 Codex CLI 是致命错误;事件既然要写给客户端(failover 已不可用),
  1763. // 就改写为客户端可重试的错误码。error 帧与 response.failed 都要改:上游降载
  1764. // 总是先推 error 帧再收 failed,两帧携带同一个错误。
  1765. if rewritten, changed := sanitizeOpenAICapacityShedErrorCodeForClient(updated); changed {
  1766. updated = rewritten
  1767. }
  1768. if !isFailedEvent {
  1769. return updated, !bytes.Equal(updated, payload)
  1770. }
  1771. if clientOutputStarted && isOpenAIContextWindowError(extractOpenAISSEErrorMessage(payload), payload) {
  1772. errorPath := ""
  1773. switch {
  1774. case gjson.GetBytes(updated, "response.error").Exists():
  1775. errorPath = "response.error"
  1776. case gjson.GetBytes(updated, "error").Exists():
  1777. errorPath = "error"
  1778. }
  1779. if errorPath != "" {
  1780. next, err := sjson.SetBytes(updated, errorPath+".type", "invalid_request_error")
  1781. if err != nil {
  1782. return payload, false
  1783. }
  1784. updated = next
  1785. next, err = sjson.SetBytes(updated, errorPath+".code", "context_length_exceeded")
  1786. if err != nil {
  1787. return payload, false
  1788. }
  1789. updated = next
  1790. }
  1791. }
  1792. if !gjson.GetBytes(updated, "response").Exists() {
  1793. return updated, !bytes.Equal(updated, payload)
  1794. }
  1795. for _, path := range []string{
  1796. "response.instructions",
  1797. "response.output",
  1798. "response.usage",
  1799. "response.metadata",
  1800. "response.reasoning",
  1801. "response.tools",
  1802. "response.tool_choice",
  1803. "response.parallel_tool_calls",
  1804. "response.text",
  1805. "response.truncation",
  1806. "response.max_output_tokens",
  1807. "response.incomplete_details",
  1808. } {
  1809. next, err := sjson.DeleteBytes(updated, path)
  1810. if err != nil {
  1811. return payload, false
  1812. }
  1813. updated = next
  1814. }
  1815. return updated, !bytes.Equal(updated, payload)
  1816. }
  1817. func (s *OpenAIGatewayService) writeOpenAINonStreamingProtocolError(resp *http.Response, c *gin.Context, message string) error {
  1818. message = sanitizeUpstreamErrorMessage(strings.TrimSpace(message))
  1819. if message == "" {
  1820. message = "Upstream returned an invalid non-streaming response"
  1821. }
  1822. setOpsUpstreamError(c, http.StatusBadGateway, message, "")
  1823. // body-signal compact 心跳可能已把响应头提交为 200,此时只能以
  1824. // response.failed 终止事件回传错误,不能再写 JSON+状态码。
  1825. if openAICompactClientWantsStream(c) && StopOpenAICompactSSEKeepaliveCommitted(c) {
  1826. writeOpenAICompactSSEFailureMessage(c, http.StatusBadGateway, "upstream_error", message)
  1827. return fmt.Errorf("non-streaming openai protocol error: %s", message)
  1828. }
  1829. responseheaders.WriteFilteredHeaders(c.Writer.Header(), resp.Header, s.responseHeaderFilter)
  1830. c.Writer.Header().Set("Content-Type", "application/json; charset=utf-8")
  1831. c.JSON(http.StatusBadGateway, gin.H{
  1832. "error": gin.H{
  1833. "type": "upstream_error",
  1834. "message": message,
  1835. },
  1836. })
  1837. return fmt.Errorf("non-streaming openai protocol error: %s", message)
  1838. }
  1839. func extractCodexFinalResponse(body string) ([]byte, bool) {
  1840. var finalResponse []byte
  1841. forEachOpenAISSEFrame(body, func(eventType string, data []byte) {
  1842. if finalResponse != nil {
  1843. return
  1844. }
  1845. if normalized, changed := normalizeCompletedImageGenerationStatus(data); changed {
  1846. data = normalized
  1847. }
  1848. if eventType == "response.done" || eventType == "response.completed" {
  1849. if response := gjson.GetBytes(data, "response"); response.Exists() && response.Type == gjson.JSON && response.Raw != "" {
  1850. finalResponse = []byte(response.Raw)
  1851. }
  1852. }
  1853. })
  1854. if finalResponse != nil {
  1855. return finalResponse, true
  1856. }
  1857. return nil, false
  1858. }
  1859. func normalizeCompletedImageGenerationStatus(data []byte) ([]byte, bool) {
  1860. if len(data) == 0 || !gjson.ValidBytes(data) {
  1861. return data, false
  1862. }
  1863. shouldNormalize := func(item gjson.Result) bool {
  1864. if !item.Exists() || !item.IsObject() ||
  1865. strings.TrimSpace(item.Get("type").String()) != "image_generation_call" {
  1866. return false
  1867. }
  1868. switch strings.TrimSpace(item.Get("status").String()) {
  1869. case "generating", "in_progress":
  1870. return strings.TrimSpace(item.Get("result").String()) != ""
  1871. default:
  1872. return false
  1873. }
  1874. }
  1875. eventType := strings.TrimSpace(gjson.GetBytes(data, "type").String())
  1876. switch eventType {
  1877. case "response.output_item.done":
  1878. if !shouldNormalize(gjson.GetBytes(data, "item")) {
  1879. return data, false
  1880. }
  1881. updated, err := sjson.SetBytes(data, "item.status", "completed")
  1882. if err != nil {
  1883. return data, false
  1884. }
  1885. return updated, true
  1886. case "response.completed", "response.done":
  1887. output := gjson.GetBytes(data, "response.output")
  1888. if !output.Exists() || !output.IsArray() {
  1889. return data, false
  1890. }
  1891. updated := data
  1892. changed := false
  1893. for i, item := range output.Array() {
  1894. if !shouldNormalize(item) {
  1895. continue
  1896. }
  1897. next, err := sjson.SetBytes(updated, "response.output."+strconv.Itoa(i)+".status", "completed")
  1898. if err != nil {
  1899. return data, false
  1900. }
  1901. updated = next
  1902. changed = true
  1903. }
  1904. return updated, changed
  1905. default:
  1906. return data, false
  1907. }
  1908. }
  1909. // responsesStreamOutputItems remembers the raw item carried by each
  1910. // response.output_item.done event, keyed by output_index.
  1911. //
  1912. // reconstructResponseOutputFromSSE already prefers the raw done items over
  1913. // delta accumulation when it rebuilds a buffered response, because the
  1914. // accumulator models only "one reasoning, one message, N function calls" and
  1915. // therefore cannot preserve item identity, per-item status/phase, ordering, or
  1916. // item types it does not know about. The streaming path had no equivalent
  1917. // because it never sees the whole body at once; this collector gives it one.
  1918. type responsesStreamOutputItems struct {
  1919. items map[int]json.RawMessage
  1920. }
  1921. func newResponsesStreamOutputItems() *responsesStreamOutputItems {
  1922. return &responsesStreamOutputItems{items: make(map[int]json.RawMessage)}
  1923. }
  1924. // Observe records the item of a response.output_item.done event verbatim. The
  1925. // raw JSON is kept byte for byte so vendor extensions and future fields survive
  1926. // the rebuild.
  1927. func (r *responsesStreamOutputItems) Observe(data []byte) {
  1928. if r == nil || len(data) == 0 || !gjson.ValidBytes(data) {
  1929. return
  1930. }
  1931. if strings.TrimSpace(gjson.GetBytes(data, "type").String()) != "response.output_item.done" {
  1932. return
  1933. }
  1934. item := gjson.GetBytes(data, "item")
  1935. if !item.Exists() || !item.IsObject() {
  1936. return
  1937. }
  1938. index := int(gjson.GetBytes(data, "output_index").Int())
  1939. r.items[index] = json.RawMessage(append([]byte(nil), item.Raw...))
  1940. }
  1941. func (r *responsesStreamOutputItems) HasItems() bool {
  1942. return r != nil && len(r.items) > 0
  1943. }
  1944. // Count reports how many distinct output items the stream reported as done.
  1945. func (r *responsesStreamOutputItems) Count() int {
  1946. if r == nil {
  1947. return 0
  1948. }
  1949. return len(r.items)
  1950. }
  1951. // BuildOutput returns the remembered items ordered by output_index.
  1952. func (r *responsesStreamOutputItems) BuildOutput() ([]byte, bool) {
  1953. if !r.HasItems() {
  1954. return nil, false
  1955. }
  1956. indexes := make([]int, 0, len(r.items))
  1957. for index := range r.items {
  1958. indexes = append(indexes, index)
  1959. }
  1960. sort.Ints(indexes)
  1961. ordered := make([]json.RawMessage, 0, len(indexes))
  1962. for _, index := range indexes {
  1963. ordered = append(ordered, r.items[index])
  1964. }
  1965. encoded, err := json.Marshal(ordered)
  1966. if err != nil {
  1967. return nil, false
  1968. }
  1969. return encoded, true
  1970. }
  1971. func normalizeResponsesStreamingTerminalOutput(data []byte, acc *apicompat.BufferedResponseAccumulator, doneItems *responsesStreamOutputItems, imageOutputs []json.RawMessage) ([]byte, bool) {
  1972. eventType := strings.TrimSpace(gjson.GetBytes(data, "type").String())
  1973. switch eventType {
  1974. case "response.completed", "response.done", "response.incomplete", "response.cancelled", "response.canceled":
  1975. default:
  1976. return data, false
  1977. }
  1978. output := gjson.GetBytes(data, "response.output")
  1979. hasAccumulatedOutput := (acc != nil && acc.HasContent()) || len(imageOutputs) > 0 || doneItems.HasItems()
  1980. if output.Exists() && output.IsArray() {
  1981. terminalCount := len(output.Array())
  1982. // A terminal output carrying at least as many items as the stream
  1983. // reported is left untouched. Carrying fewer means the terminal
  1984. // dropped items the stream already reported as done, and those
  1985. // reported items are the authoritative record of the turn.
  1986. if terminalCount > 0 && terminalCount >= doneItems.Count() {
  1987. return data, false
  1988. }
  1989. if terminalCount == 0 && !hasAccumulatedOutput {
  1990. return data, false
  1991. }
  1992. }
  1993. outputJSON := []byte("[]")
  1994. // Same precedence as reconstructResponseOutputFromSSE: the items the stream
  1995. // actually reported win over anything rebuilt from deltas. Image generation
  1996. // items arrive as done events too, so imageOutputs would duplicate them here.
  1997. if reconstructed, ok := doneItems.BuildOutput(); ok {
  1998. outputJSON = reconstructed
  1999. } else if reconstructed, ok := buildResponsesOutputJSON(acc, imageOutputs); ok {
  2000. outputJSON = reconstructed
  2001. }
  2002. updated, err := sjson.SetRawBytes(data, "response.output", outputJSON)
  2003. if err != nil {
  2004. return data, false
  2005. }
  2006. return updated, true
  2007. }
  2008. func responsesStreamEventMayContributeToOutput(eventType string) bool {
  2009. switch eventType {
  2010. case "response.output_text.delta",
  2011. "response.output_item.added",
  2012. "response.function_call_arguments.delta",
  2013. "response.reasoning_summary_text.delta":
  2014. return true
  2015. default:
  2016. return false
  2017. }
  2018. }
  2019. // collectRawResponsesOutputItemsFromSSE 按到达顺序收集 SSE 流中
  2020. // response.output_item.done 携带的原始 item。除已产生结果但仍停留在进行中
  2021. // 的图片状态外,item 以 raw JSON 逐字节保留,
  2022. // 避免经窄结构体重建时丢弃 encrypted_content/summary/opaque 等 compact
  2023. // 专属或未来新增字段(#3777 问题 2)。若整条流没有任何 done 事件,退回
  2024. // 收集 output_item.added 中的 compaction 类 item——compaction 结果没有
  2025. // delta 事件,部分上游只在 added 事件中携带完整 item。
  2026. func collectRawResponsesOutputItemsFromSSE(bodyText string) ([]byte, bool) {
  2027. var items []json.RawMessage
  2028. seen := make(map[string]struct{})
  2029. hasCompactionItem := false
  2030. appendItem := func(item gjson.Result) {
  2031. if !item.Exists() || !item.IsObject() {
  2032. return
  2033. }
  2034. key := strings.TrimSpace(item.Get("id").String())
  2035. if key == "" {
  2036. key = item.Raw
  2037. }
  2038. if _, dup := seen[key]; dup {
  2039. return
  2040. }
  2041. seen[key] = struct{}{}
  2042. if isResponsesCompactionItemType(item.Get("type").String()) {
  2043. hasCompactionItem = true
  2044. }
  2045. items = append(items, json.RawMessage(item.Raw))
  2046. }
  2047. forEachOpenAISSEFrame(bodyText, func(eventType string, data []byte) {
  2048. data = []byte(openAICompatPayloadWithEventType(string(data), eventType))
  2049. if normalized, changed := normalizeCompletedImageGenerationStatus(data); changed {
  2050. data = normalized
  2051. }
  2052. if eventType != "response.output_item.done" {
  2053. return
  2054. }
  2055. appendItem(gjson.GetBytes(data, "item"))
  2056. })
  2057. // done 事件未携带 compaction item 时再看 added:覆盖"其他 item 有 done、
  2058. // compaction 只在 added 中"的混合形态;done 已含 compaction 时跳过,
  2059. // 避免同一 item 在无 id 可去重时被收集两份(Codex 要求恰好一个)。
  2060. if !hasCompactionItem {
  2061. forEachOpenAISSEFrame(bodyText, func(eventType string, data []byte) {
  2062. if eventType != "response.output_item.added" {
  2063. return
  2064. }
  2065. item := gjson.GetBytes(data, "item")
  2066. if !isResponsesCompactionItemType(item.Get("type").String()) {
  2067. return
  2068. }
  2069. appendItem(item)
  2070. })
  2071. }
  2072. if len(items) == 0 {
  2073. return nil, false
  2074. }
  2075. outputJSON, err := json.Marshal(items)
  2076. if err != nil {
  2077. return nil, false
  2078. }
  2079. return outputJSON, true
  2080. }
  2081. // isResponsesCompactionItemType reports whether the item type is the Codex
  2082. // remote-compact result item ("compaction", upstream alias "compaction_summary").
  2083. func isResponsesCompactionItemType(itemType string) bool {
  2084. switch strings.TrimSpace(itemType) {
  2085. case "compaction", "compaction_summary":
  2086. return true
  2087. default:
  2088. return false
  2089. }
  2090. }
  2091. // supplementCompactionItemFromSSE 保证 compact 请求的终态 output 携带
  2092. // compaction item:终态 output 非空但缺失 compaction、而原始事件流的
  2093. // output_item.done(或 added)中存在时(上游不一致形态),以 raw JSON 补入。
  2094. // Codex remote compact v2 只从 output_item.done 收集 item 且要求恰好一个
  2095. // compaction item——纯流式透传(v0.1.146)下客户端直接读事件流天然拿得到,
  2096. // SSE→JSON 提取链路必须给出等价结果。非 compact 请求原样返回。
  2097. func supplementCompactionItemFromSSE(c *gin.Context, finalResponse []byte, bodyText string) []byte {
  2098. if !isOpenAIResponsesCompactPath(c) {
  2099. return finalResponse
  2100. }
  2101. if len(gjson.GetBytes(finalResponse, "output").Array()) == 0 {
  2102. // 空 output 由 reconstructResponseOutputFromSSE 整体修补,不在此处理。
  2103. return finalResponse
  2104. }
  2105. if responsesOutputHasCompactionItem(finalResponse) {
  2106. return finalResponse
  2107. }
  2108. item, found := findRawCompactionItemFromSSE(bodyText)
  2109. if !found {
  2110. return finalResponse
  2111. }
  2112. patched, err := sjson.SetRawBytes(finalResponse, "output.-1", item)
  2113. if err != nil {
  2114. return finalResponse
  2115. }
  2116. return patched
  2117. }
  2118. // responsesOutputHasCompactionItem reports whether the response JSON already
  2119. // carries a compaction item in its output array.
  2120. func responsesOutputHasCompactionItem(response []byte) bool {
  2121. for _, item := range gjson.GetBytes(response, "output").Array() {
  2122. if isResponsesCompactionItemType(item.Get("type").String()) {
  2123. return true
  2124. }
  2125. }
  2126. return false
  2127. }
  2128. // findRawCompactionItemFromSSE 从原始 SSE 事件流中提取第一个 compaction 类
  2129. // item 的 raw JSON:output_item.done 优先,output_item.added 兜底。
  2130. func findRawCompactionItemFromSSE(bodyText string) (json.RawMessage, bool) {
  2131. var found json.RawMessage
  2132. pick := func(eventType string) {
  2133. forEachOpenAISSEFrame(bodyText, func(effectiveType string, data []byte) {
  2134. if found != nil {
  2135. return
  2136. }
  2137. if effectiveType != eventType {
  2138. return
  2139. }
  2140. item := gjson.GetBytes(data, "item")
  2141. if !item.IsObject() || !isResponsesCompactionItemType(item.Get("type").String()) {
  2142. return
  2143. }
  2144. found = json.RawMessage(item.Raw)
  2145. })
  2146. }
  2147. pick("response.output_item.done")
  2148. if found == nil {
  2149. pick("response.output_item.added")
  2150. }
  2151. return found, found != nil
  2152. }
  2153. // reconstructResponseOutputFromSSE scans raw SSE body text and returns a
  2154. // JSON-encoded output array for a terminal event whose output is empty.
  2155. // Raw output_item.done items are preferred: per the Responses protocol they
  2156. // are the authoritative final form of each item. Delta accumulation only
  2157. // covers text/function_call/reasoning content and silently drops unknown
  2158. // item types such as compaction — Codex remote compact v2 then fails with
  2159. // "expected exactly one compaction output item, got 0" (#3887).
  2160. // Returns (nil, false) if nothing could be reconstructed.
  2161. func reconstructResponseOutputFromSSE(bodyText string) ([]byte, bool) {
  2162. if outputJSON, ok := collectRawResponsesOutputItemsFromSSE(bodyText); ok {
  2163. return outputJSON, true
  2164. }
  2165. acc := apicompat.NewBufferedResponseAccumulator()
  2166. imageOutputs := make([]json.RawMessage, 0, 1)
  2167. seenImages := make(map[string]struct{})
  2168. forEachOpenAISSEFrame(bodyText, func(eventType string, data []byte) {
  2169. data = []byte(openAICompatPayloadWithEventType(string(data), eventType))
  2170. if imageOutput, ok := extractImageGenerationOutputFromSSEData(data, seenImages); ok {
  2171. imageOutputs = append(imageOutputs, imageOutput)
  2172. }
  2173. if responsesStreamEventMayContributeToOutput(eventType) {
  2174. var event apicompat.ResponsesStreamEvent
  2175. if err := json.Unmarshal(data, &event); err == nil {
  2176. acc.ProcessEvent(&event)
  2177. }
  2178. }
  2179. })
  2180. return buildResponsesOutputJSON(acc, imageOutputs)
  2181. }
  2182. func buildResponsesOutputJSON(acc *apicompat.BufferedResponseAccumulator, imageOutputs []json.RawMessage) ([]byte, bool) {
  2183. if (acc == nil || !acc.HasContent()) && len(imageOutputs) == 0 {
  2184. return nil, false
  2185. }
  2186. var output []json.RawMessage
  2187. if acc != nil && acc.HasContent() {
  2188. outputJSON, err := json.Marshal(acc.BuildOutput())
  2189. if err == nil {
  2190. _ = json.Unmarshal(outputJSON, &output)
  2191. }
  2192. }
  2193. output = append(output, imageOutputs...)
  2194. if len(output) == 0 {
  2195. return nil, false
  2196. }
  2197. outputJSON, err := json.Marshal(output)
  2198. if err != nil {
  2199. return nil, false
  2200. }
  2201. return outputJSON, true
  2202. }
  2203. func extractImageGenerationOutputFromSSEData(data []byte, seen map[string]struct{}) (json.RawMessage, bool) {
  2204. if len(data) == 0 || !gjson.ValidBytes(data) {
  2205. return nil, false
  2206. }
  2207. if gjson.GetBytes(data, "type").String() != "response.output_item.done" {
  2208. return nil, false
  2209. }
  2210. item := gjson.GetBytes(data, "item")
  2211. if !item.Exists() || !item.IsObject() || item.Get("type").String() != "image_generation_call" {
  2212. return nil, false
  2213. }
  2214. if strings.TrimSpace(item.Get("result").String()) == "" {
  2215. return nil, false
  2216. }
  2217. key := strings.TrimSpace(item.Get("id").String())
  2218. if key == "" {
  2219. key = strings.TrimSpace(item.Get("output_format").String()) + "|" + strings.TrimSpace(item.Get("result").String())
  2220. }
  2221. if key != "" && seen != nil {
  2222. if _, exists := seen[key]; exists {
  2223. return nil, false
  2224. }
  2225. seen[key] = struct{}{}
  2226. }
  2227. return json.RawMessage(item.Raw), true
  2228. }
  2229. func (s *OpenAIGatewayService) parseSSEUsageFromBody(body string) *OpenAIUsage {
  2230. usage := &OpenAIUsage{}
  2231. forEachOpenAISSEFrame(body, func(eventType string, data []byte) {
  2232. s.parseSSEUsageBytesWithType(data, eventType, usage)
  2233. })
  2234. return usage
  2235. }
  2236. func (s *OpenAIGatewayService) replaceModelInSSEBody(body, fromModel, toModel string) string {
  2237. lines := strings.Split(body, "\n")
  2238. for i, line := range lines {
  2239. if _, ok := extractOpenAISSEDataLine(line); !ok {
  2240. continue
  2241. }
  2242. lines[i] = s.replaceModelInSSELine(line, fromModel, toModel)
  2243. }
  2244. return strings.Join(lines, "\n")
  2245. }