Skip to content

feat(logs): track requested vs used service tier - #2816

Merged
steebchen merged 2 commits into
mainfrom
calgary-v5
Jun 25, 2026
Merged

steebchen merged 2 commits into
mainfrom
calgary-v5

Conversation

@steebchen

@steebchen steebchen commented Jun 25, 2026 •

Copy link
Copy Markdown
Member

What

Logs now record both the service tier the client requested and the tier the provider actually served.

  • Renames the existing log.service_tier column to used_service_tier (it already stored the served tier).
  • Adds a new requested_service_tier column.
  • The gateway writes the requested tier (flex/priority) alongside the served tier on every chat/image/video log, including the unsupported-tier and budget rejection paths.
  • The API log schema (apps/api/src/routes/logs.ts) exposes both fields.
  • The UI activity card (packages/shared/src/components/log-card.tsx) and the log detail view now show Requested Service Tier and Used Service Tier as separate metadata fields. The used tier keeps the cost-multiplier annotation, since billed token costs reflect the served tier.

Why

Google (and others) silently downgrade an unsupported tier to standard, so the served tier can differ from what the client asked for. Tracking both makes that visible in the activity log instead of only showing the served value.

Migration

1782388997_absurd_shinko_yamashiro.sql:

ALTER TABLE "log" RENAME COLUMN "service_tier" TO "used_service_tier";
ALTER TABLE "log" ADD COLUMN "requested_service_tier" text;

The old service_tier column stored the served tier, so it is renamed in place to used_service_tier — existing rows keep their value under the correct field. requested_service_tier is added empty (NULL for historical rows, since the requested tier was not recorded before).

Performance at scale: both statements are catalog-only metadata operations in Postgres — RENAME COLUMN never rewrites the table, and ADD COLUMN ... text with no default is also instant (PG 11+). This runs in well under a second even on a log table with hundreds of millions of rows, with no per-row backfill UPDATE. Only a brief ACCESS EXCLUSIVE lock is held.

Testing

  • pnpm build — all 17 tasks pass
  • pnpm format
  • apps/gateway/src/api.spec.ts service-tier tests updated and passing (3 passed)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Log details now show both the tier that was requested and the tier that was actually used.
    • Activity and log cards display clearer service-tier information with updated labels.
  • Bug Fixes

    • Improved logging for chat, image, and video requests so tier-related details are recorded more accurately.
    • Existing log history is preserved while adding support for the new tier fields.

Rename the log `service_tier` column to `requested_service_tier` and add a
new `used_service_tier` column so logs record both the tier the client
requested and the tier the provider actually served.

The gateway now writes the requested tier (flex/priority) alongside the
served tier on every log; the API exposes both fields and the UI activity
card and log detail view show "Requested Service Tier" and "Used Service
Tier" separately.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 25, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

The PR splits service-tier logging into requested and used fields across the log database schema, gateway log writers, API log responses, and UI displays. Tests and media error logging update to the new field names.

Changes

Service tier logging split

Layer / File(s) Summary
Storage and response schema
packages/db/migrations/1782388997_absurd_shinko_yamashiro.sql, packages/db/migrations/meta/_journal.json, packages/db/src/schema.ts, apps/api/src/routes/logs.ts
The log table migration and schema rename serviceTier to requestedServiceTier, add usedServiceTier, and expose both fields in the logs API schema.
Gateway chat log writes
apps/gateway/src/chat/chat.ts, apps/gateway/src/api.spec.ts
The chat completions path derives requestedServiceTier, writes requested and used tier values in rejection and success logs, and the gateway tests assert the new fields.
Other gateway error logs
apps/gateway/src/images/images.ts, apps/gateway/src/videos/videos.ts
Image and video client-error logs now write requestedServiceTier and usedServiceTier as null.
UI tier display updates
packages/shared/src/components/log-card.tsx, apps/ui/src/app/dashboard/[orgId]/[projectId]/activity/[logId]/log-detail-client.tsx
The shared log card type and log detail cost section render requested and used service-tier values, with used-tier multiplier formatting preserved.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • theopenco/llmgateway#2549: Changes chat-completions service-tier logging in apps/gateway/src/chat/chat.ts, which this PR restructures into requested and used fields.
  • theopenco/llmgateway#2572: Adds the service-tier parsing and forwarding path in apps/gateway/src/chat/chat.ts that feeds the new log fields.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately describes the main change: logging requested versus used service tier.
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.

