Skip to content

Add OpenAI pricing/costs usage feature.#1054

Closed
rahulsprajapati wants to merge 15 commits intodevelopfrom
feature/pricing-cost-usage-19763151
Closed

Add OpenAI pricing/costs usage feature.#1054
rahulsprajapati wants to merge 15 commits intodevelopfrom
feature/pricing-cost-usage-19763151

Conversation

@rahulsprajapati
Copy link
Contributor

@rahulsprajapati rahulsprajapati commented Feb 13, 2026

Description of the Change

Features

  • Site owners can monitor OpenAI spend and set soft (alert-only) and hard (disable-features) limits to avoid surprise bills.
  • Optional email alerts and clear admin notices keep usage visible.
  • Hard limit prevents further API calls until the admin re-enables or raises the limit.

This PR adds OpenAI usage monitoring and spending controls to ClassifAI:

  • OpenAI Pricing Controller (OpenAIPricingController.php): Central admin controller that:

    • Fetches usage/cost data from the OpenAI Costs API (organization-level Admin API key) via a new UsageCosts class.
    • Caches usage (this month, year-to-date, all-time) in the classifai_openai_pricing option and refreshes it on a configurable interval (default 15 minutes) using Action Scheduler.
    • Implements soft threshold: when usage exceeds a configured amount for a scope (current month / year to date / all time), sends an email alert to configured addresses (once per period) and shows a dismissible warning notice in the admin.
    • Implements hard threshold: when usage exceeds a higher configured amount, sets the classifai_openai_hard_limit_reached option, which gates all OpenAI API requests in APIRequest (get/post/post_form) and shows an error notice; optionally sends an email. Users can re-enable by increasing the limit or disabling the hard threshold on the Pricing page.
    • Registers a dashboard widget (Tools → Dashboard) showing this month, YTD, and all-time usage with a “Configure alerts” link when pricing is enabled.
  • UsageCosts (UsageCosts.php): New provider class that calls the OpenAI organization Costs API (/v1/organization/costs), with optional project ID filter, supports pagination, and provides fetch_costs(start_ts, end_ts) and fetch_all_time_costs() (from 2020).

  • Admin notices (Notifications.php): New render_openai_threshold_notice() — shows a dismissible notice when soft (warning) or hard (error) threshold is exceeded, with link to Tools → ClassifAI → Pricing.

  • REST API (Settings.php): New routes:

    • GET classifai/v1/openai-usage — returns cached usage for the dashboard/settings UI.
    • GET classifai/v1/openai-pricing-settings — returns pricing settings with API key obfuscated and hard_limit_reached flag.
    • POST classifai/v1/openai-pricing-settings — updates pricing settings; supports force_refresh: true to trigger an immediate usage refresh.
  • Request gating (APIRequest.php): Before each get, post, and post_form, calls openai_request_allowed(), which respects the filter classifai_openai_can_make_request and the hard-limit option. When not allowed, returns a WP_Error with a message directing users to re-enable via ClassifAI → Pricing.

  • Plugin registration (Plugin.php): Registers OpenAIPricingController in admin_helpers so it runs in admin (and only when not using legacy settings).

  • Settings UI: New “Pricing” tab in the ClassifAI settings (React) with:

    • Toggle to enable OpenAI pricing usage, Admin API key (password), optional Project ID, refresh interval.
    • Soft threshold panel: enable, amount, scope (current month / year to date / all time), email addresses.
    • Hard threshold panel: enable, amount, scope, emails; shows an error notice when hard limit is reached.
    • “Save pricing settings” and “Force refresh data” buttons.
  • Documentation (useful-snippets.md): Documents filters and actions: classifai_openai_usage_refresh_interval_minutes, classifai_openai_cached_usage, classifai_openai_can_make_request, classifai_openai_admin_api_key, classifai_openai_usage_updated, classifai_openai_soft_threshold_exceeded, classifai_openai_hard_threshold_exceeded.

Closes #122 (comment)

