Skip to content

fix: avoid filtered providers on moderation failure#1985

Open
steebchen wants to merge 9 commits intomainfrom
moderation-failure-routing
Open

fix: avoid filtered providers on moderation failure#1985
steebchen wants to merge 9 commits intomainfrom
moderation-failure-routing

Conversation

@steebchen
Copy link
Copy Markdown
Member

@steebchen steebchen commented Apr 7, 2026

This change stops gateway routing from falling through to content-filter-sensitive providers when the OpenAI moderation check is unavailable. If another eligible provider exists, routing now prefers it; if not, the request returns 503 instead of using the sensitive provider. The PR also records moderation-unavailable routing metadata, updates the shared log card, and adds regression coverage for reroute and block behavior. Verification: targeted Vitest coverage passed, then pnpm format and pnpm build passed.

Summary by CodeRabbit

  • Bug Fixes

    • Improved handling of moderation outages: requests reroute to non-filter providers when moderation is unavailable; if none exist, requests are blocked with 503 and an outage log. Providers excluded for moderation failures are no longer selected during fallback/auto-routing.
  • New Features

    • Routing metadata now records moderation-unavailable status and per-provider exclusion reasons, merged across routing stages. Content-filter checks now surface an explicit "unavailable" indicator.
  • UI

    • Log view shows an amber “moderation unavailable” indicator for affected providers.
  • Tests

    • Added tests covering reroute, fallback, and blocking behavior during moderation outages.
  • Chore

    • Beacon payload prefers app version from environment when available.

Copilot AI review requested due to automatic review settings April 7, 2026 18:31
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 7, 2026

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

Detects OpenAI moderation outages and propagates that state into content-filter routing: providers relying on provider-side filtering can be excluded via excludedByModerationFailure, routing metadata now records contentFilterUnavailable, exclusion sets are merged across routing stages, and a pre-upstream guard can persist a block log and return 503 when no safe provider is available.

Changes

Cohort / File(s) Summary
Content filter implementation & tests
apps/gateway/src/chat/tools/openai-content-filter.ts, apps/gateway/src/chat/tools/openai-content-filter.spec.ts
Add unavailable: boolean to OpenAI content-filter results; propagate unavailable from per-request failures; update factory and tests to assert availability semantics for outage/failure cases.
Gateway routing & chat handler
apps/gateway/src/chat/chat.ts
Compute contentFilterMatched, contentFilterUnavailable, and shouldAvoidContentFilterProviders; replace routing APIs to accept these flags; add finalizeContentFilterRoutingMetadata and mergeContentFilterRoutingDecision; thread flags through auto-routing, fallback, and retry; new pre-upstream 503 block/log when moderation is down and only filter-sensitive providers remain; change one logging call to insertLogEntry.
Retry / fallback selection
apps/gateway/src/chat/tools/retry-with-fallback.ts
Extend provider score entries with excludedByModerationFailure?: boolean and update selectNextProvider to skip candidates flagged by either excludedByContentFilter or excludedByModerationFailure.
Routing metadata types & consumer
packages/actions/src/get-cheapest-from-available-providers.ts, packages/shared/src/components/log-card.tsx
Add providerScores[].excludedByModerationFailure?: boolean and top-level contentFilterUnavailable?: boolean to RoutingMetadata; UI shows an amber “moderation unavailable” indicator for affected provider score entries.
Integration tests (fallback behavior)
apps/gateway/src/fallback.spec.ts
Add/adjust tests for OpenAI moderation outage: reroute to non-filter providers with metadata, low-uptime reroute metadata, and block-with-503 case asserting logger warnings, DB log entry, and absence of upstream calls; add test helpers and cleanup of rate-limit state.
Beacon minor change
apps/api/src/routes/beacon.ts
Set properties.version from process.env.APP_VERSION if present (fallback to incoming beaconData.version).

Sequence Diagram

