Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
"""add api key usage sections

Revision ID: 20260601_010000_add_api_key_usage_sections
Revises: 20260602_060000_merge_account_workspace_and_failure_heads
Create Date: 2026-06-01 01:00:00.000000
"""

from __future__ import annotations

import sqlalchemy as sa
from alembic import op
from sqlalchemy.engine import Connection

revision = "20260601_010000_add_api_key_usage_sections"
down_revision = "20260602_060000_merge_account_workspace_and_failure_heads"
branch_labels = None
depends_on = None


def _table_exists(connection: Connection, table_name: str) -> bool:
inspector = sa.inspect(connection)
return inspector.has_table(table_name)


def _columns(connection: Connection, table_name: str) -> set[str]:
inspector = sa.inspect(connection)
if not inspector.has_table(table_name):
return set()
return {str(column["name"]) for column in inspector.get_columns(table_name) if column.get("name") is not None}


def upgrade() -> None:
bind = op.get_bind()
if not _table_exists(bind, "api_keys"):
return

existing_columns = _columns(bind, "api_keys")
with op.batch_alter_table("api_keys") as batch_op:
if "usage_sections" not in existing_columns:
batch_op.add_column(
sa.Column(
"usage_sections",
sa.Text(),
nullable=False,
server_default="upstream_limits,account_pool_usage",
)
)


def downgrade() -> None:
bind = op.get_bind()
if not _table_exists(bind, "api_keys"):
return

existing_columns = _columns(bind, "api_keys")
with op.batch_alter_table("api_keys") as batch_op:
if "usage_sections" in existing_columns:
batch_op.drop_column("usage_sections")
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
"""add hide upstream quota from api keys setting

Revision ID: 20260608_000000_add_hide_upstream_quota_from_api_keys
Revises: 20260607_000000_merge_weekly_monthly_useragent_heads
Create Date: 2026-06-08 00:00:00.000000
"""

from __future__ import annotations

import sqlalchemy as sa
from alembic import op
from sqlalchemy.engine import Connection

revision = "20260608_000000_add_hide_upstream_quota_from_api_keys"
down_revision = "20260607_000000_merge_weekly_monthly_useragent_heads"
branch_labels = None
depends_on = None


def _has_table(connection: Connection, table_name: str) -> bool:
return sa.inspect(connection).has_table(table_name)


def _columns(connection: Connection, table_name: str) -> set[str]:
if not _has_table(connection, table_name):
return set()
return {column["name"] for column in sa.inspect(connection).get_columns(table_name)}


def upgrade() -> None:
bind = op.get_bind()
dashboard_columns = _columns(bind, "dashboard_settings")
if dashboard_columns and "hide_upstream_quota_from_api_keys" not in dashboard_columns:
with op.batch_alter_table("dashboard_settings") as batch_op:
batch_op.add_column(
sa.Column(
"hide_upstream_quota_from_api_keys",
sa.Boolean(),
server_default=sa.false(),
nullable=False,
)
)


def downgrade() -> None:
bind = op.get_bind()
dashboard_columns = _columns(bind, "dashboard_settings")
if dashboard_columns and "hide_upstream_quota_from_api_keys" in dashboard_columns:
with op.batch_alter_table("dashboard_settings") as batch_op:
batch_op.drop_column("hide_upstream_quota_from_api_keys")
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
"""merge API key usage sections and quota visibility heads

Revision ID: 20260608_010000_merge_api_key_usage_and_quota_visibility_heads
Revises: 20260601_010000_add_api_key_usage_sections,
20260608_000000_add_hide_upstream_quota_from_api_keys
Create Date: 2026-06-08 01:00:00.000000
"""

from __future__ import annotations

revision = "20260608_010000_merge_api_key_usage_and_quota_visibility_heads"
down_revision = (
"20260601_010000_add_api_key_usage_sections",
"20260608_000000_add_hide_upstream_quota_from_api_keys",
)
branch_labels = None
depends_on = None


def upgrade() -> None:
pass


