Skip to content

feat(dashboard): surface failing requests so admins see dropped traffic - #449

Open
njbrake wants to merge 3 commits into
mainfrom
fix/317-surfacing-failing-requests
Open

feat(dashboard): surface failing requests so admins see dropped traffic#449
njbrake wants to merge 3 commits into
mainfrom
fix/317-surfacing-failing-requests

Conversation

@njbrake

@njbrake njbrake commented Jul 29, 2026

Copy link
Copy Markdown
Member

Description

An admin had no way to see that the gateway was dropping traffic. The require_pricing
banner told you the config was in a reject state, but not that live requests were actually
failing, or how many.

The premise in the issue was that the data already existed and only needed surfacing. It
turned out that was not quite true for the case the issue leads with. The missing-pricing
gate refunded the budget reservation and raised the 402 without writing a usage row
(_pipeline.py), so a gateway-side rejection left no trace anywhere. A banner counting
status=error would have read "0 failed" at the exact moment unpriced models were being
rejected. Every admin view was blind to it: the activity log, the overview error rate, and
the recent-activity list all showed nothing.

So this is two small changes:

Record the rejection. Both missing-pricing gates (the chat/messages/responses pipeline
and the pass-through routes used by embeddings, images, and rerank) now log a
status="error" usage row with cost=null before raising the 402. The reservation refund
is unchanged, so nothing is billed and nothing stays reserved; the row exists purely to
make the drop visible and countable.

Read that count in the banner. The pricing alarm now shows "N requests failed in the
last hour" with a link into the activity log filtered to those failures
(/activity?status=error&range=1h), so the reject state reads as an active incident
rather than a static config note. It is served by a new useFailureCount hook that polls
through TanStack Query's refetchInterval. The window is resolved inside queryFn rather
than in the query key, which keeps the key stable (a "now"-derived key would mint a new
cache entry on every render) and re-anchors on every refetch, so a tab left open keeps
reporting the last hour instead of quietly widening to everything since it was opened. The
query only runs while the alarm is up.

The count deliberately covers every failure class, not only the pricing rejections. The
operator's question in this state is "is traffic getting through", and over-reporting a
failure is safer than a banner reading "0" while requests are being dropped.

One knock-on fix: the activity detail panel's error summary said "The provider returned an
error." That was accurate when only provider failures were logged. Gateway-side rejections
land in that list now, so the copy is source-neutral ("This request failed."). Provider
diagnostics still stay server-side.

Not included: the issue also floats an "allow unpriced models" toggle in Settings. The
banner's existing "Enable default pricing" button already is that one-click mitigation, so
nothing was added for it.

Verification

Beyond the test suites, this was smoke tested against a running gateway with
require_pricing on and a deliberately fake provider key (the gate rejects before
dispatch, so no real credential is needed):

Check Result
Unpriced chat requests 402 402 402
Unpriced embeddings (pass-through gate) 402
Rejections logged, cost=null 4 rows, correct endpoints
/v1/usage/count?status=error&start_date=… {"total": 4}
Budget untouched after 4 rejections total_spend=0.0, total_reserved=0.0
Budget-exempt key still skips the gate 502, not 402
"Enable default pricing" clears the 402 502, past the gate

In the browser: the banner reported the live count, the link landed on the activity log
with the 1h preset and the Status: Error filter pre-applied, and the count updated itself
from 2 to 7 in 57 seconds with no reload. The Overview page's "Needs attention" strip, the
error-rate tile, and Recent activity all populated as a side effect, having been silent
before.

PR Type

  • New Feature
  • Bug Fix
  • Refactor
  • Documentation
  • Infrastructure / CI

Relevant issues

Fixes #317

Part of #299. Adjacent to #303 (usage and analytics) and #304 (request log viewer): showing
a safe per-row failure reason in the detail panel needs a way to tell a gateway message
from a raw provider message, which belongs with that work rather than here.

Checklist

  • I understand the code I am submitting.
  • I have added or updated tests that cover my change (tests/unit, tests/integration).
  • I ran the Definition of Done checks locally (make lint, make typecheck, make test).
  • Documentation was updated where necessary.
  • If the API contract changed, I regenerated the OpenAPI spec (uv run python scripts/generate_openapi.py).

