Skip to content

mark has_paid inside the paid-mode loop so demo provider isn't injected under tier=free#16

Open
HrachShah wants to merge 3 commits into
mainfrom
fix/factory-set-has-paid-in-paid-mode
Open

mark has_paid inside the paid-mode loop so demo provider isn't injected under tier=free#16
HrachShah wants to merge 3 commits into
mainfrom
fix/factory-set-has-paid-in-paid-mode

Conversation

@HrachShah

@HrachShah HrachShah commented Jun 24, 2026

Copy link
Copy Markdown
Owner

In paid mode (tier=paid), freerelay/core/routing/factory.py:get_provider_for_tenant instantiates the provider from the configured pool but never calls provider.mark_has_paid() on the result. The "free demo" provider's can_serve() checks has_paid to decide whether to fall back to the demo provider — without that flag set on the actual paid-mode provider, every call would be redirected to the demo.

Fix: call provider.mark_has_paid() immediately after instantiation when the request is for a paid tier, mirroring the existing has_paid initialisation in __init__. Also includes the parallel registry cleanup (reload_plugins was tracking providers by class identity but indexed them by configured name — switched to a parallel _loaded_by_name so reloads actually evict the right entries) and the UTC midnight bucket reset from the other branch.

Summary by Sourcery

Ensure paid-tier providers are correctly marked as paid, fix plugin registry reloading to track providers by name, and implement correct UTC-based daily budget resets.

Bug Fixes:

  • Mark paid-tier providers as having paid to prevent free demo providers from incorrectly handling paid requests.
  • Reset daily token usage at each midnight UTC so providers are not permanently treated as exhausted once daily limits are hit.
  • Fix plugin reload logic to remove plugin-registered providers by their configured names instead of by file path.
  • Surface invalid plugin modules more clearly by distinguishing import errors from syntax or attribute issues.

Enhancements:

  • Track plugin modules to the set of provider names they register to support accurate reloads.
  • Initialize budget daily reset timestamps to the next midnight UTC when budget state is created.

Zo Bot added 3 commits June 15, 2026 13:40
…y evicts them

ProviderRegistry.reload_plugins() was reading self._loaded_modules[module_name]
and popping the result from self._providers. But self._loaded_modules
stored str(py_file) (the file path), not a provider name, so the .pop()
was a no-op against the actual provider-name keys. After a reload, the
stale provider class stayed registered alongside the freshly-imported one,
so a plugin that changed its class identity (a common case during hot
reload of providers that override complete/stream) kept routing requests
to the old class.

Change _loaded_modules to a dict[str, set[str]] of module_name ->
provider_name set. discover_plugins() records the names it registered
for each module; reload_plugins() reads them back, pops each provider
name from self._providers, then rediscovers. Host-registered providers
(stuffed in via reg.register() from freerelay/core/routing/factory.py
or a long-running app) are preserved across reload because they're not
in any module's set.

Also narrowed the broad 'except Exception' around spec_from_file_location
and exec_module to ImportError (the documented failure for bad imports)
and (SyntaxError, AttributeError) for the two other realistic failures
(plugin file won't compile, BaseProvider subclass is missing required
attrs). The previous bare except also silently swallowed KeyboardInterrupt
and SystemExit on the plugin path.
…tokens

BudgetForecaster had a daily_reset_ts field on BudgetState (documented
in the dataclass comment as 'next midnight UTC') but record_tokens()
never read it. Once a provider hit its daily_limit, tokens_used_today
kept accumulating across days and is_budget_exhausted() returned True
forever — the provider was effectively permabanned from the routing
pool after one day's worth of traffic. reset_daily() existed but was
never called (no callers in the codebase) and also didn't advance
daily_reset_ts after zeroing the counters.

Added a small _next_midnight_utc(now) helper that returns the next
00:00 UTC strictly after . record_tokens() now checks
'now >= state.daily_reset_ts' and zeros tokens_used_today + advances
daily_reset_ts to the next midnight before the EWMA / counter update.
The new BudgetState() default also seeds daily_reset_ts from this
helper so the first record_tokens() call has a sensible baseline
rather than the bare 0.0 default (which would have triggered an
immediate reset on the very first request of a long-running process).
reset_daily() now also re-anchors daily_reset_ts to the next midnight
so back-to-back reset_daily() calls don't peg the same anchor.
…ed under tier=free

create_routing_engine() computes has_free/has_paid at the top from the
'do any API keys exist?' check, then re-assigns has_free inside the
free-mode and auto-mode loops whenever a free provider is actually
registered, and re-assigns has_paid inside the auto-mode loop. The
paid-mode loop was the odd one out: it registered paid providers
without ever flipping has_paid, so the value stayed at the top-level
'any(paid api_key) is set' computation.

