Files
server-core/app/services/compute_catalog.py
T
Pine 23733230f9 fix(opc): compute_catalog 模型映射补 price_tiers/cache_price_is_range
web 算力中心模型列表缺 price_tiers 导致分段定价被降级为区间(min~max)展示;
补两字段(compute 侧已返回,映射时漏掉),缓存区间判断同步可用
2026-09-14 19:23:43 +08:00

148 lines
5.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""算力中心模型目录:把 compute 引擎的 admin 模型(models 表,status=1)映射为桌面端可读形状。
替代原静态 pineagents_catalogOPC 桌面端展示/使用的模型 = 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)),
"price_tiers": row.get("price_tiers", "") or "",
"cache_price_is_range": bool(row.get("cache_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"]]