Skip to content

fix: handle multimodal content arrays and preserve non-standard message fields#8

Merged
jhaynie merged 5 commits intomainfrom
fix/message-content-flexible-parsing
Apr 16, 2026
Merged

fix: handle multimodal content arrays and preserve non-standard message fields#8
jhaynie merged 5 commits intomainfrom
fix/message-content-flexible-parsing

Conversation

@jhaynie
Copy link
Copy Markdown
Member

@jhaynie jhaynie commented Apr 16, 2026

Summary

  • Fixes the error json: cannot unmarshal array into Go struct field Message.Alias.messages.content of type string that occurs when OpenAI returns multimodal content as an array
  • Changes Message.Content from string to any to support both string content and multimodal content arrays (text/image parts)
  • Adds Message.Custom map to preserve non-standard message fields like tool_calls, function_call, tool_call_id, refusal, name, etc.
  • Adds custom UnmarshalJSON/MarshalJSON for Message to capture and re-emit unknown fields during serialization (required for prompt caching hash computation)
  • Fixes streaming extractors in bedrock and googleai to handle Content as any
  • Enhances Responses API extractor with annotations, reasoning outputs, function calls, and array response format support

Test plan

  • Added comprehensive tests for content as string, array, null, number, boolean
  • Added tests for non-standard message properties (tool_calls, function_call, tool_call_id, refusal, name)
  • Added tests for round-trip marshaling preserving custom fields
  • Added tests for web search with annotations
  • Added tests for reasoning outputs with reasoning_tokens
  • Added tests for multiple text segments in responses
  • Added tests for error handling
  • All existing tests pass

Summary by CodeRabbit

  • New Features

    • Support for non-text and mixed multimodal message content and preservation of non-standard message properties across JSON round-trips.
    • Expanded Responses parsing to capture output names/arguments, summaries, annotations, and richer output metadata.
  • Bug Fixes

    • Safer streaming content handling: avoids invalid concatenation when message content is not a string.
  • Tests

    • Extensive new and updated tests covering multimodal content, custom fields, round-trip serialization, and expanded Responses scenarios.

…ge fields

- Change Message.Content from string to any to support multimodal content
  (arrays of text/image parts) instead of just strings
- Add Message.Custom map to preserve non-standard fields like tool_calls,
  function_call, tool_call_id, refusal, name, etc.
- Add custom UnmarshalJSON/MarshalJSON for Message to capture and
  re-emit unknown fields during serialization (needed for prompt caching)
- Fix streaming extractors in bedrock and googleai to handle Content as any
- Enhance Responses API extractor with:
  - Annotations support (url_citation for web search results)
  - Reasoning output type with summary field
  - Function call support (name, arguments)
  - Array response format (streaming output)
  - Store full output in Custom for access to web_search_call, etc.
- Add comprehensive tests for:
  - Content as string, array, null, number, boolean
  - Non-standard message properties
  - Round-trip marshaling preserving custom fields
  - Web search with annotations
  - Reasoning outputs with reasoning_tokens
  - Multiple text segments
  - Error handling

Fixes error: "json: cannot unmarshal array into Go struct field
Message.Alias.messages.content of type string"
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 16, 2026

Warning

Rate limit exceeded

@jhaynie has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 5 minutes and 54 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 5 minutes and 54 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 68190e53-cfa3-4420-a60b-d990615010b8

📥 Commits

Reviewing files that changed from the base of the PR and between 548429b and 7ced1de.

📒 Files selected for processing (2)
  • providers/openai_compatible/responses_extractor.go
  • providers/openai_compatible/responses_test.go
📝 Walkthrough

Walkthrough

Message model now accepts arbitrary JSON in Content (any) and preserves unknown message fields in Custom via custom JSON marshal/unmarshal. Provider parsers and streaming handlers were updated for non-string content handling. Numerous parser and responses tests were added to cover multimodal content and Responses payload variations.

Changes

