Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .env.template
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,10 @@
# When enabled, token usage is tracked separately from audit logs
# USAGE_ENABLED=true

# Enable/disable the admin usage pricing recalculation action (default: true)
# Requires USAGE_ENABLED=true and supported storage; false always hides/blocks it
# USAGE_PRICING_RECALCULATION_ENABLED=true

# Enforce returning usage data in streaming responses (default: true)
# When true, stream_options: {"include_usage": true} is automatically added
# to streaming requests for OpenAI-compatible providers
Expand Down
4 changes: 4 additions & 0 deletions config/config.example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,11 @@ logging:
only_model_interactions: true

usage:
# Usage actions require USAGE_ENABLED=true (or usage.enabled: true) and a supported
# storage backend; pricing recalculation appears only when both usage tracking and
# pricing_recalculation_enabled are enabled.
enabled: true
pricing_recalculation_enabled: true
enforce_returning_usage_data: true
buffer_size: 1000
flush_interval: 5
Expand Down
16 changes: 11 additions & 5 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,11 @@ type UsageConfig struct {
// Default: true
EnforceReturningUsageData bool `yaml:"enforce_returning_usage_data" env:"ENFORCE_RETURNING_USAGE_DATA"`

// PricingRecalculationEnabled controls whether the admin pricing recalculation action is available.
// Storage and pricing metadata support are still required; false always disables the feature.
// Default: true
PricingRecalculationEnabled bool `yaml:"pricing_recalculation_enabled" env:"USAGE_PRICING_RECALCULATION_ENABLED"`

// BufferSize is the number of usage entries to buffer before flushing
// Default: 1000
BufferSize int `yaml:"buffer_size" env:"USAGE_BUFFER_SIZE"`
Expand Down Expand Up @@ -1036,11 +1041,12 @@ func buildDefaultConfig() *Config {
OnlyModelInteractions: true,
},
Usage: UsageConfig{
Enabled: true,
EnforceReturningUsageData: true,
BufferSize: 1000,
FlushInterval: 5,
RetentionDays: 90,
Enabled: true,
EnforceReturningUsageData: true,
PricingRecalculationEnabled: true,
BufferSize: 1000,
FlushInterval: 5,
RetentionDays: 90,
},
Budgets: BudgetsConfig{
Enabled: true,
Expand Down
30 changes: 30 additions & 0 deletions config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ func clearAllConfigEnvVars(t *testing.T) {
"LOGGING_ONLY_MODEL_INTERACTIONS", "LOGGING_BUFFER_SIZE",
"LOGGING_FLUSH_INTERVAL", "LOGGING_RETENTION_DAYS",
"USAGE_ENABLED", "ENFORCE_RETURNING_USAGE_DATA",
"USAGE_PRICING_RECALCULATION_ENABLED",
"USAGE_BUFFER_SIZE", "USAGE_FLUSH_INTERVAL", "USAGE_RETENTION_DAYS",
"BUDGETS_ENABLED",
"GUARDRAILS_ENABLED", "ENABLE_GUARDRAILS_FOR_BATCH_PROCESSING",
Expand Down Expand Up @@ -163,6 +164,9 @@ func TestBuildDefaultConfig(t *testing.T) {
if !cfg.Usage.EnforceReturningUsageData {
t.Error("expected Usage.EnforceReturningUsageData=true")
}
if !cfg.Usage.PricingRecalculationEnabled {
t.Error("expected Usage.PricingRecalculationEnabled=true")
}
if cfg.Usage.BufferSize != 1000 {
t.Errorf("expected Usage.BufferSize=1000, got %d", cfg.Usage.BufferSize)
}
Expand Down Expand Up @@ -424,6 +428,28 @@ func TestLoadBudgetsEnabledDisablesBudgetsWhenUsageTrackingDisabledWithoutSeedBu
})
}

func TestLoadUsagePricingRecalculationFromYAML(t *testing.T) {
clearAllConfigEnvVars(t)

withTempDir(t, func(dir string) {
yaml := `
usage:
pricing_recalculation_enabled: false
`
if err := os.WriteFile(filepath.Join(dir, "config.yaml"), []byte(yaml), 0644); err != nil {
t.Fatalf("Failed to write config.yaml: %v", err)
}

result, err := Load()
if err != nil {
t.Fatalf("Load() failed: %v", err)
}
if result.Config.Usage.PricingRecalculationEnabled {
t.Fatal("expected usage pricing recalculation to be disabled from YAML")
}
})
}

func TestLoadDisabledBudgetsIgnoreMalformedBudgetEnv(t *testing.T) {
clearAllConfigEnvVars(t)

Expand Down Expand Up @@ -1122,6 +1148,7 @@ func TestLoad_EnvOverridesDefaults(t *testing.T) {
t.Setenv("MODELS_ENABLED_BY_DEFAULT", "false")
t.Setenv("KEEP_ONLY_ALIASES_AT_MODELS_ENDPOINT", "true")
t.Setenv("CONFIGURED_PROVIDER_MODELS_MODE", "allowlist")
t.Setenv("USAGE_PRICING_RECALCULATION_ENABLED", "false")
t.Setenv("STORAGE_TYPE", "postgresql")
t.Setenv("POSTGRES_URL", "postgres://localhost/test")
t.Setenv("POSTGRES_MAX_CONNS", "20")
Expand All @@ -1147,6 +1174,9 @@ func TestLoad_EnvOverridesDefaults(t *testing.T) {
if cfg.Models.ConfiguredProviderModelsMode != ConfiguredProviderModelsModeAllowlist {
t.Errorf("expected configured provider models mode allowlist from env, got %q", cfg.Models.ConfiguredProviderModelsMode)
}
if cfg.Usage.PricingRecalculationEnabled {
t.Error("expected usage pricing recalculation to be disabled from env")
}
if cfg.Storage.Type != "postgresql" {
t.Errorf("expected storage type postgresql, got %s", cfg.Storage.Type)
}
Expand Down
1 change: 1 addition & 0 deletions docs/advanced/configuration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ Storage is shared by audit logging, usage tracking, and future features like IAM
| Variable | Description | Default |
| ------------------------------ | ---------------------------------------------- | ------- |
| `USAGE_ENABLED` | Enable token usage tracking | `true` |
| `USAGE_PRICING_RECALCULATION_ENABLED` | Enable the admin usage pricing recalculation action when supported | `true` |
| `ENFORCE_RETURNING_USAGE_DATA` | Auto-add `include_usage` to streaming requests | `true` |
| `USAGE_BUFFER_SIZE` | In-memory buffer before flush | `1000` |
| `USAGE_FLUSH_INTERVAL` | Flush interval in seconds | `5` |
Expand Down
61 changes: 61 additions & 0 deletions internal/admin/dashboard/static/css/dashboard.css
Original file line number Diff line number Diff line change
Expand Up @@ -3846,6 +3846,58 @@ body.conversation-drawer-open {
max-width: 460px;
}

.pricing-recalculate-section {
width: 100%;
}

.pricing-recalculate-grid {
display: grid;
grid-template-columns: minmax(220px, 320px) minmax(260px, 360px);
align-items: end;
justify-content: start;
gap: 12px;
width: 100%;
}

.pricing-recalculate-date-field {
grid-column: 1 / -1;
max-width: 320px;
width: 100%;
}

.pricing-recalculate-filter-field {
max-width: 360px;
width: 100%;
}

.pricing-recalculate-date-field .date-picker,
.pricing-recalculate-date-field .date-picker-trigger {
width: 100%;
}

.pricing-recalculate-date-field .date-picker-trigger {
justify-content: space-between;
}

@media (min-width: 769px) {
.pricing-recalculate-date-field .date-picker-dropdown {
top: auto;
bottom: calc(100% + 6px);
right: auto;
left: 0;
}
}

.pricing-recalculate-actions {
display: flex;
flex-wrap: wrap;
gap: 10px;
}

.pricing-recalculate-dialog {
max-width: 480px;
}

.budget-override-dialog-backdrop {
z-index: 100;
}
Expand Down Expand Up @@ -4330,6 +4382,15 @@ body.conversation-drawer-open {
.budget-settings-actions .pagination-btn {
width: 100%;
}
.pricing-recalculate-grid {
grid-template-columns: 1fr;
}
.pricing-recalculate-actions {
width: 100%;
}
.pricing-recalculate-actions .pagination-btn {
width: 100%;
}

/* Date picker mobile */
.date-picker-dropdown {
Expand Down
112 changes: 111 additions & 1 deletion internal/admin/dashboard/static/js/dashboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,22 @@ function dashboard() {
needsAuth: false,
apiKey: "",
authDialogOpen: false,
typedConfirmationDialog: {
open: false,
title: "",
titleId: "typedConfirmationDialogTitle",
inputId: "typed-confirmation-input",
message: "",
requiredText: "",
value: "",
confirmLabel: "Confirm",
icon: "alert-triangle",
dialogClass: "",
loadingKey: "",
errorKey: "",
onConfirm: null,
onClose: null,
},
authRequestGeneration: 0,
theme: "system",
sidebarCollapsed: false,
Expand Down Expand Up @@ -391,10 +407,98 @@ function dashboard() {
(this.page === "guardrails" && this.guardrailFormOpen) ||
(this.page === "auth-keys" && this.authKeyFormOpen) ||
(this.page === "budgets" && this.budgetFormOpen) ||
this.budgetResetDialogOpen
this.budgetResetDialogOpen ||
this.pricingRecalculateDialogOpen ||
(this.typedConfirmationDialog && this.typedConfirmationDialog.open)
);
},

openTypedConfirmationDialog(options) {
const next = {
open: true,
title: "",
titleId: "typedConfirmationDialogTitle",
inputId: "typed-confirmation-input",
message: "",
requiredText: "",
value: "",
confirmLabel: "Confirm",
icon: "alert-triangle",
dialogClass: "",
loadingKey: "",
errorKey: "",
onConfirm: null,
onClose: null,
...(options || {}),
};
this.typedConfirmationDialog = next;
setTimeout(() => {
const input = document.getElementById(next.inputId);
if (input && typeof input.focus === "function") {
input.focus();
}
if (typeof this.renderIconsAfterUpdate === "function") {
this.renderIconsAfterUpdate();
}
}, 0);
},

closeTypedConfirmationDialog() {
const current = this.typedConfirmationDialog || {};
if (typeof current.onClose === "function") {
current.onClose.call(this);
}
this.typedConfirmationDialog = {
open: false,
title: "",
titleId: "typedConfirmationDialogTitle",
inputId: "typed-confirmation-input",
message: "",
requiredText: "",
value: "",
confirmLabel: "Confirm",
icon: "alert-triangle",
dialogClass: "",
loadingKey: "",
errorKey: "",
onConfirm: null,
onClose: null,
};
},

typedConfirmationReady() {
const dialog = this.typedConfirmationDialog || {};
return (
String(dialog.value || "").trim().toLowerCase() ===
String(dialog.requiredText || "").trim().toLowerCase()
);
},

typedConfirmationLoading() {
const dialog = this.typedConfirmationDialog || {};
const key = String(dialog.loadingKey || "").trim();
return key ? !!this[key] : false;
},

typedConfirmationInputLabel() {
const dialog = this.typedConfirmationDialog || {};
return "Type " + String(dialog.requiredText || "").trim() + " to confirm";
},

async submitTypedConfirmationDialog() {
const dialog = this.typedConfirmationDialog || {};
if (!this.typedConfirmationReady()) {
const errorKey = String(dialog.errorKey || "").trim();
if (errorKey) {
this[errorKey] = this.typedConfirmationInputLabel() + ".";
}
return;
}
if (typeof dialog.onConfirm === "function") {
await dialog.onConfirm.call(this);
}
},

submitApiKey() {
const apiKey = this.normalizeApiKey(this.apiKey);
if (!apiKey) {
Expand Down Expand Up @@ -970,6 +1074,12 @@ function dashboard() {
: null,
"dashboardBudgetsModule",
),
resolveModuleFactory(
typeof dashboardPricingModule === "function"
? dashboardPricingModule
: null,
"dashboardPricingModule",
),
resolveModuleFactory(
typeof dashboardWorkflowsModule === "function"
? dashboardWorkflowsModule
Expand Down
46 changes: 39 additions & 7 deletions internal/admin/dashboard/static/js/modules/budgets.js
Original file line number Diff line number Diff line change
Expand Up @@ -826,15 +826,45 @@
this.budgetResetConfirmation = '';
this.budgetSettingsError = '';
this.budgetResetDialogOpen = true;
setTimeout(() => {
const input = document.getElementById('budget-reset-confirmation');
if (input && typeof input.focus === 'function') {
input.focus();
}
}, 0);
if (typeof this.openTypedConfirmationDialog === 'function') {
this.openTypedConfirmationDialog({
title: 'Reset Budgets',
titleId: 'budgetResetDialogTitle',
inputId: 'budget-reset-confirmation',
requiredText: 'reset',
confirmLabel: 'Reset All Budgets',
icon: 'rotate-ccw',
dialogClass: 'budget-reset-dialog',
loadingKey: 'budgetResetLoading',
errorKey: 'budgetSettingsError',
onConfirm: () => this.resetBudgets(),
onClose: () => {
this.budgetResetDialogOpen = false;
this.budgetResetConfirmation = '';
}
});
} else {
setTimeout(() => {
const input = document.getElementById('budget-reset-confirmation');
if (input && typeof input.focus === 'function') {
input.focus();
}
}, 0);
}
},

budgetResetConfirmationValue() {
if (this.typedConfirmationDialog && this.typedConfirmationDialog.open) {
return this.typedConfirmationDialog.value;
}
return this.budgetResetConfirmation;
},

closeBudgetResetDialog() {
if (this.typedConfirmationDialog && this.typedConfirmationDialog.open && typeof this.closeTypedConfirmationDialog === 'function') {
this.closeTypedConfirmationDialog();
return;
}
this.budgetResetDialogOpen = false;
this.budgetResetConfirmation = '';
},
Expand All @@ -843,7 +873,9 @@
if (this.budgetResetLoading) {
return;
}
if (String(this.budgetResetConfirmation || '').trim().toLowerCase() !== 'reset') {
const confirmation = this.budgetResetConfirmationValue();
this.budgetResetConfirmation = confirmation;
if (String(confirmation || '').trim().toLowerCase() !== 'reset') {
this.budgetSettingsError = 'Type reset to confirm.';
return;
}
Expand Down
Loading
Loading