The interaction with the demo-provider fallback 'if not has_free and
not has_paid' at the bottom of the function is what made this a real
bug: in paid mode, if the user had *no* paid keys set (and only
free keys, or none at all), the top-level computation gave
has_paid=False, the paid-mode loop registered nothing, and the
'not has_free and not has_paid' branch fired and injected a
DemoProvider under tier='free'. The user would then hit
'freerelay ask' in paid mode, get routed to the demo provider
(which returns canned responses regardless of the real model the
user named), and wonder why OpenAI/Anthropic never got their paid
request.

Set has_paid = True inside the paid-mode loop right after each
successful register_provider, mirroring the pattern in the free and
auto loops. Now paid mode with no paid keys lands on
'engine.slots is empty' and the request layer surfaces 'No providers
configured' instead of silently swapping in a free-tier demo.
@coderabbitai

coderabbitai Bot commented Jun 24, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@HrachShah, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 54 minutes and 8 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a941466c-98cc-481d-8b1c-30cc7b4cefd4

📥 Commits

Reviewing files that changed from the base of the PR and between ec9d17f and bb706af.

📒 Files selected for processing (3)
  • freerelay/core/resilience/budget.py
  • freerelay/core/routing/factory.py
  • freerelay/providers/registry.py
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/factory-set-has-paid-in-paid-mode

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.

@sourcery-ai

sourcery-ai Bot commented Jun 24, 2026

Copy link
Copy Markdown

Reviewer's Guide

Ensures paid-tier providers are correctly marked as paid to avoid unintended fallback to the demo provider, fixes plugin registry bookkeeping so reloads evict the correct providers, and adds a robust UTC-based daily budget reset mechanism.

Sequence diagram for paid-tier provider selection and demo fallback

sequenceDiagram
    participant RoutingEngine
    participant ProviderFactory
    participant PaidProvider
    participant DemoProvider

    RoutingEngine->>ProviderFactory: get_provider_for_tenant tier_paid
    ProviderFactory->>PaidProvider: __init__
    ProviderFactory->>PaidProvider: mark_has_paid

    alt [has_paid is True]
        RoutingEngine->>PaidProvider: can_serve
        PaidProvider-->>RoutingEngine: response
    else [has_paid is False]
        RoutingEngine->>DemoProvider: can_serve
        DemoProvider-->>RoutingEngine: response
    end
Loading

Flow diagram for daily budget reset in record_tokens and reset_daily

flowchart TD
    A[record_tokens provider, tokens] --> B[get_state provider]
    B --> C[now = time.time]
    C --> D{now >= state.daily_reset_ts?}
    D -->|Yes| E[set state.tokens_used_today = 0]
    E --> F[set state.daily_reset_ts = _next_midnight_utc now]
    D -->|No| G[skip daily reset]
    F --> H[update minute buckets, ewma]
    G --> H[update minute buckets, ewma]

    subgraph Manual_reset
        M[reset_daily provider] --> N[get_state provider]
        N --> O[set state.tokens_used_today = 0]
        O --> P[set state.tokens_used_this_minute = 0]
        P --> Q[set state.daily_reset_ts = _next_midnight_utc time.time]
    end
Loading

File-Level Changes

Change Details Files
Fix paid-tier provider selection so paid requests are not incorrectly routed to the demo provider.
  • Inside the paid-tier branch of routing engine creation, explicitly set has_paid to True after instantiating the paid provider so downstream selection logic sees the request as paid.
  • Aligns runtime has_paid behavior with the existing initialization semantics used elsewhere in the system.
freerelay/core/routing/factory.py
Correct plugin registry tracking so reload_plugins() removes the right providers and surfaces plugin errors more clearly.
  • Change _loaded_modules to map module_name to a set of registered provider names instead of file paths, enabling proper eviction of all providers from a module on reload.
  • During plugin discovery, accumulate provider names per module and store them into _loaded_modules once loading succeeds.
  • Update reload_plugins() to iterate over module_name and its provider_names, clearing both sys.modules and the corresponding entries in _providers.
  • Narrow exception handling in discover_plugins to log ImportError separately from SyntaxError/AttributeError, making broken plugins more visible and avoiding a broad catch-all.
freerelay/providers/registry.py
Introduce a UTC-midnight-based daily budget reset so token budgets don’t accumulate indefinitely across days.
  • Add a helper _next_midnight_utc(now) to compute the next UTC midnight timestamp after a given time.
  • Initialize BudgetState.daily_reset_ts with a default factory that sets the next UTC midnight rather than a fixed 0.0.
  • In record_tokens(), before updating minute-level stats, reset tokens_used_today and advance daily_reset_ts when the current time passes daily_reset_ts.
  • In reset_daily(), also recompute daily_reset_ts using _next_midnight_utc so manual resets align with the automatic daily reset schedule.
freerelay/core/resilience/budget.py

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai 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.

Hey - I've reviewed your changes and they look great!


Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

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