Skip to content

feat(responses): preserve tool semantics across Chat conversions - #2116

Open
Zacks-Zhang wants to merge 4 commits into
looplj:unstablefrom
Zacks-Zhang:codex/responses-chat-tool-compat
Open

feat(responses): preserve tool semantics across Chat conversions#2116
Zacks-Zhang wants to merge 4 commits into
looplj:unstablefrom
Zacks-Zhang:codex/responses-chat-tool-compat

Conversation

@Zacks-Zhang

@Zacks-Zhang Zacks-Zhang commented Jul 30, 2026

Copy link
Copy Markdown

Summary

  • preserve function, custom, namespace, client tool_search, additional_tools, and explicit client-owned future tool semantics across Responses-to-Chat conversion
  • restore non-streaming and streaming tool lifecycles, call IDs, argument fragments, warnings, and tool-choice validation
  • expand previous_response_id into scoped Chat history while preserving Responses instruction semantics
  • return invalid tool choices as HTTP 400 and align non-streaming incomplete_details with streaming behavior
  • add comprehensive simulation tests and bilingual documentation

Validation

  • go test ./internal/server/orchestrator ./internal/server/biz
  • cd llm && go test ./transformer/openai/...
  • deployed gpt-5.5/Kimi-K3 black-box tests passed for function/custom continuation, named server tools, non-streaming length, plain text, and streaming custom tools
  • git diff --check

Supersedes #2106 after the branch history was rebuilt.
Fixes #2105

Summary by CodeRabbit

  • New Features
    • Continued OpenAI Responses conversations now work across Chat Completions routing via previous_response_id.
    • Expanded Responses tool support, including tool search, custom tools, opaque tools, and improved streaming and non-streaming tool-call conversion.
  • Bug Fixes
    • Improved tool-call lifecycle ordering, history validation, malformed-data handling, and finish-reason mapping.
    • Added safeguards against oversized stored conversation data.
  • Documentation
    • Updated English and Chinese API references with conversation, storage, and error-handling details.

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR adds Responses-specific tool models and conversion paths, including Responses-to-Chat adaptation, streamed tool-call restoration, persisted previous_response_id history expansion, scoped response lookup, request indexing, tests, and updated API documentation.

Changes

Responses interoperability