Tests added: two integration cases in tests/integration/test_require_pricing.py (the chat
gate and the pass-through gate), both confirmed to fail without the backend change, and
five dashboard cases in PricingWarning.test.tsx including one that pins the re-anchoring
behavior (confirmed to fail against a frozen-window implementation). The OpenAPI spec is
unchanged: no route signature or schema moved.

AI Usage

  • No AI was used.
  • AI was used for drafting/refactoring.
  • This is fully AI-generated.

AI Model/Tool used:

Claude Opus 5 via Claude Code.

Any additional AI details you'd like to share:

Written by Claude working from the issue, then reviewed and smoke tested with @njbrake in
the loop. The decision to add the backend logging (rather than ship a banner that would
have counted zero) and the scope boundary around the detail-panel reason were discussed
rather than assumed.

NOTE:
When responding to reviewer questions, please respond yourself rather than copy/pasting reviewer comments into an AI and pasting back its answer. We want to discuss with you, not your AI :)

  • I am an AI Agent filling out this form (check box if true)

Summary

  • Record missing-pricing gateway rejections as failed usage entries with unknown cost while preserving budget refunds across standard and pass-through requests.
  • Add failure visibility to the Pricing Warning banner, including a live last-hour count and filtered Activity link.
  • Support source filtering in Activity and use source-neutral failure details directing administrators to gateway logs.
  • Refresh dashboard bundles and documentation to reflect the updated behavior.

Benefits

Administrators can now identify failed requests in the dashboard and investigate missing-pricing rejections without losing reservation refunds.

Testing

Added coverage for pricing-gate and pass-through logging, failure-count polling, Activity filtering, and dashboard messaging.

Requests the gateway itself refused left no trace. The missing-pricing
gate refunded the budget reservation and raised its 402 without writing
a usage row, so an operator who turned require_pricing on had no way to
see that live traffic was being dropped; they found out when a user
complained. Every admin view was blind to it: the activity log, the
overview error rate, and the recent-activity list all showed nothing.

Log the rejection at both missing-pricing gates (the chat, messages, and
responses pipeline, and the pass-through routes behind embeddings,
images, and rerank) with status=error and cost=null. The reservation
refund is unchanged, so nothing is billed and nothing stays reserved;
the row exists to make the drop visible and countable.

The pricing alarm then reads that count: "N requests failed in the last
hour", plus a link into the activity log filtered to those failures, so
the reject state reads as an active incident rather than a static config
note. A new useFailureCount hook polls it through TanStack Query and
resolves the window inside queryFn rather than in the query key, which
keeps the key stable and re-anchors on every refetch, so a tab left open
keeps reporting the last hour instead of widening to everything since it
was opened. Every failure class is counted, not only the pricing
rejections: the question in this state is whether traffic is getting
through, and over-reporting a failure beats a banner reading "0" while
requests are dropping.

The activity detail panel's error summary no longer claims the provider
returned the error. That was accurate while only provider failures were
logged; gateway-side rejections land in that list now, so the copy is
source-neutral. Provider diagnostics still stay server-side.

Fixes #317
@njbrake
njbrake temporarily deployed to integration-tests July 29, 2026 18:31 — with GitHub Actions Inactive
@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: ca04f749-e628-442d-82ba-ff9513e46f4c

📥 Commits

Reviewing files that changed from the base of the PR and between 0e225d8 and df6ca14.

📒 Files selected for processing (19)
  • docs/dashboard.md
  • src/gateway/static/dashboard/assets/ActivityPage-DPTftSxE.js
  • src/gateway/static/dashboard/assets/AliasesPage-CdcERvTp.js
  • src/gateway/static/dashboard/assets/BudgetsPage-DD21Hfu5.js
  • src/gateway/static/dashboard/assets/ConfirmDialog-DCntQ5Rx.js
  • src/gateway/static/dashboard/assets/DocsPage-CRvfGTKY.js
  • src/gateway/static/dashboard/assets/FilterChips-BcnyddEO.js
  • src/gateway/static/dashboard/assets/KeysPage-DpYcnFvQ.js
  • src/gateway/static/dashboard/assets/ModelScopeControl-CIHD_icU.js
  • src/gateway/static/dashboard/assets/ModelsPage-CDGUjauc.js
  • src/gateway/static/dashboard/assets/OverviewPage-KEawx4D6.js
  • src/gateway/static/dashboard/assets/ProvidersPage-FBow933C.js
  • src/gateway/static/dashboard/assets/SettingsPage-eR11APLm.js
  • src/gateway/static/dashboard/assets/TablePagination-BnFQxBEH.js
  • src/gateway/static/dashboard/assets/ToolsGuardrailsPage-B2ogbWmR.js
  • src/gateway/static/dashboard/assets/UsagePage-5E7SwtJ3.js
  • src/gateway/static/dashboard/assets/UsersPage-CiH8l05o.js
  • src/gateway/static/dashboard/assets/index-C3aWlAKW.js
  • src/gateway/static/dashboard/index.html
