- add /v1/responses (and /responses): OpenAI Responses API support - streaming semantic events, non-streaming aggregation, function tools - input item / content / tool / tool_choice conversion to chat format - ensure first message is system: fix upstream 11128 "first message is not system prompt" for non-harness clients (assistant/tool-leading or missing system); normalize developer role - fix tool call delta merging (applyToolCallDelta) across streamed chunks - refactor handleChatCompletions: extract upstreamChat for 429 cooldown fallback across the account pool - bump version to 1.8.1 - tests: 37 passing (session normalization, responses conversion, tool call merge, responses<->chat mapping)
700 lines
22 KiB
Go
700 lines
22 KiB
Go
package main
|
||
|
||
import (
|
||
"bufio"
|
||
"encoding/json"
|
||
"fmt"
|
||
"io"
|
||
"log"
|
||
"net/http"
|
||
"strings"
|
||
"sync/atomic"
|
||
"time"
|
||
|
||
"github.com/google/uuid"
|
||
)
|
||
|
||
// -----------------------------------------------------------------------------
|
||
// OpenAI Responses API (/v1/responses) -> 上游 Chat Completions 转译
|
||
//
|
||
// 网关只与上游 /v2/chat/completions 通信,此处负责双向协议转换:
|
||
// - 请求:input / instructions / tools(扁平) / tool_choice / max_output_tokens /
|
||
// reasoning.effort -> chat messages / tools(嵌套 function) / max_tokens / reasoning_effort
|
||
// - 非流式响应:chat.completion -> response{object:"response", output:[...]}
|
||
// - 流式响应:上游 SSE 增量 -> Responses 语义事件(response.output_text.delta、
|
||
// response.function_call_arguments.delta、response.completed ...)
|
||
// -----------------------------------------------------------------------------
|
||
|
||
// handleResponses 处理 POST /v1/responses。
|
||
func handleResponses(w http.ResponseWriter, r *http.Request) {
|
||
if r.Method != http.MethodPost {
|
||
writeOpenAIError(w, http.StatusMethodNotAllowed, "method_not_allowed", "仅支持 POST 请求")
|
||
return
|
||
}
|
||
|
||
reqID := atomic.AddUint64(&reqCounter, 1)
|
||
startTime := time.Now()
|
||
|
||
bodyBytes, err := io.ReadAll(r.Body)
|
||
if err != nil {
|
||
writeOpenAIError(w, http.StatusBadRequest, "read_error", "读取请求体失败")
|
||
return
|
||
}
|
||
defer r.Body.Close()
|
||
|
||
var respReq map[string]any
|
||
if err := json.Unmarshal(bodyBytes, &respReq); err != nil {
|
||
writeOpenAIError(w, http.StatusBadRequest, "invalid_json", "无效的 JSON 请求体")
|
||
return
|
||
}
|
||
|
||
modelName, _ := respReq["model"].(string)
|
||
if modelName == "" {
|
||
modelName = "hy4-preview"
|
||
}
|
||
isStream, _ := respReq["stream"].(bool)
|
||
|
||
chatReq, err := responsesToChatRequest(respReq, modelName)
|
||
if err != nil {
|
||
writeOpenAIError(w, http.StatusBadRequest, "invalid_request", err.Error())
|
||
return
|
||
}
|
||
chatReq["stream"] = true // 上游强制流式,非流式由网关本地聚合
|
||
|
||
applyThinkingRules(chatReq, modelName)
|
||
sanitizeMessages(chatReq)
|
||
ensureLeadingSystemMessage(chatReq)
|
||
|
||
upstreamBytes, err := json.Marshal(chatReq)
|
||
if err != nil {
|
||
writeOpenAIError(w, http.StatusInternalServerError, "encode_error", "序列化请求失败")
|
||
return
|
||
}
|
||
|
||
log.Printf("[#%d] POST /v1/responses -> Upstream [Model: %s, Stream: %v]", reqID, modelName, isStream)
|
||
|
||
resp, acc, prof, ok := upstreamChat(w, r, reqID, upstreamBytes, startTime)
|
||
if !ok {
|
||
return
|
||
}
|
||
if isStream {
|
||
streamResponsesResponse(w, resp, modelName, reqID, acc, prof, startTime)
|
||
} else {
|
||
writeResponsesAggregate(w, resp, modelName, reqID, acc, prof, startTime)
|
||
}
|
||
}
|
||
|
||
// responsesToChatRequest 将 Responses 请求体转换为上游 Chat Completions 请求体。
|
||
func responsesToChatRequest(respReq map[string]any, modelName string) (map[string]any, error) {
|
||
chat := map[string]any{"model": modelName}
|
||
|
||
messages := []any{}
|
||
if instructions, ok := respReq["instructions"].(string); ok && strings.TrimSpace(instructions) != "" {
|
||
messages = append(messages, map[string]any{"role": "system", "content": instructions})
|
||
}
|
||
|
||
switch input := respReq["input"].(type) {
|
||
case string:
|
||
if strings.TrimSpace(input) != "" {
|
||
messages = append(messages, map[string]any{"role": "user", "content": input})
|
||
}
|
||
case []any:
|
||
for _, itemAny := range input {
|
||
switch item := itemAny.(type) {
|
||
case string:
|
||
messages = append(messages, map[string]any{"role": "user", "content": item})
|
||
case map[string]any:
|
||
messages = append(messages, convertResponsesInputItem(item)...)
|
||
}
|
||
}
|
||
}
|
||
|
||
if len(messages) == 0 {
|
||
return nil, fmt.Errorf("input 为空:Responses 请求必须提供 input 或 instructions")
|
||
}
|
||
chat["messages"] = messages
|
||
|
||
if v, ok := respReq["temperature"]; ok && v != nil {
|
||
chat["temperature"] = v
|
||
}
|
||
if v, ok := respReq["top_p"]; ok && v != nil {
|
||
chat["top_p"] = v
|
||
}
|
||
if v, ok := respReq["max_output_tokens"]; ok && v != nil {
|
||
chat["max_tokens"] = v
|
||
}
|
||
if tools := convertResponsesTools(respReq["tools"]); len(tools) > 0 {
|
||
chat["tools"] = tools
|
||
}
|
||
if tc := convertResponsesToolChoice(respReq["tool_choice"]); tc != nil {
|
||
chat["tool_choice"] = tc
|
||
}
|
||
if reasoning, ok := respReq["reasoning"].(map[string]any); ok {
|
||
if effort, ok := reasoning["effort"].(string); ok && effort != "" && effort != "none" {
|
||
chat["reasoning_effort"] = effort
|
||
}
|
||
}
|
||
return chat, nil
|
||
}
|
||
|
||
// convertResponsesInputItem 将单个 Responses input item 转换为 0..1 条 chat 消息。
|
||
func convertResponsesInputItem(item map[string]any) []any {
|
||
switch typ, _ := item["type"].(string); typ {
|
||
case "function_call":
|
||
callID, _ := item["call_id"].(string)
|
||
if callID == "" {
|
||
callID, _ = item["id"].(string)
|
||
}
|
||
name, _ := item["name"].(string)
|
||
args, _ := item["arguments"].(string)
|
||
return []any{map[string]any{
|
||
"role": "assistant",
|
||
"content": nil,
|
||
"tool_calls": []any{map[string]any{
|
||
"id": ifEmpty(callID, "call_"+compactUUID()),
|
||
"type": "function",
|
||
"function": map[string]any{"name": name, "arguments": args},
|
||
}},
|
||
}}
|
||
case "function_call_output":
|
||
callID, _ := item["call_id"].(string)
|
||
return []any{map[string]any{
|
||
"role": "tool",
|
||
"tool_call_id": callID,
|
||
"content": stringifyToolOutput(item["output"]),
|
||
}}
|
||
case "reasoning":
|
||
// 上游无法接收 reasoning item,忽略(历史上下文不影响后续对话)
|
||
return nil
|
||
}
|
||
|
||
role, _ := item["role"].(string)
|
||
if role == "" {
|
||
role = "user"
|
||
}
|
||
return []any{map[string]any{"role": role, "content": convertResponsesContent(item["content"])}}
|
||
}
|
||
|
||
// convertResponsesContent 将 Responses content(string 或 parts 数组)转换为 chat content。
|
||
func convertResponsesContent(content any) any {
|
||
switch c := content.(type) {
|
||
case nil:
|
||
return ""
|
||
case string:
|
||
return c
|
||
case []any:
|
||
parts := make([]any, 0, len(c))
|
||
for _, pAny := range c {
|
||
p, ok := pAny.(map[string]any)
|
||
if !ok {
|
||
if s, ok := pAny.(string); ok {
|
||
parts = append(parts, map[string]any{"type": "text", "text": s})
|
||
}
|
||
continue
|
||
}
|
||
switch typ, _ := p["type"].(string); typ {
|
||
case "input_text", "output_text", "text":
|
||
if t, ok := p["text"].(string); ok {
|
||
parts = append(parts, map[string]any{"type": "text", "text": t})
|
||
}
|
||
case "refusal":
|
||
if t, ok := p["refusal"].(string); ok {
|
||
parts = append(parts, map[string]any{"type": "text", "text": t})
|
||
}
|
||
case "input_image":
|
||
url, _ := p["image_url"].(string)
|
||
if url != "" {
|
||
parts = append(parts, map[string]any{"type": "image_url", "image_url": map[string]any{"url": url}})
|
||
}
|
||
}
|
||
}
|
||
if len(parts) == 0 {
|
||
return ""
|
||
}
|
||
return parts
|
||
default:
|
||
return fmt.Sprintf("%v", c)
|
||
}
|
||
}
|
||
|
||
// stringifyToolOutput 将 function_call_output 的 output 统一转为字符串。
|
||
func stringifyToolOutput(output any) string {
|
||
switch o := output.(type) {
|
||
case nil:
|
||
return ""
|
||
case string:
|
||
return o
|
||
default:
|
||
if b, err := json.Marshal(o); err == nil {
|
||
return string(b)
|
||
}
|
||
return fmt.Sprintf("%v", o)
|
||
}
|
||
}
|
||
|
||
// convertResponsesTools 将 Responses 扁平 function 工具转换为 chat 嵌套 function 工具。
|
||
func convertResponsesTools(toolsAny any) []any {
|
||
arr, ok := toolsAny.([]any)
|
||
if !ok {
|
||
return nil
|
||
}
|
||
out := make([]any, 0, len(arr))
|
||
for _, tAny := range arr {
|
||
t, ok := tAny.(map[string]any)
|
||
if !ok {
|
||
continue
|
||
}
|
||
typ, _ := t["type"].(string)
|
||
if typ != "" && typ != "function" {
|
||
continue // 仅支持 function 工具
|
||
}
|
||
name, _ := t["name"].(string)
|
||
desc, _ := t["description"].(string)
|
||
params := t["parameters"]
|
||
if name == "" {
|
||
// 兼容旧式嵌套 {type:"function", function:{...}}
|
||
if fn, ok := t["function"].(map[string]any); ok {
|
||
name, _ = fn["name"].(string)
|
||
desc, _ = fn["description"].(string)
|
||
params = fn["parameters"]
|
||
}
|
||
}
|
||
if name == "" {
|
||
continue
|
||
}
|
||
fn := map[string]any{"name": name}
|
||
if desc != "" {
|
||
fn["description"] = desc
|
||
}
|
||
if params != nil {
|
||
fn["parameters"] = params
|
||
}
|
||
out = append(out, map[string]any{"type": "function", "function": fn})
|
||
}
|
||
return out
|
||
}
|
||
|
||
// convertResponsesToolChoice 将 Responses tool_choice 转换为 chat tool_choice。
|
||
func convertResponsesToolChoice(tc any) any {
|
||
switch v := tc.(type) {
|
||
case string:
|
||
if v == "auto" || v == "none" || v == "required" {
|
||
return v
|
||
}
|
||
case map[string]any:
|
||
if typ, _ := v["type"].(string); typ == "function" {
|
||
name, _ := v["name"].(string)
|
||
if name == "" {
|
||
if fn, ok := v["function"].(map[string]any); ok {
|
||
name, _ = fn["name"].(string)
|
||
}
|
||
}
|
||
if name != "" {
|
||
return map[string]any{"type": "function", "function": map[string]any{"name": name}}
|
||
}
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// writeResponsesAggregate 聚合上游 SSE 后转换为 Responses 非流式响应。
|
||
func writeResponsesAggregate(w http.ResponseWriter, resp *http.Response, modelName string, reqID uint64, acc *Account, prof *upstreamProfile, startTime time.Time) {
|
||
defer resp.Body.Close()
|
||
completionJSON, err := aggregateCompletion(resp.Body, modelName)
|
||
if err != nil {
|
||
log.Printf("[#%d] 聚合响应失败: %v", reqID, err)
|
||
writeOpenAIError(w, http.StatusInternalServerError, "aggregate_error", "聚合上游流式响应失败: "+err.Error())
|
||
return
|
||
}
|
||
out, err := chatCompletionToResponses(completionJSON, modelName)
|
||
if err != nil {
|
||
log.Printf("[#%d] Responses 转换失败: %v", reqID, err)
|
||
writeOpenAIError(w, http.StatusInternalServerError, "translate_error", "转换 Responses 响应失败: "+err.Error())
|
||
return
|
||
}
|
||
w.Header().Set("Content-Type", "application/json")
|
||
w.WriteHeader(http.StatusOK)
|
||
_, _ = w.Write(out)
|
||
log.Printf("[#%d] Responses 非流式响应完成 (账号 %s [%s], 耗时 %v)", reqID, acc.Path, prof.Label, time.Since(startTime))
|
||
}
|
||
|
||
// chatCompletionToResponses 将 chat.completion JSON 转换为 Responses 响应对象。
|
||
func chatCompletionToResponses(chatJSON []byte, modelName string) ([]byte, error) {
|
||
var chat map[string]any
|
||
if err := json.Unmarshal(chatJSON, &chat); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
var content, reasoning string
|
||
var toolCalls []any
|
||
if choices, ok := chat["choices"].([]any); ok && len(choices) > 0 {
|
||
if choice, ok := choices[0].(map[string]any); ok {
|
||
if msg, ok := choice["message"].(map[string]any); ok {
|
||
content, _ = msg["content"].(string)
|
||
reasoning, _ = msg["reasoning_content"].(string)
|
||
toolCalls, _ = msg["tool_calls"].([]any)
|
||
}
|
||
}
|
||
}
|
||
|
||
output := []any{}
|
||
if reasoning != "" {
|
||
output = append(output, map[string]any{
|
||
"id": "rs_" + compactUUID(),
|
||
"type": "reasoning",
|
||
"status": "completed",
|
||
"summary": []any{map[string]any{"type": "summary_text", "text": reasoning}},
|
||
})
|
||
}
|
||
if content != "" || len(toolCalls) == 0 {
|
||
output = append(output, map[string]any{
|
||
"id": "msg_" + compactUUID(),
|
||
"type": "message",
|
||
"status": "completed",
|
||
"role": "assistant",
|
||
"content": []any{
|
||
map[string]any{"type": "output_text", "text": content, "annotations": []any{}},
|
||
},
|
||
})
|
||
}
|
||
for _, tcAny := range toolCalls {
|
||
tc, ok := tcAny.(map[string]any)
|
||
if !ok {
|
||
continue
|
||
}
|
||
callID, _ := tc["id"].(string)
|
||
name, args := "", ""
|
||
if fn, ok := tc["function"].(map[string]any); ok {
|
||
name, _ = fn["name"].(string)
|
||
args, _ = fn["arguments"].(string)
|
||
}
|
||
output = append(output, map[string]any{
|
||
"id": "fc_" + compactUUID(),
|
||
"type": "function_call",
|
||
"status": "completed",
|
||
"call_id": ifEmpty(callID, "call_"+compactUUID()),
|
||
"name": name,
|
||
"arguments": args,
|
||
})
|
||
}
|
||
|
||
now := time.Now().Unix()
|
||
created := now
|
||
if v, ok := chat["created"].(float64); ok && v > 0 && int64(v) <= now {
|
||
created = int64(v)
|
||
}
|
||
respID, _ := chat["id"].(string)
|
||
if respID == "" {
|
||
respID = "resp_" + compactUUID()
|
||
} else {
|
||
respID = "resp_" + strings.TrimPrefix(respID, "chatcmpl-")
|
||
}
|
||
|
||
result := buildResponsesEnvelope(respID, modelName, created)
|
||
result["status"] = "completed"
|
||
result["output"] = output
|
||
result["completed_at"] = now
|
||
if usage, ok := chat["usage"].(map[string]any); ok {
|
||
result["usage"] = toResponsesUsage(usage)
|
||
}
|
||
return json.Marshal(result)
|
||
}
|
||
|
||
// buildResponsesEnvelope 构造 Responses 响应骨架(含规范要求的常见字段)。
|
||
func buildResponsesEnvelope(id, model string, createdAt int64) map[string]any {
|
||
return map[string]any{
|
||
"id": id,
|
||
"object": "response",
|
||
"created_at": createdAt,
|
||
"completed_at": nil,
|
||
"status": "in_progress",
|
||
"error": nil,
|
||
"incomplete_details": nil,
|
||
"instructions": nil,
|
||
"max_output_tokens": nil,
|
||
"model": model,
|
||
"output": []any{},
|
||
"parallel_tool_calls": true,
|
||
"previous_response_id": nil,
|
||
"reasoning": map[string]any{"effort": nil, "summary": nil},
|
||
"store": true,
|
||
"temperature": 1.0,
|
||
"text": map[string]any{"format": map[string]any{"type": "text"}},
|
||
"tool_choice": "auto",
|
||
"tools": []any{},
|
||
"top_p": 1.0,
|
||
"truncation": "disabled",
|
||
"usage": nil,
|
||
"metadata": map[string]any{},
|
||
}
|
||
}
|
||
|
||
// toResponsesUsage 将 chat usage 转换为 Responses usage 结构。
|
||
func toResponsesUsage(u map[string]any) map[string]any {
|
||
in := numOr0(u["prompt_tokens"])
|
||
out := numOr0(u["completion_tokens"])
|
||
total := numOr0(u["total_tokens"])
|
||
if total == 0 {
|
||
total = in + out
|
||
}
|
||
cached, reasoningTokens := 0.0, 0.0
|
||
if d, ok := u["prompt_tokens_details"].(map[string]any); ok {
|
||
cached = numOr0(d["cached_tokens"])
|
||
}
|
||
if d, ok := u["completion_tokens_details"].(map[string]any); ok {
|
||
reasoningTokens = numOr0(d["reasoning_tokens"])
|
||
}
|
||
return map[string]any{
|
||
"input_tokens": int64(in),
|
||
"input_tokens_details": map[string]any{"cached_tokens": int64(cached)},
|
||
"output_tokens": int64(out),
|
||
"output_tokens_details": map[string]any{"reasoning_tokens": int64(reasoningTokens)},
|
||
"total_tokens": int64(total),
|
||
}
|
||
}
|
||
|
||
func numOr0(v any) float64 {
|
||
if f, ok := v.(float64); ok {
|
||
return f
|
||
}
|
||
return 0
|
||
}
|
||
|
||
// streamResponsesResponse 将上游 Chat Completions SSE 实时转译为 Responses 语义事件流。
|
||
func streamResponsesResponse(w http.ResponseWriter, resp *http.Response, modelName string, reqID uint64, acc *Account, prof *upstreamProfile, startTime time.Time) {
|
||
defer resp.Body.Close()
|
||
w.Header().Set("Content-Type", "text/event-stream")
|
||
w.Header().Set("Cache-Control", "no-cache")
|
||
w.Header().Set("Connection", "keep-alive")
|
||
w.Header().Set("X-Accel-Buffering", "no")
|
||
|
||
flusher, ok := w.(http.Flusher)
|
||
if !ok {
|
||
writeOpenAIError(w, http.StatusInternalServerError, "streaming_unsupported", "服务器不支持流式响应 Flush")
|
||
return
|
||
}
|
||
|
||
seq := 0
|
||
emit := func(eventType string, data map[string]any) {
|
||
data["type"] = eventType
|
||
data["sequence_number"] = seq
|
||
seq++
|
||
b, err := json.Marshal(data)
|
||
if err != nil {
|
||
return
|
||
}
|
||
_, _ = fmt.Fprintf(w, "event: %s\ndata: %s\n\n", eventType, b)
|
||
flusher.Flush()
|
||
}
|
||
|
||
created := time.Now().Unix()
|
||
respID := "resp_" + compactUUID()
|
||
envelope := buildResponsesEnvelope(respID, modelName, created)
|
||
emit("response.created", map[string]any{"response": envelope})
|
||
emit("response.in_progress", map[string]any{"response": envelope})
|
||
|
||
nextIndex := 0
|
||
idxToItem := map[int]map[string]any{}
|
||
|
||
// reasoning item 状态
|
||
reasoningOpen := false
|
||
reasoningIndex := -1
|
||
reasoningItemID := ""
|
||
var reasoningSB strings.Builder
|
||
|
||
// message item 状态
|
||
msgOpen := false
|
||
msgIndex := -1
|
||
msgItemID := ""
|
||
var msgSB strings.Builder
|
||
|
||
// function_call item 状态
|
||
toolCalls := map[int]*mergedToolCall{}
|
||
var toolOrder []int
|
||
tcItemID := map[int]string{}
|
||
tcIndex := map[int]int{}
|
||
|
||
openReasoning := func() {
|
||
if reasoningOpen {
|
||
return
|
||
}
|
||
reasoningOpen = true
|
||
reasoningIndex = nextIndex
|
||
nextIndex++
|
||
reasoningItemID = "rs_" + compactUUID()
|
||
emit("response.output_item.added", map[string]any{
|
||
"output_index": reasoningIndex,
|
||
"item": map[string]any{"id": reasoningItemID, "type": "reasoning", "status": "in_progress", "summary": []any{}},
|
||
})
|
||
emit("response.reasoning_summary_part.added", map[string]any{
|
||
"item_id": reasoningItemID, "output_index": reasoningIndex, "summary_index": 0,
|
||
"part": map[string]any{"type": "summary_text", "text": ""},
|
||
})
|
||
}
|
||
|
||
openMessage := func() {
|
||
if msgOpen {
|
||
return
|
||
}
|
||
msgOpen = true
|
||
msgIndex = nextIndex
|
||
nextIndex++
|
||
msgItemID = "msg_" + compactUUID()
|
||
emit("response.output_item.added", map[string]any{
|
||
"output_index": msgIndex,
|
||
"item": map[string]any{"id": msgItemID, "type": "message", "status": "in_progress", "role": "assistant", "content": []any{}},
|
||
})
|
||
emit("response.content_part.added", map[string]any{
|
||
"item_id": msgItemID, "output_index": msgIndex, "content_index": 0,
|
||
"part": map[string]any{"type": "output_text", "annotations": []any{}, "text": ""},
|
||
})
|
||
}
|
||
|
||
var usage map[string]any
|
||
|
||
scanner := bufio.NewScanner(resp.Body)
|
||
scanner.Buffer(make([]byte, 64*1024), 4*1024*1024)
|
||
for scanner.Scan() {
|
||
data := stripDataPrefix(scanner.Text())
|
||
if data == "" || data == "[DONE]" {
|
||
continue
|
||
}
|
||
var chunk map[string]any
|
||
if json.Unmarshal([]byte(data), &chunk) != nil {
|
||
continue
|
||
}
|
||
if u, ok := chunk["usage"].(map[string]any); ok {
|
||
usage = u
|
||
}
|
||
choices, _ := chunk["choices"].([]any)
|
||
for _, c := range choices {
|
||
choice, _ := c.(map[string]any)
|
||
delta, _ := choice["delta"].(map[string]any)
|
||
if delta == nil {
|
||
continue
|
||
}
|
||
if rc, ok := delta["reasoning_content"].(string); ok && rc != "" {
|
||
openReasoning()
|
||
reasoningSB.WriteString(rc)
|
||
emit("response.reasoning_summary_text.delta", map[string]any{
|
||
"item_id": reasoningItemID, "output_index": reasoningIndex, "summary_index": 0, "delta": rc,
|
||
})
|
||
}
|
||
if ct, ok := delta["content"].(string); ok && ct != "" {
|
||
openMessage()
|
||
msgSB.WriteString(ct)
|
||
emit("response.output_text.delta", map[string]any{
|
||
"item_id": msgItemID, "output_index": msgIndex, "content_index": 0, "delta": ct,
|
||
})
|
||
}
|
||
if tcs, ok := delta["tool_calls"].([]any); ok && len(tcs) > 0 {
|
||
// 先提取本次参数增量,供合并后补发 delta 事件
|
||
type argDelta struct {
|
||
idx int
|
||
arg string
|
||
}
|
||
var argDeltas []argDelta
|
||
for _, tcAny := range tcs {
|
||
tc, ok := tcAny.(map[string]any)
|
||
if !ok {
|
||
continue
|
||
}
|
||
idx := 0
|
||
if v, ok := tc["index"].(float64); ok {
|
||
idx = int(v)
|
||
}
|
||
if fn, ok := tc["function"].(map[string]any); ok {
|
||
if a, ok := fn["arguments"].(string); ok && a != "" {
|
||
argDeltas = append(argDeltas, argDelta{idx: idx, arg: a})
|
||
}
|
||
}
|
||
}
|
||
before := len(toolOrder)
|
||
applyToolCallDelta(toolCalls, &toolOrder, tcs)
|
||
for i := before; i < len(toolOrder); i++ {
|
||
idx := toolOrder[i]
|
||
st := toolCalls[idx]
|
||
if st.ID == "" {
|
||
st.ID = "call_" + compactUUID()
|
||
}
|
||
tcItemID[idx] = "fc_" + compactUUID()
|
||
tcIndex[idx] = nextIndex
|
||
nextIndex++
|
||
emit("response.output_item.added", map[string]any{
|
||
"output_index": tcIndex[idx],
|
||
"item": map[string]any{
|
||
"id": tcItemID[idx], "type": "function_call", "status": "in_progress",
|
||
"call_id": st.ID, "name": st.Name, "arguments": "",
|
||
},
|
||
})
|
||
}
|
||
for _, ad := range argDeltas {
|
||
emit("response.function_call_arguments.delta", map[string]any{
|
||
"item_id": tcItemID[ad.idx], "output_index": tcIndex[ad.idx], "delta": ad.arg,
|
||
})
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 若无任何输出,补一个空 message,保证 output 非空且事件序列完整
|
||
if !reasoningOpen && !msgOpen && len(toolOrder) == 0 {
|
||
openMessage()
|
||
}
|
||
|
||
// 收尾:reasoning
|
||
if reasoningOpen {
|
||
text := reasoningSB.String()
|
||
emit("response.reasoning_summary_text.done", map[string]any{"item_id": reasoningItemID, "output_index": reasoningIndex, "summary_index": 0, "text": text})
|
||
emit("response.reasoning_summary_part.done", map[string]any{"item_id": reasoningItemID, "output_index": reasoningIndex, "summary_index": 0, "part": map[string]any{"type": "summary_text", "text": text}})
|
||
item := map[string]any{"id": reasoningItemID, "type": "reasoning", "status": "completed", "summary": []any{map[string]any{"type": "summary_text", "text": text}}}
|
||
emit("response.output_item.done", map[string]any{"output_index": reasoningIndex, "item": item})
|
||
idxToItem[reasoningIndex] = item
|
||
}
|
||
|
||
// 收尾:message
|
||
if msgOpen {
|
||
text := msgSB.String()
|
||
emit("response.output_text.done", map[string]any{"item_id": msgItemID, "output_index": msgIndex, "content_index": 0, "text": text})
|
||
emit("response.content_part.done", map[string]any{"item_id": msgItemID, "output_index": msgIndex, "content_index": 0, "part": map[string]any{"type": "output_text", "annotations": []any{}, "text": text}})
|
||
item := map[string]any{"id": msgItemID, "type": "message", "status": "completed", "role": "assistant", "content": []any{map[string]any{"type": "output_text", "annotations": []any{}, "text": text}}}
|
||
emit("response.output_item.done", map[string]any{"output_index": msgIndex, "item": item})
|
||
idxToItem[msgIndex] = item
|
||
}
|
||
|
||
// 收尾:function calls
|
||
for _, idx := range toolOrder {
|
||
st := toolCalls[idx]
|
||
args := st.Args.String()
|
||
emit("response.function_call_arguments.done", map[string]any{"item_id": tcItemID[idx], "output_index": tcIndex[idx], "name": st.Name, "arguments": args})
|
||
item := map[string]any{"id": tcItemID[idx], "type": "function_call", "status": "completed", "call_id": st.ID, "name": st.Name, "arguments": args}
|
||
emit("response.output_item.done", map[string]any{"output_index": tcIndex[idx], "item": item})
|
||
idxToItem[tcIndex[idx]] = item
|
||
}
|
||
|
||
output := make([]any, 0, len(idxToItem))
|
||
for i := 0; i < nextIndex; i++ {
|
||
if item, ok := idxToItem[i]; ok {
|
||
output = append(output, item)
|
||
}
|
||
}
|
||
|
||
final := buildResponsesEnvelope(respID, modelName, created)
|
||
final["status"] = "completed"
|
||
final["output"] = output
|
||
final["completed_at"] = time.Now().Unix()
|
||
if usage != nil {
|
||
final["usage"] = toResponsesUsage(usage)
|
||
}
|
||
emit("response.completed", map[string]any{"response": final})
|
||
|
||
_, _ = fmt.Fprintf(w, "data: [DONE]\n\n")
|
||
flusher.Flush()
|
||
log.Printf("[#%d] Responses 流式输出完成 (账号 %s [%s], 耗时 %v)", reqID, acc.Path, prof.Label, time.Since(startTime))
|
||
}
|
||
|
||
// compactUUID 返回去掉连字符的随机 UUID。
|
||
func compactUUID() string {
|
||
return strings.ReplaceAll(uuid.NewString(), "-", "")
|
||
}
|