Skip to content

fix(adapters): reject malformed nested response shapes - #1332

Closed
Ingwannu wants to merge 1 commit into
devfrom
agent/fix-1325-nested-adapter-shapes
Closed

fix(adapters): reject malformed nested response shapes#1332
Ingwannu wants to merge 1 commit into
devfrom
agent/fix-1325-nested-adapter-shapes

Conversation

@Ingwannu

@Ingwannu Ingwannu commented Aug 9, 2026

Copy link
Copy Markdown
Owner

Summary

  • Reject malformed nested Google candidate and OpenAI Chat tool-call shapes through the adapter error channel.
  • Keep root data: null padding behavior unchanged while failing closed once an upstream claims a candidate or tool call.
  • Preserve non-stream usage accounting and prevent raw JavaScript TypeError exceptions from escaping the adapter.

Verification

  • taskset -c 0-1 bun run typecheck — passed on the rebased exact head.
  • taskset -c 0-1 bun test tests/openai-chat-hardening.test.ts tests/google-hardening.test.ts tests/openai-chat-parallel-stream.test.ts tests/google-vertex-stream.test.ts — 83 passed, 0 failed on the rebased exact head.
  • Full pre-rebase suite — 10,130 passed, 10 skipped, 1 unrelated existing local-environment failure in tests/codex-shim.test.ts (Unix shim exports persisted service API token before running Codex); that failure reproduces in isolation and does not touch adapter code.
  • git diff --check origin/dev...HEAD — passed.

Decision Log

  • 목적과 의도: Prevent malformed nested upstream response structures from silently losing tool calls or crashing the request with an implementation-level exception.
  • 기존 구현 및 제약 조건: Root null SSE padding is intentionally tolerated, but nested candidates: [null], object-shaped tool_calls, null tool calls, and missing tool functions were trusted through casts.
  • 검토한 주요 대안: Ignore malformed entries, coerce them into empty values, or terminate the adapter turn with a structured error.
  • 선택한 방식: Preserve benign root padding and fail closed only after an upstream claims a malformed candidate or tool-call structure.
  • 다른 대안 대신 이 방식을 선택한 이유: Ignoring or coercing a claimed tool call can orphan its matching result and allow a false successful completion; a structured adapter error is deterministic and observable.
  • 장점, 단점 및 영향: Valid provider responses are unchanged and malformed turns no longer leak raw TypeError; malformed providers now receive a terminal error instead of best-effort continuation.

Checklist

  • Scope stays focused and avoids unrelated cleanup.
  • Docs or release notes were updated when needed. No public configuration or API contract changed, so no docs update is needed.
  • Security-sensitive changes were reviewed for secrets, auth, and unsafe defaults. This patch changes response validation only and does not touch credentials or routing authority.

Closes #1325

Summary by CodeRabbit

  • Bug Fixes
    • Improved handling of malformed Google response candidates by returning clear terminal errors instead of silently skipping invalid data.
    • Added validation for malformed tool-call payloads in OpenAI Chat responses, preventing runtime errors and reporting adapter errors consistently.
    • Preserved usage-token metadata when invalid tool-call data is encountered.
  • Tests
    • Added coverage for malformed Google candidates and OpenAI tool-call payloads in streaming and non-streaming responses.

@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The adapters now validate nested Google candidate and OpenAI Chat tool-call payloads. Malformed structures emit terminal adapter errors instead of causing unsafe processing or runtime exceptions. Tests cover streaming and non-streaming OpenAI responses, Google streaming responses, usage preservation, and completion suppression.

Changes

Adapter payload validation

Layer / File(s) Summary
Google candidate validation
src/adapters/google.ts, tests/google-hardening.test.ts
Streaming parsing distinguishes missing, empty, invalid, and valid candidates values. Invalid candidates emit an "invalid candidates" error and do not emit "done".
OpenAI tool-call validation
src/adapters/openai-chat.ts, tests/openai-chat-hardening.test.ts
Streaming and non-streaming parsing validates tool-call containers, entries, nested function data, and required strings. Invalid payloads emit invalid-tool-calls errors, preserve usage metadata, and prevent completion.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

Suggested reviewers: lidge-jun, wibias, devmello

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes address all linked #1325 failures across Google streaming and OpenAI streaming and non-streaming paths, with focused hardening tests.
Out of Scope Changes check ✅ Passed The adapter changes and hardening tests remain within the linked issue scope and support the stated malformed-response handling objectives.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: rejecting malformed nested response shapes in the Google and OpenAI Chat adapters.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch agent/fix-1325-nested-adapter-shapes

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.

@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Deterministic PR hygiene checks passed.

@github-actions github-actions Bot added the bug Something isn't working label Aug 9, 2026
@Ingwannu
Ingwannu requested review from Wibias and lidge-jun August 9, 2026 04:17
@Ingwannu
Ingwannu marked this pull request as ready for review August 9, 2026 04:29

@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

🤖 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 `@src/adapters/google.ts`:
- Line 605: Update the candidate processing around the parts variable and its
iteration to allow absent content or parts, while validating present parts as an
array containing only non-null objects. Route invalid containers and elements
through the terminal adapter error channel rather than allowing iteration or
property access to throw, and add regression coverage for parts: {} and parts:
[null].

In `@src/adapters/openai-chat.ts`:
- Around line 931-947: Strengthen validation in the streaming tool-call handling
around rawToolCall and flushToolCalls: accept absent partial fields, but reject
present id and function.name/function.arguments values unless they are strings,
and reject function unless it is an object when provided. Before emitting final
tool-call events, require each call to have received string id, name, and
arguments values; route all invalid cases through
terminateWithError(invalidToolCallsEvent(pendingUsage)). Add regressions
covering malformed function, id, name, and arguments payloads.
🪄 Autofix

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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: fa454dca-4429-4bfb-839b-f0eaf6ddcd18

📥 Commits

Reviewing files that changed from the base of the PR and between 4bb249b and 8ef7210.

📒 Files selected for processing (4)
  • src/adapters/google.ts
  • src/adapters/openai-chat.ts
  • tests/google-hardening.test.ts
  • tests/openai-chat-hardening.test.ts

Comment thread src/adapters/google.ts
}

const parts = candidates[0].content?.parts as { text?: string; functionCall?: { name: string; args: unknown } }[] | undefined;
const parts = candidate.content?.parts as { text?: string; functionCall?: { name: string; args: unknown } }[] | undefined;

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Validate candidate.content.parts before iterating.

The candidate guard does not validate content.parts. A frame with parts: {} throws at Line 615. A frame with parts: [null] throws at Line 616. The outer catch rethrows these errors because they are not translator-budget errors.

Allow absent content or parts frames. If parts is present, reject a non-array container or non-object part through the terminal adapter error channel. Add regression cases for both shapes.

🤖 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 `@src/adapters/google.ts` at line 605, Update the candidate processing around
the parts variable and its iteration to allow absent content or parts, while
validating present parts as an array containing only non-null objects. Route
invalid containers and elements through the terminal adapter error channel
rather than allowing iteration or property access to throw, and add regression
coverage for parts: {} and parts: [null].