How to test the Change

  1. Install/activate ClassifAI and ensure the new settings UI is used (legacy settings off).
  2. Go to Tools → ClassifAI → Pricing. Enable “OpenAI pricing usage” and add a valid OpenAI Admin API key (organization-level; see OpenAI Usage API). Optionally set Project ID and refresh interval. Save.
  3. Click “Force refresh data” and confirm usage (this month, YTD, all time) loads without error. Check the Dashboard for the “OpenAI usage (ClassifAI)” widget.
  4. Set a soft threshold (e.g. amount 0.01, scope “Current month”, add an email). Save and force refresh. Confirm the yellow admin notice appears and the email is sent (once per period).
  5. Set a hard threshold below current usage (e.g. amount 0.01), save, force refresh. Confirm the red notice and that OpenAI features (e.g. content generation) are blocked. Fix the hard-limit logic if needed (see Description) so requests are actually blocked. Then increase the hard limit or disable hard threshold and save; confirm features work again.
  6. Dismiss the threshold notice and confirm it stays dismissed for that user. Confirm link to Pricing works.
  7. With legacy settings enabled, confirm no Pricing tab, no widget, and no new notices.

Changelog Entry

Added - OpenAI usage monitoring and spending controls:

  • pricing settings page (Tools → ClassifAI → Pricing)
  • dashboard widget
  • soft threshold (email + notice)
  • hard threshold (disable OpenAI features + notice)
  • REST endpoints for usage and pricing settings
  • hooks for refresh interval, cached usage, request gating, and threshold actions.

Credits

Props @rahulsprajapati

Checklist:

@github-actions github-actions bot added this to the 3.8.0 milestone Feb 13, 2026
@github-actions
Copy link

github-actions bot commented Feb 13, 2026

✅ WordPress Plugin Check Report

✅ Status: Passed

📊 Report

All checks passed! No errors or warnings found.


🤖 Generated by WordPress Plugin Check Action • Learn more about Plugin Check

@rahulsprajapati rahulsprajapati marked this pull request as ready for review February 16, 2026 06:35
@rahulsprajapati rahulsprajapati requested review from a team, dkotter and jeffpaul as code owners February 16, 2026 06:35
@github-actions github-actions bot added the needs:code-review This requires code review. label Feb 16, 2026
@jeffpaul
Copy link
Member

  1. Change Pricing to Usage Tracking
  2. Ensure center frame has spacing below separator like other sections

space:
Screenshot 2026-02-18 at 2 15 38 PM

no space:
Screenshot 2026-02-18 at 2 15 47 PM

  1. Similar to Recommendation Service having on primary option to enable and adjust settings, let's make it the same for Usage Tracking

Recommendation Service:
Screenshot 2026-02-18 at 2 18 38 PM

Recommendation settings:
Screenshot 2026-02-18 at 2 18 45 PM

Can probably leverage some of the existing copy:
Toggle heading: OpenAI usage tracking
Helper text: Monitor OpenAI usage and set levels for alerting and deactivating features.

  1. Within the Usage Tracking settings let's group things like this we do in other sections

Example:
Screenshot 2026-02-18 at 2 24 43 PM

Potential copy for Usage Tracking...

Heading: OpenAI usage tracking settings
Enable Feature: toggle (helper text: Monitor OpenAI usage and set levels for alerting and deactivating features. Requires an OpenAI organization-level Admin API key.)
OpenAI Admin API Key: textfield with masking
OpenAI Project ID: textfield
Refresh interval in minutes: number entry

Alert: checkbox Enable
Amount in $USD: number entry
Timeframe: dropdown
Email address(es): text area

Deactivate features: checkbox Enable
Amount in $USD: number entry
Timeframe: dropdown
Email address(es): text area

left-aligned secondary action button: Back to dashboard
left-aligned primary action button: Save Settings

Let's move the "Force refresh data" from the settings directly to the Dashboard widget. Updating there to make the "Configure usage tracking settings" the primary action button and "Force refresh data" a secondary action button only displayed for admins.

Within the OpenAI usage (ClassifAI) dashboard widget...

  1. Rename to OpenAI usage tracking
  2. update costs to $X.YZ (USD) instead of X.YZ usd

I think that captures my initial feedback, but without me sharing or making visual edits it might be best to jump on a call to tweak these together or accept we may need another round of minor tweaks.

Copy link
Collaborator

@dkotter dkotter left a comment