def downgrade() -> None:
pass
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"""merge API key quota visibility and dashboard guest heads

Revision ID: 20260616_000000_merge_api_key_quota_and_dashboard_guest_heads
Revises:
- 20260608_010000_merge_api_key_usage_and_quota_visibility_heads
- 20260611_000000_merge_dashboard_guest_and_weekly_useragent_heads
Create Date: 2026-06-16 00:00:00.000000
"""

from __future__ import annotations

revision = "20260616_000000_merge_api_key_quota_and_dashboard_guest_heads"
down_revision = (
"20260608_010000_merge_api_key_usage_and_quota_visibility_heads",
"20260611_000000_merge_dashboard_guest_and_weekly_useragent_heads",
)
branch_labels = None
depends_on = None


def upgrade() -> None:
pass


def downgrade() -> None:
pass
12 changes: 12 additions & 0 deletions app/db/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,12 @@ class DashboardSettings(Base):
default=False,
nullable=False,
)
hide_upstream_quota_from_api_keys: Mapped[bool] = mapped_column(
Boolean,
default=False,
server_default=false(),
nullable=False,
)
totp_secret_encrypted: Mapped[bytes | None] = mapped_column(LargeBinary, nullable=True)
totp_last_verified_step: Mapped[int | None] = mapped_column(Integer, nullable=True)
http_responses_session_bridge_prompt_cache_idle_ttl_seconds: Mapped[int] = mapped_column(
Expand Down Expand Up @@ -625,6 +631,12 @@ class ApiKey(Base):
server_default=false(),
nullable=False,
)
usage_sections: Mapped[str | None] = mapped_column(
Text,
nullable=False,
default="upstream_limits,account_pool_usage",
server_default="upstream_limits,account_pool_usage",
)
expires_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now(), nullable=False)
Expand Down
8 changes: 8 additions & 0 deletions app/modules/api_keys/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ def _to_response(row: ApiKeyData) -> ApiKeyResponse:
enforced_reasoning_effort=row.enforced_reasoning_effort,
enforced_service_tier=row.enforced_service_tier,
traffic_class=row.traffic_class,
usage_sections=row.usage_sections,
expires_at=row.expires_at,
is_active=row.is_active,
account_assignment_scope_enabled=row.account_assignment_scope_enabled,
Expand Down Expand Up @@ -133,6 +134,11 @@ async def create_api_key(
enforced_reasoning_effort=payload.enforced_reasoning_effort,
enforced_service_tier=payload.enforced_service_tier,
traffic_class=payload.traffic_class or "foreground",
usage_sections=(
payload.usage_sections
if payload.usage_sections is not None
else "upstream_limits,account_pool_usage"
),
expires_at=payload.expires_at,
assigned_account_ids=payload.assigned_account_ids,
limits=limit_inputs,
Expand Down Expand Up @@ -188,6 +194,8 @@ async def update_api_key(
enforced_service_tier_set="enforced_service_tier" in fields,
traffic_class=payload.traffic_class,
traffic_class_set="traffic_class" in fields,
usage_sections=payload.usage_sections,
usage_sections_set="usage_sections" in fields,
expires_at=payload.expires_at,
expires_at_set="expires_at" in fields,
is_active=payload.is_active,
Expand Down
47 changes: 46 additions & 1 deletion app/modules/api_keys/repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from datetime import datetime
from enum import Enum

from sqlalchemy import Integer, cast, delete, func, select, true, update
from sqlalchemy import BigInteger, Integer, cast, delete, func, select, true, update
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import load_only, selectinload

Expand Down Expand Up @@ -256,6 +256,47 @@ async def get_usage_summary_by_key_id(self, key_id: str) -> ApiKeyUsageSummary:
total_cost_usd=round(float(row.total_cost_usd or 0.0), 6),
)

async def get_limit_usage_value(
self,
key_id: str,
*,
limit_type: LimitType,
since: datetime,
until: datetime,
model_filter: str | None,
) -> int:
if limit_type == LimitType.CREDITS:
return 0

if limit_type == LimitType.TOTAL_TOKENS:
value_expr = func.coalesce(RequestLog.input_tokens, 0) + func.coalesce(
RequestLog.output_tokens,
RequestLog.reasoning_tokens,
0,
)
elif limit_type == LimitType.INPUT_TOKENS:
value_expr = func.coalesce(RequestLog.input_tokens, 0)
elif limit_type == LimitType.OUTPUT_TOKENS:
value_expr = func.coalesce(RequestLog.output_tokens, RequestLog.reasoning_tokens, 0)
elif limit_type == LimitType.COST_USD:
value_expr = cast(func.floor(func.coalesce(RequestLog.cost_usd, 0.0) * 1_000_000), BigInteger)
else:
return 0

stmt = select(func.coalesce(func.sum(value_expr), 0)).where(
RequestLog.api_key_id == key_id,
RequestLog.status == "success",
self._exclude_warmup_clause(),
RequestLog.requested_at >= since,
RequestLog.requested_at < until,
)
Comment thread
Komzpa marked this conversation as resolved.
if model_filter is not None:
stmt = stmt.where(RequestLog.model == model_filter)

result = await self._session.execute(stmt)
value = result.scalar_one()
return int(value or 0)

async def update(
self,
key_id: str,
Expand All @@ -267,6 +308,7 @@ async def update(
enforced_reasoning_effort: str | None | _Unset = _UNSET,
enforced_service_tier: str | None | _Unset = _UNSET,
traffic_class: str | _Unset = _UNSET,
usage_sections: str | _Unset = _UNSET,
account_assignment_scope_enabled: bool | _Unset = _UNSET,
expires_at: datetime | None | _Unset = _UNSET,
is_active: bool | _Unset = _UNSET,
Expand Down Expand Up @@ -298,6 +340,9 @@ async def update(
if traffic_class is not _UNSET:
assert isinstance(traffic_class, str)
row.traffic_class = traffic_class
if usage_sections is not _UNSET:
assert isinstance(usage_sections, str)
row.usage_sections = usage_sections
if account_assignment_scope_enabled is not _UNSET:
assert isinstance(account_assignment_scope_enabled, bool)
row.account_assignment_scope_enabled = account_assignment_scope_enabled
Expand Down
3 changes: 3 additions & 0 deletions app/modules/api_keys/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ class ApiKeyCreateRequest(DashboardModel):
enforced_reasoning_effort: str | None = Field(default=None, pattern=r"(?i)^(none|minimal|low|medium|high|xhigh)$")
enforced_service_tier: str | None = Field(default=None, pattern=r"(?i)^(auto|default|priority|flex|fast)$")
traffic_class: str | None = Field(default=None, pattern=r"(?i)^(foreground|opportunistic)$")
usage_sections: str | None = None
weekly_token_limit: int | None = Field(default=None, ge=1)
expires_at: datetime | None = None
assigned_account_ids: list[str] | None = None
Expand All @@ -46,6 +47,7 @@ class ApiKeyUpdateRequest(DashboardModel):
enforced_reasoning_effort: str | None = Field(default=None, pattern=r"(?i)^(none|minimal|low|medium|high|xhigh)$")
enforced_service_tier: str | None = Field(default=None, pattern=r"(?i)^(auto|default|priority|flex|fast)$")
traffic_class: str | None = Field(default=None, pattern=r"(?i)^(foreground|opportunistic)$")
usage_sections: str | None = None
weekly_token_limit: int | None = Field(default=None, ge=1)
expires_at: datetime | None = None
is_active: bool | None = None
Expand All @@ -71,6 +73,7 @@ class ApiKeyResponse(DashboardModel):
enforced_reasoning_effort: str | None
enforced_service_tier: str | None
traffic_class: str
usage_sections: str = "upstream_limits,account_pool_usage"
expires_at: datetime | None
is_active: bool
account_assignment_scope_enabled: bool = False
Expand Down
Loading