mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 17:30:23 +08:00
feat: 缓存计费细分、能力匹配优化、用户模型调用计数
1. 缓存创建 tokens 区分 5min/1h TTL,支持按缓存时长差异化计费 - Usage 表新增 cache_creation_input_tokens_5m/1h 字段 - Claude handler 解析新格式 (ephemeral_5m/1h, claude_cache_creation_5/1h) - 计费规则支持 cache_ttl_pricing 覆盖 cache_creation 价格 2. 能力匹配机制优化 - COMPATIBLE 能力不再硬过滤,改为排序阶段通过 capability_miss_count 优先级处理 - cache_1h 改为 COMPATIBLE + REQUEST_PARAM(自动检测请求体中的 ttl=1h) - gemini_files 改为 EXCLUSIVE + REQUEST_PARAM(自动检测 fileData.fileUri) - 移除前端模型偏好/能力配置 UI(不再需要用户手动配置) 3. 新增用户-模型维度调用次数计数器 (UserModelUsageCount) - 原子递增,避免从 Usage 表聚合查询 - 前端模型目录和用户可用模型列表展示调用次数 4. 其他改进 - global_model_id 改为必填(NOT NULL),清理孤立模型 - 模型映射对话框支持从上游获取模型列表并分组折叠 - 端点测试不再依赖端点启用状态 - 异步任务页面对普通用户隐藏用户信息列 - Dashboard 响应式布局断点调整 (sm -> lg) - 号池管理仅展示已启用号池的提供商
This commit is contained in:
@@ -30,6 +30,7 @@ from src.services.model.fetch_scheduler import (
|
||||
from src.services.model.upstream_fetcher import (
|
||||
EndpointFetchConfig,
|
||||
UpstreamModelsFetchContext,
|
||||
UpstreamModelsFetcherRegistry,
|
||||
build_format_to_config,
|
||||
fetch_models_for_key,
|
||||
get_adapter_for_format,
|
||||
@@ -235,7 +236,15 @@ async def query_available_models(
|
||||
# 构建 api_format -> EndpointFetchConfig 映射(纯数据,不依赖 ORM session)
|
||||
format_to_endpoint = build_format_to_config(provider.endpoints)
|
||||
|
||||
if not format_to_endpoint:
|
||||
# 检查是否有注册自定义 fetcher(如预设模型),有则不依赖活跃 endpoint
|
||||
provider_type = str(getattr(provider, "provider_type", "") or "").lower()
|
||||
# 延迟导入避免循环依赖(与 upstream_fetcher.fetch_models_for_key 保持一致)
|
||||
from src.services.provider.envelope import ensure_providers_bootstrapped
|
||||
|
||||
ensure_providers_bootstrapped()
|
||||
has_custom_fetcher = UpstreamModelsFetcherRegistry.get(provider_type) is not None
|
||||
|
||||
if not format_to_endpoint and not has_custom_fetcher:
|
||||
raise HTTPException(status_code=400, detail="No active endpoints found for this provider")
|
||||
|
||||
# 如果指定了 api_key_id,只获取该 Key 的模型
|
||||
@@ -253,7 +262,6 @@ async def query_available_models(
|
||||
raise HTTPException(status_code=400, detail="No active API Key found for this provider")
|
||||
|
||||
# Antigravity: 按 tier/可用性排序后逐个尝试,成功即停止
|
||||
provider_type = str(getattr(provider, "provider_type", "") or "").lower()
|
||||
if provider_type == ProviderType.ANTIGRAVITY:
|
||||
return await _fetch_models_antigravity_ordered(
|
||||
provider=provider,
|
||||
@@ -610,12 +618,12 @@ async def test_model(
|
||||
raise HTTPException(status_code=404, detail="Provider not found")
|
||||
|
||||
# 构建 api_format -> endpoint 映射 和 id -> endpoint 映射
|
||||
# 测试不依赖端点启用状态,禁用的端点也可以用于测试连通性
|
||||
format_to_endpoint: dict[str, ProviderEndpoint] = {}
|
||||
id_to_endpoint: dict[str, ProviderEndpoint] = {}
|
||||
for ep in provider.endpoints:
|
||||
if ep.is_active:
|
||||
format_to_endpoint[ep.api_format] = ep
|
||||
id_to_endpoint[ep.id] = ep
|
||||
format_to_endpoint[ep.api_format] = ep
|
||||
id_to_endpoint[ep.id] = ep
|
||||
|
||||
# 找到合适的端点和 API Key
|
||||
endpoint = None
|
||||
@@ -628,7 +636,7 @@ async def test_model(
|
||||
if not endpoint:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"No active endpoint found for API format: {request.api_format}",
|
||||
detail=f"No endpoint found for API format: {request.api_format}",
|
||||
)
|
||||
|
||||
if request.api_key_id:
|
||||
@@ -657,7 +665,7 @@ async def test_model(
|
||||
# 使用指定的端点
|
||||
endpoint = id_to_endpoint.get(request.endpoint_id)
|
||||
if not endpoint:
|
||||
raise HTTPException(status_code=404, detail="Endpoint not found or not active")
|
||||
raise HTTPException(status_code=404, detail="Endpoint not found")
|
||||
|
||||
if request.api_key_id:
|
||||
# 同时指定了 Key,需要校验是否支持该端点格式
|
||||
|
||||
@@ -715,6 +715,7 @@ class AdminImportFromUpstreamAdapter(AdminApiAdapter):
|
||||
# 1. 检查是否已存在同名的 ProviderModel
|
||||
existing = (
|
||||
db.query(Model)
|
||||
.options(joinedload(Model.global_model))
|
||||
.filter(
|
||||
Model.provider_id == self.provider_id,
|
||||
Model.provider_model_name == model_id,
|
||||
@@ -727,10 +728,8 @@ class AdminImportFromUpstreamAdapter(AdminApiAdapter):
|
||||
success.append(
|
||||
ImportFromUpstreamSuccessItem(
|
||||
model_id=model_id,
|
||||
global_model_id=existing.global_model_id or "",
|
||||
global_model_name=(
|
||||
existing.global_model.name if existing.global_model else ""
|
||||
),
|
||||
global_model_id=existing.global_model_id,
|
||||
global_model_name=existing.global_model.name,
|
||||
provider_model_id=existing.id,
|
||||
created_global_model=False,
|
||||
)
|
||||
|
||||
@@ -319,7 +319,6 @@ def _build_provider_summary(db: Session, provider: Provider) -> ProviderWithEndp
|
||||
.filter(
|
||||
Model.provider_id == provider.id,
|
||||
Model.is_active == True,
|
||||
Model.global_model_id.isnot(None),
|
||||
)
|
||||
.distinct()
|
||||
.all()
|
||||
|
||||
@@ -2556,16 +2556,18 @@ def _purge_stats_and_reset_counters(db: Session) -> None:
|
||||
class AdminPurgeUsageAdapter(AdminApiAdapter):
|
||||
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
|
||||
"""清空全部使用记录及相关统计数据"""
|
||||
from src.models.database import RequestCandidate
|
||||
from src.models.database import RequestCandidate, UserModelUsageCount
|
||||
|
||||
db = context.db
|
||||
|
||||
usage_count = db.query(Usage).count()
|
||||
candidates_count = db.query(RequestCandidate).count()
|
||||
usage_counts_count = db.query(UserModelUsageCount).count()
|
||||
|
||||
# 清空使用记录
|
||||
db.query(RequestCandidate).delete()
|
||||
db.query(Usage).delete()
|
||||
db.query(UserModelUsageCount).delete()
|
||||
|
||||
_purge_stats_and_reset_counters(db)
|
||||
db.commit()
|
||||
@@ -2575,6 +2577,7 @@ class AdminPurgeUsageAdapter(AdminApiAdapter):
|
||||
"deleted": {
|
||||
"usage_records": usage_count,
|
||||
"request_candidates": candidates_count,
|
||||
"user_model_usage_counts": usage_counts_count,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -1259,6 +1259,8 @@ class AdminUsageDetailAdapter(AdminApiAdapter):
|
||||
},
|
||||
"cache_creation_input_tokens": usage_record.cache_creation_input_tokens,
|
||||
"cache_read_input_tokens": usage_record.cache_read_input_tokens,
|
||||
"cache_creation_input_tokens_5m": usage_record.cache_creation_input_tokens_5m or 0,
|
||||
"cache_creation_input_tokens_1h": usage_record.cache_creation_input_tokens_1h or 0,
|
||||
"cache_creation_cost": getattr(usage_record, "cache_creation_cost_usd", 0.0),
|
||||
"cache_read_cost": getattr(usage_record, "cache_read_cost_usd", 0.0),
|
||||
"request_cost": getattr(usage_record, "request_cost_usd", 0.0),
|
||||
|
||||
@@ -184,6 +184,8 @@ class ChatSyncExecutor:
|
||||
output_tokens = usage_info.get("output_tokens", 0)
|
||||
cache_creation_tokens = usage_info.get("cache_creation_input_tokens", 0)
|
||||
cached_tokens = usage_info.get("cache_read_input_tokens", 0)
|
||||
cache_creation_tokens_5m = usage_info.get("cache_creation_input_tokens_5m", 0)
|
||||
cache_creation_tokens_1h = usage_info.get("cache_creation_input_tokens_1h", 0)
|
||||
|
||||
# 非流式成功时,返回给客户端的是提供商响应头(透传)
|
||||
# JSONResponse 会自动设置 content-type,但我们记录实际返回的完整头
|
||||
@@ -211,6 +213,8 @@ class ChatSyncExecutor:
|
||||
provider_request_body=ctx.provider_request_body,
|
||||
cache_creation_tokens=cache_creation_tokens,
|
||||
cache_read_tokens=cached_tokens,
|
||||
cache_creation_tokens_5m=cache_creation_tokens_5m,
|
||||
cache_creation_tokens_1h=cache_creation_tokens_1h,
|
||||
is_stream=False,
|
||||
provider_request_headers=ctx.provider_request_headers,
|
||||
api_format=api_format,
|
||||
|
||||
@@ -171,7 +171,7 @@ async def _calculate_and_record_usage(
|
||||
provider = db.query(Provider).filter(Provider.id == provider_api_key.provider_id).first()
|
||||
if provider:
|
||||
for ep in provider.endpoints:
|
||||
if ep.api_format == api_format and ep.is_active:
|
||||
if ep.api_format == api_format:
|
||||
provider_endpoint = ep
|
||||
break
|
||||
|
||||
|
||||
@@ -86,6 +86,8 @@ class StreamContext:
|
||||
output_tokens: int = 0
|
||||
cached_tokens: int = 0
|
||||
cache_creation_tokens: int = 0
|
||||
cache_creation_tokens_5m: int = 0 # 5min TTL 缓存创建
|
||||
cache_creation_tokens_1h: int = 0 # 1h TTL 缓存创建
|
||||
|
||||
# 响应内容
|
||||
_collected_text_parts: list[str] = field(default_factory=list, repr=False)
|
||||
@@ -159,6 +161,8 @@ class StreamContext:
|
||||
self.output_tokens = 0
|
||||
self.cached_tokens = 0
|
||||
self.cache_creation_tokens = 0
|
||||
self.cache_creation_tokens_5m = 0
|
||||
self.cache_creation_tokens_1h = 0
|
||||
self.error_message = None
|
||||
self.upstream_response = None
|
||||
self.status_code = 200
|
||||
|
||||
@@ -228,6 +228,8 @@ class StreamTelemetryRecorder:
|
||||
provider_request_body=ctx.provider_request_body,
|
||||
cache_creation_tokens=ctx.cache_creation_tokens,
|
||||
cache_read_tokens=ctx.cached_tokens,
|
||||
cache_creation_tokens_5m=ctx.cache_creation_tokens_5m,
|
||||
cache_creation_tokens_1h=ctx.cache_creation_tokens_1h,
|
||||
is_stream=True,
|
||||
provider_request_headers=ctx.provider_request_headers,
|
||||
api_format=ctx.api_format,
|
||||
@@ -285,6 +287,8 @@ class StreamTelemetryRecorder:
|
||||
output_tokens=ctx.output_tokens,
|
||||
cache_creation_tokens=ctx.cache_creation_tokens,
|
||||
cache_read_tokens=ctx.cached_tokens,
|
||||
cache_creation_tokens_5m=ctx.cache_creation_tokens_5m,
|
||||
cache_creation_tokens_1h=ctx.cache_creation_tokens_1h,
|
||||
response_body=response_body,
|
||||
client_response_body=client_response_body,
|
||||
response_headers=ctx.response_headers,
|
||||
@@ -342,6 +346,8 @@ class StreamTelemetryRecorder:
|
||||
output_tokens=ctx.output_tokens,
|
||||
cache_creation_tokens=ctx.cache_creation_tokens,
|
||||
cache_read_tokens=ctx.cached_tokens,
|
||||
cache_creation_tokens_5m=ctx.cache_creation_tokens_5m,
|
||||
cache_creation_tokens_1h=ctx.cache_creation_tokens_1h,
|
||||
response_body=response_body,
|
||||
client_response_body=client_response_body,
|
||||
response_headers=ctx.response_headers,
|
||||
|
||||
@@ -94,6 +94,34 @@ def extract_cache_creation_tokens(usage: dict[str, Any]) -> int:
|
||||
return old_format
|
||||
|
||||
|
||||
def extract_cache_creation_tokens_detail(usage: dict[str, Any]) -> tuple[int, int, int]:
|
||||
"""
|
||||
提取缓存创建 tokens 细分(区分 5m 和 1h)
|
||||
|
||||
返回 (total, tokens_5m, tokens_1h) 三元组。
|
||||
当无法区分时,tokens_5m 和 tokens_1h 均为 0,total 为合计值。
|
||||
"""
|
||||
# 1. 嵌套格式
|
||||
cache_creation = usage.get("cache_creation")
|
||||
if isinstance(cache_creation, dict) and (
|
||||
"ephemeral_5m_input_tokens" in cache_creation
|
||||
or "ephemeral_1h_input_tokens" in cache_creation
|
||||
):
|
||||
t5m = int(cache_creation.get("ephemeral_5m_input_tokens", 0))
|
||||
t1h = int(cache_creation.get("ephemeral_1h_input_tokens", 0))
|
||||
return t5m + t1h, t5m, t1h
|
||||
|
||||
# 2. 扁平新格式
|
||||
if "claude_cache_creation_5_m_tokens" in usage or "claude_cache_creation_1_h_tokens" in usage:
|
||||
t5m = int(usage.get("claude_cache_creation_5_m_tokens", 0))
|
||||
t1h = int(usage.get("claude_cache_creation_1_h_tokens", 0))
|
||||
return t5m + t1h, t5m, t1h
|
||||
|
||||
# 3. 旧格式:无法区分
|
||||
old = int(usage.get("cache_creation_input_tokens", 0))
|
||||
return old, 0, 0
|
||||
|
||||
|
||||
def build_sse_headers(extra_headers: dict[str, str] | None = None) -> dict[str, str]:
|
||||
"""
|
||||
构建 SSE(text/event-stream)推荐响应头,用于减少代理缓冲带来的卡顿/成段输出。
|
||||
|
||||
@@ -31,14 +31,15 @@ class ClaudeCapabilityDetector:
|
||||
request_body: dict[str, Any] | None = None,
|
||||
) -> dict[str, bool]:
|
||||
"""
|
||||
从 Claude 请求头检测能力需求
|
||||
从 Claude 请求头和请求体检测能力需求
|
||||
|
||||
检测规则:
|
||||
- anthropic-beta: context-1m-xxx -> context_1m: True
|
||||
- 请求体中 cache_control.ttl = "1h" -> cache_1h: True
|
||||
|
||||
Args:
|
||||
headers: 请求头字典
|
||||
request_body: 请求体(Claude 不使用,保留用于接口统一)
|
||||
request_body: 请求体(用于检测 cache_control.ttl)
|
||||
"""
|
||||
requirements: dict[str, bool] = {}
|
||||
|
||||
@@ -47,9 +48,59 @@ class ClaudeCapabilityDetector:
|
||||
if beta_header and "context-1m" in beta_header.lower():
|
||||
requirements["context_1m"] = True
|
||||
|
||||
# 从请求体检测 cache_1h
|
||||
if request_body and _detect_cache_1h_in_body(request_body):
|
||||
requirements["cache_1h"] = True
|
||||
|
||||
return requirements
|
||||
|
||||
|
||||
def _has_cache_1h_ttl(block: dict[str, Any]) -> bool:
|
||||
"""检查单个内容块是否包含 cache_control.ttl = '1h'"""
|
||||
cache_control = block.get("cache_control")
|
||||
if isinstance(cache_control, dict):
|
||||
return cache_control.get("ttl") == "1h"
|
||||
return False
|
||||
|
||||
|
||||
def _detect_cache_1h_in_body(body: dict[str, Any]) -> bool:
|
||||
"""
|
||||
扫描 Claude 请求体,检测是否包含 cache_control.ttl = "1h"
|
||||
|
||||
检查位置:
|
||||
- system[].cache_control.ttl
|
||||
- messages[].content[].cache_control.ttl
|
||||
- tools[].cache_control.ttl
|
||||
"""
|
||||
# 检查 system(数组格式)
|
||||
system = body.get("system")
|
||||
if isinstance(system, list):
|
||||
for block in system:
|
||||
if isinstance(block, dict) and _has_cache_1h_ttl(block):
|
||||
return True
|
||||
|
||||
# 检查 messages
|
||||
messages = body.get("messages")
|
||||
if isinstance(messages, list):
|
||||
for msg in messages:
|
||||
if not isinstance(msg, dict):
|
||||
continue
|
||||
content = msg.get("content")
|
||||
if isinstance(content, list):
|
||||
for block in content:
|
||||
if isinstance(block, dict) and _has_cache_1h_ttl(block):
|
||||
return True
|
||||
|
||||
# 检查 tools
|
||||
tools = body.get("tools")
|
||||
if isinstance(tools, list):
|
||||
for tool in tools:
|
||||
if isinstance(tool, dict) and _has_cache_1h_ttl(tool):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
@register_adapter
|
||||
class ClaudeChatAdapter(ChatAdapterBase):
|
||||
"""
|
||||
|
||||
@@ -8,7 +8,7 @@ Claude Chat Handler - 基于通用 Chat Handler 基类的简化实现
|
||||
from typing import Any
|
||||
|
||||
from src.api.handlers.base.chat_handler_base import ChatHandlerBase
|
||||
from src.api.handlers.base.utils import extract_cache_creation_tokens
|
||||
from src.api.handlers.base.utils import extract_cache_creation_tokens_detail
|
||||
from src.core.api_format import ApiFamily, EndpointKind
|
||||
|
||||
|
||||
@@ -103,12 +103,15 @@ class ClaudeChatHandler(ChatHandlerBase):
|
||||
- 新格式:claude_cache_creation_5_m_tokens / claude_cache_creation_1_h_tokens
|
||||
"""
|
||||
usage = response.get("usage", {})
|
||||
total, t5m, t1h = extract_cache_creation_tokens_detail(usage)
|
||||
|
||||
return {
|
||||
"input_tokens": usage.get("input_tokens", 0),
|
||||
"output_tokens": usage.get("output_tokens", 0),
|
||||
"cache_creation_input_tokens": extract_cache_creation_tokens(usage),
|
||||
"cache_creation_input_tokens": total,
|
||||
"cache_read_input_tokens": usage.get("cache_read_input_tokens", 0),
|
||||
"cache_creation_input_tokens_5m": t5m,
|
||||
"cache_creation_input_tokens_1h": t1h,
|
||||
}
|
||||
|
||||
def _normalize_response(self, response: dict[str, Any]) -> dict[str, Any]:
|
||||
|
||||
@@ -46,7 +46,7 @@ class ClaudeCliAdapter(CliAdapterBase):
|
||||
request_body: dict[str, Any] | None = None,
|
||||
) -> dict[str, bool]:
|
||||
"""检测 Claude CLI 请求中隐含的能力需求"""
|
||||
return ClaudeCapabilityDetector.detect_from_headers(headers)
|
||||
return ClaudeCapabilityDetector.detect_from_headers(headers, request_body)
|
||||
|
||||
# =========================================================================
|
||||
# Claude CLI 特定的计费逻辑
|
||||
|
||||
@@ -10,7 +10,7 @@ from src.api.handlers.base.cli_handler_base import (
|
||||
CliMessageHandlerBase,
|
||||
StreamContext,
|
||||
)
|
||||
from src.api.handlers.base.utils import extract_cache_creation_tokens
|
||||
from src.api.handlers.base.utils import extract_cache_creation_tokens_detail
|
||||
from src.core.api_format import ApiFamily, EndpointKind
|
||||
|
||||
|
||||
@@ -114,9 +114,11 @@ class ClaudeCliMessageHandler(CliMessageHandlerBase):
|
||||
if cache_read:
|
||||
ctx.cached_tokens = cache_read
|
||||
|
||||
cache_creation = extract_cache_creation_tokens(usage)
|
||||
if cache_creation:
|
||||
ctx.cache_creation_tokens = cache_creation
|
||||
total, t5m, t1h = extract_cache_creation_tokens_detail(usage)
|
||||
if total:
|
||||
ctx.cache_creation_tokens = total
|
||||
ctx.cache_creation_tokens_5m = t5m
|
||||
ctx.cache_creation_tokens_1h = t1h
|
||||
|
||||
# 处理文本增量
|
||||
elif event_type == "content_block_delta":
|
||||
@@ -140,9 +142,11 @@ class ClaudeCliMessageHandler(CliMessageHandlerBase):
|
||||
ctx.cached_tokens = usage["cache_read_input_tokens"]
|
||||
|
||||
# 更新缓存创建 tokens
|
||||
cache_creation = extract_cache_creation_tokens(usage)
|
||||
if cache_creation > 0:
|
||||
ctx.cache_creation_tokens = cache_creation
|
||||
total, t5m, t1h = extract_cache_creation_tokens_detail(usage)
|
||||
if total > 0:
|
||||
ctx.cache_creation_tokens = total
|
||||
ctx.cache_creation_tokens_5m = t5m
|
||||
ctx.cache_creation_tokens_1h = t1h
|
||||
|
||||
# 检查是否结束
|
||||
delta = data.get("delta", {})
|
||||
|
||||
@@ -19,9 +19,30 @@ from src.core.api_format.enums import AuthMethod
|
||||
from src.core.api_format.headers import BROWSER_FINGERPRINT_HEADERS
|
||||
from src.core.logger import logger
|
||||
from src.models.gemini import GeminiRequest
|
||||
from src.services.gemini_files_mapping import extract_file_names_from_request
|
||||
from src.services.provider.transport import redact_url_for_log
|
||||
|
||||
|
||||
class GeminiCapabilityDetector:
|
||||
"""Gemini API 能力检测器"""
|
||||
|
||||
@staticmethod
|
||||
def detect_from_request(
|
||||
headers: dict[str, str], # noqa: ARG004 - 预留
|
||||
request_body: dict[str, Any] | None = None,
|
||||
) -> dict[str, bool]:
|
||||
"""
|
||||
从请求体检测 Gemini 能力需求
|
||||
|
||||
检测规则:
|
||||
- fileData.fileUri -> gemini_files: True
|
||||
"""
|
||||
requirements: dict[str, bool] = {}
|
||||
if request_body and extract_file_names_from_request(request_body):
|
||||
requirements["gemini_files"] = True
|
||||
return requirements
|
||||
|
||||
|
||||
@register_adapter
|
||||
class GeminiChatAdapter(ChatAdapterBase):
|
||||
"""
|
||||
@@ -62,11 +83,11 @@ class GeminiChatAdapter(ChatAdapterBase):
|
||||
|
||||
def detect_capability_requirements(
|
||||
self,
|
||||
headers: dict[str, str], # noqa: ARG002 - 预留
|
||||
request_body: dict[str, Any] | None = None, # noqa: ARG002 - 预留
|
||||
headers: dict[str, str],
|
||||
request_body: dict[str, Any] | None = None,
|
||||
) -> dict[str, bool]:
|
||||
"""Gemini API 无特殊能力要求"""
|
||||
return {}
|
||||
"""从请求体检测 Gemini 能力需求(fileData.fileUri -> gemini_files)"""
|
||||
return GeminiCapabilityDetector.detect_from_request(headers, request_body)
|
||||
|
||||
def _merge_path_params(
|
||||
self, original_request_body: dict[str, Any], path_params: dict[str, Any] # noqa: ARG002
|
||||
|
||||
@@ -13,7 +13,7 @@ from fastapi import Request
|
||||
|
||||
from src.api.handlers.base.cli_adapter_base import CliAdapterBase, register_cli_adapter
|
||||
from src.api.handlers.base.cli_handler_base import CliMessageHandlerBase
|
||||
from src.api.handlers.gemini.adapter import GeminiChatAdapter
|
||||
from src.api.handlers.gemini.adapter import GeminiCapabilityDetector, GeminiChatAdapter
|
||||
from src.config.settings import config
|
||||
from src.core.api_format import ApiFamily, get_auth_handler
|
||||
from src.core.api_format.enums import AuthMethod
|
||||
@@ -53,6 +53,14 @@ class GeminiCliAdapter(CliAdapterBase):
|
||||
handler = get_auth_handler(AuthMethod.GOOG_API_KEY)
|
||||
return handler.extract_credentials(request)
|
||||
|
||||
def detect_capability_requirements(
|
||||
self,
|
||||
headers: dict[str, str],
|
||||
request_body: dict[str, Any] | None = None,
|
||||
) -> dict[str, bool]:
|
||||
"""从请求体检测 Gemini 能力需求(fileData.fileUri -> gemini_files)"""
|
||||
return GeminiCapabilityDetector.detect_from_request(headers, request_body)
|
||||
|
||||
def _merge_path_params(
|
||||
self, original_request_body: dict[str, Any], path_params: dict[str, Any] # noqa: ARG002
|
||||
) -> dict[str, Any]:
|
||||
|
||||
@@ -312,18 +312,13 @@ class PublicProvidersAdapter(PublicApiAdapter):
|
||||
providers = query.offset(self.skip).limit(self.limit).all()
|
||||
result = []
|
||||
for provider in providers:
|
||||
models_count = (
|
||||
db.query(Model)
|
||||
.filter(Model.provider_id == provider.id, Model.global_model_id.isnot(None))
|
||||
.count()
|
||||
)
|
||||
models_count = db.query(Model).filter(Model.provider_id == provider.id).count()
|
||||
active_models_count = (
|
||||
db.query(Model)
|
||||
.filter(
|
||||
and_(
|
||||
Model.provider_id == provider.id,
|
||||
Model.is_active.is_(True),
|
||||
Model.global_model_id.isnot(None),
|
||||
)
|
||||
)
|
||||
.count()
|
||||
@@ -367,7 +362,6 @@ class PublicModelsAdapter(PublicApiAdapter):
|
||||
and_(
|
||||
Model.is_active.is_(True),
|
||||
Provider.is_active.is_(True),
|
||||
Model.global_model_id.isnot(None),
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -424,7 +418,6 @@ class PublicStatsAdapter(PublicApiAdapter):
|
||||
and_(
|
||||
Model.is_active.is_(True),
|
||||
Provider.is_active.is_(True),
|
||||
Model.global_model_id.isnot(None),
|
||||
)
|
||||
)
|
||||
.count()
|
||||
@@ -462,7 +455,6 @@ class PublicSearchModelsAdapter(PublicApiAdapter):
|
||||
and_(
|
||||
Model.is_active.is_(True),
|
||||
Provider.is_active.is_(True),
|
||||
Model.global_model_id.isnot(None),
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -32,7 +32,15 @@ from src.models.api import (
|
||||
UpdatePreferencesRequest,
|
||||
UpdateProfileRequest,
|
||||
)
|
||||
from src.models.database import ApiKey, GlobalModel, Model, Provider, Usage, User
|
||||
from src.models.database import (
|
||||
ApiKey,
|
||||
GlobalModel,
|
||||
Model,
|
||||
Provider,
|
||||
Usage,
|
||||
User,
|
||||
UserModelUsageCount,
|
||||
)
|
||||
from src.services.system.time_range import TimeRangeParams
|
||||
from src.services.usage.service import UsageService
|
||||
from src.services.user.apikey import ApiKeyService
|
||||
@@ -1203,6 +1211,14 @@ class ListAvailableModelsAdapter(AuthenticatedApiAdapter):
|
||||
.all()
|
||||
)
|
||||
|
||||
# 查询当前用户的每模型调用次数
|
||||
user_usage_rows = (
|
||||
db.query(UserModelUsageCount.model, UserModelUsageCount.usage_count)
|
||||
.filter(UserModelUsageCount.user_id == user.id)
|
||||
.all()
|
||||
)
|
||||
user_usage_map: dict[str, int] = {row.model: row.usage_count for row in user_usage_rows}
|
||||
|
||||
# 转换为响应格式(复用 PublicGlobalModelResponse schema)
|
||||
model_responses = [
|
||||
PublicGlobalModelResponse(
|
||||
@@ -1214,6 +1230,7 @@ class ListAvailableModelsAdapter(AuthenticatedApiAdapter):
|
||||
default_tiered_pricing=gm.default_tiered_pricing,
|
||||
supported_capabilities=gm.supported_capabilities,
|
||||
config=gm.config,
|
||||
usage_count=user_usage_map.get(gm.name, 0),
|
||||
)
|
||||
for gm in models
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user