Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 337caa4da8 | |||
| 921654cbf8 | |||
| bc91fa3f88 | |||
| 41e30cbe4b |
@@ -0,0 +1,211 @@
|
||||
"""correct DeepSeek V4 Flash token price and frozen historical costs
|
||||
|
||||
Revision ID: deepseek_v4_flash_price
|
||||
Revises: comparison_below_min_success
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from collections.abc import Sequence
|
||||
from typing import Any
|
||||
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
from alembic import op
|
||||
|
||||
revision: str = "deepseek_v4_flash_price"
|
||||
down_revision: str | Sequence[str] | None = "comparison_below_min_success"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
_JSON = sa.JSON().with_variant(postgresql.JSONB(), "postgresql")
|
||||
_CONFIG_KEY = "llm_token_price"
|
||||
_MODEL = "deepseek-v4-flash"
|
||||
_OLD_INPUT_PRICE = 3.0
|
||||
_OLD_OUTPUT_PRICE = 15.0
|
||||
_NEW_INPUT_PRICE = 1.0
|
||||
_NEW_OUTPUT_PRICE = 2.0
|
||||
_CORRECTION_MARKER = "deepseek_v4_flash_price"
|
||||
_CONFIG_MARKER_KEY = "migration_deepseek_v4_flash_price"
|
||||
|
||||
|
||||
def _decode_object(value: Any) -> dict[str, Any] | None:
|
||||
if isinstance(value, dict):
|
||||
return value
|
||||
if isinstance(value, str):
|
||||
try:
|
||||
parsed = json.loads(value)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
return parsed if isinstance(parsed, dict) else None
|
||||
return None
|
||||
|
||||
|
||||
def _model_tokens(calls: Any) -> tuple[int, int]:
|
||||
if not isinstance(calls, list):
|
||||
return 0, 0
|
||||
input_tokens = 0
|
||||
output_tokens = 0
|
||||
for call in calls:
|
||||
if not isinstance(call, dict) or call.get("error") or call.get("model") != _MODEL:
|
||||
continue
|
||||
usage = _decode_object(call.get("usage"))
|
||||
if usage is None:
|
||||
continue
|
||||
input_tokens += int(usage.get("prompt_tokens") or 0)
|
||||
output_tokens += int(usage.get("completion_tokens") or 0)
|
||||
return input_tokens, output_tokens
|
||||
|
||||
|
||||
def _app_config_table() -> sa.TableClause:
|
||||
return sa.table(
|
||||
"app_config",
|
||||
sa.column("key", sa.String(64)),
|
||||
sa.column("value", _JSON),
|
||||
sa.column("updated_at", sa.DateTime(timezone=True)),
|
||||
)
|
||||
|
||||
|
||||
def _comparison_table() -> sa.TableClause:
|
||||
return sa.table(
|
||||
"comparison_record",
|
||||
sa.column("id", sa.Integer),
|
||||
sa.column("llm_calls", _JSON),
|
||||
sa.column("llm_cost_yuan", sa.Float),
|
||||
sa.column("llm_price_snapshot", _JSON),
|
||||
)
|
||||
|
||||
|
||||
def _update_config(conn, *, upgrade: bool) -> None:
|
||||
table = _app_config_table()
|
||||
row = conn.execute(
|
||||
sa.select(table.c.value).where(table.c.key == _CONFIG_KEY)
|
||||
).mappings().first()
|
||||
if row is None or not isinstance(row["value"], dict):
|
||||
return
|
||||
|
||||
config = dict(row["value"])
|
||||
per_model = dict(config.get("per_model") or {})
|
||||
current = per_model.get(_MODEL)
|
||||
if upgrade:
|
||||
# Preserve an operator's explicit model price. The production defect is specifically
|
||||
# the missing key falling through to the generic 3/15 price.
|
||||
if current is not None:
|
||||
return
|
||||
per_model[_MODEL] = {
|
||||
"input_per_1m": _NEW_INPUT_PRICE,
|
||||
"output_per_1m": _NEW_OUTPUT_PRICE,
|
||||
}
|
||||
conn.execute(table.insert().values(key=_CONFIG_MARKER_KEY, value=True))
|
||||
else:
|
||||
marker_exists = conn.execute(
|
||||
sa.select(table.c.key).where(table.c.key == _CONFIG_MARKER_KEY)
|
||||
).scalar_one_or_none()
|
||||
if marker_exists is None:
|
||||
return
|
||||
if current != {
|
||||
"input_per_1m": _NEW_INPUT_PRICE,
|
||||
"output_per_1m": _NEW_OUTPUT_PRICE,
|
||||
}:
|
||||
conn.execute(table.delete().where(table.c.key == _CONFIG_MARKER_KEY))
|
||||
return
|
||||
per_model.pop(_MODEL, None)
|
||||
conn.execute(table.delete().where(table.c.key == _CONFIG_MARKER_KEY))
|
||||
config["per_model"] = per_model
|
||||
conn.execute(
|
||||
table.update()
|
||||
.where(table.c.key == _CONFIG_KEY)
|
||||
# 这是对历史误配置的追溯修正,不是从部署时刻开始的新价格。保留原 updated_at,
|
||||
# 否则缺失成本回填会把部署前的记录全部排除。
|
||||
.values(value=config)
|
||||
)
|
||||
|
||||
|
||||
def _correct_frozen_costs(conn, *, upgrade: bool) -> None:
|
||||
table = _comparison_table()
|
||||
rows = conn.execute(
|
||||
sa.select(
|
||||
table.c.id,
|
||||
table.c.llm_calls,
|
||||
table.c.llm_cost_yuan,
|
||||
table.c.llm_price_snapshot,
|
||||
).where(
|
||||
table.c.llm_cost_yuan.is_not(None),
|
||||
table.c.llm_price_snapshot.is_not(None),
|
||||
)
|
||||
).mappings()
|
||||
|
||||
for row in rows:
|
||||
snapshot = row["llm_price_snapshot"]
|
||||
if not isinstance(snapshot, dict):
|
||||
continue
|
||||
prices = snapshot.get("prices")
|
||||
if not isinstance(prices, dict):
|
||||
continue
|
||||
model_price = prices.get(_MODEL)
|
||||
if not isinstance(model_price, dict):
|
||||
continue
|
||||
|
||||
if upgrade:
|
||||
if not (
|
||||
model_price.get("_source") == "default"
|
||||
and model_price.get("input_per_1m") == _OLD_INPUT_PRICE
|
||||
and model_price.get("output_per_1m") == _OLD_OUTPUT_PRICE
|
||||
):
|
||||
continue
|
||||
elif snapshot.get("pricing_correction") != _CORRECTION_MARKER:
|
||||
continue
|
||||
|
||||
input_tokens, output_tokens = _model_tokens(row["llm_calls"])
|
||||
if input_tokens == 0 and output_tokens == 0:
|
||||
continue
|
||||
if upgrade:
|
||||
delta = (
|
||||
input_tokens / 1_000_000 * (_OLD_INPUT_PRICE - _NEW_INPUT_PRICE)
|
||||
+ output_tokens / 1_000_000 * (_OLD_OUTPUT_PRICE - _NEW_OUTPUT_PRICE)
|
||||
)
|
||||
corrected_price = {
|
||||
"input_per_1m": _NEW_INPUT_PRICE,
|
||||
"output_per_1m": _NEW_OUTPUT_PRICE,
|
||||
"_source": "per_model",
|
||||
}
|
||||
snapshot["pricing_correction"] = _CORRECTION_MARKER
|
||||
new_cost = max(0.0, float(row["llm_cost_yuan"]) - delta)
|
||||
else:
|
||||
delta = (
|
||||
input_tokens / 1_000_000 * (_OLD_INPUT_PRICE - _NEW_INPUT_PRICE)
|
||||
+ output_tokens / 1_000_000 * (_OLD_OUTPUT_PRICE - _NEW_OUTPUT_PRICE)
|
||||
)
|
||||
corrected_price = {
|
||||
"input_per_1m": _OLD_INPUT_PRICE,
|
||||
"output_per_1m": _OLD_OUTPUT_PRICE,
|
||||
"_source": "default",
|
||||
}
|
||||
snapshot.pop("pricing_correction", None)
|
||||
new_cost = float(row["llm_cost_yuan"]) + delta
|
||||
|
||||
updated_prices = dict(prices)
|
||||
updated_prices[_MODEL] = corrected_price
|
||||
updated_snapshot = dict(snapshot)
|
||||
updated_snapshot["prices"] = updated_prices
|
||||
conn.execute(
|
||||
table.update()
|
||||
.where(table.c.id == row["id"])
|
||||
.values(
|
||||
llm_cost_yuan=round(new_cost, 6),
|
||||
llm_price_snapshot=updated_snapshot,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
_update_config(conn, upgrade=True)
|
||||
_correct_frozen_costs(conn, upgrade=True)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
_correct_frozen_costs(conn, upgrade=False)
|
||||
_update_config(conn, upgrade=False)
|
||||
@@ -227,7 +227,11 @@ CONFIG_DEFS: dict[str, dict[str, Any]] = {
|
||||
# 编辑框;set_value 不校验类型,嵌套 JSON 照存。
|
||||
"llm_token_price": {
|
||||
"default": {
|
||||
"per_model": {"qwen3.5-flash": {"input_per_1m": 0.8, "output_per_1m": 2.0}},
|
||||
"per_model": {
|
||||
"qwen3.5-flash": {"input_per_1m": 0.8, "output_per_1m": 2.0},
|
||||
# DashScope 华北 2 公网调用原价;必须显式配置,不能落到 3/15 的未知模型兜底价。
|
||||
"deepseek-v4-flash": {"input_per_1m": 1.0, "output_per_1m": 2.0},
|
||||
},
|
||||
"default": {"input_per_1m": 3.0, "output_per_1m": 15.0},
|
||||
"currency": "CNY", "unit": "per_1m_tokens",
|
||||
},
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
def _load_migration():
|
||||
path = (
|
||||
Path(__file__).parents[1]
|
||||
/ "alembic"
|
||||
/ "versions"
|
||||
/ "deepseek_v4_flash_price.py"
|
||||
)
|
||||
spec = importlib.util.spec_from_file_location("deepseek_v4_flash_price", path)
|
||||
assert spec is not None and spec.loader is not None
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
def test_migration_follows_comparison_status_normalization():
|
||||
migration = _load_migration()
|
||||
|
||||
assert migration.down_revision == "comparison_below_min_success"
|
||||
|
||||
|
||||
def test_migration_adds_price_and_corrects_only_mispriced_snapshot(monkeypatch):
|
||||
migration = _load_migration()
|
||||
engine = sa.create_engine("sqlite://")
|
||||
metadata = sa.MetaData()
|
||||
app_config = sa.Table(
|
||||
"app_config",
|
||||
metadata,
|
||||
sa.Column("key", sa.String(64), primary_key=True),
|
||||
sa.Column("value", sa.JSON, nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime),
|
||||
)
|
||||
comparison = sa.Table(
|
||||
"comparison_record",
|
||||
metadata,
|
||||
sa.Column("id", sa.Integer, primary_key=True),
|
||||
sa.Column("llm_calls", sa.JSON),
|
||||
sa.Column("llm_cost_yuan", sa.Float),
|
||||
sa.Column("llm_price_snapshot", sa.JSON),
|
||||
)
|
||||
metadata.create_all(engine)
|
||||
|
||||
calls = [
|
||||
{
|
||||
"model": "deepseek-v4-flash",
|
||||
"error": None,
|
||||
"usage": {"prompt_tokens": 12031, "completion_tokens": 125},
|
||||
},
|
||||
{
|
||||
"model": "qwen3.5-flash",
|
||||
"error": None,
|
||||
"usage": {"prompt_tokens": 2416, "completion_tokens": 119},
|
||||
},
|
||||
]
|
||||
snapshot = {
|
||||
"mode": "per_model",
|
||||
"prices": {
|
||||
"deepseek-v4-flash": {
|
||||
"input_per_1m": 3.0,
|
||||
"output_per_1m": 15.0,
|
||||
"_source": "default",
|
||||
},
|
||||
"qwen3.5-flash": {
|
||||
"input_per_1m": 0.2,
|
||||
"output_per_1m": 2.0,
|
||||
"_source": "per_model",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
with engine.begin() as conn:
|
||||
original_updated_at = datetime(2026, 7, 13, 18, 20, 10)
|
||||
conn.execute(
|
||||
app_config.insert().values(
|
||||
key="llm_token_price",
|
||||
value={
|
||||
"per_model": {
|
||||
"qwen3.5-flash": {
|
||||
"input_per_1m": 0.2,
|
||||
"output_per_1m": 2.0,
|
||||
}
|
||||
},
|
||||
"default": {"input_per_1m": 3.0, "output_per_1m": 15.0},
|
||||
},
|
||||
updated_at=original_updated_at,
|
||||
)
|
||||
)
|
||||
conn.execute(
|
||||
comparison.insert().values(
|
||||
id=1,
|
||||
llm_calls=calls,
|
||||
llm_cost_yuan=0.038689,
|
||||
llm_price_snapshot=snapshot,
|
||||
)
|
||||
)
|
||||
monkeypatch.setattr(migration.op, "get_bind", lambda: conn)
|
||||
|
||||
migration.upgrade()
|
||||
|
||||
config = conn.execute(
|
||||
sa.select(app_config.c.value).where(
|
||||
app_config.c.key == "llm_token_price"
|
||||
)
|
||||
).scalar_one()
|
||||
assert config["per_model"]["deepseek-v4-flash"] == {
|
||||
"input_per_1m": 1.0,
|
||||
"output_per_1m": 2.0,
|
||||
}
|
||||
assert conn.execute(
|
||||
sa.select(app_config.c.updated_at).where(
|
||||
app_config.c.key == "llm_token_price"
|
||||
)
|
||||
).scalar_one() == original_updated_at
|
||||
corrected = conn.execute(sa.select(comparison)).mappings().one()
|
||||
assert corrected["llm_cost_yuan"] == 0.013002
|
||||
assert corrected["llm_price_snapshot"]["prices"]["deepseek-v4-flash"] == {
|
||||
"input_per_1m": 1.0,
|
||||
"output_per_1m": 2.0,
|
||||
"_source": "per_model",
|
||||
}
|
||||
assert corrected["llm_price_snapshot"]["pricing_correction"] == (
|
||||
"deepseek_v4_flash_price"
|
||||
)
|
||||
|
||||
migration.downgrade()
|
||||
|
||||
reverted_config = conn.execute(
|
||||
sa.select(app_config.c.value).where(
|
||||
app_config.c.key == "llm_token_price"
|
||||
)
|
||||
).scalar_one()
|
||||
assert "deepseek-v4-flash" not in reverted_config["per_model"]
|
||||
reverted = conn.execute(sa.select(comparison)).mappings().one()
|
||||
assert reverted["llm_cost_yuan"] == 0.038689
|
||||
assert "pricing_correction" not in reverted["llm_price_snapshot"]
|
||||
|
||||
|
||||
def test_downgrade_preserves_price_that_existed_before_upgrade(monkeypatch):
|
||||
migration = _load_migration()
|
||||
engine = sa.create_engine("sqlite://")
|
||||
metadata = sa.MetaData()
|
||||
app_config = sa.Table(
|
||||
"app_config",
|
||||
metadata,
|
||||
sa.Column("key", sa.String(64), primary_key=True),
|
||||
sa.Column("value", sa.JSON, nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime),
|
||||
)
|
||||
sa.Table(
|
||||
"comparison_record",
|
||||
metadata,
|
||||
sa.Column("id", sa.Integer, primary_key=True),
|
||||
sa.Column("llm_calls", sa.JSON),
|
||||
sa.Column("llm_cost_yuan", sa.Float),
|
||||
sa.Column("llm_price_snapshot", sa.JSON),
|
||||
)
|
||||
metadata.create_all(engine)
|
||||
|
||||
explicit_price = {"input_per_1m": 1.0, "output_per_1m": 2.0}
|
||||
with engine.begin() as conn:
|
||||
conn.execute(
|
||||
app_config.insert().values(
|
||||
key="llm_token_price",
|
||||
value={
|
||||
"per_model": {"deepseek-v4-flash": explicit_price},
|
||||
"default": {"input_per_1m": 3.0, "output_per_1m": 15.0},
|
||||
},
|
||||
)
|
||||
)
|
||||
monkeypatch.setattr(migration.op, "get_bind", lambda: conn)
|
||||
|
||||
migration.upgrade()
|
||||
migration.downgrade()
|
||||
|
||||
config = conn.execute(sa.select(app_config.c.value)).scalar_one()
|
||||
assert config["per_model"]["deepseek-v4-flash"] == explicit_price
|
||||
@@ -108,6 +108,10 @@ def test_get_llm_prices_falls_back_to_default_then_uses_override():
|
||||
# 无 override → CONFIG_DEFS 默认(含 per_model / default)
|
||||
prices = get_llm_prices(db)
|
||||
assert "per_model" in prices and "default" in prices
|
||||
assert prices["per_model"]["deepseek-v4-flash"] == {
|
||||
"input_per_1m": 1.0,
|
||||
"output_per_1m": 2.0,
|
||||
}
|
||||
# 有 override → 用 DB 值
|
||||
app_config.set_value(
|
||||
db, "llm_token_price",
|
||||
|
||||
Reference in New Issue
Block a user