sequenceDiagram
    participant Client
    participant Gateway as Gateway (chat.ts)
    participant Moderation as OpenAI Moderation
    participant Selector as Provider Selector
    participant Upstream as Upstream Provider

    Client->>Gateway: Send chat request
    Gateway->>Moderation: Request moderation check
    alt Moderation unavailable
        Moderation-->>Gateway: { unavailable: true }
        Gateway->>Gateway: set contentFilterUnavailable, mark providerScores.excludedByModerationFailure
        Gateway->>Selector: Request provider list (exclude provider-side filters)
        alt Alternative providers exist
            Selector-->>Gateway: choose non-filter provider
            Gateway->>Upstream: Route request
            Upstream-->>Gateway: Response
            Gateway-->>Client: 200 + routing metadata (contentFilterUnavailable, excluded providers)
        else No alternatives
            Gateway->>Gateway: persist moderation outage block log (503)
            Gateway-->>Client: 503 Service Unavailable
        end
    else Moderation available
        Moderation-->>Gateway: { flagged: true/false }
        Gateway->>Selector: Provide provider scores (mark excludedByContentFilter if flagged)
        Selector-->>Gateway: chosen provider
        Gateway->>Upstream: Route request
        Upstream-->>Gateway: Response
        Gateway-->>Client: Response + routing metadata
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • PR #1936 — Prior content-filter routing changes; this PR extends it with moderation-unavailable handling and new exclusion flags.
  • PR #1651 — Changes to retry/fallback provider selection logic and exclusion propagation; overlaps with selectNextProvider updates.
  • PR #1898 — Earlier OpenAI content-filter integration and logging changes that overlap moderation result handling.

Suggested reviewers

  • smakosh
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 11.76% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix: avoid filtered providers on moderation failure' accurately summarizes the main change: preventing routing to content-filter-sensitive providers when moderation is unavailable, with fallback or blocking behavior.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch moderation-failure-routing

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 and usage tips.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR updates gateway routing to treat OpenAI moderation outages as a signal to avoid providers marked as contentFilter (provider-side filtering), rerouting to other eligible providers when possible and returning a 503 when no non-content-filter provider is available. It also surfaces this decision in routing metadata, updates the shared log UI to display it, and adds regression coverage for reroute vs. block behavior.

Changes:

  • Add moderation-unavailable signals to routing metadata (contentFilterUnavailable, excludedByModerationFailure) and use them in routing/provider retry selection.
  • Update gateway routing logic to avoid contentFilter providers when moderation is unavailable (enabled mode), and hard-block with 503 if no alternatives exist.
  • Extend UI log card and tests to reflect and validate the new moderation-unavailable behavior.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