Layer / File(s) Summary
Tool and persistence contracts
llm/constants.go, llm/tools.go, llm/transformer/openai/responses/model.go, internal/ent/...
Adds Responses tool-search and opaque-tool types, tool-call and item fields, serialization behavior, and the (project_id, external_id) request index.
Native Responses tool conversion
llm/transformer/openai/responses/*
Converts tool-search, additional-tools, namespace, client function-like, opaque tools, calls, outputs, and preserved raw input fragments.
Responses-to-Chat bridge
llm/transformer/openai/*, llm/transformer/openai/responses_chat_tools.go
Maps Responses tools and messages to Chat functions, records reversible metadata, restores calls, and validates tool choices.
Streaming tool lifecycle restoration
llm/transformer/openai/responses/inbound_stream.go, llm/transformer/openai/responses_chat_integration_test.go
Restores fragmented and parallel custom/tool-search calls and maps finish reasons to terminal Responses events.
Persisted conversation history
internal/server/biz/request.go, internal/server/orchestrator/*, docs/*
Loads scoped completed exchanges, reconstructs bounded history chains, hydrates Chat-bound requests, and documents storage and error semantics.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ResponsesRequest
  participant PersistentOutboundTransformer
  participant RequestService
  participant ResponsesHistory
  participant ResponsesChatToolAdapter
  participant ChatUpstream
  participant ResponsesStreamRestorer
  ResponsesRequest->>PersistentOutboundTransformer: TransformRequest
  PersistentOutboundTransformer->>RequestService: LoadCompletedResponseExchange
  RequestService-->>ResponsesHistory: scoped request and response bodies
  ResponsesHistory->>ResponsesChatToolAdapter: hydrated message history
  ResponsesChatToolAdapter->>ChatUpstream: converted tools and messages
  ChatUpstream-->>ResponsesStreamRestorer: streamed Chat tool-call chunks
  ResponsesStreamRestorer-->>ResponsesRequest: restored Responses events
Loading

Possibly related PRs

  • looplj/axonhub#758: Modifies shared Responses transformer models and conversion paths.
  • looplj/axonhub#865: Extends the earlier Responses custom-tool support in the same transformer models.
  • looplj/axonhub#2079: Updates related Responses namespace-tool expansion and raw-fragment handling.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 24.03% 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the primary change: preserving Responses tool semantics during Chat conversions.
Linked Issues check ✅ Passed The changes satisfy issue #2105 by converting and restoring special tools, preserving lifecycle data, promoting catalogs, and exposing unsupported-tool degradation.
Out of Scope Changes check ✅ Passed The documentation, history hydration, persistence limits, schema index, and tests support the stated Responses-to-Chat behavior and are not unrelated changes.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Comment thread internal/server/orchestrator/responses_history.go
@greptile-apps

greptile-apps Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Adds scoped previous_response_id history expansion for Responses-to-Chat conversion, with cumulative depth and byte limits, storage-aware bounded body loading, expanded tool-semantics preservation, lifecycle conversion fixes, validation, tests, and bilingual documentation.

Confidence Score: 5/5

The PR appears safe to merge.

The previously reported unbounded history expansion is now constrained by cumulative byte and depth limits, and oversized stored bodies are rejected through bounded storage reads or database-side filtering before full materialization; no blocking failure remains.

Important Files Changed

Filename Overview
internal/server/orchestrator/responses_history.go Expands scoped previous-response chains into Chat history while enforcing cycle, depth, and cumulative byte limits.
internal/server/biz/request.go Loads completed stored exchanges within a caller-provided budget and applies database-side size filtering before body materialization.
internal/server/biz/data_storage.go Adds bounded reads across database, object-store, and filesystem-backed storage, including advertised-size and observed-size enforcement.
llm/transformer/openai/responses/outbound_convert.go Extends Responses-to-Chat conversion to retain tool identities, ownership, arguments, and lifecycle semantics.
llm/transformer/openai/responses/inbound_stream.go Restores streaming Responses tool events and finish-state conversion from Chat-compatible upstream output.

Sequence Diagram

sequenceDiagram
    participant Client
    participant Orchestrator
    participant RequestService
    participant Storage
    participant ChatUpstream
    Client->>Orchestrator: Responses request with previous_response_id
    loop Up to 1024 responses and 32 MiB
        Orchestrator->>RequestService: Load exchange with remaining byte budget
        RequestService->>Storage: Bounded request/response body read
        Storage-->>RequestService: Stored exchange
        RequestService-->>Orchestrator: Scoped exchange
        Orchestrator->>Orchestrator: Convert exchange into Chat history
    end
    Orchestrator->>ChatUpstream: Current request plus expanded history
    ChatUpstream-->>Client: Converted Responses result
Loading

Reviews (6): Last reviewed commit: "feat(responses): 合并连续工具调用消息" | Re-trigger Greptile

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
llm/transformer/openai/responses/request_extensions.go (1)

268-297: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Mirror the guards from mergeRawOnlyTools in the input-item merge.

mergeRawOnlyTools rejects negative RepresentedToolCount (Line 323) and re-checks structuredIndex > len(structuredTools) after advancing (Line 347). The new input path does neither, so a negative count from a persisted/round-tripped RawInputItems fragment inflates total, drives structuredIndex negative, and structuredItems[structuredIndex] panics.

🛡️ Proposed fix
 	representedCount := 0
 	for _, fragment := range requestExt.RawInputItems {
+		if fragment.RepresentedToolCount < 0 {
+			return nil, false
+		}
 		representedCount += fragment.RepresentedToolCount
 	}
@@
 		if fragment, ok := rawByIndex[i]; ok {
 			items = append(items, cloneRaw(fragment.Raw))
 			structuredIndex += fragment.RepresentedToolCount
+			if structuredIndex > len(structuredItems) {
+				return nil, false
+			}
 			continue
 		}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@llm/transformer/openai/responses/request_extensions.go` around lines 268 -
297, Update the input-item merge logic around RawInputItems to reject fragments
with negative RepresentedToolCount, matching mergeRawOnlyTools. After advancing
structuredIndex for a raw fragment, validate it is not greater than
len(structuredItems), and return nil, false when invalid before any indexing
occurs.
llm/transformer/openai/responses/inbound_stream.go (1)

1109-1121: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Closing tool-call items iterates a map, so parallel-call event order is nondeterministic.

for idx, tc := range s.toolCalls has randomized iteration order in Go. When two or more tool-call items are still open at finish (the new parallel case captured in testdata/tool-2.stream.jsonl Lines 142-145), the function_call_arguments.done / output_item.done pairs can be emitted for output_index 3 before 2. That both breaks the strict event-by-event golden comparison in inbound_stream_test.go (Lines 100-111) and produces out-of-order SSE for clients that rely on ascending output_index. Sorting the indexes makes it deterministic.

🔒 Proposed deterministic ordering
-	// Close any open tool call items
-	for idx, tc := range s.toolCalls {
+	// Close any open tool call items in ascending index order so parallel
+	// calls emit terminal events deterministically.
+	indexes := make([]int, 0, len(s.toolCalls))
+	for idx := range s.toolCalls {
+		indexes = append(indexes, idx)
+	}
+	sort.Ints(indexes)
+
+	for _, idx := range indexes {
+		tc := s.toolCalls[idx]
 		if !s.toolCallItemStarted[idx] {
 			continue
 		}

Requires importing sort.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@llm/transformer/openai/responses/inbound_stream.go` around lines 1109 - 1121,
Update the tool-call closing loop around s.toolCalls to collect and sort its
indexes in ascending order before emitting function_call_arguments.done and
output_item.done events. Preserve the existing filtering and item-ID fallback
logic, and add the required sort import so parallel tool calls consistently
close by ascending output_index.
🧹 Nitpick comments (6)
internal/server/orchestrator/outbound.go (1)

387-396: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Previous-response history is re-fetched from storage on every candidate retry.

llmRequest.PreviousResponseID on the original request is untouched (only the hydrated clone has it cleared), so if this candidate fails and the orchestrator retries with another Chat-format candidate, hydratePreviousResponsesForChat re-walks the full previous_response_id chain (DB queries + external storage reads, up to maxPreviousResponseChainDepth) again for identical data. Consider caching the hydrated messages/request on p.state after the first successful hydration and reusing it on subsequent attempts for the same original PreviousResponseID.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/server/orchestrator/outbound.go` around lines 387 - 396, The retry
path around hydratePreviousResponsesForChat should cache the successfully
hydrated request or messages in p.state, keyed by the original
llmRequest.PreviousResponseID, and reuse that cached result for subsequent
Chat-format candidate attempts. Ensure the cache is populated only after
successful hydration and preserves the existing error behavior and request
transformation.
llm/transformer/openai/responses/inbound.go (1)

875-919: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider extracting the repeated function-like-tool conversion.

The "namespace" sub-tool branch (function/generic-client-like path) and the "default" case's generic-client-like path both marshal tool.Parameters and build an almost identical llm.Tool{Type:"function", Function: llm.Function{...}} literal, differing only in Namespace/ResponsesOrigin handling. A small helper (e.g. buildFunctionLikeTool(tool Tool, namespace string, forceOrigin bool) (llm.Tool, error)) would remove the duplication and reduce the size of this already-large switch.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@llm/transformer/openai/responses/inbound.go` around lines 875 - 919, Extract
the duplicated function-like tool conversion from the namespace sub-tool branch
and the default generic-client-like branch into a shared helper near the
surrounding conversion logic. Have the helper marshal parameters and build the
common llm.Tool structure while accepting namespace and ResponsesOrigin behavior
as inputs; update both callers to use it and preserve their current origin
semantics and error context.
llm/transformer/openai/outbound_convert.go (1)

81-117: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Nil request returns (nil, nil, nil).

The only current caller (OutboundTransformer.TransformRequest) rejects nil earlier, so this is latent, but a future caller gets a nil *Request and nil *responsesChatToolAdapter with no error and will panic on oaiReq.ReasoningEffort / toolAdapter.mappings(). Returning an error here is cheap insurance.

♻️ Suggested change
 	if r == nil {
-		return nil, nil, nil
+		return nil, nil, fmt.Errorf("chat completion request is nil")
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@llm/transformer/openai/outbound_convert.go` around lines 81 - 117, Update
requestFromLLMWithResponsesToolAdapter to return a non-nil error when the input
request r is nil, rather than returning nil request, nil adapter, and nil error.
Preserve the existing conversion flow for non-nil requests so callers cannot
proceed with missing Request or responsesChatToolAdapter values.
llm/transformer/openai/responses_chat_tools.go (1)

472-488: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Signature omits ParametersJsonSchema.

Two same-named function tools differing only in Function.ParametersJsonSchema (see llm/tools.go:54-64) hash to the same signature, so the second is treated as a duplicate and dropped without a conflict warning. Worth including the field if that mutually-exclusive variant can reach this path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@llm/transformer/openai/responses_chat_tools.go` around lines 472 - 488, The
functionDefinitionSignature function omits Function.ParametersJsonSchema when
building the normalized signature. Include this mutually exclusive field in the
marshaled signature so tools differing by ParametersJsonSchema produce distinct
signatures, while preserving the existing Parameters handling and duplicate
detection flow.
llm/transformer/openai/responses/inbound_stream.go (1)

897-917: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Prefer toolCallItemID[toolCallIndex] for the delta's item_id.

The fallback to s.currentItemID resolves to whichever item was opened last, so with parallel tool calls a delta for an earlier index can be attributed to the wrong item when state.ID is still empty. closeCurrentOutputItem (Line 1114) already prefers the new toolCallItemID map; using it here keeps added/delta/done item IDs consistent.

♻️ Proposed change
-	itemID := s.toolCalls[toolCallIndex].ID
+	itemID := s.toolCallItemID[toolCallIndex]
+	if itemID == "" {
+		itemID = s.toolCalls[toolCallIndex].ID
+	}
 	if itemID == "" {
 		itemID = s.currentItemID
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@llm/transformer/openai/responses/inbound_stream.go` around lines 897 - 917,
Update handleToolSearchCallDelta to resolve itemID from
s.toolCallItemID[toolCallIndex] before falling back to s.currentItemID, matching
closeCurrentOutputItem’s lookup and keeping tool-search added, delta, and done
events consistent for parallel calls.
llm/transformer/openai/responses_chat_integration_test.go (1)

716-846: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Three stream helpers duplicate the same pipeline.

simulateResponsesChatStream, simulateResponsesChatCustomChoices, and simulateResponsesChatCustomStream all repeat inbound transform → chat outbound transform → TransformStream → event collection, differing only in the request body and how provider chunks are built. Extracting a single core (request body + []map[string]any choices) and letting the custom variants build choices on top would cut ~80 lines and keep future protocol changes in one place.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@llm/transformer/openai/responses_chat_integration_test.go` around lines 716 -
846, Extract the shared inbound/outbound transformation and response-event
collection from simulateResponsesChatStream, simulateResponsesChatCustomChoices,
and simulateResponsesChatCustomStream into one helper accepting the request body
and provider choices. Update each existing helper to construct only its
request-specific body and choices, then delegate to the shared helper while
preserving custom tool-name resolution and terminal events.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/server/biz/request.go`:
- Around line 51-81: Update loadStoredResponseExchangeBody to return an
invalid_request_error when databaseBody is nil for a missing stored request
body, matching the existing os.ErrNotExist behavior for external storage;
preserve the empty JSON response for absent response bodies by distinguishing
bodyName before returning.

In `@llm/tools.go`:
- Around line 44-46: The CacheControl documentation in Tool contradicts its
json:"cache_control,omitempty" tag. Either change the tag to json:"-" to enforce
non-serialization, or remove/correct the non-serialization comment so it
accurately describes the existing conditional JSON serialization, matching
ToolCall.CacheControl behavior.

In `@llm/transformer/openai/responses_chat_tools.go`:
- Around line 700-732: Update restoreResponsesChatMessage’s custom-tool unwrap
logic so it only replaces the raw arguments when JSON unmarshalling succeeds and
the input field is present; otherwise retain the original
call.Function.Arguments and emit the same missing-input warning used by the
streaming path. Preserve the existing wrapped-input behavior for valid objects
containing input.

In `@llm/transformer/openai/responses/inbound_stream.go`:
- Around line 320-349: Update enqueueTerminalResponse to handle the
"content_filter" finish reason as an incomplete response, selecting
StreamEventTypeResponseIncomplete and setting IncompleteDetails.Reason to
"content_filter". Preserve the existing "length" mapping to "max_output_tokens"
and the default completed behavior for other finish reasons.
- Around line 863-875: Update handleCustomToolCallDelta and
handleToolSearchCallDelta to ensure state.ResponseCustomToolCall and
state.ResponseToolSearchCall are initialized before appending delta input.
Lazily create each missing sub-struct for its corresponding tool-call state,
then preserve the existing accumulation and metadata handling.

---

Outside diff comments:
In `@llm/transformer/openai/responses/inbound_stream.go`:
- Around line 1109-1121: Update the tool-call closing loop around s.toolCalls to
collect and sort its indexes in ascending order before emitting
function_call_arguments.done and output_item.done events. Preserve the existing
filtering and item-ID fallback logic, and add the required sort import so
parallel tool calls consistently close by ascending output_index.

In `@llm/transformer/openai/responses/request_extensions.go`:
- Around line 268-297: Update the input-item merge logic around RawInputItems to
reject fragments with negative RepresentedToolCount, matching mergeRawOnlyTools.
After advancing structuredIndex for a raw fragment, validate it is not greater
than len(structuredItems), and return nil, false when invalid before any
indexing occurs.

---

Nitpick comments:
In `@internal/server/orchestrator/outbound.go`:
- Around line 387-396: The retry path around hydratePreviousResponsesForChat
should cache the successfully hydrated request or messages in p.state, keyed by
the original llmRequest.PreviousResponseID, and reuse that cached result for
subsequent Chat-format candidate attempts. Ensure the cache is populated only
after successful hydration and preserves the existing error behavior and request
transformation.

In `@llm/transformer/openai/outbound_convert.go`:
- Around line 81-117: Update requestFromLLMWithResponsesToolAdapter to return a
non-nil error when the input request r is nil, rather than returning nil
request, nil adapter, and nil error. Preserve the existing conversion flow for
non-nil requests so callers cannot proceed with missing Request or
responsesChatToolAdapter values.

In `@llm/transformer/openai/responses_chat_integration_test.go`:
- Around line 716-846: Extract the shared inbound/outbound transformation and
response-event collection from simulateResponsesChatStream,
simulateResponsesChatCustomChoices, and simulateResponsesChatCustomStream into
one helper accepting the request body and provider choices. Update each existing
helper to construct only its request-specific body and choices, then delegate to
the shared helper while preserving custom tool-name resolution and terminal
events.

In `@llm/transformer/openai/responses_chat_tools.go`:
- Around line 472-488: The functionDefinitionSignature function omits
Function.ParametersJsonSchema when building the normalized signature. Include
this mutually exclusive field in the marshaled signature so tools differing by
ParametersJsonSchema produce distinct signatures, while preserving the existing
Parameters handling and duplicate detection flow.

In `@llm/transformer/openai/responses/inbound_stream.go`:
- Around line 897-917: Update handleToolSearchCallDelta to resolve itemID from
s.toolCallItemID[toolCallIndex] before falling back to s.currentItemID, matching
closeCurrentOutputItem’s lookup and keeping tool-search added, delta, and done
events consistent for parallel calls.

In `@llm/transformer/openai/responses/inbound.go`:
- Around line 875-919: Extract the duplicated function-like tool conversion from
the namespace sub-tool branch and the default generic-client-like branch into a
shared helper near the surrounding conversion logic. Have the helper marshal
parameters and build the common llm.Tool structure while accepting namespace and
ResponsesOrigin behavior as inputs; update both callers to use it and preserve
their current origin semantics and error context.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 2d7a87d4-e1ee-45e8-a847-90ab54a3534f

📥 Commits

Reviewing files that changed from the base of the PR and between 131dc03 and 1b19d1e.

📒 Files selected for processing (29)
  • docs/en/api-reference/openai-api.md
  • docs/zh/api-reference/openai-api.md
  • internal/ent/internal/schema.go
  • internal/ent/migrate/schema.go
  • internal/ent/schema/request.go
  • internal/server/biz/request.go
  • internal/server/orchestrator/outbound.go
  • internal/server/orchestrator/outbound_test.go
  • internal/server/orchestrator/responses_history.go
  • internal/server/orchestrator/responses_history_test.go
  • llm/constants.go
  • llm/tools.go
  • llm/transformer/openai/outbound.go
  • llm/transformer/openai/outbound_convert.go
  • llm/transformer/openai/outbound_convert_test.go
  • llm/transformer/openai/outbound_test.go
  • llm/transformer/openai/responses/inbound.go
  • llm/transformer/openai/responses/inbound_stream.go
  • llm/transformer/openai/responses/inbound_stream_test.go
  • llm/transformer/openai/responses/inbound_test.go
  • llm/transformer/openai/responses/model.go
  • llm/transformer/openai/responses/outbound.go
  • llm/transformer/openai/responses/outbound_convert.go
  • llm/transformer/openai/responses/outbound_convert_test.go
  • llm/transformer/openai/responses/outbound_test.go
  • llm/transformer/openai/responses/request_extensions.go
  • llm/transformer/openai/responses/testdata/tool-2.stream.jsonl
  • llm/transformer/openai/responses_chat_integration_test.go
  • llm/transformer/openai/responses_chat_tools.go

Comment thread internal/server/biz/request.go
Comment thread llm/tools.go Outdated
Comment thread llm/transformer/openai/responses_chat_tools.go
Comment thread llm/transformer/openai/responses/inbound_stream.go
Comment thread llm/transformer/openai/responses/inbound_stream.go
@Zacks-Zhang
Zacks-Zhang force-pushed the codex/responses-chat-tool-compat branch from 1b19d1e to e3ecfa4 Compare July 30, 2026 09:43
Comment thread internal/server/orchestrator/responses_history.go Outdated

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1

🧹 Nitpick comments (3)
internal/server/orchestrator/responses_history.go (1)

98-173: 🚀 Performance & Scalability | 🔵 Trivial

Deep previous_response_id chains cause many sequential blocking round-trips.

Each hop does one storage fetch (LoadCompletedResponseExchange) plus two full TransformRequest JSON parses. With maxPreviousResponseChainDepth = 1024, a worst-case (or adversarial) chain triggers up to 1024 sequential DB round-trips and ~2048 transform passes synchronously on the request path before the Chat outbound call is even made.

Consider adding chain-depth metrics/logging (e.g., warn when depth exceeds some threshold) so pathological chains are observable, and/or evaluate lowering the default depth cap or batch-loading exchanges to reduce request-thread latency.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/server/orchestrator/responses_history.go` around lines 98 - 173, Add
observability to the previous_response_id traversal loop by recording chain
depth and emitting a warning or metric when it crosses a defined
pathological-chain threshold, while retaining the existing
maxPreviousResponseChainDepth validation and error behavior. Use the existing
logging/metrics facilities and anchor the change around the depth loop and
maxPreviousResponseChainDepth rather than altering response transformation
semantics.
llm/transformer/openai/responses/inbound_stream_test.go (1)

93-95: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Prefer a per-case assertion hook over matching on the fixture filename.

Adding an optional extraAssert func(*testing.T, []StreamEvent) field to the table struct keeps the case-specific check next to its case and avoids string coupling to llm-tool-2.stream.jsonl.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@llm/transformer/openai/responses/inbound_stream_test.go` around lines 93 -
95, Update the table-driven test cases and execution loop in the inbound stream
test to support an optional extraAssert func(*testing.T, []StreamEvent) hook.
Move the parallel function-call lifecycle assertion into the relevant test
case’s extraAssert field, invoke the hook when present, and remove the
inputStreamFile filename comparison.
llm/transformer/openai/responses_chat_integration_test.go (1)

716-846: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Three near-identical stream simulation helpers could share one core.

simulateResponsesChatStream, simulateResponsesChatCustomChoices, and simulateResponsesChatCustomStream repeat the same request-transform → provider-events → stream-collect pipeline. Extracting the common tail (build provider events from choices, transform, collect events) would leave each helper as a thin wrapper.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@llm/transformer/openai/responses_chat_integration_test.go` around lines 716 -
846, Extract the duplicated request-transform, provider-event construction,
stream transformation, and event-collection logic from
simulateResponsesChatStream, simulateResponsesChatCustomChoices, and
simulateResponsesChatCustomStream into one shared helper. Have the existing
helpers only prepare their request-specific choices or fragments and delegate to
the common pipeline, preserving custom tool-name setup and finish-event
behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/server/orchestrator/responses_history.go`:
- Around line 107-118: Update loadPreviousResponsesHistory around
LoadCompletedResponseExchange to enforce the 32 MB limit for each persisted
exchange before its request and response bodies are fully loaded or decoded. Add
an early size check or per-exchange cap in the loader path, reject oversized
exchanges with transformer.ErrInvalidRequest, and preserve the existing
cumulative nextPreviousResponseHistoryBytes validation.

---

Nitpick comments:
In `@internal/server/orchestrator/responses_history.go`:
- Around line 98-173: Add observability to the previous_response_id traversal
loop by recording chain depth and emitting a warning or metric when it crosses a
defined pathological-chain threshold, while retaining the existing
maxPreviousResponseChainDepth validation and error behavior. Use the existing
logging/metrics facilities and anchor the change around the depth loop and
maxPreviousResponseChainDepth rather than altering response transformation
semantics.

In `@llm/transformer/openai/responses_chat_integration_test.go`:
- Around line 716-846: Extract the duplicated request-transform, provider-event
construction, stream transformation, and event-collection logic from
simulateResponsesChatStream, simulateResponsesChatCustomChoices, and
simulateResponsesChatCustomStream into one shared helper. Have the existing
helpers only prepare their request-specific choices or fragments and delegate to
the common pipeline, preserving custom tool-name setup and finish-event
behavior.

In `@llm/transformer/openai/responses/inbound_stream_test.go`:
- Around line 93-95: Update the table-driven test cases and execution loop in
the inbound stream test to support an optional extraAssert func(*testing.T,
[]StreamEvent) hook. Move the parallel function-call lifecycle assertion into
the relevant test case’s extraAssert field, invoke the hook when present, and
remove the inputStreamFile filename comparison.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: bdedbcea-b4b4-44c2-a8cf-5bd9ea1074f6

📥 Commits

Reviewing files that changed from the base of the PR and between 1b19d1e and e3ecfa4.

📒 Files selected for processing (29)
  • docs/en/api-reference/openai-api.md
  • docs/zh/api-reference/openai-api.md
  • internal/ent/internal/schema.go
  • internal/ent/migrate/schema.go
  • internal/ent/schema/request.go
  • internal/server/biz/request.go
  • internal/server/orchestrator/outbound.go
  • internal/server/orchestrator/outbound_test.go
  • internal/server/orchestrator/responses_history.go
  • internal/server/orchestrator/responses_history_test.go
  • llm/constants.go
  • llm/tools.go
  • llm/transformer/openai/outbound.go
  • llm/transformer/openai/outbound_convert.go
  • llm/transformer/openai/outbound_convert_test.go
  • llm/transformer/openai/outbound_test.go
  • llm/transformer/openai/responses/inbound.go
  • llm/transformer/openai/responses/inbound_stream.go
  • llm/transformer/openai/responses/inbound_stream_test.go
  • llm/transformer/openai/responses/inbound_test.go
  • llm/transformer/openai/responses/model.go
  • llm/transformer/openai/responses/outbound.go
  • llm/transformer/openai/responses/outbound_convert.go
  • llm/transformer/openai/responses/outbound_convert_test.go
  • llm/transformer/openai/responses/outbound_test.go
  • llm/transformer/openai/responses/request_extensions.go
  • llm/transformer/openai/responses/testdata/tool-2.stream.jsonl
  • llm/transformer/openai/responses_chat_integration_test.go
  • llm/transformer/openai/responses_chat_tools.go
🚧 Files skipped from review as they are similar to previous changes (5)
  • llm/constants.go
  • docs/en/api-reference/openai-api.md
  • docs/zh/api-reference/openai-api.md
  • llm/transformer/openai/responses/outbound_test.go
  • llm/transformer/openai/responses/outbound_convert_test.go

Comment thread internal/server/orchestrator/responses_history.go Outdated
- 保真转换 function、custom、namespace、tool_search 与 additional_tools
- 恢复流式及非流式工具调用、错误和结束状态语义
- 展开 previous_response_id 历史并限制累计体积,增加模拟测试与文档
@Zacks-Zhang
Zacks-Zhang force-pushed the codex/responses-chat-tool-compat branch from e3ecfa4 to eb836b3 Compare July 30, 2026 09:58

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
llm/transformer/openai/responses/request_extensions.go (1)

122-139: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Align representedRawToolCount with namespace subtool conversion.

representedRawToolCount only counts subTool.Type == "function" inside namespace tools, while convertToolsToLLM also emits structured function tools for generic client execution subtools (isGenericClientFunctionLike(subTool)). A namespace containing a non-"function" client-execution subtool will generate more structured tools than the raw fragment reports, so the merge loop may reject raw replay via structuredIndex != len(structuredTools) or skip/duplicate structured tools.

🐛 Proposed fix
 func representedRawToolCount(tool Tool) int {
 	if tool.Type == "tool_search" {
 		return 1
 	}
 	if tool.Type != "namespace" {
 		return 0
 	}
 
 	count := 0
 	for _, subTool := range tool.Tools {
-		if subTool.Type == "function" {
+		if subTool.Type == "function" || isGenericClientFunctionLike(subTool) {
 			count++
 		}
 	}
 
 	return count
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@llm/transformer/openai/responses/request_extensions.go` around lines 122 -
139, Update representedRawToolCount to count every namespace subtool that
convertToolsToLLM converts into a structured function tool, including subtools
recognized by isGenericClientFunctionLike, rather than only subtools with Type
"function". Keep tool_search and non-namespace handling unchanged so the raw
count matches the structured output used by the merge loop.
llm/transformer/openai/responses/outbound_convert.go (1)

657-724: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Handle tool_search_call output in convertOutputToMessage.

TransformResponse passes resp.Output through convertOutputToMessage, so any tool_search_call item in the Responses output will pass the switch with no tool-call handling and be omitted from the assistant llm.ToolCalls. Add a tool_search_call case that creates a ResponseToolSearchCall-backed tool call, or document/store the item for tool results.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@llm/transformer/openai/responses/outbound_convert.go` around lines 657 - 724,
Update convertOutputToMessage to handle outputItem.Type == "tool_search_call"
instead of dropping it; append an llm.ToolCall backed by ResponseToolSearchCall
and populate its available call ID, name, and input fields consistently with
custom tool calls. Preserve existing handling for function_call and
custom_tool_call items.
llm/transformer/openai/responses/inbound_stream.go (2)

898-913: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Delta events don't use the new per-index toolCallItemID map. initToolCall now records the emitted item ID in s.toolCallItemID[idx] and closeCurrentOutputItem prefers it, but both delta handlers still resolve item_id from tc.ID with s.currentItemID as fallback. With parallel tool calls, currentItemID points at the most recently initialized item, so deltas for an earlier index without a call ID are attributed to the wrong item while its terminal events use the right one.

  • llm/transformer/openai/responses/inbound_stream.go#L898-L913: resolve itemID from s.toolCallItemID[toolCallIndex] first in handleCustomToolCallDelta, keeping tc.ID/currentItemID as fallbacks.
  • llm/transformer/openai/responses/inbound_stream.go#L918-L938: apply the same toolCallItemID-first resolution in handleToolSearchCallDelta.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@llm/transformer/openai/responses/inbound_stream.go` around lines 898 - 913,
The delta handlers resolve item IDs incorrectly for parallel tool calls. In
llm/transformer/openai/responses/inbound_stream.go lines 898-913, update
handleCustomToolCallDelta to prefer s.toolCallItemID[toolCallIndex], then fall
back to tc.ID and s.currentItemID; apply the same resolution order in
handleToolSearchCallDelta at lines 918-938.

1129-1141: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Close tool-call items in deterministic index order.

initToolCall no longer closes the previous tool-call item, so multiple entries can be open here and this loop iterates s.toolCalls in Go's randomized map order. The emitted arguments.done / output_item.done pairs for parallel calls can therefore come out in a different order per run — nondeterministic for clients, and it makes the positional fixture comparison against testdata/tool-2.stream.jsonl flaky.

🛠️ Proposed fix
-	for idx, tc := range s.toolCalls {
+	indices := make([]int, 0, len(s.toolCalls))
+	for idx := range s.toolCalls {
+		indices = append(indices, idx)
+	}
+	sort.Ints(indices)
+
+	for _, idx := range indices {
+		tc := s.toolCalls[idx]
 		if !s.toolCallItemStarted[idx] {
 			continue
 		}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@llm/transformer/openai/responses/inbound_stream.go` around lines 1129 - 1141,
Update the tool-call closing loop in the stream finalization logic to process
open entries in deterministic index order rather than ranging directly over the
randomized s.toolCalls map. Collect and sort the tool-call indices, then
preserve the existing itemID fallback and closing-event behavior for each sorted
index.
🧹 Nitpick comments (3)
internal/server/orchestrator/responses_history_test.go (1)

246-268: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

This test allocates a full 32 MiB body on every run.

requestSize is derived from maxPreviousResponseHistoryBytes, so the suite allocates and JSON-unmarshals a ~32 MiB padded body just to exercise the cap. Making the limit an injectable package var (overridden with t.Cleanup restore) would keep the same boundary coverage at negligible cost.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/server/orchestrator/responses_history_test.go` around lines 246 -
268, Make maxPreviousResponseHistoryBytes an injectable package-level var, then
update TestLoadPreviousResponsesHistory_RejectsCumulativeHistoryOverByteLimit to
temporarily override it with a small test value and restore the original via
t.Cleanup. Preserve the existing boundary assertion and error expectations while
avoiding allocation and unmarshalling of a full 32 MiB body.
llm/transformer/openai/responses_chat_tools.go (1)

559-647: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Pending tool-call fragments are dropped when a stream ends without a finish_reason.

Fragments buffered in r.pending are only flushed in the choice.FinishReason != nil branch. If an upstream stream terminates without a finish reason (truncated/aborted upstream), buffered names and argument fragments are silently discarded rather than surfaced as a partial call. A terminal flush hook (or emitting on stream close) would make the degradation observable.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@llm/transformer/openai/responses_chat_tools.go` around lines 559 - 647, The
restore flow only emits buffered entries from r.pending when choice.FinishReason
is present, so terminal streams without a finish reason lose partial tool calls.
Add a stream-close or terminal flush path around
responsesChatToolStreamRestorer.restore that drains pending calls for the
affected choices, applies the same mapping/plain classification, appends the
partial calls to the response, and removes them from r.pending.
llm/transformer/openai/responses_chat_integration_test.go (1)

754-884: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider collapsing the three near-identical stream simulators.

simulateResponsesChatStream, simulateResponsesChatCustomChoices, and simulateResponsesChatCustomStream repeat the same pipeline (build Responses request → chat outbound → wrap provider chunks → collect Responses events). A single helper taking the request body plus a []map[string]any choice builder would let the other two become thin wrappers.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@llm/transformer/openai/responses_chat_integration_test.go` around lines 754 -
884, The three helpers duplicate the request transformation, provider stream
construction, response transformation, and event collection pipeline.
Consolidate that shared logic into one helper parameterized by the request body
and generated choice chunks, then reduce simulateResponsesChatStream,
simulateResponsesChatCustomChoices, and simulateResponsesChatCustomStream to
thin wrappers that prepare their inputs while preserving their current request
and chunk behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@llm/transformer/openai/responses/inbound.go`:
- Around line 887-947: Update representedRawToolCount to count namespace
subtools using the same promotion predicate as the conversion logic: include
subtools whose type is "function" or satisfy isGenericClientFunctionLike, not
only those with type "function". Keep fragment counts aligned with the
structured tools produced by the namespace conversion path.

---

Outside diff comments:
In `@llm/transformer/openai/responses/inbound_stream.go`:
- Around line 898-913: The delta handlers resolve item IDs incorrectly for
parallel tool calls. In llm/transformer/openai/responses/inbound_stream.go lines
898-913, update handleCustomToolCallDelta to prefer
s.toolCallItemID[toolCallIndex], then fall back to tc.ID and s.currentItemID;
apply the same resolution order in handleToolSearchCallDelta at lines 918-938.
- Around line 1129-1141: Update the tool-call closing loop in the stream
finalization logic to process open entries in deterministic index order rather
than ranging directly over the randomized s.toolCalls map. Collect and sort the
tool-call indices, then preserve the existing itemID fallback and closing-event
behavior for each sorted index.

In `@llm/transformer/openai/responses/outbound_convert.go`:
- Around line 657-724: Update convertOutputToMessage to handle outputItem.Type
== "tool_search_call" instead of dropping it; append an llm.ToolCall backed by
ResponseToolSearchCall and populate its available call ID, name, and input
fields consistently with custom tool calls. Preserve existing handling for
function_call and custom_tool_call items.

In `@llm/transformer/openai/responses/request_extensions.go`:
- Around line 122-139: Update representedRawToolCount to count every namespace
subtool that convertToolsToLLM converts into a structured function tool,
including subtools recognized by isGenericClientFunctionLike, rather than only
subtools with Type "function". Keep tool_search and non-namespace handling
unchanged so the raw count matches the structured output used by the merge loop.

---

Nitpick comments:
In `@internal/server/orchestrator/responses_history_test.go`:
- Around line 246-268: Make maxPreviousResponseHistoryBytes an injectable
package-level var, then update
TestLoadPreviousResponsesHistory_RejectsCumulativeHistoryOverByteLimit to
temporarily override it with a small test value and restore the original via
t.Cleanup. Preserve the existing boundary assertion and error expectations while
avoiding allocation and unmarshalling of a full 32 MiB body.

In `@llm/transformer/openai/responses_chat_integration_test.go`:
- Around line 754-884: The three helpers duplicate the request transformation,
provider stream construction, response transformation, and event collection
pipeline. Consolidate that shared logic into one helper parameterized by the
request body and generated choice chunks, then reduce
simulateResponsesChatStream, simulateResponsesChatCustomChoices, and
simulateResponsesChatCustomStream to thin wrappers that prepare their inputs
while preserving their current request and chunk behavior.

In `@llm/transformer/openai/responses_chat_tools.go`:
- Around line 559-647: The restore flow only emits buffered entries from
r.pending when choice.FinishReason is present, so terminal streams without a
finish reason lose partial tool calls. Add a stream-close or terminal flush path
around responsesChatToolStreamRestorer.restore that drains pending calls for the
affected choices, applies the same mapping/plain classification, appends the
partial calls to the response, and removes them from r.pending.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 288aa3bc-3d83-45d4-8cce-5f1a2b8ce6fe

📥 Commits

Reviewing files that changed from the base of the PR and between e3ecfa4 and eb836b3.

📒 Files selected for processing (29)
  • docs/en/api-reference/openai-api.md
  • docs/zh/api-reference/openai-api.md
  • internal/ent/internal/schema.go
  • internal/ent/migrate/schema.go
  • internal/ent/schema/request.go
  • internal/server/biz/request.go
  • internal/server/orchestrator/outbound.go
  • internal/server/orchestrator/outbound_test.go
  • internal/server/orchestrator/responses_history.go
  • internal/server/orchestrator/responses_history_test.go
  • llm/constants.go
  • llm/tools.go
  • llm/transformer/openai/outbound.go
  • llm/transformer/openai/outbound_convert.go
  • llm/transformer/openai/outbound_convert_test.go
  • llm/transformer/openai/outbound_test.go
  • llm/transformer/openai/responses/inbound.go
  • llm/transformer/openai/responses/inbound_stream.go
  • llm/transformer/openai/responses/inbound_stream_test.go
  • llm/transformer/openai/responses/inbound_test.go
  • llm/transformer/openai/responses/model.go
  • llm/transformer/openai/responses/outbound.go
  • llm/transformer/openai/responses/outbound_convert.go
  • llm/transformer/openai/responses/outbound_convert_test.go
  • llm/transformer/openai/responses/outbound_test.go
  • llm/transformer/openai/responses/request_extensions.go
  • llm/transformer/openai/responses/testdata/tool-2.stream.jsonl
  • llm/transformer/openai/responses_chat_integration_test.go
  • llm/transformer/openai/responses_chat_tools.go
🚧 Files skipped from review as they are similar to previous changes (4)
  • docs/en/api-reference/openai-api.md
  • internal/server/orchestrator/outbound_test.go
  • docs/zh/api-reference/openai-api.md
  • llm/transformer/openai/responses/outbound_convert_test.go

Comment on lines +887 to +947
converted := llm.Tool{
Type: "function",
Function: llm.Function{
Name: namespaceFunctionName(tool.Name, subTool.Name),
Namespace: tool.Name,
Description: subTool.Description,
Parameters: params,
Strict: subTool.Strict,
},
})
}
if subTool.Type != "function" {
converted.ResponsesOrigin = "raw_tool"
}
result = append(result, converted)
}

default:
// Skip unsupported tool types
continue
if isGenericClientFunctionLike(tool) {
params, err := json.Marshal(tool.Parameters)
if err != nil {
return nil, fmt.Errorf("failed to marshal %s tool parameters: %w", tool.Type, err)
}
result = append(result, llm.Tool{
Type: "function",
Function: llm.Function{
Name: tool.Name, Description: tool.Description,
Parameters: params, Strict: tool.Strict,
},
ResponsesOrigin: "raw_tool",
})
continue
}
result = append(result, opaqueResponsesTool(tool, "raw_tool"))
}
}

return result, nil
}

// isGenericClientFunctionLike reports whether an unknown client tool can use function semantics.
func isGenericClientFunctionLike(tool Tool) bool {
return isFunctionLike(tool) && tool.Execution == "client"
}

// isFunctionLike reports whether a Responses declaration has a function schema.
func isFunctionLike(tool Tool) bool {
return tool.Name != "" && tool.Parameters != nil
}

// opaqueResponsesTool preserves an unsupported declaration for same-protocol replay.
func opaqueResponsesTool(tool Tool, origin string) llm.Tool {
return llm.Tool{
Type: llm.ToolTypeResponsesOpaqueTool,
ResponseOpaqueTool: &llm.ResponseOpaqueTool{
SourceType: tool.Type, Name: tool.Name, Execution: tool.Execution,
Description: tool.Description,
},
ResponsesOrigin: origin,
}
}

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.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Namespace/default tool promotion logic verified against tests — no issues found here.

isGenericClientFunctionLike/isFunctionLike/opaqueResponsesTool correctly gate promotion vs. opaque preservation and match all new test expectations (DoesNotInferUnknownNamespaceExecutionOwner, PreservesUnsupportedFutureToolForExplicitRejection, PromotesFutureClientFunctionLikeTools). Note: the predicate used here (subTool.Type == "function" || isGenericClientFunctionLike(subTool)) to decide which namespace subtools become structured function tools is not mirrored by representedRawToolCount in request_extensions.go, which only counts subTool.Type == "function" — see the corresponding comment on that file for the resulting fragment-count mismatch.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@llm/transformer/openai/responses/inbound.go` around lines 887 - 947, Update
representedRawToolCount to count namespace subtools using the same promotion
predicate as the conversion logic: include subtools whose type is "function" or
satisfy isGenericClientFunctionLike, not only those with type "function". Keep
fragment counts aligned with the structured tools produced by the namespace
conversion path.

- 保真转换 tool_search_call 流式及非流式生命周期
- 稳定并行工具调用标识与关闭顺序
- 限制历史响应读取内存并补充边界回归测试

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 2

🧹 Nitpick comments (3)
internal/server/biz/data_storage_limited_test.go (1)

111-121: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

repeatingReader.Read returning (0, nil) at exhaustion violates the io.Reader contract.

The current test only survives because io.LimitReader(reader, 5) hits its own EOF at exactly remaining. Any future tweak where remaining < readLimit makes io.ReadAll spin forever. Return io.EOF instead.

♻️ Proposed fix
 func (r *repeatingReader) Read(p []byte) (int, error) {
 	if r.remaining == 0 {
-		return 0, nil
+		return 0, io.EOF
 	}

Add the io import:

 import (
 	"errors"
+	"io"
 	"path/filepath"
 	"testing"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/server/biz/data_storage_limited_test.go` around lines 111 - 121,
Update repeatingReader.Read so it returns 0, io.EOF when remaining is zero,
adding the io import as needed; preserve the existing byte-generation and
decrement behavior while data remains.
internal/server/orchestrator/responses_history_test.go (1)

34-54: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Record maxBytes in previousResponseLoadCall to assert the shrinking budget.

loadPreviousResponsesHistory passes maxPreviousResponseHistoryBytes - historyBytes on each hop, but the fake discards it, so the cumulative-budget tests can only observe the response IDs, not that the remaining budget actually decreases per link. Capturing it makes the accounting regression-proof.

♻️ Proposed change
 	l.calls = append(l.calls, previousResponseLoadCall{
 		responseID: responseID,
 		projectID:  projectID,
 		apiKeyID:   apiKeyID,
+		maxBytes:   maxBytes,
 	})

Extend the struct accordingly:

type previousResponseLoadCall struct {
	responseID string
	projectID  int
	apiKeyID   *int
	maxBytes   int64
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/server/orchestrator/responses_history_test.go` around lines 34 - 54,
Extend previousResponseLoadCall with a maxBytes int64 field, and update
mapPreviousResponseLoader.LoadCompletedResponseExchange to record the received
maxBytes when appending each call. Preserve the existing response, project, and
API key tracking so cumulative-budget tests can assert the shrinking budget per
hop.
internal/server/biz/request.go (1)

1373-1402: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Empty bodyRows conflates "oversized" with "row disappeared", and the two size expressions needn't escape the closure.

The budget was already validated against metadata just above, so a zero-row result here almost certainly means the request row was updated or deleted between the two queries — yet it surfaces as ErrStoredResponseExchangeTooLarge, which the orchestrator turns into a misleading previous_response_id history exceeds N bytes 400. Consider re-checking existence (or returning nil, nil so the caller reports "not found"). Also requestSizeExpression/responseSizeExpression are only read inside the Modify closure, so they can be plain locals.

♻️ Proposed cleanup
-		requestSizeExpression := ""
-		responseSizeExpression := ""
 		var bodyRows []struct {
 			RequestBody  objects.JSONRawMessage `json:"request_body"`
 			ResponseBody objects.JSONRawMessage `json:"response_body"`
 		}
 		err = client.Request.Query().
 			Where(request.IDEQ(metadata.ID)).
 			Limit(1).
 			Select(request.FieldRequestBody, request.FieldResponseBody).
 			Modify(func(selector *entsql.Selector) {
-				requestSizeExpression = storedResponseBodySizeExpression(dialectName, selector.C(request.FieldRequestBody))
-				responseSizeExpression = storedResponseBodySizeExpression(dialectName, selector.C(request.FieldResponseBody))
+				requestSizeExpression := storedResponseBodySizeExpression(dialectName, selector.C(request.FieldRequestBody))
+				responseSizeExpression := storedResponseBodySizeExpression(dialectName, selector.C(request.FieldResponseBody))
 				selector.Where(entsql.P(func(builder *entsql.Builder) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/server/biz/request.go` around lines 1373 - 1402, Update the body
lookup around requestSizeExpression, responseSizeExpression, and bodyRows so
empty results are distinguished from an oversized stored exchange: re-check
whether the request identified by metadata.ID still exists (or return nil, nil
so the caller emits its not-found response), and only return
ErrStoredResponseExchangeTooLarge when the row exists but fails the size
predicate. Simplify requestSizeExpression and responseSizeExpression to
closure-local values since they are only consumed inside Modify.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/server/biz/request.go`:
- Around line 62-75: Update storedResponseBodySizeExpression to measure the raw
stored bytes for Postgres and MySQL JSON columns, rather than casting them to
text or CHAR and measuring normalized JSON. Preserve the existing SQLite
behavior and ensure the returned dialect-specific expressions match the original
JSONRawMessage byte length used by budgeting.

In `@llm/transformer/openai/responses/inbound_stream_test.go`:
- Around line 233-250: Strengthen the assertions in the deltaEvents validation
around addedItemIDs so output indexes 0, 1, and 2 are each present exactly once
before comparing item IDs. Keep the existing item-ID checks, but count or
otherwise validate each event’s OutputIndex to reject cases where all deltas use
the same index.

---

Nitpick comments:
In `@internal/server/biz/data_storage_limited_test.go`:
- Around line 111-121: Update repeatingReader.Read so it returns 0, io.EOF when
remaining is zero, adding the io import as needed; preserve the existing
byte-generation and decrement behavior while data remains.

In `@internal/server/biz/request.go`:
- Around line 1373-1402: Update the body lookup around requestSizeExpression,
responseSizeExpression, and bodyRows so empty results are distinguished from an
oversized stored exchange: re-check whether the request identified by
metadata.ID still exists (or return nil, nil so the caller emits its not-found
response), and only return ErrStoredResponseExchangeTooLarge when the row exists
but fails the size predicate. Simplify requestSizeExpression and
responseSizeExpression to closure-local values since they are only consumed
inside Modify.

In `@internal/server/orchestrator/responses_history_test.go`:
- Around line 34-54: Extend previousResponseLoadCall with a maxBytes int64
field, and update mapPreviousResponseLoader.LoadCompletedResponseExchange to
record the received maxBytes when appending each call. Preserve the existing
response, project, and API key tracking so cumulative-budget tests can assert
the shrinking budget per hop.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d9685865-319f-479d-8d86-86b7146cb703

📥 Commits

Reviewing files that changed from the base of the PR and between eb836b3 and 7aa58bd.

📒 Files selected for processing (13)
  • internal/server/biz/data_storage.go
  • internal/server/biz/data_storage_limited_test.go
  • internal/server/biz/request.go
  • internal/server/orchestrator/responses_history.go
  • internal/server/orchestrator/responses_history_test.go
  • llm/transformer/openai/responses/inbound_stream.go
  • llm/transformer/openai/responses/inbound_stream_test.go
  • llm/transformer/openai/responses/outbound_convert.go
  • llm/transformer/openai/responses/outbound_convert_test.go
  • llm/transformer/openai/responses/outbound_stream.go
  • llm/transformer/openai/responses/outbound_stream_test.go
  • llm/transformer/openai/responses/outbound_test.go
  • llm/transformer/openai/responses/request_extensions_roundtrip_test.go
🚧 Files skipped from review as they are similar to previous changes (3)
  • internal/server/orchestrator/responses_history.go
  • llm/transformer/openai/responses/outbound_test.go
  • llm/transformer/openai/responses/inbound_stream.go

Comment thread internal/server/biz/request.go
Comment thread llm/transformer/openai/responses/inbound_stream_test.go
- 按数据库持久化后的 JSON 文本字节验证历史读取限额
- 明确 PostgreSQL 与 MySQL 的 Scan 字节预算语义
- 覆盖并行工具增量的完整输出索引集合
- 合并 function、custom 与 tool_search 调用为单个 assistant turn
- 保留 namespace 映射和含冒号的 call ID
- 补充输出边界及实际并行历史回归测试
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.

[Bug/错误]: Responses 转 Chat 时特殊工具原语丢失

1 participant