mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-02 20:40:52 +08:00
776 lines
32 KiB
Go
776 lines
32 KiB
Go
package responses
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/tidwall/gjson"
|
|
"github.com/tidwall/sjson"
|
|
)
|
|
|
|
type oaiToResponsesState struct {
|
|
Seq int
|
|
ResponseID string
|
|
Created int64
|
|
Started bool
|
|
ReasoningID string
|
|
ReasoningIndex int
|
|
// aggregation buffers for response.output
|
|
// Per-output message text buffers by index
|
|
MsgTextBuf map[int]*strings.Builder
|
|
ReasoningBuf strings.Builder
|
|
FuncArgsBuf map[int]*strings.Builder // index -> args
|
|
FuncNames map[int]string // index -> name
|
|
FuncCallIDs map[int]string // index -> call_id
|
|
// message item state per output index
|
|
MsgItemAdded map[int]bool // whether response.output_item.added emitted for message
|
|
MsgContentAdded map[int]bool // whether response.content_part.added emitted for message
|
|
MsgItemDone map[int]bool // whether message done events were emitted
|
|
// function item done state
|
|
FuncArgsDone map[int]bool
|
|
FuncItemDone map[int]bool
|
|
// usage aggregation
|
|
PromptTokens int64
|
|
CachedTokens int64
|
|
CompletionTokens int64
|
|
TotalTokens int64
|
|
ReasoningTokens int64
|
|
UsageSeen bool
|
|
}
|
|
|
|
func emitRespEvent(event string, payload string) string {
|
|
return fmt.Sprintf("event: %s\ndata: %s", event, payload)
|
|
}
|
|
|
|
// ConvertOpenAIChatCompletionsResponseToOpenAIResponses converts OpenAI Chat Completions streaming chunks
|
|
// to OpenAI Responses SSE events (response.*).
|
|
func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) []string {
|
|
if *param == nil {
|
|
*param = &oaiToResponsesState{
|
|
FuncArgsBuf: make(map[int]*strings.Builder),
|
|
FuncNames: make(map[int]string),
|
|
FuncCallIDs: make(map[int]string),
|
|
MsgTextBuf: make(map[int]*strings.Builder),
|
|
MsgItemAdded: make(map[int]bool),
|
|
MsgContentAdded: make(map[int]bool),
|
|
MsgItemDone: make(map[int]bool),
|
|
FuncArgsDone: make(map[int]bool),
|
|
FuncItemDone: make(map[int]bool),
|
|
}
|
|
}
|
|
st := (*param).(*oaiToResponsesState)
|
|
|
|
if bytes.HasPrefix(rawJSON, []byte("data:")) {
|
|
rawJSON = bytes.TrimSpace(rawJSON[5:])
|
|
}
|
|
|
|
rawJSON = bytes.TrimSpace(rawJSON)
|
|
if len(rawJSON) == 0 {
|
|
return []string{}
|
|
}
|
|
if bytes.Equal(rawJSON, []byte("[DONE]")) {
|
|
return []string{}
|
|
}
|
|
|
|
root := gjson.ParseBytes(rawJSON)
|
|
obj := root.Get("object")
|
|
if obj.Exists() && obj.String() != "" && obj.String() != "chat.completion.chunk" {
|
|
return []string{}
|
|
}
|
|
if !root.Get("choices").Exists() || !root.Get("choices").IsArray() {
|
|
return []string{}
|
|
}
|
|
|
|
if usage := root.Get("usage"); usage.Exists() {
|
|
if v := usage.Get("prompt_tokens"); v.Exists() {
|
|
st.PromptTokens = v.Int()
|
|
st.UsageSeen = true
|
|
}
|
|
if v := usage.Get("prompt_tokens_details.cached_tokens"); v.Exists() {
|
|
st.CachedTokens = v.Int()
|
|
st.UsageSeen = true
|
|
}
|
|
if v := usage.Get("completion_tokens"); v.Exists() {
|
|
st.CompletionTokens = v.Int()
|
|
st.UsageSeen = true
|
|
} else if v := usage.Get("output_tokens"); v.Exists() {
|
|
st.CompletionTokens = v.Int()
|
|
st.UsageSeen = true
|
|
}
|
|
if v := usage.Get("output_tokens_details.reasoning_tokens"); v.Exists() {
|
|
st.ReasoningTokens = v.Int()
|
|
st.UsageSeen = true
|
|
} else if v := usage.Get("completion_tokens_details.reasoning_tokens"); v.Exists() {
|
|
st.ReasoningTokens = v.Int()
|
|
st.UsageSeen = true
|
|
}
|
|
if v := usage.Get("total_tokens"); v.Exists() {
|
|
st.TotalTokens = v.Int()
|
|
st.UsageSeen = true
|
|
}
|
|
}
|
|
|
|
nextSeq := func() int { st.Seq++; return st.Seq }
|
|
var out []string
|
|
|
|
if !st.Started {
|
|
st.ResponseID = root.Get("id").String()
|
|
st.Created = root.Get("created").Int()
|
|
// reset aggregation state for a new streaming response
|
|
st.MsgTextBuf = make(map[int]*strings.Builder)
|
|
st.ReasoningBuf.Reset()
|
|
st.ReasoningID = ""
|
|
st.ReasoningIndex = 0
|
|
st.FuncArgsBuf = make(map[int]*strings.Builder)
|
|
st.FuncNames = make(map[int]string)
|
|
st.FuncCallIDs = make(map[int]string)
|
|
st.MsgItemAdded = make(map[int]bool)
|
|
st.MsgContentAdded = make(map[int]bool)
|
|
st.MsgItemDone = make(map[int]bool)
|
|
st.FuncArgsDone = make(map[int]bool)
|
|
st.FuncItemDone = make(map[int]bool)
|
|
st.PromptTokens = 0
|
|
st.CachedTokens = 0
|
|
st.CompletionTokens = 0
|
|
st.TotalTokens = 0
|
|
st.ReasoningTokens = 0
|
|
st.UsageSeen = false
|
|
// response.created
|
|
created := `{"type":"response.created","sequence_number":0,"response":{"id":"","object":"response","created_at":0,"status":"in_progress","background":false,"error":null}}`
|
|
created, _ = sjson.Set(created, "sequence_number", nextSeq())
|
|
created, _ = sjson.Set(created, "response.id", st.ResponseID)
|
|
created, _ = sjson.Set(created, "response.created_at", st.Created)
|
|
out = append(out, emitRespEvent("response.created", created))
|
|
|
|
inprog := `{"type":"response.in_progress","sequence_number":0,"response":{"id":"","object":"response","created_at":0,"status":"in_progress"}}`
|
|
inprog, _ = sjson.Set(inprog, "sequence_number", nextSeq())
|
|
inprog, _ = sjson.Set(inprog, "response.id", st.ResponseID)
|
|
inprog, _ = sjson.Set(inprog, "response.created_at", st.Created)
|
|
out = append(out, emitRespEvent("response.in_progress", inprog))
|
|
st.Started = true
|
|
}
|
|
|
|
// choices[].delta content / tool_calls / reasoning_content
|
|
if choices := root.Get("choices"); choices.Exists() && choices.IsArray() {
|
|
choices.ForEach(func(_, choice gjson.Result) bool {
|
|
idx := int(choice.Get("index").Int())
|
|
delta := choice.Get("delta")
|
|
if delta.Exists() {
|
|
if c := delta.Get("content"); c.Exists() && c.String() != "" {
|
|
// Ensure the message item and its first content part are announced before any text deltas
|
|
if !st.MsgItemAdded[idx] {
|
|
item := `{"type":"response.output_item.added","sequence_number":0,"output_index":0,"item":{"id":"","type":"message","status":"in_progress","content":[],"role":"assistant"}}`
|
|
item, _ = sjson.Set(item, "sequence_number", nextSeq())
|
|
item, _ = sjson.Set(item, "output_index", idx)
|
|
item, _ = sjson.Set(item, "item.id", fmt.Sprintf("msg_%s_%d", st.ResponseID, idx))
|
|
out = append(out, emitRespEvent("response.output_item.added", item))
|
|
st.MsgItemAdded[idx] = true
|
|
}
|
|
if !st.MsgContentAdded[idx] {
|
|
part := `{"type":"response.content_part.added","sequence_number":0,"item_id":"","output_index":0,"content_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":""}}`
|
|
part, _ = sjson.Set(part, "sequence_number", nextSeq())
|
|
part, _ = sjson.Set(part, "item_id", fmt.Sprintf("msg_%s_%d", st.ResponseID, idx))
|
|
part, _ = sjson.Set(part, "output_index", idx)
|
|
part, _ = sjson.Set(part, "content_index", 0)
|
|
out = append(out, emitRespEvent("response.content_part.added", part))
|
|
st.MsgContentAdded[idx] = true
|
|
}
|
|
|
|
msg := `{"type":"response.output_text.delta","sequence_number":0,"item_id":"","output_index":0,"content_index":0,"delta":"","logprobs":[]}`
|
|
msg, _ = sjson.Set(msg, "sequence_number", nextSeq())
|
|
msg, _ = sjson.Set(msg, "item_id", fmt.Sprintf("msg_%s_%d", st.ResponseID, idx))
|
|
msg, _ = sjson.Set(msg, "output_index", idx)
|
|
msg, _ = sjson.Set(msg, "content_index", 0)
|
|
msg, _ = sjson.Set(msg, "delta", c.String())
|
|
out = append(out, emitRespEvent("response.output_text.delta", msg))
|
|
// aggregate for response.output
|
|
if st.MsgTextBuf[idx] == nil {
|
|
st.MsgTextBuf[idx] = &strings.Builder{}
|
|
}
|
|
st.MsgTextBuf[idx].WriteString(c.String())
|
|
}
|
|
|
|
// reasoning_content (OpenAI reasoning incremental text)
|
|
if rc := delta.Get("reasoning_content"); rc.Exists() && rc.String() != "" {
|
|
// On first appearance, add reasoning item and part
|
|
if st.ReasoningID == "" {
|
|
st.ReasoningID = fmt.Sprintf("rs_%s_%d", st.ResponseID, idx)
|
|
st.ReasoningIndex = idx
|
|
item := `{"type":"response.output_item.added","sequence_number":0,"output_index":0,"item":{"id":"","type":"reasoning","status":"in_progress","summary":[]}}`
|
|
item, _ = sjson.Set(item, "sequence_number", nextSeq())
|
|
item, _ = sjson.Set(item, "output_index", idx)
|
|
item, _ = sjson.Set(item, "item.id", st.ReasoningID)
|
|
out = append(out, emitRespEvent("response.output_item.added", item))
|
|
part := `{"type":"response.reasoning_summary_part.added","sequence_number":0,"item_id":"","output_index":0,"summary_index":0,"part":{"type":"summary_text","text":""}}`
|
|
part, _ = sjson.Set(part, "sequence_number", nextSeq())
|
|
part, _ = sjson.Set(part, "item_id", st.ReasoningID)
|
|
part, _ = sjson.Set(part, "output_index", st.ReasoningIndex)
|
|
out = append(out, emitRespEvent("response.reasoning_summary_part.added", part))
|
|
}
|
|
// Append incremental text to reasoning buffer
|
|
st.ReasoningBuf.WriteString(rc.String())
|
|
msg := `{"type":"response.reasoning_summary_text.delta","sequence_number":0,"item_id":"","output_index":0,"summary_index":0,"text":""}`
|
|
msg, _ = sjson.Set(msg, "sequence_number", nextSeq())
|
|
msg, _ = sjson.Set(msg, "item_id", st.ReasoningID)
|
|
msg, _ = sjson.Set(msg, "output_index", st.ReasoningIndex)
|
|
msg, _ = sjson.Set(msg, "text", rc.String())
|
|
out = append(out, emitRespEvent("response.reasoning_summary_text.delta", msg))
|
|
}
|
|
|
|
// tool calls
|
|
if tcs := delta.Get("tool_calls"); tcs.Exists() && tcs.IsArray() {
|
|
// Before emitting any function events, if a message is open for this index,
|
|
// close its text/content to match Codex expected ordering.
|
|
if st.MsgItemAdded[idx] && !st.MsgItemDone[idx] {
|
|
fullText := ""
|
|
if b := st.MsgTextBuf[idx]; b != nil {
|
|
fullText = b.String()
|
|
}
|
|
done := `{"type":"response.output_text.done","sequence_number":0,"item_id":"","output_index":0,"content_index":0,"text":"","logprobs":[]}`
|
|
done, _ = sjson.Set(done, "sequence_number", nextSeq())
|
|
done, _ = sjson.Set(done, "item_id", fmt.Sprintf("msg_%s_%d", st.ResponseID, idx))
|
|
done, _ = sjson.Set(done, "output_index", idx)
|
|
done, _ = sjson.Set(done, "content_index", 0)
|
|
done, _ = sjson.Set(done, "text", fullText)
|
|
out = append(out, emitRespEvent("response.output_text.done", done))
|
|
|
|
partDone := `{"type":"response.content_part.done","sequence_number":0,"item_id":"","output_index":0,"content_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":""}}`
|
|
partDone, _ = sjson.Set(partDone, "sequence_number", nextSeq())
|
|
partDone, _ = sjson.Set(partDone, "item_id", fmt.Sprintf("msg_%s_%d", st.ResponseID, idx))
|
|
partDone, _ = sjson.Set(partDone, "output_index", idx)
|
|
partDone, _ = sjson.Set(partDone, "content_index", 0)
|
|
partDone, _ = sjson.Set(partDone, "part.text", fullText)
|
|
out = append(out, emitRespEvent("response.content_part.done", partDone))
|
|
|
|
itemDone := `{"type":"response.output_item.done","sequence_number":0,"output_index":0,"item":{"id":"","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":""}],"role":"assistant"}}`
|
|
itemDone, _ = sjson.Set(itemDone, "sequence_number", nextSeq())
|
|
itemDone, _ = sjson.Set(itemDone, "output_index", idx)
|
|
itemDone, _ = sjson.Set(itemDone, "item.id", fmt.Sprintf("msg_%s_%d", st.ResponseID, idx))
|
|
itemDone, _ = sjson.Set(itemDone, "item.content.0.text", fullText)
|
|
out = append(out, emitRespEvent("response.output_item.done", itemDone))
|
|
st.MsgItemDone[idx] = true
|
|
}
|
|
|
|
// Only emit item.added once per tool call and preserve call_id across chunks.
|
|
newCallID := tcs.Get("0.id").String()
|
|
nameChunk := tcs.Get("0.function.name").String()
|
|
if nameChunk != "" {
|
|
st.FuncNames[idx] = nameChunk
|
|
}
|
|
existingCallID := st.FuncCallIDs[idx]
|
|
effectiveCallID := existingCallID
|
|
shouldEmitItem := false
|
|
if existingCallID == "" && newCallID != "" {
|
|
// First time seeing a valid call_id for this index
|
|
effectiveCallID = newCallID
|
|
st.FuncCallIDs[idx] = newCallID
|
|
shouldEmitItem = true
|
|
}
|
|
|
|
if shouldEmitItem && effectiveCallID != "" {
|
|
o := `{"type":"response.output_item.added","sequence_number":0,"output_index":0,"item":{"id":"","type":"function_call","status":"in_progress","arguments":"","call_id":"","name":""}}`
|
|
o, _ = sjson.Set(o, "sequence_number", nextSeq())
|
|
o, _ = sjson.Set(o, "output_index", idx)
|
|
o, _ = sjson.Set(o, "item.id", fmt.Sprintf("fc_%s", effectiveCallID))
|
|
o, _ = sjson.Set(o, "item.call_id", effectiveCallID)
|
|
name := st.FuncNames[idx]
|
|
o, _ = sjson.Set(o, "item.name", name)
|
|
out = append(out, emitRespEvent("response.output_item.added", o))
|
|
}
|
|
|
|
// Ensure args buffer exists for this index
|
|
if st.FuncArgsBuf[idx] == nil {
|
|
st.FuncArgsBuf[idx] = &strings.Builder{}
|
|
}
|
|
|
|
// Append arguments delta if available and we have a valid call_id to reference
|
|
if args := tcs.Get("0.function.arguments"); args.Exists() && args.String() != "" {
|
|
// Prefer an already known call_id; fall back to newCallID if first time
|
|
refCallID := st.FuncCallIDs[idx]
|
|
if refCallID == "" {
|
|
refCallID = newCallID
|
|
}
|
|
if refCallID != "" {
|
|
ad := `{"type":"response.function_call_arguments.delta","sequence_number":0,"item_id":"","output_index":0,"delta":""}`
|
|
ad, _ = sjson.Set(ad, "sequence_number", nextSeq())
|
|
ad, _ = sjson.Set(ad, "item_id", fmt.Sprintf("fc_%s", refCallID))
|
|
ad, _ = sjson.Set(ad, "output_index", idx)
|
|
ad, _ = sjson.Set(ad, "delta", args.String())
|
|
out = append(out, emitRespEvent("response.function_call_arguments.delta", ad))
|
|
}
|
|
st.FuncArgsBuf[idx].WriteString(args.String())
|
|
}
|
|
}
|
|
}
|
|
|
|
// finish_reason triggers finalization, including text done/content done/item done,
|
|
// reasoning done/part.done, function args done/item done, and completed
|
|
if fr := choice.Get("finish_reason"); fr.Exists() && fr.String() != "" {
|
|
// Emit message done events for all indices that started a message
|
|
if len(st.MsgItemAdded) > 0 {
|
|
// sort indices for deterministic order
|
|
idxs := make([]int, 0, len(st.MsgItemAdded))
|
|
for i := range st.MsgItemAdded {
|
|
idxs = append(idxs, i)
|
|
}
|
|
for i := 0; i < len(idxs); i++ {
|
|
for j := i + 1; j < len(idxs); j++ {
|
|
if idxs[j] < idxs[i] {
|
|
idxs[i], idxs[j] = idxs[j], idxs[i]
|
|
}
|
|
}
|
|
}
|
|
for _, i := range idxs {
|
|
if st.MsgItemAdded[i] && !st.MsgItemDone[i] {
|
|
fullText := ""
|
|
if b := st.MsgTextBuf[i]; b != nil {
|
|
fullText = b.String()
|
|
}
|
|
done := `{"type":"response.output_text.done","sequence_number":0,"item_id":"","output_index":0,"content_index":0,"text":"","logprobs":[]}`
|
|
done, _ = sjson.Set(done, "sequence_number", nextSeq())
|
|
done, _ = sjson.Set(done, "item_id", fmt.Sprintf("msg_%s_%d", st.ResponseID, i))
|
|
done, _ = sjson.Set(done, "output_index", i)
|
|
done, _ = sjson.Set(done, "content_index", 0)
|
|
done, _ = sjson.Set(done, "text", fullText)
|
|
out = append(out, emitRespEvent("response.output_text.done", done))
|
|
|
|
partDone := `{"type":"response.content_part.done","sequence_number":0,"item_id":"","output_index":0,"content_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":""}}`
|
|
partDone, _ = sjson.Set(partDone, "sequence_number", nextSeq())
|
|
partDone, _ = sjson.Set(partDone, "item_id", fmt.Sprintf("msg_%s_%d", st.ResponseID, i))
|
|
partDone, _ = sjson.Set(partDone, "output_index", i)
|
|
partDone, _ = sjson.Set(partDone, "content_index", 0)
|
|
partDone, _ = sjson.Set(partDone, "part.text", fullText)
|
|
out = append(out, emitRespEvent("response.content_part.done", partDone))
|
|
|
|
itemDone := `{"type":"response.output_item.done","sequence_number":0,"output_index":0,"item":{"id":"","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":""}],"role":"assistant"}}`
|
|
itemDone, _ = sjson.Set(itemDone, "sequence_number", nextSeq())
|
|
itemDone, _ = sjson.Set(itemDone, "output_index", i)
|
|
itemDone, _ = sjson.Set(itemDone, "item.id", fmt.Sprintf("msg_%s_%d", st.ResponseID, i))
|
|
itemDone, _ = sjson.Set(itemDone, "item.content.0.text", fullText)
|
|
out = append(out, emitRespEvent("response.output_item.done", itemDone))
|
|
st.MsgItemDone[i] = true
|
|
}
|
|
}
|
|
}
|
|
|
|
if st.ReasoningID != "" {
|
|
// Emit reasoning done events
|
|
textDone := `{"type":"response.reasoning_summary_text.done","sequence_number":0,"item_id":"","output_index":0,"summary_index":0,"text":""}`
|
|
textDone, _ = sjson.Set(textDone, "sequence_number", nextSeq())
|
|
textDone, _ = sjson.Set(textDone, "item_id", st.ReasoningID)
|
|
textDone, _ = sjson.Set(textDone, "output_index", st.ReasoningIndex)
|
|
out = append(out, emitRespEvent("response.reasoning_summary_text.done", textDone))
|
|
partDone := `{"type":"response.reasoning_summary_part.done","sequence_number":0,"item_id":"","output_index":0,"summary_index":0,"part":{"type":"summary_text","text":""}}`
|
|
partDone, _ = sjson.Set(partDone, "sequence_number", nextSeq())
|
|
partDone, _ = sjson.Set(partDone, "item_id", st.ReasoningID)
|
|
partDone, _ = sjson.Set(partDone, "output_index", st.ReasoningIndex)
|
|
out = append(out, emitRespEvent("response.reasoning_summary_part.done", partDone))
|
|
}
|
|
|
|
// Emit function call done events for any active function calls
|
|
if len(st.FuncCallIDs) > 0 {
|
|
idxs := make([]int, 0, len(st.FuncCallIDs))
|
|
for i := range st.FuncCallIDs {
|
|
idxs = append(idxs, i)
|
|
}
|
|
for i := 0; i < len(idxs); i++ {
|
|
for j := i + 1; j < len(idxs); j++ {
|
|
if idxs[j] < idxs[i] {
|
|
idxs[i], idxs[j] = idxs[j], idxs[i]
|
|
}
|
|
}
|
|
}
|
|
for _, i := range idxs {
|
|
callID := st.FuncCallIDs[i]
|
|
if callID == "" || st.FuncItemDone[i] {
|
|
continue
|
|
}
|
|
args := "{}"
|
|
if b := st.FuncArgsBuf[i]; b != nil && b.Len() > 0 {
|
|
args = b.String()
|
|
}
|
|
fcDone := `{"type":"response.function_call_arguments.done","sequence_number":0,"item_id":"","output_index":0,"arguments":""}`
|
|
fcDone, _ = sjson.Set(fcDone, "sequence_number", nextSeq())
|
|
fcDone, _ = sjson.Set(fcDone, "item_id", fmt.Sprintf("fc_%s", callID))
|
|
fcDone, _ = sjson.Set(fcDone, "output_index", i)
|
|
fcDone, _ = sjson.Set(fcDone, "arguments", args)
|
|
out = append(out, emitRespEvent("response.function_call_arguments.done", fcDone))
|
|
|
|
itemDone := `{"type":"response.output_item.done","sequence_number":0,"output_index":0,"item":{"id":"","type":"function_call","status":"completed","arguments":"","call_id":"","name":""}}`
|
|
itemDone, _ = sjson.Set(itemDone, "sequence_number", nextSeq())
|
|
itemDone, _ = sjson.Set(itemDone, "output_index", i)
|
|
itemDone, _ = sjson.Set(itemDone, "item.id", fmt.Sprintf("fc_%s", callID))
|
|
itemDone, _ = sjson.Set(itemDone, "item.arguments", args)
|
|
itemDone, _ = sjson.Set(itemDone, "item.call_id", callID)
|
|
itemDone, _ = sjson.Set(itemDone, "item.name", st.FuncNames[i])
|
|
out = append(out, emitRespEvent("response.output_item.done", itemDone))
|
|
st.FuncItemDone[i] = true
|
|
st.FuncArgsDone[i] = true
|
|
}
|
|
}
|
|
completed := `{"type":"response.completed","sequence_number":0,"response":{"id":"","object":"response","created_at":0,"status":"completed","background":false,"error":null}}`
|
|
completed, _ = sjson.Set(completed, "sequence_number", nextSeq())
|
|
completed, _ = sjson.Set(completed, "response.id", st.ResponseID)
|
|
completed, _ = sjson.Set(completed, "response.created_at", st.Created)
|
|
// Inject original request fields into response as per docs/response.completed.json
|
|
if requestRawJSON != nil {
|
|
req := gjson.ParseBytes(requestRawJSON)
|
|
if v := req.Get("instructions"); v.Exists() {
|
|
completed, _ = sjson.Set(completed, "response.instructions", v.String())
|
|
}
|
|
if v := req.Get("max_output_tokens"); v.Exists() {
|
|
completed, _ = sjson.Set(completed, "response.max_output_tokens", v.Int())
|
|
}
|
|
if v := req.Get("max_tool_calls"); v.Exists() {
|
|
completed, _ = sjson.Set(completed, "response.max_tool_calls", v.Int())
|
|
}
|
|
if v := req.Get("model"); v.Exists() {
|
|
completed, _ = sjson.Set(completed, "response.model", v.String())
|
|
}
|
|
if v := req.Get("parallel_tool_calls"); v.Exists() {
|
|
completed, _ = sjson.Set(completed, "response.parallel_tool_calls", v.Bool())
|
|
}
|
|
if v := req.Get("previous_response_id"); v.Exists() {
|
|
completed, _ = sjson.Set(completed, "response.previous_response_id", v.String())
|
|
}
|
|
if v := req.Get("prompt_cache_key"); v.Exists() {
|
|
completed, _ = sjson.Set(completed, "response.prompt_cache_key", v.String())
|
|
}
|
|
if v := req.Get("reasoning"); v.Exists() {
|
|
completed, _ = sjson.Set(completed, "response.reasoning", v.Value())
|
|
}
|
|
if v := req.Get("safety_identifier"); v.Exists() {
|
|
completed, _ = sjson.Set(completed, "response.safety_identifier", v.String())
|
|
}
|
|
if v := req.Get("service_tier"); v.Exists() {
|
|
completed, _ = sjson.Set(completed, "response.service_tier", v.String())
|
|
}
|
|
if v := req.Get("store"); v.Exists() {
|
|
completed, _ = sjson.Set(completed, "response.store", v.Bool())
|
|
}
|
|
if v := req.Get("temperature"); v.Exists() {
|
|
completed, _ = sjson.Set(completed, "response.temperature", v.Float())
|
|
}
|
|
if v := req.Get("text"); v.Exists() {
|
|
completed, _ = sjson.Set(completed, "response.text", v.Value())
|
|
}
|
|
if v := req.Get("tool_choice"); v.Exists() {
|
|
completed, _ = sjson.Set(completed, "response.tool_choice", v.Value())
|
|
}
|
|
if v := req.Get("tools"); v.Exists() {
|
|
completed, _ = sjson.Set(completed, "response.tools", v.Value())
|
|
}
|
|
if v := req.Get("top_logprobs"); v.Exists() {
|
|
completed, _ = sjson.Set(completed, "response.top_logprobs", v.Int())
|
|
}
|
|
if v := req.Get("top_p"); v.Exists() {
|
|
completed, _ = sjson.Set(completed, "response.top_p", v.Float())
|
|
}
|
|
if v := req.Get("truncation"); v.Exists() {
|
|
completed, _ = sjson.Set(completed, "response.truncation", v.String())
|
|
}
|
|
if v := req.Get("user"); v.Exists() {
|
|
completed, _ = sjson.Set(completed, "response.user", v.Value())
|
|
}
|
|
if v := req.Get("metadata"); v.Exists() {
|
|
completed, _ = sjson.Set(completed, "response.metadata", v.Value())
|
|
}
|
|
}
|
|
// Build response.output using aggregated buffers
|
|
var outputs []interface{}
|
|
if st.ReasoningBuf.Len() > 0 {
|
|
outputs = append(outputs, map[string]interface{}{
|
|
"id": st.ReasoningID,
|
|
"type": "reasoning",
|
|
"summary": []interface{}{map[string]interface{}{
|
|
"type": "summary_text",
|
|
"text": st.ReasoningBuf.String(),
|
|
}},
|
|
})
|
|
}
|
|
// Append message items in ascending index order
|
|
if len(st.MsgItemAdded) > 0 {
|
|
midxs := make([]int, 0, len(st.MsgItemAdded))
|
|
for i := range st.MsgItemAdded {
|
|
midxs = append(midxs, i)
|
|
}
|
|
for i := 0; i < len(midxs); i++ {
|
|
for j := i + 1; j < len(midxs); j++ {
|
|
if midxs[j] < midxs[i] {
|
|
midxs[i], midxs[j] = midxs[j], midxs[i]
|
|
}
|
|
}
|
|
}
|
|
for _, i := range midxs {
|
|
txt := ""
|
|
if b := st.MsgTextBuf[i]; b != nil {
|
|
txt = b.String()
|
|
}
|
|
outputs = append(outputs, map[string]interface{}{
|
|
"id": fmt.Sprintf("msg_%s_%d", st.ResponseID, i),
|
|
"type": "message",
|
|
"status": "completed",
|
|
"content": []interface{}{map[string]interface{}{
|
|
"type": "output_text",
|
|
"annotations": []interface{}{},
|
|
"logprobs": []interface{}{},
|
|
"text": txt,
|
|
}},
|
|
"role": "assistant",
|
|
})
|
|
}
|
|
}
|
|
if len(st.FuncArgsBuf) > 0 {
|
|
idxs := make([]int, 0, len(st.FuncArgsBuf))
|
|
for i := range st.FuncArgsBuf {
|
|
idxs = append(idxs, i)
|
|
}
|
|
// small-N sort without extra imports
|
|
for i := 0; i < len(idxs); i++ {
|
|
for j := i + 1; j < len(idxs); j++ {
|
|
if idxs[j] < idxs[i] {
|
|
idxs[i], idxs[j] = idxs[j], idxs[i]
|
|
}
|
|
}
|
|
}
|
|
for _, i := range idxs {
|
|
args := ""
|
|
if b := st.FuncArgsBuf[i]; b != nil {
|
|
args = b.String()
|
|
}
|
|
callID := st.FuncCallIDs[i]
|
|
name := st.FuncNames[i]
|
|
outputs = append(outputs, map[string]interface{}{
|
|
"id": fmt.Sprintf("fc_%s", callID),
|
|
"type": "function_call",
|
|
"status": "completed",
|
|
"arguments": args,
|
|
"call_id": callID,
|
|
"name": name,
|
|
})
|
|
}
|
|
}
|
|
if len(outputs) > 0 {
|
|
completed, _ = sjson.Set(completed, "response.output", outputs)
|
|
}
|
|
if st.UsageSeen {
|
|
completed, _ = sjson.Set(completed, "response.usage.input_tokens", st.PromptTokens)
|
|
completed, _ = sjson.Set(completed, "response.usage.input_tokens_details.cached_tokens", st.CachedTokens)
|
|
completed, _ = sjson.Set(completed, "response.usage.output_tokens", st.CompletionTokens)
|
|
if st.ReasoningTokens > 0 {
|
|
completed, _ = sjson.Set(completed, "response.usage.output_tokens_details.reasoning_tokens", st.ReasoningTokens)
|
|
}
|
|
total := st.TotalTokens
|
|
if total == 0 {
|
|
total = st.PromptTokens + st.CompletionTokens
|
|
}
|
|
completed, _ = sjson.Set(completed, "response.usage.total_tokens", total)
|
|
}
|
|
out = append(out, emitRespEvent("response.completed", completed))
|
|
}
|
|
|
|
return true
|
|
})
|
|
}
|
|
|
|
return out
|
|
}
|
|
|
|
// ConvertOpenAIChatCompletionsResponseToOpenAIResponsesNonStream builds a single Responses JSON
|
|
// from a non-streaming OpenAI Chat Completions response.
|
|
func ConvertOpenAIChatCompletionsResponseToOpenAIResponsesNonStream(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) string {
|
|
root := gjson.ParseBytes(rawJSON)
|
|
|
|
// Basic response scaffold
|
|
resp := `{"id":"","object":"response","created_at":0,"status":"completed","background":false,"error":null,"incomplete_details":null}`
|
|
|
|
// id: use provider id if present, otherwise synthesize
|
|
id := root.Get("id").String()
|
|
if id == "" {
|
|
id = fmt.Sprintf("resp_%x", time.Now().UnixNano())
|
|
}
|
|
resp, _ = sjson.Set(resp, "id", id)
|
|
|
|
// created_at: map from chat.completion created
|
|
created := root.Get("created").Int()
|
|
if created == 0 {
|
|
created = time.Now().Unix()
|
|
}
|
|
resp, _ = sjson.Set(resp, "created_at", created)
|
|
|
|
// Echo request fields when available (aligns with streaming path behavior)
|
|
if len(requestRawJSON) > 0 {
|
|
req := gjson.ParseBytes(requestRawJSON)
|
|
if v := req.Get("instructions"); v.Exists() {
|
|
resp, _ = sjson.Set(resp, "instructions", v.String())
|
|
}
|
|
if v := req.Get("max_output_tokens"); v.Exists() {
|
|
resp, _ = sjson.Set(resp, "max_output_tokens", v.Int())
|
|
} else {
|
|
// Also support max_tokens from chat completion style
|
|
if v = req.Get("max_tokens"); v.Exists() {
|
|
resp, _ = sjson.Set(resp, "max_output_tokens", v.Int())
|
|
}
|
|
}
|
|
if v := req.Get("max_tool_calls"); v.Exists() {
|
|
resp, _ = sjson.Set(resp, "max_tool_calls", v.Int())
|
|
}
|
|
if v := req.Get("model"); v.Exists() {
|
|
resp, _ = sjson.Set(resp, "model", v.String())
|
|
} else if v = root.Get("model"); v.Exists() {
|
|
resp, _ = sjson.Set(resp, "model", v.String())
|
|
}
|
|
if v := req.Get("parallel_tool_calls"); v.Exists() {
|
|
resp, _ = sjson.Set(resp, "parallel_tool_calls", v.Bool())
|
|
}
|
|
if v := req.Get("previous_response_id"); v.Exists() {
|
|
resp, _ = sjson.Set(resp, "previous_response_id", v.String())
|
|
}
|
|
if v := req.Get("prompt_cache_key"); v.Exists() {
|
|
resp, _ = sjson.Set(resp, "prompt_cache_key", v.String())
|
|
}
|
|
if v := req.Get("reasoning"); v.Exists() {
|
|
resp, _ = sjson.Set(resp, "reasoning", v.Value())
|
|
}
|
|
if v := req.Get("safety_identifier"); v.Exists() {
|
|
resp, _ = sjson.Set(resp, "safety_identifier", v.String())
|
|
}
|
|
if v := req.Get("service_tier"); v.Exists() {
|
|
resp, _ = sjson.Set(resp, "service_tier", v.String())
|
|
}
|
|
if v := req.Get("store"); v.Exists() {
|
|
resp, _ = sjson.Set(resp, "store", v.Bool())
|
|
}
|
|
if v := req.Get("temperature"); v.Exists() {
|
|
resp, _ = sjson.Set(resp, "temperature", v.Float())
|
|
}
|
|
if v := req.Get("text"); v.Exists() {
|
|
resp, _ = sjson.Set(resp, "text", v.Value())
|
|
}
|
|
if v := req.Get("tool_choice"); v.Exists() {
|
|
resp, _ = sjson.Set(resp, "tool_choice", v.Value())
|
|
}
|
|
if v := req.Get("tools"); v.Exists() {
|
|
resp, _ = sjson.Set(resp, "tools", v.Value())
|
|
}
|
|
if v := req.Get("top_logprobs"); v.Exists() {
|
|
resp, _ = sjson.Set(resp, "top_logprobs", v.Int())
|
|
}
|
|
if v := req.Get("top_p"); v.Exists() {
|
|
resp, _ = sjson.Set(resp, "top_p", v.Float())
|
|
}
|
|
if v := req.Get("truncation"); v.Exists() {
|
|
resp, _ = sjson.Set(resp, "truncation", v.String())
|
|
}
|
|
if v := req.Get("user"); v.Exists() {
|
|
resp, _ = sjson.Set(resp, "user", v.Value())
|
|
}
|
|
if v := req.Get("metadata"); v.Exists() {
|
|
resp, _ = sjson.Set(resp, "metadata", v.Value())
|
|
}
|
|
} else if v := root.Get("model"); v.Exists() {
|
|
// Fallback model from response
|
|
resp, _ = sjson.Set(resp, "model", v.String())
|
|
}
|
|
|
|
// Build output list from choices[...]
|
|
var outputs []interface{}
|
|
// Detect and capture reasoning content if present
|
|
rcText := gjson.GetBytes(rawJSON, "choices.0.message.reasoning_content").String()
|
|
includeReasoning := rcText != ""
|
|
if !includeReasoning && len(requestRawJSON) > 0 {
|
|
includeReasoning = gjson.GetBytes(requestRawJSON, "reasoning").Exists()
|
|
}
|
|
if includeReasoning {
|
|
rid := id
|
|
if strings.HasPrefix(rid, "resp_") {
|
|
rid = strings.TrimPrefix(rid, "resp_")
|
|
}
|
|
reasoningItem := map[string]interface{}{
|
|
"id": fmt.Sprintf("rs_%s", rid),
|
|
"type": "reasoning",
|
|
"encrypted_content": "",
|
|
}
|
|
// Prefer summary_text from reasoning_content; encrypted_content is optional
|
|
var summaries []interface{}
|
|
if rcText != "" {
|
|
summaries = append(summaries, map[string]interface{}{
|
|
"type": "summary_text",
|
|
"text": rcText,
|
|
})
|
|
}
|
|
reasoningItem["summary"] = summaries
|
|
outputs = append(outputs, reasoningItem)
|
|
}
|
|
|
|
if choices := root.Get("choices"); choices.Exists() && choices.IsArray() {
|
|
choices.ForEach(func(_, choice gjson.Result) bool {
|
|
msg := choice.Get("message")
|
|
if msg.Exists() {
|
|
// Text message part
|
|
if c := msg.Get("content"); c.Exists() && c.String() != "" {
|
|
outputs = append(outputs, map[string]interface{}{
|
|
"id": fmt.Sprintf("msg_%s_%d", id, int(choice.Get("index").Int())),
|
|
"type": "message",
|
|
"status": "completed",
|
|
"content": []interface{}{map[string]interface{}{
|
|
"type": "output_text",
|
|
"annotations": []interface{}{},
|
|
"logprobs": []interface{}{},
|
|
"text": c.String(),
|
|
}},
|
|
"role": "assistant",
|
|
})
|
|
}
|
|
|
|
// Function/tool calls
|
|
if tcs := msg.Get("tool_calls"); tcs.Exists() && tcs.IsArray() {
|
|
tcs.ForEach(func(_, tc gjson.Result) bool {
|
|
callID := tc.Get("id").String()
|
|
name := tc.Get("function.name").String()
|
|
args := tc.Get("function.arguments").String()
|
|
outputs = append(outputs, map[string]interface{}{
|
|
"id": fmt.Sprintf("fc_%s", callID),
|
|
"type": "function_call",
|
|
"status": "completed",
|
|
"arguments": args,
|
|
"call_id": callID,
|
|
"name": name,
|
|
})
|
|
return true
|
|
})
|
|
}
|
|
}
|
|
return true
|
|
})
|
|
}
|
|
if len(outputs) > 0 {
|
|
resp, _ = sjson.Set(resp, "output", outputs)
|
|
}
|
|
|
|
// usage mapping
|
|
if usage := root.Get("usage"); usage.Exists() {
|
|
// Map common tokens
|
|
if usage.Get("prompt_tokens").Exists() || usage.Get("completion_tokens").Exists() || usage.Get("total_tokens").Exists() {
|
|
resp, _ = sjson.Set(resp, "usage.input_tokens", usage.Get("prompt_tokens").Int())
|
|
if d := usage.Get("prompt_tokens_details.cached_tokens"); d.Exists() {
|
|
resp, _ = sjson.Set(resp, "usage.input_tokens_details.cached_tokens", d.Int())
|
|
}
|
|
resp, _ = sjson.Set(resp, "usage.output_tokens", usage.Get("completion_tokens").Int())
|
|
// Reasoning tokens not available in Chat Completions; set only if present under output_tokens_details
|
|
if d := usage.Get("output_tokens_details.reasoning_tokens"); d.Exists() {
|
|
resp, _ = sjson.Set(resp, "usage.output_tokens_details.reasoning_tokens", d.Int())
|
|
}
|
|
resp, _ = sjson.Set(resp, "usage.total_tokens", usage.Get("total_tokens").Int())
|
|
} else {
|
|
// Fallback to raw usage object if structure differs
|
|
resp, _ = sjson.Set(resp, "usage", usage.Value())
|
|
}
|
|
}
|
|
|
|
return resp
|
|
}
|