✏️ 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 calgary-v5

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.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 04101394c8

ℹ️ 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 thread packages/db/migrations/1782388041_round_korath.sql Outdated
Comment on lines +1447 to +1449
const requestedServiceTier = isRequestedServiceTier(service_tier)
? service_tier
: null;

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 Populate service tiers on all chat logs

For chat requests that include service_tier but exit through existing log paths like guardrail/client validation, cache hits, streaming timeouts/upstream errors, or non-streaming fetch errors, those insertLogEntry payloads still omit both requestedServiceTier and usedServiceTier. Because these nullable insert fields default to null, the API/UI will show no requested tier for those error/cached logs even though it is computed here; thread requestedServiceTier through each chat log insert and set usedServiceTier where a provider tier is known.

Useful? React with 👍 / 👎.

Rename the existing `service_tier` column (which stored the served tier) to
`used_service_tier` instead of `requested_service_tier`, so existing log rows
keep their served-tier value under the correct field. RENAME COLUMN is a
catalog-only operation, so this is instant even on a table with hundreds of
millions of rows and needs no backfill UPDATE.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@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: 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 `@packages/db/migrations/1782388997_absurd_shinko_yamashiro.sql`:
- Around line 1-8: This migration appears to have been hand-edited, including a
custom rationale and a manual column rename, so regenerate it through the local
migrations skill workflow instead of keeping the handwritten SQL. Use the
migrations skill to produce the migration and confirm the corresponding snapshot
was created, then replace this file with the skill-generated output for the
`log` table change involving `service_tier`, `used_service_tier`, and
`requested_service_tier`.
🪄 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: d83a21c3-3454-4a3c-8c12-b2bd0062d97b

📥 Commits

Reviewing files that changed from the base of the PR and between 0410139 and 2b9ea17.

📒 Files selected for processing (3)
  • packages/db/migrations/1782388997_absurd_shinko_yamashiro.sql
  • packages/db/migrations/meta/1782388997_snapshot.json
  • packages/db/migrations/meta/_journal.json

Comment thread packages/db/migrations/1782388997_absurd_shinko_yamashiro.sql
@steebchen
steebchen merged commit cbc078f into main Jun 25, 2026
18 checks passed
@steebchen
steebchen deleted the calgary-v5 branch June 25, 2026 12:22
steebchen added a commit that referenced this pull request Jun 25, 2026
Follow-up to #2816, which populated requestedServiceTier / usedServiceTier
only on the chat success paths and the two direct _insertLog rejections.
Other paths (guardrail/validation rejections, cache hits, streaming
timeouts/upstream errors, fetch errors) go through the shared
insertLogEntry wrapper and omitted both fields.

- Default requestedServiceTier + usedServiceTier (from servedServiceTier)
  in the insertLogEntry wrapper so every chat log path records them;
  explicit values still win. Declare servedServiceTier ahead of the
  wrapper and drop the now-redundant explicit fields from the two
  success inserts.
- Reset servedServiceTier at the start of each retry attempt (both the
  streaming and non-streaming loops) so a fallback that fails before
  fetch returns logs no served tier instead of inheriting the previous
  provider's.
