Add projects
This commit is contained in:
parent
2d3a9ad623
commit
8b607dd700
1802 changed files with 503346 additions and 2 deletions
|
|
@ -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,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -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 ""
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -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, ¶m)...)
|
||||
}
|
||||
|
||||
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, ¶m)...)
|
||||
}
|
||||
|
||||
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, ¶m)...)
|
||||
}
|
||||
|
||||
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, ¶m)...)
|
||||
}
|
||||
|
||||
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, ¶m)...)
|
||||
}
|
||||
|
||||
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, ¶m)...)
|
||||
}
|
||||
|
||||
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, ¶m)...)
|
||||
}
|
||||
|
||||
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, ¶m)
|
||||
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, ¶m)
|
||||
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, ¶m)
|
||||
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, ¶m)
|
||||
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, ¶m)
|
||||
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, ¶m)
|
||||
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, ¶m)
|
||||
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, ¶m)
|
||||
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, ¶m)
|
||||
doneOut := ConvertOpenAIResponsesResponseToInteractions(context.Background(), "gpt-test", nil, nil, []byte(`data: [DONE]`), ¶m)
|
||||
|
||||
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}}}`), ¶m)
|
||||
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, ¶m)...)
|
||||
}
|
||||
|
||||
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, ¶m)...)
|
||||
}
|
||||
|
||||
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, ¶m)
|
||||
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, ¶m)
|
||||
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, ¶m)...)
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue