Add projects

This commit is contained in:
Alois 2026-08-24 00:10:41 +02:00
commit 8b607dd700
Signed by: alois
SSH key fingerprint: SHA256:GBzT2DXvAuGV9XIV5W3WrzVpjU54FThmxHXdbz95J24
1802 changed files with 503346 additions and 2 deletions

View file

@ -0,0 +1,28 @@
package responses
import (
. "github.com/router-for-me/CLIProxyAPI/v7/internal/constant"
"github.com/router-for-me/CLIProxyAPI/v7/internal/interfaces"
"github.com/router-for-me/CLIProxyAPI/v7/internal/translator/translator"
)
func init() {
translator.Register(
OpenaiResponse,
Interactions,
ConvertOpenAIResponsesRequestToInteractions,
interfaces.TranslateResponse{
Stream: ConvertInteractionsResponseToOpenAIResponses,
NonStream: ConvertInteractionsResponseToOpenAIResponsesNonStream,
},
)
translator.Register(
Interactions,
OpenaiResponse,
ConvertInteractionsRequestToOpenAIResponses,
interfaces.TranslateResponse{
Stream: ConvertOpenAIResponsesResponseToInteractions,
NonStream: ConvertOpenAIResponsesResponseToInteractionsNonStream,
},
)
}

View file

