openai_images_responses.go 71 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140
  1. package service
  2. import (
  3. "bufio"
  4. "bytes"
  5. "context"
  6. "encoding/base64"
  7. "errors"
  8. "fmt"
  9. "io"
  10. "net/http"
  11. "strings"
  12. "sync/atomic"
  13. "time"
  14. "github.com/Wei-Shaw/sub2api/internal/pkg/logger"
  15. "github.com/Wei-Shaw/sub2api/internal/util/responseheaders"
  16. "github.com/gin-gonic/gin"
  17. "github.com/tidwall/gjson"
  18. "github.com/tidwall/sjson"
  19. )
  20. type openAIResponsesImageResult struct {
  21. Result string
  22. RevisedPrompt string
  23. OutputFormat string
  24. Size string
  25. Background string
  26. Quality string
  27. Model string
  28. }
  29. type OpenAIImagesUpstreamError struct {
  30. StatusCode int
  31. ErrorType string
  32. Code string
  33. Message string
  34. Param string
  35. UpstreamRequestID string
  36. // SynthesizedFromModelText marks an error the gateway inferred from the
  37. // model's plain-text output instead of reading it off a structured upstream
  38. // error frame. Such a verdict describes this one turn ("the model answered
  39. // with words instead of an image"), not the account — see
  40. // shouldCoolOpenAIImagesToolForError.
  41. SynthesizedFromModelText bool
  42. }
  43. func (e *OpenAIImagesUpstreamError) Error() string {
  44. if e == nil {
  45. return ""
  46. }
  47. code := strings.TrimSpace(e.Code)
  48. if code == "" {
  49. code = strings.TrimSpace(e.ErrorType)
  50. }
  51. message := strings.TrimSpace(e.Message)
  52. if code != "" && message != "" {
  53. return fmt.Sprintf("openai images upstream error: %s: %s", code, message)
  54. }
  55. if message != "" {
  56. return "openai images upstream error: " + message
  57. }
  58. if code != "" {
  59. return "openai images upstream error: " + code
  60. }
  61. return "openai images upstream error"
  62. }
  63. func (e *OpenAIImagesUpstreamError) clientStatusCode() int {
  64. if e == nil {
  65. return http.StatusBadGateway
  66. }
  67. if e.StatusCode > 0 {
  68. return e.StatusCode
  69. }
  70. return http.StatusBadGateway
  71. }
  72. func (e *OpenAIImagesUpstreamError) clientErrorType() string {
  73. if e == nil {
  74. return "upstream_error"
  75. }
  76. if trimmed := strings.TrimSpace(e.ErrorType); trimmed != "" {
  77. return trimmed
  78. }
  79. return "upstream_error"
  80. }
  81. func (e *OpenAIImagesUpstreamError) clientMessage() string {
  82. if e == nil {
  83. return "Upstream request failed"
  84. }
  85. if trimmed := strings.TrimSpace(e.Message); trimmed != "" {
  86. return trimmed
  87. }
  88. if trimmed := strings.TrimSpace(e.Code); trimmed != "" {
  89. return trimmed
  90. }
  91. return "Upstream request failed"
  92. }
  93. // IsOpenAIImagesRetryableUpstreamError reports whether an Images error is an
  94. // upstream server failure that may be retried on another account.
  95. func IsOpenAIImagesRetryableUpstreamError(err *OpenAIImagesUpstreamError) bool {
  96. return err != nil && err.StatusCode >= http.StatusInternalServerError
  97. }
  98. func openAIImagesSSEErrorStatus(errType, code string) int {
  99. errType = strings.ToLower(strings.TrimSpace(errType))
  100. code = strings.ToLower(strings.TrimSpace(code))
  101. switch {
  102. case strings.Contains(errType, "rate_limit"), strings.Contains(code, "rate_limit"):
  103. return http.StatusTooManyRequests
  104. case strings.Contains(errType, "authentication"), strings.Contains(code, "invalid_api_key"), code == "unauthorized":
  105. return http.StatusUnauthorized
  106. case strings.Contains(errType, "permission"), code == "forbidden":
  107. return http.StatusForbidden
  108. case strings.Contains(errType, "not_found"), strings.Contains(code, "not_found"):
  109. return http.StatusNotFound
  110. case strings.Contains(errType, "invalid_request"),
  111. errType == "image_generation_user_error",
  112. code == "moderation_blocked",
  113. strings.Contains(code, "content_policy"),
  114. strings.Contains(code, "policy_violation"),
  115. strings.Contains(code, "safety_violation"):
  116. return http.StatusBadRequest
  117. default:
  118. return http.StatusBadGateway
  119. }
  120. }
  121. func openAIImagesUpstreamErrorResponseBody(err *OpenAIImagesUpstreamError) []byte {
  122. if err == nil {
  123. return nil
  124. }
  125. body := []byte(`{"error":{"type":"","message":""}}`)
  126. body, _ = sjson.SetBytes(body, "error.type", err.clientErrorType())
  127. body, _ = sjson.SetBytes(body, "error.message", err.clientMessage())
  128. if code := strings.TrimSpace(err.Code); code != "" {
  129. body, _ = sjson.SetBytes(body, "error.code", code)
  130. }
  131. if param := strings.TrimSpace(err.Param); param != "" {
  132. body, _ = sjson.SetBytes(body, "error.param", param)
  133. }
  134. return body
  135. }
  136. func openAIResponsesImageResultKey(itemID string, result openAIResponsesImageResult) string {
  137. if strings.TrimSpace(result.Result) != "" {
  138. return strings.TrimSpace(result.OutputFormat) + "|" + strings.TrimSpace(result.Result)
  139. }
  140. return "item:" + strings.TrimSpace(itemID)
  141. }
  142. func appendOpenAIResponsesImageResultDedup(results *[]openAIResponsesImageResult, seen map[string]struct{}, itemID string, result openAIResponsesImageResult) bool {
  143. if results == nil {
  144. return false
  145. }
  146. key := openAIResponsesImageResultKey(itemID, result)
  147. if key != "" {
  148. if _, exists := seen[key]; exists {
  149. return false
  150. }
  151. seen[key] = struct{}{}
  152. }
  153. *results = append(*results, result)
  154. return true
  155. }
  156. func mergeOpenAIResponsesImageMeta(dst *openAIResponsesImageResult, src openAIResponsesImageResult) {
  157. if dst == nil {
  158. return
  159. }
  160. if trimmed := strings.TrimSpace(src.OutputFormat); trimmed != "" {
  161. dst.OutputFormat = trimmed
  162. }
  163. if trimmed := strings.TrimSpace(src.Size); trimmed != "" {
  164. dst.Size = trimmed
  165. }
  166. if trimmed := strings.TrimSpace(src.Background); trimmed != "" {
  167. dst.Background = trimmed
  168. }
  169. if trimmed := strings.TrimSpace(src.Quality); trimmed != "" {
  170. dst.Quality = trimmed
  171. }
  172. if trimmed := strings.TrimSpace(src.Model); trimmed != "" {
  173. dst.Model = trimmed
  174. }
  175. }
  176. func openAIResponsesImageResultSizes(results []openAIResponsesImageResult) []string {
  177. if len(results) == 0 {
  178. return nil
  179. }
  180. sizes := make([]string, 0, len(results))
  181. for _, result := range results {
  182. if size := strings.TrimSpace(result.Size); size != "" {
  183. sizes = append(sizes, size)
  184. }
  185. }
  186. if len(sizes) == 0 {
  187. return nil
  188. }
  189. return sizes
  190. }
  191. func extractOpenAIResponsesImageMetaFromLifecycleEvent(payload []byte) (openAIResponsesImageResult, int64, bool) {
  192. switch gjson.GetBytes(payload, "type").String() {
  193. case "response.created", "response.in_progress", "response.completed":
  194. default:
  195. return openAIResponsesImageResult{}, 0, false
  196. }
  197. response := gjson.GetBytes(payload, "response")
  198. if !response.Exists() {
  199. return openAIResponsesImageResult{}, 0, false
  200. }
  201. meta := openAIResponsesImageResult{
  202. OutputFormat: strings.TrimSpace(response.Get("tools.0.output_format").String()),
  203. Size: strings.TrimSpace(response.Get("tools.0.size").String()),
  204. Background: strings.TrimSpace(response.Get("tools.0.background").String()),
  205. Quality: strings.TrimSpace(response.Get("tools.0.quality").String()),
  206. Model: strings.TrimSpace(response.Get("tools.0.model").String()),
  207. }
  208. return meta, response.Get("created_at").Int(), true
  209. }
  210. func buildOpenAIImagesStreamPartialPayload(
  211. eventType string,
  212. b64 string,
  213. partialImageIndex int64,
  214. responseFormat string,
  215. createdAt int64,
  216. meta openAIResponsesImageResult,
  217. ) []byte {
  218. if createdAt <= 0 {
  219. createdAt = time.Now().Unix()
  220. }
  221. payload := []byte(`{"type":"","created_at":0,"partial_image_index":0,"b64_json":""}`)
  222. payload, _ = sjson.SetBytes(payload, "type", eventType)
  223. payload, _ = sjson.SetBytes(payload, "created_at", createdAt)
  224. payload, _ = sjson.SetBytes(payload, "partial_image_index", partialImageIndex)
  225. payload, _ = sjson.SetBytes(payload, "b64_json", b64)
  226. if strings.EqualFold(strings.TrimSpace(responseFormat), "url") {
  227. payload, _ = sjson.SetBytes(payload, "url", "data:"+openAIImageOutputMIMEType(meta.OutputFormat)+";base64,"+b64)
  228. }
  229. if meta.Background != "" {
  230. payload, _ = sjson.SetBytes(payload, "background", meta.Background)
  231. }
  232. if meta.OutputFormat != "" {
  233. payload, _ = sjson.SetBytes(payload, "output_format", meta.OutputFormat)
  234. }
  235. if meta.Quality != "" {
  236. payload, _ = sjson.SetBytes(payload, "quality", meta.Quality)
  237. }
  238. if meta.Size != "" {
  239. payload, _ = sjson.SetBytes(payload, "size", meta.Size)
  240. }
  241. if meta.Model != "" {
  242. payload, _ = sjson.SetBytes(payload, "model", meta.Model)
  243. }
  244. return payload
  245. }
  246. func buildOpenAIImagesStreamCompletedPayload(
  247. eventType string,
  248. img openAIResponsesImageResult,
  249. responseFormat string,
  250. createdAt int64,
  251. usageRaw []byte,
  252. ) []byte {
  253. if createdAt <= 0 {
  254. createdAt = time.Now().Unix()
  255. }
  256. payload := []byte(`{"type":"","created_at":0,"b64_json":""}`)
  257. payload, _ = sjson.SetBytes(payload, "type", eventType)
  258. payload, _ = sjson.SetBytes(payload, "created_at", createdAt)
  259. payload, _ = sjson.SetBytes(payload, "b64_json", img.Result)
  260. if strings.EqualFold(strings.TrimSpace(responseFormat), "url") {
  261. payload, _ = sjson.SetBytes(payload, "url", "data:"+openAIImageOutputMIMEType(img.OutputFormat)+";base64,"+img.Result)
  262. }
  263. if img.Background != "" {
  264. payload, _ = sjson.SetBytes(payload, "background", img.Background)
  265. }
  266. if img.OutputFormat != "" {
  267. payload, _ = sjson.SetBytes(payload, "output_format", img.OutputFormat)
  268. }
  269. if img.Quality != "" {
  270. payload, _ = sjson.SetBytes(payload, "quality", img.Quality)
  271. }
  272. if img.Size != "" {
  273. payload, _ = sjson.SetBytes(payload, "size", img.Size)
  274. }
  275. if img.Model != "" {
  276. payload, _ = sjson.SetBytes(payload, "model", img.Model)
  277. }
  278. if len(usageRaw) > 0 && gjson.ValidBytes(usageRaw) {
  279. payload, _ = sjson.SetRawBytes(payload, "usage", usageRaw)
  280. }
  281. return payload
  282. }
  283. func openAIImageOutputMIMEType(outputFormat string) string {
  284. if outputFormat == "" {
  285. return "image/png"
  286. }
  287. if strings.Contains(outputFormat, "/") {
  288. return outputFormat
  289. }
  290. switch strings.ToLower(strings.TrimSpace(outputFormat)) {
  291. case "png":
  292. return "image/png"
  293. case "jpg", "jpeg":
  294. return "image/jpeg"
  295. case "webp":
  296. return "image/webp"
  297. default:
  298. return "image/png"
  299. }
  300. }
  301. func openAIImageUploadToDataURL(upload OpenAIImagesUpload) (string, error) {
  302. if len(upload.Data) == 0 {
  303. return "", fmt.Errorf("upload %q is empty", strings.TrimSpace(upload.FileName))
  304. }
  305. contentType := strings.TrimSpace(upload.ContentType)
  306. if contentType == "" {
  307. contentType = http.DetectContentType(upload.Data)
  308. }
  309. return "data:" + contentType + ";base64," + base64.StdEncoding.EncodeToString(upload.Data), nil
  310. }
  311. // openAIImagesSelfBuiltRequestContextKey marks a request whose upstream body was
  312. // fully constructed by buildOpenAIImagesResponsesRequest, i.e. tool_choice and the
  313. // matching image_generation tool are always both present and never client-controlled.
  314. type openAIImagesSelfBuiltRequestContextKey struct{}
  315. func withOpenAIImagesSelfBuiltRequest(ctx context.Context) context.Context {
  316. if ctx == nil {
  317. ctx = context.Background()
  318. }
  319. return context.WithValue(ctx, openAIImagesSelfBuiltRequestContextKey{}, true)
  320. }
  321. func isOpenAIImagesSelfBuiltRequest(ctx context.Context) bool {
  322. if ctx == nil {
  323. return false
  324. }
  325. selfBuilt, _ := ctx.Value(openAIImagesSelfBuiltRequestContextKey{}).(bool)
  326. return selfBuilt
  327. }
  328. func buildOpenAIImagesResponsesRequest(parsed *OpenAIImagesRequest, toolModel string) ([]byte, error) {
  329. if parsed == nil {
  330. return nil, fmt.Errorf("parsed images request is required")
  331. }
  332. prompt := strings.TrimSpace(parsed.Prompt)
  333. if prompt == "" {
  334. return nil, fmt.Errorf("prompt is required")
  335. }
  336. inputImages := make([]string, 0, len(parsed.InputImageURLs)+len(parsed.Uploads))
  337. for _, imageURL := range parsed.InputImageURLs {
  338. if trimmed := strings.TrimSpace(imageURL); trimmed != "" {
  339. inputImages = append(inputImages, trimmed)
  340. }
  341. }
  342. for _, upload := range parsed.Uploads {
  343. dataURL, err := openAIImageUploadToDataURL(upload)
  344. if err != nil {
  345. return nil, err
  346. }
  347. inputImages = append(inputImages, dataURL)
  348. }
  349. if parsed.IsEdits() && len(inputImages) == 0 {
  350. return nil, fmt.Errorf("image input is required")
  351. }
  352. req := []byte(`{"instructions":"","stream":true,"reasoning":{"effort":"medium","summary":"auto"},"parallel_tool_calls":true,"include":["reasoning.encrypted_content"],"model":"","store":false,"tool_choice":{"type":"image_generation"}}`)
  353. req, _ = sjson.SetBytes(req, "model", openAIImagesResponsesMainModelValue())
  354. req, _ = sjson.SetBytes(req, "instructions", openAIImagesVerbatimPromptInstructions)
  355. input := []byte(`[{"type":"message","role":"user","content":[{"type":"input_text","text":""}]}]`)
  356. input, _ = sjson.SetBytes(input, "0.content.0.text", prompt)
  357. for index, imageURL := range inputImages {
  358. part := []byte(`{"type":"input_image","image_url":""}`)
  359. part, _ = sjson.SetBytes(part, "image_url", imageURL)
  360. input, _ = sjson.SetRawBytes(input, fmt.Sprintf("0.content.%d", index+1), part)
  361. }
  362. req, _ = sjson.SetRawBytes(req, "input", input)
  363. action := "generate"
  364. if parsed.IsEdits() {
  365. action = "edit"
  366. }
  367. tool := []byte(`{"type":"image_generation","action":"","model":""}`)
  368. tool, _ = sjson.SetBytes(tool, "action", action)
  369. tool, _ = sjson.SetBytes(tool, "model", strings.TrimSpace(toolModel))
  370. if shouldPassOpenAIImagesN(toolModel, parsed.N) {
  371. tool, _ = sjson.SetBytes(tool, "n", parsed.N)
  372. }
  373. for _, field := range []struct {
  374. path string
  375. value string
  376. }{
  377. {path: "size", value: parsed.Size},
  378. {path: "quality", value: parsed.Quality},
  379. {path: "background", value: parsed.Background},
  380. {path: "output_format", value: parsed.OutputFormat},
  381. {path: "moderation", value: parsed.Moderation},
  382. {path: "style", value: parsed.Style},
  383. } {
  384. if trimmed := strings.TrimSpace(field.value); trimmed != "" {
  385. tool, _ = sjson.SetBytes(tool, field.path, trimmed)
  386. }
  387. }
  388. if parsed.OutputCompression != nil {
  389. tool, _ = sjson.SetBytes(tool, "output_compression", *parsed.OutputCompression)
  390. }
  391. if parsed.PartialImages != nil {
  392. tool, _ = sjson.SetBytes(tool, "partial_images", *parsed.PartialImages)
  393. }
  394. maskImageURL := strings.TrimSpace(parsed.MaskImageURL)
  395. if parsed.MaskUpload != nil {
  396. dataURL, err := openAIImageUploadToDataURL(*parsed.MaskUpload)
  397. if err != nil {
  398. return nil, err
  399. }
  400. maskImageURL = dataURL
  401. }
  402. if maskImageURL != "" {
  403. tool, _ = sjson.SetBytes(tool, "input_image_mask.image_url", maskImageURL)
  404. }
  405. req, _ = sjson.SetRawBytes(req, "tools", []byte(`[]`))
  406. req, _ = sjson.SetRawBytes(req, "tools.-1", tool)
  407. return req, nil
  408. }
  409. func shouldPassOpenAIImagesN(model string, n int) bool {
  410. if n <= 1 {
  411. return false
  412. }
  413. return !strings.EqualFold(strings.TrimSpace(model), "dall-e-3")
  414. }
  415. func extractOpenAIImagesFromResponsesCompleted(payload []byte) ([]openAIResponsesImageResult, int64, []byte, openAIResponsesImageResult, error) {
  416. if gjson.GetBytes(payload, "type").String() != "response.completed" {
  417. return nil, 0, nil, openAIResponsesImageResult{}, fmt.Errorf("unexpected event type")
  418. }
  419. createdAt := gjson.GetBytes(payload, "response.created_at").Int()
  420. if createdAt <= 0 {
  421. createdAt = time.Now().Unix()
  422. }
  423. var (
  424. results []openAIResponsesImageResult
  425. firstMeta openAIResponsesImageResult
  426. )
  427. output := gjson.GetBytes(payload, "response.output")
  428. if output.IsArray() {
  429. for _, item := range output.Array() {
  430. if item.Get("type").String() != "image_generation_call" {
  431. continue
  432. }
  433. result := strings.TrimSpace(item.Get("result").String())
  434. if result == "" {
  435. continue
  436. }
  437. entry := openAIResponsesImageResult{
  438. Result: result,
  439. RevisedPrompt: strings.TrimSpace(item.Get("revised_prompt").String()),
  440. OutputFormat: strings.TrimSpace(item.Get("output_format").String()),
  441. Size: strings.TrimSpace(item.Get("size").String()),
  442. Background: strings.TrimSpace(item.Get("background").String()),
  443. Quality: strings.TrimSpace(item.Get("quality").String()),
  444. }
  445. if len(results) == 0 {
  446. firstMeta = entry
  447. }
  448. results = append(results, entry)
  449. }
  450. }
  451. var usageRaw []byte
  452. if usage := gjson.GetBytes(payload, "response.tool_usage.image_gen"); usage.Exists() && usage.IsObject() {
  453. usageRaw = []byte(usage.Raw)
  454. }
  455. return results, createdAt, usageRaw, firstMeta, nil
  456. }
  457. func extractOpenAIImageFromResponsesOutputItemDone(payload []byte) (openAIResponsesImageResult, string, bool, error) {
  458. if gjson.GetBytes(payload, "type").String() != "response.output_item.done" {
  459. return openAIResponsesImageResult{}, "", false, fmt.Errorf("unexpected event type")
  460. }
  461. item := gjson.GetBytes(payload, "item")
  462. if !item.Exists() || item.Get("type").String() != "image_generation_call" {
  463. return openAIResponsesImageResult{}, "", false, nil
  464. }
  465. result := strings.TrimSpace(item.Get("result").String())
  466. if result == "" {
  467. return openAIResponsesImageResult{}, "", false, nil
  468. }
  469. entry := openAIResponsesImageResult{
  470. Result: result,
  471. RevisedPrompt: strings.TrimSpace(item.Get("revised_prompt").String()),
  472. OutputFormat: strings.TrimSpace(item.Get("output_format").String()),
  473. Size: strings.TrimSpace(item.Get("size").String()),
  474. Background: strings.TrimSpace(item.Get("background").String()),
  475. Quality: strings.TrimSpace(item.Get("quality").String()),
  476. }
  477. return entry, strings.TrimSpace(item.Get("id").String()), true, nil
  478. }
  479. func collectOpenAIImagesFromResponsesBody(body []byte) ([]openAIResponsesImageResult, int64, []byte, openAIResponsesImageResult, bool, error) {
  480. var (
  481. fallbackResults []openAIResponsesImageResult
  482. fallbackSeen = make(map[string]struct{})
  483. finalResults []openAIResponsesImageResult
  484. finalMeta openAIResponsesImageResult
  485. collectErr error
  486. createdAt int64
  487. usageRaw []byte
  488. foundFinal bool
  489. responseMeta openAIResponsesImageResult
  490. )
  491. forEachOpenAISSEDataPayload(string(body), func(payload []byte) {
  492. if collectErr != nil || len(finalResults) > 0 {
  493. return
  494. }
  495. if !gjson.ValidBytes(payload) {
  496. return
  497. }
  498. if meta, eventCreatedAt, ok := extractOpenAIResponsesImageMetaFromLifecycleEvent(payload); ok {
  499. mergeOpenAIResponsesImageMeta(&responseMeta, meta)
  500. if eventCreatedAt > 0 {
  501. createdAt = eventCreatedAt
  502. }
  503. }
  504. switch gjson.GetBytes(payload, "type").String() {
  505. case "response.output_item.done":
  506. result, itemID, ok, err := extractOpenAIImageFromResponsesOutputItemDone(payload)
  507. if err != nil {
  508. collectErr = err
  509. return
  510. }
  511. if ok {
  512. mergeOpenAIResponsesImageMeta(&result, responseMeta)
  513. appendOpenAIResponsesImageResultDedup(&fallbackResults, fallbackSeen, itemID, result)
  514. }
  515. case "response.completed":
  516. results, completedAt, completedUsageRaw, firstMeta, err := extractOpenAIImagesFromResponsesCompleted(payload)
  517. if err != nil {
  518. collectErr = err
  519. return
  520. }
  521. foundFinal = true
  522. if completedAt > 0 {
  523. createdAt = completedAt
  524. }
  525. if len(completedUsageRaw) > 0 {
  526. usageRaw = completedUsageRaw
  527. }
  528. if len(results) > 0 {
  529. mergeOpenAIResponsesImageMeta(&firstMeta, responseMeta)
  530. finalResults = results
  531. finalMeta = firstMeta
  532. return
  533. }
  534. if len(fallbackResults) > 0 {
  535. firstMeta = fallbackResults[0]
  536. mergeOpenAIResponsesImageMeta(&firstMeta, responseMeta)
  537. finalResults = fallbackResults
  538. finalMeta = firstMeta
  539. return
  540. }
  541. }
  542. })
  543. if collectErr != nil {
  544. return nil, 0, nil, openAIResponsesImageResult{}, false, collectErr
  545. }
  546. if len(finalResults) > 0 {
  547. reconcileOpenAIResponsesImageResultSizes(finalResults, &finalMeta)
  548. return finalResults, createdAt, usageRaw, finalMeta, true, nil
  549. }
  550. if len(fallbackResults) > 0 {
  551. firstMeta := fallbackResults[0]
  552. mergeOpenAIResponsesImageMeta(&firstMeta, responseMeta)
  553. reconcileOpenAIResponsesImageResultSizes(fallbackResults, &firstMeta)
  554. return fallbackResults, createdAt, usageRaw, firstMeta, foundFinal, nil
  555. }
  556. return nil, createdAt, usageRaw, openAIResponsesImageResult{}, foundFinal, nil
  557. }
  558. func extractOpenAIImagesUpstreamError(body []byte) *OpenAIImagesUpstreamError {
  559. var upstreamErr *OpenAIImagesUpstreamError
  560. forEachOpenAISSEDataPayload(string(body), func(payload []byte) {
  561. if upstreamErr != nil || !gjson.ValidBytes(payload) {
  562. return
  563. }
  564. upstreamErr = openAIImagesUpstreamErrorFromSSEPayload(payload)
  565. })
  566. return upstreamErr
  567. }
  568. func openAIImagesUpstreamErrorFromSSEPayload(payload []byte) *OpenAIImagesUpstreamError {
  569. if !gjson.ValidBytes(payload) {
  570. return nil
  571. }
  572. switch gjson.GetBytes(payload, "type").String() {
  573. case "error":
  574. return openAIImagesUpstreamErrorFromGJSON(gjson.GetBytes(payload, "error"), "")
  575. case "response.failed":
  576. response := gjson.GetBytes(payload, "response")
  577. return openAIImagesUpstreamErrorFromGJSON(response.Get("error"), response.Get("id").String())
  578. case "response.incomplete":
  579. // 上游在生成预算内未产出图片(超时/被截断),返回 response.incomplete 而非 error。
  580. // 旧逻辑识别不到,统一报成模糊的 "upstream did not return image output" + 502,
  581. // 且不触发 failover。这里把它显式建模为可重试的上游错误,使其能换账号重试。
  582. return openAIImagesIncompleteUpstreamError(gjson.GetBytes(payload, "response"))
  583. default:
  584. return nil
  585. }
  586. }
  587. // extractOpenAIImagesModelText collects textual terminal output from an image
  588. // request. A text response is evidence that the image tool did not produce an
  589. // image; its semantics still need classification before choosing a client error.
  590. func extractOpenAIImagesModelText(body []byte) string {
  591. var b strings.Builder
  592. collect := func(s string) {
  593. if s = strings.TrimSpace(s); s != "" {
  594. if b.Len() > 0 {
  595. _ = b.WriteByte(' ')
  596. }
  597. _, _ = b.WriteString(s)
  598. }
  599. }
  600. consumePayload := func(payload []byte) {
  601. if !gjson.ValidBytes(payload) {
  602. return
  603. }
  604. switch gjson.GetBytes(payload, "type").String() {
  605. case "response.output_text.delta":
  606. collect(gjson.GetBytes(payload, "delta").String())
  607. case "response.completed", "response.output_item.done":
  608. gjson.GetBytes(payload, "response.output").ForEach(func(_, item gjson.Result) bool {
  609. if item.Get("type").String() == "message" {
  610. item.Get("content").ForEach(func(_, part gjson.Result) bool {
  611. if part.Get("type").String() == "output_text" {
  612. collect(part.Get("text").String())
  613. }
  614. return true
  615. })
  616. }
  617. return true
  618. })
  619. if item := gjson.GetBytes(payload, "item"); item.Get("type").String() == "message" {
  620. item.Get("content").ForEach(func(_, part gjson.Result) bool {
  621. if part.Get("type").String() == "output_text" {
  622. collect(part.Get("text").String())
  623. }
  624. return true
  625. })
  626. }
  627. }
  628. }
  629. if gjson.ValidBytes(body) {
  630. consumePayload(body)
  631. } else {
  632. forEachOpenAISSEDataPayload(string(body), consumePayload)
  633. }
  634. text := strings.TrimSpace(b.String())
  635. const maxText = 600
  636. if len(text) > maxText {
  637. return text[:maxText]
  638. }
  639. return text
  640. }
  641. func isOpenAIImagesContentPolicyRefusal(text string) bool {
  642. lower := strings.ToLower(text)
  643. for _, marker := range []string{
  644. "content policy", "content_policy", "content filter", "content_filter",
  645. "safety system", "safety policy", "safety violation", "moderation",
  646. "安全系统", "安全策略", "安全政策", "内容政策", "内容审核", "违规内容", "不适合生成",
  647. } {
  648. if strings.Contains(lower, marker) {
  649. return true
  650. }
  651. }
  652. return false
  653. }
  654. // extractOpenAIImagesModelRefusal returns only text with an explicit safety or
  655. // moderation signal. Plain prompt suggestions are capability failures instead.
  656. func extractOpenAIImagesModelRefusal(body []byte) string {
  657. text := extractOpenAIImagesModelText(body)
  658. if !isOpenAIImagesContentPolicyRefusal(text) {
  659. return ""
  660. }
  661. return text
  662. }
  663. func openAIImagesTextFallbackError(body []byte) *OpenAIImagesUpstreamError {
  664. return openAIImagesTextFallbackErrorForText(extractOpenAIImagesModelText(body))
  665. }
  666. func openAIImagesTextFallbackErrorForText(text string) *OpenAIImagesUpstreamError {
  667. text = strings.TrimSpace(text)
  668. if text == "" {
  669. return nil
  670. }
  671. if isOpenAIImagesContentPolicyRefusal(text) {
  672. return &OpenAIImagesUpstreamError{
  673. StatusCode: http.StatusBadRequest,
  674. ErrorType: "image_generation_user_error",
  675. Code: "content_policy_violation",
  676. Message: sanitizeUpstreamErrorMessage(text),
  677. }
  678. }
  679. return &OpenAIImagesUpstreamError{
  680. StatusCode: http.StatusBadGateway,
  681. ErrorType: "upstream_error",
  682. Code: "image_generation_unavailable",
  683. Message: "Upstream did not execute image generation",
  684. // Inferred from the model's own words, not from an upstream error frame:
  685. // good enough to fail this turn over to another account, not evidence that
  686. // this account's image tool is down for the next 30 minutes.
  687. SynthesizedFromModelText: true,
  688. }
  689. }
  690. // summarizeOpenAIImagesNoOutputBody 从上游 SSE 响应体提取诊断摘要,用于软失败时
  691. // 记录到 ops 日志(上游无图、无标准错误的场景)。提取最终事件类型、response.status、
  692. // incomplete_details.reason,并附 body 截断片段,便于事后定位上游到底返回了什么。
  693. func summarizeOpenAIImagesNoOutputBody(body []byte) string {
  694. var lastType, status, incompleteReason string
  695. forEachOpenAISSEDataPayload(string(body), func(payload []byte) {
  696. if !gjson.ValidBytes(payload) {
  697. return
  698. }
  699. if t := strings.TrimSpace(gjson.GetBytes(payload, "type").String()); t != "" {
  700. lastType = t
  701. }
  702. if resp := gjson.GetBytes(payload, "response"); resp.Exists() {
  703. if s := strings.TrimSpace(resp.Get("status").String()); s != "" {
  704. status = s
  705. }
  706. if r := strings.TrimSpace(resp.Get("incomplete_details.reason").String()); r != "" {
  707. incompleteReason = r
  708. }
  709. }
  710. })
  711. var b strings.Builder
  712. _, _ = b.WriteString("no_image_output")
  713. if lastType != "" {
  714. fmt.Fprintf(&b, " last_event=%s", lastType)
  715. }
  716. if status != "" {
  717. fmt.Fprintf(&b, " status=%s", status)
  718. }
  719. if incompleteReason != "" {
  720. fmt.Fprintf(&b, " incomplete_reason=%s", incompleteReason)
  721. }
  722. // 附 body 截断片段(脱敏后),上限 1KB,避免日志膨胀。
  723. snippet := strings.TrimSpace(string(body))
  724. const maxSnippet = 1024
  725. if len(snippet) > maxSnippet {
  726. snippet = snippet[:maxSnippet] + "...(truncated)"
  727. }
  728. if snippet != "" {
  729. fmt.Fprintf(&b, " body=%s", snippet)
  730. }
  731. return b.String()
  732. }
  733. // openAIImagesIncompleteUpstreamError 从 response.incomplete 事件构建可重试的上游错误。
  734. // incomplete_details.reason 常见取值:max_output_tokens / content_filter 等。
  735. // content_filter 视为客户端错误(400,重试无意义);其余(生成超时/截断)视为
  736. // 可重试的 502,触发 failover 换账号重试。
  737. func openAIImagesIncompleteUpstreamError(response gjson.Result) *OpenAIImagesUpstreamError {
  738. if !response.Exists() {
  739. return nil
  740. }
  741. reason := strings.TrimSpace(response.Get("incomplete_details.reason").String())
  742. statusCode := http.StatusBadGateway // 默认可重试(生成未完成)
  743. errType := "incomplete_error"
  744. if strings.Contains(strings.ToLower(reason), "content_filter") ||
  745. strings.Contains(strings.ToLower(reason), "moderation") {
  746. statusCode = http.StatusBadRequest // 内容过滤,重试无意义
  747. errType = "image_generation_user_error"
  748. }
  749. message := "Upstream did not complete image generation"
  750. if reason != "" {
  751. message = fmt.Sprintf("Upstream image generation incomplete: %s", reason)
  752. }
  753. return &OpenAIImagesUpstreamError{
  754. StatusCode: statusCode,
  755. ErrorType: errType,
  756. Code: "response_incomplete",
  757. Message: sanitizeUpstreamErrorMessage(message),
  758. UpstreamRequestID: strings.TrimSpace(response.Get("id").String()),
  759. }
  760. }
  761. func openAIImagesUpstreamErrorFromGJSON(errorObj gjson.Result, upstreamRequestID string) *OpenAIImagesUpstreamError {
  762. if !errorObj.Exists() {
  763. return nil
  764. }
  765. code := strings.TrimSpace(errorObj.Get("code").String())
  766. errType := strings.TrimSpace(errorObj.Get("type").String())
  767. message := strings.TrimSpace(errorObj.Get("message").String())
  768. param := strings.TrimSpace(errorObj.Get("param").String())
  769. statusCode := openAIImagesSSEErrorStatus(errType, code)
  770. if message == "" {
  771. message = "Upstream request failed"
  772. }
  773. return &OpenAIImagesUpstreamError{
  774. StatusCode: statusCode,
  775. ErrorType: errType,
  776. Code: code,
  777. Message: sanitizeUpstreamErrorMessage(message),
  778. Param: param,
  779. UpstreamRequestID: strings.TrimSpace(upstreamRequestID),
  780. }
  781. }
  782. // openAIImagesErrorTypeForStatus returns an OpenAI-style error type when the
  783. // upstream body does not provide one of its own.
  784. func openAIImagesErrorTypeForStatus(status int) string {
  785. switch {
  786. case status == http.StatusBadRequest:
  787. return "invalid_request_error"
  788. case status == http.StatusUnauthorized:
  789. return "authentication_error"
  790. case status == http.StatusForbidden:
  791. return "permission_error"
  792. case status == http.StatusNotFound:
  793. return "not_found_error"
  794. case status == http.StatusTooManyRequests:
  795. return "rate_limit_error"
  796. case status >= 500:
  797. return "api_error"
  798. default:
  799. return "upstream_error"
  800. }
  801. }
  802. // openAIImagesUpstreamErrorFromHTTP builds an OpenAIImagesUpstreamError from a
  803. // non-2xx upstream HTTP response, preserving the real status code, type, code,
  804. // message and param so the client sees the actual upstream error instead of a
  805. // generic 502.
  806. func openAIImagesUpstreamErrorFromHTTP(statusCode int, header http.Header, body []byte) *OpenAIImagesUpstreamError {
  807. errType := strings.TrimSpace(gjson.GetBytes(body, "error.type").String())
  808. code := strings.TrimSpace(extractUpstreamErrorCode(body))
  809. param := strings.TrimSpace(gjson.GetBytes(body, "error.param").String())
  810. message := sanitizeUpstreamErrorMessage(strings.TrimSpace(extractUpstreamErrorMessage(body)))
  811. if message == "" {
  812. message = fmt.Sprintf("Upstream request failed (status %d)", statusCode)
  813. }
  814. if errType == "" {
  815. errType = openAIImagesErrorTypeForStatus(statusCode)
  816. }
  817. requestID := ""
  818. if header != nil {
  819. requestID = strings.TrimSpace(header.Get("x-request-id"))
  820. }
  821. return &OpenAIImagesUpstreamError{
  822. StatusCode: statusCode,
  823. ErrorType: errType,
  824. Code: code,
  825. Message: message,
  826. Param: param,
  827. UpstreamRequestID: requestID,
  828. }
  829. }
  830. // handleOpenAIImagesErrorResponse is the non-failover error handler for the
  831. // images endpoints (/v1/images/generations and /v1/images/edits). Unlike the
  832. // generic handleErrorResponse — which collapses every non-failover upstream
  833. // error into a generic 502 "Upstream request failed" — it surfaces the real
  834. // upstream status code and error message/type/code/param to the client. This
  835. // mirrors how the Chat Completions and Messages compat paths use
  836. // handleCompatErrorResponse.
  837. //
  838. // It returns an *OpenAIImagesUpstreamError (already written to the client) so
  839. // the images handler treats it as a terminal user-facing error rather than
  840. // re-writing a fallback response.
  841. func (s *OpenAIGatewayService) handleOpenAIImagesErrorResponse(
  842. ctx context.Context,
  843. resp *http.Response,
  844. c *gin.Context,
  845. account *Account,
  846. requestedModel ...string,
  847. ) (*OpenAIForwardResult, error) {
  848. body := s.readUpstreamErrorBody(resp)
  849. upstreamMsg := sanitizeUpstreamErrorMessage(strings.TrimSpace(extractUpstreamErrorMessage(body)))
  850. upstreamDetail := ""
  851. if s.cfg != nil && s.cfg.Gateway.LogUpstreamErrorBody {
  852. maxBytes := s.cfg.Gateway.LogUpstreamErrorBodyMaxBytes
  853. if maxBytes <= 0 {
  854. maxBytes = 2048
  855. }
  856. upstreamDetail = truncateString(string(body), maxBytes)
  857. }
  858. setOpsUpstreamError(c, resp.StatusCode, upstreamMsg, upstreamDetail)
  859. if s.cfg != nil && s.cfg.Gateway.LogUpstreamErrorBody {
  860. logger.LegacyPrintf("service.openai_gateway",
  861. "OpenAI images upstream error %d (account=%d platform=%s type=%s): %s",
  862. resp.StatusCode,
  863. account.ID,
  864. account.Platform,
  865. account.Type,
  866. truncateForLog(body, s.cfg.Gateway.LogUpstreamErrorBodyMaxBytes),
  867. )
  868. }
  869. // Honor admin-configured error passthrough rules first.
  870. if status, errType, errMsg, matched := applyErrorPassthroughRule(
  871. c,
  872. account.Platform,
  873. resp.StatusCode,
  874. body,
  875. http.StatusBadGateway,
  876. "upstream_error",
  877. "Upstream request failed",
  878. ); matched {
  879. upErr := &OpenAIImagesUpstreamError{
  880. StatusCode: status,
  881. ErrorType: errType,
  882. Message: errMsg,
  883. UpstreamRequestID: strings.TrimSpace(resp.Header.Get("x-request-id")),
  884. }
  885. writeOpenAIImagesUpstreamErrorResponse(c, upErr)
  886. return nil, upErr
  887. }
  888. // If the account is not configured to handle this status code, fall back to
  889. // a generic gateway error without exposing upstream internals (mirrors
  890. // handleCompatErrorResponse).
  891. if !account.ShouldHandleErrorCode(resp.StatusCode) {
  892. appendOpsUpstreamError(c, OpsUpstreamErrorEvent{
  893. ProxyID: opsUpstreamProxyID(account),
  894. ProxyName: opsUpstreamProxyName(account),
  895. Platform: account.Platform,
  896. AccountID: account.ID,
  897. AccountName: account.Name,
  898. UpstreamStatusCode: resp.StatusCode,
  899. UpstreamRequestID: resp.Header.Get("x-request-id"),
  900. Kind: "http_error",
  901. Message: upstreamMsg,
  902. Detail: upstreamDetail,
  903. })
  904. upErr := &OpenAIImagesUpstreamError{
  905. StatusCode: http.StatusInternalServerError,
  906. ErrorType: "upstream_error",
  907. Message: "Upstream gateway error",
  908. UpstreamRequestID: strings.TrimSpace(resp.Header.Get("x-request-id")),
  909. }
  910. writeOpenAIImagesUpstreamErrorResponse(c, upErr)
  911. return nil, upErr
  912. }
  913. // A retired/configured Responses driver is not an image-model quota failure.
  914. // Surface the actionable upstream error instead of cooling every image account
  915. // and eventually hiding the configuration problem behind a generic 503.
  916. if account.IsOpenAIOAuthLike() &&
  917. isOpenAICodexPlanGatedModelError(resp.StatusCode, body) &&
  918. strings.Contains(extractUpstreamErrorMessage(body), "'"+openAIImagesResponsesMainModelValue()+"'") {
  919. upErr := openAIImagesUpstreamErrorFromHTTP(resp.StatusCode, resp.Header, body)
  920. writeOpenAIImagesUpstreamErrorResponse(c, upErr)
  921. return nil, upErr
  922. }
  923. // Track rate limits / decide whether to disable the account (secondary failover).
  924. var modelForCooldown string
  925. if len(requestedModel) > 0 {
  926. modelForCooldown = strings.TrimSpace(requestedModel[0])
  927. }
  928. shouldDisable := s.handleOpenAIAccountUpstreamError(ctx, account, resp.StatusCode, resp.Header, body, modelForCooldown)
  929. failoverErr := s.newOpenAIAccountFailoverError(
  930. account,
  931. resp.StatusCode,
  932. resp.Header,
  933. body,
  934. upstreamMsg,
  935. shouldDisable,
  936. false,
  937. )
  938. shouldFailover := shouldDisable || (account.IsOpenAIOAuthLike() && resp.StatusCode == http.StatusTooManyRequests && failoverErr.RetryableOnSameAccount)
  939. kind := "http_error"
  940. if shouldFailover {
  941. kind = "failover"
  942. }
  943. appendOpsUpstreamError(c, OpsUpstreamErrorEvent{
  944. ProxyID: opsUpstreamProxyID(account),
  945. ProxyName: opsUpstreamProxyName(account),
  946. Platform: account.Platform,
  947. AccountID: account.ID,
  948. AccountName: account.Name,
  949. UpstreamStatusCode: resp.StatusCode,
  950. UpstreamRequestID: resp.Header.Get("x-request-id"),
  951. Kind: kind,
  952. Message: upstreamMsg,
  953. Detail: upstreamDetail,
  954. })
  955. if shouldFailover {
  956. return nil, failoverErr
  957. }
  958. // Surface the real upstream error to the client.
  959. upErr := openAIImagesUpstreamErrorFromHTTP(resp.StatusCode, resp.Header, body)
  960. writeOpenAIImagesUpstreamErrorResponse(c, upErr)
  961. return nil, upErr
  962. }
  963. func buildOpenAIImagesAPIResponse(
  964. results []openAIResponsesImageResult,
  965. createdAt int64,
  966. usageRaw []byte,
  967. firstMeta openAIResponsesImageResult,
  968. responseFormat string,
  969. ) ([]byte, error) {
  970. if createdAt <= 0 {
  971. createdAt = time.Now().Unix()
  972. }
  973. out := []byte(`{"created":0,"data":[]}`)
  974. out, _ = sjson.SetBytes(out, "created", createdAt)
  975. format := strings.ToLower(strings.TrimSpace(responseFormat))
  976. if format == "" {
  977. format = "b64_json"
  978. }
  979. for _, img := range results {
  980. item := []byte(`{}`)
  981. if format == "url" {
  982. item, _ = sjson.SetBytes(item, "url", "data:"+openAIImageOutputMIMEType(img.OutputFormat)+";base64,"+img.Result)
  983. } else {
  984. item, _ = sjson.SetBytes(item, "b64_json", img.Result)
  985. }
  986. if img.RevisedPrompt != "" {
  987. item, _ = sjson.SetBytes(item, "revised_prompt", img.RevisedPrompt)
  988. }
  989. out, _ = sjson.SetRawBytes(out, "data.-1", item)
  990. }
  991. if firstMeta.Background != "" {
  992. out, _ = sjson.SetBytes(out, "background", firstMeta.Background)
  993. }
  994. if firstMeta.OutputFormat != "" {
  995. out, _ = sjson.SetBytes(out, "output_format", firstMeta.OutputFormat)
  996. }
  997. if firstMeta.Quality != "" {
  998. out, _ = sjson.SetBytes(out, "quality", firstMeta.Quality)
  999. }
  1000. if firstMeta.Size != "" {
  1001. out, _ = sjson.SetBytes(out, "size", firstMeta.Size)
  1002. }
  1003. if firstMeta.Model != "" {
  1004. out, _ = sjson.SetBytes(out, "model", firstMeta.Model)
  1005. }
  1006. if len(usageRaw) > 0 && gjson.ValidBytes(usageRaw) {
  1007. out, _ = sjson.SetRawBytes(out, "usage", usageRaw)
  1008. }
  1009. return out, nil
  1010. }
  1011. func openAIImagesStreamPrefix(parsed *OpenAIImagesRequest) string {
  1012. if parsed != nil && parsed.IsEdits() {
  1013. return "image_edit"
  1014. }
  1015. return "image_generation"
  1016. }
  1017. func buildOpenAIImagesStreamErrorBody(message string) []byte {
  1018. body := []byte(`{"type":"error","error":{"type":"upstream_error","message":""}}`)
  1019. if strings.TrimSpace(message) == "" {
  1020. message = "upstream request failed"
  1021. }
  1022. body, _ = sjson.SetBytes(body, "error.message", message)
  1023. return body
  1024. }
  1025. func buildOpenAIImagesStreamErrorBodyFromUpstream(err *OpenAIImagesUpstreamError) []byte {
  1026. if err == nil {
  1027. return buildOpenAIImagesStreamErrorBody("")
  1028. }
  1029. body := buildOpenAIImagesStreamErrorBody(err.clientMessage())
  1030. body, _ = sjson.SetBytes(body, "error.type", err.clientErrorType())
  1031. if code := strings.TrimSpace(err.Code); code != "" {
  1032. body, _ = sjson.SetBytes(body, "error.code", code)
  1033. }
  1034. if param := strings.TrimSpace(err.Param); param != "" {
  1035. body, _ = sjson.SetBytes(body, "error.param", param)
  1036. }
  1037. return body
  1038. }
  1039. func writeOpenAIImagesUpstreamErrorResponse(c *gin.Context, err *OpenAIImagesUpstreamError) bool {
  1040. if c == nil || c.Writer == nil || err == nil {
  1041. return false
  1042. }
  1043. if c.Writer.Written() && OpenAIImagesJSONKeepaliveAdjustedWrittenSize(c) >= 0 {
  1044. return false
  1045. }
  1046. StopOpenAIImagesJSONKeepaliveCommitted(c)
  1047. errorObj := gin.H{
  1048. "type": err.clientErrorType(),
  1049. "message": err.clientMessage(),
  1050. }
  1051. if code := strings.TrimSpace(err.Code); code != "" {
  1052. errorObj["code"] = code
  1053. }
  1054. if param := strings.TrimSpace(err.Param); param != "" {
  1055. errorObj["param"] = param
  1056. }
  1057. c.JSON(err.clientStatusCode(), gin.H{
  1058. "error": errorObj,
  1059. })
  1060. return true
  1061. }
  1062. func (s *OpenAIGatewayService) writeOpenAIImagesStreamEvent(c *gin.Context, flusher http.Flusher, eventName string, payload []byte) error {
  1063. if strings.TrimSpace(eventName) != "" {
  1064. if _, err := fmt.Fprintf(c.Writer, "event: %s\n", eventName); err != nil {
  1065. return err
  1066. }
  1067. }
  1068. if _, err := fmt.Fprintf(c.Writer, "data: %s\n\n", payload); err != nil {
  1069. return err
  1070. }
  1071. flusher.Flush()
  1072. return nil
  1073. }
  1074. func (s *OpenAIGatewayService) tryWriteOpenAIImagesStreamEvent(
  1075. c *gin.Context,
  1076. flusher http.Flusher,
  1077. clientDisconnected *bool,
  1078. lastWriteAt *time.Time,
  1079. eventName string,
  1080. payload []byte,
  1081. ) bool {
  1082. if clientDisconnected != nil && *clientDisconnected {
  1083. return false
  1084. }
  1085. if err := s.writeOpenAIImagesStreamEvent(c, flusher, eventName, payload); err != nil {
  1086. if clientDisconnected != nil {
  1087. *clientDisconnected = true
  1088. }
  1089. logger.LegacyPrintf("service.openai_gateway", "[OpenAI] Images stream client disconnected, continue draining upstream for billing")
  1090. return false
  1091. }
  1092. if lastWriteAt != nil {
  1093. *lastWriteAt = time.Now()
  1094. }
  1095. return true
  1096. }
  1097. func (s *OpenAIGatewayService) parseOpenAIImagesSSEUsageBytes(data []byte, usage *OpenAIUsage) {
  1098. s.parseSSEUsageBytes(data, usage)
  1099. if usage == nil || !gjson.ValidBytes(data) || gjson.GetBytes(data, "type").String() != "response.completed" {
  1100. return
  1101. }
  1102. if toolUsage, ok := openAIImagesToolUsageFromGJSON(gjson.GetBytes(data, "response.tool_usage.image_gen")); ok {
  1103. *usage = toolUsage
  1104. }
  1105. }
  1106. func openAIImagesToolUsageFromGJSON(value gjson.Result) (OpenAIUsage, bool) {
  1107. if !value.Exists() || !value.IsObject() {
  1108. return OpenAIUsage{}, false
  1109. }
  1110. inputTokens, inputOK := boundedJSONNonNegativeInt(value.Get("input_tokens"))
  1111. outputTokens, outputOK := boundedJSONNonNegativeInt(value.Get("output_tokens"))
  1112. imageOutputTokens, imageOutputOK := boundedJSONNonNegativeInt(value.Get("output_tokens_details.image_tokens"))
  1113. if !inputOK || !outputOK || !imageOutputOK {
  1114. return OpenAIUsage{}, false
  1115. }
  1116. imageInputTokens, _ := boundedJSONNonNegativeInt(value.Get("input_tokens_details.image_tokens"))
  1117. if imageInputTokens > inputTokens {
  1118. imageInputTokens = inputTokens
  1119. }
  1120. return OpenAIUsage{
  1121. InputTokens: inputTokens,
  1122. ImageInputTokens: imageInputTokens,
  1123. OutputTokens: outputTokens,
  1124. ImageOutputTokens: imageOutputTokens,
  1125. }, true
  1126. }
  1127. // boundedJSONNonNegativeInt parses integral JSON exponent notation without
  1128. // invoking an arbitrary-precision parser on an upstream-controlled exponent.
  1129. func boundedJSONNonNegativeInt(value gjson.Result) (int, bool) {
  1130. if !value.Exists() || value.Type != gjson.Number {
  1131. return 0, false
  1132. }
  1133. raw := value.Raw
  1134. if len(raw) == 0 || len(raw) > 64 || raw[0] == '-' {
  1135. return 0, false
  1136. }
  1137. mantissaEnd := len(raw)
  1138. for i, c := range raw {
  1139. if c != 'e' && c != 'E' {
  1140. continue
  1141. }
  1142. mantissaEnd = i
  1143. break
  1144. }
  1145. digits := raw[:mantissaEnd]
  1146. fractionDigits := 0
  1147. digitCount := 0
  1148. dotSeen := false
  1149. mantissaIsZero := true
  1150. for _, c := range digits {
  1151. switch {
  1152. case c == '.' && !dotSeen:
  1153. dotSeen = true
  1154. case c >= '0' && c <= '9':
  1155. digitCount++
  1156. mantissaIsZero = mantissaIsZero && c == '0'
  1157. if dotSeen {
  1158. fractionDigits++
  1159. }
  1160. default:
  1161. return 0, false
  1162. }
  1163. }
  1164. exponent := 0
  1165. if mantissaEnd < len(raw) {
  1166. exponentRaw := raw[mantissaEnd+1:]
  1167. negative := false
  1168. if len(exponentRaw) > 0 && (exponentRaw[0] == '+' || exponentRaw[0] == '-') {
  1169. negative = exponentRaw[0] == '-'
  1170. exponentRaw = exponentRaw[1:]
  1171. }
  1172. if len(exponentRaw) == 0 {
  1173. return 0, false
  1174. }
  1175. for len(exponentRaw) > 1 && exponentRaw[0] == '0' {
  1176. exponentRaw = exponentRaw[1:]
  1177. }
  1178. for _, digit := range exponentRaw {
  1179. if digit < '0' || digit > '9' {
  1180. return 0, false
  1181. }
  1182. }
  1183. if mantissaIsZero {
  1184. return 0, true
  1185. }
  1186. if len(exponentRaw) > 3 {
  1187. return 0, false
  1188. }
  1189. for _, digit := range exponentRaw {
  1190. exponent = exponent*10 + int(digit-'0')
  1191. }
  1192. if exponent > 100 {
  1193. return 0, false
  1194. }
  1195. if negative {
  1196. exponent = -exponent
  1197. }
  1198. }
  1199. trailingZeros := exponent - fractionDigits
  1200. scaleReduction := 0
  1201. if trailingZeros < 0 {
  1202. scaleReduction = -trailingZeros
  1203. remaining := scaleReduction
  1204. allZeros := true
  1205. for i := len(digits) - 1; i >= 0; i-- {
  1206. if digits[i] == '.' {
  1207. continue
  1208. }
  1209. if digits[i] != '0' {
  1210. allZeros = false
  1211. if remaining > 0 {
  1212. return 0, false
  1213. }
  1214. }
  1215. if remaining > 0 {
  1216. remaining--
  1217. }
  1218. }
  1219. if remaining > 0 {
  1220. if allZeros {
  1221. return 0, true
  1222. }
  1223. return 0, false
  1224. }
  1225. }
  1226. maxInt := int(^uint(0) >> 1)
  1227. parsed := 0
  1228. digitsToAccumulate := digitCount - scaleReduction
  1229. for _, c := range digits {
  1230. if c == '.' {
  1231. continue
  1232. }
  1233. if digitsToAccumulate <= 0 {
  1234. break
  1235. }
  1236. if parsed > (maxInt-int(c-'0'))/10 {
  1237. return 0, false
  1238. }
  1239. parsed = parsed*10 + int(c-'0')
  1240. digitsToAccumulate--
  1241. }
  1242. if trailingZeros < 0 {
  1243. return parsed, true
  1244. }
  1245. for ; trailingZeros > 0; trailingZeros-- {
  1246. if parsed > maxInt/10 {
  1247. return 0, false
  1248. }
  1249. parsed *= 10
  1250. }
  1251. return parsed, true
  1252. }
  1253. func (s *OpenAIGatewayService) handleOpenAIImagesOAuthNonStreamingResponse(
  1254. resp *http.Response,
  1255. c *gin.Context,
  1256. responseFormat string,
  1257. fallbackModel string,
  1258. ) (OpenAIUsage, int, []string, error) {
  1259. body, err := ReadUpstreamResponseBody(resp.Body, s.cfg, c, openAITooLargeError)
  1260. if err != nil {
  1261. if shouldClassifyOpenAIUpstreamStreamReadError(err, c.Request.Context()) {
  1262. err = newOpenAIUpstreamStreamReadError(err)
  1263. }
  1264. return OpenAIUsage{}, 0, nil, err
  1265. }
  1266. var usage OpenAIUsage
  1267. forEachOpenAISSEDataPayload(string(body), func(data []byte) {
  1268. s.parseOpenAIImagesSSEUsageBytes(data, &usage)
  1269. })
  1270. results, createdAt, usageRaw, firstMeta, _, err := collectOpenAIImagesFromResponsesBody(body)
  1271. if err != nil {
  1272. return OpenAIUsage{}, 0, nil, err
  1273. }
  1274. if len(results) == 0 {
  1275. if upstreamErr := extractOpenAIImagesUpstreamError(body); upstreamErr != nil {
  1276. setOpsUpstreamError(c, upstreamErr.clientStatusCode(), upstreamErr.clientMessage(), "")
  1277. if !IsOpenAIImagesRetryableUpstreamError(upstreamErr) {
  1278. writeOpenAIImagesUpstreamErrorResponse(c, upstreamErr)
  1279. }
  1280. return OpenAIUsage{}, 0, nil, upstreamErr
  1281. }
  1282. if textFallbackErr := openAIImagesTextFallbackError(body); textFallbackErr != nil {
  1283. setOpsUpstreamError(c, textFallbackErr.clientStatusCode(), textFallbackErr.clientMessage(), summarizeOpenAIImagesNoOutputBody(body))
  1284. if !IsOpenAIImagesRetryableUpstreamError(textFallbackErr) {
  1285. writeOpenAIImagesUpstreamErrorResponse(c, textFallbackErr)
  1286. }
  1287. return OpenAIUsage{}, 0, nil, textFallbackErr
  1288. }
  1289. // 真空响应:既无图也无文字输出。它保持短暂可重试语义,优先同账号重试。
  1290. // (B) 真空响应:记录上游诊断摘要到 ops(last_event/status/model/body 片段)便于
  1291. // 排查,并返回 UpstreamFailoverError 触发重试。因实测为「同账号概率性失败」,优先
  1292. // RetryableOnSameAccount 同账号快速重试(默认 3 次,大概率某次正常出图),用尽后
  1293. // 由 handler 自然换账号 failover(switchCount 上限保护),既提高成功率又不无谓
  1294. // 消耗其它账号配额。
  1295. setOpsUpstreamError(c, http.StatusBadGateway, "upstream did not return image output", summarizeOpenAIImagesNoOutputBody(body))
  1296. return OpenAIUsage{}, 0, nil, &UpstreamFailoverError{
  1297. StatusCode: http.StatusBadGateway,
  1298. ResponseBody: body,
  1299. RetryableOnSameAccount: true,
  1300. }
  1301. }
  1302. if strings.TrimSpace(firstMeta.Model) == "" {
  1303. firstMeta.Model = strings.TrimSpace(fallbackModel)
  1304. }
  1305. responseBody, err := buildOpenAIImagesAPIResponse(results, createdAt, usageRaw, firstMeta, responseFormat)
  1306. if err != nil {
  1307. return OpenAIUsage{}, 0, nil, err
  1308. }
  1309. responseheaders.WriteFilteredHeaders(c.Writer.Header(), resp.Header, s.responseHeaderFilter)
  1310. c.Data(resp.StatusCode, "application/json; charset=utf-8", responseBody)
  1311. return usage, len(results), openAIResponsesImageResultSizes(results), nil
  1312. }
  1313. func (s *OpenAIGatewayService) handleOpenAIImagesOAuthStreamingResponse(
  1314. resp *http.Response,
  1315. c *gin.Context,
  1316. startTime time.Time,
  1317. responseFormat string,
  1318. streamPrefix string,
  1319. fallbackModel string,
  1320. ) (OpenAIUsage, int, []string, *int, error) {
  1321. responseheaders.WriteFilteredHeaders(c.Writer.Header(), resp.Header, s.responseHeaderFilter)
  1322. c.Header("Content-Type", "text/event-stream")
  1323. c.Header("Cache-Control", "no-cache")
  1324. c.Header("Connection", "keep-alive")
  1325. c.Status(resp.StatusCode)
  1326. flusher, ok := c.Writer.(http.Flusher)
  1327. if !ok {
  1328. return OpenAIUsage{}, 0, nil, nil, fmt.Errorf("streaming is not supported by response writer")
  1329. }
  1330. format := strings.ToLower(strings.TrimSpace(responseFormat))
  1331. if format == "" {
  1332. format = "b64_json"
  1333. }
  1334. usage := OpenAIUsage{}
  1335. imageCount := 0
  1336. var imageOutputSizes []string
  1337. var firstTokenMs *int
  1338. emitted := make(map[string]struct{})
  1339. pendingResults := make([]openAIResponsesImageResult, 0, 1)
  1340. pendingSeen := make(map[string]struct{})
  1341. streamMeta := openAIResponsesImageResult{Model: strings.TrimSpace(fallbackModel)}
  1342. var fallbackText strings.Builder
  1343. appendFallbackText := func(text string) {
  1344. if text == "" || fallbackText.Len() >= 600 {
  1345. return
  1346. }
  1347. remaining := 600 - fallbackText.Len()
  1348. if len(text) > remaining {
  1349. text = text[:remaining]
  1350. }
  1351. _, _ = fallbackText.WriteString(text)
  1352. }
  1353. var createdAt int64
  1354. clientDisconnected := false
  1355. lastDownstreamWriteAt := time.Now()
  1356. var sseData openAISSEDataAccumulator
  1357. var processDataErr error
  1358. processDataDone := false
  1359. writerSizeBeforeResponse := OpenAIImagesJSONKeepaliveAdjustedWrittenSize(c)
  1360. processData := func(dataBytes []byte) {
  1361. if processDataDone || processDataErr != nil {
  1362. return
  1363. }
  1364. if firstTokenMs == nil {
  1365. ms := int(time.Since(startTime).Milliseconds())
  1366. firstTokenMs = &ms
  1367. }
  1368. s.parseOpenAIImagesSSEUsageBytes(dataBytes, &usage)
  1369. if !gjson.ValidBytes(dataBytes) {
  1370. return
  1371. }
  1372. if meta, eventCreatedAt, ok := extractOpenAIResponsesImageMetaFromLifecycleEvent(dataBytes); ok {
  1373. mergeOpenAIResponsesImageMeta(&streamMeta, meta)
  1374. if eventCreatedAt > 0 {
  1375. createdAt = eventCreatedAt
  1376. }
  1377. }
  1378. if gjson.GetBytes(dataBytes, "type").String() == "response.output_text.delta" {
  1379. appendFallbackText(gjson.GetBytes(dataBytes, "delta").String())
  1380. }
  1381. switch gjson.GetBytes(dataBytes, "type").String() {
  1382. case "response.image_generation_call.partial_image":
  1383. b64 := strings.TrimSpace(gjson.GetBytes(dataBytes, "partial_image_b64").String())
  1384. if b64 == "" {
  1385. return
  1386. }
  1387. eventName := streamPrefix + ".partial_image"
  1388. partialMeta := streamMeta
  1389. mergeOpenAIResponsesImageMeta(&partialMeta, openAIResponsesImageResult{
  1390. OutputFormat: strings.TrimSpace(gjson.GetBytes(dataBytes, "output_format").String()),
  1391. Background: strings.TrimSpace(gjson.GetBytes(dataBytes, "background").String()),
  1392. })
  1393. payload := buildOpenAIImagesStreamPartialPayload(
  1394. eventName,
  1395. b64,
  1396. gjson.GetBytes(dataBytes, "partial_image_index").Int(),
  1397. format,
  1398. createdAt,
  1399. partialMeta,
  1400. )
  1401. s.tryWriteOpenAIImagesStreamEvent(c, flusher, &clientDisconnected, &lastDownstreamWriteAt, eventName, payload)
  1402. case "response.output_item.done":
  1403. img, itemID, ok, extractErr := extractOpenAIImageFromResponsesOutputItemDone(dataBytes)
  1404. if extractErr != nil {
  1405. s.tryWriteOpenAIImagesStreamEvent(c, flusher, &clientDisconnected, &lastDownstreamWriteAt, "error", buildOpenAIImagesStreamErrorBody(extractErr.Error()))
  1406. processDataErr = extractErr
  1407. processDataDone = true
  1408. return
  1409. }
  1410. if !ok {
  1411. return
  1412. }
  1413. mergeOpenAIResponsesImageMeta(&streamMeta, img)
  1414. mergeOpenAIResponsesImageMeta(&img, streamMeta)
  1415. key := openAIResponsesImageResultKey(itemID, img)
  1416. if _, exists := emitted[key]; exists {
  1417. return
  1418. }
  1419. if _, exists := pendingSeen[key]; exists {
  1420. return
  1421. }
  1422. pendingSeen[key] = struct{}{}
  1423. pendingResults = append(pendingResults, img)
  1424. case "response.completed":
  1425. results, _, usageRaw, firstMeta, extractErr := extractOpenAIImagesFromResponsesCompleted(dataBytes)
  1426. if extractErr != nil {
  1427. s.tryWriteOpenAIImagesStreamEvent(c, flusher, &clientDisconnected, &lastDownstreamWriteAt, "error", buildOpenAIImagesStreamErrorBody(extractErr.Error()))
  1428. processDataErr = extractErr
  1429. processDataDone = true
  1430. return
  1431. }
  1432. mergeOpenAIResponsesImageMeta(&streamMeta, firstMeta)
  1433. finalResults := make([]openAIResponsesImageResult, 0, len(results)+len(pendingResults))
  1434. finalSeen := make(map[string]struct{})
  1435. for _, img := range results {
  1436. mergeOpenAIResponsesImageMeta(&img, streamMeta)
  1437. appendOpenAIResponsesImageResultDedup(&finalResults, finalSeen, "", img)
  1438. }
  1439. for _, img := range pendingResults {
  1440. mergeOpenAIResponsesImageMeta(&img, streamMeta)
  1441. appendOpenAIResponsesImageResultDedup(&finalResults, finalSeen, "", img)
  1442. }
  1443. reconcileOpenAIResponsesImageResultSizes(finalResults, nil)
  1444. if len(finalResults) == 0 {
  1445. textFallbackErr := openAIImagesTextFallbackErrorForText(fallbackText.String())
  1446. if textFallbackErr == nil {
  1447. textFallbackErr = openAIImagesTextFallbackError(dataBytes)
  1448. }
  1449. if textFallbackErr != nil {
  1450. retryable := IsOpenAIImagesRetryableUpstreamError(textFallbackErr)
  1451. setOpsUpstreamError(c, textFallbackErr.clientStatusCode(), textFallbackErr.clientMessage(), summarizeOpenAIImagesNoOutputBody(dataBytes))
  1452. if !retryable && !clientDisconnected {
  1453. s.tryWriteOpenAIImagesStreamEvent(c, flusher, &clientDisconnected, &lastDownstreamWriteAt, "error", buildOpenAIImagesStreamErrorBodyFromUpstream(textFallbackErr))
  1454. }
  1455. processDataErr = textFallbackErr
  1456. processDataDone = true
  1457. return
  1458. }
  1459. outputErr := fmt.Errorf("upstream did not return image output")
  1460. setOpsUpstreamError(c, http.StatusBadGateway, "upstream did not return image output", summarizeOpenAIImagesNoOutputBody(dataBytes))
  1461. s.tryWriteOpenAIImagesStreamEvent(c, flusher, &clientDisconnected, &lastDownstreamWriteAt, "error", buildOpenAIImagesStreamErrorBody(outputErr.Error()))
  1462. processDataErr = outputErr
  1463. processDataDone = true
  1464. return
  1465. }
  1466. eventName := streamPrefix + ".completed"
  1467. for _, img := range finalResults {
  1468. key := openAIResponsesImageResultKey("", img)
  1469. if _, exists := emitted[key]; exists {
  1470. continue
  1471. }
  1472. payload := buildOpenAIImagesStreamCompletedPayload(eventName, img, format, createdAt, usageRaw)
  1473. emitted[key] = struct{}{}
  1474. s.tryWriteOpenAIImagesStreamEvent(c, flusher, &clientDisconnected, &lastDownstreamWriteAt, eventName, payload)
  1475. }
  1476. imageCount = len(emitted)
  1477. imageOutputSizes = openAIResponsesImageResultSizes(finalResults)
  1478. processDataDone = true
  1479. case "error", "response.failed":
  1480. if upstreamErr := openAIImagesUpstreamErrorFromSSEPayload(dataBytes); upstreamErr != nil {
  1481. retryable := IsOpenAIImagesRetryableUpstreamError(upstreamErr)
  1482. if !clientDisconnected && (!retryable || c.Writer.Size() != writerSizeBeforeResponse) {
  1483. s.tryWriteOpenAIImagesStreamEvent(c, flusher, &clientDisconnected, &lastDownstreamWriteAt, "error", buildOpenAIImagesStreamErrorBodyFromUpstream(upstreamErr))
  1484. }
  1485. setOpsUpstreamError(c, upstreamErr.clientStatusCode(), upstreamErr.clientMessage(), "")
  1486. processDataErr = upstreamErr
  1487. processDataDone = true
  1488. return
  1489. }
  1490. }
  1491. }
  1492. processLine := func(line []byte) (bool, error) {
  1493. if len(line) == 0 {
  1494. return false, nil
  1495. }
  1496. sseData.AddLine(string(line), processData)
  1497. if processDataErr != nil {
  1498. return true, processDataErr
  1499. }
  1500. return processDataDone, nil
  1501. }
  1502. flushData := func() (bool, error) {
  1503. sseData.Flush(processData)
  1504. if processDataErr != nil {
  1505. return true, processDataErr
  1506. }
  1507. return processDataDone, nil
  1508. }
  1509. finalizePending := func() error {
  1510. if imageCount > 0 {
  1511. return nil
  1512. }
  1513. if len(pendingResults) > 0 {
  1514. eventName := streamPrefix + ".completed"
  1515. finalResults := append([]openAIResponsesImageResult(nil), pendingResults...)
  1516. for i := range finalResults {
  1517. mergeOpenAIResponsesImageMeta(&finalResults[i], streamMeta)
  1518. }
  1519. reconcileOpenAIResponsesImageResultSizes(finalResults, nil)
  1520. for _, img := range finalResults {
  1521. key := openAIResponsesImageResultKey("", img)
  1522. if _, exists := emitted[key]; exists {
  1523. continue
  1524. }
  1525. payload := buildOpenAIImagesStreamCompletedPayload(eventName, img, format, createdAt, nil)
  1526. emitted[key] = struct{}{}
  1527. s.tryWriteOpenAIImagesStreamEvent(c, flusher, &clientDisconnected, &lastDownstreamWriteAt, eventName, payload)
  1528. }
  1529. imageCount = len(emitted)
  1530. imageOutputSizes = openAIResponsesImageResultSizes(finalResults)
  1531. return nil
  1532. }
  1533. streamErr := fmt.Errorf("stream disconnected before image generation completed")
  1534. s.tryWriteOpenAIImagesStreamEvent(c, flusher, &clientDisconnected, &lastDownstreamWriteAt, "error", buildOpenAIImagesStreamErrorBody(streamErr.Error()))
  1535. return streamErr
  1536. }
  1537. streamInterval := s.openAIImageStreamDataInterval()
  1538. keepaliveInterval := s.openAIImageStreamKeepaliveInterval()
  1539. if streamInterval <= 0 && keepaliveInterval <= 0 {
  1540. reader := bufio.NewReader(resp.Body)
  1541. for {
  1542. line, err := reader.ReadBytes('\n')
  1543. done, processErr := processLine(line)
  1544. if processErr != nil {
  1545. return usage, imageCount, imageOutputSizes, firstTokenMs, processErr
  1546. }
  1547. if done {
  1548. return usage, imageCount, imageOutputSizes, firstTokenMs, nil
  1549. }
  1550. if err == io.EOF {
  1551. break
  1552. }
  1553. if err != nil {
  1554. if done, processErr := flushData(); processErr != nil {
  1555. return usage, imageCount, imageOutputSizes, firstTokenMs, processErr
  1556. } else if done {
  1557. return usage, imageCount, imageOutputSizes, firstTokenMs, nil
  1558. }
  1559. if shouldClassifyOpenAIUpstreamStreamReadError(err, c.Request.Context()) {
  1560. err = newOpenAIUpstreamStreamReadError(err)
  1561. }
  1562. return usage, imageCount, imageOutputSizes, firstTokenMs, err
  1563. }
  1564. }
  1565. if done, processErr := flushData(); processErr != nil {
  1566. return usage, imageCount, imageOutputSizes, firstTokenMs, processErr
  1567. } else if done {
  1568. return usage, imageCount, imageOutputSizes, firstTokenMs, nil
  1569. }
  1570. if err := finalizePending(); err != nil {
  1571. return usage, imageCount, imageOutputSizes, firstTokenMs, err
  1572. }
  1573. return usage, imageCount, imageOutputSizes, firstTokenMs, nil
  1574. }
  1575. type readEvent struct {
  1576. line []byte
  1577. err error
  1578. }
  1579. events := make(chan readEvent, 16)
  1580. done := make(chan struct{})
  1581. sendEvent := func(ev readEvent) bool {
  1582. select {
  1583. case events <- ev:
  1584. return true
  1585. case <-done:
  1586. return false
  1587. }
  1588. }
  1589. var lastReadAt int64
  1590. atomic.StoreInt64(&lastReadAt, time.Now().UnixNano())
  1591. go func() {
  1592. defer close(events)
  1593. reader := bufio.NewReader(resp.Body)
  1594. for {
  1595. line, err := reader.ReadBytes('\n')
  1596. if len(line) > 0 {
  1597. atomic.StoreInt64(&lastReadAt, time.Now().UnixNano())
  1598. }
  1599. if len(line) > 0 && !sendEvent(readEvent{line: line}) {
  1600. return
  1601. }
  1602. if err == io.EOF {
  1603. return
  1604. }
  1605. if err != nil {
  1606. _ = sendEvent(readEvent{err: err})
  1607. return
  1608. }
  1609. }
  1610. }()
  1611. defer close(done)
  1612. var intervalTicker *time.Ticker
  1613. if streamInterval > 0 {
  1614. intervalTicker = time.NewTicker(streamInterval)
  1615. defer intervalTicker.Stop()
  1616. }
  1617. var intervalCh <-chan time.Time
  1618. if intervalTicker != nil {
  1619. intervalCh = intervalTicker.C
  1620. }
  1621. var keepaliveTicker *time.Ticker
  1622. if keepaliveInterval > 0 {
  1623. keepaliveTicker = time.NewTicker(keepaliveInterval)
  1624. defer keepaliveTicker.Stop()
  1625. }
  1626. var keepaliveCh <-chan time.Time
  1627. if keepaliveTicker != nil {
  1628. keepaliveCh = keepaliveTicker.C
  1629. }
  1630. for {
  1631. select {
  1632. case ev, ok := <-events:
  1633. if !ok {
  1634. if done, processErr := flushData(); processErr != nil {
  1635. return usage, imageCount, imageOutputSizes, firstTokenMs, processErr
  1636. } else if done {
  1637. return usage, imageCount, imageOutputSizes, firstTokenMs, nil
  1638. }
  1639. if err := finalizePending(); err != nil {
  1640. return usage, imageCount, imageOutputSizes, firstTokenMs, err
  1641. }
  1642. return usage, imageCount, imageOutputSizes, firstTokenMs, nil
  1643. }
  1644. if ev.err != nil {
  1645. if done, processErr := flushData(); processErr != nil {
  1646. return usage, imageCount, imageOutputSizes, firstTokenMs, processErr
  1647. } else if done {
  1648. return usage, imageCount, imageOutputSizes, firstTokenMs, nil
  1649. }
  1650. if shouldClassifyOpenAIUpstreamStreamReadError(ev.err, c.Request.Context()) {
  1651. ev.err = newOpenAIUpstreamStreamReadError(ev.err)
  1652. }
  1653. return usage, imageCount, imageOutputSizes, firstTokenMs, ev.err
  1654. }
  1655. done, processErr := processLine(ev.line)
  1656. if processErr != nil {
  1657. return usage, imageCount, imageOutputSizes, firstTokenMs, processErr
  1658. }
  1659. if done {
  1660. return usage, imageCount, imageOutputSizes, firstTokenMs, nil
  1661. }
  1662. case <-intervalCh:
  1663. lastRead := time.Unix(0, atomic.LoadInt64(&lastReadAt))
  1664. if time.Since(lastRead) < streamInterval {
  1665. continue
  1666. }
  1667. if clientDisconnected {
  1668. return usage, imageCount, imageOutputSizes, firstTokenMs, fmt.Errorf("image stream incomplete after timeout")
  1669. }
  1670. logger.LegacyPrintf("service.openai_gateway", "[OpenAI] Images responses stream data interval timeout: interval=%s", streamInterval)
  1671. s.tryWriteOpenAIImagesStreamEvent(c, flusher, &clientDisconnected, &lastDownstreamWriteAt, "error", buildOpenAIImagesStreamErrorBody(fmt.Sprintf("upstream image stream idle for %s", streamInterval)))
  1672. return usage, imageCount, imageOutputSizes, firstTokenMs, fmt.Errorf("image stream data interval timeout")
  1673. case <-keepaliveCh:
  1674. if clientDisconnected || time.Since(lastDownstreamWriteAt) < keepaliveInterval {
  1675. continue
  1676. }
  1677. if _, writeErr := io.WriteString(c.Writer, ":\n\n"); writeErr != nil {
  1678. clientDisconnected = true
  1679. logger.LegacyPrintf("service.openai_gateway", "[OpenAI] Images responses stream client disconnected during keepalive, continue draining upstream for billing")
  1680. continue
  1681. }
  1682. flusher.Flush()
  1683. lastDownstreamWriteAt = time.Now()
  1684. }
  1685. }
  1686. }
  1687. func (s *OpenAIGatewayService) forwardOpenAIImagesOAuth(
  1688. ctx context.Context,
  1689. c *gin.Context,
  1690. account *Account,
  1691. parsed *OpenAIImagesRequest,
  1692. channelMappedModel string,
  1693. ) (*OpenAIForwardResult, error) {
  1694. startTime := time.Now()
  1695. requestModel := strings.TrimSpace(parsed.Model)
  1696. if mapped := strings.TrimSpace(channelMappedModel); mapped != "" {
  1697. requestModel = mapped
  1698. }
  1699. if requestModel == "" {
  1700. requestModel = "gpt-image-2"
  1701. }
  1702. if err := validateOpenAIImagesModel(requestModel); err != nil {
  1703. return nil, err
  1704. }
  1705. logger.LegacyPrintf(
  1706. "service.openai_gateway",
  1707. "[OpenAI] Images request routing request_model=%s endpoint=%s account_type=%s uploads=%d",
  1708. requestModel,
  1709. parsed.Endpoint,
  1710. account.Type,
  1711. len(parsed.Uploads),
  1712. )
  1713. upstreamCtx, releaseUpstreamCtx := detachUpstreamContext(ctx)
  1714. defer releaseUpstreamCtx()
  1715. token, _, err := s.GetAccessToken(upstreamCtx, account)
  1716. if err != nil {
  1717. return nil, err
  1718. }
  1719. responsesBody, err := buildOpenAIImagesResponsesRequest(parsed, requestModel)
  1720. if err != nil {
  1721. return nil, err
  1722. }
  1723. upstreamCtx = withOpenAIImagesSelfBuiltRequest(upstreamCtx)
  1724. upstreamReq, err := s.buildUpstreamRequest(upstreamCtx, c, account, responsesBody, token, true, parsed.StickySessionSeed(), false)
  1725. if err != nil {
  1726. return nil, err
  1727. }
  1728. upstreamReq.Header.Set("Content-Type", "application/json")
  1729. upstreamReq.Header.Set("Accept", "text/event-stream")
  1730. upstreamReq.Header.Set("OpenAI-Beta", "responses=experimental")
  1731. proxyURL := ""
  1732. if account.ProxyID != nil && account.Proxy != nil {
  1733. proxyURL = account.Proxy.URL()
  1734. }
  1735. upstreamStart := time.Now()
  1736. resp, err := s.doOpenAIUpstream(upstreamReq, proxyURL, account)
  1737. SetOpsLatencyMs(c, OpsUpstreamLatencyMsKey, time.Since(upstreamStart).Milliseconds())
  1738. if err != nil {
  1739. safeErr := sanitizeUpstreamErrorMessage(err.Error())
  1740. setOpsUpstreamError(c, 0, safeErr, "")
  1741. appendOpsUpstreamError(c, OpsUpstreamErrorEvent{
  1742. ProxyID: opsUpstreamProxyID(account),
  1743. ProxyName: opsUpstreamProxyName(account),
  1744. Platform: account.Platform,
  1745. AccountID: account.ID,
  1746. AccountName: account.Name,
  1747. UpstreamStatusCode: 0,
  1748. UpstreamURL: safeUpstreamURL(upstreamReq.URL.String()),
  1749. Kind: "request_error",
  1750. Message: safeErr,
  1751. })
  1752. return nil, fmt.Errorf("upstream request failed: %s", safeErr)
  1753. }
  1754. if resp.StatusCode >= 400 {
  1755. respBody := s.readUpstreamErrorBody(resp)
  1756. _ = resp.Body.Close()
  1757. respBody = s.redactAgentIdentitySensitiveBody(upstreamCtx, account, respBody)
  1758. if !agentIdentityTaskRecoveryWasTried(ctx) && s.isAgentIdentityAccount(ctx, account) && isAgentIdentityTaskInvalidHTTPResponse(resp.StatusCode, respBody) {
  1759. expectedTaskID := account.GetCredential("task_id")
  1760. if err := s.recoverAgentIdentityTask(ctx, account, expectedTaskID); err != nil {
  1761. return nil, fmt.Errorf("agent identity task recovery failed: %w", err)
  1762. }
  1763. return s.forwardOpenAIImagesOAuth(markAgentIdentityTaskRecoveryTried(ctx), c, account, parsed, channelMappedModel)
  1764. }
  1765. resp.Body = io.NopCloser(bytes.NewReader(respBody))
  1766. upstreamMsg := strings.TrimSpace(extractUpstreamErrorMessage(respBody))
  1767. upstreamMsg = sanitizeUpstreamErrorMessage(upstreamMsg)
  1768. if s.shouldFailoverOpenAIUpstreamResponse(account, resp.StatusCode, upstreamMsg, respBody) {
  1769. appendOpsUpstreamError(c, OpsUpstreamErrorEvent{
  1770. ProxyID: opsUpstreamProxyID(account),
  1771. ProxyName: opsUpstreamProxyName(account),
  1772. Platform: account.Platform,
  1773. AccountID: account.ID,
  1774. AccountName: account.Name,
  1775. UpstreamStatusCode: resp.StatusCode,
  1776. UpstreamRequestID: resp.Header.Get("x-request-id"),
  1777. UpstreamURL: safeUpstreamURL(upstreamReq.URL.String()),
  1778. Kind: "failover",
  1779. Message: upstreamMsg,
  1780. })
  1781. shouldDisable := s.handleFailoverSideEffects(upstreamCtx, resp, account, respBody, requestModel)
  1782. return nil, s.newOpenAIAccountFailoverError(
  1783. account,
  1784. resp.StatusCode,
  1785. resp.Header,
  1786. respBody,
  1787. upstreamMsg,
  1788. shouldDisable,
  1789. !shouldDisable && account.IsPoolMode() && account.IsPoolModeRetryableStatus(resp.StatusCode),
  1790. )
  1791. }
  1792. return s.handleOpenAIImagesErrorResponse(upstreamCtx, resp, c, account, requestModel)
  1793. }
  1794. defer func() { _ = resp.Body.Close() }()
  1795. var (
  1796. usage OpenAIUsage
  1797. imageCount int
  1798. imageOutputSizes []string
  1799. firstTokenMs *int
  1800. )
  1801. // 与 handleOpenAIImagesOAuthResponseError 的比较端同口径:排除非流式 JSON
  1802. // keepalive 心跳字节,避免 failover 第 2 轮起把上一轮心跳残留误判为已写响应。
  1803. writerSizeBeforeResponse := OpenAIImagesJSONKeepaliveAdjustedWrittenSize(c)
  1804. if parsed.Stream {
  1805. usage, imageCount, imageOutputSizes, firstTokenMs, err = s.handleOpenAIImagesOAuthStreamingResponse(resp, c, startTime, parsed.ResponseFormat, openAIImagesStreamPrefix(parsed), requestModel)
  1806. if err != nil {
  1807. if imageCount > 0 {
  1808. return &OpenAIForwardResult{
  1809. RequestID: resp.Header.Get("x-request-id"),
  1810. UpstreamHeaders: resp.Header,
  1811. Usage: usage,
  1812. Model: requestModel,
  1813. UpstreamModel: requestModel,
  1814. Stream: parsed.Stream,
  1815. ResponseHeaders: resp.Header.Clone(),
  1816. Duration: time.Since(startTime),
  1817. FirstTokenMs: firstTokenMs,
  1818. ImageCount: imageCount,
  1819. ImageSize: parsed.SizeTier,
  1820. ImageInputSize: parsed.Size,
  1821. ImageOutputSizes: imageOutputSizes,
  1822. }, err
  1823. }
  1824. return nil, s.handleOpenAIImagesOAuthResponseError(
  1825. upstreamCtx,
  1826. c,
  1827. account,
  1828. requestModel,
  1829. safeUpstreamURL(upstreamReq.URL.String()),
  1830. resp,
  1831. writerSizeBeforeResponse,
  1832. err,
  1833. )
  1834. }
  1835. } else {
  1836. usage, imageCount, imageOutputSizes, err = s.handleOpenAIImagesOAuthNonStreamingResponse(resp, c, parsed.ResponseFormat, requestModel)
  1837. if err != nil {
  1838. return nil, s.handleOpenAIImagesOAuthResponseError(
  1839. upstreamCtx,
  1840. c,
  1841. account,
  1842. requestModel,
  1843. safeUpstreamURL(upstreamReq.URL.String()),
  1844. resp,
  1845. writerSizeBeforeResponse,
  1846. err,
  1847. )
  1848. }
  1849. }
  1850. if imageCount <= 0 {
  1851. imageCount = parsed.N
  1852. }
  1853. return &OpenAIForwardResult{
  1854. RequestID: resp.Header.Get("x-request-id"),
  1855. UpstreamHeaders: resp.Header,
  1856. Usage: usage,
  1857. Model: requestModel,
  1858. UpstreamModel: requestModel,
  1859. Stream: parsed.Stream,
  1860. ResponseHeaders: resp.Header.Clone(),
  1861. Duration: time.Since(startTime),
  1862. FirstTokenMs: firstTokenMs,
  1863. ImageCount: imageCount,
  1864. ImageSize: parsed.SizeTier,
  1865. ImageInputSize: parsed.Size,
  1866. ImageOutputSizes: imageOutputSizes,
  1867. }, nil
  1868. }
  1869. const (
  1870. openAIImagesOAuthUnavailableDefaultCooldown = 30 * time.Minute
  1871. openAIImagesOAuthUnavailableReason = "openai_images_oauth_tool_unavailable"
  1872. )
  1873. // shouldCoolOpenAIImagesToolForError decides whether an image_generation_unavailable
  1874. // verdict is durable enough to park the account's image tool for
  1875. // openAIImagesOAuthUnavailableCooldown.
  1876. //
  1877. // Only an upstream error frame that names the condition qualifies. A verdict the
  1878. // gateway synthesized from the model's plain-text reply does not: it merely says
  1879. // this prompt produced words instead of an image, which is prompt-dependent and
  1880. // happens on healthy accounts. Writing a 30-minute account-level cooldown from it
  1881. // is doubly wrong because the very same error is classified retryable
  1882. // (IsOpenAIImagesRetryableUpstreamError: status >= 500) and drives
  1883. // newOpenAIAccountFailoverError — so one such reply walks the pool and cools every
  1884. // account the retry touches.
  1885. //
  1886. // This mirrors the rule the alpha/search path already states in words: a
  1887. // tool-endpoint failure "仍允许本次请求换号,但不修改任何账号状态"
  1888. // (see shouldApplyOpenAIAlphaSearchAccountErrorSideEffects).
  1889. func shouldCoolOpenAIImagesToolForError(upstreamErr *OpenAIImagesUpstreamError) bool {
  1890. return upstreamErr != nil && !upstreamErr.SynthesizedFromModelText
  1891. }
  1892. func (s *OpenAIGatewayService) coolOpenAIImagesOAuthTool(ctx context.Context, account *Account) {
  1893. if s == nil || s.accountRepo == nil || account == nil || account.Platform != PlatformOpenAI {
  1894. return
  1895. }
  1896. stateCtx, cancel := openAIAccountStateContext(ctx)
  1897. defer cancel()
  1898. cooldown := openAIImagesOAuthUnavailableDefaultCooldown
  1899. if s.settingService != nil {
  1900. settings, err := s.settingService.GetOpenAIImagesOAuthUnavailableCooldownSettings(stateCtx)
  1901. if err != nil {
  1902. logger.LegacyPrintf("service.openai_gateway", "[OpenAI] Images OAuth tool cooldown setting read failed error=%v", err)
  1903. } else {
  1904. cooldown = time.Duration(settings.CooldownMinutes) * time.Minute
  1905. }
  1906. }
  1907. resetAt := time.Now().Add(cooldown)
  1908. if err := s.accountRepo.SetModelRateLimit(stateCtx, account.ID, openAIImageGenerationRateLimitKey, resetAt, openAIImagesOAuthUnavailableReason); err != nil {
  1909. logger.LegacyPrintf("service.openai_gateway", "[OpenAI] Images OAuth tool cooldown write failed account_id=%d error=%v", account.ID, err)
  1910. return
  1911. }
  1912. logger.LegacyPrintf("service.openai_gateway", "[OpenAI] Images OAuth tool unavailable account_id=%d reset_in=%s", account.ID, time.Until(resetAt).Truncate(time.Second))
  1913. }
  1914. func (s *OpenAIGatewayService) handleOpenAIImagesOAuthResponseError(
  1915. ctx context.Context,
  1916. c *gin.Context,
  1917. account *Account,
  1918. requestedModel string,
  1919. upstreamURL string,
  1920. resp *http.Response,
  1921. writerSizeBeforeResponse int,
  1922. err error,
  1923. ) error {
  1924. responseWritten := c != nil && c.Writer != nil && OpenAIImagesJSONKeepaliveAdjustedWrittenSize(c) != writerSizeBeforeResponse
  1925. if code, message, ok := OpenAIUpstreamStreamReadErrorDetails(err); ok {
  1926. // A body transport failure after a successful HTTP status is retryable only
  1927. // until real image output has reached the client. Keep the upstream headers
  1928. // and request ID available to the failover/error passthrough path.
  1929. headers := http.Header(nil)
  1930. requestID := ""
  1931. statusCode := http.StatusBadGateway
  1932. if resp != nil {
  1933. headers = resp.Header.Clone()
  1934. requestID = strings.TrimSpace(resp.Header.Get("x-request-id"))
  1935. }
  1936. kind := "failover"
  1937. if responseWritten {
  1938. kind = "retry_exhausted_failover"
  1939. }
  1940. appendOpsUpstreamError(c, OpsUpstreamErrorEvent{
  1941. ProxyID: opsUpstreamProxyID(account),
  1942. ProxyName: opsUpstreamProxyName(account),
  1943. Platform: account.Platform, AccountID: account.ID, AccountName: account.Name,
  1944. UpstreamStatusCode: statusCode, UpstreamRequestID: requestID, UpstreamURL: upstreamURL,
  1945. Kind: kind, Message: message,
  1946. })
  1947. if responseWritten {
  1948. return err
  1949. }
  1950. responseBody := []byte(fmt.Sprintf(`{"error":{"type":"upstream_error","code":%q,"message":%q}}`, code, message))
  1951. shouldDisable := s.handleOpenAIAccountUpstreamError(ctx, account, statusCode, headers, responseBody, requestedModel)
  1952. return s.newOpenAIAccountFailoverError(
  1953. account,
  1954. statusCode,
  1955. headers,
  1956. responseBody,
  1957. message,
  1958. shouldDisable,
  1959. !shouldDisable && account.IsPoolMode() && account.IsPoolModeRetryableStatus(statusCode),
  1960. )
  1961. }
  1962. var upstreamErr *OpenAIImagesUpstreamError
  1963. if !errors.As(err, &upstreamErr) {
  1964. return err
  1965. }
  1966. retryable := IsOpenAIImagesRetryableUpstreamError(upstreamErr)
  1967. kind := "http_error"
  1968. if retryable {
  1969. kind = "failover"
  1970. if responseWritten {
  1971. kind = "retry_exhausted_failover"
  1972. }
  1973. }
  1974. requestID := strings.TrimSpace(upstreamErr.UpstreamRequestID)
  1975. headers := http.Header(nil)
  1976. if resp != nil {
  1977. headers = resp.Header.Clone()
  1978. if requestID == "" {
  1979. requestID = strings.TrimSpace(resp.Header.Get("x-request-id"))
  1980. }
  1981. }
  1982. appendOpsUpstreamError(c, OpsUpstreamErrorEvent{
  1983. ProxyID: opsUpstreamProxyID(account),
  1984. ProxyName: opsUpstreamProxyName(account),
  1985. Platform: account.Platform,
  1986. AccountID: account.ID,
  1987. AccountName: account.Name,
  1988. UpstreamStatusCode: upstreamErr.StatusCode,
  1989. UpstreamRequestID: requestID,
  1990. UpstreamURL: upstreamURL,
  1991. Kind: kind,
  1992. Message: upstreamErr.clientMessage(),
  1993. })
  1994. responseBody := openAIImagesUpstreamErrorResponseBody(upstreamErr)
  1995. if upstreamErr.Code == "image_generation_unavailable" {
  1996. if shouldCoolOpenAIImagesToolForError(upstreamErr) {
  1997. s.coolOpenAIImagesOAuthTool(ctx, account)
  1998. }
  1999. if responseWritten {
  2000. return err
  2001. }
  2002. return s.newOpenAIAccountFailoverError(
  2003. account,
  2004. upstreamErr.StatusCode,
  2005. headers,
  2006. responseBody,
  2007. upstreamErr.clientMessage(),
  2008. false,
  2009. false,
  2010. )
  2011. }
  2012. if !retryable || responseWritten {
  2013. return err
  2014. }
  2015. shouldDisable := s.handleOpenAIAccountUpstreamError(ctx, account, upstreamErr.StatusCode, headers, responseBody, requestedModel)
  2016. return s.newOpenAIAccountFailoverError(
  2017. account,
  2018. upstreamErr.StatusCode,
  2019. headers,
  2020. responseBody,
  2021. upstreamErr.clientMessage(),
  2022. shouldDisable,
  2023. !shouldDisable && account.IsPoolMode() && account.IsPoolModeRetryableStatus(upstreamErr.StatusCode),
  2024. )
  2025. }