feat(models): add discount field - #894
Conversation
- Add discount field to cost calculation results and apply discount multiplier to input, output, cached input, and request costs. - Include discount field in database schema for logs. - Update anthropic models to include discount from environment variable ROUTEWAY_PAID_DISCOUNT. - Display discount information in UI log card component when applicable. - Add tests to verify discount application and absence when no discount is applied. - Refactor cost calculation to handle discount properly and default to undefined when no discount. Co-authored-by: terragon-labs[bot] <terragon-labs[bot]@users.noreply.github.com>
|
Warning Rate limit exceeded@steebchen has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 10 minutes and 31 seconds before requesting another review. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. 📒 Files selected for processing (5)
WalkthroughAdds a discount multiplier across pricing, cost calculation, logging, DB schema, and UI. Costs now factor provider/model discounts; logs persist discount; UI conditionally displays it. Cheapest-model/provider selection incorporates discount. Database gains a discount column and snapshot/journal updates. Minor formatting change in conductor.json. New tests cover discount behavior. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant Client
participant Gateway as Gateway Chat
participant Costs as Costs Calculator
participant DB as DB (log table)
participant UI as Dashboard UI
Client->>Gateway: Chat request
Gateway->>Costs: Compute costs (input/output/cached) + discount
Costs-->>Gateway: Cost object { ..., discount }
Gateway->>DB: Create log { costs..., discount }
Gateway-->>Client: Response (stream/non-stream)
Note over UI,DB: Later, UI fetches log
UI->>DB: Get log entry
DB-->>UI: { ..., discount }
UI-->>UI: If discount && != 1 → show "Discount Applied" %
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested labels
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
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. Comment |
- Add tests to validate discount logic without relying on environment variables. - Replace model-specific tests with generic calculation validation. - Ensure proper handling of discount fields and costs when applicable.
Update `providerId` values to use `routeway-discount` for consistency with discount-support changes in Anthropic models.
- Add `discount` field to `ProviderModelMapping` type. - Update pricing logic to account for discounts in cost calculations. - Enhance test coverage to validate discount behavior across providers.
…models Resolved conflicts by: - Keeping discount field approach for routeway models - Adding missing cachedInputPrice fields - Combining cached token logic with discount support - Merging both test suites for costs functionality
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (10)
conductor.json (1)
3-3: Setup script can overwrite local .env; guard and make copy safer.Consider ensuring CONDUCTOR_ROOT_PATH is set and prevent accidental overwrite of an existing .env.
- "setup": "cp \"$CONDUCTOR_ROOT_PATH/.env\" .\n\npnpm install" + "setup": "if [ -z \"${CONDUCTOR_ROOT_PATH:-}\" ]; then echo 'CONDUCTOR_ROOT_PATH not set' >&2; exit 1; fi\ncp -n \"$CONDUCTOR_ROOT_PATH/.env\" .\npnpm install"packages/models/src/types.ts (1)
253-261: Add discount field to ModelWithPricing providers — LGTMType shape aligns with ProviderModelMapping. Consider documenting that this is a multiplier in [0, 1], where 1 = no discount.
packages/db/migrations/1758325232_happy_bloodstrike.sql (1)
1-1: Migration adds log.discount — verify it was generated, and consider a range constraint
- Confirm this .sql came from the standard generator (per guidelines).
- Optionally enforce a CHECK (0 ≤ discount ≤ 1) via schema so future migrations carry it.
Would you like me to add a Drizzle check constraint in schema.ts and regenerate the migration?
packages/models/src/models.ts (1)
46-49: Clarify discount semantics in docsRecommend tightening the comment to state allowed range and default behavior (omit or 1 = no discount).
Apply this diff to improve the doc:
- /** - * Discount multiplier (0-1), where 0.5 = 50% off - */ + /** + * Discount multiplier in [0, 1]. Use 1 or omit for no discount (e.g., 0.5 = 50% off). + */packages/models/src/get-cheapest-model-for-provider.ts (1)
29-36: Cheapest computation ignores requestPrice/image/cached componentsThe function’s doc says “based on input + output” so this may be intentional; just flagging that providers with non‑zero requestPrice won’t be reflected here while other helpers may factor them in.
Do you want this helper to remain “avg(io) only”, or to mirror the fuller price heuristic used elsewhere?
packages/db/src/schema.ts (1)
371-372: Schema column for discount — LGTMNullable real fits the logging use (only present when discount ≠ 1). If you later want enforcement, consider a CHECK constraint in schema and regen migrations.
apps/gateway/src/chat/chat.ts (1)
2732-2732: Coalesce to null to avoid inserting undefinedSmall robustness tweak so the column is explicitly null when no discount applies.
Apply this diff:
- discount: costs.discount, + discount: costs.discount ?? null,Please confirm calculateCosts only returns a value when multiplier ≠ 1 and clamps to [0,1].
Also applies to: 3124-3124
apps/ui/src/components/dashboard/log-card.tsx (1)
288-297: Don’t hide 0 (100% off); avoid truthy checkThe current condition
log.discount && ...suppresses display when discount === 0. Use explicit null/undefined checks and clamp percentage.Apply this diff:
- {log.discount && log.discount !== 1 && ( + {log.discount !== null && log.discount !== undefined && log.discount !== 1 && ( <> <div className="text-muted-foreground"> Discount Applied </div> <div className="font-medium text-green-600"> - {((1 - log.discount) * 100).toFixed(0)}% off + {Math.max(0, Math.min(100, (1 - log.discount) * 100)).toFixed(0)}% off </div> </> )}apps/gateway/src/lib/costs.spec.ts (1)
135-173: Comprehensive discount testing in cost calculationsThe test validates discount functionality thoroughly:
- Verifies that models without discount have
undefineddiscount field- Tests expected cost calculations with discount multipliers
- Validates that discount field is properly excluded when not applicable
However, there appears to be some test duplication and complexity due to environment variable timing issues.
Consider simplifying the test by creating mock provider data with explicit discount values instead of relying on environment variables, which can make tests more predictable and easier to understand.
packages/models/src/models/anthropic.ts (1)
52-52: Consider adding validation for the discount environment variableWhile the current implementation with
parseFloat()and fallback to "1" is functional, consider adding validation to ensure the discount value is within expected bounds (e.g., 0 < discount ≤ 1) and handle potential parsing errors more explicitly.Consider adding validation logic such as:
const rawDiscount = process.env.ROUTEWAY_PAID_DISCOUNT; const parsedDiscount = rawDiscount ? parseFloat(rawDiscount) : 1; const discount = !isNaN(parsedDiscount) && parsedDiscount > 0 && parsedDiscount <= 1 ? parsedDiscount : 1;This would provide better error handling and ensure discount values are within sensible bounds.
Also applies to: 89-89, 170-170, 208-208, 247-247
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (15)
apps/gateway/src/chat/chat.ts(8 hunks)apps/gateway/src/lib/costs.spec.ts(1 hunks)apps/gateway/src/lib/costs.ts(5 hunks)apps/ui/src/components/dashboard/log-card.tsx(1 hunks)conductor.json(1 hunks)packages/db/migrations/1758325232_happy_bloodstrike.sql(1 hunks)packages/db/migrations/meta/1758325232_snapshot.json(1 hunks)packages/db/migrations/meta/_journal.json(1 hunks)packages/db/src/schema.ts(1 hunks)packages/models/src/get-cheapest-from-available-providers.ts(2 hunks)packages/models/src/get-cheapest-model-for-provider.ts(2 hunks)packages/models/src/models.spec.ts(2 hunks)packages/models/src/models.ts(1 hunks)packages/models/src/models/anthropic.ts(5 hunks)packages/models/src/types.ts(1 hunks)
🧰 Additional context used
📓 Path-based instructions (9)
{apps/api,apps/gateway,apps/ui,apps/docs,packages}/**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
Always use top-level import; never use require() or dynamic imports (e.g., import(), next/dynamic)
Files:
packages/models/src/types.tspackages/models/src/get-cheapest-model-for-provider.tspackages/db/src/schema.tsapps/ui/src/components/dashboard/log-card.tsxpackages/models/src/models.spec.tsapps/gateway/src/chat/chat.tspackages/models/src/get-cheapest-from-available-providers.tspackages/models/src/models.tsapps/gateway/src/lib/costs.spec.tsapps/gateway/src/lib/costs.tspackages/models/src/models/anthropic.ts
**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{ts,tsx}: Never useanyoras anyin this TypeScript project unless absolutely necessary
Always use top-levelimport; never userequireor dynamic imports
Files:
packages/models/src/types.tspackages/models/src/get-cheapest-model-for-provider.tspackages/db/src/schema.tsapps/ui/src/components/dashboard/log-card.tsxpackages/models/src/models.spec.tsapps/gateway/src/chat/chat.tspackages/models/src/get-cheapest-from-available-providers.tspackages/models/src/models.tsapps/gateway/src/lib/costs.spec.tsapps/gateway/src/lib/costs.tspackages/models/src/models/anthropic.ts
{apps/api,apps/gateway,packages/db}/**/*.ts
📄 CodeRabbit inference engine (AGENTS.md)
.findMany() or db().query.
{apps/api,apps/gateway,packages/db}/**/*.ts: Use Drizzle ORM with the latest object syntax for database access
For reads, use db().query..findFirst() Files:
packages/db/src/schema.tsapps/gateway/src/chat/chat.tsapps/gateway/src/lib/costs.spec.tsapps/gateway/src/lib/costs.tspackages/db/**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Use Drizzle ORM with the latest object syntax for schema and DB utilities
Files:
packages/db/src/schema.tsapps/ui/**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
Use next/link for links and next/navigation’s router for programmatic navigation
apps/ui/**/*.{ts,tsx}: Use next/link for links and next/navigation's router for programmatic navigation in the UI
Use cookies for user settings not saved in the database to ensure SSR worksFiles:
apps/ui/src/components/dashboard/log-card.tsx**/*.spec.ts
📄 CodeRabbit inference engine (AGENTS.md)
Place unit tests in files named *.spec.ts
Unit test files must be named with the .spec.ts suffix
Files:
packages/models/src/models.spec.tsapps/gateway/src/lib/costs.spec.ts{apps/api,apps/gateway}/src/**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
For reads, use db().query.
.findMany() or db().query.
.findFirst() Files:
apps/gateway/src/chat/chat.tsapps/gateway/src/lib/costs.spec.tsapps/gateway/src/lib/costs.tspackages/db/**/*.sql
📄 CodeRabbit inference engine (AGENTS.md)
Do not write migrations manually; avoid adding .sql files (use pnpm run setup to generate/sync)
Files:
packages/db/migrations/1758325232_happy_bloodstrike.sqlpackages/db/migrations/**/*.sql
📄 CodeRabbit inference engine (CLAUDE.md)
Do not write migrations manually; use
pnpm run setupwhich generates .sql filesFiles:
packages/db/migrations/1758325232_happy_bloodstrike.sql🧬 Code graph analysis (4)
packages/models/src/get-cheapest-model-for-provider.ts (1)
packages/models/src/models.ts (1)
ProviderModelMapping(23-104)apps/ui/src/components/dashboard/log-card.tsx (1)
packages/db/src/schema.ts (1)
log(324-389)packages/models/src/models.spec.ts (3)
packages/models/src/models.ts (2)
models(163-179)ProviderModelMapping(23-104)packages/models/src/get-cheapest-model-for-provider.ts (1)
getCheapestModelForProvider(9-43)packages/models/src/get-cheapest-from-available-providers.ts (1)
getCheapestFromAvailableProviders(7-34)packages/models/src/get-cheapest-from-available-providers.ts (1)
packages/models/src/models.ts (1)
ProviderModelMapping(23-104)🔇 Additional comments (18)
conductor.json (1)
2-4: Formatting-only change; verify tab indentation matches repo conventions.No behavioral impact. Please confirm tabs align with your Prettier/EditorConfig settings to avoid future whitespace-only churn. If the repo standard is spaces, consider reverting to auto-formatted spacing.
packages/models/src/get-cheapest-model-for-provider.ts (1)
3-3: Type-only import — no issuespackages/db/migrations/meta/_journal.json (1)
354-361: Journal entry looks consistentNew tag recorded as expected.
apps/gateway/src/chat/chat.ts (2)
1440-1440: Set discount to null in cached/error/canceled logs — LGTMConsistent with “no cost on these paths.” No action needed.
Also applies to: 1534-1534, 1748-1748, 1878-1878, 2857-2857, 2960-2960
800-906: Auto routing cost heuristic doesn’t factor discount hereWithin auto selection, totalPrice uses only raw input/output averages; discount is only applied later in getCheapestFromAvailableProviders. That’s fine if intentional, but it can bias the first-stage filter.
Would you like me to align the early filter with discount as well?
Also applies to: 869-894
packages/models/src/get-cheapest-from-available-providers.ts (2)
1-1: LGTM: Import addition for discount supportThe import of
ProviderModelMappingis correctly added to support the new discount functionality.
21-25: LGTM: Discount calculation implementationThe discount logic is correctly implemented:
- Reads
discountfrom provider info, defaulting to 1 (no discount)- Applies discount multiplier to the total price calculation
- Maintains existing comparison logic for cheapest provider selection
packages/models/src/models.spec.ts (3)
3-3: LGTM: Test imports for discount functionalityThe imports for
getCheapestFromAvailableProvidersandProviderModelMappingare correctly added to support the new discount testing.Also applies to: 8-8
336-407: Comprehensive discount testing for cheapest model selectionThe test correctly validates that discount multipliers are properly considered when selecting the cheapest model for a provider. The test logic appropriately:
- Finds models with discount providers
- Compares regular vs. discounted pricing
- Validates that discounted providers are cheaper when expected
- Tests both
getCheapestModelForProviderandgetCheapestFromAvailableProvidersfunctions
410-509: Thorough testing of cheapest provider selection with discountsThe test suite comprehensively validates discount handling in provider selection:
- Tests basic cheapest provider selection
- Validates discount application by comparing regular vs. discounted providers
- Correctly calculates expected discounted prices and verifies selection logic
- Includes edge case testing with empty provider lists
apps/gateway/src/lib/costs.ts (4)
52-52: Correct discount field handling in error pathsThe
discount: undefinedfield is correctly added to all error return paths, ensuring consistent API response structure when model info is not found or token counts are missing.Also applies to: 116-116, 136-136
144-144: LGTM: Discount extraction from provider infoThe discount value is correctly extracted from
providerInfo.discountwith a sensible default of 1 (no discount).
153-158: Proper discount application to all cost componentsThe discount multiplier is correctly applied to all relevant cost calculations:
- Input cost (uncached prompt tokens)
- Output cost (completion tokens)
- Cached input cost
- Request cost
This ensures consistent discount application across all pricing components.
171-171: Clean discount field in response objectThe discount field is properly included in the response only when different from 1, keeping the API clean by not including redundant information for non-discounted pricing.
packages/db/migrations/meta/1758325232_snapshot.json (1)
817-822: Database schema correctly updated for discount columnThe migration snapshot correctly adds the
discountcolumn to the log table:
- Type:
real(appropriate for discount multipliers)- Nullable:
true(allows for logs without discount information)- Positioned logically among other cost-related fields
packages/models/src/models/anthropic.ts (3)
49-50: Clean refactor from computed to explicit pricingThe change from computed discount pricing to explicit
inputPrice/outputPricevalues with a separatediscountfield is a good architectural improvement:
- Makes pricing more transparent and easier to understand
- Separates concerns between base pricing and discount application
- The discount field correctly uses environment variable parsing with fallback
Also applies to: 52-52
86-87: Consistent discount implementation across modelsThe same refactoring pattern is consistently applied to the Claude 3.7 Sonnet model, maintaining uniformity across all routeway-discount providers.
Also applies to: 89-89
167-168: Uniform discount handling across all Anthropic modelsThe refactoring is consistently applied across all models with routeway-discount providers:
- Claude Sonnet 4
- Claude Opus 4
- Claude Opus 4.1
This maintains consistency and makes the discount logic explicit and transparent.
Also applies to: 170-170, 205-206, 208-208, 244-245, 247-247
…-field-routeway-models
Summary
discountfield to the cost calculation and logging systemChanges
Core Functionality
discountfield to cost calculation results incalculateCostsfunctiondiscountfield in database schema for logsdiscountmultiplierdiscountfield instead of price adjustment functionsAPI and Chat Layer
discountfieldUI Components
LogCardcomponent to show discount percentage when applicableTesting
costs.spec.tsto verify discount application for Routeway modelsTest plan
🌿 Generated by Terry
ℹ️ Tag @terragon-labs to ask questions and address PR feedback
📎 Task: https://www.terragonlabs.com/task/c3981849-cc09-4ea9-8fb2-ddd3a1bbc8d5
Summary by CodeRabbit
New Features
Tests
Chores