Files
CLIProxyAPI/internal/translator/openai/openai/responses/openai_openai-responses_response.go

785 lines
36 KiB
Go

package responses
import (
"bytes"
"context"
"fmt"
"sort"
"strings"
"sync/atomic"
"time"
translatorcommon "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/common"
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"
)
type oaiToResponsesStateReasoning struct {
ReasoningID string
ReasoningData string
OutputIndex int
}
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
Reasonings []oaiToResponsesStateReasoning
FuncArgsBuf map[string]*strings.Builder
FuncNames map[string]string
FuncCallIDs map[string]string
FuncOutputIx map[string]int
MsgOutputIx map[int]int
NextOutputIx int
// 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[string]bool
FuncItemDone map[string]bool
// usage aggregation
PromptTokens int64
CachedTokens int64
CompletionTokens int64
TotalTokens int64
ReasoningTokens int64
UsageSeen bool
}
// responseIDCounter provides a process-wide unique counter for synthesized response identifiers.
var responseIDCounter uint64
func emitRespEvent(event string, payload []byte) []byte {
return translatorcommon.SSEEventData(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) [][]byte {
if *param == nil {
*param = &oaiToResponsesState{
FuncArgsBuf: make(map[string]*strings.Builder),
FuncNames: make(map[string]string),
FuncCallIDs: make(map[string]string),
FuncOutputIx: make(map[string]int),
MsgOutputIx: make(map[int]int),
MsgTextBuf: make(map[int]*strings.Builder),
MsgItemAdded: make(map[int]bool),
MsgContentAdded: make(map[int]bool),
MsgItemDone: make(map[int]bool),
FuncArgsDone: make(map[string]bool),
FuncItemDone: make(map[string]bool),
Reasonings: make([]oaiToResponsesStateReasoning, 0),
}
}
st := (*param).(*oaiToResponsesState)
if bytes.HasPrefix(rawJSON, []byte("data:")) {
rawJSON = bytes.TrimSpace(rawJSON[5:])
}
rawJSON = bytes.TrimSpace(rawJSON)
if len(rawJSON) == 0 {
return [][]byte{}
}
if bytes.Equal(rawJSON, []byte("[DONE]")) {
return [][]byte{}
}
root := gjson.ParseBytes(rawJSON)
obj := root.Get("object")
if obj.Exists() && obj.String() != "" && obj.String() != "chat.completion.chunk" {
return [][]byte{}
}
if !root.Get("choices").Exists() || !root.Get("choices").IsArray() {
return [][]byte{}
}
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 }
allocOutputIndex := func() int {
ix := st.NextOutputIx
st.NextOutputIx++
return ix
}
toolStateKey := func(outputIndex, toolIndex int) string { return fmt.Sprintf("%d:%d", outputIndex, toolIndex) }
var out [][]byte
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[string]*strings.Builder)
st.FuncNames = make(map[string]string)
st.FuncCallIDs = make(map[string]string)
st.FuncOutputIx = make(map[string]int)
st.MsgOutputIx = make(map[int]int)
st.NextOutputIx = 0
st.MsgItemAdded = make(map[int]bool)
st.MsgContentAdded = make(map[int]bool)
st.MsgItemDone = make(map[int]bool)
st.FuncArgsDone = make(map[string]bool)
st.FuncItemDone = make(map[string]bool)
st.PromptTokens = 0
st.CachedTokens = 0
st.CompletionTokens = 0
st.TotalTokens = 0
st.ReasoningTokens = 0
st.UsageSeen = false
// response.created
created := []byte(`{"type":"response.created","sequence_number":0,"response":{"id":"","object":"response","created_at":0,"status":"in_progress","background":false,"error":null,"output":[]}}`)
created, _ = sjson.SetBytes(created, "sequence_number", nextSeq())
created, _ = sjson.SetBytes(created, "response.id", st.ResponseID)
created, _ = sjson.SetBytes(created, "response.created_at", st.Created)
out = append(out, emitRespEvent("response.created", created))
inprog := []byte(`{"type":"response.in_progress","sequence_number":0,"response":{"id":"","object":"response","created_at":0,"status":"in_progress"}}`)
inprog, _ = sjson.SetBytes(inprog, "sequence_number", nextSeq())
inprog, _ = sjson.SetBytes(inprog, "response.id", st.ResponseID)
inprog, _ = sjson.SetBytes(inprog, "response.created_at", st.Created)
out = append(out, emitRespEvent("response.in_progress", inprog))
st.Started = true
}
stopReasoning := func(text string) {
// Emit reasoning done events
textDone := []byte(`{"type":"response.reasoning_summary_text.done","sequence_number":0,"item_id":"","output_index":0,"summary_index":0,"text":""}`)
textDone, _ = sjson.SetBytes(textDone, "sequence_number", nextSeq())
textDone, _ = sjson.SetBytes(textDone, "item_id", st.ReasoningID)
textDone, _ = sjson.SetBytes(textDone, "output_index", st.ReasoningIndex)
textDone, _ = sjson.SetBytes(textDone, "text", text)
out = append(out, emitRespEvent("response.reasoning_summary_text.done", textDone))
partDone := []byte(`{"type":"response.reasoning_summary_part.done","sequence_number":0,"item_id":"","output_index":0,"summary_index":0,"part":{"type":"summary_text","text":""}}`)
partDone, _ = sjson.SetBytes(partDone, "sequence_number", nextSeq())
partDone, _ = sjson.SetBytes(partDone, "item_id", st.ReasoningID)
partDone, _ = sjson.SetBytes(partDone, "output_index", st.ReasoningIndex)
partDone, _ = sjson.SetBytes(partDone, "part.text", text)
out = append(out, emitRespEvent("response.reasoning_summary_part.done", partDone))
outputItemDone := []byte(`{"type":"response.output_item.done","item":{"id":"","type":"reasoning","encrypted_content":"","summary":[{"type":"summary_text","text":""}]},"output_index":0,"sequence_number":0}`)
outputItemDone, _ = sjson.SetBytes(outputItemDone, "sequence_number", nextSeq())
outputItemDone, _ = sjson.SetBytes(outputItemDone, "item.id", st.ReasoningID)
outputItemDone, _ = sjson.SetBytes(outputItemDone, "output_index", st.ReasoningIndex)
outputItemDone, _ = sjson.SetBytes(outputItemDone, "item.summary.text", text)
out = append(out, emitRespEvent("response.output_item.done", outputItemDone))
st.Reasonings = append(st.Reasonings, oaiToResponsesStateReasoning{ReasoningID: st.ReasoningID, ReasoningData: text, OutputIndex: st.ReasoningIndex})
st.ReasoningID = ""
}
// 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.ReasoningID != "" {
stopReasoning(st.ReasoningBuf.String())
st.ReasoningBuf.Reset()
}
if _, exists := st.MsgOutputIx[idx]; !exists {
st.MsgOutputIx[idx] = allocOutputIndex()
}
msgOutputIndex := st.MsgOutputIx[idx]
if !st.MsgItemAdded[idx] {
item := []byte(`{"type":"response.output_item.added","sequence_number":0,"output_index":0,"item":{"id":"","type":"message","status":"in_progress","content":[],"role":"assistant"}}`)
item, _ = sjson.SetBytes(item, "sequence_number", nextSeq())
item, _ = sjson.SetBytes(item, "output_index", msgOutputIndex)
item, _ = sjson.SetBytes(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 := []byte(`{"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.SetBytes(part, "sequence_number", nextSeq())
part, _ = sjson.SetBytes(part, "item_id", fmt.Sprintf("msg_%s_%d", st.ResponseID, idx))
part, _ = sjson.SetBytes(part, "output_index", msgOutputIndex)
part, _ = sjson.SetBytes(part, "content_index", 0)
out = append(out, emitRespEvent("response.content_part.added", part))
st.MsgContentAdded[idx] = true
}
msg := []byte(`{"type":"response.output_text.delta","sequence_number":0,"item_id":"","output_index":0,"content_index":0,"delta":"","logprobs":[]}`)
msg, _ = sjson.SetBytes(msg, "sequence_number", nextSeq())
msg, _ = sjson.SetBytes(msg, "item_id", fmt.Sprintf("msg_%s_%d", st.ResponseID, idx))
msg, _ = sjson.SetBytes(msg, "output_index", msgOutputIndex)
msg, _ = sjson.SetBytes(msg, "content_index", 0)
msg, _ = sjson.SetBytes(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 = allocOutputIndex()
item := []byte(`{"type":"response.output_item.added","sequence_number":0,"output_index":0,"item":{"id":"","type":"reasoning","status":"in_progress","summary":[]}}`)
item, _ = sjson.SetBytes(item, "sequence_number", nextSeq())
item, _ = sjson.SetBytes(item, "output_index", st.ReasoningIndex)
item, _ = sjson.SetBytes(item, "item.id", st.ReasoningID)
out = append(out, emitRespEvent("response.output_item.added", item))
part := []byte(`{"type":"response.reasoning_summary_part.added","sequence_number":0,"item_id":"","output_index":0,"summary_index":0,"part":{"type":"summary_text","text":""}}`)
part, _ = sjson.SetBytes(part, "sequence_number", nextSeq())
part, _ = sjson.SetBytes(part, "item_id", st.ReasoningID)
part, _ = sjson.SetBytes(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 := []byte(`{"type":"response.reasoning_summary_text.delta","sequence_number":0,"item_id":"","output_index":0,"summary_index":0,"delta":""}`)
msg, _ = sjson.SetBytes(msg, "sequence_number", nextSeq())
msg, _ = sjson.SetBytes(msg, "item_id", st.ReasoningID)
msg, _ = sjson.SetBytes(msg, "output_index", st.ReasoningIndex)
msg, _ = sjson.SetBytes(msg, "delta", rc.String())
out = append(out, emitRespEvent("response.reasoning_summary_text.delta", msg))
}
// tool calls
if tcs := delta.Get("tool_calls"); tcs.Exists() && tcs.IsArray() {
if st.ReasoningID != "" {
stopReasoning(st.ReasoningBuf.String())
st.ReasoningBuf.Reset()
}
// 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] {
msgOutputIndex := st.MsgOutputIx[idx]
fullText := ""
if b := st.MsgTextBuf[idx]; b != nil {
fullText = b.String()
}
done := []byte(`{"type":"response.output_text.done","sequence_number":0,"item_id":"","output_index":0,"content_index":0,"text":"","logprobs":[]}`)
done, _ = sjson.SetBytes(done, "sequence_number", nextSeq())
done, _ = sjson.SetBytes(done, "item_id", fmt.Sprintf("msg_%s_%d", st.ResponseID, idx))
done, _ = sjson.SetBytes(done, "output_index", msgOutputIndex)
done, _ = sjson.SetBytes(done, "content_index", 0)
done, _ = sjson.SetBytes(done, "text", fullText)
out = append(out, emitRespEvent("response.output_text.done", done))
partDone := []byte(`{"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.SetBytes(partDone, "sequence_number", nextSeq())
partDone, _ = sjson.SetBytes(partDone, "item_id", fmt.Sprintf("msg_%s_%d", st.ResponseID, idx))
partDone, _ = sjson.SetBytes(partDone, "output_index", msgOutputIndex)
partDone, _ = sjson.SetBytes(partDone, "content_index", 0)
partDone, _ = sjson.SetBytes(partDone, "part.text", fullText)
out = append(out, emitRespEvent("response.content_part.done", partDone))
itemDone := []byte(`{"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.SetBytes(itemDone, "sequence_number", nextSeq())
itemDone, _ = sjson.SetBytes(itemDone, "output_index", msgOutputIndex)
itemDone, _ = sjson.SetBytes(itemDone, "item.id", fmt.Sprintf("msg_%s_%d", st.ResponseID, idx))
itemDone, _ = sjson.SetBytes(itemDone, "item.content.0.text", fullText)
out = append(out, emitRespEvent("response.output_item.done", itemDone))
st.MsgItemDone[idx] = true
}
tcs.ForEach(func(_, tc gjson.Result) bool {
toolIndex := int(tc.Get("index").Int())
key := toolStateKey(idx, toolIndex)
newCallID := tc.Get("id").String()
nameChunk := tc.Get("function.name").String()
if nameChunk != "" {
st.FuncNames[key] = nameChunk
}
existingCallID := st.FuncCallIDs[key]
effectiveCallID := existingCallID
shouldEmitItem := false
if existingCallID == "" && newCallID != "" {
effectiveCallID = newCallID
st.FuncCallIDs[key] = newCallID
st.FuncOutputIx[key] = allocOutputIndex()
shouldEmitItem = true
}
if shouldEmitItem && effectiveCallID != "" {
outputIndex := st.FuncOutputIx[key]
o := []byte(`{"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.SetBytes(o, "sequence_number", nextSeq())
o, _ = sjson.SetBytes(o, "output_index", outputIndex)
o, _ = sjson.SetBytes(o, "item.id", fmt.Sprintf("fc_%s", effectiveCallID))
o, _ = sjson.SetBytes(o, "item.call_id", effectiveCallID)
o, _ = sjson.SetBytes(o, "item.name", st.FuncNames[key])
out = append(out, emitRespEvent("response.output_item.added", o))
}
if st.FuncArgsBuf[key] == nil {
st.FuncArgsBuf[key] = &strings.Builder{}
}
if args := tc.Get("function.arguments"); args.Exists() && args.String() != "" {
refCallID := st.FuncCallIDs[key]
if refCallID == "" {
refCallID = newCallID
}
if refCallID != "" {
outputIndex := st.FuncOutputIx[key]
ad := []byte(`{"type":"response.function_call_arguments.delta","sequence_number":0,"item_id":"","output_index":0,"delta":""}`)
ad, _ = sjson.SetBytes(ad, "sequence_number", nextSeq())
ad, _ = sjson.SetBytes(ad, "item_id", fmt.Sprintf("fc_%s", refCallID))
ad, _ = sjson.SetBytes(ad, "output_index", outputIndex)
ad, _ = sjson.SetBytes(ad, "delta", args.String())
out = append(out, emitRespEvent("response.function_call_arguments.delta", ad))
}
st.FuncArgsBuf[key].WriteString(args.String())
}
return true
})
}
}
// 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)
}
sort.Slice(idxs, func(i, j int) bool { return st.MsgOutputIx[idxs[i]] < st.MsgOutputIx[idxs[j]] })
for _, i := range idxs {
if st.MsgItemAdded[i] && !st.MsgItemDone[i] {
msgOutputIndex := st.MsgOutputIx[i]
fullText := ""
if b := st.MsgTextBuf[i]; b != nil {
fullText = b.String()
}
done := []byte(`{"type":"response.output_text.done","sequence_number":0,"item_id":"","output_index":0,"content_index":0,"text":"","logprobs":[]}`)
done, _ = sjson.SetBytes(done, "sequence_number", nextSeq())
done, _ = sjson.SetBytes(done, "item_id", fmt.Sprintf("msg_%s_%d", st.ResponseID, i))
done, _ = sjson.SetBytes(done, "output_index", msgOutputIndex)
done, _ = sjson.SetBytes(done, "content_index", 0)
done, _ = sjson.SetBytes(done, "text", fullText)
out = append(out, emitRespEvent("response.output_text.done", done))
partDone := []byte(`{"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.SetBytes(partDone, "sequence_number", nextSeq())
partDone, _ = sjson.SetBytes(partDone, "item_id", fmt.Sprintf("msg_%s_%d", st.ResponseID, i))
partDone, _ = sjson.SetBytes(partDone, "output_index", msgOutputIndex)
partDone, _ = sjson.SetBytes(partDone, "content_index", 0)
partDone, _ = sjson.SetBytes(partDone, "part.text", fullText)
out = append(out, emitRespEvent("response.content_part.done", partDone))
itemDone := []byte(`{"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.SetBytes(itemDone, "sequence_number", nextSeq())
itemDone, _ = sjson.SetBytes(itemDone, "output_index", msgOutputIndex)
itemDone, _ = sjson.SetBytes(itemDone, "item.id", fmt.Sprintf("msg_%s_%d", st.ResponseID, i))
itemDone, _ = sjson.SetBytes(itemDone, "item.content.0.text", fullText)
out = append(out, emitRespEvent("response.output_item.done", itemDone))
st.MsgItemDone[i] = true
}
}
}
if st.ReasoningID != "" {
stopReasoning(st.ReasoningBuf.String())
st.ReasoningBuf.Reset()
}
// Emit function call done events for any active function calls
if len(st.FuncCallIDs) > 0 {
keys := make([]string, 0, len(st.FuncCallIDs))
for key := range st.FuncCallIDs {
keys = append(keys, key)
}
sort.Slice(keys, func(i, j int) bool {
left := st.FuncOutputIx[keys[i]]
right := st.FuncOutputIx[keys[j]]
return left < right || (left == right && keys[i] < keys[j])
})
for _, key := range keys {
callID := st.FuncCallIDs[key]
if callID == "" || st.FuncItemDone[key] {
continue
}
outputIndex := st.FuncOutputIx[key]
args := "{}"
if b := st.FuncArgsBuf[key]; b != nil && b.Len() > 0 {
args = b.String()
}
fcDone := []byte(`{"type":"response.function_call_arguments.done","sequence_number":0,"item_id":"","output_index":0,"arguments":""}`)
fcDone, _ = sjson.SetBytes(fcDone, "sequence_number", nextSeq())
fcDone, _ = sjson.SetBytes(fcDone, "item_id", fmt.Sprintf("fc_%s", callID))
fcDone, _ = sjson.SetBytes(fcDone, "output_index", outputIndex)
fcDone, _ = sjson.SetBytes(fcDone, "arguments", args)
out = append(out, emitRespEvent("response.function_call_arguments.done", fcDone))
itemDone := []byte(`{"type":"response.output_item.done","sequence_number":0,"output_index":0,"item":{"id":"","type":"function_call","status":"completed","arguments":"","call_id":"","name":""}}`)
itemDone, _ = sjson.SetBytes(itemDone, "sequence_number", nextSeq())
itemDone, _ = sjson.SetBytes(itemDone, "output_index", outputIndex)
itemDone, _ = sjson.SetBytes(itemDone, "item.id", fmt.Sprintf("fc_%s", callID))
itemDone, _ = sjson.SetBytes(itemDone, "item.arguments", args)
itemDone, _ = sjson.SetBytes(itemDone, "item.call_id", callID)
itemDone, _ = sjson.SetBytes(itemDone, "item.name", st.FuncNames[key])
out = append(out, emitRespEvent("response.output_item.done", itemDone))
st.FuncItemDone[key] = true
st.FuncArgsDone[key] = true
}
}
completed := []byte(`{"type":"response.completed","sequence_number":0,"response":{"id":"","object":"response","created_at":0,"status":"completed","background":false,"error":null}}`)
completed, _ = sjson.SetBytes(completed, "sequence_number", nextSeq())
completed, _ = sjson.SetBytes(completed, "response.id", st.ResponseID)
completed, _ = sjson.SetBytes(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.SetBytes(completed, "response.instructions", v.String())
}
if v := req.Get("max_output_tokens"); v.Exists() {
completed, _ = sjson.SetBytes(completed, "response.max_output_tokens", v.Int())
}
if v := req.Get("max_tool_calls"); v.Exists() {
completed, _ = sjson.SetBytes(completed, "response.max_tool_calls", v.Int())
}
if v := req.Get("model"); v.Exists() {
completed, _ = sjson.SetBytes(completed, "response.model", v.String())
}
if v := req.Get("parallel_tool_calls"); v.Exists() {
completed, _ = sjson.SetBytes(completed, "response.parallel_tool_calls", v.Bool())
}
if v := req.Get("previous_response_id"); v.Exists() {
completed, _ = sjson.SetBytes(completed, "response.previous_response_id", v.String())
}
if v := req.Get("prompt_cache_key"); v.Exists() {
completed, _ = sjson.SetBytes(completed, "response.prompt_cache_key", v.String())
}
if v := req.Get("reasoning"); v.Exists() {
completed, _ = sjson.SetBytes(completed, "response.reasoning", v.Value())
}
if v := req.Get("safety_identifier"); v.Exists() {
completed, _ = sjson.SetBytes(completed, "response.safety_identifier", v.String())
}
if v := req.Get("service_tier"); v.Exists() {
completed, _ = sjson.SetBytes(completed, "response.service_tier", v.String())
}
if v := req.Get("store"); v.Exists() {
completed, _ = sjson.SetBytes(completed, "response.store", v.Bool())
}
if v := req.Get("temperature"); v.Exists() {
completed, _ = sjson.SetBytes(completed, "response.temperature", v.Float())
}
if v := req.Get("text"); v.Exists() {
completed, _ = sjson.SetBytes(completed, "response.text", v.Value())
}
if v := req.Get("tool_choice"); v.Exists() {
completed, _ = sjson.SetBytes(completed, "response.tool_choice", v.Value())
}
if v := req.Get("tools"); v.Exists() {
completed, _ = sjson.SetBytes(completed, "response.tools", v.Value())
}
if v := req.Get("top_logprobs"); v.Exists() {
completed, _ = sjson.SetBytes(completed, "response.top_logprobs", v.Int())
}
if v := req.Get("top_p"); v.Exists() {
completed, _ = sjson.SetBytes(completed, "response.top_p", v.Float())
}
if v := req.Get("truncation"); v.Exists() {
completed, _ = sjson.SetBytes(completed, "response.truncation", v.String())
}
if v := req.Get("user"); v.Exists() {
completed, _ = sjson.SetBytes(completed, "response.user", v.Value())
}
if v := req.Get("metadata"); v.Exists() {
completed, _ = sjson.SetBytes(completed, "response.metadata", v.Value())
}
}
// Build response.output using aggregated buffers
outputsWrapper := []byte(`{"arr":[]}`)
type completedOutputItem struct {
index int
raw []byte
}
outputItems := make([]completedOutputItem, 0, len(st.Reasonings)+len(st.MsgItemAdded)+len(st.FuncArgsBuf))
if len(st.Reasonings) > 0 {
for _, r := range st.Reasonings {
item := []byte(`{"id":"","type":"reasoning","summary":[{"type":"summary_text","text":""}]}`)
item, _ = sjson.SetBytes(item, "id", r.ReasoningID)
item, _ = sjson.SetBytes(item, "summary.0.text", r.ReasoningData)
outputItems = append(outputItems, completedOutputItem{index: r.OutputIndex, raw: item})
}
}
if len(st.MsgItemAdded) > 0 {
for i := range st.MsgItemAdded {
txt := ""
if b := st.MsgTextBuf[i]; b != nil {
txt = b.String()
}
item := []byte(`{"id":"","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":""}],"role":"assistant"}`)
item, _ = sjson.SetBytes(item, "id", fmt.Sprintf("msg_%s_%d", st.ResponseID, i))
item, _ = sjson.SetBytes(item, "content.0.text", txt)
outputItems = append(outputItems, completedOutputItem{index: st.MsgOutputIx[i], raw: item})
}
}
if len(st.FuncArgsBuf) > 0 {
for key := range st.FuncArgsBuf {
args := ""
if b := st.FuncArgsBuf[key]; b != nil {
args = b.String()
}
callID := st.FuncCallIDs[key]
name := st.FuncNames[key]
item := []byte(`{"id":"","type":"function_call","status":"completed","arguments":"","call_id":"","name":""}`)
item, _ = sjson.SetBytes(item, "id", fmt.Sprintf("fc_%s", callID))
item, _ = sjson.SetBytes(item, "arguments", args)
item, _ = sjson.SetBytes(item, "call_id", callID)
item, _ = sjson.SetBytes(item, "name", name)
outputItems = append(outputItems, completedOutputItem{index: st.FuncOutputIx[key], raw: item})
}
}
sort.Slice(outputItems, func(i, j int) bool { return outputItems[i].index < outputItems[j].index })
for _, item := range outputItems {
outputsWrapper, _ = sjson.SetRawBytes(outputsWrapper, "arr.-1", item.raw)
}
if gjson.GetBytes(outputsWrapper, "arr.#").Int() > 0 {
completed, _ = sjson.SetRawBytes(completed, "response.output", []byte(gjson.GetBytes(outputsWrapper, "arr").Raw))
}
if st.UsageSeen {
completed, _ = sjson.SetBytes(completed, "response.usage.input_tokens", st.PromptTokens)
completed, _ = sjson.SetBytes(completed, "response.usage.input_tokens_details.cached_tokens", st.CachedTokens)
completed, _ = sjson.SetBytes(completed, "response.usage.output_tokens", st.CompletionTokens)
if st.ReasoningTokens > 0 {
completed, _ = sjson.SetBytes(completed, "response.usage.output_tokens_details.reasoning_tokens", st.ReasoningTokens)
}
total := st.TotalTokens
if total == 0 {
total = st.PromptTokens + st.CompletionTokens
}
completed, _ = sjson.SetBytes(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) []byte {
root := gjson.ParseBytes(rawJSON)
// Basic response scaffold
resp := []byte(`{"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_%d", time.Now().UnixNano(), atomic.AddUint64(&responseIDCounter, 1))
}
resp, _ = sjson.SetBytes(resp, "id", id)
// created_at: map from chat.completion created
created := root.Get("created").Int()
if created == 0 {
created = time.Now().Unix()
}
resp, _ = sjson.SetBytes(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.SetBytes(resp, "instructions", v.String())
}
if v := req.Get("max_output_tokens"); v.Exists() {
resp, _ = sjson.SetBytes(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.SetBytes(resp, "max_output_tokens", v.Int())
}
}
if v := req.Get("max_tool_calls"); v.Exists() {
resp, _ = sjson.SetBytes(resp, "max_tool_calls", v.Int())
}
if v := req.Get("model"); v.Exists() {
resp, _ = sjson.SetBytes(resp, "model", v.String())
} else if v = root.Get("model"); v.Exists() {
resp, _ = sjson.SetBytes(resp, "model", v.String())
}
if v := req.Get("parallel_tool_calls"); v.Exists() {
resp, _ = sjson.SetBytes(resp, "parallel_tool_calls", v.Bool())
}
if v := req.Get("previous_response_id"); v.Exists() {
resp, _ = sjson.SetBytes(resp, "previous_response_id", v.String())
}
if v := req.Get("prompt_cache_key"); v.Exists() {
resp, _ = sjson.SetBytes(resp, "prompt_cache_key", v.String())
}
if v := req.Get("reasoning"); v.Exists() {
resp, _ = sjson.SetBytes(resp, "reasoning", v.Value())
}
if v := req.Get("safety_identifier"); v.Exists() {
resp, _ = sjson.SetBytes(resp, "safety_identifier", v.String())
}
if v := req.Get("service_tier"); v.Exists() {
resp, _ = sjson.SetBytes(resp, "service_tier", v.String())
}
if v := req.Get("store"); v.Exists() {
resp, _ = sjson.SetBytes(resp, "store", v.Bool())
}
if v := req.Get("temperature"); v.Exists() {
resp, _ = sjson.SetBytes(resp, "temperature", v.Float())
}
if v := req.Get("text"); v.Exists() {
resp, _ = sjson.SetBytes(resp, "text", v.Value())
}
if v := req.Get("tool_choice"); v.Exists() {
resp, _ = sjson.SetBytes(resp, "tool_choice", v.Value())
}
if v := req.Get("tools"); v.Exists() {
resp, _ = sjson.SetBytes(resp, "tools", v.Value())
}
if v := req.Get("top_logprobs"); v.Exists() {
resp, _ = sjson.SetBytes(resp, "top_logprobs", v.Int())
}
if v := req.Get("top_p"); v.Exists() {
resp, _ = sjson.SetBytes(resp, "top_p", v.Float())
}
if v := req.Get("truncation"); v.Exists() {
resp, _ = sjson.SetBytes(resp, "truncation", v.String())
}
if v := req.Get("user"); v.Exists() {
resp, _ = sjson.SetBytes(resp, "user", v.Value())
}
if v := req.Get("metadata"); v.Exists() {
resp, _ = sjson.SetBytes(resp, "metadata", v.Value())
}
} else if v := root.Get("model"); v.Exists() {
// Fallback model from response
resp, _ = sjson.SetBytes(resp, "model", v.String())
}
// Build output list from choices[...]
outputsWrapper := []byte(`{"arr":[]}`)
// 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_")
}
// Prefer summary_text from reasoning_content; encrypted_content is optional
reasoningItem := []byte(`{"id":"","type":"reasoning","encrypted_content":"","summary":[]}`)
reasoningItem, _ = sjson.SetBytes(reasoningItem, "id", fmt.Sprintf("rs_%s", rid))
if rcText != "" {
reasoningItem, _ = sjson.SetBytes(reasoningItem, "summary.0.type", "summary_text")
reasoningItem, _ = sjson.SetBytes(reasoningItem, "summary.0.text", rcText)
}
outputsWrapper, _ = sjson.SetRawBytes(outputsWrapper, "arr.-1", 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() != "" {
item := []byte(`{"id":"","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":""}],"role":"assistant"}`)
item, _ = sjson.SetBytes(item, "id", fmt.Sprintf("msg_%s_%d", id, int(choice.Get("index").Int())))
item, _ = sjson.SetBytes(item, "content.0.text", c.String())
outputsWrapper, _ = sjson.SetRawBytes(outputsWrapper, "arr.-1", item)
}
// 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()
item := []byte(`{"id":"","type":"function_call","status":"completed","arguments":"","call_id":"","name":""}`)
item, _ = sjson.SetBytes(item, "id", fmt.Sprintf("fc_%s", callID))
item, _ = sjson.SetBytes(item, "arguments", args)
item, _ = sjson.SetBytes(item, "call_id", callID)
item, _ = sjson.SetBytes(item, "name", name)
outputsWrapper, _ = sjson.SetRawBytes(outputsWrapper, "arr.-1", item)
return true
})
}
}
return true
})
}
if gjson.GetBytes(outputsWrapper, "arr.#").Int() > 0 {
resp, _ = sjson.SetRawBytes(resp, "output", []byte(gjson.GetBytes(outputsWrapper, "arr").Raw))
}
// 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.SetBytes(resp, "usage.input_tokens", usage.Get("prompt_tokens").Int())
if d := usage.Get("prompt_tokens_details.cached_tokens"); d.Exists() {
resp, _ = sjson.SetBytes(resp, "usage.input_tokens_details.cached_tokens", d.Int())
}
resp, _ = sjson.SetBytes(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.SetBytes(resp, "usage.output_tokens_details.reasoning_tokens", d.Int())
}
resp, _ = sjson.SetBytes(resp, "usage.total_tokens", usage.Get("total_tokens").Int())
} else {
// Fallback to raw usage object if structure differs
resp, _ = sjson.SetBytes(resp, "usage", usage.Value())
}
}
return resp
}