| @@ -4,6 +4,7 @@ import ( · 3 unmodified lines |
| 4 | 4 | "bytes" |
| 5 | 5 | "context" |
| 6 | 6 | "fmt" |
| 7 | + "sort" |
| 7 | 8 | "strings" |
| 8 | 9 | "sync/atomic" |
| 9 | 10 | "time" |
| @@ -16,6 +17,7 @@ import ( · 6 unmodified lines |
| 16 | 17 | type oaiToResponsesStateReasoning struct { |
| 17 | 18 | ReasoningID string |
| 18 | 19 | ReasoningData string |
| 20 | + OutputIndex int |
| 19 | 21 | } |
| 20 | 22 | type oaiToResponsesState struct { |
| 21 | 23 | Seq int |
| @@ -29,16 +31,19 @@ type oaiToResponsesState struct { · 7 unmodified lines |
| 29 | 31 | MsgTextBuf map[int]*strings.Builder |
| 30 | 32 | ReasoningBuf strings.Builder |
| 31 | 33 | Reasonings []oaiToResponsesStateReasoning |
| 32 | | − FuncArgsBuf map[int]*strings.Builder // index -> args |
| 33 | | − FuncNames map[int]string // index -> name |
| 34 | | − FuncCallIDs map[int]string // index -> call_id |
| 34 | + FuncArgsBuf map[string]*strings.Builder |
| 35 | + FuncNames map[string]string |
| 36 | + FuncCallIDs map[string]string |
| 37 | + FuncOutputIx map[string]int |
| 38 | + MsgOutputIx map[int]int |
| 39 | + NextOutputIx int |
| 35 | 40 | // message item state per output index |
| 36 | 41 | MsgItemAdded map[int]bool // whether response.output_item.added emitted for message |
| 37 | 42 | MsgContentAdded map[int]bool // whether response.content_part.added emitted for message |
| 38 | 43 | MsgItemDone map[int]bool // whether message done events were emitted |
| 39 | 44 | // function item done state |
| 40 | | − FuncArgsDone map[int]bool |
| 41 | | − FuncItemDone map[int]bool |
| 45 | + FuncArgsDone map[string]bool |
| 46 | + FuncItemDone map[string]bool |
| 42 | 47 | // usage aggregation |
| 43 | 48 | PromptTokens int64 |
| 44 | 49 | CachedTokens int64 |
| @@ -60,15 +65,17 @@ func emitRespEvent(event string, payload []byte) []byte { · 15 unmodified lines |
| 60 | 65 | func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) [][]byte { |
| 61 | 66 | if *param == nil { |
| 62 | 67 | *param = &oaiToResponsesState{ |
| 63 | | − FuncArgsBuf: make(map[int]*strings.Builder), |
| 64 | | − FuncNames: make(map[int]string), |
| 65 | | − FuncCallIDs: make(map[int]string), |
| 68 | + FuncArgsBuf: make(map[string]*strings.Builder), |
| 69 | + FuncNames: make(map[string]string), |
| 70 | + FuncCallIDs: make(map[string]string), |
| 71 | + FuncOutputIx: make(map[string]int), |
| 72 | + MsgOutputIx: make(map[int]int), |
| 66 | 73 | MsgTextBuf: make(map[int]*strings.Builder), |
| 67 | 74 | MsgItemAdded: make(map[int]bool), |
| 68 | 75 | MsgContentAdded: make(map[int]bool), |
| 69 | 76 | MsgItemDone: make(map[int]bool), |
| 70 | | − FuncArgsDone: make(map[int]bool), |
| 71 | | − FuncItemDone: make(map[int]bool), |
| 77 | + FuncArgsDone: make(map[string]bool), |
| 78 | + FuncItemDone: make(map[string]bool), |
| 72 | 79 | Reasonings: make([]oaiToResponsesStateReasoning, 0), |
| 73 | 80 | } |
| 74 | 81 | } |
| @@ -125,6 +132,12 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context, · 50 unmodified lines |
| 125 | 132 | } |
| 126 | 133 | |
| 127 | 134 | nextSeq := func() int { st.Seq++; return st.Seq } |
| 135 | + allocOutputIndex := func() int { |
| 136 | + ix := st.NextOutputIx |
| 137 | + st.NextOutputIx++ |
| 138 | + return ix |
| 139 | + } |
| 140 | + toolStateKey := func(outputIndex, toolIndex int) string { return fmt.Sprintf("%d:%d", outputIndex, toolIndex) } |
| 128 | 141 | var out [][]byte |
| 129 | 142 | |
| 130 | 143 | if !st.Started { |
| @@ -135,14 +148,17 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context, · 4 unmodified lines |
| 135 | 148 | st.ReasoningBuf.Reset() |
| 136 | 149 | st.ReasoningID = "" |
| 137 | 150 | st.ReasoningIndex = 0 |
| 138 | | − st.FuncArgsBuf = make(map[int]*strings.Builder) |
| 139 | | − st.FuncNames = make(map[int]string) |
| 140 | | − st.FuncCallIDs = make(map[int]string) |
| 151 | + st.FuncArgsBuf = make(map[string]*strings.Builder) |
| 152 | + st.FuncNames = make(map[string]string) |
| 153 | + st.FuncCallIDs = make(map[string]string) |
| 154 | + st.FuncOutputIx = make(map[string]int) |
| 155 | + st.MsgOutputIx = make(map[int]int) |
| 156 | + st.NextOutputIx = 0 |
| 141 | 157 | st.MsgItemAdded = make(map[int]bool) |
| 142 | 158 | st.MsgContentAdded = make(map[int]bool) |
| 143 | 159 | st.MsgItemDone = make(map[int]bool) |
| 144 | | − st.FuncArgsDone = make(map[int]bool) |
| 145 | | − st.FuncItemDone = make(map[int]bool) |
| 160 | + st.FuncArgsDone = make(map[string]bool) |
| 161 | + st.FuncItemDone = make(map[string]bool) |
| 146 | 162 | st.PromptTokens = 0 |
| 147 | 163 | st.CachedTokens = 0 |
| 148 | 164 | st.CompletionTokens = 0 |
| @@ -185,7 +201,7 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context, · 36 unmodified lines |
| 185 | 201 | outputItemDone, _ = sjson.SetBytes(outputItemDone, "item.summary.text", text) |
| 186 | 202 | out = append(out, emitRespEvent("response.output_item.done", outputItemDone)) |
| 187 | 203 | |
| 188 | | − st.Reasonings = append(st.Reasonings, oaiToResponsesStateReasoning{ReasoningID: st.ReasoningID, ReasoningData: text}) |
| 204 | + st.Reasonings = append(st.Reasonings, oaiToResponsesStateReasoning{ReasoningID: st.ReasoningID, ReasoningData: text, OutputIndex: st.ReasoningIndex}) |
| 189 | 205 | st.ReasoningID = "" |
| 190 | 206 | } |
| 191 | 207 | |
| @@ -201,10 +217,14 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context, · 9 unmodified lines |
| 201 | 217 | stopReasoning(st.ReasoningBuf.String()) |
| 202 | 218 | st.ReasoningBuf.Reset() |
| 203 | 219 | } |
| 220 | + if _, exists := st.MsgOutputIx[idx]; !exists { |
| 221 | + st.MsgOutputIx[idx] = allocOutputIndex() |
| 222 | + } |
| 223 | + msgOutputIndex := st.MsgOutputIx[idx] |
| 204 | 224 | if !st.MsgItemAdded[idx] { |
| 205 | 225 | item := []byte(`{"type":"response.output_item.added","sequence_number":0,"output_index":0,"item":{"id":"","type":"message","status":"in_progress","content":[],"role":"assistant"}}`) |
| 206 | 226 | item, _ = sjson.SetBytes(item, "sequence_number", nextSeq()) |
| 207 | | − item, _ = sjson.SetBytes(item, "output_index", idx) |
| 227 | + item, _ = sjson.SetBytes(item, "output_index", msgOutputIndex) |
| 208 | 228 | item, _ = sjson.SetBytes(item, "item.id", fmt.Sprintf("msg_%s_%d", st.ResponseID, idx)) |
| 209 | 229 | out = append(out, emitRespEvent("response.output_item.added", item)) |
| 210 | 230 | st.MsgItemAdded[idx] = true |
| @@ -213,7 +233,7 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context, · 2 unmodified lines |
| 213 | 233 | 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":""}}`) |
| 214 | 234 | part, _ = sjson.SetBytes(part, "sequence_number", nextSeq()) |
| 215 | 235 | part, _ = sjson.SetBytes(part, "item_id", fmt.Sprintf("msg_%s_%d", st.ResponseID, idx)) |
| 216 | | − part, _ = sjson.SetBytes(part, "output_index", idx) |
| 236 | + part, _ = sjson.SetBytes(part, "output_index", msgOutputIndex) |
| 217 | 237 | part, _ = sjson.SetBytes(part, "content_index", 0) |
| 218 | 238 | out = append(out, emitRespEvent("response.content_part.added", part)) |
| 219 | 239 | st.MsgContentAdded[idx] = true |
| @@ -222,7 +242,7 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context, · 2 unmodified lines |
| 222 | 242 | msg := []byte(`{"type":"response.output_text.delta","sequence_number":0,"item_id":"","output_index":0,"content_index":0,"delta":"","logprobs":[]}`) |
| 223 | 243 | msg, _ = sjson.SetBytes(msg, "sequence_number", nextSeq()) |
| 224 | 244 | msg, _ = sjson.SetBytes(msg, "item_id", fmt.Sprintf("msg_%s_%d", st.ResponseID, idx)) |
| 225 | | − msg, _ = sjson.SetBytes(msg, "output_index", idx) |
| 245 | + msg, _ = sjson.SetBytes(msg, "output_index", msgOutputIndex) |
| 226 | 246 | msg, _ = sjson.SetBytes(msg, "content_index", 0) |
| 227 | 247 | msg, _ = sjson.SetBytes(msg, "delta", c.String()) |
| 228 | 248 | out = append(out, emitRespEvent("response.output_text.delta", msg)) |
| @@ -238,10 +258,10 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context, · 9 unmodified lines |
| 238 | 258 | // On first appearance, add reasoning item and part |
| 239 | 259 | if st.ReasoningID == "" { |
| 240 | 260 | st.ReasoningID = fmt.Sprintf("rs_%s_%d", st.ResponseID, idx) |
| 241 | | − st.ReasoningIndex = idx |
| 261 | + st.ReasoningIndex = allocOutputIndex() |
| 242 | 262 | item := []byte(`{"type":"response.output_item.added","sequence_number":0,"output_index":0,"item":{"id":"","type":"reasoning","status":"in_progress","summary":[]}}`) |
| 243 | 263 | item, _ = sjson.SetBytes(item, "sequence_number", nextSeq()) |
| 244 | | − item, _ = sjson.SetBytes(item, "output_index", idx) |
| 264 | + item, _ = sjson.SetBytes(item, "output_index", st.ReasoningIndex) |
| 245 | 265 | item, _ = sjson.SetBytes(item, "item.id", st.ReasoningID) |
| 246 | 266 | out = append(out, emitRespEvent("response.output_item.added", item)) |
| 247 | 267 | part := []byte(`{"type":"response.reasoning_summary_part.added","sequence_number":0,"item_id":"","output_index":0,"summary_index":0,"part":{"type":"summary_text","text":""}}`) |
| @@ -269,6 +289,7 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context, · 21 unmodified lines |
| 269 | 289 | // Before emitting any function events, if a message is open for this index, |
| 270 | 290 | // close its text/content to match Codex expected ordering. |
| 271 | 291 | if st.MsgItemAdded[idx] && !st.MsgItemDone[idx] { |
| 292 | + msgOutputIndex := st.MsgOutputIx[idx] |
| 272 | 293 | fullText := "" |
| 273 | 294 | if b := st.MsgTextBuf[idx]; b != nil { |
| 274 | 295 | fullText = b.String() |
| @@ -276,7 +297,7 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context, · 1 unmodified line |
| 276 | 297 | done := []byte(`{"type":"response.output_text.done","sequence_number":0,"item_id":"","output_index":0,"content_index":0,"text":"","logprobs":[]}`) |
| 277 | 298 | done, _ = sjson.SetBytes(done, "sequence_number", nextSeq()) |
| 278 | 299 | done, _ = sjson.SetBytes(done, "item_id", fmt.Sprintf("msg_%s_%d", st.ResponseID, idx)) |
| 279 | | − done, _ = sjson.SetBytes(done, "output_index", idx) |
| 300 | + done, _ = sjson.SetBytes(done, "output_index", msgOutputIndex) |
| 280 | 301 | done, _ = sjson.SetBytes(done, "content_index", 0) |
| 281 | 302 | done, _ = sjson.SetBytes(done, "text", fullText) |
| 282 | 303 | out = append(out, emitRespEvent("response.output_text.done", done)) |
| @@ -284,69 +305,72 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context, · 1 unmodified line |
| 284 | 305 | 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":""}}`) |
| 285 | 306 | partDone, _ = sjson.SetBytes(partDone, "sequence_number", nextSeq()) |
| 286 | 307 | partDone, _ = sjson.SetBytes(partDone, "item_id", fmt.Sprintf("msg_%s_%d", st.ResponseID, idx)) |
| 287 | | − partDone, _ = sjson.SetBytes(partDone, "output_index", idx) |
| 308 | + partDone, _ = sjson.SetBytes(partDone, "output_index", msgOutputIndex) |
| 288 | 309 | partDone, _ = sjson.SetBytes(partDone, "content_index", 0) |
| 289 | 310 | partDone, _ = sjson.SetBytes(partDone, "part.text", fullText) |
| 290 | 311 | out = append(out, emitRespEvent("response.content_part.done", partDone)) |
| 291 | 312 | |
| 292 | 313 | 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"}}`) |
| 293 | 314 | itemDone, _ = sjson.SetBytes(itemDone, "sequence_number", nextSeq()) |
| 294 | | − itemDone, _ = sjson.SetBytes(itemDone, "output_index", idx) |
| 315 | + itemDone, _ = sjson.SetBytes(itemDone, "output_index", msgOutputIndex) |
| 295 | 316 | itemDone, _ = sjson.SetBytes(itemDone, "item.id", fmt.Sprintf("msg_%s_%d", st.ResponseID, idx)) |
| 296 | 317 | itemDone, _ = sjson.SetBytes(itemDone, "item.content.0.text", fullText) |
| 297 | 318 | out = append(out, emitRespEvent("response.output_item.done", itemDone)) |
| 298 | 319 | st.MsgItemDone[idx] = true |
| 299 | 320 | } |
| 300 | 321 | |
| 301 | | − // Only emit item.added once per tool call and preserve call_id across chunks. |
| 302 | | − newCallID := tcs.Get("0.id").String() |
| 303 | | − nameChunk := tcs.Get("0.function.name").String() |
| 304 | | − if nameChunk != "" { |
| 305 | | − st.FuncNames[idx] = nameChunk |
| 306 | | − } |
| 307 | | − existingCallID := st.FuncCallIDs[idx] |
| 308 | | − effectiveCallID := existingCallID |
| 309 | | − shouldEmitItem := false |
| 310 | | − if existingCallID == "" && newCallID != "" { |
| 311 | | − // First time seeing a valid call_id for this index |
| 312 | | − effectiveCallID = newCallID |
| 313 | | − st.FuncCallIDs[idx] = newCallID |
| 314 | | − shouldEmitItem = true |
| 315 | | − } |
| 316 | | − |
| 317 | | − if shouldEmitItem && effectiveCallID != "" { |
| 318 | | − 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":""}}`) |
| 319 | | − o, _ = sjson.SetBytes(o, "sequence_number", nextSeq()) |
| 320 | | − o, _ = sjson.SetBytes(o, "output_index", idx) |
| 321 | | − o, _ = sjson.SetBytes(o, "item.id", fmt.Sprintf("fc_%s", effectiveCallID)) |
| 322 | | − o, _ = sjson.SetBytes(o, "item.call_id", effectiveCallID) |
| 323 | | − name := st.FuncNames[idx] |
| 324 | | − o, _ = sjson.SetBytes(o, "item.name", name) |
| 325 | | − out = append(out, emitRespEvent("response.output_item.added", o)) |
| 326 | | − } |
| 327 | | − |
| 328 | | − // Ensure args buffer exists for this index |
| 329 | | − if st.FuncArgsBuf[idx] == nil { |
| 330 | | − st.FuncArgsBuf[idx] = &strings.Builder{} |
| 331 | | − } |
| 332 | | − |
| 333 | | − // Append arguments delta if available and we have a valid call_id to reference |
| 334 | | − if args := tcs.Get("0.function.arguments"); args.Exists() && args.String() != "" { |
| 335 | | − // Prefer an already known call_id; fall back to newCallID if first time |
| 336 | | − refCallID := st.FuncCallIDs[idx] |
| 337 | | − if refCallID == "" { |
| 338 | | − refCallID = newCallID |
| 322 | + tcs.ForEach(func(_, tc gjson.Result) bool { |
| 323 | + toolIndex := int(tc.Get("index").Int()) |
| 324 | + key := toolStateKey(idx, toolIndex) |
| 325 | + newCallID := tc.Get("id").String() |
| 326 | + nameChunk := tc.Get("function.name").String() |
| 327 | + if nameChunk != "" { |
| 328 | + st.FuncNames[key] = nameChunk |
| 339 | 329 | } |
| 340 | | − if refCallID != "" { |
| 341 | | − ad := []byte(`{"type":"response.function_call_arguments.delta","sequence_number":0,"item_id":"","output_index":0,"delta":""}`) |
| 342 | | − ad, _ = sjson.SetBytes(ad, "sequence_number", nextSeq()) |
| 343 | | − ad, _ = sjson.SetBytes(ad, "item_id", fmt.Sprintf("fc_%s", refCallID)) |
| 344 | | − ad, _ = sjson.SetBytes(ad, "output_index", idx) |
| 345 | | − ad, _ = sjson.SetBytes(ad, "delta", args.String()) |
| 346 | | − out = append(out, emitRespEvent("response.function_call_arguments.delta", ad)) |
| 330 | + |
| 331 | + existingCallID := st.FuncCallIDs[key] |
| 332 | + effectiveCallID := existingCallID |
| 333 | + shouldEmitItem := false |
| 334 | + if existingCallID == "" && newCallID != "" { |
| 335 | + effectiveCallID = newCallID |
| 336 | + st.FuncCallIDs[key] = newCallID |
| 337 | + st.FuncOutputIx[key] = allocOutputIndex() |
| 338 | + shouldEmitItem = true |
| 347 | 339 | } |
| 348 | | − st.FuncArgsBuf[idx].WriteString(args.String()) |
| 349 | | − } |
| 340 | + |
| 341 | + if shouldEmitItem && effectiveCallID != "" { |
| 342 | + outputIndex := st.FuncOutputIx[key] |
| 343 | + 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":""}}`) |
| 344 | + o, _ = sjson.SetBytes(o, "sequence_number", nextSeq()) |
| 345 | + o, _ = sjson.SetBytes(o, "output_index", outputIndex) |
| 346 | + o, _ = sjson.SetBytes(o, "item.id", fmt.Sprintf("fc_%s", effectiveCallID)) |
| 347 | + o, _ = sjson.SetBytes(o, "item.call_id", effectiveCallID) |
| 348 | + o, _ = sjson.SetBytes(o, "item.name", st.FuncNames[key]) |
| 349 | + out = append(out, emitRespEvent("response.output_item.added", o)) |
| 350 | + } |
| 351 | + |
| 352 | + if st.FuncArgsBuf[key] == nil { |
| 353 | + st.FuncArgsBuf[key] = &strings.Builder{} |
| 354 | + } |
| 355 | + |
| 356 | + if args := tc.Get("function.arguments"); args.Exists() && args.String() != "" { |
| 357 | + refCallID := st.FuncCallIDs[key] |
| 358 | + if refCallID == "" { |
| 359 | + refCallID = newCallID |
| 360 | + } |
| 361 | + if refCallID != "" { |
| 362 | + outputIndex := st.FuncOutputIx[key] |
| 363 | + ad := []byte(`{"type":"response.function_call_arguments.delta","sequence_number":0,"item_id":"","output_index":0,"delta":""}`) |
| 364 | + ad, _ = sjson.SetBytes(ad, "sequence_number", nextSeq()) |
| 365 | + ad, _ = sjson.SetBytes(ad, "item_id", fmt.Sprintf("fc_%s", refCallID)) |
| 366 | + ad, _ = sjson.SetBytes(ad, "output_index", outputIndex) |
| 367 | + ad, _ = sjson.SetBytes(ad, "delta", args.String()) |
| 368 | + out = append(out, emitRespEvent("response.function_call_arguments.delta", ad)) |
| 369 | + } |
| 370 | + st.FuncArgsBuf[key].WriteString(args.String()) |
| 371 | + } |
| 372 | + return true |
| 373 | + }) |
| 350 | 374 | } |
| 351 | 375 | } |
| 352 | 376 | |
| @@ -360,15 +384,10 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context, · 7 unmodified lines |
| 360 | 384 | for i := range st.MsgItemAdded { |
| 361 | 385 | idxs = append(idxs, i) |
| 362 | 386 | } |
| 363 | | − for i := 0; i < len(idxs); i++ { |
| 364 | | − for j := i + 1; j < len(idxs); j++ { |
| 365 | | − if idxs[j] < idxs[i] { |
| 366 | | − idxs[i], idxs[j] = idxs[j], idxs[i] |
| 367 | | − } |
| 368 | | − } |
| 369 | | − } |
| 387 | + sort.Slice(idxs, func(i, j int) bool { return st.MsgOutputIx[idxs[i]] < st.MsgOutputIx[idxs[j]] }) |
| 370 | 388 | for _, i := range idxs { |
| 371 | 389 | if st.MsgItemAdded[i] && !st.MsgItemDone[i] { |
| 390 | + msgOutputIndex := st.MsgOutputIx[i] |
| 372 | 391 | fullText := "" |
| 373 | 392 | if b := st.MsgTextBuf[i]; b != nil { |
| 374 | 393 | fullText = b.String() |
| @@ -376,7 +395,7 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context, · 1 unmodified line |
| 376 | 395 | done := []byte(`{"type":"response.output_text.done","sequence_number":0,"item_id":"","output_index":0,"content_index":0,"text":"","logprobs":[]}`) |
| 377 | 396 | done, _ = sjson.SetBytes(done, "sequence_number", nextSeq()) |
| 378 | 397 | done, _ = sjson.SetBytes(done, "item_id", fmt.Sprintf("msg_%s_%d", st.ResponseID, i)) |
| 379 | | − done, _ = sjson.SetBytes(done, "output_index", i) |
| 398 | + done, _ = sjson.SetBytes(done, "output_index", msgOutputIndex) |
| 380 | 399 | done, _ = sjson.SetBytes(done, "content_index", 0) |
| 381 | 400 | done, _ = sjson.SetBytes(done, "text", fullText) |
| 382 | 401 | out = append(out, emitRespEvent("response.output_text.done", done)) |
| @@ -384,14 +403,14 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context, · 1 unmodified line |
| 384 | 403 | 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":""}}`) |
| 385 | 404 | partDone, _ = sjson.SetBytes(partDone, "sequence_number", nextSeq()) |
| 386 | 405 | partDone, _ = sjson.SetBytes(partDone, "item_id", fmt.Sprintf("msg_%s_%d", st.ResponseID, i)) |
| 387 | | − partDone, _ = sjson.SetBytes(partDone, "output_index", i) |
| 406 | + partDone, _ = sjson.SetBytes(partDone, "output_index", msgOutputIndex) |
| 388 | 407 | partDone, _ = sjson.SetBytes(partDone, "content_index", 0) |
| 389 | 408 | partDone, _ = sjson.SetBytes(partDone, "part.text", fullText) |
| 390 | 409 | out = append(out, emitRespEvent("response.content_part.done", partDone)) |
| 391 | 410 | |
| 392 | 411 | 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"}}`) |
| 393 | 412 | itemDone, _ = sjson.SetBytes(itemDone, "sequence_number", nextSeq()) |
| 394 | | − itemDone, _ = sjson.SetBytes(itemDone, "output_index", i) |
| 413 | + itemDone, _ = sjson.SetBytes(itemDone, "output_index", msgOutputIndex) |
| 395 | 414 | itemDone, _ = sjson.SetBytes(itemDone, "item.id", fmt.Sprintf("msg_%s_%d", st.ResponseID, i)) |
| 396 | 415 | itemDone, _ = sjson.SetBytes(itemDone, "item.content.0.text", fullText) |
| 397 | 416 | out = append(out, emitRespEvent("response.output_item.done", itemDone)) |
| @@ -407,43 +426,42 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context, · 9 unmodified lines |
| 407 | 426 | |
| 408 | 427 | // Emit function call done events for any active function calls |
| 409 | 428 | if len(st.FuncCallIDs) > 0 { |
| 410 | | − idxs := make([]int, 0, len(st.FuncCallIDs)) |
| 411 | | − for i := range st.FuncCallIDs { |
| 412 | | − idxs = append(idxs, i) |
| 413 | | − } |
| 414 | | − for i := 0; i < len(idxs); i++ { |
| 415 | | − for j := i + 1; j < len(idxs); j++ { |
| 416 | | − if idxs[j] < idxs[i] { |
| 417 | | − idxs[i], idxs[j] = idxs[j], idxs[i] |
| 418 | | − } |
| 419 | | − } |
| 420 | | − } |
| 421 | | − for _, i := range idxs { |
| 422 | | − callID := st.FuncCallIDs[i] |
| 423 | | − if callID == "" || st.FuncItemDone[i] { |
| 429 | + keys := make([]string, 0, len(st.FuncCallIDs)) |
| 430 | + for key := range st.FuncCallIDs { |
| 431 | + keys = append(keys, key) |
| 432 | + } |
| 433 | + sort.Slice(keys, func(i, j int) bool { |
| 434 | + left := st.FuncOutputIx[keys[i]] |
| 435 | + right := st.FuncOutputIx[keys[j]] |
| 436 | + return left < right || (left == right && keys[i] < keys[j]) |
| 437 | + }) |
| 438 | + for _, key := range keys { |
| 439 | + callID := st.FuncCallIDs[key] |
| 440 | + if callID == "" || st.FuncItemDone[key] { |
| 424 | 441 | continue |
| 425 | 442 | } |
| 443 | + outputIndex := st.FuncOutputIx[key] |
| 426 | 444 | args := "{}" |
| 427 | | − if b := st.FuncArgsBuf[i]; b != nil && b.Len() > 0 { |
| 445 | + if b := st.FuncArgsBuf[key]; b != nil && b.Len() > 0 { |
| 428 | 446 | args = b.String() |
| 429 | 447 | } |
| 430 | 448 | fcDone := []byte(`{"type":"response.function_call_arguments.done","sequence_number":0,"item_id":"","output_index":0,"arguments":""}`) |
| 431 | 449 | fcDone, _ = sjson.SetBytes(fcDone, "sequence_number", nextSeq()) |
| 432 | 450 | fcDone, _ = sjson.SetBytes(fcDone, "item_id", fmt.Sprintf("fc_%s", callID)) |
| 433 | | − fcDone, _ = sjson.SetBytes(fcDone, "output_index", i) |
| 451 | + fcDone, _ = sjson.SetBytes(fcDone, "output_index", outputIndex) |
| 434 | 452 | fcDone, _ = sjson.SetBytes(fcDone, "arguments", args) |
| 435 | 453 | out = append(out, emitRespEvent("response.function_call_arguments.done", fcDone)) |
| 436 | 454 | |
| 437 | 455 | itemDone := []byte(`{"type":"response.output_item.done","sequence_number":0,"output_index":0,"item":{"id":"","type":"function_call","status":"completed","arguments":"","call_id":"","name":""}}`) |
| 438 | 456 | itemDone, _ = sjson.SetBytes(itemDone, "sequence_number", nextSeq()) |
| 439 | | − itemDone, _ = sjson.SetBytes(itemDone, "output_index", i) |
| 457 | + itemDone, _ = sjson.SetBytes(itemDone, "output_index", outputIndex) |
| 440 | 458 | itemDone, _ = sjson.SetBytes(itemDone, "item.id", fmt.Sprintf("fc_%s", callID)) |
| 441 | 459 | itemDone, _ = sjson.SetBytes(itemDone, "item.arguments", args) |
| 442 | 460 | itemDone, _ = sjson.SetBytes(itemDone, "item.call_id", callID) |
| 443 | | − itemDone, _ = sjson.SetBytes(itemDone, "item.name", st.FuncNames[i]) |
| 461 | + itemDone, _ = sjson.SetBytes(itemDone, "item.name", st.FuncNames[key]) |
| 444 | 462 | out = append(out, emitRespEvent("response.output_item.done", itemDone)) |
| 445 | | − st.FuncItemDone[i] = true |
| 446 | | − st.FuncArgsDone[i] = true |
| 463 | + st.FuncItemDone[key] = true |
| 464 | + st.FuncArgsDone[key] = true |
| 447 | 465 | } |
| 448 | 466 | } |
| 449 | 467 | completed := []byte(`{"type":"response.completed","sequence_number":0,"response":{"id":"","object":"response","created_at":0,"status":"completed","background":false,"error":null}}`) |
| @@ -516,28 +534,21 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context, · 66 unmodified lines |
| 516 | 534 | } |
| 517 | 535 | // Build response.output using aggregated buffers |
| 518 | 536 | outputsWrapper := []byte(`{"arr":[]}`) |
| 537 | + type completedOutputItem struct { |
| 538 | + index int |
| 539 | + raw []byte |
| 540 | + } |
| 541 | + outputItems := make([]completedOutputItem, 0, len(st.Reasonings)+len(st.MsgItemAdded)+len(st.FuncArgsBuf)) |
| 519 | 542 | if len(st.Reasonings) > 0 { |
| 520 | 543 | for _, r := range st.Reasonings { |
| 521 | 544 | item := []byte(`{"id":"","type":"reasoning","summary":[{"type":"summary_text","text":""}]}`) |
| 522 | 545 | item, _ = sjson.SetBytes(item, "id", r.ReasoningID) |
| 523 | 546 | item, _ = sjson.SetBytes(item, "summary.0.text", r.ReasoningData) |
| 524 | | − outputsWrapper, _ = sjson.SetRawBytes(outputsWrapper, "arr.-1", item) |
| 547 | + outputItems = append(outputItems, completedOutputItem{index: r.OutputIndex, raw: item}) |
| 525 | 548 | } |
| 526 | 549 | } |
| 527 | | − // Append message items in ascending index order |
| 528 | 550 | if len(st.MsgItemAdded) > 0 { |
| 529 | | − midxs := make([]int, 0, len(st.MsgItemAdded)) |
| 530 | 551 | for i := range st.MsgItemAdded { |
| 531 | | − midxs = append(midxs, i) |
| 532 | | − } |
| 533 | | − for i := 0; i < len(midxs); i++ { |
| 534 | | − for j := i + 1; j < len(midxs); j++ { |
| 535 | | − if midxs[j] < midxs[i] { |
| 536 | | − midxs[i], midxs[j] = midxs[j], midxs[i] |
| 537 | | − } |
| 538 | | − } |
| 539 | | − } |
| 540 | | − for _, i := range midxs { |
| 541 | 552 | txt := "" |
| 542 | 553 | if b := st.MsgTextBuf[i]; b != nil { |
| 543 | 554 | txt = b.String() |
| @@ -545,37 +556,29 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context, · 1 unmodified line |
| 545 | 556 | item := []byte(`{"id":"","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":""}],"role":"assistant"}`) |
| 546 | 557 | item, _ = sjson.SetBytes(item, "id", fmt.Sprintf("msg_%s_%d", st.ResponseID, i)) |
| 547 | 558 | item, _ = sjson.SetBytes(item, "content.0.text", txt) |
| 548 | | − outputsWrapper, _ = sjson.SetRawBytes(outputsWrapper, "arr.-1", item) |
| 559 | + outputItems = append(outputItems, completedOutputItem{index: st.MsgOutputIx[i], raw: item}) |
| 549 | 560 | } |
| 550 | 561 | } |
| 551 | 562 | if len(st.FuncArgsBuf) > 0 { |
| 552 | | − idxs := make([]int, 0, len(st.FuncArgsBuf)) |
| 553 | | − for i := range st.FuncArgsBuf { |
| 554 | | − idxs = append(idxs, i) |
| 555 | | − } |
| 556 | | − // small-N sort without extra imports |
| 557 | | − for i := 0; i < len(idxs); i++ { |
| 558 | | − for j := i + 1; j < len(idxs); j++ { |
| 559 | | − if idxs[j] < idxs[i] { |
| 560 | | − idxs[i], idxs[j] = idxs[j], idxs[i] |
| 561 | | − } |
| 562 | | − } |
| 563 | | − } |
| 564 | | − for _, i := range idxs { |
| 563 | + for key := range st.FuncArgsBuf { |
| 565 | 564 | args := "" |
| 566 | | − if b := st.FuncArgsBuf[i]; b != nil { |
| 565 | + if b := st.FuncArgsBuf[key]; b != nil { |
| 567 | 566 | args = b.String() |
| 568 | 567 | } |
| 569 | | − callID := st.FuncCallIDs[i] |
| 570 | | − name := st.FuncNames[i] |
| 568 | + callID := st.FuncCallIDs[key] |
| 569 | + name := st.FuncNames[key] |
| 571 | 570 | item := []byte(`{"id":"","type":"function_call","status":"completed","arguments":"","call_id":"","name":""}`) |
| 572 | 571 | item, _ = sjson.SetBytes(item, "id", fmt.Sprintf("fc_%s", callID)) |
| 573 | 572 | item, _ = sjson.SetBytes(item, "arguments", args) |
| 574 | 573 | item, _ = sjson.SetBytes(item, "call_id", callID) |
| 575 | 574 | item, _ = sjson.SetBytes(item, "name", name) |
| 576 | | − outputsWrapper, _ = sjson.SetRawBytes(outputsWrapper, "arr.-1", item) |
| 575 | + outputItems = append(outputItems, completedOutputItem{index: st.FuncOutputIx[key], raw: item}) |
| 577 | 576 | } |
| 578 | 577 | } |
| 578 | + sort.Slice(outputItems, func(i, j int) bool { return outputItems[i].index < outputItems[j].index }) |
| 579 | + for _, item := range outputItems { |
| 580 | + outputsWrapper, _ = sjson.SetRawBytes(outputsWrapper, "arr.-1", item.raw) |
| 581 | + } |
| 579 | 582 | if gjson.GetBytes(outputsWrapper, "arr.#").Int() > 0 { |
| 580 | 583 | completed, _ = sjson.SetRawBytes(completed, "response.output", []byte(gjson.GetBytes(outputsWrapper, "arr").Raw)) |
| 581 | 584 | } |