- Update the upstream-error test to openai/gpt-5.5 (priority, not subject
  to the upstream base-URL restriction added in #2821) so it still
  reaches the error path, and make the mock /v1/responses handler detect
  TRIGGER_ERROR / status triggers in array-form input.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
steebchen added a commit that referenced this pull request Jun 25, 2026
Surface the requested vs served processing tier (Flex/Priority) on both
the response and every chat log path.

Response:
- Add requested_service_tier + used_service_tier to the response metadata
  for non-streaming completions and the streaming final usage chunk
  (via buildFinalResponseMetadata / toResponseMetadataExtras). Emitted
  only when a premium tier was requested; used_service_tier is null when
  the upstream downgraded to standard.

Logs (follow-up to #2816, which only covered success + the two direct
_insertLog rejections):
- Default requestedServiceTier + usedServiceTier in the insertLogEntry
  wrapper so guardrail/validation rejections, cache hits, streaming
  timeouts/upstream errors and fetch errors record them too. Declare
  servedServiceTier ahead of the wrapper and drop the now-redundant
  explicit fields from the two success inserts.
- Reset servedServiceTier at the start of each retry attempt (both loops)
  so a fallback that fails before fetch returns logs no served tier
  instead of inheriting the previous provider's.

Tests/infra:
- Update the upstream-error test to openai/gpt-5.5 (priority, not subject
  to the upstream base-URL restriction from #2821) so it still reaches
  the error path; make the mock /v1/responses handler array-safe for
  TRIGGER_ERROR / status triggers and echo service_tier in its streamed
  response object.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
pull Bot pushed a commit to soitun/llmgateway that referenced this pull request Jun 26, 2026
## The fix: Vertex Priority/Flex PayGo needs two headers

Vertex requires **both**:
- `X-Vertex-AI-LLM-Shared-Request-Type: priority|flex` — selects the
tier (the gateway already sent this)
- `X-Vertex-AI-LLM-Request-Type: shared` — forces the shared PayGo path
(**was missing**)

Without the second header, a project that has **Provisioned Throughput**
allocated consumes PT first and never reaches the shared Flex/Priority
tier — so the request is silently served (and billed) as **standard**
even though `priority` was requested. This is why production showed
`requested=priority` / `used=null` on `aiplatform.googleapis.com`
projects with PT, while projects without PT worked. We now send both
headers for flex/priority (no-op on projects without PT, so it's safe
everywhere).

Per [Google's
docs](https://docs.cloud.google.com/gemini-enterprise-agent-platform/models/priority-paygo)
and [liteLLM](https://docs.litellm.ai/docs/tutorials/vertex_ai_pay_go):
*"To bypass Provisioned Throughput entirely, also set requestType:
'shared'."*

## Also in this PR: surface & log requested vs served tier

- **Response**: `requested_service_tier` + `used_service_tier` added to
the response `metadata` (non-streaming) and the **streaming final usage
chunk** (via `buildFinalResponseMetadata` / `toResponseMetadataExtras`).
Emitted only when a premium tier was requested; `used` is `null` on a
downgrade so callers can detect it.
- **Logs** (follow-up to theopenco#2816): default both fields in the
`insertLogEntry` wrapper so every chat log path (guardrail/validation
rejections, cache hits, streaming/upstream errors, fetch errors) records
them; **reset `servedServiceTier` per retry attempt** so a fallback that
fails before `fetch` returns logs no served tier instead of inheriting
the previous provider's.

## Tests / infra

- `get-provider-headers` test asserts **both** Vertex headers on
flex/priority and their absence otherwise.
- Upstream-error test uses `openai/gpt-5.5` (not subject to theopenco#2821's
upstream base-URL rule) so it still reaches the error path; mock
`/v1/responses` handler is array-safe for `TRIGGER_ERROR` /
`TRIGGER_STATUS_*` and echoes `service_tier` when streaming.
- New tests: non-streaming metadata present on a priority request /
absent without one; streaming final chunk carries both fields.

## Verification

`pnpm test:unit` (132 files), full `apps/gateway/src/api.spec.ts` (101
passed / 2 skipped), `pnpm format`, full `pnpm build` all pass. The
two-header behavior was confirmed against Google's Priority PayGo docs.

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

Co-authored-by: Claude Opus 4.8 (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.

1 participant