Cohort / File(s) Summary
Core Metadata Structure
metadata.go
Changed Message.Content from stringany. Added Message.Custom map[string]any (json:"-"). Implemented UnmarshalJSON and MarshalJSON to parse/emit flat JSON while preserving unknown fields.
Anthropic tests
providers/anthropic/parser_test.go
Switched test formatting to %v. Added TestParser_ContentArrayWithMultipleTypes to validate content arrays mixing text and non-text parts.
Bedrock parser & streaming
providers/bedrock/parser_test.go, providers/bedrock/streaming_extractor.go
Updated test formatting and added multi-block content concatenation test. Streaming now appends when existing Message.Content is a string; otherwise it overwrites with the incoming delta.
Google AI parser & streaming
providers/googleai/parser_test.go, providers/googleai/streaming_extractor.go
Added parser tests for multi-part and inline-data messages; updated assertions to %v. Streaming accumulation now type-checks Message.Content and concatenates only if it is a string, else replaces.
OpenAI-compatible parser tests
providers/openai_compatible/parser_test.go
Extensive new tests for messages[].content variants (string, arrays with mixed part types, null/number/boolean), preservation of non-standard fields into Message.Custom, tool/function-call fields, top-level refusal capture, and JSON round-trip behavior. Failure formatting updated to %v.
OpenAI-compatible responses extraction & tests
providers/openai_compatible/responses_extractor.go, providers/openai_compatible/responses_test.go
ResponsesExtractor: added fallback to parse top-level output arrays, expose parsed output in meta.Custom["output"] and raw wrapper in meta.Custom["output_raw"]. Expanded ResponsesOutputItem/ResponsesOutputContent model (name, arguments, summaries, annotations, logprobs). Simplified text assembly. Added comprehensive tests covering annotations, tool outputs, reasoning summaries, errors, and usage/cache metadata.
🚥 Pre-merge checks | ✅ 1
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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


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

Copy link
Copy Markdown

@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 (3)
metadata.go (1)

87-89: Don't let Custom override role or content.

Because Custom is merged after the typed fields, a caller can serialize a different role/content than Message actually holds. Skipping reserved keys here keeps the JSON output consistent with the struct state.

