Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 99 additions & 0 deletions service/relayconvert/responses_request_to_chat.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const (
responsesInputTypeFunctionCall = "function_call"
responsesInputTypeFunctionCallOutput = "function_call_output"
responsesInputTypeCustomToolCall = "custom_tool_call"
responsesToolSearchProxyName = "tool_search"
)

func ResponsesRequestToChatCompletionsRequest(req *dto.OpenAIResponsesRequest) (*dto.GeneralOpenAIRequest, error) {
Expand Down Expand Up @@ -331,6 +332,37 @@ func responsesRequestToolsToChat(raw json.RawMessage) ([]dto.ToolCallRequest, er
continue
}

if toolType == "mcp_server" || toolType == "namespace" {
innerTools := responsesFlattenInnerTools(tool)
out = append(out, innerTools...)
continue
}

if toolType == "tool_search" {
out = append(out, dto.ToolCallRequest{
Type: "function",
Function: dto.FunctionRequest{
Name: responsesToolSearchProxyName,
Description: "Search and load Codex tools, plugins, connectors, and MCP namespaces for the current task.",
Parameters: map[string]any{
"type": "object",
"properties": map[string]any{
"query": map[string]any{
"type": "string",
"description": "Search query for tools or connectors to load.",
},
"limit": map[string]any{
"type": "integer",
"description": "Maximum number of tool groups to return.",
},
},
"required": []string{"query"},
},
},
})
continue
}

rawTool, err := common.Marshal(tool)
if err != nil {
return nil, err
Expand All @@ -343,6 +375,51 @@ func responsesRequestToolsToChat(raw json.RawMessage) ([]dto.ToolCallRequest, er
return out, nil
}

func responsesFlattenToolName(namespace string, name string) string {
return namespace + "__" + name
}

// responsesFlattenInnerTools converts the inner tools array of an
// mcp_server or namespace tool into individual function tool entries.
// Fields specific to the Responses API (e.g. defer_loading) are stripped.
func responsesFlattenInnerTools(tool map[string]any) []dto.ToolCallRequest {
innerTools, ok := tool["tools"].([]any)
if !ok {
return nil
}
namespace := strings.TrimSpace(common.Interface2String(tool["name"]))

out := make([]dto.ToolCallRequest, 0, len(innerTools))
for _, item := range innerTools {
tool, ok := item.(map[string]any)
if !ok {
common.SysError("responses to chat conversion skipped malformed inner tool entry")
continue
}
name := strings.TrimSpace(common.Interface2String(tool["name"]))
if name == "" {
continue
}
chatName := name
if namespace != "" {
chatName = responsesFlattenToolName(namespace, name)
}
params, ok := tool["parameters"].(map[string]any)
if !ok || params == nil {
params = map[string]any{"type": "object", "properties": map[string]any{}}
}
out = append(out, dto.ToolCallRequest{
Type: "function",
Function: dto.FunctionRequest{
Name: chatName,
Description: common.Interface2String(tool["description"]),
Parameters: params,
},
})
}
return out
}

func responsesRequestToolChoiceToChat(raw json.RawMessage) (any, error) {
if !rawJSONPresent(raw) {
return nil, nil
Expand All @@ -362,6 +439,11 @@ func responsesRequestToolChoiceToChat(raw json.RawMessage) (any, error) {
if common.Interface2String(choice["type"]) == "function" {
name := strings.TrimSpace(common.Interface2String(choice["name"]))
if name != "" {
if namespace := strings.TrimSpace(common.Interface2String(choice["server_label"])); namespace != "" {
name = responsesFlattenToolName(namespace, name)
} else if namespace = strings.TrimSpace(common.Interface2String(choice["namespace"])); namespace != "" {
name = responsesFlattenToolName(namespace, name)
}
return map[string]any{
"type": "function",
"function": map[string]any{
Expand All @@ -370,6 +452,23 @@ func responsesRequestToolChoiceToChat(raw json.RawMessage) (any, error) {
}, nil
}
}
if choiceType := strings.TrimSpace(common.Interface2String(choice["type"])); choiceType == "mcp" || choiceType == "mcp_server" || choiceType == "namespace" {
name := strings.TrimSpace(common.Interface2String(choice["name"]))
if name == "" {
return choice, nil
}
if namespace := strings.TrimSpace(common.Interface2String(choice["server_label"])); namespace != "" {
name = responsesFlattenToolName(namespace, name)
} else if namespace = strings.TrimSpace(common.Interface2String(choice["namespace"])); namespace != "" {
name = responsesFlattenToolName(namespace, name)
}
return map[string]any{
"type": "function",
"function": map[string]any{
"name": name,
},
}, nil
}
return choice, nil
}

Expand Down
179 changes: 179 additions & 0 deletions service/relayconvert/responses_request_to_chat_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -268,3 +268,182 @@ func mustRawMessage(t *testing.T, value any) []byte {
require.NoError(t, err)
return raw
}

func TestResponsesRequestToChatCompletionsRequestMcpServerToolsFlattened(t *testing.T) {
got, err := ResponsesRequestToChatCompletionsRequest(&dto.OpenAIResponsesRequest{
Model: "gpt-test",
Input: mustRawMessage(t, "hello"),
Tools: mustRawMessage(t, []map[string]any{
{
"type": "mcp_server",
"name": "github",
"tools": []map[string]any{
{"name": "list_issues", "defer_loading": true, "parameters": map[string]any{"type": "object", "properties": map[string]any{}}},
{"name": "create_pr", "defer_loading": true, "parameters": map[string]any{"type": "object", "properties": map[string]any{}}},
},
},
}),
})
require.NoError(t, err)

require.Len(t, got.Tools, 2)
for _, tool := range got.Tools {
assert.Equal(t, "function", tool.Type)
assert.Empty(t, tool.Custom)
}
assert.Equal(t, "github__list_issues", got.Tools[0].Function.Name)
assert.Equal(t, "github__create_pr", got.Tools[1].Function.Name)
assert.Equal(t, "object", got.Tools[0].Function.Parameters.(map[string]any)["type"])
}

func TestResponsesRequestToChatCompletionsRequestNamespaceToolsFlattened(t *testing.T) {
got, err := ResponsesRequestToChatCompletionsRequest(&dto.OpenAIResponsesRequest{
Model: "gpt-test",
Input: mustRawMessage(t, "hello"),
Tools: mustRawMessage(t, []map[string]any{
{
"type": "namespace",
"name": "fs",
"tools": []map[string]any{
{"name": "read_file", "description": "Read a file", "parameters": map[string]any{"type": "object"}},
},
},
}),
})
require.NoError(t, err)

require.Len(t, got.Tools, 1)
assert.Equal(t, "function", got.Tools[0].Type)
assert.Equal(t, "fs__read_file", got.Tools[0].Function.Name)
assert.Equal(t, "Read a file", got.Tools[0].Function.Description)
assert.Equal(t, "object", got.Tools[0].Function.Parameters.(map[string]any)["type"])
}

func TestResponsesRequestToChatCompletionsRequestToolSearchProxied(t *testing.T) {
got, err := ResponsesRequestToChatCompletionsRequest(&dto.OpenAIResponsesRequest{
Model: "gpt-test",
Input: mustRawMessage(t, "hello"),
Tools: mustRawMessage(t, []map[string]any{
{"type": "tool_search"},
{
"type": "function",
"name": "lookup",
"description": "Lookup data",
"parameters": map[string]any{"type": "object"},
},
}),
})
require.NoError(t, err)

require.Len(t, got.Tools, 2)
assert.Equal(t, "function", got.Tools[0].Type)
assert.Equal(t, "tool_search", got.Tools[0].Function.Name)
assert.Equal(t, "Lookup data", got.Tools[1].Function.Description)
params, ok := got.Tools[0].Function.Parameters.(map[string]any)
require.True(t, ok)
assert.Equal(t, "object", params["type"])
query, ok := params["properties"].(map[string]any)["query"].(map[string]any)
require.True(t, ok)
assert.Equal(t, "string", query["type"])
assert.Equal(t, "function", got.Tools[0].Type)
assert.Equal(t, "lookup", got.Tools[1].Function.Name)
}

func TestResponsesRequestToChatCompletionsRequestMcpServerAndFunctionMixed(t *testing.T) {
got, err := ResponsesRequestToChatCompletionsRequest(&dto.OpenAIResponsesRequest{
Model: "gpt-test",
Input: mustRawMessage(t, "hello"),
Tools: mustRawMessage(t, []map[string]any{
{
"type": "mcp_server",
"name": "github",
"tools": []map[string]any{
{"name": "list_issues", "parameters": map[string]any{"type": "object"}},
{"name": "create_pr", "parameters": map[string]any{"type": "object"}},
},
},
{
"type": "function",
"name": "lookup",
"description": "Lookup data",
"parameters": map[string]any{"type": "object"},
},
{"type": "tool_search"},
}),
})
require.NoError(t, err)

require.Len(t, got.Tools, 4)
assert.Equal(t, "github__list_issues", got.Tools[0].Function.Name)
assert.Equal(t, "github__create_pr", got.Tools[1].Function.Name)
assert.Equal(t, "lookup", got.Tools[2].Function.Name)
assert.Equal(t, "tool_search", got.Tools[3].Function.Name)
}

func TestResponsesRequestToChatCompletionsRequestMcpServerInnerToolNilParameters(t *testing.T) {
got, err := ResponsesRequestToChatCompletionsRequest(&dto.OpenAIResponsesRequest{
Model: "gpt-test",
Input: mustRawMessage(t, "hello"),
Tools: mustRawMessage(t, []map[string]any{
{
"type": "mcp_server",
"name": "github",
"tools": []map[string]any{
{"name": "list_issues"},
},
},
}),
})
require.NoError(t, err)

require.Len(t, got.Tools, 1)
assert.Equal(t, "function", got.Tools[0].Type)
assert.Equal(t, "github__list_issues", got.Tools[0].Function.Name)
params, ok := got.Tools[0].Function.Parameters.(map[string]any)
require.True(t, ok)
assert.Equal(t, "object", params["type"])
}

func TestResponsesRequestToChatCompletionsRequestMcpServerInnerToolEmptyNameSkipped(t *testing.T) {
got, err := ResponsesRequestToChatCompletionsRequest(&dto.OpenAIResponsesRequest{
Model: "gpt-test",
Input: mustRawMessage(t, "hello"),
Tools: mustRawMessage(t, []map[string]any{
{
"type": "mcp_server",
"name": "github",
"tools": []map[string]any{
{"name": "list_issues", "parameters": map[string]any{"type": "object"}},
{"description": "no name"},
{"name": " ", "parameters": map[string]any{"type": "object"}},
{"name": "create_pr", "parameters": map[string]any{"type": "object"}},
},
},
}),
})
require.NoError(t, err)

require.Len(t, got.Tools, 2)
assert.Equal(t, "github__list_issues", got.Tools[0].Function.Name)
assert.Equal(t, "github__create_pr", got.Tools[1].Function.Name)
}

func TestResponsesRequestToChatCompletionsRequestMcpToolChoiceUsesFlattenedName(t *testing.T) {
got, err := ResponsesRequestToChatCompletionsRequest(&dto.OpenAIResponsesRequest{
Model: "gpt-test",
Input: mustRawMessage(t, "hello"),
ToolChoice: mustRawMessage(t, map[string]any{
"type": "mcp",
"server_label": "docs-svc",
"name": "search_docs",
}),
})
require.NoError(t, err)

assert.Equal(t, map[string]any{
"type": "function",
"function": map[string]any{
"name": "docs-svc__search_docs",
},
}, got.ToolChoice)
}