200 lines
6.5 KiB
Go
200 lines
6.5 KiB
Go
package service
|
|
|
|
import (
|
|
"strings"
|
|
|
|
"oci-portal/internal/aiwire"
|
|
)
|
|
|
|
// RespEvent 是一条 Responses SSE 语义事件(event 名 + data 载荷)。
|
|
type RespEvent struct {
|
|
Event string
|
|
Data map[string]any
|
|
}
|
|
|
|
// RespStream 把 IR chunk 流聚合为 Responses 语义事件序列。
|
|
// 与 AnthStream 同构,但终态事件须携带完整 Response 快照,故全程缓冲文本与实参。
|
|
type RespStream struct {
|
|
id string
|
|
model string
|
|
created int64
|
|
seq int
|
|
started bool
|
|
items []aiwire.RespOutItem
|
|
msgOpen bool
|
|
text strings.Builder
|
|
tool aiwire.RespOutItem
|
|
toolIdx int
|
|
tOpen bool
|
|
args strings.Builder
|
|
finish string
|
|
usage *aiwire.Usage
|
|
}
|
|
|
|
// NewRespStream 构造状态机;id 形如 resp_*,created 为响应时间戳。
|
|
func NewRespStream(id, model string, created int64) *RespStream {
|
|
return &RespStream{id: id, model: model, created: created}
|
|
}
|
|
|
|
// Feed 消化一个上游 chunk,返回应立即下发的事件。
|
|
func (s *RespStream) Feed(chunk aiwire.ChatChunk) []RespEvent {
|
|
var evs []RespEvent
|
|
if !s.started {
|
|
s.started = true
|
|
evs = append(evs, s.respEvent("response.created", "in_progress"),
|
|
s.respEvent("response.in_progress", "in_progress"))
|
|
}
|
|
if chunk.Usage != nil {
|
|
s.usage = chunk.Usage
|
|
}
|
|
if len(chunk.Choices) == 0 {
|
|
return evs
|
|
}
|
|
choice := chunk.Choices[0]
|
|
if choice.Delta.Content != "" {
|
|
evs = append(evs, s.feedText(choice.Delta.Content)...)
|
|
}
|
|
for _, tc := range choice.Delta.ToolCalls {
|
|
evs = append(evs, s.feedTool(tc)...)
|
|
}
|
|
if choice.FinishReason != nil {
|
|
s.finish = *choice.FinishReason
|
|
}
|
|
return evs
|
|
}
|
|
|
|
// Finish 关闭未闭合的块并产出终态事件(带完整 Response 快照)。
|
|
func (s *RespStream) Finish() []RespEvent {
|
|
var evs []RespEvent
|
|
if !s.started {
|
|
s.started = true
|
|
evs = append(evs, s.respEvent("response.created", "in_progress"))
|
|
}
|
|
evs = append(evs, s.closeMsg()...)
|
|
evs = append(evs, s.closeTool()...)
|
|
status := "completed"
|
|
if s.finish == "length" {
|
|
status = "incomplete"
|
|
}
|
|
return append(evs, s.respEvent("response."+status, status))
|
|
}
|
|
|
|
// Usage 返回聚合到的用量(可能为 nil),供调用日志。
|
|
func (s *RespStream) Usage() *aiwire.Usage { return s.usage }
|
|
|
|
// ev 构造带自增 sequence_number 的事件。
|
|
func (s *RespStream) ev(typ string, kv map[string]any) RespEvent {
|
|
s.seq++
|
|
data := map[string]any{"type": typ, "sequence_number": s.seq}
|
|
for k, v := range kv {
|
|
data[k] = v
|
|
}
|
|
return RespEvent{Event: typ, Data: data}
|
|
}
|
|
|
|
// respEvent 构造携带 Response 快照的生命周期事件。
|
|
func (s *RespStream) respEvent(typ, status string) RespEvent {
|
|
return s.ev(typ, map[string]any{"response": s.snapshot(status)})
|
|
}
|
|
|
|
// snapshot 组装当前累计状态的 Response 对象。
|
|
func (s *RespStream) snapshot(status string) aiwire.Response {
|
|
resp := aiwire.Response{ID: s.id, Object: "response", CreatedAt: s.created,
|
|
Status: status, Model: s.model, Store: false,
|
|
Output: append([]aiwire.RespOutItem{}, s.items...)}
|
|
resp.Usage = respUsage(s.usage)
|
|
if status == "incomplete" {
|
|
resp.IncompleteDetails = &aiwire.RespIncomplete{Reason: "max_output_tokens"}
|
|
}
|
|
return resp
|
|
}
|
|
|
|
// feedText 处理文本增量:必要时开 message item 与 content part。
|
|
func (s *RespStream) feedText(delta string) []RespEvent {
|
|
evs := s.closeTool()
|
|
if !s.msgOpen {
|
|
s.msgOpen = true
|
|
s.text.Reset()
|
|
item := aiwire.RespOutItem{Type: "message", ID: s.itemID("msg"), Status: "in_progress",
|
|
Role: "assistant", Content: []aiwire.RespOutPart{}}
|
|
evs = append(evs, s.ev("response.output_item.added", map[string]any{
|
|
"output_index": len(s.items), "item": item}))
|
|
evs = append(evs, s.ev("response.content_part.added", map[string]any{
|
|
"item_id": item.ID, "output_index": len(s.items), "content_index": 0,
|
|
"part": aiwire.RespOutPart{Type: "output_text", Text: "", Annotations: []any{}}}))
|
|
}
|
|
s.text.WriteString(delta)
|
|
return append(evs, s.ev("response.output_text.delta", map[string]any{
|
|
"item_id": s.itemID("msg"), "output_index": len(s.items), "content_index": 0, "delta": delta}))
|
|
}
|
|
|
|
// closeMsg 闭合当前 message item(text done → part done → item done)。
|
|
func (s *RespStream) closeMsg() []RespEvent {
|
|
if !s.msgOpen {
|
|
return nil
|
|
}
|
|
s.msgOpen = false
|
|
id, idx, full := s.itemID("msg"), len(s.items), s.text.String()
|
|
part := aiwire.RespOutPart{Type: "output_text", Text: full, Annotations: []any{}}
|
|
item := aiwire.RespOutItem{Type: "message", ID: id, Status: "completed",
|
|
Role: "assistant", Content: []aiwire.RespOutPart{part}}
|
|
evs := []RespEvent{
|
|
s.ev("response.output_text.done", map[string]any{"item_id": id, "output_index": idx, "content_index": 0, "text": full}),
|
|
s.ev("response.content_part.done", map[string]any{"item_id": id, "output_index": idx, "content_index": 0, "part": part}),
|
|
s.ev("response.output_item.done", map[string]any{"output_index": idx, "item": item}),
|
|
}
|
|
s.items = append(s.items, item)
|
|
return evs
|
|
}
|
|
|
|
// feedTool 处理工具调用增量:新 Index 先闭合旧调用再开新 item。
|
|
func (s *RespStream) feedTool(tc aiwire.ToolCallDelta) []RespEvent {
|
|
evs := s.closeMsg()
|
|
if s.tOpen && tc.Index != s.toolIdx {
|
|
evs = append(evs, s.closeTool()...)
|
|
}
|
|
if !s.tOpen {
|
|
s.tOpen, s.toolIdx = true, tc.Index
|
|
s.args.Reset()
|
|
s.tool = aiwire.RespOutItem{Type: "function_call", ID: s.itemID("fc"), Status: "in_progress",
|
|
CallID: tc.ID, Name: tc.Function.Name}
|
|
evs = append(evs, s.ev("response.output_item.added", map[string]any{
|
|
"output_index": len(s.items), "item": s.tool}))
|
|
}
|
|
if tc.ID != "" && s.tool.CallID == "" {
|
|
s.tool.CallID = tc.ID
|
|
}
|
|
if tc.Function.Name != "" && s.tool.Name == "" {
|
|
s.tool.Name = tc.Function.Name
|
|
}
|
|
if tc.Function.Arguments == "" {
|
|
return evs
|
|
}
|
|
s.args.WriteString(tc.Function.Arguments)
|
|
return append(evs, s.ev("response.function_call_arguments.delta", map[string]any{
|
|
"item_id": s.tool.ID, "output_index": len(s.items), "delta": tc.Function.Arguments}))
|
|
}
|
|
|
|
// closeTool 闭合当前 function_call item(arguments done → item done)。
|
|
func (s *RespStream) closeTool() []RespEvent {
|
|
if !s.tOpen {
|
|
return nil
|
|
}
|
|
s.tOpen = false
|
|
s.tool.Arguments = respArgs(s.args.String())
|
|
s.tool.Status = "completed"
|
|
idx := len(s.items)
|
|
evs := []RespEvent{
|
|
s.ev("response.function_call_arguments.done", map[string]any{
|
|
"item_id": s.tool.ID, "output_index": idx, "arguments": s.tool.Arguments}),
|
|
s.ev("response.output_item.done", map[string]any{"output_index": idx, "item": s.tool}),
|
|
}
|
|
s.items = append(s.items, s.tool)
|
|
return evs
|
|
}
|
|
|
|
// itemID 按当前 output 序号派生确定性 item ID。
|
|
func (s *RespStream) itemID(kind string) string {
|
|
return respItemID(s.id, kind, len(s.items))
|
|
}
|