0ac392e64a
- P0-4: 用户注册/登录时自动同步算力引擎账号(ensure_user+PAT+回写provisioned) - P0-4: 服务启动时异步全量对账补建缺失用户 - P0-2: calculate_compute_price从compute引擎取真实单价(30s缓存),硬编码仅兜底 - compute_catalog新增get_model_price和30s内存缓存
146 lines
5.7 KiB
Python
146 lines
5.7 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""算力中心模型目录:把 compute 引擎的 admin 模型(models 表,status=1)映射为桌面端可读形状。
|
||
|
||
替代原静态 pineagents_catalog:OPC 桌面端展示/使用的模型 = admin 端新增的模型(算力中心)。
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
|
||
from . import compute_client
|
||
|
||
|
||
def _parse_list(v):
|
||
"""JSON 数组字符串 → list;已 list 原样。"""
|
||
if isinstance(v, list):
|
||
return v
|
||
if isinstance(v, str):
|
||
try:
|
||
a = json.loads(v)
|
||
return a if isinstance(a, list) else []
|
||
except Exception: # noqa: BLE001
|
||
return []
|
||
return []
|
||
|
||
|
||
def _map(row: dict) -> dict:
|
||
"""Model 行 → 桌面端模型形状。"""
|
||
name = row.get("name") or row.get("model_name") or ""
|
||
return {
|
||
"id": row.get("model_name") or row.get("name") or "",
|
||
"model_name": row.get("model_name", ""),
|
||
"name": name,
|
||
"actual_model": row.get("actual_model") or row.get("model_name", ""),
|
||
"company": "",
|
||
"group": row.get("group", "default"),
|
||
"billing_unit": row.get("billing_unit", "/百万tokens"),
|
||
"input_price": row.get("input_price", 0) or 0,
|
||
"output_price": row.get("output_price", 0) or 0,
|
||
"cache_hit_price": row.get("cache_hit_price", 0) or 0,
|
||
"vision_support": bool(row.get("vision_support", False)),
|
||
"image_support": bool(row.get("image_support", False)),
|
||
"audio_support": bool(row.get("audio_support", False)),
|
||
"video_support": bool(row.get("video_support", False)),
|
||
"tags": (row.get("tags") or "").split(",") if row.get("tags") else [],
|
||
"vendor_id": row.get("vendor_id", 0),
|
||
# 富字段(models.json 语义)
|
||
"icon": row.get("icon", ""),
|
||
"icon_file": row.get("icon_file", ""),
|
||
"modality": row.get("modality", ""),
|
||
"input_forms": _parse_list(row.get("input_forms")),
|
||
"output_forms": _parse_list(row.get("output_forms")),
|
||
"function_icons": _parse_list(row.get("function_icons")),
|
||
"url": row.get("url", ""),
|
||
"author": row.get("author", ""),
|
||
"author_icon": row.get("author_icon", ""),
|
||
"supplier": row.get("supplier", ""),
|
||
"supplier_icon": row.get("supplier_icon", ""),
|
||
"supplier_icon_file": row.get("supplier_icon_file", ""),
|
||
# 封面与系列(模型展示)
|
||
"cover": row.get("cover", ""),
|
||
"cover_file": row.get("cover_file", ""),
|
||
"series": row.get("series", ""),
|
||
"series_icon": row.get("series_icon", ""),
|
||
"series_icon_file": row.get("series_icon_file", ""),
|
||
"notes": row.get("notes", ""),
|
||
# 缓存计费:缓存输入 / 缓存写入 + 区间
|
||
"cache_input_price": row.get("cache_input_price", 0) or 0,
|
||
"cache_input_price_min": row.get("cache_input_price_min", 0) or 0,
|
||
"cache_input_price_max": row.get("cache_input_price_max", 0) or 0,
|
||
"cache_write_price": row.get("cache_write_price", 0) or 0,
|
||
"cache_write_price_min": row.get("cache_write_price_min", 0) or 0,
|
||
"cache_write_price_max": row.get("cache_write_price_max", 0) or 0,
|
||
"price_is_range": bool(row.get("price_is_range", False)),
|
||
"input_price_min": row.get("input_price_min", 0) or 0,
|
||
"input_price_max": row.get("input_price_max", 0) or 0,
|
||
"output_price_min": row.get("output_price_min", 0) or 0,
|
||
"output_price_max": row.get("output_price_max", 0) or 0,
|
||
"context_window": row.get("context_window", 0) or 0,
|
||
"context_window_unit": row.get("context_window_unit", "M"),
|
||
"context_window_display": row.get("context_window_display", ""),
|
||
"max_output": row.get("max_output", 0) or 0,
|
||
"max_output_unit": row.get("max_output_unit", "K"),
|
||
"max_output_display": row.get("max_output_display", ""),
|
||
"free_trial": bool(row.get("free_trial", False)),
|
||
}
|
||
|
||
|
||
async def catalog() -> list[dict]:
|
||
"""从 compute 引擎拉取 admin 模型(status=1)并映射。空/不可达 → 返回空列表。
|
||
|
||
30s 内存缓存,避免每次计费请求都穿透到 compute。
|
||
"""
|
||
return await _catalog_cached()
|
||
|
||
|
||
_CACHE: dict[str, object] = {"ts": 0.0, "data": []}
|
||
_CACHE_TTL = 30.0 # 秒
|
||
|
||
|
||
async def _catalog_cached() -> list[dict]:
|
||
import time as _time
|
||
now = _time.time()
|
||
if now - float(_CACHE.get("ts", 0)) < _CACHE_TTL:
|
||
return list(_CACHE.get("data") or []) # type: ignore[arg-type]
|
||
try:
|
||
items = await compute_client.list_models(status=1)
|
||
except compute_client.ComputeError:
|
||
items = []
|
||
data = [_map(row) for row in items if (row.get("status", 1) == 1)]
|
||
_CACHE["ts"] = now
|
||
_CACHE["data"] = data
|
||
return list(data)
|
||
|
||
|
||
async def get_model_price(model_name: str) -> tuple[float, float, float]:
|
||
"""按模型名查真实单价(元/百万token):(input, output, cache_hit)。
|
||
|
||
匹配优先级:model.id == name → model.name == name → 未找到返回 (0,0,0)。
|
||
"""
|
||
for m in await _catalog_cached():
|
||
if m.get("id") == model_name or m.get("name") == model_name:
|
||
return (
|
||
float(m.get("input_price", 0) or 0),
|
||
float(m.get("output_price", 0) or 0),
|
||
float(m.get("cache_hit_price", 0) or 0),
|
||
)
|
||
return (0.0, 0.0, 0.0)
|
||
|
||
|
||
async def models() -> list[dict]:
|
||
return await catalog()
|
||
|
||
|
||
async def prices() -> list[dict]:
|
||
return [
|
||
{"id": m["id"], "name": m["name"], "input": m["input_price"],
|
||
"output": m["output_price"], "cache_hit": m["cache_hit_price"],
|
||
"unit": m["billing_unit"]}
|
||
for m in await catalog()
|
||
]
|
||
|
||
|
||
async def model_ids() -> list[str]:
|
||
"""供 /v1/models 使用:admin 模型的对外 id。"""
|
||
return [m["id"] for m in await catalog() if m["id"]]
|