@ -0,0 +1,722 @@
package responses
import (
"strings"
translatorcommon "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/common"
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"
)
func ConvertOpenAIResponsesRequestToInteractions(modelName string, inputRawJSON []byte, stream bool) []byte {
root := gjson.ParseBytes(inputRawJSON)
out := []byte(`{"model":"","input":[]}`)
model := requestModel(modelName, root)
out, _ = sjson.SetBytes(out, "model", model)
if streamValue, ok := requestStreamValue(root, stream); ok {
out, _ = sjson.SetBytes(out, "stream", streamValue)
}
if instructions := root.Get("instructions"); instructions.Exists() {
out, _ = sjson.SetBytes(out, "system_instruction", responsesInstructionsText(instructions))
}
if previousResponseID := firstNonEmpty(root.Get("previous_response_id").String(), root.Get("previous_interaction_id").String()); previousResponseID != "" {
out, _ = sjson.SetBytes(out, "previous_interaction_id", previousResponseID)
}
if environmentID := firstNonEmpty(root.Get("environment_id").String(), root.Get("environment.id").String()); environmentID != "" {
out, _ = sjson.SetBytes(out, "environment_id", environmentID)
}
if agentConfig := root.Get("agent_config"); agentConfig.Exists() {
out, _ = sjson.SetRawBytes(out, "agent_config", []byte(agentConfig.Raw))
}
if input := root.Get("input"); input.Exists() {
out = setResponsesInputOnInteractions(out, input)
}
out = appendResponsesToolsToInteractions(out, root.Get("tools"))
if toolChoice := root.Get("tool_choice"); toolChoice.Exists() {
out, _ = sjson.SetRawBytes(out, "generation_config.tool_choice", []byte(toolChoice.Raw))
}
if effort := root.Get("reasoning.effort"); effort.Exists() && effort.Type == gjson.String {
out, _ = sjson.SetBytes(out, "generation_config.thinking_level", strings.ToLower(strings.TrimSpace(effort.String())))
}
if summary := root.Get("reasoning.summary"); summary.Exists() && summary.Type == gjson.String {
out, _ = sjson.SetBytes(out, "generation_config.thinking_summaries", summary.String())
}
if format := root.Get("response_format"); format.Exists() {
out, _ = sjson.SetRawBytes(out, "response_format", []byte(format.Raw))
} else if format := root.Get("text.format"); format.Exists() {
out, _ = sjson.SetRawBytes(out, "response_format", []byte(format.Raw))
}
if isAntigravityModel(model) {
if maxOutputTokens := firstExisting(root.Get("max_output_tokens"), root.Get("max_tokens"), root.Get("max_completion_tokens")); maxOutputTokens.Exists() && !root.Get("agent_config.max_total_tokens").Exists() {
out, _ = sjson.SetBytes(out, "agent_config.max_total_tokens", maxOutputTokens.Int())
}
for _, knob := range []string{"temperature", "top_p", "top_k", "stop_sequences", "max_output_tokens", "presence_penalty", "frequency_penalty", "candidate_count"} {
out, _ = sjson.DeleteBytes(out, "generation_config."+knob)
}
}
return out
}
func ConvertInteractionsRequestToOpenAIResponses(modelName string, inputRawJSON []byte, stream bool) []byte {
root := gjson.ParseBytes(inputRawJSON)
out := []byte(`{"model":"","input":[]}`)
out, _ = sjson.SetBytes(out, "model", requestModel(modelName, root))
if stream || root.Get("stream").Bool() {
out, _ = sjson.SetBytes(out, "stream", true)
}
if instructions := interactionsSystemInstructionText(root); instructions != "" {
out, _ = sjson.SetBytes(out, "instructions", instructions)
}
if previousInteractionID := firstNonEmpty(root.Get("previous_interaction_id").String(), root.Get("previous_response_id").String()); previousInteractionID != "" {
out, _ = sjson.SetBytes(out, "previous_response_id", previousInteractionID)
}
if environmentID := firstNonEmpty(root.Get("environment_id").String(), root.Get("environment.id").String()); environmentID != "" {
out, _ = sjson.SetBytes(out, "environment_id", environmentID)
}
if agentConfig := root.Get("agent_config"); agentConfig.Exists() {
out, _ = sjson.SetRawBytes(out, "agent_config", []byte(agentConfig.Raw))
}
if input := root.Get("input"); input.Exists() {
out = setInteractionsInputOnResponses(out, input)
}
out = appendInteractionsToolsToResponses(out, root.Get("tools"))
if toolChoice := root.Get("generation_config.tool_choice"); toolChoice.Exists() {
out, _ = sjson.SetRawBytes(out, "tool_choice", []byte(toolChoice.Raw))
} else if toolChoice := root.Get("tool_choice"); toolChoice.Exists() {
out, _ = sjson.SetRawBytes(out, "tool_choice", []byte(toolChoice.Raw))
}
if effort := interactionsThinkingEffort(root); effort != "" {
out, _ = sjson.SetBytes(out, "reasoning.effort", effort)
}
if summary := root.Get("generation_config.thinking_summaries"); summary.Exists() && summary.Type == gjson.String {
out, _ = sjson.SetBytes(out, "reasoning.summary", summary.String())
}
if responseModalities := root.Get("response_modalities"); responseModalities.Exists() {
out, _ = sjson.SetRawBytes(out, "modalities", []byte(responseModalities.Raw))
}
if serviceTier := root.Get("service_tier"); serviceTier.Exists() && serviceTier.Type == gjson.String {
out, _ = sjson.SetBytes(out, "service_tier", serviceTier.String())
}
if format := root.Get("response_format"); format.Exists() {
out, _ = sjson.SetRawBytes(out, "text.format", []byte(format.Raw))
}
return out
}
func requestModel(modelName string, root gjson.Result) string {
if strings.TrimSpace(modelName) != "" {
return modelName
}
return root.Get("model").String()
}
func requestStreamValue(root gjson.Result, stream bool) (bool, bool) {
if value := root.Get("stream"); value.Exists() {
return value.Bool(), true
}
if stream {
return true, true
}
return false, false
}
func responsesInstructionsText(instructions gjson.Result) string {
if instructions.Type == gjson.String {
return instructions.String()
}
if text := instructions.Get("text"); text.Exists() {
return text.String()
}
if parts := instructions.Get("content"); parts.Exists() && parts.IsArray() {
var builder strings.Builder
parts.ForEach(func(_, part gjson.Result) bool {
if text := part.Get("text").String(); text != "" {
builder.WriteString(text)
}
return true
})
return builder.String()
}
return instructions.String()
}
func interactionsSystemInstructionText(root gjson.Result) string {
sys := root.Get("system_instruction")
if !sys.Exists() {
return ""
}
if sys.Type == gjson.String {
return sys.String()
}
if text := sys.Get("text"); text.Exists() {
return text.String()
}
if parts := sys.Get("parts"); parts.Exists() && parts.IsArray() {
var builder strings.Builder
parts.ForEach(func(_, part gjson.Result) bool {
if text := part.Get("text").String(); text != "" {
builder.WriteString(text)
}
return true
})
return builder.String()
}
return ""
}
func interactionsThinkingEffort(root gjson.Result) string {
for _, path := range []string{
"generation_config.thinking_level",
"generation_config.thinkingConfig.thinkingLevel",
"generation_config.thinkingConfig.thinking_level",
"generation_config.thinking_config.thinking_level",
} {
if level := root.Get(path); level.Exists() && level.Type == gjson.String {
return strings.ToLower(strings.TrimSpace(level.String()))
}
}
return ""
}
func setResponsesInputOnInteractions(out []byte, input gjson.Result) []byte {
functionNamesByCallID := make(map[string]string)
items := make([][]byte, 0)
if input.Type == gjson.String {
items = append(items, interactionsTextStep("user_input", input.String()))
} else if input.IsArray() {
input.ForEach(func(_, item gjson.Result) bool {
if converted := responsesInputItemToInteractions(item, functionNamesByCallID); converted != nil {
items = append(items, converted)
}
return true
})
} else if input.IsObject() {
if converted := responsesInputItemToInteractions(input, functionNamesByCallID); converted != nil {
items = append(items, converted)
}
}
if len(items) > 0 {
out, _ = sjson.SetRawBytes(out, "input", translatorcommon.JoinRawArray(items))
}
return out
}
func responsesInputItemToInteractions(item gjson.Result, functionNamesByCallID map[string]string) []byte {
switch item.Get("type").String() {
case "message":
stepType := "user_input"
if role := item.Get("role").String(); role == "assistant" || role == "model" {
stepType = "model_output"
}
step := []byte(`{"type":"","content":[]}`)
step, _ = sjson.SetBytes(step, "type", stepType)
return appendResponsesContentToInteractions(step, item.Get("content"))
case "function_call":
callID := firstNonEmpty(item.Get("call_id").String(), item.Get("id").String())
if callID != "" {
if name := item.Get("name").String(); name != "" {
functionNamesByCallID[callID] = name
}
}
return responsesFunctionCallToInteractions(item)
case "function_call_output":
return responsesFunctionOutputToInteractions(item, functionNamesByCallID)
case "input_text", "output_text", "text":
stepType := "user_input"
if item.Get("type").String() == "output_text" {
stepType = "model_output"
}
return interactionsTextStep(stepType, item.Get("text").String())
case "input_image", "output_image":
stepType := "user_input"
if item.Get("type").String() == "output_image" {
stepType = "model_output"
}
step := []byte(`{"type":"","content":[]}`)
step, _ = sjson.SetBytes(step, "type", stepType)
if part, ok := responsesContentPartToInteractions(item); ok {
step = translatorcommon.SetRawArrayItems(step, "content", [][]byte{part})
}
return step
default:
if content := item.Get("content"); content.Exists() {
step := []byte(`{"type":"user_input","content":[]}`)
return appendResponsesContentToInteractions(step, content)
}
}
return nil
}
func appendResponsesContentToInteractions(step []byte, content gjson.Result) []byte {
var contentItems [][]byte
if content.Type == gjson.String {
part := []byte(`{"type":"text","text":""}`)
part, _ = sjson.SetBytes(part, "text", content.String())
contentItems = append(contentItems, part)
} else if content.IsArray() {
content.ForEach(func(_, item gjson.Result) bool {
if part, ok := responsesContentPartToInteractions(item); ok {
contentItems = append(contentItems, part)
}
return true
})
} else if content.IsObject() {
if part, ok := responsesContentPartToInteractions(content); ok {
contentItems = append(contentItems, part)
}
}
if len(contentItems) > 0 {
step = translatorcommon.SetRawArrayItems(step, "content", contentItems)
}
return step
}
func responsesContentPartToInteractions(part gjson.Result) ([]byte, bool) {
switch part.Get("type").String() {
case "input_text", "output_text", "text":
out := []byte(`{"type":"text","text":""}`)
out, _ = sjson.SetBytes(out, "text", part.Get("text").String())
return out, true
case "input_image", "output_image":
return responsesImagePartToInteractions(part), true
}
if text := part.Get("text"); text.Exists() {
out := []byte(`{"type":"text","text":""}`)
out, _ = sjson.SetBytes(out, "text", text.String())
return out, true
}
return nil, false
}
func responsesImagePartToInteractions(part gjson.Result) []byte {
out := []byte(`{"type":"image"}`)
imageURL := firstNonEmpty(part.Get("image_url").String(), part.Get("url").String())
if mimeType, data, ok := parseDataURL(imageURL); ok {
out, _ = sjson.SetBytes(out, "mime_type", mimeType)
out, _ = sjson.SetBytes(out, "data", data)
return out
}
if data := part.Get("data").String(); data != "" {
out, _ = sjson.SetBytes(out, "data", data)
if mimeType := part.Get("mime_type").String(); mimeType != "" {
out, _ = sjson.SetBytes(out, "mime_type", mimeType)
}
return out
}
if imageURL != "" {
out, _ = sjson.SetBytes(out, "image_url", imageURL)
}
return out
}
func responsesFunctionCallToInteractions(item gjson.Result) []byte {
out := []byte(`{"type":"function_call","name":"","arguments":{}}`)
out, _ = sjson.SetBytes(out, "name", item.Get("name").String())
if callID := firstNonEmpty(item.Get("call_id").String(), item.Get("id").String()); callID != "" {
out, _ = sjson.SetBytes(out, "call_id", callID)
}
setJSONValue(&out, "arguments", item.Get("arguments"), []byte(`{}`))
return out
}
func responsesFunctionOutputToInteractions(item gjson.Result, functionNamesByCallID map[string]string) []byte {
out := []byte(`{"type":"function_result","name":"","result":{}}`)
callID := firstNonEmpty(item.Get("call_id").String(), item.Get("id").String())
if name := item.Get("name").String(); name != "" {
out, _ = sjson.SetBytes(out, "name", name)
} else if name := functionNamesByCallID[callID]; name != "" {
out, _ = sjson.SetBytes(out, "name", name)
}
if callID != "" {
out, _ = sjson.SetBytes(out, "call_id", callID)
}
result := item.Get("output")
if !result.Exists() {
result = item.Get("result")
}
setJSONValue(&out, "result", result, []byte(`{}`))
return out
}
func interactionsTextStep(stepType, text string) []byte {
step := []byte(`{"type":"","content":[{"type":"text","text":""}]}`)
step, _ = sjson.SetBytes(step, "type", stepType)
step, _ = sjson.SetBytes(step, "content.0.text", text)
return step
}
func appendResponsesToolsToInteractions(out []byte, tools gjson.Result) []byte {
if !tools.Exists() || !tools.IsArray() {
return out
}
var toolItems [][]byte
tools.ForEach(func(_, tool gjson.Result) bool {
switch tool.Get("type").String() {
case "function", "":
if converted, ok := functionToolToInteractions(tool); ok {
toolItems = append(toolItems, converted)
}
case "namespace":
declarationItems := make([][]byte, 0, 4)
children := tool.Get("children")
if !children.Exists() {
children = tool.Get("tools")
}
children.ForEach(func(_, child gjson.Result) bool {
if converted, ok := functionDeclarationFromTool(child); ok {
declarationItems = append(declarationItems, converted)
}
return true
})
if len(declarationItems) > 0 {
group := []byte(`{"function_declarations":[]}`)
group, _ = sjson.SetRawBytes(group, "function_declarations", translatorcommon.JoinRawArray(declarationItems))
toolItems = append(toolItems, group)
}
}
return true
})
if len(toolItems) > 0 {
out, _ = sjson.SetRawBytes(out, "tools", translatorcommon.JoinRawArray(toolItems))
}
return out
}
func functionToolToInteractions(tool gjson.Result) ([]byte, bool) {
name := firstNonEmpty(tool.Get("name").String(), tool.Get("function.name").String())
if name == "" {
return nil, false
}
out := []byte(`{"type":"function","name":""}`)
out, _ = sjson.SetBytes(out, "name", name)
copyOptionalString(&out, "description", firstExisting(tool.Get("description"), tool.Get("function.description")))
copyOptionalRaw(&out, "parameters", firstExisting(tool.Get("parameters"), tool.Get("function.parameters")))
return out, true
}
func functionDeclarationFromTool(tool gjson.Result) ([]byte, bool) {
name := firstNonEmpty(tool.Get("name").String(), tool.Get("function.name").String())
if name == "" {
return nil, false
}
out := []byte(`{"name":""}`)
out, _ = sjson.SetBytes(out, "name", name)
copyOptionalString(&out, "description", firstExisting(tool.Get("description"), tool.Get("function.description")))
copyOptionalRaw(&out, "parameters", firstExisting(tool.Get("parameters"), tool.Get("function.parameters")))
return out, true
}
func setInteractionsInputOnResponses(out []byte, input gjson.Result) []byte {
items := make([][]byte, 0)
if input.Type == gjson.String {
items = append(items, interactionsTextMessage(input.String()))
} else if input.IsArray() {
input.ForEach(func(_, item gjson.Result) bool {
if converted := interactionsInputItemToResponses(item); converted != nil {
items = append(items, converted)
}
return true
})
} else if input.IsObject() {
if converted := interactionsInputItemToResponses(input); converted != nil {
items = append(items, converted)
}
}
if len(items) > 0 {
out, _ = sjson.SetRawBytes(out, "input", translatorcommon.JoinRawArray(items))
}
return out
}
func interactionsTextMessage(text string) []byte {
item := []byte(`{"type":"message","role":"user","content":[{"type":"input_text","text":""}]}`)
item, _ = sjson.SetBytes(item, "content.0.text", text)
return item
}
func interactionsInputItemToResponses(item gjson.Result) []byte {
switch item.Get("type").String() {
case "user_input":
return interactionsMessageToResponses(item, "user")
case "model_output":
return interactionsMessageToResponses(item, "assistant")
case "thought":
return interactionsThoughtToResponses(item)
case "function_call":
return interactionsFunctionCallToResponses(item)
case "function_result":
return interactionsFunctionResultToResponses(item)
default:
if item.Type == gjson.String {
return interactionsTextMessage(item.String())
}
}
return nil
}
func interactionsMessageToResponses(item gjson.Result, role string) []byte {
var contentItems [][]byte
content := item.Get("content")
if content.Type == gjson.String {
partType := "input_text"
if role == "assistant" {
partType = "output_text"
}
part := []byte(`{"type":"","text":""}`)
part, _ = sjson.SetBytes(part, "type", partType)
part, _ = sjson.SetBytes(part, "text", content.String())
contentItems = append(contentItems, part)
} else {
content.ForEach(func(_, part gjson.Result) bool {
if converted, ok := interactionsContentPartToResponses(part, role); ok {
contentItems = append(contentItems, converted)
}
return true
})
}
out := []byte(`{"type":"message","role":"","content":[]}`)
out, _ = sjson.SetBytes(out, "role", role)
out = translatorcommon.SetRawArrayItems(out, "content", contentItems)
return out
}
func interactionsThoughtToResponses(item gjson.Result) []byte {
var summaryItems [][]byte
for _, text := range interactionsContentTexts(item.Get("content")) {
part := []byte(`{"type":"summary_text","text":""}`)
part, _ = sjson.SetBytes(part, "text", text)
summaryItems = append(summaryItems, part)
}
out := []byte(`{"type":"reasoning","summary":[]}`)
out = translatorcommon.SetRawArrayItems(out, "summary", summaryItems)
return out
}
func interactionsContentPartToResponses(part gjson.Result, role string) ([]byte, bool) {
partType := part.Get("type").String()
if partType == "" && part.Get("text").Exists() {
partType = "text"
}
switch partType {
case "text":
outType := "input_text"
if role == "assistant" {
outType = "output_text"
}
out := []byte(`{"type":"","text":""}`)
out, _ = sjson.SetBytes(out, "type", outType)
out, _ = sjson.SetBytes(out, "text", part.Get("text").String())
return out, true
case "image":
outType := "input_image"
if role == "assistant" {
outType = "output_image"
}
out := []byte(`{"type":""}`)
out, _ = sjson.SetBytes(out, "type", outType)
imageURL := interactionsMediaDataURL(part)
if imageURL != "" {
out, _ = sjson.SetBytes(out, "image_url", imageURL)
}
return out, true
case "audio":
out := []byte(`{"type":"output_text","text":""}`)
format := mediaFormat(part.Get("mime_type").String())
out, _ = sjson.SetBytes(out, "text", "Audio content: inline data (Format: "+format+")")
return out, true
case "video", "document":
outType := "input_file"
if role == "assistant" {
outType = "output_file"
}
out := []byte(`{"type":""}`)
out, _ = sjson.SetBytes(out, "type", outType)
if dataURL := interactionsMediaDataURL(part); dataURL != "" {
out, _ = sjson.SetBytes(out, "file_data", dataURL)
}
if filename := part.Get("filename").String(); filename != "" {
out, _ = sjson.SetBytes(out, "filename", filename)
}
return out, true
}
return nil, false
}
func interactionsFunctionCallToResponses(item gjson.Result) []byte {
out := []byte(`{"type":"function_call","call_id":"","name":"","arguments":"{}"}`)
if callID := firstNonEmpty(item.Get("call_id").String(), item.Get("id").String()); callID != "" {
out, _ = sjson.SetBytes(out, "call_id", callID)
}
out, _ = sjson.SetBytes(out, "name", item.Get("name").String())
out, _ = sjson.SetBytes(out, "arguments", jsonStringValue(item.Get("arguments"), "{}"))
return out
}
func interactionsFunctionResultToResponses(item gjson.Result) []byte {
out := []byte(`{"type":"function_call_output","call_id":"","output":""}`)
if callID := firstNonEmpty(item.Get("call_id").String(), item.Get("id").String()); callID != "" {
out, _ = sjson.SetBytes(out, "call_id", callID)
}
if name := item.Get("name").String(); name != "" {
out, _ = sjson.SetBytes(out, "name", name)
}
result := item.Get("result")
if !result.Exists() {
result = item.Get("output")
}
out, _ = sjson.SetBytes(out, "output", jsonStringValue(result, ""))
return out
}
func appendInteractionsToolsToResponses(out []byte, tools gjson.Result) []byte {
if !tools.Exists() || !tools.IsArray() {
return out
}
var toolItems [][]byte
tools.ForEach(func(_, tool gjson.Result) bool {
if converted, ok := responsesToolFromInteractionsTool(tool); ok {
toolItems = append(toolItems, converted)
}
if decls := tool.Get("function_declarations"); decls.Exists() && decls.IsArray() {
decls.ForEach(func(_, decl gjson.Result) bool {
if converted, ok := responsesToolFromInteractionsTool(decl); ok {
toolItems = append(toolItems, converted)
}
return true
})
}
return true
})
if len(toolItems) > 0 {
out, _ = sjson.SetRawBytes(out, "tools", translatorcommon.JoinRawArray(toolItems))
}
return out
}
func responsesToolFromInteractionsTool(tool gjson.Result) ([]byte, bool) {
name := firstNonEmpty(tool.Get("name").String(), tool.Get("function.name").String())
if name == "" {
return nil, false
}
out := []byte(`{"type":"function","name":""}`)
out, _ = sjson.SetBytes(out, "name", name)
copyOptionalString(&out, "description", firstExisting(tool.Get("description"), tool.Get("function.description")))
copyOptionalRaw(&out, "parameters", firstExisting(tool.Get("parameters"), tool.Get("function.parameters"), tool.Get("parametersJsonSchema")))
return out, true
}
func interactionsContentTexts(content gjson.Result) []string {
texts := make([]string, 0)
if content.Type == gjson.String {
return append(texts, content.String())
}
if content.IsArray() {
content.ForEach(func(_, part gjson.Result) bool {
if text := firstNonEmpty(part.Get("text").String(), part.Get("content.text").String()); text != "" {
texts = append(texts, text)
}
return true
})
}
return texts
}
func interactionsMediaDataURL(part gjson.Result) string {
if url := firstNonEmpty(part.Get("image_url").String(), part.Get("file_data").String(), part.Get("url").String()); url != "" {
return url
}
data := part.Get("data").String()
if data == "" {
return ""
}
mimeType := part.Get("mime_type").String()
if mimeType == "" {
mimeType = "application/octet-stream"
}
return "data:" + mimeType + ";base64," + data
}
func mediaFormat(mimeType string) string {
if mimeType == "" {
return "unknown"
}
if _, format, ok := strings.Cut(mimeType, "/"); ok && format != "" {
return format
}
return mimeType
}
func parseDataURL(value string) (string, string, bool) {
if !strings.HasPrefix(value, "data:") {
return "", "", false
}
header, data, ok := strings.Cut(strings.TrimPrefix(value, "data:"), ",")
if !ok {
return "", "", false
}
mimeType, _, _ := strings.Cut(header, ";")
if mimeType == "" {
mimeType = "application/octet-stream"
}
return mimeType, data, true
}
func setJSONValue(out *[]byte, path string, value gjson.Result, defaultRaw []byte) {
if !value.Exists() {
*out, _ = sjson.SetRawBytes(*out, path, defaultRaw)
return
}
if value.Type == gjson.String && gjson.Valid(value.String()) {
*out, _ = sjson.SetRawBytes(*out, path, []byte(value.String()))
return
}
if value.Type == gjson.String {
*out, _ = sjson.SetBytes(*out, path, value.String())
return
}
*out, _ = sjson.SetRawBytes(*out, path, []byte(value.Raw))
}
func jsonStringValue(value gjson.Result, fallback string) string {
if !value.Exists() {
return fallback
}
if value.Type == gjson.String {
return value.String()
}
return value.Raw
}
func copyOptionalString(out *[]byte, path string, value gjson.Result) {
if value.Exists() {
*out, _ = sjson.SetBytes(*out, path, value.String())
}
}
func copyOptionalRaw(out *[]byte, path string, value gjson.Result) {
if value.Exists() {
*out, _ = sjson.SetRawBytes(*out, path, []byte(value.Raw))
}
}
func isAntigravityModel(model string) bool {
return strings.Contains(strings.ToLower(model), "antigravity")
}
func firstExisting(values ...gjson.Result) gjson.Result {
for _, value := range values {
if value.Exists() {
return value
}
}
return gjson.Result{}
}
func firstNonEmpty(values ...string) string {
for _, value := range values {
if strings.TrimSpace(value) != "" {
return value
}
}
return ""
}

