mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-03 09:50:21 +08:00
feat: 添加格式转换追踪、模型过滤规则和 Provider 超时配置
1. Usage 格式转换追踪 - 新增 endpoint_api_format 和 has_format_conversion 字段 - 在用量记录中显示格式转换信息(请求格式 → 端点格式) - 兼容历史数据的回填逻辑 2. Provider API Key 模型过滤规则 - 新增 model_include_patterns 和 model_exclude_patterns 字段 - 支持 * 和 ? 通配符,不区分大小写 - 自动获取模型时应用过滤规则 3. Provider 超时配置 - 新增 stream_first_byte_timeout 和 request_timeout 字段 - 支持每个 Provider 单独配置超时时间 - 优先使用 Provider 配置,否则回退到全局配置 Close #122, Close #123 Co-Authored-By: AAEE86 <33052466+AAEE86@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,114 @@
|
|||||||
|
"""add_format_conversion_tracking_and_model_filter_patterns_and_provider_timeout
|
||||||
|
|
||||||
|
Revision ID: f7c8d9e0a1b2
|
||||||
|
Revises: 4b4c7b0df1a2
|
||||||
|
Create Date: 2026-01-27 10:00:00+00:00
|
||||||
|
|
||||||
|
Changes:
|
||||||
|
1. usage 表: 添加 endpoint_api_format 和 has_format_conversion 字段
|
||||||
|
2. provider_api_keys 表: 添加 model_include_patterns 和 model_exclude_patterns 字段
|
||||||
|
- 支持通配符规则自动过滤从上游获取的模型列表
|
||||||
|
- 包含规则和排除规则(支持 * 和 ? 通配符)
|
||||||
|
3. providers 表: 添加 stream_first_byte_timeout 和 request_timeout 字段
|
||||||
|
- 允许每个提供商单独配置超时时间
|
||||||
|
"""
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy import inspect
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = "f7c8d9e0a1b2"
|
||||||
|
down_revision = "4b4c7b0df1a2"
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def table_exists(table_name: str) -> bool:
|
||||||
|
bind = op.get_bind()
|
||||||
|
inspector = inspect(bind)
|
||||||
|
return table_name in inspector.get_table_names()
|
||||||
|
|
||||||
|
|
||||||
|
def column_exists(table_name: str, column_name: str) -> bool:
|
||||||
|
bind = op.get_bind()
|
||||||
|
inspector = inspect(bind)
|
||||||
|
columns = [col["name"] for col in inspector.get_columns(table_name)]
|
||||||
|
return column_name in columns
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# === usage 表: 格式转换追踪 ===
|
||||||
|
if table_exists("usage"):
|
||||||
|
# 添加 endpoint_api_format 字段(端点原生 API 格式)
|
||||||
|
if not column_exists("usage", "endpoint_api_format"):
|
||||||
|
op.add_column(
|
||||||
|
"usage",
|
||||||
|
sa.Column("endpoint_api_format", sa.String(50), nullable=True),
|
||||||
|
)
|
||||||
|
|
||||||
|
# 添加 has_format_conversion 字段(是否发生了格式转换)
|
||||||
|
if not column_exists("usage", "has_format_conversion"):
|
||||||
|
op.add_column(
|
||||||
|
"usage",
|
||||||
|
sa.Column("has_format_conversion", sa.Boolean(), nullable=True, server_default="false"),
|
||||||
|
)
|
||||||
|
|
||||||
|
# === provider_api_keys 表: 模型过滤规则 ===
|
||||||
|
if table_exists("provider_api_keys"):
|
||||||
|
# 添加 model_include_patterns 字段(包含规则,支持 * 和 ? 通配符)
|
||||||
|
if not column_exists("provider_api_keys", "model_include_patterns"):
|
||||||
|
op.add_column(
|
||||||
|
"provider_api_keys",
|
||||||
|
sa.Column("model_include_patterns", sa.JSON(), nullable=True),
|
||||||
|
)
|
||||||
|
|
||||||
|
# 添加 model_exclude_patterns 字段(排除规则,支持 * 和 ? 通配符)
|
||||||
|
if not column_exists("provider_api_keys", "model_exclude_patterns"):
|
||||||
|
op.add_column(
|
||||||
|
"provider_api_keys",
|
||||||
|
sa.Column("model_exclude_patterns", sa.JSON(), nullable=True),
|
||||||
|
)
|
||||||
|
|
||||||
|
# === providers 表: 超时配置 ===
|
||||||
|
if table_exists("providers"):
|
||||||
|
# 添加 stream_first_byte_timeout 字段(流式请求首字节超时)
|
||||||
|
if not column_exists("providers", "stream_first_byte_timeout"):
|
||||||
|
op.add_column(
|
||||||
|
"providers",
|
||||||
|
sa.Column("stream_first_byte_timeout", sa.Float(), nullable=True),
|
||||||
|
)
|
||||||
|
|
||||||
|
# 添加 request_timeout 字段(非流式请求整体超时)
|
||||||
|
if not column_exists("providers", "request_timeout"):
|
||||||
|
op.add_column(
|
||||||
|
"providers",
|
||||||
|
sa.Column("request_timeout", sa.Float(), nullable=True),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# === providers 表: 移除超时配置 ===
|
||||||
|
if table_exists("providers"):
|
||||||
|
if column_exists("providers", "request_timeout"):
|
||||||
|
op.drop_column("providers", "request_timeout")
|
||||||
|
|
||||||
|
if column_exists("providers", "stream_first_byte_timeout"):
|
||||||
|
op.drop_column("providers", "stream_first_byte_timeout")
|
||||||
|
|
||||||
|
# === provider_api_keys 表: 移除模型过滤规则 ===
|
||||||
|
if table_exists("provider_api_keys"):
|
||||||
|
if column_exists("provider_api_keys", "model_exclude_patterns"):
|
||||||
|
op.drop_column("provider_api_keys", "model_exclude_patterns")
|
||||||
|
|
||||||
|
if column_exists("provider_api_keys", "model_include_patterns"):
|
||||||
|
op.drop_column("provider_api_keys", "model_include_patterns")
|
||||||
|
|
||||||
|
# === usage 表: 移除格式转换追踪 ===
|
||||||
|
if table_exists("usage"):
|
||||||
|
if column_exists("usage", "has_format_conversion"):
|
||||||
|
op.drop_column("usage", "has_format_conversion")
|
||||||
|
|
||||||
|
if column_exists("usage", "endpoint_api_format"):
|
||||||
|
op.drop_column("usage", "endpoint_api_format")
|
||||||
@@ -98,6 +98,8 @@ export async function addProviderKey(
|
|||||||
capabilities?: Record<string, boolean>
|
capabilities?: Record<string, boolean>
|
||||||
note?: string
|
note?: string
|
||||||
auto_fetch_models?: boolean // 是否启用自动获取模型
|
auto_fetch_models?: boolean // 是否启用自动获取模型
|
||||||
|
model_include_patterns?: string[] // 模型包含规则
|
||||||
|
model_exclude_patterns?: string[] // 模型排除规则
|
||||||
}
|
}
|
||||||
): Promise<EndpointAPIKey> {
|
): Promise<EndpointAPIKey> {
|
||||||
const response = await client.post(`/api/admin/endpoints/providers/${providerId}/keys`, data)
|
const response = await client.post(`/api/admin/endpoints/providers/${providerId}/keys`, data)
|
||||||
@@ -125,6 +127,8 @@ export async function updateProviderKey(
|
|||||||
is_active: boolean
|
is_active: boolean
|
||||||
note: string
|
note: string
|
||||||
auto_fetch_models: boolean // 是否启用自动获取模型
|
auto_fetch_models: boolean // 是否启用自动获取模型
|
||||||
|
model_include_patterns: string[] // 模型包含规则
|
||||||
|
model_exclude_patterns: string[] // 模型排除规则
|
||||||
}>
|
}>
|
||||||
): Promise<EndpointAPIKey> {
|
): Promise<EndpointAPIKey> {
|
||||||
const response = await client.put(`/api/admin/endpoints/keys/${keyId}`, data)
|
const response = await client.put(`/api/admin/endpoints/keys/${keyId}`, data)
|
||||||
|
|||||||
@@ -189,6 +189,9 @@ export interface EndpointAPIKey {
|
|||||||
last_models_fetch_at?: string // 最后获取模型时间
|
last_models_fetch_at?: string // 最后获取模型时间
|
||||||
last_models_fetch_error?: string // 最后获取模型错误信息
|
last_models_fetch_error?: string // 最后获取模型错误信息
|
||||||
locked_models?: string[] // 被锁定的模型列表
|
locked_models?: string[] // 被锁定的模型列表
|
||||||
|
// 模型过滤规则(仅当 auto_fetch_models=true 时生效)
|
||||||
|
model_include_patterns?: string[] // 模型包含规则(支持 * 和 ? 通配符)
|
||||||
|
model_exclude_patterns?: string[] // 模型排除规则(支持 * 和 ? 通配符)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 按格式的健康度数据
|
// 按格式的健康度数据
|
||||||
@@ -227,6 +230,9 @@ export interface EndpointAPIKeyUpdate {
|
|||||||
is_active?: boolean
|
is_active?: boolean
|
||||||
auto_fetch_models?: boolean // 是否启用自动获取模型
|
auto_fetch_models?: boolean // 是否启用自动获取模型
|
||||||
locked_models?: string[] // 被锁定的模型列表
|
locked_models?: string[] // 被锁定的模型列表
|
||||||
|
// 模型过滤规则(仅当 auto_fetch_models=true 时生效)
|
||||||
|
model_include_patterns?: string[] // 模型包含规则(支持 * 和 ? 通配符)
|
||||||
|
model_exclude_patterns?: string[] // 模型排除规则(支持 * 和 ? 通配符)
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EndpointHealthDetail {
|
export interface EndpointHealthDetail {
|
||||||
@@ -312,6 +318,9 @@ export interface ProviderWithEndpointsSummary {
|
|||||||
// 请求配置(从 Endpoint 迁移)
|
// 请求配置(从 Endpoint 迁移)
|
||||||
max_retries?: number // 最大重试次数
|
max_retries?: number // 最大重试次数
|
||||||
proxy?: ProxyConfig | null // 代理配置
|
proxy?: ProxyConfig | null // 代理配置
|
||||||
|
// 超时配置(秒),为空时使用全局配置
|
||||||
|
stream_first_byte_timeout?: number // 流式请求首字节超时
|
||||||
|
request_timeout?: number // 非流式请求整体超时
|
||||||
is_active: boolean
|
is_active: boolean
|
||||||
total_endpoints: number
|
total_endpoints: number
|
||||||
active_endpoints: number
|
active_endpoints: number
|
||||||
|
|||||||
@@ -754,14 +754,12 @@ watch(() => props.open, async (open) => {
|
|||||||
// 加载全局模型
|
// 加载全局模型
|
||||||
await loadGlobalModels()
|
await loadGlobalModels()
|
||||||
|
|
||||||
// 自动获取模式下,获取上游模型并刷新(保留锁定的模型)
|
// 自动获取模式下,获取上游模型用于显示(但选中状态使用已保存的 allowed_models)
|
||||||
if (props.apiKey.auto_fetch_models) {
|
if (props.apiKey.auto_fetch_models) {
|
||||||
await fetchUpstreamModels()
|
await fetchUpstreamModels()
|
||||||
// 锁定的模型 + 最新上游模型(去重)
|
// 注意:不再将所有上游模型自动标记为选中
|
||||||
const newSelected = new Set(lockedModels.value)
|
// 因为后端有过滤规则,实际保存的 allowed_models 是过滤后的结果
|
||||||
upstreamModelNames.value.forEach(m => newSelected.add(m))
|
// selectedModels 已在上面从 props.apiKey.allowed_models 初始化
|
||||||
selectedModels.value = Array.from(newSelected)
|
|
||||||
initialSelectedModels.value = [...selectedModels.value]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 提取自定义模型(不在全局模型和上游模型中的)
|
// 提取自定义模型(不在全局模型和上游模型中的)
|
||||||
|
|||||||
@@ -211,20 +211,48 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 自动获取模型 -->
|
<!-- 自动获取模型 -->
|
||||||
<div class="flex items-center justify-between py-2 px-3 rounded-md border border-border/60 bg-muted/30">
|
<div class="space-y-3 py-2 px-3 rounded-md border border-border/60 bg-muted/30">
|
||||||
<div class="space-y-0.5">
|
<div class="flex items-center justify-between">
|
||||||
<Label class="text-sm font-medium">自动获取上游可用模型</Label>
|
<div class="space-y-0.5">
|
||||||
|
<Label class="text-sm font-medium">自动获取上游可用模型</Label>
|
||||||
|
<p class="text-xs text-muted-foreground">
|
||||||
|
定时更新上游模型, 配合模型映射使用
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
v-if="showAutoFetchWarning"
|
||||||
|
class="text-xs text-amber-600 dark:text-amber-400"
|
||||||
|
>
|
||||||
|
已配置的模型权限将在下次获取时被覆盖
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Switch v-model="form.auto_fetch_models" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 模型过滤规则(仅当开启自动获取时显示) -->
|
||||||
|
<div
|
||||||
|
v-if="form.auto_fetch_models"
|
||||||
|
class="space-y-2 pt-2 border-t border-border/40"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<Label class="text-xs">包含规则</Label>
|
||||||
|
<Input
|
||||||
|
v-model="form.model_include_patterns_text"
|
||||||
|
placeholder="gpt-*, claude-*, 留空包含全部"
|
||||||
|
class="h-8 text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label class="text-xs">排除规则</Label>
|
||||||
|
<Input
|
||||||
|
v-model="form.model_exclude_patterns_text"
|
||||||
|
placeholder="*-preview, *-beta"
|
||||||
|
class="h-8 text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<p class="text-xs text-muted-foreground">
|
<p class="text-xs text-muted-foreground">
|
||||||
定时更新上游模型, 配合模型映射使用
|
逗号分隔,支持 * ? 通配符,不区分大小写
|
||||||
</p>
|
|
||||||
<p
|
|
||||||
v-if="showAutoFetchWarning"
|
|
||||||
class="text-xs text-amber-600 dark:text-amber-400"
|
|
||||||
>
|
|
||||||
已配置的模型权限将在下次获取时被覆盖
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Switch v-model="form.auto_fetch_models" />
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
@@ -335,7 +363,9 @@ const form = ref({
|
|||||||
note: '',
|
note: '',
|
||||||
is_active: true,
|
is_active: true,
|
||||||
capabilities: {} as Record<string, boolean>,
|
capabilities: {} as Record<string, boolean>,
|
||||||
auto_fetch_models: false
|
auto_fetch_models: false,
|
||||||
|
model_include_patterns_text: '', // 包含规则文本(逗号分隔)
|
||||||
|
model_exclude_patterns_text: '' // 排除规则文本(逗号分隔)
|
||||||
})
|
})
|
||||||
|
|
||||||
// 加载能力列表
|
// 加载能力列表
|
||||||
@@ -419,7 +449,9 @@ function resetForm() {
|
|||||||
note: '',
|
note: '',
|
||||||
is_active: true,
|
is_active: true,
|
||||||
capabilities: {},
|
capabilities: {},
|
||||||
auto_fetch_models: false
|
auto_fetch_models: false,
|
||||||
|
model_include_patterns_text: '',
|
||||||
|
model_exclude_patterns_text: ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -449,7 +481,9 @@ function loadKeyData() {
|
|||||||
note: props.editingKey.note || '',
|
note: props.editingKey.note || '',
|
||||||
is_active: props.editingKey.is_active,
|
is_active: props.editingKey.is_active,
|
||||||
capabilities: { ...(props.editingKey.capabilities || {}) },
|
capabilities: { ...(props.editingKey.capabilities || {}) },
|
||||||
auto_fetch_models: props.editingKey.auto_fetch_models ?? false
|
auto_fetch_models: props.editingKey.auto_fetch_models ?? false,
|
||||||
|
model_include_patterns_text: (props.editingKey.model_include_patterns || []).join(', '),
|
||||||
|
model_exclude_patterns_text: (props.editingKey.model_exclude_patterns || []).join(', ')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -467,6 +501,17 @@ function createFieldNonce(): string {
|
|||||||
return Math.random().toString(36).slice(2, 10)
|
return Math.random().toString(36).slice(2, 10)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 将逗号分隔的文本解析为数组(去空、去重)
|
||||||
|
// 返回空数组而非 undefined,以便后端能正确清除已有规则
|
||||||
|
function parsePatternText(text: string): string[] {
|
||||||
|
if (!text.trim()) return []
|
||||||
|
const patterns = text
|
||||||
|
.split(',')
|
||||||
|
.map(s => s.trim())
|
||||||
|
.filter(s => s.length > 0)
|
||||||
|
return [...new Set(patterns)]
|
||||||
|
}
|
||||||
|
|
||||||
async function handleSave() {
|
async function handleSave() {
|
||||||
// 必须有 providerId
|
// 必须有 providerId
|
||||||
if (!props.providerId) {
|
if (!props.providerId) {
|
||||||
@@ -529,7 +574,9 @@ async function handleSave() {
|
|||||||
note: form.value.note,
|
note: form.value.note,
|
||||||
is_active: form.value.is_active,
|
is_active: form.value.is_active,
|
||||||
capabilities: capabilitiesData,
|
capabilities: capabilitiesData,
|
||||||
auto_fetch_models: form.value.auto_fetch_models
|
auto_fetch_models: form.value.auto_fetch_models,
|
||||||
|
model_include_patterns: parsePatternText(form.value.model_include_patterns_text),
|
||||||
|
model_exclude_patterns: parsePatternText(form.value.model_exclude_patterns_text)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (form.value.api_key.trim()) {
|
if (form.value.api_key.trim()) {
|
||||||
@@ -551,7 +598,9 @@ async function handleSave() {
|
|||||||
max_probe_interval_minutes: form.value.max_probe_interval_minutes,
|
max_probe_interval_minutes: form.value.max_probe_interval_minutes,
|
||||||
note: form.value.note,
|
note: form.value.note,
|
||||||
capabilities: capabilitiesData || undefined,
|
capabilities: capabilitiesData || undefined,
|
||||||
auto_fetch_models: form.value.auto_fetch_models
|
auto_fetch_models: form.value.auto_fetch_models,
|
||||||
|
model_include_patterns: parsePatternText(form.value.model_include_patterns_text),
|
||||||
|
model_exclude_patterns: parsePatternText(form.value.model_exclude_patterns_text)
|
||||||
})
|
})
|
||||||
success('密钥已添加', '成功')
|
success('密钥已添加', '成功')
|
||||||
// 添加模式:不关闭对话框,只清除名称和密钥以便继续添加
|
// 添加模式:不关闭对话框,只清除名称和密钥以便继续添加
|
||||||
|
|||||||
@@ -93,6 +93,40 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 超时配置 -->
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div class="space-y-1.5">
|
||||||
|
<Label>
|
||||||
|
流式首字节超时
|
||||||
|
<span class="text-xs text-muted-foreground">(秒)</span>
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
:model-value="form.stream_first_byte_timeout ?? ''"
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
max="300"
|
||||||
|
step="1"
|
||||||
|
placeholder="30"
|
||||||
|
@update:model-value="(v) => form.stream_first_byte_timeout = parseNumberInput(v)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-1.5">
|
||||||
|
<Label>
|
||||||
|
非流式请求超时
|
||||||
|
<span class="text-xs text-muted-foreground">(秒)</span>
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
:model-value="form.request_timeout ?? ''"
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
max="600"
|
||||||
|
step="1"
|
||||||
|
placeholder="300"
|
||||||
|
@update:model-value="(v) => form.request_timeout = parseNumberInput(v)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 月卡配置 -->
|
<!-- 月卡配置 -->
|
||||||
<div
|
<div
|
||||||
v-if="form.billing_type === 'monthly_quota'"
|
v-if="form.billing_type === 'monthly_quota'"
|
||||||
@@ -267,6 +301,9 @@ const form = ref({
|
|||||||
concurrent_limit: undefined as number | undefined,
|
concurrent_limit: undefined as number | undefined,
|
||||||
// 请求配置
|
// 请求配置
|
||||||
max_retries: undefined as number | undefined,
|
max_retries: undefined as number | undefined,
|
||||||
|
// 超时配置(秒)
|
||||||
|
stream_first_byte_timeout: undefined as number | undefined,
|
||||||
|
request_timeout: undefined as number | undefined,
|
||||||
// 代理配置(扁平化便于表单绑定)
|
// 代理配置(扁平化便于表单绑定)
|
||||||
proxy_enabled: false,
|
proxy_enabled: false,
|
||||||
proxy_url: '',
|
proxy_url: '',
|
||||||
@@ -291,6 +328,9 @@ function resetForm() {
|
|||||||
concurrent_limit: undefined,
|
concurrent_limit: undefined,
|
||||||
// 请求配置
|
// 请求配置
|
||||||
max_retries: undefined,
|
max_retries: undefined,
|
||||||
|
// 超时配置
|
||||||
|
stream_first_byte_timeout: undefined,
|
||||||
|
request_timeout: undefined,
|
||||||
// 代理配置
|
// 代理配置
|
||||||
proxy_enabled: false,
|
proxy_enabled: false,
|
||||||
proxy_url: '',
|
proxy_url: '',
|
||||||
@@ -321,6 +361,9 @@ function loadProviderData() {
|
|||||||
concurrent_limit: undefined,
|
concurrent_limit: undefined,
|
||||||
// 请求配置
|
// 请求配置
|
||||||
max_retries: props.provider.max_retries ?? undefined,
|
max_retries: props.provider.max_retries ?? undefined,
|
||||||
|
// 超时配置
|
||||||
|
stream_first_byte_timeout: props.provider.stream_first_byte_timeout ?? undefined,
|
||||||
|
request_timeout: props.provider.request_timeout ?? undefined,
|
||||||
// 代理配置
|
// 代理配置
|
||||||
proxy_enabled: proxy?.enabled ?? false,
|
proxy_enabled: proxy?.enabled ?? false,
|
||||||
proxy_url: proxy?.url || '',
|
proxy_url: proxy?.url || '',
|
||||||
@@ -376,6 +419,9 @@ const handleSubmit = async () => {
|
|||||||
is_active: form.value.is_active,
|
is_active: form.value.is_active,
|
||||||
// 请求配置
|
// 请求配置
|
||||||
max_retries: form.value.max_retries ?? undefined,
|
max_retries: form.value.max_retries ?? undefined,
|
||||||
|
// 超时配置(null 表示清除,使用全局配置)
|
||||||
|
stream_first_byte_timeout: form.value.stream_first_byte_timeout ?? null,
|
||||||
|
request_timeout: form.value.request_timeout ?? null,
|
||||||
proxy,
|
proxy,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -299,75 +299,90 @@
|
|||||||
v-if="isAdmin"
|
v-if="isAdmin"
|
||||||
class="py-4 w-[60px]"
|
class="py-4 w-[60px]"
|
||||||
>
|
>
|
||||||
<div class="flex flex-col text-xs gap-0.5">
|
<div class="flex items-center gap-1">
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex flex-col text-xs gap-0.5">
|
||||||
<span>{{ record.provider }}</span>
|
<span>{{ record.provider }}</span>
|
||||||
<!-- 故障转移图标(优先显示) -->
|
|
||||||
<span
|
<span
|
||||||
v-if="record.has_fallback"
|
v-if="record.api_key_name"
|
||||||
class="inline-flex items-center justify-center w-4 h-4 text-xs text-amber-600 dark:text-amber-400"
|
class="text-muted-foreground truncate"
|
||||||
title="此请求发生了 Provider 故障转移"
|
:title="record.api_key_name"
|
||||||
>
|
>
|
||||||
<svg
|
{{ record.api_key_name }}
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
<span
|
||||||
viewBox="0 0 24 24"
|
v-if="record.rate_multiplier && record.rate_multiplier !== 1.0"
|
||||||
fill="none"
|
class="text-foreground/60"
|
||||||
stroke="currentColor"
|
>({{ record.rate_multiplier }}x)</span>
|
||||||
stroke-width="2"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
class="w-3.5 h-3.5"
|
|
||||||
>
|
|
||||||
<path d="m16 3 4 4-4 4" />
|
|
||||||
<path d="M20 7H4" />
|
|
||||||
<path d="m8 21-4-4 4-4" />
|
|
||||||
<path d="M4 17h16" />
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
<!-- 重试图标(仅在无故障转移时显示) -->
|
|
||||||
<span
|
|
||||||
v-else-if="record.has_retry"
|
|
||||||
class="inline-flex items-center justify-center w-4 h-4 text-xs text-blue-600 dark:text-blue-400"
|
|
||||||
title="此请求发生了亲和缓存重试"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="2"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
class="w-3.5 h-3.5"
|
|
||||||
>
|
|
||||||
<path d="M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16" />
|
|
||||||
<path d="M21 21v-5h-5" />
|
|
||||||
<path d="M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" />
|
|
||||||
<path d="M3 3v5h5" />
|
|
||||||
</svg>
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span
|
<!-- 故障转移图标(优先显示) -->
|
||||||
v-if="record.api_key_name"
|
<svg
|
||||||
class="text-muted-foreground truncate"
|
v-if="record.has_fallback"
|
||||||
:title="record.api_key_name"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
class="w-3.5 h-3.5 text-amber-600 dark:text-amber-400 flex-shrink-0"
|
||||||
|
title="此请求发生了 Provider 故障转移"
|
||||||
>
|
>
|
||||||
{{ record.api_key_name }}
|
<path d="m16 3 4 4-4 4" />
|
||||||
<span
|
<path d="M20 7H4" />
|
||||||
v-if="record.rate_multiplier && record.rate_multiplier !== 1.0"
|
<path d="m8 21-4-4 4-4" />
|
||||||
class="text-foreground/60"
|
<path d="M4 17h16" />
|
||||||
>({{ record.rate_multiplier }}x)</span>
|
</svg>
|
||||||
</span>
|
<!-- 重试图标(仅在无故障转移时显示) -->
|
||||||
|
<svg
|
||||||
|
v-else-if="record.has_retry"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
class="w-3.5 h-3.5 text-blue-600 dark:text-blue-400 flex-shrink-0"
|
||||||
|
title="此请求发生了亲和缓存重试"
|
||||||
|
>
|
||||||
|
<path d="M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16" />
|
||||||
|
<path d="M21 21v-5h-5" />
|
||||||
|
<path d="M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" />
|
||||||
|
<path d="M3 3v5h5" />
|
||||||
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell class="py-4 w-[80px]">
|
<TableCell
|
||||||
<span
|
class="py-4 w-[80px]"
|
||||||
v-if="record.api_format"
|
:title="getApiFormatTooltip(record)"
|
||||||
class="inline-flex items-center px-2 py-0.5 rounded-full border border-border/60 text-[10px] font-medium whitespace-nowrap text-muted-foreground"
|
>
|
||||||
:title="record.api_format"
|
<!-- 有格式转换:两行显示 -->
|
||||||
|
<div
|
||||||
|
v-if="record.api_format && record.has_format_conversion && record.endpoint_api_format"
|
||||||
|
class="flex flex-col text-xs gap-0.5"
|
||||||
>
|
>
|
||||||
{{ formatApiFormat(record.api_format) }}
|
<div class="flex items-center gap-1">
|
||||||
</span>
|
<span>{{ formatApiFormat(record.api_format) }}</span>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
class="w-3 h-3 text-muted-foreground flex-shrink-0"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M3 10a.75.75 0 01.75-.75h10.638L10.23 5.29a.75.75 0 111.04-1.08l5.5 5.25a.75.75 0 010 1.08l-5.5 5.25a.75.75 0 11-1.04-1.08l4.158-3.96H3.75A.75.75 0 013 10z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<span class="text-muted-foreground">{{ formatApiFormat(record.endpoint_api_format) }}</span>
|
||||||
|
</div>
|
||||||
|
<!-- 无格式转换:单行显示 -->
|
||||||
|
<span
|
||||||
|
v-else-if="record.api_format"
|
||||||
|
class="text-xs"
|
||||||
|
>{{ formatApiFormat(record.api_format) }}</span>
|
||||||
<span
|
<span
|
||||||
v-else
|
v-else
|
||||||
class="text-muted-foreground text-xs"
|
class="text-muted-foreground text-xs"
|
||||||
@@ -685,6 +700,22 @@ function formatApiFormat(format: string): string {
|
|||||||
return formatMap[format.toUpperCase()] || format
|
return formatMap[format.toUpperCase()] || format
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取 API 格式的 tooltip(包含转换信息)
|
||||||
|
function getApiFormatTooltip(record: UsageRecord): string {
|
||||||
|
if (!record.api_format) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
const displayFormat = formatApiFormat(record.api_format)
|
||||||
|
|
||||||
|
// 如果发生了格式转换,显示详细信息
|
||||||
|
if (record.has_format_conversion && record.endpoint_api_format) {
|
||||||
|
const endpointDisplayFormat = formatApiFormat(record.endpoint_api_format)
|
||||||
|
return `用户请求格式: ${displayFormat}\n端点原生格式: ${endpointDisplayFormat}\n系统进行了 ${displayFormat} → ${endpointDisplayFormat} 格式转换`
|
||||||
|
}
|
||||||
|
|
||||||
|
return record.api_format
|
||||||
|
}
|
||||||
|
|
||||||
// 获取实际使用的模型(优先 target_model,其次 model_version)
|
// 获取实际使用的模型(优先 target_model,其次 model_version)
|
||||||
// 只有当实际模型与请求模型不同时才返回,用于显示映射箭头
|
// 只有当实际模型与请求模型不同时才返回,用于显示映射箭头
|
||||||
function getActualModel(record: UsageRecord): string | null {
|
function getActualModel(record: UsageRecord): string | null {
|
||||||
|
|||||||
@@ -72,6 +72,8 @@ export interface UsageRecord {
|
|||||||
model: string
|
model: string
|
||||||
target_model?: string | null // 映射后的目标模型名(若无映射则为空)
|
target_model?: string | null // 映射后的目标模型名(若无映射则为空)
|
||||||
api_format?: string
|
api_format?: string
|
||||||
|
endpoint_api_format?: string // 端点原生格式
|
||||||
|
has_format_conversion?: boolean // 是否发生了格式转换
|
||||||
input_tokens: number
|
input_tokens: number
|
||||||
output_tokens: number
|
output_tokens: number
|
||||||
cache_creation_input_tokens?: number
|
cache_creation_input_tokens?: number
|
||||||
|
|||||||
@@ -224,6 +224,10 @@ class AdminUpdateEndpointKeyAdapter(AdminApiAdapter):
|
|||||||
# 记录 allowed_models 变化前的值
|
# 记录 allowed_models 变化前的值
|
||||||
allowed_models_before = set(key.allowed_models or [])
|
allowed_models_before = set(key.allowed_models or [])
|
||||||
|
|
||||||
|
# 记录过滤规则变化前的值(用于检测是否需要重新应用过滤)
|
||||||
|
include_patterns_before = key.model_include_patterns
|
||||||
|
exclude_patterns_before = key.model_exclude_patterns
|
||||||
|
|
||||||
update_data = self.key_data.model_dump(exclude_unset=True)
|
update_data = self.key_data.model_dump(exclude_unset=True)
|
||||||
if "api_key" in update_data:
|
if "api_key" in update_data:
|
||||||
update_data["api_key"] = crypto_service.encrypt(update_data["api_key"])
|
update_data["api_key"] = crypto_service.encrypt(update_data["api_key"])
|
||||||
@@ -247,6 +251,17 @@ class AdminUpdateEndpointKeyAdapter(AdminApiAdapter):
|
|||||||
if isinstance(lm, list) and len(lm) == 0:
|
if isinstance(lm, list) and len(lm) == 0:
|
||||||
update_data["locked_models"] = None
|
update_data["locked_models"] = None
|
||||||
|
|
||||||
|
# 处理模型过滤规则:空字符串 -> None
|
||||||
|
if "model_include_patterns" in update_data:
|
||||||
|
patterns = update_data["model_include_patterns"]
|
||||||
|
if isinstance(patterns, list) and len(patterns) == 0:
|
||||||
|
update_data["model_include_patterns"] = None
|
||||||
|
|
||||||
|
if "model_exclude_patterns" in update_data:
|
||||||
|
patterns = update_data["model_exclude_patterns"]
|
||||||
|
if isinstance(patterns, list) and len(patterns) == 0:
|
||||||
|
update_data["model_exclude_patterns"] = None
|
||||||
|
|
||||||
for field, value in update_data.items():
|
for field, value in update_data.items():
|
||||||
setattr(key, field, value)
|
setattr(key, field, value)
|
||||||
key.updated_at = datetime.now(timezone.utc)
|
key.updated_at = datetime.now(timezone.utc)
|
||||||
@@ -285,6 +300,27 @@ class AdminUpdateEndpointKeyAdapter(AdminApiAdapter):
|
|||||||
)
|
)
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(key)
|
db.refresh(key)
|
||||||
|
elif auto_fetch_enabled_after:
|
||||||
|
# auto_fetch_models 保持开启状态,检查过滤规则是否变更
|
||||||
|
include_patterns_after = key.model_include_patterns
|
||||||
|
exclude_patterns_after = key.model_exclude_patterns
|
||||||
|
patterns_changed = (
|
||||||
|
include_patterns_before != include_patterns_after
|
||||||
|
or exclude_patterns_before != exclude_patterns_after
|
||||||
|
)
|
||||||
|
if patterns_changed:
|
||||||
|
# 过滤规则变更,重新应用过滤(使用缓存的上游模型数据)
|
||||||
|
logger.info(
|
||||||
|
"[AUTO_FETCH] Key %s 过滤规则变更,重新应用过滤",
|
||||||
|
self.key_id,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
from src.services.model.fetch_scheduler import get_model_fetch_scheduler
|
||||||
|
|
||||||
|
scheduler = get_model_fetch_scheduler()
|
||||||
|
await scheduler._fetch_models_for_key_by_id(self.key_id)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"重新应用过滤规则失败: {e}")
|
||||||
|
|
||||||
# 任何字段更新都清除缓存,确保缓存一致性
|
# 任何字段更新都清除缓存,确保缓存一致性
|
||||||
# 包括 is_active、allowed_models、capabilities 等影响权限和行为的字段
|
# 包括 is_active、allowed_models、capabilities 等影响权限和行为的字段
|
||||||
@@ -622,6 +658,12 @@ class AdminCreateProviderKeyAdapter(AdminApiAdapter):
|
|||||||
max_probe_interval_minutes=self.key_data.max_probe_interval_minutes,
|
max_probe_interval_minutes=self.key_data.max_probe_interval_minutes,
|
||||||
auto_fetch_models=self.key_data.auto_fetch_models,
|
auto_fetch_models=self.key_data.auto_fetch_models,
|
||||||
locked_models=self.key_data.locked_models if self.key_data.locked_models else None,
|
locked_models=self.key_data.locked_models if self.key_data.locked_models else None,
|
||||||
|
model_include_patterns=(
|
||||||
|
self.key_data.model_include_patterns if self.key_data.model_include_patterns else None
|
||||||
|
),
|
||||||
|
model_exclude_patterns=(
|
||||||
|
self.key_data.model_exclude_patterns if self.key_data.model_exclude_patterns else None
|
||||||
|
),
|
||||||
request_count=0,
|
request_count=0,
|
||||||
success_count=0,
|
success_count=0,
|
||||||
error_count=0,
|
error_count=0,
|
||||||
|
|||||||
@@ -295,6 +295,9 @@ class AdminCreateProviderAdapter(AdminApiAdapter):
|
|||||||
concurrent_limit=validated_data.concurrent_limit,
|
concurrent_limit=validated_data.concurrent_limit,
|
||||||
max_retries=validated_data.max_retries,
|
max_retries=validated_data.max_retries,
|
||||||
proxy=validated_data.proxy.model_dump() if validated_data.proxy else None,
|
proxy=validated_data.proxy.model_dump() if validated_data.proxy else None,
|
||||||
|
# 超时配置
|
||||||
|
stream_first_byte_timeout=validated_data.stream_first_byte_timeout,
|
||||||
|
request_timeout=validated_data.request_timeout,
|
||||||
config=validated_data.config,
|
config=validated_data.config,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -310,6 +310,8 @@ def _build_provider_summary(db: Session, provider: Provider) -> ProviderWithEndp
|
|||||||
quota_expires_at=provider.quota_expires_at,
|
quota_expires_at=provider.quota_expires_at,
|
||||||
max_retries=provider.max_retries,
|
max_retries=provider.max_retries,
|
||||||
proxy=provider.proxy,
|
proxy=provider.proxy,
|
||||||
|
stream_first_byte_timeout=provider.stream_first_byte_timeout,
|
||||||
|
request_timeout=provider.request_timeout,
|
||||||
total_endpoints=total_endpoints,
|
total_endpoints=total_endpoints,
|
||||||
active_endpoints=active_endpoints,
|
active_endpoints=active_endpoints,
|
||||||
total_keys=total_keys,
|
total_keys=total_keys,
|
||||||
|
|||||||
@@ -802,6 +802,22 @@ class AdminUsageRecordsAdapter(AdminApiAdapter):
|
|||||||
if usage.provider_id and str(usage.provider_id) in provider_map:
|
if usage.provider_id and str(usage.provider_id) in provider_map:
|
||||||
provider_name = provider_map[str(usage.provider_id)]
|
provider_name = provider_map[str(usage.provider_id)]
|
||||||
|
|
||||||
|
# 格式转换追踪(兼容历史数据:尽量回填可展示信息)
|
||||||
|
api_format = usage.api_format or (
|
||||||
|
endpoint.api_format if endpoint and endpoint.api_format else None
|
||||||
|
)
|
||||||
|
endpoint_api_format = usage.endpoint_api_format or (
|
||||||
|
endpoint.api_format if endpoint else None
|
||||||
|
)
|
||||||
|
|
||||||
|
has_format_conversion = usage.has_format_conversion
|
||||||
|
if has_format_conversion is None:
|
||||||
|
client_fmt = str(api_format or "").upper()
|
||||||
|
endpoint_fmt = str(endpoint_api_format or "").upper()
|
||||||
|
has_format_conversion = bool(
|
||||||
|
client_fmt and endpoint_fmt and client_fmt != endpoint_fmt
|
||||||
|
)
|
||||||
|
|
||||||
data.append(
|
data.append(
|
||||||
{
|
{
|
||||||
"id": usage.id,
|
"id": usage.id,
|
||||||
@@ -842,8 +858,9 @@ class AdminUsageRecordsAdapter(AdminApiAdapter):
|
|||||||
"has_fallback": fallback_map.get(usage.request_id, False),
|
"has_fallback": fallback_map.get(usage.request_id, False),
|
||||||
"has_retry": retry_map.get(usage.request_id, False),
|
"has_retry": retry_map.get(usage.request_id, False),
|
||||||
"has_rectified": rectified_map.get(usage.request_id, False),
|
"has_rectified": rectified_map.get(usage.request_id, False),
|
||||||
"api_format": usage.api_format
|
"api_format": api_format,
|
||||||
or (endpoint.api_format if endpoint and endpoint.api_format else None),
|
"endpoint_api_format": endpoint_api_format,
|
||||||
|
"has_format_conversion": bool(has_format_conversion),
|
||||||
"api_key_name": provider_api_key.name if provider_api_key else None,
|
"api_key_name": provider_api_key.name if provider_api_key else None,
|
||||||
"request_metadata": usage.request_metadata, # Provider 响应元数据
|
"request_metadata": usage.request_metadata, # Provider 响应元数据
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -109,6 +109,9 @@ class MessageTelemetry:
|
|||||||
provider_endpoint_id: Optional[str] = None,
|
provider_endpoint_id: Optional[str] = None,
|
||||||
provider_api_key_id: Optional[str] = None,
|
provider_api_key_id: Optional[str] = None,
|
||||||
api_format: Optional[str] = None,
|
api_format: Optional[str] = None,
|
||||||
|
# 格式转换追踪
|
||||||
|
endpoint_api_format: Optional[str] = None, # 端点原生 API 格式
|
||||||
|
has_format_conversion: bool = False, # 是否发生了格式转换
|
||||||
# 模型映射信息
|
# 模型映射信息
|
||||||
target_model: Optional[str] = None,
|
target_model: Optional[str] = None,
|
||||||
# Provider 响应元数据(如 Gemini 的 modelVersion)
|
# Provider 响应元数据(如 Gemini 的 modelVersion)
|
||||||
@@ -135,6 +138,8 @@ class MessageTelemetry:
|
|||||||
cache_read_input_tokens=cache_read_tokens,
|
cache_read_input_tokens=cache_read_tokens,
|
||||||
request_type="chat",
|
request_type="chat",
|
||||||
api_format=api_format,
|
api_format=api_format,
|
||||||
|
endpoint_api_format=endpoint_api_format,
|
||||||
|
has_format_conversion=has_format_conversion,
|
||||||
is_stream=is_stream,
|
is_stream=is_stream,
|
||||||
response_time_ms=response_time_ms,
|
response_time_ms=response_time_ms,
|
||||||
first_byte_time_ms=first_byte_time_ms, # 传递首字时间
|
first_byte_time_ms=first_byte_time_ms, # 传递首字时间
|
||||||
@@ -195,6 +200,9 @@ class MessageTelemetry:
|
|||||||
response_body: Optional[Dict[str, Any]] = None,
|
response_body: Optional[Dict[str, Any]] = None,
|
||||||
response_headers: Optional[Dict[str, Any]] = None,
|
response_headers: Optional[Dict[str, Any]] = None,
|
||||||
client_response_headers: Optional[Dict[str, Any]] = None,
|
client_response_headers: Optional[Dict[str, Any]] = None,
|
||||||
|
# 格式转换追踪
|
||||||
|
endpoint_api_format: Optional[str] = None,
|
||||||
|
has_format_conversion: bool = False,
|
||||||
# 模型映射信息
|
# 模型映射信息
|
||||||
target_model: Optional[str] = None,
|
target_model: Optional[str] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -230,6 +238,8 @@ class MessageTelemetry:
|
|||||||
cache_read_input_tokens=cache_read_tokens,
|
cache_read_input_tokens=cache_read_tokens,
|
||||||
request_type="chat",
|
request_type="chat",
|
||||||
api_format=api_format,
|
api_format=api_format,
|
||||||
|
endpoint_api_format=endpoint_api_format,
|
||||||
|
has_format_conversion=has_format_conversion,
|
||||||
is_stream=is_stream,
|
is_stream=is_stream,
|
||||||
response_time_ms=response_time_ms,
|
response_time_ms=response_time_ms,
|
||||||
status_code=status_code,
|
status_code=status_code,
|
||||||
@@ -265,6 +275,9 @@ class MessageTelemetry:
|
|||||||
response_body: Optional[Dict[str, Any]] = None,
|
response_body: Optional[Dict[str, Any]] = None,
|
||||||
response_headers: Optional[Dict[str, Any]] = None,
|
response_headers: Optional[Dict[str, Any]] = None,
|
||||||
client_response_headers: Optional[Dict[str, Any]] = None,
|
client_response_headers: Optional[Dict[str, Any]] = None,
|
||||||
|
# 格式转换追踪
|
||||||
|
endpoint_api_format: Optional[str] = None,
|
||||||
|
has_format_conversion: bool = False,
|
||||||
target_model: Optional[str] = None,
|
target_model: Optional[str] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
@@ -286,6 +299,8 @@ class MessageTelemetry:
|
|||||||
cache_read_input_tokens=cache_read_tokens,
|
cache_read_input_tokens=cache_read_tokens,
|
||||||
request_type="chat",
|
request_type="chat",
|
||||||
api_format=api_format,
|
api_format=api_format,
|
||||||
|
endpoint_api_format=endpoint_api_format,
|
||||||
|
has_format_conversion=has_format_conversion,
|
||||||
is_stream=is_stream,
|
is_stream=is_stream,
|
||||||
response_time_ms=response_time_ms,
|
response_time_ms=response_time_ms,
|
||||||
first_byte_time_ms=first_byte_time_ms,
|
first_byte_time_ms=first_byte_time_ms,
|
||||||
@@ -488,6 +503,9 @@ class BaseMessageHandler:
|
|||||||
key_id = ctx.key_id
|
key_id = ctx.key_id
|
||||||
first_byte_time_ms = ctx.first_byte_time_ms
|
first_byte_time_ms = ctx.first_byte_time_ms
|
||||||
api_format = ctx.api_format
|
api_format = ctx.api_format
|
||||||
|
# 格式转换追踪
|
||||||
|
endpoint_api_format = ctx.provider_api_format or None
|
||||||
|
has_format_conversion = ctx.needs_conversion
|
||||||
|
|
||||||
# 如果 provider 为空,记录警告(不应该发生,但用于调试)
|
# 如果 provider 为空,记录警告(不应该发生,但用于调试)
|
||||||
if not provider:
|
if not provider:
|
||||||
@@ -512,6 +530,8 @@ class BaseMessageHandler:
|
|||||||
provider_api_key_id=key_id,
|
provider_api_key_id=key_id,
|
||||||
first_byte_time_ms=first_byte_time_ms,
|
first_byte_time_ms=first_byte_time_ms,
|
||||||
api_format=api_format,
|
api_format=api_format,
|
||||||
|
endpoint_api_format=endpoint_api_format,
|
||||||
|
has_format_conversion=has_format_conversion,
|
||||||
)
|
)
|
||||||
finally:
|
finally:
|
||||||
db.close()
|
db.close()
|
||||||
|
|||||||
@@ -713,7 +713,8 @@ class ChatHandlerBase(BaseMessageHandler, ABC):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# 流式请求使用 stream_first_byte_timeout 作为首字节超时
|
# 流式请求使用 stream_first_byte_timeout 作为首字节超时
|
||||||
request_timeout = config.stream_first_byte_timeout
|
# 优先使用 Provider 配置,否则使用全局配置
|
||||||
|
request_timeout = provider.stream_first_byte_timeout or config.stream_first_byte_timeout
|
||||||
|
|
||||||
# 创建 HTTP 客户端(支持代理配置,从 Provider 读取)
|
# 创建 HTTP 客户端(支持代理配置,从 Provider 读取)
|
||||||
from src.clients.http_client import HTTPClientPool
|
from src.clients.http_client import HTTPClientPool
|
||||||
@@ -851,6 +852,9 @@ class ChatHandlerBase(BaseMessageHandler, ABC):
|
|||||||
provider_request_headers=ctx.provider_request_headers,
|
provider_request_headers=ctx.provider_request_headers,
|
||||||
response_headers=ctx.response_headers,
|
response_headers=ctx.response_headers,
|
||||||
client_response_headers=client_response_headers,
|
client_response_headers=client_response_headers,
|
||||||
|
# 格式转换追踪
|
||||||
|
endpoint_api_format=ctx.provider_api_format or None,
|
||||||
|
has_format_conversion=ctx.needs_conversion,
|
||||||
target_model=ctx.mapped_model,
|
target_model=ctx.mapped_model,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -987,7 +991,8 @@ class ChatHandlerBase(BaseMessageHandler, ABC):
|
|||||||
from src.clients.http_client import HTTPClientPool
|
from src.clients.http_client import HTTPClientPool
|
||||||
|
|
||||||
# 非流式请求使用 http_request_timeout 作为整体超时
|
# 非流式请求使用 http_request_timeout 作为整体超时
|
||||||
request_timeout = config.http_request_timeout
|
# 优先使用 Provider 配置,否则使用全局配置
|
||||||
|
request_timeout = provider.request_timeout or config.http_request_timeout
|
||||||
http_client = await HTTPClientPool.get_proxy_client(
|
http_client = await HTTPClientPool.get_proxy_client(
|
||||||
proxy_config=provider.proxy,
|
proxy_config=provider.proxy,
|
||||||
)
|
)
|
||||||
@@ -1140,6 +1145,9 @@ class ChatHandlerBase(BaseMessageHandler, ABC):
|
|||||||
is_stream=False,
|
is_stream=False,
|
||||||
provider_request_headers=provider_request_headers,
|
provider_request_headers=provider_request_headers,
|
||||||
api_format=api_format,
|
api_format=api_format,
|
||||||
|
# 格式转换追踪
|
||||||
|
endpoint_api_format=provider_api_format_for_error or None,
|
||||||
|
has_format_conversion=needs_conversion_for_error,
|
||||||
provider_id=provider_id,
|
provider_id=provider_id,
|
||||||
provider_endpoint_id=endpoint_id,
|
provider_endpoint_id=endpoint_id,
|
||||||
provider_api_key_id=key_id,
|
provider_api_key_id=key_id,
|
||||||
@@ -1203,6 +1211,9 @@ class ChatHandlerBase(BaseMessageHandler, ABC):
|
|||||||
provider_request_headers=provider_request_headers,
|
provider_request_headers=provider_request_headers,
|
||||||
response_headers=response_headers,
|
response_headers=response_headers,
|
||||||
client_response_headers={"content-type": "application/json"},
|
client_response_headers={"content-type": "application/json"},
|
||||||
|
# 格式转换追踪
|
||||||
|
endpoint_api_format=provider_api_format_for_error or None,
|
||||||
|
has_format_conversion=needs_conversion_for_error,
|
||||||
target_model=mapped_model_result,
|
target_model=mapped_model_result,
|
||||||
)
|
)
|
||||||
client_format = (client_api_format_for_error or "").upper()
|
client_format = (client_api_format_for_error or "").upper()
|
||||||
@@ -1249,6 +1260,9 @@ class ChatHandlerBase(BaseMessageHandler, ABC):
|
|||||||
response_headers=error_response_headers,
|
response_headers=error_response_headers,
|
||||||
# 非流式失败返回给客户端的是 JSON 错误响应
|
# 非流式失败返回给客户端的是 JSON 错误响应
|
||||||
client_response_headers={"content-type": "application/json"},
|
client_response_headers={"content-type": "application/json"},
|
||||||
|
# 格式转换追踪
|
||||||
|
endpoint_api_format=provider_api_format_for_error or None,
|
||||||
|
has_format_conversion=needs_conversion_for_error,
|
||||||
# 模型映射信息
|
# 模型映射信息
|
||||||
target_model=mapped_model_result,
|
target_model=mapped_model_result,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -739,7 +739,8 @@ class CliMessageHandlerBase(BaseMessageHandler):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# 流式请求使用 stream_first_byte_timeout 作为首字节超时
|
# 流式请求使用 stream_first_byte_timeout 作为首字节超时
|
||||||
request_timeout = config.stream_first_byte_timeout
|
# 优先使用 Provider 配置,否则使用全局配置
|
||||||
|
request_timeout = provider.stream_first_byte_timeout or config.stream_first_byte_timeout
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f" └─ [{self.request_id}] 发送流式请求: "
|
f" └─ [{self.request_id}] 发送流式请求: "
|
||||||
@@ -1068,7 +1069,8 @@ class CliMessageHandlerBase(BaseMessageHandler):
|
|||||||
provider_parser = self.parser
|
provider_parser = self.parser
|
||||||
|
|
||||||
# 使用共享的 TTFB 超时函数读取首字节
|
# 使用共享的 TTFB 超时函数读取首字节
|
||||||
ttfb_timeout = config.stream_first_byte_timeout
|
# 优先使用 Provider 配置,否则使用全局配置
|
||||||
|
ttfb_timeout = provider.stream_first_byte_timeout or config.stream_first_byte_timeout
|
||||||
first_chunk, aiter = await read_first_chunk_with_ttfb_timeout(
|
first_chunk, aiter = await read_first_chunk_with_ttfb_timeout(
|
||||||
byte_iterator,
|
byte_iterator,
|
||||||
timeout=ttfb_timeout,
|
timeout=ttfb_timeout,
|
||||||
@@ -1804,6 +1806,9 @@ class CliMessageHandlerBase(BaseMessageHandler):
|
|||||||
response_body=response_body,
|
response_body=response_body,
|
||||||
response_headers=ctx.response_headers,
|
response_headers=ctx.response_headers,
|
||||||
client_response_headers=client_response_headers,
|
client_response_headers=client_response_headers,
|
||||||
|
# 格式转换追踪
|
||||||
|
endpoint_api_format=ctx.provider_api_format or None,
|
||||||
|
has_format_conversion=ctx.needs_conversion,
|
||||||
# 模型映射信息
|
# 模型映射信息
|
||||||
target_model=ctx.mapped_model,
|
target_model=ctx.mapped_model,
|
||||||
)
|
)
|
||||||
@@ -1845,6 +1850,9 @@ class CliMessageHandlerBase(BaseMessageHandler):
|
|||||||
is_stream=True,
|
is_stream=True,
|
||||||
provider_request_headers=ctx.provider_request_headers,
|
provider_request_headers=ctx.provider_request_headers,
|
||||||
api_format=ctx.api_format,
|
api_format=ctx.api_format,
|
||||||
|
# 格式转换追踪
|
||||||
|
endpoint_api_format=ctx.provider_api_format or None,
|
||||||
|
has_format_conversion=ctx.needs_conversion,
|
||||||
# Provider 侧追踪信息(用于记录真实成本)
|
# Provider 侧追踪信息(用于记录真实成本)
|
||||||
provider_id=ctx.provider_id,
|
provider_id=ctx.provider_id,
|
||||||
provider_endpoint_id=ctx.endpoint_id,
|
provider_endpoint_id=ctx.endpoint_id,
|
||||||
@@ -1977,6 +1985,9 @@ class CliMessageHandlerBase(BaseMessageHandler):
|
|||||||
provider_request_headers=ctx.provider_request_headers,
|
provider_request_headers=ctx.provider_request_headers,
|
||||||
response_headers=ctx.response_headers,
|
response_headers=ctx.response_headers,
|
||||||
client_response_headers=client_response_headers,
|
client_response_headers=client_response_headers,
|
||||||
|
# 格式转换追踪
|
||||||
|
endpoint_api_format=ctx.provider_api_format or None,
|
||||||
|
has_format_conversion=ctx.needs_conversion,
|
||||||
# 模型映射信息
|
# 模型映射信息
|
||||||
target_model=ctx.mapped_model,
|
target_model=ctx.mapped_model,
|
||||||
)
|
)
|
||||||
@@ -2101,7 +2112,8 @@ class CliMessageHandlerBase(BaseMessageHandler):
|
|||||||
from src.clients.http_client import HTTPClientPool
|
from src.clients.http_client import HTTPClientPool
|
||||||
|
|
||||||
# 非流式请求使用 http_request_timeout 作为整体超时
|
# 非流式请求使用 http_request_timeout 作为整体超时
|
||||||
request_timeout = config.http_request_timeout
|
# 优先使用 Provider 配置,否则使用全局配置
|
||||||
|
request_timeout = provider.request_timeout or config.http_request_timeout
|
||||||
http_client = await HTTPClientPool.get_proxy_client(
|
http_client = await HTTPClientPool.get_proxy_client(
|
||||||
proxy_config=provider.proxy,
|
proxy_config=provider.proxy,
|
||||||
)
|
)
|
||||||
@@ -2274,6 +2286,9 @@ class CliMessageHandlerBase(BaseMessageHandler):
|
|||||||
is_stream=False,
|
is_stream=False,
|
||||||
provider_request_headers=provider_request_headers,
|
provider_request_headers=provider_request_headers,
|
||||||
api_format=api_format,
|
api_format=api_format,
|
||||||
|
# 格式转换追踪
|
||||||
|
endpoint_api_format=provider_api_format or None,
|
||||||
|
has_format_conversion=needs_conversion,
|
||||||
# Provider 侧追踪信息(用于记录真实成本)
|
# Provider 侧追踪信息(用于记录真实成本)
|
||||||
provider_id=provider_id,
|
provider_id=provider_id,
|
||||||
provider_endpoint_id=endpoint_id,
|
provider_endpoint_id=endpoint_id,
|
||||||
@@ -2346,6 +2361,9 @@ class CliMessageHandlerBase(BaseMessageHandler):
|
|||||||
response_headers=error_response_headers,
|
response_headers=error_response_headers,
|
||||||
# 非流式失败返回给客户端的是 JSON 错误响应
|
# 非流式失败返回给客户端的是 JSON 错误响应
|
||||||
client_response_headers={"content-type": "application/json"},
|
client_response_headers={"content-type": "application/json"},
|
||||||
|
# 格式转换追踪
|
||||||
|
endpoint_api_format=provider_api_format or None,
|
||||||
|
has_format_conversion=needs_conversion,
|
||||||
# 模型映射信息
|
# 模型映射信息
|
||||||
target_model=mapped_model_result,
|
target_model=mapped_model_result,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -213,7 +213,8 @@ class StreamProcessor:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
# 使用共享的 TTFB 超时函数读取首字节
|
# 使用共享的 TTFB 超时函数读取首字节
|
||||||
ttfb_timeout = config.stream_first_byte_timeout
|
# 优先使用 Provider 配置,否则使用全局配置
|
||||||
|
ttfb_timeout = provider.stream_first_byte_timeout or config.stream_first_byte_timeout
|
||||||
first_chunk, aiter = await read_first_chunk_with_ttfb_timeout(
|
first_chunk, aiter = await read_first_chunk_with_ttfb_timeout(
|
||||||
byte_iterator,
|
byte_iterator,
|
||||||
timeout=ttfb_timeout,
|
timeout=ttfb_timeout,
|
||||||
@@ -422,6 +423,8 @@ class StreamProcessor:
|
|||||||
f"[{self.request_id}] needs_conversion=True 但 provider_format 为空,回退到透传模式"
|
f"[{self.request_id}] needs_conversion=True 但 provider_format 为空,回退到透传模式"
|
||||||
)
|
)
|
||||||
needs_conversion = False
|
needs_conversion = False
|
||||||
|
# 保持 ctx 与实际行为一致,避免 Usage 记录误标记为转换
|
||||||
|
ctx.needs_conversion = False
|
||||||
|
|
||||||
def _mark_stream_started() -> None:
|
def _mark_stream_started() -> None:
|
||||||
nonlocal start_time, streaming_started
|
nonlocal start_time, streaming_started
|
||||||
|
|||||||
@@ -91,6 +91,9 @@ class CreateProviderRequest(BaseModel):
|
|||||||
# 请求配置(从 Endpoint 迁移)
|
# 请求配置(从 Endpoint 迁移)
|
||||||
max_retries: Optional[int] = Field(2, ge=0, le=10, description="最大重试次数")
|
max_retries: Optional[int] = Field(2, ge=0, le=10, description="最大重试次数")
|
||||||
proxy: Optional[ProxyConfig] = Field(None, description="代理配置")
|
proxy: Optional[ProxyConfig] = Field(None, description="代理配置")
|
||||||
|
# 超时配置(秒),为空时使用全局配置
|
||||||
|
stream_first_byte_timeout: Optional[float] = Field(None, ge=1, le=300, description="流式请求首字节超时(秒)")
|
||||||
|
request_timeout: Optional[float] = Field(None, ge=1, le=600, description="非流式请求整体超时(秒)")
|
||||||
config: Optional[Dict[str, Any]] = Field(None, description="其他配置")
|
config: Optional[Dict[str, Any]] = Field(None, description="其他配置")
|
||||||
|
|
||||||
@field_validator("name", "description")
|
@field_validator("name", "description")
|
||||||
@@ -161,6 +164,9 @@ class UpdateProviderRequest(BaseModel):
|
|||||||
# 请求配置(从 Endpoint 迁移)
|
# 请求配置(从 Endpoint 迁移)
|
||||||
max_retries: Optional[int] = Field(None, ge=0, le=10, description="最大重试次数")
|
max_retries: Optional[int] = Field(None, ge=0, le=10, description="最大重试次数")
|
||||||
proxy: Optional[ProxyConfig] = Field(None, description="代理配置")
|
proxy: Optional[ProxyConfig] = Field(None, description="代理配置")
|
||||||
|
# 超时配置(秒),为空时使用全局配置
|
||||||
|
stream_first_byte_timeout: Optional[float] = Field(None, ge=1, le=300, description="流式请求首字节超时(秒)")
|
||||||
|
request_timeout: Optional[float] = Field(None, ge=1, le=600, description="非流式请求整体超时(秒)")
|
||||||
config: Optional[Dict[str, Any]] = None
|
config: Optional[Dict[str, Any]] = None
|
||||||
|
|
||||||
# 复用相同的验证器
|
# 复用相同的验证器
|
||||||
|
|||||||
@@ -405,6 +405,10 @@ class ProviderCreate(BaseModel):
|
|||||||
config: Optional[dict] = Field(None, description="额外配置")
|
config: Optional[dict] = Field(None, description="额外配置")
|
||||||
is_active: bool = Field(False, description="是否启用(默认false,需要配置API密钥后才能启用)")
|
is_active: bool = Field(False, description="是否启用(默认false,需要配置API密钥后才能启用)")
|
||||||
|
|
||||||
|
# 超时配置(秒),为空时使用全局配置
|
||||||
|
stream_first_byte_timeout: Optional[float] = Field(None, ge=1, le=300, description="流式请求首字节超时(秒)")
|
||||||
|
request_timeout: Optional[float] = Field(None, ge=1, le=600, description="非流式请求整体超时(秒)")
|
||||||
|
|
||||||
|
|
||||||
class ProviderUpdate(BaseModel):
|
class ProviderUpdate(BaseModel):
|
||||||
"""更新提供商请求"""
|
"""更新提供商请求"""
|
||||||
@@ -423,6 +427,10 @@ class ProviderUpdate(BaseModel):
|
|||||||
config: Optional[dict] = None
|
config: Optional[dict] = None
|
||||||
is_active: Optional[bool] = None
|
is_active: Optional[bool] = None
|
||||||
|
|
||||||
|
# 超时配置(秒),为空时使用全局配置
|
||||||
|
stream_first_byte_timeout: Optional[float] = Field(None, ge=1, le=300, description="流式请求首字节超时(秒)")
|
||||||
|
request_timeout: Optional[float] = Field(None, ge=1, le=600, description="非流式请求整体超时(秒)")
|
||||||
|
|
||||||
|
|
||||||
class ProviderResponse(BaseModel):
|
class ProviderResponse(BaseModel):
|
||||||
"""提供商响应"""
|
"""提供商响应"""
|
||||||
@@ -447,6 +455,10 @@ class ProviderResponse(BaseModel):
|
|||||||
active_models_count: int = 0
|
active_models_count: int = 0
|
||||||
api_keys_count: int = 0
|
api_keys_count: int = 0
|
||||||
|
|
||||||
|
# 超时配置
|
||||||
|
stream_first_byte_timeout: Optional[float] = None
|
||||||
|
request_timeout: Optional[float] = None
|
||||||
|
|
||||||
model_config = ConfigDict(from_attributes=True)
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -327,7 +327,9 @@ class Usage(Base):
|
|||||||
|
|
||||||
# 请求详情
|
# 请求详情
|
||||||
request_type = Column(String(50)) # chat, completion, embedding等
|
request_type = Column(String(50)) # chat, completion, embedding等
|
||||||
api_format = Column(String(50), nullable=True) # API 格式: CLAUDE, OPENAI 等
|
api_format = Column(String(50), nullable=True) # API 格式: CLAUDE, OPENAI 等(用户请求格式)
|
||||||
|
endpoint_api_format = Column(String(50), nullable=True) # 端点原生 API 格式
|
||||||
|
has_format_conversion = Column(Boolean, nullable=True, default=False) # 是否发生了格式转换
|
||||||
is_stream = Column(Boolean, default=False) # 是否为流式请求
|
is_stream = Column(Boolean, default=False) # 是否为流式请求
|
||||||
status_code = Column(Integer)
|
status_code = Column(Integer)
|
||||||
error_message = Column(Text, nullable=True)
|
error_message = Column(Text, nullable=True)
|
||||||
@@ -653,6 +655,10 @@ class Provider(Base):
|
|||||||
max_retries = Column(Integer, default=2, nullable=True) # 最大重试次数
|
max_retries = Column(Integer, default=2, nullable=True) # 最大重试次数
|
||||||
proxy = Column(JSONB, nullable=True) # 代理配置: {url, username, password, enabled}
|
proxy = Column(JSONB, nullable=True) # 代理配置: {url, username, password, enabled}
|
||||||
|
|
||||||
|
# 超时配置(秒),为 None 时使用全局配置
|
||||||
|
stream_first_byte_timeout = Column(Float, nullable=True) # 流式请求首字节超时
|
||||||
|
request_timeout = Column(Float, nullable=True) # 非流式请求整体超时
|
||||||
|
|
||||||
# 配置
|
# 配置
|
||||||
config = Column(JSON, nullable=True) # 额外配置(如Azure deployment name等)
|
config = Column(JSON, nullable=True) # 额外配置(如Azure deployment name等)
|
||||||
|
|
||||||
@@ -1178,6 +1184,9 @@ class ProviderAPIKey(Base):
|
|||||||
last_models_fetch_at = Column(DateTime(timezone=True), nullable=True) # 最后获取时间
|
last_models_fetch_at = Column(DateTime(timezone=True), nullable=True) # 最后获取时间
|
||||||
last_models_fetch_error = Column(Text, nullable=True) # 最后获取错误信息
|
last_models_fetch_error = Column(Text, nullable=True) # 最后获取错误信息
|
||||||
locked_models = Column(JSON, nullable=True) # 被锁定的模型列表(刷新时不会被删除)
|
locked_models = Column(JSON, nullable=True) # 被锁定的模型列表(刷新时不会被删除)
|
||||||
|
# 模型过滤规则(支持 * 和 ? 通配符,如 "gpt-*", "claude-?-sonnet")
|
||||||
|
model_include_patterns = Column(JSON, nullable=True) # 包含规则列表,空表示不过滤(包含所有)
|
||||||
|
model_exclude_patterns = Column(JSON, nullable=True) # 排除规则列表,空表示不排除
|
||||||
|
|
||||||
# 时间戳
|
# 时间戳
|
||||||
created_at = Column(
|
created_at = Column(
|
||||||
|
|||||||
@@ -211,6 +211,14 @@ class EndpointAPIKeyCreate(BaseModel):
|
|||||||
default=None, description="被锁定的模型列表(刷新时不会被删除)"
|
default=None, description="被锁定的模型列表(刷新时不会被删除)"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# 模型过滤规则(仅当 auto_fetch_models=True 时生效)
|
||||||
|
model_include_patterns: Optional[List[str]] = Field(
|
||||||
|
default=None, description="模型包含规则(支持 * 和 ? 通配符),空表示包含所有"
|
||||||
|
)
|
||||||
|
model_exclude_patterns: Optional[List[str]] = Field(
|
||||||
|
default=None, description="模型排除规则(支持 * 和 ? 通配符),空表示不排除"
|
||||||
|
)
|
||||||
|
|
||||||
@field_validator("api_formats")
|
@field_validator("api_formats")
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate_api_formats(cls, v: Optional[List[str]]) -> Optional[List[str]]:
|
def validate_api_formats(cls, v: Optional[List[str]]) -> Optional[List[str]]:
|
||||||
@@ -345,6 +353,13 @@ class EndpointAPIKeyUpdate(BaseModel):
|
|||||||
locked_models: Optional[List[str]] = Field(
|
locked_models: Optional[List[str]] = Field(
|
||||||
default=None, description="被锁定的模型列表(刷新时不会被删除)"
|
default=None, description="被锁定的模型列表(刷新时不会被删除)"
|
||||||
)
|
)
|
||||||
|
# 模型过滤规则(仅当 auto_fetch_models=True 时生效)
|
||||||
|
model_include_patterns: Optional[List[str]] = Field(
|
||||||
|
default=None, description="模型包含规则(支持 * 和 ? 通配符),空表示包含所有"
|
||||||
|
)
|
||||||
|
model_exclude_patterns: Optional[List[str]] = Field(
|
||||||
|
default=None, description="模型排除规则(支持 * 和 ? 通配符),空表示不排除"
|
||||||
|
)
|
||||||
|
|
||||||
@field_validator("api_formats")
|
@field_validator("api_formats")
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -502,6 +517,9 @@ class EndpointAPIKeyResponse(BaseModel):
|
|||||||
last_models_fetch_at: Optional[datetime] = Field(None, description="最后获取模型时间")
|
last_models_fetch_at: Optional[datetime] = Field(None, description="最后获取模型时间")
|
||||||
last_models_fetch_error: Optional[str] = Field(None, description="最后获取模型错误信息")
|
last_models_fetch_error: Optional[str] = Field(None, description="最后获取模型错误信息")
|
||||||
locked_models: Optional[List[str]] = Field(None, description="被锁定的模型列表")
|
locked_models: Optional[List[str]] = Field(None, description="被锁定的模型列表")
|
||||||
|
# 模型过滤规则
|
||||||
|
model_include_patterns: Optional[List[str]] = Field(None, description="模型包含规则")
|
||||||
|
model_exclude_patterns: Optional[List[str]] = Field(None, description="模型排除规则")
|
||||||
|
|
||||||
# 时间戳
|
# 时间戳
|
||||||
last_used_at: Optional[datetime] = None
|
last_used_at: Optional[datetime] = None
|
||||||
@@ -605,6 +623,9 @@ class ProviderUpdateRequest(BaseModel):
|
|||||||
# 请求配置(从 Endpoint 迁移)
|
# 请求配置(从 Endpoint 迁移)
|
||||||
max_retries: Optional[int] = Field(None, ge=0, le=10, description="最大重试次数")
|
max_retries: Optional[int] = Field(None, ge=0, le=10, description="最大重试次数")
|
||||||
proxy: Optional[Dict[str, Any]] = Field(None, description="代理配置")
|
proxy: Optional[Dict[str, Any]] = Field(None, description="代理配置")
|
||||||
|
# 超时配置(秒),为空时使用全局配置
|
||||||
|
stream_first_byte_timeout: Optional[float] = Field(None, ge=1, le=300, description="流式请求首字节超时(秒)")
|
||||||
|
request_timeout: Optional[float] = Field(None, ge=1, le=600, description="非流式请求整体超时(秒)")
|
||||||
|
|
||||||
|
|
||||||
class ProviderWithEndpointsSummary(BaseModel):
|
class ProviderWithEndpointsSummary(BaseModel):
|
||||||
@@ -629,6 +650,9 @@ class ProviderWithEndpointsSummary(BaseModel):
|
|||||||
# 请求配置(从 Endpoint 迁移)
|
# 请求配置(从 Endpoint 迁移)
|
||||||
max_retries: Optional[int] = Field(default=2, description="最大重试次数")
|
max_retries: Optional[int] = Field(default=2, description="最大重试次数")
|
||||||
proxy: Optional[Dict[str, Any]] = Field(default=None, description="代理配置")
|
proxy: Optional[Dict[str, Any]] = Field(default=None, description="代理配置")
|
||||||
|
# 超时配置(秒),为空时使用全局配置
|
||||||
|
stream_first_byte_timeout: Optional[float] = Field(default=None, description="流式请求首字节超时(秒)")
|
||||||
|
request_timeout: Optional[float] = Field(default=None, description="非流式请求整体超时(秒)")
|
||||||
|
|
||||||
# Endpoint 统计
|
# Endpoint 统计
|
||||||
total_endpoints: int = Field(default=0, description="总 Endpoint 数量")
|
total_endpoints: int = Field(default=0, description="总 Endpoint 数量")
|
||||||
|
|||||||
@@ -7,13 +7,15 @@
|
|||||||
- 扫描所有启用了 auto_fetch_models 的 ProviderAPIKey
|
- 扫描所有启用了 auto_fetch_models 的 ProviderAPIKey
|
||||||
- 调用 Adapter.fetch_models() 获取模型列表
|
- 调用 Adapter.fetch_models() 获取模型列表
|
||||||
- 更新 Key 的 allowed_models(保留 locked_models 中的模型)
|
- 更新 Key 的 allowed_models(保留 locked_models 中的模型)
|
||||||
|
- 支持包含/排除规则过滤模型
|
||||||
- 记录获取结果和错误信息
|
- 记录获取结果和错误信息
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import fnmatch
|
||||||
import os
|
import os
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from typing import Optional
|
from typing import List, Optional, Set
|
||||||
|
|
||||||
from sqlalchemy.orm import Session, joinedload
|
from sqlalchemy.orm import Session, joinedload
|
||||||
|
|
||||||
@@ -42,6 +44,68 @@ KEY_FETCH_TIMEOUT_SECONDS = 120
|
|||||||
UPSTREAM_MODELS_CACHE_TTL_SECONDS = MODEL_FETCH_INTERVAL_MINUTES * 60
|
UPSTREAM_MODELS_CACHE_TTL_SECONDS = MODEL_FETCH_INTERVAL_MINUTES * 60
|
||||||
|
|
||||||
|
|
||||||
|
def _match_pattern(model_id: str, pattern: str) -> bool:
|
||||||
|
"""
|
||||||
|
检查模型 ID 是否匹配模式
|
||||||
|
|
||||||
|
支持的通配符:
|
||||||
|
- * 匹配任意字符(包括空)
|
||||||
|
- ? 匹配单个字符
|
||||||
|
|
||||||
|
Args:
|
||||||
|
model_id: 模型 ID
|
||||||
|
pattern: 匹配模式
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
是否匹配
|
||||||
|
"""
|
||||||
|
return fnmatch.fnmatch(model_id.lower(), pattern.lower())
|
||||||
|
|
||||||
|
|
||||||
|
def _filter_models_by_patterns(
|
||||||
|
model_ids: Set[str],
|
||||||
|
include_patterns: Optional[List[str]],
|
||||||
|
exclude_patterns: Optional[List[str]],
|
||||||
|
) -> Set[str]:
|
||||||
|
"""
|
||||||
|
根据包含/排除规则过滤模型列表
|
||||||
|
|
||||||
|
规则优先级:
|
||||||
|
1. 如果 include_patterns 为空或 None,则包含所有模型
|
||||||
|
2. 如果 include_patterns 不为空,则只包含匹配的模型
|
||||||
|
3. exclude_patterns 总是会排除匹配的模型(优先级高于 include)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
model_ids: 原始模型 ID 集合
|
||||||
|
include_patterns: 包含规则列表(支持 * 和 ? 通配符)
|
||||||
|
exclude_patterns: 排除规则列表(支持 * 和 ? 通配符)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
过滤后的模型 ID 集合
|
||||||
|
"""
|
||||||
|
result = set()
|
||||||
|
|
||||||
|
for model_id in model_ids:
|
||||||
|
# 步骤1: 检查是否应该包含
|
||||||
|
should_include = True
|
||||||
|
if include_patterns:
|
||||||
|
# 有包含规则时,必须匹配至少一个规则
|
||||||
|
should_include = any(_match_pattern(model_id, p) for p in include_patterns)
|
||||||
|
|
||||||
|
if not should_include:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 步骤2: 检查是否应该排除
|
||||||
|
should_exclude = False
|
||||||
|
if exclude_patterns:
|
||||||
|
should_exclude = any(_match_pattern(model_id, p) for p in exclude_patterns)
|
||||||
|
|
||||||
|
if not should_exclude:
|
||||||
|
result.add(model_id)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
def _get_upstream_models_cache_key(provider_id: str, api_key_id: str) -> str:
|
def _get_upstream_models_cache_key(provider_id: str, api_key_id: str) -> str:
|
||||||
"""生成上游模型缓存的 key"""
|
"""生成上游模型缓存的 key"""
|
||||||
return f"upstream_models:{provider_id}:{api_key_id}"
|
return f"upstream_models:{provider_id}:{api_key_id}"
|
||||||
@@ -341,7 +405,7 @@ class ModelFetchScheduler:
|
|||||||
|
|
||||||
def _update_key_allowed_models(self, key: ProviderAPIKey, fetched_model_ids: set[str]) -> bool:
|
def _update_key_allowed_models(self, key: ProviderAPIKey, fetched_model_ids: set[str]) -> bool:
|
||||||
"""
|
"""
|
||||||
更新 Key 的 allowed_models,保留 locked_models
|
更新 Key 的 allowed_models,保留 locked_models,应用过滤规则
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: 是否有变化
|
bool: 是否有变化
|
||||||
@@ -349,9 +413,26 @@ class ModelFetchScheduler:
|
|||||||
# 获取当前锁定的模型
|
# 获取当前锁定的模型
|
||||||
locked_models = set(key.locked_models or [])
|
locked_models = set(key.locked_models or [])
|
||||||
|
|
||||||
# 新的 allowed_models = 获取到的模型 + 锁定的模型
|
# 应用包含/排除过滤规则
|
||||||
|
include_patterns = key.model_include_patterns
|
||||||
|
exclude_patterns = key.model_exclude_patterns
|
||||||
|
|
||||||
|
filtered_model_ids = _filter_models_by_patterns(
|
||||||
|
fetched_model_ids, include_patterns, exclude_patterns
|
||||||
|
)
|
||||||
|
|
||||||
|
# 记录过滤结果
|
||||||
|
if include_patterns or exclude_patterns:
|
||||||
|
filtered_count = len(fetched_model_ids) - len(filtered_model_ids)
|
||||||
|
if filtered_count > 0:
|
||||||
|
logger.info(
|
||||||
|
f"Key {key.id} 过滤规则生效: 原始 {len(fetched_model_ids)} 个模型, "
|
||||||
|
f"过滤后 {len(filtered_model_ids)} 个 (排除 {filtered_count} 个)"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 新的 allowed_models = 过滤后的模型 + 锁定的模型
|
||||||
# 锁定模型无论上游是否返回都会保留
|
# 锁定模型无论上游是否返回都会保留
|
||||||
new_allowed_models = list(fetched_model_ids | locked_models)
|
new_allowed_models = list(filtered_model_ids | locked_models)
|
||||||
new_allowed_models.sort() # 保持顺序稳定
|
new_allowed_models.sort() # 保持顺序稳定
|
||||||
|
|
||||||
# 检查是否有变化
|
# 检查是否有变化
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ class UsageRecordParams:
|
|||||||
cache_read_input_tokens: int
|
cache_read_input_tokens: int
|
||||||
request_type: str
|
request_type: str
|
||||||
api_format: Optional[str]
|
api_format: Optional[str]
|
||||||
|
endpoint_api_format: Optional[str] # 端点原生 API 格式
|
||||||
|
has_format_conversion: bool # 是否发生了格式转换
|
||||||
is_stream: bool
|
is_stream: bool
|
||||||
response_time_ms: Optional[int]
|
response_time_ms: Optional[int]
|
||||||
first_byte_time_ms: Optional[int]
|
first_byte_time_ms: Optional[int]
|
||||||
@@ -214,6 +216,8 @@ class UsageService:
|
|||||||
cache_read_input_tokens: int,
|
cache_read_input_tokens: int,
|
||||||
request_type: str,
|
request_type: str,
|
||||||
api_format: Optional[str],
|
api_format: Optional[str],
|
||||||
|
endpoint_api_format: Optional[str],
|
||||||
|
has_format_conversion: bool,
|
||||||
is_stream: bool,
|
is_stream: bool,
|
||||||
response_time_ms: Optional[int],
|
response_time_ms: Optional[int],
|
||||||
first_byte_time_ms: Optional[int],
|
first_byte_time_ms: Optional[int],
|
||||||
@@ -349,6 +353,8 @@ class UsageService:
|
|||||||
"price_per_request": request_price,
|
"price_per_request": request_price,
|
||||||
"request_type": request_type,
|
"request_type": request_type,
|
||||||
"api_format": api_format,
|
"api_format": api_format,
|
||||||
|
"endpoint_api_format": endpoint_api_format,
|
||||||
|
"has_format_conversion": has_format_conversion,
|
||||||
"is_stream": is_stream,
|
"is_stream": is_stream,
|
||||||
"status_code": status_code,
|
"status_code": status_code,
|
||||||
"error_message": error_message,
|
"error_message": error_message,
|
||||||
@@ -706,6 +712,8 @@ class UsageService:
|
|||||||
cache_read_input_tokens=params.cache_read_input_tokens,
|
cache_read_input_tokens=params.cache_read_input_tokens,
|
||||||
request_type=params.request_type,
|
request_type=params.request_type,
|
||||||
api_format=params.api_format,
|
api_format=params.api_format,
|
||||||
|
endpoint_api_format=params.endpoint_api_format,
|
||||||
|
has_format_conversion=params.has_format_conversion,
|
||||||
is_stream=params.is_stream,
|
is_stream=params.is_stream,
|
||||||
response_time_ms=params.response_time_ms,
|
response_time_ms=params.response_time_ms,
|
||||||
first_byte_time_ms=params.first_byte_time_ms,
|
first_byte_time_ms=params.first_byte_time_ms,
|
||||||
@@ -756,6 +764,8 @@ class UsageService:
|
|||||||
cache_read_input_tokens: int = 0,
|
cache_read_input_tokens: int = 0,
|
||||||
request_type: str = "chat",
|
request_type: str = "chat",
|
||||||
api_format: Optional[str] = None,
|
api_format: Optional[str] = None,
|
||||||
|
endpoint_api_format: Optional[str] = None,
|
||||||
|
has_format_conversion: bool = False,
|
||||||
is_stream: bool = False,
|
is_stream: bool = False,
|
||||||
response_time_ms: Optional[int] = None,
|
response_time_ms: Optional[int] = None,
|
||||||
first_byte_time_ms: Optional[int] = None,
|
first_byte_time_ms: Optional[int] = None,
|
||||||
@@ -794,7 +804,9 @@ class UsageService:
|
|||||||
input_tokens=input_tokens, output_tokens=output_tokens,
|
input_tokens=input_tokens, output_tokens=output_tokens,
|
||||||
cache_creation_input_tokens=cache_creation_input_tokens,
|
cache_creation_input_tokens=cache_creation_input_tokens,
|
||||||
cache_read_input_tokens=cache_read_input_tokens,
|
cache_read_input_tokens=cache_read_input_tokens,
|
||||||
request_type=request_type, api_format=api_format, is_stream=is_stream,
|
request_type=request_type, api_format=api_format,
|
||||||
|
endpoint_api_format=endpoint_api_format, has_format_conversion=has_format_conversion,
|
||||||
|
is_stream=is_stream,
|
||||||
response_time_ms=response_time_ms, first_byte_time_ms=first_byte_time_ms,
|
response_time_ms=response_time_ms, first_byte_time_ms=first_byte_time_ms,
|
||||||
status_code=status_code, error_message=error_message, metadata=metadata,
|
status_code=status_code, error_message=error_message, metadata=metadata,
|
||||||
request_headers=request_headers, request_body=request_body,
|
request_headers=request_headers, request_body=request_body,
|
||||||
@@ -849,6 +861,8 @@ class UsageService:
|
|||||||
cache_read_input_tokens: int = 0,
|
cache_read_input_tokens: int = 0,
|
||||||
request_type: str = "chat",
|
request_type: str = "chat",
|
||||||
api_format: Optional[str] = None,
|
api_format: Optional[str] = None,
|
||||||
|
endpoint_api_format: Optional[str] = None,
|
||||||
|
has_format_conversion: bool = False,
|
||||||
is_stream: bool = False,
|
is_stream: bool = False,
|
||||||
response_time_ms: Optional[int] = None,
|
response_time_ms: Optional[int] = None,
|
||||||
first_byte_time_ms: Optional[int] = None,
|
first_byte_time_ms: Optional[int] = None,
|
||||||
@@ -889,7 +903,9 @@ class UsageService:
|
|||||||
input_tokens=input_tokens, output_tokens=output_tokens,
|
input_tokens=input_tokens, output_tokens=output_tokens,
|
||||||
cache_creation_input_tokens=cache_creation_input_tokens,
|
cache_creation_input_tokens=cache_creation_input_tokens,
|
||||||
cache_read_input_tokens=cache_read_input_tokens,
|
cache_read_input_tokens=cache_read_input_tokens,
|
||||||
request_type=request_type, api_format=api_format, is_stream=is_stream,
|
request_type=request_type, api_format=api_format,
|
||||||
|
endpoint_api_format=endpoint_api_format, has_format_conversion=has_format_conversion,
|
||||||
|
is_stream=is_stream,
|
||||||
response_time_ms=response_time_ms, first_byte_time_ms=first_byte_time_ms,
|
response_time_ms=response_time_ms, first_byte_time_ms=first_byte_time_ms,
|
||||||
status_code=status_code, error_message=error_message, metadata=metadata,
|
status_code=status_code, error_message=error_message, metadata=metadata,
|
||||||
request_headers=request_headers, request_body=request_body,
|
request_headers=request_headers, request_body=request_body,
|
||||||
@@ -1486,6 +1502,8 @@ class UsageService:
|
|||||||
provider_endpoint_id: Optional[str] = None,
|
provider_endpoint_id: Optional[str] = None,
|
||||||
provider_api_key_id: Optional[str] = None,
|
provider_api_key_id: Optional[str] = None,
|
||||||
api_format: Optional[str] = None,
|
api_format: Optional[str] = None,
|
||||||
|
endpoint_api_format: Optional[str] = None,
|
||||||
|
has_format_conversion: Optional[bool] = None,
|
||||||
) -> Optional[Usage]:
|
) -> Optional[Usage]:
|
||||||
"""
|
"""
|
||||||
快速更新使用记录状态
|
快速更新使用记录状态
|
||||||
@@ -1502,6 +1520,8 @@ class UsageService:
|
|||||||
provider_endpoint_id: Endpoint ID(可选,streaming 状态时更新)
|
provider_endpoint_id: Endpoint ID(可选,streaming 状态时更新)
|
||||||
provider_api_key_id: Provider API Key ID(可选,streaming 状态时更新)
|
provider_api_key_id: Provider API Key ID(可选,streaming 状态时更新)
|
||||||
api_format: API 格式(可选,用于获取按格式配置的倍率)
|
api_format: API 格式(可选,用于获取按格式配置的倍率)
|
||||||
|
endpoint_api_format: 端点原生 API 格式(可选)
|
||||||
|
has_format_conversion: 是否发生了格式转换(可选)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
更新后的 Usage 记录,如果未找到则返回 None
|
更新后的 Usage 记录,如果未找到则返回 None
|
||||||
@@ -1540,6 +1560,10 @@ class UsageService:
|
|||||||
)
|
)
|
||||||
if rate_multiplier is not None:
|
if rate_multiplier is not None:
|
||||||
usage.rate_multiplier = rate_multiplier
|
usage.rate_multiplier = rate_multiplier
|
||||||
|
if endpoint_api_format is not None:
|
||||||
|
usage.endpoint_api_format = endpoint_api_format
|
||||||
|
if has_format_conversion is not None:
|
||||||
|
usage.has_format_conversion = has_format_conversion
|
||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
|
|||||||
@@ -41,6 +41,9 @@ class StreamUsageTracker:
|
|||||||
provider_api_key_id: Optional[str] = None,
|
provider_api_key_id: Optional[str] = None,
|
||||||
# API 格式(用于选择正确的响应解析器)
|
# API 格式(用于选择正确的响应解析器)
|
||||||
api_format: Optional[str] = None,
|
api_format: Optional[str] = None,
|
||||||
|
# 格式转换信息
|
||||||
|
endpoint_api_format: Optional[str] = None,
|
||||||
|
has_format_conversion: bool = False,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
初始化流式用量跟踪器
|
初始化流式用量跟踪器
|
||||||
@@ -60,6 +63,8 @@ class StreamUsageTracker:
|
|||||||
provider_endpoint_id: Endpoint ID(用于记录真实成本)
|
provider_endpoint_id: Endpoint ID(用于记录真实成本)
|
||||||
provider_api_key_id: API Key ID(用于记录真实成本)
|
provider_api_key_id: API Key ID(用于记录真实成本)
|
||||||
api_format: API 格式(CLAUDE, CLAUDE_CLI, OPENAI, OPENAI_CLI)
|
api_format: API 格式(CLAUDE, CLAUDE_CLI, OPENAI, OPENAI_CLI)
|
||||||
|
endpoint_api_format: 端点原生 API 格式
|
||||||
|
has_format_conversion: 是否发生了格式转换
|
||||||
"""
|
"""
|
||||||
self.db = db
|
self.db = db
|
||||||
# 只存储ID,避免会话绑定问题
|
# 只存储ID,避免会话绑定问题
|
||||||
@@ -79,6 +84,8 @@ class StreamUsageTracker:
|
|||||||
|
|
||||||
# API 格式和响应解析器
|
# API 格式和响应解析器
|
||||||
self.api_format = api_format or "CLAUDE"
|
self.api_format = api_format or "CLAUDE"
|
||||||
|
self.endpoint_api_format = endpoint_api_format
|
||||||
|
self.has_format_conversion = has_format_conversion
|
||||||
self.response_parser = get_parser_for_format(self.api_format)
|
self.response_parser = get_parser_for_format(self.api_format)
|
||||||
self.stream_stats = StreamStats() # 解析器统计信息
|
self.stream_stats = StreamStats() # 解析器统计信息
|
||||||
|
|
||||||
@@ -488,6 +495,8 @@ class StreamUsageTracker:
|
|||||||
provider_endpoint_id=self.provider_endpoint_id,
|
provider_endpoint_id=self.provider_endpoint_id,
|
||||||
provider_api_key_id=self.provider_api_key_id,
|
provider_api_key_id=self.provider_api_key_id,
|
||||||
api_format=self.api_format,
|
api_format=self.api_format,
|
||||||
|
endpoint_api_format=self.endpoint_api_format,
|
||||||
|
has_format_conversion=self.has_format_conversion,
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"更新使用记录状态为 streaming 失败: {e}")
|
logger.warning(f"更新使用记录状态为 streaming 失败: {e}")
|
||||||
@@ -710,6 +719,8 @@ class StreamUsageTracker:
|
|||||||
cache_read_input_tokens=self.cache_read_input_tokens,
|
cache_read_input_tokens=self.cache_read_input_tokens,
|
||||||
request_type="chat",
|
request_type="chat",
|
||||||
api_format=self.api_format,
|
api_format=self.api_format,
|
||||||
|
endpoint_api_format=self.endpoint_api_format,
|
||||||
|
has_format_conversion=self.has_format_conversion,
|
||||||
is_stream=True,
|
is_stream=True,
|
||||||
response_time_ms=response_time_ms,
|
response_time_ms=response_time_ms,
|
||||||
status_code=self.status_code, # 使用实际的状态码
|
status_code=self.status_code, # 使用实际的状态码
|
||||||
@@ -801,6 +812,9 @@ class EnhancedStreamUsageTracker(StreamUsageTracker):
|
|||||||
provider_api_key_id: Optional[str] = None,
|
provider_api_key_id: Optional[str] = None,
|
||||||
# API 格式(用于选择正确的响应解析器)
|
# API 格式(用于选择正确的响应解析器)
|
||||||
api_format: Optional[str] = None,
|
api_format: Optional[str] = None,
|
||||||
|
# 格式转换信息
|
||||||
|
endpoint_api_format: Optional[str] = None,
|
||||||
|
has_format_conversion: bool = False,
|
||||||
):
|
):
|
||||||
super().__init__(
|
super().__init__(
|
||||||
db,
|
db,
|
||||||
@@ -817,6 +831,8 @@ class EnhancedStreamUsageTracker(StreamUsageTracker):
|
|||||||
provider_endpoint_id,
|
provider_endpoint_id,
|
||||||
provider_api_key_id,
|
provider_api_key_id,
|
||||||
api_format,
|
api_format,
|
||||||
|
endpoint_api_format,
|
||||||
|
has_format_conversion,
|
||||||
)
|
)
|
||||||
# 用于更准确的token计算
|
# 用于更准确的token计算
|
||||||
self._init_tokenizer()
|
self._init_tokenizer()
|
||||||
@@ -950,6 +966,8 @@ class EnhancedStreamUsageTracker(StreamUsageTracker):
|
|||||||
provider_endpoint_id=self.provider_endpoint_id,
|
provider_endpoint_id=self.provider_endpoint_id,
|
||||||
provider_api_key_id=self.provider_api_key_id,
|
provider_api_key_id=self.provider_api_key_id,
|
||||||
api_format=self.api_format,
|
api_format=self.api_format,
|
||||||
|
endpoint_api_format=self.endpoint_api_format,
|
||||||
|
has_format_conversion=self.has_format_conversion,
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"更新使用记录状态为 streaming 失败: {e}")
|
logger.warning(f"更新使用记录状态为 streaming 失败: {e}")
|
||||||
@@ -1054,6 +1072,9 @@ def create_stream_tracker(
|
|||||||
provider_api_key_id: Optional[str] = None,
|
provider_api_key_id: Optional[str] = None,
|
||||||
# API 格式(用于选择正确的响应解析器)
|
# API 格式(用于选择正确的响应解析器)
|
||||||
api_format: Optional[str] = None,
|
api_format: Optional[str] = None,
|
||||||
|
# 格式转换信息
|
||||||
|
endpoint_api_format: Optional[str] = None,
|
||||||
|
has_format_conversion: bool = False,
|
||||||
) -> StreamUsageTracker:
|
) -> StreamUsageTracker:
|
||||||
"""
|
"""
|
||||||
创建流式用量跟踪器
|
创建流式用量跟踪器
|
||||||
@@ -1074,6 +1095,8 @@ def create_stream_tracker(
|
|||||||
provider_endpoint_id: Endpoint ID(用于记录真实成本)
|
provider_endpoint_id: Endpoint ID(用于记录真实成本)
|
||||||
provider_api_key_id: API Key ID(用于记录真实成本)
|
provider_api_key_id: API Key ID(用于记录真实成本)
|
||||||
api_format: API 格式(CLAUDE, CLAUDE_CLI, OPENAI, OPENAI_CLI)
|
api_format: API 格式(CLAUDE, CLAUDE_CLI, OPENAI, OPENAI_CLI)
|
||||||
|
endpoint_api_format: 端点原生 API 格式
|
||||||
|
has_format_conversion: 是否发生了格式转换
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
流式用量跟踪器实例
|
流式用量跟踪器实例
|
||||||
@@ -1094,6 +1117,8 @@ def create_stream_tracker(
|
|||||||
provider_endpoint_id,
|
provider_endpoint_id,
|
||||||
provider_api_key_id,
|
provider_api_key_id,
|
||||||
api_format,
|
api_format,
|
||||||
|
endpoint_api_format,
|
||||||
|
has_format_conversion,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
return StreamUsageTracker(
|
return StreamUsageTracker(
|
||||||
@@ -1111,4 +1136,6 @@ def create_stream_tracker(
|
|||||||
provider_endpoint_id,
|
provider_endpoint_id,
|
||||||
provider_api_key_id,
|
provider_api_key_id,
|
||||||
api_format,
|
api_format,
|
||||||
|
endpoint_api_format,
|
||||||
|
has_format_conversion,
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user