交易流水对账明细 | AI引擎计价规则

This commit is contained in:
2026-07-10 16:57:23 +08:00
parent ad2e40bc67
commit 468184d352
50 changed files with 6998 additions and 506 deletions
@@ -0,0 +1,582 @@
"""add model pricing rules and credit pricing snapshots
Revision ID: 1c93b40133f0
Revises: 2026070902
Create Date: 2026-07-10 15:10:44.983521
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision: str = "1c93b40133f0"
down_revision: Union[str, None] = "2026070902"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
FK_CREDIT_RECORD_PRICING_RULE = (
"fk_credit_records_pricing_rule_id_model_pricing_rules"
)
def _json_type() -> sa.types.TypeEngine:
"""Use JSONB on PostgreSQL and JSON on other supported development DBs."""
return sa.JSON().with_variant(
postgresql.JSONB(astext_type=sa.Text()),
"postgresql",
)
def upgrade() -> None:
# 1. Versioned model pricing rules.
op.create_table(
"model_pricing_rules",
sa.Column("id", sa.String(length=32), nullable=False),
sa.Column("provider", sa.String(length=32), nullable=False),
sa.Column("model_name", sa.String(length=128), nullable=False),
sa.Column("model_category", sa.String(length=16), nullable=False),
sa.Column("billing_mode", sa.String(length=48), nullable=False),
sa.Column("calculator_version", sa.String(length=64), nullable=False),
sa.Column("version_code", sa.String(length=64), nullable=False),
sa.Column("effective_from", sa.DateTime(timezone=True), nullable=False),
sa.Column("effective_to", sa.DateTime(timezone=True), nullable=True),
sa.Column("publish_status", sa.String(length=16), nullable=False),
sa.Column("currency", sa.String(length=8), nullable=False),
sa.Column("rule_schema_version", sa.Integer(), nullable=False),
sa.Column("rule_json", _json_type(), nullable=False),
sa.Column("rule_content_hash", sa.String(length=64), nullable=False),
sa.Column("source_url", sa.Text(), nullable=True),
sa.Column("source_updated_at", sa.DateTime(timezone=True), nullable=True),
sa.Column("remark", sa.Text(), nullable=True),
sa.Column("created_by", sa.String(length=32), nullable=True),
sa.Column("updated_by", sa.String(length=32), nullable=True),
sa.Column(
"created_at",
sa.DateTime(timezone=True),
server_default=sa.text("now()"),
nullable=False,
),
sa.Column(
"updated_at",
sa.DateTime(timezone=True),
server_default=sa.text("now()"),
nullable=False,
),
# A disabled future rule is represented by an empty interval where
# effective_to == effective_from, so equality must be allowed.
sa.CheckConstraint(
"effective_to IS NULL OR effective_to >= effective_from",
name="ck_model_pricing_rules_effective_range",
),
sa.PrimaryKeyConstraint("id"),
)
op.create_index(
"uq_model_pricing_rules_provider_model_version",
"model_pricing_rules",
["provider", "model_name", "version_code"],
unique=True,
)
op.create_index(
"ix_model_pricing_rules_resolve",
"model_pricing_rules",
[
"provider",
"model_name",
"publish_status",
"effective_from",
"effective_to",
],
unique=False,
)
op.create_index(
"ix_model_pricing_rules_category_status",
"model_pricing_rules",
["model_category", "publish_status"],
unique=False,
)
op.create_index(
"ix_model_pricing_rules_provider",
"model_pricing_rules",
["provider"],
unique=False,
)
op.create_index(
"ix_model_pricing_rules_model_name",
"model_pricing_rules",
["model_name"],
unique=False,
)
op.create_index(
"ix_model_pricing_rules_model_category",
"model_pricing_rules",
["model_category"],
unique=False,
)
op.create_index(
"ix_model_pricing_rules_billing_mode",
"model_pricing_rules",
["billing_mode"],
unique=False,
)
op.create_index(
"ix_model_pricing_rules_calculator_version",
"model_pricing_rules",
["calculator_version"],
unique=False,
)
op.create_index(
"ix_model_pricing_rules_effective_from",
"model_pricing_rules",
["effective_from"],
unique=False,
)
op.create_index(
"ix_model_pricing_rules_effective_to",
"model_pricing_rules",
["effective_to"],
unique=False,
)
op.create_index(
"ix_model_pricing_rules_publish_status",
"model_pricing_rules",
["publish_status"],
unique=False,
)
op.create_index(
"ix_model_pricing_rules_rule_content_hash",
"model_pricing_rules",
["rule_content_hash"],
unique=False,
)
# 2. Bind provider callbacks to the exact billing attempt.
op.add_column(
"chat_generation_tasks",
sa.Column("current_billing_attempt_no", sa.Integer(), nullable=True),
)
op.create_index(
"ix_chat_generation_tasks_current_billing_attempt_no",
"chat_generation_tasks",
["current_billing_attempt_no"],
unique=False,
)
# 3. Immutable pricing, usage, attachment, and output snapshots.
op.add_column(
"credit_records",
sa.Column("pricing_rule_id", sa.String(length=32), nullable=True),
)
op.add_column(
"credit_records",
sa.Column("pricing_version_code", sa.String(length=64), nullable=True),
)
op.add_column(
"credit_records",
sa.Column("pricing_billing_mode", sa.String(length=48), nullable=True),
)
op.add_column(
"credit_records",
sa.Column(
"pricing_calculator_version",
sa.String(length=64),
nullable=True,
),
)
op.add_column(
"credit_records",
sa.Column("pricing_usage_source", sa.String(length=32), nullable=True),
)
op.add_column(
"credit_records",
sa.Column("pricing_reference_at", sa.DateTime(timezone=True), nullable=True),
)
op.add_column(
"credit_records",
sa.Column("pricing_effective_from", sa.DateTime(timezone=True), nullable=True),
)
op.add_column(
"credit_records",
sa.Column("pricing_effective_to", sa.DateTime(timezone=True), nullable=True),
)
op.add_column(
"credit_records",
sa.Column("pricing_snapshot_schema_version", sa.Integer(), nullable=True),
)
op.add_column(
"credit_records",
sa.Column("pricing_snapshot_hash", sa.String(length=64), nullable=True),
)
op.add_column(
"credit_records",
sa.Column("provider_cost_currency", sa.String(length=8), nullable=True),
)
op.add_column(
"credit_records",
sa.Column(
"provider_cost_amount",
sa.Numeric(precision=20, scale=8),
nullable=True,
),
)
op.add_column(
"credit_records",
sa.Column("provider_cost_status", sa.String(length=32), nullable=True),
)
op.add_column(
"credit_records",
sa.Column(
"provider_cost_calculated_at",
sa.DateTime(timezone=True),
nullable=True,
),
)
op.add_column(
"credit_records",
sa.Column(
"provider_cost_finalized_at",
sa.DateTime(timezone=True),
nullable=True,
),
)
op.add_column(
"credit_records",
sa.Column(
"provider_usage_primary",
sa.Boolean(),
server_default=sa.text("true"),
nullable=False,
),
)
op.add_column(
"credit_records",
sa.Column(
"provider_cost_is_estimated",
sa.Boolean(),
server_default=sa.text("false"),
nullable=False,
),
)
op.add_column(
"credit_records",
sa.Column(
"attachment_image_count",
sa.Integer(),
server_default=sa.text("0"),
nullable=False,
),
)
op.add_column(
"credit_records",
sa.Column(
"attachment_video_count",
sa.Integer(),
server_default=sa.text("0"),
nullable=False,
),
)
op.add_column(
"credit_records",
sa.Column(
"attachment_audio_count",
sa.Integer(),
server_default=sa.text("0"),
nullable=False,
),
)
op.add_column(
"credit_records",
sa.Column(
"attachment_total_count",
sa.Integer(),
server_default=sa.text("0"),
nullable=False,
),
)
op.add_column(
"credit_records",
sa.Column(
"attachment_video_duration_seconds",
sa.Numeric(precision=20, scale=6),
server_default=sa.text("0"),
nullable=False,
),
)
op.add_column(
"credit_records",
sa.Column(
"attachment_audio_duration_seconds",
sa.Numeric(precision=20, scale=6),
server_default=sa.text("0"),
nullable=False,
),
)
op.add_column(
"credit_records",
sa.Column(
"requested_output_count",
sa.Integer(),
server_default=sa.text("0"),
nullable=False,
),
)
op.add_column(
"credit_records",
sa.Column(
"generated_image_count",
sa.Integer(),
server_default=sa.text("0"),
nullable=False,
),
)
op.add_column(
"credit_records",
sa.Column(
"generated_video_count",
sa.Integer(),
server_default=sa.text("0"),
nullable=False,
),
)
op.add_column(
"credit_records",
sa.Column(
"generated_total_count",
sa.Integer(),
server_default=sa.text("0"),
nullable=False,
),
)
op.add_column(
"credit_records",
sa.Column("pricing_snapshot_json", _json_type(), nullable=True),
)
op.add_column(
"credit_records",
sa.Column("usage_snapshot_json", _json_type(), nullable=True),
)
op.add_column(
"credit_records",
sa.Column("attachment_snapshot_json", _json_type(), nullable=True),
)
op.add_column(
"credit_records",
sa.Column("generation_snapshot_json", _json_type(), nullable=True),
)
op.create_index(
"ix_credit_records_pricing_rule_id",
"credit_records",
["pricing_rule_id"],
unique=False,
)
op.create_index(
"ix_credit_records_pricing_version_code",
"credit_records",
["pricing_version_code"],
unique=False,
)
op.create_index(
"ix_credit_records_pricing_usage_source",
"credit_records",
["pricing_usage_source"],
unique=False,
)
op.create_index(
"ix_credit_records_pricing_reference_at",
"credit_records",
["pricing_reference_at"],
unique=False,
)
op.create_index(
"ix_credit_records_provider_cost_status",
"credit_records",
["provider_cost_status"],
unique=False,
)
op.create_index(
"ix_credit_records_pricing_status_time",
"credit_records",
["provider_cost_status", "created_at"],
unique=False,
)
op.create_index(
"ix_credit_records_pricing_model_time",
"credit_records",
["engine_provider", "engine_model_name", "pricing_reference_at"],
unique=False,
)
op.create_index(
"ix_credit_records_pricing_version",
"credit_records",
["pricing_version_code", "pricing_rule_id"],
unique=False,
)
op.create_foreign_key(
FK_CREDIT_RECORD_PRICING_RULE,
"credit_records",
"model_pricing_rules",
["pricing_rule_id"],
["id"],
ondelete="RESTRICT",
)
# 4. Lock the engine and billing attempt for legacy project generations.
op.add_column(
"generation_records",
sa.Column("engine_id", sa.String(length=32), nullable=True),
)
op.add_column(
"generation_records",
sa.Column("engine_snapshot_json", sa.Text(), nullable=True),
)
op.add_column(
"generation_records",
sa.Column("provider_response_json", sa.Text(), nullable=True),
)
op.add_column(
"generation_records",
sa.Column("current_billing_attempt_no", sa.Integer(), nullable=True),
)
op.create_index(
"ix_generation_records_engine_id",
"generation_records",
["engine_id"],
unique=False,
)
op.create_index(
"ix_generation_records_current_billing_attempt_no",
"generation_records",
["current_billing_attempt_no"],
unique=False,
)
def downgrade() -> None:
# Reverse legacy generation record extensions.
op.drop_index(
"ix_generation_records_current_billing_attempt_no",
table_name="generation_records",
)
op.drop_index(
"ix_generation_records_engine_id",
table_name="generation_records",
)
op.drop_column("generation_records", "current_billing_attempt_no")
op.drop_column("generation_records", "provider_response_json")
op.drop_column("generation_records", "engine_snapshot_json")
op.drop_column("generation_records", "engine_id")
# Reverse credit pricing snapshots before dropping the referenced rule table.
op.drop_constraint(
FK_CREDIT_RECORD_PRICING_RULE,
"credit_records",
type_="foreignkey",
)
op.drop_index("ix_credit_records_pricing_version", table_name="credit_records")
op.drop_index("ix_credit_records_pricing_model_time", table_name="credit_records")
op.drop_index("ix_credit_records_pricing_status_time", table_name="credit_records")
op.drop_index("ix_credit_records_provider_cost_status", table_name="credit_records")
op.drop_index("ix_credit_records_pricing_reference_at", table_name="credit_records")
op.drop_index("ix_credit_records_pricing_usage_source", table_name="credit_records")
op.drop_index("ix_credit_records_pricing_version_code", table_name="credit_records")
op.drop_index("ix_credit_records_pricing_rule_id", table_name="credit_records")
op.drop_column("credit_records", "generation_snapshot_json")
op.drop_column("credit_records", "attachment_snapshot_json")
op.drop_column("credit_records", "usage_snapshot_json")
op.drop_column("credit_records", "pricing_snapshot_json")
op.drop_column("credit_records", "generated_total_count")
op.drop_column("credit_records", "generated_video_count")
op.drop_column("credit_records", "generated_image_count")
op.drop_column("credit_records", "requested_output_count")
op.drop_column("credit_records", "attachment_audio_duration_seconds")
op.drop_column("credit_records", "attachment_video_duration_seconds")
op.drop_column("credit_records", "attachment_total_count")
op.drop_column("credit_records", "attachment_audio_count")
op.drop_column("credit_records", "attachment_video_count")
op.drop_column("credit_records", "attachment_image_count")
op.drop_column("credit_records", "provider_cost_is_estimated")
op.drop_column("credit_records", "provider_usage_primary")
op.drop_column("credit_records", "provider_cost_finalized_at")
op.drop_column("credit_records", "provider_cost_calculated_at")
op.drop_column("credit_records", "provider_cost_status")
op.drop_column("credit_records", "provider_cost_amount")
op.drop_column("credit_records", "provider_cost_currency")
op.drop_column("credit_records", "pricing_snapshot_hash")
op.drop_column("credit_records", "pricing_snapshot_schema_version")
op.drop_column("credit_records", "pricing_effective_to")
op.drop_column("credit_records", "pricing_effective_from")
op.drop_column("credit_records", "pricing_reference_at")
op.drop_column("credit_records", "pricing_usage_source")
op.drop_column("credit_records", "pricing_calculator_version")
op.drop_column("credit_records", "pricing_billing_mode")
op.drop_column("credit_records", "pricing_version_code")
op.drop_column("credit_records", "pricing_rule_id")
# Reverse exact billing-attempt binding.
op.drop_index(
"ix_chat_generation_tasks_current_billing_attempt_no",
table_name="chat_generation_tasks",
)
op.drop_column("chat_generation_tasks", "current_billing_attempt_no")
# Reverse pricing-rule storage.
op.drop_index(
"ix_model_pricing_rules_rule_content_hash",
table_name="model_pricing_rules",
)
op.drop_index(
"ix_model_pricing_rules_publish_status",
table_name="model_pricing_rules",
)
op.drop_index(
"ix_model_pricing_rules_effective_to",
table_name="model_pricing_rules",
)
op.drop_index(
"ix_model_pricing_rules_effective_from",
table_name="model_pricing_rules",
)
op.drop_index(
"ix_model_pricing_rules_calculator_version",
table_name="model_pricing_rules",
)
op.drop_index(
"ix_model_pricing_rules_billing_mode",
table_name="model_pricing_rules",
)
op.drop_index(
"ix_model_pricing_rules_model_category",
table_name="model_pricing_rules",
)
op.drop_index(
"ix_model_pricing_rules_model_name",
table_name="model_pricing_rules",
)
op.drop_index(
"ix_model_pricing_rules_provider",
table_name="model_pricing_rules",
)
op.drop_index(
"ix_model_pricing_rules_category_status",
table_name="model_pricing_rules",
)
op.drop_index(
"ix_model_pricing_rules_resolve",
table_name="model_pricing_rules",
)
op.drop_index(
"uq_model_pricing_rules_provider_model_version",
table_name="model_pricing_rules",
)
op.drop_table("model_pricing_rules")