Choose a reason for hiding this comment

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

I've left a few comments on things I'm seeing, still need to do a more thorough review.

That said, one thing I'm noticing here is how specific to OpenAI everything is. I know the end goal is to only support OpenAI for this initial release but there is potential to extend this to other Providers in the future.

At the moment, we'd need to either do a lot of refactoring at that point or have a lot of code duplication. I don't necessarily want to slow down this PR but thinking we may want to refactor here to have more generic classes and functions that we can then extend with OpenAI specific needs. For instance, I'd imagine any Provider we want to support is going to use the same functionality in regards to showing admin notices or rendering the dashboard widget. Or even getting usage information. The actual API requests will need to be unique but most everything else feels like it could be shared.

Let me know if you have any thoughts on that or if I can explain things better.

* @return array|WP_Error
*/
public function get( string $url, array $options = [] ) {
if ( ! $this->openai_request_allowed() ) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

We'll need to update how we turn off Features as other Providers use this same APIRequest class (lots of Providers follow the OpenAI API spec). We're also looking to remove this class in the near future and replace with a central APIRequest class.

My suggestion for how to handle this is to use the Feature enable filters ('classifai_' . static::ID . '_is_feature_enabled' or 'classifai_' . static::ID . '_is_enabled'). If neither of those filters will work, we can look to add a new filter to handle this use case

}

$key = 'openai_threshold_reached';
if ( get_user_meta( get_current_user_id(), "classifai_dismissed_{$key}", true ) ) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm wondering if we want this dismissible? I typically err on the side of making things dismissible to avoid user's being annoyed but if threshold limits are reached, seems like this is an admin notice we would want to always show. Maybe dismissible on the soft threshold, non-dismissible on the hard?

$classes[] = 'notice-error';
$message = sprintf(
/* translators: 1: amount with currency, 2: link to settings */
__( 'OpenAI features are currently disabled due to exceeded your hard limit of %1$s for this period. <a href="%2$s">Re-enable it from the pricing page</a>.', 'classifai' ),
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
__( 'OpenAI features are currently disabled due to exceeded your hard limit of %1$s for this period. <a href="%2$s">Re-enable it from the pricing page</a>.', 'classifai' ),
__( 'OpenAI Features are currently disabled due to you exceeding your hard limit of %1$s for this period. <a href="%2$s">Re-enable it from the pricing page</a>.', 'classifai' ),

],
];

$response = safe_wp_remote_get( $url, $options );
Copy link
Collaborator

Choose a reason for hiding this comment

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

Could we use the APIRequest class to handle this request? If so, would reduce the amount of code here a bit

'soft_threshold_amount' => isset( $pricing['soft_threshold_amount'] ) ? abs( (float) $pricing['soft_threshold_amount'] ) : 0,
'soft_threshold_scope' => $soft_scope,
'soft_threshold_emails' => isset( $pricing['soft_threshold_emails'] ) ? sanitize_textarea_field( $pricing['soft_threshold_emails'] ) : ( $current['soft_threshold_emails'] ?? '' ),
'hard_threshold_amount' => isset( $pricing['hard_threshold_amount'] ) ? abs( (float) $pricing['hard_threshold_amount'] ) : 0,
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'd suggest we have better defaults than $0 for the thresholds. Right now if I turn things on but don't modify the value, I get alerts immediately because I've exceeded $0. Maybe something like $5 for the soft, $10 for the hard?