packages/shared/src/components/log-card.tsx Displays provider-score annotations for moderation-unavailable exclusions and adds metadata fields.
packages/actions/src/get-cheapest-from-available-providers.ts Extends RoutingMetadata typings to carry moderation-unavailable exclusion fields.
apps/gateway/src/fallback.spec.ts Adds regression tests for rerouting away from content-filter providers and blocking when only such providers exist.
apps/gateway/src/chat/tools/retry-with-fallback.ts Ensures retry selection skips providers excluded due to moderation failure as well as content-filter matches.
apps/gateway/src/chat/tools/openai-content-filter.ts Adds unavailable flag to moderation check results and sets it based on per-request success.
apps/gateway/src/chat/tools/openai-content-filter.spec.ts Updates expected results to include the new unavailable field.
apps/gateway/src/chat/chat.ts Implements the core routing change: avoid content-filter providers when moderation is unavailable, add routing metadata, and block with 503 when unavoidable.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 97 to 101
const sorted = [...providerScores].sort((a, b) => a.score - b.score);
for (const score of sorted) {
if (score.excludedByContentFilter) {
if (score.excludedByContentFilter || score.excludedByModerationFailure) {
continue;
}
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

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

selectNextProvider() now skips scores marked excludedByModerationFailure, but there’s no accompanying unit test verifying this new exclusion behavior (the existing spec only covers excludedByContentFilter). Please add a test case to ensure providers excluded due to moderation unavailability are never selected during fallback retries.

Copilot uses AI. Check for mistakes.
Comment on lines 470 to 484
if (moderationRequests.length === 0) {
logModerationResult(context, {
durationMs: Date.now() - startTime,
flagged: false,
model: OPENAI_MODERATION_MODEL,
upstreamRequestId: null,
hasImages: false,
requestCount: 0,
imageRequestCount: 0,
flaggedCategories: [],
results: [],
});

return createFailedOpenAIContentFilterResult();
return createFailedOpenAIContentFilterResult(null, false);
}
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

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

In the moderationRequests.length === 0 path, the code returns createFailedOpenAIContentFilterResult(null, false). Since this isn’t actually a failure case, consider introducing a separate helper (or renaming the existing one) to represent a successful “no moderation needed” result, to avoid confusing semantics around unavailable vs. failure.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (4)
apps/gateway/src/chat/tools/openai-content-filter.ts (2)

523-537: Include unavailable in the aggregate moderation log.

Routing now branches on this flag, but the structured gateway_content_filter event still looks like a normal success. Emitting it here will make reroutes and 503s much easier to diagnose without correlating separate error logs.

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

In `@apps/gateway/src/chat/tools/openai-content-filter.ts` around lines 523 - 537,
The structured moderation log emitted by logModerationResult currently omits the
aggregate "unavailable" flag, so add unavailable: moderationResults.some((r) =>
!r.success) to the object passed into logModerationResult (the call near
logModerationResult(...) in openai-content-filter.ts) so the
gateway_content_filter event includes the overall unavailable state; keep the
rest of the payload unchanged and ensure the value is computed the same way as
the function's return value to keep logs and routing consistent.

341-344: Split the empty-result path from the failure helper.

createFailedOpenAIContentFilterResult(null, false) is now the "nothing to moderate" case. A separate empty-result helper would make the unavailable contract easier to read and harder to misuse later.

Also applies to: 483-483

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

In `@apps/gateway/src/chat/tools/openai-content-filter.ts` around lines 341 - 344,
The helper createFailedOpenAIContentFilterResult conflates "service unavailable"
and "nothing to moderate" by using upstreamRequestId=null and unavailable=false;
split these concerns by keeping
createFailedOpenAIContentFilterResult(upstreamRequestId: string | null,
unavailable = true) solely for true failures/unavailable states and add a new
helper (e.g., createEmptyOpenAIContentFilterResult() or
createNoContentToModerateResult()) that returns the explicit "nothing to
moderate" OpenAIContentFilterCheckResult; update all call sites that currently
call createFailedOpenAIContentFilterResult(null, false) to call the new
empty-result helper, and ensure any callers that represent real failures
continue to use createFailedOpenAIContentFilterResult with unavailable=true so
the contract is clear and not misused.
apps/gateway/src/fallback.spec.ts (1)

1764-1854: Assert the persisted log on the 503 branch too.

The reroute case verifies contentFilterUnavailable / excludedByModerationFailure in the DB log, but the no-eligible-provider case only checks the HTTP response. Adding the log assertion here would protect the log-card path as well.

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

In `@apps/gateway/src/fallback.spec.ts` around lines 1764 - 1854, In the "openai
moderation failure blocks when only content-filter providers are available"
test, after asserting the 503 response and body, also fetch the persisted
request log from the DB and assert it contains the contentFilterUnavailable and
excludedByModerationFailure flags (the same fields checked in the reroute test)
so the "log-card" path is covered; locate this test (the async function that
calls app.request and uses togetherProvider/fetchSpy) and add a DB lookup for
the created request log (using your existing test helper used by other tests to
read stored logs) and assert the persisted log includes
contentFilterUnavailable: true and excludedByModerationFailure: true before
restoring fetchSpy and environment toggles.
packages/shared/src/components/log-card.tsx (1)

50-85: Avoid maintaining a second RoutingMetadata contract here.

This local copy already had to be manually updated for the moderation-outage fields. Moving the type into a shared DTO/module would remove the drift risk between packages/actions/src/get-cheapest-from-available-providers.ts and this component.

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

In `@packages/shared/src/components/log-card.tsx` around lines 50 - 85, The
RoutingMetadata interface in this component duplicates the DTO used elsewhere
and causes drift; remove the local RoutingMetadata declaration and instead
import the shared type exported from the common DTO/module used by
get-cheapest-from-available-providers.ts (or its defining module), update all
references in this file (e.g., props or variables typed as RoutingMetadata) to
use the imported type, and ensure the shared DTO is extended there with the
moderation/outage fields so this component no longer carries a separate
contract.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/gateway/src/chat/chat.ts`:
- Around line 1906-1910: The moderation-outage avoidance (the logic computing
contentFilterUnavailable / shouldAvoidContentFilterProviders and calling
getContentFilterRoutingDecision()/addContentFilterRoutingMetadata()) must run
before any code sets usedProvider so auto-routing and specific-provider
fallbacks honor it; move or duplicate this check so that before usedProvider is
assigned in the auto routing and fallback paths you evaluate contentFilterMode,
contentFilterMatched, and openAIContentFilterResult?.unavailable and then call
getContentFilterRoutingDecision() and addContentFilterRoutingMetadata() as
appropriate; update the branches that set usedProvider (including the auto route
and the specific-provider fallback code paths referenced) to respect the
computed shouldAvoidContentFilterProviders value instead of running the check
later.
- Around line 2996-2999: Move the moderation-outage guard (the
contentFilterSensitiveProviderBlocked check) to execute before
checkProviderRateLimit() so we don't consume a provider slot for requests
blocked by content-moderation outage; when blocking, explicitly write the
request log (call the existing request-log insertion routine used for
successful/other-failed requests) including routing metadata and then throw the
HTTPException(503) so the blocked request appears in logs; also add a clear log
entry (logger.warn/error) describing the moderation outage and reference this
same change for the duplicate guard around the 3140-3145 area.

---

Nitpick comments:
In `@apps/gateway/src/chat/tools/openai-content-filter.ts`:
- Around line 523-537: The structured moderation log emitted by
logModerationResult currently omits the aggregate "unavailable" flag, so add
unavailable: moderationResults.some((r) => !r.success) to the object passed into
logModerationResult (the call near logModerationResult(...) in
openai-content-filter.ts) so the gateway_content_filter event includes the
overall unavailable state; keep the rest of the payload unchanged and ensure the
value is computed the same way as the function's return value to keep logs and
routing consistent.
- Around line 341-344: The helper createFailedOpenAIContentFilterResult
conflates "service unavailable" and "nothing to moderate" by using
upstreamRequestId=null and unavailable=false; split these concerns by keeping
createFailedOpenAIContentFilterResult(upstreamRequestId: string | null,
unavailable = true) solely for true failures/unavailable states and add a new
helper (e.g., createEmptyOpenAIContentFilterResult() or
createNoContentToModerateResult()) that returns the explicit "nothing to
moderate" OpenAIContentFilterCheckResult; update all call sites that currently
call createFailedOpenAIContentFilterResult(null, false) to call the new
empty-result helper, and ensure any callers that represent real failures
continue to use createFailedOpenAIContentFilterResult with unavailable=true so
the contract is clear and not misused.

In `@apps/gateway/src/fallback.spec.ts`:
- Around line 1764-1854: In the "openai moderation failure blocks when only
content-filter providers are available" test, after asserting the 503 response
and body, also fetch the persisted request log from the DB and assert it
contains the contentFilterUnavailable and excludedByModerationFailure flags (the
same fields checked in the reroute test) so the "log-card" path is covered;
locate this test (the async function that calls app.request and uses
togetherProvider/fetchSpy) and add a DB lookup for the created request log
(using your existing test helper used by other tests to read stored logs) and
assert the persisted log includes contentFilterUnavailable: true and
excludedByModerationFailure: true before restoring fetchSpy and environment
toggles.

In `@packages/shared/src/components/log-card.tsx`:
- Around line 50-85: The RoutingMetadata interface in this component duplicates
the DTO used elsewhere and causes drift; remove the local RoutingMetadata
declaration and instead import the shared type exported from the common
DTO/module used by get-cheapest-from-available-providers.ts (or its defining
module), update all references in this file (e.g., props or variables typed as
RoutingMetadata) to use the imported type, and ensure the shared DTO is extended
there with the moderation/outage fields so this component no longer carries a
separate contract.
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 2b4f4198-8fce-45e8-9040-d71e265c5a56

📥 Commits

Reviewing files that changed from the base of the PR and between 72569f6 and 152a8bd.

📒 Files selected for processing (7)
  • apps/gateway/src/chat/chat.ts
  • apps/gateway/src/chat/tools/openai-content-filter.spec.ts
  • apps/gateway/src/chat/tools/openai-content-filter.ts
  • apps/gateway/src/chat/tools/retry-with-fallback.ts
  • apps/gateway/src/fallback.spec.ts
  • packages/actions/src/get-cheapest-from-available-providers.ts
  • packages/shared/src/components/log-card.tsx

@steebchen steebchen added this pull request to the merge queue Apr 7, 2026
@steebchen steebchen removed this pull request from the merge queue due to a manual request Apr 7, 2026
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e8d62dec40

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +2893 to +2896
const contentFilterSensitiveProviderBlocked =
contentFilterMode === "enabled" &&
contentFilterUnavailable &&
isContentFilterProvider(usedProvider);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Re-check fallback candidates before blocking on moderation outage

This new contentFilterSensitiveProviderBlocked gate can return 503 even when a non-content-filter provider is still eligible, because the requested-provider fallback paths (rate-limit/low-uptime rerouting in chat.ts) select the cheapest provider without applying getContentFilterRoutingDecision. In a moderation outage, that can pick a content-filter provider first and then hard-block here, causing avoidable outages instead of routing to a safe alternative. Please re-run content-filter-aware candidate selection (or reselect) before throwing this 503.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
apps/gateway/src/chat/chat.ts (1)

2319-2322: ⚠️ Potential issue | 🟠 Major

Moderation-outage rerouting is still skipped when usedProvider is preselected.

At Line 2319, getContentFilterRoutingDecision() only runs in the if (!usedProvider) flow. Auto-routing and specific-provider resolution can set usedProvider earlier, so moderation outages can still fall through to contentFilterSensitiveProviderBlocked and return 503 even when a non-sensitive alternative exists.

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

In `@apps/gateway/src/chat/chat.ts` around lines 2319 - 2322, The code only
computes contentFilterRoutingDecision via getContentFilterRoutingDecision when
usedProvider is falsy, causing moderation-outage rerouting to be skipped if
usedProvider was preselected; update the flow to always call
getContentFilterRoutingDecision(availableModelProviders,
shouldAvoidContentFilterProviders) regardless of usedProvider and then use its
result to override or reroute when the provider is blocked by
contentFilterSensitiveProviderBlocked (and similar checks) so a non-sensitive
alternative can be selected; locate the usedProvider assignment and the
contentFilterSensitiveProviderBlocked check in chat.ts and ensure
contentFilterRoutingDecision is computed before those checks and applied to
choose an alternative provider when the current usedProvider is impacted.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/gateway/src/fallback.spec.ts`:
- Around line 1658-1764: Add assertions to ensure the sensitive upstream
(mockServerUrl) is not contacted by checking the spied fetch calls: after
sending the request in the "openai moderation failure reroutes away from
content-filter providers" test (the test function in which fetchSpy is created),
assert that fetch was called exactly once for mockServerUrl in the reroute case
(expect(fetchSpy.mock.calls.filter(c => c[0] && (typeof c[0] === "string" ? c[0]
: c[0].url) === mockServerUrl).length).toBe(1)) and in the related block-case
test assert the same filter count is 0; place these assertions after the
response checks and before fetchSpy.mockRestore(), referencing the existing
fetchSpy and mockServerUrl identifiers so the tests prove the upstream was
skipped.

---

Duplicate comments:
In `@apps/gateway/src/chat/chat.ts`:
- Around line 2319-2322: The code only computes contentFilterRoutingDecision via
getContentFilterRoutingDecision when usedProvider is falsy, causing
moderation-outage rerouting to be skipped if usedProvider was preselected;
update the flow to always call
getContentFilterRoutingDecision(availableModelProviders,
shouldAvoidContentFilterProviders) regardless of usedProvider and then use its
result to override or reroute when the provider is blocked by
contentFilterSensitiveProviderBlocked (and similar checks) so a non-sensitive
alternative can be selected; locate the usedProvider assignment and the
contentFilterSensitiveProviderBlocked check in chat.ts and ensure
contentFilterRoutingDecision is computed before those checks and applied to
choose an alternative provider when the current usedProvider is impacted.
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: b01596c1-73aa-4696-aaf3-fd9f6efbeb1d

📥 Commits

Reviewing files that changed from the base of the PR and between 152a8bd and e8d62de.

📒 Files selected for processing (2)
  • apps/gateway/src/chat/chat.ts
  • apps/gateway/src/fallback.spec.ts

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9fe0c9bc98

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +3116 to +3119
if (contentFilterSensitiveProviderBlocked) {
const moderationOutageMessage =
"OpenAI moderation is unavailable and no eligible provider without provider-side content filtering is available.";
const baseLogEntry = createLogEntry(
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Evaluate content-filter blocks before provider limit checks

This new early-return path handles only contentFilterSensitiveProviderBlocked, but regular contentFilterBlocked requests are now evaluated later (after provider RPM consumption and retention-credit checks). In the contentFilterMatched && !contentFilterRoutingApplied case, the request can now decrement provider limits or fail with 402/429 before reaching the gateway content-filter response path, which is a behavior regression from the previous ordering and can incorrectly throttle/deny blocked prompts.

Useful? React with 👍 / 👎.

Comment on lines +3086 to +3089
const contentFilterSensitiveProviderBlocked =
contentFilterMode === "enabled" &&
contentFilterUnavailable &&
isContentFilterProvider(usedProvider);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Do not treat flagged moderation results as outage-only failures

contentFilterSensitiveProviderBlocked is triggered whenever moderation is unavailable and the selected provider is content-filter-sensitive, even if moderation also returned flagged: true. checkOpenAIContentFilter can produce both states (e.g., one moderation sub-request fails while another flags), so this branch can return 503 before the normal contentFilterBlocked flow, changing a definite filter decision into an outage error for mixed-success moderation responses.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
apps/gateway/src/fallback.spec.ts (1)

1681-1811: ⚠️ Potential issue | 🟡 Minor

Assert that the filtered upstream is never contacted.

These cases still only prove the final reroute/503 outcome. They will pass if the gateway briefly calls the content-filter-sensitive upstream before rerouting or blocking, which is the core regression this PR is trying to prevent.

🧪 Tighten the regression assertions
 				expect(res.status).not.toBe(503);
 				expect(fetchSpy).toHaveBeenCalled();
+				const rerouteUpstreamCalls = fetchSpy.mock.calls.filter(([input]) => {
+					const url =
+						typeof input === "string"
+							? input
+							: input instanceof URL
+								? input.toString()
+								: input.url;
+					return url.startsWith(mockServerUrl);
+				});
+				expect(rerouteUpstreamCalls).toHaveLength(1);
...
 				expect(res.status).toBe(503);
+				const blockedUpstreamCalls = fetchSpy.mock.calls.filter(([input]) => {
+					const url =
+						typeof input === "string"
+							? input
+							: input instanceof URL
+								? input.toString()
+								: input.url;
+					return url.startsWith(mockServerUrl);
+				});
+				expect(blockedUpstreamCalls).toHaveLength(0);

Repeat the reroute assertion in the auto-routing and low-uptime-fallback cases as well.

Also applies to: 1813-1918, 1920-2067, 2069-2197

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

In `@apps/gateway/src/fallback.spec.ts` around lines 1681 - 1811, The test "openai
moderation failure reroutes away from content-filter providers" currently only
checks final routing/logs but doesn't assert the content-filtered upstream was
never contacted; update the test to assert the filtered provider was not called
by inspecting fetchSpy calls (e.g., ensure no fetch call made to the together.ai
endpoint or togetherProvider.baseUrl) — use the existing fetchSpy variable and
togetherProvider identifier to check fetchSpy.mock.calls contain no request URLs
matching together.ai (and add the same non-call assertion to the other affected
tests named for auto-routing and low-uptime-fallback so they also verify the
filtered upstream was never contacted).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/gateway/src/chat/chat.ts`:
- Around line 2139-2167: The current routing logic can switch away from the
active usedProvider when every fallback candidate is moderation-sensitive;
instead, if a usedProvider exists and is eligible (not excluded, not
rate-limited, and not itself moderation-sensitive), prefer staying on it rather
than routing into a moderation-sensitive fallback pool. Modify the
candidatesForRouting selection (the block that uses
contentFilterPreferredNonRateLimitedAlternatives,
contentFilterPreferredProviders, nonRateLimitedAlternatives,
availableModelProviders) to first check usedProvider eligibility (check
usedProvider against contentFilterRoutingExcludedProviders,
rateLimitedAlternatives, and whether it appears in
contentFilterPreferredProviders or otherwise is non-moderation-sensitive) and,
if eligible, place usedProvider as the sole/first candidate or return
[usedProvider]; otherwise fall back to the existing multi-branch selection.
Ensure this same check is applied at the other location mentioned (the similar
block at the later lines).

---

Duplicate comments:
In `@apps/gateway/src/fallback.spec.ts`:
- Around line 1681-1811: The test "openai moderation failure reroutes away from
content-filter providers" currently only checks final routing/logs but doesn't
assert the content-filtered upstream was never contacted; update the test to
assert the filtered provider was not called by inspecting fetchSpy calls (e.g.,
ensure no fetch call made to the together.ai endpoint or
togetherProvider.baseUrl) — use the existing fetchSpy variable and
togetherProvider identifier to check fetchSpy.mock.calls contain no request URLs
matching together.ai (and add the same non-call assertion to the other affected
tests named for auto-routing and low-uptime-fallback so they also verify the
filtered upstream was never contacted).
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 3c7d43cc-96a3-4a6a-93a0-facb76f2900f

📥 Commits

Reviewing files that changed from the base of the PR and between e8d62de and 9fe0c9b.

📒 Files selected for processing (2)
  • apps/gateway/src/chat/chat.ts
  • apps/gateway/src/fallback.spec.ts

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 290557d94d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +522 to +525
return {
excludedProviders,
routingApplied: existingRoutingApplied || decision.rerouted,
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Defer content-filter reroute flag until fallback succeeds

routingApplied is set from decision.rerouted before a replacement provider is actually chosen. In the rate-limit and low-uptime fallback paths, getCheapestFromAvailableProviders(...) can still return null (for example when preferred alternatives are filtered out as unstable), leaving usedProvider unchanged, but this flag stays true. Because contentFilterBlocked later gates on !contentFilterRoutingApplied, a matched prompt can skip the gateway content-filter block and continue to the original provider even though no reroute occurred.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
apps/gateway/src/fallback.spec.ts (1)

1745-1750: Reuse getFetchCallUrl() inside the fetch spies.

The same URL-normalization branch is copied four times right below the shared helper. Reusing the helper keeps that parsing logic in one place.

♻️ Deduplicate the URL extraction
- const url =
- 	typeof input === "string"
- 		? input
- 		: input instanceof URL
- 			? input.toString()
- 			: input.url;
+ const url = getFetchCallUrl(input);

Also applies to: 1885-1890, 1998-2003, 2167-2172

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

In `@apps/gateway/src/fallback.spec.ts` around lines 1745 - 1750, The duplicated
URL-normalization logic inside the fetch spies should be replaced by reusing the
existing helper getFetchCallUrl(); locate each instance in the spec where the
code computes url via typeof input === "string" ? input : input instanceof URL ?
input.toString() : input.url (seen in the fetch spy blocks around the failing
tests) and call getFetchCallUrl(input) instead. Update the four spots (the fetch
spy callbacks/assertions) to call getFetchCallUrl rather than repeating the
branch so all URL parsing lives in one place and tests remain consistent.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/gateway/src/fallback.spec.ts`:
- Line 873: Replace the weak status assertion expect(res.status).not.toBe(503)
with a strict success assertion expect(res.status).toBe(200) in the failing
test(s) so the success path is pinned to HTTP 200; update the same pattern at
the other occurrences mentioned (the other expect(res.status).not.toBe(503)
instances) to expect 200 instead.
- Around line 1804-1811: The tests currently only assert routing and token
selection (fetchSpy, getMockServerTokens) but don't assert that the gateway
actually attempted a call to https://api.openai.com/v1/moderations; add an
assertion that one of fetchSpy.mock.calls targeted that exact moderation URL
(e.g., examine the first argument(s) from fetchSpy.mock.calls with
getMockServerCalls(fetchSpy.mock.calls) or inspect call[0].url) to ensure the
moderation request was made; update the same pattern in the other failing blocks
(lines around where getMockServerTokens is checked: the groups at 1919-1926,
2072-2083, 2206-2209) so each outage-path test explicitly expects a call to the
OpenAI moderations endpoint.

---

Nitpick comments:
In `@apps/gateway/src/fallback.spec.ts`:
- Around line 1745-1750: The duplicated URL-normalization logic inside the fetch
spies should be replaced by reusing the existing helper getFetchCallUrl();
locate each instance in the spec where the code computes url via typeof input
=== "string" ? input : input instanceof URL ? input.toString() : input.url (seen
in the fetch spy blocks around the failing tests) and call
getFetchCallUrl(input) instead. Update the four spots (the fetch spy
callbacks/assertions) to call getFetchCallUrl rather than repeating the branch
so all URL parsing lives in one place and tests remain consistent.
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: e41b3177-aafe-4534-b290-865913b9f306

📥 Commits

Reviewing files that changed from the base of the PR and between 9fe0c9b and 290557d.

📒 Files selected for processing (1)
  • apps/gateway/src/fallback.spec.ts

});

expect(res.status).toBe(200);
expect(res.status).not.toBe(503);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Keep the success-path assertions pinned to HTTP 200.

not.toBe(503) only excludes one failure mode. A generic 4xx/5xx regression can still satisfy the status check, which weakens these fallback/reroute tests unnecessarily.

🧪 Keep the contract strict
- expect(res.status).not.toBe(503);
+ expect(res.status).toBe(200);

Also applies to: 1673-1673, 1803-1803

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

In `@apps/gateway/src/fallback.spec.ts` at line 873, Replace the weak status
assertion expect(res.status).not.toBe(503) with a strict success assertion
expect(res.status).toBe(200) in the failing test(s) so the success path is
pinned to HTTP 200; update the same pattern at the other occurrences mentioned
(the other expect(res.status).not.toBe(503) instances) to expect 200 instead.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 54bb162bde

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +3086 to +3089
const contentFilterSensitiveProviderBlocked =
contentFilterMode === "enabled" &&
contentFilterUnavailable &&
isContentFilterProvider(usedProvider);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Re-route sensitive direct requests before outage blocking

This outage gate blocks as soon as the selected provider is content-filter-sensitive, but for direct-provider requests we only evaluate alternative providers in the rate-limit/low-uptime branches. In the common case where the requested sensitive provider is healthy, contentFilterSensitiveProviderBlocked becomes true and returns 503 even when noFallback is not set and a non-sensitive provider is available, creating avoidable outages during moderation API incidents.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e26c302510

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

// Log the filtered request
try {
await insertLog({
await insertLogEntry({
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Log content-filter blocks with moderation metadata

Route this branch through the local insertLog(...) wrapper instead of calling insertLogEntry(...) directly; the wrapper is the only place that attaches gatewayContentFilterResponse (and content-filter tagging) to log records. With the current call site, requests blocked by gateway content filtering are persisted without the moderation payload, which regresses observability/debugging for blocked prompts and can hide why the filter fired in downstream log consumers.

Useful? React with 👍 / 👎.

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