Skip to content

[FEAT] Provider Budgets#45

Open
Henrik-3 wants to merge 4 commits into
masterfrom
feat/provider-budgets
Open

[FEAT] Provider Budgets#45
Henrik-3 wants to merge 4 commits into
masterfrom
feat/provider-budgets

Conversation

@Henrik-3

@Henrik-3 Henrik-3 commented Jul 18, 2026

Copy link
Copy Markdown
Member

Greptile Summary

This PR adds upstream provider billing and cached budget views. The main changes are:

  • OpenAI and OpenRouter billing adapters.
  • Encrypted billing configuration and admin routes.
  • Cached snapshots and monthly local-spend aggregation.
  • Scheduled refreshes with bounded concurrency.
  • Provider billing cards and configuration controls.

Confidence Score: 4/5

Provider status handling needs fixes before merging.

  • Anthropic is shown as configurable even though configuration is rejected.
  • A first-refresh authorization failure is presented as missing setup.
  • Credential storage and permission checks otherwise preserve the intended security boundary.

src/types/providers/billing_repository.rs; frontend/pages/settings/providers.vue

Important Files Changed

Filename Overview
src/types/providers/billing_repository.rs Adds cached overview queries and snapshot persistence, but classifies Anthropic as configurable.
src/routes/admin/provider_billing.rs Adds permission-gated configuration, deletion, and refresh handlers with encryption checks.
src/utils/provider_billing/mod.rs Coordinates credential loading, upstream requests, normalized snapshots, and failure states.
src/utils/provider_billing/openai.rs Adds paginated monthly cost and spend-threshold retrieval.
src/utils/provider_billing/openrouter.rs Adds key-level limits and account-credit retrieval.
src/jobs.rs Adds a scheduled billing refresh with four-way bounded concurrency.
frontend/pages/settings/providers.vue Adds billing cards and configuration controls, but mislabels first-refresh authorization failures.
migrations/20260716000000_provider_billing.sql Adds billing connection and snapshot tables plus translated UI labels.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant UI as Providers UI
    participant API as Admin Billing API
    participant DB as Billing Cache
    participant Upstream as OpenAI/OpenRouter
    participant Job as Refresh Job

    UI->>API: Load billing overviews
    API->>DB: Read snapshots and local spend
    DB-->>API: Cached billing data
    API-->>UI: Render provider cards

    UI->>API: Save encrypted configuration
    API->>DB: Upsert billing connection

    UI->>API: Request manual refresh
    API->>Upstream: Fetch billing metrics
    Upstream-->>API: Costs, thresholds, or credits
    API->>DB: Save snapshot or failure status
    API-->>UI: Return updated overview

    Job->>DB: List enabled connections
    loop Four concurrent refreshes
        Job->>Upstream: Fetch billing metrics
        Job->>DB: Save snapshot or failure status
    end
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant UI as Providers UI
    participant API as Admin Billing API
    participant DB as Billing Cache
    participant Upstream as OpenAI/OpenRouter
    participant Job as Refresh Job

    UI->>API: Load billing overviews
    API->>DB: Read snapshots and local spend
    DB-->>API: Cached billing data
    API-->>UI: Render provider cards

    UI->>API: Save encrypted configuration
    API->>DB: Upsert billing connection

    UI->>API: Request manual refresh
    API->>Upstream: Fetch billing metrics
    Upstream-->>API: Costs, thresholds, or credits
    API->>DB: Save snapshot or failure status
    API-->>UI: Return updated overview

    Job->>DB: List enabled connections
    loop Four concurrent refreshes
        Job->>Upstream: Fetch billing metrics
        Job->>DB: Save snapshot or failure status
    end
Loading

Reviews (1): Last reviewed commit: "Implement provider billing synchronizati..." | Re-trigger Greptile

Greptile also left 2 inline comments on this PR.

Context used (4)

  • Context used - CodeGraph MCP usage guide — when to use which tool (source)
  • Context used - src/types/AGENTS.md (source)
  • Context used - CLAUDE.md (source)
  • Context used - AGENTS.md (source)

Summary by CodeRabbit

  • New Features

    • Added provider billing management for supported providers, including credentials, scopes, usage, limits, spending, sync status, and refresh controls.
    • Billing data now refreshes automatically in the background and can be manually refreshed from Settings.
    • Added OpenAI and OpenRouter billing metrics, including monthly spend and credit information.
    • Added local credential encryption with automatic key generation and support for shared deployment keys.
  • Bug Fixes

    • Improved handling of invalid or undecryptable provider credentials, with clearer errors and safer startup behavior.

- Add background job to periodically refresh provider billing metrics every 15 minutes
- Implement billing adapters for OpenAI (project costs/alerts) and OpenRouter (key limits/credits)
- Add admin REST API for listing, getting, updating, and manually refreshing billing connections
- Support API key encryption for billing credentials with mandatory `ENCRYPTION_KEY` checks
- Integrate local spend tracking from `usage_events` to show comparative monthly consumption
- Update frontend provider settings with billing status, progress bars, and configuration tabs
- Bump `omniference` to 0.3.1
@coderabbitai