description={ sprintf(
/* translators: %d: default minutes */
__(
'How often to fetch usage (default %d). Filter: classifai_openai_usage_refresh_interval_minutes',
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
'How often to fetch usage (default %d). Filter: classifai_openai_usage_refresh_interval_minutes',
'How often to fetch usage (default %d)',


$interval = $this->get_refresh_interval_minutes();
if ( ! \as_has_scheduled_action( self::CRON_HOOK ) ) {
\as_schedule_recurring_action( time(), $interval, self::CRON_HOOK, [], 'classifai' );
Copy link
Collaborator

Choose a reason for hiding this comment

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

So I don't think this is actually working right now, as Action Scheduler is only available if certain Features are turned on. So if those Features are on, this works. But if none of those are, these functions won't exist. We'll need to modify our Action Scheduler loading logic to account for this

Comment on lines +205 to +223
'enabled' => false,
'admin_api_key' => '',
'project_id' => '',
'refresh_interval_minutes' => ( self::DEFAULT_REFRESH_INTERVAL / MINUTE_IN_SECONDS ),
'soft_threshold_enabled' => false,
'soft_threshold_amount' => 0,
'soft_threshold_scope' => 'current_month',
'soft_threshold_emails' => '',
'hard_threshold_amount' => 0,
'hard_threshold_scope' => 'current_month',
'hard_threshold_emails' => '',
'hard_threshold_enabled' => false,
'api_start_year' => 2020,
'all_year_pricing' => [],
'this_month_total' => 0,
'ytd_total' => 0,
'all_time_total' => 0,
'usage_currency' => 'USD',
'usage_last_updated' => 0,
Copy link
Collaborator

Choose a reason for hiding this comment

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

All of these fields are repeated multiple places throughout the code, both on the PHP side and JS side. I don't know exact approach but seems like there should be a better way to manage these that doesn't require us to repeat all of these over and over

@dkotter
Copy link
Collaborator

dkotter commented Feb 18, 2026

That said, one thing I'm noticing here is how specific to OpenAI everything is. I know the end goal is to only support OpenAI for this initial release but there is potential to extend this to other Providers in the future.

To this point, did a quick refactor using Claude Code that I think is closer to what I envision (noting I have not fully tested this, it was a quick iteration with AI so definitely needs more work). But was hoping this would better explain how this could work: https://github.com/10up/classifai/compare/refactor/usage-tracking-extensibility?expand=1

@rahulsprajapati
Copy link
Contributor Author

I've left a few comments on things I'm seeing, still need to do a more thorough review.

That said, one thing I'm noticing here is how specific to OpenAI everything is. I know the end goal is to only support OpenAI for this initial release but there is potential to extend this to other Providers in the future.

At the moment, we'd need to either do a lot of refactoring at that point or have a lot of code duplication. I don't necessarily want to slow down this PR but thinking we may want to refactor here to have more generic classes and functions that we can then extend with OpenAI specific needs. For instance, I'd imagine any Provider we want to support is going to use the same functionality in regards to showing admin notices or rendering the dashboard widget. Or even getting usage information. The actual API requests will need to be unique but most everything else feels like it could be shared.

Let me know if you have any thoughts on that or if I can explain things better.

Yeah this make sense and now that we do have some time(few more days), we can refactor this. I tried to add this feature asap given the time. I tried to reuse as much as code/structure I could, but majority of time I ended up spending time on fixing issue, so created some new file/template.

I'll review this https://github.com/10up/classifai/compare/refactor/usage-tracking-extensibility?expand=1 and update this PR.

@jeffpaul
Copy link
Member

At the moment, we'd need to either do a lot of refactoring at that point or have a lot of code duplication. I don't necessarily want to slow down this PR but thinking we may want to refactor here to have more generic classes and functions that we can then extend with OpenAI specific needs. For instance, I'd imagine any Provider we want to support is going to use the same functionality in regards to showing admin notices or rendering the dashboard widget. Or even getting usage information. The actual API requests will need to be unique but most everything else feels like it could be shared.

I'd prefer to refactor before shipping, especially since known extensions on this can leverage this branch for now (or an iteration of the branch) while we get things "better" for broad usage in ClassifAI core.

@jeffpaul jeffpaul mentioned this pull request Feb 19, 2026
28 tasks
@rahulsprajapati
Copy link
Contributor Author

@dkotter , @jeffpaul I created new PR #1060 to keep clean PR with refactoring. I used existing code structure to get proper UI for this. This fixes all the spacing issue and keeps the consistence UI across all features. Also, used provider and features classes so it can be reused to add new provider for this feature.

We can close this ticket and start review on new one.

@dkotter dkotter removed this from the 3.8.0 milestone Feb 26, 2026
@dkotter
Copy link
Collaborator

dkotter commented Feb 26, 2026

Closing in favor of #1060

@dkotter dkotter closed this Feb 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs:code-review This requires code review.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Display available credits/quotas from service providers

3 participants