View file

@ -0,0 +1,347 @@
package responses
import (
"testing"
"github.com/tidwall/gjson"
)
func TestConvertOpenAIResponsesRequestToInteractions(t *testing.T) {
raw := []byte(`{
"model":"gpt-test",
"instructions":"be brief",
"input":[
{"type":"message","role":"user","content":[{"type":"input_text","text":"hi"},{"type":"input_image","image_url":"data:image/png;base64,aGVsbG8="}]},
{"type":"function_call","name":"lookup","call_id":"call_1","arguments":"{\"q\":\"x\"}"},
{"type":"function_call_output","call_id":"call_1","output":{"ok":true}}
],
"tools":[{"type":"function","name":"lookup","parameters":{"type":"object"}}],
"tool_choice":"auto",
"reasoning":{"effort":"high","summary":"auto"},
"response_format":{"type":"json_object"},
"stream":true
}`)
out := ConvertOpenAIResponsesRequestToInteractions("gpt-test", raw, true)
if got := gjson.GetBytes(out, "input.0.type").String(); got != "user_input" {
t.Fatalf("input.0.type = %q, want user_input. Output: %s", got, string(out))
}
if got := gjson.GetBytes(out, "input.0.content.0.type").String(); got != "text" {
t.Fatalf("content.0.type = %q, want text. Output: %s", got, string(out))
}
if got := gjson.GetBytes(out, "input.0.content.0.text").String(); got != "hi" {
t.Fatalf("input text = %q, want hi. Output: %s", got, string(out))
}
if got := gjson.GetBytes(out, "input.0.content.1.mime_type").String(); got != "image/png" {
t.Fatalf("image mime_type = %q, want image/png. Output: %s", got, string(out))
}
if got := gjson.GetBytes(out, "input.1.call_id").String(); got != "call_1" {
t.Fatalf("function call_id = %q, want call_1. Output: %s", got, string(out))
}
if got := gjson.GetBytes(out, "input.2.type").String(); got != "function_result" {
t.Fatalf("function result type = %q, want function_result. Output: %s", got, string(out))
}
if got := gjson.GetBytes(out, "input.2.name").String(); got != "lookup" {
t.Fatalf("function result name = %q, want lookup. Output: %s", got, string(out))
}
sys := gjson.GetBytes(out, "system_instruction")
if sys.Type != gjson.String {
t.Fatalf("system_instruction type = %v, want string. Output: %s", sys.Type, string(out))
}
if got := sys.String(); got != "be brief" {
t.Fatalf("system_instruction = %q, want be brief. Output: %s", got, string(out))
}
if gjson.GetBytes(out, "system_instruction.parts").Exists() {
t.Fatalf("system_instruction.parts should not be forwarded. Output: %s", string(out))
}
if got := gjson.GetBytes(out, "generation_config.thinking_level").String(); got != "high" {
t.Fatalf("thinking_level = %q, want high. Output: %s", got, string(out))
}
if got := gjson.GetBytes(out, "tools.0.name").String(); got != "lookup" {
t.Fatalf("tool name = %q, want lookup. Output: %s", got, string(out))
}
if got := gjson.GetBytes(out, "generation_config.tool_choice").String(); got != "auto" {
t.Fatalf("tool_choice = %q, want auto. Output: %s", got, string(out))
}
if got := gjson.GetBytes(out, "response_format.type").String(); got != "json_object" {
t.Fatalf("response_format.type = %q, want json_object. Output: %s", got, string(out))
}
}
func TestConvertOpenAIResponsesRequestToInteractionsPreservesRequestStream(t *testing.T) {
out := ConvertOpenAIResponsesRequestToInteractions("gpt-test", []byte(`{"model":"gpt-test","input":"hi","stream":true}`), false)
if got := gjson.GetBytes(out, "stream").Bool(); !got {
t.Fatalf("stream = %v, want true. Output: %s", got, string(out))
}
out = ConvertOpenAIResponsesRequestToInteractions("gpt-test", []byte(`{"model":"gpt-test","input":"hi","stream":false}`), true)
if got := gjson.GetBytes(out, "stream").Bool(); got {
t.Fatalf("stream = %v, want false. Output: %s", got, string(out))
}
}
func TestConvertOpenAIResponsesRequestToInteractionsPreservesPreviousResponseID(t *testing.T) {
out := ConvertOpenAIResponsesRequestToInteractions("gpt-test", []byte(`{"model":"gpt-test","input":"hi","previous_response_id":"resp_123"}`), false)
if got := gjson.GetBytes(out, "previous_interaction_id").String(); got != "resp_123" {
t.Fatalf("previous_interaction_id = %q, want resp_123. Output: %s", got, string(out))
}
}
func TestConvertInteractionsRequestToOpenAIResponsesWithToolMessages(t *testing.T) {
raw := []byte(`{"model":"gpt-test","input":[{"type":"user_input","content":[{"type":"text","text":"hi"}]},{"type":"function_call","name":"lookup","call_id":"call_1","arguments":{"q":"x"}},{"type":"function_result","name":"lookup","call_id":"call_1","result":{"ok":true}}]}`)
out := ConvertInteractionsRequestToOpenAIResponses("gpt-test", raw, false)
foundFunctionCall := false
foundFunctionOutput := false
gjson.GetBytes(out, "input").ForEach(func(_, item gjson.Result) bool {
if item.Get("type").String() == "function_call" {
foundFunctionCall = true
if item.Get("name").String() != "lookup" {
t.Fatalf("name = %q, want lookup", item.Get("name").String())
}
}
if item.Get("type").String() == "function_call_output" {
foundFunctionOutput = true
}
return true
})
if !foundFunctionCall {
t.Fatal("function_call input not found")
}
if !foundFunctionOutput {
t.Fatal("function_call_output input not found")
}
}
func TestConvertInteractionsRequestToOpenAIResponsesPreservesStringSystemAndThinkingConfig(t *testing.T) {
raw := []byte(`{"model":"gpt-test","system_instruction":"You are a helpful assistant.","input":[{"type":"user_input","content":[{"type":"text","text":"hi"}]}],"tools":[{"name":"lookup","type":"function","parameters":{"type":"object"}}],"generation_config":{"tool_choice":"auto","thinking_level":"high","thinking_summaries":"auto"},"stream":true}`)
out := ConvertInteractionsRequestToOpenAIResponses("gpt-test", raw, true)
if got := gjson.GetBytes(out, "instructions").String(); got != "You are a helpful assistant." {
t.Fatalf("instructions = %q, want system instruction. Output: %s", got, string(out))
}
if got := gjson.GetBytes(out, "tool_choice").String(); got != "auto" {
t.Fatalf("tool_choice = %q, want auto. Output: %s", got, string(out))
}
if got := gjson.GetBytes(out, "reasoning.effort").String(); got != "high" {
t.Fatalf("reasoning.effort = %q, want high. Output: %s", got, string(out))
}
if got := gjson.GetBytes(out, "reasoning.summary").String(); got != "auto" {
t.Fatalf("reasoning.summary = %q, want auto. Output: %s", got, string(out))
}
}
func TestConvertInteractionsRequestToOpenAIResponsesPreservesInteractionStream(t *testing.T) {
out := ConvertInteractionsRequestToOpenAIResponses("gpt-test", []byte(`{"model":"gpt-test","input":"hi","stream":true}`), false)
if got := gjson.GetBytes(out, "stream").Bool(); !got {
t.Fatalf("stream = %v, want true. Output: %s", got, string(out))
}
}
func TestConvertInteractionsRequestToOpenAIResponsesPreservesPreviousInteractionID(t *testing.T) {
out := ConvertInteractionsRequestToOpenAIResponses("gpt-test", []byte(`{"model":"gpt-test","input":"hi","previous_interaction_id":"interaction_123"}`), false)
if got := gjson.GetBytes(out, "previous_response_id").String(); got != "interaction_123" {
t.Fatalf("previous_response_id = %q, want interaction_123. Output: %s", got, string(out))
}
}
func TestConvertInteractionsRequestToOpenAIResponsesPreservesToolCallID(t *testing.T) {
out := ConvertInteractionsRequestToOpenAIResponses("gpt-test", []byte(`{"model":"gpt-test","input":[{"type":"function_call","name":"lookup","call_id":"call_gateway","arguments":{"q":"x"}},{"type":"function_result","name":"lookup","call_id":"call_gateway","result":{"ok":true}}]}`), false)
foundFunctionCall := false
foundFunctionOutput := false
gjson.GetBytes(out, "input").ForEach(func(_, item gjson.Result) bool {
switch item.Get("type").String() {
case "function_call":
foundFunctionCall = true
if got := item.Get("call_id").String(); got != "call_gateway" {
t.Fatalf("function_call call_id = %q, want call_gateway. Output: %s", got, string(out))
}
case "function_call_output":
foundFunctionOutput = true
if got := item.Get("call_id").String(); got != "call_gateway" {
t.Fatalf("function_call_output call_id = %q, want call_gateway. Output: %s", got, string(out))
}
}
return true
})
if !foundFunctionCall {
t.Fatal("function_call input not found")
}
if !foundFunctionOutput {
t.Fatal("function_call_output input not found")
}
}
func TestConvertInteractionsRequestToOpenAIResponsesConvertsSimpleTools(t *testing.T) {
out := ConvertInteractionsRequestToOpenAIResponses("gpt-test", []byte(`{"model":"gpt-test","tools":[{"name":"lookup","description":"Find data","parameters":{"type":"object","properties":{"q":{"type":"string"}}}}],"input":"hi"}`), false)
if got := gjson.GetBytes(out, "tools.0.type").String(); got != "function" {
t.Fatalf("tools.0.type = %q, want function. Output: %s", got, string(out))
}
if got := gjson.GetBytes(out, "tools.0.name").String(); got != "lookup" {
t.Fatalf("tools.0.name = %q, want lookup. Output: %s", got, string(out))
}
if gjson.GetBytes(out, "tools.0.function").Exists() {
t.Fatalf("tools.0.function should not be forwarded. Output: %s", string(out))
}
if got := gjson.GetBytes(out, "tools.0.parameters.properties.q.type").String(); got != "string" {
t.Fatalf("tools.0.parameters.properties.q.type = %q, want string. Output: %s", got, string(out))
}
}
func TestConvertInteractionsRequestToOpenAIResponsesConvertsFunctionDeclarationsTools(t *testing.T) {
out := ConvertInteractionsRequestToOpenAIResponses("gpt-test", []byte(`{"model":"gpt-test","tools":[{"function_declarations":[{"name":"lookup","description":"Find data","parameters":{"type":"object","properties":{"q":{"type":"string"}}}}]}],"input":"hi"}`), false)
if got := gjson.GetBytes(out, "tools.0.type").String(); got != "function" {
t.Fatalf("tools.0.type = %q, want function. Output: %s", got, string(out))
}
if got := gjson.GetBytes(out, "tools.0.name").String(); got != "lookup" {
t.Fatalf("tools.0.name = %q, want lookup. Output: %s", got, string(out))
}
if gjson.GetBytes(out, "tools.0.function_declarations").Exists() {
t.Fatalf("tools.0.function_declarations should not be forwarded. Output: %s", string(out))
}
}
func TestConvertInteractionsRequestToOpenAIResponsesWithImageContent(t *testing.T) {
raw := []byte(`{"model":"gpt-test","input":[{"type":"user_input","content":[{"type":"text","text":"describe"},{"type":"image","mime_type":"image/png","data":"aGVsbG8="}]}]}`)
out := ConvertInteractionsRequestToOpenAIResponses("gpt-test", raw, false)
if got := gjson.GetBytes(out, "input.0.content.1.type").String(); got != "input_image" {
t.Fatalf("content.1.type = %q, want input_image", got)
}
if got := gjson.GetBytes(out, "input.0.content.1.image_url").String(); got != "data:image/png;base64,aGVsbG8=" {
t.Fatalf("image_url = %q, want data URL", got)
}
}
func TestConvertInteractionsRequestToOpenAIResponsesPreservesNonImageMediaContent(t *testing.T) {
out := ConvertInteractionsRequestToOpenAIResponses("gpt-test", []byte(`{"model":"gpt-test","input":[{"type":"model_output","content":[{"type":"audio","mime_type":"audio/wav","data":"UklGRg=="},{"type":"video","mime_type":"video/mp4","data":"AAAAIGZ0eXA="},{"type":"document","mime_type":"application/pdf","data":"JVBERi0="}]}]}`), false)
if got := gjson.GetBytes(out, "input.0.content.0.type").String(); got != "output_text" {
t.Fatalf("audio fallback type = %q, want output_text. Output: %s", got, string(out))
}
if got := gjson.GetBytes(out, "input.0.content.1.type").String(); got != "output_file" {
t.Fatalf("video type = %q, want output_file. Output: %s", got, string(out))
}
if got := gjson.GetBytes(out, "input.0.content.2.type").String(); got != "output_file" {
t.Fatalf("document type = %q, want output_file. Output: %s", got, string(out))
}
if gjson.GetBytes(out, "input.0.content.#(type==\"output_image\")").Exists() {
t.Fatalf("non-image media must not be converted to output_image. Output: %s", string(out))
}
}
func TestConvertInteractionsRequestToOpenAIResponsesWithAssistantTextContent(t *testing.T) {
raw := []byte(`{"model":"gpt-test","input":[{"type":"model_output","content":[{"type":"text","text":"hello"}]}]}`)
out := ConvertInteractionsRequestToOpenAIResponses("gpt-test", raw, false)
if got := gjson.GetBytes(out, "input.0.content.0.type").String(); got != "output_text" {
t.Fatalf("content.0.type = %q, want output_text", got)
}
if got := gjson.GetBytes(out, "input.0.content.0.text").String(); got != "hello" {
t.Fatalf("content.0.text = %q, want hello", got)
}
}
func TestConvertInteractionsRequestToOpenAIResponsesWithUserObjectContent(t *testing.T) {
raw := []byte(`{"model":"gpt-test","input":[{"type":"user_input","content":[{"type":"text","text":"hi"}]}]}`)
out := ConvertInteractionsRequestToOpenAIResponses("gpt-test", raw, false)
if got := gjson.GetBytes(out, "input.0.content.0.type").String(); got != "input_text" {
t.Fatalf("content.0.type = %q, want input_text", got)
}
if got := gjson.GetBytes(out, "input.0.content.0.text").String(); got != "hi" {
t.Fatalf("content.0.text = %q, want hi", got)
}
}
func TestConvertInteractionsRequestToOpenAIResponsesWithStringFunctionArguments(t *testing.T) {
raw := []byte(`{"model":"gpt-test","input":[{"type":"function_call","name":"lookup","call_id":"call_1","arguments":{"q":"x"}},{"type":"function_result","name":"lookup","call_id":"call_1","result":{"ok":true}}]}`)
out := ConvertInteractionsRequestToOpenAIResponses("gpt-test", raw, false)
found := false
gjson.GetBytes(out, "input").ForEach(func(_, item gjson.Result) bool {
if item.Get("type").String() == "function_call" {
found = true
if item.Get("arguments").Type != gjson.String {
t.Fatalf("arguments should be string, got %v", item.Get("arguments").Type)
}
if got := item.Get("arguments").String(); got != `{"q":"x"}` {
t.Fatalf("arguments = %q, want {\"q\":\"x\"}", got)
}
}
return true
})
if !found {
t.Fatal("function_call input not found")
}
}
func TestConvertInteractionsRequestToOpenAIResponsesPreservesExpressibleFields(t *testing.T) {
out := ConvertInteractionsRequestToOpenAIResponses("gpt-test", []byte(`{"model":"gpt-test","tool_choice":{"type":"function","function":{"name":"lookup"}},"response_modalities":["text","image"],"service_tier":"priority","store":true,"background":true,"webhook_config":{"url":"https://example.com"},"input":"hi"}`), false)
if got := gjson.GetBytes(out, "tool_choice.type").String(); got != "function" {
t.Fatalf("tool_choice.type = %q, want function. Output: %s", got, string(out))
}
if got := gjson.GetBytes(out, "tool_choice.function.name").String(); got != "lookup" {
t.Fatalf("tool_choice.function.name = %q, want lookup. Output: %s", got, string(out))
}
if got := gjson.GetBytes(out, "modalities.0").String(); got != "text" {
t.Fatalf("modalities.0 = %q, want text. Output: %s", got, string(out))
}
if got := gjson.GetBytes(out, "modalities.1").String(); got != "image" {
t.Fatalf("modalities.1 = %q, want image. Output: %s", got, string(out))
}
if got := gjson.GetBytes(out, "service_tier").String(); got != "priority" {
t.Fatalf("service_tier = %q, want priority. Output: %s", got, string(out))
}
for _, path := range []string{"store", "background", "webhook_config"} {
if gjson.GetBytes(out, path).Exists() {
t.Fatalf("%s should not be forwarded. Output: %s", path, string(out))
}
}
}
func TestConvertOpenAIResponsesRequestToInteractions_PreservesEnvironmentID(t *testing.T) {
out := ConvertOpenAIResponsesRequestToInteractions("gpt-test", []byte(`{"model":"gpt-test","input":"hi","previous_response_id":"resp_123","environment_id":"env_abc456"}`), false)
if got := gjson.GetBytes(out, "previous_interaction_id").String(); got != "resp_123" {
t.Fatalf("previous_interaction_id = %q, want resp_123. Output: %s", got, string(out))
}
if got := gjson.GetBytes(out, "environment_id").String(); got != "env_abc456" {
t.Fatalf("environment_id = %q, want env_abc456. Output: %s", got, string(out))
}
}
func TestConvertInteractionsRequestToOpenAIResponses_PreservesEnvironmentID(t *testing.T) {
out := ConvertInteractionsRequestToOpenAIResponses("gpt-test", []byte(`{"model":"gpt-test","input":"hi","previous_interaction_id":"interaction_123","environment_id":"env_abc456"}`), false)
if got := gjson.GetBytes(out, "previous_response_id").String(); got != "interaction_123" {
t.Fatalf("previous_response_id = %q, want interaction_123. Output: %s", got, string(out))
}
if got := gjson.GetBytes(out, "environment_id").String(); got != "env_abc456" {
t.Fatalf("environment_id = %q, want env_abc456. Output: %s", got, string(out))
}
}
func TestConvertOpenAIResponsesRequestToInteractions_AntigravitySanitizesGenerationConfigAndSetsAgentConfig(t *testing.T) {
raw := []byte(`{
"model":"antigravity-preview-05-2026",
"input":"Search the web",
"previous_response_id":"v1_Chd3...",
"environment_id":"env_789",
"max_output_tokens":2048,
"temperature":0.7,
"top_p":0.95,
"tools":[{"type":"function","name":"web_search","parameters":{"type":"object"}}]
}`)
out := ConvertOpenAIResponsesRequestToInteractions("antigravity-preview-05-2026", raw, false)
if got := gjson.GetBytes(out, "previous_interaction_id").String(); got != "v1_Chd3..." {
t.Fatalf("previous_interaction_id = %q, want v1_Chd3.... Output: %s", got, string(out))
}
if got := gjson.GetBytes(out, "environment_id").String(); got != "env_789" {
t.Fatalf("environment_id = %q, want env_789. Output: %s", got, string(out))
}
// temperature, top_p, max_output_tokens should be stripped from generation_config for Antigravity models
for _, knob := range []string{"temperature", "top_p", "top_k", "stop_sequences", "max_output_tokens"} {
if gjson.GetBytes(out, "generation_config."+knob).Exists() {
t.Fatalf("generation_config.%s should be stripped for antigravity model. Output: %s", knob, string(out))
}
}
// max_output_tokens should be mapped to agent_config.max_total_tokens
if got := gjson.GetBytes(out, "agent_config.max_total_tokens").Int(); got != 2048 {
t.Fatalf("agent_config.max_total_tokens = %d, want 2048. Output: %s", got, string(out))
}
}

