diff --git a/internal/translator/antigravity/interactions/interactions_antigravity_response.go b/internal/translator/antigravity/interactions/interactions_antigravity_response.go index 4b46c060..b2a957a3 100644 --- a/internal/translator/antigravity/interactions/interactions_antigravity_response.go +++ b/internal/translator/antigravity/interactions/interactions_antigravity_response.go @@ -93,12 +93,16 @@ func ConvertAntigravityResponseToInteractionsNonStream(ctx context.Context, mode } out, _ = sjson.SetBytes(out, "id", id) out, _ = sjson.SetBytes(out, "model", modelName) + var steps [][]byte root.Get("candidates.0.content.parts").ForEach(func(_, part gjson.Result) bool { if step := antigravityPartToInteractionsStep(part); len(step) > 0 { - out, _ = sjson.SetRawBytes(out, "steps.-1", step) + steps = append(steps, step) } return true }) + if len(steps) > 0 { + out = translatorcommon.SetRawArrayItems(out, "steps", steps) + } out = setInteractionsUsageFromAntigravity(out, "usage", root) return out } @@ -338,7 +342,7 @@ func antigravityPartToInteractionsStep(part gjson.Result) []byte { } item := []byte(`{"type":"text","text":""}`) item, _ = sjson.SetBytes(item, "text", text.String()) - step, _ = sjson.SetRawBytes(step, "content.-1", item) + step = translatorcommon.SetRawArrayItems(step, "content", [][]byte{item}) return step } if inline := part.Get("inlineData"); inline.Exists() { diff --git a/internal/translator/codex/claude/codex_claude_response.go b/internal/translator/codex/claude/codex_claude_response.go index 45efd8a6..a0bae8a4 100644 --- a/internal/translator/codex/claude/codex_claude_response.go +++ b/internal/translator/codex/claude/codex_claude_response.go @@ -370,6 +370,7 @@ func ConvertCodexResponseToClaudeNonStream(_ context.Context, _ string, original hasToolCall := false webSearchSeen := make(map[string]struct{}) + var contentBlocks [][]byte if output := responseData.Get("output"); output.Exists() && output.IsArray() { output.ForEach(func(_, item gjson.Result) bool { @@ -413,7 +414,7 @@ func ConvertCodexResponseToClaudeNonStream(_ context.Context, _ string, original if signature != "" { block, _ = sjson.SetBytes(block, "signature", signature) } - out, _ = sjson.SetRawBytes(out, "content.-1", block) + contentBlocks = append(contentBlocks, block) } case "message": if content := item.Get("content"); content.Exists() { @@ -424,7 +425,7 @@ func ConvertCodexResponseToClaudeNonStream(_ context.Context, _ string, original if text != "" { block := []byte(`{"type":"text","text":""}`) block, _ = sjson.SetBytes(block, "text", text) - out, _ = sjson.SetRawBytes(out, "content.-1", block) + contentBlocks = append(contentBlocks, block) } } return true @@ -434,12 +435,12 @@ func ConvertCodexResponseToClaudeNonStream(_ context.Context, _ string, original if text != "" { block := []byte(`{"type":"text","text":""}`) block, _ = sjson.SetBytes(block, "text", text) - out, _ = sjson.SetRawBytes(out, "content.-1", block) + contentBlocks = append(contentBlocks, block) } } } case "web_search_call": - out = appendCodexWebSearchNonStreamContent(out, item, webSearchSeen) + contentBlocks = appendCodexWebSearchNonStreamBlocks(contentBlocks, item, webSearchSeen) case "function_call": hasToolCall = true name := item.Get("name").String() @@ -458,12 +459,16 @@ func ConvertCodexResponseToClaudeNonStream(_ context.Context, _ string, original } } toolBlock, _ = sjson.SetRawBytes(toolBlock, "input", []byte(inputRaw)) - out, _ = sjson.SetRawBytes(out, "content.-1", toolBlock) + contentBlocks = append(contentBlocks, toolBlock) } return true }) } + if len(contentBlocks) > 0 { + out = translatorcommon.SetRawArrayItems(out, "content", contentBlocks) + } + out, _ = sjson.SetBytes(out, "stop_reason", mapCodexStopReasonToClaude(codexStopReason(responseData), hasToolCall)) out = setClaudeStopSequence(out, "stop_sequence", responseData) diff --git a/internal/translator/codex/claude/codex_claude_response_web_search.go b/internal/translator/codex/claude/codex_claude_response_web_search.go index c5c8f866..b6f70287 100644 --- a/internal/translator/codex/claude/codex_claude_response_web_search.go +++ b/internal/translator/codex/claude/codex_claude_response_web_search.go @@ -134,7 +134,7 @@ func codexWebSearchResultContent(root, item gjson.Result) []byte { if !results.IsArray() { return nil } - content := []byte(`[]`) + var resultBlocks [][]byte results.ForEach(func(_, result gjson.Result) bool { url := strings.TrimSpace(result.Get("url").String()) if url == "" { @@ -147,28 +147,31 @@ func codexWebSearchResultContent(root, item gjson.Result) []byte { title = url } block, _ = sjson.SetBytes(block, "title", title) - content, _ = sjson.SetRawBytes(content, "-1", block) + resultBlocks = append(resultBlocks, block) return true }) - return content + if len(resultBlocks) == 0 { + return []byte(`[]`) + } + return translatorcommon.JoinRawArray(resultBlocks) } -func appendCodexWebSearchNonStreamContent(out []byte, item gjson.Result, seen map[string]struct{}) []byte { +func appendCodexWebSearchNonStreamBlocks(contentBlocks [][]byte, item gjson.Result, seen map[string]struct{}) [][]byte { id := strings.TrimSpace(item.Get("id").String()) if id == "" { - return out + return contentBlocks } if seen == nil { seen = make(map[string]struct{}) } if _, ok := seen[id]; ok { - return out + return contentBlocks } emptyRoot := gjson.Result{} query := codexWebSearchQuery(emptyRoot, item) resultContent := codexWebSearchResultContent(emptyRoot, item) if query == "" && len(resultContent) == 0 { - return out + return contentBlocks } useBlock := []byte(`{"type":"server_tool_use","id":"","name":"web_search","input":{}}`) @@ -177,14 +180,22 @@ func appendCodexWebSearchNonStreamContent(out []byte, item gjson.Result, seen ma input, _ := json.Marshal(map[string]string{"query": query}) useBlock, _ = sjson.SetRawBytes(useBlock, "input", input) } - out, _ = sjson.SetRawBytes(out, "content.-1", useBlock) + contentBlocks = append(contentBlocks, useBlock) resultBlock := []byte(`{"type":"web_search_tool_result","tool_use_id":"","content":[]}`) resultBlock, _ = sjson.SetBytes(resultBlock, "tool_use_id", id) if len(resultContent) > 0 { resultBlock, _ = sjson.SetRawBytes(resultBlock, "content", resultContent) } - out, _ = sjson.SetRawBytes(out, "content.-1", resultBlock) + contentBlocks = append(contentBlocks, resultBlock) seen[id] = struct{}{} + return contentBlocks +} + +func appendCodexWebSearchNonStreamContent(out []byte, item gjson.Result, seen map[string]struct{}) []byte { + blocks := appendCodexWebSearchNonStreamBlocks(nil, item, seen) + for _, block := range blocks { + out, _ = sjson.SetRawBytes(out, "content.-1", block) + } return out } diff --git a/internal/translator/codex/interactions/interactions_codex_response.go b/internal/translator/codex/interactions/interactions_codex_response.go index cea921a5..7d6ad3d6 100644 --- a/internal/translator/codex/interactions/interactions_codex_response.go +++ b/internal/translator/codex/interactions/interactions_codex_response.go @@ -101,19 +101,31 @@ func ConvertCodexResponseToInteractionsNonStream(ctx context.Context, modelName } else { out, _ = sjson.SetBytes(out, "model", modelName) } + var steps [][]byte response.Get("output").ForEach(func(_, item gjson.Result) bool { switch item.Get("type").String() { case "message": - out = appendCodexMessageItemToInteractions(out, item) + if step := buildCodexMessageItemToInteractions(item); len(step) > 0 { + steps = append(steps, step) + } case "reasoning": - out = appendCodexReasoningItemToInteractions(out, item) + if step := buildCodexReasoningItemToInteractions(item); len(step) > 0 { + steps = append(steps, step) + } case "function_call", "tool_call": - out = appendCodexFunctionCallItemToInteractions(out, item) + if step := buildCodexFunctionCallItemToInteractions(item); len(step) > 0 { + steps = append(steps, step) + } case "image_generation_call": - out = appendCodexImageItemToInteractions(out, item) + if step := buildCodexImageItemToInteractions(item); len(step) > 0 { + steps = append(steps, step) + } } return true }) + if len(steps) > 0 { + out = translatorcommon.SetRawArrayItems(out, "steps", steps) + } out = setCodexInteractionsUsage(out, "usage", response.Get("usage"), false) return out } @@ -303,33 +315,32 @@ func appendCodexInteractionsStepStop(out [][]byte, st *codexToInteractionsStream return out } -func appendCodexMessageItemToInteractions(out []byte, item gjson.Result) []byte { - step := []byte(`{"type":"model_output","content":[]}`) +func buildCodexMessageItemToInteractions(item gjson.Result) []byte { + var contents [][]byte item.Get("content").ForEach(func(_, content gjson.Result) bool { if contentItem := codexContentToInteractionsContent(content); len(contentItem) > 0 { - step, _ = sjson.SetRawBytes(step, "content.-1", contentItem) + contents = append(contents, contentItem) } return true }) - if gjson.GetBytes(step, "content.#").Int() == 0 { - return out + if len(contents) == 0 { + return nil } - out, _ = sjson.SetRawBytes(out, "steps.-1", step) - return out + step := []byte(`{"type":"model_output","content":[]}`) + return translatorcommon.SetRawArrayItems(step, "content", contents) } -func appendCodexReasoningItemToInteractions(out []byte, item gjson.Result) []byte { +func buildCodexReasoningItemToInteractions(item gjson.Result) []byte { text := codexReasoningText(item) if text == "" { - return out + return nil } step := []byte(`{"type":"thought","content":[{"type":"text","text":""}]}`) step, _ = sjson.SetBytes(step, "content.0.text", text) - out, _ = sjson.SetRawBytes(out, "steps.-1", step) - return out + return step } -func appendCodexFunctionCallItemToInteractions(out []byte, item gjson.Result) []byte { +func buildCodexFunctionCallItemToInteractions(item gjson.Result) []byte { step := []byte(`{"type":"function_call","name":"","arguments":{}}`) step, _ = sjson.SetBytes(step, "name", item.Get("name").String()) if callID := codexItemCallID(item); callID != "" { @@ -338,19 +349,45 @@ func appendCodexFunctionCallItemToInteractions(out []byte, item gjson.Result) [] if args := codexArgumentsJSON(item.Get("arguments")); len(args) > 0 { step, _ = sjson.SetRawBytes(step, "arguments", args) } - out, _ = sjson.SetRawBytes(out, "steps.-1", step) - return out + return step } -func appendCodexImageItemToInteractions(out []byte, item gjson.Result) []byte { +func buildCodexImageItemToInteractions(item gjson.Result) []byte { result := item.Get("result").String() if result == "" { - return out + return nil } step := []byte(`{"type":"model_output","content":[{"type":"image","mime_type":"","data":""}]}`) step, _ = sjson.SetBytes(step, "content.0.mime_type", mimeTypeFromCodexOutputFormat(item.Get("output_format").String())) step, _ = sjson.SetBytes(step, "content.0.data", result) - out, _ = sjson.SetRawBytes(out, "steps.-1", step) + return step +} + +func appendCodexMessageItemToInteractions(out []byte, item gjson.Result) []byte { + if step := buildCodexMessageItemToInteractions(item); len(step) > 0 { + out, _ = sjson.SetRawBytes(out, "steps.-1", step) + } + return out +} + +func appendCodexReasoningItemToInteractions(out []byte, item gjson.Result) []byte { + if step := buildCodexReasoningItemToInteractions(item); len(step) > 0 { + out, _ = sjson.SetRawBytes(out, "steps.-1", step) + } + return out +} + +func appendCodexFunctionCallItemToInteractions(out []byte, item gjson.Result) []byte { + if step := buildCodexFunctionCallItemToInteractions(item); len(step) > 0 { + out, _ = sjson.SetRawBytes(out, "steps.-1", step) + } + return out +} + +func appendCodexImageItemToInteractions(out []byte, item gjson.Result) []byte { + if step := buildCodexImageItemToInteractions(item); len(step) > 0 { + out, _ = sjson.SetRawBytes(out, "steps.-1", step) + } return out } diff --git a/internal/translator/gemini/claude/gemini_claude_request.go b/internal/translator/gemini/claude/gemini_claude_request.go index 4cf0ecaa..39bf176d 100644 --- a/internal/translator/gemini/claude/gemini_claude_request.go +++ b/internal/translator/gemini/claude/gemini_claude_request.go @@ -68,7 +68,11 @@ func convertClaudeRequestToGemini(modelName string, inputRawJSON []byte, _ bool, out, _ = sjson.SetRawBytes(out, "systemInstruction", systemInstruction) } } else if systemResult.Type == gjson.String && !util.IsClaudeCodeAttributionSystemText(systemResult.String()) { - out, _ = sjson.SetBytes(out, "systemInstruction.parts.-1.text", systemResult.String()) + part := []byte(`{"text":""}`) + part, _ = sjson.SetBytes(part, "text", systemResult.String()) + systemInstruction := []byte(`{"parts":[]}`) + systemInstruction = translatorcommon.SetRawArrayItems(systemInstruction, "parts", [][]byte{part}) + out, _ = sjson.SetRawBytes(out, "systemInstruction", systemInstruction) } // contents diff --git a/internal/translator/gemini/claude/gemini_claude_request_test.go b/internal/translator/gemini/claude/gemini_claude_request_test.go index 58b9077c..64a56a62 100644 --- a/internal/translator/gemini/claude/gemini_claude_request_test.go +++ b/internal/translator/gemini/claude/gemini_claude_request_test.go @@ -53,6 +53,9 @@ func TestConvertClaudeRequestToGemini_StringSystemInstruction(t *testing.T) { if got := gjson.GetBytes(output, "systemInstruction.parts.0.text").String(); got != "Be concise" { t.Fatalf("Expected systemInstruction text %q, got %q", "Be concise", got) } + if gjson.GetBytes(output, "systemInstruction.role").Exists() { + t.Fatalf("Expected systemInstruction.role to not exist, got %q", gjson.GetBytes(output, "systemInstruction.role").String()) + } if gjson.GetBytes(output, "system_instruction").Exists() { t.Fatalf("Legacy system_instruction field should not be emitted: %s", output) } diff --git a/internal/translator/gemini/interactions/interactions_gemini_common.go b/internal/translator/gemini/interactions/interactions_gemini_common.go index fdecea0c..303ae534 100644 --- a/internal/translator/gemini/interactions/interactions_gemini_common.go +++ b/internal/translator/gemini/interactions/interactions_gemini_common.go @@ -376,12 +376,16 @@ func convertGeminiResponseToInteractionsNonStreamDirect(modelName string, origin } out, _ = sjson.SetBytes(out, "id", id) out, _ = sjson.SetBytes(out, "model", modelName) + var steps [][]byte root.Get("candidates.0.content.parts").ForEach(func(_, part gjson.Result) bool { if step := geminiPartToInteractionsStep(part); len(step) > 0 { - out, _ = sjson.SetRawBytes(out, "steps.-1", step) + steps = append(steps, step) } return true }) + if len(steps) > 0 { + out = translatorcommon.SetRawArrayItems(out, "steps", steps) + } out = setInteractionsUsageFromGemini(out, "usage", root) return out } @@ -1259,7 +1263,7 @@ func geminiPartToInteractionsStep(part gjson.Result) []byte { } item := []byte(`{"text":""}`) item, _ = sjson.SetBytes(item, "text", text.String()) - step, _ = sjson.SetRawBytes(step, "content.-1", item) + step = translatorcommon.SetRawArrayItems(step, "content", [][]byte{item}) return step } if inline := part.Get("inlineData"); inline.Exists() { @@ -1269,13 +1273,13 @@ func geminiPartToInteractionsStep(part gjson.Result) []byte { } item := geminiInlineDataToInteractionsContent(mimeType, inline.Get("data").String()) step := []byte(`{"type":"model_output","content":[]}`) - step, _ = sjson.SetRawBytes(step, "content.-1", item) + step = translatorcommon.SetRawArrayItems(step, "content", [][]byte{item}) return step } if inline := part.Get("inline_data"); inline.Exists() { item := geminiInlineDataToInteractionsContent(inline.Get("mime_type").String(), inline.Get("data").String()) step := []byte(`{"type":"model_output","content":[]}`) - step, _ = sjson.SetRawBytes(step, "content.-1", item) + step = translatorcommon.SetRawArrayItems(step, "content", [][]byte{item}) return step } return nil diff --git a/internal/translator/gemini/interactions/interactions_gemini_response.go b/internal/translator/gemini/interactions/interactions_gemini_response.go index c89b3052..0c1e9d2b 100644 --- a/internal/translator/gemini/interactions/interactions_gemini_response.go +++ b/internal/translator/gemini/interactions/interactions_gemini_response.go @@ -231,11 +231,15 @@ func buildInteractionsGeminiChunk(st *interactionsToGeminiStreamState, modelName if len(parts) == 0 && includeEmptyPart { parts = append(parts, geminiTextPartJSON("", false)) } + validParts := make([][]byte, 0, len(parts)) for _, part := range parts { if len(part) > 0 { - out, _ = sjson.SetRawBytes(out, "candidates.0.content.parts.-1", part) + validParts = append(validParts, part) } } + if len(validParts) > 0 { + out = translatorcommon.SetRawArrayItems(out, "candidates.0.content.parts", validParts) + } if finishReason != "" { out, _ = sjson.SetBytes(out, "candidates.0.finishReason", finishReason) } diff --git a/internal/translator/gemini/openai/responses/gemini_openai-responses_request.go b/internal/translator/gemini/openai/responses/gemini_openai-responses_request.go index 6ebb4336..afbe9a3c 100644 --- a/internal/translator/gemini/openai/responses/gemini_openai-responses_request.go +++ b/internal/translator/gemini/openai/responses/gemini_openai-responses_request.go @@ -420,10 +420,19 @@ func coalesceAdjacentOpenAIResponsesModelContents(contents [][]byte) [][]byte { coalesced = append(coalesced, content) continue } + var extraParts [][]byte parts.ForEach(func(_, part gjson.Result) bool { - merged, _ = sjson.SetRawBytes(merged, "parts.-1", []byte(part.Raw)) + extraParts = append(extraParts, []byte(part.Raw)) return true }) + if len(extraParts) > 0 { + var existingParts [][]byte + gjson.GetBytes(merged, "parts").ForEach(func(_, p gjson.Result) bool { + existingParts = append(existingParts, []byte(p.Raw)) + return true + }) + merged = translatorcommon.SetRawArrayItems(merged, "parts", append(existingParts, extraParts...)) + } coalesced[lastIndex] = merged } return coalesced @@ -809,31 +818,30 @@ func buildOpenAIResponsesReasoningModelContent(thoughtText, visibleText, signatu if thoughtText == "" && visibleText == "" { carrier := []byte(`{"text":"","thoughtSignature":""}`) carrier, _ = sjson.SetBytes(carrier, "thoughtSignature", signature) - modelContent, _ = sjson.SetRawBytes(modelContent, "parts.-1", carrier) - return modelContent + return translatorcommon.SetRawArrayItems(modelContent, "parts", [][]byte{carrier}) } + var parts [][]byte if thoughtText != "" { thought := []byte(`{"text":"","thought":true}`) thought, _ = sjson.SetBytes(thought, "text", thoughtText) if visibleText == "" { thought, _ = sjson.SetBytes(thought, "thoughtSignature", signature) } - modelContent, _ = sjson.SetRawBytes(modelContent, "parts.-1", thought) + parts = append(parts, thought) } if visibleText != "" { visible := []byte(`{"text":"","thoughtSignature":""}`) visible, _ = sjson.SetBytes(visible, "text", visibleText) visible, _ = sjson.SetBytes(visible, "thoughtSignature", signature) - modelContent, _ = sjson.SetRawBytes(modelContent, "parts.-1", visible) + parts = append(parts, visible) } - return modelContent + return translatorcommon.SetRawArrayItems(modelContent, "parts", parts) } thought := []byte(`{"text":"","thoughtSignature":"","thought":true}`) thought, _ = sjson.SetBytes(thought, "text", thoughtText) thought, _ = sjson.SetBytes(thought, "thoughtSignature", signature) - modelContent, _ = sjson.SetRawBytes(modelContent, "parts.-1", thought) - return modelContent + return translatorcommon.SetRawArrayItems(modelContent, "parts", [][]byte{thought}) } func openAIResponsesGeminiThoughtSignature(rawSignature string) string { diff --git a/internal/translator/interactions/claude/interactions_claude_response.go b/internal/translator/interactions/claude/interactions_claude_response.go index 42a27990..2e9a2cb9 100644 --- a/internal/translator/interactions/claude/interactions_claude_response.go +++ b/internal/translator/interactions/claude/interactions_claude_response.go @@ -61,13 +61,14 @@ func ConvertInteractionsResponseToClaudeNonStream(_ context.Context, modelName s steps = root.Get("steps") } sawToolCall := false + var contentBlocks [][]byte steps.ForEach(func(_, step gjson.Result) bool { switch step.Get("type").String() { case "thought": for _, text := range interactionsContentTexts(step.Get("content")) { block := []byte(`{"type":"thinking","thinking":""}`) block, _ = sjson.SetBytes(block, "thinking", text) - out, _ = sjson.SetRawBytes(out, "content.-1", block) + contentBlocks = append(contentBlocks, block) } case "function_call": sawToolCall = true @@ -81,16 +82,19 @@ func ConvertInteractionsResponseToClaudeNonStream(_ context.Context, modelName s if args.Exists() && args.IsObject() { block, _ = sjson.SetRawBytes(block, "input", []byte(args.Raw)) } - out, _ = sjson.SetRawBytes(out, "content.-1", block) + contentBlocks = append(contentBlocks, block) default: for _, text := range interactionsContentTexts(step.Get("content")) { block := []byte(`{"type":"text","text":""}`) block, _ = sjson.SetBytes(block, "text", text) - out, _ = sjson.SetRawBytes(out, "content.-1", block) + contentBlocks = append(contentBlocks, block) } } return true }) + if len(contentBlocks) > 0 { + out = translatorcommon.SetRawArrayItems(out, "content", contentBlocks) + } if sawToolCall { out, _ = sjson.SetBytes(out, "stop_reason", "tool_use") } diff --git a/internal/translator/openai/interactions/chat-completions/interactions_openai_response.go b/internal/translator/openai/interactions/chat-completions/interactions_openai_response.go index e2c81ec3..d839366c 100644 --- a/internal/translator/openai/interactions/chat-completions/interactions_openai_response.go +++ b/internal/translator/openai/interactions/chat-completions/interactions_openai_response.go @@ -58,20 +58,21 @@ func ConvertOpenAIResponseToInteractionsNonStream(ctx context.Context, modelName out, _ = sjson.SetBytes(out, "id", firstNonEmpty(root.Get("id").String(), fmt.Sprintf("interaction_%d", time.Now().UnixNano()))) out, _ = sjson.SetBytes(out, "model", firstNonEmpty(modelName, root.Get("model").String())) choices := root.Get("choices") + var steps [][]byte choices.ForEach(func(_, choice gjson.Result) bool { message := choice.Get("message") if reasoning := message.Get("reasoning_content"); reasoning.Exists() { for _, text := range openAIReasoningTexts(reasoning) { - out, _ = sjson.SetRawBytes(out, "steps.-1", interactionsTextStep("thought", text)) + steps = append(steps, interactionsTextStep("thought", text)) } } if content := message.Get("content"); content.Exists() && content.String() != "" { - out, _ = sjson.SetRawBytes(out, "steps.-1", interactionsTextStep("model_output", content.String())) + steps = append(steps, interactionsTextStep("model_output", content.String())) } if toolCalls := message.Get("tool_calls"); toolCalls.Exists() && toolCalls.IsArray() { toolCalls.ForEach(func(_, toolCall gjson.Result) bool { if step, ok := openAIToolCallToInteractionsStep(toolCall); ok { - out, _ = sjson.SetRawBytes(out, "steps.-1", step) + steps = append(steps, step) } return true }) @@ -81,6 +82,9 @@ func ConvertOpenAIResponseToInteractionsNonStream(ctx context.Context, modelName } return true }) + if len(steps) > 0 { + out = translatorcommon.SetRawArrayItems(out, "steps", steps) + } out = setInteractionsUsageFromOpenAIChat(out, "usage", root.Get("usage")) return out } diff --git a/internal/translator/openai/interactions/chat-completions/openai_interactions_response.go b/internal/translator/openai/interactions/chat-completions/openai_interactions_response.go index 52899f33..503ae122 100644 --- a/internal/translator/openai/interactions/chat-completions/openai_interactions_response.go +++ b/internal/translator/openai/interactions/chat-completions/openai_interactions_response.go @@ -64,6 +64,7 @@ func ConvertInteractionsResponseToOpenAINonStream(ctx context.Context, modelName var textBuilder strings.Builder var reasoningBuilder strings.Builder sawToolCall := false + var toolCalls [][]byte steps.ForEach(func(_, step gjson.Result) bool { switch step.Get("type").String() { case "model_output": @@ -76,7 +77,7 @@ func ConvertInteractionsResponseToOpenAINonStream(ctx context.Context, modelName } case "function_call": sawToolCall = true - out, _ = sjson.SetRawBytes(out, "choices.0.message.tool_calls.-1", openAIChatToolCallFromInteractions(step, gjson.Result{})) + toolCalls = append(toolCalls, openAIChatToolCallFromInteractions(step, gjson.Result{})) } return true }) @@ -86,6 +87,9 @@ func ConvertInteractionsResponseToOpenAINonStream(ctx context.Context, modelName if reasoningBuilder.Len() > 0 { out, _ = sjson.SetBytes(out, "choices.0.message.reasoning_content", reasoningBuilder.String()) } + if len(toolCalls) > 0 { + out = translatorcommon.SetRawArrayItems(out, "choices.0.message.tool_calls", toolCalls) + } if sawToolCall { out, _ = sjson.SetBytes(out, "choices.0.message.content", nil) out, _ = sjson.SetBytes(out, "choices.0.finish_reason", "tool_calls") diff --git a/internal/translator/openai/interactions/responses/interactions_openai_responses_request.go b/internal/translator/openai/interactions/responses/interactions_openai_responses_request.go index 4b8ace80..2dda10db 100644 --- a/internal/translator/openai/interactions/responses/interactions_openai_responses_request.go +++ b/internal/translator/openai/interactions/responses/interactions_openai_responses_request.go @@ -235,7 +235,7 @@ func responsesInputItemToInteractions(item gjson.Result, functionNamesByCallID m step := []byte(`{"type":"","content":[]}`) step, _ = sjson.SetBytes(step, "type", stepType) if part, ok := responsesContentPartToInteractions(item); ok { - step, _ = sjson.SetRawBytes(step, "content.-1", part) + step = translatorcommon.SetRawArrayItems(step, "content", [][]byte{part}) } return step default: diff --git a/internal/translator/openai/interactions/responses/interactions_openai_responses_response.go b/internal/translator/openai/interactions/responses/interactions_openai_responses_response.go index 97f53e99..47b9b321 100644 --- a/internal/translator/openai/interactions/responses/interactions_openai_responses_response.go +++ b/internal/translator/openai/interactions/responses/interactions_openai_responses_response.go @@ -157,14 +157,18 @@ func interactionsStepToResponsesOutput(step gjson.Result) ([]byte, bool) { if content.Type == gjson.String { part := []byte(`{"type":"output_text","text":""}`) part, _ = sjson.SetBytes(part, "text", content.String()) - item, _ = sjson.SetRawBytes(item, "content.-1", part) + item = translatorcommon.SetRawArrayItems(item, "content", [][]byte{part}) } else { + var parts [][]byte content.ForEach(func(_, part gjson.Result) bool { if converted, ok := interactionsContentPartToResponses(part, "assistant"); ok { - item, _ = sjson.SetRawBytes(item, "content.-1", converted) + parts = append(parts, converted) } return true }) + if len(parts) > 0 { + item = translatorcommon.SetRawArrayItems(item, "content", parts) + } } return item, true case "thought": @@ -172,10 +176,15 @@ func interactionsStepToResponsesOutput(step gjson.Result) ([]byte, bool) { if signature := interactionsThoughtSignature(step); signature != "" { item, _ = sjson.SetBytes(item, "encrypted_content", signature) } - for _, text := range interactionsContentTexts(step.Get("content")) { - part := []byte(`{"type":"summary_text","text":""}`) - part, _ = sjson.SetBytes(part, "text", text) - item, _ = sjson.SetRawBytes(item, "summary.-1", part) + texts := interactionsContentTexts(step.Get("content")) + if len(texts) > 0 { + summaries := make([][]byte, 0, len(texts)) + for _, text := range texts { + part := []byte(`{"type":"summary_text","text":""}`) + part, _ = sjson.SetBytes(part, "text", text) + summaries = append(summaries, part) + } + item = translatorcommon.SetRawArrayItems(item, "summary", summaries) } return item, true case "function_call": @@ -447,6 +456,7 @@ func setResponsesCompletedOutput(payload []byte, st *interactionsToResponsesStre maxIndex = index } } + var outputItems [][]byte for index := 0; index <= maxIndex; index++ { itemType, ok := st.ItemTypes[index] if !ok { @@ -454,9 +464,12 @@ func setResponsesCompletedOutput(payload []byte, st *interactionsToResponsesStre } item, ok := responsesCompletedOutputItem(index, itemType, st) if ok { - payload, _ = sjson.SetRawBytes(payload, "response.output.-1", item) + outputItems = append(outputItems, item) } } + if len(outputItems) > 0 { + payload = translatorcommon.SetRawArrayItems(payload, "response.output", outputItems) + } return payload } @@ -475,7 +488,7 @@ func responsesCompletedOutputItem(index int, itemType string, st *interactionsTo if builder := st.TextOutputs[index]; builder != nil && builder.String() != "" { part := []byte(`{"type":"output_text","text":""}`) part, _ = sjson.SetBytes(part, "text", builder.String()) - item, _ = sjson.SetRawBytes(item, "content.-1", part) + item = translatorcommon.SetRawArrayItems(item, "content", [][]byte{part}) } return item, true case "thought": @@ -500,10 +513,15 @@ func responsesReasoningItem(index int, st *interactionsToResponsesStreamState) [ if signature := st.ReasoningEncrypted[index]; signature != "" { item, _ = sjson.SetBytes(item, "encrypted_content", signature) } - for _, text := range st.ReasoningSummaries[index] { - part := []byte(`{"type":"summary_text","text":""}`) - part, _ = sjson.SetBytes(part, "text", text) - item, _ = sjson.SetRawBytes(item, "summary.-1", part) + summaries := st.ReasoningSummaries[index] + if len(summaries) > 0 { + summaryBlocks := make([][]byte, 0, len(summaries)) + for _, text := range summaries { + part := []byte(`{"type":"summary_text","text":""}`) + part, _ = sjson.SetBytes(part, "text", text) + summaryBlocks = append(summaryBlocks, part) + } + item = translatorcommon.SetRawArrayItems(item, "summary", summaryBlocks) } return item } diff --git a/internal/translator/openai/openai/responses/openai_openai-responses_response.go b/internal/translator/openai/openai/responses/openai_openai-responses_response.go index dc412c08..c538c5db 100644 --- a/internal/translator/openai/openai/responses/openai_openai-responses_response.go +++ b/internal/translator/openai/openai/responses/openai_openai-responses_response.go @@ -885,7 +885,7 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponsesNonStream(_ context.Co } // Build output list from choices[...] - outputsWrapper := []byte(`{"arr":[]}`) + var outputItems [][]byte // Detect and capture reasoning content if present rcText := gjson.GetBytes(rawJSON, "choices.0.message.reasoning_content").String() includeReasoning := rcText != "" @@ -904,7 +904,7 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponsesNonStream(_ context.Co reasoningItem, _ = sjson.SetBytes(reasoningItem, "summary.0.type", "summary_text") reasoningItem, _ = sjson.SetBytes(reasoningItem, "summary.0.text", rcText) } - outputsWrapper, _ = sjson.SetRawBytes(outputsWrapper, "arr.-1", reasoningItem) + outputItems = append(outputItems, reasoningItem) } if choices := root.Get("choices"); choices.Exists() && choices.IsArray() { @@ -921,7 +921,7 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponsesNonStream(_ context.Co item, _ = sjson.SetBytes(item, "id", fmt.Sprintf("msg_%s_%d", id, int(choice.Get("index").Int()))) item, _ = sjson.SetBytes(item, "status", itemStatus) item, _ = sjson.SetBytes(item, "content.0.text", c.String()) - outputsWrapper, _ = sjson.SetRawBytes(outputsWrapper, "arr.-1", item) + outputItems = append(outputItems, item) } // Function/tool calls @@ -947,7 +947,7 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponsesNonStream(_ context.Co item, _ = sjson.SetBytes(item, "input", unwrapCustomToolInput(args)) item, _ = sjson.SetBytes(item, "call_id", callID) item = applyResponsesFunctionCallNamespaceFields(item, requestForNamespace, name, "") - outputsWrapper, _ = sjson.SetRawBytes(outputsWrapper, "arr.-1", item) + outputItems = append(outputItems, item) return true } item := []byte(`{"id":"","type":"function_call","status":"completed","arguments":"","call_id":"","name":""}`) @@ -956,7 +956,7 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponsesNonStream(_ context.Co item, _ = sjson.SetBytes(item, "arguments", args) item, _ = sjson.SetBytes(item, "call_id", callID) item = applyResponsesFunctionCallNamespaceFields(item, requestForNamespace, name, "") - outputsWrapper, _ = sjson.SetRawBytes(outputsWrapper, "arr.-1", item) + outputItems = append(outputItems, item) return true }) } @@ -964,8 +964,8 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponsesNonStream(_ context.Co return true }) } - if gjson.GetBytes(outputsWrapper, "arr.#").Int() > 0 { - resp, _ = sjson.SetRawBytes(resp, "output", []byte(gjson.GetBytes(outputsWrapper, "arr").Raw)) + if len(outputItems) > 0 { + resp, _ = sjson.SetRawBytes(resp, "output", translatorcommon.JoinRawArray(outputItems)) } // usage mapping