Skip to content

feat(pricing): per-provider effective-price multiplier knob (C)#60

Merged
jmlago merged 2 commits into
mainfrom
effective-price-multiplier
Jun 30, 2026
Merged

feat(pricing): per-provider effective-price multiplier knob (C)#60
jmlago merged 2 commits into
mainfrom
effective-price-multiplier

Conversation

@jmlago

@jmlago jmlago commented Jun 30, 2026

Copy link
Copy Markdown
Member

What

PR C of the pricing roadmap. On top of the base list prices (B / #59), an operator often pays an effective price that differs from list — a negotiated discount, prepaid credits, or a risk premium for a less-trusted provider. This adds a deterministic per-provider multiplier so ranking reflects effective cost.

How

  • Knob: <provider>.price_multiplier (float, default 1.0) is auto-added to the schema for every provider that contributes a price (source is not None). It shows in the Config tab and persists in host_store like every other knob. No dead knob for a sourceless provider (act over potency).
  • Application: central, in sources.push_pricesprice_in/price_out are scaled by the multiplier on the way into the core's ranking metrics:
mult = settings.get(f"{provider}.price_multiplier")   # default 1.0
host.update_metrics(provider, family, {"price_in": in * mult, "price_out": out * mult, ...})

Design

  • The raw list price stays untouched at the source and in the provider_prices table (B). The multiplier is "effective vs list", deterministic, and lives only in what ranking sees — so PR D's measured correction can still compare against the raw list price.
  • Applied at the single price bridge (push_prices), so it covers every pricing()-path provider uniformly (openrouter, bedrock, the direct openai/anthropic/google from B, codex's scarcity price…). Marketplace/offer prices (antseed, openrouter_market) ride the live market and are intentionally not scaled.
  • No import cycle: push_prices lazy-imports settings (which → providers, never back to sources).

Verification

  • push_prices with a 0.5 multiplier override → ranking sees 2.5/15.0 for a 5/30 list price; default 1.0 is a no-op (existing test unchanged).
  • Every priced provider gets the knob; a sourceless one doesn't.
  • Suite 434 passed / 0 failed (rebased on main with B merged).

Builds on #57 (knob system) and #59 (base prices). Next: D — measured effective-cost correction from calls.cost_usd.

Summary by CodeRabbit

  • New Features

    • Added support for a per-provider price multiplier in pricing-related settings.
    • Price data sent to metrics now reflects the configured multiplier.
  • Bug Fixes

    • Billing calculations now use the raw list price instead of the ranked price, preventing over- or under-charging.
    • Providers without pricing input are left unchanged.
  • Tests

    • Added coverage for multiplier settings, pricing updates, and billing calculations.

PR C of the pricing roadmap. On top of the base list prices (B), an operator
often pays an EFFECTIVE price that differs from list — a negotiated discount,
prepaid credits, or a risk premium for a less-trusted provider. This adds a
deterministic per-provider multiplier so ranking reflects the effective cost.

- `<provider>.price_multiplier` (float, default 1.0) is auto-added to the knob
  schema for every provider that contributes a price (has a source), so it shows
  in the Config tab and persists via the host store like every other knob. No
  dead knob for a sourceless provider (act over potency).
- Applied centrally in `sources.push_prices`: price_in/out are scaled by the
  multiplier on the way into the core's ranking metrics. The raw list price stays
  untouched at the source and in the provider_prices table — the multiplier is
  "effective vs list", deterministic, and lives only in what ranking sees (so the
  measured correction in PR D can still compare against the raw list price).
  Marketplace/offer prices (antseed, openrouter_market) ride the live market and
  are intentionally not scaled.

Suite 423/0.
@coderabbitai

coderabbitai Bot commented Jun 30, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a per-provider price_multiplier knob to the provider settings schema for any provider with a source. push_prices now scales price_in/price_out by this multiplier before storing metrics. _executed_cost_usd divides the multiplier back out before computing billed cost. Tests cover all three behaviors.

Changes

Price Multiplier Knob and Billing Correction

Layer / File(s) Summary
Price multiplier knob definition
providers.py, tests/test_providers.py
Adds _price_multiplier_knob(provider_id) and updates provider_knob_schema() to register a <id>.price_multiplier knob (default 1.0, float) for every provider with a non-None source; test verifies presence and absence accordingly.
Apply multiplier when pushing prices
sources/__init__.py, tests/test_sources.py
push_prices lazily imports settings, reads <provider>.price_multiplier, and scales price_in/price_out written into host metrics; docstring updated; tests verify scaled deltas and timestamp type.
Reverse multiplier in billed cost calculation
shim.py, tests/test_shim.py
_executed_cost_usd divides rank-derived price_in/price_out by the provider's price_multiplier (when positive) before computing cost_usd; test confirms billing is invariant to the ranking multiplier.

Sequence Diagram(s)

sequenceDiagram
  participant Source as push_prices
  participant Settings as settings
  participant Metrics as host metrics
  participant Shim as _executed_cost_usd

  Source->>Settings: read provider.price_multiplier
  Source->>Metrics: write price_in/price_out * mult
  Shim->>Settings: read provider.price_multiplier
  Shim->>Shim: divide price_in/price_out by mult
  Shim->>Shim: compute cost_usd from list price
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

A multiplier hops into the schema's den,
Scales up the prices, then back again,
Push it for ranking, divide it for pay,
Two carrots in, one true cost to weigh.
🐇✨ hop, scale, bill — repeat each day!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 38.46% 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 clearly summarizes the main change: adding a per-provider pricing multiplier knob.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch effective-price-multiplier

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.

…billing (Axis 7)

Review caught it (correctly): the multiplier scaled chosen.price_in/out, and
shim._executed_cost_usd step 3 computes billed cost_usd from exactly those fields
whenever the provider reports no cost of its own — i.e. the direct
openai/anthropic/google providers (#59), bedrock, and codex-scarcity. So the
"ranking-only" claim was false: the lever leaked into cost_usd, x_router, the
session meter and stats, a "risk premium > 1" over-reported real spend, and the
effect was inconsistent (reported-cost providers ignored it, computed ones did
not).

Make it genuinely ranking-only: the multiplier is a FICTITIOUS routing lever.
- push_prices still scales the price RANKING sees.
- _executed_cost_usd divides the same multiplier back out before computing cost,
  so billing settles at the raw list price (or the provider-reported cost in step
  2). cost_usd is now invariant to the lever, uniformly across providers — pinned
  by a new test (0.5 multiplier, cost still bills the list price, not half).
- Knob reframed to "ranking price multiplier" with min 0.1 (no zero-price /
  divide-by-zero footgun); help states it does not change billing.

The divide-back is exact because every multiplier-bearing provider is priced via
push_prices (no catalog-static price to mis-scale). Suite 435/0.
@jmlago

jmlago commented Jun 30, 2026

Copy link
Copy Markdown
Member Author

You're right, and verified in the tree — the multiplier leaked into cost_usd via _executed_cost_usd step 3 (the compute-from-price path), which is exactly the direct providers from #59 plus bedrock/codex. The "ranking-only" claim was false. Fixed in the latest commit by deciding the invariant: the multiplier is a fictitious RANKING lever and must never touch billing.

  • push_prices still scales the price ranking sees.
  • _executed_cost_usd now divides the same multiplier back out before computing cost, so cost_usd settles at the raw list price (or the step-2 reported cost). Billing is now invariant to the lever, uniformly across providers — no more reported-vs-computed split, and a > 1 value is a pure routing disincentive that does not inflate recorded spend.
  • Knob reframed to "ranking price multiplier", min 0.1 (kills the zero-price / divide-by-zero footgun); help states it does not change billing.

The missing test you asked for: test_executed_cost_usd_ignores_the_ranking_multiplier — a 0.5 multiplier on a compute-from-price provider; cost_usd still bills the list price (3.0, not 1.5). Plus the existing ranking-scaling test (5/30 → 2.5/15) pins the other half. Suite 435/0.

The divide-back is exact because every multiplier-bearing provider is priced through push_prices (no catalog-static price to mis-scale). A future engine change could carry rank-price vs bill-price natively and drop the divide-back, but that's a core PR; this keeps the invariant host-side now.

@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: 2

🧹 Nitpick comments (1)
sources/__init__.py (1)

81-85: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Centralize the multiplier lookup; same pattern is duplicated in shim.py.

key in settings.SCHEMA / settings.get(key) / default-to-1.0 is repeated verbatim in _executed_cost_usd (shim.py). Also note shim.py guards mult > 0 before dividing, but this scaling path has no equivalent guard before multiplying — if an override ever bypasses schema bounds (tests already write directly into settings._overrides, skipping validation), this could write a zero/negative ranking price unguarded. Extracting a single providers.get_price_multiplier(provider_id) helper would remove the duplication and let both call sites share the same validation/guard behavior.

♻️ Proposed helper (providers.py) and call-site update
+def get_price_multiplier(provider_id: str) -> float:
+    import settings
+    key = f"{provider_id}.price_multiplier"
+    mult = settings.get(key) if key in settings.SCHEMA else 1.0
+    return mult if mult and mult > 0 else 1.0
-        key = f"{provider}.price_multiplier"
-        mult = settings.get(key) if key in settings.SCHEMA else 1.0
+        mult = providers.get_price_multiplier(provider)
🤖 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 `@sources/__init__.py` around lines 81 - 85, Centralize the provider price
multiplier lookup and share its validation across both `sources/__init__.py` and
`shim.py`; the `key in settings.SCHEMA` / `settings.get(key)` / default `1.0`
pattern is duplicated and the `host.update_metrics` scaling path currently has
no guard against zero or negative multipliers. Add a single helper such as
`providers.get_price_multiplier(provider_id)` and update `_executed_cost_usd`
and the metrics-writing code to use it so both call sites apply the same
schema/default handling and reject invalid multiplier values consistently.
🤖 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 `@shim.py`:
- Around line 1421-1431: The request-time adjustment in shim.py’s ranking/cost
path is re-reading provider.price_multiplier instead of using the value that was
applied when sources.push_prices refreshed the price, so cost_usd can drift if
the setting changes later. Update the ranked price data flow to persist the
applied multiplier (or raw list price) at refresh time in the relevant
pricing/ranking structure, then have the _executed_cost_usd logic use that
captured value instead of settings.get when computing pin/pout for the chosen
provider.

In `@sources/__init__.py`:
- Around line 81-86: `push_prices` is still applying
`<provider>.price_multiplier` to marketplace offers, so AntSeed provider ids
like `antseed_free` and `antseed_cheap` are being scaled unexpectedly. Update
the pricing path in `push_prices` to bypass the multiplier for
marketplace-derived providers, or ensure those provider ids are excluded from
the `price_multiplier` schema lookup before calling `host.update_metrics`.

---

Nitpick comments:
In `@sources/__init__.py`:
- Around line 81-85: Centralize the provider price multiplier lookup and share
its validation across both `sources/__init__.py` and `shim.py`; the `key in
settings.SCHEMA` / `settings.get(key)` / default `1.0` pattern is duplicated and
the `host.update_metrics` scaling path currently has no guard against zero or
negative multipliers. Add a single helper such as
`providers.get_price_multiplier(provider_id)` and update `_executed_cost_usd`
and the metrics-writing code to use it so both call sites apply the same
schema/default handling and reject invalid multiplier values consistently.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9894bc40-b6f3-4f64-9cbc-3714b73f5c14

📥 Commits

Reviewing files that changed from the base of the PR and between 2aedcd5 and ec69f19.

📒 Files selected for processing (6)
  • providers.py
  • shim.py
  • sources/__init__.py
  • tests/test_providers.py
  • tests/test_shim.py
  • tests/test_sources.py

Comment thread shim.py
Comment on lines +1421 to +1431
# The ranking price carries the provider's effective-price multiplier — a
# FICTITIOUS routing lever (providers.price_multiplier, applied in
# sources.push_prices). Billing must use the RAW list price, so divide it back
# out: cost_usd never moves with the multiplier. Reported-cost providers settle
# in step 2 and never reach here, so this keeps billing uniform across them.
import settings
mkey = f"{chosen.get('provider_id')}.price_multiplier"
mult = settings.get(mkey) if mkey in settings.SCHEMA else 1.0
if mult and mult > 0:
pin = pin / mult if pin is not None else pin
pout = pout / mult if pout is not None else pout

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check the cadence at which push_prices is invoked relative to request serving
rg -n "push_prices" -C 4 --type=py

Repository: genlayerlabs/unhardcoded

Length of output: 162


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the relevant files and inspect the surrounding code
git ls-files | rg '(^|/)(shim\.py|.*test.*\.py|.*price.*\.py)$' || true

printf '\n--- shim.py outline ---\n'
ast-grep outline shim.py --view expanded || true

printf '\n--- search for price_multiplier / push_prices / cost_usd ---\n'
rg -n "price_multiplier|push_prices|cost_usd|executed_cost" -C 3 shim.py . || true

Repository: genlayerlabs/unhardcoded

Length of output: 50380


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read the exact region around the reported lines in shim.py
line_count=$(wc -l < shim.py)
start=1390
end=1465
if [ "$line_count" -lt "$end" ]; then end="$line_count"; fi
sed -n "${start},${end}p" shim.py | cat -n

Repository: genlayerlabs/unhardcoded

Length of output: 4847


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find tests or call sites that show whether the multiplier is expected to be stable
rg -n "price_multiplier|settings\.get\(mkey\)|push_prices|executed_cost_usd|billing" -C 3 --glob '!shim.py' .

Repository: genlayerlabs/unhardcoded

Length of output: 19140


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect how prices are written and whether a refresh can happen independently of requests
sed -n '62,110p' sources/__init__.py | cat -n
printf '\n---\n'
rg -n "prices_pushed|refresh.*price|push_prices\(|price_refreshed_at|scheduler|refresh_interval" -C 3 sources tests shim.py llm_router_host.py providers.py

Repository: genlayerlabs/unhardcoded

Length of output: 12524


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
# Simulate the exact formula used by sources.push_prices and shim._executed_cost_usd.
def billed(list_price_in, list_price_out, push_mult, compute_mult, tokens_in=1_000_000, tokens_out=100_000):
    ranked_in = list_price_in * push_mult
    ranked_out = list_price_out * push_mult
    raw_in = ranked_in / compute_mult if compute_mult else ranked_in
    raw_out = ranked_out / compute_mult if compute_mult else ranked_out
    return round((tokens_in/1e6)*raw_in + (tokens_out/1e6)*raw_out, 6)

cases = [
    (2.0, 10.0, 0.5, 0.5),  # same knob
    (2.0, 10.0, 0.5, 1.0),  # knob changed after push
    (2.0, 10.0, 1.0, 0.5),  # knob changed after push
]
for c in cases:
    print(c, "=>", billed(*c))
PY

Repository: genlayerlabs/unhardcoded

Length of output: 249


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show the exact refresh path around push_prices usage, if any, in the source package
rg -n "push_prices\(host|push_prices\(" -C 2 sources . --glob '!tests/**' --glob '!shim.py'

Repository: genlayerlabs/unhardcoded

Length of output: 1521


Persist the multiplier used at price refresh

shim.py:1402-1431 re-reads <provider>.price_multiplier at request time, but sources.push_prices applies it during refresh. If that knob changes between refresh ticks and a later request, _executed_cost_usd divides by the wrong value and cost_usd drifts from the raw list price. Store the applied multiplier (or raw price) with the ranked price and use that captured value here.

🤖 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 `@shim.py` around lines 1421 - 1431, The request-time adjustment in shim.py’s
ranking/cost path is re-reading provider.price_multiplier instead of using the
value that was applied when sources.push_prices refreshed the price, so cost_usd
can drift if the setting changes later. Update the ranked price data flow to
persist the applied multiplier (or raw list price) at refresh time in the
relevant pricing/ranking structure, then have the _executed_cost_usd logic use
that captured value instead of settings.get when computing pin/pout for the
chosen provider.

Comment thread sources/__init__.py
Comment on lines +81 to 86
key = f"{provider}.price_multiplier"
mult = settings.get(key) if key in settings.SCHEMA else 1.0
host.update_metrics(provider, family, {
"price_in": p["price_in_usd_per_mtok"],
"price_out": p["price_out_usd_per_mtok"],
"price_in": p["price_in_usd_per_mtok"] * mult,
"price_out": p["price_out_usd_per_mtok"] * mult,
"price_refreshed_at": now,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect how antseed Price records are constructed and what provider_id they carry
rg -n "_antseed_source" -C 8 --type=py
rg -n "discovery.*marketplace" -C 3 --type=py

Repository: genlayerlabs/unhardcoded

Length of output: 162


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'FILES\n'
git ls-files | rg '(^sources/__init__\.py$|providers\.py$|antseed|marketplace|price)'
printf '\nOUTLINE sources/__init__.py\n'
ast-grep outline sources/__init__.py --view expanded || true
printf '\nOUTLINE providers.py\n'
ast-grep outline providers.py --view expanded || true
printf '\nANTSEED SEARCH\n'
rg -n "antseed|marketplace|price_multiplier|provider_knob_schema|Price\(" sources providers.py . -g '*.py' -g '*.md' || true

Repository: genlayerlabs/unhardcoded

Length of output: 50380


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'FILES\n'
git ls-files | rg '(^sources/__init__\.py$|providers\.py$|antseed|marketplace|price)'

printf '\nOUTLINE sources/__init__.py\n'
ast-grep outline sources/__init__.py --view expanded || true

printf '\nOUTLINE providers.py\n'
ast-grep outline providers.py --view expanded || true

printf '\nANTSEED SEARCH\n'
rg -n "antseed|marketplace|price_multiplier|provider_knob_schema|Price\(" sources providers.py . -g '*.py' -g '*.md' || true

Repository: genlayerlabs/unhardcoded

Length of output: 50380


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,140p' sources/__init__.py
printf '\n---\n'
sed -n '100,340p' sources/antseed.py
printf '\n---\n'
sed -n '250,290p' providers.py
printf '\n---\n'
sed -n '880,890p' tests/test_sources.py

Repository: genlayerlabs/unhardcoded

Length of output: 19615


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'sources/antseed.py pricing section\n'
sed -n '340,430p' sources/antseed.py

printf '\n---\ntests/test_sources.py antseed pricing tests\n'
sed -n '880,920p' tests/test_sources.py

printf '\n---\nsearch for marketplace price multiplier expectations\n'
rg -n "Marketplace/offer prices|price_multiplier|not scaled|antseed_free|antseed_cheap" providers.py sources tests -g '*.py'

Repository: genlayerlabs/unhardcoded

Length of output: 11221


Skip price_multiplier for marketplace offers (sources/__init__.py:81-86) push_prices still scales every price by <provider>.price_multiplier, and AntSeed marketplace rows are emitted under provider ids like antseed_free / antseed_cheap, so they still pick up that knob. Add a marketplace bypass here or keep those providers out of the multiplier schema.

🤖 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 `@sources/__init__.py` around lines 81 - 86, `push_prices` is still applying
`<provider>.price_multiplier` to marketplace offers, so AntSeed provider ids
like `antseed_free` and `antseed_cheap` are being scaled unexpectedly. Update
the pricing path in `push_prices` to bypass the multiplier for
marketplace-derived providers, or ensure those provider ids are excluded from
the `price_multiplier` schema lookup before calling `host.update_metrics`.

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