View file

@ -0,0 +1,705 @@
package responses
import (
"bytes"
"context"
"encoding/base64"
"strings"
"testing"
"github.com/tidwall/gjson"
)
func TestConvertInteractionsResponseToOpenAIResponsesNonStream(t *testing.T) {
raw := []byte(`{"id":"interaction_1","object":"interaction","status":"completed","steps":[{"type":"model_output","content":[{"text":"ok"}]}],"usage":{"input_tokens":1,"output_tokens":2,"total_tokens":3}}`)
out := ConvertInteractionsResponseToOpenAIResponsesNonStream(context.Background(), "gpt-test", []byte(`{"model":"gpt-test"}`), nil, raw, nil)
if got := gjson.GetBytes(out, "output.0.content.0.text").String(); got != "ok" {
t.Fatalf("response text = %q, want ok. Output: %s", got, string(out))
}
if got := gjson.GetBytes(out, "usage.total_tokens").Int(); got != 3 {
t.Fatalf("usage.total_tokens = %d, want 3. Output: %s", got, string(out))
}
}
func TestConvertInteractionsResponseToOpenAIResponsesStream(t *testing.T) {
var param any
var out [][]byte
for _, raw := range [][]byte{
[]byte(`event: interaction.created
data: {"interaction":{"id":"interaction_1","model":"source-model"},"event_type":"interaction.created"}
`),
[]byte(`event: step.delta
data: {"index":0,"delta":{"content":{"text":"thinking","type":"text"},"type":"thought_summary"},"event_type":"step.delta"}
`),
[]byte(`event: step.delta
data: {"index":1,"delta":{"text":"I will call a tool.","type":"text"},"event_type":"step.delta"}
`),
[]byte(`event: step.start
data: {"index":2,"step":{"id":"call_1","type":"function_call","name":"get_weather","arguments":{}},"event_type":"step.start"}
`),
[]byte(`event: step.delta
data: {"index":2,"delta":{"arguments":"{\"location\":\"北京\"}","type":"arguments_delta"},"event_type":"step.delta"}
`),
[]byte(`event: step.stop
data: {"index":2,"event_type":"step.stop"}
`),
[]byte(`event: interaction.completed
data: {"interaction":{"id":"interaction_1","status":"completed","usage":{"total_tokens":399,"total_input_tokens":123,"total_cached_tokens":5,"total_output_tokens":36,"total_thought_tokens":240},"created":"2026-07-06T06:01:35Z","object":"interaction","model":"gpt-test"},"event_type":"interaction.completed"}
`),
[]byte(`event: done
data: [DONE]
`),
} {
out = append(out, ConvertInteractionsResponseToOpenAIResponses(context.Background(), "gpt-test", []byte(`{"model":"gpt-test"}`), nil, raw, &param)...)
}
if payload := findResponsesEventPayload(out, "response.output_text.delta"); gjson.GetBytes(payload, "delta").String() != "I will call a tool." {
t.Fatalf("output_text delta payload = %s", string(payload))
}
if payload := findResponsesEventPayload(out, "response.function_call_arguments.delta"); gjson.GetBytes(payload, "delta").String() != `{"location":"北京"}` {
t.Fatalf("function args delta payload = %s", string(payload))
}
argumentsDonePayload := findResponsesEventPayload(out, "response.function_call_arguments.done")
if got := gjson.GetBytes(argumentsDonePayload, "item_id").String(); got != "call_1" {
t.Fatalf("function args done item_id = %q, want call_1. Payload: %s", got, string(argumentsDonePayload))
}
if got := gjson.GetBytes(argumentsDonePayload, "arguments").String(); got != `{"location":"北京"}` {
t.Fatalf("function args done arguments = %q, want full arguments. Payload: %s", got, string(argumentsDonePayload))
}
createdPayload := findResponsesEventPayload(out, "response.created")
if got := gjson.GetBytes(createdPayload, "response.model").String(); got != "gpt-test" {
t.Fatalf("response.created models = %q, want gpt-test", got)
}
completedPayload := findResponsesEventPayload(out, "response.completed")
if got := gjson.GetBytes(completedPayload, "response.usage.total_tokens").Int(); got != 399 {
t.Fatalf("total_tokens = %d, want 399. Payload: %s", got, string(completedPayload))
}
if got := gjson.GetBytes(completedPayload, "response.usage.output_tokens_details.reasoning_tokens").Int(); got != 240 {
t.Fatalf("reasoning_tokens = %d, want 240. Payload: %s", got, string(completedPayload))
}
if got := strings.Join(responsesEventNames(out), ","); !strings.Contains(got, "response.completed") {
t.Fatalf("events = %s, want response.completed", got)
}
}
func TestConvertInteractionsResponseToOpenAIResponsesStreamFunctionCallStartArguments(t *testing.T) {
var param any
var out [][]byte
for _, raw := range [][]byte{
[]byte(`event: step.start
data: {"index":0,"step":{"id":"call_1","type":"function_call","name":"lookup","arguments":{"q":"x"}},"event_type":"step.start"}
`),
[]byte(`event: step.stop
data: {"index":0,"event_type":"step.stop"}
`),
} {
out = append(out, ConvertInteractionsResponseToOpenAIResponses(context.Background(), "gpt-test", nil, nil, raw, &param)...)
}
gotEvents := strings.Join(responsesEventNames(out), ",")
wantEvents := "response.output_item.added,response.function_call_arguments.delta,response.function_call_arguments.done,response.output_item.done"
if gotEvents != wantEvents {
t.Fatalf("events = %s, want %s", gotEvents, wantEvents)
}
if payload := findResponsesEventPayload(out, "response.function_call_arguments.delta"); gjson.GetBytes(payload, "delta").String() != `{"q":"x"}` {
t.Fatalf("function args delta = %s", string(payload))
}
if payload := findResponsesEventPayload(out, "response.function_call_arguments.done"); gjson.GetBytes(payload, "arguments").String() != `{"q":"x"}` {
t.Fatalf("function args done = %s", string(payload))
}
if payload := findResponsesEventPayload(out, "response.output_item.done"); gjson.GetBytes(payload, "item.arguments").String() != `{"q":"x"}` {
t.Fatalf("output item done = %s", string(payload))
}
}
func TestConvertInteractionsResponseToOpenAIResponsesStreamFunctionCallEmptyArguments(t *testing.T) {
var param any
var out [][]byte
for _, raw := range [][]byte{
[]byte(`event: step.start
data: {"index":0,"step":{"id":"call_1","type":"function_call","name":"lookup","arguments":{}},"event_type":"step.start"}
`),
[]byte(`event: step.stop
data: {"index":0,"event_type":"step.stop"}
`),
[]byte(`event: interaction.completed
data: {"interaction":{"id":"interaction_1","status":"completed","model":"gpt-test"},"event_type":"interaction.completed"}
`),
} {
out = append(out, ConvertInteractionsResponseToOpenAIResponses(context.Background(), "gpt-test", nil, nil, raw, &param)...)
}
gotEvents := strings.Join(responsesEventNames(out), ",")
wantEvents := "response.output_item.added,response.function_call_arguments.done,response.output_item.done,response.completed"
if gotEvents != wantEvents {
t.Fatalf("events = %s, want %s", gotEvents, wantEvents)
}
if payload := findResponsesEventPayload(out, "response.function_call_arguments.done"); gjson.GetBytes(payload, "arguments").String() != "{}" {
t.Fatalf("function args done = %s", string(payload))
}
if payload := findResponsesEventPayload(out, "response.output_item.done"); gjson.GetBytes(payload, "item.arguments").String() != "{}" {
t.Fatalf("output item done = %s", string(payload))
}
if payload := findResponsesEventPayload(out, "response.completed"); gjson.GetBytes(payload, "response.output.0.arguments").String() != "{}" {
t.Fatalf("completed output = %s", string(payload))
}
}
func TestConvertInteractionsResponseToOpenAIResponsesStreamFunctionCallEventsAreIdempotent(t *testing.T) {
var param any
var out [][]byte
for _, raw := range [][]byte{
[]byte(`event: step.start
data: {"index":0,"step":{"id":"call_1","type":"function_call","name":"lookup","arguments":{"q":"x"}},"event_type":"step.start"}
`),
[]byte(`event: step.start
data: {"index":0,"step":{"id":"call_1","type":"function_call","name":"lookup","arguments":{"q":"x"}},"event_type":"step.start"}
`),
[]byte(`event: step.stop
data: {"index":0,"event_type":"step.stop"}
`),
[]byte(`event: step.stop
data: {"index":0,"event_type":"step.stop"}
`),
} {
out = append(out, ConvertInteractionsResponseToOpenAIResponses(context.Background(), "gpt-test", nil, nil, raw, &param)...)
}
gotEvents := strings.Join(responsesEventNames(out), ",")
wantEvents := "response.output_item.added,response.function_call_arguments.delta,response.function_call_arguments.done,response.output_item.done"
if gotEvents != wantEvents {
t.Fatalf("events = %s, want %s", gotEvents, wantEvents)
}
}
func TestConvertInteractionsResponseToOpenAIResponsesStreamModelOutputDoneIncludesText(t *testing.T) {
var param any
var out [][]byte
for _, raw := range [][]byte{
[]byte(`event: step.start
data: {"index":0,"step":{"id":"msg_1","type":"model_output"},"event_type":"step.start"}
`),
[]byte(`event: step.delta
data: {"index":0,"delta":{"text":"hello","type":"text"},"event_type":"step.delta"}
`),
[]byte(`event: step.delta
data: {"index":0,"delta":{"text":" world","type":"text"},"event_type":"step.delta"}
`),
[]byte(`event: step.stop
data: {"index":0,"event_type":"step.stop"}
`),
} {
out = append(out, ConvertInteractionsResponseToOpenAIResponses(context.Background(), "gpt-test", []byte(`{"model":"gpt-test"}`), nil, raw, &param)...)
}
if payload := findResponsesEventPayload(out, "response.output_text.done"); gjson.GetBytes(payload, "text").String() != "hello world" {
t.Fatalf("output_text done payload = %s", string(payload))
}
if payload := findResponsesEventPayload(out, "response.content_part.done"); gjson.GetBytes(payload, "part.text").String() != "hello world" {
t.Fatalf("content_part done payload = %s", string(payload))
}
if payload := findResponsesEventPayload(out, "response.output_item.done"); gjson.GetBytes(payload, "item.content.0.text").String() != "hello world" {
t.Fatalf("output_item done payload = %s", string(payload))
}
}
func testGPTResponsesReasoningSignature() string {
payload := make([]byte, 1+8+16+16+32)
payload[0] = 0x80
payload[8] = 1
for i := 9; i < len(payload); i++ {
payload[i] = byte(i)
}
return base64.URLEncoding.EncodeToString(payload)
}
func TestConvertInteractionsResponseToOpenAIResponsesStreamPreservesThoughtSignature(t *testing.T) {
var param any
signature := testGPTResponsesReasoningSignature()
var out [][]byte
for _, raw := range [][]byte{
[]byte(`event: step.start
data: {"index":0,"step":{"type":"thought"},"event_type":"step.start"}
`),
[]byte(`event: step.delta
data: {"index":0,"delta":{"content":{"text":"thinking","type":"text"},"type":"thought_summary"},"event_type":"step.delta"}
`),
[]byte(`event: step.delta
data: {"index":0,"delta":{"signature":"","type":"thought_signature"},"event_type":"step.delta"}
`),
[]byte(`event: step.delta
data: {"index":0,"delta":{"signature":"` + signature + `","type":"thought_signature"},"event_type":"step.delta"}
`),
[]byte(`event: step.stop
data: {"index":0,"event_type":"step.stop"}
`),
[]byte(`event: interaction.completed
data: {"interaction":{"id":"interaction_1","status":"completed","object":"interaction","model":"gpt-test"},"event_type":"interaction.completed"}
`),
} {
out = append(out, ConvertInteractionsResponseToOpenAIResponses(context.Background(), "gpt-test", []byte(`{"model":"gpt-test"}`), nil, raw, &param)...)
}
if got := strings.Join(responsesEventNames(out), ","); strings.Contains(got, "response.output_text.delta") {
t.Fatalf("events = %s, did not expect output_text delta for thought signature", got)
}
donePayload := findResponsesEventPayload(out, "response.output_item.done")
if got := gjson.GetBytes(donePayload, "item.encrypted_content").String(); got != signature {
t.Fatalf("done encrypted_content = %q, want %q. Payload: %s", got, signature, string(donePayload))
}
if got := gjson.GetBytes(donePayload, "item.summary.0.text").String(); got != "thinking" {
t.Fatalf("done summary = %q, want thinking. Payload: %s", got, string(donePayload))
}
completedPayload := findResponsesEventPayload(out, "response.completed")
if got := gjson.GetBytes(completedPayload, "response.output.0.encrypted_content").String(); got != signature {
t.Fatalf("completed encrypted_content = %q, want %q. Payload: %s", got, signature, string(completedPayload))
}
}
func TestConvertInteractionsResponseToOpenAIResponsesStreamDropsForeignThoughtSignature(t *testing.T) {
var param any
foreignSignature := "foreign-gemini-signature"
var out [][]byte
for _, raw := range [][]byte{
[]byte(`event: step.start
data: {"index":0,"step":{"type":"thought"},"event_type":"step.start"}
`),
[]byte(`event: step.delta
data: {"index":0,"delta":{"content":{"text":"thinking","type":"text"},"type":"thought_summary"},"event_type":"step.delta"}
`),
[]byte(`event: step.delta
data: {"index":0,"delta":{"signature":"` + foreignSignature + `","type":"thought_signature"},"event_type":"step.delta"}
`),
[]byte(`event: step.stop
data: {"index":0,"event_type":"step.stop"}
`),
[]byte(`event: interaction.completed
data: {"interaction":{"id":"interaction_1","status":"completed","object":"interaction","model":"gpt-test"},"event_type":"interaction.completed"}
`),
} {
out = append(out, ConvertInteractionsResponseToOpenAIResponses(context.Background(), "gpt-test", []byte(`{"model":"gpt-test"}`), nil, raw, &param)...)
}
donePayload := findResponsesEventPayload(out, "response.output_item.done")
if got := gjson.GetBytes(donePayload, "item.encrypted_content").String(); got != "" {
t.Fatalf("done encrypted_content = %q, want empty for foreign signature. Payload: %s", got, string(donePayload))
}
if got := gjson.GetBytes(donePayload, "item.summary.0.text").String(); got != "thinking" {
t.Fatalf("done summary = %q, want thinking. Payload: %s", got, string(donePayload))
}
completedPayload := findResponsesEventPayload(out, "response.completed")
if got := gjson.GetBytes(completedPayload, "response.output.0.encrypted_content").String(); got != "" {
t.Fatalf("completed encrypted_content = %q, want empty for foreign signature. Payload: %s", got, string(completedPayload))
}
}
func TestConvertInteractionsResponseToOpenAIResponsesNonStreamThoughtSignature(t *testing.T) {
validSig := testGPTResponsesReasoningSignature()
rawValid := []byte(`{"id":"interaction_1","object":"interaction","status":"completed","steps":[{"type":"thought","signature":"` + validSig + `","content":[{"type":"text","text":"thinking"}]}],"usage":{"total_tokens":1}}`)
outValid := ConvertInteractionsResponseToOpenAIResponsesNonStream(context.Background(), "gpt-test", []byte(`{"model":"gpt-test"}`), nil, rawValid, nil)
if got := gjson.GetBytes(outValid, "output.0.encrypted_content").String(); got != validSig {
t.Fatalf("valid encrypted_content = %q, want %q. Output: %s", got, validSig, string(outValid))
}
if got := gjson.GetBytes(outValid, "output.0.summary.0.text").String(); got != "thinking" {
t.Fatalf("summary = %q, want thinking. Output: %s", got, string(outValid))
}
rawForeign := []byte(`{"id":"interaction_1","object":"interaction","status":"completed","steps":[{"type":"thought","thought_signature":"foreign-gemini-signature","content":[{"type":"text","text":"thinking"}]}],"usage":{"total_tokens":1}}`)
outForeign := ConvertInteractionsResponseToOpenAIResponsesNonStream(context.Background(), "gpt-test", []byte(`{"model":"gpt-test"}`), nil, rawForeign, nil)
if got := gjson.GetBytes(outForeign, "output.0.encrypted_content").String(); got != "" {
t.Fatalf("foreign encrypted_content = %q, want empty. Output: %s", got, string(outForeign))
}
if got := gjson.GetBytes(outForeign, "output.0.summary.0.text").String(); got != "thinking" {
t.Fatalf("summary = %q, want thinking. Output: %s", got, string(outForeign))
}
}
func TestConvertOpenAIResponsesResponseToInteractionsNonStreamFunctionCall(t *testing.T) {
raw := []byte(`{"id":"resp_1","output":[{"type":"function_call","name":"lookup","call_id":"call_1","arguments":{"q":"x"}}],"usage":{"input_tokens":1,"output_tokens":2,"total_tokens":3}}`)
out := ConvertOpenAIResponsesResponseToInteractionsNonStream(context.Background(), "gpt-test", nil, nil, raw, nil)
if got := gjson.GetBytes(out, "steps.0.type").String(); got != "function_call" {
t.Fatalf("step type = %q, want function_call", got)
}
if got := gjson.GetBytes(out, "steps.0.name").String(); got != "lookup" {
t.Fatalf("name = %q, want lookup", got)
}
if got := gjson.GetBytes(out, "steps.0.call_id").String(); got != "call_1" {
t.Fatalf("call_id = %q, want call_1", got)
}
}
func TestConvertOpenAIResponsesResponseToInteractionsNonStreamFunctionCallStringArgs(t *testing.T) {
raw := []byte(`{"id":"resp_1","output":[{"type":"function_call","name":"lookup","call_id":"call_1","arguments":"{\"q\":\"x\"}"}],"usage":{"input_tokens":1,"output_tokens":2,"total_tokens":3}}`)
out := ConvertOpenAIResponsesResponseToInteractionsNonStream(context.Background(), "gpt-test", nil, nil, raw, nil)
if got := gjson.GetBytes(out, "steps.0.type").String(); got != "function_call" {
t.Fatalf("step type = %q, want function_call", got)
}
if got := gjson.GetBytes(out, "steps.0.arguments.q").String(); got != "x" {
t.Fatalf("arguments.q = %q, want x", got)
}
}
func TestConvertOpenAIResponsesResponseToInteractionsNonStreamUsageDetails(t *testing.T) {
raw := []byte(`{"id":"resp_1","output":[{"type":"message","content":[{"type":"output_text","text":"ok"}]}],"usage":{"input_tokens":11,"output_tokens":13,"total_tokens":24,"input_tokens_details":{"cached_tokens":5},"output_tokens_details":{"reasoning_tokens":7}}}`)
out := ConvertOpenAIResponsesResponseToInteractionsNonStream(context.Background(), "gpt-test", nil, nil, raw, nil)
if got := gjson.GetBytes(out, "id").String(); got != "resp_1" {
t.Fatalf("id = %q, want resp_1. Output: %s", got, string(out))
}
if got := gjson.GetBytes(out, "usage.input_tokens").Int(); got != 11 {
t.Fatalf("usage.input_tokens = %d, want 11. Output: %s", got, string(out))
}
if got := gjson.GetBytes(out, "usage.output_tokens").Int(); got != 13 {
t.Fatalf("usage.output_tokens = %d, want 13. Output: %s", got, string(out))
}
if got := gjson.GetBytes(out, "usage.reasoning_tokens").Int(); got != 7 {
t.Fatalf("usage.reasoning_tokens = %d, want 7. Output: %s", got, string(out))
}
if got := gjson.GetBytes(out, "usage.cached_tokens").Int(); got != 5 {
t.Fatalf("usage.cached_tokens = %d, want 5. Output: %s", got, string(out))
}
}
func TestConvertOpenAIResponsesResponseToInteractionsStreamFunctionCallCallID(t *testing.T) {
var param any
raw := []byte(`{"type":"response.output_item.done","item":{"type":"function_call","id":"fc_1","call_id":"call_stream_1","name":"lookup","arguments":"{\"q\":\"x\"}"}}`)
out := ConvertOpenAIResponsesResponseToInteractions(context.Background(), "gpt-test", nil, nil, raw, &param)
payload := findInteractionsStepDeltaPayload(out)
if len(payload) == 0 {
t.Fatalf("step.delta payload not found")
}
startPayload := findInteractionsEventPayload(out, "step.start")
if got := gjson.GetBytes(startPayload, "step.id").String(); got != "call_stream_1" {
t.Fatalf("step.id = %q, want call_stream_1", got)
}
if got := gjson.GetBytes(payload, "delta.arguments").String(); got != `{"q":"x"}` {
t.Fatalf("delta.arguments = %q, want JSON string", got)
}
}
func TestConvertOpenAIResponsesResponseToInteractionsStreamSkipsDoneArgumentsAfterDelta(t *testing.T) {
var param any
deltaRaw := []byte(`{"type":"response.function_call_arguments.delta","output_index":0,"item_id":"fc_1","call_id":"call_1","delta":"{\"q\":\"x\"}"}`)
deltaOut := ConvertOpenAIResponsesResponseToInteractions(context.Background(), "gpt-test", nil, nil, deltaRaw, &param)
payload := findInteractionsStepDeltaPayload(deltaOut)
if len(payload) == 0 {
t.Fatalf("delta step.delta payload not found")
}
if got := gjson.GetBytes(payload, "delta.arguments").String(); got != `{"q":"x"}` {
t.Fatalf("delta.arguments = %q, want JSON string. Payload: %s", got, string(payload))
}
doneRaw := []byte(`{"type":"response.output_item.done","output_index":0,"item":{"type":"function_call","id":"fc_1","call_id":"call_1","name":"lookup","arguments":"{\"q\":\"x\"}"}}`)
doneOut := ConvertOpenAIResponsesResponseToInteractions(context.Background(), "gpt-test", nil, nil, doneRaw, &param)
if got := countInteractionsEventType(doneOut, "step.delta"); got != 0 {
t.Fatalf("done step.delta count = %d, want 0", got)
}
if got := countInteractionsEventType(doneOut, "step.stop"); got != 1 {
t.Fatalf("done step.stop count = %d, want 1", got)
}
}
func TestConvertOpenAIResponsesResponseToInteractionsStreamSkipsDoneTextAfterDelta(t *testing.T) {
var param any
deltaRaw := []byte(`{"type":"response.output_text.delta","item_id":"msg_1","output_index":0,"content_index":0,"delta":"hi"}`)
deltaOut := ConvertOpenAIResponsesResponseToInteractions(context.Background(), "gpt-test", nil, nil, deltaRaw, &param)
payload := findInteractionsStepDeltaPayload(deltaOut)
if len(payload) == 0 {
t.Fatalf("delta step.delta payload not found")
}
if got := gjson.GetBytes(payload, "delta.text").String(); got != "hi" {
t.Fatalf("delta.text = %q, want hi. Payload: %s", got, string(payload))
}
doneRaw := []byte(`{"type":"response.output_item.done","output_index":0,"item":{"type":"message","id":"msg_1","content":[{"type":"output_text","text":"hi"}]}}`)
doneOut := ConvertOpenAIResponsesResponseToInteractions(context.Background(), "gpt-test", nil, nil, doneRaw, &param)
if got := countInteractionsEventType(doneOut, "step.delta"); got != 0 {
t.Fatalf("done step.delta count = %d, want 0", got)
}
}
func TestConvertOpenAIResponsesResponseToInteractionsStreamSkipsDoneTextAfterUnkeyedDelta(t *testing.T) {
var param any
deltaRaw := []byte(`{"type":"response.output_text.delta","item_id":"msg_1","output_index":0,"delta":"hi"}`)
deltaOut := ConvertOpenAIResponsesResponseToInteractions(context.Background(), "gpt-test", nil, nil, deltaRaw, &param)
payload := findInteractionsStepDeltaPayload(deltaOut)
if len(payload) == 0 {
t.Fatalf("delta step.delta payload not found")
}
if got := gjson.GetBytes(payload, "delta.text").String(); got != "hi" {
t.Fatalf("delta.text = %q, want hi. Payload: %s", got, string(payload))
}
doneRaw := []byte(`{"type":"response.output_item.done","output_index":0,"item":{"type":"message","id":"msg_1","content":[{"type":"output_text","text":"hi"}]}}`)
doneOut := ConvertOpenAIResponsesResponseToInteractions(context.Background(), "gpt-test", nil, nil, doneRaw, &param)
if got := countInteractionsEventType(doneOut, "step.delta"); got != 0 {
t.Fatalf("done step.delta count = %d, want 0", got)
}
}
func TestConvertOpenAIResponsesResponseToInteractionsStreamCompletedOutputFallback(t *testing.T) {
var param any
raw := []byte(`{"type":"response.completed","response":{"output":[{"type":"message","id":"msg_1","content":[{"type":"output_text","text":"final"}]}],"usage":{"input_tokens":1,"output_tokens":2,"total_tokens":3}}}`)
out := ConvertOpenAIResponsesResponseToInteractions(context.Background(), "gpt-test", nil, nil, raw, &param)
payload := findInteractionsStepDeltaPayload(out)
if len(payload) == 0 {
t.Fatalf("fallback step.delta payload not found")
}
if got := gjson.GetBytes(payload, "delta.text").String(); got != "final" {
t.Fatalf("delta.text = %q, want final. Payload: %s", got, string(payload))
}
if got := countInteractionsEventType(out, "interaction.completed"); got != 1 {
t.Fatalf("interaction.completed count = %d, want 1", got)
}
}
func TestConvertOpenAIResponsesResponseToInteractionsStreamEmitsDone(t *testing.T) {
var param any
completedRaw := []byte(`{"type":"response.completed","response":{"output":[],"usage":{"input_tokens":1,"output_tokens":2,"total_tokens":3}}}`)
completedOut := ConvertOpenAIResponsesResponseToInteractions(context.Background(), "gpt-test", nil, nil, completedRaw, &param)
doneOut := ConvertOpenAIResponsesResponseToInteractions(context.Background(), "gpt-test", nil, nil, []byte(`data: [DONE]`), &param)
if got := countInteractionsEventType(completedOut, "interaction.completed"); got != 1 {
t.Fatalf("completed interaction.completed count = %d, want 1", got)
}
if got := countInteractionsEventType(completedOut, "done"); got != 1 {
t.Fatalf("completed done count = %d, want 1", got)
}
if got := countInteractionsEventType(doneOut, "interaction.completed"); got != 0 {
t.Fatalf("done interaction.completed count = %d, want 0", got)
}
if got := countInteractionsEventType(doneOut, "done"); got != 0 {
t.Fatalf("done event count = %d, want 0", got)
}
if payload := findInteractionsEventPayload(completedOut, "done"); string(payload) != "[DONE]" {
t.Fatalf("done payload = %q, want [DONE]", string(payload))
}
}
func TestConvertInteractionsResponseToOpenAIResponsesStreamFinishMetadataUsage(t *testing.T) {
var param any
out := ConvertInteractionsResponseToOpenAIResponses(context.Background(), "gpt-test", nil, nil, []byte(`data: {"event_type":"finish","metadata":{"total_usage":{"total_input_tokens":2,"total_output_tokens":6,"total_thought_tokens":3,"total_cached_tokens":1,"total_tokens":11}}}`), &param)
payload := findResponsesEventPayload(out, "response.completed")
if len(payload) == 0 {
t.Fatalf("response.completed payload not found")
}
if got := gjson.GetBytes(payload, "response.usage.input_tokens").Int(); got != 2 {
t.Fatalf("input_tokens = %d, want 2. Payload: %s", got, string(payload))
}
if got := gjson.GetBytes(payload, "response.usage.output_tokens").Int(); got != 6 {
t.Fatalf("output_tokens = %d, want 6. Payload: %s", got, string(payload))
}
if got := gjson.GetBytes(payload, "response.usage.output_tokens_details.reasoning_tokens").Int(); got != 3 {
t.Fatalf("reasoning_tokens = %d, want 3. Payload: %s", got, string(payload))
}
if got := gjson.GetBytes(payload, "response.usage.input_tokens_details.cached_tokens").Int(); got != 1 {
t.Fatalf("cached_tokens = %d, want 1. Payload: %s", got, string(payload))
}
if got := gjson.GetBytes(payload, "response.usage.total_tokens").Int(); got != 11 {
t.Fatalf("total_tokens = %d, want 11. Payload: %s", got, string(payload))
}
}
func TestConvertOpenAIResponsesResponseToInteractionsStreamCreatedThenDelta(t *testing.T) {
var param any
var out [][]byte
for _, raw := range [][]byte{
[]byte(`{"type":"response.created","response":{"id":"resp_1","model":"gpt-test"}}`),
[]byte(`{"type":"response.output_text.delta","item_id":"msg_1","output_index":0,"content_index":0,"delta":"hi"}`),
} {
out = append(out, ConvertOpenAIResponsesResponseToInteractions(context.Background(), "gpt-test", nil, nil, raw, &param)...)
}
got := strings.Join(interactionsEventNames(out), ",")
want := "interaction.created,interaction.status_update,step.start,step.delta"
if got != want {
t.Fatalf("events = %s, want %s", got, want)
}
payload := findInteractionsEventPayload(out, "interaction.status_update")
if gotID := gjson.GetBytes(payload, "interaction_id").String(); gotID != "resp_1" {
t.Fatalf("interaction_id = %q, want resp_1. Payload: %s", gotID, string(payload))
}
}
func TestConvertOpenAIResponsesResponseToInteractionsStreamCompletesAfterSteps(t *testing.T) {
var param any
var out [][]byte
for _, raw := range [][]byte{
[]byte(`{"type":"response.output_text.delta","item_id":"msg_1","output_index":0,"content_index":0,"delta":"我将调用工具。"}`),
[]byte(`{"type":"response.output_item.done","output_index":1,"item":{"type":"function_call","id":"fc_1","call_id":"call_1","name":"lookup","arguments":"{\"q\":\"weather\"}"}}`),
[]byte(`{"type":"response.completed","response":{"output":[],"usage":{"input_tokens":1,"output_tokens":2,"total_tokens":3}}}`),
} {
out = append(out, ConvertOpenAIResponsesResponseToInteractions(context.Background(), "gpt-test", nil, nil, raw, &param)...)
}
got := strings.Join(interactionsEventNames(out), ",")
want := "interaction.created,interaction.status_update,step.start,step.delta,step.stop,step.start,step.delta,step.stop,interaction.completed,done"
if got != want {
t.Fatalf("events = %s, want %s", got, want)
}
completedPayload := findInteractionsEventPayload(out, "interaction.completed")
if gotTokens := gjson.GetBytes(completedPayload, "interaction.usage.total_tokens").Int(); gotTokens != 3 {
t.Fatalf("total_tokens = %d, want 3. Payload: %s", gotTokens, string(completedPayload))
}
}
func TestConvertOpenAIResponsesResponseToInteractionsStreamSkipsCompletedTextAfterUnkeyedDelta(t *testing.T) {
var param any
deltaRaw := []byte(`{"type":"response.output_text.delta","item_id":"msg_1","output_index":0,"delta":"final"}`)
deltaOut := ConvertOpenAIResponsesResponseToInteractions(context.Background(), "gpt-test", nil, nil, deltaRaw, &param)
payload := findInteractionsStepDeltaPayload(deltaOut)
if len(payload) == 0 {
t.Fatalf("delta step.delta payload not found")
}
if got := gjson.GetBytes(payload, "delta.text").String(); got != "final" {
t.Fatalf("delta.text = %q, want final. Payload: %s", got, string(payload))
}
raw := []byte(`{"type":"response.completed","response":{"output":[{"type":"message","id":"msg_1","content":[{"type":"output_text","text":"final"}]}],"usage":{"input_tokens":1,"output_tokens":2,"total_tokens":3}}}`)
out := ConvertOpenAIResponsesResponseToInteractions(context.Background(), "gpt-test", nil, nil, raw, &param)
if got := countInteractionsEventType(out, "step.delta"); got != 0 {
t.Fatalf("completed step.delta count = %d, want 0", got)
}
if got := countInteractionsEventType(out, "interaction.completed"); got != 1 {
t.Fatalf("interaction.completed count = %d, want 1", got)
}
}
func findInteractionsStepDeltaPayload(events [][]byte) []byte {
return findInteractionsEventPayload(events, "step.delta")
}
func findInteractionsEventPayload(events [][]byte, eventType string) []byte {
for _, event := range events {
payload := ssePayload(event)
if interactionsEventName(event, payload) == eventType {
return payload
}
}
return nil
}
func ssePayload(event []byte) []byte {
const prefix = "\ndata: "
idx := bytes.Index(event, []byte(prefix))
if idx < 0 {
return nil
}
return event[idx+len(prefix):]
}
func countInteractionsEventType(events [][]byte, eventType string) int {
count := 0
for _, event := range events {
payload := ssePayload(event)
if interactionsEventName(event, payload) == eventType {
count++
}
}
return count
}
func interactionsEventNames(events [][]byte) []string {
names := make([]string, 0, len(events))
for _, event := range events {
payload := ssePayload(event)
if name := interactionsEventName(event, payload); name != "" {
names = append(names, name)
}
}
return names
}
func interactionsEventName(event, payload []byte) string {
if eventType := gjson.GetBytes(payload, "event_type").String(); eventType != "" {
return eventType
}
const prefix = "event: "
lineEnd := bytes.IndexByte(event, '\n')
if lineEnd < 0 || !bytes.HasPrefix(event, []byte(prefix)) {
return ""
}
return string(event[len(prefix):lineEnd])
}
func findResponsesEventPayload(events [][]byte, eventType string) []byte {
for _, event := range events {
payload := ssePayload(event)
if gjson.GetBytes(payload, "type").String() == eventType {
return payload
}
}
return nil
}
func responsesEventNames(events [][]byte) []string {
names := make([]string, 0, len(events))
for _, event := range events {
payload := ssePayload(event)
if name := gjson.GetBytes(payload, "type").String(); name != "" {
names = append(names, name)
}
}
return names
}
func TestConvertInteractionsResponseToOpenAIResponsesNonStream_PreservesEnvironmentID(t *testing.T) {
raw := []byte(`{"id":"interaction_1","object":"interaction","environment_id":"env_abc123","status":"completed","steps":[{"type":"model_output","content":[{"text":"ok"}]}],"usage":{"input_tokens":1,"output_tokens":2,"total_tokens":3}}`)
out := ConvertInteractionsResponseToOpenAIResponsesNonStream(context.Background(), "antigravity-preview-05-2026", []byte(`{"model":"antigravity-preview-05-2026"}`), nil, raw, nil)
if got := gjson.GetBytes(out, "environment_id").String(); got != "env_abc123" {
t.Fatalf("environment_id = %q, want env_abc123. Output: %s", got, string(out))
}
}
func TestConvertInteractionsResponseToOpenAIResponsesStream_PreservesEnvironmentID(t *testing.T) {
var param any
var out [][]byte
rawEvents := [][]byte{
[]byte("event: interaction.created\ndata: {\"interaction\":{\"id\":\"interaction_1\",\"environment_id\":\"env_stream123\",\"model\":\"antigravity-preview-05-2026\"},\"event_type\":\"interaction.created\"}\n\n"),
[]byte("event: interaction.completed\ndata: {\"interaction\":{\"id\":\"interaction_1\",\"environment_id\":\"env_stream123\",\"status\":\"completed\"},\"event_type\":\"interaction.completed\"}\n\n"),
[]byte("event: done\ndata: [DONE]\n\n"),
}
for _, raw := range rawEvents {
out = append(out, ConvertInteractionsResponseToOpenAIResponses(context.Background(), "antigravity-preview-05-2026", []byte(`{"model":"antigravity-preview-05-2026"}`), nil, raw, &param)...)
}
createdPayload := findResponsesEventPayload(out, "response.created")
if got := gjson.GetBytes(createdPayload, "response.environment_id").String(); got != "env_stream123" {
t.Fatalf("response.created environment_id = %q, want env_stream123. Payload: %s", got, string(createdPayload))
}
completedPayload := findResponsesEventPayload(out, "response.completed")
if got := gjson.GetBytes(completedPayload, "response.environment_id").String(); got != "env_stream123" {
t.Fatalf("response.completed environment_id = %q, want env_stream123. Payload: %s", got, string(completedPayload))
}
}