🚧 Files skipped from review as they are similar to previous changes (1)
  • docs/dashboard.md

Walkthrough

The gateway now records missing-pricing rejections as usage errors. The dashboard polls and displays recent gateway failures, supports source filtering in Activity, updates related documentation, and refreshes bundled dashboard assets.

Changes

Failure visibility

Layer / File(s) Summary
Record missing-pricing rejections
src/gateway/api/routes/_pipeline.py, src/gateway/api/routes/_passthrough.py, tests/integration/test_require_pricing.py
Missing-pricing HTTP 402 responses now create error usage records, with integration coverage for standard and pass-through requests.
Poll and display recent failures
web/src/api/hooks.ts, web/src/components/PricingWarning.tsx, web/src/components/PricingWarning.test.tsx
The pricing warning polls gateway error counts and displays a singular or plural failed-request message with an Activity link; polling and disabled-state behavior are tested.
Explain failures in activity views
web/src/pages/ActivityPage.tsx, web/src/pages/ActivityPage.test.tsx, docs/dashboard.md
Activity supports URL-backed source filtering and source chips, error details use gateway-neutral wording, and documentation explains refused-request logging.
Rebuild dashboard assets
src/gateway/static/dashboard/assets/*, src/gateway/static/dashboard/index.html
Dashboard page bundles, shared chunks, dependency mappings, and the HTML entrypoint are regenerated or rewired.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

Suggested reviewers: khaledosman

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Title check ⚠️ Warning The title is related to the change, but it does not use the required Conventional Commit prefix format (feat: etc.). Rewrite it to start with a required prefix like feat: and keep the summary concise, e.g. feat: surface failing requests for admins.
Docstring Coverage ⚠️ Warning Docstring coverage is 1.68% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description check ✅ Passed The PR description matches the template sections and includes description, PR type, linked issues, checklist, and AI usage.
Linked Issues check ✅ Passed The backend logging, failure count banner, activity filtering, and source-neutral wording address the core goals of #317.
Out of Scope Changes check ✅ Passed The regenerated dashboard bundles and asset updates appear to support the feature work rather than add unrelated scope.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/317-surfacing-failing-requests
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch fix/317-surfacing-failing-requests

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 requested a review from khaledosman July 29, 2026 18:35

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

Actionable comments posted: 1

🤖 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 `@docs/dashboard.md`:
- Around line 148-150: Update the Activity view description in docs/dashboard.md
to remove the claim that filtering by error status shows failure reasons. State
that failed requests are visible in Activity, while diagnostic details remain
available in the gateway logs.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 26dcc8db-da56-4d2b-90ad-832f4d752f00

📥 Commits

Reviewing files that changed from the base of the PR and between ffad6b5 and 86180b1.

📒 Files selected for processing (34)
  • docs/dashboard.md
  • src/gateway/api/routes/_passthrough.py
  • src/gateway/api/routes/_pipeline.py
  • src/gateway/static/dashboard/assets/ActivityPage-D2jsRt9a.js
  • src/gateway/static/dashboard/assets/ActivityPage-DIACtZv5.js
  • src/gateway/static/dashboard/assets/AliasesPage-CrabMwlU.js
  • src/gateway/static/dashboard/assets/BudgetsPage-DjVks5Lb.js
  • src/gateway/static/dashboard/assets/ConfirmDialog-KQrh5qGO.js
  • src/gateway/static/dashboard/assets/DocsPage-Bo_X9aMz.js
  • src/gateway/static/dashboard/assets/FilterChips-BbDynOdb.js
  • src/gateway/static/dashboard/assets/FilterChips-DZjxf-ot.js
  • src/gateway/static/dashboard/assets/KeysPage--hq_RPOL.js
  • src/gateway/static/dashboard/assets/ModelScopeControl-DYPdGezt.js
  • src/gateway/static/dashboard/assets/ModelsPage-Dt9ZAYGy.js
  • src/gateway/static/dashboard/assets/OverviewPage-BedTU_aZ.js
  • src/gateway/static/dashboard/assets/OverviewPage-DPI1n-nt.js
  • src/gateway/static/dashboard/assets/ProvidersPage-BP60GuZv.js
  • src/gateway/static/dashboard/assets/ProvidersPage-CC2Glu9P.js
  • src/gateway/static/dashboard/assets/SettingsPage-CYDMBdEO.js
  • src/gateway/static/dashboard/assets/SettingsPage-DR0J2MHH.js
  • src/gateway/static/dashboard/assets/TablePagination-BR55oZ8k.js
  • src/gateway/static/dashboard/assets/ToolsGuardrailsPage-B68S03Js.js
  • src/gateway/static/dashboard/assets/UsagePage-BYqtE3tC.js
  • src/gateway/static/dashboard/assets/UsagePage-Ce_jAzFl.js
  • src/gateway/static/dashboard/assets/UsersPage-CZXIBvdD.js
  • src/gateway/static/dashboard/assets/index-Br3lxdY5.js
  • src/gateway/static/dashboard/assets/index-DV8qvdUj.js
  • src/gateway/static/dashboard/index.html
  • tests/integration/test_require_pricing.py
  • web/src/api/hooks.ts
  • web/src/components/PricingWarning.test.tsx
  • web/src/components/PricingWarning.tsx
  • web/src/pages/ActivityPage.test.tsx
  • web/src/pages/ActivityPage.tsx
💤 Files with no reviewable changes (7)
  • src/gateway/static/dashboard/assets/UsagePage-Ce_jAzFl.js
  • src/gateway/static/dashboard/assets/SettingsPage-DR0J2MHH.js
  • src/gateway/static/dashboard/assets/ActivityPage-DIACtZv5.js
  • src/gateway/static/dashboard/assets/FilterChips-DZjxf-ot.js
  • src/gateway/static/dashboard/assets/ProvidersPage-BP60GuZv.js
  • src/gateway/static/dashboard/assets/index-DV8qvdUj.js
  • src/gateway/static/dashboard/assets/OverviewPage-BedTU_aZ.js

Comment thread docs/dashboard.md Outdated
Comment on lines +148 to +150
Requests the gateway refused are logged too, so filtering to the `error`
status shows what is being dropped and why (for example a model with no
pricing under `require_pricing`).

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Do not claim the Activity view shows failure reasons.

Line 148 says the error filter shows why traffic was dropped, but the detail panel deliberately renders only “This request failed” and directs operators to gateway logs. State that failed requests are visible in Activity and that diagnostics remain in gateway logs.

🤖 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 `@docs/dashboard.md` around lines 148 - 150, Update the Activity view
description in docs/dashboard.md to remove the claim that filtering by error
status shows failure reasons. State that failed requests are visible in
Activity, while diagnostic details remain available in the gateway logs.

The count read every status=error row, but imported usage carries that
status too: /v1/usage/external-events documents "error" as a first-class
value. So a Claude Code session with 5 failed calls made the banner claim
the gateway had dropped 5 requests it never saw. Importing 5 failures
alongside 2 real 402s reported 7 instead of 2, turning an incident signal
into a false alarm.

Scope the count to source=gateway, which is sound because RESERVED_SOURCES
forbids an import from claiming that source. Carry the same scope on the
drill-down link and teach the activity page to honor a source param, so
the filtered view reports the number the banner just claimed instead of a
larger one. The param has no select of its own (it arrives from the
drill-down), so it surfaces as a clearable chip rather than as hidden
filter state.
@njbrake
njbrake temporarily deployed to integration-tests July 29, 2026 21:11 — with GitHub Actions Inactive
The Activity entry said filtering to the error status shows what is being
dropped "and why", but the request detail panel deliberately renders only
"This request failed. Inspect gateway logs for details.": it cannot tell a
safe gateway message from a raw provider one, so it shows neither. The doc
promised something the UI withholds by design.

Say that refused requests are visible there, and that diagnostic detail
stays in the gateway logs. Flagged by CodeRabbit on #449.

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.

Pull request overview

This PR improves operator visibility into dropped traffic by ensuring gateway-side missing-pricing rejections are recorded in UsageLog as failed requests (with cost=null), and by surfacing a live “failed in the last hour” signal in the dashboard’s pricing warning with a drill-down link into Activity filtered to those failures.

Changes:

  • Log require_pricing missing-pricing rejections as status="error" usage rows (budget reservation still refunded; no spend recorded).
  • Add a polling useFailureCount hook and extend the PricingWarning banner to show last-hour failure count + link to filtered Activity.
  • Make Activity’s failure summary source-neutral and add a URL-driven source filter displayed as a clearable chip; update docs accordingly.

Reviewed changes

Copilot reviewed 26 out of 34 changed files in this pull request and generated no comments.

Show a summary per file
File Description
web/src/pages/ActivityPage.tsx Adds source URL filter support (chip-only), threads it through usage queries, and updates failure copy to be source-neutral.
web/src/pages/ActivityPage.test.tsx Tests the source drill-down param and validates source-neutral error summary text.
web/src/components/PricingWarning.tsx Adds live last-hour failure count (polled) and a drill-down link to Activity scoped to gateway failures.
web/src/components/PricingWarning.test.tsx Adds coverage for polling behavior, window re-anchoring, link scope, and quiet-alarm behavior.
web/src/api/hooks.ts Introduces useFailureCount using TanStack Query polling and an anchored rolling window resolved inside queryFn.
tests/integration/test_require_pricing.py Adds integration regression tests asserting missing-pricing 402s are logged as error usage rows with cost=null.
src/gateway/api/routes/_pipeline.py Logs missing-pricing rejections (chat/messages/responses pipeline) before raising 402, without changing refund behavior.
src/gateway/api/routes/_passthrough.py Logs missing-pricing rejections for pass-through routes (e.g. embeddings) before raising 402, without changing refund behavior.
docs/dashboard.md Documents that gateway-refused requests are also visible in Activity (with diagnostics remaining server-side).
src/gateway/static/dashboard/index.html Updates dashboard bundle entrypoint hash.
src/gateway/static/dashboard/assets/UsagePage-Ce_jAzFl.js Removes old built dashboard chunk (bundle refresh).
src/gateway/static/dashboard/assets/UsagePage-5E7SwtJ3.js Adds refreshed built dashboard chunk reflecting new UI behavior.
src/gateway/static/dashboard/assets/ToolsGuardrailsPage-B2ogbWmR.js Bundle refresh referencing new index chunk hash.
src/gateway/static/dashboard/assets/TablePagination-BnFQxBEH.js Bundle refresh referencing new index chunk hash.
src/gateway/static/dashboard/assets/SettingsPage-eR11APLm.js Adds refreshed built Settings page chunk (bundle refresh).
src/gateway/static/dashboard/assets/SettingsPage-DR0J2MHH.js Removes old built Settings page chunk (bundle refresh).
src/gateway/static/dashboard/assets/ProvidersPage-FBow933C.js Adds refreshed built Providers page chunk (bundle refresh).
src/gateway/static/dashboard/assets/ProvidersPage-BP60GuZv.js Removes old built Providers page chunk (bundle refresh).
src/gateway/static/dashboard/assets/OverviewPage-KEawx4D6.js Adds refreshed built Overview page chunk (bundle refresh).
src/gateway/static/dashboard/assets/OverviewPage-BedTU_aZ.js Removes old built Overview page chunk (bundle refresh).
src/gateway/static/dashboard/assets/ModelScopeControl-CIHD_icU.js Bundle refresh referencing new index chunk hash.
src/gateway/static/dashboard/assets/FilterChips-DZjxf-ot.js Removes old built FilterChips chunk (bundle refresh).
src/gateway/static/dashboard/assets/FilterChips-BcnyddEO.js Adds refreshed built FilterChips chunk (bundle refresh).
src/gateway/static/dashboard/assets/ConfirmDialog-DCntQ5Rx.js Bundle refresh referencing new index chunk hash.
src/gateway/static/dashboard/assets/AliasesPage-CdcERvTp.js Bundle refresh referencing new index chunk hash.
src/gateway/static/dashboard/assets/ActivityPage-DPTftSxE.js Adds refreshed built Activity page chunk reflecting new source filter and source-neutral copy.
src/gateway/static/dashboard/assets/ActivityPage-DIACtZv5.js Removes old built Activity page chunk (bundle refresh).
Files not reviewed (7)
  • src/gateway/static/dashboard/assets/ActivityPage-DPTftSxE.js: Generated file
  • src/gateway/static/dashboard/assets/FilterChips-BcnyddEO.js: Generated file
  • src/gateway/static/dashboard/assets/OverviewPage-KEawx4D6.js: Generated file
  • src/gateway/static/dashboard/assets/ProvidersPage-FBow933C.js: Generated file
  • src/gateway/static/dashboard/assets/SettingsPage-eR11APLm.js: Generated file
  • src/gateway/static/dashboard/assets/UsagePage-5E7SwtJ3.js: Generated file
  • src/gateway/static/dashboard/assets/index-C3aWlAKW.js: Generated file

@khaledosman khaledosman left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The premise correction is the valuable part here: the issue assumed the data already existed and it did not. A banner over status=error without the backend write would have read "0 failed" at precisely the moment traffic was being dropped. Catching that before building the UI, and covering both gates rather than only the one the issue names, is the right call.

I checked the budget-safety property this change could plausibly have broken: both gates are guarded by not budget_exempt, so counts_toward_budget=not budget_exempt is always True on these rows. They cannot be mistaken for imported rows, which means bulk delete and set-price cannot reach them and the source=gateway count does not miss them. That invariant deserves a test (see inline).

Five non-blocking notes inline.

Landing order: this, #450, and #451 all edit docs/dashboard.md and rewrite the same hashed bundle assets, so whoever lands second needs a rebase plus npm --prefix web run build.

🤖 Generated with Claude Code

# Record the rejection so dropped traffic is visible in the activity
# log and countable as an error, rather than only reaching the
# operator as a user complaint. cost stays null: nothing was spent.
await log_writer.put(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is the third hand-built UsageLog literal in this file (also the success row at ~263 and the provider-error row at ~290), all eleven fields repeated. A local _usage_row(status, **overrides) helper would make the next field addition a one-line change instead of three sites to keep in sync.

api_key_id=api_key_id,
user_id=user_id,
timestamp=datetime.now(UTC),
model=resolved.model,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Logs model=resolved.model (bare, e.g. text-embedding-3-small) where _pipeline.py logs model=model, the full selector (openai:gpt-4o). Each matches its own file's success path, so this PR does not introduce the split, but it does extend it to the new error rows: in the Activity log a rejected embeddings row and a rejected chat row render their Model column differently, and the model filter matches one form or the other.

Not necessarily worth changing here, but the new tests do not pin it either (the chat test asserts the model string, the passthrough one does not), so asserting the expected embeddings model would at least record which form is intended.


rows = c.get("/v1/usage", params={"status": "error"}, headers=_MASTER_HEADER).json()
assert len(rows) == 1
assert rows[0]["endpoint"] == "/v1/embeddings"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Missing the case that guards the invariant this change depends on: a budget-exempt key skips the gate entirely, so no error row is written and no spend moves. The PR description's smoke table covers it (502, not 402), but this is the one place where a future refactor of the budget_exempt condition would silently start writing rows with counts_toward_budget=False, which the dashboard then classifies as imported and offers up for bulk delete. Cheap to pin here.

Comment thread web/src/api/hooks.ts
staleTime: 0,
// A failed count is not worth surfacing: it sits beside its own alarm, and
// the next poll retries anyway.
retry: false,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Resolving the window inside queryFn rather than in the key is the right call and the re-anchoring test pins it well.

One consequence worth confirming is intended: retry: false plus staleTime: 0 means a transient failure leaves failures.data on the last successful value until the next 60s tick, so the banner can display a stale count for up to a minute after the gateway starts erroring. Advisory number, so probably fine, but it is the opposite of failing loud.

<strong className="font-semibold">
{failureCount.toLocaleString()} {failureCount === 1 ? "request" : "requests"} failed in the last hour.
</strong>{" "}
<NavLink to="/activity?status=error&range=1h&source=gateway" className="underline underline-offset-2">

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

NavLink is Link plus active-state styling, and the active state can never apply here (the banner is never rendered on the activity route it points at). Link expresses the intent without the dead machinery.

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.

Surface failing requests so admins notice missing-billing (and other) rejections

3 participants