Skip to content

[DO NOT MERGE] fix(openai): preserve responses tool fields (#3063)#3127

Open
roroghost17 wants to merge 3 commits intov1.4.xfrom
cherrypick/openai-responses-tool-fields
Open

[DO NOT MERGE] fix(openai): preserve responses tool fields (#3063)#3127
roroghost17 wants to merge 3 commits intov1.4.xfrom
cherrypick/openai-responses-tool-fields

Conversation

@roroghost17
Copy link
Copy Markdown
Contributor

@roroghost17 roroghost17 commented Apr 29, 2026

Summary

Adds support for two new Responses API tool types — tool_search and namespace — and extends the web_search tool schema with external_web_access and search_content_types fields that were previously dropped during request conversion.

Changes

  • Added ResponsesToolTypeNamespace to the allowlist in filterUnsupportedTools so namespace tools are no longer stripped before being sent to OpenAI.
  • Added ExternalWebAccess (*bool) and SearchContentTypes ([]string) fields to ResponsesToolWebSearch and ensured they are copied through filterUnsupportedTools rather than silently discarded.
  • Introduced ResponsesToolToolSearch struct with Execution and Parameters fields, and ResponsesToolNamespace (referenced but defined elsewhere) to the ResponsesTool union type.
  • Wired tool_search and namespace cases into both MarshalJSON and UnmarshalJSON on ResponsesTool so these tool types round-trip correctly.
  • Added TestResponsesTool_MarshalUnmarshal_ToolSearchTool to verify marshal/unmarshal correctness for tool_search.
  • Added TestToOpenAIResponsesRequest_PreservesNamespaceAndWebSearchFields to verify that namespace tools and the new web_search fields survive the full conversion pipeline.

Type of change

  • Bug fix
  • Feature
  • Refactor
  • Documentation
  • Chore/CI

Affected areas

  • Core (Go)
  • Transports (HTTP)
  • Providers/Integrations
  • Plugins
  • UI (React)
  • Docs

How to test

go test ./core/providers/openai/... ./core/schemas/...

Expected: all tests pass, including the two new test cases covering tool_search marshal/unmarshal and preservation of namespace tools and extended web_search fields through ToOpenAIResponsesRequest.

Screenshots/Recordings

N/A

Breaking changes

  • Yes
  • No

Related issues

N/A

Security considerations

No new auth, secrets, or PII handling introduced. The namespace tool type may encapsulate child tools (e.g., MCP-backed functions); no additional sandboxing is required beyond what the upstream API enforces.

Checklist

  • I read docs/contributing/README.md and followed the guidelines
  • I added/updated tests where appropriate
  • I updated documentation where needed
  • I verified builds succeed (Go and UI)
  • I verified the CI pipeline passes locally if applicable

Co-authored-by: Prince Pal <princepal7021@gmail.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 29, 2026

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Extended OpenAI integration to support namespace tool type.
  • Improvements

    • Enhanced web search tool configuration to preserve additional settings during conversion.
  • Tests

    • Expanded test coverage for tool serialization and conversion workflows to ensure configuration data integrity.

Walkthrough

Adds support for a new namespace tool type and tool_search tool variant in response schemas with custom JSON (un)marshalling, and preserves external_web_access and search_content_types when converting WebSearch tools for the OpenAI provider.

Changes

Cohort / File(s) Summary
OpenAI Responses Implementation
core/providers/openai/responses.go
Allow namespace in tool-type allowlist; when converting web_search tools, deep-copy external_web_access and clone search_content_types; minor whitespace cleanup.
OpenAI Responses Tests
core/providers/openai/responses_test.go
Add marshal/unmarshal round-trip test for tool_search and conversion test ensuring web_search fields (external_web_access, ordered search_content_types) and namespace tool structure survive ToOpenAIResponsesRequest.
Schema Definitions
core/schemas/responses.go
Add ResponsesToolToolSearch and ResponsesToolNamespace structs and ResponsesToolTypeNamespace constant; extend ResponsesTool union, add external_web_access and search_content_types to ResponsesToolWebSearch, and introduce custom UnmarshalJSON for message/stream normalization and updated tool (un)marshalling.

Sequence Diagram(s)

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

Possibly related PRs

Suggested reviewers

  • akshaydeo

Poem

🐰 I hopped through JSON, tidy and bright,

Saved search fields and gave namespace light.
Tools now parade in clean, gentle rows,
Tests clap their paws as the new logic grows.
✨🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title mentions 'preserve responses tool fields' which aligns with the main objective of preserving tool fields during conversion, though it downplays the feature by labeling it as a 'fix' and includes '[DO NOT MERGE]' prefix.
Description check ✅ Passed The description is comprehensive and covers all key template sections: Summary, Changes, Type of change, Affected areas, How to test, Breaking changes, and Security considerations. All required information is present and well-documented.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch cherrypick/openai-responses-tool-fields

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (2.11.4)

level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain main module or its selected dependencies"


Review rate limit: 4/5 reviews remaining, refill in 12 minutes.

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 29, 2026

Confidence Score: 1/5

Not safe to merge — both new UnmarshalJSON methods will cause a stack overflow at runtime.

Two P0 infinite-recursion bugs affect core deserialization paths (ResponsesMessage and BifrostResponsesStreamResponse). Any code that deserializes these types will crash immediately with a stack overflow.

core/schemas/responses.go — both UnmarshalJSON implementations must be fixed with a type alias before merging.

Important Files Changed

Filename Overview
core/schemas/responses.go Adds namespace/tool_search types, new web_search fields, and two custom UnmarshalJSON methods — but both methods call Unmarshal on their own receiver type, causing unconditional infinite recursion (stack overflow).
core/providers/openai/responses.go Adds namespace to the allowlist and copies ExternalWebAccess/SearchContentTypes through filterUnsupportedTools; changes look correct and consistent with existing field-copy patterns.
core/providers/openai/responses_test.go Adds two new test cases covering tool_search marshal/unmarshal and namespace/web_search field preservation; tests are well-structured but will stack-overflow at runtime due to the recursion bug in the UnmarshalJSON methods they exercise.

Reviews (3): Last reviewed commit: "add custom unmarshaller" | Re-trigger Greptile

Comment thread core/schemas/responses.go
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
core/providers/openai/responses_test.go (1)

1608-1678: Add a direct namespace marshal/unmarshal round-trip test.

This covers provider conversion well, but it still doesn't exercise the new ResponsesTool.MarshalJSON / UnmarshalJSON namespace branches in core/schemas/responses.go. A schema-level round-trip test, parallel to tool_search, would pin the union wiring independently of ToOpenAIResponsesRequest.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/providers/openai/responses_test.go` around lines 1608 - 1678, Add a new
unit test that performs a direct marshal/unmarshal round-trip for the namespace
branch of the ResponsesTool union to exercise ResponsesTool.MarshalJSON and
ResponsesTool.UnmarshalJSON in core/schemas/responses.go; create a ResponsesTool
value with Type=ResponsesToolTypeNamespace and a populated
ResponsesToolNamespace.Tools slice (including a child function tool with
Parameters), marshal it to JSON, unmarshal back to a ResponsesTool, and assert
the resulting struct has the same Type, non-nil ResponsesToolNamespace, and
preserved child tool fields (name, type, parameters); name the test something
like TestResponsesTool_MarshalUnmarshal_Namespace so it parallels the existing
tool_search round-trip test.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@core/providers/openai/responses.go`:
- Line 321: The ResponsesToolNamespace type is being allowed through without
recursing into namespace.Tools, so child tools (e.g., nested function schemas)
skip the top-level normalization/filtering path; update the Responses handling
to iterate over ResponsesToolNamespace.Tools and run the same normalize/filter
helper used by ToOpenAIResponsesRequest (ensuring Parameters.Normalized() is
applied) on each child tool so unsupported fields/types are stripped and nested
function parameters are normalized before being added to the final map.

---

Nitpick comments:
In `@core/providers/openai/responses_test.go`:
- Around line 1608-1678: Add a new unit test that performs a direct
marshal/unmarshal round-trip for the namespace branch of the ResponsesTool union
to exercise ResponsesTool.MarshalJSON and ResponsesTool.UnmarshalJSON in
core/schemas/responses.go; create a ResponsesTool value with
Type=ResponsesToolTypeNamespace and a populated ResponsesToolNamespace.Tools
slice (including a child function tool with Parameters), marshal it to JSON,
unmarshal back to a ResponsesTool, and assert the resulting struct has the same
Type, non-nil ResponsesToolNamespace, and preserved child tool fields (name,
type, parameters); name the test something like
TestResponsesTool_MarshalUnmarshal_Namespace so it parallels the existing
tool_search round-trip test.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 997caa4a-ed9c-48c9-85d0-672f992ec47f

📥 Commits

Reviewing files that changed from the base of the PR and between 1b394bb and 0803375.

📒 Files selected for processing (3)
  • core/providers/openai/responses.go
  • core/providers/openai/responses_test.go
  • core/schemas/responses.go

schemas.ResponsesToolTypeWebSearchPreview: true,
schemas.ResponsesToolTypeMemory: true,
schemas.ResponsesToolTypeToolSearch: true,
schemas.ResponsesToolTypeNamespace: true,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Recurse into namespace.Tools before allowing this type through.

Now that namespace survives filtering, its child tools bypass the top-level normalization/sanitization path. Nested function schemas never go through Parameters.Normalized(), and unsupported child tool fields/types can still leak inside ResponsesToolNamespace.Tools, which can change request-prefix serialization and still trigger OpenAI validation errors.

Suggested direction
+func normalizeAndFilterTools(tools []schemas.ResponsesTool) []schemas.ResponsesTool {
+	filtered := make([]schemas.ResponsesTool, 0, len(tools))
+	for _, tool := range tools {
+		switch tool.Type {
+		case schemas.ResponsesToolTypeFunction:
+			if tool.ResponsesToolFunction != nil && tool.ResponsesToolFunction.Parameters != nil {
+				funcCopy := *tool.ResponsesToolFunction
+				funcCopy.Parameters = tool.ResponsesToolFunction.Parameters.Normalized()
+				tool.ResponsesToolFunction = &funcCopy
+			}
+			filtered = append(filtered, tool)
+		case schemas.ResponsesToolTypeNamespace:
+			if tool.ResponsesToolNamespace != nil {
+				nsCopy := *tool.ResponsesToolNamespace
+				nsCopy.Tools = normalizeAndFilterTools(nsCopy.Tools)
+				tool.ResponsesToolNamespace = &nsCopy
+			}
+			filtered = append(filtered, tool)
+		// ...existing supported-tool filtering/copy logic...
+		}
+	}
+	return filtered
+}

Use the same helper from ToOpenAIResponsesRequest instead of only normalizing/filtering the top-level slice.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/providers/openai/responses.go` at line 321, The ResponsesToolNamespace
type is being allowed through without recursing into namespace.Tools, so child
tools (e.g., nested function schemas) skip the top-level normalization/filtering
path; update the Responses handling to iterate over ResponsesToolNamespace.Tools
and run the same normalize/filter helper used by ToOpenAIResponsesRequest
(ensuring Parameters.Normalized() is applied) on each child tool so unsupported
fields/types are stripped and nested function parameters are normalized before
being added to the final map.

@CLAassistant
Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you all sign our Contributor License Agreement before we can accept your contribution.
1 out of 2 committers have signed the CLA.

✅ princepal9120
❌ roroghost17
You have signed the CLA already but the status is still pending? Let us recheck it.

@roroghost17 roroghost17 changed the title fix(openai): preserve responses tool fields (#3063) [DO NOT MERGE] fix(openai): preserve responses tool fields (#3063) Apr 29, 2026
@coderabbitai coderabbitai Bot requested a review from akshaydeo April 29, 2026 08:26
Comment thread core/schemas/responses.go
Comment on lines +579 to +594
func (m *ResponsesMessage) UnmarshalJSON(data []byte) error {
if argVal := gjson.GetBytes(data, "arguments"); argVal.Exists() && argVal.Type == gjson.JSON {
normalized, err := Marshal(argVal.Raw)
if err != nil {
return err
}
data, err = sjson.SetRawBytes(data, "arguments", normalized)
if err != nil {
return err
}
}
if err := Unmarshal(data, m); err != nil {
return err
}
return nil
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P0 Infinite recursion — stack overflow on every unmarshal

Unmarshal(data, m) where m is *ResponsesMessage will call m.UnmarshalJSON(data) again (both sonic.Unmarshal and json.Unmarshal honour the json.Unmarshaler interface). The call is unconditional, so every unmarshal of a ResponsesMessage will immediately stack-overflow regardless of whether the arguments normalisation branch is taken.

Fix: use a local type alias to break the recursion, as is standard in Go:

func (m *ResponsesMessage) UnmarshalJSON(data []byte) error {
	if argVal := gjson.GetBytes(data, "arguments"); argVal.Exists() && argVal.Type == gjson.JSON {
		normalized, err := Marshal(argVal.Raw)
		if err != nil {
			return err
		}
		data, err = sjson.SetRawBytes(data, "arguments", normalized)
		if err != nil {
			return err
		}
	}
	type alias ResponsesMessage
	return Unmarshal(data, (*alias)(m))
}

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@core/schemas/responses.go`:
- Around line 575-594: The UnmarshalJSON methods are calling Unmarshal(data,
m/resp) on the same concrete type which triggers recursion; fix by defining a
local alias type (e.g. type responsesMessageAlias ResponsesMessage), unmarshal
into a variable of that alias (var alias responsesMessageAlias; if err :=
Unmarshal(data, &alias); err != nil { return err }), then assign back ( *m =
ResponsesMessage(alias) ); apply the same alias-based pattern to the other
UnmarshalJSON implementation that currently calls Unmarshal(data, resp).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a77300e4-5125-4bc7-a97d-c754dfaae6be

📥 Commits

Reviewing files that changed from the base of the PR and between 411621d and a6fd88a.

📒 Files selected for processing (1)
  • core/schemas/responses.go

Comment thread core/schemas/responses.go
Comment on lines +575 to +594
// UnmarshalJSON normalises the "arguments" field which OpenAI returns as a plain
// JSON string for most tool types but as a raw JSON object for some (e.g.
// tool_search). When an object is encountered it is compact-encoded as a JSON
// string so all downstream consumers continue to see a *string value.
func (m *ResponsesMessage) UnmarshalJSON(data []byte) error {
if argVal := gjson.GetBytes(data, "arguments"); argVal.Exists() && argVal.Type == gjson.JSON {
normalized, err := Marshal(argVal.Raw)
if err != nil {
return err
}
data, err = sjson.SetRawBytes(data, "arguments", normalized)
if err != nil {
return err
}
}
if err := Unmarshal(data, m); err != nil {
return err
}
return nil
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Avoid self-recursive UnmarshalJSON calls (stack overflow risk).

Line 590 and Line 2320 call Unmarshal(data, m/resp) on the same type that defines UnmarshalJSON, which can recurse indefinitely. Unmarshal into an alias type, then assign back.

✅ Proposed fix
 func (m *ResponsesMessage) UnmarshalJSON(data []byte) error {
 	if argVal := gjson.GetBytes(data, "arguments"); argVal.Exists() && argVal.Type == gjson.JSON {
 		normalized, err := Marshal(argVal.Raw)
 		if err != nil {
 			return err
 		}
 		data, err = sjson.SetRawBytes(data, "arguments", normalized)
 		if err != nil {
 			return err
 		}
 	}
-	if err := Unmarshal(data, m); err != nil {
+	type alias ResponsesMessage
+	var decoded alias
+	if err := Unmarshal(data, &decoded); err != nil {
 		return err
 	}
+	*m = ResponsesMessage(decoded)
 	return nil
 }
 func (resp *BifrostResponsesStreamResponse) UnmarshalJSON(data []byte) error {
 	if argVal := gjson.GetBytes(data, "arguments"); argVal.Exists() && argVal.Type == gjson.JSON {
 		normalized, err := Marshal(argVal.Raw)
 		if err != nil {
 			return err
 		}
 		data, err = sjson.SetRawBytes(data, "arguments", normalized)
 		if err != nil {
 			return err
 		}
 	}
-	if err := Unmarshal(data, resp); err != nil {
+	type alias BifrostResponsesStreamResponse
+	var decoded alias
+	if err := Unmarshal(data, &decoded); err != nil {
 		return err
 	}
+	*resp = BifrostResponsesStreamResponse(decoded)
 	return nil
 }

Also applies to: 2307-2324

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/schemas/responses.go` around lines 575 - 594, The UnmarshalJSON methods
are calling Unmarshal(data, m/resp) on the same concrete type which triggers
recursion; fix by defining a local alias type (e.g. type responsesMessageAlias
ResponsesMessage), unmarshal into a variable of that alias (var alias
responsesMessageAlias; if err := Unmarshal(data, &alias); err != nil { return
err }), then assign back ( *m = ResponsesMessage(alias) ); apply the same
alias-based pattern to the other UnmarshalJSON implementation that currently
calls Unmarshal(data, resp).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants