Skip to content

fix(gateway): keep google's original finish reason - #3516

Merged
steebchen merged 1 commit into
mainfrom
gemini3-image-content-filter
Aug 9, 2026
Merged

steebchen merged 1 commit into
mainfrom
gemini3-image-content-filter

Conversation

@steebchen

@steebchen steebchen commented Aug 9, 2026 •

Copy link
Copy Markdown
Member

Google requests were always logged with finish reason content_filter, hiding the reason Google actually returned.

Two causes:

  • In parse-provider-response.ts, a response with no candidates set finishReason = "content_filter" before the promptFeedback.blockReason assignment, which is guarded by if (!finishReason). The real reason was therefore always discarded. It is now preserved. When Google reports no reason at all — no candidates, no blockReason, no candidate finish reason — the finish reason is left unset (logged as unknown) rather than guessing at a block; the existing warning still carries the full response body for diagnosis.
  • NO_IMAGE, IMAGE_OTHER and OTHER were mapped to content_filter. NO_IMAGE means generation finished without emitting an image, and the other two are Google's unspecified / forward-compatibility buckets — none is a policy block. They now map to stop on the OpenAI surface and to unknown in the logs, alongside the existing OTHER handling.

This also removes a disagreement between map-finish-reason-to-openai.ts and logs.ts, which classified OTHER differently. Genuine blocks (SAFETY, PROHIBITED_CONTENT, IMAGE_SAFETY, IMAGE_PROHIBITED_CONTENT, BLOCKLIST, SPII, RECITATION, IMAGE_RECITATION, LANGUAGE) are unchanged.

NO_IMAGE and IMAGE_OTHER are added to isExpectedUnknownFinishReason so the new unknown mapping does not log an error on every occurrence.

🤖 Generated with Claude Code

A Google response with no candidates set the finish reason to the generic
"content_filter" before the block-reason assignment ran, so the actual
reason Google returned was discarded and every such request was logged as
"content_filter". Keep the reported reason and only fall back to
"content_filter" when Google gives none.

Also stop classifying NO_IMAGE, IMAGE_OTHER and OTHER as content filters.
NO_IMAGE means generation finished without emitting an image and the other
two are Google's unspecified buckets, so none of them is a policy block.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings August 9, 2026 22:52
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@steebchen
steebchen enabled auto-merge (squash) August 9, 2026 22:52

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Aug 9, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Google finish-reason parsing now preserves provider block reasons before fallback. Non-policy image reasons map to stop and log as UNKNOWN; policy reasons remain content_filter.

Changes

Google finish reason normalization

Layer / File(s) Summary
Preserve Google block reasons
apps/gateway/src/chat/tools/parse-provider-response.ts, apps/gateway/src/chat/tools/parse-provider-response.spec.ts
The parser preserves Google prompt and candidate block reasons before applying the content_filter fallback.
Map Google finish reasons
apps/gateway/src/chat/tools/map-finish-reason-to-openai.ts
Non-policy image and unspecified Google reasons map to stop. Policy-related reasons remain content_filter.
Classify Google log results
apps/gateway/src/lib/logs.ts, apps/gateway/src/lib/logs.spec.ts
Google IMAGE_OTHER, NO_IMAGE, OTHER, and MALFORMED_RESPONSE classify as UNKNOWN. Related expectations are updated.

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

Possibly related PRs

Suggested reviewers: bhuvan2134686

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: preserving Google’s original finish reason in the gateway.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ 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 gemini3-image-content-filter

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.

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

🧹 Nitpick comments (1)
apps/gateway/src/lib/logs.ts (1)

28-39: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add direct coverage for the new expected unknown reasons.

isExpectedUnknownFinishReason now suppresses logging for IMAGE_OTHER and NO_IMAGE. The supplied tests cover only OTHER and MALFORMED_RESPONSE. Add assertions for both new reasons so a later change does not re-enable erroneous unknown-reason logging.

🤖 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 `@apps/gateway/src/lib/logs.ts` around lines 28 - 39, Add direct test
assertions for IMAGE_OTHER and NO_IMAGE in the isExpectedUnknownFinishReason
test coverage, verifying both return the expected unknown-result behavior for
supported providers. Keep the existing OTHER and MALFORMED_RESPONSE assertions
unchanged.
🤖 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.

Nitpick comments:
In `@apps/gateway/src/lib/logs.ts`:
- Around line 28-39: Add direct test assertions for IMAGE_OTHER and NO_IMAGE in
the isExpectedUnknownFinishReason test coverage, verifying both return the
expected unknown-result behavior for supported providers. Keep the existing
OTHER and MALFORMED_RESPONSE assertions unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 3934d21d-8bf4-463d-b9b8-98df96bbfb05

📥 Commits

Reviewing files that changed from the base of the PR and between c1cd97b and 0bcdb45.

📒 Files selected for processing (5)
  • apps/gateway/src/chat/tools/map-finish-reason-to-openai.ts
  • apps/gateway/src/chat/tools/parse-provider-response.spec.ts
  • apps/gateway/src/chat/tools/parse-provider-response.ts
  • apps/gateway/src/lib/logs.spec.ts
  • apps/gateway/src/lib/logs.ts

@steebchen
steebchen merged commit c78b726 into main Aug 9, 2026
12 checks passed
@steebchen
steebchen deleted the gemini3-image-content-filter branch August 9, 2026 23:11
steebchen added a commit that referenced this pull request Aug 9, 2026
Follow-up to #3516.

That PR stopped the generic `content_filter` label from overwriting the
reason Google actually returned, but it kept a fallback: when a Google
response has no candidates, no `promptFeedback.blockReason` and no
candidate finish reason, it still recorded `content_filter`.

That is the same guess the rest of the change removes, made with even
less evidence — Google reported nothing at all, so there is nothing to
suggest a policy block. The finish reason is now left unset, which
`getUnifiedFinishReason` already maps to `unknown`.

No other behaviour changes:

- The existing `logger.warn` for this case still fires and carries the
full response body.
- `hasEmptyNonStreamingResponse` is gated on a truthy finish reason, so
an empty response here is treated exactly as before (`content_filter`
was explicitly excluded from that check).
- The "Unknown finish reason encountered" error log is gated on a truthy
finish reason, so it does not fire.

The only client-visible difference is that `finish_reason` becomes
`stop` instead of `content_filter` for a response that carried no
information at all.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Improved handling of blocked Google responses when no candidates or
block reason are provided.
* Finish reasons now accurately reflect available response information
instead of defaulting to a content-filter status.
  * Diagnostic warnings remain available for affected responses.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.

2 participants