Suggested guard
  for k, v := range m.Custom {
+		if k == "role" || k == "content" {
+			continue
+		}
 		result[k] = v
  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@metadata.go` around lines 87 - 89, When merging m.Custom into result (the
loop over m.Custom), skip any reserved keys "role" and "content" so they cannot
override the typed fields; update the loop that iterates m.Custom (referencing
m.Custom and result) to continue/ignore entries where k == "role" or k ==
"content", leaving the existing typed values intact.
providers/openai_compatible/parser_test.go (2)

607-623: Tighten this test to assert text payloads, not just count.

Right now it only checks array length. Adding assertions for both "hello" and "world" would make this test materially stronger.

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

In `@providers/openai_compatible/parser_test.go` around lines 607 - 623, The test
TestParser_ContentAsArrayOfText currently only asserts that
meta.Messages[0].Content is an array of length 2; update it to also assert the
actual text payloads by casting each element of meta.Messages[0].Content to the
expected map/struct shape and verifying the "text" fields equal "hello" and
"world" respectively (i.e., after obtaining content :=
meta.Messages[0].Content.([]interface{}), check content[0]["text"] == "hello"
and content[1]["text"] == "world" or the equivalent field access used by
Parser.Parse).

593-603: Avoid panic-prone type assertions in test checks.

Several direct casts can panic and hide the specific assertion failure. Prefer checked assertions to keep failures actionable.

Proposed test-hardening pattern
- textPart := content[0].(map[string]interface{})
+ textPart, ok := content[0].(map[string]interface{})
+ if !ok {
+   t.Fatalf("expected content[0] to be object, got %T", content[0])
+ }

- imagePart := content[1].(map[string]interface{})
+ imagePart, ok := content[1].(map[string]interface{})
+ if !ok {
+   t.Fatalf("expected content[1] to be object, got %T", content[1])
+ }

Also applies to: 649-652, 673-676, 711-714

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

Inline comments:
In `@providers/openai_compatible/responses_extractor.go`:
- Around line 20-29: The fallback that checks if the payload is an array
inspects body[0], which fails when the JSON array is preceded by whitespace;
modify the logic in the responses extractor around the json.Unmarshal error
handling (variables: body, responsesResp, outputItems) to first find the first
non-whitespace byte (scan bytes until a non-space/newline/tab/carriage-return)
and test that byte == '[' before attempting the alternate unmarshal into
[]ResponsesOutputItem and setting responsesResp.Output/Status; keep existing
error returns otherwise.

---

Nitpick comments:
In `@metadata.go`:
- Around line 87-89: When merging m.Custom into result (the loop over m.Custom),
skip any reserved keys "role" and "content" so they cannot override the typed
fields; update the loop that iterates m.Custom (referencing m.Custom and result)
to continue/ignore entries where k == "role" or k == "content", leaving the
existing typed values intact.

In `@providers/openai_compatible/parser_test.go`:
- Around line 607-623: The test TestParser_ContentAsArrayOfText currently only
asserts that meta.Messages[0].Content is an array of length 2; update it to also
assert the actual text payloads by casting each element of
meta.Messages[0].Content to the expected map/struct shape and verifying the
"text" fields equal "hello" and "world" respectively (i.e., after obtaining
content := meta.Messages[0].Content.([]interface{}), check content[0]["text"] ==
"hello" and content[1]["text"] == "world" or the equivalent field access used by
Parser.Parse).
🪄 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: 8c89ce81-2a2c-4ba6-b3c4-af3100188e83

📥 Commits

Reviewing files that changed from the base of the PR and between b213390 and 1aed301.

📒 Files selected for processing (9)
  • metadata.go
  • providers/anthropic/parser_test.go
  • providers/bedrock/parser_test.go
  • providers/bedrock/streaming_extractor.go
  • providers/googleai/parser_test.go
  • providers/googleai/streaming_extractor.go
  • providers/openai_compatible/parser_test.go
  • providers/openai_compatible/responses_extractor.go
  • providers/openai_compatible/responses_test.go
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (11)
providers/googleai/streaming_extractor.go (1)

106-110: Nice type guard for streaming text accumulation.

This avoids invalid string concatenation now that Message.Content can hold non-string shapes.

providers/anthropic/parser_test.go (1)

205-219: Good multimodal regression test.

This locks in the current behavior for mixed text/image content without assuming Message.Content is always a string.

providers/bedrock/parser_test.go (1)

195-206: Useful multi-block coverage.

This protects the Bedrock text-concatenation path for messages with multiple content blocks.

providers/bedrock/streaming_extractor.go (1)

255-259: Nice compatibility fix for streamed deltas.

The type check keeps Bedrock streaming accumulation safe after Message.Content stopped being string-only.

providers/googleai/parser_test.go (1)

189-213: Good coverage for multipart and inline-data requests.

These cases exercise both text concatenation and the non-text-part path.

providers/openai_compatible/responses_test.go (1)

291-1227: Great edge-case coverage for the Responses API.

This suite materially lowers risk around array outputs, annotations, reasoning items, tool calls, usage details, and mixed multimodal inputs.

metadata.go (1)

49-59: Unknown request fields lose numeric precision, but this doesn't affect prompt-cache hashing.

Unmarshalling into map[string]any does convert JSON numbers to float64, so {"x": 123} becomes {"x": 123.0} when round-tripped. However, this is a minor issue for truly unknown fields. Prompt-cache hashing uses rawBody (the original request bytes), not m.Custom, so field precision here has no impact on cache key generation. If a provider includes structured metadata in requests, it should be modeled as a typed field in BodyMetadata.Custom rather than relying on lossy map conversion. For truly dynamic fields you cannot model, consider preserving critical numeric values as strings if exact round-trip is required.

			> Likely an incorrect or invalid review comment.
providers/openai_compatible/parser_test.go (4)

5-5: Good update: assertions now match polymorphic Content type.

Switching assertion logs to %v is the right move now that Message.Content can be non-string values.

Also applies to: 33-34, 136-137, 150-151, 236-237, 265-269, 294-295


562-624: Strong content-type coverage for parser behavior.

These cases give solid protection for Content as any (string, array, null, number, boolean), which is the core regression surface of this PR.

Also applies to: 789-829


625-716: Great validation of non-standard message field preservation.

The new tests meaningfully protect Message.Custom behavior for tool/function/refusal and arbitrary provider-specific keys.

Also applies to: 831-843


754-787: Round-trip test is high-value for serialization safety.

Nice addition—this catches regressions where custom keys or multimodal content shape could be dropped during marshal/unmarshal.

Comment thread providers/openai_compatible/responses_extractor.go
- Handle whitespace before JSON array in responses extractor
- Skip reserved keys (role, content) when merging Custom in MarshalJSON
- Add assertions for text content in TestParser_ContentAsArrayOfText
- Add tests for whitespace array and Custom field override protection
Copy link
Copy Markdown

@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.

🧹 Nitpick comments (1)
providers/openai_compatible/responses_extractor.go (1)

95-103: Consider using strings.Join for cleaner text concatenation.

The manual loop concatenation works but strings.Join would be more idiomatic and slightly more efficient for larger arrays.

♻️ Suggested simplification
-	if len(texts) == 0 {
-		return ""
-	}
-	if len(texts) == 1 {
-		return texts[0]
-	}
-	// Join multiple text segments with newline
-	result := texts[0]
-	for _, t := range texts[1:] {
-		result += "\n" + t
-	}
-	return result
+	return strings.Join(texts, "\n")

Note: This requires adding "strings" to the imports.

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

In `@providers/openai_compatible/responses_extractor.go` around lines 95 - 103,
Replace the manual loop that concatenates the slice variable texts with
strings.Join(texts, "\n") for clearer and more efficient concatenation; add
"strings" to the imports and remove the special-case if len(texts) == 1 branch
(strings.Join handles single-element slices correctly). Use the same surrounding
function in responses_extractor.go that returns the concatenated result so
callers are unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@providers/openai_compatible/responses_extractor.go`:
- Around line 95-103: Replace the manual loop that concatenates the slice
variable texts with strings.Join(texts, "\n") for clearer and more efficient
concatenation; add "strings" to the imports and remove the special-case if
len(texts) == 1 branch (strings.Join handles single-element slices correctly).
Use the same surrounding function in responses_extractor.go that returns the
concatenated result so callers are unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: bf585c7f-a1f3-4042-8b01-b3e7b740a7a6

📥 Commits

Reviewing files that changed from the base of the PR and between 1aed301 and 6aa3ded.

📒 Files selected for processing (4)
  • metadata.go
  • providers/openai_compatible/parser_test.go
  • providers/openai_compatible/responses_extractor.go
  • providers/openai_compatible/responses_test.go
✅ Files skipped from review due to trivial changes (1)
  • providers/openai_compatible/responses_test.go
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (10)
providers/openai_compatible/responses_extractor.go (3)

21-37: Whitespace handling before JSON array detection looks correct.

The implementation now properly scans for the first non-whitespace character before checking if the body is a JSON array. This addresses the previous review feedback. The fallback parsing path correctly populates responsesResp.Output and sets a default status.


117-145: Type definitions are well-structured for Responses API output.

The new fields (Name, Arguments, Summary, Annotations, Logprobs) properly model the richer output structures from the Responses API. Using *int for Index correctly handles nullable integers, and interface{} for Logprobs accommodates varying structures.


74-76: LGTM - storing full output in Custom.

Preserving the complete Output array in meta.Custom["output"] allows callers to access provider-specific fields (e.g., web_search_call, function call details) that aren't captured in the normalized Choices structure.

metadata.go (3)

28-35: LGTM - Message struct changes are well-designed.

Changing Content to any properly supports multimodal content arrays. The Custom map with json:"-" tag correctly excludes it from standard marshaling while the custom MarshalJSON handles its serialization.


37-62: LGTM - UnmarshalJSON correctly captures unknown fields.

The implementation uses the standard alias pattern to avoid recursion and properly filters reserved fields (role, content) when populating Custom. Double unmarshaling is the idiomatic Go approach for this use case.


64-95: LGTM - MarshalJSON correctly protects reserved fields.

The implementation properly prevents Custom entries from overriding role or content (lines 88-90). While the triple marshal/unmarshal pattern has some overhead, it ensures correctness and handles edge cases well. The early return optimization for empty Custom (lines 78-80) avoids unnecessary work in the common case.

providers/openai_compatible/parser_test.go (4)

32-33: LGTM - Format string update is correct.

Changing from %q to %v is appropriate since Content is now any type. The %v verb handles all types correctly for error messages.


562-843: Comprehensive test coverage for Content type variations.

The tests thoroughly cover all JSON value types that Content can now hold: strings, arrays (multimodal), null, numbers, and booleans. This validates the any type change works correctly across all expected inputs.


639-729: LGTM - Custom field preservation tests are thorough.

These tests validate that provider-specific message fields (tool_calls, tool_call_id, function_call, name, refusal) are correctly captured in Message.Custom. This is essential for accurate API proxying and prompt-caching hash computation.


768-889: LGTM - Round-trip and reserved field protection tests.

TestParser_MessageRoundTrip validates that custom properties survive parse → marshal → unmarshal cycles, which is critical for prompt-caching hash computation. TestParser_MessageCustomDoesNotOverrideReservedFields confirms the security of the MarshalJSON implementation.

Copy link
Copy Markdown

@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

Caution

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

⚠️ Outside diff range comments (2)
providers/openai_compatible/responses_extractor.go (2)

45-49: ⚠️ Potential issue | 🟠 Major

reasoning_tokens is parsed but never exposed.

ResponsesUsage.OutputTokensDetails was added, but Extract only copies prompt/completion/total counts. That silently drops reasoning-token accounting from the returned metadata, so the new reasoning support is incomplete.

📦 Suggested fix
  meta.Custom["status"] = responsesResp.Status
  meta.Custom["api_type"] = llmproxy.APITypeResponses
+ if responsesResp.Usage.OutputTokensDetails != nil && responsesResp.Usage.OutputTokensDetails.ReasoningTokens > 0 {
+ 	meta.Custom["output_tokens_details"] = map[string]any{
+ 		"reasoning_tokens": responsesResp.Usage.OutputTokensDetails.ReasoningTokens,
+ 	}
+ }
  if responsesResp.Error != nil {
  	meta.Custom["error"] = responsesResp.Error
  }

Also applies to: 70-77, 152-154

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

In `@providers/openai_compatible/responses_extractor.go` around lines 45 - 49, The
Extract logic is dropping the new reasoning-token counts; update every place
where a llmproxy.Usage literal is constructed (the blocks that currently set
PromptTokens, CompletionTokens, TotalTokens) to also populate the new reasoning
token field by reading responsesResp.Usage.OutputTokensDetails.Reasoning (or
ReasoningTokens if that is the exact field name on OutputTokensDetails) into the
corresponding llmproxy.Usage field (e.g., ReasoningTokens). Ensure you update
all occurrences (the Usage: llmproxy.Usage{...} construction sites in
responses_extractor.go) so reasoning token accounting is carried through the
returned metadata.

82-88: ⚠️ Potential issue | 🟠 Major

Add Refusal field handling to ResponsesOutputContent struct and extraction logic.

extractResponsesContent only appends output_text content, but Responses API can include message content items with type refusal. When a response contains only refusal content and no output_text, the message content becomes empty, losing the refusal message.

Add a Refusal field to ResponsesOutputContent and update the extraction logic to include it:

Suggested changes
 type ResponsesOutputContent struct {
 	Type        string                      `json:"type"`
 	Text        string                      `json:"text,omitempty"`
+	Refusal     string                      `json:"refusal,omitempty"`
 	Annotations []ResponsesOutputAnnotation `json:"annotations,omitempty"`
 	Logprobs    interface{}                 `json:"logprobs,omitempty"`
 }
@@
 		if item.Type == "message" {
 			for _, c := range item.Content {
 				if c.Type == "output_text" && c.Text != "" {
 					texts = append(texts, c.Text)
+				} else if c.Type == "refusal" && c.Refusal != "" {
+					texts = append(texts, c.Refusal)
 				}
 			}
 		}

Also applies to similar extraction logic at lines 126-131.

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

In `@providers/openai_compatible/responses_extractor.go` around lines 82 - 88, The
ResponsesOutputContent struct and extraction logic currently ignore refusal
items, causing messages with only refusal to be dropped; update the
ResponsesOutputContent model to add a Refusal string field (e.g., Refusal string
`json:"refusal,omitempty"`), then modify extractResponsesContent (and the
analogous extraction block around lines 126-131) to check each content item for
Type == "refusal" and if present append its text (or refusal text) into the
texts slice (or incorporate it into the built message) alongside output_text so
refusal-only messages are preserved; update any JSON tags/parsing as needed to
map the refusal payload into the new field.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@providers/openai_compatible/responses_extractor.go`:
- Around line 75-76: The ResponsesOutputAnnotation struct and the code that
stores responsesResp.Output into meta.Custom["output"] are dropping
start_index/end_index span fields; update ResponsesOutputAnnotation to include
StartIndex and EndIndex fields with `json:"start_index"` and `json:"end_index"`
tags (matching the Responses API names) so unmarshaling preserves span metadata,
and change the assignment that writes to meta.Custom["output"] to preserve the
raw JSON (e.g., store the original raw output bytes or json.RawMessage instead
of the partially unmarshaled struct) so callers can access both the annotated
structs and the original output with character positions.

---

Outside diff comments:
In `@providers/openai_compatible/responses_extractor.go`:
- Around line 45-49: The Extract logic is dropping the new reasoning-token
counts; update every place where a llmproxy.Usage literal is constructed (the
blocks that currently set PromptTokens, CompletionTokens, TotalTokens) to also
populate the new reasoning token field by reading
responsesResp.Usage.OutputTokensDetails.Reasoning (or ReasoningTokens if that is
the exact field name on OutputTokensDetails) into the corresponding
llmproxy.Usage field (e.g., ReasoningTokens). Ensure you update all occurrences
(the Usage: llmproxy.Usage{...} construction sites in responses_extractor.go) so
reasoning token accounting is carried through the returned metadata.
- Around line 82-88: The ResponsesOutputContent struct and extraction logic
currently ignore refusal items, causing messages with only refusal to be
dropped; update the ResponsesOutputContent model to add a Refusal string field
(e.g., Refusal string `json:"refusal,omitempty"`), then modify
extractResponsesContent (and the analogous extraction block around lines
126-131) to check each content item for Type == "refusal" and if present append
its text (or refusal text) into the texts slice (or incorporate it into the
built message) alongside output_text so refusal-only messages are preserved;
update any JSON tags/parsing as needed to map the refusal payload into the new
field.
🪄 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: 191e8ab9-23ce-4e1e-a019-246d90848284

📥 Commits

Reviewing files that changed from the base of the PR and between 6aa3ded and 94bcfc6.

📒 Files selected for processing (1)
  • providers/openai_compatible/responses_extractor.go
📜 Review details
🔇 Additional comments (1)
providers/openai_compatible/responses_extractor.go (1)

21-38: Nice fix on the array fallback.

Scanning to the first non-whitespace byte before checking for [ closes the whitespace-prefixed array case cleanly.

Comment thread providers/openai_compatible/responses_extractor.go
- Add StartIndex and EndIndex fields to ResponsesOutputAnnotation
- Store raw output as json.RawMessage in output_raw Custom field
- Add comprehensive tests for span fields and raw output preservation
Copy link
Copy Markdown

@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: 2

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

Inline comments:
In `@providers/openai_compatible/responses_extractor.go`:
- Around line 75-81: The code currently only sets meta.Custom["output_raw"] when
the response is wrapped as { "output": ... }, losing raw payload for bare-array
responses; modify the block around responsesResp.Output handling in
responses_extractor.go (where responsesResp, meta.Custom and output_raw are
referenced) to also detect and preserve a top-level JSON array: if
json.Unmarshal into the wrapper fails or if the raw body begins with '[' (or can
be unmarshaled into []ResponsesOutputItem), set meta.Custom["output_raw"] to the
raw body (as json.RawMessage) so array-mode responses preserve extra fields.
Ensure this logic runs alongside the existing wrapper unmarshalling so both
wrapped and bare-array responses populate output_raw.
- Around line 139-145: The ResponsesOutputAnnotation struct's StartIndex and
EndIndex should be changed from int to *int so zero values (0) are preserved
during JSON marshalling; update the struct definition
(ResponsesOutputAnnotation) to use StartIndex *int and EndIndex *int and then
audit places that construct or mutate ResponsesOutputAnnotation (e.g., any code
that sets StartIndex/EndIndex) to allocate integers (take addresses or use
helper funcs) so callers provide non-nil pointers when they intend to include 0.
🪄 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: b17e86dd-c658-4b6e-8c8b-4069be6cd598

📥 Commits

Reviewing files that changed from the base of the PR and between 94bcfc6 and 548429b.

📒 Files selected for processing (2)
  • providers/openai_compatible/responses_extractor.go
  • providers/openai_compatible/responses_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • providers/openai_compatible/responses_test.go

Comment thread providers/openai_compatible/responses_extractor.go Outdated
Comment thread providers/openai_compatible/responses_extractor.go Outdated
- Change StartIndex/EndIndex to *int to preserve zero values in JSON
- Set output_raw for bare-array responses (not just wrapped responses)
- Add tests for zero span values, bare-array raw output, missing fields
@jhaynie jhaynie merged commit 94d029e into main Apr 16, 2026
2 checks passed
@jhaynie jhaynie deleted the fix/message-content-flexible-parsing branch April 16, 2026 01:46
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.

1 participant