coderabbitai Bot commented Jul 18, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 403dfe03-42ed-4510-aa21-f7f2fa8929fe

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This change adds provider billing storage, upstream OpenAI/OpenRouter metric retrieval, scheduled refreshes, admin APIs, and settings-page controls. It also introduces persistent credential encryption with versioned ciphertext, legacy compatibility, typed errors, and propagated decryption failures.

Changes

Provider billing and encryption

Layer / File(s) Summary
Persistent credential encryption
.env.example, src/utils/encryption.rs, src/ai.rs, src/routes/admin/providers.rs, src/utils/providers.rs, src/utils/tools/builtin/imagegen.rs, src/main.rs, src/tests/*
Encryption now uses persisted or explicitly supplied keys, versioned authenticated ciphertext, legacy decryption support, typed errors, and explicit failure handling across provider flows.
Billing contracts and database schema
migrations/20260716000000_provider_billing.sql, src/types/providers/{billing.rs,requests.rs,responses.rs}, frontend/types/providers.ts, src/types/usage/repository.rs
Billing connection, snapshot, metric, request, response, frontend, translation, and current-month spend structures are added.
Billing repository and overview assembly
src/types/providers/{mod.rs,billing_repository.rs}
Repository methods persist connections and snapshots, list enabled providers, aggregate local spend, and construct billing overviews.
Upstream billing refresh
src/utils/provider_billing/*, src/jobs.rs
OpenAI and OpenRouter billing metrics are fetched, normalized, persisted, and periodically refreshed with bounded concurrency and failure tracking.
Administrative billing API and settings UI
src/routes/{mod.rs,admin/*}, frontend/stores/providerBillingStore.ts, frontend/pages/settings/providers.vue
Authorized billing CRUD and refresh endpoints are wired to a Pinia store and provider settings tabs displaying upstream and local billing information.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

  • NxtCore/OxideChat#42: Adds the usage analytics infrastructure used by the new provider spend query.
  • NxtCore/OxideChat#44: Overlaps the image-generation provider configuration path changed for encryption error handling.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 57.97% 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 summarizes the main addition: provider budgeting and billing features.
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 unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/provider-budgets

Comment @coderabbitai help to get the list of available commands.

Comment on lines +174 to +175
.unwrap_or_else(|| match row.provider_kind {
ProviderKind::Google | ProviderKind::OpenaiCompat | ProviderKind::Custom => ProviderBillingStatus::Unsupported,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Anthropic Gets Configurable Status

Anthropic is rejected as unsupported by the update route, but this fallback returns NOT_CONFIGURED for it. Every Anthropic card therefore shows billing as setup-required even though the API will reject any attempt to enable it.

Context Used: AGENTS.md (source)

Comment thread frontend/pages/settings/providers.vue Outdated
Comment on lines +515 to +521
function billingStatusKey(billing: ProviderBillingOverview) {
if ((billing.status === 'UPSTREAM_ERROR' || billing.status === 'UNAUTHORIZED') && billing.upstream) return 'settings.providers.billing.failed';
if (billing.status === 'AVAILABLE' && billing.upstream) return 'settings.providers.billing.provider_reported';
if (billing.status === 'UNSUPPORTED') return 'settings.providers.billing.unsupported';
if (billing.status === 'NOT_CONFIGURED' || billing.status === 'UNAUTHORIZED') return 'settings.providers.billing.setup_required';
return 'settings.providers.billing.failed';
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Unauthorized State Looks Unconfigured

When the first refresh receives 401 or 403, the overview has UNAUTHORIZED with no cached upstream metric. This mapping labels that state as setup-required rather than a failed update, so an admin who supplied an invalid key receives the wrong diagnosis.

Context Used: AGENTS.md (source)

…e encryption utilities

- Remove unused `.plan/provider-billing-overview.md` file
- Update encryption utilities:
  - Add `CredenatialDecryptionFailed` error and include it in upstream billing workflow
  - Refactor decryption logic and improve error handling for credentials
  - Extend `utils/encryption.rs` with a secure local key management feature for encryption initialization
@Henrik-3

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 18, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

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

🧹 Nitpick comments (3)
src/types/providers/mod.rs (1)

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

Do not re-export the private SQL row type.

Keep ProviderBillingOverviewRow internal to the billing repository/row implementation instead of exposing it through the domain entry point.

Proposed adjustment
-pub(crate) use billing::{ProviderBillingConnection, ProviderBillingMetric, ProviderBillingOverviewRow};
+pub(crate) use billing::{ProviderBillingConnection, ProviderBillingMetric};

As per coding guidelines, “In mod.rs, … avoid re-exporting private SQL row structs.”

🤖 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 `@src/types/providers/mod.rs` at line 16, Remove ProviderBillingOverviewRow
from the pub(crate) re-export in the providers module, while continuing to
re-export ProviderBillingConnection and ProviderBillingMetric. Keep
ProviderBillingOverviewRow accessible only within the billing repository/row
implementation.

Source: Coding guidelines

src/types/providers/requests.rs (1)

5-11: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Document the new public billing API contracts.

  • src/types/providers/requests.rs#L5-L11: document update and credential-preservation semantics.
  • src/types/providers/responses.rs#L8-L110: document status, metric, monetary, and timestamp semantics.

As per coding guidelines, “Document public APIs with /// doc comments.”

🤖 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 `@src/types/providers/requests.rs` around lines 5 - 11, The public billing API
types need `///` documentation. In src/types/providers/requests.rs lines 5-11,
document UpdateProviderBillingRequest and clarify update behavior, including how
omitted credential values preserve existing credentials. In
src/types/providers/responses.rs lines 8-110, document each public response type
and field’s status, metric, monetary, and timestamp semantics as applicable.

Source: Coding guidelines

frontend/pages/settings/providers.vue (1)

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

Replace fixed fallback/status colors with theme tokens.

Use text-primary, bg-primary/10, and text-primary so these elements follow customized themes.

As per coding guidelines: “Use customizable Tailwind tokens like bg-primary and text-primary instead of fixed colors.”

Also applies to: 224-225

🤖 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 `@frontend/pages/settings/providers.vue` at line 32, Replace the fixed `#6366f1`
color in the BrainCircuit fallback/status styling and the corresponding elements
around lines 224-225 with customizable Tailwind theme tokens: use text-primary
for foreground/icon colors and bg-primary/10 for the background. Remove inline
fixed-color styles while preserving the existing visual states and structure.

Source: Coding guidelines

🤖 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 `@frontend/pages/settings/providers.vue`:
- Around line 45-52: Rename the ShadCN components used throughout the settings
providers view to their auto-injected Shad-prefixed names, including Button,
Dialog, Input, Switch, Tabs, and related components. Update all template
references and remove any now-unnecessary imports, preserving the existing
props, events, and behavior.
- Around line 515-520: Update billingStatusKey so every UNAUTHORIZED status
returns settings.providers.billing.failed regardless of whether billing.upstream
exists; restrict settings.providers.billing.setup_required to NOT_CONFIGURED
only, while preserving the existing AVAILABLE and UNSUPPORTED mappings.
- Around line 636-650: Update addCustomProvider to reset the dialog’s active tab
to the default custom-provider tab before setting dialogOpen.value to true,
ensuring the dialog content and Save button render correctly regardless of the
previously selected tab.
- Around line 497-512: Update the provider-selection flow around loadBillingForm
so opening a provider immediately resets the billing form before starting the
request. In loadBillingForm, only apply the fetched billing values when the
currently selected provider still matches providerId; discard late responses for
previously selected providers to prevent stale data from overwriting the active
form.

In `@migrations/20260716000000_provider_billing.sql`:
- Around line 17-18: Update the provider_id foreign key in
provider_billing_snapshots to reference
provider_billing_connections(provider_id) with cascading deletes, replacing the
current providers(id) reference so snapshots cannot outlive their billing
connection.

In `@src/routes/admin/provider_billing.rs`:
- Line 62: Update the existing billing lookup in the admin provider flow to
propagate failures from Provider::find_billing_connection_for_admin as
InternalError instead of converting them to None via ok().flatten(). Preserve
the absent-connection behavior for successful queries that return no record,
while ensuring an existing OpenAI key with a database failure does not return
validation_failed.

In `@src/tests/encryption.rs`:
- Around line 29-33: Update the tampering test around decrypt_api_key to
Base64-decode encrypted before modifying it, flip a byte in the decoded
ciphertext/envelope rather than the trailing padding, then Base64-encode it
again. Assert that decryption returns the specific
EncryptionError::DecryptionFailed variant, preserving coverage of AES-GCM
authentication failure.

In `@src/types/providers/billing_repository.rs`:
- Around line 52-59: Update the upsert conflict handler in the billing
repository so changes to the credential or external scope invalidate cached
billing state transactionally. Detect whether the incoming credential,
external_scope_id, or external_scope_name differs from the existing
provider_billing_connections values, then reset or delete the associated
snapshot and status while preserving the current behavior for unchanged
configuration.
- Around line 170-177: Update the fallback match in the billing status logic to
include ProviderKind::Anthropic in the branch returning
ProviderBillingStatus::Unsupported, while preserving
ProviderBillingStatus::NotConfigured for other provider kinds.

In `@src/types/usage/repository.rs`:
- Around line 19-29: Replace the static query_as::<_, ProviderSpendRow>
monthly-spend query in src/types/usage/repository.rs:19-29 with the appropriate
checked SQLx query_as! macro, preserving its SQL, bindings, and returned fields.
In src/types/providers/billing_repository.rs:27-159, convert every static
runtime query_as and query call to the corresponding query_as! or query! macro,
ensuring all SQL shapes and bind types remain equivalent.

In `@src/utils/encryption.rs`:
- Around line 219-226: Update the AlreadyExists branch in the key creation match
to retry read_key_file(path) until the creator finishes writing, using a bounded
timeout and an appropriate short delay between attempts. Preserve immediate
success for a complete key and return the final read error when the timeout
expires.

In `@src/utils/provider_billing/mod.rs`:
- Around line 14-15: Document every newly exposed provider-billing API with
accurate /// comments: in src/utils/provider_billing/mod.rs (lines 14-15), cover
the public ProviderBillingError type, code, and refresh function; in
src/utils/provider_billing/openai.rs (line 51), document fetch_metric and its
errors; in src/utils/provider_billing/openrouter.rs (lines 35-40), document both
public fetch functions and their errors; and in
src/routes/admin/provider_billing.rs (line 18), document each public route
handler. Include # Errors and # Panics sections wherever applicable, and make no
other changes.

In `@src/utils/provider_billing/openai.rs`:
- Around line 38-44: Update the spend-alert pagination loop and AlertPage to use
the endpoint’s cursor contract: track the response’s after/last_id values
instead of page/next_page, and pass the cursor using the after and last_id
request parameters. Preserve has_more termination and ensure multi-page project
alerts continue until no further cursor is provided.

In `@src/utils/provider_billing/openrouter.rs`:
- Around line 16-22: Update the KeyData deserialization fields so limit_reset is
represented as its reset-type value rather than a timestamp, and select the
matching period-specific usage field (usage_daily, usage_weekly, or
usage_monthly) instead of always using usage_monthly. Preserve serde defaults
and ensure downstream billing logic consumes the selected period consistently.

---

Nitpick comments:
In `@frontend/pages/settings/providers.vue`:
- Line 32: Replace the fixed `#6366f1` color in the BrainCircuit fallback/status
styling and the corresponding elements around lines 224-225 with customizable
Tailwind theme tokens: use text-primary for foreground/icon colors and
bg-primary/10 for the background. Remove inline fixed-color styles while
preserving the existing visual states and structure.

In `@src/types/providers/mod.rs`:
- Line 16: Remove ProviderBillingOverviewRow from the pub(crate) re-export in
the providers module, while continuing to re-export ProviderBillingConnection
and ProviderBillingMetric. Keep ProviderBillingOverviewRow accessible only
within the billing repository/row implementation.

In `@src/types/providers/requests.rs`:
- Around line 5-11: The public billing API types need `///` documentation. In
src/types/providers/requests.rs lines 5-11, document
UpdateProviderBillingRequest and clarify update behavior, including how omitted
credential values preserve existing credentials. In
src/types/providers/responses.rs lines 8-110, document each public response type
and field’s status, metric, monetary, and timestamp semantics as applicable.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 07de8515-5cd2-4292-b1b1-79a91fcc0392

📥 Commits

Reviewing files that changed from the base of the PR and between 0b0ff29 and c307675.

📒 Files selected for processing (28)
  • .env.example
  • .intent/.gitignore
  • frontend/pages/settings/providers.vue
  • frontend/stores/providerBillingStore.ts
  • frontend/types/providers.ts
  • migrations/20260716000000_provider_billing.sql
  • src/ai.rs
  • src/jobs.rs
  • src/main.rs
  • src/routes/admin/mod.rs
  • src/routes/admin/provider_billing.rs
  • src/routes/admin/providers.rs
  • src/routes/mod.rs
  • src/tests/encryption.rs
  • src/tests/mod.rs
  • src/types/providers/billing.rs
  • src/types/providers/billing_repository.rs
  • src/types/providers/mod.rs
  • src/types/providers/requests.rs
  • src/types/providers/responses.rs
  • src/types/usage/repository.rs
  • src/utils/encryption.rs
  • src/utils/mod.rs
  • src/utils/provider_billing/mod.rs
  • src/utils/provider_billing/openai.rs
  • src/utils/provider_billing/openrouter.rs
  • src/utils/providers.rs
  • src/utils/tools/builtin/imagegen.rs
💤 Files with no reviewable changes (1)
  • .intent/.gitignore

Comment on lines +45 to +52
<Button variant="outline" size="sm" class="gap-2" @click="openConfigDialog(item)">
<Settings2 class="h-4 w-4" />
<span v-if="!item.isConfigured">{{ store.getTranslation('settings.providers.configure') }}</span>
</Button>
<Button v-if="item.isConfigured" variant="outline" size="sm" class="gap-2" @click="syncProvider(item)">
<RotateCw class="h-4 w-4" />
</Button>
<Switch :modelValue="item.is_enabled || false" :disabled="!item.isConfigured" @update:modelValue="(val: boolean) => toggleProvider(item, val)" />

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Use the auto-injected Shad* component names consistently.

Replace imported Button, Dialog, Input, Switch, Tabs, and related components with ShadButton, ShadDialog, ShadInput, ShadSwitch, ShadTabs, etc.

As per coding guidelines: “Use Shad*-prefixed auto-injected ShadCN components such as ShadButton and ShadSelect.”

Also applies to: 65-75, 95-247, 260-265

🤖 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 `@frontend/pages/settings/providers.vue` around lines 45 - 52, Rename the
ShadCN components used throughout the settings providers view to their
auto-injected Shad-prefixed names, including Button, Dialog, Input, Switch,
Tabs, and related components. Update all template references and remove any
now-unnecessary imports, preserving the existing props, events, and behavior.

Source: Coding guidelines

Comment thread frontend/pages/settings/providers.vue Outdated
Comment on lines 497 to 512
dialogOpen.value = true;
if (item.isConfigured) loadBillingForm(item.id);
}

if (!isAlreadyAdded) {
result.push({
kind: template.kind,
name: template.name,
description: template.description,
template,
isConfigured: false,
is_enabled: false,
});
}
async function loadBillingForm(providerId: string) {
try {
const billing = await billingStore.fetchProviderBilling(providerId);
billingForm.isEnabled = billing.is_enabled;
billingForm.credential = '';
billingForm.scopeId = billing.external_scope_id ?? '';
billingForm.scopeName = billing.external_scope_name ?? '';
billingForm.hasCredential = billing.has_billing_credential;
billingForm.hasConnection = billing.is_enabled || billing.has_billing_credential || billing.external_scope_id !== null;
} catch (error) {
console.error('Failed to load billing configuration:', error);
}

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 | ⚡ Quick win

Prevent stale billing requests from overwriting another provider’s form.

Opening provider B while provider A’s request is pending allows A’s late response to populate B’s form; saving then writes A’s scope/enabled state to B. Reset immediately and discard responses unless the selected provider still matches providerId.

Proposed fix
 	dialogOpen.value = true;
-	if (item.isConfigured) loadBillingForm(item.id);
+	Object.assign(billingForm, {
+		isEnabled: false,
+		credential: '',
+		scopeId: '',
+		scopeName: '',
+		hasCredential: false,
+		hasConnection: false,
+	});
+	if (item.isConfigured) void loadBillingForm(item.id);
 }

 async function loadBillingForm(providerId: string) {
 	try {
 		const billing = await billingStore.fetchProviderBilling(providerId);
+		if (configForm.existingProvider?.id !== providerId) return;
 		billingForm.isEnabled = billing.is_enabled;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
dialogOpen.value = true;
if (item.isConfigured) loadBillingForm(item.id);
}
if (!isAlreadyAdded) {
result.push({
kind: template.kind,
name: template.name,
description: template.description,
template,
isConfigured: false,
is_enabled: false,
});
}
async function loadBillingForm(providerId: string) {
try {
const billing = await billingStore.fetchProviderBilling(providerId);
billingForm.isEnabled = billing.is_enabled;
billingForm.credential = '';
billingForm.scopeId = billing.external_scope_id ?? '';
billingForm.scopeName = billing.external_scope_name ?? '';
billingForm.hasCredential = billing.has_billing_credential;
billingForm.hasConnection = billing.is_enabled || billing.has_billing_credential || billing.external_scope_id !== null;
} catch (error) {
console.error('Failed to load billing configuration:', error);
}
dialogOpen.value = true;
Object.assign(billingForm, {
isEnabled: false,
credential: '',
scopeId: '',
scopeName: '',
hasCredential: false,
hasConnection: false,
});
if (item.isConfigured) void loadBillingForm(item.id);
}
async function loadBillingForm(providerId: string) {
try {
const billing = await billingStore.fetchProviderBilling(providerId);
if (configForm.existingProvider?.id !== providerId) return;
billingForm.isEnabled = billing.is_enabled;
billingForm.credential = '';
billingForm.scopeId = billing.external_scope_id ?? '';
billingForm.scopeName = billing.external_scope_name ?? '';
billingForm.hasCredential = billing.has_billing_credential;
billingForm.hasConnection = billing.is_enabled || billing.has_billing_credential || billing.external_scope_id !== null;
} catch (error) {
console.error('Failed to load billing configuration:', error);
}
🤖 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 `@frontend/pages/settings/providers.vue` around lines 497 - 512, Update the
provider-selection flow around loadBillingForm so opening a provider immediately
resets the billing form before starting the request. In loadBillingForm, only
apply the fetched billing values when the currently selected provider still
matches providerId; discard late responses for previously selected providers to
prevent stale data from overwriting the active form.

Comment thread frontend/pages/settings/providers.vue Outdated
Comment on lines +515 to +520
function billingStatusKey(billing: ProviderBillingOverview) {
if ((billing.status === 'UPSTREAM_ERROR' || billing.status === 'UNAUTHORIZED') && billing.upstream) return 'settings.providers.billing.failed';
if (billing.status === 'AVAILABLE' && billing.upstream) return 'settings.providers.billing.provider_reported';
if (billing.status === 'UNSUPPORTED') return 'settings.providers.billing.unsupported';
if (billing.status === 'NOT_CONFIGURED' || billing.status === 'UNAUTHORIZED') return 'settings.providers.billing.setup_required';
return 'settings.providers.billing.failed';

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Do not classify first-refresh authorization failures as missing setup.

When status === 'UNAUTHORIZED' and no prior snapshot exists, Line 519 returns setup_required. Classify UNAUTHORIZED as failed regardless of upstream; reserve setup-required for NOT_CONFIGURED.

Proposed fix
 function billingStatusKey(billing: ProviderBillingOverview) {
-	if ((billing.status === 'UPSTREAM_ERROR' || billing.status === 'UNAUTHORIZED') && billing.upstream) return 'settings.providers.billing.failed';
+	if (billing.status === 'UPSTREAM_ERROR' || billing.status === 'UNAUTHORIZED') return 'settings.providers.billing.failed';
 	if (billing.status === 'AVAILABLE' && billing.upstream) return 'settings.providers.billing.provider_reported';
 	if (billing.status === 'UNSUPPORTED') return 'settings.providers.billing.unsupported';
-	if (billing.status === 'NOT_CONFIGURED' || billing.status === 'UNAUTHORIZED') return 'settings.providers.billing.setup_required';
+	if (billing.status === 'NOT_CONFIGURED') return 'settings.providers.billing.setup_required';
 	return 'settings.providers.billing.failed';
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function billingStatusKey(billing: ProviderBillingOverview) {
if ((billing.status === 'UPSTREAM_ERROR' || billing.status === 'UNAUTHORIZED') && billing.upstream) return 'settings.providers.billing.failed';
if (billing.status === 'AVAILABLE' && billing.upstream) return 'settings.providers.billing.provider_reported';
if (billing.status === 'UNSUPPORTED') return 'settings.providers.billing.unsupported';
if (billing.status === 'NOT_CONFIGURED' || billing.status === 'UNAUTHORIZED') return 'settings.providers.billing.setup_required';
return 'settings.providers.billing.failed';
function billingStatusKey(billing: ProviderBillingOverview) {
if (billing.status === 'UPSTREAM_ERROR' || billing.status === 'UNAUTHORIZED') return 'settings.providers.billing.failed';
if (billing.status === 'AVAILABLE' && billing.upstream) return 'settings.providers.billing.provider_reported';
if (billing.status === 'UNSUPPORTED') return 'settings.providers.billing.unsupported';
if (billing.status === 'NOT_CONFIGURED') return 'settings.providers.billing.setup_required';
return 'settings.providers.billing.failed';
}
🤖 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 `@frontend/pages/settings/providers.vue` around lines 515 - 520, Update
billingStatusKey so every UNAUTHORIZED status returns
settings.providers.billing.failed regardless of whether billing.upstream exists;
restrict settings.providers.billing.setup_required to NOT_CONFIGURED only, while
preserving the existing AVAILABLE and UNSUPPORTED mappings.

Comment on lines +636 to +650
function addCustomProvider() {
selectedProvider.value = {
kind: 'OPENAI_COMPAT',
name: '',
description: '',
icon: null,
brandColor: '#6366f1',
isPreConfigured: false,
};
configForm.name = '';
configForm.apiKey = '';
configForm.baseUrl = '';
configForm.isEnabled = true;
configForm.existingProvider = null;
dialogOpen.value = true;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Reset the active tab before opening the custom-provider dialog.

If the previous dialog was on billing or catalog, the custom dialog opens without matching tab content; on catalog, the Save button also disappears.

Proposed fix
 function addCustomProvider() {
+	activeProviderTab.value = 'settings';
+	catalogLoaded.value = false;
+	catalogModels.value = [];
+	catalogSearch.value = '';
 	selectedProvider.value = {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function addCustomProvider() {
selectedProvider.value = {
kind: 'OPENAI_COMPAT',
name: '',
description: '',
icon: null,
brandColor: '#6366f1',
isPreConfigured: false,
};
configForm.name = '';
configForm.apiKey = '';
configForm.baseUrl = '';
configForm.isEnabled = true;
configForm.existingProvider = null;
dialogOpen.value = true;
function addCustomProvider() {
activeProviderTab.value = 'settings';
catalogLoaded.value = false;
catalogModels.value = [];
catalogSearch.value = '';
selectedProvider.value = {
kind: 'OPENAI_COMPAT',
name: '',
description: '',
icon: null,
brandColor: '`#6366f1`',
isPreConfigured: false,
};
configForm.name = '';
configForm.apiKey = '';
configForm.baseUrl = '';
configForm.isEnabled = true;
configForm.existingProvider = null;
dialogOpen.value = true;
🤖 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 `@frontend/pages/settings/providers.vue` around lines 636 - 650, Update
addCustomProvider to reset the dialog’s active tab to the default
custom-provider tab before setting dialogOpen.value to true, ensuring the dialog
content and Save button render correctly regardless of the previously selected
tab.

Comment on lines +17 to +18
CREATE TABLE provider_billing_snapshots (
provider_id UUID PRIMARY KEY REFERENCES providers(id) ON DELETE CASCADE,

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 | ⚡ Quick win

Tie snapshots to billing connections to prevent orphaned cached data.

A refresh that already loaded a connection can insert a snapshot after an admin concurrently deletes that connection, because this FK only requires the provider to exist. Reference provider_billing_connections(provider_id) so connection deletion atomically removes and prevents snapshots.

Proposed fix
 CREATE TABLE provider_billing_snapshots (
-    provider_id UUID PRIMARY KEY REFERENCES providers(id) ON DELETE CASCADE,
+    provider_id UUID PRIMARY KEY REFERENCES provider_billing_connections(provider_id) ON DELETE CASCADE,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
CREATE TABLE provider_billing_snapshots (
provider_id UUID PRIMARY KEY REFERENCES providers(id) ON DELETE CASCADE,
CREATE TABLE provider_billing_snapshots (
provider_id UUID PRIMARY KEY REFERENCES provider_billing_connections(provider_id) ON DELETE CASCADE,
🤖 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 `@migrations/20260716000000_provider_billing.sql` around lines 17 - 18, Update
the provider_id foreign key in provider_billing_snapshots to reference
provider_billing_connections(provider_id) with cascading deletes, replacing the
current providers(id) reference so snapshots cannot outlive their billing
connection.

Comment on lines +19 to +29
let rows = sqlx::query_as::<_, ProviderSpendRow>(
r#"SELECT provider_id AS provider_id, COALESCE(SUM(cost_total), 0)::numeric AS spent_amount
FROM usage_events
WHERE provider_id = ANY($1)
AND created_at >= date_trunc('month', NOW() AT TIME ZONE 'UTC') AT TIME ZONE 'UTC'
AND created_at < (date_trunc('month', NOW() AT TIME ZONE 'UTC') + INTERVAL '1 month') AT TIME ZONE 'UTC'
GROUP BY provider_id"#,
)
.bind(provider_ids)
.fetch_all(pool)
.await?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

set -euo pipefail

echo "== files =="
git ls-files 'src/types/usage/repository.rs' 'src/types/providers/billing_repository.rs' '.sqlx/*' | sed 's/^/ - /'

echo
echo "== usage repository =="
cat -n src/types/usage/repository.rs

echo
echo "== billing repository (selected ranges) =="
sed -n '1,240p' src/types/providers/billing_repository.rs | cat -n

echo
echo "== sqlx metadata presence =="
find .sqlx -maxdepth 1 -type f 2>/dev/null | sed 's/^/ - /' | head -n 80

Repository: NxtCore/OxideChat

Length of output: 20630


🏁 Script executed:

set -euo pipefail

echo "== code search for existing checked macros in billing path =="
rg -n "query_as!|query!" src/types/providers src/types/usage || true

echo
echo "== list sqlx metadata entries mentioning billing/provider usage =="
rg -n "provider_billing|usage_events|provider_id|spent_amount" .sqlx || true

Repository: NxtCore/OxideChat

Length of output: 331


🏁 Script executed:

set -euo pipefail

echo "== locate related type definitions =="
rg -n "struct ProviderSpendRow|struct ProviderBillingConnection|struct ProviderBillingOverviewRow|enum ProviderBillingStatus|struct ProviderBillingMetric|struct UpdateProviderBillingRequest" src/types -g '*.rs'

echo
echo "== read type definitions around matches =="
for f in \
  $(rg -l "struct ProviderSpendRow|struct ProviderBillingConnection|struct ProviderBillingOverviewRow|enum ProviderBillingStatus|struct ProviderBillingMetric|struct UpdateProviderBillingRequest" src/types -g '*.rs' | sort -u); do
  echo "--- $f ---"
  sed -n '1,260p' "$f" | cat -n
done

echo
echo "== offline sqlx metadata files =="
find .sqlx -type f | sed 's/^/ - /' | sort | head -n 200

Repository: NxtCore/OxideChat

Length of output: 19076


🏁 Script executed:

set -euo pipefail

# Show the definitions for the row/DTO types used by the reviewed queries.
for sym in ProviderSpendRow ProviderBillingConnection ProviderBillingOverviewRow ProviderBillingStatus ProviderBillingMetric UpdateProviderBillingRequest; do
  echo "== $sym =="
  rg -n "$sym" src/types -g '*.rs' || true
  echo
done

# Read only the files that define the matched types, if any.
files=$(rg -l "ProviderSpendRow|ProviderBillingConnection|ProviderBillingOverviewRow|ProviderBillingStatus|ProviderBillingMetric|UpdateProviderBillingRequest" src/types -g '*.rs' | sort -u)
for f in $files; do
  echo "--- $f ---"
  wc -l "$f"
  sed -n '1,220p' "$f" | cat -n
done

echo
echo "== sqlx offline metadata candidates =="
find .sqlx -type f 2>/dev/null | sort | sed -n '1,120p'

Repository: NxtCore/OxideChat

Length of output: 37945


Switch these static SQL statements to checked SQLx macros

  • src/types/usage/repository.rs:19-29 still uses query_as::<_, ProviderSpendRow> for a static monthly-spend read.
  • src/types/providers/billing_repository.rs:27-159 still uses runtime query_as/query for static reads and writes.

Convert them to query_as!/query! so the SQL shape and bind types are checked at compile time.

📍 Affects 2 files
  • src/types/usage/repository.rs#L19-L29 (this comment)
  • src/types/providers/billing_repository.rs#L27-L159
🤖 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 `@src/types/usage/repository.rs` around lines 19 - 29, Replace the static
query_as::<_, ProviderSpendRow> monthly-spend query in
src/types/usage/repository.rs:19-29 with the appropriate checked SQLx query_as!
macro, preserving its SQL, bindings, and returned fields. In
src/types/providers/billing_repository.rs:27-159, convert every static runtime
query_as and query call to the corresponding query_as! or query! macro, ensuring
all SQL shapes and bind types remain equivalent.

Source: Coding guidelines

Comment thread src/utils/encryption.rs
Comment on lines +219 to +226
match options.open(path) {
Ok(mut file) => {
file.write_all(encoded.as_bytes())?;
file.write_all(b"\n")?;
file.sync_all()?;
Ok((key, KeySource::Generated))
}
Err(error) if error.kind() == ErrorKind::AlreadyExists => read_key_file(path).map(|key| (key, KeySource::File)),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Handle concurrent key-file creation without reading a partial file.

create_new publishes the empty file before Lines 221-223 finish writing. A concurrent instance can hit AlreadyExists, immediately read the incomplete file, report InvalidKeyFile, and exit. Retry the read until the creator finishes, with a bounded timeout.

🤖 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 `@src/utils/encryption.rs` around lines 219 - 226, Update the AlreadyExists
branch in the key creation match to retry read_key_file(path) until the creator
finishes writing, using a bounded timeout and an appropriate short delay between
attempts. Preserve immediate success for a complete key and return the final
read error when the timeout expires.

Comment on lines +14 to +15
#[derive(Debug, Error, Clone, Copy)]
pub enum ProviderBillingError {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Document all newly exposed provider-billing APIs.

  • src/utils/provider_billing/mod.rs#L14-L15: document the public error type, code, and refresh function.
  • src/utils/provider_billing/openai.rs#L51-L51: document fetch_metric and its errors.
  • src/utils/provider_billing/openrouter.rs#L35-L40: document both public fetch functions and their errors.
  • src/routes/admin/provider_billing.rs#L18-L18: document each public route handler.

As per coding guidelines: “Document public APIs with /// doc comments” and “Include # Errors and # Panics sections in documentation where applicable.”

📍 Affects 4 files
  • src/utils/provider_billing/mod.rs#L14-L15 (this comment)
  • src/utils/provider_billing/openai.rs#L51-L51
  • src/utils/provider_billing/openrouter.rs#L35-L40
  • src/routes/admin/provider_billing.rs#L18-L18
🤖 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 `@src/utils/provider_billing/mod.rs` around lines 14 - 15, Document every newly
exposed provider-billing API with accurate /// comments: in
src/utils/provider_billing/mod.rs (lines 14-15), cover the public
ProviderBillingError type, code, and refresh function; in
src/utils/provider_billing/openai.rs (line 51), document fetch_metric and its
errors; in src/utils/provider_billing/openrouter.rs (lines 35-40), document both
public fetch functions and their errors; and in
src/routes/admin/provider_billing.rs (line 18), document each public route
handler. Include # Errors and # Panics sections wherever applicable, and make no
other changes.

Source: Coding guidelines

Comment thread src/utils/provider_billing/openai.rs Outdated
Comment on lines +38 to +44
struct AlertPage {
#[serde(default)]
data: Vec<SpendAlert>,
#[serde(default)]
has_more: bool,
next_page: Option<String>,
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
curl -fsSL https://raw.githubusercontent.com/openai/openai-python/main/src/openai/resources/admin/organization/projects/spend_alerts.py |
  rg -n 'after|SyncConversationCursorPage'
curl -fsSL https://raw.githubusercontent.com/openai/openai-python/main/src/openai/pagination.py |
  rg -n 'SyncConversationCursorPage|last_id|after'

Repository: NxtCore/OxideChat

Length of output: 1268


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file='src/utils/provider_billing/openai.rs'
wc -l "$file"
printf '\n--- lines 1-220 ---\n'
sed -n '1,220p' "$file" | cat -n

Repository: NxtCore/OxideChat

Length of output: 5850


Use the spend-alert cursor contract here. spend_alerts paginates with after and last_id, not page/next_page, so this will fail with InvalidResponse once a project spans multiple alert pages. Update both this loop and the AlertPage fields to match the endpoint.

🤖 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 `@src/utils/provider_billing/openai.rs` around lines 38 - 44, Update the
spend-alert pagination loop and AlertPage to use the endpoint’s cursor contract:
track the response’s after/last_id values instead of page/next_page, and pass
the cursor using the after and last_id request parameters. Preserve has_more
termination and ensure multi-page project alerts continue until no further
cursor is provided.

Comment on lines +16 to +22
struct KeyData {
limit: Option<Decimal>,
limit_remaining: Option<Decimal>,
limit_reset: Option<String>,
#[serde(default)]
usage_monthly: Decimal,
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
curl -fsSL https://openrouter.ai/docs/api/reference/limits.md |
  rg -n 'limit_reset|usage_daily|usage_weekly|usage_monthly'

Repository: NxtCore/OxideChat

Length of output: 952


🏁 Script executed:

#!/bin/bash
set -euo pipefail

tmp="$(mktemp)"
curl -fsSL https://openrouter.ai/docs/api/reference/limits.md >"$tmp"

sed -n '280,330p' "$tmp"

Repository: NxtCore/OxideChat

Length of output: 2767


limit_reset is a reset type, not a timestamp. Use the period-specific usage field (usage_daily, usage_weekly, or usage_monthly) instead of always reading usage_monthly.

🤖 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 `@src/utils/provider_billing/openrouter.rs` around lines 16 - 22, Update the
KeyData deserialization fields so limit_reset is represented as its reset-type
value rather than a timestamp, and select the matching period-specific usage
field (usage_daily, usage_weekly, or usage_monthly) instead of always using
usage_monthly. Preserve serde defaults and ensure downstream billing logic
consumes the selected period consistently.

Henrik-3 added 2 commits July 18, 2026 23:12
…ent, teams, tooling, and default models migration files.
…d, backend, and database to improve consistency and data integrity.
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