Comment on lines +931 to +947
const rawToolCalls = delta.tool_calls;
if (rawToolCalls !== undefined) {
// A claimed tool-call payload is not benign padding. Dropping it can leave the
// matching result permanently orphaned, so malformed nested shapes fail closed
// through the adapter error channel instead of escaping as TypeError (#1325).
if (!Array.isArray(rawToolCalls)) {
return yield* terminateWithError(invalidToolCallsEvent(pendingUsage));
}
for (const rawToolCall of rawToolCalls) {
if (!isRecord(rawToolCall)) {
return yield* terminateWithError(invalidToolCallsEvent(pendingUsage));
}
const tc = rawToolCall as {
index?: number;
id?: string;
function?: { name?: string; arguments?: string };
};

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

Validate nested streaming tool-call fields and final call state.

Lines 943-947 only validate that each entry is an object. A fragment such as { "function": [] } or { "id": 7 } is accepted. The adapter can then emit a tool_call_start with an empty or non-string id or name, followed by done.

Accept partial fragments when fields are absent. If a field is present, require its expected type. Before flushToolCalls emits events, reject calls that never received a string id, name, and arguments value. Add streaming regressions for malformed function, id, name, and arguments.

🤖 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 `@src/adapters/openai-chat.ts` around lines 931 - 947, Strengthen validation in
the streaming tool-call handling around rawToolCall and flushToolCalls: accept
absent partial fields, but reject present id and
function.name/function.arguments values unless they are strings, and reject
function unless it is an object when provided. Before emitting final tool-call
events, require each call to have received string id, name, and arguments
values; route all invalid cases through
terminateWithError(invalidToolCallsEvent(pendingUsage)). Add regressions
covering malformed function, id, name, and arguments payloads.

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

The core boundary is right on 8ef72105: this treats a malformed nested candidate/tool-call payload as claimed response data and fails closed, while preserving #1240's benign root-padding skip. That is the important distinction here, and the patch gets it right.

I verified the issue's six reported shapes against this exact head:

  • Google streaming candidates: [null] emits only google response contained invalid candidates; the later STOP frame is not consumed as a successful completion.
  • OpenAI streaming object-shaped tool_calls and [null] emit only upstream response contained invalid tool calls, preserve usage, and suppress the following [DONE].
  • OpenAI non-streaming object-shaped tool_calls, [null], and a call without function return the same structured error with usage instead of leaking a JavaScript exception.

The #1240 control also holds: tests/sse-null-data-frame.test.ts still proves that mid-stream root data: null is skipped in both Google and OpenAI Chat, the later content/finish signal and [DONE] complete the turn, and an all-padding stream still fails closed. Local result was 119 passed / 0 failed across that file plus the four focused suites named in the PR. Local typecheck remains the known Windows control failure only (@napi-rs/keyring missing): exit 2 with output line-for-line identical on this head and current dev@a9838c1a. The exact PR head's cross-platform ci check is successful.

I also independently reproduced both current CodeRabbit findings rather than taking them on trust:

  • content.parts container/element validation: parts: {} and parts: [null] still escape as raw TypeErrors.
  • streaming tool-call field validation: present function: [], numeric id, numeric function.name, and numeric function.arguments are accepted and followed by done; the latter three also emit non-string values through the string-typed adapter event contract. A temporary fail-closed probe was 0 passed / 6 failed across those two Google and four OpenAI shapes.

One qualification to the second automated recommendation: validating fields that are present is supported by the reproduction, but requiring every assembled streaming call to have received an ID and name would conflict with the existing, passing compatibility contracts in tests/openai-chat-parallel-stream.test.ts (synthesized missing ID, and T7's explicit missing-name parity). I would preserve those absence cases unless changing them is intentional and separately justified.

So, for the six #1325 cases and the #1240 interaction, I found no additional defect; the implementation and focused regressions are sound. I am leaving this as a comment rather than an approval while the two independently verified adjacent findings are unresolved. Please correct me if there is provider evidence that makes the existing missing-ID/name compatibility contract obsolete.

@lidge-jun

Copy link
Copy Markdown
Owner

Landed on dev as 2495de3, rebased onto the current head with your authorship preserved.

Re-verified against the moving dev head before merge: src/adapters/google.ts:580-588 still dereferenced candidates[0].finishReason and .content after checking only array length, src/adapters/openai-chat.ts:919-921 still iterated an unchecked delta.tool_calls, and :1076-1079 still dereferenced tc.function.name. Distinguishing benign root padding from malformed claimed nested content is the part that makes this safe — valid responses behave exactly as before, and malformed ones now fail deterministically through the existing adapter error channel instead of throwing raw. Full suite green (10243 pass, 0 fail).

Thanks — covering all six issue shapes across both streaming and non-streaming paths made this easy to trust.

@lidge-jun lidge-jun closed this Aug 9, 2026
@Wibias
Wibias deleted the agent/fix-1325-nested-adapter-shapes branch August 9, 2026 07:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants