bedrock_stream.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414
  1. package service
  2. import (
  3. "bufio"
  4. "context"
  5. "encoding/base64"
  6. "errors"
  7. "fmt"
  8. "hash/crc32"
  9. "io"
  10. "net/http"
  11. "sync/atomic"
  12. "time"
  13. "github.com/gin-gonic/gin"
  14. "github.com/tidwall/gjson"
  15. "github.com/tidwall/sjson"
  16. "github.com/Wei-Shaw/sub2api/internal/pkg/logger"
  17. )
  18. // handleBedrockStreamingResponse 处理 Bedrock InvokeModelWithResponseStream 的 EventStream 响应
  19. // Bedrock 返回 AWS EventStream 二进制格式,每个事件的 payload 中 chunk.bytes 是 base64 编码的
  20. // Claude SSE 事件 JSON。本方法解码后转换为标准 SSE 格式写入客户端。
  21. func (s *GatewayService) handleBedrockStreamingResponse(
  22. ctx context.Context,
  23. resp *http.Response,
  24. c *gin.Context,
  25. account *Account,
  26. startTime time.Time,
  27. model string,
  28. ) (*streamingResult, error) {
  29. w := c.Writer
  30. flusher, ok := w.(http.Flusher)
  31. if !ok {
  32. return nil, errors.New("streaming not supported")
  33. }
  34. c.Header("Content-Type", "text/event-stream")
  35. c.Header("Cache-Control", "no-cache")
  36. c.Header("Connection", "keep-alive")
  37. c.Header("X-Accel-Buffering", "no")
  38. if v := resp.Header.Get("x-amzn-requestid"); v != "" {
  39. c.Header("x-request-id", v)
  40. }
  41. usage := &ClaudeUsage{}
  42. var firstTokenMs *int
  43. clientDisconnected := false
  44. // Bedrock EventStream 使用 application/vnd.amazon.eventstream 二进制格式。
  45. // 每个帧结构:total_length(4) + headers_length(4) + prelude_crc(4) + headers + payload + message_crc(4)
  46. // 但更实用的方式是使用行扫描找 JSON chunks,因为 Bedrock 的响应在二进制帧中。
  47. // 我们使用 EventStream decoder 来正确解析。
  48. decoder := newBedrockEventStreamDecoder(resp.Body)
  49. type decodeEvent struct {
  50. payload []byte
  51. err error
  52. }
  53. events := make(chan decodeEvent, 16)
  54. done := make(chan struct{})
  55. sendEvent := func(ev decodeEvent) bool {
  56. select {
  57. case events <- ev:
  58. return true
  59. case <-done:
  60. return false
  61. }
  62. }
  63. var lastReadAt atomic.Int64
  64. lastReadAt.Store(time.Now().UnixNano())
  65. go func() {
  66. defer close(events)
  67. for {
  68. payload, err := decoder.Decode()
  69. if err != nil {
  70. if err == io.EOF {
  71. return
  72. }
  73. _ = sendEvent(decodeEvent{err: err})
  74. return
  75. }
  76. lastReadAt.Store(time.Now().UnixNano())
  77. if !sendEvent(decodeEvent{payload: payload}) {
  78. return
  79. }
  80. }
  81. }()
  82. defer close(done)
  83. streamInterval := time.Duration(0)
  84. if s.cfg != nil && s.cfg.Gateway.StreamDataIntervalTimeout > 0 {
  85. streamInterval = time.Duration(s.cfg.Gateway.StreamDataIntervalTimeout) * time.Second
  86. }
  87. var intervalTicker *time.Ticker
  88. if streamInterval > 0 {
  89. intervalTicker = time.NewTicker(streamInterval)
  90. defer intervalTicker.Stop()
  91. }
  92. var intervalCh <-chan time.Time
  93. if intervalTicker != nil {
  94. intervalCh = intervalTicker.C
  95. }
  96. for {
  97. select {
  98. case ev, ok := <-events:
  99. if !ok {
  100. if !clientDisconnected {
  101. flusher.Flush()
  102. }
  103. return &streamingResult{usage: usage, firstTokenMs: firstTokenMs, clientDisconnect: clientDisconnected}, nil
  104. }
  105. if ev.err != nil {
  106. if clientDisconnected {
  107. return &streamingResult{usage: usage, firstTokenMs: firstTokenMs, clientDisconnect: true}, nil
  108. }
  109. if errors.Is(ev.err, context.Canceled) || errors.Is(ev.err, context.DeadlineExceeded) {
  110. return &streamingResult{usage: usage, firstTokenMs: firstTokenMs, clientDisconnect: true}, nil
  111. }
  112. return &streamingResult{usage: usage, firstTokenMs: firstTokenMs}, fmt.Errorf("bedrock stream read error: %w", ev.err)
  113. }
  114. // payload 是 JSON,提取 chunk.bytes(base64 编码的 Claude SSE 事件数据)
  115. sseData := extractBedrockChunkData(ev.payload)
  116. if sseData == nil {
  117. continue
  118. }
  119. if firstTokenMs == nil {
  120. ms := int(time.Since(startTime).Milliseconds())
  121. firstTokenMs = &ms
  122. }
  123. // 转换 Bedrock 特有的 amazon-bedrock-invocationMetrics 为标准 Anthropic usage 格式
  124. // 同时移除该字段避免透传给客户端
  125. sseData = transformBedrockInvocationMetrics(sseData)
  126. // 解析 SSE 事件数据提取 usage
  127. parseSSEUsagePassthrough(string(sseData), usage)
  128. // 确定 SSE event type
  129. eventType := gjson.GetBytes(sseData, "type").String()
  130. // 写入标准 SSE 格式
  131. if !clientDisconnected {
  132. var writeErr error
  133. if eventType != "" {
  134. _, writeErr = fmt.Fprintf(w, "event: %s\ndata: %s\n\n", eventType, sseData)
  135. } else {
  136. _, writeErr = fmt.Fprintf(w, "data: %s\n\n", sseData)
  137. }
  138. if writeErr != nil {
  139. clientDisconnected = true
  140. logger.LegacyPrintf("service.gateway", "[Bedrock] Client disconnected during streaming, continue draining for usage: account=%d", account.ID)
  141. } else {
  142. flusher.Flush()
  143. }
  144. }
  145. case <-intervalCh:
  146. lastRead := time.Unix(0, lastReadAt.Load())
  147. if time.Since(lastRead) < streamInterval {
  148. continue
  149. }
  150. if clientDisconnected {
  151. return &streamingResult{usage: usage, firstTokenMs: firstTokenMs, clientDisconnect: true}, nil
  152. }
  153. logger.LegacyPrintf("service.gateway", "[Bedrock] Stream data interval timeout: account=%d model=%s interval=%s", account.ID, model, streamInterval)
  154. if s.rateLimitService != nil {
  155. s.rateLimitService.HandleStreamTimeout(ctx, account, model)
  156. }
  157. return &streamingResult{usage: usage, firstTokenMs: firstTokenMs}, fmt.Errorf("stream data interval timeout")
  158. }
  159. }
  160. }
  161. // extractBedrockChunkData 从 Bedrock EventStream payload 中提取 Claude SSE 事件数据
  162. // Bedrock payload 格式:{"bytes":"<base64-encoded-json>"}
  163. func extractBedrockChunkData(payload []byte) []byte {
  164. b64 := gjson.GetBytes(payload, "bytes").String()
  165. if b64 == "" {
  166. return nil
  167. }
  168. decoded, err := base64.StdEncoding.DecodeString(b64)
  169. if err != nil {
  170. return nil
  171. }
  172. return decoded
  173. }
  174. // transformBedrockInvocationMetrics 将 Bedrock 特有的 amazon-bedrock-invocationMetrics
  175. // 转换为标准 Anthropic usage 格式,并从 SSE 数据中移除该字段。
  176. //
  177. // Bedrock Invoke 返回的 message_delta 事件可能包含:
  178. //
  179. // {"type":"message_delta","delta":{...},"amazon-bedrock-invocationMetrics":{"inputTokenCount":150,"outputTokenCount":42}}
  180. //
  181. // 转换为:
  182. //
  183. // {"type":"message_delta","delta":{...},"usage":{"input_tokens":150,"output_tokens":42}}
  184. func transformBedrockInvocationMetrics(data []byte) []byte {
  185. metrics := gjson.GetBytes(data, "amazon-bedrock-invocationMetrics")
  186. if !metrics.Exists() || !metrics.IsObject() {
  187. return data
  188. }
  189. // 移除 Bedrock 特有字段
  190. data, _ = sjson.DeleteBytes(data, "amazon-bedrock-invocationMetrics")
  191. // 如果已有标准 usage 字段,不覆盖
  192. if gjson.GetBytes(data, "usage").Exists() {
  193. return data
  194. }
  195. // 转换 camelCase → snake_case 写入 usage
  196. inputTokens := metrics.Get("inputTokenCount")
  197. outputTokens := metrics.Get("outputTokenCount")
  198. if inputTokens.Exists() {
  199. data, _ = sjson.SetBytes(data, "usage.input_tokens", inputTokens.Int())
  200. }
  201. if outputTokens.Exists() {
  202. data, _ = sjson.SetBytes(data, "usage.output_tokens", outputTokens.Int())
  203. }
  204. return data
  205. }
  206. // bedrockEventStreamDecoder 解码 AWS EventStream 二进制帧
  207. // EventStream 帧格式:
  208. //
  209. // [total_byte_length: 4 bytes]
  210. // [headers_byte_length: 4 bytes]
  211. // [prelude_crc: 4 bytes]
  212. // [headers: variable]
  213. // [payload: variable]
  214. // [message_crc: 4 bytes]
  215. type bedrockEventStreamDecoder struct {
  216. reader *bufio.Reader
  217. }
  218. func newBedrockEventStreamDecoder(r io.Reader) *bedrockEventStreamDecoder {
  219. return &bedrockEventStreamDecoder{
  220. reader: bufio.NewReaderSize(r, 64*1024),
  221. }
  222. }
  223. // Decode 读取下一个 EventStream 帧并返回 chunk 类型事件的 payload
  224. func (d *bedrockEventStreamDecoder) Decode() ([]byte, error) {
  225. for {
  226. // 读取 prelude: total_length(4) + headers_length(4) + prelude_crc(4) = 12 bytes
  227. prelude := make([]byte, 12)
  228. if _, err := io.ReadFull(d.reader, prelude); err != nil {
  229. return nil, err
  230. }
  231. // 验证 prelude CRC(AWS EventStream 使用标准 CRC32 / IEEE)
  232. preludeCRC := bedrockReadUint32(prelude[8:12])
  233. if crc32.Checksum(prelude[0:8], crc32IEEETable) != preludeCRC {
  234. return nil, fmt.Errorf("eventstream prelude CRC mismatch")
  235. }
  236. totalLength := bedrockReadUint32(prelude[0:4])
  237. headersLength := bedrockReadUint32(prelude[4:8])
  238. if totalLength < 16 { // minimum: 12 prelude + 4 message_crc
  239. return nil, fmt.Errorf("invalid eventstream frame: total_length=%d", totalLength)
  240. }
  241. // 读取 headers + payload + message_crc
  242. remaining := int(totalLength) - 12
  243. if remaining <= 0 {
  244. continue
  245. }
  246. data := make([]byte, remaining)
  247. if _, err := io.ReadFull(d.reader, data); err != nil {
  248. return nil, err
  249. }
  250. // 验证 message CRC(覆盖 prelude + headers + payload)
  251. messageCRC := bedrockReadUint32(data[len(data)-4:])
  252. h := crc32.New(crc32IEEETable)
  253. _, _ = h.Write(prelude)
  254. _, _ = h.Write(data[:len(data)-4])
  255. if h.Sum32() != messageCRC {
  256. return nil, fmt.Errorf("eventstream message CRC mismatch")
  257. }
  258. // 解析 headers
  259. headers := data[:headersLength]
  260. payload := data[headersLength : len(data)-4] // 去掉 message_crc
  261. // 从 headers 中提取 :event-type
  262. eventType := extractEventStreamHeaderValue(headers, ":event-type")
  263. // 只处理 chunk 事件
  264. if eventType == "chunk" {
  265. // payload 是完整的 JSON,包含 bytes 字段
  266. return payload, nil
  267. }
  268. // 检查异常事件
  269. exceptionType := extractEventStreamHeaderValue(headers, ":exception-type")
  270. if exceptionType != "" {
  271. return nil, fmt.Errorf("bedrock exception: %s: %s", exceptionType, string(payload))
  272. }
  273. messageType := extractEventStreamHeaderValue(headers, ":message-type")
  274. if messageType == "exception" || messageType == "error" {
  275. return nil, fmt.Errorf("bedrock error: %s", string(payload))
  276. }
  277. // 跳过其他事件类型(如 initial-response)
  278. }
  279. }
  280. // extractEventStreamHeaderValue 从 EventStream headers 二进制数据中提取指定 header 的字符串值
  281. // EventStream header 格式:
  282. //
  283. // [name_length: 1 byte][name: variable][value_type: 1 byte][value: variable]
  284. //
  285. // value_type = 7 表示 string 类型,前 2 bytes 为长度
  286. func extractEventStreamHeaderValue(headers []byte, targetName string) string {
  287. pos := 0
  288. for pos < len(headers) {
  289. if pos >= len(headers) {
  290. break
  291. }
  292. nameLen := int(headers[pos])
  293. pos++
  294. if pos+nameLen > len(headers) {
  295. break
  296. }
  297. name := string(headers[pos : pos+nameLen])
  298. pos += nameLen
  299. if pos >= len(headers) {
  300. break
  301. }
  302. valueType := headers[pos]
  303. pos++
  304. switch valueType {
  305. case 7: // string
  306. if pos+2 > len(headers) {
  307. return ""
  308. }
  309. valueLen := int(bedrockReadUint16(headers[pos : pos+2]))
  310. pos += 2
  311. if pos+valueLen > len(headers) {
  312. return ""
  313. }
  314. value := string(headers[pos : pos+valueLen])
  315. pos += valueLen
  316. if name == targetName {
  317. return value
  318. }
  319. case 0: // bool true
  320. if name == targetName {
  321. return "true"
  322. }
  323. case 1: // bool false
  324. if name == targetName {
  325. return "false"
  326. }
  327. case 2: // byte
  328. pos++
  329. if name == targetName {
  330. return ""
  331. }
  332. case 3: // short
  333. pos += 2
  334. if name == targetName {
  335. return ""
  336. }
  337. case 4: // int
  338. pos += 4
  339. if name == targetName {
  340. return ""
  341. }
  342. case 5: // long
  343. pos += 8
  344. if name == targetName {
  345. return ""
  346. }
  347. case 6: // bytes
  348. if pos+2 > len(headers) {
  349. return ""
  350. }
  351. valueLen := int(bedrockReadUint16(headers[pos : pos+2]))
  352. pos += 2 + valueLen
  353. case 8: // timestamp
  354. pos += 8
  355. case 9: // uuid
  356. pos += 16
  357. default:
  358. return "" // 未知类型,无法继续解析
  359. }
  360. }
  361. return ""
  362. }
  363. // crc32IEEETable is the CRC32 / IEEE table used by AWS EventStream.
  364. var crc32IEEETable = crc32.MakeTable(crc32.IEEE)
  365. func bedrockReadUint32(b []byte) uint32 {
  366. return uint32(b[0])<<24 | uint32(b[1])<<16 | uint32(b[2])<<8 | uint32(b[3])
  367. }
  368. func bedrockReadUint16(b []byte) uint16 {
  369. return uint16(b[0])<<8 | uint16(b[1])
  370. }